From a41f694cf0faa19c2f22afd1c22664107cef358b Mon Sep 17 00:00:00 2001 From: NiccoloN Date: Fri, 29 May 2026 19:07:24 +0200 Subject: [PATCH] batched matmul pattern add conv helpers new validation tests for matmul --- .../ONNXToSpatial/Patterns/Math/Conv.cpp | 142 +-- .../ONNXToSpatial/Patterns/Math/MatMul.cpp | 860 +++++++++++++++--- validation/operations/README.md | 16 +- .../operations/conv/dynamic/conv_dynamic.onnx | Bin 189 -> 189 bytes .../operations/gemm/dynamic/gemm_dynamic.onnx | Bin 104 -> 104 bytes .../dynamic_alpha/gemm_dynamic_alpha.onnx | Bin 127 -> 127 bytes .../gemm/dynamic_beta/gemm_dynamic_beta.onnx | Bin 146 -> 146 bytes .../gemm/dynamic_bias/gemm_dynamic_bias.onnx | Bin 129 -> 129 bytes .../gemm_dynamic_bias_alpha_beta.onnx | Bin 174 -> 174 bytes .../dynamic_transB/gemm_dynamic_transB.onnx | Bin 126 -> 126 bytes .../operations/gemm/simple/gemm_simple.onnx | Bin 69829 -> 69801 bytes validation/operations/gen_tests.py | 51 ++ .../matmul_batched_3d_dynamic.onnx | Bin 0 -> 131 bytes .../matmul_batched_left_constant.onnx | Bin 0 -> 173 bytes .../matmul_batched_lhs_broadcast.onnx | Bin 0 -> 217 bytes .../matmul_batched_rhs_broadcast.onnx | Bin 0 -> 171 bytes .../matmul/dynamic/matmul_dynamic.onnx | Bin 108 -> 108 bytes .../left_constant/matmul_left_constant.onnx | Bin 130 -> 130 bytes 18 files changed, 877 insertions(+), 192 deletions(-) create mode 100644 validation/operations/matmul/batched_3d_dynamic/matmul_batched_3d_dynamic.onnx create mode 100644 validation/operations/matmul/batched_left_constant/matmul_batched_left_constant.onnx create mode 100644 validation/operations/matmul/batched_lhs_broadcast/matmul_batched_lhs_broadcast.onnx create mode 100644 validation/operations/matmul/batched_rhs_broadcast/matmul_batched_rhs_broadcast.onnx diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp index ca04343..3fa2a59 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/Conv.cpp @@ -51,23 +51,107 @@ static Value createPaddedRows(Value tensorValue, if (tensorType.getDimSize(0) == paddedRows) return tensorValue; - auto paddedType = RankedTensorType::get({paddedRows, tensorType.getDimSize(1)}, tensorType.getElementType()); + auto paddedType = + RankedTensorType::get({paddedRows, tensorType.getDimSize(1)}, tensorType.getElementType(), tensorType.getEncoding()); SmallVector lowPads = {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; SmallVector highPads = {rewriter.getIndexAttr(paddedRows - tensorType.getDimSize(0)), rewriter.getIndexAttr(0)}; auto padOp = tensor::PadOp::create(rewriter, loc, paddedType, tensorValue, lowPads, highPads); auto* padBlock = new Block(); - for (int i = 0; i < 2; i++) + for (int i = 0; i < 2; ++i) padBlock->addArgument(rewriter.getIndexType(), loc); padOp.getRegion().push_back(padBlock); rewriter.setInsertionPointToStart(padBlock); - auto zero = getOrCreateConstant(rewriter, padOp.getOperation(), rewriter.getZeroAttr(tensorType.getElementType()), + auto zero = getOrCreateConstant(rewriter, + padOp.getOperation(), + rewriter.getZeroAttr(tensorType.getElementType()), tensorType.getElementType()); tensor::YieldOp::create(rewriter, loc, zero); rewriter.setInsertionPointAfter(padOp); return padOp.getResult(); } +static Value packRowsForParallelGemm(Value rows, + RankedTensorType rowsType, + int64_t packFactor, + ConversionPatternRewriter& rewriter, + Location loc) { + if (packFactor == 1) + return rows; + + const int64_t packedNumRows = ceilIntegerDivide(rowsType.getDimSize(0), packFactor); + const int64_t paddedNumRows = packedNumRows * packFactor; + const int64_t rowWidth = rowsType.getDimSize(1); + auto groupedType = + RankedTensorType::get({packedNumRows, packFactor, rowWidth}, rowsType.getElementType(), rowsType.getEncoding()); + auto packedType = + RankedTensorType::get({packedNumRows, packFactor * rowWidth}, rowsType.getElementType(), rowsType.getEncoding()); + + Value paddedRows = createPaddedRows(rows, rowsType, paddedNumRows, rewriter, loc); + Value groupedRows = tensor::ExpandShapeOp::create(rewriter, + loc, + groupedType, + paddedRows, + SmallVector { + {0, 1}, + {2} + }); + return tensor::CollapseShapeOp::create(rewriter, + loc, + packedType, + groupedRows, + SmallVector { + {0}, + {1, 2} + }); +} + +static Value unpackRowsFromParallelGemm(Value packedRows, + RankedTensorType packedRowsType, + int64_t unpackedRows, + int64_t rowWidth, + int64_t packFactor, + ConversionPatternRewriter& rewriter, + Location loc) { + if (packFactor == 1) + return packedRows; + + const int64_t packedNumRows = packedRowsType.getDimSize(0); + const int64_t paddedNumRows = packedNumRows * packFactor; + auto expandedType = + RankedTensorType::get({packedNumRows, packFactor, rowWidth}, + packedRowsType.getElementType(), + packedRowsType.getEncoding()); + auto paddedType = + RankedTensorType::get({paddedNumRows, rowWidth}, packedRowsType.getElementType(), packedRowsType.getEncoding()); + auto unpackedType = + RankedTensorType::get({unpackedRows, rowWidth}, packedRowsType.getElementType(), packedRowsType.getEncoding()); + + Value expandedRows = tensor::ExpandShapeOp::create(rewriter, + loc, + expandedType, + packedRows, + SmallVector { + {0}, + {1, 2} + }); + Value paddedRows = tensor::CollapseShapeOp::create(rewriter, + loc, + paddedType, + expandedRows, + SmallVector { + {0, 1}, + {2} + }); + if (paddedNumRows == unpackedRows) + return paddedRows; + + SmallVector offsets {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; + SmallVector sizes {rewriter.getIndexAttr(unpackedRows), rewriter.getIndexAttr(rowWidth)}; + SmallVector strides {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; + return tensor::ExtractSliceOp::create(rewriter, loc, unpackedType, paddedRows, offsets, sizes, strides); +} + static Value buildPackedWeight(DenseElementsAttr wDenseAttr, Value wTrans, RankedTensorType wType, @@ -189,7 +273,6 @@ static Value createIm2colRowComputes(Value x, Location loc) { auto elemType = xType.getElementType(); constexpr size_t numInputs = 1; - const int64_t packedNumRows = ceilIntegerDivide(numPatches, packFactor); auto im2colComputeOp = createSpatCompute(rewriter, loc, TypeRange {gemmInputRowsType}, {}, x, [&](Value xArg) { Value paddedInput = xArg; @@ -278,26 +361,7 @@ static Value createIm2colRowComputes(Value x, Value gemmInputRows = im2col; if (packFactor != 1) { - const int64_t paddedNumPatches = packedNumRows * packFactor; - auto groupedType = RankedTensorType::get({packedNumRows, packFactor, patchSize}, elemType); - auto packedType = RankedTensorType::get({packedNumRows, packFactor * patchSize}, elemType); - Value paddedIm2col = createPaddedRows(im2col, im2colType, paddedNumPatches, rewriter, loc); - Value groupedIm2col = tensor::ExpandShapeOp::create(rewriter, - loc, - groupedType, - paddedIm2col, - SmallVector { - {0, 1}, - {2} - }); - gemmInputRows = tensor::CollapseShapeOp::create(rewriter, - loc, - packedType, - groupedIm2col, - SmallVector { - {0}, - {1, 2} - }); + gemmInputRows = packRowsForParallelGemm(im2col, im2colType, packFactor, rewriter, loc); } spatial::SpatYieldOp::create(rewriter, loc, gemmInputRows); @@ -316,41 +380,15 @@ static Value createCollectedConvOutput(ValueRange gemmRows, int64_t packFactor, ConversionPatternRewriter& rewriter, Location loc) { - const int64_t packedNumRows = ceilIntegerDivide(numPatches, packFactor); - const int64_t paddedNumPatches = packedNumRows * packFactor; auto collectComputeOp = createSpatCompute(rewriter, loc, convType, {}, gemmRows, [&](ValueRange gemmRowArgs) { Value gemmOut; if (packFactor == 1) { gemmOut = createSpatConcat(rewriter, loc, /*axis=*/0, gemmRowArgs); } else { - auto expandedType = RankedTensorType::get({packedNumRows, packFactor, numChannelsOut}, outType.getElementType()); - auto paddedType = RankedTensorType::get({paddedNumPatches, numChannelsOut}, outType.getElementType()); Value packedOutput = createSpatConcat(rewriter, loc, /*axis=*/0, gemmRowArgs); - Value expandedOutput = tensor::ExpandShapeOp::create(rewriter, - loc, - expandedType, - packedOutput, - SmallVector { - {0}, - {1, 2} - }); - Value paddedOutput = tensor::CollapseShapeOp::create(rewriter, - loc, - paddedType, - expandedOutput, - SmallVector { - {0, 1}, - {2} - }); - - gemmOut = paddedOutput; - if (paddedNumPatches != numPatches) { - SmallVector offsets = {rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; - SmallVector sizes = {rewriter.getIndexAttr(numPatches), rewriter.getIndexAttr(numChannelsOut)}; - SmallVector strides = {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; - gemmOut = tensor::ExtractSliceOp::create(rewriter, loc, gemmOutType, paddedOutput, offsets, sizes, strides); - } + gemmOut = unpackRowsFromParallelGemm( + packedOutput, cast(packedOutput.getType()), numPatches, numChannelsOut, packFactor, rewriter, loc); } // Restore to NCHW layout: diff --git a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp index eb7329e..7037125 100644 --- a/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp +++ b/src/PIM/Conversion/ONNXToSpatial/Patterns/Math/MatMul.cpp @@ -1,3 +1,5 @@ +#include "mlir/Dialect/Arith/IR/Arith.h" +#include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/PatternMatch.h" @@ -5,9 +7,6 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" -#include -#include - #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Common/Common.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/CompileTime.hpp" #include "src/Accelerators/PIM/Conversion/ONNXToSpatial/Patterns.hpp" @@ -66,6 +65,26 @@ expandBatchDims(Value value, RankedTensorType outputType, size_t batchRank, Patt return materializeOrComputeUnary(value, outputType, rewriter, loc, buildExpanded); } +static Value ensureBatchedTensor( + Value value, int64_t batchSize, int64_t rows, int64_t cols, PatternRewriter& rewriter, Location loc) { + auto type = cast(value.getType()); + if (type.getRank() == 3) + return value; + + auto batchedType = RankedTensorType::get({batchSize, rows, cols}, type.getElementType(), type.getEncoding()); + auto buildExpanded = [&](Value input) -> Value { + return tensor::ExpandShapeOp::create(rewriter, + loc, + batchedType, + input, + SmallVector { + {0, 1}, + {2} + }); + }; + return materializeOrComputeUnary(value, batchedType, rewriter, loc, buildExpanded); +} + static Value extractBatchMatrix(Value value, int64_t batchIndex, int64_t batchSize, @@ -130,164 +149,737 @@ static Value transposeLastTwoDimsInCompute(Value value, PatternRewriter& rewrite perm = {0, 2, 1}; } + auto transposeCompute = createSpatCompute<1>(rewriter, loc, transposedType, {}, ValueRange {value}, [&](Value input) { + Value transposed = ONNXTransposeOp::create(rewriter, loc, transposedType, input, rewriter.getI64ArrayAttr(perm)); + spatial::SpatYieldOp::create(rewriter, loc, transposed); + }); + return transposeCompute.getResult(0); +} + +static Value createZeroPaddedTensor(Value value, RankedTensorType resultType, PatternRewriter& rewriter, Location loc) { + auto sourceType = cast(value.getType()); + SmallVector lowPads(sourceType.getRank(), rewriter.getIndexAttr(0)); + SmallVector highPads; + highPads.reserve(sourceType.getRank()); + for (auto [sourceDim, resultDim] : llvm::zip(sourceType.getShape(), resultType.getShape())) + highPads.push_back(rewriter.getIndexAttr(resultDim - sourceDim)); + + auto padOp = tensor::PadOp::create(rewriter, loc, resultType, value, lowPads, highPads); + auto* padBlock = new Block(); + for (int64_t i = 0; i < sourceType.getRank(); ++i) + padBlock->addArgument(rewriter.getIndexType(), loc); + padOp.getRegion().push_back(padBlock); + rewriter.setInsertionPointToStart(padBlock); + auto zero = getOrCreateConstant( + rewriter, padOp.getOperation(), rewriter.getZeroAttr(sourceType.getElementType()), sourceType.getElementType()); + tensor::YieldOp::create(rewriter, loc, zero); + rewriter.setInsertionPointAfter(padOp); + return padOp.getResult(); +} + +static Value createPaddedBatchedInputCompute(Value input, + RankedTensorType paddedInputType, + PatternRewriter& rewriter, + Location loc) { + auto inputType = cast(input.getType()); + if (inputType == paddedInputType) + return input; + + auto computeOp = createSpatCompute<1>(rewriter, loc, TypeRange {paddedInputType}, {}, input, [&](Value computeInput) { + Value paddedInput = createZeroPaddedTensor(computeInput, paddedInputType, rewriter, loc); + spatial::SpatYieldOp::create(rewriter, loc, paddedInput); + }); + return computeOp.getResult(0); +} + +static FailureOr materializePaddedBatchedWeight( + Value value, int64_t sourceBatch, int64_t targetBatch, RankedTensorType resultType, PatternRewriter& rewriter) { + auto sourceType = cast(value.getType()); + if (sourceType == resultType) + return value; + + auto denseAttr = getHostConstDenseElementsAttr(value); + if (!denseAttr) + return failure(); + + const int64_t sourceRows = sourceType.getRank() == 2 ? sourceType.getDimSize(0) : sourceType.getDimSize(1); + const int64_t sourceCols = sourceType.getRank() == 2 ? sourceType.getDimSize(1) : sourceType.getDimSize(2); + const int64_t targetRows = resultType.getDimSize(1); + const int64_t targetCols = resultType.getDimSize(2); + SmallVector sourceValues(denseAttr.getValues()); + SmallVector resultValues(resultType.getNumElements(), rewriter.getZeroAttr(resultType.getElementType())); + + for (int64_t batchIdx = 0; batchIdx < targetBatch; ++batchIdx) { + const int64_t sourceBatchIdx = sourceType.getRank() == 2 ? 0 : (sourceBatch == 1 ? 0 : batchIdx); + const int64_t sourceBatchBase = sourceType.getRank() == 2 ? 0 : sourceBatchIdx * sourceRows * sourceCols; + const int64_t targetBatchBase = batchIdx * targetRows * targetCols; + for (int64_t row = 0; row < sourceRows; ++row) + for (int64_t col = 0; col < sourceCols; ++col) + resultValues[targetBatchBase + row * targetCols + col] = sourceValues[sourceBatchBase + row * sourceCols + col]; + } + + auto resultAttr = DenseElementsAttr::get(resultType, resultValues); + return getOrCreateConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), resultAttr, resultType); +} + +static Value extractBatchedATile(Value a, + int64_t sourceBatchCount, + Value batch, + Value row, + Value kOffset, + RankedTensorType aTileType, + PatternRewriter& rewriter, + Location loc) { + auto aSliceType = RankedTensorType::get({1, 1, aTileType.getDimSize(1)}, aTileType.getElementType()); + SmallVector offsets { + sourceBatchCount == 1 ? OpFoldResult(rewriter.getIndexAttr(0)) : OpFoldResult(batch), row, kOffset}; + SmallVector sizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(aTileType.getDimSize(1))}; + auto slice = + tensor::ExtractSliceOp::create(rewriter, loc, aSliceType, a, offsets, sizes, getUnitStrides(rewriter, 3)); + return tensor::CollapseShapeOp::create(rewriter, + loc, + aTileType, + slice, + SmallVector { + {0, 1}, + {2} + }); +} + +static Value extractBatchedBTile(Value b, + int64_t sourceBatchCount, + Value batch, + Value kOffset, + Value hOffset, + RankedTensorType bTileType, + PatternRewriter& rewriter, + Location loc) { + auto bSliceType = + RankedTensorType::get({1, bTileType.getDimSize(0), bTileType.getDimSize(1)}, bTileType.getElementType()); + SmallVector offsets { + sourceBatchCount == 1 ? OpFoldResult(rewriter.getIndexAttr(0)) : OpFoldResult(batch), kOffset, hOffset}; + SmallVector sizes {rewriter.getIndexAttr(1), + rewriter.getIndexAttr(bTileType.getDimSize(0)), + rewriter.getIndexAttr(bTileType.getDimSize(1))}; + auto slice = + tensor::ExtractSliceOp::create(rewriter, loc, bSliceType, b, offsets, sizes, getUnitStrides(rewriter, 3)); + return tensor::CollapseShapeOp::create(rewriter, + loc, + bTileType, + slice, + SmallVector { + {0, 1}, + {2} + }); +} + +static Value getBatchLaneIndex( + Value lane, int64_t numOutRows, int64_t numKSlices, int64_t numOutHSlices, PatternRewriter& rewriter, Location loc) { + return floorDivIndexByConstant(rewriter, loc, lane, numOutRows * numKSlices * numOutHSlices); +} + +static spatial::SpatComputeBatch createBatchedVmmBatch(Value a, + Value b, + RankedTensorType aType, + int64_t aBatchCount, + RankedTensorType bType, + int64_t bBatchCount, + RankedTensorType partialPiecesType, + int64_t numOutRows, + int64_t numKSlices, + int64_t numOutHSlices, + PatternRewriter& rewriter, + Location loc) { + const int64_t laneCount = partialPiecesType.getDimSize(0); + auto batchOp = createSpatComputeBatch( + rewriter, + loc, + TypeRange {partialPiecesType}, + laneCount, + ValueRange {b}, + ValueRange {a}, + [&](detail::SpatComputeBatchBodyArgs args) { + Value row = modIndexByConstant(rewriter, loc, args.lane, numOutRows); + Value outerLane = floorDivIndexByConstant(rewriter, loc, args.lane, numOutRows); + Value batch = getBatchLaneIndex(args.lane, numOutRows, numKSlices, numOutHSlices, rewriter, loc); + Value sliceLane = modIndexByConstant(rewriter, loc, outerLane, numKSlices * numOutHSlices); + Value kSlice = modIndexByConstant(rewriter, loc, sliceLane, numKSlices); + Value hSlice = floorDivIndexByConstant(rewriter, loc, sliceLane, numKSlices); + Value kOffset = + multiplyIndexByConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), kSlice, crossbarSize.getValue()); + Value hOffset = + multiplyIndexByConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), hSlice, crossbarSize.getValue()); + + auto aTileType = + RankedTensorType::get({1, static_cast(crossbarSize.getValue())}, aType.getElementType()); + auto bTileType = RankedTensorType::get( + {static_cast(crossbarSize.getValue()), static_cast(crossbarSize.getValue())}, + bType.getElementType()); + auto pieceType = + RankedTensorType::get({1, static_cast(crossbarSize.getValue())}, partialPiecesType.getElementType()); + + Value aTile = + extractBatchedATile(args.inputs.front(), aBatchCount, batch, row, kOffset, aTileType, rewriter, loc); + Value bTile = + extractBatchedBTile(args.weights.front(), bBatchCount, batch, kOffset, hOffset, bTileType, rewriter, loc); + Value piece = spatial::SpatVMMOp::create(rewriter, loc, pieceType, bTile, aTile).getResult(); + + SmallVector pieceOffsets {args.lane, rewriter.getIndexAttr(0)}; + SmallVector pieceSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(crossbarSize.getValue())}; + createParallelInsertSliceIntoBatchOutput( + rewriter, loc, piece, args.outputs.front(), pieceOffsets, pieceSizes, getUnitStrides(rewriter, 2)); + }); + assert(succeeded(batchOp) && "expected batched MatMul VMM construction to succeed"); + return *batchOp; +} + +static Value extractDynamicBatchedBColumn(Value matrix, + int64_t sourceBatchCount, + Value batch, + Value column, + RankedTensorType vectorType, + PatternRewriter& rewriter, + Location loc) { + auto columnSliceType = RankedTensorType::get({1, vectorType.getDimSize(1), 1}, vectorType.getElementType()); + SmallVector offsets {sourceBatchCount == 1 ? OpFoldResult(rewriter.getIndexAttr(0)) + : OpFoldResult(batch), + rewriter.getIndexAttr(0), + column}; + SmallVector sizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(vectorType.getDimSize(1)), rewriter.getIndexAttr(1)}; + SmallVector strides {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; + Value columnSlice = tensor::ExtractSliceOp::create(rewriter, loc, columnSliceType, matrix, offsets, sizes, strides); + auto collapsedType = RankedTensorType::get({vectorType.getDimSize(1)}, vectorType.getElementType()); + Value collapsed = tensor::CollapseShapeOp::create(rewriter, + loc, + collapsedType, + columnSlice, + SmallVector { + {0, 1, 2} + }) + .getResult(); + return tensor::ExpandShapeOp::create(rewriter, + loc, + vectorType, + collapsed, + SmallVector { + {0, 1} + }) + .getResult(); +} + +static Value extractDynamicBatchedBRow(Value matrix, + int64_t sourceBatchCount, + Value batch, + Value row, + RankedTensorType vectorType, + PatternRewriter& rewriter, + Location loc) { + auto rowSliceType = RankedTensorType::get({1, 1, vectorType.getDimSize(1)}, vectorType.getElementType()); + SmallVector offsets {sourceBatchCount == 1 ? OpFoldResult(rewriter.getIndexAttr(0)) + : OpFoldResult(batch), + row, + rewriter.getIndexAttr(0)}; + SmallVector sizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(vectorType.getDimSize(1))}; + auto rowSlice = + tensor::ExtractSliceOp::create(rewriter, loc, rowSliceType, matrix, offsets, sizes, getUnitStrides(rewriter, 3)); + return tensor::CollapseShapeOp::create(rewriter, + loc, + vectorType, + rowSlice, + SmallVector { + {0, 1}, + {2} + }); +} + +static Value extractDynamicBatchedRowVector(Value matrix, + int64_t sourceBatchCount, + Value batch, + Value row, + RankedTensorType vectorType, + PatternRewriter& rewriter, + Location loc) { + auto rowSliceType = RankedTensorType::get({1, 1, vectorType.getDimSize(1)}, vectorType.getElementType()); + SmallVector offsets {sourceBatchCount == 1 ? OpFoldResult(rewriter.getIndexAttr(0)) + : OpFoldResult(batch), + row, + rewriter.getIndexAttr(0)}; + SmallVector sizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(vectorType.getDimSize(1))}; + auto rowSlice = + tensor::ExtractSliceOp::create(rewriter, loc, rowSliceType, matrix, offsets, sizes, getUnitStrides(rewriter, 3)); + return tensor::CollapseShapeOp::create(rewriter, + loc, + vectorType, + rowSlice, + SmallVector { + {0, 1}, + {2} + }); +} + +static spatial::SpatComputeBatch createBatchedVvdmulBatch(Value a, + int64_t aBatchCount, + Value b, + int64_t bBatchCount, + RankedTensorType aType, + RankedTensorType bType, + RankedTensorType scalarPiecesType, + RankedTensorType outType, + bool bAlreadyTransposed, + 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 * numOutRows * numOutCols; + auto batchOp = createSpatComputeBatch( + rewriter, + loc, + TypeRange {scalarPiecesType}, + laneCount, + ValueRange {}, + ValueRange {a, b}, + [&](detail::SpatComputeBatchBodyArgs args) { + Value batch = floorDivIndexByConstant(rewriter, loc, args.lane, numOutRows * numOutCols); + Value batchLane = modIndexByConstant(rewriter, loc, args.lane, numOutRows * numOutCols); + Value row = floorDivIndexByConstant(rewriter, loc, batchLane, numOutCols); + Value column = modIndexByConstant(rewriter, loc, batchLane, numOutCols); + + auto vectorType = RankedTensorType::get({1, reductionSize}, aType.getElementType()); + auto scalarType = RankedTensorType::get({1, 1}, outType.getElementType()); + Value aVector = + extractDynamicBatchedRowVector(args.inputs[0], aBatchCount, batch, row, vectorType, rewriter, loc); + Value bVector = + bAlreadyTransposed + ? extractDynamicBatchedBRow(args.inputs[1], bBatchCount, batch, column, vectorType, rewriter, loc) + : extractDynamicBatchedBColumn(args.inputs[1], bBatchCount, batch, column, vectorType, rewriter, loc); + Value scalar = spatial::SpatVVDMulOp::create(rewriter, loc, scalarType, aVector, bVector).getResult(); + SmallVector outputOffsets {args.lane, rewriter.getIndexAttr(0)}; + SmallVector scalarSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; + createParallelInsertSliceIntoBatchOutput( + rewriter, loc, scalar, args.outputs.front(), outputOffsets, scalarSizes, getUnitStrides(rewriter, 2)); + }); + assert(succeeded(batchOp) && "expected batched MatMul VVDMul construction to succeed"); + return *batchOp; +} + +static Value createBatchedDynamicOutputCompute(Value scalarPieces, + RankedTensorType scalarPiecesType, + RankedTensorType outType, + PatternRewriter& rewriter, + Location loc) { + const int64_t laneCount = scalarPiecesType.getDimSize(0); + const int64_t numOutRows = outType.getDimSize(1); + const int64_t numOutCols = outType.getDimSize(2); + auto scalarType = RankedTensorType::get({1, 1}, outType.getElementType()); + auto outputScalarType = RankedTensorType::get({1, 1, 1}, outType.getElementType()); + + auto computeOp = + createSpatCompute<1>(rewriter, loc, TypeRange {outType}, {}, ValueRange {scalarPieces}, [&](Value pieces) { + 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 = scf::ForOp::create(rewriter, loc, c0, cLaneCount, c1, ValueRange {outputInit}); + rewriter.setInsertionPointToStart(loop.getBody()); + + Value lane = loop.getInductionVar(); + Value outputAcc = loop.getRegionIterArgs().front(); + Value batch = floorDivIndexByConstant(rewriter, loc, lane, numOutRows * numOutCols); + Value batchLane = modIndexByConstant(rewriter, loc, lane, numOutRows * numOutCols); + Value row = floorDivIndexByConstant(rewriter, loc, batchLane, numOutCols); + Value column = modIndexByConstant(rewriter, loc, batchLane, numOutCols); + SmallVector scalarOffsets {lane, rewriter.getIndexAttr(0)}; + SmallVector scalarSizes {rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; + Value scalar = tensor::ExtractSliceOp::create( + rewriter, loc, scalarType, pieces, scalarOffsets, scalarSizes, getUnitStrides(rewriter, 2)); + Value expanded = tensor::ExpandShapeOp::create(rewriter, + loc, + outputScalarType, + scalar, + SmallVector { + {0}, + {1, 2} + }); + SmallVector outputOffsets {batch, row, column}; + SmallVector outputSizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(1), rewriter.getIndexAttr(1)}; + scf::YieldOp::create( + rewriter, + loc, + tensor::InsertSliceOp::create( + rewriter, loc, expanded, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3)) + .getResult()); + + rewriter.setInsertionPointAfter(loop); + spatial::SpatYieldOp::create(rewriter, loc, loop.getResult(0)); + }); + return computeOp.getResult(0); +} + +static Value transposeBatchedOutput(Value value, RankedTensorType outputType, PatternRewriter& rewriter, Location loc) { auto transposeCompute = - createSpatCompute<1>(rewriter, loc, transposedType, {}, ValueRange {value}, [&](Value input) { - Value transposed = ONNXTransposeOp::create(rewriter, loc, transposedType, input, rewriter.getI64ArrayAttr(perm)); + createSpatCompute<1>(rewriter, loc, TypeRange {outputType}, {}, ValueRange {value}, [&](Value input) { + Value transposed = ONNXTransposeOp::create(rewriter, loc, outputType, input, rewriter.getI64ArrayAttr({0, 2, 1})); spatial::SpatYieldOp::create(rewriter, loc, transposed); }); return transposeCompute.getResult(0); } -static Value concatValues(ValueRange inputs, int64_t axis, PatternRewriter& rewriter, Location loc) { - auto firstType = cast(inputs.front().getType()); - SmallVector outputShape(firstType.getShape().begin(), firstType.getShape().end()); - int64_t concatDimSize = 0; - for (Value input : inputs) - concatDimSize += cast(input.getType()).getDimSize(axis); - outputShape[axis] = concatDimSize; - auto resultType = RankedTensorType::get(outputShape, firstType.getElementType(), firstType.getEncoding()); +static Value extractBatchedReductionPiece(Value partialPiecesArg, + Value batch, + Value hSlice, + int64_t kSlice, + RankedTensorType pieceType, + int64_t numKSlices, + int64_t numOutHSlices, + int64_t numOutRows, + PatternRewriter& rewriter, + Location loc) { + Value batchOffset = multiplyIndexByConstant( + rewriter, rewriter.getInsertionBlock()->getParentOp(), batch, numOutRows * numKSlices * numOutHSlices); + Value hOffset = + multiplyIndexByConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), hSlice, numKSlices * numOutRows); + Value kOffset = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), kSlice * numOutRows); + Value batchAndHSlice = arith::AddIOp::create(rewriter, loc, batchOffset, hOffset); + Value pieceOffset = arith::AddIOp::create(rewriter, loc, batchAndHSlice, kOffset); + SmallVector offsets {pieceOffset, rewriter.getIndexAttr(0)}; + SmallVector sizes {rewriter.getIndexAttr(numOutRows), rewriter.getIndexAttr(crossbarSize.getValue())}; + return tensor::ExtractSliceOp::create( + rewriter, loc, pieceType, partialPiecesArg, offsets, sizes, getUnitStrides(rewriter, 2)); +} - if (llvm::all_of(inputs, isCompileTimeComputable)) - return createSpatConcat(rewriter, loc, axis, inputs); +static Value reduceBatchedPartialPiecesForHSlice(Value partialPiecesArg, + Value batch, + Value hSlice, + RankedTensorType pieceType, + int64_t numKSlices, + int64_t numOutHSlices, + int64_t numOutRows, + PatternRewriter& rewriter, + Location loc) { + SmallVector activePieces; + activePieces.reserve(numKSlices); + for (int64_t kSlice = 0; kSlice < numKSlices; ++kSlice) + activePieces.push_back(extractBatchedReductionPiece( + partialPiecesArg, batch, hSlice, kSlice, pieceType, numKSlices, numOutHSlices, numOutRows, rewriter, loc)); - auto concatCompute = createSpatCompute(rewriter, loc, TypeRange {resultType}, {}, inputs, [&](ValueRange args) { - spatial::SpatYieldOp::create(rewriter, loc, createSpatConcat(rewriter, loc, axis, args)); - }); - return concatCompute.getResult(0); + while (activePieces.size() > 1) { + SmallVector nextPieces; + nextPieces.reserve((activePieces.size() + 1) / 2); + for (size_t pieceIndex = 0; pieceIndex + 1 < activePieces.size(); pieceIndex += 2) + nextPieces.push_back( + spatial::SpatVAddOp::create(rewriter, loc, pieceType, activePieces[pieceIndex], activePieces[pieceIndex + 1]) + .getResult()); + if (activePieces.size() % 2 != 0) + nextPieces.push_back(activePieces.back()); + activePieces = std::move(nextPieces); + } + + return activePieces.front(); +} + +static Value createBatchedReductionCompute(Value partialPieces, + RankedTensorType partialPiecesType, + RankedTensorType outType, + RankedTensorType paddedOutType, + int64_t numBatches, + int64_t numKSlices, + PatternRewriter& rewriter, + Location loc) { + auto computeOp = createSpatCompute<1>( + rewriter, loc, TypeRange {outType}, {}, ValueRange {partialPieces}, [&](Value partialPiecesArg) { + const int64_t numOutRows = outType.getDimSize(1); + const int64_t numOutHSlices = ceilIntegerDivide(outType.getDimSize(2), crossbarSize.getValue()); + auto pieceType = RankedTensorType::get({numOutRows, static_cast(crossbarSize.getValue())}, + partialPiecesType.getElementType()); + auto outputSliceType = RankedTensorType::get({1, numOutRows, static_cast(crossbarSize.getValue())}, + partialPiecesType.getElementType()); + + Value outputInit = + tensor::EmptyOp::create(rewriter, loc, paddedOutType.getShape(), paddedOutType.getElementType()).getResult(); + Value c0 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 0); + Value c1 = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), 1); + Value cNumBatches = getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), numBatches); + Value cNumOutHSlices = + getOrCreateIndexConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), numOutHSlices); + + auto batchLoop = scf::ForOp::create(rewriter, loc, c0, cNumBatches, c1, ValueRange {outputInit}); + rewriter.setInsertionPointToStart(batchLoop.getBody()); + Value batch = batchLoop.getInductionVar(); + Value batchAcc = batchLoop.getRegionIterArgs().front(); + + auto hLoop = scf::ForOp::create(rewriter, loc, c0, cNumOutHSlices, c1, ValueRange {batchAcc}); + rewriter.setInsertionPointToStart(hLoop.getBody()); + Value hSlice = hLoop.getInductionVar(); + Value outputAcc = hLoop.getRegionIterArgs().front(); + + Value reduced = reduceBatchedPartialPiecesForHSlice( + partialPiecesArg, batch, hSlice, pieceType, numKSlices, numOutHSlices, numOutRows, rewriter, loc); + Value expandedReduced = tensor::ExpandShapeOp::create(rewriter, + loc, + outputSliceType, + reduced, + SmallVector { + {0, 1}, + {2} + }); + Value hOffset = + multiplyIndexByConstant(rewriter, rewriter.getInsertionBlock()->getParentOp(), hSlice, crossbarSize.getValue()); + SmallVector outputOffsets {batch, rewriter.getIndexAttr(0), hOffset}; + SmallVector outputSizes { + rewriter.getIndexAttr(1), rewriter.getIndexAttr(numOutRows), rewriter.getIndexAttr(crossbarSize.getValue())}; + scf::YieldOp::create( + rewriter, + loc, + tensor::InsertSliceOp::create( + rewriter, loc, expandedReduced, outputAcc, outputOffsets, outputSizes, getUnitStrides(rewriter, 3)) + .getResult()); + + rewriter.setInsertionPointAfter(hLoop); + scf::YieldOp::create(rewriter, loc, hLoop.getResult(0)); + + rewriter.setInsertionPointAfter(batchLoop); + Value paddedOutput = batchLoop.getResult(0); + Value result = paddedOutput; + if (paddedOutType != outType) { + SmallVector outputOffsets { + rewriter.getIndexAttr(0), rewriter.getIndexAttr(0), rewriter.getIndexAttr(0)}; + SmallVector outputSizes {rewriter.getIndexAttr(numBatches), + rewriter.getIndexAttr(outType.getDimSize(1)), + rewriter.getIndexAttr(outType.getDimSize(2))}; + result = tensor::ExtractSliceOp::create( + rewriter, loc, outType, paddedOutput, outputOffsets, outputSizes, getUnitStrides(rewriter, 3)); + } + spatial::SpatYieldOp::create(rewriter, loc, result); + }); + return computeOp.getResult(0); +} + +struct MatMulShapeInfo { + RankedTensorType lhsType; + RankedTensorType rhsType; + RankedTensorType outType; + SmallVector batchShape; + int64_t lhsBatch; + int64_t rhsBatch; + int64_t batch; + int64_t m; + int64_t k; + int64_t n; +}; + +static FailureOr analyzeMatMulShape(ONNXMatMulOp matmulOp) { + auto lhsType = dyn_cast(matmulOp.getA().getType()); + auto rhsType = dyn_cast(matmulOp.getB().getType()); + auto outType = dyn_cast(matmulOp.getY().getType()); + if (!lhsType || !rhsType || !outType || !lhsType.hasStaticShape() || !rhsType.hasStaticShape() + || !outType.hasStaticShape()) + return failure(); + if (lhsType.getRank() < 2 || rhsType.getRank() < 2 || outType.getRank() < 2) + return failure(); + if (!hasStaticPositiveShape(lhsType) || !hasStaticPositiveShape(rhsType) || !hasStaticPositiveShape(outType)) + return failure(); + + SmallVector lhsBatchShape(lhsType.getShape().begin(), lhsType.getShape().end() - 2); + SmallVector rhsBatchShape(rhsType.getShape().begin(), rhsType.getShape().end() - 2); + auto batchShape = inferSupportedBatchShape(lhsBatchShape, rhsBatchShape); + if (failed(batchShape)) + return failure(); + + const int64_t lhsBatch = lhsBatchShape.empty() ? 1 : getStaticShapeElementCount(lhsBatchShape); + const int64_t rhsBatch = rhsBatchShape.empty() ? 1 : getStaticShapeElementCount(rhsBatchShape); + const int64_t batch = batchShape->empty() ? 1 : getStaticShapeElementCount(*batchShape); + const int64_t m = lhsType.getDimSize(lhsType.getRank() - 2); + const int64_t k = lhsType.getDimSize(lhsType.getRank() - 1); + const int64_t rhsK = rhsType.getDimSize(rhsType.getRank() - 2); + const int64_t n = rhsType.getDimSize(rhsType.getRank() - 1); + if (k != rhsK) + return failure(); + + if (outType.getRank() == 2) { + if (batch != 1 || outType.getDimSize(0) != m || outType.getDimSize(1) != n) + return failure(); + } + else { + SmallVector outBatchShape(outType.getShape().begin(), outType.getShape().end() - 2); + if (!llvm::equal(outBatchShape, *batchShape) || outType.getDimSize(outType.getRank() - 2) != m + || outType.getDimSize(outType.getRank() - 1) != n) + return failure(); + } + + return MatMulShapeInfo {lhsType, rhsType, outType, *batchShape, lhsBatch, rhsBatch, batch, m, k, n}; } struct MatMulToGemm : OpRewritePattern { using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(ONNXMatMulOp matmulOp, PatternRewriter& rewriter) const override { - auto lhsType = dyn_cast(matmulOp.getA().getType()); - auto rhsType = dyn_cast(matmulOp.getB().getType()); - auto outType = dyn_cast(matmulOp.getY().getType()); - if (!lhsType || !rhsType || !outType || !lhsType.hasStaticShape() || !rhsType.hasStaticShape() - || !outType.hasStaticShape()) + auto shapeInfo = analyzeMatMulShape(matmulOp); + if (failed(shapeInfo) || shapeInfo->outType.getRank() != 2) return failure(); - if (lhsType.getRank() < 2 || rhsType.getRank() < 2 || outType.getRank() < 2) - return failure(); - if (!hasStaticPositiveShape(lhsType) || !hasStaticPositiveShape(rhsType) || !hasStaticPositiveShape(outType)) - return failure(); - - SmallVector lhsBatchShape(lhsType.getShape().begin(), lhsType.getShape().end() - 2); - SmallVector rhsBatchShape(rhsType.getShape().begin(), rhsType.getShape().end() - 2); - auto batchShape = inferSupportedBatchShape(lhsBatchShape, rhsBatchShape); - if (failed(batchShape)) - return failure(); - const int64_t lhsBatch = lhsBatchShape.empty() ? 1 : getStaticShapeElementCount(lhsBatchShape); - const int64_t rhsBatch = rhsBatchShape.empty() ? 1 : getStaticShapeElementCount(rhsBatchShape); - const int64_t batch = batchShape->empty() ? 1 : getStaticShapeElementCount(*batchShape); - - const int64_t m = lhsType.getDimSize(lhsType.getRank() - 2); - const int64_t k = lhsType.getDimSize(lhsType.getRank() - 1); - const int64_t rhsK = rhsType.getDimSize(rhsType.getRank() - 2); - const int64_t n = rhsType.getDimSize(rhsType.getRank() - 1); - if (k != rhsK) - return failure(); - - if (outType.getRank() == 2) { - if (batch != 1 || outType.getDimSize(0) != m || outType.getDimSize(1) != n) - return failure(); - } - else { - SmallVector outBatchShape(outType.getShape().begin(), outType.getShape().end() - 2); - if (!llvm::equal(outBatchShape, *batchShape) || outType.getDimSize(outType.getRank() - 2) != m - || outType.getDimSize(outType.getRank() - 1) != n) - return failure(); - } Location loc = matmulOp.getLoc(); bool useTransposedForm = isCompileTimeComputable(matmulOp.getA()) && !isCompileTimeComputable(matmulOp.getB()); - Value lhs = collapseBatchDims(matmulOp.getA(), lhsBatch, m, k, rewriter, loc); - Value rhs = collapseBatchDims(matmulOp.getB(), rhsBatch, k, n, rewriter, loc); - int64_t lhsBatchForGemm = lhsBatch; - int64_t rhsBatchForGemm = rhsBatch; - int64_t gemmM = m; - int64_t gemmK = k; - int64_t gemmN = n; + Value lhs = collapseBatchDims(matmulOp.getA(), shapeInfo->lhsBatch, shapeInfo->m, shapeInfo->k, rewriter, loc); + Value rhs = collapseBatchDims(matmulOp.getB(), shapeInfo->rhsBatch, shapeInfo->k, shapeInfo->n, rewriter, loc); + int64_t lhsBatchForGemm = shapeInfo->lhsBatch; + int64_t rhsBatchForGemm = shapeInfo->rhsBatch; + int64_t gemmM = shapeInfo->m; + int64_t gemmK = shapeInfo->k; + int64_t gemmN = shapeInfo->n; if (useTransposedForm) { lhs = transposeLastTwoDimsInCompute(matmulOp.getB(), rewriter, loc); - lhsBatchForGemm = rhsBatch; + lhsBatchForGemm = shapeInfo->rhsBatch; rhs = transposeLastTwoDims(matmulOp.getA(), rewriter, loc); - rhsBatchForGemm = lhsBatch; - gemmM = n; - gemmN = m; + rhsBatchForGemm = shapeInfo->lhsBatch; + gemmM = shapeInfo->n; + gemmN = shapeInfo->m; } - auto gemmType = RankedTensorType::get({gemmM, gemmN}, outType.getElementType()); - auto batchedOutType = RankedTensorType::get({1, m, n}, outType.getElementType()); + auto gemmType = RankedTensorType::get({gemmM, gemmN}, shapeInfo->outType.getElementType()); Value none = ONNXNoneOp::create(rewriter, loc, rewriter.getNoneType()); - - if (outType.getRank() == 2) { - Value lhsMatrix = extractBatchMatrix(lhs, /*batchIndex=*/0, lhsBatchForGemm, gemmM, gemmK, rewriter, loc); - Value rhsMatrix = extractBatchMatrix(rhs, /*batchIndex=*/0, rhsBatchForGemm, gemmK, gemmN, rewriter, loc); - Value gemmResult = ONNXGemmOp::create(rewriter, - loc, - gemmType, - lhsMatrix, - rhsMatrix, - none, - rewriter.getF32FloatAttr(1.0f), - rewriter.getF32FloatAttr(1.0f), - rewriter.getBoolAttr(false), - rewriter.getBoolAttr(false)) - .getY(); - if (useTransposedForm) { - auto transposeCompute = - createSpatCompute<1>(rewriter, loc, TypeRange {outType}, {}, gemmResult, [&](Value input) { - Value transposed = ONNXTransposeOp::create(rewriter, loc, outType, input, rewriter.getI64ArrayAttr({1, 0})); - spatial::SpatYieldOp::create(rewriter, loc, transposed); - }); - gemmResult = transposeCompute.getResult(0); - } - rewriter.replaceOp(matmulOp, gemmResult); - return success(); - } - - SmallVector batchResults; - batchResults.reserve(batch); - for (int64_t batchIdx = 0; batchIdx < batch; batchIdx++) { - Value lhsMatrix = extractBatchMatrix(lhs, batchIdx, lhsBatchForGemm, gemmM, gemmK, rewriter, loc); - Value rhsMatrix = extractBatchMatrix(rhs, batchIdx, rhsBatchForGemm, gemmK, gemmN, rewriter, loc); - Value gemmResult = ONNXGemmOp::create(rewriter, - loc, - gemmType, - lhsMatrix, - rhsMatrix, - none, - rewriter.getF32FloatAttr(1.0f), - rewriter.getF32FloatAttr(1.0f), - rewriter.getBoolAttr(false), - rewriter.getBoolAttr(false)) - .getY(); - auto batchResultCompute = - createSpatCompute<1>(rewriter, loc, TypeRange {batchedOutType}, {}, gemmResult, [&](Value input) { - Value resultMatrix = input; - if (useTransposedForm) { - resultMatrix = ONNXTransposeOp::create(rewriter, - loc, - RankedTensorType::get({m, n}, outType.getElementType()), - input, - rewriter.getI64ArrayAttr({1, 0})); - } - Value expanded = tensor::ExpandShapeOp::create(rewriter, - loc, - batchedOutType, - resultMatrix, - SmallVector { - {0, 1}, - {2} - }); - spatial::SpatYieldOp::create(rewriter, loc, expanded); + Value lhsMatrix = extractBatchMatrix(lhs, /*batchIndex=*/0, lhsBatchForGemm, gemmM, gemmK, rewriter, loc); + Value rhsMatrix = extractBatchMatrix(rhs, /*batchIndex=*/0, rhsBatchForGemm, gemmK, gemmN, rewriter, loc); + Value gemmResult = ONNXGemmOp::create(rewriter, + loc, + gemmType, + lhsMatrix, + rhsMatrix, + none, + rewriter.getF32FloatAttr(1.0f), + rewriter.getF32FloatAttr(1.0f), + rewriter.getBoolAttr(false), + rewriter.getBoolAttr(false)) + .getY(); + if (useTransposedForm) { + auto transposeCompute = + createSpatCompute<1>(rewriter, loc, TypeRange {shapeInfo->outType}, {}, gemmResult, [&](Value input) { + Value transposed = + ONNXTransposeOp::create(rewriter, loc, shapeInfo->outType, input, rewriter.getI64ArrayAttr({1, 0})); + spatial::SpatYieldOp::create(rewriter, loc, transposed); }); - batchResults.push_back(batchResultCompute.getResult(0)); + gemmResult = transposeCompute.getResult(0); + } + rewriter.replaceOp(matmulOp, gemmResult); + return success(); + } +}; + +struct MatMulBatchedToSpatialComputes : OpRewritePattern { + using OpRewritePattern::OpRewritePattern; + + LogicalResult matchAndRewrite(ONNXMatMulOp matmulOp, PatternRewriter& rewriter) const override { + auto shapeInfo = analyzeMatMulShape(matmulOp); + if (failed(shapeInfo)) + return failure(); + if (shapeInfo->outType.getRank() == 2) + return failure(); + + Location loc = matmulOp.getLoc(); + bool useTransposedForm = isCompileTimeComputable(matmulOp.getA()) && !isCompileTimeComputable(matmulOp.getB()); + + Value lhs = collapseBatchDims(matmulOp.getA(), shapeInfo->lhsBatch, shapeInfo->m, shapeInfo->k, rewriter, loc); + Value rhs = collapseBatchDims(matmulOp.getB(), shapeInfo->rhsBatch, shapeInfo->k, shapeInfo->n, rewriter, loc); + int64_t lhsBatchForGemm = shapeInfo->lhsBatch; + int64_t rhsBatchForGemm = shapeInfo->rhsBatch; + int64_t gemmM = shapeInfo->m; + int64_t gemmK = shapeInfo->k; + int64_t gemmN = shapeInfo->n; + if (useTransposedForm) { + lhs = transposeLastTwoDimsInCompute(matmulOp.getB(), rewriter, loc); + lhsBatchForGemm = shapeInfo->rhsBatch; + rhs = transposeLastTwoDims(matmulOp.getA(), rewriter, loc); + rhsBatchForGemm = shapeInfo->lhsBatch; + gemmM = shapeInfo->n; + gemmN = shapeInfo->m; } - Value result = concatValues(batchResults, /*axis=*/0, rewriter, loc); - result = expandBatchDims(result, outType, batchShape->size(), rewriter, loc); + lhs = ensureBatchedTensor(lhs, lhsBatchForGemm, gemmM, gemmK, rewriter, loc); + rhs = ensureBatchedTensor(rhs, rhsBatchForGemm, gemmK, gemmN, rewriter, loc); + auto lhsBatchedType = cast(lhs.getType()); + auto rhsBatchedType = cast(rhs.getType()); + auto directOutType = RankedTensorType::get({shapeInfo->batch, gemmM, gemmN}, shapeInfo->outType.getElementType()); + + if (isCompileTimeComputable(rhs)) { + const int64_t numKSlices = ceilIntegerDivide(gemmK, crossbarSize.getValue()); + const int64_t numOutHSlices = ceilIntegerDivide(gemmN, crossbarSize.getValue()); + const int64_t paddedReductionSize = numKSlices * static_cast(crossbarSize.getValue()); + const int64_t paddedOutCols = numOutHSlices * static_cast(crossbarSize.getValue()); + auto paddedLhsType = RankedTensorType::get( + {lhsBatchForGemm, gemmM, paddedReductionSize}, lhsBatchedType.getElementType(), lhsBatchedType.getEncoding()); + auto paddedRhsType = RankedTensorType::get({shapeInfo->batch, paddedReductionSize, paddedOutCols}, + rhsBatchedType.getElementType(), + rhsBatchedType.getEncoding()); + auto paddedOutType = + RankedTensorType::get({shapeInfo->batch, gemmM, paddedOutCols}, shapeInfo->outType.getElementType()); + + auto paddedRhs = materializePaddedBatchedWeight(rhs, rhsBatchForGemm, shapeInfo->batch, paddedRhsType, rewriter); + if (succeeded(paddedRhs)) { + Value paddedLhs = createPaddedBatchedInputCompute(lhs, paddedLhsType, rewriter, loc); + const int64_t laneCount = shapeInfo->batch * gemmM * numKSlices * numOutHSlices; + auto partialPiecesType = RankedTensorType::get({laneCount, static_cast(crossbarSize.getValue())}, + shapeInfo->outType.getElementType()); + auto batchOp = createBatchedVmmBatch(paddedLhs, + *paddedRhs, + paddedLhsType, + lhsBatchForGemm, + paddedRhsType, + rhsBatchForGemm, + partialPiecesType, + gemmM, + numKSlices, + numOutHSlices, + rewriter, + loc); + Value result = createBatchedReductionCompute(batchOp.getResult(0), + partialPiecesType, + directOutType, + paddedOutType, + shapeInfo->batch, + numKSlices, + rewriter, + loc); + if (useTransposedForm) + result = transposeBatchedOutput( + result, + RankedTensorType::get({shapeInfo->batch, shapeInfo->m, shapeInfo->n}, shapeInfo->outType.getElementType()), + rewriter, + loc); + result = expandBatchDims(result, shapeInfo->outType, shapeInfo->batchShape.size(), rewriter, loc); + rewriter.replaceOp(matmulOp, result); + return success(); + } + } + const int64_t laneCount = shapeInfo->batch * gemmM * gemmN; + auto scalarPiecesType = RankedTensorType::get({laneCount, 1}, shapeInfo->outType.getElementType()); + auto batchOp = createBatchedVvdmulBatch(lhs, + lhsBatchForGemm, + rhs, + rhsBatchForGemm, + lhsBatchedType, + rhsBatchedType, + scalarPiecesType, + directOutType, + false, + rewriter, + loc); + Value result = + createBatchedDynamicOutputCompute(batchOp.getResult(0), scalarPiecesType, directOutType, rewriter, loc); + if (useTransposedForm) + result = transposeBatchedOutput( + result, + RankedTensorType::get({shapeInfo->batch, shapeInfo->m, shapeInfo->n}, shapeInfo->outType.getElementType()), + rewriter, + loc); + result = expandBatchDims(result, shapeInfo->outType, shapeInfo->batchShape.size(), rewriter, loc); rewriter.replaceOp(matmulOp, result); return success(); } @@ -296,7 +888,7 @@ struct MatMulToGemm : OpRewritePattern { } // namespace void populateMatMulRewritePatterns(RewritePatternSet& patterns, MLIRContext* ctx) { - patterns.insert(ctx); + patterns.insert(ctx); } } // namespace onnx_mlir diff --git a/validation/operations/README.md b/validation/operations/README.md index 237d1c8..e724410 100644 --- a/validation/operations/README.md +++ b/validation/operations/README.md @@ -48,12 +48,16 @@ python3 validation/operations/gen_tests.py ## MatMul -| Test | Directory | A input | B tensor | Output | Notes | -|------------|---------------------|---------|----------|---------|------------------------------------| -| Basic | `matmul/basic` | [2,3] | [3,4] | [2,4] | Direct 2D MatMul rewrite path | -| Left constant | `matmul/left_constant` | [2,3] | [3,4] | [2,4] | Constant LHS transpose rewrite path | -| Dynamic | `matmul/dynamic` | [2,3] | [3,4] | [2,4] | Runtime matrix operands | -| Batched 3D | `matmul/batched_3d` | [2,2,3] | [2,3,4] | [2,2,4] | Matching-batch MatMul rewrite path | +| Test | Directory | A input | B tensor | Output | Notes | +|---------------------|----------------------------------|----------|----------|---------|-------------------------------------------------| +| Basic | `matmul/basic` | [2,3] | [3,4] | [2,4] | Direct 2D MatMul rewrite path | +| Left constant | `matmul/left_constant` | [2,3] | [3,4] | [2,4] | Constant LHS transpose rewrite path | +| Dynamic | `matmul/dynamic` | [2,3] | [3,4] | [2,4] | Runtime matrix operands | +| Batched 3D | `matmul/batched_3d` | [2,2,3] | [2,3,4] | [2,2,4] | Matching-batch direct batched lowering | +| Batched 3D dynamic | `matmul/batched_3d_dynamic` | [2,2,3] | [2,3,4] | [2,2,4] | Batched runtime operands | +| Batched left const | `matmul/batched_left_constant` | [2,2,3] | [2,3,4] | [2,2,4] | Batched constant-LHS transpose path | +| Batched RHS broadcast | `matmul/batched_rhs_broadcast` | [2,2,3] | [3,4] | [2,2,4] | Rank-2 RHS broadcast across batch | +| Batched LHS broadcast | `matmul/batched_lhs_broadcast` | [2,3] | [2,3,4] | [2,2,4] | Rank-2 LHS broadcast across batched RHS | ## Gemv diff --git a/validation/operations/conv/dynamic/conv_dynamic.onnx b/validation/operations/conv/dynamic/conv_dynamic.onnx index 2f634952cb0d3cea9831fa3f75f3e5c6c418138d..c21dcf329f8cb2c8d71a332cd79d88167bff28de 100644 GIT binary patch delta 10 RcmdnXxR;TMgLfj+ZU7KI0^$Gw delta 10 RcmdnXxR;TMgJUAoZU7J}0^I-r diff --git a/validation/operations/gemm/dynamic/gemm_dynamic.onnx b/validation/operations/gemm/dynamic/gemm_dynamic.onnx index 917113d8e898ceb29eff98b33c1785da2da47320..f23e103ab5c3370be7f507dbd7f04b1c1447f7ce 100644 GIT binary patch delta 8 Pcmd1EVB+AN$dm>E2&Dnx delta 8 Pcmd1EVB+AI$dm>E2$2EY diff --git a/validation/operations/gemm/dynamic_alpha/gemm_dynamic_alpha.onnx b/validation/operations/gemm/dynamic_alpha/gemm_dynamic_alpha.onnx index 4decf30990bfa77b84a072cdf612ba1859a92133..2fdccb3888593516dfff4d41c62d8a16b2e69068 100644 GIT binary patch delta 8 Pcmb=gXX4U0$~6E delta 10 RcmbQlIEj&ogJUAo1ON>A0$cz9 diff --git a/validation/operations/gemm/dynamic_bias/gemm_dynamic_bias.onnx b/validation/operations/gemm/dynamic_bias/gemm_dynamic_bias.onnx index 69d3a3a3c493bf928cf63caca4217e3e87f8c00e..5ffd977fa1df4e840c2ff4c69c856467f11ae40a 100644 GIT binary patch delta 10 RcmZoyDgLfj+8UPO=0<{1D delta 10 RcmZ3-xQ>yDgJUAo8UPOs0dMld%Fv{X_h@8hOS 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 LUXw+WET{bqqccuw literal 69829 zcmeIwF-ikL6b9f-++s2)E`wD{OTi1+NMa#a3n4ZVajVssK>j8tOtO)T*(%@LB!O)SYR&52J+EJ@BtO^G*7iBGA_OU%to vjuHo|782qT;9wNu;bP)of?{TngcF(sGf5zmAixU%kVq3i literal 0 HcmV?d00001 diff --git a/validation/operations/matmul/batched_left_constant/matmul_batched_left_constant.onnx b/validation/operations/matmul/batched_left_constant/matmul_batched_left_constant.onnx new file mode 100644 index 0000000000000000000000000000000000000000..91921f76a6d4a68fa9b0d261398252ad77a94162 GIT binary patch literal 173 zcmd=9^gJTbd&zlbcwQTbdJ}lvt9Sk(v^plbTi%pPZjp zT#}eqqGij$1cb~2j82S>UIt!{CHrD@pV~2qY}Qi`9F#?H1j?y|He; zR=1$N);x~=8KM>YKRC?a7bOm|Oo&T>gHecw3uqD$GXpV85>O%%O#&pz;>5zmAixU% D@f9mx literal 0 HcmV?d00001 diff --git a/validation/operations/matmul/batched_lhs_broadcast/matmul_batched_lhs_broadcast.onnx b/validation/operations/matmul/batched_lhs_broadcast/matmul_batched_lhs_broadcast.onnx new file mode 100644 index 0000000000000000000000000000000000000000..b1d78103982ef215a6c573007e24bc30558cde8c GIT binary patch literal 217 zcmd=9^gJTbd&zlbcwQTbdJ}lvt9Sk(v^plTjR>RFt2X zlAKsvqLs_R#KFwLBEaax=;W2~;fIdB$kdwsbL%?Y); z?q}I}bsvjuhP~z50Q-Xpv-kbGz1!|g&B48wTzYN1YZ>jbQ@HID1SM_NS%vK<2WjpX zoSkc5wbo_-=2>j^Mq6IlMhOGmC&b6a!@(%T!NtVE1jNiq;#`c8LPA^uKtUd;AV`A6 KiG_=9^gJTbd&zlbcwQTbdJ}lvt9Sk(v@;lu;a?RFt2X zlAKsvqGip&%)uhS=)~ycWuVabXP?5%L|cvptoG;rb=qw!6W{+}LdO2$qqep^4