blob: 3ca4d331fcf0757590b832a9b47c050ae25fc208 [file] [edit]
// Copyright 2023 The IREE Authors
//
// Licensed 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 "iree/compiler/Dialect/Flow/Transforms/RegionOpUtils.h"
#include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtDialect.h"
#include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtInterfaces.h"
#include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.h"
#include "iree/compiler/Dialect/LinalgExt/Utils/Utils.h"
#include "iree/compiler/GlobalOptimization/Passes.h"
#include "iree/compiler/GlobalOptimization/Utils.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Casting.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Linalg/IR/Linalg.h"
#include "mlir/Dialect/Linalg/Transforms/Transforms.h"
#include "mlir/Dialect/Linalg/Utils/Utils.h"
#include "mlir/Dialect/Math/IR/Math.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/IR/BuiltinAttributes.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/IR/TypeUtilities.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
using namespace mlir;
namespace mlir::iree_compiler::GlobalOptimization {
#define GEN_PASS_DEF_RAISESPECIALOPSPASS
#include "iree/compiler/GlobalOptimization/Passes.h.inc"
namespace {
//===----------------------------------------------------------------------===//
// Slice Raising
//===----------------------------------------------------------------------===//
/// Matches a linalg.generic operation reading data from a tensor `source` using
/// tensor.extract, and raises the `source` tensor to an input of the linalg
/// operation.
static FailureOr<linalg::GenericOp>
raiseTensorExtractToInput(linalg::GenericOp linalgOp, RewriterBase &rewriter) {
if (!linalgOp.hasPureTensorSemantics()) {
return failure();
}
if (!isElementwise(linalgOp)) {
return failure();
}
if (!llvm::hasSingleElement(linalgOp.getResults())) {
return failure();
}
// Find a tensor.extract op in the linalgOp body.
auto extractOps = linalgOp.getBody()->getOps<tensor::ExtractOp>();
if (!llvm::hasSingleElement(extractOps)) {
return failure();
}
tensor::ExtractOp extractOp = *extractOps.begin();
auto resultType = dyn_cast<TensorType>(linalgOp.getResult(0).getType());
if (!resultType) {
return failure();
}
ArrayRef<int64_t> sourceShape = extractOp.getTensor().getType().getShape();
ArrayRef<int64_t> resultShape = resultType.getShape();
// Raise the tensor.extract op to an input.
SmallVector<AffineExpr> exprs;
for (auto [idx, indexValue] : llvm::enumerate(extractOp.getIndices())) {
// For raising, the indexing value must be one of the following:
// 1. A constant value.
// 2. A linalg.index.
// 1. Indexing value is a constant.
APInt constantIndex;
if (matchPattern(indexValue, m_ConstantInt(&constantIndex))) {
// Restrict to cases where the constant is 0. This is because handling
// constants other than 0 in indexing map, may cause problems in the
// lowering pipeline later.
if (constantIndex.getLimitedValue() != 0) {
return failure();
}
exprs.push_back(getAffineConstantExpr(0, rewriter.getContext()));
continue;
}
// 2. The indexing value is a linalg.index.
if (auto indexOp = indexValue.getDefiningOp<linalg::IndexOp>()) {
// Make sure that for this index, the size of the input and output
// match and are not dynamic. We need this to maintain the op to be
// elementwise.
// TODO: This restriction can be relaxed by adding a extract_slice op
// on the `source` tensor. This is not same as raising the whole
// operation to an extract_slice, as there can be permutations and
// projections involved.
if (ShapedType::isDynamic(sourceShape[idx]) ||
ShapedType::isDynamic(resultShape[indexOp.getDim()]) ||
sourceShape[idx] != resultShape[indexOp.getDim()]) {
return failure();
}
exprs.push_back(
getAffineDimExpr(indexOp.getDim(), rewriter.getContext()));
continue;
}
return failure();
}
AffineMap indexingMap = AffineMap::get(
/*dimCount=*/linalgOp.getNumLoops(),
/*symbolCount=*/0, exprs, rewriter.getContext());
// Replace the linalgOp with a new linalgOp where the source tensor is
// an input with the indexing map.
SmallVector<Value> newInputs = linalgOp.getInputs();
newInputs.insert(newInputs.begin(), extractOp.getTensor());
SmallVector<Attribute> newIndexingMaps;
newIndexingMaps.push_back(AffineMapAttr::get(indexingMap));
for (AffineMap map : linalgOp.getIndexingMapsArray()) {
newIndexingMaps.push_back(AffineMapAttr::get(map));
}
auto bodyBuilder = [&](OpBuilder &builder, Location loc, ValueRange args) {
// Create an IR mapping from old block arguments to new ones.
IRMapping mapper;
ArrayRef<BlockArgument> oldArgs = linalgOp.getBody()->getArguments();
// Map i^th old argument to (i + 1)^th new argument.
for (unsigned i = 0; i < oldArgs.size(); ++i) {
mapper.map(oldArgs[i], args[i + 1]);
}
// Clone the body of the linalgOp.
for (Operation &op : linalgOp.getBody()->getOperations()) {
// Replace the extractOp with the first block argument.
if (&op == extractOp) {
mapper.map(op.getResult(0), args[0]);
} else {
builder.clone(op, mapper);
}
}
};
linalg::GenericOp newLinalgOp = linalg::GenericOp::create(
rewriter, linalgOp.getLoc(), linalgOp.getResultTypes(), newInputs,
linalgOp.getOutputs(),
ArrayAttr::get(linalgOp->getContext(), newIndexingMaps),
linalgOp.getIteratorTypesAttr(), linalgOp.getDocAttr(),
linalgOp.getLibraryCallAttr(), bodyBuilder);
return newLinalgOp;
}
/// Given a linalg.generic operation, and input/output tensors with their
/// indexing maps, tries to raise the operation to a tensor.extract_slice
/// operation. The tensor.extract_slice produced can be rank reducing.
static FailureOr<tensor::ExtractSliceOp>
tryRaiseToExtractSlice(AffineMap inputIndexingMap, AffineMap outputIndexingMap,
Value input, Value output, linalg::GenericOp linalgOp,
RewriterBase &rewriter) {
// Output shape must be smaller than input shape.
if (outputIndexingMap.getNumResults() >= inputIndexingMap.getNumResults()) {
return failure();
}
// Output map should be identity.
if (!outputIndexingMap.isIdentity()) {
return failure();
}
auto outType = dyn_cast<RankedTensorType>(output.getType());
if (!outType) {
return failure();
}
ArrayRef<int64_t> outShape = outType.getShape();
int64_t outRank = outShape.size();
// Try to match each output dimension to an input dimension, in order.
// If we find a constant access, we assume that dimension is supposed to be
// rank reduced.
// TODO: Support cases where the constant access matches the output dimension.
SmallVector<OpFoldResult> offsets;
SmallVector<OpFoldResult> sizes;
IntegerAttr zero = rewriter.getI64IntegerAttr(0);
IntegerAttr one = rewriter.getI64IntegerAttr(1);
unsigned currOutDim = 0;
for (auto [idx, expr] : llvm::enumerate(inputIndexingMap.getResults())) {
// Assume that the constant access is a rank reducing access.
if (isa<AffineConstantExpr>(expr)) {
IntegerAttr constIdx =
rewriter.getI64IntegerAttr(cast<AffineConstantExpr>(expr).getValue());
offsets.push_back(constIdx);
sizes.push_back(one);
continue;
}
// Check if the input dimension matches the current output dimension.
if (currOutDim < outRank &&
expr == outputIndexingMap.getResult(currOutDim)) {
offsets.push_back(zero);
// Get the dim size from the output tensor.
if (ShapedType::isDynamic(outShape[currOutDim])) {
auto dim = tensor::DimOp::create(rewriter, linalgOp.getLoc(), output,
currOutDim);
sizes.push_back(dim.getResult());
} else {
sizes.push_back(rewriter.getI64IntegerAttr(outShape[currOutDim]));
}
++currOutDim;
continue;
}
// Unknown access, fail.
return failure();
}
// All output dimensions did not match an input dimension.
if (currOutDim != outputIndexingMap.getNumResults()) {
return failure();
}
// We only support dim expr or a constant expr on the input map, so strides
// will always be 1.
SmallVector<OpFoldResult> strides(inputIndexingMap.getNumResults(), one);
return tensor::ExtractSliceOp::create(rewriter, linalgOp.getLoc(), outType,
input, offsets, sizes, strides);
}
/// Matches a linalg.generic operation with a single input and init output
/// tensor, and tries to raise it to a view-like operation on the input tensor.
static FailureOr<Operation *> tryRaiseToView(linalg::GenericOp linalgOp,
RewriterBase &rewriter) {
if (!linalgOp.hasPureTensorSemantics()) {
return failure();
}
// Assume there is only 1 input, and 1 init tensor.
if (linalgOp.getNumDpsInputs() != 1 || linalgOp.getNumDpsInits() != 1) {
return failure();
}
OpOperand *inputOperand = linalgOp.getDpsInputOperand(0);
OpOperand *outputOperand = linalgOp.getDpsInitOperand(0);
// Check if linalg.yield yields a block argument.
auto yieldOp = dyn_cast<linalg::YieldOp>(linalgOp.getBody()->getTerminator());
if (!yieldOp) {
return failure();
}
auto blockArg = dyn_cast<BlockArgument>(yieldOp.getOperand(0));
if (!blockArg) {
return failure();
}
// Check if the block argument is an argument of the linalgOp.
if (blockArg.getOwner() != linalgOp.getBody()) {
return failure();
}
// Check that the block argument corresponds to the input.
if (blockArg.getArgNumber() != 0) {
return failure();
}
Value input = inputOperand->get();
Value output = outputOperand->get();
AffineMap inputIndexingMap = linalgOp.getMatchingIndexingMap(inputOperand);
AffineMap outputIndexingMap = linalgOp.getMatchingIndexingMap(outputOperand);
// Try raising to tensor.collapse_shape.
return tryRaiseToExtractSlice(inputIndexingMap, outputIndexingMap, input,
output, linalgOp, rewriter);
}
//===----------------------------------------------------------------------===//
// Named GEMM-like extensions
//===----------------------------------------------------------------------===//
template <typename OpTy>
class NamedImplicitCastOpConversion : public OpInterfaceRewritePattern<OpTy> {
public:
using OpInterfaceRewritePattern<OpTy>::OpInterfaceRewritePattern;
NamedImplicitCastOpConversion(MLIRContext *ctx, PatternBenefit b = 1)
: OpInterfaceRewritePattern<OpTy>(ctx, b) {}
LogicalResult matchAndRewrite(OpTy namedOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(namedOp)) {
return failure();
}
auto linalgOp = dyn_cast<linalg::LinalgOp>(namedOp.getOperation());
if (!linalgOp || !linalgOp.hasPureTensorSemantics()) {
return rewriter.notifyMatchFailure(
namedOp, "expected an op with pure tensor semantics");
}
// Look for a producer of the given operand that does an elementwise extend
// and replace the operand with the source of the elementwise producer.
// Returns true if the operand was updated to inform the pattern rewriter
// of a change.
Type outElementType = getElementTypeOrSelf(namedOp->getResultTypes()[0]);
bool didChangeOperand = false;
{
OpBuilder::InsertionGuard guard(rewriter);
Block *block = &namedOp->getRegion(0).front();
rewriter.setInsertionPointToStart(block);
auto replaceOperandWithTypeCast = [&](OpOperand &operand) {
// If the op already has implicit casting semantics for this operand,
// do not fuse.
if (getElementTypeOrSelf(operand.get().getType()) != outElementType) {
return false;
}
auto producer = operand.get().getDefiningOp<linalg::GenericOp>();
if (!producer) {
return false;
}
if (!llvm::all_of(producer.getIndexingMapsArray(),
[](AffineMap map) { return map.isIdentity(); })) {
return false;
}
std::optional<CastOpInterface> castOp =
getDefiningNonI1ExtendingCastOp(operand.get());
if (!castOp) {
return false;
}
// We only handle arith.extf/exti because truncating operations like
// `arith.truncf` should not be fused with consumers; it would be
// preferred to fuse those with producers (and the consumer fusion is
// arguably the less canonical form).
auto canFoldCast = [&]() {
if (isa<arith::ExtFOp>(*castOp)) {
return true;
}
// Signed operations can only be folded with (implicitly) signed
// linalg named ops
if (isa<arith::ExtSIOp>(*castOp)) {
if (auto matmul =
dyn_cast<linalg::MatmulOp>(namedOp.getOperation())) {
return matmul.getCast() != linalg::TypeFn::cast_unsigned;
}
return !isa<linalg::PoolingNhwcMaxUnsignedOp,
linalg::PoolingNhwcMinUnsignedOp,
linalg::PoolingNwcMaxUnsignedOp,
linalg::PoolingNwcMinUnsignedOp>(namedOp);
}
return false;
};
if (!canFoldCast()) {
return false;
}
Type producerElementType = getElementTypeOrSelf(
producer.getDpsInputOperand(0)->get().getType());
int64_t operandNumber = operand.getOperandNumber();
// Set the operand to the linalg op to the smaller one.
namedOp->setOperand(operandNumber, producer->getOperand(0));
// Insert a new block argument into the body of the named op with the
// correct type.
Value blockArg = block->insertArgument(
operandNumber, producerElementType, namedOp.getLoc());
// Create the extf/extsi.
IRMapping mapping;
mapping.map(castOp.value()->getOperand(0), blockArg);
Value ext = rewriter.clone(*castOp.value(), mapping)->getResult(0);
// Replace uses of the old argument with the extended value.
rewriter.replaceAllUsesWith(block->getArgument(operandNumber + 1), ext);
// Erase the old argument.
block->eraseArgument(operandNumber + 1);
return true;
};
didChangeOperand = replaceOperandWithTypeCast(namedOp->getOpOperand(0));
didChangeOperand |= replaceOperandWithTypeCast(namedOp->getOpOperand(1));
}
return success(didChangeOperand);
}
};
//===----------------------------------------------------------------------===//
// Softmax Raising
//===----------------------------------------------------------------------===//
// Recognizing a numerically-stabilized softmax means walking a small graph of
// `linalg.generic` ops. The helpers below match the individual nodes of that
// graph directly, replacing the generic `transform_ext` matcher framework that
// used to live in iree-dialects. The matched dataflow, working back from the
// root op, is:
//
// max = reduce_max(src) (reduction over the innermost dim)
// sub = src - broadcast(max)
// exp = exp(sub)
// sum = reduce_add(exp) (reduction over the innermost dim)
// root = exp * reciprocal(sum) OR exp / broadcast(sum)
//
// where each broadcast is either implicit (the consumer indexing map drops the
// innermost dim) or explicit (a pass-through `linalg.generic` that broadcasts
// along the innermost dim).
// Returns true if every iterator of `op` is parallel.
static bool isAllParallel(linalg::GenericOp op) {
return llvm::all_of(op.getIteratorTypesArray(), [](utils::IteratorType t) {
return t == utils::IteratorType::parallel;
});
}
// Returns true if `operand`'s indexing map is the identity.
static bool isIdentityOperand(linalg::GenericOp op, OpOperand *operand) {
return op.getMatchingIndexingMap(operand).isIdentity();
}
// Returns true if `operand`'s indexing map is the projection that drops the
// innermost (last) loop dim, e.g. (d0, d1, d2) -> (d0, d1).
static bool dropsInnermostDim(linalg::GenericOp op, OpOperand *operand) {
AffineMap map = op.getMatchingIndexingMap(operand);
unsigned numLoops = op.getNumLoops();
if (!map.isProjectedPermutation() || map.getNumResults() + 1 != numLoops) {
return false;
}
for (unsigned i = 0, e = map.getNumResults(); i < e; ++i) {
if (map.getDimPosition(i) != i) {
return false;
}
}
return true;
}
// Returns the single compute op of `op`'s body when the body is exactly that op
// followed by a `linalg.yield` of its single result; otherwise nullptr.
static Operation *getSingleComputeOp(linalg::GenericOp op) {
Block *body = op.getBlock();
if (body->getOperations().size() != 2) {
return nullptr;
}
Operation *computeOp = &body->front();
auto yieldOp = cast<linalg::YieldOp>(body->getTerminator());
if (computeOp->getNumResults() != 1 || yieldOp.getNumOperands() != 1 ||
yieldOp.getOperand(0).getDefiningOp() != computeOp) {
return nullptr;
}
return computeOp;
}
// Returns true if `v` references block argument number `argNumber` of `block`.
static bool isBlockArg(Value v, Block *block, unsigned argNumber) {
auto arg = dyn_cast<BlockArgument>(v);
return arg && arg.getOwner() == block && arg.getArgNumber() == argNumber;
}
// Returns true if `op`'s body is a single `OpTy` whose operands are the leading
// block arguments in order. When `commutative`, a two-operand body may also use
// the arguments in swapped order.
template <typename OpTy>
static bool hasSingleOpBody(linalg::GenericOp op, bool commutative = false) {
Operation *computeOp = getSingleComputeOp(op);
if (!isa_and_nonnull<OpTy>(computeOp)) {
return false;
}
Block *body = op.getBlock();
if (commutative && computeOp->getNumOperands() == 2) {
return (isBlockArg(computeOp->getOperand(0), body, 0) &&
isBlockArg(computeOp->getOperand(1), body, 1)) ||
(isBlockArg(computeOp->getOperand(0), body, 1) &&
isBlockArg(computeOp->getOperand(1), body, 0));
}
for (auto [index, operand] : llvm::enumerate(computeOp->getOperands())) {
if (!isBlockArg(operand, body, index)) {
return false;
}
}
return true;
}
// Returns true if `op`'s body just yields its (single) block argument, i.e. it
// is a pure broadcast/transpose of the input.
static bool isPassThroughBody(linalg::GenericOp op) {
Block *body = op.getBlock();
if (body->getOperations().size() != 1) {
return false;
}
auto yieldOp = cast<linalg::YieldOp>(body->getTerminator());
for (auto [index, operand] : llvm::enumerate(yieldOp.getOperands())) {
if (!isBlockArg(operand, body, index)) {
return false;
}
}
return true;
}
// Returns true if `v` is a float constant satisfying `pred`.
static bool isFloatConstant(Value v, llvm::function_ref<bool(APFloat)> pred) {
auto cstOp = v.getDefiningOp<arith::ConstantFloatOp>();
return cstOp && pred(cstOp.value());
}
// Returns true if `v` is a `linalg.fill` whose scalar value is a float constant
// satisfying `pred`.
static bool isFilledWith(Value v, llvm::function_ref<bool(APFloat)> pred) {
auto fillOp = v.getDefiningOp<linalg::FillOp>();
return fillOp && isFloatConstant(fillOp.getInputs()[0], pred);
}
// Matches a single-input `linalg.generic` reduction over the innermost dim with
// a commutative `OpTy` body and an init produced by a `linalg.fill` of a
// constant satisfying `fillPred`. Returns the reduced value on success.
template <typename OpTy>
static Value
matchInnermostReduction(Value v, llvm::function_ref<bool(APFloat)> fillPred) {
auto op = v.getDefiningOp<linalg::GenericOp>();
if (!op || op.getNumDpsInputs() != 1 || op.getNumDpsInits() != 1) {
return {};
}
SmallVector<utils::IteratorType> iterators = op.getIteratorTypesArray();
if (iterators.empty() || iterators.back() != utils::IteratorType::reduction) {
return {};
}
for (unsigned i = 0, e = iterators.size() - 1; i < e; ++i) {
if (iterators[i] != utils::IteratorType::parallel) {
return {};
}
}
OpOperand *input = op.getDpsInputOperand(0);
OpOperand *init = op.getDpsInitOperand(0);
if (!isIdentityOperand(op, input) || !dropsInnermostDim(op, init) ||
!hasSingleOpBody<OpTy>(op, /*commutative=*/true) ||
!isFilledWith(init->get(), fillPred)) {
return {};
}
return input->get();
}
// Matches a single-input all-parallel `linalg.generic` with identity-mapped
// input and output and an `OpTy` body. Returns the input value on success.
template <typename OpTy>
static Value matchUnaryElementwise(Value v) {
auto op = v.getDefiningOp<linalg::GenericOp>();
if (!op || op.getNumDpsInputs() != 1 || op.getNumDpsInits() != 1 ||
!isAllParallel(op)) {
return {};
}
if (!isIdentityOperand(op, op.getDpsInputOperand(0)) ||
!isIdentityOperand(op, op.getDpsInitOperand(0)) ||
!hasSingleOpBody<OpTy>(op)) {
return {};
}
return op.getDpsInputOperand(0)->get();
}
// Matches a reciprocal `linalg.generic` (body `divf 1.0, %arg0`) that is
// single-input, all-parallel and identity-mapped. Returns the input value.
static Value matchReciprocal(Value v) {
auto op = v.getDefiningOp<linalg::GenericOp>();
if (!op || op.getNumDpsInputs() != 1 || op.getNumDpsInits() != 1 ||
!isAllParallel(op)) {
return {};
}
if (!isIdentityOperand(op, op.getDpsInputOperand(0)) ||
!isIdentityOperand(op, op.getDpsInitOperand(0))) {
return {};
}
auto divOp = dyn_cast_or_null<arith::DivFOp>(getSingleComputeOp(op));
if (!divOp ||
!isFloatConstant(divOp.getOperand(0),
[](APFloat f) { return f.convertToDouble() == 1.0; }) ||
!isBlockArg(divOp.getOperand(1), op.getBlock(), 0)) {
return {};
}
return op.getDpsInputOperand(0)->get();
}
// Resolves `operand` of `consumer`, which is expected to carry the broadcast of
// an innermost-reduced value, to that pre-broadcast value. Handles both the
// implicit broadcast (the operand's map drops the innermost dim) and the
// explicit broadcast (the operand is produced by a pass-through generic that
// broadcasts along the innermost dim).
static Value resolveBroadcastedReduction(linalg::GenericOp consumer,
OpOperand *operand) {
if (dropsInnermostDim(consumer, operand)) {
return operand->get();
}
if (!isIdentityOperand(consumer, operand)) {
return {};
}
auto bcast = operand->get().getDefiningOp<linalg::GenericOp>();
if (!bcast || bcast.getNumDpsInputs() != 1 || bcast.getNumDpsInits() != 1 ||
!isAllParallel(bcast) || !isPassThroughBody(bcast) ||
!dropsInnermostDim(bcast, bcast.getDpsInputOperand(0)) ||
!isIdentityOperand(bcast, bcast.getDpsInitOperand(0))) {
return {};
}
return bcast.getDpsInputOperand(0)->get();
}
// Recognizes the softmax graph rooted at `rootOp` and returns the softmax
// source value on success.
static FailureOr<Value> matchSoftmax(linalg::LinalgOp rootOp) {
auto root = dyn_cast<linalg::GenericOp>(rootOp.getOperation());
if (!root || root.getNumDpsInputs() != 2 || root.getNumDpsInits() != 1 ||
!isAllParallel(root)) {
return failure();
}
// The numerator (exp(...)) always flows in as input 0 with an identity map.
OpOperand *numerator = root.getDpsInputOperand(0);
OpOperand *denominator = root.getDpsInputOperand(1);
if (!isIdentityOperand(root, numerator)) {
return failure();
}
// The root normalizes either as `exp * reciprocal(sum)` or `exp / sum`.
Value sumValue;
if (hasSingleOpBody<arith::MulFOp>(root, /*commutative=*/true)) {
if (!dropsInnermostDim(root, denominator)) {
return failure();
}
sumValue = matchReciprocal(denominator->get());
} else if (hasSingleOpBody<arith::DivFOp>(root)) {
sumValue = resolveBroadcastedReduction(root, denominator);
}
if (!sumValue) {
return failure();
}
// exp = exp(sub); sum = reduce_add(exp).
Value expValue = numerator->get();
Value subValue = matchUnaryElementwise<math::ExpOp>(expValue);
Value summedValue = matchInnermostReduction<arith::AddFOp>(
sumValue, [](APFloat f) { return f.isZero(); });
if (!subValue || summedValue != expValue) {
return failure();
}
// sub = src - broadcast(max).
auto subOp = subValue.getDefiningOp<linalg::GenericOp>();
if (!subOp || subOp.getNumDpsInputs() != 2 || subOp.getNumDpsInits() != 1 ||
!isAllParallel(subOp) ||
!isIdentityOperand(subOp, subOp.getDpsInitOperand(0)) ||
!hasSingleOpBody<arith::SubFOp>(subOp)) {
return failure();
}
OpOperand *subSource = subOp.getDpsInputOperand(0);
if (!isIdentityOperand(subOp, subSource)) {
return failure();
}
Value maxValue =
resolveBroadcastedReduction(subOp, subOp.getDpsInputOperand(1));
if (!maxValue) {
return failure();
}
// max = reduce_max(src), reducing the same source the subtraction reads. The
// init must be -inf or the lowest finite value. Accept both arith.maximumf
// (NaN-propagating, as emitted by e.g. StableHLO frontends) and arith.maxnumf
// (NaN-ignoring, which is what linalg.softmax itself decomposes to), since
// both denote the stabilizing max of a softmax.
auto isNegInfOrLowest = [](APFloat f) {
return (f.isLargest() || f.isInfinity()) && f.isNegative();
};
Value source = subSource->get();
Value reducedValue =
matchInnermostReduction<arith::MaximumFOp>(maxValue, isNegInfOrLowest);
if (!reducedValue) {
reducedValue =
matchInnermostReduction<arith::MaxNumFOp>(maxValue, isNegInfOrLowest);
}
if (reducedValue != source) {
return failure();
}
return source;
}
class RaiseSoftmax : public OpInterfaceRewritePattern<linalg::LinalgOp> {
public:
using OpInterfaceRewritePattern<linalg::LinalgOp>::OpInterfaceRewritePattern;
RaiseSoftmax(MLIRContext *ctx, PatternBenefit b = 1)
: OpInterfaceRewritePattern<linalg::LinalgOp>(ctx, b) {}
LogicalResult matchAndRewrite(linalg::LinalgOp linalgOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(linalgOp)) {
return failure();
}
FailureOr<Value> src = matchSoftmax(linalgOp);
if (failed(src)) {
return rewriter.notifyMatchFailure(linalgOp,
"failed to match softmax root");
}
rewriter.setInsertionPoint(linalgOp);
rewriter.replaceOpWithNewOp<linalg::SoftmaxOp>(
linalgOp, linalgOp->getResultTypes(), *src,
linalgOp.getDpsInitOperand(0)->get(), linalgOp.getNumLoops() - 1);
return success();
}
};
//===----------------------------------------------------------------------===//
// (Batch) Matmul Transpose Fusion
//===----------------------------------------------------------------------===//
// Method to match a transpose operation on the two most minor dimensions of the
// specified rank.
static bool matchInner2DTranspose(linalg::LinalgOp genericOp, unsigned rank) {
// Only makes sense for minimum rank 2.
if (rank < 2) {
return false;
}
if (genericOp.getNumDpsInputs() != 1 || genericOp.getNumDpsInits() != 1) {
return false;
}
// Check only for ops of the specified rank.
if (genericOp.getNumLoops() != rank ||
genericOp.getNumLoops() != genericOp.getNumParallelLoops()) {
return false;
}
// Check for transpose map.
SmallVector<AffineExpr> exprList(rank);
MLIRContext *context = genericOp.getContext();
bindDimsList(context, MutableArrayRef{exprList});
SmallVector<AffineExpr> transposeExprList(exprList);
std::swap(transposeExprList[rank - 1], transposeExprList[rank - 2]);
SmallVector<AffineMap> expectedMaps = {
AffineMap::get(rank, 0, exprList, context),
AffineMap::get(rank, 0, transposeExprList, context)};
if (genericOp.getIndexingMapsArray() != expectedMaps) {
return false;
}
Block *body = genericOp.getBlock();
if (!llvm::hasSingleElement(*body)) {
return false;
}
auto yieldOp = cast<linalg::YieldOp>(body->getTerminator());
auto blockArg = dyn_cast<BlockArgument>(yieldOp.getOperand(0));
if (!blockArg || blockArg.getOwner() != body ||
blockArg.getArgNumber() != 0) {
return false;
}
return true;
}
// Method to match a linalg.matmul(a, linalg.transpose(b)). Returns `b` on
// success.
static std::optional<Value> matchATransposeBMatmul(linalg::LinalgOp matmulOp) {
if (!IREE::LinalgExt::isPureMatmul(matmulOp)) {
return std::nullopt;
}
auto rhs = matmulOp.getDpsInputOperand(1);
auto genericOp = rhs->get().getDefiningOp<linalg::GenericOp>();
if (genericOp && matchInner2DTranspose(genericOp, 2)) {
return genericOp.getDpsInputOperand(0)->get();
}
return std::nullopt;
}
// Method to match a linalg.batch_matmul(a, linalg.transpose(b)). Returns `b` on
// success.
static std::optional<Value>
matchATransposeBBatchMatmul(linalg::LinalgOp bmmOp) {
if (!IREE::LinalgExt::isPureBatchMatmul(bmmOp)) {
return std::nullopt;
}
auto rhs = bmmOp.getDpsInputOperand(1);
auto genericOp = rhs->get().getDefiningOp<linalg::GenericOp>();
if (genericOp && matchInner2DTranspose(genericOp, 3)) {
return genericOp.getDpsInputOperand(0)->get();
}
return std::nullopt;
}
class FuseMatmulTranspose : public OpRewritePattern<linalg::MatmulOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(linalg::MatmulOp matmulOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(matmulOp)) {
return failure();
}
std::optional<Value> newRhs = matchATransposeBMatmul(matmulOp);
if (!newRhs) {
return rewriter.notifyMatchFailure(matmulOp,
"Failed to match transpose on RHS");
}
Value lhs = matmulOp.getDpsInputOperand(0)->get();
Value init = matmulOp.getDpsInitOperand(0)->get();
rewriter.setInsertionPoint(matmulOp);
SmallVector<NamedAttribute> attrs = getPrunedAttributeList(matmulOp);
rewriter.replaceOpWithNewOp<linalg::MatmulTransposeBOp>(
matmulOp, ValueRange{lhs, *newRhs}, ValueRange{init}, attrs);
return success();
}
};
class FuseBatchMatmulTranspose
: public OpRewritePattern<linalg::BatchMatmulOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(linalg::BatchMatmulOp bmmOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(bmmOp)) {
return failure();
}
std::optional<Value> newRhs = matchATransposeBBatchMatmul(bmmOp);
if (!newRhs) {
return rewriter.notifyMatchFailure(bmmOp,
"Failed to match transpose on RHS");
}
Value lhs = bmmOp.getDpsInputOperand(0)->get();
Value init = bmmOp.getDpsInitOperand(0)->get();
rewriter.setInsertionPoint(bmmOp);
SmallVector<NamedAttribute> attrs = getPrunedAttributeList(bmmOp);
rewriter.replaceOpWithNewOp<linalg::BatchMatmulTransposeBOp>(
bmmOp, ValueRange{lhs, *newRhs}, ValueRange{init}, attrs);
return success();
}
};
//===----------------------------------------------------------------------===//
// Fill Raising
//===----------------------------------------------------------------------===//
// Method to match a linalg.generic op representing a linalg.fill op. Returns
// the fill value (input operand to linalg.fill) on success.
static std::optional<Value> matchGenericFill(linalg::LinalgOp linalgOp) {
if (isa<linalg::GenericOp>(linalgOp.getOperation()) &&
linalgOp.getNumDpsInputs() == 0 && linalgOp.getNumDpsInits() == 1 &&
linalgOp.getNumParallelLoops() == linalgOp.getNumLoops() &&
linalgOp.getIndexingMapsArray()[0].isIdentity()) {
// Check that the op body is only a linalg.yield op.
if (linalgOp.getBlock()->getOperations().size() != 1) {
return std::nullopt;
}
Value yieldOperand =
cast<linalg::YieldOp>(linalgOp.getBlock()->getOperations().begin())
.getOperand(0);
// Check that the operand of the linalg.yield op is not an argument of the
// linalg.generic basic block.
for (Value blockArg : linalgOp.getBlock()->getArguments()) {
if (yieldOperand == blockArg) {
return std::nullopt;
}
}
return yieldOperand;
}
return std::nullopt;
}
class RaiseGenericFill : public OpRewritePattern<linalg::GenericOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(linalg::GenericOp genericOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(genericOp)) {
return failure();
}
std::optional<Value> fillInput = matchGenericFill(genericOp);
if (!fillInput) {
return rewriter.notifyMatchFailure(genericOp,
"failed to match as a fill");
}
Value init = genericOp.getDpsInitOperand(0)->get();
rewriter.setInsertionPoint(genericOp);
SmallVector<NamedAttribute> attrs = getPrunedAttributeList(genericOp);
rewriter.replaceOpWithNewOp<linalg::FillOp>(
genericOp, ValueRange{*fillInput}, ValueRange{init}, attrs);
return success();
}
};
//===----------------------------------------------------------------------===//
// Insert Slice to Pad Raising
//===----------------------------------------------------------------------===//
class RaiseInsertSliceToPad : public OpRewritePattern<tensor::InsertSliceOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(tensor::InsertSliceOp sliceOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(sliceOp)) {
return failure();
}
if (sliceOp.getSourceType().getRank() !=
sliceOp.getResultType().getRank()) {
return rewriter.notifyMatchFailure(
sliceOp, "unimplemented: rank reducing slice raising");
}
if (!sliceOp.hasUnitStride()) {
return rewriter.notifyMatchFailure(sliceOp, "strided insert slice");
}
auto constantDest = sliceOp.getDest().getDefiningOp<arith::ConstantOp>();
if (!constantDest) {
return rewriter.notifyMatchFailure(sliceOp,
"Non constant slice destination");
}
auto denseAttr = dyn_cast<DenseElementsAttr>(constantDest.getValue());
if (!denseAttr || !denseAttr.isSplat()) {
return rewriter.notifyMatchFailure(
sliceOp, "Failed to match constant splat destination");
}
Location loc = sliceOp.getLoc();
AffineExpr d0, d1, d2;
bindDims(rewriter.getContext(), d0, d1, d2);
SmallVector<OpFoldResult> lowPadding = sliceOp.getMixedOffsets();
SmallVector<OpFoldResult> highPadding;
for (auto [dim, offset, size] :
llvm::zip_equal(denseAttr.getType().getShape(), lowPadding,
sliceOp.getMixedSizes())) {
highPadding.push_back(affine::makeComposedFoldedAffineApply(
rewriter, loc, d0 - d1 - d2,
{rewriter.getIndexAttr(dim), size, offset}));
}
Value paddingValue = arith::ConstantOp::create(
rewriter, constantDest.getLoc(), denseAttr.getElementType(),
denseAttr.getSplatValue<TypedAttr>());
rewriter.replaceOpWithNewOp<tensor::PadOp>(sliceOp, sliceOp.getResultType(),
sliceOp.getSource(), lowPadding,
highPadding, paddingValue);
return success();
}
};
//===----------------------------------------------------------------------===//
// Concatenate Negate and Slice Raising Patterns
//===----------------------------------------------------------------------===//
static bool isUnaryNegate(linalg::GenericOp op) {
auto block = op.getBlock();
if (block->getNumArguments() != 2) {
return false;
}
Operation *terminator = block->getTerminator();
if (terminator->getNumOperands() != 1) {
return false;
}
auto yielded = terminator->getOperand(0).getDefiningOp<arith::NegFOp>();
if (!yielded) {
return false;
}
if (yielded->getOperand(0) != block->getArgument(0)) {
return false;
}
return true;
}
// This aims to match a partial negation and reverse of a tensor and rewrite
// it as a single linalg generic. For example, this will match sequences like
// the following:
//
// %slice_0 = tensor.extract_slice %0[0, ..., 0] [..., 64] [1, ..., 1]
// : tensor<...x128xf16> to tensor<...x64xf16>
// %slice_1 = tensor.extract_slice %0[0, ..., 64] [..., 64] [1, ..., 1]
// : tensor<...x128xf16> to tensor<...x64xf16>
// %3 = linalg.generic {
// indexing_maps = [affine_map<(d0, ..., dn-1) -> (d0, ..., dn-1)>,
// affine_map<(d0, ..., dn-1) -> (d0, ..., dn-1)>],
// iterator_types = [n-1 x "parallel"]}
// ins(%slice_1 : tensor<...x64xf16>) outs(%2 : tensor<...x64xf16>) {
// ^bb0(%in: f16, %out: f16):
// %5 = arith.negf %in : f16
// linalg.yield %5 : f16
// } -> tensor<...x64xf16>
// %in_slice_0 = tensor.insert_slice %3 into
// %1[0, ..., 0] [..., 64] [1, ..., 1]
// : tensor<...x64xf16> into tensor<...x128xf16>
// %in_slice_1 = tensor.insert_slice %slice_0 into
// %in_slice_0[0, ..., 64] [..., 64] [1, ..., 1]
// : tensor<...x64xf16> into tensor<...x128xf16>
//
// Where the input tensor is broken down along the inner most dimension, then
// the bottom elements are negated and the tensor is reconstructed, reversing
// the order of the slices. This is then rewritten as
//
// %expanded = tensor.expand_shape %0 [[0], ..., [n-1, n]]
// : tensor<...x128xf16> into tensor<...x2x64xf16>
// %2 = linalg.generic {
// indexing_maps = [
// affine_map<(d0, ..., dn-1, dn) -> (d0, ..., dn-1, dn)>],
// iterator_types = [n x "parallel"]
// } outs(%output : tensor<...x2x64xf16>) {
// ^bb0(%out: f16):
// %i0 = linalg.index 0 : index
// ...
// %in-1 = linalg.index n-1 : index
// %in = linalg.index n : index
// %rev = arith.subi %c1, %in-1 : index
// %extracted = tensor.extract %expanded[%i0, ..., %rev, %in]
// : tensor<...x2x64xf16>
// %neg = arith.negf %extracted : f16
// %cmp = arith.cmpi eq, %in-1, %c1 : index
// %sel = arith.select %cmp, %neg, %extracted : f16
// linalg.yield %sel : f16
// } -> tensor<...x2x64xf16>
// %collapsed = tensor.collapse_shape %2 [[0], ..., [n-1, n]]
// : tensor<...x2x64xf16> into tensor<...x128xf16>
static std::optional<Value>
matchCatNegateAndSlice(tensor::InsertSliceOp insertOp) {
/// First match against the desired op chain.
auto topHalf = insertOp.getSource().getDefiningOp<tensor::ExtractSliceOp>();
if (!topHalf) {
return std::nullopt;
}
auto dest = insertOp.getDest().getDefiningOp<tensor::InsertSliceOp>();
if (!dest) {
return std::nullopt;
}
/// TODO: This could be extended to other unary (or in general, elementwise
/// operations) if the need arises.
auto negate = dest.getSource().getDefiningOp<linalg::GenericOp>();
if (!negate || !isUnaryNegate(negate)) {
return std::nullopt;
}
auto bottomHalf =
negate.getDpsInputs()[0].getDefiningOp<tensor::ExtractSliceOp>();
if (!bottomHalf) {
return std::nullopt;
}
Value source = topHalf.getSource();
if (source != bottomHalf.getSource()) {
return std::nullopt;
}
/// Require that the overall operation isn't changing the tensor shape.
if (source.getType() != dest.getType()) {
return std::nullopt;
}
auto sourceType = dyn_cast<RankedTensorType>(source.getType());
if (!sourceType || sourceType.getRank() == 0) {
return std::nullopt;
}
/// Here we are verifying that the slice sequence inserts and extracts in the
/// same order. In other words, the offsets, sizes, and strides for the nth
/// extract slice match the nth insert slice.
/// TODO: We could relax this condition if we see cases where the elementwise
/// unary operation happens on a different slices of the tensor, but for now
/// don't engineer for cases that don't exist.
auto isSame = [](OpFoldResult a, OpFoldResult b) { return a == b; };
if (!insertOp.isSameAs(bottomHalf, isSame) ||
!dest.isSameAs(topHalf, isSame)) {
return std::nullopt;
}
/// All slices need unit stride and the initial slice should have a zero
/// offset.
if (!insertOp.hasUnitStride() || !dest.hasUnitStride() ||
!dest.hasZeroOffset()) {
return std::nullopt;
}
/// Verify all slice sizes are the same.
for (auto [bottom, top] :
llvm::zip_equal(insertOp.getMixedSizes(), dest.getMixedSizes())) {
if (bottom != top) {
return std::nullopt;
}
}
/// Verify that the final InsertSlice has zero offsets for all except the
/// inner most dim.
SmallVector<OpFoldResult> insertOffsets = insertOp.getMixedOffsets();
for (int i = 0, e = insertOffsets.size() - 1; i < e; ++i) {
if (!isZeroInteger(insertOffsets[i])) {
return std::nullopt;
}
}
/// Finally verify that the inner most offset == the inner most slice size,
/// and because we verified all slice sizes are the same, this necessarily
/// is slicing exactly half of the inner most dimension.
if (insertOffsets.back() != insertOp.getMixedSizes().back()) {
return std::nullopt;
}
return source;
}
// This aims to match the exact same logical operation, but instead where the
// final concatenation is actually represented as a concatenation.
//
// %slice_0 = tensor.extract_slice %0[0, ..., 0] [..., 64] [1, ..., 1]
// : tensor<...x128xf16> to tensor<...x64xf16>
// %slice_1 = tensor.extract_slice %0[0, ..., 64] [..., 64] [1, ..., 1]
// : tensor<...x128xf16> to tensor<...x64xf16>
// %3 = linalg.generic {
// indexing_maps = [affine_map<(d0, ..., dn-1) -> (d0, ..., dn-1)>,
// affine_map<(d0, ..., dn-1) -> (d0, ..., dn-1)>],
// iterator_types = [n-1 x "parallel"]}
// ins(%slice_1 : tensor<...x64xf16>) outs(%2 : tensor<...x64xf16>) {
// ^bb0(%in: f16, %out: f16):
// %5 = arith.negf %in : f16
// linalg.yield %5 : f16
// } -> tensor<...x64xf16>
// %in_slice_0 = tensor.concat %3, %slice_0 : ... -> tensor<...x128xf16>
//
// With the exact same rewritten IR.
static std::optional<Value> matchCatNegateAndSlice(tensor::ConcatOp concatOp) {
/// First match against the desired op chain.
ValueRange inputs = concatOp.getInputs();
if (inputs.size() != 2) {
return std::nullopt;
}
Value left = inputs[0];
Value right = inputs[1];
if (left.getType() != right.getType()) {
return std::nullopt;
}
// The concatenation must happen along the inner most dimension.
auto inputType = cast<RankedTensorType>(left.getType());
if (concatOp.getDim() != inputType.getRank() - 1) {
return std::nullopt;
}
/// TODO: This could be extended to other unary (or in general, elementwise
/// operations) if the need arises.
auto negate = left.getDefiningOp<linalg::GenericOp>();
if (!negate || !isUnaryNegate(negate)) {
return std::nullopt;
}
auto topHalf = right.getDefiningOp<tensor::ExtractSliceOp>();
if (!topHalf) {
return std::nullopt;
}
auto bottomHalf =
negate.getDpsInputs()[0].getDefiningOp<tensor::ExtractSliceOp>();
if (!bottomHalf) {
return std::nullopt;
}
Value source = topHalf.getSource();
if (source != bottomHalf.getSource()) {
return std::nullopt;
}
/// Require that the overall operation isn't changing the tensor shape.
if (source.getType() != concatOp.getType()) {
return std::nullopt;
}
return source;
}
static Value createCatNegateAndSlice(RewriterBase &rewriter, Value outTensor,
Value source) {
Location loc = source.getLoc();
/// The matcher checks that this cast is valid.
auto sourceType = cast<RankedTensorType>(source.getType());
SmallVector<int64_t> targetShape(sourceType.getShape());
SmallVector<ReassociationIndices> reassoc;
for (int i = 0, e = targetShape.size(); i < e; i++) {
reassoc.push_back(ReassociationIndices{i});
}
reassoc.back().push_back(targetShape.size());
/// Note that because we've verified the pair of slices is exactly half
/// of the whole inner most extent, the sliceSize is necessarily
/// divisible by 2.
int64_t sliceSize = targetShape.back();
targetShape[targetShape.size() - 1] = 2;
targetShape.push_back(ShapedType::isDynamic(sliceSize) ? sliceSize
: sliceSize / 2);
Type expandedType =
RankedTensorType::get(targetShape, sourceType.getElementType());
Value expanded = tensor::ExpandShapeOp::create(rewriter, loc, expandedType,
source, reassoc);
Value expandedOutTensor = tensor::ExpandShapeOp::create(
rewriter, loc, expandedType, outTensor, reassoc);
SmallVector<AffineMap> indexingMaps = {
rewriter.getMultiDimIdentityMap(targetShape.size())};
SmallVector<utils::IteratorType> iteratorTypes(targetShape.size(),
utils::IteratorType::parallel);
auto bodyBuilder = [&](OpBuilder &b, Location loc, ValueRange args) {
SmallVector<Value> extractionIndices;
for (size_t i = 0, e = targetShape.size(); i < e; ++i) {
extractionIndices.push_back(linalg::IndexOp::create(b, loc, i));
}
Value c1 =
arith::ConstantOp::create(rewriter, loc, rewriter.getIndexAttr(1));
// Take the reverse of the second to last iterator. Because we statically
// guaranteed it to be 2 it just becomes `1 - iters[-2]`.
Value reverseSplitIdx = arith::SubIOp::create(
rewriter, loc, c1, extractionIndices[targetShape.size() - 2]);
extractionIndices[targetShape.size() - 2] = reverseSplitIdx;
// Extract the value from input tensor and negate the top half of the result
// slice (lower half of the input slice).
Value inputVal =
tensor::ExtractOp::create(b, loc, expanded, extractionIndices);
Value maybeNegate = arith::NegFOp::create(b, loc, inputVal);
Value isEqual = arith::CmpIOp::create(b, loc, arith::CmpIPredicate::eq,
reverseSplitIdx, c1);
Value select =
arith::SelectOp::create(rewriter, loc, isEqual, maybeNegate, inputVal);
linalg::YieldOp::create(b, loc, select);
};
Value result =
linalg::GenericOp::create(rewriter, loc, expandedOutTensor.getType(),
ValueRange(), expandedOutTensor, indexingMaps,
iteratorTypes, bodyBuilder)
.getResult(0);
return tensor::CollapseShapeOp::create(rewriter, loc, outTensor.getType(),
result, reassoc);
}
static Value rewriteCatNegateAndSlice(RewriterBase &rewriter,
tensor::InsertSliceOp sliceOp,
Value source) {
rewriter.setInsertionPoint(sliceOp);
Value outTensor =
sliceOp.getDest().getDefiningOp<tensor::InsertSliceOp>().getDest();
return createCatNegateAndSlice(rewriter, outTensor, source);
}
static Value rewriteCatNegateAndSlice(RewriterBase &rewriter,
tensor::ConcatOp concatOp, Value source) {
rewriter.setInsertionPoint(concatOp);
Type elemType = cast<RankedTensorType>(source.getType()).getElementType();
Value outTensor = tensor::EmptyOp::create(
rewriter, source.getLoc(),
tensor::getMixedSizes(rewriter, source.getLoc(), source), elemType);
return createCatNegateAndSlice(rewriter, outTensor, source);
}
class InsertSliceNegateAndSlicePattern
: public OpRewritePattern<tensor::InsertSliceOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(tensor::InsertSliceOp sliceOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(sliceOp)) {
return failure();
}
std::optional<Value> dagRoot = matchCatNegateAndSlice(sliceOp);
if (!dagRoot) {
return rewriter.notifyMatchFailure(sliceOp, "failed to match target dag");
}
Value res = rewriteCatNegateAndSlice(rewriter, sliceOp, *dagRoot);
rewriter.replaceOp(sliceOp, res);
return success();
}
};
class ConcatenateNegateAndSlicePattern
: public OpRewritePattern<tensor::ConcatOp> {
public:
using Base::Base;
LogicalResult matchAndRewrite(tensor::ConcatOp concatOp,
PatternRewriter &rewriter) const override {
if (!IREE::Flow::isNonNullAndOutsideDispatch(concatOp)) {
return failure();
}
std::optional<Value> dagRoot = matchCatNegateAndSlice(concatOp);
if (!dagRoot) {
return rewriter.notifyMatchFailure(concatOp,
"failed to match target dag");
}
Value res = rewriteCatNegateAndSlice(rewriter, concatOp, *dagRoot);
rewriter.replaceOp(concatOp, res);
return success();
}
};
//===----------------------------------------------------------------------===//
// Pass Implementation
//===----------------------------------------------------------------------===//
struct RaiseSpecialOpsPass
: impl::RaiseSpecialOpsPassBase<RaiseSpecialOpsPass> {
void getDependentDialects(DialectRegistry &registry) const override {
registry.insert<IREE::LinalgExt::IREELinalgExtDialect>();
}
void runOnOperation() override {
Operation *funcOp = getOperation();
MLIRContext *context = &getContext();
// First walk the IR and try to raise any slice-like generics to tensor.
IRRewriter rewriter(context);
SmallVector<linalg::GenericOp> genericOps;
funcOp->walk([&](linalg::GenericOp op) { genericOps.push_back(op); });
for (linalg::GenericOp linalgOp : genericOps) {
// Try raising to tensor.extract to an input and create an linalg.generic.
OpBuilder::InsertionGuard guard(rewriter);
rewriter.setInsertionPoint(linalgOp);
FailureOr<linalg::GenericOp> maybeNewOp =
raiseTensorExtractToInput(linalgOp, rewriter);
if (succeeded(maybeNewOp)) {
rewriter.replaceOp(linalgOp, *maybeNewOp);
linalgOp = *maybeNewOp;
}
// Try raising to a view-like operation. Replace if the op raising was
// successful.
FailureOr<Operation *> maybeRaisedView =
tryRaiseToView(linalgOp, rewriter);
if (succeeded(maybeRaisedView)) {
rewriter.replaceOp(linalgOp, *maybeRaisedView);
}
}
// Next run a variety of raising patterns.
{
RewritePatternSet patterns(context);
patterns.insert<
NamedImplicitCastOpConversion<linalg::ConvolutionOpInterface>,
NamedImplicitCastOpConversion<linalg::ContractionOpInterface>>(
context);
patterns.insert<RaiseSoftmax>(context);
patterns.insert<FuseMatmulTranspose>(context);
patterns.insert<FuseBatchMatmulTranspose>(context);
patterns.insert<RaiseGenericFill>(context);
patterns.insert<InsertSliceNegateAndSlicePattern>(context);
patterns.insert<ConcatenateNegateAndSlicePattern>(context);
patterns.insert<RaiseInsertSliceToPad>(context);
if (failed(applyPatternsGreedily(funcOp, std::move(patterns)))) {
return signalPassFailure();
}
}
}
};
} // namespace
} // namespace mlir::iree_compiler::GlobalOptimization