| //===- StaticMemoryPlannerAnalysis.cpp - Static memory planning -----------===// |
| // |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Transforms memref.alloc/memref.dealloc pairs into a single arena allocation |
| // with memref.view. Delegates offset computation to planning algorithms in |
| // StaticMemoryPlanning.h. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "mlir/Dialect/Arith/IR/Arith.h" |
| #include "mlir/Dialect/Bufferization/Transforms/BufferViewFlowAnalysis.h" |
| #include "mlir/Dialect/Bufferization/Transforms/Passes.h" |
| #include "mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h" |
| #include "mlir/Dialect/MemRef/IR/MemRef.h" |
| #include "mlir/IR/Builders.h" |
| #include "mlir/Interfaces/FunctionInterfaces.h" |
| #include "llvm/ADT/SmallPtrSet.h" |
| #include "llvm/Support/Debug.h" |
| #include <numeric> |
| |
| #define DEBUG_TYPE "static-memory-planner" |
| |
| namespace mlir { |
| namespace bufferization { |
| #define GEN_PASS_DEF_STATICMEMORYPLANNERANALYSISPASS |
| #include "mlir/Dialect/Bufferization/Transforms/Passes.h.inc" |
| } // namespace bufferization |
| } // namespace mlir |
| |
| using namespace mlir; |
| |
| namespace { |
| |
| /// A candidate allocation with its matching deallocation(s) and assigned |
| /// offset. An alloc may be freed indirectly through arith.select chains, |
| /// yielding multiple potential deallocs — all must be in the same block. |
| struct AllocationCandidate { |
| memref::AllocOp alloc; |
| SmallVector<memref::DeallocOp> deallocs; |
| int64_t offset = 0; // Offset in bytes from arena start (assigned by planner) |
| int64_t sizeInBytes = 0; // Size in bytes |
| int64_t alignment = 1; // Required alignment in bytes |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // Helper utilities |
| //===----------------------------------------------------------------------===// |
| |
| /// Collect all dealloc ops that might free the given alloc value. Instead of a |
| /// bespoke traversal, this uses the shared `BufferViewFlowAnalysis`, which |
| /// already models all the ways a buffer can flow to a dealloc: |
| /// - `arith.select` (via BufferViewFlowOpInterface) |
| /// - `scf.if`/`scf.for` (via RegionBranchOpInterface region/result wiring) |
| /// - `cf.br`/`cf.cond_br` (via BranchOpInterface block arguments) |
| /// - `memref.view`/subview (via ViewLikeOpInterface) |
| /// `analysis.resolve(alloc)` returns the forward alias set (the alloc plus |
| /// every value it may flow into); a dealloc on any of those aliases frees the |
| /// alloc. For example: |
| /// %0 = memref.alloc() |
| /// %2 = arith.select %c, %0, %1 |
| /// memref.dealloc %2 <- covers %0 conditionally (via alias set) |
| /// %3 = scf.if %c { yield %0 } else { yield %1 } |
| /// memref.dealloc %3 <- also covers %0 conditionally |
| static void collectDeallocs(Value alloc, const BufferViewFlowAnalysis &analysis, |
| SmallVectorImpl<memref::DeallocOp> &deallocs) { |
| for (Value alias : analysis.resolve(alloc)) |
| for (Operation *user : alias.getUsers()) |
| if (auto dealloc = dyn_cast<memref::DeallocOp>(user)) |
| deallocs.push_back(dealloc); |
| } |
| |
| /// Return the set of allocation ops whose buffer may be freed by `dealloc`, |
| /// i.e. the terminal `memref.alloc` sources that flow into the dealloc operand. |
| /// Uses the reverse alias set so that a dealloc reached through a `scf.if` |
| /// result or `arith.select` is attributed to every alloc it may free. |
| static SmallVector<memref::AllocOp> |
| findFreedAllocs(memref::DeallocOp dealloc, |
| const BufferViewFlowAnalysis &analysis) { |
| SmallVector<memref::AllocOp> allocs; |
| for (Value source : analysis.resolveReverse(dealloc.getMemref())) |
| if (auto allocOp = source.getDefiningOp<memref::AllocOp>()) |
| allocs.push_back(allocOp); |
| return allocs; |
| } |
| |
| /// Compute the size in bytes for a memref type. |
| static int64_t computeSizeInBytes(MemRefType memrefType) { |
| int64_t numElements = memrefType.getNumElements(); |
| unsigned elementSizeInBits = memrefType.getElementTypeBitWidth(); |
| return (numElements * elementSizeInBits + 7) / 8; // Round up to bytes |
| } |
| |
| /// Build lifetime-annotated allocation descriptors from candidates. |
| /// Returns the arena alignment (LCM of all individual alignments). |
| /// Uses a single block scan (O(n+m)) instead of one scan per candidate. |
| static int64_t buildAllocInfos( |
| MutableArrayRef<AllocationCandidate> candidates, |
| SmallVectorImpl<bufferization::MemoryPlannerAlloc> &allocInfos) { |
| // Build an op-index map with a single pass over the plan block. |
| DenseMap<Operation *, int64_t> opIndex; |
| Block *planBlock = nullptr; |
| if (!candidates.empty()) { |
| planBlock = candidates.front().alloc->getBlock(); |
| int64_t idx = 0; |
| for (Operation &op : *planBlock) |
| opIndex[&op] = idx++; |
| } |
| |
| int64_t arenaAlignment = 1; |
| for (auto &candidate : candidates) { |
| bufferization::MemoryPlannerAlloc info; |
| info.sizeInBytes = candidate.sizeInBytes; |
| info.alignment = candidate.alignment; |
| info.timeStart = opIndex.lookup(candidate.alloc.getOperation()); |
| // Conservative: timeEnd = latest dealloc position among all potential |
| // deallocs. A dealloc may be nested (e.g. inside an scf.if body); its |
| // lifetime contribution is bounded by the enclosing op in the plan block. |
| int64_t timeEnd = info.timeStart; |
| for (memref::DeallocOp d : candidate.deallocs) { |
| Operation *anchor = planBlock->findAncestorOpInBlock(*d.getOperation()); |
| timeEnd = std::max(timeEnd, opIndex.lookup(anchor)); |
| } |
| info.timeEnd = timeEnd; |
| allocInfos.push_back(info); |
| arenaAlignment = std::lcm(arenaAlignment, candidate.alignment); |
| } |
| return arenaAlignment; |
| } |
| |
| /// Collect alloc/dealloc groups eligible for arena placement. |
| /// |
| /// Eligibility uses the shared `BufferViewFlowAnalysis` so that buffers flowing |
| /// through `arith.select`, `scf.if`/`scf.for` results, `cf` branches, or view |
| /// ops are handled uniformly. An allocation is eligible when: |
| /// - it has a static shape (dynamic shapes are silently skipped), and |
| /// - it lives directly in the function's entry block (allocs nested in a |
| /// region are skipped for now), and |
| /// - every dealloc that may free it is anchored in that same entry block -- |
| /// either directly, or nested inside an op of that block (e.g. an |
| /// `scf.if` body), which conservatively bounds the lifetime. |
| /// A dealloc that escapes the entry block entirely (e.g. lives in a sibling |
| /// `cf` block) is reported as an error, as is an alloc with no dealloc. |
| /// |
| /// To keep the rewrite safe, a dealloc is only accepted if *all* allocs it may |
| /// free (per the reverse alias set) are themselves candidates in this block; |
| /// otherwise erasing it during the rewrite could leak or double-free a buffer |
| /// that is not managed by the arena. |
| static LogicalResult |
| collectCandidates(FunctionOpInterface funcOp, |
| const BufferViewFlowAnalysis &analysis, |
| llvm::Statistic &numSkipDynamic, |
| llvm::Statistic &numSkipNested, llvm::Statistic &numEligible, |
| SmallVector<AllocationCandidate> &candidates) { |
| // All candidates are planned relative to the function's entry block. |
| if (funcOp.getFunctionBody().empty()) |
| return success(); |
| Block *planBlock = &funcOp.getFunctionBody().front(); |
| |
| bool walkFailed = false; |
| funcOp->walk([&](memref::AllocOp allocOp) -> WalkResult { |
| MemRefType memrefType = allocOp.getType(); |
| if (!memrefType.hasStaticShape()) { |
| ++numSkipDynamic; |
| return WalkResult::advance(); |
| } |
| |
| // Only plan allocs that live directly in the entry block. Allocs nested in |
| // a region (loop/conditional body) are skipped for now. |
| if (allocOp->getBlock() != planBlock) { |
| ++numSkipNested; |
| return WalkResult::advance(); |
| } |
| |
| SmallVector<memref::DeallocOp> deallocs; |
| collectDeallocs(allocOp.getResult(), analysis, deallocs); |
| |
| if (deallocs.empty()) { |
| allocOp.emitError("no dealloc found; run the deallocation pipeline " |
| "before this pass"); |
| walkFailed = true; |
| return WalkResult::interrupt(); |
| } |
| |
| for (memref::DeallocOp d : deallocs) { |
| // The dealloc must be anchored in the plan block (directly or via an |
| // enclosing op such as an scf.if). A dealloc in a sibling block escapes. |
| if (!planBlock->findAncestorOpInBlock(*d.getOperation())) { |
| allocOp.emitError("unstructured control flow is not supported"); |
| walkFailed = true; |
| return WalkResult::interrupt(); |
| } |
| // Every alloc that this dealloc may free must also be an entry-block |
| // candidate; otherwise erasing it during the rewrite is unsafe. |
| for (memref::AllocOp freed : findFreedAllocs(d, analysis)) { |
| if (freed->getBlock() != planBlock) { |
| ++numSkipNested; |
| return WalkResult::advance(); |
| } |
| } |
| } |
| |
| ++numEligible; |
| AllocationCandidate candidate; |
| candidate.alloc = allocOp; |
| candidate.deallocs = deallocs; |
| candidate.sizeInBytes = computeSizeInBytes(memrefType); |
| candidate.alignment = allocOp.getAlignment().value_or(1); |
| candidates.push_back(candidate); |
| return WalkResult::advance(); |
| }); |
| |
| return failure(walkFailed); |
| } |
| |
| /// Create or obtain the arena buffer based on the arena mode. |
| /// Returns failure if the mode is invalid or preconditions aren't met. |
| static FailureOr<Value> createArena(OpBuilder &builder, |
| FunctionOpInterface funcOp, |
| StringRef arenaMode, int64_t totalSize, |
| int64_t arenaAlignment) { |
| Location loc = funcOp->getLoc(); |
| |
| if (arenaMode == "allocate") { |
| auto arenaType = MemRefType::get({totalSize}, builder.getI8Type()); |
| auto arenaAlloc = |
| memref::AllocOp::create(builder, loc, arenaType, ValueRange{}, |
| builder.getI64IntegerAttr(arenaAlignment)); |
| LLVM_DEBUG(llvm::dbgs() |
| << "[static-memory-planner] created arena via AllocOp: size=" |
| << totalSize << " bytes, alignment=" << arenaAlignment |
| << " bytes\n"); |
| return arenaAlloc.getResult(); |
| } |
| |
| if (arenaMode == "arg") { |
| if (funcOp.getNumArguments() == 0) |
| return funcOp->emitError( |
| "arena-mode=arg requires at least one function argument"); |
| |
| Value arenaValue = funcOp.getArgument(0); |
| auto arenaType = dyn_cast<MemRefType>(arenaValue.getType()); |
| if (!arenaType || !arenaType.getElementType().isInteger(8) || |
| arenaType.getRank() != 1) |
| return funcOp->emitError( |
| "arena-mode=arg requires first argument to be memref<...xi8>"); |
| |
| LLVM_DEBUG(llvm::dbgs() |
| << "[static-memory-planner] using arena from function arg 0\n"); |
| return arenaValue; |
| } |
| |
| return funcOp->emitError("invalid arena-mode: '" + arenaMode + |
| "' (must be 'allocate' or 'arg')"); |
| } |
| |
| /// Replace each alloc/dealloc pair with a memref.view into the arena. |
| static void rewriteAllocations(MutableArrayRef<AllocationCandidate> candidates, |
| Value arenaValue) { |
| SmallPtrSet<Operation *, 8> deallocsToErase; |
| SmallVector<Operation *> allocsToErase; |
| |
| // Replace all alloc results with views (rewires selects too). |
| for (auto &candidate : candidates) { |
| OpBuilder builder(candidate.alloc); |
| Location loc = candidate.alloc.getLoc(); |
| MemRefType originalType = candidate.alloc.getType(); |
| |
| Value offsetIndex = |
| arith::ConstantIndexOp::create(builder, loc, candidate.offset); |
| auto view = memref::ViewOp::create(builder, loc, originalType, arenaValue, |
| offsetIndex, SmallVector<Value>{}); |
| candidate.alloc.getResult().replaceAllUsesWith(view.getResult()); |
| allocsToErase.push_back(candidate.alloc.getOperation()); |
| |
| for (memref::DeallocOp d : candidate.deallocs) |
| deallocsToErase.insert(d.getOperation()); |
| } |
| |
| // Erase deallocs first (they may reference alloc results via selects). |
| for (Operation *d : deallocsToErase) |
| d->erase(); |
| |
| // Erase allocs last (no users remain after replaceAllUsesWith). |
| for (Operation *allocOp : allocsToErase) |
| allocOp->erase(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // StaticMemoryPlannerAnalysisPass |
| //===----------------------------------------------------------------------===// |
| |
| struct StaticMemoryPlannerAnalysisPass |
| : public bufferization::impl::StaticMemoryPlannerAnalysisPassBase< |
| StaticMemoryPlannerAnalysisPass> { |
| public: |
| using Base = bufferization::impl::StaticMemoryPlannerAnalysisPassBase< |
| StaticMemoryPlannerAnalysisPass>; |
| using Base::Base; |
| |
| void runOnOperation() override; |
| }; |
| |
| void StaticMemoryPlannerAnalysisPass::runOnOperation() { |
| auto funcOp = llvm::cast<FunctionOpInterface>(getOperation()); |
| |
| // Step 0: Check for memref return types (not supported) |
| for (Type resultType : funcOp.getResultTypes()) { |
| if (isa<BaseMemRefType>(resultType)) { |
| funcOp->emitError("static-memory-planner does not support functions " |
| "with memref return types"); |
| return signalPassFailure(); |
| } |
| } |
| |
| // Step 1: Collect eligible allocation candidates. The buffer view-flow |
| // analysis models how buffers flow through selects, scf.if results, branches, |
| // and view ops so we can find deallocs and freed allocs uniformly. |
| BufferViewFlowAnalysis analysis(funcOp); |
| SmallVector<AllocationCandidate> candidates; |
| if (failed(collectCandidates(funcOp, analysis, numSkipDynamic, numSkipNested, |
| numEligible, candidates))) |
| return signalPassFailure(); |
| |
| if (candidates.empty()) |
| return; |
| |
| // Step 2: Build allocation descriptors with lifetime info. |
| SmallVector<bufferization::MemoryPlannerAlloc> allocInfos; |
| int64_t arenaAlignment = buildAllocInfos(candidates, allocInfos); |
| |
| // Step 3: Run the planning algorithm. |
| SmallVector<int64_t> offsets; |
| switch (algorithm) { |
| case bufferization::MemoryPlannerAlgorithm::Trivial: |
| offsets = bufferization::trivialMemoryPlanner(arenaAlignment, allocInfos); |
| break; |
| case bufferization::MemoryPlannerAlgorithm::BestFit: |
| offsets = bufferization::bestFitMemoryPlanner(arenaAlignment, allocInfos); |
| break; |
| } |
| |
| // Step 4: Compute total arena size and assign offsets. |
| int64_t totalSize = 0; |
| for (size_t i = 0; i < candidates.size(); ++i) { |
| candidates[i].offset = offsets[i]; |
| totalSize = std::max(totalSize, offsets[i] + candidates[i].sizeInBytes); |
| LLVM_DEBUG(llvm::dbgs() |
| << "[static-memory-planner] offset=" << candidates[i].offset |
| << " size=" << candidates[i].sizeInBytes |
| << " alignment=" << candidates[i].alignment << "\n"); |
| } |
| |
| // Step 5: Obtain arena based on arena mode. |
| Operation *firstAlloc = candidates.front().alloc; |
| OpBuilder builder(firstAlloc); |
| FailureOr<Value> arenaValue = |
| createArena(builder, funcOp, arenaMode, totalSize, arenaAlignment); |
| if (failed(arenaValue)) |
| return signalPassFailure(); |
| |
| // Step 6: Replace each alloc with memref.view into the arena. |
| rewriteAllocations(candidates, *arenaValue); |
| } |
| |
| } // end anonymous namespace |