[Mlir-commits] [mlir] [mlir][bufferization] Add best-fit algorithm to static memory planner (PR #207403)
Javed Absar
llvmlistbot at llvm.org
Tue Jul 7 08:46:45 PDT 2026
https://github.com/javedabsar1 updated https://github.com/llvm/llvm-project/pull/207403
>From 63bad1fb8d1de9fd84c3adc391d0eb4b9f56427f Mon Sep 17 00:00:00 2001
From: mabsar <mabsar at qti.qualcommm.com>
Date: Fri, 3 Jul 2026 07:20:15 -0700
Subject: [PATCH] [mlir][bufferization] Add best-fit algorithm to static memory
planner
Introduces an algorithm selection option to the static memory planner pass
and add a best-fit algorithm that reuses memory from expired allocations
by finding the smallest suitable gap. The planning algorithms are factored
into a separate StaticMemoryPlanning.{h,cpp} to keep them independent of
MLIR IR and easily testable.
Signed-off-by: mabsar <mabsar at qti.qualcommm.com>
---
.../Bufferization/IR/BufferizationEnums.td | 8 +
.../Bufferization/Transforms/Passes.td | 31 +-
.../Transforms/StaticMemoryPlanning.h | 50 +++
.../Bufferization/Transforms/CMakeLists.txt | 1 +
.../StaticMemoryPlannerAnalysis.cpp | 344 +++++++++---------
.../Transforms/StaticMemoryPlanning.cpp | 131 +++++++
.../static-memory-planner-best-fit.mlir | 118 ++++++
7 files changed, 504 insertions(+), 179 deletions(-)
create mode 100644 mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h
create mode 100644 mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp
create mode 100644 mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir
diff --git a/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td b/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
index bafa84645e57b..a8cad26e9341b 100644
--- a/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
+++ b/mlir/include/mlir/Dialect/Bufferization/IR/BufferizationEnums.td
@@ -24,4 +24,12 @@ def LayoutMapOption : I32EnumAttr<"LayoutMapOption",
let cppNamespace = "::mlir::bufferization";
}
+def MemoryPlannerAlgorithm : I32EnumAttr<"MemoryPlannerAlgorithm",
+ "memory planning algorithm", [
+ I32EnumAttrCase<"Trivial", 0, "trivial">,
+ I32EnumAttrCase<"BestFit", 1, "best-fit">
+]> {
+ let cppNamespace = "::mlir::bufferization";
+}
+
#endif // BUFFERIZATION_ENUMS
diff --git a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
index c7f6cae571e40..8408315dda607 100644
--- a/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
+++ b/mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td
@@ -197,13 +197,11 @@ def StaticMemoryPlannerAnalysisPass
- Unique same-block `memref.dealloc`.
- Allocations in nested blocks are ignored for now.
- Eligible allocations are packed into a single arena. Currently, we use a
- trivial sequential allocation strategy with alignment padding. But the
- interface will also allow any packing algorithm to be plugged in as long as
- it respects the interface. The arena is an i8 byte buffer
- (`memref<Nxi8>`) that can hold allocations of different element types.
- Each original allocation is replaced with a `memref.view` operation that
- creates a typed view into the arena at the computed offset.
+ Eligible allocations are packed into a single arena using a configurable
+ planning algorithm (see the `algorithm` option). The arena is an i8 byte
+ buffer (`memref<Nxi8>`) that can hold allocations of different element
+ types. Each original allocation is replaced with a `memref.view` operation
+ that creates a typed view into the arena at the computed offset.
Ineligible allocations are skipped and retain their original
alloc/dealloc operations. Skip reasons are reported via op remarks.
@@ -234,11 +232,24 @@ def StaticMemoryPlannerAnalysisPass
```
}];
- let options = [Option<
- "arenaMode", "arena-mode", "std::string",
+ let options = [
+ Option<"arenaMode", "arena-mode", "std::string",
/*default=*/"\"allocate\"",
"Arena allocation mode: 'allocate' creates arena via AllocOp, "
- "'arg' extracts arena from function arguments">];
+ "'arg' extracts arena from function arguments">,
+ Option<"algorithm", "algorithm",
+ "::mlir::bufferization::MemoryPlannerAlgorithm",
+ /*default=*/"::mlir::bufferization::MemoryPlannerAlgorithm::Trivial",
+ "Memory planning algorithm to use.",
+ [{::llvm::cl::values(
+ clEnumValN(::mlir::bufferization::MemoryPlannerAlgorithm::Trivial,
+ "trivial",
+ "Sequential packing without lifetime overlap"),
+ clEnumValN(::mlir::bufferization::MemoryPlannerAlgorithm::BestFit,
+ "best-fit",
+ "Best-fit packing with lifetime-aware gap reuse")
+ )}]>,
+ ];
let statistics =
[Statistic<"numEligible", "num-eligible",
diff --git a/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h b/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h
new file mode 100644
index 0000000000000..626e384ce4642
--- /dev/null
+++ b/mlir/include/mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h
@@ -0,0 +1,50 @@
+//===- StaticMemoryPlanning.h - Memory planning algorithms ------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Pure memory planning algorithms for static arena allocation. These operate
+// on abstract allocation descriptors (size, alignment, lifetime) and produce
+// byte offsets. They are independent of MLIR IR.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
+#define MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallVector.h"
+#include <cstdint>
+
+namespace mlir {
+namespace bufferization {
+
+/// Descriptor for a single allocation to be placed by the memory planner.
+struct MemoryPlannerAlloc {
+ int64_t sizeInBytes = 0;
+ int64_t alignment = 1;
+ int64_t timeStart = 0; // Operation index when allocation becomes live
+ int64_t timeEnd = 0; // Operation index when allocation is freed
+};
+
+/// Sequential packing without lifetime overlap. Each allocation is placed
+/// immediately after the previous one (with alignment padding). Ignores
+/// lifetimes entirely.
+llvm::SmallVector<int64_t>
+trivialMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs);
+
+/// Best-fit packing with lifetime-aware gap reuse. Processes allocations in
+/// time order and places each one in the smallest gap left by expired
+/// allocations. Falls back to extending the arena if no gap fits.
+llvm::SmallVector<int64_t>
+bestFitMemoryPlanner(int64_t arenaAlignment,
+ llvm::ArrayRef<MemoryPlannerAlloc> allocs);
+
+} // namespace bufferization
+} // namespace mlir
+
+#endif // MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_STATICMEMORYPLANNING_H
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
index 5df9f19a5e30a..006fcd1ce0ec7 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
+++ b/mlir/lib/Dialect/Bufferization/Transforms/CMakeLists.txt
@@ -15,6 +15,7 @@ add_mlir_dialect_library(MLIRBufferizationTransforms
OwnershipBasedBufferDeallocation.cpp
TensorCopyInsertion.cpp
OptimizeAllocationLiveness.cpp
+ StaticMemoryPlanning.cpp
StaticMemoryPlannerAnalysis.cpp
ADDITIONAL_HEADER_DIRS
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
index 48b6e97c809ef..c2ac40a8427e7 100644
--- a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
+++ b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlannerAnalysis.cpp
@@ -7,18 +7,18 @@
//===----------------------------------------------------------------------===//
//
// Transforms memref.alloc/memref.dealloc pairs into a single arena allocation
-// with memref.view. Uses simple sequential offset assignment where each
-// allocation gets its own space without overlap (baseline algorithm).
+// with memref.view. Delegates offset computation to planning algorithms in
+// StaticMemoryPlanning.h.
//
//===----------------------------------------------------------------------===//
#include "mlir/Dialect/Arith/IR/Arith.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/Support/Debug.h"
-#include "llvm/Support/MathExtras.h"
#include <numeric>
#define DEBUG_TYPE "static-memory-planner"
@@ -34,19 +34,6 @@ using namespace mlir;
namespace {
-//===----------------------------------------------------------------------===//
-// Data structures
-//===----------------------------------------------------------------------===//
-
-/// Allocation info for memory planning (independent of MLIR).
-/// This can be used with pure planning algorithms.
-struct Alloc {
- int64_t sizeInBytes = 0; // Size in bytes
- int64_t alignment = 1; // Required alignment in bytes
- int64_t timeStart = 0; // Operation index when allocation starts
- int64_t timeEnd = 0; // Operation index when allocation ends (dealloc)
-};
-
/// A candidate allocation with its matching deallocation and assigned offset.
struct AllocationCandidate {
memref::AllocOp alloc;
@@ -81,36 +68,134 @@ static int64_t computeSizeInBytes(MemRefType memrefType) {
return (numElements * elementSizeInBits + 7) / 8; // Round up to bytes
}
-/// Align an offset to the specified alignment.
-/// Returns the smallest value >= offset that is a multiple of alignment.
-static int64_t alignOffset(int64_t offset, int64_t alignment) {
- return llvm::alignTo(offset, alignment);
+/// Build lifetime-annotated allocation descriptors from candidates.
+/// Returns the arena alignment (LCM of all individual alignments).
+static int64_t buildAllocInfos(
+ MutableArrayRef<AllocationCandidate> candidates,
+ SmallVectorImpl<bufferization::MemoryPlannerAlloc> &allocInfos) {
+ int64_t arenaAlignment = 1;
+ for (auto &candidate : candidates) {
+ bufferization::MemoryPlannerAlloc info;
+ info.sizeInBytes = candidate.sizeInBytes;
+ info.alignment = candidate.alignment;
+
+ Block *block = candidate.alloc->getBlock();
+ int64_t opIdx = 0;
+ for (Operation &op : *block) {
+ if (&op == candidate.alloc.getOperation())
+ info.timeStart = opIdx;
+ if (&op == candidate.dealloc.getOperation())
+ info.timeEnd = opIdx;
+ ++opIdx;
+ }
+
+ allocInfos.push_back(info);
+ arenaAlignment = std::lcm(arenaAlignment, candidate.alignment);
+ }
+ return arenaAlignment;
}
-//===----------------------------------------------------------------------===//
-// Memory Planning Algorithms
-//===----------------------------------------------------------------------===//
+/// Collect alloc/dealloc pairs eligible for arena placement.
+/// An allocation is eligible if it has a static shape and a unique dealloc
+/// in the same block.
+static SmallVector<AllocationCandidate>
+collectCandidates(FunctionOpInterface funcOp, llvm::Statistic &numSkipDynamic,
+ llvm::Statistic &numSkipNoDealloc,
+ llvm::Statistic &numEligible) {
+ SmallVector<AllocationCandidate> candidates;
+
+ funcOp->walk([&](memref::AllocOp allocOp) {
+ MemRefType memrefType = allocOp.getType();
+
+ // Skip dynamic shapes
+ if (!memrefType.hasStaticShape()) {
+ ++numSkipDynamic;
+ return;
+ }
-/// Simple sequential memory planner (baseline algorithm).
-/// arenaAlignment must be a multiple (LCM) of all alloc.alignment values.
-/// Allocates each buffer one after another with proper alignment padding.
-/// Returns offsets in bytes for each allocation.
-static SmallVector<int64_t> trivialMemoryPlanner(int64_t arenaAlignment,
- ArrayRef<Alloc> allocs) {
- SmallVector<int64_t> offsets;
- int64_t currentOffset = 0;
-
- for (const auto &alloc : allocs) {
- currentOffset = alignOffset(currentOffset, alloc.alignment);
-#ifndef NDEBUG
- assert((arenaAlignment + currentOffset) % alloc.alignment == 0 &&
- "invalid alignment");
-#endif
- offsets.push_back(currentOffset);
- currentOffset += alloc.sizeInBytes;
+ // Find unique dealloc in the same block
+ memref::DeallocOp deallocOp = findUniqueDealloc(allocOp.getResult());
+ if (!deallocOp) {
+ ++numSkipNoDealloc;
+ return;
+ }
+
+ if (deallocOp->getBlock() != allocOp->getBlock()) {
+ ++numSkipNoDealloc;
+ return;
+ }
+
+ // This allocation is eligible
+ ++numEligible;
+ AllocationCandidate candidate;
+ candidate.alloc = allocOp;
+ candidate.dealloc = deallocOp;
+ candidate.sizeInBytes = computeSizeInBytes(memrefType);
+ candidate.alignment = allocOp.getAlignment().value_or(1);
+ candidates.push_back(candidate);
+ });
+
+ return candidates;
+}
+
+/// 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 offsets;
+ 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) {
+ 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());
+ candidate.alloc.erase();
+ candidate.dealloc.erase();
+ }
}
//===----------------------------------------------------------------------===//
@@ -125,143 +210,64 @@ struct StaticMemoryPlannerAnalysisPass
StaticMemoryPlannerAnalysisPass>;
using Base::Base;
- void runOnOperation() override {
- 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
- SmallVector<AllocationCandidate> candidates;
-
- funcOp->walk([&](memref::AllocOp allocOp) {
- MemRefType memrefType = allocOp.getType();
-
- // Skip dynamic shapes
- if (!memrefType.hasStaticShape()) {
- ++numSkipDynamic;
- return;
- }
-
- // Find unique dealloc in the same block
- memref::DeallocOp deallocOp = findUniqueDealloc(allocOp.getResult());
- if (!deallocOp) {
- ++numSkipNoDealloc;
- return;
- }
-
- if (deallocOp->getBlock() != allocOp->getBlock()) {
- ++numSkipNoDealloc;
- return;
- }
-
- // This allocation is eligible
- ++numEligible;
- AllocationCandidate candidate;
- candidate.alloc = allocOp;
- candidate.dealloc = deallocOp;
- candidate.sizeInBytes = computeSizeInBytes(memrefType);
- candidate.alignment = allocOp.getAlignment().value_or(1);
- candidates.push_back(candidate);
- });
-
- if (candidates.empty())
- return;
-
- // Step 2: Prepare allocation info for planner
- SmallVector<Alloc> allocInfos;
- int64_t arenaAlignment = 1;
- for (const auto &candidate : candidates) {
- Alloc allocInfo;
- allocInfo.sizeInBytes = candidate.sizeInBytes;
- allocInfo.alignment = candidate.alignment;
- allocInfos.push_back(allocInfo);
- arenaAlignment = std::lcm(arenaAlignment, candidate.alignment);
- }
+ void runOnOperation() override;
+};
- // Step 3: Run the planning algorithm
- SmallVector<int64_t> offsets =
- trivialMemoryPlanner(arenaAlignment, allocInfos);
-
- // Assign computed offsets back to candidates
- 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");
- }
+void StaticMemoryPlannerAnalysisPass::runOnOperation() {
+ auto funcOp = llvm::cast<FunctionOpInterface>(getOperation());
- // Step 4: Obtain arena based on arena mode
- Operation *firstAlloc = candidates.front().alloc;
- OpBuilder builder(firstAlloc);
- Value arenaValue;
-
- if (arenaMode == "allocate") {
- // Arena is i8 byte buffer to support multiple data types (f32, i64, etc.)
- auto arenaType = MemRefType::get({totalSize}, builder.getI8Type());
- auto arenaAlloc = memref::AllocOp::create(
- builder, firstAlloc->getLoc(), arenaType, ValueRange{},
- builder.getI64IntegerAttr(arenaAlignment));
- arenaValue = arenaAlloc.getResult();
-
- LLVM_DEBUG(llvm::dbgs()
- << "[static-memory-planner] created arena via AllocOp: size="
- << totalSize << " bytes, alignment=" << arenaAlignment
- << " bytes\n");
- } else if (arenaMode == "arg") {
- if (funcOp.getNumArguments() == 0) {
- funcOp->emitError(
- "arena-mode=arg requires at least one function argument");
- return signalPassFailure();
- }
-
- arenaValue = funcOp.getArgument(0);
- auto arenaType = dyn_cast<MemRefType>(arenaValue.getType());
- if (!arenaType || !arenaType.getElementType().isInteger(8) ||
- arenaType.getRank() != 1) {
- funcOp->emitError(
- "arena-mode=arg requires first argument to be memref<...xi8>");
- return signalPassFailure();
- }
-
- LLVM_DEBUG(
- llvm::dbgs()
- << "[static-memory-planner] using arena from function arg 0\n");
- } else {
- funcOp->emitError("invalid arena-mode: '" + arenaMode +
- "' (must be 'allocate' or 'arg')");
+ // 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 5: Replace each alloc with memref.view directly on arena
- for (auto &candidate : candidates) {
- builder.setInsertionPoint(candidate.alloc);
- Location loc = candidate.alloc.getLoc();
+ // Step 1: Collect eligible allocation candidates.
+ SmallVector<AllocationCandidate> candidates =
+ collectCandidates(funcOp, numSkipDynamic, numSkipNoDealloc, numEligible);
- MemRefType originalType = candidate.alloc.getType();
+ if (candidates.empty())
+ return;
- // Create a constant for the byte offset into the arena
- Value offsetIndex =
- arith::ConstantIndexOp::create(builder, loc, candidate.offset);
+ // Step 2: Build allocation descriptors with lifetime info.
+ SmallVector<bufferization::MemoryPlannerAlloc> allocInfos;
+ int64_t arenaAlignment = buildAllocInfos(candidates, allocInfos);
- // Use memref.view to create a typed view into the i8 arena
- auto view = memref::ViewOp::create(builder, loc, originalType, arenaValue,
- offsetIndex, SmallVector<Value>{});
+ // 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;
+ }
- candidate.alloc.getResult().replaceAllUsesWith(view.getResult());
- candidate.alloc.erase();
- candidate.dealloc.erase();
- }
+ // 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
diff --git a/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp
new file mode 100644
index 0000000000000..b0c237507d2f3
--- /dev/null
+++ b/mlir/lib/Dialect/Bufferization/Transforms/StaticMemoryPlanning.cpp
@@ -0,0 +1,131 @@
+//===- StaticMemoryPlanning.cpp - Memory planning algorithms --------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Dialect/Bufferization/Transforms/StaticMemoryPlanning.h"
+#include "llvm/Support/MathExtras.h"
+#include <numeric>
+
+using namespace mlir::bufferization;
+
+/// Align an offset to the specified alignment.
+static int64_t alignOffset(int64_t offset, int64_t alignment) {
+ return llvm::alignTo(offset, alignment);
+}
+
+/// Trivial sequential packing: places each allocation immediately after the
+/// previous one with alignment padding. Does not consider lifetimes, so no
+/// memory is reused. This gives a simple upper bound on arena size.
+/// Complexity: O(n) where n is the number of allocations.
+llvm::SmallVector<int64_t> mlir::bufferization::trivialMemoryPlanner(
+ int64_t arenaAlignment, llvm::ArrayRef<MemoryPlannerAlloc> allocs) {
+ llvm::SmallVector<int64_t> offsets;
+ int64_t currentOffset = 0;
+
+ for (const auto &alloc : allocs) {
+ currentOffset = alignOffset(currentOffset, alloc.alignment);
+ assert((arenaAlignment + currentOffset) % alloc.alignment == 0 &&
+ "invalid alignment");
+ offsets.push_back(currentOffset);
+ currentOffset += alloc.sizeInBytes;
+ }
+
+ return offsets;
+}
+
+/// Best-fit lifetime-aware packing: processes allocations in start-time order
+/// and places each one in the smallest gap left by allocations whose lifetimes
+/// have ended. If no existing gap is large enough, the arena is extended.
+/// This minimizes peak memory usage when allocations have non-overlapping
+/// lifetimes.
+/// Complexity: O(n^2) where n is the number of allocations.
+llvm::SmallVector<int64_t> mlir::bufferization::bestFitMemoryPlanner(
+ int64_t arenaAlignment, llvm::ArrayRef<MemoryPlannerAlloc> allocs) {
+ // Tracks where each allocation was placed. We only need timeEnd because
+ // allocations are processed in timeStart order — by the time we place a new
+ // allocation, all earlier placements already started, so we only need to
+ // check which ones are still live.
+ struct Placement {
+ int64_t offset;
+ int64_t size;
+ int64_t timeEnd;
+ };
+
+ // Process allocations in order of start time.
+ llvm::SmallVector<unsigned> order(allocs.size());
+ std::iota(order.begin(), order.end(), 0);
+ llvm::sort(order, [&](unsigned a, unsigned b) {
+ return allocs[a].timeStart < allocs[b].timeStart;
+ });
+
+ llvm::SmallVector<Placement> placements;
+ llvm::SmallVector<int64_t> offsets(allocs.size(), 0);
+ int64_t arenaEnd = 0;
+
+ // Loop over all required allocations and fit them into best gaps.
+ for (unsigned idx : order) {
+ const MemoryPlannerAlloc &alloc = allocs[idx];
+
+ // Collect allocations that are still live at this alloc's start time.
+ // occupied is pairs of <offset_start, offset_end>.
+ llvm::SmallVector<std::pair<int64_t, int64_t>> occupied;
+ for (const auto &p : placements) {
+ if (p.timeEnd > alloc.timeStart)
+ occupied.push_back({p.offset, p.offset + p.size});
+ }
+ llvm::sort(occupied);
+
+ // Find the best (smallest) gap that fits this allocation.
+ int64_t bestOffset = -1;
+ int64_t bestGapSize = INT64_MAX;
+
+ int64_t gapStart = 0;
+ for (const auto &[occStart, occEnd] : occupied) {
+ int64_t alignedStart = alignOffset(gapStart, alloc.alignment);
+ if (alignedStart >= occStart) {
+ gapStart = std::max(gapStart, occEnd);
+ continue;
+ }
+ int64_t gapEnd = occStart;
+ int64_t gapSize = gapEnd - alignedStart;
+ // Gap is large enough to fit this allocation (after alignment).
+ if (gapSize >= alloc.sizeInBytes) {
+ // Track the smallest sufficient gap (best-fit strategy).
+ if (gapSize < bestGapSize) {
+ bestGapSize = gapSize;
+ bestOffset = alignedStart;
+ }
+ }
+ gapStart = std::max(gapStart, occEnd);
+ }
+
+ // Check the trailing gap (between last occupied region and arena end).
+ // This is only a reuse candidate if the allocation fits within the current
+ // arena bounds. If it doesn't fit, placing here would extend the arena —
+ // that case is handled by the fallback below (bestOffset < 0).
+ int64_t alignedTrailing = alignOffset(gapStart, alloc.alignment);
+ if (alignedTrailing + alloc.sizeInBytes <= arenaEnd) {
+ int64_t trailingSize = arenaEnd - alignedTrailing;
+ if (trailingSize < bestGapSize) {
+ bestGapSize = trailingSize;
+ bestOffset = alignedTrailing;
+ }
+ }
+
+ // If no existing gap worked, append at the end.
+ if (bestOffset < 0)
+ bestOffset = alignedTrailing;
+
+ assert((arenaAlignment + bestOffset) % alloc.alignment == 0 &&
+ "invalid alignment");
+ offsets[idx] = bestOffset;
+ placements.push_back({bestOffset, alloc.sizeInBytes, alloc.timeEnd});
+ arenaEnd = std::max(arenaEnd, bestOffset + alloc.sizeInBytes);
+ }
+
+ return offsets;
+}
diff --git a/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir
new file mode 100644
index 0000000000000..6c261880140a5
--- /dev/null
+++ b/mlir/test/Dialect/Bufferization/Transforms/static-memory-planner-best-fit.mlir
@@ -0,0 +1,118 @@
+// RUN: mlir-opt %s -pass-pipeline="builtin.module(func.func(static-memory-planner-analysis{algorithm=best-fit}))" \
+// RUN: -split-input-file | FileCheck %s
+
+// -----
+
+// Test 1: Non-overlapping lifetimes reuse the same memory.
+// With trivial packing this would be 8192 bytes; best-fit reuses the space.
+// CHECK-LABEL: func @reuse_non_overlapping
+func.func @reuse_non_overlapping() {
+ // Arena should be 4096 bytes (1024 * 4), not 8192.
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<4096xi8>
+ // First allocation at offset 0
+ // CHECK-NEXT: %[[C0_0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0_0]]][] : memref<4096xi8> to memref<1024xf32>
+ // Second allocation also at offset 0 (reuses freed space)
+ // CHECK-NEXT: %[[C0_1:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0_1]]][] : memref<4096xi8> to memref<1024xf32>
+ %0 = memref.alloc() : memref<1024xf32>
+ memref.dealloc %0 : memref<1024xf32>
+ %1 = memref.alloc() : memref<1024xf32>
+ memref.dealloc %1 : memref<1024xf32>
+ return
+}
+
+// -----
+
+// Test 2: Overlapping lifetimes cannot reuse memory.
+// CHECK-LABEL: func @no_reuse_overlapping
+func.func @no_reuse_overlapping() {
+ // Both are live at the same time, so arena = 4096 + 2048 = 6144 bytes.
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<6144xi8>
+ // CHECK-NEXT: %[[C0:.*]] = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C0]]][] : memref<6144xi8> to memref<1024xf32>
+ // CHECK-NEXT: %[[C4096:.*]] = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]][%[[C4096]]][] : memref<6144xi8> to memref<512xf32>
+ %0 = memref.alloc() : memref<1024xf32>
+ %1 = memref.alloc() : memref<512xf32>
+ memref.dealloc %0 : memref<1024xf32>
+ memref.dealloc %1 : memref<512xf32>
+ return
+}
+
+// -----
+
+// Test 3: Best-fit picks the smallest suitable gap.
+// Layout: A(4096) at 0, B(1024) at 4096, C(4096) at 5120, D(1024) at 9216.
+// B and D are freed while A and C are still live, creating two gaps:
+// [4096, 5120) = 1024 bytes (B's slot)
+// [9216, 10240) = 1024 bytes (D's slot)
+// Then we free A, creating gap [0, 4096) = 4096 bytes.
+// Now allocate E(512 bytes). Gaps: [0,4096)=4096, [4096,5120)=1024, [9216,10240)=1024.
+// Best-fit should pick one of the 1024-byte gaps (smallest fit for 512).
+// CHECK-LABEL: func @best_fit_smallest_gap
+func.func @best_fit_smallest_gap() {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 1 : i64} : memref<10240xi8>
+ // A at offset 0
+ // CHECK-NEXT: %{{.*}} = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ // B at offset 4096
+ // CHECK-NEXT: %{{.*}} = arith.constant 4096 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ // C at offset 5120
+ // CHECK-NEXT: %{{.*}} = arith.constant 5120 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ // D at offset 9216
+ // CHECK-NEXT: %{{.*}} = arith.constant 9216 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ // E at offset 9216 (best-fit picks 1024-byte trailing gap over 5120-byte gap)
+ // CHECK-NEXT: %{{.*}} = arith.constant 9216 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]
+ %a = memref.alloc() : memref<1024xf32>
+ %b = memref.alloc() : memref<256xf32>
+ %c = memref.alloc() : memref<1024xf32>
+ %d = memref.alloc() : memref<256xf32>
+ memref.dealloc %b : memref<256xf32>
+ memref.dealloc %d : memref<256xf32>
+ memref.dealloc %a : memref<1024xf32>
+ %e = memref.alloc() : memref<128xf32>
+ memref.dealloc %c : memref<1024xf32>
+ memref.dealloc %e : memref<128xf32>
+ return
+}
+
+// -----
+
+// Test 4: Alignment padding can disqualify a gap.
+// A(128, align 128) at 0 (live), B(56, align 1) at 128, C(128, align 128) at 256 (live).
+// B is freed => gap [128, 256) = 128 bytes.
+// D(64, align 128): aligned start = 128, fits in gap.
+// E(64, align 256): next 256-aligned offset in [128,256) is 256 = gap end, doesn't fit.
+// E must go past the arena high-water mark.
+// CHECK-LABEL: func @best_fit_alignment
+func.func @best_fit_alignment() {
+ // CHECK: %[[ARENA:.*]] = memref.alloc() {alignment = 256 : i64} : memref<576xi8>
+ // CHECK-NEXT: %{{.*}} = arith.constant 0 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]{{.*}} to memref<128xi8>
+ // CHECK-NEXT: %{{.*}} = arith.constant 128 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]{{.*}} to memref<56xi8>
+ // CHECK-NEXT: %{{.*}} = arith.constant 256 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]{{.*}} to memref<128xi8>
+ // D fits in gap at offset 128
+ // CHECK-NEXT: %{{.*}} = arith.constant 128 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]{{.*}} to memref<64xi8>
+ // E cannot fit in gap (alignment 256), placed at offset 512
+ // CHECK-NEXT: %{{.*}} = arith.constant 512 : index
+ // CHECK-NEXT: %{{.*}} = memref.view %[[ARENA]]{{.*}} to memref<64xi8>
+ %a = memref.alloc() {alignment = 128 : i64} : memref<128xi8>
+ %b = memref.alloc() : memref<56xi8>
+ %c = memref.alloc() {alignment = 128 : i64} : memref<128xi8>
+ memref.dealloc %b : memref<56xi8>
+ %d = memref.alloc() {alignment = 128 : i64} : memref<64xi8>
+ %e = memref.alloc() {alignment = 256 : i64} : memref<64xi8>
+ memref.dealloc %a : memref<128xi8>
+ memref.dealloc %c : memref<128xi8>
+ memref.dealloc %d : memref<64xi8>
+ memref.dealloc %e : memref<64xi8>
+ return
+}
More information about the Mlir-commits
mailing list