| //===- LoopSpecialization.cpp - scf.parallel/SCR.for specialization -------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Specializes parallel loops and for loops for easier unrolling and | |
| // vectorization. | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "mlir/Dialect/SCF/Transforms/Passes.h" | |
| #include "mlir/Dialect/Affine/Analysis/AffineStructures.h" | |
| #include "mlir/Dialect/Affine/IR/AffineOps.h" | |
| #include "mlir/Dialect/Arith/IR/Arith.h" | |
| #include "mlir/Dialect/SCF/IR/SCF.h" | |
| #include "mlir/Dialect/SCF/Transforms/Transforms.h" | |
| #include "mlir/Dialect/SCF/Utils/AffineCanonicalizationUtils.h" | |
| #include "mlir/Dialect/Utils/StaticValueUtils.h" | |
| #include "mlir/IR/AffineExpr.h" | |
| #include "mlir/IR/IRMapping.h" | |
| #include "mlir/IR/PatternMatch.h" | |
| #include "mlir/Transforms/GreedyPatternRewriteDriver.h" | |
| namespace mlir { | |
| #define GEN_PASS_DEF_SCFFORLOOPPEELING | |
| #define GEN_PASS_DEF_SCFFORLOOPSPECIALIZATION | |
| #define GEN_PASS_DEF_SCFPARALLELLOOPSPECIALIZATION | |
| #include "mlir/Dialect/SCF/Transforms/Passes.h.inc" | |
| } // namespace mlir | |
| using namespace mlir; | |
| using namespace mlir::affine; | |
| using scf::ForOp; | |
| using scf::ParallelOp; | |
| /// Rewrite a parallel loop with bounds defined by an affine.min with a constant | |
| /// into 2 loops after checking if the bounds are equal to that constant. This | |
| /// is beneficial if the loop will almost always have the constant bound and | |
| /// that version can be fully unrolled and vectorized. | |
| static void specializeParallelLoopForUnrolling(ParallelOp op) { | |
| SmallVector<int64_t, 2> constantIndices; | |
| constantIndices.reserve(op.getUpperBound().size()); | |
| for (auto bound : op.getUpperBound()) { | |
| auto minOp = bound.getDefiningOp<AffineMinOp>(); | |
| if (!minOp) | |
| return; | |
| int64_t minConstant = std::numeric_limits<int64_t>::max(); | |
| for (AffineExpr expr : minOp.getMap().getResults()) { | |
| if (auto constantIndex = dyn_cast<AffineConstantExpr>(expr)) | |
| minConstant = std::min(minConstant, constantIndex.getValue()); | |
| } | |
| if (minConstant == std::numeric_limits<int64_t>::max()) | |
| return; | |
| constantIndices.push_back(minConstant); | |
| } | |
| OpBuilder b(op); | |
| IRMapping map; | |
| Value cond; | |
| for (auto bound : llvm::zip(op.getUpperBound(), constantIndices)) { | |
| Value constant = | |
| arith::ConstantIndexOp::create(b, op.getLoc(), std::get<1>(bound)); | |
| Value cmp = arith::CmpIOp::create(b, op.getLoc(), arith::CmpIPredicate::eq, | |
| std::get<0>(bound), constant); | |
| cond = cond ? arith::AndIOp::create(b, op.getLoc(), cond, cmp) : cmp; | |
| map.map(std::get<0>(bound), constant); | |
| } | |
| auto ifOp = scf::IfOp::create(b, op.getLoc(), op.getResultTypes(), cond, | |
| /*addThenBlock=*/true, /*addElseBlock=*/true); | |
| OpBuilder thenBuilder = OpBuilder::atBlockEnd(&ifOp.getThenRegion().front()); | |
| Operation *thenOp = thenBuilder.clone(*op.getOperation(), map); | |
| scf::YieldOp::create(thenBuilder, op.getLoc(), thenOp->getResults()); | |
| OpBuilder elseBuilder = OpBuilder::atBlockEnd(&ifOp.getElseRegion().front()); | |
| Operation *elseOp = elseBuilder.clone(*op.getOperation()); | |
| scf::YieldOp::create(elseBuilder, op.getLoc(), elseOp->getResults()); | |
| op.replaceAllUsesWith(ifOp.getResults()); | |
| op.erase(); | |
| } | |
| /// Rewrite a for loop with bounds defined by an affine.min with a constant into | |
| /// 2 loops after checking if the bounds are equal to that constant. This is | |
| /// beneficial if the loop will almost always have the constant bound and that | |
| /// version can be fully unrolled and vectorized. | |
| static void specializeForLoopForUnrolling(ForOp op) { | |
| auto bound = op.getUpperBound(); | |
| auto minOp = bound.getDefiningOp<AffineMinOp>(); | |
| if (!minOp) | |
| return; | |
| AffineMap canonicalizedMap = minOp.getMap(); | |
| SmallVector<Value> operands(minOp.getMapOperands()); | |
| affine::canonicalizeMapAndOperands(&canonicalizedMap, &operands); | |
| int64_t minConstant = std::numeric_limits<int64_t>::max(); | |
| for (AffineExpr expr : canonicalizedMap.getResults()) { | |
| if (auto constantIndex = dyn_cast<AffineConstantExpr>(expr)) | |
| minConstant = std::min(minConstant, constantIndex.getValue()); | |
| } | |
| if (minConstant == std::numeric_limits<int64_t>::max()) | |
| return; | |
| OpBuilder b(op); | |
| IRMapping map; | |
| Value constant = arith::ConstantOp::create( | |
| b, op.getLoc(), | |
| IntegerAttr::get(op.getUpperBound().getType(), minConstant)); | |
| Value cond = arith::CmpIOp::create(b, op.getLoc(), arith::CmpIPredicate::eq, | |
| bound, constant); | |
| map.map(bound, constant); | |
| auto ifOp = scf::IfOp::create(b, op.getLoc(), op->getResultTypes(), cond, | |
| /*withElseRegion=*/true); | |
| OpBuilder thenB = ifOp.getThenBodyBuilder(); | |
| Operation *thenLoop = thenB.clone(*op.getOperation(), map); | |
| OpBuilder elseB = ifOp.getElseBodyBuilder(); | |
| Operation *elseLoop = elseB.clone(*op.getOperation()); | |
| if (op->getNumResults() > 0) { | |
| scf::YieldOp::create(thenB, op.getLoc(), thenLoop->getResults()); | |
| scf::YieldOp::create(elseB, op.getLoc(), elseLoop->getResults()); | |
| op->replaceAllUsesWith(ifOp.getResults()); | |
| } | |
| op.erase(); | |
| } | |
| /// Rewrite a for loop with bounds/step that potentially do not divide evenly | |
| /// into a for loop where the step divides the iteration space evenly, followed | |
| /// by an scf.if for the last (partial) iteration (if any). | |
| /// | |
| /// This function rewrites the given scf.for loop in-place and creates a new | |
| /// scf.if operation for the last iteration. It replaces all uses of the | |
| /// unpeeled loop with the results of the newly generated scf.if. | |
| /// | |
| /// The newly generated scf.if operation is returned via `ifOp`. The boundary | |
| /// at which the loop is split (new upper bound) is returned via `splitBound`. | |
| /// The return value indicates whether the loop was rewritten or not. | |
| /// | |
| /// Note: Loops with a step size of 0 cannot be peeled. Applying this function | |
| /// to such a loop may result in IR with undefined behavior. | |
| static LogicalResult peelForLoop(RewriterBase &b, ForOp forOp, | |
| ForOp &partialIteration, Value &splitBound) { | |
| RewriterBase::InsertionGuard guard(b); | |
| Value lbVal = forOp.getLowerBound(); | |
| Value ubVal = forOp.getUpperBound(); | |
| Value stepVal = forOp.getStep(); | |
| b.setInsertionPoint(forOp); | |
| auto loc = forOp.getLoc(); | |
| bool isUnsigned = forOp.getUnsignedCmp(); | |
| IntegerAttr lbAttr, ubAttr, stepAttr; | |
| if (matchPattern(stepVal, m_Constant(&stepAttr))) { | |
| APInt step = stepAttr.getValue(); | |
| if (isUnsigned ? step.ule(1) : step.sle(1)) | |
| return failure(); | |
| } | |
| if (matchPattern(lbVal, m_Constant(&lbAttr)) && | |
| matchPattern(ubVal, m_Constant(&ubAttr))) { | |
| APInt lb = lbAttr.getValue(); | |
| APInt ub = ubAttr.getValue(); | |
| if (isUnsigned ? ub.ule(lb) : ub.sle(lb)) | |
| return failure(); | |
| // lb < ub here, so ub - lb as an unsigned value is the exact distance, even | |
| // for signed loops whose range exceeds the signed max. hence urem for both. | |
| if (stepAttr && (ub - lb).urem(stepAttr.getValue()).isZero()) | |
| return failure(); | |
| } | |
| // affine reasoning is signed and index-only, | |
| // so use it only for signed index loops. | |
| if (!isUnsigned && forOp.getInductionVar().getType().isIndex()) { | |
| AffineExpr sym0, sym1, sym2; | |
| bindSymbols(b.getContext(), sym0, sym1, sym2); | |
| SmallVector<Value> operands{forOp.getLowerBound(), forOp.getUpperBound(), | |
| forOp.getStep()}; | |
| AffineMap map = AffineMap::get(0, 3, {(sym1 - sym0) % sym2}); | |
| affine::fullyComposeAffineMapAndOperands(&map, &operands); | |
| if (auto constExpr = dyn_cast<AffineConstantExpr>(map.getResult(0))) | |
| if (constExpr.getValue() == 0) | |
| return failure(); | |
| } | |
| // split = ub - ((ub - min(lb, ub)) remui step), with a signed or unsigned min | |
| // to match the loop. For ub <= lb the min gives a zero remainder, so | |
| // split = ub and both loops stay empty. For ub > lb the wrapping subtraction | |
| // is the exact distance when read as unsigned, even beyond the signed range, | |
| // which is why remui is used for signed loops as well. | |
| Value lbEff = isUnsigned ? b.createOrFold<arith::MinUIOp>(loc, lbVal, ubVal) | |
| : b.createOrFold<arith::MinSIOp>(loc, lbVal, ubVal); | |
| Value diff = b.createOrFold<arith::SubIOp>(loc, ubVal, lbEff); | |
| Value rem = b.createOrFold<arith::RemUIOp>(loc, diff, stepVal); | |
| splitBound = b.createOrFold<arith::SubIOp>(loc, ubVal, rem); | |
| // Create ForOp for partial iteration. | |
| b.setInsertionPointAfter(forOp); | |
| partialIteration = cast<ForOp>(b.clone(*forOp.getOperation())); | |
| partialIteration.getLowerBoundMutable().assign(splitBound); | |
| b.replaceAllUsesWith(forOp.getResults(), partialIteration->getResults()); | |
| partialIteration.getInitArgsMutable().assign(forOp->getResults()); | |
| // Set new upper loop bound. | |
| b.modifyOpInPlace(forOp, | |
| [&]() { forOp.getUpperBoundMutable().assign(splitBound); }); | |
| return success(); | |
| } | |
| static void rewriteAffineOpAfterPeeling(RewriterBase &rewriter, ForOp forOp, | |
| ForOp partialIteration, | |
| Value previousUb) { | |
| Value mainIv = forOp.getInductionVar(); | |
| Value partialIv = partialIteration.getInductionVar(); | |
| assert(forOp.getStep() == partialIteration.getStep() && | |
| "expected same step in main and partial loop"); | |
| Value step = forOp.getStep(); | |
| forOp.walk([&](Operation *affineOp) { | |
| if (!isa<AffineMinOp, AffineMaxOp>(affineOp)) | |
| return WalkResult::advance(); | |
| (void)scf::rewritePeeledMinMaxOp(rewriter, affineOp, mainIv, previousUb, | |
| step, | |
| /*insideLoop=*/true); | |
| return WalkResult::advance(); | |
| }); | |
| partialIteration.walk([&](Operation *affineOp) { | |
| if (!isa<AffineMinOp, AffineMaxOp>(affineOp)) | |
| return WalkResult::advance(); | |
| (void)scf::rewritePeeledMinMaxOp(rewriter, affineOp, partialIv, previousUb, | |
| step, /*insideLoop=*/false); | |
| return WalkResult::advance(); | |
| }); | |
| } | |
| LogicalResult mlir::scf::peelForLoopAndSimplifyBounds(RewriterBase &rewriter, | |
| ForOp forOp, | |
| ForOp &partialIteration) { | |
| Value previousUb = forOp.getUpperBound(); | |
| Value splitBound; | |
| if (failed(peelForLoop(rewriter, forOp, partialIteration, splitBound))) | |
| return failure(); | |
| // The affine.min/max simplification relies on signed reasoning, which does | |
| // not hold for unsigned loops, so leave those ops untouched. | |
| if (forOp.getUnsignedCmp()) | |
| return success(); | |
| // Rewrite affine.min and affine.max ops. | |
| rewriteAffineOpAfterPeeling(rewriter, forOp, partialIteration, previousUb); | |
| return success(); | |
| } | |
| /// Rewrites the original scf::ForOp as two scf::ForOp Ops, the first | |
| /// scf::ForOp corresponds to the first iteration of the loop which can be | |
| /// canonicalized away in the following optimizations. The second loop Op | |
| /// contains the remaining iterations, with a lower bound updated as the | |
| /// original lower bound plus the step (i.e. skips the first iteration). | |
| LogicalResult mlir::scf::peelForLoopFirstIteration(RewriterBase &b, ForOp forOp, | |
| ForOp &firstIteration) { | |
| RewriterBase::InsertionGuard guard(b); | |
| Value lbVal = forOp.getLowerBound(); | |
| Value ubVal = forOp.getUpperBound(); | |
| Value stepVal = forOp.getStep(); | |
| auto loc = forOp.getLoc(); | |
| Value splitBound; | |
| b.setInsertionPoint(forOp); | |
| // Check whether we have a unsigned loop | |
| if (forOp.getUnsignedCmp()) { | |
| IntegerAttr lbAttr, ubAttr, stepAttr; | |
| if (matchPattern(lbVal, m_Constant(&lbAttr)) && | |
| matchPattern(ubVal, m_Constant(&ubAttr)) && | |
| matchPattern(stepVal, m_Constant(&stepAttr))) { | |
| // Get the constants as APInt at the loop’s own bit width | |
| APInt lb = lbAttr.getValue(); | |
| APInt ub = ubAttr.getValue(); | |
| APInt step = stepAttr.getValue(); | |
| // bail out if ub <= lb or ub - lb <= step (unsigned constants) | |
| if (ub.ule(lb) || (ub - lb).ule(step)) | |
| return failure(); | |
| } | |
| // split = max(lb, min(lb + step, ub)): the first loop runs one iteration, | |
| // or none if the loop is empty. If lb + step wraps, the loop has at most | |
| // one iteration and split falls back to lb, so the main loop does the work. | |
| // use unsigned ops (MinUIOp, MaxUIOp) | |
| Value sum = b.createOrFold<arith::AddIOp>(loc, lbVal, stepVal); | |
| Value capped = b.createOrFold<arith::MinUIOp>(loc, sum, ubVal); | |
| splitBound = b.createOrFold<arith::MaxUIOp>(loc, lbVal, capped); | |
| } else { | |
| auto lbInt = getConstantIntValue(lbVal); | |
| auto ubInt = getConstantIntValue(ubVal); | |
| auto stepInt = getConstantIntValue(stepVal); | |
| // Peeling is not needed if there is at most one iteration. scf.for requires | |
| // a positive step, so the trip count is ceil((UB - LB) / Step) when UB > LB | |
| // and 0 otherwise. With a positive step, "UB - LB <= Step" therefore covers | |
| // both the empty loop and the single-iteration loop. Bail out if the | |
| // subtraction overflows, since we can't reason about the trip count then. | |
| if (lbInt && ubInt && stepInt) { | |
| int64_t diff; | |
| if (llvm::SubOverflow(*ubInt, *lbInt, diff) || diff <= *stepInt) | |
| return failure(); | |
| } | |
| // split = max(lb, min(lb + step, ub)): the first loop runs one iteration, | |
| // or none if the loop is empty. If lb + step wraps, the loop has at most | |
| // one iteration and split falls back to lb, so the main loop does the work. | |
| Value sum = b.createOrFold<arith::AddIOp>(loc, lbVal, stepVal); | |
| Value capped = b.createOrFold<arith::MinSIOp>(loc, sum, ubVal); | |
| splitBound = b.createOrFold<arith::MaxSIOp>(loc, lbVal, capped); | |
| } | |
| // Peel the first iteration. | |
| firstIteration = cast<ForOp>(b.clone(*forOp.getOperation())); | |
| b.modifyOpInPlace(firstIteration, [&]() { | |
| firstIteration.getUpperBoundMutable().assign(splitBound); | |
| }); | |
| // Update main loop with new lower bound. | |
| b.modifyOpInPlace(forOp, [&]() { | |
| forOp.getInitArgsMutable().assign(firstIteration->getResults()); | |
| forOp.getLowerBoundMutable().assign(splitBound); | |
| }); | |
| return success(); | |
| } | |
| static constexpr char kPeeledLoopLabel[] = "__peeled_loop__"; | |
| static constexpr char kPartialIterationLabel[] = "__partial_iteration__"; | |
| namespace { | |
| struct ForLoopPeelingPattern : public OpRewritePattern<ForOp> { | |
| ForLoopPeelingPattern(MLIRContext *ctx, bool peelFront, bool skipPartial) | |
| : OpRewritePattern<ForOp>(ctx), peelFront(peelFront), | |
| skipPartial(skipPartial) {} | |
| LogicalResult matchAndRewrite(ForOp forOp, | |
| PatternRewriter &rewriter) const override { | |
| // Do not peel already peeled loops. | |
| if (forOp->hasDiscardableAttr(kPeeledLoopLabel)) | |
| return failure(); | |
| scf::ForOp partialIteration; | |
| // The case for peeling the first iteration of the loop. | |
| if (peelFront) { | |
| if (failed( | |
| peelForLoopFirstIteration(rewriter, forOp, partialIteration))) { | |
| return failure(); | |
| } | |
| } else { | |
| if (skipPartial) { | |
| // No peeling of loops inside the partial iteration of another peeled | |
| // loop. | |
| Operation *op = forOp.getOperation(); | |
| while ((op = op->getParentOfType<scf::ForOp>())) { | |
| if (op->hasDiscardableAttr(kPartialIterationLabel)) | |
| return failure(); | |
| } | |
| } | |
| // Apply loop peeling. | |
| if (failed( | |
| peelForLoopAndSimplifyBounds(rewriter, forOp, partialIteration))) | |
| return failure(); | |
| } | |
| // Apply label, so that the same loop is not rewritten a second time. | |
| rewriter.modifyOpInPlace(partialIteration, [&]() { | |
| partialIteration->setDiscardableAttr(kPeeledLoopLabel, | |
| rewriter.getUnitAttr()); | |
| partialIteration->setDiscardableAttr(kPartialIterationLabel, | |
| rewriter.getUnitAttr()); | |
| }); | |
| rewriter.modifyOpInPlace(forOp, [&]() { | |
| forOp->setDiscardableAttr(kPeeledLoopLabel, rewriter.getUnitAttr()); | |
| }); | |
| return success(); | |
| } | |
| // If set to true, the first iteration of the loop will be peeled. Otherwise, | |
| // the unevenly divisible loop will be peeled at the end. | |
| bool peelFront; | |
| /// If set to true, loops inside partial iterations of another peeled loop | |
| /// are not peeled. This reduces the size of the generated code. Partial | |
| /// iterations are not usually performance critical. | |
| /// Note: Takes into account the entire chain of parent operations, not just | |
| /// the direct parent. | |
| bool skipPartial; | |
| }; | |
| } // namespace | |
| namespace { | |
| struct ParallelLoopSpecialization | |
| : public impl::SCFParallelLoopSpecializationBase< | |
| ParallelLoopSpecialization> { | |
| void runOnOperation() override { | |
| getOperation()->walk( | |
| [](ParallelOp op) { specializeParallelLoopForUnrolling(op); }); | |
| } | |
| }; | |
| struct ForLoopSpecialization | |
| : public impl::SCFForLoopSpecializationBase<ForLoopSpecialization> { | |
| void runOnOperation() override { | |
| getOperation()->walk([](ForOp op) { specializeForLoopForUnrolling(op); }); | |
| } | |
| }; | |
| struct ForLoopPeeling : public impl::SCFForLoopPeelingBase<ForLoopPeeling> { | |
| using impl::SCFForLoopPeelingBase<ForLoopPeeling>::SCFForLoopPeelingBase; | |
| void runOnOperation() override { | |
| auto *parentOp = getOperation(); | |
| MLIRContext *ctx = parentOp->getContext(); | |
| RewritePatternSet patterns(ctx); | |
| patterns.add<ForLoopPeelingPattern>(ctx, peelFront, skipPartial); | |
| (void)applyPatternsGreedily(parentOp, std::move(patterns)); | |
| // Drop the markers. | |
| parentOp->walk([](Operation *op) { | |
| op->removeDiscardableAttr(kPeeledLoopLabel); | |
| op->removeDiscardableAttr(kPartialIterationLabel); | |
| }); | |
| } | |
| }; | |
| } // namespace | |
| std::unique_ptr<Pass> mlir::createParallelLoopSpecializationPass() { | |
| return std::make_unique<ParallelLoopSpecialization>(); | |
| } | |
| std::unique_ptr<Pass> mlir::createForLoopSpecializationPass() { | |
| return std::make_unique<ForLoopSpecialization>(); | |
| } |