Compare commits

9 Commits

Author SHA1 Message Date
ilgeco f3a4e19f7c Python script for compare
Validate Operations / validate-operations (push) Has been cancelled
2026-07-24 12:47:37 +02:00
ilgeco e7611be8e1 Now also googlenet runs 2026-07-23 17:47:30 +02:00
ilgeco c59e320efa Almost all model compile and they are faster then pimcomp 2026-07-23 14:40:58 +02:00
ilgeco c491078757 Merge branch 'TestRottoConDeadLock' of chef.heaplab.deib.polimi.it:nnicolosi/Raptor into TestRottoConDeadLock 2026-07-22 18:04:12 +02:00
ilgeco bb4594fc01 validate 2026-07-22 18:03:36 +02:00
ilgeco 24c6ea3c6e validation 2026-07-22 14:08:57 +02:00
ilgeco b651968f30 Merge branch 'TestRottoConDeadLock' of chef.heaplab.deib.polimi.it:nnicolosi/Raptor into TestRottoConDeadLock 2026-07-22 13:36:55 +02:00
ilgeco d88bced271 After merge 2026-07-22 13:36:15 +02:00
ilgeco 6bc7709aa4 slighlty faster 2026-07-21 15:43:22 +02:00
23 changed files with 1090 additions and 119 deletions
@@ -152,7 +152,12 @@ fn get_crossbars(config: &Value, args: &Args) -> anyhow::Result<HashMap<String,
}
let bytes = std::fs::read(weight_file.path()).expect("Failed to read binary file");
let mut crossbar = Crossbar::new(column_corssbar * 4, rows_crossbar, CoreMemory::new());
let stored_row_bytes = bytes.len() / rows_crossbar;
let mut crossbar = Crossbar::new(
std::cmp::max(column_corssbar * 4, stored_row_bytes),
rows_crossbar,
CoreMemory::new(),
);
crossbar.execute_store(&bytes).unwrap();
res.insert(
weight_file
@@ -2,6 +2,7 @@
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/Tensor/IR/Tensor.h"
#include "DeferredBoundaryRealization.hpp"
#include "DeferredProjectionAnalysis.hpp"
#include "DeferredResultRealization.hpp"
#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp"
#include "src/Accelerators/PIM/Common/IR/StaticIntGrid.hpp"
@@ -428,9 +429,9 @@ static FailureOr<Value> insertProjectionFragment(Value fragment, Value specializ
DeferredExchangePlan &exchange, bool grouped, DeferredEmissionContext &context) {
Value shaped = fragment;
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
SmallVector<int64_t> shape(leaf.leadingRankReduced ? leaf.reconstructedType.getShape() : leaf.reconstructedType.getShape().drop_front());
RankedTensorType projectedType = getDeferredProjectedFragmentType(leaf);
shaped = extractMixedSliceOrIdentity(context.rewriter, exchange.deferred.getLoc(), shaped,
RankedTensorType::get(shape, leaf.reconstructedType.getElementType()),
projectedType,
lookupGeometry(geometry, geometryRow, runtimeLane, exchange.deferred, context, exchange.deferred.getLoc()));
if (!shaped) return failure();
}
@@ -494,8 +495,26 @@ static LogicalResult emitLeafCollectionUpdate(const EmitReceiveAssemblyRun &run,
return emitCollectionUpdate(run.lanes, lane, laneCount, key, current, exchange.deferred, context, emit);
}
static FailureOr<Value> transformAssemblySource(Value fragment, const DeferredInsertAssemblyEntryTemplate &entry,
DeferredExchangePlan &exchange, DeferredEmissionContext &context) {
static FailureOr<Value> transformAssemblySource(
Value fragment, const DeferredInsertAssemblyEntryTemplate &entry,
Value runtimeLane, const DeferredResultPlan &resultPlan,
DeferredExchangePlan &exchange, DeferredEmissionContext &context) {
if (entry.coordinate.leafIndex >= exchange.program.leaves.size()
|| entry.coordinate.leafIndex >= resultPlan.innerGeometry.size())
return failure();
const DeferredProjectionLeafTemplate &leaf =
exchange.program.leaves[entry.coordinate.leafIndex];
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
fragment = extractMixedSliceOrIdentity(
context.rewriter, exchange.deferred.getLoc(), fragment,
getDeferredProjectedFragmentType(leaf),
lookupGeometry(resultPlan.innerGeometry[entry.coordinate.leafIndex],
context.constants.getIndex(0), runtimeLane,
exchange.deferred, context,
exchange.deferred.getLoc()));
if (!fragment)
return failure();
}
switch (entry.sourceTransform) {
case DeferredAssemblySourceTransform::Identity:
return fragment.getType() == entry.sourceType ? FailureOr<Value>(fragment) : FailureOr<Value>(failure());
@@ -510,12 +529,15 @@ static FailureOr<Value> transformAssemblySource(Value fragment, const DeferredIn
static FailureOr<Value> materializeLoopedLocalAssemblySource(
RequirementFamily &requirement,
const DeferredInsertAssemblyEntryTemplate &entry,
Value localOffset, DeferredExchangePlan &exchange,
Value localOffset, Value runtimeLane,
const DeferredResultPlan &resultPlan, DeferredExchangePlan &exchange,
DeferredEmissionContext &context) {
Value payload = requirement.producer->payload;
auto payloadType = dyn_cast<RankedTensorType>(payload.getType());
RankedTensorType sourceType = entry.sourceType;
if (entry.sourceTransform
if (exchange.program.leaves[entry.coordinate.leafIndex].form
!= DeferredLeafForm::GraphBatchProjection
&& entry.sourceTransform
== DeferredAssemblySourceTransform::RemoveLeadingUnitDimension
&& payloadType && sourceType
&& payloadType.getRank() > sourceType.getRank()
@@ -542,7 +564,8 @@ static FailureOr<Value> materializeLoopedLocalAssemblySource(
exchange.deferred.getLoc());
if (failed(fragment))
return failure();
return transformAssemblySource(*fragment, entry, exchange, context);
return transformAssemblySource(
*fragment, entry, runtimeLane, resultPlan, exchange, context);
}
static LogicalResult emitLoopedLocalCollectionUpdate(
@@ -590,7 +613,8 @@ static LogicalResult emitLoopedLocalCollectionUpdate(
RequirementFamily &requirement = *run.families.front()->requirement;
FailureOr<Value> source = entry
? materializeLoopedLocalAssemblySource(
requirement, *entry, localOffset, exchange, context)
requirement, *entry, localOffset, runtimeLane, resultPlan,
exchange, context)
: materializeSendPayload(
requirement, localOffset, nullptr, context, loc);
if (failed(source))
@@ -650,7 +674,8 @@ static LogicalResult emitInsertAssemblyUpdate(const EmitReceiveAssemblyRun &run,
auto emit = [&](Value initial) {
return emitReceiveAssembly(run, lane, laneCount, initial, context,
[&](Value fragment, Value position, Value, Value runtimeLane, Value assembled) -> FailureOr<Value> {
auto shaped = transformAssemblySource(fragment, sourceEntry, exchange, context);
auto shaped = transformAssemblySource(
fragment, sourceEntry, runtimeLane, resultPlan, exchange, context);
if (failed(shaped) || shaped->getType() != sourceEntry.sourceType) return failure();
return insertMixedSlice(context.rewriter, exchange.deferred.getLoc(), *shaped, assembled,
lookupGeometry(resultPlan.assemblyGeometry, position, runtimeLane, exchange.deferred, context, exchange.deferred.getLoc()));
@@ -775,7 +800,9 @@ static LogicalResult emitLocalCollectionUpdate(const EmitLocalCollectionRun &upd
auto emit = [&](Value assembled) -> FailureOr<Value> {
auto fragment = materialize();
if (failed(fragment)) return failure();
auto shaped = transformAssemblySource(*fragment, entry, exchange, context);
auto shaped = transformAssemblySource(
*fragment, entry, lane ? lane : context.constants.getIndex(0),
resultPlan, exchange, context);
if (failed(shaped) || shaped->getType() != entry.sourceType) return failure();
return insertMixedSlice(context.rewriter, exchange.deferred.getLoc(), *shaped, assembled,
lookupGeometry(resultPlan.assemblyGeometry, context.constants.getIndex(update.collectionPosition), lane ? lane : context.constants.getIndex(0),
@@ -262,13 +262,12 @@ static void collectClosure(Value value, Block &body, const DeferredInputPlan &pl
} // namespace
bool isDeferredFragmentAssemblyInput(
Value input, const ComputeInstance &consumerInstance) {
bool isDeferredFragmentAssemblyInput(Value input) {
auto blueprint = input.getDefiningOp<SpatBlueprintOp>();
if (!blueprint || blueprint.getMode() != "fragment_assembly")
return false;
return llvm::all_of(getBlueprintFragments(blueprint), [&](Value fragment) {
return getProducerValueRef(fragment, &consumerInstance).has_value();
return getProducerValueRef(fragment, nullptr).has_value();
});
}
@@ -279,7 +278,7 @@ LogicalResult prepareSingleCpuInput(OpBuilder &, Location loc, Value input, Bloc
Value graphLane, Value scheduledGraphLane,
DeferredInputPlan &plan) {
plan = {graphInput, {}, {}, {}, graphLane, scheduledGraphLane, {}, {}, {}, {}, 1, nullptr};
if (isDeferredFragmentAssemblyInput(input, consumerInstance)) {
if (isDeferredFragmentAssemblyInput(input)) {
plan.blueprint = input.getDefiningOp<SpatBlueprintOp>();
plan.originalSources = getBlueprintFragments(plan.blueprint);
return success();
@@ -310,7 +309,7 @@ LogicalResult prepareMultiCpuTupleInput(OpBuilder &, Location loc, Value input,
DeferredInputPlan &plan) {
const ComputeInstance &representative = tuple.instances.front();
plan = {graphInput, {}, {}, {}, graphLane, scheduledGraphLane, scheduledLane, {}, {}, {}, 1, nullptr};
if (isDeferredFragmentAssemblyInput(input, representative)) {
if (isDeferredFragmentAssemblyInput(input)) {
plan.blueprint = input.getDefiningOp<SpatBlueprintOp>();
plan.originalSources = getBlueprintFragments(plan.blueprint);
return success();
@@ -22,8 +22,7 @@ struct DeferredInputPlan {
Block *scalarizedHoistBlock = nullptr;
};
bool isDeferredFragmentAssemblyInput(Value input,
const ComputeInstance &consumerInstance);
bool isDeferredFragmentAssemblyInput(Value input);
LogicalResult prepareSingleCpuInput(OpBuilder &builder, Location loc, Value input,
BlockArgument graphInput,
@@ -354,6 +354,16 @@ FailureOr<int64_t> evaluateDeferredIndex(
return evaluate(value, environment, visiting);
}
RankedTensorType getDeferredProjectedFragmentType(
const DeferredProjectionLeafTemplate &leaf) {
if (leaf.form == DeferredLeafForm::GraphBatchProjection
&& !leaf.leadingRankReduced)
return RankedTensorType::get(
leaf.reconstructedType.getShape().drop_front(),
leaf.reconstructedType.getElementType());
return leaf.reconstructedType;
}
FailureOr<int64_t> evaluateDeferredIndex(
OpFoldResult value, const StaticIndexEnvironment &environment) {
if (auto attr = dyn_cast<Attribute>(value))
@@ -18,6 +18,9 @@ mlir::FailureOr<int64_t> evaluateDeferredIndex(
mlir::FailureOr<DeferredProgramTemplate> analyzeDeferredProgramTemplate(
SpatDeferredCommunicationOp deferred);
mlir::RankedTensorType getDeferredProjectedFragmentType(
const DeferredProjectionLeafTemplate &leaf);
class DeferredLaneValueEvaluator {
public:
DeferredLaneValueEvaluator(const DeferredProgramTemplate &program,
@@ -72,11 +72,7 @@ static LogicalResult buildLeafCollections(DeferredExchangePlan &exchange,
<< ": publication fragment types differ or are unranked";
RankedTensorType normalized = fragmentType;
if (leaf.form == DeferredLeafForm::GraphBatchProjection)
normalized = leaf.leadingRankReduced
? leaf.reconstructedType
: RankedTensorType::get(
leaf.reconstructedType.getShape().drop_front(),
leaf.reconstructedType.getElementType());
normalized = getDeferredProjectedFragmentType(leaf);
bool direct = positionCount == 1 && normalized == leaf.reconstructedType;
bool leading = leaf.reconstructedType.getRank() == normalized.getRank() + 1
&& leaf.reconstructedType.getDimSize(0) == positionCount
@@ -113,11 +109,20 @@ static LogicalResult buildLeafCollections(DeferredExchangePlan &exchange,
}
static bool supportsAssemblyTransform(
Type publicationType, const DeferredInsertAssemblyEntryTemplate &entry) {
Type publicationType, const DeferredInsertAssemblyEntryTemplate &entry,
const DeferredProjectionLeafTemplate &leaf) {
auto publication = dyn_cast<RankedTensorType>(publicationType);
RankedTensorType source = entry.sourceType;
if (!publication || !source)
return false;
if (leaf.form == DeferredLeafForm::GraphBatchProjection) {
auto physical = dyn_cast<RankedTensorType>(leaf.sourceRoot.getType());
if (!physical || physical.getRank() != publication.getRank() + 1
|| physical.getElementType() != publication.getElementType()
|| physical.getShape().drop_front() != publication.getShape())
return false;
publication = getDeferredProjectedFragmentType(leaf);
}
switch (entry.sourceTransform) {
case DeferredAssemblySourceTransform::Identity:
return publication == source;
@@ -150,11 +155,16 @@ static LogicalResult buildInsertAssemblyCollection(
for (RequirementFamily &requirement : exchange.requirements) {
if (!(requirement.coordinate == entry.coordinate))
continue;
const DeferredProjectionLeafTemplate &leaf =
exchange.program.leaves[entry.coordinate.leafIndex];
if (!supportsAssemblyTransform(
requirement.publicationFragmentType, entry))
requirement.publicationFragmentType, entry, leaf))
return exchange.deferred.emitOpError(
"insert assembly source transform does not match publication type at entry ")
<< position;
<< position << ": publication "
<< requirement.publicationFragmentType << ", assembly source "
<< entry.sourceType << ", transform "
<< static_cast<unsigned>(entry.sourceTransform);
if (!collected.insert(&requirement).second)
return exchange.deferred.emitOpError(
"insert assembly requirement is owned by multiple entries at entry ")
@@ -179,7 +179,7 @@ LogicalResult collectPeftClassOperandsAndResults(
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input, instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input))
appendUnique(peftClassPlan.inputs, input);
}
return success();
@@ -222,7 +222,7 @@ LogicalResult collectPeftClassOperandsAndResults(
for (Value weight : getComputeInstanceWeights(instance))
appendUnique(peftClassPlan.weights, weight);
for (Value input : getComputeInstanceInputs(instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input, instance))
if (!getProducerValueRef(input, &instance) && !isDeferredFragmentAssemblyInput(input))
appendUnique(peftClassPlan.inputs, input);
}
}
@@ -25,6 +25,7 @@
#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"
#include "src/Support/TypeUtilities.hpp"
namespace onnx_mlir {
@@ -195,9 +196,9 @@ struct PimsimSchedulerCostModel {
};
std::optional<uint64_t> getStaticTripCount(scf::ForOp loop);
[[maybe_unused]] Cost getOperationCost(Operation& op);
Cost getOperationCost(Operation& op);
[[maybe_unused]] Cost getRegionCost(Region& body) {
Cost getRegionCost(Region& body) {
Cost cost = 0;
for (Block& block : body)
for (Operation& op : block)
@@ -205,7 +206,7 @@ std::optional<uint64_t> getStaticTripCount(scf::ForOp loop);
return cost;
}
[[maybe_unused]] Cost getOperationCost(Operation& op) {
Cost getOperationCost(Operation& op) {
if (auto loop = dyn_cast<scf::ForOp>(&op)) {
std::optional<uint64_t> tripCount = getStaticTripCount(loop);
if (!tripCount)
@@ -271,8 +272,7 @@ std::optional<uint64_t> getStaticTripCount(scf::ForOp loop) {
}
Cost getComputeBodyCost(Region& body) {
constexpr Cost kOperationCost = 100;
return checkedMultiply(static_cast<Cost>(countComputeBodyOperationInstances(body)), kOperationCost);
return getRegionCost(body);
}
uint64_t countOperationInstances(Operation& op) {
@@ -535,6 +535,27 @@ evaluateIndexLike(Value value, const DenseMap<Value, int64_t>& bindings, std::op
return evaluateAffineApply(affineApply,
[&](Value operand) { return evaluateIndexLike(operand, bindings, lane, laneArg); });
Operation* op = value.getDefiningOp();
if (!op || !isPureIndexComputationOp(op))
return failure();
SmallVector<Attribute> operands;
Builder builder(op->getContext());
for (Value operand : op->getOperands()) {
FailureOr<int64_t> folded =
evaluateIndexLike(operand, bindings, lane, laneArg);
if (failed(folded))
return failure();
operands.push_back(builder.getIntegerAttr(operand.getType(), *folded));
}
SmallVector<OpFoldResult> results;
if (failed(op->fold(operands, results)) || results.size() != 1)
return failure();
if (auto attribute = dyn_cast<Attribute>(results.front()))
if (auto integer = dyn_cast<IntegerAttr>(attribute))
return integer.getInt();
if (auto folded = dyn_cast<Value>(results.front()))
return evaluateIndexLike(folded, bindings, lane, laneArg);
return failure();
}
@@ -6,14 +6,19 @@
#include <cmath>
#include <limits>
#include <optional>
#include <queue>
#include <tuple>
#include <vector>
#include "PeftScheduler.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.hpp"
namespace onnx_mlir {
namespace spatial {
using namespace mlir;
namespace {
// Pressure means distinct weights exceed half the fleet's one-copy capacity.
@@ -154,6 +159,132 @@ bool hasHighCrossbarPressure(const ComputeGraph& graph, size_t processorCount, s
return false;
}
std::vector<CrossbarUsage> planCrossbarReservations(const ComputeGraph& graph,
size_t processorCount,
size_t crossbarCapacity,
const MeshModel& mesh,
bool preferCrossbarReuse) {
std::vector<size_t> weightedTasks;
for (size_t task = 0; task < graph.nodes.size(); ++task)
if (!graph.nodes[task].crossbarUsage.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();
return graph.nodes[lhs].originalOrder < graph.nodes[rhs].originalOrder;
});
std::vector<CrossbarUsage> 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)
continue;
size_t addedCrossbars = crossbarUnion - reservations[processor].size();
ReservationScore score {preferCrossbarReuse ? addedCrossbars : reservedLoad[processor],
preferCrossbarReuse ? reservedLoad[processor] : addedCrossbars,
mesh.getCenterDistance(processor),
processor};
if (!bestScore || score < *bestScore) {
bestProcessor = processor;
bestScore = score;
}
}
if (bestProcessor == std::numeric_limits<size_t>::max()) {
std::string message =
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(),
processorCount,
crossbarCapacity)
.str();
llvm::report_fatal_error(llvm::StringRef(message));
}
insertCrossbarWeights(reservations[bestProcessor], graph.nodes[task].crossbarUsage);
reservedLoad[bestProcessor] = addOrMax(reservedLoad[bestProcessor], graph.nodes[task].cost);
}
return reservations;
}
using LanePublicationSignatures = llvm::SmallVector<llvm::SmallVector<int64_t, 8>, 8>;
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);
if (failed(publicationMap))
return failure();
for (auto [useIndex, use] : llvm::enumerate(result.getUses())) {
auto blueprint = dyn_cast<SpatBlueprintOp>(use.getOwner());
if (!blueprint || blueprint.getMode() != "fragment_assembly")
continue;
auto operandIndices = blueprint.getFragmentOperandIndices();
auto sourceSlots = blueprint.getFragmentSourceSlots();
auto sourceOffsets = blueprint.getFragmentSourceOffsets();
auto fragmentStrides = blueprint.getFragmentStrides();
auto outputType = dyn_cast<RankedTensorType>(blueprint.getOutput().getType());
if (!operandIndices || !sourceSlots || !sourceOffsets || !fragmentStrides
|| !outputType || !outputType.hasStaticShape())
return blueprint.emitOpError("PEFT publication compatibility requires complete static fragment metadata"),
failure();
llvm::ArrayRef<int64_t> fragmentOffsets = blueprint.getFragmentOffsets();
llvm::ArrayRef<int64_t> fragmentSizes = blueprint.getFragmentSizes();
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()
|| sourceOffsets->size() != operandIndices->size())
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)) {
if (operandIndex != static_cast<int64_t>(use.getOperandNumber()))
continue;
int64_t slot = (*sourceSlots)[fragmentIndex];
if (slot < 0 || slot >= static_cast<int64_t>((*publicationMap)->physicalSlotToGraphLane.size()))
return blueprint.emitOpError("PEFT publication fragment source slot is out of range"), failure();
int64_t graphLane = (*publicationMap)->physicalSlotToGraphLane[slot];
if (graphLane < 0 || graphLane >= batch.getLaneCount())
return blueprint.emitOpError("PEFT publication fragment has no graph lane owner"), failure();
fragmentsByLane[graphLane].push_back(fragmentIndex);
}
for (auto [lane, fragments] : llvm::enumerate(fragmentsByLane)) {
if (fragments.empty())
continue;
llvm::SmallVector<int64_t, 8>& signature = signatures[lane];
signature.push_back(resultIndex);
signature.push_back(useIndex);
signature.push_back(fragments.size());
signature.push_back(rank);
size_t firstFragment = fragments.front();
for (size_t fragmentIndex : fragments) {
signature.push_back((*sourceOffsets)[fragmentIndex] - (*sourceOffsets)[firstFragment]);
for (int64_t dim = 0; dim < rank; ++dim) {
size_t index = fragmentIndex * rank + dim;
size_t firstIndex = firstFragment * rank + dim;
signature.push_back(fragmentOffsets[index] - fragmentOffsets[firstIndex]);
signature.push_back(fragmentSizes[index]);
signature.push_back((*fragmentStrides)[index]);
}
}
}
}
}
return signatures;
}
} // namespace
Time getPeftTransferTime(Time transferCost, size_t sourceProcessor, size_t targetProcessor, size_t processorCount) {
@@ -167,6 +298,8 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
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);
verifyOctTableSize(nodeCount, processorCount);
std::vector<std::vector<size_t>> reverseLevels = buildReverseLevels(graph);
@@ -267,7 +400,8 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
for (size_t processor = 0; processor < processorCount; ++processor) {
unsigned int overlapCount = countCrossbarOverlap(processorCrossbars[processor], graph.nodes[task].crossbarUsage);
size_t crossbarUnion = getCrossbarUnionSize(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;
@@ -361,6 +495,7 @@ 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);
// 3. CRITICAL FIX: Topological Append
@@ -391,6 +526,28 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
// 5. Check if equal schedule in two level
llvm::DenseMap<size_t, mlir::SmallVector<size_t, 5>> equivalentClass;
GraphBatchPublicationCache publicationCache;
llvm::DenseMap<Operation *, LanePublicationSignatures> publicationSignatures;
llvm::DenseSet<Operation *> invalidPublicationSignatures;
auto haveCompatiblePublications = [&](const ComputeInstance& lhs, const ComputeInstance& rhs) {
auto batch = dyn_cast<SpatComputeBatch>(lhs.op);
if (!batch || batch.getNumResults() == 0)
return true;
auto signatures = publicationSignatures.find(lhs.op);
if (signatures == publicationSignatures.end() && !invalidPublicationSignatures.contains(lhs.op)) {
auto built = buildLanePublicationSignatures(batch, publicationCache);
if (failed(built))
invalidPublicationSignatures.insert(lhs.op);
else
signatures = publicationSignatures.try_emplace(lhs.op, std::move(*built)).first;
}
if (invalidPublicationSignatures.contains(lhs.op))
return false;
for (uint32_t lane = 0; lane < lhs.laneCount; ++lane)
if (signatures->second[lhs.laneStart + lane] != signatures->second[rhs.laneStart + lane])
return false;
return true;
};
for (size_t currentProcessor = 0; currentProcessor < processorCount - 1; ++currentProcessor) {
for (size_t controlProcessor = currentProcessor; controlProcessor < processorCount; ++controlProcessor) {
if (tasksByProcessor[currentProcessor].size() != tasksByProcessor[controlProcessor].size())
@@ -403,7 +560,8 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu
const ComputeInstance currentComputeInstance = graph.nodes[currentTask].instance;
const ComputeInstance controlComputeInstance = graph.nodes[controlTask].instance;
if (currentComputeInstance.op != controlComputeInstance.op
|| currentComputeInstance.laneCount != controlComputeInstance.laneCount) {
|| currentComputeInstance.laneCount != controlComputeInstance.laneCount
|| !haveCompatiblePublications(currentComputeInstance, controlComputeInstance)) {
equalSchedule = false;
break;
}
@@ -0,0 +1,246 @@
# PIMCOMP paper models
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.
## Models and provenance
| Directory | Model | Input | Provenance |
| --- | --- | --- | --- |
| `resnet18/` | ResNet-18 v1 | `1x3x224x224` | Symlink to the complete ONNX Model Zoo model already present at `../resnetv2/depth_68/resnetv2_depth_68.onnx`. |
| `resnet34/` | ResNet-34 v1 | `1x3x224x224` | [ONNX Model Zoo `resnet34-v1-7`](https://huggingface.co/onnxmodelzoo/resnet34-v1-7), with its symbolic batch fixed to 1 as PIMCOMP's frontend does. |
| `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.
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
graphs exactly equal to PIMCOMP-NN's bundled `resnet18.json`, `resnet34.json`,
and `googlenet.json`.
There is no VGG-8 artifact in the ONNX Model Zoo or any PIMCOMP-NN revision.
The included VGG-8 therefore has deterministic random weights and is suitable
for compiler and simulator comparison, not paper-accuracy reproduction. The
paper also says that VGG-8 and ResNet-18 were trained on MNIST, while the
published PIMCOMP graphs and ResNet Model Zoo artifacts use ImageNet shapes.
Current SHA-256 checksums:
```text
788088b908e233d924c7c26b997e89ee861290c7bc56783a306e8201d79aac8f resnet18/resnet18-v1-7.onnx
c3231061d081bdd47884137b02134f85142752a39e87263c529cd14ed242b096 resnet34/resnet34-v1-7.onnx
c99c507058eaf41de8723408fdda7db8325cb57f0a89f2ee07a716d6e963e14e googlenet/googlenet-12.onnx
a35bad96441efbee28699cb61d1656cca7f7281f14040cf01699c3d0cfd8b202 googlenet/googlenet-12-no-softmax.onnx
396cdea21e5e7d02c3f26f14d22ef20975171702493f5c5e79b8e0d896e541ef vgg8/vgg8-mnist-reconstructed.onnx
```
## Paper hardware profiles
The files in `configs/` encode Table V's explicit resource parameters.
| Config | Cores | Crossbars/core | Crossbar | Cell | PIMCOMP layout |
| --- | ---: | ---: | --- | ---: | --- |
| `arch-a.json` | 168 | 96 | `128x128` | 2-bit | `12x14` |
| `arch-b.json` | 138 | 128 | `128x128` | 2-bit | `6x23` |
| `arch-c.json` | 64 (16 chips x 4) | 8 | `512x1024` | 2-bit | flattened `8x8` |
`adc_count` is 16, matching the paper's 16-bit fixed-point weight precision.
The paper does not give a two-dimensional core topology for Arch-A/B, so the
factorizations above preserve core count but cannot reproduce unpublished NoC
placement details. Released PIMCOMP-NN has no chip-count field; Arch-C is
therefore flattened to 64 cores and does not model chip boundaries.
The remaining latency and power values come from PIMCOMP-NN's released default
configuration. Consequently, instruction/resource comparisons are
reproducible, but absolute paper power and energy numbers are not.
## Build and validate the ONNX files
From the Raptor repository root:
```bash
.venv/bin/python -m pip install numpy onnx onnxruntime onnxsim colorama
cmake --build ./build_release
cmake --build third_party/PIMCOMP-NN/build --target PIMCOMP-NN
.venv/bin/python -c \
'from pathlib import Path; import onnx; [onnx.checker.check_model(onnx.load(p)) for p in Path("validation/networks/pimcomp_models").glob("*/*.onnx")]'
```
Do not build either project with `ninja` directly.
## Compile with PIMCOMP
PIMCOMP-NN reads `third_party/PIMCOMP-NN/config.json` directly. Back it up,
select one paper profile, and restore it when the shell exits:
```bash
RAPTOR_ROOT=$PWD
PIMCOMP="$RAPTOR_ROOT/third_party/PIMCOMP-NN"
PAPER_MODELS="$RAPTOR_ROOT/validation/networks/pimcomp_models"
CONFIG_BACKUP=$(mktemp)
cp "$PIMCOMP/config.json" "$CONFIG_BACKUP"
trap 'cp "$CONFIG_BACKUP" "$PIMCOMP/config.json"' EXIT
cp "$PAPER_MODELS/configs/arch-a.json" "$PIMCOMP/config.json"
```
The Model Zoo files map exactly to PIMCOMP's bundled model names, so compile
them directly:
```bash
cd "$PIMCOMP/build"
# High-throughput mode; the paper evaluates batches of 128 samples.
./PIMCOMP-NN -m=resnet18 -r=balance -p=batch -o=YES -v=YES -s=YES
./PIMCOMP-NN -m=resnet34 -r=balance -p=batch -o=YES -v=YES -s=YES
./PIMCOMP-NN -m=googlenet -r=balance -p=batch -o=YES -v=YES -s=YES
# Low-latency mode; the paper uses batch size 1.
./PIMCOMP-NN -m=resnet18 -r=balance -p=element -o=YES -v=YES -s=YES
./PIMCOMP-NN -m=resnet34 -r=balance -p=element -o=YES -v=YES -s=YES
./PIMCOMP-NN -m=googlenet -r=balance -p=element -o=YES -v=YES -s=YES
```
VGG-8 first needs PIMCOMP's JSON frontend. Use a temporary ONNX copy because
the released frontend rewrites the input batch dimension in place:
```bash
cd /path/to/Raptor
cp validation/networks/pimcomp_models/vgg8/vgg8-mnist-reconstructed.onnx /tmp/vgg8-pimcomp.onnx
.venv/bin/python third_party/PIMCOMP-NN/frontend/frontend.py \
--model_path /tmp/vgg8-pimcomp.onnx \
--save_path third_party/PIMCOMP-NN/models/JSON/vgg8_paper_reconstructed.json
cd third_party/PIMCOMP-NN/build
./PIMCOMP-NN -m=vgg8_paper_reconstructed -r=balance -p=batch -o=YES -v=YES -s=YES
./PIMCOMP-NN -m=vgg8_paper_reconstructed -r=balance -p=element -o=YES -v=YES -s=YES
```
Repeat after selecting `arch-b.json` and `arch-c.json`. All four models were
compiled successfully in both modes with all three configs. The released
random placement code occasionally segfaults; an unchanged retry succeeded in
the observed cases.
The paper's optimizer uses a genetic algorithm with population 200 and up to
1000 iterations. Select it with `-r=GA` for optimizer studies. The released
source keeps population 200 but sets `max_iteration = 3`, so reproducing the
paper's optimization search also requires changing that value in
`backend/GeneticAlgorithm.h`. GA allocates roughly 32 GB in its fast evaluator;
use monolith below instead of reducing cores or crossbars when local RAM is
insufficient.
## Compare Raptor and PIMCOMP
The comparison driver uses one random input and one native ONNX-MLIR reference,
compiles both instruction streams, validates both through Raptor's Rust
simulator, and writes Markdown and JSON reports.
To reproduce the complete Arch-A latency experiment, use the serial experiment
runner. It creates an isolated PIMCOMP build with population 200 and 1000 GA
iterations, runs only the `element`/batch-1 latency pipeline, and invokes the
comparison driver for one model at a time:
```bash
.venv/bin/python validation/tools/run_pimcomp_paper_latency.py \
--out-dir /tmp/raptor-pimcomp-paper-latency
```
Reports are written under `<out-dir>/<model>/comparison_report.{md,json}`.
Use `--models vgg8` to run one model, `--resume` after an interruption, or
`--dry-run` to inspect every command. The runner continues after a failed model
so all reports are produced, then returns a nonzero status if any comparison
failed.
Arch-A low-latency example:
```bash
RAPTOR_ROOT=$PWD
PIMCOMP="$RAPTOR_ROOT/third_party/PIMCOMP-NN"
CONFIG_BACKUP=$(mktemp)
cp "$PIMCOMP/config.json" "$CONFIG_BACKUP"
trap 'cp "$CONFIG_BACKUP" "$PIMCOMP/config.json"' EXIT
cp "$RAPTOR_ROOT/validation/networks/pimcomp_models/configs/arch-a.json" "$PIMCOMP/config.json"
"$RAPTOR_ROOT/.venv/bin/python" "$RAPTOR_ROOT/validation/tools/compare_raptor_pimcomp.py" \
--model "$RAPTOR_ROOT/validation/networks/pimcomp_models/vgg8/vgg8-mnist-reconstructed.onnx" \
--out-dir /tmp/compare-vgg8-arch-a-ll \
--core-count 168 \
--crossbar-count 96 \
--crossbar-size 128 \
--mesh-rows 12 \
--mesh-cols 14 \
--pimsim-mode latency \
--pimcomp-pipeline element \
--fail-on-error
```
For Arch-A high throughput, use `--pimsim-mode throughput
--pimcomp-pipeline batch`. For Arch-B, use 138 cores, 128 crossbars, a
`6x23` mesh, and `configs/arch-b.json`.
If only semantic and instruction comparison is required, add
`--skip-pimsim-nn`. This exact VGG-8 Arch-A LL smoke test passed both semantic
validations with maximum output differences below `5e-10`.
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.
- 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.
Do not change the hardware profile to bypass either limitation; that would no
longer be a paper-matched comparison.
## Monolith fallback
The local `monolith` SSH alias points to the high-memory host. Copy only this
suite and the comparison driver; `-L` materializes the ResNet-18 symlink because
the canonical `resnetv2/depth_68` file may not exist remotely:
```bash
REMOTE_REPO=/home/gmagnani/Project/Raptor
rsync -azL validation/networks/pimcomp_models/ \
"monolith:$REMOTE_REPO/validation/networks/pimcomp_models/"
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 --exclude=.git --exclude=build --exclude=output \
third_party/PIMCOMP-NN/ \
"monolith:$REMOTE_REPO/third_party/PIMCOMP-NN/"
```
Then use the same commands over SSH:
```bash
ssh monolith
cd /home/gmagnani/Project/Raptor
# One-time setup if the repository virtual environment is absent.
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 \
--out-dir /tmp/raptor-pimcomp-paper-latency
```
Copy reports back without transferring large compiler artifacts:
```bash
rsync -az --include='*/' --include='comparison_report.*' --exclude='*' \
monolith:/tmp/raptor-pimcomp-paper-latency/ \
/tmp/raptor-pimcomp-paper-latency/
```
@@ -0,0 +1,33 @@
# 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 \
--out-dir /tmp/raptor-pimcomp-paper-latency \
--models googlenet
```
See [README.md](README.md) for model provenance, limitations, and monolith
instructions.
@@ -0,0 +1,54 @@
{
"chip_config": {
"core_config": {
"period": 1,
"matrix_config": {
"xbar_array_count": 96,
"period": 1,
"pipeline_mode": true,
"dac_resolution": 1,
"dac_count": 128,
"xbar_size": [128, 128],
"cell_precision": 2,
"xbar_latency": 7,
"xbar_read_power": 22.24,
"sample_hold_latency_cycle": 1,
"adc_resolution": 8,
"adc_latency_cycle": 20,
"adc_static_power": 0.322,
"adc_dynamic_power": 1.135,
"adc_count": 16,
"shift_adder_latency_cycle": 1,
"output_buffer_latency_cycle": 1
},
"vector_width": 32,
"vector_latency_cycle": 4,
"local_memory_config": {
"data_width": 64,
"period": 1,
"write_latency_cycle": 30,
"read_latency_cycle": 20
},
"global_memory_switch_id": -10
},
"global_memory_config": {
"data_width": 32,
"period": 1,
"write_latency_cycle": 50,
"read_latency_cycle": 40
},
"network_config": {
"bus_topology": "mesh",
"bus_width": 32,
"layout": [12, 14],
"net_config_file_path": "network_mesh_168.json"
},
"core_cnt": 168,
"global_memory_switch_id": -10
},
"sim_config": {
"sim_mode": 0,
"sim_time": 200,
"report_verbose_level": 0
}
}
@@ -0,0 +1,54 @@
{
"chip_config": {
"core_config": {
"period": 1,
"matrix_config": {
"xbar_array_count": 128,
"period": 1,
"pipeline_mode": true,
"dac_resolution": 1,
"dac_count": 128,
"xbar_size": [128, 128],
"cell_precision": 2,
"xbar_latency": 7,
"xbar_read_power": 22.24,
"sample_hold_latency_cycle": 1,
"adc_resolution": 8,
"adc_latency_cycle": 20,
"adc_static_power": 0.322,
"adc_dynamic_power": 1.135,
"adc_count": 16,
"shift_adder_latency_cycle": 1,
"output_buffer_latency_cycle": 1
},
"vector_width": 32,
"vector_latency_cycle": 4,
"local_memory_config": {
"data_width": 64,
"period": 1,
"write_latency_cycle": 30,
"read_latency_cycle": 20
},
"global_memory_switch_id": -10
},
"global_memory_config": {
"data_width": 32,
"period": 1,
"write_latency_cycle": 50,
"read_latency_cycle": 40
},
"network_config": {
"bus_topology": "mesh",
"bus_width": 32,
"layout": [6, 23],
"net_config_file_path": "network_mesh_138.json"
},
"core_cnt": 138,
"global_memory_switch_id": -10
},
"sim_config": {
"sim_mode": 0,
"sim_time": 200,
"report_verbose_level": 0
}
}
@@ -0,0 +1,54 @@
{
"chip_config": {
"core_config": {
"period": 1,
"matrix_config": {
"xbar_array_count": 8,
"period": 1,
"pipeline_mode": true,
"dac_resolution": 1,
"dac_count": 128,
"xbar_size": [512, 1024],
"cell_precision": 2,
"xbar_latency": 7,
"xbar_read_power": 22.24,
"sample_hold_latency_cycle": 1,
"adc_resolution": 8,
"adc_latency_cycle": 20,
"adc_static_power": 0.322,
"adc_dynamic_power": 1.135,
"adc_count": 16,
"shift_adder_latency_cycle": 1,
"output_buffer_latency_cycle": 1
},
"vector_width": 32,
"vector_latency_cycle": 4,
"local_memory_config": {
"data_width": 64,
"period": 1,
"write_latency_cycle": 30,
"read_latency_cycle": 20
},
"global_memory_switch_id": -10
},
"global_memory_config": {
"data_width": 32,
"period": 1,
"write_latency_cycle": 50,
"read_latency_cycle": 40
},
"network_config": {
"bus_topology": "mesh",
"bus_width": 32,
"layout": [8, 8],
"net_config_file_path": "network_mesh_64.json"
},
"core_cnt": 64,
"global_memory_switch_id": -10
},
"sim_config": {
"sim_mode": 0,
"sim_time": 200,
"report_verbose_level": 0
}
}
@@ -0,0 +1 @@
../../resnetv2/depth_68/resnetv2_depth_68.onnx
Binary file not shown.
+193 -84
View File
@@ -186,26 +186,48 @@ def remove_tree(path: Path) -> None:
def load_model_inputs(model_path: Path, seed: int):
model = onnx.load(model_path)
initializer_names = {init.name for init in model.graph.initializer}
initializer_values = {
init.name: onnx.numpy_helper.to_array(init) for init in model.graph.initializer
}
inputs_desc, outputs_desc = onnx_io(model_path)
runtime_desc = [desc for desc in inputs_desc if desc[1] not in initializer_names]
runtime_arrays, _ = gen_random_inputs(runtime_desc, seed=seed)
arrays_in_order, _ = gen_random_inputs(inputs_desc, seed=seed)
return inputs_desc, outputs_desc, arrays_in_order, arrays_in_order
runtime_by_name = {
desc[1]: arr for desc, arr in zip(runtime_desc, runtime_arrays)
}
arrays_in_order = []
for _, name, elem_type, _ in inputs_desc:
if name in initializer_values:
arrays_in_order.append(initializer_values[name].astype(_ONNX_TO_NP[elem_type], copy=False))
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]]:
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
def prepare_pimcomp_model(model_path: Path, out_dir: Path) -> Path:
out_dir.mkdir(parents=True, exist_ok=True)
output_path = out_dir / f"{model_path.stem}_pimcomp.onnx"
model = onnx.load(model_path)
if any(node.op_type == "BatchNormalization" for node in model.graph.node):
from onnxsim import simplify
model, equivalent = simplify(model, check_n=1)
if not equivalent:
raise RuntimeError("Conv+BatchNormalization folding changed the model output")
if any(node.op_type == "BatchNormalization" for node in model.graph.node):
import onnxruntime as ort
options = ort.SessionOptions()
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
options.optimized_model_filepath = str(output_path)
ort.InferenceSession(str(model_path), options, providers=["CPUExecutionProvider"])
model = onnx.load(output_path)
if any(node.op_type == "BatchNormalization" for node in model.graph.node):
raise RuntimeError("PIMCOMP model preparation did not eliminate BatchNormalization")
else:
arrays_in_order.append(runtime_by_name[name])
runtime_only = [arr for desc, arr in zip(inputs_desc, arrays_in_order) if desc[1] not in initializer_names]
return inputs_desc, outputs_desc, arrays_in_order, runtime_only
onnx.save(model, output_path)
else:
shutil.copy2(model_path, output_path)
return output_path
def compare_simulator_outputs(
@@ -215,11 +237,15 @@ def compare_simulator_outputs(
*,
threshold: float,
rtol: float,
channel_last: bool = False,
) -> CompareResult:
sim_arrays = parse_pim_simulator_outputs(output_bin, outputs_desc)
max_diffs: dict[str, float] = {}
passed = True
for sim_array, (idx, name, _, shape) in zip(sim_arrays, outputs_desc):
if channel_last and len(shape) == 4:
n, c, h, w = shape
sim_array = sim_array.reshape(n, h, w, c).transpose(0, 3, 1, 2)
csv_name = reference_dir / f"output{idx}_{sanitize_output_name(name)}.csv"
ref = np.loadtxt(csv_name, delimiter=",", dtype=np.float32).reshape(shape)
diff = np.abs(sim_array.astype(np.float64) - ref.astype(np.float64))
@@ -254,9 +280,9 @@ def load_effective_hardware(args: argparse.Namespace) -> dict[str, int]:
def write_pimsim_config(args: argparse.Namespace, out_dir: Path, hardware: dict[str, int]) -> Path:
mesh_builder = load_mesh_builder()
example_config = REPO / "backend-simulators/pim/pimsim-nn/example/config/latency_config.json"
with open(example_config, "r", encoding="utf-8") as f:
with open(args.pimcomp_dir / "config.json", "r", encoding="utf-8") as f:
config = json.load(f)
config["chip_config"]["core_config"].setdefault("rob_size", 1)
config["chip_config"]["core_config"]["matrix_config"]["xbar_array_count"] = hardware["crossbar_count"]
config["chip_config"]["core_config"]["matrix_config"]["xbar_size"] = [
hardware["crossbar_size"],
@@ -307,7 +333,7 @@ def compile_reference(
run_logged(
"Reference Emit ONNX IR",
[str(args.raptor_path), str(model_path), "-o", str(onnx_ir_base), "--EmitONNXIR",
"--mlir-elide-elementsattrs-if-larger=16"],
"--mlir-elide-elementsattrs-if-larger=16", "--enable-conv-opt-pass=false"],
cwd=REPO,
timeout_sec=args.timeout_seconds,
steps=steps,
@@ -433,6 +459,8 @@ def run_rust_validation(
reference_dir: Path,
steps: list[StepRecord],
args: argparse.Namespace,
*,
channel_last: bool = False,
) -> CompareResult:
output_bin = pim_dir.parent / "semantic_validation" / "out.bin"
dump_ranges = build_dump_ranges(config_path, outputs_desc)
@@ -468,13 +496,14 @@ def run_rust_validation(
reference_dir,
threshold=args.threshold,
rtol=args.rtol,
channel_last=channel_last,
)
def copy_pimcomp_outputs(args: argparse.Namespace, out_dir: Path):
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"):
shutil.copy2(args.pimcomp_dir / "output" / name, out_dir / name)
shutil.copy2(source_dir / name, out_dir / name)
def compile_pimcomp(
@@ -484,10 +513,10 @@ def compile_pimcomp(
steps: list[StepRecord],
) -> tuple[Path, Path]:
out_dir.mkdir(parents=True, exist_ok=True)
model_name = f"compare_{model_path.stem}"
model_name = args.pimcomp_model_name or f"compare_{model_path.stem}"
frontend_json = args.pimcomp_dir / "models/JSON" / f"{model_name}.json"
frontend_cmd = [
"python3",
sys.executable,
"frontend.py",
"--model_path",
str(model_path),
@@ -504,7 +533,8 @@ def compile_pimcomp(
backend_cmd = [
str(args.pimcomp_dir / "build" / "PIMCOMP-NN"),
f"-m={model_name}",
"-p=batch",
f"-r={args.pimcomp_replication}",
f"-p={args.pimcomp_pipeline}",
"-v=YES",
"-s=YES",
]
@@ -515,7 +545,7 @@ def compile_pimcomp(
timeout_sec=args.timeout_seconds,
steps=steps,
)
copy_pimcomp_outputs(args, out_dir)
copy_pimcomp_outputs(args.pimcomp_dir / "output", out_dir)
return out_dir / "VerificationInfo.json", out_dir / "SimulationInfo.gz"
@@ -527,24 +557,20 @@ def export_pimcomp_for_pimsim_nn(simulation_info: Path, output_dir: Path) -> Pat
output_dir.mkdir(parents=True, exist_ok=True)
sim_config = sim_info["config"]
present_core_indices = sorted(
int(key[4:]) for key, value in sim_info.items() if key.startswith("core") and isinstance(value, list) and value
)
if not present_core_indices:
raise ValueError("PIMCOMP SimulationInfo.gz does not contain any non-empty core instruction streams")
expected_core_indices = list(range(present_core_indices[-1] + 1))
if present_core_indices != expected_core_indices:
raise ValueError(f"PIMCOMP core numbering is not contiguous: {present_core_indices}")
core_count = int(sim_config["core_cnt"])
if core_count <= 0:
raise ValueError("PIMCOMP SimulationInfo.gz must configure at least one core")
core_indices = range(core_count)
config = {
"core_cnt": len(present_core_indices),
"core_cnt": core_count,
"xbar_size": sim_config["xbar_size"],
"xbar_array_count": sim_config["xbar_array_count"],
"cell_precision": sim_config["cell_precision"],
"adc_count": sim_config["adc_count"],
"array_group_map": {},
}
for core_idx in present_core_indices:
for core_idx in core_indices:
core_name = f"core{core_idx}"
config["array_group_map"][core_name] = sim_config["array_group_map"].get(core_name, [])
@@ -552,9 +578,9 @@ def export_pimcomp_for_pimsim_nn(simulation_info: Path, output_dir: Path) -> Pat
json.dump(config, f, separators=(",", ":"))
f.write("\n")
for core_idx in present_core_indices:
for core_idx in core_indices:
core_key = f"core{core_idx}"
instructions = sim_info[core_key]
instructions = sim_info.get(core_key, []) or [{"op": "lldi", "rd": 0, "imm": 0, "len": 0}]
with open(output_dir / f"core_{core_idx}.json", "w", encoding="utf-8") as f:
json.dump(instructions, f, separators=(",", ":"))
f.write("\n")
@@ -652,11 +678,12 @@ def export_pimcomp_for_rust(
core_dir.mkdir(parents=True, exist_ok=True)
local_to_global = local_group_map.get(core_idx, {})
ag_counts = sim_info["config"]["array_group_map"].get(core_name, [])
group_prefix = []
local_group_to_physical = {}
total_crossbars = 0
for count in ag_counts:
group_prefix.append(total_crossbars)
total_crossbars += count
for local_group in sorted(local_to_global):
width = ag_counts[local_group]
local_group_to_physical[local_group] = total_crossbars
total_crossbars += width
config["array_group_map"][core_name] = list(range(total_crossbars))
for local_group, global_ag in sorted(local_to_global.items()):
@@ -664,7 +691,7 @@ def export_pimcomp_for_rust(
weight_name = output_to_weight[info["node_name"]]
matrix = gemm_weights[weight_name]
row_slice = slice(info["height_start"], info["height_end"] + 1)
first_physical = group_prefix[local_group]
first_physical = local_group_to_physical[local_group]
for crossbar_idx, crossbar in enumerate(info["crossbar"]):
col_slice = slice(crossbar["width_start"], crossbar["width_end"] + 1)
tile = np.zeros((xbar_size, col_slice.stop - col_slice.start), dtype=np.float32)
@@ -700,7 +727,13 @@ def export_pimcomp_for_rust(
)
if op == "ld":
if ver_inst["stage"] == "INPUT":
src = input_addr + exporter.byte_offset(ver_inst["source_offset"])
if ver_inst["node_index"] == 1:
src = input_addr + exporter.byte_offset(ver_inst["source_offset"])
else:
provider_index = -ver_inst["source_address"]
src = output_base + exporter.byte_offset(
provider_index * max_output + ver_inst["source_offset"]
)
elif ver_inst["stage"] == "BIAS":
src = bias_addrs[node_list[ver_inst["node_index"]]["name"]] + exporter.byte_offset(ver_inst["source_offset"])
else:
@@ -742,7 +775,7 @@ def export_pimcomp_for_rust(
elif op == "mvmul":
local_group = sim_inst["group"]
global_ag = local_to_global[local_group]
first_physical = group_prefix[local_group]
first_physical = local_group_to_physical[local_group]
widths = [
crossbar["width_end"] - crossbar["width_start"] + 1
for crossbar in ag_info[global_ag]["crossbar"]
@@ -774,7 +807,7 @@ def export_pimcomp_for_rust(
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.]+)\s+samples/s",
"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",
@@ -987,12 +1020,16 @@ def write_report(
pimcomp_instr: dict[str, Any],
raptor_pass_timings: dict[str, float],
pimsim_mode: str,
pimcomp_pipeline: str,
pimcomp_replication: str,
):
lines = [
"# Raptor vs PIMCOMP Comparison Report",
"",
f"- Model: `{model_path}`",
f"- Hardware: `{hardware.get('core_count', 'n/a')} cores`, `{hardware.get('crossbar_count', 'n/a')} xbars/core`, `{hardware.get('crossbar_size', 'n/a')}x{hardware.get('crossbar_size', 'n/a')}` crossbars, mesh `{hardware.get('mesh_rows', 'n/a')}x{hardware.get('mesh_cols', 'n/a')}`",
f"- PIMCOMP pipeline: `{pimcomp_pipeline}`",
f"- PIMCOMP replication: `{pimcomp_replication}`",
"",
]
@@ -1152,15 +1189,34 @@ def main():
parser.add_argument("--mesh-cols", type=int)
parser.add_argument("--pimsim-time-ms", type=int, default=1000)
parser.add_argument("--pimsim-mode", choices=["latency", "throughput"], default="latency")
parser.add_argument("--pimcomp-pipeline", choices=["element", "batch"])
parser.add_argument("--pimcomp-model-name", help="Use a PIMCOMP built-in model name such as vgg16.")
parser.add_argument(
"--pimcomp-replication",
choices=["balance", "W0H0", "uniform", "GA"],
default="balance",
)
parser.add_argument(
"--reuse-raptor-report",
type=Path,
help="Reuse Raptor artifacts and results from an existing comparison_report.json.",
)
parser.add_argument(
"--reuse-pimcomp-dir",
type=Path,
help="Reuse a directory containing PIMCOMP SimulationInfo.gz, VerificationInfo.json, and MappingResult.txt.",
)
parser.add_argument("--skip-pimsim-nn", action="store_true")
parser.add_argument("--verbose-raptor-compile", action="store_true")
parser.add_argument("--raptor-extra-arg", action="append", default=[])
parser.add_argument(
"--fail-on-error",
action="store_true",
help="Return a non-zero process status after writing the reports if any compilation/run stage failed.",
help="Return a non-zero status if a stage or semantic validation fails.",
)
args = parser.parse_args()
if args.pimcomp_pipeline is None:
args.pimcomp_pipeline = "element" if args.pimsim_mode == "latency" else "batch"
model_path = args.model.resolve()
out_dir = args.out_dir.resolve()
@@ -1187,7 +1243,9 @@ def main():
verification_info: Path | None = None
simulation_info: Path | None = None
pimcomp_export_dir: Path | None = None
pimcomp_model_path: Path | None = None
pimsim_config: Path | None = None
reuse_raptor = args.reuse_raptor_report is not None
raptor_validation = skipped_validation("Raptor validation did not run")
pimcomp_validation = skipped_validation("PIMCOMP validation did not run")
@@ -1204,25 +1262,42 @@ def main():
if model_io is not None:
inputs_desc, outputs_desc, arrays_in_order, runtime_inputs = model_io
if reuse_raptor and model_io is not None:
reuse_report_path = args.reuse_raptor_report.resolve()
with open(reuse_report_path, "r", encoding="utf-8") as f:
reused = json.load(f)
reused_hardware = reused["hardware"]
if reused_hardware != hardware:
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, inputs_desc, reuse_report_path.parent / "inputs")
raptor_validation = CompareResult(**reused["raptor_validation"])
raptor_perf = reused["raptor_performance"]
raptor_instr = reused["raptor_instruction_summary"]
raptor_pass_timings = reused["raptor_pass_timings"]
print(f"\n[Reuse Raptor]\n Report: {reuse_report_path}")
expected_runner_path = out_dir / "runner" / "build" / "runner"
reference_compile = try_stage(
failures,
"Compile reference",
compile_reference,
args,
model_path,
out_dir,
steps,
)
if reference_compile is not None:
runner_path = reference_compile
else:
if expected_runner_path.exists():
runner_path = expected_runner_path
print(f"\n[Continue] Reusing partial runner: {runner_path}")
if not reuse_raptor:
reference_compile = try_stage(
failures,
"Compile reference",
compile_reference,
args,
model_path,
out_dir,
steps,
)
if reference_compile is not None:
runner_path = reference_compile
else:
if expected_runner_path.exists():
runner_path = expected_runner_path
print(f"\n[Continue] Reusing partial runner: {runner_path}")
if runner_path is not None and runner_path.exists() and model_io is not None:
if not reuse_raptor and runner_path is not None and runner_path.exists() and model_io is not None:
generated_reference = try_stage(
failures,
"Run reference",
@@ -1237,14 +1312,14 @@ def main():
)
if generated_reference is not None:
reference_dir = generated_reference
else:
elif not reuse_raptor:
record_failure(
failures,
"Skip reference outputs",
"Reference outputs were skipped because the native runner or model inputs are not available.",
)
if model_path.exists() and hardware["core_count"] > 0:
if not reuse_raptor and model_path.exists() and hardware["core_count"] > 0:
compiled_raptor = try_stage(
failures,
"Compile Raptor PIM",
@@ -1257,14 +1332,14 @@ def main():
)
if compiled_raptor is not None:
raptor_pim_dir, raptor_pass_timings = compiled_raptor
else:
elif not reuse_raptor:
record_failure(
failures,
"Skip Raptor PIM compile",
"Raptor PIM compile was skipped because the ONNX model or hardware configuration is not available.",
)
if raptor_pim_dir is not None:
if not reuse_raptor and raptor_pim_dir is not None:
wrote_inputs = try_stage_success(
failures,
"Write Raptor inputs",
@@ -1293,27 +1368,49 @@ def main():
raptor_validation = skipped_validation("Output descriptors are not available")
else:
raptor_validation = skipped_validation("Raptor input materialization failed")
else:
elif not reuse_raptor:
raptor_validation = skipped_validation("Raptor PIM compilation did not produce a PIM directory")
compiled_pimcomp = try_stage(
pimcomp_model_path = try_stage(
failures,
"Compile PIMCOMP",
compile_pimcomp,
args,
"Prepare PIMCOMP model",
prepare_pimcomp_model,
model_path,
out_dir / "pimcomp",
steps,
out_dir / "pimcomp_model",
)
if compiled_pimcomp is not None:
verification_info, simulation_info = compiled_pimcomp
if args.reuse_pimcomp_dir is not None:
reused_pimcomp_dir = args.reuse_pimcomp_dir.resolve()
copied_pimcomp = try_stage_success(
failures,
"Reuse PIMCOMP outputs",
copy_pimcomp_outputs,
reused_pimcomp_dir,
out_dir / "pimcomp",
)
if copied_pimcomp:
verification_info = out_dir / "pimcomp/VerificationInfo.json"
simulation_info = out_dir / "pimcomp/SimulationInfo.gz"
print(f"\n[Reuse PIMCOMP]\n Directory: {reused_pimcomp_dir}")
else:
compiled_pimcomp = try_stage(
failures,
"Compile PIMCOMP",
compile_pimcomp,
args,
pimcomp_model_path,
out_dir / "pimcomp",
steps,
) if pimcomp_model_path is not None else None
if compiled_pimcomp is not None:
verification_info, simulation_info = compiled_pimcomp
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_rust,
model_path,
pimcomp_model_path,
verification_info,
simulation_info,
runtime_inputs,
@@ -1346,6 +1443,7 @@ def main():
reference_dir,
steps,
args,
channel_last=True,
)
pimcomp_validation = validation if validation is not None else failed_validation("PIMCOMP validation failed")
elif pimcomp_export_dir is None:
@@ -1374,13 +1472,15 @@ def main():
)
if args.skip_pimsim_nn:
raptor_perf = skipped_perf("Skipped by --skip-pimsim-nn")
if not reuse_raptor:
raptor_perf = skipped_perf("Skipped by --skip-pimsim-nn")
pimcomp_perf = skipped_perf("Skipped by --skip-pimsim-nn")
elif pimsim_config is None:
raptor_perf = skipped_perf("pimsim-nn config is not available")
if not reuse_raptor:
raptor_perf = skipped_perf("pimsim-nn config is not available")
pimcomp_perf = skipped_perf("pimsim-nn config is not available")
else:
if raptor_pim_dir is not None:
if not reuse_raptor and raptor_pim_dir is not None:
perf = try_stage(
failures,
"pimsim-nn Raptor",
@@ -1393,7 +1493,7 @@ def main():
args,
)
raptor_perf = perf if perf is not None else failed_perf("pimsim-nn Raptor failed")
else:
elif not reuse_raptor:
raptor_perf = skipped_perf("Raptor PIM directory is not available")
if simulation_info is not None:
@@ -1422,10 +1522,10 @@ def main():
else:
pimcomp_perf = skipped_perf("PIMCOMP SimulationInfo.gz is not available")
if raptor_pim_dir is not None and raptor_pim_dir.exists():
if not reuse_raptor and raptor_pim_dir is not None and raptor_pim_dir.exists():
parsed = try_stage(failures, "Parse Raptor instructions", parse_raptor_instructions, raptor_pim_dir)
raptor_instr = parsed if parsed is not None else empty_instruction_summary(error="Failed to parse Raptor instructions")
else:
elif not reuse_raptor:
raptor_instr = empty_instruction_summary("Raptor PIM directory is not available")
if simulation_info is not None and simulation_info.exists():
@@ -1449,12 +1549,17 @@ def main():
pimcomp_instr=pimcomp_instr,
raptor_pass_timings=raptor_pass_timings,
pimsim_mode=args.pimsim_mode,
pimcomp_pipeline=args.pimcomp_pipeline,
pimcomp_replication=args.pimcomp_replication,
)
json_report = {
"model": str(model_path),
"hardware": hardware,
"pimsim_mode": args.pimsim_mode,
"pimcomp_pipeline": args.pimcomp_pipeline,
"pimcomp_replication": args.pimcomp_replication,
"reused_raptor_report": optional_path(args.reuse_raptor_report.resolve()) if reuse_raptor else None,
"failures": failures,
"steps": [asdict(step) for step in steps],
"raptor_validation": asdict(raptor_validation),
@@ -1484,7 +1589,11 @@ def main():
if failures or any(step.status != "passed" for step in steps):
print(f" Completed with {len(failures)} recorded failure/skipped stage(s).")
if args.fail_on_error and (failures or any(step.status != "passed" for step in steps)):
semantic_failure = any(
result.status == "done" and not result.passed
for result in (raptor_validation, pimcomp_validation)
)
if args.fail_on_error and (failures or any(step.status != "passed" for step in steps) or semantic_failure):
raise SystemExit(1)
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
SUITE = REPO / "validation/networks/pimcomp_models"
PIMCOMP_SOURCE = REPO / "third_party/PIMCOMP-NN"
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(f"$ {shlex.join(command)}", flush=True)
if dry_run:
return 0
return subprocess.run(command, cwd=REPO, check=check).returncode
def prepare_pimcomp(work_dir: Path) -> None:
shutil.copytree(
PIMCOMP_SOURCE,
work_dir,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(".git", "build", "output"),
)
header = work_dir / "backend/GeneticAlgorithm.h"
source = header.read_text(encoding="utf-8")
if "int population_num = 200;" not in source:
raise RuntimeError("PIMCOMP GA population is not 200")
source, replacements = re.subn(
r"int max_iteration = \d+;",
"int max_iteration = 1000;",
source,
)
if replacements != 1:
raise RuntimeError("Could not set PIMCOMP GA max_iteration")
header.write_text(source, encoding="utf-8")
shutil.copy2(SUITE / "configs/arch-a.json", work_dir / "config.json")
def comparison_command(model: Path, result_dir: Path, pimcomp_dir: Path, timeout: float) -> list[str]:
return [
sys.executable,
str(COMPARE),
"--model",
str(model),
"--out-dir",
str(result_dir),
"--pimcomp-dir",
str(pimcomp_dir),
"--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", required=True, type=Path)
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="Keep the existing work tree and 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()
work_dir = out_dir / "pimcomp-ga1000"
if not args.dry_run and out_dir.exists() and any(out_dir.iterdir()) and not args.resume:
parser.error(f"{out_dir} is not empty; choose a fresh directory or pass --resume")
missing = [str(MODELS[name]) for name in args.models if not MODELS[name].exists()]
if missing:
parser.error(f"missing model(s): {', '.join(missing)}")
if args.dry_run:
print(f"# prepare isolated PIMCOMP GA build in {work_dir}")
else:
out_dir.mkdir(parents=True, exist_ok=True)
prepare_pimcomp(work_dir)
run(["cmake", "--build", str(REPO / "build_release")], dry_run=args.dry_run)
run(
["cmake", "-S", str(work_dir), "-B", str(work_dir / "build")],
dry_run=args.dry_run,
)
run(
["cmake", "--build", str(work_dir / "build"), "--target", "PIMCOMP-NN"],
dry_run=args.dry_run,
)
failed = []
for name in args.models:
result_dir = out_dir / name
if args.resume and (result_dir / "comparison_report.json").exists():
print(f"[{name}] completed report exists; skipping", flush=True)
continue
print(f"\n[{name}] Arch-A latency comparison", flush=True)
returncode = run(
comparison_command(MODELS[name], result_dir, work_dir, args.timeout_seconds),
dry_run=args.dry_run,
check=False,
)
if returncode:
failed.append(name)
if failed:
print(f"\nCompleted with failed comparisons: {', '.join(failed)}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
import argparse
from pathlib import Path
import onnx
def split_prefixes(model_path: Path, output_dir: Path, name: str) -> None:
model = onnx.shape_inference.infer_shapes(onnx.load(model_path))
initializer_names = {initializer.name for initializer in model.graph.initializer}
input_names = [value.name for value in model.graph.input if value.name not in initializer_names]
extractor = onnx.utils.Extractor(model)
output_dir.mkdir(parents=True, exist_ok=True)
for depth, node in enumerate(model.graph.node):
output_name = next(output for output in node.output if output)
prefix = extractor.extract_model(input_names, [output_name])
prefix.ir_version = max(prefix.ir_version, 4)
onnx.checker.check_model(prefix)
depth_name = f"depth_{depth:02d}"
depth_dir = output_dir / depth_name
depth_dir.mkdir(parents=True, exist_ok=True)
output_path = depth_dir / f"{name}_{depth_name}.onnx"
onnx.save(prefix, output_path)
print(f"{depth_name}: {node.op_type} -> {output_name} ({len(prefix.graph.node)} nodes)")
def main() -> None:
parser = argparse.ArgumentParser(description="Split an ONNX graph into one ancestor prefix per node.")
parser.add_argument("model", type=Path)
parser.add_argument("output_dir", type=Path)
parser.add_argument("--name", required=True)
args = parser.parse_args()
split_prefixes(args.model, args.output_dir, args.name)
if __name__ == "__main__":
main()