1 //===- FusionOnTensors.cpp - Implementation of linalg Fusion --------------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 // 9 // This file implements linalg fusion on tensors 10 // 11 //===----------------------------------------------------------------------===// 12 13 #include "PassDetail.h" 14 #include "mlir/Analysis/SliceAnalysis.h" 15 #include "mlir/Dialect/Affine/IR/AffineOps.h" 16 #include "mlir/Dialect/Linalg/IR/Linalg.h" 17 #include "mlir/Dialect/Linalg/Passes.h" 18 #include "mlir/Dialect/Linalg/Transforms/Transforms.h" 19 #include "mlir/Dialect/Linalg/Utils/Utils.h" 20 #include "mlir/Dialect/Tensor/IR/Tensor.h" 21 #include "mlir/IR/AffineExpr.h" 22 #include "mlir/IR/AffineMap.h" 23 #include "mlir/Support/LLVM.h" 24 25 using namespace mlir; 26 using namespace linalg; 27 28 //===----------------------------------------------------------------------===// 29 // StructuredOp specific helpers. 30 //===----------------------------------------------------------------------===// 31 32 /// Returns the tiled slice dimensions given the tiled consumer loop dimensions. 33 /// The slice defines a hyper rectangular iteration space and fusing the 34 /// producer is always possible. However, depending on the consumer indexing 35 /// map, not all slice elements may be consumed and the tiles may overlap. In 36 /// these cases, fusion introduces redundant computation. 37 static SmallVector<int64_t> getTiledSliceDims(OpOperand *consumerOperand, 38 ArrayRef<int64_t> tiledLoopDims) { 39 // Get the consumer operand indexing map. 40 LinalgOp consumerOp = consumerOperand->getOwner(); 41 AffineMap indexingMap = consumerOp.getTiedIndexingMap(consumerOperand); 42 43 // Search the slice dimensions tiled by a tile loop dimension. 44 DenseSet<int64_t> tiledSliceDimIndices; 45 for (const auto &en : enumerate(indexingMap.getResults())) { 46 for (auto tiledLoopDim : tiledLoopDims) { 47 if (en.value().isFunctionOfDim(tiledLoopDim)) 48 tiledSliceDimIndices.insert(en.index()); 49 } 50 } 51 return {tiledSliceDimIndices.begin(), tiledSliceDimIndices.end()}; 52 } 53 54 /// Given a vector of `tiledSliceDimIndices` that represent the tiled dimensions 55 /// of the producer result slice returns the tiled producer loop dimensions. 56 /// Example: 57 /// ``` 58 /// %res = linalg.fill(%cst, %input) 59 /// scf.for %i 60 /// scf.for %j 61 /// %slice = tensor.extract_slice %res[%i, %j] 62 /// ``` 63 /// getTiledProducerLoops(%res, [0, 1]) returns the loop indices [0, 1]. 64 static SmallVector<int64_t> 65 getTiledProducerLoops(OpResult producerResult, 66 ArrayRef<int64_t> tiledSliceDimIndices) { 67 LinalgOp producerOp = producerResult.getOwner(); 68 69 // Get the indexing map of the `producerOp` output operand that matches 70 // ´producerResult´. 71 AffineMap producerIndexingMap = producerOp.getTiedIndexingMap( 72 producerOp.getOutputOperand(producerResult.getResultNumber())); 73 74 // Keep only the tiled result slice dimensions of `producerIndexingMap`. 75 AffineMap tiledProducerIndexingSubMap = 76 producerIndexingMap.getSubMap(SmallVector<unsigned>( 77 tiledSliceDimIndices.begin(), tiledSliceDimIndices.end())); 78 79 // Compute the producer loop indices mapped to the tiled result slice 80 // dimensions. As the output indexing map of structured operations are 81 // projected permutations, `tiledProducerIndexingSubMap` has to be a 82 // projected permutation as well. We can thus obtain the producer loop indices 83 // by getting the positions of the result dimensions. 84 // Example: 85 // (d0, d1, d2) -> (d0, d2) has the result positions [0, 2]. 86 assert(tiledProducerIndexingSubMap.isProjectedPermutation() && 87 "expect slice and producer loop dimensions map one-to-one"); 88 SmallVector<int64_t> tiledProducerLoopIndices; 89 transform(llvm::seq<unsigned>(0, tiledProducerIndexingSubMap.getNumResults()), 90 std::back_inserter(tiledProducerLoopIndices), [&](unsigned idx) { 91 return tiledProducerIndexingSubMap.getDimPosition(idx); 92 }); 93 94 return tiledProducerLoopIndices; 95 } 96 97 /// Returns the producer fused in place of `sliceOp`. Tile the producer operands 98 /// along the `tiledSliceDimIndices` and clone the producer. Consider the case 99 /// of fusion of an output tensor: 100 /// ``` 101 /// %1 = producer ins(...) outs(%0) 102 /// %2 = consumer ins(...) outs(%1) 103 /// ``` 104 /// When consumer is tiled, %1 appears in the loop iter_args: 105 /// ``` 106 /// %1 = producer ins(...) outs(%0) 107 /// %2 = scf.for ... iter_args(%1) .. (%bbarg) { 108 /// %t1 = tensor.extract_slice %bbarg[..] 109 /// %t2 = consumer ins(...) outs(%t1) 110 /// %r = tensor.insert_slice %t2, %bbarg[...] 111 /// } 112 /// ``` 113 /// Fusing %1 into the loop requires updating iter_args(%1) to iter_args(%0): 114 /// ``` 115 /// %2 = scf.for ... iter_args(%0) .. (%bbarg) { 116 /// %t0 = tensor.extract_slice %bbarg[..] 117 /// %t1 = producer ins(...) outs(%t0) 118 /// %t2 = consumer ins(...) outs(%t1) 119 /// %r = tensor.insert_slice %t2, %bbarg[...] 120 /// } 121 /// ``` 122 /// This transformation is only valid if %bbarg is exclusively used by the 123 /// output ExtractSliceOp / InsertSliceOp pair, which is checked by the 124 /// `fuseProducer` method. 125 /// TODO: instead of check and failure, insert new iter_args each time a 126 /// producer is fused into a consumer and fold away unused iter_args. 127 static LinalgOp getTiledProducer(OpBuilder &b, OpResult producerResult, 128 tensor::ExtractSliceOp sliceOp, 129 ArrayRef<int64_t> tiledSliceDimIndices, 130 ArrayRef<int64_t> tiledProducerLoopIndices, 131 OpOperand *iterArg) { 132 // Clone the producer after `sliceOp` since the slice may be reused to pass in 133 // the producer result. 134 OpBuilder::InsertionGuard guard(b); 135 b.setInsertionPointAfter(sliceOp); 136 137 // Get the producer. 138 LinalgOp producerOp = producerResult.getOwner(); 139 Location loc = producerOp.getLoc(); 140 141 // Obtain the `producerOp` loop bounds and the `sliceOp` ranges. 142 SmallVector<Value> producerLoopBounds; 143 transform(producerOp.createLoopRanges(b, loc), 144 std::back_inserter(producerLoopBounds), 145 [](Range range) { return range.size; }); 146 SmallVector<Range> sliceOpRanges = sliceOp.getOrCreateRanges(b, loc); 147 148 // Tile the producer operands given the `sliceOp` ranges. Iterate the 149 // `tiledSliceDimIndices` and store the tile offset and size for the tiled 150 // slice dimension. 151 auto zero = b.create<arith::ConstantIndexOp>(loc, 0); 152 SmallVector<Value> tileIvs(producerOp.getNumLoops(), nullptr); 153 SmallVector<Value> tileSizes(producerOp.getNumLoops(), zero); 154 SmallVector<Value> allIvs(producerOp.getNumLoops(), nullptr); 155 for (auto it : zip(tiledSliceDimIndices, tiledProducerLoopIndices)) { 156 int64_t tiledSliceDim = std::get<0>(it); 157 int64_t tiledProducerLoop = std::get<1>(it); 158 tileIvs[tiledProducerLoop] = sliceOpRanges[tiledSliceDim].offset; 159 tileSizes[tiledProducerLoop] = sliceOpRanges[tiledSliceDim].size; 160 allIvs[tiledProducerLoop] = tileIvs[tiledProducerLoop]; 161 } 162 erase_value(tileIvs, nullptr); 163 SmallVector<Value> tiledOperands = producerOp.getInputAndOutputOperands(); 164 tiledOperands = makeTiledShapes(b, loc, producerOp, tiledOperands, tileIvs, 165 tileSizes, producerLoopBounds); 166 167 // Output fusion has to update the iteration arguments of the tile loop nest. 168 // In particular, the iteration argument of the outermost tile loop needs to 169 // be set to the producer output instead of the producer result and `clonedOp` 170 // shall use the existing `sliceOp` result instead of the tiled producer 171 // output operand. 172 if (iterArg) { 173 OpOperand *outputOperand = 174 producerOp.getOutputOperand(producerResult.getResultNumber()); 175 iterArg->set(outputOperand->get()); 176 tiledOperands[outputOperand->getOperandNumber()] = sliceOp.getResult(); 177 } 178 179 // Clone the producer using the tiled producer operands. 180 TypeRange resultTypes = ValueRange(tiledOperands) 181 .take_back(producerOp.getNumOutputs()) 182 .getTypes(); 183 LinalgOp clonedOp = producerOp.clone(b, loc, resultTypes, tiledOperands); 184 185 // Shift all IndexOp results by the tile offset. 186 addTileLoopIvsToIndexOpResults(b, clonedOp, allIvs); 187 188 return clonedOp; 189 } 190 191 //===----------------------------------------------------------------------===// 192 // TileLoopNest specific helpers. 193 //===----------------------------------------------------------------------===// 194 195 bool TileLoopNest::isEmpty() { return tileLoopOps.empty(); } 196 197 bool TileLoopNest::isValid() { 198 // Check if `rootOp` has been tiled at least once. 199 if (isEmpty() || tiledRootAndFusedOpsLoops.count(rootOp) == 0) 200 return false; 201 202 // Check if the number of loop operations and dimensions match. 203 if (tileLoopOps.size() != tiledRootAndFusedOpsLoops[rootOp].size()) 204 return false; 205 206 // Check if the innermost tile loop is the parent of `tiledOp`. 207 if (rootOp->getParentOp() != tileLoopOps.back()) 208 return false; 209 210 // Check if the tile loops are directly nested. 211 return std::adjacent_find(tileLoopOps.begin(), tileLoopOps.end(), 212 [](Operation *op1, Operation *op2) { 213 return op1 != op2->getParentOp(); 214 }) == tileLoopOps.end(); 215 } 216 217 SmallVector<BlockArgument> TileLoopNest::getTiedBBArgs(BlockArgument bbArg) { 218 assert(bbArg && "expect the block argument to be non-zero"); 219 SmallVector<BlockArgument> bbArgs; 220 221 // Search all tile loop block arguments from inner to outer. 222 for (auto tileLoop : reverse(tileLoopOps)) { 223 if (bbArg.getOwner()->getParentOp() != tileLoop) 224 return {}; 225 bbArgs.push_back(bbArg); 226 OpOperand *iterArg = &tileLoop.getOpOperandForRegionIterArg(bbArg); 227 bbArg = iterArg->get().dyn_cast<BlockArgument>(); 228 } 229 230 // Reverse the block arguments to order them from outer to inner. 231 return {bbArgs.rbegin(), bbArgs.rend()}; 232 } 233 234 OpOperand *TileLoopNest::getTiedIterArg(BlockArgument bbArg) { 235 // Search all block arguments and return the matching iteration argument. 236 SmallVector<BlockArgument> bbArgs = getTiedBBArgs(bbArg); 237 if (bbArgs.size() != tileLoopOps.size()) 238 return nullptr; 239 return &tileLoopOps.front().getOpOperandForRegionIterArg(bbArgs.front()); 240 } 241 242 bool TileLoopNest::hasOtherUses(BlockArgument bbArg, 243 tensor::ExtractSliceOp sliceOp) { 244 // Check the innermost block argument is either used by the ExtractSliceOp 245 // `sliceOp`, the matching InsertSliceOp, or by a DimOp. Handle other uses 246 // conservatively. 247 for (Operation *op : bbArg.getUsers()) { 248 if (!isa<tensor::DimOp, tensor::InsertSliceOp, tensor::ExtractSliceOp>(op)) 249 return false; 250 if (auto extractSliceOp = dyn_cast<tensor::ExtractSliceOp>(op)) { 251 if (extractSliceOp != sliceOp) 252 return false; 253 } 254 if (auto insertSliceOp = dyn_cast<tensor::InsertSliceOp>(op)) { 255 SetVector<Operation *> backwardSlice; 256 getBackwardSlice(insertSliceOp.source(), &backwardSlice, 257 [](Operation *op) { 258 return isa<LinalgOp, tensor::InsertSliceOp>(op); 259 }); 260 if (backwardSlice.empty() || backwardSlice.front() != sliceOp) 261 return false; 262 } 263 } 264 265 // Check the block arguments, except for the innermost one, have one use. 266 SmallVector<BlockArgument> bbArgs = getTiedBBArgs(bbArg); 267 return !all_of(bbArgs, [&](BlockArgument bbArg) { 268 return bbArg.hasOneUse() || bbArg == bbArgs.back(); 269 }); 270 } 271 272 LogicalResult TileLoopNest::tileRootOp( 273 OpBuilder &b, ArrayRef<int64_t> tileSizes, 274 ArrayRef<int64_t> tileInterchange, 275 Optional<LinalgLoopDistributionOptions> tileDistribution) { 276 // Exit if all tile sizes are zero. 277 if (tileSizes.size() == static_cast<size_t>(count(tileSizes, 0))) 278 return success(); 279 280 // Tile the root operation. 281 LinalgTilingOptions tilingOptions; 282 tilingOptions = tilingOptions 283 .setInterchange(SmallVector<unsigned>( 284 tileInterchange.begin(), tileInterchange.end())) 285 .setTileSizes(tileSizes) 286 .setLoopType(LinalgTilingLoopType::Loops); 287 if (tileDistribution) 288 tilingOptions = 289 tilingOptions.setDistributionOptions(tileDistribution.getValue()); 290 291 // TODO: Propagate RewriterBase everywhere. 292 IRRewriter rewriter(b); 293 FailureOr<TiledLinalgOp> tiledRootOp = 294 tileLinalgOp(rewriter, rootOp, tilingOptions); 295 296 // Exit if tiling the root operation fails. 297 if (failed(tiledRootOp)) 298 return failure(); 299 300 // Replace all uses of the root operation if it has been tiled before. All 301 // uses of the original untiled root operation are updated by the calling pass 302 // or pattern. 303 if (!isEmpty()) 304 rootOp->replaceAllUsesWith(tiledRootOp->tensorResults); 305 306 // Transfer the stored `rootOp` loop dimensions if it has been tiled before. 307 if (tiledRootAndFusedOpsLoops.count(rootOp) != 0) { 308 tiledRootAndFusedOpsLoops[tiledRootOp->op] = 309 tiledRootAndFusedOpsLoops[rootOp]; 310 } 311 312 // Update the root operation and append the loops and tile loop dimensions. 313 rootOp = tiledRootOp->op; 314 tileLoopOps.append(tiledRootOp->loops.begin(), tiledRootOp->loops.end()); 315 for (const auto &en : enumerate(tileSizes)) { 316 // Copy only the tiled loop dimensions with non-zero tile size. 317 if (en.value() == 0) 318 continue; 319 tiledRootAndFusedOpsLoops[rootOp].push_back(tileInterchange[en.index()]); 320 } 321 assert(isValid() && "expect tile loop nest to be valid after tiling"); 322 return success(); 323 } 324 325 FailureOr<LinalgOp> TileLoopNest::fuseProducer(OpBuilder &b, 326 OpOperand *consumerOpOperand) { 327 // Check if the consumer has been tiled before. For example, it may not have 328 // been tiled if the outermost tile loop is a reduction loop. 329 if (tiledRootAndFusedOpsLoops.count(consumerOpOperand->getOwner()) == 0) 330 return failure(); 331 332 assert(this->isValid() && 333 "expect the tile loop nest to satisfy all invariants"); 334 335 // Check the tile loop nest is non-empty. 336 if (isEmpty()) 337 return failure(); 338 339 // Check `consumerOpOperand` is defined by an ExtractSliceOp. 340 auto sliceOp = 341 consumerOpOperand->get().getDefiningOp<tensor::ExtractSliceOp>(); 342 if (!sliceOp) 343 return failure(); 344 345 // Check `sliceOp` and `consumerOp` are in the same block. 346 LinalgOp consumerOp = consumerOpOperand->getOwner(); 347 if (sliceOp->getBlock() != rootOp->getBlock() || 348 consumerOp->getBlock() != rootOp->getBlock()) 349 return failure(); 350 351 // Check if the producer is a LinalgOp possibly passed by iteration argument. 352 OpOperand *iterArg = nullptr; 353 auto producerResult = sliceOp.source().dyn_cast<OpResult>(); 354 if (auto bbArg = sliceOp.source().dyn_cast<BlockArgument>()) { 355 iterArg = getTiedIterArg(bbArg); 356 // Check the iteration argument may be used to pass in the producer output. 357 if (!iterArg || hasOtherUses(bbArg, sliceOp)) 358 return failure(); 359 producerResult = iterArg->get().dyn_cast<OpResult>(); 360 } 361 if (!producerResult || !isa<LinalgOp>(producerResult.getOwner())) 362 return failure(); 363 364 // Compute the tiled producer slice dimensions given the tiled consumer loops. 365 SmallVector<int64_t> tiledSliceDimIndices = getTiledSliceDims( 366 consumerOpOperand, tiledRootAndFusedOpsLoops[consumerOp]); 367 if (tiledSliceDimIndices.empty()) 368 return failure(); 369 370 // Compute the tiled producer loop indices. 371 SmallVector<int64_t> tiledProducerLoopIndices = 372 getTiledProducerLoops(producerResult, tiledSliceDimIndices); 373 374 // Tile the producer operands and clone the producer in place of `sliceOp`. 375 LinalgOp clonedOp = 376 getTiledProducer(b, producerResult, sliceOp, tiledSliceDimIndices, 377 tiledProducerLoopIndices, iterArg); 378 tiledRootAndFusedOpsLoops[clonedOp] = tiledProducerLoopIndices; 379 380 // Cast the `clonedOp` result to gap type mismatches before canonicalization. 381 Type consumerOperandType = consumerOpOperand->get().getType(); 382 Value newResult = clonedOp->getResult(producerResult.getResultNumber()); 383 if (newResult.getType() != consumerOperandType) { 384 OpBuilder::InsertionGuard guard(b); 385 b.setInsertionPointAfter(clonedOp); 386 newResult = b.create<tensor::CastOp>(producerResult.getLoc(), 387 consumerOperandType, newResult); 388 } 389 390 // Replace the `sliceOp` uses except for the `clonedOp` output uses. 391 sliceOp.getResult().replaceAllUsesExcept(newResult, clonedOp); 392 return clonedOp; 393 } 394 395 ValueRange TileLoopNest::getRootOpReplacementResults() { 396 assert(!isEmpty() && "expect tile loop nest to be non-empty"); 397 return tileLoopOps.front()->getOpResults(); 398 } 399 400 SmallVector<LinalgOp> TileLoopNest::getAllTiledAndFusedOps() { 401 SmallVector<LinalgOp> result; 402 for (const auto &kvp : tiledRootAndFusedOpsLoops) { 403 auto linalgOp = dyn_cast<LinalgOp>(kvp.getFirst()); 404 assert(linalgOp && 405 "expect all tiled and fused operations are linalg operations"); 406 result.push_back(linalgOp); 407 } 408 return result; 409 } 410 411 //===----------------------------------------------------------------------===// 412 // Tile and fuse entry-points. 413 //===----------------------------------------------------------------------===// 414 415 FailureOr<TileLoopNest> mlir::linalg::tileConsumerAndFuseProducers( 416 OpBuilder &b, LinalgOp consumerOp, ArrayRef<int64_t> tileSizes, 417 ArrayRef<int64_t> tileInterchange, 418 const Optional<LinalgLoopDistributionOptions> &tileDistribution) { 419 assert(tileSizes.size() == tileInterchange.size() && 420 "expect the number of tile sizes and interchange dims to match"); 421 assert(isPermutation(tileInterchange) && 422 "expect tile interchange is a permutation"); 423 424 // Create an empty tile loop nest. 425 TileLoopNest tileLoopNest(consumerOp); 426 427 // Search the number of outer parallel loops to separate them from possible 428 // inner reduction dimensions. 429 SmallVector<StringAttr> iterTypes = 430 llvm::to_vector<6>(consumerOp.iterator_types().getAsRange<StringAttr>()); 431 applyPermutationToVector(iterTypes, tileInterchange); 432 auto *it = find_if(iterTypes, [&](StringAttr iterType) { 433 return !isParallelIterator(iterType); 434 }); 435 int64_t split = std::distance(iterTypes.begin(), it); 436 437 // Helper to fuse the producers greedily using a queue of fusion candidates. 438 auto fuseProducersGreedily = [&](ArrayRef<OpOperand *> operands) { 439 SmallVector<OpOperand *> candidates(operands.begin(), operands.end()); 440 while (!candidates.empty()) { 441 FailureOr<LinalgOp> fusedProducer = 442 tileLoopNest.fuseProducer(b, candidates.pop_back_val()); 443 if (failed(fusedProducer)) 444 continue; 445 candidates.append(fusedProducer->getInputAndOutputOperands()); 446 } 447 }; 448 449 // Tile the outer parallel loops and fuse the output operands. 450 SmallVector<int64_t> outerTileSizes; 451 outerTileSizes.append(tileSizes.begin(), tileSizes.begin() + split); 452 outerTileSizes.append(tileSizes.size() - split, 0); 453 if (failed(tileLoopNest.tileRootOp(b, outerTileSizes, tileInterchange, 454 tileDistribution))) 455 return failure(); 456 fuseProducersGreedily(tileLoopNest.getRootOp().getOutputOperands()); 457 458 // Tile the remaining loops and fuse the input operands. 459 SmallVector<int64_t> innerTileSizes; 460 innerTileSizes.append(split, 0); 461 innerTileSizes.append(tileSizes.begin() + split, tileSizes.end()); 462 if (failed(tileLoopNest.tileRootOp(b, innerTileSizes, tileInterchange, 463 tileDistribution))) 464 return failure(); 465 fuseProducersGreedily(tileLoopNest.getRootOp().getInputOperands()); 466 467 // Exit if the tile loop nest is empty since all tile sizes are zero. 468 if (tileLoopNest.isEmpty()) 469 return failure(); 470 471 return tileLoopNest; 472 } 473