Compare commits
9 Commits
45578ef4c4
...
f3a4e19f7c
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a4e19f7c | |||
| e7611be8e1 | |||
| c59e320efa | |||
| c491078757 | |||
| bb4594fc01 | |||
| 24c6ea3c6e | |||
| b651968f30 | |||
| d88bced271 | |||
| 6bc7709aa4 |
@@ -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
|
||||
|
||||
+37
-10
@@ -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),
|
||||
|
||||
+4
-5
@@ -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();
|
||||
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
../../resnetv2/depth_68/resnetv2_depth_68.onnx
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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())
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user