blob: 6d5a9f0aba6a96b6610b301a13aa896be18a0c02 [file]
//===- 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