From 893e90feacebe18da0cd2dfb6099c3993b7ac844 Mon Sep 17 00:00:00 2001 From: NiccoloN Date: Sun, 2 Aug 2026 11:37:22 +0200 Subject: [PATCH] temp commit: i will soft-reset and recommit after next changes --- README.md | 2 - src/PIM/Common/IR/AddressAnalysis.cpp | 76 +- src/PIM/Compiler/PimCompilerOptions.cpp | 5 - src/PIM/Compiler/PimCompilerOptions.hpp | 1 - .../Common/ComputeRegionBuilder.cpp | 8 +- .../Common/ComputeRegionBuilder.hpp | 33 + .../Common/RowStripLayoutUtils.cpp | 22 +- .../ONNXToSpatial/LowerSpatialPlansPass.cpp | 37 + .../ONNXToSpatial/ONNXToSpatialPass.cpp | 13 +- .../ONNXToSpatial/ONNXToSpatialVerifier.cpp | 1 + src/PIM/Conversion/ONNXToSpatial/Patterns.hpp | 1 + .../ONNXToSpatial/Patterns/Math/Conv.cpp | 1811 ++++------------- .../Patterns/Math/Elementwise.cpp | 51 + .../ONNXToSpatial/Patterns/Math/MatMul.cpp | 384 +++- .../ONNXToSpatial/Patterns/Tensor/Resize.cpp | 276 +-- .../Patterns/Tensor/Transpose.cpp | 76 + .../Conversion/ONNXToSpatial/PlanLowering.hpp | 8 + .../SpatialLayoutPlanningPass.cpp | 22 + src/PIM/Conversion/SpatialToPim/Common.cpp | 26 + src/PIM/Conversion/SpatialToPim/Common.hpp | 5 + .../SpatialToPim/CoreLoweringPatterns.cpp | 10 + src/PIM/Conversion/SpatialToPim/Patterns.cpp | 10 + .../HostConstantFolding/Patterns/Constant.cpp | 73 +- src/PIM/Dialect/Spatial/Spatial.td | 15 + src/PIM/Dialect/Spatial/SpatialOps.cpp | 64 + src/PIM/Dialect/Spatial/SpatialOps.hpp | 9 + src/PIM/Dialect/Spatial/SpatialOpsVerify.cpp | 58 +- .../DeferredCommunicationPlanning.cpp | 107 +- .../DeferredCommunicationRealization.cpp | 24 + .../DeferredProjectionAnalysis.cpp | 28 +- .../DeferredResultRealization.cpp | 4 +- .../DeferredTransferPlanning.cpp | 3 + .../Scheduling/PeftScheduler.cpp | 14 +- .../SpatialDataflowCsvExporter.cpp | 340 +++- validation/.gitignore | 3 + .../pimcomp_models/validation_results.csv | 6 + validation/operations/README.md | 9 +- .../conv_yolo11n_depthwise_head.onnx | Bin 0 -> 19547 bytes .../yolo11n_heavy/conv_yolo11n_heavy.onnx | Bin 0 -> 295828 bytes validation/operations/gen_tests.py | 63 + .../yolo_attention/matmul_yolo_attention.onnx | Bin 0 -> 301 bytes validation/operations/validation_results.csv | 327 +-- validation/tools/analyze_yolo11n_attention.py | 182 ++ 43 files changed, 2159 insertions(+), 2048 deletions(-) create mode 100644 validation/networks/pimcomp_models/validation_results.csv create mode 100644 validation/operations/conv/yolo11n_depthwise_head/conv_yolo11n_depthwise_head.onnx create mode 100644 validation/operations/conv/yolo11n_heavy/conv_yolo11n_heavy.onnx create mode 100644 validation/operations/matmul/yolo_attention/matmul_yolo_attention.onnx create mode 100644 validation/tools/analyze_yolo11n_attention.py diff --git a/README.md b/README.md index 5519851..f243e35 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,6 @@ options; `onnx-mlir --help` lists the inherited ONNX-MLIR options. elements per convolution before streaming. Default is `1048576`. - `--pim-conv-stream-chunk-positions=` - maximum output positions per streamed convolution chunk. Default is `1024`. -- `--pim-report-conv-lowering=` - emit the bounded convolution - lowering report. Default is `true`. - `--use-experimental-conv-impl` - use the alternate convolution lowering. - `--pim-detect-communication-deadlock` - statically simulate expanded send/receive ordering and reject blocking deadlocks. Default is off. diff --git a/src/PIM/Common/IR/AddressAnalysis.cpp b/src/PIM/Common/IR/AddressAnalysis.cpp index 1fb13bc..0e3eb92 100644 --- a/src/PIM/Common/IR/AddressAnalysis.cpp +++ b/src/PIM/Common/IR/AddressAnalysis.cpp @@ -1,10 +1,13 @@ #include "mlir/Dialect/Affine/IR/AffineOps.h" #include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/Bufferization/IR/Bufferization.h" #include "mlir/Dialect/MemRef/IR/MemRef.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/BuiltinAttributes.h" #include "mlir/Interfaces/DestinationStyleOpInterface.h" +#include "llvm/ADT/SmallPtrSet.h" + #include #include "src/Accelerators/PIM/Common/IR/AddressAnalysis.hpp" @@ -36,6 +39,10 @@ mlir::Value resolveAlias(mlir::Value value, const StaticValueKnowledge* knowledg llvm::FailureOr compileIndexValueImpl(mlir::Value value); llvm::FailureOr compileContiguousAddressExprImpl(mlir::Value value); +using AliasResolutionSet = llvm::SmallPtrSet; +mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, + const StaticValueKnowledge* knowledge, + AliasResolutionSet& visited); mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnowledge* knowledge); template @@ -45,18 +52,23 @@ CompiledIndexExpr makeCompiledIndexExpr(Args&&... args) { static mlir::Value resolveForYieldedAliasToInit(mlir::scf::ForOp forOp, mlir::Value yieldedValue, - const StaticValueKnowledge* knowledge) { - yieldedValue = resolveLoopCarriedAliasImpl(yieldedValue, knowledge); + const StaticValueKnowledge* knowledge, + AliasResolutionSet& visited) { + yieldedValue = resolveLoopCarriedAliasImpl(yieldedValue, knowledge, visited); if (auto blockArgument = mlir::dyn_cast(yieldedValue)) { if (blockArgument.getOwner() == forOp.getBody() && blockArgument.getArgNumber() > 0 && static_cast(blockArgument.getArgNumber() - 1) < forOp.getInitArgs().size()) - return resolveLoopCarriedAliasImpl(forOp.getInitArgs()[blockArgument.getArgNumber() - 1], knowledge); + return resolveLoopCarriedAliasImpl(forOp.getInitArgs()[blockArgument.getArgNumber() - 1], knowledge, visited); } return yieldedValue; } -mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnowledge* knowledge) { +mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, + const StaticValueKnowledge* knowledge, + AliasResolutionSet& visited) { value = resolveAlias(value, knowledge); + if (!value || !visited.insert(value).second) + return value; if (auto blockArgument = mlir::dyn_cast(value)) { auto forOp = mlir::dyn_cast_or_null(blockArgument.getOwner()->getParentOp()); @@ -64,9 +76,12 @@ mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnow const unsigned iterArgIndex = blockArgument.getArgNumber() - 1; auto yieldOp = mlir::dyn_cast(forOp.getBody()->getTerminator()); if (iterArgIndex < forOp.getInitArgs().size() && yieldOp - && iterArgIndex < yieldOp.getNumOperands() - && resolveAlias(yieldOp.getOperand(iterArgIndex), knowledge) == blockArgument) - return resolveLoopCarriedAliasImpl(forOp.getInitArgs()[iterArgIndex], knowledge); + && iterArgIndex < yieldOp.getNumOperands()) { + mlir::Value yieldedValue = resolveAlias(yieldOp.getOperand(iterArgIndex), knowledge); + if (yieldedValue == blockArgument + || (yieldedValue && resolveLoopCarriedAliasImpl(yieldedValue, knowledge, visited) == blockArgument)) + return resolveLoopCarriedAliasImpl(forOp.getInitArgs()[iterArgIndex], knowledge, visited); + } } return value; } @@ -75,10 +90,15 @@ mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnow if (!definingOp) return value; + if (auto toBufferOp = mlir::dyn_cast(definingOp)) + return resolveLoopCarriedAliasImpl(toBufferOp.getTensor(), knowledge, visited); + if (auto toTensorOp = mlir::dyn_cast(definingOp)) + return resolveLoopCarriedAliasImpl(toTensorOp.getBuffer(), knowledge, visited); + if (auto dpsDefiningOp = mlir::dyn_cast(definingOp)) { if (auto result = mlir::dyn_cast(value)) if (mlir::OpOperand* tiedOperand = dpsDefiningOp.getTiedOpOperand(result)) - return resolveLoopCarriedAliasImpl(tiedOperand->get(), knowledge); + return resolveLoopCarriedAliasImpl(tiedOperand->get(), knowledge, visited); } if (auto forOp = mlir::dyn_cast(definingOp)) { @@ -86,20 +106,26 @@ mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnow if (result) { auto yieldOp = mlir::dyn_cast(forOp.getBody()->getTerminator()); if (yieldOp && result.getResultNumber() < yieldOp.getNumOperands()) - return resolveForYieldedAliasToInit(forOp, yieldOp.getOperand(result.getResultNumber()), knowledge); + return resolveForYieldedAliasToInit( + forOp, yieldOp.getOperand(result.getResultNumber()), knowledge, visited); } } if (auto castOp = mlir::dyn_cast(definingOp)) - return resolveLoopCarriedAliasImpl(castOp.getSource(), knowledge); + return resolveLoopCarriedAliasImpl(castOp.getSource(), knowledge, visited); if (auto collapseOp = mlir::dyn_cast(definingOp)) - return resolveLoopCarriedAliasImpl(collapseOp.getSrc(), knowledge); + return resolveLoopCarriedAliasImpl(collapseOp.getSrc(), knowledge, visited); if (auto expandOp = mlir::dyn_cast(definingOp)) - return resolveLoopCarriedAliasImpl(expandOp.getSrc(), knowledge); + return resolveLoopCarriedAliasImpl(expandOp.getSrc(), knowledge, visited); return value; } +mlir::Value resolveLoopCarriedAliasImpl(mlir::Value value, const StaticValueKnowledge* knowledge) { + AliasResolutionSet visited; + return resolveLoopCarriedAliasImpl(value, knowledge, visited); +} + llvm::FailureOr resolveOpFoldResult(mlir::OpFoldResult ofr, const StaticValueKnowledge* knowledge); llvm::FailureOr resolveIndexValueImpl(mlir::Value value, const StaticValueKnowledge* knowledge); @@ -524,6 +550,15 @@ llvm::FailureOr resolveContiguousAddressImpl(mlir::Va if (!definingOp) return mlir::failure(); + if (auto toBufferOp = mlir::dyn_cast(definingOp)) { + value = resolveAlias(toBufferOp.getTensor(), knowledge); + continue; + } + if (auto toTensorOp = mlir::dyn_cast(definingOp)) { + value = resolveAlias(toTensorOp.getBuffer(), knowledge); + continue; + } + if (auto dpsDefiningOp = mlir::dyn_cast(definingOp)) { mlir::OpOperand* tiedOperand = dpsDefiningOp.getTiedOpOperand(mlir::dyn_cast(value)); if (!tiedOperand) @@ -538,7 +573,9 @@ llvm::FailureOr resolveContiguousAddressImpl(mlir::Va return mlir::failure(); auto yieldOp = mlir::cast(forOp.getBody()->getTerminator()); - value = resolveForYieldedAliasToInit(forOp, yieldOp.getOperand(result.getResultNumber()), knowledge); + AliasResolutionSet visited; + value = resolveForYieldedAliasToInit( + forOp, yieldOp.getOperand(result.getResultNumber()), knowledge, visited); continue; } @@ -643,6 +680,15 @@ llvm::FailureOr compileContiguousAddressExprImpl(mlir::Valu if (!definingOp) return mlir::failure(); + if (auto toBufferOp = mlir::dyn_cast(definingOp)) { + value = toBufferOp.getTensor(); + continue; + } + if (auto toTensorOp = mlir::dyn_cast(definingOp)) { + value = toTensorOp.getBuffer(); + continue; + } + if (auto dpsDefiningOp = mlir::dyn_cast(definingOp)) { mlir::OpOperand* tiedOperand = dpsDefiningOp.getTiedOpOperand(mlir::dyn_cast(value)); if (!tiedOperand) @@ -657,7 +703,9 @@ llvm::FailureOr compileContiguousAddressExprImpl(mlir::Valu return mlir::failure(); auto yieldOp = mlir::cast(forOp.getBody()->getTerminator()); - value = resolveForYieldedAliasToInit(forOp, yieldOp.getOperand(result.getResultNumber()), nullptr); + AliasResolutionSet visited; + value = resolveForYieldedAliasToInit( + forOp, yieldOp.getOperand(result.getResultNumber()), nullptr, visited); continue; } diff --git a/src/PIM/Compiler/PimCompilerOptions.cpp b/src/PIM/Compiler/PimCompilerOptions.cpp index dfa5e6c..cd22550 100644 --- a/src/PIM/Compiler/PimCompilerOptions.cpp +++ b/src/PIM/Compiler/PimCompilerOptions.cpp @@ -87,11 +87,6 @@ llvm::cl::opt pimConvStreamChunkPositions( llvm::cl::init(1024), llvm::cl::cat(OnnxMlirOptions)); -llvm::cl::opt pimReportConvLowering("pim-report-conv-lowering", - llvm::cl::desc("Emit a bounded Conv lowering report"), - llvm::cl::init(true), - llvm::cl::cat(OnnxMlirOptions)); - llvm::cl::opt pimEmitJson("pim-emit-json", llvm::cl::desc("Also emit per-core JSON instruction files alongside binary .pim files"), llvm::cl::init(false), diff --git a/src/PIM/Compiler/PimCompilerOptions.hpp b/src/PIM/Compiler/PimCompilerOptions.hpp index 00c2472..84cf80f 100644 --- a/src/PIM/Compiler/PimCompilerOptions.hpp +++ b/src/PIM/Compiler/PimCompilerOptions.hpp @@ -57,7 +57,6 @@ extern llvm::cl::opt pimExportSpatialDataflow; extern llvm::cl::opt pimOnlyCodegen; extern llvm::cl::opt useExperimentalConvImpl; extern llvm::cl::opt pimEmitJson; -extern llvm::cl::opt pimReportConvLowering; extern llvm::cl::opt pimDetectCommunicationDeadlock; extern llvm::cl::opt pimMaterializeScalarFanoutGlobalOrder; extern llvm::cl::opt pimTraceCommunicationMaterialization; diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp index 9f430b0..93db327 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.cpp @@ -47,13 +47,17 @@ FailureOr createFragmentAssemblyBlueprint(Value physicalBatch, llvm::append_range(offsets, entry.destinationOffsets); llvm::append_range(sizes, entry.sizes); } - return spatial::SpatBlueprintOp::create(rewriter, loc, logicalType, physicalBatch, ValueRange {}, + auto blueprint = 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(); + rewriter.getStringAttr("disjoint"), rewriter.getStringAttr("complete")); + if (indexMap == spatial::kContiguousRowMajorFragments + && !spatial::isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + blueprint.setIndexMapAttr(rewriter.getStringAttr("fragment_assembly")); + return blueprint.getOutput(); } Value sumTensors(ArrayRef tensors, PatternRewriter& rewriter) { diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp index 398a520..0132655 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/ComputeRegionBuilder.hpp @@ -394,6 +394,39 @@ extractGraphBatchPhysicalFragment(mlir::PatternRewriter& rewriter, rewriter, loc, physicalBatch, fragmentType, {offsets, sizes, strides}); } +template +mlir::FailureOr mapGraphBatchFragments(mlir::Value input, + mlir::RankedTensorType outputType, + mlir::PatternRewriter& rewriter, + mlir::Location loc, + BodyFn&& build) { + auto inputType = mlir::dyn_cast(input.getType()); + if (!inputType || !inputType.hasStaticShape() || !outputType.hasStaticShape() + || inputType.getRank() != outputType.getRank() || inputType.getRank() < 2 + || inputType.getDimSize(0) != outputType.getDimSize(0)) + return mlir::failure(); + auto inputFragmentType = mlir::RankedTensorType::get( + inputType.getShape().drop_front(), inputType.getElementType(), inputType.getEncoding()); + auto outputFragmentType = mlir::RankedTensorType::get( + outputType.getShape().drop_front(), outputType.getElementType(), outputType.getEncoding()); + auto batch = createSpatComputeBatch( + rewriter, loc, mlir::TypeRange {outputType}, inputType.getDimSize(0), {}, mlir::ValueRange {input}, + [&](detail::SpatComputeBatchBodyArgs args) -> mlir::LogicalResult { + auto fragment = extractGraphBatchPhysicalFragment( + rewriter, loc, args.inputs.front(), args.lane, inputFragmentType); + if (mlir::failed(fragment)) + return mlir::failure(); + mlir::FailureOr result = build(*fragment, outputFragmentType); + if (mlir::failed(result) || result->getType() != outputFragmentType) + return mlir::failure(); + publishGraphBatchPhysicalFragment(rewriter, loc, *result, args.outputs.front(), args.lane); + return mlir::success(); + }); + if (mlir::failed(batch)) + return mlir::failure(); + return batch->getResult(0); +} + template mlir::Value materializeOrComputeUnary(mlir::Value input, mlir::RankedTensorType resultType, diff --git a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp index 413911c..a39de5d 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.cpp @@ -186,25 +186,9 @@ static FailureOr applyRowStripActivation(const RowStripPhysicalValue& val Location loc, BuildActivation buildActivation) { auto storageType = cast(value.storage.getType()); - const int64_t laneCount = storageType.getDimSize(0); - auto batchOp = createSpatComputeBatch(rewriter, - loc, - TypeRange {storageType}, - laneCount, - {}, - ValueRange {value.storage}, - [&](detail::SpatComputeBatchBodyArgs args) { - FailureOr fragment = extractGraphBatchPhysicalFragment( - rewriter, loc, args.inputs.front(), args.lane, value.fragmentType); - if (failed(fragment)) return failure(); - Value result = buildActivation(*fragment); - publishGraphBatchPhysicalFragment( - rewriter, loc, result, args.outputs.front(), args.lane); - return success(); - }); - if (failed(batchOp)) - return failure(); - return batchOp->getResult(0); + return mapGraphBatchFragments(value.storage, storageType, rewriter, loc, [&](Value fragment, RankedTensorType) { + return FailureOr(buildActivation(fragment)); + }); } FailureOr applyRowStripRelu(const RowStripPhysicalValue& value, PatternRewriter& rewriter, Location loc) { diff --git a/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp b/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp index cdd6ff0..74bb30b 100644 --- a/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/LowerSpatialPlansPass.cpp @@ -399,6 +399,42 @@ struct LowerSpatialPlansPass final : PassWrapper(&op)) { + FailureOr input = + getRowStripValue(rowStripValues, planOp.getInput()); + rewriter.setInsertionPoint(planOp); + auto lowered = lowerSelectedResizeNearestPlan( + planOp, succeeded(input) ? std::optional(input->storage) : std::nullopt, + rewriter); + if (failed(lowered)) { + planOp.emitOpError("failed to lower selected nearest Resize plan"); + signalPassFailure(); + return; + } + if (failed(input)) { + rewriter.replaceOp(planOp, *lowered); + continue; + } + auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) { + auto blueprint = dyn_cast(user); + return blueprint && blueprint.getPhysicalLayout() == kRowStripLayout; + }); + if (outputBlueprint == planOp.getResult().getUsers().end()) { + planOp.emitOpError("row-strip Resize plan requires a row-strip blueprint result"); + signalPassFailure(); + return; + } + auto blueprint = cast(*outputBlueprint); + auto output = buildRowStripValue(blueprint, *lowered); + if (failed(output)) { + signalPassFailure(); + return; + } + rowStripValues[blueprint.getResult()] = *output; + eraseAfterLowering.insert(planOp); + eraseAfterLowering.insert(blueprint); + continue; + } if (auto planOp = dyn_cast(&op)) { auto outputBlueprint = llvm::find_if(planOp.getResult().getUsers(), [](Operation* user) { auto blueprint = dyn_cast(user); @@ -701,6 +737,7 @@ struct LowerSpatialPlansPass final : PassWrapper(op) diff --git a/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialPass.cpp b/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialPass.cpp index 1e25d72..a7b4d72 100644 --- a/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialPass.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialPass.cpp @@ -52,13 +52,16 @@ static void populateEmptyFunction(func::FuncOp funcOp) { SmallVector concatPlans(funcOp.getOps()); SmallVector reluPlans(funcOp.getOps()); SmallVector siluPlans(funcOp.getOps()); + SmallVector resizePlans( + funcOp.getOps()); SmallVector maxPoolPlans(funcOp.getOps()); SmallVector globalAveragePoolPlans( funcOp.getOps()); SmallVector blueprints(funcOp.getOps()); SmallVector materializers(funcOp.getOps()); if (!computes.empty() || !computeBatches.empty() || !convPlans.empty() || !biasAddPlans.empty() || !addPlans.empty() - || !concatPlans.empty() || !reluPlans.empty() || !siluPlans.empty() || !maxPoolPlans.empty() || !blueprints.empty() + || !concatPlans.empty() || !reluPlans.empty() || !siluPlans.empty() || !resizePlans.empty() + || !maxPoolPlans.empty() || !blueprints.empty() || !globalAveragePoolPlans.empty() || !materializers.empty()) { return; } @@ -123,6 +126,14 @@ void ONNXToSpatialPass::runOnOperation() { return; } + RewritePatternSet matmulPatterns(ctx); + populateMatMulFusionPatterns(matmulPatterns, ctx); + if (failed(applyPatternsGreedily(moduleOp, std::move(matmulPatterns)))) { + moduleOp.emitError("failed to lower MatMul before producer conversion"); + signalPassFailure(); + return; + } + RewritePatternSet fusionPatterns(ctx); populateElementwiseFusionPatterns(fusionPatterns, ctx); if (failed(applyPatternsGreedily(moduleOp, std::move(fusionPatterns)))) { diff --git a/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.cpp b/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.cpp index 9e92dcf..258fc89 100644 --- a/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/ONNXToSpatialVerifier.cpp @@ -150,6 +150,7 @@ void verifyLogicalTopLevelOps(func::FuncOp funcOp, pim::CappedDiagnosticReporter spatial::SpatConcatPlanOp, spatial::SpatReluPlanOp, spatial::SpatSiluPlanOp, + spatial::SpatResizeNearestPlanOp, spatial::SpatMaxPool2DPlanOp, spatial::SpatGlobalAveragePoolPlanOp, spatial::SpatBlueprintOp, diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns.hpp b/src/PIM/Conversion/ONNXToSpatial/Patterns.hpp index e3cec73..bb9d069 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns.hpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns.hpp @@ -20,6 +20,7 @@ void populateElementwisePatterns(mlir::RewritePatternSet& patterns, mlir::MLIRCo void populateElementwiseFusionPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); void populateGemmPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); void populateMatMulRewritePatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); +void populateMatMulFusionPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); void populatePoolPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); void populateReduceMeanPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); void populateReluPatterns(mlir::RewritePatternSet& patterns, mlir::MLIRContext* ctx); diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp index 2450228..4eb5450 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp @@ -1,28 +1,19 @@ #include "mlir/Dialect/Arith/IR/Arith.h" -#include "mlir/Dialect/Func/IR/FuncOps.h" #include "mlir/Dialect/Linalg/IR/Linalg.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/IR/BuiltinAttributes.h" #include "mlir/IR/BuiltinTypes.h" -#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/SmallVector.h" -#include "llvm/Support/raw_ostream.h" #include -#include -#include -#include #include -#include -#include #include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp" #include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp" #include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp" #include "src/Accelerators/PIM/Common/Support/Diagnostics.hpp" -#include "src/Accelerators/PIM/Common/Support/ReportUtils.hpp" #include "src/Accelerators/PIM/Compiler/PimCompilerOptions.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/BiasAddUtils.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp" @@ -46,199 +37,12 @@ struct ConvToGemm : OpConversionPattern { ConversionPatternRewriter& rewriter) const override; }; -struct ConvLoweringDecision { - PimConvLoweringType strategy; - std::string reason; - bool isAuto = false; - std::string fallbackReason; - std::string rejectedAutoStrategy; -}; - struct PreparedConvInput { Value value; RankedTensorType type; }; -struct ConvStrategyEstimate { - uint64_t estimatedMvmCount = 0; - uint64_t estimatedReductionVAddCount = 0; - uint64_t estimatedOutputFragments = 1; - bool perOutputPositionReduction = false; - bool requiresFuncReturnMaterialization = false; - bool giantCollectorConcatExpected = false; - bool fullInputBroadcastExpected = false; - uint64_t concatOperandCount = 0; - std::string materializationKind = "none"; - std::string collectorCore = "-"; -}; - -struct ConvReportEntry { - uint64_t id; - std::string where; - std::string strategy; - std::string mode; - std::string inputShape; - std::string weightShape; - std::string outputShape; - int64_t groups; - int64_t k; - int64_t c; - int64_t p; - int64_t xbarSize; - int64_t pack; - uint64_t im2colElements; - uint64_t im2colBudget; - std::string chunkText; - int64_t batchSize; - int64_t numberOfBatches; - std::string spatialComputeBatch; - std::string batchedInstructionEmission; - std::string reason; - std::string fallbackReason; - std::string rejectedAutoStrategy; - uint64_t estimatedMvmCount; - uint64_t estimatedReductionVAddCount; - uint64_t estimatedOutputFragments; - std::string materializationRequiredAtReturn; - std::string materializationKind; - uint64_t concatOperandCount; - std::string collectorCore; - std::string giantCollectorConcatExpected; - std::string fullInputBroadcastExpected; -}; - -enum class DistributedTensorOpKind { - Relu, - Sigmoid, - Add, - Sub, - Mul, - Div, - Conv, -}; - -enum class DistributedConvBarrierKind { - Return, - UnsupportedConsumer, - Fanout, - DeadValue, - GroupedConv, - Depthwise, -}; - -enum class DistributedTensorConstantKind { - None, - Splat, - PerChannel, -}; - -enum class DistributedTensorLayoutKind { - NchwRowStrip, -}; - -struct DistributedFragmentInfo { - SmallVector offsets; - SmallVector sizes; - SmallVector strides; - int64_t producerLane = 0; -}; - -struct DistributedTensorInfo { - Value storage; - RankedTensorType logicalType; - DistributedTensorLayoutKind layoutKind = DistributedTensorLayoutKind::NchwRowStrip; - SmallVector fragments; - int64_t laneCount = 0; - int64_t fragmentHeight = 1; - int64_t channels = 0; - int64_t height = 0; - int64_t width = 0; - - bool isRowStripNchw() const { return layoutKind == DistributedTensorLayoutKind::NchwRowStrip; } -}; - -struct DistributedTensorRegistry { - llvm::DenseMap infos; - - void bind(Value value, const DistributedTensorInfo& info) { infos[value] = info; } - - const DistributedTensorInfo* lookup(Value value) const { - auto it = infos.find(value); - if (it == infos.end()) - return nullptr; - return &it->second; - } -}; - -struct DistributedTensorStep { - Operation* op = nullptr; - DistributedTensorOpKind kind; - DenseElementsAttr constantAttr; - DistributedTensorConstantKind constantKind = DistributedTensorConstantKind::None; - bool fragmentOnLhs = true; - std::optional convState; -}; - -struct DistributedConvAnalysis { - SmallVector steps; - Operation* replacementOp = nullptr; - DistributedConvBarrierKind barrierKind = DistributedConvBarrierKind::UnsupportedConsumer; - std::string barrierDetail; - - bool hasLocalConsumers() const { return !steps.empty(); } - bool hasDistributedConvConsumer() const { - return llvm::any_of(steps, [](const DistributedTensorStep& step) { return step.kind == DistributedTensorOpKind::Conv; }); - } -}; - -struct DistributedChainReportEntry { - uint64_t chainId = 0; - uint64_t chainLength = 0; - std::string producerKind; - std::string distributedOps; - std::string materializationPoints; - std::string firstMaterializationReason; - uint64_t maxLiveFragments = 0; - uint64_t maxFragmentFanout = 0; - uint64_t patchBuilderCoreCount = 0; - std::string centralJunctionDetected = "no"; - std::string convInputMaterializationKind = "dense_materialization_fallback"; - uint64_t localPatchFragments = 0; - uint64_t remotePatchFragments = 0; - uint64_t haloTransferCount = 0; - uint64_t groupedTransferCount = 0; - std::string fallbackReason; -}; - -struct DistributedConvReportTotals { - uint64_t totalConvs = 0; - uint64_t distributedTensorsCreated = 0; - uint64_t distributedValuesPropagated = 0; - uint64_t distributedConsumersHandled = 0; - uint64_t distributedConvInputsSeen = 0; - uint64_t distributedConvInputsConsumed = 0; - uint64_t materializationBarriersInserted = 0; - std::map fallbackReasons; - std::map barrierReasons; - SmallVector chains; -}; - static Value createZeroGemmBias(RankedTensorType gemmResultType, PatternRewriter& rewriter); - -static FailureOr analyzeConvLoweringState(ONNXConvOp convOp, Value x, Value w, Value b); - -static StringRef stringifyDistributedConvBarrierKind(DistributedConvBarrierKind kind) { - switch (kind) { - case DistributedConvBarrierKind::Return: return "func.return"; - case DistributedConvBarrierKind::UnsupportedConsumer: return "unsupported_consumer"; - case DistributedConvBarrierKind::Fanout: return "fanout"; - case DistributedConvBarrierKind::DeadValue: return "dead_value"; - case DistributedConvBarrierKind::GroupedConv: return "grouped_conv"; - case DistributedConvBarrierKind::Depthwise: return "depthwise_conv"; - } - llvm_unreachable("unknown distributed conv barrier kind"); -} - static StringRef stringifyConvLoweringStrategy(PimConvLoweringType strategy) { switch (strategy) { case PimConvLoweringAuto: return "auto"; @@ -254,825 +58,29 @@ static StringRef stringifyConvLoweringStrategy(PimConvLoweringType strategy) { llvm_unreachable("unknown conv lowering strategy"); } -static bool requiresFuncReturnMaterialization(const DistributedConvAnalysis& analysis) { - return !analysis.hasLocalConsumers() && analysis.barrierKind == DistributedConvBarrierKind::Return; -} - -static ConvStrategyEstimate estimateConvStrategy(const ConvGeometry& geo, - PimConvLoweringType strategy, - const DistributedConvAnalysis& analysis) { - ConvStrategyEstimate estimate; - estimate.requiresFuncReturnMaterialization = requiresFuncReturnMaterialization(analysis); - estimate.materializationKind = estimate.requiresFuncReturnMaterialization ? "func.return" : "none"; - - switch (strategy) { - case PimConvLoweringLegacy: - case PimConvLoweringPackedIm2Col: - estimate.estimatedMvmCount = static_cast(std::max(1, geo.p)); - break; - case PimConvLoweringStreamedPatch: - case PimConvLoweringStreamedPacked: - case PimConvLoweringOutputChannelTiled: { - uint64_t chunkPositions = chooseStreamChunkPositions(geo, /*packFactor=*/1); - estimate.estimatedMvmCount = static_cast(std::max(1, geo.p)); - estimate.estimatedOutputFragments = - std::max(1, static_cast(ceilIntegerDivide(geo.p, static_cast(chunkPositions)))); - break; - } - case PimConvLoweringInputKTiled: { - const int64_t numKSlices = ceilIntegerDivide(geo.k, geo.xbarSize); - const uint64_t maxLanesPerBatch = - std::max(1, - static_cast(crossbarCountInCore.getValue()) - / static_cast(std::max(1, numKSlices * 4))); - const uint64_t rowChunkWidth = std::max( - 1, - std::min({chooseStreamChunkPositions(geo, /*packFactor=*/1), - maxLanesPerBatch, - static_cast(std::max(1, geo.outWidth))})); - estimate.estimatedMvmCount = - static_cast(std::max(1, geo.p)) * static_cast(std::max(1, numKSlices)); - estimate.estimatedReductionVAddCount = - static_cast(std::max(1, geo.p)) - * static_cast(std::max(0, numKSlices - 1) + (geo.hasBias ? 1 : 0)); - estimate.estimatedOutputFragments = static_cast(std::max(1, geo.batchSize)) - * static_cast(std::max(1, geo.outHeight)) - * static_cast( - ceilIntegerDivide(geo.outWidth, static_cast(rowChunkWidth))); - estimate.perOutputPositionReduction = numKSlices > 1; - estimate.fullInputBroadcastExpected = estimate.estimatedOutputFragments > 1; - if (estimate.requiresFuncReturnMaterialization && estimate.estimatedOutputFragments >= 128) { - estimate.giantCollectorConcatExpected = true; - estimate.materializationKind = "single_collector_concat"; - estimate.concatOperandCount = estimate.estimatedOutputFragments; - estimate.collectorCore = "scheduled_post_merge"; - } - break; - } - case PimConvLoweringTiled2D: - estimate.estimatedMvmCount = static_cast(std::max(1, geo.p)); - break; - case PimConvLoweringDepthwise: - case PimConvLoweringAuto: - break; - } - - if (estimate.requiresFuncReturnMaterialization && estimate.materializationKind == "none") - estimate.materializationKind = "func.return"; - return estimate; -} - -static std::string formatShape(ArrayRef dims) { - std::string text; - llvm::raw_string_ostream os(text); - os << "["; - for (size_t i = 0; i < dims.size(); ++i) { - if (i != 0) - os << "x"; - os << dims[i]; - } - os << "]"; - return text; -} - -static std::string collapseWhitespace(StringRef text) { - std::string out; - out.reserve(text.size()); - bool lastWasSpace = false; - for (char c : text) { - bool isSpace = std::isspace(static_cast(c)); - if (isSpace) { - if (!lastWasSpace && !out.empty()) - out.push_back(' '); - lastWasSpace = true; - continue; - } - out.push_back(c); - lastWasSpace = false; - } - return out; -} - -static std::string abbreviate(StringRef text, size_t maxLen) { - if (text.size() <= maxLen) - return text.str(); - return (text.take_front(maxLen - 3) + "...").str(); -} - -static std::string abbreviateFromEnd(StringRef text, size_t maxLen) { - if (text.size() <= maxLen) - return text.str(); - return ("..." + text.take_back(maxLen - 3)).str(); -} - -static std::string summarizeLocation(Location loc, size_t maxLen = 44) { - std::string text; - llvm::raw_string_ostream os(text); - loc.print(os); - os.flush(); - std::string collapsed = collapseWhitespace(text); - if (collapsed.size() <= maxLen) - return collapsed; - if (collapsed.find('/') != std::string::npos || collapsed.find('#') != std::string::npos) - return abbreviateFromEnd(collapsed, maxLen); - return abbreviate(collapsed, maxLen); -} - -static std::string alignCell(StringRef text, size_t width, bool rightAlign = false) { - std::string cell = text.str(); - if (cell.size() < width) { - size_t padding = width - cell.size(); - if (rightAlign) - cell.insert(cell.begin(), padding, ' '); - else - cell.append(padding, ' '); - } - return cell; -} - -static bool hasSameStaticTensorType(Value value, Type expectedType) { - auto valueType = dyn_cast(value.getType()); - auto expectedTensorType = dyn_cast(expectedType); - return valueType && expectedTensorType && valueType.hasStaticShape() && valueType == expectedTensorType; -} - -static bool isSplatConstantValue(Value value, DenseElementsAttr& denseAttr) { - denseAttr = getHostConstDenseElementsAttr(value); - return static_cast(denseAttr) && denseAttr.isSplat(); -} - -static bool isPerChannelConstantValue(Value value, RankedTensorType currentType, DenseElementsAttr& denseAttr) { - denseAttr = getHostConstDenseElementsAttr(value); - if (!denseAttr || denseAttr.isSplat()) - return false; - - auto constantType = dyn_cast(denseAttr.getType()); - if (!constantType || !constantType.hasStaticShape()) - return false; - - const int64_t channels = currentType.getDimSize(1); - if (constantType.getRank() == 1) - return constantType.getDimSize(0) == channels; - if (constantType.getRank() == 2) - return constantType.getDimSize(0) == 1 && constantType.getDimSize(1) == channels; - if (constantType.getRank() != 4) - return false; - return constantType.getDimSize(0) == 1 && constantType.getDimSize(1) == channels - && constantType.getDimSize(2) == 1 && constantType.getDimSize(3) == 1; -} - -static std::optional -classifyDistributedBinaryConsumer(Operation* user, - Value currentValue, - Value lhs, - Value rhs, - DistributedTensorOpKind kind, - bool allowFragmentOnRhs, - std::string& failureDetail) { - if (user->getNumResults() != 1 || !hasSameStaticTensorType(user->getResult(0), currentValue.getType())) { - failureDetail = "result type mismatch"; - return std::nullopt; - } - - auto currentType = cast(currentValue.getType()); - DenseElementsAttr constantAttr; - if (lhs == currentValue) { - DistributedTensorConstantKind constantKind = DistributedTensorConstantKind::None; - if (isSplatConstantValue(rhs, constantAttr)) - constantKind = DistributedTensorConstantKind::Splat; - else if (isPerChannelConstantValue(rhs, currentType, constantAttr)) - constantKind = DistributedTensorConstantKind::PerChannel; - else - failureDetail = "unsupported rhs broadcast"; - if (constantKind == DistributedTensorConstantKind::None) - return std::nullopt; - return DistributedTensorStep {user, kind, constantAttr, constantKind, /*fragmentOnLhs=*/true, std::nullopt}; - } - - if (rhs == currentValue && allowFragmentOnRhs) { - DistributedTensorConstantKind constantKind = DistributedTensorConstantKind::None; - if (isSplatConstantValue(lhs, constantAttr)) - constantKind = DistributedTensorConstantKind::Splat; - else if (isPerChannelConstantValue(lhs, currentType, constantAttr)) - constantKind = DistributedTensorConstantKind::PerChannel; - else - failureDetail = "unsupported lhs broadcast"; - if (constantKind == DistributedTensorConstantKind::None) - return std::nullopt; - return DistributedTensorStep {user, kind, constantAttr, constantKind, /*fragmentOnLhs=*/false, std::nullopt}; - } - - failureDetail = "conv result is not the supported binary operand"; - return std::nullopt; -} - -static std::string stringifyDistributedTensorOpKind(DistributedTensorOpKind kind) { - switch (kind) { - case DistributedTensorOpKind::Relu: return "Relu"; - case DistributedTensorOpKind::Sigmoid: return "Sigmoid"; - case DistributedTensorOpKind::Add: return "Add"; - case DistributedTensorOpKind::Sub: return "Sub"; - case DistributedTensorOpKind::Mul: return "Mul"; - case DistributedTensorOpKind::Div: return "Div"; - case DistributedTensorOpKind::Conv: return "Conv"; - } - llvm_unreachable("unknown distributed tensor op kind"); -} - -[[maybe_unused]] static DistributedConvAnalysis analyzeDistributedConvConsumers(ONNXConvOp convOp) { - DistributedConvAnalysis analysis; - analysis.replacementOp = convOp; - - Value currentValue = convOp.getResult(); - while (true) { - if (currentValue.use_empty()) { - analysis.barrierKind = DistributedConvBarrierKind::DeadValue; - analysis.barrierDetail = "result has no users"; - return analysis; - } - - if (!currentValue.hasOneUse()) { - analysis.barrierKind = DistributedConvBarrierKind::Fanout; - analysis.barrierDetail = "value has multiple users"; - return analysis; - } - - Operation* user = *currentValue.getUsers().begin(); - if (isa(user)) { - analysis.barrierKind = DistributedConvBarrierKind::Return; - analysis.barrierDetail = "materialize at func.return"; - return analysis; - } - - std::optional step; - std::string failureDetail; - if (auto reluOp = dyn_cast(user)) { - if (hasSameStaticTensorType(reluOp.getResult(), currentValue.getType())) - step = DistributedTensorStep { - user, DistributedTensorOpKind::Relu, {}, DistributedTensorConstantKind::None, true, std::nullopt}; - else - failureDetail = "relu result type mismatch"; - } - else if (auto sigmoidOp = dyn_cast(user)) { - if (hasSameStaticTensorType(sigmoidOp.getResult(), currentValue.getType())) - step = DistributedTensorStep { - user, DistributedTensorOpKind::Sigmoid, {}, DistributedTensorConstantKind::None, true, std::nullopt}; - else - failureDetail = "sigmoid result type mismatch"; - } - else if (auto addOp = dyn_cast(user)) { - step = classifyDistributedBinaryConsumer( - user, currentValue, addOp.getA(), addOp.getB(), DistributedTensorOpKind::Add, /*allowFragmentOnRhs=*/true, - failureDetail); - } - else if (auto subOp = dyn_cast(user)) { - step = classifyDistributedBinaryConsumer( - user, currentValue, subOp.getA(), subOp.getB(), DistributedTensorOpKind::Sub, /*allowFragmentOnRhs=*/true, - failureDetail); - } - else if (auto mulOp = dyn_cast(user)) { - step = classifyDistributedBinaryConsumer( - user, currentValue, mulOp.getA(), mulOp.getB(), DistributedTensorOpKind::Mul, /*allowFragmentOnRhs=*/true, - failureDetail); - } - else if (auto divOp = dyn_cast(user)) { - step = classifyDistributedBinaryConsumer( - user, currentValue, divOp.getA(), divOp.getB(), DistributedTensorOpKind::Div, /*allowFragmentOnRhs=*/false, - failureDetail); - if (step) { - auto denseAttr = dyn_cast(step->constantAttr); - if (!denseAttr) { - failureDetail = "div requires floating-point splat constant"; - step.reset(); - } - } - } - else if (auto nextConv = dyn_cast(user)) { - failureDetail = "onnx.Conv distributed consumer blocked by dim0-only whole-batch materialization in MergeComputeNodes"; - } - else { - failureDetail = (user->getName().getStringRef() + " is not distributed-aware yet").str(); - } - - if (!step) { - analysis.barrierKind = DistributedConvBarrierKind::UnsupportedConsumer; - analysis.barrierDetail = failureDetail; - return analysis; - } - - analysis.replacementOp = user; - analysis.steps.push_back(*step); - currentValue = user->getResult(0); - } -} - -static void rewriteDistributedConvReport(const DistributedConvReportTotals& totals) { - std::fstream reportFile = openReportFile("conv_distributed_consumption_report"); - if (!reportFile.is_open()) - return; - - reportFile << "# PIM Conv Distributed Consumption Report\n\n"; - reportFile << "Totals:\n"; - reportFile << "- convs_seen: " << totals.totalConvs << "\n"; - reportFile << "- distributed_tensors_created: " << totals.distributedTensorsCreated << "\n"; - reportFile << "- distributed_values_propagated: " << totals.distributedValuesPropagated << "\n"; - reportFile << "- distributed_consumers_handled_locally: " << totals.distributedConsumersHandled << "\n"; - reportFile << "- distributed_conv_inputs_seen: " << totals.distributedConvInputsSeen << "\n"; - reportFile << "- distributed_conv_inputs_consumed: " << totals.distributedConvInputsConsumed << "\n"; - reportFile << "- materialization_barriers_inserted: " << totals.materializationBarriersInserted << "\n\n"; - - if (!totals.barrierReasons.empty()) { - reportFile << "Materialization barriers:\n"; - for (const auto& [reason, count] : totals.barrierReasons) - reportFile << "- " << reason << ": " << count << "\n"; - reportFile << "\n"; - } - - if (!totals.fallbackReasons.empty()) { - reportFile << "Fallback / no-distribution reasons:\n"; - for (const auto& [reason, count] : totals.fallbackReasons) - reportFile << "- " << reason << ": " << count << "\n"; - reportFile << "\n"; - } - - if (!totals.chains.empty()) { - reportFile << "Chains:\n"; - for (const DistributedChainReportEntry& chain : totals.chains) { - reportFile << "- chain_id: " << chain.chainId << "\n"; - reportFile << " chain_length: " << chain.chainLength << "\n"; - reportFile << " producer_kind: " << chain.producerKind << "\n"; - reportFile << " distributed_ops: " << chain.distributedOps << "\n"; - reportFile << " materialization_points: " << chain.materializationPoints << "\n"; - reportFile << " first_materialization_reason: " << chain.firstMaterializationReason << "\n"; - reportFile << " max_live_fragments: " << chain.maxLiveFragments << "\n"; - reportFile << " max_fragment_fanout: " << chain.maxFragmentFanout << "\n"; - reportFile << " patch_builder_core_count: " << chain.patchBuilderCoreCount << "\n"; - reportFile << " central_junction_detected: " << chain.centralJunctionDetected << "\n"; - reportFile << " conv_input_materialization_kind: " << chain.convInputMaterializationKind << "\n"; - reportFile << " local_patch_fragments: " << chain.localPatchFragments << "\n"; - reportFile << " remote_patch_fragments: " << chain.remotePatchFragments << "\n"; - reportFile << " halo_transfer_count: " << chain.haloTransferCount << "\n"; - reportFile << " grouped_transfer_count: " << chain.groupedTransferCount << "\n"; - if (!chain.fallbackReason.empty()) - reportFile << " fallback_reason: " << chain.fallbackReason << "\n"; - } - } -} - -[[maybe_unused]] static void recordDistributedConvOutcome(const DistributedConvAnalysis& analysis) { - static std::mutex reportMutex; - static DistributedConvReportTotals totals; - - std::string barrierKey = stringifyDistributedConvBarrierKind(analysis.barrierKind).str(); - if (!analysis.barrierDetail.empty()) - barrierKey += ": " + analysis.barrierDetail; - - std::lock_guard guard(reportMutex); - totals.totalConvs++; - if (analysis.hasLocalConsumers()) { - totals.distributedTensorsCreated++; - totals.distributedValuesPropagated += analysis.steps.size(); - totals.distributedConsumersHandled += llvm::count_if(analysis.steps, [](const DistributedTensorStep& step) { - return step.kind != DistributedTensorOpKind::Conv; - }); - totals.distributedConvInputsSeen += llvm::count_if(analysis.steps, [](const DistributedTensorStep& step) { - return step.kind == DistributedTensorOpKind::Conv; - }); - totals.distributedConvInputsConsumed += llvm::count_if(analysis.steps, [](const DistributedTensorStep& step) { - return step.kind == DistributedTensorOpKind::Conv; - }); - totals.materializationBarriersInserted++; - totals.barrierReasons[barrierKey]++; - } - else { - totals.fallbackReasons[barrierKey]++; - } - DistributedChainReportEntry chain; - chain.chainId = totals.totalConvs; - chain.chainLength = analysis.steps.size() + 1; - chain.producerKind = "Conv"; - chain.materializationPoints = stringifyDistributedConvBarrierKind(analysis.barrierKind).str(); - chain.firstMaterializationReason = analysis.barrierDetail; - chain.maxFragmentFanout = analysis.barrierKind == DistributedConvBarrierKind::Fanout ? 2 : 1; - std::string ops; - for (size_t index = 0; index < analysis.steps.size(); ++index) { - if (!ops.empty()) - ops += ", "; - ops += stringifyDistributedTensorOpKind(analysis.steps[index].kind); - } - chain.distributedOps = ops; - if (analysis.hasDistributedConvConsumer()) { - chain.convInputMaterializationKind = "distributed_with_halo_exchange"; - chain.patchBuilderCoreCount = 1; - } - if (totals.chains.size() == 16) - totals.chains.erase(totals.chains.begin()); - totals.chains.push_back(std::move(chain)); - rewriteDistributedConvReport(totals); -} - -static std::string makeDivider(ArrayRef widths) { - std::string divider = "+"; - for (size_t width : widths) { - divider.append(width + 2, '-'); - divider.push_back('+'); - } - return divider; -} - -static void printConvReportLegend(std::fstream& reportFile) { - reportFile << "# PIM Conv Lowering Report\n\n"; - reportFile << "Legend:\n"; - reportFile << "- `id`: sequential Conv report entry index within this compiler invocation.\n"; - reportFile << "- `where`: summarized MLIR location of the Conv op.\n"; - reportFile << "- `mode`: whether the selected strategy came from `auto` policy or a forced compiler option.\n"; - reportFile << "- `strategy`: selected Conv lowering algorithm.\n"; - reportFile << "- `input`, `weight`, `output`: tensor shapes of the Conv operands/result.\n"; - reportFile << "- `groups`: ONNX Conv group count.\n"; - reportFile << "- `K`: logical reduction size, `CinPerGroup * Kh * Kw`.\n"; - reportFile << "- `C`: logical output-channel width handled by the selected strategy.\n"; - reportFile << "- `P`: total output positions, `N * Hout * Wout`.\n"; - reportFile << "- `X`: crossbar size.\n"; - reportFile << "- `pack`: packed spatial positions per MVM group, `floor(X / max(K, C))`.\n"; - reportFile << "- `im2col`: total explicit im2col element count, `P * K`.\n"; - reportFile << "- `im2col_budget`: maximum im2col element budget allowed by the compiler option.\n"; - reportFile << "- `stream_chunk`: output positions materialized per streamed chunk, or `-` when not applicable.\n"; - reportFile << "- `batch_size`: logical batch size passed into the compute lowering for this Conv form.\n"; - reportFile << "- `batches`: number of repeated compute batches emitted for the Conv.\n"; - reportFile << "- `spatial_compute_batch`: whether ONNX-to-Spatial used `spat.compute_batch` for this Conv.\n"; - reportFile << "- `batched_instruction_emission`: whether the lowering is expected to reach batched PIM emission.\n"; - reportFile << "- `reason`: strategy-selection reason.\n"; - reportFile << "- Per-conv details below the table include profitability estimates and materialization diagnostics.\n"; - reportFile << "- placeholders like `[7]`: value too long for the table cell; see the appendix at the end.\n\n"; -} - -struct ConvReportOverflow { - uint64_t placeholderId; - SmallVector rowIds; - std::string column; - std::string value; -}; - -static StringRef describeConvLoweringStrategy(PimConvLoweringType strategy) { - switch (strategy) { - case PimConvLoweringAuto: return "Automatic policy selection."; - case PimConvLoweringLegacy: return "Legacy Conv lowering path kept for compatibility."; - case PimConvLoweringDepthwise: return "Specialized depthwise lowering that avoids generic cross-channel GEMM mixing."; - case PimConvLoweringPackedIm2Col: return "Explicit im2col plus packed GEMM lowering for Conv shapes that fit well in one crossbar."; - case PimConvLoweringStreamedPatch: return "Chunked per-patch streaming Conv lowering without global im2col materialization."; - case PimConvLoweringStreamedPacked: return "Chunked streamed Conv lowering that still packs multiple output positions per MVM group."; - case PimConvLoweringOutputChannelTiled: return "Conv lowering that splits output channels across tiles when C exceeds one crossbar width."; - case PimConvLoweringInputKTiled: return "Conv lowering that splits the reduction dimension K across tiles and accumulates partial sums."; - case PimConvLoweringTiled2D: return "Conv lowering that tiles both K and output channels because neither dimension fits one crossbar."; - } - llvm_unreachable("unknown conv lowering strategy"); -} - -static std::string fitConvReportCell(StringRef text, - size_t width, - uint64_t rowId, - StringRef column, - std::vector& overflows, - uint64_t& nextPlaceholderId, - bool rightAlign = false) { - if (text.size() <= width) - return alignCell(text, width, rightAlign); - - for (ConvReportOverflow& overflow : overflows) { - if (overflow.column == column && overflow.value == text) { - if (llvm::find(overflow.rowIds, rowId) == overflow.rowIds.end()) - overflow.rowIds.push_back(rowId); - std::string placeholder = "[" + std::to_string(overflow.placeholderId) + "]"; - return alignCell(placeholder, width, rightAlign); - } - } - - std::string placeholder = "[" + std::to_string(nextPlaceholderId++) + "]"; - overflows.push_back({nextPlaceholderId - 1, {rowId}, column.str(), text.str()}); - return alignCell(placeholder, width, rightAlign); -} - -static void writeConvReportTable(std::fstream& reportFile, ArrayRef entries) { - static constexpr size_t kIdWidth = 4; - static constexpr size_t kWhereWidth = 24; - static constexpr size_t kModeWidth = 6; - static constexpr size_t kStrategyWidth = 20; - static constexpr size_t kShapeWidth = 14; - static constexpr size_t kGroupsWidth = 3; - static constexpr size_t kSmallWidth = 5; - static constexpr size_t kPWidth = 10; - static constexpr size_t kIm2colWidth = 10; - static constexpr size_t kChunkWidth = 8; - static constexpr size_t kFlagWidth = 3; - static constexpr size_t kReasonWidth = 24; - - const SmallVector widths = { - kIdWidth, kWhereWidth, kModeWidth, kStrategyWidth, kShapeWidth, kShapeWidth, kShapeWidth, kGroupsWidth, - kSmallWidth, kSmallWidth, kPWidth, kSmallWidth, kSmallWidth, kIm2colWidth, kIm2colWidth, - kChunkWidth, kSmallWidth, kSmallWidth, kFlagWidth, kFlagWidth, kReasonWidth, - }; - const std::string divider = makeDivider(widths); - std::vector overflows; - uint64_t nextPlaceholderId = 1; - - auto printRow = [&](ArrayRef cells) { - reportFile << "|"; - for (size_t i = 0; i < cells.size(); ++i) - reportFile << " " << cells[i] << " |"; - reportFile << "\n"; - }; - - reportFile << divider << "\n"; - printRow({ - alignCell("id", kIdWidth, true), - alignCell("where", kWhereWidth), - alignCell("mode", kModeWidth), - alignCell("strategy", kStrategyWidth), - alignCell("input", kShapeWidth), - alignCell("weight", kShapeWidth), - alignCell("output", kShapeWidth), - alignCell("grp", kGroupsWidth, true), - alignCell("K", kSmallWidth, true), - alignCell("C", kSmallWidth, true), - alignCell("P", kPWidth, true), - alignCell("X", kSmallWidth, true), - alignCell("pack", kSmallWidth, true), - alignCell("im2col", kIm2colWidth, true), - alignCell("budget", kIm2colWidth, true), - alignCell("chunk", kChunkWidth, true), - alignCell("batch", kSmallWidth, true), - alignCell("nbat", kSmallWidth, true), - alignCell("scb", kFlagWidth), - alignCell("bie", kFlagWidth), - alignCell("reason", kReasonWidth), - }); - reportFile << divider << "\n"; - - for (const ConvReportEntry& entry : entries) { - printRow({ - alignCell(std::to_string(entry.id), kIdWidth, true), - fitConvReportCell(entry.where, kWhereWidth, entry.id, "where", overflows, nextPlaceholderId), - alignCell(entry.mode, kModeWidth), - fitConvReportCell(entry.strategy, kStrategyWidth, entry.id, "strategy", overflows, nextPlaceholderId), - fitConvReportCell(entry.inputShape, kShapeWidth, entry.id, "input", overflows, nextPlaceholderId), - fitConvReportCell(entry.weightShape, kShapeWidth, entry.id, "weight", overflows, nextPlaceholderId), - fitConvReportCell(entry.outputShape, kShapeWidth, entry.id, "output", overflows, nextPlaceholderId), - alignCell(std::to_string(entry.groups), kGroupsWidth, true), - alignCell(std::to_string(entry.k), kSmallWidth, true), - alignCell(std::to_string(entry.c), kSmallWidth, true), - alignCell(std::to_string(entry.p), kPWidth, true), - alignCell(std::to_string(entry.xbarSize), kSmallWidth, true), - alignCell(std::to_string(entry.pack), kSmallWidth, true), - alignCell(std::to_string(entry.im2colElements), kIm2colWidth, true), - alignCell(std::to_string(entry.im2colBudget), kIm2colWidth, true), - fitConvReportCell(entry.chunkText, kChunkWidth, entry.id, "stream_chunk", overflows, nextPlaceholderId, true), - alignCell(std::to_string(entry.batchSize), kSmallWidth, true), - alignCell(std::to_string(entry.numberOfBatches), kSmallWidth, true), - alignCell(entry.spatialComputeBatch, kFlagWidth), - alignCell(entry.batchedInstructionEmission, kFlagWidth), - fitConvReportCell(entry.reason, kReasonWidth, entry.id, "reason", overflows, nextPlaceholderId), - }); - } - reportFile << divider << "\n"; - - if (overflows.empty()) - reportFile << "\n"; - else { - reportFile << "\nAppendix:\n"; - for (const ConvReportOverflow& overflow : overflows) { - reportFile << " [" << overflow.placeholderId << "] rows "; - for (size_t i = 0; i < overflow.rowIds.size(); ++i) { - if (i != 0) - reportFile << ", "; - reportFile << overflow.rowIds[i]; - } - reportFile << ", " << overflow.column << ": " << overflow.value << "\n"; - } - reportFile << "\n"; - } - - reportFile << "Per-Conv Details:\n"; - for (const ConvReportEntry& entry : entries) { - reportFile << "- Conv " << entry.id << ": mode=" << entry.mode << ", strategy=" << entry.strategy - << ", reason=" << entry.reason << "\n"; - reportFile << " K=" << entry.k << ", Cout=" << entry.c << ", output_positions=" << entry.p - << ", estimated_mvm_count=" << entry.estimatedMvmCount - << ", estimated_reduction_vadd_count=" << entry.estimatedReductionVAddCount - << ", estimated_output_fragments=" << entry.estimatedOutputFragments << "\n"; - reportFile << " materialization_required_at_func_return=" << entry.materializationRequiredAtReturn - << ", materialization_kind=" << entry.materializationKind - << ", giant_collector_concat_expected=" << entry.giantCollectorConcatExpected - << ", concat_operand_count=" << entry.concatOperandCount - << ", collector_core=" << entry.collectorCore - << ", full_input_broadcast_expected=" << entry.fullInputBroadcastExpected << "\n"; - if (!entry.rejectedAutoStrategy.empty()) - reportFile << " rejected_auto_strategy=" << entry.rejectedAutoStrategy << "\n"; - if (!entry.fallbackReason.empty()) - reportFile << " fallback_reason=" << entry.fallbackReason << "\n"; - } - reportFile << "\n"; - - llvm::SmallVector usedStrategies; - for (const ConvReportEntry& entry : entries) { - PimConvLoweringType strategy = PimConvLoweringAuto; - for (PimConvLoweringType candidate : { - PimConvLoweringAuto, - PimConvLoweringLegacy, - PimConvLoweringDepthwise, - PimConvLoweringPackedIm2Col, - PimConvLoweringStreamedPatch, - PimConvLoweringStreamedPacked, - PimConvLoweringOutputChannelTiled, - PimConvLoweringInputKTiled, - PimConvLoweringTiled2D, - }) { - if (entry.strategy == stringifyConvLoweringStrategy(candidate)) { - strategy = candidate; - break; - } - } - if (llvm::find(usedStrategies, strategy) == usedStrategies.end()) - usedStrategies.push_back(strategy); - } - - reportFile << "Strategies used in this report:\n"; - for (PimConvLoweringType strategy : usedStrategies) - reportFile << "- `" << stringifyConvLoweringStrategy(strategy).str() << "`: " - << describeConvLoweringStrategy(strategy).str() << "\n"; -} - -static void rewriteConvLoweringReport(ArrayRef entries) { - std::fstream reportFile = openReportFile("conv_lowering_report"); - if (!reportFile.is_open()) - return; - printConvReportLegend(reportFile); - writeConvReportTable(reportFile, entries); -} - -[[maybe_unused]] static FailureOr resolveRequestedConvLoweringStrategy(ONNXConvOp convOp) { - if (!useExperimentalConvImpl) - return pimConvLowering.getValue(); - - if (pimConvLowering != PimConvLoweringAuto && pimConvLowering != PimConvLoweringPackedIm2Col) { - convOp.emitOpError() << "--use-experimental-conv-impl conflicts with --pim-conv-lowering=" - << stringifyConvLoweringStrategy(pimConvLowering); - return failure(); - } - return PimConvLoweringPackedIm2Col; -} - -static ConvLoweringDecision chooseConvLoweringStrategy(const ConvGeometry& geo, - PimConvLoweringType requested, - const DistributedConvAnalysis& analysis) { +static PimConvLoweringType chooseConvLoweringStrategy(const ConvGeometry& geo, + PimConvLoweringType requested) { if (requested != PimConvLoweringAuto) - return {requested, "forced by compiler option", /*isAuto=*/false, "", ""}; + return requested; // Transform-based convolution is intentionally not selected for this ISA: // it would require explicit transform sequences and staging traffic on top of // the same crossbar MVM primitive, which is not attractive here. if (geo.isDepthwise) - return {PimConvLoweringDepthwise, "depthwise convolution", /*isAuto=*/true, "", ""}; + return PimConvLoweringDepthwise; if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize && geo.pack >= 2 && geo.im2colElements <= pimConvIm2colMaxElements) - return {PimConvLoweringPackedIm2Col, - "fits crossbar, packing useful, and global im2col fits budget", - /*isAuto=*/true, - "", - ""}; + return PimConvLoweringPackedIm2Col; if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize && geo.pack >= 2 && geo.im2colElements > pimConvIm2colMaxElements) - return {PimConvLoweringStreamedPacked, - "fits crossbar and packing useful, but global im2col exceeds budget", - /*isAuto=*/true, - "", - ""}; + return PimConvLoweringStreamedPacked; if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize) - return {PimConvLoweringStreamedPatch, "fits crossbar but packing is not useful", /*isAuto=*/true, "", ""}; + return PimConvLoweringStreamedPatch; if (geo.k <= geo.xbarSize && geo.c > geo.xbarSize) - return {PimConvLoweringOutputChannelTiled, - "output channels exceed one crossbar width", - /*isAuto=*/true, - "", - ""}; - if (geo.k > geo.xbarSize && geo.c <= geo.xbarSize) { - ConvStrategyEstimate estimate = estimateConvStrategy(geo, PimConvLoweringInputKTiled, analysis); - std::string fallbackReason = "auto rejects input-k-tiled because the reduction-heavy path is force-only for now"; - if (estimate.requiresFuncReturnMaterialization && estimate.perOutputPositionReduction - && estimate.giantCollectorConcatExpected) { - fallbackReason += "; func.return would materialize " + std::to_string(estimate.concatOperandCount) - + " output fragments through a single collector concat after per-position reductions"; - } - if (estimate.fullInputBroadcastExpected) - fallbackReason += "; the current lowering also broadcasts the padded input to many workers"; - return {PimConvLoweringLegacy, - "fall back to legacy explicit-im2col for the current auto policy", - /*isAuto=*/true, - fallbackReason, - stringifyConvLoweringStrategy(PimConvLoweringInputKTiled).str()}; - } - return {PimConvLoweringTiled2D, "both reduction K and output channels exceed one crossbar", /*isAuto=*/true, "", ""}; + return PimConvLoweringOutputChannelTiled; + if (geo.k > geo.xbarSize && geo.c <= geo.xbarSize) + return PimConvLoweringLegacy; + return PimConvLoweringTiled2D; } -[[maybe_unused]] static LogicalResult verifyForcedConvLoweringStrategy(ONNXConvOp convOp, - const ConvGeometry& geo, - PimConvLoweringType strategy) { - switch (strategy) { - case PimConvLoweringAuto: - case PimConvLoweringLegacy: - return success(); - case PimConvLoweringDepthwise: - if (geo.isDepthwise) - return success(); - return convOp.emitOpError("forced depthwise Conv lowering requires a depthwise convolution"); - case PimConvLoweringPackedIm2Col: - if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize && geo.pack >= 2 && geo.im2colElements <= pimConvIm2colMaxElements) - return success(); - return convOp.emitOpError("forced packed-im2col Conv lowering requires K/C to fit, pack >= 2, and im2col within budget"); - case PimConvLoweringStreamedPatch: - if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize) - return success(); - return convOp.emitOpError("forced streamed-patch Conv lowering requires K and C to each fit one crossbar"); - case PimConvLoweringStreamedPacked: - if (geo.k <= geo.xbarSize && geo.c <= geo.xbarSize && geo.pack >= 2) - return success(); - return convOp.emitOpError("forced streamed-packed Conv lowering requires K/C to fit and pack >= 2"); - case PimConvLoweringOutputChannelTiled: - if (geo.k <= geo.xbarSize && geo.c > geo.xbarSize) - return success(); - return convOp.emitOpError("forced output-channel-tiled Conv lowering requires K <= X and C > X"); - case PimConvLoweringInputKTiled: - if (geo.k > geo.xbarSize && geo.c <= geo.xbarSize) - return success(); - return convOp.emitOpError("forced input-k-tiled Conv lowering requires K > X and C <= X"); - case PimConvLoweringTiled2D: - if (geo.k > geo.xbarSize && geo.c > geo.xbarSize) - return success(); - return convOp.emitOpError("forced tiled-2d Conv lowering requires K > X and C > X"); - } - llvm_unreachable("unknown conv lowering strategy"); -} - -static void reportConvLoweringDecision(ONNXConvOp convOp, - const ConvGeometry& geo, - const ConvLoweringDecision& decision, - const ConvStrategyEstimate& estimate, - int64_t batchSize, - int64_t numberOfBatches, - bool usesComputeBatch, - bool usesBatchedInstructionEmission, - std::optional streamChunkPositions = std::nullopt) { - if (!pimReportConvLowering) - return; - - const std::string location = summarizeLocation(convOp.getLoc()); - const std::string strategy = stringifyConvLoweringStrategy(decision.strategy).str(); - const std::string mode = decision.isAuto ? "auto" : "forced"; - const std::string inputShape = formatShape(cast(convOp.getX().getType()).getShape()); - const std::string weightShape = formatShape(cast(convOp.getW().getType()).getShape()); - const std::string outputShape = formatShape(cast(convOp.getY().getType()).getShape()); - const std::string chunkText = streamChunkPositions ? std::to_string(*streamChunkPositions) : "-"; - const std::string scbText = usesComputeBatch ? "yes" : "no"; - const std::string bieText = usesBatchedInstructionEmission ? "yes" : "no"; - - static uint64_t reportIndex = 0; - const uint64_t currentIndex = ++reportIndex; - static std::mutex reportMutex; - static std::vector reportEntries; - std::lock_guard lock(reportMutex); - reportEntries.push_back({ - currentIndex, - location, - strategy, - mode, - inputShape, - weightShape, - outputShape, - geo.group, - geo.k, - geo.c, - geo.p, - geo.xbarSize, - geo.pack, - geo.im2colElements, - pimConvIm2colMaxElements, - chunkText, - batchSize, - numberOfBatches, - scbText, - bieText, - decision.reason, - decision.fallbackReason, - decision.rejectedAutoStrategy, - estimate.estimatedMvmCount, - estimate.estimatedReductionVAddCount, - estimate.estimatedOutputFragments, - estimate.requiresFuncReturnMaterialization ? "yes" : "no", - estimate.materializationKind, - estimate.concatOperandCount, - estimate.collectorCore, - estimate.giantCollectorConcatExpected ? "yes" : "no", - estimate.fullInputBroadcastExpected ? "yes" : "no", - }); - rewriteConvLoweringReport(reportEntries); -} static Value expandBiasIfNeeded(Value bias, PatternRewriter& rewriter, Location loc) { auto biasType = cast(bias.getType()); @@ -1184,7 +192,6 @@ static Value createCollectedConvOutput(ValueRange gemmRows, int64_t numPatches, int64_t numChannelsOut, int64_t packFactor, - ArrayRef distributedConsumers, PatternRewriter& rewriter, Location loc); static FailureOr analyzeConvLoweringState(ONNXConvOp convOp, Value x, Value w, Value b); @@ -1241,10 +248,12 @@ static Value buildPackedWeights(DenseElementsAttr wDenseAttr, RankedTensorType wType, const Tiling& tiling, PatternRewriter& rewriter, - Location loc) { + Location loc, + int64_t paddedInputRows = -1) { const int64_t paddedOutputChannels = static_cast(crossbarSize.getValue()); + const int64_t packedInputRows = paddedInputRows > 0 ? paddedInputRows : tiling.tileInputRows; auto packedWeightType = RankedTensorType::get( - {tiling.numChannelTiles, tiling.tileInputRows, paddedOutputChannels}, wType.getElementType()); + {tiling.numChannelTiles, packedInputRows, paddedOutputChannels}, wType.getElementType()); SmallVector packedValues(packedWeightType.getNumElements(), cast(rewriter.getZeroAttr(wType.getElementType()))); SmallVector sourceValues(wDenseAttr.getValues()); @@ -1263,7 +272,7 @@ static Value buildPackedWeights(DenseElementsAttr wDenseAttr, ((globalOutChannel * wType.getDimSize(1) * wType.getDimSize(2)) + kernelH) * wType.getDimSize(3) + kernelW; const int64_t targetCol = localChannel * tiling.outputMultiplier + multiplierIndex; const int64_t targetFlatIndex = - ((tileIndex * tiling.tileInputRows) + targetRow) * paddedOutputChannels + targetCol; + ((tileIndex * packedInputRows) + targetRow) * paddedOutputChannels + targetCol; packedValues[targetFlatIndex] = sourceValues[sourceFlatIndex]; } } @@ -1703,7 +712,6 @@ rewriteConv(Operation* convOp, const ConvLoweringState& state, PatternRewriter& tiling->totalPatches, state.outType.getDimSize(1), /*packFactor=*/1, - {}, rewriter, loc); } @@ -1952,7 +960,6 @@ static Value createPaddedOutputChannelTiledWeightConstant(DenseElementsAttr sour } static FailureOr rewriteInputKTiledConv(const ConvLoweringState& state, - ArrayRef distributedConsumers, PatternRewriter& rewriter, Location loc) { PreparedConvInput preparedInput = prepareInputForIm2Col(state, rewriter, loc); @@ -2139,7 +1146,7 @@ static FailureOr rewriteInputKTiledConv(const ConvLoweringState& state, elementType); return createCollectedConvOutput( chunkRows, state.outType, cast(chunkRows.front().getType()), nhwcType, state.outType, totalPatches, - state.numChannelsOut, /*packFactor=*/1, distributedConsumers, rewriter, loc); + state.numChannelsOut, /*packFactor=*/1, rewriter, loc); } static Value buildPackedWeights(DenseElementsAttr wDenseAttr, @@ -2434,7 +1441,6 @@ static Value createStreamedConvRows(const ConvLoweringState& state, } static Value rewritePackedIm2ColConv(const ConvLoweringState& state, - ArrayRef distributedConsumers, PatternRewriter& rewriter, Location loc) { auto wDenseAttr = getHostConstDenseElementsAttr(state.w); @@ -2480,13 +1486,11 @@ static Value rewritePackedIm2ColConv(const ConvLoweringState& state, plan.chunkNumPatches, state.numChannelsOut, plan.effectiveMaxParallelPixels, - distributedConsumers, rewriter, loc); } static Value rewriteStreamedConv(const ConvLoweringState& state, - ArrayRef distributedConsumers, PatternRewriter& rewriter, Location loc, int64_t forcedPackFactor) { @@ -2516,38 +1520,11 @@ static Value rewriteStreamedConv(const ConvLoweringState& state, state.outType.getElementType()); return createCollectedConvOutput( ValueRange {collectedRows}, state.outType, gemmOutType, nhwcType, state.outType, gemmOutType.getDimSize(0), - state.numChannelsOut, /*packFactor=*/1, distributedConsumers, rewriter, loc); + state.numChannelsOut, /*packFactor=*/1, rewriter, loc); } } // namespace standard -static SmallVector buildRowStripFragments(RankedTensorType tensorType) { - SmallVector fragments; - auto [offsets, sizes] = buildRowStripMetadata(tensorType); - const int64_t rank = tensorType.getRank(); - fragments.reserve(offsets.size() / rank); - for (int64_t row = 0; row < static_cast(offsets.size() / rank); ++row) { - fragments.push_back(DistributedFragmentInfo { - {offsets.begin() + row * rank, offsets.begin() + (row + 1) * rank}, - {sizes.begin() + row * rank, sizes.begin() + (row + 1) * rank}, - {1, 1, 1, 1}, - row, - }); - } - return fragments; -} - -static DistributedTensorInfo makeDistributedTensorInfo(Value storage, RankedTensorType logicalType) { - DistributedTensorInfo info; - info.storage = storage; - info.logicalType = logicalType; - info.fragments = buildRowStripFragments(logicalType); - info.laneCount = logicalType.getDimSize(2); - info.channels = logicalType.getDimSize(1); - info.height = logicalType.getDimSize(2); - info.width = logicalType.getDimSize(3); - return info; -} static Value createZeroGemmBias(RankedTensorType gemmResultType, PatternRewriter& rewriter) { auto zeroAttr = DenseElementsAttr::get(gemmResultType, rewriter.getZeroAttr(gemmResultType.getElementType())); @@ -2817,55 +1794,50 @@ static Value extractDenseConvWindowRow(Value denseInput, rewriter, loc, fragmentType, nchw, rewriter.getI64ArrayAttr({0, 2, 3, 1})); } -static FailureOr createRowStripWindowMaskTable(const ConvLoweringState& state, PatternRewriter& rewriter) { - auto elementType = state.xType.getElementType(); - auto floatType = dyn_cast(elementType); - if (!floatType) - return failure(); - +static Value createRowStripWindowMaskTable(const ConvLoweringState& state, PatternRewriter& rewriter) { + auto elementType = cast(state.xType.getElementType()); Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); - auto tableType = RankedTensorType::get({state.outHeight * state.wHeight, 1, state.xWidth, state.numChannelsIn}, - elementType, - state.xType.getEncoding()); - Attribute zero = rewriter.getZeroAttr(elementType); - Attribute one = rewriter.getFloatAttr(floatType, 1.0); + auto tableType = RankedTensorType::get( + {2, 1, state.xWidth, state.numChannelsIn}, elementType, state.xType.getEncoding()); + SmallVector values(tableType.getNumElements(), rewriter.getZeroAttr(elementType)); + std::fill(values.begin() + tableType.getNumElements() / 2, values.end(), rewriter.getFloatAttr(elementType, 1.0)); + return getOrCreateConstant(rewriter, anchorOp, DenseElementsAttr::get(tableType, values), tableType); +} + +static Value createRowStripWindowMaskIndexTable(const ConvLoweringState& state, PatternRewriter& rewriter) { + Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); + auto tableType = RankedTensorType::get({state.outHeight * state.wHeight}, rewriter.getIndexType()); SmallVector values; values.reserve(tableType.getNumElements()); - for (int64_t outputRow = 0; outputRow < state.outHeight; ++outputRow) { + for (int64_t outputRow = 0; outputRow < state.outHeight; ++outputRow) for (int64_t kernelRow = 0; kernelRow < state.wHeight; ++kernelRow) { int64_t sourceRow = outputRow * state.strideHeight + kernelRow * state.dilationHeight - state.padHeightBegin; - Attribute value = (sourceRow < 0 || sourceRow >= state.xHeight) ? zero : one; - for (int64_t width = 0; width < state.xWidth; ++width) - for (int64_t channel = 0; channel < state.numChannelsIn; ++channel) - values.push_back(value); + values.push_back(rewriter.getIndexAttr(sourceRow >= 0 && sourceRow < state.xHeight)); } - } - return getOrCreateConstant(rewriter, anchorOp, DenseElementsAttr::get(tableType, values), tableType); } static Value extractProjectedRowStripWindowMask(Value maskTable, + Value maskIndexTable, const ConvLoweringState& state, Value outputHeight, Value kernelRow, PatternRewriter& rewriter, Location loc) { Value tableIndex = createRowStripWindowTableIndex(outputHeight, kernelRow, state, rewriter, loc); + Value maskIndex = tensor::ExtractOp::create(rewriter, loc, maskIndexTable, ValueRange {tableIndex}).getResult(); auto fragmentType = getRowStripFragmentType(state.xType); - SmallVector offsets { - tableIndex, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; - SmallVector sizes {rewriter.getIndexAttr(1), - rewriter.getIndexAttr(1), - rewriter.getIndexAttr(state.xWidth), - rewriter.getIndexAttr(state.numChannelsIn)}; - return tensor::ExtractSliceOp::create(rewriter, - loc, - fragmentType, - maskTable, - offsets, - sizes, - getUnitStrides(rewriter, 4)); + return tensor::ExtractSliceOp::create( + rewriter, + loc, + fragmentType, + maskTable, + SmallVector { + maskIndex, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(state.xWidth), rewriter.getIndexAttr(state.numChannelsIn)}, + getUnitStrides(rewriter, 4)); } static FailureOr createConvInputWindow(Value input, @@ -2876,20 +1848,23 @@ static FailureOr createConvInputWindow(Value input, auto fragmentType = getRowStripFragmentType(state.xType); auto inputType = dyn_cast(input.getType()); const bool denseInput = inputType == state.xType; - if (!denseInput && failed(describeRowStripPhysicalValue(input, state.xType))) + FailureOr physicalInput = describeRowStripPhysicalValue(input, state.xType); + if (!denseInput && failed(physicalInput)) return failure(); + if (!denseInput && physicalInput->tilesPerRow == 1 && state.wHeight == 1 && state.wWidth == 1 + && state.strideHeight == 1 && state.strideWidth == 1 && state.padHeightBegin == 0 + && state.padHeightEnd == 0 && state.padWidthBegin == 0 && state.padWidthEnd == 0) + return extractGraphBatchPhysicalFragment( + rewriter, loc, input, outputHeight, physicalInput->fragmentType); auto paddedWindowType = RankedTensorType::get( {1, state.wHeight, state.xWidth + state.padWidthBegin + state.padWidthEnd, state.numChannelsIn}, state.xType.getElementType(), state.xType.getEncoding()); - FailureOr physicalInput = - denseInput ? FailureOr(failure()) : describeRowStripPhysicalValue(input, state.xType); Value sourceIndexTable = denseInput ? createRowStripWindowSourceRowTable(state, rewriter) : createRowStripWindowSourceSlotTable(state, physicalInput->tilesPerRow, rewriter); - FailureOr maskTable = createRowStripWindowMaskTable(state, rewriter); - if (failed(maskTable)) - return failure(); + Value maskTable = createRowStripWindowMaskTable(state, rewriter); + Value maskIndexTable = createRowStripWindowMaskIndexTable(state, rewriter); Value initWindow = createZeroTensorConstant(paddedWindowType, rewriter); Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); @@ -2914,7 +1889,8 @@ static FailureOr createConvInputWindow(Value input, return failure(); Value semanticRow = *sourceRow; if (state.padHeightBegin != 0 || state.padHeightEnd != 0) { - Value mask = extractProjectedRowStripWindowMask(*maskTable, state, outputHeight, kernelRow, rewriter, rowLoc); + Value mask = extractProjectedRowStripWindowMask( + maskTable, maskIndexTable, state, outputHeight, kernelRow, rewriter, rowLoc); semanticRow = spatial::SpatVMulOp::create(rewriter, rowLoc, fragmentType, semanticRow, mask).getResult(); } Value paddedRow = createHorizontallyPaddedRowStripFragment(semanticRow, state, rewriter, rowLoc); @@ -3632,126 +2608,269 @@ static FailureOr createPointwiseOutputFromRowStripFragments(Value rowStri return batch->getResult(0); } +static bool canConsumeDepthwiseRowStrip(const ConvLoweringState& state) { + if (state.batchSize != 1 || state.group != state.numChannelsIn + || state.dilationHeight != 1 || state.dilationWidth != 1 + || !isa(state.xType.getElementType()) + || !getHostConstDenseElementsAttr(state.w) + || (state.hasBias && !isSupportedBiasAddValue(state.b, state.outType))) + return false; + auto tiling = depthwise::computeTiling(state.batchSize, + state.numChannelsIn, + state.numChannelsOut, + state.wHeight, + state.wWidth, + state.outHeight, + state.outWidth); + return tiling && tiling->numChannelTiles <= static_cast(crossbarCountInCore.getValue()); +} + +static Value insertDepthwiseInputSegment(Value inputWindow, + Value scratch, + Value tileIndex, + Value kernelRow, + Value sourceWidth, + Value scratchOffset, + int64_t inputChannel, + const depthwise::Tiling& tiling, + PatternRewriter& rewriter, + Location loc) { + auto inputWindowType = cast(inputWindow.getType()); + auto inputPixelType = RankedTensorType::get( + {1, 1, 1, tiling.channelsPerTile}, inputWindowType.getElementType(), inputWindowType.getEncoding()); + Value inputPixel = tensor::ExtractSliceOp::create( + rewriter, + loc, + inputPixelType, + inputWindow, + SmallVector {rewriter.getIndexAttr(0), kernelRow, sourceWidth, + rewriter.getIndexAttr(inputChannel)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(1), rewriter.getIndexAttr(tiling.channelsPerTile)}, + getUnitStrides(rewriter, 4)); + return tensor::InsertSliceOp::create( + rewriter, + loc, + inputPixel, + scratch, + SmallVector {tileIndex, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), scratchOffset}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(tiling.channelsPerTile)}, + getUnitStrides(rewriter, 4)); +} + +static FailureOr assembleDepthwiseInputScratch(Value inputWindow, + Value scratch, + Value inputWidth, + const ConvLoweringState& state, + const depthwise::Tiling& tiling, + PatternRewriter& rewriter, + Location loc) { + Operation* anchor = rewriter.getInsertionBlock()->getParentOp(); + Value c0 = getOrCreateIndexConstant(rewriter, anchor, 0); + Value c1 = getOrCreateIndexConstant(rewriter, anchor, 1); + Value cKernelElements = getOrCreateIndexConstant(rewriter, anchor, tiling.kernelElements); + SmallVector tileIndices; + tileIndices.reserve(tiling.numChannelTiles); + for (int64_t tile = 0; tile < tiling.numChannelTiles; ++tile) + tileIndices.push_back(getOrCreateIndexConstant(rewriter, anchor, tile)); + auto kernelLoop = buildNormalizedScfFor( + rewriter, + loc, + c0, + cKernelElements, + c1, + ValueRange {scratch}, + [&](OpBuilder&, Location kernelLoc, Value kernelIndex, ValueRange iterArgs, + SmallVectorImpl& yielded) { + Value kernelRow = affineFloorDivConst(rewriter, kernelLoc, kernelIndex, state.wWidth, anchor); + Value kernelColumn = affineModConst(rewriter, kernelLoc, kernelIndex, state.wWidth, anchor); + Value sourceWidth = createOrFoldAffineApply( + rewriter, + kernelLoc, + getAffineDimExpr(0, rewriter.getContext()) + getAffineDimExpr(1, rewriter.getContext()), + ValueRange {inputWidth, kernelColumn}, + anchor); + Value scratchOffset = affineMulConst( + rewriter, kernelLoc, kernelIndex, tiling.channelsPerTile, anchor); + Value nextScratch = iterArgs.front(); + for (int64_t tile = 0; tile < tiling.numChannelTiles; ++tile) + nextScratch = insertDepthwiseInputSegment(inputWindow, + nextScratch, + tileIndices[tile], + kernelRow, + sourceWidth, + scratchOffset, + tile * tiling.channelsPerTile, + tiling, + rewriter, + kernelLoc); + yielded.push_back(nextScratch); + return success(); + }); + if (failed(kernelLoop)) + return failure(); + return kernelLoop->results.front(); +} + +static FailureOr createDepthwiseOutputFromRowStripFragments(Value rowStripStorage, + const ConvLoweringState& state, + PatternRewriter& rewriter, + Location loc) { + if (!canConsumeDepthwiseRowStrip(state) + || failed(describeRowStripPhysicalValue(rowStripStorage, state.xType))) + return failure(); + auto tiling = depthwise::computeTiling(state.batchSize, + state.numChannelsIn, + state.numChannelsOut, + state.wHeight, + state.wWidth, + state.outHeight, + state.outWidth); + auto weight = getHostConstDenseElementsAttr(state.w); + if (!tiling || !weight) + return failure(); + + Value packedWeights = depthwise::buildPackedWeights( + weight, state.wType, *tiling, rewriter, loc, static_cast(crossbarSize.getValue())); + Value bias = state.hasBias ? expandBiasIfNeeded(state.b, rewriter, loc) : Value(); + auto paddedOutputType = RankedTensorType::get( + {1, static_cast(crossbarSize.getValue())}, state.outType.getElementType()); + auto outputTileType = RankedTensorType::get( + {1, tiling->tileOutputChannels}, state.outType.getElementType()); + auto outputPixelType = RankedTensorType::get( + {1, 1, 1, tiling->tileOutputChannels}, state.outType.getElementType()); + auto fragmentType = getRowStripFragmentType(state.outType); + auto storageType = getRowStripStorageType(state.outType); + + auto batch = createSpatComputeBatch( + rewriter, + loc, + TypeRange {storageType}, + state.outHeight, + ValueRange {packedWeights}, + state.hasBias ? ValueRange {rowStripStorage, bias} : ValueRange {rowStripStorage}, + [&](detail::SpatComputeBatchBodyArgs args) { + Operation* anchor = rewriter.getInsertionBlock()->getParentOp(); + FailureOr inputWindow = + createConvInputWindow(args.inputs.front(), state, args.lane, rewriter, loc); + if (failed(inputWindow)) + return failure(); + Value c0 = getOrCreateIndexConstant(rewriter, anchor, 0); + Value c1 = getOrCreateIndexConstant(rewriter, anchor, 1); + Value cOutWidth = getOrCreateIndexConstant(rewriter, anchor, state.outWidth); + const int64_t xbarDim = static_cast(crossbarSize.getValue()); + auto paddedInputScratchType = RankedTensorType::get( + {tiling->numChannelTiles, 1, 1, xbarDim}, state.xType.getElementType(), state.xType.getEncoding()); + auto tileScratchType = RankedTensorType::get( + {1, 1, 1, xbarDim}, state.xType.getElementType(), state.xType.getEncoding()); + auto vmmInputType = RankedTensorType::get( + {1, xbarDim}, state.xType.getElementType(), state.xType.getEncoding()); + Value zeroScratch = createZeroTensorConstant(paddedInputScratchType, rewriter); + Value fragment = tensor::EmptyOp::create( + rewriter, loc, fragmentType.getShape(), fragmentType.getElementType()); + SmallVector weightTiles; + SmallVector biasTiles; + SmallVector tileIndices; + SmallVector weightTileSizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(xbarDim), rewriter.getIndexAttr(xbarDim)}; + for (int64_t tile = 0; tile < tiling->numChannelTiles; ++tile) { + Value tileIndex = getOrCreateIndexConstant(rewriter, anchor, tile); + tileIndices.push_back(tileIndex); + weightTiles.push_back(extractMixedSliceOrIdentity( + rewriter, + loc, + args.weights.front(), + RankedTensorType::get({xbarDim, xbarDim}, state.wType.getElementType()), + {SmallVector {tileIndex, rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}, + weightTileSizes, + getUnitStrides(rewriter, 3)})); + if (state.hasBias) + biasTiles.push_back(depthwise::createBiasTile(args.inputs[1], tileIndex, *tiling, rewriter, loc)); + } + auto widthLoop = buildNormalizedScfFor( + rewriter, loc, c0, cOutWidth, c1, ValueRange {fragment, zeroScratch}, + [&](OpBuilder&, Location widthLoc, Value width, ValueRange iterArgs, + SmallVectorImpl& yielded) { + Value next = iterArgs.front(); + Value scratch = iterArgs[1]; + // Valid prefix entries are overwritten per tile; the padded tail stays zero. + Value inputWidth = affineMulConst( + rewriter, widthLoc, width, state.strideWidth, anchor); + FailureOr nextScratch = assembleDepthwiseInputScratch( + *inputWindow, scratch, inputWidth, state, *tiling, rewriter, widthLoc); + if (failed(nextScratch)) + return failure(); + scratch = *nextScratch; + for (int64_t tile = 0; tile < tiling->numChannelTiles; ++tile) { + Value tileScratch = tensor::ExtractSliceOp::create( + rewriter, + widthLoc, + tileScratchType, + scratch, + SmallVector {tileIndices[tile], rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), + rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(1), rewriter.getIndexAttr(xbarDim)}, + getUnitStrides(rewriter, 4)); + Value vmmInput = tensor::CollapseShapeOp::create( + rewriter, + widthLoc, + vmmInputType, + tileScratch, + SmallVector {{0, 1, 2}, {3}}); + Value output = spatial::SpatVMMOp::create( + rewriter, widthLoc, paddedOutputType, weightTiles[tile], vmmInput); + Value validOutput = tensor::ExtractSliceOp::create( + rewriter, + widthLoc, + outputTileType, + output, + SmallVector {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), + rewriter.getIndexAttr(tiling->tileOutputChannels)}, + getUnitStrides(rewriter, 2)); + if (state.hasBias) + validOutput = spatial::SpatVAddOp::create( + rewriter, widthLoc, outputTileType, validOutput, biasTiles[tile]); + Value pixel = tensor::ExpandShapeOp::create( + rewriter, widthLoc, outputPixelType, validOutput, + SmallVector {{0, 1, 2}, {3}}); + next = tensor::InsertSliceOp::create( + rewriter, + widthLoc, + pixel, + next, + SmallVector {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), width, + rewriter.getIndexAttr(tile * tiling->tileOutputChannels)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(1), rewriter.getIndexAttr(tiling->tileOutputChannels)}, + getUnitStrides(rewriter, 4)); + } + yielded.push_back(next); + yielded.push_back(scratch); + return success(); + }); + if (failed(widthLoop)) + return failure(); + publishGraphBatchPhysicalFragment(rewriter, loc, widthLoop->results.front(), args.outputs.front(), args.lane); + return success(); + }); + return failed(batch) ? FailureOr(failure()) : FailureOr(batch->getResult(0)); +} + static FailureOr createConvOutputFromRowStripInput(const ConvLoweringState& state, - [[maybe_unused]] const ConvLoweringDecision& decision, Value rowStripInput, + PimConvLoweringType strategy, PatternRewriter& rewriter, Location loc) { + if (strategy == PimConvLoweringDepthwise) + return createDepthwiseOutputFromRowStripFragments(rowStripInput, state, rewriter, loc); if (state.xHeight == 1 && state.xWidth == 1 && state.wHeight == 1 && state.wWidth == 1) return createPointwiseOutputFromRowStripFragments(rowStripInput, state, rewriter, loc); return createConvOutputFromPixelMajorRowStripFragments(rowStripInput, state, rewriter, loc); } -static Value createFragmentConstant(const DistributedTensorStep& step, - RankedTensorType fragmentType, - PatternRewriter& rewriter) { - if (step.constantKind == DistributedTensorConstantKind::PerChannel) { - FailureOr constant = createPerChannelConstantFragment(step.constantAttr, fragmentType, rewriter); - assert(succeeded(constant) && "distributed per-channel constants are classified before lowering"); - return *constant; - } - - Attribute splatValue = step.constantAttr.getSplatValue(); - return getOrCreateConstant(rewriter, - rewriter.getInsertionBlock()->getParentOp(), - DenseElementsAttr::get(fragmentType, splatValue), - fragmentType); -} - -static Value createFragmentReciprocalConstant(const DistributedTensorStep& step, - RankedTensorType fragmentType, - PatternRewriter& rewriter) { - SmallVector values; - if (step.constantKind == DistributedTensorConstantKind::PerChannel) { - auto denseType = cast(step.constantAttr.getType()); - SmallVector channelValues; - for (const APFloat& value : step.constantAttr.getValues()) - channelValues.push_back(value); - values.reserve(fragmentType.getNumElements()); - for (int64_t n = 0; n < fragmentType.getDimSize(0); ++n) - for (int64_t h = 0; h < fragmentType.getDimSize(1); ++h) - for (int64_t w = 0; w < fragmentType.getDimSize(2); ++w) - for (int64_t channel = 0; channel < fragmentType.getDimSize(3); ++channel) { - APFloat reciprocal = channelValues[channel]; - APFloat one(reciprocal.getSemantics(), 1); - [[maybe_unused]] APFloat::opStatus status = one.divide(reciprocal, APFloat::rmNearestTiesToEven); - assert(!(status & APFloat::opInvalidOp) && "distributed conv div requires finite non-zero constant"); - values.push_back(one); - } - (void)denseType; - } - else { - APFloat reciprocal = cast(step.constantAttr).getSplatValue(); - APFloat one(reciprocal.getSemantics(), 1); - [[maybe_unused]] APFloat::opStatus status = one.divide(reciprocal, APFloat::rmNearestTiesToEven); - assert(!(status & APFloat::opInvalidOp) && "distributed conv div requires finite non-zero constant"); - values.assign(fragmentType.getNumElements(), one); - } - return getOrCreateConstant(rewriter, - rewriter.getInsertionBlock()->getParentOp(), - DenseFPElementsAttr::get(fragmentType, values), - fragmentType); -} - -[[maybe_unused]] static FailureOr applyDistributedPreservingStep(const DistributedTensorInfo& inputInfo, - const DistributedTensorStep& step, - PatternRewriter& rewriter, - Location loc) { - auto logicalType = inputInfo.logicalType; - auto fragmentType = getRowStripFragmentType(logicalType); - auto storageType = getRowStripStorageType(logicalType); - auto batchOp = createSpatComputeBatch(rewriter, - loc, - TypeRange {storageType}, - inputInfo.laneCount, - {}, - ValueRange {inputInfo.storage}, - [&](detail::SpatComputeBatchBodyArgs args) { - Value fragment = - extractRowStripFragment(args.inputs.front(), logicalType, args.lane, rewriter, loc); - switch (step.kind) { - case DistributedTensorOpKind::Relu: - fragment = spatial::SpatReluOp::create(rewriter, loc, fragmentType, fragment).getResult(); - break; - case DistributedTensorOpKind::Sigmoid: - fragment = spatial::SpatSigmoidOp::create(rewriter, loc, fragmentType, fragment).getResult(); - break; - case DistributedTensorOpKind::Add: { - Value constant = createFragmentConstant(step, fragmentType, rewriter); - fragment = - spatial::SpatVAddOp::create(rewriter, loc, fragmentType, fragment, constant).getResult(); - break; - } - case DistributedTensorOpKind::Sub: { - Value constant = createFragmentConstant(step, fragmentType, rewriter); - Value lhs = step.fragmentOnLhs ? fragment : constant; - Value rhs = step.fragmentOnLhs ? constant : fragment; - fragment = spatial::SpatVSubOp::create(rewriter, loc, fragmentType, lhs, rhs).getResult(); - break; - } - case DistributedTensorOpKind::Mul: { - Value constant = createFragmentConstant(step, fragmentType, rewriter); - fragment = - spatial::SpatVMulOp::create(rewriter, loc, fragmentType, fragment, constant).getResult(); - break; - } - case DistributedTensorOpKind::Div: { - Value constant = createFragmentReciprocalConstant(step, fragmentType, rewriter); - fragment = - spatial::SpatVMulOp::create(rewriter, loc, fragmentType, fragment, constant).getResult(); - break; - } - case DistributedTensorOpKind::Conv: - return failure(); - } - insertRowStripFragment( - fragment, args.outputs.front(), logicalType, args.lane, rewriter, loc); - return success(); - }); - if (failed(batchOp)) - return failure(); - return makeDistributedTensorInfo(batchOp->getResult(0), logicalType); -} static Value createCollectedConvOutput(ValueRange gemmRows, Type convType, @@ -3761,77 +2880,15 @@ static Value createCollectedConvOutput(ValueRange gemmRows, int64_t numPatches, int64_t numChannelsOut, int64_t packFactor, - ArrayRef distributedConsumers, PatternRewriter& rewriter, Location loc) { - auto materializeSplatTensor = [&](DenseElementsAttr denseAttr, RankedTensorType targetType) { - Attribute splatValue = denseAttr.getSplatValue(); - auto targetAttr = DenseElementsAttr::get(targetType, splatValue); - return getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), targetAttr, targetType); - }; - - auto materializeReciprocalSplatTensor = [&](DenseFPElementsAttr denseAttr, RankedTensorType targetType) { - APFloat reciprocal = denseAttr.getSplatValue(); - APFloat one(reciprocal.getSemantics(), 1); - [[maybe_unused]] APFloat::opStatus status = one.divide(reciprocal, APFloat::rmNearestTiesToEven); - assert(!(status & APFloat::opInvalidOp) && "distributed conv div consumer requires finite non-zero scalar"); - return getOrCreateConstant( - rewriter, rewriter.getInsertionBlock()->getParentOp(), DenseFPElementsAttr::get(targetType, one), targetType); - }; - - auto applyDistributedConsumers = [&](Value fragment) { - Value current = fragment; - for (const DistributedTensorStep& step : distributedConsumers) { - auto fragmentType = cast(current.getType()); - switch (step.kind) { - case DistributedTensorOpKind::Relu: - current = spatial::SpatReluOp::create(rewriter, loc, fragmentType, current).getResult(); - break; - case DistributedTensorOpKind::Sigmoid: - current = spatial::SpatSigmoidOp::create(rewriter, loc, fragmentType, current).getResult(); - break; - case DistributedTensorOpKind::Add: { - Value splat = materializeSplatTensor(step.constantAttr, fragmentType); - current = spatial::SpatVAddOp::create(rewriter, loc, fragmentType, current, splat).getResult(); - break; - } - case DistributedTensorOpKind::Sub: { - Value splat = materializeSplatTensor(step.constantAttr, fragmentType); - Value lhs = step.fragmentOnLhs ? current : splat; - Value rhs = step.fragmentOnLhs ? splat : current; - current = spatial::SpatVSubOp::create(rewriter, loc, fragmentType, lhs, rhs).getResult(); - break; - } - case DistributedTensorOpKind::Mul: { - Value splat = materializeSplatTensor(step.constantAttr, fragmentType); - current = spatial::SpatVMulOp::create(rewriter, loc, fragmentType, current, splat).getResult(); - break; - } - case DistributedTensorOpKind::Div: { - auto reciprocalAttr = cast(step.constantAttr); - Value reciprocal = materializeReciprocalSplatTensor(reciprocalAttr, fragmentType); - current = spatial::SpatVMulOp::create(rewriter, loc, fragmentType, current, reciprocal).getResult(); - break; - } - case DistributedTensorOpKind::Conv: - llvm_unreachable("conv-consuming distributed chains should not materialize through createCollectedConvOutput"); - } - } - return current; - }; - auto collectComputeOp = createSpatCompute(rewriter, loc, convType, {}, gemmRows, [&](ValueRange gemmRowArgs) { - SmallVector transformedRows; - transformedRows.reserve(gemmRowArgs.size()); - for (Value row : gemmRowArgs) - transformedRows.push_back(applyDistributedConsumers(row)); - Value gemmOut; if (packFactor == 1) { - gemmOut = createSpatConcat(rewriter, loc, /*axis=*/0, transformedRows); + gemmOut = createSpatConcat(rewriter, loc, /*axis=*/0, gemmRowArgs); } else { - Value packedOutput = createSpatConcat(rewriter, loc, /*axis=*/0, transformedRows); + Value packedOutput = createSpatConcat(rewriter, loc, /*axis=*/0, gemmRowArgs); gemmOut = standard::unpackRowsFromParallelGemm( packedOutput, cast(packedOutput.getType()), numPatches, numChannelsOut, packFactor, rewriter, loc); } @@ -4108,6 +3165,22 @@ static LogicalResult verifyForcedConvLoweringStrategy(Operation* op, llvm_unreachable("unknown conv lowering strategy"); } +static FailureOr selectConvLoweringStrategy(Operation* op, + const ConvLoweringState& state) { + FailureOr requested = resolveRequestedConvLoweringStrategy(op); + if (failed(requested)) + return failure(); + + ConvGeometry geometry = buildConvGeometry(state); + PimConvLoweringType strategy = chooseConvLoweringStrategy(geometry, *requested); + if (strategy == PimConvLoweringDepthwise && !depthwise::canUseStructuredRewrite(state) + && *requested == PimConvLoweringAuto) + strategy = PimConvLoweringLegacy; + if (failed(verifyForcedConvLoweringStrategy(op, geometry, strategy))) + return failure(); + return strategy; +} + static FailureOr lowerDenseSelectedConvPlan(Operation* op, const ConvLoweringState& state, PimConvLoweringType strategy, @@ -4123,15 +3196,13 @@ static ConvLoweringState makeGroupedConvLoweringState(const ConvLoweringState& p static FailureOr buildConvValueForStrategy(Operation* op, Location loc, const ConvLoweringState& state, - const ConvLoweringDecision& decision, - const DistributedConvAnalysis& analysis, - ArrayRef distributedConsumers, + PimConvLoweringType strategy, PatternRewriter& rewriter); static FailureOr buildGroupedConvValue(Operation* op, Location loc, const ConvLoweringState& state, - const ConvLoweringDecision& decision, + PimConvLoweringType strategy, PatternRewriter& rewriter); static FailureOr lowerGroupedSelectedConvPlan(Operation* op, @@ -4139,8 +3210,7 @@ static FailureOr lowerGroupedSelectedConvPlan(Operation* op, PimConvLoweringType strategy, PatternRewriter& rewriter, Location loc) { - ConvLoweringDecision decision {strategy, "", false, "", ""}; - return buildGroupedConvValue(op, loc, state, decision, rewriter); + return buildGroupedConvValue(op, loc, state, strategy, rewriter); } static FailureOr lowerDenseSelectedConvPlan(Operation* op, @@ -4148,40 +3218,33 @@ static FailureOr lowerDenseSelectedConvPlan(Operation* op, PimConvLoweringType strategy, PatternRewriter& rewriter, Location loc) { - DistributedConvAnalysis analysis; - analysis.barrierKind = DistributedConvBarrierKind::UnsupportedConsumer; - analysis.barrierDetail = "selected dense layout"; - ConvLoweringDecision decision {strategy, "", false, "", ""}; - return buildConvValueForStrategy(op, loc, state, decision, analysis, {}, rewriter); + return buildConvValueForStrategy(op, loc, state, strategy, rewriter); } static FailureOr buildConvValueForStrategy(Operation* op, Location loc, const ConvLoweringState& state, - const ConvLoweringDecision& decision, - const DistributedConvAnalysis& analysis, - ArrayRef distributedConsumers, + PimConvLoweringType strategy, PatternRewriter& rewriter) { - (void)analysis; const ConvGeometry geo = buildConvGeometry(state); - switch (decision.strategy) { + switch (strategy) { case PimConvLoweringDepthwise: { return depthwise::rewriteConv(op, state, rewriter, loc); } case PimConvLoweringLegacy: case PimConvLoweringPackedIm2Col: { - return standard::rewritePackedIm2ColConv(state, distributedConsumers, rewriter, loc); + return standard::rewritePackedIm2ColConv(state, rewriter, loc); } case PimConvLoweringStreamedPatch: case PimConvLoweringOutputChannelTiled: case PimConvLoweringTiled2D: { - return standard::rewriteStreamedConv(state, distributedConsumers, rewriter, loc, /*forcedPackFactor=*/1); + return standard::rewriteStreamedConv(state, rewriter, loc, /*forcedPackFactor=*/1); } case PimConvLoweringInputKTiled: { - return standard::rewriteInputKTiledConv(state, distributedConsumers, rewriter, loc); + return standard::rewriteInputKTiledConv(state, rewriter, loc); } case PimConvLoweringStreamedPacked: { - return standard::rewriteStreamedConv(state, distributedConsumers, rewriter, loc, geo.pack); + return standard::rewriteStreamedConv(state, rewriter, loc, geo.pack); } case PimConvLoweringAuto: break; @@ -4190,130 +3253,6 @@ static FailureOr buildConvValueForStrategy(Operation* op, return failure(); } -static LogicalResult -createConvValueForStrategy(ONNXConvOp convOp, - const ConvLoweringState& state, - const ConvLoweringDecision& decision, - const DistributedConvAnalysis& analysis, - ArrayRef distributedConsumers, - PatternRewriter& rewriter, - FailureOr& result) { - result = buildConvValueForStrategy(convOp, convOp.getLoc(), state, decision, analysis, distributedConsumers, rewriter); - if (failed(result)) - return failure(); - - const ConvGeometry geo = buildConvGeometry(state); - const ConvStrategyEstimate estimate = estimateConvStrategy(geo, decision.strategy, analysis); - switch (decision.strategy) { - case PimConvLoweringDepthwise: - reportConvLoweringDecision( - convOp, geo, decision, estimate, /*batchSize=*/geo.p, /*numberOfBatches=*/1, /*usesComputeBatch=*/true, - /*usesBatchedInstructionEmission=*/true, std::nullopt); - return success(); - case PimConvLoweringLegacy: - case PimConvLoweringPackedIm2Col: - reportConvLoweringDecision( - convOp, geo, decision, estimate, /*batchSize=*/geo.pack, /*numberOfBatches=*/1, /*usesComputeBatch=*/true, - /*usesBatchedInstructionEmission=*/true, std::nullopt); - return success(); - case PimConvLoweringStreamedPatch: - case PimConvLoweringOutputChannelTiled: - case PimConvLoweringTiled2D: { - uint64_t chunkPositions = chooseStreamChunkPositions(geo, /*packFactor=*/1); - const int64_t batches = ceilIntegerDivide(geo.p, static_cast(chunkPositions)); - reportConvLoweringDecision(convOp, - geo, - decision, - estimate, - /*batchSize=*/1, - batches, - /*usesComputeBatch=*/true, - /*usesBatchedInstructionEmission=*/true, - chunkPositions); - return success(); - } - case PimConvLoweringInputKTiled: { - const int64_t numKSlices = ceilIntegerDivide(geo.k, geo.xbarSize); - const uint64_t maxLanesPerBatch = - std::max(1, - static_cast(crossbarCountInCore.getValue()) - / static_cast(std::max(1, numKSlices * 4))); - const uint64_t rowChunkWidth = std::max( - 1, - std::min({chooseStreamChunkPositions(geo, /*packFactor=*/1), - maxLanesPerBatch, - static_cast(state.outWidth)})); - const int64_t batches = - state.batchSize * state.outHeight * ceilIntegerDivide(state.outWidth, static_cast(rowChunkWidth)); - reportConvLoweringDecision(convOp, - geo, - decision, - estimate, - /*batchSize=*/1, - batches, - /*usesComputeBatch=*/false, - /*usesBatchedInstructionEmission=*/false, - rowChunkWidth); - return success(); - } - case PimConvLoweringStreamedPacked: { - uint64_t chunkPositions = chooseStreamChunkPositions(geo, geo.pack); - const int64_t batches = ceilIntegerDivide(geo.p, static_cast(chunkPositions)); - reportConvLoweringDecision(convOp, - geo, - decision, - estimate, - /*batchSize=*/geo.pack, - batches, - /*usesComputeBatch=*/true, - /*usesBatchedInstructionEmission=*/true, - chunkPositions); - return success(); - } - case PimConvLoweringAuto: - break; - } - return convOp.emitOpError("unexpected auto strategy at Conv lowering dispatch"); -} - -static LogicalResult -rewriteSelectedConv(ONNXConvOp convOp, - const ConvLoweringState& state, - const ConvLoweringDecision& decision, - const DistributedConvAnalysis& analysis, - PatternRewriter& rewriter) { - FailureOr result = failure(); - if (failed(createConvValueForStrategy(convOp, state, decision, analysis, analysis.steps, rewriter, result))) - return failure(); - - if (!analysis.hasLocalConsumers()) { - rewriter.replaceOp(convOp, *result); - return success(); - } - - assert(analysis.replacementOp && "conv rewrite expects a replacement op"); - rewriter.replaceOp(analysis.replacementOp, *result); - for (auto it = analysis.steps.rbegin(); it != analysis.steps.rend(); ++it) - if (it->op != analysis.replacementOp) - rewriter.eraseOp(it->op); - rewriter.eraseOp(convOp); - return success(); -} - -[[maybe_unused]] static LogicalResult -rewriteUngroupedConv(ONNXConvOp convOp, - const ConvLoweringState& state, - const ConvLoweringDecision& decision, - const DistributedConvAnalysis& analysis, - PatternRewriter& rewriter) { - return rewriteSelectedConv(convOp, state, decision, analysis, rewriter); -} - -static LogicalResult -rewriteGroupedConv(ONNXConvOp convOp, - const ConvLoweringState& state, - const ConvLoweringDecision& decision, - PatternRewriter& rewriter); static ConvLoweringState makeGroupedConvLoweringState(const ConvLoweringState& parent, Value groupX, @@ -4349,7 +3288,7 @@ static ConvLoweringState makeGroupedConvLoweringState( static FailureOr buildGroupedConvValue(Operation* op, Location loc, const ConvLoweringState& state, - const ConvLoweringDecision& decision, + PimConvLoweringType strategy, PatternRewriter& rewriter) { SmallVector xSlices = sliceTensor(state.x, /*axis=*/1, state.numChannelsInPerGroup, rewriter, loc); SmallVector wSlices = sliceTensor(state.w, /*axis=*/0, state.numChannelsOutPerGroup, rewriter, loc); @@ -4384,11 +3323,7 @@ static FailureOr buildGroupedConvValue(Operation* op, Value groupW = wSlices[groupId]; Value groupB = state.hasBias ? bSlices[groupId] : Value(); ConvLoweringState groupState = makeGroupedConvLoweringState(state, groupX, groupW, groupB, groupOutType); - DistributedConvAnalysis groupAnalysis; - groupAnalysis.barrierKind = DistributedConvBarrierKind::GroupedConv; - groupAnalysis.barrierDetail = "grouped convolution still materializes densely"; - FailureOr groupResult = - buildConvValueForStrategy(op, loc, groupState, decision, groupAnalysis, {}, rewriter); + FailureOr groupResult = buildConvValueForStrategy(op, loc, groupState, strategy, rewriter); if (failed(groupResult)) return failure(); groupResults.push_back(*groupResult); @@ -4403,17 +3338,6 @@ static FailureOr buildGroupedConvValue(Operation* op, return concatCompute.getResult(0); } -[[maybe_unused]] static LogicalResult -rewriteGroupedConv(ONNXConvOp convOp, - const ConvLoweringState& state, - const ConvLoweringDecision& decision, - PatternRewriter& rewriter) { - FailureOr result = buildGroupedConvValue(convOp.getOperation(), convOp.getLoc(), state, decision, rewriter); - if (failed(result)) - return failure(); - rewriter.replaceOp(convOp, *result); - return success(); -} } // namespace @@ -4459,29 +3383,14 @@ LogicalResult canLowerConvPlanToRowStrip(spatial::SpatConv2DPlanOp planOp) { if (state->hasBias && !isSupportedBiasAddValue(state->b, state->outType)) return failure(); - FailureOr requestedStrategy = resolveRequestedConvLoweringStrategy(planOp.getOperation()); - if (failed(requestedStrategy)) - return failure(); - - DistributedConvAnalysis analysis; - analysis.barrierKind = DistributedConvBarrierKind::UnsupportedConsumer; - analysis.barrierDetail = "selected row-strip layout"; ConvGeometry geometry = buildConvGeometry(*state); if (!rowStripOutputChannelTileFitsOneCore(geometry)) return failure(); - ConvLoweringDecision decision = chooseConvLoweringStrategy(geometry, *requestedStrategy, analysis); - if (decision.strategy == PimConvLoweringDepthwise && !depthwise::canUseStructuredRewrite(*state) - && *requestedStrategy == PimConvLoweringAuto) { - decision = {PimConvLoweringLegacy, - "depthwise auto fallback when structured depthwise lowering is not representable", - /*isAuto=*/true, - "", - ""}; - } - if (failed(verifyForcedConvLoweringStrategy(planOp.getOperation(), geometry, decision.strategy))) + FailureOr strategy = selectConvLoweringStrategy(planOp.getOperation(), *state); + if (failed(strategy)) return failure(); - switch (decision.strategy) { + switch (*strategy) { case PimConvLoweringLegacy: case PimConvLoweringDepthwise: case PimConvLoweringPackedIm2Col: @@ -4502,6 +3411,11 @@ LogicalResult canConsumeAndProduceRowStrip(spatial::SpatConv2DPlanOp planOp) { if (failed(state)) return failure(); + FailureOr strategy = selectConvLoweringStrategy(planOp.getOperation(), *state); + if (failed(strategy)) + return failure(); + if (*strategy == PimConvLoweringDepthwise) + return canConsumeDepthwiseRowStrip(*state) ? success() : failure(); StringRef failureReason; return canConsumePixelMajorRowStripFragments(*state, failureReason) ? success() : failure(); } @@ -4515,31 +3429,16 @@ lowerSelectedConv2DPlan(spatial::SpatConv2DPlanOp planOp, if (failed(state)) return failure(); - FailureOr requestedStrategy = resolveRequestedConvLoweringStrategy(planOp.getOperation()); - if (failed(requestedStrategy)) - return failure(); - - DistributedConvAnalysis analysis; - analysis.barrierKind = DistributedConvBarrierKind::UnsupportedConsumer; - analysis.barrierDetail = emitRowStripLayout ? "selected row-strip layout" : "selected dense layout"; - ConvGeometry geometry = buildConvGeometry(*state); - ConvLoweringDecision decision = chooseConvLoweringStrategy(geometry, *requestedStrategy, analysis); - if (decision.strategy == PimConvLoweringDepthwise && !depthwise::canUseStructuredRewrite(*state) - && *requestedStrategy == PimConvLoweringAuto) { - decision = {PimConvLoweringLegacy, - "depthwise auto fallback when structured depthwise lowering is not representable", - /*isAuto=*/true, - "", - ""}; - } - if (failed(verifyForcedConvLoweringStrategy(planOp.getOperation(), geometry, decision.strategy))) + FailureOr strategy = selectConvLoweringStrategy(planOp.getOperation(), *state); + if (failed(strategy)) return failure(); if (emitRowStripLayout) { if (rowStripInput) { if (failed(canConsumeAndProduceRowStrip(planOp))) return planOp.emitOpError("selected row-strip input/output layout is not supported for this Conv plan"), failure(); - return createConvOutputFromRowStripInput(*state, decision, *rowStripInput, rewriter, planOp.getLoc()); + return createConvOutputFromRowStripInput( + *state, *rowStripInput, *strategy, rewriter, planOp.getLoc()); } if (failed(canLowerConvPlanToRowStrip(planOp))) return planOp.emitOpError("selected row-strip layout is not supported for this Conv plan"), failure(); @@ -4549,11 +3448,11 @@ lowerSelectedConv2DPlan(spatial::SpatConv2DPlanOp planOp, return *rowStripStorage; } - if (decision.strategy == PimConvLoweringDepthwise) - return lowerDenseSelectedConvPlan(planOp.getOperation(), *state, decision.strategy, rewriter, planOp.getLoc()); + if (*strategy == PimConvLoweringDepthwise) + return lowerDenseSelectedConvPlan(planOp.getOperation(), *state, *strategy, rewriter, planOp.getLoc()); if (state->group != 1) - return lowerGroupedSelectedConvPlan(planOp.getOperation(), *state, decision.strategy, rewriter, planOp.getLoc()); - return lowerDenseSelectedConvPlan(planOp.getOperation(), *state, decision.strategy, rewriter, planOp.getLoc()); + return lowerGroupedSelectedConvPlan(planOp.getOperation(), *state, *strategy, rewriter, planOp.getLoc()); + return lowerDenseSelectedConvPlan(planOp.getOperation(), *state, *strategy, rewriter, planOp.getLoc()); } } // namespace onnx_mlir diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Elementwise.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Elementwise.cpp index a71808d..871640d 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Elementwise.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Elementwise.cpp @@ -48,6 +48,56 @@ static DenseElementsAttr getDenseConstantAttr(Value value) { return nullptr; } +struct BlueprintSplatMulToSpatial : OpConversionPattern { + explicit BlueprintSplatMulToSpatial(MLIRContext* ctx) : OpConversionPattern(ctx, 2) {} + + LogicalResult + matchAndRewrite(ONNXMulOp op, ONNXMulOpAdaptor adaptor, ConversionPatternRewriter& rewriter) const override { + auto blueprint = adaptor.getA().getDefiningOp(); + Value scalar = adaptor.getB(); + if (!blueprint) { + blueprint = adaptor.getB().getDefiningOp(); + scalar = adaptor.getA(); + } + auto scalarAttr = getDenseConstantAttr(scalar); + auto resultType = dyn_cast(op.getResult().getType()); + auto storageType = blueprint ? dyn_cast(blueprint.getInput().getType()) : RankedTensorType(); + if (!blueprint || !blueprint.getFragments().empty() || !scalarAttr || !scalarAttr.isSplat() || !resultType + || resultType != blueprint.getOutput().getType() || !storageType) + return failure(); + + auto mapped = mapGraphBatchFragments( + blueprint.getInput(), storageType, rewriter, op.getLoc(), [&](Value fragment, RankedTensorType fragmentType) { + auto splat = DenseElementsAttr::get(fragmentType, scalarAttr.getSplatValue()); + Value constant = arith::ConstantOp::create(rewriter, op.getLoc(), fragmentType, splat); + return FailureOr( + spatial::SpatVMulOp::create(rewriter, op.getLoc(), fragmentType, fragment, constant).getResult()); + }); + if (failed(mapped)) + return failure(); + + auto result = spatial::SpatBlueprintOp::create(rewriter, + op.getLoc(), + resultType, + *mapped, + ValueRange {}, + blueprint.getLogicalLayoutAttr(), + blueprint.getPhysicalLayoutAttr(), + blueprint.getFragmentOffsetsAttr(), + blueprint.getFragmentSizesAttr(), + blueprint.getIndexMapAttr(), + blueprint.getModeAttr(), + blueprint.getFragmentOperandIndicesAttr(), + blueprint.getFragmentSourceSlotsAttr(), + blueprint.getFragmentSourceOffsetsAttr(), + blueprint.getFragmentStridesAttr(), + blueprint.getConflictPolicyAttr(), + blueprint.getCoveragePolicyAttr()); + rewriter.replaceOp(op, result.getOutput()); + return success(); + } +}; + static FailureOr materializeBroadcastedConstantTensor(Value value, RankedTensorType resultType, ConversionPatternRewriter& rewriter, @@ -246,6 +296,7 @@ void populateElementwiseFusionPatterns(RewritePatternSet& patterns, MLIRContext* } void populateElementwisePatterns(RewritePatternSet& patterns, MLIRContext* ctx) { + patterns.add(ctx); patterns.add(ctx); patterns.add>(ctx); patterns.add>(ctx); diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp index fd26d78..3332073 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp @@ -106,6 +106,92 @@ static Value mapOutputBatchIndexToSourceBatchIndex(Value outputBatchIndex, return sourceBatchIndex; } +static FailureOr collapseFragmentAssemblyBatchDims(Value value, + RankedTensorType resultType, + PatternRewriter& rewriter, + Location loc) { + auto blueprint = value.getDefiningOp(); + auto inputType = dyn_cast(value.getType()); + auto storageType = blueprint ? dyn_cast(blueprint.getInput().getType()) : RankedTensorType(); + auto operandIndices = blueprint ? blueprint.getFragmentOperandIndices() : std::nullopt; + auto sourceOffsets = blueprint ? blueprint.getFragmentSourceOffsets() : std::nullopt; + auto fragmentStrides = blueprint ? blueprint.getFragmentStrides() : std::nullopt; + if (!blueprint || !inputType || !storageType || !inputType.hasStaticShape() || !storageType.hasStaticShape() + || inputType.getRank() <= 3 || resultType.getRank() != 3 || !blueprint.getFragments().empty() + || blueprint.getMode() != "fragment_assembly" || !operandIndices || !sourceOffsets || !fragmentStrides + || storageType.getRank() != inputType.getRank() + 1) + return failure(); + if (blueprint.getIndexMap() == spatial::kContiguousRowMajorFragments + && !spatial::isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + return blueprint.emitOpError("contiguous row-major fragment physical source order or storage is not canonical"), failure(); + + const int64_t batchRank = inputType.getRank() - 2; + SmallVector reassociation {ReassociationIndices {}, + ReassociationIndices {batchRank}, + ReassociationIndices {batchRank + 1}}; + for (int64_t dim = 0; dim < batchRank; ++dim) + reassociation.front().push_back(dim); + SmallVector outputFragmentShape {1, storageType.getDimSize(batchRank + 1), storageType.getDimSize(batchRank + 2)}; + auto outputFragmentType = RankedTensorType::get(outputFragmentShape, storageType.getElementType()); + auto outputStorageType = spatial::getGraphBatchPhysicalResultType(storageType.getDimSize(0), outputFragmentType); + SmallVector storageReassociation { + ReassociationIndices {0}, ReassociationIndices {}, ReassociationIndices {batchRank + 1}, + ReassociationIndices {batchRank + 2}}; + for (int64_t dim = 0; dim < batchRank; ++dim) + storageReassociation[1].push_back(dim + 1); + Value collapsedStorage = tensor::CollapseShapeOp::create( + rewriter, loc, outputStorageType, blueprint.getInput(), storageReassociation); + + const int64_t inputRank = inputType.getRank(); + const int64_t fragmentCount = operandIndices->size(); + ArrayRef inputOffsets = blueprint.getFragmentOffsets(); + ArrayRef inputSizes = blueprint.getFragmentSizes(); + SmallVector batchShape(inputType.getShape().drop_back(2)); + SmallVector batchStrides = computeRowMajorStrides(batchShape); + SmallVector offsets, sizes, strides; + offsets.reserve(fragmentCount * 3); + sizes.reserve(fragmentCount * 3); + strides.reserve(fragmentCount * 3); + for (int64_t fragment = 0; fragment < fragmentCount; ++fragment) { + int64_t flatBatch = 0; + for (int64_t dim = 0; dim < batchRank; ++dim) { + const int64_t index = fragment * inputRank + dim; + if (inputSizes[index] != 1 || (*fragmentStrides)[index] != 1) + return failure(); + flatBatch += inputOffsets[index] * batchStrides[dim]; + } + offsets.push_back(flatBatch); + sizes.push_back(1); + strides.push_back(1); + for (int64_t dim = batchRank; dim < inputRank; ++dim) { + const int64_t index = fragment * inputRank + dim; + offsets.push_back(inputOffsets[index]); + sizes.push_back(inputSizes[index]); + strides.push_back((*fragmentStrides)[index]); + } + } + auto collapsedBlueprint = spatial::SpatBlueprintOp::create(rewriter, + loc, + resultType, + collapsedStorage, + ValueRange {}, + blueprint.getLogicalLayoutAttr(), + rewriter.getStringAttr("fragmented"), + rewriter.getDenseI64ArrayAttr(offsets), + rewriter.getDenseI64ArrayAttr(sizes), + rewriter.getStringAttr("collapsed_fragments"), + blueprint.getModeAttr(), + blueprint.getFragmentOperandIndicesAttr(), + blueprint.getFragmentSourceSlotsAttr(), + blueprint.getFragmentSourceOffsetsAttr(), + rewriter.getDenseI64ArrayAttr(strides), + blueprint.getConflictPolicyAttr(), + blueprint.getCoveragePolicyAttr()); + if (spatial::isCanonicalContiguousRowMajorFragmentAssembly(collapsedBlueprint)) + collapsedBlueprint.setIndexMapAttr(rewriter.getStringAttr(spatial::kContiguousRowMajorFragments)); + return collapsedBlueprint.getOutput(); +} + static Value collapseBatchDims(Value value, int64_t batchSize, int64_t rows, int64_t cols, PatternRewriter& rewriter, Location loc) { auto type = cast(value.getType()); @@ -113,6 +199,8 @@ collapseBatchDims(Value value, int64_t batchSize, int64_t rows, int64_t cols, Pa return value; auto collapsedType = RankedTensorType::get({batchSize, rows, cols}, type.getElementType(), type.getEncoding()); + if (auto collapsed = collapseFragmentAssemblyBatchDims(value, collapsedType, rewriter, loc); succeeded(collapsed)) + return *collapsed; SmallVector reassociation = {ReassociationIndices {}, ReassociationIndices {static_cast(type.getRank() - 2)}, ReassociationIndices {static_cast(type.getRank() - 1)}}; @@ -241,7 +329,34 @@ static Value extractBatchMatrix(Value value, return materializeOrComputeUnary(value, matrixType, rewriter, loc, buildMatrix); } +static Value getLastTwoTransposeInput(Value value) { + auto type = cast(value.getType()); + if (auto transpose = value.getDefiningOp()) { + auto permutation = getTransposePermutationChecked(transpose.getPermAttr(), type.getRank()); + if (succeeded(permutation) && llvm::all_of(llvm::seq(0, type.getRank()), [&](int64_t dim) { + return (*permutation)[dim] == (dim < type.getRank() - 2 ? dim : 2 * type.getRank() - 3 - dim); + })) + return transpose.getData(); + } + return {}; +} + +static std::pair splitSplatMultiply(Value value) { + if (!value) + return {}; + auto multiply = value.getDefiningOp(); + if (!multiply) + return {}; + for (auto [data, scale] : {std::pair {multiply.getA(), multiply.getB()}, + std::pair {multiply.getB(), multiply.getA()}}) + if (auto constant = getHostConstDenseElementsAttr(scale); constant && constant.isSplat()) + return {data, scale}; + return {}; +} + static Value transposeLastTwoDims(Value value, PatternRewriter& rewriter, Location loc) { + if (Value input = getLastTwoTransposeInput(value)) + return input; auto type = cast(value.getType()); auto shape = type.getShape(); auto createONNXTranspose = [&](RankedTensorType resultType, ArrayRef permutation) { @@ -407,123 +522,152 @@ static Value extractDynamicBatchedRowVector(Value matrix, {offsets, sizes, getUnitStrides(rewriter, 3)}); } +static int64_t chooseDynamicMatMulRowsPerLane(int64_t rows, int64_t reductionSize, int64_t columns) { + const int64_t crossbarElements = static_cast(crossbarSize.getValue() * crossbarSize.getValue()); + const int64_t target = std::min(rows, ceilIntegerDivide(reductionSize * columns, crossbarElements)); + int64_t rowsPerLane = 1; + for (int64_t candidate = 2; candidate <= target; ++candidate) + if (rows % candidate == 0) + rowsPerLane = candidate; + return rowsPerLane; +} + static FailureOr createBatchedVvdmulBatch(Value a, ArrayRef aBatchShape, Value b, ArrayRef bBatchShape, ArrayRef outputBatchShape, - RankedTensorType aType, - RankedTensorType bType, - RankedTensorType columnPiecesType, + int64_t reductionSize, + int64_t rowsPerLane, + RankedTensorType rowPiecesType, RankedTensorType outType, + RankedTensorType publicationFragmentType, PatternRewriter& rewriter, Location loc) { const int64_t numBatches = outType.getDimSize(0); const int64_t numOutRows = outType.getDimSize(1); const int64_t numOutCols = outType.getDimSize(2); - const int64_t reductionSize = aType.getDimSize(2); - const int64_t laneCount = numBatches * numOutCols; - auto vectorType = RankedTensorType::get({1, reductionSize}, aType.getElementType()); + const int64_t rowGroups = numOutRows / rowsPerLane; + const int64_t laneCount = numBatches * rowGroups; + auto vectorType = RankedTensorType::get({1, reductionSize}, outType.getElementType()); auto scalarType = RankedTensorType::get({1, 1}, outType.getElementType()); - auto columnType = RankedTensorType::get({numOutRows, 1}, outType.getElementType()); + auto rowType = RankedTensorType::get({1, numOutCols}, outType.getElementType()); + auto rowsType = RankedTensorType::get({rowsPerLane, numOutCols}, outType.getElementType()); auto batchOp = createSpatComputeBatch( rewriter, loc, - TypeRange {columnPiecesType}, + TypeRange {rowPiecesType}, laneCount, ValueRange {}, ValueRange {a, b}, - [&](detail::SpatComputeBatchBodyArgs args) { + [&](detail::SpatComputeBatchBodyArgs args) -> LogicalResult { Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); - Value batch = affineFloorDivConst(rewriter, loc, args.lane, numOutCols, anchorOp); - Value column = affineModConst(rewriter, loc, args.lane, numOutCols, anchorOp); - Value bVector = extractDynamicBatchedRowVector( - args.inputs[1], bBatchShape, outputBatchShape, batch, column, vectorType, rewriter, loc); - Value columnInit = tensor::EmptyOp::create(rewriter, loc, columnType.getShape(), columnType.getElementType()); + Value batch = affineFloorDivConst(rewriter, loc, args.lane, rowGroups, anchorOp); + Value rowGroup = affineModConst(rewriter, loc, args.lane, rowGroups, anchorOp); + Value rowBase = affineMulConst(rewriter, loc, rowGroup, rowsPerLane, anchorOp); Value c0 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 0); Value c1 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 1); - Value cNumOutRows = - getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), numOutRows); - auto loop = buildNormalizedScfFor( + Value cRows = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), rowsPerLane); + Value cNumOutCols = + getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), numOutCols); + Value rowsInit = tensor::EmptyOp::create(rewriter, loc, rowsType.getShape(), rowsType.getElementType()); + auto rowsLoop = buildNormalizedScfFor( rewriter, loc, c0, - cNumOutRows, + cRows, c1, - ValueRange {columnInit}, - [&](OpBuilder&, Location nestedLoc, Value row, ValueRange iterArgs, SmallVectorImpl& yielded) { + ValueRange {rowsInit}, + [&](OpBuilder&, Location nestedLoc, Value rowOffset, ValueRange iterArgs, SmallVectorImpl& yielded) { + Value row = arith::AddIOp::create(rewriter, nestedLoc, rowBase, rowOffset); Value aVector = extractDynamicBatchedRowVector( args.inputs[0], aBatchShape, outputBatchShape, batch, row, vectorType, rewriter, nestedLoc); - Value scalar = spatial::SpatVVDMulOp::create(rewriter, nestedLoc, scalarType, aVector, bVector).getResult(); - Value next = tensor::InsertSliceOp::create(rewriter, - nestedLoc, - scalar, - iterArgs.front(), - SmallVector {row, rewriter.getIndexAttr(0)}, - SmallVector {rewriter.getIndexAttr(1), - rewriter.getIndexAttr(1)}, - getUnitStrides(rewriter, 2)); - yielded.push_back(next); + Value rowInit = tensor::EmptyOp::create(rewriter, nestedLoc, rowType.getShape(), rowType.getElementType()); + auto columnsLoop = buildNormalizedScfFor( + rewriter, + nestedLoc, + c0, + cNumOutCols, + c1, + ValueRange {rowInit}, + [&](OpBuilder&, Location columnLoc, Value column, ValueRange columnArgs, SmallVectorImpl& rowYielded) { + Value bVector = extractDynamicBatchedRowVector( + args.inputs[1], bBatchShape, outputBatchShape, batch, column, vectorType, rewriter, columnLoc); + Value scalar = spatial::SpatVVDMulOp::create(rewriter, columnLoc, scalarType, aVector, bVector).getResult(); + rowYielded.push_back(tensor::InsertSliceOp::create( + rewriter, + columnLoc, + scalar, + columnArgs.front(), + SmallVector {rewriter.getIndexAttr(0), column}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}, + getUnitStrides(rewriter, 2))); + return success(); + }); + assert(succeeded(columnsLoop) && "dynamic MatMul column loop construction must succeed"); + yielded.push_back(tensor::InsertSliceOp::create( + rewriter, + nestedLoc, + columnsLoop->results.front(), + iterArgs.front(), + SmallVector {rowOffset, rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(numOutCols)}, + getUnitStrides(rewriter, 2))); return success(); }); - assert(succeeded(loop) && "dynamic MatMul row loop construction must succeed"); - publishGraphBatchPhysicalFragment(rewriter, loc, loop->results.front(), args.outputs.front(), args.lane); + assert(succeeded(rowsLoop) && "dynamic MatMul row-group loop construction must succeed"); + Value fragment = rowsLoop->results.front(); + while (fragment.getType() != publicationFragmentType) { + auto expanded = addLeadingUnitTensorDimension(rewriter, loc, fragment); + if (failed(expanded)) + return failure(); + fragment = *expanded; + } + publishGraphBatchPhysicalFragment(rewriter, loc, fragment, args.outputs.front(), args.lane); + return success(); }); if (failed(batchOp)) return failure(); return *batchOp; } -static FailureOr createBatchedDynamicOutputCompute(Value scalarPieces, - RankedTensorType scalarPiecesType, - RankedTensorType outType, - PatternRewriter& rewriter, - Location loc) { - const int64_t laneCount = scalarPiecesType.getDimSize(0); - const int64_t numOutCols = outType.getDimSize(2); - auto columnType = RankedTensorType::get({outType.getDimSize(1), 1}, outType.getElementType()); - - auto computeOp = createSpatCompute<1>( - rewriter, loc, TypeRange {outType}, {}, ValueRange {scalarPieces}, [&](Value pieces) -> LogicalResult { - Value outputInit = - tensor::EmptyOp::create(rewriter, loc, outType.getShape(), outType.getElementType()).getResult(); - Value c0 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 0); - Value c1 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 1); - Value cLaneCount = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), laneCount); - auto loop = buildNormalizedScfFor( - rewriter, - loc, - c0, - cLaneCount, - c1, - ValueRange {outputInit}, - [&](OpBuilder&, Location nestedLoc, Value lane, ValueRange iterArgs, SmallVectorImpl& yielded) { - Value outputAcc = iterArgs.front(); - Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); - Value batch = affineFloorDivConst(rewriter, nestedLoc, lane, numOutCols, anchorOp); - Value column = affineModConst(rewriter, nestedLoc, lane, numOutCols, anchorOp); - FailureOr columnPiece = - extractGraphBatchPhysicalFragment(rewriter, nestedLoc, pieces, lane, columnType); - if (failed(columnPiece)) - return failure(); - SmallVector outputOffsets {batch, rewriter.getIndexAttr(0), column}; - SmallVector outputSizes = { - rewriter.getIndexAttr(1), rewriter.getIndexAttr(outType.getDimSize(1)), rewriter.getIndexAttr(1)}; - Value next = - tensor::InsertSliceOp::create( - rewriter, nestedLoc, *columnPiece, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3)) - .getResult(); - yielded.push_back(next); - return success(); - }); - if (failed(loop)) - return failure(); - spatial::SpatYieldOp::create(rewriter, loc, loop->results.front()); - return success(); - }); - if (failed(computeOp)) - return failure(); - return computeOp->getResult(0); +static FailureOr createBatchedRowOutputBlueprint(Value rowPieces, + RankedTensorType outType, + ArrayRef batchShape, + int64_t rowsPerFragment, + PatternRewriter& rewriter, + Location loc) { + SmallVector entries; + const int64_t rowAxis = outType.getRank() - 2; + const int64_t rows = outType.getDimSize(rowAxis); + const int64_t columns = outType.getDimSize(rowAxis + 1); + const int64_t batches = batchShape.empty() ? 1 : getStaticShapeElementCount(batchShape); + SmallVector batchStrides = computeRowMajorStrides(batchShape); + const int64_t rowGroups = rows / rowsPerFragment; + entries.reserve(batches * rowGroups); + for (int64_t batch = 0; batch < batches; ++batch) + for (int64_t row = 0; row < rows; row += rowsPerFragment) { + SmallVector offsets; + for (auto [dim, size] : llvm::enumerate(batchShape)) + offsets.push_back((batch / batchStrides[dim]) % size); + offsets.push_back(row); + offsets.push_back(0); + SmallVector sizes(outType.getRank(), 1); + sizes[rowAxis] = rowsPerFragment; + sizes.back() = columns; + entries.push_back({batch * rowGroups + row / rowsPerFragment, + 0, + std::move(offsets), + std::move(sizes)}); + } + return createFragmentAssemblyBlueprint( + rowPieces, + outType, + entries, + "dense_nchw", + rowsPerFragment == 1 ? spatial::kContiguousRowMajorFragments : "row_group_fragments", + rewriter, + loc); } static Value extractBatchedReductionPiece(Value partialPiecesArg, @@ -822,6 +966,8 @@ static Value finalizeNormalizedMatMulResult(Value value, const NormalizedMatMulInfo& info, PatternRewriter& rewriter, Location loc) { + if (value.getType() == info.outType) + return value; // The direct lowered result is always [flatBatch, normalizedM, normalizedN]. // Restore ONNX MatMul result rank by expanding right-aligned batch dimensions // and removing the synthetic unit matrix axes introduced for vector operands. @@ -923,17 +1069,34 @@ struct MatMulBatchedToSpatialComputes : OpRewritePattern { Location loc = matmulOp.getLoc(); bool useTransposedForm = !shapeInfo->lhsWasVector && !shapeInfo->rhsWasVector && isCompileTimeComputable(matmulOp.getA()) && !isCompileTimeComputable(matmulOp.getB()); + Value rhsRows = getLastTwoTransposeInput(matmulOp.getB()); + ONNXTransposeOp foldedTranspose = matmulOp.getB().getDefiningOp(); + ONNXMulOp foldedMultiply = rhsRows ? rhsRows.getDefiningOp() : ONNXMulOp {}; + auto [unscaledRhsRows, outputScale] = splitSplatMultiply(rhsRows); + if (unscaledRhsRows) + rhsRows = unscaledRhsRows; + const bool rhsStoredAsRows = rhsRows && !useTransposedForm; Value lhs = normalizeMatMulOperand(matmulOp.getA(), shapeInfo->normalizedLhsType, shapeInfo->lhsWasVector, rewriter, loc); - Value rhs = - normalizeMatMulOperand(matmulOp.getB(), shapeInfo->normalizedRhsType, shapeInfo->rhsWasVector, rewriter, loc); + Value rhs = normalizeMatMulOperand( + rhsStoredAsRows ? rhsRows : matmulOp.getB(), shapeInfo->normalizedRhsType, shapeInfo->rhsWasVector, rewriter, loc); lhs = collapseBatchDims(lhs, shapeInfo->lhsBatch, shapeInfo->m, shapeInfo->k, rewriter, loc); - rhs = collapseBatchDims(rhs, shapeInfo->rhsBatch, shapeInfo->k, shapeInfo->n, rewriter, loc); + rhs = collapseBatchDims(rhs, + shapeInfo->rhsBatch, + rhsStoredAsRows ? shapeInfo->n : shapeInfo->k, + rhsStoredAsRows ? shapeInfo->k : shapeInfo->n, + rewriter, + loc); MatMulLoweringPlan plan = buildLoweringPlan(lhs, rhs, *shapeInfo, useTransposedForm, rewriter, loc); plan.lhs = ensureBatchedTensor(plan.lhs, plan.lhsBatch, plan.m, plan.k, rewriter, loc); - plan.rhs = ensureBatchedTensor(plan.rhs, plan.rhsBatch, plan.k, plan.n, rewriter, loc); + plan.rhs = ensureBatchedTensor(plan.rhs, + plan.rhsBatch, + rhsStoredAsRows ? plan.n : plan.k, + rhsStoredAsRows ? plan.k : plan.n, + rewriter, + loc); plan.lhsType = cast(plan.lhs.getType()); plan.rhsType = cast(plan.rhs.getType()); auto directOutType = RankedTensorType::get( @@ -997,26 +1160,34 @@ struct MatMulBatchedToSpatialComputes : OpRewritePattern { return success(); } } - const int64_t laneCount = plan.batch * plan.n; - auto columnType = RankedTensorType::get({plan.m, 1}, shapeInfo->outType.getElementType()); - auto scalarPiecesType = spatial::getGraphBatchPhysicalResultType( - laneCount, columnType); - Value transposedRhs = transposeLastTwoDims(plan.rhs, rewriter, loc); + RankedTensorType blueprintType = !shapeInfo->lhsWasVector && !shapeInfo->rhsWasVector + ? shapeInfo->outType : directOutType; + SmallVector blueprintBatchShape = !shapeInfo->lhsWasVector && !shapeInfo->rhsWasVector + ? shapeInfo->outputBatchShape : SmallVector {plan.batch}; + const int64_t rowsPerLane = chooseDynamicMatMulRowsPerLane(plan.m, plan.k, plan.n); + const int64_t laneCount = plan.batch * plan.m / rowsPerLane; + SmallVector fragmentShape(blueprintType.getRank(), 1); + fragmentShape[fragmentShape.size() - 2] = rowsPerLane; + fragmentShape.back() = plan.n; + auto fragmentType = RankedTensorType::get(fragmentShape, shapeInfo->outType.getElementType()); + auto rowPiecesType = spatial::getGraphBatchPhysicalResultType(laneCount, fragmentType); + Value transposedRhs = rhsStoredAsRows ? plan.rhs : transposeLastTwoDims(plan.rhs, rewriter, loc); auto batchOp = createBatchedVvdmulBatch(plan.lhs, plan.lhsBatchShape, transposedRhs, plan.rhsBatchShape, plan.outputBatchShape, - plan.lhsType, - plan.rhsType, - scalarPiecesType, + plan.k, + rowsPerLane, + rowPiecesType, directOutType, + fragmentType, rewriter, loc); if (failed(batchOp)) return failure(); - auto result = - createBatchedDynamicOutputCompute(batchOp->getResult(0), scalarPiecesType, directOutType, rewriter, loc); + auto result = createBatchedRowOutputBlueprint( + batchOp->getResult(0), blueprintType, blueprintBatchShape, rowsPerLane, rewriter, loc); if (failed(result)) return failure(); Value finalResult = *result; @@ -1029,13 +1200,34 @@ struct MatMulBatchedToSpatialComputes : OpRewritePattern { .getResult(); } finalResult = finalizeNormalizedMatMulResult(finalResult, directOutType, *shapeInfo, rewriter, loc); + if (outputScale) + finalResult = ONNXMulOp::create( + rewriter, loc, shapeInfo->outType, finalResult, outputScale).getResult(); rewriter.replaceOp(matmulOp, finalResult); + if (foldedTranspose && foldedTranspose->use_empty()) + rewriter.eraseOp(foldedTranspose); + if (foldedMultiply && foldedMultiply->use_empty()) + rewriter.eraseOp(foldedMultiply); return success(); } }; +struct TransposedRhsMatMulToSpatial : MatMulBatchedToSpatialComputes { + using MatMulBatchedToSpatialComputes::MatMulBatchedToSpatialComputes; + + LogicalResult matchAndRewrite(ONNXMatMulOp matmulOp, PatternRewriter& rewriter) const override { + if (!getLastTwoTransposeInput(matmulOp.getB())) + return failure(); + return MatMulBatchedToSpatialComputes::matchAndRewrite(matmulOp, rewriter); + } +}; + } // namespace +void populateMatMulFusionPatterns(RewritePatternSet& patterns, MLIRContext* ctx) { + patterns.add(ctx); +} + void populateMatMulRewritePatterns(RewritePatternSet& patterns, MLIRContext* ctx) { patterns.insert(ctx); } diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Resize.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Resize.cpp index 769a943..764ea71 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Resize.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Resize.cpp @@ -5,8 +5,11 @@ #include "llvm/ADT/STLExtras.h" +#include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp" #include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp" +#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/RowStripLayoutUtils.hpp" +#include "src/Accelerators/PIM/Conversion/ONNXToSpatial/PlanLowering.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Patterns.hpp" #include "src/Accelerators/PIM/Dialect/Spatial/SpatialOps.hpp" #include "src/Dialect/ONNX/ONNXOps.hpp" @@ -17,126 +20,144 @@ namespace onnx_mlir { namespace { static Value buildNearestAsymmetricIndex( - Value outputIndex, int64_t inputDim, int64_t outputDim, ConversionPatternRewriter& rewriter, Location loc) { + Value outputIndex, int64_t inputDim, int64_t outputDim, PatternRewriter& rewriter, Location loc) { + if (inputDim == outputDim) + return outputIndex; Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); + if (outputDim % inputDim == 0) + return affineFloorDivConst(rewriter, loc, outputIndex, outputDim / inputDim, anchorOp); + if (inputDim % outputDim == 0) + return affineMulConst(rewriter, loc, outputIndex, inputDim / outputDim, anchorOp); Value cInputDim = getOrCreateIndexConstant(rewriter, anchorOp, inputDim); Value cOutputDim = getOrCreateIndexConstant(rewriter, anchorOp, outputDim); - Value cInputDimLast = getOrCreateIndexConstant(rewriter, anchorOp, inputDim - 1); Value scaledIndex = arith::MulIOp::create(rewriter, loc, outputIndex, cInputDim); - Value inputIndex = arith::DivUIOp::create(rewriter, loc, scaledIndex, cOutputDim); - return arith::MinUIOp::create(rewriter, loc, inputIndex, cInputDimLast); + return arith::DivUIOp::create(rewriter, loc, scaledIndex, cOutputDim); } -static FailureOr buildNearestResizeLoop(Value input, - RankedTensorType inputType, - RankedTensorType resultType, - ConversionPatternRewriter& rewriter, - Location loc) { - auto elemType = resultType.getElementType(); - SmallVector unitShape(resultType.getRank(), 1); - auto unitTensorType = RankedTensorType::get(unitShape, elemType); - - SmallVector unitSizes(resultType.getRank(), rewriter.getIndexAttr(1)); - SmallVector unitStrides(resultType.getRank(), rewriter.getIndexAttr(1)); - - Operation* anchorOp = rewriter.getInsertionBlock()->getParentOp(); - Value c0 = getOrCreateIndexConstant(rewriter, anchorOp, 0); - Value c1 = getOrCreateIndexConstant(rewriter, anchorOp, 1); - Value cOutputN = getOrCreateIndexConstant(rewriter, anchorOp, resultType.getDimSize(0)); - Value cOutputC = getOrCreateIndexConstant(rewriter, anchorOp, resultType.getDimSize(1)); - Value cOutputH = getOrCreateIndexConstant(rewriter, anchorOp, resultType.getDimSize(2)); - Value cOutputW = getOrCreateIndexConstant(rewriter, anchorOp, resultType.getDimSize(3)); - - Value outputInit = tensor::EmptyOp::create(rewriter, loc, resultType.getShape(), elemType); - - auto batchLoop = buildNormalizedScfFor( - rewriter, - loc, - c0, - cOutputN, - c1, - ValueRange {outputInit}, - [&](OpBuilder&, Location nestedLoc, Value outputN, ValueRange batchIterArgs, SmallVectorImpl& batchYielded) { - Value outputBatchAcc = batchIterArgs.front(); - Value inputN = - buildNearestAsymmetricIndex(outputN, inputType.getDimSize(0), resultType.getDimSize(0), rewriter, nestedLoc); - - auto channelLoop = buildNormalizedScfFor( - rewriter, - nestedLoc, - c0, - cOutputC, - c1, - ValueRange {outputBatchAcc}, - [&](OpBuilder&, - Location channelLoc, - Value outputC, - ValueRange channelIterArgs, - SmallVectorImpl& channelYielded) { - Value outputChannelAcc = channelIterArgs.front(); - Value inputC = buildNearestAsymmetricIndex( - outputC, inputType.getDimSize(1), resultType.getDimSize(1), rewriter, channelLoc); - - auto heightLoop = buildNormalizedScfFor( - rewriter, - channelLoc, - c0, - cOutputH, - c1, - ValueRange {outputChannelAcc}, - [&](OpBuilder&, - Location heightLoc, - Value outputH, - ValueRange heightIterArgs, - SmallVectorImpl& heightYielded) { - Value outputHeightAcc = heightIterArgs.front(); - Value inputH = buildNearestAsymmetricIndex( - outputH, inputType.getDimSize(2), resultType.getDimSize(2), rewriter, heightLoc); - - auto widthLoop = buildNormalizedScfFor( - rewriter, - heightLoc, - c0, - cOutputW, - c1, - ValueRange {outputHeightAcc}, - [&](OpBuilder&, - Location widthLoc, - Value outputW, - ValueRange widthIterArgs, - SmallVectorImpl& widthYielded) { - Value outputWidthAcc = widthIterArgs.front(); - Value inputW = buildNearestAsymmetricIndex( - outputW, inputType.getDimSize(3), resultType.getDimSize(3), rewriter, widthLoc); - - SmallVector inputOffsets = {inputN, inputC, inputH, inputW}; - Value inputSlice = tensor::ExtractSliceOp::create( - rewriter, widthLoc, unitTensorType, input, inputOffsets, unitSizes, unitStrides); - - SmallVector outputOffsets = {outputN, outputC, outputH, outputW}; - Value updatedOutput = tensor::InsertSliceOp::create( - rewriter, widthLoc, inputSlice, outputWidthAcc, outputOffsets, unitSizes, unitStrides); - widthYielded.push_back(updatedOutput); - return success(); - }); - if (failed(widthLoop)) - return failure(); - heightYielded.push_back(widthLoop->results.front()); - return success(); - }); - if (failed(heightLoop)) - return failure(); - channelYielded.push_back(heightLoop->results.front()); +static FailureOr buildDenseNearestResize(Value input, + RankedTensorType inputType, + RankedTensorType resultType, + PatternRewriter& rewriter, + Location loc) { + ArrayRef shape = resultType.getShape(); + int64_t rowCount = shape[0] * shape[1] * shape[2]; + auto scalarType = RankedTensorType::get({1, 1, 1, 1}, resultType.getElementType()); + auto rowType = RankedTensorType::get({1, 1, 1, shape[3]}, resultType.getElementType()); + auto rowsType = RankedTensorType::get({rowCount, 1, 1, 1, shape[3]}, resultType.getElementType()); + auto batch = createSpatComputeBatch( + rewriter, loc, TypeRange {rowsType}, rowCount, {}, ValueRange {input}, + [&](detail::SpatComputeBatchBodyArgs args) { + Operation* anchor = rewriter.getInsertionBlock()->getParentOp(); + Value outputN = affineFloorDivConst(rewriter, loc, args.lane, shape[1] * shape[2], anchor); + Value channelRow = affineModConst(rewriter, loc, args.lane, shape[1] * shape[2], anchor); + Value outputC = affineFloorDivConst(rewriter, loc, channelRow, shape[2], anchor); + Value outputH = affineModConst(rewriter, loc, channelRow, shape[2], anchor); + Value inputN = buildNearestAsymmetricIndex(outputN, inputType.getDimSize(0), shape[0], rewriter, loc); + Value inputC = buildNearestAsymmetricIndex(outputC, inputType.getDimSize(1), shape[1], rewriter, loc); + Value inputH = buildNearestAsymmetricIndex(outputH, inputType.getDimSize(2), shape[2], rewriter, loc); + Value row = tensor::EmptyOp::create(rewriter, loc, rowType.getShape(), rowType.getElementType()); + Value c0 = getOrCreateIndexConstant(rewriter, anchor, 0); + Value c1 = getOrCreateIndexConstant(rewriter, anchor, 1); + Value width = getOrCreateIndexConstant(rewriter, anchor, shape[3]); + auto loop = buildNormalizedScfFor( + rewriter, loc, c0, width, c1, ValueRange {row}, + [&](OpBuilder&, Location nestedLoc, Value outputW, ValueRange iterArgs, SmallVectorImpl& yielded) { + Value inputW = buildNearestAsymmetricIndex( + outputW, inputType.getDimSize(3), shape[3], rewriter, nestedLoc); + SmallVector unitSizes(4, rewriter.getIndexAttr(1)); + SmallVector unitStrides(4, rewriter.getIndexAttr(1)); + Value scalar = tensor::ExtractSliceOp::create( + rewriter, nestedLoc, scalarType, args.inputs.front(), + SmallVector {inputN, inputC, inputH, inputW}, unitSizes, unitStrides); + yielded.push_back(tensor::InsertSliceOp::create( + rewriter, nestedLoc, scalar, iterArgs.front(), + SmallVector {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), + rewriter.getIndexAttr(0), outputW}, + unitSizes, unitStrides)); return success(); }); - if (failed(channelLoop)) + assert(succeeded(loop) && "nearest Resize row loop construction must succeed"); + publishGraphBatchPhysicalFragment(rewriter, loc, loop->results.front(), args.outputs.front(), args.lane); + }); + if (failed(batch)) + return failure(); + + SmallVector entries; + entries.reserve(rowCount); + for (int64_t n = 0; n < shape[0]; ++n) + for (int64_t c = 0; c < shape[1]; ++c) + for (int64_t h = 0; h < shape[2]; ++h) + entries.push_back({(n * shape[1] + c) * shape[2] + h, 0, {n, c, h, 0}, {1, 1, 1, shape[3]}}); + return createFragmentAssemblyBlueprint( + batch->getResult(0), resultType, entries, "dense_nchw", spatial::kContiguousRowMajorFragments, rewriter, loc); +} + +static FailureOr buildRowStripNearestResize( + Value storage, RankedTensorType inputType, RankedTensorType resultType, + PatternRewriter& rewriter, Location loc) { + auto input = describeRowStripPhysicalValue(storage, inputType); + if (failed(input)) + return failure(); + int64_t tilesPerRow = input->tilesPerRow; + int64_t outputHeight = resultType.getDimSize(2); + int64_t outputWidth = resultType.getDimSize(3); + int64_t tileChannels = input->fragmentType.getDimSize(3); + int64_t laneCount = outputHeight * tilesPerRow; + auto outputFragmentType = RankedTensorType::get( + {1, 1, outputWidth, tileChannels}, resultType.getElementType()); + auto outputStorageType = spatial::getGraphBatchPhysicalResultType( + laneCount, outputFragmentType); + auto pixelType = RankedTensorType::get( + {1, 1, 1, tileChannels}, resultType.getElementType()); + auto batch = createSpatComputeBatch( + rewriter, loc, TypeRange {outputStorageType}, laneCount, {}, ValueRange {storage}, + [&](detail::SpatComputeBatchBodyArgs args) { + Operation* anchor = rewriter.getInsertionBlock()->getParentOp(); + Value outputRow = affineFloorDivConst(rewriter, loc, args.lane, tilesPerRow, anchor); + Value tile = affineModConst(rewriter, loc, args.lane, tilesPerRow, anchor); + Value inputRow = buildNearestAsymmetricIndex( + outputRow, inputType.getDimSize(2), outputHeight, rewriter, loc); + Value inputSlot = arith::AddIOp::create( + rewriter, loc, affineMulConst(rewriter, loc, inputRow, tilesPerRow, anchor), tile); + auto source = extractGraphBatchPhysicalFragment( + rewriter, loc, args.inputs.front(), inputSlot, input->fragmentType); + if (failed(source)) return failure(); - batchYielded.push_back(channelLoop->results.front()); + Value initial = tensor::EmptyOp::create( + rewriter, loc, outputFragmentType.getShape(), resultType.getElementType()); + Value c0 = getOrCreateIndexConstant(rewriter, anchor, 0); + Value c1 = getOrCreateIndexConstant(rewriter, anchor, 1); + Value width = getOrCreateIndexConstant(rewriter, anchor, outputWidth); + auto loop = buildNormalizedScfFor( + rewriter, loc, c0, width, c1, ValueRange {initial}, + [&](OpBuilder&, Location nestedLoc, Value outputColumn, ValueRange iterArgs, + SmallVectorImpl& yielded) { + Value inputColumn = buildNearestAsymmetricIndex( + outputColumn, inputType.getDimSize(3), outputWidth, rewriter, nestedLoc); + Value pixel = tensor::ExtractSliceOp::create( + rewriter, nestedLoc, pixelType, *source, + SmallVector {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), + inputColumn, rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(1), rewriter.getIndexAttr(tileChannels)}, + getUnitStrides(rewriter, 4)); + yielded.push_back(tensor::InsertSliceOp::create( + rewriter, nestedLoc, pixel, iterArgs.front(), + SmallVector {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), + outputColumn, rewriter.getIndexAttr(0)}, + SmallVector {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), + rewriter.getIndexAttr(1), rewriter.getIndexAttr(tileChannels)}, + getUnitStrides(rewriter, 4))); + return success(); + }); + if (failed(loop)) + return failure(); + publishGraphBatchPhysicalFragment( + rewriter, loc, loop->results.front(), args.outputs.front(), args.lane); return success(); }); - if (failed(batchLoop)) - return failure(); - return batchLoop->results.front(); + return failed(batch) ? FailureOr(failure()) + : FailureOr(batch->getResult(0)); } struct Resize : OpConversionPattern { @@ -161,23 +182,38 @@ struct Resize : OpConversionPattern { || llvm::any_of(resultType.getShape(), [](int64_t dim) { return dim <= 0; })) return rewriter.notifyMatchFailure(resizeOp, "resize lowering requires positive static dimensions."); - auto computeOp = createSpatCompute<1>( - rewriter, resizeOp.getLoc(), TypeRange {resultType}, {}, adaptor.getX(), [&](Value x) -> LogicalResult { - auto result = buildNearestResizeLoop(x, inputType, resultType, rewriter, resizeOp.getLoc()); - if (failed(result)) - return failure(); - spatial::SpatYieldOp::create(rewriter, resizeOp.getLoc(), *result); - return success(); - }); - if (failed(computeOp)) - return failure(); - rewriter.replaceOp(resizeOp, computeOp->getResults()); + auto plan = spatial::SpatResizeNearestPlanOp::create( + rewriter, resizeOp.getLoc(), resultType, adaptor.getX(), rewriter.getStringAttr("nchw")); + rewriter.replaceOp(resizeOp, plan.getResult()); return success(); } }; } // namespace +LogicalResult canLowerResizeNearestPlanToRowStrip( + spatial::SpatResizeNearestPlanOp planOp) { + auto inputType = dyn_cast(planOp.getInput().getType()); + auto outputType = dyn_cast(planOp.getOutput().getType()); + return success(inputType && outputType && inputType.hasStaticShape() + && outputType.hasStaticShape() && inputType.getRank() == 4 + && outputType.getRank() == 4 && inputType.getDimSize(0) == 1 + && outputType.getDimSize(0) == 1 + && inputType.getDimSize(1) == outputType.getDimSize(1)); +} + +FailureOr lowerSelectedResizeNearestPlan( + spatial::SpatResizeNearestPlanOp planOp, std::optional rowStripInput, + PatternRewriter& rewriter) { + auto inputType = cast(planOp.getInput().getType()); + auto outputType = cast(planOp.getOutput().getType()); + if (rowStripInput) + return buildRowStripNearestResize( + *rowStripInput, inputType, outputType, rewriter, planOp.getLoc()); + return buildDenseNearestResize( + planOp.getInput(), inputType, outputType, rewriter, planOp.getLoc()); +} + void populateResizePatterns(RewritePatternSet& patterns, MLIRContext* ctx) { patterns.add(ctx); } } // namespace onnx_mlir diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Transpose.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Transpose.cpp index 4a37d9e..2088717 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Transpose.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Tensor/Transpose.cpp @@ -61,6 +61,74 @@ static FailureOr materializeTransposedConstant(Value input, resultType); } +static FailureOr transposeFragmentAssemblyBlueprint(spatial::SpatBlueprintOp blueprint, + RankedTensorType resultType, + ArrayRef permutation, + ConversionPatternRewriter& rewriter, + Location loc) { + auto storageType = dyn_cast(blueprint.getInput().getType()); + auto sourceOffsets = blueprint.getFragmentSourceOffsets(); + auto fragmentStrides = blueprint.getFragmentStrides(); + if (!storageType || !storageType.hasStaticShape() || !resultType.hasStaticShape() + || !blueprint.getFragments().empty() || blueprint.getMode() != "fragment_assembly" + || !blueprint.getFragmentOperandIndices() || !sourceOffsets || !fragmentStrides + || llvm::any_of(*sourceOffsets, [](int64_t offset) { return offset != 0; }) + || storageType.getRank() != resultType.getRank() + 1) + return failure(); + if (blueprint.getIndexMap() == spatial::kContiguousRowMajorFragments + && !spatial::isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + return blueprint.emitOpError("contiguous row-major fragment physical source order or storage is not canonical"), failure(); + + SmallVector outputStorageShape {storageType.getDimSize(0)}; + for (int64_t sourceDim : permutation) + outputStorageShape.push_back(storageType.getDimSize(sourceDim + 1)); + auto outputStorageType = RankedTensorType::get(outputStorageShape, storageType.getElementType()); + auto mapped = mapGraphBatchFragments( + blueprint.getInput(), outputStorageType, rewriter, loc, [&](Value fragment, RankedTensorType fragmentType) { + Value init = createTransposeInit(fragment, fragmentType, permutation, rewriter, loc); + return FailureOr( + linalg::TransposeOp::create(rewriter, loc, fragment, init, permutation).getResult()[0]); + }); + if (failed(mapped)) + return failure(); + + const int64_t rank = resultType.getRank(); + const int64_t fragmentCount = blueprint.getFragmentOperandIndices()->size(); + SmallVector offsets, sizes, strides; + offsets.reserve(fragmentCount * rank); + sizes.reserve(fragmentCount * rank); + strides.reserve(fragmentCount * rank); + ArrayRef inputOffsets = blueprint.getFragmentOffsets(); + ArrayRef inputSizes = blueprint.getFragmentSizes(); + for (int64_t fragment = 0; fragment < fragmentCount; ++fragment) + for (int64_t sourceDim : permutation) { + const int64_t index = fragment * rank + sourceDim; + offsets.push_back(inputOffsets[index]); + sizes.push_back(inputSizes[index]); + strides.push_back((*fragmentStrides)[index]); + } + auto transposedBlueprint = spatial::SpatBlueprintOp::create(rewriter, + loc, + resultType, + *mapped, + ValueRange {}, + blueprint.getLogicalLayoutAttr(), + rewriter.getStringAttr("fragmented"), + rewriter.getDenseI64ArrayAttr(offsets), + rewriter.getDenseI64ArrayAttr(sizes), + rewriter.getStringAttr("permuted_fragments"), + blueprint.getModeAttr(), + blueprint.getFragmentOperandIndicesAttr(), + blueprint.getFragmentSourceSlotsAttr(), + blueprint.getFragmentSourceOffsetsAttr(), + rewriter.getDenseI64ArrayAttr(strides), + blueprint.getConflictPolicyAttr(), + blueprint.getCoveragePolicyAttr()); + if (spatial::isCanonicalContiguousRowMajorFragmentAssembly(transposedBlueprint)) + transposedBlueprint.setIndexMapAttr(rewriter.getStringAttr(spatial::kContiguousRowMajorFragments)); + return transposedBlueprint.getOutput(); +} + struct TransposeToLinalgTranspose : OpConversionPattern { using OpConversionPattern::OpConversionPattern; @@ -75,6 +143,14 @@ struct TransposeToLinalgTranspose : OpConversionPattern { auto permutation = getTransposePermutationChecked(transposeOp.getPermAttr(), inputType.getRank()); if (failed(permutation)) return failure(); + if (auto blueprint = adaptor.getData().getDefiningOp()) { + auto transposed = + transposeFragmentAssemblyBlueprint(blueprint, resultType, *permutation, rewriter, transposeOp.getLoc()); + if (succeeded(transposed)) { + rewriter.replaceOp(transposeOp, *transposed); + return success(); + } + } if (isCompileTimeComputable(adaptor.getData())) { auto constantTranspose = materializeTransposedConstant(adaptor.getData(), resultType, *permutation, rewriter, transposeOp.getLoc()); diff --git a/src/PIM/Conversion/ONNXToSpatial/PlanLowering.hpp b/src/PIM/Conversion/ONNXToSpatial/PlanLowering.hpp index be2308e..7a952a5 100644 --- a/src/PIM/Conversion/ONNXToSpatial/PlanLowering.hpp +++ b/src/PIM/Conversion/ONNXToSpatial/PlanLowering.hpp @@ -20,6 +20,14 @@ lowerSelectedConv2DPlan(spatial::SpatConv2DPlanOp planOp, mlir::LogicalResult canLowerConvPlanToRowStrip(spatial::SpatConv2DPlanOp planOp); mlir::LogicalResult canConsumeAndProduceRowStrip(spatial::SpatConv2DPlanOp planOp); +mlir::LogicalResult canLowerResizeNearestPlanToRowStrip( + spatial::SpatResizeNearestPlanOp planOp); + +mlir::FailureOr lowerSelectedResizeNearestPlan( + spatial::SpatResizeNearestPlanOp planOp, + std::optional rowStripInput, + mlir::PatternRewriter& rewriter); + mlir::LogicalResult canLowerMaxPoolPlanToRowStrip(spatial::SpatMaxPool2DPlanOp planOp); mlir::FailureOr diff --git a/src/PIM/Conversion/ONNXToSpatial/SpatialLayoutPlanningPass.cpp b/src/PIM/Conversion/ONNXToSpatial/SpatialLayoutPlanningPass.cpp index df69231..0b39303 100644 --- a/src/PIM/Conversion/ONNXToSpatial/SpatialLayoutPlanningPass.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/SpatialLayoutPlanningPass.cpp @@ -36,6 +36,8 @@ static bool usesSelectedRowStrip(Operation* user, llvm::DenseMap(user)) return getSelectedLayout(layouts, siluPlan.getResult()) == SelectedLayout::PixelMajorRowStrip; + if (auto resizePlan = dyn_cast(user)) + return getSelectedLayout(layouts, resizePlan.getResult()) == SelectedLayout::PixelMajorRowStrip; if (auto biasAddPlan = dyn_cast(user)) return getSelectedLayout(layouts, biasAddPlan.getResult()) == SelectedLayout::PixelMajorRowStrip; if (auto addPlan = dyn_cast(user)) @@ -66,6 +68,8 @@ static bool allUsersCanHandleRowStrip(Value value, llvm::DenseMap(user)) return true; + if (auto resizePlan = dyn_cast(user)) + return succeeded(canLowerResizeNearestPlanToRowStrip(resizePlan)); if (auto biasAddPlan = dyn_cast(user)) { auto resultType = dyn_cast(biasAddPlan.getOutput().getType()); return resultType && isSupportedBiasAddValue(biasAddPlan.getBias(), resultType); @@ -117,6 +121,14 @@ static SelectedLayout chooseActivationLayout(Value input, return SelectedLayout::PixelMajorRowStrip; } +static SelectedLayout chooseResizeLayout( + spatial::SpatResizeNearestPlanOp resizePlan, + llvm::DenseMap& layouts) { + return getSelectedLayout(layouts, resizePlan.getInput()) == SelectedLayout::PixelMajorRowStrip + && succeeded(canLowerResizeNearestPlanToRowStrip(resizePlan)) + ? SelectedLayout::PixelMajorRowStrip : SelectedLayout::DenseNchw; +} + static SelectedLayout chooseBiasAddLayout(spatial::SpatBiasAddPlanOp biasAddPlan, llvm::DenseMap& layouts) { if (getSelectedLayout(layouts, biasAddPlan.getInput()) != SelectedLayout::PixelMajorRowStrip) @@ -255,6 +267,14 @@ struct SpatialLayoutPlanningPass final : PassWrapper(&op)) { + SelectedLayout selected = chooseResizeLayout(resizePlan, layouts); + if (layouts[resizePlan.getResult()] != selected) { + layouts[resizePlan.getResult()] = selected; + changed = true; + } + continue; + } if (auto biasAddPlan = dyn_cast(&op)) { SelectedLayout selected = chooseBiasAddLayout(biasAddPlan, layouts); if (layouts[biasAddPlan.getResult()] != selected) { @@ -312,6 +332,8 @@ struct SpatialLayoutPlanningPass final : PassWrapper(&op)) producedValue = siluPlan.getResult(); + else if (auto resizePlan = dyn_cast(&op)) + producedValue = resizePlan.getResult(); else if (auto maxPoolPlan = dyn_cast(&op)) producedValue = maxPoolPlan.getResult(); else if (auto averagePoolPlan = dyn_cast(&op)) diff --git a/src/PIM/Conversion/SpatialToPim/Common.cpp b/src/PIM/Conversion/SpatialToPim/Common.cpp index 8e3d840..e4eddac 100644 --- a/src/PIM/Conversion/SpatialToPim/Common.cpp +++ b/src/PIM/Conversion/SpatialToPim/Common.cpp @@ -129,6 +129,32 @@ LogicalResult validateFragmentAssemblyMetadata(spatial::SpatBlueprintOp blueprin return success(); } +FailureOr reshapeContiguousRowMajorFragments(RewriterBase& rewriter, + Location loc, + mlir::Value source, + RankedTensorType resultType) { + auto sourceType = dyn_cast(source.getType()); + if (!sourceType || !sourceType.hasStaticShape() || !resultType.hasStaticShape() || resultType.getRank() < 2 + || sourceType.getRank() != resultType.getRank() + 1 || sourceType.getElementType() != resultType.getElementType() + || sourceType.getNumElements() != resultType.getNumElements() + || sourceType.getDimSize(0) != getStaticShapeElementCount(resultType.getShape().drop_back()) + || sourceType.getDimSize(sourceType.getRank() - 1) != resultType.getDimSize(resultType.getRank() - 1) + || llvm::any_of(sourceType.getShape().slice(1, sourceType.getRank() - 2), [](int64_t dim) { return dim != 1; })) + return failure(); + + SmallVector collapse {{}, {sourceType.getRank() - 1}}; + for (int64_t dim = 0; dim < sourceType.getRank() - 1; ++dim) + collapse.front().push_back(dim); + auto flatType = RankedTensorType::get( + {sourceType.getDimSize(0), sourceType.getDimSize(sourceType.getRank() - 1)}, resultType.getElementType()); + mlir::Value flat = tensor::CollapseShapeOp::create(rewriter, loc, flatType, source, collapse); + + SmallVector expand {{}, {resultType.getRank() - 1}}; + for (int64_t dim = 0; dim < resultType.getRank() - 1; ++dim) + expand.front().push_back(dim); + return tensor::ExpandShapeOp::create(rewriter, loc, resultType, flat, expand).getResult(); +} + static SmallVector expandFlatElementIndex(int64_t flatIndex, ArrayRef shape) { SmallVector indices(shape.size(), 0); for (int64_t dim = static_cast(shape.size()) - 1; dim >= 0; --dim) { diff --git a/src/PIM/Conversion/SpatialToPim/Common.hpp b/src/PIM/Conversion/SpatialToPim/Common.hpp index 481976c..61e74a4 100644 --- a/src/PIM/Conversion/SpatialToPim/Common.hpp +++ b/src/PIM/Conversion/SpatialToPim/Common.hpp @@ -51,6 +51,11 @@ mlir::LogicalResult validateFragmentAssemblyMetadata(onnx_mlir::spatial::SpatBlu llvm::ArrayRef flatSizes, llvm::ArrayRef flatStrides); +mlir::FailureOr reshapeContiguousRowMajorFragments(mlir::RewriterBase& rewriter, + mlir::Location loc, + mlir::Value source, + mlir::RankedTensorType resultType); + mlir::FailureOr> getStaticSliceOffsetsForElementOffset(mlir::Operation* anchor, mlir::ShapedType sourceType, diff --git a/src/PIM/Conversion/SpatialToPim/CoreLoweringPatterns.cpp b/src/PIM/Conversion/SpatialToPim/CoreLoweringPatterns.cpp index 8b1ad80..f2cb16d 100644 --- a/src/PIM/Conversion/SpatialToPim/CoreLoweringPatterns.cpp +++ b/src/PIM/Conversion/SpatialToPim/CoreLoweringPatterns.cpp @@ -71,6 +71,16 @@ static FailureOr lowerFragmentAssemblyBlueprint(IRRewriter& rewriter, flatStrides))) return failure(); + if (blueprint.getIndexMap() == spatial::kContiguousRowMajorFragments) { + if (!spatial::isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + return blueprint.emitOpError("contiguous row-major fragment physical source order or storage is not canonical"), failure(); + Value source = mapping.lookupOrDefault(blueprint.getInput()); + auto reshaped = reshapeContiguousRowMajorFragments( + rewriter, blueprint.getLoc(), source, cast(resultType)); + if (failed(reshaped)) + return blueprint.emitOpError("contiguous row-major fragment storage does not match its logical result"), failure(); + return *reshaped; + } SmallVector hostStrides = computeRowMajorStrides(resultType.getShape()); SmallVector copies; for (int64_t fragmentIndex = 0; fragmentIndex < static_cast(operandIndices.size()); ++fragmentIndex) { diff --git a/src/PIM/Conversion/SpatialToPim/Patterns.cpp b/src/PIM/Conversion/SpatialToPim/Patterns.cpp index 68ede56..c450e0b 100644 --- a/src/PIM/Conversion/SpatialToPim/Patterns.cpp +++ b/src/PIM/Conversion/SpatialToPim/Patterns.cpp @@ -49,6 +49,16 @@ struct LowerFragmentAssemblyBlueprintPattern op, rank, fragmentOperands.size(), operandIndices, sourceOffsets, flatOffsets, flatSizes, flatStrides))) return failure(); + if (op.getIndexMap() == spatial::kContiguousRowMajorFragments) { + if (!spatial::isCanonicalContiguousRowMajorFragmentAssembly(op)) + return op.emitOpError("contiguous row-major fragment physical source order or storage is not canonical"); + auto reshaped = reshapeContiguousRowMajorFragments( + rewriter, op.getLoc(), adaptor.getInput(), cast(resultType)); + if (failed(reshaped)) + return op.emitOpError("contiguous row-major fragment storage does not match its logical result"); + rewriter.replaceOp(op, *reshaped); + return success(); + } Value currentOutput = tensor::EmptyOp::create(rewriter, op.getLoc(), resultType.getShape(), resultType.getElementType()).getResult(); for (int64_t fragmentIndex = 0; fragmentIndex < static_cast(operandIndices.size()); ++fragmentIndex) { diff --git a/src/PIM/Dialect/Pim/Transforms/HostConstantFolding/Patterns/Constant.cpp b/src/PIM/Dialect/Pim/Transforms/HostConstantFolding/Patterns/Constant.cpp index ad557d3..cb4e22b 100644 --- a/src/PIM/Dialect/Pim/Transforms/HostConstantFolding/Patterns/Constant.cpp +++ b/src/PIM/Dialect/Pim/Transforms/HostConstantFolding/Patterns/Constant.cpp @@ -532,54 +532,74 @@ struct FoldConstantMemCpPattern final : OpRewritePattern { } }; -static bool isOne(Attribute value) { - if (auto floatValue = dyn_cast(value)) - return floatValue.getValue().isExactlyValue(1.0); - if (auto integerValue = dyn_cast(value)) - return integerValue.getValue() == 1; - return false; +enum class MultiplicationConstant { Other, Zero, One }; + +static MultiplicationConstant classifyMultiplicationConstant(Attribute value) { + if (auto floatValue = dyn_cast(value)) { + const APFloat& number = floatValue.getValue(); + if (number.isZero() && !number.isNegative()) + return MultiplicationConstant::Zero; + if (number.isExactlyValue(1.0)) + return MultiplicationConstant::One; + } + if (auto integerValue = dyn_cast(value)) { + if (integerValue.getValue().isZero()) + return MultiplicationConstant::Zero; + if (integerValue.getValue() == 1) + return MultiplicationConstant::One; + } + return MultiplicationConstant::Other; } -static bool isAllOneHostCopy(pim::PimMemCopyHostToDevOp copyOp, ModuleOp moduleOp, MemRefType copiedType) { +static MultiplicationConstant classifyUniformHostCopy( + pim::PimMemCopyHostToDevOp copyOp, ModuleOp moduleOp, MemRefType copiedType) { auto targetOffset = resolveIndexValue(copyOp.getDeviceTargetOffset()); auto sourceOffset = resolveIndexValue(copyOp.getHostSourceOffset()); if (failed(targetOffset) || failed(sourceOffset) || *targetOffset != 0) - return false; + return MultiplicationConstant::Other; Type elementType = copiedType.getElementType(); if (!elementType.isIntOrFloat()) - return false; + return MultiplicationConstant::Other; unsigned bitWidth = elementType.getIntOrFloatBitWidth(); if (bitWidth == 0 || bitWidth % 8 != 0) - return false; + return MultiplicationConstant::Other; int64_t elementBytes = bitWidth / 8; int64_t copiedElements = copiedType.getNumElements(); if (*sourceOffset % elementBytes != 0 || copyOp.getSize() != copiedElements * elementBytes) - return false; + return MultiplicationConstant::Other; auto source = getDenseGlobalValue(moduleOp, copyOp.getHostSource()); if (failed(source) || source->getElementType() != elementType) - return false; + return MultiplicationConstant::Other; int64_t firstElement = *sourceOffset / elementBytes; int64_t endElement = firstElement + copiedElements; if (firstElement < 0 || endElement > source->getNumElements()) - return false; + return MultiplicationConstant::Other; if (source->isSplat()) - return isOne(source->getSplatValue()); + return classifyMultiplicationConstant(source->getSplatValue()); + MultiplicationConstant classification = MultiplicationConstant::Other; int64_t index = 0; for (Attribute value : source->getValues()) { - if (index >= firstElement && index < endElement && !isOne(value)) - return false; + if (index >= firstElement && index < endElement) { + MultiplicationConstant current = classifyMultiplicationConstant(value); + if (current == MultiplicationConstant::Other) + return current; + if (classification == MultiplicationConstant::Other) + classification = current; + else if (classification != current) + return MultiplicationConstant::Other; + } if (++index >= endElement) break; } - return true; + return classification; } -struct FoldMultiplyByOnePattern final : OpRewritePattern { +struct FoldMultiplyByConstantPattern final : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(pim::PimVVMulOp mulOp, PatternRewriter& rewriter) const override { @@ -605,14 +625,19 @@ struct FoldMultiplyByOnePattern final : OpRewritePattern { copyOp = candidate; } auto maskType = dyn_cast(mask.getType()); - if (!copyOp || !copyOp.use_empty() || !maskType || !isAllOneHostCopy(copyOp, moduleOp, maskType)) + if (!copyOp || !copyOp.use_empty() || !maskType) + continue; + MultiplicationConstant constant = classifyUniformHostCopy(copyOp, moduleOp, maskType); + if (constant == MultiplicationConstant::Other) continue; auto outputAlloc = mulOp.getOutputBuffer().getDefiningOp(); - rewriter.replaceOp(mulOp, input); - rewriter.eraseOp(copyOp); - if (maskAlloc.use_empty()) - rewriter.eraseOp(maskAlloc); + rewriter.replaceOp(mulOp, constant == MultiplicationConstant::One ? input : mask); + if (constant == MultiplicationConstant::One) { + rewriter.eraseOp(copyOp); + if (maskAlloc.use_empty()) + rewriter.eraseOp(maskAlloc); + } if (outputAlloc && outputAlloc.use_empty()) rewriter.eraseOp(outputAlloc); return success(); @@ -629,7 +654,7 @@ void populateConstantFoldingConstantPatterns(RewritePatternSet& patterns) { FoldConstantCoreMapPattern, FoldConstantHostCopyPattern, FoldConstantMemCpPattern, - FoldMultiplyByOnePattern>(patterns.getContext()); + FoldMultiplyByConstantPattern>(patterns.getContext()); } } // namespace onnx_mlir diff --git a/src/PIM/Dialect/Spatial/Spatial.td b/src/PIM/Dialect/Spatial/Spatial.td index 00af2bc..22f099f 100644 --- a/src/PIM/Dialect/Spatial/Spatial.td +++ b/src/PIM/Dialect/Spatial/Spatial.td @@ -303,6 +303,21 @@ def SpatSiluPlanOp : SpatOp<"silu_plan", []> { let hasVerifier = 1; } +def SpatResizeNearestPlanOp : SpatOp<"resize_nearest_plan", []> { + let summary = "Layout-aware nearest asymmetric Resize planning op"; + + let arguments = (ins + SpatTensor:$input, + StrAttr:$logicalLayout + ); + + let results = (outs + SpatTensor:$output + ); + + let hasVerifier = 1; +} + def SpatMaxPool2DPlanOp : SpatOp<"max_pool2d_plan", []> { let summary = "Layout-aware 2D NCHW MaxPool planning op"; diff --git a/src/PIM/Dialect/Spatial/SpatialOps.cpp b/src/PIM/Dialect/Spatial/SpatialOps.cpp index 6acd8b5..166b8a8 100644 --- a/src/PIM/Dialect/Spatial/SpatialOps.cpp +++ b/src/PIM/Dialect/Spatial/SpatialOps.cpp @@ -11,6 +11,70 @@ using namespace mlir; namespace onnx_mlir { namespace spatial { +bool hasCanonicalContiguousRowMajorFragments(RankedTensorType logicalType, + ArrayRef offsets, + ArrayRef sizes, + ArrayRef strides) { + if (!logicalType || !logicalType.hasStaticShape() || logicalType.getRank() <= 0 + || logicalType.getDimSize(logicalType.getRank() - 1) <= 0) + return false; + const int64_t rank = logicalType.getRank(); + const int64_t rowCount = logicalType.getNumElements() / logicalType.getDimSize(rank - 1); + if (offsets.size() != static_cast(rowCount * rank) || sizes.size() != offsets.size() + || strides.size() != offsets.size()) + return false; + for (int64_t row = 0; row < rowCount; ++row) { + int64_t remaining = row; + for (int64_t dim = rank - 2; dim >= 0; --dim) { + const int64_t index = row * rank + dim; + if (offsets[index] != remaining % logicalType.getDimSize(dim) || sizes[index] != 1 || strides[index] != 1) + return false; + remaining /= logicalType.getDimSize(dim); + } + const int64_t last = row * rank + rank - 1; + if (offsets[last] != 0 || sizes[last] != logicalType.getDimSize(rank - 1) || strides[last] != 1) + return false; + } + return true; +} + +bool isCanonicalContiguousRowMajorFragmentAssembly(SpatBlueprintOp blueprint) { + auto logicalType = dyn_cast(blueprint.getOutput().getType()); + auto physicalType = dyn_cast(blueprint.getInput().getType()); + auto operandIndices = blueprint.getFragmentOperandIndices(); + auto sourceSlots = blueprint.getFragmentSourceSlots(); + auto sourceOffsets = blueprint.getFragmentSourceOffsets(); + auto fragmentStrides = blueprint.getFragmentStrides(); + if (!logicalType || !physicalType || !logicalType.hasStaticShape() || !physicalType.hasStaticShape() + || logicalType.getRank() < 2 || !blueprint.getFragments().empty() + || blueprint.getMode() != "fragment_assembly" || !operandIndices || !sourceSlots || !sourceOffsets + || !fragmentStrides) + return false; + + ArrayRef offsets = blueprint.getFragmentOffsets(); + ArrayRef sizes = blueprint.getFragmentSizes(); + if (!hasCanonicalContiguousRowMajorFragments(logicalType, offsets, sizes, *fragmentStrides) + || operandIndices->empty() || operandIndices->size() != sourceSlots->size() + || operandIndices->size() != sourceOffsets->size() + || operandIndices->size() * static_cast(logicalType.getRank()) != offsets.size() + || physicalType.getRank() != logicalType.getRank() + 1 + || physicalType.getDimSize(0) != static_cast(operandIndices->size()) + || physicalType.getDimSize(0) + != logicalType.getNumElements() / logicalType.getDimSize(logicalType.getRank() - 1) + || physicalType.getElementType() != logicalType.getElementType() + || physicalType.getNumElements() != logicalType.getNumElements() + || physicalType.getDimSize(physicalType.getRank() - 1) != logicalType.getDimSize(logicalType.getRank() - 1) + || llvm::any_of(physicalType.getShape().slice(1, physicalType.getRank() - 2), + [](int64_t dim) { return dim != 1; })) + return false; + + for (auto [fragmentIndex, operandIndex] : llvm::enumerate(*operandIndices)) + if (operandIndex != 0 || (*sourceSlots)[fragmentIndex] != static_cast(fragmentIndex) + || (*sourceOffsets)[fragmentIndex] != 0) + return false; + return true; +} + RankedTensorType getGraphBatchPhysicalResultType(int64_t laneCount, RankedTensorType fragmentType) { SmallVector shape {laneCount}; llvm::append_range(shape, fragmentType.getShape()); diff --git a/src/PIM/Dialect/Spatial/SpatialOps.hpp b/src/PIM/Dialect/Spatial/SpatialOps.hpp index 222766e..0953af3 100644 --- a/src/PIM/Dialect/Spatial/SpatialOps.hpp +++ b/src/PIM/Dialect/Spatial/SpatialOps.hpp @@ -30,6 +30,15 @@ namespace onnx_mlir { namespace spatial { +inline constexpr llvm::StringLiteral kContiguousRowMajorFragments = "contiguous_row_major_fragments"; + +bool hasCanonicalContiguousRowMajorFragments(mlir::RankedTensorType logicalType, + llvm::ArrayRef offsets, + llvm::ArrayRef sizes, + llvm::ArrayRef strides); + +bool isCanonicalContiguousRowMajorFragmentAssembly(SpatBlueprintOp blueprint); + mlir::RankedTensorType getGraphBatchPhysicalResultType(int64_t laneCount, mlir::RankedTensorType fragmentType); mlir::FailureOr getGraphBatchFragmentType(mlir::RankedTensorType physicalType, int64_t expectedLaneCount); diff --git a/src/PIM/Dialect/Spatial/SpatialOpsVerify.cpp b/src/PIM/Dialect/Spatial/SpatialOpsVerify.cpp index 09f7eed..7001d62 100644 --- a/src/PIM/Dialect/Spatial/SpatialOpsVerify.cpp +++ b/src/PIM/Dialect/Spatial/SpatialOpsVerify.cpp @@ -480,6 +480,23 @@ LogicalResult SpatSiluPlanOp::verify() { return success(); } +LogicalResult SpatResizeNearestPlanOp::verify() { + if (failed(verifyPlanTensorTypes( + getOperation(), getInput(), getOutput(), "spat.resize_nearest_plan"))) + return failure(); + auto inputType = dyn_cast(getInput().getType()); + auto outputType = dyn_cast(getOutput().getType()); + if (!inputType.hasStaticShape() || !outputType.hasStaticShape() + || inputType.getRank() != 4 || outputType.getRank() != 4) + return emitError("requires static rank-4 input and output tensors"); + if (getLogicalLayout() != "nchw") + return emitError("requires logical layout \"nchw\""); + if (llvm::any_of(inputType.getShape(), [](int64_t dim) { return dim <= 0; }) + || llvm::any_of(outputType.getShape(), [](int64_t dim) { return dim <= 0; })) + return emitError("requires positive dimensions"); + return success(); +} + LogicalResult SpatMaxPool2DPlanOp::verify() { if (failed(verifyPlanTensorTypes(getOperation(), getInput(), getOutput(), "spat.max_pool2d_plan"))) return failure(); @@ -587,8 +604,12 @@ LogicalResult SpatBlueprintOp::verify() { if (offsets.size() != sizes.size()) return emitError("fragment offset and size arrays must have the same length"); int64_t rank = logicalType.getRank(); - if (offsets.empty()) + bool isContiguousRowMajor = getIndexMap() == kContiguousRowMajorFragments; + if (offsets.empty()) { + if (isContiguousRowMajor) + return emitError("contiguous row-major fragment destination geometry is not canonical"); return success(); + } if (rank <= 0 || offsets.size() % rank != 0) return emitError("fragment metadata must be a whole number of rank-sized fragments"); @@ -611,6 +632,8 @@ LogicalResult SpatBlueprintOp::verify() { }; if (!isFragmentAssembly) { + if (isContiguousRowMajor) + return emitError("contiguous row-major fragments require fragment assembly metadata"); if (failed(verifyBoundsOnly({}))) return failure(); if (!getFragments().empty()) @@ -662,6 +685,13 @@ LogicalResult SpatBlueprintOp::verify() { if (failed(verifyBoundsOnly(strides))) return failure(); + if (isContiguousRowMajor) { + if (!hasCanonicalContiguousRowMajorFragments(logicalType, offsets, sizes, strides)) + return emitError("contiguous row-major fragment destination geometry is not canonical"); + if (!isCanonicalContiguousRowMajorFragmentAssembly(*this)) + return emitError("contiguous row-major fragment physical source order or storage is not canonical"); + } + SmallVector, SmallVector>, 8> slices; slices.reserve(static_cast(fragmentCount)); SmallVector fragmentCountsByOperand(static_cast(operandCount), 0); @@ -713,20 +743,22 @@ LogicalResult SpatBlueprintOp::verify() { if (sourceSliceOffsets[dim] + fragmentSizes[dim] > fragmentType.getDimSize(dim)) return emitError("fragment assembly source offset must describe a valid unit-stride slice"); - for (const auto& [existingOffsets, existingSizes] : slices) { - bool overlaps = true; - for (int64_t dim = 0; dim < rank; ++dim) { - int64_t begin = fragmentOffsets[dim]; - int64_t end = begin + fragmentSizes[dim]; - int64_t existingBegin = existingOffsets[dim]; - int64_t existingEnd = existingBegin + existingSizes[dim]; - if (end <= existingBegin || existingEnd <= begin) { - overlaps = false; - break; + if (!isContiguousRowMajor) { + for (const auto& [existingOffsets, existingSizes] : slices) { + bool overlaps = true; + for (int64_t dim = 0; dim < rank; ++dim) { + int64_t begin = fragmentOffsets[dim]; + int64_t end = begin + fragmentSizes[dim]; + int64_t existingBegin = existingOffsets[dim]; + int64_t existingEnd = existingBegin + existingSizes[dim]; + if (end <= existingBegin || existingEnd <= begin) { + overlaps = false; + break; + } } + if (overlaps) + return emitError("fragment assembly blueprint requires disjoint static slices"); } - if (overlaps) - return emitError("fragment assembly blueprint requires disjoint static slices"); } slices.push_back({std::move(fragmentOffsets), std::move(fragmentSizes)}); } diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationPlanning.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationPlanning.cpp index e6846b2..0fc3985 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationPlanning.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationPlanning.cpp @@ -9,6 +9,7 @@ #include "llvm/ADT/SmallPtrSet.h" #include "src/Accelerators/PIM/Common/IR/AffineUtils.hpp" +#include "src/Accelerators/PIM/Common/IR/LoopUtils.hpp" #include "src/Accelerators/PIM/Common/IR/ShapingUtils.hpp" #include "src/Accelerators/PIM/Common/IR/StaticIntSequence.hpp" #include "src/Accelerators/PIM/Common/IR/TensorSliceUtils.hpp" @@ -37,6 +38,63 @@ static SmallVector getBlueprintFragments(SpatBlueprintOp blueprint) { return fragments; } +static FailureOr buildContiguousRowMajorReconstruction( + OpBuilder &builder, Location loc, SpatBlueprintOp blueprint, + Value source) { + auto resultType = dyn_cast(blueprint.getOutput().getType()); + auto sourceType = dyn_cast(source.getType()); + if (!resultType || !sourceType || !resultType.hasStaticShape() + || !sourceType.hasStaticShape() || resultType.getRank() <= 0 + || sourceType.getRank() != resultType.getRank() + 1) + return failure(); + int64_t rank = resultType.getRank(); + int64_t width = resultType.getDimSize(rank - 1); + int64_t rowCount = resultType.getNumElements() / width; + if (sourceType.getDimSize(0) != rowCount || sourceType.getDimSize(rank) != width) + return failure(); + + auto flatType = RankedTensorType::get({rowCount, width}, resultType.getElementType()); + auto rowType = RankedTensorType::get({1, width}, resultType.getElementType()); + auto physicalRowType = RankedTensorType::get(sourceType.getShape().drop_front(), resultType.getElementType()); + Value init = tensor::EmptyOp::create(builder, loc, flatType.getShape(), flatType.getElementType()); + Value c0 = arith::ConstantIndexOp::create(builder, loc, 0); + Value c1 = arith::ConstantIndexOp::create(builder, loc, 1); + Value rows = arith::ConstantIndexOp::create(builder, loc, rowCount); + auto loop = buildNormalizedScfFor( + builder, loc, c0, rows, c1, ValueRange {init}, + [&](OpBuilder &nested, Location nestedLoc, Value row, ValueRange iterArgs, + SmallVectorImpl &yielded) { + SmallVector offsets {row}; + SmallVector sizes {nested.getIndexAttr(1)}; + SmallVector strides {nested.getIndexAttr(1)}; + for (int64_t dim : physicalRowType.getShape()) { + offsets.push_back(nested.getIndexAttr(0)); + sizes.push_back(nested.getIndexAttr(dim)); + strides.push_back(nested.getIndexAttr(1)); + } + Value physicalRow = tensor::ExtractSliceOp::create( + nested, nestedLoc, physicalRowType, source, offsets, sizes, strides); + SmallVector collapse {{}}; + for (int64_t dim = 0; dim < rank - 1; ++dim) + collapse.front().push_back(dim); + collapse.push_back({rank - 1}); + Value flatRow = tensor::CollapseShapeOp::create( + nested, nestedLoc, rowType, physicalRow, collapse); + yielded.push_back(tensor::InsertSliceOp::create( + nested, nestedLoc, flatRow, iterArgs.front(), + SmallVector {row, nested.getIndexAttr(0)}, + SmallVector {nested.getIndexAttr(1), nested.getIndexAttr(width)}, + SmallVector {nested.getIndexAttr(1), nested.getIndexAttr(1)})); + return success(); + }); + if (failed(loop)) + return failure(); + SmallVector expand {{}, {rank - 1}}; + for (int64_t dim = 0; dim < rank - 1; ++dim) + expand.front().push_back(dim); + return tensor::ExpandShapeOp::create(builder, loc, resultType, loop->results.front(), expand).getResult(); +} + static FailureOr buildBlueprintReconstruction( OpBuilder &builder, Location loc, SpatBlueprintOp blueprint, ValueRange sourceBlockArgs) { @@ -57,6 +115,13 @@ static FailureOr buildBlueprintReconstruction( sourceOffsets->size() != operandIndices->size()) return blueprint.emitOpError("phase 1 fragment assembly metadata has inconsistent sizes"), failure(); + if (blueprint.getIndexMap() == kContiguousRowMajorFragments) { + if (!isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + return blueprint.emitOpError("contiguous row-major fragment physical source order or storage is not canonical"), failure(); + if (sourceBlockArgs.size() != 1) + return blueprint.emitOpError("contiguous row-major fragment reconstruction requires one physical source"), failure(); + return buildContiguousRowMajorReconstruction(builder, loc, blueprint, sourceBlockArgs.front()); + } Value result = tensor::EmptyOp::create(builder, loc, resultType.getShape(), resultType.getElementType()); for (auto [fragmentIndex, operandIndex] : llvm::enumerate(*operandIndices)) { @@ -178,6 +243,15 @@ static Operation *getTopLevelDeferredOperation( return op && isTopLevelDeferredOperation(op, body, plan) ? op : nullptr; } +static bool isDefinedInside(Operation *owner, Value value) { + if (Operation *definition = value.getDefiningOp()) + return owner->isProperAncestor(definition); + auto argument = dyn_cast(value); + Region *region = argument ? argument.getOwner()->getParent() : nullptr; + return region == &owner->getRegion(0) + || (region && owner->getRegion(0).isAncestor(region)); +} + static bool isEligible(Value value, Block &body, const DeferredInputPlan &plan, llvm::SmallPtrSetImpl &seen) { if (value == plan.graphInput || value == plan.graphLane || value == plan.scheduledLane) @@ -195,18 +269,10 @@ static bool isEligible(Value value, Block &body, const DeferredInputPlan &plan, loop.getRegion().walk([&](Operation *nested) { if (isa(nested) && nested != loop) eligible = false; - for (Value operand : nested->getOperands()) { - Operation *definition = operand.getDefiningOp(); - auto argument = dyn_cast(operand); - Region *argumentRegion = argument - ? argument.getOwner()->getParent() : nullptr; - bool definedInside = definition - ? loop->isProperAncestor(definition) - : argumentRegion == &loop.getRegion() - || (argumentRegion && loop.getRegion().isAncestor(argumentRegion)); - if (!definedInside && !isEligible(operand, body, plan, seen)) + for (Value operand : nested->getOperands()) + if (!isDefinedInside(loop, operand) + && !isEligible(operand, body, plan, seen)) eligible = false; - } }); if (!eligible) return false; @@ -267,19 +333,9 @@ static FailureOr clonePayloadRoot(Value root, Block &body, const Deferred if (auto loop = dyn_cast(op)) { SmallVector captures; loop.getRegion().walk([&](Operation *nested) { - for (Value operand : nested->getOperands()) { - Operation *definition = operand.getDefiningOp(); - auto argument = dyn_cast(operand); - Region *argumentRegion = argument - ? argument.getOwner()->getParent() : nullptr; - bool definedInside = definition - ? loop->isProperAncestor(definition) - : argumentRegion == &loop.getRegion() - || (argumentRegion - && loop.getRegion().isAncestor(argumentRegion)); - if (!definedInside && !mapping.contains(operand)) + for (Value operand : nested->getOperands()) + if (!isDefinedInside(loop, operand) && !mapping.contains(operand)) captures.push_back(operand); - } }); for (Value capture : captures) if (!mapping.contains(capture) && failed(clone(capture))) @@ -304,7 +360,10 @@ static bool dependsOnGraphLane(Value value, Value graphLane, Block &body, if (auto loop = dyn_cast(op)) { bool depends = false; loop.getRegion().walk([&](Operation *nested) { - depends |= llvm::is_contained(nested->getOperands(), graphLane); + for (Value operand : nested->getOperands()) + if (!isDefinedInside(loop, operand) + && dependsOnGraphLane(operand, graphLane, body, plan, seen)) + depends = true; }); if (depends) return true; diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp index 8af6bf9..cdf9f11 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredCommunicationRealization.cpp @@ -14,6 +14,26 @@ namespace onnx_mlir::spatial { using namespace mlir; namespace { +static LogicalResult verifyNoEscapingRegionValues(Operation* owner, StringRef phase) { + Operation* escapingDefinition = nullptr; + Operation* escapingUser = nullptr; + owner->walk([&](Operation* nested) { + for (Value result : nested->getResults()) + for (Operation* user : result.getUsers()) + if (!owner->isProperAncestor(user)) { + escapingDefinition = nested; + escapingUser = user; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + if (!escapingDefinition) + return success(); + return owner->emitOpError() << phase << " left a value defined by " << escapingDefinition->getName() + << " at " << escapingDefinition->getLoc() << " captured by " + << escapingUser->getName() << " at " << escapingUser->getLoc(); +} + static LogicalResult placeLogicalProcessorsOnPhysicalCores(DeferredTransferPlan& plan, const SchedulingTarget& target) { std::vector logicalTrafficFlits(target.processorCount * target.processorCount, 0); for (const std::unique_ptr& exchange : plan.exchanges) @@ -138,6 +158,8 @@ static LogicalResult eraseOldGraph(func::FuncOp funcOp, IRRewriter& rewriter) { } } } + if (failed(verifyNoEscapingRegionValues(op, "phase 2"))) + return failure(); rewriter.eraseOp(op); } return success(); @@ -217,6 +239,8 @@ LogicalResult realizeDeferredCommunication(func::FuncOp funcOp, op->getResult(0).replaceAllUsesWith(replacement); if (!op->use_empty()) return op->emitOpError("phase 2 cannot erase deferred communication with live uses"); + if (failed(verifyNoEscapingRegionValues(op, "phase 2 deferred communication"))) + return failure(); rewriter.eraseOp(op); } if (failed(eraseDeferredSourceSelectors(funcOp, rewriter)) || failed(eraseOldGraph(funcOp, rewriter)) diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.cpp index 42ae3a1..20551ec 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredProjectionAnalysis.cpp @@ -180,13 +180,18 @@ static bool originatesFromDeferredSource( return originatesFromDeferredSource(value, deferred, visited); } -static bool isInsideDeferredLoop(Operation *op, - SpatDeferredCommunicationOp deferred) { +static scf::ForOp getEnclosingDeferredLoop( + Operation *op, SpatDeferredCommunicationOp deferred) { for (Operation *parent = op->getParentOp(); parent && parent != deferred; parent = parent->getParentOp()) - if (isa(parent)) - return true; - return false; + if (auto loop = dyn_cast(parent)) + return loop; + return {}; +} + +static bool isInsideDeferredLoop( + Operation *op, SpatDeferredCommunicationOp deferred) { + return static_cast(getEnclosingDeferredLoop(op, deferred)); } static FailureOr getLoopIterationCount( @@ -297,7 +302,7 @@ static LogicalResult validateDeferredProgram( && llvm::any_of(op->getOperands(), [&](Value operand) { return originatesFromDeferredSource(operand, deferred); })) { - auto loop = op->getParentOfType(); + auto loop = getEnclosingDeferredLoop(op, deferred); scf::ForOp outerLoop; for (Operation *parent = loop ? loop->getParentOp() : nullptr; parent && parent != deferred; parent = parent->getParentOp()) @@ -578,7 +583,7 @@ FailureOr analyzeDeferredProgramTemplate( SmallVector( ArrayRef(slice.getMixedStrides()).drop_front())}; leaf.reconstructedType = cast(value.getType()); - leaf.enclosingLoop = slice->getParentOfType(); + leaf.enclosingLoop = getEnclosingDeferredLoop(slice, deferred); if (graphProjection && slice.getSourceType().getRank() == leaf.reconstructedType.getRank() + 1 @@ -609,8 +614,11 @@ FailureOr analyzeDeferredProgramTemplate( program.leaves.push_back(std::move(leaf)); return success(); } - if (value.getType().isIndex() || isa(value.getType())) - return success(); + if (value.getType().isIndex() || isa(value.getType())) { + Operation *definition = value.getDefiningOp(); + if (!definition || !deferred->isProperAncestor(definition)) + return success(); + } if (auto argument = dyn_cast(value)) { auto loop = dyn_cast_or_null( argument.getOwner()->getParentOp()); @@ -619,7 +627,7 @@ FailureOr analyzeDeferredProgramTemplate( } Operation *op = value.getDefiningOp(); if (!op || (op->getBlock() != &body - && !op->getParentOfType())) + && !getEnclosingDeferredLoop(op, deferred))) return deferred.emitOpError( "deferred residual escapes its verified body: ") << value; if (auto loop = dyn_cast(op)) { diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredResultRealization.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredResultRealization.cpp index 6cdb525..dbd01a1 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredResultRealization.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredResultRealization.cpp @@ -289,7 +289,9 @@ static Value cloneResidual( mapping.map(oldValue, newValue); } for (Operation *op : exchange.program.residualOps) { - if (op->hasTrait()) + if (op->hasTrait() + || llvm::all_of(op->getResults(), + [&](Value result) { return mapping.contains(result); })) continue; if (auto oldLoop = dyn_cast(op)) { SmallVector initArgs; diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredTransferPlanning.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredTransferPlanning.cpp index f6e554b..fcb6729 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredTransferPlanning.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/DeferredTransferPlanning.cpp @@ -454,6 +454,9 @@ retargetBlueprint(DeferredTransferPlan& plan, SpatBlueprintOp blueprint, GraphBa OpBuilder builder(blueprint); blueprint->setAttr("fragmentOperandIndices", builder.getDenseI64ArrayAttr(newOperands)); blueprint->setAttr("fragmentSourceSlots", builder.getDenseI64ArrayAttr(newSlots)); + if (blueprint.getIndexMap() == kContiguousRowMajorFragments + && !isCanonicalContiguousRowMajorFragmentAssembly(blueprint)) + blueprint.setIndexMapAttr(builder.getStringAttr("fragment_assembly")); return success(); } diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.cpp index 66d35ea..0f0a5dd 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/Scheduling/PeftScheduler.cpp @@ -400,6 +400,7 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu std::vector processorResidentWeights(processorCount); std::vector schedules(nodeCount); std::vector> tasksByProcessor(processorCount); + std::vector> timelineByProcessor(processorCount); size_t scheduledCount = 0; while (!readyQueue.empty()) { @@ -441,7 +442,7 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu Time currentEnd = 0; bool foundGap = false; - for (size_t schedTaskIndex : tasksByProcessor[processor]) { + for (size_t schedTaskIndex : timelineByProcessor[processor]) { const ScheduledTask& schedTask = schedules[schedTaskIndex]; Time gapStart = std::max(currentEnd, dataReady); @@ -532,10 +533,13 @@ MergeScheduleResult runPeftScheduler(const ComputeGraph& graph, const PeftSchedu insertResidentWeights(capacityReservations[bestProcessor], graph.nodes[task].residentWeights); insertResidentWeights(processorResidentWeights[bestProcessor], graph.nodes[task].residentWeights); - // 3. CRITICAL FIX: Topological Append - // Because the readyQueue pops in strict topological order, simply pushing to the - // back guarantees the Monoliths will be physically generated cycle-free. - // The hardware will still benefit from the processor assignment chosen by PEFT. + auto& timeline = timelineByProcessor[bestProcessor]; + timeline.insert(llvm::upper_bound(timeline, task, [&](size_t lhs, size_t rhs) { + return schedules[lhs].startTime < schedules[rhs].startTime; + }), task); + + // Materialization requires topological order; gap placement requires the + // separate chronological timeline above. tasksByProcessor[bestProcessor].push_back(task); for (const auto& [child, weight] : graph.successors[task]) { diff --git a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.cpp b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.cpp index 301c3b3..1ab3838 100644 --- a/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.cpp +++ b/src/PIM/Dialect/Spatial/Transforms/MergeComputeNodes/SpatialDataflowCsvExporter.cpp @@ -1,5 +1,6 @@ #include "mlir/Dialect/Affine/IR/AffineOps.h" #include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/IR/AsmState.h" #include "mlir/IR/BuiltinAttributes.h" @@ -16,6 +17,7 @@ #include #include +#include #include #include #include @@ -54,6 +56,12 @@ struct ChannelSendRecord { std::optional sourceLane; }; +struct ChannelEvaluationContext { + Value laneArg; + uint32_t lane = 0; + DenseMap bindings; +}; + enum class LogicalNodeSelector { Scalar, Lane, @@ -249,10 +257,20 @@ void addBatchNodeRows(std::fstream& nodesFile, } } -std::optional evaluateIndexLike(Value value, Value laneArg, uint32_t lane); +std::optional evaluateIndexLike(Value value, + Value laneArg, + uint32_t lane, + const DenseMap* bindings); -std::optional evaluateIndexLike(Value value, Value laneArg, uint32_t lane) { - if (value == laneArg) +std::optional evaluateIndexLike(Value value, + Value laneArg, + uint32_t lane, + const DenseMap* bindings) { + if (bindings) + if (auto it = bindings->find(value); it != bindings->end()) + return it->second; + + if (laneArg && value == laneArg) return static_cast(lane); if (std::optional constant = matchConstantIndexValue(value)) @@ -270,7 +288,8 @@ std::optional evaluateIndexLike(Value value, Value laneArg, uint32_t la if (!elements || !shapedType || shapedType.getRank() != 1 || extract.getIndices().size() != 1) return std::nullopt; - std::optional index = evaluateIndexLike(extract.getIndices().front(), laneArg, lane); + std::optional index = + evaluateIndexLike(extract.getIndices().front(), laneArg, lane, bindings); if (!index || *index < 0 || *index >= static_cast(elements.getNumElements())) return std::nullopt; @@ -279,11 +298,104 @@ std::optional evaluateIndexLike(Value value, Value laneArg, uint32_t la return std::nullopt; } + if (auto indexCast = value.getDefiningOp()) + return evaluateIndexLike(indexCast.getIn(), laneArg, lane, bindings); + + if (auto add = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(add.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(add.getRhs(), laneArg, lane, bindings); + if (lhs && rhs) + return *lhs + *rhs; + return std::nullopt; + } + + if (auto sub = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(sub.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(sub.getRhs(), laneArg, lane, bindings); + if (lhs && rhs) + return *lhs - *rhs; + return std::nullopt; + } + + if (auto mul = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(mul.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(mul.getRhs(), laneArg, lane, bindings); + if (lhs && rhs) + return *lhs * *rhs; + return std::nullopt; + } + + if (auto div = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(div.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(div.getRhs(), laneArg, lane, bindings); + if (!lhs || !rhs || *rhs == 0 + || (*lhs == std::numeric_limits::min() && *rhs == -1)) + return std::nullopt; + return *lhs / *rhs; + } + + if (auto div = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(div.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(div.getRhs(), laneArg, lane, bindings); + if (!lhs || !rhs || *rhs == 0) + return std::nullopt; + return static_cast(static_cast(*lhs) / static_cast(*rhs)); + } + + if (auto rem = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(rem.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(rem.getRhs(), laneArg, lane, bindings); + if (!lhs || !rhs || *rhs == 0) + return std::nullopt; + if (*lhs == std::numeric_limits::min() && *rhs == -1) + return 0; + return *lhs % *rhs; + } + + if (auto rem = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(rem.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(rem.getRhs(), laneArg, lane, bindings); + if (!lhs || !rhs || *rhs == 0) + return std::nullopt; + return static_cast(static_cast(*lhs) % static_cast(*rhs)); + } + + if (auto cmp = value.getDefiningOp()) { + auto lhs = evaluateIndexLike(cmp.getLhs(), laneArg, lane, bindings); + auto rhs = evaluateIndexLike(cmp.getRhs(), laneArg, lane, bindings); + if (!lhs || !rhs) + return std::nullopt; + bool result = false; + switch (cmp.getPredicate()) { + case arith::CmpIPredicate::eq: result = *lhs == *rhs; break; + case arith::CmpIPredicate::ne: result = *lhs != *rhs; break; + case arith::CmpIPredicate::slt: result = *lhs < *rhs; break; + case arith::CmpIPredicate::sle: result = *lhs <= *rhs; break; + case arith::CmpIPredicate::sgt: result = *lhs > *rhs; break; + case arith::CmpIPredicate::sge: result = *lhs >= *rhs; break; + case arith::CmpIPredicate::ult: result = static_cast(*lhs) < static_cast(*rhs); break; + case arith::CmpIPredicate::ule: result = static_cast(*lhs) <= static_cast(*rhs); break; + case arith::CmpIPredicate::ugt: result = static_cast(*lhs) > static_cast(*rhs); break; + case arith::CmpIPredicate::uge: result = static_cast(*lhs) >= static_cast(*rhs); break; + } + return result ? 1 : 0; + } + + if (auto select = value.getDefiningOp()) { + auto condition = evaluateIndexLike(select.getCondition(), laneArg, lane, bindings); + if (!condition) + return std::nullopt; + return evaluateIndexLike(*condition ? select.getTrueValue() : select.getFalseValue(), + laneArg, + lane, + bindings); + } + if (auto affineApply = value.getDefiningOp()) if (FailureOr folded = evaluateAffineApply(affineApply, [&](Value operand) -> FailureOr { if (std::optional resolved = - evaluateIndexLike(operand, laneArg, lane)) + evaluateIndexLike(operand, laneArg, lane, bindings)) return *resolved; return failure(); }); @@ -294,24 +406,70 @@ std::optional evaluateIndexLike(Value value, Value laneArg, uint32_t la return std::nullopt; } -SmallVector collectPossibleIntValues(Value value, Value laneArg, uint32_t lane) { - if (std::optional exact = evaluateIndexLike(value, laneArg, lane)) - return {*exact}; +bool containsChannelOperation(Operation* root) { + bool found = false; + root->walk([&](Operation* op) { + found |= isa(op); + }); + return found; +} - auto extract = value.getDefiningOp(); - auto constant = extract ? extract.getTensor().getDefiningOp() : nullptr; - auto elements = constant ? dyn_cast(constant.getValue()) : nullptr; - if (!elements) - return {}; +template +LogicalResult walkChannelRegion(Region& region, const ChannelEvaluationContext& context, Emit& emit) { + if (region.empty()) + return success(); - SmallVector values; - if (auto denseInts = dyn_cast(elements)) { - values.reserve(elements.getNumElements()); - for (APInt element : denseInts.getValues()) - if (!llvm::is_contained(values, element.getSExtValue())) - values.push_back(element.getSExtValue()); + for (Operation& op : region.front()) { + if (auto ifOp = dyn_cast(&op)) { + auto condition = evaluateIndexLike( + ifOp.getCondition(), context.laneArg, context.lane, &context.bindings); + if (!condition) + return ifOp.emitOpError("has an unresolved condition in Spatial dataflow export"); + Region& selected = *condition ? ifOp.getThenRegion() : ifOp.getElseRegion(); + if (failed(walkChannelRegion(selected, context, emit))) + return failure(); + continue; + } + + if (auto forOp = dyn_cast(&op)) { + if (!containsChannelOperation(forOp)) + continue; + + auto lower = evaluateIndexLike(forOp.getLowerBound(), context.laneArg, context.lane, &context.bindings); + auto upper = evaluateIndexLike(forOp.getUpperBound(), context.laneArg, context.lane, &context.bindings); + auto step = evaluateIndexLike(forOp.getStep(), context.laneArg, context.lane, &context.bindings); + if (!lower || !upper || !step || *step == 0) + return forOp.emitOpError("has unresolved or invalid bounds in Spatial dataflow export"); + + constexpr uint64_t kMaxExportedLoopIterations = 1 << 20; + uint64_t iterationCount = 0; + int64_t induction = *lower; + while ((*step > 0 && induction < *upper) || (*step < 0 && induction > *upper)) { + if (++iterationCount > kMaxExportedLoopIterations) + return forOp.emitOpError("exceeds the bounded iteration limit in Spatial dataflow export"); + ChannelEvaluationContext iterationContext = context; + iterationContext.bindings[forOp.getInductionVar()] = induction; + if (failed(walkChannelRegion(forOp.getRegion(), iterationContext, emit))) + return failure(); + if ((*step > 0 && induction > std::numeric_limits::max() - *step) + || (*step < 0 && induction < std::numeric_limits::min() - *step)) + return forOp.emitOpError("overflows while enumerating Spatial dataflow export iterations"); + induction += *step; + } + continue; + } + + if (!isa(&op)) + continue; + auto channel = dyn_cast(&op); + Value channelValue = channel ? channel.getChannelId() : cast(&op).getChannelId(); + auto channelId = evaluateIndexLike(channelValue, context.laneArg, context.lane, &context.bindings); + if (!channelId) + return op.emitError("has an unresolved channel identity in Spatial dataflow export"); + if (failed(emit(op, *channelId, context))) + return failure(); } - return values; + return success(); } template @@ -604,50 +762,44 @@ LogicalResult emitDataEdges(std::fstream& edgesFile, } template -void collectChannelSends(DenseMap>& sendsByChannelId, - const DenseMap, ExpandedNodeInfo>& expandedNodes, - BatchOpTy batch) { +LogicalResult collectChannelSends(DenseMap>& sendsByChannelId, + const DenseMap, ExpandedNodeInfo>& expandedNodes, + BatchOpTy batch) { std::optional laneArg = batch.getLaneArgument(); if (!laneArg) - return; + return success(); for (uint32_t lane = 0; lane < static_cast(batch.getLaneCount()); ++lane) { std::string sourceId = getExpandedNodeId(expandedNodes, batch.getOperation(), lane); if (sourceId.empty()) continue; - batch.getBody().walk([&](SpatChannelSendOp send) { - std::optional channelId = evaluateIndexLike(send.getChannelId(), *laneArg, lane); - if (!channelId) - return; - sendsByChannelId[*channelId].push_back({sourceId, lane}); - }); + ChannelEvaluationContext context; + context.laneArg = *laneArg; + context.lane = lane; + auto emit = [&](Operation& op, int64_t channelId, const ChannelEvaluationContext&) { + if (auto send = dyn_cast(&op)) + sendsByChannelId[channelId].push_back({sourceId, lane}); + return success(); + }; + if (failed(walkChannelRegion(batch.getBody(), context, emit))) + return failure(); } + return success(); } -void collectChannelSends(DenseMap>& sendsByChannelId, - const DenseMap, ExpandedNodeInfo>& expandedNodes, - SpatScheduledCompute compute) { +LogicalResult collectChannelSends(DenseMap>& sendsByChannelId, + const DenseMap, ExpandedNodeInfo>& expandedNodes, + SpatScheduledCompute compute) { std::string sourceId = getExpandedNodeId(expandedNodes, compute.getOperation(), 0); if (sourceId.empty()) - return; - compute.getBody().walk([&](SpatChannelSendOp send) { - std::optional channelId = evaluateIndexLike(send.getChannelId(), Value(), 0); - if (!channelId) - return; - sendsByChannelId[*channelId].push_back({sourceId, std::nullopt}); - }); -} - -DenseMap> -buildNodesByCore(const DenseMap, ExpandedNodeInfo>& expandedNodes) { - DenseMap> nodesByCore; - for (const auto& entry : expandedNodes) { - const ExpandedNodeInfo& node = entry.second; - if (!node.core) - continue; - nodesByCore[*node.core].push_back({node.id, node.lane}); - } - return nodesByCore; + return success(); + ChannelEvaluationContext context; + auto emit = [&](Operation& op, int64_t channelId, const ChannelEvaluationContext&) { + if (isa(&op)) + sendsByChannelId[channelId].push_back({sourceId, std::nullopt}); + return success(); + }; + return walkChannelRegion(compute.getBody(), context, emit); } template @@ -660,14 +812,18 @@ LogicalResult emitExplicitChannelEdges(std::fstream& edgesFile, const TopLevelOpInfo& info = entry.second; if (auto compute = dyn_cast(op)) { - compute.getBody().walk([&](SpatChannelReceiveOp receive) { - SmallVector sources = resolveChannelSources(receive, 0); - if (sources.empty()) - return; - std::optional channelId = evaluateIndexLike(receive.getChannelId(), Value(), 0); + ChannelEvaluationContext context; + auto emit = [&](Operation& channelOp, int64_t channelId, const ChannelEvaluationContext&) { + auto receive = dyn_cast(&channelOp); + if (!receive) + return success(); + FailureOr> sources = + resolveChannelSources(receive, channelId, 0); + if (failed(sources)) + return failure(); std::string targetId = getScalarId(info.isScheduled, info.opId); std::optional byteSize = getTypeSizeBytes(receive.getType()); - for (const ChannelSendRecord& source : sources) + for (const ChannelSendRecord& source : *sources) emitEdgeRow(edgesFile, source.sourceId, targetId, @@ -677,7 +833,10 @@ LogicalResult emitExplicitChannelEdges(std::fstream& edgesFile, source.sourceLane, std::nullopt, channelId); - }); + return success(); + }; + if (failed(walkChannelRegion(compute.getBody(), context, emit))) + return failure(); continue; } @@ -688,14 +847,20 @@ LogicalResult emitExplicitChannelEdges(std::fstream& edgesFile, if (!laneArg) continue; for (uint32_t lane = 0; lane < static_cast(batch.getLaneCount()); ++lane) { - std::string targetId = getBatchLaneId(info.isScheduled, info.opId, lane); - batch.getBody().walk([&](SpatChannelReceiveOp receive) { - SmallVector sources = resolveChannelSources(receive, lane); - if (sources.empty()) - return; - std::optional channelId = evaluateIndexLike(receive.getChannelId(), *laneArg, lane); + ChannelEvaluationContext context; + context.laneArg = *laneArg; + context.lane = lane; + auto emit = [&](Operation& channelOp, int64_t channelId, const ChannelEvaluationContext& eventContext) { + auto receive = dyn_cast(&channelOp); + if (!receive) + return success(); + FailureOr> sources = + resolveChannelSources(receive, channelId, eventContext.lane); + if (failed(sources)) + return failure(); + std::string targetId = getBatchLaneId(info.isScheduled, info.opId, eventContext.lane); std::optional byteSize = getTypeSizeBytes(receive.getType()); - for (const ChannelSendRecord& source : sources) + for (const ChannelSendRecord& source : *sources) emitEdgeRow(edgesFile, source.sourceId, targetId, @@ -703,9 +868,12 @@ LogicalResult emitExplicitChannelEdges(std::fstream& edgesFile, receive.getType(), stage, source.sourceLane, - lane, + eventContext.lane, channelId); - }); + return success(); + }; + if (failed(walkChannelRegion(batch.getBody(), context, emit))) + return failure(); } } @@ -810,33 +978,25 @@ LogicalResult exportScheduled(func::FuncOp func, DenseMap> sendsByChannelId; for (const auto& entry : topLevelInfo) { Operation* op = entry.first; + LogicalResult collected = success(); if (auto compute = dyn_cast(op)) - collectChannelSends(sendsByChannelId, expandedNodes, compute); + collected = collectChannelSends(sendsByChannelId, expandedNodes, compute); else if (auto batch = dyn_cast(op)) - collectChannelSends(sendsByChannelId, expandedNodes, batch); + collected = collectChannelSends(sendsByChannelId, expandedNodes, batch); + if (failed(collected)) + return failure(); } - DenseMap> nodesByCore = buildNodesByCore(expandedNodes); - auto resolveChannelSources = [&](SpatChannelReceiveOp receive, uint32_t lane) { + DenseMap consumedSendsByChannelId; + auto resolveChannelSources = [&](SpatChannelReceiveOp receive, int64_t channelId, uint32_t) { SmallVector sources; - - Value laneArg; - if (auto owner = receive->getParentOfType()) - if (auto maybeLaneArg = owner.getLaneArgument()) - laneArg = *maybeLaneArg; - - if (std::optional channelId = evaluateIndexLike(receive.getChannelId(), laneArg, lane)) { - if (auto it = sendsByChannelId.find(*channelId); it != sendsByChannelId.end()) - return it->second; - } - - for (int64_t sourceCore : collectPossibleIntValues(receive.getSourceCoreId(), laneArg, lane)) { - auto it = nodesByCore.find(static_cast(sourceCore)); - if (it == nodesByCore.end()) - continue; - llvm::append_range(sources, it->second); - } - return sources; + auto sends = sendsByChannelId.find(channelId); + size_t& consumed = consumedSendsByChannelId[channelId]; + if (sends == sendsByChannelId.end() || consumed >= sends->second.size()) + return receive.emitOpError("has no matching realized channel send in Spatial dataflow export"), + FailureOr>(failure()); + sources.push_back(sends->second[consumed++]); + return FailureOr>(std::move(sources)); }; return emitExplicitChannelEdges( diff --git a/validation/.gitignore b/validation/.gitignore index 3737e4c..edfd889 100644 --- a/validation/.gitignore +++ b/validation/.gitignore @@ -3,6 +3,7 @@ operations/**/outputs operations/**/raptor operations/**/runner operations/**/simulation +operations/**/*.csv networks/**/inputs networks/**/outputs networks/**/raptor @@ -14,3 +15,5 @@ networks/**/*.png networks/**/*.jpg networks/**/*.csv !networks/pimcomp_models/results.csv +!networks/pimcomp_models/validation_results.csv +!operations/validation_results.csv diff --git a/validation/networks/pimcomp_models/validation_results.csv b/validation/networks/pimcomp_models/validation_results.csv new file mode 100644 index 0000000..4549cd5 --- /dev/null +++ b/validation/networks/pimcomp_models/validation_results.csv @@ -0,0 +1,6 @@ +Operation,Result,Compile,Host mem,Cores mem,Cores,Xbars,Latency,Power,Energy +vgg8-mnist-reconstructed,PASS,1.009 s,1.37 MiB,3.14 MiB,141,761,1.465778 ms,325.627854 mW,477298145.040001 pJ +resnet18-v1-7,PASS,11.548 s,9.89 MiB,40.24 MiB,168,7676,28.099952 ms,312.513408 mW,8781611766.119984 pJ +resnet34-v1-7,PASS,28.495 s,9.90 MiB,48.89 MiB,168,15292,45.781486 ms,326.833870 mW,14962940227.679951 pJ +googlenet-12-latency,PASS,6.573 s,10.74 MiB,22.41 MiB,168,7176,13.371204 ms,457.538139 mW,6117835798.919991 pJ +yolo11n-latency,FAIL,58.572 s,82.55 MiB,185.68 MiB,168,6484,885.264931 ms,189.218985 mW,167508931321.001465 pJ diff --git a/validation/operations/README.md b/validation/operations/README.md index 861a54e..cee977c 100644 --- a/validation/operations/README.md +++ b/validation/operations/README.md @@ -43,7 +43,7 @@ and writes the same rows to `validation_results.csv`. ## Complete inventory -The suite contains 165 models. Tensor shapes, attributes, and constants are +The suite contains 168 models. Tensor shapes, attributes, and constants are defined in `gen_tests.py` and in the checked-in ONNX models. ### Add (5) @@ -64,7 +64,7 @@ defined in `gen_tests.py` and in the checked-in ONNX models. | `negative_axis` | Concatenates tensors using a negative axis. | | `three_inputs_channel_axis` | Concatenates three runtime NCHW tensors along the channel axis. | -### Conv (32) +### Conv (34) | Case | Description | |---|---| @@ -99,6 +99,8 @@ defined in `gen_tests.py` and in the checked-in ONNX models. | `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. | +| `yolo11n_depthwise_head` | YOLO11n pointwise-to-depthwise head boundary at `80x80`, preserving row fragments. | +| `yolo11n_heavy` | Two largest standard YOLO11n Conv-SiLU blocks by MAC count at `64x80x80`. | | `yolo11n_stem` | First two YOLO11n `Conv-SiLU` blocks at `640x640`, including the distributed activation boundary. | ### Div (6) @@ -158,7 +160,7 @@ defined in `gen_tests.py` and in the checked-in ONNX models. | `with_homogeneous_constant` | Adds a constant bias matching the output shape. | | `with_scalar_constant` | Adds a scalar broadcast bias. | -### MatMul (11) +### MatMul (12) | Case | Description | |---|---| @@ -173,6 +175,7 @@ defined in `gen_tests.py` and in the checked-in ONNX models. | `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. | +| `yolo_attention` | YOLO11n rank-4 dynamic MatMul-scale-transpose-MatMul attention chain. | ### Mul (5) diff --git a/validation/operations/conv/yolo11n_depthwise_head/conv_yolo11n_depthwise_head.onnx b/validation/operations/conv/yolo11n_depthwise_head/conv_yolo11n_depthwise_head.onnx new file mode 100644 index 0000000000000000000000000000000000000000..5921c1af7b61f4822075f7ccbf1f03347662136c GIT binary patch literal 19547 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 literal 0 HcmV?d00001 diff --git a/validation/operations/conv/yolo11n_heavy/conv_yolo11n_heavy.onnx b/validation/operations/conv/yolo11n_heavy/conv_yolo11n_heavy.onnx new file mode 100644 index 0000000000000000000000000000000000000000..f3ef7ed0404a02098484e7e8e061606c3028c989 GIT binary patch literal 295828 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

