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