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 the linalg dialect Fusion on tensors operations pass. 10 // 11 //===----------------------------------------------------------------------===// 12 #include "PassDetail.h" 13 #include "mlir/Dialect/Affine/IR/AffineOps.h" 14 #include "mlir/Dialect/Linalg/IR/LinalgOps.h" 15 #include "mlir/Dialect/Linalg/IR/LinalgTypes.h" 16 #include "mlir/Dialect/Linalg/Passes.h" 17 #include "mlir/Dialect/Linalg/Transforms/Transforms.h" 18 #include "mlir/Dialect/Linalg/Utils/Utils.h" 19 #include "mlir/IR/AffineExpr.h" 20 #include "mlir/IR/AffineMap.h" 21 #include "mlir/IR/PatternMatch.h" 22 #include "mlir/Support/LLVM.h" 23 #include "mlir/Transforms/GreedyPatternRewriteDriver.h" 24 25 using namespace mlir; 26 using namespace mlir::linalg; 27 28 /// Implementation of fusion of generic ops and indexed_generic ops. 29 static bool areElementwiseOpsFusable(LinalgOp producer, LinalgOp consumer, 30 unsigned consumerIdx) { 31 // Producer and consumer must have tensor semantics. 32 if (!producer.hasTensorSemantics() || !consumer.hasTensorSemantics()) 33 return false; 34 35 // Verify that 36 // - the producer has all "parallel" iterator type. 37 if (producer.getNumParallelLoops() != producer.getNumLoops()) 38 return false; 39 40 // Only allow fusing the producer of an input operand for now. 41 // TODO: allow fusing the producer of an output operand. 42 if (consumerIdx >= consumer.getNumInputs()) 43 return false; 44 45 // Get the consumer index map. The number of results of the consumer index 46 // map must match the number of loops of the producer. 47 AffineMap consumerIndexMap = consumer.getIndexingMap(consumerIdx); 48 if (consumerIndexMap.getNumResults() != producer.getNumLoops()) 49 return false; 50 51 // Currently support only operations with single result. 52 if (producer.getNumOutputs() != 1) 53 return false; 54 55 // Finally the index_map for the result must be invertible. For now just 56 // verify it is a permutation. 57 AffineMap producerResultIndexMap = producer.getOutputIndexingMap(0); 58 return producerResultIndexMap.isPermutation(); 59 } 60 61 /// Append to `fusedOpIndexingMapAttrs` the indexing maps for the operands of 62 /// the `producer` to use in the fused operation given the indexing map of the 63 /// result of the producer in the consumer. 64 static AffineMap getIndexingMapOfProducerOperandsInCoordinatesOfFusedOp( 65 OpOperand &producerOpOperand, AffineMap producerResultIndexMap, 66 AffineMap fusedConsumerArgIndexMap) { 67 // The indexing map in the consumer op (fusedConsumerArgIndexMap) is a map 68 // from consumer loop -> consumer arg tensor index/producer result tensor 69 // index. The fused loop is same as the consumer loop. For each producer arg 70 // the indexing map to be computed is a map from consumer loop -> producer 71 // arg tensor index. 72 // producerResultIndexMap is a map from producer loop -> tensor index. 73 // Compute the inverse to get map from tensor index -> producer loop. 74 // The inverse is a map from producer result tensor index -> producer loop. 75 AffineMap invProducerResultIndexMap = 76 inversePermutation(producerResultIndexMap); 77 assert(invProducerResultIndexMap && 78 "expected producer result indexig map to be invertible"); 79 80 LinalgOp producer = cast<LinalgOp>(producerOpOperand.getOwner()); 81 // argMap is a map from producer loop -> producer arg tensor index. 82 AffineMap argMap = 83 producer.getIndexingMap(producerOpOperand.getOperandNumber()); 84 85 // Compose argMap with invProducerResultIndexMap to get a map from 86 // producer result tensor index -> producer arg tensor index. 87 AffineMap t1 = argMap.compose(invProducerResultIndexMap); 88 89 // Compose t1 with fusedConsumerArgIndexMap gives an indexing map from 90 // consumer loop/ fused loop -> producer arg tensor index. 91 return t1.compose(fusedConsumerArgIndexMap); 92 } 93 94 /// Generate the region of the fused tensor operation. The region of the fused 95 /// op must be empty. 96 static void 97 generateFusedElementwiseOpRegion(PatternRewriter &rewriter, Operation *fusedOp, 98 LinalgOp producer, LinalgOp consumer, 99 AffineMap consumerToProducerLoopsMap, 100 unsigned consumerIdx, unsigned nloops) { 101 // Build the region of the fused op. 102 Block &producerBlock = producer->getRegion(0).front(); 103 Block &consumerBlock = consumer->getRegion(0).front(); 104 Block *fusedBlock = new Block(); 105 fusedOp->getRegion(0).push_back(fusedBlock); 106 BlockAndValueMapping mapper; 107 OpBuilder::InsertionGuard guard(rewriter); 108 rewriter.setInsertionPointToStart(fusedBlock); 109 110 // The block arguments are 111 // [index_0, index_1, ... , 112 // consumer_operand_0, ... , consumer_operand_(`consumerIdx`-1), 113 // producer_operand_0, ... , producer_operand_(n-1)], 114 // consumer_operand_(`consumerIdx`), .. consumer_operand_(m-1)] 115 // , where n is the number of producer's operand and m is the number 116 // consumer's operand. 117 // If both `numProducerIndices` and `numConsumerIndices` are zero, this is a 118 // generic op. In this case, there are no indices in block arguments. 119 unsigned numProducerIndices = isa<IndexedGenericOp>(producer.getOperation()) 120 ? producer.getNumLoops() 121 : 0; 122 unsigned numConsumerIndices = isa<IndexedGenericOp>(consumer.getOperation()) 123 ? consumer.getNumLoops() 124 : 0; 125 unsigned numFusedOpIndices = 126 (isa<IndexedGenericOp>(producer.getOperation()) || 127 isa<IndexedGenericOp>(consumer.getOperation())) 128 ? std::max(producer.getNumLoops(), consumer.getNumLoops()) 129 : 0; 130 131 // 0. Firstly, add all the indices to the block arguments. 132 for (unsigned i = 0, e = numFusedOpIndices; i < e; ++i) 133 fusedBlock->addArgument(rewriter.getIndexType()); 134 // 1. Map consumer indices to fusedBlock indices 1-1. 135 mapper.map(consumerBlock.getArguments().take_front(numConsumerIndices), 136 fusedBlock->getArguments().take_front(numConsumerIndices)); 137 // 2. Embed producer indices into fusedBlock index space 1-1. 138 for (auto it : 139 llvm::zip(producerBlock.getArguments().take_front(numProducerIndices), 140 fusedBlock->getArguments().take_front(numProducerIndices))) { 141 auto newIndex = rewriter.create<mlir::AffineApplyOp>( 142 producer.getLoc(), 143 consumerToProducerLoopsMap.getSubMap(std::get<0>(it).getArgNumber()), 144 fusedBlock->getArguments().take_front(numFusedOpIndices)); 145 mapper.map(std::get<0>(it), newIndex); 146 } 147 // TODO: allow fusing the producer of an output operand. 148 assert(consumerIdx < consumer.getNumInputs() && 149 "expected producer of input operand"); 150 // 3. Consumer input operands up to consumerIdx (exclusive). 151 for (BlockArgument bbArg : consumerBlock.getArguments() 152 .drop_front(numConsumerIndices) 153 .take_front(consumerIdx)) // input assumption. 154 mapper.map(bbArg, fusedBlock->addArgument(bbArg.getType())); 155 156 // Replacing consumerIdx requires getting the cloned, yielded, value from 157 // the (cloned) producer block. This happens in step 9. 158 159 // 4. Splice in producer's input operands. 160 for (BlockArgument bbArg : producerBlock.getArguments() 161 .drop_front(numProducerIndices) 162 .take_front(producer.getNumInputs())) 163 mapper.map(bbArg, fusedBlock->addArgument(bbArg.getType())); 164 165 // 4.b. Producer output operand/map that is fused needs to be mapped to the 166 // producer bbArg if it is an "initTensor" (i.e. its value is actually read). 167 assert(producer->getNumResults() == 1 && "expected single result producer"); 168 if (producer.isInitTensor(&producer.getOutputOpOperands()[0])) { 169 BlockArgument bbArg = 170 producerBlock.getArguments() 171 .drop_front(numConsumerIndices + producer.getNumInputs()) 172 // TODO: bbArg index of 173 .front(); 174 mapper.map(bbArg, fusedBlock->addArgument(bbArg.getType())); 175 } 176 // 5. Remaining consumer's input operands (drop past index `consumerIdx`). 177 for (BlockArgument bbArg : consumerBlock.getArguments() 178 .drop_front(numConsumerIndices) 179 .take_front(consumer.getNumInputs()) 180 .drop_front(consumerIdx + 1)) 181 mapper.map(bbArg, fusedBlock->addArgument(bbArg.getType())); 182 // 6. All of consumer's output operands. 183 for (BlockArgument bbArg : 184 consumerBlock.getArguments().take_back(consumer.getNumOutputs())) 185 mapper.map(bbArg, fusedBlock->addArgument(bbArg.getType())); 186 // 7. All of producer's output operands except the one fused. 187 // TODO: allow fusion of multi-result producers. 188 assert(producer->getNumResults() == 1 && "expected single result producer"); 189 190 // 8. Clone operations from producer (except the yield operation) to the fused 191 // op. 192 for (auto &op : producerBlock.without_terminator()) 193 rewriter.clone(op, mapper); 194 // 9. Now we can map the consumerBlock's `consumerIdx` block argument. Just 195 // forward the yield operand. 196 auto yieldOp = cast<linalg::YieldOp>(producerBlock.getTerminator()); 197 // TODO: allow fusion of multi-result producers. 198 assert(producer->getNumResults() == 1 && "expected single result producer"); 199 unsigned producerResultNumber = 0; 200 Value replacement = 201 mapper.lookupOrDefault(yieldOp.getOperand(producerResultNumber)); 202 // Sanity checks, if replacement is not already in the mapper then it must be 203 // produced outside. 204 if (replacement == yieldOp.getOperand(producerResultNumber)) { 205 if (auto bb = replacement.dyn_cast<BlockArgument>()) 206 assert(bb.getOwner() != &producerBlock && 207 "yielded block argument must have been mapped"); 208 else 209 assert(!producer->isAncestor(replacement.getDefiningOp()) && 210 "yielded value must have been mapped"); 211 } 212 mapper.map(consumerBlock.getArgument(consumerIdx + numConsumerIndices), 213 replacement); 214 // 10. Clone operations from the consumer to the fused op. 215 for (auto &op : consumerBlock.getOperations()) 216 rewriter.clone(op, mapper); 217 218 // Sanity checks. 219 assert(fusedBlock->getNumArguments() == 220 fusedOp->getNumOperands() + numFusedOpIndices && 221 "Ill-formed LinalgOp region"); 222 } 223 224 static Optional<SmallVector<Value, 1>> 225 fuseElementwiseOpsImpl(LinalgOp producer, OpOperand &consumerOpOperand, 226 const ControlElementwiseOpsFusionFn &controlFn, 227 PatternRewriter &rewriter) { 228 LinalgOp consumer = cast<LinalgOp>(consumerOpOperand.getOwner()); 229 unsigned consumerIdx = consumerOpOperand.getOperandNumber(); 230 if (!areElementwiseOpsFusable(producer, consumer, consumerIdx) || 231 !controlFn(producer->getResult(0), consumerOpOperand)) 232 return llvm::None; 233 234 // TODO: allow fusing the producer of an output operand. 235 assert(consumerIdx < consumer.getNumInputs() && 236 "expected producer of input operand"); 237 238 // Compute the fused operands list and indexing maps. 239 SmallVector<Value> fusedOperands; 240 SmallVector<AffineMap> fusedIndexMaps; 241 fusedOperands.reserve(producer->getNumOperands() + 242 consumer->getNumOperands()); 243 fusedIndexMaps.reserve(producer->getNumOperands() + 244 consumer->getNumOperands()); 245 // In the following, numbering matches that of `generateFusedTensorOpRegion`. 246 // 3. Consumer input operands/maps up to consumerIdx (exclusive). 247 llvm::append_range(fusedOperands, 248 consumer.getInputs().take_front(consumerIdx)); 249 llvm::append_range( 250 fusedIndexMaps, 251 ArrayRef<AffineMap>{consumer.getInputIndexingMaps()}.take_front( 252 consumerIdx)); 253 // 4. Splice in producer's input operands/maps. 254 llvm::append_range(fusedOperands, producer.getInputs()); 255 assert(producer->getNumResults() == 1 && "expected single result producer"); 256 AffineMap producerResultIndexMap = producer.getOutputIndexingMap(0); 257 for (auto &inputOpOperand : producer.getInputOpOperands()) { 258 // Compute indexing maps for the producer args in the fused operation. 259 AffineMap map = getIndexingMapOfProducerOperandsInCoordinatesOfFusedOp( 260 inputOpOperand, producerResultIndexMap, 261 consumer.getInputIndexingMap(consumerIdx)); 262 fusedIndexMaps.push_back(map); 263 } 264 // 4.b. Producer output operand/map that is fused needs to be passed if it is 265 // an "initTensor" (i.e. its value is actually read). 266 assert(producer->getNumResults() == 1 && "expected single result producer"); 267 if (producer.isInitTensor(&producer.getOutputOpOperands()[0])) { 268 llvm::append_range(fusedOperands, producer.getOutputs().take_front()); 269 // Compute indexing maps for the producer args in the fused operation. 270 AffineMap map = getIndexingMapOfProducerOperandsInCoordinatesOfFusedOp( 271 producer.getOutputOpOperands().front(), producerResultIndexMap, 272 consumer.getOutputIndexingMap(0)); 273 fusedIndexMaps.push_back(map); 274 } 275 // 5. Remaining consumer's input operands/maps (drop past index 276 // `consumerIdx`). 277 llvm::append_range(fusedOperands, 278 consumer.getInputs().drop_front(consumerIdx + 1)); 279 llvm::append_range( 280 fusedIndexMaps, 281 ArrayRef<AffineMap>{consumer.getInputIndexingMaps()}.drop_front( 282 consumerIdx + 1)); 283 // 6. All of consumer's output operands (skip operands: added by the builder). 284 // llvm::append_range(fusedOperands, consumer.getOutputs()); 285 llvm::append_range(fusedIndexMaps, consumer.getOutputIndexingMaps()); 286 // 7. All of producer's output operands/maps except the one fused. 287 // TODO: allow fusion of multi-result producers. 288 assert(producer->getNumResults() == 1 && "expected single result producer"); 289 290 // Generate the fused op. 291 Operation *fusedOp; 292 if (isa<GenericOp>(producer.getOperation()) && 293 isa<GenericOp>(consumer.getOperation())) { 294 fusedOp = rewriter.create<GenericOp>( 295 consumer.getLoc(), consumer->getResultTypes(), 296 /*inputs=*/fusedOperands, 297 // TODO: handle outputs. 298 consumer.getOutputs(), rewriter.getAffineMapArrayAttr(fusedIndexMaps), 299 consumer.iterator_types(), 300 /*doc=*/nullptr, 301 /*library_call=*/nullptr, 302 /*sparse=*/nullptr); 303 } else { 304 fusedOp = rewriter.create<IndexedGenericOp>( 305 consumer.getLoc(), consumer->getResultTypes(), 306 /*inputs=*/fusedOperands, 307 // TODO: handle outputs. 308 consumer.getOutputs(), rewriter.getAffineMapArrayAttr(fusedIndexMaps), 309 consumer.iterator_types(), 310 /*doc=*/nullptr, 311 /*library_call=*/nullptr, 312 /*sparse=*/nullptr); 313 } 314 315 // Construct an AffineMap from consumer loops to producer loops. 316 // consumer loop -> tensor index 317 AffineMap consumerResultIndexMap = consumer.getInputIndexingMap(consumerIdx); 318 // tensor index -> producer loop 319 AffineMap invProducerResultIndexMap = 320 inversePermutation(producerResultIndexMap); 321 assert(invProducerResultIndexMap && 322 "expected producer result indexig map to be invertible"); 323 // consumer loop -> producer loop 324 AffineMap consumerToProducerLoopsMap = 325 invProducerResultIndexMap.compose(consumerResultIndexMap); 326 327 generateFusedElementwiseOpRegion(rewriter, fusedOp, producer, consumer, 328 consumerToProducerLoopsMap, consumerIdx, 329 consumer.getNumLoops()); 330 return SmallVector<Value, 1>(fusedOp->getResults()); 331 } 332 333 /// Linearize the expressions in `sourceMap` based on the `reassociationMaps` 334 /// provided, given the shape of the source tensor that corresponds to the 335 /// `sourceMap`. Note that this implicitly assumes that the tensors dimensions 336 /// are "row-major" ordered logically. 337 /// 338 /// For example: 339 /// 340 /// %0 = op ... : tensor<?x?x4x5xf32> 341 /// with output index_map `affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)>` 342 /// 343 /// and reshape: 344 /// %1 = linalg.tensor_reshape %0 [affine_map<(i, j, k, l) -> (i)>, 345 /// affine_map<(i, j, k, l) -> (j, k, l)>] : 346 /// tensor<?x?x4x5xf32> into tensor<?x?xf32> 347 /// 348 /// would be rewritten into: 349 /// %0 = op ... : tensor<?x?x4x5xf32> 350 /// with output index_map 351 /// `affine_map<(d0, d1, d2, d3) -> (d0, d1 * 20 + d2 * 5 + d3)>` 352 static AffineMap linearizeCollapsedDims(AffineMap sourceMap, 353 ArrayRef<int64_t> sourceShape, 354 ArrayRef<AffineMap> reassociationMaps) { 355 SmallVector<AffineExpr, 4> resultExprs; 356 resultExprs.reserve(reassociationMaps.size()); 357 ArrayRef<AffineExpr> sourceExprs = sourceMap.getResults(); 358 MLIRContext *context = sourceMap.getContext(); 359 360 // Compute the result exprs based on the reassociation maps. 361 for (AffineMap map : reassociationMaps) { 362 ArrayRef<AffineExpr> collapsedDims = map.getResults(); 363 // Assume that they are in-order and contiguous (already checked in 364 // verifier). 365 assert(!collapsedDims.empty()); 366 unsigned startDim = 367 collapsedDims.front().cast<AffineDimExpr>().getPosition(); 368 SmallVector<int64_t, 4> sizes; 369 SmallVector<AffineExpr, 4> dimExprs; 370 for (auto en : 371 llvm::zip(sourceShape.slice(startDim, collapsedDims.size()), 372 sourceExprs.slice(startDim, collapsedDims.size()))) { 373 if (std::get<0>(en) == 1) 374 continue; 375 sizes.push_back(std::get<0>(en)); 376 dimExprs.push_back(std::get<1>(en)); 377 } 378 AffineExpr linearizedExpr = 379 makeCanonicalStridedLayoutExpr(sizes, dimExprs, context); 380 resultExprs.push_back(linearizedExpr); 381 } 382 return AffineMap::get(sourceMap.getNumDims(), sourceMap.getNumSymbols(), 383 resultExprs, context); 384 } 385 386 /// Checks if the `reshapeOp` can be fused with it consumer (if `asProducer` is 387 /// true) or its producer (if `asProducer` is false) given the indexing map at 388 /// its use. 389 static bool isTensorReshapeOpFoldableByLinearization(TensorReshapeOp reshapeOp, 390 AffineMap useIndexMap, 391 bool asProducer) { 392 RankedTensorType returnType = reshapeOp.getResultType(); 393 RankedTensorType operandType = reshapeOp.getSrcType(); 394 // Reshape is fusable with its consumer (i.e. reshape as a producer) when its 395 // operand is of lesser rank than the result. Fusing when operand has higher 396 // rank will require use of mods and divs in the indexing maps of the fused op 397 // which would make it non-invertible. Similarly reshape is fused with its 398 // producer (i.e. reshape as consumer) only if the return type has lesser 399 // rank. 400 if ((asProducer && reshapeOp.getSrcType().hasStaticShape() && 401 returnType.getRank() < operandType.getRank()) || 402 (!asProducer && reshapeOp.getResultType().hasStaticShape() && 403 operandType.getRank() < returnType.getRank())) 404 return false; 405 return useIndexMap.isPermutation(); 406 } 407 408 /// Based on the type of `op` create a linalg op of the same type, i.e. if `op` 409 /// is a linalg.generic operation, the create a `linalg.generic` operation with 410 /// the given `args`. Expects `op` to be `linalg.generic` or 411 /// `linalg.indexed_generic`. 412 template <typename... Args> 413 static LinalgOp createLinalgOpOfSameType(LinalgOp op, PatternRewriter &rewriter, 414 Args... args) { 415 if (isa<GenericOp>(op.getOperation())) 416 return rewriter.create<GenericOp>(args...); 417 if (isa<IndexedGenericOp>(op.getOperation())) 418 return rewriter.create<IndexedGenericOp>(args...); 419 llvm_unreachable( 420 "expected only linalg.generic or linalg.indexed_generic ops"); 421 return nullptr; 422 } 423 424 /// Check if the reshape operation is only expansion into/collapsing of 425 /// unit-dimension. 426 static bool isUnitDimExpansionOnly(ArrayRef<int64_t> expandedShape, 427 ArrayRef<AffineMap> reassociation) { 428 for (auto &map : reassociation) { 429 unsigned numUnitDims = 0; 430 for (AffineExpr expr : map.getResults()) { 431 unsigned position = expr.cast<AffineDimExpr>().getPosition(); 432 if (expandedShape[position] == 1) 433 numUnitDims++; 434 } 435 if (numUnitDims != map.getNumResults() - 1) 436 return false; 437 } 438 return true; 439 } 440 441 /// Conditions for folding a generic/indexed-generic operation with a reshape op 442 /// by expanding the iteration space dimensionality for tensor operations. These 443 /// are preconditions assumed by `foldReshapeByDimExpansion` which implements 444 /// the following fusion pattern. 445 /// 446 /// Consider 447 /// 448 /// %c = linalg.generic ins(%a, %b : memref<?x?x?xf32>, memref<?x?xf32>) 449 /// indexing_maps = [affine_map<(d0, d1, d2) -> (d1, d0, d2)>, 450 /// affine_map<(d0, d1, d2) -> (d1, d2)>, 451 /// affine_map<(d0, d1, d2) -> (d0, d2, d1)>] 452 /// %d = linalg.tensor_reshape %c 453 /// [affine_map<(d0, d1, d2, d3, d4, d5) -> (d0, d1)>, 454 /// affine_map<(d0, d1, d2, d3, d4, d5) -> (d2)>, 455 /// affine_map<(d0, d1, d2, d3, d4, d5) -> (d3, d4, d5)>] 456 /// : tensor<?x?x?xf32> into tensor<?x?x?x?x?x?xf32> 457 /// 458 /// The reshape can be folded into the `linalgOp` if the 459 /// generic/indexed-generic op loop dimensionality is increased to match the 460 /// result (operand) of the tensor_reshape when the reshape is expanding 461 /// (folding). The indexing_map of the fused tensor in the `linalgOp` and the 462 /// reassociation map helps compute the indexing maps of the modified op. For 463 /// the above example, based on the reassociation map it can be concluded that 464 /// 465 /// - The loop used to access the first dimension of the fused tensor is split 466 /// into two. 467 /// - The loop used to access the second dimension of the fused tensor is kept 468 /// as is. 469 /// - The loop used to access the third dimension of the fused tensor is split 470 /// into three. 471 /// 472 /// i.e. (e0, e1, e2, e3, e4) is the domain of the indexing map of the modified 473 /// op, then 474 /// 475 /// d0 -> e0, e1 476 /// d1 -> e2, e3, e4 477 /// d2 -> e5 478 /// 479 /// substituting this, the generic op can be rewritten as 480 /// 481 /// %d = linalg.generic ins(%0, %1 : ) 482 /// indexing_maps = 483 /// [affine_map<(e0, e1, e2, e3, e4, e5) -> (e2, e3, e4, e0, e1, e5)>, 484 /// affine_map<(e0, e1, e2, e3, e4, e5) -> (e2, e3, e4, e5)>, 485 /// affine_map<(e0, e1, e2, e3, e4, e5) -> (e0, e1, e5, e2, e3, e4)>] 486 /// 487 /// Since operands to the linalg generic are now 5D, reshapes can be introduced 488 /// to make it consistent 489 /// 490 /// %0 = linalg.tensor_reshape %a 491 /// [affine_map<(e0, e1, e2, e3, e4, e5) -> (e0, e1, e2), 492 /// affine_map<(e0, e1, e2, e3, e4, e5) -> (e3, e4), 493 /// affine_map<(e0, e1, e2, e3, e4, e5) -> (e5)] 494 /// : tensor<?x?x?xf32> into tensor<?x?x?x?x?x?xf32> 495 /// %1 = linalg.tensor_reshape %b 496 /// [affine_map<(e0, e1, e2, e3) -> (e0, e1, e2), 497 /// affine_map<(e0, e1, e2, e3) -> (e3)] 498 /// : tensor<?x?x?xf32> into tensor<?x?x?x?xf32> 499 /// 500 /// The added reshapes are again expanding patterns, so they will get fused 501 /// with its producers if possible. 502 static bool isFusableWithReshapeByDimExpansion(LinalgOp linalgOp, 503 unsigned fusedTensorIndex) { 504 // Is fusable only if: 505 // - The linalgOp is a generic op, or an indexed_generic. 506 // - All the indexing maps for operands and results in linalgOp are projected 507 // permutations. 508 // - The fused tensor is not a scalar. 509 // - All the loops in linalgOp are parallel loops. 510 return isa<GenericOp, IndexedGenericOp>(linalgOp.getOperation()) && 511 linalgOp.hasTensorSemantics() && 512 llvm::all_of(linalgOp.indexing_maps().getValue(), 513 [](Attribute attr) { 514 return attr.cast<AffineMapAttr>() 515 .getValue() 516 .isProjectedPermutation(); 517 }) && 518 linalgOp.getIndexingMap(fusedTensorIndex).getNumResults() > 0 && 519 llvm::all_of(linalgOp.iterator_types(), [](Attribute attr) { 520 return attr.cast<StringAttr>().getValue() == 521 getParallelIteratorTypeName(); 522 }); 523 } 524 525 namespace { 526 /// Information needed to expand a generic/indexed_generic operation to fold the 527 /// reshape with it. 528 class ExpansionInfo { 529 public: 530 // Computes the mapping from original dimensions of the op to the dimensions 531 // of the expanded op given the `indexingMap` of the fused operand/result of 532 // the generic/indexed_generic op, the `reassocationMaps` of the reshape op 533 // and the shape of the expanded op. 534 LogicalResult compute(LinalgOp linalgOp, unsigned fusedTensorIndex, 535 ArrayRef<AffineMap> reassociationMaps, 536 ArrayRef<int64_t> expandedShape); 537 unsigned getOrigOpNumDims() const { return reassociation.size(); } 538 unsigned getExpandedOpNumDims() const { return expandedOpNumDims; } 539 ReassociationIndicesRef getExpandedDims(unsigned i) const { 540 return reassociation[i]; 541 } 542 ArrayRef<int64_t> getExpandedShapeOfDim(unsigned i) const { 543 return expandedShapeMap[i]; 544 } 545 546 private: 547 /// Reassociation from the dimensions in the original operation to the 548 /// dimension of the expanded operation. 549 SmallVector<ReassociationIndices, 4> reassociation; 550 /// Mapping from extent of loops in the original operation, to the extent of 551 /// loops in the expanded operation. 552 SmallVector<SmallVector<int64_t, 4>, 4> expandedShapeMap; 553 unsigned expandedOpNumDims; 554 }; 555 } // namespace 556 557 LogicalResult ExpansionInfo::compute(LinalgOp linalgOp, 558 unsigned fusedTensorIndex, 559 ArrayRef<AffineMap> reassociationMaps, 560 ArrayRef<int64_t> expandedShape) { 561 if (reassociationMaps.empty()) 562 return failure(); 563 AffineMap fusedIndexMap = linalgOp.getIndexingMap(fusedTensorIndex); 564 565 Optional<SmallVector<int64_t, 4>> originalLoopRange = 566 linalgOp.getStaticLoopRanges(); 567 if (!originalLoopRange) 568 return linalgOp.emitError("unable to find loop range for operation"); 569 570 reassociation.clear(); 571 expandedShapeMap.clear(); 572 // Compute the number of dimension in the expanded op that correspond to each 573 // dimension of the original op. 574 SmallVector<unsigned, 4> numExpandedDims(fusedIndexMap.getNumDims(), 1); 575 expandedShapeMap.resize(fusedIndexMap.getNumDims()); 576 for (auto resultExpr : llvm::enumerate(fusedIndexMap.getResults())) { 577 unsigned pos = resultExpr.value().cast<AffineDimExpr>().getPosition(); 578 AffineMap foldedDims = reassociationMaps[resultExpr.index()]; 579 numExpandedDims[pos] = foldedDims.getNumResults(); 580 ArrayRef<int64_t> shape = 581 expandedShape.slice(foldedDims.getDimPosition(0), numExpandedDims[pos]); 582 expandedShapeMap[pos].assign(shape.begin(), shape.end()); 583 } 584 // The remaining dimensions remain the same. 585 for (unsigned i : llvm::seq<unsigned>(0, fusedIndexMap.getNumDims())) 586 if (expandedShapeMap[i].empty()) 587 expandedShapeMap[i] = {(*originalLoopRange)[i]}; 588 589 // Compute reassociation map from the original op to the expanded op. 590 unsigned sum = 0; 591 reassociation.reserve(fusedIndexMap.getNumDims()); 592 for (auto numFoldedDim : llvm::enumerate(numExpandedDims)) { 593 auto seq = llvm::seq<int64_t>(sum, sum + numFoldedDim.value()); 594 reassociation.emplace_back(seq.begin(), seq.end()); 595 sum += numFoldedDim.value(); 596 } 597 expandedOpNumDims = sum; 598 return success(); 599 } 600 601 /// To expand an indexed_generic operation, the body of the indexed generic op 602 /// need to be modified appropriately. Specifically, uses of arguments for 603 /// induction variables in the original operation need to be replaced with 604 /// linearization of the corresponding arguments in the expanded op. That 605 /// requires the shape of the expanded dimensions (at least all but the most 606 /// significant. For now check that these are all statically sized. Note that 607 /// this could be extended to handle dynamic case, but the implementation below 608 /// uses `affine.apply` which seems to have issues when the shapes are not 609 /// static. 610 LogicalResult isIndexedGenericOpExpandable(LinalgOp linalgOp, 611 const ExpansionInfo &expansionInfo) { 612 for (unsigned i : llvm::seq<unsigned>(0, expansionInfo.getOrigOpNumDims())) { 613 ArrayRef<int64_t> expandedShape = expansionInfo.getExpandedShapeOfDim(i); 614 if (expandedShape.size() == 1) 615 continue; 616 for (int64_t shape : expandedShape.drop_front()) { 617 if (ShapedType::isDynamic(shape)) { 618 return linalgOp.emitError( 619 "unable to fuse indexed generic op where the expanded dim is " 620 "dynamic"); 621 } 622 } 623 } 624 return success(); 625 } 626 627 /// Return the indexing map to use in the expanded op for a given the 628 /// `indexingMap` of the original operation. 629 static AffineMap 630 getIndexingMapInExpandedOp(OpBuilder &builder, AffineMap indexingMap, 631 const ExpansionInfo &expansionInfo) { 632 SmallVector<AffineExpr, 4> newExprs; 633 for (AffineExpr expr : indexingMap.getResults()) { 634 unsigned pos = expr.cast<AffineDimExpr>().getPosition(); 635 SmallVector<AffineExpr, 4> expandedExprs = llvm::to_vector<4>( 636 llvm::map_range(expansionInfo.getExpandedDims(pos), [&](int64_t v) { 637 return builder.getAffineDimExpr(static_cast<unsigned>(v)); 638 })); 639 newExprs.append(expandedExprs.begin(), expandedExprs.end()); 640 } 641 return AffineMap::get(expansionInfo.getExpandedOpNumDims(), 642 indexingMap.getNumSymbols(), newExprs, 643 builder.getContext()); 644 } 645 646 /// Return the type of the operand/result to use in the expanded op given the 647 /// type in the original op. 648 static RankedTensorType getExpandedType(RankedTensorType originalType, 649 AffineMap indexingMap, 650 const ExpansionInfo &expansionInfo) { 651 SmallVector<int64_t, 4> expandedShape; 652 for (AffineExpr expr : indexingMap.getResults()) { 653 unsigned dim = expr.cast<AffineDimExpr>().getPosition(); 654 auto dimExpansion = expansionInfo.getExpandedShapeOfDim(dim); 655 expandedShape.append(dimExpansion.begin(), dimExpansion.end()); 656 } 657 return RankedTensorType::get(expandedShape, originalType.getElementType()); 658 } 659 660 /// Returns the reassociation maps to use in the `linalg.tensor_reshape` 661 /// operation to convert the operands of the origial operation to operands of 662 /// the expanded operation. The same method is used to compute the 663 /// `linalg.tensor_reshape` used to collapse the result of the expanded op to 664 /// get the value that can replace all uses of the results of the original op. 665 static SmallVector<ReassociationIndices, 4> 666 getReassociationForExpansion(AffineMap indexingMap, 667 const ExpansionInfo &expansionInfo) { 668 SmallVector<ReassociationIndices, 4> reassociation; 669 unsigned numReshapeDims = 0; 670 for (AffineExpr expr : indexingMap.getResults()) { 671 unsigned dim = expr.cast<AffineDimExpr>().getPosition(); 672 auto numExpandedDims = expansionInfo.getExpandedDims(dim).size(); 673 auto indices = llvm::to_vector<2>( 674 llvm::seq<int64_t>(numReshapeDims, numReshapeDims + numExpandedDims)); 675 reassociation.emplace_back(std::move(indices)); 676 numReshapeDims += numExpandedDims; 677 } 678 return reassociation; 679 } 680 681 /// Build the body of the expanded IndexedGenericOp. The arguments for the 682 /// induction variables of the original operation need to be recovered by 683 /// linearizing the arguments of the corresponding dimensions of the expanded 684 /// op. For now it is assumed that the shapes of the expanded op needed for 685 /// linearization are static. 686 static void buildExpandedIndexedGenericOpRegion( 687 PatternRewriter &rewriter, Location loc, Region &originalOpRegion, 688 Region &fusedOpRegion, const ExpansionInfo &expansionInfo) { 689 assert(fusedOpRegion.empty() && "expected fused op to have empty region"); 690 // Create an entry block in the fused region with same number of arguments 691 // as the fused op 692 Block *fusedEntryBlock = new Block; 693 fusedOpRegion.push_back(fusedEntryBlock); 694 rewriter.cloneRegionBefore(originalOpRegion, fusedOpRegion, 695 fusedOpRegion.end()); 696 697 // Merge the entry block of the fused op with the cloned blocks. For this 698 // compute the value for arguments of the region in the original operation 699 // in terms of the arguments of the fused op. Since the original operation 700 // is expanded, the expanded dimensions need to be folded back to get the 701 // replacement value for the arguments corresponding to interation index. 702 // For now this expects that all the loop ranges are constants, which is 703 // true if the shapes are all static. This has already been checked in the 704 // precondition. 705 using namespace edsc::op; 706 using namespace edsc::intrinsics; 707 OpBuilder::InsertionGuard guard(rewriter); 708 SmallVector<Value, 4> argReplacements(originalOpRegion.getNumArguments()); 709 rewriter.setInsertionPointToStart(fusedEntryBlock); 710 edsc::ScopedContext scopedContext(rewriter, loc); 711 IndexType indexType = rewriter.getIndexType(); 712 for (auto i : llvm::seq<unsigned>(0, expansionInfo.getOrigOpNumDims())) { 713 Value linearizedIndex = fusedEntryBlock->addArgument(indexType); 714 ArrayRef<int64_t> expandedDimsShape = 715 expansionInfo.getExpandedShapeOfDim(i).drop_front(); 716 for (unsigned shape : expandedDimsShape) { 717 assert(!ShapedType::isDynamic(shape)); 718 linearizedIndex = linearizedIndex * std_constant_index(shape); 719 linearizedIndex = 720 linearizedIndex + fusedEntryBlock->addArgument(indexType); 721 } 722 argReplacements[i] = linearizedIndex; 723 } 724 for (auto i : llvm::seq<unsigned>(expansionInfo.getOrigOpNumDims(), 725 argReplacements.size())) { 726 argReplacements[i] = 727 fusedEntryBlock->addArgument(originalOpRegion.getArgument(i).getType()); 728 } 729 rewriter.mergeBlocks(fusedEntryBlock->getNextNode(), fusedEntryBlock, 730 argReplacements); 731 } 732 733 /// Implements the fusion of a tensor_reshape op and a generic/indexed_generic 734 /// op as explained in `isFusableWithReshapeByExpansion`. Assumes that those 735 /// conditions have been satisfied. 736 static Optional<SmallVector<Value, 1>> 737 fuseWithReshapeByExpansion(LinalgOp linalgOp, TensorReshapeOp reshapeOp, 738 unsigned fusedTensorIndex, 739 PatternRewriter &rewriter) { 740 assert(isFusableWithReshapeByDimExpansion(linalgOp, fusedTensorIndex) && 741 "preconditions for fuse operation failed"); 742 // Check if reshape is expanding or collapsing. 743 bool isExpanding = 744 reshapeOp.getSrcType().getRank() < reshapeOp.getResultType().getRank(); 745 RankedTensorType expandedType = 746 isExpanding ? reshapeOp.getResultType() : reshapeOp.getSrcType(); 747 748 ExpansionInfo expansionInfo; 749 if (failed(expansionInfo.compute(linalgOp, fusedTensorIndex, 750 reshapeOp.getReassociationMaps(), 751 expandedType.getShape()))) 752 return llvm::None; 753 754 if (isa<IndexedGenericOp>(linalgOp.getOperation()) && 755 failed(isIndexedGenericOpExpandable(linalgOp, expansionInfo))) 756 return llvm::None; 757 758 SmallVector<AffineMap, 4> expandedOpIndexingMaps = llvm::to_vector<4>( 759 llvm::map_range(linalgOp.getIndexingMaps(), [&](AffineMap m) { 760 return getIndexingMapInExpandedOp(rewriter, m, expansionInfo); 761 })); 762 763 SmallVector<Value, 4> expandedOpOperands; 764 for (auto operand : llvm::enumerate(linalgOp.getInputs())) { 765 if (operand.index() == fusedTensorIndex) { 766 expandedOpOperands.push_back(reshapeOp.src()); 767 continue; 768 } 769 AffineMap indexingMap = linalgOp.getInputIndexingMap(operand.index()); 770 RankedTensorType expandedOperandType = 771 getExpandedType(operand.value().getType().cast<RankedTensorType>(), 772 indexingMap, expansionInfo); 773 if (expandedOperandType != operand.value().getType()) { 774 // Reshape the operand to get the right type. 775 SmallVector<ReassociationIndices, 4> reassociation = 776 getReassociationForExpansion(indexingMap, expansionInfo); 777 expandedOpOperands.push_back(rewriter.create<TensorReshapeOp>( 778 linalgOp.getLoc(), expandedOperandType, operand.value(), 779 reassociation)); 780 continue; 781 } 782 expandedOpOperands.push_back(operand.value()); 783 } 784 785 Location loc = linalgOp.getLoc(); 786 SmallVector<Value, 1> outputs; 787 for (auto result : llvm::enumerate(linalgOp.getOutputs())) { 788 AffineMap indexingMap = linalgOp.getOutputIndexingMap(result.index()); 789 RankedTensorType expandedOutputType = 790 getExpandedType(result.value().getType().cast<RankedTensorType>(), 791 indexingMap, expansionInfo); 792 if (expandedOutputType != result.value().getType()) { 793 SmallVector<ReassociationIndices, 4> reassociation = 794 getReassociationForExpansion(indexingMap, expansionInfo); 795 outputs.push_back(rewriter.create<TensorReshapeOp>( 796 linalgOp.getLoc(), expandedOutputType, result.value(), 797 reassociation)); 798 } 799 } 800 801 // The iterator types of the expanded op are all parallel. 802 SmallVector<StringRef, 4> iteratorTypes(expansionInfo.getExpandedOpNumDims(), 803 getParallelIteratorTypeName()); 804 805 TypeRange resultTypes = ValueRange(outputs).getTypes(); 806 LinalgOp fusedOp = createLinalgOpOfSameType( 807 linalgOp, rewriter, linalgOp.getLoc(), resultTypes, 808 /*inputs=*/expandedOpOperands, outputs, expandedOpIndexingMaps, 809 iteratorTypes); 810 Region &fusedRegion = fusedOp->getRegion(0); 811 Region &originalRegion = linalgOp->getRegion(0); 812 813 if (isa<GenericOp>(linalgOp.getOperation())) { 814 rewriter.cloneRegionBefore(originalRegion, fusedRegion, 815 fusedRegion.begin()); 816 } else { 817 assert(isa<IndexedGenericOp>(linalgOp.getOperation())); 818 buildExpandedIndexedGenericOpRegion(rewriter, loc, originalRegion, 819 fusedRegion, expansionInfo); 820 } 821 822 // Reshape the result values to their original shape if this is a collapsing 823 // reshape folded into its consumer. 824 SmallVector<Value, 1> resultVals; 825 for (auto result : llvm::enumerate(linalgOp->getResults())) { 826 if (!isExpanding && 827 resultTypes[result.index()] != result.value().getType()) { 828 SmallVector<ReassociationIndices, 4> reassociation = 829 getReassociationForExpansion( 830 linalgOp.getOutputIndexingMap(result.index()), expansionInfo); 831 resultVals.push_back(rewriter.create<TensorReshapeOp>( 832 linalgOp.getLoc(), result.value().getType(), 833 fusedOp->getResult(result.index()), reassociation)); 834 } else { 835 resultVals.push_back(fusedOp->getResult(result.index())); 836 } 837 } 838 // Assuming a single result. 839 return resultVals; 840 } 841 842 namespace { 843 844 /// Pattern to fold tensor_reshape op with its consumer by using the source of 845 /// the reshape op as the operand in the consumer (instead of the result of the 846 /// tensor_reshapeop) when the tensor_reshape op is collapsing. The 847 /// corresponding index map in the consumer needs to be modified to linearize 848 /// the folded dimension. 849 /// 850 /// For example, 851 /// 852 /// #map0 = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)> 853 /// %0 = linalg.tensor_reshape %arg0 854 /// [affine_map<(i, j, k, l) -> (i)>, affine_map<(i, j, k, l) -> (j, k)>, 855 /// affine_map<(i, j, k, l) -> (l)>] 856 /// tensor<?x?x?xf32> into tensor<?x?x4x?xf32> 857 /// %1 = linalg.generic { indexing_maps = [#map0, #map0, #map0], ... } 858 /// ins(%0, %arg1 : tensor<?x?x4x?xf32>, tensor<?x?x4x?xf32>) ... 859 /// -> tensor<?x?x4x?xf32> 860 /// 861 /// can be folded into 862 /// 863 /// #map0 = affine_map<(d0, d1, d2, d3) -> (d0, d1 * 4 + d2, d3)> 864 /// #map1 = affine_map<(d0, d1, d2, d3) -> (d0, d1, d2, d3)> 865 /// %0 = linalg.generic { indexing_maps = [#map0, #map1, #map1] ... } 866 /// ins(%arg0, %arg1 : tensor<?x?x?xf32>, tensor<?x?x4x?xf32>) ... 867 /// -> tensor<?x?x4x?xf32> 868 template <typename LinalgOpTy, bool foldUnitDimReshapesOnly> 869 struct FoldProducerReshapeOpByLinearization 870 : public OpRewritePattern<LinalgOpTy> { 871 using OpRewritePattern<LinalgOpTy>::OpRewritePattern; 872 873 LogicalResult matchAndRewrite(LinalgOpTy op, 874 PatternRewriter &rewriter) const override { 875 if (!op.hasTensorSemantics()) 876 return failure(); 877 LinalgOp linalgOp = cast<LinalgOp>(op.getOperation()); 878 for (auto operand : llvm::enumerate(linalgOp.getInputs())) { 879 TensorReshapeOp reshapeOp = 880 operand.value().getDefiningOp<TensorReshapeOp>(); 881 if (!reshapeOp || 882 !isTensorReshapeOpFoldableByLinearization( 883 reshapeOp, linalgOp.getInputIndexingMap(operand.index()), 884 /*asProducer =*/true) || 885 (foldUnitDimReshapesOnly && 886 !isUnitDimExpansionOnly(reshapeOp.getResultType().getShape(), 887 reshapeOp.getReassociationMaps()))) 888 continue; 889 890 // Compute the fused operands list, 891 SmallVector<Value, 2> fusedOperands(linalgOp.getInputs()); 892 fusedOperands[operand.index()] = reshapeOp.src(); 893 fusedOperands.append(linalgOp.getOutputs().begin(), 894 linalgOp.getOutputs().end()); 895 896 // Compute indexing_maps for the fused operation. The indexing_maps for 897 // the operands of the consumers that arent fused are the same. 898 SmallVector<AffineMap, 4> fusedIndexMaps = llvm::to_vector<4>( 899 op.indexing_maps().template getAsValueRange<AffineMapAttr>()); 900 901 // Accepted consumer maps are either identity or permutation. 902 auto invMap = inversePermutation(fusedIndexMaps[operand.index()]); 903 904 // Compute the indexing map to use for the result of the producer. 905 AffineMap modifiedMap = 906 linearizeCollapsedDims(invMap, reshapeOp.getResultType().getShape(), 907 reshapeOp.getReassociationMaps()); 908 for (AffineExpr expr : modifiedMap.getResults()) { 909 if (!expr.isPureAffine()) 910 return failure(); 911 } 912 fusedIndexMaps[operand.index()] = modifiedMap; 913 914 // Further check that the resulting index maps can be fused and 915 // inverted. Without this the resultant op is not legal. 916 if (!inversePermutation(concatAffineMaps(fusedIndexMaps))) { 917 return rewriter.notifyMatchFailure( 918 op, "fused op loop bound computation failed"); 919 } 920 921 rewriter.startRootUpdate(op); 922 op->setOperands(fusedOperands); 923 op.indexing_mapsAttr(rewriter.getAffineMapArrayAttr(fusedIndexMaps)); 924 rewriter.finalizeRootUpdate(op); 925 return success(); 926 } 927 return failure(); 928 } 929 }; 930 931 /// Pattern to fuse a tensor_reshape op with its consumer 932 /// generic/indexed_generic op, when the reshape op is collapsing 933 /// dimensions. The dimensionality of the loop in the consumer is expanded. 934 template <typename GenericOpTy> 935 class FoldWithProducerReshapeOpByExpansion 936 : public OpRewritePattern<GenericOpTy> { 937 public: 938 FoldWithProducerReshapeOpByExpansion(MLIRContext *context, 939 bool foldUnitDimReshapes, 940 PatternBenefit benefit = 1) 941 : OpRewritePattern<GenericOpTy>(context, benefit), 942 allowFoldingUnitDimReshapes(foldUnitDimReshapes) {} 943 944 LogicalResult matchAndRewrite(GenericOpTy genericOp, 945 PatternRewriter &rewriter) const override { 946 LinalgOp linalgOp = cast<LinalgOp>(genericOp.getOperation()); 947 for (auto operand : llvm::enumerate(linalgOp.getInputs())) { 948 TensorReshapeOp reshapeOp = 949 operand.value().getDefiningOp<TensorReshapeOp>(); 950 if (!reshapeOp) 951 continue; 952 953 // Fold only if 954 // - The tensor reshape op is folding. 955 // - All constraints of fusing with reshape by expansion are met. 956 if (reshapeOp.getSrcType().getRank() < 957 reshapeOp.getResultType().getRank() || 958 !isFusableWithReshapeByDimExpansion(linalgOp, operand.index()) || 959 (!allowFoldingUnitDimReshapes && 960 isUnitDimExpansionOnly(reshapeOp.getSrcType().getShape(), 961 reshapeOp.getReassociationMaps()))) 962 continue; 963 964 Optional<SmallVector<Value, 1>> replacementValues = 965 fuseWithReshapeByExpansion(linalgOp, reshapeOp, operand.index(), 966 rewriter); 967 if (!replacementValues) 968 return failure(); 969 rewriter.replaceOp(genericOp, replacementValues.getValue()); 970 return success(); 971 } 972 return failure(); 973 } 974 975 private: 976 bool allowFoldingUnitDimReshapes; 977 }; 978 979 /// Pattern to fold tensor_reshape op with its producer. The corresponding index 980 /// map in the consumer needs to be modified to linearize the folded dimension. 981 template <bool foldUnitDimReshapesOnly> 982 struct FoldConsumerReshapeOpByLinearization 983 : public OpRewritePattern<TensorReshapeOp> { 984 using OpRewritePattern<TensorReshapeOp>::OpRewritePattern; 985 986 LogicalResult matchAndRewrite(TensorReshapeOp reshapeOp, 987 PatternRewriter &rewriter) const override { 988 LinalgOp producer = reshapeOp.src().getDefiningOp<LinalgOp>(); 989 if (!producer || 990 !isa<GenericOp, IndexedGenericOp>(producer.getOperation()) || 991 !producer.hasTensorSemantics() || producer.getNumOutputs() != 1 || 992 !isTensorReshapeOpFoldableByLinearization( 993 reshapeOp, producer.getOutputIndexingMap(0), 994 /*asProducer =*/false) || 995 (foldUnitDimReshapesOnly && 996 !isUnitDimExpansionOnly(reshapeOp.getSrcType().getShape(), 997 reshapeOp.getReassociationMaps()))) 998 return failure(); 999 // The indexing_maps for the operands of the fused operation are same as 1000 // those for the operands of the producer. 1001 SmallVector<AffineMap, 4> fusedIndexMaps = llvm::to_vector<4>( 1002 producer.indexing_maps().getAsValueRange<AffineMapAttr>()); 1003 1004 auto invMap = inversePermutation(producer.getOutputIndexingMap(0)); 1005 1006 // Compute the indexing map to use for the operand of the producer. 1007 AffineMap modifiedMap = 1008 linearizeCollapsedDims(invMap, reshapeOp.getSrcType().getShape(), 1009 reshapeOp.getReassociationMaps()); 1010 for (AffineExpr expr : modifiedMap.getResults()) { 1011 if (!expr.isPureAffine()) { 1012 return rewriter.notifyMatchFailure( 1013 producer, "fused op indexing map is not affine"); 1014 } 1015 } 1016 fusedIndexMaps.back() = modifiedMap; 1017 1018 // Further check that the resulting index maps can be fused and 1019 // inverted. Without this the resultant op is not legal. 1020 if (!inversePermutation(concatAffineMaps(fusedIndexMaps))) { 1021 return rewriter.notifyMatchFailure( 1022 producer, "fused op loop bound computation failed"); 1023 } 1024 1025 Location loc = producer.getLoc(); 1026 Value output = rewriter.create<TensorReshapeOp>( 1027 loc, producer.getOutputs()[0], reshapeOp.getReassociationExprs()); 1028 LinalgOp fusedOp = createLinalgOpOfSameType( 1029 producer, rewriter, loc, reshapeOp.getResultType(), 1030 /*inputs=*/producer.getInputs(), 1031 // TODO: handle outputs. 1032 /*outputs=*/output, rewriter.getAffineMapArrayAttr(fusedIndexMaps), 1033 producer.iterator_types(), 1034 /*doc=*/nullptr, 1035 /*library_call=*/nullptr, 1036 /*sparse=*/nullptr); 1037 auto &fusedRegion = fusedOp->getRegion(0); 1038 rewriter.cloneRegionBefore(producer->getRegion(0), fusedRegion, 1039 fusedRegion.begin()); 1040 rewriter.replaceOp(reshapeOp, fusedOp->getResults()); 1041 return success(); 1042 } 1043 }; 1044 1045 /// Pattern to fold a tensor_reshape op with its producer generic op if the 1046 /// tensor_reshape op is expanding, by expanding the dimensionality of the loop 1047 /// in the producer op. 1048 struct FoldReshapeWithGenericOpByExpansion 1049 : public OpRewritePattern<TensorReshapeOp> { 1050 using OpRewritePattern<TensorReshapeOp>::OpRewritePattern; 1051 LogicalResult matchAndRewrite(TensorReshapeOp reshapeOp, 1052 PatternRewriter &rewriter) const override { 1053 // Fold only if 1054 // - The tensor reshape op is a expanding case. 1055 // - All constraints of fusing with reshape by expansion are met. 1056 if (reshapeOp.getSrcType().getRank() > reshapeOp.getResultType().getRank()) 1057 return failure(); 1058 LinalgOp producer = reshapeOp.src().getDefiningOp<LinalgOp>(); 1059 if (!producer || producer.getNumOutputs() != 1 || 1060 !isFusableWithReshapeByDimExpansion(producer, 1061 producer.getNumInputs()) || 1062 isUnitDimExpansionOnly(reshapeOp.getResultType().getShape(), 1063 reshapeOp.getReassociationMaps())) 1064 return failure(); 1065 Optional<SmallVector<Value, 1>> replacementValues = 1066 fuseWithReshapeByExpansion(producer, reshapeOp, producer.getNumInputs(), 1067 rewriter); 1068 if (!replacementValues) 1069 return failure(); 1070 rewriter.replaceOp(reshapeOp, replacementValues.getValue()); 1071 return success(); 1072 } 1073 }; 1074 1075 /// Pattern to fold a GenericOp/IndexedGenericOp with a splat constant. 1076 template <typename LinalgOpTy> 1077 class FoldSplatConstants : public OpRewritePattern<LinalgOpTy> { 1078 public: 1079 FoldSplatConstants(MLIRContext *context, ControlElementwiseOpsFusionFn &fun, 1080 PatternBenefit benefit = 1) 1081 : OpRewritePattern<LinalgOpTy>(context, benefit), controlFn(fun) {} 1082 1083 LogicalResult matchAndRewrite(LinalgOpTy op, 1084 PatternRewriter &rewriter) const override { 1085 if (!op.hasTensorSemantics()) 1086 return failure(); 1087 LinalgOp linalgOp = cast<LinalgOp>(op.getOperation()); 1088 for (auto operand : llvm::enumerate(linalgOp.getInputOpOperands())) { 1089 ConstantOp constantOp = operand.value().get().getDefiningOp<ConstantOp>(); 1090 if (!constantOp || 1091 !constantOp.value().cast<DenseElementsAttr>().isSplat() || 1092 !controlFn(constantOp->getResult(0), operand.value())) 1093 continue; 1094 1095 // The indexing_maps for the operands of the fused operation are same as 1096 // those for the operands of the linalgOp without the indexing map at 1097 // operand.index() 1098 SmallVector<AffineMap, 4> fusedIndexMaps = llvm::to_vector<4>( 1099 linalgOp.indexing_maps().getAsValueRange<AffineMapAttr>()); 1100 fusedIndexMaps.erase(std::next(fusedIndexMaps.begin(), operand.index())); 1101 1102 // The operands list is same as the linalgOp with the argument for 1103 // constant index dropped. 1104 SmallVector<Value, 4> fusedOperands(linalgOp.getInputs()); 1105 fusedOperands.erase(std::next(fusedOperands.begin(), operand.index())); 1106 1107 // Create a constant scalar value from the splat constant. 1108 Value scalarConstant = rewriter.create<ConstantOp>( 1109 constantOp.getLoc(), 1110 constantOp.value().cast<DenseElementsAttr>().getSplatValue()); 1111 1112 LinalgOp fusedOp = createLinalgOpOfSameType( 1113 linalgOp, rewriter, rewriter.getUnknownLoc(), 1114 linalgOp->getResultTypes(), 1115 /*inputs=*/fusedOperands, 1116 /*outputs=*/linalgOp.getOutputs(), 1117 rewriter.getAffineMapArrayAttr(fusedIndexMaps), 1118 linalgOp.iterator_types(), 1119 /*doc=*/nullptr, 1120 /*library_call=*/nullptr, 1121 /*sparse=*/nullptr); 1122 1123 // Map the block argument corresponding to the replaced argument with the 1124 // scalar constant. 1125 Region &linalgOpRegion = linalgOp->getRegion(0); 1126 Block &entryBlock = *linalgOpRegion.begin(); 1127 unsigned argIndex = entryBlock.getNumArguments() - 1128 linalgOp.getNumShapedOperands() + operand.index(); 1129 BlockAndValueMapping mapping; 1130 mapping.map(entryBlock.getArgument(argIndex), scalarConstant); 1131 Region &fusedRegion = fusedOp->getRegion(0); 1132 rewriter.cloneRegionBefore(linalgOpRegion, fusedRegion, 1133 fusedRegion.begin(), mapping); 1134 rewriter.replaceOp(linalgOp, fusedOp->getResults()); 1135 return success(); 1136 } 1137 return failure(); 1138 } 1139 1140 private: 1141 ControlElementwiseOpsFusionFn controlFn; 1142 }; 1143 } // namespace 1144 1145 static Optional<SmallVector<Value, 1>> 1146 fuseElementwiseOps(PatternRewriter &rewriter, OpOperand &consumerOpOperand, 1147 const ControlElementwiseOpsFusionFn &controlFn) { 1148 Operation *producer = consumerOpOperand.get().getDefiningOp(); 1149 if (!producer || producer->getNumResults() != 1) 1150 return llvm::None; 1151 1152 // Fuse when consumer is GenericOp or IndexedGenericOp. 1153 if (!isa<GenericOp, IndexedGenericOp>(consumerOpOperand.getOwner()) || 1154 !isa<GenericOp, IndexedGenericOp>(producer)) 1155 return llvm::None; 1156 1157 return fuseElementwiseOpsImpl(cast<LinalgOp>(producer), consumerOpOperand, 1158 controlFn, rewriter); 1159 } 1160 1161 namespace { 1162 /// Patterns to fuse a generic op, with the producer of its operands. 1163 template <typename LinalgOpTy> 1164 class FuseElementwiseOps : public OpRewritePattern<LinalgOpTy> { 1165 public: 1166 FuseElementwiseOps(MLIRContext *context, ControlElementwiseOpsFusionFn &fun, 1167 PatternBenefit benefit = 1) 1168 : OpRewritePattern<LinalgOpTy>(context, benefit), controlFn(fun) {} 1169 1170 LogicalResult matchAndRewrite(LinalgOpTy op, 1171 PatternRewriter &rewriter) const override { 1172 // Find the first operand that is defined by another generic op on tensors. 1173 for (OpOperand &opOperand : op.getShapedOpOperands()) { 1174 LinalgOp producerOp = 1175 dyn_cast_or_null<LinalgOp>(opOperand.get().getDefiningOp()); 1176 if (!producerOp || !producerOp.hasTensorSemantics()) 1177 continue; 1178 Optional<SmallVector<Value, 1>> fusedOpResults = 1179 fuseElementwiseOps(rewriter, opOperand, controlFn); 1180 if (fusedOpResults) { 1181 rewriter.replaceOp(op, *fusedOpResults); 1182 return success(); 1183 } 1184 } 1185 return failure(); 1186 } 1187 1188 private: 1189 ControlElementwiseOpsFusionFn controlFn; 1190 }; 1191 1192 /// Pass that fuses generic ops on tensors. Used only for testing. 1193 struct FusionOfTensorOpsPass 1194 : public LinalgFusionOfTensorOpsBase<FusionOfTensorOpsPass> { 1195 void runOnOperation() override { 1196 Operation *op = getOperation(); 1197 RewritePatternSet patterns(op->getContext()); 1198 populateElementwiseOpsFusionPatterns( 1199 patterns, 1200 LinalgElementwiseFusionOptions().setAllowFoldingUnitDimReshapes( 1201 allowFoldingUnitDimReshapes)); 1202 (void)applyPatternsAndFoldGreedily(op->getRegions(), std::move(patterns)); 1203 } 1204 }; 1205 1206 /// Pass to test folding of reshape op with generic/indexed_generic ops by 1207 /// linearization. 1208 struct FoldReshapeOpsByLinearizationPass 1209 : public LinalgFoldReshapeOpsByLinearizationBase< 1210 FoldReshapeOpsByLinearizationPass> { 1211 void runOnOperation() override { 1212 Operation *op = getOperation(); 1213 RewritePatternSet patterns(op->getContext()); 1214 populateFoldReshapeOpsByLinearizationPatterns(patterns); 1215 (void)applyPatternsAndFoldGreedily(op->getRegions(), std::move(patterns)); 1216 } 1217 }; 1218 1219 } // namespace 1220 1221 void mlir::linalg::populateFoldReshapeOpsByLinearizationPatterns( 1222 RewritePatternSet &patterns) { 1223 patterns.add<FoldProducerReshapeOpByLinearization<GenericOp, false>, 1224 FoldProducerReshapeOpByLinearization<IndexedGenericOp, false>, 1225 FoldConsumerReshapeOpByLinearization<false>>( 1226 patterns.getContext()); 1227 } 1228 1229 void mlir::linalg::populateFoldUnitDimsReshapeOpsByLinearizationPatterns( 1230 RewritePatternSet &patterns) { 1231 patterns.add<FoldProducerReshapeOpByLinearization<GenericOp, true>, 1232 FoldProducerReshapeOpByLinearization<IndexedGenericOp, true>, 1233 FoldConsumerReshapeOpByLinearization<true>>( 1234 patterns.getContext()); 1235 } 1236 1237 void mlir::linalg::populateFoldReshapeOpsByExpansionPatterns( 1238 RewritePatternSet &patterns, bool allowFoldingUnitDimReshapes) { 1239 patterns.add<FoldReshapeWithGenericOpByExpansion>(patterns.getContext()); 1240 patterns.add<FoldWithProducerReshapeOpByExpansion<GenericOp>, 1241 FoldWithProducerReshapeOpByExpansion<IndexedGenericOp>>( 1242 patterns.getContext(), allowFoldingUnitDimReshapes); 1243 } 1244 1245 void mlir::linalg::populateElementwiseOpsFusionPatterns( 1246 RewritePatternSet &patterns, LinalgElementwiseFusionOptions options) { 1247 auto *context = patterns.getContext(); 1248 patterns 1249 .add<FuseElementwiseOps<GenericOp>, FuseElementwiseOps<IndexedGenericOp>, 1250 FoldSplatConstants<GenericOp>, FoldSplatConstants<IndexedGenericOp>>( 1251 context, options.controlElementwiseOpsFusionFn); 1252 populateFoldReshapeOpsByExpansionPatterns( 1253 patterns, options.allowFoldingUnitDimReshapes); 1254 GenericOp::getCanonicalizationPatterns(patterns, context); 1255 IndexedGenericOp::getCanonicalizationPatterns(patterns, context); 1256 TensorReshapeOp::getCanonicalizationPatterns(patterns, context); 1257 } 1258 1259 std::unique_ptr<Pass> mlir::createLinalgFusionOfTensorOpsPass() { 1260 return std::make_unique<FusionOfTensorOpsPass>(); 1261 } 1262 1263 std::unique_ptr<Pass> mlir::createFoldReshapeOpsByLinearizationPass() { 1264 return std::make_unique<FoldReshapeOpsByLinearizationPass>(); 1265 } 1266