MLIR 24.0.0git
StaticMemoryPlannerAnalysis.cpp
Go to the documentation of this file.
1//===- StaticMemoryPlannerAnalysis.cpp - Static memory planning -----------===//
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// Transforms memref.alloc/memref.dealloc pairs into a single arena allocation
10// with memref.view. Delegates offset computation to planning algorithms in
11// StaticMemoryPlanning.h.
12//
13//===----------------------------------------------------------------------===//
14
20#include "mlir/IR/Builders.h"
22#include "llvm/ADT/SmallPtrSet.h"
23#include "llvm/Support/Debug.h"
24#include <numeric>
25
26#define DEBUG_TYPE "static-memory-planner"
27
28namespace mlir {
29namespace bufferization {
30#define GEN_PASS_DEF_STATICMEMORYPLANNERANALYSISPASS
31#include "mlir/Dialect/Bufferization/Transforms/Passes.h.inc"
32} // namespace bufferization
33} // namespace mlir
34
35using namespace mlir;
36
37namespace {
38
39/// A candidate allocation with its matching deallocation(s) and assigned
40/// offset. An alloc may be freed indirectly through arith.select chains,
41/// yielding multiple potential deallocs — all must be in the same block.
42struct AllocationCandidate {
43 memref::AllocOp alloc;
44 SmallVector<memref::DeallocOp> deallocs;
45 int64_t offset = 0; // Offset in bytes from arena start (assigned by planner)
46 int64_t sizeInBytes = 0; // Size in bytes
47 int64_t alignment = 1; // Required alignment in bytes
48};
49
50//===----------------------------------------------------------------------===//
51// Helper utilities
52//===----------------------------------------------------------------------===//
53
54/// Collect all dealloc ops that might free the given alloc value. Instead of a
55/// bespoke traversal, this uses the shared `BufferViewFlowAnalysis`, which
56/// already models all the ways a buffer can flow to a dealloc:
57/// - `arith.select` (via BufferViewFlowOpInterface)
58/// - `scf.if`/`scf.for` (via RegionBranchOpInterface region/result wiring)
59/// - `cf.br`/`cf.cond_br` (via BranchOpInterface block arguments)
60/// - `memref.view`/subview (via ViewLikeOpInterface)
61/// `analysis.resolve(alloc)` returns the forward alias set (the alloc plus
62/// every value it may flow into); a dealloc on any of those aliases frees the
63/// alloc. For example:
64/// %0 = memref.alloc()
65/// %2 = arith.select %c, %0, %1
66/// memref.dealloc %2 <- covers %0 conditionally (via alias set)
67/// %3 = scf.if %c { yield %0 } else { yield %1 }
68/// memref.dealloc %3 <- also covers %0 conditionally
69static void collectDeallocs(Value alloc, const BufferViewFlowAnalysis &analysis,
71 for (Value alias : analysis.resolve(alloc))
72 for (Operation *user : alias.getUsers())
73 if (auto dealloc = dyn_cast<memref::DeallocOp>(user))
74 deallocs.push_back(dealloc);
75}
76
77/// Return the set of allocation ops whose buffer may be freed by `dealloc`,
78/// i.e. the terminal `memref.alloc` sources that flow into the dealloc operand.
79/// Uses the reverse alias set so that a dealloc reached through a `scf.if`
80/// result or `arith.select` is attributed to every alloc it may free.
82findFreedAllocs(memref::DeallocOp dealloc,
83 const BufferViewFlowAnalysis &analysis) {
85 for (Value source : analysis.resolveReverse(dealloc.getMemref()))
86 if (auto allocOp = source.getDefiningOp<memref::AllocOp>())
87 allocs.push_back(allocOp);
88 return allocs;
89}
90
91/// Compute the size in bytes for a memref type.
92static int64_t computeSizeInBytes(MemRefType memrefType) {
93 int64_t numElements = memrefType.getNumElements();
94 unsigned elementSizeInBits = memrefType.getElementTypeBitWidth();
95 return (numElements * elementSizeInBits + 7) / 8; // Round up to bytes
96}
97
98/// Build lifetime-annotated allocation descriptors from candidates.
99/// Returns the arena alignment (LCM of all individual alignments).
100/// Uses a single block scan (O(n+m)) instead of one scan per candidate.
101static int64_t buildAllocInfos(
104 // Build an op-index map with a single pass over the plan block.
106 Block *planBlock = nullptr;
107 if (!candidates.empty()) {
108 planBlock = candidates.front().alloc->getBlock();
109 int64_t idx = 0;
110 for (Operation &op : *planBlock)
111 opIndex[&op] = idx++;
112 }
113
114 int64_t arenaAlignment = 1;
115 for (auto &candidate : candidates) {
117 info.sizeInBytes = candidate.sizeInBytes;
118 info.alignment = candidate.alignment;
119 info.timeStart = opIndex.lookup(candidate.alloc.getOperation());
120 // Conservative: timeEnd = latest dealloc position among all potential
121 // deallocs. A dealloc may be nested (e.g. inside an scf.if body); its
122 // lifetime contribution is bounded by the enclosing op in the plan block.
123 int64_t timeEnd = info.timeStart;
124 for (memref::DeallocOp d : candidate.deallocs) {
125 Operation *anchor = planBlock->findAncestorOpInBlock(*d.getOperation());
126 timeEnd = std::max(timeEnd, opIndex.lookup(anchor));
127 }
128 info.timeEnd = timeEnd;
129 allocInfos.push_back(info);
130 arenaAlignment = std::lcm(arenaAlignment, candidate.alignment);
131 }
132 return arenaAlignment;
133}
134
135/// Collect alloc/dealloc groups eligible for arena placement.
136///
137/// Eligibility uses the shared `BufferViewFlowAnalysis` so that buffers flowing
138/// through `arith.select`, `scf.if`/`scf.for` results, `cf` branches, or view
139/// ops are handled uniformly. An allocation is eligible when:
140/// - it has a static shape (dynamic shapes are silently skipped), and
141/// - it lives directly in the function's entry block (allocs nested in a
142/// region are skipped for now), and
143/// - every dealloc that may free it is anchored in that same entry block --
144/// either directly, or nested inside an op of that block (e.g. an
145/// `scf.if` body), which conservatively bounds the lifetime.
146/// A dealloc that escapes the entry block entirely (e.g. lives in a sibling
147/// `cf` block) is reported as an error, as is an alloc with no dealloc.
148///
149/// To keep the rewrite safe, a dealloc is only accepted if *all* allocs it may
150/// free (per the reverse alias set) are themselves candidates in this block;
151/// otherwise erasing it during the rewrite could leak or double-free a buffer
152/// that is not managed by the arena.
153static LogicalResult
154collectCandidates(FunctionOpInterface funcOp,
155 const BufferViewFlowAnalysis &analysis,
156 llvm::Statistic &numSkipDynamic,
157 llvm::Statistic &numSkipNested, llvm::Statistic &numEligible,
159 // All candidates are planned relative to the function's entry block.
160 if (funcOp.getFunctionBody().empty())
161 return success();
162 Block *planBlock = &funcOp.getFunctionBody().front();
163
164 bool walkFailed = false;
165 funcOp->walk([&](memref::AllocOp allocOp) -> WalkResult {
166 MemRefType memrefType = allocOp.getType();
167 if (!memrefType.hasStaticShape()) {
168 ++numSkipDynamic;
169 return WalkResult::advance();
170 }
171
172 // Only plan allocs that live directly in the entry block. Allocs nested in
173 // a region (loop/conditional body) are skipped for now.
174 if (allocOp->getBlock() != planBlock) {
175 ++numSkipNested;
176 return WalkResult::advance();
177 }
178
180 collectDeallocs(allocOp.getResult(), analysis, deallocs);
181
182 if (deallocs.empty()) {
183 allocOp.emitError("no dealloc found; run the deallocation pipeline "
184 "before this pass");
185 walkFailed = true;
186 return WalkResult::interrupt();
187 }
188
189 for (memref::DeallocOp d : deallocs) {
190 // The dealloc must be anchored in the plan block (directly or via an
191 // enclosing op such as an scf.if). A dealloc in a sibling block escapes.
192 if (!planBlock->findAncestorOpInBlock(*d.getOperation())) {
193 allocOp.emitError("unstructured control flow is not supported");
194 walkFailed = true;
195 return WalkResult::interrupt();
196 }
197 // Every alloc that this dealloc may free must also be an entry-block
198 // candidate; otherwise erasing it during the rewrite is unsafe.
199 for (memref::AllocOp freed : findFreedAllocs(d, analysis)) {
200 if (freed->getBlock() != planBlock) {
201 ++numSkipNested;
202 return WalkResult::advance();
203 }
204 }
205 }
206
207 ++numEligible;
208 AllocationCandidate candidate;
209 candidate.alloc = allocOp;
210 candidate.deallocs = deallocs;
211 candidate.sizeInBytes = computeSizeInBytes(memrefType);
212 candidate.alignment = allocOp.getAlignment().value_or(1);
213 candidates.push_back(candidate);
214 return WalkResult::advance();
215 });
216
217 return failure(walkFailed);
218}
219
220/// Create or obtain the arena buffer based on the arena mode.
221/// Returns failure if the mode is invalid or preconditions aren't met.
222static FailureOr<Value> createArena(OpBuilder &builder,
223 FunctionOpInterface funcOp,
224 StringRef arenaMode, int64_t totalSize,
225 int64_t arenaAlignment) {
226 Location loc = funcOp->getLoc();
227
228 if (arenaMode == "allocate") {
229 auto arenaType = MemRefType::get({totalSize}, builder.getI8Type());
230 auto arenaAlloc =
231 memref::AllocOp::create(builder, loc, arenaType, ValueRange{},
232 builder.getI64IntegerAttr(arenaAlignment));
233 LLVM_DEBUG(llvm::dbgs()
234 << "[static-memory-planner] created arena via AllocOp: size="
235 << totalSize << " bytes, alignment=" << arenaAlignment
236 << " bytes\n");
237 return arenaAlloc.getResult();
238 }
239
240 if (arenaMode == "arg") {
241 if (funcOp.getNumArguments() == 0)
242 return funcOp->emitError(
243 "arena-mode=arg requires at least one function argument");
244
245 Value arenaValue = funcOp.getArgument(0);
246 auto arenaType = dyn_cast<MemRefType>(arenaValue.getType());
247 if (!arenaType || !arenaType.getElementType().isInteger(8) ||
248 arenaType.getRank() != 1)
249 return funcOp->emitError(
250 "arena-mode=arg requires first argument to be memref<...xi8>");
251
252 LLVM_DEBUG(llvm::dbgs()
253 << "[static-memory-planner] using arena from function arg 0\n");
254 return arenaValue;
255 }
256
257 return funcOp->emitError("invalid arena-mode: '" + arenaMode +
258 "' (must be 'allocate' or 'arg')");
259}
260
261/// Replace each alloc/dealloc pair with a memref.view into the arena.
262static void rewriteAllocations(MutableArrayRef<AllocationCandidate> candidates,
263 Value arenaValue) {
264 SmallPtrSet<Operation *, 8> deallocsToErase;
265 SmallVector<Operation *> allocsToErase;
266
267 // Replace all alloc results with views (rewires selects too).
268 for (auto &candidate : candidates) {
269 OpBuilder builder(candidate.alloc);
270 Location loc = candidate.alloc.getLoc();
271 MemRefType originalType = candidate.alloc.getType();
272
273 Value offsetIndex =
274 arith::ConstantIndexOp::create(builder, loc, candidate.offset);
275 auto view = memref::ViewOp::create(builder, loc, originalType, arenaValue,
276 offsetIndex, SmallVector<Value>{});
277 candidate.alloc.getResult().replaceAllUsesWith(view.getResult());
278 allocsToErase.push_back(candidate.alloc.getOperation());
279
280 for (memref::DeallocOp d : candidate.deallocs)
281 deallocsToErase.insert(d.getOperation());
282 }
283
284 // Erase deallocs first (they may reference alloc results via selects).
285 for (Operation *d : deallocsToErase)
286 d->erase();
287
288 // Erase allocs last (no users remain after replaceAllUsesWith).
289 for (Operation *allocOp : allocsToErase)
290 allocOp->erase();
291}
292
293//===----------------------------------------------------------------------===//
294// StaticMemoryPlannerAnalysisPass
295//===----------------------------------------------------------------------===//
296
297struct StaticMemoryPlannerAnalysisPass
298 : public bufferization::impl::StaticMemoryPlannerAnalysisPassBase<
299 StaticMemoryPlannerAnalysisPass> {
300public:
301 using Base = bufferization::impl::StaticMemoryPlannerAnalysisPassBase<
302 StaticMemoryPlannerAnalysisPass>;
303 using Base::Base;
304
305 void runOnOperation() override;
306};
307
308void StaticMemoryPlannerAnalysisPass::runOnOperation() {
309 auto funcOp = llvm::cast<FunctionOpInterface>(getOperation());
310
311 // Step 0: Check for memref return types (not supported)
312 for (Type resultType : funcOp.getResultTypes()) {
313 if (isa<BaseMemRefType>(resultType)) {
314 funcOp->emitError("static-memory-planner does not support functions "
315 "with memref return types");
316 return signalPassFailure();
317 }
318 }
319
320 // Step 1: Collect eligible allocation candidates. The buffer view-flow
321 // analysis models how buffers flow through selects, scf.if results, branches,
322 // and view ops so we can find deallocs and freed allocs uniformly.
323 BufferViewFlowAnalysis analysis(funcOp);
324 SmallVector<AllocationCandidate> candidates;
325 if (failed(collectCandidates(funcOp, analysis, numSkipDynamic, numSkipNested,
326 numEligible, candidates)))
327 return signalPassFailure();
328
329 if (candidates.empty())
330 return;
331
332 // Step 2: Build allocation descriptors with lifetime info.
333 SmallVector<bufferization::MemoryPlannerAlloc> allocInfos;
334 int64_t arenaAlignment = buildAllocInfos(candidates, allocInfos);
335
336 // Step 3: Run the planning algorithm.
337 SmallVector<int64_t> offsets;
338 switch (algorithm) {
339 case bufferization::MemoryPlannerAlgorithm::Trivial:
340 offsets = bufferization::trivialMemoryPlanner(arenaAlignment, allocInfos);
341 break;
342 case bufferization::MemoryPlannerAlgorithm::BestFit:
343 offsets = bufferization::bestFitMemoryPlanner(arenaAlignment, allocInfos);
344 break;
345 }
346
347 // Step 4: Compute total arena size and assign offsets.
348 int64_t totalSize = 0;
349 for (size_t i = 0; i < candidates.size(); ++i) {
350 candidates[i].offset = offsets[i];
351 totalSize = std::max(totalSize, offsets[i] + candidates[i].sizeInBytes);
352 LLVM_DEBUG(llvm::dbgs()
353 << "[static-memory-planner] offset=" << candidates[i].offset
354 << " size=" << candidates[i].sizeInBytes
355 << " alignment=" << candidates[i].alignment << "\n");
356 }
357
358 // Step 5: Obtain arena based on arena mode.
359 Operation *firstAlloc = candidates.front().alloc;
360 OpBuilder builder(firstAlloc);
361 FailureOr<Value> arenaValue =
362 createArena(builder, funcOp, arenaMode, totalSize, arenaAlignment);
363 if (failed(arenaValue))
364 return signalPassFailure();
365
366 // Step 6: Replace each alloc with memref.view into the arena.
367 rewriteAllocations(candidates, *arenaValue);
368}
369
370} // end anonymous namespace
return success()
Block represents an ordered list of Operations.
Definition Block.h:33
Operation * findAncestorOpInBlock(Operation &op)
Returns 'op' if 'op' lies in this block, or otherwise finds the ancestor operation of 'op' that lies ...
Definition Block.cpp:74
Operation & front()
Definition Block.h:177
A straight-forward alias analysis which ensures that all dependencies of all values will be determine...
IntegerAttr getI64IntegerAttr(int64_t value)
Definition Builders.cpp:120
IntegerType getI8Type()
Definition Builders.cpp:67
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
This class helps build Operations.
Definition Builders.h:210
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
std::enable_if_t< llvm::function_traits< std::decay_t< FnT > >::num_args==1, RetT > walk(FnT &&callback)
Walk the operation by calling the callback for each nested operation (including this one),...
Definition Operation.h:842
user_range getUsers()
Returns a range of all users.
Definition Operation.h:918
This class provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
Type getType() const
Return the type of this value.
Definition Value.h:105
A utility result that is used to signal how to proceed with an ongoing walk:
Definition WalkResult.h:29
static WalkResult advance()
Definition WalkResult.h:47
static WalkResult interrupt()
Definition WalkResult.h:46
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
Definition ArithOps.cpp:398
llvm::SmallVector< int64_t > trivialMemoryPlanner(int64_t arenaAlignment, llvm::ArrayRef< MemoryPlannerAlloc > allocs)
Sequential packing without lifetime overlap.
llvm::SmallVector< int64_t > bestFitMemoryPlanner(int64_t arenaAlignment, llvm::ArrayRef< MemoryPlannerAlloc > allocs)
Best-fit packing with lifetime-aware gap reuse.
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:717
detail::InFlightRemark analysis(Location loc, RemarkOpts opts)
Report an optimization analysis remark.
Definition Remarks.h:723
Include the generated interface declarations.
llvm::DenseMap< KeyT, ValueT, KeyInfoT, BucketT > DenseMap
Definition LLVM.h:120
Descriptor for a single allocation to be placed by the memory planner.