Enzyme main
Loading...
Searching...
No Matches
EnzymeBatchDiffPass.h
Go to the documentation of this file.
1#ifndef ENZYME_BATCH_DIFF_PASS_H
2#define ENZYME_BATCH_DIFF_PASS_H
3
4#include "Dialect/Ops.h"
6#include "Interfaces/Utils.h"
7#include "PassDetails.h"
8#include "Passes/Passes.h"
9
10#include "mlir/Analysis/AliasAnalysis.h"
11#include "mlir/Dialect/Func/IR/FuncOps.h"
12#include "mlir/IR/Builders.h"
13#include "mlir/Interfaces/FunctionInterfaces.h"
14#include <cstdint>
15
16namespace mlir {
17namespace enzyme {
18namespace batchutils {
19
21 FunctionOpInterface function;
22 SmallVector<mlir::Value> inputs;
23 SmallVector<enzyme::Activity> inActivity;
24 SmallVector<enzyme::Activity> retActivity;
25 Block *blk;
26
27 // for use in std::map:
28 bool operator<(const BatchDiffCacheKey &other) const {
29 auto lhs_name = const_cast<FunctionOpInterface &>(function).getName();
30 auto rhs_name = const_cast<FunctionOpInterface &>(other.function).getName();
31
32 if (lhs_name < rhs_name)
33 return true;
34 if (rhs_name < lhs_name)
35 return false;
36 if (inputs.size() < other.inputs.size())
37 return true;
38 if (other.inputs.size() < inputs.size())
39 return false;
40
41 // Sizes are equal, so compare elements
42 for (size_t i = 0; i < inputs.size(); ++i) {
43 auto lhs_ptr = inputs[i].getAsOpaquePointer();
44 auto rhs_ptr = other.inputs[i].getAsOpaquePointer();
45 if (lhs_ptr < rhs_ptr)
46 return true;
47 if (rhs_ptr < lhs_ptr)
48 return false;
49 }
50
51 if (inActivity < other.inActivity)
52 return true;
53 if (other.inActivity < inActivity)
54 return false;
55 if (retActivity < other.retActivity)
56 return true;
57 if (other.retActivity < retActivity)
58 return false;
59
60 return blk < other.blk;
61 }
62};
63
64template <typename SourceOp>
65BatchDiffCacheKey createDiffCacheKey(SourceOp uop, FunctionOpInterface fn) {
66 // extract in_activity, ret_activity, in_args
67 SmallVector<Activity> inActivity(
68 uop.getActivity().template getAsValueRange<ActivityAttr>());
69 SmallVector<Activity> retActivity(
70 uop.getRetActivity().template getAsValueRange<ActivityAttr>());
71 SmallVector<Value> in_args = uop.getPrimalInputs();
72 batchutils::BatchDiffCacheKey key{fn, in_args, inActivity, retActivity,
73 uop->getBlock()};
74 return key;
75}
76
77template <typename SourceOp,
78 std::enable_if_t<
79 llvm::is_one_of<SourceOp, ForwardDiffOp, AutoDiffOp>::value,
80 bool> = true>
81SmallVector<MemoryEffects::EffectInstance> findCallerEffects(
82 SourceOp callerOp, FunctionOpInterface innerFnOp,
83 const SmallVector<MemoryEffects::EffectInstance> &innerEffects) {
84 SmallVector<MemoryEffects::EffectInstance> outerEffects;
85 for (auto &eff : innerEffects) {
86
87 Value effVal = eff.getValue();
88 if (!effVal) {
89 // unknown effect which isn't tied to a value, just add to result
90 outerEffects.push_back(eff);
91 continue;
92 }
93
94 // Find primal argument corresponding to effect value
95 size_t primalArgPos = 0;
96 bool foundPrimal = false;
97 if (auto effBA = dyn_cast<BlockArgument>(effVal)) {
98 if (llvm::is_contained(innerFnOp.getArguments(), effBA)) {
99 foundPrimal = true;
100 primalArgPos = effBA.getArgNumber();
101 }
102 }
103
104 if (!foundPrimal) {
105 // TODO: Handle this either as a global value, or a value which
106 // is inside of the MLIR function(for inter-proc alias analysis) -
107 // Just skip for now, since we don't have interprocedural alias-analysis
108 // implemented yet.
109 continue;
110 }
111
112 // Add primal effects to caller effect map for all ops
113 Value primalVal = callerOp.getPrimalInputs()[primalArgPos];
114 outerEffects.push_back(
115 oputils::getEffectOfVal(primalVal, eff.getEffect(), eff.getResource()));
116
117 // Add derivative effects(only if primal arg is dup)
118 // read(primal) -> read(derivative)
119 // write(primal) -> write(derivative)
120
121 // find position of dup arg for primal
122 bool primalIsDup =
123 (cast<ActivityAttr>(callerOp.getActivity()[primalArgPos]).getValue() ==
124 Activity::enzyme_dup) ||
125 (cast<ActivityAttr>(callerOp.getActivity()[primalArgPos]).getValue() ==
126 Activity::enzyme_dupnoneed);
127
128 if (primalIsDup) {
129 auto gradArgPos = 0;
130 for (auto [idx, act] : llvm::enumerate(callerOp.getActivity())) {
131 auto iattr = cast<ActivityAttr>(act);
132 auto act_val = iattr.getValue();
133 ++gradArgPos;
134
135 if (idx == primalArgPos)
136 break;
137
138 if (act_val == Activity::enzyme_dup ||
139 act_val == Activity::enzyme_dupnoneed) {
140 ++gradArgPos;
141 }
142 }
143
144 Value dVal = callerOp.getInputs()[gradArgPos];
145 // specialze effects based on callerOp type
146 if constexpr (std::is_same_v<SourceOp, ForwardDiffOp>) {
147 outerEffects.push_back(
148 oputils::getEffectOfVal(dVal, eff.getEffect(), eff.getResource()));
149 } else {
150 outerEffects.push_back(oputils::getEffectOfVal(
151 dVal, MemoryEffects::Write::get(), eff.getResource()));
152 outerEffects.push_back(oputils::getEffectOfVal(
153 dVal, MemoryEffects::Read::get(), eff.getResource()));
154 }
155 }
156 }
157 return outerEffects;
158}
159
160template <typename SourceOp,
161 std::enable_if_t<
162 llvm::is_one_of<SourceOp, ForwardDiffOp, AutoDiffOp>::value,
163 bool> = true>
164llvm::SmallVector<SourceOp, 2> pruneGradDefs(BatchDiffCacheKey &key,
165 SmallVector<SourceOp> &allDiffs) {
166 SmallVector<SourceOp, 2> prunedSources;
167
168 // We first prune and check that all derivative arguments are defined before
169 // the first diff in the same block. (ops in allDiffs are guaranteed to belong
170 // to the same basic block)
171 auto firstDiffOp = allDiffs[0];
172 for (auto uop : allDiffs) {
173
174 auto diffArgs = uop.getShadows();
175 if constexpr (std::is_same_v<SourceOp, AutoDiffOp>) {
176 auto diffeRet = uop.getDifferentialReturns();
177 diffArgs.append(diffeRet.begin(), diffeRet.end());
178 }
179
180 bool definedBeforeFirst = true;
181
182 for (auto diffVal : diffArgs) {
183 if (auto diffValOR = dyn_cast<OpResult>(diffVal)) {
184 // check that defining op appears before the current op
185 auto parentOp = diffValOR.getOwner();
186 if (!parentOp->isBeforeInBlock(firstDiffOp.getOperation())) {
187 definedBeforeFirst = false;
188 break;
189 }
190 }
191 }
192
193 if (definedBeforeFirst) {
194 prunedSources.push_back(uop);
195 }
196 }
197
198 return prunedSources;
199}
200
201template <typename SourceOp,
202 std::enable_if_t<
203 llvm::is_one_of<SourceOp, ForwardDiffOp, AutoDiffOp>::value,
204 bool> = true>
205llvm::SmallVector<SourceOp> pruneMemoryEffects(
206 SymbolTableCollection &symbolTable, BatchDiffCacheKey &key,
207 SmallVector<SourceOp> &prunedSources,
208 DenseMap<SourceOp, SmallVector<MemoryEffects::EffectInstance>>
209 &callerEffectMap,
210 llvm::DenseMap<FunctionOpInterface,
211 SmallVector<MemoryEffects::EffectInstance>>
212 &innerEffectCache) {
213 // Find a mergeable subset of diff operations, which do not violate memory
214 // effects wrt reads and writes. Note that callerEffects only contains the
215 // aliased set of primal effects, so we have to first map these primal effects
216 // to corresponding derivative effects in `prunedSources`
217 // TODO: Also handle global values, and non-primal values inside callerEffects
218 // through inter-procedural alias analysis. Skip for now
219
220 if (callerEffectMap.empty()) {
221 // legal to merge since there is no effect overwrite in mergeable ops
222 return prunedSources;
223 }
224
225 SmallVector<SourceOp> legalMerge;
226 auto lastOp = prunedSources[0];
227
228 SmallVector<MemoryEffects::EffectInstance, 4> betweenEffects;
229 for (auto candidateOp : prunedSources) {
230 // Update betweenEffects to include memory effects from lastOp to
231 // candidateOp
232 for (Operation *curr = lastOp.getOperation();
233 curr != candidateOp.getOperation(); curr = curr->getNextNode()) {
234 auto currSourceOp = dyn_cast<SourceOp>(curr);
235 if (currSourceOp && callerEffectMap.contains(currSourceOp)) {
236 // curr is/was a mergeable candidate, and we would have already computed
237 // its memory effects in the effect map
238 betweenEffects.append(callerEffectMap[currSourceOp]);
239 } else if (auto currFwdOp = dyn_cast<ForwardDiffOp>(curr)) {
240 // curr is a previously un-encountered fwddiff op
241 auto fnOp = dyn_cast_or_null<FunctionOpInterface>(
242 symbolTable.lookupNearestSymbolFrom(currFwdOp,
243 currFwdOp.getFnAttr()));
244 if (!fnOp)
245 continue;
246
247 if (!innerEffectCache.contains(fnOp)) {
248 innerEffectCache[fnOp] = oputils::collectFnEffects(fnOp);
249 }
250
251 // map to outerEffects
252 betweenEffects.append(batchutils::findCallerEffects(
253 currFwdOp, fnOp, innerEffectCache[fnOp]));
254 } else if (auto currBwdOp = dyn_cast<AutoDiffOp>(curr)) {
255 // curr is a previously un-encountered revdiff op
256 auto fnOp = dyn_cast_or_null<FunctionOpInterface>(
257 symbolTable.lookupNearestSymbolFrom(currBwdOp,
258 currBwdOp.getFnAttr()));
259 if (!fnOp)
260 continue;
261
262 if (!innerEffectCache.contains(fnOp)) {
263 innerEffectCache[fnOp] = oputils::collectFnEffects(fnOp);
264 }
265
266 // map to outerEffects
267 betweenEffects.append(batchutils::findCallerEffects(
268 currBwdOp, fnOp, innerEffectCache[fnOp]));
269 } else {
270 // TODO: move forwarddiff and revdiff effect collection specialization
271 // from `findCallerEffects` into collectOpEffects(), accounting for
272 // inter-procedural alias analysis
273 SmallVector<MemoryEffects::EffectInstance> currOpEffects;
274 (void)oputils::collectOpEffects(curr, currOpEffects);
275 betweenEffects.append(currOpEffects);
276 }
277 }
278
279 // Check conflicts between betweenEffects and candidateOp. Since the batched
280 // version essentially "pushes up" the candidateOp, we ideally want to stop
281 // this if it violates the final order of writes to the candidate op owned
282 // value
283 bool foundConflict = false;
284 for (auto candidateEffect : callerEffectMap[candidateOp]) {
285 for (auto prevEffect : betweenEffects) {
286 // We will disable batching any candidiate operation which re-orders the
287 // relative order of writes to the primal and derivative arguments. For
288 // this, we alias the underlying effects in the preceding effects and
289 // the current candidate operation.
290 if ((isa<MemoryEffects::Write>(prevEffect.getEffect()) &&
291 isa<MemoryEffects::Read>(candidateEffect.getEffect())) ||
292 (isa<MemoryEffects::Read>(prevEffect.getEffect()) &&
293 isa<MemoryEffects::Write>(candidateEffect.getEffect())) ||
294 (isa<MemoryEffects::Write>(prevEffect.getEffect()) &&
295 isa<MemoryEffects::Write>(candidateEffect.getEffect()))) {
296
297 // if the effects alias each other, then this is not a candidate for
298 // merging
299 if (oputils::mayAlias(candidateEffect, prevEffect)) {
300 foundConflict = true;
301 break;
302 }
303 }
304 }
305 }
306
307 if (!foundConflict)
308 legalMerge.push_back(candidateOp);
309
310 // mark start of next range
311 lastOp = candidateOp;
312 }
313
314 return legalMerge;
315}
316
317} // namespace batchutils
318} // namespace enzyme
319} // namespace mlir
320
321#endif // ENZYME_BATCH_DIFF_PASS_H
BatchDiffCacheKey createDiffCacheKey(SourceOp uop, FunctionOpInterface fn)
SmallVector< MemoryEffects::EffectInstance > findCallerEffects(SourceOp callerOp, FunctionOpInterface innerFnOp, const SmallVector< MemoryEffects::EffectInstance > &innerEffects)
llvm::SmallVector< SourceOp, 2 > pruneGradDefs(BatchDiffCacheKey &key, SmallVector< SourceOp > &allDiffs)
llvm::SmallVector< SourceOp > pruneMemoryEffects(SymbolTableCollection &symbolTable, BatchDiffCacheKey &key, SmallVector< SourceOp > &prunedSources, DenseMap< SourceOp, SmallVector< MemoryEffects::EffectInstance > > &callerEffectMap, llvm::DenseMap< FunctionOpInterface, SmallVector< MemoryEffects::EffectInstance > > &innerEffectCache)
bool mayAlias(Value v1, Value v2)
Definition Utils.cpp:145
bool collectOpEffects(Operation *rootOp, SmallVector< MemoryEffects::EffectInstance > &effects)
Returns the side effects of an operation(similar to mlir::getEffectsRecursively).
Definition Utils.cpp:306
SmallVector< MemoryEffects::EffectInstance > collectFnEffects(FunctionOpInterface fnOp)
Definition Utils.cpp:418
MemoryEffects::EffectInstance getEffectOfVal(Value val, MemoryEffects::Effect *effect, SideEffects::Resource *resource)
Definition Utils.cpp:431
SmallVector< enzyme::Activity > retActivity
bool operator<(const BatchDiffCacheKey &other) const
SmallVector< enzyme::Activity > inActivity