Enzyme main
Loading...
Searching...
No Matches
RemovalUtils.cpp
Go to the documentation of this file.
1//===- RemovalUtils.cpp - Utilities to remove Enzyme ops -------* C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
9#include "RemovalUtils.h"
13#include "Utils.h"
14#include "mlir/Analysis/TopologicalSortUtils.h"
15#include "mlir/IR/PatternMatch.h"
16#include "mlir/Transforms/RegionUtils.h"
17#include <cassert>
18#include <deque>
19
20// loop-invariant cache requires a copy, which is implemented using an scf.for
21#include "mlir/Dialect/SCF/IR/SCF.h"
22
23#include "llvm/ADT/MapVector.h"
24#include "llvm/ADT/PointerIntPair.h"
25#include "llvm/ADT/SetVector.h"
26
27using namespace mlir;
28using namespace mlir::enzyme;
29
30#define DEBUG_TYPE "enzyme-mincut"
31
32static llvm::cl::opt<bool>
33 DebugGraphviz("mincut-print-graphviz", llvm::cl::init(false),
34 llvm::cl::Hidden,
35 llvm::cl::desc("Use with DEBUG_TYPE 'enzyme-mincut' to print "
36 "the mincut graphs in GraphViz"));
37
38void mlir::enzyme::localizeGradients(OpBuilder &builder,
40 Block *fwd) {
41 OpBuilder::InsertionGuard guard(builder);
42 Operation *parent = fwd->getParentOp();
43
44 auto localizeGradientValue = [&](Value val) {
45 if (gutils->isConstantValue(val))
46 return;
47 auto iface = dyn_cast<AutoDiffTypeInterface>(val.getType());
48 if (iface && !iface.isMutable()) {
49 auto grad = gutils->getDifferential(val);
50
51 enzyme::SetOp initialSet = nullptr;
52 for (auto user : grad.getUsers()) {
53 if (!parent->isProperAncestor(user)) {
54 assert(!initialSet);
55 initialSet = dyn_cast<enzyme::SetOp>(user);
56 assert(initialSet);
57 }
58 }
59
60 auto initOp = grad.getDefiningOp<enzyme::InitOp>();
61
62 {
63 OpBuilder::InsertionGuard g(builder);
64 Value zero =
65 iface.createNullValue(builder, initialSet.getValue().getLoc());
66 builder.setInsertionPointAfter(zero.getDefiningOp());
67 enzyme::SetOp::create(builder, initialSet.getLoc(), grad, zero);
68 initialSet->erase();
69 }
70
71 builder.setInsertionPointToStart(builder.getBlock());
72 initOp->remove();
73 builder.insert(initOp);
74 }
75 };
76
77 for (auto operand : fwd->getArguments()) {
78 localizeGradientValue(operand);
79 }
80
81 for (auto &it : fwd->getOperations()) {
82 for (auto res : it.getResults()) {
83 localizeGradientValue(res);
84 }
85 }
86}
87
89 Block *block, IRMapping &mapping, PatternRewriter &rewriter,
90 llvm::SetVector<Value> &gradients,
91 llvm::MapVector<Value, CacheInfo> &caches) {
92 for (auto it = block->begin(), e = block->end(); it != e;) {
93 Operation *op = &*it;
94
95 if (auto setOp = dyn_cast<enzyme::SetOp>(op)) {
96 auto grad = setOp.getGradient();
97 auto value = setOp.getValue();
98 mapping.map(grad, value);
99 gradients.insert(grad);
100
101 ++it;
102 rewriter.eraseOp(setOp);
103 continue;
104 }
105
106 if (auto getOp = dyn_cast<enzyme::GetOp>(op)) {
107 auto grad = getOp.getGradient();
108 Value value = mapping.lookupOrNull(getOp.getGradient());
109 if (!value) {
110 value = enzyme::GetOp::create(rewriter, getOp->getLoc(),
111 getOp.getResult().getType(), grad);
112 mapping.map(grad, value);
113 }
114 ++it;
115 rewriter.replaceOp(getOp, value);
116 continue;
117 }
118
119 if (auto pushOp = dyn_cast<enzyme::PushOp>(op)) {
120 CacheInfo info(pushOp.getCache());
121
122 Value pushedValue = info.pushedValue();
123
124 // Then we can push the value before the if, if it is defined before the
125 // if
126 if (pushedValue.getParentBlock() != block) {
127 enzyme::PushOp::create(rewriter, pushOp->getLoc(), pushOp.getCache(),
128 pushedValue);
129
130 ++it; // Increment iterator to allow in place deletion
131 rewriter.eraseOp(pushOp);
132
133 // Move the pop before the other if
134 OpBuilder::InsertionGuard guard(rewriter);
135 rewriter.setInsertionPoint(info.popOp->getParentOp());
136
137 auto newPop =
138 enzyme::PopOp::create(rewriter, info.popOp->getLoc(),
139 pushedValue.getType(), info.popOp.getCache());
140 rewriter.replaceAllUsesWith(info.popOp.getResult(), newPop);
141 rewriter.eraseOp(info.popOp);
142
143 continue;
144 }
145
146 if (caches.contains(pushedValue)) {
147 info = info.merge(caches.lookup(pushedValue), rewriter);
148 }
149 caches[pushedValue] = info;
150 }
151
152 ++it;
153 }
154}
155
156namespace {
157
158// A node in the compute/flow graph. It identifies either an operation or a
159// value, plus a one-bit "outgoing" side selector used by the node-split min-cut
160// (see minCutValues): a value is split into an incoming endpoint
161// (outgoing=false) and an outgoing endpoint (outgoing=true); an operation is
162// never split (outgoing is always false).
163//
164// The op-or-value pointer and the bit are packed into a PointerIntPair so that
165// Node stays pointer-sized and pointer-like -- usable directly in
166// SmallPtrSet/DenseMap/MapVector via the trait specializations below. Use the
167// isValue()/isOperation()/getValue()/getOperation() accessors to inspect it.
168struct Node {
169 using Base = llvm::PointerUnion<Operation *, Value>;
170 llvm::PointerIntPair<Base, 1, bool> pair;
171
172 Node() : pair(Base(), false) {}
173 Node(Operation *op) : pair(Base(op), false) {}
174 Node(Value v, bool outgoing = false) : pair(Base(v), outgoing) {}
175 Node(Base base, bool outgoing) : pair(base, outgoing) {
176 assert((base.isNull() || isa<Value>(base) || !outgoing) &&
177 "operations are never split: they have a single endpoint");
178 }
179
180 Base getBase() const { return pair.getPointer(); }
181 bool outgoing() const { return pair.getInt(); }
182 bool isNull() const { return getBase().isNull(); }
183
184 bool isValue() const {
185 Base b = getBase();
186 return isa<Value>(b);
187 }
188 bool isOperation() const {
189 Base b = getBase();
190 return isa<Operation *>(b);
191 }
192 Value getValue() const {
193 Base b = getBase();
194 return cast<Value>(b);
195 }
196 Operation *getOperation() const {
197 Base b = getBase();
198 return cast<Operation *>(b);
199 }
200 // The value if this node is one, otherwise a null Value.
201 Value dynValue() const {
202 Base b = getBase();
203 return dyn_cast_if_present<Value>(b);
204 }
205
206 bool operator==(const Node &o) const { return pair == o.pair; }
207 bool operator!=(const Node &o) const { return pair != o.pair; }
208
209 void *getOpaqueValue() const { return pair.getOpaqueValue(); }
210 static Node getFromOpaqueValue(void *p) {
211 Node n;
212 n.pair = llvm::PointerIntPair<Base, 1, bool>::getFromOpaqueValue(p);
213 return n;
214 }
215};
216
217} // namespace
218
219// Make Node usable as a pointer-like key/element (SmallPtrSet, DenseMap, ...)
220// by delegating to the packed PointerIntPair. DenseMap tracks empty/tombstone
221// buckets out-of-band, so only getHashValue/isEqual are required (matching the
222// DenseMapInfo LLVM provides for raw pointers and PointerIntPair).
223//
224// These name templates in namespace llvm, so they cannot sit inside the
225// anonymous namespace above; specializing on an internal-linkage type from
226// here is fine, and keeps the specializations internal too.
227template <> struct llvm::PointerLikeTypeTraits<Node> {
228 using Inner = llvm::PointerIntPair<Node::Base, 1, bool>;
229 static inline void *getAsVoidPointer(Node n) { return n.getOpaqueValue(); }
230 static inline Node getFromVoidPointer(void *p) {
231 return Node::getFromOpaqueValue(p);
232 }
233};
234
235template <> struct llvm::DenseMapInfo<Node> {
236 using Inner = llvm::PointerIntPair<Node::Base, 1, bool>;
237 static unsigned getHashValue(const Node &n) {
238 return llvm::DenseMapInfo<Inner>::getHashValue(n.pair);
239 }
240 static bool isEqual(const Node &a, const Node &b) { return a.pair == b.pair; }
241};
242
243namespace {
244
245void dump(const Node &n) {
246 if (n.isValue())
247 llvm::errs() << "[" << n.getValue() << ", "
248 << (n.outgoing() ? "Value(out)" : "Value(in)") << "]\n";
249 else if (n.isOperation())
250 llvm::errs() << "[" << *n.getOperation() << ", "
251 << "Operation"
252 << "]\n";
253 else
254 llvm::errs() << "["
255 << "NULL"
256 << ", "
257 << "None"
258 << "]\n";
259}
260
261// The adjacency sets are insertion-ordered rather than hashed. A Node wraps a
262// Value or Operation pointer, so iterating a SmallPtrSet of them visits
263// neighbours in address order, which varies run to run under ASLR. The max
264// flow below then explores augmenting paths in a different order and, whenever
265// several minimum cuts have the same capacity, settles on a different one --
266// producing correct but different caching decisions on every run.
267using NodeSet = llvm::SmallSetVector<Node, 2>;
268
269struct Graph : public llvm::MapVector<Node, NodeSet> {
270 const NodeSet &at(const Node &n) {
271 auto found = find(n);
272 assert(found != end());
273 return found->second;
274 }
275};
276
277static void dumpGraphviz(Graph &G) {
278 auto serialize = [&](Node n) -> std::string {
279 std::string s;
280 llvm::raw_string_ostream ss(s);
281 if (n.isValue()) {
282 auto v = n.getValue();
283 const char *side = n.outgoing() ? "[val:out]" : "[val:in]";
284 if (isa<OpResult>(v)) {
285 auto res = cast<OpResult>(v);
286 ss << side << "(" << res.getResultNumber() << ")";
287 if (res.getOwner()->hasAttr("dbg")) {
288 auto dbg = res.getOwner()->getAttrOfType<StringAttr>("dbg");
289 ss << dbg.getValue();
290 } else {
291 ss << res.getOwner()->getName().getStringRef();
292 }
293 } else {
294 ss << side << v;
295 }
296 } else if (n.isOperation()) {
297 auto op = n.getOperation();
298 ss << "[op]";
299 if (op->hasAttr("dbg")) {
300 auto dbg = op->getAttrOfType<StringAttr>("dbg");
301 ss << dbg.getValue();
302 } else {
303 ss << op->getName().getStringRef();
304 }
305 } else {
306 ss << "none";
307 }
308 return s;
309 };
310
311 using llvm::errs;
312 errs() << "digraph G {\n";
313 for (auto &pair : G) {
314 for (const auto &N : pair.second) {
315 errs() << " \"" << serialize(pair.first) << "\" -> \"" << serialize(N)
316 << "\";\n";
317 }
318 }
319
320 errs() << "}\n";
321}
322
323static void dump(Graph &G) {
324 if (DebugGraphviz) {
325 dumpGraphviz(G);
326 } else {
327 for (auto &pair : G) {
328 dump(pair.first);
329 for (const auto &N : pair.second) {
330 llvm::errs() << "\t";
331 dump(N);
332 }
333 }
334 }
335}
336
337// A node in the compute graph.
338// Operation nodes have outgoing edges to value nodes that they produce and
339// incoming nodes from values they take as operands.
340
341// parent is populated with a path from each connected leaf node of G to one
342// of the Value in Source.
343static inline void bfs(const Graph &G, const llvm::SetVector<Value> &Sources,
344 DenseMap<Node, Node> &parent) {
345 std::deque<Node> q;
346 for (const auto &V : Sources) {
347 Node N(V);
348 parent.try_emplace(N, Node());
349 q.push_back(N);
350 }
351
352 // Standard BFS Loop
353
354 SmallPtrSet<Node, 2> done;
355
356 while (!q.empty()) {
357 auto u = q.front();
358 q.pop_front();
359 auto found = G.find(u);
360 if (found == G.end())
361 continue;
362
363 if (!done.insert(u).second)
364 continue;
365
366 for (const auto &v : found->second) {
367 if (parent.try_emplace(v, u).second) {
368 q.push_back(v);
369 }
370 }
371 }
372}
373
374static inline bool isLoadMovable(Operation *op) {
375 if (!hasSingleEffect<MemoryEffects::Read>(op)) {
376 return false;
377 }
378 return op->hasAttr("enzyme.readonly");
379}
380
381static inline bool isMovable(Operation *op);
382static inline bool isRegionBranchMovable(RegionBranchOpInterface regionBranch) {
383 // We can move region ops that only contain pure operations. As a heuristic,
384 // we only consider non-looping ops.
385 if (regionBranch.hasLoop())
386 return false;
387 for (auto &region : regionBranch->getRegions()) {
388 // Regions with multiple blocks potentially contain loops
389 if (!region.hasOneBlock())
390 return false;
391
392 for (auto &bodyOp : region.front()) {
393 if (auto bodyRegionBranch = dyn_cast<RegionBranchOpInterface>(&bodyOp)) {
394 if (!isRegionBranchMovable(bodyRegionBranch))
395 return false;
396 // Terminators are considered not movable, but do not impact our ability
397 // to move their enclosing region op.
398 } else if (&bodyOp != region.front().getTerminator() &&
399 !isMovable(&bodyOp)) {
400 return false;
401 }
402 }
403 }
404 return true;
405}
406
407// Whether or not an operation can be moved from the forward region to the
408// reverse region or vice-versa.
409static inline bool isMovable(Operation *op) {
410 if (auto regionBranch = dyn_cast<RegionBranchOpInterface>(op))
411 return isRegionBranchMovable(regionBranch);
412 return op->getNumRegions() == 0 && op->getBlock()->getTerminator() != op &&
413 (mlir::isPure(op) || isLoadMovable(op));
414}
415
416// Given a graph `G`, construct a new graph `G2`, where all paths must terminate
417// in a node in the set `Required` and start at `Root`.
418template <typename T>
419static Graph filterGraph(const Graph &Orig, const SetVector<Value> &Roots,
420 const SetVector<T> &Required) {
421 Graph inverted;
422
423 // Compute the graph with inverted edges by a floodfill, stopping at the first
424 // `required`. This is required in the case of a root -> required -> required
425 // edge. We do not want to contain the required->required subgraph.
426 if (false) {
427 std::deque<Node> worklist;
428 for (auto val : Roots) {
429 worklist.push_back(val);
430 }
431
432 SmallPtrSet<Node, 2> done;
433 for (auto src : Required) {
434 done.insert(src);
435 }
436
437 while (!worklist.empty()) {
438 Node N = worklist.front();
439 worklist.pop_front();
440
441 if (!done.insert(N).second)
442 continue;
443
444 auto pair = Orig.find(N);
445 if (pair == Orig.end()) {
446 continue;
447 }
448
449 for (const auto &NN : pair->second) {
450
451 inverted[NN].insert(N);
452 if (!done.contains(NN)) {
453 worklist.push_back(NN);
454 }
455 }
456 }
457
458 } else {
459 for (auto &pair : Orig) {
460 for (auto N : pair.second) {
461 inverted[N].insert(pair.first);
462 }
463 }
464 }
465
466 std::deque<Node> worklist;
467 for (auto snk : Required) {
468 worklist.emplace_back(snk);
469 }
470
471 SmallPtrSet<Node, 2> done;
472 for (auto src : Roots) {
473 done.insert(src);
474 }
475
476 Graph G;
477
478 while (!worklist.empty()) {
479 Node N = worklist.front();
480 worklist.pop_front();
481
482 if (!done.insert(N).second)
483 continue;
484
485 auto pair = inverted.find(N);
486 if (pair == inverted.end()) {
487 continue;
488 }
489
490 for (const auto &NN : pair->second) {
491
492 G[NN].insert(N);
493 if (!done.contains(NN)) {
494 worklist.push_back(NN);
495 }
496 }
497 }
498
499 return G;
500}
501
502static int64_t computeSizeOfType(Value val) {
503 auto T = dyn_cast<AutoDiffTypeInterface>(val.getType());
504 return T ? T.getApproxSize() : INT64_MAX;
505};
506
507static int64_t computeRankOfType(Value val) {
508 auto TT = dyn_cast<RankedTensorType>(val.getType());
509 return TT ? TT.getRank() : 0;
510}
511
512/// Find a common IsolatedFromAbove ancestor of the given ops. If at least one
513/// op is a top-level module op (which is expected to be isolated from above),
514/// return that op.
515static Operation *findCommonAncestor(ArrayRef<Operation *> ops) {
516 // Check if there is a top-level operation within `ops`. If so, return that
517 // op.
518 for (Operation *op : ops) {
519 if (!op->getParentOp()) {
520#ifndef NDEBUG
521 assert(op->hasTrait<OpTrait::IsIsolatedFromAbove>() &&
522 "expected top-level op to be isolated from above");
523 for (Operation *other : ops)
524 assert(op->isAncestor(other) &&
525 "expected ops to have a common ancestor");
526#endif // NDEBUG
527 return op;
528 }
529 }
530
531 // No top-level op. Find a common ancestor.
532 Operation *commonAncestor =
533 ops.front()->getParentWithTrait<OpTrait::IsIsolatedFromAbove>();
534 for (Operation *op : ops.drop_front()) {
535 while (!commonAncestor->isProperAncestor(op)) {
536 commonAncestor =
537 commonAncestor->getParentWithTrait<OpTrait::IsIsolatedFromAbove>();
538 assert(commonAncestor &&
539 "expected to find a common isolated from above ancestor");
540 }
541 }
542
543 return commonAncestor;
544}
545
546// Annotate operations with a debug attribute. This makes the GraphViz printing
547// nicer.
548static void annotate_ops(Block *forward, Block *reverse) {
549 unsigned counter = 0;
550 forward->walk([&](Operation *op) {
551 auto debugName =
552 StringAttr::get(op->getContext(),
553 op->getName().stripDialect() + llvm::Twine(counter++));
554 op->setAttr("dbg", debugName);
555 });
556 reverse->walk([&](Operation *op) {
557 auto debugName =
558 StringAttr::get(op->getContext(),
559 op->getName().stripDialect() + llvm::Twine(counter++));
560 op->setAttr("dbg", debugName);
561 });
562}
563
564// The node used when a graph node is the outgoing (tail) endpoint of an edge: a
565// value uses its outgoing endpoint (outgoing=true), an operation uses its
566// single node.
567static Node edgeTail(Node n) {
568 if (Value v = n.dynValue())
569 return Node(v, /*outgoing=*/true);
570 return n;
571}
572// The node used when a graph node is the incoming (head) endpoint of an edge: a
573// value uses its incoming endpoint (outgoing=false), an operation uses its
574// single node.
575static Node edgeHead(Node n) {
576 if (Value v = n.dynValue())
577 return Node(v, /*outgoing=*/false);
578 return n;
579}
580
581// Given the compute graph `Orig` (with `roots` the non-recomputable sources and
582// `Required` the operations that force a value to be live in reverse), return
583// the minimal set of values to cache.
584//
585// The cut is computed on a node-split version of the graph: every value V is
586// split into an incoming node (outgoing=false) and an outgoing node
587// (outgoing=true) joined by a single unit-capacity edge, so that all of V's
588// flow funnels through it and caching V costs exactly one regardless of how
589// many operations consume it. Operations are not split. Since the split graph
590// is a plain `Graph` over the same `Node` type, the existing bfs/dump utilities
591// apply to it directly.
592static SetVector<Value> minCutValues(const Graph &Orig,
593 const SetVector<Value> &roots,
594 const SetVector<Operation *> &Required) {
595 Graph G;
596 // Build the node-split graph. Each original edge is either operation->value
597 // (a definition) or value->operation (a use).
598 for (const auto &pair : Orig) {
599 Node A = pair.first;
600 // Internal split edge for the tail value (no-op for an operation).
601 if (Value va = A.dynValue())
602 G[Node(va, /*outgoing=*/false)].insert(Node(va, /*outgoing=*/true));
603 for (Node B : pair.second) {
604 G[edgeTail(A)].insert(edgeHead(B));
605 // Internal split edge for the head value (no-op for an operation).
606 if (Value vb = B.dynValue())
607 G[Node(vb, /*outgoing=*/false)].insert(Node(vb, /*outgoing=*/true));
608 }
609 }
610
611 LLVM_DEBUG(llvm::dbgs() << "node-split flow graph: \n";);
612 LLVM_DEBUG(dump(G));
613
614 // Save the original edges for cut extraction; the max-flow below mutates G
615 // into its residual graph. The flow sources are the incoming endpoints of the
616 // roots, which is exactly what bfs() seeds from a set of root values.
617 Graph Split = G;
618
619 // Edmonds-Karp: repeatedly augment along a shortest source->sink path in the
620 // residual graph until no augmenting path remains. All edges have unit
621 // capacity, represented by set membership.
622 while (true) {
623 DenseMap<Node, Node> parent;
624 bfs(G, roots, parent);
625 Node end;
626 for (Operation *req : Required) {
627 if (parent.count(Node(req))) {
628 end = Node(req);
629 break;
630 }
631 }
632 if (end.isNull())
633 break;
634 // Flip the residual edges along the found path, stopping at the source (a
635 // root's incoming endpoint).
636 Node v = end;
637 while (true) {
638 assert(parent.count(v));
639 Node u = parent.find(v)->second;
640 assert(!u.isNull());
641 G[u].remove(v);
642 G[v].insert(u);
643 if (u.isValue() && !u.outgoing() && roots.contains(u.getValue()))
644 break;
645 v = u;
646 }
647 }
648
649 // Reachable set from the sources in the residual graph.
650 DenseMap<Node, Node> parent;
651 bfs(G, roots, parent);
652
653 LLVM_DEBUG(llvm::dbgs() << "residual flow graph: \n";);
654 LLVM_DEBUG(dump(G));
655
656 // The min cut is the set of edges from a reachable node to a non-reachable
657 // node in the original graph. Each such edge names exactly one value to
658 // cache: the value being transported across it. Because values are split,
659 // this edge is one of
660 // - the internal split edge V_in -> V_out (=> cache V), or
661 // - a use edge V_out -> op (=> cache V), or
662 // - a def edge op -> V_in (=> cache the produced value).
663 // Operations are never split, so at least one endpoint of the cut edge is a
664 // value, and that value is the one to cache.
665 SetVector<Value> newCaches;
666 for (const auto &pair : Split) {
667 if (!parent.count(pair.first))
668 continue;
669 for (const Node &N : pair.second) {
670 if (parent.count(N))
671 continue;
672 assert((pair.first.isValue() || N.isValue()) &&
673 "min-cut edge must transport a value");
674 Value cache = pair.first.isValue() ? pair.first.getValue() : N.getValue();
675 newCaches.insert(cache);
676 }
677 }
678 return newCaches;
679}
680
681// When the min cut is ambiguous, prefer caching the LAST value in a computation
682// chain.
683//
684// Max flow fixes the *capacity* of the cut but not which of the equal-capacity
685// cuts is returned, and `minCutValues` extracts the one reachable from the
686// sources -- i.e. the cut nearest the roots, which caches the earliest value in
687// every chain and leaves the longest possible tail to recompute in reverse.
688// Sliding a cut edge downstream past an operation that has a single graph user
689// and a single result yields another cut of the same capacity (the slid edge is
690// the only way from that value to a required op), so this never costs an extra
691// cache; it just shrinks what the reverse pass rebuilds.
692//
693// The size guard keeps the slide from trading a small buffer for a larger one:
694// capacities here are unit, so the flow minimizes the NUMBER of cached values,
695// not their bytes, and two equal-capacity cuts can differ in size.
696//
697// This mirrors the "push to cache the last value in a computation chain"
698// heuristic in the LLVM Enzyme mincut (Enzyme/DifferentialUseAnalysis.cpp).
699static void pushCachesDownstream(const Graph &Orig,
700 const SetVector<Operation *> &Required,
701 SetVector<Value> &newCaches) {
702 SmallVector<Value> todo(newCaches.begin(), newCaches.end());
703
704 while (!todo.empty()) {
705 Value cur = todo.pop_back_val();
706 // May have been slid away already by an earlier iteration.
707 if (!newCaches.contains(cur))
708 continue;
709
710 // `cur` must feed exactly one operation; otherwise moving the cut past it
711 // would mean caching several values in place of one.
712 auto users = Orig.find(Node(cur));
713 if (users == Orig.end() || users->second.size() != 1)
714 continue;
715 Node userNode = *users->second.begin();
716 if (!userNode.isOperation())
717 continue;
718 Operation *user = userNode.getOperation();
719
720 // A required operation consumes `cur` itself in the reverse pass, so there
721 // is nothing downstream of it to slide to.
722 if (Required.contains(user))
723 continue;
724
725 // ... and that operation must produce exactly one value.
726 auto results = Orig.find(userNode);
727 if (results == Orig.end() || results->second.size() != 1)
728 continue;
729 Node resNode = *results->second.begin();
730 if (!resNode.isValue())
731 continue;
732 Value next = resNode.getValue();
733
734 // Never slide into a deeper region: a value defined inside a nested block
735 // is live more often than the one it would replace. (The LLVM mincut makes
736 // the same check by loop nest, and additionally slides *outwards*; here we
737 // only ever move within one block.)
738 if (next.getParentBlock() != cur.getParentBlock())
739 continue;
740
741 // Never trade a cached buffer for a larger one.
742 if (computeSizeOfType(cur) < computeSizeOfType(next))
743 continue;
744
745 newCaches.remove(cur);
746 newCaches.insert(next);
747 // Keep sliding down the chain.
748 todo.push_back(next);
749 }
750}
751
752} // namespace
753
754// Given the full forward/backward compute graph, the push/pop can be seen
755// as a special cut of this graph. This function tries to modifies the
756// boundary of the push/pop to minimize the amount of memory that is live
757// across different loops.
758// The insertion point of rewriter must be in the reverse block, after any
759// fwdrevmap settings have been created.
760void mlir::enzyme::minCutCache(Block *forward, Block *reverse,
761 SmallVector<CacheInfo> &caches0,
762 PatternRewriter &rewriter,
763 const IRMapping &fwdrevmap, Operation *lastFwd) {
764 assert(rewriter.getInsertionBlock() == reverse);
765 assert(rewriter.getInsertionPoint()->getBlock() == reverse);
766 if (caches0.empty())
767 return;
768
769 LLVM_DEBUG(if (DebugGraphviz) annotate_ops(forward, reverse));
770
771 // where to build the new inits
772 Operation *entry = caches0[0].initOp;
773
774 IRMapping mapping = fwdrevmap;
775 SmallVector<CacheInfo> caches;
776 // Hoist out pushes of values that are defined outside of the block
777 for (auto &info : caches0) {
778 auto todo = info.pushedValue();
779 bool isDefinedOutside =
780 !forward->getParent()->isAncestor(todo.getParentRegion());
781 if (isDefinedOutside) {
782 rewriter.modifyOpInPlace(info.pushOp, [&]() {
783 if (&*rewriter.getInsertionPoint() == info.pushOp)
784 rewriter.setInsertionPoint(info.pushOp->getNextNode());
785
786 info.pushOp->moveBefore(forward->getParentOp());
787 });
788 rewriter.modifyOpInPlace(info.popOp, [&]() {
789 if (&*rewriter.getInsertionPoint() == info.popOp)
790 rewriter.setInsertionPoint(info.popOp->getNextNode());
791 info.popOp->moveBefore(reverse->getParentOp());
792 });
793 mapping.map(info.pushedValue(), info.popOp);
794 continue;
795 }
796 caches.push_back(info);
797 }
798 assert(rewriter.getInsertionPoint()->getBlock() == reverse);
799
800 if (caches.empty()) {
801 caches0.clear();
802 return;
803 }
804
805 // Maintain a mapping of forward to reverse blocks. We later use this to place
806 // the new cache pops and cloned ops to the correct blocks.
807 DenseMap<Block *, OpBuilder::InsertPoint> insertionPointMap;
808 for (const auto &info : caches) {
809 Block *fwdBlock = info.pushOp->getBlock();
810 Block *revBlock = info.popOp->getBlock();
811 // For the top-level reverse block, we use the provided rewriter's insertion
812 // point (to skip over things like IV calculations). New operations in inner
813 // blocks should be inserted at the beginning of those blocks.
814 if (revBlock == reverse) {
815 insertionPointMap[fwdBlock] = rewriter.saveInsertionPoint();
816 } else {
817 insertionPointMap[fwdBlock] =
818 OpBuilder::InsertPoint(revBlock, revBlock->begin());
819 }
820 }
821
822 Graph G;
823
824 LLVM_DEBUG(llvm::dbgs() << "trying min/cut\n");
825 LLVM_DEBUG(
826 findCommonAncestor({forward->getParentOp(), reverse->getParentOp()})
827 ->dump());
828
829 LLVM_DEBUG(llvm::dbgs() << "forward: " << *forward << "\n";);
830 LLVM_DEBUG(llvm::dbgs() << "reverse: " << *reverse << "\n";);
831
832 SmallVector<Value> worklist;
833 for (auto &cache : caches) {
834 worklist.push_back(cache.pushedValue());
835 }
836
837 // nodes that cannot be recomputed
838 SetVector<Value> roots;
839
840 // Walk Backward
841 //
842 // Roots (sources) are either block arguments or values which are defined
843 // outside of forward.
844 while (!worklist.empty()) {
845 Value todo = worklist.pop_back_val();
846
847 bool isDefinedOutside =
848 !forward->getParent()->isAncestor(todo.getParentRegion());
849 if (isDefinedOutside || fwdrevmap.contains(todo)) {
850 continue;
851 }
852
853 Operation *owner = todo.getDefiningOp();
854 if (!owner || !isMovable(owner)) {
855 roots.insert(todo);
856 continue;
857 }
858
859 bool inserted = G[Node(owner)].insert(Node(todo));
860 if (inserted) {
861 for (Value operand : owner->getOperands()) {
862 G[Node(operand)].insert(Node(owner));
863 worklist.push_back(operand);
864 }
865 }
866 if (auto regionBranch = dyn_cast<RegionBranchOpInterface>(owner)) {
867 SetVector<Value> valuesDefinedAbove;
868 mlir::getUsedValuesDefinedAbove(regionBranch->getRegions(),
869 valuesDefinedAbove);
870 for (Value val : valuesDefinedAbove) {
871 G[Node(val)].insert(Node(regionBranch));
872 worklist.push_back(val);
873 }
874 }
875 }
876
877 worklist.clear();
878
879 // The operation whose use of a value forces a value to be available
880 // in the reverse pass
881 SetVector<Operation *> Required;
882
883 {
884 for (auto &info : caches) {
885 Value poped = info.popOp.getResult();
886
887 bool isRequired = false;
888 for (auto user : poped.getUsers()) {
889 if (user->getBlock() != reverse || !isMovable(user)) {
890 G[info.pushedValue()].insert(Node(user));
891 Required.insert(user);
892 isRequired = true;
893 break;
894 }
895 }
896 if (!isRequired)
897 for (auto user : poped.getUsers()) {
898 G[Node(info.pushedValue())].insert(user);
899 for (Value res : user->getResults()) {
900 G[Node(user)].insert(res);
901 worklist.push_back(res);
902 }
903 }
904 }
905
906 // Walk Forward
907 while (!worklist.empty()) {
908 Value todo = worklist.pop_back_val();
909
910 bool isRequired = false;
911 for (auto user : todo.getUsers()) {
912 if (user->getBlock() != reverse || !isMovable(user)) {
913 G[todo].insert(Node(user));
914 Required.insert(user);
915 isRequired = true;
916 break;
917 }
918 }
919 if (isRequired)
920 continue;
921
922 for (auto user : todo.getUsers()) {
923 Node N(user);
924 bool inserted = G[Node(todo)].insert(N);
925 if (inserted) {
926 for (Value res : user->getResults()) {
927 G[N].insert(Node(res));
928 worklist.push_back(res);
929 }
930 }
931 }
932 }
933
934 for (auto N : G) {
935 if (!N.first.isOperation())
936 continue;
937 auto op = N.first.getOperation();
938 if (op->getBlock() != reverse)
939 continue;
940 for (auto v : op->getOperands()) {
941 if (v.getParentBlock() != reverse) {
942 continue;
943 }
944 if (G.contains(Node(v))) {
945 continue;
946 }
947 Required.insert(op);
948 break;
949 }
950 }
951 assert(rewriter.getInsertionPoint()->getBlock() == reverse);
952
953 LLVM_DEBUG(llvm::dbgs() << "Required: \n";);
954 LLVM_DEBUG(for (auto R : Required) llvm::dbgs() << " + " << *R << "\n";);
955
956 LLVM_DEBUG(llvm::dbgs() << "Roots: \n";);
957 LLVM_DEBUG(for (auto R : roots) llvm::dbgs() << " + " << R << "\n";);
958 }
959
960 LLVM_DEBUG(llvm::dbgs() << "pre filter graph: \n";);
961 LLVM_DEBUG(dump(G));
962 G = filterGraph(G, roots, Required);
963 LLVM_DEBUG(llvm::dbgs() << "post filter graph: \n";);
964 LLVM_DEBUG(dump(G));
965
966 Graph Orig = G;
967
968 // Compute the values to cache via a minimum cut. The cut is computed on a
969 // node-split version of the graph (see minCutValues), so that caching a value
970 // costs exactly one regardless of how many operations consume it. Without the
971 // split, a value used by N operations is charged N (one per outgoing edge),
972 // which causes the mincut to prefer caching several downstream values over a
973 // single cheaper upstream one (e.g. an input reused many times). This mirrors
974 // the node-splitting done by the LLVM Enzyme mincut in
975 // DifferentialUseAnalysis.cpp.
976 SetVector<Value> newCaches = minCutValues(Orig, roots, Required);
977
978 // The cut above is only one of several equal-capacity min cuts; slide it as
979 // far downstream as is free so the reverse pass recomputes as little as
980 // possible.
981 pushCachesDownstream(Orig, Required, newCaches);
982
983 assert(rewriter.getInsertionPoint()->getBlock() == reverse);
984
985 // compute path from new caches to required
986 DenseMap<Node, Node> parent;
987 bfs(Orig, newCaches, parent);
988
989 LLVM_DEBUG({
990 llvm::dbgs() << "initial new caches: \n";
991 for (Value v : newCaches) {
992 v.dump();
993 }
994 });
995
996 // The cachegraph is a sub graph of Orig with only pathes new caches
997 // to Required nodes.
998 Graph cacheGraph = filterGraph(Orig, newCaches, Required);
999
1000 LLVM_DEBUG(llvm::dbgs() << "cacheGraph:\n");
1001 LLVM_DEBUG(dump(cacheGraph));
1002
1003 SmallVector<CacheInfo> newCacheInfos;
1004
1005 // We guard here so then the IP after this is immediately before the new pop's
1006 Operation *firstClone = nullptr;
1007
1008 // Refine cached values based on some heuristics
1009 if (newCaches.size()) {
1010
1011 // sort caches to provide determinism.
1012 // llvm::sort(newCaches.getArrayRef().begin(),
1013 // newCaches.getArrayRef().end(), mlir::enzyme::valueCmp);
1014
1015 SmallVector<Value> todo(newCaches.begin(), newCaches.end());
1016 while (todo.size()) {
1017 auto cur = todo.pop_back_val();
1018
1019 auto &next = cacheGraph.at(Node(cur));
1020
1021 if (next.size() > 1)
1022 continue;
1023
1024 auto nextF = *next.begin();
1025 assert(nextF.isOperation());
1026 auto opNext = nextF.getOperation();
1027
1028 if (Required.count(opNext))
1029 continue;
1030
1031 if (opNext->getNumResults() != 1)
1032 continue;
1033
1034 Value candidate = opNext->getResult(0);
1035
1036 int64_t curSize = computeSizeOfType(cur),
1037 curRank = computeRankOfType(cur);
1038
1039 int64_t newSize = computeSizeOfType(candidate),
1040 newRank = computeRankOfType(candidate);
1041
1042 if (newRank < curRank || (newRank == curRank && newSize < curSize)) {
1043 newCaches.remove(cur);
1044 newCaches.insert(candidate);
1045 todo.push_back(candidate);
1046 cacheGraph.erase(cur);
1047 cacheGraph.erase(opNext);
1048 }
1049 }
1050
1051 LLVM_DEBUG(llvm::dbgs() << "refined cacheGraph:\n");
1052 LLVM_DEBUG(dump(cacheGraph));
1053 LLVM_DEBUG({
1054 llvm::dbgs() << "refined new caches: \n";
1055 for (Value v : newCaches) {
1056 v.dump();
1057 }
1058 });
1059
1060 SetVector<Value> reverseCaches;
1061 for (Value newCache : newCaches) {
1062 if (!forward->getParent()->isAncestor(newCache.getParentRegion())) {
1063 reverseCaches.insert(newCache);
1064 continue;
1065 }
1066 assert(rewriter.getInsertionBlock() == reverse);
1067
1068 enzyme::InitOp initOp = ({
1069 OpBuilder::InsertionGuard guard(rewriter);
1070 rewriter.setInsertionPoint(entry);
1071 enzyme::InitOp::create(
1072 rewriter, newCache.getLoc(),
1073 enzyme::CacheType::get(newCache.getContext(), newCache.getType()));
1074 });
1075
1076 enzyme::PushOp pushOp = ({
1077 OpBuilder::InsertionGuard guard(rewriter);
1078 if (lastFwd && isa<BlockArgument>(newCache)) {
1079 rewriter.setInsertionPointAfter(lastFwd);
1080 } else {
1081 rewriter.setInsertionPointAfterValue(newCache);
1082 }
1083 enzyme::PushOp::create(rewriter, newCache.getLoc(), initOp.getResult(),
1084 newCache);
1085 });
1086
1087 OpBuilder::InsertionGuard guard(rewriter);
1088 rewriter.restoreInsertionPoint(
1089 insertionPointMap.lookup(newCache.getParentBlock()));
1090 enzyme::PopOp popOp = enzyme::PopOp::create(
1091 rewriter, newCache.getLoc(), newCache.getType(), initOp.getResult());
1092 insertionPointMap[newCache.getParentBlock()] =
1093 rewriter.saveInsertionPoint();
1094 if (!firstClone)
1095 firstClone = popOp;
1096 mapping.map(newCache, popOp.getResult());
1097
1098 CacheInfo info;
1099 info.initOp = initOp;
1100 info.pushOp = pushOp;
1101 info.popOp = popOp;
1102 newCacheInfos.push_back(info);
1103 }
1104
1105 if (reverseCaches.size()) {
1106 Graph fwdGraph = filterGraph(Orig, roots, newCaches);
1107
1108 IRMapping fwdmap;
1109 for (auto &info : caches) {
1110 fwdmap.map(info.popOp->getResult(0), info.pushedValue());
1111 }
1112
1113 SmallVector<Operation *> toErase;
1114 for (auto &op : llvm::make_early_inc_range(*reverse)) {
1115 if (!fwdGraph.contains(Node(&op)))
1116 continue;
1117
1118 Operation *newO = ({
1119 OpBuilder::InsertionGuard guard(rewriter);
1120 rewriter.setInsertionPoint(forward->getTerminator());
1121 rewriter.clone(op, fwdmap);
1122 });
1123
1124 bool hasUse = false;
1125 for (auto &&[res, newRes] :
1126 llvm::zip_equal(op.getResults(), newO->getResults())) {
1127 if (newCaches.contains(res)) {
1128 enzyme::InitOp initOp = ({
1129 OpBuilder::InsertionGuard guard(rewriter);
1130 rewriter.setInsertionPoint(entry);
1131 enzyme::InitOp::create(rewriter, newRes.getLoc(),
1132 enzyme::CacheType::get(newRes.getContext(),
1133 newRes.getType()));
1134 });
1135
1136 enzyme::PushOp pushOp = ({
1137 OpBuilder::InsertionGuard guard(rewriter);
1138 rewriter.setInsertionPoint(forward->getTerminator());
1139 enzyme::PushOp::create(rewriter, newRes.getLoc(),
1140 initOp.getResult(), newRes);
1141 });
1142
1143 enzyme::PopOp popOp = ({
1144 OpBuilder::InsertionGuard guard(rewriter);
1145 rewriter.setInsertionPoint(&op);
1146 enzyme::PopOp::create(rewriter, newRes.getLoc(), newRes.getType(),
1147 initOp.getResult());
1148 });
1149
1150 rewriter.replaceAllUsesWith(res, popOp->getResult(0));
1151
1152 CacheInfo info;
1153 info.initOp = initOp;
1154 info.pushOp = pushOp;
1155 info.popOp = popOp;
1156 newCacheInfos.push_back(info);
1157 }
1158 if (!hasUse) {
1159 for (auto user : res.getUsers()) {
1160 if (!fwdGraph.contains(Node(user))) {
1161 hasUse = true;
1162 break;
1163 }
1164 }
1165 }
1166 }
1167
1168 if (!hasUse && !op.hasAttr("enzyme.no_erase")) {
1169 toErase.push_back(&op);
1170 }
1171 }
1172 for (auto op : llvm::reverse(toErase)) {
1173 rewriter.eraseOp(op);
1174 }
1175 }
1176 }
1177
1178 forward->walk([&](Operation *op) {
1179 if (!cacheGraph.contains(Node(op)))
1180 return;
1181 bool hasUse = false;
1182 for (auto res : op->getResults()) {
1183 if (newCaches.contains(res)) {
1184 continue;
1185 }
1186 hasUse = true;
1187 }
1188 if (!hasUse)
1189 return;
1190 for (auto v : op->getOperands()) {
1191 if (mapping.contains(v))
1192 continue;
1193 if (forward->getParent()->isAncestor(v.getParentRegion()))
1194 continue;
1195
1196 enzyme::InitOp initOp = ({
1197 OpBuilder::InsertionGuard guard(rewriter);
1198 rewriter.setInsertionPoint(entry);
1199 enzyme::InitOp::create(
1200 rewriter, v.getLoc(),
1201 enzyme::CacheType::get(v.getContext(), v.getType()));
1202 });
1203
1204 {
1205 OpBuilder::InsertionGuard guard(rewriter);
1206 rewriter.setInsertionPoint(forward->getParentOp());
1207 enzyme::PushOp::create(rewriter, v.getLoc(), initOp.getResult(), v);
1208 };
1209
1210 enzyme::PopOp popOp = ({
1211 OpBuilder::InsertionGuard guard(rewriter);
1212 rewriter.setInsertionPoint(reverse->getParentOp());
1213 enzyme::PopOp::create(rewriter, v.getLoc(), v.getType(),
1214 initOp.getResult());
1215 });
1216 mapping.map(v, popOp->getResult(0));
1217 }
1218 OpBuilder::InsertionGuard guard(rewriter);
1219 rewriter.restoreInsertionPoint(insertionPointMap.lookup(op->getBlock()));
1220 auto cop = rewriter.clone(*op, mapping);
1221 insertionPointMap[op->getBlock()] = rewriter.saveInsertionPoint();
1222 if (!firstClone)
1223 firstClone = cop;
1224 });
1225
1226 if (firstClone)
1227 rewriter.setInsertionPoint(firstClone);
1228
1229 // Remove old caches
1230 for (auto &info : caches) {
1231 if (mapping.contains(info.pushedValue())) {
1232 rewriter.replaceOp(info.popOp, mapping.lookup(info.pushedValue()));
1233 } else {
1234 rewriter.eraseOp(info.popOp);
1235 }
1236 rewriter.eraseOp(info.pushOp);
1237 rewriter.eraseOp(info.initOp);
1238 }
1239
1240 LLVM_DEBUG(llvm::dbgs() << "post min/cut\n");
1241 LLVM_DEBUG(
1242 findCommonAncestor({forward->getParentOp(), reverse->getParentOp()})
1243 ->dump());
1244
1245 caches0 = std::move(newCacheInfos);
1246}
1247
1248static LogicalResult loopInvariantCacheImpl(CacheInfo info,
1249 LoopLikeOpInterface fwdLoop,
1250 LoopLikeOpInterface revLoop,
1251 IRMapping &mapping,
1252 int64_t threshold) {
1253 Operation *definingOp = info.pushedValue().getDefiningOp();
1254 if (!definingOp)
1255 return failure();
1256 auto loadOp = dyn_cast<memref::LoadOp>(definingOp);
1257 if (!loadOp)
1258 return failure();
1259 auto alloc = loadOp.getMemRef();
1260 auto allocaOp = dyn_cast_if_present<memref::AllocaOp>(alloc.getDefiningOp());
1261 if (!allocaOp)
1262 return failure();
1263 // This transformation will store the entire allocation, so only store when
1264 // it has a known "small" size.
1265 if (!(allocaOp.getType().hasStaticShape() &&
1266 allocaOp.getType().getNumElements() <= threshold)) {
1267 return failure();
1268 }
1269
1270 // Check if the allocation is written to inside the loop body
1271 // TODO: need alias analysis for full correctness
1272 auto walkResult = fwdLoop->walk([&alloc](Operation *op) {
1273 if (hasEffect<MemoryEffects::Write>(op, alloc) || hasUnknownEffects(op))
1274 return WalkResult::interrupt();
1275 return WalkResult::advance();
1276 });
1277 if (walkResult.wasInterrupted())
1278 return failure();
1279
1280 ImplicitLocOpBuilder builder(alloc.getLoc(), info.initOp);
1281 // Find the values that are necessary to re-compute the indices
1282 SmallVector<Value> pushedValues;
1283 SmallVector<Operation *> toCopy;
1284 std::queue<Value> worklist;
1285 DenseSet<Value> visited;
1286 for (Value idx : loadOp.getIndices()) {
1287 worklist.push(idx);
1288 visited.insert(idx);
1289 }
1290 while (!worklist.empty()) {
1291 Value curr = worklist.front();
1292 worklist.pop();
1293
1294 if (mapping.contains(curr))
1295 continue;
1296 DominanceInfo dom;
1297 // Check if we can just re-use the same value in forward and reverse
1298 if (dom.dominates(curr, revLoop))
1299 continue;
1300
1301 if (Operation *definingOp = curr.getDefiningOp()) {
1302 // Push values that are defined outside the fwd loop while copying ops
1303 // defined inside
1304 if (fwdLoop->isProperAncestor(definingOp)) {
1305 toCopy.push_back(definingOp);
1306 for (Value operand : definingOp->getOperands()) {
1307 if (!visited.contains(operand)) {
1308 visited.insert(operand);
1309 worklist.push(operand);
1310 }
1311 }
1312 } else {
1313 pushedValues.push_back(curr);
1314 }
1315 } else {
1316 return failure();
1317 }
1318 }
1319
1320 // Make new caches for the values needed to store the indices
1321 builder.setInsertionPoint(info.initOp);
1322 SmallVector<Value> newInits =
1323 llvm::map_to_vector(pushedValues, [&](Value val) -> Value {
1324 return enzyme::InitOp::create(
1325 builder, CacheType::get(val.getContext(), val.getType()));
1326 });
1327 builder.setInsertionPoint(fwdLoop);
1328 for (auto &&[cache, val] : llvm::zip(newInits, pushedValues)) {
1329 enzyme::PushOp::create(builder, cache, val);
1330 }
1331
1332 builder.setInsertionPoint(revLoop);
1333 for (auto &&[cache, val] : llvm::zip(newInits, pushedValues)) {
1334 Value popped = enzyme::PopOp::create(builder, val.getType(), cache);
1335 mapping.map(val, popped);
1336 }
1337
1338 // Clone the ops necessary to re-compute the load indices
1339 builder.setInsertionPoint(info.popOp);
1340 for (Operation *op : llvm::reverse(toCopy)) {
1341 builder.clone(*op, mapping);
1342 }
1343
1344 Value cachedMemRef;
1345 if (mapping.contains(alloc)) {
1346 cachedMemRef = mapping.lookup(alloc);
1347 } else {
1348 builder.setInsertionPoint(info.initOp);
1349 Value newCache = enzyme::InitOp::create(
1350 builder, enzyme::CacheType::get(builder.getContext(), alloc.getType()));
1351
1352 builder.setInsertionPoint(fwdLoop);
1353 Value copyAlloc = memref::AllocOp::create(
1354 builder, allocaOp.getType(), allocaOp.getDynamicSizes(),
1355 allocaOp.getSymbolOperands(), allocaOp.getAlignmentAttr());
1356 // Copy over the contents in a loop nest so any resulting subviews may be
1357 // folded
1358 Value zero = arith::ConstantIndexOp::create(builder, 0);
1359 Value one = arith::ConstantIndexOp::create(builder, 1);
1360
1361 int64_t rank = loadOp.getMemRefType().getRank();
1362 SmallVector<Value> lbs(rank, zero);
1363 SmallVector<Value> steps(rank, one);
1364 SmallVector<Value> ubs(rank);
1365 for (int64_t i = 0; i < rank; i++)
1366 ubs[i] = memref::DimOp::create(builder, alloc, i);
1367
1368 scf::buildLoopNest(
1369 builder, builder.getLoc(), lbs, ubs, steps,
1370 [&](OpBuilder &bodyBuilder, Location loc, ValueRange ivs) {
1371 Value loaded = memref::LoadOp::create(bodyBuilder, loc, alloc, ivs);
1372 memref::StoreOp::create(bodyBuilder, loc, loaded, copyAlloc, ivs);
1373 });
1374
1375 enzyme::PushOp::create(builder, newCache, copyAlloc);
1376
1377 builder.setInsertionPoint(revLoop);
1378 cachedMemRef = enzyme::PopOp::create(builder, alloc.getType(), newCache);
1379 mapping.map(alloc, cachedMemRef);
1380 }
1381
1382 builder.setInsertionPoint(info.popOp);
1383 SmallVector<Value> revIndices =
1384 llvm::map_to_vector(loadOp.getIndices(), [&](Value idx) -> Value {
1385 return mapping.lookupOrDefault(idx);
1386 });
1387
1388 Value newLoad = memref::LoadOp::create(builder, cachedMemRef, revIndices);
1389 info.popOp.replaceAllUsesWith(newLoad);
1390 return success();
1391}
1392
1393void mlir::enzyme::loopInvariantCache(SmallVectorImpl<CacheInfo> &caches,
1394 LoopLikeOpInterface fwdLoop,
1395 LoopLikeOpInterface revLoop,
1396 const IRMapping &fwdrevmap,
1397 int64_t threshold) {
1398 SmallVector<CacheInfo> newCaches;
1399 IRMapping mapping = fwdrevmap;
1400 for (auto info : caches) {
1401 if (succeeded(loopInvariantCacheImpl(info, fwdLoop, revLoop, mapping,
1402 threshold))) {
1403 info.pushOp->erase();
1404 info.popOp->erase();
1405 info.initOp->erase();
1406 } else {
1407 newCaches.push_back(info);
1408 }
1409 }
1410 caches = std::move(newCaches);
1411}
1412
1415 mlir::PatternRewriter &rewriter) {
1416 assert(other.pushOp->getBlock() == pushOp->getBlock());
1417 assert(other.popOp->getBlock() == popOp->getBlock());
1418
1419 enzyme::InitOp newInitOp;
1420 if (other.initOp->isBeforeInBlock(initOp)) {
1421 newInitOp = other.initOp;
1422 rewriter.replaceAllUsesWith(initOp.getResult(), newInitOp.getResult());
1423 rewriter.eraseOp(initOp);
1424 } else {
1425 newInitOp = initOp;
1426 rewriter.replaceAllUsesWith(other.initOp.getResult(),
1427 newInitOp.getResult());
1428 rewriter.eraseOp(other.initOp);
1429 }
1430
1431 rewriter.eraseOp(other.pushOp);
1432
1433 enzyme::PopOp newPopOp;
1434 if (other.popOp->isBeforeInBlock(popOp)) {
1435 newPopOp = other.popOp;
1436 rewriter.replaceAllUsesWith(popOp.getResult(), newPopOp.getResult());
1437 rewriter.eraseOp(popOp);
1438 } else {
1439 newPopOp = popOp;
1440 rewriter.replaceAllUsesWith(other.popOp.getResult(), newPopOp.getResult());
1441 rewriter.eraseOp(other.popOp);
1442 }
1443
1444 CacheInfo newInfo{newInitOp};
1445 return newInfo;
1446}
static bool operator==(const LoopContext &lhs, const LoopContext &rhs)
void dump(const Constraints &c)
#define getOp(vtmp)
static LogicalResult loopInvariantCacheImpl(CacheInfo info, LoopLikeOpInterface fwdLoop, LoopLikeOpInterface revLoop, IRMapping &mapping, int64_t threshold)
static llvm::cl::opt< bool > DebugGraphviz("mincut-print-graphviz", llvm::cl::init(false), llvm::cl::Hidden, llvm::cl::desc("Use with DEBUG_TYPE 'enzyme-mincut' to print " "the mincut graphs in GraphViz"))
mlir::Value getDifferential(mlir::Value origv)
bool isConstantValue(mlir::Value v) const
void bfs(const std::map< Node, std::set< Node > > &G, const llvm::SetVector< llvm::Value * > &Recompute, std::map< Node, Node > &parent)
void removalBlockExplore(Block *block, IRMapping &mapping, PatternRewriter &rewriter, llvm::SetVector< Value > &gradients, llvm::MapVector< Value, CacheInfo > &caches)
void localizeGradients(OpBuilder &builder, MGradientUtilsReverse *gutils, Block *fwd)
void minCutCache(Block *forward, Block *reverse, SmallVector< CacheInfo > &caches, PatternRewriter &rewriter, const IRMapping &fwdrevmap, Operation *lastFwd=nullptr)
void loopInvariantCache(SmallVectorImpl< CacheInfo > &caches, LoopLikeOpInterface fwdLoop, LoopLikeOpInterface revLoop, const IRMapping &mapping, int64_t threshold=16)
Information about a cache, each cache init should have one corresponding push and pop.
CacheInfo merge(CacheInfo other, PatternRewriter &rewriter)
enzyme::InitOp initOp
enzyme::PushOp pushOp