Enzyme main
Loading...
Searching...
No Matches
EnzymeBatchDiffPass.cpp
Go to the documentation of this file.
1//===- EnzymeBatchDiffPass.cpp - Merge autodiff calls into their batched
2// versions
3//------------ //
4//
5// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6// See https://llvm.org/LICENSE.txt for license information.
7// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8//
9//===----------------------------------------------------------------------===//
10
12#include "Dialect/Ops.h"
14#include "Interfaces/Utils.h"
15#include "PassDetails.h"
16#include "Passes/Passes.h"
17#include "Passes/Utils.h"
18
19#include "mlir/Analysis/AliasAnalysis.h"
20#include "mlir/Dialect/MemRef/IR/MemRef.h"
21#include "mlir/IR/Builders.h"
22#include "mlir/Interfaces/FunctionInterfaces.h"
23#include "mlir/Interfaces/SideEffectInterfaces.h"
24#include "llvm/ADT/DenseMap.h"
25
26#define DEBUG_TYPE "enzyme-diff-batch"
27#define ENZYME_DBGS llvm::dbgs() << "[" << DEBUG_TYPE << "]"
28
29using namespace mlir;
30using namespace mlir::enzyme;
31using namespace enzyme;
32
33namespace mlir {
34namespace enzyme {
35#define GEN_PASS_DEF_BATCHDIFFPASS
36#include "Passes/Passes.h.inc"
37} // namespace enzyme
38} // namespace mlir
39
40namespace {
41
42struct BatchDiffPass : public enzyme::impl::BatchDiffPassBase<BatchDiffPass> {
43 void runOnOperation() override;
44
45 void mergeFwddiffCalls(SymbolTableCollection &symbolTable,
46 FunctionOpInterface op) {
47 // TODO: Use a modified version of inter-procedural DataFlowAliasAnalysis
48 // for mapping primal effects
49 llvm::DenseMap<FunctionOpInterface,
50 SmallVector<MemoryEffects::EffectInstance>>
51 innerEffectCache;
52
53 OpBuilder builder(op);
54
55 op->walk([&](Block *blk) {
57 SmallVector<enzyme::ForwardDiffOp>>
58 diffMergeSet;
59
60 for (auto fwdOp : blk->getOps<enzyme::ForwardDiffOp>()) {
61 auto fnOp = dyn_cast_or_null<FunctionOpInterface>(
62 symbolTable.lookupNearestSymbolFrom(fwdOp, fwdOp.getFnAttr()));
63 if (!fnOp)
64 continue;
65
68
69 diffMergeSet[key].push_back(fwdOp);
70 }
71
72 for (auto &pair : diffMergeSet) {
73 auto key = pair.first;
74 auto allDiffs = pair.second;
75 if (allDiffs.size() < 2)
76 continue;
77
78 // Collect inner effects of function
79 if (!innerEffectCache.contains(key.function)) {
80 innerEffectCache[key.function] =
81 oputils::collectFnEffects(key.function);
82 }
83
84 SmallVector<MemoryEffects::EffectInstance> &calleeEffects =
85 innerEffectCache[key.function];
86
87 // TODO: skip if known readnone from existing analyses
88 bool skipMergeEntry = false;
89
90 // Map callee(primal function) memory effects to the calling
91 // function's(autodiff op's) memory effects. This allows us to also
92 // reason about memory effects on the derivative argument potentially
93 // being passed in(if the primal argument has activity enzyme_dup)
94
95 // ForwardDiff read(primal) ?-> read(deriv);
96 // write(primal) ?-> write(deriv)
97 //
98 // ReverseDiff read(primal) ?-> read or write(deriv);
99 // write(primal) ?-> read or write(deriv);
100 //
101 // Unknown effects are retained in the caller, and will always alias to
102 // true with any other effect
103 llvm::DenseMap<ForwardDiffOp,
104 SmallVector<MemoryEffects::EffectInstance>>
105 callerEffectMap;
106
107 for (auto &eff : calleeEffects) {
108 if (!isa<MemoryEffects::Read>(eff.getEffect()) &&
109 !isa<MemoryEffects::Write>(eff.getEffect())) {
110 // encountered allocate/load, skip merging
111 skipMergeEntry = true;
112 break;
113 }
114
115 Value effVal = eff.getValue();
116 if (!effVal) {
117 // unknown effect which isnt a value, skip merging
118 skipMergeEntry = true;
119 break;
120 }
121
122 // Find primal argument corresponding to effect value
123 size_t primalArgPos = 0;
124 bool foundPrimal = false;
125 if (auto effBA = dyn_cast<BlockArgument>(effVal)) {
126 if (llvm::is_contained(key.function.getArguments(), effBA)) {
127 foundPrimal = true;
128 primalArgPos = effBA.getArgNumber();
129 }
130 }
131
132 if (!foundPrimal) {
133 // TODO: Handle this either as a global value, or a value which
134 // is inside of the MLIR function(for inter-proc alias analysis) -
135 // skip for now
136 skipMergeEntry = true;
137 break;
138 }
139
140 // Add primal effects to caller effect map for all ops
141 Value primalVal = key.inputs[primalArgPos];
142 for (auto dop : allDiffs) {
143 callerEffectMap[dop].push_back(oputils::getEffectOfVal(
144 primalVal, eff.getEffect(), eff.getResource()));
145 }
146
147 // Add derivative effects(only if primal arg is dup)
148 // read(primal) -> read(derivative)
149 // write(primal) -> write(derivative)
150
151 // find position of dup arg for primal
152 bool primalIsDup =
153 (key.inActivity[primalArgPos] == Activity::enzyme_dup) ||
154 (key.inActivity[primalArgPos] == Activity::enzyme_dupnoneed);
155
156 if (primalIsDup) {
157 size_t gradArgPos = 0;
158 for (auto [idx, act] : llvm::enumerate(key.inActivity)) {
159 ++gradArgPos;
160
161 if (idx == primalArgPos)
162 break;
163
164 if (act == Activity::enzyme_dup ||
165 act == Activity::enzyme_dupnoneed) {
166 ++gradArgPos;
167 }
168 }
169
170 for (auto dop : allDiffs) {
171 Value dVal = dop.getInputs()[gradArgPos];
172 callerEffectMap[dop].push_back(oputils::getEffectOfVal(
173 dVal, eff.getEffect(), eff.getResource()));
174 }
175 }
176 }
177
178 if (skipMergeEntry)
179 continue;
180
181 SmallVector<ForwardDiffOp> prunedSources =
182 batchutils::pruneGradDefs(key, allDiffs);
183
184 SmallVector<ForwardDiffOp> legalMerge = batchutils::pruneMemoryEffects(
185 symbolTable, key, prunedSources, callerEffectMap, innerEffectCache);
186
187 {
188 SmallVector<enzyme::ForwardDiffOp> &allOps = legalMerge;
189 int64_t width = allOps.size();
190 if (width < 2)
191 continue;
192
193 auto firstDiffOp = allOps.front();
194 IRRewriter::InsertionGuard insertGuard(builder);
195 builder.setInsertionPoint(firstDiffOp);
196 auto loc = firstDiffOp->getLoc();
197 auto context = builder.getContext();
198
199 SmallVector<mlir::Value> in_args;
200 SmallVector<ActivityAttr, 2> inActivityAttrs;
201 SmallVector<ActivityAttr, 2> retActivityAttrs;
202 SmallVector<mlir::Type, 2> out_ty;
203 auto in_idx = 0;
204 for (auto [idx, act] : llvm::enumerate(key.inActivity)) {
205 ActivityAttr iattr = ActivityAttr::get(context, act);
206 inActivityAttrs.push_back(iattr);
207 in_args.push_back(key.inputs[idx]);
208 in_idx++;
209 // batched input derivative
210 SmallVector<mlir::Value> derivList;
211 if (act == Activity::enzyme_dup ||
212 act == Activity::enzyme_dupnoneed) {
213 for (auto uop : allOps) {
214 derivList.push_back(uop.getInputs()[in_idx]);
215 }
216
217 mlir::Value batchedDeriv =
218 getConcatValue(builder, loc, derivList);
219 in_args.push_back(batchedDeriv);
220 in_idx++;
221 }
222 }
223
224 auto out_idx = 0;
225 for (auto [idx, ract] : llvm::enumerate(key.retActivity)) {
226 ActivityAttr iattr = ActivityAttr::get(context, ract);
227 retActivityAttrs.push_back(iattr);
228 switch (ract) {
229 case Activity::enzyme_active: {
230 mlir::Value res = firstDiffOp.getOutputs()[out_idx];
231 out_ty.push_back(res.getType());
232 ++out_idx;
233 break;
234 }
235 case Activity::enzyme_const: {
236 mlir::Value res = firstDiffOp.getOutputs()[out_idx];
237 out_ty.push_back(res.getType());
238 ++out_idx;
239 break;
240 }
241 case Activity::enzyme_dupnoneed: {
242 // batched output derivative
243 mlir::Value dres = firstDiffOp.getOutputs()[out_idx];
244 out_ty.push_back(getConcatType(dres, width));
245 ++out_idx;
246 break;
247 }
248 case Activity::enzyme_dup: {
249 mlir::Value res = firstDiffOp.getOutputs()[out_idx];
250 out_ty.push_back(res.getType());
251 ++out_idx;
252 // batched output derivative
253 mlir::Value dres = firstDiffOp.getOutputs()[out_idx];
254 out_ty.push_back(getConcatType(dres, width));
255 ++out_idx;
256 break;
257 }
258 case Activity::enzyme_constnoneed: {
259 break;
260 }
261 case Activity::enzyme_activenoneed: {
262 mlir::Value res = firstDiffOp.getOutputs()[out_idx];
263 out_ty.push_back(res.getType());
264 ++out_idx;
265 break;
266 }
267 default:
268 llvm_unreachable(
269 "unknown activity value encountered for ret_activity");
270 }
271 }
272
273 ArrayAttr newInActivity = ArrayAttr::get(
274 context, llvm::ArrayRef<Attribute>(inActivityAttrs.begin(),
275 inActivityAttrs.end()));
276 ArrayAttr newRetActivity = ArrayAttr::get(
277 context, llvm::ArrayRef<Attribute>(retActivityAttrs.begin(),
278 retActivityAttrs.end()));
279 IntegerAttr newWidthAttr =
280 IntegerAttr::get(firstDiffOp.getWidthAttr().getType(), width);
281 auto newDiffOp = ForwardDiffOp::create(
282 builder, loc, out_ty, firstDiffOp.getFnAttr(), in_args,
283 newInActivity, newRetActivity, newWidthAttr,
284 firstDiffOp.getStrongZeroAttr());
285
286 // Rename uses
287 // out -> primal
288 // dout -> derivative
289 out_idx = 0;
290 for (auto [idx, ract] : llvm::enumerate(key.retActivity)) {
291 switch (ract) {
292 case Activity::enzyme_constnoneed:
293 break;
294
295 case Activity::enzyme_active:
296 case Activity::enzyme_activenoneed:
297 case Activity::enzyme_const: {
298 auto new_out = newDiffOp.getOutputs()[out_idx];
299 for (auto dop : allOps) {
300 dop.getOutputs()[out_idx].replaceAllUsesWith(new_out);
301 }
302 out_idx++;
303 break;
304 }
305
306 case Activity::enzyme_dupnoneed: {
307 // batched derivative
308 auto batch_dout = newDiffOp.getOutputs()[out_idx];
309 for (auto [dop_idx, dop] : llvm::enumerate(allOps)) {
310 auto old_dout = dop.getOutputs()[out_idx];
311 auto doutTy = old_dout.getType();
312 auto new_dout =
313 getExtractValue(builder, loc, doutTy, batch_dout, dop_idx);
314
315 old_dout.replaceAllUsesWith(new_dout);
316 }
317 ++out_idx;
318 break;
319 }
320
321 case Activity::enzyme_dup: {
322 mlir::Value new_out = newDiffOp.getOutputs()[out_idx];
323 for (ForwardDiffOp dop : allOps) {
324 dop.getOutputs()[out_idx].replaceAllUsesWith(new_out);
325 }
326 out_idx++;
327
328 // batched derivative
329 auto batch_dout = newDiffOp.getOutputs()[out_idx];
330 for (auto [dop_idx, dop] : llvm::enumerate(allOps)) {
331 auto old_dout = dop.getOutputs()[out_idx];
332 auto doutTy = old_dout.getType();
333 auto new_dout =
334 getExtractValue(builder, loc, doutTy, batch_dout, dop_idx);
335
336 old_dout.replaceAllUsesWith(new_dout);
337 }
338 ++out_idx;
339 break;
340 }
341 default:
342 llvm_unreachable(
343 "unknown activity value encountered for ret_activity");
344 }
345 }
346
347 // erase all old ops
348 for (auto dop : allOps) {
349 dop->erase();
350 }
351 }
352 }
353 }); // block walker
354 }
355
356 void mergeRevdiffCalls(SymbolTableCollection &symbolTable,
357 FunctionOpInterface op) {
358
359 // TODO: Use a modified version of inter-procedural DataFlowAliasAnalysis
360 // for mapping primal effects
361
362 // list of values read/written to inside fn
363 llvm::DenseMap<FunctionOpInterface,
364 SmallVector<MemoryEffects::EffectInstance>>
365 innerEffectCache;
366
367 OpBuilder builder(op);
368
369 op->walk([&](Block *blk) {
370 // map tracking batchable AD calls
372 SmallVector<enzyme::AutoDiffOp>>
373 diffMergeSet;
374
375 for (auto revOp : blk->getOps<enzyme::AutoDiffOp>()) {
376 auto fnOp = dyn_cast_or_null<FunctionOpInterface>(
377 symbolTable.lookupNearestSymbolFrom(revOp, revOp.getFnAttr()));
378 if (!fnOp)
379 continue;
380
383
384 diffMergeSet[key].push_back(revOp);
385 }
386
387 for (auto &pair : diffMergeSet) {
388 auto key = pair.first;
389 auto allDiffs = pair.second;
390 if (allDiffs.size() < 2)
391 continue;
392
393 // Collect inner effects of function
394 if (!innerEffectCache.contains(key.function)) {
395 innerEffectCache[key.function] =
397 }
398
399 SmallVector<MemoryEffects::EffectInstance> &calleeEffects =
400 innerEffectCache[key.function];
401
402 // TODO: skip if known readonly from existing analyses
403 bool skipMergeEntry = false;
404
405 llvm::DenseMap<AutoDiffOp, SmallVector<MemoryEffects::EffectInstance>>
406 callerEffectMap;
407
408 for (auto &eff : calleeEffects) {
409 if (!isa<MemoryEffects::Read>(eff.getEffect()) &&
410 !isa<MemoryEffects::Write>(eff.getEffect())) {
411 // encountered allocate/load, skip merging
412 skipMergeEntry = true;
413 break;
414 }
415
416 Value effVal = eff.getValue();
417 if (!effVal) {
418 // unknown effect which isnt a value, skip merging
419 skipMergeEntry = true;
420 break;
421 }
422
423 // Find primal argument corresponding to effect value
424 size_t primalArgPos = 0;
425 bool foundPrimal = false;
426 if (auto effBA = dyn_cast<BlockArgument>(effVal)) {
427 if (llvm::is_contained(key.function.getArguments(), effBA)) {
428 foundPrimal = true;
429 primalArgPos = effBA.getArgNumber();
430 }
431 }
432
433 if (!foundPrimal) {
434 // TODO: Handle this either as a global value, or a value which
435 // is inside of the MLIR function(for inter-proc alias analysis) -
436 // skip for now
437 skipMergeEntry = true;
438 break;
439 }
440
441 // Add primal effects to caller effect map for all ops
442 Value primalVal = key.inputs[primalArgPos];
443 for (auto dop : allDiffs) {
444 callerEffectMap[dop].push_back(oputils::getEffectOfVal(
445 primalVal, eff.getEffect(), eff.getResource()));
446 }
447
448 // Add derivative effects(only if primal arg is dup)
449 // read(primal) -> read(derivative) + write(derivative)
450 // write(primal) -> write(derivative) + read(derivative)
451
452 // find position of dup arg for primal
453 bool primalIsDup =
454 (key.inActivity[primalArgPos] == Activity::enzyme_dup) ||
455 (key.inActivity[primalArgPos] == Activity::enzyme_dupnoneed);
456
457 if (primalIsDup) {
458 size_t gradArgPos = 0;
459 for (auto [idx, act] : llvm::enumerate(key.inActivity)) {
460 ++gradArgPos;
461
462 if (idx == primalArgPos)
463 break;
464
465 if (act == Activity::enzyme_dup ||
466 act == Activity::enzyme_dupnoneed) {
467 ++gradArgPos;
468 }
469 }
470
471 for (auto dop : allDiffs) {
472 Value dVal = dop.getInputs()[gradArgPos];
473 callerEffectMap[dop].emplace_back(oputils::getEffectOfVal(
474 dVal, MemoryEffects::Write::get(), eff.getResource()));
475 callerEffectMap[dop].emplace_back(oputils::getEffectOfVal(
476 dVal, MemoryEffects::Read::get(), eff.getResource()));
477 }
478 }
479 }
480
481 if (skipMergeEntry)
482 continue;
483
484 SmallVector<AutoDiffOp> prunedSources =
485 batchutils::pruneGradDefs(key, allDiffs);
486
487 SmallVector<AutoDiffOp> legalMerge = batchutils::pruneMemoryEffects(
488 symbolTable, key, prunedSources, callerEffectMap, innerEffectCache);
489
490 // go ahead and actually do the merge now
491 {
492
493 SmallVector<enzyme::AutoDiffOp> &allOps = legalMerge;
494 int64_t width = allOps.size();
495
496 if (width < 2)
497 continue;
498
499 auto firstDiffOp = allOps.front();
500 IRRewriter::InsertionGuard insertGuard(builder);
501 builder.setInsertionPoint(firstDiffOp);
502 auto loc = firstDiffOp->getLoc();
503 auto context = builder.getContext();
504
505 // Prepare args for merged operation
506
507 SmallVector<mlir::Value> in_args;
508 SmallVector<ActivityAttr, 2> inActivityAttrs;
509 SmallVector<ActivityAttr, 2> retActivityAttrs;
510 SmallVector<mlir::Type, 2> out_ty;
511
512 // fill in_args using inputs
513 size_t call_idx = 0;
514 for (auto [idx, act] : llvm::enumerate(key.inActivity)) {
515 auto iattr = ActivityAttr::get(context, act);
516 inActivityAttrs.push_back(iattr);
517 in_args.push_back(key.inputs[idx]);
518 call_idx++;
519
520 if (act == Activity::enzyme_dup ||
521 act == Activity::enzyme_dupnoneed) {
522
523 SmallVector<mlir::Value> derivList;
524 for (auto uop : allOps) {
525 derivList.push_back(uop.getInputs()[call_idx]);
526 }
527
528 mlir::Value b_din = getConcatValue(builder, loc, derivList);
529 in_args.push_back(b_din);
530 call_idx++;
531 }
532 }
533
534 // Skip if function is non-differentiable
535 if (call_idx == firstDiffOp.getInputs().size()) {
536 continue;
537 }
538
539 // fill in_args using d<out>, fill out_ty using out
540 size_t out_idx = 0;
541 for (auto ract : key.retActivity) {
542 auto iattr = ActivityAttr::get(context, ract);
543 retActivityAttrs.push_back(iattr);
544
545 // no effect on out or d<out>
546 if (ract == Activity::enzyme_constnoneed ||
547 ract == Activity::enzyme_dupnoneed) {
548 continue;
549 }
550
551 // handle d<out>
552 if (ract == Activity::enzyme_active ||
553 ract == Activity::enzyme_activenoneed) {
554 SmallVector<mlir::Value> derivList;
555 for (auto uop : allOps) {
556 derivList.push_back(uop.getInputs()[call_idx]);
557 }
558
559 Value batch_dout = getConcatValue(builder, loc, derivList);
560 in_args.push_back(batch_dout);
561 call_idx++;
562 }
563
564 // handle out
565 if (ract == Activity::enzyme_active ||
566 ract == Activity::enzyme_const ||
567 ract == Activity::enzyme_dup) {
568 Value out = firstDiffOp.getOutputs()[out_idx];
569 out_ty.push_back(out.getType());
570 ++out_idx;
571 }
572 }
573
574 // fill out_ty using d<in>
575 for (auto act : key.inActivity) {
576 if (act == Activity::enzyme_active) {
577 Value din = firstDiffOp.getOutputs()[out_idx];
578 out_ty.push_back(getConcatType(din, width));
579 ++out_idx;
580 }
581 }
582
583 ArrayAttr newInActivity = ArrayAttr::get(
584 context, llvm::ArrayRef<Attribute>(inActivityAttrs.begin(),
585 inActivityAttrs.end()));
586
587 ArrayAttr newRetActivity = ArrayAttr::get(
588 context, llvm::ArrayRef<Attribute>(retActivityAttrs.begin(),
589 retActivityAttrs.end()));
590
591 IntegerAttr newWidthAttr =
592 IntegerAttr::get(firstDiffOp.getWidthAttr().getType(), width);
593
594 auto newDiffOp = AutoDiffOp::create(
595 builder, loc, out_ty, firstDiffOp.getFnAttr(), in_args,
596 newInActivity, newRetActivity, newWidthAttr,
597 firstDiffOp.getStrongZeroAttr(), firstDiffOp.getAtomicAddAttr());
598
599 // Map old uses to new uses
600 out_idx = 0;
601 for (auto ract : key.retActivity) {
602 if (ract == Activity::enzyme_active ||
603 ract == Activity::enzyme_const ||
604 ract == Activity::enzyme_dup) {
605 Value new_out = newDiffOp.getOutputs()[out_idx];
606 for (auto dop : allOps) {
607 dop.getOutputs()[out_idx].replaceAllUsesWith(new_out);
608 }
609 ++out_idx;
610 }
611 }
612
613 for (auto act : key.inActivity) {
614 if (act == Activity::enzyme_active) {
615 Value batch_din = newDiffOp.getOutputs()[out_idx];
616 for (auto [dop_idx, dop] : llvm::enumerate(allOps)) {
617 Value old_din = dop.getOutputs()[out_idx];
618 auto dinTy = old_din.getType();
619 auto new_din =
620 getExtractValue(builder, loc, dinTy, batch_din, dop_idx);
621
622 old_din.replaceAllUsesWith(new_din);
623 }
624 }
625 }
626 // erase all old ops
627 for (auto dop : allOps) {
628 dop->erase();
629 }
630 }
631 }
632 }); // block walker
633 }
634};
635
636} // end anonymous namespace
637
638void BatchDiffPass::runOnOperation() {
639 SymbolTableCollection symbolTable;
640 symbolTable.getSymbolTable(getOperation());
641 getOperation()->walk([&](FunctionOpInterface op) {
642 mergeFwddiffCalls(symbolTable, op);
643 mergeRevdiffCalls(symbolTable, op);
644 });
645}
BatchDiffCacheKey createDiffCacheKey(SourceOp uop, FunctionOpInterface fn)
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)
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
Type getConcatType(Value val, int64_t width)
Definition Utils.cpp:133
Value getConcatValue(OpBuilder &builder, Location loc, ArrayRef< Value > argList)
Definition Utils.cpp:155
Value getExtractValue(OpBuilder &builder, Location loc, Type argTy, Value val, int64_t index)
Definition Utils.cpp:163
SmallVector< enzyme::Activity > retActivity
SmallVector< enzyme::Activity > inActivity