Merge branch 'TestRottoConDeadLock' of chef.heaplab.deib.polimi.it:nnicolosi/Raptor into TestRottoConDeadLock

This commit is contained in:
ilgeco
2026-07-22 18:04:12 +02:00
45 changed files with 1252 additions and 613 deletions
@@ -32,6 +32,18 @@ schedule semantics, scheduling granularity, and available parallelism.
An optimization is acceptable only when its static runtime proxies are equal or
better.
## Asymptotic cost
For each new or materially changed compiler algorithm, identify the relevant
input size and target linear or sublinear time and space. Avoid repeated full-IR
walks, nested scans, and per-operation recomputation when indexing, caching, or
a single traversal can express the same behavior.
When linear-or-better complexity is not possible, use the lowest justified
complexity and report the actual time and space Big-O, the input variable, and
why a lower bound is not practical. Include that cost in the final report; do
not hide a superlinear path behind small current test sizes.
## Forbidden optimization trades
Do not:
+1 -1
View File
@@ -24,7 +24,7 @@ void dumpModule(mlir::ModuleOp moduleOp, const std::string& name, bool assumeVer
llvm::raw_os_ostream os(file);
mlir::OpPrintingFlags flags;
flags.elideLargeElementsAttrs().enableDebugInfo(false, false);
flags.elideLargeElementsAttrs().enableDebugInfo(true, false);
if (assumeVerified)
flags.assumeVerified();
moduleOp.print(os, flags);
@@ -9,6 +9,53 @@ using namespace mlir;
namespace onnx_mlir {
FailureOr<Value> createFragmentAssemblyBlueprint(Value physicalBatch,
RankedTensorType logicalType,
ArrayRef<FragmentAssemblyEntry> entries,
StringRef physicalLayout,
StringRef indexMap,
PatternRewriter& rewriter,
Location loc) {
auto physicalType = dyn_cast<RankedTensorType>(physicalBatch.getType());
if (!physicalType || !physicalType.hasStaticShape() || !logicalType || !logicalType.hasStaticShape()
|| physicalType.getRank() != logicalType.getRank() + 1 || entries.empty())
return emitError(loc, "invalid static physical batch for fragment assembly"), failure();
const int64_t rank = logicalType.getRank();
const int64_t laneCount = physicalType.getDimSize(0);
if (laneCount <= 0)
return emitError(loc, "fragment assembly requires at least one physical source slot"), failure();
const int64_t fragmentElements = physicalType.getNumElements() / laneCount;
SmallVector<int64_t> operandIndices(entries.size(), 0), sourceSlots, sourceOffsets, offsets, sizes,
strides(entries.size() * rank, 1);
for (const FragmentAssemblyEntry& entry : entries) {
if (entry.sourceSlot < 0 || entry.sourceSlot >= laneCount || entry.sourceOffset < 0
|| entry.destinationOffsets.size() != static_cast<size_t>(rank)
|| entry.sizes.size() != static_cast<size_t>(rank))
return emitError(loc, "invalid fragment assembly entry"), failure();
int64_t entryElements = 1;
for (int64_t dim = 0; dim < rank; ++dim) {
if (entry.destinationOffsets[dim] < 0 || entry.sizes[dim] <= 0
|| entry.destinationOffsets[dim] + entry.sizes[dim] > logicalType.getDimSize(dim))
return emitError(loc, "fragment assembly entry exceeds the logical tensor"), failure();
entryElements *= entry.sizes[dim];
}
if (entry.sourceOffset + entryElements > fragmentElements)
return emitError(loc, "fragment assembly entry exceeds its physical source slot"), failure();
sourceSlots.push_back(entry.sourceSlot);
sourceOffsets.push_back(entry.sourceOffset);
llvm::append_range(offsets, entry.destinationOffsets);
llvm::append_range(sizes, entry.sizes);
}
return spatial::SpatBlueprintOp::create(rewriter, loc, logicalType, physicalBatch, ValueRange {},
rewriter.getStringAttr("nchw"), rewriter.getStringAttr(physicalLayout),
rewriter.getDenseI64ArrayAttr(offsets), rewriter.getDenseI64ArrayAttr(sizes),
rewriter.getStringAttr(indexMap), rewriter.getStringAttr("fragment_assembly"),
rewriter.getDenseI64ArrayAttr(operandIndices), rewriter.getDenseI64ArrayAttr(sourceSlots),
rewriter.getDenseI64ArrayAttr(sourceOffsets), rewriter.getDenseI64ArrayAttr(strides),
rewriter.getStringAttr("disjoint"), rewriter.getStringAttr("complete")).getOutput();
}
Value sumTensors(ArrayRef<Value> tensors, PatternRewriter& rewriter) {
if (tensors.size() == 1)
return tensors[0];
@@ -19,6 +19,13 @@
namespace onnx_mlir {
struct FragmentAssemblyEntry {
int64_t sourceSlot;
int64_t sourceOffset;
llvm::SmallVector<int64_t, 4> destinationOffsets;
llvm::SmallVector<int64_t, 4> sizes;
};
namespace detail {
inline mlir::ValueRange getBlockArgs(mlir::Block* block) { return mlir::ValueRange(block->getArguments()); }
@@ -407,4 +414,12 @@ mlir::Value materializeOrComputeUnary(mlir::Value input,
mlir::Value sumTensors(mlir::ArrayRef<mlir::Value> tensors, mlir::PatternRewriter& rewriter);
mlir::FailureOr<mlir::Value> createFragmentAssemblyBlueprint(mlir::Value physicalBatch,
mlir::RankedTensorType logicalType,
llvm::ArrayRef<FragmentAssemblyEntry> entries,
llvm::StringRef physicalLayout,
llvm::StringRef indexMap,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
} // namespace onnx_mlir
@@ -4,6 +4,7 @@
#include "src/Accelerators/PIM/Common/IR/ConstantUtils.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/BiasAddUtils.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp"
#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp"
#include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp"
#include "src/Dialect/ONNX/ONNXOps.hpp"
@@ -12,6 +13,22 @@ using namespace mlir;
namespace onnx_mlir {
FailureOr<RowStripPhysicalValue> describeRowStripPhysicalValue(Value storage, RankedTensorType logicalType) {
auto storageType = dyn_cast<RankedTensorType>(storage.getType());
if (!storageType || !storageType.hasStaticShape() || !logicalType || !logicalType.hasStaticShape()
|| storageType.getRank() != 5 || logicalType.getRank() != 4 || logicalType.getDimSize(0) != 1
|| storageType.getElementType() != logicalType.getElementType()
|| storageType.getDimSize(1) != 1 || storageType.getDimSize(2) <= 0
|| storageType.getDimSize(3) != 1 || storageType.getDimSize(4) != logicalType.getDimSize(3))
return failure();
const int64_t tilesPerRow = ceilIntegerDivide(logicalType.getDimSize(1), storageType.getDimSize(2));
if (storageType.getDimSize(0) != logicalType.getDimSize(2) * tilesPerRow)
return failure();
return RowStripPhysicalValue {storage, logicalType,
RankedTensorType::get(storageType.getShape().drop_front(), storageType.getElementType(), storageType.getEncoding()),
tilesPerRow};
}
RankedTensorType getRowStripFragmentType(RankedTensorType logicalType) {
return RankedTensorType::get({logicalType.getDimSize(0), logicalType.getDimSize(1), 1, logicalType.getDimSize(3)},
logicalType.getElementType(),
@@ -123,42 +140,39 @@ FailureOr<Value> createRowStripStorageFromRows(Value rows,
return batchOp->getResult(0);
}
FailureOr<Value>
createRowStripAssemblyBlueprint(Value storage, RankedTensorType logicalType, PatternRewriter& rewriter, Location loc) {
auto storageType = dyn_cast<RankedTensorType>(storage.getType());
if (!storageType || storageType != getRowStripStorageType(logicalType))
return failure();
auto [offsets, sizes] = buildRowStripMetadata(logicalType);
int64_t height = logicalType.getDimSize(2);
SmallVector<int64_t> operandIndices(height, 0), sourceSlots, sourceOffsets(height, 0), strides(height * 4, 1);
for (int64_t row = 0; row < height; ++row)
sourceSlots.push_back(row);
return spatial::SpatBlueprintOp::create(rewriter, loc, logicalType, storage, ValueRange {},
rewriter.getStringAttr("nchw"), rewriter.getStringAttr("nchw_row_strip"),
rewriter.getDenseI64ArrayAttr(offsets), rewriter.getDenseI64ArrayAttr(sizes),
rewriter.getStringAttr("nchw_row_strip_fragments"), rewriter.getStringAttr("fragment_assembly"),
rewriter.getDenseI64ArrayAttr(operandIndices), rewriter.getDenseI64ArrayAttr(sourceSlots),
rewriter.getDenseI64ArrayAttr(sourceOffsets), rewriter.getDenseI64ArrayAttr(strides),
rewriter.getStringAttr("disjoint"), rewriter.getStringAttr("complete")).getOutput();
FailureOr<Value> createRowStripAssemblyBlueprint(const RowStripPhysicalValue& value,
PatternRewriter& rewriter,
Location loc) {
SmallVector<FragmentAssemblyEntry> entries;
const int64_t tileChannels = value.fragmentType.getDimSize(1);
for (int64_t row = 0; row < value.logicalType.getDimSize(2); ++row)
for (int64_t tile = 0; tile < value.tilesPerRow; ++tile) {
const int64_t channelOffset = tile * tileChannels;
entries.push_back({row * value.tilesPerRow + tile, 0, {0, channelOffset, row, 0},
{1, std::min(tileChannels, value.logicalType.getDimSize(1) - channelOffset), 1,
value.logicalType.getDimSize(3)}});
}
return createFragmentAssemblyBlueprint(value.storage, value.logicalType, entries, "nchw_row_strip",
kRowStripIndexMap, rewriter, loc);
}
FailureOr<Value>
applyRowStripRelu(Value storage, RankedTensorType logicalType, PatternRewriter& rewriter, Location loc) {
auto fragmentType = getRowStripFragmentType(logicalType);
auto storageType = getRowStripStorageType(logicalType);
FailureOr<Value> applyRowStripRelu(const RowStripPhysicalValue& value, PatternRewriter& rewriter, Location loc) {
auto storageType = cast<RankedTensorType>(value.storage.getType());
const int64_t laneCount = storageType.getDimSize(0);
auto batchOp = createSpatComputeBatch(rewriter,
loc,
TypeRange {storageType},
logicalType.getDimSize(2),
laneCount,
{},
ValueRange {storage},
ValueRange {value.storage},
[&](detail::SpatComputeBatchBodyArgs args) {
Value fragment =
extractRowStripFragment(args.inputs.front(), logicalType, args.lane, rewriter, loc);
fragment = spatial::SpatReluOp::create(rewriter, loc, fragmentType, fragment).getResult();
insertRowStripFragment(
fragment, args.outputs.front(), logicalType, args.lane, rewriter, loc);
FailureOr<Value> fragment = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs.front(), args.lane, value.fragmentType);
if (failed(fragment)) return failure();
Value relu = spatial::SpatReluOp::create(
rewriter, loc, value.fragmentType, *fragment).getResult();
publishGraphBatchPhysicalFragment(
rewriter, loc, relu, args.outputs.front(), args.lane);
return success();
});
if (failed(batchOp))
@@ -166,41 +180,47 @@ applyRowStripRelu(Value storage, RankedTensorType logicalType, PatternRewriter&
return batchOp->getResult(0);
}
FailureOr<Value>
applyRowStripBiasAdd(Value storage, RankedTensorType logicalType, Value bias, PatternRewriter& rewriter, Location loc) {
FailureOr<Value> applyRowStripBiasAdd(const RowStripPhysicalValue& value,
Value bias,
PatternRewriter& rewriter,
Location loc) {
DenseElementsAttr denseAttr;
if (!isSupportedBiasAddValue(bias, logicalType, &denseAttr))
if (!isSupportedBiasAddValue(bias, value.logicalType, &denseAttr))
return failure();
auto fragmentType = getRowStripFragmentType(logicalType);
auto storageType = getRowStripStorageType(logicalType);
FailureOr<SmallVector<Attribute>> channelValues = getBiasChannelValues(denseAttr, value.logicalType);
if (failed(channelValues)) return failure();
auto storageType = cast<RankedTensorType>(value.storage.getType());
auto biasStorageType = spatial::getGraphBatchPhysicalResultType(value.tilesPerRow, value.fragmentType);
SmallVector<Attribute> biasValues(
biasStorageType.getNumElements(), cast<Attribute>(rewriter.getZeroAttr(value.fragmentType.getElementType())));
const int64_t tileChannels = value.fragmentType.getDimSize(1);
const int64_t width = value.fragmentType.getDimSize(3);
for (int64_t channel = 0; channel < value.logicalType.getDimSize(1); ++channel)
for (int64_t w = 0; w < width; ++w)
biasValues[((channel / tileChannels) * tileChannels + channel % tileChannels) * width + w] =
(*channelValues)[channel];
Value biasStorage = getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(),
DenseElementsAttr::get(biasStorageType, biasValues), biasStorageType);
const int64_t laneCount = storageType.getDimSize(0);
auto batchOp = createSpatComputeBatch(rewriter,
loc,
TypeRange {storageType},
logicalType.getDimSize(2),
laneCount,
{},
ValueRange {storage},
ValueRange {value.storage, biasStorage},
[&](detail::SpatComputeBatchBodyArgs args) {
Value fragment =
extractRowStripFragment(args.inputs.front(), logicalType, args.lane, rewriter, loc);
Value constant;
if (denseAttr.isSplat()) {
constant = getOrCreateConstant(
rewriter,
rewriter.getInsertionBlock()->getParentOp(),
DenseElementsAttr::get(fragmentType, denseAttr.getSplatValue<Attribute>()),
fragmentType);
}
else {
FailureOr<Value> perChannel =
createPerChannelConstantFragment(denseAttr, fragmentType, rewriter);
if (failed(perChannel))
return failure();
constant = *perChannel;
}
fragment =
spatial::SpatVAddOp::create(rewriter, loc, fragmentType, fragment, constant).getResult();
insertRowStripFragment(
fragment, args.outputs.front(), logicalType, args.lane, rewriter, loc);
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
FailureOr<Value> fragment = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs[0], args.lane, value.fragmentType);
Value tile = affineModConst(rewriter, loc, args.lane,
value.tilesPerRow, anchorOp);
FailureOr<Value> constant = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs[1], tile, value.fragmentType);
if (failed(fragment) || failed(constant)) return failure();
Value added = spatial::SpatVAddOp::create(
rewriter, loc, value.fragmentType, *fragment, *constant).getResult();
publishGraphBatchPhysicalFragment(
rewriter, loc, added, args.outputs.front(), args.lane);
return success();
});
if (failed(batchOp))
@@ -11,10 +11,13 @@ inline constexpr llvm::StringLiteral kRowStripIndexMap = "nchw_row_strip_fragmen
struct RowStripPhysicalValue {
mlir::Value storage;
mlir::RankedTensorType logicalType;
llvm::SmallVector<int64_t, 16> fragmentOffsets;
llvm::SmallVector<int64_t, 16> fragmentSizes;
mlir::RankedTensorType fragmentType;
int64_t tilesPerRow;
};
mlir::FailureOr<RowStripPhysicalValue> describeRowStripPhysicalValue(mlir::Value storage,
mlir::RankedTensorType logicalType);
std::pair<llvm::SmallVector<int64_t>, llvm::SmallVector<int64_t>>
buildRowStripMetadata(mlir::RankedTensorType type);
@@ -50,18 +53,15 @@ mlir::FailureOr<mlir::Value> createRowStripStorageFromRows(mlir::Value rows,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
mlir::FailureOr<mlir::Value> createRowStripAssemblyBlueprint(mlir::Value storage,
mlir::RankedTensorType logicalType,
mlir::FailureOr<mlir::Value> createRowStripAssemblyBlueprint(const RowStripPhysicalValue& value,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
mlir::FailureOr<mlir::Value> applyRowStripRelu(mlir::Value storage,
mlir::RankedTensorType logicalType,
mlir::FailureOr<mlir::Value> applyRowStripRelu(const RowStripPhysicalValue& value,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
mlir::FailureOr<mlir::Value> applyRowStripBiasAdd(mlir::Value storage,
mlir::RankedTensorType logicalType,
mlir::FailureOr<mlir::Value> applyRowStripBiasAdd(const RowStripPhysicalValue& value,
mlir::Value bias,
mlir::PatternRewriter& rewriter,
mlir::Location loc);
@@ -45,38 +45,30 @@ static FailureOr<RowStripPhysicalValue> buildRowStripValue(spatial::SpatBlueprin
auto logicalType = dyn_cast<RankedTensorType>(blueprint.getOutput().getType());
if (!logicalType)
return blueprint.emitOpError("requires ranked logical output type"), failure();
RowStripPhysicalValue value;
value.storage = storage;
value.logicalType = logicalType;
value.fragmentOffsets.append(blueprint.getFragmentOffsets().begin(), blueprint.getFragmentOffsets().end());
value.fragmentSizes.append(blueprint.getFragmentSizes().begin(), blueprint.getFragmentSizes().end());
if (blueprint.getIndexMap() != kRowStripIndexMap)
return blueprint.emitOpError("requires the canonical row-strip index map"), failure();
auto storageType = dyn_cast<RankedTensorType>(storage.getType());
if (!storageType || storageType != getRowStripStorageType(logicalType))
FailureOr<RowStripPhysicalValue> value = describeRowStripPhysicalValue(storage, logicalType);
if (failed(value))
return blueprint.emitOpError("requires physical row-strip fragment storage"), failure();
return value;
return *value;
}
static FailureOr<Value>
lowerRowStripRelu(const RowStripPhysicalValue& input, spatial::SpatReluPlanOp planOp, PatternRewriter& rewriter) {
return applyRowStripRelu(input.storage, input.logicalType, rewriter, planOp.getLoc());
return applyRowStripRelu(input, rewriter, planOp.getLoc());
}
static FailureOr<Value> lowerRowStripBiasAdd(const RowStripPhysicalValue& input,
spatial::SpatBiasAddPlanOp planOp,
PatternRewriter& rewriter) {
return applyRowStripBiasAdd(input.storage, input.logicalType, planOp.getBias(), rewriter, planOp.getLoc());
return applyRowStripBiasAdd(input, planOp.getBias(), rewriter, planOp.getLoc());
}
static FailureOr<Value>
materializeRowStripToDense(const RowStripPhysicalValue& rowStripValue, Location loc, PatternRewriter& rewriter) {
if (rowStripValue.logicalType.getRank() != 4 || !rowStripValue.logicalType.hasStaticShape())
return failure();
auto [expectedOffsets, expectedSizes] = buildRowStripMetadata(rowStripValue.logicalType);
if (!llvm::equal(rowStripValue.fragmentOffsets, expectedOffsets) || !llvm::equal(rowStripValue.fragmentSizes, expectedSizes))
return failure();
return createRowStripAssemblyBlueprint(rowStripValue.storage, rowStripValue.logicalType, rewriter, loc);
return createRowStripAssemblyBlueprint(rowStripValue, rewriter, loc);
}
static FailureOr<Value> lowerDenseBatchBiasAdd(Value input, Value bias, RankedTensorType resultType,
@@ -168,9 +160,12 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
});
if (rowStripBlueprint != planOp.getResult().getUsers().end()) {
rewriter.setInsertionPoint(planOp);
std::optional<Value> physicalInput;
if (succeeded(rowStripInput))
physicalInput = rowStripInput->storage;
FailureOr<Value> lowered = lowerSelectedConv2DPlan(
planOp,
succeeded(rowStripInput) ? std::optional<Value> {rowStripInput->storage} : std::nullopt,
physicalInput,
/*emitRowStripLayout=*/true,
rewriter);
if (failed(lowered)) {
@@ -255,8 +250,23 @@ struct LowerSpatialPlansPass final : PassWrapper<LowerSpatialPlansPass, Operatio
FailureOr<RowStripPhysicalValue> input = getRowStripValue(rowStripValues, planOp.getInput());
rewriter.setInsertionPoint(planOp);
std::optional<Value> physicalInput;
if (succeeded(input)) {
if (input->tilesPerRow == 1) {
physicalInput = input->storage;
}
else {
FailureOr<Value> denseInput = materializeRowStripToDense(*input, planOp.getLoc(), rewriter);
if (failed(denseInput)) {
planOp.emitOpError("failed to materialize tiled row-strip input for MaxPool");
signalPassFailure();
return;
}
planOp.getInputMutable().assign(*denseInput);
}
}
FailureOr<Value> lowered = lowerSelectedMaxPool2DPlan(
planOp, succeeded(input) ? std::optional<Value> {input->storage} : std::nullopt, rewriter);
planOp, physicalInput, rewriter);
if (failed(lowered)) {
planOp.emitOpError("failed to lower selected row-strip Spatial MaxPool plan");
signalPassFailure();
@@ -2326,54 +2326,25 @@ static Value maybeUnpackChunkRows(Value gemmRows,
return unpackCompute.getResult(0);
}
static Value createChunkedConvRows(const ConvLoweringState& state,
const PreparedConvInput& preparedInput,
Value weightMatrix,
Value biasMatrix,
DenseElementsAttr wDenseAttr,
DenseElementsAttr biasDenseAttr,
int64_t forcedPackFactor,
uint64_t chunkPositions,
PatternRewriter& rewriter,
Location loc) {
SmallVector<Value> chunkRows;
static Value createStreamedConvRows(const ConvLoweringState& state,
const PreparedConvInput& preparedInput,
Value weightMatrix,
Value biasMatrix,
DenseElementsAttr wDenseAttr,
DenseElementsAttr biasDenseAttr,
int64_t forcedPackFactor,
PatternRewriter& rewriter,
Location loc) {
const int64_t totalPatches = state.batchSize * state.outHeight * state.outWidth;
for (int64_t chunkStart = 0; chunkStart < totalPatches; chunkStart += static_cast<int64_t>(chunkPositions)) {
const int64_t chunkNumPatches = std::min<int64_t>(static_cast<int64_t>(chunkPositions), totalPatches - chunkStart);
ConvGemmPlan chunkPlan = buildConvGemmPlan(state,
static_cast<bool>(wDenseAttr),
!state.hasBias || static_cast<bool>(biasDenseAttr),
chunkStart,
chunkNumPatches,
forcedPackFactor);
Value chunkInputRows = createIm2colRows(state, preparedInput, chunkPlan, rewriter, loc);
Value chunkB = buildPackedWeights(wDenseAttr, weightMatrix, state, chunkPlan, rewriter, loc);
Value gemmBias = createZeroGemmBias(chunkPlan.gemmOutputRowsType, rewriter);
if (state.hasBias)
gemmBias = state.b;
Value chunkC = buildPackedBias(gemmBias, biasMatrix, biasDenseAttr, state, chunkPlan, rewriter, loc);
Value chunkGemmRows = ONNXGemmOp::create(rewriter,
loc,
chunkPlan.gemmOutputRowsType,
chunkInputRows,
chunkB,
chunkC,
APFloat(1.0f),
APFloat(1.0f),
/*transA=*/0,
/*transB=*/0)
.getY();
chunkRows.push_back(maybeUnpackChunkRows(chunkGemmRows, chunkPlan, rewriter, loc));
}
if (chunkRows.size() == 1)
return chunkRows.front();
auto rowType = RankedTensorType::get({totalPatches, state.numChannelsOut}, state.outType.getElementType());
auto collectRows = createSpatCompute(rewriter, loc, TypeRange {rowType}, {}, chunkRows, [&](ValueRange rows) {
spatial::SpatYieldOp::create(rewriter, loc, createSpatConcat(rewriter, loc, /*axis=*/0, rows));
});
return collectRows.getResult(0);
ConvGemmPlan plan = buildConvGemmPlan(state, static_cast<bool>(wDenseAttr),
!state.hasBias || static_cast<bool>(biasDenseAttr), 0, totalPatches, forcedPackFactor);
Value inputRows = createIm2colRows(state, preparedInput, plan, rewriter, loc);
Value packedWeights = buildPackedWeights(wDenseAttr, weightMatrix, state, plan, rewriter, loc);
Value gemmBias = state.hasBias ? state.b : createZeroGemmBias(plan.gemmOutputRowsType, rewriter);
Value packedBias = buildPackedBias(gemmBias, biasMatrix, biasDenseAttr, state, plan, rewriter, loc);
Value gemmRows = ONNXGemmOp::create(rewriter, loc, plan.gemmOutputRowsType, inputRows,
packedWeights, packedBias, APFloat(1.0f), APFloat(1.0f), 0, 0).getY();
return maybeUnpackChunkRows(gemmRows, plan, rewriter, loc);
}
static Value rewritePackedIm2ColConv(const ConvLoweringState& state,
@@ -2444,16 +2415,13 @@ static Value rewriteStreamedConv(const ConvLoweringState& state,
ConvGemmPlan seedPlan = buildConvGemmPlan(
state, static_cast<bool>(wDenseAttr), !state.hasBias || static_cast<bool>(biasDenseAttr), 0, 1, forcedPackFactor);
Value weightMatrix = createWeightMatrix(state.w, seedPlan, rewriter, loc);
ConvGeometry geo = buildConvGeometry(state);
uint64_t chunkPositions = chooseStreamChunkPositions(geo, forcedPackFactor);
Value collectedRows = createChunkedConvRows(state,
Value collectedRows = createStreamedConvRows(state,
preparedInput,
weightMatrix,
biasMatrix,
wDenseAttr,
biasDenseAttr,
forcedPackFactor,
chunkPositions,
rewriter,
loc);
auto gemmOutType = cast<RankedTensorType>(collectedRows.getType());
@@ -2524,6 +2492,21 @@ static bool canConsumeNchwRowStripFragments(const ConvLoweringState& state, Stri
failureReason = "dilation_not_one";
return false;
}
ConvGeometry geometry = buildConvGeometry(state);
const bool pointwise = state.xHeight == 1 && state.xWidth == 1 && state.outHeight == 1 && state.outWidth == 1
&& state.wHeight == 1 && state.wWidth == 1 && state.padHeightBegin == 0
&& state.padHeightEnd == 0 && state.padWidthBegin == 0 && state.padWidthEnd == 0;
if (pointwise) {
if (!getHostConstDenseElementsAttr(state.w)) {
failureReason = "non_constant_weight";
return false;
}
if (state.hasBias && !isSupportedBiasAddValue(state.b, state.outType)) {
failureReason = "unsupported_bias";
return false;
}
return true;
}
if (state.wHeight != 3 || state.wWidth != 3) {
failureReason = "kernel_not_3x3";
return false;
@@ -2544,7 +2527,6 @@ static bool canConsumeNchwRowStripFragments(const ConvLoweringState& state, Stri
failureReason = "unsupported_bias";
return false;
}
ConvGeometry geometry = buildConvGeometry(state);
if (geometry.c > geometry.xbarSize) {
failureReason = "output_channels_exceed_crossbar";
return false;
@@ -2575,6 +2557,25 @@ static FailureOr<Value> createPaddedBiasRowConstant(const ConvLoweringState& sta
return getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), biasAttr, biasType);
}
static FailureOr<Value> createPaddedBiasTileConstant(const ConvLoweringState& state,
int64_t tileChannels,
PatternRewriter& rewriter) {
DenseElementsAttr denseAttr;
if (!isSupportedBiasAddValue(state.b, state.outType, &denseAttr))
return failure();
FailureOr<SmallVector<Attribute>> channelValues = getBiasChannelValues(denseAttr, state.outType);
if (failed(channelValues))
return failure();
const int64_t tileCount = ceilIntegerDivide(state.numChannelsOut, tileChannels);
auto tileType = RankedTensorType::get({tileCount, 1, tileChannels}, state.outType.getElementType());
SmallVector<Attribute> values(
tileType.getNumElements(), cast<Attribute>(rewriter.getZeroAttr(tileType.getElementType())));
for (int64_t channel = 0; channel < state.numChannelsOut; ++channel)
values[channel] = (*channelValues)[channel];
return getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(),
DenseElementsAttr::get(tileType, values), tileType);
}
static Value createHorizontallyPaddedRowStripFragment(Value fragment,
const ConvLoweringState& state,
PatternRewriter& rewriter,
@@ -2732,8 +2733,11 @@ static FailureOr<Value> createConvInputWindow(Value input,
? extractDenseConvWindowRow(input, sourceRowTable, state, outputHeight, kernelRow, rewriter, loc)
: extractProjectedRowStripWindowRow(
input, sourceRowTable, state, outputHeight, kernelRow, rewriter, loc);
Value mask = extractProjectedRowStripWindowMask(*maskTable, state, outputHeight, kernelRow, rewriter, loc);
Value semanticRow = spatial::SpatVMulOp::create(rewriter, loc, fragmentType, sourceRow, mask).getResult();
Value semanticRow = sourceRow;
if (state.padHeightBegin != 0 || state.padHeightEnd != 0) {
Value mask = extractProjectedRowStripWindowMask(*maskTable, state, outputHeight, kernelRow, rewriter, loc);
semanticRow = spatial::SpatVMulOp::create(rewriter, loc, fragmentType, sourceRow, mask).getResult();
}
Value paddedRow = createHorizontallyPaddedRowStripFragment(semanticRow, state, rewriter, loc);
window = tensor::InsertSliceOp::create(rewriter,
loc,
@@ -2906,12 +2910,6 @@ static FailureOr<Value> createPaddedConvOutputRow(Value patchRow,
.getResult();
}
static bool rowStripOutputFitsOneCore(const ConvGeometry& geometry) {
const int64_t inputTileCount = ceilIntegerDivide(geometry.k, geometry.xbarSize);
const int64_t outputTileCount = ceilIntegerDivide(geometry.c, geometry.xbarSize);
return inputTileCount * outputTileCount <= static_cast<int64_t>(crossbarCountInCore.getValue());
}
static bool rowStripOutputTileFitsOneCore(const ConvGeometry& geometry) {
return ceilIntegerDivide(geometry.k, geometry.xbarSize)
<= static_cast<int64_t>(crossbarCountInCore.getValue());
@@ -2928,38 +2926,40 @@ static FailureOr<Value> createOutputChannelTiledRowStripConvOutput(const ConvLow
const int64_t patchSize = state.numChannelsIn * state.wHeight * state.wWidth;
auto elementType = state.outType.getElementType();
auto paddedPatchRowType = RankedTensorType::get({1, paddedK}, elementType);
auto paddedRowType = RankedTensorType::get({1, xbarDim}, elementType);
auto tilePixelType = RankedTensorType::get({1, xbarDim, 1, 1}, elementType);
auto tileFragmentType = RankedTensorType::get({1, xbarDim, 1, state.outWidth}, elementType);
auto tileWeightsType = RankedTensorType::get({paddedK, xbarDim}, state.wType.getElementType());
SmallVector<Value> outputTiles;
outputTiles.reserve(outputTileCount);
for (int64_t outputTile = 0; outputTile < outputTileCount; ++outputTile) {
const int64_t channelOffset = outputTile * xbarDim;
const int64_t tileChannels = std::min(xbarDim, state.numChannelsOut - channelOffset);
auto tileRowType = RankedTensorType::get({1, tileChannels}, elementType);
auto tilePixelType = RankedTensorType::get({1, tileChannels, 1, 1}, elementType);
auto tileFragmentType = RankedTensorType::get({1, tileChannels, 1, state.outWidth}, elementType);
auto tileStorageType = spatial::getGraphBatchPhysicalResultType(state.outHeight, tileFragmentType);
SmallVector<OpFoldResult> weightOffsets {
rewriter.getIndexAttr(outputTile), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> weightSizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(paddedK), rewriter.getIndexAttr(xbarDim)};
Value tileWeights = extractStaticSliceOrIdentity(
rewriter, loc, paddedWeights, tileWeightsType, weightOffsets, weightSizes, getUnitStrides(rewriter, 3));
auto tileBatch = createSpatComputeBatch(
rewriter,
loc,
TypeRange {tileStorageType},
state.outHeight,
ValueRange {tileWeights},
ValueRange {state.x},
[&](detail::SpatComputeBatchBodyArgs args) {
const int64_t laneCount = state.outHeight * outputTileCount;
auto tileStorageType = spatial::getGraphBatchPhysicalResultType(laneCount, tileFragmentType);
FailureOr<Value> paddedBias = failure();
if (state.hasBias)
paddedBias = createPaddedBiasTileConstant(state, xbarDim, rewriter);
if (state.hasBias && failed(paddedBias))
return failure();
auto tileBatch = createSpatComputeBatch(
rewriter, loc, TypeRange {tileStorageType}, laneCount, ValueRange {paddedWeights},
state.hasBias ? ValueRange {state.x, *paddedBias} : ValueRange {state.x},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cOutWidth = getOrCreateIndexConstant(rewriter, anchorOp, state.outWidth);
Value outputRow = affineFloorDivConst(rewriter, loc, args.lane, outputTileCount, anchorOp);
Value outputTile = affineModConst(rewriter, loc, args.lane, outputTileCount, anchorOp);
SmallVector<OpFoldResult> weightOffsets {
outputTile, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> weightSizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(paddedK), rewriter.getIndexAttr(xbarDim)};
Value tileWeights = tensor::ExtractSliceOp::create(
rewriter, loc, tileWeightsType, args.weights.front(), weightOffsets, weightSizes, getUnitStrides(rewriter, 3));
FailureOr<Value> biasTile = failure();
if (state.hasBias)
biasTile = extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[1], outputTile, paddedRowType);
if (state.hasBias && failed(biasTile))
return failure();
FailureOr<Value> inputWindow =
createConvInputWindow(args.inputs.front(), state, args.lane, rewriter, loc);
createConvInputWindow(args.inputs.front(), state, outputRow, rewriter, loc);
if (failed(inputWindow))
return failure();
Value fragmentInit = tensor::EmptyOp::create(rewriter, loc, tileFragmentType.getShape(), elementType);
@@ -2984,28 +2984,18 @@ static FailureOr<Value> createOutputChannelTiledRowStripConvOutput(const ConvLow
paddedPatchRow = createZeroPaddedTensor(
paddedPatchRow, paddedPatchRowType, {0, 0}, {0, paddedK - patchSize}, rewriter, widthLoc);
FailureOr<Value> paddedOutputRow = createPaddedConvOutputTile(
paddedPatchRow, args.weights.front(), numKSlices, xbarDim, rewriter, widthLoc);
paddedPatchRow, tileWeights, numKSlices, xbarDim, rewriter, widthLoc);
if (failed(paddedOutputRow))
return failure();
Value outputRow = *paddedOutputRow;
if (tileChannels != xbarDim) {
SmallVector<OpFoldResult> rowOffsets {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> rowSizes {
rewriter.getIndexAttr(1), rewriter.getIndexAttr(tileChannels)};
outputRow = tensor::ExtractSliceOp::create(rewriter,
widthLoc,
tileRowType,
outputRow,
rowOffsets,
rowSizes,
getUnitStrides(rewriter, 2));
}
if (state.hasBias)
paddedOutputRow = spatial::SpatVAddOp::create(
rewriter, widthLoc, paddedRowType, *paddedOutputRow, *biasTile).getResult();
Value outputPixel = tensor::ExpandShapeOp::create(
rewriter, widthLoc, tilePixelType, outputRow, SmallVector<ReassociationIndices> {{0}, {1, 2, 3}});
rewriter, widthLoc, tilePixelType, *paddedOutputRow, SmallVector<ReassociationIndices> {{0}, {1, 2, 3}});
SmallVector<OpFoldResult> rowOffsets {
rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), widthIndex};
SmallVector<OpFoldResult> rowSizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tileChannels),
rewriter.getIndexAttr(xbarDim),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(1)};
Value nextFragment = tensor::InsertSliceOp::create(rewriter,
@@ -3024,58 +3014,9 @@ static FailureOr<Value> createOutputChannelTiledRowStripConvOutput(const ConvLow
rewriter, loc, widthLoop->results.front(), args.outputs.front(), args.lane);
return success();
});
if (failed(tileBatch))
return failure();
outputTiles.push_back(tileBatch->getResult(0));
}
auto fragmentType = getRowStripFragmentType(state.outType);
auto outputStorageType = getRowStripStorageType(state.outType);
auto assemblyBatch = createSpatComputeBatch(rewriter,
loc,
TypeRange {outputStorageType},
state.outHeight,
{},
ValueRange(outputTiles),
[&](detail::SpatComputeBatchBodyArgs args) {
Value fragment = tensor::EmptyOp::create(
rewriter, loc, fragmentType.getShape(), elementType);
for (int64_t outputTile = 0; outputTile < outputTileCount; ++outputTile) {
const int64_t channelOffset = outputTile * xbarDim;
const int64_t tileChannels =
std::min(xbarDim, state.numChannelsOut - channelOffset);
auto tileFragmentType = RankedTensorType::get(
{1, tileChannels, 1, state.outWidth}, elementType);
FailureOr<Value> tileFragment = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs[outputTile], args.lane, tileFragmentType);
if (failed(tileFragment))
return failure();
SmallVector<OpFoldResult> offsets {rewriter.getIndexAttr(0),
rewriter.getIndexAttr(channelOffset),
rewriter.getIndexAttr(0),
rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> sizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(tileChannels),
rewriter.getIndexAttr(1),
rewriter.getIndexAttr(state.outWidth)};
fragment = tensor::InsertSliceOp::create(rewriter,
loc,
*tileFragment,
fragment,
offsets,
sizes,
getUnitStrides(rewriter, 4));
}
insertRowStripFragment(
fragment, args.outputs.front(), state.outType, args.lane, rewriter, loc);
return success();
});
if (failed(assemblyBatch))
if (failed(tileBatch))
return failure();
Value output = assemblyBatch->getResult(0);
if (state.hasBias)
return applyRowStripBiasAdd(output, state.outType, state.b, rewriter, loc);
return output;
return tileBatch->getResult(0);
}
static FailureOr<Value>
@@ -3104,7 +3045,7 @@ createRowStripConvOutputFromDenseInput(const ConvLoweringState& state, PatternRe
weightDenseAttr, state, paddedK, xbarDim, rewriter)
: standard::createPaddedOutputChannelTiledWeightConstant(
weightDenseAttr, state, paddedK, xbarDim, rewriter);
if (!rowStripOutputFitsOneCore(geometry))
if (state.numChannelsOut > xbarDim)
return createOutputChannelTiledRowStripConvOutput(
state, paddedWeights, paddedK, numKSlices, xbarDim, rewriter, loc);
@@ -3279,11 +3220,106 @@ static FailureOr<Value> createConvOutputFromNchwRowStripFragments(Value rowStrip
return batchOp->getResult(0);
}
static FailureOr<Value> createPointwiseOutputFromRowStripFragments(Value rowStripStorage,
const ConvLoweringState& state,
PatternRewriter& rewriter,
Location loc) {
FailureOr<RowStripPhysicalValue> input = describeRowStripPhysicalValue(rowStripStorage, state.xType);
if (failed(input)) return failure();
ConvGeometry geometry = buildConvGeometry(state);
const int64_t xbarDim = geometry.xbarSize;
const int64_t inputFragmentChannels = input->fragmentType.getDimSize(1);
if (inputFragmentChannels % xbarDim != 0 || state.numChannelsIn % xbarDim != 0)
return failure();
auto weightDenseAttr = getHostConstDenseElementsAttr(state.w);
if (!weightDenseAttr) return failure();
const int64_t outputTileCount = ceilIntegerDivide(state.numChannelsOut, xbarDim);
const int64_t numKSlices = state.numChannelsIn / xbarDim;
auto elementType = state.outType.getElementType();
auto paddedRowType = RankedTensorType::get({1, xbarDim}, elementType);
auto inputRowType = RankedTensorType::get({1, inputFragmentChannels}, elementType);
auto weightTileType = RankedTensorType::get({state.numChannelsIn, xbarDim}, state.wType.getElementType());
auto weightSliceType = RankedTensorType::get({xbarDim, xbarDim}, state.wType.getElementType());
auto outputFragmentType = RankedTensorType::get({1, xbarDim, 1, 1}, elementType);
auto outputStorageType = spatial::getGraphBatchPhysicalResultType(outputTileCount, outputFragmentType);
Value paddedWeights = standard::createPaddedOutputChannelTiledWeightConstant(
weightDenseAttr, state, state.numChannelsIn, xbarDim, rewriter);
FailureOr<Value> paddedBias = failure();
if (state.hasBias) paddedBias = createPaddedBiasTileConstant(state, xbarDim, rewriter);
if (state.hasBias && failed(paddedBias)) return failure();
auto batch = createSpatComputeBatch(rewriter, loc, TypeRange {outputStorageType}, outputTileCount,
ValueRange {paddedWeights},
state.hasBias ? ValueRange {rowStripStorage, *paddedBias} : ValueRange {rowStripStorage},
[&](detail::SpatComputeBatchBodyArgs args) {
Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp();
Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0);
Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1);
Value cNumKSlices = getOrCreateIndexConstant(rewriter, anchorOp, numKSlices);
SmallVector<OpFoldResult> weightOffsets {args.lane, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)};
SmallVector<OpFoldResult> weightSizes {rewriter.getIndexAttr(1),
rewriter.getIndexAttr(state.numChannelsIn), rewriter.getIndexAttr(xbarDim)};
Value weightTile = tensor::ExtractSliceOp::create(
rewriter, loc, weightTileType, args.weights.front(), weightOffsets, weightSizes, getUnitStrides(rewriter, 3));
auto createPiece = [&](Value kSlice, Location pieceLoc) -> FailureOr<Value> {
Value channelOffset = affineMulConst(rewriter, pieceLoc, kSlice, xbarDim, anchorOp);
Value sourceSlot = affineFloorDivConst(
rewriter, pieceLoc, channelOffset, inputFragmentChannels, anchorOp);
Value sourceOffset = affineModConst(
rewriter, pieceLoc, channelOffset, inputFragmentChannels, anchorOp);
FailureOr<Value> fragment = extractGraphBatchPhysicalFragment(
rewriter, pieceLoc, args.inputs.front(), sourceSlot, input->fragmentType);
if (failed(fragment)) return failure();
Value inputRow = tensor::CollapseShapeOp::create(rewriter, pieceLoc, inputRowType, *fragment,
SmallVector<ReassociationIndices> {{0}, {1, 2, 3}});
Value inputSlice = tensor::ExtractSliceOp::create(rewriter, pieceLoc, paddedRowType, inputRow,
SmallVector<OpFoldResult> {rewriter.getIndexAttr(0), sourceOffset},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(1), rewriter.getIndexAttr(xbarDim)},
getUnitStrides(rewriter, 2));
Value weightSlice = tensor::ExtractSliceOp::create(rewriter, pieceLoc, weightSliceType, weightTile,
SmallVector<OpFoldResult> {channelOffset, rewriter.getIndexAttr(0)},
SmallVector<OpFoldResult> {rewriter.getIndexAttr(xbarDim), rewriter.getIndexAttr(xbarDim)},
getUnitStrides(rewriter, 2));
return spatial::SpatVMMOp::create(rewriter, pieceLoc, paddedRowType, weightSlice, inputSlice).getResult();
};
FailureOr<Value> result = createPiece(c0, loc);
if (failed(result)) return failure();
if (numKSlices > 1) {
auto reduction = buildNormalizedScfFor(rewriter, loc, c1, cNumKSlices, c1, ValueRange {*result},
[&](OpBuilder&, Location reduceLoc, Value kSlice, ValueRange iterArgs,
SmallVectorImpl<Value>& yielded) {
FailureOr<Value> piece = createPiece(kSlice, reduceLoc);
if (failed(piece)) return failure();
yielded.push_back(spatial::SpatVAddOp::create(
rewriter, reduceLoc, paddedRowType, iterArgs.front(), *piece).getResult());
return success();
});
if (failed(reduction)) return failure();
result = reduction->results.front();
}
if (state.hasBias) {
FailureOr<Value> bias = extractGraphBatchPhysicalFragment(
rewriter, loc, args.inputs[1], args.lane, paddedRowType);
if (failed(bias)) return failure();
result = spatial::SpatVAddOp::create(rewriter, loc, paddedRowType, *result, *bias).getResult();
}
Value fragment = tensor::ExpandShapeOp::create(rewriter, loc, outputFragmentType, *result,
SmallVector<ReassociationIndices> {{0}, {1, 2, 3}});
publishGraphBatchPhysicalFragment(rewriter, loc, fragment, args.outputs.front(), args.lane);
return success();
});
if (failed(batch)) return failure();
return batch->getResult(0);
}
static FailureOr<Value> createConvOutputFromRowStripInput(const ConvLoweringState& state,
[[maybe_unused]] const ConvLoweringDecision& decision,
Value rowStripInput,
PatternRewriter& rewriter,
Location loc) {
if (state.xHeight == 1 && state.xWidth == 1 && state.wHeight == 1 && state.wWidth == 1)
return createPointwiseOutputFromRowStripFragments(rowStripInput, state, rewriter, loc);
return createConvOutputFromNchwRowStripFragments(rowStripInput, state, rewriter, loc);
}
@@ -4187,22 +4223,9 @@ lowerSelectedConv2DPlan(spatial::SpatConv2DPlanOp planOp,
}
if (failed(canLowerConvPlanToRowStrip(planOp)))
return planOp.emitOpError("selected row-strip layout is not supported for this Conv plan"), failure();
ConvLoweringState rowState = *state;
const bool applyBiasAfterStorage = rowState.hasBias;
Value originalBias = rowState.b;
if (applyBiasAfterStorage) {
rowState.b = Value();
rowState.hasBias = false;
}
FailureOr<Value> rowStripStorage = createRowStripConvOutputFromDenseInput(rowState, rewriter, planOp.getLoc());
FailureOr<Value> rowStripStorage = createRowStripConvOutputFromDenseInput(*state, rewriter, planOp.getLoc());
if (failed(rowStripStorage))
return planOp.emitOpError("failed to build row-strip fragment storage for the selected Conv plan"), failure();
if (applyBiasAfterStorage) {
rowStripStorage = applyRowStripBiasAdd(*rowStripStorage, state->outType, originalBias, rewriter, planOp.getLoc());
if (failed(rowStripStorage))
return planOp.emitOpError("failed to apply row-strip Conv bias per fragment"), failure();
}
return *rowStripStorage;
}
@@ -77,9 +77,16 @@ static LogicalResult eraseOldGraph(func::FuncOp funcOp,
rewriter.eraseOp(blueprint);
continue;
}
if (!op->use_empty())
return op->emitOpError(
"phase 2 cannot erase an old graph compute with live results");
if (!op->use_empty()) {
for (OpResult result : op->getResults()) {
if (!result.use_empty()) {
Operation *user = result.use_begin()->getOwner();
return op->emitOpError()
<< "phase 2 cannot erase old graph result "
<< result.getResultNumber() << " used by " << user->getName();
}
}
}
rewriter.eraseOp(op);
}
return success();
@@ -100,6 +107,25 @@ static LogicalResult eraseDeferredSourceSelectors(
return success();
}
static void eraseUnusedIdentityDeferredCommunications(
func::FuncOp funcOp, IRRewriter &rewriter) {
SmallVector<SpatDeferredCommunicationOp> unused;
funcOp.walk([&](SpatDeferredCommunicationOp deferred) {
if (!deferred.getOutput().use_empty() || !deferred.getBody().hasOneBlock())
return;
Block &body = deferred.getBody().front();
auto yield = dyn_cast<SpatYieldOp>(body.getTerminator());
auto argument = yield && yield.getOutputs().size() == 1
? dyn_cast<BlockArgument>(yield.getOutputs().front())
: BlockArgument();
if (argument && argument.getOwner() == &body
&& argument.getArgNumber() < deferred.getSources().size())
unused.push_back(deferred);
});
for (SpatDeferredCommunicationOp deferred : llvm::reverse(unused))
rewriter.eraseOp(deferred);
}
static LogicalResult verifyDominance(func::FuncOp funcOp) {
DominanceInfo dominance(funcOp);
WalkResult result = funcOp.walk([&](Operation *op) {
@@ -119,6 +145,9 @@ static LogicalResult verifyDominance(func::FuncOp funcOp) {
LogicalResult realizeDeferredCommunication(
func::FuncOp funcOp,
const ScheduledComputeMaterializationResult &materialization) {
IRRewriter rewriter(funcOp.getContext());
eraseUnusedIdentityDeferredCommunications(funcOp, rewriter);
auto transfers = buildDeferredTransferPlan(funcOp, materialization);
if (failed(transfers))
return funcOp.emitOpError(
@@ -134,7 +163,6 @@ LogicalResult realizeDeferredCommunication(
return funcOp.emitOpError(
"phase 2 failed to build sparse boundary programs");
IRRewriter rewriter(funcOp.getContext());
if (failed(retargetDeferredPublications(funcOp, *transfers))
|| failed(replaceFinalGraphPublications(funcOp, *transfers)))
return failure();
@@ -42,6 +42,17 @@ static bool hasCapacityFor(Operation* producer, Operation* consumer) {
return getCrossbarUnionSize(producerWeights, consumerWeights) <= static_cast<size_t>(crossbarCountInCore.getValue());
}
template <typename ConsumerOp>
static bool isUniqueGraphComputePredecessor(Operation *candidate, ConsumerOp consumer) {
llvm::SmallSetVector<Operation *, 4> predecessors;
for (Value input : consumer.getInputs()) {
Operation *producer = input.getDefiningOp();
if (producer && isGraphComputeLike(producer))
predecessors.insert(producer);
}
return predecessors.size() == 1 && predecessors.front() == candidate;
}
struct TrivialGraphMergeStats {
size_t scalarBefore = 0;
size_t batchBefore = 0;
@@ -153,7 +164,8 @@ struct MergeTrivialScalarComputes : OpRewritePattern<SpatGraphCompute> {
for (Value input : consumer.getInputs()) {
auto candidate = input.getDefiningOp<SpatGraphCompute>();
if (candidate && candidate->getBlock() == consumer->getBlock() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isExclusivelyConsumedBy(candidate, consumer)
&& hasOnlyStructuralAttrs(consumer) && isUniqueGraphComputePredecessor(candidate, consumer)
&& isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasNoNestedArgumentCaptures(candidate)
&& hasNoNestedArgumentCaptures(consumer)) {
producer = candidate;
@@ -319,7 +331,8 @@ struct FoldBatchLeadingUnitNormalization : OpRewritePattern<SpatGraphCompute> {
? SpatGraphComputeBatch()
: consumer.getInputs().front().getDefiningOp<SpatGraphComputeBatch>();
if (!producer || producer->getBlock() != consumer->getBlock() || !hasOnlyStructuralAttrs(producer)
|| !hasOnlyStructuralAttrs(consumer) || !isExclusivelyConsumedBy(producer, consumer))
|| !hasOnlyStructuralAttrs(consumer) || !isUniqueGraphComputePredecessor(producer, consumer)
|| !isExclusivelyConsumedBy(producer, consumer))
return failure();
auto fragments = collectPublishedFragments(producer);
if (!matchLeadingUnitNormalization(producer, consumer) || failed(fragments))
@@ -394,7 +407,8 @@ struct MergeTrivialBatchComputes : OpRewritePattern<SpatGraphComputeBatch> {
auto candidate = input.getDefiningOp<SpatGraphComputeBatch>();
if (candidate && candidate->getBlock() == consumer->getBlock()
&& candidate.getLaneCount() == consumer.getLaneCount() && hasOnlyStructuralAttrs(candidate)
&& hasOnlyStructuralAttrs(consumer) && isExclusivelyConsumedBy(candidate, consumer)
&& hasOnlyStructuralAttrs(consumer) && isUniqueGraphComputePredecessor(candidate, consumer)
&& isExclusivelyConsumedBy(candidate, consumer)
&& hasCapacityFor(candidate, consumer) && hasDirectLaneConsumers(candidate, consumer)
&& succeeded(fragments = collectPublishedFragments(candidate))) {
producer = candidate;
@@ -462,7 +476,7 @@ struct TrivialGraphComputeMergePass final : PassWrapper<TrivialGraphComputeMerge
StringRef getArgument() const override { return "pim-trivial-graph-compute-merge"; }
StringRef getDescription() const override {
return "Inline graph computes with exclusive direct dependencies before PEFT scheduling.";
return "Inline linear exclusive graph compute chains while preserving fan-in boundaries.";
}
void runOnOperation() override {
+6 -4
View File
@@ -155,11 +155,11 @@ Motifs are not inferred from rendered geometry. For each operation graph the too
## Viewer and API
Except for spatial4, the viewer initially fetches an aggregate operation graph unless the browser has a saved view choice. Sigma renders the graph with WebGL. Controls cover report/view/metric selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Mapping panels remain available for operation aggregate edges.
The viewer initially fetches an aggregate operation graph, so a previously selected raw view cannot make a new report exceed the display safety cap before the first render. Browser responses disable caching so HTML and JavaScript from different tool revisions cannot be mixed. Sigma renders the graph with WebGL. Controls cover report/view selection, text and tensor search, mapping filters, self edges, relayout, fitting, and motif selection. Edges use a fixed width. Raw nodes and edges have their own selection details and never call aggregate-only detail or mapping endpoints. Nodes without an SSA name fall back to their source identity. Mapping panels remain available for operation aggregate edges.
Operation expansion is a deterministic projection of the source graph. Expand selected, Expand all operations, Collapse selected operation, and Collapse all rebuild the complete display from the current expanded-operation set. An expanded operation's raw nodes replace its aggregate node, including isolated raw nodes. An aggregate edge is retained only when both endpoint operations are collapsed; otherwise its raw edges replace it with endpoints calculated from the complete expansion set. Expansion order therefore cannot leave stale endpoints or simultaneous aggregate/raw representations.
Operation ranks are calculated from the complete operation graph, including isolated operations. Collapsed nodes use stable operation anchors. Expanded nodes are sorted by lane and node ID; lane numbers form perpendicular rows, equal lanes align across adjacent operations, and lane-less nodes use distinct deterministic scalar rows. This same model is used by Rerun layout, so unrelated operations do not jump during expansion or collapse and disconnected nodes never pile up at `(0, 0)`.
Operation ranks are calculated from the complete operation graph, including isolated operations, and every view flows top-to-bottom. A collapsed operation reserves one displayed row. Expanded nodes use compact rows for distinct lanes in numeric order, followed by deterministic lane-less rows, so sparse lane numbers create no empty geometric space. Raw nodes sharing a lane receive centered deterministic horizontal offsets. Operation and raw views use stable server coordinates, and `Reset layout` restores them; core and node-kind views use ELK, and `Rerun layout` runs it again. Operation labels use the first SSA name; Sigma's collision grid thins normal labels, and a label is skipped when its measured screen rectangle intersects another visible node. Selected or hovered nodes keep the original white bubble with black text. Selecting a motif frames its member nodes on the first click. Aggregate-node sizes remain bounded.
Browser dependencies are pinned in one place, `static/index.html`:
@@ -192,8 +192,10 @@ Raw pages default to 100 and cannot exceed 500. Subgraph depth cannot exceed fiv
## Performance behavior
- CSV readers stream rows and insert them in bounded batches.
- Temporary SQLite staging and bulk joins resolve endpoints; ingestion performs no per-edge node query.
- Secondary indexes are created after raw insertion.
- Temporary SQLite staging tables are function-scoped and dropped immediately; bulk joins resolve endpoints without per-edge node queries.
- CSVs without additional columns use a constant empty JSON representation.
- Ingestion closes its write connection before derived indexes, aggregation, motifs, and diagnostics reopen the database.
- Secondary indexes are created after raw insertion and focus on serving and aggregation queries.
- Raw edges are never retained as a Python object graph or loaded into NetworkX.
- Only aggregate operation nodes/edges enter NetworkX.
- Mapping statistics are grouped in SQL; exact stencil comparison streams one operation pair.
@@ -42,6 +42,12 @@ def create_app(output_directory: str | Path, *, max_page_size: int = 500,
manifest = json.loads(manifest_path.read_text())
app = FastAPI(title="Raptor Graph Explorer", version=manifest.get("tool_version", "unknown"))
@app.middleware("http")
async def disable_browser_cache(request, call_next):
response = await call_next(request)
response.headers["Cache-Control"] = "no-store"
return response
def connection():
db = connect_readonly(database)
try:
@@ -10,7 +10,7 @@ from pathlib import Path
from . import __version__
from .aggregate import aggregate_report
from .database import connect_readonly, create_database, create_indexes, store_diagnostics
from .database import connect_readonly, connect_writable, create_database, create_indexes, store_diagnostics
from .discovery import discover_reports
from .ingest import ingest_report
from .motifs import analyze_report
@@ -52,6 +52,12 @@ def build_output(inputs: list[str | Path], output_directory: str | Path, *, stri
with connection:
for pair in pairs:
ingest_report(connection, pair, diagnostics)
finally:
connection.close()
connection = connect_writable(output / "graph.sqlite")
try:
with connection:
create_indexes(connection)
for pair in pairs:
aggregate_report(connection, pair.report_id)
@@ -7,7 +7,6 @@ from pathlib import Path
from .schema import Diagnostic, SCHEMA_VERSION
DDL = """
PRAGMA foreign_keys=ON;
CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE reports(
report_id TEXT PRIMARY KEY, stage TEXT NOT NULL, raw_node_count INTEGER NOT NULL DEFAULT 0,
@@ -65,25 +64,25 @@ CREATE TABLE diagnostics(
"""
INDEX_DDL = """
CREATE INDEX raw_nodes_report_op ON raw_nodes(report_id,op_id);
CREATE INDEX raw_nodes_report_lane ON raw_nodes(report_id,op_id,lane);
CREATE INDEX raw_nodes_report_core ON raw_nodes(report_id,core);
CREATE INDEX raw_edges_source_node ON raw_edges(report_id,source_node_id);
CREATE INDEX raw_edges_target_node ON raw_edges(report_id,target_node_id);
CREATE INDEX raw_edges_source_op ON raw_edges(report_id,source_op_id,target_op_id);
CREATE INDEX raw_edges_target_op ON raw_edges(report_id,target_op_id,source_op_id);
CREATE INDEX raw_edges_mapping ON raw_edges(report_id,source_op_id,target_op_id,source_lane,target_lane);
CREATE INDEX raw_edges_channel ON raw_edges(report_id,channel_id);
CREATE INDEX aggregate_nodes_lookup ON aggregate_nodes(report_id,level,op_id,core);
CREATE INDEX aggregate_edges_lookup ON aggregate_edges(report_id,level);
CREATE INDEX motifs_report ON motifs(report_id);
"""
def create_database(path: Path) -> sqlite3.Connection:
def connect_writable(path: Path) -> sqlite3.Connection:
connection = sqlite3.connect(path)
connection.row_factory = sqlite3.Row
connection.executescript("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;" + DDL)
connection.executescript("PRAGMA foreign_keys=ON; PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
return connection
def create_database(path: Path) -> sqlite3.Connection:
connection = connect_writable(path)
connection.executescript(DDL)
connection.execute("INSERT INTO metadata VALUES('schema_version',?)", (str(SCHEMA_VERSION),))
return connection
@@ -4,6 +4,7 @@ import csv
import json
import re
import sqlite3
import sys
from pathlib import Path
from .schema import DiagnosticCollector, EDGE_COLUMNS, NODE_COLUMNS, NODE_KINDS, ReportPair
@@ -36,9 +37,10 @@ def _id_parts(node_id: str) -> tuple[str, int | None, int | None]:
return prefix, op_id, lane
def _extra(row: dict[str, str | None], known: set[str]) -> str:
return json.dumps({key: value for key, value in row.items() if key not in known},
sort_keys=True, separators=(",", ":"))
def _extra(row: dict[str, str | None], columns: tuple[str, ...]) -> str:
if not columns:
return "{}"
return json.dumps({key: row.get(key) for key in columns}, sort_keys=True, separators=(",", ":"))
def _batches(rows, size: int = BATCH_SIZE):
@@ -61,6 +63,15 @@ def _check_columns(reader: csv.DictReader, required: set[str], kind: str, report
return True
def _drop_staging(connection: sqlite3.Connection, table: str) -> None:
active_exception = sys.exc_info()[0]
try:
connection.execute(f"DROP TABLE IF EXISTS temp.{table}")
except sqlite3.Error:
if active_exception is None:
raise
def _read_nodes(connection: sqlite3.Connection, pair: ReportPair, stage: str,
diagnostics: DiagnosticCollector) -> int:
connection.executescript("""
@@ -70,63 +81,67 @@ def _read_nodes(connection: sqlite3.Connection, pair: ReportPair, stage: str,
core INTEGER, ssa_name TEXT, attributes_json TEXT
);
""")
valid = 0
with pair.nodes_path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.DictReader(stream)
if not _check_columns(reader, NODE_COLUMNS, "node", pair.report_id, diagnostics):
return 0
try:
valid = 0
with pair.nodes_path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.DictReader(stream)
if not _check_columns(reader, NODE_COLUMNS, "node", pair.report_id, diagnostics):
return 0
extra_columns = tuple(column for column in reader.fieldnames or () if column not in NODE_COLUMNS)
def normalized():
nonlocal valid
for row_number, row in enumerate(reader, 2):
node_id = (row.get("Id") or "").strip()
if not node_id:
diagnostics.add("invalid_node", "node Id is empty", pair.report_id, f"row {row_number}")
continue
prefix, id_op, id_lane = _id_parts(node_id)
try:
op_id = _integer(row.get("op_id"), "op_id")
lane = _integer(row.get("lane"), "lane")
core = _integer(row.get("core"), "core")
except ValueError as exc:
diagnostics.add("malformed_integer", str(exc), pair.report_id, f"row {row_number}")
continue
if op_id is None:
if id_op is None:
diagnostics.add("invalid_node", "missing op_id and unusable node ID fallback", pair.report_id,
f"row {row_number}: {node_id}")
def normalized():
nonlocal valid
for row_number, row in enumerate(reader, 2):
node_id = (row.get("Id") or "").strip()
if not node_id:
diagnostics.add("invalid_node", "node Id is empty", pair.report_id, f"row {row_number}")
continue
op_id = id_op
diagnostics.add("id_fallback", "op_id parsed from node ID", pair.report_id, node_id,
severity="warning")
if lane is None and prefix in {"gcb", "scb"} and id_lane is not None:
lane = id_lane
diagnostics.add("id_fallback", "lane parsed from node ID", pair.report_id, node_id,
severity="warning")
valid += 1
yield (row_number, node_id, NODE_KINDS.get(prefix, f"unknown:{prefix}"), op_id, lane, core,
row.get("ssa_name") or "", _extra(row, NODE_COLUMNS))
prefix, id_op, id_lane = _id_parts(node_id)
try:
op_id = _integer(row.get("op_id"), "op_id")
lane = _integer(row.get("lane"), "lane")
core = _integer(row.get("core"), "core")
except ValueError as exc:
diagnostics.add("malformed_integer", str(exc), pair.report_id, f"row {row_number}")
continue
if op_id is None:
if id_op is None:
diagnostics.add("invalid_node", "missing op_id and unusable node ID fallback",
pair.report_id, f"row {row_number}: {node_id}")
continue
op_id = id_op
diagnostics.add("id_fallback", "op_id parsed from node ID", pair.report_id, node_id,
severity="warning")
if lane is None and prefix in {"gcb", "scb"} and id_lane is not None:
lane = id_lane
diagnostics.add("id_fallback", "lane parsed from node ID", pair.report_id, node_id,
severity="warning")
valid += 1
yield (row_number, node_id, NODE_KINDS.get(prefix, f"unknown:{prefix}"), op_id, lane, core,
row.get("ssa_name") or "", _extra(row, extra_columns))
for batch in _batches(normalized()):
connection.executemany("INSERT INTO ingest_nodes VALUES(?,?,?,?,?,?,?,?)", batch)
for batch in _batches(normalized()):
connection.executemany("INSERT INTO ingest_nodes VALUES(?,?,?,?,?,?,?,?)", batch)
duplicate = connection.execute(
"SELECT COALESCE(SUM(c-1),0) FROM (SELECT COUNT(*) c FROM ingest_nodes GROUP BY node_id HAVING c>1)"
).fetchone()[0]
if duplicate:
examples = [row[0] for row in connection.execute(
"SELECT node_id FROM ingest_nodes GROUP BY node_id HAVING COUNT(*)>1 ORDER BY node_id LIMIT 5")]
diagnostics.add("duplicate_node_id", "duplicate node ID; first row retained", pair.report_id,
", ".join(examples), duplicate)
connection.execute("""
INSERT INTO raw_nodes(report_id,stage,node_id,node_kind,op_id,lane,core,ssa_name,attributes_json)
SELECT ?,?,n.node_id,n.node_kind,n.op_id,n.lane,n.core,n.ssa_name,n.attributes_json
FROM ingest_nodes n
JOIN (SELECT node_id,MIN(row_number) first_row FROM ingest_nodes GROUP BY node_id) first
ON first.node_id=n.node_id AND first.first_row=n.row_number
ORDER BY n.row_number
""", (pair.report_id, stage))
return valid - duplicate
duplicate = connection.execute(
"SELECT COALESCE(SUM(c-1),0) FROM (SELECT COUNT(*) c FROM ingest_nodes GROUP BY node_id HAVING c>1)"
).fetchone()[0]
if duplicate:
examples = [row[0] for row in connection.execute(
"SELECT node_id FROM ingest_nodes GROUP BY node_id HAVING COUNT(*)>1 ORDER BY node_id LIMIT 5")]
diagnostics.add("duplicate_node_id", "duplicate node ID; first row retained", pair.report_id,
", ".join(examples), duplicate)
connection.execute("""
INSERT INTO raw_nodes(report_id,stage,node_id,node_kind,op_id,lane,core,ssa_name,attributes_json)
SELECT ?,?,n.node_id,n.node_kind,n.op_id,n.lane,n.core,n.ssa_name,n.attributes_json
FROM ingest_nodes n
JOIN (SELECT node_id,MIN(row_number) first_row FROM ingest_nodes GROUP BY node_id) first
ON first.node_id=n.node_id AND first.first_row=n.row_number
ORDER BY n.row_number
""", (pair.report_id, stage))
return valid - duplicate
finally:
_drop_staging(connection, "ingest_nodes")
def _read_edges(connection: sqlite3.Connection, pair: ReportPair, stage: str,
@@ -139,74 +154,79 @@ def _read_edges(connection: sqlite3.Connection, pair: ReportPair, stage: str,
tensor_type TEXT, channel_id INTEGER, attributes_json TEXT
);
""")
with pair.edges_path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.DictReader(stream)
if not _check_columns(reader, EDGE_COLUMNS, "edge", pair.report_id, diagnostics):
return 0
try:
with pair.edges_path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.DictReader(stream)
if not _check_columns(reader, EDGE_COLUMNS, "edge", pair.report_id, diagnostics):
return 0
extra_columns = tuple(column for column in reader.fieldnames or () if column not in EDGE_COLUMNS)
def normalized():
for row_number, row in enumerate(reader, 2):
source = (row.get("Source") or "").strip()
target = (row.get("Target") or "").strip()
if not source or not target:
diagnostics.add("invalid_edge", "edge Source or Target is empty", pair.report_id,
f"row {row_number}")
continue
try:
source_lane = _integer(row.get("source_lane"), "source_lane")
target_lane = _integer(row.get("target_lane"), "target_lane")
weight = _integer(row.get("Weight"), "Weight")
channel_id = _integer(row.get("channel_id"), "channel_id")
except ValueError as exc:
diagnostics.add("malformed_integer", str(exc), pair.report_id, f"row {row_number}")
continue
yield (row_number, (row.get("stage") or stage).strip() or stage, source, target,
source_lane, target_lane, weight, row.get("Type") or "", channel_id,
_extra(row, EDGE_COLUMNS))
def normalized():
for row_number, row in enumerate(reader, 2):
source = (row.get("Source") or "").strip()
target = (row.get("Target") or "").strip()
if not source or not target:
diagnostics.add("invalid_edge", "edge Source or Target is empty", pair.report_id,
f"row {row_number}")
continue
try:
source_lane = _integer(row.get("source_lane"), "source_lane")
target_lane = _integer(row.get("target_lane"), "target_lane")
weight = _integer(row.get("Weight"), "Weight")
channel_id = _integer(row.get("channel_id"), "channel_id")
except ValueError as exc:
diagnostics.add("malformed_integer", str(exc), pair.report_id, f"row {row_number}")
continue
yield (row_number, (row.get("stage") or stage).strip() or stage, source, target,
source_lane, target_lane, weight, row.get("Type") or "", channel_id,
_extra(row, extra_columns))
for batch in _batches(normalized()):
connection.executemany("INSERT INTO ingest_edges VALUES(?,?,?,?,?,?,?,?,?,?)", batch)
for batch in _batches(normalized()):
connection.executemany("INSERT INTO ingest_edges VALUES(?,?,?,?,?,?,?,?,?,?)", batch)
unknown_count = connection.execute("""
SELECT COUNT(*) FROM ingest_edges e
LEFT JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
LEFT JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
WHERE s.node_id IS NULL OR t.node_id IS NULL
""", (pair.report_id, pair.report_id)).fetchone()[0]
if unknown_count:
examples = [f"{row[0]}->{row[1]}" for row in connection.execute("""
SELECT e.source_node_id,e.target_node_id FROM ingest_edges e
unknown_count = connection.execute("""
SELECT COUNT(*) FROM ingest_edges e
LEFT JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
LEFT JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
WHERE s.node_id IS NULL OR t.node_id IS NULL ORDER BY e.row_number LIMIT 5
""", (pair.report_id, pair.report_id))]
diagnostics.add("unknown_endpoint", "edge endpoint is absent from the matching report; edge omitted",
pair.report_id, ", ".join(examples), unknown_count)
WHERE s.node_id IS NULL OR t.node_id IS NULL
""", (pair.report_id, pair.report_id)).fetchone()[0]
if unknown_count:
examples = [f"{row[0]}->{row[1]}" for row in connection.execute("""
SELECT e.source_node_id,e.target_node_id FROM ingest_edges e
LEFT JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
LEFT JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
WHERE s.node_id IS NULL OR t.node_id IS NULL ORDER BY e.row_number LIMIT 5
""", (pair.report_id, pair.report_id))]
diagnostics.add("unknown_endpoint", "edge endpoint is absent from the matching report; edge omitted",
pair.report_id, ", ".join(examples), unknown_count)
contradictions = connection.execute("""
SELECT COUNT(*) FROM ingest_edges e
JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
WHERE (e.explicit_source_lane IS NOT NULL AND s.lane IS NOT NULL AND e.explicit_source_lane<>s.lane)
OR (e.explicit_target_lane IS NOT NULL AND t.lane IS NOT NULL AND e.explicit_target_lane<>t.lane)
""", (pair.report_id, pair.report_id)).fetchone()[0]
if contradictions:
diagnostics.add("contradictory_lane", "explicit edge lane contradicts endpoint lane; explicit lane retained",
pair.report_id, count=contradictions)
contradictions = connection.execute("""
SELECT COUNT(*) FROM ingest_edges e
JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
WHERE (e.explicit_source_lane IS NOT NULL AND s.lane IS NOT NULL AND e.explicit_source_lane<>s.lane)
OR (e.explicit_target_lane IS NOT NULL AND t.lane IS NOT NULL AND e.explicit_target_lane<>t.lane)
""", (pair.report_id, pair.report_id)).fetchone()[0]
if contradictions:
diagnostics.add("contradictory_lane",
"explicit edge lane contradicts endpoint lane; explicit lane retained",
pair.report_id, count=contradictions)
connection.execute("""
INSERT INTO raw_edges(
edge_id,report_id,stage,source_node_id,target_node_id,source_op_id,target_op_id,
source_lane,target_lane,weight,tensor_type,channel_id,attributes_json)
SELECT printf('%s:e:%012d',?,e.row_number),?,e.stage,e.source_node_id,e.target_node_id,s.op_id,t.op_id,
COALESCE(e.explicit_source_lane,s.lane),COALESCE(e.explicit_target_lane,t.lane),
e.weight,e.tensor_type,e.channel_id,e.attributes_json
FROM ingest_edges e
JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
ORDER BY e.row_number
""", (pair.report_id, pair.report_id, pair.report_id, pair.report_id))
return connection.execute("SELECT COUNT(*) FROM raw_edges WHERE report_id=?", (pair.report_id,)).fetchone()[0]
connection.execute("""
INSERT INTO raw_edges(
edge_id,report_id,stage,source_node_id,target_node_id,source_op_id,target_op_id,
source_lane,target_lane,weight,tensor_type,channel_id,attributes_json)
SELECT printf('%s:e:%012d',?,e.row_number),?,e.stage,e.source_node_id,e.target_node_id,s.op_id,t.op_id,
COALESCE(e.explicit_source_lane,s.lane),COALESCE(e.explicit_target_lane,t.lane),
e.weight,e.tensor_type,e.channel_id,e.attributes_json
FROM ingest_edges e
JOIN raw_nodes s ON s.report_id=? AND s.node_id=e.source_node_id
JOIN raw_nodes t ON t.report_id=? AND t.node_id=e.target_node_id
ORDER BY e.row_number
""", (pair.report_id, pair.report_id, pair.report_id, pair.report_id))
return connection.execute("SELECT COUNT(*) FROM raw_edges WHERE report_id=?", (pair.report_id,)).fetchone()[0]
finally:
_drop_staging(connection, "ingest_edges")
def ingest_report(connection: sqlite3.Connection, pair: ReportPair, diagnostics: DiagnosticCollector) -> None:
@@ -69,12 +69,15 @@ def _dominates(dominators: dict, entry: int, node: int) -> bool:
def detect_motifs(graph: nx.DiGraph) -> tuple[list[Motif], int]:
if not graph:
return [], 0
dag, components, _ = _condense(graph)
dag, components, member_to_component = _condense(graph)
source, sink = "__source__", "__sink__"
dag.add_node(source)
dag.add_node(sink)
dag.add_edges_from((source, node) for node in list(dag) if node not in {source, sink} and dag.in_degree(node) == 0)
dag.add_edges_from((node, sink) for node in list(dag) if node not in {source, sink} and dag.out_degree(node) == 0)
ranks = {}
for component in nx.topological_sort(dag):
ranks[component] = max((ranks[parent] + 1 for parent in dag.predecessors(component)), default=0)
dominators = nx.immediate_dominators(dag, source)
post_dominators = nx.immediate_dominators(dag.reverse(copy=False), sink)
motifs = []
@@ -113,7 +116,8 @@ def detect_motifs(graph: nx.DiGraph) -> tuple[list[Motif], int]:
motifs.append(Motif(entry_aggregate, exit_aggregate, internal_aggregates, branch_aggregates, paths,
len(expanded), subgraph.number_of_edges(), cycle, kind,
1.0 if kind != "split_rejoin" else 0.9))
motifs.sort(key=lambda motif: (motif.entry, motif.exit, motif.internal))
motifs.sort(key=lambda motif: (ranks[member_to_component[motif.entry]], motif.entry,
motif.exit, motif.internal))
return motifs, len(components)
@@ -11,7 +11,9 @@ JSON_COLUMNS = {
"tensor_type_histogram",
}
LANE_SPACING = 42
OPERATION_SPACING = 240
OPERATION_SPACING = 126
RANK_SPACING = 600
DUPLICATE_NODE_SPACING = 12
class DisplayGraphTooLarge(ValueError):
@@ -25,7 +27,8 @@ def _record(row: sqlite3.Row) -> dict:
return result
def _operation_anchors(nodes: list[dict], edges: list[dict]) -> dict[int, tuple[float, float]]:
def _operation_anchors(nodes: list[dict], edges: list[dict],
expanded_rows: dict[int, int]) -> dict[int, tuple[float, float]]:
graph = nx.DiGraph()
graph.add_nodes_from(node["op_id"] for node in nodes)
graph.add_edges_from((edge["source_op_id"], edge["target_op_id"]) for edge in edges)
@@ -39,17 +42,13 @@ def _operation_anchors(nodes: list[dict], edges: list[dict]) -> dict[int, tuple[
by_rank[operation_ranks[node["op_id"]]].append(node)
anchors = {}
rank_offset = 0
for rank, ranked_nodes in sorted(by_rank.items()):
ranked_nodes.sort(key=lambda item: item["op_id"])
cursor = 0
for node in ranked_nodes:
minimum = node["minimum_lane"] if node["minimum_lane"] is not None else 0
maximum = node["maximum_lane"] if node["maximum_lane"] is not None else minimum - 1
lane_rows = max(0, maximum - minimum + 1)
scalar_rows = max(0, node["instance_count"] - node["lane_count"])
anchor_y = 0 if len(ranked_nodes) == 1 else cursor - minimum * LANE_SPACING
anchors[node["op_id"]] = (rank * OPERATION_SPACING, anchor_y)
cursor += max(1, lane_rows + scalar_rows) * LANE_SPACING + 2 * LANE_SPACING
for index, node in enumerate(ranked_nodes):
anchors[node["op_id"]] = (index * OPERATION_SPACING, -rank_offset)
rank_offset += max(expanded_rows.get(node["op_id"], 1) for node in ranked_nodes) * LANE_SPACING
rank_offset += RANK_SPACING
return anchors
@@ -90,6 +89,35 @@ def _raw_edges(db: sqlite3.Connection, report_id: str, expanded: set[int], all_e
return [result[key] for key in sorted(result)]
def _display_rows(raw_nodes: list[dict]) -> tuple[
dict[int, int], dict[int, dict[int, int]], dict[int, dict[str, int]], dict[str, float]]:
by_operation: dict[int, list[dict]] = defaultdict(list)
for node in raw_nodes:
by_operation[node["op_id"]].append(node)
row_counts = {}
lane_rows = {}
scalar_rows = {}
duplicate_offsets = {}
for op_id, nodes in by_operation.items():
lanes = sorted({node["lane"] for node in nodes if node["lane"] is not None})
lane_rows[op_id] = {lane: index for index, lane in enumerate(lanes)}
scalars = sorted(node["node_id"] for node in nodes if node["lane"] is None)
scalar_rows[op_id] = {node_id: len(lanes) + index for index, node_id in enumerate(scalars)}
row_counts[op_id] = max(1, len(lanes) + len(scalars))
duplicates: dict[int, list[str]] = defaultdict(list)
for node in nodes:
if node["lane"] is not None:
duplicates[node["lane"]].append(node["node_id"])
for node_ids in duplicates.values():
node_ids.sort()
centre = (len(node_ids) - 1) / 2
for index, node_id in enumerate(node_ids):
duplicate_offsets[node_id] = (index - centre) * DUPLICATE_NODE_SPACING
return row_counts, lane_rows, scalar_rows, duplicate_offsets
def display_graph(db: sqlite3.Connection, report_id: str, *, view: str = "operation",
expanded_operation_ids=(), expand_all: bool = False, cap: int = 5_000) -> dict:
if view not in {"operation", "raw"}:
@@ -119,7 +147,9 @@ def display_graph(db: sqlite3.Connection, report_id: str, *, view: str = "operat
f"safety cap is {cap}. Use an aggregate view or increase the configured expansion cap."
)
anchors = _operation_anchors(operation_nodes, operation_edges)
raw_nodes = _raw_nodes(db, report_id, expanded, expanded == known)
row_counts, lane_rows, scalar_rows, duplicate_offsets = _display_rows(raw_nodes)
anchors = _operation_anchors(operation_nodes, operation_edges, row_counts)
nodes = []
for node in operation_nodes:
if node["op_id"] not in expanded:
@@ -127,22 +157,15 @@ def display_graph(db: sqlite3.Connection, report_id: str, *, view: str = "operat
nodes.append({**node, "display_id": node["aggregate_id"], "representation": "aggregate",
"x": x, "y": y})
raw_nodes = _raw_nodes(db, report_id, expanded, expanded == known)
lane_occurrences: dict[tuple[int, int], int] = defaultdict(int)
scalar_indices: dict[int, int] = defaultdict(int)
maximum_lanes = {node["op_id"]: node["maximum_lane"] for node in operation_nodes}
for node in raw_nodes:
anchor_x, anchor_y = anchors[node["op_id"]]
if node["lane"] is None:
index = scalar_indices[node["op_id"]]
scalar_indices[node["op_id"]] += 1
row = (maximum_lanes[node["op_id"]] if maximum_lanes[node["op_id"]] is not None else -1) + 1 + index
x, y = anchor_x, anchor_y + row * LANE_SPACING
row = scalar_rows[node["op_id"]][node["node_id"]]
x = anchor_x
else:
key = (node["op_id"], node["lane"])
x = anchor_x + lane_occurrences[key] * 12
y = anchor_y + node["lane"] * LANE_SPACING
lane_occurrences[key] += 1
row = lane_rows[node["op_id"]][node["lane"]]
x = anchor_x + duplicate_offsets[node["node_id"]]
y = anchor_y - row * LANE_SPACING
nodes.append({**node, "display_id": node["node_id"], "representation": "raw", "x": x, "y": y})
edges = []
@@ -16,18 +16,16 @@ function definition(object, keys) {
return `<dl>${keys.filter(key => object[key] !== undefined).map(key => `<dt>${html(key.replaceAll("_", " "))}</dt><dd>${html(typeof object[key] === "object" ? JSON.stringify(object[key]) : object[key])}</dd>`).join("")}</dl>`;
}
function status(message, error = false) { $("status").textContent = message; $("status").className = error ? "error" : ""; }
function selectedReport() { return state.reports.find(report => report.report_id === $("report").value); }
function defaultViewForReport(report) {
if (report?.stage === "spatial4") return "raw";
const saved = localStorage.getItem("raptor-graph-view");
return ["operation", "raw", "core", "node_kind"].includes(saved) ? saved : "operation";
function updateLayoutButton() {
$("layout").textContent = ["operation", "raw"].includes($("level").value) ? "Reset layout" : "Rerun layout";
}
async function loadReports() {
state.reports = await api("/api/reports");
if (!state.reports.length) throw new Error("No reports were preprocessed.");
$("report").innerHTML = state.reports.map(report => `<option>${html(report.report_id)}</option>`).join("");
$("level").value = defaultViewForReport(selectedReport());
$("level").value = "operation";
updateLayoutButton();
await loadGraph();
}
function aggregateGraph(data) {
@@ -72,32 +70,60 @@ function updateStatus() {
} else status(`${state.data.nodes.length} aggregate nodes, ${state.data.edges.length} aggregate edges`);
}
function nodeLabel(node) {
if (node.representation === "raw") return node.lane === null ? node.node_id : `${node.node_id} · lane ${node.lane}`;
return node.op_id !== null ? `op ${node.op_id}` : node.group_key;
if (node.representation === "raw") {
const name = node.ssa_name || node.node_id;
return node.lane === null ? name : `${name} · lane ${node.lane}`;
}
return node.op_id !== null ? node.ssa_summary.find(Boolean) || `op ${node.op_id}` : node.group_key;
}
function edgeSize(edge) {
if (edge.representation === "raw") return 1;
const value = edge[$("metric").value] || 0;
return 1 + Math.log2(1 + Math.max(0, value)) / 3;
function labelIntersectsNode(context, data, settings) {
context.font = `${settings.labelWeight} ${settings.labelSize}px ${settings.labelFont}`;
const metrics = context.measureText(data.label), x = data.x + data.size + 3, y = data.y + settings.labelSize / 3;
const bounds = {
left: x - (metrics.actualBoundingBoxLeft || 0), right: x + (metrics.actualBoundingBoxRight || metrics.width),
top: y - (metrics.actualBoundingBoxAscent || settings.labelSize), bottom: y + (metrics.actualBoundingBoxDescent || 0),
};
return state.graph.nodes().some(id => {
if (id === data.key) return false;
const node = state.renderer.getNodeDisplayData(id);
if (!node || node.hidden) return false;
const position = state.renderer.framedGraphToViewport(node), radius = state.renderer.scaleSize(node.size) + 1;
const dx = position.x - Math.max(bounds.left, Math.min(position.x, bounds.right));
const dy = position.y - Math.max(bounds.top, Math.min(position.y, bounds.bottom));
return dx * dx + dy * dy < radius * radius;
});
}
async function renderGraph() {
if (state.renderer) state.renderer.kill();
const graph = new graphology.MultiDirectedGraph();
for (const node of state.data.nodes) graph.addNode(node.display_id, {
label: nodeLabel(node), x: node.x, y: node.y,
size: node.representation === "raw" ? 3 : 4 + Math.log2(1 + node.instance_count),
size: node.representation === "raw" ? 3 : Math.min(6, 3 + Math.log2(1 + node.instance_count) / 8),
color: node.representation === "raw" ? colors.raw : colors[node.level], raw: node,
searchable: JSON.stringify(node).toLowerCase(),
});
for (const edge of state.data.edges) if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
graph.addDirectedEdgeWithKey(edge.display_id, edge.source, edge.target, {
size: edgeSize(edge), color: edge.representation === "raw" ? "#e0a94f" : "#718096",
size: 1, color: edge.representation === "raw" ? "#e0a94f" : "#718096",
type: "arrow", raw: edge,
});
}
state.graph = graph;
await runLayout();
state.renderer = new Sigma(graph, $("graph"), {defaultEdgeType: "arrow", enableEdgeEvents: true, nodeReducer: reduceNode, edgeReducer: reduceEdge});
state.renderer = new Sigma(graph, $("graph"), {
defaultEdgeType: "arrow", enableEdgeEvents: true,
labelColor: {color: "#e7edf5"}, labelWeight: "500",
labelDensity: 0.6, labelGridCellSize: 120, labelRenderedSizeThreshold: 0,
stagePadding: 60,
nodeReducer: reduceNode, edgeReducer: reduceEdge,
});
const labelRenderer = state.renderer.getSetting("labelRenderer");
state.renderer.setSetting("labelRenderer", (context, data, settings) => {
if (!labelIntersectsNode(context, data, settings)) labelRenderer(context, data, settings);
});
const hoverRenderer = state.renderer.getSetting("hoverRenderer");
state.renderer.setSetting("hoverRenderer", (context, data, settings) =>
hoverRenderer(context, data, {...settings, labelColor: {color: "#000000"}}));
state.renderer.on("clickNode", event => showNode(event.node));
state.renderer.on("clickEdge", event => showEdge(event.edge));
applyFilters();
@@ -111,7 +137,7 @@ async function runLayout() {
} else {
const layout = await new ELK().layout({
id: "root",
layoutOptions: {"elk.algorithm": "layered", "elk.direction": "RIGHT", "elk.spacing.nodeNode": "35", "elk.layered.spacing.nodeNodeBetweenLayers": "65"},
layoutOptions: {"elk.algorithm": "layered", "elk.direction": "DOWN", "elk.spacing.nodeNode": "35", "elk.layered.spacing.nodeNodeBetweenLayers": "65"},
children: state.graph.nodes().map(id => ({id, width: 28, height: 28})),
edges: state.graph.edges().map(id => ({id, sources: [state.graph.source(id)], targets: [state.graph.target(id)]})),
});
@@ -128,7 +154,7 @@ function reduceNode(id, attributes) {
if (state.motif) {
const members = new Set([state.motif.entry_aggregate_id, state.motif.exit_aggregate_id, ...state.motif.internal_aggregate_ids]);
const identity = motifIdentity(attributes.raw);
if (!members.has(identity)) { result.color = "#263341"; result.label = ""; }
if (!members.has(identity)) result.color = "#263341";
else if (identity === state.motif.entry_aggregate_id) { result.color = "#54d67b"; result.size *= 1.5; }
else if (identity === state.motif.exit_aggregate_id) { result.color = "#ff6f6f"; result.size *= 1.5; }
}
@@ -210,11 +236,32 @@ async function loadMappingRows(id) {
$("next").onclick = () => { state.mappingOffset += 25; loadMappingRows(id); };
}
function renderMotifs() {
$("motifs").innerHTML = state.motifs.length ? state.motifs.map((motif, index) => `<button class="motif" data-index="${index}">${html(motif.motif_kind)}: ${html(motif.entry_aggregate_id)}${html(motif.exit_aggregate_id)} (${motif.node_count})</button>`).join("") : "No motifs detected.";
$("motifs").innerHTML = state.motifs.length ? state.motifs.map((motif, index) => `<button class="motif" data-index="${index}">${html(motif.motif_kind)}: ${html(operationName(motif.entry_aggregate_id))}${html(operationName(motif.exit_aggregate_id))} (${motif.node_count})</button>`).join("") : "No motifs detected.";
document.querySelectorAll(".motif").forEach(button => button.onclick = () => selectMotif(state.motifs[button.dataset.index]));
}
function operationName(aggregateId) {
const node = state.data.nodes.find(item => motifIdentity(item) === aggregateId);
const name = node?.ssa_summary?.[0] || node?.ssa_name;
return name || aggregateId;
}
function fitMotif(motif) {
const members = new Set([motif.entry_aggregate_id, motif.exit_aggregate_id, ...motif.internal_aggregate_ids]);
const positions = state.graph.nodes()
.filter(id => members.has(motifIdentity(state.graph.getNodeAttribute(id, "raw"))))
.map(id => state.graph.getNodeAttributes(id));
if (!positions.length) return;
const xs = positions.map(node => node.x), ys = positions.map(node => node.y);
const width = Math.max(1, Math.max(...xs) - Math.min(...xs));
const height = Math.max(1, Math.max(...ys) - Math.min(...ys));
state.renderer.setCustomBBox({
x: [Math.min(...xs) - width * 0.1, Math.max(...xs) + width * 0.1],
y: [Math.min(...ys) - height * 0.1, Math.max(...ys) + height * 0.1],
});
state.renderer.refresh();
state.renderer.getCamera().animatedReset();
}
function selectMotif(motif) {
state.motif = motif; applyFilters();
state.motif = motif; applyFilters(); fitMotif(motif);
$("details").innerHTML = '<p class="muted">Structural SCC-condensed split/rejoin; rendered geometry is not evidence.</p>' + definition(motif, ["motif_kind", "entry_aggregate_id", "exit_aggregate_id", "internal_aggregate_ids", "branch_successor_aggregate_ids", "representative_paths", "node_count", "edge_count", "contains_cycle", "confidence"]);
}
async function loadSummary() {
@@ -223,10 +270,9 @@ async function loadSummary() {
$("mapping").innerHTML = '<option value="">All mappings</option>' + Object.keys(summary.mapping_classes).map(kind => `<option>${html(kind)}</option>`).join("");
}
$("report").onchange = () => { $("level").value = defaultViewForReport(selectedReport()); loadGraph(); };
$("level").onchange = () => { localStorage.setItem("raptor-graph-view", $("level").value); loadGraph(); };
$("metric").onchange = renderGraph;
$("report").onchange = () => { $("level").value = "operation"; updateLayoutButton(); loadGraph(); };
$("level").onchange = () => { updateLayoutButton(); loadGraph(); };
for (const id of ["search", "mapping", "tensor", "selfEdges"]) $(id).addEventListener(id === "selfEdges" ? "change" : "input", applyFilters);
$("layout").onclick = runLayout; $("fit").onclick = () => state.renderer?.getCamera().animatedReset();
$("layout").onclick = runLayout; $("fit").onclick = () => { state.renderer?.setCustomBBox(null); state.renderer?.refresh(); state.renderer?.getCamera().animatedReset(); };
$("expandAll").onclick = expandAllOperations; $("collapseAll").onclick = collapseAllOperations;
loadReports().catch(error => status(error.message, true));
@@ -15,17 +15,16 @@
<aside class="controls" aria-label="Graph controls">
<label>Report<select id="report"></select></label>
<label>Aggregation/view<select id="level"><option value="operation">Operation</option><option value="raw">Raw instances</option><option value="core">Core</option><option value="node_kind">Node kind</option></select></label>
<label>Edge metric<select id="metric"><option value="raw_edge_count">Count</option><option value="total_weight">Total weight</option></select></label>
<label>Search<input id="search" type="search" placeholder="ID, op, core, SSA"></label>
<label>Mapping<select id="mapping"><option value="">All mappings</option></select></label>
<label>Tensor type<input id="tensor" type="search" placeholder="substring"></label>
<label class="check"><input id="selfEdges" type="checkbox">Show self edges</label>
<div class="buttons"><button id="expandAll">Expand all operations</button><button id="collapseAll" hidden>Collapse all</button><button id="layout">Rerun layout</button><button id="fit">Fit graph</button></div>
<div class="buttons"><button id="expandAll">Expand all operations</button><button id="collapseAll" hidden>Collapse all</button><button id="layout">Reset layout</button><button id="fit">Fit graph</button></div>
<h2>Motifs</h2><div id="motifs" class="scroll">No motifs detected.</div>
<h2>Report statistics and diagnostics</h2><div id="summary" class="scroll"></div>
</aside>
<section id="graph" aria-label="Interactive directed graph"></section>
<aside class="details" aria-label="Selection details"><h2>Details</h2><div id="details">Select a node, edge, or motif.</div><canvas id="matrix" width="320" height="220" aria-label="Lane mapping heatmap" hidden></canvas><div id="mappingRows"></div></aside>
</main>
<script src="/app.js"></script>
<script src="/app.js?v=5"></script>
</body></html>
+41 -1
View File
@@ -8,7 +8,10 @@ from raptor_graph_explorer.api import create_app
def test_manifest_graph_motifs_and_pagination(built_output: Path):
client=TestClient(create_app(built_output,max_page_size=2))
assert "Raptor Graph Explorer" in client.get("/").text
page = client.get("/")
assert "Raptor Graph Explorer" in page.text
assert page.headers["cache-control"] == "no-store"
assert client.get("/app.js").headers["cache-control"] == "no-store"
assert "runLayout" in client.get("/app.js").text
assert client.get("/api/manifest").status_code==200
assert len(client.get("/api/reports").json())==2
@@ -43,3 +46,40 @@ def test_unsupported_database_schema_is_rejected(built_output: Path):
db.execute("UPDATE metadata SET value='999' WHERE key='schema_version'");db.commit();db.close()
with pytest.raises(ValueError,match="unsupported database schema"):
create_app(built_output)
def test_static_layout_and_label_settings(built_output: Path):
client = TestClient(create_app(built_output))
script = client.get("/app.js").text
assert 'id="metric"' not in client.get("/").text
assert "edgeSize" not in script and '$("metric")' not in script
assert "size: 1" in script
assert "localStorage" not in script
assert 'node.ssa_name || node.node_id' in script
assert 'node.ssa_summary.find(Boolean) || `op ${node.op_id}`' in script
motif_reducer = script[script.index("function reduceNode"):script.index("function reduceEdge")]
assert 'result.label = ""' not in motif_reducer
assert 'labelColor: {color: "#e7edf5"}' in script
assert "labelDensity: 0.6" in script
assert "labelGridCellSize: 120" in script
assert "labelRenderedSizeThreshold: 0" in script
assert "stagePadding: 60" in script
assert "forceLabel" not in script
assert 'getSetting("labelRenderer")' in script
assert "labelIntersectsNode" in script
assert "framedGraphToViewport" in script
assert "dx * dx + dy * dy < radius * radius" in script
assert "Math.min(6, 3 + Math.log2(1 + node.instance_count) / 8)" in script
assert 'getSetting("hoverRenderer")' in script
assert 'labelColor: {color: "#000000"}' in script
projection = script[script.index("async function rebuildProjection"):script.index("function expandOperation")]
assert projection.count("renderGraph()") == 1
assert '"Reset layout"' in script and '"Rerun layout"' in script
layout = script[script.index("async function runLayout"):script.index("function motifIdentity")]
assert '["operation", "raw"].includes' in layout
assert "new ELK().layout" in layout
assert '"elk.direction": "DOWN"' in layout
assert "fitMotif(motif)" in script
assert "setCustomBBox" in script
fit = script[script.index("function fitMotif"):script.index("function selectMotif")]
assert "setCustomBBox" in fit and "refresh()" in fit and "animatedReset()" in fit
+30 -1
View File
@@ -1,6 +1,7 @@
from pathlib import Path
from raptor_graph_explorer.cli import main
from raptor_graph_explorer.cli import build_output, main
from raptor_graph_explorer.database import connect_readonly
def test_build_inspect_and_exit_codes(tmp_path: Path, fixture_dir: Path, capsys):
@@ -11,3 +12,31 @@ def test_build_inspect_and_exit_codes(tmp_path: Path, fixture_dir: Path, capsys)
assert "demo_graph" in captured and "mappings:" in captured
assert main(["build",str(fixture_dir),"--output",str(output)])==2
assert main(["build",str(tmp_path/"missing"),"--output",str(tmp_path/"bad")])==2
def test_multi_report_build_finalizes_readonly_database(tmp_path: Path):
reports = tmp_path / "reports"; reports.mkdir()
header = "Id,op_id,lane,core,ssa_name\n"
edge_header = "Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n"
(reports / "a.nodes.csv").write_text(header + "gc:0,0,,,%0\n")
(reports / "a.edges.csv").write_text(edge_header)
(reports / "b.nodes.csv").write_text(header + "gc:0,0,,,%0\ngc:1,1,,,%1\n")
(reports / "b.edges.csv").write_text(
edge_header
+ "gc:0,gc:1,1,tensor<1xf32>,b,,,\n"
+ "gc:1,gc:missing,1,tensor<1xf32>,b,,,\n")
output = tmp_path / "output"
manifest = build_output([reports], output)
assert (output / "manifest.json").is_file()
assert [report["report_id"] for report in manifest["reports"]] == ["a", "b"]
assert manifest["diagnostic_count"] == 1
db = connect_readonly(output / "graph.sqlite")
counts = [tuple(row) for row in db.execute(
"SELECT report_id,raw_node_count,raw_edge_count,operation_node_count,operation_edge_count "
"FROM reports ORDER BY report_id")]
assert counts == [("a", 1, 0, 1, 0), ("b", 2, 1, 2, 1)]
assert tuple(db.execute("SELECT code,occurrence_count FROM diagnostics").fetchone()) == ("unknown_endpoint", 1)
assert db.execute("PRAGMA journal_mode").fetchone()[0] == "delete"
db.close()
@@ -5,6 +5,14 @@ import sqlite3
from pathlib import Path
from raptor_graph_explorer.cli import build_output
from raptor_graph_explorer.database import create_database
from raptor_graph_explorer.ingest import _read_edges, _read_nodes
from raptor_graph_explorer.schema import DiagnosticCollector, ReportPair
def _temporary_tables(db: sqlite3.Connection) -> list[str]:
return [row[0] for row in db.execute(
"SELECT name FROM sqlite_temp_master WHERE type='table' ORDER BY name")]
def test_normalization_unknown_columns_prefix_and_lane_fallback(built_output: Path):
@@ -56,3 +64,47 @@ def test_explicit_edge_lane_precedes_endpoint_lane(tmp_path: Path):
assert db.execute("SELECT source_lane,target_lane FROM raw_edges").fetchone()==(9,8)
assert db.execute("SELECT occurrence_count FROM diagnostics WHERE code='contradictory_lane'").fetchone()==(1,)
db.close()
def test_ingestion_drops_staging_tables_on_success_and_missing_columns(tmp_path: Path):
nodes = tmp_path / "good.nodes.csv"
nodes.write_text("Id,op_id,lane,core,ssa_name\ngc:0,0,,,%0\ngc:1,1,,,%1\n")
edges = tmp_path / "good.edges.csv"
edges.write_text(
"Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n"
"gc:0,gc:1,1,tensor<1xf32>,good,,,\n")
pair = ReportPair("good", nodes, edges)
diagnostics = DiagnosticCollector()
db = create_database(tmp_path / "graph.sqlite")
db.execute("INSERT INTO reports(report_id,stage) VALUES('good','good')")
assert _read_nodes(db, pair, "good", diagnostics) == 2
assert _temporary_tables(db) == []
assert _read_edges(db, pair, "good", diagnostics) == 1
assert _temporary_tables(db) == []
missing_nodes = tmp_path / "missing.nodes.csv"
missing_nodes.write_text("Id,op_id,lane,core\ngc:2,2,,,\n")
db.execute("INSERT INTO reports(report_id,stage) VALUES('missing','missing')")
assert _read_nodes(db, ReportPair("missing", missing_nodes, edges), "missing", diagnostics) == 0
assert _temporary_tables(db) == []
missing_edges = tmp_path / "missing.edges.csv"
missing_edges.write_text("Source,Target\ngc:0,gc:1\n")
assert _read_edges(db, ReportPair("good", nodes, missing_edges), "good", diagnostics) == 0
assert _temporary_tables(db) == []
db.close()
def test_required_columns_store_literal_empty_attributes(tmp_path: Path):
reports = tmp_path / "reports"; reports.mkdir()
(reports / "x.nodes.csv").write_text(
"Id,op_id,lane,core,ssa_name\ngc:0,0,,,%0\ngc:1,1,,,%1\n")
(reports / "x.edges.csv").write_text(
"Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n"
"gc:0,gc:1,1,tensor<1xf32>,x,,,\n")
output = tmp_path / "out"; build_output([reports], output)
db = sqlite3.connect(output / "graph.sqlite")
assert db.execute("SELECT DISTINCT attributes_json FROM raw_nodes").fetchall() == [("{}",)]
assert db.execute("SELECT DISTINCT attributes_json FROM raw_edges").fetchall() == [("{}",)]
db.close()
@@ -49,3 +49,9 @@ def test_external_entrance_rejected():
def test_external_exit_rejected():
edges=[("s","a"),("s","b"),("a","t"),("b","t"),("a","x")]
assert detect_motifs(graph(edges))[0] == []
def test_motifs_are_sorted_by_topological_rank_not_node_name():
edges = [("9", "a"), ("9", "b"), ("a", "z"), ("b", "z"), ("z", "10"),
("10", "c"), ("10", "d"), ("c", "20"), ("d", "20")]
assert [motif.entry for motif in detect_motifs(graph(edges))[0]] == ["9", "10"]
@@ -6,8 +6,9 @@ from fastapi.testclient import TestClient
import pytest
from raptor_graph_explorer.api import create_app
from raptor_graph_explorer.cli import build_output
from raptor_graph_explorer.database import connect_readonly
from raptor_graph_explorer.projection import DisplayGraphTooLarge, display_graph
from raptor_graph_explorer.projection import DisplayGraphTooLarge, LANE_SPACING, display_graph
def project(output: Path, report_id: str, expanded=(), *, view="operation", cap=5_000):
@@ -18,6 +19,36 @@ def project(output: Path, report_id: str, expanded=(), *, view="operation", cap=
db.close()
@pytest.fixture
def sparse_projection_output(tmp_path: Path) -> Path:
reports = tmp_path / "reports"; reports.mkdir()
(reports / "sparse.nodes.csv").write_text(
"Id,op_id,lane,core,ssa_name\n"
"gc:0,0,,,%0\n"
"gcb:1:0:a,1,0,,%1\n"
"gcb:1:0:b,1,0,,%2\n"
"gcb:1:0:c,1,0,,%3\n"
"gcb:1:1000000,1,1000000,,%4\n"
"gc:1:scalar_a,1,,,%5\n"
"gc:1:scalar_b,1,,,%6\n"
"gc:2,2,,,%7\n")
edge_header = "Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n"
pairs = [
("gc:0", "gcb:1:0:a"),
("gcb:1:0:a", "gcb:1:0:b"),
("gcb:1:0:b", "gcb:1:0:c"),
("gcb:1:0:c", "gcb:1:1000000"),
("gcb:1:1000000", "gc:1:scalar_a"),
("gc:1:scalar_a", "gc:1:scalar_b"),
("gc:1:scalar_b", "gc:2"),
]
(reports / "sparse.edges.csv").write_text(
edge_header + "".join(
f"{source},{target},1,tensor<1xf32>,sparse,,,\n" for source, target in pairs))
output = tmp_path / "output"; build_output([reports], output)
return output
def test_spatial1_baseline_and_one_operation_expansion(regression_output: Path):
db = connect_readonly(regression_output / "graph.sqlite")
source_counts = db.execute("""
@@ -113,7 +144,7 @@ def test_display_api_and_static_raw_interactions(regression_output: Path):
index = client.get("/").text
script = client.get("/app.js").text
assert '<option value="raw">Raw instances</option>' in index
assert 'report?.stage === "spatial4"' in script
assert '$("level").value = "operation"' in script
assert 'local.representation === "raw"' in script
reducer = script[script.index("function reduceEdge"):script.index("function applyFilters")]
assert "state.graph.source(id)" not in reducer
@@ -123,3 +154,52 @@ def test_display_api_and_static_raw_interactions(regression_output: Path):
assert "return;" in script[raw_edge_branch:aggregate_request]
aggregate_edge = "operation:spatial1_graph:1->2"
assert client.get(f"/api/aggregate-edges/{aggregate_edge}/matrix").status_code == 200
def test_collapsed_sparse_lanes_have_compact_deterministic_extent(sparse_projection_output: Path):
first = project(sparse_projection_output, "sparse")
second = project(sparse_projection_output, "sparse")
aggregate_coordinates = [(node["x"], node["y"]) for node in first["nodes"]]
assert first["nodes"] == second["nodes"]
assert len(set(aggregate_coordinates)) == 3
assert max(y for _, y in aggregate_coordinates) - min(y for _, y in aggregate_coordinates) < 100 * LANE_SPACING
def test_operation_edges_are_laid_out_top_to_bottom(sparse_projection_output: Path):
graph = project(sparse_projection_output, "sparse")
y = {node["op_id"]: node["y"] for node in graph["nodes"]}
cross_operation = [edge for edge in graph["edges"] if edge["source_op_id"] != edge["target_op_id"]]
assert cross_operation
assert all(y[edge["source_op_id"]] > y[edge["target_op_id"]] for edge in cross_operation)
def test_expanded_sparse_lanes_use_compact_rows_and_valid_endpoints(sparse_projection_output: Path):
graph = project(sparse_projection_output, "sparse", [1])
nodes = {node["node_id"]: node for node in graph["nodes"] if node["representation"] == "raw"}
assert nodes["gcb:1:1000000"]["y"] - nodes["gcb:1:0:a"]["y"] == -LANE_SPACING
assert "operation:sparse:1" not in {node["display_id"] for node in graph["nodes"]}
displayed = {node["display_id"] for node in graph["nodes"]}
assert all(edge["source"] in displayed and edge["target"] in displayed for edge in graph["edges"])
def test_same_lane_duplicates_are_centered_and_deterministic(sparse_projection_output: Path):
collapsed = project(sparse_projection_output, "sparse")
anchor_x = next(node["x"] for node in collapsed["nodes"] if node["op_id"] == 1)
first = project(sparse_projection_output, "sparse", [1])
second = project(sparse_projection_output, "sparse", [1])
duplicate_ids = ["gcb:1:0:a", "gcb:1:0:b", "gcb:1:0:c"]
nodes = {node["node_id"]: node for node in first["nodes"] if node["representation"] == "raw"}
xs = [nodes[node_id]["x"] for node_id in duplicate_ids]
assert len({nodes[node_id]["y"] for node_id in duplicate_ids}) == 1
assert len(set(xs)) == 3
assert sum(xs) / len(xs) == pytest.approx(anchor_x)
assert first["nodes"] == second["nodes"]
def test_lane_less_nodes_follow_lane_rows_in_node_id_order(sparse_projection_output: Path):
graph = project(sparse_projection_output, "sparse", [1])
nodes = {node["node_id"]: node for node in graph["nodes"] if node["representation"] == "raw"}
lane_y = min(node["y"] for node in nodes.values() if node["lane"] is not None)
scalar_y = [nodes[node_id]["y"] for node_id in ("gc:1:scalar_a", "gc:1:scalar_b")]
assert scalar_y[0] < lane_y
assert scalar_y[1] - scalar_y[0] == -LANE_SPACING
+270 -136
View File
@@ -1,174 +1,308 @@
# Validation Operations
# Operation Validation Suite
ONNX test models used by `validate.py` to verify the Raptor compiler + PIM simulator pipeline.
This directory contains the ONNX models used by `validation/validate.py` to
validate individual operations through compilation, PIM simulation, and
comparison with the ONNX-MLIR reference runtime.
Generated tests can be regenerated with:
## Naming
```
python3 validation/operations/gen_tests.py
Every model uses the same path convention:
```text
<category>/<case>/<category>_<case>.onnx
```
## Conv
The `<category>/<case>` pair is the operation ID printed by the validator.
Use lowercase `snake_case` for both components. Keep the generator function,
graph name, directory, and filename based on the same operation ID when adding
a test.
| Test | Directory | Input | Output | Kernel | Stride | Padding | Bias | Notes |
|------------------|-------------------------|-----------|-----------|--------|--------|------------|------|------------------------------------|
| Simple | `conv/simple` | [1,3,3,3] | [1,1,2,2] | 2x2 | 1 | none | no | Basic conv, hand-crafted |
| With constant | `conv/with_constant` | [1,3,3,3] | [1,1,3,3] | 2x2 | 1 | SAME_UPPER | yes | Hand-crafted, constant weight+bias |
| Batch 2 | `conv/batch_2` | [2,3,3,3] | [2,1,3,3] | 2x2 | 1 | SAME_UPPER | yes | Batched input |
| Kernel 3x3 | `conv/kernel_3x3` | [1,1,5,5] | [1,1,3,3] | 3x3 | 1 | none | no | Larger kernel |
| Stride 2 | `conv/stride_2` | [1,1,6,6] | [1,1,2,2] | 3x3 | 2 | none | no | Strided convolution |
| Multi channel | `conv/multi_channel` | [1,3,5,5] | [1,4,3,3] | 3x3 | 1 | none | no | 3 in channels, 4 out channels |
| Pointwise 1x1 | `conv/pointwise_1x1` | [1,8,4,4] | [1,4,4,4] | 1x1 | 1 | none | no | Channel mixing |
| SAME padding 3x3 | `conv/same_padding_3x3` | [1,1,5,5] | [1,1,5,5] | 3x3 | 1 | SAME_UPPER | no | Spatial dims preserved |
| Explicit padding | `conv/explicit_padding` | [1,1,4,4] | [1,1,4,4] | 3x3 | 1 | [1,1,1,1] | no | Symmetric explicit pads |
| With bias 3x3 | `conv/with_bias_3x3` | [1,3,5,5] | [1,2,3,3] | 3x3 | 1 | none | yes | Multi-channel with bias |
| Large spatial | `conv/large_spatial` | [1,1,8,8] | [1,1,6,6] | 3x3 | 1 | none | no | Larger spatial input |
| Grouped two groups | `conv/grouped_two_groups` | [1,4,4,4] | [1,4,4,4] | 1x1 | 1 | none | yes | group=2 channel partitioning |
| Depthwise grouped | `conv/depthwise_grouped` | [1,3,4,4] | [1,3,2,2] | 3x3 | 1 | none | no | group=3, one input channel per group |
| Dynamic | `conv/dynamic` | [1,1,4,4] | [1,1,2,2] | 3x3 | 1 | none | no | Runtime input and weight |
## Generate and validate
## Gemm
Regenerate all generated models from the repository root:
| Test | Directory | A (input) | B/W tensor | Output | transB | alpha | beta | Bias | Notes |
|---------------|-------------------------|-----------|------------|----------|--------|-------|------|-------|------------------------------|
| Simple | `gemm/simple` | [10,132] | [132,132] | [10,132] | no | 1 | 1 | no | Square weights |
| Non-square | `gemm/non_square` | [4,128] | [128,64] | [4,64] | no | 1 | 1 | no | K != N |
| With bias | `gemm/with_bias` | [4,128] | [128,128] | [4,128] | no | 1 | 1 | [128] | Bias vector |
| transB | `gemm/transB` | [4,128] | [64,128] | [4,64] | yes | 1 | 1 | no | Transposed weight |
| Alpha/beta | `gemm/alpha_beta` | [4,64] | [64,64] | [4,64] | no | 0.5 | 0.25 | [64] | Scaled matmul + bias |
| Small | `gemm/small` | [2,8] | [8,4] | [2,4] | no | 1 | 1 | no | Tiny matrices |
| Large | `gemm/large` | [8,256] | [256,128] | [8,128] | no | 1 | 1 | no | Larger matrices |
| transB + bias | `gemm/transB_with_bias` | [4,128] | [64,128] | [4,64] | yes | 1 | 1 | [64] | Combined |
| Dynamic | `gemm/dynamic` | [2,8] | [8,4] | [2,4] | no | 1 | 1 | no | Runtime matrix operands |
| Dynamic transB | `gemm/dynamic_transB` | [2,8] | [4,8] | [2,4] | yes | 1 | 1 | no | Runtime transpose handling |
| Dynamic bias | `gemm/dynamic_bias` | [2,8] | [8,4] | [2,4] | no | 1 | 1 | [4] | Runtime bias broadcast |
| Dynamic alpha | `gemm/dynamic_alpha` | [2,8] | [8,4] | [2,4] | no | 0.5 | 1 | no | Runtime alpha scaling |
| Dynamic beta | `gemm/dynamic_beta` | [2,8] | [8,4] | [2,4] | no | 1 | 2 | [4] | Runtime beta scaling |
| Dynamic bias + scale | `gemm/dynamic_bias_alpha_beta` | [2,8] | [8,4] | [2,4] | no | 0.5 | 2 | [4] | Runtime operands and bias |
```bash
.venv/bin/python validation/operations/gen_tests.py
```
## MatMul
Run the complete suite with deadlock detection:
| Test | Directory | A input | B tensor | Output | Notes |
|---------------------|----------------------------------|----------|----------|---------|-------------------------------------------------|
| Basic | `matmul/basic` | [2,3] | [3,4] | [2,4] | Direct 2D MatMul rewrite path |
| Left constant | `matmul/left_constant` | [2,3] | [3,4] | [2,4] | Constant LHS transpose rewrite path |
| Dynamic | `matmul/dynamic` | [2,3] | [3,4] | [2,4] | Runtime matrix operands |
| Batched 3D | `matmul/batched_3d` | [2,2,3] | [2,3,4] | [2,2,4] | Matching-batch direct batched lowering |
| Batched 3D dynamic | `matmul/batched_3d_dynamic` | [2,2,3] | [2,3,4] | [2,2,4] | Batched runtime operands |
| Batched left const | `matmul/batched_left_constant` | [2,2,3] | [2,3,4] | [2,2,4] | Batched constant-LHS transpose path |
| Batched RHS broadcast | `matmul/batched_rhs_broadcast` | [2,2,3] | [3,4] | [2,2,4] | Rank-2 RHS broadcast across batch |
| Batched LHS broadcast | `matmul/batched_lhs_broadcast` | [2,3] | [2,3,4] | [2,2,4] | Rank-2 LHS broadcast across batched RHS |
```bash
.venv/bin/python validation/validate.py \
--raptor-path build_release/Release/bin/onnx-mlir \
--onnx-include-dir onnx-mlir/include \
--operations-dir validation/operations \
--crossbar-count 64 \
--crossbar-size 128 \
--core-count 144 \
--raptor-extra-arg=--pim-detect-communication-deadlock \
--raptor-extra-arg=--pim-export-spatial-dataflow=none
```
## Gemv
Use `--compile-only` for compiler and deadlock checks, then `--run-only` to
reuse those artifacts for reference execution, simulation, and comparison.
The validator prints the complete operation results table before its summary.
| Test | Directory | Input | W (weight) | Output | Bias | Notes |
|---------------------|------------------------------------|----------|------------|---------|---------|----------------------------|
| Simple | `gemv/simple` | [1,132] | [132,132] | [1,132] | no | Single-sample matmul |
| Constant | `gemv/constant` | _(none)_ | [132,132] | [1,132] | no | All inputs constant |
| Homogeneous const | `gemv/with_homogeneous_constant` | [1,132] | [132,132] | [1,132] | [1,132] | Bias matches output shape |
| Heterogeneous const | `gemv/with_heterogeneous_constant` | [1,132] | [132,132] | [1,132] | [1,132] | Different constant pattern |
| Scalar const | `gemv/with_scalar_constant` | [1,132] | [132,132] | [1,132] | [1,1] | Scalar bias, broadcast |
## Complete inventory
## Pool
The suite contains 164 models. Tensor shapes, attributes, and constants are
defined in `gen_tests.py` and in the checked-in ONNX models.
| Test | Directory | Input | Output | Kernel | Stride | Padding | Notes |
|----------------------------|---------------------------------|-----------|-----------|------------------------|--------|------------|----------------------------------|
| Max basic | `pool/max_basic` | [1,1,4,4] | [1,1,3,3] | 2x2 | 1 | none | Basic max pooling |
| Max stride 2 multi-channel | `pool/max_stride2_multichannel` | [1,5,6,6] | [1,5,3,3] | 2x2 | 2 | none | Channel-preserving max pool |
| Max SAME_UPPER | `pool/max_same_upper` | [1,1,5,5] | [1,1,3,3] | 3x3 | 2 | SAME_UPPER | Deprecated auto_pad path |
| Avg basic | `pool/avg_basic` | [1,3,4,4] | [1,3,3,3] | 2x2 | 1 | none | Basic average pooling |
| Avg explicit padding | `pool/avg_explicit_padding` | [1,2,4,4] | [1,2,2,2] | 3x3 | 2 | [1,1,1,1] | `count_include_pad=0` |
| Avg include pad | `pool/avg_include_pad` | [1,2,4,4] | [1,2,2,2] | 3x3 | 2 | [1,1,1,1] | `count_include_pad=1` |
| Max after Conv | `pool/max_after_conv` | [1,3,6,6] | [1,4,2,2] | Conv 3x3 then Pool 2x2 | 2 | none | Regression for `pool(conv(...))` |
### Add (5)
## ReduceMean
| Case | Description |
|---|---|
| `after_gemm` | Gemm followed by Add with a broadcast bias vector. |
| `basic` | Elementwise Add on two inputs with identical shapes. |
| `broadcast_row` | Elementwise Add with row-vector broadcasting. |
| `channel_broadcast_1024` | NCHW per-channel broadcasting over 1024 channels. |
| `leading_dimension_broadcast` | Trailing-dimension broadcasting across leading dimensions. |
| Test | Directory | Input | Output | Axes | Keepdims | Notes |
|------------|--------------------------|-----------|-----------|-------|----------|-------------------------------------------------|
| Basic | `reduce_mean/basic` | [4,8] | [4,1] | [1] | 1 | Reduce feature dimension, preserving rank |
| Keepdims 0 | `reduce_mean/keepdims_0` | [4,8] | [4] | [1] | 0 | Reduce feature dimension, dropping reduced axis |
| 4D spatial | `reduce_mean/4d_spatial` | [1,3,4,4] | [1,3,1,1] | [2,3] | 1 | Reduce H and W on NCHW input |
| After Conv | `reduce_mean/after_conv` | [1,3,5,5] | [1,2,1,1] | [2,3] | 1 | Conv 3x3 + bias, then spatial ReduceMean |
### Concat (3)
## Relu
| Case | Description |
|---|---|
| `channel_axis` | Concatenates two runtime NCHW tensors along the channel axis. |
| `negative_axis` | Concatenates tensors using a negative axis. |
| `three_inputs_channel_axis` | Concatenates three runtime NCHW tensors along the channel axis. |
| Test | Directory | Input | Output | Notes |
|------------|-------------------|-----------|-----------|----------------------------|
| Basic | `relu/basic` | [4,8] | [4,8] | Standalone 2D Relu |
| 4D | `relu/4d` | [2,3,4,4] | [2,3,4,4] | Standalone NCHW Relu |
| After Conv | `relu/after_conv` | [1,3,5,5] | [1,2,3,3] | Conv 3x3 + bias, then Relu |
| After Gemm | `relu/after_gemm` | [4,64] | [4,32] | Gemm + bias, then Relu |
### Conv (31)
## Sigmoid
| Case | Description |
|---|---|
| `batch_2` | Batched Conv with SAME_UPPER padding and bias. |
| `batch_4_pointwise` | Pointwise Conv with batch size four. |
| `depthwise_1024_channels` | Depthwise pointwise Conv with 1024 groups. |
| `depthwise_grouped` | Depthwise-style grouped Conv with one input channel per group. |
| `dilated_3x3` | Conv with a dilated 3x3 kernel. |
| `dynamic` | Conv with runtime input and weight tensors. |
| `explicit_padding` | 3x3 Conv with symmetric explicit padding. |
| `grouped_many_groups` | Pointwise Conv with many groups and high channel counts. |
| `grouped_two_groups` | Two-group pointwise Conv with bias. |
| `huge_pointwise_1024` | Pointwise Conv with 1024 input and output channels. |
| `huge_pointwise_1024_dynamic` | The 1024-channel pointwise Conv with runtime weights. |
| `kernel_3x3` | Basic 3x3 Conv without padding. |
| `kernel_equals_input_spatial` | Conv whose kernel covers the full spatial input. |
| `large_input_channels_1x1` | Pointwise Conv with 1024 input channels and modest output width. |
| `large_output_channels_1x1` | Pointwise Conv with modest input width and 1024 output channels. |
| `large_spatial` | 3x3 Conv on a larger spatial input. |
| `multi_channel` | 3x3 Conv with multiple input and output channels. |
| `non_square_kernel_1x3` | Conv with a non-square 1x3 kernel. |
| `non_square_kernel_3x1` | Conv with a non-square 3x1 kernel. |
| `non_uniform_stride` | Conv with different height and width strides. |
| `pointwise_1x1` | Basic pointwise channel-mixing Conv. |
| `pointwise_tiled_chain` | Relu and chained pointwise Convs with a tiled intermediate. |
| `real_asymmetric_padding` | Conv with asymmetric explicit padding. |
| `relu_conv_store` | Conv, Relu, and a second Conv to validate an intermediate stored result. |
| `same_lower_3x3` | 3x3 Conv with SAME_LOWER padding. |
| `same_padding_3x3` | 3x3 Conv with SAME_UPPER padding. |
| `simple` | Hand-authored basic 2x2 Conv. |
| `stride_2` | 3x3 Conv with stride two. |
| `with_bias_3x3` | Multi-channel 3x3 Conv with bias. |
| `with_constant` | Hand-authored SAME_UPPER Conv with constant weight and bias. |
| `without_kernel_shape_attr` | Conv whose kernel shape is inferred from its weight tensor. |
| Test | Directory | Input | Output | Notes |
|------------|----------------------|-----------|-----------|---------------------------|
| Basic | `sigmoid/basic` | [4,8] | [4,8] | Standalone 2D Sigmoid |
| 4D | `sigmoid/4d` | [2,3,4,4] | [2,3,4,4] | Standalone NCHW Sigmoid |
| After Gemm | `sigmoid/after_gemm` | [4,64] | [4,32] | Gemm + bias, then Sigmoid |
### Div (6)
## Softmax
| Case | Description |
|---|---|
| `after_gemm` | Gemm followed by Div with a broadcast divisor vector. |
| `basic` | Elementwise Div by a same-shape constant tensor. |
| `channel_broadcast_1024` | Div with NCHW per-channel broadcasting over 1024 channels. |
| `leading_dimension_broadcast` | Div with trailing-dimension broadcasting. |
| `runtime_scalar_rhs` | Div of a runtime tensor by a scalar initializer. |
| `scalar_constant` | Div with scalar broadcasting on a 2D tensor. |
| Test | Directory | Input | Output | Axis | Notes |
|--------------|--------------------------|-------------|-------------|------|---------------------------------|
| Basic | `softmax/basic` | [3,5] | [3,5] | 1 | Row-wise softmax over features |
| 3D last axis | `softmax/3d_last_axis` | [2,3,4] | [2,3,4] | 2 | Last-dimension normalization |
| Channel axis | `softmax/channel_axis` | [1,3,2,2] | [1,3,2,2] | 1 | NCHW channel-wise softmax |
### Gather (5)
## Resize
| Case | Description |
|---|---|
| `3d_input_axis1` | Gathers along axis one of a 3D input. |
| `axis0_matrix_indices` | Gathers rows using a 2D indices tensor. |
| `axis1` | Gathers selected columns from a 2D tensor. |
| `negative_axis` | Gathers using a negative axis. |
| `negative_indices` | Gathers with negative indices along axis zero. |
| Test | Directory | Input | Output | Mode | Notes |
|---------------------|-------------------------|-----------|-----------|---------|-----------------------------------------|
| Nearest 2x | `resize/nearest_2x` | [1,1,2,3] | [1,1,4,6] | nearest | NCHW upsampling with scales [1,1,2,2] |
| Non-uniform scales | `resize/non_uniform` | [1,1,2,3] | [1,1,6,6] | nearest | Different height/width scaling factors |
| Explicit sizes | `resize/with_sizes` | [1,1,2,3] | [1,1,3,5] | nearest | Sizes input used instead of scales |
### Gemm (21)
## Split
| Case | Description |
|---|---|
| `alpha_beta` | Applies non-default alpha and beta scaling with bias. |
| `bias_rank2_broadcast` | Broadcasts a rank-2 bias across output rows. |
| `dynamic` | Uses both matrix operands at runtime. |
| `dynamic_alpha` | Uses runtime operands with non-default alpha scaling. |
| `dynamic_beta` | Uses runtime operands and bias with non-default beta scaling. |
| `dynamic_bias` | Uses runtime matrix operands and runtime bias. |
| `dynamic_bias_alpha_beta` | Combines runtime operands and bias with alpha and beta scaling. |
| `dynamic_transB` | Transposes a runtime right-hand matrix. |
| `huge_1024` | Uses 1024-wide inner and output dimensions. |
| `large` | Exercises larger rectangular matrices. |
| `large_k_small_n` | Uses a large reduction dimension and narrow output. |
| `non_square` | Uses different reduction and output widths. |
| `scalar_bias` | Broadcasts a scalar bias to the full output. |
| `simple` | Basic Gemm with square weights. |
| `small` | Tiny Gemm for fast focused validation. |
| `small_k_large_n` | Uses a modest reduction dimension and wide output. |
| `transA` | Transposes the left-hand matrix. |
| `transA_transB` | Transposes both matrix operands. |
| `transB` | Transposes the right-hand weight matrix. |
| `transB_with_bias` | Combines a transposed weight matrix with bias. |
| `with_bias` | Basic matrix product with vector bias. |
| Test | Directory | Input | Outputs | Axis | Notes |
|-----------------|---------------------------|-------|----------------------|------|-------------------------------------|
| Basic | `split/basic` | [2,6] | [2,2], [2,4] | 1 | Two-way split with explicit sizes |
| Equal three-way | `split/equal_three_way` | [2,6] | [2,2], [2,2], [2,2] | 1 | Optional split input omitted |
### Gemv (5)
## Gather
| Case | Description |
|---|---|
| `constant` | Vector-matrix product with all inputs constant. |
| `simple` | Basic single-row vector-matrix product. |
| `with_heterogeneous_constant` | Adds a non-uniform constant bias pattern. |
| `with_homogeneous_constant` | Adds a constant bias matching the output shape. |
| `with_scalar_constant` | Adds a scalar broadcast bias. |
| Test | Directory | Input | Indices | Output | Axis | Notes |
|----------------------|--------------------------------|-------|---------|----------|------|--------------------------------|
| Axis 1 | `gather/axis1` | [3,4] | [2] | [3,2] | 1 | Select two columns |
| Axis 0 matrix indices| `gather/axis0_matrix_indices` | [4,3] | [2,2] | [2,2,3] | 0 | Gather rows with 2D indices |
### MatMul (11)
## Concat
| Case | Description |
|---|---|
| `basic` | Direct 2D MatMul with constant right-hand matrix. |
| `batched_3d` | Batched 3D MatMul with matching batch dimensions. |
| `batched_3d_dynamic` | Batched 3D MatMul with both operands at runtime. |
| `batched_left_constant` | Batched 3D MatMul with constant left-hand matrix. |
| `batched_lhs_broadcast` | Broadcasts a 2D left-hand matrix across a batched right-hand tensor. |
| `batched_rhs_broadcast` | Broadcasts a 2D right-hand matrix across a batched left-hand tensor. |
| `dynamic` | Direct 2D MatMul with both operands at runtime. |
| `huge_1024` | Uses 1024-wide inner and output dimensions. |
| `left_constant` | Direct 2D MatMul with constant left-hand matrix. |
| `matrix_vector` | Matrix-vector multiplication producing a 1D output. |
| `vector_matrix` | Vector-matrix multiplication producing a 1D output. |
| Test | Directory | Input(s) | Output | Axis | Notes |
|--------------|-----------------------|---------------------------|-----------|------|-----------------------------|
| Channel axis | `concat/channel_axis` | A:[1,1,2,2], B:[1,2,2,2] | [1,3,2,2] | 1 | Runtime NCHW channel concat |
### Mul (5)
## Reshape
| Case | Description |
|---|---|
| `after_conv` | Conv followed by per-channel scaling. |
| `basic` | Elementwise Mul on two inputs with identical shapes. |
| `channel_broadcast_1024` | Mul with NCHW per-channel broadcasting over 1024 channels. |
| `leading_dimension_broadcast` | Mul with trailing-dimension broadcasting. |
| `scalar_constant` | Mul with scalar broadcasting. |
| Test | Directory | Input | Output | Notes |
|-----------|---------------------|-------|--------|----------------------------------------------|
| Same rank | `reshape/same_rank` | [2,3] | [3,2] | Runtime tensor with static shape initializer |
### Pool (15)
## Add
| Case | Description |
|---|---|
| `avg_basic` | AveragePool with a 2x2 kernel and unit stride. |
| `avg_ceil_mode` | AveragePool with ceil mode enabled. |
| `avg_explicit_padding` | Explicitly padded AveragePool excluding pad from the divisor. |
| `avg_include_pad` | Explicitly padded AveragePool including pad in the divisor. |
| `avg_large_channels` | AveragePool with a large channel count and small spatial extent. |
| `avg_non_uniform_stride` | AveragePool with different height and width strides. |
| `avg_real_asymmetric_padding` | AveragePool with asymmetric explicit padding. |
| `max_after_conv` | Conv followed by MaxPool. |
| `max_basic` | MaxPool with a 2x2 kernel and unit stride. |
| `max_ceil_mode` | MaxPool with ceil mode enabled. |
| `max_global_style_kernel_equals_input` | MaxPool whose kernel covers the full spatial input. |
| `max_non_square_kernel` | MaxPool with a non-square kernel. |
| `max_real_asymmetric_padding` | MaxPool with asymmetric explicit padding. |
| `max_same_upper` | MaxPool with SAME_UPPER padding. |
| `max_stride2_multichannel` | Multi-channel MaxPool with stride two. |
| Test | Directory | Input(s) | Output | Notes |
|---------------|---------------------|------------------|--------|---------------------------------------------|
| Basic | `add/basic` | A:[4,8], B:[4,8] | [4,8] | Elementwise add, same-shape inputs |
| Broadcast row | `add/broadcast_row` | A:[4,8], B:[8] | [4,8] | Row-vector broadcasting via initializer |
| After Gemm | `add/after_gemm` | A:[4,64], D:[32] | [4,32] | Gemm + bias, then Add with broadcast vector |
### ReduceMean (17)
## Mul
| Case | Description |
|---|---|
| `4d_spatial` | Reduces height and width of an NCHW tensor while preserving rank. |
| `4d_spatial_keepdims_0` | Reduces NCHW height and width while dropping those axes. |
| `after_conv` | Conv followed by a spatial ReduceMean. |
| `all_axes_keepdims_0` | Reduces all axes to a scalar. |
| `all_axes_keepdims_1` | Reduces all axes while preserving rank. |
| `basic` | Reduces a feature dimension while preserving rank. |
| `channel_axis_nchw` | Reduces the channel axis of an NCHW tensor. |
| `keepdims_0` | Reduces a feature dimension and drops that axis. |
| `large_dimension_1024` | Reduces a dimension of length 1024. |
| `legacy_axes_1_2_keepdims_1` | Opset-18 reduction over multiple positive axes. |
| `legacy_axis1_keepdims_0` | Opset-18 reduction over one axis while dropping it. |
| `legacy_axis1_keepdims_1` | Opset-18 reduction over one axis while preserving rank. |
| `legacy_empty_axes_noop` | Opset-18 empty-axes no-op followed by Relu. |
| `legacy_nchw_spatial` | Opset-18 spatial reduction on NCHW input. |
| `legacy_negative_axis` | Opset-18 reduction using a negative axis. |
| `legacy_reduce_all_keepdims_1` | Opset-18 all-axis reduction with the axes input omitted. |
| `negative_axis` | ReduceMean using a negative axis. |
| Test | Directory | Input(s) | Output | Notes |
|-----------------|-----------------------|--------------------------|-----------|-------------------------------------------|
| Basic | `mul/basic` | A:[4,8], B:[4,8] | [4,8] | Elementwise multiply, same-shape inputs |
| Scalar constant | `mul/scalar_constant` | X:[4,8], S:[1] | [4,8] | Scalar broadcasting via initializer |
| After Conv | `mul/after_conv` | X:[1,3,5,5], S:[1,2,1,1] | [1,2,3,3] | Conv 3x3 + bias, then per-channel scaling |
### Relu (4)
## Div
| Case | Description |
|---|---|
| `4d` | Standalone Relu on an NCHW tensor. |
| `after_conv` | Conv followed by Relu. |
| `after_gemm` | Gemm followed by Relu. |
| `basic` | Standalone Relu on a 2D tensor. |
| Test | Directory | Input(s) | Output | Notes |
|-----------------|-----------------------|------------------|--------|------------------------------------------------------|
| Basic | `div/basic` | X:[4,8], D:[4,8] | [4,8] | Elementwise divide by same-shape constant tensor |
| Scalar constant | `div/scalar_constant` | X:[4,8], S:[1] | [4,8] | Scalar broadcasting via initializer |
| After Gemm | `div/after_gemm` | A:[4,64], D:[32] | [4,32] | Gemm + bias, then Div with positive broadcast vector |
### Reshape (4)
| Case | Description |
|---|---|
| `4d_to_2d_flatten` | Flattens a 4D tensor into a 2D view. |
| `infer_dim_minus_one` | Uses `-1` to infer one output dimension. |
| `same_rank` | Changes shape without changing rank. |
| `zero_copies_input_dim` | Uses `0` to copy an input dimension. |
### Resize (6)
| Case | Description |
|---|---|
| `height_only` | Nearest-neighbor resize of only the height dimension. |
| `nearest_2x` | Nearest-neighbor upsampling by a factor of two. |
| `nearest_downsample` | Nearest-neighbor downsampling. |
| `non_uniform` | Nearest-neighbor resize with different spatial scales. |
| `width_only` | Nearest-neighbor resize of only the width dimension. |
| `with_sizes` | Resize using explicit output sizes instead of scales. |
### Sigmoid (3)
| Case | Description |
|---|---|
| `4d` | Standalone Sigmoid on an NCHW tensor. |
| `after_gemm` | Gemm followed by Sigmoid. |
| `basic` | Standalone Sigmoid on a 2D tensor. |
### Slice (8)
| Case | Description |
|---|---|
| `2d_basic` | Slices a 2D tensor with explicit axes and unit steps. |
| `after_conv` | Conv followed by a spatial crop. |
| `default_axes` | Omits axes and steps to use positional defaults. |
| `large_channel_1024` | Slices a channel range from a 1024-channel tensor. |
| `nchw_spatial_crop` | Crops the spatial axes of an NCHW tensor. |
| `negative_axis` | Slices using a negative axis. |
| `negative_indices` | Slices using negative indices. |
| `step2` | Slices using a positive step greater than one. |
### Softmax (5)
| Case | Description |
|---|---|
| `3d_last_axis` | Softmax over the last axis of a 3D tensor. |
| `basic` | Softmax over the last dimension of a 2D tensor. |
| `channel_axis` | Softmax over the channel axis of an NCHW tensor. |
| `large_dimension_1024` | Softmax over a last dimension of length 1024. |
| `negative_axis` | Softmax using a negative axis. |
### Split (4)
| Case | Description |
|---|---|
| `basic` | Splits a 2D tensor into two explicit output sizes. |
| `equal_three_way` | Splits a 2D tensor evenly into three outputs. |
| `negative_axis` | Splits using a negative axis. |
| `uneven_channel_axis_4d` | Splits an NCHW channel axis into uneven outputs. |
### Sub (6)
| Case | Description |
|---|---|
| `after_gemm` | Gemm followed by Sub with a broadcast constant vector. |
| `basic` | Elementwise Sub on two runtime inputs with identical shapes. |
| `broadcast_row` | Sub with a broadcast row-vector right-hand constant. |
| `channel_broadcast_1024` | Sub with NCHW per-channel broadcasting over 1024 channels. |
| `constant_lhs_broadcast` | Sub with a broadcast constant left-hand operand. |
| `leading_dimension_broadcast` | Sub with trailing-dimension broadcasting. |
+44 -16
View File
@@ -80,7 +80,7 @@ def conv_1x1():
kernel_shape=[1, 1], strides=[1, 1], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "conv_1x1", [X], [Y], initializer=[W])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "conv/pointwise_1x1", "conv_1x1.onnx")
save_model(model, "conv/pointwise_1x1", "conv_pointwise_1x1.onnx")
def conv_same_padding_3x3():
@@ -218,6 +218,33 @@ def conv_huge_pointwise_1024_dynamic():
save_model(model, "conv/huge_pointwise_1024_dynamic", "conv_huge_pointwise_1024_dynamic.onnx")
def conv_pointwise_tiled_chain():
"""Chained pointwise Convs with a tiled intermediate."""
X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 1024, 1, 1])
Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 256, 1, 1])
rng = np.random.default_rng(79)
W1 = numpy_helper.from_array(
rng.uniform(-1, 1, (1024, 1024, 1, 1)).astype(np.float32), name="W1")
B1 = numpy_helper.from_array(
rng.uniform(-1, 1, 1024).astype(np.float32), name="B1")
W2 = numpy_helper.from_array(
rng.uniform(-1, 1, (256, 1024, 1, 1)).astype(np.float32), name="W2")
B2 = numpy_helper.from_array(
rng.uniform(-1, 1, 256).astype(np.float32), name="B2")
nodes = [
helper.make_node("Relu", ["X"], ["X_relu"]),
helper.make_node("Conv", ["X_relu", "W1", "B1"], ["hidden"],
kernel_shape=[1, 1], strides=[1, 1], pads=[0, 0, 0, 0]),
helper.make_node("Relu", ["hidden"], ["hidden_relu"]),
helper.make_node("Conv", ["hidden_relu", "W2", "B2"], ["Y"],
kernel_shape=[1, 1], strides=[1, 1], pads=[0, 0, 0, 0]),
]
graph = helper.make_graph(
nodes, "conv_pointwise_tiled_chain", [X], [Y], initializer=[W1, B1, W2, B2])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "conv/pointwise_tiled_chain", "conv_pointwise_tiled_chain.onnx")
def conv_large_output_channels_1x1():
"""1x1 Conv with modest inputs and very large output channel count."""
X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64, 1, 1])
@@ -790,7 +817,7 @@ def maxpool_basic():
node = helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[2, 2], strides=[1, 1], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "maxpool_basic", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_basic", "maxpool_basic.onnx")
save_model(model, "pool/max_basic", "pool_max_basic.onnx")
def maxpool_stride2_multichannel():
@@ -800,7 +827,7 @@ def maxpool_stride2_multichannel():
node = helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[2, 2], strides=[2, 2], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "maxpool_stride2_multichannel", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_stride2_multichannel", "maxpool_stride2_multichannel.onnx")
save_model(model, "pool/max_stride2_multichannel", "pool_max_stride2_multichannel.onnx")
def maxpool_same_upper():
@@ -810,7 +837,7 @@ def maxpool_same_upper():
node = helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[3, 3], strides=[2, 2], auto_pad="SAME_UPPER")
graph = helper.make_graph([node], "maxpool_same_upper", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_same_upper", "maxpool_same_upper.onnx")
save_model(model, "pool/max_same_upper", "pool_max_same_upper.onnx")
def avgpool_basic():
@@ -820,7 +847,7 @@ def avgpool_basic():
node = helper.make_node("AveragePool", ["X"], ["Y"], kernel_shape=[2, 2], strides=[1, 1], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "avgpool_basic", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_basic", "avgpool_basic.onnx")
save_model(model, "pool/avg_basic", "pool_avg_basic.onnx")
def avgpool_explicit_padding():
@@ -831,7 +858,7 @@ def avgpool_explicit_padding():
kernel_shape=[3, 3], strides=[2, 2], pads=[1, 1, 1, 1], count_include_pad=0)
graph = helper.make_graph([node], "avgpool_explicit_padding", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_explicit_padding", "avgpool_explicit_padding.onnx")
save_model(model, "pool/avg_explicit_padding", "pool_avg_explicit_padding.onnx")
def avgpool_include_pad():
@@ -842,7 +869,7 @@ def avgpool_include_pad():
kernel_shape=[3, 3], strides=[2, 2], pads=[1, 1, 1, 1], count_include_pad=1)
graph = helper.make_graph([node], "avgpool_include_pad", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_include_pad", "avgpool_include_pad.onnx")
save_model(model, "pool/avg_include_pad", "pool_avg_include_pad.onnx")
def maxpool_after_conv():
@@ -855,7 +882,7 @@ def maxpool_after_conv():
pool = helper.make_node("MaxPool", ["C"], ["Y"], kernel_shape=[2, 2], strides=[2, 2], pads=[0, 0, 0, 0])
graph = helper.make_graph([conv, pool], "maxpool_after_conv", [X], [Y], initializer=[W])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_after_conv", "maxpool_after_conv.onnx")
save_model(model, "pool/max_after_conv", "pool_max_after_conv.onnx")
def maxpool_ceil_mode():
@@ -866,7 +893,7 @@ def maxpool_ceil_mode():
kernel_shape=[2, 2], strides=[2, 2], pads=[0, 0, 0, 0], ceil_mode=1)
graph = helper.make_graph([node], "maxpool_ceil_mode", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_ceil_mode", "maxpool_ceil_mode.onnx")
save_model(model, "pool/max_ceil_mode", "pool_max_ceil_mode.onnx")
def avgpool_ceil_mode():
@@ -877,7 +904,7 @@ def avgpool_ceil_mode():
kernel_shape=[2, 2], strides=[2, 2], pads=[0, 0, 0, 0], ceil_mode=1)
graph = helper.make_graph([node], "avgpool_ceil_mode", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_ceil_mode", "avgpool_ceil_mode.onnx")
save_model(model, "pool/avg_ceil_mode", "pool_avg_ceil_mode.onnx")
def maxpool_real_asymmetric_padding():
@@ -888,7 +915,7 @@ def maxpool_real_asymmetric_padding():
kernel_shape=[3, 3], strides=[1, 2], pads=[0, 1, 2, 1])
graph = helper.make_graph([node], "maxpool_real_asymmetric_padding", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_real_asymmetric_padding", "maxpool_real_asymmetric_padding.onnx")
save_model(model, "pool/max_real_asymmetric_padding", "pool_max_real_asymmetric_padding.onnx")
def avgpool_real_asymmetric_padding():
@@ -899,7 +926,7 @@ def avgpool_real_asymmetric_padding():
kernel_shape=[3, 3], strides=[1, 2], pads=[0, 1, 2, 1], count_include_pad=0)
graph = helper.make_graph([node], "avgpool_real_asymmetric_padding", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_real_asymmetric_padding", "avgpool_real_asymmetric_padding.onnx")
save_model(model, "pool/avg_real_asymmetric_padding", "pool_avg_real_asymmetric_padding.onnx")
def maxpool_non_square_kernel():
@@ -910,7 +937,7 @@ def maxpool_non_square_kernel():
kernel_shape=[2, 3], strides=[1, 2], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "maxpool_non_square_kernel", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_non_square_kernel", "maxpool_non_square_kernel.onnx")
save_model(model, "pool/max_non_square_kernel", "pool_max_non_square_kernel.onnx")
def avgpool_non_uniform_stride():
@@ -921,7 +948,7 @@ def avgpool_non_uniform_stride():
kernel_shape=[2, 3], strides=[1, 2], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "avgpool_non_uniform_stride", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_non_uniform_stride", "avgpool_non_uniform_stride.onnx")
save_model(model, "pool/avg_non_uniform_stride", "pool_avg_non_uniform_stride.onnx")
def maxpool_global_style_kernel_equals_input():
@@ -931,7 +958,7 @@ def maxpool_global_style_kernel_equals_input():
node = helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[4, 4], strides=[1, 1], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "maxpool_global_style_kernel_equals_input", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/max_global_style_kernel_equals_input", "maxpool_global_style_kernel_equals_input.onnx")
save_model(model, "pool/max_global_style_kernel_equals_input", "pool_max_global_style_kernel_equals_input.onnx")
def avgpool_large_channels():
@@ -941,7 +968,7 @@ def avgpool_large_channels():
node = helper.make_node("AveragePool", ["X"], ["Y"], kernel_shape=[2, 2], strides=[1, 1], pads=[0, 0, 0, 0])
graph = helper.make_graph([node], "avgpool_large_channels", [X], [Y])
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)])
save_model(model, "pool/avg_large_channels", "avgpool_large_channels.onnx")
save_model(model, "pool/avg_large_channels", "pool_avg_large_channels.onnx")
# ---------------------------------------------------------------------------
@@ -2005,6 +2032,7 @@ if __name__ == "__main__":
conv_dynamic()
conv_huge_pointwise_1024()
conv_huge_pointwise_1024_dynamic()
conv_pointwise_tiled_chain()
conv_large_output_channels_1x1()
conv_large_input_channels_1x1()
conv_depthwise_1024_channels()
+12 -16
View File
@@ -174,25 +174,21 @@ def main():
# Summary
n_passed = sum(1 for passed in results.values() if passed)
n_total = len(results)
status_width = len("Result")
path_width = max(len("Operation"), *(len(rel) for rel in results))
separator = f"+-{'-' * path_width}-+-{'-' * status_width}-+"
print(separator)
print(f"| {'Operation'.ljust(path_width)} | {'Result'.ljust(status_width)} |")
print(separator)
for rel, passed in results.items():
plain_status = "PASS" if passed else "FAIL"
status = Fore.GREEN + plain_status.ljust(status_width) + Style.RESET_ALL if passed else \
Fore.RED + plain_status.ljust(status_width) + Style.RESET_ALL
print(f"| {rel.ljust(path_width)} | {status} |")
print(separator)
print("\n" + Style.BRIGHT + Fore.CYAN + "Summary" + Style.RESET_ALL)
print(Style.BRIGHT + f"Passed: {n_passed}" + Style.RESET_ALL)
print(Style.BRIGHT + f"Failed: {n_total - n_passed}" + Style.RESET_ALL)
failing = [rel for rel, passed in results.items() if not passed]
if a.verbose or failing:
status_width = len("Result")
path_width = max(len("Operation"), *(len(rel) for rel in results))
separator = f"+-{'-' * path_width}-+-{'-' * status_width}-+"
print(separator)
print(f"| {'Operation'.ljust(path_width)} | {'Result'.ljust(status_width)} |")
print(separator)
for rel, passed in results.items():
if not a.verbose and passed:
continue
plain_status = "PASS" if passed else "FAIL"
status = Fore.GREEN + plain_status.ljust(status_width) + Style.RESET_ALL if passed else \
Fore.RED + plain_status.ljust(status_width) + Style.RESET_ALL
print(f"| {rel.ljust(path_width)} | {status} |")
print(separator)
if a.verbose:
print_average_pim_pass_timings(
pass_timing_sums,