Enzyme main
Loading...
Searching...
No Matches
Ops.cpp
Go to the documentation of this file.
1//===- EnzymeOps.cpp - Enzyme dialect ops -----------------------*- C++ -*-===//
2// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
3// See https://llvm.org/LICENSE.txt for license information.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
8#include "Ops.h"
10#include "mlir/Dialect/LLVMIR/LLVMTypes.h"
11#include "mlir/IR/AffineExpr.h"
12#include "mlir/IR/Builders.h"
13#include "mlir/IR/Matchers.h"
14#include "mlir/IR/PatternMatch.h"
15#include "mlir/IR/Value.h"
16#include "mlir/Interfaces/MemorySlotInterfaces.h"
17
18#include "mlir/Dialect/Affine/IR/AffineOps.h"
19#include "mlir/Dialect/Func/IR/FuncOps.h"
20#include "mlir/Dialect/MemRef/IR/MemRef.h"
21#include "mlir/IR/IntegerSet.h"
22
23#include "llvm/ADT/STLExtras.h"
24#include "llvm/ADT/SmallVector.h"
25
26#include "llvm/Support/ErrorHandling.h"
27#include "llvm/Support/LogicalResult.h"
28#include <type_traits>
29
30#define DEBUG_TYPE "enzyme"
31
32using namespace mlir;
33using namespace enzyme;
34using namespace mlir::arith;
35
36//===----------------------------------------------------------------------===//
37// InitOp
38//===----------------------------------------------------------------------===//
39
40llvm::SmallVector<MemorySlot> InitOp::getPromotableSlots() {
41 auto Ty = this->getType();
42 if (isa<CacheType>(Ty))
43 return {};
44
45 if (!getOperation()->getBlock()->isEntryBlock())
46 return {};
47
48 auto gTy = cast<GradientType>(Ty);
49 MemorySlot slot = {this->getResult(), gTy.getBasetype()};
50
51 return {slot};
52}
53
54Value InitOp::getDefaultValue(const MemorySlot &slot, OpBuilder &builder) {
55 auto gTy = cast<GradientType>(this->getType());
56 return cast<AutoDiffTypeInterface>(gTy.getBasetype())
57 .createNullValue(builder, this->getLoc());
58}
59
60void InitOp::handleBlockArgument(const MemorySlot &slot, BlockArgument argument,
61 OpBuilder &builder) {}
62
63std::optional<mlir::PromotableAllocationOpInterface>
64InitOp::handlePromotionComplete(const MemorySlot &slot, Value defaultValue,
65 OpBuilder &builder) {
66 if (defaultValue && defaultValue.use_empty())
67 defaultValue.getDefiningOp()->erase();
68 this->erase();
69 return std::nullopt;
70}
71
72//===----------------------------------------------------------------------===//
73// BinomialProgressOp
74//===----------------------------------------------------------------------===//
75
76// Revolve "split" function: given `n` remaining steps and `s` available
77// checkpoints, return how many steps to advance before placing the next one.
78//
79// Let beta(s, t) = C(s + t, t) be the longest chain reversible with `s`
80// checkpoints and at most `t` recomputations of each step, and let `t` be
81// minimal with beta(s, t) >= n. Every advance in
82//
83// [ n - beta(s-1, t) , beta(s, t-1) ]
84//
85// attains that optimal `t`, and the interval is non-empty because
86// beta(s, t) = beta(s-1, t) + beta(s, t-1) (Pascal). We take its midpoint:
87// either edge can collapse onto the clamp (a 1-step advance that burns a
88// checkpoint on nothing), while the midpoint stays within ~2% of the minimal
89// *total* recomputation across the sizes measured.
90//
91// With a single checkpoint left there is nothing better than replaying the
92// whole remaining stretch from it, so advance all of it. Together with every
93// other advance landing in [1, n-1], that is what makes the per-slot advances
94// sum to exactly `n` across `budget` slots -- which is what the callers' outer
95// loop relies on to reach the end of the primal.
96static int64_t binomialProgress(int64_t n, int64_t s) {
97 if (n <= 0)
98 return 0;
99 if (n == 1)
100 return 1;
101 if (s <= 1)
102 return n;
103
104 int64_t t = 0, beta = 1; // beta == C(s + t, t)
105 while (beta < n) {
106 ++t;
107 beta = beta * (s + t) / t; // C(s+t,t) from C(s+t-1,t-1); exact
108 }
109
110 // beta(s-1, t) == beta * s / (s + t) and beta(s, t-1) == beta * t / (s + t),
111 // both exact in integers.
112 int64_t lo = n - beta * s / (s + t);
113 int64_t hi = beta * t / (s + t);
114 if (lo < 1)
115 lo = 1;
116 if (hi > n - 1)
117 hi = n - 1;
118 int64_t m = (lo + hi) / 2; // lo <= hi, so this is already in [1, n-1]
119
120 // Leave at least one step for each of the s-1 checkpoints still to be placed.
121 // Without this the advances can exhaust the interval before the slots run
122 // out, and a caller that walks one slot per iteration then records slots at a
123 // step past the end -- holding the final state rather than a checkpoint.
124 int64_t cap = n - (s - 1);
125 if (m > cap)
126 m = cap;
127 return m < 1 ? 1 : m;
128}
129
130OpFoldResult BinomialProgressOp::fold(FoldAdaptor adaptor) {
131 auto numSteps = dyn_cast_or_null<IntegerAttr>(adaptor.getNumSteps());
132 auto budget = dyn_cast_or_null<IntegerAttr>(adaptor.getBudget());
133 if (!numSteps || !budget)
134 return {};
135
136 int64_t n = numSteps.getInt(), s = budget.getInt();
137 if (n <= 0 || s <= 0)
138 return {};
139
140 return IntegerAttr::get(getType(), binomialProgress(n, s));
141}
142
143//===----------------------------------------------------------------------===//
144// GetOp
145//===----------------------------------------------------------------------===//
146
147bool GetOp::loadsFrom(const MemorySlot &slot) {
148 return this->getGradient() == slot.ptr;
149}
150
151bool GetOp::storesTo(const MemorySlot &slot) { return false; }
152
153Value GetOp::getStored(const MemorySlot &slot, OpBuilder &builder,
154 Value reachingDef, const DataLayout &dataLayout) {
155 return {};
156}
157
158bool GetOp::canUsesBeRemoved(
159 const MemorySlot &slot,
160 const llvm::SmallPtrSetImpl<OpOperand *> &blockingUses,
161 llvm::SmallVectorImpl<OpOperand *> &newBlockingUses,
162 const mlir::DataLayout &dataLayout) {
163 if (blockingUses.size() != 1)
164 return false;
165
166 Value blockingUse = (*blockingUses.begin())->get();
167 return blockingUse == slot.ptr && getGradient() == slot.ptr;
168}
169
170DeletionKind GetOp::removeBlockingUses(
171 const MemorySlot &slot,
172 const llvm::SmallPtrSetImpl<OpOperand *> &blockingUses, OpBuilder &builder,
173 Value reachingDefinition, const DataLayout &dataLayout) {
174 this->getResult().replaceAllUsesWith(reachingDefinition);
175 return DeletionKind::Delete;
176}
177
178llvm::LogicalResult GetOp::ensureOnlySafeAccesses(
179 const MemorySlot &slot, llvm::SmallVectorImpl<MemorySlot> &mustBeSafelyUsed,
180 const DataLayout &dataLayout) {
181 return success(slot.ptr == getGradient());
182}
183
184//===----------------------------------------------------------------------===//
185// SetOp
186//===----------------------------------------------------------------------===//
187
188bool SetOp::loadsFrom(const MemorySlot &slot) { return false; }
189
190bool SetOp::storesTo(const MemorySlot &slot) {
191 return this->getGradient() == slot.ptr;
192}
193
194Value SetOp::getStored(const MemorySlot &slot, OpBuilder &builder,
195 Value reachingDef, const DataLayout &dataLayout) {
196 return this->getValue();
197}
198
199bool SetOp::canUsesBeRemoved(
200 const MemorySlot &slot,
201 const llvm::SmallPtrSetImpl<OpOperand *> &blockingUses,
202 llvm::SmallVectorImpl<OpOperand *> &newBlockingUses,
203 const mlir::DataLayout &dataLayout) {
204 return true;
205}
206
207DeletionKind SetOp::removeBlockingUses(
208 const MemorySlot &slot,
209 const llvm::SmallPtrSetImpl<OpOperand *> &blockingUses, OpBuilder &builder,
210 Value reachingDefinition, const DataLayout &dataLayout) {
211 return DeletionKind::Delete;
212}
213
214llvm::LogicalResult SetOp::ensureOnlySafeAccesses(
215 const MemorySlot &slot, llvm::SmallVectorImpl<MemorySlot> &mustBeSafelyUsed,
216 const DataLayout &dataLayout) {
217 return success(slot.ptr == getGradient());
218}
219
220//===----------------------------------------------------------------------===//
221// GetFuncOp
222//===----------------------------------------------------------------------===//
223
224LogicalResult
225ForwardDiffOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
226 // TODO: Verify that the result type is same as the type of the referenced
227 // func.func op.
228 // Any function will do, as for enzyme.autodiff below -- what is being
229 // differentiated is often an llvm.func.
230 auto global = symbolTable.lookupNearestSymbolFrom<FunctionOpInterface>(
231 *this, getFnAttr());
232 if (!global)
233 return emitOpError("'")
234 << getFn() << "' does not reference a valid global funcOp";
235
236 return success();
237}
238
239LogicalResult JacobianOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
240 auto global =
241 symbolTable.lookupNearestSymbolFrom<func::FuncOp>(*this, getFnAttr());
242 if (!global)
243 return emitOpError("'")
244 << getFn() << "' does not reference a valid global funcOp";
245
246 return success();
247}
248
249//===----------------------------------------------------------------------===//
250// ForwardDiffOp
251//===----------------------------------------------------------------------===//
252
253// Some templated helpers for rewriting EnzymeOps(we can overload the create
254// definitions as and when necessary)
255template <typename SourceOp> struct EnzymeOpCreator;
256
257template <> struct EnzymeOpCreator<AutoDiffOp> {
258 static AutoDiffOp create(PatternRewriter &rewriter, AutoDiffOp uop,
259 TypeRange out_ty, ValueRange in_args,
260 ArrayAttr newInActivity, ArrayAttr newRetActivity) {
261
262 return AutoDiffOp::create(rewriter, uop.getLoc(), out_ty, uop.getFnAttr(),
263 in_args, newInActivity, newRetActivity,
264 uop.getWidthAttr(), uop.getStrongZeroAttr(),
265 uop.getAtomicAddAttr());
266 }
267};
268
269template <> struct EnzymeOpCreator<AutoDiffRegionOp> {
270 static AutoDiffRegionOp create(PatternRewriter &rewriter,
271 AutoDiffRegionOp uop, TypeRange out_ty,
272 ValueRange in_args, ArrayAttr newInActivity,
273 ArrayAttr newRetActivity) {
274 auto newOp = AutoDiffRegionOp::create(
275 rewriter, uop.getLoc(), out_ty, in_args, newInActivity, newRetActivity,
276 uop.getWidthAttr(), uop.getStrongZeroAttr(), uop.getAtomicAddAttr(),
277 uop.getFnAttr());
278
279 rewriter.inlineRegionBefore(uop.getBody(), newOp.getBody(),
280 newOp.getBody().begin());
281 return newOp;
282 }
283};
284
285template <> struct EnzymeOpCreator<ForwardDiffOp> {
286 static ForwardDiffOp create(PatternRewriter &rewriter, ForwardDiffOp uop,
287 TypeRange out_ty, ValueRange in_args,
288 ArrayAttr newInActivity,
289 ArrayAttr newRetActivity) {
290 return ForwardDiffOp::create(
291 rewriter, uop.getLoc(), out_ty, uop.getFnAttr(), in_args, newInActivity,
292 newRetActivity, uop.getWidthAttr(), uop.getStrongZeroAttr());
293 }
294};
295
296template <> struct EnzymeOpCreator<ForwardDiffRegionOp> {
297 static ForwardDiffRegionOp create(PatternRewriter &rewriter,
298 ForwardDiffRegionOp uop, TypeRange out_ty,
299 ValueRange in_args, ArrayAttr newInActivity,
300 ArrayAttr newRetActivity) {
301 auto newOp = ForwardDiffRegionOp::create(
302 rewriter, uop.getLoc(), out_ty, in_args, newInActivity, newRetActivity,
303 uop.getWidthAttr(), uop.getStrongZeroAttr(), uop.getFnAttr());
304 rewriter.inlineRegionBefore(uop.getBody(), newOp.getBody(),
305 newOp.getBody().begin());
306 return newOp;
307 }
308};
309
310// Helper: check if any input is mutable.
311static inline bool isMutable(Type type) {
312 if (isa<mlir::MemRefType>(type) || isa<mlir::UnrankedMemRefType>(type) ||
313 isa<mlir::LLVM::LLVMPointerType>(type)) {
314 return true;
315 }
316
317 return false;
318}
319
320/**
321 *
322 * Modifies input activites for the FwdDiffOp
323 * The activity promotion flow is as follows
324 * (depending on variable use):
325 *
326 * -----> enzyme_dupnoneed
327 * / /
328 * enzyme_dup /
329 * \ v
330 * ------> enzyme_const
331 *
332 */
333template <typename SourceOp>
334class FwdInpOpt final : public OpRewritePattern<SourceOp> {
335public:
337
338 LogicalResult matchAndRewrite(SourceOp uop,
339 PatternRewriter &rewriter) const override {
340
341 if (uop.getOutputs().size() == 0)
342 return failure();
343
344 auto inActivity = uop.getActivity();
345
346 auto in_idx = 0;
347 SmallVector<mlir::Value, 2> in_args;
348 SmallVector<ActivityAttr, 2> newInActivityArgs;
349 bool changed = false;
350 for (auto [idx, act] : llvm::enumerate(inActivity)) {
351 auto iattr = cast<ActivityAttr>(act);
352 auto val = iattr.getValue();
353
354 // Forward mode Input activities can only take values {dup, dupnoneed,
355 // const }
356
357 mlir::Value inp = uop.getInputs()[in_idx];
358
359 switch (val) {
360
361 case mlir::enzyme::Activity::enzyme_const:
362 in_args.push_back(inp);
363 newInActivityArgs.push_back(iattr);
364 break;
365
366 case Activity::enzyme_dupnoneed: {
367 // always pass in primal
368 in_args.push_back(inp);
369 in_idx++;
370
371 // selectively push or skip directional derivative
372 inp = uop.getInputs()[in_idx];
373 auto ET = inp.getType();
374 auto ETintf = dyn_cast<AutoDiffTypeInterface>(ET);
375
376 if (ETintf && !isMutable(ET) && ETintf.isZero(inp)) {
377 // skip and promote to const
378 auto new_const = mlir::enzyme::ActivityAttr::get(
379 rewriter.getContext(), mlir::enzyme::Activity::enzyme_const);
380 newInActivityArgs.push_back(new_const);
381 changed = true;
382 } else {
383 // push derivative value
384 in_args.push_back(inp);
385 newInActivityArgs.push_back(iattr);
386 }
387 break;
388 }
389
390 case Activity::enzyme_dup: {
391 // always pass in primal
392 in_args.push_back(inp);
393 in_idx++;
394
395 // selectively push or skip directional derivative
396 inp = uop.getInputs()[in_idx];
397 auto ET = inp.getType();
398 auto ETintf = dyn_cast<AutoDiffTypeInterface>(ET);
399
400 if (ETintf && !isMutable(ET) && ETintf.isZero(inp)) {
401 // skip and promote to const
402 auto new_const = mlir::enzyme::ActivityAttr::get(
403 rewriter.getContext(), mlir::enzyme::Activity::enzyme_const);
404 newInActivityArgs.push_back(new_const);
405 changed = true;
406 } else {
407 // push derivative value
408 in_args.push_back(inp);
409 newInActivityArgs.push_back(iattr);
410 }
411 break;
412 }
413 default:
414 llvm_unreachable("unexpected input activity arg");
415 }
416
417 in_idx++;
418 }
419
420 if (!changed)
421 return failure();
422
423 // create the new op
424 ArrayAttr newInActivity =
425 ArrayAttr::get(rewriter.getContext(),
426 llvm::ArrayRef<Attribute>(newInActivityArgs.begin(),
427 newInActivityArgs.end()));
428
429 if constexpr (std::is_same_v<SourceOp, ForwardDiffOp>) {
430
431 rewriter.replaceOpWithNewOp<ForwardDiffOp>(
432 uop, uop->getResultTypes(), uop.getFnAttr(), in_args, newInActivity,
433 uop.getRetActivityAttr(), uop.getWidthAttr(),
434 uop.getStrongZeroAttr());
435 } else {
436 rewriter.replaceOpWithNewOp<ForwardDiffRegionOp>(
437 uop, uop->getResultTypes(), in_args, newInActivity,
438 uop.getRetActivityAttr(), uop.getWidthAttr(), uop.getStrongZeroAttr(),
439 uop.getFnAttr());
440 }
441 return success();
442 }
443};
444
445/**
446 *
447 * Modifies return activites for the FwdDiffOp
448 * The activity promotion flow is as follows
449 * (depending on variable use):
450 *
451 * -----> enzyme_dupnoneed ----
452 * / \
453 * enzyme_dup ---> enzyme_constnoneed
454 * \ /
455 * ------> enzyme_const -----
456 *
457 */
458template <typename SourceOp>
459class FwdRetOpt final : public OpRewritePattern<SourceOp> {
460private:
462
463public:
465
466 LogicalResult matchAndRewrite(SourceOp uop,
467 PatternRewriter &rewriter) const override {
468
469 if (uop.getOutputs().size() == 0)
470 return failure();
471
472 auto retActivity = uop.getRetActivity();
473 auto out_idx = 0;
474 SmallVector<mlir::Value, 2> outs_args;
475 SmallVector<Type, 2> out_ty;
476 SmallVector<ActivityAttr, 2> newRetActivityArgs;
477 bool changed = false;
478
479 for (auto [idx, act] : llvm::enumerate(retActivity)) {
480 auto iattr = cast<ActivityAttr>(act);
481 auto val = iattr.getValue();
482
483 // const_noneed does not have a value associated with it
484 // so we can't index into outputs.
485 if (val == Activity::enzyme_constnoneed) {
486 newRetActivityArgs.push_back(iattr);
487 continue;
488 }
489
490 mlir::Value res = uop.getOutputs()[out_idx];
491
492 switch (val) {
493 case Activity::enzyme_active:
494 outs_args.push_back(res);
495 out_ty.push_back(res.getType());
496 newRetActivityArgs.push_back(iattr);
497 break;
498
499 case mlir::enzyme::Activity::enzyme_const:
500 if (!res.use_empty()) {
501 outs_args.push_back(res);
502 out_ty.push_back(res.getType());
503 newRetActivityArgs.push_back(iattr);
504 } else {
505 changed = true;
506 auto new_constnn = mlir::enzyme::ActivityAttr::get(
507 rewriter.getContext(),
508 mlir::enzyme::Activity::enzyme_constnoneed);
509 newRetActivityArgs.push_back(new_constnn);
510 }
511 break;
512 case Activity::enzyme_dupnoneed:
513
514 if (!res.use_empty()) {
515 outs_args.push_back(res);
516 out_ty.push_back(res.getType());
517 newRetActivityArgs.push_back(iattr);
518 } else {
519 if (!isMutable(res.getType())) {
520 changed = true;
521 auto new_constnn = mlir::enzyme::ActivityAttr::get(
522 rewriter.getContext(),
523 mlir::enzyme::Activity::enzyme_constnoneed);
524 newRetActivityArgs.push_back(new_constnn);
525 } else {
526 outs_args.push_back(res);
527 out_ty.push_back(res.getType());
528 newRetActivityArgs.push_back(iattr);
529 }
530 }
531 break;
532 case Activity::enzyme_constnoneed:
533 outs_args.push_back(res);
534 out_ty.push_back(res.getType());
535 newRetActivityArgs.push_back(iattr);
536 break;
537 case Activity::enzyme_activenoneed:
538 outs_args.push_back(res);
539 out_ty.push_back(res.getType());
540 newRetActivityArgs.push_back(iattr);
541 break;
542 case Activity::enzyme_dup: {
543 ActivityAttr new_dup = iattr;
544 if (!res.use_empty()) {
545 outs_args.push_back(res);
546 out_ty.push_back(res.getType());
547 } else {
548 changed = true;
549 // discard return, change attr
550 new_dup = ActivityAttr::get(rewriter.getContext(),
551 Activity::enzyme_dupnoneed);
552 }
553
554 out_idx++;
555
556 // derivative
557 res = uop.getOutputs()[out_idx];
558 if (!res.use_empty()) {
559 // activity arg doesn't update
560 out_ty.push_back(res.getType());
561 outs_args.push_back(res);
562 } else {
563 // no uses, can discard
564 if (!isMutable(res.getType())) {
565 changed = true;
566 // check if primal is used
567 if (new_dup.getValue() == Activity::enzyme_dupnoneed) {
568 new_dup = ActivityAttr::get(rewriter.getContext(),
569 Activity::enzyme_constnoneed);
570 } else {
571 new_dup = ActivityAttr::get(rewriter.getContext(),
572 Activity::enzyme_const);
573 }
574 } else {
575 out_ty.push_back(res.getType());
576 outs_args.push_back(res);
577 }
578 }
579 newRetActivityArgs.push_back(new_dup);
580 break;
581 }
582 default:
583 llvm_unreachable("unexpected activity arg");
584 }
585
586 out_idx++;
587 }
588
589 if (!changed)
590 return failure();
591
592 ArrayAttr newRetActivity =
593 ArrayAttr::get(rewriter.getContext(),
594 llvm::ArrayRef<Attribute>(newRetActivityArgs.begin(),
595 newRetActivityArgs.end()));
596
597 SmallVector<Value> in_args = uop.getInputs();
598 SourceOp newOp = SourceOpCreator::create(
599 rewriter, uop, out_ty, in_args, uop.getActivityAttr(), newRetActivity);
600
601 // Map old uses of uop to newOp
602 auto oldIdx = 0;
603 auto newIdx = 0;
604 for (auto [idx, old_act, new_act] :
605 llvm::enumerate(retActivity, newRetActivityArgs)) {
606
607 auto iattr = cast<ActivityAttr>(old_act);
608 auto old_val = iattr.getValue();
609 auto new_val = new_act.getValue();
610
611 if (old_val == new_val) {
612 // don't index into op if its a const_noneed
613 if (old_val == Activity::enzyme_constnoneed) {
614 continue;
615 }
616 // replace use
617 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
618 newOp.getOutputs()[newIdx++]);
619 if (old_val == Activity::enzyme_dup) {
620 // 2nd replacement for derivative
621 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
622 newOp.getOutputs()[newIdx++]);
623 }
624 } else {
625 // handle all substitutions
626 if (new_val == Activity::enzyme_dupnoneed &&
627 old_val == Activity::enzyme_dup) {
628 ++oldIdx; // skip primal
629 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
630 newOp.getOutputs()[newIdx++]);
631 } else if (new_val == mlir::enzyme::Activity::enzyme_constnoneed &&
632 old_val == mlir::enzyme::Activity::enzyme_const) {
633 ++oldIdx; // skip const
634 } else if (new_val == mlir::enzyme::Activity::enzyme_constnoneed &&
635 old_val == mlir::enzyme::Activity::enzyme_dupnoneed) {
636 ++oldIdx; // skip gradient too
637 } else if (new_val == Activity::enzyme_const &&
638 old_val == Activity::enzyme_dup) {
639
640 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
641 newOp.getOutputs()[newIdx++]);
642 ++oldIdx; // skip derivative
643 } else if (new_val == Activity::enzyme_constnoneed &&
644 old_val == Activity::enzyme_dup) {
645 ++oldIdx; // skip primal
646 ++oldIdx; // skip derivative
647 }
648 }
649 }
650
651 rewriter.eraseOp(uop);
652 return success();
653 }
654};
655
656void ForwardDiffOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
657 MLIRContext *context) {
658
660}
661
662LogicalResult AutoDiffOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
663 // TODO: Verify that the result type is same as the type of the referenced
664 // func.func op.
665 auto global = symbolTable.lookupNearestSymbolFrom<FunctionOpInterface>(
666 *this, getFnAttr());
667 if (!global)
668 return emitOpError("'")
669 << getFn() << "' does not reference a valid global funcOp";
670
671 return success();
672}
673
674LogicalResult BatchOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
675 // TODO: Verify that the result type is same as the type of the referenced
676 // func.func op.
677 auto global = symbolTable.lookupNearestSymbolFrom<FunctionOpInterface>(
678 *this, getFnAttr());
679 if (!global)
680 return emitOpError("'")
681 << getFn() << "' does not reference a valid global funcOp";
682
683 return success();
684}
685
686//===----------------------------------------------------------------------===//
687// BroadcastOp
688//===----------------------------------------------------------------------===//
689
690void BroadcastOp::build(OpBuilder &builder, OperationState &result, Value input,
691 ArrayRef<int64_t> shape) {
692 auto shapeAttr = builder.getDenseI64ArrayAttr(shape);
693 auto resultTy = input.getType();
694 for (auto s : llvm::reverse(shape)) {
695 resultTy = cast<AutoDiffTypeInterface>(resultTy).getShadowType(s);
696 }
697 build(builder, result, resultTy, input, shapeAttr);
698}
699
700/**
701 *
702 * Modifies activities for the AutoDiffOp.
703 *
704 * This is also a nice place to understand the semantics of the rev-mode
705 * autodiff op. At it's core, the reverse mode autodiff takes in a function
706 *
707 * f: def f (pInput):
708 * ...perform computation
709 * return pOutput
710 *
711 * One can assign a very simple function signature to f here:
712 * f: pInput -> pOutput
713 *
714 * When trying to differentiate this function (using autodiff op), Enzyme
715 * creates a new function which also takes in the co-tangents of the outputs
716 * (dOutput), and computes and returns both the output and the input co-tangent
717 * (dInput). This is how the generated autodiff op eventually looks like:
718 *
719 * def revdiff_f(pInput, dOutput):
720 * ...perform computation to compute pOutput
721 * ...perform computation to compute dInput
722 * return pOutput, dInput
723 *
724 * The new function signature now becomes
725 * revdiff_f : (pInput', dOutput) -> (pOutput, dInput)
726 *
727 * I mention pInput' here because it is not exactly the input arguments to the
728 * function we are differentiating, for example, if the input argument type is
729 * an `enzyme_dup`, we will provide both tht primal value along with the
730 * shadow(which we then accumulate into and return). So specifically,
731 *
732 * pInput' = pInput (if the activity is enzyme_active, enzyme_const)
733 * | pInput, dInput (if the activity is enzyme_dup,
734 * enzyme_dupnoneed)
735 *
736 * Now that we have fixed the codegen semantics, we can go ahead and optimize
737 * for both the input return activities based on usage. Possible activity
738 * promotion flow for the inputs can be as follows:
739 * 1. enzyme_active --> enzyme_const (dInput is never used, so we simply don't
740 * compute it)
741 * 2. enzyme_dup --> enzyme_const (pInput is mutable, readonly, nocapture,
742 * dInput is never used post AD)
743 *
744 * Similarly, one can define a similar activity promotion flow for the outputs:
745 * 1. enzyme_active --> enzyme_activenoneed (we do need to pass in dOutput into
746 * the function, but we can see that pOutput is never used, so let's just not
747 * return it. This has the advantage of triggering some additional DCE inside
748 * the generated derivative function)
749 * 2. enzyme_const --> enzyme_constnoneed (same as above, but we now simply skip
750 * over this output)
751 *
752 * One other thing to note here is that these optimizations preserve the input
753 * function signature, and only modify the number of outputs.
754 *
755 */
756
757template <typename SourceOp>
758class ReverseRetOpt final : public OpRewritePattern<SourceOp> {
759private:
761
762public:
764
765 LogicalResult matchAndRewrite(SourceOp uop,
766 PatternRewriter &rewriter) const override {
767 // early return if there are no outputs
768 if (uop.getOutputs().size() == 0)
769 return failure();
770
771 auto inpActivity = uop.getActivity();
772 auto retActivity = uop.getRetActivity();
773 auto out_idx = 0;
774 SmallVector<mlir::Value, 2> in_args;
775 SmallVector<mlir::Value, 2> outs_args;
776 SmallVector<Type, 2> in_ty;
777 SmallVector<Type, 2> out_ty;
778 SmallVector<ActivityAttr, 2> newInActivityArgs;
779 SmallVector<ActivityAttr, 2> newRetActivityArgs;
780
781 bool changed = false;
782 auto in_idx = 0;
783
784 // go upto dOutput
785 for (auto [idx, act] : llvm::enumerate(inpActivity)) {
786 auto iattr = cast<ActivityAttr>(act);
787 auto val = iattr.getValue();
788 mlir::Value res = uop.getInputs()[in_idx];
789 in_args.push_back(res);
790 in_ty.push_back(res.getType());
791 in_idx++;
792
793 if (val == Activity::enzyme_dup || val == Activity::enzyme_dupnoneed) {
794 mlir::Value dres = uop.getInputs()[in_idx];
795 in_args.push_back(dres);
796 in_ty.push_back(dres.getType());
797 in_idx++;
798 }
799 }
800 // function isn't differentiable
801 if (in_idx == uop.getInputs().size())
802 return failure();
803
804 // handle pOutput
805 for (auto [idx, act] : llvm::enumerate(retActivity)) {
806 auto iattr = cast<ActivityAttr>(act);
807 auto val = iattr.getValue();
808
809 // skip primal return
810 if (val == Activity::enzyme_constnoneed ||
811 val == Activity::enzyme_dupnoneed) {
812 newRetActivityArgs.push_back(iattr);
813 continue;
814 }
815
816 // An op whose outputs do not line up with its activities (there is no
817 // verifier saying they must) is left for the AD pass to diagnose;
818 // reading on would walk off the ends of the ranges.
819 if (out_idx >= (int64_t)uop.getOutputs().size())
820 return failure();
821 mlir::Value res = uop.getOutputs()[out_idx];
822
823 switch (val) {
824 case Activity::enzyme_active: {
825 // active -> activenoneed(if res isn't used)
826 // active -> const(if dres == 0)
827 // active -> constnoneed(both)
828
829 if (in_idx >= (int64_t)uop.getInputs().size())
830 return failure();
831 mlir::Value dres = uop.getInputs()[in_idx];
832 in_idx++;
833
834 auto dres_type = dres.getType();
835 auto dres_type_intf = dyn_cast<AutoDiffTypeInterface>(dres_type);
836
837 if (!res.use_empty()) {
838 outs_args.push_back(res);
839 out_ty.push_back(res.getType());
840 ActivityAttr new_act = iattr;
841 if (dres_type_intf && !isMutable(dres_type) &&
842 dres_type_intf.isZero(dres)) {
843 // const
844 changed = true;
845 new_act = ActivityAttr::get(rewriter.getContext(),
846 Activity::enzyme_const);
847 } else {
848 in_args.push_back(dres);
849 in_ty.push_back(dres_type);
850 }
851 newRetActivityArgs.push_back(new_act);
852 } else {
853 changed = true;
854 ActivityAttr new_act = ActivityAttr::get(
855 rewriter.getContext(), Activity::enzyme_activenoneed);
856 if (dres_type_intf && !isMutable(dres_type) &&
857 dres_type_intf.isZero(dres)) {
858 // constnoneed
859 new_act = ActivityAttr::get(rewriter.getContext(),
860 Activity::enzyme_constnoneed);
861 } else {
862 // activenoneed
863 in_args.push_back(dres);
864 in_ty.push_back(dres_type);
865 }
866 newRetActivityArgs.push_back(new_act);
867 }
868
869 ++out_idx;
870 break;
871 }
872
873 case Activity::enzyme_activenoneed:
874 // activenoneed -> constnoneed
875 {
876 if (in_idx >= (int64_t)uop.getInputs().size())
877 return failure();
878 mlir::Value dres = uop.getInputs()[in_idx];
879 in_idx++;
880 auto new_act = iattr;
881
882 auto dres_type = dres.getType();
883 auto dres_type_intf = dyn_cast<AutoDiffTypeInterface>(dres_type);
884 if (dres_type_intf && !isMutable(dres_type) &&
885 dres_type_intf.isZero(dres)) {
886 // constnoneed
887 new_act = ActivityAttr::get(rewriter.getContext(),
888 Activity::enzyme_constnoneed);
889 } else {
890 in_args.push_back(dres);
891 in_ty.push_back(dres_type);
892 }
893 newRetActivityArgs.push_back(iattr);
894 break;
895 }
896 case Activity::enzyme_const:
897 // const -> constnoneed
898 {
899 auto new_act = iattr;
900 if (!res.use_empty()) {
901 outs_args.push_back(res);
902 out_ty.push_back(res.getType());
903 newRetActivityArgs.push_back(new_act);
904 } else {
905 changed = true;
906 new_act = ActivityAttr::get(rewriter.getContext(),
907 Activity::enzyme_constnoneed);
908 newRetActivityArgs.push_back(new_act);
909 }
910 ++out_idx;
911 break;
912 }
913
914 case Activity::enzyme_dup:
915 // TODO: check if ret_arg == enzyme_dup inserts a derivative as the
916 // output and input both
917 outs_args.push_back(res);
918 out_ty.push_back(res.getType());
919 newRetActivityArgs.push_back(iattr);
920 ++out_idx;
921 break;
922
923 case Activity::enzyme_constnoneed:
924 case Activity::enzyme_dupnoneed:
925 break;
926
927 default:
928 llvm_unreachable("unexpected activity arg");
929 }
930 }
931
932 // handle dInputs
933 for (auto [idx, act] : llvm::enumerate(inpActivity)) {
934 auto iattr = cast<ActivityAttr>(act);
935 auto val = iattr.getValue();
936
937 if (val == Activity::enzyme_active) {
938 if (out_idx >= (int64_t)uop.getOutputs().size())
939 return failure();
940 mlir::Value res = uop.getOutputs()[out_idx];
941 if (!res.use_empty()) {
942 out_ty.push_back(res.getType());
943 outs_args.push_back(res);
944 newInActivityArgs.push_back(iattr);
945 } else {
946 // TODO: check if we can relax immutability here
947 if (!isMutable(res.getType())) {
948 changed = true;
949 auto new_const = ActivityAttr::get(rewriter.getContext(),
950 Activity::enzyme_const);
951 newInActivityArgs.push_back(new_const);
952 } else {
953 // noop even if its not used.
954 out_ty.push_back(res.getType());
955 outs_args.push_back(res);
956 newInActivityArgs.push_back(iattr);
957 }
958 }
959
960 ++out_idx;
961 } else if (val == Activity::enzyme_activenoneed) {
962 mlir::Value res = uop.getOutputs()[out_idx];
963 out_ty.push_back(res.getType());
964 outs_args.push_back(res);
965 newInActivityArgs.push_back(iattr);
966 ++out_idx;
967 llvm_unreachable("unsupported arg activenoneed");
968 } else {
969 newInActivityArgs.push_back(iattr);
970 }
971 }
972
973 if (!changed)
974 return failure();
975
976 ArrayAttr newInActivity =
977 ArrayAttr::get(rewriter.getContext(),
978 llvm::ArrayRef<Attribute>(newInActivityArgs.begin(),
979 newInActivityArgs.end()));
980 ArrayAttr newRetActivity =
981 ArrayAttr::get(rewriter.getContext(),
982 llvm::ArrayRef<Attribute>(newRetActivityArgs.begin(),
983 newRetActivityArgs.end()));
984
985 SourceOp newOp = SourceOpCreator::create(rewriter, uop, out_ty, in_args,
986 newInActivity, newRetActivity);
987
988 // Map old uses of uop to newOp
989 auto oldIdx = 0;
990 auto newIdx = 0;
991 for (auto [idx, old_act, new_act] :
992 llvm::enumerate(retActivity, newRetActivityArgs)) {
993 auto iattr = cast<ActivityAttr>(old_act);
994 auto old_val = iattr.getValue();
995 auto new_val = new_act.getValue();
996
997 if (old_val == new_val) {
998 // don't index into op if no primal is returned
999 if (old_val == Activity::enzyme_constnoneed ||
1000 old_val == Activity::enzyme_activenoneed ||
1001 old_val == Activity::enzyme_dupnoneed) {
1002 continue;
1003 }
1004 // replace current Primal
1005 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
1006 newOp.getOutputs()[newIdx++]);
1007 } else {
1008 // handle all substitutions
1009 if (new_val == Activity::enzyme_activenoneed &&
1010 old_val == Activity::enzyme_active) {
1011 ++oldIdx; // skip active primal
1012 } else if (new_val == Activity::enzyme_constnoneed &&
1013 old_val == Activity::enzyme_const) {
1014 ++oldIdx; // skip const primal
1015 } else if (old_val == Activity::enzyme_active &&
1016 new_val == Activity::enzyme_const) {
1017 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
1018 newOp.getOutputs()[newIdx++]);
1019 } else if (old_val == Activity::enzyme_active &&
1020 new_val == Activity::enzyme_constnoneed) {
1021 ++oldIdx;
1022 } else if (old_val == Activity::enzyme_activenoneed &&
1023 new_val == Activity::enzyme_constnoneed) {
1024 // just skip
1025 }
1026 }
1027 }
1028
1029 for (auto [idx, old_act, new_act] :
1030 llvm::enumerate(inpActivity, newInActivityArgs)) {
1031 auto iattr = cast<ActivityAttr>(old_act);
1032 auto old_val = iattr.getValue();
1033 auto new_val = new_act.getValue();
1034
1035 if (old_val == new_val) {
1036 if (old_val == Activity::enzyme_active ||
1037 old_val == Activity::enzyme_activenoneed) {
1038 uop.getOutputs()[oldIdx++].replaceAllUsesWith(
1039 newOp.getOutputs()[newIdx++]);
1040 } else {
1041 continue;
1042 }
1043 } else {
1044 if (old_val == Activity::enzyme_active &&
1045 new_val == Activity::enzyme_const) {
1046 oldIdx++; // skip derivative
1047 }
1048 }
1049 }
1050 rewriter.eraseOp(uop);
1051 return success();
1052 }
1053};
1054
1055template <typename SourceRegionOp>
1056class RemoveUnusedArgs final : public OpRewritePattern<SourceRegionOp> {
1057
1058private:
1060
1061public:
1062 using OpRewritePattern<SourceRegionOp>::OpRewritePattern;
1063
1064 LogicalResult matchAndRewrite(SourceRegionOp uop,
1065 PatternRewriter &rewriter) const override {
1066 SmallVector<Value> newInArgs;
1067 SmallVector<size_t> argIdxToErase;
1068 SmallVector<ActivityAttr> newInActivityArgs;
1069 llvm::SmallVector<Value> blockArg(uop.getBody().getArguments());
1070 auto in_idx = 0;
1071 for (auto [idx, act] : llvm::enumerate(
1072 uop.getActivity().template getAsRange<ActivityAttr>())) {
1073 auto act_val = act.getValue();
1074 Value res = uop.getInputs()[in_idx++];
1075
1076 if (blockArg[idx].use_empty()) {
1077 argIdxToErase.push_back(idx);
1078 if (act_val == Activity::enzyme_dup ||
1079 act_val == Activity::enzyme_dupnoneed) {
1080 in_idx++;
1081 }
1082 } else {
1083 newInActivityArgs.push_back(act);
1084 newInArgs.push_back(res);
1085 if (act_val == Activity::enzyme_dup ||
1086 act_val == Activity::enzyme_dupnoneed) {
1087 res = uop.getInputs()[in_idx++];
1088 newInArgs.push_back(res);
1089 }
1090 }
1091 }
1092
1093 if (argIdxToErase.empty())
1094 return failure();
1095
1096 // only needed for Autodiff region op
1097 if constexpr (std::is_same_v<SourceRegionOp, AutoDiffRegionOp>) {
1098 newInArgs.append(uop.getDifferentialReturns());
1099 }
1100
1101 ArrayAttr newInActivity =
1102 ArrayAttr::get(rewriter.getContext(),
1103 llvm::ArrayRef<Attribute>(newInActivityArgs.begin(),
1104 newInActivityArgs.end()));
1105 auto newOp =
1106 SourceOpCreator::create(rewriter, uop, uop.getResultTypes(), newInArgs,
1107 newInActivity, uop.getRetActivity());
1108
1109 for (auto idx : llvm::reverse(argIdxToErase)) {
1110 newOp.getBody().eraseArgument(idx);
1111 }
1112
1113 rewriter.replaceOp(uop, newOp);
1114 return success();
1115 }
1116};
1117
1118void AutoDiffOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
1119 MLIRContext *context) {
1120 patterns.add<ReverseRetOpt<AutoDiffOp>>(context);
1121}
1122
1123void AutoDiffRegionOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
1124 MLIRContext *context) {
1125 patterns
1127 context);
1128}
1129
1130void ForwardDiffRegionOp::getCanonicalizationPatterns(
1131 RewritePatternSet &patterns, MLIRContext *context) {
1134}
1135
1136// Adding zero leaves memory alone, so the read-modify-write is only a read.
1137// This is only true of a floating point zero when signed zeros are not
1138// significant: adding +0.0 to a stored -0.0 does change it to +0.0.
1139static bool isRemovableAccumulation(arith::AtomicRMWKind kind, Value value,
1140 arith::FastMathFlags fastmath) {
1141 switch (kind) {
1142 case arith::AtomicRMWKind::addi:
1143 return matchPattern(value, m_Zero());
1144 case arith::AtomicRMWKind::addf:
1145 return matchPattern(value, m_AnyZeroFloat()) &&
1146 bitEnumContainsAll(fastmath, arith::FastMathFlags::nsz);
1147 default:
1148 return false;
1149 }
1150}
1151
1152// Whether a plain load reads what an atomic read-modify-write of this ordering
1153// would have. Anything an observer could order against needs a real atomic
1154// load, which the memref dialect has no way to spell.
1155static bool readIsUnordered(enzyme::Ordering ordering) {
1156 return ordering == enzyme::Ordering::not_atomic ||
1157 ordering == enzyme::Ordering::unordered ||
1158 ordering == enzyme::Ordering::monotonic;
1159}
1160
1161LogicalResult AtomicRMWOp::canonicalize(AtomicRMWOp op,
1162 PatternRewriter &rewriter) {
1163 if (!isRemovableAccumulation(op.getKind(), op.getValue(), op.getFastmath()))
1164 return failure();
1165
1166 if (op.getResult().use_empty()) {
1167 rewriter.eraseOp(op);
1168 return success();
1169 }
1170
1171 if (!readIsUnordered(op.getOrdering()))
1172 return failure();
1173
1174 auto load = memref::LoadOp::create(rewriter, op.getLoc(), op.getMemref(),
1175 op.getIndices());
1176 if (auto alignment = op.getAlignmentAttr())
1177 load.setAlignmentAttr(alignment);
1178 rewriter.replaceOp(op, load.getResult());
1179 return success();
1180}
1181
1182// A memref.cast that only makes the memref's sizes dynamic folds into the
1183// access, as it does into loads and stores.
1184OpFoldResult AtomicRMWOp::fold(FoldAdaptor adaptor) {
1185 if (succeeded(memref::foldMemRefCast(*this, getValue())))
1186 return getResult();
1187 return OpFoldResult();
1188}
1189
1190OpFoldResult AffineAtomicRMWOp::fold(FoldAdaptor adaptor) {
1191 if (succeeded(memref::foldMemRefCast(*this, getValue())))
1192 return getResult();
1193 return OpFoldResult();
1194}
1195
1196LogicalResult AffineAtomicRMWOp::canonicalize(AffineAtomicRMWOp op,
1197 PatternRewriter &rewriter) {
1198 if (!isRemovableAccumulation(op.getKind(), op.getValue(), op.getFastmath()))
1199 return failure();
1200
1201 if (op.getResult().use_empty()) {
1202 rewriter.eraseOp(op);
1203 return success();
1204 }
1205
1206 auto load = affine::AffineLoadOp::create(
1207 rewriter, op.getLoc(), op.getMemref(), op.getMap(), op.getIndices());
1208 if (auto alignment = op.getAlignmentAttr())
1209 load->setAttr("alignment", alignment);
1210 rewriter.replaceOp(op, load.getResult());
1211 return success();
1212}
static bool isMutable(Type type)
Definition Ops.cpp:311
static bool readIsUnordered(enzyme::Ordering ordering)
Definition Ops.cpp:1155
static bool isRemovableAccumulation(arith::AtomicRMWKind kind, Value value, arith::FastMathFlags fastmath)
Definition Ops.cpp:1139
static int64_t binomialProgress(int64_t n, int64_t s)
Definition Ops.cpp:96
Modifies input activites for the FwdDiffOp The activity promotion flow is as follows (depending on va...
Definition Ops.cpp:334
LogicalResult matchAndRewrite(SourceOp uop, PatternRewriter &rewriter) const override
Definition Ops.cpp:338
Modifies return activites for the FwdDiffOp The activity promotion flow is as follows (depending on v...
Definition Ops.cpp:459
LogicalResult matchAndRewrite(SourceOp uop, PatternRewriter &rewriter) const override
Definition Ops.cpp:466
LogicalResult matchAndRewrite(SourceRegionOp uop, PatternRewriter &rewriter) const override
Definition Ops.cpp:1064
Modifies activities for the AutoDiffOp.
Definition Ops.cpp:758
LogicalResult matchAndRewrite(SourceOp uop, PatternRewriter &rewriter) const override
Definition Ops.cpp:765
static AutoDiffOp create(PatternRewriter &rewriter, AutoDiffOp uop, TypeRange out_ty, ValueRange in_args, ArrayAttr newInActivity, ArrayAttr newRetActivity)
Definition Ops.cpp:258
static AutoDiffRegionOp create(PatternRewriter &rewriter, AutoDiffRegionOp uop, TypeRange out_ty, ValueRange in_args, ArrayAttr newInActivity, ArrayAttr newRetActivity)
Definition Ops.cpp:270
static ForwardDiffOp create(PatternRewriter &rewriter, ForwardDiffOp uop, TypeRange out_ty, ValueRange in_args, ArrayAttr newInActivity, ArrayAttr newRetActivity)
Definition Ops.cpp:286
static ForwardDiffRegionOp create(PatternRewriter &rewriter, ForwardDiffRegionOp uop, TypeRange out_ty, ValueRange in_args, ArrayAttr newInActivity, ArrayAttr newRetActivity)
Definition Ops.cpp:297