[Mlir-commits] [mlir] [TOSA] Convert tosa.elementwise to linalg.elementwise (PR #224344)
Renato Golin
llvmlistbot at llvm.org
Thu Sep 24 05:14:09 PDT 2026
https://github.com/rengolin updated https://github.com/llvm/llvm-project/pull/224344
>From 12e4626be1d098f3dacf03afb0652a361cd7ccdd Mon Sep 17 00:00:00 2001
From: rengolin <rengolin at systemcall.eu>
Date: Wed, 16 Sep 2026 07:30:07 -0700
Subject: [PATCH 1/5] [MLIR][TOSA] Convert elementwise to linalg.elementwise
Add a new converter for elementwise TOSA operations to Linalg
elementwise operations. Most match directly, some have slightly
different semantics that need adjusting.
Fixes #221267
---
.../Conversion/TosaToLinalg/TosaToLinalg.h | 4 +
.../TosaToLinalg/TosaToLinalgNamed.cpp | 247 ++++++++++++++++++
.../TosaToLinalg/TosaToLinalgNamedPass.cpp | 11 +
.../TosaToLinalg/tosa-to-linalg-invalid.mlir | 13 +
.../TosaToLinalg/tosa-to-linalg-named.mlir | 40 +++
.../TosaToLinalg/tosa-to-linalg.mlir | 74 +++++-
6 files changed, 377 insertions(+), 12 deletions(-)
diff --git a/mlir/include/mlir/Conversion/TosaToLinalg/TosaToLinalg.h b/mlir/include/mlir/Conversion/TosaToLinalg/TosaToLinalg.h
index c1d28528a2f90d..825b4366c5b25b 100644
--- a/mlir/include/mlir/Conversion/TosaToLinalg/TosaToLinalg.h
+++ b/mlir/include/mlir/Conversion/TosaToLinalg/TosaToLinalg.h
@@ -54,6 +54,10 @@ void populateTosaToLinalgNamedConversionPatterns(
const TypeConverter &converter, RewritePatternSet *patterns,
const TosaToLinalgNamedOptions &options);
+/// Returns true if `op` is a TOSA elementwise operation that the
+/// TosaToLinalgNamed conversion can lower to a `linalg.elementwise` named op.
+bool isConvertibleToLinalgElementwise(Operation *op);
+
} // namespace tosa
} // namespace mlir
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
index 4a11a927090bf6..97353d2dc4d674 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
@@ -18,15 +18,161 @@
#include "mlir/Dialect/Tosa/IR/TosaOps.h"
#include "mlir/Dialect/Tosa/Utils/ConversionUtils.h"
#include "mlir/Dialect/Utils/ReshapeOpsUtils.h"
+#include "mlir/IR/Matchers.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Transforms/DialectConversion.h"
#include "llvm/ADT/SmallVectorExtras.h"
+#include "llvm/ADT/TypeSwitch.h"
+#include <optional>
#include <type_traits>
using namespace mlir;
using namespace mlir::tosa;
+// Number of leading data operands the op forwards to linalg.elementwise,
+// matching its unary/binary/ternary arity group. Trailing metadata operands
+// (e.g. MulOp's shift, NegateOp's zero points) are excluded.
+static unsigned getArity(Operation *op) {
+ return llvm::TypeSwitch<Operation *, unsigned>(op)
+ .Case<tosa::AbsOp, tosa::CeilOp, tosa::FloorOp, tosa::ExpOp, tosa::LogOp,
+ tosa::RsqrtOp, tosa::SinOp, tosa::CosOp, tosa::TanhOp, tosa::ErfOp,
+ tosa::ReciprocalOp, tosa::NegateOp>([](Operation *) { return 1u; })
+ .Case<tosa::AddOp, tosa::SubOp, tosa::IntDivOp, tosa::MulOp, tosa::PowOp,
+ tosa::MaximumOp, tosa::MinimumOp>([](Operation *) { return 2u; })
+ .Case<tosa::SelectOp>([](Operation *) { return 3u; })
+ .Default([](Operation *op) { return op->getNumOperands(); });
+}
+
+// Return the linalg.elementwise kind that is semantically equivalent to the
+// given TOSA elementwise op for the provided element type, or std::nullopt if
+// the op has no direct linalg.elementwise counterpart. Only ops whose TOSA
+// lowering maps to a single linalg.elementwise function are handled; the more
+// involved lowerings (e.g. quantized negate, mul with shift, casts) are left to
+// the generic TosaToLinalg conversion.
+static std::optional<linalg::ElementwiseKind>
+getElementwiseKind(Operation *op, Type elementTy) {
+ using linalg::ElementwiseKind;
+ const bool isFloat = isa<FloatType>(elementTy);
+ const bool isSignlessInt =
+ elementTy.isSignlessInteger() && !elementTy.isInteger(1);
+
+ // Unary ops. linalg.elementwise only defines these on floating point types.
+ if (isFloat) {
+ if (isa<tosa::AbsOp>(op))
+ return ElementwiseKind::abs;
+ if (isa<tosa::CeilOp>(op))
+ return ElementwiseKind::ceil;
+ if (isa<tosa::FloorOp>(op))
+ return ElementwiseKind::floor;
+ if (isa<tosa::ExpOp>(op))
+ return ElementwiseKind::exp;
+ if (isa<tosa::LogOp>(op))
+ return ElementwiseKind::log;
+ if (isa<tosa::RsqrtOp>(op))
+ return ElementwiseKind::rsqrt;
+ if (isa<tosa::SinOp>(op))
+ return ElementwiseKind::sin;
+ if (isa<tosa::CosOp>(op))
+ return ElementwiseKind::cos;
+ if (isa<tosa::TanhOp>(op))
+ return ElementwiseKind::tanh;
+ if (isa<tosa::ErfOp>(op))
+ return ElementwiseKind::erf;
+ if (isa<tosa::ReciprocalOp>(op))
+ return ElementwiseKind::reciprocal;
+ if (isa<tosa::PowOp>(op))
+ return ElementwiseKind::powf;
+ }
+
+ // Binary ops defined on both floating point and (non-boolean) integers.
+ if (isFloat || isSignlessInt) {
+ if (isa<tosa::AddOp>(op))
+ return ElementwiseKind::add;
+ if (isa<tosa::SubOp>(op))
+ return ElementwiseKind::sub;
+ }
+
+ // Signed integer division.
+ if (isSignlessInt && isa<tosa::IntDivOp>(op))
+ return ElementwiseKind::div;
+
+ // Multiply maps to a plain elementwise multiply only when no rescaling shift
+ // is requested (the shift must be a constant zero) and the inputs already
+ // share the result element type. The shifted/rescaled integer variant is left
+ // to the generic conversion.
+ if (auto mulOp = dyn_cast<tosa::MulOp>(op)) {
+ DenseElementsAttr shiftAttr;
+ const bool zeroShift =
+ matchPattern(mulOp.getShift(), m_Constant(&shiftAttr)) &&
+ shiftAttr.getValues<APInt>()[0].isZero();
+ const bool matchingTypes =
+ cast<ShapedType>(mulOp.getInput1().getType()).getElementType() ==
+ elementTy &&
+ cast<ShapedType>(mulOp.getInput2().getType()).getElementType() ==
+ elementTy;
+ if ((isFloat || isSignlessInt) && zeroShift && matchingTypes)
+ return ElementwiseKind::mul;
+ }
+
+ // Negate maps to floating-point negation only when the zero points are zero.
+ // The quantized integer variant is left to the generic conversion.
+ if (auto negateOp = dyn_cast<tosa::NegateOp>(op)) {
+ if (isFloat) {
+ FailureOr<int64_t> inZp = negateOp.getInput1ZeroPoint();
+ FailureOr<int64_t> outZp = negateOp.getOutputZeroPoint();
+ if (succeeded(inZp) && succeeded(outZp) && *inZp == 0 && *outZp == 0)
+ return ElementwiseKind::negf;
+ }
+ }
+
+ // Maximum/minimum. The NaN "IGNORE" mode requires an additional compare and
+ // select that linalg.elementwise cannot express, so only the default
+ // "PROPAGATE" mode is handled for floating point types.
+ auto nanModeIsPropagate = [](auto tosaOp) {
+ return tosaOp.getNanMode() == NanPropagationMode::PROPAGATE;
+ };
+ if (isFloat || isSignlessInt) {
+ if (auto maxOp = dyn_cast<tosa::MaximumOp>(op)) {
+ if (!isFloat || nanModeIsPropagate(maxOp))
+ return ElementwiseKind::max_signed;
+ }
+ if (auto minOp = dyn_cast<tosa::MinimumOp>(op)) {
+ if (!isFloat || nanModeIsPropagate(minOp))
+ return ElementwiseKind::min_signed;
+ }
+ }
+
+ // Select (ternary) is valid for any element type.
+ if (isa<tosa::SelectOp>(op))
+ return ElementwiseKind::select;
+
+ return std::nullopt;
+}
+
+bool mlir::tosa::isConvertibleToLinalgElementwise(Operation *op) {
+ if (op->getNumResults() != 1)
+ return false;
+
+ auto resultTy = dyn_cast<ShapedType>(op->getResult(0).getType());
+ if (!resultTy || !resultTy.hasRank())
+ return false;
+
+ if (!getElementwiseKind(op, resultTy.getElementType()))
+ return false;
+
+ // linalg.elementwise broadcasts operands through their indexing maps, so
+ // operands only need to be ranked shaped types (tensors or memrefs, possibly
+ // dynamic); their rank need not match the result's.
+ const unsigned numInputs = getArity(op);
+ for (unsigned i = 0; i < numInputs; ++i) {
+ auto operandTy = dyn_cast<ShapedType>(op->getOperand(i).getType());
+ if (!operandTy || !operandTy.hasRank())
+ return false;
+ }
+ return true;
+}
+
static mlir::Value applyPad(Location loc, Value input, ArrayRef<int64_t> pad,
TypedAttr padAttr, OpBuilder &rewriter) {
// Input should be padded only if necessary.
@@ -603,6 +749,84 @@ class DepthwiseConvConverter
}
};
+// Lower TOSA elementwise ops that have a direct linalg.elementwise named op
+// counterpart. Unlike linalg.generic, linalg.elementwise dispatches on a single
+// function kind and infers the body, so the conversion is a handful of checks
+// (kind, ranks, static shapes) followed by construction of the indexing maps
+// and the named op.
+template <typename TosaOp>
+class ElementwiseConverter : public OpConversionPattern<TosaOp> {
+public:
+ using OpConversionPattern<TosaOp>::OpConversionPattern;
+ using typename OpConversionPattern<TosaOp>::OpAdaptor;
+
+ LogicalResult
+ matchAndRewrite(TosaOp op, OpAdaptor adaptor,
+ ConversionPatternRewriter &rewriter) const final {
+ Operation *operation = op;
+ if (!isConvertibleToLinalgElementwise(operation))
+ return rewriter.notifyMatchFailure(
+ op, "op has no linalg.elementwise counterpart");
+
+ Location loc = op.getLoc();
+ auto resultTy = cast<RankedTensorType>(op.getType());
+ const int64_t rank = resultTy.getRank();
+
+ std::optional<linalg::ElementwiseKind> kind =
+ getElementwiseKind(operation, resultTy.getElementType());
+
+ // Trailing metadata operands (e.g. MulOp's shift, NegateOp's zero points)
+ // are not forwarded to linalg.elementwise.
+ ValueRange inputs = adaptor.getOperands().take_front(getArity(operation));
+
+ // Build one indexing map per input, broadcasting static size-1 dimensions,
+ // followed by the identity map for the output.
+ SmallVector<AffineMap> indexingMaps;
+ for (Value operand : inputs) {
+ ArrayRef<int64_t> shape = cast<ShapedType>(operand.getType()).getShape();
+ SmallVector<AffineExpr> exprs;
+ for (const auto &dim : llvm::enumerate(shape)) {
+ bool requiresBroadcast =
+ dim.value() == 1 && resultTy.getDimSize(dim.index()) != 1;
+ exprs.push_back(requiresBroadcast
+ ? rewriter.getAffineConstantExpr(0)
+ : rewriter.getAffineDimExpr(dim.index()));
+ }
+ indexingMaps.push_back(
+ AffineMap::get(rank, /*symbolCount=*/0, exprs, rewriter.getContext()));
+ }
+ indexingMaps.push_back(rewriter.getMultiDimIdentityMap(rank));
+
+ // Compute the output sizes, taking each dynamic result dimension from the
+ // first input that is not broadcast (statically 1) along that dimension.
+ SmallVector<OpFoldResult> sizes;
+ for (int64_t i = 0; i < rank; ++i) {
+ if (!resultTy.isDynamicDim(i)) {
+ sizes.push_back(rewriter.getIndexAttr(resultTy.getDimSize(i)));
+ continue;
+ }
+ Value dimSource = inputs.front();
+ for (Value operand : inputs) {
+ auto operandTy = cast<ShapedType>(operand.getType());
+ if (operandTy.getRank() == rank && operandTy.getDimSize(i) != 1) {
+ dimSource = operand;
+ break;
+ }
+ }
+ sizes.push_back(
+ tensor::DimOp::create(rewriter, loc, dimSource, i).getResult());
+ }
+
+ Value emptyTensor = tensor::EmptyOp::create(rewriter, loc, sizes,
+ resultTy.getElementType());
+
+ rewriter.replaceOpWithNewOp<linalg::ElementwiseOp>(
+ op, inputs, ValueRange{emptyTensor}, *kind,
+ rewriter.getAffineMapArrayAttr(indexingMaps));
+ return success();
+ }
+};
+
class MatMulConverter : public OpConversionPattern<tosa::MatMulOp> {
public:
using OpConversionPattern<tosa::MatMulOp>::OpConversionPattern;
@@ -1138,6 +1362,29 @@ void mlir::tosa::populateTosaToLinalgNamedConversionPatterns(
TransposeConverter
>(patterns->getContext());
+ patterns->add<
+ ElementwiseConverter<tosa::AbsOp>,
+ ElementwiseConverter<tosa::CeilOp>,
+ ElementwiseConverter<tosa::FloorOp>,
+ ElementwiseConverter<tosa::ExpOp>,
+ ElementwiseConverter<tosa::LogOp>,
+ ElementwiseConverter<tosa::RsqrtOp>,
+ ElementwiseConverter<tosa::SinOp>,
+ ElementwiseConverter<tosa::CosOp>,
+ ElementwiseConverter<tosa::TanhOp>,
+ ElementwiseConverter<tosa::ErfOp>,
+ ElementwiseConverter<tosa::ReciprocalOp>,
+ ElementwiseConverter<tosa::PowOp>,
+ ElementwiseConverter<tosa::AddOp>,
+ ElementwiseConverter<tosa::SubOp>,
+ ElementwiseConverter<tosa::IntDivOp>,
+ ElementwiseConverter<tosa::MulOp>,
+ ElementwiseConverter<tosa::NegateOp>,
+ ElementwiseConverter<tosa::MaximumOp>,
+ ElementwiseConverter<tosa::MinimumOp>,
+ ElementwiseConverter<tosa::SelectOp>
+ >(patterns->getContext());
+
patterns->add<
MaxPool2dConverter
>(converter, patterns->getContext());
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamedPass.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamedPass.cpp
index df066a5cac2612..008951120c58d5 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamedPass.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamedPass.cpp
@@ -62,6 +62,17 @@ struct TosaToLinalgNamed
target.addIllegalOp<tosa::MatMulOp>();
target.addIllegalOp<tosa::TransposeOp>();
+ // TOSA elementwise ops are legal unless they can be lowered to a
+ // linalg.elementwise named op.
+ target.addDynamicallyLegalOp<
+ tosa::AbsOp, tosa::CeilOp, tosa::FloorOp, tosa::ExpOp, tosa::LogOp,
+ tosa::RsqrtOp, tosa::SinOp, tosa::CosOp, tosa::TanhOp, tosa::ErfOp,
+ tosa::ReciprocalOp, tosa::PowOp, tosa::AddOp, tosa::SubOp,
+ tosa::IntDivOp, tosa::MulOp, tosa::NegateOp, tosa::MaximumOp,
+ tosa::MinimumOp, tosa::SelectOp>([](Operation *op) {
+ return !tosa::isConvertibleToLinalgElementwise(op);
+ });
+
target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
FunctionOpInterface func = getOperation();
diff --git a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-invalid.mlir b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-invalid.mlir
index ecfd953f9ef1f4..e8a249d84dc490 100644
--- a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-invalid.mlir
+++ b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-invalid.mlir
@@ -73,3 +73,16 @@ func.func @unranked_gather(%arg0: tensor<13x21x3xf32>, %arg1: tensor<13x26xi32>)
%0 = tosa.gather %arg0, %arg1 : (tensor<13x21x3xf32>, tensor<13x26xi32>) -> tensor<*xf32>
return %0 : tensor<*xf32>
}
+
+// -----
+
+// CHECK-LABEL: @unsupportedRescaleInexactRound
+func.func @unsupportedRescaleInexactRound(%arg0 : tensor<2xi8>) -> (tensor<2xi8>) {
+ %multiplier = "tosa.const"() {values = dense<19689> : tensor<1xi32> } : () -> tensor<1xi32>
+ %shift = "tosa.const"() {values = dense<33> : tensor<1xi8> } : () -> tensor<1xi8>
+ %input_zp = "tosa.const"() {values = dense<0> : tensor<1xi8>} : () -> tensor<1xi8>
+ %output_zp = "tosa.const"() {values = dense<0> : tensor<1xi8>} : () -> tensor<1xi8>
+ // expected-error at +1 {{failed to legalize operation 'tosa.rescale'}}
+ %0 = tosa.rescale %arg0, %multiplier, %shift, %input_zp, %output_zp {scale32 = true, rounding_mode = INEXACT_ROUND, per_channel = false, input_unsigned = false, output_unsigned = false} : (tensor<2xi8>, tensor<1xi32>, tensor<1xi8>, tensor<1xi8>, tensor<1xi8>) -> tensor<2xi8>
+ return %0 : tensor<2xi8>
+}
diff --git a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-named.mlir b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-named.mlir
index 801144c01829a9..0d14f47374859c 100644
--- a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-named.mlir
+++ b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg-named.mlir
@@ -1078,3 +1078,43 @@ func.func @max_pool2d_nan_ignore(%arg0: tensor<1x6x34x62xf32>) -> (tensor<1x4x32
%0 = tosa.max_pool2d %arg0 {pad = array<i64: 0, 0, 0, 0>, kernel = array<i64: 3, 3>, stride = array<i64: 1, 1>, nan_mode = IGNORE} : (tensor<1x6x34x62xf32>) -> tensor<1x4x32x62xf32>
return %0: tensor<1x4x32x62xf32>
}
+
+// -----
+
+// CHECK-LABEL: @elementwise_unary_dyn
+func.func @elementwise_unary_dyn(%arg0: tensor<?x3xf32>) -> tensor<?x3xf32> {
+ // CHECK: %[[C0:.+]] = arith.constant 0 : index
+ // CHECK: %[[DIM:.+]] = tensor.dim %arg0, %[[C0]] : tensor<?x3xf32>
+ // CHECK: %[[EMPTY:.+]] = tensor.empty(%[[DIM]]) : tensor<?x3xf32>
+ // CHECK: linalg.elementwise <abs> ins(%arg0 : tensor<?x3xf32>) outs(%[[EMPTY]] : tensor<?x3xf32>) -> tensor<?x3xf32>
+ %0 = tosa.abs %arg0 : (tensor<?x3xf32>) -> tensor<?x3xf32>
+ return %0 : tensor<?x3xf32>
+}
+
+// -----
+
+// CHECK-LABEL: @elementwise_binary_dyn
+func.func @elementwise_binary_dyn(%arg0: tensor<?x?xf32>, %arg1: tensor<?x?xf32>) -> tensor<?x?xf32> {
+ // CHECK: %[[C0:.+]] = arith.constant 0 : index
+ // CHECK: %[[DIM0:.+]] = tensor.dim %arg0, %[[C0]] : tensor<?x?xf32>
+ // CHECK: %[[C1:.+]] = arith.constant 1 : index
+ // CHECK: %[[DIM1:.+]] = tensor.dim %arg0, %[[C1]] : tensor<?x?xf32>
+ // CHECK: %[[EMPTY:.+]] = tensor.empty(%[[DIM0]], %[[DIM1]]) : tensor<?x?xf32>
+ // CHECK: linalg.elementwise <add> ins(%arg0, %arg1 : tensor<?x?xf32>, tensor<?x?xf32>) outs(%[[EMPTY]] : tensor<?x?xf32>) -> tensor<?x?xf32>
+ %0 = tosa.add %arg0, %arg1 : (tensor<?x?xf32>, tensor<?x?xf32>) -> tensor<?x?xf32>
+ return %0 : tensor<?x?xf32>
+}
+
+// -----
+
+// CHECK-LABEL: @elementwise_ternary_select_dyn
+func.func @elementwise_ternary_select_dyn(%arg0: tensor<?x?xi1>, %arg1: tensor<?x?xf32>, %arg2: tensor<?x?xf32>) -> tensor<?x?xf32> {
+ // CHECK: %[[C0:.+]] = arith.constant 0 : index
+ // CHECK: %[[DIM0:.+]] = tensor.dim %arg0, %[[C0]] : tensor<?x?xi1>
+ // CHECK: %[[C1:.+]] = arith.constant 1 : index
+ // CHECK: %[[DIM1:.+]] = tensor.dim %arg0, %[[C1]] : tensor<?x?xi1>
+ // CHECK: %[[EMPTY:.+]] = tensor.empty(%[[DIM0]], %[[DIM1]]) : tensor<?x?xf32>
+ // CHECK: linalg.elementwise <select> ins(%arg0, %arg1, %arg2 : tensor<?x?xi1>, tensor<?x?xf32>, tensor<?x?xf32>) outs(%[[EMPTY]] : tensor<?x?xf32>) -> tensor<?x?xf32>
+ %0 = tosa.select %arg0, %arg1, %arg2 : (tensor<?x?xi1>, tensor<?x?xf32>, tensor<?x?xf32>) -> tensor<?x?xf32>
+ return %0 : tensor<?x?xf32>
+}
diff --git a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg.mlir b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg.mlir
index a803ee7d99153c..7af876b96e51fc 100644
--- a/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg.mlir
+++ b/mlir/test/Conversion/TosaToLinalg/tosa-to-linalg.mlir
@@ -1,9 +1,13 @@
// RUN: mlir-opt --split-input-file -pass-pipeline="builtin.module(func.func(tosa-to-linalg))" %s -verify-diagnostics -o -| FileCheck %s
+// RUN: mlir-opt --split-input-file -pass-pipeline="builtin.module(func.func(tosa-to-linalg-named))" %s -verify-diagnostics -o -| FileCheck %s --check-prefix=NAMED
// CHECK: #[[$MAP0:.*]] = affine_map<() -> ()>
// CHECK-LABEL: @test_abs_scalar
// CHECK-SAME: ([[ARG0:%[0-9a-zA-Z_]*]]
+
+// NAMED-LABEL: @test_abs_scalar
+// NAMED-SAME: ([[ARG0:%[0-9a-zA-Z_]*]]
func.func @test_abs_scalar(%arg0: tensor<f32>) -> tensor<f32> {
// CHECK: [[INIT:%.+]] = tensor.empty() : tensor<f32>
// CHECK: [[GENERIC:%.+]] = linalg.generic {indexing_maps = [#[[$MAP0]], #[[$MAP0]]], iterator_types = []} ins([[ARG0]] : tensor<f32>) outs([[INIT]] : tensor<f32>) {
@@ -11,6 +15,8 @@ func.func @test_abs_scalar(%arg0: tensor<f32>) -> tensor<f32> {
// CHECK: [[ELEMENT:%.*]] = math.absf [[ARG1]] : f32
// CHECK: linalg.yield [[ELEMENT]] : f32
// CHECK: } -> tensor<f32>
+ // NAMED: linalg.elementwise <abs>
+ // NAMED-SAME: ins([[ARG0]]
%0 = tosa.abs %arg0 : (tensor<f32>) -> tensor<f32>
// CHECK: return [[GENERIC]] : tensor<f32>
@@ -84,6 +90,8 @@ func.func @test_abs_1d_dynamic(%arg0: tensor<?xf32>) -> tensor<?xf32> {
// CHECK-LABEL: @test_add_0d
// CHECK-SAME: [[ARG0:%[0-9a-zA-Z_]*]]:
// CHECK-SAME: [[ARG1:%[0-9a-zA-Z_]*]]:
+// NAMED-LABEL: @test_add_0d
+// NAMED: linalg.elementwise <add>
func.func @test_add_0d(%arg0: tensor<f32>, %arg1: tensor<f32>) -> tensor<f32> {
// CHECK: [[EMPTY:%.+]] = tensor.empty() : tensor<f32>
@@ -103,6 +111,8 @@ func.func @test_add_0d(%arg0: tensor<f32>, %arg1: tensor<f32>) -> tensor<f32> {
// CHECK: #[[$MAP0:.+]] = affine_map<(d0, d1) -> (d0, d1)>
// CHECK: #[[$MAP1:.+]] = affine_map<(d0, d1) -> (0, d1)>
+// NAMED: #[[$MAP0:.+]] = affine_map<(d0, d1) -> (d0, d1)>
+// NAMED: #[[$MAP1:.+]] = affine_map<(d0, d1) -> (0, d1)>
// CHECK-LABEL: func.func @test_add_2d_broadcast(
// CHECK-SAME: %[[ARG0:.*]]: tensor<2x1xf32>,
@@ -115,6 +125,9 @@ func.func @test_add_0d(%arg0: tensor<f32>, %arg1: tensor<f32>) -> tensor<f32> {
// CHECK: } -> tensor<2x1xf32>
// CHECK: return %[[RESULT]] : tensor<2x1xf32>
// CHECK: }
+// NAMED-LABEL: func.func @test_add_2d_broadcast
+// NAMED: linalg.elementwise <add>
+// NAMED-SAME: indexing_maps = [#[[$MAP0]], #[[$MAP1]], #[[$MAP0]]]
func.func @test_add_2d_broadcast(%arg0: tensor<2x1xf32>, %arg1: tensor<1x1xf32>) -> tensor<2x1xf32> {
// tosa element-wise operators now require operands of equal ranks
%0 = tosa.add %arg0, %arg1 : (tensor<2x1xf32>, tensor<1x1xf32>) -> tensor<2x1xf32>
@@ -211,9 +224,14 @@ func.func @test_add_1d_broadcast_dynamic_to_static(%arg0: tensor<5xf32>, %arg1:
// CHECK: #[[$MAP0:.+]] = affine_map<(d0) -> (0)>
// CHECK: #[[$MAP1:.+]] = affine_map<(d0) -> (d0)>
+// NAMED: #[[$MAP0:.+]] = affine_map<(d0) -> (0)>
+// NAMED: #[[$MAP1:.+]] = affine_map<(d0) -> (d0)>
// CHECK-LABEL: @test_add_1d_broadcast_static_to_dynamic
// CHECK-SAME: %[[ARG0:[0-9a-zA-Z_]*]]:
// CHECK-SAME: %[[ARG1:[0-9a-zA-Z_]*]]:
+// NAMED-LABEL: @test_add_1d_broadcast_static_to_dynamic
+// NAMED: linalg.elementwise <add>
+// NAMED-SAME: indexing_maps = [#[[$MAP0]], #[[$MAP1]], #[[$MAP1]]]
func.func @test_add_1d_broadcast_static_to_dynamic(%arg0: tensor<1xf32>, %arg1: tensor<?xf32>) -> tensor<?xf32> {
// CHECK: %[[CONST0:.*]] = arith.constant 0 : index
@@ -234,9 +252,14 @@ func.func @test_add_1d_broadcast_static_to_dynamic(%arg0: tensor<1xf32>, %arg1:
// CHECK: #[[$MAP0:.+]] = affine_map<(d0) -> (0)>
// CHECK: #[[$MAP1:.+]] = affine_map<(d0) -> (d0)>
+// NAMED: #[[$MAP0:.+]] = affine_map<(d0) -> (0)>
+// NAMED: #[[$MAP1:.+]] = affine_map<(d0) -> (d0)>
// CHECK-LABEL: @test_add_1d_broadcast_static_to_static
// CHECK-SAME: %[[ARG0:[0-9a-zA-Z_]*]]:
// CHECK-SAME: %[[ARG1:[0-9a-zA-Z_]*]]:
+// NAMED-LABEL: @test_add_1d_broadcast_static_to_static
+// NAMED: linalg.elementwise <add>
+// NAMED-SAME: indexing_maps = [#[[$MAP0]], #[[$MAP1]], #[[$MAP1]]]
func.func @test_add_1d_broadcast_static_to_static(%arg0: tensor<1xf32>, %arg1: tensor<3xf32>) -> tensor<3xf32> {
// CHECK: %[[VAL_0:.*]] = tensor.empty() : tensor<3xf32>
@@ -257,6 +280,8 @@ func.func @test_add_1d_broadcast_static_to_static(%arg0: tensor<1xf32>, %arg1: t
// CHECK-LABEL: @test_add_1d_matching_no_broadcast
// CHECK-SAME: %[[ARG0:[0-9a-zA-Z_]*]]:
// CHECK-SAME: %[[ARG1:[0-9a-zA-Z_]*]]:
+// NAMED-LABEL: @test_add_1d_matching_no_broadcast
+// NAMED: linalg.elementwise <add>
func.func @test_add_1d_matching_no_broadcast(%arg0: tensor<1xf32>, %arg1: tensor<1xf32>) -> tensor<1xf32> {
// CHECK: %[[VAL_0:.*]] = tensor.empty() : tensor<1xf32>
@@ -277,6 +302,8 @@ func.func @test_add_1d_matching_no_broadcast(%arg0: tensor<1xf32>, %arg1: tensor
// CHECK-LABEL: @test_add_1d_matching_static
// CHECK-SAME: %[[ARG0:[0-9a-zA-Z_]*]]:
// CHECK-SAME: %[[ARG1:[0-9a-zA-Z_]*]]:
+// NAMED-LABEL: @test_add_1d_matching_static
+// NAMED: linalg.elementwise <add>
func.func @test_add_1d_matching_static(%arg0: tensor<3xf32>, %arg1: tensor<3xf32>) -> tensor<3xf32> {
// CHECK: %[[VAL_0:.*]] = tensor.empty() : tensor<3xf32>
@@ -453,6 +480,26 @@ func.func @test_select_2d_one_dynamic(%arg0: tensor<2x?xi1>, %arg1: tensor<2x?xf
// -----
// CHECK-LABEL: @test_simple_f32
+// NAMED-LABEL: @test_simple_f32
+// NAMED: linalg.elementwise <tanh>
+// NAMED: linalg.elementwise <abs>
+// NAMED: linalg.elementwise <add>
+// NAMED: linalg.elementwise <sub>
+// NAMED: linalg.elementwise <mul>
+// NAMED: linalg.elementwise <negf>
+// NAMED: linalg.elementwise <powf>
+// NAMED: linalg.elementwise <rsqrt>
+// NAMED: linalg.elementwise <log>
+// NAMED: linalg.elementwise <exp>
+// NAMED: linalg.elementwise <select>
+// NAMED: linalg.elementwise <max_signed>
+// NAMED: linalg.elementwise <min_signed>
+// NAMED: linalg.elementwise <ceil>
+// NAMED: linalg.elementwise <floor>
+// NAMED: linalg.elementwise <reciprocal>
+// NAMED: linalg.elementwise <erf>
+// NAMED: linalg.elementwise <sin>
+// NAMED: linalg.elementwise <cos>
func.func @test_simple_f32(%arg0: tensor<1xf32>) -> () {
// CHECK: linalg.generic
// CHECK: tanh
@@ -639,6 +686,13 @@ func.func @test_simple_ui8(%arg0: tensor<1xui8>) -> () {
// -----
// CHECK-LABEL: @test_simple_i32
+// NAMED-LABEL: @test_simple_i32
+// NAMED: linalg.elementwise <add>
+// NAMED: linalg.elementwise <sub>
+// NAMED: linalg.elementwise <div>
+// NAMED: linalg.elementwise <select>
+// NAMED: linalg.elementwise <max_signed>
+// NAMED: linalg.elementwise <min_signed>
func.func @test_simple_i32(%arg0: tensor<1xi32>, %unsigned: tensor<1xui32>, %unsigned64: tensor<1xui64>) -> () {
// CHECK: linalg.generic
// CHECK: arith.addi
@@ -1537,18 +1591,6 @@ func.func @rescaleUnnecessaryDoubleRound(%arg0 : tensor<2xi8>) -> (tensor<2xi8>)
// -----
-func.func @unsupportedRescaleInexactRound(%arg0 : tensor<2xi8>) -> (tensor<2xi8>) {
- %multiplier = "tosa.const"() {values = dense<19689> : tensor<1xi32> } : () -> tensor<1xi32>
- %shift = "tosa.const"() {values = dense<33> : tensor<1xi8> } : () -> tensor<1xi8>
- %input_zp = "tosa.const"() {values = dense<0> : tensor<1xi8>} : () -> tensor<1xi8>
- %output_zp = "tosa.const"() {values = dense<0> : tensor<1xi8>} : () -> tensor<1xi8>
- // expected-error at +1 {{failed to legalize operation 'tosa.rescale'}}
- %0 = tosa.rescale %arg0, %multiplier, %shift, %input_zp, %output_zp {scale32 = true, rounding_mode = INEXACT_ROUND, per_channel = false, input_unsigned = false, output_unsigned = false} : (tensor<2xi8>, tensor<1xi32>, tensor<1xi8>, tensor<1xi8>, tensor<1xi8>) -> tensor<2xi8>
- return %0 : tensor<2xi8>
-}
-
-// -----
-
// CHECK: #[[$MAP0:.+]] = affine_map<(d0) -> (d0)>
// CHECK: #[[$MAP1:.+]] = affine_map<(d0) -> ()>
// CHECK-LABEL: @rescale_no_const
@@ -2464,6 +2506,8 @@ func.func @reduce_max_nan_ignore(%arg0: tensor<5x4xf32>, %arg1: tensor<5x4xf32>)
// -----
// CHECK-LABEL: @minimum_nan_propagate
+// NAMED-LABEL: @minimum_nan_propagate
+// NAMED: linalg.elementwise <min_signed>
func.func @minimum_nan_propagate(%arg0: tensor<5x4xf32>, %arg1: tensor<5x4xf32>) -> () {
// CHECK: linalg.generic
// CHECK: arith.minimumf
@@ -2477,6 +2521,8 @@ func.func @minimum_nan_propagate(%arg0: tensor<5x4xf32>, %arg1: tensor<5x4xf32>)
// -----
// CHECK-LABEL: @maximum_nan_propagate
+// NAMED-LABEL: @maximum_nan_propagate
+// NAMED: linalg.elementwise <max_signed>
func.func @maximum_nan_propagate(%arg0: tensor<5x4xf32>, %arg1: tensor<5x4xf32>) -> () {
// CHECK: linalg.generic
// CHECK: arith.maximumf
@@ -2490,6 +2536,8 @@ func.func @maximum_nan_propagate(%arg0: tensor<5x4xf32>, %arg1: tensor<5x4xf32>)
// -----
// CHECK-LABEL: @minimum_nan_ignore_int
+// NAMED-LABEL: @minimum_nan_ignore_int
+// NAMED: linalg.elementwise <min_signed>
func.func @minimum_nan_ignore_int(%arg0: tensor<5x4xi8>, %arg1: tensor<5x4xi8>) -> () {
// CHECK: linalg.generic
// CHECK: arith.minsi
@@ -2503,6 +2551,8 @@ func.func @minimum_nan_ignore_int(%arg0: tensor<5x4xi8>, %arg1: tensor<5x4xi8>)
// -----
// CHECK-LABEL: @maximum_nan_ignore_int
+// NAMED-LABEL: @maximum_nan_ignore_int
+// NAMED: linalg.elementwise <max_signed>
func.func @maximum_nan_ignore_int(%arg0: tensor<5x4xi8>, %arg1: tensor<5x4xi8>) -> () {
// CHECK: linalg.generic
// CHECK: arith.maxsi
>From 037c09ee3e568a210d7a9923dd92099af6b8b976 Mon Sep 17 00:00:00 2001
From: rengolin <rengolin at systemcall.eu>
Date: Thu, 17 Sep 2026 08:42:44 -0700
Subject: [PATCH 2/5] format
---
mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
index 97353d2dc4d674..bef528e8aa3551 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
@@ -792,8 +792,8 @@ class ElementwiseConverter : public OpConversionPattern<TosaOp> {
? rewriter.getAffineConstantExpr(0)
: rewriter.getAffineDimExpr(dim.index()));
}
- indexingMaps.push_back(
- AffineMap::get(rank, /*symbolCount=*/0, exprs, rewriter.getContext()));
+ indexingMaps.push_back(AffineMap::get(rank, /*symbolCount=*/0, exprs,
+ rewriter.getContext()));
}
indexingMaps.push_back(rewriter.getMultiDimIdentityMap(rank));
>From 116a5688991f577243ea977a4f346d13c7facc9a Mon Sep 17 00:00:00 2001
From: rengolin <rengolin at systemcall.eu>
Date: Mon, 21 Sep 2026 08:23:31 -0700
Subject: [PATCH 3/5] unreachable for getArity
---
mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
index bef528e8aa3551..0d4cb9f174d3d8 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
@@ -41,7 +41,7 @@ static unsigned getArity(Operation *op) {
.Case<tosa::AddOp, tosa::SubOp, tosa::IntDivOp, tosa::MulOp, tosa::PowOp,
tosa::MaximumOp, tosa::MinimumOp>([](Operation *) { return 2u; })
.Case<tosa::SelectOp>([](Operation *) { return 3u; })
- .Default([](Operation *op) { return op->getNumOperands(); });
+ .DefaultUnreachable("Invalid elementwise operation");
}
// Return the linalg.elementwise kind that is semantically equivalent to the
>From ce26bb706d4656d1da12cbf943eac5214cda01dd Mon Sep 17 00:00:00 2001
From: rengolin <rengolin at systemcall.eu>
Date: Thu, 24 Sep 2026 05:08:37 -0700
Subject: [PATCH 4/5] remove getArity helper
---
.../TosaToLinalg/TosaToLinalgNamed.cpp | 30 +++++++------------
1 file changed, 11 insertions(+), 19 deletions(-)
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
index 0d4cb9f174d3d8..40a7835766a5f6 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
@@ -30,20 +30,6 @@
using namespace mlir;
using namespace mlir::tosa;
-// Number of leading data operands the op forwards to linalg.elementwise,
-// matching its unary/binary/ternary arity group. Trailing metadata operands
-// (e.g. MulOp's shift, NegateOp's zero points) are excluded.
-static unsigned getArity(Operation *op) {
- return llvm::TypeSwitch<Operation *, unsigned>(op)
- .Case<tosa::AbsOp, tosa::CeilOp, tosa::FloorOp, tosa::ExpOp, tosa::LogOp,
- tosa::RsqrtOp, tosa::SinOp, tosa::CosOp, tosa::TanhOp, tosa::ErfOp,
- tosa::ReciprocalOp, tosa::NegateOp>([](Operation *) { return 1u; })
- .Case<tosa::AddOp, tosa::SubOp, tosa::IntDivOp, tosa::MulOp, tosa::PowOp,
- tosa::MaximumOp, tosa::MinimumOp>([](Operation *) { return 2u; })
- .Case<tosa::SelectOp>([](Operation *) { return 3u; })
- .DefaultUnreachable("Invalid elementwise operation");
-}
-
// Return the linalg.elementwise kind that is semantically equivalent to the
// given TOSA elementwise op for the provided element type, or std::nullopt if
// the op has no direct linalg.elementwise counterpart. Only ops whose TOSA
@@ -158,14 +144,18 @@ bool mlir::tosa::isConvertibleToLinalgElementwise(Operation *op) {
if (!resultTy || !resultTy.hasRank())
return false;
- if (!getElementwiseKind(op, resultTy.getElementType()))
+ std::optional<linalg::ElementwiseKind> kind =
+ getElementwiseKind(op, resultTy.getElementType());
+ if (!kind)
return false;
// linalg.elementwise broadcasts operands through their indexing maps, so
// operands only need to be ranked shaped types (tensors or memrefs, possibly
- // dynamic); their rank need not match the result's.
- const unsigned numInputs = getArity(op);
- for (unsigned i = 0; i < numInputs; ++i) {
+ // dynamic); their rank need not match the result's. The arity group enum
+ // (Unary=1, Binary=2, Ternary=3) gives the number of forwarded operands.
+ const unsigned arity =
+ llvm::to_underlying(linalg::getArityGroupAndKind(*kind).arityGroup);
+ for (unsigned i = 0; i < arity; ++i) {
auto operandTy = dyn_cast<ShapedType>(op->getOperand(i).getType());
if (!operandTy || !operandTy.hasRank())
return false;
@@ -774,10 +764,12 @@ class ElementwiseConverter : public OpConversionPattern<TosaOp> {
std::optional<linalg::ElementwiseKind> kind =
getElementwiseKind(operation, resultTy.getElementType());
+ const unsigned arity =
+ llvm::to_underlying(linalg::getArityGroupAndKind(*kind).arityGroup);
// Trailing metadata operands (e.g. MulOp's shift, NegateOp's zero points)
// are not forwarded to linalg.elementwise.
- ValueRange inputs = adaptor.getOperands().take_front(getArity(operation));
+ ValueRange inputs = adaptor.getOperands().take_front(arity);
// Build one indexing map per input, broadcasting static size-1 dimensions,
// followed by the identity map for the output.
>From d80432391136ffd42c0ebb4d39dd2c236d5a68d0 Mon Sep 17 00:00:00 2001
From: rengolin <rengolin at systemcall.eu>
Date: Thu, 24 Sep 2026 05:13:51 -0700
Subject: [PATCH 5/5] clarify boolean property
---
.../Conversion/TosaToLinalg/TosaToLinalgNamed.cpp | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
index 40a7835766a5f6..61592ff879460f 100644
--- a/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
+++ b/mlir/lib/Conversion/TosaToLinalg/TosaToLinalgNamed.cpp
@@ -40,8 +40,8 @@ static std::optional<linalg::ElementwiseKind>
getElementwiseKind(Operation *op, Type elementTy) {
using linalg::ElementwiseKind;
const bool isFloat = isa<FloatType>(elementTy);
- const bool isSignlessInt =
- elementTy.isSignlessInteger() && !elementTy.isInteger(1);
+ const bool isSignlessInt = elementTy.isSignlessInteger();
+ const bool isBoolean = elementTy.isInteger(1);
// Unary ops. linalg.elementwise only defines these on floating point types.
if (isFloat) {
@@ -72,7 +72,7 @@ getElementwiseKind(Operation *op, Type elementTy) {
}
// Binary ops defined on both floating point and (non-boolean) integers.
- if (isFloat || isSignlessInt) {
+ if (isFloat || (isSignlessInt && !isBoolean)) {
if (isa<tosa::AddOp>(op))
return ElementwiseKind::add;
if (isa<tosa::SubOp>(op))
@@ -80,7 +80,7 @@ getElementwiseKind(Operation *op, Type elementTy) {
}
// Signed integer division.
- if (isSignlessInt && isa<tosa::IntDivOp>(op))
+ if (isSignlessInt && !isBoolean && isa<tosa::IntDivOp>(op))
return ElementwiseKind::div;
// Multiply maps to a plain elementwise multiply only when no rescaling shift
@@ -97,7 +97,7 @@ getElementwiseKind(Operation *op, Type elementTy) {
elementTy &&
cast<ShapedType>(mulOp.getInput2().getType()).getElementType() ==
elementTy;
- if ((isFloat || isSignlessInt) && zeroShift && matchingTypes)
+ if ((isFloat || (isSignlessInt && !isBoolean)) && zeroShift && matchingTypes)
return ElementwiseKind::mul;
}
@@ -118,7 +118,7 @@ getElementwiseKind(Operation *op, Type elementTy) {
auto nanModeIsPropagate = [](auto tosaOp) {
return tosaOp.getNanMode() == NanPropagationMode::PROPAGATE;
};
- if (isFloat || isSignlessInt) {
+ if (isFloat || (isSignlessInt && !isBoolean)) {
if (auto maxOp = dyn_cast<tosa::MaximumOp>(op)) {
if (!isFloat || nanModeIsPropagate(maxOp))
return ElementwiseKind::max_signed;
More information about the Mlir-commits
mailing list