%b8wnnt4dC(k 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 literal 0 HcmV?d00001 diff --git a/validation/operations/gen_tests.py b/validation/operations/gen_tests.py index 9ca077f..88930b3 100644 --- a/validation/operations/gen_tests.py +++ b/validation/operations/gen_tests.py @@ -242,6 +242,50 @@ def conv_yolo11n_stem(): save_model(model, "conv/yolo11n_stem", "conv_yolo11n_stem.onnx") +def conv_yolo11n_heavy(): + """Two largest YOLO11n standard Conv-SiLU blocks by MAC count.""" + X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64, 80, 80]) + Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 64, 80, 80]) + rng = np.random.default_rng(111) + W0 = numpy_helper.from_array(rng.uniform(-1, 1, (64, 64, 3, 3)).astype(np.float32), name="W0") + B0 = numpy_helper.from_array(rng.uniform(-1, 1, (64,)).astype(np.float32), name="B0") + W1 = numpy_helper.from_array(rng.uniform(-1, 1, (64, 64, 3, 3)).astype(np.float32), name="W1") + B1 = numpy_helper.from_array(rng.uniform(-1, 1, (64,)).astype(np.float32), name="B1") + nodes = [ + helper.make_node("Conv", ["X", "W0", "B0"], ["C0"], + kernel_shape=[3, 3], strides=[1, 1], pads=[1, 1, 1, 1]), + helper.make_node("Sigmoid", ["C0"], ["S0"]), + helper.make_node("Mul", ["C0", "S0"], ["A0"]), + helper.make_node("Conv", ["A0", "W1", "B1"], ["C1"], + kernel_shape=[3, 3], strides=[1, 1], pads=[1, 1, 1, 1]), + helper.make_node("Sigmoid", ["C1"], ["S1"]), + helper.make_node("Mul", ["C1", "S1"], ["Y"]), + ] + graph = helper.make_graph(nodes, "conv_yolo11n_heavy", [X], [Y], initializer=[W0, B0, W1, B1]) + model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) + save_model(model, "conv/yolo11n_heavy", "conv_yolo11n_heavy.onnx") + + +def conv_yolo11n_depthwise_head(): + """YOLO11n pointwise-to-depthwise head boundary at its largest feature map.""" + X = helper.make_tensor_value_info("X", TensorProto.FLOAT, [1, 64, 80, 80]) + Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 64, 80, 80]) + rng = np.random.default_rng(110) + W0 = numpy_helper.from_array(rng.uniform(-1, 1, (64, 64, 1, 1)).astype(np.float32), name="W0") + B0 = numpy_helper.from_array(rng.uniform(-1, 1, (64,)).astype(np.float32), name="B0") + W1 = numpy_helper.from_array(rng.uniform(-1, 1, (64, 1, 3, 3)).astype(np.float32), name="W1") + B1 = numpy_helper.from_array(rng.uniform(-1, 1, (64,)).astype(np.float32), name="B1") + nodes = [ + helper.make_node("Conv", ["X", "W0", "B0"], ["P"], + kernel_shape=[1, 1], strides=[1, 1], pads=[0, 0, 0, 0]), + helper.make_node("Conv", ["P", "W1", "B1"], ["Y"], + kernel_shape=[3, 3], strides=[1, 1], pads=[1, 1, 1, 1], group=64), + ] + graph = helper.make_graph(nodes, "conv_yolo11n_depthwise_head", [X], [Y], initializer=[W0, B0, W1, B1]) + model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) + save_model(model, "conv/yolo11n_depthwise_head", "conv_yolo11n_depthwise_head.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]) @@ -760,6 +804,22 @@ def matmul_batched_3d_dynamic(): save_model(model, "matmul/batched_3d_dynamic", "matmul_batched_3d_dynamic.onnx") +def matmul_yolo_attention(): + """YOLO11n attention chain with rank-4 dynamic matrices.""" + Q = helper.make_tensor_value_info("Q", TensorProto.FLOAT, [1, 2, 400, 32]) + K = helper.make_tensor_value_info("K", TensorProto.FLOAT, [1, 2, 32, 400]) + V = helper.make_tensor_value_info("V", TensorProto.FLOAT, [1, 2, 64, 400]) + Y = helper.make_tensor_value_info("Y", TensorProto.FLOAT, [1, 2, 64, 400]) + scale = numpy_helper.from_array(np.asarray([0.1767767], dtype=np.float32), name="scale") + nodes = [helper.make_node("MatMul", ["Q", "K"], ["scores"]), + helper.make_node("Mul", ["scores", "scale"], ["scaled"]), + helper.make_node("Transpose", ["scaled"], ["weights"], perm=[0, 1, 3, 2]), + helper.make_node("MatMul", ["V", "weights"], ["Y"])] + graph = helper.make_graph(nodes, "matmul_yolo_attention", [Q, K, V], [Y], initializer=[scale]) + model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 13)]) + save_model(model, "matmul/yolo_attention", "matmul_yolo_attention.onnx") + + def matmul_batched_left_constant(): """Batched 3D MatMul with constant LHS and runtime RHS.""" rng = np.random.default_rng(70) @@ -2057,6 +2117,8 @@ if __name__ == "__main__": conv_huge_pointwise_1024() conv_huge_pointwise_1024_dynamic() conv_yolo11n_stem() + conv_yolo11n_heavy() + conv_yolo11n_depthwise_head() conv_pointwise_tiled_chain() conv_large_output_channels_1x1() conv_large_input_channels_1x1() @@ -2078,6 +2140,7 @@ if __name__ == "__main__": matmul_dynamic() matmul_batched_3d() matmul_batched_3d_dynamic() + matmul_yolo_attention() matmul_batched_left_constant() matmul_batched_rhs_broadcast() matmul_batched_lhs_broadcast() diff --git a/validation/operations/matmul/yolo_attention/matmul_yolo_attention.onnx b/validation/operations/matmul/yolo_attention/matmul_yolo_attention.onnx new file mode 100644 index 0000000000000000000000000000000000000000..85b046365b7da8308565f6fdc9e12d959780a1aa GIT binary patch literal 301 zcmd%ic^=)mm2w1APFOPq@_j0>h# zh%pjokC13?Vo7dkPJCs4PJVo1Nl9v6NoIbYmLLbC0HYJcYA=>AET(o*GC-FKiF1i^ zFbWBBF>x>gF%uUv#{^~|Ljfe~jUfw^0ILAWhGCI)0Lvx;Wg~IQI`o54E6 literal 0 HcmV?d00001 diff --git a/validation/operations/validation_results.csv b/validation/operations/validation_results.csv index e00c427..c14c1c3 100644 --- a/validation/operations/validation_results.csv +++ b/validation/operations/validation_results.csv @@ -1,166 +1,169 @@ Operation,Result,Compile,Host mem,Cores mem,Cores,Xbars,Latency,Power,Energy -add/after_gemm,PASS,0.141 s,0.01 MiB,0.01 MiB,5,4,0.007784 ms,104.703618 mW,815012.960000 pJ -add/basic,PASS,0.114 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -add/broadcast_row,PASS,0.110 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -add/channel_broadcast_1024,PASS,0.105 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ -add/leading_dimension_broadcast,PASS,0.137 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -concat/channel_axis,PASS,0.127 s,0.00 MiB,0.00 MiB,1,0,0.000457 ms,78.157549 mW,35718.000000 pJ -concat/negative_axis,PASS,0.102 s,0.00 MiB,0.00 MiB,1,0,0.001043 ms,78.092042 mW,81450.000000 pJ -concat/three_inputs_channel_axis,PASS,0.127 s,0.00 MiB,0.00 MiB,1,0,0.000644 ms,78.149068 mW,50328.000000 pJ -conv/batch_2,PASS,0.102 s,0.00 MiB,0.00 MiB,2,2,0.013694 ms,82.623885 mW,1131451.480000 pJ -conv/batch_4_pointwise,PASS,0.110 s,0.00 MiB,0.01 MiB,5,4,0.003932 ms,116.078576 mW,456420.960000 pJ -conv/depthwise_1024_channels,PASS,0.167 s,0.19 MiB,0.38 MiB,129,128,0.220751 ms,178.454307 mW,39393966.720000 pJ -conv/depthwise_grouped,PASS,0.109 s,0.01 MiB,0.00 MiB,5,4,0.006024 ms,108.326521 mW,652558.960000 pJ -conv/dilated_3x3,PASS,0.143 s,0.00 MiB,0.00 MiB,3,3,0.004045 ms,110.234541 mW,445898.720000 pJ -conv/dynamic,PASS,0.107 s,0.00 MiB,0.00 MiB,5,0,0.001835 ms,92.281199 mW,169336.000000 pJ -conv/explicit_padding,PASS,0.139 s,0.00 MiB,0.00 MiB,4,4,0.004327 ms,115.794768 mW,501043.960000 pJ -conv/grouped_many_groups,PASS,0.616 s,0.05 MiB,0.09 MiB,65,64,0.181845 ms,142.210104 mW,25860196.360000 pJ -conv/grouped_two_groups,PASS,0.145 s,0.00 MiB,0.00 MiB,3,2,0.005360 ms,101.459418 mW,543822.480000 pJ -conv/huge_pointwise_1024,PASS,0.749 s,0.01 MiB,0.01 MiB,1,64,0.028261 ms,133.488743 mW,3772525.360000 pJ -conv/huge_pointwise_1024_dynamic,PASS,0.097 s,8.04 MiB,12.61 MiB,168,0,2.627964 ms,169.518697 mW,445489032.000000 pJ -conv/kernel_3x3,PASS,0.155 s,0.00 MiB,0.00 MiB,3,3,0.003091 ms,115.862414 mW,358130.720000 pJ -conv/kernel_equals_input_spatial,PASS,0.156 s,0.00 MiB,0.00 MiB,1,2,0.008443 ms,83.863849 mW,708062.480000 pJ -conv/large_input_channels_1x1,PASS,0.160 s,0.01 MiB,0.01 MiB,1,8,0.017167 ms,89.416900 mW,1535019.920000 pJ -conv/large_output_channels_1x1,PASS,0.141 s,0.00 MiB,0.01 MiB,1,8,0.004964 ms,117.628106 mW,583905.920000 pJ -conv/large_spatial,PASS,0.141 s,0.00 MiB,0.01 MiB,6,6,0.004096 ms,129.015000 mW,528445.440000 pJ -conv/multi_channel,PASS,0.143 s,0.00 MiB,0.00 MiB,3,3,0.005148 ms,106.453520 mW,548022.720000 pJ -conv/non_square_kernel_1x3,PASS,0.127 s,0.00 MiB,0.00 MiB,5,5,0.004029 ms,123.600943 mW,497988.200000 pJ -conv/non_square_kernel_3x1,PASS,0.085 s,0.00 MiB,0.00 MiB,3,3,0.005526 ms,105.464843 mW,582798.720000 pJ -conv/non_uniform_stride,PASS,0.120 s,0.00 MiB,0.00 MiB,4,4,0.005808 ms,110.081433 mW,639352.960000 pJ -conv/pointwise_1x1,PASS,0.139 s,0.00 MiB,0.00 MiB,4,4,0.004539 ms,114.835858 mW,521239.960000 pJ -conv/pointwise_tiled_chain,PASS,0.943 s,0.01 MiB,0.02 MiB,2,80,0.084437 ms,102.289307 mW,8637002.200000 pJ -conv/real_asymmetric_padding,PASS,0.115 s,0.00 MiB,0.00 MiB,4,4,0.005232 ms,111.870214 mW,585304.960000 pJ -conv/relu_conv_store,PASS,0.107 s,0.05 MiB,0.08 MiB,32,32,0.062978 ms,246.649827 mW,15533512.800000 pJ -conv/same_lower_3x3,PASS,0.094 s,0.00 MiB,0.00 MiB,5,5,0.004700 ms,119.232170 mW,560391.200000 pJ -conv/same_padding_3x3,PASS,0.126 s,0.00 MiB,0.00 MiB,5,5,0.004700 ms,119.232170 mW,560391.200000 pJ -conv/simple,PASS,0.125 s,0.00 MiB,0.00 MiB,2,2,0.003148 ms,94.665972 mW,298008.480000 pJ -conv/stride_2,PASS,0.146 s,0.00 MiB,0.00 MiB,2,2,0.002827 ms,96.393873 mW,272505.480000 pJ -conv/with_bias_3x3,PASS,0.121 s,0.00 MiB,0.00 MiB,3,3,0.004898 ms,107.176546 mW,524950.720000 pJ -conv/with_constant,PASS,0.106 s,0.00 MiB,0.00 MiB,3,3,0.004273 ms,109.362677 mW,467306.720000 pJ -conv/without_kernel_shape_attr,PASS,0.105 s,0.00 MiB,0.00 MiB,3,3,0.003091 ms,115.862414 mW,358130.720000 pJ -conv/yolo11n_stem,PASS,3.091 s,29.20 MiB,31.38 MiB,168,488,9.799213 ms,361.075203 mW,3538252825.000010 pJ -div/after_gemm,PASS,0.130 s,0.01 MiB,0.01 MiB,5,4,0.007784 ms,104.703618 mW,815012.960000 pJ -div/basic,PASS,0.116 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -div/channel_broadcast_1024,PASS,0.121 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ -div/leading_dimension_broadcast,PASS,0.095 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -div/runtime_scalar_rhs,PASS,0.126 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ -div/scalar_constant,PASS,0.069 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -gather/3d_input_axis1,PASS,0.121 s,0.00 MiB,0.00 MiB,1,0,0.000589 ms,78.081494 mW,45990.000000 pJ -gather/axis0_matrix_indices,PASS,0.088 s,0.00 MiB,0.00 MiB,1,0,0.000697 ms,78.068867 mW,54414.000000 pJ -gather/axis1,PASS,0.123 s,0.00 MiB,0.00 MiB,1,0,0.000801 ms,78.059925 mW,62526.000000 pJ -gather/negative_axis,PASS,0.087 s,0.00 MiB,0.00 MiB,1,0,0.001437 ms,78.033403 mW,112134.000000 pJ -gather/negative_indices,PASS,0.115 s,0.00 MiB,0.00 MiB,1,0,0.000376 ms,78.127660 mW,29376.000000 pJ -gemm/alpha_beta,PASS,0.121 s,0.01 MiB,0.01 MiB,5,4,0.007456 ms,105.272125 mW,784908.960000 pJ -gemm/bias_rank2_broadcast,PASS,0.127 s,0.00 MiB,0.01 MiB,5,4,0.007072 ms,105.979208 mW,749484.960000 pJ -gemm/dynamic,PASS,0.084 s,0.00 MiB,0.00 MiB,5,0,0.002421 ms,91.480793 mW,221475.000000 pJ -gemm/dynamic_alpha,PASS,0.104 s,0.00 MiB,0.00 MiB,5,0,0.003262 ms,91.415696 mW,298198.000000 pJ -gemm/dynamic_beta,PASS,0.110 s,0.00 MiB,0.00 MiB,5,0,0.004365 ms,91.316151 mW,398595.000000 pJ -gemm/dynamic_bias,PASS,0.092 s,0.00 MiB,0.00 MiB,5,0,0.002665 ms,91.445779 mW,243703.000000 pJ -gemm/dynamic_bias_alpha_beta,PASS,0.089 s,0.00 MiB,0.00 MiB,5,0,0.005629 ms,91.279268 mW,513811.000000 pJ -gemm/dynamic_transB,PASS,0.077 s,0.00 MiB,0.00 MiB,5,0,0.001301 ms,91.378171 mW,118883.000000 pJ -gemm/huge_1024,PASS,0.219 s,0.01 MiB,0.10 MiB,73,64,0.017522 ms,215.037402 mW,3767885.360000 pJ -gemm/large,PASS,0.097 s,0.02 MiB,0.03 MiB,17,16,0.011229 ms,140.152181 mW,1573768.840000 pJ -gemm/large_k_small_n,PASS,0.106 s,0.01 MiB,0.01 MiB,9,8,0.004748 ms,133.481449 mW,633769.920000 pJ -gemm/non_square,PASS,0.102 s,0.00 MiB,0.01 MiB,5,4,0.003527 ms,118.958310 mW,419565.960000 pJ -gemm/scalar_bias,PASS,0.111 s,0.00 MiB,0.01 MiB,5,4,0.007072 ms,105.979208 mW,749484.960000 pJ -gemm/simple,PASS,0.135 s,0.03 MiB,0.08 MiB,42,40,0.021640 ms,151.774196 mW,3284393.600000 pJ -gemm/small,PASS,0.074 s,0.00 MiB,0.00 MiB,2,2,0.004420 ms,90.144000 mW,398436.480000 pJ -gemm/small_k_large_n,PASS,0.129 s,0.01 MiB,0.02 MiB,17,8,0.007962 ms,131.005014 mW,1043061.920000 pJ -gemm/transA,PASS,0.089 s,0.00 MiB,0.01 MiB,5,4,0.005762 ms,109.140743 mW,628868.960000 pJ -gemm/transA_transB,PASS,0.115 s,0.00 MiB,0.01 MiB,5,4,0.005762 ms,109.140743 mW,628868.960000 pJ -gemm/transB,PASS,0.068 s,0.00 MiB,0.01 MiB,5,4,0.003527 ms,118.958310 mW,419565.960000 pJ -gemm/transB_with_bias,PASS,0.069 s,0.01 MiB,0.01 MiB,5,4,0.005046 ms,110.546762 mW,557818.960000 pJ -gemm/with_bias,PASS,0.113 s,0.01 MiB,0.01 MiB,5,4,0.005562 ms,108.767882 mW,604966.960000 pJ -gemv/constant,PASS,0.112 s,0.00 MiB,0.00 MiB,0,0,0.000000 ms,2.000000 mW,0.000000 pJ -gemv/simple,PASS,0.136 s,0.00 MiB,0.01 MiB,6,4,0.005160 ms,111.150380 mW,573535.960000 pJ -gemv/with_heterogeneous_constant,PASS,0.138 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ -gemv/with_homogeneous_constant,PASS,0.140 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ -gemv/with_scalar_constant,PASS,0.124 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ -matmul/basic,PASS,0.081 s,0.00 MiB,0.00 MiB,2,2,0.004420 ms,90.144000 mW,398436.480000 pJ -matmul/batched_3d,PASS,0.133 s,0.00 MiB,0.01 MiB,5,4,0.005958 ms,108.588949 mW,646972.960000 pJ -matmul/batched_3d_dynamic,PASS,0.105 s,0.00 MiB,0.00 MiB,9,0,0.003525 ms,92.330213 mW,325464.000000 pJ -matmul/batched_left_constant,PASS,0.136 s,0.00 MiB,0.02 MiB,9,8,0.008822 ms,114.385164 mW,1009105.920000 pJ -matmul/batched_lhs_broadcast,PASS,0.133 s,0.00 MiB,0.01 MiB,5,4,0.005681 ms,109.389361 mW,621440.960000 pJ -matmul/batched_rhs_broadcast,PASS,0.134 s,0.00 MiB,0.01 MiB,5,4,0.005958 ms,108.588949 mW,646972.960000 pJ -matmul/dynamic,PASS,0.093 s,0.00 MiB,0.00 MiB,5,0,0.001621 ms,91.421962 mW,148195.000000 pJ -matmul/huge_1024,PASS,0.277 s,0.01 MiB,0.10 MiB,73,64,0.017522 ms,215.037402 mW,3767885.360000 pJ -matmul/left_constant,PASS,0.123 s,0.00 MiB,0.01 MiB,5,4,0.005853 ms,108.861944 mW,637168.960000 pJ -matmul/matrix_vector,PASS,0.150 s,0.52 MiB,0.78 MiB,168,173,0.384660 ms,202.131271 mW,77751814.880000 pJ -matmul/vector_matrix,PASS,0.186 s,0.01 MiB,0.01 MiB,9,8,0.007409 ms,118.680243 mW,879301.920000 pJ -mul/after_conv,PASS,0.127 s,0.00 MiB,0.00 MiB,4,3,0.005453 ms,107.639046 mW,586955.720000 pJ -mul/basic,PASS,0.070 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -mul/channel_broadcast_1024,PASS,0.065 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ -mul/leading_dimension_broadcast,PASS,0.108 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -mul/scalar_constant,PASS,0.082 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -pool/avg_basic,PASS,0.070 s,0.00 MiB,0.00 MiB,1,0,0.011939 ms,78.022112 mW,931506.000000 pJ -pool/avg_ceil_mode,PASS,0.107 s,0.00 MiB,0.00 MiB,1,0,0.004359 ms,78.033035 mW,340146.000000 pJ -pool/avg_explicit_padding,PASS,0.111 s,0.00 MiB,0.00 MiB,1,0,0.008822 ms,78.027205 mW,688356.000000 pJ -pool/avg_include_pad,PASS,0.075 s,0.00 MiB,0.00 MiB,1,0,0.008506 ms,78.016929 mW,663612.000000 pJ -pool/avg_large_channels,PASS,0.064 s,0.04 MiB,0.02 MiB,1,0,0.178249 ms,78.280327 mW,13953390.000000 pJ -pool/avg_non_uniform_stride,PASS,0.068 s,0.00 MiB,0.00 MiB,1,0,0.014513 ms,78.016537 mW,1132254.000000 pJ -pool/avg_real_asymmetric_padding,PASS,0.064 s,0.00 MiB,0.00 MiB,1,0,0.025206 ms,78.024756 mW,1966692.000000 pJ -pool/max_after_conv,PASS,0.070 s,0.00 MiB,0.00 MiB,6,4,0.006452 ms,96.374606 mW,621808.960000 pJ +add/after_gemm,PASS,0.072 s,0.01 MiB,0.01 MiB,5,4,0.007784 ms,104.703618 mW,815012.960000 pJ +add/basic,PASS,0.049 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +add/broadcast_row,PASS,0.048 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +add/channel_broadcast_1024,PASS,0.051 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ +add/leading_dimension_broadcast,PASS,0.051 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +concat/channel_axis,PASS,0.051 s,0.00 MiB,0.00 MiB,1,0,0.000457 ms,78.157549 mW,35718.000000 pJ +concat/negative_axis,PASS,0.061 s,0.00 MiB,0.00 MiB,1,0,0.001043 ms,78.092042 mW,81450.000000 pJ +concat/three_inputs_channel_axis,PASS,0.054 s,0.00 MiB,0.00 MiB,1,0,0.000644 ms,78.149068 mW,50328.000000 pJ +conv/batch_2,PASS,0.059 s,0.00 MiB,0.00 MiB,2,2,0.013694 ms,82.623885 mW,1131451.480000 pJ +conv/batch_4_pointwise,PASS,0.056 s,0.00 MiB,0.01 MiB,5,4,0.003932 ms,116.078576 mW,456420.960000 pJ +conv/depthwise_1024_channels,PASS,0.081 s,0.19 MiB,0.38 MiB,129,128,0.220751 ms,178.454307 mW,39393966.720000 pJ +conv/depthwise_grouped,PASS,0.059 s,0.01 MiB,0.00 MiB,5,4,0.006024 ms,108.326521 mW,652558.960000 pJ +conv/dilated_3x3,PASS,0.059 s,0.00 MiB,0.00 MiB,3,3,0.004045 ms,110.234541 mW,445898.720000 pJ +conv/dynamic,PASS,0.059 s,0.00 MiB,0.00 MiB,5,0,0.001835 ms,92.281199 mW,169336.000000 pJ +conv/explicit_padding,PASS,0.058 s,0.00 MiB,0.00 MiB,4,4,0.004327 ms,115.794768 mW,501043.960000 pJ +conv/grouped_many_groups,PASS,0.505 s,0.05 MiB,0.09 MiB,65,64,0.181845 ms,142.210104 mW,25860196.360000 pJ +conv/grouped_two_groups,PASS,0.061 s,0.00 MiB,0.00 MiB,3,2,0.005360 ms,101.459418 mW,543822.480000 pJ +conv/huge_pointwise_1024,PASS,0.643 s,0.01 MiB,0.01 MiB,1,64,0.028261 ms,133.488743 mW,3772525.360000 pJ +conv/huge_pointwise_1024_dynamic,PASS,0.081 s,8.04 MiB,12.61 MiB,168,0,2.627964 ms,169.518697 mW,445489032.000000 pJ +conv/kernel_3x3,PASS,0.056 s,0.00 MiB,0.00 MiB,3,3,0.003091 ms,115.862414 mW,358130.720000 pJ +conv/kernel_equals_input_spatial,PASS,0.060 s,0.00 MiB,0.00 MiB,1,2,0.008443 ms,83.863849 mW,708062.480000 pJ +conv/large_input_channels_1x1,PASS,0.088 s,0.01 MiB,0.01 MiB,1,8,0.017167 ms,89.416900 mW,1535019.920000 pJ +conv/large_output_channels_1x1,PASS,0.087 s,0.00 MiB,0.01 MiB,1,8,0.004964 ms,117.628106 mW,583905.920000 pJ +conv/large_spatial,PASS,0.055 s,0.00 MiB,0.01 MiB,6,6,0.004096 ms,129.015000 mW,528445.440000 pJ +conv/multi_channel,PASS,0.056 s,0.00 MiB,0.00 MiB,3,3,0.005148 ms,106.453520 mW,548022.720000 pJ +conv/non_square_kernel_1x3,PASS,0.056 s,0.00 MiB,0.00 MiB,5,5,0.004029 ms,123.600943 mW,497988.200000 pJ +conv/non_square_kernel_3x1,PASS,0.058 s,0.00 MiB,0.00 MiB,3,3,0.005526 ms,105.464843 mW,582798.720000 pJ +conv/non_uniform_stride,PASS,0.062 s,0.00 MiB,0.00 MiB,4,4,0.005808 ms,110.081433 mW,639352.960000 pJ +conv/pointwise_1x1,PASS,0.057 s,0.00 MiB,0.00 MiB,4,4,0.004539 ms,114.835858 mW,521239.960000 pJ +conv/pointwise_tiled_chain,PASS,0.771 s,0.01 MiB,0.02 MiB,2,80,0.084437 ms,102.289307 mW,8637002.200000 pJ +conv/real_asymmetric_padding,PASS,0.055 s,0.00 MiB,0.00 MiB,4,4,0.005232 ms,111.870214 mW,585304.960000 pJ +conv/relu_conv_store,PASS,0.076 s,0.02 MiB,0.10 MiB,32,32,0.064390 ms,243.916397 mW,15705776.800000 pJ +conv/same_lower_3x3,PASS,0.057 s,0.00 MiB,0.00 MiB,5,5,0.004700 ms,119.232170 mW,560391.200000 pJ +conv/same_padding_3x3,PASS,0.060 s,0.00 MiB,0.00 MiB,5,5,0.004700 ms,119.232170 mW,560391.200000 pJ +conv/simple,PASS,0.055 s,0.00 MiB,0.00 MiB,2,2,0.003148 ms,94.665972 mW,298008.480000 pJ +conv/stride_2,PASS,0.053 s,0.00 MiB,0.00 MiB,2,2,0.002827 ms,96.393873 mW,272505.480000 pJ +conv/with_bias_3x3,PASS,0.055 s,0.00 MiB,0.00 MiB,3,3,0.004898 ms,107.176546 mW,524950.720000 pJ +conv/with_constant,PASS,0.073 s,0.00 MiB,0.00 MiB,3,3,0.004273 ms,109.362677 mW,467306.720000 pJ +conv/without_kernel_shape_attr,PASS,0.057 s,0.00 MiB,0.00 MiB,3,3,0.003091 ms,115.862414 mW,358130.720000 pJ +conv/yolo11n_depthwise_head,PASS,0.509 s,4.82 MiB,15.92 MiB,160,720,4.105780 ms,492.826315 mW,2023436428.000010 pJ +conv/yolo11n_heavy,PASS,0.488 s,4.82 MiB,20.66 MiB,160,800,6.275385 ms,418.028199 mW,2623287892.000010 pJ +conv/yolo11n_stem,PASS,1.730 s,12.86 MiB,31.38 MiB,168,488,9.799204 ms,361.075380 mW,3538251304.000010 pJ +div/after_gemm,PASS,0.059 s,0.01 MiB,0.01 MiB,5,4,0.007784 ms,104.703618 mW,815012.960000 pJ +div/basic,PASS,0.048 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +div/channel_broadcast_1024,PASS,0.051 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ +div/leading_dimension_broadcast,PASS,0.063 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +div/runtime_scalar_rhs,PASS,0.057 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ +div/scalar_constant,PASS,0.049 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +gather/3d_input_axis1,PASS,0.050 s,0.00 MiB,0.00 MiB,1,0,0.000589 ms,78.081494 mW,45990.000000 pJ +gather/axis0_matrix_indices,PASS,0.050 s,0.00 MiB,0.00 MiB,1,0,0.000697 ms,78.068867 mW,54414.000000 pJ +gather/axis1,PASS,0.053 s,0.00 MiB,0.00 MiB,1,0,0.000801 ms,78.059925 mW,62526.000000 pJ +gather/negative_axis,PASS,0.053 s,0.00 MiB,0.00 MiB,1,0,0.001437 ms,78.033403 mW,112134.000000 pJ +gather/negative_indices,PASS,0.052 s,0.00 MiB,0.00 MiB,1,0,0.000376 ms,78.127660 mW,29376.000000 pJ +gemm/alpha_beta,PASS,0.056 s,0.01 MiB,0.01 MiB,5,4,0.007456 ms,105.272125 mW,784908.960000 pJ +gemm/bias_rank2_broadcast,PASS,0.056 s,0.00 MiB,0.01 MiB,5,4,0.007072 ms,105.979208 mW,749484.960000 pJ +gemm/dynamic,PASS,0.054 s,0.00 MiB,0.00 MiB,5,0,0.002421 ms,91.480793 mW,221475.000000 pJ +gemm/dynamic_alpha,PASS,0.056 s,0.00 MiB,0.00 MiB,5,0,0.003262 ms,91.415696 mW,298198.000000 pJ +gemm/dynamic_beta,PASS,0.059 s,0.00 MiB,0.00 MiB,5,0,0.004365 ms,91.316151 mW,398595.000000 pJ +gemm/dynamic_bias,PASS,0.056 s,0.00 MiB,0.00 MiB,5,0,0.002665 ms,91.445779 mW,243703.000000 pJ +gemm/dynamic_bias_alpha_beta,PASS,0.053 s,0.00 MiB,0.00 MiB,5,0,0.005629 ms,91.279268 mW,513811.000000 pJ +gemm/dynamic_transB,PASS,0.054 s,0.00 MiB,0.00 MiB,5,0,0.001301 ms,91.378171 mW,118883.000000 pJ +gemm/huge_1024,PASS,0.157 s,0.01 MiB,0.10 MiB,73,64,0.017522 ms,215.037402 mW,3767885.360000 pJ +gemm/large,PASS,0.067 s,0.02 MiB,0.03 MiB,17,16,0.011229 ms,140.152181 mW,1573768.840000 pJ +gemm/large_k_small_n,PASS,0.097 s,0.01 MiB,0.01 MiB,9,8,0.004748 ms,133.481449 mW,633769.920000 pJ +gemm/non_square,PASS,0.060 s,0.00 MiB,0.01 MiB,5,4,0.003527 ms,118.958310 mW,419565.960000 pJ +gemm/scalar_bias,PASS,0.054 s,0.00 MiB,0.01 MiB,5,4,0.007072 ms,105.979208 mW,749484.960000 pJ +gemm/simple,PASS,0.071 s,0.03 MiB,0.08 MiB,42,40,0.021640 ms,151.774196 mW,3284393.600000 pJ +gemm/small,PASS,0.052 s,0.00 MiB,0.00 MiB,2,2,0.004420 ms,90.144000 mW,398436.480000 pJ +gemm/small_k_large_n,PASS,0.089 s,0.01 MiB,0.02 MiB,17,8,0.007962 ms,131.005014 mW,1043061.920000 pJ +gemm/transA,PASS,0.057 s,0.00 MiB,0.01 MiB,5,4,0.005762 ms,109.140743 mW,628868.960000 pJ +gemm/transA_transB,PASS,0.053 s,0.00 MiB,0.01 MiB,5,4,0.005762 ms,109.140743 mW,628868.960000 pJ +gemm/transB,PASS,0.062 s,0.00 MiB,0.01 MiB,5,4,0.003527 ms,118.958310 mW,419565.960000 pJ +gemm/transB_with_bias,PASS,0.057 s,0.01 MiB,0.01 MiB,5,4,0.005046 ms,110.546762 mW,557818.960000 pJ +gemm/with_bias,PASS,0.056 s,0.01 MiB,0.01 MiB,5,4,0.005562 ms,108.767882 mW,604966.960000 pJ +gemv/constant,PASS,0.051 s,0.00 MiB,0.00 MiB,0,0,0.000000 ms,2.000000 mW,0.000000 pJ +gemv/simple,PASS,0.066 s,0.00 MiB,0.01 MiB,6,4,0.005160 ms,111.150380 mW,573535.960000 pJ +gemv/with_heterogeneous_constant,PASS,0.066 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ +gemv/with_homogeneous_constant,PASS,0.063 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ +gemv/with_scalar_constant,PASS,0.064 s,0.00 MiB,0.01 MiB,6,4,0.005549 ms,109.816536 mW,609371.960000 pJ +matmul/basic,PASS,0.064 s,0.00 MiB,0.00 MiB,2,2,0.004420 ms,90.144000 mW,398436.480000 pJ +matmul/batched_3d,PASS,0.058 s,0.00 MiB,0.01 MiB,5,4,0.005958 ms,108.588949 mW,646972.960000 pJ +matmul/batched_3d_dynamic,PASS,0.053 s,0.00 MiB,0.00 MiB,4,0,0.001822 ms,92.192645 mW,167975.000000 pJ +matmul/batched_left_constant,PASS,0.058 s,0.00 MiB,0.02 MiB,9,8,0.008822 ms,114.385164 mW,1009105.920000 pJ +matmul/batched_lhs_broadcast,PASS,0.056 s,0.00 MiB,0.01 MiB,5,4,0.005681 ms,109.389361 mW,621440.960000 pJ +matmul/batched_rhs_broadcast,PASS,0.059 s,0.00 MiB,0.01 MiB,5,4,0.005958 ms,108.588949 mW,646972.960000 pJ +matmul/dynamic,PASS,0.054 s,0.00 MiB,0.00 MiB,5,0,0.001621 ms,91.421962 mW,148195.000000 pJ +matmul/huge_1024,PASS,0.142 s,0.01 MiB,0.10 MiB,73,64,0.017522 ms,215.037402 mW,3767885.360000 pJ +matmul/left_constant,PASS,0.064 s,0.00 MiB,0.01 MiB,5,4,0.005853 ms,108.861944 mW,637168.960000 pJ +matmul/matrix_vector,PASS,0.097 s,0.52 MiB,0.78 MiB,168,173,0.384660 ms,202.131271 mW,77751814.880000 pJ +matmul/vector_matrix,PASS,0.086 s,0.01 MiB,0.01 MiB,9,8,0.007409 ms,118.680243 mW,879301.920000 pJ +matmul/yolo_attention,PASS,0.417 s,1.02 MiB,43.44 MiB,168,0,8.151445 ms,170.003707 mW,1385775865.000000 pJ +mul/after_conv,PASS,0.060 s,0.00 MiB,0.00 MiB,4,3,0.005453 ms,107.639046 mW,586955.720000 pJ +mul/basic,PASS,0.051 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +mul/channel_broadcast_1024,PASS,0.060 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ +mul/leading_dimension_broadcast,PASS,0.058 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +mul/scalar_constant,PASS,0.053 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +pool/avg_basic,PASS,0.057 s,0.00 MiB,0.00 MiB,1,0,0.011939 ms,78.022112 mW,931506.000000 pJ +pool/avg_ceil_mode,PASS,0.054 s,0.00 MiB,0.00 MiB,1,0,0.004359 ms,78.033035 mW,340146.000000 pJ +pool/avg_explicit_padding,PASS,0.062 s,0.00 MiB,0.00 MiB,1,0,0.008822 ms,78.027205 mW,688356.000000 pJ +pool/avg_include_pad,PASS,0.055 s,0.00 MiB,0.00 MiB,1,0,0.008506 ms,78.016929 mW,663612.000000 pJ +pool/avg_large_channels,PASS,0.067 s,0.04 MiB,0.02 MiB,1,0,0.178249 ms,78.280327 mW,13953390.000000 pJ +pool/avg_non_uniform_stride,PASS,0.060 s,0.00 MiB,0.00 MiB,1,0,0.014513 ms,78.016537 mW,1132254.000000 pJ +pool/avg_real_asymmetric_padding,PASS,0.071 s,0.00 MiB,0.00 MiB,1,0,0.025206 ms,78.024756 mW,1966692.000000 pJ +pool/max_after_conv,PASS,0.066 s,0.00 MiB,0.00 MiB,6,4,0.006452 ms,96.374606 mW,621808.960000 pJ pool/max_basic,PASS,0.063 s,0.00 MiB,0.00 MiB,3,0,0.001634 ms,92.132191 mW,150544.000000 pJ -pool/max_ceil_mode,PASS,0.065 s,0.00 MiB,0.00 MiB,2,0,0.001297 ms,79.111025 mW,102607.000000 pJ -pool/max_global_style_kernel_equals_input,PASS,0.089 s,0.00 MiB,0.00 MiB,1,0,0.004366 ms,78.010994 mW,340596.000000 pJ -pool/max_non_square_kernel,PASS,0.103 s,0.00 MiB,0.00 MiB,4,0,0.003409 ms,93.253447 mW,317901.000000 pJ -pool/max_real_asymmetric_padding,PASS,0.088 s,0.00 MiB,0.00 MiB,4,0,0.003078 ms,93.124756 mW,286638.000000 pJ -pool/max_same_upper,PASS,0.065 s,0.00 MiB,0.00 MiB,3,0,0.003024 ms,92.095238 mW,278496.000000 pJ -pool/max_stride2_multichannel,PASS,0.086 s,0.00 MiB,0.00 MiB,3,0,0.004012 ms,92.269192 mW,370184.000000 pJ -reduce_mean/4d_spatial,PASS,0.061 s,0.00 MiB,0.00 MiB,3,0,0.000321 ms,92.448598 mW,29676.000000 pJ -reduce_mean/4d_spatial_keepdims_0,PASS,0.067 s,0.00 MiB,0.00 MiB,4,0,0.000655 ms,94.352672 mW,61801.000000 pJ -reduce_mean/after_conv,PASS,0.115 s,0.00 MiB,0.00 MiB,5,3,0.005342 ms,106.951089 mW,571332.720000 pJ -reduce_mean/all_axes_keepdims_0,PASS,0.103 s,0.00 MiB,0.00 MiB,2,0,0.000391 ms,79.237852 mW,30982.000000 pJ -reduce_mean/all_axes_keepdims_1,PASS,0.086 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ -reduce_mean/basic,PASS,0.099 s,0.00 MiB,0.00 MiB,4,0,0.000373 ms,93.514745 mW,34881.000000 pJ -reduce_mean/channel_axis_nchw,PASS,0.078 s,0.03 MiB,0.02 MiB,4,0,0.164926 ms,93.596631 mW,15436518.000000 pJ -reduce_mean/keepdims_0,PASS,0.073 s,0.00 MiB,0.00 MiB,5,0,0.000748 ms,91.401070 mW,68368.000000 pJ -reduce_mean/large_dimension_1024,PASS,0.115 s,0.01 MiB,0.00 MiB,1,0,0.002785 ms,78.017235 mW,217278.000000 pJ -reduce_mean/legacy_axes_1_2_keepdims_1,PASS,0.074 s,0.00 MiB,0.00 MiB,2,0,0.000271 ms,79.354244 mW,21505.000000 pJ -reduce_mean/legacy_axis1_keepdims_0,PASS,0.111 s,0.00 MiB,0.00 MiB,9,0,0.001986 ms,92.501511 mW,183708.000000 pJ -reduce_mean/legacy_axis1_keepdims_1,PASS,0.086 s,0.00 MiB,0.00 MiB,8,0,0.001373 ms,94.559359 mW,129830.000000 pJ -reduce_mean/legacy_empty_axes_noop,PASS,0.115 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ -reduce_mean/legacy_nchw_spatial,PASS,0.090 s,0.00 MiB,0.00 MiB,3,0,0.000321 ms,92.448598 mW,29676.000000 pJ -reduce_mean/legacy_negative_axis,PASS,0.110 s,0.00 MiB,0.00 MiB,6,0,0.000553 ms,93.520796 mW,51717.000000 pJ -reduce_mean/legacy_reduce_all_keepdims_1,PASS,0.107 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ -reduce_mean/negative_axis,PASS,0.120 s,0.00 MiB,0.00 MiB,6,0,0.000553 ms,93.520796 mW,51717.000000 pJ -relu/4d,PASS,0.116 s,0.00 MiB,0.00 MiB,1,0,0.000521 ms,78.184261 mW,40734.000000 pJ -relu/after_conv,PASS,0.078 s,0.00 MiB,0.00 MiB,3,3,0.004956 ms,106.998935 mW,530286.720000 pJ -relu/after_gemm,PASS,0.077 s,0.01 MiB,0.01 MiB,5,4,0.007513 ms,105.158653 mW,790056.960000 pJ -relu/basic,PASS,0.066 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ -reshape/4d_to_2d_flatten,PASS,0.098 s,0.00 MiB,0.00 MiB,1,0,0.000258 ms,78.279070 mW,20196.000000 pJ -reshape/infer_dim_minus_one,PASS,0.105 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ -reshape/same_rank,PASS,0.057 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ -reshape/zero_copies_input_dim,PASS,0.090 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ -resize/height_only,PASS,0.104 s,0.00 MiB,0.00 MiB,1,0,0.000795 ms,78.060377 mW,62058.000000 pJ -resize/nearest_2x,PASS,0.104 s,0.00 MiB,0.00 MiB,1,0,0.001422 ms,78.033755 mW,110964.000000 pJ -resize/nearest_downsample,PASS,0.059 s,0.00 MiB,0.00 MiB,1,0,0.000481 ms,78.099792 mW,37566.000000 pJ -resize/non_uniform,PASS,0.063 s,0.00 MiB,0.00 MiB,1,0,0.002108 ms,78.034156 mW,164496.000000 pJ -resize/width_only,PASS,0.068 s,0.00 MiB,0.00 MiB,1,0,0.000792 ms,78.060606 mW,61824.000000 pJ -resize/with_sizes,PASS,0.056 s,0.00 MiB,0.00 MiB,1,0,0.000951 ms,78.050473 mW,74226.000000 pJ -sigmoid/4d,PASS,0.104 s,0.00 MiB,0.00 MiB,1,0,0.000521 ms,78.184261 mW,40734.000000 pJ -sigmoid/after_gemm,PASS,0.118 s,0.01 MiB,0.01 MiB,5,4,0.007513 ms,105.158653 mW,790056.960000 pJ -sigmoid/basic,PASS,0.083 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ -slice/2d_basic,PASS,0.067 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ -slice/after_conv,PASS,0.111 s,0.00 MiB,0.01 MiB,7,6,0.011296 ms,118.190765 mW,1335082.880000 pJ -slice/default_axes,PASS,0.065 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ -slice/large_channel_1024,PASS,0.060 s,0.01 MiB,0.00 MiB,1,0,0.002832 ms,78.144068 mW,221304.000000 pJ -slice/nchw_spatial_crop,PASS,0.067 s,0.00 MiB,0.00 MiB,1,0,0.001302 ms,78.239631 mW,101868.000000 pJ -slice/negative_axis,PASS,0.098 s,0.00 MiB,0.00 MiB,1,0,0.000562 ms,78.298932 mW,44004.000000 pJ -slice/negative_indices,PASS,0.106 s,0.00 MiB,0.00 MiB,1,0,0.000322 ms,78.298137 mW,25212.000000 pJ -slice/step2,PASS,0.068 s,0.00 MiB,0.00 MiB,1,0,0.002042 ms,78.293830 mW,159876.000000 pJ -softmax/3d_last_axis,PASS,0.072 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED -softmax/basic,PASS,0.088 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED -softmax/channel_axis,PASS,0.110 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED -softmax/large_dimension_1024,PASS,0.104 s,0.01 MiB,0.01 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED -softmax/negative_axis,PASS,0.118 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED -split/basic,PASS,0.077 s,0.00 MiB,0.00 MiB,1,0,0.000403 ms,78.297767 mW,31554.000000 pJ -split/equal_three_way,PASS,0.107 s,0.00 MiB,0.00 MiB,1,0,0.000564 ms,78.297872 mW,44160.000000 pJ -split/negative_axis,PASS,0.111 s,0.00 MiB,0.00 MiB,1,0,0.001083 ms,78.288089 mW,84786.000000 pJ -split/uneven_channel_axis_4d,PASS,0.061 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ +pool/max_ceil_mode,PASS,0.078 s,0.00 MiB,0.00 MiB,2,0,0.001297 ms,79.111025 mW,102607.000000 pJ +pool/max_global_style_kernel_equals_input,PASS,0.067 s,0.00 MiB,0.00 MiB,1,0,0.004366 ms,78.010994 mW,340596.000000 pJ +pool/max_non_square_kernel,PASS,0.073 s,0.00 MiB,0.00 MiB,4,0,0.003409 ms,93.253447 mW,317901.000000 pJ +pool/max_real_asymmetric_padding,PASS,0.064 s,0.00 MiB,0.00 MiB,4,0,0.003078 ms,93.124756 mW,286638.000000 pJ +pool/max_same_upper,PASS,0.063 s,0.00 MiB,0.00 MiB,3,0,0.003024 ms,92.095238 mW,278496.000000 pJ +pool/max_stride2_multichannel,PASS,0.066 s,0.00 MiB,0.00 MiB,3,0,0.004012 ms,92.269192 mW,370184.000000 pJ +reduce_mean/4d_spatial,PASS,0.078 s,0.00 MiB,0.00 MiB,3,0,0.000321 ms,92.448598 mW,29676.000000 pJ +reduce_mean/4d_spatial_keepdims_0,PASS,0.066 s,0.00 MiB,0.00 MiB,4,0,0.000655 ms,94.352672 mW,61801.000000 pJ +reduce_mean/after_conv,PASS,0.068 s,0.00 MiB,0.00 MiB,5,3,0.005342 ms,106.951089 mW,571332.720000 pJ +reduce_mean/all_axes_keepdims_0,PASS,0.067 s,0.00 MiB,0.00 MiB,2,0,0.000391 ms,79.237852 mW,30982.000000 pJ +reduce_mean/all_axes_keepdims_1,PASS,0.056 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ +reduce_mean/basic,PASS,0.052 s,0.00 MiB,0.00 MiB,4,0,0.000373 ms,93.514745 mW,34881.000000 pJ +reduce_mean/channel_axis_nchw,PASS,0.054 s,0.03 MiB,0.02 MiB,4,0,0.164926 ms,93.596631 mW,15436518.000000 pJ +reduce_mean/keepdims_0,PASS,0.054 s,0.00 MiB,0.00 MiB,5,0,0.000748 ms,91.401070 mW,68368.000000 pJ +reduce_mean/large_dimension_1024,PASS,0.056 s,0.01 MiB,0.00 MiB,1,0,0.002785 ms,78.017235 mW,217278.000000 pJ +reduce_mean/legacy_axes_1_2_keepdims_1,PASS,0.064 s,0.00 MiB,0.00 MiB,2,0,0.000271 ms,79.354244 mW,21505.000000 pJ +reduce_mean/legacy_axis1_keepdims_0,PASS,0.071 s,0.00 MiB,0.00 MiB,9,0,0.001986 ms,92.501511 mW,183708.000000 pJ +reduce_mean/legacy_axis1_keepdims_1,PASS,0.065 s,0.00 MiB,0.00 MiB,8,0,0.001373 ms,94.559359 mW,129830.000000 pJ +reduce_mean/legacy_empty_axes_noop,PASS,0.053 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ +reduce_mean/legacy_nchw_spatial,PASS,0.050 s,0.00 MiB,0.00 MiB,3,0,0.000321 ms,92.448598 mW,29676.000000 pJ +reduce_mean/legacy_negative_axis,PASS,0.064 s,0.00 MiB,0.00 MiB,6,0,0.000553 ms,93.520796 mW,51717.000000 pJ +reduce_mean/legacy_reduce_all_keepdims_1,PASS,0.054 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ +reduce_mean/negative_axis,PASS,0.059 s,0.00 MiB,0.00 MiB,6,0,0.000553 ms,93.520796 mW,51717.000000 pJ +relu/4d,PASS,0.068 s,0.00 MiB,0.00 MiB,1,0,0.000521 ms,78.184261 mW,40734.000000 pJ +relu/after_conv,PASS,0.057 s,0.00 MiB,0.00 MiB,3,3,0.004956 ms,106.998935 mW,530286.720000 pJ +relu/after_gemm,PASS,0.064 s,0.01 MiB,0.01 MiB,5,4,0.007513 ms,105.158653 mW,790056.960000 pJ +relu/basic,PASS,0.056 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ +reshape/4d_to_2d_flatten,PASS,0.068 s,0.00 MiB,0.00 MiB,1,0,0.000258 ms,78.279070 mW,20196.000000 pJ +reshape/infer_dim_minus_one,PASS,0.059 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ +reshape/same_rank,PASS,0.065 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ +reshape/zero_copies_input_dim,PASS,0.057 s,0.00 MiB,0.00 MiB,1,0,0.000162 ms,78.296296 mW,12684.000000 pJ +resize/height_only,PASS,0.063 s,0.00 MiB,0.00 MiB,4,0,0.000693 ms,93.554113 mW,64833.000000 pJ +resize/nearest_2x,PASS,0.064 s,0.00 MiB,0.00 MiB,4,0,0.001173 ms,93.572890 mW,109761.000000 pJ +resize/nearest_downsample,PASS,0.062 s,0.00 MiB,0.00 MiB,2,0,0.000427 ms,79.449649 mW,33925.000000 pJ +resize/non_uniform,PASS,0.068 s,0.00 MiB,0.00 MiB,6,0,0.001753 ms,93.575014 mW,164037.000000 pJ +resize/width_only,PASS,0.070 s,0.00 MiB,0.00 MiB,2,0,0.000667 ms,79.503748 mW,53029.000000 pJ +resize/with_sizes,PASS,0.052 s,0.00 MiB,0.00 MiB,3,0,0.000797 ms,92.542033 mW,73756.000000 pJ +sigmoid/4d,PASS,0.067 s,0.00 MiB,0.00 MiB,1,0,0.000521 ms,78.184261 mW,40734.000000 pJ +sigmoid/after_gemm,PASS,0.070 s,0.01 MiB,0.01 MiB,5,4,0.007513 ms,105.158653 mW,790056.960000 pJ +sigmoid/basic,PASS,0.062 s,0.00 MiB,0.00 MiB,1,0,0.000221 ms,78.217195 mW,17286.000000 pJ +slice/2d_basic,PASS,0.049 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ +slice/after_conv,PASS,0.062 s,0.00 MiB,0.01 MiB,7,6,0.011296 ms,118.190765 mW,1335082.880000 pJ +slice/default_axes,PASS,0.056 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ +slice/large_channel_1024,PASS,0.049 s,0.01 MiB,0.00 MiB,1,0,0.002832 ms,78.144068 mW,221304.000000 pJ +slice/nchw_spatial_crop,PASS,0.063 s,0.00 MiB,0.00 MiB,1,0,0.001302 ms,78.239631 mW,101868.000000 pJ +slice/negative_axis,PASS,0.058 s,0.00 MiB,0.00 MiB,1,0,0.000562 ms,78.298932 mW,44004.000000 pJ +slice/negative_indices,PASS,0.059 s,0.00 MiB,0.00 MiB,1,0,0.000322 ms,78.298137 mW,25212.000000 pJ +slice/step2,PASS,0.054 s,0.00 MiB,0.00 MiB,1,0,0.002042 ms,78.293830 mW,159876.000000 pJ +softmax/3d_last_axis,PASS,0.059 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +softmax/basic,PASS,0.069 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +softmax/channel_axis,PASS,0.061 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +softmax/large_dimension_1024,PASS,0.045 s,0.01 MiB,0.01 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +softmax/negative_axis,PASS,0.064 s,0.00 MiB,0.00 MiB,1,0,UNSUPPORTED,UNSUPPORTED,UNSUPPORTED +split/basic,PASS,0.066 s,0.00 MiB,0.00 MiB,1,0,0.000403 ms,78.297767 mW,31554.000000 pJ +split/equal_three_way,PASS,0.065 s,0.00 MiB,0.00 MiB,1,0,0.000564 ms,78.297872 mW,44160.000000 pJ +split/negative_axis,PASS,0.058 s,0.00 MiB,0.00 MiB,1,0,0.001083 ms,78.288089 mW,84786.000000 pJ +split/uneven_channel_axis_4d,PASS,0.062 s,0.00 MiB,0.00 MiB,1,0,0.000242 ms,78.297521 mW,18948.000000 pJ sub/after_gemm,PASS,0.068 s,0.01 MiB,0.01 MiB,5,4,0.007784 ms,104.703618 mW,815012.960000 pJ -sub/basic,PASS,0.063 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -sub/broadcast_row,PASS,0.075 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ -sub/channel_broadcast_1024,PASS,0.052 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ +sub/basic,PASS,0.060 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +sub/broadcast_row,PASS,0.062 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +sub/channel_broadcast_1024,PASS,0.057 s,0.02 MiB,0.01 MiB,1,0,0.006913 ms,78.118038 mW,540030.000000 pJ sub/constant_lhs_broadcast,PASS,0.057 s,0.00 MiB,0.00 MiB,1,0,0.000322 ms,78.223602 mW,25188.000000 pJ -sub/leading_dimension_broadcast,PASS,0.053 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ +sub/leading_dimension_broadcast,PASS,0.058 s,0.00 MiB,0.00 MiB,1,0,0.000323 ms,78.222910 mW,25266.000000 pJ diff --git a/validation/tools/analyze_yolo11n_attention.py b/validation/tools/analyze_yolo11n_attention.py new file mode 100644 index 0000000..8453e4f --- /dev/null +++ b/validation/tools/analyze_yolo11n_attention.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Save exact attention-tap arrays and quantify the MatMul error sources.""" + +import argparse +import hashlib +import json +import sys +from pathlib import Path + +import numpy as np +import onnx +from onnx import numpy_helper + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT / "validation")) + +from raptor_validation.onnx_utils import onnx_io # noqa: E402 +from raptor_validation.validate_one import ( # noqa: E402 + parse_pim_simulator_outputs, + sanitize_output_name, +) + + +TAP_NAMES = { + "v": "/model.10/m/m.0/attn/Split_output_2", + "raw": "/model.10/m/m.0/attn/MatMul_output_0", + "scaled": "/model.10/m/m.0/attn/Mul_output_0", + "rhs": "/model.10/m/m.0/attn/Transpose_1_output_0", + "c": "/model.10/m/m.0/attn/MatMul_1_output_0", +} +ABSOLUTE_TOLERANCE = 1e-3 +RELATIVE_TOLERANCE = 1e-5 + + +def sha256(path): + digest = hashlib.sha256() + with Path(path).open("rb") as stream: + for block in iter(lambda: stream.read(1 << 20), b""): + digest.update(block) + return digest.hexdigest() + + +def metric(actual, expected): + difference = np.abs(actual.astype(np.float64) - expected.astype(np.float64)) + allowed = ABSOLUTE_TOLERANCE + RELATIVE_TOLERANCE * np.abs(expected.astype(np.float64)) + return { + "max_abs": float(np.max(difference)), + "mean_abs": float(np.mean(difference)), + "rms": float(np.sqrt(np.mean(np.square(difference)))), + "elements_over_validator_limit": int(np.count_nonzero(difference > allowed)), + } + + +def f32_matmul(lhs, rhs): + return np.matmul(lhs.astype(np.float32), rhs.astype(np.float32)).astype(np.float32) + + +def f64_matmul(lhs, rhs): + return np.matmul(lhs.astype(np.float64), rhs.astype(np.float64)).astype(np.float64) + + +def load_constant(model, output_name): + for initializer in model.graph.initializer: + if initializer.name == output_name: + return float(numpy_helper.to_array(initializer).reshape(-1)[0]) + for node in model.graph.node: + if output_name not in node.output: + continue + for attribute in node.attribute: + if attribute.name == "value" and attribute.HasField("t"): + return float(numpy_helper.to_array(attribute.t).reshape(-1)[0]) + raise ValueError(f"could not find ONNX Constant producing {output_name}") + + +def load_arrays(workspace, model_path): + model = onnx.load(model_path) + descriptors = onnx_io(model_path) + output_descriptors = {name: (index, dtype, shape) for index, name, dtype, shape in descriptors[1]} + missing = sorted(set(TAP_NAMES.values()) - set(output_descriptors)) + if missing: + raise ValueError("tap model is missing outputs: " + ", ".join(missing)) + + sim_arrays = parse_pim_simulator_outputs( + workspace / "simulation" / "out.bin", descriptors[1] + ) + reference = {} + simulated = {} + input_files = {} + for key, name in TAP_NAMES.items(): + index, _dtype, shape = output_descriptors[name] + csv_path = workspace / "outputs" / f"output{index}_{sanitize_output_name(name)}.csv" + reference[key] = np.loadtxt(csv_path, delimiter=",", dtype=np.float32).reshape(shape) + simulated[key] = np.asarray(sim_arrays[index], dtype=np.float32).reshape(shape) + input_files[key] = csv_path + return reference, simulated, input_files + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--workspace", type=Path, required=True, + help="validator workspace containing inputs, outputs, and simulation") + parser.add_argument("--model", type=Path, required=True, help="five-output ONNX tap model") + parser.add_argument("--output-dir", type=Path, required=True, + help="directory for arrays.npz, metadata.json, and decomposition.json") + args = parser.parse_args() + args.output_dir.mkdir(parents=True, exist_ok=True) + + model = onnx.load(args.model) + reference, simulated, source_files = load_arrays(args.workspace, args.model) + scale = np.float32(load_constant(model, "/model.10/m/m.0/attn/Constant_1_output_0")) + ref_v, ref_raw, ref_scaled, ref_rhs, ref_c = (reference[key] for key in ("v", "raw", "scaled", "rhs", "c")) + sim_v, sim_raw, sim_scaled, sim_rhs, sim_c = (simulated[key] for key in ("v", "raw", "scaled", "rhs", "c")) + + ref_score_transpose = np.swapaxes(ref_scaled, -1, -2) + sim_score_transpose = np.swapaxes(sim_scaled, -1, -2) + ref_ss_f32 = f32_matmul(ref_v, ref_rhs) + sim_ss_f32 = f32_matmul(sim_v, sim_rhs) + ref_ss_f64 = f64_matmul(ref_v, ref_rhs) + sim_ss_f64 = f64_matmul(sim_v, sim_rhs) + ref_split_f32 = (f32_matmul(ref_v, np.swapaxes(ref_raw, -1, -2)) * scale).astype(np.float32) + sim_split_f32 = (f32_matmul(sim_v, np.swapaxes(sim_raw, -1, -2)) * scale).astype(np.float32) + ref_split_f64 = f64_matmul(ref_v, np.swapaxes(ref_raw, -1, -2)) * np.float64(scale) + sim_split_f64 = f64_matmul(sim_v, np.swapaxes(sim_raw, -1, -2)) * np.float64(scale) + + arrays = { + **{f"ref_{key}": value for key, value in reference.items()}, + **{f"sim_{key}": value for key, value in simulated.items()}, + "ref_ss_f32": ref_ss_f32, + "sim_ss_f32": sim_ss_f32, + "ref_ss_f64": ref_ss_f64, + "sim_ss_f64": sim_ss_f64, + "ref_split_f32": ref_split_f32, + "sim_split_f32": sim_split_f32, + "ref_split_f64": ref_split_f64, + "sim_split_f64": sim_split_f64, + } + arrays_path = args.output_dir / "arrays.npz" + np.savez_compressed(arrays_path, **arrays) + + metrics = { + "validator_policy": { + "absolute_tolerance": ABSOLUTE_TOLERANCE, + "relative_tolerance": RELATIVE_TOLERANCE, + }, + "scale": float(scale), + "shape": list(ref_c.shape), + "tap_differences": {key: metric(simulated[key], reference[key]) for key in TAP_NAMES}, + "rhs_transpose_consistency": metric(ref_rhs, ref_score_transpose), + "sim_rhs_transpose_consistency": metric(sim_rhs, sim_score_transpose), + "c_sim_vs_ss_f32": metric(sim_c, ref_ss_f32), + "c_ref_vs_ss_f32": metric(ref_c, ref_ss_f32), + "c_sim_vs_simulated_inputs_ss_f32": metric(sim_c, sim_ss_f32), + "v_drift_only": metric(f32_matmul(sim_v, ref_rhs), ref_ss_f32), + "rhs_drift_only": metric(f32_matmul(ref_v, sim_rhs), ref_ss_f32), + "joint_input_drift": metric(sim_ss_f32, ref_ss_f32), + "scale_reassociation_reference": metric(ref_split_f32, ref_ss_f32), + "scale_reassociation_simulated": metric(sim_split_f32, sim_ss_f32), + "reference_accumulation_f32_vs_f64": metric(ref_ss_f32, ref_ss_f64), + "simulated_accumulation_f32_vs_f64": metric(sim_ss_f32, sim_ss_f64), + "split_accumulation_reference_f32_vs_f64": metric(ref_split_f32, ref_split_f64), + "split_accumulation_simulated_f32_vs_f64": metric(sim_split_f32, sim_split_f64), + } + decomposition_path = args.output_dir / "decomposition.json" + decomposition_path.write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8") + + metadata = { + "model": str(args.model), + "model_sha256": sha256(args.model), + "workspace": str(args.workspace), + "arrays_sha256": sha256(arrays_path), + "source_sha256": {key: sha256(path) for key, path in source_files.items()}, + "simulator_output_sha256": sha256(args.workspace / "simulation" / "out.bin"), + "input_sha256": sha256(args.workspace / "inputs" / "in0.csv"), + "outputs": TAP_NAMES, + "arrays": {key: {"dtype": str(value.dtype), "shape": list(value.shape)} for key, value in arrays.items()}, + } + (args.output_dir / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8") + print(json.dumps(metrics, indent=2)) + + +if __name__ == "__main__": + main()