Enzyme main
Loading...
Searching...
No Matches
RemoveUnusedEnzymeOps.cpp
Go to the documentation of this file.
1//===- RemoveUnusedEnzymeOps.cpp - Remove unnecessary or unused gradient and
2// cache ops
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//
11//===----------------------------------------------------------------------===//
12
13#include "Dialect/Dialect.h"
14#include "Dialect/Ops.h"
16#include "PassDetails.h"
17#include "Passes/Passes.h"
18#include "Passes/RemovalUtils.h"
19#include "mlir/Dialect/Arith/IR/Arith.h"
20#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
21#include "mlir/Dialect/Func/IR/FuncOps.h"
22#include "mlir/Dialect/MemRef/IR/MemRef.h"
23#include "mlir/Dialect/SCF/IR/SCF.h"
24#include "mlir/Dialect/Tensor/IR/Tensor.h"
25#include "mlir/Transforms/DialectConversion.h"
26
27#include "mlir/Rewrite/PatternApplicator.h"
28#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
29
30#include "mlir/IR/Dominance.h"
31#include "llvm/Support/raw_ostream.h"
32
33namespace mlir {
34namespace enzyme {
35#define GEN_PASS_DEF_REMOVEUNUSEDENZYMEOPSPASS
36#include "Passes/Passes.h.inc"
37} // namespace enzyme
38} // namespace mlir
39
40using namespace mlir;
41using namespace enzyme;
42namespace {
43
44// Starting at the beginning of blk, is there a path that can execute
45// check before end.
46bool mayExecuteBefore(Block *blk, Operation *check, Operation *end) {
47 auto reg = blk->getParent();
48 (void)reg;
49 assert(reg->isAncestor(end->getParentRegion()));
50
51 DenseSet<Block *> visitedBlocks;
52
53 SmallVector<Block *> blocksToVisit;
54 for (auto succ : blk->getSuccessors()) {
55 blocksToVisit.push_back(succ);
56 }
57
58 while (!blocksToVisit.empty()) {
59 Block *cur = blocksToVisit.pop_back_val();
60
61 if (visitedBlocks.contains(cur))
62 continue;
63
64 visitedBlocks.insert(cur);
65
66 bool seenEnd = false;
67 for (auto &op : *cur) {
68
69 // If we've seen the thing to check with, it may execute before
70 if (op.isAncestor(check)) {
71 // The sole exception to this is if they are in the same sub region,
72 // which is known to execute only once. TODO this later
73 /*
74 if (op.isAncestor(end)) {
75
76 for (auto reg2 : op.getRegions()) {
77
78 }
79 }
80 */
81
82 return true;
83 }
84
85 // Otherwise if we've seen the end op, this path is over as the route we
86 // found here didn't first find a check.
87 if (op.isAncestor(end)) {
88 seenEnd = true;
89 break;
90 }
91 }
92
93 if (seenEnd)
94 continue;
95
96 // If we didn't find the end, try all successors
97 for (auto succ : cur->getSuccessors()) {
98 blocksToVisit.push_back(succ);
99 }
100 }
101
102 return false;
103}
104
105bool mayExecuteBetween(Operation *start, Operation *check, Operation *end) {
106 Block *blk = start->getBlock();
107 auto checkAnc = check;
108 while (checkAnc && checkAnc->getBlock() != blk)
109 checkAnc = checkAnc->getParentOp();
110
111 auto endAnc = end;
112 while (endAnc && endAnc->getBlock() != blk)
113 endAnc = endAnc->getParentOp();
114
115 // Both operations are either the same, or in a joint parent (which could
116 // rerun)
117 if (checkAnc && checkAnc == endAnc)
118 return true;
119
120 if (checkAnc) {
121 if (checkAnc->isBeforeInBlock(start))
122 checkAnc = nullptr;
123 }
124 if (endAnc) {
125 if (endAnc->isBeforeInBlock(start))
126 endAnc = nullptr;
127 }
128
129 if (checkAnc && endAnc) {
130 if (checkAnc->isBeforeInBlock(endAnc))
131 return true;
132 else
133 return false;
134 }
135
136 if (checkAnc)
137 return true;
138 if (endAnc)
139 return false;
140
141 auto reg = blk->getParent();
142 if (reg->isAncestor(end->getParentRegion())) {
143 return mayExecuteBefore(blk, check, end);
144 }
145
146 // If the check is in the parent op, but the end is not, assume
147 // we may execute that parent op part before going to any later ops
148 if (reg->isAncestor(check->getParentRegion())) {
149 return true;
150 }
151
152 return mayExecuteBetween(start->getParentOp(), check, end);
153}
154
155// TODO this isn't necessarily correct. This is because there could be a
156// non dominating use bewteen the dominating one and the op, causing
157// correctness issues when not seen. In interim, be conservative and only
158// succeed if these have the same parent block, and no other ops in path
159template <class T, class T2 = T>
160T findNearestDominatingOpByUse(Operation *op, Value v) {
161 DominanceInfo dInfo;
162 PostDominanceInfo pdInfo;
163
164 SmallVector<T, 1> options;
165 SmallVector<Operation *, 1> conflicts;
166 for (Operation *userSet : v.getUsers()) {
167 if (auto setOp = dyn_cast<T>(userSet)) {
168 options.push_back(setOp);
169 conflicts.push_back(setOp);
170 continue;
171 }
172 if (auto setOp = dyn_cast<T2>(userSet)) {
173 conflicts.push_back(setOp);
174 continue;
175 }
176 }
177
178 for (auto opt : options) {
179 if (!dInfo.dominates(opt, op))
180 continue;
181 bool conflict = false;
182 for (auto opt2 : conflicts) {
183 if (opt == opt2)
184 continue;
185 if (opt2 == op)
186 continue;
187
188 if (!mayExecuteBetween(opt, opt2, op)) {
189 continue;
190 }
191
192 conflict = true;
193 }
194 if (!conflict) {
195 return opt;
196 }
197 }
198
199 return nullptr;
200}
201
202struct PopSimplify : public OpRewritePattern<enzyme::PopOp> {
203 using OpRewritePattern<enzyme::PopOp>::OpRewritePattern;
204
205 LogicalResult matchAndRewrite(enzyme::PopOp pop,
206 PatternRewriter &rewriter) const final {
207
208 auto init = pop.getCache().getDefiningOp<enzyme::InitOp>();
209 if (!init)
210 return failure();
211
212 SmallVector<enzyme::PopOp, 1> pops;
213 SmallVector<enzyme::PushOp, 1> pushes;
214 for (Operation *userSet : init.getResult().getUsers()) {
215 if (auto push = dyn_cast<enzyme::PushOp>(userSet)) {
216 pushes.push_back(push);
217 continue;
218 }
219 if (auto pop = dyn_cast<enzyme::PopOp>(userSet)) {
220 pops.push_back(pop);
221 continue;
222 }
223 return failure();
224 }
225
226 if (auto push = findNearestDominatingOpByUse<enzyme::PushOp, enzyme::PopOp>(
227 pop, init)) {
228 // Do the block check to conservatively avoid multi execute push/pop
229 if (pop->getBlock() == push->getBlock()) {
230 rewriter.replaceOp(pop, push.getValue());
231 rewriter.eraseOp(push);
232 return success();
233 }
234 }
235
236 return failure();
237 }
238};
239
240// A pop nobody reads is worth keeping only for what it does to the cache: it
241// moves the stack on for whatever pops next. When it is the one pop its cache
242// has, there is no next, and the whole cache is dead -- so the pushes go with
243// it, in the one rewrite. Taking the pop alone would leave a cache pushed and
244// never popped, which is worse than what was there before.
245//
246// PopSimplify cannot reach these: it pairs a pop with the push that dominates
247// it and gives up when the two are in different blocks, which is exactly the
248// shape reverse mode leaves behind for a branch whose adjoint turned out empty.
249struct DeadPopSimplify : public OpRewritePattern<enzyme::PopOp> {
250 using OpRewritePattern<enzyme::PopOp>::OpRewritePattern;
251
252 LogicalResult matchAndRewrite(enzyme::PopOp pop,
253 PatternRewriter &rewriter) const final {
254 if (!pop.getResult().use_empty())
255 return failure();
256
257 auto init = pop.getCache().getDefiningOp<enzyme::InitOp>();
258 if (!init)
259 return failure();
260
261 // Anything else holding the cache could be reading the stack in a way this
262 // cannot see; only a cache that is nothing but pushed and popped is known.
263 SmallVector<enzyme::PushOp> pushes;
264 for (Operation *user : init.getResult().getUsers()) {
265 if (user == pop)
266 continue;
267 if (auto push = dyn_cast<enzyme::PushOp>(user)) {
268 pushes.push_back(push);
269 continue;
270 }
271 return failure();
272 }
273
274 rewriter.eraseOp(pop);
275 for (enzyme::PushOp push : pushes)
276 rewriter.eraseOp(push);
277 rewriter.eraseOp(init);
278 return success();
279 }
280};
281
282struct GetSimplify : public OpRewritePattern<enzyme::GetOp> {
283 using OpRewritePattern<enzyme::GetOp>::OpRewritePattern;
284
285 LogicalResult matchAndRewrite(enzyme::GetOp get,
286 PatternRewriter &rewriter) const final {
287
288 auto init = get.getGradient().getDefiningOp<enzyme::InitOp>();
289 if (!init)
290 return failure();
291
292 for (Operation *userSet : init.getResult().getUsers()) {
293 if (isa<enzyme::GetOp>(userSet))
294 continue;
295 if (isa<enzyme::SetOp>(userSet))
296 continue;
297 return failure();
298 }
299
300 if (auto set = findNearestDominatingOpByUse<enzyme::SetOp>(get, init)) {
301 rewriter.replaceOp(get, set.getValue());
302 return success();
303 }
304 return failure();
305 }
306};
307
308struct SetSimplify : public OpRewritePattern<enzyme::SetOp> {
309 using OpRewritePattern<enzyme::SetOp>::OpRewritePattern;
310
311 LogicalResult matchAndRewrite(enzyme::SetOp get,
312 PatternRewriter &rewriter) const final {
313
314 auto init = get.getGradient().getDefiningOp<enzyme::InitOp>();
315 if (!init)
316 return failure();
317
318 for (Operation *userSet : init.getResult().getUsers()) {
319 if (isa<enzyme::SetOp>(userSet))
320 continue;
321 return failure();
322 }
323
324 rewriter.eraseOp(get);
325 return success();
326 }
327};
328
329struct PushSimplify : public OpRewritePattern<enzyme::PushOp> {
330 using OpRewritePattern<enzyme::PushOp>::OpRewritePattern;
331
332 LogicalResult matchAndRewrite(enzyme::PushOp get,
333 PatternRewriter &rewriter) const final {
334
335 auto init = get.getCache().getDefiningOp<enzyme::InitOp>();
336 if (!init)
337 return failure();
338
339 for (Operation *userSet : init.getResult().getUsers()) {
340 if (isa<enzyme::PushOp>(userSet))
341 continue;
342 return failure();
343 }
344
345 rewriter.eraseOp(get);
346 return success();
347 }
348};
349
350struct InitSimplify : public OpRewritePattern<enzyme::InitOp> {
351 using OpRewritePattern<enzyme::InitOp>::OpRewritePattern;
352
353 LogicalResult matchAndRewrite(enzyme::InitOp init,
354 PatternRewriter &rewriter) const final {
355 if (init.use_empty()) {
356 rewriter.eraseOp(init);
357 return success();
358 }
359 return failure();
360 }
361};
362
363struct IgnoreDerivativesSimplifyPattern
364 : public OpRewritePattern<enzyme::IgnoreDerivativesOp> {
365 using OpRewritePattern<enzyme::IgnoreDerivativesOp>::OpRewritePattern;
366
367 LogicalResult matchAndRewrite(enzyme::IgnoreDerivativesOp op,
368 PatternRewriter &rewriter) const override {
369 rewriter.replaceOp(op, op.getOperand());
370 return success();
371 }
372};
373
374static void applyPatterns(Operation *op) {
375 RewritePatternSet patterns(op->getContext());
376 patterns.insert<PopSimplify, DeadPopSimplify, GetSimplify, PushSimplify,
377 SetSimplify, InitSimplify, IgnoreDerivativesSimplifyPattern>(
378 op->getContext());
379
380 GreedyRewriteConfig config;
381 config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Normal);
382 config.enableFolding();
383 (void)applyPatternsGreedily(op, std::move(patterns), config);
384}
385
386// Same patterns, for an op that is not IsolatedFromAbove: the greedy driver
387// only takes those, so the enzyme ops inside are handed to it individually.
388static void applyPatternsToEnzymeOps(Operation *op,
389 RewriterBase::Listener *listener) {
390 SmallVector<Operation *> ops;
391 op->walk([&](Operation *nested) {
392 if (isa<enzyme::InitOp, enzyme::PushOp, enzyme::PopOp, enzyme::GetOp,
393 enzyme::SetOp>(nested))
394 ops.push_back(nested);
395 });
396 if (ops.empty())
397 return;
398
399 RewritePatternSet patterns(op->getContext());
400 patterns.insert<PopSimplify, DeadPopSimplify, GetSimplify, PushSimplify,
401 SetSimplify, InitSimplify>(op->getContext());
402
403 GreedyRewriteConfig config;
404 config.setRegionSimplificationLevel(GreedySimplifyRegionLevel::Normal);
405 config.enableFolding();
406 if (listener)
407 config.setListener(listener);
408 (void)applyOpPatternsGreedily(ops, std::move(patterns), config);
409}
410
411static bool hasInitInRegions(Operation *op) {
412 return op->walk([](enzyme::InitOp) { return WalkResult::interrupt(); })
413 .wasInterrupted();
414}
415
416static void annotateRegionOpsInLoops(Operation *op) {
417 // When we have non-looping region branch ops (e.g. scf.if) inside of a loop,
418 // we want the pushes/pops to be removed by the outer loop remover, not the
419 // inner op remover. This helps mincut reduce the overall caching overhead.
420 //
421 // Only for caches the loop remover can take. One it can is initialized
422 // outside the loop, so that it outlives an iteration and its pushes can be
423 // paired with pops in the reverse loop. An init inside the loop belongs to
424 // that iteration alone; removal stops at the init's parent and never reaches
425 // the loop, so standing the region op down for it leaves nothing to remove it
426 // at all. Inlining a differentiated function into a loop body brings in
427 // exactly those, along with the ifs it cached across.
428 op->walk([](LoopLikeOpInterface loop) {
429 loop->walk([&](RegionBranchOpInterface regionBranch) {
430 if (regionBranch.hasLoop())
431 return;
432
433 // The attribute covers the whole region op, so one loop-local cache in it
434 // is enough to have to keep removing them here. Both ends count: the
435 // push and the pop of such a cache sit in different region ops, and
436 // standing either of them down strands it.
437 bool anyLoopLocal = false;
438 auto checkCache = [&](Value cache) {
439 Operation *init = cache.getDefiningOp();
440 if (!init || loop->isProperAncestor(init))
441 anyLoopLocal = true;
442 };
443 regionBranch->walk([&](Operation *nested) {
444 // A cache under a nested loop is that loop's to pair, and its own
445 // region ops are annotated when the walk above reaches it.
446 if (nested != regionBranch.getOperation() &&
447 isa<LoopLikeOpInterface>(nested))
448 return WalkResult::skip();
449
450 if (auto push = dyn_cast<enzyme::PushOp>(nested))
451 checkCache(push.getCache());
452 else if (auto pop = dyn_cast<enzyme::PopOp>(nested))
453 checkCache(pop.getCache());
454 return WalkResult::advance();
455 });
456 if (anyLoopLocal)
457 return;
458
459 regionBranch->setAttr(kPreserveCacheAttrName,
460 UnitAttr::get(regionBranch.getContext()));
461 });
462 });
463}
464
465// A worklist that supports removing operations
466// original implementation is from
467// https://github.com/llvm/llvm-project/blob/9d8d538e40ef040cb53e8db7a32f3024865187f3/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp#L198
468class Worklist {
469public:
470 Worklist() { list.reserve(8); }
471
472 bool empty();
473 void push(Operation *op);
474 void remove(Operation *op);
475 Operation *pop();
476 void reverse();
477
478private:
479 std::vector<Operation *> list;
480 llvm::DenseMap<Operation *, unsigned> map;
481};
482
483bool Worklist::empty() {
484 // Skip all nullptr.
485 return !llvm::any_of(list,
486 [](Operation *op) { return static_cast<bool>(op); });
487}
488
489void Worklist::push(Operation *op) {
490 assert(op && "cannot push nullptr to worklist");
491 // Check to see if the worklist already contains this op.
492 if (!map.insert({op, list.size()}).second)
493 return;
494 list.push_back(op);
495}
496
497void Worklist::reverse() {
498 std::reverse(list.begin(), list.end());
499 for (size_t i = 0, e = list.size(); i < e; ++i)
500 map[list[i]] = i;
501}
502
503Operation *Worklist::pop() {
504 // Skip and remove all trailing nullptr.
505 while (!list.back())
506 list.pop_back();
507 Operation *op = list.back();
508 list.pop_back();
509 map.erase(op);
510 // Cleanup: Remove all trailing nullptr.
511 while (!list.empty() && !list.back())
512 list.pop_back();
513 return op;
514}
515
516void Worklist::remove(Operation *op) {
517 assert(op && "cannot remove nullptr from worklist");
518 auto it = map.find(op);
519 if (it != map.end()) {
520 assert(list[it->second] == op && "malformed worklist data structure");
521 list[it->second] = nullptr;
522 map.erase(it);
523 }
524}
525
526// Drives Enzyme ops removal with the following goals:
527// * Each EnzymeOpsRemoverOpInterface should be processed once.
528// * Inserted ops next in the post order should still be run.
529class PostOrderWalkDriver : public RewriterBase::Listener {
530public:
531 PostOrderWalkDriver(Operation *root_) : root(root_) {}
532
533 void initializeWorklist();
534 LogicalResult processWorklist();
535
536protected:
537 void notifyOperationInserted(Operation *op,
538 OpBuilder::InsertPoint previous) override;
539 void notifyOperationErased(Operation *op) override;
540
541 void
542 notifyMatchFailure(Location loc,
543 function_ref<void(Diagnostic &)> reasonCallback) override;
544
545private:
546 void addToWorklist(Operation *op);
547
548 Worklist worklist;
549
550 Operation *current = nullptr;
551 Operation *root;
552};
553
554void PostOrderWalkDriver::addToWorklist(Operation *op) {
555 // This driver only processes EnzymeOpsRemoverOpInterface ops.
556 if (!isa<EnzymeOpsRemoverOpInterface>(op))
557 return;
558 worklist.push(op);
559}
560
561void PostOrderWalkDriver::notifyOperationInserted(
562 Operation *op, OpBuilder::InsertPoint previous) {
563 if (!isa<EnzymeOpsRemoverOpInterface>(op))
564 return;
565
566 if (!current) {
567 addToWorklist(op);
568 return;
569 }
570
571 // Check if the inserted op would be next in the post order or not compared to
572 // the current operation.
573 bool shouldInsert = false;
574 (void)root->walk([&](EnzymeOpsRemoverOpInterface iface) {
575 if ((Operation *)iface == current) {
576 shouldInsert = true;
577 return WalkResult::interrupt();
578 }
579
580 if ((Operation *)iface == op) {
581 shouldInsert = false;
582 return WalkResult::interrupt();
583 }
584
585 return WalkResult::advance();
586 });
587
588 if (shouldInsert)
589 addToWorklist(op);
590}
591
592void PostOrderWalkDriver::notifyOperationErased(Operation *op) {
593 if (op == current) {
594 current = nullptr;
595 }
596 if (!isa<EnzymeOpsRemoverOpInterface>(op))
597 return;
598 worklist.remove(op);
599}
600
601void PostOrderWalkDriver::notifyMatchFailure(
602 Location loc, function_ref<void(Diagnostic &)> reasonCallback) {
603 auto diag = mlir::emitError(loc);
604 reasonCallback(*diag.getUnderlyingDiagnostic());
605}
606
607void PostOrderWalkDriver::initializeWorklist() {
608 root->walk<WalkOrder::PreOrder>(
609 [this](EnzymeOpsRemoverOpInterface iface) { addToWorklist(iface); });
610}
611
612LogicalResult PostOrderWalkDriver::processWorklist() {
613 PatternRewriter rewriter(root->getContext());
614 rewriter.setListener(this);
615
616 bool result = true;
617 while (!worklist.empty()) {
618 auto op = worklist.pop();
619 auto iface = cast<EnzymeOpsRemoverOpInterface>(op);
620 current = op;
621
622 if (hasInitInRegions(op))
623 applyPatternsToEnzymeOps(op, this);
624
625 rewriter.setInsertionPoint(current);
626 result &= iface.removeEnzymeOps(rewriter).succeeded();
627 current = nullptr;
628 }
629
630 return LogicalResult::success(result);
631}
632
633struct RemoveUnusedEnzymeOpsPass
634 : public enzyme::impl::RemoveUnusedEnzymeOpsPassBase<
635 RemoveUnusedEnzymeOpsPass> {
636 void runOnOperation() override {
637 auto op = getOperation();
638
639 applyPatterns(op);
640
641 annotateRegionOpsInLoops(op);
642 bool failed = false;
643 op->walk([&](FunctionOpInterface func) {
644 PostOrderWalkDriver driver(func);
645 driver.initializeWorklist();
646 failed |= driver.processWorklist().failed();
647 });
648
649 if (failed) {
650 signalPassFailure();
651 return;
652 }
653
654 applyPatterns(op);
655 }
656};
657
658} // end anonymous namespace
static constexpr llvm::StringLiteral kPreserveCacheAttrName