blob: 41dad1384da750f4ab467f91a8047f23344d0378 [file]
//===-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/IR/Matchers.h"
#include "mlir/IR/TypeUtilities.h"
#include "mlir/Transforms/DialectConversion.h"
#include "llvm/ADT/Repeated.h"
#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 {
/// Returns true if `rc` represents a scalar view (all sizes == 1)
/// into a memref that has exactly one non-unit dimension located at
/// either the first or last position (i.e. a "row" or "column").
///
/// Examples that return true:
///
/// // Row-major slice (last dim is non-unit)
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [1, 1, 1], strides: [1, 1, 1]
/// : memref<1x1x8xi32> to memref<1x1x1xi32>
///
/// // Column-major slice (first dim is non-unit)
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [1, 1], strides: [1, 1]
/// : memref<2x1xf32> to memref<1x1xf32>
///
/// // Random strides
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [1, 1], strides: [10, 100]
/// : memref<2x1xf32, strided<[10, 100]>>
/// to memref<1x1xf32>
///
/// // Rank-1 case
/// memref.reinterpret_cast %buf to offset: [%off],
/// sizes: [1], strides: [1]
/// : memref<8xi32> to memref<1xi32>
///
/// Examples that return false:
///
/// // More non-unit dims
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [1, 1, 1], strides: [1, 1, 1]
/// : memref<1x2x8xi32> to memref<1x1x1xi32>
///
/// // View is not scalar (size != 1)
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [2, 1], strides: [1, 1]
/// : memref<1x2xf32> to memref<2x1xf32>
///
/// // Base has non-identity layout
/// %buff = memref.alloc() : memref<1x2xf32, strided<[1, 3]>>
/// memref.reinterpret_cast %buff to offset: [%off],
/// sizes: [1, 1], strides: [1, 1]
/// : memref<1x2xf32, strided<[1, 3]>> to memref<1x1xf32>
static bool isScalarSlice(memref::ReinterpretCastOp rc) {
auto rcInputTy = dyn_cast<MemRefType>(rc.getSource().getType());
auto rcOutputTy = dyn_cast<MemRefType>(rc.getType());
// Reject strided base - logic for computing linear idx is TODO
if (!rcInputTy.getLayout().isIdentity())
return false;
// Reject non-matching ranks
unsigned srcRank = rcInputTy.getRank();
if (srcRank != rcOutputTy.getRank())
return false;
ArrayRef<int64_t> sizes = rc.getStaticSizes();
// View must be scalar: memref<1x...x1>
if (!llvm::all_of(rcOutputTy.getShape(),
[](int64_t dim) { return dim == 1; }))
return false;
// Sizes must all be statically 1
if (!llvm::all_of(sizes, [](int64_t size) {
return !ShapedType::isDynamic(size) && size == 1;
}))
return false;
// Rank-1 special case
if (srcRank == 1) {
// Reject non-scalar output
if (rcOutputTy.getDimSize(0) > 1)
return false;
}
int nonUnitCount =
std::count_if(rcInputTy.getShape().begin(), rcInputTy.getShape().end(),
[](int dim) { return dim != 1; });
return nonUnitCount == 1;
}
/// Rewrites `memref.copy` of a 1-element MemRef as a scalar load-store pair
///
/// The pattern matches a reinterpret_cast that creates a scalar view
/// (`sizes = [1, ..., 1]`) into a memref with a single non-unit dimension.
/// Since the view contains only one element, the accessed address is
/// determined solely by the base pointer and the offset.
///
/// Two layouts are supported:
/// * row-major slice (stride pattern [N, ..., 1])
/// * column-major slice (stride pattern [1, ..., N])
///
/// BEFORE (row-major slice)
/// %view = memref.reinterpret_cast %base
/// to offset: [%off], sizes: [1, ..., 1], strides: [N, ..., 1]
/// : memref<1x...xNxf32>
/// to memref<1x...x1xf32, strided<[N, ..., 1], offset: ?>>
/// memref.copy %src, %view
/// : memref<1x...x1xf32>
/// to memref<1x...x1xf32, strided<[N, ..., 1], offset: ?>>
///
/// AFTER
/// %c0 = arith.constant 0 : index
/// %v = memref.load %src[%c0, ..., %c0] : memref<1x...x1xf32>
/// memref.store %v, %base[%c0, ..., %off] : memref<1x...xNxf32>
///
/// BEFORE (column-major slice)
/// %view = memref.reinterpret_cast %base
/// to offset: [%off], sizes: [1, ..., 1], strides: [1, ..., N]
/// : memref<Nx...x1xf32>
/// to memref<1x...x1xf32, strided<[1, ..., N], offset: ?>>
/// memref.copy %src, %view
/// : memref<1x...x1xf32>
/// to memref<1x...x1xf32, strided<[1, ..., N], offset: ?>>
///
/// AFTER
/// %c0 = arith.constant 0 : index
/// %v = memref.load %src[%c0, ..., %c0] : memref<1x...x1xf32>
/// memref.store %v, %base[%off, ..., %c0] : memref<Nx...x1xf32>
struct CopyToScalarLoadAndStore : public OpRewritePattern<memref::CopyOp> {
public:
using OpRewritePattern::OpRewritePattern;
LogicalResult matchAndRewrite(memref::CopyOp op,
PatternRewriter &rewriter) const final {
Value rcOutput = op.getTarget();
auto rc = rcOutput.getDefiningOp<memref::ReinterpretCastOp>();
if (!rc)
return rewriter.notifyMatchFailure(
op, "target is not a memref.reinterpret_cast");
if (!isScalarSlice(rc))
return rewriter.notifyMatchFailure(
op, "reinterpret_cast does not match scalar slice");
Location loc = op.getLoc();
Value src = op.getSource();
Value dst = rc.getSource();
auto dstType = cast<MemRefType>(dst.getType());
unsigned dstRank = dstType.getRank();
Value zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
auto srcType = cast<MemRefType>(src.getType());
Repeated<Value> loadIndices(srcType.getRank(), zero);
auto offsets = rc.getMixedOffsets();
assert(offsets.size() == 1 && "Expecting single offset");
OpFoldResult offset = offsets[0];
Value storeOffset = getValueOrCreateConstantIndexOp(rewriter, loc, offset);
unsigned offsetDim = dstType.getDimSize(0) == 1 ? dstRank - 1 : 0;
SmallVector<Value> storeIndices(dstRank, zero);
storeIndices[offsetDim] = storeOffset;
// If the only user of `rc` is the current Op (which is about to be erased),
// we can safely erase it.
if (rcOutput.hasOneUse())
rewriter.eraseOp(rc);
Value val = memref::LoadOp::create(rewriter, loc, src, loadIndices);
memref::StoreOp::create(rewriter, loc, val, dst, storeIndices);
rewriter.eraseOp(op);
return success();
}
};
/// Captures info about MemRefs that are effectively 1D (the leading or trailing
/// dims are all 1). The only accepted non-unit dim is either the leading of the
/// trailing dim.
///
/// Examples:
/// memref<1x1x4xf32>, memref<4x1x1xf32>, memref<1x1x1xf32>
///
struct ShapeInfoFor1DMemRef {
// Are all dims == 1? `false` means that there is exactly one dim != 1.
bool allOnes = true;
// If there is a non-unit boundary dim, is it the leading or the trailing dim?
bool isLeadingDimNonUnit = false;
};
/// Returns information about a MemRef if it contains at most one non-unit
/// dimension.
///
/// The single non-unit dimension, if present, must be on the left or right
/// boundary. Rank-1 non-unit MemRefs are treated as being on both boundaries.
static std::optional<ShapeInfoFor1DMemRef>
getShapeInfoFor1DMemRef(MemRefType type) {
ArrayRef<int64_t> shape = type.getShape();
int64_t nonUnitCount =
llvm::count_if(shape, [](int64_t dim) { return dim != 1; });
// Return default values if missing non-unit dimension (all-ones MemRef).
if (nonUnitCount == 0)
return ShapeInfoFor1DMemRef{};
// Return no info if MemRef has more non-unit dimensions.
if (nonUnitCount > 1)
return std::nullopt;
// Return no info if MemRef has non-unit dimension in non-boundary positions.
if (shape.front() == 1 && shape.back() == 1)
return std::nullopt;
return ShapeInfoFor1DMemRef{/*allOnes=*/false,
/*isLeadingDimNonUnit=*/shape.front() != 1};
}
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);
}
/// Examples accepted by this shape restriction:
/// memref<999xf32> <-> memref<1x1x999xf32>
/// memref<1x108xf32> <-> memref<1x1x1x108xf32>
/// memref<100x1xf32> <-> memref<100x1x1xf32>
/// memref<1> <-> memref<1x1x1>
///
/// General reinterpret_casts are intentionally rejected.
static bool isPureRankExpansionOrCollapsingRC(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 false;
// 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 false;
// Only shapes with at most one non-unit dimension are accepted. This rules
// out more general multi-dimensional reinterpret_casts and restricts the
// helper to unit-dim insertion/removal around a single logical dimension.
std::optional<ShapeInfoFor1DMemRef> inputNonUnitDim =
getShapeInfoFor1DMemRef(inputTy);
std::optional<ShapeInfoFor1DMemRef> outputNonUnitDim =
getShapeInfoFor1DMemRef(outputTy);
// Bail out if either type does not satisfy the single-boundary-non-unit-dim
// restriction described above.
if (!inputNonUnitDim || !outputNonUnitDim)
return false;
// The source and result must either both have a single non-unit dimension
// or both be all-ones.
if (inputNonUnitDim->allOnes != outputNonUnitDim->allOnes)
return false;
if (inputNonUnitDim->allOnes)
return true;
// The preserved non-unit dimension must have the same size.
if (inputTy.getDimSize(
inputNonUnitDim->isLeadingDimNonUnit ? 0 : inputTy.getRank() - 1) !=
outputTy.getDimSize(
outputNonUnitDim->isLeadingDimNonUnit ? 0 : outputTy.getRank() - 1))
return false;
// If both sides have rank > 1, the non-unit dimension must be on the same
// boundary. Rank-1 MemRefs are accepted against either boundary.
if (inputTy.getRank() != 1 && outputTy.getRank() != 1 &&
inputNonUnitDim->isLeadingDimNonUnit !=
outputNonUnitDim->isLeadingDimNonUnit)
return false;
return true;
}
/// Checks statically known and constant indices accessed by a load from a pure
/// rank expansion/collapsing 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 pure rank-only `reinterpret_cast` by
/// mapping the load indices directly onto the source MemRef.
/// Shape restriction gated by isPureRankExpansionOrCollapsingRC().
///
/// BEFORE (rank expansion)
/// %view = memref.reinterpret_cast %src
/// : memref<Nxf32> to memref<1x1xNxf32>
/// %v = memref.load %view[%c0, %c0, %i] : memref<1x1xNxf32>
///
/// AFTER
/// %v = memref.load %src[%i] : memref<Nxf32>
///
/// BEFORE (rank collapsing)
/// %view = memref.reinterpret_cast %src
/// : memref<1x1xNxf32> to memref<Nxf32>
/// %v = memref.load %view[%i] : memref<Nxf32>
///
/// AFTER
/// %c0 = arith.constant 0 : index
/// %v = memref.load %src[%c0, %c0, %i] : memref<1x1xNxf32>
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");
if (!isPureRankExpansionOrCollapsingRC(rc))
return rewriter.notifyMatchFailure(
op, "reinterpret_cast is not a pure rank expansion or collapsing of "
"a single dimension");
assert(areIndicesInBounds(op) &&
"load from reinterpret_cast indexes out of bounds!");
auto rcOutputTy = cast<MemRefType>(rc.getResult().getType());
auto rcInputTy = cast<MemRefType>(rc.getSource().getType());
int64_t rcOutputRank = rcOutputTy.getRank();
int64_t rcInputRank = rcInputTy.getRank();
SmallVector<Value> idxs(op.getIndices().begin(), op.getIndices().end());
SmallVector<Value> rcInputIdxs;
rcInputIdxs.reserve(rcInputRank);
// The rewrite only supports reinterpret_casts with at most one non-unit
// dimension, located at the left or right boundary.
//
// The higher-rank side tells which side the reinterpret_cast has
// expanded/collapsed.
//
// expansion: rcOutput has the higher rank
// collapsing : rcInput has the higher rank
//
// Example:
// memref<999> -> memref<1x1x999> : leading extra dims
// memref<999x1x1> -> memref<999> : trailing extra dims
MemRefType expandedTy =
rcOutputRank >= rcInputRank ? rcOutputTy : rcInputTy;
std::optional<ShapeInfoFor1DMemRef> expandedNonUnitDim =
getShapeInfoFor1DMemRef(expandedTy);
assert(expandedNonUnitDim && "expected a single boundary non-unit dim");
bool keepLeadingIndices = expandedNonUnitDim->isLeadingDimNonUnit;
if (rcOutputRank >= rcInputRank) {
// Rank expansion:
// memref<N> -> memref<1x1xN> : keep the last rcInputRank indices
// memref<N> -> memref<Nx1x1> : keep the first rcInputRank indices
// memref<1> -> memref<1x1x1> : all indices are zero
//
// Any discarded indices are known to be zero from
// areIndicesInBounds().
int64_t firstKeptPos =
keepLeadingIndices ? 0 : rcOutputRank - rcInputRank;
rcInputIdxs.append(idxs.begin() + firstKeptPos,
idxs.begin() + firstKeptPos + rcInputRank);
} else {
// Rank collapsing:
// memref<1x1xN> -> memref<N> : reinsert leading zeros
// memref<Nx1x1> -> memref<N> : reinsert trailing zeros
// memref<1x1x1> -> memref<1> : all indices are zero
//
// The collapsed-away dimensions are unit dims, so re-adding them with
// zero indices preserves semantics.
Value c0 = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0);
int64_t rankDiff = rcInputRank - rcOutputRank;
if (keepLeadingIndices) {
rcInputIdxs.append(idxs.begin(), idxs.end());
rcInputIdxs.append(rankDiff, c0);
} else {
rcInputIdxs.append(rankDiff, c0);
rcInputIdxs.append(idxs.begin(), idxs.end());
}
}
assert(rcInputIdxs.size() == static_cast<size_t>(rcInputRank) &&
"Incorrect number of indices!");
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;
return !isScalarSlice(rc);
});
target.addDynamicallyLegalOp<memref::LoadOp>([](memref::LoadOp op) {
auto rc = op.getMemRef().getDefiningOp<memref::ReinterpretCastOp>();
if (!rc)
return true;
return !isPureRankExpansionOrCollapsingRC(rc);
});
target.addLegalDialect<arith::ArithDialect, memref::MemRefDialect>();
if (failed(applyPartialConversion(getOperation(), target,
std::move(patterns))))
signalPassFailure();
}
};
} // namespace
void mlir::memref::populateElideReinterpretCastPatterns(
RewritePatternSet &patterns) {
patterns.add<CopyToScalarLoadAndStore, RewriteLoadFromReinterpretCast>(
patterns.getContext());
}