Enzyme main
Loading...
Searching...
No Matches
LoopCheckpointing.h
Go to the documentation of this file.
1//===- LoopCheckpointing.h - Generic loop checkpointing --------*- 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//
9// Op generic implementation of checkpointing.
10//
11//===----------------------------------------------------------------------===//
12
13#ifndef ENZYME_MLIR_IMPLEMENTATIONS_LOOPCHECKPOINTING_H
14#define ENZYME_MLIR_IMPLEMENTATIONS_LOOPCHECKPOINTING_H
15
16#include "Dialect/Ops.h"
19#include "Passes/RemovalUtils.h"
20#include "mlir/Dialect/Arith/IR/Arith.h"
21#include "mlir/Dialect/MemRef/IR/MemRef.h"
22#include "mlir/Dialect/SCF/IR/SCF.h"
23#include "mlir/IR/IRMapping.h"
24#include "mlir/Transforms/RegionUtils.h"
25#include "llvm/ADT/STLExtras.h"
26#include "llvm/ADT/ScopeExit.h"
27
28#include <array>
29#include <cmath>
30#include <functional>
31#include <optional>
32#include <utility>
33
34namespace mlir {
35namespace enzyme {
36
37template <typename FinalClass, typename OpName> struct LoopCheckpointing {
38 // How the trip count is decomposed for periodic checkpointing: `nOuter`
39 // segments of `nInner` iterations, plus a shorter trailing segment of
40 // `trailingIters`.
41 //
42 // A stated period budgets the *outer* half of that: it is the number of
43 // segments, hence of checkpoints, hence both the trip count of the scaffold's
44 // outer loops and the size of the one allocation that outlives the forward
45 // pass. The segment length is what is derived from it, as
46 // ceil(numIters / nOuter). Reading the period as the segment length instead
47 // would leave the checkpoint storage growing with the trip count --
48 // ceil(numIters / period) live checkpoints -- which is the opposite of what
49 // stating a budget is for.
50 //
51 // Every dialect's outer loops therefore count segments, one iteration per
52 // checkpoint, and derive the segment's base iteration as nInner * index.
54 // Iterations per full segment, or -1 when only `nInnerV` knows it.
55 int64_t nInner = 0;
56 // Number of full segments. Always known at compile time.
57 int64_t nOuter = -1;
58 // Iterations in the short trailing segment, 0 when the full segments
59 // divide the trip count evenly -- and always 0 for a dynamic trip count,
60 // where every segment is instead clamped against the trip count at
61 // runtime. That clamp covers both the short segment and the empty ones
62 // that a rounded-up segment length can leave at the end.
63 int64_t trailingIters = 0;
64
65 // Non-null exactly when the trip count is dynamic. numIters/start/step are
66 // materialized before the scaffold (and cached for the reverse pass);
67 // nOuterV is the segment count as a value, which only the dynamic schedule
68 // needs (a static one has numSegments() for that).
70
71 bool isDynamic() const { return numItersV != nullptr; }
72 bool hasTrailing() const { return trailingIters > 0; }
73 // Total number of segments, the trailing one included.
74 int64_t numSegments() const { return nOuter + hasTrailing(); }
75 };
76
77 // Names of the attributes the checkpointing directives are read from. A
78 // downstream project differentiating its own loop op spells them with its
79 // own prefix (Enzyme-JAX uses `enzymexla.*`), so every access below goes
80 // through these three hooks rather than a literal.
81 static StringRef enableCheckpointingAttrName() {
82 return "enzyme.enable_checkpointing";
83 }
84
85 static StringRef binomialCheckpointingAttrName() {
86 return "enzyme.binomial_checkpointing";
87 }
88
89 // Both schemes read the period as a budget on the number of live
90 // checkpoints: the size of the binomial checkpoint table, or the number of
91 // segments a periodic decomposition is cut into. See PeriodicSchedule.
92 static StringRef checkpointPeriodAttrName() {
93 return "enzyme.checkpoint_period";
94 }
95
96 // A loop created while differentiating `oldOp` is still doing that loop's
97 // work, so it inherits what was set on it. The checkpointing directives are
98 // left behind, since the rewrite has already acted on them.
99 static void preserveAttributesButCheckpointing(Operation *newOp,
100 Operation *oldOp) {
101 for (auto attr : oldOp->getDiscardableAttrs()) {
102 auto name = attr.getName();
103 if (name != FinalClass::enableCheckpointingAttrName() &&
104 name != FinalClass::binomialCheckpointingAttrName() &&
105 name != FinalClass::checkpointPeriodAttrName())
106 newOp->setAttr(name, attr.getValue());
107 }
108 }
109
110 static bool hasBinomialAttr(OpName forOp) {
111 return forOp->hasAttr(FinalClass::binomialCheckpointingAttrName());
112 }
113
114 static bool checkpointingEnabled(OpName forOp) {
115 auto a = forOp->template getAttrOfType<BoolAttr>(
116 FinalClass::enableCheckpointingAttrName());
117 return a && a.getValue();
118 }
119
120 // Whether periodic checkpointing can be built for a loop whose trip count is
121 // only known at runtime. Dialects whose scaffold bounds must be compile-time
122 // constants (affine.for, whose bounds are AffineMaps) say no and fall back to
123 // the plain reverse path, exactly as they did before dynamic support existed.
124 static bool supportsDynamicPeriodic() { return true; }
125
126 static bool needsCheckpointing(OpName forOp) {
127 if (!FinalClass::checkpointingEnabled(forOp) ||
128 FinalClass::hasBinomialAttr(forOp))
129 return false;
130 if (FinalClass::getConstantNumberOfIterations(forOp).has_value())
131 return true;
132 // A dynamic trip count has no compile-time N to take the square root of,
133 // so the period cannot be defaulted: it has to be stated, and (being the
134 // segment count, which the scaffold divides by) it has to be positive.
135 if (!FinalClass::supportsDynamicPeriodic())
136 return false;
137 auto period = FinalClass::getCheckpointBudget(forOp);
138 return period.has_value() && *period > 0;
139 }
140
141 static bool needsBinomialCheckpointing(OpName forOp) {
142 return FinalClass::checkpointingEnabled(forOp) &&
143 FinalClass::hasBinomialAttr(forOp);
144 }
145
146 static std::optional<int64_t> getCheckpointBudget(OpName forOp) {
147 if (auto a = forOp->template getAttrOfType<IntegerAttr>(
148 FinalClass::checkpointPeriodAttrName()))
149 return a.getInt();
150 return std::nullopt;
151 }
152
153 // The "init" operands used as initialization values for the loop's
154 // iter_args. No FinalClass hook needed: scf::ForOp exposes this correctly
155 // through LoopLikeOpInterface (it overrides getInitsMutable), and
156 // affine::AffineForOp exposes its own `$inits`-derived accessor of the
157 // same name, which -- being declared directly on the op -- takes priority
158 // over (and happens to be the one actually wired up, unlike) the
159 // interface's own default. Both resolve correctly when called directly on
160 // a concrete OpName.
161 static ValueRange getInits(OpName forOp) { return forOp.getInits(); }
162
163 static Value getInductionVar(OpName forOp) { return forOp.getInductionVar(); }
164
165 // The op's body block. Spelling differs across dialects (scf.for and
166 // affine.for hand out the block, stablehlo.while a whole region), and every
167 // scaffold loop the periodic hooks build is itself an OpName, so this is the
168 // one accessor the shared code uses.
169 static Block *getBodyBlock(OpName forOp) { return forOp.getBody(); }
170
171 // Move `builder` to where the next op of `block`'s body belongs. scf.for and
172 // affine.for hand out a body whose terminator the scaffold has yet to create,
173 // so that is simply the end of the block; an op whose builder pre-creates the
174 // terminator (stablehlo.while) has to insert before it instead.
175 static void setInsertionPointToBodyEnd(OpBuilder &builder, Block *block) {
176 builder.setInsertionPointToEnd(block);
177 }
178
179 static size_t getNumRegionIterArgs(OpName forOp) {
180 return forOp.getNumRegionIterArgs();
181 }
182
183 // The loop-carried subset of a body terminator's operands, in iter-arg
184 // order. An op whose terminator also yields the next induction variable
185 // (stablehlo.while) drops it here, so that the shared code can zip these
186 // against the iter args.
187 static OperandRange getCarriedTerminatorOperands(Operation *term) {
188 return term->getOperands();
189 }
190
191 // The loop-carried subset of a scaffold loop's results, in iter-arg order.
192 // Same asymmetry as above: an op that carries its induction variable as a
193 // regular operand also returns it as a regular result.
194 static ResultRange getCarriedResults(Operation *loop) {
195 return loop->getResults();
196 }
197
198 //===--------------------------------------------------------------------===//
199 // Scalar ("index-like") arithmetic. Everything the scaffold computes for
200 // itself -- step counters, segment bounds, checkpoint slot numbers -- is
201 // built out of these, so that a dialect which does not count on `index`
202 // (stablehlo counts in tensor<i64>) can supply its own primitives.
203 //===--------------------------------------------------------------------===//
204
205 static Type getIndexLikeType(OpBuilder &builder) {
206 return builder.getIndexType();
207 }
208
209 static Value emitConst(OpBuilder &b, Location loc, int64_t v) {
210 return arith::ConstantIndexOp::create(b, loc, v);
211 }
212
213 static Value emitAdd(OpBuilder &b, Location loc, Value l, Value r) {
214 return arith::AddIOp::create(b, loc, l, r);
215 }
216
217 static Value emitSub(OpBuilder &b, Location loc, Value l, Value r) {
218 return arith::SubIOp::create(b, loc, l, r);
219 }
220
221 static Value emitMul(OpBuilder &b, Location loc, Value l, Value r) {
222 return arith::MulIOp::create(b, loc, l, r);
223 }
224
225 static Value emitDivU(OpBuilder &b, Location loc, Value l, Value r) {
226 return arith::DivUIOp::create(b, loc, l, r);
227 }
228
229 static Value emitMin(OpBuilder &b, Location loc, Value l, Value r) {
230 return arith::MinUIOp::create(b, loc, l, r);
231 }
232
233 static Value getNumIterationsValue(OpBuilder &builder, Location loc,
234 OpName forOp,
235 MGradientUtilsReverse *gutils) {
236 Value lb = FinalClass::materializeLowerBound(builder, loc, forOp, gutils);
237 Value ub = FinalClass::materializeUpperBound(builder, loc, forOp, gutils);
238 Value step = FinalClass::materializeStep(builder, loc, forOp, gutils);
239 Value diff = FinalClass::emitSub(builder, loc, ub, lb);
240 return FinalClass::emitDivU(builder, loc, diff, step);
241 }
242
243 static Value castToType(OpBuilder &builder, Location loc, Value v,
244 Type targetType) {
245 if (v.getType() == targetType)
246 return v;
247 assert(targetType.isIndex() || v.getType().isIndex());
248 return arith::IndexCastOp::create(builder, loc, targetType, v);
249 }
250
251 static MemRefType checkpointBufferType(int64_t budget, Type t) {
252 if (auto mt = dyn_cast<MemRefType>(t)) {
253 SmallVector<int64_t> shape;
254 shape.push_back(budget);
255 shape.append(mt.getShape().begin(), mt.getShape().end());
256 return MemRefType::get(shape, mt.getElementType(),
257 MemRefLayoutAttrInterface{}, mt.getMemorySpace());
258 }
259 return MemRefType::get({budget}, t);
260 }
261
262 static Value checkpointRow(OpBuilder &b, Location loc, Value buf, Value slot,
263 MemRefType rowTy) {
264 auto bufTy = cast<MemRefType>(buf.getType());
265 SmallVector<OpFoldResult> offsets, sizes, strides;
266 offsets.push_back(slot);
267 sizes.push_back(b.getIndexAttr(1));
268 strides.push_back(b.getIndexAttr(1));
269 for (int64_t i = 0, e = rowTy.getRank(); i < e; ++i) {
270 offsets.push_back(b.getIndexAttr(0));
271 if (rowTy.isDynamicDim(i))
272 sizes.push_back(memref::DimOp::create(b, loc, buf, i + 1).getResult());
273 else
274 sizes.push_back(b.getIndexAttr(rowTy.getDimSize(i)));
275 strides.push_back(b.getIndexAttr(1));
276 }
277 auto resTy = memref::SubViewOp::inferRankReducedResultType(
278 rowTy.getShape(), bufTy, offsets, sizes, strides);
279 return memref::SubViewOp::create(b, loc, cast<MemRefType>(resTy), buf,
280 offsets, sizes, strides);
281 }
282
283 static void storeCheckpoint(OpBuilder &b, Location loc, Value buf, Value slot,
284 Value val) {
285 if (auto mt = dyn_cast<MemRefType>(val.getType())) {
286 Value row = checkpointRow(b, loc, buf, slot, mt);
287 memref::CopyOp::create(b, loc, val, row);
288 } else {
289 memref::StoreOp::create(b, loc, val, buf, ValueRange{slot});
290 }
291 }
292
293 // Read a snapshot from checkpoint buffer slot `slot`. For scalars returns
294 // the loaded value; for memrefs returns a fresh alloc initialized from the
295 // row (the caller is responsible for deallocating it).
296 static Value loadCheckpoint(OpBuilder &b, Location loc, Value buf, Value slot,
297 Type valTy) {
298 if (auto mt = dyn_cast<MemRefType>(valTy)) {
299 Value row = checkpointRow(b, loc, buf, slot, mt);
300 SmallVector<Value> dynSizes;
301 for (int64_t i = 0, e = mt.getRank(); i < e; ++i)
302 if (mt.isDynamicDim(i))
303 dynSizes.push_back(memref::DimOp::create(b, loc, row, i));
304 Value fresh = memref::AllocOp::create(b, loc, mt, dynSizes);
305 memref::CopyOp::create(b, loc, row, fresh);
306 return fresh;
307 }
308 return memref::LoadOp::create(b, loc, buf, ValueRange{slot});
309 }
310
311 // Whether copyBlockWithoutTerminator also publishes the caller's seed
312 // mappings -- the values the copied block reads from above, mapped to popped
313 // caches or to the per-segment clone that replays them -- into
314 // gutils->originalToNewFn, alongside the values the block itself defines.
315 //
316 // The memref dialects need them published: a load's reverse rule looking up
317 // getNewFromOriginal() of a memref read from above must see the clone this
318 // replay writes through, not the caller's. Dialects whose outside refs are
319 // immutable values instead keep them private, so that the ops visited after
320 // the loop (in reverse order) still see the caller's own value.
321 static bool publishesCopiedSeeds() { return true; }
322
323 // True when `v` is defined inside `b` (directly or in a nested region),
324 // i.e. it is one of the values the copied block itself brings into being
325 // rather than one of the caller's seeds.
326 static bool isDefinedInBlock(Block *b, Value v) {
327 Block *owner = isa<BlockArgument>(v) ? cast<BlockArgument>(v).getOwner()
328 : v.getDefiningOp()->getBlock();
329 for (; owner; owner = owner->getParentOp()
330 ? owner->getParentOp()->getBlock()
331 : nullptr)
332 if (owner == b)
333 return true;
334 return false;
335 }
336
337 static void copyBlockWithoutTerminator(OpBuilder &builder, Block *b,
338 MGradientUtilsReverse *gutils,
339 IRMapping &mapping) {
340 for (auto &it : b->without_terminator()) {
341 OpBuilder::InsertionGuard g(builder);
342 FinalClass::cloneOp(builder, it, mapping);
343 }
344
345 for (auto [oldVal, newVal] : mapping.getValueMap())
346 if (FinalClass::publishesCopiedSeeds() || isDefinedInBlock(b, oldVal))
347 gutils->originalToNewFn.map(oldVal, newVal);
348
349 for (auto [oldBlock, newBlock] : mapping.getBlockMap()) {
350 gutils->originalToNewFn.map(oldBlock, newBlock);
351 for (auto [oldArg, newArg] :
352 llvm::zip(oldBlock->getArguments(), newBlock->getArguments()))
353 gutils->originalToNewFn.map(oldArg, newArg);
354 }
355
356 for (auto [oldOp, newOp] : mapping.getOperationMap())
357 gutils->originalToNewFnOps[oldOp] = newOp;
358 }
359
360 // Splits the values `forOp` reads from above into the ones holding mutable
361 // memory, which has to be snapshotted for a replay, and the ones that can
362 // just be forwarded. cacheBinomial and reverseBinomial must produce the
363 // same order, which they do: getUsedValuesDefinedAbove is deterministic for
364 // a fixed IR and both walk the *original* forOp.
365 static void splitOutsideRefs(OpName forOp,
366 SmallVectorImpl<Value> &mutableRefs,
367 SmallVectorImpl<Value> &immutableRefs) {
368 SetVector<Value> outsideRefs;
369 getUsedValuesDefinedAbove(forOp->getRegions(), outsideRefs);
370 for (auto ref : outsideRefs) {
371 if (isa<ClonableTypeInterface>(ref.getType()))
372 mutableRefs.push_back(ref);
373 else
374 immutableRefs.push_back(ref);
375 }
376 }
377
378 // Index layout of the `caches` vector handed from cacheBinomial to
379 // reverseBinomial:
380 //
381 // [ ckptBufs (one per iter arg) | idxBuf
382 // | per mutable ref: clone buffer, + shadow if the ref is active
383 // | immutableRefs
384 // | numIters, start, step -- only when the loop bounds are dynamic ]
385 //
386 // The per-ref region has variable width. That used to be re-derived by hand
387 // at each use, with one site testing the predicate on the wrong value;
388 // computing it once here is what keeps the two sides from drifting.
390 size_t numIterArgs = 0;
391 SmallVector<bool> mutableActive;
392 size_t numImmutable = 0;
393 bool isDynamic = false;
394
395 static BinomialCacheLayout get(OpName forOp, ArrayRef<Value> mutableRefs,
396 ArrayRef<Value> immutableRefs,
397 bool isDynamic,
398 MGradientUtilsReverse *gutils) {
400 l.numIterArgs = FinalClass::getNumRegionIterArgs(forOp);
401 for (auto ref : mutableRefs)
402 l.mutableActive.push_back(!gutils->isConstantValue(ref));
403 l.numImmutable = immutableRefs.size();
405 return l;
406 }
407
408 size_t ckptBuf(size_t i) const { return i; }
409 size_t idxBuf() const { return numIterArgs; }
410
411 size_t mutableBegin() const { return numIterArgs + 1; }
412 size_t mutableWidth() const {
413 size_t w = 0;
414 for (bool active : mutableActive)
415 w += 1 + active;
416 return w;
417 }
418 size_t cloneBuf(size_t r) const {
419 size_t idx = mutableBegin();
420 for (size_t i = 0; i < r; ++i)
421 idx += 1 + mutableActive[i];
422 return idx;
423 }
424 size_t shadow(size_t r) const {
425 assert(mutableActive[r] && "inactive ref has no shadow cache");
426 return cloneBuf(r) + 1;
427 }
428 size_t immutable(size_t i) const {
429 return mutableBegin() + mutableWidth() + i;
430 }
431 size_t numIters() const {
432 assert(isDynamic && "bounds are static; not cached");
433 return immutable(numImmutable);
434 }
435 size_t start() const { return numIters() + 1; }
436 size_t step() const { return numIters() + 2; }
437 size_t size() const {
438 return immutable(numImmutable) + (isDynamic ? 3 : 0);
439 }
440 };
441
442 //===--------------------------------------------------------------------===//
443 // Checkpoint storage.
444 //
445 // A store holds `budget` snapshots of one value, indexed by the checkpoint
446 // stack pointer. Two shapes of storage are covered. A mutable buffer has one
447 // identity that every write goes through, so it can be allocated once and
448 // referred to from anywhere (memref). An immutable one has no identity: a
449 // write produces a *new* value, which then has to be carried out of every
450 // loop it was written inside (a tensor). storesAreLoopCarried() picks between
451 // them, and the scheme below threads the store values through its scaffold
452 // loops as a trailing group of iteration arguments when it is set.
453 //===--------------------------------------------------------------------===//
454
455 static bool storesAreLoopCarried() { return false; }
456
457 static Value createStore(OpBuilder &b, Location loc, int64_t budget,
458 Type valTy) {
459 return memref::AllocOp::create(b, loc, checkpointBufferType(budget, valTy));
460 }
461
462 // Returns the store to use from here on: the same one for a buffer, the
463 // updated value for an immutable store.
464 static Value storeSlot(OpBuilder &b, Location loc, Value store, Value slot,
465 Value val) {
466 storeCheckpoint(b, loc, store, slot, val);
467 return store;
468 }
469
470 static Value loadSlot(OpBuilder &b, Location loc, Value store, Value slot,
471 Type valTy) {
472 return loadCheckpoint(b, loc, store, slot, valTy);
473 }
474
475 static void destroyStore(OpBuilder &b, Location loc, Value store) {
476 memref::DeallocOp::create(b, loc, store);
477 }
478
479 //===--------------------------------------------------------------------===//
480 // Scaffold loops -- the scheme's own bookkeeping loops, as opposed to the
481 // replayed copies of the differentiated loop. A counted one has body
482 // arguments [induction variable, carried...]; a while loop carries no
483 // induction variable of its own.
484 //===--------------------------------------------------------------------===//
485
487 Operation *op = nullptr;
488 Block *body = nullptr;
489 // Index of the first carried value among the body's arguments and among
490 // the op's results: an op that models its induction variable as a regular
491 // iteration argument (stablehlo.while) has one of each in front.
492 unsigned firstCarriedArg = 0;
493 unsigned firstCarriedResult = 0;
494
495 Value getIV() const { return body->getArgument(0); }
496 Block::BlockArgListType args() const {
497 return body->getArguments().drop_front(firstCarriedArg);
498 }
499 ResultRange results() const {
500 return op->getResults().drop_front(firstCarriedResult);
501 }
502 };
503
504 static ScaffoldLoop createScaffoldForLoop(OpBuilder &b, Location loc,
505 Value lb, Value ub, Value step,
506 ValueRange inits) {
507 auto loop = scf::ForOp::create(b, loc, lb, ub, step, inits);
508 // scf.for materializes a terminator of its own when nothing is carried;
509 // finalizeScaffoldLoop creates it in either case.
510 if (!loop.getBody()->empty())
511 loop.getBody()->front().erase();
512 return {loop, loop.getBody(), /*firstCarriedArg=*/1,
513 /*firstCarriedResult=*/0};
514 }
515
516 // `cond` is called with the carried values and returns the i1 deciding
517 // whether to run the body again.
519 OpBuilder &b, Location loc, ValueRange inits,
520 llvm::function_ref<Value(OpBuilder &, Location, ValueRange)> cond) {
521 SmallVector<Type> types = llvm::to_vector(inits.getTypes());
522 SmallVector<Location> locs(inits.size(), loc);
523 auto loop = scf::WhileOp::create(b, loc, types, inits);
524 {
525 OpBuilder::InsertionGuard g(b);
526 Block *before = b.createBlock(&loop.getBefore(), {}, types, locs);
527 b.setInsertionPointToEnd(before);
528 scf::ConditionOp::create(b, loc, cond(b, loc, before->getArguments()),
529 before->getArguments());
530 b.createBlock(&loop.getAfter(), {}, types, locs);
531 }
532 return {loop, &loop.getAfter().front(), 0, 0};
533 }
534
535 static void finalizeScaffoldLoop(OpBuilder &b, Location loc,
536 const ScaffoldLoop &loop,
537 ValueRange yields) {
538 scf::YieldOp::create(b, loc, yields);
539 }
540
541 static Value emitCmpLT(OpBuilder &b, Location loc, Value l, Value r) {
542 return arith::CmpIOp::create(b, loc, arith::CmpIPredicate::slt, l, r);
543 }
544
545 static Value emitCmpEQ(OpBuilder &b, Location loc, Value l, Value r) {
546 return arith::CmpIOp::create(b, loc, arith::CmpIPredicate::eq, l, r);
547 }
548
549 static Value emitSelect(OpBuilder &b, Location loc, Value c, Value t,
550 Value f) {
551 return arith::SelectOp::create(b, loc, c, t, f);
552 }
553
554 // The values that stand in for the original loop's results once a scaffold
555 // has replaced it. `state` is the scaffold's final carried state; an op that
556 // returns its own induction variable has to supply that itself, since the
557 // scaffold's counter is not the original's.
558 static SmallVector<Value> getPrimalResults(OpBuilder &b, Location loc,
559 OpName forOp, ValueRange state,
560 MGradientUtilsReverse *gutils) {
561 return llvm::to_vector(state);
562 }
563
564 static Value cloneSlot(OpBuilder &builder, Location loc, Value base,
565 Value buf, Value slot, int64_t budget) {
566 auto iface = cast<ClonableTypeInterface>(base.getType());
567 if (iface.implementsBatchAllocation(
568 base, OpFoldResult(builder.getI64IntegerAttr(budget)))) {
569 Value loaded = iface.deriveSubElement(builder, loc, base, buf, slot);
570 return loaded;
571 }
572
573 return memref::LoadOp::create(builder, loc, buf, ValueRange{slot});
574 }
575
576 // A `budget`-slot buffer of clone *handles* for one mutable ref, with a
577 // clone of `proto` already in every slot. Not checkpointBufferType: what
578 // lives here is the identity of a snapshot allocation (slot j pairs with
579 // ckptBufs slot j), not its contents -- for a bare pointer the extent of
580 // the contents is not in the type at all.
581 //
582 // The clones are made first because they are what the buffer is typed
583 // from: a clone does not have to have the type of what it clones (a
584 // pointer whose size hint annotates a memory space is cloned into that
585 // space), and a buffer built from the ref's type could then neither hold
586 // nor free a handle.
587 //
588 // Deliberately unrolled: `budget` is static, and an allocation sitting in a
589 // loop body is both a hoisting candidate and (for pointers, under a raised
590 // alloca threshold) an alloca-promotion candidate -- either would silently
591 // make all slots alias.
592 static Value allocCloneSlots(OpBuilder &b, Location loc, int64_t budget,
593 Value proto, ClonableTypeInterface iface) {
594 OpFoldResult size = OpFoldResult(b.getI64IntegerAttr(budget));
595
596 if (iface.implementsBatchAllocation(proto, size)) {
597 Value batchAlloc = iface.batchAllocate(b, loc, proto, size);
598 return batchAlloc;
599 }
600
601 Value buf = memref::AllocOp::create(
602 b, loc, MemRefType::get({budget}, proto.getType()));
603
604 Value lb = arith::ConstantIndexOp::create(b, loc, 0);
605 Value ub = arith::ConstantIndexOp::create(b, loc, budget);
606 Value step = arith::ConstantIndexOp::create(b, loc, 1);
607 auto loop = scf::ForOp::create(b, loc, lb, ub, step);
608
609 {
610 OpBuilder::InsertionGuard guard(b);
611 b.setInsertionPoint(loop.getBody()->getTerminator());
612 Value clone = iface.cloneValue(b, proto);
613 memref::StoreOp::create(b, loc, clone, buf,
614 ValueRange{loop.getInductionVar()});
615 }
616
617 return buf;
618 }
619
620 // Free each slot's clone, then the handle buffer itself. A loop is fine
621 // here: a free has side effects so it cannot be hoisted, and nothing can
622 // alias.
623 static void freeCloneSlots(OpBuilder &builder, Value ref, Value buf,
624 int64_t budget) {
625 ClonableTypeInterface iface = cast<ClonableTypeInterface>(ref.getType());
626 OpFoldResult numel = builder.getI64IntegerAttr(budget);
627
628 if (iface.implementsBatchAllocation(ref, numel)) {
629 iface.freeClonedValue(builder, buf);
630 return;
631 }
632
633 Value lb = arith::ConstantIndexOp::create(builder, ref.getLoc(), 0);
634 Value ub = arith::ConstantIndexOp::create(builder, ref.getLoc(), budget);
635 Value step = arith::ConstantIndexOp::create(builder, ref.getLoc(), 1);
636 auto loop = scf::ForOp::create(builder, ref.getLoc(), lb, ub, step);
637
638 {
639 OpBuilder::InsertionGuard guard(builder);
640 builder.setInsertionPoint(loop.getBody()->getTerminator());
641
642 Value val = memref::LoadOp::create(builder, buf.getLoc(), buf,
643 loop.getInductionVar());
644 iface.freeClonedValue(builder, val);
645 }
646
647 memref::DeallocOp::create(builder, buf.getLoc(), buf);
648 }
649
650 // Forward augmentation for binomial (Revolve) checkpointing. Builds an
651 // outer loop of `budget` iterations that snapshots the loop state into
652 // memref checkpoint buffers at Revolve-scheduled positions, advancing the
653 // primal in an inner recompute loop between snapshots. Returns the caches
654 // (buffer handles + index buffer + outside refs) transported to the
655 // reverse pass; see BinomialCacheLayout for their order.
656 static SmallVector<Value> cacheBinomial(OpName forOp, int64_t budget,
657 MGradientUtilsReverse *gutils) {
658 Location loc = forOp.getLoc();
659 bool isDynamic =
660 !FinalClass::getConstantNumberOfIterations(forOp).has_value();
661
662 auto newForOp = cast<OpName>(gutils->getNewFromOriginal(forOp));
663 OpBuilder builder(newForOp);
664 Type idxTy = FinalClass::getIndexLikeType(builder);
665
666 Value c0 = FinalClass::emitConst(builder, loc, 0);
667 Value c1 = FinalClass::emitConst(builder, loc, 1);
668
669 // Loop trip count / lower bound / step as index values (constant-folded
670 // when the bounds are constant).
671 Value numItersV, startV, stepV;
672 if (isDynamic) {
673 startV = FinalClass::materializeLowerBound(builder, loc, forOp, gutils);
674 stepV = FinalClass::materializeStep(builder, loc, forOp, gutils);
675 numItersV =
676 FinalClass::getNumIterationsValue(builder, loc, forOp, gutils);
677 numItersV = FinalClass::castToType(builder, loc, numItersV, idxTy);
678 } else {
679 int64_t numIters =
680 FinalClass::getConstantNumberOfIterations(forOp).value();
681 numItersV = FinalClass::emitConst(builder, loc, numIters);
682 startV = FinalClass::emitConst(builder, loc,
683 FinalClass::getConstantStart(forOp));
684 stepV = FinalClass::emitConst(builder, loc,
685 FinalClass::getConstantStep(forOp));
686 }
687
688 // Effective budget = min(requested budget, trip count): never keep more
689 // checkpoints than there are iterations. Buffers stay sized by the
690 // (static) requested budget; the effective budget bounds the loops at
691 // runtime.
692 //
693 // Unconditional: with a budget above the trip count this loop would run
694 // more iterations than there are steps, and the slots past the end get
695 // recorded at a step beyond the last one -- holding the final state
696 // instead of a checkpoint, which the reverse pass then replays from.
697 Value budgetV = FinalClass::emitMin(
698 builder, loc, FinalClass::emitConst(builder, loc, budget), numItersV);
699
700 SmallVector<Value> immutableRefs, mutableRefs;
701 FinalClass::splitOutsideRefs(forOp, mutableRefs, immutableRefs);
702 auto layout = BinomialCacheLayout::get(forOp, mutableRefs, immutableRefs,
703 isDynamic, gutils);
704
705 IRMapping mapping;
706 SmallVector<Value> caches;
707
708 // One checkpoint store per iter arg, plus one holding the forward step each
709 // checkpoint was taken at. Kept in a single list, since they are carried,
710 // updated and transported together; the index store is the last entry.
711 size_t numIterArgs = FinalClass::getNumRegionIterArgs(forOp);
712 SmallVector<Value> stores;
713 for (auto arg : FinalClass::getInits(newForOp))
714 stores.push_back(
715 FinalClass::createStore(builder, loc, budget, arg.getType()));
716 stores.push_back(FinalClass::createStore(builder, loc, budget, idxTy));
717 const size_t idxStore = stores.size() - 1;
718 const bool carriedStores = FinalClass::storesAreLoopCarried();
719
720 // One clone buffer per mutable ref, filled up front so that taking a
721 // checkpoint is a pure copy into an existing allocation. Slot j of these
722 // buffers holds the ref's content at forward step idxBuf[j] -- the same
723 // instant as ckptBufs[*][j]; the reverse pass indexes all three by the
724 // checkpoint stack pointer.
725 SmallVector<Value> mutBufs;
726 for (auto ref : mutableRefs) {
727 auto iface = cast<ClonableTypeInterface>(ref.getType());
728 Value buf = allocCloneSlots(builder, loc, budget,
729 gutils->getNewFromOriginal(ref), iface);
730 mutBufs.push_back(buf);
731 }
732
733 // Outer checkpoint-placement loop: for %k = 0 to budgetV carrying
734 // (stepCtr, state..., [stores...]).
735 SmallVector<Value> outerInit;
736 outerInit.push_back(c0);
737 auto newForOpInits = FinalClass::getInits(newForOp);
738 outerInit.append(newForOpInits.begin(), newForOpInits.end());
739 if (carriedStores)
740 outerInit.append(stores.begin(), stores.end());
741 auto outerFwd = FinalClass::createScaffoldForLoop(builder, loc, c0, budgetV,
742 c1, outerInit);
743 FinalClass::preserveAttributesButCheckpointing(outerFwd.op, forOp);
744
745 builder.setInsertionPointToStart(outerFwd.body);
746 Value k = outerFwd.getIV();
747 auto outerArgs = outerFwd.args();
748 Value stepCtr = outerArgs[0];
749 auto state = outerArgs.slice(1, numIterArgs);
750
751 // Inside the loop the stores are the iteration arguments, not the values
752 // they were initialized from.
753 SmallVector<Value> liveStores(stores);
754 if (carriedStores)
755 for (auto &&[i, arg] :
756 llvm::enumerate(outerArgs.slice(1 + numIterArgs, stores.size())))
757 liveStores[i] = arg;
758
759 for (auto &&[i, val] : llvm::enumerate(state))
760 liveStores[i] =
761 FinalClass::storeSlot(builder, loc, liveStores[i], k, val);
762 liveStores[idxStore] =
763 FinalClass::storeSlot(builder, loc, liveStores[idxStore], k, stepCtr);
764
765 Value numStepsRem = FinalClass::emitSub(builder, loc, numItersV, stepCtr);
766 Value budgetRem = FinalClass::emitSub(builder, loc, budgetV, k);
767 // Never use more checkpoints than remaining steps (binomial_progress is
768 // degenerate for budget > steps).
769 budgetRem = FinalClass::emitMin(builder, loc, budgetRem, numStepsRem);
770 Value split = enzyme::BinomialProgressOp::create(builder, loc, idxTy,
771 numStepsRem, budgetRem);
772
773 // Snapshot each mutable ref into slot `k`, reusing the clone already
774 // there. Stays here, before innerFwd, so the snapshot precedes the
775 // advance.
776 for (auto &&[ref, buf] : llvm::zip_equal(mutableRefs, mutBufs)) {
777 auto iface = cast<ClonableTypeInterface>(ref.getType());
778 iface.copyValue(builder,
779 cloneSlot(builder, loc, gutils->getNewFromOriginal(ref),
780 buf, k, budget),
781 gutils->getNewFromOriginal(ref));
782 }
783
784 // Inner recompute loop: advance the primal `split` steps.
785 auto innerFwd = FinalClass::createScaffoldForLoop(
786 builder, loc, c0, split, c1,
787 SmallVector<Value>(state.begin(), state.end()));
788 FinalClass::preserveAttributesButCheckpointing(innerFwd.op, forOp);
789
790 builder.setInsertionPointToStart(innerFwd.body);
791 Value i = innerFwd.getIV();
792 Value globalStep = FinalClass::emitAdd(builder, loc, stepCtr, i);
793 globalStep =
794 FinalClass::castToType(builder, loc, globalStep, stepV.getType());
795 Value iv = FinalClass::emitAdd(
796 builder, loc, startV,
797 FinalClass::emitMul(builder, loc, stepV, globalStep));
798
799 Block *newForOpBody = FinalClass::getBodyBlock(newForOp);
800 for (auto &&[oldArg, newArg] : llvm::zip_equal(
801 newForOpBody->getArguments().drop_front(), innerFwd.args()))
802 mapping.map(oldArg, newArg);
803 mapping.map(FinalClass::getInductionVar(newForOp), iv);
804
805 copyBlockWithoutTerminator(builder, newForOpBody, gutils, mapping);
806
807 SmallVector<Value> innerYields;
808 for (auto operand : FinalClass::getCarriedTerminatorOperands(
809 newForOpBody->getTerminator()))
810 innerYields.push_back(mapping.lookupOrDefault(operand));
811 FinalClass::finalizeScaffoldLoop(builder, loc, innerFwd, innerYields);
812
813 FinalClass::setInsertionPointToBodyEnd(builder, outerFwd.body);
814 SmallVector<Value> outerYields;
815 outerYields.push_back(FinalClass::emitAdd(builder, loc, stepCtr, split));
816 auto innerResults = innerFwd.results();
817 outerYields.append(innerResults.begin(), innerResults.end());
818 if (carriedStores)
819 outerYields.append(liveStores.begin(), liveStores.end());
820 FinalClass::finalizeScaffoldLoop(builder, loc, outerFwd, outerYields);
821
822 builder.setInsertionPointAfter(outerFwd.op);
823
824 // Carried stores leave the loop as its results; a buffer is the same value
825 // it always was.
826 auto outerResults = outerFwd.results();
827 SmallVector<Value> finalStores(stores);
828 if (carriedStores)
829 for (auto &&[i, res] :
830 llvm::enumerate(outerResults.slice(1 + numIterArgs, stores.size())))
831 finalStores[i] = res;
832
833 // Cache the stores + outside refs (single push each).
834 for (auto store : finalStores)
835 caches.push_back(gutils->initAndPushCache(store, builder));
836
837 // One push per mutable ref: the clone buffer, plus the shadow, which is
838 // loop-invariant (it is the accumulating gradient buffer, not a
839 // snapshot) and so does not belong in a per-checkpoint slot.
840 for (auto &&[r, ref] : llvm::enumerate(mutableRefs)) {
841 caches.push_back(gutils->initAndPushCache(mutBufs[r], builder));
842 if (layout.mutableActive[r])
843 caches.push_back(gutils->initAndPushCache(
844 gutils->invertPointerM(ref, builder), builder));
845 }
846
847 for (auto ref : immutableRefs)
848 caches.push_back(
849 gutils->initAndPushCache(gutils->getNewFromOriginal(ref), builder));
850
851 // For dynamic bounds the reverse pass cannot recover the trip count /
852 // lower bound / step from constants, so cache them (as the trailing
853 // entries).
854 if (isDynamic) {
855 caches.push_back(gutils->initAndPushCache(numItersV, builder));
856 caches.push_back(gutils->initAndPushCache(startV, builder));
857 caches.push_back(gutils->initAndPushCache(stepV, builder));
858 }
859 assert(caches.size() == layout.size() && "binomial cache layout mismatch");
860
861 // The primal result of the loop is the final state.
862 gutils->replaceOrigOpWith(
863 forOp, FinalClass::getPrimalResults(builder, loc, forOp,
864 outerResults.slice(1, numIterArgs),
865 gutils));
866 gutils->erase(newForOp);
867 gutils->originalToNewFnOps[forOp] = outerFwd.op;
868
869 return caches;
870 }
871
872 // forceAugmentedReturns() (called once, early, over the whole original
873 // function) plants an enzyme.placeholder for every mutable-typed value's
874 // shadow, including ones nested inside a checkpointed forOp's augmented
875 // copy. Those placeholders are meant to survive until the reverse pass
876 // visits their defining op and replaces them via setInvertedPointer. But
877 // checkpointing discards the whole augmented-forward copy of forOp (it's
878 // redundant once the checkpoint-buffer reconstruction takes over) via
879 // gutils->erase(...), which -- if it happens before that replacement runs
880 // -- destroys the still-referenced placeholder out from under
881 // invertedPointers, leaving a dangling entry that only crashes later,
882 // whenever something finally reads it. Hoist any not-yet-replaced
883 // placeholders out of the doomed subtree first so they survive.
884 static void hoistPlaceholdersBefore(Operation *root, Operation *before) {
885 SmallVector<enzyme::PlaceholderOp> placeholders;
886 root->walk([&](enzyme::PlaceholderOp p) { placeholders.push_back(p); });
887 for (auto p : placeholders)
888 p->moveBefore(before);
889 }
890
891 // Reverse pass for binomial (Revolve) checkpointing. Iterates all N steps
892 // in reverse; for each step it reconstructs the state just before that
893 // step from the top checkpoint (recursively re-placing finer checkpoints
894 // during the remat), then emits the adjoint of a single body step.
895 static LogicalResult reverseBinomial(OpName forOp, int64_t budget,
896 OpBuilder &builder,
897 MGradientUtilsReverse *gutils,
898 SmallVector<Value> caches,
899 ArrayRef<bool> operandsActive,
900 ArrayRef<Value> incomingGradients) {
901 Location loc = forOp.getLoc();
902 bool isDynamic =
903 !FinalClass::getConstantNumberOfIterations(forOp).has_value();
904 auto numIterArgs = FinalClass::getNumRegionIterArgs(forOp);
905
906 SmallVector<Value> immutableRefs, mutableRefs;
907 FinalClass::splitOutsideRefs(forOp, mutableRefs, immutableRefs);
908 auto layout = BinomialCacheLayout::get(forOp, mutableRefs, immutableRefs,
909 isDynamic, gutils);
910 assert(caches.size() == layout.size() && "binomial cache layout mismatch");
911
912 IRMapping mapping;
913
914 // Below, each mutableRef's shadow is re-bound to the popped shadow
915 // handle. That binding must not outlive this function: ops *outside* the
916 // loop are visited after it (reverse order) and expect the caller's
917 // shadow, not ours. Restore the previous bindings on the way out.
918 SmallVector<std::pair<Value, Value>> savedMutableShadows;
919 for (auto ref : mutableRefs)
920 savedMutableShadows.emplace_back(
921 ref, gutils->invertedPointers.lookupOrNull(ref));
922 auto restoreMutableShadows = llvm::scope_exit([&]() {
923 for (auto &[ref, prev] : savedMutableShadows) {
924 if (prev)
925 gutils->invertedPointers.map(ref, prev);
926 else
927 gutils->invertedPointers.erase(ref);
928 }
929 });
930
931 // Pop the checkpoint stores (indices from the shared layout); the index
932 // store is the last of them, same as in cacheBinomial.
933 SmallVector<Value> stores;
934 for (size_t j = 0; j < numIterArgs; ++j)
935 stores.push_back(gutils->popCache(caches[layout.ckptBuf(j)], builder));
936 stores.push_back(gutils->popCache(caches[layout.idxBuf()], builder));
937 const size_t idxStore = stores.size() - 1;
938 const bool carriedStores = FinalClass::storesAreLoopCarried();
939
940 SmallVector<Value> immutableRefsCaches;
941 for (auto &&[i, ref] : llvm::enumerate(immutableRefs)) {
942 Value cached = gutils->popCache(caches[layout.immutable(i)], builder);
943 mapping.map(ref, cached);
944 immutableRefsCaches.push_back(cached);
945 }
946
947 Value c0 = FinalClass::emitConst(builder, loc, 0);
948 Value c1 = FinalClass::emitConst(builder, loc, 1);
949
950 // Loop trip count / lower bound / step as index values. For dynamic
951 // bounds these were cached by cacheBinomial (trailing entries, same
952 // order).
953 Value numItersV, startV, stepV;
954 if (isDynamic) {
955 numItersV = gutils->popCache(caches[layout.numIters()], builder);
956 startV = gutils->popCache(caches[layout.start()], builder);
957 stepV = gutils->popCache(caches[layout.step()], builder);
958 } else {
959 int64_t numIters =
960 FinalClass::getConstantNumberOfIterations(forOp).value();
961 numItersV = FinalClass::emitConst(builder, loc, numIters);
962 startV = FinalClass::emitConst(builder, loc,
963 FinalClass::getConstantStart(forOp));
964 stepV = FinalClass::emitConst(builder, loc,
965 FinalClass::getConstantStep(forOp));
966 }
967
968 // Effective budget = min(requested budget, trip count); must match
969 // cacheBinomial, including being unconditional.
970 Value budgetV = FinalClass::emitMin(
971 builder, loc, FinalClass::emitConst(builder, loc, budget), numItersV);
972
973 // Clone buffers and shadows are single-entry caches, so they are popped
974 // once, here, outside the loop. Each ref also gets a working clone the
975 // reverse pass owns: the replay writes through the ref, so it must not
976 // write into a checkpoint slot -- slot `capo` is re-read on the next
977 // iteration whenever the remat placed finer checkpoints above it.
978 SmallVector<Value> mutBufs, workClones;
979 for (auto &&[r, ref] : llvm::enumerate(mutableRefs)) {
980 auto iface = cast<ClonableTypeInterface>(ref.getType());
981 mutBufs.push_back(gutils->popCache(caches[layout.cloneBuf(r)], builder));
982 workClones.push_back(
983 iface.cloneValue(builder, gutils->getNewFromOriginal(ref)));
984 mapping.map(ref, workClones.back());
985 if (layout.mutableActive[r])
986 gutils->invertedPointers.map(
987 ref, gutils->popCache(caches[layout.shadow(r)], builder));
988 }
989
990 // Outer reverse loop over all N steps; carries (sp, adjoints..., [stores]).
991 SmallVector<Value> outerInit;
992 outerInit.push_back(budgetV); // live checkpoint count
993 outerInit.append(incomingGradients.begin(), incomingGradients.end());
994 if (carriedStores)
995 outerInit.append(stores.begin(), stores.end());
996
997 auto revOuter = FinalClass::createScaffoldForLoop(builder, loc, c0,
998 numItersV, c1, outerInit);
999 FinalClass::preserveAttributesButCheckpointing(revOuter.op, forOp);
1000
1001 OpBuilder::InsertionGuard guard(builder);
1002 builder.setInsertionPointToStart(revOuter.body);
1003
1004 Value ivO = revOuter.getIV();
1005 auto revOuterArgs = revOuter.args();
1006 Value sp = revOuterArgs[0];
1007 auto adjArgs = revOuterArgs.slice(1, incomingGradients.size());
1008
1009 SmallVector<Value> liveStores(stores);
1010 if (carriedStores)
1011 for (auto &&[i, arg] : llvm::enumerate(
1012 revOuterArgs.slice(1 + incomingGradients.size(), stores.size())))
1013 liveStores[i] = arg;
1014
1015 Value capo = FinalClass::emitSub(builder, loc, sp, c1);
1016 Value currentRevStep = FinalClass::emitSub(builder, loc, numItersV, ivO);
1017
1018 // Load the top checkpoint state + its forward step.
1019 SmallVector<Value> ckptState;
1020 for (auto &&[store, arg] : llvm::zip_equal(
1021 ArrayRef<Value>(liveStores).drop_back(),
1022 FinalClass::getBodyBlock(forOp)->getArguments().drop_front()))
1023 ckptState.push_back(
1024 FinalClass::loadSlot(builder, loc, store, capo, arg.getType()));
1025 Value ckptStep =
1026 FinalClass::loadSlot(builder, loc, liveStores[idxStore], capo,
1027 FinalClass::getIndexLikeType(builder));
1028
1029 // Re-prime each working clone from the snapshot paired with slot `capo`,
1030 // so the replay below starts from the mutable memory as it was at
1031 // ckptStep.
1032 for (auto &&[r, ref] : llvm::enumerate(mutableRefs)) {
1033 auto iface = cast<ClonableTypeInterface>(ref.getType());
1034 iface.copyValue(builder, workClones[r],
1035 cloneSlot(builder, loc, gutils->getNewFromOriginal(ref),
1036 mutBufs[r], capo, budget));
1037 }
1038
1039 // Inner remat loop: reconstruct state at (currentRevStep - 1), carrying
1040 // (pos, capo, state..., [stores...]).
1041 SmallVector<Value> whileInit;
1042 whileInit.push_back(ckptStep);
1043 whileInit.push_back(capo);
1044 whileInit.append(ckptState.begin(), ckptState.end());
1045 if (carriedStores)
1046 whileInit.append(liveStores.begin(), liveStores.end());
1047
1048 auto revWhile = FinalClass::createScaffoldWhileLoop(
1049 builder, loc, whileInit,
1050 [&](OpBuilder &b, Location l, ValueRange args) {
1051 Value posPlus1 = FinalClass::emitAdd(b, l, args[0], c1);
1052 return FinalClass::emitCmpLT(b, l, posPlus1, currentRevStep);
1053 });
1054 {
1055 FinalClass::setInsertionPointToBodyEnd(builder, revWhile.body);
1056 auto wargs = revWhile.args();
1057 Value pos = wargs[0];
1058 Value acapo = wargs[1];
1059 auto astate = wargs.slice(2, numIterArgs);
1060
1061 SmallVector<Value> wStores(liveStores);
1062 if (carriedStores)
1063 for (auto &&[i, arg] :
1064 llvm::enumerate(wargs.slice(2 + numIterArgs, stores.size())))
1065 wStores[i] = arg;
1066
1067 Value remaining = FinalClass::emitSub(builder, loc, currentRevStep, pos);
1068 Value budgetRem = FinalClass::emitSub(builder, loc, budgetV, acapo);
1069 // Never use more checkpoints than remaining steps (binomial_progress
1070 // is degenerate for budget > steps).
1071 budgetRem = FinalClass::emitMin(builder, loc, budgetRem, remaining);
1072 Value split = enzyme::BinomialProgressOp::create(
1073 builder, loc, FinalClass::getIndexLikeType(builder), remaining,
1074 budgetRem);
1075
1076 // Place a checkpoint at slot `acapo`. The mutable-ref snapshot has to
1077 // go with it: the working clones currently hold the content at step
1078 // `pos` (innerRemat below advances them only after this store), and a
1079 // slot whose state and snapshot came from different steps replays
1080 // wrongly.
1081 for (auto &&[i, val] : llvm::enumerate(astate))
1082 wStores[i] =
1083 FinalClass::storeSlot(builder, loc, wStores[i], acapo, val);
1084 wStores[idxStore] =
1085 FinalClass::storeSlot(builder, loc, wStores[idxStore], acapo, pos);
1086
1087 for (auto &&[r, ref] : llvm::enumerate(mutableRefs)) {
1088 auto iface = cast<ClonableTypeInterface>(ref.getType());
1089 iface.copyValue(builder,
1090 cloneSlot(builder, loc, gutils->getNewFromOriginal(ref),
1091 mutBufs[r], acapo, budget),
1092 workClones[r]);
1093 }
1094
1095 Value posPlusSplit = FinalClass::emitAdd(builder, loc, pos, split);
1096 Value isLast =
1097 FinalClass::emitCmpEQ(builder, loc, posPlusSplit, currentRevStep);
1098 Value rematUB = FinalClass::emitSelect(
1099 builder, loc, isLast,
1100 FinalClass::emitSub(builder, loc, posPlusSplit, c1), posPlusSplit);
1101
1102 // Recompute the primal from `pos` to `rematUB`.
1103 auto innerRemat = FinalClass::createScaffoldForLoop(
1104 builder, loc, pos, rematUB, c1,
1105 SmallVector<Value>(astate.begin(), astate.end()));
1106 FinalClass::preserveAttributesButCheckpointing(innerRemat.op, forOp);
1107
1108 {
1109 OpBuilder::InsertionGuard g2(builder);
1110 builder.setInsertionPointToStart(innerRemat.body);
1111 Value idx = FinalClass::castToType(builder, loc, innerRemat.getIV(),
1112 stepV.getType());
1113 Value iv =
1114 FinalClass::emitAdd(builder, loc, startV,
1115 FinalClass::emitMul(builder, loc, stepV, idx));
1116
1117 Block *origBodyBlock = FinalClass::getBodyBlock(forOp);
1118 for (auto &&[oldArg, newArg] : llvm::zip_equal(
1119 origBodyBlock->getArguments().drop_front(), innerRemat.args()))
1120 mapping.map(oldArg, newArg);
1121 mapping.map(FinalClass::getInductionVar(forOp), iv);
1122
1123 copyBlockWithoutTerminator(builder, origBodyBlock, gutils, mapping);
1124
1125 SmallVector<Value> yields;
1126 for (auto operand : FinalClass::getCarriedTerminatorOperands(
1127 origBodyBlock->getTerminator()))
1128 yields.push_back(mapping.lookupOrDefault(operand));
1129 FinalClass::finalizeScaffoldLoop(builder, loc, innerRemat, yields);
1130 }
1131
1132 Value newCapo = FinalClass::emitAdd(builder, loc, acapo, c1);
1133 SmallVector<Value> afterYields;
1134 afterYields.push_back(posPlusSplit);
1135 afterYields.push_back(newCapo);
1136 auto rematResults = innerRemat.results();
1137 afterYields.append(rematResults.begin(), rematResults.end());
1138 if (carriedStores)
1139 afterYields.append(wStores.begin(), wStores.end());
1140 FinalClass::finalizeScaffoldLoop(builder, loc, revWhile, afterYields);
1141 }
1142
1143 FinalClass::setInsertionPointToBodyEnd(builder, revOuter.body);
1144 auto whileResults = revWhile.results();
1145 Value newSp = whileResults[1];
1146 auto reconState = whileResults.slice(2, numIterArgs);
1147 if (carriedStores)
1148 for (auto &&[i, res] :
1149 llvm::enumerate(whileResults.slice(2 + numIterArgs, stores.size())))
1150 liveStores[i] = res;
1151
1152 // Adjoint of a single body step at (currentRevStep - 1).
1153 Value stepAdj = FinalClass::emitSub(builder, loc, currentRevStep, c1);
1154 Value stepAdjC =
1155 FinalClass::castToType(builder, loc, stepAdj, stepV.getType());
1156 Value ivAdj =
1157 FinalClass::emitAdd(builder, loc, startV,
1158 FinalClass::emitMul(builder, loc, stepV, stepAdjC));
1159
1160 mapping = IRMapping();
1161
1162 for (auto [ref, cached] :
1163 llvm::zip_equal(immutableRefs, immutableRefsCaches))
1164 mapping.map(ref, cached);
1165
1166 // Re-bind after the mapping reset above; the shadows were bound once,
1167 // before revOuter, and do not need rebinding per iteration.
1168 for (auto &&[r, ref] : llvm::enumerate(mutableRefs))
1169 mapping.map(ref, workClones[r]);
1170
1171 for (auto &&[oldArg, newArg] : llvm::zip_equal(
1172 FinalClass::getBodyBlock(forOp)->getArguments().drop_front(),
1173 reconState))
1174 mapping.map(oldArg, newArg);
1175 mapping.map(FinalClass::getInductionVar(forOp), ivAdj);
1176
1177 // Re-materialize primal ops of this step for the reverse visitor.
1178 copyBlockWithoutTerminator(builder, FinalClass::getBodyBlock(forOp), gutils,
1179 mapping);
1180
1181 // forceAugmentedReturns() seeded invertedPointers with one PlaceholderOp
1182 // per active mutable value, positioned in the single augmented primal --
1183 // which cacheBinomial has since erased, leaving those entries dangling
1184 // (a later invertPointerM() would hand out freed IR). Re-seed a
1185 // placeholder per body value here, in this per-step reconstruction: that
1186 // is where the shadow that replaces it legitimately lives, and where the
1187 // per-iteration popCache shadows of the outside refs (bound above)
1188 // dominate it. Hoisting the originals out of the loop instead cannot
1189 // work, precisely because those source shadows are per-iteration values
1190 // inside revOuter.
1191 SmallVector<Value> reseededShadowKeys;
1192 {
1193 OpBuilder::InsertionGuard g4(builder);
1194 FinalClass::getBodyBlock(forOp)->walk([&](Operation *inner) {
1195 for (Value res : inner->getResults()) {
1196 if (gutils->isConstantValue(res))
1197 continue;
1198 Type shadowTy = gutils->getShadowType(res.getType());
1199 auto iface = dyn_cast<AutoDiffTypeInterface>(shadowTy);
1200 if (!iface || !iface.isMutable())
1201 continue;
1202 Value newRes = mapping.lookupOrNull(res);
1203 if (!newRes)
1204 continue;
1205 if (Operation *defOp = newRes.getDefiningOp())
1206 builder.setInsertionPointAfter(defOp);
1207 else
1208 continue;
1209 auto ph =
1210 enzyme::PlaceholderOp::create(builder, res.getLoc(), shadowTy);
1211 gutils->invertedPointers.map(res, ph);
1212 reseededShadowKeys.push_back(res);
1213 }
1214 });
1215 }
1216
1217 // Reset every (non-mutable) intermediate gradient slot to zero at the
1218 // start of each reverse step and zero the diffe of the yielded
1219 // operands; the loop-carried gradient is supplied via the outer carried
1220 // adjoints. Without this, scalar gradient slots (e.g. the diffe of a
1221 // value loaded from an enzyme_dup'ed memref) leak across reverse
1222 // iterations and over-accumulate into the external shadow. Mirrors the
1223 // non-checkpointed reverse path.
1224 auto term = FinalClass::getBodyBlock(forOp)->getTerminator();
1225 FinalClass::primeStepGradients(builder, gutils,
1226 FinalClass::getBodyBlock(forOp),
1227 revOuter.body, operandsActive);
1228
1229 // Seed adjoints of the yielded operands from the outer carried
1230 // gradients.
1231 unsigned revIdx = 0;
1232 for (auto &&[active, operand] : llvm::zip_equal(
1233 operandsActive, FinalClass::getCarriedTerminatorOperands(term))) {
1234 if (active) {
1235 gutils->addToDiffe(operand, adjArgs[revIdx], builder);
1236 revIdx++;
1237 }
1238 }
1239
1240 bool valid = true;
1241 auto first = FinalClass::getBodyBlock(forOp)->rbegin();
1242 first++; // skip terminator
1243 auto last = FinalClass::getBodyBlock(forOp)->rend();
1244 {
1245 // Same as in reversePeriodic: a cache a body op's reverse rule asks for
1246 // is per-step, so it is created outside the remat loop.
1247 SegmentCacheCreatorGuard cacheGuard(gutils, revWhile.op);
1248 for (auto it = first; it != last; ++it)
1249 valid &= gutils->Logic.visitChild(&*it, builder, gutils).succeeded();
1250 }
1251
1252 // Placeholders re-seeded above are consumed by setInvertedPointer (which
1253 // RAUWs and erases them) only for values some rule actually asked to
1254 // invert. Drop the unused remainder rather than leaving
1255 // enzyme.placeholder litter in the output -- keyed by the original
1256 // value, so invertedPointers never keeps an entry pointing at IR we just
1257 // erased. A resolved entry no longer names a PlaceholderOp, which is
1258 // what distinguishes it (its op is already gone, so we must not
1259 // dereference the recorded pointer).
1260 for (Value orig : reseededShadowKeys) {
1261 Value cur = gutils->invertedPointers.lookupOrNull(orig);
1262 if (!cur)
1263 continue;
1264 auto ph = cur.getDefiningOp<enzyme::PlaceholderOp>();
1265 if (ph && ph->use_empty()) {
1266 gutils->invertedPointers.erase(orig);
1267 ph->erase();
1268 }
1269 }
1270
1271 SmallVector<Value> newAdjoints;
1272 for (auto &&[active, arg] : llvm::zip_equal(
1273 operandsActive,
1274 FinalClass::getBodyBlock(forOp)->getArguments().drop_front())) {
1275 if (active) {
1276 newAdjoints.push_back(gutils->diffe(arg, builder));
1277 if (!gutils->isConstantValue(arg))
1278 gutils->zeroDiffe(arg, builder);
1279 }
1280 }
1281
1282 SmallVector<Value> outerYields;
1283 outerYields.push_back(newSp);
1284 outerYields.append(newAdjoints.begin(), newAdjoints.end());
1285 if (carriedStores)
1286 outerYields.append(liveStores.begin(), liveStores.end());
1287 FinalClass::finalizeScaffoldLoop(builder, loc, revOuter, outerYields);
1288
1289 builder.setInsertionPointAfter(revOuter.op);
1290
1291 revIdx = 0;
1292 auto revOuterResults = revOuter.results();
1293 auto forOpInits = FinalClass::getInits(forOp);
1294 for (auto &&[active, arg] : llvm::zip_equal(operandsActive, forOpInits)) {
1295 if (active) {
1296 if (!gutils->isConstantValue(arg))
1297 gutils->addToDiffe(arg, revOuterResults[revIdx + 1], builder);
1298 revIdx++;
1299 }
1300 }
1301
1302 // Release the checkpoint stores (a no-op for storage that is a value
1303 // rather than an allocation) and the cloned mutable refs.
1304 for (auto store : stores)
1305 FinalClass::destroyStore(builder, loc, store);
1306 for (auto &&[r, ref] : llvm::enumerate(mutableRefs)) {
1307 auto iface = cast<ClonableTypeInterface>(ref.getType());
1308 iface.freeClonedValue(builder, workClones[r]);
1309 freeCloneSlots(builder, ref, mutBufs[r], budget);
1310 }
1311
1312 return success(valid);
1313 }
1314
1315 //===--------------------------------------------------------------------===//
1316 // Periodic (sqrt-decomposition) checkpointing: decompose the N-iteration
1317 // loop into ~sqrt(N) outer segments, each holding down ~sqrt(N) primal
1318 // steps in a checkpoint; the reverse pass replays one segment at a time. A
1319 // stated period replaces the sqrt(N) segment count with one the caller
1320 // budgets for, and the segment length grows to match.
1321 //===--------------------------------------------------------------------===//
1322
1323 // The compile-time half of the decomposition. A stated period is a budget on
1324 // the *segment* count -- see PeriodicSchedule -- so it is the segment length
1325 // that is derived from it, as ceil(numIters / period). Without one the
1326 // classic sqrt(N) split is used, which deliberately keeps nOuter == nInner
1327 // and lets the remainder spill into a trailing segment that may be as long as
1328 // nInner itself.
1330 PeriodicSchedule sched;
1331 auto period = FinalClass::getCheckpointBudget(forOp);
1332 auto numIters = FinalClass::getConstantNumberOfIterations(forOp);
1333 if (!numIters) {
1334 // Same reading of the period, with the division left to the runtime:
1335 // nInnerV ends up holding ceil(numIters / nOuter).
1336 // needsCheckpointing() only admits a dynamic trip count with a positive
1337 // period.
1338 sched.nInner = -1;
1339 sched.nOuter = *period;
1340 return sched;
1341 }
1342 if (*numIters <= 0) {
1343 // Nothing to segment. Spelled out because both splits below divide by a
1344 // quantity derived from the trip count, and because a zero-length segment
1345 // would give the scaffold loop a zero step.
1346 sched.nInner = 1;
1347 sched.nOuter = 0;
1348 return sched;
1349 }
1350 if (period && *period > 0) {
1351 // Rounding the length up is what holds the segment count to the budget:
1352 // numSegments() then lands at or below `period`, never above.
1353 sched.nInner = (*numIters + *period - 1) / *period;
1354 } else {
1355 sched.nInner = (int64_t)std::sqrt(*numIters);
1356 }
1357 // Whole segments, plus a remainder shorter than one of them. The remainder
1358 // has to be the shorter part: a dialect whose segment bound is
1359 // min(nInner, remaining) -- affine.for's boundary-tile map, stablehlo's
1360 // runtime clamp -- silently truncates a trailing segment longer than
1361 // nInner, and the tail of the loop is then never replayed at all. Splitting
1362 // sqrt(N) as nOuter == nInner and letting the remainder run on up to
1363 // 2*sqrt(N), which is what this did, is what made that reachable.
1364 sched.nOuter = *numIters / sched.nInner;
1365 sched.trailingIters = *numIters % sched.nInner;
1366 return sched;
1367 }
1368
1369 // Where a body op's reverse rule materializes the caches it asks for while a
1370 // segment is being replayed. Returning null keeps the enclosing function's
1371 // own cache creator, which is what the memref dialects want; a dialect that
1372 // must place them relative to the replay loop instead (stablehlo emits an
1373 // enzyme.init just before it) returns a creator anchored on `anchor`.
1374 using CacheCreator = std::function<std::pair<Value, Value>(Type)>;
1375
1377 Operation *anchor) {
1378 return nullptr;
1379 }
1380
1381 // Registers that creator, if there is one, for as long as the replayed body
1382 // is being differentiated.
1384 public:
1386 : gutils(gutils),
1387 hook(FinalClass::makeSegmentCacheCreator(gutils, anchor)) {
1388 if (hook)
1389 gutils->registerCacheCreatorHook(hook);
1390 }
1392 if (hook)
1393 gutils->deregisterCacheCreatorHook(hook);
1394 }
1398
1399 private:
1400 MGradientUtilsReverse *gutils;
1401 CacheCreator hook;
1402 };
1403
1404 // Prepare the gradient slots for one replayed segment, before the caller
1405 // seeds the incoming adjoints from the reverse outer loop's iter_args.
1406 //
1407 // Resetting every (non-mutable) intermediate slot to zero and zeroing the
1408 // diffe of the yielded operands is what keeps a scalar slot -- e.g. the diffe
1409 // of a value loaded from an enzyme_dup'ed memref -- from leaking across
1410 // reverse iterations, getting promoted to a loop-carried iter_arg and
1411 // over-accumulating into the external shadow.
1412 static void primeSegmentGradients(OpBuilder &builder,
1413 MGradientUtilsReverse *gutils,
1414 Block *origBody, Block *revLoopBody,
1415 ArrayRef<bool> operandsActive) {
1416 builder.setInsertionPointToStart(revLoopBody);
1417 mlir::enzyme::localizeGradients(builder, gutils, origBody);
1418
1419 builder.setInsertionPointToEnd(revLoopBody);
1420 for (auto &&[active, operand] : llvm::zip_equal(
1421 operandsActive, FinalClass::getCarriedTerminatorOperands(
1422 origBody->getTerminator()))) {
1423 if (active)
1424 gutils->zeroDiffe(operand, builder);
1425 }
1426 }
1427
1428 // The binomial counterpart of primeSegmentGradients: one *step*, rather than
1429 // a whole segment, is differentiated per iteration of the reverse loop, so
1430 // the slots are reset at the top of that loop's body.
1431 static void primeStepGradients(OpBuilder &builder,
1432 MGradientUtilsReverse *gutils, Block *origBody,
1433 Block *revOuterBody,
1434 ArrayRef<bool> operandsActive) {
1435 {
1436 OpBuilder::InsertionGuard g(builder);
1437 builder.setInsertionPointToStart(revOuterBody);
1438 mlir::enzyme::localizeGradients(builder, gutils, origBody);
1439 }
1440 for (auto &&[active, operand] : llvm::zip_equal(
1441 operandsActive, FinalClass::getCarriedTerminatorOperands(
1442 origBody->getTerminator()))) {
1443 if (active)
1444 gutils->zeroDiffe(operand, builder);
1445 }
1446 }
1447
1448 // The segment length as a value, which every segment bound and replayed
1449 // induction variable is derived from. Split out (rather than transported from
1450 // the forward pass) because the reverse pass recomputes it from the popped
1451 // trip count, by the very same formula: it is a pure function of the
1452 // schedule.
1453 static void materializeSegmentValues(OpBuilder &builder, Location loc,
1454 PeriodicSchedule &sched) {
1455 if (!sched.isDynamic()) {
1456 sched.nInnerV = FinalClass::emitConst(builder, loc, sched.nInner);
1457 return;
1458 }
1459 // nInner = ceil(numIters / nOuter). Rounding *up* is what keeps the
1460 // statically-many segments covering the whole trip count; it is also what
1461 // lets the last segments start past the end of the loop, so each segment's
1462 // length has to be clamped against the trip count where it is built.
1463 sched.nOuterV = FinalClass::emitConst(builder, loc, sched.nOuter);
1464 Value roundUp = FinalClass::emitAdd(
1465 builder, loc, sched.numItersV,
1466 FinalClass::emitConst(builder, loc, sched.nOuter - 1));
1467 sched.nInnerV = FinalClass::emitDivU(builder, loc, roundUp, sched.nOuterV);
1468 }
1469
1470 // How many iterations the segment based at `base` covers: nInner, except for
1471 // the trailing one. Which segment that is, and how short, is the one thing
1472 // the two trip counts disagree on, so it is decided here rather than in each
1473 // direction's hook -- both directions ask this the same question, about a
1474 // base they derived the same way.
1475 static Value segmentLength(OpBuilder &builder, Location loc,
1476 const PeriodicSchedule &sched, Value base) {
1477 if (!sched.isDynamic()) {
1478 if (!sched.hasTrailing())
1479 return sched.nInnerV;
1480 // The trailing segment is the one based just past the last full one.
1481 Value isTrailing = FinalClass::emitCmpEQ(
1482 builder, loc, base,
1483 FinalClass::emitConst(builder, loc, sched.nInner * sched.nOuter));
1484 return FinalClass::emitSelect(
1485 builder, loc, isTrailing,
1486 FinalClass::emitConst(builder, loc, sched.trailingIters),
1487 sched.nInnerV);
1488 }
1489
1490 // A runtime trip count cannot say which segment is short, so every one of
1491 // them is clamped: min(nInner, numIters - base). The subtraction saturates
1492 // rather than wrapping, which is the other half of rounding the segment
1493 // length up -- the last segments can then start at or past the end of the
1494 // loop, and an unsigned wrap there would turn an empty segment into a full
1495 // one running off the end.
1496 Value inBounds = FinalClass::emitMin(builder, loc, base, sched.numItersV);
1497 return FinalClass::emitMin(
1498 builder, loc, sched.nInnerV,
1499 FinalClass::emitSub(builder, loc, sched.numItersV, inBounds));
1500 }
1501
1502 // The base iteration of segment `index`, counted from the start of the loop.
1503 static Value segmentBase(OpBuilder &builder, Location loc,
1504 const PeriodicSchedule &sched, Value index) {
1505 return FinalClass::emitMul(builder, loc, index, sched.nInnerV);
1506 }
1507
1508 // Materialize trip count / lower bound / step for a dynamic trip count, at
1509 // the builder's insertion point -- which must dominate the whole scaffold,
1510 // since every segment bound and induction variable is derived from these.
1511 static void materializeDynamicSchedule(OpBuilder &builder, Location loc,
1512 OpName forOp,
1513 MGradientUtilsReverse *gutils,
1514 PeriodicSchedule &sched) {
1515 sched.startV =
1516 FinalClass::materializeLowerBound(builder, loc, forOp, gutils);
1517 sched.stepV = FinalClass::materializeStep(builder, loc, forOp, gutils);
1518 sched.numItersV = FinalClass::castToType(
1519 builder, loc,
1520 FinalClass::getNumIterationsValue(builder, loc, forOp, gutils),
1521 FinalClass::getIndexLikeType(builder));
1522 FinalClass::materializeSegmentValues(builder, loc, sched);
1523 }
1524
1525 static SmallVector<Value> cachePeriodic(OpName forOp, Operation *op,
1526 MGradientUtilsReverse *gutils) {
1527 Operation *newOpBase = gutils->getNewFromOriginal(op);
1528 OpBuilder cacheBuilder(newOpBase);
1529 Location loc = forOp.getLoc();
1530
1531 PeriodicSchedule sched = FinalClass::getStaticPeriodicSchedule(forOp);
1532 if (!FinalClass::getConstantNumberOfIterations(forOp).has_value())
1533 materializeDynamicSchedule(cacheBuilder, loc, forOp, gutils, sched);
1534 else
1535 FinalClass::materializeSegmentValues(cacheBuilder, loc, sched);
1536
1537 SmallVector<Value> immutableRefs, mutableRefs;
1538 FinalClass::splitOutsideRefs(forOp, mutableRefs, immutableRefs);
1539
1540 SmallVector<Value> caches;
1541
1542 OpName newForOp = cast<OpName>(newOpBase);
1543
1544 auto newForOpInits = FinalClass::getInits(newForOp);
1545 auto outerFwd = FinalClass::createForwardOuterLoop(cacheBuilder, loc, sched,
1546 newForOpInits);
1547 FinalClass::preserveAttributesButCheckpointing(outerFwd, forOp);
1548
1549 Block *outerFwdBody = FinalClass::getBodyBlock(outerFwd);
1550 cacheBuilder.setInsertionPointToStart(outerFwdBody);
1551
1552 // A segment index, in every dialect and whether or not the trip count is
1553 // known: the outer loops run one iteration per checkpoint. Recovering the
1554 // segment's base iteration from it (nInner * index) is left to the hooks,
1555 // which is what `fwdHint` carries.
1556 Value outerFwdIV = FinalClass::getInductionVar(outerFwd);
1557
1558 IRMapping mapping;
1559
1560 // The bound computation (for scf.for) must happen before the
1561 // mutable-ref cloning loop below, and the loop itself must be created
1562 // after it, matching the original code's exact order: canonicalize does
1563 // not freely reorder ops, so this keeps scf.for's output byte-identical.
1564 SmallVector<Value> fwdHint = FinalClass::computeForwardSegmentHint(
1565 cacheBuilder, loc, outerFwdIV, sched);
1566
1567 SmallVector<Value> mutableRefsCaches;
1568 for (auto ref : mutableRefs) {
1569 auto iface = cast<ClonableTypeInterface>(ref.getType());
1570 auto clone =
1571 iface.cloneValue(cacheBuilder, gutils->getNewFromOriginal(ref));
1572 mutableRefsCaches.push_back(
1573 gutils->initAndPushCache(clone, cacheBuilder));
1574 }
1575
1576 auto innerFwd = FinalClass::createForwardSegmentLoop(
1577 cacheBuilder, loc, outerFwdIV, fwdHint, sched,
1578 outerFwdBody->getArguments().drop_front());
1579 FinalClass::preserveAttributesButCheckpointing(innerFwd, forOp);
1580
1581 Block *innerFwdBody = FinalClass::getBodyBlock(innerFwd);
1582 FinalClass::setInsertionPointToBodyEnd(cacheBuilder, innerFwdBody);
1583
1584 Value currentIV = FinalClass::computeForwardSegmentIV(
1585 cacheBuilder, loc, forOp, outerFwdIV,
1586 FinalClass::getInductionVar(innerFwd), sched, fwdHint);
1587
1588 Block *newForOpBody = FinalClass::getBodyBlock(newForOp);
1589 for (auto [oldArg, newArg] : llvm::zip_equal(newForOpBody->getArguments(),
1590 innerFwdBody->getArguments()))
1591 mapping.map(oldArg, newArg);
1592
1593 mapping.map(FinalClass::getInductionVar(newForOp), currentIV);
1594
1595 copyBlockWithoutTerminator(cacheBuilder, newForOpBody, gutils, mapping);
1596
1597 Operation *fwdTerm = newForOpBody->getTerminator();
1598 SmallVector<Value> fwdYields;
1599 for (auto operand : FinalClass::getCarriedTerminatorOperands(fwdTerm))
1600 fwdYields.push_back(mapping.lookupOrDefault(operand));
1601 FinalClass::createScaffoldYield(cacheBuilder, fwdTerm->getLoc(), fwdYields);
1602
1603 FinalClass::setInsertionPointToBodyEnd(cacheBuilder, outerFwdBody);
1604 for (auto initArg : FinalClass::getInits(innerFwd))
1605 caches.push_back(gutils->initAndPushCache(initArg, cacheBuilder));
1606
1607 FinalClass::createScaffoldYield(
1608 cacheBuilder,
1609 FinalClass::getBodyBlock(forOp)->getTerminator()->getLoc(),
1610 FinalClass::getCarriedResults(innerFwd));
1611
1612 cacheBuilder.setInsertionPointAfter(outerFwd);
1613
1614 caches.append(mutableRefsCaches);
1615
1616 for (auto ref : immutableRefs)
1617 caches.push_back(gutils->initAndPushCache(gutils->getNewFromOriginal(ref),
1618 cacheBuilder));
1619
1620 // A dynamic trip count cannot be recovered from constants in the reverse
1621 // pass, so transport it along with the bounds it was derived from.
1622 if (sched.isDynamic()) {
1623 caches.push_back(gutils->initAndPushCache(sched.numItersV, cacheBuilder));
1624 caches.push_back(gutils->initAndPushCache(sched.startV, cacheBuilder));
1625 caches.push_back(gutils->initAndPushCache(sched.stepV, cacheBuilder));
1626 }
1627
1628 gutils->replaceOrigOpWith(op, FinalClass::getPrimalResults(
1629 cacheBuilder, loc, forOp,
1630 FinalClass::getCarriedResults(outerFwd),
1631 gutils));
1632 hoistPlaceholdersBefore(newForOp, newForOp);
1633 gutils->erase(newForOp);
1634 gutils->originalToNewFnOps[op] = outerFwd;
1635
1636 // caches is composed of:
1637 // [
1638 // <caches of iter args>...,
1639 // <caches of mutable values>...,
1640 // <caches of immutable values>...,
1641 // <numIters, start, step -- only when the trip count is dynamic>
1642 // ]
1643 return caches;
1644 }
1645
1646 static LogicalResult reversePeriodic(OpName forOp, Operation *op,
1647 OpBuilder &builder,
1648 MGradientUtilsReverse *gutils,
1649 SmallVector<Value> caches,
1650 ArrayRef<bool> operandsActive,
1651 ArrayRef<Value> incomingGradients) {
1652 auto numIterArgs = FinalClass::getNumRegionIterArgs(forOp);
1653 Location loc = forOp.getLoc();
1654
1655 PeriodicSchedule sched = FinalClass::getStaticPeriodicSchedule(forOp);
1656
1657 SmallVector<Value> immutableRefs, mutableRefs;
1658 FinalClass::splitOutsideRefs(forOp, mutableRefs, immutableRefs);
1659
1660 IRMapping mapping;
1661
1662 assert(caches.size() ==
1663 numIterArgs + mutableRefs.size() + immutableRefs.size() +
1664 (FinalClass::getConstantNumberOfIterations(forOp) ? 0 : 3) &&
1665 "periodic cache layout mismatch");
1666
1667 for (auto [i, ref] : llvm::enumerate(immutableRefs)) {
1668 Value refVal = gutils->popCache(
1669 caches[numIterArgs + mutableRefs.size() + i], builder);
1670 mapping.map(ref, refVal);
1671 }
1672
1673 // The trailing three caches carry what a dynamic trip count cannot get
1674 // from constants; the segment count is recomputed from them here rather
1675 // than transported, since it is a pure function of the trip count.
1676 if (!FinalClass::getConstantNumberOfIterations(forOp).has_value()) {
1677 size_t base = numIterArgs + mutableRefs.size() + immutableRefs.size();
1678 sched.numItersV = gutils->popCache(caches[base], builder);
1679 sched.startV = gutils->popCache(caches[base + 1], builder);
1680 sched.stepV = gutils->popCache(caches[base + 2], builder);
1681 }
1682 FinalClass::materializeSegmentValues(builder, loc, sched);
1683
1684 auto revOuter = FinalClass::createReverseOuterLoop(builder, loc, sched,
1685 incomingGradients);
1686 FinalClass::preserveAttributesButCheckpointing(revOuter, forOp);
1687
1688 OpBuilder::InsertionGuard guard(builder);
1689 Block *revOuterBody = FinalClass::getBodyBlock(revOuter);
1690 // The same segment index as the forward direction, counted from the end:
1691 // segment numSegments() - 1 is replayed first. `revHint` carries whatever
1692 // the hooks derive from it.
1693 Value revOuterIV = FinalClass::getInductionVar(revOuter);
1694 FinalClass::setInsertionPointToBodyEnd(builder, revOuterBody);
1695
1696 SmallVector<Value> cachedOutsideRefs;
1697 for (auto [i, ref] : llvm::enumerate(mutableRefs)) {
1698 Value refVal = gutils->popCache(caches[numIterArgs + i], builder);
1699 cachedOutsideRefs.push_back(refVal);
1700 mapping.map(ref, refVal);
1701 }
1702
1703 // Must happen here, before the initArgs pop loop below, matching the
1704 // original code's exact order (canonicalize does not freely reorder
1705 // ops, so this keeps scf.for's output byte-identical). Whatever the dialect
1706 // returns is handed back to it unchanged by the two hooks below; it is
1707 // computed once, here, only so that its ops land at this point.
1708 SmallVector<Value> revHint =
1709 FinalClass::computeReverseSegmentHint(builder, loc, revOuterIV, sched);
1710
1711 SmallVector<Value> initArgs(numIterArgs, nullptr);
1712 for (size_t i = 0; i < numIterArgs; ++i) {
1713 initArgs[i] = gutils->popCache(caches[i], builder);
1714 }
1715
1716 auto revInner = FinalClass::createReverseSegmentLoop(
1717 builder, loc, revOuterIV, revHint, sched, initArgs);
1718 FinalClass::preserveAttributesButCheckpointing(revInner, forOp);
1719
1720 Block *revInnerBody = FinalClass::getBodyBlock(revInner);
1721 FinalClass::setInsertionPointToBodyEnd(builder, revInnerBody);
1722
1723 Value currentIV = FinalClass::computeReverseSegmentIV(
1724 builder, loc, forOp, revOuterIV, FinalClass::getInductionVar(revInner),
1725 sched, revHint);
1726
1727 Block *origBodyBlock = FinalClass::getBodyBlock(forOp);
1728 for (auto [oldArg, newArg] : llvm::zip_equal(origBodyBlock->getArguments(),
1729 revInnerBody->getArguments()))
1730 mapping.map(oldArg, newArg);
1731
1732 mapping.map(FinalClass::getInductionVar(forOp), currentIV);
1733
1734 copyBlockWithoutTerminator(builder, origBodyBlock, gutils, mapping);
1735 Operation *segTerm = origBodyBlock->getTerminator();
1736 SmallVector<Value> segYields;
1737 for (auto operand : FinalClass::getCarriedTerminatorOperands(segTerm))
1738 segYields.push_back(mapping.lookupOrDefault(operand));
1739 FinalClass::createScaffoldYield(builder, segTerm->getLoc(), segYields);
1740
1741 FinalClass::setInsertionPointToBodyEnd(builder, revOuterBody);
1742
1743 for (auto outsideRef : cachedOutsideRefs) {
1744 if (auto cachableT =
1745 dyn_cast<ClonableTypeInterface>(outsideRef.getType())) {
1746 cachableT.freeClonedValue(builder, outsideRef);
1747 }
1748 }
1749
1750 auto revLoop = FinalClass::createLoopWithSameBounds(
1751 builder, loc, revInner, revOuterBody->getArguments().drop_front());
1752 FinalClass::preserveAttributesButCheckpointing(revLoop, forOp);
1753
1754 Block *revLoopBody = FinalClass::getBodyBlock(revLoop);
1755 Block *origBody = origBodyBlock;
1756
1757 FinalClass::primeSegmentGradients(builder, gutils, origBody, revLoopBody,
1758 operandsActive);
1759
1760 FinalClass::setInsertionPointToBodyEnd(builder, revLoopBody);
1761 int revIdx = 1;
1762 for (auto &&[active, operand] : llvm::zip_equal(
1763 operandsActive, FinalClass::getCarriedTerminatorOperands(
1764 origBody->getTerminator()))) {
1765 if (active) {
1766 gutils->addToDiffe(operand, revLoopBody->getArgument(revIdx), builder);
1767 revIdx++;
1768 }
1769 }
1770
1771 bool valid = true;
1772
1773 auto first = origBody->rbegin();
1774 first++; // skip terminator
1775
1776 auto last = origBody->rend();
1777
1778 {
1779 // Any cache a body op's reverse rule needs is per-segment, so it has to
1780 // be created outside the replayed segment -- `revInner` is the anchor the
1781 // dialect gets to place it before.
1782 SegmentCacheCreatorGuard cacheGuard(gutils, revInner);
1783 for (auto it = first; it != last; ++it) {
1784 Operation *o = &*it;
1785 valid &= gutils->Logic.visitChild(o, builder, gutils).succeeded();
1786 }
1787 }
1788
1789 SmallVector<Value> newResults;
1790 for (auto &&[active, arg] : llvm::zip_equal(
1791 operandsActive, origBody->getArguments().drop_front())) {
1792 if (active) {
1793 newResults.push_back(gutils->diffe(arg, builder));
1794 if (!gutils->isConstantValue(arg))
1795 gutils->zeroDiffe(arg, builder);
1796 }
1797 }
1798
1799 FinalClass::setInsertionPointToBodyEnd(builder, revLoopBody);
1800 FinalClass::createScaffoldYield(
1801 builder, origBody->getTerminator()->getLoc(), newResults);
1802
1803 FinalClass::setInsertionPointToBodyEnd(builder, revOuterBody);
1804 FinalClass::createScaffoldYield(builder,
1805 origBody->getTerminator()->getLoc(),
1806 FinalClass::getCarriedResults(revLoop));
1807
1808 builder.setInsertionPointAfter(revOuter);
1809
1810 revIdx = 0;
1811 auto revOuterResults = FinalClass::getCarriedResults(revOuter);
1812 auto forOpInits = FinalClass::getInits(forOp);
1813 for (auto &&[active, arg] : llvm::zip_equal(operandsActive, forOpInits)) {
1814 if (active) {
1815 if (!gutils->isConstantValue(arg)) {
1816 gutils->addToDiffe(arg, revOuterResults[revIdx], builder);
1817 }
1818 revIdx++;
1819 }
1820 }
1821
1822 return success(valid);
1823 }
1824
1825 //===--------------------------------------------------------------------===//
1826 // Entry points
1827 //===--------------------------------------------------------------------===//
1828
1829 static std::optional<SmallVector<Value>>
1830 tryCacheValues(OpName forOp, Operation *op, MGradientUtilsReverse *gutils) {
1831 if (!FinalClass::needsBinomialCheckpointing(forOp) &&
1832 !FinalClass::needsCheckpointing(forOp))
1833 return std::nullopt;
1834
1835 // Both schemes build their bookkeeping scaffold (budget/recompute loops,
1836 // the remat scf.while) out of scf ops regardless of which dialect the
1837 // differentiated loop itself belongs to. Loading the scf dialect this
1838 // late (mid-pass, possibly under a multi-threaded PassManager) is not
1839 // safe -- MLIR requires it to be declared in the triggering pass's
1840 // `dependentDialects` instead (see DifferentiatePass/
1841 // DifferentiateWrapperPass in Passes.td), so it's preloaded up front.
1842
1843 // Bound shapes materializeLowerBound/UpperBound can't handle (e.g.
1844 // affine min/max multi-result maps) are rejected up front, before any
1845 // bound materialization is attempted below; trivial success() for
1846 // dialects whose bounds are always single-valued (scf.for). The error is
1847 // emitted here so it surfaces even though cacheValues runs before
1848 // createReverseModeAdjoint.
1849 if (failed(FinalClass::requireSingleResultBounds(forOp)))
1850 return SmallVector<Value>();
1851
1852 if (FinalClass::needsBinomialCheckpointing(forOp)) {
1853 auto budget = FinalClass::getCheckpointBudget(forOp);
1854 if (!budget || *budget <= 1) {
1855 // Error is reported in tryCreateReverseModeAdjoint; fall back to
1856 // caching the bounds so the reverse pass can proceed to emit the
1857 // diagnostic (mirrors the plain-loop path's 3-cache convention).
1858 Operation *newOp = gutils->getNewFromOriginal(op);
1859 OpBuilder cacheBuilder(newOp);
1860 Location loc = forOp.getLoc();
1861 SmallVector<Value> dummyCaches;
1862 dummyCaches.push_back(gutils->initAndPushCache(
1863 FinalClass::materializeLowerBound(cacheBuilder, loc, forOp, gutils),
1864 cacheBuilder));
1865 dummyCaches.push_back(gutils->initAndPushCache(
1866 FinalClass::materializeUpperBound(cacheBuilder, loc, forOp, gutils),
1867 cacheBuilder));
1868 dummyCaches.push_back(gutils->initAndPushCache(
1869 FinalClass::materializeStep(cacheBuilder, loc, forOp, gutils),
1870 cacheBuilder));
1871 return dummyCaches;
1872 }
1873 return cacheBinomial(forOp, *budget, gutils);
1874 }
1875
1876 if (FinalClass::needsCheckpointing(forOp))
1877 return cachePeriodic(forOp, op, gutils);
1878
1879 return std::nullopt;
1880 }
1881
1882 static std::optional<LogicalResult> tryCreateReverseModeAdjoint(
1883 OpName forOp, Operation *op, OpBuilder &builder,
1884 MGradientUtilsReverse *gutils, SmallVector<Value> caches,
1885 ArrayRef<bool> operandsActive, ArrayRef<Value> incomingGradients) {
1886 if (!FinalClass::needsBinomialCheckpointing(forOp) &&
1887 !FinalClass::needsCheckpointing(forOp))
1888 return std::nullopt;
1889
1890 // Mirrors the guard in tryCacheValues; already reported there, but
1891 // createReverseModeAdjoint must still fail (not fall through to the
1892 // plain-loop path, whose cache layout wouldn't match what cacheValues
1893 // actually cached).
1894 if (failed(FinalClass::requireSingleResultBounds(forOp)))
1895 return failure();
1896
1897 if (FinalClass::needsBinomialCheckpointing(forOp)) {
1898 auto budget = FinalClass::getCheckpointBudget(forOp);
1899 if (!budget || *budget <= 1) {
1900 op->emitError() << "binomial checkpointing requires a "
1901 << FinalClass::checkpointPeriodAttrName()
1902 << " attribute greater than 1";
1903 return failure();
1904 }
1905 return reverseBinomial(forOp, *budget, builder, gutils, caches,
1906 operandsActive, incomingGradients);
1907 }
1908
1909 if (FinalClass::needsCheckpointing(forOp))
1910 return reversePeriodic(forOp, op, builder, gutils, caches, operandsActive,
1911 incomingGradients);
1912
1913 return std::nullopt;
1914 }
1915};
1916
1917} // namespace enzyme
1918} // namespace mlir
1919
1920#endif // ENZYME_MLIR_IMPLEMENTATIONS_LOOPCHECKPOINTING_H
Operation * clone(Operation *src, IRMapping &mapper, Operation::CloneOptions options, std::map< Operation *, Operation * > &opMap)
bool cached
SegmentCacheCreatorGuard(MGradientUtilsReverse *gutils, Operation *anchor)
SegmentCacheCreatorGuard(const SegmentCacheCreatorGuard &)=delete
SegmentCacheCreatorGuard & operator=(const SegmentCacheCreatorGuard &)=delete
mlir::Value diffe(mlir::Value origv, mlir::OpBuilder &builder)
void zeroDiffe(mlir::Value origv, mlir::OpBuilder &builder)
LogicalResult visitChild(Operation *op, OpBuilder &builder, MGradientUtilsReverse *gutils)
Value popCache(Value cache, OpBuilder &builder)
Value initAndPushCache(Value v, OpBuilder &builder)
void deregisterCacheCreatorHook(std::function< std::pair< Value, Value >(Type)> hook)
void registerCacheCreatorHook(std::function< std::pair< Value, Value >(Type)> hook)
void addToDiffe(mlir::Value oldGradient, mlir::Value addedGradient, OpBuilder &builder)
mlir::Type getShadowType(mlir::Type T)
void replaceOrigOpWith(Operation *op, ValueRange vals)
std::map< Operation *, Operation * > originalToNewFnOps
void erase(Operation *op)
mlir::Value invertPointerM(mlir::Value v, OpBuilder &Builder2)
SmallVector< mlir::Value, 1 > getNewFromOriginal(ValueRange originst) const
bool isConstantValue(mlir::Value v) const
void localizeGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *fwd)
static BinomialCacheLayout get(OpName forOp, ArrayRef< Value > mutableRefs, ArrayRef< Value > immutableRefs, bool isDynamic, MGradientUtilsReverse *gutils)
static CacheCreator makeSegmentCacheCreator(MGradientUtilsReverse *gutils, Operation *anchor)
static StringRef checkpointPeriodAttrName()
static void finalizeScaffoldLoop(OpBuilder &b, Location loc, const ScaffoldLoop &loop, ValueRange yields)
static Value storeSlot(OpBuilder &b, Location loc, Value store, Value slot, Value val)
static Value emitCmpLT(OpBuilder &b, Location loc, Value l, Value r)
static size_t getNumRegionIterArgs(OpName forOp)
static MemRefType checkpointBufferType(int64_t budget, Type t)
static void splitOutsideRefs(OpName forOp, SmallVectorImpl< Value > &mutableRefs, SmallVectorImpl< Value > &immutableRefs)
static SmallVector< Value > cacheBinomial(OpName forOp, int64_t budget, MGradientUtilsReverse *gutils)
static bool isDefinedInBlock(Block *b, Value v)
static void primeSegmentGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *origBody, Block *revLoopBody, ArrayRef< bool > operandsActive)
static Value emitCmpEQ(OpBuilder &b, Location loc, Value l, Value r)
static std::optional< LogicalResult > tryCreateReverseModeAdjoint(OpName forOp, Operation *op, OpBuilder &builder, MGradientUtilsReverse *gutils, SmallVector< Value > caches, ArrayRef< bool > operandsActive, ArrayRef< Value > incomingGradients)
static void setInsertionPointToBodyEnd(OpBuilder &builder, Block *block)
static void materializeSegmentValues(OpBuilder &builder, Location loc, PeriodicSchedule &sched)
static void copyBlockWithoutTerminator(OpBuilder &builder, Block *b, MGradientUtilsReverse *gutils, IRMapping &mapping)
static Value createStore(OpBuilder &b, Location loc, int64_t budget, Type valTy)
static Value emitDivU(OpBuilder &b, Location loc, Value l, Value r)
static ValueRange getInits(OpName forOp)
static Value getInductionVar(OpName forOp)
static Value emitSelect(OpBuilder &b, Location loc, Value c, Value t, Value f)
static std::optional< SmallVector< Value > > tryCacheValues(OpName forOp, Operation *op, MGradientUtilsReverse *gutils)
static StringRef binomialCheckpointingAttrName()
static std::optional< int64_t > getCheckpointBudget(OpName forOp)
static Value getNumIterationsValue(OpBuilder &builder, Location loc, OpName forOp, MGradientUtilsReverse *gutils)
static Value segmentLength(OpBuilder &builder, Location loc, const PeriodicSchedule &sched, Value base)
static Value cloneSlot(OpBuilder &builder, Location loc, Value base, Value buf, Value slot, int64_t budget)
static Value emitMul(OpBuilder &b, Location loc, Value l, Value r)
static Block * getBodyBlock(OpName forOp)
static void destroyStore(OpBuilder &b, Location loc, Value store)
static bool needsBinomialCheckpointing(OpName forOp)
static Value loadCheckpoint(OpBuilder &b, Location loc, Value buf, Value slot, Type valTy)
static ResultRange getCarriedResults(Operation *loop)
static Value emitMin(OpBuilder &b, Location loc, Value l, Value r)
static void preserveAttributesButCheckpointing(Operation *newOp, Operation *oldOp)
static void primeStepGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *origBody, Block *revOuterBody, ArrayRef< bool > operandsActive)
static LogicalResult reverseBinomial(OpName forOp, int64_t budget, OpBuilder &builder, MGradientUtilsReverse *gutils, SmallVector< Value > caches, ArrayRef< bool > operandsActive, ArrayRef< Value > incomingGradients)
static SmallVector< Value > cachePeriodic(OpName forOp, Operation *op, MGradientUtilsReverse *gutils)
static bool hasBinomialAttr(OpName forOp)
static LogicalResult reversePeriodic(OpName forOp, Operation *op, OpBuilder &builder, MGradientUtilsReverse *gutils, SmallVector< Value > caches, ArrayRef< bool > operandsActive, ArrayRef< Value > incomingGradients)
static Value checkpointRow(OpBuilder &b, Location loc, Value buf, Value slot, MemRefType rowTy)
static PeriodicSchedule getStaticPeriodicSchedule(OpName forOp)
static void storeCheckpoint(OpBuilder &b, Location loc, Value buf, Value slot, Value val)
static Value loadSlot(OpBuilder &b, Location loc, Value store, Value slot, Type valTy)
static ScaffoldLoop createScaffoldForLoop(OpBuilder &b, Location loc, Value lb, Value ub, Value step, ValueRange inits)
static Value emitConst(OpBuilder &b, Location loc, int64_t v)
static bool checkpointingEnabled(OpName forOp)
static Value castToType(OpBuilder &builder, Location loc, Value v, Type targetType)
static void hoistPlaceholdersBefore(Operation *root, Operation *before)
static void freeCloneSlots(OpBuilder &builder, Value ref, Value buf, int64_t budget)
static Value emitAdd(OpBuilder &b, Location loc, Value l, Value r)
std::function< std::pair< Value, Value >(Type)> CacheCreator
static StringRef enableCheckpointingAttrName()
static OperandRange getCarriedTerminatorOperands(Operation *term)
static bool needsCheckpointing(OpName forOp)
static Type getIndexLikeType(OpBuilder &builder)
static Value emitSub(OpBuilder &b, Location loc, Value l, Value r)
static Value allocCloneSlots(OpBuilder &b, Location loc, int64_t budget, Value proto, ClonableTypeInterface iface)
static void materializeDynamicSchedule(OpBuilder &builder, Location loc, OpName forOp, MGradientUtilsReverse *gutils, PeriodicSchedule &sched)
static Value segmentBase(OpBuilder &builder, Location loc, const PeriodicSchedule &sched, Value index)
static SmallVector< Value > getPrimalResults(OpBuilder &b, Location loc, OpName forOp, ValueRange state, MGradientUtilsReverse *gutils)
static ScaffoldLoop createScaffoldWhileLoop(OpBuilder &b, Location loc, ValueRange inits, llvm::function_ref< Value(OpBuilder &, Location, ValueRange)> cond)