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