[Mlir-commits] [mlir] [mlir][linalg] Fix incorrect hoisting of transfers with mismatched masks (PR #232246)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Thu Oct 15 02:49:02 PDT 2026
https://github.com/Pecco-314 created https://github.com/llvm/llvm-project/pull/232246
`hoist_redundant_vector_transfers` can change the result of a loop when the read and write have different mask operands.
For example, `%buffer` initially contains `[0, 0]`, `%mask` is `[true, false]`, `%zero` is `0`, and `%ones` is `[1, 1]`:
```mlir
scf.for %i = %c0 to %c2 step %c1 {
%v = vector.transfer_read %buffer[%c0], %zero, %mask
{in_bounds = [true]} : memref<2xi32>, vector<2xi32>
%next = arith.addi %v, %ones : vector<2xi32>
vector.transfer_write %next, %buffer[%c0]
{in_bounds = [true]} : vector<2xi32>, memref<2xi32>
}
```
The masked read returns zero for the second element on every iteration. The two iterations write `[1, 1]` and `[2, 1]`, respectively.
The transformation hoists the read and write out of the loop:
```mlir
%init = vector.transfer_read %buffer[%c0], %zero, %mask
{in_bounds = [true]} : memref<2xi32>, vector<2xi32>
%result = scf.for %i = %c0 to %c2 step %c1
iter_args(%v = %init) -> (vector<2xi32>) {
%next = arith.addi %v, %ones : vector<2xi32>
scf.yield %next : vector<2xi32>
}
vector.transfer_write %result, %buffer[%c0]
{in_bounds = [true]} : vector<2xi32>, memref<2xi32>
```
Here, the mask is applied only before the loop. Both elements are incremented on every iteration, so the final write stores `[2, 2]` instead of `[2, 1]`.
To fix this, hoisting is disallowed when the transfers' mask operands differ, including when only one transfer has a mask. Pairs with identical mask operands or no masks remain eligible for hoisting.
Assisted-by: Codex (OpenAI)
>From 408352a672de47331237d67cbfd21a399d06f96d Mon Sep 17 00:00:00 2001
From: Yanfeng Gao <gaoyanfeng.gyf at alibaba-inc.com>
Date: Thu, 15 Oct 2026 17:12:49 +0800
Subject: [PATCH] [mlir][linalg] Fix incorrect hoisting of transfers with
mismatched masks
`hoist_redundant_vector_transfers` can change the result of a loop when
the read and write have different mask operands.
For example, `%buffer` initially contains `[0, 0]`, `%mask` is
`[true, false]`, `%zero` is `0`, and `%ones` is `[1, 1]`:
```mlir
scf.for %i = %c0 to %c2 step %c1 {
%v = vector.transfer_read %buffer[%c0], %zero, %mask
{in_bounds = [true]} : memref<2xi32>, vector<2xi32>
%next = arith.addi %v, %ones : vector<2xi32>
vector.transfer_write %next, %buffer[%c0]
{in_bounds = [true]} : vector<2xi32>, memref<2xi32>
}
```
The masked read returns zero for the second element on every iteration.
The two iterations write `[1, 1]` and `[2, 1]`, respectively.
The transformation hoists the read and write out of the loop:
```mlir
%init = vector.transfer_read %buffer[%c0], %zero, %mask
{in_bounds = [true]} : memref<2xi32>, vector<2xi32>
%result = scf.for %i = %c0 to %c2 step %c1
iter_args(%v = %init) -> (vector<2xi32>) {
%next = arith.addi %v, %ones : vector<2xi32>
scf.yield %next : vector<2xi32>
}
vector.transfer_write %result, %buffer[%c0]
{in_bounds = [true]} : vector<2xi32>, memref<2xi32>
```
Here, the mask is applied only before the loop. Both elements are
incremented on every iteration, so the final write stores `[2, 2]`
instead of `[2, 1]`.
To fix this, hoisting is disallowed when the transfers' mask operands
differ, including when only one transfer has a mask. Pairs with identical
mask operands or no masks remain eligible for hoisting.
Assisted-by: Codex (OpenAI)
---
.../Dialect/Linalg/Transforms/Hoisting.cpp | 5 +-
.../hoist-redundant-vector-transfers.mlir | 206 ++++++++++++++++++
2 files changed, 209 insertions(+), 2 deletions(-)
diff --git a/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp b/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
index 317f36b3569a18..da90044756a50a 100644
--- a/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
+++ b/mlir/lib/Dialect/Linalg/Transforms/Hoisting.cpp
@@ -302,8 +302,8 @@ void mlir::linalg::hoistRedundantVectorTransfers(Operation *root,
<< "\n");
// Approximate aliasing by checking that:
- // 1. indices, vector type and permutation map are the same (i.e., the
- // transfer_read/transfer_write ops are matching),
+ // 1. indices, vector type, permutation map and mask are the same
+ // (i.e., the transfer_read/transfer_write ops are matching),
// 2. source operands for transfer.{read|write} do not originate from
// nor have users that are Ops implementing ViewLikeOpInterface.
// 3. no other operations in the loop access the same memref except
@@ -312,6 +312,7 @@ void mlir::linalg::hoistRedundantVectorTransfers(Operation *root,
// Check 1.
if (transferRead.getIndices() != transferWrite.getIndices() ||
+ transferRead.getMask() != transferWrite.getMask() ||
transferRead.getVectorType() != transferWrite.getVectorType() ||
transferRead.getPermutationMap() != transferWrite.getPermutationMap())
return WalkResult::advance();
diff --git a/mlir/test/Dialect/Linalg/hoist-redundant-vector-transfers.mlir b/mlir/test/Dialect/Linalg/hoist-redundant-vector-transfers.mlir
index b5eb210964b2af..97af6a67dadb25 100644
--- a/mlir/test/Dialect/Linalg/hoist-redundant-vector-transfers.mlir
+++ b/mlir/test/Dialect/Linalg/hoist-redundant-vector-transfers.mlir
@@ -1747,3 +1747,209 @@ module attributes {transform.with_named_sequence} {
transform.yield
}
}
+
+// -----
+
+// Only the read is masked, so hoisting is not safe. The second element must
+// be read as zero on every iteration, leaving 1 in memory after the loop.
+
+// CHECK-LABEL: func.func @masked_read_unmasked_write()
+// CHECK: %[[MASK:.*]] = arith.constant dense<[true, false]>
+// CHECK: scf.for {{.*}} {
+// CHECK-NEXT: %[[READ:.*]] = vector.transfer_read {{.*}}, %[[MASK]]
+// CHECK-NEXT: %[[NEXT:.*]] = arith.addi %[[READ]],
+// CHECK-NEXT: vector.transfer_write %[[NEXT]],
+// CHECK-NEXT: }{{$}}
+// CHECK: memref.load
+func.func @masked_read_unmasked_write() -> i32 {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %zero = arith.constant 0 : i32
+ %zeros = arith.constant dense<0> : vector<2xi32>
+ %ones = arith.constant dense<1> : vector<2xi32>
+ %mask = arith.constant dense<[true, false]> : vector<2xi1>
+ %buffer = memref.alloca() : memref<2xi32>
+ vector.transfer_write %zeros, %buffer[%c0]
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ scf.for %i = %c0 to %c2 step %c1 {
+ %v = vector.transfer_read %buffer[%c0], %zero, %mask
+ {in_bounds = [true]} : memref<2xi32>, vector<2xi32>
+ %next = arith.addi %v, %ones : vector<2xi32>
+ vector.transfer_write %next, %buffer[%c0]
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ }
+ %result = memref.load %buffer[%c1] : memref<2xi32>
+ return %result : i32
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %funcs = transform.structured.match ops{["func.func"]} in %root
+ : (!transform.any_op) -> !transform.any_op
+ transform.structured.hoist_redundant_vector_transfers %funcs verify_non_zero_trip
+ : (!transform.any_op) -> !transform.any_op
+ transform.yield
+ }
+}
+
+// -----
+
+// Only the write is masked, so hoisting is not safe. The next read must see
+// the elements that the masked write leaves unchanged in memory.
+
+// CHECK-LABEL: func.func @unmasked_read_masked_write(
+// CHECK-SAME: %[[MEM:.*]]: memref<2xi32>, %[[MASK:.*]]: vector<2xi1>
+// CHECK: scf.for {{.*}} {
+// CHECK-NEXT: %[[READ:.*]] = vector.transfer_read %[[MEM]]
+// CHECK-NEXT: %[[SWAP:.*]] = vector.shuffle %[[READ]], %{{.*}} [1, 0]
+// CHECK-NEXT: %[[NEXT:.*]] = arith.addi %[[SWAP]],
+// CHECK-NEXT: vector.transfer_write %[[NEXT]], %[[MEM]]{{.*}}, %[[MASK]]
+// CHECK-NEXT: }{{$}}
+func.func @unmasked_read_masked_write(%mem: memref<2xi32>, %mask: vector<2xi1>) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %zero = arith.constant 0 : i32
+ %ones = arith.constant dense<1> : vector<2xi32>
+ scf.for %i = %c0 to %c2 step %c1 {
+ %v = vector.transfer_read %mem[%c0], %zero
+ {in_bounds = [true]} : memref<2xi32>, vector<2xi32>
+ %swap = vector.shuffle %v, %v [1, 0] : vector<2xi32>, vector<2xi32>
+ %next = arith.addi %swap, %ones : vector<2xi32>
+ vector.transfer_write %next, %mem[%c0], %mask
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %funcs = transform.structured.match ops{["func.func"]} in %root
+ : (!transform.any_op) -> !transform.any_op
+ transform.structured.hoist_redundant_vector_transfers %funcs verify_non_zero_trip
+ : (!transform.any_op) -> !transform.any_op
+ transform.yield
+ }
+}
+
+// -----
+
+// The read and write use separate mask arguments. Hoisting is not safe
+// because the masks may select different elements.
+
+// CHECK-LABEL: func.func @distinct_transfer_masks(
+// CHECK-SAME: %[[MEM:.*]]: memref<2xi32>,
+// CHECK-SAME: %[[MASK:.*]]: vector<2xi1>,
+// CHECK-SAME: %[[READ_MASK:.*]]: vector<2xi1>
+// CHECK: scf.for {{.*}} {
+// CHECK-NEXT: %[[READ:.*]] = vector.transfer_read %[[MEM]]{{.*}}, %[[READ_MASK]]
+// CHECK-NEXT: %[[SWAP:.*]] = vector.shuffle %[[READ]], %{{.*}} [1, 0]
+// CHECK-NEXT: %[[NEXT:.*]] = arith.addi %[[SWAP]],
+// CHECK-NEXT: vector.transfer_write %[[NEXT]], %[[MEM]]{{.*}}, %[[MASK]]
+// CHECK-NEXT: }{{$}}
+func.func @distinct_transfer_masks(
+ %mem: memref<2xi32>, %mask: vector<2xi1>, %read_mask: vector<2xi1>) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %zero = arith.constant 0 : i32
+ %ones = arith.constant dense<1> : vector<2xi32>
+ scf.for %i = %c0 to %c2 step %c1 {
+ %v = vector.transfer_read %mem[%c0], %zero, %read_mask
+ {in_bounds = [true]} : memref<2xi32>, vector<2xi32>
+ %swap = vector.shuffle %v, %v [1, 0] : vector<2xi32>, vector<2xi32>
+ %next = arith.addi %swap, %ones : vector<2xi32>
+ vector.transfer_write %next, %mem[%c0], %mask
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %funcs = transform.structured.match ops{["func.func"]} in %root
+ : (!transform.any_op) -> !transform.any_op
+ transform.structured.hoist_redundant_vector_transfers %funcs verify_non_zero_trip
+ : (!transform.any_op) -> !transform.any_op
+ transform.yield
+ }
+}
+
+// -----
+
+// Hoisting is safe when both transfers use the same mask and the loop
+// updates each vector element independently.
+
+// CHECK-LABEL: func.func @same_transfer_mask(
+// CHECK-SAME: %[[MEM:.*]]: memref<2xi32>, %[[MASK:.*]]: vector<2xi1>
+// CHECK: %[[READ:.*]] = vector.transfer_read %[[MEM]]{{.*}}, %[[MASK]]
+// CHECK-NEXT: %[[LOOP:.*]] = scf.for {{.*}} iter_args(%[[ARG:.*]] = %[[READ]])
+// CHECK-NEXT: %[[NEXT:.*]] = arith.addi %[[ARG]],
+// CHECK-NEXT: scf.yield %[[NEXT]]
+// CHECK-NEXT: }{{$}}
+// CHECK-NEXT: vector.transfer_write %[[LOOP]], %[[MEM]]{{.*}}, %[[MASK]]
+func.func @same_transfer_mask(%mem: memref<2xi32>, %mask: vector<2xi1>) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %zero = arith.constant 0 : i32
+ %ones = arith.constant dense<1> : vector<2xi32>
+ scf.for %i = %c0 to %c2 step %c1 {
+ %v = vector.transfer_read %mem[%c0], %zero, %mask
+ {in_bounds = [true]} : memref<2xi32>, vector<2xi32>
+ %next = arith.addi %v, %ones : vector<2xi32>
+ vector.transfer_write %next, %mem[%c0], %mask
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %funcs = transform.structured.match ops{["func.func"]} in %root
+ : (!transform.any_op) -> !transform.any_op
+ transform.structured.hoist_redundant_vector_transfers %funcs verify_non_zero_trip
+ : (!transform.any_op) -> !transform.any_op
+ transform.yield
+ }
+}
+
+// -----
+
+// The write mask changes on every iteration, so the write must stay
+// inside the loop.
+
+// CHECK-LABEL: func.func @loop_variant_write_mask(
+// CHECK: scf.for %[[IV:.*]] = {{.*}} {
+// CHECK-NEXT: %[[MASK:.*]] = vector.create_mask %[[IV]]
+// CHECK-NEXT: %[[READ:.*]] = vector.transfer_read
+// CHECK-NEXT: %[[NEXT:.*]] = arith.addi %[[READ]],
+// CHECK-NEXT: vector.transfer_write %[[NEXT]], {{.*}}, %[[MASK]]
+// CHECK-NEXT: }{{$}}
+func.func @loop_variant_write_mask(%mem: memref<2xi32>) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %zero = arith.constant 0 : i32
+ %ones = arith.constant dense<1> : vector<2xi32>
+ scf.for %i = %c0 to %c2 step %c1 {
+ %mask = vector.create_mask %i : vector<2xi1>
+ %v = vector.transfer_read %mem[%c0], %zero
+ {in_bounds = [true]} : memref<2xi32>, vector<2xi32>
+ %next = arith.addi %v, %ones : vector<2xi32>
+ vector.transfer_write %next, %mem[%c0], %mask
+ {in_bounds = [true]} : vector<2xi32>, memref<2xi32>
+ }
+ return
+}
+
+module attributes {transform.with_named_sequence} {
+ transform.named_sequence @__transform_main(%root: !transform.any_op {transform.readonly}) {
+ %funcs = transform.structured.match ops{["func.func"]} in %root
+ : (!transform.any_op) -> !transform.any_op
+ transform.structured.hoist_redundant_vector_transfers %funcs verify_non_zero_trip
+ : (!transform.any_op) -> !transform.any_op
+ transform.yield
+ }
+}
More information about the Mlir-commits
mailing list