unify memory opimization passes
Validate Operations / validate-operations (push) Has been cancelled

better reports
cleanups
This commit is contained in:
NiccoloN
2026-07-21 17:26:01 +02:00
parent 3e468b58c8
commit f47fcebb83
31 changed files with 693 additions and 1443 deletions
@@ -1,20 +0,0 @@
# Parallelism and Runtime Invariant
Compile-time optimization must not trade away execution parallelism or worsen
theoretical runtime behavior. The absence of a precise runtime measurement does
not make such a trade acceptable.
In particular, a compile-time optimization must not:
- serialize work that can execute independently;
- coarsen lane, core, batch, or scheduling granularity in a way that reduces
available execution parallelism;
- increase the theoretical runtime critical path, instruction count, memory
traffic, synchronization, or required copies merely to reduce compiler work;
- merge independently scheduled work when doing so may reduce concurrency.
Compiler representations may share analysis, planning, or code-generation
work only when the emitted execution retains the same independent work,
schedule semantics, and available parallelism. If runtime impact cannot be
measured precisely, it must be established from the transformation's semantics;
uncertainty is not permission to accept a possible runtime regression.
@@ -11,7 +11,6 @@ This invariant applies to:
- deferred communication; - deferred communication;
- Spatial-to-PIM lowering; - Spatial-to-PIM lowering;
- bufferization; - bufferization;
- memory coalescing;
- liveness planning; - liveness planning;
- PIM code generation. - PIM code generation.
@@ -19,12 +18,17 @@ This invariant applies to:
A compiler compile-time or memory optimization must not reduce available A compiler compile-time or memory optimization must not reduce available
hardware parallelism or worsen theoretical execution time. In the absence of a hardware parallelism or worsen theoretical execution time. In the absence of a
precise runtime model, it is forbidden to serialize independent work, reduce precise runtime model, uncertainty is not permission to accept a possible
runtime regression. It is forbidden to serialize independent work, reduce
the number of simultaneously schedulable compute instances, replace parallel the number of simultaneously schedulable compute instances, replace parallel
graph or core batches with sequential loops, introduce recomputation, add graph or core batches with sequential loops, introduce recomputation, add
runtime copies, increase communication or instruction count, or lengthen the runtime copies, increase communication or instruction count, or lengthen the
schedule critical path merely to reduce compiler time or memory usage. schedule critical path merely to reduce compiler time or memory usage.
Compiler representations may share analysis, planning, or code-generation
work only when the emitted execution retains the same independent work,
schedule semantics, scheduling granularity, and available parallelism.
An optimization is acceptable only when its static runtime proxies are equal or An optimization is acceptable only when its static runtime proxies are equal or
better. better.
@@ -33,6 +37,8 @@ better.
Do not: Do not:
- use fewer parallel lanes or cores to simplify IR; - use fewer parallel lanes or cores to simplify IR;
- coarsen lane, core, batch, or scheduling granularity when it may reduce
concurrency;
- scalarize a valid graph or core batch; - scalarize a valid graph or core batch;
- replace independent operations with one sequential loop; - replace independent operations with one sequential loop;
- recompute values to reduce storage; - recompute values to reduce storage;
+1
View File
@@ -14,6 +14,7 @@ Before modifying the relevant subsystem, read:
* Use the debug build only when it is useful to obtain a clear stack trace with symbols, inspect names, place breakpoints, or test a small case interactively * Use the debug build only when it is useful to obtain a clear stack trace with symbols, inspect names, place breakpoints, or test a small case interactively
* The debug build is very slow, so use it only on small fast tests such as operation validations, not on network validations * The debug build is very slow, so use it only on small fast tests such as operation validations, not on network validations
* Always prepend rtk to shell commands if missing and if rtk is available * Always prepend rtk to shell commands if missing and if rtk is available
* Always use the repository Python virtual environment (`.venv/bin/python` and its bundled tools) for Python commands
# Core engineering philosophy # Core engineering philosophy
+30 -17
View File
@@ -67,14 +67,11 @@ ONNX-MLIR -> Spatial -> Pim (tensor) -> Pim (bufferized) -> PIM artifacts
Converts tensor-semantics PIM IR into memref-semantics PIM IR using MLIR's Converts tensor-semantics PIM IR into memref-semantics PIM IR using MLIR's
bufferization interfaces. bufferization interfaces.
5. **Safe IR memory coalescing** 5. **PIM local-memory planning**
(`src/PIM/Dialect/Pim/Transforms/MemoryCoalescing`).
Normalizes compatible block-local allocations before final planning.
6. **PIM local-memory planning**
(`src/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning`). (`src/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning`).
Computes whole-core lifetimes, coalesces non-overlapping allocations into Computes whole-core lifetimes, reuses addresses for non-overlapping
physical slots, assigns addresses, and records the explicit plan in PIM IR. allocations, and records the explicit plan in PIM IR.
7. **PIM verification and code generation** (`src/PIM/Pass/PimCodegen` and 6. **PIM verification and code generation** (`src/PIM/Pass/PimCodegen` and
`src/PIM/Compiler`). `src/PIM/Compiler`).
Verifies the memory plan and other PIM invariants, then emits `.pim` core Verifies the memory plan and other PIM invariants, then emits `.pim` core
files, weights, and `memory.bin` / `config.json` without rerunning liveness. files, weights, and `memory.bin` / `config.json` without rerunning liveness.
@@ -90,9 +87,10 @@ Supporting pieces:
- `src/PIM/Pass` - pass registration and auxiliary passes. - `src/PIM/Pass` - pass registration and auxiliary passes.
- `src/PIM/PimAccelerator.{cpp,hpp}` - ONNX-MLIR accelerator entry point. - `src/PIM/PimAccelerator.{cpp,hpp}` - ONNX-MLIR accelerator entry point.
## Key compiler options ## PIM compiler options
Pass these to `onnx-mlir` when compiling for PIM: Pass these to `onnx-mlir` when compiling for PIM. These are all Raptor/PIM-specific
options; `onnx-mlir --help` lists the inherited ONNX-MLIR options.
- `--maccel=PIM` - select the PIM accelerator. - `--maccel=PIM` - select the PIM accelerator.
- `--EmitSpatial`, `--EmitPim`, `--EmitPimBufferized`, - `--EmitSpatial`, `--EmitPim`, `--EmitPimBufferized`,
@@ -101,14 +99,30 @@ Pass these to `onnx-mlir` when compiling for PIM:
- `--core-count=<N>` - required positive core count for PIM compilation. - `--core-count=<N>` - required positive core count for PIM compilation.
- `--crossbar-size=<N>` - crossbar width/height. Default in code is `128`. - `--crossbar-size=<N>` - crossbar width/height. Default in code is `128`.
- `--crossbar-count=<N>` - crossbars per core. Default in code is `64`. - `--crossbar-count=<N>` - crossbars per core. Default in code is `64`.
- `--pim-memory-report=<summary|none>` - emit the concise combined memory report
under `reports/memory_report.txt`, or disable it. Default is `summary`.
- `--pim-only-codegen` - assume input is already bufferized PIM IR and only run - `--pim-only-codegen` - assume input is already bufferized PIM IR and only run
the codegen tail. the codegen tail.
- `--pim-emit-json` - also emit `core_*.json` instruction files alongside - `--pim-emit-json` - also emit `core_*.json` instruction files alongside
`core_*.pim`. `core_*.pim`.
- `--pim-export-spatial-dataflow=<none|spatial1|spatial2|spatial3|spatial4|all>` - control Spatial - `--pim-export-spatial-dataflow=<none|spatial1|spatial2|spatial3|spatial4|all>` -
dataflow CSV reports for the graph, trivially merged graph, scheduled, and control Spatial dataflow CSV reports for the graph, trivially merged graph,
realized snapshots under `reports/`. scheduled, and realized snapshots under `reports/`. Default is `none`.
- `--pim-conv-lowering=<auto|legacy|depthwise|packed-im2col|streamed-patch|streamed-packed|output-channel-tiled|input-k-tiled|tiled-2d>` -
select the convolution lowering strategy. Default is `auto`.
- `--pim-conv-im2col-max-elements=<N>` - maximum globally materialized im2col
elements per convolution before streaming. Default is `1048576`.
- `--pim-conv-stream-chunk-positions=<N>` - maximum output positions per
streamed convolution chunk. Default is `1024`.
- `--pim-report-conv-lowering=<true|false>` - emit the bounded convolution
lowering report. Default is `true`.
- `--use-experimental-conv-impl` - use the alternate convolution lowering. - `--use-experimental-conv-impl` - use the alternate convolution lowering.
- `--pim-detect-communication-deadlock` - statically simulate expanded
send/receive ordering and reject blocking deadlocks. Default is off.
- `--pim-materialize-scalar-fanout-global-order` - use the experimental,
expensive globally ordered scalar-fanout materializer. Default is off.
- `--pim-trace-communication-materialization` - emit verbose communication
materialization diagnostics and provenance attributes. Default is off.
- `--ignore-concat-error` - soft-fail a ConcatOp corner case. - `--ignore-concat-error` - soft-fail a ConcatOp corner case.
## Standard PIM hardware profile ## Standard PIM hardware profile
@@ -149,7 +163,7 @@ Python dependencies used by the validation scripts are `numpy`, `onnx`, and
Per-operation validation from the repository root: Per-operation validation from the repository root:
```bash ```bash
python validation/validate.py \ .venv/bin/python validation/validate.py \
--raptor-path build_release/Release/bin/onnx-mlir \ --raptor-path build_release/Release/bin/onnx-mlir \
--onnx-include-dir onnx-mlir/include \ --onnx-include-dir onnx-mlir/include \
--operations-dir validation/operations \ --operations-dir validation/operations \
@@ -165,7 +179,7 @@ Validate one network or a subset by pointing `--operations-dir` at any directory
containing `.onnx` files: containing `.onnx` files:
```bash ```bash
python validation/validate.py \ .venv/bin/python validation/validate.py \
--raptor-path build_release/Release/bin/onnx-mlir \ --raptor-path build_release/Release/bin/onnx-mlir \
--onnx-include-dir onnx-mlir/include \ --onnx-include-dir onnx-mlir/include \
--operations-dir validation/networks/yolo11n/depth_04 \ --operations-dir validation/networks/yolo11n/depth_04 \
@@ -173,7 +187,6 @@ python validation/validate.py \
--crossbar-size 128 \ --crossbar-size 128 \
--core-count 144 \ --core-count 144 \
--verbose \ --verbose \
--raptor-extra-arg=--pim-memory-report=summary \
--raptor-extra-arg=--pim-detect-communication-deadlock \ --raptor-extra-arg=--pim-detect-communication-deadlock \
--raptor-extra-arg=--pim-export-spatial-dataflow=none --raptor-extra-arg=--pim-export-spatial-dataflow=none
``` ```
@@ -200,7 +213,7 @@ Each validation run writes artifacts in the model workspace, for example under
The compiler currently dumps dialect snapshots such as `spatial0.mlir`, The compiler currently dumps dialect snapshots such as `spatial0.mlir`,
`spatial1_graph.mlir`, `spatial2_trivial_merged.mlir`, `spatial1_graph.mlir`, `spatial2_trivial_merged.mlir`,
`spatial3_scheduled_no_comm.mlir`, `spatial4_scheduled.mlir`, `pim0.mlir`, `pim1_buff.mlir`, `spatial3_scheduled_no_comm.mlir`, `spatial4_scheduled.mlir`, `pim0.mlir`, `pim1_buff.mlir`,
`pim2_folded.mlir`, `pim3_coalesced.mlir`, and `pim4_memory_planned.mlir` when an output directory is `pim2_folded.mlir`, and `pim3_memory_planned.mlir` when an output directory is
available. available.
To rerun the simulator manually with tracing after validation has produced a To rerun the simulator manually with tracing after validation has produced a
@@ -229,7 +242,7 @@ Available operation suites under `validation/operations/`: `add`, `concat`,
Generated operation tests can be regenerated with: Generated operation tests can be regenerated with:
```bash ```bash
python3 validation/operations/gen_tests.py .venv/bin/python validation/operations/gen_tests.py
``` ```
## Build ## Build
-1
View File
@@ -121,7 +121,6 @@ add_pim_library(OMPIMAccel
OMPimCommon OMPimCommon
OMPimBufferization OMPimBufferization
OMPimHostConstantFolding OMPimHostConstantFolding
OMPimMemoryCoalescing
OMPimVerification OMPimVerification
MLIRTensorInferTypeOpInterfaceImpl MLIRTensorInferTypeOpInterfaceImpl
) )
+8 -5
View File
@@ -23,6 +23,7 @@
#include "src/Compiler/CompilerOptions.hpp" #include "src/Compiler/CompilerOptions.hpp"
#include <cstdint> #include <cstdint>
#include <array>
#include <limits> #include <limits>
namespace onnx_mlir { namespace onnx_mlir {
@@ -30,11 +31,13 @@ namespace onnx_mlir {
inline constexpr llvm::StringLiteral kCoreIdAttrName = "coreId"; inline constexpr llvm::StringLiteral kCoreIdAttrName = "coreId";
inline constexpr llvm::StringLiteral kCoreIdsAttrName = "coreIds"; inline constexpr llvm::StringLiteral kCoreIdsAttrName = "coreIds";
inline constexpr llvm::StringLiteral kLocalMemoryAddressAttrName = "pim.local_memory_address"; inline constexpr llvm::StringLiteral kLocalMemoryAddressAttrName = "pim.local_memory_address";
inline constexpr llvm::StringLiteral kLocalMemorySlotAttrName = "pim.local_memory_slot"; inline constexpr llvm::StringLiteral kLocalMemorySizeAttrName = "pim.local_memory_size";
inline constexpr llvm::StringLiteral kLocalMemorySlotSizeAttrName = "pim.local_memory_slot_size"; inline constexpr std::array<llvm::StringLiteral, 4> kRemovedLocalMemoryPlanAttrNames = {
inline constexpr llvm::StringLiteral kLocalMemoryFallbackCountAttrName = "pim.local_memory_fallback_count"; "pim.local_memory_slot",
inline constexpr llvm::StringLiteral kLocalMemoryNestedSingleUseCountAttrName = "pim.local_memory_slot_size",
"pim.local_memory_nested_single_use_count"; "pim.local_memory_fallback_count",
"pim.local_memory_nested_single_use_count",
};
inline constexpr size_t kPimLocalMemoryAddressLimit = static_cast<size_t>(std::numeric_limits<int32_t>::max()); inline constexpr size_t kPimLocalMemoryAddressLimit = static_cast<size_t>(std::numeric_limits<int32_t>::max());
} // namespace onnx_mlir } // namespace onnx_mlir
+133 -166
View File
@@ -141,6 +141,19 @@ static void printMemoryOverflowDiagnostic(const MemoryValueKey& key,
} }
} }
static std::fstream openMemoryReport(bool enabled) {
if (std::string outputRoot = getOutputDir(); !outputRoot.empty()) {
for (std::string name : {std::string("memory_") + "coalescing_report.txt",
std::string("pim_memory_") + "liveness_report.txt",
std::string("pim_memory_") + "liveness_report.json",
std::string("pim_memory_") + "liveness_timeline.dot"})
sys::fs::remove(outputRoot + "/reports/" + name);
if (!enabled)
sys::fs::remove(outputRoot + "/reports/memory_report.txt");
}
return enabled ? openReportFile("memory_report") : std::fstream();
}
} // namespace } // namespace
size_t PimMemory::allocateAddress(size_t size, const MemoryValueKey& key) { size_t PimMemory::allocateAddress(size_t size, const MemoryValueKey& key) {
@@ -193,12 +206,11 @@ void PimMemory::allocateMemoryForValue(const MemoryValueKey& key, MemEntry& memE
globalMemEntriesMap[key] = memEntry; globalMemEntriesMap[key] = memEntry;
switch (reportKind) { switch (reportKind) {
case MemoryReportKind::Alloca: break; case MemoryReportKind::Alloca:
case MemoryReportKind::Global: case MemoryReportKind::Global:
++reportRow.numGlobal;
reportRow.sizeGlobal += memEntry.size;
break;
case MemoryReportKind::Input: case MemoryReportKind::Input:
++reportRow.hostObjectCount;
break;
case MemoryReportKind::None: break; case MemoryReportKind::None: break;
} }
} }
@@ -242,12 +254,15 @@ void PimMemory::allocateHost(ModuleOp moduleOp, func::FuncOp funcOp) {
} }
void PimMemory::allocateCore(const CompiledCoreMemoryPlan& plan, std::optional<unsigned> lane) { void PimMemory::allocateCore(const CompiledCoreMemoryPlan& plan, std::optional<unsigned> lane) {
if (localPhysicalSlots.empty()) { if (!localArenaSize) {
llvm::append_range(localPhysicalSlots, plan.slots); localArenaSize = plan.arenaSize;
reportRow.logicalAllocaBytes = plan.logicalBytes; reportRow.logicalLocalAllocationCount = plan.logicalAllocationCount;
reportRow.fallbackIntervals = plan.fallbackIntervals; reportRow.logicalLocalBytes = plan.logicalBytes;
reportRow.nestedSingleUseIntervals = plan.nestedSingleUseIntervals;
} }
else if (*localArenaSize != plan.arenaSize
|| reportRow.logicalLocalAllocationCount != plan.logicalAllocationCount
|| reportRow.logicalLocalBytes != plan.logicalBytes)
llvm_unreachable("inconsistent PIM local-memory plan across core-batch lanes");
for (const CompiledLocalMemoryEntry& entry : plan.entries) { for (const CompiledLocalMemoryEntry& entry : plan.entries) {
MemoryValueKey key = getMemoryValueKey(entry.value, lane); MemoryValueKey key = getMemoryValueKey(entry.value, lane);
ownedMemEntriesMap[key] = entry.memory; ownedMemEntriesMap[key] = entry.memory;
@@ -255,49 +270,10 @@ void PimMemory::allocateCore(const CompiledCoreMemoryPlan& plan, std::optional<u
} }
} }
static void printHostMemoryReportRow(raw_ostream& os, const MemoryReportRow& row) {
llvm::SmallVector<ReportField, 2> fields = {
{"Number of globals", std::to_string(row.numGlobal) },
{"Global memory", formatReportMemory(row.sizeGlobal)}
};
printReportFlatFields(os, fields);
}
static void printCoreMemoryReportRow(raw_ostream& os, const MemoryReportEntry& entry) {
llvm::SmallVector<ReportField, 2> fields = {
{"Number of allocas", std::to_string(entry.row.numAlloca) },
{"Allocated memory", formatReportMemory(entry.row.sizeAlloca)}
};
printReportFlatFields(os, fields);
}
static void printBatchMemoryReportRow(raw_ostream& os, const MemoryReportEntry& entry) {
llvm::SmallVector<ReportField, 2> perCoreFields = {
{"Number of allocas", std::to_string(entry.row.numAlloca) },
{"Allocated memory", formatReportMemory(entry.row.sizeAlloca)}
};
llvm::SmallVector<ReportField, 2> totalFields = {
{"Number of allocas", std::to_string(entry.totalAllocaCount) },
{"Batch memory", formatReportMemory(entry.totalAllocaBytes)}
};
printReportPerCoreAndTotalFields(os, perCoreFields, totalFields);
}
static MemoryReportRow addMemoryReportRows(const MemoryReportRow& lhs, const MemoryReportRow& rhs) {
MemoryReportRow result = lhs;
result.numAlloca += rhs.numAlloca;
result.sizeAlloca += rhs.sizeAlloca;
result.numGlobal += rhs.numGlobal;
result.sizeGlobal += rhs.sizeGlobal;
return result;
}
MemoryReportRow PimMemory::getReportRow() const { MemoryReportRow PimMemory::getReportRow() const {
MemoryReportRow row = reportRow; MemoryReportRow row = reportRow;
row.numAlloca = localPhysicalSlots.size(); row.hostBytes = firstAvailableAddress;
row.sizeAlloca = 0; row.physicalLocalBytes = localArenaSize.value_or(0);
for (const PhysicalSlotInfo& slot : localPhysicalSlots)
row.sizeAlloca += slot.size;
return row; return row;
} }
@@ -379,29 +355,32 @@ llvm::FailureOr<int64_t> PimAcceleratorMemory::getIndexValue(mlir::Value value,
return compiledIt->second.evaluate(knowledge); return compiledIt->second.evaluate(knowledge);
} }
PimAcceleratorMemory::PimAcceleratorMemory()
: hostMem(memEntriesMap), fileReport(openMemoryReport(pimMemoryReport == PimMemoryReportSummary)) {}
PimAcceleratorMemory::PimAcceleratorMemory(
const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport)
: memEntriesMap(initialMemEntries),
hostMem(memEntriesMap),
fileReport(enableReport ? openMemoryReport(true) : std::fstream()) {}
void PimAcceleratorMemory::reportHost() { hostReportRow = hostMem.getReportRow(); } void PimAcceleratorMemory::reportHost() { hostReportRow = hostMem.getReportRow(); }
void PimAcceleratorMemory::recordCoreReport(size_t coreId, const MemoryReportRow& row) { void PimAcceleratorMemory::recordCoreReport(size_t coreId, const MemoryReportRow& row) {
reportEntries.push_back({MemoryReportEntry::Kind::Core, reportEntries.push_back({MemoryReportEntry::Kind::Core,
coreId, coreId,
{pim::checkedI32OrCrash(coreId, "memory report core id")}, {pim::checkedI32OrCrash(coreId, "memory report core id")},
row, row});
row.numAlloca,
row.sizeAlloca});
} }
void PimAcceleratorMemory::recordBatchReport(uint64_t batchId, void PimAcceleratorMemory::recordBatchReport(uint64_t batchId,
ArrayRef<int32_t> coreIds, ArrayRef<int32_t> coreIds,
const MemoryReportRow& perCoreRow, const MemoryReportRow& perCoreRow) {
uint64_t totalAllocaCount,
uint64_t totalAllocaBytes) {
MemoryReportEntry entry; MemoryReportEntry entry;
entry.kind = MemoryReportEntry::Kind::Batch; entry.kind = MemoryReportEntry::Kind::Batch;
entry.id = batchId; entry.id = batchId;
llvm::append_range(entry.coreIds, coreIds); llvm::append_range(entry.coreIds, coreIds);
entry.row = perCoreRow; entry.row = perCoreRow;
entry.totalAllocaCount = totalAllocaCount;
entry.totalAllocaBytes = totalAllocaBytes;
reportEntries.push_back(std::move(entry)); reportEntries.push_back(std::move(entry));
} }
@@ -410,93 +389,91 @@ void PimAcceleratorMemory::flushReport() {
return; return;
llvm::raw_os_ostream os(fileReport); llvm::raw_os_ostream os(fileReport);
uint64_t totalGlobalMemory = hostReportRow.has_value() ? hostReportRow->sizeGlobal : 0; struct ReportGroup {
uint64_t totalWeightsMemory = totalWeightBytes; MemoryReportRow row;
uint64_t totalCoresMemory = 0; SmallVector<int32_t, 8> coreIds;
uint64_t totalLogicalCoreMemory = 0; std::optional<uint64_t> batchId;
uint64_t totalFallbackIntervals = 0;
uint64_t totalNestedSingleUseIntervals = 0;
uint64_t maximumCorePeak = 0;
std::string worstEntry = "<none>";
for (const MemoryReportEntry& entry : reportEntries) {
totalCoresMemory += entry.totalAllocaBytes;
uint64_t factor = entry.kind == MemoryReportEntry::Kind::Batch ? entry.coreIds.size() : 1;
totalLogicalCoreMemory += entry.row.logicalAllocaBytes * factor;
totalFallbackIntervals += entry.row.fallbackIntervals * factor;
totalNestedSingleUseIntervals += entry.row.nestedSingleUseIntervals * factor;
if (entry.row.sizeAlloca <= maximumCorePeak)
continue;
maximumCorePeak = entry.row.sizeAlloca;
worstEntry = entry.kind == MemoryReportEntry::Kind::Batch ? "Batch " + std::to_string(entry.id)
: "Core " + std::to_string(entry.coreIds.front());
}
uint64_t savedCoreMemory =
totalLogicalCoreMemory >= totalCoresMemory ? totalLogicalCoreMemory - totalCoresMemory : 0;
double savedPercent = totalLogicalCoreMemory == 0
? 0.0
: 100.0 * static_cast<double>(savedCoreMemory) / static_cast<double>(totalLogicalCoreMemory);
llvm::SmallVector<ReportField, 10> totalFields = {
{"Global memory", formatReportMemory(totalGlobalMemory) },
{"Weights memory", formatReportMemory(totalWeightsMemory) },
{"Logical core allocations", formatReportMemory(totalLogicalCoreMemory) },
{"Physical core memory", formatReportMemory(totalCoresMemory) },
{"Cores memory", formatReportMemory(totalCoresMemory) },
{"Saved core memory", formatReportMemory(savedCoreMemory) },
{"Saved core memory percent", formatv("{0:F2}%", savedPercent).str() },
{"Maximum core peak", formatReportMemory(maximumCorePeak) },
{"Worst core/batch", worstEntry },
{"Fallback intervals", std::to_string(totalFallbackIntervals) },
{"Nested single-use intervals", std::to_string(totalNestedSingleUseIntervals) }
}; };
printReportTotalsBlock(os, totalFields); SmallVector<ReportGroup, 32> groups;
for (const MemoryReportEntry& entry : reportEntries) {
if (hostReportRow.has_value()) { auto group = llvm::find_if(groups, [&](const ReportGroup& candidate) {
os << "\nHost:\n"; return candidate.row.logicalLocalBytes == entry.row.logicalLocalBytes
printHostMemoryReportRow(os, *hostReportRow); && candidate.row.physicalLocalBytes == entry.row.physicalLocalBytes;
});
if (group == groups.end()) {
ReportGroup newGroup;
newGroup.row = entry.row;
llvm::append_range(newGroup.coreIds, entry.coreIds);
if (entry.kind == MemoryReportEntry::Kind::Batch)
newGroup.batchId = entry.id;
groups.push_back(std::move(newGroup));
continue;
} }
llvm::append_range(group->coreIds, entry.coreIds);
group->batchId.reset();
}
llvm::sort(groups, [](const ReportGroup& lhs, const ReportGroup& rhs) {
if (lhs.row.physicalLocalBytes != rhs.row.physicalLocalBytes)
return lhs.row.physicalLocalBytes > rhs.row.physicalLocalBytes;
return lhs.row.logicalLocalBytes > rhs.row.logicalLocalBytes;
});
uint64_t logicalBytes = 0;
uint64_t physicalBytes = 0;
for (const ReportGroup& group : groups) {
logicalBytes += group.row.logicalLocalBytes * group.coreIds.size();
physicalBytes += group.row.physicalLocalBytes * group.coreIds.size();
}
uint64_t savedBytes = logicalBytes - physicalBytes;
double savedPercent = logicalBytes == 0 ? 0.0 : 100.0 * savedBytes / logicalBytes;
uint64_t largest = groups.empty() ? 0 : groups.front().row.physicalLocalBytes;
auto printLabel = [&](const ReportGroup& group) {
if (group.batchId)
os << "Batch " << *group.batchId << " — cores ";
else if (group.coreIds.size() == 1)
os << "Core ";
else
os << "Cores ";
printCompressedIntegerEntries(os, ArrayRef<int32_t>(group.coreIds));
};
if (!reportEntries.empty()) { os << "Summary\n";
if (hostReportRow.has_value()) os << " Host memory: " << formatReportMemory(hostReportRow ? hostReportRow->hostBytes : 0) << "\n";
os << " Weights memory: " << formatReportMemory(totalWeightBytes) << "\n";
os << " Local memory before reuse: " << formatReportMemory(logicalBytes) << "\n";
os << " Local memory after reuse: " << formatReportMemory(physicalBytes) << "\n";
os << " Saved local memory: " << formatReportMemory(savedBytes) << " ("
<< formatv("{0:F1}%", savedPercent) << ")\n";
os << " Largest core local memory: " << formatReportMemory(largest) << "\n";
if (!groups.empty()) {
os << " ";
printLabel(groups.front());
os << "\n"; os << "\n";
sortReportEntriesByFirstCore(reportEntries);
for (size_t index = 0; index < reportEntries.size();) {
size_t runEnd = index + 1;
while (runEnd < reportEntries.size() && reportEntries[runEnd].kind == reportEntries[index].kind
&& reportEntries[runEnd].row == reportEntries[index].row
&& reportEntries[runEnd].totalAllocaCount == reportEntries[index].totalAllocaCount
&& reportEntries[runEnd].totalAllocaBytes == reportEntries[index].totalAllocaBytes) {
++runEnd;
} }
if (reportEntries[index].kind == MemoryReportEntry::Kind::Batch) { os << "\nLargest core groups\n";
os << "Batch "; constexpr size_t kGroupLimit = 8;
for (size_t batchIndex = index; batchIndex < runEnd; ++batchIndex) { for (const ReportGroup& group : ArrayRef(groups).take_front(kGroupLimit)) {
if (batchIndex != index) printLabel(group);
os << ",\n "; os << "\n";
os << reportEntries[batchIndex].id << " (cores "; uint64_t groupSaved = group.row.logicalLocalBytes - group.row.physicalLocalBytes;
printCompressedIntegerEntries(os, ArrayRef<int32_t>(reportEntries[batchIndex].coreIds)); double groupPercent = group.row.logicalLocalBytes == 0
os << ")"; ? 0.0
} : 100.0 * groupSaved / group.row.logicalLocalBytes;
if (group.coreIds.size() == 1) {
os << " Local memory: " << formatReportMemory(group.row.logicalLocalBytes) << ""
<< formatReportMemory(group.row.physicalLocalBytes) << " (" << formatv("{0:F1}% saved", groupPercent)
<< ")\n";
} }
else { else {
llvm::SmallVector<int32_t, 8> coreIds; os << " Per core: " << formatReportMemory(group.row.logicalLocalBytes) << ""
for (size_t coreIndex = index; coreIndex < runEnd; ++coreIndex) << formatReportMemory(group.row.physicalLocalBytes) << " (" << formatv("{0:F1}% saved", groupPercent)
coreIds.push_back(reportEntries[coreIndex].coreIds.front()); << ")\n";
os << "Core "; os << " Total after reuse: " << formatReportMemory(group.row.physicalLocalBytes * group.coreIds.size())
printCompressedIntegerEntries(os, ArrayRef<int32_t>(coreIds)); << "\n";
}
os << ":\n";
if (reportEntries[index].kind == MemoryReportEntry::Kind::Batch)
printBatchMemoryReportRow(os, reportEntries[index]);
else
printCoreMemoryReportRow(os, reportEntries[index]);
printReportEntrySeparator(os, runEnd < reportEntries.size());
index = runEnd;
} }
} }
if (groups.size() > kGroupLimit)
os << " " << groups.size() - kGroupLimit << " smaller groups omitted\n";
os.flush(); os.flush();
fileReport.close(); fileReport.close();
@@ -834,25 +811,20 @@ static SmallVector<Operation*> collectTopLevelCoreLikeOps(func::FuncOp funcOp) {
static FailureOr<CompiledCoreMemoryPlan> compileCoreMemoryPlan(Operation* coreLikeOp) { static FailureOr<CompiledCoreMemoryPlan> compileCoreMemoryPlan(Operation* coreLikeOp) {
CompiledCoreMemoryPlan plan; CompiledCoreMemoryPlan plan;
auto fallbackAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemoryFallbackCountAttrName); auto arenaAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemorySizeAttrName);
auto nestedSingleUseAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemoryNestedSingleUseCountAttrName); if (!arenaAttr || arenaAttr.getInt() < 0
if (!fallbackAttr || !nestedSingleUseAttr || fallbackAttr.getInt() < 0 || nestedSingleUseAttr.getInt() < 0) { || static_cast<uint64_t>(arenaAttr.getInt()) > kPimLocalMemoryAddressLimit) {
coreLikeOp->emitError("requires complete PIM local-memory planning summary attributes before codegen"); coreLikeOp->emitError("requires a valid pim.local_memory_size attribute before codegen");
return failure(); return failure();
} }
plan.fallbackIntervals = static_cast<uint64_t>(fallbackAttr.getInt()); plan.arenaSize = static_cast<size_t>(arenaAttr.getInt());
plan.nestedSingleUseIntervals = static_cast<uint64_t>(nestedSingleUseAttr.getInt());
SmallDenseMap<size_t, size_t, 32> slotIndices;
bool hasFailure = false; bool hasFailure = false;
coreLikeOp->walk([&](memref::AllocOp allocOp) { coreLikeOp->walk([&](memref::AllocOp allocOp) {
if (hasFailure) if (hasFailure)
return; return;
auto addressAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemoryAddressAttrName); auto addressAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemoryAddressAttrName);
auto slotAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemorySlotAttrName); if (!addressAttr || addressAttr.getInt() < 0) {
auto slotSizeAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemorySlotSizeAttrName); allocOp.emitOpError("requires a non-negative pim.local_memory_address attribute before codegen");
if (!addressAttr || !slotAttr || !slotSizeAttr || addressAttr.getInt() < 0 || slotAttr.getInt() < 0
|| slotSizeAttr.getInt() < 0) {
allocOp.emitOpError("requires a complete non-negative PIM local-memory plan before codegen");
hasFailure = true; hasFailure = true;
return; return;
} }
@@ -864,25 +836,23 @@ static FailureOr<CompiledCoreMemoryPlan> compileCoreMemoryPlan(Operation* coreLi
return; return;
} }
size_t address = static_cast<size_t>(addressAttr.getInt()); size_t address = static_cast<size_t>(addressAttr.getInt());
size_t slotId = static_cast<size_t>(slotAttr.getInt()); if (address > plan.arenaSize || *checkedSize > plan.arenaSize - address) {
size_t slotSize = static_cast<size_t>(slotSizeAttr.getInt()); allocOp.emitOpError("has a planned local-memory range outside its core arena");
plan.entries.push_back({allocOp.getResult(), {address, static_cast<size_t>(*checkedSize)}}); hasFailure = true;
plan.logicalBytes += *checkedSize;
auto [slotIt, inserted] = slotIndices.try_emplace(slotId, plan.slots.size());
if (inserted) {
plan.slots.push_back({slotId, 0, slotSize});
return; return;
} }
const PhysicalSlotInfo& existing = plan.slots[slotIt->second]; plan.entries.push_back({allocOp.getResult(), {address, static_cast<size_t>(*checkedSize)}});
if (existing.size != slotSize) { auto logicalBytes = pim::checkedAdd(
allocOp.emitOpError("has a PIM local-memory arena inconsistent with another allocation"); static_cast<size_t>(plan.logicalBytes), static_cast<size_t>(*checkedSize), allocOp, "logical local bytes");
if (failed(logicalBytes)) {
hasFailure = true; hasFailure = true;
return;
} }
plan.logicalBytes = *logicalBytes;
++plan.logicalAllocationCount;
}); });
if (hasFailure) if (hasFailure)
return failure(); return failure();
llvm::sort(plan.slots, [](const PhysicalSlotInfo& lhs, const PhysicalSlotInfo& rhs) { return lhs.id < rhs.id; });
return plan; return plan;
} }
@@ -1462,7 +1432,6 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
for (const SmallVector<size_t>& group : batchJobIndices) { for (const SmallVector<size_t>& group : batchJobIndices) {
SmallVector<int32_t> reportedCoreIds; SmallVector<int32_t> reportedCoreIds;
MemoryReportRow batchRow;
std::optional<MemoryReportRow> batchPerCoreRow; std::optional<MemoryReportRow> batchPerCoreRow;
for (size_t jobIndex : group) { for (size_t jobIndex : group) {
@@ -1475,15 +1444,13 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
reportedCoreIds.push_back(pim::checkedI32OrCrash(job.emittedCoreId, "batch report core id")); reportedCoreIds.push_back(pim::checkedI32OrCrash(job.emittedCoreId, "batch report core id"));
if (!batchPerCoreRow) if (!batchPerCoreRow)
batchPerCoreRow = result.reportRow; batchPerCoreRow = result.reportRow;
batchRow = addMemoryReportRows(batchRow, result.reportRow); else if (!(*batchPerCoreRow == result.reportRow))
llvm_unreachable("one PIM core batch produced inconsistent per-core memory reports");
} }
uint64_t batchReportId = jobs[group.front()].batchReportId.value_or(0); uint64_t batchReportId = jobs[group.front()].batchReportId.value_or(0);
memory.recordBatchReport(batchReportId, memory.recordBatchReport(
reportedCoreIds, batchReportId, reportedCoreIds, batchPerCoreRow.value_or(MemoryReportRow {}));
batchPerCoreRow.value_or(MemoryReportRow {}),
batchRow.numAlloca,
batchRow.sizeAlloca);
} }
maxCoreId = nextEmittedCoreId == 0 ? 0 : nextEmittedCoreId - 1; maxCoreId = nextEmittedCoreId == 0 ? 0 : nextEmittedCoreId - 1;
+16 -34
View File
@@ -33,12 +33,6 @@ struct MemEntry {
size_t size; size_t size;
}; };
struct PhysicalSlotInfo {
size_t id = 0;
size_t address = 0;
size_t size = 0;
};
struct MemoryValueKey { struct MemoryValueKey {
mlir::Value value; mlir::Value value;
std::optional<unsigned> lane; std::optional<unsigned> lane;
@@ -53,26 +47,22 @@ struct CompiledLocalMemoryEntry {
struct CompiledCoreMemoryPlan { struct CompiledCoreMemoryPlan {
llvm::SmallVector<CompiledLocalMemoryEntry, 32> entries; llvm::SmallVector<CompiledLocalMemoryEntry, 32> entries;
llvm::SmallVector<PhysicalSlotInfo, 32> slots; uint64_t logicalAllocationCount = 0;
uint64_t logicalBytes = 0; uint64_t logicalBytes = 0;
uint64_t fallbackIntervals = 0; size_t arenaSize = 0;
uint64_t nestedSingleUseIntervals = 0;
}; };
struct MemoryReportRow { struct MemoryReportRow {
uint64_t numAlloca = 0; uint64_t hostObjectCount = 0;
uint64_t sizeAlloca = 0; uint64_t hostBytes = 0;
uint64_t numGlobal = 0; uint64_t logicalLocalAllocationCount = 0;
uint64_t sizeGlobal = 0; uint64_t logicalLocalBytes = 0;
uint64_t logicalAllocaBytes = 0; uint64_t physicalLocalBytes = 0;
uint64_t fallbackIntervals = 0;
uint64_t nestedSingleUseIntervals = 0;
bool operator==(const MemoryReportRow& other) const { bool operator==(const MemoryReportRow& other) const {
return numAlloca == other.numAlloca && sizeAlloca == other.sizeAlloca && numGlobal == other.numGlobal return hostObjectCount == other.hostObjectCount && hostBytes == other.hostBytes
&& sizeGlobal == other.sizeGlobal && logicalAllocaBytes == other.logicalAllocaBytes && logicalLocalAllocationCount == other.logicalLocalAllocationCount
&& fallbackIntervals == other.fallbackIntervals && logicalLocalBytes == other.logicalLocalBytes && physicalLocalBytes == other.physicalLocalBytes;
&& nestedSingleUseIntervals == other.nestedSingleUseIntervals;
} }
}; };
@@ -99,16 +89,14 @@ struct MemoryReportEntry {
uint64_t id = 0; uint64_t id = 0;
llvm::SmallVector<int32_t, 8> coreIds; llvm::SmallVector<int32_t, 8> coreIds;
MemoryReportRow row; MemoryReportRow row;
uint64_t totalAllocaCount = 0;
uint64_t totalAllocaBytes = 0;
}; };
class PimMemory { class PimMemory {
llvm::SmallVector<PendingMemEntry, 32> memEntries; llvm::SmallVector<PendingMemEntry, 32> memEntries;
llvm::SmallVector<PhysicalSlotInfo, 32> localPhysicalSlots;
llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& globalMemEntriesMap; llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& globalMemEntriesMap;
llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32> ownedMemEntriesMap; llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32> ownedMemEntriesMap;
MemoryReportRow reportRow; MemoryReportRow reportRow;
std::optional<size_t> localArenaSize;
size_t minAlignment = 4; size_t minAlignment = 4;
size_t firstAvailableAddress = 0; size_t firstAvailableAddress = 0;
@@ -145,12 +133,9 @@ private:
mutable llvm::DenseMap<mlir::Value, CompiledAddressExpr> compiledAddressExprs; mutable llvm::DenseMap<mlir::Value, CompiledAddressExpr> compiledAddressExprs;
public: public:
PimAcceleratorMemory() PimAcceleratorMemory();
: hostMem(memEntriesMap), fileReport(openReportFile("memory_report")) {} PimAcceleratorMemory(
PimAcceleratorMemory(const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport) const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport);
: memEntriesMap(initialMemEntries),
hostMem(memEntriesMap),
fileReport(enableReport ? openReportFile("memory_report") : std::fstream()) {}
PimMemory& getOrCreateDeviceMem(size_t id); PimMemory& getOrCreateDeviceMem(size_t id);
@@ -160,11 +145,8 @@ public:
llvm::FailureOr<int64_t> getIndexValue(mlir::Value value, const StaticValueKnowledge& knowledge = {}) const; llvm::FailureOr<int64_t> getIndexValue(mlir::Value value, const StaticValueKnowledge& knowledge = {}) const;
void reportHost(); void reportHost();
void recordCoreReport(size_t coreId, const MemoryReportRow& row); void recordCoreReport(size_t coreId, const MemoryReportRow& row);
void recordBatchReport(uint64_t batchId, void recordBatchReport(
llvm::ArrayRef<int32_t> coreIds, uint64_t batchId, llvm::ArrayRef<int32_t> coreIds, const MemoryReportRow& perCoreRow);
const MemoryReportRow& perCoreRow,
uint64_t totalAllocaCount,
uint64_t totalAllocaBytes);
void setTotalWeightBytes(uint64_t bytes) { totalWeightBytes = bytes; } void setTotalWeightBytes(uint64_t bytes) { totalWeightBytes = bytes; }
void flushReport(); void flushReport();
}; };
+2 -10
View File
@@ -19,10 +19,8 @@ llvm::cl::opt<PimMemoryReportLevel> pimMemoryReport(
"pim-memory-report", "pim-memory-report",
llvm::cl::desc("Emit a human-readable PIM memory planning report"), llvm::cl::desc("Emit a human-readable PIM memory planning report"),
llvm::cl::values(clEnumValN(PimMemoryReportNone, "none", "Do not emit any PIM memory planning report")), llvm::cl::values(clEnumValN(PimMemoryReportNone, "none", "Do not emit any PIM memory planning report")),
llvm::cl::values( llvm::cl::values(clEnumValN(PimMemoryReportSummary, "summary", "Emit a concise PIM memory summary")),
clEnumValN(PimMemoryReportSummary, "summary", "Emit a concise slot reuse report with key offenders")), llvm::cl::init(PimMemoryReportSummary),
llvm::cl::values(clEnumValN(PimMemoryReportFull, "full", "Emit the full detailed PIM memory planning report")),
llvm::cl::init(PimMemoryReportNone),
llvm::cl::cat(OnnxMlirOptions)); llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<PimConvLoweringType> pimConvLowering( llvm::cl::opt<PimConvLoweringType> pimConvLowering(
@@ -72,12 +70,6 @@ llvm::cl::opt<bool>
llvm::cl::init(false), llvm::cl::init(false),
llvm::cl::cat(OnnxMlirOptions)); llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<bool>
pimDisableMemoryCoalescing("pim-disable-memory-coalescing",
llvm::cl::desc("Skip the early PIM IR memory coalescing pass (developer diagnostic)"),
llvm::cl::init(false),
llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<bool> useExperimentalConvImpl("use-experimental-conv-impl", llvm::cl::opt<bool> useExperimentalConvImpl("use-experimental-conv-impl",
llvm::cl::desc("Use experimental implementation for convolution"), llvm::cl::desc("Use experimental implementation for convolution"),
llvm::cl::init(false), llvm::cl::init(false),
-2
View File
@@ -23,7 +23,6 @@ typedef enum {
typedef enum { typedef enum {
PimMemoryReportNone = 0, PimMemoryReportNone = 0,
PimMemoryReportSummary = 1, PimMemoryReportSummary = 1,
PimMemoryReportFull = 2,
} PimMemoryReportLevel; } PimMemoryReportLevel;
typedef enum { typedef enum {
@@ -53,7 +52,6 @@ extern llvm::cl::opt<PimMemoryReportLevel> pimMemoryReport;
extern llvm::cl::opt<PimConvLoweringType> pimConvLowering; extern llvm::cl::opt<PimConvLoweringType> pimConvLowering;
extern llvm::cl::opt<PimSpatialDataflowExportType> pimExportSpatialDataflow; extern llvm::cl::opt<PimSpatialDataflowExportType> pimExportSpatialDataflow;
extern llvm::cl::opt<bool> pimDisableMemoryCoalescing;
extern llvm::cl::opt<bool> pimOnlyCodegen; extern llvm::cl::opt<bool> pimOnlyCodegen;
extern llvm::cl::opt<bool> useExperimentalConvImpl; extern llvm::cl::opt<bool> useExperimentalConvImpl;
extern llvm::cl::opt<bool> pimEmitJson; extern llvm::cl::opt<bool> pimEmitJson;
+1 -2
View File
@@ -22,6 +22,7 @@ void addPassesPim(OwningOpRef<ModuleOp>& module,
if (pimOnlyCodegen) { if (pimOnlyCodegen) {
pm.addPass(createPimLocalMemoryPlanningPass()); pm.addPass(createPimLocalMemoryPlanningPass());
pm.addPass(createPimVerificationPass());
pm.addPass(createEmitPimCodePass()); pm.addPass(createEmitPimCodePass());
return; return;
} }
@@ -52,8 +53,6 @@ void addPassesPim(OwningOpRef<ModuleOp>& module,
pm.addPass(mlir::createLowerAffinePass()); pm.addPass(mlir::createLowerAffinePass());
pm.addPass(createPimHostConstantFoldingPass()); pm.addPass(createPimHostConstantFoldingPass());
pm.addPass(createMessagePass("Pim host constants folded")); pm.addPass(createMessagePass("Pim host constants folded"));
if (!pimDisableMemoryCoalescing)
pm.addPass(createPimMemoryCoalescingPass());
pm.addPass(createPimLocalMemoryPlanningPass()); pm.addPass(createPimLocalMemoryPlanningPass());
pm.addPass(createMessagePass("Pim local memory planned")); pm.addPass(createMessagePass("Pim local memory planned"));
pm.addPass(createPimVerificationPass()); pm.addPass(createPimVerificationPass());
@@ -5,4 +5,8 @@ add_pim_library(OMPimLocalMemoryLifetimeAnalysis
INCLUDE_DIRS PUBLIC INCLUDE_DIRS PUBLIC
${PIM_PUBLIC_INCLUDE_DIRS} ${PIM_PUBLIC_INCLUDE_DIRS}
LINK_LIBS PUBLIC
OMPimCommon
PimOps
) )
@@ -2,70 +2,204 @@
#include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Interfaces/DestinationStyleOpInterface.h" #include "mlir/Interfaces/DestinationStyleOpInterface.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallPtrSet.h"
#include "llvm/ADT/SmallVector.h" #include "llvm/ADT/SmallVector.h"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp" #include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
using namespace mlir; using namespace mlir;
namespace onnx_mlir { namespace onnx_mlir::pim {
namespace pim { namespace {
struct OperationOrdering {
DenseMap<Operation*, uint64_t> position;
DenseMap<Operation*, uint64_t> subtreeEnd;
uint64_t next = 0;
};
static void orderOperation(Operation* op, OperationOrdering& ordering) {
uint64_t end = ordering.next;
ordering.position[op] = ordering.next++;
for (Region& region : op->getRegions())
for (Block& block : region)
for (Operation& nested : block) {
orderOperation(&nested, ordering);
end = std::max(end, ordering.subtreeEnd.lookup(&nested));
}
ordering.subtreeEnd[op] = end;
}
static OperationOrdering buildOrdering(Operation* coreLikeOp) {
OperationOrdering ordering;
if (!coreLikeOp || coreLikeOp->getNumRegions() != 1 || coreLikeOp->getRegion(0).empty())
return ordering;
for (Operation& op : coreLikeOp->getRegion(0).front())
orderOperation(&op, ordering);
return ordering;
}
static bool isRuntimeMemoryTouch(Operation* op) {
return isa<PimMemCopyHostToDevOp,
PimMemCopyDevToHostOp,
PimMemCopyOp,
PimReceiveOp,
PimSendOp,
PimConcatOp,
PimVMMOp,
PimTransposeOp,
PimVVAddOp,
PimVVSubOp,
PimVVMulOp,
PimVVMaxOp,
PimVVDMulOp,
PimVAvgOp,
PimVReluOp,
PimVTanhOp,
PimVSigmOp,
PimVSoftmaxOp>(op);
}
static bool isIgnoredUser(Operation* op) {
return isLocalMemoryAliasOp(op) || isa<scf::ForOp, scf::YieldOp, memref::DeallocOp>(op)
|| isCoreStaticAddressOp(op);
}
static bool isWithin(Value value, Region* region) {
if (auto argument = dyn_cast<BlockArgument>(value))
return argument.getOwner()->getParent() == region;
if (Operation* definingOp = value.getDefiningOp())
return definingOp->getParentRegion() == region || region->isAncestor(definingOp->getParentRegion());
return false;
}
static std::pair<uint64_t, uint64_t> getTouchRange(Value definingValue, Operation* user, const OperationOrdering& ordering) {
uint64_t start = ordering.position.lookup(user);
uint64_t end = start;
for (Operation* current = user; current; current = current->getParentOp())
if (auto forOp = dyn_cast<scf::ForOp>(current); forOp && !isWithin(definingValue, &forOp.getRegion())) {
start = std::min(start, ordering.position.lookup(forOp));
end = std::max(end, ordering.subtreeEnd.lookup(forOp));
}
return {start, end};
}
static FailureOr<size_t> getAllocationSize(memref::AllocOp allocation) {
auto type = dyn_cast<ShapedType>(allocation.getType());
if (!type)
return failure();
auto bytes = getCheckedShapedTypeSizeInBytes(type, allocation, "memory allocation byte size");
if (failed(bytes))
return failure();
return checkedSize(*bytes, allocation, "memory allocation byte size");
}
} // namespace
bool isLocalMemoryAliasOp(Operation* op) { bool isLocalMemoryAliasOp(Operation* op) {
return isa<memref::SubViewOp, memref::CastOp, memref::CollapseShapeOp, memref::ExpandShapeOp>(op); return isa<memref::SubViewOp, memref::CastOp, memref::CollapseShapeOp, memref::ExpandShapeOp>(op);
} }
LogicalResult walkLocalMemoryUses(Value root, LogicalResult walkLocalMemoryUses(Value root, LocalMemoryUseCallback callback) {
llvm::function_ref<LogicalResult(Value, Operation*)> visitUser) {
llvm::SmallPtrSet<Value, 16> visitedValues; llvm::SmallPtrSet<Value, 16> visitedValues;
llvm::SmallPtrSet<Operation*, 32> visitedUsers; llvm::SmallPtrSet<OpOperand*, 32> visitedUses;
llvm::SmallVector<Value> pendingValues {root}; SmallVector<Value> pending = {root};
auto addAlias = [&](Value value) { pendingValues.push_back(value); }; while (!pending.empty()) {
Value value = pending.pop_back_val();
while (!pendingValues.empty()) {
Value value = pendingValues.pop_back_val();
if (!visitedValues.insert(value).second) if (!visitedValues.insert(value).second)
continue; continue;
for (Operation* user : value.getUsers()) { for (OpOperand& use : value.getUses()) {
if (!visitedUsers.insert(user).second) if (!visitedUses.insert(&use).second || failed(callback(use)))
continue;
if (failed(visitUser(value, user)))
return failure(); return failure();
Operation* user = use.getOwner();
if (isLocalMemoryAliasOp(user)) if (isLocalMemoryAliasOp(user) && use.getOperandNumber() == 0) {
for (Value result : user->getResults()) if (user->getNumResults() != 1)
addAlias(result); return failure();
pending.push_back(user->getResult(0));
if (auto dpsOp = dyn_cast<DestinationStyleOpInterface>(user))
for (OpResult result : user->getResults())
if (OpOperand* tied = dpsOp.getTiedOpOperand(result); tied && tied->get() == value)
addAlias(result);
if (auto forOp = dyn_cast<scf::ForOp>(user))
for (auto [index, initArg] : llvm::enumerate(forOp.getInitArgs()))
if (initArg == value) {
addAlias(forOp.getRegionIterArgs()[index]);
addAlias(forOp.getResult(index));
} }
auto yieldOp = dyn_cast<scf::YieldOp>(user); if (auto dpsOp = dyn_cast<DestinationStyleOpInterface>(user); dpsOp && dpsOp.isDpsInit(&use))
if (!yieldOp) pending.push_back(dpsOp.getTiedOpResult(&use));
continue;
for (auto [index, operand] : llvm::enumerate(yieldOp.getOperands())) { if (auto forOp = dyn_cast<scf::ForOp>(user))
if (operand != value) for (auto [index, init] : llvm::enumerate(forOp.getInitArgsMutable()))
continue; if (&init == &use) {
if (auto forOp = dyn_cast<scf::ForOp>(yieldOp->getParentOp())) pending.push_back(forOp.getRegionIterArgs()[index]);
addAlias(forOp.getResult(index)); pending.push_back(forOp.getResult(index));
else if (auto ifOp = dyn_cast<scf::IfOp>(yieldOp->getParentOp())) }
addAlias(ifOp.getResult(index));
else if (auto switchOp = dyn_cast<scf::IndexSwitchOp>(yieldOp->getParentOp())) if (auto yieldOp = dyn_cast<scf::YieldOp>(user)) {
addAlias(switchOp.getResult(index)); unsigned index = use.getOperandNumber();
Operation* parent = yieldOp->getParentOp();
if (auto forOp = dyn_cast<scf::ForOp>(parent))
pending.push_back(forOp.getResult(index));
else if (auto ifOp = dyn_cast<scf::IfOp>(parent))
pending.push_back(ifOp.getResult(index));
else if (auto switchOp = dyn_cast<scf::IndexSwitchOp>(parent))
pending.push_back(switchOp.getResult(index));
else
return failure();
} }
} }
} }
return success(); return success();
} }
} // namespace pim FailureOr<SmallVector<LocalMemoryInterval, 0>> analyzeLocalMemoryLifetimes(Operation* coreLikeOp) {
} // namespace onnx_mlir SmallVector<LocalMemoryInterval, 0> intervals;
OperationOrdering ordering = buildOrdering(coreLikeOp);
if (ordering.position.empty())
return intervals;
uint64_t fallbackEnd = ordering.next - 1;
bool failedSize = false;
coreLikeOp->walk([&](memref::AllocOp allocation) {
auto size = getAllocationSize(allocation);
if (failed(size)) {
failedSize = true;
return;
}
LocalMemoryInterval interval {allocation, ordering.position.lookup(allocation), ordering.position.lookup(allocation), *size};
bool hasRuntimeTouch = false;
bool needsFallback = false;
if (failed(walkLocalMemoryUses(allocation.getResult(), [&](OpOperand& use) {
Operation* user = use.getOwner();
if (isRuntimeMemoryTouch(user)) {
auto [start, end] = getTouchRange(allocation.getResult(), user, ordering);
if (!hasRuntimeTouch) {
interval.start = start;
interval.end = end;
hasRuntimeTouch = true;
}
else {
interval.start = std::min(interval.start, start);
interval.end = std::max(interval.end, end);
}
}
else if (!isIgnoredUser(user)) {
needsFallback = true;
}
return success();
})))
needsFallback = true;
if (!hasRuntimeTouch || needsFallback) {
interval.start = std::min(interval.start, ordering.position.lookup(allocation));
interval.end = fallbackEnd;
}
intervals.push_back(interval);
});
if (failedSize)
return failure();
return intervals;
}
bool localMemoryLifetimesOverlap(const LocalMemoryInterval& lhs, const LocalMemoryInterval& rhs) {
return !(lhs.end < rhs.start || rhs.end < lhs.start);
}
} // namespace onnx_mlir::pim
@@ -1,17 +1,25 @@
#pragma once #pragma once
#include "mlir/IR/Operation.h" #include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "llvm/ADT/STLFunctionalExtras.h" #include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/SmallVector.h"
namespace onnx_mlir { namespace onnx_mlir::pim {
namespace pim {
struct LocalMemoryInterval {
mlir::memref::AllocOp allocation;
uint64_t start = 0;
uint64_t end = 0;
size_t size = 0;
};
using LocalMemoryUseCallback = llvm::function_ref<mlir::LogicalResult(mlir::OpOperand&)>;
bool isLocalMemoryAliasOp(mlir::Operation* op); bool isLocalMemoryAliasOp(mlir::Operation* op);
mlir::LogicalResult walkLocalMemoryUses(mlir::Value root, LocalMemoryUseCallback callback);
mlir::FailureOr<llvm::SmallVector<LocalMemoryInterval, 0>>
analyzeLocalMemoryLifetimes(mlir::Operation* coreLikeOp);
bool localMemoryLifetimesOverlap(const LocalMemoryInterval& lhs, const LocalMemoryInterval& rhs);
mlir::LogicalResult walkLocalMemoryUses( } // namespace onnx_mlir::pim
mlir::Value root,
llvm::function_ref<mlir::LogicalResult(mlir::Value, mlir::Operation*)> visitUser);
} // namespace pim
} // namespace onnx_mlir
-1
View File
@@ -4,7 +4,6 @@ add_onnx_mlir_dialect_doc(pim Pim.td)
add_subdirectory(Analysis) add_subdirectory(Analysis)
add_subdirectory(Transforms/Bufferization) add_subdirectory(Transforms/Bufferization)
add_subdirectory(Transforms/HostConstantFolding) add_subdirectory(Transforms/HostConstantFolding)
add_subdirectory(Transforms/MemoryCoalescing)
add_subdirectory(Transforms/LocalMemoryPlanning) add_subdirectory(Transforms/LocalMemoryPlanning)
add_subdirectory(Transforms/Verification) add_subdirectory(Transforms/Verification)
@@ -8,7 +8,6 @@ add_pim_library(OMPimLocalMemoryPlanning
LINK_LIBS PUBLIC LINK_LIBS PUBLIC
OMPimCommon OMPimCommon
OMPimCompilerOptions
OMPimLocalMemoryLifetimeAnalysis OMPimLocalMemoryLifetimeAnalysis
PimOps PimOps
) )
@@ -1,668 +1,169 @@
#include "mlir/Dialect/Bufferization/IR/BufferizableOpInterface.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h" #include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Pass/Pass.h" #include "mlir/Pass/Pass.h"
#include "mlir/IR/Value.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/STLExtras.h"
#include "llvm/Support/MathExtras.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/raw_os_ostream.h"
#include "llvm/Support/raw_ostream.h"
#include <fstream>
#include <memory>
#include <numeric> #include <numeric>
#include <string>
#include <tuple> #include <tuple>
#include <utility>
#include "Common/Support/CheckedArithmetic.hpp"
#include "Common/Support/ReportUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/BatchCoreUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp" #include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp" #include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning/LocalMemoryPlanning.hpp" #include "src/Accelerators/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning/LocalMemoryPlanning.hpp"
#include "src/Accelerators/PIM/Pass/PIMPasses.h" #include "src/Accelerators/PIM/Pass/PIMPasses.h"
using namespace llvm; using namespace llvm;
using namespace mlir; using namespace mlir;
using namespace onnx_mlir;
namespace {
struct MemoryTouchInterval {
uint64_t start = 0;
uint64_t end = 0;
Operation* firstTouchOp = nullptr;
Operation* lastTouchOp = nullptr;
uint64_t firstTouchPosition = 0;
uint64_t lastTouchPosition = 0;
bool hasRuntimeUse = false;
bool startUsedAllocFallback = false;
bool endUsedFallback = false;
bool escapesLoop = false;
std::string fallbackReason;
};
struct OperationOrdering {
llvm::DenseMap<Operation*, uint64_t> position;
llvm::DenseMap<Operation*, uint64_t> subtreeEnd;
uint64_t nextPosition = 0;
};
static std::string abbreviate(StringRef text, size_t maxLen) {
if (text.size() <= maxLen)
return text.str();
return (text.take_front(maxLen - 3) + "...").str();
}
static std::string summarizeValue(mlir::Value value, size_t maxLen = 72) {
std::string text;
llvm::raw_string_ostream os(text);
if (auto result = dyn_cast<OpResult>(value))
os << result.getOwner()->getName() << '#' << result.getResultNumber();
else if (auto blockArg = dyn_cast<BlockArgument>(value))
os << "block_arg#" << blockArg.getArgNumber();
else
os << "<unknown value>";
os << " : " << value.getType();
os.flush();
return abbreviate(text, maxLen);
}
static std::string summarizeOperation(Operation* op, size_t maxLen = 96) {
if (!op)
return "<none>";
return abbreviate(op->getName().getStringRef(), maxLen);
}
static void assignOperationOrdering(Operation* op, OperationOrdering& ordering) {
uint64_t position = ordering.nextPosition++;
ordering.position[op] = position;
uint64_t end = position;
for (Region& region : op->getRegions())
for (Block& block : region)
for (Operation& nestedOp : block) {
assignOperationOrdering(&nestedOp, ordering);
end = std::max(end, ordering.subtreeEnd.lookup(&nestedOp));
}
ordering.subtreeEnd[op] = end;
}
static OperationOrdering buildOperationOrdering(Operation* coreLikeOp) {
OperationOrdering ordering;
if (!coreLikeOp || coreLikeOp->getNumRegions() != 1 || coreLikeOp->getRegion(0).empty())
return ordering;
for (Operation& op : coreLikeOp->getRegion(0).front())
assignOperationOrdering(&op, ordering);
return ordering;
}
static bool isRuntimeMemoryTouchOp(Operation* op) {
return isa<pim::PimMemCopyHostToDevOp,
pim::PimMemCopyDevToHostOp,
pim::PimMemCopyOp,
pim::PimReceiveOp,
pim::PimSendOp,
pim::PimConcatOp,
pim::PimVMMOp,
pim::PimTransposeOp,
pim::PimVVAddOp,
pim::PimVVSubOp,
pim::PimVVMulOp,
pim::PimVVMaxOp,
pim::PimVVDMulOp,
pim::PimVAvgOp,
pim::PimVReluOp,
pim::PimVTanhOp,
pim::PimVSigmOp,
pim::PimVSoftmaxOp>(op);
}
static bool isIgnoredLivenessUser(Operation* op) {
return pim::isLocalMemoryAliasOp(op) || isa<scf::ForOp, scf::YieldOp, memref::DeallocOp>(op)
|| isCoreStaticAddressOp(op);
}
static bool isWithin(mlir::Value value, Region* region) {
if (!region)
return false;
if (auto blockArg = dyn_cast<BlockArgument>(value))
return blockArg.getOwner()->getParent() == region;
if (Operation* definingOp = value.getDefiningOp())
return definingOp->getParentRegion() == region || region->isAncestor(definingOp->getParentRegion());
return false;
}
static bool isNestedAllocation(Operation* coreLikeOp, memref::AllocOp allocOp) {
if (!coreLikeOp || coreLikeOp->getNumRegions() != 1 || coreLikeOp->getRegion(0).empty())
return false;
return allocOp->getBlock() != &coreLikeOp->getRegion(0).front();
}
static void addFallbackReason(std::string& reason, StringRef newReason) {
if (newReason.empty())
return;
if (!reason.empty())
reason += "; ";
reason += newReason.str();
}
struct OrderedTouchRange {
uint64_t start = 0;
uint64_t end = 0;
bool escapedLoop = false;
};
static OrderedTouchRange
getEffectiveTouchRange(mlir::Value definingValue, Operation* user, const OperationOrdering& ordering) {
OrderedTouchRange range {ordering.position.lookup(user), ordering.position.lookup(user), false};
for (Operation* current = user; current; current = current->getParentOp()) {
auto forOp = dyn_cast<scf::ForOp>(current);
if (!forOp || isWithin(definingValue, &forOp.getRegion()))
continue;
range.start = std::min(range.start, ordering.position.lookup(forOp));
range.end = std::max(range.end, ordering.subtreeEnd.lookup(forOp));
range.escapedLoop = true;
}
return range;
}
static MemoryTouchInterval computeMemoryTouchInterval(memref::AllocOp allocOp,
const OperationOrdering& ordering,
uint64_t fallbackEnd) {
MemoryTouchInterval interval;
interval.start = ordering.position.lookup(allocOp);
interval.end = interval.start;
auto parentLoop = allocOp->getParentOfType<scf::ForOp>();
(void) pim::walkLocalMemoryUses(
allocOp.getResult(),
[&](mlir::Value value, Operation* user) {
if (auto forOp = dyn_cast<scf::ForOp>(user);
forOp && parentLoop && forOp != parentLoop && llvm::is_contained(forOp.getInitArgs(), value))
interval.escapesLoop = true;
if (auto yieldOp = dyn_cast<scf::YieldOp>(user)) {
auto forOp = dyn_cast<scf::ForOp>(yieldOp->getParentOp());
auto ifOp = dyn_cast<scf::IfOp>(yieldOp->getParentOp());
auto indexSwitch = dyn_cast<scf::IndexSwitchOp>(yieldOp->getParentOp());
if (!forOp && !ifOp && !indexSwitch)
addFallbackReason(interval.fallbackReason, "yield without scf.for parent");
else if (forOp && parentLoop && forOp == parentLoop && llvm::is_contained(yieldOp.getOperands(), value))
interval.escapesLoop = true;
}
if (isRuntimeMemoryTouchOp(user)) {
uint64_t touchPosition = ordering.position.lookup(user);
if (!interval.hasRuntimeUse || touchPosition < interval.firstTouchPosition) {
interval.firstTouchPosition = touchPosition;
interval.firstTouchOp = user;
}
if (!interval.hasRuntimeUse || touchPosition > interval.lastTouchPosition) {
interval.lastTouchPosition = touchPosition;
interval.lastTouchOp = user;
}
OrderedTouchRange range = getEffectiveTouchRange(allocOp.getResult(), user, ordering);
interval.escapesLoop |= range.escapedLoop;
if (!interval.hasRuntimeUse) {
interval.start = range.start;
interval.end = range.end;
interval.hasRuntimeUse = true;
}
else {
if (range.start < interval.start)
interval.start = range.start;
if (range.end > interval.end)
interval.end = range.end;
}
return success();
}
if (isIgnoredLivenessUser(user))
return success();
addFallbackReason(interval.fallbackReason, "unhandled user op");
interval.endUsedFallback = true;
return success();
});
if (!interval.hasRuntimeUse) {
interval.startUsedAllocFallback = true;
interval.endUsedFallback = true;
interval.start = ordering.position.lookup(allocOp);
interval.end = fallbackEnd;
interval.firstTouchPosition = interval.start;
interval.lastTouchPosition = interval.end;
addFallbackReason(interval.fallbackReason, "no runtime memory touch");
return interval;
}
if (interval.endUsedFallback)
interval.end = std::max(interval.end, fallbackEnd);
return interval;
}
static FailureOr<size_t> getAllocSizeBytes(memref::AllocOp allocOp) {
auto type = dyn_cast<ShapedType>(allocOp.getType());
if (!type)
return failure();
auto checkedBytes = pim::getCheckedShapedTypeSizeInBytes(type, allocOp, "memory allocation byte size");
if (failed(checkedBytes))
return failure();
return pim::checkedSize(*checkedBytes, allocOp, "memory allocation byte size");
}
static bool intervalsOverlap(const LocalAllocInterval& lhs, const LocalAllocInterval& rhs) {
return !(lhs.end < rhs.start || rhs.end < lhs.start);
}
static uint64_t getSlotLogicalBytes(const PlannedPhysicalSlot& slot, ArrayRef<LocalAllocInterval> intervals) {
uint64_t slotLogicalBytes = 0;
for (size_t intervalIndex : slot.intervalIndices)
slotLogicalBytes += intervals[intervalIndex].size;
return slotLogicalBytes;
}
static bool placementsOverlap(const PlannedPhysicalSlot& lhs, const PlannedPhysicalSlot& rhs) {
return lhs.address < rhs.address + rhs.requiredSize && rhs.address < lhs.address + lhs.requiredSize;
}
static bool hasAddressReuse(size_t slotIndex, ArrayRef<PlannedPhysicalSlot> slots) {
if (slots[slotIndex].intervalIndices.size() > 1)
return true;
return llvm::any_of(llvm::enumerate(slots), [&](auto indexedSlot) {
return indexedSlot.index() != slotIndex && placementsOverlap(slots[slotIndex], indexedSlot.value());
});
}
} // namespace
SmallVector<LocalAllocInterval, 0> onnx_mlir::buildLocalAllocIntervals(Operation* coreLikeOp) {
SmallVector<LocalAllocInterval, 0> intervals;
OperationOrdering ordering = buildOperationOrdering(coreLikeOp);
if (ordering.position.empty())
return intervals;
uint64_t fallbackEnd = ordering.nextPosition == 0 ? 0 : ordering.nextPosition - 1;
size_t nextIntervalId = 0;
coreLikeOp->walk([&](memref::AllocOp allocOp) {
auto checkedSize = getAllocSizeBytes(allocOp);
if (failed(checkedSize)) {
llvm::errs() << "Failed to compute local allocation size for value: ";
allocOp.getResult().print(llvm::errs());
llvm::errs() << "\n";
llvm_unreachable("Failed to compute local allocation size");
}
MemoryTouchInterval touchInterval = computeMemoryTouchInterval(allocOp, ordering, fallbackEnd);
LocalAllocInterval interval;
interval.id = nextIntervalId++;
interval.alloc = allocOp;
interval.start = touchInterval.start;
interval.end = touchInterval.end;
interval.size = *checkedSize;
interval.firstTouchOp = touchInterval.firstTouchOp;
interval.lastTouchOp = touchInterval.lastTouchOp;
interval.firstTouchPosition = touchInterval.firstTouchPosition;
interval.lastTouchPosition = touchInterval.lastTouchPosition;
interval.startUsedAllocFallback = touchInterval.startUsedAllocFallback;
interval.endUsedFallback = touchInterval.endUsedFallback;
interval.hasRuntimeUse = touchInterval.hasRuntimeUse;
interval.insideNestedRegion = isNestedAllocation(coreLikeOp, allocOp);
interval.escapesLoop = touchInterval.escapesLoop;
interval.fallbackReason = std::move(touchInterval.fallbackReason);
interval.valueSummary = summarizeValue(allocOp.getResult(), 88);
interval.firstTouchSummary = summarizeOperation(touchInterval.firstTouchOp);
interval.lastTouchSummary = summarizeOperation(touchInterval.lastTouchOp);
intervals.push_back(std::move(interval));
});
return intervals;
}
SmallVector<PlannedPhysicalSlot, 0> onnx_mlir::planPhysicalSlots(MutableArrayRef<LocalAllocInterval> intervals) {
SmallVector<PlannedPhysicalSlot, 0> slots;
SmallVector<size_t> intervalOrder(intervals.size());
std::iota(intervalOrder.begin(), intervalOrder.end(), 0);
llvm::stable_sort(intervalOrder, [&](size_t lhsIndex, size_t rhsIndex) {
const LocalAllocInterval& lhs = intervals[lhsIndex];
const LocalAllocInterval& rhs = intervals[rhsIndex];
if (lhs.size != rhs.size)
return lhs.size > rhs.size;
if (lhs.start != rhs.start)
return lhs.start < rhs.start;
if (lhs.end != rhs.end)
return lhs.end < rhs.end;
return lhs.id < rhs.id;
});
for (size_t intervalIndex : intervalOrder) {
LocalAllocInterval& interval = intervals[intervalIndex];
SmallVector<const PlannedPhysicalSlot*, 16> conflictingSlots;
SmallVector<size_t, 16> candidateAddresses = {0};
size_t currentPeak = 0;
for (const PlannedPhysicalSlot& slot : slots) {
currentPeak = std::max(currentPeak, slot.address + slot.requiredSize);
if (llvm::any_of(slot.intervalIndices, [&](size_t otherIndex) {
return intervalsOverlap(interval, intervals[otherIndex]);
})) {
conflictingSlots.push_back(&slot);
size_t candidate = llvm::alignTo(slot.address + slot.requiredSize, 4);
if (!llvm::is_contained(candidateAddresses, candidate))
candidateAddresses.push_back(candidate);
}
}
size_t bestAddress = std::numeric_limits<size_t>::max();
auto bestKey = std::tuple<size_t, size_t>(std::numeric_limits<size_t>::max(),
std::numeric_limits<size_t>::max());
for (size_t candidate : candidateAddresses) {
bool overlaps = llvm::any_of(conflictingSlots, [&](const PlannedPhysicalSlot* slot) {
return candidate < slot->address + slot->requiredSize && slot->address < candidate + interval.size;
});
if (overlaps)
continue;
auto candidateKey = std::tuple<size_t, size_t>(std::max(currentPeak, candidate + interval.size), candidate);
if (candidateKey < bestKey) {
bestKey = candidateKey;
bestAddress = candidate;
}
}
assert(bestAddress != std::numeric_limits<size_t>::max() && "address after all conflicts must be available");
auto reusable = llvm::find_if(slots, [&](const PlannedPhysicalSlot& slot) {
return slot.address == bestAddress && slot.requiredSize == interval.size;
});
if (reusable != slots.end()) {
reusable->intervalIndices.push_back(intervalIndex);
interval.slotPlanIndex = static_cast<size_t>(reusable - slots.begin());
}
else {
slots.push_back({slots.size(), interval.size, bestAddress, {intervalIndex}});
interval.slotPlanIndex = slots.size() - 1;
}
}
return slots;
}
CoreMemoryPlan onnx_mlir::buildCoreMemoryPlan(Operation* coreLikeOp) {
CoreMemoryPlan plan;
plan.coreLikeOp = coreLikeOp;
plan.intervals = buildLocalAllocIntervals(coreLikeOp);
plan.slots = planPhysicalSlots(plan.intervals);
return plan;
}
LogicalResult onnx_mlir::assignPhysicalSlotAddresses(CoreMemoryPlan& plan, size_t addressLimit) {
SmallVector<size_t> slotOrder(plan.slots.size());
std::iota(slotOrder.begin(), slotOrder.end(), 0);
llvm::stable_sort(slotOrder, [&](size_t lhsIndex, size_t rhsIndex) {
const PlannedPhysicalSlot& lhs = plan.slots[lhsIndex];
const PlannedPhysicalSlot& rhs = plan.slots[rhsIndex];
if (lhs.requiredSize != rhs.requiredSize)
return lhs.requiredSize > rhs.requiredSize;
return lhs.id < rhs.id;
});
size_t nextSlotId = 0;
for (size_t slotIndex : slotOrder) {
PlannedPhysicalSlot& slot = plan.slots[slotIndex];
if (slot.address > addressLimit || slot.requiredSize > addressLimit - slot.address) {
plan.coreLikeOp->emitError() << "PIM local memory plan exceeds the signed int32 address range while assigning "
"a "
<< slot.requiredSize << " byte physical placement at address " << slot.address;
return failure();
}
slot.id = nextSlotId++;
}
return success();
}
std::string onnx_mlir::buildMemoryPlanReport(Operation* coreLikeOp,
ArrayRef<LocalAllocInterval> intervals,
ArrayRef<PlannedPhysicalSlot> slots,
size_t addressLimit,
PimMemoryReportLevel reportLevel) {
std::string artifacts;
uint64_t totalLogicalBytes = 0;
uint64_t totalPhysicalBytes = 0;
uint64_t fallbackIntervals = 0;
uint64_t noRuntimeTouchIntervals = 0;
uint64_t reusedAllocations = 0;
uint64_t nestedIntervals = 0;
uint64_t loopEscapingIntervals = 0;
size_t largestLogicalAllocation = 0;
size_t largestPhysicalSlot = 0;
size_t maximumAssignedAddress = 0;
for (const LocalAllocInterval& interval : intervals) {
totalLogicalBytes += interval.size;
largestLogicalAllocation = std::max(largestLogicalAllocation, interval.size);
if (interval.startUsedAllocFallback || interval.endUsedFallback)
++fallbackIntervals;
if (!interval.hasRuntimeUse)
++noRuntimeTouchIntervals;
if (interval.insideNestedRegion)
++nestedIntervals;
if (interval.escapesLoop)
++loopEscapingIntervals;
}
for (size_t slotIndex = 0; slotIndex < slots.size(); ++slotIndex) {
const PlannedPhysicalSlot& slot = slots[slotIndex];
largestPhysicalSlot = std::max(largestPhysicalSlot, slot.requiredSize);
maximumAssignedAddress = std::max(maximumAssignedAddress, slot.address + slot.requiredSize);
if (hasAddressReuse(slotIndex, slots))
reusedAllocations += slot.intervalIndices.size();
}
totalPhysicalBytes = maximumAssignedAddress;
uint64_t savedBytes = totalLogicalBytes >= totalPhysicalBytes ? totalLogicalBytes - totalPhysicalBytes : 0;
double savedPercent =
totalLogicalBytes == 0 ? 0.0 : 100.0 * static_cast<double>(savedBytes) / static_cast<double>(totalLogicalBytes);
raw_string_ostream os(artifacts);
os << "=== PIM Memory Liveness Report ===\n";
os << "Op: " << coreLikeOp->getName() << "\n";
os << "Summary:\n";
os << " logical allocation bytes: " << formatReportMemory(totalLogicalBytes) << " (" << totalLogicalBytes << ")\n";
os << " physical allocation bytes: " << formatReportMemory(totalPhysicalBytes) << " (" << totalPhysicalBytes
<< ")\n";
os << " saved bytes: " << formatReportMemory(savedBytes) << " (" << savedBytes << ")\n";
os << " saved percent: " << format("%.2f%%", savedPercent) << "\n";
os << " intervals: " << intervals.size() << "\n";
os << " address placements: " << slots.size() << "\n";
os << " allocations sharing address space: " << reusedAllocations << "\n";
os << " fallback intervals: " << fallbackIntervals << "\n";
os << " intervals with no runtime memory touch: " << noRuntimeTouchIntervals << "\n";
os << " nested allocations: " << nestedIntervals << "\n";
os << " loop-escaping allocations: " << loopEscapingIntervals << "\n";
os << " largest logical allocation: " << largestLogicalAllocation << "\n";
os << " largest address placement: " << largestPhysicalSlot << "\n";
os << " address limit: " << addressLimit << "\n";
os << " peak physical memory: " << formatReportMemory(maximumAssignedAddress) << " (" << maximumAssignedAddress
<< ")\n";
os << " maximum assigned address: " << maximumAssignedAddress << "\n";
SmallVector<const PlannedPhysicalSlot*> reusedSlots;
SmallVector<const PlannedPhysicalSlot*> singleUseSlots;
for (size_t slotIndex = 0; slotIndex < slots.size(); ++slotIndex) {
if (hasAddressReuse(slotIndex, slots))
reusedSlots.push_back(&slots[slotIndex]);
else
singleUseSlots.push_back(&slots[slotIndex]);
}
llvm::stable_sort(reusedSlots, [&](const PlannedPhysicalSlot* lhs, const PlannedPhysicalSlot* rhs) {
uint64_t lhsLogicalBytes = getSlotLogicalBytes(*lhs, intervals);
uint64_t rhsLogicalBytes = getSlotLogicalBytes(*rhs, intervals);
if (lhs->intervalIndices.size() != rhs->intervalIndices.size())
return lhs->intervalIndices.size() > rhs->intervalIndices.size();
if (lhsLogicalBytes != rhsLogicalBytes)
return lhsLogicalBytes > rhsLogicalBytes;
if (lhs->requiredSize != rhs->requiredSize)
return lhs->requiredSize > rhs->requiredSize;
return lhs->id < rhs->id;
});
llvm::stable_sort(singleUseSlots, [&](const PlannedPhysicalSlot* lhs, const PlannedPhysicalSlot* rhs) {
if (lhs->requiredSize != rhs->requiredSize)
return lhs->requiredSize > rhs->requiredSize;
return lhs->id < rhs->id;
});
constexpr size_t kSummaryReuseLimit = 6;
constexpr size_t kSummaryOffenderLimit = 10;
os << "\nBest Address Reuse:\n";
if (reusedSlots.empty()) {
os << " no address ranges were reused\n";
}
else {
for (const PlannedPhysicalSlot* slot : ArrayRef(reusedSlots).take_front(kSummaryReuseLimit)) {
uint64_t slotLogicalBytes = getSlotLogicalBytes(*slot, intervals);
os << " slot #" << slot->id << " addr=" << slot->address << " size=" << formatReportMemory(slot->requiredSize)
<< " intervals=" << slot->intervalIndices.size() << " logical_sum=" << formatReportMemory(slotLogicalBytes)
<< "\n";
}
}
os << "\nTop Offenders:\n";
for (const PlannedPhysicalSlot* slot : ArrayRef(singleUseSlots).take_front(kSummaryOffenderLimit)) {
const LocalAllocInterval& interval = intervals[slot->intervalIndices.front()];
os << " slot #" << slot->id << " is single-use"
<< " size=" << formatReportMemory(slot->requiredSize) << " interval=#" << interval.id
<< " value=" << abbreviate(interval.valueSummary, 56) << "\n";
os << " first=" << abbreviate(interval.firstTouchSummary, 40)
<< " last=" << abbreviate(interval.lastTouchSummary, 40)
<< " nested=" << (interval.insideNestedRegion ? "yes" : "no")
<< " escapes_loop=" << (interval.escapesLoop ? "yes" : "no") << "\n";
}
if (singleUseSlots.empty())
os << " no obvious blockers detected in this core\n";
if (reportLevel == PimMemoryReportFull) {
os << "\nSlot Reuse:\n";
for (const PlannedPhysicalSlot& slot : slots) {
uint64_t slotLogicalBytes = getSlotLogicalBytes(slot, intervals);
os << " slot #" << slot.id << " addr=" << slot.address << " size=" << formatReportMemory(slot.requiredSize)
<< " (" << slot.requiredSize << ")"
<< " intervals=" << slot.intervalIndices.size() << " logical_sum=" << formatReportMemory(slotLogicalBytes)
<< "\n";
for (size_t intervalIndex : slot.intervalIndices) {
const LocalAllocInterval& interval = intervals[intervalIndex];
os << " [" << interval.start << "," << interval.end << "]"
<< " #" << interval.id << " logical=" << formatReportMemory(interval.size) << " (" << interval.size
<< ")"
<< " nested=" << (interval.insideNestedRegion ? "yes" : "no")
<< " escapes_loop=" << (interval.escapesLoop ? "yes" : "no")
<< " first=" << abbreviate(interval.firstTouchSummary, 48)
<< " last=" << abbreviate(interval.lastTouchSummary, 48) << "\n";
os << " value=" << abbreviate(interval.valueSummary, 72) << "\n";
if (!interval.fallbackReason.empty())
os << " fallback_reason=" << interval.fallbackReason << "\n";
}
}
}
os.flush();
return artifacts;
}
namespace onnx_mlir { namespace onnx_mlir {
namespace { namespace {
static bool rangesOverlap(size_t lhsAddress, size_t lhsSize, size_t rhsAddress, size_t rhsSize) {
return lhsAddress < rhsAddress + rhsSize && rhsAddress < lhsAddress + lhsSize;
}
static FailureOr<size_t> alignedEnd(size_t address, size_t size, size_t limit) {
if (address > limit || size > limit - address)
return failure();
size_t end = address + size;
size_t padding = (4 - end % 4) % 4;
if (padding > limit - end)
return failure();
return end + padding;
}
struct PimLocalMemoryPlanningPass : PassWrapper<PimLocalMemoryPlanningPass, OperationPass<ModuleOp>> { struct PimLocalMemoryPlanningPass : PassWrapper<PimLocalMemoryPlanningPass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PimLocalMemoryPlanningPass) MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PimLocalMemoryPlanningPass)
StringRef getArgument() const override { return "pim-local-memory-planning"; } StringRef getArgument() const override { return "pim-local-memory-planning"; }
StringRef getDescription() const override { StringRef getDescription() const override {
return "Plan liveness-based physical slots and addresses for PIM core-local memory"; return "Plan liveness-based addresses for PIM core-local memory";
} }
void runOnOperation() override { void runOnOperation() override {
std::fstream reportFile;
std::unique_ptr<raw_os_ostream> report;
if (pimMemoryReport != PimMemoryReportNone) {
reportFile = openReportFileWithExtension("pim_memory_liveness_report", "txt");
if (reportFile.is_open())
report = std::make_unique<raw_os_ostream>(reportFile);
}
else if (std::string outputRoot = getOutputDir(); !outputRoot.empty()) {
sys::fs::remove(outputRoot + "/reports/pim_memory_liveness_report.txt");
}
Builder builder(&getContext()); Builder builder(&getContext());
uint64_t nextBatchId = 0; bool hasFailure = false;
bool failedPlanning = false;
getOperation().walk([&](Operation* op) { getOperation().walk([&](Operation* op) {
if (failedPlanning || !isa<pim::PimCoreOp, pim::PimCoreBatchOp>(op)) if (hasFailure || !isa<pim::PimCoreOp, pim::PimCoreBatchOp>(op))
return; return;
CoreMemoryPlan plan = buildCoreMemoryPlan(op); op->removeAttr(kLocalMemorySizeAttrName);
if (failed(assignPhysicalSlotAddresses(plan, kPimLocalMemoryAddressLimit))) { for (StringRef name : kRemovedLocalMemoryPlanAttrNames)
failedPlanning = true; op->removeAttr(name);
return; op->walk([&](memref::AllocOp allocation) {
} allocation->removeAttr(kLocalMemoryAddressAttrName);
for (StringRef name : kRemovedLocalMemoryPlanAttrNames)
size_t arenaSize = 0; allocation->removeAttr(name);
for (const PlannedPhysicalSlot& placement : plan.slots)
arenaSize = std::max(arenaSize, placement.address + placement.requiredSize);
for (const LocalAllocInterval& interval : plan.intervals) {
const PlannedPhysicalSlot& slot = plan.slots[interval.slotPlanIndex];
interval.alloc->setAttr(kLocalMemoryAddressAttrName, builder.getI64IntegerAttr(slot.address));
interval.alloc->setAttr(kLocalMemorySlotAttrName, builder.getI64IntegerAttr(0));
interval.alloc->setAttr(kLocalMemorySlotSizeAttrName, builder.getI64IntegerAttr(arenaSize));
}
uint64_t fallbackIntervals = llvm::count_if(plan.intervals, [](const LocalAllocInterval& interval) {
return interval.startUsedAllocFallback || interval.endUsedFallback;
});
uint64_t nestedSingleUseIntervals = llvm::count_if(plan.intervals, [&](const LocalAllocInterval& interval) {
return interval.insideNestedRegion && !hasAddressReuse(interval.slotPlanIndex, plan.slots);
});
op->setAttr(kLocalMemoryFallbackCountAttrName, builder.getI64IntegerAttr(fallbackIntervals));
op->setAttr(kLocalMemoryNestedSingleUseCountAttrName, builder.getI64IntegerAttr(nestedSingleUseIntervals));
if (!report)
return;
std::string planReport = buildMemoryPlanReport(
op, plan.intervals, plan.slots, kPimLocalMemoryAddressLimit, pimMemoryReport);
if (auto coreOp = dyn_cast<pim::PimCoreOp>(op)) {
*report << "Core " << coreOp.getCoreId() << ":\n";
*report << planReport;
return;
}
SmallVector<int32_t> coreIds = getBatchCoreIds(cast<pim::PimCoreBatchOp>(op));
llvm::sort(coreIds);
coreIds.erase(std::unique(coreIds.begin(), coreIds.end()), coreIds.end());
uint64_t batchId = nextBatchId++;
for (int32_t coreId : coreIds)
*report << "Batch " << batchId << " core " << coreId << ":\n" << planReport;
}); });
if (report) { auto plan = buildCoreMemoryPlan(op);
report->flush(); if (failed(plan)) {
reportFile.close(); hasFailure = true;
return;
} }
if (failedPlanning) { op->setAttr(kLocalMemorySizeAttrName, builder.getI64IntegerAttr(plan->arenaSize));
for (const LocalMemoryPlacement& placement : plan->placements)
for (size_t intervalIndex : placement.intervalIndices)
plan->intervals[intervalIndex].allocation->setAttr(
kLocalMemoryAddressAttrName, builder.getI64IntegerAttr(placement.address));
});
if (hasFailure) {
signalPassFailure(); signalPassFailure();
return; return;
} }
dumpModule(getOperation(), "pim4_memory_planned"); dumpModule(getOperation(), "pim3_memory_planned");
} }
}; };
} // namespace } // namespace
FailureOr<SmallVector<LocalMemoryPlacement, 0>>
planLocalMemoryPlacements(ArrayRef<pim::LocalMemoryInterval> intervals, size_t addressLimit) {
SmallVector<LocalMemoryPlacement, 0> placements;
SmallVector<size_t> order(intervals.size());
std::iota(order.begin(), order.end(), 0);
llvm::stable_sort(order, [&](size_t lhsIndex, size_t rhsIndex) {
const auto& lhs = intervals[lhsIndex];
const auto& rhs = intervals[rhsIndex];
if (lhs.size != rhs.size)
return lhs.size > rhs.size;
if (lhs.start != rhs.start)
return lhs.start < rhs.start;
if (lhs.end != rhs.end)
return lhs.end < rhs.end;
return lhsIndex < rhsIndex;
});
for (size_t intervalIndex : order) {
const auto& interval = intervals[intervalIndex];
SmallVector<const LocalMemoryPlacement*, 16> conflicts;
SmallVector<size_t, 16> candidates = {0};
size_t currentPeak = 0;
for (const LocalMemoryPlacement& placement : placements) {
auto end = alignedEnd(placement.address, placement.size, addressLimit);
if (failed(end))
return failure();
currentPeak = std::max(currentPeak, *end);
if (!llvm::any_of(placement.intervalIndices, [&](size_t otherIndex) {
return pim::localMemoryLifetimesOverlap(interval, intervals[otherIndex]);
}))
continue;
conflicts.push_back(&placement);
if (!llvm::is_contained(candidates, *end))
candidates.push_back(*end);
}
size_t bestAddress = std::numeric_limits<size_t>::max();
auto bestKey = std::tuple(std::numeric_limits<size_t>::max(), bestAddress);
for (size_t candidate : candidates) {
auto end = alignedEnd(candidate, interval.size, addressLimit);
if (failed(end) || llvm::any_of(conflicts, [&](const LocalMemoryPlacement* placement) {
return rangesOverlap(candidate, interval.size, placement->address, placement->size);
}))
continue;
auto key = std::tuple(std::max(currentPeak, *end), candidate);
if (key < bestKey) {
bestKey = key;
bestAddress = candidate;
}
}
if (bestAddress == std::numeric_limits<size_t>::max())
return failure();
auto reusable = llvm::find_if(placements, [&](const LocalMemoryPlacement& placement) {
return placement.address == bestAddress && placement.size == interval.size;
});
if (reusable != placements.end())
reusable->intervalIndices.push_back(intervalIndex);
else
placements.push_back({bestAddress, interval.size, {intervalIndex}});
}
return placements;
}
FailureOr<CoreMemoryPlan> buildCoreMemoryPlan(Operation* coreLikeOp) {
CoreMemoryPlan plan;
plan.coreLikeOp = coreLikeOp;
auto intervals = pim::analyzeLocalMemoryLifetimes(coreLikeOp);
if (failed(intervals))
return failure();
plan.intervals = std::move(*intervals);
auto placements = planLocalMemoryPlacements(plan.intervals, kPimLocalMemoryAddressLimit);
if (failed(placements)) {
coreLikeOp->emitError("PIM local-memory plan exceeds the signed int32 address range");
return failure();
}
plan.placements = std::move(*placements);
for (const LocalMemoryPlacement& placement : plan.placements) {
auto end = alignedEnd(placement.address, placement.size, kPimLocalMemoryAddressLimit);
if (failed(end)) {
coreLikeOp->emitError("PIM local-memory plan has invalid address arithmetic");
return failure();
}
plan.arenaSize = std::max(plan.arenaSize, *end);
}
return plan;
}
std::unique_ptr<Pass> createPimLocalMemoryPlanningPass() { std::unique_ptr<Pass> createPimLocalMemoryPlanningPass() {
return std::make_unique<PimLocalMemoryPlanningPass>(); return std::make_unique<PimLocalMemoryPlanningPass>();
} }
@@ -1,64 +1,24 @@
#pragma once #pragma once
#include "mlir/Dialect/MemRef/IR/MemRef.h" #include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallVector.h"
#include <limits>
#include <string>
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
namespace onnx_mlir { namespace onnx_mlir {
struct LocalAllocInterval { struct LocalMemoryPlacement {
size_t id = 0;
mlir::memref::AllocOp alloc;
uint64_t start = 0;
uint64_t end = 0;
size_t size = 0;
mlir::Operation* firstTouchOp = nullptr;
mlir::Operation* lastTouchOp = nullptr;
uint64_t firstTouchPosition = 0;
uint64_t lastTouchPosition = 0;
bool startUsedAllocFallback = false;
bool endUsedFallback = false;
bool hasRuntimeUse = false;
bool insideNestedRegion = false;
bool escapesLoop = false;
std::string fallbackReason;
std::string valueSummary;
std::string firstTouchSummary;
std::string lastTouchSummary;
size_t slotPlanIndex = std::numeric_limits<size_t>::max();
};
struct PlannedPhysicalSlot {
size_t id = std::numeric_limits<size_t>::max();
size_t requiredSize = 0;
size_t address = 0; size_t address = 0;
size_t size = 0;
llvm::SmallVector<size_t, 8> intervalIndices; llvm::SmallVector<size_t, 8> intervalIndices;
}; };
struct CoreMemoryPlan { struct CoreMemoryPlan {
mlir::Operation* coreLikeOp = nullptr; mlir::Operation* coreLikeOp = nullptr;
llvm::SmallVector<LocalAllocInterval, 0> intervals; llvm::SmallVector<pim::LocalMemoryInterval, 0> intervals;
llvm::SmallVector<PlannedPhysicalSlot, 0> slots; llvm::SmallVector<LocalMemoryPlacement, 0> placements;
size_t arenaSize = 0;
}; };
llvm::SmallVector<LocalAllocInterval, 0> buildLocalAllocIntervals(mlir::Operation* coreLikeOp); mlir::FailureOr<llvm::SmallVector<LocalMemoryPlacement, 0>>
planLocalMemoryPlacements(llvm::ArrayRef<pim::LocalMemoryInterval> intervals, size_t addressLimit);
llvm::SmallVector<PlannedPhysicalSlot, 0> planPhysicalSlots(llvm::MutableArrayRef<LocalAllocInterval> intervals); mlir::FailureOr<CoreMemoryPlan> buildCoreMemoryPlan(mlir::Operation* coreLikeOp);
CoreMemoryPlan buildCoreMemoryPlan(mlir::Operation* coreLikeOp);
mlir::LogicalResult assignPhysicalSlotAddresses(CoreMemoryPlan& plan, size_t addressLimit);
std::string buildMemoryPlanReport(mlir::Operation* coreLikeOp,
llvm::ArrayRef<LocalAllocInterval> intervals,
llvm::ArrayRef<PlannedPhysicalSlot> slots,
size_t addressLimit,
PimMemoryReportLevel reportLevel);
} // namespace onnx_mlir } // namespace onnx_mlir
@@ -1,15 +0,0 @@
add_pim_library(OMPimMemoryCoalescing
MemoryCoalescing.cpp
MemoryCoalescing.hpp
MemoryCoalescingPass.cpp
EXCLUDE_FROM_OM_LIBS
INCLUDE_DIRS PUBLIC
${PIM_PUBLIC_INCLUDE_DIRS}
LINK_LIBS PUBLIC
OMPimCommon
OMPimLocalMemoryLifetimeAnalysis
PimOps
)
@@ -1,194 +0,0 @@
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include <limits>
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/MemoryCoalescing/MemoryCoalescing.hpp"
using namespace mlir;
namespace onnx_mlir {
namespace pim {
namespace {
static bool isCandidateAllocType(MemRefType type) {
return type && type.hasStaticShape() && type.getLayout().isIdentity()
&& hasByteSizedElementType(type.getElementType());
}
static uint64_t getTypeSizeBytes(MemRefType type) {
return static_cast<uint64_t>(type.getNumElements() * getElementTypeSizeInBytes(type.getElementType()));
}
static Operation* getTopLevelAncestorInBlock(Operation* op, Block& block) {
Operation* current = op;
while (current && current->getBlock() != &block)
current = current->getParentOp();
return current;
}
static void analyzeBlock(Block& block, MemoryCoalescingAnalysis& analysis);
static FailureOr<uint64_t>
getLastUseInstruction(memref::AllocOp allocOp, Block& scopeBlock, const DenseMap<Operation*, uint64_t>& opOrder) {
uint64_t endInstruction = opOrder.lookup(allocOp);
if (failed(walkLocalMemoryUses(allocOp.getResult(), [&](Value, Operation* user) {
if (auto yieldOp = dyn_cast<scf::YieldOp>(user)) {
if (!isa<scf::ForOp>(yieldOp->getParentOp()))
return failure();
}
Operation* orderedUser = getTopLevelAncestorInBlock(user, scopeBlock);
if (!orderedUser)
return failure();
auto order = opOrder.find(orderedUser);
if (order == opOrder.end())
return failure();
endInstruction = std::max(endInstruction, order->second);
return success();
})))
return failure();
return endInstruction;
}
static void analyzeBlock(Block& block, MemoryCoalescingAnalysis& analysis) {
for (Operation& op : block)
for (Region& region : op.getRegions())
for (Block& nestedBlock : region)
analyzeBlock(nestedBlock, analysis);
DenseMap<Operation*, uint64_t> opOrder;
uint64_t nextInstruction = 0;
for (Operation& op : block)
opOrder.try_emplace(&op, nextInstruction++);
MemoryCoalescingBlockAnalysis blockAnalysis;
blockAnalysis.block = &block;
for (Operation& op : block) {
auto allocOp = dyn_cast<memref::AllocOp>(&op);
if (!allocOp)
continue;
auto allocType = dyn_cast<MemRefType>(allocOp.getType());
if (!isCandidateAllocType(allocType)) {
++blockAnalysis.skippedAllocations;
continue;
}
auto endInstruction = getLastUseInstruction(allocOp, block, opOrder);
if (failed(endInstruction)) {
++blockAnalysis.skippedAllocations;
continue;
}
blockAnalysis.candidates.push_back(
AllocationCandidate {allocOp, opOrder.lookup(allocOp), *endInstruction, getTypeSizeBytes(allocType)});
}
analysis.skippedAllocations += blockAnalysis.skippedAllocations;
if (!blockAnalysis.candidates.empty() || blockAnalysis.skippedAllocations != 0)
analysis.blocks.push_back(std::move(blockAnalysis));
}
} // namespace
uint64_t MemoryCoalescingAnalysis::getCandidateCount() const {
uint64_t total = 0;
for (const MemoryCoalescingBlockAnalysis& block : blocks)
total += block.candidates.size();
return total;
}
uint64_t MemoryCoalescingAnalysis::getCandidateBytes() const {
uint64_t total = 0;
for (const MemoryCoalescingBlockAnalysis& block : blocks)
for (const AllocationCandidate& candidate : block.candidates)
total += candidate.sizeBytes;
return total;
}
MemoryCoalescingAnalysis analyzeMemoryCoalescingCandidates(Operation* coreLikeOp) {
MemoryCoalescingAnalysis analysis;
if (!coreLikeOp || coreLikeOp->getNumRegions() != 1 || coreLikeOp->getRegion(0).empty())
return analysis;
analyzeBlock(coreLikeOp->getRegion(0).front(), analysis);
return analysis;
}
MemoryCoalescingStats
coalesceMemory(Operation* coreLikeOp, const MemoryCoalescingAnalysis& analysis, RewriterBase& rewriter) {
(void) coreLikeOp;
MemoryCoalescingStats stats;
stats.skippedAllocations = analysis.skippedAllocations;
for (const MemoryCoalescingBlockAnalysis& blockAnalysis : analysis.blocks) {
auto candidates = blockAnalysis.candidates;
llvm::sort(candidates, [](const AllocationCandidate& lhs, const AllocationCandidate& rhs) {
if (lhs.startInstruction != rhs.startInstruction)
return lhs.startInstruction < rhs.startInstruction;
return lhs.endInstruction < rhs.endInstruction;
});
struct ActiveStorage {
memref::AllocOp root;
uint64_t endInstruction = 0;
};
SmallVector<ActiveStorage> active;
SmallVector<memref::AllocOp> freeList;
for (AllocationCandidate& candidate : candidates) {
for (auto it = active.begin(); it != active.end();) {
if (it->endInstruction < candidate.startInstruction) {
freeList.push_back(it->root);
it = active.erase(it);
continue;
}
++it;
}
auto bestFit = freeList.end();
uint64_t bestFitBytes = std::numeric_limits<uint64_t>::max();
auto candidateType = cast<MemRefType>(candidate.alloc.getType());
for (auto it = freeList.begin(); it != freeList.end(); ++it) {
auto freeType = cast<MemRefType>((*it).getType());
if (freeType != candidateType)
continue;
uint64_t freeBytes = getTypeSizeBytes(freeType);
if (freeBytes < candidate.sizeBytes || freeBytes >= bestFitBytes)
continue;
bestFit = it;
bestFitBytes = freeBytes;
}
if (bestFit == freeList.end()) {
active.push_back(ActiveStorage {candidate.alloc, candidate.endInstruction});
continue;
}
memref::AllocOp root = *bestFit;
freeList.erase(bestFit);
candidate.alloc.getResult().replaceAllUsesWith(root.getResult());
rewriter.eraseOp(candidate.alloc);
active.push_back(ActiveStorage {root, candidate.endInstruction});
++stats.removedAllocs;
stats.savedBytes += candidate.sizeBytes;
}
}
return stats;
}
} // namespace pim
} // namespace onnx_mlir
@@ -1,44 +0,0 @@
#pragma once
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/IR/PatternMatch.h"
#include "llvm/ADT/SmallVector.h"
namespace onnx_mlir {
namespace pim {
struct AllocationCandidate {
mlir::memref::AllocOp alloc;
uint64_t startInstruction = 0;
uint64_t endInstruction = 0;
uint64_t sizeBytes = 0;
};
struct MemoryCoalescingBlockAnalysis {
mlir::Block* block = nullptr;
llvm::SmallVector<AllocationCandidate> candidates;
uint64_t skippedAllocations = 0;
};
struct MemoryCoalescingAnalysis {
llvm::SmallVector<MemoryCoalescingBlockAnalysis> blocks;
uint64_t skippedAllocations = 0;
uint64_t getCandidateCount() const;
uint64_t getCandidateBytes() const;
};
struct MemoryCoalescingStats {
uint64_t removedAllocs = 0;
uint64_t savedBytes = 0;
uint64_t skippedAllocations = 0;
};
MemoryCoalescingAnalysis analyzeMemoryCoalescingCandidates(mlir::Operation* coreLikeOp);
MemoryCoalescingStats
coalesceMemory(mlir::Operation* coreLikeOp, const MemoryCoalescingAnalysis& analysis, mlir::RewriterBase& rewriter);
} // namespace pim
} // namespace onnx_mlir
@@ -1,101 +0,0 @@
#include "mlir/Pass/Pass.h"
#include "llvm/Support/raw_os_ostream.h"
#include <fstream>
#include "src/Accelerators/PIM/Common/IR/BatchCoreUtils.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/ReportUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/MemoryCoalescing/MemoryCoalescing.hpp"
#include "src/Accelerators/PIM/Pass/PIMPasses.h"
using namespace mlir;
namespace onnx_mlir {
namespace {
struct CoalescingReportRow {
uint64_t candidates = 0;
uint64_t logicalBytes = 0;
uint64_t skipped = 0;
uint64_t removed = 0;
uint64_t savedBytes = 0;
};
struct CoalescingReportEntry {
std::string label;
uint64_t coreCount = 1;
CoalescingReportRow row;
};
static void emitReport(ArrayRef<CoalescingReportEntry> entries) {
std::fstream file = openReportFile("memory_coalescing_report");
if (!file.is_open())
return;
llvm::raw_os_ostream os(file);
CoalescingReportRow total;
for (const CoalescingReportEntry& entry : entries) {
total.candidates += entry.row.candidates * entry.coreCount;
total.logicalBytes += entry.row.logicalBytes * entry.coreCount;
total.skipped += entry.row.skipped * entry.coreCount;
total.removed += entry.row.removed * entry.coreCount;
total.savedBytes += entry.row.savedBytes * entry.coreCount;
}
printReportTotalsBlock(os,
{{"Number of candidates", std::to_string(total.candidates)},
{"Logical candidate bytes", formatReportMemory(total.logicalBytes)},
{"Skipped allocations", std::to_string(total.skipped)},
{"Removed allocations", std::to_string(total.removed)},
{"Saved memory", formatReportMemory(total.savedBytes)}});
for (const CoalescingReportEntry& entry : entries) {
os << "\n" << entry.label << ":\n";
printReportFlatFields(os,
{{"Number of candidates", std::to_string(entry.row.candidates)},
{"Logical candidate bytes", formatReportMemory(entry.row.logicalBytes)},
{"Skipped allocations", std::to_string(entry.row.skipped)},
{"Removed allocations", std::to_string(entry.row.removed)},
{"Saved memory", formatReportMemory(entry.row.savedBytes)}});
}
os.flush();
}
struct PimMemoryCoalescingPass : PassWrapper<PimMemoryCoalescingPass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PimMemoryCoalescingPass)
StringRef getArgument() const override { return "pim-memory-coalescing"; }
StringRef getDescription() const override { return "Safely coalesce compatible block-local PIM allocations"; }
void runOnOperation() override {
IRRewriter rewriter(&getContext());
SmallVector<CoalescingReportEntry, 32> reportEntries;
uint64_t nextBatchId = 0;
getOperation().walk([&](Operation* op) {
if (!isa<pim::PimCoreOp, pim::PimCoreBatchOp>(op))
return;
auto analysis = pim::analyzeMemoryCoalescingCandidates(op);
auto stats = pim::coalesceMemory(op, analysis, rewriter);
CoalescingReportRow row {analysis.getCandidateCount(),
analysis.getCandidateBytes(),
stats.skippedAllocations,
stats.removedAllocs,
stats.savedBytes};
if (auto coreOp = dyn_cast<pim::PimCoreOp>(op)) {
reportEntries.push_back({"Core " + std::to_string(coreOp.getCoreId()), 1, row});
return;
}
SmallVector<int32_t> coreIds = getBatchCoreIds(cast<pim::PimCoreBatchOp>(op));
reportEntries.push_back(
{"Batch " + std::to_string(nextBatchId++), std::max<uint64_t>(1, coreIds.size()), row});
});
emitReport(reportEntries);
dumpModule(getOperation(), "pim3_coalesced");
}
};
} // namespace
std::unique_ptr<Pass> createPimMemoryCoalescingPass() { return std::make_unique<PimMemoryCoalescingPass>(); }
} // namespace onnx_mlir
@@ -7,6 +7,7 @@ add_pim_library(OMPimVerification
OMPimCommon OMPimCommon
OMPimCompilerOptions OMPimCompilerOptions
OMPimBufferization OMPimBufferization
OMPimLocalMemoryLifetimeAnalysis
PimOps PimOps
SpatialOps SpatialOps
) )
@@ -8,6 +8,7 @@
#include "llvm/Support/FormatVariadic.h" #include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/raw_ostream.h" #include "llvm/Support/raw_ostream.h"
#include <map>
#include <string> #include <string>
#include "src/Accelerators/PIM/Common/IR/BatchCoreUtils.hpp" #include "src/Accelerators/PIM/Common/IR/BatchCoreUtils.hpp"
@@ -18,6 +19,7 @@
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp" #include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
#include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp" #include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp" #include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Analysis/LocalMemoryLifetimeAnalysis.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp" #include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/Bufferization/ContiguityPatterns.hpp" #include "src/Accelerators/PIM/Dialect/Pim/Transforms/Bufferization/ContiguityPatterns.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
@@ -109,61 +111,127 @@ static bool isCoreWeightBlockArgument(Value value) {
return false; return false;
} }
struct VerifiedLocalMemorySlot {
uint64_t size = 0;
};
static LogicalResult static LogicalResult
verifyLocalMemoryPlan(Operation* coreLikeOp, pim::CappedDiagnosticReporter& diagnostics) { verifyLocalMemoryPlan(Operation* coreLikeOp, pim::CappedDiagnosticReporter& diagnostics) {
DenseMap<uint64_t, VerifiedLocalMemorySlot> slots;
bool hasFailure = false; bool hasFailure = false;
auto fallbackAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemoryFallbackCountAttrName); for (StringRef name : kRemovedLocalMemoryPlanAttrNames)
auto nestedSingleUseAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemoryNestedSingleUseCountAttrName); if (coreLikeOp->hasAttr(name)) {
if (!fallbackAttr || !nestedSingleUseAttr || fallbackAttr.getInt() < 0 || nestedSingleUseAttr.getInt() < 0) { diagnostics.report(coreLikeOp, [name](Operation* op) {
diagnostics.report(coreLikeOp, [&](Operation* op) { op->emitError() << "contains removed PIM local-memory planning attribute '" << name << "'";
op->emitError("requires complete non-negative PIM local-memory planning summary attributes");
}); });
hasFailure = true; hasFailure = true;
} }
coreLikeOp->walk([&](memref::AllocOp allocOp) {
auto addressAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemoryAddressAttrName);
auto slotAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemorySlotAttrName);
auto slotSizeAttr = allocOp->getAttrOfType<IntegerAttr>(kLocalMemorySlotSizeAttrName);
if (!addressAttr || !slotAttr || !slotSizeAttr || addressAttr.getInt() < 0 || slotAttr.getInt() < 0
|| slotSizeAttr.getInt() < 0) {
diagnostics.report(allocOp, [&](Operation*) {
allocOp.emitOpError("requires a complete non-negative PIM local-memory plan");
});
hasFailure = true;
return;
}
auto arenaAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemorySizeAttrName);
if (!arenaAttr || arenaAttr.getInt() < 0
|| static_cast<uint64_t>(arenaAttr.getInt()) > kPimLocalMemoryAddressLimit) {
diagnostics.report(coreLikeOp, [](Operation* op) {
op->emitError("requires one valid non-negative pim.local_memory_size attribute");
});
hasFailure = true;
}
if (hasFailure)
return failure();
uint64_t arenaSize = static_cast<uint64_t>(arenaAttr.getInt());
auto analyzed = pim::analyzeLocalMemoryLifetimes(coreLikeOp);
if (failed(analyzed)) {
diagnostics.report(coreLikeOp, [](Operation* op) {
op->emitError("cannot analyze PIM local-memory lifetimes for plan verification");
});
return failure();
}
struct Event {
uint64_t position;
bool start;
size_t intervalIndex;
};
SmallVector<Event, 0> events;
SmallVector<size_t, 0> addresses(analyzed->size());
for (auto indexed : llvm::enumerate(*analyzed)) {
size_t index = indexed.index();
auto& interval = indexed.value();
memref::AllocOp allocation = interval.allocation;
for (StringRef name : kRemovedLocalMemoryPlanAttrNames)
if (allocation->hasAttr(name)) {
diagnostics.report(allocation, [name](Operation* op) {
op->emitOpError() << "contains removed PIM local-memory planning attribute '" << name << "'";
});
hasFailure = true;
}
auto addressAttr = allocation->getAttrOfType<IntegerAttr>(kLocalMemoryAddressAttrName);
if (!addressAttr || addressAttr.getInt() < 0) {
diagnostics.report(allocation, [](Operation* op) {
op->emitOpError("requires one non-negative pim.local_memory_address attribute");
});
hasFailure = true;
continue;
}
uint64_t address = static_cast<uint64_t>(addressAttr.getInt()); uint64_t address = static_cast<uint64_t>(addressAttr.getInt());
uint64_t slotId = static_cast<uint64_t>(slotAttr.getInt()); if (address % 4 != 0 || address > arenaSize || interval.size > arenaSize - address) {
uint64_t slotSize = static_cast<uint64_t>(slotSizeAttr.getInt()); diagnostics.report(allocation, [&](Operation* op) {
auto logicalSize = pim::getCheckedShapedTypeSizeInBytes( op->emitOpError() << "has invalid PIM local-memory range [" << address << ", "
cast<ShapedType>(allocOp.getType()), allocOp, "planned local allocation byte size"); << (address <= arenaSize && interval.size <= arenaSize - address
bool invalidRange = failed(logicalSize) || address > slotSize || *logicalSize > slotSize - address ? address + interval.size
|| slotSize > kPimLocalMemoryAddressLimit || address % 4 != 0 || slotId != 0; : arenaSize)
if (invalidRange) { << ") for arena size " << arenaSize;
diagnostics.report(allocOp, [&](Operation*) {
allocOp.emitOpError() << "has an invalid PIM local-memory range: address=" << address
<< ", logical size=" << (succeeded(logicalSize) ? *logicalSize : 0)
<< ", slot size=" << slotSize;
}); });
hasFailure = true; hasFailure = true;
return; continue;
}
addresses[index] = static_cast<size_t>(address);
events.push_back({interval.start, true, index});
events.push_back({interval.end, false, index});
}
if (hasFailure)
return failure();
llvm::sort(events, [](const Event& lhs, const Event& rhs) {
return lhs.position != rhs.position ? lhs.position < rhs.position : lhs.start > rhs.start;
});
std::map<size_t, size_t> active;
for (const Event& event : events) {
size_t index = event.intervalIndex;
size_t address = addresses[index];
if (!event.start) {
auto it = active.find(address);
if (it != active.end() && it->second == index)
active.erase(it);
continue;
} }
auto [slotIt, inserted] = slots.try_emplace(slotId, VerifiedLocalMemorySlot {slotSize}); const auto& interval = (*analyzed)[index];
if (!inserted && slotIt->second.size != slotSize) { auto successor = active.lower_bound(address);
diagnostics.report(allocOp, [&](Operation*) { auto conflict = [&](std::map<size_t, size_t>::iterator it) {
allocOp.emitOpError() << "has inconsistent size for PIM local-memory arena " << slotId; if (it == active.end())
return false;
const auto& other = (*analyzed)[it->second];
return address < it->first + other.size && it->first < address + interval.size;
};
auto predecessor = successor;
bool hasPredecessor = predecessor != active.begin();
if (hasPredecessor)
--predecessor;
auto conflicting = conflict(successor) ? successor
: hasPredecessor && conflict(predecessor) ? predecessor : active.end();
if (conflicting != active.end()) {
const auto& other = (*analyzed)[conflicting->second];
memref::AllocOp allocation = interval.allocation;
memref::AllocOp otherAllocation = other.allocation;
diagnostics.report(allocation, [&](Operation*) {
auto diagnostic = allocation.emitOpError()
<< "PIM local-memory plan assigns simultaneously live allocations to overlapping ranges; first range ["
<< conflicting->first << ", " << conflicting->first + other.size << "), second range [" << address
<< ", " << address + interval.size << "), live positions overlap at ["
<< std::max(interval.start, other.start) << ", " << std::min(interval.end, other.end) << "]";
diagnostic.attachNote(otherAllocation.getLoc()) << "first allocation";
}); });
hasFailure = true; return failure();
} }
}); active.emplace(address, index);
return success(!hasFailure); }
return success();
} }
static bool isSupportedCoreInstructionOp(Operation* op) { static bool isSupportedCoreInstructionOp(Operation* op) {
@@ -16,6 +16,11 @@ namespace spatial {
namespace { namespace {
// Pressure means distinct weights exceed half the fleet's one-copy capacity.
// The reserved headroom conservatively avoids greedy capacity fragmentation;
// makespan, communication, instruction-path, and local-memory proxies remain stable.
constexpr size_t kHighCrossbarPressureCapacityDivisor = 2;
struct ScheduledTask { struct ScheduledTask {
size_t processor = std::numeric_limits<size_t>::max(); size_t processor = std::numeric_limits<size_t>::max();
Time startTime = 0; Time startTime = 0;
@@ -136,7 +141,7 @@ void verifyOctTableSize(size_t nodeCount, size_t processorCount) {
bool hasHighCrossbarPressure(const ComputeGraph& graph, size_t processorCount, size_t crossbarCapacity) { bool hasHighCrossbarPressure(const ComputeGraph& graph, size_t processorCount, size_t crossbarCapacity) {
if (crossbarCapacity > std::numeric_limits<size_t>::max() / processorCount) if (crossbarCapacity > std::numeric_limits<size_t>::max() / processorCount)
return false; return false;
const size_t threshold = processorCount * crossbarCapacity / 2; const size_t threshold = processorCount * crossbarCapacity / kHighCrossbarPressureCapacityDivisor;
CrossbarUsage distinctWeights; CrossbarUsage distinctWeights;
for (const ComputeGraphNode& node : graph.nodes) { for (const ComputeGraphNode& node : graph.nodes) {
for (const CrossbarWeight& weight : node.crossbarUsage) { for (const CrossbarWeight& weight : node.crossbarUsage) {
-2
View File
@@ -15,8 +15,6 @@ std::unique_ptr<mlir::Pass> createSpatialToPimPass();
std::unique_ptr<mlir::Pass> createPimBufferizationPass(); std::unique_ptr<mlir::Pass> createPimBufferizationPass();
std::unique_ptr<mlir::Pass> createPimMemoryCoalescingPass();
std::unique_ptr<mlir::Pass> createMergeComputeNodesPass(); std::unique_ptr<mlir::Pass> createMergeComputeNodesPass();
std::unique_ptr<mlir::Pass> createTrivialGraphComputeMergePass(); std::unique_ptr<mlir::Pass> createTrivialGraphComputeMergePass();
-1
View File
@@ -76,7 +76,6 @@ void PimAccelerator::registerPasses(int optLevel) const {
registerPass(createLowerSpatialPlansPass); registerPass(createLowerSpatialPlansPass);
registerPass(createSpatialToPimPass); registerPass(createSpatialToPimPass);
registerPass(createPimBufferizationPass); registerPass(createPimBufferizationPass);
registerPass(createPimMemoryCoalescingPass);
registerPass(createTrivialGraphComputeMergePass); registerPass(createTrivialGraphComputeMergePass);
registerPass(createMergeComputeNodesPass); registerPass(createMergeComputeNodesPass);
registerPass(createPimHostConstantFoldingPass); registerPass(createPimHostConstantFoldingPass);
+47 -53
View File
@@ -4,103 +4,97 @@
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning/LocalMemoryPlanning.hpp" #include "src/Accelerators/PIM/Dialect/Pim/Transforms/LocalMemoryPlanning/LocalMemoryPlanning.hpp"
using onnx_mlir::LocalAllocInterval; using onnx_mlir::LocalMemoryPlacement;
using onnx_mlir::CoreMemoryPlan; using onnx_mlir::pim::LocalMemoryInterval;
using onnx_mlir::assignPhysicalSlotAddresses; using onnx_mlir::planLocalMemoryPlacements;
using onnx_mlir::planPhysicalSlots;
namespace { namespace {
LocalAllocInterval makeInterval(size_t id, size_t size, uint64_t start, uint64_t end) { LocalMemoryInterval makeInterval(size_t size, uint64_t start, uint64_t end) {
LocalAllocInterval interval; LocalMemoryInterval interval;
interval.id = id;
interval.size = size; interval.size = size;
interval.start = start; interval.start = start;
interval.end = end; interval.end = end;
return interval; return interval;
} }
size_t getPeak(llvm::ArrayRef<onnx_mlir::PlannedPhysicalSlot> slots) { size_t getPeak(llvm::ArrayRef<LocalMemoryPlacement> placements) {
size_t peak = 0; size_t peak = 0;
for (const auto& slot : slots) for (const auto& placement : placements)
peak = std::max(peak, slot.address + slot.requiredSize); peak = std::max(peak, placement.address + placement.size);
return peak; return peak;
} }
void assertSingleSlotCase(LocalAllocInterval a, LocalAllocInterval b, size_t expectedSlotSize) { void assertSinglePlacementCase(LocalMemoryInterval a, LocalMemoryInterval b, size_t expectedSize) {
llvm::SmallVector<LocalAllocInterval, 4> intervals = {a, b}; llvm::SmallVector<LocalMemoryInterval, 4> intervals = {a, b};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(slots.size() == 1); assert(mlir::succeeded(placements));
assert(slots.front().requiredSize == expectedSlotSize); assert(placements->size() == 1);
assert(intervals[0].slotPlanIndex == intervals[1].slotPlanIndex); assert(placements->front().size == expectedSize);
assert(placements->front().intervalIndices.size() == 2);
} }
int testSameSizeNonOverlap() { int testSameSizeNonOverlap() {
std::cout << "testSameSizeNonOverlap:" << std::endl; std::cout << "testSameSizeNonOverlap:" << std::endl;
assertSingleSlotCase(makeInterval(0, 64, 0, 10), makeInterval(1, 64, 11, 20), 64); assertSinglePlacementCase(makeInterval(64, 0, 10), makeInterval(64, 11, 20), 64);
return 0; return 0;
} }
int testLargerFirst() { int testLargerFirst() {
std::cout << "testLargerFirst:" << std::endl; std::cout << "testLargerFirst:" << std::endl;
llvm::SmallVector<LocalAllocInterval, 4> intervals = { llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
makeInterval(0, 100, 0, 10), makeInterval(1, 40, 11, 20)}; makeInterval(100, 0, 10), makeInterval(40, 11, 20)};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(slots.size() == 2); assert(mlir::succeeded(placements) && placements->size() == 2);
assert(getPeak(slots) == 100); assert(getPeak(*placements) == 100);
return 0; return 0;
} }
int testSmallerFirst() { int testSmallerFirst() {
std::cout << "testSmallerFirst:" << std::endl; std::cout << "testSmallerFirst:" << std::endl;
llvm::SmallVector<LocalAllocInterval, 4> intervals = { llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
makeInterval(0, 40, 0, 10), makeInterval(1, 100, 11, 20)}; makeInterval(40, 0, 10), makeInterval(100, 11, 20)};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(slots.size() == 2); assert(mlir::succeeded(placements) && placements->size() == 2);
assert(getPeak(slots) == 100); assert(getPeak(*placements) == 100);
return 0; return 0;
} }
int testOverlapNeedsTwoSlots() { int testOverlapNeedsTwoSlots() {
std::cout << "testOverlapNeedsTwoSlots:" << std::endl; std::cout << "testOverlapNeedsTwoSlots:" << std::endl;
llvm::SmallVector<LocalAllocInterval, 4> intervals = { llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
makeInterval(0, 100, 0, 20), makeInterval(1, 40, 10, 30)}; makeInterval(100, 0, 20), makeInterval(40, 10, 30)};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(slots.size() == 2); assert(mlir::succeeded(placements) && placements->size() == 2);
assert(intervals[0].slotPlanIndex != intervals[1].slotPlanIndex); assert((*placements)[0].address != (*placements)[1].address);
return 0; return 0;
} }
int testReuseChain() { int testReuseChain() {
std::cout << "testReuseChain:" << std::endl; std::cout << "testReuseChain:" << std::endl;
llvm::SmallVector<LocalAllocInterval, 4> intervals = { llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
makeInterval(0, 40, 0, 10), makeInterval(1, 100, 11, 20), makeInterval(2, 20, 21, 30)}; makeInterval(40, 0, 10), makeInterval(100, 11, 20), makeInterval(20, 21, 30)};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(slots.size() == 3); assert(mlir::succeeded(placements) && placements->size() == 3);
assert(getPeak(slots) == 100); assert(getPeak(*placements) == 100);
return 0; return 0;
} }
int testPartialAddressReuse() { int testPartialAddressReuse() {
std::cout << "testPartialAddressReuse:" << std::endl; std::cout << "testPartialAddressReuse:" << std::endl;
llvm::SmallVector<LocalAllocInterval, 4> intervals = { llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
makeInterval(0, 100, 0, 10), makeInterval(1, 60, 11, 20), makeInterval(2, 40, 11, 20)}; makeInterval(100, 0, 10), makeInterval(60, 11, 20), makeInterval(40, 11, 20)};
auto slots = planPhysicalSlots(intervals); auto placements = planLocalMemoryPlacements(intervals, 1024);
assert(getPeak(slots) == 100); assert(mlir::succeeded(placements));
assert(getPeak(*placements) == 100);
return 0; return 0;
} }
int testAddressAssignment() { int testAddressLimit() {
std::cout << "testAddressAssignment:" << std::endl; std::cout << "testAddressLimit:" << std::endl;
CoreMemoryPlan plan; llvm::SmallVector<LocalMemoryInterval, 4> intervals = {
plan.intervals = {makeInterval(0, 100, 0, 20), makeInterval(1, 40, 10, 30)}; makeInterval(100, 0, 20), makeInterval(40, 10, 30)};
plan.slots = planPhysicalSlots(plan.intervals); assert(mlir::failed(planLocalMemoryPlacements(intervals, 128)));
assert(mlir::succeeded(assignPhysicalSlotAddresses(plan, 1024)));
assert(plan.slots.size() == 2);
const auto& firstSlot = plan.slots[plan.intervals[0].slotPlanIndex];
const auto& secondSlot = plan.slots[plan.intervals[1].slotPlanIndex];
assert(firstSlot.address == 0 && firstSlot.requiredSize == 100);
assert(secondSlot.address == 100 && secondSlot.requiredSize == 40);
return 0; return 0;
} }
@@ -117,7 +111,7 @@ int main(int argc, char *argv[]) {
failures += testOverlapNeedsTwoSlots(); failures += testOverlapNeedsTwoSlots();
failures += testReuseChain(); failures += testReuseChain();
failures += testPartialAddressReuse(); failures += testPartialAddressReuse();
failures += testAddressAssignment(); failures += testAddressLimit();
if (failures != 0) { if (failures != 0) {
std::cerr << failures << " test failures\n"; std::cerr << failures << " test failures\n";
return EXIT_FAILURE; return EXIT_FAILURE;
+1 -4
View File
@@ -11,7 +11,6 @@ PIM_PASS_LABELS = (
("SpatialToPimPass", "Spatial to PIM"), ("SpatialToPimPass", "Spatial to PIM"),
("PimBufferizationPass", "Bufferize PIM"), ("PimBufferizationPass", "Bufferize PIM"),
("HostConstantFoldingPass", "Fold Host Constants"), ("HostConstantFoldingPass", "Fold Host Constants"),
("PimMemoryCoalescingPass", "Coalesce Local Memory"),
("PimLocalMemoryPlanningPass", "Plan Local Memory"), ("PimLocalMemoryPlanningPass", "Plan Local Memory"),
("VerificationPass", "Verify PIM"), ("VerificationPass", "Verify PIM"),
("EmitPimCodePass", "Emit PIM Code"), ("EmitPimCodePass", "Emit PIM Code"),
@@ -43,7 +42,7 @@ def _format_command(cmd):
def compile_with_raptor(network_path, raptor_onnx_path: Path, output_base: Path, def compile_with_raptor(network_path, raptor_onnx_path: Path, output_base: Path,
crossbar_size, crossbar_count, core_count=None, crossbar_size, crossbar_count, core_count=None,
pim_memory_report="none", raptor_extra_args=None, cwd=None, verbose=False, raptor_extra_args=None, cwd=None, verbose=False,
reporter=None, timeout_sec=None): reporter=None, timeout_sec=None):
# Define the arguments, with the possibility to set crossbar size and count # Define the arguments, with the possibility to set crossbar size and count
args = [ args = [
@@ -57,8 +56,6 @@ def compile_with_raptor(network_path, raptor_onnx_path: Path, output_base: Path,
] ]
if core_count is not None: if core_count is not None:
args.append(f"--core-count={core_count}") args.append(f"--core-count={core_count}")
if pim_memory_report != "none":
args.append(f"--pim-memory-report={pim_memory_report}")
if raptor_extra_args: if raptor_extra_args:
args.extend(str(arg) for arg in raptor_extra_args) args.extend(str(arg) for arg in raptor_extra_args)
if verbose: if verbose:
-3
View File
@@ -75,8 +75,6 @@ def main():
ap.add_argument("--crossbar-size", type=int, default=128) ap.add_argument("--crossbar-size", type=int, default=128)
ap.add_argument("--crossbar-count", type=int, default=64) ap.add_argument("--crossbar-count", type=int, default=64)
ap.add_argument("--core-count", type=int, default=144) ap.add_argument("--core-count", type=int, default=144)
ap.add_argument("--pim-memory-report", choices=("none", "summary", "full"), default="none",
help="Emit a human-readable PIM memory planning report during codegen.")
ap.add_argument("--raptor-extra-arg", action="append", default=[], ap.add_argument("--raptor-extra-arg", action="append", default=[],
help="Additional argument to pass through to the Raptor compiler. Repeat as needed.") help="Additional argument to pass through to the Raptor compiler. Repeat as needed.")
ap.add_argument("--command-timeout-seconds", type=float, default=1000000.0, ap.add_argument("--command-timeout-seconds", type=float, default=1000000.0,
@@ -144,7 +142,6 @@ def main():
result = validate_network( result = validate_network(
onnx_path, a.raptor_path, a.onnx_include_dir, simulator_dir, onnx_path, a.raptor_path, a.onnx_include_dir, simulator_dir,
crossbar_size=a.crossbar_size, crossbar_count=a.crossbar_count, core_count=a.core_count, crossbar_size=a.crossbar_size, crossbar_count=a.crossbar_count, core_count=a.core_count,
pim_memory_report=a.pim_memory_report,
raptor_extra_args=a.raptor_extra_arg, raptor_extra_args=a.raptor_extra_arg,
command_timeout_seconds=a.command_timeout_seconds, command_timeout_seconds=a.command_timeout_seconds,
threshold=a.threshold, threshold=a.threshold,
+3 -3
View File
@@ -312,7 +312,7 @@ def validate_outputs(sim_arrays, runner_out_dir, outputs_descriptor, threshold=1
def validate_network(network_onnx_path, raptor_path, onnx_include_dir, def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
simulator_dir, crossbar_size=128, crossbar_count=64, core_count=144, simulator_dir, crossbar_size=128, crossbar_count=64, core_count=144,
pim_memory_report="none", raptor_extra_args=None, raptor_extra_args=None,
threshold=1e-3, rtol=1e-5, threshold=1e-3, rtol=1e-5,
seed=0, reporter=None, model_index=1, model_total=1, verbose=False, seed=0, reporter=None, model_index=1, model_total=1, verbose=False,
command_timeout_seconds=60.0, mode=MODE_FULL): command_timeout_seconds=60.0, mode=MODE_FULL):
@@ -367,7 +367,7 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
pim_pass_timings = compile_with_raptor( pim_pass_timings = compile_with_raptor(
network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count, network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count, core_count=core_count,
pim_memory_report=pim_memory_report, raptor_extra_args=raptor_extra_args, raptor_extra_args=raptor_extra_args,
cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds) cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds)
print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}") print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}")
reporter.advance() reporter.advance()
@@ -407,7 +407,7 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
pim_pass_timings = compile_with_raptor( pim_pass_timings = compile_with_raptor(
network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count, network_onnx_path, raptor_path, pim_output_base, crossbar_size, crossbar_count,
core_count=core_count, core_count=core_count,
pim_memory_report=pim_memory_report, raptor_extra_args=raptor_extra_args, raptor_extra_args=raptor_extra_args,
cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds) cwd=raptor_dir, verbose=verbose, reporter=reporter, timeout_sec=command_timeout_seconds)
print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}") print_info(reporter, f"PIM artifacts saved to {raptor_dir / 'pim'}")
reporter.advance() reporter.advance()