| // Copyright 2022 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/ExternalInterfaces/UtilExternalModels.h" |
| |
| #include "iree/compiler/Dialect/Encoding/IR/EncodingDialect.h" |
| #include "iree/compiler/Dialect/Encoding/IR/EncodingOps.h" |
| #include "iree/compiler/Dialect/Flow/IR/FlowDialect.h" |
| #include "iree/compiler/Dialect/Flow/IR/FlowOps.h" |
| #include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtDialect.h" |
| #include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.h" |
| #include "iree/compiler/Dialect/LinalgExt/Utils/Utils.h" |
| #include "iree/compiler/Dialect/Util/IR/UtilDialect.h" |
| #include "iree/compiler/Dialect/Util/IR/UtilOps.h" |
| #include "iree/compiler/Dialect/Util/IR/UtilTypes.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/MLProgram/IR/MLProgram.h" |
| #include "mlir/Dialect/MemRef/IR/MemRef.h" |
| #include "mlir/Dialect/SCF/IR/SCF.h" |
| #include "mlir/Dialect/Tensor/IR/Tensor.h" |
| #include "mlir/IR/AffineExprVisitor.h" |
| #include "mlir/IR/IRMapping.h" |
| #include "mlir/IR/Matchers.h" |
| #include "mlir/Interfaces/ControlFlowInterfaces.h" |
| #include "mlir/Interfaces/ValueBoundsOpInterface.h" |
| |
| #include <numeric> |
| |
| namespace mlir::iree_compiler { |
| |
| namespace { |
| |
| //===----------------------------------------------------------------------===// |
| // InferIntDivisibilityOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| static IREE::Util::ConstantIntDivisibility |
| getDivisibilityOfOperand(Value v, |
| IREE::Util::IntegerDivisibility divisibility) { |
| if (!divisibility.isUninitialized()) { |
| return divisibility.getValue(); |
| } |
| APInt intVal; |
| if (matchPattern(v, m_ConstantInt(&intVal))) { |
| uint64_t udiv = intVal.getZExtValue(); |
| uint64_t sdiv = std::abs(intVal.getSExtValue()); |
| return IREE::Util::ConstantIntDivisibility(udiv, sdiv); |
| } |
| return IREE::Util::ConstantIntDivisibility(1, 1); |
| } |
| |
| /// Visits affine expressions and recursively calculates the divisibilities of |
| /// each subexpression. The final divisibilities of the expression and its |
| /// subexpressions will be stored in the map for which a reference is provided |
| /// to the AffineExprDivisibilityFinder (i.e., `divisibilityMap`). |
| class AffineExprDivisibilityFinder |
| : public AffineExprVisitor<AffineExprDivisibilityFinder, |
| IREE::Util::ConstantIntDivisibility> { |
| public: |
| using ExprDivisibilityMap = |
| llvm::DenseMap<AffineExpr, IREE::Util::ConstantIntDivisibility>; |
| AffineExprDivisibilityFinder(ExprDivisibilityMap &divisibilityMap) |
| : divisibilityMap(divisibilityMap) {} |
| |
| IREE::Util::ConstantIntDivisibility |
| visitConstantExpr(AffineConstantExpr expr) { |
| // Constant expressions are trivial, since they are always static. |
| uint64_t constValue = std::abs(expr.getValue()); |
| return IREE::Util::ConstantIntDivisibility(constValue, constValue); |
| } |
| |
| IREE::Util::ConstantIntDivisibility visitDimExpr(AffineDimExpr expr) { |
| // Dim expressions cannot be analyzed further, so return the divisibility |
| // in `divisibilityMap` if it has been populated by the caller, or fallback |
| // to the minimum divisibility. |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| return IREE::Util::IntegerDivisibility::getMinDivisibility().getValue(); |
| } |
| |
| IREE::Util::ConstantIntDivisibility visitSymbolExpr(AffineSymbolExpr expr) { |
| // Symbol expressions cannot be analyzed further, so return the divisibility |
| // in `divisibilityMap` if it has been populated by the caller, or fallback |
| // to the minimum divisibility. |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| return IREE::Util::IntegerDivisibility::getMinDivisibility().getValue(); |
| } |
| |
| /// Infer the divisibility of an addition or subtraction expression by |
| /// recursively visiting the LHS and RHS, and then unioning the results. |
| IREE::Util::ConstantIntDivisibility visitAddExpr(AffineBinaryOpExpr expr) { |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| // The divisibility of an addition is the GCD of its constituents' |
| // divisibilities. |
| IREE::Util::ConstantIntDivisibility lhsDiv = visit(expr.getLHS()); |
| IREE::Util::ConstantIntDivisibility rhsDiv = visit(expr.getRHS()); |
| return lhsDiv.getUnion(rhsDiv); |
| } |
| |
| /// Infer the divisibility of a multiplication expression by recursively |
| /// visiting the LHS and RHS, and then multiplying the results. |
| IREE::Util::ConstantIntDivisibility visitMulExpr(AffineBinaryOpExpr expr) { |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| // The divisibility of a multiplication is the product of its constituents' |
| // divisibilities. |
| IREE::Util::ConstantIntDivisibility lhsDiv = visit(expr.getLHS()); |
| IREE::Util::ConstantIntDivisibility rhsDiv = visit(expr.getRHS()); |
| return IREE::Util::ConstantIntDivisibility(lhsDiv.udiv() * rhsDiv.udiv(), |
| lhsDiv.sdiv() * rhsDiv.sdiv()); |
| } |
| |
| IREE::Util::ConstantIntDivisibility |
| visitFloorDivExpr(AffineBinaryOpExpr expr) { |
| return visitDivExpr(expr); |
| } |
| |
| IREE::Util::ConstantIntDivisibility |
| visitCeilDivExpr(AffineBinaryOpExpr expr) { |
| return visitDivExpr(expr); |
| } |
| |
| /// Infer the divisibility of a mod expression. If the RHS is a constant, |
| /// the result divisibility is gcd(lhs_divisibility, rhs_constant), since |
| /// (d * k) mod c is always divisible by gcd(d, c). Furthermore, if the |
| /// LHS divisibility is itself divisible by the constant (i.e., d % c == 0), |
| /// then (d * k) mod c is always zero, represented as divisibility 0. |
| IREE::Util::ConstantIntDivisibility visitModExpr(AffineBinaryOpExpr expr) { |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| auto constRhs = dyn_cast<AffineConstantExpr>(expr.getRHS()); |
| if (!constRhs || constRhs.getValue() == 0) { |
| return IREE::Util::ConstantIntDivisibility(1, 1); |
| } |
| auto constValue = static_cast<uint64_t>(std::abs(constRhs.getValue())); |
| IREE::Util::ConstantIntDivisibility lhsDiv = visit(expr.getLHS()); |
| // If the LHS is always a multiple of constValue, x mod constValue is |
| // always zero. Divisibility 0 is the lattice top ("divides everything"). |
| uint64_t modUDiv = (lhsDiv.udiv() % constValue == 0) |
| ? 0 |
| : std::gcd(lhsDiv.udiv(), constValue); |
| uint64_t modSDiv = (lhsDiv.sdiv() % constValue == 0) |
| ? 0 |
| : std::gcd(lhsDiv.sdiv(), constValue); |
| return IREE::Util::ConstantIntDivisibility(modUDiv, modSDiv); |
| } |
| |
| private: |
| IREE::Util::ConstantIntDivisibility |
| visitInvalidExpr(AffineBinaryOpExpr expr) { |
| return IREE::Util::IntegerDivisibility::getMinDivisibility().getValue(); |
| } |
| |
| /// Helper shared by ceildiv and floordiv implementations. Returns the minimum |
| /// divisibility as a fallback if the divisor is not a constant, because the |
| /// divisibility cannot be inferred in this case. If the divisor is a |
| /// constant, then this function recursively visits the dividend, and returns |
| /// the quotient of the dividend's divisibility with the divisor. |
| IREE::Util::ConstantIntDivisibility visitDivExpr(AffineBinaryOpExpr expr) { |
| if (divisibilityMap.contains(expr)) { |
| return divisibilityMap[expr]; |
| } |
| auto constRhs = dyn_cast<AffineConstantExpr>(expr.getRHS()); |
| // Division by zero is undefined, so return the minimum divisibility. |
| if (!constRhs || constRhs.getValue() == 0) { |
| return IREE::Util::ConstantIntDivisibility(1, 1); |
| } |
| auto constValue = static_cast<uint64_t>(std::abs(constRhs.getValue())); |
| IREE::Util::ConstantIntDivisibility lhsDiv = visit(expr.getLHS()); |
| uint64_t divUDiv = |
| lhsDiv.udiv() % constValue == 0 ? lhsDiv.udiv() / constValue : 1; |
| uint64_t divSDiv = |
| lhsDiv.sdiv() % constValue == 0 ? lhsDiv.sdiv() / constValue : 1; |
| return IREE::Util::ConstantIntDivisibility(divUDiv, divSDiv); |
| } |
| |
| ExprDivisibilityMap &divisibilityMap; |
| }; |
| |
| /// Returns the divisibilities of each AffineMap result based on the |
| /// divisibilities of its dims and symbols. The `dimAndSymbolDivisibilities` |
| /// should contain the divisibilities of the dims, followed by the |
| /// divisibilities of the symbols in ascending order by their positions. |
| static SmallVector<IREE::Util::ConstantIntDivisibility> getResultDivisibilities( |
| AffineMap map, |
| ArrayRef<IREE::Util::ConstantIntDivisibility> dimAndSymbolDivisibilities) { |
| // Seed the AffineExprDivisibilityFinder with the dimAndSymbolDivisibilities. |
| llvm::DenseMap<AffineExpr, IREE::Util::ConstantIntDivisibility> |
| exprDivisibilityMap; |
| SmallVector<AffineExpr> inputExprs; |
| inputExprs.append(llvm::map_to_vector( |
| llvm::seq<int64_t>(map.getNumDims()), |
| [&](int64_t dim) { return getAffineDimExpr(dim, map.getContext()); })); |
| inputExprs.append(llvm::map_to_vector( |
| llvm::seq<int64_t>(map.getNumSymbols()), |
| [&](int64_t sym) { return getAffineSymbolExpr(sym, map.getContext()); })); |
| for (auto [expr, divisibility] : |
| llvm::zip_equal(inputExprs, dimAndSymbolDivisibilities)) { |
| exprDivisibilityMap[expr] = divisibility; |
| } |
| AffineExprDivisibilityFinder divisibilityFinder(exprDivisibilityMap); |
| |
| // Walk each result expression and compute their divisibilities. |
| SmallVector<IREE::Util::ConstantIntDivisibility> resultDivisibilities; |
| for (AffineExpr resultExpr : map.getResults()) { |
| resultDivisibilities.push_back(divisibilityFinder.visit(resultExpr)); |
| } |
| return resultDivisibilities; |
| } |
| |
| struct AffineApplyInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| AffineApplyInferIntDivisibilityOpInterface, affine::AffineApplyOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto affineApplyOp = cast<affine::AffineApplyOp>(op); |
| SmallVector<IREE::Util::ConstantIntDivisibility> operandDivisibilities; |
| for (auto [operand, divisibility] : |
| llvm::zip(affineApplyOp.getOperands(), argDivs)) { |
| operandDivisibilities.push_back( |
| getDivisibilityOfOperand(operand, divisibility)); |
| } |
| |
| SmallVector<IREE::Util::ConstantIntDivisibility> resultDivisibilities = |
| getResultDivisibilities(affineApplyOp.getMap(), operandDivisibilities); |
| for (auto [result, divisibility] : |
| llvm::zip_equal(affineApplyOp->getResults(), resultDivisibilities)) { |
| setResultDivs(result, divisibility); |
| } |
| } |
| }; |
| |
| /// Infer the result divisibility of an affine.min or affine.max operation |
| /// based on its operand divisibilities. The result divisibility is the GCD |
| /// of the divisibilities of each of the affine map results, because the result |
| /// of the affine.min/max op could be any of these results. |
| template <typename MinOrMaxTy> |
| static void inferAffineMinOrMaxResultDivisibility( |
| MinOrMaxTy minOrMaxOp, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) { |
| static_assert( |
| llvm::is_one_of<MinOrMaxTy, affine::AffineMinOp, |
| affine::AffineMaxOp>::value, |
| "MinOrMaxTy must be affine::AffineMinOp or affine::AffineMaxOp"); |
| SmallVector<IREE::Util::ConstantIntDivisibility> operandDivisibilities; |
| for (auto [operand, divisibility] : |
| llvm::zip(minOrMaxOp.getOperands(), argDivs)) { |
| operandDivisibilities.push_back( |
| getDivisibilityOfOperand(operand, divisibility)); |
| } |
| |
| SmallVector<IREE::Util::ConstantIntDivisibility> resultDivisibilities = |
| getResultDivisibilities(minOrMaxOp.getMap(), operandDivisibilities); |
| |
| IREE::Util::ConstantIntDivisibility resultDivisibility = |
| resultDivisibilities.pop_back_val(); |
| for (auto divisibility : resultDivisibilities) { |
| resultDivisibility = resultDivisibility.getUnion(divisibility); |
| } |
| setResultDivs(minOrMaxOp.getResult(), resultDivisibility); |
| } |
| |
| struct AffineMinInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| AffineMinInferIntDivisibilityOpInterface, affine::AffineMinOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto affineMinOp = cast<affine::AffineMinOp>(op); |
| inferAffineMinOrMaxResultDivisibility(affineMinOp, argDivs, setResultDivs); |
| } |
| }; |
| |
| struct AffineMaxInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| AffineMaxInferIntDivisibilityOpInterface, affine::AffineMaxOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto affineMaxOp = cast<affine::AffineMaxOp>(op); |
| inferAffineMinOrMaxResultDivisibility(affineMaxOp, argDivs, setResultDivs); |
| } |
| }; |
| |
| struct AffineDelinearizeIndexInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| AffineDelinearizeIndexInferIntDivisibilityOpInterface, |
| affine::AffineDelinearizeIndexOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto delinearizeOp = cast<affine::AffineDelinearizeIndexOp>(op); |
| MLIRContext *ctx = op->getContext(); |
| |
| // Operands are: [linear_index, dynamic_basis_values...] |
| IREE::Util::ConstantIntDivisibility linearDiv = |
| getDivisibilityOfOperand(delinearizeOp.getLinearIndex(), argDivs[0]); |
| |
| ArrayRef<int64_t> staticBasis = delinearizeOp.getStaticBasis(); |
| int64_t numResults = delinearizeOp.getNumResults(); |
| |
| // Build affine expressions for each result. |
| // Dim 0 = linear index, symbols = dynamic basis values. |
| AffineExpr linearExpr = getAffineDimExpr(0, ctx); |
| |
| // Collect operand divisibilities: [linear_index_div, dynamic_basis_divs...] |
| SmallVector<IREE::Util::ConstantIntDivisibility> operandDivs; |
| operandDivs.push_back(linearDiv); |
| |
| // Map static/dynamic basis values to affine expressions. |
| int64_t dynIdx = 0; |
| SmallVector<AffineExpr> basisExprs; |
| for (int64_t i = 0, e = static_cast<int64_t>(staticBasis.size()); i < e; |
| ++i) { |
| if (ShapedType::isDynamic(staticBasis[i])) { |
| basisExprs.push_back(getAffineSymbolExpr(dynIdx, ctx)); |
| operandDivs.push_back(getDivisibilityOfOperand( |
| delinearizeOp.getDynamicBasis()[dynIdx], argDivs[1 + dynIdx])); |
| dynIdx++; |
| } else { |
| basisExprs.push_back(getAffineConstantExpr(staticBasis[i], ctx)); |
| } |
| } |
| |
| // The computation basis skips the outer bound if present. |
| bool hasOuter = delinearizeOp.hasOuterBound(); |
| int64_t basisStart = hasOuter ? 1 : 0; |
| |
| // Each result[i] can be expressed as an affine expression of the linear |
| // index using the effective basis (after dropping outer bound if present). |
| // Effective basis B[k] = basisExprs[basisStart + k], for k = 0..N-2. |
| // Stride s[i] = product of B[i..N-2] = product of |
| // basisExprs[basisStart+i .. end]. |
| // |
| // result[0] = x floordiv s[0] |
| // result[i>0] = (x floordiv s[i]) mod B[i-1] |
| // For i=N-1, s[N-1]=1, so result[N-1] = x mod B[N-2]. |
| |
| AffineExpr stride = getAffineConstantExpr(1, ctx); |
| for (int64_t i = numResults - 1; i >= 0; --i) { |
| AffineExpr resultExpr; |
| if (i == 0) { |
| resultExpr = linearExpr.floorDiv(stride); |
| } else { |
| resultExpr = |
| (linearExpr.floorDiv(stride)) % basisExprs[basisStart + i - 1]; |
| } |
| |
| AffineMap resultMap = AffineMap::get(1, dynIdx, resultExpr, ctx); |
| SmallVector<IREE::Util::ConstantIntDivisibility> divs = |
| getResultDivisibilities(resultMap, operandDivs); |
| setResultDivs(delinearizeOp.getResult(i), divs[0]); |
| |
| if (i > 0) { |
| stride = basisExprs[basisStart + i - 1] * stride; |
| } |
| } |
| } |
| }; |
| |
| /// Helper for binary arith ops whose result divisibility is the GCD (union) of |
| /// their operands' divisibilities. This covers add, sub, min, and max. |
| template <typename OpTy> |
| struct ArithBinaryGCDInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<OpTy>, OpTy> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto binOp = cast<OpTy>(op); |
| auto lhsDiv = getDivisibilityOfOperand(binOp.getLhs(), argDivs[0]); |
| auto rhsDiv = getDivisibilityOfOperand(binOp.getRhs(), argDivs[1]); |
| setResultDivs(binOp.getResult(), lhsDiv.getUnion(rhsDiv)); |
| } |
| }; |
| |
| /// For arith.select, the result divisibility is the GCD of the true and false |
| /// operands' divisibilities. The condition (operand 0) is i1 and irrelevant. |
| struct ArithSelectInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| ArithSelectInferIntDivisibilityOpInterface, arith::SelectOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto selectOp = cast<arith::SelectOp>(op); |
| // argDivs[0] is the condition (i1), argDivs[1] is true, argDivs[2] is |
| // false. |
| auto trueDiv = |
| getDivisibilityOfOperand(selectOp.getTrueValue(), argDivs[1]); |
| auto falseDiv = |
| getDivisibilityOfOperand(selectOp.getFalseValue(), argDivs[2]); |
| setResultDivs(selectOp.getResult(), trueDiv.getUnion(falseDiv)); |
| } |
| }; |
| |
| struct ArithConstantInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| ArithConstantInferIntDivisibilityOpInterface, arith::ConstantOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto constOp = cast<arith::ConstantOp>(op); |
| auto constAttr = dyn_cast_if_present<IntegerAttr>(constOp.getValue()); |
| if (constAttr) { |
| const APInt &value = constAttr.getValue(); |
| uint64_t udiv = value.getZExtValue(); |
| uint64_t sdiv = std::abs(value.getSExtValue()); |
| setResultDivs(constOp.getResult(), |
| IREE::Util::ConstantIntDivisibility(udiv, sdiv)); |
| } |
| } |
| }; |
| |
| struct ArithMulIInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| ArithMulIInferIntDivisibilityOpInterface, arith::MulIOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto mulOp = cast<arith::MulIOp>(op); |
| |
| auto lhsDivisibility = getDivisibilityOfOperand(mulOp.getLhs(), argDivs[0]); |
| auto rhsDivisibility = getDivisibilityOfOperand(mulOp.getRhs(), argDivs[1]); |
| |
| uint64_t mulUDiv = lhsDivisibility.udiv() * rhsDivisibility.udiv(); |
| uint64_t mulSDiv = lhsDivisibility.sdiv() * rhsDivisibility.sdiv(); |
| |
| setResultDivs(mulOp.getResult(), |
| IREE::Util::ConstantIntDivisibility(mulUDiv, mulSDiv)); |
| } |
| }; |
| |
| struct ArithDivUIInferIntDivisibilityOpInterface |
| : IREE::Util::InferIntDivisibilityOpInterface::ExternalModel< |
| ArithDivUIInferIntDivisibilityOpInterface, arith::DivUIOp> { |
| |
| void inferResultDivisibility( |
| Operation *op, ArrayRef<IREE::Util::IntegerDivisibility> argDivs, |
| IREE::Util::SetIntDivisibilityFn setResultDivs) const { |
| auto divOp = cast<arith::DivUIOp>(op); |
| |
| APInt intVal; |
| if (!matchPattern(divOp.getRhs(), m_ConstantInt(&intVal))) { |
| return; |
| } |
| |
| auto lhsDivisibility = getDivisibilityOfOperand(divOp.getLhs(), argDivs[0]); |
| |
| uint64_t divUDiv = lhsDivisibility.udiv() % intVal.getZExtValue() == 0 |
| ? lhsDivisibility.udiv() / intVal.getZExtValue() |
| : 1; |
| uint64_t divSDiv = |
| lhsDivisibility.sdiv() % std::abs(intVal.getSExtValue()) == 0 |
| ? lhsDivisibility.sdiv() / std::abs(intVal.getSExtValue()) |
| : 1; |
| |
| setResultDivs(divOp, IREE::Util::ConstantIntDivisibility(divUDiv, divSDiv)); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // ValueBoundsOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| /// For some reason, this interface has to be done as an external model. |
| struct UtilAssumeIntValueBoundsOpInterface |
| : ValueBoundsOpInterface::ExternalModel<UtilAssumeIntValueBoundsOpInterface, |
| IREE::Util::AssumeIntOp> { |
| void populateBoundsForIndexValue(Operation *op, Value value, |
| ValueBoundsConstraintSet &cstr) const { |
| auto assumeOp = cast<IREE::Util::AssumeIntOp>(op); |
| auto result = cast<OpResult>(value); |
| assert(result.getOwner() == op && "value is a result of this index op"); |
| auto [min, max] = |
| assumeOp.getUnionedUnsignedRange(result.getResultNumber()); |
| |
| std::optional<int64_t> udiv = |
| assumeOp.getUnionedUnsignedDivisor(result.getResultNumber()); |
| |
| if (min) { |
| cstr.bound(result) >= *min; |
| } |
| if (max) { |
| cstr.bound(result) <= *max; |
| } |
| if (udiv) { |
| // To represent the divisibility guarantee, emit a bound clamping the |
| // value to the udiv value. i.e. |
| // |
| // v == floordiv(v, udiv) * udiv |
| // |
| // Mod/divide folders can cleanup such terms with the appropriate bounds |
| // query. |
| AffineExpr expr = |
| cstr.getExpr(assumeOp.getOperand(result.getResultNumber())); |
| AffineExpr udivCst = |
| getAffineConstantExpr(udiv.value(), op->getContext()); |
| AffineExpr clampExpr = expr.floorDiv(udivCst) * udivCst; |
| cstr.bound(result) == clampExpr; |
| } |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // GlobalOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| struct GlobalOpInterfaceExternalModel |
| : IREE::Util::GlobalOpInterface::ExternalModel< |
| GlobalOpInterfaceExternalModel, ml_program::GlobalOp> { |
| Attribute getGlobalInitialValue(Operation *op) const { |
| return cast<ml_program::GlobalOp>(op).getValueAttr(); |
| } |
| void setGlobalInitialValue(Operation *op, Attribute value) const { |
| if (value) { |
| cast<ml_program::GlobalOp>(op).setValueAttr(value); |
| } else { |
| cast<ml_program::GlobalOp>(op).removeValueAttr(); |
| } |
| } |
| |
| IREE::Util::InliningPolicyAttrInterface |
| getGlobalInliningPolicy(Operation *op) const { |
| if (op->hasAttr("noinline")) { |
| return IREE::Util::InlineNeverAttr::get(op->getContext()); |
| } |
| return {}; |
| } |
| void |
| setGlobalInliningPolicy(Operation *op, |
| IREE::Util::InliningPolicyAttrInterface value) const { |
| if (isa_and_nonnull<IREE::Util::InlineNeverAttr>(value)) { |
| op->setAttr("noinline", UnitAttr::get(op->getContext())); |
| } else { |
| op->removeAttr("noinline"); |
| } |
| } |
| |
| IREE::Util::GlobalLoadOpInterface createLoadOp(Operation *op, Location loc, |
| OpBuilder &builder) const { |
| auto globalOp = cast<ml_program::GlobalOp>(op); |
| if (globalOp.getIsMutable()) { |
| return cast<IREE::Util::GlobalLoadOpInterface>( |
| ml_program::GlobalLoadOp::create(builder, loc, globalOp.getType(), |
| FlatSymbolRefAttr::get(globalOp)) |
| .getOperation()); |
| } else { |
| return cast<IREE::Util::GlobalLoadOpInterface>( |
| ml_program::GlobalLoadConstOp::create( |
| builder, loc, globalOp.getType(), |
| FlatSymbolRefAttr::get(globalOp)) |
| .getOperation()); |
| } |
| } |
| |
| IREE::Util::GlobalStoreOpInterface createStoreOp(Operation *op, Location loc, |
| Value value, |
| OpBuilder &builder) const { |
| auto globalOp = cast<ml_program::GlobalOp>(op); |
| return cast<IREE::Util::GlobalStoreOpInterface>( |
| ml_program::GlobalStoreOp ::create( |
| builder, loc, FlatSymbolRefAttr::get(globalOp), value) |
| .getOperation()); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // NumericCastOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| // Since all details of the interface are provided via default implementations, |
| // we can just have one templated external model to apply per op, vs one |
| // explicit model per op. |
| struct GenericNumericCastExternalModel { |
| template <typename OpTy> |
| struct ExternalModel |
| : IREE::Util::NumericCastOpInterface::ExternalModel<ExternalModel<OpTy>, |
| OpTy> {}; |
| |
| template <typename OpTy> |
| static void add(MLIRContext *context) { |
| OpTy::template attachInterface<ExternalModel<OpTy>>(*context); |
| } |
| |
| template <typename OpTy1, typename OpTy2, typename... More> |
| static void add(MLIRContext *context) { |
| add<OpTy1>(context); |
| add<OpTy2, More...>(context); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // TiedOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| struct InsertSliceOpTiedOpInterface |
| : IREE::Util::TiedOpInterface::ExternalModel<InsertSliceOpTiedOpInterface, |
| tensor::InsertSliceOp> { |
| Value getTiedResult(Operation *op, unsigned resultIndex) const { |
| auto insertSliceOp = cast<tensor::InsertSliceOp>(op); |
| return IREE::Util::TiedOpInterface::findTiedBaseValue( |
| insertSliceOp.getDest()); |
| } |
| |
| ::std::optional<unsigned> |
| getTiedResultOperandIndex(Operation *op, unsigned resultIndex) const { |
| return {1}; // dest |
| } |
| |
| SmallVector<int64_t> getTiedResultOperandIndices(Operation *op) const { |
| return {1}; // dest |
| } |
| }; |
| |
| template <typename OpTy> |
| struct LinalgOpTiedOpInterface |
| : IREE::Util::TiedOpInterface::ExternalModel<LinalgOpTiedOpInterface<OpTy>, |
| OpTy> { |
| Value getTiedResult(Operation *op, unsigned resultIndex) const { |
| auto linalgOp = cast<OpTy>(op); |
| return IREE::Util::TiedOpInterface::findTiedBaseValue( |
| linalgOp.getDpsInits()[resultIndex]); |
| } |
| |
| ::std::optional<unsigned> |
| getTiedResultOperandIndex(Operation *op, unsigned resultIndex) const { |
| auto linalgOp = cast<OpTy>(op); |
| return {linalgOp.getDpsInitsMutable()[resultIndex].getOperandNumber()}; |
| } |
| |
| SmallVector<int64_t> getTiedResultOperandIndices(Operation *op) const { |
| SmallVector<int64_t> result; |
| for (unsigned i = 0; i < op->getNumResults(); ++i) { |
| result.push_back(*getTiedResultOperandIndex(op, i)); |
| } |
| return result; |
| } |
| }; |
| |
| /// Helper structure that iterates over all LinalgOps in `OpTys` and registers |
| /// the `TiedOpInterface` with each of them. |
| template <typename... Ops> |
| struct LinalgOpTiedOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (void)std::initializer_list<int>{ |
| 0, |
| (Ops::template attachInterface<LinalgOpTiedOpInterface<Ops>>(*context), |
| 0)...}; |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // HoistableOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| template <typename OpTy> |
| struct UnhoistableOpInterface : IREE::Util::HoistableOpInterface::ExternalModel< |
| UnhoistableOpInterface<OpTy>, OpTy> { |
| bool isHoistableOp(Operation *) const { return false; } |
| bool isHoistableLeafOp(Operation *) const { return false; } |
| }; |
| |
| template <typename OpTy> |
| struct HoistableNonLeafOpInterface |
| : IREE::Util::HoistableOpInterface::ExternalModel< |
| HoistableNonLeafOpInterface<OpTy>, OpTy> { |
| bool isHoistableLeafOp(Operation *) const { return false; } |
| }; |
| |
| // The default interface is always hoistable. This acts as an override |
| // for other default hoistability checks as the interface is checked |
| // first. |
| template <typename OpTy> |
| struct AlwaysHoistableOpInterface |
| : IREE::Util::HoistableOpInterface::ExternalModel< |
| AlwaysHoistableOpInterface<OpTy>, OpTy> {}; |
| |
| template <typename OpTy> |
| struct HoistableLinalgOpInterface |
| : IREE::Util::HoistableOpInterface::ExternalModel< |
| HoistableLinalgOpInterface<OpTy>, OpTy> { |
| bool isHoistableOp(Operation *) const { return true; } |
| |
| // Determines if a linalg op is a hoistable leaf, based on heuristics. |
| bool isHoistableLeafOp(Operation *op) const { |
| // Don't hoist bit extend ops because fusing them with their |
| // consumers prevents materializing the high bit-width tensor and they |
| // preform very little real computation. |
| if (IREE::LinalgExt::isBitExtendOp(op)) { |
| return false; |
| } |
| |
| // Hoist all non-generic linalg ops except for fill ops which should be |
| // fused with their consumers. |
| auto genericOp = dyn_cast<linalg::GenericOp>(op); |
| if (!genericOp) { |
| return !isa<linalg::FillOp>(op); |
| } |
| |
| // Don't hoist ops with no tensor inputs. They are likely to be fill-like |
| // or sequences (from `linalg.index`) which can be fused with their |
| // consumers. |
| if (IREE::LinalgExt::hasOnlyScalarInputs(genericOp)) { |
| return false; |
| } |
| |
| // Don't hoist broadcast-like ops because fusing them makes the new |
| // op cheaper. |
| if (linalg::isaBroadcastOpInterface(genericOp).has_value()) { |
| return false; |
| } |
| |
| // Hoist all other ops. |
| return true; |
| } |
| bool isAtomicallyHoistableOp(Operation *) const { return true; } |
| bool isOperandHoistable(Operation *, OpOperand *) const { return true; } |
| }; |
| |
| /// Helper structures that iterates over all Op types in `OpTys` and registers |
| /// the associated Hoistable___OpInterface. |
| template <typename... Ops> |
| struct UnhoistableOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (Ops::template attachInterface<UnhoistableOpInterface<Ops>>(*context), ...); |
| } |
| }; |
| |
| template <typename... Ops> |
| struct HoistableNonLeafOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (Ops::template attachInterface<HoistableNonLeafOpInterface<Ops>>(*context), |
| ...); |
| } |
| }; |
| |
| template <typename... Ops> |
| struct AlwaysHoistableOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (Ops::template attachInterface<AlwaysHoistableOpInterface<Ops>>(*context), |
| ...); |
| } |
| }; |
| |
| template <typename... Ops> |
| struct HoistableLinalgOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (Ops::template attachInterface<HoistableLinalgOpInterface<Ops>>(*context), |
| ...); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // MutableRegionBranchOpInterface |
| //===----------------------------------------------------------------------===// |
| |
| // External model for scf.for operation. |
| struct SCFForOpMutableRegionBranchOpInterface |
| : IREE::Util::MutableRegionBranchOpInterface::ExternalModel< |
| SCFForOpMutableRegionBranchOpInterface, scf::ForOp> { |
| Operation *rebuildWithExpandedTypes( |
| Operation *op, |
| llvm::function_ref<void(Type, SmallVectorImpl<Type> &)> expandTypeFn, |
| llvm::function_ref<void(Value, SmallVectorImpl<Value> &, OpBuilder &)> |
| expandOperandFn, |
| std::optional< |
| llvm::function_ref<Value(Location, Value, ValueRange, OpBuilder &)>> |
| wrapExpandedBlockArgFn, |
| llvm::function_ref<void(Region &, bool /*canModifyEntryBlock*/)> |
| expandRegionFn, |
| OpBuilder &builder) const { |
| auto forOp = cast<scf::ForOp>(op); |
| Location loc = forOp.getLoc(); |
| |
| // Expand iter_args operands (lb, ub, step are never expanded). |
| SmallVector<Value> newIterArgs; |
| for (Value iterArg : forOp.getInitArgs()) { |
| expandOperandFn(iterArg, newIterArgs, builder); |
| } |
| |
| // Expand result types. |
| SmallVector<Type> newResultTypes; |
| for (Type resultType : forOp.getResultTypes()) { |
| expandTypeFn(resultType, newResultTypes); |
| } |
| |
| // Create new for loop with expanded signature. |
| auto newForOp = |
| scf::ForOp::create(builder, loc, forOp.getLowerBound(), |
| forOp.getUpperBound(), forOp.getStep(), newIterArgs); |
| |
| // Move the body from old to new. |
| newForOp.getBodyRegion().takeBody(forOp.getBodyRegion()); |
| |
| // The new op now has the old body but with unexpanded block arguments. |
| // We need to update the block arguments to match the expanded iter args. |
| Block &body = *newForOp.getBody(); |
| |
| // Insert new block arguments for expanded types. |
| // Start from the back to preserve indices. |
| // Note we skip the induction var at 0. |
| for (int i = body.getNumArguments() - 1; i >= 1; --i) { |
| auto arg = body.getArgument(i); |
| SmallVector<Type> expandedTypes; |
| expandTypeFn(arg.getType(), expandedTypes); |
| if (expandedTypes.size() > 1) { |
| // This type expands to multiple values. |
| // Insert new arguments after the current one. |
| SmallVector<Value> expandedArgs; |
| expandedArgs.push_back(arg); |
| for (unsigned j = 1; j < expandedTypes.size(); ++j) { |
| expandedArgs.push_back( |
| body.insertArgument(i + j, expandedTypes[j], arg.getLoc())); |
| } |
| |
| // If wrapper callback provided, create replacement value and update |
| // uses. |
| if (wrapExpandedBlockArgFn) { |
| OpBuilder blockBuilder(&body, body.begin()); |
| Value replacement = (*wrapExpandedBlockArgFn)( |
| arg.getLoc(), arg, expandedArgs, blockBuilder); |
| arg.replaceAllUsesExcept(replacement, replacement.getDefiningOp()); |
| } |
| } |
| } |
| |
| // Recursively expand operations in the body. |
| expandRegionFn(newForOp.getBodyRegion(), /*canModifyEntryBlock=*/false); |
| |
| // Update the yield to match expanded result types. |
| if (body.mightHaveTerminator()) { |
| auto *terminator = body.getTerminator(); |
| if (auto yieldOp = dyn_cast<scf::YieldOp>(terminator)) { |
| SmallVector<Value> newYieldOperands; |
| builder.setInsertionPoint(yieldOp); |
| for (Value operand : yieldOp.getOperands()) { |
| expandOperandFn(operand, newYieldOperands, builder); |
| } |
| scf::YieldOp::create(builder, yieldOp.getLoc(), newYieldOperands); |
| yieldOp.erase(); |
| } |
| } |
| |
| return newForOp; |
| } |
| |
| llvm::BitVector getPreservedBlockArguments(Operation *op, |
| unsigned regionIndex) const { |
| auto forOp = cast<scf::ForOp>(op); |
| llvm::BitVector preserved(forOp.getBody()->getNumArguments()); |
| preserved.set(0); // Preserve induction variable. |
| return preserved; |
| } |
| |
| OperandRange getRegionEntryOperands(Operation *op, |
| unsigned regionIndex) const { |
| auto forOp = cast<scf::ForOp>(op); |
| return forOp.getInitArgs(); |
| } |
| |
| ResultRange getRegionExitResults(Operation *op, unsigned regionIndex) const { |
| return op->getResults(); |
| } |
| |
| OperandRange getExpandableTerminatorOperands(Operation *op, |
| Operation *terminator, |
| unsigned regionIndex) const { |
| // For scf.yield, all operands are expandable. |
| return terminator->getOperands(); |
| } |
| }; |
| |
| // External model for scf.if operation. |
| struct SCFIfOpMutableRegionBranchOpInterface |
| : IREE::Util::MutableRegionBranchOpInterface::ExternalModel< |
| SCFIfOpMutableRegionBranchOpInterface, scf::IfOp> { |
| Operation *rebuildWithExpandedTypes( |
| Operation *op, |
| llvm::function_ref<void(Type, SmallVectorImpl<Type> &)> expandTypeFn, |
| llvm::function_ref<void(Value, SmallVectorImpl<Value> &, OpBuilder &)> |
| expandOperandFn, |
| std::optional< |
| llvm::function_ref<Value(Location, Value, ValueRange, OpBuilder &)>> |
| wrapExpandedBlockArgFn, |
| llvm::function_ref<void(Region &, bool /*canModifyEntryBlock*/)> |
| expandRegionFn, |
| OpBuilder &builder) const { |
| // Note: scf.if has no block arguments, so wrapExpandedBlockArgFn is unused. |
| (void)wrapExpandedBlockArgFn; |
| auto ifOp = cast<scf::IfOp>(op); |
| Location loc = ifOp.getLoc(); |
| |
| // Expand result types. |
| SmallVector<Type> newResultTypes; |
| for (Type resultType : ifOp.getResultTypes()) { |
| expandTypeFn(resultType, newResultTypes); |
| } |
| |
| // Create new if op with expanded result types. |
| auto newIfOp = |
| scf::IfOp::create(builder, loc, newResultTypes, ifOp.getCondition(), |
| /*withElseRegion=*/!ifOp.getElseRegion().empty()); |
| |
| // Copy regions from old to new op. |
| // Use takeBody to move the region contents. |
| newIfOp.getThenRegion().takeBody(ifOp.getThenRegion()); |
| if (!ifOp.getElseRegion().empty()) { |
| newIfOp.getElseRegion().takeBody(ifOp.getElseRegion()); |
| } |
| |
| // Now recursively expand operations in the regions. |
| // This will handle the expansion of operations inside. |
| expandRegionFn(newIfOp.getThenRegion(), /*canModifyEntryBlock=*/false); |
| if (!newIfOp.getElseRegion().empty()) { |
| expandRegionFn(newIfOp.getElseRegion(), /*canModifyEntryBlock=*/false); |
| } |
| |
| // Now we need to update the yield operations to match the expanded result |
| // types. The yield ops need to yield expanded values for resource results. |
| auto updateYield = [&](Block &block) { |
| if (!block.mightHaveTerminator()) { |
| return; |
| } |
| auto *terminator = block.getTerminator(); |
| if (!terminator) { |
| return; |
| } |
| auto yieldOp = dyn_cast<scf::YieldOp>(terminator); |
| if (!yieldOp) { |
| return; |
| } |
| |
| SmallVector<Value> newYieldOperands; |
| builder.setInsertionPoint(yieldOp); |
| for (Value operand : yieldOp.getOperands()) { |
| expandOperandFn(operand, newYieldOperands, builder); |
| } |
| scf::YieldOp::create(builder, yieldOp.getLoc(), newYieldOperands); |
| yieldOp.erase(); |
| }; |
| |
| updateYield(newIfOp.getThenRegion().front()); |
| if (!newIfOp.getElseRegion().empty()) { |
| updateYield(newIfOp.getElseRegion().front()); |
| } |
| |
| return newIfOp; |
| } |
| |
| llvm::BitVector getPreservedBlockArguments(Operation *op, |
| unsigned regionIndex) const { |
| // scf.if has no block arguments. |
| return llvm::BitVector(0); |
| } |
| |
| OperandRange getRegionEntryOperands(Operation *op, |
| unsigned regionIndex) const { |
| // No operands map to block args. |
| return OperandRange(op->operand_end(), op->operand_end()); |
| } |
| |
| ResultRange getRegionExitResults(Operation *op, unsigned regionIndex) const { |
| return op->getResults(); |
| } |
| |
| OperandRange getExpandableTerminatorOperands(Operation *op, |
| Operation *terminator, |
| unsigned regionIndex) const { |
| // For scf.yield, all operands are expandable. |
| return terminator->getOperands(); |
| } |
| }; |
| |
| // External model for scf.while operation. |
| struct SCFWhileOpMutableRegionBranchOpInterface |
| : IREE::Util::MutableRegionBranchOpInterface::ExternalModel< |
| SCFWhileOpMutableRegionBranchOpInterface, scf::WhileOp> { |
| Operation *rebuildWithExpandedTypes( |
| Operation *op, |
| llvm::function_ref<void(Type, SmallVectorImpl<Type> &)> expandTypeFn, |
| llvm::function_ref<void(Value, SmallVectorImpl<Value> &, OpBuilder &)> |
| expandOperandFn, |
| std::optional< |
| llvm::function_ref<Value(Location, Value, ValueRange, OpBuilder &)>> |
| wrapExpandedBlockArgFn, |
| llvm::function_ref<void(Region &, bool /*canModifyEntryBlock*/)> |
| expandRegionFn, |
| OpBuilder &builder) const { |
| auto whileOp = cast<scf::WhileOp>(op); |
| Location loc = whileOp.getLoc(); |
| |
| // Expand initial operands. |
| SmallVector<Value> newInits; |
| for (Value init : whileOp.getInits()) { |
| expandOperandFn(init, newInits, builder); |
| } |
| |
| // Expand result types. |
| SmallVector<Type> newResultTypes; |
| for (Type resultType : whileOp.getResultTypes()) { |
| expandTypeFn(resultType, newResultTypes); |
| } |
| |
| // scf.while builder needs the before and after region builders. |
| // We'll create it without them and then manually populate. |
| auto newWhileOp = |
| scf::WhileOp::create(builder, loc, newResultTypes, newInits, |
| /*beforeBuilder=*/nullptr, |
| /*afterBuilder=*/nullptr); |
| |
| // Move the regions from old to new. |
| newWhileOp.getBefore().takeBody(whileOp.getBefore()); |
| newWhileOp.getAfter().takeBody(whileOp.getAfter()); |
| |
| // Helper lambda to expand block arguments and call wrapper. |
| auto expandBlockArgs = [&](Block &block) { |
| for (int i = block.getNumArguments() - 1; i >= 0; --i) { |
| auto arg = block.getArgument(i); |
| SmallVector<Type> expandedTypes; |
| expandTypeFn(arg.getType(), expandedTypes); |
| if (expandedTypes.size() > 1) { |
| // This type expands to multiple values. |
| // Insert new arguments after the current one. |
| SmallVector<Value> expandedArgs; |
| expandedArgs.push_back(arg); |
| for (unsigned j = 1; j < expandedTypes.size(); ++j) { |
| expandedArgs.push_back( |
| block.insertArgument(i + j, expandedTypes[j], arg.getLoc())); |
| } |
| |
| // If wrapper callback provided, create replacement value and update |
| // uses. |
| if (wrapExpandedBlockArgFn) { |
| OpBuilder blockBuilder(&block, block.begin()); |
| Value replacement = (*wrapExpandedBlockArgFn)( |
| arg.getLoc(), arg, expandedArgs, blockBuilder); |
| arg.replaceAllUsesExcept(replacement, replacement.getDefiningOp()); |
| } |
| } |
| } |
| }; |
| |
| // Expand block arguments in both regions. |
| Block &beforeBlock = newWhileOp.getBefore().front(); |
| expandBlockArgs(beforeBlock); |
| |
| Block &afterBlock = newWhileOp.getAfter().front(); |
| expandBlockArgs(afterBlock); |
| |
| // Recursively expand operations in both regions. |
| expandRegionFn(newWhileOp.getBefore(), /*canModifyEntryBlock=*/false); |
| expandRegionFn(newWhileOp.getAfter(), /*canModifyEntryBlock=*/false); |
| |
| // Rebuild the condition with expanded operands. |
| if (beforeBlock.mightHaveTerminator()) { |
| if (auto condOp = |
| dyn_cast<scf::ConditionOp>(beforeBlock.getTerminator())) { |
| SmallVector<Value> newCondArgs; |
| builder.setInsertionPoint(condOp); |
| for (Value operand : condOp.getArgs()) { |
| expandOperandFn(operand, newCondArgs, builder); |
| } |
| scf::ConditionOp::create(builder, condOp.getLoc(), |
| condOp.getCondition(), newCondArgs); |
| condOp.erase(); |
| } |
| } |
| |
| // Rebuild the yield in the after region with expanded operands. |
| if (afterBlock.mightHaveTerminator()) { |
| if (auto yieldOp = dyn_cast<scf::YieldOp>(afterBlock.getTerminator())) { |
| SmallVector<Value> newYieldOperands; |
| builder.setInsertionPoint(yieldOp); |
| for (Value operand : yieldOp.getOperands()) { |
| expandOperandFn(operand, newYieldOperands, builder); |
| } |
| scf::YieldOp::create(builder, yieldOp.getLoc(), newYieldOperands); |
| yieldOp.erase(); |
| } |
| } |
| |
| return newWhileOp; |
| } |
| |
| llvm::BitVector getPreservedBlockArguments(Operation *op, |
| unsigned regionIndex) const { |
| // All block arguments in scf.while can be expanded. |
| auto whileOp = cast<scf::WhileOp>(op); |
| Region ®ion = |
| regionIndex == 0 ? whileOp.getBefore() : whileOp.getAfter(); |
| return llvm::BitVector(region.front().getNumArguments()); |
| } |
| |
| OperandRange getRegionEntryOperands(Operation *op, |
| unsigned regionIndex) const { |
| auto whileOp = cast<scf::WhileOp>(op); |
| if (regionIndex == 0) { |
| // Before region gets the init operands. |
| return whileOp.getInits(); |
| } |
| // After region receives operands from the condition region's scf.condition |
| // op, which are not direct operands of the scf.while itself. Return empty |
| // range. |
| return OperandRange(op->operand_end(), op->operand_end()); |
| } |
| |
| ResultRange getRegionExitResults(Operation *op, unsigned regionIndex) const { |
| if (regionIndex == 1) { |
| // After region produces the results. |
| return op->getResults(); |
| } |
| // Before region doesn't directly produce results. |
| return ResultRange(op->result_end(), op->result_end()); |
| } |
| |
| OperandRange getExpandableTerminatorOperands(Operation *op, |
| Operation *terminator, |
| unsigned regionIndex) const { |
| if (auto condOp = dyn_cast<scf::ConditionOp>(terminator)) { |
| // For condition, expand the args but not the condition boolean. |
| return condOp.getArgs(); |
| } |
| // For scf.yield, all operands are expandable. |
| return terminator->getOperands(); |
| } |
| }; |
| |
| // External model for scf.index_switch operation. |
| struct SCFIndexSwitchOpMutableRegionBranchOpInterface |
| : IREE::Util::MutableRegionBranchOpInterface::ExternalModel< |
| SCFIndexSwitchOpMutableRegionBranchOpInterface, scf::IndexSwitchOp> { |
| Operation *rebuildWithExpandedTypes( |
| Operation *op, |
| llvm::function_ref<void(Type, SmallVectorImpl<Type> &)> expandTypeFn, |
| llvm::function_ref<void(Value, SmallVectorImpl<Value> &, OpBuilder &)> |
| expandOperandFn, |
| std::optional< |
| llvm::function_ref<Value(Location, Value, ValueRange, OpBuilder &)>> |
| wrapExpandedBlockArgFn, |
| llvm::function_ref<void(Region &, bool /*canModifyEntryBlock*/)> |
| expandRegionFn, |
| OpBuilder &builder) const { |
| // Note: scf.index_switch has no block arguments, so wrapExpandedBlockArgFn |
| // is unused. |
| (void)wrapExpandedBlockArgFn; |
| auto switchOp = cast<scf::IndexSwitchOp>(op); |
| Location loc = switchOp.getLoc(); |
| |
| // Expand result types. |
| SmallVector<Type> newResultTypes; |
| for (Type resultType : switchOp.getResultTypes()) { |
| expandTypeFn(resultType, newResultTypes); |
| } |
| |
| // Create new index_switch op with expanded result types. |
| auto newSwitchOp = scf::IndexSwitchOp::create( |
| builder, loc, newResultTypes, switchOp.getArg(), switchOp.getCases(), |
| switchOp.getCases().size()); |
| |
| // Clone each case region. |
| for (unsigned i = 0; i < switchOp.getCaseRegions().size(); ++i) { |
| Region &oldRegion = switchOp.getCaseRegions()[i]; |
| Region &newRegion = newSwitchOp.getCaseRegions()[i]; |
| if (oldRegion.empty()) { |
| continue; |
| } |
| builder.createBlock(&newRegion); |
| builder.setInsertionPointToStart(&newRegion.front()); |
| |
| IRMapping mapping; |
| for (Operation &op : oldRegion.front()) { |
| if (auto yieldOp = dyn_cast<scf::YieldOp>(op)) { |
| SmallVector<Value> newYieldOperands; |
| for (Value operand : yieldOp.getOperands()) { |
| if (mapping.contains(operand)) { |
| expandOperandFn(mapping.lookup(operand), newYieldOperands, |
| builder); |
| } else { |
| expandOperandFn(operand, newYieldOperands, builder); |
| } |
| } |
| scf::YieldOp::create(builder, yieldOp.getLoc(), newYieldOperands); |
| } else { |
| builder.clone(op, mapping); |
| } |
| } |
| } |
| |
| // Clone default region. |
| Region &oldDefault = switchOp.getDefaultRegion(); |
| Region &newDefault = newSwitchOp.getDefaultRegion(); |
| if (!oldDefault.empty()) { |
| builder.createBlock(&newDefault); |
| builder.setInsertionPointToStart(&newDefault.front()); |
| |
| IRMapping mapping; |
| for (Operation &op : oldDefault.front()) { |
| if (auto yieldOp = dyn_cast<scf::YieldOp>(op)) { |
| SmallVector<Value> newYieldOperands; |
| for (Value operand : yieldOp.getOperands()) { |
| if (mapping.contains(operand)) { |
| expandOperandFn(mapping.lookup(operand), newYieldOperands, |
| builder); |
| } else { |
| expandOperandFn(operand, newYieldOperands, builder); |
| } |
| } |
| scf::YieldOp::create(builder, yieldOp.getLoc(), newYieldOperands); |
| } else { |
| builder.clone(op, mapping); |
| } |
| } |
| } |
| |
| // Recursively expand operations in all regions. |
| for (Region ®ion : newSwitchOp.getCaseRegions()) { |
| if (!region.empty()) { |
| expandRegionFn(region, /*canModifyEntryBlock=*/false); |
| } |
| } |
| if (!newSwitchOp.getDefaultRegion().empty()) { |
| expandRegionFn(newSwitchOp.getDefaultRegion(), |
| /*canModifyEntryBlock=*/false); |
| } |
| |
| return newSwitchOp; |
| } |
| |
| llvm::BitVector getPreservedBlockArguments(Operation *op, |
| unsigned regionIndex) const { |
| // scf.index_switch has no block arguments. |
| return llvm::BitVector(0); |
| } |
| |
| OperandRange getRegionEntryOperands(Operation *op, |
| unsigned regionIndex) const { |
| // No operands map to block args. |
| return OperandRange(op->operand_end(), op->operand_end()); |
| } |
| |
| ResultRange getRegionExitResults(Operation *op, unsigned regionIndex) const { |
| return op->getResults(); |
| } |
| |
| OperandRange getExpandableTerminatorOperands(Operation *op, |
| Operation *terminator, |
| unsigned regionIndex) const { |
| // For scf.yield, all operands are expandable. |
| return terminator->getOperands(); |
| } |
| }; |
| |
| // Hoistable interface for region-containing control flow operations. |
| // Control flow is hoistable if control operands are constant and |
| // nested operations are hoistable (checked via atomic hoisting). |
| template <typename OpTy> |
| struct RegionControlFlowHoistableOpInterface |
| : IREE::Util::HoistableOpInterface::ExternalModel< |
| RegionControlFlowHoistableOpInterface<OpTy>, OpTy> { |
| bool isHoistableOp(Operation *op) const { |
| // Control flow is hoistable if all nested operations are hoistable. |
| for (Region ®ion : op->getRegions()) { |
| WalkResult result = region.walk([](Operation *nestedOp) { |
| // Check if nested op is hoistable. |
| bool isHoistable = false; |
| if (auto hoistable = |
| dyn_cast<IREE::Util::HoistableOpInterface>(nestedOp)) { |
| isHoistable = hoistable.isHoistableOp(); |
| } else { |
| // Ops without interface must be memory-effect-free to be hoistable. |
| isHoistable = mlir::isMemoryEffectFree(nestedOp); |
| } |
| if (!isHoistable) { |
| return WalkResult::interrupt(); |
| } |
| // Don't descend into IsolatedFromAbove ops - treat them atomically. |
| return nestedOp->hasTrait<OpTrait::IsIsolatedFromAbove>() |
| ? WalkResult::skip() |
| : WalkResult::advance(); |
| }); |
| if (result.wasInterrupted()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool isHoistableLeafOp(Operation *) const { return false; } |
| bool isAtomicallyHoistableOp(Operation *) const { return true; } |
| bool isOperandHoistable(Operation *, OpOperand *) const { return true; } |
| }; |
| |
| template <typename... Ops> |
| struct RegionControlFlowHoistableOpInterfaceHelper { |
| static void registerOpInterface(MLIRContext *context) { |
| (Ops::template attachInterface<RegionControlFlowHoistableOpInterface<Ops>>( |
| *context), |
| ...); |
| } |
| }; |
| |
| } // namespace |
| |
| void registerUtilExternalModels(DialectRegistry ®istry) { |
| // Must ensure that any dependent dialects are registered. |
| registry.insert<affine::AffineDialect>(); |
| registry.insert<arith::ArithDialect>(); |
| registry.insert<linalg::LinalgDialect>(); |
| registry.insert<ml_program::MLProgramDialect>(); |
| registry.insert<scf::SCFDialect>(); |
| registry.insert<tensor::TensorDialect>(); |
| |
| registry.addExtension( |
| +[](MLIRContext *context, ml_program::MLProgramDialect *dialect) { |
| ml_program::GlobalOp::attachInterface<GlobalOpInterfaceExternalModel>( |
| *context); |
| }); |
| |
| registry.addExtension(+[](MLIRContext *context, |
| arith::ArithDialect *dialect) { |
| GenericNumericCastExternalModel::add< |
| arith::BitcastOp, arith::ExtFOp, arith::ExtUIOp, arith::ExtSIOp, |
| arith::FPToSIOp, arith::FPToUIOp, arith::IndexCastOp, arith::TruncFOp, |
| arith::TruncIOp, arith::SIToFPOp, arith::UIToFPOp>(context); |
| arith::ConstantOp::attachInterface< |
| ArithConstantInferIntDivisibilityOpInterface>(*context); |
| arith::MulIOp::attachInterface<ArithMulIInferIntDivisibilityOpInterface>( |
| *context); |
| arith::DivUIOp::attachInterface<ArithDivUIInferIntDivisibilityOpInterface>( |
| *context); |
| arith::AddIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::AddIOp>>(*context); |
| arith::SubIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::SubIOp>>(*context); |
| arith::MinUIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::MinUIOp>>( |
| *context); |
| arith::MaxUIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::MaxUIOp>>( |
| *context); |
| arith::MinSIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::MinSIOp>>( |
| *context); |
| arith::MaxSIOp::attachInterface< |
| ArithBinaryGCDInferIntDivisibilityOpInterface<arith::MaxSIOp>>( |
| *context); |
| arith::SelectOp::attachInterface< |
| ArithSelectInferIntDivisibilityOpInterface>(*context); |
| }); |
| |
| registry.addExtension( |
| +[](MLIRContext *context, affine::AffineDialect *dialect) { |
| affine::AffineApplyOp::attachInterface< |
| AffineApplyInferIntDivisibilityOpInterface>(*context); |
| affine::AffineMinOp::attachInterface< |
| AffineMinInferIntDivisibilityOpInterface>(*context); |
| affine::AffineMaxOp::attachInterface< |
| AffineMaxInferIntDivisibilityOpInterface>(*context); |
| affine::AffineDelinearizeIndexOp::attachInterface< |
| AffineDelinearizeIndexInferIntDivisibilityOpInterface>(*context); |
| }); |
| |
| registry.addExtension( |
| +[](MLIRContext *context, tensor::TensorDialect *dialect) { |
| tensor::InsertSliceOp::attachInterface<InsertSliceOpTiedOpInterface>( |
| *context); |
| }); |
| |
| registry.addExtension( |
| +[](MLIRContext *context, linalg::LinalgDialect *dialect) { |
| // Register all Linalg structured ops. `LinalgOp` is an interface and it |
| // is not possible to attach an external interface to an existing |
| // interface. Therefore, attach the `TiedOpInterface` to all ops |
| // one-by-one. |
| LinalgOpTiedOpInterfaceHelper< |
| #define GET_OP_LIST |
| #include "mlir/Dialect/Linalg/IR/LinalgStructuredOps.cpp.inc" |
| >::registerOpInterface(context); |
| }); |
| |
| registry.addExtension(+[](MLIRContext *context, |
| IREE::LinalgExt::IREELinalgExtDialect *dialect) { |
| LinalgOpTiedOpInterfaceHelper< |
| #define GET_OP_LIST |
| #include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.cpp.inc" |
| >::registerOpInterface(context); |
| }); |
| |
| // Hoistable Op Interface registration. |
| |
| // Register hoistable op interfaces for Encoding ops. |
| registry.addExtension( |
| +[](MLIRContext *context, IREE::Encoding::IREEEncodingDialect *dialect) { |
| UnhoistableOpInterfaceHelper< |
| IREE::Encoding::SetEncodingOp>::registerOpInterface(context); |
| }); |
| |
| // Register hoistable op interfaces for Flow ops. |
| registry.addExtension( |
| +[](MLIRContext *context, IREE::Flow::FlowDialect *dialect) { |
| UnhoistableOpInterfaceHelper< |
| IREE::Flow::DispatchWorkgroupCountOp>::registerOpInterface(context); |
| |
| AlwaysHoistableOpInterfaceHelper< |
| IREE::Flow::TensorEncodeOp>::registerOpInterface(context); |
| }); |
| |
| // Register hoistable op interfaces for linalg ops. |
| // We have a specific allow-list for Linalg ops because we want to consider |
| // new additions carefully. |
| registry.addExtension( |
| +[](MLIRContext *context, linalg::LinalgDialect *dialect) { |
| // Structured op implementations and a handful of pure ops are included. |
| // Notably: IndexOp is not included because it establishes a hidden |
| // dependency to the iterator and is non-const. |
| |
| // Register all LinalgOps ops. `LinalgOp` is an interface and it is |
| // not possible to attach an external interface to an existing |
| // interface. Therefore, attach the `HoistableLinalgOpInterface` to all |
| // ops one-by-one. |
| HoistableLinalgOpInterfaceHelper< |
| #define GET_OP_LIST |
| #include "mlir/Dialect/Linalg/IR/LinalgStructuredOps.cpp.inc" |
| >::registerOpInterface(context); |
| UnhoistableOpInterfaceHelper< |
| #define GET_OP_LIST |
| #include "mlir/Dialect/Linalg/IR/LinalgOps.cpp.inc" |
| >::registerOpInterface(context); |
| |
| AlwaysHoistableOpInterfaceHelper< |
| linalg::PackOp, linalg::UnPackOp>::registerOpInterface(context); |
| }); |
| // Register hoistable op interfaces for tensor ops. |
| registry.addExtension( |
| +[](MLIRContext *context, tensor::TensorDialect *dialect) { |
| // Never hoist empty and other pure metadata ops as a leaf. It's fine to |
| // hoist them as a part of a larger constant tree that does actual work. |
| HoistableNonLeafOpInterfaceHelper< |
| tensor::EmptyOp, tensor::ExpandShapeOp, tensor::CollapseShapeOp, |
| tensor::ExtractSliceOp>::registerOpInterface(context); |
| // Cases of trivial pack/unpack should be handled as canonicalizations |
| // before we get here, thus we're safe to always hoist. |
| AlwaysHoistableOpInterfaceHelper<tensor::PadOp>::registerOpInterface( |
| context); |
| }); |
| registry.addExtension( |
| +[](MLIRContext *context, IREE::Util::UtilDialect *dialect) { |
| IREE::Util::AssumeIntOp::attachInterface< |
| UtilAssumeIntValueBoundsOpInterface>(*context); |
| }); |
| |
| // Register MutableRegionBranchOpInterface for SCF ops. |
| // Register hoistable op interfaces for SCF control flow ops. |
| registry.addExtension(+[](MLIRContext *context, scf::SCFDialect *dialect) { |
| scf::ForOp::attachInterface<SCFForOpMutableRegionBranchOpInterface>( |
| *context); |
| scf::IfOp::attachInterface<SCFIfOpMutableRegionBranchOpInterface>(*context); |
| scf::WhileOp::attachInterface<SCFWhileOpMutableRegionBranchOpInterface>( |
| *context); |
| scf::IndexSwitchOp::attachInterface< |
| SCFIndexSwitchOpMutableRegionBranchOpInterface>(*context); |
| RegionControlFlowHoistableOpInterfaceHelper< |
| scf::ForOp, scf::IfOp, scf::WhileOp>::registerOpInterface(context); |
| }); |
| } |
| |
| } // namespace mlir::iree_compiler |