Enzyme main
Loading...
Searching...
No Matches
SinkCheckpointViewsPass.cpp
Go to the documentation of this file.
1//===- SinkCheckpointViewsPass.cpp - Sink views into checkpointed loops ---- //
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file implements a pass that sinks pure views of mutable values into the
10// checkpointed loops that use them, so that a view and the value it views are
11// never captured by the same loop.
12//
13//===----------------------------------------------------------------------===//
14
16#include "Passes/Passes.h"
17
18#include "mlir/Dialect/SCF/IR/SCF.h"
19#include "mlir/IR/Matchers.h"
20#include "mlir/Interfaces/SideEffectInterfaces.h"
21#include "mlir/Interfaces/ViewLikeInterface.h"
22#include "mlir/Transforms/RegionUtils.h"
23
24namespace mlir {
25namespace enzyme {
26using namespace mlir::enzyme;
27#define GEN_PASS_DEF_SINKCHECKPOINTVIEWSPASS
28#include "Passes/Passes.h.inc"
29} // namespace enzyme
30} // namespace mlir
31
32namespace {
33using namespace mlir;
34using namespace enzyme;
35
36// Checkpointing snapshots every mutable value a loop body uses from outside it,
37// so that the reverse pass can replay a segment of the primal from a restored
38// state. When both a value and a pure view of that value are captured, both get
39// snapshotted -- but they are the same memory, so the loop ends up recomputing
40// from two independent copies of one buffer, and only the writes that happen to
41// go through the copy that was restored last are visible. No alias analysis
42// makes that correct; the two clones are the bug.
43//
44// It also fails much more loudly than that on a GPU: the view's type carries no
45// memory space (a `memref<?xi8>` view of a device pointer looks like host
46// memory), so its snapshot is a host malloc/memcpy of device memory, which
47// segfaults before any wrong answer can be observed.
48//
49// Sinking the view into the loop region leaves only its source captured. The
50// view is then re-derived inside the body from whatever the source resolves to,
51// which during a replay is the one restored checkpoint.
52static bool isCheckpointed(Operation *op) {
53 auto enable = op->getAttrOfType<BoolAttr>("enzyme.enable_checkpointing");
54 return enable && enable.getValue();
55}
56
57/// A view op is safe to re-evaluate anywhere its operands are available, which
58/// is what makes sinking it a no-op semantically.
59static bool isSinkableView(Operation *op) {
60 return op && isa<ViewLikeOpInterface>(op) && isMemoryEffectFree(op) &&
61 op->getNumResults() == 1 && op->getNumRegions() == 0;
62}
63
64/// Replaces uses of `view`'s result inside `loop` with a copy of `view` placed
65/// at the top of each of the loop's region entry blocks. `view`'s operands are
66/// defined above the loop, so they dominate those insertion points.
67static void sinkViewInto(Operation *loop, Operation *view) {
68 Value result = view->getResult(0);
69
70 for (Region &region : loop->getRegions()) {
71 if (region.empty())
72 continue;
73
74 // Only clone if this region actually reads the view.
75 bool used = llvm::any_of(result.getUses(), [&](OpOperand &use) {
76 return region.isAncestor(use.getOwner()->getParentRegion());
77 });
78 if (!used)
79 continue;
80
81 OpBuilder builder(&region.front(), region.front().begin());
82 Operation *sunk = builder.clone(*view);
83 result.replaceUsesWithIf(sunk->getResult(0), [&](OpOperand &use) {
84 return use.getOwner() != sunk &&
85 region.isAncestor(use.getOwner()->getParentRegion());
86 });
87 }
88}
89
90/// Sinks captured views until none of `loop`'s captures is a view of a mutable
91/// value. Iterates because sinking a view exposes its source as a new capture,
92/// which may itself be a view.
93static void sinkCapturedViews(Operation *loop) {
94 bool changed = true;
95 while (changed) {
96 changed = false;
97
98 SetVector<Value> captures;
99 getUsedValuesDefinedAbove(loop->getRegions(), captures);
100
101 for (Value capture : captures) {
102 // Only mutable captures get snapshotted, so only they can be double
103 // cloned; sinking anything else would just be code motion.
104 if (!isa<ClonableTypeInterface>(capture.getType()))
105 continue;
106
107 Operation *def = capture.getDefiningOp();
108 if (!isSinkableView(def))
109 continue;
110
111 sinkViewInto(loop, def);
112 changed = true;
113 break; // captures is stale now
114 }
115 }
116}
117
118struct SinkCheckpointViewsPass
119 : public enzyme::impl::SinkCheckpointViewsPassBase<
120 SinkCheckpointViewsPass> {
121 using SinkCheckpointViewsPassBase::SinkCheckpointViewsPassBase;
122
123 void runOnOperation() override {
124 SmallVector<Operation *> loops;
125 getOperation()->walk([&](LoopLikeOpInterface loop) {
126 if (isCheckpointed(loop))
127 loops.push_back(loop);
128 });
129
130 for (Operation *loop : loops)
131 sinkCapturedViews(loop);
132 }
133};
134
135} // end anonymous namespace