//===- TosaToLinalgNamed.cpp - Lowering Tosa to Linalg Named Ops ----------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // These rewriters lower from the Tosa to the Linalg named ops. // //===----------------------------------------------------------------------===// #include "mlir/Conversion/TosaToLinalg/TosaToLinalg.h" #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Linalg/IR/Linalg.h" #include "mlir/Dialect/Math/IR/Math.h" #include "mlir/Dialect/Tensor/IR/Tensor.h" #include "mlir/Dialect/Tosa/IR/TosaOps.h" #include "mlir/Dialect/Tosa/Utils/ConversionUtils.h" #include "mlir/Dialect/Utils/ReshapeOpsUtils.h" #include "mlir/IR/PatternMatch.h" #include "mlir/Transforms/DialectConversion.h" #include using namespace mlir; using namespace mlir::tosa; static mlir::Value applyPad(Location loc, Value input, ArrayRef pad, TypedAttr padAttr, OpBuilder &rewriter) { // Input should be padded only if necessary. if (llvm::all_of(pad, [](int64_t p) { return p == 0; })) return input; ShapedType inputTy = cast(input.getType()); Type inputETy = inputTy.getElementType(); auto inputShape = inputTy.getShape(); assert((inputShape.size() * 2) == pad.size()); SmallVector paddedShape; SmallVector lowIndices; SmallVector highIndices; for (size_t i : llvm::seq(inputShape.size())) { auto lowPad = pad[i * 2]; auto highPad = pad[i * 2 + 1]; if (ShapedType::isDynamic(inputShape[i])) paddedShape.push_back(inputShape[i]); else paddedShape.push_back(inputShape[i] + highPad + lowPad); lowIndices.push_back(rewriter.getIndexAttr(lowPad)); highIndices.push_back(rewriter.getIndexAttr(highPad)); } Value padValue = arith::ConstantOp::create(rewriter, loc, padAttr); return tensor::PadOp::create(rewriter, loc, RankedTensorType::get(paddedShape, inputETy), input, lowIndices, highIndices, padValue); } static mlir::Value linalgIntBroadcastExtSIAdd(PatternRewriter &rewriter, Location loc, Value bias, Value conv, Value result, ArrayRef indexingMaps) { ShapedType resultTy = cast(conv.getType()); return linalg::GenericOp::create( rewriter, loc, resultTy, ValueRange({bias, conv}), result, indexingMaps, getNParallelLoopsAttrs(resultTy.getRank()), [](OpBuilder &builder, Location loc, ValueRange args) { Value biasVal = args[0]; Type resType = args[1].getType(); if (resType != biasVal.getType()) { biasVal = arith::ExtSIOp::create(builder, loc, resType, biasVal); } Value added = arith::AddIOp::create(builder, loc, biasVal, args[1]); linalg::YieldOp::create(builder, loc, added); }) .getResult(0); } // Construct the affine map that a linalg generic would use to broadcast the // source tensor into the shape of the result tensor. static AffineMap getBroadcastingMap(PatternRewriter &rewriter, Value source, Value result) { ShapedType resultTy = cast(result.getType()); ShapedType sourceTy = cast(source.getType()); const int64_t resultRank = resultTy.getRank(); const int64_t sourceRank = sourceTy.getRank(); // The source tensor is broadcast to all the outer dimensions of the // result tensor. SmallVector sourceDims; // In the case of a rank one source tensor with a single element TOSA // specifies that the value be broadcast meaning we need an edge case for a // constant map. assert(sourceTy.hasStaticShape() && "Dynamic broadcasting shapes not supported!"); if (sourceRank == 1 && sourceTy.getDimSize(0) == 1) { sourceDims.push_back(rewriter.getAffineConstantExpr(0)); } else { for (auto dim : llvm::seq(0, sourceRank)) { auto expr = rewriter.getAffineDimExpr(dim + resultRank - sourceRank); sourceDims.push_back(expr); } } return AffineMap::get(/*dimCount=*/resultRank, /*symbolCount=*/0, sourceDims, rewriter.getContext()); } // Broadcast the source value to all the outer dimensions of the result value. // If required, the element type is expanded using an arith.extsi or arith.extf // operation as appropriate. static mlir::Value linalgBroadcastAndMaybeExt(PatternRewriter &rewriter, Location loc, Value source, Value result) { ShapedType resultTy = cast(result.getType()); const int64_t resultRank = resultTy.getRank(); // Creating maps for the input and output of the broacast-like generic op. SmallVector indexingMaps; indexingMaps.push_back(getBroadcastingMap(rewriter, source, result)); indexingMaps.push_back(rewriter.getMultiDimIdentityMap(resultRank)); // Build the broadcast-like operation as a linalg.generic. return linalg::GenericOp::create( rewriter, loc, resultTy, ValueRange({source}), result, indexingMaps, getNParallelLoopsAttrs(resultTy.getRank()), [&resultTy](OpBuilder &builder, Location loc, ValueRange args) { Value biasVal = args[0]; Type resType = args[1].getType(); if (resType != biasVal.getType()) { biasVal = resultTy.getElementType().isFloat() ? arith::ExtFOp::create(builder, loc, resType, biasVal) .getResult() : arith::ExtSIOp::create(builder, loc, resType, biasVal) .getResult(); } linalg::YieldOp::create(builder, loc, biasVal); }) .getResult(0); } static mlir::Value reifyConstantDim(int64_t attr, ImplicitLocOpBuilder &builder) { return arith::ConstantIndexOp::create(builder, attr); } // Calculating the output width/height using the formula: // H = ((IH+pad_top+pad_bottom-(dilation_y*(KH-1)+1))/stride_y)+1 // W = ((IW+pad_left+pad_right-(dilation_x*(KW-1)+1))/stride_x)+1 static mlir::Value getConvOrPoolOutputDim(Location loc, Value inputDim, int64_t padBeforeAttr, int64_t padAfterAttr, Value kernelDim, int64_t strideAttr, int64_t dilationAttr, OpBuilder &rewriter) { ImplicitLocOpBuilder builder(loc, rewriter); auto one = arith::ConstantOp::create(rewriter, loc, IntegerAttr::get(inputDim.getType(), 1)); Value padBefore = reifyConstantDim(padBeforeAttr, builder); Value paddedBefore = arith::AddIOp::create(builder, inputDim, padBefore); Value padAfter = reifyConstantDim(padAfterAttr, builder); Value paddedAfter = arith::AddIOp::create(builder, paddedBefore, padAfter); Value subOne = arith::SubIOp::create(builder, kernelDim, one); Value dilation = reifyConstantDim(dilationAttr, builder); Value dilated = arith::MulIOp::create(builder, dilation, subOne); Value addOne = arith::AddIOp::create(builder, dilated, one); Value subtract = arith::SubIOp::create(builder, paddedAfter, addOne); Value stride = reifyConstantDim(strideAttr, builder); Value divide = arith::DivUIOp::create(builder, subtract, stride); return arith::AddIOp::create(builder, divide, one); } // Creates a vector of the dynamic output dims for Conv2D and Depthwise_Conv2D static SmallVector inferDynamicDimsForConv( Location loc, Value input, Value weight, ShapedType resultTy, ArrayRef padAttr, ArrayRef strideAttr, ArrayRef dilationAttr, ArrayRef inputSizeDims, ArrayRef kernelSizeDims, OpBuilder &rewriter) { ShapedType inputTy = cast(input.getType()); int64_t inputRank = inputTy.getRank(); SmallVector dynDims; dynDims.resize(resultTy.getRank()); for (uint32_t i = 0, s = inputSizeDims.size(); i < s; ++i) { int64_t inputDim = inputSizeDims[i]; int64_t kernelDim = kernelSizeDims[i]; if (resultTy.isDynamicDim(inputDim)) { auto padTop = padAttr[i * 2]; auto padBottom = padAttr[i * 2 + 1]; auto stride = strideAttr[i]; auto dilation = dilationAttr[i]; Value initDynDim = tensor::DimOp::create(rewriter, loc, input, inputDim); Value kernelDynDim = tensor::DimOp::create(rewriter, loc, weight, kernelDim); // H = F(IH, pad_top, pad_bottom, dilation_y, KH, stride_y) dynDims[inputDim] = getConvOrPoolOutputDim(loc, initDynDim, padTop, padBottom, kernelDynDim, stride, dilation, rewriter); } } // Get the batch/channels dimensions. for (int i = 0; i < inputRank; i++) { if (resultTy.isDynamicDim(i) && !dynDims[i]) dynDims[i] = tensor::DimOp::create(rewriter, loc, input, i); } SmallVector filteredDims = condenseValues(dynDims); return filteredDims; } // Creates a map to collapse the last dimension of the Depthwise convolution op // due to a shape mismatch static void createDepthwiseConvCollapseMap( int64_t outputRank, SmallVector &reassociationMap, OpBuilder &rewriter) { reassociationMap.resize(outputRank); for (int i = 0; i < outputRank; i++) { reassociationMap[i].push_back(rewriter.getAffineDimExpr(i)); } reassociationMap[outputRank - 1].push_back( rewriter.getAffineDimExpr(outputRank)); } namespace { template class ConvConverter : public OpConversionPattern { public: using OpConversionPattern::OpConversionPattern; LogicalResult matchAndRewrite(TosaConvOp op, typename TosaConvOp::Adaptor adaptor, ConversionPatternRewriter &rewriter) const final { Location loc = op->getLoc(); Value input = op->getOperand(0); Value weight = op->getOperand(1); Value bias = op->getOperand(2); ShapedType inputTy = cast(input.getType()); ShapedType weightTy = cast(weight.getType()); ShapedType biasTy = cast(bias.getType()); ShapedType resultTy = cast(op->getResult(0).getType()); Type inputETy = inputTy.getElementType(); DenseI64ArrayAttr padAttr = op.getPadAttr(); DenseI64ArrayAttr strideTosaAttr = op.getStrideAttr(); DenseI64ArrayAttr dilationTosaAttr = op.getDilationAttr(); Type accETy = op.getAccType(); Type accTy = RankedTensorType::get(resultTy.getShape(), accETy); // Get and verify zero points. FailureOr maybeIZp = op.getInputZeroPoint(); if (failed(maybeIZp)) return rewriter.notifyMatchFailure( op, "input zero point cannot be statically determined"); FailureOr maybeWZp = op.getWeightZeroPoint(); if (failed(maybeWZp)) return rewriter.notifyMatchFailure( op, "weight zero point cannot be statically determined"); const int64_t inputZpVal = *maybeIZp; const int64_t weightZpVal = *maybeWZp; if (op.verifyInputZeroPoint(inputZpVal).failed()) return rewriter.notifyMatchFailure( op, "input zero point must be zero for non-int8 integer types"); if (op.verifyWeightZeroPoint(weightZpVal).failed()) return rewriter.notifyMatchFailure( op, "weight zero point must be zero for non-int8 integer types"); bool hasZp = (inputZpVal != 0) || (weightZpVal != 0); if (!weightTy.hasStaticShape() || !biasTy.hasStaticShape()) return rewriter.notifyMatchFailure( op, "tosa.conv ops require static shapes for weight and bias"); if (inputETy.isUnsignedInteger()) return rewriter.notifyMatchFailure( op, "tosa.conv ops does not support unsigned integer input"); llvm::SmallVector inputSizeDims; llvm::SmallVector kernelSizeDims; for (int i = 1; i < resultTy.getRank() - 1; i++) { inputSizeDims.push_back(i); kernelSizeDims.push_back(i); } SmallVector filteredDims = inferDynamicDimsForConv( loc, input, weight, resultTy, padAttr.asArrayRef(), strideTosaAttr.asArrayRef(), dilationTosaAttr.asArrayRef(), inputSizeDims, kernelSizeDims, rewriter); auto weightShape = weightTy.getShape(); // Apply padding as necessary. TypedAttr zeroAttr = rewriter.getZeroAttr(inputETy); if (hasZp) { int64_t intMin = APInt::getSignedMinValue(inputETy.getIntOrFloatBitWidth()) .getSExtValue(); int64_t intMax = APInt::getSignedMaxValue(inputETy.getIntOrFloatBitWidth()) .getSExtValue(); if (inputZpVal < intMin || inputZpVal > intMax) return rewriter.notifyMatchFailure( op, "tosa.conv op quantization has zp outside of input range"); zeroAttr = rewriter.getIntegerAttr(inputETy, inputZpVal); } llvm::SmallVector pad; pad.resize(2, 0); llvm::append_range(pad, padAttr.asArrayRef()); pad.resize(pad.size() + 2, 0); input = applyPad(loc, input, pad, zeroAttr, rewriter); if (4 == inputTy.getRank()) { // For 2D convolutions, we need to check if the target convolution op // wants a HWCF kernel layout. bool wantHwcf = hasZp ? std::is_same_v : std::is_same_v; if (wantHwcf) { // Transpose the kernel to match dimension ordering of the linalg // convolution operation. // TODO(suderman): See if this can be efficiently folded - check whether // the input is used anywhere else, if not fold the constant. SmallVector weightPerm; for (int i = 1; i < resultTy.getRank(); i++) weightPerm.push_back(i); weightPerm.push_back(0); SmallVector newWeightShape; for (auto dim : weightPerm) newWeightShape.push_back(weightShape[dim]); auto weightPermAttr = rewriter.getDenseI32ArrayAttr(weightPerm); Type newWeightTy = RankedTensorType::get(newWeightShape, weightTy.getElementType()); weight = tosa::TransposeOp::create(rewriter, loc, newWeightTy, weight, weightPermAttr); } } // For Conv3D transpose the kernel to match dimension ordering of the linalg // convolution operation. Conv2D has a 1-1 mapping in linalg so better to // map directly and then transpose later if desired. if (5 == inputTy.getRank()) { // TODO(suderman): See if this can be efficiently folded - check whether // the input is used anywhere else, if not fold the constant. SmallVector weightPerm; for (int i = 1; i < resultTy.getRank(); i++) weightPerm.push_back(i); weightPerm.push_back(0); SmallVector newWeightShape; for (auto dim : weightPerm) newWeightShape.push_back(weightShape[dim]); auto weightPermAttr = rewriter.getDenseI32ArrayAttr(weightPerm); Type newWeightTy = RankedTensorType::get(newWeightShape, weightTy.getElementType()); weight = tosa::TransposeOp::create(rewriter, loc, newWeightTy, weight, weightPermAttr); } // Extract the attributes for convolution. ArrayRef stride = strideTosaAttr; ArrayRef dilation = dilationTosaAttr; // Create the convolution op. auto strideAttr = rewriter.getI64TensorAttr(stride); auto dilationAttr = rewriter.getI64TensorAttr(dilation); Value biasEmptyTensor = tensor::EmptyOp::create( rewriter, loc, resultTy.getShape(), accETy, filteredDims); Value broadcastBias = linalgBroadcastAndMaybeExt(rewriter, loc, bias, biasEmptyTensor); if (hasZp) { auto iZp = rewriter.getI32IntegerAttr(inputZpVal); auto kZp = rewriter.getI32IntegerAttr(weightZpVal); auto iZpVal = arith::ConstantOp::create(rewriter, loc, iZp); auto kZpVal = arith::ConstantOp::create(rewriter, loc, kZp); Value conv = LinalgConvQOp::create( rewriter, loc, resultTy, ValueRange{input, weight, iZpVal, kZpVal}, ValueRange{broadcastBias}, strideAttr, dilationAttr) ->getResult(0); rewriter.replaceOp(op, conv); return success(); } Value conv = LinalgConvOp::create( rewriter, loc, accTy, ValueRange{input, weight}, ValueRange{broadcastBias}, strideAttr, dilationAttr) ->getResult(0); // We may need to truncate back to the result type if the accumulator was // wider than the result. if (resultTy != accTy) conv = tosa::CastOp::create(rewriter, loc, resultTy, conv); rewriter.replaceOp(op, conv); return success(); } }; class DepthwiseConvConverter : public OpConversionPattern { public: using OpConversionPattern::OpConversionPattern; LogicalResult matchAndRewrite(tosa::DepthwiseConv2DOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const final { Location loc = op->getLoc(); Value input = op->getOperand(0); Value weight = op->getOperand(1); Value bias = op->getOperand(2); ShapedType inputTy = cast(input.getType()); ShapedType weightTy = cast(weight.getType()); ShapedType biasTy = cast(bias.getType()); ShapedType resultTy = cast(op->getResult(0).getType()); int64_t resultRank = resultTy.getRank(); Type inputETy = inputTy.getElementType(); Type resultETy = resultTy.getElementType(); auto padAttr = cast(op->getAttr("pad")); auto strideTosaAttr = cast(op->getAttr("stride")); auto dilationTosaAttr = cast(op->getAttr("dilation")); Type accETy = op.getAccType(); if (!weightTy.hasStaticShape() || !biasTy.hasStaticShape()) return rewriter.notifyMatchFailure( op, "tosa.depthwise_conv ops require static shapes"); // Compute output dynamic dims SmallVector filteredDims = inferDynamicDimsForConv( loc, input, weight, resultTy, padAttr.asArrayRef(), strideTosaAttr.asArrayRef(), dilationTosaAttr.asArrayRef(), /*inputSizeDims=*/{1, 2}, /*kernelSizeDims=*/{0, 1}, rewriter); // Get and verify zero points. FailureOr maybeIZp = op.getInputZeroPoint(); FailureOr maybeWZp = op.getWeightZeroPoint(); if (failed(maybeIZp)) return rewriter.notifyMatchFailure( op, "input zero point cannot be statically determined"); if (failed(maybeWZp)) return rewriter.notifyMatchFailure( op, "weight zero point cannot be statically determined"); const int64_t inputZpVal = *maybeIZp; const int64_t weightZpVal = *maybeWZp; if (op.verifyInputZeroPoint(inputZpVal).failed()) return rewriter.notifyMatchFailure( op, "input zero point must be zero for non-int8 integer types"); if (op.verifyWeightZeroPoint(weightZpVal).failed()) return rewriter.notifyMatchFailure( op, "weight zero point must be zero for non-int8 integer types"); bool hasNullZps = (inputZpVal == 0) && (weightZpVal == 0); auto weightShape = weightTy.getShape(); auto resultShape = resultTy.getShape(); // Apply padding as necessary. TypedAttr zeroAttr = rewriter.getZeroAttr(inputETy); if (!hasNullZps) { int64_t intMin = APInt::getSignedMinValue(inputETy.getIntOrFloatBitWidth()) .getSExtValue(); int64_t intMax = APInt::getSignedMaxValue(inputETy.getIntOrFloatBitWidth()) .getSExtValue(); if (inputZpVal < intMin || inputZpVal > intMax) return rewriter.notifyMatchFailure( op, "tosa.depthwise_conv op quantization has zp outside of input " "range"); zeroAttr = rewriter.getIntegerAttr(inputETy, inputZpVal); } llvm::SmallVector pad; pad.resize(2, 0); llvm::append_range(pad, padAttr.asArrayRef()); pad.resize(pad.size() + 2, 0); input = applyPad(loc, input, pad, zeroAttr, rewriter); // Extract the attributes for convolution. ArrayRef stride = strideTosaAttr; ArrayRef dilation = dilationTosaAttr; // Create the convolution op. auto strideAttr = rewriter.getI64TensorAttr(stride); auto dilationAttr = rewriter.getI64TensorAttr(dilation); ShapedType linalgConvTy = RankedTensorType::get({resultShape[0], resultShape[1], resultShape[2], weightShape[2], weightShape[3]}, accETy); auto resultZeroAttr = rewriter.getZeroAttr(accETy); Value emptyTensor = tensor::EmptyOp::create( rewriter, loc, linalgConvTy.getShape(), accETy, filteredDims); Value zero = arith::ConstantOp::create(rewriter, loc, resultZeroAttr); Value zeroTensor = linalg::FillOp::create(rewriter, loc, ValueRange{zero}, ValueRange{emptyTensor}) .result(); Value biasEmptyTensor = tensor::EmptyOp::create( rewriter, loc, resultTy.getShape(), resultETy, filteredDims); // Broadcast the initial value to the output tensor before convolving. SmallVector indexingMaps; indexingMaps.push_back(getBroadcastingMap(rewriter, bias, biasEmptyTensor)); indexingMaps.push_back(rewriter.getMultiDimIdentityMap(resultRank)); indexingMaps.push_back(rewriter.getMultiDimIdentityMap(resultRank)); if (hasNullZps) { Value conv = linalg::DepthwiseConv2DNhwcHwcmOp::create( rewriter, loc, linalgConvTy, ValueRange{input, weight}, ValueRange{zeroTensor}, strideAttr, dilationAttr) .getResult(0); // We may need to truncate back to the result type if the accumulator was // wider than the result. if (accETy != resultETy) conv = tosa::CastOp::create( rewriter, loc, RankedTensorType::get(cast(conv.getType()).getShape(), resultETy), conv); SmallVector reassociationMap; createDepthwiseConvCollapseMap(resultRank, reassociationMap, rewriter); Value convReshape = tensor::CollapseShapeOp::create( rewriter, loc, resultTy, conv, reassociationMap); Value result = linalg::GenericOp::create( rewriter, loc, resultTy, ValueRange({bias, convReshape}), biasEmptyTensor, indexingMaps, getNParallelLoopsAttrs(resultRank), [&](OpBuilder &nestedBuilder, Location nestedLoc, ValueRange args) { Value added; if (llvm::isa(inputETy)) added = arith::AddFOp::create(nestedBuilder, loc, args[0], args[1]); else added = arith::AddIOp::create(nestedBuilder, loc, args[0], args[1]); linalg::YieldOp::create(nestedBuilder, nestedLoc, added); }) .getResult(0); rewriter.replaceOp(op, result); } else { IntegerAttr iZp = rewriter.getI32IntegerAttr(inputZpVal); IntegerAttr wZp = rewriter.getI32IntegerAttr(weightZpVal); auto iZpVal = arith::ConstantOp::create(rewriter, loc, iZp); auto kZpVal = arith::ConstantOp::create(rewriter, loc, wZp); Value conv = linalg::DepthwiseConv2DNhwcHwcmQOp::create( rewriter, loc, linalgConvTy, ValueRange{input, weight, iZpVal, kZpVal}, ValueRange{zeroTensor}, strideAttr, dilationAttr) .getResult(0); SmallVector reassociationMap; createDepthwiseConvCollapseMap(resultRank, reassociationMap, rewriter); Value convReshape = tensor::CollapseShapeOp::create( rewriter, loc, resultTy, conv, reassociationMap); Value result = linalgIntBroadcastExtSIAdd( rewriter, loc, bias, convReshape, biasEmptyTensor, indexingMaps); rewriter.replaceOp(op, result); } return success(); } }; class MatMulConverter : public OpConversionPattern { public: using OpConversionPattern::OpConversionPattern; LogicalResult matchAndRewrite(tosa::MatMulOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const final { Location loc = op.getLoc(); auto outputTy = cast(op.getType()); auto outputElementTy = outputTy.getElementType(); SmallVector dynDims; dynDims.resize(cast(op->getResult(0).getType()).getRank()); if (!outputTy.hasRank() || outputTy.isDynamicDim(0)) { dynDims[0] = tensor::DimOp::create(rewriter, loc, op->getOperand(0), 0); } if (!outputTy.hasRank() || outputTy.isDynamicDim(1)) { dynDims[1] = tensor::DimOp::create(rewriter, loc, op->getOperand(0), 1); } if (!outputTy.hasRank() || outputTy.isDynamicDim(2)) { dynDims[2] = tensor::DimOp::create(rewriter, loc, op->getOperand(1), 2); } SmallVector filteredDims = condenseValues(dynDims); auto zeroAttr = rewriter.getZeroAttr(outputElementTy); Value zero = arith::ConstantOp::create(rewriter, loc, zeroAttr); auto emptyTensor = tensor::EmptyOp::create(rewriter, loc, outputTy.getShape(), outputTy.getElementType(), filteredDims); Value zeroTensor = linalg::FillOp::create(rewriter, loc, ValueRange{zero}, ValueRange{emptyTensor}) .result(); FailureOr maybeAZp = op.getAZeroPoint(); FailureOr maybeBZp = op.getBZeroPoint(); if (failed(maybeAZp)) return rewriter.notifyMatchFailure( op, "input a zero point cannot be statically determined"); if (failed(maybeBZp)) return rewriter.notifyMatchFailure( op, "input b zero point cannot be statically determined"); const int64_t aZpVal = *maybeAZp; const int64_t bZpVal = *maybeBZp; if (op.verifyAZeroPoint(aZpVal).failed()) return rewriter.notifyMatchFailure( op, "input a zero point must be zero for non-int8 integer types"); if (op.verifyBZeroPoint(bZpVal).failed()) return rewriter.notifyMatchFailure( op, "input b zero point must be zero for non-int8 integer types"); if (aZpVal == 0 && bZpVal == 0) { rewriter.replaceOpWithNewOp( op, TypeRange{op.getType()}, ValueRange{adaptor.getA(), adaptor.getB()}, ValueRange{zeroTensor}); return success(); } auto aZp = arith::ConstantOp::create(rewriter, loc, rewriter.getI32IntegerAttr(aZpVal)); auto bZp = arith::ConstantOp::create(rewriter, loc, rewriter.getI32IntegerAttr(bZpVal)); rewriter.replaceOpWithNewOp( op, TypeRange{op.getType()}, ValueRange{adaptor.getA(), adaptor.getB(), aZp, bZp}, zeroTensor); return success(); } }; class MaxPool2dConverter : public OpConversionPattern { public: using OpConversionPattern::OpConversionPattern; // Compute the dynamic output sizes of the maxpool operation. static SmallVector computeDynamicOutputSizes(tosa::MaxPool2dOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) { TensorType resultTy = op.getType(); Location loc = op.getLoc(); Value input = adaptor.getInput(); ArrayRef kernel = op.getKernel(); ArrayRef pad = op.getPad(); ArrayRef stride = op.getStride(); SmallVector dynamicDims; // Batch dimension if (resultTy.isDynamicDim(0)) dynamicDims.push_back(tensor::DimOp::create(rewriter, loc, input, 0)); // Height/width dimensions for (int64_t dim : {1, 2}) { if (!resultTy.isDynamicDim(dim)) continue; // Index into the attribute arrays int64_t index = dim - 1; // Input height/width Value ihw = tensor::DimOp::create(rewriter, loc, input, dim); // Kernel height/width Value khw = arith::ConstantIndexOp::create(rewriter, loc, kernel[index]); // Output height/width Value ohw = getConvOrPoolOutputDim(loc, ihw, pad[index * 2], pad[index * 2 + 1], khw, stride[index], /*dilationAttr=*/1, rewriter); dynamicDims.push_back(ohw); } // Channel dimension if (resultTy.isDynamicDim(3)) dynamicDims.push_back(tensor::DimOp::create(rewriter, loc, input, 3)); return dynamicDims; } LogicalResult matchAndRewrite(tosa::MaxPool2dOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const final { Location loc = op.getLoc(); Value input = adaptor.getInput(); ShapedType inputTy = cast(input.getType()); bool isUnsigned = op.getType().getElementType().isUnsignedInteger(); ShapedType resultTy = getTypeConverter()->convertType(op.getType()); if (!resultTy) return rewriter.notifyMatchFailure(op, "failed to convert type"); Type resultETy = inputTy.getElementType(); SmallVector dynamicDims = computeDynamicOutputSizes(op, adaptor, rewriter); // Determine what the initial value needs to be for the max pool op. TypedAttr initialAttr; if (resultETy.isF32() || resultETy.isBF16() || resultETy.isF16()) initialAttr = rewriter.getFloatAttr( resultETy, APFloat::getLargest( cast(resultETy).getFloatSemantics(), true)); else if (isUnsigned) initialAttr = rewriter.getIntegerAttr( resultETy, APInt::getZero(resultETy.getIntOrFloatBitWidth())); else if (isa(resultETy)) initialAttr = rewriter.getIntegerAttr( resultETy, APInt::getSignedMinValue(resultETy.getIntOrFloatBitWidth())); if (!initialAttr) return rewriter.notifyMatchFailure( op, "Unsupported initial value for tosa.maxpool_2d op"); // Apply padding as necessary. llvm::SmallVector pad; pad.resize(2, 0); llvm::append_range(pad, op.getPad()); pad.resize(pad.size() + 2, 0); Value paddedInput = applyPad(loc, input, pad, initialAttr, rewriter); Value initialValue = arith::ConstantOp::create(rewriter, loc, initialAttr); ArrayRef kernel = op.getKernel(); ArrayRef stride = op.getStride(); Attribute strideAttr = rewriter.getI64VectorAttr(stride); Attribute dilationAttr = rewriter.getI64VectorAttr({1, 1}); // Create the linalg op that performs pooling. Value emptyTensor = tensor::EmptyOp::create(rewriter, loc, resultTy.getShape(), resultTy.getElementType(), dynamicDims); Value filledEmptyTensor = linalg::FillOp::create(rewriter, loc, initialValue, emptyTensor) .result(); Value fakeWindowDims = tensor::EmptyOp::create(rewriter, loc, kernel, resultETy); if (isUnsigned) { rewriter.replaceOpWithNewOp( op, ArrayRef{resultTy}, ValueRange{paddedInput, fakeWindowDims}, filledEmptyTensor, strideAttr, dilationAttr); return llvm::success(); } auto resultOp = linalg::PoolingNhwcMaxOp::create( rewriter, op->getLoc(), ArrayRef{resultTy}, ValueRange{paddedInput, fakeWindowDims}, filledEmptyTensor, strideAttr, dilationAttr); NanPropagationMode nanMode = op.getNanMode(); rewriter.replaceOp(op, resultOp); // NaN propagation has no meaning for non floating point types. if (!isa(getElementTypeOrSelf(inputTy))) return success(); // "PROPAGATE" mode matches the behaviour of the LinAlg named op, so no // compare and select materialization is required. // // In the case of "IGNORE" we need to insert a compare and select. Since // we've already produced a named op we will just take its body and modify // it to include the appropriate checks. If the current value is NaN the // old value of pool will be taken otherwise we use the result. if (nanMode == NanPropagationMode::IGNORE) { auto genericOp = linalg::GenericOp::create( rewriter, loc, resultOp.getType(0), resultOp.getInputs(), resultOp.getOutputs(), resultOp.getIndexingMapsArray(), resultOp.getIteratorTypesArray(), [&](OpBuilder &opBuilder, Location loc, ValueRange blockArgs) { IRMapping map; auto oldBlock = resultOp.getRegion().begin(); auto oldArgs = oldBlock->getArguments(); auto &oldMaxOp = *resultOp.getBlock()->begin(); map.map(oldArgs, blockArgs); auto *newOp = opBuilder.clone(oldMaxOp, map); Value isNaN = arith::CmpFOp::create(opBuilder, loc, arith::CmpFPredicate::UNO, blockArgs.front(), blockArgs.front()); auto selectOp = arith::SelectOp::create( opBuilder, loc, isNaN, blockArgs.back(), newOp->getResult(0)); linalg::YieldOp::create(opBuilder, loc, selectOp.getResult()); }); rewriter.replaceOp(resultOp, genericOp); } return success(); } }; class AvgPool2dConverter : public OpRewritePattern { public: using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(tosa::AvgPool2dOp op, PatternRewriter &rewriter) const final { Location loc = op.getLoc(); Value input = op.getInput(); ShapedType inputTy = cast(input.getType()); Type inElementTy = inputTy.getElementType(); ShapedType resultTy = cast(op.getType()); Type resultETy = cast(op.getType()).getElementType(); Type accETy = op.getAccType(); ShapedType accTy = resultTy.clone(accETy); auto dynamicDimsOr = checkHasDynamicBatchDims(rewriter, op, {input, op.getOutput()}); if (!dynamicDimsOr.has_value()) return failure(); SmallVector dynamicDims = *dynamicDimsOr; FailureOr maybeIZp = op.getInputZeroPoint(); FailureOr maybeOZp = op.getOutputZeroPoint(); if (failed(maybeIZp)) return rewriter.notifyMatchFailure( op, "input zero point could not be statically determined"); if (failed(maybeOZp)) return rewriter.notifyMatchFailure( op, "output zero point could not be statically determined"); const int64_t inputZpVal = *maybeIZp; const int64_t outputZpVal = *maybeOZp; // Apply padding as necessary. llvm::SmallVector pad; pad.resize(2, 0); llvm::append_range(pad, op.getPad()); pad.resize(pad.size() + 2, 0); TypedAttr padAttr = rewriter.getZeroAttr(inElementTy); // Unsupported element type if (!padAttr) return failure(); Value paddedInput = applyPad(loc, input, pad, padAttr, rewriter); auto initialAttr = rewriter.getZeroAttr(accETy); Value initialValue = arith::ConstantOp::create(rewriter, loc, initialAttr); ArrayRef kernel = op.getKernel(); ArrayRef stride = op.getStride(); Attribute strideAttr = rewriter.getI64VectorAttr(stride); Attribute dilationAttr = rewriter.getI64VectorAttr({1, 1}); // Create the linalg op that performs pooling. Value poolEmptyTensor = tensor::EmptyOp::create( rewriter, loc, accTy.getShape(), accETy, dynamicDims); Value filledEmptyTensor = linalg::FillOp::create(rewriter, loc, ValueRange{initialValue}, ValueRange{poolEmptyTensor}) .result(); Value fakeWindowDims = tensor::EmptyOp::create(rewriter, loc, kernel, accETy); // Sum across the pooled region. Value poolingOp = linalg::PoolingNhwcSumOp::create( rewriter, loc, ArrayRef{accTy}, ValueRange{paddedInput, fakeWindowDims}, filledEmptyTensor, strideAttr, dilationAttr) .getResult(0); // Normalize the summed value by the number of elements grouped in each // pool. Value iH = tensor::DimOp::create(rewriter, loc, poolingOp, 1); Value iW = tensor::DimOp::create(rewriter, loc, poolingOp, 2); auto one = arith::ConstantIndexOp::create(rewriter, loc, 1); iH = arith::SubIOp::create(rewriter, loc, iH, one); iW = arith::SubIOp::create(rewriter, loc, iW, one); Value genericEmptyTensor = tensor::EmptyOp::create( rewriter, loc, resultTy.getShape(), resultETy, dynamicDims); auto affineMap = rewriter.getMultiDimIdentityMap(resultTy.getRank()); auto genericOp = linalg::GenericOp::create( rewriter, loc, ArrayRef({resultTy}), ValueRange{poolingOp}, ValueRange{genericEmptyTensor}, ArrayRef({affineMap, affineMap}), getNParallelLoopsAttrs(resultTy.getRank()), [&](OpBuilder &b, Location loc, ValueRange args) { auto zero = arith::ConstantIndexOp::create(rewriter, loc, 0); // Determines what the portion of valid input is covered by the // kernel. auto padFn = [&](Value valid, Value pos, int64_t pad) -> Value { if (pad == 0) return valid; auto padVal = arith::ConstantIndexOp::create(rewriter, loc, pad); Value dpos = arith::SubIOp::create(rewriter, loc, pos, padVal); Value offset = arith::MinSIOp::create(rewriter, loc, dpos, zero); return arith::AddIOp::create(rewriter, loc, valid, offset) ->getResult(0); }; auto coverageFn = [&](int64_t i, Value isize) -> Value { Value strideVal = arith::ConstantIndexOp::create(rewriter, loc, stride[i - 1]); Value val = arith::ConstantIndexOp::create(rewriter, loc, kernel[i - 1]); // Find the position relative to the input tensor's ends. Value left = linalg::IndexOp::create(rewriter, loc, i); Value right = arith::SubIOp::create(rewriter, loc, isize, left); left = arith::MulIOp::create(rewriter, loc, left, strideVal); right = arith::MulIOp::create(rewriter, loc, right, strideVal); // Determine how much padding was included. val = padFn(val, left, pad[i * 2]); val = padFn(val, right, pad[i * 2 + 1]); return arith::MaxSIOp::create(rewriter, loc, one, val); }; // Compute the indices from either end. Value kH3 = coverageFn(1, iH); Value kW3 = coverageFn(2, iW); // Compute the total number of elements and normalize. auto count = arith::IndexCastOp::create( rewriter, loc, rewriter.getI32Type(), arith::MulIOp::create(rewriter, loc, kH3, kW3)); // Divide by the number of summed values. For floats this is just // a div however for quantized values input normalization had // to be applied. Value poolVal = args[0]; if (isa(accETy)) { auto countF = arith::SIToFPOp::create(rewriter, loc, accETy, count); poolVal = arith::DivFOp::create(rewriter, loc, poolVal, countF) ->getResult(0); if (accETy.getIntOrFloatBitWidth() > resultETy.getIntOrFloatBitWidth()) poolVal = arith::TruncFOp::create(rewriter, loc, resultETy, poolVal); } else { // If we have quantization information we need to apply an offset // for the input zp value. if (inputZpVal != 0) { auto inputZp = arith::ConstantOp::create( rewriter, loc, b.getIntegerAttr(accETy, inputZpVal)); Value offset = arith::MulIOp::create(rewriter, loc, accETy, count, inputZp); poolVal = arith::SubIOp::create(rewriter, loc, accETy, poolVal, offset); } // Compute: k = 32 - count_leading_zeros(value - 1) Value one32 = arith::ConstantOp::create( rewriter, loc, rewriter.getI32IntegerAttr(1)); Value thirtyTwo32 = arith::ConstantOp::create( rewriter, loc, rewriter.getI32IntegerAttr(32)); Value countSubOne = arith::SubIOp::create(rewriter, loc, count, one32); Value leadingZeros = math::CountLeadingZerosOp::create(rewriter, loc, countSubOne); Value k = arith::SubIOp::create(rewriter, loc, thirtyTwo32, leadingZeros); // Compute: numerator = ((1 << 30) + 1) << k Value k64 = arith::ExtUIOp::create(rewriter, loc, rewriter.getI64Type(), k); Value thirtyShiftPlusOne = arith::ConstantOp::create( rewriter, loc, rewriter.getI64IntegerAttr((1 << 30) + 1)); Value numerator = arith::ShLIOp::create(rewriter, loc, thirtyShiftPlusOne, k64); // Compute: scale.multiplier = numerator / value; Value count64 = arith::ExtUIOp::create( rewriter, loc, rewriter.getI64Type(), count); Value multiplier = arith::DivUIOp::create(rewriter, loc, numerator, count64); multiplier = arith::TruncIOp::create( rewriter, loc, rewriter.getI32Type(), multiplier); // Compute: scale.shift = 30 + k Value k8 = arith::TruncIOp::create(rewriter, loc, rewriter.getI8Type(), k); Value thirty8 = arith::ConstantOp::create( rewriter, loc, rewriter.getI8IntegerAttr(30)); Value shift = arith::AddIOp::create(rewriter, loc, k8, thirty8); auto roundingAttr = RoundingModeAttr::get( rewriter.getContext(), RoundingMode::SINGLE_ROUND); auto scaled = tosa::ApplyScaleOp::create( rewriter, loc, rewriter.getI32Type(), poolVal, multiplier, shift, roundingAttr) .getResult(); // If we have quantization information we need to apply output // zeropoint. if (outputZpVal != 0) { auto outputZp = arith::ConstantOp::create( rewriter, loc, b.getIntegerAttr(scaled.getType(), outputZpVal)); scaled = arith::AddIOp::create(rewriter, loc, scaled, outputZp) .getResult(); } // Apply Clip. int64_t outBitwidth = resultETy.getIntOrFloatBitWidth(); auto min = arith::ConstantIntOp::create( rewriter, loc, accETy, APInt::getSignedMinValue(outBitwidth).getSExtValue()); auto max = arith::ConstantIntOp::create( rewriter, loc, accETy, APInt::getSignedMaxValue(outBitwidth).getSExtValue()); auto clamp = clampIntHelper(loc, scaled, min, max, rewriter, /*isUnsigned=*/false); poolVal = clamp; // Convert type. if (resultETy != clamp.getType()) { poolVal = arith::TruncIOp::create(rewriter, loc, resultETy, poolVal); } } linalg::YieldOp::create(rewriter, loc, poolVal); }); rewriter.replaceOp(op, genericOp.getResult(0)); return success(); } }; class TransposeConverter : public OpRewritePattern { public: using OpRewritePattern::OpRewritePattern; LogicalResult matchAndRewrite(tosa::TransposeOp op, PatternRewriter &rewriter) const final { const llvm::ArrayRef constantPerms = op.getPerms(); Location loc = op.getLoc(); // The verifier should have made sure we have a valid TOSA permutation // tensor. isPermutationVector doesn't actually check the TOSA perms we // expect. SmallVector inputSizes = tensor::getMixedSizes(rewriter, loc, op.getInput1()); auto permutedSizes = applyTOSAPermutation(inputSizes, constantPerms); auto permutedInit = tensor::EmptyOp::create(rewriter, loc, permutedSizes, op.getInput1().getType().getElementType()); rewriter.replaceOpWithNewOp( op, op.getInput1(), permutedInit, llvm::to_vector(llvm::map_range( constantPerms, [](int32_t v) -> int64_t { return v; }))); return success(); } }; } // namespace void mlir::tosa::populateTosaToLinalgNamedConversionPatterns( const TypeConverter &converter, RewritePatternSet *patterns, const TosaToLinalgNamedOptions &options) { if (options.preferConv2DKernelLayoutHWCF) { patterns->add>( patterns->getContext()); } else { patterns->add>( patterns->getContext()); } patterns->add< // clang-format off ConvConverter, DepthwiseConvConverter, MatMulConverter, AvgPool2dConverter, TransposeConverter >(patterns->getContext()); patterns->add< MaxPool2dConverter >(converter, patterns->getContext()); // clang-format on }