//===- RankReductionPatterns.cpp - Patterns related to rank reductions ----===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "mlir/Dialect/Affine/IR/AffineOps.h" #include "mlir/Dialect/Arith/Utils/Utils.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/Dialect/Tensor/Transforms/Transforms.h" #include "mlir/IR/PatternMatch.h" #include "mlir/Interfaces/ValueBoundsOpInterface.h" #include "llvm/Support/Debug.h" #include "llvm/Support/LogicalResult.h" using namespace mlir; using namespace mlir::tensor; namespace { /// Fold expand_shape(extract_slice) ops that cancel itself out. struct FoldExpandOfRankReducingExtract : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(ExpandShapeOp expandShapeOp, PatternRewriter &rewriter) const override { RankedTensorType resultType = expandShapeOp.getResultType(); auto extractSliceOp = expandShapeOp.getSrc().getDefiningOp(); if (!extractSliceOp) return failure(); RankedTensorType srcType = extractSliceOp.getSourceType(); // Only cases where the ExpandShapeOp can be folded away entirely are // supported. Moreover, only simple cases where the resulting ExtractSliceOp // has no rank-reduction anymore are supported at the moment. RankedTensorType nonReducingExtractType = ExtractSliceOp::inferResultType( srcType, extractSliceOp.getStaticOffsets(), extractSliceOp.getStaticSizes(), extractSliceOp.getStaticStrides()); if (nonReducingExtractType != resultType) return failure(); SmallVector mixedOffsets = extractSliceOp.getMixedOffsets(); SmallVector mixedSizes = extractSliceOp.getMixedSizes(); SmallVector mixedStrides = extractSliceOp.getMixedStrides(); rewriter.replaceOpWithNewOp( expandShapeOp, extractSliceOp.getSource(), mixedOffsets, mixedSizes, mixedStrides); return success(); } }; /// Fold collapse_shape which only removes static dimensions of size `1` /// into extract_slice. struct FoldUnPaddingCollapseIntoExtract : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(tensor::CollapseShapeOp collapseShapeOp, PatternRewriter &rewriter) const override { auto extractSliceOp = collapseShapeOp.getSrc().getDefiningOp(); // Collapse cannot be folded away with multiple users of the extract slice // and it is not necessarily beneficial to only convert the collapse into // another extract slice. if (!extractSliceOp || !extractSliceOp->hasOneUse()) return failure(); // Only fold away simple collapse where all removed dimensions have static // size `1`. SliceVerificationResult res = isRankReducedType( collapseShapeOp.getSrcType(), collapseShapeOp.getResultType()); if (res != SliceVerificationResult::Success) return rewriter.notifyMatchFailure(collapseShapeOp, "expected unpadding collapse"); Value unPaddedExtractSlice = rewriter.create( extractSliceOp.getLoc(), collapseShapeOp.getResultType(), extractSliceOp.getSource(), extractSliceOp.getMixedOffsets(), extractSliceOp.getMixedSizes(), extractSliceOp.getMixedStrides()); rewriter.replaceOp(collapseShapeOp, unPaddedExtractSlice); return success(); } }; /// Fold insert_slice(collapse_shape) ops that cancel itself out. template struct FoldInsertOfRankReducingInsert : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(OpTy insertSliceOp, PatternRewriter &rewriter) const override { auto collapseShapeOp = insertSliceOp.getSource().template getDefiningOp(); if (!collapseShapeOp) return failure(); RankedTensorType srcType = collapseShapeOp.getSrcType(); // Only cases where the CollapseShapeOp can be folded away entirely are // supported. Moreover, only simple cases where the resulting InsertSliceOp // has no rank-reduction anymore are supported at the moment. RankedTensorType nonReducingInsertType = RankedTensorType::get(insertSliceOp.getStaticSizes(), insertSliceOp.getDestType().getElementType()); if (nonReducingInsertType != srcType) return failure(); SmallVector mixedOffsets = insertSliceOp.getMixedOffsets(); SmallVector mixedSizes = insertSliceOp.getMixedSizes(); SmallVector mixedStrides = insertSliceOp.getMixedStrides(); rewriter.replaceOpWithNewOp(insertSliceOp, collapseShapeOp.getSrc(), insertSliceOp.getDest(), mixedOffsets, mixedSizes, mixedStrides); return success(); } }; /// Fold expand_shape which only adds static dimensions of size `1` /// into insert_slice. template struct FoldPaddingExpandIntoInsert : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(OpTy insertSliceOp, PatternRewriter &rewriter) const override { auto expandShapeOp = insertSliceOp.getSource() .template getDefiningOp(); if (!expandShapeOp) return failure(); // Only fold away simple expansion where all added dimensions have static // size `1`. SliceVerificationResult res = isRankReducedType( expandShapeOp.getResultType(), expandShapeOp.getSrcType()); if (res != SliceVerificationResult::Success) return rewriter.notifyMatchFailure(insertSliceOp, "expected rank increasing expansion"); rewriter.modifyOpInPlace(insertSliceOp, [&]() { insertSliceOp.getSourceMutable().assign(expandShapeOp.getSrc()); }); return success(); } }; /// Pattern to bubble up a tensor.expand_shape op through a producer /// tensor.collapse_shape op that has non intersecting reassociations. struct BubbleUpExpandThroughParallelCollapse : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(tensor::ExpandShapeOp expandOp, PatternRewriter &rewriter) const override { auto collapseOp = expandOp.getSrc().getDefiningOp(); if (!collapseOp) return failure(); auto expandReInds = expandOp.getReassociationIndices(); auto collapseReInds = collapseOp.getReassociationIndices(); // Special case where the collapsed tensor to expand is a 0-D tensor, // then the reassociation maps will be empty and not produce valid results. if (expandReInds.size() == 0) { return failure(); } // Reshapes are parallel to each other if none of the reassociation indices // have greater than 1 index for both reshapes. for (auto [expandReassociation, collapseReassociation] : llvm::zip_equal(expandReInds, collapseReInds)) { if (collapseReassociation.size() != 1 && expandReassociation.size() != 1) return failure(); } // Compute new reassociation indices and expanded/collaped shapes. SmallVector newExpandReInds, newCollapseReInds; Location loc = expandOp->getLoc(); SmallVector collapseSizes = tensor::getMixedSizes(rewriter, loc, collapseOp.getSrc()); SmallVector expandSizes(getMixedValues( expandOp.getStaticOutputShape(), expandOp.getOutputShape(), rewriter)); SmallVector newExpandSizes; int64_t index = 0, expandIndex = 0, collapseIndex = 0; for (auto [idx, collapseReassociation] : llvm::enumerate(collapseReInds)) { if (collapseReassociation.size() != 1) { ReassociationIndices newCollapseReassociation; for (size_t i = 0; i < collapseReassociation.size(); ++i) { newCollapseReassociation.push_back(index); newExpandReInds.push_back({index++}); newExpandSizes.push_back(collapseSizes[collapseIndex++]); } newCollapseReInds.push_back(newCollapseReassociation); expandIndex++; continue; } ReassociationIndices newExpandReassociation; auto expandReassociation = expandReInds[idx]; for (size_t i = 0; i < expandReassociation.size(); ++i) { newExpandReassociation.push_back(index); newCollapseReInds.push_back({index++}); newExpandSizes.push_back(expandSizes[expandIndex++]); } newExpandReInds.push_back(newExpandReassociation); collapseIndex++; } // Swap reshape order. SmallVector dynamicSizes; SmallVector staticSizes; dispatchIndexOpFoldResults(newExpandSizes, dynamicSizes, staticSizes); auto expandResultType = expandOp.getResultType().clone(staticSizes); auto newExpand = rewriter.create( loc, expandResultType, collapseOp.getSrc(), newExpandReInds, newExpandSizes); rewriter.replaceOpWithNewOp( expandOp, newExpand.getResult(), newCollapseReInds); return success(); } }; /// Converts `tensor.extract_slice(tensor.expand_shape)` to /// `tensor.expand_shape(tensor.extract_slice)`. /// /// For this transformation to be possible, the slice must be fully contiguous /// within each reassociation group of the expand_shape. A slice is defined as /// fully contiguous within a reassociation group if after flattening the /// reassociation group to a single 1D range, then the slice taken out of the /// group could be defined as a single contiguous subrange within that range. /// /// Rank reducing slices are not supported. /// /// Example: /// The transformation is possible because each reassociation group has a /// contiguous slice (i.e., [2x4->2x4], [2x8->1x5], [4x2x4->1x1x4]). /// ``` /// BEFORE: /// %reshape = tensor.expand_shape %in [[0, 1], [2, 3], [4, 5, 6]] /// tensor<8x16x32xf32> to tensor<2x4x2x8x4x2x4xf32> /// %slice = tensor.extract_slice %reshape ... /// tensor<2x4x2x8x4x2x4xf32> to tensor<2x4x1x5x1x1x4xf32> /// /// AFTER: /// %slice = tensor.extract_slice %in ... /// tensor<8x16x32xf32> to tensor<8x5x4xf32> /// %reshape = tensor.expand_shape %slice [[0, 1], [2, 3], [4, 5, 6]] /// tensor<8x5x4xf32> to tensor<2x4x1x5x1x1x4xf32> /// ``` /// /// Note - this pattern could be extended to be a swap pattern between /// `tensor.expand_shape` and `tensor.extract_slice`, but is currently /// implemented only as a bubble up pattern for `tensor.extract_slice`. struct BubbleUpExpandShapeThroughExtractSlice : public OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(tensor::ExtractSliceOp sliceOp, PatternRewriter &rewriter) const override { auto expandShapeOp = sliceOp.getSource().getDefiningOp(); if (checkPreconditionForBubbleUpExtractSlice(sliceOp, expandShapeOp, rewriter) .failed()) return failure(); // The tensor.extract_slice before applying the pattern works on the result // of the tensor.expand_shape, so variables (i.e. inputs for ExtractSliceOp) // referring to the state before applying the pattern are named with the // prefix "expanded", and ones referring to the state after applying the // pattern are named with the prefix "collapsed". SmallVector expandedOffsets = sliceOp.getMixedOffsets(); SmallVector expandedSizes = sliceOp.getMixedSizes(); SmallVector expandedShape = getMixedValues(expandShapeOp.getStaticOutputShape(), expandShapeOp.getOutputShape(), rewriter); // Helper variables and function for accumulating the size values. Location loc = expandShapeOp->getLoc(); AffineExpr d0, d1, d2; bindDims(rewriter.getContext(), d0, d1, d2); // Multiply two integers. auto mul = [&](OpFoldResult v1, OpFoldResult v2) { auto mulMap = AffineMap::get(2, 0, {d0 * d1}); return affine::makeComposedFoldedAffineApply(rewriter, loc, mulMap, {v1, v2}); }; // Compute new offsets, sizes, and strides for tensor.extract_slice. // The new tensor.extract_slice will work on a tensor that has has a rank of // ReassociationIndices.size(). In the loop a single offset, size, and // stride value is computed per reassociation group. SmallVector collapsedOffsets, collapsedSizes, collapsedStrides; for (const ReassociationIndices &indices : expandShapeOp.getReassociationIndices()) { // collapsedSize will hold the size of the single dim that represents the // reassociation group in the non expanded tensor. OpFoldResult collapsedSize = rewriter.getIndexAttr(1); // The reassocGroupSizes and reassocGroupOffsets are used to create an // affine.linearize_index op to linearize the single offset value required // for this reassociation group. SmallVector reassocGroupSizes, reassocGroupOffsets; for (long expandedDim : indices) { // reassocGroupSizes and reassocGroupOffsets can be obtained directly // from the expanded state, but the collapsed size requires calculation // as it did not previously exist. reassocGroupSizes.push_back(expandedShape[expandedDim]); reassocGroupOffsets.push_back(expandedOffsets[expandedDim]); collapsedSize = mul(collapsedSize, expandedSizes[expandedDim]); } SmallVector offsetVals = llvm::map_to_vector(reassocGroupOffsets, [&](OpFoldResult ofr) { return getValueOrCreateConstantIndexOp(rewriter, loc, ofr); }); OpFoldResult collapsedOffset = rewriter .create(loc, offsetVals, reassocGroupSizes, /*disjoint=*/true) .getResult(); collapsedOffsets.push_back(collapsedOffset); collapsedSizes.push_back(collapsedSize); // Only unit stride is supported. collapsedStrides.push_back(rewriter.getIndexAttr(1)); } // The shape of the result can be obtained from the sizes passed in. SmallVector dynDims; SmallVector shape; dispatchIndexOpFoldResults(expandedSizes, dynDims, shape); RankedTensorType resultType = RankedTensorType::get( shape, expandShapeOp.getResultType().getElementType()); // Create a new ExtractSliceOp and ExpandShapeOp. Value newSliceOp = rewriter.create( loc, expandShapeOp.getSrc(), collapsedOffsets, collapsedSizes, collapsedStrides); rewriter.replaceOpWithNewOp( sliceOp, resultType, newSliceOp, expandShapeOp.getReassociationIndices(), expandedSizes); return success(); } // Helper function to check if all the required conditions for the // tensor.extract_slice to be bubbled up through the tensor.expand_shape are // met. LogicalResult checkPreconditionForBubbleUpExtractSlice(tensor::ExtractSliceOp sliceOp, tensor::ExpandShapeOp expandShapeOp, PatternRewriter &rewriter) const { if (!expandShapeOp) { return rewriter.notifyMatchFailure( sliceOp, "tensor.extract_slice source not produced by expand_shape"); } if (!sliceOp.hasUnitStride()) { return rewriter.notifyMatchFailure( sliceOp, "unsupported: non-unit stride. Only contiguous slices can " "be supported in this transformation."); } SmallVector offsets = sliceOp.getMixedOffsets(); SmallVector sizes = sliceOp.getMixedSizes(); if (static_cast(sliceOp.getResultType().getRank()) != sizes.size()) { return rewriter.notifyMatchFailure(sliceOp, "unimplemented: rank reducing slice"); } SmallVector outputShape = getMixedValues(expandShapeOp.getStaticOutputShape(), expandShapeOp.getOutputShape(), rewriter); std::function isZeroOffsetAndFullSize = [](OpFoldResult offset, OpFoldResult sliceSize, OpFoldResult size) { if (!isConstantIntValue(offset, 0)) return false; FailureOr maybeEqual = ValueBoundsConstraintSet::areEqual(sliceSize, size); return llvm::succeeded(maybeEqual) && maybeEqual.value(); }; // Check that the slice is contiguous within each reassociation group. // The slice is contiguous only if after the first dimension where a non // unit slice is taken, the slice size on all subsequent dimensions of the // group is equal to the entire size of the dimension. // Examples of contiguous slices: // full sizes: [8, 8, 10] slice offsets: [0, 0, 0] slice sizes: [1, 1, 10] // full sizes: [5, 10] slice offsets: [3, 0] slice sizes: [2, 10] // Examples of non contiguous slices: // full sizes: [8, 8, 10] slice offsets: [0, 0, 0] slice sizes: [1, 2, 5] // full sizes: [5, 10] slice offsets: [0, 4] slice sizes: [2, 5] for (const ReassociationIndices &indices : expandShapeOp.getReassociationIndices()) { int64_t i = 0; int64_t e = indices.size(); // Find the first expanded dim after the first dim with non-unit extracted // size. for (; i < e; ++i) { if (!isConstantIntValue(sizes[indices[i]], 1)) { // +1 to skip the first non-unit size dim. i++; break; } } // Verify that all subsequent dimensions extract the full size of the // source tensor. for (; i < e; ++i) { int64_t expandedDim = indices[i]; if (!isZeroOffsetAndFullSize(offsets[expandedDim], sizes[expandedDim], outputShape[expandedDim])) { return rewriter.notifyMatchFailure( sliceOp, "Not a contiguous slice of the expanded tensor."); } } } return success(); } }; } // namespace void mlir::tensor::populateReassociativeReshapeFoldingPatterns( RewritePatternSet &patterns) { patterns .add, FoldInsertOfRankReducingInsert, FoldPaddingExpandIntoInsert, FoldPaddingExpandIntoInsert>( patterns.getContext()); } void mlir::tensor::populateBubbleUpExpandShapePatterns( RewritePatternSet &patterns) { patterns.add(patterns.getContext()); } void mlir::tensor::populateBubbleUpExtractSliceOpPatterns( RewritePatternSet &patterns) { patterns.add(patterns.getContext()); }