finally fast googlenet with correct latency artifacts for fair comparison
Validate Operations / validate-operations (push) Has been cancelled

This commit is contained in:
NiccoloN
2026-07-29 18:20:44 +02:00
parent 060a21172e
commit 1b4f070bef
74 changed files with 2773 additions and 1311 deletions
@@ -0,0 +1,73 @@
# Spatial Target Generality Invariant
## Scope
This invariant applies to:
- the Spatial dialect and its verifiers;
- ONNX-to-Spatial planning and lowering;
- graph transforms and scheduling over Spatial IR;
- target information consumed while optimizing Spatial IR;
- lowerings from Spatial to target-specific dialects.
## Invariant
Spatial represents logical compute, dataflow, layout choices, parallel work,
and target-independent resource requirements. It must remain usable by
different targets, including PIM and future targets such as PULPIM.
Raptor may ingest target information and use it to choose Spatial layouts,
partitions, placements, or schedules. That information must cross an explicit
target interface and be expressed in target-neutral terms at the Spatial
layer. The Spatial dialect and scheduler must not parse a simulator-specific
configuration, depend on a target dialect, or encode one target's instruction
latencies, memory hierarchy, communication protocol, or resource policy.
Target adapters own translating a target configuration into the neutral
information consumed by Spatial. Target-specific dialects and their lowerings
own instruction semantics, physical memory details, communication mechanisms,
and final legality.
## Ownership boundary
- Spatial IR owns logical and physical planning concepts shared across targets.
- A target adapter owns configuration parsing and cost-model construction.
- The scheduler consumes an injected cost/resource model; it does not infer a
target from global PIM options or hardcoded constants.
- Spatial-to-target lowering makes the selected representation explicit.
- Target dialect verifiers reject target-specific illegal states.
Target-neutral information may include processor topology, operation and
transfer cost queries, available parallel capacity, and opaque resource
requirements. Names and APIs at this boundary must describe those concepts,
not a particular simulator or target implementation.
## Forbidden coupling
Do not:
- include PIM or PULPIM dialect headers in the Spatial dialect or scheduler;
- read PIM compiler globals directly from generic scheduling algorithms;
- parse `pimsim-nn`, PIMCOMP, or another simulator's schema in Spatial code;
- hardcode Arch-A timing, mesh, crossbar, memory, or vector constants in
Spatial cost calculations;
- add target-named Spatial operations when an existing logical/layout concept
expresses the invariant;
- repair target-specific legality in generic Spatial cleanup passes.
## Required proof
Changes that use target information in Spatial must show:
- the target boundary or injected interface used;
- that Spatial IR remains valid without target-specific attributes;
- that an unknown or unsupported target fails clearly rather than silently
using PIM defaults;
- unit coverage with at least two distinct target profiles when scheduling or
cost decisions change;
- target-specific validation after lowering for every implemented target
affected by the change.
If only one target implementation exists, keep the interface narrow and test
it with two profiles. Do not add speculative target operations or a framework
for unimplemented targets.
+1
View File
@@ -6,6 +6,7 @@ Before modifying the relevant subsystem, read:
* `.agents/invariants/GRAPH_COMPUTE_BATCH_INVARIANT.md`
* `.agents/invariants/PERFORMANCE_OPTIMIZATION_INVARIANT.md`
* `.agents/invariants/SPATIAL_TARGET_GENERALITY_INVARIANT.md`
* Build commands:
* `cmake --build ./build_release`
* `cmake --build ./build_debug`
+4 -1
View File
@@ -100,6 +100,9 @@ options; `onnx-mlir --help` lists the inherited ONNX-MLIR options.
- `--core-count=<N>` - required positive core count for PIM compilation.
- `--crossbar-size=<N>` - crossbar width/height. Default in code is `128`.
- `--crossbar-count=<N>` - crossbars per core. Default in code is `64`.
- `--pim-target-config=<PATH>` - optional PIM target configuration used by the
target adapter to construct the target-neutral Spatial scheduling cost and
topology model. Resource values must match the explicit core/crossbar flags.
- `--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
@@ -155,7 +158,7 @@ This writes PIM artifacts under `/tmp/raptor/pim/`.
## Validation
Functional validation compiles ONNX models, compares native ONNX-MLIR and PIM
simulator outputs, and optionally reports latency and power. See
simulator outputs, and optionally reports latency, power, and energy. See
[`validation/README.md`](validation/README.md) for prerequisites, usage,
options, artifacts, and results.
@@ -16,7 +16,7 @@ use anyhow::{Context, Result, ensure};
use rayon::prelude::*;
use paste::paste;
use std::{borrow::Cow, cell::OnceCell, collections::HashMap };
use std::{borrow::Cow, cell::OnceCell, collections::HashMap, mem::size_of};
use std::{collections::HashSet, sync::LazyLock};
macro_rules! add_name {
@@ -170,8 +170,6 @@ macro_rules! add_simd_to_map {
tmp.insert((32_usize,64_usize), ([<$id _impl>]::<f32, f64> as InstructionType));
tmp.insert((64_usize,32_usize), ([<$id _impl>]::<f64, f32> as InstructionType));
tmp.insert((64_usize,64_usize), ([<$id _impl>]::<f64, f64> as InstructionType));
//TODO WTF WHY
tmp.insert((8_usize,8_usize), ([<$id _impl>]::<f32, f32> as InstructionType));
$storage.insert($id as *const () as usize, tmp);
}
@@ -224,6 +222,14 @@ pub fn is_setbw(functor: InstructionType) -> bool {
functor as usize == setbw as *const () as usize
}
fn vector_lengths<F>(imm_len: i32) -> Result<(usize, usize)> {
let element_count: usize = imm_len.try_into().context("imm_len can not be negative")?;
let byte_len = element_count
.checked_mul(size_of::<F>())
.context("vector byte length overflow")?;
Ok((element_count, byte_len))
}
#[inline(never)]
pub fn setbw(cores: &mut CPU, data: InstructionData) -> Result<InstructionStatus> {
panic!("You are calling a placeholder, this instruction is resolved in the construction phase");
@@ -361,11 +367,11 @@ where
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let r2_val = add_offset_r2(r2_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let imm_len: usize = imm_len.try_into().context("imm_len can not be negative")?;
let (element_count, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core
.reserve_load(r1_val, imm_len)?
.reserve_load(r2_val, imm_len)?
.reserve_load(r1_val, byte_len)?
.reserve_load(r2_val, byte_len)?
.execute_load::<F>()?;
let (load1, load2) = (loads[0], loads[1]);
let res: Vec<F> = load1
@@ -374,7 +380,7 @@ where
.map(|(&a, &b)| a + b)
.collect();
ensure!(
imm_len / size_of::<F>() == res.len(),
element_count == res.len(),
"vvadd generate a vector bigger thant it's requested elements"
);
let res_up: Cow<[T]> = res.as_slice().up();
@@ -405,11 +411,11 @@ where
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let r2_val = add_offset_r2(r2_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let imm_len: usize = imm_len.try_into().context("imm_len can not be negative")?;
let (element_count, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core
.reserve_load(r1_val, imm_len)?
.reserve_load(r2_val, imm_len)?
.reserve_load(r1_val, byte_len)?
.reserve_load(r2_val, byte_len)?
.execute_load::<F>()?;
let (load1, load2) = (loads[0], loads[1]);
let res: Vec<F> = load1
@@ -418,7 +424,7 @@ where
.map(|(&a, &b)| a - b)
.collect();
ensure!(
imm_len / size_of::<F>() == res.len(),
element_count == res.len(),
"vvadd generate a vector bigger thant it's requested elements"
);
let res_up: Cow<[T]> = res.as_slice().up();
@@ -447,10 +453,10 @@ where
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let r2_val = add_offset_r2(r2_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let imm_len: usize = imm_len.try_into().context("imm_len can not be negative")?;
let (element_count, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core
.reserve_load(r1_val, imm_len)?
.reserve_load(r2_val, imm_len)?
.reserve_load(r1_val, byte_len)?
.reserve_load(r2_val, byte_len)?
.execute_load::<F>()?;
let (load1, load2) = (loads[0], loads[1]);
let res: Vec<F> = load1
@@ -459,7 +465,7 @@ where
.map(|(&a, &b)| a * b)
.collect();
ensure!(
imm_len / size_of::<F>() == res.len(),
element_count == res.len(),
"vvadd generate a vector bigger thant it's requested elements"
);
let res_up: Cow<[T]> = res.as_slice().up();
@@ -488,9 +494,10 @@ where
let rd_val = core.register(rd);
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let r2_val = add_offset_r2(r2_val, offset_select, offset_value);
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core
.reserve_load(r1_val, imm_len)?
.reserve_load(r2_val, imm_len)?
.reserve_load(r1_val, byte_len)?
.reserve_load(r2_val, byte_len)?
.execute_load::<F>()?;
let (load1, load2) = (loads[0], loads[1]);
let res: [F; 1] = [load1
@@ -527,10 +534,11 @@ where
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let r2_val = add_offset_r2(r2_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core
.reserve_load(r1_val, imm_len)?
.reserve_load(r2_val, imm_len)?
.reserve_load(r1_val, byte_len)?
.reserve_load(r2_val, byte_len)?
.execute_load::<F>()?;
let (load1, load2) = (loads[0], loads[1]);
let res: Vec<F> = load1
@@ -583,7 +591,8 @@ where
"Offset select cannot be different from 1"
);
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let loads = core.reserve_load(r1_val, imm_len)?.execute_load::<F>()?;
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core.reserve_load(r1_val, byte_len)?.execute_load::<F>()?;
let load1 = loads[0];
let len = load1.len();
let res: [F; _] =
@@ -613,7 +622,8 @@ where
let rd_val = core.register(rd);
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let loads = core.reserve_load(r1_val, imm_len)?.execute_load::<F>()?;
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core.reserve_load(r1_val, byte_len)?.execute_load::<F>()?;
let load1 = loads[0];
let res: Vec<F> = load1
.iter()
@@ -646,7 +656,8 @@ where
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let loads = core.reserve_load(r1_val, imm_len)?.execute_load::<F>()?;
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core.reserve_load(r1_val, byte_len)?.execute_load::<F>()?;
let load1 = loads[0];
let res: Vec<F> = load1.iter().map(|&a| a.tanh()).collect();
@@ -675,7 +686,8 @@ where
let rd_val = core.register(rd);
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let loads = core.reserve_load(r1_val, imm_len)?.execute_load::<F>()?;
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core.reserve_load(r1_val, byte_len)?.execute_load::<F>()?;
let load1 = loads[0];
let res: Vec<F> = load1.iter().map(|&a| a.sigm()).collect();
let res_up: Cow<[T]> = res.as_slice().up();
@@ -706,7 +718,8 @@ where
let rd_val = core.register(rd);
let r1_val = add_offset_r1(r1_val, offset_select, offset_value);
let rd_val = add_offset_rd(rd_val, offset_select, offset_value);
let loads = core.reserve_load(r1_val, imm_len)?.execute_load::<F>()?;
let (_, byte_len) = vector_lengths::<F>(imm_len)?;
let loads = core.reserve_load(r1_val, byte_len)?.execute_load::<F>()?;
let load1 = loads[0];
ensure!(!load1.is_empty(), "vsoftmax does not support empty vectors");
let max_val = load1
@@ -1,5 +1,5 @@
use crate::{tracing::trace::pretty_print, utility::add_offset_r2};
use std::fs::File;
use std::{fs::File, mem::size_of};
use crate::{
cpu::CPU,
@@ -381,7 +381,15 @@ impl Trace {
writeln!(file, "\trs1({}): {}", r1, r1_val);
writeln!(file, "\trs2({}): {}", r2, r2_val);
writeln!(file, "{} Immediate:", prefix);
writeln!(file, "\tLoad Length: {} bytes", imm_len);
let element_count: usize = imm_len.try_into().expect("imm_len can not be negative");
let byte_len = element_count
.checked_mul(size_of::<F>())
.expect("vector byte length overflow");
writeln!(
file,
"\tLoad Length: {} elements ({} bytes)",
element_count, byte_len
);
writeln!(
file,
"\toffset_select: {} offset_value: {}",
@@ -390,22 +398,21 @@ impl Trace {
let r1_final = add_offset_r1(r1_val, offset_select, offset_value);
let r2_final = add_offset_r2(r2_val, offset_select, offset_value);
let rd_final = add_offset_rd(rd_val, offset_select, offset_value);
let imm_len: usize = imm_len.try_into().expect("imm_len can not be negative");
let loads = core
.reserve_load(r1_final, imm_len)
.reserve_load(r1_final, byte_len)
.unwrap()
.reserve_load(r2_final, imm_len)
.reserve_load(r2_final, byte_len)
.unwrap()
.reserve_load(rd_final, imm_len)
.reserve_load(rd_final, byte_len)
.unwrap()
.execute_load::<F>()
.unwrap();
writeln!(file, "{} Memory:", prefix);
write!(file, "\tLocal[{}:{}](A): ", r1_final, r1_final + imm_len);
write!(file, "\tLocal[{}:{}](A): ", r1_final, r1_final + byte_len);
pretty_print::print_slice::<_,f32>(file, loads[0], 30);
write!(file, "\tLocal[{}:{}](B): ", r2_final , r2_final + imm_len);
write!(file, "\tLocal[{}:{}](B): ", r2_final , r2_final + byte_len);
pretty_print::print_slice::<_,f32>(file, loads[1], 30);
write!(file, "\tLocal[{}:{}](out): ", rd_final, rd_final+ imm_len);
write!(file, "\tLocal[{}:{}](out): ", rd_final, rd_final+ byte_len);
pretty_print::print_slice::<_,f32>(file, loads[2], 30);
if prefix == "Post" {
writeln!(file, "\n###############################################\n");
@@ -25,7 +25,7 @@ fn wrong_size_place_holder() {
vvadd,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<f32>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -33,6 +33,25 @@ fn wrong_size_place_holder() {
executable.execute();
}
#[test]
#[should_panic(expected = "Function not found for the requested size input:8 output:8")]
fn unsupported_8_bit_vectors_do_not_alias_f32() {
let mut inst_builder = InstructionsBuilder::new();
let mut idata_build = InstructionDataBuilder::new();
idata_build.set_core_indx(0).fix_core_indx();
inst_builder.make_inst(
setbw,
idata_build.set_ibiw_obiw(8, 8).build(),
);
inst_builder.make_inst(
vvadd,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8)
.build(),
);
}
fn place_holder(inst : InstructionType) {
@@ -55,7 +55,7 @@ where
vvadd,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -159,7 +159,7 @@ where
vvsub,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -263,7 +263,7 @@ where
vvmul,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -367,7 +367,7 @@ where
vvdmul,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -464,7 +464,7 @@ where
vvmax,
idata_build
.set_rdr1r2(3, 1, 2)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -565,7 +565,7 @@ where
idata_build
.set_rdr1r2(3, 1, 1)
.set_offset_select(1)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -658,7 +658,7 @@ where
vrelu,
idata_build
.set_rdr1r2(3, 1, 1)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -758,7 +758,7 @@ where
vtanh,
idata_build
.set_rdr1r2(3, 1, 1)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
@@ -860,7 +860,7 @@ where
vsigm,
idata_build
.set_rdr1r2(3, 1, 1)
.set_imm_len(8 * size_of::<F>() as i32)
.set_imm_len(8)
.build(),
);
let core_instruction = vec![inst_builder.build().into()];
+94 -109
View File
@@ -92,11 +92,12 @@ static MemoryReportKind classifyMemoryReportKind(mlir::Value value) {
return MemoryReportKind::None;
}
static int32_t getVectorByteSizeOrCrash(ShapedType type) {
auto byteSize = pim::getCheckedShapedTypeSizeInBytes(type, UnknownLoc::get(type.getContext()), "vector byte size");
if (failed(byteSize))
llvm_unreachable("Failed to compute checked vector byte size");
return pim::checkedI32OrCrash(*byteSize, "vector byte size");
static int32_t getVectorElementCountOrCrash(ShapedType type) {
return pim::checkedI32OrCrash(type.getNumElements(), "vector element count");
}
static int32_t getVectorElementBitwidthOrCrash(ShapedType type) {
return pim::checkedI32OrCrash(static_cast<uint64_t>(type.getElementTypeBitWidth()), "vector element bitwidth");
}
static Operation* getDiagnosticAnchor(mlir::Value value) {
@@ -165,8 +166,7 @@ size_t PimMemory::allocateAddress(size_t size, const MemoryValueKey& key) {
checkedAlignedEnd = checkedAlignTo(*checkedEnd, minAlignment, anchor, "local memory alignment");
if (address > kPimLocalMemoryAddressLimit || failed(checkedEnd) || *checkedEnd > kPimLocalMemoryAddressLimit
|| failed(checkedAlignedEnd) || *checkedAlignedEnd > kPimLocalMemoryAddressLimit) {
printMemoryOverflowDiagnostic(
key,
printMemoryOverflowDiagnostic(key,
size,
firstAvailableAddress,
succeeded(checkedAlignedEnd) ? *checkedAlignedEnd : kPimLocalMemoryAddressLimit);
@@ -208,9 +208,7 @@ void PimMemory::allocateMemoryForValue(const MemoryValueKey& key, MemEntry& memE
switch (reportKind) {
case MemoryReportKind::Alloca:
case MemoryReportKind::Global:
case MemoryReportKind::Input:
++reportRow.hostObjectCount;
break;
case MemoryReportKind::Input: ++reportRow.hostObjectCount; break;
case MemoryReportKind::None: break;
}
}
@@ -259,8 +257,7 @@ void PimMemory::allocateCore(const CompiledCoreMemoryPlan& plan, std::optional<u
reportRow.logicalLocalAllocationCount = plan.logicalAllocationCount;
reportRow.logicalLocalBytes = plan.logicalBytes;
}
else if (*localArenaSize != plan.arenaSize
|| reportRow.logicalLocalAllocationCount != plan.logicalAllocationCount
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) {
@@ -358,8 +355,8 @@ llvm::FailureOr<int64_t> PimAcceleratorMemory::getIndexValue(mlir::Value value,
PimAcceleratorMemory::PimAcceleratorMemory()
: hostMem(memEntriesMap), fileReport(openMemoryReport(pimMemoryReport == PimMemoryReportSummary)) {}
PimAcceleratorMemory::PimAcceleratorMemory(
const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport)
PimAcceleratorMemory::PimAcceleratorMemory(const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries,
bool enableReport)
: memEntriesMap(initialMemEntries),
hostMem(memEntriesMap),
fileReport(enableReport ? openMemoryReport(true) : std::fstream()) {}
@@ -367,10 +364,8 @@ PimAcceleratorMemory::PimAcceleratorMemory(
void PimAcceleratorMemory::reportHost() { hostReportRow = hostMem.getReportRow(); }
void PimAcceleratorMemory::recordCoreReport(size_t coreId, const MemoryReportRow& row) {
reportEntries.push_back({MemoryReportEntry::Kind::Core,
coreId,
{pim::checkedI32OrCrash(coreId, "memory report core id")},
row});
reportEntries.push_back(
{MemoryReportEntry::Kind::Core, coreId, {pim::checkedI32OrCrash(coreId, "memory report core id")}, row});
}
void PimAcceleratorMemory::recordBatchReport(uint64_t batchId,
@@ -441,8 +436,7 @@ void PimAcceleratorMemory::flushReport() {
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 << " Saved local memory: " << formatReportMemory(savedBytes) << " (" << formatv("{0:F1}%", savedPercent) << ")\n";
os << " Largest core local memory: " << formatReportMemory(largest) << "\n";
if (!groups.empty()) {
os << " ";
@@ -456,20 +450,15 @@ void PimAcceleratorMemory::flushReport() {
printLabel(group);
os << "\n";
uint64_t groupSaved = group.row.logicalLocalBytes - group.row.physicalLocalBytes;
double groupPercent = group.row.logicalLocalBytes == 0
? 0.0
: 100.0 * groupSaved / group.row.logicalLocalBytes;
double groupPercent = group.row.logicalLocalBytes == 0 ? 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";
<< formatReportMemory(group.row.physicalLocalBytes) << " (" << formatv("{0:F1}% saved", groupPercent) << ")\n";
}
else {
os << " Per core: " << formatReportMemory(group.row.logicalLocalBytes) << ""
<< formatReportMemory(group.row.physicalLocalBytes) << " (" << formatv("{0:F1}% saved", groupPercent)
<< ")\n";
os << " Total after reuse: " << formatReportMemory(group.row.physicalLocalBytes * group.coreIds.size())
<< "\n";
<< formatReportMemory(group.row.physicalLocalBytes) << " (" << formatv("{0:F1}% saved", groupPercent) << ")\n";
os << " Total after reuse: " << formatReportMemory(group.row.physicalLocalBytes * group.coreIds.size()) << "\n";
}
}
if (groups.size() > kGroupLimit)
@@ -479,12 +468,6 @@ void PimAcceleratorMemory::flushReport() {
fileReport.close();
}
size_t PimCodeGen::remapCoreId(size_t coreId) const {
auto it = emittedCoreIds.find(coreId);
assert(it != emittedCoreIds.end() && "Missing emitted core id remapping");
return it->second;
}
void PimCodeGen::emitInstruction(const pim_binary::InstructionRecord& instruction) const {
if (failed(instructionWriter.append(instruction)))
return;
@@ -493,6 +476,19 @@ void PimCodeGen::emitInstruction(const pim_binary::InstructionRecord& instructio
updateScalarRegisterCache(instruction);
}
void PimCodeGen::ensureVectorBitwidth(int32_t inputBitwidth, int32_t outputBitwidth) const {
std::array<int32_t, 2> requested = {inputBitwidth, outputBitwidth};
if (vectorBitwidths == requested)
return;
pim_binary::InstructionRecord instruction;
instruction.opcode = pim_binary::Opcode::setbw;
instruction.generic1 = inputBitwidth;
instruction.generic2 = outputBitwidth;
emitInstruction(instruction);
vectorBitwidths = requested;
}
void PimCodeGen::updateScalarRegisterCache(const pim_binary::InstructionRecord& instruction) const {
switch (instruction.opcode) {
case pim_binary::Opcode::sldi: scalarRegisterValues[instruction.rd] = instruction.r2OrImm; break;
@@ -563,7 +559,7 @@ void PimCodeGen::emitCommunicationOp(pim_binary::Opcode opcode, size_t bufferAdd
pim_binary::InstructionRecord instruction;
instruction.opcode = opcode;
instruction.rd = 0;
instruction.r2OrImm = pim::checkedI32OrCrash(remapCoreId(coreId), "communication core id");
instruction.r2OrImm = pim::checkedI32OrCrash(coreId, "physical communication core id");
instruction.generic1 = 0;
instruction.generic2 = 0;
instruction.generic3 = pim::checkedI32OrCrash(size, "communication byte size");
@@ -679,6 +675,8 @@ void PimCodeGen::codeGenMVMLikeOp(size_t mvmId,
MVMTy mvmLikeOp,
bool transposeMatrix,
const StaticValueKnowledge& knowledge) {
ensureVectorBitwidth(getVectorElementBitwidthOrCrash(cast<ShapedType>(mvmLikeOp.getInput().getType())),
getVectorElementBitwidthOrCrash(cast<ShapedType>(mvmLikeOp.getOutputBuffer().getType())));
emitMvmOp(mvmId, addressOf(mvmLikeOp.getOutputBuffer(), knowledge), 0, addressOf(mvmLikeOp.getInput(), knowledge), 0);
// TODO: save weights somewhere (if transposeMatrix=true, transpose the weight matrix)
@@ -688,25 +686,29 @@ void PimCodeGen::emitBinaryVectorOp(pim_binary::Opcode opcode,
mlir::Value output,
mlir::Value lhs,
mlir::Value rhs,
size_t byteSize,
const StaticValueKnowledge& knowledge) const {
auto inputType = cast<ShapedType>(lhs.getType());
ensureVectorBitwidth(getVectorElementBitwidthOrCrash(inputType),
getVectorElementBitwidthOrCrash(cast<ShapedType>(output.getType())));
setupRdRs1Rs2(addressOf(output, knowledge), 0, addressOf(lhs, knowledge), 0, addressOf(rhs, knowledge), 0);
pim_binary::InstructionRecord instruction;
instruction.opcode = opcode;
instruction.rd = 0;
instruction.r1 = 1;
instruction.r2OrImm = 2;
instruction.generic3 = pim::checkedI32OrCrash(byteSize, "vector byte size");
instruction.generic3 = getVectorElementCountOrCrash(inputType);
emitInstruction(instruction);
}
void PimCodeGen::emitUnaryVectorOp(pim_binary::Opcode opcode,
mlir::Value output,
mlir::Value input,
size_t byteSize,
const StaticValueKnowledge& knowledge,
int32_t r2OrImm,
int32_t generic1) const {
auto inputType = cast<ShapedType>(input.getType());
ensureVectorBitwidth(getVectorElementBitwidthOrCrash(inputType),
getVectorElementBitwidthOrCrash(cast<ShapedType>(output.getType())));
setupRdRs1(addressOf(output, knowledge), 0, addressOf(input, knowledge), 0);
pim_binary::InstructionRecord instruction;
instruction.opcode = opcode;
@@ -714,7 +716,7 @@ void PimCodeGen::emitUnaryVectorOp(pim_binary::Opcode opcode,
instruction.r1 = 1;
instruction.r2OrImm = r2OrImm;
instruction.generic1 = generic1;
instruction.generic3 = pim::checkedI32OrCrash(byteSize, "vector byte size");
instruction.generic3 = getVectorElementCountOrCrash(inputType);
emitInstruction(instruction);
}
@@ -812,8 +814,7 @@ static SmallVector<Operation*> collectTopLevelCoreLikeOps(func::FuncOp funcOp) {
static FailureOr<CompiledCoreMemoryPlan> compileCoreMemoryPlan(Operation* coreLikeOp) {
CompiledCoreMemoryPlan plan;
auto arenaAttr = coreLikeOp->getAttrOfType<IntegerAttr>(kLocalMemorySizeAttrName);
if (!arenaAttr || arenaAttr.getInt() < 0
|| static_cast<uint64_t>(arenaAttr.getInt()) > kPimLocalMemoryAddressLimit) {
if (!arenaAttr || arenaAttr.getInt() < 0 || static_cast<uint64_t>(arenaAttr.getInt()) > kPimLocalMemoryAddressLimit) {
coreLikeOp->emitError("requires a valid pim.local_memory_size attribute before codegen");
return failure();
}
@@ -841,7 +842,9 @@ static FailureOr<CompiledCoreMemoryPlan> compileCoreMemoryPlan(Operation* coreLi
hasFailure = true;
return;
}
plan.entries.push_back({allocOp.getResult(), {address, static_cast<size_t>(*checkedSize)}});
plan.entries.push_back({
allocOp.getResult(), {address, static_cast<size_t>(*checkedSize)}
});
auto logicalBytes = pim::checkedAdd(
static_cast<size_t>(plan.logicalBytes), static_cast<size_t>(*checkedSize), allocOp, "logical local bytes");
if (failed(logicalBytes)) {
@@ -995,21 +998,10 @@ static LogicalResult executeCompiledCorePlan(
}
auto emitBinary = [&](auto op, pim_binary::Opcode opcode) {
coreCodeGen.emitBinaryVectorOp(opcode,
op.getOutputBuffer(),
op.getLhs(),
op.getRhs(),
getVectorByteSizeOrCrash(cast<ShapedType>(op.getLhs().getType())),
knowledge);
coreCodeGen.emitBinaryVectorOp(opcode, op.getOutputBuffer(), op.getLhs(), op.getRhs(), knowledge);
};
auto emitUnary = [&](auto op, pim_binary::Opcode opcode, int32_t r2OrImm, int32_t generic1) {
coreCodeGen.emitUnaryVectorOp(opcode,
op.getOutputBuffer(),
op.getInput(),
getVectorByteSizeOrCrash(cast<ShapedType>(op.getInput().getType())),
knowledge,
r2OrImm,
generic1);
coreCodeGen.emitUnaryVectorOp(opcode, op.getOutputBuffer(), op.getInput(), knowledge, r2OrImm, generic1);
};
switch (node.opKind) {
@@ -1092,8 +1084,8 @@ static void aliasMaterializedHostGlobals(CoreLikeOpTy coreLikeOp,
});
}
static OnnxMlirCompilerErrorCodes emitEmptyCoreArtifacts(StringRef outputDirPath, size_t emittedCoreId) {
std::string outputCorePath = (outputDirPath + "/core_" + std::to_string(emittedCoreId) + ".pim").str();
static OnnxMlirCompilerErrorCodes emitEmptyCoreArtifacts(StringRef outputDirPath, size_t physicalCoreId) {
std::string outputCorePath = (outputDirPath + "/core_" + std::to_string(physicalCoreId) + ".pim").str();
std::error_code errorCode;
raw_fd_ostream coreBinaryStream(outputCorePath, errorCode, sys::fs::OF_None);
if (errorCode) {
@@ -1117,7 +1109,7 @@ static OnnxMlirCompilerErrorCodes emitEmptyCoreArtifacts(StringRef outputDirPath
if (!pimEmitJson.getValue())
return CompilerSuccess;
std::string outputCoreJsonPath = (outputDirPath + "/core_" + std::to_string(emittedCoreId) + ".json").str();
std::string outputCoreJsonPath = (outputDirPath + "/core_" + std::to_string(physicalCoreId) + ".json").str();
errorCode = std::error_code();
raw_fd_ostream coreJsonStream(outputCoreJsonPath, errorCode);
if (errorCode) {
@@ -1179,36 +1171,15 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
return it->second.get();
};
llvm::DenseMap<size_t, size_t> emittedCoreIds;
size_t nextEmittedCoreId = 0;
for (Operation* op : coreLikeOps) {
if (auto coreOp = dyn_cast<pim::PimCoreOp>(op)) {
size_t originalCoreId = static_cast<size_t>(coreOp.getCoreId());
if (!emittedCoreIds.contains(originalCoreId))
emittedCoreIds[originalCoreId] = nextEmittedCoreId++;
continue;
}
auto coreBatchOp = cast<pim::PimCoreBatchOp>(op);
auto batchCoreIds = getBatchCoreIds(coreBatchOp);
for (unsigned lane = 0; lane < static_cast<unsigned>(coreBatchOp.getLaneCount()); ++lane) {
size_t originalCoreId = static_cast<size_t>(batchCoreIds[lane]);
if (!emittedCoreIds.contains(originalCoreId))
emittedCoreIds[originalCoreId] = nextEmittedCoreId++;
}
}
SmallVector<CoreEmissionJob> jobs;
SmallVector<SmallVector<size_t>> batchJobIndices;
for (Operation* op : coreLikeOps) {
if (auto coreOp = dyn_cast<pim::PimCoreOp>(op)) {
size_t originalCoreId = static_cast<size_t>(coreOp.getCoreId());
CoreEmissionJob job;
job.coreLikeOp = coreOp;
job.program = getCompiledProgram(op);
job.memoryPlan = getMemoryPlan(op);
job.emittedCoreId = emittedCoreIds.lookup(originalCoreId);
job.physicalCoreId = static_cast<size_t>(coreOp.getCoreId());
jobs.push_back(std::move(job));
continue;
}
@@ -1220,16 +1191,15 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
lanesByCoreId[static_cast<size_t>(batchCoreIds[lane])].push_back(lane);
SmallVector<size_t> jobIndices;
SmallVector<size_t> orderedOriginalCoreIds = llvm::to_vector(lanesByCoreId.keys());
llvm::sort(orderedOriginalCoreIds,
[&](size_t lhs, size_t rhs) { return emittedCoreIds.lookup(lhs) < emittedCoreIds.lookup(rhs); });
for (size_t originalCoreId : orderedOriginalCoreIds) {
SmallVector<size_t> physicalCoreIds = llvm::to_vector(lanesByCoreId.keys());
llvm::sort(physicalCoreIds);
for (size_t physicalCoreId : physicalCoreIds) {
CoreEmissionJob job;
job.coreLikeOp = coreBatchOp;
job.program = getCompiledProgram(op);
job.memoryPlan = getMemoryPlan(op);
job.emittedCoreId = emittedCoreIds.lookup(originalCoreId);
job.lanes = lanesByCoreId.lookup(originalCoreId);
job.physicalCoreId = physicalCoreId;
job.lanes = lanesByCoreId.lookup(physicalCoreId);
job.batchReportId = nextBatchReportId;
jobIndices.push_back(jobs.size());
jobs.push_back(std::move(job));
@@ -1238,8 +1208,11 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
++nextBatchReportId;
}
auto linkCoreWeights =
[&](size_t coreId, ArrayRef<std::string> weightFiles, json::Array& xbarsPerGroup) -> OnnxMlirCompilerErrorCodes {
auto linkCoreWeights = [&](size_t coreId,
ArrayRef<std::string> weightFiles,
ArrayRef<ResolvedWeightView> weights,
json::Array& xbarsPerGroup) -> OnnxMlirCompilerErrorCodes {
assert(weightFiles.size() == weights.size() && "weight files must match resolved weight views");
auto coreWeightsDirPath = outputDirPath + "/core_" + std::to_string(coreId);
if (auto error = sys::fs::create_directory(coreWeightsDirPath); error && error != std::errc::file_exists) {
errs() << "Error creating core directory: " << coreWeightsDirPath << ": " << error.message() << '\n';
@@ -1247,7 +1220,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
}
for (auto [slot, fileName] : llvm::enumerate(weightFiles)) {
xbarsPerGroup.push_back(1);
xbarsPerGroup.push_back(weights[slot].shape[1] / static_cast<int64_t>(crossbarSize));
std::string sourcePath = outputDirPath + "/weights/" + fileName;
std::string targetPath = coreWeightsDirPath + "/crossbar_" + std::to_string(slot) + ".bin";
sys::fs::remove(targetPath);
@@ -1295,7 +1268,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
};
std::error_code errorCode;
auto outputCorePath = outputDirPath + "/core_" + std::to_string(job.emittedCoreId) + ".pim";
auto outputCorePath = outputDirPath + "/core_" + std::to_string(job.physicalCoreId) + ".pim";
raw_fd_ostream coreBinaryStream(outputCorePath, errorCode, sys::fs::OF_None);
if (errorCode) {
errs() << "Error while opening core file `" << outputCorePath << "`: " << errorCode.message() << '\n';
@@ -1305,7 +1278,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
std::unique_ptr<raw_fd_ostream> coreJsonStream;
if (pimEmitJson.getValue()) {
std::string outputCoreJsonPath = outputDirPath + "/core_" + std::to_string(job.emittedCoreId) + ".json";
std::string outputCoreJsonPath = outputDirPath + "/core_" + std::to_string(job.physicalCoreId) + ".json";
errorCode = std::error_code();
coreJsonStream = std::make_unique<raw_fd_ostream>(outputCoreJsonPath, errorCode);
if (errorCode) {
@@ -1317,7 +1290,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
}
PimInstructionWriter instructionWriter(coreBinaryStream);
PimCodeGen coreCodeGen(jobMemory, instructionWriter, coreJsonStream.get(), emittedCoreIds);
PimCodeGen coreCodeGen(jobMemory, instructionWriter, coreJsonStream.get());
auto finalizeInstructions = [&]() {
bool succeeded = mlir::succeeded(instructionWriter.finalize());
coreBinaryStream.close();
@@ -1330,7 +1303,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
if (auto coreOp = dyn_cast<pim::PimCoreOp>(job.coreLikeOp)) {
aliasMaterializedHostGlobals(coreOp, moduleOp, materializedHostGlobals, jobMemory);
auto& deviceMemory = jobMemory.getOrCreateDeviceMem(job.emittedCoreId);
auto& deviceMemory = jobMemory.getOrCreateDeviceMem(job.physicalCoreId);
deviceMemory.allocateCore(*job.memoryPlan);
StaticValueKnowledge knowledge = seedCoreCodegenKnowledge(coreOp);
@@ -1345,7 +1318,7 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
else {
auto coreBatchOp = cast<pim::PimCoreBatchOp>(job.coreLikeOp);
aliasMaterializedHostGlobals(coreBatchOp, moduleOp, materializedHostGlobals, jobMemory);
auto& deviceMemory = jobMemory.getOrCreateDeviceMem(job.emittedCoreId);
auto& deviceMemory = jobMemory.getOrCreateDeviceMem(job.physicalCoreId);
for (unsigned lane : job.lanes) {
StaticValueKnowledge knowledge = seedCoreBatchCodegenKnowledge(coreBatchOp, lane);
@@ -1398,18 +1371,29 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
if (jobResults[jobIndex].status != CompilerSuccess)
return jobResults[jobIndex].status;
if (jobs.empty()) {
if (auto err = emitEmptyCoreArtifacts(outputDirPath, 0))
size_t maxPhysicalCoreId = 0;
for (const CoreEmissionJob& job : jobs)
maxPhysicalCoreId = std::max(maxPhysicalCoreId, job.physicalCoreId);
std::vector<bool> activePhysicalCores(maxPhysicalCoreId + 1);
for (const CoreEmissionJob& job : jobs)
activePhysicalCores[job.physicalCoreId] = true;
for (size_t physicalCoreId = 0;
physicalCoreId < activePhysicalCores.size(); ++physicalCoreId) {
if (activePhysicalCores[physicalCoreId])
continue;
if (auto err =
emitEmptyCoreArtifacts(outputDirPath, physicalCoreId))
return err;
xbarsPerArrayGroup["core0"] = json::Array {};
memory.recordCoreReport(0, MemoryReportRow {});
xbarsPerArrayGroup["core" + std::to_string(physicalCoreId)] =
json::Array {};
memory.recordCoreReport(physicalCoreId, MemoryReportRow {});
}
llvm::SmallVector<WeightFileRequest, 8> weightRequests;
weightRequests.reserve(jobs.size());
for (size_t jobIndex = 0; jobIndex < jobs.size(); ++jobIndex) {
WeightFileRequest request;
request.coreId = jobs[jobIndex].emittedCoreId;
request.coreId = jobs[jobIndex].physicalCoreId;
request.weights = jobResults[jobIndex].usedWeights;
weightRequests.push_back(std::move(request));
}
@@ -1422,10 +1406,11 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
json::Array xbarsPerGroup;
if (auto coreOp = dyn_cast<pim::PimCoreOp>(job.coreLikeOp)) {
if (auto err = linkCoreWeights(job.emittedCoreId, mapCoreWeightToFileName[job.emittedCoreId], xbarsPerGroup))
if (auto err = linkCoreWeights(
job.physicalCoreId, mapCoreWeightToFileName[job.physicalCoreId], result.usedWeights, xbarsPerGroup))
return err;
xbarsPerArrayGroup["core" + std::to_string(job.emittedCoreId)] = std::move(xbarsPerGroup);
memory.recordCoreReport(job.emittedCoreId, result.reportRow);
xbarsPerArrayGroup["core" + std::to_string(job.physicalCoreId)] = std::move(xbarsPerGroup);
memory.recordCoreReport(job.physicalCoreId, result.reportRow);
continue;
}
}
@@ -1438,10 +1423,11 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
const CoreEmissionJob& job = jobs[jobIndex];
const CoreEmissionResult& result = jobResults[jobIndex];
json::Array xbarsPerGroup;
if (auto err = linkCoreWeights(job.emittedCoreId, mapCoreWeightToFileName[job.emittedCoreId], xbarsPerGroup))
if (auto err = linkCoreWeights(
job.physicalCoreId, mapCoreWeightToFileName[job.physicalCoreId], result.usedWeights, xbarsPerGroup))
return err;
xbarsPerArrayGroup["core" + std::to_string(job.emittedCoreId)] = std::move(xbarsPerGroup);
reportedCoreIds.push_back(pim::checkedI32OrCrash(job.emittedCoreId, "batch report core id"));
xbarsPerArrayGroup["core" + std::to_string(job.physicalCoreId)] = std::move(xbarsPerGroup);
reportedCoreIds.push_back(pim::checkedI32OrCrash(job.physicalCoreId, "batch report physical core id"));
if (!batchPerCoreRow)
batchPerCoreRow = result.reportRow;
else if (!(*batchPerCoreRow == result.reportRow))
@@ -1449,11 +1435,10 @@ OnnxMlirCompilerErrorCodes onnx_mlir::compileToPimCode(ModuleOp& moduleOp, std::
}
uint64_t batchReportId = jobs[group.front()].batchReportId.value_or(0);
memory.recordBatchReport(
batchReportId, reportedCoreIds, batchPerCoreRow.value_or(MemoryReportRow {}));
memory.recordBatchReport(batchReportId, reportedCoreIds, batchPerCoreRow.value_or(MemoryReportRow {}));
}
maxCoreId = nextEmittedCoreId == 0 ? 0 : nextEmittedCoreId - 1;
maxCoreId = maxPhysicalCoreId;
memory.flushReport();
return writeConfigJson(funcOp, memory, maxCoreId, std::move(xbarsPerArrayGroup), outputDirPath);
+7 -15
View File
@@ -134,8 +134,7 @@ private:
public:
PimAcceleratorMemory();
PimAcceleratorMemory(
const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport);
PimAcceleratorMemory(const llvm::SmallDenseMap<MemoryValueKey, MemEntry, 32>& initialMemEntries, bool enableReport);
PimMemory& getOrCreateDeviceMem(size_t id);
@@ -145,8 +144,7 @@ public:
llvm::FailureOr<int64_t> getIndexValue(mlir::Value value, const StaticValueKnowledge& knowledge = {}) const;
void reportHost();
void recordCoreReport(size_t coreId, const MemoryReportRow& row);
void recordBatchReport(
uint64_t batchId, llvm::ArrayRef<int32_t> coreIds, const MemoryReportRow& perCoreRow);
void recordBatchReport(uint64_t batchId, llvm::ArrayRef<int32_t> coreIds, const MemoryReportRow& perCoreRow);
void setTotalWeightBytes(uint64_t bytes) { totalWeightBytes = bytes; }
void flushReport();
};
@@ -155,7 +153,7 @@ struct CoreEmissionJob {
mlir::Operation* coreLikeOp = nullptr;
const CompiledCoreProgram* program = nullptr;
const CompiledCoreMemoryPlan* memoryPlan = nullptr;
size_t emittedCoreId = 0;
size_t physicalCoreId = 0;
llvm::SmallVector<unsigned, 4> lanes;
std::optional<uint64_t> batchReportId;
};
@@ -164,17 +162,16 @@ class PimCodeGen {
PimAcceleratorMemory& memory;
PimInstructionWriter& instructionWriter;
llvm::raw_fd_ostream* coreJsonStream;
const llvm::DenseMap<size_t, size_t>& emittedCoreIds;
std::optional<unsigned> batchLane;
mutable std::array<std::optional<int32_t>, 256> scalarRegisterValues = {};
mutable std::optional<std::array<int32_t, 2>> vectorBitwidths;
size_t addressOf(mlir::Value value, const StaticValueKnowledge& knowledge) const {
return memory.getValueAddress(value, knowledge, batchLane);
}
size_t remapCoreId(size_t coreId) const;
void emitInstruction(const pim_binary::InstructionRecord& instruction) const;
void updateScalarRegisterCache(const pim_binary::InstructionRecord& instruction) const;
void ensureVectorBitwidth(int32_t inputBitwidth, int32_t outputBitwidth) const;
void genSetRegisterImmediate(uint8_t registerNumber, int32_t immediate) const;
void genSetRegisterImmediateUnsigned(size_t registerNumber, size_t immediate) const;
@@ -198,21 +195,16 @@ public:
mlir::Value output,
mlir::Value lhs,
mlir::Value rhs,
size_t byteSize,
const StaticValueKnowledge& knowledge) const;
void emitUnaryVectorOp(pim_binary::Opcode opcode,
mlir::Value output,
mlir::Value input,
size_t byteSize,
const StaticValueKnowledge& knowledge,
int32_t r2OrImm = 0,
int32_t generic1 = 0) const;
PimCodeGen(PimAcceleratorMemory& memory,
PimInstructionWriter& instructionWriter,
llvm::raw_fd_ostream* coreJson,
const llvm::DenseMap<size_t, size_t>& emittedCoreIds)
: memory(memory), instructionWriter(instructionWriter), coreJsonStream(coreJson), emittedCoreIds(emittedCoreIds) {}
PimCodeGen(PimAcceleratorMemory& memory, PimInstructionWriter& instructionWriter, llvm::raw_fd_ostream* coreJson)
: memory(memory), instructionWriter(instructionWriter), coreJsonStream(coreJson) {}
void setBatchLane(std::optional<unsigned> lane) { batchLane = lane; }
llvm::FailureOr<int64_t> indexOf(mlir::Value value, const StaticValueKnowledge& knowledge) const {
+6
View File
@@ -125,6 +125,12 @@ llvm::cl::opt<long> coresCount("core-count",
llvm::cl::desc("Number of cores in the chip. Required for PIM compilation."),
llvm::cl::init(-1));
llvm::cl::opt<std::string> pimTargetConfig(
"pim-target-config",
llvm::cl::desc("PIM target configuration used to construct the Spatial scheduling cost model"),
llvm::cl::init(""),
llvm::cl::cat(OnnxMlirOptions));
llvm::cl::opt<bool>
ignoreConcatError("ignore-concat-error",
llvm::cl::desc("Ignore ConcatOp corner case: do not assert and do a simplification"),
+3
View File
@@ -2,6 +2,8 @@
#include "llvm/Support/CommandLine.h"
#include <string>
#define INSTRUMENTSTAGE_ENUM_PIM
#define INSTRUMENTSTAGE_CL_ENUM_PIM
@@ -63,6 +65,7 @@ extern llvm::cl::opt<bool> pimTraceCommunicationMaterialization;
extern llvm::cl::opt<size_t> crossbarSize;
extern llvm::cl::opt<size_t> crossbarCountInCore;
extern llvm::cl::opt<long> coresCount;
extern llvm::cl::opt<std::string> pimTargetConfig;
extern llvm::cl::opt<uint64_t> pimConvIm2colMaxElements;
extern llvm::cl::opt<uint64_t> pimConvStreamChunkPositions;
+264 -2
View File
@@ -1,9 +1,21 @@
#include "mlir/Conversion/AffineToStandard/AffineToStandard.h"
#include "mlir/Transforms/Passes.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/ADT/SmallString.h"
#include <cmath>
#include <limits>
#include <tuple>
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/PimOps.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/SchedulingTarget.hpp"
#include "src/Accelerators/PIM/Pass/PIMPasses.h"
#include "src/Compiler/CompilerPasses.hpp"
@@ -14,6 +26,254 @@ using namespace onnx_mlir;
namespace onnx_mlir {
namespace {
void setDefaultPimInterProcessorLatencies(
spatial::SchedulingTarget& target) {
size_t rows = static_cast<size_t>(
std::sqrt(static_cast<long double>(target.processorCount)));
while (rows > 1 && target.processorCount % rows != 0)
--rows;
size_t columns = (target.processorCount + rows - 1) / rows;
target.interProcessorLatencyNs.assign(
target.processorCount * target.processorCount, 0);
Cost latencySum = 0;
size_t pairCount = 0;
for (size_t source = 0; source < target.processorCount; ++source) {
for (size_t destination = 0;
destination < target.processorCount; ++destination) {
if (source == destination)
continue;
size_t sourceRow = source / columns;
size_t sourceColumn = source % columns;
size_t destinationRow = destination / columns;
size_t destinationColumn = destination % columns;
size_t rowDistance = sourceRow > destinationRow
? sourceRow - destinationRow
: destinationRow - sourceRow;
size_t columnDistance = sourceColumn > destinationColumn
? sourceColumn - destinationColumn
: destinationColumn - sourceColumn;
Cost latency = static_cast<Cost>(2 + rowDistance + columnDistance);
target.interProcessorLatencyNs[
source * target.processorCount + destination] = latency;
latencySum = checkedAdd(latencySum, latency);
++pairCount;
}
}
target.averageInterProcessorLatencyNs =
pairCount == 0
? 0
: (latencySum + static_cast<Cost>(pairCount) - 1)
/ static_cast<Cost>(pairCount);
}
spatial::SchedulingTarget getDefaultPimSchedulingTarget() {
spatial::SchedulingTarget target;
target.processorCount = static_cast<size_t>(coresCount.getValue());
target.residentWeightCapacity = crossbarCountInCore.getValue();
target.matrixRows = crossbarSize.getValue();
target.matrixColumns = crossbarSize.getValue();
setDefaultPimInterProcessorLatencies(target);
return target;
}
const llvm::json::Object& requireObject(const llvm::json::Object& object,
llvm::StringRef key,
llvm::StringRef path) {
const llvm::json::Object* nested = object.getObject(key);
if (!nested)
llvm::report_fatal_error("PIM target config is missing object '" + path + "." + key + "'");
return *nested;
}
Cost getConfigCost(const llvm::json::Object& object,
llvm::StringRef key,
Cost fallback,
bool allowZero = false) {
std::optional<double> number = object.getNumber(key);
if (!number)
return fallback;
if (!std::isfinite(*number) || *number < 0.0 || (!allowZero && *number == 0.0)
|| *number > static_cast<double>(std::numeric_limits<Cost>::max()))
llvm::report_fatal_error("PIM target config field '" + key + "' must be a valid positive number");
return static_cast<Cost>(std::ceil(*number));
}
std::pair<size_t, size_t> getConfigPair(const llvm::json::Object& object,
llvm::StringRef key) {
const llvm::json::Array* values = object.getArray(key);
if (!values || values->size() != 2)
llvm::report_fatal_error("PIM target config field '" + key + "' must contain two integers");
std::optional<int64_t> first = (*values)[0].getAsInteger();
std::optional<int64_t> second = (*values)[1].getAsInteger();
if (!first || !second || *first <= 0 || *second <= 0)
llvm::report_fatal_error("PIM target config field '" + key + "' must contain two positive integers");
return {static_cast<size_t>(*first), static_cast<size_t>(*second)};
}
void loadPimInterProcessorLatencies(
spatial::SchedulingTarget& target,
const llvm::json::Object& network) {
std::optional<llvm::StringRef> filename =
network.getString("net_config_file_path");
if (!filename)
llvm::report_fatal_error(
"PIM target config is missing network latency file path");
llvm::SmallString<256> networkPath(*filename);
if (!llvm::sys::path::is_absolute(networkPath)) {
llvm::SmallString<256> configDirectory(pimTargetConfig.getValue());
llvm::sys::path::remove_filename(configDirectory);
llvm::sys::path::append(configDirectory, networkPath);
networkPath = configDirectory;
}
auto buffer = llvm::MemoryBuffer::getFile(networkPath);
if (!buffer)
llvm::report_fatal_error(
llvm::Twine("failed to read PIM network config '")
+ networkPath + "': " + buffer.getError().message());
auto parsed = llvm::json::parse(buffer.get()->getBuffer());
if (!parsed)
llvm::report_fatal_error(
llvm::Twine("failed to parse PIM network config '")
+ networkPath + "': " + llvm::toString(parsed.takeError()));
const llvm::json::Object* root = parsed->getAsObject();
const llvm::json::Object* latencies =
root ? root->getObject("latency") : nullptr;
if (!latencies)
llvm::report_fatal_error(
"PIM network config is missing its latency matrix");
target.interProcessorLatencyNs.assign(
target.processorCount * target.processorCount, 0);
Cost latencySum = 0;
size_t pairCount = 0;
for (size_t source = 0; source < target.processorCount; ++source) {
std::string sourceKey = std::to_string(source);
const llvm::json::Object* row = latencies->getObject(sourceKey);
if (!row)
llvm::report_fatal_error(
llvm::Twine("PIM network config is missing latency row ")
+ sourceKey);
for (size_t destination = 0;
destination < target.processorCount; ++destination) {
if (source == destination)
continue;
std::string destinationKey = std::to_string(destination);
std::optional<double> latency = row->getNumber(destinationKey);
if (!latency || !std::isfinite(*latency) || *latency <= 0.0)
llvm::report_fatal_error(
llvm::Twine("PIM network config is missing latency ")
+ sourceKey + " -> " + destinationKey);
Cost roundedLatency = static_cast<Cost>(std::ceil(*latency));
target.interProcessorLatencyNs[
source * target.processorCount + destination] = roundedLatency;
latencySum = checkedAdd(latencySum, roundedLatency);
++pairCount;
}
}
target.averageInterProcessorLatencyNs =
pairCount == 0
? 0
: (latencySum + static_cast<Cost>(pairCount) - 1)
/ static_cast<Cost>(pairCount);
}
spatial::SchedulingTarget getPimSchedulingTarget() {
spatial::SchedulingTarget target = getDefaultPimSchedulingTarget();
if (pimTargetConfig.empty())
return target;
auto buffer = llvm::MemoryBuffer::getFile(pimTargetConfig);
if (!buffer)
llvm::report_fatal_error(
llvm::Twine("failed to read PIM target config '")
+ pimTargetConfig.getValue() + "': " + buffer.getError().message());
auto parsed = llvm::json::parse(buffer.get()->getBuffer());
if (!parsed)
llvm::report_fatal_error(
llvm::Twine("failed to parse PIM target config '")
+ pimTargetConfig.getValue() + "': "
+ llvm::toString(parsed.takeError()));
const llvm::json::Object* root = parsed->getAsObject();
if (!root)
llvm::report_fatal_error("PIM target config must contain a JSON object");
const llvm::json::Object& chip = requireObject(*root, "chip_config", "root");
const llvm::json::Object& core = requireObject(chip, "core_config", "chip_config");
const llvm::json::Object& matrix =
requireObject(core, "matrix_config", "chip_config.core_config");
const llvm::json::Object& localMemory =
requireObject(core, "local_memory_config", "chip_config.core_config");
const llvm::json::Object& network =
requireObject(chip, "network_config", "chip_config");
std::optional<int64_t> coreCount = chip.getInteger("core_cnt");
if (!coreCount || *coreCount <= 0)
llvm::report_fatal_error("PIM target config field 'core_cnt' must be a positive integer");
target.processorCount = static_cast<size_t>(*coreCount);
target.residentWeightCapacity =
getConfigCost(matrix, "xbar_array_count", target.residentWeightCapacity);
std::tie(target.matrixRows, target.matrixColumns) =
getConfigPair(matrix, "xbar_size");
if (target.processorCount != static_cast<size_t>(coresCount.getValue())
|| target.residentWeightCapacity != crossbarCountInCore.getValue()
|| target.matrixRows != crossbarSize.getValue()
|| target.matrixColumns != crossbarSize.getValue())
llvm::report_fatal_error("PIM target config resources do not match --core-count, "
"--crossbar-count, and --crossbar-size");
loadPimInterProcessorLatencies(target, network);
target.processorPeriodNs =
getConfigCost(core, "period", target.processorPeriodNs);
target.localMemoryWidthBytes =
getConfigCost(localMemory, "data_width", target.localMemoryWidthBytes);
target.localMemoryReadLatencyCycles =
getConfigCost(localMemory, "read_latency_cycle", target.localMemoryReadLatencyCycles);
target.localMemoryWriteLatencyCycles =
getConfigCost(localMemory, "write_latency_cycle", target.localMemoryWriteLatencyCycles);
target.transferWidthBytes =
getConfigCost(network, "bus_width", target.transferWidthBytes);
target.vectorWidth = getConfigCost(core, "vector_width", target.vectorWidth);
target.vectorLatencyCycles =
getConfigCost(core, "vector_latency_cycle", target.vectorLatencyCycles);
target.matrixPeriodNs =
getConfigCost(matrix, "period", target.matrixPeriodNs);
target.matrixInputResolutionBits =
getConfigCost(matrix, "dac_resolution", target.matrixInputResolutionBits);
target.matrixInputLatencyCycles =
getConfigCost(matrix, "dac_latency_cycle", target.matrixInputLatencyCycles);
target.matrixInputParallelism =
getConfigCost(matrix, "dac_count", target.matrixInputParallelism);
target.matrixReadLatencyNs =
getConfigCost(matrix, "xbar_latency", target.matrixReadLatencyNs);
target.matrixSampleLatencyCycles =
getConfigCost(matrix, "sample_hold_latency_cycle", target.matrixSampleLatencyCycles);
target.matrixOutputLatencyCycles =
getConfigCost(matrix, "adc_latency_cycle", target.matrixOutputLatencyCycles);
target.matrixOutputParallelism =
getConfigCost(matrix, "adc_count", target.matrixOutputParallelism);
target.matrixShiftLatencyCycles =
getConfigCost(matrix, "shift_adder_latency_cycle", target.matrixShiftLatencyCycles);
target.matrixBufferLatencyCycles =
getConfigCost(matrix, "output_buffer_latency_cycle", target.matrixBufferLatencyCycles);
target.matrixInputBufferLatencyCycles =
getConfigCost(matrix,
"input_buffer_latency_cycle",
target.matrixInputBufferLatencyCycles,
/*allowZero=*/true);
target.matrixPipeline = matrix.getBoolean("pipeline_mode").value_or(target.matrixPipeline);
return target;
}
} // namespace
void addPassesPim(OwningOpRef<ModuleOp>& module,
PassManager& pm,
EmissionTargetType& emissionTarget,
@@ -31,11 +291,13 @@ void addPassesPim(OwningOpRef<ModuleOp>& module,
addONNXToMLIRPasses(pm, /*target CPU*/ false);
if (pimEmissionTarget >= EmitSpatial) {
spatial::SchedulingTarget schedulingTarget = getPimSchedulingTarget();
pm.addPass(createONNXToSpatialPass());
pm.addPass(createSpatialLayoutPlanningPass());
pm.addPass(createLowerSpatialPlansPass());
pm.addPass(createTrivialGraphComputeMergePass());
pm.addPass(createMergeComputeNodesPass());
pm.addPass(createTrivialGraphComputeMergePass(
schedulingTarget.residentWeightCapacity));
pm.addPass(createMergeComputeNodesPass(schedulingTarget));
pm.addPass(createMessagePass("Onnx lowered to Spatial"));
}
+5 -3
View File
@@ -42,7 +42,9 @@ WeightEmissionResult createAndPopulateWeightFolder(ArrayRef<WeightFileRequest> r
assert(isMatrixShape(shape) && "Weight matrix must be 2-dimensional");
int64_t numRows = shape[0];
int64_t numCols = shape[1];
assert(numRows <= xbarSize && numCols <= xbarSize && "Weight dimensions must not exceed crossbar size");
assert(numRows <= xbarSize && numCols % xbarSize == 0
&& numCols / xbarSize <= static_cast<int64_t>(crossbarCountInCore)
&& "Weight dimensions must fit in one array group");
size_t elementByteWidth = getElementTypeSizeInBytes(denseAttr.getElementType());
@@ -57,7 +59,7 @@ WeightEmissionResult createAndPopulateWeightFolder(ArrayRef<WeightFileRequest> r
uint64_t zero = 0;
for (int64_t row = 0; row < xbarSize; row++) {
for (int64_t col = 0; col < xbarSize; col++) {
for (int64_t col = 0; col < numCols; col++) {
if (row < numRows && col < numCols) {
int64_t elementIndex = weightView.offset + row * weightView.strides[0] + col * weightView.strides[1];
APInt bits = denseAttr.getValues<APFloat>()[elementIndex].bitcastToAPInt();
@@ -73,7 +75,7 @@ WeightEmissionResult createAndPopulateWeightFolder(ArrayRef<WeightFileRequest> r
weightFileStream.close();
materializedWeights.push_back({weightView, newFileName});
uint64_t weightBytes = pim::checkedMulOrCrash(
pim::checkedMulOrCrash(static_cast<size_t>(xbarSize), static_cast<size_t>(xbarSize), "weight element count"),
pim::checkedMulOrCrash(static_cast<size_t>(xbarSize), static_cast<size_t>(numCols), "weight element count"),
elementByteWidth,
"weight byte size");
result.totalWeightBytes = pim::checkedAddOrCrash(result.totalWeightBytes, weightBytes, "total weight bytes");
@@ -8,7 +8,6 @@ add_pim_library(OMONNXToSpatial
ONNXToSpatialVerifier.cpp
Patterns/Pre.cpp
Patterns/Post.cpp
Patterns/GeneratedConversion.cpp
Patterns/Math/Conv.cpp
Patterns/Math/ConvGeometry.cpp
Patterns/Math/Elementwise.cpp
@@ -19,9 +19,11 @@ FailureOr<RowStripPhysicalValue> describeRowStripPhysicalValue(Value storage, Ra
|| storageType.getRank() != 5 || logicalType.getRank() != 4 || logicalType.getDimSize(0) != 1
|| storageType.getElementType() != logicalType.getElementType()
|| storageType.getDimSize(1) != 1 || storageType.getDimSize(2) != 1
|| storageType.getDimSize(3) != logicalType.getDimSize(3) || storageType.getDimSize(4) <= 0)
|| storageType.getDimSize(3) != logicalType.getDimSize(3)
|| storageType.getDimSize(4) <= 0)
return failure();
const int64_t tilesPerRow = ceilIntegerDivide(logicalType.getDimSize(1), storageType.getDimSize(4));
const int64_t tilesPerRow =
ceilIntegerDivide(logicalType.getDimSize(1), storageType.getDimSize(4));
if (storageType.getDimSize(0) != logicalType.getDimSize(2) * tilesPerRow)
return failure();
return RowStripPhysicalValue {storage, logicalType,
@@ -249,4 +251,111 @@ FailureOr<Value> applyRowStripBiasAdd(const RowStripPhysicalValue& value,
return batchOp->getResult(0);
}
FailureOr<Value> applyRowStripAdd(const RowStripPhysicalValue& lhs,
const RowStripPhysicalValue& rhs,
PatternRewriter& rewriter,
Location loc) {
if (lhs.logicalType != rhs.logicalType || lhs.fragmentType != rhs.fragmentType
|| lhs.storage.getType() != rhs.storage.getType() || lhs.tilesPerRow != rhs.tilesPerRow)
return failure();
auto storageType = cast<RankedTensorType>(lhs.storage.getType());
const int64_t laneCount = storageType.getDimSize(0);
auto batch = createSpatComputeBatch(
rewriter,
loc,
TypeRange {storageType},
laneCount,
{},
ValueRange {lhs.storage, rhs.storage},
[&](detail::SpatComputeBatchBodyArgs args) {
FailureOr<Value> lhsFragment =
extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[0], args.lane, lhs.fragmentType);
FailureOr<Value> rhsFragment =
extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[1], args.lane, rhs.fragmentType);
if (failed(lhsFragment) || failed(rhsFragment))
return failure();
Value added = spatial::SpatVAddOp::create(rewriter, loc, lhs.fragmentType, *lhsFragment, *rhsFragment);
publishGraphBatchPhysicalFragment(rewriter, loc, added, args.outputs.front(), args.lane);
return success();
});
if (failed(batch))
return failure();
return batch->getResult(0);
}
FailureOr<Value> applyRowStripConcat(ArrayRef<RowStripPhysicalValue> inputs,
RankedTensorType outputType,
PatternRewriter& rewriter,
Location loc) {
if (inputs.empty() || !outputType || !outputType.hasStaticShape() || outputType.getRank() != 4
|| outputType.getDimSize(0) != 1)
return failure();
int64_t channels = 0;
for (const RowStripPhysicalValue& input : inputs) {
if (input.logicalType.getElementType() != outputType.getElementType()
|| input.logicalType.getDimSize(0) != outputType.getDimSize(0)
|| input.logicalType.getDimSize(2) != outputType.getDimSize(2)
|| input.logicalType.getDimSize(3) != outputType.getDimSize(3))
return failure();
channels += input.logicalType.getDimSize(1);
}
if (channels != outputType.getDimSize(1))
return failure();
SmallVector<Value> storages;
llvm::transform(
inputs, std::back_inserter(storages), [](const RowStripPhysicalValue& input) { return input.storage; });
const int64_t tileWidth = outputType.getDimSize(3);
auto fragmentType = getRowStripFragmentType(outputType);
auto storageType = getRowStripStorageType(outputType);
auto batch = createSpatComputeBatch(
rewriter,
loc,
TypeRange {storageType},
outputType.getDimSize(2),
{},
storages,
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchor = rewriter.getInsertionBlock()->getParentOp();
SmallVector<Value> fragments;
for (auto [inputIndex, input] : llvm::enumerate(inputs)) {
Value tileStart = affineMulConst(
rewriter, loc, args.lane, input.tilesPerRow, anchor);
for (int64_t tile = 0; tile < input.tilesPerRow; ++tile) {
Value slot =
affineAddConst(rewriter, loc, tileStart, tile, anchor);
FailureOr<Value> fragment =
extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[inputIndex], slot, input.fragmentType);
if (failed(fragment))
return failure();
int64_t channelOffset = tile * input.fragmentType.getDimSize(3);
int64_t validChannels =
std::min(input.fragmentType.getDimSize(3), input.logicalType.getDimSize(1) - channelOffset);
auto validType =
RankedTensorType::get(
{1, 1, tileWidth, validChannels},
outputType.getElementType());
MixedSliceGeometry slice;
slice.offsets.assign(4, rewriter.getIndexAttr(0));
slice.sizes = {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tileWidth),
rewriter.getIndexAttr(validChannels)};
slice.strides.assign(4, rewriter.getIndexAttr(1));
Value valid = extractMixedSliceOrIdentity(rewriter, loc, *fragment, validType, slice);
if (!valid)
return failure();
fragments.push_back(valid);
}
}
Value concatenated =
spatial::SpatConcatOp::create(rewriter, loc, fragmentType, rewriter.getI64IntegerAttr(3), fragments);
publishGraphBatchPhysicalFragment(rewriter, loc, concatenated, args.outputs.front(), args.lane);
return success();
});
if (failed(batch))
return failure();
return batch->getResult(0);
}
} // namespace onnx_mlir
@@ -66,4 +66,14 @@ mlir::FailureOr<mlir::Value> applyRowStripBiasAdd(const RowStripPhysicalValue& v
mlir::PatternRewriter& rewriter,
mlir::Location loc);
mlir::FailureOr<mlir::Value> applyRowStripAdd(const RowStripPhysicalValue& lhs,
const RowStripPhysicalValue& rhs,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
mlir::FailureOr<mlir::Value> applyRowStripConcat(llvm::ArrayRef<RowStripPhysicalValue> inputs,
mlir::RankedTensorType outputType,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
} // namespace onnx_mlir
@@ -64,6 +64,22 @@ static FailureOr<Value> lowerRowStripBiasAdd(const RowStripPhysicalValue& input,
return applyRowStripBiasAdd(input, planOp.getBias(), rewriter, planOp.getLoc());
}
static FailureOr<Value> lowerRowStripAdd(const RowStripPhysicalValue& lhs,
const RowStripPhysicalValue& rhs,
spatial::SpatAddPlanOp planOp,
PatternRewriter& rewriter) {
return applyRowStripAdd(lhs, rhs, rewriter, planOp.getLoc());
}
static FailureOr<Value> lowerRowStripConcat(ArrayRef<RowStripPhysicalValue> inputs,
spatial::SpatConcatPlanOp planOp,
PatternRewriter& rewriter) {
auto outputType = dyn_cast<RankedTensorType>(planOp.getOutput().getType());
if (!outputType)
return failure();
return applyRowStripConcat(inputs, outputType, rewriter, planOp.getLoc());
}
static FailureOr<Value>
materializeRowStripToDense(const RowStripPhysicalValue& rowStripValue, Location loc, PatternRewriter& rewriter) {
if (rowStripValue.logicalType.getRank() != 4 || !rowStripValue.logicalType.hasStaticShape())
@@ -122,6 +138,99 @@ static FailureOr<Value> lowerDenseBatchBiasAdd(Value input, Value bias, RankedTe
return batch->getResult(0);
}
static LogicalResult lowerAddPlan(spatial::SpatAddPlanOp planOp,
llvm::DenseMap<Value, RowStripPhysicalValue>& rowStripValues,
llvm::SmallPtrSetImpl<Operation*>& eraseAfterLowering,
PatternRewriter& rewriter) {
FailureOr<RowStripPhysicalValue> lhs = getRowStripValue(rowStripValues, planOp.getLhs());
FailureOr<RowStripPhysicalValue> rhs = getRowStripValue(rowStripValues, planOp.getRhs());
if (succeeded(lhs) && succeeded(rhs)) {
auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) {
auto blueprint = dyn_cast<spatial::SpatBlueprintOp>(user);
return blueprint && blueprint.getPhysicalLayout() == kRowStripLayout;
});
if (outputBlueprint == planOp.getResult().getUsers().end())
return planOp.emitOpError("row-strip add plan requires a row-strip blueprint result");
rewriter.setInsertionPoint(planOp);
FailureOr<Value> lowered = lowerRowStripAdd(*lhs, *rhs, planOp, rewriter);
if (failed(lowered))
return planOp.emitOpError("failed to lower selected row-strip Spatial add plan");
auto blueprint = cast<spatial::SpatBlueprintOp>(*outputBlueprint);
FailureOr<RowStripPhysicalValue> output = buildRowStripValue(blueprint, *lowered);
if (failed(output))
return failure();
rowStripValues[blueprint.getResult()] = *output;
eraseAfterLowering.insert(planOp);
eraseAfterLowering.insert(blueprint);
return success();
}
rewriter.setInsertionPoint(planOp);
auto compute = createSpatCompute<2>(rewriter,
planOp.getLoc(),
planOp.getOutput().getType(),
{},
ValueRange {planOp.getLhs(), planOp.getRhs()},
[&](Value lhsValue, Value rhsValue) {
Value added = spatial::SpatVAddOp::create(
rewriter, planOp.getLoc(), planOp.getOutput().getType(), lhsValue, rhsValue);
spatial::SpatYieldOp::create(rewriter, planOp.getLoc(), added);
});
rewriter.replaceOp(planOp, compute.getResults());
return success();
}
static LogicalResult lowerConcatPlan(spatial::SpatConcatPlanOp planOp,
llvm::DenseMap<Value, RowStripPhysicalValue>& rowStripValues,
llvm::SmallPtrSetImpl<Operation*>& eraseAfterLowering,
PatternRewriter& rewriter) {
SmallVector<RowStripPhysicalValue> inputs;
for (Value input : planOp.getInputs()) {
FailureOr<RowStripPhysicalValue> physical = getRowStripValue(rowStripValues, input);
if (failed(physical)) {
inputs.clear();
break;
}
inputs.push_back(*physical);
}
if (inputs.size() == planOp.getInputs().size()) {
auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) {
auto blueprint = dyn_cast<spatial::SpatBlueprintOp>(user);
return blueprint && blueprint.getPhysicalLayout() == kRowStripLayout;
});
if (outputBlueprint == planOp.getResult().getUsers().end())
return planOp.emitOpError("row-strip concat plan requires a row-strip blueprint result");
rewriter.setInsertionPoint(planOp);
FailureOr<Value> lowered = lowerRowStripConcat(inputs, planOp, rewriter);
if (failed(lowered))
return planOp.emitOpError("failed to lower selected row-strip Spatial concat plan");
auto blueprint = cast<spatial::SpatBlueprintOp>(*outputBlueprint);
FailureOr<RowStripPhysicalValue> output = buildRowStripValue(blueprint, *lowered);
if (failed(output))
return failure();
rowStripValues[blueprint.getResult()] = *output;
eraseAfterLowering.insert(planOp);
eraseAfterLowering.insert(blueprint);
return success();
}
rewriter.setInsertionPoint(planOp);
auto compute = createSpatCompute(
rewriter,
planOp.getLoc(),
TypeRange {planOp.getOutput().getType()},
{},
planOp.getInputs(),
[&](ValueRange values) {
Value concatenated = spatial::SpatConcatOp::create(
rewriter, planOp.getLoc(), planOp.getOutput().getType(), rewriter.getI64IntegerAttr(planOp.getAxis()), values);
spatial::SpatYieldOp::create(rewriter, planOp.getLoc(), concatenated);
});
rewriter.replaceOp(planOp, compute.getResults());
return success();
}
struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(LowerSpatialPlansPass)
@@ -274,6 +383,40 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
eraseAfterLowering.insert(blueprint);
continue;
}
if (auto planOp = dyn_cast<spatial::SpatGlobalAveragePoolPlanOp>(&op)) {
auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) {
auto blueprint = dyn_cast<spatial::SpatBlueprintOp>(user);
return blueprint && blueprint.getPhysicalLayout() == kRowStripLayout;
});
if (outputBlueprint == planOp.getResult().getUsers().end()) {
planOp.emitOpError("selected global AveragePool plan requires a row-strip blueprint result");
signalPassFailure();
return;
}
FailureOr<RowStripPhysicalValue> input = getRowStripValue(rowStripValues, planOp.getInput());
rewriter.setInsertionPoint(planOp);
std::optional<Value> physicalInput;
if (succeeded(input))
physicalInput = input->storage;
FailureOr<Value> lowered =
lowerSelectedGlobalAveragePoolPlan(planOp, physicalInput, rewriter);
if (failed(lowered)) {
planOp.emitOpError("failed to lower selected row-strip Spatial global AveragePool plan");
signalPassFailure();
return;
}
auto blueprint = cast<spatial::SpatBlueprintOp>(*outputBlueprint);
FailureOr<RowStripPhysicalValue> output = buildRowStripValue(blueprint, *lowered);
if (failed(output)) {
signalPassFailure();
return;
}
rowStripValues[blueprint.getResult()] = *output;
eraseAfterLowering.insert(planOp);
eraseAfterLowering.insert(blueprint);
continue;
}
if (auto planOp = dyn_cast<spatial::SpatBiasAddPlanOp>(&op)) {
if (succeeded(getRowStripValue(rowStripValues, planOp.getInput()))) {
auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) {
@@ -339,6 +482,20 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
rewriter.replaceOp(planOp, computeOp.getResults());
continue;
}
if (auto planOp = dyn_cast<spatial::SpatAddPlanOp>(&op)) {
if (failed(lowerAddPlan(planOp, rowStripValues, eraseAfterLowering, rewriter))) {
signalPassFailure();
return;
}
continue;
}
if (auto planOp = dyn_cast<spatial::SpatConcatPlanOp>(&op)) {
if (failed(lowerConcatPlan(planOp, rowStripValues, eraseAfterLowering, rewriter))) {
signalPassFailure();
return;
}
continue;
}
if (auto flattenOp = dyn_cast<spatial::SpatGraphCompute>(&op)) {
if (flattenOp.getInputs().size() == 1) {
FailureOr<RowStripPhysicalValue> input =
@@ -488,12 +645,15 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
return;
op->emitOpError("planning blueprint must not remain after LowerSpatialPlans");
hasIllegalOps = true;
} else if (isa<spatial::SpatConv2DPlanOp,
spatial::SpatBiasAddPlanOp,
spatial::SpatReluPlanOp,
spatial::SpatMaxPool2DPlanOp,
spatial::SpatMaterializeLayoutOp>(op)
|| op->getDialect()->getNamespace() == "onnx") {
}
else if (isa<spatial::SpatConv2DPlanOp,
spatial::SpatBiasAddPlanOp,
spatial::SpatAddPlanOp,
spatial::SpatReluPlanOp,
spatial::SpatMaxPool2DPlanOp,
spatial::SpatGlobalAveragePoolPlanOp,
spatial::SpatMaterializeLayoutOp>(op)
|| op->getDialect()->getNamespace() == "onnx") {
op->emitOpError("operation must not remain after LowerSpatialPlans");
hasIllegalOps = true;
}
@@ -60,11 +60,6 @@ def convAddToConvWithBiasRight : Pat<
def replaceWithOperationOfValue : NativeCodeCall<"$0">;
def removeLRN : Pat<
(ONNXLRNOp $A, $_, $_, $_, $_),
(replaceWithOperationOfValue $A)
>;
def HaveSameStaticShape: Constraint<
CPred<"onnx_mlir::haveSameStaticShape($0, $1)">,
"Two tensors have the same static shape">;
@@ -47,12 +47,17 @@ static void populateEmptyFunction(func::FuncOp funcOp) {
SmallVector<spatial::SpatGraphComputeBatch> computeBatches(funcOp.getOps<spatial::SpatGraphComputeBatch>());
SmallVector<spatial::SpatConv2DPlanOp> convPlans(funcOp.getOps<spatial::SpatConv2DPlanOp>());
SmallVector<spatial::SpatBiasAddPlanOp> biasAddPlans(funcOp.getOps<spatial::SpatBiasAddPlanOp>());
SmallVector<spatial::SpatAddPlanOp> addPlans(funcOp.getOps<spatial::SpatAddPlanOp>());
SmallVector<spatial::SpatConcatPlanOp> concatPlans(funcOp.getOps<spatial::SpatConcatPlanOp>());
SmallVector<spatial::SpatReluPlanOp> reluPlans(funcOp.getOps<spatial::SpatReluPlanOp>());
SmallVector<spatial::SpatMaxPool2DPlanOp> maxPoolPlans(funcOp.getOps<spatial::SpatMaxPool2DPlanOp>());
SmallVector<spatial::SpatGlobalAveragePoolPlanOp> globalAveragePoolPlans(
funcOp.getOps<spatial::SpatGlobalAveragePoolPlanOp>());
SmallVector<spatial::SpatBlueprintOp> blueprints(funcOp.getOps<spatial::SpatBlueprintOp>());
SmallVector<spatial::SpatMaterializeLayoutOp> materializers(funcOp.getOps<spatial::SpatMaterializeLayoutOp>());
if (!computes.empty() || !computeBatches.empty() || !convPlans.empty() || !biasAddPlans.empty() || !reluPlans.empty()
|| !maxPoolPlans.empty() || !blueprints.empty() || !materializers.empty()) {
if (!computes.empty() || !computeBatches.empty() || !convPlans.empty() || !biasAddPlans.empty() || !addPlans.empty()
|| !concatPlans.empty() || !reluPlans.empty() || !maxPoolPlans.empty() || !blueprints.empty()
|| !globalAveragePoolPlans.empty() || !materializers.empty()) {
return;
}
@@ -146,8 +146,11 @@ void verifyLogicalTopLevelOps(func::FuncOp funcOp, pim::CappedDiagnosticReporter
spatial::SpatGraphComputeBatch,
spatial::SpatConv2DPlanOp,
spatial::SpatBiasAddPlanOp,
spatial::SpatAddPlanOp,
spatial::SpatConcatPlanOp,
spatial::SpatReluPlanOp,
spatial::SpatMaxPool2DPlanOp,
spatial::SpatGlobalAveragePoolPlanOp,
spatial::SpatBlueprintOp,
spatial::SpatMaterializeLayoutOp>(&op)) {
continue;
@@ -8,7 +8,6 @@ namespace onnx_mlir {
void populatePrePatterns(RewritePatternSet& patterns, MLIRContext* ctx) { populateGeneratedPrePatterns(patterns, ctx); }
void populateConversionPatterns(RewritePatternSet& patterns, MLIRContext* ctx) {
populateGeneratedConversionPatterns(patterns, ctx);
populateElementwisePatterns(patterns, ctx);
populateMatMulRewritePatterns(patterns, ctx);
populateGemmPatterns(patterns, ctx);
@@ -13,7 +13,6 @@ void populateConversionPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRCon
void populatePostPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx);
void populateGeneratedPrePatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx);
void populateGeneratedConversionPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx);
void populateWeightPromotionPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx);
void populateConvPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx);
@@ -1,18 +0,0 @@
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Patterns.hpp"
using namespace mlir;
namespace onnx_mlir {
namespace {
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/ONNXToSpatial.hpp.inc"
} // namespace
void populateGeneratedConversionPatterns(RewritePatternSet& patterns, MLIRContext* ctx) {
patterns.add<removeLRN>(ctx);
}
} // namespace onnx_mlir
@@ -1242,8 +1242,9 @@ static Value buildPackedWeights(DenseElementsAttr wDenseAttr,
const Tiling& tiling,
PatternRewriter& rewriter,
Location loc) {
const int64_t paddedOutputChannels = static_cast<int64_t>(crossbarSize.getValue());
auto packedWeightType = RankedTensorType::get(
{tiling.numChannelTiles, tiling.tileInputRows, tiling.tileOutputChannels}, wType.getElementType());
{tiling.numChannelTiles, tiling.tileInputRows, paddedOutputChannels}, wType.getElementType());
SmallVector<Attribute> packedValues(packedWeightType.getNumElements(),
cast<Attribute>(rewriter.getZeroAttr(wType.getElementType())));
SmallVector<Attribute> sourceValues(wDenseAttr.getValues<Attribute>());
@@ -1262,7 +1263,7 @@ static Value buildPackedWeights(DenseElementsAttr wDenseAttr,
((globalOutChannel * wType.getDimSize(1) * wType.getDimSize(2)) + kernelH) * wType.getDimSize(3) + kernelW;
const int64_t targetCol = localChannel * tiling.outputMultiplier + multiplierIndex;
const int64_t targetFlatIndex =
((tileIndex * tiling.tileInputRows) + targetRow) * tiling.tileOutputChannels + targetCol;
((tileIndex * tiling.tileInputRows) + targetRow) * paddedOutputChannels + targetCol;
packedValues[targetFlatIndex] = sourceValues[sourceFlatIndex];
}
}
@@ -1353,11 +1354,12 @@ static Value createWeightTile(Value packedWeights,
PatternRewriter& rewriter,
Location loc) {
SmallVector<OpFoldResult> offsets {channelTileIndex, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
const int64_t paddedOutputChannels = static_cast<int64_t>(crossbarSize.getValue());
SmallVector<OpFoldResult> sizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tiling.tileInputRows),
rewriter.getIndexAttr(tiling.tileOutputChannels)};
rewriter.getIndexAttr(paddedOutputChannels)};
auto collapsedType =
RankedTensorType::get({tiling.tileInputRows, tiling.tileOutputChannels}, packedWeightType.getElementType());
RankedTensorType::get({tiling.tileInputRows, paddedOutputChannels}, packedWeightType.getElementType());
return extractMixedSliceOrIdentity(
rewriter, loc, packedWeights, collapsedType,
{offsets, sizes, getUnitStrides(rewriter, 3)});
@@ -1547,6 +1549,8 @@ rewriteConv(Operation* convOp, const ConvLoweringState& state, PatternRewriter&
auto gemmOutType =
RankedTensorType::get({tiling->totalPatches, state.outType.getDimSize(1)}, state.outType.getElementType());
auto rowTileType = RankedTensorType::get({1, tiling->tileOutputChannels}, state.outType.getElementType());
auto paddedRowTileType = RankedTensorType::get(
{1, static_cast<int64_t>(crossbarSize.getValue())}, state.outType.getElementType());
auto piecesType = spatial::getGraphBatchPhysicalResultType(
tiling->totalPatches * tiling->numChannelTiles, rowTileType);
auto paddedInputType = cast<RankedTensorType>(paddedInput.getType());
@@ -1617,7 +1621,17 @@ rewriteConv(Operation* convOp, const ConvLoweringState& state, PatternRewriter&
*tiling,
rewriter,
loc);
Value rowTile = spatial::SpatVMMOp::create(rewriter, loc, rowTileType, weightTile, inputTile).getResult();
Value paddedRowTile =
spatial::SpatVMMOp::create(rewriter, loc, paddedRowTileType, weightTile, inputTile).getResult();
Value rowTile = tensor::ExtractSliceOp::create(
rewriter,
loc,
rowTileType,
paddedRowTile,
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tiling->tileOutputChannels)},
getUnitStrides(rewriter, 2));
if (args.inputs.size() > 1) {
Value biasArg = pickInputByRank(/*rank=*/2);
if (!biasArg) {
@@ -2493,6 +2507,7 @@ static Value createZeroGemmBias(RankedTensorType gemmResultType, PatternRewriter
static bool rowStripOutputTileFitsOneCore(const ConvGeometry& geometry) {
return ceilIntegerDivide(geometry.k, geometry.xbarSize)
* ceilIntegerDivide(geometry.c, geometry.xbarSize)
<= static_cast<int64_t>(crossbarCountInCore.getValue());
}
@@ -2521,28 +2536,6 @@ static bool canConsumePixelMajorRowStripFragments(const ConvLoweringState& state
failureReason = "dilation_not_one";
return false;
}
const bool pointwise = state.xHeight == 1 && state.xWidth == 1 && state.outHeight == 1 && state.outWidth == 1
&& state.wHeight == 1 && state.wWidth == 1 && state.padHeightBegin == 0
&& state.padHeightEnd == 0 && state.padWidthBegin == 0 && state.padWidthEnd == 0;
if (pointwise) {
if (!getHostConstDenseElementsAttr(state.w)) {
failureReason = "non_constant_weight";
return false;
}
if (state.hasBias && !isSupportedBiasAddValue(state.b, state.outType)) {
failureReason = "unsupported_bias";
return false;
}
return true;
}
if (state.wHeight != 3 || state.wWidth != 3) {
failureReason = "kernel_not_3x3";
return false;
}
if (state.padHeightBegin != 1 || state.padHeightEnd != 1 || state.padWidthBegin != 1 || state.padWidthEnd != 1) {
failureReason = "padding_not_1";
return false;
}
if (state.outHeight != state.xHeight || state.outWidth != state.xWidth) {
failureReason = "not_same_spatial_shape";
return false;
@@ -3043,57 +3036,154 @@ static FailureOr<Value> createConvOutputRow(ValueRange inputTiles,
Location loc) {
auto elementType = cast<RankedTensorType>(inputTiles.front().getType()).getElementType();
auto rowType = RankedTensorType::get({1, outputChannels}, elementType);
auto tileWeightsType =
RankedTensorType::get({paddedK, xbarDim},
cast<RankedTensorType>(paddedWeights.getType()).getElementType());
const int64_t outputTileCount = ceilIntegerDivide(outputChannels, xbarDim);
auto getTileWeights = [&](int64_t outputTile) {
if (outputTileCount == 1)
return paddedWeights;
SmallVector<OpFoldResult> offsets {
rewriter.getIndexAttr(outputTile), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> sizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(paddedK), rewriter.getIndexAttr(xbarDim)};
return extractStaticSliceOrIdentity(
rewriter, loc, paddedWeights, tileWeightsType, offsets, sizes, getUnitStrides(rewriter, 3));
};
if (outputTileCount == 1) {
FailureOr<Value> rowResult = createConvOutputTile(
inputTiles, getTileWeights(0), outputChannels, xbarDim, rewriter, loc);
if (failed(rowResult))
return failure();
Value validRow = *rowResult;
if (bias)
validRow = spatial::SpatVAddOp::create(rewriter, loc, rowType, validRow, bias).getResult();
return validRow;
}
const int64_t paddedOutputChannels = outputTileCount * xbarDim;
auto paddedOutputType = RankedTensorType::get({1, paddedOutputChannels}, elementType);
Value paddedOutput = tensor::EmptyOp::create(rewriter, loc, paddedOutputType.getShape(), elementType);
for (int64_t outputTile = 0; outputTile < outputTileCount; ++outputTile) {
FailureOr<Value> tileResult = createConvOutputTile(
inputTiles, getTileWeights(outputTile), xbarDim, xbarDim, rewriter, loc);
if (failed(tileResult))
return failure();
SmallVector<OpFoldResult> tileOffsets {
rewriter.getIndexAttr(0), rewriter.getIndexAttr(outputTile * xbarDim)};
SmallVector<OpFoldResult> tileSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(xbarDim)};
paddedOutput = tensor::InsertSliceOp::create(
rewriter, loc, *tileResult, paddedOutput, tileOffsets, tileSizes, getUnitStrides(rewriter, 2));
auto weightSliceType = RankedTensorType::get(
{xbarDim, paddedOutputChannels},
cast<RankedTensorType>(paddedWeights.getType()).getElementType());
Value paddedOutput;
for (auto [kSlice, inputTile] : llvm::enumerate(inputTiles)) {
const int64_t kOffset = static_cast<int64_t>(kSlice) * xbarDim;
Value weightSlice = extractStaticSliceOrIdentity(
rewriter,
loc,
paddedWeights,
weightSliceType,
SmallVector<OpFoldResult> {rewriter.getIndexAttr(kOffset), rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(xbarDim),
rewriter.getIndexAttr(paddedOutputChannels)},
getUnitStrides(rewriter, 2));
Value piece =
spatial::SpatVMMOp::create(rewriter, loc, paddedOutputType, weightSlice, inputTile).getResult();
paddedOutput = paddedOutput
? spatial::SpatVAddOp::create(
rewriter, loc, paddedOutputType, paddedOutput, piece).getResult()
: piece;
}
SmallVector<OpFoldResult> outputOffsets {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> outputSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(outputChannels)};
Value validRow = tensor::ExtractSliceOp::create(
rewriter, loc, rowType, paddedOutput, outputOffsets, outputSizes, getUnitStrides(rewriter, 2));
Value validRow = outputChannels == paddedOutputChannels
? paddedOutput
: tensor::ExtractSliceOp::create(
rewriter,
loc,
rowType,
paddedOutput,
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(outputChannels)},
getUnitStrides(rewriter, 2))
.getResult();
if (bias)
validRow = spatial::SpatVAddOp::create(rewriter, loc, rowType, validRow, bias).getResult();
return validRow;
}
static FailureOr<Value> createRowStripConvOutput(const ConvLoweringState& state,
Value input,
Value paddedWeights,
Value bias,
int64_t paddedK,
int64_t numKSlices,
int64_t xbarDim,
PatternRewriter& rewriter,
Location loc) {
const int64_t laneCount = state.outHeight;
const int64_t patchSize = state.numChannelsIn * state.wHeight * state.wWidth;
const bool hasPartialInputTile = patchSize % xbarDim != 0;
auto elementType = state.outType.getElementType();
auto partialInputScratchType = RankedTensorType::get({1, xbarDim}, elementType);
auto outputPixelType = RankedTensorType::get({1, 1, 1, state.numChannelsOut}, elementType);
auto fragmentType = getRowStripFragmentType(state.outType);
auto storageType = getRowStripStorageType(state.outType);
auto batch = createSpatComputeBatch(
rewriter,
loc,
TypeRange {storageType},
laneCount,
ValueRange {paddedWeights},
bias ? ValueRange {input, bias} : ValueRange {input},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cOutWidth = getOrCreateIndexConstant(rewriter, anchorOp, state.outWidth);
FailureOr<Value> inputWindow =
createConvInputWindow(args.inputs.front(), state, args.lane, rewriter, loc);
if (failed(inputWindow))
return failure();
Value fragmentInit = tensor::EmptyOp::create(rewriter, loc, fragmentType.getShape(), elementType);
SmallVector<Value> loopInit {fragmentInit};
if (hasPartialInputTile)
loopInit.push_back(createZeroTensorConstant(partialInputScratchType, rewriter));
auto loop = buildNormalizedScfFor(
rewriter,
loc,
c0,
cOutWidth,
c1,
loopInit,
[&](OpBuilder&,
Location pixelLoc,
Value localColumn,
ValueRange iterArgs,
SmallVectorImpl<Value>& yielded) {
Value partialInputScratch = hasPartialInputTile ? iterArgs[1] : Value();
FailureOr<SmallVector<Value>> inputTiles = createConvInputTiles(*inputWindow,
state,
localColumn,
partialInputScratch,
patchSize,
numKSlices,
xbarDim,
rewriter,
pixelLoc);
if (failed(inputTiles))
return failure();
FailureOr<Value> output = createConvOutputRow(*inputTiles,
paddedK,
state.numChannelsOut,
args.weights.front(),
bias ? args.inputs[1] : Value(),
xbarDim,
rewriter,
pixelLoc);
if (failed(output))
return failure();
Value outputPixel = tensor::ExpandShapeOp::create(
rewriter, pixelLoc, outputPixelType, *output, SmallVector<ReassociationIndices> {{0, 1, 2}, {3}});
Value next = tensor::InsertSliceOp::create(
rewriter,
pixelLoc,
outputPixel,
iterArgs.front(),
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0),
rewriter.getIndexAttr(0),
localColumn,
rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(state.numChannelsOut)},
getUnitStrides(rewriter, 4));
yielded.push_back(next);
if (hasPartialInputTile)
yielded.push_back(partialInputScratch);
return success();
});
if (failed(loop))
return failure();
publishGraphBatchPhysicalFragment(
rewriter, loc, loop->results.front(), args.outputs.front(), args.lane);
return success();
});
if (failed(batch))
return failure();
return batch->getResult(0);
}
static FailureOr<Value> createOutputChannelTiledRowStripConvOutput(const ConvLoweringState& state,
Value input,
Value paddedWeights,
@@ -3229,21 +3319,11 @@ static FailureOr<Value>
const int64_t patchSize = state.numChannelsIn * state.wHeight * state.wWidth;
const int64_t numKSlices = ceilIntegerDivide(patchSize, xbarDim);
const int64_t paddedK = numKSlices * xbarDim;
const bool hasPartialInputTile = patchSize % xbarDim != 0;
auto elementType = state.outType.getElementType();
auto partialInputScratchType = RankedTensorType::get({1, xbarDim}, elementType);
auto fragmentType = getRowStripFragmentType(state.outType);
auto outputPixelType = RankedTensorType::get({1, 1, 1, state.numChannelsOut}, elementType);
auto outputStorageType = getRowStripStorageType(state.outType);
Value paddedWeights = state.numChannelsOut <= xbarDim
? standard::createPaddedPixelMajorWeightConstant(
weightDenseAttr, state, paddedK, xbarDim, rewriter)
: standard::createPaddedOutputChannelTiledWeightConstant(
weightDenseAttr, state, paddedK, xbarDim, rewriter);
if (state.numChannelsOut > xbarDim)
return createOutputChannelTiledRowStripConvOutput(
state, state.x, paddedWeights, paddedK, numKSlices, xbarDim, rewriter, loc);
const int64_t paddedOutputChannels =
ceilIntegerDivide(state.numChannelsOut, xbarDim) * xbarDim;
Value paddedWeights = standard::createPaddedPixelMajorWeightConstant(
weightDenseAttr, state, paddedK, paddedOutputChannels, rewriter);
FailureOr<Value> bias = failure();
if (state.hasBias)
@@ -3251,83 +3331,9 @@ static FailureOr<Value>
if (state.hasBias && failed(bias))
return failure();
auto batchOp = createSpatComputeBatch(
rewriter,
loc,
TypeRange {outputStorageType},
state.outHeight,
ValueRange {paddedWeights},
state.hasBias ? ValueRange {state.x, *bias} : ValueRange {state.x},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cOutWidth = getOrCreateIndexConstant(rewriter, anchorOp, state.outWidth);
FailureOr<Value> inputWindow =
createConvInputWindow(args.inputs.front(), state, args.lane, rewriter, loc);
if (failed(inputWindow))
return failure();
Value fragmentInit = tensor::EmptyOp::create(rewriter, loc, fragmentType.getShape(), elementType);
SmallVector<Value> widthLoopInit {fragmentInit};
if (hasPartialInputTile)
widthLoopInit.push_back(createZeroTensorConstant(partialInputScratchType, rewriter));
auto widthLoop = buildNormalizedScfFor(
rewriter,
loc,
c0,
cOutWidth,
c1,
widthLoopInit,
[&](OpBuilder&, Location widthLoc, Value widthIndex, ValueRange widthIterArgs, SmallVectorImpl<Value>& widthYielded) {
Value partialInputScratch = hasPartialInputTile ? widthIterArgs[1] : Value();
FailureOr<SmallVector<Value>> inputTiles = createConvInputTiles(*inputWindow,
state,
widthIndex,
partialInputScratch,
patchSize,
numKSlices,
xbarDim,
rewriter,
widthLoc);
if (failed(inputTiles))
return failure();
FailureOr<Value> outputRow = createConvOutputRow(*inputTiles,
paddedK,
state.numChannelsOut,
args.weights.front(),
state.hasBias ? args.inputs[1] : Value(),
xbarDim,
rewriter,
widthLoc);
if (failed(outputRow))
return failure();
Value outputFragment = tensor::ExpandShapeOp::create(rewriter,
widthLoc,
outputPixelType,
*outputRow,
SmallVector<ReassociationIndices> {{0, 1, 2}, {3}});
SmallVector<OpFoldResult> rowOffsets {
rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), widthIndex, rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> rowSizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1),
rewriter.getIndexAttr(state.numChannelsOut)};
Value nextFragment = tensor::InsertSliceOp::create(
rewriter, widthLoc, outputFragment, widthIterArgs.front(), rowOffsets, rowSizes, getUnitStrides(rewriter, 4));
widthYielded.push_back(nextFragment);
if (hasPartialInputTile)
widthYielded.push_back(partialInputScratch);
return success();
});
if (failed(widthLoop))
return failure();
insertRowStripFragment(widthLoop->results.front(), args.outputs.front(), state.outType, args.lane, rewriter, loc);
return success();
});
if (failed(batchOp))
return failure();
return batchOp->getResult(0);
return createRowStripConvOutput(
state, state.x, paddedWeights, state.hasBias ? *bias : Value(),
paddedK, numKSlices, xbarDim, rewriter, loc);
}
static FailureOr<Value> createConvOutputFromPixelMajorRowStripFragments(Value rowStripStorage,
@@ -3346,105 +3352,22 @@ static FailureOr<Value> createConvOutputFromPixelMajorRowStripFragments(Value ro
const int64_t patchSize = state.numChannelsIn * state.wHeight * state.wWidth;
const int64_t numKSlices = ceilIntegerDivide(patchSize, xbarDim);
const int64_t paddedK = numKSlices * xbarDim;
const bool hasPartialInputTile = patchSize % xbarDim != 0;
auto elementType = state.outType.getElementType();
auto partialInputScratchType = RankedTensorType::get({1, xbarDim}, elementType);
auto outputPixelType = RankedTensorType::get({1, 1, 1, state.numChannelsOut}, elementType);
auto outputStorageType = getRowStripStorageType(state.outType);
auto weightDenseAttr = getHostConstDenseElementsAttr(state.w);
if (!weightDenseAttr)
return failure();
Value paddedWeights = state.numChannelsOut <= xbarDim
? standard::createPaddedPixelMajorWeightConstant(
weightDenseAttr, state, paddedK, xbarDim, rewriter)
: standard::createPaddedOutputChannelTiledWeightConstant(
weightDenseAttr, state, paddedK, xbarDim, rewriter);
if (state.numChannelsOut > xbarDim)
return createOutputChannelTiledRowStripConvOutput(
state, rowStripStorage, paddedWeights, paddedK, numKSlices, xbarDim, rewriter, loc);
const int64_t paddedOutputChannels =
ceilIntegerDivide(state.numChannelsOut, xbarDim) * xbarDim;
Value paddedWeights = standard::createPaddedPixelMajorWeightConstant(
weightDenseAttr, state, paddedK, paddedOutputChannels, rewriter);
FailureOr<Value> bias = failure();
if (state.hasBias)
bias = createBiasRowConstant(state, rewriter);
if (state.hasBias && failed(bias))
return failure();
auto batchOp = createSpatComputeBatch(
rewriter,
loc,
TypeRange {outputStorageType},
state.outHeight,
ValueRange {paddedWeights},
state.hasBias ? ValueRange {rowStripStorage, *bias} : ValueRange {rowStripStorage},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cOutWidth = getOrCreateIndexConstant(rewriter, anchorOp, state.outWidth);
auto fragmentType = getRowStripFragmentType(state.outType);
FailureOr<Value> inputWindow = createConvInputWindow(args.inputs.front(), state, args.lane, rewriter, loc);
if (failed(inputWindow))
return failure();
Value fragmentInit = tensor::EmptyOp::create(rewriter, loc, fragmentType.getShape(), elementType);
SmallVector<Value> widthLoopInit {fragmentInit};
if (hasPartialInputTile)
widthLoopInit.push_back(createZeroTensorConstant(partialInputScratchType, rewriter));
auto widthLoop = buildNormalizedScfFor(
rewriter,
loc,
c0,
cOutWidth,
c1,
widthLoopInit,
[&](OpBuilder&, Location widthLoc, Value widthIndex, ValueRange widthIterArgs, SmallVectorImpl<Value>& widthYielded) {
Value partialInputScratch = hasPartialInputTile ? widthIterArgs[1] : Value();
FailureOr<SmallVector<Value>> inputTiles = createConvInputTiles(*inputWindow,
state,
widthIndex,
partialInputScratch,
patchSize,
numKSlices,
xbarDim,
rewriter,
widthLoc);
if (failed(inputTiles))
return failure();
FailureOr<Value> outputRow = createConvOutputRow(*inputTiles,
paddedK,
state.numChannelsOut,
args.weights.front(),
state.hasBias ? args.inputs[1] : Value(),
xbarDim,
rewriter,
widthLoc);
if (failed(outputRow))
return failure();
Value outputFragment = tensor::ExpandShapeOp::create(rewriter,
widthLoc,
outputPixelType,
*outputRow,
SmallVector<ReassociationIndices> {{0, 1, 2}, {3}});
SmallVector<OpFoldResult> rowOffsets {
rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), widthIndex, rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> rowSizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1),
rewriter.getIndexAttr(state.numChannelsOut)};
Value nextFragment = tensor::InsertSliceOp::create(
rewriter, widthLoc, outputFragment, widthIterArgs.front(), rowOffsets, rowSizes, getUnitStrides(rewriter, 4));
widthYielded.push_back(nextFragment);
if (hasPartialInputTile)
widthYielded.push_back(partialInputScratch);
return success();
});
if (failed(widthLoop))
return failure();
insertRowStripFragment(widthLoop->results.front(), args.outputs.front(), state.outType, args.lane, rewriter, loc);
return success();
});
if (failed(batchOp))
return failure();
return batchOp->getResult(0);
return createRowStripConvOutput(
state, rowStripStorage, paddedWeights, state.hasBias ? *bias : Value(),
paddedK, numKSlices, xbarDim, rewriter, loc);
}
static FailureOr<Value> createPointwiseOutputFromRowStripFragments(Value rowStripStorage,
@@ -193,6 +193,13 @@ struct AddToSpatialCompute : OpConversionPattern<ONNXAddOp> {
return success();
}
if (resultType.getRank() == 4 && adaptor.getA().getType() == resultType && adaptor.getB().getType() == resultType) {
auto plan = spatial::SpatAddPlanOp::create(
rewriter, op.getLoc(), resultType, adaptor.getA(), adaptor.getB(), rewriter.getStringAttr("nchw"));
rewriter.replaceOp(op, plan.getResult());
return success();
}
auto lhs = prepareElementwiseOperand(adaptor.getA(), resultType, rewriter, op.getLoc());
if (failed(lhs))
return failure();
@@ -246,6 +246,15 @@ struct PoolToSpatialComputeBase : public OpConversionPattern<PoolOp> {
return success();
}
}
else if (batchSize == 1 && outputHeight == 1 && outputWidth == 1
&& kernelHeight == inputHeight && kernelWidth == inputWidth
&& dilationHeight == 1 && dilationWidth == 1 && padTop == 0
&& padLeft == 0 && padBottom == 0 && padRight == 0) {
auto plan = spatial::SpatGlobalAveragePoolPlanOp::create(
rewriter, loc, outType, x, rewriter.getStringAttr("nchw"));
rewriter.replaceOp(poolOp, plan.getResult());
return success();
}
const int64_t xbarSize = static_cast<int64_t>(crossbarSize.getValue());
const int64_t channelTileCount = (channels + xbarSize - 1) / xbarSize;
@@ -676,6 +685,132 @@ FailureOr<Value> lowerSelectedMaxPool2DPlan(spatial::SpatMaxPool2DPlanOp planOp,
return batch->getResult(0);
}
LogicalResult canLowerGlobalAveragePoolPlanToRowStrip(spatial::SpatGlobalAveragePoolPlanOp planOp) {
auto inputType = dyn_cast<RankedTensorType>(planOp.getInput().getType());
auto outputType = dyn_cast<RankedTensorType>(planOp.getOutput().getType());
if (!inputType || !outputType || !inputType.hasStaticShape() || !outputType.hasStaticShape())
return failure();
if (inputType.getRank() != 4 || outputType.getRank() != 4 || inputType.getDimSize(0) != 1
|| outputType.getDimSize(0) != 1 || inputType.getDimSize(1) != outputType.getDimSize(1)
|| outputType.getDimSize(2) != 1 || outputType.getDimSize(3) != 1)
return failure();
return success();
}
FailureOr<Value> lowerSelectedGlobalAveragePoolPlan(spatial::SpatGlobalAveragePoolPlanOp planOp,
std::optional<Value> rowStripInput,
PatternRewriter& rewriter) {
if (failed(canLowerGlobalAveragePoolPlanToRowStrip(planOp)))
return failure();
Location loc = planOp.getLoc();
auto inputType = cast<RankedTensorType>(planOp.getInput().getType());
auto outputType = cast<RankedTensorType>(planOp.getOutput().getType());
auto elementType = dyn_cast<FloatType>(inputType.getElementType());
if (!elementType)
return failure();
Value input = rowStripInput.value_or(planOp.getInput());
auto actualInputType = dyn_cast<RankedTensorType>(input.getType());
FailureOr<RowStripPhysicalValue> physicalValue = describeRowStripPhysicalValue(input, inputType);
const bool physicalInput = succeeded(physicalValue);
if (!physicalInput && actualInputType != inputType)
return failure();
const int64_t height = inputType.getDimSize(2);
const int64_t width = inputType.getDimSize(3);
const int64_t channels = inputType.getDimSize(1);
const int64_t tilesPerRow = physicalInput ? physicalValue->tilesPerRow : 1;
auto inputFragmentType =
physicalInput ? physicalValue->fragmentType : getRowStripFragmentType(inputType);
auto nchwInputFragmentType = RankedTensorType::get(
{1, channels, 1, width}, inputType.getElementType(), inputType.getEncoding());
auto outputFragmentType = RankedTensorType::get(
{1, 1, 1, inputFragmentType.getDimSize(3)}, elementType, outputType.getEncoding());
auto outputStorageType =
spatial::getGraphBatchPhysicalResultType(tilesPerRow, outputFragmentType);
auto zero = getOrCreateConstant(
rewriter, rewriter.getInsertionBlock()->getParentOp(), rewriter.getZeroAttr(outputFragmentType), outputFragmentType);
auto scaleAttr = DenseElementsAttr::get(
outputFragmentType, rewriter.getFloatAttr(elementType, 1.0 / static_cast<double>(height * width)));
auto scale = getOrCreateConstant(
rewriter, rewriter.getInsertionBlock()->getParentOp(), scaleAttr, outputFragmentType);
auto batch = createSpatComputeBatch(
rewriter,
loc,
TypeRange {outputStorageType},
tilesPerRow,
ValueRange {zero, scale},
ValueRange {input},
[&](detail::SpatComputeBatchBodyArgs args) -> LogicalResult {
Value reduced = args.weights[0];
for (int64_t row = 0; row < height; ++row) {
Value fragment;
if (physicalInput) {
Value sourceSlot = args.lane;
if (row != 0)
sourceSlot = arith::AddIOp::create(
rewriter,
loc,
sourceSlot,
getOrCreateIndexConstant(
rewriter, rewriter.getInsertionBlock()->getParentOp(), row * tilesPerRow));
FailureOr<Value> physicalFragment = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs.front(), sourceSlot, inputFragmentType);
if (failed(physicalFragment))
return failure();
fragment = *physicalFragment;
}
else {
Value nchw = tensor::ExtractSliceOp::create(
rewriter,
loc,
nchwInputFragmentType,
args.inputs.front(),
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0),
rewriter.getIndexAttr(0),
rewriter.getIndexAttr(row),
rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(channels),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(width)},
getUnitStrides(rewriter, 4));
fragment = ONNXTransposeOp::create(
rewriter, loc, inputFragmentType, nchw, rewriter.getI64ArrayAttr({0, 2, 3, 1}));
}
for (int64_t column = 0; column < width; ++column) {
Value point = tensor::ExtractSliceOp::create(
rewriter,
loc,
outputFragmentType,
fragment,
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0),
rewriter.getIndexAttr(0),
rewriter.getIndexAttr(column),
rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(inputFragmentType.getDimSize(3))},
getUnitStrides(rewriter, 4));
point = materializeTileTensor(rewriter, loc, point);
reduced = spatial::SpatVAddOp::create(
rewriter, loc, outputFragmentType, reduced, point);
}
}
reduced = spatial::SpatVMulOp::create(
rewriter, loc, outputFragmentType, reduced, args.weights[1]);
publishGraphBatchPhysicalFragment(
rewriter, loc, reduced, args.outputs.front(), args.lane);
return success();
});
if (failed(batch))
return failure();
return batch->getResult(0);
}
void populatePoolPatterns(RewritePatternSet& patterns, MLIRContext* ctx) {
patterns.insert<PoolToSpatialCompute<ONNXMaxPoolSingleOutOp>>(ctx);
patterns.insert<PoolToSpatialCompute<ONNXAveragePoolOp>>(ctx);
@@ -25,6 +25,17 @@ struct Concat : public OpConversionPattern<ONNXConcatOp> {
return success();
}
auto resultType = dyn_cast<RankedTensorType>(maxpoolOp.getResult().getType());
if (axis == 1 && resultType && resultType.hasStaticShape() && resultType.getRank() == 4
&& llvm::all_of(inputs, [](Value input) {
auto type = dyn_cast<RankedTensorType>(input.getType());
return type && type.hasStaticShape() && type.getRank() == 4;
})) {
rewriter.replaceOpWithNewOp<spatial::SpatConcatPlanOp>(
maxpoolOp, resultType, inputs, rewriter.getI64IntegerAttr(axis), rewriter.getStringAttr("nchw"));
return success();
}
auto computeOp = createSpatCompute(
rewriter, maxpoolOp.getLoc(), TypeRange {maxpoolOp.getResult().getType()}, {}, inputs, [&](ValueRange args) {
spatial::SpatYieldOp::create(
@@ -27,6 +27,14 @@ lowerSelectedMaxPool2DPlan(spatial::SpatMaxPool2DPlanOp planOp,
std::optional<mlir::Value> rowStripInput,
mlir::PatternRewriter& rewriter);
mlir::LogicalResult
canLowerGlobalAveragePoolPlanToRowStrip(spatial::SpatGlobalAveragePoolPlanOp planOp);
mlir::FailureOr<mlir::Value>
lowerSelectedGlobalAveragePoolPlan(spatial::SpatGlobalAveragePoolPlanOp planOp,
std::optional<mlir::Value> rowStripInput,
mlir::PatternRewriter& rewriter);
mlir::LogicalResult canLowerFlattenFromRowStrip(spatial::SpatGraphCompute flattenOp);
mlir::LogicalResult lowerFlattenFromRowStrip(const RowStripPhysicalValue& input,
@@ -36,10 +36,16 @@ static bool usesSelectedRowStrip(Operation* user, llvm::DenseMap<Value, Selected
return getSelectedLayout(layouts, reluPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto biasAddPlan = dyn_cast<spatial::SpatBiasAddPlanOp>(user))
return getSelectedLayout(layouts, biasAddPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto addPlan = dyn_cast<spatial::SpatAddPlanOp>(user))
return getSelectedLayout(layouts, addPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto concatPlan = dyn_cast<spatial::SpatConcatPlanOp>(user))
return getSelectedLayout(layouts, concatPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto convPlan = dyn_cast<spatial::SpatConv2DPlanOp>(user))
return getSelectedLayout(layouts, convPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto maxPoolPlan = dyn_cast<spatial::SpatMaxPool2DPlanOp>(user))
return getSelectedLayout(layouts, maxPoolPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto averagePoolPlan = dyn_cast<spatial::SpatGlobalAveragePoolPlanOp>(user))
return getSelectedLayout(layouts, averagePoolPlan.getResult()) == SelectedLayout::PixelMajorRowStrip;
if (auto flattenCompute = dyn_cast<spatial::SpatGraphCompute>(user))
return succeeded(canLowerFlattenFromRowStrip(flattenCompute));
return false;
@@ -62,10 +68,16 @@ static bool canConsumeRowStripAsUser(Operation* user) {
auto resultType = dyn_cast<RankedTensorType>(biasAddPlan.getOutput().getType());
return resultType && isSupportedBiasAddValue(biasAddPlan.getBias(), resultType);
}
if (isa<spatial::SpatAddPlanOp>(user))
return true;
if (isa<spatial::SpatConcatPlanOp>(user))
return true;
if (auto convPlan = dyn_cast<spatial::SpatConv2DPlanOp>(user))
return succeeded(canConsumeAndProduceRowStrip(convPlan));
if (auto maxPoolPlan = dyn_cast<spatial::SpatMaxPool2DPlanOp>(user))
return succeeded(canLowerMaxPoolPlanToRowStrip(maxPoolPlan));
if (auto averagePoolPlan = dyn_cast<spatial::SpatGlobalAveragePoolPlanOp>(user))
return succeeded(canLowerGlobalAveragePoolPlanToRowStrip(averagePoolPlan));
return false;
}
@@ -118,11 +130,38 @@ static SelectedLayout chooseBiasAddLayout(spatial::SpatBiasAddPlanOp biasAddPlan
return SelectedLayout::PixelMajorRowStrip;
}
static SelectedLayout chooseAddLayout(spatial::SpatAddPlanOp addPlan, llvm::DenseMap<Value, SelectedLayout>& layouts) {
if (getSelectedLayout(layouts, addPlan.getLhs()) != SelectedLayout::PixelMajorRowStrip
|| getSelectedLayout(layouts, addPlan.getRhs()) != SelectedLayout::PixelMajorRowStrip)
return SelectedLayout::DenseNchw;
if (!allUsersCanHandleRowStrip(addPlan.getResult(), layouts))
return SelectedLayout::DenseNchw;
return SelectedLayout::PixelMajorRowStrip;
}
static SelectedLayout chooseConcatLayout(spatial::SpatConcatPlanOp concatPlan,
llvm::DenseMap<Value, SelectedLayout>& layouts) {
if (llvm::any_of(concatPlan.getInputs(), [&](Value input) {
return getSelectedLayout(layouts, input) != SelectedLayout::PixelMajorRowStrip;
}))
return SelectedLayout::DenseNchw;
if (!allUsersCanHandleRowStrip(concatPlan.getResult(), layouts))
return SelectedLayout::DenseNchw;
return SelectedLayout::PixelMajorRowStrip;
}
static SelectedLayout chooseMaxPoolLayout(spatial::SpatMaxPool2DPlanOp maxPoolPlan) {
return succeeded(canLowerMaxPoolPlanToRowStrip(maxPoolPlan)) ? SelectedLayout::PixelMajorRowStrip
: SelectedLayout::DenseNchw;
}
static SelectedLayout chooseGlobalAveragePoolLayout(
spatial::SpatGlobalAveragePoolPlanOp averagePoolPlan) {
return succeeded(canLowerGlobalAveragePoolPlanToRowStrip(averagePoolPlan))
? SelectedLayout::PixelMajorRowStrip
: SelectedLayout::DenseNchw;
}
static spatial::SpatBlueprintOp insertRowStripBlueprint(IRRewriter& rewriter, Value value) {
auto outputType = cast<RankedTensorType>(value.getType());
auto [offsets, sizes] = buildRowStripMetadata(outputType);
@@ -215,6 +254,22 @@ struct SpatialLayoutPlanningPass final : PassWrapper<SpatialLayoutPlanningPass,
}
continue;
}
if (auto addPlan = dyn_cast<spatial::SpatAddPlanOp>(&op)) {
SelectedLayout selected = chooseAddLayout(addPlan, layouts);
if (layouts[addPlan.getResult()] != selected) {
layouts[addPlan.getResult()] = selected;
changed = true;
}
continue;
}
if (auto concatPlan = dyn_cast<spatial::SpatConcatPlanOp>(&op)) {
SelectedLayout selected = chooseConcatLayout(concatPlan, layouts);
if (layouts[concatPlan.getResult()] != selected) {
layouts[concatPlan.getResult()] = selected;
changed = true;
}
continue;
}
if (auto maxPoolPlan = dyn_cast<spatial::SpatMaxPool2DPlanOp>(&op)) {
SelectedLayout selected = chooseMaxPoolLayout(maxPoolPlan);
if (layouts[maxPoolPlan.getResult()] != selected) {
@@ -223,6 +278,14 @@ struct SpatialLayoutPlanningPass final : PassWrapper<SpatialLayoutPlanningPass,
}
continue;
}
if (auto averagePoolPlan = dyn_cast<spatial::SpatGlobalAveragePoolPlanOp>(&op)) {
SelectedLayout selected = chooseGlobalAveragePoolLayout(averagePoolPlan);
if (layouts[averagePoolPlan.getResult()] != selected) {
layouts[averagePoolPlan.getResult()] = selected;
changed = true;
}
continue;
}
}
}
@@ -232,10 +295,16 @@ struct SpatialLayoutPlanningPass final : PassWrapper<SpatialLayoutPlanningPass,
producedValue = convPlan.getResult();
else if (auto biasAddPlan = dyn_cast<spatial::SpatBiasAddPlanOp>(&op))
producedValue = biasAddPlan.getResult();
else if (auto addPlan = dyn_cast<spatial::SpatAddPlanOp>(&op))
producedValue = addPlan.getResult();
else if (auto concatPlan = dyn_cast<spatial::SpatConcatPlanOp>(&op))
producedValue = concatPlan.getResult();
else if (auto reluPlan = dyn_cast<spatial::SpatReluPlanOp>(&op))
producedValue = reluPlan.getResult();
else if (auto maxPoolPlan = dyn_cast<spatial::SpatMaxPool2DPlanOp>(&op))
producedValue = maxPoolPlan.getResult();
else if (auto averagePoolPlan = dyn_cast<spatial::SpatGlobalAveragePoolPlanOp>(&op))
producedValue = averagePoolPlan.getResult();
else
continue;
@@ -264,7 +264,13 @@ LogicalResult raptor::SpatialToPimPass::enlargeVMMOutTensorsToCrossbarSize(func:
auto outputType = cast<RankedTensorType>(vmmOp.getOutput().getType());
ArrayRef<int64_t> outputShape = outputType.getShape();
assert(isHVectorShape(outputShape) && "expected a horizontal vector output");
assert(outputShape[1] <= static_cast<int64_t>(crossbarSize) && "output width must fit in one crossbar");
auto weightType = cast<RankedTensorType>(vmmOp.getWeight().getType());
const int64_t xbarDim = static_cast<int64_t>(crossbarSize);
const int64_t paddedOutputWidth = ceilIntegerDivide(outputShape[1], xbarDim) * xbarDim;
assert(weightType.getRank() == 2 && weightType.getDimSize(1) == paddedOutputWidth
&& "expected VMM weight width to match the padded output width");
assert(paddedOutputWidth / xbarDim <= static_cast<int64_t>(crossbarCountInCore)
&& "output width must fit in one core");
rewriter.setInsertionPoint(vmmOp);
auto paddedInput = padHVectorInputToCrossbarSize(rewriter, vmmOp.getLoc(), vmmOp.getInput());
@@ -273,8 +279,8 @@ LogicalResult raptor::SpatialToPimPass::enlargeVMMOutTensorsToCrossbarSize(func:
return WalkResult::interrupt();
}
auto paddedOutputType = RankedTensorType::get(
{outputShape[0], static_cast<int64_t>(crossbarSize)}, outputType.getElementType(), outputType.getEncoding());
Value paddedOutputBuffer = outputShape[1] == static_cast<int64_t>(crossbarSize)
{outputShape[0], paddedOutputWidth}, outputType.getElementType(), outputType.getEncoding());
Value paddedOutputBuffer = outputShape[1] == paddedOutputWidth
? vmmOp.getOutputBuffer()
: createEmptyTensorFromShaped(rewriter, vmmOp.getLoc(), paddedOutputType).getResult();
vmmOp.getInputMutable().assign(*paddedInput);
@@ -282,7 +288,7 @@ LogicalResult raptor::SpatialToPimPass::enlargeVMMOutTensorsToCrossbarSize(func:
vmmOp.getOutput().setType(paddedOutputType);
if (outputShape[1] == static_cast<int64_t>(crossbarSize))
if (outputShape[1] == paddedOutputWidth)
return WalkResult::advance();
SmallVector<OpFoldResult> offsets = {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
+8 -5
View File
@@ -143,18 +143,21 @@ LogicalResult PimVMMOp::verify() {
int64_t M = matrixShape[1];
if (N <= 0 || M <= 0)
return emitError("matrix shape must be (N, M) with N > 0 and M > 0");
if (N > static_cast<int64_t>(crossbarSize) || M > static_cast<int64_t>(crossbarSize))
return emitError("matrix dimensions must fit in one crossbar");
const int64_t xbarDim = static_cast<int64_t>(crossbarSize);
if (N > xbarDim || M > xbarDim * static_cast<int64_t>(crossbarCountInCore))
return emitError("matrix dimensions must fit in one array group");
if (M % xbarDim != 0)
return emitError("matrix output width must be padded to a whole number of crossbars");
int64_t vector1 = vectorShape[0];
int64_t vectorWidth = vectorShape[1];
if (vector1 != 1 || vectorWidth != static_cast<int64_t>(crossbarSize))
if (vector1 != 1 || vectorWidth != xbarDim)
return emitError("vector shape must be (1, crossbar-size)");
int64_t output1 = outputShape[0];
int64_t outputWidth = outputShape[1];
if (output1 != 1 || outputWidth != static_cast<int64_t>(crossbarSize))
return emitError("output shape must be (1, crossbar-size)");
if (output1 != 1 || outputWidth != M)
return emitError("output shape must match the array-group width");
return success();
}
@@ -6,7 +6,9 @@
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "mlir/IR/Dominance.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Interfaces/DestinationStyleOpInterface.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
@@ -78,6 +80,15 @@ lowerMemRefCopyToPimCopy(memref::CopyOp copyOp,
bool sourceIsDevice = isDeviceLocalPimAddress(copyOp.getSource(), knowledge);
bool targetIsDevice = isDeviceLocalPimAddress(copyOp.getTarget(), knowledge);
auto sourceAddress = resolveContiguousAddress(copyOp.getSource(), knowledge);
auto targetAddress = resolveContiguousAddress(copyOp.getTarget(), knowledge);
if (succeeded(sourceAddress) && succeeded(targetAddress)
&& sourceAddress->base == targetAddress->base
&& sourceAddress->byteOffset == targetAddress->byteOffset) {
rewriter.eraseOp(copyOp);
return success();
}
if (targetIsDevice && sourceIsHost) {
pim::PimMemCopyHostToDevOp::create(rewriter,
copyOp.getLoc(),
@@ -184,6 +195,62 @@ static void forwardSingleConsumerContiguousInputCopies(func::FuncOp funcOp) {
}
}
static void forwardSingleConsumerPimOutputCopies(func::FuncOp funcOp) {
DominanceInfo dominance(funcOp);
SmallVector<memref::CopyOp> copies;
funcOp.walk([&](memref::CopyOp copy) { copies.push_back(copy); });
for (memref::CopyOp copy : copies) {
Value copiedSource = copy.getSource();
auto expand = copiedSource.getDefiningOp<memref::ExpandShapeOp>();
Value producerOutput = expand ? expand.getSrc() : copiedSource;
auto result = dyn_cast<OpResult>(producerOutput);
Operation* producer = result ? result.getOwner() : nullptr;
auto dps = dyn_cast_or_null<DestinationStyleOpInterface>(producer);
if (!producer || producer->getName().getDialectNamespace()
!= PimDialect::getDialectNamespace()
|| result.getResultNumber() != 0 || !copiedSource.hasOneUse()
|| !producerOutput.hasOneUse() || !dps)
continue;
MutableOperandRange inits = dps.getDpsInitsMutable();
if (inits.size() != 1)
continue;
OpOperand& init = *inits.begin();
auto outputAlloc = init.get().getDefiningOp<memref::AllocOp>();
if (!outputAlloc || !init.get().hasOneUse())
continue;
Value destination = copy.getTarget();
Operation* destinationView = destination.getDefiningOp();
if (!dominance.dominates(destination, producer)) {
if (!destinationView || destinationView->getBlock() != producer->getBlock()
|| !destination.hasOneUse()
|| !llvm::all_of(destinationView->getOperands(),
[&](Value operand) {
return dominance.dominates(operand, producer);
}))
continue;
destinationView->moveBefore(producer);
}
OpBuilder builder(producer);
if (expand)
destination = memref::CollapseShapeOp::create(
builder, copy.getLoc(), destination,
expand.getReassociationIndices());
if (cast<ShapedType>(destination.getType()).getShape()
!= cast<ShapedType>(producerOutput.getType()).getShape()
|| cast<ShapedType>(destination.getType()).getElementType()
!= cast<ShapedType>(producerOutput.getType()).getElementType())
continue;
init.set(destination);
if (outputAlloc.use_empty())
outputAlloc.erase();
}
}
enum class ExpectedPimCopyDirection { HostToDevice, DeviceToHost, DeviceToDevice };
static LogicalResult verifyPimCopyEndpoints(Operation* copy,
@@ -340,6 +407,7 @@ void PimBufferizationPass::runOnOperation() {
}
forwardSingleConsumerContiguousInputCopies(funcOp);
forwardSingleConsumerPimOutputCopies(funcOp);
MLIRContext* ctx = moduleOp.getContext();
PatternRewriter rewriter(ctx);
@@ -9,4 +9,5 @@ add_pim_library(OMPimHostConstantFolding
LINK_LIBS PUBLIC
MLIRLinalgDialect
OMPimCommon
OMPimBufferization
)
@@ -5,6 +5,7 @@
#include "Patterns.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Dialect/Pim/Transforms/Bufferization/ContiguityPatterns.hpp"
using namespace mlir;
@@ -26,6 +27,7 @@ struct HostConstantFoldingPass : PassWrapper<HostConstantFoldingPass, OperationP
populateConstantFoldingConstantPatterns(owningPatterns);
populateConstantFoldingSubviewPatterns(owningPatterns);
pim::populatePimContiguityNormalizationPatterns(owningPatterns);
patterns = std::make_shared<FrozenRewritePatternSet>(std::move(owningPatterns));
return success();
+47
View File
@@ -307,6 +307,21 @@ def SpatMaxPool2DPlanOp : SpatOp<"max_pool2d_plan", []> {
let hasVerifier = 1;
}
def SpatGlobalAveragePoolPlanOp : SpatOp<"global_average_pool_plan", []> {
let summary = "Layout-aware NCHW global average-pool planning op";
let arguments = (ins
SpatTensor:$input,
StrAttr:$logicalLayout
);
let results = (outs
SpatTensor:$output
);
let hasVerifier = 1;
}
def SpatBiasAddPlanOp : SpatOp<"bias_add_plan", []> {
let summary = "Layout-aware Conv-style bias add planning op";
@@ -323,6 +338,38 @@ def SpatBiasAddPlanOp : SpatOp<"bias_add_plan", []> {
let hasVerifier = 1;
}
def SpatAddPlanOp : SpatOp<"add_plan", []> {
let summary = "Layout-aware elementwise add planning op";
let arguments = (ins
SpatTensor:$lhs,
SpatTensor:$rhs,
StrAttr:$logicalLayout
);
let results = (outs
SpatTensor:$output
);
let hasVerifier = 1;
}
def SpatConcatPlanOp : SpatOp<"concat_plan", []> {
let summary = "Layout-aware tensor concatenation planning op";
let arguments = (ins
Variadic<SpatTensor>:$inputs,
I64Attr:$axis,
StrAttr:$logicalLayout
);
let results = (outs
SpatTensor:$output
);
let hasVerifier = 1;
}
def SpatBlueprintOp : SpatOp<"blueprint", []> {
let summary = "Blueprint for assembling logical tensors from published fragments";
+7 -2
View File
@@ -149,7 +149,8 @@ void printComputeLikeOp(ComputeOpTy op, OpAsmPrinter& printer) {
if (auto coreIdAttr = op->template getAttrOfType<IntegerAttr>(onnx_mlir::kCoreIdAttrName))
printer << " coreId " << coreIdAttr.getInt();
printer << " crossbarWeights " << collectDistinctCrossbarWeights(op.getOperation()).size();
printer << " crossbarWeights "
<< collectDistinctResidentWeights(op.getOperation()).size();
printer.printOptionalAttrDict(op->getAttrs(), {op.getOperandSegmentSizesAttrName().getValue(), onnx_mlir::kCoreIdAttrName});
@@ -275,7 +276,11 @@ void printComputeBatchLikeOp(ComputeBatchOpTy op, OpAsmPrinter& printer) {
printer << " shared_outs";
printBlockArgumentList(printer, outputArgs);
}
printer << " crossbarWeights " << getComputeInstanceCrossbarUsage({op.getOperation(), 0, op.getLaneCount()}).size();
printer << " crossbarWeights "
<< getComputeInstanceResidentWeights(
{op.getOperation(), 0,
static_cast<uint32_t>(op.getLaneCount())})
.size();
if (auto coreIdsAttr = op->template getAttrOfType<DenseI32ArrayAttr>(onnx_mlir::kCoreIdsAttrName)) {
printer << " coreIds ";
printCompressedIntegerList(printer, coreIdsAttr.asArrayRef());
+60 -13
View File
@@ -353,31 +353,30 @@ LogicalResult SpatExtractRowsOp::verify() {
return success();
}
LogicalResult SpatConcatOp::verify() {
if (getInputs().empty())
return emitError("requires at least one input");
static LogicalResult verifyConcatTypes(Operation* op, ValueRange inputs, Value output, int64_t axis) {
if (inputs.empty())
return op->emitError("requires at least one input");
auto outputType = dyn_cast<ShapedType>(getOutput().getType());
auto outputType = dyn_cast<ShapedType>(output.getType());
if (!outputType || !outputType.hasRank())
return emitError("output must be a ranked shaped type");
return op->emitError("output must be a ranked shaped type");
int64_t axis = getAxis();
int64_t rank = outputType.getRank();
if (axis < 0 || axis >= rank)
return emitError("axis must be within the output rank");
return op->emitError("axis must be within the output rank");
int64_t concatenatedDimSize = 0;
bool concatenatedDimDynamic = false;
Type outputElementType = outputType.getElementType();
for (Value input : getInputs()) {
for (Value input : inputs) {
auto inputType = dyn_cast<ShapedType>(input.getType());
if (!inputType || !inputType.hasRank())
return emitError("inputs must be ranked shaped types");
return op->emitError("inputs must be ranked shaped types");
if (inputType.getRank() != rank)
return emitError("all inputs must have the same rank as the output");
return op->emitError("all inputs must have the same rank as the output");
if (inputType.getElementType() != outputElementType)
return emitError("all inputs must have the same element type as the output");
return op->emitError("all inputs must have the same element type as the output");
for (int64_t dim = 0; dim < rank; ++dim) {
if (dim == axis)
@@ -385,7 +384,7 @@ LogicalResult SpatConcatOp::verify() {
int64_t inputDim = inputType.getDimSize(dim);
int64_t outputDim = outputType.getDimSize(dim);
if (!ShapedType::isDynamic(inputDim) && !ShapedType::isDynamic(outputDim) && inputDim != outputDim)
return emitError("non-concatenated dimensions must match the output shape");
return op->emitError("non-concatenated dimensions must match the output shape");
}
int64_t inputConcatDim = inputType.getDimSize(axis);
@@ -398,11 +397,24 @@ LogicalResult SpatConcatOp::verify() {
int64_t outputConcatDim = outputType.getDimSize(axis);
if (!concatenatedDimDynamic && !ShapedType::isDynamic(outputConcatDim) && concatenatedDimSize != outputConcatDim)
return emitError("output concatenated dimension must equal the sum of input sizes");
return op->emitError("output concatenated dimension must equal the sum of input sizes");
return success();
}
LogicalResult SpatConcatOp::verify() { return verifyConcatTypes(getOperation(), getInputs(), getOutput(), getAxis()); }
LogicalResult SpatConcatPlanOp::verify() {
if (getLogicalLayout() != "nchw")
return emitError("requires logicalLayout = \"nchw\"");
auto outputType = dyn_cast<RankedTensorType>(getOutput().getType());
if (!outputType || !outputType.hasStaticShape() || outputType.getRank() != 4)
return emitError("requires a static rank-4 output");
if (getAxis() != 1)
return emitError("only channel-axis concatenation is supported");
return verifyConcatTypes(getOperation(), getInputs(), getOutput(), getAxis());
}
static bool isKnownLogicalLayout(StringRef layout) { return layout == "nchw"; }
static bool isKnownPhysicalLayout(StringRef layout) {
@@ -480,6 +492,24 @@ LogicalResult SpatMaxPool2DPlanOp::verify() {
return success();
}
LogicalResult SpatGlobalAveragePoolPlanOp::verify() {
if (failed(verifyPlanTensorTypes(
getOperation(), getInput(), getOutput(), "spat.global_average_pool_plan")))
return failure();
auto inputType = dyn_cast<RankedTensorType>(getInput().getType());
auto outputType = dyn_cast<RankedTensorType>(getOutput().getType());
if (!inputType.hasStaticShape() || !outputType.hasStaticShape() || inputType.getRank() != 4
|| outputType.getRank() != 4)
return emitError("requires static rank-4 input and output tensors");
if (getLogicalLayout() != "nchw")
return emitError("requires logical layout \"nchw\"");
if (inputType.getDimSize(0) != 1 || outputType.getDimSize(0) != 1
|| inputType.getDimSize(1) != outputType.getDimSize(1)
|| outputType.getDimSize(2) != 1 || outputType.getDimSize(3) != 1)
return emitError("requires batch-one input and matching 1x1 output channels");
return success();
}
LogicalResult SpatBiasAddPlanOp::verify() {
if (failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.bias_add_plan")))
return failure();
@@ -513,6 +543,23 @@ LogicalResult SpatBiasAddPlanOp::verify() {
return success();
}
LogicalResult SpatAddPlanOp::verify() {
auto lhsType = dyn_cast<RankedTensorType>(getLhs().getType());
auto rhsType = dyn_cast<RankedTensorType>(getRhs().getType());
auto outputType = dyn_cast<RankedTensorType>(getOutput().getType());
if (!lhsType || !rhsType || !outputType)
return emitError("requires ranked tensor operands and output");
if (!lhsType.hasStaticShape() || !rhsType.hasStaticShape() || !outputType.hasStaticShape())
return emitError("requires static tensor operands and output");
if (lhsType != rhsType || lhsType != outputType)
return emitError("requires matching operand and output tensor types");
if (outputType.getRank() != 4)
return emitError("requires rank-4 operands and output");
if (getLogicalLayout() != "nchw")
return emitError("requires logical layout \"nchw\"");
return success();
}
LogicalResult SpatBlueprintOp::verify() {
auto modeAttr = getModeAttr();
bool isFragmentAssembly = modeAttr && modeAttr.getValue() == "fragment_assembly";
@@ -262,28 +262,30 @@ static void collectClosure(Value value, Block &body, const DeferredInputPlan &pl
} // namespace
bool isDeferredFragmentAssemblyInput(Value input) {
bool isDeferredFragmentAssemblyInput(Value input, size_t processorCount) {
auto blueprint = input.getDefiningOp<SpatBlueprintOp>();
if (!blueprint || blueprint.getMode() != "fragment_assembly")
return false;
return llvm::all_of(getBlueprintFragments(blueprint), [&](Value fragment) {
return getProducerValueRef(fragment, nullptr).has_value();
return getProducerValueRef(fragment, nullptr, processorCount).has_value();
});
}
LogicalResult prepareSingleCpuInput(OpBuilder &, Location loc, Value input, BlockArgument graphInput,
const ComputeInstance &consumerInstance, const MergeScheduleResult &,
const ComputeInstance &consumerInstance,
const MergeScheduleResult &schedule,
ValueRange scheduledInputs, Block &block, unsigned firstInputArgument,
const DenseMap<ProducerValueKey, MaterializedProducerRef> &availableValues,
Value graphLane, Value scheduledGraphLane,
DeferredInputPlan &plan) {
plan = {graphInput, {}, {}, {}, graphLane, scheduledGraphLane, {}, {}, {}, {}, 1, nullptr};
if (isDeferredFragmentAssemblyInput(input)) {
if (isDeferredFragmentAssemblyInput(input, schedule.processorCount)) {
plan.blueprint = input.getDefiningOp<SpatBlueprintOp>();
plan.originalSources = getBlueprintFragments(plan.blueprint);
return success();
}
auto producer = getProducerValueRef(input, &consumerInstance);
auto producer = getProducerValueRef(
input, &consumerInstance, schedule.processorCount);
if (!producer) { plan.availableValue = getBlockOperand(block, scheduledInputs, input, firstInputArgument); return success(); }
ProducerValueKey key {producer->instance, producer->resultIndex};
auto batch = dyn_cast<SpatComputeBatch>(producer->instance.op);
@@ -304,17 +306,19 @@ LogicalResult prepareSingleCpuInput(OpBuilder &, Location loc, Value input, Bloc
LogicalResult prepareMultiCpuTupleInput(OpBuilder &, Location loc, Value input, BlockArgument graphInput,
const ComputeStepTuple &tuple, const PeftClassPlan &,
const MergeScheduleResult &, ValueRange scheduledInputs, Block &block,
const MergeScheduleResult &schedule,
ValueRange scheduledInputs, Block &block,
unsigned firstInputArgument, Value graphLane, Value scheduledGraphLane, Value scheduledLane,
DeferredInputPlan &plan) {
const ComputeInstance &representative = tuple.instances.front();
plan = {graphInput, {}, {}, {}, graphLane, scheduledGraphLane, scheduledLane, {}, {}, {}, 1, nullptr};
if (isDeferredFragmentAssemblyInput(input)) {
if (isDeferredFragmentAssemblyInput(input, schedule.processorCount)) {
plan.blueprint = input.getDefiningOp<SpatBlueprintOp>();
plan.originalSources = getBlueprintFragments(plan.blueprint);
return success();
}
auto producer = getProducerValueRef(input, &representative);
auto producer = getProducerValueRef(
input, &representative, schedule.processorCount);
if (!producer) { plan.availableValue = getBlockOperand(block, scheduledInputs, input, firstInputArgument); return success(); }
auto inputs = getComputeInstanceInputs(representative);
auto it = llvm::find(inputs, input);
@@ -323,7 +327,8 @@ LogicalResult prepareMultiCpuTupleInput(OpBuilder &, Location loc, Value input,
for (const ComputeInstance &instance : tuple.instances) {
auto laneInputs = getComputeInstanceInputs(instance);
if (inputIndex >= laneInputs.size()) return emitError(loc) << "scheduled batch step input out of range";
auto laneProducer = getProducerValueRef(laneInputs[inputIndex], &instance);
auto laneProducer = getProducerValueRef(
laneInputs[inputIndex], &instance, schedule.processorCount);
if (!laneProducer) return emitError(loc) << "scheduled batch step mixes host and producer inputs";
auto source = getOriginalProducerValue(*laneProducer);
if (failed(source)) return emitError(loc) << "cannot resolve original graph producer value";
@@ -22,7 +22,7 @@ struct DeferredInputPlan {
Block *scalarizedHoistBlock = nullptr;
};
bool isDeferredFragmentAssemblyInput(Value input);
bool isDeferredFragmentAssemblyInput(Value input, size_t processorCount);
LogicalResult prepareSingleCpuInput(OpBuilder &builder, Location loc, Value input,
BlockArgument graphInput,
@@ -1,39 +1,97 @@
#include "DeferredCommunicationRealization.hpp"
#include "mlir/IR/Dominance.h"
#include "DeferredBoundaryPlanning.hpp"
#include "DeferredBoundaryRealization.hpp"
#include "DeferredCommunicationDeadlock.hpp"
#include "DeferredCommunicationRealization.hpp"
#include "DeferredCommunicationScheduling.hpp"
#include "DeferredTransferPlanning.hpp"
#include "mlir/IR/Dominance.h"
#include "Scheduling/PeftScheduler.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/CheckedArithmetic.hpp"
namespace onnx_mlir::spatial {
using namespace mlir;
namespace {
static LogicalResult replaceFinalGraphPublications(
func::FuncOp funcOp, DeferredTransferPlan &plan) {
for (Operation &op : funcOp.getOps()) {
static LogicalResult placeLogicalProcessorsOnPhysicalCores(DeferredTransferPlan& plan, const SchedulingTarget& target) {
std::vector<Cost> logicalTrafficFlits(target.processorCount * target.processorCount, 0);
for (const std::unique_ptr<DeferredExchangePlan>& exchange : plan.exchanges)
for (const ExternalTransferFamily& transfer : exchange->external) {
auto fragmentType = dyn_cast<ShapedType>(transfer.requirement->publicationFragmentType);
if (!fragmentType || !fragmentType.hasStaticShape())
return exchange->deferred.emitOpError("physical core placement requires a static transfer fragment");
auto fragmentBytes = pim::getCheckedShapedTypeSizeInBytes(
fragmentType, exchange->deferred, "physical core placement transfer fragment");
if (failed(fragmentBytes))
return failure();
Cost flits = static_cast<Cost>(*fragmentBytes) / target.transferWidthBytes
+ (*fragmentBytes % target.transferWidthBytes != 0);
for (size_t index = 0; index < transfer.sourceCores.size(); ++index) {
size_t sourceLogicalProcessor = static_cast<size_t>(transfer.sourceCores.valueAt(index));
size_t targetLogicalProcessor = static_cast<size_t>(transfer.targetCores.valueAt(index));
Cost& traffic = logicalTrafficFlits[sourceLogicalProcessor * target.processorCount + targetLogicalProcessor];
traffic = checkedAdd(traffic, flits);
}
}
std::vector<size_t> physicalCoreForLogicalProcessor =
mapLogicalProcessorsToPhysicalCores(logicalTrafficFlits, target);
auto getPhysicalCore = [&](int64_t logicalProcessor) {
assert(logicalProcessor >= 0 && static_cast<size_t>(logicalProcessor) < physicalCoreForLogicalProcessor.size()
&& "logical processor is outside the scheduling target");
return static_cast<int64_t>(physicalCoreForLogicalProcessor[logicalProcessor]);
};
auto remap = [&](StaticIntSequence& logicalProcessors) {
SmallVector<int64_t> physicalCores;
physicalCores.reserve(logicalProcessors.size());
for (size_t index = 0; index < logicalProcessors.size(); ++index)
physicalCores.push_back(getPhysicalCore(logicalProcessors.valueAt(index)));
logicalProcessors = StaticIntSequence::fromValues(physicalCores);
};
for (ScheduledInfo& scheduled : plan.scheduled) {
for (int64_t& logicalProcessor : scheduled.cores)
logicalProcessor = getPhysicalCore(logicalProcessor);
if (isa<SpatScheduledCompute>(scheduled.op)) {
scheduled.op->setAttr(
kCoreIdAttrName, IntegerAttr::get(IntegerType::get(scheduled.op->getContext(), 32), scheduled.cores.front()));
}
else {
SmallVector<int32_t> physicalCores;
physicalCores.reserve(scheduled.cores.size());
for (int64_t physicalCore : scheduled.cores)
physicalCores.push_back(static_cast<int32_t>(physicalCore));
scheduled.op->setAttr(kCoreIdsAttrName, DenseI32ArrayAttr::get(scheduled.op->getContext(), physicalCores));
}
}
for (const std::unique_ptr<ProducedValue>& produced : plan.producedStorage)
produced->core = getPhysicalCore(produced->core);
for (const std::unique_ptr<DeferredExchangePlan>& exchange : plan.exchanges)
for (ExternalTransferFamily& transfer : exchange->external) {
remap(transfer.sourceCores);
remap(transfer.targetCores);
}
return success();
}
static LogicalResult replaceFinalGraphPublications(func::FuncOp funcOp, DeferredTransferPlan& plan) {
for (Operation& op : funcOp.getOps()) {
if (!isa<SpatGraphCompute, SpatGraphComputeBatch>(op))
continue;
auto graphId = op.getAttrOfType<IntegerAttr>("scheduled.graph_id");
if (!graphId)
continue;
for (auto [resultIndex, result] : llvm::enumerate(op.getResults())) {
SmallVector<OpOperand *> externalUses;
for (OpOperand &use : result.getUses()) {
Operation *user = use.getOwner();
if (isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(user))
SmallVector<OpOperand*> externalUses;
for (OpOperand& use : result.getUses()) {
Operation* user = use.getOwner();
if (isa<SpatGraphCompute, SpatGraphComputeBatch, SpatDeferredCommunicationOp>(user))
continue;
if (auto blueprint = dyn_cast<SpatBlueprintOp>(user)) {
bool blueprintEscapes = llvm::any_of(
blueprint.getOutput().getUses(), [](OpOperand &blueprintUse) {
return !isa<SpatGraphCompute, SpatGraphComputeBatch,
SpatDeferredCommunicationOp>(
blueprintUse.getOwner());
});
bool blueprintEscapes = llvm::any_of(blueprint.getOutput().getUses(), [](OpOperand& blueprintUse) {
return !isa<SpatGraphCompute, SpatGraphComputeBatch, SpatDeferredCommunicationOp>(blueprintUse.getOwner());
});
if (!blueprintEscapes)
continue;
}
@@ -42,21 +100,16 @@ static LogicalResult replaceFinalGraphPublications(
if (externalUses.empty())
continue;
SmallVector<Value> exact;
for (ProducedValue *produced :
plan.producedByGraph.lookup(graphId.getInt()))
if (produced->resultIndex == resultIndex
&& produced->published
&& produced->published.getType() == result.getType()
&& !llvm::is_contained(exact, produced->published))
for (ProducedValue* produced : plan.producedByGraph.lookup(graphId.getInt()))
if (produced->resultIndex == resultIndex && produced->published
&& produced->published.getType() == result.getType() && !llvm::is_contained(exact, produced->published))
exact.push_back(produced->published);
if (exact.size() != 1)
return op.emitOpError(
"phase 2 final publication ownership changed after planning");
for (OpOperand *use : externalUses) {
Operation *consumer = use->getOwner();
Operation *producer = exact.front().getDefiningOp();
if (consumer->getBlock() == producer->getBlock()
&& consumer->isBeforeInBlock(producer))
return op.emitOpError("phase 2 final publication ownership changed after planning");
for (OpOperand* use : externalUses) {
Operation* consumer = use->getOwner();
Operation* producer = exact.front().getDefiningOp();
if (consumer->getBlock() == producer->getBlock() && consumer->isBeforeInBlock(producer))
consumer->moveAfter(producer);
use->set(exact.front());
}
@@ -65,13 +118,12 @@ static LogicalResult replaceFinalGraphPublications(
return success();
}
static LogicalResult eraseOldGraph(func::FuncOp funcOp,
IRRewriter &rewriter) {
SmallVector<Operation *> oldGraph;
for (Operation &op : funcOp.getOps())
static LogicalResult eraseOldGraph(func::FuncOp funcOp, IRRewriter& rewriter) {
SmallVector<Operation*> oldGraph;
for (Operation& op : funcOp.getOps())
if (isa<SpatGraphCompute, SpatGraphComputeBatch, SpatBlueprintOp>(op))
oldGraph.push_back(&op);
for (Operation *op : llvm::reverse(oldGraph)) {
for (Operation* op : llvm::reverse(oldGraph)) {
if (auto blueprint = dyn_cast<SpatBlueprintOp>(op)) {
if (blueprint.getOutput().use_empty())
rewriter.eraseOp(blueprint);
@@ -80,10 +132,9 @@ static LogicalResult eraseOldGraph(func::FuncOp funcOp,
if (!op->use_empty()) {
for (OpResult result : op->getResults()) {
if (!result.use_empty()) {
Operation *user = result.use_begin()->getOwner();
return op->emitOpError()
<< "phase 2 cannot erase old graph result "
<< result.getResultNumber() << " used by " << user->getName();
Operation* user = result.use_begin()->getOwner();
return op->emitOpError() << "phase 2 cannot erase old graph result " << result.getResultNumber()
<< " used by " << user->getName();
}
}
}
@@ -92,34 +143,27 @@ static LogicalResult eraseOldGraph(func::FuncOp funcOp,
return success();
}
static LogicalResult eraseDeferredSourceSelectors(
func::FuncOp funcOp, IRRewriter &rewriter) {
static LogicalResult eraseDeferredSourceSelectors(func::FuncOp funcOp, IRRewriter& rewriter) {
SmallVector<SpatDeferredSourceSelectOp> selectors;
funcOp.walk([&](SpatDeferredSourceSelectOp selector) {
selectors.push_back(selector);
});
funcOp.walk([&](SpatDeferredSourceSelectOp selector) { selectors.push_back(selector); });
for (SpatDeferredSourceSelectOp selector : llvm::reverse(selectors)) {
if (!selector.getOutput().use_empty())
return selector.emitOpError(
"phase 2 left a live deferred source selection");
return selector.emitOpError("phase 2 left a live deferred source selection");
rewriter.eraseOp(selector);
}
return success();
}
static void eraseUnusedIdentityDeferredCommunications(
func::FuncOp funcOp, IRRewriter &rewriter) {
static void eraseUnusedIdentityDeferredCommunications(func::FuncOp funcOp, IRRewriter& rewriter) {
SmallVector<SpatDeferredCommunicationOp> unused;
funcOp.walk([&](SpatDeferredCommunicationOp deferred) {
if (!deferred.getOutput().use_empty() || !deferred.getBody().hasOneBlock())
return;
Block &body = deferred.getBody().front();
Block& body = deferred.getBody().front();
auto yield = dyn_cast<SpatYieldOp>(body.getTerminator());
auto argument = yield && yield.getOutputs().size() == 1
? dyn_cast<BlockArgument>(yield.getOutputs().front())
: BlockArgument();
if (argument && argument.getOwner() == &body
&& argument.getArgNumber() < deferred.getSources().size())
auto argument =
yield && yield.getOutputs().size() == 1 ? dyn_cast<BlockArgument>(yield.getOutputs().front()) : BlockArgument();
if (argument && argument.getOwner() == &body && argument.getArgNumber() < deferred.getSources().size())
unused.push_back(deferred);
});
for (SpatDeferredCommunicationOp deferred : llvm::reverse(unused))
@@ -128,11 +172,10 @@ static void eraseUnusedIdentityDeferredCommunications(
static LogicalResult verifyDominance(func::FuncOp funcOp) {
DominanceInfo dominance(funcOp);
WalkResult result = funcOp.walk([&](Operation *op) {
WalkResult result = funcOp.walk([&](Operation* op) {
for (auto [index, operand] : llvm::enumerate(op->getOperands()))
if (!dominance.dominates(operand, op)) {
op->emitOpError() << "phase 2 produced non-dominating operand "
<< index << ": " << operand;
op->emitOpError() << "phase 2 produced non-dominating operand " << index << ": " << operand;
return WalkResult::interrupt();
}
return WalkResult::advance();
@@ -142,26 +185,23 @@ static LogicalResult verifyDominance(func::FuncOp funcOp) {
} // namespace
LogicalResult realizeDeferredCommunication(
func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization) {
LogicalResult realizeDeferredCommunication(func::FuncOp funcOp,
const ScheduledComputeMaterializationResult& materialization,
const SchedulingTarget& target) {
IRRewriter rewriter(funcOp.getContext());
eraseUnusedIdentityDeferredCommunications(funcOp, rewriter);
auto transfers = buildDeferredTransferPlan(funcOp, materialization);
if (failed(transfers))
return funcOp.emitOpError(
"phase 2 failed to build symbolic transfer families");
return funcOp.emitOpError("phase 2 failed to build symbolic transfer families");
if (failed(placeLogicalProcessorsOnPhysicalCores(*transfers, target)))
return failure();
auto schedule = scheduleDeferredCommunication(funcOp, *transfers);
if (failed(schedule)
|| failed(verifyPlannedCommunicationDeadlockFree(
funcOp, transfers->stepCounts, *schedule)))
return funcOp.emitOpError(
"phase 2 failed to schedule symbolic communication");
if (failed(schedule) || failed(verifyPlannedCommunicationDeadlockFree(funcOp, transfers->stepCounts, *schedule)))
return funcOp.emitOpError("phase 2 failed to schedule symbolic communication");
auto boundaries = buildDeferredBoundaryPlan(*transfers, *schedule);
if (failed(boundaries))
return funcOp.emitOpError(
"phase 2 failed to build sparse boundary programs");
return funcOp.emitOpError("phase 2 failed to build sparse boundary programs");
if (failed(retargetDeferredPublications(funcOp, *transfers))
|| failed(replaceFinalGraphPublications(funcOp, *transfers)))
@@ -169,28 +209,22 @@ LogicalResult realizeDeferredCommunication(
ConstantPool constants(funcOp, rewriter);
DeferredEmissionContext context(rewriter, constants);
DeferredReplacementMap replacements;
if (failed(realizeDeferredBoundaries(
boundaries->boundaries, boundaries->results, context, replacements)))
if (failed(realizeDeferredBoundaries(boundaries->boundaries, boundaries->results, context, replacements)))
return failure();
for (auto [op, replacement] : replacements) {
if (op->getResult(0) == replacement)
return op->emitOpError(
"phase 2 cannot replace deferred communication with itself");
return op->emitOpError("phase 2 cannot replace deferred communication with itself");
op->getResult(0).replaceAllUsesWith(replacement);
if (!op->use_empty())
return op->emitOpError(
"phase 2 cannot erase deferred communication with live uses");
return op->emitOpError("phase 2 cannot erase deferred communication with live uses");
rewriter.eraseOp(op);
}
if (failed(eraseDeferredSourceSelectors(funcOp, rewriter))
|| failed(eraseOldGraph(funcOp, rewriter))
|| failed(verifyDominance(funcOp))
|| failed(verifyRealizedCommunicationDeadlockFree(funcOp, *schedule)))
if (failed(eraseDeferredSourceSelectors(funcOp, rewriter)) || failed(eraseOldGraph(funcOp, rewriter))
|| failed(verifyDominance(funcOp)) || failed(verifyRealizedCommunicationDeadlockFree(funcOp, *schedule)))
return failure();
bool deferredRemains = false;
funcOp.walk([&](SpatDeferredCommunicationOp deferred) {
deferred.emitOpError(
"phase 2 left an unrealized deferred communication");
deferred.emitOpError("phase 2 left an unrealized deferred communication");
deferredRemains = true;
});
return success(!deferredRemains);
@@ -5,9 +5,10 @@
namespace onnx_mlir::spatial {
struct ScheduledComputeMaterializationResult;
struct SchedulingTarget;
mlir::LogicalResult realizeDeferredCommunication(
mlir::func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization);
mlir::LogicalResult realizeDeferredCommunication(mlir::func::FuncOp funcOp,
const ScheduledComputeMaterializationResult& materialization,
const SchedulingTarget& target);
} // namespace onnx_mlir::spatial
@@ -1,15 +1,14 @@
#include "mlir/Pass/Pass.h"
#include "DeferredCommunicationRealization.hpp"
#include "ScheduledComputeMaterialization.hpp"
#include "ScheduledComputeReport.hpp"
#include "ScheduledComputeVerification.hpp"
#include "SpatialDataflowCsvExporter.hpp"
#include "DeferredCommunicationRealization.hpp"
#include "mlir/Pass/Pass.h"
#include "Scheduling/MergeSchedulingAnalysis.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.hpp"
#include "SpatialDataflowCsvExporter.hpp"
#include "src/Accelerators/PIM/Common/PimCommon.hpp"
#include "src/Accelerators/PIM/Common/Support/DebugDump.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.hpp"
#include "src/Accelerators/PIM/Pass/PIMPasses.h"
using namespace mlir;
@@ -21,6 +20,10 @@ namespace {
struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MergeComputeNodesPass)
MergeComputeNodesPass() = default;
explicit MergeComputeNodesPass(const SchedulingTarget& schedulingTarget)
: target(schedulingTarget), hasTarget(true) {}
StringRef getArgument() const override { return "pim-merge-compute-nodes"; }
StringRef getDescription() const override {
return "Materialize scheduled Spatial compute with deferred communication placeholders.";
@@ -28,6 +31,13 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
void runOnOperation() override {
ModuleOp moduleOp = getOperation();
if (!hasTarget || target.processorCount == 0 || target.residentWeightCapacity == 0 || target.transferWidthBytes == 0
|| target.interProcessorLatencyNs.size() != target.processorCount * target.processorCount
|| (target.processorCount > 1 && target.averageInterProcessorLatencyNs == 0)) {
moduleOp.emitError("MergeComputeNodes requires an explicit valid Spatial scheduling target");
signalPassFailure();
return;
}
auto entryFunc = getPimEntryFunc(moduleOp);
if (failed(entryFunc)) {
moduleOp.emitError("failed to locate the PIM entry function during MergeComputeNodes");
@@ -36,10 +46,10 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
}
func::FuncOp funcOp = *entryFunc;
MergeScheduleResult schedule = MergeSchedulingAnalysis(funcOp).getResult();
MergeScheduleResult logicalSchedule = MergeSchedulingAnalysis(funcOp, target).getResult();
PatternRewriter rewriter(moduleOp.getContext());
FailureOr<ScheduledComputeMaterializationResult> materialization =
materializeScheduledCompute(funcOp, schedule, rewriter);
materializeScheduledCompute(funcOp, logicalSchedule, rewriter);
if (failed(materialization)) {
signalPassFailure();
return;
@@ -48,7 +58,7 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
// payloads must be diagnosed from the producer-owned body.
dumpModule(moduleOp, "spatial3_scheduled_no_comm", /*assumeVerified=*/true);
if (failed(verifyMaterializedScheduleMapping(funcOp,
schedule,
logicalSchedule,
materialization->peftClassPlans,
materialization->graphComputeToBlockMap,
materialization->materializedSchedules))) {
@@ -75,18 +85,14 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
SpatialDataflowExportStage exportMode = getSpatialDataflowExportStage();
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial3)
&& failed(exportSpatialDataflowCsvScheduled(
funcOp, materialization->materializedSchedules,
"spatial3_scheduled_no_comm", "spatial3"))) {
funcOp, materialization->materializedSchedules, "spatial3_scheduled_no_comm", "spatial3"))) {
signalPassFailure();
return;
}
dumpScheduledComputeReport(moduleOp,
funcOp,
schedule,
materialization->peftClassPlans,
materialization->materializedSchedules);
if (failed(realizeDeferredCommunication(funcOp, *materialization))) {
dumpScheduledComputeReport(
moduleOp, funcOp, logicalSchedule, materialization->peftClassPlans, materialization->materializedSchedules);
if (failed(realizeDeferredCommunication(funcOp, *materialization, target))) {
moduleOp.emitError("MergeComputeNodes phase 2 communication realization failed");
signalPassFailure();
return;
@@ -100,11 +106,14 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
}
if (shouldExportSpatialDataflowStage(exportMode, SpatialDataflowExportStage::Spatial4)
&& failed(exportSpatialDataflowCsvScheduled(
funcOp, materialization->materializedSchedules,
"spatial4_scheduled", "spatial4"))) {
funcOp, materialization->materializedSchedules, "spatial4_scheduled", "spatial4"))) {
signalPassFailure();
}
}
private:
SchedulingTarget target;
bool hasTarget = false;
};
} // namespace
@@ -112,4 +121,8 @@ struct MergeComputeNodesPass final : PassWrapper<MergeComputeNodesPass, Operatio
std::unique_ptr<Pass> createMergeComputeNodesPass() { return std::make_unique<spatial::MergeComputeNodesPass>(); }
std::unique_ptr<Pass> createMergeComputeNodesPass(const spatial::SchedulingTarget& target) {
return std::make_unique<spatial::MergeComputeNodesPass>(target);
}
} // namespace onnx_mlir
@@ -164,7 +164,8 @@ inline size_t getScheduledCpuForComputeInstance(const ComputeInstance &instance,
auto batch = dyn_cast<SpatComputeBatch>(instance.op);
assert(batch && instance.laneCount != 0 && "missing scheduled CPU for non-batch compute instance");
assert(instance.laneStart < static_cast<uint32_t>(batch.getLaneCount()) && "batch lane start out of range");
ComputeInstance chunk = getBatchChunkForLane(batch, instance.laneStart);
ComputeInstance chunk = getBatchChunkForLane(
batch, instance.laneStart, schedule.processorCount);
auto it = schedule.computeToCpuMap.find(chunk);
assert(it != schedule.computeToCpuMap.end() && "missing scheduled CPU for batch chunk");
return it->second;
@@ -184,13 +185,17 @@ inline unsigned getScheduledBatchResultArgBase(SpatScheduledComputeBatch schedul
return inputArgBase + scheduled.getInputs().size();
}
inline SmallVector<GraphComputeBlockKey> collectExpectedGraphComputeBlockKeys(func::FuncOp funcOp) {
inline SmallVector<GraphComputeBlockKey> collectExpectedGraphComputeBlockKeys(
func::FuncOp funcOp, size_t processorCount) {
SmallVector<GraphComputeBlockKey> keys;
for (Operation &op : funcOp.getOps()) {
if (auto compute = dyn_cast<SpatGraphCompute>(&op))
keys.push_back(getGraphComputeBlockKey({compute.getOperation(), 0, 1}));
else if (auto batch = dyn_cast<SpatGraphComputeBatch>(&op))
for (ComputeInstance chunk : getBatchChunksForRange(batch, 0, static_cast<uint32_t>(batch.getLaneCount())))
for (ComputeInstance chunk :
getBatchChunksForRange(
batch, 0, static_cast<uint32_t>(batch.getLaneCount()),
processorCount))
keys.push_back(getGraphComputeBlockKey(chunk));
}
return keys;
@@ -179,7 +179,9 @@ LogicalResult collectPeftClassOperandsAndResults(
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input))
if (!getProducerValueRef(input, &instance, schedule.processorCount)
&& !isDeferredFragmentAssemblyInput(
input, schedule.processorCount))
appendUnique(peftClassPlan.inputs, input);
}
return success();
@@ -222,7 +224,9 @@ LogicalResult collectPeftClassOperandsAndResults(
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input))
if (!getProducerValueRef(input, &instance, schedule.processorCount)
&& !isDeferredFragmentAssemblyInput(
input, schedule.processorCount))
appendUnique(peftClassPlan.inputs, input);
}
}
@@ -48,7 +48,8 @@ LogicalResult verifyMaterializedScheduleMapping(
}
}
for (GraphComputeBlockKey key : collectExpectedGraphComputeBlockKeys(funcOp)) {
for (GraphComputeBlockKey key :
collectExpectedGraphComputeBlockKeys(funcOp, schedule.processorCount)) {
if (graphComputeToBlockMap.count(key))
continue;
diagnostics.report(key.op, [&](Operation *illegalOp) {
@@ -66,10 +67,12 @@ LogicalResult verifyMaterializedScheduleMapping(
}
}
if (graphComputeToBlockMap.size() != collectExpectedGraphComputeBlockKeys(funcOp).size()) {
const size_t expectedGraphComputeBlockCount =
collectExpectedGraphComputeBlockKeys(funcOp, schedule.processorCount).size();
if (graphComputeToBlockMap.size() != expectedGraphComputeBlockCount) {
diagnostics.report(funcOp.getOperation(), [&](Operation *illegalOp) {
illegalOp->emitOpError() << "phase-check expected "
<< collectExpectedGraphComputeBlockKeys(funcOp).size()
<< expectedGraphComputeBlockCount
<< " graph compute block mappings but saw " << graphComputeToBlockMap.size();
});
}
@@ -12,7 +12,6 @@
#include "llvm/Support/Casting.h"
#include <algorithm>
#include <cmath>
#include <iterator>
#include <limits>
#include <optional>
@@ -22,7 +21,6 @@
#include "ComputeGraph.hpp"
#include "ComputeInstanceUtils.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/ShapingUtils.hpp"
@@ -38,28 +36,25 @@ uint64_t countComputeBodyOperationInstances(Region& body);
namespace {
struct PimsimSchedulerCostModel {
static constexpr Cost kDefaultBitwidth = 8;
static constexpr Cost kCorePeriodNs = 1;
static constexpr Cost kLocalMemoryWidthBytes = 64;
static constexpr Cost kLocalMemoryLatencyCycles = 1;
static constexpr Cost kNetworkBusWidthBytes = 8;
static constexpr Cost kNetworkBaseLatencyNs = 2;
static constexpr Cost kNetworkPerHopLatencyNs = 1;
static constexpr Cost kVectorWidth = 16;
static constexpr Cost kVectorLatencyCycles = 4;
static constexpr Cost kDacResolutionBits = 1;
static constexpr Cost kDacLatencyCycles = 1;
static constexpr Cost kDacCount = 128;
static constexpr Cost kXbarReadLatencyNs = 30;
static constexpr Cost kSampleHoldLatencyCycles = 1;
static constexpr Cost kAdcLatencyCycles = 10;
static constexpr Cost kAdcCount = 2;
static constexpr Cost kShiftAdderLatencyCycles = 1;
static constexpr Cost kOutputBufferLatencyCycles = 1;
static constexpr Cost kInputBufferLatencyCycles = 0;
TransferCost addTransferCosts(const TransferCost& lhs,
const TransferCost& rhs) {
return {checkedAdd(lhs.fixed, rhs.fixed),
checkedAdd(lhs.networkFlits, rhs.networkFlits)};
}
TransferCost scaleTransferCost(const TransferCost& cost,
Cost numerator,
Cost denominator = 1) {
assert(denominator > 0 && "transfer cost denominator must be positive");
return {checkedMultiply(cost.fixed, numerator) / denominator,
checkedMultiply(cost.networkFlits, numerator) / denominator};
}
struct SchedulerCostModel {
static constexpr Cost kFallbackOperationCost = 1;
const SchedulingTarget& target;
static Cost ceilDiv(Cost numerator, Cost denominator) {
assert(denominator > 0 && "denominator must be positive");
return (numerator + denominator - 1) / denominator;
@@ -72,7 +67,7 @@ struct PimsimSchedulerCostModel {
return static_cast<Cost>(shaped.getNumElements());
}
static Cost getBitwidthOrDefault(Type type) {
Cost getBitwidthOrDefault(Type type) const {
if (auto shaped = dyn_cast<ShapedType>(type))
type = shaped.getElementType();
if (auto intType = dyn_cast<IntegerType>(type))
@@ -81,14 +76,14 @@ struct PimsimSchedulerCostModel {
return floatType.getWidth();
if (isa<IndexType>(type))
return 64;
return kDefaultBitwidth;
return target.computeBitwidth;
}
static Cost getComputeBitwidth(Type type) {
return std::min(getBitwidthOrDefault(type), kDefaultBitwidth);
Cost getComputeBitwidth(Type type) const {
return std::min(getBitwidthOrDefault(type), target.computeBitwidth);
}
static Cost getByteSize(Type type, Cost fallbackBitwidth = kDefaultBitwidth) {
Cost getByteSize(Type type, Cost fallbackBitwidth = 0) const {
auto elementCount = getStaticElementCount(type);
if (!elementCount)
return kFallbackOperationCost;
@@ -96,60 +91,40 @@ struct PimsimSchedulerCostModel {
if (bitwidth <= 0)
bitwidth = getBitwidthOrDefault(type);
if (bitwidth <= 0)
bitwidth = kDefaultBitwidth;
bitwidth = target.computeBitwidth;
return ceilDiv(checkedMultiply(*elementCount, bitwidth), static_cast<Cost>(8));
}
static Cost getVectorReadWriteCost(Cost readBytes, Cost writeBytes) {
Cost totalBytes = checkedAdd(readBytes, writeBytes);
return checkedMultiply(ceilDiv(totalBytes, kLocalMemoryWidthBytes),
checkedMultiply(kLocalMemoryLatencyCycles, kCorePeriodNs));
Cost getVectorReadWriteCost(Cost readBytes, Cost writeBytes) const {
Cost reads = checkedMultiply(ceilDiv(readBytes, target.localMemoryWidthBytes),
target.localMemoryReadLatencyCycles);
Cost writes = checkedMultiply(ceilDiv(writeBytes, target.localMemoryWidthBytes),
target.localMemoryWriteLatencyCycles);
return checkedMultiply(checkedAdd(reads, writes), target.processorPeriodNs);
}
static Cost getVectorComputeCost(Cost elementCount) {
return checkedMultiply(ceilDiv(elementCount, kVectorWidth),
checkedMultiply(kVectorLatencyCycles, kCorePeriodNs));
Cost getVectorComputeCost(Cost elementCount) const {
return checkedMultiply(ceilDiv(elementCount, target.vectorWidth),
checkedMultiply(target.vectorLatencyCycles, target.processorPeriodNs));
}
static Cost getTensorMoveCost(Type type) {
Cost getTensorMoveCost(Type type) const {
return getVectorReadWriteCost(getByteSize(type), 0);
}
static std::pair<Cost, Cost> estimateMeshShape() {
Cost coreCount = static_cast<Cost>(std::max<long>(1, coresCount.getValue()));
Cost rows = static_cast<Cost>(std::sqrt(static_cast<long double>(coreCount)));
if (rows == 0)
rows = 1;
while (rows > 1 && coreCount % rows != 0)
--rows;
Cost cols = ceilDiv(coreCount, rows);
return {rows, cols};
TransferCost getTransferCostFromBytes(Cost bytes) const {
Cost localRead = checkedMultiply(ceilDiv(bytes, target.localMemoryWidthBytes),
checkedMultiply(target.localMemoryReadLatencyCycles,
target.processorPeriodNs));
Cost localWrite = checkedMultiply(ceilDiv(bytes, target.localMemoryWidthBytes),
checkedMultiply(target.localMemoryWriteLatencyCycles,
target.processorPeriodNs));
Cost payloadFlits = ceilDiv(bytes, target.transferWidthBytes);
return {checkedAdd(localRead, localWrite),
checkedAdd(static_cast<Cost>(2), payloadFlits)};
}
static Cost getAverageInterCoreLatencyNs() {
auto [rows, cols] = estimateMeshShape();
auto averageAxisDistance = [](Cost size) -> Cost {
if (size <= 1)
return 0;
return checkedMultiply(size, size) - 1;
};
Cost avgRow = averageAxisDistance(rows) / (static_cast<Cost>(3) * rows);
Cost avgCol = averageAxisDistance(cols) / (static_cast<Cost>(3) * cols);
return checkedAdd(kNetworkBaseLatencyNs, checkedMultiply(kNetworkPerHopLatencyNs, checkedAdd(avgRow, avgCol)));
}
static Cost getInterCoreTransferCostFromBytes(Cost bytes) {
Cost localRead = checkedMultiply(ceilDiv(bytes, kLocalMemoryWidthBytes),
checkedMultiply(kLocalMemoryLatencyCycles, kCorePeriodNs));
Cost localWrite = checkedMultiply(ceilDiv(bytes, kLocalMemoryWidthBytes),
checkedMultiply(kLocalMemoryLatencyCycles, kCorePeriodNs));
Cost payloadFlits = ceilDiv(bytes, kNetworkBusWidthBytes);
Cost averageNoCLatency = getAverageInterCoreLatencyNs();
Cost network = checkedMultiply(checkedAdd(static_cast<Cost>(2), payloadFlits), averageNoCLatency);
return checkedAdd(checkedAdd(localRead, localWrite), network);
}
static Cost getUnaryVectorCost(Type inputType, Type outputType, bool scalarOutput = false) {
Cost getUnaryVectorCost(Type inputType, Type outputType, bool scalarOutput = false) const {
auto maybeElements = getStaticElementCount(inputType);
if (!maybeElements)
return kFallbackOperationCost;
@@ -159,7 +134,7 @@ struct PimsimSchedulerCostModel {
return checkedAdd(getVectorReadWriteCost(inputBytes, outputBytes), getVectorComputeCost(*maybeElements));
}
static Cost getBinaryVectorCost(Type lhsType, Type rhsType, Type outputType, bool scalarOutput = false) {
Cost getBinaryVectorCost(Type lhsType, Type rhsType, Type outputType, bool scalarOutput = false) const {
auto maybeElements = getStaticElementCount(lhsType);
if (!maybeElements)
return kFallbackOperationCost;
@@ -170,24 +145,35 @@ struct PimsimSchedulerCostModel {
return checkedAdd(getVectorReadWriteCost(readBytes, outputBytes), getVectorComputeCost(*maybeElements));
}
static Cost getMatrixComputeLatency(Cost inputBitwidth) {
Cost xbarDim = static_cast<Cost>(crossbarSize.getValue());
Cost inputTimes = ceilDiv(inputBitwidth, kDacResolutionBits);
Cost dacTimes = ceilDiv(xbarDim, kDacCount);
Cost adcTimes = ceilDiv(xbarDim, kAdcCount);
Cost frontStage = kInputBufferLatencyCycles + kDacLatencyCycles + kXbarReadLatencyNs + kSampleHoldLatencyCycles;
Cost backPipe = std::max(kAdcLatencyCycles, checkedAdd(kShiftAdderLatencyCycles, kOutputBufferLatencyCycles));
Cost backStage = checkedAdd(checkedAdd(kAdcLatencyCycles, kShiftAdderLatencyCycles), kOutputBufferLatencyCycles);
backStage = checkedAdd(backStage, checkedMultiply(adcTimes - 1, backPipe));
Cost totalTimes = checkedMultiply(inputTimes, dacTimes);
Cost getMatrixComputeLatency(Cost inputBitwidth) const {
Cost inputTimes = ceilDiv(inputBitwidth, target.matrixInputResolutionBits);
Cost inputPasses = ceilDiv(target.matrixRows, target.matrixInputParallelism);
Cost outputPasses = ceilDiv(target.matrixColumns, target.matrixOutputParallelism);
Cost readCycles = ceilDiv(target.matrixReadLatencyNs, target.matrixPeriodNs);
Cost frontStage = target.matrixInputBufferLatencyCycles + target.matrixInputLatencyCycles
+ readCycles + target.matrixSampleLatencyCycles;
Cost backPipe = std::max(target.matrixOutputLatencyCycles,
checkedAdd(target.matrixShiftLatencyCycles,
target.matrixBufferLatencyCycles));
Cost backStage = checkedAdd(
checkedAdd(target.matrixOutputLatencyCycles, target.matrixShiftLatencyCycles),
target.matrixBufferLatencyCycles);
backStage = checkedAdd(backStage, checkedMultiply(outputPasses - 1, backPipe));
Cost totalTimes = checkedMultiply(inputTimes, inputPasses);
if (!target.matrixPipeline)
return checkedMultiply(
checkedMultiply(checkedAdd(frontStage, backStage), totalTimes),
target.matrixPeriodNs);
Cost stagePipe = std::max(frontStage, backStage);
return checkedAdd(checkedAdd(frontStage, backStage),
checkedMultiply(totalTimes - 1, stagePipe));
return checkedMultiply(
checkedAdd(checkedAdd(frontStage, backStage),
checkedMultiply(totalTimes - 1, stagePipe)),
target.matrixPeriodNs);
}
static Cost getWvmmCost(Type inputType, Type outputType) {
Cost getWvmmCost(Type inputType, Type outputType) const {
Cost inputBitwidth = getComputeBitwidth(inputType);
Cost inputBytes = checkedMultiply(static_cast<Cost>(crossbarSize.getValue()),
Cost inputBytes = checkedMultiply(target.matrixRows,
ceilDiv(inputBitwidth, static_cast<Cost>(8)));
inputBytes = checkedMultiply(inputBytes, static_cast<Cost>(8));
Cost outputBytes = getByteSize(outputType, getComputeBitwidth(outputType));
@@ -196,22 +182,22 @@ struct PimsimSchedulerCostModel {
};
std::optional<uint64_t> getStaticTripCount(scf::ForOp loop);
Cost getOperationCost(Operation& op);
Cost getOperationCost(Operation& op, const SchedulerCostModel& costModel);
Cost getRegionCost(Region& body) {
Cost getRegionCost(Region& body, const SchedulerCostModel& costModel) {
Cost cost = 0;
for (Block& block : body)
for (Operation& op : block)
cost = checkedAdd(cost, getOperationCost(op));
cost = checkedAdd(cost, getOperationCost(op, costModel));
return cost;
}
Cost getOperationCost(Operation& op) {
Cost getOperationCost(Operation& op, const SchedulerCostModel& costModel) {
if (auto loop = dyn_cast<scf::ForOp>(&op)) {
std::optional<uint64_t> tripCount = getStaticTripCount(loop);
if (!tripCount)
return PimsimSchedulerCostModel::kFallbackOperationCost;
return checkedMultiply(getRegionCost(loop.getRegion()), static_cast<Cost>(*tripCount));
return SchedulerCostModel::kFallbackOperationCost;
return checkedMultiply(getRegionCost(loop.getRegion(), costModel), static_cast<Cost>(*tripCount));
}
if (isa<SpatYieldOp, SpatInParallelOp, affine::AffineApplyOp, arith::ConstantOp,
@@ -219,42 +205,43 @@ Cost getOperationCost(Operation& op) {
return 0;
if (auto wvmm = dyn_cast<SpatVMMOp>(&op))
return PimsimSchedulerCostModel::getWvmmCost(wvmm.getInput().getType(), wvmm.getOutput().getType());
return costModel.getWvmmCost(wvmm.getInput().getType(), wvmm.getOutput().getType());
if (auto vvdmul = dyn_cast<SpatVVDMulOp>(&op))
return PimsimSchedulerCostModel::getBinaryVectorCost(
return costModel.getBinaryVectorCost(
vvdmul.getLhs().getType(), vvdmul.getRhs().getType(), vvdmul.getOutput().getType(), /*scalarOutput=*/true);
if (auto vadd = dyn_cast<SpatVAddOp>(&op))
return PimsimSchedulerCostModel::getBinaryVectorCost(vadd.getLhs().getType(), vadd.getRhs().getType(),
vadd.getOutput().getType());
return costModel.getBinaryVectorCost(
vadd.getLhs().getType(), vadd.getRhs().getType(), vadd.getOutput().getType());
if (auto vsub = dyn_cast<SpatVSubOp>(&op))
return PimsimSchedulerCostModel::getBinaryVectorCost(vsub.getLhs().getType(), vsub.getRhs().getType(),
vsub.getOutput().getType());
return costModel.getBinaryVectorCost(
vsub.getLhs().getType(), vsub.getRhs().getType(), vsub.getOutput().getType());
if (auto vmul = dyn_cast<SpatVMulOp>(&op))
return PimsimSchedulerCostModel::getBinaryVectorCost(vmul.getLhs().getType(), vmul.getRhs().getType(),
vmul.getOutput().getType());
return costModel.getBinaryVectorCost(
vmul.getLhs().getType(), vmul.getRhs().getType(), vmul.getOutput().getType());
if (auto vmax = dyn_cast<SpatVMaxOp>(&op))
return PimsimSchedulerCostModel::getBinaryVectorCost(vmax.getLhs().getType(), vmax.getRhs().getType(),
vmax.getOutput().getType());
return costModel.getBinaryVectorCost(
vmax.getLhs().getType(), vmax.getRhs().getType(), vmax.getOutput().getType());
if (auto vavg = dyn_cast<SpatVAvgOp>(&op))
return PimsimSchedulerCostModel::getUnaryVectorCost(vavg.getInput().getType(), vavg.getOutput().getType(),
/*scalarOutput=*/true);
return costModel.getUnaryVectorCost(
vavg.getInput().getType(), vavg.getOutput().getType(), /*scalarOutput=*/true);
if (auto relu = dyn_cast<SpatReluOp>(&op))
return PimsimSchedulerCostModel::getUnaryVectorCost(relu.getInput().getType(), relu.getOutput().getType());
return costModel.getUnaryVectorCost(relu.getInput().getType(), relu.getOutput().getType());
if (auto sigm = dyn_cast<SpatSigmoidOp>(&op))
return PimsimSchedulerCostModel::getUnaryVectorCost(sigm.getInput().getType(), sigm.getOutput().getType());
return costModel.getUnaryVectorCost(sigm.getInput().getType(), sigm.getOutput().getType());
if (auto softmax = dyn_cast<SpatSoftmaxOp>(&op)) {
Cost unary = PimsimSchedulerCostModel::getUnaryVectorCost(softmax.getInput().getType(), softmax.getOutput().getType());
Cost unary =
costModel.getUnaryVectorCost(softmax.getInput().getType(), softmax.getOutput().getType());
return checkedMultiply(unary, static_cast<Cost>(4));
}
if (auto extract = dyn_cast<tensor::ExtractSliceOp>(&op))
return PimsimSchedulerCostModel::getTensorMoveCost(extract.getResult().getType());
return costModel.getTensorMoveCost(extract.getResult().getType());
if (auto insert = dyn_cast<tensor::InsertSliceOp>(&op))
return PimsimSchedulerCostModel::getTensorMoveCost(insert.getSource().getType());
return costModel.getTensorMoveCost(insert.getSource().getType());
Cost nestedCost = 0;
for (Region& region : op.getRegions())
nestedCost = checkedAdd(nestedCost, getRegionCost(region));
return checkedAdd(PimsimSchedulerCostModel::kFallbackOperationCost, nestedCost);
nestedCost = checkedAdd(nestedCost, getRegionCost(region, costModel));
return checkedAdd(SchedulerCostModel::kFallbackOperationCost, nestedCost);
}
std::optional<uint64_t> getStaticTripCount(scf::ForOp loop) {
@@ -271,8 +258,8 @@ std::optional<uint64_t> getStaticTripCount(scf::ForOp loop) {
return (distance + stride - 1) / stride;
}
Cost getComputeBodyCost(Region& body) {
return getRegionCost(body);
Cost getComputeBodyCost(Region& body, const SchedulerCostModel& costModel) {
return getRegionCost(body, costModel);
}
uint64_t countOperationInstances(Operation& op) {
@@ -348,7 +335,9 @@ std::optional<uint32_t> getConstantExtractLane(tensor::ExtractSliceOp extract) {
return std::nullopt;
}
std::optional<Cost> getBatchProjectedInputTransferCost(SpatComputeBatch batch, Value input) {
std::optional<TransferCost> getBatchProjectedInputTransferCost(
SpatComputeBatch batch, Value input,
const SchedulerCostModel& costModel) {
auto inputIt = llvm::find(batch.getInputs(), input);
if (inputIt == batch.getInputs().end())
return std::nullopt;
@@ -359,7 +348,7 @@ std::optional<Cost> getBatchProjectedInputTransferCost(SpatComputeBatch batch, V
if (!inputArg || !laneArg)
return std::nullopt;
Cost projectedCost = 0;
TransferCost projectedCost;
for (Operation* user : inputArg->getUsers()) {
auto extract = dyn_cast<tensor::ExtractSliceOp>(user);
if (!extract || extract.getSource() != *inputArg)
@@ -370,11 +359,13 @@ std::optional<Cost> getBatchProjectedInputTransferCost(SpatComputeBatch batch, V
auto resultType = dyn_cast<ShapedType>(extract.getResult().getType());
if (!resultType || !resultType.hasStaticShape())
return std::nullopt;
projectedCost = checkedAdd(
projectedCost, PimsimSchedulerCostModel::getInterCoreTransferCostFromBytes(static_cast<Cost>(getSizeInBytes(resultType))));
projectedCost = addTransferCosts(
projectedCost,
costModel.getTransferCostFromBytes(
costModel.getByteSize(resultType, costModel.getComputeBitwidth(resultType))));
}
if (projectedCost == 0)
if (projectedCost.fixed == 0 && projectedCost.networkFlits == 0)
return std::nullopt;
return projectedCost;
}
@@ -382,7 +373,8 @@ std::optional<Cost> getBatchProjectedInputTransferCost(SpatComputeBatch batch, V
static std::optional<SmallVector<ProducerValueRef, 4>>
collectProjectedProducerValueRefs(SpatComputeBatch producer,
Value input,
const ComputeInstance& consumerInstance) {
const ComputeInstance& consumerInstance,
size_t processorCount) {
auto consumer = dyn_cast<SpatComputeBatch>(consumerInstance.op);
if (!consumer)
return std::nullopt;
@@ -417,7 +409,8 @@ collectProjectedProducerValueRefs(SpatComputeBatch producer,
int64_t producerLane = *offset + index * *stride;
if (producerLane < 0 || producerLane >= producer.getLaneCount())
return std::nullopt;
ComputeInstance instance = getBatchChunkForLane(producer, static_cast<uint32_t>(producerLane));
ComputeInstance instance = getBatchChunkForLane(
producer, static_cast<uint32_t>(producerLane), processorCount);
if (llvm::none_of(producers, [&](const ProducerValueRef& ref) { return ref.instance == instance; }))
producers.push_back({instance, 0});
}
@@ -426,12 +419,16 @@ collectProjectedProducerValueRefs(SpatComputeBatch producer,
return producers;
}
Cost getInputTransferCost(const ComputeInstance& consumerInstance, Value input) {
TransferCost getInputTransferCost(const ComputeInstance& consumerInstance,
Value input,
const SchedulerCostModel& costModel) {
auto inputType = cast<ShapedType>(input.getType());
if (auto batch = dyn_cast<SpatComputeBatch>(consumerInstance.op))
if (std::optional<Cost> projectedCost = getBatchProjectedInputTransferCost(batch, input))
if (std::optional<TransferCost> projectedCost =
getBatchProjectedInputTransferCost(batch, input, costModel))
return *projectedCost;
return PimsimSchedulerCostModel::getInterCoreTransferCostFromBytes(static_cast<Cost>(getSizeInBytes(inputType)));
return costModel.getTransferCostFromBytes(
costModel.getByteSize(inputType, costModel.getComputeBitwidth(inputType)));
}
uint32_t getLaneOverlapCount(const ComputeInstance& lhs, const ComputeInstance& rhs) {
@@ -442,15 +439,20 @@ uint32_t getLaneOverlapCount(const ComputeInstance& lhs, const ComputeInstance&
: 0;
}
Cost scaleTransferCostByLaneCount(Cost totalCost, uint32_t totalLaneCount, uint32_t fragmentLaneCount) {
TransferCost scaleTransferCostByLaneCount(
const TransferCost& totalCost, uint32_t totalLaneCount,
uint32_t fragmentLaneCount) {
assert(totalLaneCount > 0 && "laneCount must be positive");
assert(fragmentLaneCount > 0 && "fragmentLaneCount must be positive");
if (fragmentLaneCount >= totalLaneCount)
return totalCost;
return checkedMultiply(totalCost, static_cast<Cost>(fragmentLaneCount)) / static_cast<Cost>(totalLaneCount);
return scaleTransferCost(totalCost, static_cast<Cost>(fragmentLaneCount),
static_cast<Cost>(totalLaneCount));
}
SmallVector<ProducerValueRef, 4> collectProducerValueRefs(Value value, const ComputeInstance& consumerInstance) {
SmallVector<ProducerValueRef, 4> collectProducerValueRefs(
Value value, const ComputeInstance& consumerInstance,
size_t processorCount) {
SmallVector<ProducerValueRef, 4> producers;
Operation* op = value.getDefiningOp();
if (!op)
@@ -461,13 +463,16 @@ SmallVector<ProducerValueRef, 4> collectProducerValueRefs(Value value, const Com
auto batch = dyn_cast_or_null<SpatComputeBatch>(source.getDefiningOp());
if (batch && batch.getNumResults() != 0) {
if (std::optional<uint32_t> lane = getConstantExtractLane(extract)) {
ComputeInstance instance = getBatchChunkForLane(batch, *lane);
ComputeInstance instance =
getBatchChunkForLane(batch, *lane, processorCount);
producers.push_back({instance, 0});
return producers;
}
for (ComputeInstance instance :
getBatchChunksForRange(batch, 0, static_cast<uint32_t>(batch.getLaneCount())))
getBatchChunksForRange(batch, 0,
static_cast<uint32_t>(batch.getLaneCount()),
processorCount))
producers.push_back({instance, 0});
return producers;
}
@@ -488,19 +493,24 @@ SmallVector<ProducerValueRef, 4> collectProducerValueRefs(Value value, const Com
if (auto batch = dyn_cast<SpatComputeBatch>(op)) {
if (batch.getNumResults() != 0) {
if (auto projected = collectProjectedProducerValueRefs(batch, value, consumerInstance))
if (auto projected = collectProjectedProducerValueRefs(
batch, value, consumerInstance, processorCount))
return *projected;
std::optional<ProducerValueRef> producer = getProducerValueRef(value, &consumerInstance);
std::optional<ProducerValueRef> producer =
getProducerValueRef(value, &consumerInstance, processorCount);
if (!producer)
return producers;
for (ComputeInstance instance :
getBatchChunksForRange(batch, producer->instance.laneStart, producer->instance.laneCount))
getBatchChunksForRange(batch, producer->instance.laneStart,
producer->instance.laneCount,
processorCount))
producers.push_back({instance, 0});
return producers;
}
uint32_t lane = cast<OpResult>(value).getResultNumber();
ComputeInstance instance = getBatchChunkForLane(batch, lane);
ComputeInstance instance =
getBatchChunkForLane(batch, lane, processorCount);
producers.push_back({instance, lane - instance.laneStart});
return producers;
}
@@ -508,18 +518,23 @@ SmallVector<ProducerValueRef, 4> collectProducerValueRefs(Value value, const Com
return producers;
}
Cost getProducerTransferCost(Value input,
const ComputeInstance& consumerInstance,
const ProducerValueRef& producerRef) {
Cost transferCost = getInputTransferCost(consumerInstance, input);
TransferCost getProducerTransferCost(
Value input, const ComputeInstance& consumerInstance,
const ProducerValueRef& producerRef,
const SchedulerCostModel& costModel) {
TransferCost transferCost =
getInputTransferCost(consumerInstance, input, costModel);
auto producerBatch = dyn_cast<SpatComputeBatch>(producerRef.instance.op);
if (!producerBatch || producerBatch.getNumResults() == 0)
return transferCost;
if (auto consumerBatch = dyn_cast<SpatComputeBatch>(consumerInstance.op)) {
if (std::optional<Cost> projectedCost = getBatchProjectedInputTransferCost(consumerBatch, input)) {
if (std::optional<TransferCost> projectedCost =
getBatchProjectedInputTransferCost(consumerBatch, input, costModel)) {
uint32_t overlapLaneCount = getLaneOverlapCount(consumerInstance, producerRef.instance);
return checkedMultiply(*projectedCost, static_cast<Cost>(std::max<uint32_t>(1, overlapLaneCount)));
return scaleTransferCost(
*projectedCost,
static_cast<Cost>(std::max<uint32_t>(1, overlapLaneCount)));
}
}
@@ -527,8 +542,8 @@ Cost getProducerTransferCost(Value input,
transferCost, static_cast<uint32_t>(producerBatch.getLaneCount()), producerRef.instance.laneCount);
}
static CrossbarWeight getOpaqueCrossbarWeight(Value value, std::optional<uint32_t> lane) {
CrossbarWeight weight;
static ResidentWeight getOpaqueResidentWeight(Value value, std::optional<uint32_t> lane) {
ResidentWeight weight;
weight.opaqueValue = value;
weight.opaqueLane = lane.value_or(std::numeric_limits<uint32_t>::max());
return weight;
@@ -619,7 +634,7 @@ static FailureOr<SmallVector<int64_t, 4>> evaluateIndexList(ArrayRef<OpFoldResul
return result;
}
static Value resolveCrossbarWeightRoot(Operation* owner, Value root) {
static Value resolveResidentWeightRoot(Operation* owner, Value root) {
if (auto arg = dyn_cast<BlockArgument>(root)) {
if (auto compute = dyn_cast<SpatCompute>(owner)) {
for (auto [index, operand] : llvm::enumerate(compute.getWeights()))
@@ -637,11 +652,11 @@ static Value resolveCrossbarWeightRoot(Operation* owner, Value root) {
return root;
}
static CrossbarWeight completeCrossbarWeight(Value root,
static ResidentWeight completeResidentWeight(Value root,
SmallVector<int64_t, 4> offsets,
SmallVector<int64_t, 4> sizes,
SmallVector<int64_t, 4> strides) {
CrossbarWeight weight;
ResidentWeight weight;
weight.root = root;
if (auto constant = root.getDefiningOp<arith::ConstantOp>())
weight.rootAttr = static_cast<Attribute>(constant.getValue());
@@ -651,14 +666,14 @@ static CrossbarWeight completeCrossbarWeight(Value root,
return weight;
}
static FailureOr<CrossbarWeight> getStaticCrossbarWeight(Operation* owner,
static FailureOr<ResidentWeight> getStaticResidentWeight(Operation* owner,
Value value,
const DenseMap<Value, int64_t>& bindings,
std::optional<uint32_t> lane,
Value laneArg) {
if (auto extract = value.getDefiningOp<tensor::ExtractSliceOp>()) {
FailureOr<CrossbarWeight> sourceWeight =
getStaticCrossbarWeight(owner, extract.getSource(), bindings, lane, laneArg);
FailureOr<ResidentWeight> sourceWeight =
getStaticResidentWeight(owner, extract.getSource(), bindings, lane, laneArg);
auto offsets = evaluateIndexList(extract.getMixedOffsets(), bindings, lane, laneArg);
auto sizes = evaluateIndexList(extract.getMixedSizes(), bindings, lane, laneArg);
auto strides = evaluateIndexList(extract.getMixedStrides(), bindings, lane, laneArg);
@@ -678,7 +693,7 @@ static FailureOr<CrossbarWeight> getStaticCrossbarWeight(Operation* owner,
return *sourceWeight;
}
Value root = resolveCrossbarWeightRoot(owner, value);
Value root = resolveResidentWeightRoot(owner, value);
auto type = dyn_cast<ShapedType>(root.getType());
if (!type || !type.hasStaticShape())
return failure();
@@ -686,18 +701,18 @@ static FailureOr<CrossbarWeight> getStaticCrossbarWeight(Operation* owner,
SmallVector<int64_t, 4> offsets(type.getRank(), 0);
SmallVector<int64_t, 4> sizes(type.getShape().begin(), type.getShape().end());
SmallVector<int64_t, 4> strides(type.getRank(), 1);
return completeCrossbarWeight(root, std::move(offsets), std::move(sizes), std::move(strides));
return completeResidentWeight(root, std::move(offsets), std::move(sizes), std::move(strides));
}
static void addCrossbarWeight(CrossbarUsage& usage, CrossbarWeight weight) {
if (!containsCrossbarWeight(usage, weight))
static void addResidentWeight(ResidentWeightSet& usage, ResidentWeight weight) {
if (!containsResidentWeight(usage, weight))
usage.push_back(std::move(weight));
}
static void collectCrossbarWeightsFromOp(Operation* op,
static void collectResidentWeightsFromOp(Operation* op,
Operation* owner,
DenseMap<Value, int64_t>& bindings,
CrossbarUsage& usage,
ResidentWeightSet& usage,
Value laneArg,
std::optional<uint32_t> lane) {
if (auto loop = dyn_cast<scf::ForOp>(op)) {
@@ -710,36 +725,37 @@ static void collectCrossbarWeightsFromOp(Operation* op,
for (int64_t iv = *lb; iv < *ub; iv += *step) {
bindings[loop.getInductionVar()] = iv;
for (Operation& nested : loop.getBody()->without_terminator())
collectCrossbarWeightsFromOp(&nested, owner, bindings, usage, laneArg, lane);
collectResidentWeightsFromOp(&nested, owner, bindings, usage, laneArg, lane);
}
bindings.erase(loop.getInductionVar());
return;
}
if (auto vmm = dyn_cast<SpatVMMOp>(op)) {
FailureOr<CrossbarWeight> weight = getStaticCrossbarWeight(owner, vmm.getWeight(), bindings, lane, laneArg);
FailureOr<ResidentWeight> weight = getStaticResidentWeight(owner, vmm.getWeight(), bindings, lane, laneArg);
if (failed(weight)) {
addCrossbarWeight(usage, getOpaqueCrossbarWeight(vmm.getWeight(), lane));
addResidentWeight(usage, getOpaqueResidentWeight(vmm.getWeight(), lane));
return;
}
addCrossbarWeight(usage, *weight);
addResidentWeight(usage, *weight);
return;
}
for (Region& region : op->getRegions())
for (Block& block : region)
for (Operation& nested : block.without_terminator())
collectCrossbarWeightsFromOp(&nested, owner, bindings, usage, laneArg, lane);
collectResidentWeightsFromOp(&nested, owner, bindings, usage, laneArg, lane);
}
std::vector<ComputeGraphEdge> aggregateEdges(llvm::ArrayRef<ComputeGraphEdge> edges) {
llvm::DenseMap<std::pair<size_t, size_t>, Cost> edgeCosts;
llvm::DenseMap<std::pair<size_t, size_t>, TransferCost> edgeCosts;
for (const ComputeGraphEdge& edge : edges) {
if (edge.source == edge.target)
continue;
auto inserted = edgeCosts.try_emplace({edge.source, edge.target}, edge.transferCost);
if (!inserted.second)
inserted.first->second = checkedAdd(inserted.first->second, edge.transferCost);
inserted.first->second =
addTransferCosts(inserted.first->second, edge.transferCost);
}
std::vector<ComputeGraphEdge> aggregatedEdges;
@@ -770,8 +786,8 @@ uint64_t countComputeBodyOperationInstances(Region& body) {
return instances;
}
CrossbarUsage collectDistinctCrossbarWeights(Operation* owner, std::optional<uint32_t> lane) {
CrossbarUsage usage;
ResidentWeightSet collectDistinctResidentWeights(Operation* owner, std::optional<uint32_t> lane) {
ResidentWeightSet usage;
DenseMap<Value, int64_t> bindings;
Value laneArg;
if (auto batch = dyn_cast<SpatComputeBatch>(owner))
@@ -781,54 +797,87 @@ CrossbarUsage collectDistinctCrossbarWeights(Operation* owner, std::optional<uin
for (Region& region : owner->getRegions())
for (Block& block : region)
for (Operation& op : block.without_terminator())
collectCrossbarWeightsFromOp(&op, owner, bindings, usage, laneArg, lane);
collectResidentWeightsFromOp(&op, owner, bindings, usage, laneArg, lane);
return usage;
}
Cost getComputeInstanceCost(const ComputeInstance& instance) {
Cost getComputeInstanceCost(const ComputeInstance& instance, const SchedulingTarget& target) {
SchedulerCostModel costModel {target};
if (auto spatCompute = dyn_cast<SpatCompute>(instance.op))
return getComputeBodyCost(spatCompute.getBody());
return getComputeBodyCost(spatCompute.getBody(), costModel);
auto batch = cast<SpatComputeBatch>(instance.op);
return checkedMultiply(getComputeBodyCost(batch.getBody()), static_cast<Cost>(instance.laneCount));
return checkedMultiply(
getComputeBodyCost(batch.getBody(), costModel), static_cast<Cost>(instance.laneCount));
}
bool containsCrossbarWeight(ArrayRef<CrossbarWeight> usage, const CrossbarWeight& weight) {
bool containsResidentWeight(ArrayRef<ResidentWeight> usage, const ResidentWeight& weight) {
return llvm::is_contained(usage, weight);
}
unsigned countCrossbarOverlap(ArrayRef<CrossbarWeight> lhs, ArrayRef<CrossbarWeight> rhs) {
unsigned countResidentWeightOverlap(ArrayRef<ResidentWeight> lhs, ArrayRef<ResidentWeight> rhs) {
unsigned overlap = 0;
for (const CrossbarWeight& weight : rhs)
if (containsCrossbarWeight(lhs, weight))
for (const ResidentWeight& weight : rhs)
if (containsResidentWeight(lhs, weight))
++overlap;
return overlap;
}
size_t getCrossbarUnionSize(ArrayRef<CrossbarWeight> lhs, ArrayRef<CrossbarWeight> rhs) {
size_t getResidentWeightUnionSize(ArrayRef<ResidentWeight> lhs, ArrayRef<ResidentWeight> rhs) {
size_t size = lhs.size();
for (const CrossbarWeight& weight : rhs)
if (!containsCrossbarWeight(lhs, weight))
for (const ResidentWeight& weight : rhs)
if (!containsResidentWeight(lhs, weight))
++size;
return size;
}
void insertCrossbarWeights(CrossbarUsage& usage, ArrayRef<CrossbarWeight> weights) {
for (const CrossbarWeight& weight : weights)
addCrossbarWeight(usage, weight);
void insertResidentWeights(ResidentWeightSet& usage, ArrayRef<ResidentWeight> weights) {
for (const ResidentWeight& weight : weights)
addResidentWeight(usage, weight);
}
CrossbarUsage getComputeInstanceCrossbarUsage(const ComputeInstance& instance) {
CrossbarUsage usage;
ResidentWeightSet getComputeInstanceResidentWeights(const ComputeInstance& instance) {
ResidentWeightSet usage;
if (isa<SpatCompute>(instance.op))
return collectDistinctCrossbarWeights(instance.op);
return collectDistinctResidentWeights(instance.op);
for (uint32_t lane = instance.laneStart; lane < instance.laneStart + instance.laneCount; ++lane)
insertCrossbarWeights(usage, collectDistinctCrossbarWeights(instance.op, lane));
insertResidentWeights(usage, collectDistinctResidentWeights(instance.op, lane));
return usage;
}
ComputeGraph buildComputeGraph(Operation* entryOp) {
ResidentWeightSet getComputeInstanceResidentWeights(const ComputeInstance& instance,
const SchedulingTarget& target) {
ResidentWeightSet tiled;
for (const ResidentWeight& weight : getComputeInstanceResidentWeights(instance)) {
if (weight.opaqueValue || weight.sizes.size() < 2
|| target.matrixRows == 0 || target.matrixColumns == 0) {
addResidentWeight(tiled, weight);
continue;
}
const size_t rowDim = weight.sizes.size() - 2;
const size_t columnDim = weight.sizes.size() - 1;
for (int64_t row = 0; row < weight.sizes[rowDim];
row += static_cast<int64_t>(target.matrixRows)) {
for (int64_t column = 0; column < weight.sizes[columnDim];
column += static_cast<int64_t>(target.matrixColumns)) {
ResidentWeight tile = weight;
tile.offsets[rowDim] += row * tile.strides[rowDim];
tile.offsets[columnDim] += column * tile.strides[columnDim];
tile.sizes[rowDim] =
std::min<int64_t>(target.matrixRows, weight.sizes[rowDim] - row);
tile.sizes[columnDim] =
std::min<int64_t>(target.matrixColumns, weight.sizes[columnDim] - column);
addResidentWeight(tiled, std::move(tile));
}
}
}
return tiled;
}
ComputeGraph buildComputeGraph(Operation* entryOp, const SchedulingTarget& target) {
ComputeGraph graph;
SchedulerCostModel costModel {target};
for (Region& region : entryOp->getRegions()) {
for (Block& block : region) {
@@ -838,20 +887,26 @@ ComputeGraph buildComputeGraph(Operation* entryOp) {
continue;
ComputeInstance instance {spatCompute.getOperation(), 0, 1};
size_t index = graph.nodes.size();
graph.nodes.push_back(
{instance, getComputeInstanceCost(instance), getComputeInstanceCrossbarUsage(instance), index});
graph.nodes.push_back({instance,
getComputeInstanceCost(instance, target),
getComputeInstanceResidentWeights(instance, target),
index});
graph.instanceToIndex[instance] = index;
continue;
}
if (auto batch = dyn_cast<SpatComputeBatch>(&op)) {
if (isUsedAsWeightOnly(batch.getOperation()))
continue;
size_t chunkCount = getBatchChunkTargetCount(batch);
size_t chunkCount =
getBatchChunkTargetCount(batch, target.processorCount);
for (size_t chunkIndex = 0; chunkIndex < chunkCount; ++chunkIndex) {
ComputeInstance instance = getBatchChunkForIndex(batch, chunkIndex);
ComputeInstance instance = getBatchChunkForIndex(
batch, chunkIndex, target.processorCount);
size_t index = graph.nodes.size();
graph.nodes.push_back(
{instance, getComputeInstanceCost(instance), getComputeInstanceCrossbarUsage(instance), index});
graph.nodes.push_back({instance,
getComputeInstanceCost(instance, target),
getComputeInstanceResidentWeights(instance, target),
index});
graph.instanceToIndex[instance] = index;
}
}
@@ -863,12 +918,15 @@ ComputeGraph buildComputeGraph(Operation* entryOp) {
for (const auto& [targetIndex, node] : llvm::enumerate(graph.nodes)) {
llvm::SmallVector<Value, 4> inputs = getComputeInstanceInputs(node.instance);
for (Value input : inputs) {
for (const ProducerValueRef& producerRef : collectProducerValueRefs(input, node.instance)) {
for (const ProducerValueRef& producerRef :
collectProducerValueRefs(input, node.instance,
target.processorCount)) {
auto producerIt = graph.instanceToIndex.find(producerRef.instance);
if (producerIt == graph.instanceToIndex.end())
continue;
rawEdges.push_back(
{producerIt->second, targetIndex, getProducerTransferCost(input, node.instance, producerRef)});
rawEdges.push_back({producerIt->second,
targetIndex,
getProducerTransferCost(input, node.instance, producerRef, costModel)});
}
}
}
@@ -11,9 +11,10 @@
#include <vector>
#include "ComputeInstance.hpp"
#include "SchedulingTarget.hpp"
#include "Utils.hpp"
struct CrossbarWeight {
struct ResidentWeight {
mlir::Value root;
mlir::Attribute rootAttr;
llvm::SmallVector<int64_t, 4> offsets;
@@ -22,14 +23,14 @@ struct CrossbarWeight {
mlir::Value opaqueValue;
uint32_t opaqueLane = 0;
bool operator==(const CrossbarWeight& other) const {
bool operator==(const ResidentWeight& other) const {
bool sameRoot = rootAttr && other.rootAttr ? rootAttr == other.rootAttr : root == other.root;
return sameRoot && offsets == other.offsets && sizes == other.sizes && strides == other.strides
&& opaqueValue == other.opaqueValue && opaqueLane == other.opaqueLane;
}
};
using CrossbarUsage = llvm::SmallVector<CrossbarWeight, 6>;
using ResidentWeightSet = llvm::SmallVector<ResidentWeight, 6>;
namespace onnx_mlir {
namespace spatial {
@@ -37,36 +38,47 @@ namespace spatial {
struct ComputeGraphNode {
ComputeInstance instance;
Cost cost = 0;
CrossbarUsage crossbarUsage;
ResidentWeightSet residentWeights;
size_t originalOrder = 0;
};
struct TransferCost {
Cost fixed = 0;
Cost networkFlits = 0;
};
struct ComputeGraphEdge {
size_t source = 0;
size_t target = 0;
Cost transferCost = 0;
TransferCost transferCost;
};
struct ComputeGraph {
std::vector<ComputeGraphNode> nodes;
std::vector<ComputeGraphEdge> edges;
std::vector<std::vector<std::pair<size_t, Cost>>> successors;
std::vector<std::vector<std::pair<size_t, Cost>>> predecessors;
std::vector<std::vector<std::pair<size_t, TransferCost>>> successors;
std::vector<std::vector<std::pair<size_t, TransferCost>>> predecessors;
llvm::DenseMap<ComputeInstance, size_t> instanceToIndex;
};
ComputeGraph buildComputeGraph(mlir::Operation* entryOp);
ComputeGraph buildComputeGraph(mlir::Operation* entryOp, const SchedulingTarget& target);
bool verifyAcyclic(const ComputeGraph& graph);
uint64_t countComputeBodyInstructions(mlir::Region& body);
uint64_t countComputeBodyOperationInstances(mlir::Region& body);
Cost getComputeInstanceCost(const ComputeInstance& instance);
CrossbarUsage collectDistinctCrossbarWeights(mlir::Operation* owner, std::optional<uint32_t> lane = std::nullopt);
CrossbarUsage getComputeInstanceCrossbarUsage(const ComputeInstance& instance);
bool containsCrossbarWeight(llvm::ArrayRef<CrossbarWeight> usage, const CrossbarWeight& weight);
unsigned countCrossbarOverlap(llvm::ArrayRef<CrossbarWeight> lhs, llvm::ArrayRef<CrossbarWeight> rhs);
size_t getCrossbarUnionSize(llvm::ArrayRef<CrossbarWeight> lhs, llvm::ArrayRef<CrossbarWeight> rhs);
void insertCrossbarWeights(CrossbarUsage& usage, llvm::ArrayRef<CrossbarWeight> weights);
Cost getComputeInstanceCost(const ComputeInstance& instance, const SchedulingTarget& target);
ResidentWeightSet collectDistinctResidentWeights(mlir::Operation* owner,
std::optional<uint32_t> lane = std::nullopt);
ResidentWeightSet getComputeInstanceResidentWeights(const ComputeInstance& instance);
ResidentWeightSet getComputeInstanceResidentWeights(const ComputeInstance& instance,
const SchedulingTarget& target);
bool containsResidentWeight(llvm::ArrayRef<ResidentWeight> usage, const ResidentWeight& weight);
unsigned countResidentWeightOverlap(llvm::ArrayRef<ResidentWeight> lhs,
llvm::ArrayRef<ResidentWeight> rhs);
size_t getResidentWeightUnionSize(llvm::ArrayRef<ResidentWeight> lhs,
llvm::ArrayRef<ResidentWeight> rhs);
void insertResidentWeights(ResidentWeightSet& usage,
llvm::ArrayRef<ResidentWeight> weights);
} // namespace spatial
} // namespace onnx_mlir
@@ -2,23 +2,15 @@
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include <algorithm>
#include <limits>
#include <optional>
#include "ComputeInstanceUtils.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
using namespace mlir;
namespace onnx_mlir {
namespace spatial {
size_t getSchedulingCpuBudget() {
if (coresCount.getValue() > 0)
return static_cast<size_t>(coresCount.getValue());
return std::numeric_limits<size_t>::max();
}
static BatchChunkRange getBatchChunkRange(int32_t laneCount, size_t chunkCount, size_t chunkIndex) {
assert(laneCount > 0 && "laneCount must be positive");
assert(chunkIndex < chunkCount && "chunkIndex out of range");
@@ -33,22 +25,27 @@ static BatchChunkRange getBatchChunkRange(int32_t laneCount, size_t chunkCount,
return {static_cast<uint32_t>(start), static_cast<uint32_t>(count)};
}
size_t getBatchChunkTargetCount(SpatComputeBatch batch) {
size_t getBatchChunkTargetCount(SpatComputeBatch batch, size_t processorCount) {
int32_t laneCount = batch.getLaneCount();
assert(laneCount > 0 && "laneCount must be positive");
return std::min(static_cast<size_t>(laneCount), getSchedulingCpuBudget());
assert(processorCount > 0 && "processorCount must be positive");
return std::min(static_cast<size_t>(laneCount), processorCount);
}
BatchChunkRange getBatchChunkRange(SpatComputeBatch batch, size_t chunkIndex) {
return getBatchChunkRange(batch.getLaneCount(), getBatchChunkTargetCount(batch), chunkIndex);
BatchChunkRange getBatchChunkRange(SpatComputeBatch batch, size_t chunkIndex,
size_t processorCount) {
return getBatchChunkRange(batch.getLaneCount(),
getBatchChunkTargetCount(batch, processorCount),
chunkIndex);
}
size_t getBatchChunkIndexForLane(SpatComputeBatch batch, uint32_t lane) {
size_t getBatchChunkIndexForLane(SpatComputeBatch batch, uint32_t lane,
size_t processorCount) {
int32_t laneCount = batch.getLaneCount();
assert(laneCount > 0 && "laneCount must be positive");
assert(lane < static_cast<uint32_t>(laneCount) && "lane out of range");
size_t chunkCount = getBatchChunkTargetCount(batch);
size_t chunkCount = getBatchChunkTargetCount(batch, processorCount);
size_t laneCountSize = static_cast<size_t>(laneCount);
size_t baseChunkSize = laneCountSize / chunkCount;
size_t remainder = laneCountSize % chunkCount;
@@ -61,17 +58,22 @@ size_t getBatchChunkIndexForLane(SpatComputeBatch batch, uint32_t lane) {
return remainder + ((laneIndex - largerChunkLanes) / baseChunkSize);
}
ComputeInstance getBatchChunkForIndex(SpatComputeBatch batch, size_t chunkIndex) {
BatchChunkRange chunk = getBatchChunkRange(batch, chunkIndex);
ComputeInstance getBatchChunkForIndex(SpatComputeBatch batch, size_t chunkIndex,
size_t processorCount) {
BatchChunkRange chunk = getBatchChunkRange(batch, chunkIndex, processorCount);
return {batch.getOperation(), chunk.laneStart, chunk.laneCount};
}
ComputeInstance getBatchChunkForLane(SpatComputeBatch batch, uint32_t lane) {
return getBatchChunkForIndex(batch, getBatchChunkIndexForLane(batch, lane));
ComputeInstance getBatchChunkForLane(SpatComputeBatch batch, uint32_t lane,
size_t processorCount) {
return getBatchChunkForIndex(
batch, getBatchChunkIndexForLane(batch, lane, processorCount),
processorCount);
}
llvm::SmallVector<ComputeInstance, 4>
getBatchChunksForRange(SpatComputeBatch batch, uint32_t laneStart, uint32_t laneCount) {
getBatchChunksForRange(SpatComputeBatch batch, uint32_t laneStart,
uint32_t laneCount, size_t processorCount) {
llvm::SmallVector<ComputeInstance, 4> chunks;
if (laneCount == 0)
return chunks;
@@ -80,11 +82,13 @@ getBatchChunksForRange(SpatComputeBatch batch, uint32_t laneStart, uint32_t lane
assert(laneEnd >= laneStart && "lane range overflow");
assert(laneEnd <= static_cast<uint32_t>(batch.getLaneCount()) && "lane range out of bounds");
size_t firstChunk = getBatchChunkIndexForLane(batch, laneStart);
size_t lastChunk = getBatchChunkIndexForLane(batch, laneEnd - 1);
size_t firstChunk =
getBatchChunkIndexForLane(batch, laneStart, processorCount);
size_t lastChunk =
getBatchChunkIndexForLane(batch, laneEnd - 1, processorCount);
chunks.reserve(lastChunk - firstChunk + 1);
for (size_t chunkIndex = firstChunk; chunkIndex <= lastChunk; ++chunkIndex)
chunks.push_back(getBatchChunkForIndex(batch, chunkIndex));
chunks.push_back(getBatchChunkForIndex(batch, chunkIndex, processorCount));
return chunks;
}
@@ -150,7 +154,9 @@ static std::optional<ProducerValueRef> getResultfulBatchProducerValueRef(SpatCom
};
}
std::optional<ProducerValueRef> getProducerValueRef(Value value, const ComputeInstance* consumerInstance) {
std::optional<ProducerValueRef> getProducerValueRef(
Value value, const ComputeInstance* consumerInstance,
size_t processorCount) {
Operation* op = value.getDefiningOp();
if (!op)
return std::nullopt;
@@ -187,7 +193,8 @@ std::optional<ProducerValueRef> getProducerValueRef(Value value, const ComputeIn
if (batch.getNumResults() != 0)
return getResultfulBatchProducerValueRef(batch, value, consumerInstance);
uint32_t lane = cast<OpResult>(value).getResultNumber();
ComputeInstance instance = getBatchChunkForLane(batch, lane);
ComputeInstance instance =
getBatchChunkForLane(batch, lane, processorCount);
size_t resultIndex = lane - instance.laneStart;
return ProducerValueRef {instance, resultIndex};
}
@@ -195,8 +202,11 @@ std::optional<ProducerValueRef> getProducerValueRef(Value value, const ComputeIn
return std::nullopt;
}
std::optional<ComputeInstance> getComputeProducerInstance(Value value, const ComputeInstance* consumerInstance) {
if (std::optional<ProducerValueRef> producer = getProducerValueRef(value, consumerInstance))
std::optional<ComputeInstance> getComputeProducerInstance(
Value value, const ComputeInstance* consumerInstance,
size_t processorCount) {
if (std::optional<ProducerValueRef> producer =
getProducerValueRef(value, consumerInstance, processorCount))
return producer->instance;
return std::nullopt;
}
@@ -26,19 +26,25 @@ struct BatchChunkRange {
uint32_t laneCount = 0;
};
size_t getSchedulingCpuBudget();
size_t getBatchChunkTargetCount(SpatComputeBatch batch);
BatchChunkRange getBatchChunkRange(SpatComputeBatch batch, size_t chunkIndex);
size_t getBatchChunkIndexForLane(SpatComputeBatch batch, uint32_t lane);
ComputeInstance getBatchChunkForIndex(SpatComputeBatch batch, size_t chunkIndex);
ComputeInstance getBatchChunkForLane(SpatComputeBatch batch, uint32_t lane);
size_t getBatchChunkTargetCount(SpatComputeBatch batch, size_t processorCount);
BatchChunkRange getBatchChunkRange(SpatComputeBatch batch, size_t chunkIndex,
size_t processorCount);
size_t getBatchChunkIndexForLane(SpatComputeBatch batch, uint32_t lane,
size_t processorCount);
ComputeInstance getBatchChunkForIndex(SpatComputeBatch batch, size_t chunkIndex,
size_t processorCount);
ComputeInstance getBatchChunkForLane(SpatComputeBatch batch, uint32_t lane,
size_t processorCount);
llvm::SmallVector<ComputeInstance, 4>
getBatchChunksForRange(SpatComputeBatch batch, uint32_t laneStart, uint32_t laneCount);
getBatchChunksForRange(SpatComputeBatch batch, uint32_t laneStart,
uint32_t laneCount, size_t processorCount);
std::optional<ProducerValueRef> getProducerValueRef(mlir::Value value,
const ComputeInstance* consumerInstance = nullptr);
const ComputeInstance* consumerInstance,
size_t processorCount);
std::optional<ComputeInstance> getComputeProducerInstance(mlir::Value value,
const ComputeInstance* consumerInstance = nullptr);
const ComputeInstance* consumerInstance,
size_t processorCount);
llvm::SmallVector<mlir::Value, 4> getComputeInstanceInputs(const ComputeInstance& instance);
llvm::SmallVector<mlir::Value, 4> getComputeInstanceWeights(const ComputeInstance& instance);
@@ -13,6 +13,7 @@ namespace onnx_mlir {
namespace spatial {
struct MergeScheduleResult {
size_t processorCount = 0;
std::vector<ComputeInstance> dominanceOrderCompute;
llvm::DenseMap<ComputeInstance, size_t> computeToCpuMap;
llvm::DenseMap<ComputeInstance, size_t> computeToCpuSlotMap;
@@ -9,7 +9,6 @@
#include "ComputeGraph.hpp"
#include "MergeSchedulingAnalysis.hpp"
#include "PeftScheduler.hpp"
#include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp"
namespace onnx_mlir {
namespace spatial {
@@ -18,8 +17,7 @@ namespace {
void verifySchedule(const ComputeGraph& graph,
const MergeScheduleResult& result,
unsigned long crossbarCapacity,
size_t processorCount) {
const SchedulingTarget& target) {
llvm::DenseMap<size_t, std::vector<std::pair<size_t, size_t>>> tasksByCpu;
tasksByCpu.reserve(result.cpuToLastComputeMap.size());
@@ -44,13 +42,13 @@ void verifySchedule(const ComputeGraph& graph,
return lhs.second < rhs.second;
});
CrossbarUsage usedCrossbars;
ResidentWeightSet residentWeights;
for (size_t slot = 0; slot < scheduledTasks.size(); ++slot) {
if (scheduledTasks[slot].first != slot)
llvm::report_fatal_error("merge scheduling: CPU slots are not contiguous");
insertCrossbarWeights(usedCrossbars, graph.nodes[scheduledTasks[slot].second].crossbarUsage);
if (usedCrossbars.size() > crossbarCapacity)
llvm::report_fatal_error("merge scheduling: CPU crossbar capacity exceeded");
insertResidentWeights(residentWeights, graph.nodes[scheduledTasks[slot].second].residentWeights);
if (residentWeights.size() > target.residentWeightCapacity)
llvm::report_fatal_error("merge scheduling: processor resident-weight capacity exceeded");
}
const ComputeInstance expectedLast = graph.nodes[scheduledTasks.back().second].instance;
@@ -63,20 +61,21 @@ void verifySchedule(const ComputeGraph& graph,
for (const ComputeGraphEdge& edge : graph.edges) {
const ComputeInstance source = graph.nodes[edge.source].instance;
const ComputeInstance target = graph.nodes[edge.target].instance;
const ComputeInstance destination = graph.nodes[edge.target].instance;
const size_t sourceCpu = result.computeToCpuMap.lookup(source);
const size_t targetCpu = result.computeToCpuMap.lookup(target);
const size_t targetCpu = result.computeToCpuMap.lookup(destination);
const size_t sourceSlot = result.computeToCpuSlotMap.lookup(source);
const size_t targetSlot = result.computeToCpuSlotMap.lookup(target);
const size_t targetSlot = result.computeToCpuSlotMap.lookup(destination);
const Time sourceStart = static_cast<Time>(result.computeToAestMap.lookup(source));
const Time targetStart = static_cast<Time>(result.computeToAestMap.lookup(target));
const Time targetStart =
static_cast<Time>(result.computeToAestMap.lookup(destination));
if (sourceCpu == targetCpu && sourceSlot >= targetSlot)
llvm::report_fatal_error("merge scheduling: same-CPU dependency order is invalid");
Time earliestTargetStart = addOrMax(sourceStart, graph.nodes[edge.source].cost);
if (sourceCpu != targetCpu)
earliestTargetStart = addOrMax(
earliestTargetStart, getPeftTransferTime(edge.transferCost, sourceCpu, targetCpu, processorCount));
earliestTargetStart, getPeftTransferTime(edge.transferCost, sourceCpu, targetCpu, target));
if (targetStart < earliestTargetStart) {
std::string message = llvm::formatv("merge scheduling: dependency legality failed between tasks {0} and {1}",
graph.nodes[edge.source].originalOrder,
@@ -89,30 +88,22 @@ void verifySchedule(const ComputeGraph& graph,
} // namespace
MergeSchedulingAnalysis::MergeSchedulingAnalysis(mlir::Operation* op)
: entryOp(op) {
MergeSchedulingAnalysis::MergeSchedulingAnalysis(mlir::Operation* op,
const SchedulingTarget& schedulingTarget)
: entryOp(op), target(schedulingTarget) {
result = run();
}
MergeScheduleResult MergeSchedulingAnalysis::run() {
verifyExplicitPimCoreCount();
ComputeGraph graph = buildComputeGraph(entryOp);
ComputeGraph graph = buildComputeGraph(entryOp, target);
if (!verifyAcyclic(graph))
llvm::report_fatal_error("merge scheduling: compute graph is cyclic");
size_t processorCount = 0;
if (coresCount.getValue() > 0)
processorCount = static_cast<size_t>(coresCount.getValue());
MergeScheduleResult schedule = runPeftScheduler(
graph, PeftScheduleOptions {
processorCount,
static_cast<unsigned long>(crossbarCountInCore.getValue()),
target,
entryOp->getContext()});
verifySchedule(graph,
schedule,
static_cast<unsigned long>(crossbarCountInCore.getValue()),
processorCount);
verifySchedule(graph, schedule, target);
return schedule;
}
@@ -3,17 +3,19 @@
#include "mlir/IR/Operation.h"
#include "MergeSchedule.hpp"
#include "SchedulingTarget.hpp"
namespace onnx_mlir {
namespace spatial {
class MergeSchedulingAnalysis {
public:
explicit MergeSchedulingAnalysis(mlir::Operation* op);
MergeSchedulingAnalysis(mlir::Operation* op, const SchedulingTarget& target);
MergeScheduleResult& getResult() { return result; }
private:
mlir::Operation* entryOp = nullptr;
const SchedulingTarget& target;
MergeScheduleResult result;
MergeScheduleResult run();
@@ -4,8 +4,8 @@
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/FormatVariadic.h"
#include <cmath>
#include <limits>
#include <numeric>
#include <optional>
#include <queue>
#include <tuple>
@@ -22,9 +22,7 @@ using namespace mlir;
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;
constexpr size_t kHighResidentWeightPressureCapacityDivisor = 2;
struct ScheduledTask {
size_t processor = std::numeric_limits<size_t>::max();
@@ -32,63 +30,22 @@ struct ScheduledTask {
Time endTime = 0;
};
struct MeshModel {
size_t rows = 1;
size_t cols = 1;
long double averageDistance = 0.0L;
static MeshModel infer(size_t processorCount) {
MeshModel model;
if (processorCount == 0)
return model;
model.rows = static_cast<size_t>(std::sqrt(static_cast<long double>(processorCount)));
if (model.rows == 0)
model.rows = 1;
while (model.rows > 1 && processorCount % model.rows != 0)
--model.rows;
model.cols = (processorCount + model.rows - 1) / model.rows;
auto averageAxisDistance = [](size_t size) -> long double {
if (size <= 1)
return 0.0L;
return static_cast<long double>(size * size - 1) / (3.0L * static_cast<long double>(size));
};
model.averageDistance = averageAxisDistance(model.rows) + averageAxisDistance(model.cols);
return model;
}
std::pair<size_t, size_t> getCoord(size_t processor) const {
return {processor / cols, processor % cols};
}
size_t getDistance(size_t lhs, size_t rhs) const {
auto [lhsRow, lhsCol] = getCoord(lhs);
auto [rhsRow, rhsCol] = getCoord(rhs);
size_t rowDistance = lhsRow > rhsRow ? lhsRow - rhsRow : rhsRow - lhsRow;
size_t colDistance = lhsCol > rhsCol ? lhsCol - rhsCol : rhsCol - lhsCol;
return rowDistance + colDistance;
}
Time scaleTransferCost(Time transferCost, size_t sourceProcessor, size_t targetProcessor) const {
if (sourceProcessor == targetProcessor || transferCost == 0)
return 0;
long double distance = static_cast<long double>(getDistance(sourceProcessor, targetProcessor));
long double scale = averageDistance > 0.0L ? distance / averageDistance : 1.0L;
scale = std::max(0.25L, scale);
return static_cast<Time>(std::ceil(static_cast<long double>(transferCost) * scale));
}
struct TopologyModel {
const SchedulingTarget& target;
size_t getCenterDistance(size_t processor) const {
auto [row, col] = getCoord(processor);
size_t centerRow = rows / 2;
size_t centerCol = cols / 2;
size_t rowDistance = row > centerRow ? row - centerRow : centerRow - row;
size_t colDistance = col > centerCol ? col - centerCol : centerCol - col;
return rowDistance + colDistance;
Cost total = 0;
for (size_t other = 0; other < target.processorCount; ++other)
total = checkedAdd(total, target.getInterProcessorLatencyNs(processor, other));
return static_cast<size_t>(total);
}
};
Time getAverageTransferTime(const TransferCost& transferCost, const SchedulingTarget& target) {
return checkedAdd(transferCost.fixed,
checkedMultiply(transferCost.networkFlits, target.averageInterProcessorLatencyNs));
}
std::vector<std::vector<size_t>> buildReverseLevels(const ComputeGraph& graph) {
std::vector<size_t> remainingSuccessors(graph.nodes.size(), 0);
std::queue<size_t> readySinks;
@@ -143,14 +100,14 @@ void verifyOctTableSize(size_t nodeCount, size_t processorCount) {
}
}
bool hasHighCrossbarPressure(const ComputeGraph& graph, size_t processorCount, size_t crossbarCapacity) {
if (crossbarCapacity > std::numeric_limits<size_t>::max() / processorCount)
bool hasHighResidentWeightPressure(const ComputeGraph& graph, size_t processorCount, size_t residentWeightCapacity) {
if (residentWeightCapacity > std::numeric_limits<size_t>::max() / processorCount)
return false;
const size_t threshold = processorCount * crossbarCapacity / kHighCrossbarPressureCapacityDivisor;
CrossbarUsage distinctWeights;
const size_t threshold = processorCount * residentWeightCapacity / kHighResidentWeightPressureCapacityDivisor;
ResidentWeightSet distinctWeights;
for (const ComputeGraphNode& node : graph.nodes) {
for (const CrossbarWeight& weight : node.crossbarUsage) {
if (!containsCrossbarWeight(distinctWeights, weight))
for (const ResidentWeight& weight : node.residentWeights) {
if (!containsResidentWeight(distinctWeights, weight))
distinctWeights.push_back(weight);
if (distinctWeights.size() > threshold)
return true;
@@ -159,36 +116,36 @@ bool hasHighCrossbarPressure(const ComputeGraph& graph, size_t processorCount, s
return false;
}
std::vector<CrossbarUsage> planCrossbarReservations(const ComputeGraph& graph,
std::vector<ResidentWeightSet> planResidentWeightReservations(const ComputeGraph& graph,
size_t processorCount,
size_t crossbarCapacity,
const MeshModel& mesh,
bool preferCrossbarReuse) {
size_t residentWeightCapacity,
const TopologyModel& topology,
bool preferWeightReuse) {
std::vector<size_t> weightedTasks;
for (size_t task = 0; task < graph.nodes.size(); ++task)
if (!graph.nodes[task].crossbarUsage.empty())
if (!graph.nodes[task].residentWeights.empty())
weightedTasks.push_back(task);
llvm::sort(weightedTasks, [&](size_t lhs, size_t rhs) {
if (graph.nodes[lhs].crossbarUsage.size() != graph.nodes[rhs].crossbarUsage.size())
return graph.nodes[lhs].crossbarUsage.size() > graph.nodes[rhs].crossbarUsage.size();
if (graph.nodes[lhs].residentWeights.size() != graph.nodes[rhs].residentWeights.size())
return graph.nodes[lhs].residentWeights.size() > graph.nodes[rhs].residentWeights.size();
return graph.nodes[lhs].originalOrder < graph.nodes[rhs].originalOrder;
});
std::vector<CrossbarUsage> reservations(processorCount);
std::vector<ResidentWeightSet> reservations(processorCount);
std::vector<Time> reservedLoad(processorCount, 0);
for (size_t task : weightedTasks) {
size_t bestProcessor = std::numeric_limits<size_t>::max();
using ReservationScore = std::tuple<Time, size_t, size_t, size_t>;
std::optional<ReservationScore> bestScore;
for (size_t processor = 0; processor < processorCount; ++processor) {
size_t crossbarUnion =
getCrossbarUnionSize(reservations[processor], graph.nodes[task].crossbarUsage);
if (crossbarUnion > crossbarCapacity)
size_t residentWeightUnion =
getResidentWeightUnionSize(reservations[processor], graph.nodes[task].residentWeights);
if (residentWeightUnion > residentWeightCapacity)
continue;
size_t addedCrossbars = crossbarUnion - reservations[processor].size();
ReservationScore score {preferCrossbarReuse ? addedCrossbars : reservedLoad[processor],
preferCrossbarReuse ? reservedLoad[processor] : addedCrossbars,
mesh.getCenterDistance(processor),
size_t addedWeights = residentWeightUnion - reservations[processor].size();
ReservationScore score {preferWeightReuse ? addedWeights : reservedLoad[processor],
preferWeightReuse ? reservedLoad[processor] : addedWeights,
topology.getCenterDistance(processor),
processor};
if (!bestScore || score < *bestScore) {
bestProcessor = processor;
@@ -200,13 +157,13 @@ std::vector<CrossbarUsage> planCrossbarReservations(const ComputeGraph& graph,
llvm::formatv("PEFT reservation planner: cannot place task {0} with {1} distinct weights in {2} "
"processors of capacity {3}",
graph.nodes[task].originalOrder,
graph.nodes[task].crossbarUsage.size(),
graph.nodes[task].residentWeights.size(),
processorCount,
crossbarCapacity)
residentWeightCapacity)
.str();
llvm::report_fatal_error(llvm::StringRef(message));
}
insertCrossbarWeights(reservations[bestProcessor], graph.nodes[task].crossbarUsage);
insertResidentWeights(reservations[bestProcessor], graph.nodes[task].residentWeights);
reservedLoad[bestProcessor] = addOrMax(reservedLoad[bestProcessor], graph.nodes[task].cost);
}
return reservations;
@@ -214,8 +171,8 @@ std::vector<CrossbarUsage> planCrossbarReservations(const ComputeGraph& graph,
using LanePublicationSignatures = llvm::SmallVector<llvm::SmallVector<int64_t, 8>, 8>;
FailureOr<LanePublicationSignatures>
buildLanePublicationSignatures(SpatComputeBatch batch, GraphBatchPublicationCache& publicationCache) {
FailureOr<LanePublicationSignatures> buildLanePublicationSignatures(SpatComputeBatch batch,
GraphBatchPublicationCache& publicationCache) {
LanePublicationSignatures signatures(batch.getLaneCount());
for (auto [resultIndex, result] : llvm::enumerate(batch.getResults())) {
auto publicationMap = getGraphBatchPublicationMap(batch, resultIndex, publicationCache);
@@ -231,8 +188,8 @@ buildLanePublicationSignatures(SpatComputeBatch batch, GraphBatchPublicationCach
auto sourceOffsets = blueprint.getFragmentSourceOffsets();
auto fragmentStrides = blueprint.getFragmentStrides();
auto outputType = dyn_cast<RankedTensorType>(blueprint.getOutput().getType());
if (!operandIndices || !sourceSlots || !sourceOffsets || !fragmentStrides
|| !outputType || !outputType.hasStaticShape())
if (!operandIndices || !sourceSlots || !sourceOffsets || !fragmentStrides || !outputType
|| !outputType.hasStaticShape())
return blueprint.emitOpError("PEFT publication compatibility requires complete static fragment metadata"),
failure();
@@ -241,11 +198,9 @@ buildLanePublicationSignatures(SpatComputeBatch batch, GraphBatchPublicationCach
int64_t rank = outputType.getRank();
if (rank <= 0 || fragmentOffsets.size() != fragmentSizes.size()
|| fragmentOffsets.size() != fragmentStrides->size()
|| fragmentOffsets.size() != operandIndices->size() * rank
|| sourceSlots->size() != operandIndices->size()
|| fragmentOffsets.size() != operandIndices->size() * rank || sourceSlots->size() != operandIndices->size()
|| sourceOffsets->size() != operandIndices->size())
return blueprint.emitOpError("PEFT publication compatibility found inconsistent fragment metadata"),
failure();
return blueprint.emitOpError("PEFT publication compatibility found inconsistent fragment metadata"), failure();
llvm::SmallVector<llvm::SmallVector<size_t, 2>, 8> fragmentsByLane(batch.getLaneCount());
for (auto [fragmentIndex, operandIndex] : llvm::enumerate(*operandIndices)) {
@@ -287,19 +242,83 @@ buildLanePublicationSignatures(SpatComputeBatch batch, GraphBatchPublicationCach
} // namespace
Time getPeftTransferTime(Time transferCost, size_t sourceProcessor, size_t targetProcessor, size_t processorCount) {
return MeshModel::infer(processorCount).scaleTransferCost(transferCost, sourceProcessor, targetProcessor);
std::vector<size_t> mapLogicalProcessorsToPhysicalCores(ArrayRef<Cost> logicalTrafficFlits,
const SchedulingTarget& target) {
const size_t processorCount = target.processorCount;
assert(logicalTrafficFlits.size() == processorCount * processorCount
&& "logical traffic matrix must cover every processor pair");
std::vector<size_t> physicalCoreForLogicalProcessor(processorCount);
std::iota(physicalCoreForLogicalProcessor.begin(), physicalCoreForLogicalProcessor.end(), 0);
auto transferCost = [&](size_t sourceLogicalProcessor,
size_t targetLogicalProcessor,
size_t sourcePhysicalCore,
size_t targetPhysicalCore) {
Cost traffic = logicalTrafficFlits[sourceLogicalProcessor * processorCount + targetLogicalProcessor];
return checkedMultiply(traffic, target.getInterProcessorLatencyNs(sourcePhysicalCore, targetPhysicalCore));
};
for (size_t logicalProcessor = 0; logicalProcessor < processorCount; ++logicalProcessor) {
size_t bestPeerLogicalProcessor = logicalProcessor;
Cost bestSaving = 0;
for (size_t peerLogicalProcessor = 0; peerLogicalProcessor < processorCount; ++peerLogicalProcessor) {
if (peerLogicalProcessor == logicalProcessor)
continue;
size_t physicalCore = physicalCoreForLogicalProcessor[logicalProcessor];
size_t peerPhysicalCore = physicalCoreForLogicalProcessor[peerLogicalProcessor];
Cost currentCost = 0;
Cost swappedCost = 0;
for (size_t otherLogicalProcessor = 0; otherLogicalProcessor < processorCount; ++otherLogicalProcessor) {
if (otherLogicalProcessor == logicalProcessor || otherLogicalProcessor == peerLogicalProcessor)
continue;
size_t otherPhysicalCore = physicalCoreForLogicalProcessor[otherLogicalProcessor];
currentCost = checkedAdd(
currentCost, transferCost(logicalProcessor, otherLogicalProcessor, physicalCore, otherPhysicalCore));
currentCost = checkedAdd(
currentCost, transferCost(otherLogicalProcessor, logicalProcessor, otherPhysicalCore, physicalCore));
currentCost = checkedAdd(
currentCost, transferCost(peerLogicalProcessor, otherLogicalProcessor, peerPhysicalCore, otherPhysicalCore));
currentCost = checkedAdd(
currentCost, transferCost(otherLogicalProcessor, peerLogicalProcessor, otherPhysicalCore, peerPhysicalCore));
swappedCost = checkedAdd(
swappedCost, transferCost(logicalProcessor, otherLogicalProcessor, peerPhysicalCore, otherPhysicalCore));
swappedCost = checkedAdd(
swappedCost, transferCost(otherLogicalProcessor, logicalProcessor, otherPhysicalCore, peerPhysicalCore));
swappedCost = checkedAdd(
swappedCost, transferCost(peerLogicalProcessor, otherLogicalProcessor, physicalCore, otherPhysicalCore));
swappedCost = checkedAdd(
swappedCost, transferCost(otherLogicalProcessor, peerLogicalProcessor, otherPhysicalCore, physicalCore));
}
currentCost =
checkedAdd(currentCost, transferCost(logicalProcessor, peerLogicalProcessor, physicalCore, peerPhysicalCore));
currentCost =
checkedAdd(currentCost, transferCost(peerLogicalProcessor, logicalProcessor, peerPhysicalCore, physicalCore));
swappedCost =
checkedAdd(swappedCost, transferCost(logicalProcessor, peerLogicalProcessor, peerPhysicalCore, physicalCore));
swappedCost =
checkedAdd(swappedCost, transferCost(peerLogicalProcessor, logicalProcessor, physicalCore, peerPhysicalCore));
if (currentCost > swappedCost && currentCost - swappedCost > bestSaving) {
bestSaving = currentCost - swappedCost;
bestPeerLogicalProcessor = peerLogicalProcessor;
}
}
std::swap(physicalCoreForLogicalProcessor[logicalProcessor],
physicalCoreForLogicalProcessor[bestPeerLogicalProcessor]);
}
return physicalCoreForLogicalProcessor;
}
MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftScheduleOptions& options) {
const size_t nodeCount = graph.nodes.size();
const size_t processorCount = options.processorCount;
const size_t processorCount = options.target.processorCount;
if (processorCount == 0)
llvm::report_fatal_error("PEFT scheduler: processor count must be positive");
MeshModel mesh = MeshModel::infer(processorCount);
const bool preferCrossbarReuse = hasHighCrossbarPressure(graph, processorCount, options.crossbarCapacity);
std::vector<CrossbarUsage> capacityReservations =
planCrossbarReservations(graph, processorCount, options.crossbarCapacity, mesh, preferCrossbarReuse);
TopologyModel topology {options.target};
const bool preferWeightReuse =
hasHighResidentWeightPressure(graph, processorCount, options.target.residentWeightCapacity);
std::vector<ResidentWeightSet> capacityReservations = planResidentWeightReservations(
graph, processorCount, options.target.residentWeightCapacity, topology, preferWeightReuse);
verifyOctTableSize(nodeCount, processorCount);
std::vector<std::vector<size_t>> reverseLevels = buildReverseLevels(graph);
@@ -317,7 +336,7 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
std::vector<Time> maxVals(processorCount, 0);
for (const auto& [succ, comm] : graph.successors[task]) {
Time valDifferentCpu = addOrMax(minOctPlusComp[succ], comm);
Time valDifferentCpu = addOrMax(minOctPlusComp[succ], getAverageTransferTime(comm, options.target));
for (size_t processor = 0; processor < processorCount; ++processor) {
Time valSameCpu = addOrMax(oct[succ * processorCount + processor], getComputeCost(succ, processor));
Time bestSucc = std::min(valSameCpu, valDifferentCpu);
@@ -378,7 +397,7 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
}
std::vector<char> scheduled(nodeCount, false);
std::vector<CrossbarUsage> processorCrossbars(processorCount);
std::vector<ResidentWeightSet> processorResidentWeights(processorCount);
std::vector<ScheduledTask> schedules(nodeCount);
std::vector<std::vector<size_t>> tasksByProcessor(processorCount);
@@ -394,23 +413,25 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
Time bestEft = 0;
Time bestOeft = std::numeric_limits<Time>::max();
unsigned int bestOverlapCount = 0;
size_t bestTaskCount = std::numeric_limits<size_t>::max();
size_t bestCenterDistance = std::numeric_limits<size_t>::max();
size_t smallestCrossbarUnion = std::numeric_limits<size_t>::max();
bool crossbarRejected = false;
size_t smallestResidentWeightUnion = std::numeric_limits<size_t>::max();
bool residentWeightRejected = false;
for (size_t processor = 0; processor < processorCount; ++processor) {
unsigned int overlapCount = countCrossbarOverlap(processorCrossbars[processor], graph.nodes[task].crossbarUsage);
size_t crossbarUnion =
getCrossbarUnionSize(capacityReservations[processor], graph.nodes[task].crossbarUsage);
smallestCrossbarUnion = std::min(smallestCrossbarUnion, crossbarUnion);
if (!graph.nodes[task].crossbarUsage.empty() && crossbarUnion > options.crossbarCapacity) {
crossbarRejected = true;
unsigned int overlapCount =
countResidentWeightOverlap(processorResidentWeights[processor], graph.nodes[task].residentWeights);
size_t residentWeightUnion =
getResidentWeightUnionSize(capacityReservations[processor], graph.nodes[task].residentWeights);
smallestResidentWeightUnion = std::min(smallestResidentWeightUnion, residentWeightUnion);
if (!graph.nodes[task].residentWeights.empty() && residentWeightUnion > options.target.residentWeightCapacity) {
residentWeightRejected = true;
continue;
}
Time dataReady = 0;
for (const auto& [pred, comm] : graph.predecessors[task]) {
const ScheduledTask& predSchedule = schedules[pred];
Time commPenalty = getPeftTransferTime(comm, predSchedule.processor, processor, processorCount);
Time commPenalty = getPeftTransferTime(comm, predSchedule.processor, processor, options.target);
dataReady = std::max(dataReady, addOrMax(predSchedule.endTime, commPenalty));
}
@@ -437,9 +458,10 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
Time eft = addOrMax(est, computeCost);
Time oeft = addOrMax(eft, oct[task * processorCount + processor]);
size_t centerDistance = mesh.getCenterDistance(processor);
bool betterCrossbarChoice =
preferCrossbarReuse ? overlapCount > bestOverlapCount : overlapCount < bestOverlapCount;
size_t centerDistance = topology.getCenterDistance(processor);
size_t taskCount = tasksByProcessor[processor].size();
bool betterResidentWeightChoice =
preferWeightReuse ? overlapCount > bestOverlapCount : overlapCount < bestOverlapCount;
if (oeft < bestOeft || (oeft == bestOeft && eft < bestEft)
|| (oeft == bestOeft && eft == bestEft && est < bestEst)) {
@@ -448,40 +470,52 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
bestEft = eft;
bestOeft = oeft;
bestOverlapCount = overlapCount;
bestTaskCount = taskCount;
bestCenterDistance = centerDistance;
}
else if (oeft == bestOeft && eft == bestEft && est == bestEst
else if (oeft == bestOeft && eft == bestEft && est == bestEst && taskCount < bestTaskCount) {
bestProcessor = processor;
bestEst = est;
bestEft = eft;
bestOeft = oeft;
bestOverlapCount = overlapCount;
bestTaskCount = taskCount;
bestCenterDistance = centerDistance;
}
else if (oeft == bestOeft && eft == bestEft && est == bestEst && taskCount == bestTaskCount
&& centerDistance < bestCenterDistance) {
bestProcessor = processor;
bestEst = est;
bestEft = eft;
bestOeft = oeft;
bestOverlapCount = overlapCount;
bestTaskCount = taskCount;
bestCenterDistance = centerDistance;
}
else if (oeft == bestOeft && eft == bestEft && est == bestEst
&& centerDistance == bestCenterDistance && betterCrossbarChoice) {
else if (oeft == bestOeft && eft == bestEft && est == bestEst && taskCount == bestTaskCount
&& centerDistance == bestCenterDistance && betterResidentWeightChoice) {
bestProcessor = processor;
bestEst = est;
bestEft = eft;
bestOeft = oeft;
bestOverlapCount = overlapCount;
bestTaskCount = taskCount;
bestCenterDistance = centerDistance;
}
}
if (bestProcessor == std::numeric_limits<size_t>::max()) {
if (crossbarRejected) {
if (residentWeightRejected) {
const ComputeInstance& instance = graph.nodes[task].instance;
std::string message =
llvm::formatv("PEFT scheduler: no valid processor for task {0} (lanes {1}..{2}, {3} distinct weights); "
"smallest processor union is {4}, exceeding crossbar capacity {5}",
"smallest processor union is {4}, exceeding resident-weight capacity {5}",
graph.nodes[task].originalOrder,
instance.laneStart,
instance.laneStart + instance.laneCount,
graph.nodes[task].crossbarUsage.size(),
smallestCrossbarUnion,
options.crossbarCapacity)
graph.nodes[task].residentWeights.size(),
smallestResidentWeightUnion,
options.target.residentWeightCapacity)
.str();
llvm::report_fatal_error(llvm::StringRef(message));
}
@@ -495,8 +529,8 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
schedules[task] = {bestProcessor, bestEst, bestEft};
scheduled[task] = true;
++scheduledCount;
insertCrossbarWeights(capacityReservations[bestProcessor], graph.nodes[task].crossbarUsage);
insertCrossbarWeights(processorCrossbars[bestProcessor], graph.nodes[task].crossbarUsage);
insertResidentWeights(capacityReservations[bestProcessor], graph.nodes[task].residentWeights);
insertResidentWeights(processorResidentWeights[bestProcessor], graph.nodes[task].residentWeights);
// 3. CRITICAL FIX: Topological Append
// Because the readyQueue pops in strict topological order, simply pushing to the
@@ -576,6 +610,7 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
// 6. Populate Final Result
MergeScheduleResult result;
result.processorCount = processorCount;
result.dominanceOrderCompute.reserve(nodeCount);
for (size_t task : scheduledOrder)
@@ -4,18 +4,32 @@
#include "ComputeGraph.hpp"
#include "MergeSchedule.hpp"
#include "SchedulingTarget.hpp"
namespace onnx_mlir {
namespace spatial {
struct PeftScheduleOptions {
size_t processorCount = 0;
unsigned long crossbarCapacity = 0;
SchedulingTarget target;
mlir::MLIRContext* context = nullptr;
};
Time getPeftTransferTime(Time transferCost, size_t sourceProcessor, size_t targetProcessor, size_t processorCount);
inline Time getPeftTransferTime(const TransferCost& transferCost,
size_t sourceProcessor,
size_t targetProcessor,
const SchedulingTarget& target) {
if (sourceProcessor == targetProcessor)
return 0;
return checkedAdd(transferCost.fixed,
checkedMultiply(transferCost.networkFlits, target.averageInterProcessorLatencyNs));
}
// PEFT assigns logical processors. Physical core IDs are chosen only after
// materialization exposes the exact transfer traffic.
MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftScheduleOptions& options);
std::vector<size_t> mapLogicalProcessorsToPhysicalCores(llvm::ArrayRef<Cost> logicalTrafficFlits,
const SchedulingTarget& target);
} // namespace spatial
} // namespace onnx_mlir
@@ -0,0 +1,54 @@
#pragma once
#include <cassert>
#include <cstddef>
#include <vector>
#include "Utils.hpp"
namespace onnx_mlir {
namespace spatial {
struct SchedulingTarget {
size_t processorCount = 0;
size_t residentWeightCapacity = 0;
std::vector<Cost> interProcessorLatencyNs;
Cost averageInterProcessorLatencyNs = 0;
Cost computeBitwidth = 8;
Cost processorPeriodNs = 1;
Cost localMemoryWidthBytes = 64;
Cost localMemoryReadLatencyCycles = 1;
Cost localMemoryWriteLatencyCycles = 1;
Cost transferWidthBytes = 8;
Cost vectorWidth = 16;
Cost vectorLatencyCycles = 4;
Cost matrixRows = 128;
Cost matrixColumns = 128;
Cost matrixPeriodNs = 1;
Cost matrixInputResolutionBits = 1;
Cost matrixInputLatencyCycles = 1;
Cost matrixInputParallelism = 128;
Cost matrixReadLatencyNs = 30;
Cost matrixSampleLatencyCycles = 1;
Cost matrixOutputLatencyCycles = 10;
Cost matrixOutputParallelism = 2;
Cost matrixShiftLatencyCycles = 1;
Cost matrixBufferLatencyCycles = 1;
Cost matrixInputBufferLatencyCycles = 0;
bool matrixPipeline = true;
Cost getInterProcessorLatencyNs(size_t source,
size_t destination) const {
assert(source < processorCount && destination < processorCount
&& "processor index out of range");
assert(interProcessorLatencyNs.size()
== processorCount * processorCount
&& "incomplete inter-processor latency matrix");
return interProcessorLatencyNs[source * processorCount + destination];
}
};
} // namespace spatial
} // namespace onnx_mlir
@@ -36,10 +36,14 @@ static bool hasOnlyStructuralAttrs(ComputeOp op) {
});
}
static bool hasCapacityFor(Operation* producer, Operation* consumer) {
CrossbarUsage producerWeights = collectDistinctCrossbarWeights(producer);
CrossbarUsage consumerWeights = collectDistinctCrossbarWeights(consumer);
return getCrossbarUnionSize(producerWeights, consumerWeights) <= static_cast<size_t>(crossbarCountInCore.getValue());
static bool hasCapacityFor(Operation* producer, Operation* consumer,
size_t residentWeightCapacity) {
ResidentWeightSet producerWeights =
collectDistinctResidentWeights(producer);
ResidentWeightSet consumerWeights =
collectDistinctResidentWeights(consumer);
return getResidentWeightUnionSize(producerWeights, consumerWeights)
<= residentWeightCapacity;
}
template <typename ConsumerOp>
@@ -156,8 +160,11 @@ static void mapExternalArguments(OldOp oldOp, NewOp newOp, IRMapping& mapper, bo
}
struct MergeTrivialScalarComputes : OpRewritePattern<SpatGraphCompute> {
MergeTrivialScalarComputes(MLIRContext *context, TrivialGraphMergeStats *stats)
: OpRewritePattern(context), stats(stats) {}
MergeTrivialScalarComputes(MLIRContext *context,
TrivialGraphMergeStats *stats,
size_t residentWeightCapacity)
: OpRewritePattern(context), stats(stats),
residentWeightCapacity(residentWeightCapacity) {}
LogicalResult matchAndRewrite(SpatGraphCompute consumer, PatternRewriter& rewriter) const override {
SpatGraphCompute producer;
@@ -166,7 +173,8 @@ struct MergeTrivialScalarComputes : OpRewritePattern<SpatGraphCompute> {
if (candidate && candidate->getBlock() == consumer->getBlock() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isUniqueGraphComputePredecessor(candidate, consumer)
&& isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasNoNestedArgumentCaptures(candidate)
&& hasCapacityFor(candidate, consumer, residentWeightCapacity)
&& hasNoNestedArgumentCaptures(candidate)
&& hasNoNestedArgumentCaptures(consumer)) {
producer = candidate;
break;
@@ -201,6 +209,7 @@ struct MergeTrivialScalarComputes : OpRewritePattern<SpatGraphCompute> {
private:
TrivialGraphMergeStats *stats;
size_t residentWeightCapacity;
};
static bool isLaneIndex(Value value, Value lane, int64_t laneCount) {
@@ -397,8 +406,11 @@ static bool hasDirectLaneConsumers(SpatGraphComputeBatch producer, SpatGraphComp
}
struct MergeTrivialBatchComputes : OpRewritePattern<SpatGraphComputeBatch> {
MergeTrivialBatchComputes(MLIRContext *context, TrivialGraphMergeStats *stats)
: OpRewritePattern(context), stats(stats) {}
MergeTrivialBatchComputes(MLIRContext *context,
TrivialGraphMergeStats *stats,
size_t residentWeightCapacity)
: OpRewritePattern(context), stats(stats),
residentWeightCapacity(residentWeightCapacity) {}
LogicalResult matchAndRewrite(SpatGraphComputeBatch consumer, PatternRewriter& rewriter) const override {
SpatGraphComputeBatch producer;
@@ -409,7 +421,8 @@ struct MergeTrivialBatchComputes : OpRewritePattern<SpatGraphComputeBatch> {
&& candidate.getLaneCount() == consumer.getLaneCount() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isUniqueGraphComputePredecessor(candidate, consumer)
&& isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasDirectLaneConsumers(candidate, consumer)
&& hasCapacityFor(candidate, consumer, residentWeightCapacity)
&& hasDirectLaneConsumers(candidate, consumer)
&& succeeded(fragments = collectPublishedFragments(candidate))) {
producer = candidate;
break;
@@ -469,11 +482,16 @@ struct MergeTrivialBatchComputes : OpRewritePattern<SpatGraphComputeBatch> {
private:
TrivialGraphMergeStats *stats;
size_t residentWeightCapacity;
};
struct TrivialGraphComputeMergePass final : PassWrapper<TrivialGraphComputeMergePass, OperationPass<ModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TrivialGraphComputeMergePass)
TrivialGraphComputeMergePass() = default;
explicit TrivialGraphComputeMergePass(size_t residentWeightCapacity)
: residentWeightCapacity(residentWeightCapacity) {}
StringRef getArgument() const override { return "pim-trivial-graph-compute-merge"; }
StringRef getDescription() const override {
return "Inline linear exclusive graph compute chains while preserving fan-in boundaries.";
@@ -481,10 +499,17 @@ struct TrivialGraphComputeMergePass final : PassWrapper<TrivialGraphComputeMerge
void runOnOperation() override {
ModuleOp module = getOperation();
if (residentWeightCapacity == 0) {
module.emitError(
"TrivialGraphComputeMerge requires an explicit valid resident-weight capacity");
signalPassFailure();
return;
}
TrivialGraphMergeStats stats;
std::tie(stats.scalarBefore, stats.batchBefore) = countGraphComputes(module);
RewritePatternSet patterns(&getContext());
patterns.add<MergeTrivialScalarComputes, MergeTrivialBatchComputes>(&getContext(), &stats);
patterns.add<MergeTrivialScalarComputes, MergeTrivialBatchComputes>(
&getContext(), &stats, residentWeightCapacity);
if (failed(applyPatternsGreedily(module, std::move(patterns)))) {
signalPassFailure();
return;
@@ -510,6 +535,9 @@ struct TrivialGraphComputeMergePass final : PassWrapper<TrivialGraphComputeMerge
signalPassFailure();
}
}
private:
size_t residentWeightCapacity = 0;
};
} // namespace
@@ -519,4 +547,10 @@ std::unique_ptr<Pass> createTrivialGraphComputeMergePass() {
return std::make_unique<spatial::TrivialGraphComputeMergePass>();
}
std::unique_ptr<Pass> createTrivialGraphComputeMergePass(
size_t residentWeightCapacity) {
return std::make_unique<spatial::TrivialGraphComputeMergePass>(
residentWeightCapacity);
}
} // namespace onnx_mlir
+7
View File
@@ -2,10 +2,14 @@
#include "mlir/Pass/Pass.h"
#include <cstddef>
#include <memory>
#include <string>
namespace onnx_mlir {
namespace spatial {
struct SchedulingTarget;
}
std::unique_ptr<mlir::Pass> createONNXToSpatialPass();
std::unique_ptr<mlir::Pass> createSpatialLayoutPlanningPass();
@@ -16,8 +20,11 @@ std::unique_ptr<mlir::Pass> createSpatialToPimPass();
std::unique_ptr<mlir::Pass> createPimBufferizationPass();
std::unique_ptr<mlir::Pass> createMergeComputeNodesPass();
std::unique_ptr<mlir::Pass> createMergeComputeNodesPass(const spatial::SchedulingTarget& target);
std::unique_ptr<mlir::Pass> createTrivialGraphComputeMergePass();
std::unique_ptr<mlir::Pass> createTrivialGraphComputeMergePass(
size_t residentWeightCapacity);
std::unique_ptr<mlir::Pass> createPimHostConstantFoldingPass();
+2 -2
View File
@@ -76,8 +76,8 @@ void PimAccelerator::registerPasses(int optLevel) const {
registerPass(createLowerSpatialPlansPass);
registerPass(createSpatialToPimPass);
registerPass(createPimBufferizationPass);
registerPass(createTrivialGraphComputeMergePass);
registerPass(createMergeComputeNodesPass);
mlir::registerPass([] { return createTrivialGraphComputeMergePass(); });
mlir::registerPass([] { return createMergeComputeNodesPass(); });
registerPass(createPimHostConstantFoldingPass);
registerPass(createPimLocalMemoryPlanningPass);
registerPass(createPimVerificationPass);
+7
View File
@@ -29,3 +29,10 @@ add_pim_unittest(PimMemoryLivenessPlannerTest
LINK_LIBS PRIVATE
OMPimCompilerUtils
)
add_pim_unittest(SpatialSchedulingTargetTest
SpatialSchedulingTargetTest.cpp
LINK_LIBS PRIVATE
OMPimCompilerUtils
)
+58
View File
@@ -0,0 +1,58 @@
#include <cassert>
#include <cstdlib>
#include <vector>
#include "src/Accelerators/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.hpp"
using namespace onnx_mlir::spatial;
int main() {
TransferCost transfer {.fixed = 50, .networkFlits = 4};
SchedulingTarget fast;
fast.processorCount = 2;
fast.interProcessorLatencyNs = {0, 3, 3, 0};
fast.averageInterProcessorLatencyNs = 3;
SchedulingTarget slow = fast;
slow.interProcessorLatencyNs = {0, 10, 10, 0};
slow.averageInterProcessorLatencyNs = 10;
assert(getPeftTransferTime(transfer, 0, 0, fast) == 0);
assert(getPeftTransferTime(transfer, 0, 1, fast) == 62);
assert(getPeftTransferTime(transfer, 0, 1, slow) == 90);
assert(fast.getInterProcessorLatencyNs(0, 1) == 3);
assert(slow.getInterProcessorLatencyNs(0, 1) == 10);
SchedulingTarget line;
line.processorCount = 3;
line.interProcessorLatencyNs = {
0,
1,
10,
1,
0,
1,
10,
1,
0,
};
std::vector<Cost> logicalTrafficFlits(9, 0);
logicalTrafficFlits[2] = 100;
assert(mapLogicalProcessorsToPhysicalCores(logicalTrafficFlits, line) == std::vector<size_t>({1, 0, 2}));
SchedulingTarget alreadyPlaced = line;
alreadyPlaced.interProcessorLatencyNs = {
0,
10,
1,
10,
0,
1,
1,
1,
0,
};
assert(mapLogicalProcessorsToPhysicalCores(logicalTrafficFlits, alreadyPlaced) == std::vector<size_t>({0, 1, 2}));
return EXIT_SUCCESS;
}
+1
View File
@@ -13,3 +13,4 @@ networks/**/real_image_val
networks/**/*.png
networks/**/*.jpg
networks/**/*.csv
!networks/pimcomp_models/results.csv
+7 -7
View File
@@ -8,7 +8,7 @@ model it can:
3. compile PIM artifacts with Raptor;
4. run the reference implementation and functional PIM simulator;
5. compare their outputs;
6. run `pimsim-nn` to report latency and power.
6. run `pimsim-nn` to report latency, power, and energy.
Run the script from the repository root with the repository Python environment.
@@ -65,7 +65,7 @@ The script discovers them recursively.
The PIMCOMP paper-model suite has a one-command Arch-A comparison:
```bash
.venv/bin/python validation/tools/run_pimcomp_paper_latency.py
.venv/bin/python validation/tools/pimcomp/run_pimcomp_paper_latency.py
```
The runner verifies PIMCOMP's population-200, 1000-iteration GA settings,
@@ -134,7 +134,7 @@ count with `-j` or `--jobs`:
| `--simulator-dir PATH` | Functional `pim-simulator` crate directory. Defaults to the in-tree simulator. |
| `--non-functional-simulator-build-dir PATH` | `pimsim-nn` build directory. Defaults to the in-tree build. |
| `--pimcomp-config {arch-a,arch-b,arch-c}` | Non-functional hardware/timing profile. Defaults to `arch-a`. |
| `--skip-non-functional-simulation` | Skip `pimsim-nn` latency and power measurement. |
| `--skip-non-functional-simulation` | Skip `pimsim-nn` latency, power, and energy measurement. |
| `--threshold FLOAT` | Absolute output-comparison tolerance. Defaults to `1e-3`. |
| `--relative-threshold FLOAT` | Relative output-comparison tolerance. Defaults to `1e-5`. |
| `--seed INT` | Seed for generated inputs. Defaults to `0`. |
@@ -165,17 +165,17 @@ prints the incompatible values; functional validation still runs.
The checked-in profiles are under
`validation/pimsim_configs/pimcomp/<profile>/latency_config.json`.
Use `--skip-non-functional-simulation` when latency and power are not required.
Use `--skip-non-functional-simulation` when latency, power, and energy are not required.
The summary reports non-functional results as measured, failed, unsupported, or
skipped.
Overall PASS/FAIL is determined by compilation and functional output
comparison. A non-functional simulation failure remains visible as `ERROR` in
the latency and power columns but does not change a functional PASS.
the latency, power, and energy columns but does not change a functional PASS.
`pimsim-nn` does not currently implement the `vsoftmax` instruction. When its
explicit unsupported-op diagnostic is encountered, Softmax validations retain
their functional PASS and show `UNSUPPORTED` in both non-functional columns.
their functional PASS and show `UNSUPPORTED` in the non-functional columns.
Other `pimsim-nn` failures remain `ERROR`.
## Generated artifacts
@@ -186,7 +186,7 @@ Artifacts are written beside each model:
|---|---|
| `inputs/` | Generated input CSV files. |
| `outputs/` | ONNX-MLIR reference output CSV files. |
| `raptor/` | Exported MLIR, dialect snapshots, reports, and final `pim/` artifacts. |
| `raptor/` | Exported MLIR, dialect snapshots, reports, final FP32 `pim/` artifacts, and the int8-equivalent `pimsim_nn/` latency view. |
| `runner/` | Generated reference runner source, build tree, and shared library. |
| `simulation/` | Functional simulator outputs used for numerical comparison. |
| `pimcomp/` | PIMCOMP graph, instruction, simulator, and comparison-report artifacts. |
+44 -21
View File
@@ -4,7 +4,8 @@ This directory contains the four networks evaluated in
[PIMCOMP: An End-to-End DNN Compiler for Processing-In-Memory Accelerators](https://arxiv.org/pdf/2411.09159):
VGG-8, ResNet-18, ResNet-34, and GoogLeNet.
See [RESULTS.md](RESULTS.md) for the current latency-only result status.
See the runner-generated [results.csv](results.csv) for the current latency
and energy results.
## Models and provenance
@@ -15,11 +16,11 @@ See [RESULTS.md](RESULTS.md) for the current latency-only result status.
| `googlenet/` | GoogLeNet | `1x3x224x224` | Unmodified [ONNX Model Zoo `googlenet-12`](https://huggingface.co/onnxmodelzoo/googlenet-12). |
| `vgg8/` | VGG-8 reconstruction | `1x1x28x28` | Deterministic compiler workload with six convolution and two fully connected layers. |
`googlenet/googlenet-12-no-softmax.onnx` is a derived latency model that
exposes the original model's final FC logits (`loss3/classifier_1`) as its
output. This matches PIMCOMP's instruction stream, which records but does not
schedule the terminal `OP_SOFTMAX`. Keep `googlenet-12.onnx` for full-model
functional validation.
`googlenet/googlenet-12-latency.onnx` is the explicit common latency model.
It exposes the original model's final FC logits (`loss3/classifier_1`) and
removes its two LRN nodes and terminal Softmax so the comparison covers only
operations scheduled by PIMCOMP. Keep `googlenet-12.onnx` as the unmodified
source model.
The PIMCOMP authors did not publish the ONNX checkpoints used by the paper.
Running PIMCOMP's frontend on the three Model Zoo files above produces JSON
@@ -38,7 +39,7 @@ Current SHA-256 checksums:
788088b908e233d924c7c26b997e89ee861290c7bc56783a306e8201d79aac8f resnet18/resnet18-v1-7.onnx
c3231061d081bdd47884137b02134f85142752a39e87263c529cd14ed242b096 resnet34/resnet34-v1-7.onnx
c99c507058eaf41de8723408fdda7db8325cb57f0a89f2ee07a716d6e963e14e googlenet/googlenet-12.onnx
a35bad96441efbee28699cb61d1656cca7f7281f14040cf01699c3d0cfd8b202 googlenet/googlenet-12-no-softmax.onnx
a26f9e33901c573e60c34a3f0abbb4744fff83e4e0f21b18fc66e20395e72982 googlenet/googlenet-12-latency.onnx
396cdea21e5e7d02c3f26f14d22ef20975171702493f5c5e79b8e0d896e541ef vgg8/vgg8-mnist-reconstructed.onnx
```
@@ -145,17 +146,17 @@ uses the fixed seed `1`, so repeated serial and parallel runs are reproducible.
## Compare Raptor and PIMCOMP
The comparison driver uses one random input and one native ONNX-MLIR reference,
compiles both instruction streams, runs both through `pimsim-nn`, validates
Raptor through the Rust simulator, and writes Markdown and JSON reports.
PIMCOMP Rust validation also runs when its optional exporter is available.
compiles both instruction streams, runs both through `pimsim-nn`, runs
functional validation through `pim-simulator`, and writes Markdown and JSON
reports.
To reproduce the complete Arch-A latency experiment, use the model-by-model
runner. It verifies the paper GA settings, builds Raptor and the existing
`third_party/PIMCOMP-NN/build` tree, then runs the `element`/batch-1 comparison
for one model at a time:
for one model at a time and regenerates `results.csv` from the JSON reports:
```bash
.venv/bin/python validation/tools/run_pimcomp_paper_latency.py
.venv/bin/python validation/tools/pimcomp/run_pimcomp_paper_latency.py
```
Each model directory reuses regular validation's ignored `inputs/`, `outputs/`,
@@ -176,7 +177,7 @@ Arch-A low-latency example:
```bash
RAPTOR_ROOT=$PWD
"$RAPTOR_ROOT/.venv/bin/python" "$RAPTOR_ROOT/validation/tools/compare_raptor_pimcomp.py" \
"$RAPTOR_ROOT/.venv/bin/python" "$RAPTOR_ROOT/validation/tools/pimcomp/compare_raptor_pimcomp.py" \
--model "$RAPTOR_ROOT/validation/networks/pimcomp_models/resnet34/resnet34-v1-7.onnx" \
--out-dir "$RAPTOR_ROOT/validation/networks/pimcomp_models/resnet34" \
--pimcomp-config "$RAPTOR_ROOT/validation/pimsim_configs/pimcomp/arch-a/latency_config.json" \
@@ -202,12 +203,36 @@ The comparison runner enables `--fail-on-error`, so a failed compiler,
simulation, or semantic validation makes the command fail while preserving the
generated report.
### Numeric precision and simulator artifacts
The functional and non-functional simulators intentionally consume different
artifacts:
- Raptor and PIMCOMP are validated against the native ONNX-MLIR reference as
FP32 programs in the Rust simulator. Raptor's emitted program is already
FP32. The PIMCOMP-to-Rust export expands its element-addressed storage and
byte-sized transfers to FP32, emits `setbw 32, 32`, and keeps vector
`imm_len` fields as element counts.
- PIMCOMP's original `SimulationInfo.gz` is copied unchanged for `pimsim-nn`.
PIMCOMP hardcodes `setbw 8, 8` and one byte per element without performing
numerical quantization; this artifact is used only for latency estimation.
- Raptor's original FP32 artifact remains unchanged for functional validation.
A separate `raptor/pimsim_nn/` view uses `setbw 8, 8` and scales its
byte-addressed storage and transfer sizes from four bytes to one byte per
element. Vector `imm_len` fields remain element counts. Like PIMCOMP's
artifact, this view is not numerically valid and is used only for a fair
non-functional comparison.
The ISA defines vector lengths in elements, while `ld`, `st`, `lldi`, `lmv`,
`send`, `recv`, addresses, and non-vector offsets are byte-based. Do not use
either latency-only artifact for semantic validation.
Current Raptor status:
- VGG-8, ResNet-18, fixed-batch ResNet-34, and GoogLeNet compile on Arch-A.
- Use `googlenet-12-no-softmax.onnx` for the paper-matched latency comparison.
The original model's final `vsoftmax` is supported by Raptor's functional
simulator but not by `pimsim-nn`; PIMCOMP does not schedule that operation.
- Use `googlenet-12-latency.onnx` for the paper-matched latency comparison.
It removes the two LRN nodes and terminal softmax that PIMCOMP does not
schedule.
- Raptor currently accepts one square `--crossbar-size`; Arch-C's rectangular
`512x1024` arrays can therefore be compiled by PIMCOMP but not compared
exactly with Raptor.
@@ -227,10 +252,8 @@ rsync -azL validation/networks/pimcomp_models/ \
"monolith:$REMOTE_REPO/validation/networks/pimcomp_models/"
rsync -az validation/pimsim_configs/pimcomp/ \
"monolith:$REMOTE_REPO/validation/pimsim_configs/pimcomp/"
rsync -az validation/tools/compare_raptor_pimcomp.py \
"monolith:$REMOTE_REPO/validation/tools/compare_raptor_pimcomp.py"
rsync -az validation/tools/run_pimcomp_paper_latency.py \
"monolith:$REMOTE_REPO/validation/tools/run_pimcomp_paper_latency.py"
rsync -az validation/tools/pimcomp/ \
"monolith:$REMOTE_REPO/validation/tools/pimcomp/"
rsync -az --exclude=.git --exclude=build --exclude=output \
third_party/PIMCOMP-NN/ \
"monolith:$REMOTE_REPO/third_party/PIMCOMP-NN/"
@@ -247,7 +270,7 @@ python3 -m venv .venv
.venv/bin/python -m pip install numpy onnx onnxruntime onnxsim colorama
# Run every latency comparison serially.
.venv/bin/python validation/tools/run_pimcomp_paper_latency.py
.venv/bin/python validation/tools/pimcomp/run_pimcomp_paper_latency.py
```
Copy reports back without transferring large compiler artifacts:
@@ -1,32 +0,0 @@
# Raptor vs PIMCOMP latency results
## GoogLeNet, Arch-A, low latency
Measured with `googlenet-12-no-softmax.onnx`, batch 1, Raptor's best current
schedule, and PIMCOMP's GA/element artifacts. Both instruction streams were
simulated by the same `pimsim-nn` build using the complete Arch-A timing and
precision configuration.
| Compiler | Latency (ms) | Instructions | Sends | Receives | MVMUL |
| --- | ---: | ---: | ---: | ---: | ---: |
| Raptor | 1132.458315 | 77,717,248 | 29,115 | 29,115 | 157,158 |
| PIMCOMP | 41.790450 | 3,398,070 | 110,334 | 110,334 | 113,639 |
PIMCOMP is 27.10x faster in this latency simulation.
Semantic validation did not pass the comparison driver's strict default
tolerance: the maximum logit differences from the native ONNX reference were
`0.01995039` for Raptor and `7.768404` for PIMCOMP. Treat these as performance
results, not as a correctness-equivalent comparison.
No throughput experiment was run.
## Reproduce
```bash
.venv/bin/python validation/tools/run_pimcomp_paper_latency.py \
--models googlenet
```
See [README.md](README.md) for model provenance, limitations, and monolith
instructions.
@@ -0,0 +1,5 @@
model,raptor_latency_ms,pimcomp_latency_ms,raptor_energy_pj,pimcomp_energy_pj,faster_compiler,speedup
vgg8,2.451664,7.985074,647608825.120001,1597904071.120000,raptor,3.26
resnet18,113.483357,58.853733,23204566467.119949,13983168748.119974,pimcomp,1.93
resnet34,120.655292,91.607980,27614808203.679939,22722922369.680016,pimcomp,1.32
googlenet,22.936609,62.923463,7798109358.239990,14547526780.240000,raptor,2.74
1 model raptor_latency_ms pimcomp_latency_ms raptor_energy_pj pimcomp_energy_pj faster_compiler speedup
2 vgg8 2.451664 7.985074 647608825.120001 1597904071.120000 raptor 3.26
3 resnet18 113.483357 58.853733 23204566467.119949 13983168748.119974 pimcomp 1.93
4 resnet34 120.655292 91.607980 27614808203.679939 22722922369.680016 pimcomp 1.32
5 googlenet 22.936609 62.923463 7798109358.239990 14547526780.240000 raptor 2.74
+73
View File
@@ -0,0 +1,73 @@
import json
import re
import shutil
from pathlib import Path
_METRIC_PATTERNS = {
"output_count": r"output count:\s+([0-9]+)\s+samples",
"throughput": r"throughput:\s+([0-9.eE+-]+)\s+samples/s",
"average_latency_ms": r"average latency:\s+([0-9.eE+-]+)\s+ms",
"latency_ms": r"latency:\s+([0-9.eE+-]+)\s+ms",
"average_power_mw": r"average power:\s+([0-9.eE+-]+)\s+mW",
"average_energy_pj": r"average energy:\s+([0-9.eE+-]+)\s+pJ(?:/it)?",
}
def parse_pimsim_nn_metrics(output):
metrics = {"raw_output": output}
for name, pattern in _METRIC_PATTERNS.items():
match = re.search(pattern, output)
if match:
value = match.group(1)
metrics[name] = int(value) if name == "output_count" else float(value)
return metrics
def export_raptor_latency_artifact(pim_dir, output_dir):
pim_dir = Path(pim_dir)
output_dir = Path(output_dir)
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.mkdir(parents=True)
def int8_bytes(value, field):
if value % 4:
raise ValueError(f"Raptor {field}={value} is not aligned to its fp32 element width")
return value // 4
with open(pim_dir / "config.json", encoding="utf-8") as f:
config = json.load(f)
for field in ("inputs_addresses", "outputs_addresses"):
if field in config:
config[field] = [int8_bytes(value, field) for value in config[field]]
with open(output_dir / "config.json", "w", encoding="utf-8") as f:
json.dump(config, f, separators=(",", ":"))
f.write("\n")
byte_size_fields = {
"ld": "size",
"st": "size",
"lldi": "len",
"lmv": "len",
"send": "size",
"recv": "size",
}
for source in sorted(pim_dir.glob("core_*.json"), key=lambda path: int(path.stem.split("_")[1])):
with open(source, encoding="utf-8") as f:
instructions = json.load(f)
for instruction in instructions:
op = instruction["op"]
if op == "setbw":
instruction["ibiw"] = 8
instruction["obiw"] = 8
elif op == "sldi":
instruction["imm"] = int8_bytes(instruction["imm"], "address")
if field := byte_size_fields.get(op):
instruction[field] = int8_bytes(instruction[field], f"{op} {field}")
if offset := instruction.get("offset"):
offset["offset_value"] = int8_bytes(offset["offset_value"], f"{op} offset")
with open(output_dir / source.name, "w", encoding="utf-8") as f:
json.dump(instructions, f, separators=(",", ":"))
f.write("\n")
return output_dir
+17 -12
View File
@@ -1,6 +1,5 @@
import json
import os
import re
import shutil
import subprocess
import sys
@@ -11,6 +10,7 @@ from colorama import Style, Fore
from .gen_network_runner import gen_network_runner
from .onnx_utils import gen_random_inputs, save_inputs_to_files, onnx_io, write_inputs_to_memory_bin, _ONNX_TO_NP
from .raptor import compile_with_raptor
from .pimsim_nn import export_raptor_latency_artifact, parse_pimsim_nn_metrics
from .subprocess_utils import run_command_with_reporter
STAGE_TITLES = (
@@ -61,9 +61,7 @@ PIMSIM_FAILED = "ERROR"
PIMSIM_UNSUPPORTED = "UNSUPPORTED"
PIMSIM_SKIPPED = "SKIP"
PIMSIM_NOT_RUN = "-"
PIMSIM_UNSUPPORTED_VSOFTMAX = "pimsim-nn does not support binary opcode vsoftmax"
PIMSIM_LATENCY_RE = re.compile(r"\blatency:\s+([0-9.eE+-]+)\s+ms")
PIMSIM_POWER_RE = re.compile(r"\baverage power:\s+([0-9.eE+-]+)\s+mW")
PIMSIM_UNSUPPORTED_VSOFTMAX = "pimsim-nn does not support opcode vsoftmax"
class PimSimUnsupportedError(RuntimeError):
@@ -80,6 +78,7 @@ class ValidationResult:
pim_pass_timings: dict[str, float] = field(default_factory=dict)
pimsim_latency_ms: float | None = None
pimsim_power_mw: float | None = None
pimsim_energy_pj: float | None = None
pimsim_status: str = PIMSIM_SKIPPED
@@ -262,9 +261,10 @@ def pimcomp_compatibility_errors(config_path, *, core_count, crossbar_count, cro
def run_pimsim_nn(pimsim_nn_build_dir, pim_dir, config_path, reporter=None, timeout_sec=None):
latency_artifact = export_raptor_latency_artifact(pim_dir, Path(pim_dir).parent / "pimsim_nn")
try:
output = run_command(
[pimsim_nn_build_dir / "ChipTest", pim_dir, config_path, "--gui=false"],
[pimsim_nn_build_dir / "ChipTest", latency_artifact, config_path, "--gui=false"],
cwd=pimsim_nn_build_dir,
reporter=reporter,
timeout_sec=timeout_sec,
@@ -275,11 +275,11 @@ def run_pimsim_nn(pimsim_nn_build_dir, pim_dir, config_path, reporter=None, time
if PIMSIM_UNSUPPORTED_VSOFTMAX in error_output:
raise PimSimUnsupportedError(PIMSIM_UNSUPPORTED_VSOFTMAX) from exc
raise
latency_match = PIMSIM_LATENCY_RE.search(output)
power_match = PIMSIM_POWER_RE.search(output)
if not latency_match or not power_match:
raise RuntimeError("pimsim-nn output did not contain latency and average power")
return float(latency_match.group(1)), float(power_match.group(1))
metrics = parse_pimsim_nn_metrics(output)
required = ("latency_ms", "average_power_mw", "average_energy_pj")
if any(name not in metrics for name in required):
raise RuntimeError("pimsim-nn output did not contain latency, average power, and average energy")
return tuple(metrics[name] for name in required)
def clean_workspace_artifacts(workspace_dir, model_stem):
@@ -416,6 +416,8 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
pimsim_nn_build_dir = Path(pimsim_nn_build_dir).resolve()
pimsim_config_path = Path(pimsim_config_path).resolve()
compile_extra_args = list(raptor_extra_args or [])
if pimsim_enabled and "--pim-emit-json" not in compile_extra_args:
compile_extra_args.append("--pim-emit-json")
owns_reporter = reporter is None
reporter = reporter or ProgressReporter(model_total, stages_per_model=len(MODE_STAGE_TITLES[mode]), verbose=verbose)
@@ -540,10 +542,11 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
"Run Non-functional Simulation")
pimsim_latency_ms = None
pimsim_power_mw = None
pimsim_energy_pj = None
pimsim_status = PIMSIM_SKIPPED
if pimsim_enabled:
try:
pimsim_latency_ms, pimsim_power_mw = run_pimsim_nn(
pimsim_latency_ms, pimsim_power_mw, pimsim_energy_pj = run_pimsim_nn(
pimsim_nn_build_dir,
pim_dir,
pimsim_config_path,
@@ -554,7 +557,8 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
print_info(
reporter,
f"Latency: {pimsim_latency_ms:.6f} ms, "
f"Power: {pimsim_power_mw:.6f} mW")
f"Power: {pimsim_power_mw:.6f} mW, "
f"Energy: {pimsim_energy_pj:.6f} pJ")
except PimSimUnsupportedError as exc:
pimsim_status = PIMSIM_UNSUPPORTED
print_info(reporter, str(exc))
@@ -581,6 +585,7 @@ def validate_network(network_onnx_path, raptor_path, onnx_include_dir,
pim_pass_timings=pim_pass_timings,
pimsim_latency_ms=pimsim_latency_ms,
pimsim_power_mw=pimsim_power_mw,
pimsim_energy_pj=pimsim_energy_pj,
pimsim_status=pimsim_status,
)
except Exception:
@@ -24,9 +24,10 @@ import onnx
from colorama import Fore, Style
REPO = Path(__file__).resolve().parents[2]
REPO = Path(__file__).resolve().parents[3]
VALIDATION_DIR = REPO / "validation"
PIMSIM_CONFIG_DIR = VALIDATION_DIR / "pimsim_configs/pimcomp"
PIMCOMP_OUTPUT_FILES = ("SimulationInfo.gz", "VerificationInfo.json", "MappingResult.txt")
sys.path.insert(0, str(VALIDATION_DIR))
from raptor_validation.gen_network_runner import gen_network_runner # noqa: E402
@@ -38,6 +39,10 @@ from raptor_validation.onnx_utils import ( # noqa: E402
write_inputs_to_memory_bin,
)
from raptor_validation.raptor import compile_with_raptor # noqa: E402
from raptor_validation.pimsim_nn import ( # noqa: E402
export_raptor_latency_artifact,
parse_pimsim_nn_metrics,
)
from raptor_validation.validate_one import ( # noqa: E402
STAGE_COLORS,
build_dump_ranges,
@@ -70,7 +75,12 @@ def load_pimcomp_exporter():
module = importlib.util.module_from_spec(spec)
assert spec is not None and spec.loader is not None
sys.modules.setdefault("cv2", types.ModuleType("cv2"))
spec.loader.exec_module(module)
write_bytecode = sys.dont_write_bytecode
sys.dont_write_bytecode = True
try:
spec.loader.exec_module(module)
finally:
sys.dont_write_bytecode = write_bytecode
return module
@@ -183,37 +193,21 @@ def run_logged(
return proc.stdout
def remove_tree(path: Path) -> None:
if not path.exists() and not path.is_symlink():
return
if path.is_symlink() or path.is_file():
path.unlink()
return
while True:
children = list(path.iterdir())
if not children:
break
for child in children:
remove_tree(child)
path.rmdir()
def load_model_inputs(model_path: Path, seed: int):
inputs_desc, outputs_desc = onnx_io(model_path)
arrays_in_order, _ = gen_random_inputs(inputs_desc, seed=seed)
return inputs_desc, outputs_desc, arrays_in_order, arrays_in_order
return inputs_desc, outputs_desc, arrays_in_order
def load_saved_inputs(
model_path: Path,
inputs_desc: list[tuple[int, str, int, list[int]]],
inputs_dir: Path,
) -> tuple[list[np.ndarray], list[np.ndarray]]:
) -> list[np.ndarray]:
arrays = []
for idx, name, elem_type, shape in inputs_desc:
array = np.loadtxt(inputs_dir / f"in{idx}.csv", delimiter=",", dtype=_ONNX_TO_NP[elem_type]).reshape(shape)
arrays.append(array)
return arrays, arrays
return arrays
def prepare_pimcomp_model(model_path: Path, out_dir: Path) -> Path:
@@ -328,7 +322,7 @@ def compile_reference(
runner_base = runner_dir / stem
run_logged(
"Reference Emit ONNX IR",
"Compile Reference ONNX IR",
[str(args.raptor_path), str(model_path), "-o", str(onnx_ir_base), "--EmitONNXIR",
"--mlir-elide-elementsattrs-if-larger=16", "--enable-conv-opt-pass=false"],
cwd=REPO,
@@ -337,7 +331,7 @@ def compile_reference(
stage="Compile ONNX",
)
run_logged(
"Reference Native Compile",
"Compile Reference Native",
[str(args.raptor_path), "-O3", str(model_path), "-o", str(runner_base)],
cwd=REPO,
timeout_sec=args.timeout_seconds,
@@ -416,13 +410,18 @@ def compile_raptor_target(
f"--crossbar-size={hardware['crossbar_size']}",
f"--crossbar-count={hardware['crossbar_count']}",
f"--core-count={hardware['core_count']}",
f"--pim-target-config={args.pimcomp_config}",
"--pim-emit-json",
*args.raptor_extra_arg,
]
print_step("Compile Raptor PIM", cmd, REPO, "Compile PIM")
start = time.perf_counter()
command = shell_join(cmd)
raptor_extra_args = ["--pim-emit-json", *args.raptor_extra_arg]
raptor_extra_args = [
f"--pim-target-config={args.pimcomp_config}",
"--pim-emit-json",
*args.raptor_extra_arg,
]
try:
timings = compile_with_raptor(
model_path,
@@ -459,7 +458,7 @@ def compile_raptor_target(
return out_dir / "pim", timings
def run_rust_validation(
def run_functional_validation(
label: str,
pim_dir: Path,
config_path: Path,
@@ -510,7 +509,7 @@ def run_rust_validation(
def copy_pimcomp_outputs(source_dir: Path, out_dir: Path):
out_dir.mkdir(parents=True, exist_ok=True)
for name in ("SimulationInfo.gz", "VerificationInfo.json", "MappingResult.txt"):
for name in PIMCOMP_OUTPUT_FILES:
shutil.copy2(source_dir / name, out_dir / name)
@@ -527,7 +526,7 @@ def compile_pimcomp(
shutil.copy2(args.pimcomp_config, runtime_config)
pimcomp_output_dir = out_dir / "output"
pimcomp_output_dir.mkdir(parents=True, exist_ok=True)
for name in ("SimulationInfo.gz", "VerificationInfo.json", "MappingResult.txt"):
for name in PIMCOMP_OUTPUT_FILES:
(pimcomp_output_dir / name).unlink(missing_ok=True)
model_name = args.pimcomp_model_name or f"compare_{model_path.stem}"
frontend_json = frontend_json_dir / f"{model_name}.json"
@@ -540,7 +539,7 @@ def compile_pimcomp(
str(frontend_json),
]
run_logged(
"PIMCOMP Frontend",
"Compile PIMCOMP Frontend",
frontend_cmd,
cwd=args.pimcomp_dir / "frontend",
timeout_sec=args.timeout_seconds,
@@ -556,20 +555,20 @@ def compile_pimcomp(
"-s=YES",
]
run_logged(
"PIMCOMP Backend",
"Compile PIMCOMP Backend",
backend_cmd,
cwd=frontend_json_dir.parent,
timeout_sec=args.timeout_seconds,
steps=steps,
stage="Compile PIM",
)
remove_tree(frontend_json_dir.parent)
shutil.rmtree(frontend_json_dir.parent)
return pimcomp_output_dir / "VerificationInfo.json", pimcomp_output_dir / "SimulationInfo.gz"
def export_pimcomp_for_pimsim_nn(simulation_info: Path, output_dir: Path) -> Path:
if output_dir.exists():
remove_tree(output_dir)
shutil.rmtree(output_dir)
with gzip.open(simulation_info, "rt", encoding="utf-8") as f:
sim_info = json.load(f)
@@ -598,7 +597,7 @@ def export_pimcomp_for_pimsim_nn(simulation_info: Path, output_dir: Path) -> Pat
for core_idx in core_indices:
core_key = f"core{core_idx}"
instructions = sim_info.get(core_key, []) or [{"op": "lldi", "rd": 0, "imm": 0, "len": 0}]
instructions = sim_info.get(core_key, [])
with open(output_dir / f"core_{core_idx}.json", "w", encoding="utf-8") as f:
json.dump(instructions, f, separators=(",", ":"))
f.write("\n")
@@ -622,7 +621,7 @@ def export_pimcomp_for_rust(
if len(runtime_inputs) != 1:
raise ValueError("PIMCOMP export currently requires exactly one runtime input tensor")
if output_dir.exists():
remove_tree(output_dir)
shutil.rmtree(output_dir)
exporter = load_pimcomp_exporter()
with open(verification_info, "r", encoding="utf-8") as f:
final_info = json.load(f)
@@ -727,7 +726,7 @@ def export_pimcomp_for_rust(
for sim_inst in sim_info.get(core_name, []) or []:
op = sim_inst["op"]
if op == "setbw":
instructions.append(sim_inst)
instructions.append({"op": "setbw", "ibiw": 32, "obiw": 32})
continue
if op == "sldi":
translated = {"op": "sldi", "rd": sim_inst["rd"], "imm": exporter.byte_offset(sim_inst["imm"])}
@@ -782,10 +781,12 @@ def export_pimcomp_for_rust(
"offset": sim_inst["offset"],
}
)
elif op in ("lmv", "vvadd", "vvmul", "vvmax", "vrelu"):
elif op == "lmv":
translated = dict(sim_inst)
translated["len"] = exporter.byte_offset(sim_inst["len"])
instructions.append(translated)
elif op in ("vvadd", "vvmul", "vvmax", "vrelu"):
instructions.append(sim_inst)
elif op in ("send", "recv"):
translated = dict(sim_inst)
translated["size"] = exporter.byte_offset(sim_inst["size"])
@@ -822,24 +823,6 @@ def export_pimcomp_for_rust(
return output_dir
def parse_pimsim_nn_report(output: str) -> dict[str, float | int | str]:
patterns = {
"output_count": r"output count:\s+([0-9]+)\s+samples",
"throughput": r"throughput:\s+([0-9.eE+-]+)\s+samples/s",
"average_latency_ms": r"average latency:\s+([0-9.eE+-]+)\s+ms",
"latency_ms": r"latency:\s+([0-9.eE+-]+)\s+ms",
"average_power_mw": r"average power:\s+([0-9.eE+-]+)\s+mW",
"average_energy_pj": r"average energy:\s+([0-9.eE+-]+)\s+pJ/it",
}
result: dict[str, float | int | str] = {"raw_output": output}
for key, pattern in patterns.items():
match = re.search(pattern, output)
if match:
value = match.group(1)
result[key] = int(value) if key == "output_count" else float(value)
return result
def run_pimsim_nn(
label: str,
inst_path: Path,
@@ -861,7 +844,7 @@ def run_pimsim_nn(
steps=steps,
stage="Run Non-functional Simulation",
)
return parse_pimsim_nn_report(output)
return parse_pimsim_nn_metrics(output)
def parse_raptor_instructions(pim_dir: Path) -> dict[str, Any]:
@@ -1069,7 +1052,7 @@ def write_report(
lines.extend(
[
"## Semantic Validation",
"## Functional Validation",
"",
f"- Raptor via `pim-simulator`: `{validation_status(raptor_validation)}`",
f"- PIMCOMP via exported `pim-simulator`: `{validation_status(pimcomp_validation)}`",
@@ -1268,6 +1251,7 @@ def main():
runner_path: Path | None = None
reference_dir: Path | None = None
raptor_pim_dir: Path | None = None
raptor_pimsim_dir: Path | None = None
raptor_pass_timings: dict[str, float] = {}
verification_info: Path | None = None
simulation_info: Path | None = None
@@ -1289,7 +1273,8 @@ def main():
model_io = try_stage(failures, "Load model inputs", load_model_inputs, model_path, args.seed)
if model_io is not None:
inputs_desc, outputs_desc, arrays_in_order, runtime_inputs = model_io
inputs_desc, outputs_desc, arrays_in_order = model_io
runtime_inputs = arrays_in_order
if reuse_raptor and model_io is not None:
reuse_report_path = args.reuse_raptor_report.resolve()
@@ -1300,11 +1285,11 @@ def main():
raise ValueError(f"Reused Raptor hardware differs: {reused_hardware} != {hardware}")
reference_dir = Path(reused["paths"]["reference_outputs"])
raptor_pim_dir = Path(reused["paths"]["raptor_pim"])
arrays_in_order, runtime_inputs = load_saved_inputs(
model_path,
arrays_in_order = load_saved_inputs(
inputs_desc,
reference_dir.parent / "inputs",
)
runtime_inputs = arrays_in_order
raptor_validation = CompareResult(**reused["raptor_validation"])
raptor_perf = reused["raptor_performance"]
raptor_instr = reused["raptor_instruction_summary"]
@@ -1392,9 +1377,9 @@ def main():
if wrote_inputs and reference_dir is not None and outputs_desc:
validation = try_stage(
failures,
"Rust Validation Raptor",
run_rust_validation,
"Rust Validation Raptor",
"Functional Validation Raptor",
run_functional_validation,
"Functional Validation Raptor",
raptor_pim_dir,
raptor_pim_dir / "config.json",
out_dir / "simulation/out.bin",
@@ -1451,7 +1436,7 @@ def main():
if verification_info is not None and simulation_info is not None and model_io is not None:
exported = try_stage(
failures,
"Export PIMCOMP for Rust",
"Export PIMCOMP for Functional Validation",
export_pimcomp_for_rust,
pimcomp_model_path,
verification_info,
@@ -1464,22 +1449,22 @@ def main():
elif verification_info is None or simulation_info is None:
record_failure(
failures,
"Export PIMCOMP for Rust",
"PIMCOMP Rust export failed because PIMCOMP did not produce VerificationInfo.json and SimulationInfo.gz.",
"Export PIMCOMP for Functional Validation",
"PIMCOMP functional export failed because PIMCOMP did not produce VerificationInfo.json and SimulationInfo.gz.",
)
else:
record_failure(
failures,
"Export PIMCOMP for Rust",
"PIMCOMP Rust export failed because model inputs are not available.",
"Export PIMCOMP for Functional Validation",
"PIMCOMP functional export failed because model inputs are not available.",
)
if pimcomp_export_dir is not None and reference_dir is not None and outputs_desc:
validation = try_stage(
failures,
"Rust Validation PIMCOMP",
run_rust_validation,
"Rust Validation PIMCOMP",
"Functional Validation PIMCOMP",
run_functional_validation,
"Functional Validation PIMCOMP",
pimcomp_export_dir,
pimcomp_export_dir / "config.json",
out_dir / "simulation/pimcomp.out.bin",
@@ -1491,7 +1476,7 @@ def main():
)
pimcomp_validation = validation if validation is not None else failed_validation("PIMCOMP validation failed")
elif pimcomp_export_dir is None:
pimcomp_validation = failed_validation("PIMCOMP Rust export is not available")
pimcomp_validation = failed_validation("PIMCOMP functional export is not available")
elif reference_dir is None:
pimcomp_validation = failed_validation("Reference outputs are not available")
else:
@@ -1524,17 +1509,27 @@ def main():
pimcomp_perf = skipped_perf("pimsim-nn config is not available")
else:
if not reuse_raptor and raptor_pim_dir is not None:
perf = try_stage(
raptor_pimsim_dir = try_stage(
failures,
"pimsim-nn Raptor",
run_pimsim_nn,
"pimsim-nn Raptor",
"Export Raptor for pimsim-nn",
export_raptor_latency_artifact,
raptor_pim_dir,
pimsim_config,
steps,
args,
out_dir / "raptor/pimsim_nn",
)
raptor_perf = perf if perf is not None else failed_perf("pimsim-nn Raptor failed")
if raptor_pimsim_dir is not None:
perf = try_stage(
failures,
"Non-Functional Simulation Raptor",
run_pimsim_nn,
"Non-Functional Simulation Raptor",
raptor_pimsim_dir,
pimsim_config,
steps,
args,
)
raptor_perf = perf if perf is not None else failed_perf("pimsim-nn Raptor failed")
else:
raptor_perf = failed_perf("Raptor pimsim-nn export failed")
elif not reuse_raptor:
raptor_perf = skipped_perf("Raptor PIM directory is not available")
@@ -1549,9 +1544,9 @@ def main():
if pimcomp_pimsim_dir is not None:
perf = try_stage(
failures,
"pimsim-nn PIMCOMP",
"Non-Functional Simulation PIMCOMP",
run_pimsim_nn,
"pimsim-nn PIMCOMP",
"Non-Functional Simulation PIMCOMP",
pimcomp_pimsim_dir,
pimsim_config,
steps,
@@ -1613,6 +1608,7 @@ def main():
"paths": {
"reference_outputs": optional_path(reference_dir),
"raptor_pim": optional_path(raptor_pim_dir),
"raptor_pimsim_nn": optional_path(raptor_pimsim_dir),
"pimcomp_simulation_info": optional_path(simulation_info),
"pimcomp_exported_pim": optional_path(pimcomp_export_dir),
"pimsim_config": optional_path(pimsim_config),
@@ -1625,11 +1621,11 @@ def main():
f.write("\n")
failed_steps = any(step.status != "passed" for step in steps)
semantic_failure = any(
functional_failure = any(
result.status == "done" and not result.passed
for result in (raptor_validation, pimcomp_validation)
)
failed = bool(failures or failed_steps or semantic_failure)
failed = bool(failures or failed_steps or functional_failure)
result = "FAIL" if args.fail_on_error and failed else "DONE" if failed else "PASS"
color = Fore.RED if result == "FAIL" else Fore.YELLOW if result == "DONE" else Fore.GREEN
print("\n" + Style.BRIGHT + f"Result: {color}{result}" + Style.RESET_ALL)
+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import json
import shlex
import subprocess
import sys
from pathlib import Path
from colorama import Fore, Style
REPO = Path(__file__).resolve().parents[3]
SUITE = REPO / "validation/networks/pimcomp_models"
sys.path.insert(0, str(REPO / "validation"))
from raptor_validation.pimsim_nn import parse_pimsim_nn_metrics # noqa: E402
from raptor_validation.validate_one import STAGE_COLORS # noqa: E402
PIMCOMP_SOURCE = REPO / "third_party/PIMCOMP-NN"
PIMCOMP_CONFIG = REPO / "validation/pimsim_configs/pimcomp/arch-a/latency_config.json"
COMPARE = Path(__file__).resolve().with_name("compare_raptor_pimcomp.py")
MODELS = {
"vgg8": SUITE / "vgg8/vgg8-mnist-reconstructed.onnx",
"resnet18": SUITE / "resnet18/resnet18-v1-7.onnx",
"resnet34": SUITE / "resnet34/resnet34-v1-7.onnx",
"googlenet": SUITE / "googlenet/googlenet-12-latency.onnx",
}
def result_dir(root: Path | None, name: str) -> Path:
return root / name if root is not None else MODELS[name].parent
def write_results_csv(root: Path | None) -> Path:
output = (root or SUITE) / "results.csv"
fields = (
"model",
"raptor_latency_ms",
"pimcomp_latency_ms",
"raptor_energy_pj",
"pimcomp_energy_pj",
"faster_compiler",
"speedup",
)
rows = []
for name in MODELS:
report_path = result_dir(root, name) / "pimcomp/comparison_report.json"
if not report_path.exists():
continue
report = json.loads(report_path.read_text(encoding="utf-8"))
raptor = report.get("raptor_performance") or {}
pimcomp = report.get("pimcomp_performance") or {}
raptor_latency = raptor.get("latency_ms")
pimcomp_latency = pimcomp.get("latency_ms")
if raptor_latency is None or pimcomp_latency is None:
continue
raptor_energy = (raptor.get("average_energy_pj")
or parse_pimsim_nn_metrics(raptor.get("raw_output", "")).get("average_energy_pj"))
pimcomp_energy = (pimcomp.get("average_energy_pj")
or parse_pimsim_nn_metrics(pimcomp.get("raw_output", "")).get("average_energy_pj"))
faster = "raptor" if raptor_latency < pimcomp_latency else "pimcomp"
rows.append({
"model": name,
"raptor_latency_ms": f"{raptor_latency:.6f}",
"pimcomp_latency_ms": f"{pimcomp_latency:.6f}",
"raptor_energy_pj": "" if raptor_energy is None else f"{raptor_energy:.6f}",
"pimcomp_energy_pj": "" if pimcomp_energy is None else f"{pimcomp_energy:.6f}",
"faster_compiler": faster,
"speedup": f"{max(raptor_latency, pimcomp_latency) / min(raptor_latency, pimcomp_latency):.2f}",
})
with open(output, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields, lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
return output
def print_stage(title: str, color: str) -> None:
print("\n" + Style.BRIGHT + color + f"[{title}]" + Style.RESET_ALL, flush=True)
def run(command: list[str], *, dry_run: bool, check: bool = True) -> int:
print(f" cwd: {REPO}", flush=True)
print(f" $ {shlex.join(command)}", flush=True)
if dry_run:
return 0
return subprocess.run(command, cwd=REPO, check=check).returncode
def validate_pimcomp_source() -> None:
header = PIMCOMP_SOURCE / "backend/GeneticAlgorithm.h"
source = header.read_text(encoding="utf-8")
for setting in ("int population_num = 200;", "int max_iteration = 1000;"):
if setting not in source:
raise RuntimeError(f"PIMCOMP paper setting is missing: {setting}")
def comparison_command(model: Path, result_dir: Path, timeout: float) -> list[str]:
return [
sys.executable,
str(COMPARE),
"--model",
str(model),
"--out-dir",
str(result_dir),
"--pimcomp-dir",
str(PIMCOMP_SOURCE),
"--pimcomp-config",
str(PIMCOMP_CONFIG),
"--core-count",
"168",
"--crossbar-count",
"96",
"--crossbar-size",
"128",
"--mesh-rows",
"12",
"--mesh-cols",
"14",
"--pimsim-mode",
"latency",
"--pimcomp-pipeline",
"element",
"--pimcomp-replication",
"GA",
"--timeout-seconds",
str(timeout),
"--fail-on-error",
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Reproduce the serial Arch-A latency comparison from the PIMCOMP paper."
)
parser.add_argument(
"--out-dir",
type=Path,
help="Result root (default: artifacts beside each model under validation/).",
)
parser.add_argument("--models", nargs="+", choices=MODELS, default=list(MODELS))
parser.add_argument("--timeout-seconds", type=float, default=3600.0)
parser.add_argument(
"--resume",
action="store_true",
help="Skip models with a completed JSON report.",
)
parser.add_argument("--dry-run", action="store_true", help="Print commands without modifying files.")
args = parser.parse_args()
out_dir = args.out_dir.resolve() if args.out_dir is not None else None
missing = [str(MODELS[name]) for name in args.models if not MODELS[name].exists()]
if missing:
parser.error(f"missing model(s): {', '.join(missing)}")
validate_pimcomp_source()
if out_dir is not None and not args.dry_run:
out_dir.mkdir(parents=True, exist_ok=True)
print(Style.BRIGHT + f"Found {len(args.models)} PIMCOMP model(s) to compare." + Style.RESET_ALL)
print(f"Results root: {out_dir or SUITE}")
print("=" * 72)
print_stage("Build Raptor", STAGE_COLORS["Build Runner"])
run(["cmake", "--build", str(REPO / "build_release")], dry_run=args.dry_run)
print_stage("Build PIMCOMP", STAGE_COLORS["Build Runner"])
run(
["cmake", "--build", str(PIMCOMP_SOURCE / "build"), "--target", "PIMCOMP-NN"],
dry_run=args.dry_run,
)
failed = []
for index, name in enumerate(args.models, start=1):
model_result_dir = result_dir(out_dir, name)
print(
"\n" + Fore.CYAN + f"[{index}/{len(args.models)}]" + Style.RESET_ALL
+ f" {Style.BRIGHT}Comparing {name}{Style.RESET_ALL}",
flush=True,
)
if args.resume and (model_result_dir / "pimcomp/comparison_report.json").exists():
print(
Fore.YELLOW + " Completed report exists; skipping" + Style.RESET_ALL,
flush=True,
)
continue
returncode = run(
comparison_command(MODELS[name], model_result_dir, args.timeout_seconds),
dry_run=args.dry_run,
check=False,
)
if returncode:
failed.append(name)
if args.dry_run:
return 1 if failed else 0
results_path = write_results_csv(out_dir)
print_stage("Results", STAGE_COLORS["Compare Outputs"])
print(results_path.read_text(encoding="utf-8"), end="")
print("\n" + Style.BRIGHT + Fore.CYAN + "Summary" + Style.RESET_ALL)
print(Style.BRIGHT + f"Passed: {len(args.models) - len(failed)}" + Style.RESET_ALL)
print(Style.BRIGHT + f"Failed: {len(failed)}" + Style.RESET_ALL)
print(Style.BRIGHT + f"Results: {results_path}" + Style.RESET_ALL)
if failed:
print(
Fore.RED + f"Failed comparisons: {', '.join(failed)}" + Style.RESET_ALL,
file=sys.stderr,
)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,148 +0,0 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import shlex
import subprocess
import sys
from pathlib import Path
from colorama import Fore, Style
REPO = Path(__file__).resolve().parents[2]
SUITE = REPO / "validation/networks/pimcomp_models"
PIMCOMP_SOURCE = REPO / "third_party/PIMCOMP-NN"
PIMCOMP_CONFIG = REPO / "validation/pimsim_configs/pimcomp/arch-a/latency_config.json"
COMPARE = REPO / "validation/tools/compare_raptor_pimcomp.py"
MODELS = {
"vgg8": SUITE / "vgg8/vgg8-mnist-reconstructed.onnx",
"resnet18": SUITE / "resnet18/resnet18-v1-7.onnx",
"resnet34": SUITE / "resnet34/resnet34-v1-7.onnx",
"googlenet": SUITE / "googlenet/googlenet-12-no-softmax.onnx",
}
def run(command: list[str], *, dry_run: bool, check: bool = True) -> int:
print(Fore.CYAN + "$ " + Style.RESET_ALL + shlex.join(command), flush=True)
if dry_run:
return 0
return subprocess.run(command, cwd=REPO, check=check).returncode
def validate_pimcomp_source() -> None:
header = PIMCOMP_SOURCE / "backend/GeneticAlgorithm.h"
source = header.read_text(encoding="utf-8")
for setting in ("int population_num = 200;", "int max_iteration = 1000;"):
if setting not in source:
raise RuntimeError(f"PIMCOMP paper setting is missing: {setting}")
def comparison_command(model: Path, result_dir: Path, timeout: float) -> list[str]:
return [
sys.executable,
str(COMPARE),
"--model",
str(model),
"--out-dir",
str(result_dir),
"--pimcomp-dir",
str(PIMCOMP_SOURCE),
"--pimcomp-config",
str(PIMCOMP_CONFIG),
"--core-count",
"168",
"--crossbar-count",
"96",
"--crossbar-size",
"128",
"--mesh-rows",
"12",
"--mesh-cols",
"14",
"--pimsim-mode",
"latency",
"--pimcomp-pipeline",
"element",
"--pimcomp-replication",
"GA",
"--timeout-seconds",
str(timeout),
"--fail-on-error",
]
def main() -> int:
parser = argparse.ArgumentParser(
description="Reproduce the serial Arch-A latency comparison from the PIMCOMP paper."
)
parser.add_argument(
"--out-dir",
type=Path,
help="Result root (default: artifacts beside each model under validation/).",
)
parser.add_argument("--models", nargs="+", choices=MODELS, default=list(MODELS))
parser.add_argument("--timeout-seconds", type=float, default=3600.0)
parser.add_argument(
"--resume",
action="store_true",
help="Skip models with a completed JSON report.",
)
parser.add_argument("--dry-run", action="store_true", help="Print commands without modifying files.")
args = parser.parse_args()
out_dir = args.out_dir.resolve() if args.out_dir is not None else None
missing = [str(MODELS[name]) for name in args.models if not MODELS[name].exists()]
if missing:
parser.error(f"missing model(s): {', '.join(missing)}")
validate_pimcomp_source()
if out_dir is not None and not args.dry_run:
out_dir.mkdir(parents=True, exist_ok=True)
run(["cmake", "--build", str(REPO / "build_release")], dry_run=args.dry_run)
run(
["cmake", "--build", str(PIMCOMP_SOURCE / "build"), "--target", "PIMCOMP-NN"],
dry_run=args.dry_run,
)
failed = []
for name in args.models:
result_dir = out_dir / name if out_dir is not None else MODELS[name].parent
if args.resume and (result_dir / "pimcomp/comparison_report.json").exists():
print(
Fore.YELLOW + f"[{name}] completed report exists; skipping" + Style.RESET_ALL,
flush=True,
)
continue
print(
"\n" + Fore.CYAN + f"[{name}]" + Style.RESET_ALL
+ f" {Style.BRIGHT}Arch-A latency comparison{Style.RESET_ALL}",
flush=True,
)
returncode = run(
comparison_command(MODELS[name], result_dir, args.timeout_seconds),
dry_run=args.dry_run,
check=False,
)
if returncode:
failed.append(name)
if failed:
print(
"\n" + Style.BRIGHT + Fore.RED + "Result: FAIL" + Style.RESET_ALL,
file=sys.stderr,
)
print(
Fore.RED + f"Failed comparisons: {', '.join(failed)}" + Style.RESET_ALL,
file=sys.stderr,
)
return 1
if not args.dry_run:
print("\n" + Style.BRIGHT + f"Result: {Fore.GREEN}PASS" + Style.RESET_ALL)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+12 -5
View File
@@ -255,6 +255,10 @@ def main():
total_timing_sum = 0.0
timed_benchmark_count = 0
reporter = ProgressReporter(len(onnx_files), stages_per_model=1, verbose=a.verbose)
raptor_extra_args = list(a.raptor_extra_arg)
if not any(str(arg).startswith("--pim-target-config=") for arg in raptor_extra_args):
raptor_extra_args.append(f"--pim-target-config={pimsim_config_path}")
validation_kwargs = {
"raptor_path": a.raptor_path,
"onnx_include_dir": a.onnx_include_dir,
@@ -262,7 +266,7 @@ def main():
"crossbar_size": a.crossbar_size,
"crossbar_count": a.crossbar_count,
"core_count": a.core_count,
"raptor_extra_args": a.raptor_extra_arg,
"raptor_extra_args": raptor_extra_args,
"pimsim_nn_build_dir": pimsim_nn_build_dir,
"pimsim_config_path": selected_pimsim_config,
"command_timeout_seconds": a.command_timeout_seconds,
@@ -337,28 +341,31 @@ def main():
rel: (
format_pimsim_metric(result, result.pimsim_latency_ms, "ms"),
format_pimsim_metric(result, result.pimsim_power_mw, "mW"),
format_pimsim_metric(result, result.pimsim_energy_pj, "pJ"),
)
for rel, result in results.items()
}
latency_width = max(len("Latency"), *(len(metrics[0]) for metrics in formatted_metrics.values()))
power_width = max(len("Power"), *(len(metrics[1]) for metrics in formatted_metrics.values()))
energy_width = max(len("Energy"), *(len(metrics[2]) for metrics in formatted_metrics.values()))
separator = (
f"+-{'-' * path_width}-+-{'-' * status_width}-+-{'-' * latency_width}"
f"-+-{'-' * power_width}-+")
f"-+-{'-' * power_width}-+-{'-' * energy_width}-+")
print(separator)
print(
f"| {'Operation'.ljust(path_width)} | {'Result'.ljust(status_width)} | "
f"{'Latency'.rjust(latency_width)} | {'Power'.rjust(power_width)} |"
f"{'Latency'.rjust(latency_width)} | {'Power'.rjust(power_width)} | "
f"{'Energy'.rjust(energy_width)} |"
)
print(separator)
for rel, result in results.items():
plain_status = "PASS" if result.passed else "FAIL"
status = Fore.GREEN + plain_status.ljust(status_width) + Style.RESET_ALL if result.passed else \
Fore.RED + plain_status.ljust(status_width) + Style.RESET_ALL
latency, power = formatted_metrics[rel]
latency, power, energy = formatted_metrics[rel]
print(
f"| {rel.ljust(path_width)} | {status} | {latency.rjust(latency_width)} | "
f"{power.rjust(power_width)} |")
f"{power.rjust(power_width)} | {energy.rjust(energy_width)} |")
print(separator)
print("\n" + Style.BRIGHT + Fore.CYAN + "Summary" + Style.RESET_ALL)
print(Style.BRIGHT + f"Passed: {n_passed}" + Style.RESET_ALL)