//===- LoopUtils.cpp ---- Misc utilities for loop transformation ----------===// // // Part of the MLIR 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 // //===----------------------------------------------------------------------===// // // This file implements miscellaneous loop transformation routines. // //===----------------------------------------------------------------------===// #include "mlir/Transforms/LoopUtils.h" #include "mlir/Analysis/AffineAnalysis.h" #include "mlir/Analysis/LoopAnalysis.h" #include "mlir/Analysis/SliceAnalysis.h" #include "mlir/Analysis/Utils.h" #include "mlir/Dialect/AffineOps/AffineOps.h" #include "mlir/Dialect/LoopOps/LoopOps.h" #include "mlir/IR/AffineMap.h" #include "mlir/IR/BlockAndValueMapping.h" #include "mlir/IR/Function.h" #include "mlir/Transforms/RegionUtils.h" #include "mlir/Transforms/Utils.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/MapVector.h" #include "llvm/ADT/SetVector.h" #include "llvm/ADT/SmallPtrSet.h" #include "llvm/Support/Debug.h" #include "llvm/Support/raw_ostream.h" #define DEBUG_TYPE "LoopUtils" using namespace mlir; using llvm::SetVector; using llvm::SmallMapVector; /// Computes the cleanup loop lower bound of the loop being unrolled with /// the specified unroll factor; this bound will also be upper bound of the main /// part of the unrolled loop. Computes the bound as an AffineMap with its /// operands or a null map when the trip count can't be expressed as an affine /// expression. void mlir::getCleanupLoopLowerBound(AffineForOp forOp, unsigned unrollFactor, AffineMap *map, SmallVectorImpl *operands, OpBuilder &b) { auto lbMap = forOp.getLowerBoundMap(); // Single result lower bound map only. if (lbMap.getNumResults() != 1) { *map = AffineMap(); return; } AffineMap tripCountMap; SmallVector tripCountOperands; buildTripCountMapAndOperands(forOp, &tripCountMap, &tripCountOperands); // Sometimes the trip count cannot be expressed as an affine expression. if (!tripCountMap) { *map = AffineMap(); return; } unsigned step = forOp.getStep(); auto lb = b.create(forOp.getLoc(), lbMap, forOp.getLowerBoundOperands()); // For each upper bound expr, get the range. // Eg: affine.for %i = lb to min (ub1, ub2), // where tripCountExprs yield (tr1, tr2), we create affine.apply's: // lb + tr1 - tr1 % ufactor, lb + tr2 - tr2 % ufactor; the results of all // these affine.apply's make up the cleanup loop lower bound. SmallVector bumpExprs(tripCountMap.getNumResults()); SmallVector bumpValues(tripCountMap.getNumResults()); for (unsigned i = 0, e = tripCountMap.getNumResults(); i < e; i++) { auto tripCountExpr = tripCountMap.getResult(i); bumpExprs[i] = (tripCountExpr - tripCountExpr % unrollFactor) * step; auto bumpMap = AffineMap::get(tripCountMap.getNumDims(), tripCountMap.getNumSymbols(), bumpExprs[i]); bumpValues[i] = b.create(forOp.getLoc(), bumpMap, tripCountOperands); } SmallVector newUbExprs(tripCountMap.getNumResults()); for (unsigned i = 0, e = bumpExprs.size(); i < e; i++) newUbExprs[i] = b.getAffineDimExpr(0) + b.getAffineDimExpr(i + 1); operands->clear(); operands->push_back(lb); operands->append(bumpValues.begin(), bumpValues.end()); *map = AffineMap::get(1 + tripCountMap.getNumResults(), 0, newUbExprs); // Simplify the map + operands. fullyComposeAffineMapAndOperands(map, operands); *map = simplifyAffineMap(*map); canonicalizeMapAndOperands(map, operands); // Remove any affine.apply's that became dead from the simplification above. for (auto v : bumpValues) { if (v.use_empty()) v.getDefiningOp()->erase(); } if (lb.use_empty()) lb.erase(); } /// Promotes the loop body of a forOp to its containing block if the forOp /// was known to have a single iteration. // TODO(bondhugula): extend this for arbitrary affine bounds. LogicalResult mlir::promoteIfSingleIteration(AffineForOp forOp) { Optional tripCount = getConstantTripCount(forOp); if (!tripCount.hasValue() || tripCount.getValue() != 1) return failure(); // TODO(mlir-team): there is no builder for a max. if (forOp.getLowerBoundMap().getNumResults() != 1) return failure(); // Replaces all IV uses to its single iteration value. auto iv = forOp.getInductionVar(); Operation *op = forOp.getOperation(); if (!iv.use_empty()) { if (forOp.hasConstantLowerBound()) { OpBuilder topBuilder(op->getParentOfType().getBody()); auto constOp = topBuilder.create( forOp.getLoc(), forOp.getConstantLowerBound()); iv.replaceAllUsesWith(constOp); } else { AffineBound lb = forOp.getLowerBound(); SmallVector lbOperands(lb.operand_begin(), lb.operand_end()); OpBuilder builder(op->getBlock(), Block::iterator(op)); if (lb.getMap() == builder.getDimIdentityMap()) { // No need of generating an affine.apply. iv.replaceAllUsesWith(lbOperands[0]); } else { auto affineApplyOp = builder.create( op->getLoc(), lb.getMap(), lbOperands); iv.replaceAllUsesWith(affineApplyOp); } } } // Move the loop body operations, except for terminator, to the loop's // containing block. auto *block = op->getBlock(); forOp.getBody()->getOperations().back().erase(); block->getOperations().splice(Block::iterator(op), forOp.getBody()->getOperations()); forOp.erase(); return success(); } /// Promotes all single iteration for op's in the FuncOp, i.e., moves /// their body into the containing Block. void mlir::promoteSingleIterationLoops(FuncOp f) { // Gathers all innermost loops through a post order pruned walk. f.walk([](AffineForOp forOp) { promoteIfSingleIteration(forOp); }); } /// Generates a 'affine.for' op with the specified lower and upper bounds /// while generating the right IV remappings for the shifted operations. The /// operation blocks that go into the loop are specified in instGroupQueue /// starting from the specified offset, and in that order; the first element of /// the pair specifies the shift applied to that group of operations; note /// that the shift is multiplied by the loop step before being applied. Returns /// nullptr if the generated loop simplifies to a single iteration one. static AffineForOp generateLoop(AffineMap lbMap, AffineMap ubMap, const std::vector>> &instGroupQueue, unsigned offset, AffineForOp srcForInst, OpBuilder b) { SmallVector lbOperands(srcForInst.getLowerBoundOperands()); SmallVector ubOperands(srcForInst.getUpperBoundOperands()); assert(lbMap.getNumInputs() == lbOperands.size()); assert(ubMap.getNumInputs() == ubOperands.size()); auto loopChunk = b.create(srcForInst.getLoc(), lbOperands, lbMap, ubOperands, ubMap, srcForInst.getStep()); auto loopChunkIV = loopChunk.getInductionVar(); auto srcIV = srcForInst.getInductionVar(); BlockAndValueMapping operandMap; OpBuilder bodyBuilder = loopChunk.getBodyBuilder(); for (auto it = instGroupQueue.begin() + offset, e = instGroupQueue.end(); it != e; ++it) { uint64_t shift = it->first; auto insts = it->second; // All 'same shift' operations get added with their operands being // remapped to results of cloned operations, and their IV used remapped. // Generate the remapping if the shift is not zero: remappedIV = newIV - // shift. if (!srcIV.use_empty() && shift != 0) { auto ivRemap = bodyBuilder.create( srcForInst.getLoc(), bodyBuilder.getSingleDimShiftAffineMap( -static_cast(srcForInst.getStep() * shift)), loopChunkIV); operandMap.map(srcIV, ivRemap); } else { operandMap.map(srcIV, loopChunkIV); } for (auto *op : insts) { if (!isa(op)) bodyBuilder.clone(*op, operandMap); } }; if (succeeded(promoteIfSingleIteration(loopChunk))) return AffineForOp(); return loopChunk; } /// Skew the operations in the body of a 'affine.for' operation with the /// specified operation-wise shifts. The shifts are with respect to the /// original execution order, and are multiplied by the loop 'step' before being /// applied. A shift of zero for each operation will lead to no change. // The skewing of operations with respect to one another can be used for // example to allow overlap of asynchronous operations (such as DMA // communication) with computation, or just relative shifting of operations // for better register reuse, locality or parallelism. As such, the shifts are // typically expected to be at most of the order of the number of operations. // This method should not be used as a substitute for loop distribution/fission. // This method uses an algorithm// in time linear in the number of operations // in the body of the for loop - (using the 'sweep line' paradigm). This method // asserts preservation of SSA dominance. A check for that as well as that for // memory-based dependence preservation check rests with the users of this // method. LogicalResult mlir::instBodySkew(AffineForOp forOp, ArrayRef shifts, bool unrollPrologueEpilogue) { if (forOp.getBody()->begin() == std::prev(forOp.getBody()->end())) return success(); // If the trip counts aren't constant, we would need versioning and // conditional guards (or context information to prevent such versioning). The // better way to pipeline for such loops is to first tile them and extract // constant trip count "full tiles" before applying this. auto mayBeConstTripCount = getConstantTripCount(forOp); if (!mayBeConstTripCount.hasValue()) { LLVM_DEBUG(forOp.emitRemark("non-constant trip count loop not handled")); return success(); } uint64_t tripCount = mayBeConstTripCount.getValue(); assert(isInstwiseShiftValid(forOp, shifts) && "shifts will lead to an invalid transformation\n"); int64_t step = forOp.getStep(); unsigned numChildInsts = forOp.getBody()->getOperations().size(); // Do a linear time (counting) sort for the shifts. uint64_t maxShift = 0; for (unsigned i = 0; i < numChildInsts; i++) { maxShift = std::max(maxShift, shifts[i]); } // Such large shifts are not the typical use case. if (maxShift >= numChildInsts) { forOp.emitWarning("not shifting because shifts are unrealistically large"); return success(); } // An array of operation groups sorted by shift amount; each group has all // operations with the same shift in the order in which they appear in the // body of the 'affine.for' op. std::vector> sortedInstGroups(maxShift + 1); unsigned pos = 0; for (auto &op : *forOp.getBody()) { auto shift = shifts[pos++]; sortedInstGroups[shift].push_back(&op); } // Unless the shifts have a specific pattern (which actually would be the // common use case), prologue and epilogue are not meaningfully defined. // Nevertheless, if 'unrollPrologueEpilogue' is set, we will treat the first // loop generated as the prologue and the last as epilogue and unroll these // fully. AffineForOp prologue; AffineForOp epilogue; // Do a sweep over the sorted shifts while storing open groups in a // vector, and generating loop portions as necessary during the sweep. A block // of operations is paired with its shift. std::vector>> instGroupQueue; auto origLbMap = forOp.getLowerBoundMap(); uint64_t lbShift = 0; OpBuilder b(forOp.getOperation()); for (uint64_t d = 0, e = sortedInstGroups.size(); d < e; ++d) { // If nothing is shifted by d, continue. if (sortedInstGroups[d].empty()) continue; if (!instGroupQueue.empty()) { assert(d >= 1 && "Queue expected to be empty when the first block is found"); // The interval for which the loop needs to be generated here is: // [lbShift, min(lbShift + tripCount, d)) and the body of the // loop needs to have all operations in instQueue in that order. AffineForOp res; if (lbShift + tripCount * step < d * step) { res = generateLoop( b.getShiftedAffineMap(origLbMap, lbShift), b.getShiftedAffineMap(origLbMap, lbShift + tripCount * step), instGroupQueue, 0, forOp, b); // Entire loop for the queued op groups generated, empty it. instGroupQueue.clear(); lbShift += tripCount * step; } else { res = generateLoop(b.getShiftedAffineMap(origLbMap, lbShift), b.getShiftedAffineMap(origLbMap, d), instGroupQueue, 0, forOp, b); lbShift = d * step; } if (!prologue && res) prologue = res; epilogue = res; } else { // Start of first interval. lbShift = d * step; } // Augment the list of operations that get into the current open interval. instGroupQueue.push_back({d, sortedInstGroups[d]}); } // Those operations groups left in the queue now need to be processed (FIFO) // and their loops completed. for (unsigned i = 0, e = instGroupQueue.size(); i < e; ++i) { uint64_t ubShift = (instGroupQueue[i].first + tripCount) * step; epilogue = generateLoop(b.getShiftedAffineMap(origLbMap, lbShift), b.getShiftedAffineMap(origLbMap, ubShift), instGroupQueue, i, forOp, b); lbShift = ubShift; if (!prologue) prologue = epilogue; } // Erase the original for op. forOp.erase(); if (unrollPrologueEpilogue && prologue) loopUnrollFull(prologue); if (unrollPrologueEpilogue && !epilogue && epilogue.getOperation() != prologue.getOperation()) loopUnrollFull(epilogue); return success(); } // Collect perfectly nested loops starting from `rootForOps`. Loops are // perfectly nested if each loop is the first and only non-terminator operation // in the parent loop. Collect at most `maxLoops` loops and append them to // `forOps`. template static void getPerfectlyNestedLoopsImpl( SmallVectorImpl &forOps, T rootForOp, unsigned maxLoops = std::numeric_limits::max()) { for (unsigned i = 0; i < maxLoops; ++i) { forOps.push_back(rootForOp); Block &body = rootForOp.region().front(); if (body.begin() != std::prev(body.end(), 2)) return; rootForOp = dyn_cast(&body.front()); if (!rootForOp) return; } } /// Get perfectly nested sequence of loops starting at root of loop nest /// (the first op being another AffineFor, and the second op - a terminator). /// A loop is perfectly nested iff: the first op in the loop's body is another /// AffineForOp, and the second op is a terminator). void mlir::getPerfectlyNestedLoops(SmallVectorImpl &nestedLoops, AffineForOp root) { getPerfectlyNestedLoopsImpl(nestedLoops, root); } void mlir::getPerfectlyNestedLoops(SmallVectorImpl &nestedLoops, loop::ForOp root) { getPerfectlyNestedLoopsImpl(nestedLoops, root); } /// Unrolls this loop completely. LogicalResult mlir::loopUnrollFull(AffineForOp forOp) { Optional mayBeConstantTripCount = getConstantTripCount(forOp); if (mayBeConstantTripCount.hasValue()) { uint64_t tripCount = mayBeConstantTripCount.getValue(); if (tripCount == 1) { return promoteIfSingleIteration(forOp); } return loopUnrollByFactor(forOp, tripCount); } return failure(); } /// Unrolls and jams this loop by the specified factor or by the trip count (if /// constant) whichever is lower. LogicalResult mlir::loopUnrollUpToFactor(AffineForOp forOp, uint64_t unrollFactor) { Optional mayBeConstantTripCount = getConstantTripCount(forOp); if (mayBeConstantTripCount.hasValue() && mayBeConstantTripCount.getValue() < unrollFactor) return loopUnrollByFactor(forOp, mayBeConstantTripCount.getValue()); return loopUnrollByFactor(forOp, unrollFactor); } /// Unrolls this loop by the specified factor. Returns success if the loop /// is successfully unrolled. LogicalResult mlir::loopUnrollByFactor(AffineForOp forOp, uint64_t unrollFactor) { assert(unrollFactor >= 1 && "unroll factor should be >= 1"); if (unrollFactor == 1) return promoteIfSingleIteration(forOp); if (forOp.getBody()->empty() || forOp.getBody()->begin() == std::prev(forOp.getBody()->end())) return failure(); // Loops where the lower bound is a max expression isn't supported for // unrolling since the trip count can be expressed as an affine function when // both the lower bound and the upper bound are multi-result maps. However, // one meaningful way to do such unrolling would be to specialize the loop for // the 'hotspot' case and unroll that hotspot. if (forOp.getLowerBoundMap().getNumResults() != 1) return failure(); // If the trip count is lower than the unroll factor, no unrolled body. // TODO(bondhugula): option to specify cleanup loop unrolling. Optional mayBeConstantTripCount = getConstantTripCount(forOp); if (mayBeConstantTripCount.hasValue() && mayBeConstantTripCount.getValue() < unrollFactor) return failure(); // Generate the cleanup loop if trip count isn't a multiple of unrollFactor. Operation *op = forOp.getOperation(); if (getLargestDivisorOfTripCount(forOp) % unrollFactor != 0) { OpBuilder builder(op->getBlock(), ++Block::iterator(op)); auto cleanupForInst = cast(builder.clone(*op)); AffineMap cleanupMap; SmallVector cleanupOperands; getCleanupLoopLowerBound(forOp, unrollFactor, &cleanupMap, &cleanupOperands, builder); assert(cleanupMap && "cleanup loop lower bound map for single result lower bound maps " "can always be determined"); cleanupForInst.setLowerBound(cleanupOperands, cleanupMap); // Promote the loop body up if this has turned into a single iteration loop. promoteIfSingleIteration(cleanupForInst); // Adjust upper bound of the original loop; this is the same as the lower // bound of the cleanup loop. forOp.setUpperBound(cleanupOperands, cleanupMap); } // Scale the step of loop being unrolled by unroll factor. int64_t step = forOp.getStep(); forOp.setStep(step * unrollFactor); // Builder to insert unrolled bodies just before the terminator of the body of // 'forOp'. OpBuilder builder = forOp.getBodyBuilder(); // Keep a pointer to the last non-terminator operation in the original block // so that we know what to clone (since we are doing this in-place). Block::iterator srcBlockEnd = std::prev(forOp.getBody()->end(), 2); // Unroll the contents of 'forOp' (append unrollFactor-1 additional copies). auto forOpIV = forOp.getInductionVar(); for (unsigned i = 1; i < unrollFactor; i++) { BlockAndValueMapping operandMap; // If the induction variable is used, create a remapping to the value for // this unrolled instance. if (!forOpIV.use_empty()) { // iv' = iv + 1/2/3...unrollFactor-1; auto d0 = builder.getAffineDimExpr(0); auto bumpMap = AffineMap::get(1, 0, {d0 + i * step}); auto ivUnroll = builder.create(forOp.getLoc(), bumpMap, forOpIV); operandMap.map(forOpIV, ivUnroll); } // Clone the original body of 'forOp'. for (auto it = forOp.getBody()->begin(); it != std::next(srcBlockEnd); it++) { builder.clone(*it, operandMap); } } // Promote the loop body up if this has turned into a single iteration loop. promoteIfSingleIteration(forOp); return success(); } /// Performs loop interchange on 'forOpA' and 'forOpB', where 'forOpB' is /// nested within 'forOpA' as the only non-terminator operation in its block. void mlir::interchangeLoops(AffineForOp forOpA, AffineForOp forOpB) { auto *forOpAInst = forOpA.getOperation(); assert(&*forOpA.getBody()->begin() == forOpB.getOperation()); auto &forOpABody = forOpA.getBody()->getOperations(); auto &forOpBBody = forOpB.getBody()->getOperations(); // 1) Splice forOpA's non-terminator operations (which is just forOpB) just // before forOpA (in ForOpA's parent's block) this should leave 'forOpA's // body containing only the terminator. forOpAInst->getBlock()->getOperations().splice(Block::iterator(forOpAInst), forOpABody, forOpABody.begin(), std::prev(forOpABody.end())); // 2) Splice forOpB's non-terminator operations into the beginning of forOpA's // body (this leaves forOpB's body containing only the terminator). forOpABody.splice(forOpABody.begin(), forOpBBody, forOpBBody.begin(), std::prev(forOpBBody.end())); // 3) Splice forOpA into the beginning of forOpB's body. forOpBBody.splice(forOpBBody.begin(), forOpAInst->getBlock()->getOperations(), Block::iterator(forOpAInst)); } // Checks each dependence component against the permutation to see if the // desired loop interchange would violate dependences by making the // dependence component lexicographically negative. static bool checkLoopInterchangeDependences( const std::vector> &depCompsVec, ArrayRef loops, ArrayRef loopPermMap) { // Invert permutation map. unsigned maxLoopDepth = loops.size(); SmallVector loopPermMapInv; loopPermMapInv.resize(maxLoopDepth); for (unsigned i = 0; i < maxLoopDepth; ++i) loopPermMapInv[loopPermMap[i]] = i; // Check each dependence component against the permutation to see if the // desired loop interchange permutation would make the dependence vectors // lexicographically negative. // Example 1: [-1, 1][0, 0] // Example 2: [0, 0][-1, 1] for (unsigned i = 0, e = depCompsVec.size(); i < e; ++i) { const SmallVector &depComps = depCompsVec[i]; assert(depComps.size() >= maxLoopDepth); // Check if the first non-zero dependence component is positive. // This iterates through loops in the desired order. for (unsigned j = 0; j < maxLoopDepth; ++j) { unsigned permIndex = loopPermMapInv[j]; assert(depComps[permIndex].lb.hasValue()); int64_t depCompLb = depComps[permIndex].lb.getValue(); if (depCompLb > 0) break; if (depCompLb < 0) return false; } } return true; } /// Checks if the loop interchange permutation 'loopPermMap' of the perfectly /// nested sequence of loops in 'loops' would violate dependences. bool mlir::isValidLoopInterchangePermutation(ArrayRef loops, ArrayRef loopPermMap) { // Gather dependence components for dependences between all ops in loop nest // rooted at 'loops[0]', at loop depths in range [1, maxLoopDepth]. assert(loopPermMap.size() == loops.size()); unsigned maxLoopDepth = loops.size(); std::vector> depCompsVec; getDependenceComponents(loops[0], maxLoopDepth, &depCompsVec); return checkLoopInterchangeDependences(depCompsVec, loops, loopPermMap); } /// Performs a sequence of loop interchanges of loops in perfectly nested /// sequence of loops in 'loops', as specified by permutation in 'loopPermMap'. unsigned mlir::interchangeLoops(ArrayRef loops, ArrayRef loopPermMap) { Optional loopNestRootIndex; for (int i = loops.size() - 1; i >= 0; --i) { int permIndex = static_cast(loopPermMap[i]); // Store the index of the for loop which will be the new loop nest root. if (permIndex == 0) loopNestRootIndex = i; if (permIndex > i) { // Sink loop 'i' by 'permIndex - i' levels deeper into the loop nest. sinkLoop(loops[i], permIndex - i); } } assert(loopNestRootIndex.hasValue()); return loopNestRootIndex.getValue(); } // Sinks all sequential loops to the innermost levels (while preserving // relative order among them) and moves all parallel loops to the // outermost (while again preserving relative order among them). AffineForOp mlir::sinkSequentialLoops(AffineForOp forOp) { SmallVector loops; getPerfectlyNestedLoops(loops, forOp); if (loops.size() < 2) return forOp; // Gather dependence components for dependences between all ops in loop nest // rooted at 'loops[0]', at loop depths in range [1, maxLoopDepth]. unsigned maxLoopDepth = loops.size(); std::vector> depCompsVec; getDependenceComponents(loops[0], maxLoopDepth, &depCompsVec); // Mark loops as either parallel or sequential. SmallVector isParallelLoop(maxLoopDepth, true); for (unsigned i = 0, e = depCompsVec.size(); i < e; ++i) { SmallVector &depComps = depCompsVec[i]; assert(depComps.size() >= maxLoopDepth); for (unsigned j = 0; j < maxLoopDepth; ++j) { DependenceComponent &depComp = depComps[j]; assert(depComp.lb.hasValue() && depComp.ub.hasValue()); if (depComp.lb.getValue() != 0 || depComp.ub.getValue() != 0) isParallelLoop[j] = false; } } // Count the number of parallel loops. unsigned numParallelLoops = 0; for (unsigned i = 0, e = isParallelLoop.size(); i < e; ++i) if (isParallelLoop[i]) ++numParallelLoops; // Compute permutation of loops that sinks sequential loops (and thus raises // parallel loops) while preserving relative order. SmallVector loopPermMap(maxLoopDepth); unsigned nextSequentialLoop = numParallelLoops; unsigned nextParallelLoop = 0; for (unsigned i = 0; i < maxLoopDepth; ++i) { if (isParallelLoop[i]) { loopPermMap[i] = nextParallelLoop++; } else { loopPermMap[i] = nextSequentialLoop++; } } // Check if permutation 'loopPermMap' would violate dependences. if (!checkLoopInterchangeDependences(depCompsVec, loops, loopPermMap)) return forOp; // Perform loop interchange according to permutation 'loopPermMap'. unsigned loopNestRootIndex = interchangeLoops(loops, loopPermMap); return loops[loopNestRootIndex]; } /// Performs a series of loop interchanges to sink 'forOp' 'loopDepth' levels /// deeper in the loop nest. void mlir::sinkLoop(AffineForOp forOp, unsigned loopDepth) { for (unsigned i = 0; i < loopDepth; ++i) { AffineForOp nextForOp = cast(forOp.getBody()->front()); interchangeLoops(forOp, nextForOp); } } // Factors out common behavior to add a new `iv` (resp. `iv` + `offset`) to the // lower (resp. upper) loop bound. When called for both the lower and upper // bounds, the resulting IR resembles: // // ```mlir // affine.for %i = max (`iv, ...) to min (`iv` + `offset`) { // ... // } // ``` static void augmentMapAndBounds(OpBuilder &b, Value iv, AffineMap *map, SmallVector *operands, int64_t offset = 0) { auto bounds = llvm::to_vector<4>(map->getResults()); bounds.push_back(b.getAffineDimExpr(map->getNumDims()) + offset); operands->insert(operands->begin() + map->getNumDims(), iv); *map = AffineMap::get(map->getNumDims() + 1, map->getNumSymbols(), bounds); canonicalizeMapAndOperands(map, operands); } // Stripmines `forOp` by `factor` and sinks it under each of the `targets`. // Stripmine-sink is a primitive building block for generalized tiling of // imperfectly nested loops. // This transformation is purely mechanical and does not check legality, // profitability or even structural correctness. It is the user's // responsibility to specify `targets` that are dominated by `forOp`. // Returns the new AffineForOps, one per `targets`, nested immediately under // each of the `targets`. static SmallVector stripmineSink(AffineForOp forOp, uint64_t factor, ArrayRef targets) { auto originalStep = forOp.getStep(); auto scaledStep = originalStep * factor; forOp.setStep(scaledStep); auto *op = forOp.getOperation(); OpBuilder b(op->getBlock(), ++Block::iterator(op)); // Lower-bound map creation. auto lbMap = forOp.getLowerBoundMap(); SmallVector lbOperands(forOp.getLowerBoundOperands()); augmentMapAndBounds(b, forOp.getInductionVar(), &lbMap, &lbOperands); // Upper-bound map creation. auto ubMap = forOp.getUpperBoundMap(); SmallVector ubOperands(forOp.getUpperBoundOperands()); augmentMapAndBounds(b, forOp.getInductionVar(), &ubMap, &ubOperands, /*offset=*/scaledStep); auto iv = forOp.getInductionVar(); SmallVector innerLoops; for (auto t : targets) { // Insert newForOp before the terminator of `t`. OpBuilder b = t.getBodyBuilder(); auto newForOp = b.create(t.getLoc(), lbOperands, lbMap, ubOperands, ubMap, originalStep); auto begin = t.getBody()->begin(); // Skip terminator and `newForOp` which is just before the terminator. auto nOps = t.getBody()->getOperations().size() - 2; newForOp.getBody()->getOperations().splice( newForOp.getBody()->getOperations().begin(), t.getBody()->getOperations(), begin, std::next(begin, nOps)); replaceAllUsesInRegionWith(iv, newForOp.getInductionVar(), newForOp.region()); innerLoops.push_back(newForOp); } return innerLoops; } static Loops stripmineSink(loop::ForOp forOp, Value factor, ArrayRef targets) { auto originalStep = forOp.step(); auto iv = forOp.getInductionVar(); OpBuilder b(forOp); forOp.setStep(b.create(forOp.getLoc(), originalStep, factor)); Loops innerLoops; for (auto t : targets) { // Save information for splicing ops out of t when done auto begin = t.getBody()->begin(); auto nOps = t.getBody()->getOperations().size(); // Insert newForOp before the terminator of `t`. OpBuilder b(t.getBodyBuilder()); Value stepped = b.create(t.getLoc(), iv, forOp.step()); Value less = b.create(t.getLoc(), CmpIPredicate::slt, forOp.upperBound(), stepped); Value ub = b.create(t.getLoc(), less, forOp.upperBound(), stepped); // Splice [begin, begin + nOps - 1) into `newForOp` and replace uses. auto newForOp = b.create(t.getLoc(), iv, ub, originalStep); newForOp.getBody()->getOperations().splice( newForOp.getBody()->getOperations().begin(), t.getBody()->getOperations(), begin, std::next(begin, nOps - 1)); replaceAllUsesInRegionWith(iv, newForOp.getInductionVar(), newForOp.region()); innerLoops.push_back(newForOp); } return innerLoops; } // Stripmines a `forOp` by `factor` and sinks it under a single `target`. // Returns the new AffineForOps, nested immediately under `target`. template static ForType stripmineSink(ForType forOp, SizeType factor, ForType target) { // TODO(ntv): Use cheap structural assertions that targets are nested under // forOp and that targets are not nested under each other when DominanceInfo // exposes the capability. It seems overkill to construct a whole function // dominance tree at this point. auto res = stripmineSink(forOp, factor, ArrayRef{target}); assert(res.size() == 1 && "Expected 1 inner forOp"); return res[0]; } template static SmallVector, 8> tileImpl(ArrayRef forOps, ArrayRef sizes, ArrayRef targets) { SmallVector, 8> res; SmallVector currentTargets(targets.begin(), targets.end()); for (auto it : llvm::zip(forOps, sizes)) { auto step = stripmineSink(std::get<0>(it), std::get<1>(it), currentTargets); res.push_back(step); currentTargets = step; } return res; } SmallVector, 8> mlir::tile(ArrayRef forOps, ArrayRef sizes, ArrayRef targets) { return tileImpl(forOps, sizes, targets); } SmallVector mlir::tile(ArrayRef forOps, ArrayRef sizes, ArrayRef targets) { return tileImpl(forOps, sizes, targets); } template static SmallVector tileImpl(ArrayRef forOps, ArrayRef sizes, ForType target) { SmallVector res; for (auto loops : tile(forOps, sizes, ArrayRef{target})) { assert(loops.size() == 1); res.push_back(loops[0]); } return res; } SmallVector mlir::tile(ArrayRef forOps, ArrayRef sizes, AffineForOp target) { return tileImpl(forOps, sizes, target); } Loops mlir::tile(ArrayRef forOps, ArrayRef sizes, loop::ForOp target) { return tileImpl(forOps, sizes, target); } Loops mlir::tilePerfectlyNested(loop::ForOp rootForOp, ArrayRef sizes) { // Collect perfectly nested loops. If more size values provided than nested // loops available, truncate `sizes`. SmallVector forOps; forOps.reserve(sizes.size()); getPerfectlyNestedLoopsImpl(forOps, rootForOp, sizes.size()); if (forOps.size() < sizes.size()) sizes = sizes.take_front(forOps.size()); return ::tile(forOps, sizes, forOps.back()); } // Build the IR that performs ceil division of a positive value by a constant: // ceildiv(a, B) = divis(a + (B-1), B) // where divis is rounding-to-zero division. static Value ceilDivPositive(OpBuilder &builder, Location loc, Value dividend, int64_t divisor) { assert(divisor > 0 && "expected positive divisor"); assert(dividend.getType().isIndex() && "expected index-typed value"); Value divisorMinusOneCst = builder.create(loc, divisor - 1); Value divisorCst = builder.create(loc, divisor); Value sum = builder.create(loc, dividend, divisorMinusOneCst); return builder.create(loc, sum, divisorCst); } // Build the IR that performs ceil division of a positive value by another // positive value: // ceildiv(a, b) = divis(a + (b - 1), b) // where divis is rounding-to-zero division. static Value ceilDivPositive(OpBuilder &builder, Location loc, Value dividend, Value divisor) { assert(dividend.getType().isIndex() && "expected index-typed value"); Value cstOne = builder.create(loc, 1); Value divisorMinusOne = builder.create(loc, divisor, cstOne); Value sum = builder.create(loc, dividend, divisorMinusOne); return builder.create(loc, sum, divisor); } // Hoist the ops within `outer` that appear before `inner`. // Such ops include the ops that have been introduced by parametric tiling. // Ops that come from triangular loops (i.e. that belong to the program slice // rooted at `outer`) and ops that have side effects cannot be hoisted. // Return failure when any op fails to hoist. static LogicalResult hoistOpsBetween(loop::ForOp outer, loop::ForOp inner) { SetVector forwardSlice; getForwardSlice(outer.getOperation(), &forwardSlice, [&inner](Operation *op) { return op != inner.getOperation(); }); LogicalResult status = success(); SmallVector toHoist; for (auto &op : outer.getBody()->getOperations()) { // Stop when encountering the inner loop. if (&op == inner.getOperation()) break; // Skip over non-hoistable ops. if (forwardSlice.count(&op) > 0) { status = failure(); continue; } // Skip loop::ForOp, these are not considered a failure. if (op.getNumRegions() > 0) continue; // Skip other ops with regions. if (op.getNumRegions() > 0) { status = failure(); continue; } // Skip if op has side effects. // TODO(ntv): loads to immutable memory regions are ok. if (!op.hasNoSideEffect()) { status = failure(); continue; } toHoist.push_back(&op); } auto *outerForOp = outer.getOperation(); for (auto *op : toHoist) op->moveBefore(outerForOp); return status; } // Traverse the interTile and intraTile loops and try to hoist ops such that // bands of perfectly nested loops are isolated. // Return failure if either perfect interTile or perfect intraTile bands cannot // be formed. static LogicalResult tryIsolateBands(const TileLoops &tileLoops) { LogicalResult status = success(); auto &interTile = tileLoops.first; auto &intraTile = tileLoops.second; auto size = interTile.size(); assert(size == intraTile.size()); if (size <= 1) return success(); for (unsigned s = 1; s < size; ++s) status = succeeded(status) ? hoistOpsBetween(intraTile[0], intraTile[s]) : failure(); for (unsigned s = 1; s < size; ++s) status = succeeded(status) ? hoistOpsBetween(interTile[0], interTile[s]) : failure(); return status; } TileLoops mlir::extractFixedOuterLoops(loop::ForOp rootForOp, ArrayRef sizes) { // Collect perfectly nested loops. If more size values provided than nested // loops available, truncate `sizes`. SmallVector forOps; forOps.reserve(sizes.size()); getPerfectlyNestedLoopsImpl(forOps, rootForOp, sizes.size()); if (forOps.size() < sizes.size()) sizes = sizes.take_front(forOps.size()); // Compute the tile sizes such that i-th outer loop executes size[i] // iterations. Given that the loop current executes // numIterations = ceildiv((upperBound - lowerBound), step) // iterations, we need to tile with size ceildiv(numIterations, size[i]). SmallVector tileSizes; tileSizes.reserve(sizes.size()); for (unsigned i = 0, e = sizes.size(); i < e; ++i) { assert(sizes[i] > 0 && "expected strictly positive size for strip-mining"); auto forOp = forOps[i]; OpBuilder builder(forOp); auto loc = forOp.getLoc(); Value diff = builder.create(loc, forOp.upperBound(), forOp.lowerBound()); Value numIterations = ceilDivPositive(builder, loc, diff, forOp.step()); Value iterationsPerBlock = ceilDivPositive(builder, loc, numIterations, sizes[i]); tileSizes.push_back(iterationsPerBlock); } // Call parametric tiling with the given sizes. auto intraTile = tile(forOps, tileSizes, forOps.back()); TileLoops tileLoops = std::make_pair(forOps, intraTile); // TODO(ntv, zinenko) for now we just ignore the result of band isolation. // In the future, mapping decisions may be impacted by the ability to // isolate perfectly nested bands. tryIsolateBands(tileLoops); return tileLoops; } // Replaces all uses of `orig` with `replacement` except if the user is listed // in `exceptions`. static void replaceAllUsesExcept(Value orig, Value replacement, const SmallPtrSetImpl &exceptions) { for (auto &use : llvm::make_early_inc_range(orig.getUses())) { if (exceptions.count(use.getOwner()) == 0) use.set(replacement); } } // Transform a loop with a strictly positive step // for %i = %lb to %ub step %s // into a 0-based loop with step 1 // for %ii = 0 to ceildiv(%ub - %lb, %s) step 1 { // %i = %ii * %s + %lb // Insert the induction variable remapping in the body of `inner`, which is // expected to be either `loop` or another loop perfectly nested under `loop`. // Insert the definition of new bounds immediate before `outer`, which is // expected to be either `loop` or its parent in the loop nest. static void normalizeLoop(loop::ForOp loop, loop::ForOp outer, loop::ForOp inner) { OpBuilder builder(outer); Location loc = loop.getLoc(); // Check if the loop is already known to have a constant zero lower bound or // a constant one step. bool isZeroBased = false; if (auto ubCst = dyn_cast_or_null(loop.lowerBound().getDefiningOp())) isZeroBased = ubCst.getValue() == 0; bool isStepOne = false; if (auto stepCst = dyn_cast_or_null(loop.step().getDefiningOp())) isStepOne = stepCst.getValue() == 1; if (isZeroBased && isStepOne) return; // Compute the number of iterations the loop executes: ceildiv(ub - lb, step) // assuming the step is strictly positive. Update the bounds and the step // of the loop to go from 0 to the number of iterations, if necessary. // TODO(zinenko): introduce support for negative steps or emit dynamic asserts // on step positivity, whatever gets implemented first. Value diff = builder.create(loc, loop.upperBound(), loop.lowerBound()); Value numIterations = ceilDivPositive(builder, loc, diff, loop.step()); loop.setUpperBound(numIterations); Value lb = loop.lowerBound(); if (!isZeroBased) { Value cst0 = builder.create(loc, 0); loop.setLowerBound(cst0); } Value step = loop.step(); if (!isStepOne) { Value cst1 = builder.create(loc, 1); loop.setStep(cst1); } // Insert code computing the value of the original loop induction variable // from the "normalized" one. builder.setInsertionPointToStart(inner.getBody()); Value scaled = isStepOne ? loop.getInductionVar() : builder.create(loc, loop.getInductionVar(), step); Value shifted = isZeroBased ? scaled : builder.create(loc, scaled, lb); SmallPtrSet preserve{scaled.getDefiningOp(), shifted.getDefiningOp()}; replaceAllUsesExcept(loop.getInductionVar(), shifted, preserve); } void mlir::coalesceLoops(MutableArrayRef loops) { if (loops.size() < 2) return; loop::ForOp innermost = loops.back(); loop::ForOp outermost = loops.front(); // 1. Make sure all loops iterate from 0 to upperBound with step 1. This // allows the following code to assume upperBound is the number of iterations. for (auto loop : loops) normalizeLoop(loop, outermost, innermost); // 2. Emit code computing the upper bound of the coalesced loop as product // of the number of iterations of all loops. OpBuilder builder(outermost); Location loc = outermost.getLoc(); Value upperBound = outermost.upperBound(); for (auto loop : loops.drop_front()) upperBound = builder.create(loc, upperBound, loop.upperBound()); outermost.setUpperBound(upperBound); builder.setInsertionPointToStart(outermost.getBody()); // 3. Remap induction variables. For each original loop, the value of the // induction variable can be obtained by dividing the induction variable of // the linearized loop by the total number of iterations of the loops nested // in it modulo the number of iterations in this loop (remove the values // related to the outer loops): // iv_i = floordiv(iv_linear, product-of-loop-ranges-until-i) mod range_i. // Compute these iteratively from the innermost loop by creating a "running // quotient" of division by the range. Value previous = outermost.getInductionVar(); for (unsigned i = 0, e = loops.size(); i < e; ++i) { unsigned idx = loops.size() - i - 1; if (i != 0) previous = builder.create(loc, previous, loops[idx + 1].upperBound()); Value iv = (i == e - 1) ? previous : builder.create( loc, previous, loops[idx].upperBound()); replaceAllUsesInRegionWith(loops[idx].getInductionVar(), iv, loops.back().region()); } // 4. Move the operations from the innermost just above the second-outermost // loop, delete the extra terminator and the second-outermost loop. loop::ForOp second = loops[1]; innermost.getBody()->back().erase(); outermost.getBody()->getOperations().splice( Block::iterator(second.getOperation()), innermost.getBody()->getOperations()); second.erase(); } void mlir::mapLoopToProcessorIds(loop::ForOp forOp, ArrayRef processorId, ArrayRef numProcessors) { assert(processorId.size() == numProcessors.size()); if (processorId.empty()) return; OpBuilder b(forOp); Location loc(forOp.getLoc()); Value mul = processorId.front(); for (unsigned i = 1, e = processorId.size(); i < e; ++i) mul = b.create(loc, b.create(loc, mul, numProcessors[i]), processorId[i]); Value lb = b.create(loc, forOp.lowerBound(), b.create(loc, forOp.step(), mul)); forOp.setLowerBound(lb); Value step = forOp.step(); for (auto numProcs : numProcessors) step = b.create(loc, step, numProcs); forOp.setStep(step); } /// Given a memref region, determine the lowest depth at which transfers can be /// placed for it, and return the corresponding block, start and end positions /// in the block for placing incoming (read) and outgoing (write) copies /// respectively. The lowest depth depends on whether the region being accessed /// is hoistable with respect to one or more immediately surrounding loops. static void findHighestBlockForPlacement(const MemRefRegion ®ion, Block &block, Block::iterator &begin, Block::iterator &end, Block **copyPlacementBlock, Block::iterator *copyInPlacementStart, Block::iterator *copyOutPlacementStart) { const auto *cst = region.getConstraints(); SmallVector symbols; cst->getIdValues(cst->getNumDimIds(), cst->getNumDimAndSymbolIds(), &symbols); SmallVector enclosingFors; getLoopIVs(*block.begin(), &enclosingFors); // Walk up loop parents till we find an IV on which this region is // symbolic/variant. auto it = enclosingFors.rbegin(); for (auto e = enclosingFors.rend(); it != e; ++it) { // TODO(bondhugula): also need to be checking this for regions symbols that // aren't loop IVs, whether we are within their resp. defs' dominance scope. if (llvm::is_contained(symbols, it->getInductionVar())) break; } if (it != enclosingFors.rbegin()) { auto lastInvariantIV = *std::prev(it); *copyInPlacementStart = Block::iterator(lastInvariantIV.getOperation()); *copyOutPlacementStart = std::next(*copyInPlacementStart); *copyPlacementBlock = lastInvariantIV.getOperation()->getBlock(); } else { *copyInPlacementStart = begin; *copyOutPlacementStart = end; *copyPlacementBlock = █ } } // Info comprising stride and number of elements transferred every stride. struct StrideInfo { int64_t stride; int64_t numEltPerStride; }; /// Returns striding information for a copy/transfer of this region with /// potentially multiple striding levels from outermost to innermost. For an /// n-dimensional region, there can be at most n-1 levels of striding /// successively nested. // TODO(bondhugula): make this work with non-identity layout maps. static void getMultiLevelStrides(const MemRefRegion ®ion, ArrayRef bufferShape, SmallVectorImpl *strideInfos) { if (bufferShape.size() <= 1) return; int64_t numEltPerStride = 1; int64_t stride = 1; for (int d = bufferShape.size() - 1; d >= 1; d--) { int64_t dimSize = region.memref.getType().cast().getDimSize(d); stride *= dimSize; numEltPerStride *= bufferShape[d]; // A stride is needed only if the region has a shorter extent than the // memref along the dimension *and* has an extent greater than one along the // next major dimension. if (bufferShape[d] < dimSize && bufferShape[d - 1] > 1) { strideInfos->push_back({stride, numEltPerStride}); } } } /// Generates a point-wise copy from/to `memref' to/from `fastMemRef' and /// returns the outermost AffineForOp of the copy loop nest. `memIndicesStart' /// holds the lower coordinates of the region in the original memref to copy /// in/out. If `copyOut' is true, generates a copy-out; otherwise a copy-in. static AffineForOp generatePointWiseCopy(Location loc, Value memref, Value fastMemRef, AffineMap memAffineMap, ArrayRef memIndicesStart, ArrayRef fastBufferShape, bool isCopyOut, OpBuilder b) { assert(!memIndicesStart.empty() && "only 1-d or more memrefs"); // The copy-in nest is generated as follows as an example for a 2-d region: // for x = ... // for y = ... // fast_buf[x][y] = buf[mem_x + x][mem_y + y] SmallVector fastBufIndices, memIndices; AffineForOp copyNestRoot; for (unsigned d = 0, e = fastBufferShape.size(); d < e; ++d) { auto forOp = b.create(loc, 0, fastBufferShape[d]); if (d == 0) copyNestRoot = forOp; b = forOp.getBodyBuilder(); fastBufIndices.push_back(forOp.getInductionVar()); Value memBase = (memAffineMap == b.getMultiDimIdentityMap(memAffineMap.getNumDims())) ? memIndicesStart[d] : b.create( loc, AffineMap::get(memAffineMap.getNumDims(), memAffineMap.getNumSymbols(), memAffineMap.getResult(d)), memIndicesStart); // Construct the subscript for the slow memref being copied. auto memIndex = b.create( loc, AffineMap::get(2, 0, b.getAffineDimExpr(0) + b.getAffineDimExpr(1)), ValueRange({memBase, forOp.getInductionVar()})); memIndices.push_back(memIndex); } if (!isCopyOut) { // Copy in. auto load = b.create(loc, memref, memIndices); b.create(loc, load, fastMemRef, fastBufIndices); return copyNestRoot; } // Copy out. auto load = b.create(loc, fastMemRef, fastBufIndices); b.create(loc, load, memref, memIndices); return copyNestRoot; } static InFlightDiagnostic LLVM_ATTRIBUTE_UNUSED emitRemarkForBlock(Block &block) { return block.getParentOp()->emitRemark(); } /// Creates a buffer in the faster memory space for the specified memref region; /// generates a copy from the lower memory space to this one, and replaces all /// loads/stores in the block range [`begin', `end') of `block' to load/store /// from that buffer. Returns failure if copies could not be generated due to /// yet unimplemented cases. `copyInPlacementStart` and `copyOutPlacementStart` /// in copyPlacementBlock specify the insertion points where the incoming copies /// and outgoing copies, respectively, should be inserted (the insertion happens /// right before the insertion point). Since `begin` can itself be invalidated /// due to the memref rewriting done from this method, the output argument /// `nBegin` is set to its replacement (set to `begin` if no invalidation /// happens). Since outgoing copies could have been inserted at `end`, the /// output argument `nEnd` is set to the new end. `sizeInBytes` is set to the /// size of the fast buffer allocated. static LogicalResult generateCopy( const MemRefRegion ®ion, Block *block, Block::iterator begin, Block::iterator end, Block *copyPlacementBlock, Block::iterator copyInPlacementStart, Block::iterator copyOutPlacementStart, AffineCopyOptions copyOptions, DenseMap &fastBufferMap, DenseSet ©Nests, uint64_t *sizeInBytes, Block::iterator *nBegin, Block::iterator *nEnd) { *nBegin = begin; *nEnd = end; FuncOp f = begin->getParentOfType(); OpBuilder topBuilder(f.getBody()); Value zeroIndex = topBuilder.create(f.getLoc(), 0); if (begin == end) return success(); // Is the copy out point at the end of the block where we are doing // explicit copying. bool isCopyOutAtEndOfBlock = (end == copyOutPlacementStart); // Copies for read regions are going to be inserted at 'begin'. OpBuilder prologue(copyPlacementBlock, copyInPlacementStart); // Copies for write regions are going to be inserted at 'end'. OpBuilder epilogue(copyPlacementBlock, copyOutPlacementStart); OpBuilder &b = region.isWrite() ? epilogue : prologue; // Builder to create constants at the top level. auto func = copyPlacementBlock->getParent()->getParentOfType(); OpBuilder top(func.getBody()); auto loc = region.loc; auto memref = region.memref; auto memRefType = memref.getType().cast(); auto layoutMaps = memRefType.getAffineMaps(); if (layoutMaps.size() > 1 || (layoutMaps.size() == 1 && !layoutMaps[0].isIdentity())) { LLVM_DEBUG(llvm::dbgs() << "Non-identity layout map not yet supported\n"); return failure(); } // Indices to use for the copying. // Indices for the original memref being copied from/to. SmallVector memIndices; // Indices for the faster buffer being copied into/from. SmallVector bufIndices; unsigned rank = memRefType.getRank(); SmallVector fastBufferShape; // Compute the extents of the buffer. std::vector> lbs; SmallVector lbDivisors; lbs.reserve(rank); Optional numElements = region.getConstantBoundingSizeAndShape( &fastBufferShape, &lbs, &lbDivisors); if (!numElements.hasValue()) { LLVM_DEBUG(llvm::dbgs() << "Non-constant region size not supported\n"); return failure(); } if (numElements.getValue() == 0) { LLVM_DEBUG(llvm::dbgs() << "Nothing to copy\n"); *sizeInBytes = 0; return success(); } const FlatAffineConstraints *cst = region.getConstraints(); // 'regionSymbols' hold values that this memory region is symbolic/parametric // on; these typically include loop IVs surrounding the level at which the // copy generation is being done or other valid symbols in MLIR. SmallVector regionSymbols; cst->getIdValues(rank, cst->getNumIds(), ®ionSymbols); // Construct the index expressions for the fast memory buffer. The index // expression for a particular dimension of the fast buffer is obtained by // subtracting out the lower bound on the original memref's data region // along the corresponding dimension. // Index start offsets for faster memory buffer relative to the original. SmallVector offsets; offsets.reserve(rank); for (unsigned d = 0; d < rank; d++) { assert(lbs[d].size() == cst->getNumCols() - rank && "incorrect bound size"); AffineExpr offset = top.getAffineConstantExpr(0); for (unsigned j = 0, e = cst->getNumCols() - rank - 1; j < e; j++) { offset = offset + lbs[d][j] * top.getAffineDimExpr(j); } assert(lbDivisors[d] > 0); offset = (offset + lbs[d][cst->getNumCols() - 1 - rank]).floorDiv(lbDivisors[d]); // Set copy start location for this dimension in the lower memory space // memref. if (auto caf = offset.dyn_cast()) { auto indexVal = caf.getValue(); if (indexVal == 0) { memIndices.push_back(zeroIndex); } else { memIndices.push_back( top.create(loc, indexVal).getResult()); } } else { // The coordinate for the start location is just the lower bound along the // corresponding dimension on the memory region (stored in 'offset'). auto map = AffineMap::get( cst->getNumDimIds() + cst->getNumSymbolIds() - rank, 0, offset); memIndices.push_back(b.create(loc, map, regionSymbols)); } // The fast buffer is copied into at location zero; addressing is relative. bufIndices.push_back(zeroIndex); // Record the offsets since they are needed to remap the memory accesses of // the original memref further below. offsets.push_back(offset); } // The faster memory space buffer. Value fastMemRef; // Check if a buffer was already created. bool existingBuf = fastBufferMap.count(memref) > 0; if (!existingBuf) { AffineMap fastBufferLayout = b.getMultiDimIdentityMap(rank); auto fastMemRefType = MemRefType::get(fastBufferShape, memRefType.getElementType(), fastBufferLayout, copyOptions.fastMemorySpace); // Create the fast memory space buffer just before the 'affine.for' // operation. fastMemRef = prologue.create(loc, fastMemRefType).getResult(); // Record it. fastBufferMap[memref] = fastMemRef; // fastMemRefType is a constant shaped memref. *sizeInBytes = getMemRefSizeInBytes(fastMemRefType).getValue(); LLVM_DEBUG(emitRemarkForBlock(*block) << "Creating fast buffer of type " << fastMemRefType << " and size " << llvm::divideCeil(*sizeInBytes, 1024) << " KiB\n"); } else { // Reuse the one already created. fastMemRef = fastBufferMap[memref]; *sizeInBytes = 0; } auto numElementsSSA = top.create(loc, numElements.getValue()); SmallVector strideInfos; getMultiLevelStrides(region, fastBufferShape, &strideInfos); // TODO(bondhugula): use all stride levels once DmaStartOp is extended for // multi-level strides. if (strideInfos.size() > 1) { LLVM_DEBUG(llvm::dbgs() << "Only up to one level of stride supported\n"); return failure(); } Value stride = nullptr; Value numEltPerStride = nullptr; if (!strideInfos.empty()) { stride = top.create(loc, strideInfos[0].stride); numEltPerStride = top.create(loc, strideInfos[0].numEltPerStride); } // Record the last operation where we want the memref replacement to end. We // later do the memref replacement only in [begin, postDomFilter] so // that the original memref's used in the data movement code themselves don't // get replaced. auto postDomFilter = std::prev(end); // Create fully composed affine maps for each memref. auto memAffineMap = b.getMultiDimIdentityMap(memIndices.size()); fullyComposeAffineMapAndOperands(&memAffineMap, &memIndices); auto bufAffineMap = b.getMultiDimIdentityMap(bufIndices.size()); fullyComposeAffineMapAndOperands(&bufAffineMap, &bufIndices); if (!copyOptions.generateDma) { // Point-wise copy generation. auto copyNest = generatePointWiseCopy(loc, memref, fastMemRef, memAffineMap, memIndices, fastBufferShape, /*isCopyOut=*/region.isWrite(), b); // Record this so that we can skip it from yet another copy. copyNests.insert(copyNest); // Since new ops are being appended (for copy out's), adjust the end to // mark end of block range being processed if necessary. if (region.isWrite() && isCopyOutAtEndOfBlock) *nEnd = Block::iterator(copyNest.getOperation()); } else { // DMA generation. // Create a tag (single element 1-d memref) for the DMA. auto tagMemRefType = MemRefType::get({1}, top.getIntegerType(32), {}, copyOptions.tagMemorySpace); auto tagMemRef = prologue.create(loc, tagMemRefType); SmallVector tagIndices({zeroIndex}); auto tagAffineMap = b.getMultiDimIdentityMap(tagIndices.size()); fullyComposeAffineMapAndOperands(&tagAffineMap, &tagIndices); if (!region.isWrite()) { // DMA non-blocking read from original buffer to fast buffer. b.create(loc, memref, memAffineMap, memIndices, fastMemRef, bufAffineMap, bufIndices, tagMemRef, tagAffineMap, tagIndices, numElementsSSA, stride, numEltPerStride); } else { // DMA non-blocking write from fast buffer to the original memref. auto op = b.create( loc, fastMemRef, bufAffineMap, bufIndices, memref, memAffineMap, memIndices, tagMemRef, tagAffineMap, tagIndices, numElementsSSA, stride, numEltPerStride); // Since new ops may be appended at 'end' (for outgoing DMAs), adjust the // end to mark end of block range being processed. if (isCopyOutAtEndOfBlock) *nEnd = Block::iterator(op.getOperation()); } // Matching DMA wait to block on completion; tag always has a 0 index. b.create(loc, tagMemRef, tagAffineMap, zeroIndex, numElementsSSA); // Generate dealloc for the tag. auto tagDeallocOp = epilogue.create(loc, tagMemRef); if (*nEnd == end && isCopyOutAtEndOfBlock) // Since new ops are being appended (for outgoing DMAs), adjust the end to // mark end of range of the original. *nEnd = Block::iterator(tagDeallocOp.getOperation()); } // Generate dealloc for the buffer. if (!existingBuf) { auto bufDeallocOp = epilogue.create(loc, fastMemRef); // When generating pointwise copies, `nEnd' has to be set to deallocOp on // the fast buffer (since it marks the new end insertion point). if (!copyOptions.generateDma && *nEnd == end && isCopyOutAtEndOfBlock) *nEnd = Block::iterator(bufDeallocOp.getOperation()); } // Replace all uses of the old memref with the faster one while remapping // access indices (subtracting out lower bound offsets for each dimension). // Ex: to replace load %A[%i, %j] with load %Abuf[%i - %iT, %j - %jT], // index remap will be (%i, %j) -> (%i - %iT, %j - %jT), // i.e., affine.apply (d0, d1, d2, d3) -> (d2-d0, d3-d1) (%iT, %jT, %i, %j), // and (%iT, %jT) will be the 'extraOperands' for 'rep all memref uses with'. // d2, d3 correspond to the original indices (%i, %j). SmallVector remapExprs; remapExprs.reserve(rank); for (unsigned i = 0; i < rank; i++) { // The starting operands of indexRemap will be regionSymbols (the symbols on // which the memref region is parametric); then those corresponding to // the memref's original indices follow. auto dimExpr = b.getAffineDimExpr(regionSymbols.size() + i); remapExprs.push_back(dimExpr - offsets[i]); } auto indexRemap = AffineMap::get(regionSymbols.size() + rank, 0, remapExprs); // Record the begin since it may be invalidated by memref replacement. Block::iterator prevOfBegin; bool isBeginAtStartOfBlock = (begin == block->begin()); if (!isBeginAtStartOfBlock) prevOfBegin = std::prev(begin); // *Only* those uses within the range [begin, end) of 'block' are replaced. replaceAllMemRefUsesWith(memref, fastMemRef, /*extraIndices=*/{}, indexRemap, /*extraOperands=*/regionSymbols, /*symbolOperands=*/{}, /*domInstFilter=*/&*begin, /*postDomInstFilter=*/&*postDomFilter); *nBegin = isBeginAtStartOfBlock ? block->begin() : std::next(prevOfBegin); return success(); } /// Construct the memref region to just include the entire memref. Returns false /// dynamic shaped memref's for now. `numParamLoopIVs` is the number of /// enclosing loop IVs of opInst (starting from the outermost) that the region /// is parametric on. static bool getFullMemRefAsRegion(Operation *opInst, unsigned numParamLoopIVs, MemRefRegion *region) { unsigned rank; if (auto loadOp = dyn_cast(opInst)) { rank = loadOp.getMemRefType().getRank(); region->memref = loadOp.getMemRef(); region->setWrite(false); } else if (auto storeOp = dyn_cast(opInst)) { rank = storeOp.getMemRefType().getRank(); region->memref = storeOp.getMemRef(); region->setWrite(true); } else { assert(false && "expected load or store op"); return false; } auto memRefType = region->memref.getType().cast(); if (!memRefType.hasStaticShape()) return false; auto *regionCst = region->getConstraints(); // Just get the first numSymbols IVs, which the memref region is parametric // on. SmallVector ivs; getLoopIVs(*opInst, &ivs); ivs.resize(numParamLoopIVs); SmallVector symbols; extractForInductionVars(ivs, &symbols); regionCst->reset(rank, numParamLoopIVs, 0); regionCst->setIdValues(rank, rank + numParamLoopIVs, symbols); // Memref dim sizes provide the bounds. for (unsigned d = 0; d < rank; d++) { auto dimSize = memRefType.getDimSize(d); assert(dimSize > 0 && "filtered dynamic shapes above"); regionCst->addConstantLowerBound(d, 0); regionCst->addConstantUpperBound(d, dimSize - 1); } return true; } /// Generates copies for a contiguous sequence of operations in `block` in the /// iterator range [`begin', `end'), where `end' can't be past the terminator of /// the block (since additional operations are potentially inserted right before /// `end'. Returns the total size of the fast buffers used. // Since we generate alloc's and dealloc's for all fast buffers (before and // after the range of operations resp.), all of the fast memory capacity is // assumed to be available for processing this block range. uint64_t mlir::affineDataCopyGenerate(Block::iterator begin, Block::iterator end, const AffineCopyOptions ©Options, DenseSet ©Nests) { if (begin == end) return 0; assert(begin->getBlock() == std::prev(end)->getBlock() && "Inconsistent block begin/end args"); assert(end != end->getBlock()->end() && "end can't be the block terminator"); Block *block = begin->getBlock(); // Copies will be generated for this depth, i.e., symbolic in all loops // surrounding the this block range. unsigned copyDepth = getNestingDepth(*begin); LLVM_DEBUG(llvm::dbgs() << "Generating copies at depth " << copyDepth << "\n"); LLVM_DEBUG(llvm::dbgs() << "from begin: " << *begin << "\n"); LLVM_DEBUG(llvm::dbgs() << "to inclusive end: " << *std::prev(end) << "\n"); // List of memory regions to copy for. We need a map vector to have a // guaranteed iteration order to write test cases. CHECK-DAG doesn't help here // since the alloc's for example are identical except for the SSA id. SmallMapVector, 4> readRegions; SmallMapVector, 4> writeRegions; // Map from original memref's to the fast buffers that their accesses are // replaced with. DenseMap fastBufferMap; // To check for errors when walking the block. bool error = false; // Walk this range of operations to gather all memory regions. block->walk(begin, end, [&](Operation *opInst) { // Gather regions to allocate to buffers in faster memory space. if (auto loadOp = dyn_cast(opInst)) { if ((loadOp.getMemRefType().getMemorySpace() != copyOptions.slowMemorySpace)) return; } else if (auto storeOp = dyn_cast(opInst)) { if (storeOp.getMemRefType().getMemorySpace() != copyOptions.slowMemorySpace) return; } else { // Neither load nor a store op. return; } // Compute the MemRefRegion accessed. auto region = std::make_unique(opInst->getLoc()); if (failed(region->compute(opInst, copyDepth))) { LLVM_DEBUG(llvm::dbgs() << "Error obtaining memory region: semi-affine maps?\n"); LLVM_DEBUG(llvm::dbgs() << "over-approximating to the entire memref\n"); if (!getFullMemRefAsRegion(opInst, copyDepth, region.get())) { LLVM_DEBUG( opInst->emitError("non-constant memref sizes not yet supported")); error = true; return; } } // Each memref has a single buffer associated with it irrespective of how // many load's and store's happen on it. // TODO(bondhugula): in the future, when regions don't intersect and satisfy // other properties (based on load/store regions), we could consider // multiple buffers per memref. // Add to the appropriate region if it's not already in it, or take a // bounding box union with the existing one if it's already in there. // Note that a memref may have both read and write regions - so update the // region in the other list if one exists (write in case of read and vice // versa) since there is a single bounding box for a memref across all reads // and writes that happen on it. // Attempts to update; returns true if 'region' exists in targetRegions. auto updateRegion = [&](const SmallMapVector, 4> &targetRegions) { auto it = targetRegions.find(region->memref); if (it == targetRegions.end()) return false; // Perform a union with the existing region. if (failed(it->second->unionBoundingBox(*region))) { LLVM_DEBUG(llvm::dbgs() << "Memory region bounding box failed; " "over-approximating to the entire memref\n"); // If the union fails, we will overapproximate. if (!getFullMemRefAsRegion(opInst, copyDepth, region.get())) { LLVM_DEBUG(opInst->emitError( "non-constant memref sizes not yet supported")); error = true; return true; } it->second->getConstraints()->clearAndCopyFrom( *region->getConstraints()); } else { // Union was computed and stored in 'it->second': copy to 'region'. region->getConstraints()->clearAndCopyFrom( *it->second->getConstraints()); } return true; }; bool existsInRead = updateRegion(readRegions); if (error) return; bool existsInWrite = updateRegion(writeRegions); if (error) return; // Finally add it to the region list. if (region->isWrite() && !existsInWrite) { writeRegions[region->memref] = std::move(region); } else if (!region->isWrite() && !existsInRead) { readRegions[region->memref] = std::move(region); } }); if (error) { begin->emitError( "copy generation failed for one or more memref's in this block\n"); return 0; } uint64_t totalCopyBuffersSizeInBytes = 0; bool ret = true; auto processRegions = [&](const SmallMapVector, 4> ®ions) { for (const auto ®ionEntry : regions) { // For each region, hoist copy in/out past all hoistable // 'affine.for's. Block::iterator copyInPlacementStart, copyOutPlacementStart; Block *copyPlacementBlock; findHighestBlockForPlacement( *regionEntry.second, *block, begin, end, ©PlacementBlock, ©InPlacementStart, ©OutPlacementStart); uint64_t sizeInBytes; Block::iterator nBegin, nEnd; LogicalResult iRet = generateCopy( *regionEntry.second, block, begin, end, copyPlacementBlock, copyInPlacementStart, copyOutPlacementStart, copyOptions, fastBufferMap, copyNests, &sizeInBytes, &nBegin, &nEnd); if (succeeded(iRet)) { // begin/end could have been invalidated, and need update. begin = nBegin; end = nEnd; totalCopyBuffersSizeInBytes += sizeInBytes; } ret = ret & succeeded(iRet); } }; processRegions(readRegions); processRegions(writeRegions); if (!ret) { begin->emitError( "copy generation failed for one or more memref's in this block\n"); return totalCopyBuffersSizeInBytes; } // For a range of operations, a note will be emitted at the caller. AffineForOp forOp; uint64_t sizeInKib = llvm::divideCeil(totalCopyBuffersSizeInBytes, 1024); if (llvm::DebugFlag && (forOp = dyn_cast(&*begin))) { forOp.emitRemark() << sizeInKib << " KiB of copy buffers in fast memory space for this block\n"; } if (totalCopyBuffersSizeInBytes > copyOptions.fastMemCapacityBytes) { StringRef str = "Total size of all copy buffers' for this block " "exceeds fast memory capacity\n"; block->getParentOp()->emitError(str); } return totalCopyBuffersSizeInBytes; }