[Mlir-commits] [mlir] [mlir][linalg] Fix incorrect hoisting of transfers with mismatched masks (PR #232246)
Adam Siemieniuk
llvmlistbot at llvm.org
Thu Oct 22 02:27:02 PDT 2026
================
@@ -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.
----------------
adam-smnk wrote:
Sounds true but doesn't it get rejected only based on read-write mask inequality?
https://github.com/llvm/llvm-project/pull/232246
More information about the Mlir-commits
mailing list