Enzyme main
Loading...
Searching...
No Matches
RemovalUtils.h
Go to the documentation of this file.
1//===- RemovalUtils.h - Utilities to remove Enzyme ops -------* C++ -*-===//
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#pragma once
9
10#include "Dialect/Ops.h"
13#include "mlir/Dialect/Affine/IR/AffineOps.h"
14#include "mlir/Dialect/Arith/IR/Arith.h"
15#include "mlir/Dialect/GPU/IR/GPUDialect.h"
16#include "mlir/Dialect/MemRef/IR/MemRef.h"
17#include "mlir/Dialect/Tensor/IR/Tensor.h"
18#include "mlir/IR/Builders.h"
19#include "mlir/IR/IRMapping.h"
20#include "mlir/Interfaces/LoopLikeInterface.h"
21#include "mlir/Transforms/LoopInvariantCodeMotionUtils.h"
22
23#include "mlir/Interfaces/FunctionInterfaces.h"
24
25#include "mlir/IR/Matchers.h"
26
27namespace mlir {
28namespace enzyme {
29
30constexpr static llvm::StringLiteral kPreserveCacheAttrName = "preserve_cache";
31
32/// Information about a cache, each cache init should have one corresponding
33/// push and pop.
34struct CacheInfo {
35 enzyme::InitOp initOp;
36 enzyme::PushOp pushOp;
37 enzyme::PopOp popOp;
38
40 initOp = nullptr;
41 pushOp = nullptr;
42 popOp = nullptr;
43 }
44 CacheInfo(Value cache) {
45 initOp = cache.getDefiningOp<enzyme::InitOp>();
46 unsigned nusers = 0;
47 for (auto user : cache.getUsers()) {
48 nusers++;
49 if (!popOp)
50 popOp = dyn_cast<enzyme::PopOp>(user);
51 if (!pushOp)
52 pushOp = dyn_cast<enzyme::PushOp>(user);
53 }
54 (void)nusers;
55 assert(nusers == 2); // TODO: support more uses
56 }
57
58 Value pushedValue() { return pushOp.getValue(); }
59 Type cachedType() {
60 return cast<enzyme::CacheType>(initOp.getResult().getType()).getType();
61 }
62
63 // Pushed values must be the same
64 CacheInfo merge(CacheInfo other, PatternRewriter &rewriter);
65};
66
67// Tries to limit the amount of values cache from block `forward` to `reverse`
68// using a mincut algorithm and heuristics based on the size of values.
69// All pushes must go after `lastFwd`, if non null
70void minCutCache(Block *forward, Block *reverse, SmallVector<CacheInfo> &caches,
71 PatternRewriter &rewriter, const IRMapping &fwdrevmap,
72 Operation *lastFwd = nullptr);
73
74// Perform a modified version of the loop invariant cache algorithm described in
75// Fig. 5 of https://dl.acm.org/doi/pdf/10.1145/3458817.3476165
76//
77// array = alloca
78// for i = 0 to N {
79// idx = some_computation(%i)
80// val = load array[idx]
81// push(cache, val)
82// }
83// for ri = N to 0 {
84// val = pop(cache)
85// use(val)
86// }
87//
88// Becomes:
89// array = alloca
90// copy(cache, array)
91// for i = 0 to N {
92// %idx = some_computation(%i)
93// ...
94// }
95// for ri = N to 0 {
96// idx = some_computation(i)
97// val = cache[idx]
98// use(val)
99// }
100//
101// The size of the cache goes from O(N) to O(sizeof(array)). This heuristically
102// only stores stack allocations with a statically-known number of elements
103// <= 'threshold'.
104void loopInvariantCache(SmallVectorImpl<CacheInfo> &caches,
105 LoopLikeOpInterface fwdLoop,
106 LoopLikeOpInterface revLoop, const IRMapping &mapping,
107 int64_t threshold = 16);
108
110
111static LoopCacheType getCacheType(Operation *op) {
113 if (op->hasAttr("enzyme.cache_use_tensor")) {
114 cacheType = LoopCacheType::TENSOR;
115 }
116 return cacheType;
117}
118
119static bool hasMinCut(Operation *op) {
120 if (op->hasAttr("enzyme.disable_mincut"))
121 return false;
122
123 while ((op = op->getParentOp()))
124 if (op->hasAttr("enzyme.disable_mincut"))
125 return false;
126
127 return true;
128}
129
130static bool hasLICM(Operation *op) {
131 return !op->hasAttr("enzyme.disable_licm");
132}
133
134static Value ensureIndexType(Value value, OpBuilder &builder) {
135 if (isa<IndexType>(value.getType())) {
136 return value;
137 }
138 OpBuilder::InsertionGuard guard(builder);
139 builder.setInsertionPointAfterValue(value);
140 return arith::IndexCastOp::create(builder, value.getLoc(),
141 builder.getIndexType(), value);
142}
143
144static Value traceToNonBlockArgSource(Value value) {
145 while (!isa<BlockArgument>(value)) {
146 if (auto svOp = value.getDefiningOp<memref::SubViewOp>())
147 value = svOp.getSource();
148 else if (auto castOp = value.getDefiningOp<memref::CastOp>())
149 value = castOp.getSource();
150 else if (auto rcOp = value.getDefiningOp<memref::ReinterpretCastOp>())
151 value = rcOp.getSource();
152 else
153 break;
154 }
155 return isa<BlockArgument>(value) ? nullptr : value;
156}
157
158/// The pushed value may reach its allocation through casts that erase static
159/// extents: canonicalization rewrites `memref.alloc(%c) : memref<?xT>` into
160/// `memref.alloc() : memref<CxT>` + `memref.cast`. When the allocation itself
161/// becomes the multi-dimensional cache storage, its type is the accurate one --
162/// sizing the cache from the cast's dynamic type instead leaves dynamic
163/// dimensions that `appendDynamicDims` has no operand for, yielding an invalid
164/// cache allocation and row slices whose sizes don't match their operands.
165static ShapedType
167 MultidimensionalAllocInterface allocOp) {
168 if (!allocOp || allocOp->getNumResults() != 1)
169 return ST;
170 auto allocTy = dyn_cast<ShapedType>(allocOp->getResult(0).getType());
171 if (!allocTy || allocTy.getElementType() != ST.getElementType() ||
172 allocTy.getRank() != ST.getRank())
173 return ST;
174 // Only take the allocation's type when it strictly adds static information.
175 for (auto [a, s] : llvm::zip_equal(allocTy.getShape(), ST.getShape()))
176 if (!ShapedType::isDynamic(s) && a != s)
177 return ST;
178 return allocTy;
179}
180
181static MultidimensionalAllocInterface
182getMultiDimCacheAllocOp(Value pushedValue, LoopCacheType cacheType,
183 ShapedType shapeTy, Operation *forOp) {
184 if (cacheType != LoopCacheType::MEMREF)
185 return nullptr;
186 Value source = traceToNonBlockArgSource(pushedValue);
187 if (!source)
188 return nullptr;
189 auto allocOp =
190 dyn_cast_or_null<MultidimensionalAllocInterface>(source.getDefiningOp());
191 if (!allocOp || !allocOp.hoistable(forOp))
192 return nullptr;
193 return allocOp;
194}
195
196template <typename FinalClass, typename OpName>
198 : public EnzymeOpsRemoverOpInterface::ExternalModel<FinalClass, OpName> {
199private:
200public:
201 struct IntOrValue {
202 size_t ival;
203 mlir::Value vval;
204 IntOrValue(mlir::Value vval) : ival(0), vval(vval) {}
205 IntOrValue(size_t ival) : ival(ival), vval(nullptr) {}
206 };
207
208 static bool Equivalent(Value lhs, Value rhs) {
209 if (lhs == rhs)
210 return true;
211 Attribute la, ra;
212 if (matchPattern(lhs, m_Constant(&la)) &&
213 matchPattern(rhs, m_Constant(&ra)))
214 return true;
215 auto pop = rhs.getDefiningOp<enzyme::PopOp>();
216 if (!pop)
217 return false;
218 auto init = pop.getOperand().getDefiningOp<enzyme::InitOp>();
219 if (!init)
220 return false;
221 for (auto u : init->getResult(0).getUsers()) {
222 if (u == pop)
223 continue;
224 auto push = dyn_cast<enzyme::PushOp>(u);
225 if (!push)
226 continue;
227 return push.getValue() == lhs;
228 }
229 return false;
230 }
231
232 static llvm::SmallVector<mlir::Value>
233 computeReversedIndices(PatternRewriter &rewriter, OpName op,
234 llvm::ArrayRef<mlir::Value> otherInductionVariable,
235 llvm::ArrayRef<IntOrValue> bounds) {
236 llvm::SmallVector<mlir::Value> results;
237 for (auto &&[bound, iv] : llvm::zip_equal(bounds, otherInductionVariable)) {
238 if (affine::isAffineInductionVar(iv)) {
239 AffineExpr reversedIV;
240 SmallVector<Value> operands{iv};
241 unsigned numSymbols = 0;
242 AffineExpr ivExpr = rewriter.getAffineDimExpr(0);
243
244 if (bound.vval) {
245 AffineExpr boundExpr = rewriter.getAffineSymbolExpr(0);
246 reversedIV = boundExpr - 1 - ivExpr;
247 operands.push_back(bound.vval);
248 numSymbols = 1;
249 } else {
250 reversedIV = rewriter.getAffineConstantExpr(bound.ival - 1) - ivExpr;
251 }
252
253 AffineMap map = AffineMap::get(/*dimCount=*/1, numSymbols, reversedIV);
254 results.push_back(affine::AffineApplyOp::create(rewriter, op->getLoc(),
255 map, operands));
256 continue;
257 }
258
259 Value boundv;
260 if (bound.vval) {
261 Value c1;
262 if (iv.getType().isIndex())
263 c1 = arith::ConstantIndexOp::create(rewriter, op->getLoc(), 1);
264 else
265 c1 = arith::ConstantIntOp::create(rewriter, op->getLoc(),
266 iv.getType(), 1);
267 boundv = arith::SubIOp::create(rewriter, op->getLoc(), bound.vval, c1);
268 } else {
269 if (iv.getType().isIndex())
270 boundv = arith::ConstantIndexOp::create(rewriter, op->getLoc(),
271 bound.ival - 1);
272 else
273 boundv = arith::ConstantIntOp::create(rewriter, op->getLoc(),
274 iv.getType(), bound.ival - 1);
275 }
276 Value result = arith::SubIOp::create(rewriter, op->getLoc(), boundv, iv);
277 results.push_back(result);
278 }
279 return results;
280 }
281
282 LogicalResult removeEnzymeOps(Operation *op,
283 PatternRewriter &rewriter) const {
284 auto forOp = cast<OpName>(op);
285
286 if (hasLICM(op)) { // perform licm
287 auto loopLike = dyn_cast<LoopLikeOpInterface>(op);
288
289 if (loopLike) {
290 mlir::moveLoopInvariantCode(loopLike);
291 }
292 }
293
294 OpName otherForOp = nullptr; // where caches pops are
295
296 // There is support for two push/pop removal modes, one is using immutable
297 // tensors, the other uses memrefs. memref is the default, but tensor can be
298 // enabled with enzyme.cache_use_tensor
299 auto cacheType = getCacheType(op);
300
301 // Gradients whose values need to be passed as iteration variables.
302 llvm::SetVector<Value> updatedGradients;
303
304 llvm::MapVector<Value, CacheInfo> cachesMap;
305 SmallVector<CacheInfo> toDelete;
306
307 Block *body = forOp.getBody();
308
309 body->walk([&](Operation *op) {
310 if (auto setOp = dyn_cast<enzyme::SetOp>(op)) {
311 if (!body->getParent()->isAncestor(
312 setOp.getGradient().getDefiningOp()->getParentRegion()))
313 updatedGradients.insert(setOp.getGradient());
314 }
315
316 if (auto pushOp = dyn_cast<enzyme::PushOp>(op)) {
317 CacheInfo info(pushOp.getCache());
318
319 Value pushedValue = info.pushedValue();
320 if (cachesMap.contains(pushedValue)) {
321 info = info.merge(cachesMap.lookup(pushedValue), rewriter);
322 }
323
324 if (info.pushOp->getBlock() == body && info.popOp->getBlock() == body &&
325 info.pushOp->isBeforeInBlock(info.popOp)) {
326 toDelete.push_back(info);
327 return;
328 }
329
330 cachesMap[pushedValue] = info;
331
332 if (isa<OpName>(info.popOp->getParentOp())) {
333 otherForOp = cast<OpName>(info.popOp->getParentOp());
334 }
335 }
336 });
337
338 while (!toDelete.empty()) {
339 CacheInfo info = toDelete.pop_back_val();
340 rewriter.replaceAllUsesWith(info.popOp.getResult(),
341 info.pushOp.getValue());
342 rewriter.eraseOp(info.pushOp);
343 rewriter.eraseOp(info.popOp);
344 rewriter.eraseOp(info.initOp);
345 }
346
347 SmallVector<CacheInfo> caches0 =
348 llvm::map_to_vector(cachesMap, [](auto p) { return std::get<1>(p); });
349
350 SmallVector<CacheInfo> caches = caches0;
351
352 // nothing to do
353 if (updatedGradients.empty() && caches.empty())
354 return success();
355
356 // What is left here is carried out of this loop, so removing it needs the
357 // loop the pops are in, and that is only found when a pop's parent is a
358 // loop of this same kind (see where otherForOp is set). Without one, every
359 // use of otherForOp below would be through a null op. Report it rather than
360 // leaving caches nothing further down can lower either. Checked before the
361 // gradient rewriting starts, so the loop is not left half-transformed.
362 if (!caches.empty() && !otherForOp) {
363 auto diag = forOp->emitError()
364 << "cannot remove " << caches.size()
365 << " cache(s) pushed in this loop: no " << forOp->getName()
366 << " holding their pops to pair it with";
367 for (CacheInfo &info : caches)
368 diag.attachNote(info.popOp->getLoc())
369 << "pop is in " << info.popOp->getParentOp()->getName()
370 << ", pushed from " << info.pushOp->getParentOp()->getName();
371 return diag;
372 }
373
374 DenseMap<Value, llvm::SmallVector<Operation *>> updatedGradientUsers;
375
376 for (auto &it : llvm::make_early_inc_range(*body)) {
377 Operation *op = &it;
378
379 auto getOp = dyn_cast<enzyme::GetOp>(op);
380
381 if (getOp && updatedGradients.contains(getOp.getGradient())) {
382 updatedGradientUsers[getOp.getGradient()].push_back(getOp);
383 } else if (auto setOp = dyn_cast<enzyme::SetOp>(op)) {
384 updatedGradientUsers[setOp.getGradient()].push_back(setOp);
385 }
386
387 if (!getOp || updatedGradients.contains(getOp.getGradient()))
388 continue;
389
390 auto outerGet = enzyme::GetOp::create(rewriter, getOp->getLoc(),
391 getOp.getResult().getType(),
392 getOp.getGradient());
393
394 rewriter.replaceAllUsesWith(getOp.getResult(), outerGet.getResult());
395 rewriter.eraseOp(getOp);
396 }
397
398 // postadd means that the loops init is zero and that the result
399 // is added with the previous grad after the loop.
400 bool postAdd = FinalClass::mustPostAdd(forOp);
401
402 auto term = body->getTerminator();
403
404 SmallVector<Value> newOperands(FinalClass::getInits(forOp));
405 for (auto grad : updatedGradients) {
406 auto Ty = cast<enzyme::GradientType>(grad.getType()).getBasetype();
407
408 Value newInit;
409
410 if (!postAdd) {
411 newInit = enzyme::GetOp::create(rewriter, grad.getLoc(), Ty, grad);
412 } else {
413 newInit = cast<AutoDiffTypeInterface>(Ty).createNullValue(
414 rewriter, grad.getLoc());
415 }
416
417 newOperands.push_back(newInit);
418
419 // here we do a primitive form of mem2reg within the loop. We have a
420 // sorted (by instruction number) list of all users of the
421 // instruction.
422 Value val = FinalClass::initialValueInBlock(rewriter, body, grad);
423 for (auto user : updatedGradientUsers[grad]) {
424 if (auto getOp = dyn_cast<enzyme::GetOp>(user)) {
425 rewriter.replaceOp(getOp, val);
426 } else {
427 auto setOp = cast<enzyme::SetOp>(user);
428 val = setOp.getValue();
429 rewriter.eraseOp(setOp);
430 }
431 }
432
433 term->insertOperands(term->getNumOperands(), ValueRange(val));
434 }
435
436 IRMapping fwdrevmap;
437 bool mincut = false;
438
439 // [0,..., N - 1] counter
440 SmallVector<Value> inductionVariable;
441 SmallVector<Value> otherInductionVariable;
442 SmallVector<Value> reversedIndex;
443
444 SmallVector<IntOrValue> revNumIters;
445 SmallVector<IntOrValue> fwdNumIters;
446
447 if (!fwdNumIters.size()) {
448 OpBuilder::InsertionGuard guard(rewriter);
449 rewriter.setInsertionPoint(forOp);
450 fwdNumIters = FinalClass::getDimensionBounds(rewriter, forOp);
451 }
452
453 Operation *lastFwd = nullptr;
454 if (caches.size()) {
455 rewriter.setInsertionPointToStart(forOp.getBody());
456 inductionVariable = llvm::map_to_vector(
457 FinalClass::getCanonicalLoopIVs(rewriter, forOp),
458 [&rewriter](Value iv) { return ensureIndexType(iv, rewriter); });
459 if (rewriter.getInsertionPoint() != forOp.getBody()->begin()) {
460 lastFwd = rewriter.getInsertionPoint()->getPrevNode();
461 }
462
463 rewriter.setInsertionPointToStart(otherForOp.getBody());
464 otherInductionVariable =
465 FinalClass::getCanonicalLoopIVs(rewriter, otherForOp);
466
467 // The reverse iteration count may not be known at this point, as it may
468 // be cached via a push/pop, use the fwd count in that case.
469 if (!revNumIters.size()) {
470 OpBuilder::InsertionGuard guard(rewriter);
471 rewriter.setInsertionPoint(otherForOp);
472 revNumIters = FinalClass::getDimensionBounds(rewriter, otherForOp);
473 for (auto &&[rev, fwd] : llvm::zip_equal(revNumIters, fwdNumIters)) {
474 if (!fwd.vval && rev.vval) {
475 rev.vval = nullptr;
476 rev.ival = fwd.ival;
477 }
478 }
479 }
480
481 reversedIndex = FinalClass::computeReversedIndices(
482 rewriter, otherForOp, otherInductionVariable, revNumIters);
483 fwdrevmap = FinalClass::createArgumentMap(
484 rewriter, forOp, inductionVariable, otherForOp, reversedIndex);
485 for (auto v : inductionVariable) {
486 if (auto op = v.getDefiningOp()) {
487 op->setAttr("enzyme.no_erase", rewriter.getUnitAttr());
488 }
489 }
490 for (auto v : otherInductionVariable) {
491 if (auto op = v.getDefiningOp()) {
492 op->setAttr("enzyme.no_erase", rewriter.getUnitAttr());
493 }
494 }
495 }
496
497 if (hasMinCut(forOp) && caches.size()) {
498 mincut = true;
499 mlir::enzyme::minCutCache(forOp.getBody(), otherForOp.getBody(), caches,
500 rewriter, fwdrevmap, lastFwd);
501 mlir::enzyme::loopInvariantCache(caches, forOp, otherForOp, fwdrevmap);
502 }
503 for (auto v : inductionVariable) {
504 if (auto op = v.getDefiningOp()) {
505 op->removeAttr("enzyme.no_erase");
506 }
507 }
508 for (auto v : otherInductionVariable) {
509 if (auto op = v.getDefiningOp()) {
510 op->removeAttr("enzyme.no_erase");
511 }
512 }
513 auto revIP = rewriter.saveInsertionPoint();
514
515 SmallVector<Value> newPushValues;
516
517 unsigned numNewValuePushes = 0;
518
519 if (lastFwd)
520 rewriter.setInsertionPointAfter(lastFwd);
521 else
522 rewriter.setInsertionPointToStart(forOp.getBody());
523 for (auto &info : caches) {
524
525 Value pushedValue = info.pushedValue();
526 if (mincut)
527 assert(forOp.getRegion().isAncestor(pushedValue.getParentRegion()));
528
529 // Otherwise, add a new variable to keep track.
530 if (!inductionVariable.size()) {
531 Value zero = arith::ConstantOp::create(rewriter, forOp->getLoc(),
532 rewriter.getIndexAttr(0));
533 newOperands.push_back(zero);
534
535 inductionVariable = {
536 body->addArgument(zero.getType(), forOp->getLoc())};
537 {
538 OpBuilder::InsertionGuard guard(rewriter);
539 rewriter.setInsertionPoint(term);
540
541 auto one = arith::ConstantOp::create(rewriter, forOp->getLoc(),
542 rewriter.getIndexAttr(1));
543 auto newInductionVar = arith::AddIOp::create(
544 rewriter, forOp->getLoc(), inductionVariable[0], one);
545 term->insertOperands(term->getNumOperands(),
546 ValueRange(newInductionVar));
547 }
548 }
549
550 SmallVector<int64_t> newShape;
551 SmallVector<Value> dynamicDims;
552 for (const auto &dim : fwdNumIters) {
553 if (dim.vval) {
554 newShape.push_back(mlir::ShapedType::kDynamic);
555 dynamicDims.push_back(ensureIndexType(dim.vval, rewriter));
556 } else {
557 newShape.push_back(dim.ival);
558 }
559 }
560
561 // Number of entries in `dynamicDims` describing the loop-iteration
562 // dimensions. Only *dynamic* iteration counts contribute an entry, so
563 // this is <= inductionVariable.size(); anything appended past this point
564 // (by appendDynamicDims below) describes the cached value's own shape.
565 size_t numLoopDynamicDims = dynamicDims.size();
566
567 auto ET = info.cachedType();
568 ShapedType NT;
569
570 bool multiDim = false;
571 Attribute memorySpace = nullptr;
572 MultidimensionalAllocInterface allocOp;
573 // The cached value's shaped type, refined with whatever static extents
574 // its allocation knows about. Used in place of `info.cachedType()` below
575 // so the cache allocation and the row slices into it agree.
576 ShapedType cachedShapedTy;
577 if (auto ST = dyn_cast<ShapedType>(ET)) {
578 allocOp = getMultiDimCacheAllocOp(pushedValue, cacheType, ST, forOp);
579 ST = refineCacheShapeFromAlloc(ST, allocOp);
580 cachedShapedTy = ST;
581 bool fullyStatic = llvm::all_of(ST.getShape(), [](int64_t dim) {
582 return dim != ShapedType::kDynamic;
583 });
584 multiDim =
585 allocOp || (fullyStatic && cacheType == LoopCacheType::TENSOR);
586 if (allocOp) {
587 if (auto MT = dyn_cast<MemRefType>(pushedValue.getType()))
588 memorySpace = MT.getMemorySpace();
589 allocOp.appendDynamicDims(dynamicDims);
590 }
591
592 if (multiDim) {
593 newShape.append(ST.getShape().begin(), ST.getShape().end());
594 ET = ST.getElementType();
595 }
596 }
597
598 if (!memorySpace) {
599 if (auto innerMT = dyn_cast<MemRefType>(ET))
600 memorySpace = innerMT.getMemorySpace();
601 }
602
603 auto newType = cacheType == LoopCacheType::TENSOR
604 ? cast<ShapedType>(RankedTensorType::get(newShape, ET))
605 : cast<ShapedType>(MemRefType::get(newShape, ET));
606 if (memorySpace)
607 newType = cast<ShapedType>(cast<MemRefType>(newType)
608 .clonePtrWith(memorySpace, std::nullopt)
609 .value());
610
611 // Replace the pushes in the forward pass
612 if (cacheType == LoopCacheType::TENSOR) {
613 {
614 OpBuilder::InsertionGuard guard(rewriter);
615 rewriter.setInsertionPoint(forOp);
616 Value initValue = tensor::EmptyOp::create(
617 rewriter, info.initOp->getLoc(), newType, dynamicDims);
618
619 newOperands.push_back(initValue);
620 }
621
622 auto cacheValue = body->addArgument(newType, info.pushOp->getLoc());
623
624 {
625 OpBuilder::InsertionGuard guard(rewriter);
626 rewriter.setInsertionPoint(info.pushOp);
627
628 Value newCacheValue;
629 if (auto TT = dyn_cast<TensorType>(info.cachedType())) {
630 auto shape = TT.getShape();
631
632 SmallVector<int64_t> offsets(shape.size() + 1, 0);
633 offsets[0] = ShapedType::kDynamic;
634
635 SmallVector<int64_t> sizes;
636 sizes.reserve(shape.size() + 1);
637 sizes.push_back(1);
638 sizes.append(shape.begin(), shape.end());
639
640 SmallVector<int64_t> strides(shape.size() + 1, 1);
641
642 newCacheValue = tensor::InsertSliceOp::create(
643 rewriter, info.pushOp->getLoc(), info.pushOp.getValue(),
644 cacheValue, inductionVariable, ValueRange(), ValueRange(),
645 rewriter.getDenseI64ArrayAttr(offsets),
646 rewriter.getDenseI64ArrayAttr(sizes),
647 rewriter.getDenseI64ArrayAttr(strides));
648 } else {
649 newCacheValue = tensor::InsertOp::create(
650 rewriter, info.pushOp->getLoc(), info.pushOp.getValue(),
651 cacheValue, inductionVariable);
652 }
653
654 term->insertOperands(term->getNumOperands(),
655 ValueRange(newCacheValue));
656
657 numNewValuePushes++;
658 }
659 } else if (cacheType == LoopCacheType::MEMREF) {
660 Value initValue;
661 {
662 OpBuilder::InsertionGuard guard(rewriter);
663 rewriter.setInsertionPoint(forOp);
664 if (allocOp) {
665 initValue = allocOp.allocate(rewriter, info.initOp->getLoc(),
666 newType, dynamicDims);
667 } else {
668 initValue =
669 memref::AllocOp::create(rewriter, info.initOp->getLoc(),
670 cast<MemRefType>(newType), dynamicDims);
671 }
672 newPushValues.push_back(initValue);
673 }
674
675 {
676 OpBuilder::InsertionGuard guard(rewriter);
677 rewriter.setInsertionPoint(info.pushOp);
678
679 auto MT = dyn_cast_or_null<MemRefType>(cachedShapedTy);
680 if (multiDim && MT) {
681 auto memref = info.pushOp.getValue();
682 auto shape = MT.getShape();
683
684 SmallVector<int64_t> offsets(newShape.size(), 0);
685 SmallVector<int64_t> sizes;
686 for (auto [i, _] : llvm::enumerate(inductionVariable)) {
687 offsets[i] = ShapedType::kDynamic;
688 sizes.push_back(1);
689 }
690
691 SmallVector<Value> dynSizes;
692 for (size_t i = numLoopDynamicDims; i < dynamicDims.size(); ++i) {
693 dynSizes.push_back(dynamicDims[i]);
694 }
695
696 sizes.append(shape.begin(), shape.end());
697
698 SmallVector<int64_t> strides(newShape.size(), 1);
699
700 auto RT = memref::SubViewOp::inferRankReducedResultType(
701 MT.getShape(), cast<MemRefType>(initValue.getType()), offsets,
702 sizes, strides);
703
704 rewriter.setInsertionPointAfterValue(memref);
705 rewriter.replaceAllUsesWith(
706 memref,
707 memref::SubViewOp::create(
708 rewriter, memref.getLoc(), RT, initValue,
709 /*offsets*/ inductionVariable,
710 /*sizes*/ dynSizes,
711 /*strides*/ ValueRange(),
712 /*static_offsets*/ rewriter.getDenseI64ArrayAttr(offsets),
713 /*static_sizes*/ rewriter.getDenseI64ArrayAttr(sizes),
714 /*static_strides*/ rewriter.getDenseI64ArrayAttr(strides))
715 .getResult());
716
717 if (auto defOp = memref.getDefiningOp())
718 rewriter.eraseOp(defOp);
719 } else {
720 if (dynamicDims.empty()) {
721 memref::StoreOp::create(rewriter, info.pushOp->getLoc(),
722 info.pushOp.getValue(), initValue,
723 inductionVariable);
724 } else {
725 auto memrefType = cast<MemRefType>(initValue.getType());
726 enzyme::StoreOp::create(rewriter, info.pushOp.getLoc(),
727 info.pushOp.getValue(), initValue,
728 inductionVariable, dynamicDims,
729 memrefType.getShape());
730 }
731 }
732 }
733 }
734 }
735
736 // Replace the reverse pass loop
737 auto numInitArgs = FinalClass::getInits(forOp).size();
738 rewriter.setInsertionPoint(forOp);
739
740 forOp = FinalClass::replaceWithNewOperands(rewriter, forOp, newOperands);
741 if (cacheType == LoopCacheType::TENSOR) {
742 for (size_t i = 0; i < numNewValuePushes; ++i)
743 newPushValues.push_back(
744 forOp->getResult(forOp->getNumResults() - numNewValuePushes + i));
745 }
746
747 rewriter.setInsertionPointAfter(forOp);
748
749 unsigned resultIdx = numInitArgs;
750 for (auto grad : updatedGradients) {
751 // set the updated gradient after the new for op.
752
753 Value incoming = forOp->getResult(resultIdx);
754 Value outgoing;
755 if (!postAdd) {
756 outgoing = incoming;
757 } else {
758 auto T = cast<AutoDiffTypeInterface>(incoming.getType());
759 Value current = enzyme::GetOp::create(rewriter, grad.getLoc(), T, grad);
760 outgoing = T.createAddOp(rewriter, grad.getLoc(), incoming, current);
761 }
762 enzyme::SetOp::create(rewriter, grad.getLoc(), grad, outgoing);
763 ++resultIdx;
764 }
765
766 int pushedValueIdx = 0;
767
768 if (caches.size()) {
769 if (otherInductionVariable.size()) {
770 rewriter.restoreInsertionPoint(revIP);
771 } else
772 rewriter.setInsertionPointToStart(otherForOp.getBody());
773 }
774 for (auto &info : caches) {
775 if (mincut)
776 assert(
777 forOp.getRegion().isAncestor(info.pushedValue().getParentRegion()));
778
779 Value cache = info.initOp.getResult();
780
781 // The reverse iteration count may not be known at this point, as it may
782 // be cached via a push/pop, use the fwd count in that case.
783 if (!revNumIters.size()) {
784 OpBuilder::InsertionGuard guard(rewriter);
785 rewriter.setInsertionPoint(otherForOp);
786 revNumIters = FinalClass::getDimensionBounds(rewriter, otherForOp);
787 for (auto &&[rev, fwd] : llvm::zip_equal(revNumIters, fwdNumIters)) {
788 if (!fwd.vval && rev.vval) {
789 rev.vval = nullptr;
790 rev.ival = fwd.ival;
791 }
792 }
793 }
794
795 // First, try to get canonical vars from looking up directly
796 if (otherInductionVariable.size() && !reversedIndex.size()) {
797 reversedIndex = FinalClass::computeReversedIndices(
798 rewriter, otherForOp, otherInductionVariable, revNumIters);
799 }
800
801 // Otherwise, add a new variable to keep track.
802 if (!otherInductionVariable.size()) {
803 Value zero = arith::ConstantOp::create(rewriter, otherForOp->getLoc(),
804 rewriter.getIndexAttr(0));
805 SmallVector<Value> newOperands =
806 llvm::to_vector(FinalClass::getInits(otherForOp));
807 newOperands.push_back(zero);
808
809 otherInductionVariable = {
810 body->addArgument(zero.getType(), otherForOp->getLoc())};
811 {
812 OpBuilder::InsertionGuard guard(rewriter);
813 rewriter.setInsertionPoint(term);
814
815 auto one = arith::ConstantOp::create(rewriter, forOp->getLoc(),
816 rewriter.getIndexAttr(1));
817 auto newInductionVar = arith::AddIOp::create(
818 rewriter, forOp->getLoc(), otherInductionVariable[0], one);
819 term->insertOperands(term->getNumOperands(),
820 ValueRange(newInductionVar));
821 }
822
823 {
824 OpBuilder::InsertionGuard guard(rewriter);
825 rewriter.setInsertionPoint(otherForOp);
826 otherForOp = FinalClass::replaceWithNewOperands(rewriter, otherForOp,
827 newOperands);
828 }
829
830 reversedIndex = FinalClass::computeReversedIndices(
831 rewriter, otherForOp, otherInductionVariable, revNumIters);
832 }
833 reversedIndex = llvm::map_to_vector(reversedIndex, [&rewriter](Value iv) {
834 return ensureIndexType(iv, rewriter);
835 });
836
837 SmallVector<int64_t> newShape;
838 SmallVector<Value> dynamicDims;
839 for (const auto &dim : revNumIters) {
840 if (dim.vval) {
841 newShape.push_back(mlir::ShapedType::kDynamic);
842 dynamicDims.push_back(ensureIndexType(dim.vval, rewriter));
843 } else {
844 newShape.push_back(dim.ival);
845 }
846 }
847
848 auto ET = info.cachedType();
849 ShapedType NT;
850
851 bool multiDim = false;
852 Attribute memorySpace = nullptr;
853 MultidimensionalAllocInterface allocOp;
854 // See the forward pass: kept in sync with the shape the cache allocation
855 // was actually built with.
856 ShapedType cachedShapedTy;
857 if (auto ST = dyn_cast<ShapedType>(ET)) {
858 if (auto MT = dyn_cast<MemRefType>(ST))
859 memorySpace = MT.getMemorySpace();
860 allocOp =
861 getMultiDimCacheAllocOp(info.pushedValue(), cacheType, ST, forOp);
862 // The cache's element type can have lost the memory space -- what gets
863 // pushed may be a bare pointer-like memref<?xf32> while the allocation
864 // behind it lives in memory space 1. The pushed value is what names
865 // the space, and the forward pass reads it from exactly here; both
866 // sides must agree, or the reverse-pass subviews slice an allocation
867 // whose memory space their result type does not carry.
868 if (allocOp)
869 if (auto MT = dyn_cast<MemRefType>(info.pushedValue().getType()))
870 memorySpace = MT.getMemorySpace();
871 ST = refineCacheShapeFromAlloc(ST, allocOp);
872 cachedShapedTy = ST;
873 bool fullyStatic = llvm::all_of(ST.getShape(), [](int64_t dim) {
874 return dim != ShapedType::kDynamic;
875 });
876 multiDim =
877 allocOp || (fullyStatic && cacheType == LoopCacheType::TENSOR);
878
879 if (multiDim) {
880 newShape.append(ST.getShape().begin(), ST.getShape().end());
881 ET = ST.getElementType();
882 }
883 }
884
885 if (!memorySpace) {
886 if (auto innerMT = dyn_cast<MemRefType>(ET))
887 memorySpace = innerMT.getMemorySpace();
888 }
889
890 auto newType = cacheType == LoopCacheType::TENSOR
891 ? cast<ShapedType>(RankedTensorType::get(newShape, ET))
892 : cast<ShapedType>(MemRefType::get(newShape, ET));
893 if (memorySpace)
894 newType = cast<ShapedType>(cast<MemRefType>(newType)
895 .clonePtrWith(memorySpace, std::nullopt)
896 .value());
897 enzyme::InitOp newInit = ({
898 OpBuilder::InsertionGuard guard(rewriter);
899 rewriter.setInsertionPoint(info.initOp);
900
901 enzyme::InitOp::create(
902 rewriter, info.initOp->getLoc(),
903 enzyme::CacheType::get(cache.getContext(), newType));
904 });
905 info.pushOp = ({
906 OpBuilder::InsertionGuard guard(rewriter);
907 rewriter.setInsertionPointAfter(forOp);
908 auto newPush = enzyme::PushOp::create(rewriter, cache.getLoc(),
909 newInit.getResult(),
910 newPushValues[pushedValueIdx]);
911 rewriter.eraseOp(info.pushOp);
912 newPush;
913 });
914
915 pushedValueIdx++;
916
917 OpBuilder::InsertionGuard guard(rewriter);
918
919 rewriter.setInsertionPoint(otherForOp);
920
921 auto popNewValue = enzyme::PopOp::create(rewriter, info.popOp->getLoc(),
922 newType, newInit.getResult());
923
924 rewriter.setInsertionPoint(info.popOp);
925
926 Value popValue;
927 if (cacheType == LoopCacheType::TENSOR) {
928 if (auto TT = dyn_cast<TensorType>(info.cachedType())) {
929 auto shape = TT.getShape();
930 SmallVector<int64_t> offsets(shape.size() + 1, 0);
931 offsets[0] = ShapedType::kDynamic;
932
933 SmallVector<int64_t> sizes;
934 sizes.reserve(shape.size() + 1);
935 sizes.push_back(1);
936 sizes.append(shape.begin(), shape.end());
937
938 SmallVector<int64_t> strides(shape.size() + 1, 1);
939
940 popValue = tensor::ExtractSliceOp::create(
941 rewriter, info.popOp->getLoc(), TT, popNewValue,
942 reversedIndex, ValueRange(), ValueRange(),
943 rewriter.getDenseI64ArrayAttr(offsets),
944 rewriter.getDenseI64ArrayAttr(sizes),
945 rewriter.getDenseI64ArrayAttr(strides))
946 .getResult();
947 } else {
948 popValue = tensor::ExtractOp::create(rewriter, info.popOp->getLoc(),
949 popNewValue, reversedIndex)
950 .getResult();
951 }
952 } else if (cacheType == LoopCacheType::MEMREF) {
953
954 auto MT = dyn_cast_or_null<MemRefType>(cachedShapedTy);
955 if (multiDim && MT) {
956 auto shape = MT.getShape();
957
958 SmallVector<int64_t> offsets(newShape.size(), 0);
959 SmallVector<int64_t> sizes;
960 for (auto [i, _] : llvm::enumerate(reversedIndex)) {
961 offsets[i] = ShapedType::kDynamic;
962 sizes.push_back(1);
963 }
964
965 sizes.append(shape.begin(), shape.end());
966
967 SmallVector<Value> dynSizes;
968 for (size_t i = reversedIndex.size(); i < newShape.size(); ++i) {
969 // we use memref.dim here to know the size, hopefully further
970 // optimization/canonicalizations can just forward the right size
971 // here.
972 if (newShape[i] == ShapedType::kDynamic)
973 dynSizes.push_back(memref::DimOp::create(
974 rewriter, popNewValue.getLoc(), popNewValue,
975 arith::ConstantIndexOp::create(rewriter, popNewValue.getLoc(),
976 i)));
977 }
978
979 SmallVector<int64_t> strides(newShape.size(), 1);
980
981 auto RT = memref::SubViewOp::inferRankReducedResultType(
982 MT.getShape(), cast<MemRefType>(popNewValue.getType()), offsets,
983 sizes, strides);
984
985 popValue = memref::SubViewOp::create(
986 rewriter, info.popOp->getLoc(), RT, popNewValue,
987 /*offsets*/ reversedIndex,
988 /*sizes*/ dynSizes,
989 /*strides*/ ValueRange(),
990 /*static_offsets*/ rewriter.getDenseI64ArrayAttr(offsets),
991 /*static_sizes*/ rewriter.getDenseI64ArrayAttr(sizes),
992 /*static_strides*/ rewriter.getDenseI64ArrayAttr(strides));
993
994 for (auto user :
995 llvm::make_early_inc_range(info.popOp.getResult().getUsers())) {
996 if (allocOp ? allocOp.isDeallocation(user)
997 : isa<memref::DeallocOp>(user))
998 rewriter.eraseOp(user);
999 }
1000 } else {
1001 if (dynamicDims.empty()) {
1002 popValue = memref::LoadOp::create(rewriter, info.popOp->getLoc(),
1003 popNewValue, reversedIndex);
1004 } else {
1005 auto memrefType = cast<MemRefType>(popNewValue.getType());
1006 popValue = enzyme::LoadOp::create(
1007 rewriter, info.popOp.getLoc(),
1008 cast<MemRefType>(popNewValue.getType()).getElementType(),
1009 popNewValue.getResult(), reversedIndex, dynamicDims,
1010 memrefType.getShape());
1011 }
1012 }
1013
1014 // this memref was allocated on push, dealloc it
1015 rewriter.setInsertionPointAfter(otherForOp);
1016 if (allocOp) {
1017 allocOp.deallocate(rewriter, info.initOp->getLoc(), popNewValue);
1018 } else {
1019 memref::DeallocOp::create(rewriter, info.initOp->getLoc(),
1020 popNewValue);
1021 }
1022 }
1023
1024 rewriter.replaceAllUsesWith(info.popOp.getResult(), popValue);
1025 rewriter.eraseOp(info.popOp);
1026 }
1027
1028 return success();
1029 }
1030};
1031
1032// All values defined in fwd should have no use outside this block
1033// therefore we can localize their differential to only the rev block in order
1034// to simplify the work of the remove-unnecessary-enzyme-ops pass.
1035//
1036// The builder insertion point should be at the start of the corresponding rev
1037// block.
1038void localizeGradients(OpBuilder &builder, MGradientUtilsReverse *gutils,
1039 Block *fwd);
1040
1041void removalBlockExplore(Block *block, IRMapping &mapping,
1042 PatternRewriter &rewriter,
1043 llvm::SetVector<Value> &gradients,
1044 llvm::MapVector<Value, CacheInfo> &caches);
1045
1046// ReverseOpName is the type of the if-like op generated for the reverse pass
1047// corresponding to this OpName. It usually is the same as OpName, but need
1048// not be (e.g. the reverse of an affine.if is an scf.if).
1049template <typename FinalClass, typename OpName, typename ReverseOpName = OpName>
1051 : public EnzymeOpsRemoverOpInterface::ExternalModel<FinalClass, OpName> {
1052 LogicalResult removeEnzymeOps(Operation *op,
1053 PatternRewriter &rewriter) const {
1054 auto ifOp = cast<OpName>(op);
1055 // Gradients:
1056 //
1057 // For each set in a branch, we instead set after the if by using the
1058 // return value.
1059 //
1060 // if %pred {
1061 // enzyme.set %grad, %2
1062 // } else {
1063 // }
1064 //
1065 // %0 = enzyme.get %grad
1066 // %1 = if %pred {
1067 // return %2
1068 // } else {
1069 // return %0
1070 // }
1071 // enzyme.set %grad, %1
1072 //
1073 // For each get in a branch, we get before and use that instead of the
1074 // get.
1075
1076 // Caches:
1077 //
1078 // For each push, push after the if instead add a dummy value in the other
1079 // branch.
1080 //
1081 // For each pop in the reverse if, pop before the if instead of inside a
1082 // branch.
1083
1084 Block *trueBlock = FinalClass::getThenBlock(ifOp, rewriter),
1085 *falseBlock = FinalClass::getElseBlock(ifOp, rewriter);
1086
1087 // Gradients whose value is set in either branches.
1088 llvm::SetVector<Value> gradients;
1089
1090 // We assume pushes are exclusive.
1091 llvm::MapVector<Value, CacheInfo> truePushedCaches, falsePushedCaches;
1092
1093 // Grad to value
1094 IRMapping trueMapping, falseMapping;
1095
1096 removalBlockExplore(trueBlock, trueMapping, rewriter, gradients,
1097 truePushedCaches);
1098 removalBlockExplore(falseBlock, falseMapping, rewriter, gradients,
1099 falsePushedCaches);
1100
1101 bool cachesEmpty = truePushedCaches.empty() && falsePushedCaches.empty();
1102 if (gradients.empty() && cachesEmpty)
1103 return success();
1104 bool removeCaches = !op->hasAttr(kPreserveCacheAttrName) && !cachesEmpty;
1105 SmallVector<CacheInfo> trueCaches = llvm::map_to_vector(
1106 truePushedCaches, [](auto pair) { return std::get<1>(pair); });
1107 SmallVector<CacheInfo> falseCaches = llvm::map_to_vector(
1108 falsePushedCaches, [](auto pair) { return std::get<1>(pair); });
1109
1110 if (removeCaches && hasMinCut(ifOp)) {
1111 // Find the reverse if op
1112 ReverseOpName reverseIfOp = nullptr;
1113 auto findReverseIf = [&](Operation *parent) {
1114 if (isa<ReverseOpName>(parent) &&
1115 (reverseIfOp == nullptr || parent->isProperAncestor(reverseIfOp))) {
1116 reverseIfOp = cast<ReverseOpName>(parent);
1117 }
1118 };
1119 for (auto &[_, info] : truePushedCaches)
1120 findReverseIf(info.popOp->getParentOp());
1121 for (auto &[_, info] : falsePushedCaches)
1122 findReverseIf(info.popOp->getParentOp());
1123
1124 assert(reverseIfOp && "unable to find reverse if op");
1125
1126 Block *reverseTrueBlock = FinalClass::getThenBlock(reverseIfOp, rewriter),
1127 *reverseFalseBlock =
1128 FinalClass::getElseBlock(reverseIfOp, rewriter);
1129
1130 OpBuilder::InsertionGuard guard(rewriter);
1131 rewriter.setInsertionPointToStart(reverseTrueBlock);
1132 enzyme::minCutCache(trueBlock, reverseTrueBlock, trueCaches, rewriter,
1133 trueMapping);
1134 rewriter.setInsertionPointToStart(reverseFalseBlock);
1135 enzyme::minCutCache(falseBlock, reverseFalseBlock, falseCaches, rewriter,
1136 falseMapping);
1137 }
1138
1139 Operation *trueTerm = trueBlock->getTerminator();
1140 Operation *falseTerm = falseBlock->getTerminator();
1141
1142 for (auto grad : gradients) {
1143 auto trueValue = trueMapping.lookupOrNull(grad);
1144 if (!trueValue) {
1145 trueValue = enzyme::GetOp::create(
1146 rewriter, grad.getLoc(),
1147 cast<enzyme::GradientType>(grad.getType()).getBasetype(), grad);
1148 }
1149 trueTerm->insertOperands(trueTerm->getNumOperands(),
1150 ValueRange(trueValue));
1151
1152 auto falseValue = falseMapping.lookupOrNull(grad);
1153 if (!falseValue) {
1154 falseValue = enzyme::GetOp::create(
1155 rewriter, grad.getLoc(),
1156 cast<enzyme::GradientType>(grad.getType()).getBasetype(), grad);
1157 }
1158 falseTerm->insertOperands(falseTerm->getNumOperands(),
1159 ValueRange(falseValue));
1160 }
1161
1162 SmallVector<CacheInfo> allCaches;
1163 allCaches.append(trueCaches);
1164 allCaches.append(falseCaches);
1165 if (removeCaches) {
1166 for (auto &info : allCaches) {
1167 Value pushedValue = info.pushedValue();
1168 Value dummy = FinalClass::getDummyValue(rewriter, pushedValue.getLoc(),
1169 pushedValue.getType());
1170
1171 Value trueValue =
1172 pushedValue.getParentBlock() == trueBlock ? pushedValue : dummy;
1173 Value falseValue =
1174 pushedValue.getParentBlock() == falseBlock ? pushedValue : dummy;
1175
1176 trueTerm->insertOperands(trueTerm->getNumOperands(),
1177 ValueRange(trueValue));
1178 falseTerm->insertOperands(falseTerm->getNumOperands(),
1179 ValueRange(falseValue));
1180 }
1181 }
1182
1183 size_t idx = ifOp->getNumResults();
1184 ifOp = FinalClass::replace(rewriter, ifOp, trueTerm->getOperandTypes());
1185
1186 for (auto grad : gradients) {
1187 enzyme::SetOp::create(rewriter, grad.getLoc(), grad,
1188 ifOp->getResult(idx));
1189 idx++;
1190 }
1191
1192 if (removeCaches) {
1193 for (auto &info : allCaches) {
1194 enzyme::PushOp::create(rewriter, info.pushOp->getLoc(),
1195 info.initOp.getResult(), ifOp->getResult(idx));
1196 rewriter.eraseOp(info.pushOp);
1197
1198 OpBuilder::InsertionGuard guard(rewriter);
1199 rewriter.setInsertionPoint(info.popOp->getParentOp());
1200
1201 auto newPop = enzyme::PopOp::create(rewriter, info.popOp->getLoc(),
1202 info.popOp.getResult().getType(),
1203 info.popOp.getCache());
1204 rewriter.replaceAllUsesWith(info.popOp.getResult(), newPop);
1205 rewriter.eraseOp(info.popOp);
1206
1207 idx++;
1208 }
1209 }
1210
1211 return success();
1212 }
1213};
1214
1215} // namespace enzyme
1216} // namespace mlir
#define getOp(vtmp)
void removalBlockExplore(Block *block, IRMapping &mapping, PatternRewriter &rewriter, llvm::SetVector< Value > &gradients, llvm::MapVector< Value, CacheInfo > &caches)
void localizeGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *fwd)
static ShapedType refineCacheShapeFromAlloc(ShapedType ST, MultidimensionalAllocInterface allocOp)
The pushed value may reach its allocation through casts that erase static extents: canonicalization r...
static Value traceToNonBlockArgSource(Value value)
static constexpr llvm::StringLiteral kPreserveCacheAttrName
void minCutCache(Block *forward, Block *reverse, SmallVector< CacheInfo > &caches, PatternRewriter &rewriter, const IRMapping &fwdrevmap, Operation *lastFwd=nullptr)
static bool hasLICM(Operation *op)
static MultidimensionalAllocInterface getMultiDimCacheAllocOp(Value pushedValue, LoopCacheType cacheType, ShapedType shapeTy, Operation *forOp)
static bool hasMinCut(Operation *op)
void loopInvariantCache(SmallVectorImpl< CacheInfo > &caches, LoopLikeOpInterface fwdLoop, LoopLikeOpInterface revLoop, const IRMapping &mapping, int64_t threshold=16)
static LoopCacheType getCacheType(Operation *op)
static Value ensureIndexType(Value value, OpBuilder &builder)
Information about a cache, each cache init should have one corresponding push and pop.
CacheInfo merge(CacheInfo other, PatternRewriter &rewriter)
enzyme::InitOp initOp
enzyme::PushOp pushOp
static bool Equivalent(Value lhs, Value rhs)
static llvm::SmallVector< mlir::Value > computeReversedIndices(PatternRewriter &rewriter, OpName op, llvm::ArrayRef< mlir::Value > otherInductionVariable, llvm::ArrayRef< IntOrValue > bounds)
LogicalResult removeEnzymeOps(Operation *op, PatternRewriter &rewriter) const
LogicalResult removeEnzymeOps(Operation *op, PatternRewriter &rewriter) const