Enzyme main
Loading...
Searching...
No Matches
InlineEnzymeRegions.cpp
Go to the documentation of this file.
1//===- InlineEnzymeRegions.cpp - Inline/outline enzyme.autodiff ------------ //
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file implements passes to inlining and outlining to convert
10// between enzyme.autodiff and enzyme.autodiff_region ops.
11//
12//===----------------------------------------------------------------------===//
13#include "Dialect/Ops.h"
15#include "Passes/Passes.h"
16
17#include "mlir/Dialect/Func/IR/FuncOps.h"
18#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
19#include "mlir/Dialect/MemRef/IR/MemRef.h"
20#include "mlir/IR/Matchers.h"
21#include "mlir/Interfaces/FunctionInterfaces.h"
22#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
23#include "mlir/Transforms/RegionUtils.h"
24#include "llvm/ADT/TypeSwitch.h"
25
26using namespace mlir;
27
28namespace mlir {
29namespace enzyme {
30#define GEN_PASS_DEF_INLINEENZYMEINTOREGIONPASS
31#define GEN_PASS_DEF_OUTLINEENZYMEFROMREGIONPASS
32#include "Passes/Passes.h.inc"
33} // namespace enzyme
34} // namespace mlir
35
36namespace {
37constexpr static llvm::StringLiteral kFnAttrsName = "fn_attrs";
38
39static StringRef getFunctionTypeAttrName(Operation *operation) {
40 if (auto iface = dyn_cast<mlir::enzyme::AutoDiffFunctionInterface>(operation))
41 return iface.getFunctionTypeAttrName();
42 return "";
43}
44
45static StringRef getArgAttrsAttrName(Operation *operation) {
46 if (auto iface = dyn_cast<mlir::enzyme::AutoDiffFunctionInterface>(operation))
47 return iface.getArgAttrsAttrName();
48 return "";
49}
50
51static void serializeFunctionAttributes(Operation *fn, Operation *regionOp) {
52 SmallVector<NamedAttribute> fnAttrs;
53 fnAttrs.reserve(fn->getAttrDictionary().size());
54 for (auto attr : fn->getAttrs()) {
55 // Don't store the function type or sym_name because they may change when
56 // outlining
57 if (attr.getName() == getFunctionTypeAttrName(fn) ||
58 attr.getName() == SymbolTable::getSymbolAttrName())
59 continue;
60 fnAttrs.push_back(attr);
61 }
62
63 regionOp->setAttr(kFnAttrsName,
64 DictionaryAttr::getWithSorted(fn->getContext(), fnAttrs));
65}
66
67template <typename DiffRegionOp>
68static void deserializeFunctionAttributes(DiffRegionOp op,
69 Operation *outlinedFunc,
70 unsigned addedArgCount) {
71 if (!op->template hasAttrOfType<DictionaryAttr>(kFnAttrsName))
72 return;
73
74 MLIRContext *ctx = op->getContext();
75 SmallVector<NamedAttribute> fnAttrs;
76 for (auto attr : op->template getAttrOfType<DictionaryAttr>(kFnAttrsName)) {
77 // New arguments are potentially added when outlining due to references to
78 // values outside the region. Insert an empty arg attr for each newly
79 // added argument.
80 if (attr.getName() == getArgAttrsAttrName(outlinedFunc)) {
81 SmallVector<Attribute> argAttrs(
82 cast<ArrayAttr>(attr.getValue())
83 .template getAsRange<DictionaryAttr>());
84 for (unsigned i = 0; i < addedArgCount; ++i)
85 argAttrs.push_back(DictionaryAttr::getWithSorted(ctx, {}));
86 fnAttrs.push_back(
87 NamedAttribute(attr.getName(), ArrayAttr::get(ctx, argAttrs)));
88 } else
89 fnAttrs.push_back(attr);
90 }
91 outlinedFunc->setAttrs(fnAttrs);
92}
93
94struct InlineEnzymeAutoDiff : public OpRewritePattern<enzyme::AutoDiffOp> {
95 using OpRewritePattern<enzyme::AutoDiffOp>::OpRewritePattern;
96 LogicalResult matchAndRewrite(enzyme::AutoDiffOp op,
97 PatternRewriter &rewriter) const override {
98 SymbolTableCollection symbolTable;
99 if (!mlir::enzyme::inlineAutodiffOp(op, rewriter, symbolTable))
100 return failure();
101
102 return success();
103 }
104};
105
106struct InlineEnzymeForwardDiff
107 : public OpRewritePattern<enzyme::ForwardDiffOp> {
108 using OpRewritePattern<enzyme::ForwardDiffOp>::OpRewritePattern;
109 LogicalResult matchAndRewrite(enzyme::ForwardDiffOp op,
110 PatternRewriter &rewriter) const override {
111 SymbolTableCollection symbolTable;
112 FunctionOpInterface fn = dyn_cast_or_null<FunctionOpInterface>(
113 symbolTable.lookupNearestSymbolFrom(op, op.getFnAttr()));
114
115 if (!fn)
116 return failure();
117
118 Region &targetRegion = fn.getFunctionBody();
119
120 if (targetRegion.empty())
121 return failure();
122
123 // Use a StringAttr rather than a SymbolRefAttr so the function can get
124 // symbol-DCE'd
125 auto fnAttr = StringAttr::get(op.getContext(), op.getFn());
126 auto regionOp = rewriter.replaceOpWithNewOp<enzyme::ForwardDiffRegionOp>(
127 op, op.getResultTypes(), op.getInputs(), op.getActivity(),
128 op.getRetActivity(), op.getWidth(), op.getStrongZero(), fnAttr);
129
130 serializeFunctionAttributes(fn, regionOp);
131 rewriter.cloneRegionBefore(targetRegion, regionOp.getBody(),
132 regionOp.getBody().begin());
133
134 SmallVector<Operation *> toErase;
135 for (Operation &bodyOp : regionOp.getBody().getOps()) {
136 if (bodyOp.hasTrait<OpTrait::ReturnLike>()) {
137 PatternRewriter::InsertionGuard insertionGuard(rewriter);
138 rewriter.setInsertionPoint(&bodyOp);
139 enzyme::YieldOp::create(rewriter, bodyOp.getLoc(),
140 bodyOp.getOperands());
141 toErase.push_back(&bodyOp);
142 }
143 }
144
145 for (Operation *opToErase : toErase)
146 rewriter.eraseOp(opToErase);
147 return success();
148 }
149};
150
151// Based on
152// https://github.com/llvm/llvm-project/blob/665da0a1649814471739c41a702e0e9447316b20/mlir/lib/Dialect/GPU/Transforms/KernelOutlining.cpp
153
154/// Identifies operations that are beneficial to sink into kernels. These
155/// operations may not have side-effects, as otherwise sinking (and hence
156/// duplicating them) is not legal.
157static bool isLikelyAnIndexComputation(Operation *op) {
158 return matchPattern(op, m_Constant()) ||
159 isa<memref::DimOp, arith::SelectOp, arith::CmpIOp>(op);
160}
161
162/// For a given operation `op`, computes whether it is beneficial to sink the
163/// operation into the kernel. An operation can be sunk if doing so does not
164/// introduce new kernel arguments. Whether a value is already available in the
165/// kernel (and hence does not introduce new arguments) is checked by
166/// querying `existingDependencies` and `availableValues`.
167/// If an operand is not yet available, we recursively check whether it can be
168/// made available by siking its defining op.
169/// Operations that are indentified for sinking are added to `beneficiaryOps` in
170/// the order they should appear in the kernel. Furthermore, `availableValues`
171/// is updated with results that will be available after sinking the identified
172/// ops.
173static bool extractBeneficiaryOps(
174 Operation *op, const SetVector<Value> &existingDependencies,
175 SetVector<Operation *> &beneficiaryOps,
176 llvm::SmallPtrSetImpl<Value> &availableValues,
177 llvm::function_ref<bool(Operation *)> isSinkingBeneficiary) {
178 if (beneficiaryOps.count(op))
179 return true;
180
181 if (!isSinkingBeneficiary(op))
182 return false;
183
184 for (Value operand : op->getOperands()) {
185 // It is already visible in the kernel, keep going.
186 if (availableValues.count(operand))
187 continue;
188 // Else check whether it can be made available via sinking or already is a
189 // dependency.
190 Operation *definingOp = operand.getDefiningOp();
191 if ((!definingOp || !extractBeneficiaryOps(definingOp, existingDependencies,
192 beneficiaryOps, availableValues,
193 isSinkingBeneficiary)) &&
194 !existingDependencies.count(operand))
195 return false;
196 }
197 // We will sink the operation, mark its results as now available.
198 beneficiaryOps.insert(op);
199 for (Value result : op->getResults())
200 availableValues.insert(result);
201 return true;
202}
203
204template <typename DiffRegionOp>
205static LogicalResult sinkOperationsIntoRegionOp(
206 DiffRegionOp regionOp,
207 function_ref<bool(Operation *)> isSinkingBeneficiary) {
208 assert(isSinkingBeneficiary);
209 Region &body = regionOp.getBody();
210
211 SetVector<Value> sinkCandidates;
212 getUsedValuesDefinedAbove(body, sinkCandidates);
213
214 SetVector<Operation *> toBeSunk;
215 llvm::SmallPtrSet<Value, 4> availableValues;
216 for (Value operand : sinkCandidates) {
217 Operation *operandOp = operand.getDefiningOp();
218 if (!operandOp)
219 continue;
220 extractBeneficiaryOps(operandOp, sinkCandidates, toBeSunk, availableValues,
221 isSinkingBeneficiary);
222 }
223
224 // Insert operations so that the defs get cloned before uses.
225 IRMapping map;
226 OpBuilder builder(body);
227 for (Operation *op : toBeSunk) {
228 Operation *clonedOp = builder.clone(*op, map);
229 // Only replace uses within the launch op.
230 for (auto pair : llvm::zip(op->getResults(), clonedOp->getResults()))
231 replaceAllUsesInRegionWith(std::get<0>(pair), std::get<1>(pair), body);
232 }
233 return success();
234}
235
236template <typename DiffRegionOp>
237static FailureOr<func::FuncOp> outlineAutoDiffFunc(
238 DiffRegionOp op, StringRef funcName, SmallVectorImpl<Value> &inputs,
239 SmallVectorImpl<enzyme::Activity> &argActivities, OpBuilder &builder) {
240 Region &autodiffRegion = op.getBody();
241 if (failed(sinkOperationsIntoRegionOp(op, isLikelyAnIndexComputation))) {
242 return failure();
243 }
244
245 SmallVector<Type> argTypes(autodiffRegion.getArgumentTypes()), resultTypes;
246 SmallVector<Location> argLocs(autodiffRegion.getNumArguments(), op.getLoc());
247 // Infer the result types from an enzyme.yield op
248 bool found = false;
249 autodiffRegion.walk([&](enzyme::YieldOp yieldOp) {
250 found = true;
251 llvm::append_range(resultTypes, yieldOp.getOperandTypes());
252 return WalkResult::interrupt();
253 });
254 if (!found)
255 return op.emitError()
256 << "enzyme.yield was not found in enzyme.autodiff_region";
257
258 llvm::SetVector<Value> freeValues;
259 getUsedValuesDefinedAbove(autodiffRegion, freeValues);
260
261 llvm::SmallVector<Value> primalValuesAbove = op.getPrimalInputs();
262 llvm::SmallVector<Value> blockArgs(autodiffRegion.getArguments());
263 for (Value value : llvm::make_early_inc_range(freeValues)) {
264 bool isPrimal = false;
265 for (auto [pval, bval] : llvm::zip(primalValuesAbove, blockArgs)) {
266 if (value == pval) {
267 isPrimal = true;
268 for (OpOperand &use : llvm::make_early_inc_range(value.getUses())) {
269 if (op->isProperAncestor(use.getOwner()))
270 use.assign(bval);
271 }
272 freeValues.remove(value);
273 }
274 }
275
276 if (!isPrimal) {
277 inputs.push_back(value);
278 argTypes.push_back(value.getType());
279 argLocs.push_back(value.getLoc());
280 argActivities.push_back(enzyme::Activity::enzyme_const);
281 }
282 }
283 auto fnType = builder.getFunctionType(argTypes, resultTypes);
284
285 // FIXME: making this location the location of the
286 // enzyme.autodiff_region op causes translation to LLVM IR to fail due
287 // to some issue with the dbg info.
288 Location loc = UnknownLoc::get(op.getContext());
289 auto outlinedFunc = func::FuncOp::create(builder, loc, funcName, fnType);
290 outlinedFunc.setPrivate();
291 Region &outlinedBody = outlinedFunc.getBody();
292 deserializeFunctionAttributes(op, outlinedFunc, freeValues.size());
293
294 // Copy over the function body.
295 IRMapping map;
296 Block *entryBlock = builder.createBlock(&outlinedBody, outlinedBody.begin(),
297 argTypes, argLocs);
298 unsigned originalArgCount = autodiffRegion.getNumArguments();
299 for (const auto &arg : autodiffRegion.getArguments())
300 map.map(arg, entryBlock->getArgument(arg.getArgNumber()));
301 for (const auto &operand : enumerate(freeValues))
302 map.map(operand.value(),
303 entryBlock->getArgument(originalArgCount + operand.index()));
304 autodiffRegion.cloneInto(&outlinedBody, map);
305
306 // Replace the terminators with returns
307 for (Block &block : autodiffRegion) {
308 Block *clonedBlock = map.lookup(&block);
309 auto terminator = dyn_cast<enzyme::YieldOp>(clonedBlock->getTerminator());
310 if (!terminator)
311 continue;
312 OpBuilder replacer(terminator);
313 func::ReturnOp::create(replacer, terminator->getLoc(),
314 terminator->getOperands());
315 terminator->erase();
316 }
317
318 // cloneInto results in two blocks, the actual outlined entry block and the
319 // cloned autodiff_region entry block. Splice the cloned entry block into
320 // the actual entry block, then erase the cloned autodiff_region entry.
321 Block *clonedEntry = map.lookup(&autodiffRegion.front());
322 entryBlock->getOperations().splice(entryBlock->getOperations().end(),
323 clonedEntry->getOperations());
324 clonedEntry->erase();
325 return outlinedFunc;
326}
327
328LogicalResult outlineEnzymeAutoDiffRegion(enzyme::AutoDiffRegionOp op,
329 StringRef funcName,
330 OpBuilder &builder) {
331 OpBuilder::InsertionGuard insertionGuard(builder);
332 builder.setInsertionPointAfter(op->getParentOfType<SymbolOpInterface>());
333
334 SmallVector<enzyme::Activity> argActivities =
335 llvm::map_to_vector(op.getActivity().getAsRange<enzyme::ActivityAttr>(),
336 [](auto attr) { return attr.getValue(); });
337 size_t numSeeds = llvm::count_if(
338 op.getRetActivity().getAsRange<enzyme::ActivityAttr>(), [](auto attr) {
339 switch (attr.getValue()) {
340 case enzyme::Activity::enzyme_active:
341 case enzyme::Activity::enzyme_activenoneed:
342 return true;
343 default:
344 return false;
345 }
346 });
347 size_t numPrimalsAndShadows = op.getInputs().size() - numSeeds;
348 SmallVector<Value> primalsAndShadows(
349 op.getInputs().begin(), op.getInputs().begin() + numPrimalsAndShadows);
350 SmallVector<Value> seedInputs(op.getInputs().begin() + numPrimalsAndShadows,
351 op.getInputs().end());
352
353 // Free variables are appended to primalInputs.
354 // The final input ordering should be:
355 // 1. primals and duplicated argument shadows
356 // 2. free variables
357 // 3. return variable shadows (a.k.a. seeds)
358 FailureOr<func::FuncOp> outlinedFunc = outlineAutoDiffFunc(
359 op, funcName, primalsAndShadows, argActivities, builder);
360 if (failed(outlinedFunc))
361 return failure();
362
363 SmallVector<Value> allInputs;
364 allInputs.append(primalsAndShadows);
365 allInputs.append(seedInputs);
366
367 builder.setInsertionPoint(op);
368 ArrayAttr argActivityAttr = builder.getArrayAttr(llvm::map_to_vector(
369 argActivities, [&op](enzyme::Activity actv) -> Attribute {
370 return enzyme::ActivityAttr::get(op.getContext(), actv);
371 }));
372 auto newOp = enzyme::AutoDiffOp::create(
373 builder, op.getLoc(), op.getResultTypes(), outlinedFunc->getName(),
374 allInputs, argActivityAttr, op.getRetActivity(), op.getWidth(),
375 op.getStrongZero(), op.getAtomicAdd());
376 op.replaceAllUsesWith(newOp.getResults());
377 op.erase();
378 return success();
379}
380
381LogicalResult outlineEnzymeForwardDiffRegion(enzyme::ForwardDiffRegionOp op,
382 StringRef funcName,
383 OpBuilder &builder) {
384 OpBuilder::InsertionGuard insertionGuard(builder);
385 builder.setInsertionPointAfter(op->getParentOfType<SymbolOpInterface>());
386
387 SmallVector<enzyme::Activity> argActivities =
388 llvm::map_to_vector(op.getActivity().getAsRange<enzyme::ActivityAttr>(),
389 [](auto attr) { return attr.getValue(); });
390 SmallVector<Value> primalsAndShadows(op.getInputs());
391
392 // Free variables are appended to primalInputs.
393 // The final input ordering should be:
394 // 1. primals and duplicated argument shadows
395 // 2. free variables
396 FailureOr<func::FuncOp> outlinedFunc = outlineAutoDiffFunc(
397 op, funcName, primalsAndShadows, argActivities, builder);
398 if (failed(outlinedFunc))
399 return failure();
400
401 SmallVector<Value> allInputs;
402 allInputs.append(primalsAndShadows);
403
404 builder.setInsertionPoint(op);
405 ArrayAttr argActivityAttr = builder.getArrayAttr(llvm::map_to_vector(
406 argActivities, [&op](enzyme::Activity actv) -> Attribute {
407 return enzyme::ActivityAttr::get(op.getContext(), actv);
408 }));
409 auto newOp = enzyme::ForwardDiffOp::create(
410 builder, op.getLoc(), op.getResultTypes(), outlinedFunc->getName(),
411 primalsAndShadows, argActivityAttr, op.getRetActivity(), op.getWidth(),
412 op.getStrongZero());
413 op.replaceAllUsesWith(newOp.getResults());
414 op.erase();
415 return success();
416}
417
418struct InlineEnzymeIntoRegion
419 : public enzyme::impl::InlineEnzymeIntoRegionPassBase<
420 InlineEnzymeIntoRegion> {
421 void runOnOperation() override {
422 RewritePatternSet patterns(&getContext());
423 patterns.insert<InlineEnzymeAutoDiff, InlineEnzymeForwardDiff>(
424 &getContext());
425
426 GreedyRewriteConfig config;
427 config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Normal);
428 (void)applyPatternsGreedily(getOperation(), std::move(patterns), config);
429 }
430};
431
432struct OutlineEnzymeFromRegion
433 : public enzyme::impl::OutlineEnzymeFromRegionPassBase<
434 OutlineEnzymeFromRegion> {
435 void runOnOperation() override {
436 SmallVector<enzyme::AutoDiffRegionOp> toOutlineRev;
437 getOperation()->walk(
438 [&](enzyme::AutoDiffRegionOp op) { toOutlineRev.push_back(op); });
439
440 SmallVector<enzyme::ForwardDiffRegionOp> toOutlineFwd;
441 getOperation()->walk(
442 [&](enzyme::ForwardDiffRegionOp op) { toOutlineFwd.push_back(op); });
443
444 OpBuilder builder(getOperation());
445 unsigned increment = 0;
446 for (auto regionOp : toOutlineRev) {
447 auto symbol = regionOp->getParentOfType<SymbolOpInterface>();
448 std::string defaultName =
449 (Twine(symbol.getName(), "_to_diff") + Twine(increment)).str();
450 if (failed(outlineEnzymeAutoDiffRegion(regionOp, defaultName, builder)))
451 return signalPassFailure();
452
453 ++increment;
454 }
455
456 for (auto regionOp : toOutlineFwd) {
457 auto symbol = regionOp->getParentOfType<SymbolOpInterface>();
458 std::string defaultName =
459 (Twine(symbol.getName(), "_to_fwddiff") + Twine(increment)).str();
460 if (failed(
461 outlineEnzymeForwardDiffRegion(regionOp, defaultName, builder)))
462 return signalPassFailure();
463
464 ++increment;
465 }
466 }
467};
468
469} // namespace
470
471bool mlir::enzyme::inlineAutodiffOp(enzyme::AutoDiffOp &op,
472 RewriterBase &rewriter,
473 SymbolTableCollection &symbolTable) {
474 FunctionOpInterface fn = dyn_cast_or_null<FunctionOpInterface>(
475 symbolTable.lookupNearestSymbolFrom(op, op.getFnAttr()));
476 if (!fn)
477 return false;
478 Region &targetRegion = fn.getFunctionBody();
479 if (targetRegion.empty())
480 return false;
481
482 // Use a StringAttr rather than a SymbolRefAttr so the function can get
483 // symbol-DCE'd
484 auto fnAttr = StringAttr::get(op.getContext(), op.getFn());
485 auto regionOp = rewriter.replaceOpWithNewOp<enzyme::AutoDiffRegionOp>(
486 op, op.getResultTypes(), op.getInputs(), op.getActivity(),
487 op.getRetActivity(), op.getWidth(), op.getStrongZero(), op.getAtomicAdd(),
488 fnAttr);
489 serializeFunctionAttributes(fn, regionOp);
490 rewriter.cloneRegionBefore(targetRegion, regionOp.getBody(),
491 regionOp.getBody().begin());
492 SmallVector<Operation *> toErase;
493 for (Operation &bodyOp : regionOp.getBody().getOps()) {
494 if (bodyOp.hasTrait<OpTrait::ReturnLike>()) {
495 PatternRewriter::InsertionGuard insertionGuard(rewriter);
496 rewriter.setInsertionPoint(&bodyOp);
497 enzyme::YieldOp::create(rewriter, bodyOp.getLoc(), bodyOp.getOperands());
498 toErase.push_back(&bodyOp);
499 }
500 }
501
502 for (Operation *opToErase : toErase)
503 rewriter.eraseOp(opToErase);
504 return true;
505}
static std::string str(AugmentedStruct c)
Definition EnzymeLogic.h:62
bool inlineAutodiffOp(enzyme::AutoDiffOp &op, RewriterBase &rewriter, SymbolTableCollection &symbolTable)