diff --git a/.agents/invariants/PERFORMANCE_OPTIMIZATION_INVARIANT.md b/.agents/invariants/PERFORMANCE_OPTIMIZATION_INVARIANT.md index 2b53e5b..9443c84 100644 --- a/.agents/invariants/PERFORMANCE_OPTIMIZATION_INVARIANT.md +++ b/.agents/invariants/PERFORMANCE_OPTIMIZATION_INVARIANT.md @@ -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: diff --git a/src/PIM/Common/Support/DebugDump.cpp b/src/PIM/Common/Support/DebugDump.cpp index 05b5f05..632a3a9 100644 --- a/src/PIM/Common/Support/DebugDump.cpp +++ b/src/PIM/Common/Support/DebugDump.cpp @@ -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); diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp index 62823a2..9f430b0 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp @@ -9,6 +9,53 @@ using namespace mlir; namespace onnx_mlir { +FailureOr createFragmentAssemblyBlueprint(Value physicalBatch, + RankedTensorType logicalType, + ArrayRef entries, + StringRef physicalLayout, + StringRef indexMap, + PatternRewriter& rewriter, + Location loc) { + auto physicalType = dyn_cast(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 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(rank) + || entry.sizes.size() != static_cast(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 tensors, PatternRewriter& rewriter) { if (tensors.size() == 1) return tensors[0]; diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp index abe0093..398a520 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp @@ -19,6 +19,13 @@ namespace onnx_mlir { +struct FragmentAssemblyEntry { + int64_t sourceSlot; + int64_t sourceOffset; + llvm::SmallVector destinationOffsets; + llvm::SmallVector 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 tensors, mlir::PatternRewriter& rewriter); +mlir::FailureOr createFragmentAssemblyBlueprint(mlir::Value physicalBatch, + mlir::RankedTensorType logicalType, + llvm::ArrayRef entries, + llvm::StringRef physicalLayout, + llvm::StringRef indexMap, + mlir::PatternRewriter& rewriter, + mlir::Location loc); + } // namespace onnx_mlir diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp index 90d04a4..396e925 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp @@ -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 describeRowStripPhysicalValue(Value storage, RankedTensorType logicalType) { + auto storageType = dyn_cast(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 createRowStripStorageFromRows(Value rows, return batchOp->getResult(0); } -FailureOr -createRowStripAssemblyBlueprint(Value storage, RankedTensorType logicalType, PatternRewriter& rewriter, Location loc) { - auto storageType = dyn_cast(storage.getType()); - if (!storageType || storageType != getRowStripStorageType(logicalType)) - return failure(); - - auto [offsets, sizes] = buildRowStripMetadata(logicalType); - int64_t height = logicalType.getDimSize(2); - SmallVector 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 createRowStripAssemblyBlueprint(const RowStripPhysicalValue& value, + PatternRewriter& rewriter, + Location loc) { + SmallVector 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 -applyRowStripRelu(Value storage, RankedTensorType logicalType, PatternRewriter& rewriter, Location loc) { - auto fragmentType = getRowStripFragmentType(logicalType); - auto storageType = getRowStripStorageType(logicalType); +FailureOr applyRowStripRelu(const RowStripPhysicalValue& value, PatternRewriter& rewriter, Location loc) { + auto storageType = cast(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 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 -applyRowStripBiasAdd(Value storage, RankedTensorType logicalType, Value bias, PatternRewriter& rewriter, Location loc) { +FailureOr 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> channelValues = getBiasChannelValues(denseAttr, value.logicalType); + if (failed(channelValues)) return failure(); + auto storageType = cast(value.storage.getType()); + auto biasStorageType = spatial::getGraphBatchPhysicalResultType(value.tilesPerRow, value.fragmentType); + SmallVector biasValues( + biasStorageType.getNumElements(), cast(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()), - fragmentType); - } - else { - FailureOr 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 fragment = extractGraphBatchPhysicalFragment( + rewriter, loc, args.inputs[0], args.lane, value.fragmentType); + Value tile = affineModConst(rewriter, loc, args.lane, + value.tilesPerRow, anchorOp); + FailureOr 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)) diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp index abb850a..2e74af6 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp @@ -11,10 +11,13 @@ inline constexpr llvm::StringLiteral kRowStripIndexMap = "nchw_row_strip_fragmen struct RowStripPhysicalValue { mlir::Value storage; mlir::RankedTensorType logicalType; - llvm::SmallVector fragmentOffsets; - llvm::SmallVector fragmentSizes; + mlir::RankedTensorType fragmentType; + int64_t tilesPerRow; }; +mlir::FailureOr describeRowStripPhysicalValue(mlir::Value storage, + mlir::RankedTensorType logicalType); + std::pair, llvm::SmallVector> buildRowStripMetadata(mlir::RankedTensorType type); @@ -50,18 +53,15 @@ mlir::FailureOr createRowStripStorageFromRows(mlir::Value rows, mlir::PatternRewriter& rewriter, mlir::Location loc); -mlir::FailureOr createRowStripAssemblyBlueprint(mlir::Value storage, - mlir::RankedTensorType logicalType, +mlir::FailureOr createRowStripAssemblyBlueprint(const RowStripPhysicalValue& value, mlir::PatternRewriter& rewriter, mlir::Location loc); -mlir::FailureOr applyRowStripRelu(mlir::Value storage, - mlir::RankedTensorType logicalType, +mlir::FailureOr applyRowStripRelu(const RowStripPhysicalValue& value, mlir::PatternRewriter& rewriter, mlir::Location loc); -mlir::FailureOr applyRowStripBiasAdd(mlir::Value storage, - mlir::RankedTensorType logicalType, +mlir::FailureOr applyRowStripBiasAdd(const RowStripPhysicalValue& value, mlir::Value bias, mlir::PatternRewriter& rewriter, mlir::Location loc); diff --git a/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp b/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp index ae85bca..539cb9e 100644 --- a/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp @@ -45,38 +45,30 @@ static FailureOr buildRowStripValue(spatial::SpatBlueprin auto logicalType = dyn_cast(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(storage.getType()); - if (!storageType || storageType != getRowStripStorageType(logicalType)) + FailureOr value = describeRowStripPhysicalValue(storage, logicalType); + if (failed(value)) return blueprint.emitOpError("requires physical row-strip fragment storage"), failure(); - return value; + return *value; } static FailureOr 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 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 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 lowerDenseBatchBiasAdd(Value input, Value bias, RankedTensorType resultType, @@ -168,9 +160,12 @@ struct LowerSpatialPlansPass final : PassWrapper physicalInput; + if (succeeded(rowStripInput)) + physicalInput = rowStripInput->storage; FailureOr lowered = lowerSelectedConv2DPlan( planOp, - succeeded(rowStripInput) ? std::optional {rowStripInput->storage} : std::nullopt, + physicalInput, /*emitRowStripLayout=*/true, rewriter); if (failed(lowered)) { @@ -255,8 +250,23 @@ struct LowerSpatialPlansPass final : PassWrapper input = getRowStripValue(rowStripValues, planOp.getInput()); rewriter.setInsertionPoint(planOp); + std::optional physicalInput; + if (succeeded(input)) { + if (input->tilesPerRow == 1) { + physicalInput = input->storage; + } + else { + FailureOr 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 lowered = lowerSelectedMaxPool2DPlan( - planOp, succeeded(input) ? std::optional {input->storage} : std::nullopt, rewriter); + planOp, physicalInput, rewriter); if (failed(lowered)) { planOp.emitOpError("failed to lower selected row-strip Spatial MaxPool plan"); signalPassFailure(); diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp index cbc1f2a..5dc5dd2 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp @@ -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 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(chunkPositions)) { - const int64_t chunkNumPatches = std::min(static_cast(chunkPositions), totalPatches - chunkStart); - ConvGemmPlan chunkPlan = buildConvGemmPlan(state, - static_cast(wDenseAttr), - !state.hasBias || static_cast(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(wDenseAttr), + !state.hasBias || static_cast(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(wDenseAttr), !state.hasBias || static_cast(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(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 createPaddedBiasRowConstant(const ConvLoweringState& sta return getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), biasAttr, biasType); } +static FailureOr createPaddedBiasTileConstant(const ConvLoweringState& state, + int64_t tileChannels, + PatternRewriter& rewriter) { + DenseElementsAttr denseAttr; + if (!isSupportedBiasAddValue(state.b, state.outType, &denseAttr)) + return failure(); + FailureOr> 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 values( + tileType.getNumElements(), cast(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 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 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(crossbarCountInCore.getValue()); -} - static bool rowStripOutputTileFitsOneCore(const ConvGeometry& geometry) { return ceilIntegerDivide(geometry.k, geometry.xbarSize) <= static_cast(crossbarCountInCore.getValue()); @@ -2928,38 +2926,40 @@ static FailureOr 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 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 weightOffsets { - rewriter.getIndexAttr(outputTile), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; - SmallVector 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 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 weightOffsets { + outputTile, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; + SmallVector 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 biasTile = failure(); + if (state.hasBias) + biasTile = extractGraphBatchPhysicalFragment(rewriter, loc, args.inputs[1], outputTile, paddedRowType); + if (state.hasBias && failed(biasTile)) + return failure(); FailureOr 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 createOutputChannelTiledRowStripConvOutput(const ConvLow paddedPatchRow = createZeroPaddedTensor( paddedPatchRow, paddedPatchRowType, {0, 0}, {0, paddedK - patchSize}, rewriter, widthLoc); FailureOr 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 rowOffsets {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; - SmallVector 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 {{0}, {1, 2, 3}}); + rewriter, widthLoc, tilePixelType, *paddedOutputRow, SmallVector {{0}, {1, 2, 3}}); SmallVector rowOffsets { rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), widthIndex}; SmallVector 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 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 tileFragment = extractGraphBatchPhysicalFragment( - rewriter, loc, args.inputs[outputTile], args.lane, tileFragmentType); - if (failed(tileFragment)) - return failure(); - SmallVector offsets {rewriter.getIndexAttr(0), - rewriter.getIndexAttr(channelOffset), - rewriter.getIndexAttr(0), - rewriter.getIndexAttr(0)}; - SmallVector 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 @@ -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 createConvOutputFromNchwRowStripFragments(Value rowStrip return batchOp->getResult(0); } +static FailureOr createPointwiseOutputFromRowStripFragments(Value rowStripStorage, + const ConvLoweringState& state, + PatternRewriter& rewriter, + Location loc) { + FailureOr 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 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 weightOffsets {args.lane, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; + SmallVector 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 channelOffset = affineMulConst(rewriter, pieceLoc, kSlice, xbarDim, anchorOp); + Value sourceSlot = affineFloorDivConst( + rewriter, pieceLoc, channelOffset, inputFragmentChannels, anchorOp); + Value sourceOffset = affineModConst( + rewriter, pieceLoc, channelOffset, inputFragmentChannels, anchorOp); + FailureOr 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 {{0}, {1, 2, 3}}); + Value inputSlice = tensor::ExtractSliceOp::create(rewriter, pieceLoc, paddedRowType, inputRow, + SmallVector {rewriter.getIndexAttr(0), sourceOffset}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(xbarDim)}, + getUnitStrides(rewriter, 2)); + Value weightSlice = tensor::ExtractSliceOp::create(rewriter, pieceLoc, weightSliceType, weightTile, + SmallVector {channelOffset, rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(xbarDim), rewriter.getIndexAttr(xbarDim)}, + getUnitStrides(rewriter, 2)); + return spatial::SpatVMMOp::create(rewriter, pieceLoc, paddedRowType, weightSlice, inputSlice).getResult(); + }; + FailureOr 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& yielded) { + FailureOr 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 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 {{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 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 rowStripStorage = createRowStripConvOutputFromDenseInput(rowState, rewriter, planOp.getLoc()); + FailureOr 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; } diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp index c888732..e82dfb5 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp @@ -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 unused; + funcOp.walk([&](SpatDeferredCommunicationOp deferred) { + if (!deferred.getOutput().use_empty() || !deferred.getBody().hasOneBlock()) + return; + Block &body = deferred.getBody().front(); + auto yield = dyn_cast(body.getTerminator()); + auto argument = yield && yield.getOutputs().size() == 1 + ? dyn_cast(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(); diff --git a/src/PIM/Dialect/Spatial/Transforms/TrivialGraphComputeMergePass.cpp b/src/PIM/Dialect/Spatial/Transforms/TrivialGraphComputeMergePass.cpp index efcb9a4..9bd691e 100644 --- a/src/PIM/Dialect/Spatial/Transforms/TrivialGraphComputeMergePass.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/TrivialGraphComputeMergePass.cpp @@ -42,6 +42,17 @@ static bool hasCapacityFor(Operation* producer, Operation* consumer) { return getCrossbarUnionSize(producerWeights, consumerWeights) <= static_cast(crossbarCountInCore.getValue()); } +template +static bool isUniqueGraphComputePredecessor(Operation *candidate, ConsumerOp consumer) { + llvm::SmallSetVector 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 { for (Value input : consumer.getInputs()) { auto candidate = input.getDefiningOp(); 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 { ? SpatGraphComputeBatch() : consumer.getInputs().front().getDefiningOp(); 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 { auto candidate = input.getDefiningOp(); 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 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 diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer/ingest.py b/tools/raptor_graph_explorer/raptor_graph_explorer/ingest.py index 9c6ced9..7417f03 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer/ingest.py +++ b/tools/raptor_graph_explorer/raptor_graph_explorer/ingest.py @@ -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: diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer/motifs.py b/tools/raptor_graph_explorer/raptor_graph_explorer/motifs.py index a83ba9d..bcf52e4 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer/motifs.py +++ b/tools/raptor_graph_explorer/raptor_graph_explorer/motifs.py @@ -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) diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer/projection.py b/tools/raptor_graph_explorer/raptor_graph_explorer/projection.py index 8ea4288..5aade7c 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer/projection.py +++ b/tools/raptor_graph_explorer/raptor_graph_explorer/projection.py @@ -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 = [] diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer/static/app.js b/tools/raptor_graph_explorer/raptor_graph_explorer/static/app.js index 7616753..0bddd74 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer/static/app.js +++ b/tools/raptor_graph_explorer/raptor_graph_explorer/static/app.js @@ -16,18 +16,16 @@ function definition(object, keys) { return `
${keys.filter(key => object[key] !== undefined).map(key => `
${html(key.replaceAll("_", " "))}
${html(typeof object[key] === "object" ? JSON.stringify(object[key]) : object[key])}
`).join("")}
`; } 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 => ``).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) => ``).join("") : "No motifs detected."; + $("motifs").innerHTML = state.motifs.length ? state.motifs.map((motif, index) => ``).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 = '

Structural SCC-condensed split/rejoin; rendered geometry is not evidence.

' + 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 = '' + Object.keys(summary.mapping_classes).map(kind => ``).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)); diff --git a/tools/raptor_graph_explorer/raptor_graph_explorer/static/index.html b/tools/raptor_graph_explorer/raptor_graph_explorer/static/index.html index ccd1409..76974aa 100644 --- a/tools/raptor_graph_explorer/raptor_graph_explorer/static/index.html +++ b/tools/raptor_graph_explorer/raptor_graph_explorer/static/index.html @@ -15,17 +15,16 @@
- + diff --git a/tools/raptor_graph_explorer/tests/test_api.py b/tools/raptor_graph_explorer/tests/test_api.py index 8f07441..d723b34 100644 --- a/tools/raptor_graph_explorer/tests/test_api.py +++ b/tools/raptor_graph_explorer/tests/test_api.py @@ -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 diff --git a/tools/raptor_graph_explorer/tests/test_cli.py b/tools/raptor_graph_explorer/tests/test_cli.py index 13b399c..2ed4808 100644 --- a/tools/raptor_graph_explorer/tests/test_cli.py +++ b/tools/raptor_graph_explorer/tests/test_cli.py @@ -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() diff --git a/tools/raptor_graph_explorer/tests/test_ingest.py b/tools/raptor_graph_explorer/tests/test_ingest.py index c00a30d..09647f6 100644 --- a/tools/raptor_graph_explorer/tests/test_ingest.py +++ b/tools/raptor_graph_explorer/tests/test_ingest.py @@ -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() diff --git a/tools/raptor_graph_explorer/tests/test_motifs.py b/tools/raptor_graph_explorer/tests/test_motifs.py index 11a745b..6d03fd9 100644 --- a/tools/raptor_graph_explorer/tests/test_motifs.py +++ b/tools/raptor_graph_explorer/tests/test_motifs.py @@ -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"] diff --git a/tools/raptor_graph_explorer/tests/test_projection.py b/tools/raptor_graph_explorer/tests/test_projection.py index 16df5d4..51e6523 100644 --- a/tools/raptor_graph_explorer/tests/test_projection.py +++ b/tools/raptor_graph_explorer/tests/test_projection.py @@ -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 '' 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 diff --git a/validation/operations/README.md b/validation/operations/README.md index e724410..3067dca 100644 --- a/validation/operations/README.md +++ b/validation/operations/README.md @@ -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 +//_.onnx ``` -## Conv +The `/` 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. | diff --git a/validation/operations/conv/pointwise_1x1/conv_1x1.onnx b/validation/operations/conv/pointwise_1x1/conv_pointwise_1x1.onnx similarity index 100% rename from validation/operations/conv/pointwise_1x1/conv_1x1.onnx rename to validation/operations/conv/pointwise_1x1/conv_pointwise_1x1.onnx diff --git a/validation/operations/conv/pointwise_tiled_chain/conv_pointwise_tiled_chain.onnx b/validation/operations/conv/pointwise_tiled_chain/conv_pointwise_tiled_chain.onnx new file mode 100644 index 0000000..15c0471 Binary files /dev/null and b/validation/operations/conv/pointwise_tiled_chain/conv_pointwise_tiled_chain.onnx differ diff --git a/validation/operations/conv/simple/conv.onnx b/validation/operations/conv/simple/conv_simple.onnx similarity index 100% rename from validation/operations/conv/simple/conv.onnx rename to validation/operations/conv/simple/conv_simple.onnx diff --git a/validation/operations/gen_tests.py b/validation/operations/gen_tests.py index 4700f5f..d77a417 100644 --- a/validation/operations/gen_tests.py +++ b/validation/operations/gen_tests.py @@ -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() diff --git a/validation/operations/pool/avg_basic/avgpool_basic.onnx b/validation/operations/pool/avg_basic/pool_avg_basic.onnx similarity index 100% rename from validation/operations/pool/avg_basic/avgpool_basic.onnx rename to validation/operations/pool/avg_basic/pool_avg_basic.onnx diff --git a/validation/operations/pool/avg_ceil_mode/avgpool_ceil_mode.onnx b/validation/operations/pool/avg_ceil_mode/pool_avg_ceil_mode.onnx similarity index 100% rename from validation/operations/pool/avg_ceil_mode/avgpool_ceil_mode.onnx rename to validation/operations/pool/avg_ceil_mode/pool_avg_ceil_mode.onnx diff --git a/validation/operations/pool/avg_explicit_padding/avgpool_explicit_padding.onnx b/validation/operations/pool/avg_explicit_padding/pool_avg_explicit_padding.onnx similarity index 100% rename from validation/operations/pool/avg_explicit_padding/avgpool_explicit_padding.onnx rename to validation/operations/pool/avg_explicit_padding/pool_avg_explicit_padding.onnx diff --git a/validation/operations/pool/avg_include_pad/avgpool_include_pad.onnx b/validation/operations/pool/avg_include_pad/pool_avg_include_pad.onnx similarity index 100% rename from validation/operations/pool/avg_include_pad/avgpool_include_pad.onnx rename to validation/operations/pool/avg_include_pad/pool_avg_include_pad.onnx diff --git a/validation/operations/pool/avg_large_channels/avgpool_large_channels.onnx b/validation/operations/pool/avg_large_channels/pool_avg_large_channels.onnx similarity index 100% rename from validation/operations/pool/avg_large_channels/avgpool_large_channels.onnx rename to validation/operations/pool/avg_large_channels/pool_avg_large_channels.onnx diff --git a/validation/operations/pool/avg_non_uniform_stride/avgpool_non_uniform_stride.onnx b/validation/operations/pool/avg_non_uniform_stride/pool_avg_non_uniform_stride.onnx similarity index 100% rename from validation/operations/pool/avg_non_uniform_stride/avgpool_non_uniform_stride.onnx rename to validation/operations/pool/avg_non_uniform_stride/pool_avg_non_uniform_stride.onnx diff --git a/validation/operations/pool/avg_real_asymmetric_padding/avgpool_real_asymmetric_padding.onnx b/validation/operations/pool/avg_real_asymmetric_padding/pool_avg_real_asymmetric_padding.onnx similarity index 100% rename from validation/operations/pool/avg_real_asymmetric_padding/avgpool_real_asymmetric_padding.onnx rename to validation/operations/pool/avg_real_asymmetric_padding/pool_avg_real_asymmetric_padding.onnx diff --git a/validation/operations/pool/max_after_conv/maxpool_after_conv.onnx b/validation/operations/pool/max_after_conv/pool_max_after_conv.onnx similarity index 100% rename from validation/operations/pool/max_after_conv/maxpool_after_conv.onnx rename to validation/operations/pool/max_after_conv/pool_max_after_conv.onnx diff --git a/validation/operations/pool/max_basic/maxpool_basic.onnx b/validation/operations/pool/max_basic/pool_max_basic.onnx similarity index 100% rename from validation/operations/pool/max_basic/maxpool_basic.onnx rename to validation/operations/pool/max_basic/pool_max_basic.onnx diff --git a/validation/operations/pool/max_ceil_mode/maxpool_ceil_mode.onnx b/validation/operations/pool/max_ceil_mode/pool_max_ceil_mode.onnx similarity index 100% rename from validation/operations/pool/max_ceil_mode/maxpool_ceil_mode.onnx rename to validation/operations/pool/max_ceil_mode/pool_max_ceil_mode.onnx diff --git a/validation/operations/pool/max_global_style_kernel_equals_input/maxpool_global_style_kernel_equals_input.onnx b/validation/operations/pool/max_global_style_kernel_equals_input/pool_max_global_style_kernel_equals_input.onnx similarity index 100% rename from validation/operations/pool/max_global_style_kernel_equals_input/maxpool_global_style_kernel_equals_input.onnx rename to validation/operations/pool/max_global_style_kernel_equals_input/pool_max_global_style_kernel_equals_input.onnx diff --git a/validation/operations/pool/max_non_square_kernel/maxpool_non_square_kernel.onnx b/validation/operations/pool/max_non_square_kernel/pool_max_non_square_kernel.onnx similarity index 100% rename from validation/operations/pool/max_non_square_kernel/maxpool_non_square_kernel.onnx rename to validation/operations/pool/max_non_square_kernel/pool_max_non_square_kernel.onnx diff --git a/validation/operations/pool/max_real_asymmetric_padding/maxpool_real_asymmetric_padding.onnx b/validation/operations/pool/max_real_asymmetric_padding/pool_max_real_asymmetric_padding.onnx similarity index 100% rename from validation/operations/pool/max_real_asymmetric_padding/maxpool_real_asymmetric_padding.onnx rename to validation/operations/pool/max_real_asymmetric_padding/pool_max_real_asymmetric_padding.onnx diff --git a/validation/operations/pool/max_same_upper/maxpool_same_upper.onnx b/validation/operations/pool/max_same_upper/pool_max_same_upper.onnx similarity index 100% rename from validation/operations/pool/max_same_upper/maxpool_same_upper.onnx rename to validation/operations/pool/max_same_upper/pool_max_same_upper.onnx diff --git a/validation/operations/pool/max_stride2_multichannel/maxpool_stride2_multichannel.onnx b/validation/operations/pool/max_stride2_multichannel/pool_max_stride2_multichannel.onnx similarity index 100% rename from validation/operations/pool/max_stride2_multichannel/maxpool_stride2_multichannel.onnx rename to validation/operations/pool/max_stride2_multichannel/pool_max_stride2_multichannel.onnx diff --git a/validation/validate.py b/validation/validate.py index 29d5be3..65042b4 100755 --- a/validation/validate.py +++ b/validation/validate.py @@ -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,