[Mlir-commits] [mlir] [mlir][linalg] Skip rank reduction for unsigned-cast contractions (PR #225993)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Sun Sep 27 09:20:29 PDT 2026


https://github.com/speryt updated https://github.com/llvm/llvm-project/pull/225993

>From c7c33f58a3d576eb6cb591a53a6957ffdaa776d0 Mon Sep 17 00:00:00 2001
From: s-peryt <1652033+s-peryt at users.noreply.github.com>
Date: Wed, 23 Sep 2026 18:27:21 -0700
Subject: [PATCH 1/2] [mlir][linalg] Don't rank-reduce contractions whose cast
 can't be preserved

Rank reduction could turn a matmul or batch_matmul with
cast_unsigned into vecmat/matvec (or their batch versions). Those ops
have no cast attribute and always sign-extend, so the unsigned
semantics were silently lost.

Only matmul can carry the cast, so skip the rewrite for other targets
when the cast isn't signed.

Fixes #225253

Used Claude to write test.
---
 .../Linalg/Transforms/DropUnitDims.cpp        |  8 ++
 .../Linalg/rank-reduce-contraction-ops.mlir   | 93 +++++++++++++++++++
 2 files changed, 101 insertions(+)

diff --git a/mlir/lib/Dialect/Linalg/Transforms/DropUnitDims.cpp b/mlir/lib/Dialect/Linalg/Transforms/DropUnitDims.cpp
index dac2891ce65e56..95fbea6197e1db 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/DropUnitDims.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/DropUnitDims.cpp
@@ -963,6 +963,14 @@ struct RankReduceContractionOps : OpRewritePattern<FromOpTy> {
       return rewriter.notifyMatchFailure(
           contractionOp, "ops with user-defined maps are not supported");
     }
+    // linalg.matmul is the only target op with a `cast` attribute.
+    if constexpr (!std::is_same_v<ToOpTy, MatmulOp>) {
+      auto castAttr = dyn_cast_if_present<TypeFnAttr>(
+          contractionOp->getInherentAttr("cast").value_or(Attribute()));
+      if (castAttr && castAttr.getValue() != TypeFn::cast_signed)
+        return rewriter.notifyMatchFailure(
+            contractionOp, "target op cannot represent a non-signed cast");
+    }
 
     auto loc = contractionOp.getLoc();
     auto inputs = contractionOp.getDpsInputs();
diff --git a/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir b/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
index b7c095ca96b2d3..cf7ef21f612252 100644
--- a/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
+++ b/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
@@ -175,6 +175,99 @@ func.func @batch_matmul_to_vecmat(%arg0: memref<1x1x?xf32>, %arg1: memref<1x?x?x
 
 // -----
 
+// linalg.vecmat has no cast attribute (it always uses signed casts), so a
+// matmul with an unsigned cast must not be rank-reduced into it.
+func.func @negative_matmul_to_vecmat_unsigned_cast(
+    %arg0: tensor<1x8xi16>, %arg1: tensor<8x6xi64>,
+    %arg2: tensor<1x6xi32>) -> tensor<1x6xi32> {
+  // CHECK-LABEL: @negative_matmul_to_vecmat_unsigned_cast
+  // CHECK-NOT:   collapse_shape
+  // CHECK:       linalg.matmul {cast = #linalg.type_fn<cast_unsigned>}
+  // CHECK-NOT:   linalg.vecmat
+  %0 = linalg.matmul {cast = #linalg.type_fn<cast_unsigned>}
+      ins(%arg0, %arg1 : tensor<1x8xi16>, tensor<8x6xi64>)
+      outs(%arg2 : tensor<1x6xi32>) -> tensor<1x6xi32>
+  return %0 : tensor<1x6xi32>
+}
+
+// -----
+
+func.func @negative_matmul_to_matvec_unsigned_cast(
+    %arg0: tensor<6x8xi16>, %arg1: tensor<8x1xi64>,
+    %arg2: tensor<6x1xi32>) -> tensor<6x1xi32> {
+  // CHECK-LABEL: @negative_matmul_to_matvec_unsigned_cast
+  // CHECK-NOT:   collapse_shape
+  // CHECK:       linalg.matmul {cast = #linalg.type_fn<cast_unsigned>}
+  // CHECK-NOT:   linalg.matvec
+  %0 = linalg.matmul {cast = #linalg.type_fn<cast_unsigned>}
+      ins(%arg0, %arg1 : tensor<6x8xi16>, tensor<8x1xi64>)
+      outs(%arg2 : tensor<6x1xi32>) -> tensor<6x1xi32>
+  return %0 : tensor<6x1xi32>
+}
+
+// -----
+
+func.func @negative_batch_matmul_to_batch_vecmat_unsigned_cast(
+    %arg0: tensor<2x1x8xi16>, %arg1: tensor<2x8x6xi64>,
+    %arg2: tensor<2x1x6xi32>) -> tensor<2x1x6xi32> {
+  // CHECK-LABEL: @negative_batch_matmul_to_batch_vecmat_unsigned_cast
+  // CHECK-NOT:   collapse_shape
+  // CHECK:       linalg.batch_matmul {cast = #linalg.type_fn<cast_unsigned>}
+  // CHECK-NOT:   linalg.batch_vecmat
+  %0 = linalg.batch_matmul {cast = #linalg.type_fn<cast_unsigned>}
+      ins(%arg0, %arg1 : tensor<2x1x8xi16>, tensor<2x8x6xi64>)
+      outs(%arg2 : tensor<2x1x6xi32>) -> tensor<2x1x6xi32>
+  return %0 : tensor<2x1x6xi32>
+}
+
+// -----
+
+func.func @negative_batch_matmul_to_batch_matvec_unsigned_cast(
+    %arg0: tensor<2x6x8xi16>, %arg1: tensor<2x8x1xi64>,
+    %arg2: tensor<2x6x1xi32>) -> tensor<2x6x1xi32> {
+  // CHECK-LABEL: @negative_batch_matmul_to_batch_matvec_unsigned_cast
+  // CHECK-NOT:   collapse_shape
+  // CHECK:       linalg.batch_matmul {cast = #linalg.type_fn<cast_unsigned>}
+  // CHECK-NOT:   linalg.batch_matvec
+  %0 = linalg.batch_matmul {cast = #linalg.type_fn<cast_unsigned>}
+      ins(%arg0, %arg1 : tensor<2x6x8xi16>, tensor<2x8x1xi64>)
+      outs(%arg2 : tensor<2x6x1xi32>) -> tensor<2x6x1xi32>
+  return %0 : tensor<2x6x1xi32>
+}
+
+// -----
+
+// The unit batch dim can still be dropped since linalg.matmul preserves the
+// cast, but the rank reduction must stop there.
+func.func @singleton_batch_matmul_unsigned_cast_to_matmul_only(
+    %arg0: tensor<1x1x8xi16>, %arg1: tensor<1x8x6xi64>,
+    %arg2: tensor<1x1x6xi32>) -> tensor<1x1x6xi32> {
+  // CHECK-LABEL: @singleton_batch_matmul_unsigned_cast_to_matmul_only
+  // CHECK:       linalg.matmul {cast = #linalg.type_fn<cast_unsigned>}
+  // CHECK-NOT:   linalg.vecmat
+  // CHECK-NOT:   linalg.batch_vecmat
+  %0 = linalg.batch_matmul {cast = #linalg.type_fn<cast_unsigned>}
+      ins(%arg0, %arg1 : tensor<1x1x8xi16>, tensor<1x8x6xi64>)
+      outs(%arg2 : tensor<1x1x6xi32>) -> tensor<1x1x6xi32>
+  return %0 : tensor<1x1x6xi32>
+}
+
+// -----
+
+// An explicit signed cast matches the target's semantics and is rank-reduced.
+func.func @matmul_to_vecmat_signed_cast(
+    %arg0: tensor<1x8xi16>, %arg1: tensor<8x6xi64>,
+    %arg2: tensor<1x6xi32>) -> tensor<1x6xi32> {
+  // CHECK-LABEL: @matmul_to_vecmat_signed_cast
+  // CHECK:       linalg.vecmat
+  %0 = linalg.matmul {cast = #linalg.type_fn<cast_signed>}
+      ins(%arg0, %arg1 : tensor<1x8xi16>, tensor<8x6xi64>)
+      outs(%arg2 : tensor<1x6xi32>) -> tensor<1x6xi32>
+  return %0 : tensor<1x6xi32>
+}
+
+// -----
+
 func.func @matvec_to_dot(%arg0: memref<1x?xf32>, %arg1: memref<?xf32>, %arg2: memref<1xf32>) {
   // CHECK-LABEL: @matvec_to_dot
   //  CHECK-SAME:     %[[LHS:[a-zA-Z0-9]+]]: memref<1x?xf32>

>From 924b5631850fcfa90b217a000a4dac94e4114755 Mon Sep 17 00:00:00 2001
From: s-peryt <1652033+s-peryt at users.noreply.github.com>
Date: Sun, 27 Sep 2026 09:12:03 -0700
Subject: [PATCH 2/2] Updated test comment

---
 mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir b/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
index cf7ef21f612252..ff5f63f291625c 100644
--- a/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
+++ b/mlir/test/Dialect/Linalg/rank-reduce-contraction-ops.mlir
@@ -175,8 +175,8 @@ func.func @batch_matmul_to_vecmat(%arg0: memref<1x1x?xf32>, %arg1: memref<1x?x?x
 
 // -----
 
-// linalg.vecmat has no cast attribute (it always uses signed casts), so a
-// matmul with an unsigned cast must not be rank-reduced into it.
+// linalg.vecmat currently has no cast attribute (it always uses signed casts),
+// so a matmul with an unsigned cast must not be rank-reduced into it.
 func.func @negative_matmul_to_vecmat_unsigned_cast(
     %arg0: tensor<1x8xi16>, %arg1: tensor<8x6xi64>,
     %arg2: tensor<1x6xi32>) -> tensor<1x6xi32> {



More information about the Mlir-commits mailing list