Enzyme main
Loading...
Searching...
No Matches
AffineAutoDiffOpInterfaceImpl.cpp
Go to the documentation of this file.
1//===- AffineAutoDiffOpInterfaceImpl.cpp - Interface external model -------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file contains the external model implementation of the automatic
10// differentiation op interfaces for the upstream MLIR Affine dialect.
11//
12//===----------------------------------------------------------------------===//
13
14#include "Dialect/Ops.h"
19#include "Interfaces/Utils.h"
20#include "Passes/RemovalUtils.h"
21#include "Passes/Utils.h"
22#include "mlir/Dialect/Affine/IR/AffineOps.h"
23#include "mlir/Dialect/Affine/Utils.h"
24#include "mlir/Dialect/MemRef/IR/MemRef.h"
25#include "mlir/Dialect/SCF/IR/SCF.h"
26#include "mlir/IR/IntegerSet.h"
27#include "llvm/ADT/ScopeExit.h"
28
29#include <array>
30
31using namespace mlir;
32using namespace mlir::enzyme;
33using namespace mlir::affine;
34
35namespace {
36
37affine::AffineForOp
38createAffineForWithShadows(Operation *op, OpBuilder &builder,
39 MGradientUtils *gutils, Operation *original,
40 ValueRange remappedOperands, TypeRange rettys) {
41 affine::AffineForOpAdaptor adaptor(remappedOperands,
42 cast<affine::AffineForOp>(original));
43 auto repFor = affine::AffineForOp::create(
44 builder, original->getLoc(), adaptor.getLowerBoundOperands(),
45 adaptor.getLowerBoundMap(), adaptor.getUpperBoundOperands(),
46 adaptor.getUpperBoundMap(), adaptor.getStep().getZExtValue(),
47 // This dance is necessary because the adaptor accessors are based on the
48 // internal attribute containing the number of operands associated with
49 // each named operand group. This attribute is carried over from the
50 // original operation and does not account for the shadow-related iter
51 // args. Instead, assume lower/upper bound operands must not have shadows
52 // since they are integer-typed and take the result of operands as iter
53 // args.
54 remappedOperands.drop_front(adaptor.getLowerBoundOperands().size() +
55 adaptor.getUpperBoundOperands().size()));
56 return repFor;
57}
58
59affine::AffineIfOp createAffineIfWithShadows(Operation *op, OpBuilder &builder,
60 MGradientUtils *gutils,
61 affine::AffineIfOp original,
62 ValueRange remappedOperands,
63 TypeRange rettys) {
64 affine::AffineIfOpAdaptor adaptor(remappedOperands, original);
65 return affine::AffineIfOp::create(
66 builder, original->getLoc(), rettys, original.getIntegerSet(),
67 adaptor.getOperands(), !original.getElseRegion().empty());
68}
69
70struct AffineIfOpInterfaceReverse
71 : public ReverseAutoDiffOpInterface::ExternalModel<
72 AffineIfOpInterfaceReverse, affine::AffineIfOp> {
73 LogicalResult createReverseModeAdjoint(Operation *op, OpBuilder &builder,
75 SmallVector<Value> caches) const {
76 auto ifOp = cast<affine::AffineIfOp>(op);
77 bool hasElse = ifOp.hasElse();
78 Value cond = gutils->popCache(caches[0], builder);
79
80 SmallVector<bool> resultsActive(ifOp.getNumResults(), false);
81 for (int i = 0, e = resultsActive.size(); i < e; ++i) {
82 auto result = ifOp.getResult(i);
83 auto iface = dyn_cast<AutoDiffTypeInterface>(result.getType());
84 bool needsGrad = iface && !iface.isMutable();
85 resultsActive[i] = needsGrad && !gutils->isConstantValue(result);
86 }
87
88 SmallVector<Value> incomingGradients;
89 for (auto &&[active, res] :
90 llvm::zip_equal(resultsActive, ifOp.getResults())) {
91 if (active) {
92 incomingGradients.push_back(gutils->diffe(res, builder));
93 if (!gutils->isConstantValue(res))
94 gutils->zeroDiffe(res, builder);
95 }
96 }
97
98 auto revIf =
99 scf::IfOp::create(builder, ifOp.getLoc(), TypeRange{}, cond, hasElse);
100
101 bool valid = true;
102 for (auto &&[oldReg, newReg] :
103 llvm::zip(op->getRegions(), revIf->getRegions())) {
104 for (auto &&[oBB, revBB] : llvm::zip(oldReg, newReg)) {
105 OpBuilder bodyBuilder(&revBB, revBB.end());
106 bodyBuilder.setInsertionPoint(revBB.getTerminator());
107
108 // All values defined in the body should have no use outside this
109 // block therefore we can set their diffe to zero upon entering the
110 // reverse block to simplify the work of the
111 // remove-unnecessary-enzyme-ops pass.
112 for (auto &it : oBB.getOperations()) {
113 for (auto res : it.getResults()) {
114 if (!gutils->isConstantValue(res)) {
115 auto iface = dyn_cast<AutoDiffTypeInterface>(res.getType());
116 if (iface && !iface.isMutable())
117 gutils->zeroDiffe(res, bodyBuilder);
118 }
119 }
120 }
121
122 auto term = oBB.getTerminator();
123 // Align incomingGradients with their corresponding yield operands.
124 SmallVector<Value> activeTermOperands;
125 activeTermOperands.reserve(incomingGradients.size());
126 for (auto &&[resultActive, operand] :
127 llvm::zip_equal(resultsActive, term->getOperands())) {
128 if (resultActive)
129 activeTermOperands.push_back(operand);
130 }
131
132 for (auto &&[arg, operand] :
133 llvm::zip_equal(incomingGradients, activeTermOperands)) {
134 // Check activity of the argument separately from the result. If
135 // some branches yield inactive values while others yield active
136 // values, the result will be active, but this operand may still be
137 // inactive (and we cannot addToDiffe)
138 if (!gutils->isConstantValue(operand)) {
139 gutils->addToDiffe(operand, arg, bodyBuilder);
140 }
141 }
142
143 auto first = oBB.rbegin();
144 first++; // skip terminator
145
146 auto last = oBB.rend();
147
148 for (auto it = first; it != last; ++it) {
149 Operation *op = &*it;
150 valid &=
151 gutils->Logic.visitChild(op, bodyBuilder, gutils).succeeded();
152 }
153 }
154 }
155 return success(valid);
156 }
157
158 SmallVector<Value> cacheValues(Operation *op,
159 MGradientUtilsReverse *gutils) const {
160 auto ifOp = cast<affine::AffineIfOp>(op);
161 auto is = ifOp.getCondition();
162
163 auto newOp = gutils->getNewFromOriginal(op);
164 OpBuilder cacheBuilder(newOp);
165
166 Value cond = nullptr;
167
168 Value zero = arith::ConstantIndexOp::create(cacheBuilder, ifOp.getLoc(), 0);
169
170 auto getNewFromOriginal = [&](Value v) {
171 return gutils->getNewFromOriginal(v);
172 };
173
174 SmallVector<Value> dims = llvm::map_to_vector(
175 ifOp->getOperands().take_front(is.getNumDims()), getNewFromOriginal);
176 SmallVector<Value> symbols = llvm::map_to_vector(
177 ifOp->getOperands().drop_front(is.getNumDims()), getNewFromOriginal);
178
179 for (auto [eq, E] : llvm::zip_equal(is.getEqFlags(), is.getConstraints())) {
180 Value lhs = affine::expandAffineExpr(cacheBuilder, ifOp.getLoc(), E, dims,
181 symbols);
182 Value constraintCond = arith::CmpIOp::create(
183 cacheBuilder, ifOp.getLoc(),
184 eq ? arith::CmpIPredicate::eq : arith::CmpIPredicate::sge, lhs, zero);
185
186 if (cond) {
187 cond = arith::AndIOp::create(cacheBuilder, ifOp.getLoc(),
188 constraintCond, cond);
189 } else {
190 cond = constraintCond;
191 }
192 }
193
194 cond = cond ? cond
195 : arith::ConstantIntOp::create(cacheBuilder, ifOp.getLoc(),
196 cacheBuilder.getI1Type(),
197 /*value=*/1);
198
199 return {gutils->initAndPushCache(cond, cacheBuilder)};
200 }
201
202 LogicalResult createShadowValues(Operation *op, OpBuilder &builder,
203 MGradientUtilsReverse *gutils) const {
204 return success();
205 }
206};
207
208// The reverse of an affine.if is an scf.if (see AffineIfOpInterfaceReverse
209// above), so the reverse if op searched for by the min-cut cache logic below
210// is an scf::IfOp rather than an affine::AffineIfOp.
211struct AffineIfOpEnzymeOpsRemover
212 : public IfLikeEnzymeOpsRemover<AffineIfOpEnzymeOpsRemover,
213 affine::AffineIfOp, scf::IfOp> {
214 static Block *getThenBlock(affine::AffineIfOp ifOp, OpBuilder &builder) {
215 return ifOp.getThenBlock();
216 }
217
218 static Block *getElseBlock(affine::AffineIfOp ifOp, OpBuilder &builder) {
219 // Ensure the if has an else block
220 if (ifOp.getElseRegion().empty()) {
221 OpBuilder::InsertionGuard guard(builder);
222 Block &newBlock = ifOp.getElseRegion().emplaceBlock();
223 builder.setInsertionPointToStart(&newBlock);
224 affine::AffineYieldOp::create(builder, ifOp.getLoc());
225 }
226
227 return ifOp.getElseBlock();
228 }
229
230 static Block *getThenBlock(scf::IfOp ifOp, OpBuilder &builder) {
231 return ifOp.thenBlock();
232 }
233
234 static Block *getElseBlock(scf::IfOp ifOp, OpBuilder &builder) {
235 // Ensure the if has an else block
236 if (ifOp.getElseRegion().empty()) {
237 OpBuilder::InsertionGuard guard(builder);
238 Block &newBlock = ifOp.getElseRegion().emplaceBlock();
239 builder.setInsertionPointToStart(&newBlock);
240 scf::YieldOp::create(builder, ifOp.getLoc());
241 }
242
243 return ifOp.elseBlock();
244 }
245
246 static Value getDummyValue(OpBuilder &builder, Location loc, Type dummyType) {
247 return cast<AutoDiffTypeInterface>(dummyType).createNullValue(builder, loc);
248 }
249
250 static affine::AffineIfOp replace(PatternRewriter &rewriter,
251 affine::AffineIfOp otherIfOp,
252 TypeRange resultTypes) {
253 auto newIf = affine::AffineIfOp::create(
254 rewriter, otherIfOp->getLoc(), resultTypes, otherIfOp.getIntegerSet(),
255 otherIfOp->getOperands(), /*withElseRegion=*/true);
256
257 newIf.getThenRegion().takeBody(otherIfOp.getThenRegion());
258 newIf.getElseRegion().takeBody(otherIfOp.getElseRegion());
259
260 rewriter.replaceAllUsesWith(
261 otherIfOp->getResults(),
262 newIf->getResults().slice(0, otherIfOp->getNumResults()));
263 rewriter.eraseOp(otherIfOp);
264 return newIf;
265 }
266};
267
268affine::AffineParallelOp
269createAffineParallelWithShadows(Operation *op, OpBuilder &builder,
270 MGradientUtils *gutils,
271 affine::AffineParallelOp original,
272 ValueRange remappedOperands, TypeRange rettys) {
273 // A result of an affine.parallel is a reduction, and a reduction of the
274 // tangents is the derivative of a sum. Anything else needs its own rule,
275 // which reverse mode does not have either.
276 SmallVector<Attribute> reductions;
277 for (auto &&[reduction, result] :
278 llvm::zip_equal(original.getReductions(), original.getResults())) {
279 reductions.push_back(reduction);
280 if (gutils->isConstantValue(result))
281 continue;
282 auto kind = cast<arith::AtomicRMWKindAttr>(reduction).getValue();
283 if (kind != arith::AtomicRMWKind::addf &&
284 kind != arith::AtomicRMWKind::addi)
285 original.emitError() << "forward mode of an active "
286 << stringifyEnum(kind)
287 << " reduction is not yet implemented";
288 reductions.push_back(reduction);
289 }
290 auto reductionsAttr = builder.getArrayAttr(reductions);
291 return affine::AffineParallelOp::create(
292 builder, original->getLoc(), rettys, reductionsAttr,
293 original.getLowerBoundsMapAttr(), original.getLowerBoundsGroupsAttr(),
294 original.getUpperBoundsMapAttr(), original.getUpperBoundsGroupsAttr(),
295 original.getStepsAttr(), remappedOperands);
296}
297
298struct AffineForOpInterfaceReverse
299 : public ReverseAutoDiffOpInterface::ExternalModel<
300 AffineForOpInterfaceReverse, affine::AffineForOp>,
301 public LoopCheckpointing<AffineForOpInterfaceReverse,
302 affine::AffineForOp> {
303 // ---- hooks required by LoopCheckpointing<AffineForOpInterfaceReverse,
304 // affine::AffineForOp> ----
305
306 static std::optional<int64_t>
307 getConstantNumberOfIterations(affine::AffineForOp forOp) {
308 if (!forOp.hasConstantLowerBound())
309 return std::nullopt;
310 if (!forOp.hasConstantUpperBound())
311 return std::nullopt;
312 return (forOp.getConstantUpperBound() - forOp.getConstantLowerBound()) /
313 forOp.getStepAsInt();
314 }
315
316 static Value materializeLowerBound(OpBuilder &builder, Location loc,
317 affine::AffineForOp forOp,
318 MGradientUtilsReverse *gutils) {
319 SmallVector<Value> ops =
320 llvm::map_to_vector(forOp.getLowerBoundOperands(), [&](Value v) {
321 return gutils->getNewFromOriginal(v);
322 });
323 return affine::AffineApplyOp::create(builder, loc, forOp.getLowerBoundMap(),
324 ops);
325 }
326
327 static Value materializeUpperBound(OpBuilder &builder, Location loc,
328 affine::AffineForOp forOp,
329 MGradientUtilsReverse *gutils) {
330 SmallVector<Value> ops =
331 llvm::map_to_vector(forOp.getUpperBoundOperands(), [&](Value v) {
332 return gutils->getNewFromOriginal(v);
333 });
334 return affine::AffineApplyOp::create(builder, loc, forOp.getUpperBoundMap(),
335 ops);
336 }
337
338 static Value materializeStep(OpBuilder &builder, Location loc,
339 affine::AffineForOp forOp,
341 return arith::ConstantIndexOp::create(builder, loc, forOp.getStepAsInt());
342 }
343
344 static int64_t getConstantStart(affine::AffineForOp forOp) {
345 return forOp.getConstantLowerBound();
346 }
347
348 static int64_t getConstantStep(affine::AffineForOp forOp) {
349 return forOp.getStepAsInt();
350 }
351
352 static LogicalResult requireSingleResultBounds(affine::AffineForOp forOp) {
353 if (forOp.getLowerBound().getMap().getNumResults() != 1 ||
354 forOp.getUpperBound().getMap().getNumResults() != 1)
355 return forOp->emitError()
356 << "cannot differentiate loop with minmax bounds yet";
357 return success();
358 }
359
360 // The checkpointing scaffold substitutes a generically-computed index
361 // Value for the loop's own induction variable, which is not a valid
362 // affine dimension/symbol (it isn't the IV of an affine.for/parallel, nor
363 // a top-level value). Cloning an affine.load/affine.store that indexes
364 // through it verbatim would fail the verifier ("operand cannot be used as
365 // a dimension id"), so instead expand its index map into plain arithmetic
366 // (valid for any index Value, affine or not) via affine::expandAffineMap
367 // -- the same primitive the upstream -lower-affine pass uses -- and emit
368 // memref.load/memref.store. Every other op clones as-is.
369 static void cloneOp(OpBuilder &builder, Operation &op, IRMapping &mapping) {
370 if (auto loadOp = dyn_cast<affine::AffineLoadOp>(&op)) {
371 SmallVector<Value> operands =
372 llvm::map_to_vector(loadOp.getIndices(), [&](Value v) {
373 return mapping.lookupOrDefault(v);
374 });
375 auto indices = affine::expandAffineMap(builder, loadOp.getLoc(),
376 loadOp.getAffineMap(), operands);
377 assert(indices && "failed to expand affine.load's index map");
378 Value memref = mapping.lookupOrDefault(loadOp.getMemref());
379 auto newLoadOp =
380 memref::LoadOp::create(builder, loadOp.getLoc(), memref, *indices);
381 newLoadOp.setAlignmentAttr(
382 loadOp->getAttrOfType<IntegerAttr>("alignment"));
383 Operation *newLoad = newLoadOp;
384 mapping.map(loadOp.getResult(), newLoad->getResult(0));
385 // IRMapping tracks an operation map alongside its value map (populated
386 // automatically by builder.clone, which this bypasses); callers such
387 // as publishClonedStep/publishClonedOps and, later,
388 // gutils->getNewFromOriginal(originalLoadOp) (used by
389 // AffineLoadOpInterfaceReverse::cacheValues) all key off it, so it must
390 // be registered explicitly here.
391 mapping.map(&op, newLoad);
392 return;
393 }
394 if (auto storeOp = dyn_cast<affine::AffineStoreOp>(&op)) {
395 SmallVector<Value> operands =
396 llvm::map_to_vector(storeOp.getIndices(), [&](Value v) {
397 return mapping.lookupOrDefault(v);
398 });
399 auto indices = affine::expandAffineMap(builder, storeOp.getLoc(),
400 storeOp.getAffineMap(), operands);
401 assert(indices && "failed to expand affine.store's index map");
402 Value memref = mapping.lookupOrDefault(storeOp.getMemref());
403 Value value = mapping.lookupOrDefault(storeOp.getValue());
404 auto newStoreOp = memref::StoreOp::create(builder, storeOp.getLoc(),
405 value, memref, *indices);
406 newStoreOp.setAlignmentAttr(
407 storeOp->getAttrOfType<IntegerAttr>("alignment"));
408 Operation *newStore = newStoreOp;
409 mapping.map(&op, newStore);
410 return;
411 }
412 builder.clone(op, mapping);
413 }
414
415 // ---- periodic-scaffold hooks (see LoopCheckpointing.h doc comment) ----
416 // Unlike binomial, periodic's decomposition is entirely compile-time
417 // constant, so its scaffold can be built as genuine affine.for -- keeping
418 // any affine.load/affine.store cloned into it valid without needing
419 // cloneOp's memref-lowering fallback.
420
421 // An affine.for's bounds are AffineMaps, i.e. compile-time constants plus
422 // dimensions valid in the enclosing affine scope; a runtime trip count is
423 // neither, so a dynamic loop falls back to the plain reverse path.
424 static bool supportsDynamicPeriodic() { return false; }
425
426 static affine::AffineForOp
427 createConstantScaffoldLoop(OpBuilder &builder, Location loc, int64_t lb,
428 int64_t ub, int64_t step, ValueRange inits) {
429 return affine::AffineForOp::create(builder, loc, lb, ub, step, inits);
430 }
431
432 // Both outer loops count segments: one iteration per checkpoint, so the trip
433 // count is the stated period. Every bound and index below is an affine
434 // expression in that segment index.
435 static affine::AffineForOp
436 createForwardOuterLoop(OpBuilder &builder, Location loc,
437 const PeriodicSchedule &sched, ValueRange inits) {
438 return createConstantScaffoldLoop(builder, loc, 0, sched.numSegments(), 1,
439 inits);
440 }
441
442 static affine::AffineForOp
443 createReverseOuterLoop(OpBuilder &builder, Location loc,
444 const PeriodicSchedule &sched, ValueRange inits) {
445 return createConstantScaffoldLoop(builder, loc, 0, sched.numSegments(), 1,
446 inits);
447 }
448
449 static void materializeSegmentValues(OpBuilder &, Location,
450 PeriodicSchedule &sched) {
451 assert(!sched.isDynamic() &&
452 "affine.for has no dynamic periodic path -- see "
453 "supportsDynamicPeriodic");
454 }
455
456 static SmallVector<Value>
457 computeForwardSegmentHint(OpBuilder &, Location, Value,
458 const PeriodicSchedule &) {
459 return {};
460 }
461
462 // Bound = min(nInner, numIters - nInner * j), for segment index j: the
463 // standard affine-tiling idiom for a boundary tile, expressed as a
464 // multi-result upper-bound map (the "min" keyword documented on affine.for).
465 // Equivalent to scf.for's cmpi+select formula in every case: the two only
466 // disagree where neither formula's boundary case is active.
467 static affine::AffineForOp
468 createForwardSegmentLoop(OpBuilder &builder, Location loc, Value outerIV,
469 ArrayRef<Value> /*fwdHint*/,
470 const PeriodicSchedule &sched, ValueRange inits) {
471 int64_t nInner = sched.nInner, nOuter = sched.nOuter,
472 trailingIters = sched.trailingIters;
473 int64_t numIters = nInner * nOuter + trailingIters;
474 MLIRContext *ctx = builder.getContext();
475 AffineExpr j = builder.getAffineDimExpr(0);
476 AffineExpr nInnerExpr = builder.getAffineConstantExpr(nInner);
477 AffineExpr remainingExpr =
478 builder.getAffineConstantExpr(numIters) - nInnerExpr * j;
479 AffineMap ubMap = AffineMap::get(1, 0, {nInnerExpr, remainingExpr}, ctx);
480 return affine::AffineForOp::create(
481 builder, loc, /*lbOperands=*/ValueRange{},
482 AffineMap::getConstantMap(0, ctx), ValueRange{outerIV}, ubMap,
483 /*step=*/1, inits);
484 }
485
486 // Reverse counterpart. `outerIV` (call it j) is the reverse outer loop's own
487 // segment counter, so the forward segment being replayed is
488 // k = (numSegments - 1) - j, its base is segmentBase = k * nInner, and the
489 // bound is min(nInner, numIters - segmentBase), expanded into one affine map
490 // of j.
491 //
492 // A min bound cannot express a trailing segment longer than nInner -- it
493 // would clamp it to nInner and leave the tail of the loop unreplayed -- which
494 // is why getStaticPeriodicSchedule keeps the remainder the shorter part of
495 // the split.
496 static affine::AffineForOp
497 createReverseSegmentLoop(OpBuilder &builder, Location loc, Value outerIV,
498 ArrayRef<Value> /*revHint*/,
499 const PeriodicSchedule &sched, ValueRange inits) {
500 int64_t nInner = sched.nInner, nOuter = sched.nOuter,
501 trailingIters = sched.trailingIters;
502 int64_t numIters = nInner * nOuter + trailingIters;
503 int64_t s = nOuter + (trailingIters > 0 ? 1 : 0);
504 // numIters - segmentBase, segmentBase = ((s-1) - j) * nInner
505 // = numIters - (s-1)*nInner + nInner*j = constOffset + nInner*j
506 int64_t constOffset = numIters - (s - 1) * nInner;
507 MLIRContext *ctx = builder.getContext();
508 AffineExpr j = builder.getAffineDimExpr(0);
509 AffineExpr nInnerExpr = builder.getAffineConstantExpr(nInner);
510 AffineExpr remainingExpr =
511 builder.getAffineConstantExpr(constOffset) + nInnerExpr * j;
512 AffineMap ubMap = AffineMap::get(1, 0, {nInnerExpr, remainingExpr}, ctx);
513 return affine::AffineForOp::create(
514 builder, loc, /*lbOperands=*/ValueRange{},
515 AffineMap::getConstantMap(0, ctx), ValueRange{outerIV}, ubMap,
516 /*step=*/1, inits);
517 }
518
519 static affine::AffineForOp
520 createLoopWithSameBounds(OpBuilder &builder, Location loc,
521 affine::AffineForOp templateLoop, ValueRange inits) {
522 return affine::AffineForOp::create(
523 builder, loc, templateLoop.getLowerBoundOperands(),
524 templateLoop.getLowerBoundMap(), templateLoop.getUpperBoundOperands(),
525 templateLoop.getUpperBoundMap(), templateLoop.getStepAsInt(), inits);
526 }
527
528 // (nInner * j + localIV) * step + start, as one affine.apply -- the whole
529 // chain must be built as a single affine expression (not the generic
530 // arith ops scf.for's implementation uses) so the result is itself a valid
531 // affine dimension, usable as an index by any affine.load/affine.store
532 // cloned with it substituted in for forOp's own induction variable.
533 static Value computeForwardSegmentIV(OpBuilder &builder, Location loc,
534 affine::AffineForOp forOp, Value outerIV,
535 Value localIV,
536 const PeriodicSchedule &sched,
537 ArrayRef<Value> /*fwdHint*/) {
538 int64_t step = getConstantStep(forOp);
539 int64_t start = getConstantStart(forOp);
540 MLIRContext *ctx = builder.getContext();
541 AffineExpr j = builder.getAffineDimExpr(0),
542 localIdx = builder.getAffineDimExpr(1);
543 AffineExpr expr = (j * sched.nInner + localIdx) * step + start;
544 AffineMap map = AffineMap::get(2, 0, {expr}, ctx);
545 return affine::AffineApplyOp::create(builder, loc, map,
546 ValueRange{outerIV, localIV});
547 }
548
549 // (segmentBase + localIV) * step + start, with segmentBase = nInner *
550 // ((numSegments - 1) - j) -- see createReverseSegmentLoop.
551 static SmallVector<Value>
552 computeReverseSegmentHint(OpBuilder &builder, Location loc, Value outerIV,
553 const PeriodicSchedule &) {
554 return {};
555 }
556
557 static Value computeReverseSegmentIV(OpBuilder &builder, Location loc,
558 affine::AffineForOp forOp, Value outerIV,
559 Value localIV,
560 const PeriodicSchedule &sched,
561 ArrayRef<Value> /*revHint*/) {
562 int64_t nInner = sched.nInner;
563 int64_t s = sched.nOuter + (sched.trailingIters > 0 ? 1 : 0);
564 int64_t lastSegmentBase = (s - 1) * nInner;
565 int64_t step = getConstantStep(forOp);
566 int64_t start = getConstantStart(forOp);
567 MLIRContext *ctx = builder.getContext();
568 AffineExpr j = builder.getAffineDimExpr(0),
569 localIdx = builder.getAffineDimExpr(1);
570 AffineExpr segmentBase = lastSegmentBase - j * nInner;
571 AffineExpr flatIV = segmentBase + localIdx;
572 AffineExpr expr = flatIV * step + start;
573 AffineMap map = AffineMap::get(2, 0, {expr}, ctx);
574 return affine::AffineApplyOp::create(builder, loc, map,
575 ValueRange{outerIV, localIV});
576 }
577
578 static void createScaffoldYield(OpBuilder &builder, Location loc,
579 ValueRange operands) {
580 affine::AffineYieldOp::create(builder, loc, operands);
581 }
582
583 LogicalResult createReverseModeAdjoint(Operation *op, OpBuilder &builder,
584 MGradientUtilsReverse *gutils,
585 SmallVector<Value> caches) const {
586 auto forOp = cast<affine::AffineForOp>(op);
587
588 affine::AffineBound lb = forOp.getLowerBound();
589 affine::AffineBound ub = forOp.getUpperBound();
590
591 if (lb.getMap().getNumResults() != 1 || ub.getMap().getNumResults() != 1) {
592 op->emitError() << "cannot differentiate loop with minmax bounds yet";
593 return failure();
594 }
595
596 SmallVector<bool> operandsActive;
597 for (auto [operand, result] : llvm::zip_equal(
598 op->getOperands().slice(forOp.getNumControlOperands(),
599 forOp->getNumOperands() -
600 forOp.getNumControlOperands()),
601 op->getResults())) {
602 operandsActive.push_back(!gutils->isConstantValue(operand) ||
603 !gutils->isConstantValue(result));
604 }
605
606 // Checkpointing dispatch happens before touching `caches` at all: the
607 // checkpointed schemes cached a completely different layout (see
608 // LoopCheckpointing::cacheBinomial/cachePeriodic) than the plain-loop
609 // path below, so caches[i] must not be popped as a control operand
610 // unless we already know the plain path is the one that applies.
611 if (needsBinomialCheckpointing(forOp) || needsCheckpointing(forOp)) {
612 SmallVector<Value> incomingGradients;
613 for (auto &&[active, res] :
614 llvm::zip_equal(operandsActive, op->getResults())) {
615 if (active) {
616 incomingGradients.push_back(gutils->diffe(res, builder));
617 if (!gutils->isConstantValue(res))
618 gutils->zeroDiffe(res, builder);
619 }
620 }
621 if (auto r =
622 tryCreateReverseModeAdjoint(forOp, op, builder, gutils, caches,
623 operandsActive, incomingGradients))
624 return *r;
625 }
626
627 // ---- plain (non-checkpointed) reverse path, unchanged ----
628
629 SmallVector<Value> revLBOperands, revUBOperands, incomingGradients;
630
631 for (int i = 0, e = lb.getNumOperands(); i < e; ++i) {
632 revLBOperands.push_back(gutils->popCache(caches[i], builder));
633 }
634
635 for (int i = lb.getNumOperands(), e = forOp.getNumControlOperands(); i < e;
636 ++i) {
637 revUBOperands.push_back(gutils->popCache(caches[i], builder));
638 }
639
640 for (auto &&[active, res] :
641 llvm::zip_equal(operandsActive, op->getResults())) {
642 if (active) {
643 incomingGradients.push_back(gutils->diffe(res, builder));
644 if (!gutils->isConstantValue(res))
645 gutils->zeroDiffe(res, builder);
646 }
647 }
648
649 auto revFor = affine::AffineForOp::create(
650 builder, op->getLoc(), revLBOperands, lb.getMap(), revUBOperands,
651 ub.getMap(), forOp.getStepAsInt(), incomingGradients);
652 preserveAttributesButCheckpointing(revFor, op);
653
654 bool valid = true;
655 for (auto &&[oldReg, newReg] :
656 llvm::zip(op->getRegions(), revFor->getRegions())) {
657 for (auto &&[oBB, revBB] : llvm::zip(oldReg, newReg)) {
658 OpBuilder bodyBuilder(&revBB, revBB.end());
659
660 // Create implicit terminator if not present (when num results > 0)
661 if (revBB.empty()) {
662 affine::AffineYieldOp::create(bodyBuilder, revFor->getLoc());
663 }
664 bodyBuilder.setInsertionPoint(revBB.getTerminator());
665
666 // All values defined in the body should have no use outside this block
667 // therefore we can set their diffe to zero upon entering the reverse
668 // block to simplify the work of the remove-unnecessary-enzyme-ops pass.
669 for (auto operand : oBB.getArguments().slice(1)) {
670 if (!gutils->isConstantValue(operand)) {
671 gutils->zeroDiffe(operand, bodyBuilder);
672 }
673 }
674
675 for (auto &it : oBB.getOperations()) {
676 for (auto res : it.getResults()) {
677 if (!gutils->isConstantValue(res)) {
678 auto iface = dyn_cast<AutoDiffTypeInterface>(res.getType());
679 if (iface && !iface.isMutable())
680 gutils->zeroDiffe(res, bodyBuilder);
681 }
682 }
683 }
684
685 auto term = oBB.getTerminator();
686
687 unsigned argIdx = 1; // Skip over the reversed IV
688 for (auto &&[active, operand] :
689 llvm::zip_equal(operandsActive, term->getOperands())) {
690 if (active) {
691 // Set diffe here, not add because it should not accumulate across
692 // iterations. Instead the new gradient for this operand is passed
693 // in the return of the reverse for body.
694 gutils->setDiffe(operand, revBB.getArgument(argIdx), bodyBuilder);
695 argIdx++;
696 }
697 }
698
699 auto first = oBB.rbegin();
700 first++; // skip terminator
701
702 auto last = oBB.rend();
703
704 for (auto it = first; it != last; ++it) {
705 Operation *op = &*it;
706 valid &=
707 gutils->Logic.visitChild(op, bodyBuilder, gutils).succeeded();
708 }
709
710 SmallVector<Value> newResults;
711 newResults.reserve(incomingGradients.size());
712
713 for (auto &&[active, arg] :
714 llvm::zip_equal(operandsActive, oBB.getArguments().slice(1))) {
715 if (active) {
716 newResults.push_back(gutils->diffe(arg, bodyBuilder));
717 if (!gutils->isConstantValue(arg))
718 gutils->zeroDiffe(arg, bodyBuilder);
719 }
720 }
721
722 // yield new gradient values
723 revBB.getTerminator()->setOperands(newResults);
724 }
725 }
726
727 unsigned resIdx = 0;
728 for (auto &&[active, arg] :
729 llvm::zip_equal(operandsActive, forOp.getInits())) {
730 if (active) {
731 if (!gutils->isConstantValue(arg)) {
732 gutils->addToDiffe(arg, revFor.getResult(resIdx), builder);
733 resIdx++;
734 }
735 }
736 }
737
738 return success(valid);
739 }
740
741 SmallVector<Value> cacheValues(Operation *op,
742 MGradientUtilsReverse *gutils) const {
743 auto forOp = cast<affine::AffineForOp>(op);
744
745 if (auto r = tryCacheValues(forOp, op, gutils))
746 return *r;
747
748 SmallVector<Value> caches;
749 OpBuilder cacheBuilder(gutils->getNewFromOriginal(op));
750 for (auto operand : forOp.getControlOperands()) {
751 caches.push_back(gutils->initAndPushCache(
752 gutils->getNewFromOriginal(operand), cacheBuilder));
753 }
754
755 return caches;
756 }
757
758 LogicalResult createShadowValues(Operation *op, OpBuilder &builder,
759 MGradientUtilsReverse *gutils) const {
760 return success();
761 }
762};
763
764struct AffineParallelOpInterfaceReverse
765 : public ReverseAutoDiffOpInterface::ExternalModel<
766 AffineParallelOpInterfaceReverse, affine::AffineParallelOp> {
767 LogicalResult createReverseModeAdjoint(Operation *op, OpBuilder &builder,
768 MGradientUtilsReverse *gutils,
769 SmallVector<Value> caches) const {
770 auto parOp = cast<affine::AffineParallelOp>(op);
771 if (!parOp.getReductions().empty()) {
772 return parOp.emitError() << "parallel reductions not yet implemented";
773 }
774 if (parOp.hasMinMaxBounds()) {
775 return parOp.emitError() << "minmax bounds not yet supported";
776 }
777
778 SmallVector<Value> bounds = llvm::map_to_vector(
779 caches, [&](Value cache) { return gutils->popCache(cache, builder); });
780 auto revPar = affine::AffineParallelOp::create(
781 builder, op->getLoc(), parOp.getResultTypes(), parOp.getReductions(),
782 parOp.getLowerBoundsMap(), parOp.getLowerBoundsGroups(),
783 parOp.getUpperBoundsMap(), parOp.getUpperBoundsGroups(),
784 parOp.getSteps(), bounds);
785
786 // Create the body block and terminator
787 OpBuilder::InsertionGuard guard(builder);
788 SmallVector<Type> ivTypes(parOp.getIVs().size(), builder.getIndexType());
789 SmallVector<Location> ivLocs(parOp.getIVs().size(), parOp.getLoc());
790 builder.createBlock(&revPar.getBodyRegion(), revPar.getBodyRegion().begin(),
791 ivTypes, ivLocs);
792 affine::AffineYieldOp::create(builder, parOp.getLoc());
793
794 bool valid = true;
795 bool wasAtomic = gutils->AtomicAdd;
796 gutils->AtomicAdd = true;
797
798 {
799 Block *oBB = parOp.getBody();
800 Block *rBB = revPar.getBody();
801
802 OpBuilder bodyBuilder = revPar.getBodyBuilder();
803
804 bodyBuilder.setInsertionPointToStart(revPar.getBody());
805 mlir::enzyme::localizeGradients(bodyBuilder, gutils, oBB);
806
807 bodyBuilder.setInsertionPoint(rBB->getTerminator());
808
809 auto first = oBB->rbegin();
810 first++; // skip terminator
811
812 auto last = oBB->rend();
813
814 for (auto it = first; it != last; ++it) {
815 Operation *op = &*it;
816 valid &= gutils->Logic.visitChild(op, bodyBuilder, gutils).succeeded();
817 }
818 }
819
820 gutils->AtomicAdd = wasAtomic;
821 return success(valid);
822 }
823
824 SmallVector<Value> cacheValues(Operation *op,
825 MGradientUtilsReverse *gutils) const {
826 auto parOp = cast<affine::AffineParallelOp>(op);
827
828 SmallVector<Value> caches;
829 OpBuilder cacheBuilder(gutils->getNewFromOriginal(op));
830 for (auto operand : parOp.getMapOperands()) {
831 caches.push_back(gutils->initAndPushCache(
832 gutils->getNewFromOriginal(operand), cacheBuilder));
833 }
834 return caches;
835 }
836
837 LogicalResult createShadowValues(Operation *op, OpBuilder &builder,
838 MGradientUtilsReverse *gutils) const {
839 return success();
840 }
841};
842
843struct AffineParallelOpEnzymeOpsRemover
844 : public ForLikeEnzymeOpsRemover<AffineParallelOpEnzymeOpsRemover,
845 affine::AffineParallelOp> {
846 static SmallVector<IntOrValue, 1>
847 getDimensionBounds(OpBuilder &builder, affine::AffineParallelOp parOp) {
848 SmallVector<IntOrValue, 1> bounds;
849 auto ranges = parOp.getConstantRanges();
850 if (ranges) {
851 for (auto &&[r, step] : llvm::zip(*ranges, parOp.getSteps())) {
852 bounds.push_back(r / step);
853 }
854 } else {
855 for (auto &&[dim, step] : llvm::enumerate(parOp.getSteps())) {
856 auto lb = AffineApplyOp::create(builder, parOp.getLoc(),
857 parOp.getLowerBoundMap(dim),
858 parOp.getLowerBoundsOperands());
859 auto ub = AffineApplyOp::create(builder, parOp.getLoc(),
860 parOp.getUpperBoundMap(dim),
861 parOp.getUpperBoundsOperands());
862 Value diff = arith::SubIOp::create(builder, parOp.getLoc(), ub, lb);
863 if (step != 1) {
864 Value stepVal =
865 arith::ConstantIndexOp::create(builder, parOp.getLoc(), step);
866 diff = arith::DivUIOp::create(builder, parOp.getLoc(), diff, stepVal);
867 }
868 bounds.push_back(diff);
869 }
870 }
871 return bounds;
872 }
873
874 static SmallVector<Value> computeReversedIndices(
875 PatternRewriter &rewriter, affine::AffineParallelOp parOp,
876 ArrayRef<Value> otherInductionVariable, ArrayRef<IntOrValue> bounds) {
877 return SmallVector<Value>(otherInductionVariable);
878 }
879
880 static SmallVector<Value>
881 getCanonicalLoopIVs(OpBuilder &builder, affine::AffineParallelOp parOp) {
882 SmallVector<Value> ivs(parOp.getIVs());
883 for (auto &&[dim, step] : llvm::enumerate(parOp.getSteps())) {
884 Value iv = ivs[dim];
885 auto lbMap = parOp.getLowerBoundMap(dim);
886 if (!(lbMap.isSingleConstant() && lbMap.getSingleConstantResult() == 0)) {
887 auto lb = AffineApplyOp::create(builder, parOp.getLoc(), lbMap,
888 parOp.getLowerBoundsOperands());
889 iv = arith::SubIOp::create(builder, parOp.getLoc(), iv, lb);
890 }
891
892 if (step != 1) {
893 auto stepVal =
894 arith::ConstantIndexOp::create(builder, parOp.getLoc(), step);
895 iv = arith::DivUIOp::create(builder, parOp.getLoc(), iv, stepVal);
896 }
897
898 ivs[dim] = iv;
899 }
900 return ivs;
901 }
902
903 static IRMapping createArgumentMap(PatternRewriter &rewriter,
904 affine::AffineParallelOp parOp,
905 ArrayRef<Value> indPar,
906 affine::AffineParallelOp otherParOp,
907 ArrayRef<Value> indOther) {
908 IRMapping map;
909 for (auto &&[f, o] : llvm::zip_equal(indPar, indOther))
910 map.map(f, o);
911
912 for (auto &&[fiv, oiv] :
913 llvm::zip_equal(parOp.getIVs(), otherParOp.getIVs())) {
914 if (!map.contains(fiv)) {
915 assert(parOp.getLowerBoundsMap() == otherParOp.getLowerBoundsMap());
916 for (auto &&[f, o] :
917 llvm::zip_equal(parOp.getLowerBoundsOperands(),
918 otherParOp.getLowerBoundsOperands())) {
919 (void)f;
920 (void)o;
921 assert(Equivalent(f, o));
922 }
923 for (auto [fstep, ostep] :
924 llvm::zip_equal(parOp.getSteps(), otherParOp.getSteps())) {
925 (void)fstep;
926 (void)ostep;
927 assert(fstep == ostep);
928 }
929 map.map(fiv, oiv);
930 }
931 }
932 return map;
933 }
934
935 static affine::AffineParallelOp
936 replaceWithNewOperands(PatternRewriter &rewriter,
937 affine::AffineParallelOp otherParOp,
938 ArrayRef<Value> operands) {
939 SmallVector<mlir::Attribute> reductionKinds(
940 otherParOp.getReductions().begin(), otherParOp.getReductions().end());
941
942 for (unsigned i = otherParOp->getNumOperands(); i < operands.size(); i++) {
943 reductionKinds.push_back(arith::AtomicRMWKindAttr::get(
944 otherParOp.getContext(), arith::AtomicRMWKind::addf));
945 }
946
947 ValueRange operands_(operands);
948 auto newOtherParOp = affine::AffineParallelOp::create(
949 rewriter, otherParOp.getLoc(), operands_.getTypes(),
950 ArrayAttr::get(otherParOp.getContext(), reductionKinds),
951 otherParOp.getLowerBoundsMap(), otherParOp.getLowerBoundsGroups(),
952 otherParOp.getUpperBoundsMap(), otherParOp.getUpperBoundsGroups(),
953 otherParOp.getSteps(), otherParOp.getMapOperands());
954
955 newOtherParOp->setDiscardableAttrs(
956 otherParOp->getDiscardableAttrDictionary());
957
958 newOtherParOp.getRegion().takeBody(otherParOp.getRegion());
959 rewriter.replaceOp(otherParOp, newOtherParOp->getResults().slice(
960 0, otherParOp->getNumResults()));
961 return newOtherParOp;
962 }
963
964 static ValueRange getInits(affine::AffineParallelOp parOp) {
965 return parOp.getInits();
966 }
967
968 static bool mustPostAdd(affine::AffineParallelOp forOp) { return true; }
969
970 static Value initialValueInBlock(OpBuilder &builder, Block *body,
971 Value grad) {
972 OpBuilder::InsertionGuard guard(builder);
973 builder.setInsertionPointToStart(body);
974 return cast<AutoDiffTypeInterface>(
975 cast<enzyme::GradientType>(grad.getType()).getBasetype())
976 .createNullValue(builder, grad.getLoc());
977 }
978};
979
980struct AffineParallelRegionBranchOpInterface
981 : public mlir::RegionBranchOpInterface::FallbackModel<
982 AffineParallelRegionBranchOpInterface> {
983 OperandRange getEntrySuccessorOperands(Operation *op,
984 RegionSuccessor successor) const {
985 return cast<AffineParallelOp>(op).getInits();
986 }
987
988 void getEntrySuccessorRegions(
989 Operation *op, ::llvm::ArrayRef<::mlir::Attribute> operands,
990 ::llvm::SmallVectorImpl<::mlir::RegionSuccessor> &regions) const {
991 getSuccessorRegions(op, RegionBranchPoint::parent(), regions);
992 }
993
994 void getSuccessorRegions(
995 Operation *op, RegionBranchPoint point,
996 ::llvm::SmallVectorImpl<::mlir::RegionSuccessor> &regions) const {
997 // Both the operation itself and the region may be branching into the body
998 // or back into the operation itself. It is possible for the loop to not
999 // enter the body.
1000 regions.push_back(RegionSuccessor(&cast<AffineParallelOp>(op).getRegion()));
1001 regions.push_back(RegionSuccessor(op));
1002 }
1003
1004 void getSuccessorRegions(
1005 Operation *op, ::mlir::Region &region,
1006 ::llvm::SmallVectorImpl<::mlir::RegionSuccessor> &regions) const {
1007 for (::mlir::Block &block : region) {
1008 if (block.empty())
1009 continue;
1010 if (auto terminator =
1011 dyn_cast<RegionBranchTerminatorOpInterface>(block.back()))
1012 getSuccessorRegions(op, RegionBranchPoint(terminator), regions);
1013 }
1014 }
1015
1016 void getRegionInvocationBounds(
1017 Operation *op, ArrayRef<Attribute> operands,
1018 SmallVectorImpl<InvocationBounds> &invocationBounds) const {
1019 invocationBounds.append(op->getNumRegions(),
1020 InvocationBounds::getUnknown());
1021 }
1022
1023 bool areTypesCompatible(Operation *op, Type lhs, Type rhs) const {
1024 return lhs == rhs;
1025 }
1026
1027 ValueRange getSuccessorInputs(Operation *op,
1028 RegionSuccessor successor) const {
1029 auto parOp = cast<affine::AffineParallelOp>(op);
1030 return successor.isOperation() ? ValueRange(parOp.getResults())
1031 : ValueRange(parOp.getRegionIterArgs());
1032 }
1033
1034 void getPredecessorValues(Operation *op, RegionSuccessor successor, int index,
1035 SmallVectorImpl<Value> &predecessorValues) const {}
1036
1037 void getPredecessors(Operation *op, RegionSuccessor successor,
1038 SmallVectorImpl<RegionBranchPoint> &predecessors) const {
1039 }
1040};
1041
1042struct AffineLoadOpInterfaceReverse
1043 : public ReverseAutoDiffOpInterface::ExternalModel<
1044 AffineLoadOpInterfaceReverse, affine::AffineLoadOp> {
1045 LogicalResult createReverseModeAdjoint(Operation *op, OpBuilder &builder,
1046 MGradientUtilsReverse *gutils,
1047 SmallVector<Value> caches) const {
1048 auto loadOp = cast<affine::AffineLoadOp>(op);
1049 Value memref = loadOp.getMemref();
1050
1051 if (auto iface = dyn_cast<AutoDiffTypeInterface>(loadOp.getType())) {
1052 // A mutable type's derivative is a shadow, not a value to add into: there
1053 // is nothing to accumulate here, the same way the store adjoint has
1054 // nothing to take back out. Loading a pointer is the case in hand.
1055 if (!gutils->isConstantValue(loadOp) &&
1056 !gutils->isConstantValue(memref) && !iface.isMutable()) {
1057 Value gradient = gutils->diffe(loadOp, builder);
1058 Value memrefGradient = gutils->popCache(caches.front(), builder);
1059
1060 SmallVector<Value> retrievedArguments;
1061 for (Value cache : ValueRange(caches).drop_front(1)) {
1062 Value retrievedValue = gutils->popCache(cache, builder);
1063 retrievedArguments.push_back(retrievedValue);
1064 }
1065
1066 if (!gutils->AtomicAdd) {
1067 auto alignAttr = loadOp->getAttrOfType<IntegerAttr>("alignment");
1068 bool hasIndex = loadOp.getAffineMap().getNumDims() > 0;
1069 // if index had to be cached, the pop is not necessarily a valid index
1070 if (hasIndex) {
1071 SmallVector<Value> indices;
1072 computeAffineIndices(builder, loadOp.getLoc(),
1073 loadOp.getAffineMap(), retrievedArguments,
1074 indices);
1075
1076 auto loadedGradientOp = memref::LoadOp::create(
1077 builder, loadOp.getLoc(), memrefGradient, indices);
1078 loadedGradientOp.setAlignmentAttr(alignAttr);
1079 Value addedGradient = iface.createAddOp(builder, loadOp.getLoc(),
1080 loadedGradientOp, gradient);
1081 auto storeGradientOp =
1082 memref::StoreOp::create(builder, loadOp.getLoc(), addedGradient,
1083 memrefGradient, indices);
1084 storeGradientOp.setAlignmentAttr(alignAttr);
1085 } else {
1086 auto loadedGradientOp = affine::AffineLoadOp::create(
1087 builder, loadOp.getLoc(), memrefGradient, loadOp.getAffineMap(),
1088 ArrayRef<Value>(retrievedArguments));
1089 if (alignAttr)
1090 loadedGradientOp->setAttr("alignment", alignAttr);
1091 Value addedGradient = iface.createAddOp(builder, loadOp.getLoc(),
1092 loadedGradientOp, gradient);
1093 auto storeGradientOp = affine::AffineStoreOp::create(
1094 builder, loadOp.getLoc(), addedGradient, memrefGradient,
1095 loadOp.getAffineMap(), ArrayRef<Value>(retrievedArguments));
1096 if (alignAttr)
1097 storeGradientOp->setAttr("alignment", alignAttr);
1098 }
1099 } else {
1100 bool hasIndex = loadOp.getAffineMap().getNumDims() > 0;
1101 auto alignAttr = loadOp->getAttrOfType<IntegerAttr>("alignment");
1102 // if index had to be cached, the pop is not necessarily a valid index
1103 if (hasIndex) {
1104 SmallVector<Value> indices;
1105 computeAffineIndices(builder, loadOp.getLoc(),
1106 loadOp.getAffineMap(), retrievedArguments,
1107 indices);
1108 setDerivativeFastMath(enzyme::AtomicRMWOp::create(
1109 builder, loadOp.getLoc(), gradient.getType(),
1110 arith::AtomicRMWKind::addf, Ordering::monotonic, gradient,
1111 memrefGradient, indices, alignAttr));
1112 } else {
1113 setDerivativeFastMath(enzyme::AffineAtomicRMWOp::create(
1114 builder, loadOp.getLoc(), gradient.getType(),
1115 arith::AtomicRMWKind::addf, Ordering::monotonic, gradient,
1116 memrefGradient, retrievedArguments, loadOp.getAffineMap(),
1117 alignAttr));
1118 }
1119 }
1120 }
1121 }
1122 return success();
1123 }
1124
1125 SmallVector<Value> cacheValues(Operation *op,
1126 MGradientUtilsReverse *gutils) const {
1127 auto loadOp = cast<affine::AffineLoadOp>(op);
1128 Value memref = loadOp.getMemref();
1129 ValueRange indices = loadOp.getIndices();
1130 if (auto iface = dyn_cast<AutoDiffTypeInterface>(loadOp.getType())) {
1131 if (!gutils->isConstantValue(loadOp) &&
1132 !gutils->isConstantValue(memref)) {
1133 OpBuilder cacheBuilder(gutils->getNewFromOriginal(op));
1134 SmallVector<Value> caches;
1135 caches.push_back(gutils->initAndPushCache(
1136 gutils->invertPointerM(memref, cacheBuilder), cacheBuilder));
1137 for (Value v : indices) {
1138 caches.push_back(gutils->initAndPushCache(
1139 gutils->getNewFromOriginal(v), cacheBuilder));
1140 }
1141 return caches;
1142 }
1143 }
1144 return SmallVector<Value>();
1145 }
1146
1147 LogicalResult createShadowValues(Operation *op, OpBuilder &builder,
1148 MGradientUtilsReverse *gutils) const {
1149 auto loadOp = cast<affine::AffineLoadOp>(op);
1150 Value memref = loadOp.getMemref();
1151 auto iface = dyn_cast<AutoDiffTypeInterface>(loadOp.getType());
1152 // What a load of a mutable type reads is itself a handle on active memory,
1153 // so what stands for it is the handle held at the same place in the shadow:
1154 // the same load, off the shadow memref. Reading it out is the whole of the
1155 // derivative -- see the adjoint above, which leaves it alone.
1156 if (!iface)
1157 return op->emitError()
1158 << "could not compute the shadow of a load of a type without "
1159 "autodiff semantics "
1160 << *op;
1161 // An immutable load's derivative is the adjoint's to accumulate.
1162 if (!iface.isMutable())
1163 return success();
1164 if (gutils->isConstantValue(loadOp))
1165 return success();
1166 // Cannot load a non-constant value out of a constant memref: there is no
1167 // shadow to read the handle from, so the claimed activity cannot be
1168 // honored.
1169 if (gutils->isConstantValue(memref))
1170 return op->emitError()
1171 << "cannot load a non-constant value out of a constant memref "
1172 << *op;
1173 Value memrefShadow = gutils->invertPointerM(memref, builder);
1174 auto newLoad = cast<affine::AffineLoadOp>(gutils->getNewFromOriginal(op));
1175 auto shadowLoad = cast<affine::AffineLoadOp>(builder.clone(*newLoad));
1176 shadowLoad.getMemrefMutable().assign(memrefShadow);
1177 gutils->setInvertedPointer(loadOp.getResult(), shadowLoad.getResult());
1178 return success();
1179 }
1180};
1181
1182// Lets activity analysis treat affine.store generically via StoreLikeInterface.
1183// getStoredPointer returns the base memref; the affine map indices apply within
1184// the op.
1185struct AffineStoreLike
1186 : public StoreLikeInterface::ExternalModel<AffineStoreLike,
1187 affine::AffineStoreOp> {
1188 Value getStoredValue(Operation *op) const {
1189 return cast<affine::AffineStoreOp>(op).getValueToStore();
1190 }
1191 Value getStoredPointer(Operation *op) const {
1192 return cast<affine::AffineStoreOp>(op).getMemRef();
1193 }
1194};
1195
1196struct AffineStoreOpInterfaceReverse
1197 : public ReverseAutoDiffOpInterface::ExternalModel<
1198 AffineStoreOpInterfaceReverse, affine::AffineStoreOp> {
1199 LogicalResult createReverseModeAdjoint(Operation *op, OpBuilder &builder,
1200 MGradientUtilsReverse *gutils,
1201 SmallVector<Value> caches) const {
1202 auto storeOp = cast<affine::AffineStoreOp>(op);
1203 Value val = storeOp.getValue();
1204 Value memref = storeOp.getMemref();
1205 // ValueRange indices = storeOp.getIndices();
1206
1207 auto iface = cast<AutoDiffTypeInterface>(val.getType());
1208
1209 if (!gutils->isConstantValue(memref)) {
1210 OpBuilder cacheBuilder(gutils->getNewFromOriginal(op));
1211
1212 Value memrefGradient = gutils->popCache(caches.front(), builder);
1213
1214 SmallVector<Value> retrievedArguments;
1215 for (Value cache : ValueRange(caches).drop_front(1)) {
1216 Value retrievedValue = gutils->popCache(cache, builder);
1217 retrievedArguments.push_back(retrievedValue);
1218 }
1219
1220 bool hasIndex = storeOp.getAffineMap().getNumDims() > 0;
1221 auto alignAttr = storeOp->getAttrOfType<IntegerAttr>("alignment");
1222
1223 if (!iface.isMutable()) {
1224 if (!gutils->isConstantValue(val)) {
1225 Value loadedGradient;
1226 if (hasIndex) {
1227 SmallVector<Value> indices;
1228 computeAffineIndices(builder, storeOp.getLoc(),
1229 storeOp.getAffineMap(), retrievedArguments,
1230 indices);
1231 auto loadedGradientOp = memref::LoadOp::create(
1232 builder, storeOp.getLoc(), memrefGradient, indices);
1233 loadedGradientOp.setAlignmentAttr(alignAttr);
1234 loadedGradient = loadedGradientOp;
1235 } else {
1236 auto loadedGradientOp = affine::AffineLoadOp::create(
1237 builder, storeOp.getLoc(), memrefGradient,
1238 storeOp.getAffineMap(), ArrayRef<Value>(retrievedArguments));
1239 if (alignAttr)
1240 loadedGradientOp->setAttr("alignment", alignAttr);
1241 loadedGradient = loadedGradientOp;
1242 }
1243 gutils->addToDiffe(val, loadedGradient, builder);
1244 }
1245
1246 auto zero =
1247 cast<AutoDiffTypeInterface>(gutils->getShadowType(val.getType()))
1248 .createNullValue(builder, op->getLoc());
1249
1250 // if index had to be cached, the pop is not necessarily a valid index
1251 if (hasIndex) {
1252 SmallVector<Value> indices;
1253 computeAffineIndices(builder, storeOp.getLoc(),
1254 storeOp.getAffineMap(), retrievedArguments,
1255 indices);
1256 auto zeroStoreOp = memref::StoreOp::create(
1257 builder, storeOp.getLoc(), zero, memrefGradient, indices);
1258 zeroStoreOp.setAlignmentAttr(alignAttr);
1259 } else {
1260 auto zeroStoreOp = affine::AffineStoreOp::create(
1261 builder, storeOp.getLoc(), zero, memrefGradient,
1262 storeOp.getAffineMap(), ArrayRef<Value>(retrievedArguments));
1263 if (alignAttr)
1264 zeroStoreOp->setAttr("alignment", alignAttr);
1265 }
1266 }
1267
1268 // A store into memory whose primal contents the caller declared
1269 // unneeded (enzyme_dupnoneed) need not happen in the augmented
1270 // forward pass either.
1271 if (gutils->primalStoreElidable(storeOp.getMemref()))
1272 gutils->erase(gutils->getNewFromOriginal(op));
1273 }
1274 return success();
1275 }
1276
1277 SmallVector<Value> cacheValues(Operation *op,
1278 MGradientUtilsReverse *gutils) const {
1279 auto storeOp = cast<affine::AffineStoreOp>(op);
1280 Value memref = storeOp.getMemref();
1281 ValueRange indices = storeOp.getIndices();
1282 Value val = storeOp.getValue();
1283 if (auto iface = dyn_cast<AutoDiffTypeInterface>(val.getType())) {
1284 if (!gutils->isConstantValue(memref)) {
1285 OpBuilder cacheBuilder(gutils->getNewFromOriginal(op));
1286 SmallVector<Value> caches;
1287 caches.push_back(gutils->initAndPushCache(
1288 gutils->invertPointerM(memref, cacheBuilder), cacheBuilder));
1289 for (Value v : indices) {
1290 caches.push_back(gutils->initAndPushCache(
1291 gutils->getNewFromOriginal(v), cacheBuilder));
1292 }
1293 return caches;
1294 }
1295 }
1296 return SmallVector<Value>();
1297 }
1298
1299 // Same structural story as memref.store: a stored mutable value's shadow
1300 // must land at the same affine position in the shadow memref.
1301 LogicalResult createShadowValues(Operation *op, OpBuilder &builder,
1302 MGradientUtilsReverse *gutils) const {
1303 auto storeOp = cast<affine::AffineStoreOp>(op);
1304 Value val = storeOp.getValue();
1305 Value memref = storeOp.getMemref();
1306 auto iface = dyn_cast<AutoDiffTypeInterface>(val.getType());
1307 if (!iface)
1308 return op->emitError()
1309 << "could not compute the shadow of a store of a type without "
1310 "autodiff semantics "
1311 << *op;
1312 if (gutils->isConstantValue(memref))
1313 return success();
1314 // Immutable values' shadows live in the reverse sweep's adjoint, which
1315 // accumulates and zeroes the slot; only mutable values need the forward
1316 // sweep to place their shadow.
1317 if (!iface.isMutable())
1318 return success();
1319 Value memrefShadow = gutils->invertPointerM(memref, builder);
1320 Value valShadow =
1321 gutils->isConstantValue(val)
1322 ? oputils::inactiveStoredValueShadow(op, *gutils, val, builder)
1323 : gutils->invertPointerM(val, builder);
1324 auto newOp = cast<affine::AffineStoreOp>(gutils->getNewFromOriginal(op));
1325 auto shadowOp = cast<affine::AffineStoreOp>(builder.clone(*newOp));
1326 shadowOp.getValueMutable().assign(valShadow);
1327 shadowOp.getMemrefMutable().assign(memrefShadow);
1328 return success();
1329 }
1330};
1331
1332struct AffineForOpADDataFlow
1333 : public ADDataFlowOpInterface::ExternalModel<AffineForOpADDataFlow,
1334 affine::AffineForOp> {
1335 SmallVector<Value> getPotentialIncomingValuesRes(Operation *op,
1336 OpResult res) const {
1337 auto forOp = cast<affine::AffineForOp>(op);
1338 return {
1339 forOp.getInits()[res.getResultNumber()],
1340 forOp.getBody()->getTerminator()->getOperand(res.getResultNumber())};
1341 }
1342 SmallVector<Value> getPotentialIncomingValuesArg(Operation *op,
1343 BlockArgument arg) const {
1344 auto forOp = cast<affine::AffineForOp>(op);
1345 if (arg.getArgNumber() < 1) {
1346 return {};
1347 }
1348 auto idx = arg.getArgNumber() - 1;
1349 return {forOp.getInits()[idx],
1350 forOp.getBody()->getTerminator()->getOperand(idx)};
1351 }
1352 SmallVector<Value> getPotentialTerminatorUsers(Operation *op, Operation *term,
1353 Value val) const {
1354 auto forOp = cast<affine::AffineForOp>(op);
1355 SmallVector<Value> sv;
1356
1357 for (auto &&[res, arg, barg] :
1358 llvm::zip_equal(forOp->getResults(), term->getOperands(),
1359 forOp.getRegionIterArgs())) {
1360 if (arg == val) {
1361 sv.push_back(res);
1362 sv.push_back(barg);
1363 }
1364 }
1365
1366 return sv;
1367 }
1368};
1369
1370struct AffineForOpEnzymeOpsRemover
1371 : public ForLikeEnzymeOpsRemover<AffineForOpEnzymeOpsRemover,
1372 affine::AffineForOp> {
1373public:
1374 // TODO: support non constant number of iteration by using unknown dimensions
1375 static std::optional<int64_t>
1376 getConstantNumberOfIterations(affine::AffineForOp forOp) {
1377 if (!forOp.hasConstantLowerBound())
1378 return std::nullopt;
1379 if (!forOp.hasConstantUpperBound())
1380 return std::nullopt;
1381 return (forOp.getConstantUpperBound() - forOp.getConstantLowerBound()) /
1382 forOp.getStepAsInt();
1383 }
1384
1385 // A bound map with several results is a min (upper bound) or a max (lower
1386 // bound) -- affine.for's own reading of one, and the shape the periodic
1387 // checkpointing scaffold gives a segment that may be short:
1388 // `min(nInner, remaining)`. affine.apply cannot express it, since it requires
1389 // a single-result map, so materialize it with the op that can.
1390 static Value materializeBound(OpBuilder &builder, Location loc, AffineMap map,
1391 ValueRange operands, bool isUpper) {
1392 if (map.getNumResults() == 1)
1393 return AffineApplyOp::create(builder, loc, map, operands);
1394 if (isUpper)
1395 return affine::AffineMinOp::create(builder, loc, map, operands);
1396 return affine::AffineMaxOp::create(builder, loc, map, operands);
1397 }
1398
1399 static SmallVector<IntOrValue, 1>
1400 getDimensionBounds(OpBuilder &builder, affine::AffineForOp forOp) {
1401 auto iters = getConstantNumberOfIterations(forOp);
1402 if (iters) {
1403 return {IntOrValue(*iters)};
1404 } else {
1405 Value lb =
1406 materializeBound(builder, forOp.getLoc(), forOp.getLowerBoundMap(),
1407 forOp.getLowerBoundOperands(),
1408 /*isUpper=*/false);
1409 Value ub =
1410 materializeBound(builder, forOp.getLoc(), forOp.getUpperBoundMap(),
1411 forOp.getUpperBoundOperands(),
1412 /*isUpper=*/true);
1413
1414 Value diff = arith::SubIOp::create(builder, forOp->getLoc(), ub, lb);
1415 if (forOp.getStepAsInt() != 1) {
1416 // arith::ConstantIntOp requires an IntegerType; diff's type here is
1417 // always index (the result of an affine.apply), which
1418 // arith::ConstantOp + IntegerAttr handles uniformly (unlike
1419 // ConstantIntOp, which asserts on IndexType).
1420 auto step = arith::ConstantOp::create(
1421 builder, forOp->getLoc(),
1422 IntegerAttr::get(diff.getType(), forOp.getStepAsInt()));
1423 diff = arith::DivUIOp::create(builder, forOp->getLoc(), diff, step);
1424 }
1425 return {IntOrValue(diff)};
1426 }
1427 }
1428
1429 static SmallVector<Value> getCanonicalLoopIVs(OpBuilder &builder,
1430 affine::AffineForOp forOp) {
1431 Value val = forOp.getBody()->getArgument(0);
1432 if (!forOp.hasConstantLowerBound() || forOp.getConstantLowerBound() != 0) {
1433 Value lb =
1434 materializeBound(builder, forOp.getLoc(), forOp.getLowerBoundMap(),
1435 forOp.getLowerBoundOperands(),
1436 /*isUpper=*/false);
1437 val = arith::SubIOp::create(builder, forOp->getLoc(), val, lb);
1438 }
1439
1440 if (forOp.getStepAsInt() != 1) {
1441 // See the identical note in getDimensionBounds above: val's type here
1442 // is always index, which needs arith::ConstantOp + IntegerAttr rather
1443 // than arith::ConstantIntOp (IntegerType-only).
1444 auto step = arith::ConstantOp::create(
1445 builder, forOp->getLoc(),
1446 IntegerAttr::get(val.getType(), forOp.getStepAsInt()));
1447 val = arith::DivUIOp::create(builder, forOp->getLoc(), val, step);
1448 }
1449 return {val};
1450 }
1451
1452 static IRMapping createArgumentMap(PatternRewriter &rewriter,
1453 affine::AffineForOp forOp,
1454 ArrayRef<Value> indFor,
1455 affine::AffineForOp otherForOp,
1456 ArrayRef<Value> indOther) {
1457 IRMapping map;
1458 for (auto &&[f, o] : llvm::zip_equal(indFor, indOther))
1459 map.map(f, o);
1460
1461 Value canIdx = forOp.getBody()->getArgument(0);
1462 if (!map.contains(canIdx)) {
1463 assert(forOp.getLowerBoundMap() == otherForOp.getLowerBoundMap());
1464 for (auto &&[f, o] :
1465 llvm::zip_equal(forOp.getLowerBoundOperands(),
1466 otherForOp.getLowerBoundOperands())) {
1467 (void)f;
1468 (void)o;
1469 assert(Equivalent(f, o));
1470 }
1471 assert(forOp.getStep() == otherForOp.getStep());
1472 map.map(forOp.getBody()->getArgument(0),
1473 otherForOp.getBody()->getArgument(0));
1474 }
1475 return map;
1476 }
1477
1478 static affine::AffineForOp
1479 replaceWithNewOperands(PatternRewriter &rewriter,
1480 affine::AffineForOp otherForOp,
1481 ArrayRef<Value> operands) {
1482 auto newOtherForOp = affine::AffineForOp::create(
1483 rewriter, otherForOp->getLoc(), otherForOp.getLowerBoundOperands(),
1484 otherForOp.getLowerBoundMap(), otherForOp.getUpperBoundOperands(),
1485 otherForOp.getUpperBoundMap(), otherForOp.getStepAsInt(), operands);
1486
1487 // Same loop, more iteration arguments: it keeps what was set on it.
1488 newOtherForOp->setDiscardableAttrs(
1489 otherForOp->getDiscardableAttrDictionary());
1490
1491 newOtherForOp.getRegion().takeBody(otherForOp.getRegion());
1492 rewriter.replaceOp(otherForOp, newOtherForOp->getResults().slice(
1493 0, otherForOp->getNumResults()));
1494 return newOtherForOp;
1495 }
1496
1497 static ValueRange getInits(affine::AffineForOp forOp) {
1498 return forOp.getInits();
1499 }
1500
1501 static bool mustPostAdd(affine::AffineForOp forOp) { return false; }
1502
1503 static Value initialValueInBlock(OpBuilder &builder, Block *body,
1504 Value grad) {
1505 auto Ty = cast<enzyme::GradientType>(grad.getType()).getBasetype();
1506 return body->addArgument(Ty, grad.getLoc());
1507 }
1508};
1509
1510#include "Implementations/AffineDerivatives.inc"
1511} // namespace
1512
1514 DialectRegistry &registry) {
1515 registry.addExtension(+[](MLIRContext *context, affine::AffineDialect *) {
1516 // Binomial checkpointing's scaffold loops are always plain scf ops,
1517 // even when checkpointing an affine.for -- load scf here rather than
1518 // relying on whatever pass happens to run this interface to have
1519 // declared it as a dependent dialect.
1520 context->getOrLoadDialect<scf::SCFDialect>();
1521 registerInterfaces(context);
1522 affine::AffineLoadOp::attachInterface<AffineLoadOpInterfaceReverse>(
1523 *context);
1524 affine::AffineStoreOp::attachInterface<AffineStoreLike>(*context);
1525 affine::AffineStoreOp::attachInterface<AffineStoreOpInterfaceReverse>(
1526 *context);
1527 affine::AffineForOp::attachInterface<AffineForOpInterfaceReverse>(*context);
1528 affine::AffineForOp::attachInterface<AffineForOpEnzymeOpsRemover>(*context);
1529 affine::AffineForOp::attachInterface<AffineForOpADDataFlow>(*context);
1530 affine::AffineParallelOp::attachInterface<AffineParallelOpInterfaceReverse>(
1531 *context);
1532 affine::AffineParallelOp::attachInterface<AffineParallelOpEnzymeOpsRemover>(
1533 *context);
1534 affine::AffineParallelOp::attachInterface<
1535 AffineParallelRegionBranchOpInterface>(*context);
1536 affine::AffineIfOp::attachInterface<AffineIfOpInterfaceReverse>(*context);
1537 affine::AffineIfOp::attachInterface<AffineIfOpEnzymeOpsRemover>(*context);
1538 });
1539}
static std::optional< SmallVector< Value > > getPotentialTerminatorUsers(Operation *op, Value parent)
void setDiffe(mlir::Value origv, mlir::Value newv, mlir::OpBuilder &builder)
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 addToDiffe(mlir::Value oldGradient, mlir::Value addedGradient, OpBuilder &builder)
bool primalStoreElidable(mlir::Value ptr)
True when a primal store through this pointer may be omitted: declaring the base enzyme_dupnoneed is ...
mlir::Type getShadowType(mlir::Type T)
void erase(Operation *op)
mlir::Value invertPointerM(mlir::Value v, OpBuilder &Builder2)
SmallVector< mlir::Value, 1 > getNewFromOriginal(ValueRange originst) const
void setInvertedPointer(mlir::Value origv, mlir::Value newv)
bool isConstantValue(mlir::Value v) const
Value inactiveStoredValueShadow(Operation *orig, MGradientUtils &gutils, Value stored, OpBuilder &builder)
Definition Utils.cpp:104
void computeAffineIndices(OpBuilder &builder, Location loc, AffineMap map, ValueRange operands, SmallVectorImpl< Value > &indices)
Definition Utils.cpp:171
void localizeGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *fwd)
void setDerivativeFastMath(Operation *op)
Give an operation built for a derivative the fast-math flags the LLVM path gives everything it builds...
void registerAffineDialectAutoDiffInterface(DialectRegistry &registry)