1 //===- Fusion.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/LinalgOps.h" 17 #include "mlir/Dialect/Linalg/IR/LinalgTypes.h" 18 #include "mlir/Dialect/Linalg/Passes.h" 19 #include "mlir/Dialect/Linalg/Transforms/Transforms.h" 20 #include "mlir/Dialect/Linalg/Utils/Utils.h" 21 #include "mlir/Dialect/Tensor/IR/Tensor.h" 22 #include "mlir/IR/AffineExpr.h" 23 #include "mlir/IR/AffineMap.h" 24 #include "mlir/Support/LLVM.h" 25 26 using namespace mlir; 27 using namespace linalg; 28 29 //===----------------------------------------------------------------------===// 30 // StructuredOp specific helpers. 31 //===----------------------------------------------------------------------===// 32 33 /// Returns the tiled slice dimensions given the tiled consumer loop dimensions. 34 /// The slice defines a hyper rectangular iteration space and fusing the 35 /// producer is always possible. However, depending on the consumer indexing 36 /// map, not all slice elements may be consumed and the tiles may overlap. In 37 /// these cases, fusion introduces redundant computation. 38 SmallVector<int64_t> getTiledSliceDims(OpOperand *consumerOperand, 39 ArrayRef<int64_t> tiledLoopDims) { 40 // Get the consumer operand indexing map. 41 LinalgOp consumerOp = consumerOperand->getOwner(); 42 AffineMap indexingMap = consumerOp.getTiedIndexingMap(consumerOperand); 43 44 // Search the slice dimensions tiled by a tile loop dimension. 45 DenseSet<int64_t> tiledSliceDims; 46 for (auto en : enumerate(indexingMap.getResults())) { 47 for (auto tiledLoopDim : tiledLoopDims) { 48 if (en.value().isFunctionOfDim(tiledLoopDim)) 49 tiledSliceDims.insert(en.index()); 50 } 51 } 52 return {tiledSliceDims.begin(), tiledSliceDims.end()}; 53 } 54 55 /// Returns the producer fused in place of `sliceOp`. Tile the producer operands 56 /// along the `tiledSliceDims` and clone the producer. Consider the case of 57 /// fusion of an output tensor: 58 /// ``` 59 /// %1 = producer ins(...) outs(%0) 60 /// %2 = consumer ins(...) outs(%1) 61 /// ``` 62 /// When consumer is tiled, %1 appears in the loop iter_args: 63 /// ``` 64 /// %1 = producer ins(...) outs(%0) 65 /// %2 = scf.for ... iter_args(%1) .. (%bbarg) { 66 /// %t1 = tensor.extract_slice %bbarg[..] 67 /// %t2 = consumer ins(...) outs(%t1) 68 /// %r = tensor.insert_slice %t2, %bbarg[...] 69 /// } 70 /// ``` 71 /// Fusing %1 into the loop requires updating iter_args(%1) to iter_args(%0): 72 /// ``` 73 /// %2 = scf.for ... iter_args(%0) .. (%bbarg) { 74 /// %t0 = tensor.extract_slice %bbarg[..] 75 /// %t1 = producer ins(...) outs(%t0) 76 /// %t2 = consumer ins(...) outs(%t1) 77 /// %r = tensor.insert_slice %t2, %bbarg[...] 78 /// } 79 /// ``` 80 /// This transformation is only valid if %bbarg is exclusively used by the 81 /// output ExtractSliceOp / InsertSliceOp pair, which is checked by the 82 /// `fuseProducer` method. 83 /// TODO: instead of check and failure, insert new iter_args each time a 84 /// producer is fused into a consumer and fold away unused iter_args. 85 static LinalgOp getTiledProducer(OpBuilder &b, OpResult producerResult, 86 tensor::ExtractSliceOp sliceOp, 87 ArrayRef<int64_t> tiledSliceDims, 88 OpOperand *iterArg) { 89 // Clone the producer after `sliceOp` since the slice may be reused to pass in 90 // the producer result. 91 OpBuilder::InsertionGuard guard(b); 92 b.setInsertionPointAfter(sliceOp); 93 94 // Get the producer. 95 LinalgOp producerOp = producerResult.getOwner(); 96 Location loc = producerOp.getLoc(); 97 98 // Obtain the `producerOp` loop bounds and the `sliceOp` ranges. 99 SmallVector<Value> producerLoopBounds; 100 transform(producerOp.createLoopRanges(b, loc), 101 std::back_inserter(producerLoopBounds), 102 [](Range range) { return range.size; }); 103 SmallVector<Range> sliceOpRanges = sliceOp.getOrCreateRanges(b, loc); 104 105 // Get the producer result indexing map. 106 AffineMap producerIndexingMap = producerOp.getTiedIndexingMap( 107 producerOp.getOutputOperand(producerResult.getResultNumber())); 108 109 // Tile the producer operands given the `sliceOp` ranges. Iterate the 110 // `tiledSliceDims` and store the tile offset and size for the tiled slice 111 // dimension. Assumes the mapping from slice dimensions to producer loops is a 112 // permutation. 113 auto zero = b.create<ConstantIndexOp>(loc, 0); 114 SmallVector<Value> tileIvs(producerOp.getNumLoops(), nullptr); 115 SmallVector<Value> tileSizes(producerOp.getNumLoops(), zero); 116 SmallVector<Value> allIvs(producerOp.getNumLoops(), nullptr); 117 for (int64_t tiledSliceDim : tiledSliceDims) { 118 AffineExpr result = producerIndexingMap.getResults()[tiledSliceDim]; 119 assert(result.isa<AffineDimExpr>() && 120 "expect producer indexing map is a projected permutation"); 121 int64_t tiledProducerLoop = result.cast<AffineDimExpr>().getPosition(); 122 tileIvs[tiledProducerLoop] = sliceOpRanges[tiledSliceDim].offset; 123 tileSizes[tiledProducerLoop] = sliceOpRanges[tiledSliceDim].size; 124 allIvs[tiledProducerLoop] = tileIvs[tiledProducerLoop]; 125 } 126 erase_value(tileIvs, nullptr); 127 SmallVector<Value> tiledOperands = producerOp.getInputAndOutputOperands(); 128 tiledOperands = makeTiledShapes(b, loc, producerOp, tiledOperands, tileIvs, 129 tileSizes, producerLoopBounds); 130 131 // Output fusion has to update the iteration arguments of the tile loop nest. 132 // In particular, the iteration argument of the outermost tile loop needs to 133 // be set to the producer output instead of the producer result and `clonedOp` 134 // shall use the existing `sliceOp` result instead of the tiled producer 135 // output operand. 136 if (iterArg) { 137 OpOperand *outputOperand = 138 producerOp.getOutputOperand(producerResult.getResultNumber()); 139 iterArg->set(outputOperand->get()); 140 tiledOperands[outputOperand->getOperandNumber()] = sliceOp.getResult(); 141 } 142 143 // Clone the producer using the tiled producer operands. 144 TypeRange resultTypes = ValueRange(tiledOperands) 145 .take_back(producerOp.getNumOutputs()) 146 .getTypes(); 147 LinalgOp clonedOp = producerOp.clone(b, loc, resultTypes, tiledOperands); 148 149 // Shift all IndexOp results by the tile offset. 150 addTileLoopIvsToIndexOpResults(b, clonedOp, allIvs); 151 152 return clonedOp; 153 } 154 155 //===----------------------------------------------------------------------===// 156 // TileLoopNest specific helpers. 157 //===----------------------------------------------------------------------===// 158 159 bool TileLoopNest::isEmpty() { return loopOps.empty(); } 160 161 bool TileLoopNest::isValid() { 162 // Check if the number of `tileLoopOps` and `tileLoopDims` match. 163 if (loopOps.size() != loopDims.size()) 164 return false; 165 166 // Check if the innermost tile loop is the parent of `tiledOp`. 167 if (rootOp->getParentOp() != loopOps.back()) 168 return false; 169 170 // Check if the tile loops are directly nested. 171 return std::adjacent_find(loopOps.begin(), loopOps.end(), 172 [](Operation *op1, Operation *op2) { 173 return op1 != op2->getParentOp(); 174 }) == loopOps.end(); 175 } 176 177 SmallVector<BlockArgument> TileLoopNest::getTiedBBArgs(BlockArgument bbArg) { 178 assert(bbArg && "expect the block argument to be non-zero"); 179 SmallVector<BlockArgument> bbArgs; 180 181 // Search all tile loop block arguments from inner to outer. 182 for (auto tileLoop : reverse(loopOps)) { 183 if (bbArg.getOwner()->getParentOp() != tileLoop) 184 return {}; 185 bbArgs.push_back(bbArg); 186 OpOperand *iterArg = &tileLoop.getOpOperandForRegionIterArg(bbArg); 187 bbArg = iterArg->get().dyn_cast<BlockArgument>(); 188 } 189 190 // Reverse the block arguments to order them from outer to inner. 191 return {bbArgs.rbegin(), bbArgs.rend()}; 192 } 193 194 OpOperand *TileLoopNest::getTiedIterArg(BlockArgument bbArg) { 195 // Search all block arguments and return the matching iteration argument. 196 SmallVector<BlockArgument> bbArgs = getTiedBBArgs(bbArg); 197 if (bbArgs.size() != loopOps.size()) 198 return nullptr; 199 return &loopOps.front().getOpOperandForRegionIterArg(bbArgs.front()); 200 } 201 202 bool TileLoopNest::hasOtherUses(BlockArgument bbArg, 203 tensor::ExtractSliceOp sliceOp) { 204 // Check the innermost block argument is either used by the ExtractSliceOp 205 // `sliceOp`, the matching InsertSliceOp, or by a DimOp. Handle other uses 206 // conservatively. 207 for (Operation *op : bbArg.getUsers()) { 208 if (!isa<tensor::DimOp, tensor::InsertSliceOp, tensor::ExtractSliceOp>(op)) 209 return false; 210 if (auto extractSliceOp = dyn_cast<tensor::ExtractSliceOp>(op)) { 211 if (extractSliceOp != sliceOp) 212 return false; 213 } 214 if (auto insertSliceOp = dyn_cast<tensor::InsertSliceOp>(op)) { 215 SetVector<Operation *> backwardSlice; 216 getBackwardSlice(insertSliceOp.source(), &backwardSlice, 217 [](Operation *op) { 218 return isa<LinalgOp, tensor::InsertSliceOp>(op); 219 }); 220 if (backwardSlice.empty() || backwardSlice.front() != sliceOp) 221 return false; 222 } 223 } 224 225 // Check the block arguments, except for the innermost one, have one use. 226 SmallVector<BlockArgument> bbArgs = getTiedBBArgs(bbArg); 227 return !all_of(bbArgs, [&](BlockArgument bbArg) { 228 return bbArg.hasOneUse() || bbArg == bbArgs.back(); 229 }); 230 } 231 232 LogicalResult TileLoopNest::tileRootOp(OpBuilder &b, 233 ArrayRef<int64_t> tileSizes, 234 ArrayRef<int64_t> tileInterchange) { 235 // Exit if all tile sizes are zero. 236 if (tileSizes.size() == static_cast<size_t>(count(tileSizes, 0))) 237 return success(); 238 239 // Tile the root operation. 240 LinalgTilingOptions tilingOptions; 241 tilingOptions = tilingOptions 242 .setInterchange(SmallVector<unsigned>( 243 tileInterchange.begin(), tileInterchange.end())) 244 .setTileSizes(tileSizes) 245 .setLoopType(LinalgTilingLoopType::Loops); 246 Optional<TiledLinalgOp> tiledRootOp = tileLinalgOp(b, rootOp, tilingOptions); 247 248 // Replace all uses of the root operation. 249 if (!tiledRootOp.hasValue()) 250 return failure(); 251 rootOp->replaceAllUsesWith(tiledRootOp->tensorResults); 252 253 // Update the root operation and append the loops and tile loop dimensions. 254 rootOp = tiledRootOp->op; 255 loopOps.append(tiledRootOp->loops.begin(), tiledRootOp->loops.end()); 256 for (auto en : enumerate(tileSizes)) { 257 // Copy only the tiled loop dimensions with non-zero tile size. 258 if (en.value() == 0) 259 continue; 260 loopDims.push_back(tileInterchange[en.index()]); 261 } 262 assert(isValid() && "expect tile loop nest to be valid after tiling"); 263 264 return success(); 265 } 266 267 FailureOr<LinalgOp> TileLoopNest::fuseProducer(OpBuilder &b, 268 OpOperand *rootOpOperand) { 269 assert(rootOpOperand->getOwner() == rootOp && 270 "expect the root op to be the owner of the operand to fuse"); 271 assert(this->isValid() && 272 "expect the tile loop nest to satisfy all invariants"); 273 274 // Check the tile loop nest is non-empty. 275 if (isEmpty()) 276 return failure(); 277 278 // Check `rootOpOperand` is defined by an ExtractSliceOp. 279 auto sliceOp = rootOpOperand->get().getDefiningOp<tensor::ExtractSliceOp>(); 280 if (!sliceOp) 281 return failure(); 282 283 // Check `sliceOp` is tiled by the tile loop nest. 284 if (sliceOp->getParentOp() != rootOp->getParentOp()) 285 return failure(); 286 287 // Check if the producer is a LinalgOp possibly passed by iteration argument. 288 OpOperand *iterArg = nullptr; 289 auto producerResult = sliceOp.source().dyn_cast<OpResult>(); 290 if (auto bbArg = sliceOp.source().dyn_cast<BlockArgument>()) { 291 iterArg = getTiedIterArg(bbArg); 292 // Check the iteration argument may be used to pass in the producer output. 293 if (!iterArg || hasOtherUses(bbArg, sliceOp)) 294 return failure(); 295 producerResult = iterArg->get().dyn_cast<OpResult>(); 296 } 297 if (!producerResult || !isa<LinalgOp>(producerResult.getOwner())) 298 return failure(); 299 300 // Compute the tiled producer slice dimensions given the tiled root operation 301 // loop dimensions `loopDims`. 302 SmallVector<int64_t> tiledSliceDims = 303 getTiledSliceDims(rootOpOperand, loopDims); 304 if (tiledSliceDims.empty()) 305 return failure(); 306 307 // Tile the producer operands and clone the producer in place of `sliceOp`. 308 LinalgOp clonedOp = 309 getTiledProducer(b, producerResult, sliceOp, tiledSliceDims, iterArg); 310 311 // Cast the `clonedOp` result to gap type mismatches before canonicalization. 312 Type consumerOperandType = rootOpOperand->get().getType(); 313 Value newResult = clonedOp->getResult(producerResult.getResultNumber()); 314 if (newResult.getType() != consumerOperandType) { 315 OpBuilder::InsertionGuard guard(b); 316 b.setInsertionPointAfter(clonedOp); 317 newResult = b.create<tensor::CastOp>(producerResult.getLoc(), 318 consumerOperandType, newResult); 319 } 320 321 // Replace the `sliceOp` uses except for the `clonedOp` output uses. 322 sliceOp.getResult().replaceAllUsesExcept(newResult, clonedOp); 323 return clonedOp; 324 } 325 326 //===----------------------------------------------------------------------===// 327 // Tile and fuse entry-points. 328 //===----------------------------------------------------------------------===// 329 330 FailureOr<TileLoopNest> 331 mlir::linalg::tileConsumerAndFuseProducers(OpBuilder &b, LinalgOp consumerOp, 332 ArrayRef<int64_t> tileSizes, 333 ArrayRef<int64_t> tileInterchange) { 334 assert(tileSizes.size() == tileInterchange.size() && 335 "expect the number of tile sizes and interchange dims to match"); 336 assert(isPermutation(tileInterchange) && 337 "expect tile interchange is a permutation"); 338 339 // Create an empty tile loop nest. 340 TileLoopNest tileLoopNest(consumerOp); 341 342 // Search the number of outer parallel loops to separate them from possible 343 // inner reduction dimensions. 344 SmallVector<StringAttr> iterTypes = 345 llvm::to_vector<6>(consumerOp.iterator_types().getAsRange<StringAttr>()); 346 applyPermutationToVector(iterTypes, tileInterchange); 347 auto *it = find_if(iterTypes, [&](StringAttr iterType) { 348 return !isParallelIterator(iterType); 349 }); 350 int64_t split = std::distance(iterTypes.begin(), it); 351 352 // Tile the outer parallel loops and fuse the output operands. 353 SmallVector<int64_t> outerTileSizes; 354 outerTileSizes.append(tileSizes.begin(), tileSizes.begin() + split); 355 outerTileSizes.append(tileSizes.size() - split, 0); 356 if (failed(tileLoopNest.tileRootOp(b, outerTileSizes, tileInterchange))) 357 return failure(); 358 for (OpOperand *opOperand : tileLoopNest.getRootOp().getOutputOperands()) 359 (void)tileLoopNest.fuseProducer(b, opOperand); 360 361 // Tile the remaining loops and fuse the input operands. 362 SmallVector<int64_t> innerTileSizes; 363 innerTileSizes.append(split, 0); 364 innerTileSizes.append(tileSizes.begin() + split, tileSizes.end()); 365 if (failed(tileLoopNest.tileRootOp(b, innerTileSizes, tileInterchange))) 366 return failure(); 367 SmallVector<OpOperand *> inputOperands = 368 tileLoopNest.getRootOp().getInputOperands(); 369 for (OpOperand *opOperand : tileLoopNest.getRootOp().getInputOperands()) 370 (void)tileLoopNest.fuseProducer(b, opOperand); 371 372 return tileLoopNest; 373 } 374 375 namespace { 376 struct LinalgTileAndFuseTensorOps 377 : public LinalgTileAndFuseTensorOpsBase<LinalgTileAndFuseTensorOps> { 378 379 void notifyFailure(StringRef message) { 380 llvm::errs() << " - LinalgTileAndFuseTensorOps: " << message << "\n"; 381 signalPassFailure(); 382 } 383 384 void runOnFunction() override { 385 FuncOp funcOp = getFunction(); 386 OpBuilder b(funcOp.getContext()); 387 388 // Heuristic to find a good operation to tile and start fusion. Walk all 389 // operations and select the one with the maximal backward slice of fusion 390 // candidates. 391 LinalgOp rootOp = nullptr; 392 int64_t numFusionCandidates = -1; 393 funcOp.walk([&](LinalgOp linalgOp) { 394 SetVector<Operation *> backwardSlice; 395 getBackwardSlice(linalgOp, &backwardSlice); 396 int64_t backwardSliceSize = count_if( 397 backwardSlice, [](Operation *op) { return isa<LinalgOp>(op); }); 398 if (backwardSliceSize > numFusionCandidates) { 399 rootOp = linalgOp; 400 numFusionCandidates = backwardSliceSize; 401 } 402 }); 403 if (!rootOp) 404 return notifyFailure("expect to find a root operation"); 405 406 // Check `tileSizes` contains a tile size for every `rootOp` loop dimension. 407 if (tileSizes.size() < rootOp.getNumLoops()) 408 return notifyFailure("expect #tile sizes >= #loops"); 409 410 // Check `tileInterchange` contains no entries or as many as `tileSizes`. 411 if (!tileInterchange.empty() && 412 tileInterchange.size() != tileSizes.size()) { 413 return notifyFailure( 414 "expect the number of tile sizes and interchange dims to match"); 415 } 416 417 // Copy the `tileSizes` and `tileInterchange` prefixes needed to tile 418 // `rootOp` or use the identity interchange if `tileInterchange` is empty. 419 SmallVector<int64_t> rootTileSizes( 420 tileSizes.begin(), tileSizes.begin() + rootOp.getNumLoops()); 421 SmallVector<int64_t> rootInterchange = 422 tileInterchange.empty() 423 ? llvm::to_vector<6>(llvm::seq<int64_t>(0, rootOp.getNumLoops())) 424 : SmallVector<int64_t>(tileInterchange.begin(), 425 tileInterchange.begin() + 426 rootOp.getNumLoops()); 427 428 // Check `rootInterchange` is a permutation of the `rootOp` loop dimensions. 429 // It has to be a permutation since the tiling cannot tile the same loop 430 // dimension multiple times. 431 if (!isPermutation(rootInterchange)) 432 return notifyFailure( 433 "expect the tile interchange permutes the root loops"); 434 435 // Tile `rootOp` and fuse its producers. 436 if (failed(tileConsumerAndFuseProducers(b, rootOp, rootTileSizes, 437 rootInterchange))) 438 return notifyFailure("tileConsumerAndFuseProducers failed unexpectedly"); 439 } 440 }; 441 } // namespace 442 443 std::unique_ptr<OperationPass<FuncOp>> 444 mlir::createLinalgTileAndFuseTensorOpsPass() { 445 return std::make_unique<LinalgTileAndFuseTensorOps>(); 446 } 447