| //===-ElideReinterpretCast.cpp - Expansion patterns for MemRef operations-===// |
| // |
| // 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/Arith/IR/Arith.h" |
| #include "mlir/Dialect/Arith/Transforms/Passes.h" |
| #include "mlir/Dialect/Arith/Utils/Utils.h" |
| #include "mlir/Dialect/MemRef/IR/MemRef.h" |
| #include "mlir/Dialect/MemRef/Transforms/Transforms.h" |
| #include "mlir/Dialect/SCF/IR/SCF.h" |
| #include "mlir/Dialect/Utils/IndexingUtils.h" |
| #include "mlir/IR/Matchers.h" |
| #include "mlir/IR/TypeUtilities.h" |
| #include "mlir/Transforms/DialectConversion.h" |
| #include <array> |
| #include <cassert> |
| #include <optional> |
| |
| namespace mlir { |
| namespace memref { |
| #define GEN_PASS_DEF_ELIDEREINTERPRETCASTPASS |
| #include "mlir/Dialect/MemRef/Transforms/Passes.h.inc" |
| } // namespace memref |
| } // namespace mlir |
| |
| using namespace mlir; |
| |
| namespace { |
| |
| //===----------------------------------------------------------------------===// |
| // Copy Rewrite Helpers |
| //===----------------------------------------------------------------------===// |
| |
| /// Copy-relevant information derived from a reinterpret_cast. |
| struct ResultNonUnitDimsAndOffsetsForRC { |
| // Non-unit dimensions of the reinterpret_cast result. |
| SmallVector<unsigned> nonUnitDimsPos; |
| // Delinearized offsets to in-bounds reinterpret_cast source indices. |
| // Optional since it is only supported for static offsets. |
| std::optional<SmallVector<int64_t>> delinearizedOffsets; |
| }; |
| |
| /// Returns delinearized offset indices for a static reinterpret_cast offset of |
| /// an identity-layout source. |
| static std::optional<SmallVector<int64_t>> |
| delinearizeStaticRCOffset(memref::ReinterpretCastOp rc) { |
| ArrayRef<int64_t> rcOffsets = rc.getStaticOffsets(); |
| MemRefType srcType = dyn_cast<MemRefType>(rc.getSource().getType()); |
| // FIXME: Despite what `getStaticOffsets` implies, `reinterpret_cast` takes |
| // only a single offset. That should be fixed at the op definition level. |
| assert(rcOffsets.size() == 1 && "Expecting single offset"); |
| |
| assert(ShapedType::isStatic(rcOffsets[0]) && "expected static offset"); |
| assert(rcOffsets[0] >= 0 && |
| "static reinterpret_cast offset must be non-negative"); |
| assert(srcType.getLayout().isIdentity() && |
| "Expecting identity source layout."); |
| if (srcType.getRank() == 0) { |
| assert(rcOffsets[0] == 0 && |
| "non-zero static offset is invalid for rank-0 source memref"); |
| return SmallVector<int64_t>{}; |
| } |
| |
| SmallVector<int64_t> offsetIdxs(srcType.getRank(), 0); |
| int64_t remainder = rcOffsets[0]; |
| SmallVector<int64_t> srcStrides = computeStrides(srcType.getShape()); |
| // Convert the scalar reinterpret_cast offset to per-dimension source starting |
| // indices. |
| for (auto [dim, stride] : llvm::enumerate(srcStrides)) { |
| offsetIdxs[dim] = remainder / stride; |
| assert(offsetIdxs[dim] < srcType.getDimSize(dim) && |
| "static reinterpret_cast offset must delinearize to in-bounds " |
| "source indices"); |
| remainder %= stride; |
| } |
| |
| assert(remainder == 0 && |
| "Assuming identity source layout, the trailing stride == 1 " |
| "so, the remainder should be 0 at the end of index calculation."); |
| return offsetIdxs; |
| } |
| |
| static bool hasExactlyOneTruncatedNonUnitDim(memref::ReinterpretCastOp rc) { |
| MemRefType srcType = dyn_cast<MemRefType>(rc.getSource().getType()); |
| MemRefType resType = dyn_cast<MemRefType>(rc.getType()); |
| assert(srcType.hasStaticShape() && resType.hasStaticShape() && |
| "expected static shapes"); |
| assert(srcType.getRank() == resType.getRank() && |
| "expected rank-preserving reinterpret_cast"); |
| |
| unsigned truncatedDims = 0; |
| |
| for (auto [srcSize, resSize] : |
| llvm::zip_equal(srcType.getShape(), resType.getShape())) { |
| if (srcSize == resSize) |
| continue; |
| |
| // Only one non-unit source dimension may be truncated. |
| if (srcSize != 1 && resSize < srcSize) { |
| ++truncatedDims; |
| continue; |
| } |
| |
| // The size change is not a supported single non-unit source dimension |
| // reduction. |
| return false; |
| } |
| |
| // Make sure there is only one truncated dimension. |
| return truncatedDims == 1; |
| } |
| |
| /// Returns the unique non-unit dim or nullopt if # non-unit-dims != 1. |
| static std::optional<unsigned> getSingleNonUnitDim(MemRefType type) { |
| assert(type.hasStaticShape() && "expected static shape"); |
| ArrayRef<int64_t> shape = type.getShape(); |
| |
| // Find all non-unit dims |
| auto nonUnitDims = llvm::make_filter_range( |
| llvm::enumerate(shape), [](auto it) { return it.value() != 1; }); |
| |
| // Expect single non-unit dims |
| if (llvm::range_size(nonUnitDims) != 1) |
| return std::nullopt; |
| |
| // Return the index of the unique non-unit dim. |
| return (*nonUnitDims.begin()).index(); |
| } |
| |
| /// Returns reinterpret_cast's result non-unit dimensions and, for static |
| /// offsets, delinearized offset. |
| /// |
| /// Supports ranked, static-shape, rank-preserving reinterpret_casts from |
| /// identity-layout sources. |
| /// * Scalar-shaped results may have arbitrary result strides. |
| /// * Non-scalar results must have static offsets, static result strides |
| /// identical to the source identity strides, and exactly one non-unit |
| /// source dimension size truncated. |
| /// |
| /// Returns nullopt for unsupported reinterpret_casts. |
| /// |
| /// Examples that return info: |
| /// |
| /// reinterpret_cast memref<1xMxNxf32, identity-layout> |
| /// to memref<1xMxKxf32, strided<[M*N, N, 1], offset: OFF>> |
| /// where K < N |
| /// |
| /// reinterpret_cast memref<1xMxf32, identity-layout> |
| /// to memref<1x1xf32, strided<[?, ?], offset: ?>> |
| /// |
| /// Examples that return no info: |
| /// |
| /// reinterpret_cast memref<1xMxNxf32, identity-layout> |
| /// to memref<1xMxKxf32, strided<[?, N, 1]>> |
| /// |
| /// reinterpret_cast memref<1xMxNxf32, identity-layout> |
| /// to memref<1xNxPxf32, strided<[M*N, N, 1], offset: OFF>> |
| /// where M != N && N != P |
| static std::optional<ResultNonUnitDimsAndOffsetsForRC> |
| getResultNonUnitDimsAndOffsetsForRC(memref::ReinterpretCastOp rc) { |
| MemRefType srcType = dyn_cast<MemRefType>(rc.getSource().getType()); |
| MemRefType resType = dyn_cast<MemRefType>(rc.getType()); |
| |
| // Ranked memref types are required to statically build load/store index |
| // lists. |
| if (!srcType || !resType) |
| return std::nullopt; |
| |
| // TODO: Support rank-modifying reinterpret_casts. |
| if (srcType.getRank() != resType.getRank()) |
| return std::nullopt; |
| |
| // TODO: Support dynamic shapes with mixed size operands as loop bounds. |
| if (!(srcType.hasStaticShape() && resType.hasStaticShape())) |
| return std::nullopt; |
| |
| // TODO: Support non-identity source layouts by computing source strides from |
| // the layout map. |
| if (!srcType.getLayout().isIdentity()) |
| return std::nullopt; |
| |
| ResultNonUnitDimsAndOffsetsForRC dimsAndOffs; |
| |
| // Track non-unit result dimensions; unit dimensions are |
| // always indexed at 0. |
| for (auto [dim, resultSize] : llvm::enumerate(resType.getShape())) { |
| if (resultSize != 1) |
| dimsAndOffs.nonUnitDimsPos.push_back(static_cast<unsigned>(dim)); |
| } |
| |
| ArrayRef<int64_t> rcOffsets = rc.getStaticOffsets(); |
| // FIXME: Despite what `getStaticOffsets` implies, `reinterpret_cast` takes |
| // only a single offset. That should be fixed at the op definition level. |
| assert(rcOffsets.size() == 1 && "Expecting single offset"); |
| |
| bool isScalarRes = |
| llvm::all_of(resType.getShape(), [](int64_t size) { return size == 1; }); |
| |
| bool isOffsetDynamic = ShapedType::isDynamic(rcOffsets[0]); |
| |
| // Cases with at least one non-unit dimension in reinterpret_cast's result are |
| // restricted to preserving all but one dimension from the source, which |
| // collapsed to `1` in the result, and fully static metadata. |
| if (!isScalarRes) { |
| if (isOffsetDynamic) |
| return std::nullopt; |
| |
| SmallVector<int64_t> srcIdentityStrides = |
| computeStrides(srcType.getShape()); |
| ArrayRef<int64_t> rcResultStrides = rc.getStaticStrides(); |
| |
| if (!llvm::all_of(llvm::zip_equal(srcIdentityStrides, rcResultStrides), |
| [](auto pair) { |
| auto [srcStride, resultStride] = pair; |
| return !ShapedType::isDynamic(resultStride) && |
| srcStride == resultStride; |
| })) |
| return std::nullopt; |
| |
| if (!hasExactlyOneTruncatedNonUnitDim(rc)) |
| return std::nullopt; |
| } |
| |
| // CASE 1: Dynamic ReinterpretCast offset. |
| // |
| // Dynamic offsets are supported only for effectively-1D to scalar |
| // reinterpret_casts. |
| if (isOffsetDynamic) { |
| // With an effectively-1D source, a dynamic offset can be mapped to its |
| // unique non-unit dim. For other cases, bail out. |
| if (llvm::count_if(srcType.getShape(), |
| [](int64_t size) { return size != 1; }) != 1) |
| return std::nullopt; |
| |
| return dimsAndOffs; |
| } |
| |
| // CASE 2: Static ReinterpretCast offset |
| // Delinearize static ReinterpretCast offset as in-bounds indices (one for |
| // every source dimension). |
| dimsAndOffs.delinearizedOffsets = delinearizeStaticRCOffset(rc); |
| |
| return dimsAndOffs; |
| } |
| |
| /// Rewrites supported copy operations through `memref.reinterpret_cast` to |
| /// scalar load/store operations. |
| /// |
| /// Supported cases: |
| /// 1. Scalar-shaped reinterpret_cast results. Result strides are ignored; |
| /// the store index is derived from the reinterpret_cast offset. |
| /// |
| /// 2. Non-scalar reinterpret_cast results that preserve all non-unit source |
| /// dimensions sizes except one. Result |
| /// strides must be static and identical to the identity strides of the |
| /// source, and the offset must be static. |
| /// |
| /// // BEFORE (scalar-shaped result) |
| /// %strided = memref.reinterpret_cast %dst |
| /// to offset: [OFF], sizes: [1, ..., 1], strides: [...] |
| /// memref.copy %src, %strided |
| /// |
| /// // AFTER |
| /// %v = memref.load %src[0, ..., 0] |
| /// memref.store %v, %dst[delinearized(OFF)] |
| /// |
| /// // BEFORE (one truncated non-unit dimension) |
| /// %strided = memref.reinterpret_cast %dst |
| /// to offset: [OFF], sizes: [1, M, K], strides: [M*N, N, 1] |
| /// : memref<1xMxNxf32> |
| /// to memref<1xMxKxf32, strided<[M*N, N, 1], offset: OFF>> |
| /// memref.copy %src, %strided |
| /// |
| /// // AFTER |
| /// // Assuming OFF delinearizes to [0, 0, DELIN_OFF]: |
| /// scf.for %i = 0 to M step 1 { |
| /// scf.for %k = 0 to K step 1 { |
| /// %v = memref.load %src[0, %i, %k] |
| /// memref.store %v, %dst[0, %i, DELIN_OFF + %k] |
| /// } |
| /// } |
| struct CopyToLoadAndStore : public OpRewritePattern<memref::CopyOp> { |
| public: |
| using OpRewritePattern::OpRewritePattern; |
| |
| LogicalResult matchAndRewrite(memref::CopyOp op, |
| PatternRewriter &rewriter) const final { |
| Value src = op.getSource(); |
| MemRefType cpSrcType = cast<MemRefType>(src.getType()); |
| if (!cpSrcType || !cpSrcType.hasStaticShape()) |
| return rewriter.notifyMatchFailure( |
| op, "only ranked, static copy sources are supported."); |
| |
| Value rcOutput = op.getTarget(); |
| auto rc = rcOutput.getDefiningOp<memref::ReinterpretCastOp>(); |
| if (!rc) |
| return rewriter.notifyMatchFailure( |
| op, "target is not a memref.reinterpret_cast"); |
| |
| std::optional<ResultNonUnitDimsAndOffsetsForRC> dimsAndOffs = |
| getResultNonUnitDimsAndOffsetsForRC(rc); |
| if (!dimsAndOffs) |
| return rewriter.notifyMatchFailure( |
| op, |
| "unsupported reinterpret_cast result dimensions, strides, or offset"); |
| |
| Location loc = op.getLoc(); |
| Value dst = rc.getSource(); |
| MemRefType dstType = cast<MemRefType>(dst.getType()); |
| MemRefType rcResType = cast<MemRefType>(rc.getType()); |
| |
| // Sanity check that the copy doesn't access strided MemRef out-of-bounds. |
| // Such cases should probably be rejected by Op verifier. |
| // FIXME: Add run-time verification for cases like this. |
| if (ShapedType::isStatic(rc.getStaticOffsets()[0]) && |
| llvm::any_of(llvm::enumerate(rcResType.getShape()), [&](auto it) { |
| unsigned dim = it.index(); |
| int64_t rcResultSize = it.value(); |
| return (*dimsAndOffs->delinearizedOffsets)[dim] + rcResultSize > |
| dstType.getDimSize(dim); |
| })) |
| return rewriter.notifyMatchFailure(op, "copy accesses are OOB"); |
| |
| // Constant Op cache to reuse common index constants across bounds, steps, |
| // and static offsets: 0 is stored at index 0 and 1 is stored at index 1. |
| std::array<Value, 2> cachedIndexConstants; |
| auto getOrCreateIndexConstant = [&](int64_t value) -> Value { |
| if (value == 0 || value == 1) { |
| Value &cached = cachedIndexConstants[value]; |
| if (!cached) |
| cached = arith::ConstantIndexOp::create(rewriter, loc, value); |
| return cached; |
| } |
| return arith::ConstantIndexOp::create(rewriter, loc, value); |
| }; |
| |
| auto getZeroIdxs = [&](int64_t rank) { |
| SmallVector<Value> idxs; |
| idxs.reserve(rank); |
| if (rank != 0) |
| idxs.append(rank, getOrCreateIndexConstant(0)); |
| return idxs; |
| }; |
| |
| // Create loop bounds before moving the insertion point into the loop nest, |
| // so loop-invariant constants are emitted outside the generated loops. |
| SmallVector<Value> upperBounds; |
| upperBounds.reserve(dimsAndOffs->nonUnitDimsPos.size()); |
| for (unsigned dim : dimsAndOffs->nonUnitDimsPos) { |
| upperBounds.push_back( |
| getOrCreateIndexConstant(rcResType.getDimSize(dim))); |
| } |
| |
| // All indices are initialised to zero. |
| SmallVector<Value> rcSrcStoreIdxs = getZeroIdxs(dstType.getRank()); |
| std::optional<unsigned> srcNonUnitDimPos; |
| if (dimsAndOffs->delinearizedOffsets) { |
| // Initialize store indices from the static reinterpret_cast offset, |
| // delinearized in function gating rewrite. |
| for (auto [idx, offset] : |
| llvm::enumerate(*dimsAndOffs->delinearizedOffsets)) { |
| if (offset == 0) |
| continue; |
| rcSrcStoreIdxs[idx] = getOrCreateIndexConstant(offset); |
| } |
| } else { |
| // Dynamic offset is used directly only for effectively-1D sources. |
| assert(dimsAndOffs->nonUnitDimsPos.size() <= 1 && |
| "Expecting at most one non-unit result dimension."); |
| |
| srcNonUnitDimPos = getSingleNonUnitDim(dstType); |
| assert(srcNonUnitDimPos && |
| "Expecting single non-unit dimension source to receive the " |
| "dynamic offset."); |
| |
| SmallVector<OpFoldResult> rcOffsets = rc.getMixedOffsets(); |
| // FIXME: Despite what `getMixedOffsets` implies, `reinterpret_cast` takes |
| // only a single offset. That should be fixed at the op definition level. |
| assert(rcOffsets.size() == 1 && "Expecting single offset"); |
| // Only the index corresponding to the single non-unit dim is updated. |
| rcSrcStoreIdxs[*srcNonUnitDimPos] = |
| getValueOrCreateConstantIndexOp(rewriter, loc, rcOffsets[0]); |
| } |
| |
| // Create the loop nest and emit the load/store at the innermost insertion |
| // point. |
| { |
| OpBuilder::InsertionGuard guard(rewriter); |
| |
| SmallVector<Value> loadIdxs = getZeroIdxs(cpSrcType.getRank()); |
| SmallVector<Value> storeIdxs(rcSrcStoreIdxs); |
| |
| if (!dimsAndOffs->nonUnitDimsPos.empty()) { |
| Value lowerBound = getOrCreateIndexConstant(0); |
| Value step = getOrCreateIndexConstant(1); |
| |
| // Build one nested loop per non-unit reinterpret_cast result dimension. |
| for (auto [loopIndex, dim] : |
| llvm::enumerate(dimsAndOffs->nonUnitDimsPos)) { |
| scf::ForOp loop = scf::ForOp::create(rewriter, loc, lowerBound, |
| upperBounds[loopIndex], step); |
| |
| rewriter.setInsertionPointToStart(loop.getBody()); |
| |
| Value iv = loop.getInductionVar(); |
| // Since result strides match source identity strides dimension-wise, |
| // each IV indexes the same dimension in both the copy source and rc |
| // source. |
| loadIdxs[dim] = iv; |
| |
| if (storeIdxs[dim] == getOrCreateIndexConstant(0)) { |
| storeIdxs[dim] = iv; |
| } else { |
| storeIdxs[dim] = |
| arith::AddIOp::create(rewriter, loc, storeIdxs[dim], iv); |
| } |
| } |
| } |
| |
| // Emit the scalar load/store at the innermost loop body, or directly at |
| // the original copy location for scalar copies. |
| Value val = memref::LoadOp::create(rewriter, loc, src, loadIdxs); |
| memref::StoreOp::create(rewriter, loc, val, dst, storeIdxs); |
| } |
| |
| // If the only user of `rc` is the current Op (which is about to be erased), |
| // we can safely erase it. |
| bool eraseRc = rcOutput.hasOneUse(); |
| rewriter.eraseOp(op); |
| if (eraseRc) |
| rewriter.eraseOp(rc); |
| return success(); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // Load Rewrite Helpers |
| //===----------------------------------------------------------------------===// |
| |
| static bool hasStaticZeroOffset(memref::ReinterpretCastOp rc) { |
| ArrayRef<int64_t> offsets = rc.getStaticOffsets(); |
| // FIXME: Despite what `getStaticOffsets` implies, `reinterpret_cast` takes |
| // only a single offset. That should be fixed at the op definition level. |
| assert(offsets.size() == 1 && "Expecting single offset"); |
| return !ShapedType::isDynamic(offsets[0]) && offsets[0] == 0; |
| } |
| |
| static std::optional<int64_t> getConstantIndex(Value v) { |
| if (auto cst = v.getDefiningOp<arith::ConstantIndexOp>()) |
| return cst.value(); |
| // Non-constant and dynamic indices |
| return std::nullopt; |
| } |
| |
| /// Return true if input index is in bounds, i.e. `0 <= idx < upperBound`. |
| /// Fully dynamic index values (i.e. non-constant) that cannot be analysed are |
| /// treated as in-bounds. |
| static bool isConstantIndexExplicitlyOutOfBounds(Value idx, |
| int64_t upperBound) { |
| // Only statically known `arith.constant` indices are checked here. |
| std::optional<int64_t> idxVal = getConstantIndex(idx); |
| return idxVal && (*idxVal < 0 || *idxVal >= upperBound); |
| } |
| |
| using NonUnitDimMapping = SmallVector<std::pair<int64_t, int64_t>>; |
| |
| /// Shape restriction accepting only unit-dim insertion/removal |
| /// reinterpret_casts. |
| /// |
| /// Examples accepted: |
| /// memref<1x1x1x108xf32> <-> memref<1x108xf32> |
| /// memref<100x1xf32> <-> memref<100x1x1xf32> |
| /// memref<1x33x40xf32> <-> memref<33x1x1x40xf32> |
| /// memref<1> <-> memref<1x1x1> |
| /// |
| /// Returns the mapping of non-unit dimensions from the source |
| /// to the result MemRef if the reinterpret_cast preserved sizes and order (no |
| /// transposition) of these dimensions. |
| static std::optional<NonUnitDimMapping> |
| getNonUnitDimMapping(memref::ReinterpretCastOp rc) { |
| auto inputTy = cast<MemRefType>(rc.getSource().getType()); |
| auto outputTy = cast<MemRefType>(rc.getResult().getType()); |
| |
| // Only zero, statically known offsets are accepted. Non-zero or dynamic |
| // offsets would require reasoning about storage shifts in the underlying |
| // reinterpret_cast, which this helper does not model. |
| if (!hasStaticZeroOffset(rc)) |
| return std::nullopt; |
| |
| // Dynamic sizes/strides prevent precise reasoning about the underlying |
| // reinterpret_cast, so only fully static shape metadata is accepted. |
| if (llvm::any_of(rc.getStaticSizes(), ShapedType::isDynamic) || |
| llvm::any_of(rc.getStaticStrides(), ShapedType::isDynamic)) |
| return std::nullopt; |
| |
| ArrayRef<int64_t> inputShape = inputTy.getShape(); |
| ArrayRef<int64_t> outputShape = outputTy.getShape(); |
| int64_t inputDim = 0; |
| int64_t outputDim = 0; |
| int64_t inputRank = inputTy.getRank(); |
| int64_t outputRank = outputTy.getRank(); |
| NonUnitDimMapping mapping; |
| |
| // The preserved non-unit dimensions must have the same static sizes and |
| // appear in the same order. |
| while (inputDim < inputRank || outputDim < outputRank) { |
| if (inputDim < inputRank && inputShape[inputDim] == 1) { |
| ++inputDim; |
| continue; |
| } |
| if (outputDim < outputRank && outputShape[outputDim] == 1) { |
| ++outputDim; |
| continue; |
| } |
| |
| if (inputDim == inputRank || outputDim == outputRank) |
| return std::nullopt; |
| |
| if (ShapedType::isDynamic(inputShape[inputDim]) || |
| ShapedType::isDynamic(outputShape[outputDim]) || |
| inputShape[inputDim] != outputShape[outputDim]) |
| return std::nullopt; |
| |
| mapping.push_back({inputDim, outputDim}); |
| ++inputDim; |
| ++outputDim; |
| } |
| return mapping; |
| } |
| |
| /// Checks statically known and constant indices accessed by a load from a |
| /// unit-dim insertion/removal reinterpret_cast to ensure in-bounds only access. |
| /// Fully dynamic indices are skipped (there is no way to verify them). |
| [[maybe_unused]] static bool areIndicesInBounds(memref::LoadOp load) { |
| auto rc = load.getMemRef().getDefiningOp<memref::ReinterpretCastOp>(); |
| auto rcOutputTy = cast<MemRefType>(rc.getResult().getType()); |
| |
| for (auto [pos, idx] : llvm::enumerate(load.getIndices())) { |
| // FIXME: This should be ensured by the memref.load semantics. |
| // In the long term, this sanity-check may live in the same debug-only |
| // checks as `MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS`. This rejects |
| // only explicit constant OOB indices. Dynamic/non-constant indices are not |
| // filtered here. |
| if (isConstantIndexExplicitlyOutOfBounds(idx, rcOutputTy.getDimSize(pos))) |
| return false; |
| } |
| return true; |
| } |
| |
| /// Rewrites `memref.load` through a reinterpret_cast that only inserts/removes |
| /// unit dimensions by mapping the load indices directly onto the source MemRef. |
| /// |
| /// Shape restriction gated by getNonUnitDimMapping(). |
| /// |
| /// BEFORE (rank expansion) |
| /// %view = memref.reinterpret_cast %src |
| /// : memref<1xNxMxf32> to memref<Nx1x1xMxf32> |
| /// %v = memref.load %view[%i, %c0, %c0, %j] : memref<Nx1x1xMxf32> |
| /// |
| /// AFTER |
| /// %v = memref.load %src[%c0, %i, %j] : memref<1xNxMxf32> |
| /// |
| /// BEFORE (rank collapsing) |
| /// %view = memref.reinterpret_cast %src |
| /// : memref<Nx1x1xMxf32> to memref<1xNxMxf32> |
| /// %v = memref.load %view[%c0, %i, %j] : memref<1xNxMxf32> |
| /// |
| /// AFTER |
| /// %v = memref.load %src[%i, %c0, %c0, %j] : memref<Nx1x1xMxf32> |
| struct RewriteLoadFromReinterpretCast |
| : public OpRewritePattern<memref::LoadOp> { |
| public: |
| using OpRewritePattern::OpRewritePattern; |
| |
| LogicalResult matchAndRewrite(memref::LoadOp op, |
| PatternRewriter &rewriter) const override { |
| auto rc = op.getMemRef().getDefiningOp<memref::ReinterpretCastOp>(); |
| if (!rc) |
| return rewriter.notifyMatchFailure( |
| op, "target is not a memref.reinterpret_cast"); |
| std::optional<NonUnitDimMapping> dimMapping = getNonUnitDimMapping(rc); |
| if (!dimMapping) |
| return rewriter.notifyMatchFailure( |
| op, "reinterpret_cast is not a unit-dim insertion/removal preserving " |
| "non-unit dimensions"); |
| |
| assert(areIndicesInBounds(op) && |
| "load from reinterpret_cast indexes out of bounds!"); |
| |
| auto rcInputTy = cast<MemRefType>(rc.getSource().getType()); |
| |
| int64_t rcInputRank = rcInputTy.getRank(); |
| |
| SmallVector<Value> oldIdxs(op.getIndices().begin(), op.getIndices().end()); |
| |
| // Prefer reusing an explicit constant-zero index from the old load. |
| Value zeroIndex; |
| for (Value idx : oldIdxs) { |
| std::optional<int64_t> idxVal = getConstantIndex(idx); |
| if (idxVal && *idxVal == 0) { |
| zeroIndex = idx; |
| break; |
| } |
| } |
| if (!zeroIndex) |
| zeroIndex = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0); |
| |
| // Initialize new load indices to all 0s. |
| SmallVector<Value> rcInputIdxs(rcInputRank, zeroIndex); |
| for (auto [inputDim, outputDim] : *dimMapping) |
| rcInputIdxs[inputDim] = oldIdxs[outputDim]; |
| |
| auto rcInput = rc.getSource(); |
| // If the only user of rc is the current Op (which is about to be erased), |
| // we can safely erase it. |
| if (rc.getResult().hasOneUse()) |
| rewriter.eraseOp(rc); |
| rewriter.replaceOpWithNewOp<memref::LoadOp>(op, rcInput, rcInputIdxs); |
| return success(); |
| } |
| }; |
| |
| struct ElideReinterpretCastPass |
| : public memref::impl::ElideReinterpretCastPassBase< |
| ElideReinterpretCastPass> { |
| void runOnOperation() override { |
| MLIRContext &ctx = getContext(); |
| |
| RewritePatternSet patterns(&ctx); |
| memref::populateElideReinterpretCastPatterns(patterns); |
| ConversionTarget target(ctx); |
| target.addDynamicallyLegalOp<memref::CopyOp>([](memref::CopyOp op) { |
| auto rc = op.getTarget().getDefiningOp<memref::ReinterpretCastOp>(); |
| if (!rc) |
| return true; |
| // Pattern applies only when the copy source shape is static and the |
| // reinterpret_cast result can be mapped back to base memref indices. |
| MemRefType cpSrcType = dyn_cast<MemRefType>(op.getSource().getType()); |
| return !(cpSrcType && cpSrcType.hasStaticShape() && |
| getResultNonUnitDimsAndOffsetsForRC(rc)); |
| }); |
| target.addDynamicallyLegalOp<memref::LoadOp>([](memref::LoadOp op) { |
| auto rc = op.getMemRef().getDefiningOp<memref::ReinterpretCastOp>(); |
| if (!rc) |
| return true; |
| return !getNonUnitDimMapping(rc); |
| }); |
| target.addLegalDialect<arith::ArithDialect, memref::MemRefDialect, |
| scf::SCFDialect>(); |
| if (failed(applyPartialConversion(getOperation(), target, |
| std::move(patterns)))) |
| signalPassFailure(); |
| } |
| }; |
| |
| } // namespace |
| |
| void mlir::memref::populateElideReinterpretCastPatterns( |
| RewritePatternSet &patterns) { |
| patterns.add<CopyToLoadAndStore, RewriteLoadFromReinterpretCast>( |
| patterns.getContext()); |
| } |