1 //===- Hoisting.cpp - Linalg hoisting transformations ---------------------===//
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 functions concerned with hoisting invariant operations
10 // in the context of Linalg transformations.
11 //
12 //===----------------------------------------------------------------------===//
13 
14 #include "mlir/Dialect/Linalg/Transforms/Hoisting.h"
15 #include "mlir/Analysis/SliceAnalysis.h"
16 #include "mlir/Dialect/Linalg/IR/LinalgOps.h"
17 #include "mlir/Dialect/SCF/SCF.h"
18 #include "mlir/Dialect/SCF/Utils.h"
19 #include "mlir/Dialect/StandardOps/IR/Ops.h"
20 #include "mlir/Dialect/Vector/VectorOps.h"
21 #include "mlir/Dialect/Vector/VectorUtils.h"
22 #include "mlir/IR/BuiltinOps.h"
23 #include "mlir/IR/Dominance.h"
24 #include "mlir/Transforms/LoopUtils.h"
25 #include "llvm/ADT/StringRef.h"
26 #include "llvm/Support/Debug.h"
27 
28 #define DEBUG_TYPE "linalg-hoisting"
29 
30 #define DBGS() (dbgs() << '[' << DEBUG_TYPE << "] ")
31 
32 using namespace mlir;
33 using namespace mlir::linalg;
34 
35 using llvm::dbgs;
36 
37 void mlir::linalg::hoistViewAllocOps(FuncOp func) {
38   bool changed = true;
39   while (changed) {
40     changed = false;
41     func.walk([&changed](Operation *op) {
42       if (!isa<AllocOp, AllocaOp, DeallocOp>(op))
43         return;
44 
45       LLVM_DEBUG(DBGS() << "Candidate for hoisting: " << *op << "\n");
46       auto loop = dyn_cast<scf::ForOp>(op->getParentOp());
47       LLVM_DEBUG(DBGS() << "Parent op: " << *op->getParentOp() << "\n");
48 
49       // Only hoist out of immediately enclosing scf::ForOp.
50       if (!loop)
51         return;
52 
53       // If any operand is defined inside the loop don't hoist.
54       if (llvm::any_of(op->getOperands(), [&](Value v) {
55             return !loop.isDefinedOutsideOfLoop(v);
56           }))
57         return;
58 
59       LLVM_DEBUG(DBGS() << "All operands defined outside \n");
60 
61       // If alloc has other uses than ViewLikeOp and DeallocOp don't hoist.
62       Value v;
63       if (op->getNumResults() > 0) {
64         assert(op->getNumResults() == 1 && "Unexpected multi-result alloc");
65         v = op->getResult(0);
66       }
67       if (v && !llvm::all_of(v.getUses(), [&](OpOperand &operand) {
68             return isa<ViewLikeOpInterface, DeallocOp>(operand.getOwner());
69           })) {
70         LLVM_DEBUG(DBGS() << "Found non view-like or dealloc use: bail\n");
71         return;
72       }
73 
74       // Move AllocOp before the loop.
75       if (isa<AllocOp, AllocaOp>(op))
76         loop.moveOutOfLoop({op});
77       else // Move DeallocOp outside of the loop.
78         op->moveAfter(loop);
79       changed = true;
80     });
81   }
82 }
83 
84 /// Look for a transfer_read, in the given tensor uses, accessing the same
85 /// offset as the transfer_write.
86 static vector::TransferReadOp
87 findMatchingTransferRead(vector::TransferWriteOp write, Value srcTensor) {
88   for (Operation *user : srcTensor.getUsers()) {
89     auto read = dyn_cast<vector::TransferReadOp>(user);
90     if (read && read.indices() == write.indices() &&
91         read.getVectorType() == write.getVectorType()) {
92       return read;
93     }
94   }
95   return nullptr;
96 }
97 
98 /// Check if the chunk of data inserted by the transfer_write in the given
99 /// tensor are read by any other op than the read candidate.
100 static bool tensorChunkAccessedByUnknownOp(vector::TransferWriteOp write,
101                                            vector::TransferReadOp candidateRead,
102                                            Value srcTensor) {
103   // Make sure none of the other uses read the part of the tensor modified
104   // by the transfer_write.
105   llvm::SmallVector<Value::use_range, 1> uses;
106   uses.push_back(srcTensor.getUses());
107   while (!uses.empty()) {
108     for (OpOperand &use : uses.pop_back_val()) {
109       Operation *user = use.getOwner();
110       // Skip the candidate use, only inspect the "other" uses.
111       if (user == candidateRead.getOperation() || user == write.getOperation())
112         continue;
113       // Consider all transitive uses through a vector.transfer_write.
114       if (auto writeUser = dyn_cast<vector::TransferWriteOp>(user)) {
115         uses.push_back(writeUser->getResult(0).getUses());
116         continue;
117       }
118       // Consider all nested uses through an scf::ForOp. We may have
119       // pass-through tensor arguments left from previous level of
120       // hoisting.
121       if (auto forUser = dyn_cast<scf::ForOp>(user)) {
122         Value arg = forUser.getLoopBody().getArgument(
123             use.getOperandNumber() - forUser.getNumControlOperands() +
124             /*iv value*/ 1);
125         uses.push_back(arg.getUses());
126         continue;
127       }
128       // Follow the use yield as long as it doesn't escape the original
129       // region.
130       scf::YieldOp yieldUser = dyn_cast<scf::YieldOp>(user);
131       if (yieldUser &&
132           write->getParentOp()->isAncestor(yieldUser->getParentOp())) {
133         Value ret = yieldUser->getParentOp()->getResult(use.getOperandNumber());
134         uses.push_back(ret.getUses());
135         continue;
136       }
137       auto read = dyn_cast<vector::TransferReadOp>(user);
138       if (!read || !isDisjointTransferIndices(
139                        cast<VectorTransferOpInterface>(read.getOperation()),
140                        cast<VectorTransferOpInterface>(write.getOperation()))) {
141         return true;
142       }
143     }
144   }
145   return false;
146 }
147 
148 // To hoist transfer op on tensor the logic can be significantly simplified
149 // compared to the case on buffer. The transformation follows this logic:
150 // 1. Look for transfer_write with a single use from ForOp yield
151 // 2. Check the uses of the matching block argument and look for a transfer_read
152 // with the same indices.
153 // 3. Check that all the other uses of the tensor argument are either disjoint
154 // tensor_read or transfer_write. For transfer_write uses recurse to make sure
155 // the new tensor has the same restrictions on its uses.
156 // 4. Hoist the tensor_read/tensor_write and update the tensor SSA links.
157 // After this transformation the scf.forOp may have unused arguments that can be
158 // remove by the canonicalization pass.
159 void mlir::linalg::hoistRedundantVectorTransfersOnTensor(FuncOp func) {
160   bool changed = true;
161   while (changed) {
162     changed = false;
163     func.walk([&](scf::ForOp forOp) {
164       Operation *yield = forOp.getBody()->getTerminator();
165       for (auto it : llvm::enumerate(forOp.getRegionIterArgs())) {
166         Value ret = yield->getOperand(it.index());
167         auto write = ret.getDefiningOp<vector::TransferWriteOp>();
168         if (!write || !write->hasOneUse())
169           continue;
170         LLVM_DEBUG(DBGS() << "Candidate write for hoisting: "
171                           << *write.getOperation() << "\n");
172         if (llvm::any_of(write.indices(), [&forOp](Value index) {
173               return !forOp.isDefinedOutsideOfLoop(index);
174             }))
175           continue;
176         // Find a read with the same type and indices.
177         vector::TransferReadOp matchingRead =
178             findMatchingTransferRead(write, it.value());
179         // Make sure none of the other uses read the part of the tensor modified
180         // by the transfer_write.
181         if (!matchingRead ||
182             tensorChunkAccessedByUnknownOp(write, matchingRead, it.value()))
183           continue;
184 
185         // Hoist read before.
186         if (failed(forOp.moveOutOfLoop({matchingRead})))
187           llvm_unreachable(
188               "Unexpected failure to move transfer read out of loop");
189         // Update the source tensor.
190         matchingRead.sourceMutable().assign(forOp.initArgs()[it.index()]);
191 
192         // Hoist write after.
193         write->moveAfter(forOp);
194         yield->setOperand(it.index(), write.source());
195 
196         // Rewrite `loop` with new yields by cloning and erase the original
197         // loop.
198         OpBuilder b(matchingRead);
199         auto newForOp =
200             cloneWithNewYields(b, forOp, matchingRead.vector(), write.vector());
201 
202         // Transfer write has been hoisted, need to update the vector and tensor
203         // source. Replace the result of the loop to use the new tensor created
204         // outside the loop.
205         newForOp.getResult(it.index()).replaceAllUsesWith(write.getResult(0));
206         write.vectorMutable().assign(newForOp.getResults().back());
207         write.sourceMutable().assign(newForOp.getResult(it.index()));
208 
209         changed = true;
210         forOp.erase();
211         // Need to interrupt and restart because erasing the loop messes up the
212         // walk.
213         return WalkResult::interrupt();
214       }
215       return WalkResult::advance();
216     });
217   }
218 }
219 
220 void mlir::linalg::hoistRedundantVectorTransfers(FuncOp func) {
221   bool changed = true;
222   while (changed) {
223     changed = false;
224 
225     func.walk([&](vector::TransferReadOp transferRead) {
226       if (!transferRead.getShapedType().isa<MemRefType>())
227         return WalkResult::advance();
228 
229       LLVM_DEBUG(DBGS() << "Candidate for hoisting: "
230                         << *transferRead.getOperation() << "\n");
231       auto loop = dyn_cast<scf::ForOp>(transferRead->getParentOp());
232       LLVM_DEBUG(DBGS() << "Parent op: " << *transferRead->getParentOp()
233                         << "\n");
234       if (!loop)
235         return WalkResult::advance();
236 
237       if (failed(moveLoopInvariantCode(
238               cast<LoopLikeOpInterface>(loop.getOperation()))))
239         llvm_unreachable(
240             "Unexpected failure to move invariant code out of loop");
241 
242       LLVM_DEBUG(DBGS() << "Candidate read: " << *transferRead.getOperation()
243                         << "\n");
244 
245       llvm::SetVector<Operation *> forwardSlice;
246       getForwardSlice(transferRead, &forwardSlice);
247 
248       // Look for the last TransferWriteOp in the forwardSlice of
249       // `transferRead` that operates on the same memref.
250       vector::TransferWriteOp transferWrite;
251       for (auto *sliceOp : llvm::reverse(forwardSlice)) {
252         auto candidateWrite = dyn_cast<vector::TransferWriteOp>(sliceOp);
253         if (!candidateWrite || candidateWrite.source() != transferRead.source())
254           continue;
255         transferWrite = candidateWrite;
256       }
257 
258       // All operands of the TransferRead must be defined outside of the loop.
259       for (auto operand : transferRead.getOperands())
260         if (!loop.isDefinedOutsideOfLoop(operand))
261           return WalkResult::advance();
262 
263       // Only hoist transfer_read / transfer_write pairs for now.
264       if (!transferWrite)
265         return WalkResult::advance();
266 
267       LLVM_DEBUG(DBGS() << "Candidate: " << *transferWrite.getOperation()
268                         << "\n");
269 
270       // Approximate aliasing by checking that:
271       //   1. indices are the same,
272       //   2. no other operations in the loop access the same memref except
273       //      for transfer_read/transfer_write accessing statically disjoint
274       //      slices.
275       if (transferRead.indices() != transferWrite.indices() &&
276           transferRead.getVectorType() == transferWrite.getVectorType())
277         return WalkResult::advance();
278 
279       // TODO: may want to memoize this information for performance but it
280       // likely gets invalidated often.
281       DominanceInfo dom(loop);
282       if (!dom.properlyDominates(transferRead.getOperation(), transferWrite))
283         return WalkResult::advance();
284       for (auto &use : transferRead.source().getUses()) {
285         if (!dom.properlyDominates(loop, use.getOwner()))
286           continue;
287         if (use.getOwner() == transferRead.getOperation() ||
288             use.getOwner() == transferWrite.getOperation())
289           continue;
290         if (auto transferWriteUse =
291                 dyn_cast<vector::TransferWriteOp>(use.getOwner())) {
292           if (!isDisjointTransferSet(
293                   cast<VectorTransferOpInterface>(transferWrite.getOperation()),
294                   cast<VectorTransferOpInterface>(
295                       transferWriteUse.getOperation())))
296             return WalkResult::advance();
297         } else if (auto transferReadUse =
298                        dyn_cast<vector::TransferReadOp>(use.getOwner())) {
299           if (!isDisjointTransferSet(
300                   cast<VectorTransferOpInterface>(transferWrite.getOperation()),
301                   cast<VectorTransferOpInterface>(
302                       transferReadUse.getOperation())))
303             return WalkResult::advance();
304         } else {
305           // Unknown use, we cannot prove that it doesn't alias with the
306           // transferRead/transferWrite operations.
307           return WalkResult::advance();
308         }
309       }
310 
311       // Hoist read before.
312       if (failed(loop.moveOutOfLoop({transferRead})))
313         llvm_unreachable(
314             "Unexpected failure to move transfer read out of loop");
315 
316       // Hoist write after.
317       transferWrite->moveAfter(loop);
318 
319       // Rewrite `loop` with new yields by cloning and erase the original loop.
320       OpBuilder b(transferRead);
321       auto newForOp = cloneWithNewYields(b, loop, transferRead.vector(),
322                                          transferWrite.vector());
323 
324       // Transfer write has been hoisted, need to update the written value to
325       // the value yielded by the newForOp.
326       transferWrite.vector().replaceAllUsesWith(
327           newForOp.getResults().take_back()[0]);
328 
329       changed = true;
330       loop.erase();
331       // Need to interrupt and restart because erasing the loop messes up the
332       // walk.
333       return WalkResult::interrupt();
334     });
335   }
336 }
337 
338 /// Ensure prerequisites that guarantee pad op hoisting can occur.
339 /// Return failure in the cases when we cannot perform hoisting; i.e. if either:
340 ///   1. There exists a use of `simplePadOp` that is not a linalg input operand.
341 ///   2. There isn't an enclosing `outermostEnclosingForOp` loop.
342 ///   3. There exists an op with a region that is dominated by
343 ///   `outermostEnclosingForOp` and that isn't a LoopLikeInterface or a
344 ///    LinalgOp.
345 ///   3. There exists an op with side effects that is dominated by
346 ///    `outermostEnclosingForOp` and that isn't a LoopLikeInterface.
347 ///
348 /// While ensuring prerequisites:
349 ///   1. Fill the `backwardSlice` to contain the topologically sorted ops
350 ///   dominated by `outermostEnclosingForOp`.
351 ///   2. Fill the `packingLoops` to contain only the enclosing loops of
352 ///   `backwardSlice` whose IV is actually used in computing padding. Loops that
353 ///   remain in `backwardSlice` but that are not in `packingLoops` are
354 ///   dimensions of reuse.
355 static LogicalResult
356 hoistPaddingOnTensorsPrerequisites(linalg::SimplePadOp simplePadOp, int nLevels,
357                                    llvm::SetVector<Operation *> &backwardSlice,
358                                    llvm::SetVector<Operation *> &packingLoops) {
359   // Bail on any use that isn't an input of a Linalg op.
360   // Hoisting of inplace updates happens after vectorization.
361   for (OpOperand &use : simplePadOp.result().getUses()) {
362     auto linalgUser = dyn_cast<linalg::LinalgOp>(use.getOwner());
363     if (!linalgUser || !linalgUser.isInputTensor(&use))
364       return failure();
365   }
366 
367   // Get at most nLevels of enclosing loops.
368   SmallVector<LoopLikeOpInterface> reverseEnclosingLoops;
369   Operation *outermostEnclosingForOp = nullptr,
370             *nextEnclosingForOp =
371                 simplePadOp->getParentOfType<LoopLikeOpInterface>();
372   while (nLevels-- > 0 && nextEnclosingForOp) {
373     outermostEnclosingForOp = nextEnclosingForOp;
374     reverseEnclosingLoops.push_back(outermostEnclosingForOp);
375     nextEnclosingForOp =
376         nextEnclosingForOp->getParentOfType<LoopLikeOpInterface>();
377   }
378   if (!outermostEnclosingForOp)
379     return failure();
380 
381   // Get the backwards slice from `simplePadOp` that is dominated by the
382   // outermost enclosing loop.
383   DominanceInfo domInfo(outermostEnclosingForOp);
384   getBackwardSlice(simplePadOp, &backwardSlice, [&](Operation *op) {
385     return domInfo.dominates(outermostEnclosingForOp, op);
386   });
387 
388   #if 0
389 
390   // Bail on any op with a region that is not a LoopLikeInterface or a LinalgOp.
391   // Bail on any op with side effects that is not a LoopLikeInterface.
392   if (llvm::any_of(backwardSlice, [](Operation *op) {
393         if (isa<LoopLikeOpInterface>(op))
394           return false;
395         if (!MemoryEffectOpInterface::hasNoEffect(op))
396           return true;
397         return op->getNumRegions() > 0 && !isa<LinalgOp>(op);
398       }))
399     return failure();
400 
401   #else
402 
403   // Bail on any op with a region that is not a LoopLikeInterface or a LinalgOp.
404   if (llvm::any_of(backwardSlice, [](Operation *op) {
405         return op->getNumRegions() > 0 && !isa<LoopLikeOpInterface>(op) &&
406                !isa<LinalgOp>(op);
407       }))
408     return failure();
409 
410   #endif
411 
412   // Filter out the loops whose induction variable is not used to compute the
413   // padded result. As a first approximation, just look for IVs that have no use
414   // in the backwardSlice.
415   // These are the dimensions of reuse that we can exploit to reduce the amount
416   // of work / memory.
417   // TODO: would this optimization compose better as a canonicalization?
418   for (LoopLikeOpInterface loop : reverseEnclosingLoops) {
419     auto forOp = dyn_cast<scf::ForOp>(loop.getOperation());
420     if (!forOp)
421       continue;
422     for (Operation *user : forOp.getInductionVar().getUsers()) {
423       if (backwardSlice.contains(user)) {
424         packingLoops.insert(forOp);
425         break;
426       }
427     }
428   }
429 
430   // Backward slice is a topologically sorted list of ops starting at
431   // `outermostEnclosingForOp`.
432   assert(outermostEnclosingForOp == backwardSlice.front());
433 
434   return success();
435 }
436 
437 static Value buildLoopTripCount(OpBuilder &b, Operation *op) {
438   MLIRContext *ctx = op->getContext();
439   AffineExpr lb, ub, step = getAffineSymbolExpr(0, ctx);
440   bindDims(ctx, lb, ub);
441   scf::ForOp forOp = cast<scf::ForOp>(op);
442   return b.create<AffineApplyOp>(
443       op->getLoc(), AffineMap::get(2, 1, {(ub - lb).ceilDiv(step)}, ctx),
444       ValueRange{forOp.lowerBound(), forOp.upperBound(), forOp.step()});
445 }
446 
447 /// Mechanically hoist padding operations on tensors by at most `nLoops` into a
448 /// new, generally larger tensor. This achieves packing of multiple padding ops
449 /// into a larger tensor. On success, `simplePadOp` is replaced by the cloned
450 /// version in the packing loop so the caller can continue reasoning about the
451 /// padding operation.
452 ///
453 /// Example in pseudo-mlir:
454 /// =======================
455 ///
456 /// If hoistPaddingOnTensors is called with `nLoops` = 2 on the following IR.
457 /// ```
458 ///    scf.for (%i, %j, %k)
459 ///      %st0 = subtensor f(%i, %k) : ... to tensor<?x?xf32>
460 ///      %0 = linalg.simple_pad %st0 pad %pad :
461 ///             tensor<?x?xf32> to tensor<4x8xf32>
462 ///      compute(%0)
463 /// ```
464 ///
465 /// IR resembling the following is produced:
466 ///
467 /// ```
468 ///    scf.for (%i) {
469 ///      %packed_init = linalg.init_tensor range(%j) : tensor<?x4x8xf32>
470 ///      %packed = scf.for (%k) iter_args(%p : %packed_init)
471 ///        %st0 = subtensor f(%i, %k) : ... to tensor<?x?xf32>
472 ///        %0 = linalg.simple_pad %st0 pad %pad :
473 ///               tensor<?x?xf32> to tensor<4x8xf32>
474 ///        scf.yield %1: tensor<?x4x8xf32>
475 ///      } -> tensor<?x4x8xf32>
476 ///      scf.for (%j, %k) {
477 ///        %st0 = subtensor %packed [%k, 0, 0][1, 4, 8][1, 1, 1] :
478 ///                 tensor<?x4x8xf32> to tensor<4x8xf32>
479 ///        compute(%st0)
480 ///      }
481 ///    }
482 /// ```
483 LogicalResult mlir::linalg::hoistPaddingOnTensors(SimplePadOp &simplePadOp,
484                                                   unsigned nLoops) {
485   llvm::SetVector<Operation *> backwardSlice, packingLoops;
486   if (failed(hoistPaddingOnTensorsPrerequisites(simplePadOp, nLoops,
487                                                 backwardSlice, packingLoops)))
488     return failure();
489 
490   // Update actual number of loops, which may be smaller.
491   nLoops = packingLoops.size();
492 
493   Location loc = simplePadOp->getLoc();
494   RankedTensorType paddedTensorType = simplePadOp.getResultType();
495   unsigned paddedRank = paddedTensorType.getRank();
496 
497   // Backward slice is a topologically sorted list of ops starting at
498   // `outermostEnclosingForOp`.
499   Operation *outermostEnclosingForOp = backwardSlice.front();
500   // IP just before the outermost loop considered that we hoist above.
501   OpBuilder b(outermostEnclosingForOp);
502 
503   // Create the packed tensor<?x?x..?xpadded_shape> into which we amortize
504   // padding.
505   SmallVector<int64_t> packedShape(nLoops, ShapedType::kDynamicSize);
506   // TODO: go grab dims when necessary, for now SimplePadOp returns a static
507   // tensor.
508   llvm::append_range(packedShape, paddedTensorType.getShape());
509   auto packedTensorType =
510       RankedTensorType::get(packedShape, paddedTensorType.getElementType());
511   auto dynamicSizes = llvm::to_vector<4>(llvm::map_range(
512       packingLoops, [&](Operation *op) { return buildLoopTripCount(b, op); }));
513   Value packedTensor = b.create<linalg::InitTensorOp>(
514       loc, dynamicSizes, packedTensorType.getShape(),
515       packedTensorType.getElementType());
516 
517   // Clone the operations involved in the backward slice, iteratively stepping
518   // into the loops that we encounter.
519   // The implementation proceeds in a stack-like fashion:
520   //   1. Iteratively clone and step into the loops, pushing the `packedTensor`
521   //      deeper in the stack.
522   //   2. Create a SubTensorInsert at the top of the stack.
523   //   3. Iteratively pop and yield the result of the SubTensorInsertOp across
524   //     the cloned loops.
525   SmallVector<Value> clonedLoopIvs;
526   clonedLoopIvs.reserve(nLoops);
527   BlockAndValueMapping bvm;
528   // Stack step 1. iteratively clone loops and push `packedTensor`.
529   // Insert `simplePadOp` into the backwardSlice so we clone it too.
530   backwardSlice.insert(simplePadOp);
531   for (Operation *op : backwardSlice) {
532     if (op->getNumRegions() == 0) {
533       b.clone(*op, bvm);
534       continue;
535     }
536     // TODO: support more cases as they appear.
537     auto forOp = dyn_cast<scf::ForOp>(op);
538     assert(forOp && "Expected scf::ForOp when hoisting pad ops");
539     // Unused loop, just skip it.
540     if (!packingLoops.contains(forOp))
541       continue;
542     auto clonedForOp =
543         b.create<scf::ForOp>(loc, forOp.lowerBound(), forOp.upperBound(),
544                              forOp.step(), packedTensor);
545     assert(clonedForOp->getNumRegions() == 1);
546     clonedLoopIvs.push_back(clonedForOp.getInductionVar());
547     b.setInsertionPointToStart(&clonedForOp->getRegion(0).front());
548     bvm.map(forOp.getInductionVar(), clonedLoopIvs.back());
549     packedTensor = clonedForOp.getRegionIterArgs().front();
550   }
551 
552   // Stack step 2. create SubTensorInsertOp at the top of the stack.
553   // offsets = [clonedLoopIvs, 0 .. 0].
554   SmallVector<OpFoldResult> offsets(clonedLoopIvs.begin(), clonedLoopIvs.end());
555   offsets.append(paddedRank, b.getIndexAttr(0));
556   // sizes = [1 .. 1, paddedShape].
557   SmallVector<OpFoldResult> sizes(nLoops, b.getIndexAttr(1));
558   for (int64_t sz : paddedTensorType.getShape()) {
559     // TODO: go grab dims when necessary, for now SimplePadOp returns a static
560     // tensor.
561     assert(!ShapedType::isDynamic(sz) && "padded tensor needs static sizes");
562     sizes.push_back(b.getIndexAttr(sz));
563   }
564   // strides = [1 .. 1].
565   SmallVector<OpFoldResult> strides(nLoops + paddedRank, b.getIndexAttr(1));
566 
567   Value inserted =
568       b.create<SubTensorInsertOp>(loc, bvm.lookup(simplePadOp.result()),
569                                   packedTensor, offsets, sizes, strides);
570 
571   // Stack step 3. iteratively pop the stack and propagate the yield.
572   Value valueToYield = inserted;
573   for (Value iv : llvm::reverse(clonedLoopIvs)) {
574     auto forOp = scf::getForInductionVarOwner(iv);
575     b.setInsertionPointToEnd(&forOp.getRegion().front());
576     b.create<scf::YieldOp>(loc, valueToYield);
577     valueToYield = forOp.getResult(0);
578   }
579 
580   // Now the packed tensor is ready, replace the original padding op by a
581   // 1x..x1 SubTensor [originalLoopIvs, 0 .. 0][1 .. 1, paddedShape][1 .. 1].
582   b.setInsertionPoint(simplePadOp);
583   SmallVector<Value> originalLoopIvs =
584       llvm::to_vector<4>(llvm::map_range(packingLoops, [](Operation *loop) {
585         return cast<scf::ForOp>(loop).getInductionVar();
586       }));
587   // offsets = [originalLoopIvs, 0 .. 0].
588   offsets.assign(originalLoopIvs.begin(), originalLoopIvs.end());
589   offsets.append(paddedRank, b.getIndexAttr(0));
590   // sizes = [1 .. 1, paddedShape] (definedabove).
591   // strides = [1 .. 1] (defined above)
592   packedTensor =
593       scf::getForInductionVarOwner(clonedLoopIvs.front())->getResult(0);
594   simplePadOp.replaceAllUsesWith(
595       b.create<SubTensorOp>(loc, simplePadOp.getResultType(), packedTensor,
596                             offsets, sizes, strides)
597           ->getResult(0));
598 
599   Operation *toErase = simplePadOp;
600 
601   // Make the newly cloned `simplePadOp` available to the caller.
602   simplePadOp =
603       cast<SimplePadOp>(bvm.lookup(simplePadOp.result()).getDefiningOp());
604 
605   toErase->erase();
606 
607   return success();
608 }
609