Enzyme main
Loading...
Searching...
No Matches
HoistLoopAllocations.cpp
Go to the documentation of this file.
1//===- HoistLoopAllocations.cpp - Hoist per-iteration scratch buffers -----===//
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 a pass that lifts a matched allocation/deallocation
10// pair out of a sequential loop, so a scratch buffer is allocated once for the
11// whole loop instead of once per iteration.
12//
13//===----------------------------------------------------------------------===//
14
15#include "Passes/Passes.h"
16
17#include "mlir/Dialect/Affine/IR/AffineOps.h"
18#include "mlir/Dialect/MemRef/IR/MemRef.h"
19#include "mlir/Dialect/SCF/IR/SCF.h"
20#include "mlir/Interfaces/LoopLikeInterface.h"
21#include "mlir/Interfaces/SideEffectInterfaces.h"
22#include "mlir/Interfaces/ViewLikeInterface.h"
23
24namespace mlir {
25namespace enzyme {
26#define GEN_PASS_DEF_HOISTLOOPALLOCATIONSPASS
27#include "Passes/Passes.h.inc"
28} // namespace enzyme
29} // namespace mlir
30
31using namespace mlir;
32using namespace mlir::enzyme;
33
34namespace {
35
36/// Returns the buffer allocated by `op`, if `op` does nothing but allocate it.
37/// An op with any further effect (a read, a write, a second allocation) is not
38/// something we can replay once for the whole loop.
39static Value getAllocatedBuffer(Operation *op) {
40 auto iface = dyn_cast<MemoryEffectOpInterface>(op);
41 if (!iface || op->getNumResults() != 1 ||
42 !isa<MemRefType>(op->getResult(0).getType()))
43 return {};
44
45 SmallVector<MemoryEffects::EffectInstance> effects;
46 iface.getEffects(effects);
47
48 bool allocates = false;
49 for (const MemoryEffects::EffectInstance &effect : effects) {
50 if (!isa<MemoryEffects::Allocate>(effect.getEffect()))
51 return {};
52 if (effect.getValue() != op->getResult(0))
53 return {};
54 allocates = true;
55 }
56 return allocates ? op->getResult(0) : Value();
57}
58
59/// Walks everything reachable from `buffer` and returns the single op that
60/// frees it, or null if the buffer is not a self-contained per-iteration
61/// scratch buffer.
62///
63/// This is the whole legality argument of the transformation. Hoisting makes
64/// every iteration share one buffer, so it is only correct when nothing
65/// derived from the buffer survives the iteration that allocated it: no yield
66/// through `iter_args`, no store of the pointer, no call that might capture.
67/// Rather than enumerate the ways a buffer can escape, we accept only the uses
68/// we can prove harmless -- plain reads and writes, and views that are
69/// themselves only read and written -- and reject the rest.
70static Operation *findPairedDealloc(Value buffer, Operation *loop) {
71 SmallVector<Value> worklist{buffer};
72 SmallPtrSet<Value, 4> seen;
73 Operation *dealloc = nullptr;
74
75 while (!worklist.empty()) {
76 Value alias = worklist.pop_back_val();
77 if (!seen.insert(alias).second)
78 continue;
79
80 for (OpOperand &use : alias.getUses()) {
81 Operation *user = use.getOwner();
82
83 // Values defined inside the loop cannot be used after it, so this only
84 // fires for uses in a sibling region of a multi-region loop.
85 if (!loop->isProperAncestor(user))
86 return nullptr;
87
88 if (hasSingleEffect<MemoryEffects::Free>(user, alias)) {
89 // Only a free of the allocation itself can move after the loop. A free
90 // of a view would leave the view op behind in the body, where the
91 // dealloc can no longer reach it.
92 if (alias != buffer || dealloc)
93 return nullptr;
94 dealloc = user;
95 continue;
96 }
97
98 // A view is re-derived from the buffer each iteration, so it stays in
99 // the body; we just have to keep following it.
100 if (isa<ViewLikeOpInterface>(user)) {
101 worklist.append(user->result_begin(), user->result_end());
102 continue;
103 }
104
105 // A terminator hands the buffer to the next iteration or out of the
106 // loop; a region we do not model may do anything with it.
107 if (user->hasTrait<OpTrait::IsTerminator>() || user->getNumRegions() != 0)
108 return nullptr;
109
110 auto iface = dyn_cast<MemoryEffectOpInterface>(user);
111 if (!iface)
112 return nullptr;
113
114 SmallVector<MemoryEffects::EffectInstance> effects;
115 iface.getEffectsOnValue(alias, effects);
116 if (effects.empty())
117 return nullptr;
118 for (const MemoryEffects::EffectInstance &effect : effects)
119 if (!isa<MemoryEffects::Read, MemoryEffects::Write>(effect.getEffect()))
120 return nullptr;
121
122 // A result that may alias the buffer without being a recognized view is
123 // outside what this analysis can follow.
124 if (llvm::any_of(user->getResultTypes(), llvm::IsaPred<MemRefType>))
125 return nullptr;
126 }
127 }
128
129 return dealloc;
130}
131
132/// Hoisting turns a sum of per-iteration live ranges into a set of overlapping
133/// ones, so a body with several scratch buffers can raise peak memory. The
134/// budget lets a caller bound that; a dynamically sized buffer has no size we
135/// can check it against.
136static bool fitsBudget(Value buffer, uint64_t maxHoistedBytes) {
137 if (maxHoistedBytes == 0)
138 return true;
139
140 auto type = cast<MemRefType>(buffer.getType());
141 if (!type.hasStaticShape() || !type.getElementType().isIntOrFloat())
142 return false;
143
144 uint64_t bytes = llvm::divideCeil(type.getElementTypeBitWidth(), 8);
145 return type.getNumElements() * bytes <= maxHoistedBytes;
146}
147
148/// Only sequential loops qualify: hoisting out of a parallel loop gives every
149/// concurrent iteration the same buffer.
150static bool isSupportedLoop(Operation *op) {
151 return isa<scf::ForOp, affine::AffineForOp>(op);
152}
153
154} // end anonymous namespace
155
156bool mlir::enzyme::hoistLoopAllocations(LoopLikeOpInterface loop,
157 uint64_t maxHoistedBytes) {
158 if (!isSupportedLoop(loop))
159 return false;
160
161 SmallVector<Region *> regions = loop.getLoopRegions();
162 if (regions.size() != 1 || !regions.front()->hasOneBlock())
163 return false;
164 Block *body = &regions.front()->front();
165
166 // Collect first: hoisting rewrites the block we are iterating over.
167 SmallVector<std::pair<Operation *, Operation *>> pairs;
168 for (Operation &op : *body) {
169 Value buffer = getAllocatedBuffer(&op);
170 if (!buffer || !fitsBudget(buffer, maxHoistedBytes))
171 continue;
172
173 // The size, alignment and memory space have to mean the same thing before
174 // the loop as they did inside it.
175 if (!llvm::all_of(op.getOperands(), [&](Value operand) {
176 return loop.isDefinedOutsideOfLoop(operand);
177 }))
178 continue;
179
180 Operation *dealloc = findPairedDealloc(buffer, loop);
181
182 // v1 requires both ops directly in the body's entry block, which is the
183 // shape bufferization emits. It is stricter than necessary -- the real
184 // condition is that the dealloc post-dominates the alloc within the body,
185 // which also admits an alloc/dealloc pair nested together under the same
186 // `scf.if` -- but it needs no dominance information to check.
187 if (!dealloc || dealloc->getBlock() != body)
188 continue;
189
190 pairs.emplace_back(&op, dealloc);
191 }
192
193 for (auto [alloc, dealloc] : pairs) {
194 alloc->moveBefore(loop);
195 dealloc->moveAfter(loop);
196 }
197 return !pairs.empty();
198}
199
200namespace {
201
202struct HoistLoopAllocationsPass
203 : public enzyme::impl::HoistLoopAllocationsPassBase<
204 HoistLoopAllocationsPass> {
205 using HoistLoopAllocationsPassBase::HoistLoopAllocationsPassBase;
206
207 void runOnOperation() override {
208 // Post-order gives us innermost loops first. Hoisting out of an inner loop
209 // leaves the pair in the enclosing body, where the enclosing loop's turn
210 // lifts it another level, so a nest is drained bottom-up in one walk.
211 SmallVector<LoopLikeOpInterface> loops;
212 getOperation()->walk([&](LoopLikeOpInterface loop) {
213 if (isSupportedLoop(loop))
214 loops.push_back(loop);
215 });
216
217 for (LoopLikeOpInterface loop : loops)
218 hoistLoopAllocations(loop, maxHoistedBytes);
219 }
220};
221
222} // end anonymous namespace
bool hoistLoopAllocations(LoopLikeOpInterface loop, uint64_t maxHoistedBytes=0)
Moves allocation/deallocation pairs that are private to one iteration of loop out of it,...