[Mlir-commits] [mlir] [mlir][mem2reg] Promote whole-buffer memref to a vector SSA value (PR #211880)
Andrzej WarzyĆski
llvmlistbot at llvm.org
Mon Aug 10 23:24:19 PDT 2026
================
@@ -0,0 +1,607 @@
+// RUN: mlir-opt %s --mem2reg --split-input-file | FileCheck %s
+
+// A memref that is only ever accessed as a whole buffer through
+// vector.transfer_read / vector.transfer_write is promoted to a single vector
+// SSA value.
+
+// CHECK-LABEL: func.func @whole_buffer_write_read
+// CHECK-SAME: (%[[PAD:.*]]: f32)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[CST:.*]] = arith.constant dense<1.000000e+00> : vector<4xf32>
+// CHECK: return %[[CST]] : vector<4xf32>
+func.func @whole_buffer_write_read(%pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A whole-buffer slot carried across scf.for is threaded as an iter_arg/result.
+
+// CHECK-LABEL: func.func @whole_buffer_in_loop
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[RES:.*]] = scf.for {{.*}} iter_args(%[[IT:.*]] = %{{.*}}) -> (vector<4xf32>)
+// CHECK: %[[NEXT:.*]] = arith.addf %[[IT]], %[[IT]] : vector<4xf32>
+// CHECK: scf.yield %[[NEXT]] : vector<4xf32>
+// CHECK: return %[[RES]] : vector<4xf32>
+func.func @whole_buffer_in_loop(%pad: f32, %lb: index, %ub: index, %step: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ scf.for %i = %lb to %ub step %step {
+ %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ %n = arith.addf %v, %v : vector<4xf32>
+ vector.transfer_write %n, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ }
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A multi-dimensional whole-buffer memref is promoted to a matching vector.
+
+// CHECK-LABEL: func.func @whole_buffer_2d
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: return %{{.*}} : vector<2x4xf32>
+func.func @whole_buffer_2d(%pad: f32) -> vector<2x4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<2x4xf32>
+ %a = memref.alloca() : memref<2x4xf32>
+ vector.transfer_write %cst, %a[%c0, %c0] {in_bounds = [true, true]} : vector<2x4xf32>, memref<2x4xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {in_bounds = [true, true]} : memref<2x4xf32>, vector<2x4xf32>
+ return %r : vector<2x4xf32>
+}
+
+// -----
+
+// Non-zero access offset: the transfer does not cover the whole buffer, so the
+// slot must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_nonzero_index
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_nonzero_index(%pad: f32) -> vector<4xf32> {
+ %c1 = arith.constant 1 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %cst, %a[%c1] {in_bounds = [true]} : vector<4xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c1], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A masked transfer only touches part of the buffer: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_masked
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_masked(%pad: f32, %m: vector<4xi1>) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0], %m {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A partial (out-of-bounds) transfer must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_out_of_bounds
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_out_of_bounds(%pad: f32) -> vector<8xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<8xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] : vector<8xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad : memref<4xf32>, vector<8xf32>
+ return %r : vector<8xf32>
+}
+
+// -----
+
+// A non-identity (transposing) permutation map is not a whole-buffer identity
+// access: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_transpose_map
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_transpose_map(%pad: f32) -> vector<4x2xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<2x4xf32>
+ %a = memref.alloca() : memref<2x4xf32>
+ vector.transfer_write %cst, %a[%c0, %c0] {in_bounds = [true, true]} : vector<2x4xf32>, memref<2x4xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {in_bounds = [true, true], permutation_map = affine_map<(d0, d1) -> (d1, d0)>} : memref<2x4xf32>, vector<4x2xf32>
+ return %r : vector<4x2xf32>
+}
+
+// -----
+
+// An alloca also accessed through a scalar memref.load cannot be promoted to a
+// vector: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_mixed_scalar_access
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+// CHECK: memref.load
+func.func @negative_mixed_scalar_access(%pad: f32) -> (vector<4xf32>, f32) {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca() : memref<4xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<4xf32>, vector<4xf32>
+ %s = memref.load %a[%c0] : memref<4xf32>
+ return %r, %s : vector<4xf32>, f32
+}
+
+// -----
+
+// A vscale*C-sized memref accessed by a whole-buffer vector<[C]> transfer is
+// promoted: its runtime length matches the vector exactly. Here the scalable
+// slot is carried across scf.for as an iter_arg/result.
+
+// CHECK-LABEL: func.func @scalable_whole_buffer_in_loop
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[RES:.*]] = scf.for {{.*}} iter_args(%[[IT:.*]] = %{{.*}}) -> (vector<[4]xf32>)
+// CHECK: %[[NEXT:.*]] = arith.addf %[[IT]], %[[IT]] : vector<[4]xf32>
+// CHECK: scf.yield %[[NEXT]] : vector<[4]xf32>
+// CHECK: return %[[RES]] : vector<[4]xf32>
+func.func @scalable_whole_buffer_in_loop(%pad: f32, %lb: index, %ub: index, %step: index) -> vector<[4]xf32> {
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %cst = arith.constant dense<1.0> : vector<[4]xf32>
+ %vs = vector.vscale
+ %sz = arith.muli %vs, %c4 : index
+ %a = memref.alloca(%sz) : memref<?xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<[4]xf32>, memref<?xf32>
+ scf.for %i = %lb to %ub step %step {
+ %v = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<[4]xf32>
+ %n = arith.addf %v, %v : vector<[4]xf32>
+ vector.transfer_write %n, %a[%c0] {in_bounds = [true]} : vector<[4]xf32>, memref<?xf32>
+ }
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
+
+// -----
+
+// Size mismatch (vscale*8 buffer, vector<[4]xf32> transfer) is a partial
+// access: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_scalable
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_scalable(%pad: f32) -> vector<[4]xf32> {
+ %c0 = arith.constant 0 : index
+ %c8 = arith.constant 8 : index
+ %cst = arith.constant dense<1.0> : vector<[4]xf32>
+ %vs = vector.vscale
+ %sz = arith.muli %vs, %c8 : index
+ %a = memref.alloca(%sz) : memref<?xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<[4]xf32>, memref<?xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
+
+// -----
+
+// An unanalyzable dynamic size (not a vscale multiple) cannot be matched to the
+// vector length, even with a scalable transfer: must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_scalable_plain_dynamic
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_scalable_plain_dynamic(%pad: f32, %d: index) -> vector<[4]xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<[4]xf32>
+ %a = memref.alloca(%d) : memref<?xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<[4]xf32>, memref<?xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
+
+// -----
+
+// A dynamic-shape memref with a fixed-size transfer: the runtime extent is
+// unknown, so the transfer cannot cover the whole buffer. Must NOT be promoted.
+
+// CHECK-LABEL: func.func @negative_dynamic_shape
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @negative_dynamic_shape(%pad: f32, %d: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant dense<1.0> : vector<4xf32>
+ %a = memref.alloca(%d) : memref<?xf32>
+ vector.transfer_write %cst, %a[%c0] {in_bounds = [true]} : vector<4xf32>, memref<?xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A static, same-rank memref.subview is a promotable sub-slice alias: a write
+// into the subview becomes vector.insert_strided_slice into the buffer's value,
+// and the whole-buffer read returns that composed value.
+
+// CHECK-LABEL: func.func @subview_static_write
+// CHECK-SAME: (%[[V:.*]]: vector<4xf32>, %[[INIT:.*]]: vector<8xf32>, %[[PAD:.*]]: f32)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: %[[INS:.*]] = vector.insert_strided_slice %[[V]], %[[INIT]] {offsets = [2], strides = [1]}
+// CHECK: return %[[INS]] : vector<8xf32>
+func.func @subview_static_write(%v: vector<4xf32>, %init: vector<8xf32>, %pad: f32) -> vector<8xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %init, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[2] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: 2>>
+ vector.transfer_write %v, %sv[%c0] {in_bounds = [true]} : vector<4xf32>, memref<4xf32, strided<[1], offset: 2>>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<8xf32>
+ return %r : vector<8xf32>
+}
+
+// -----
+
+// A read of a static subview becomes vector.extract_strided_slice of the value.
+
+// CHECK-LABEL: func.func @subview_static_read
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[EXT:.*]] = vector.extract_strided_slice %{{.*}} {offsets = [2], sizes = [4], strides = [1]}
+// CHECK: return %[[EXT]] : vector<4xf32>
+func.func @subview_static_read(%init: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %init, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[2] [4] [1] : memref<8xf32> to memref<4xf32, strided<[1], offset: 2>>
+ %r = vector.transfer_read %sv[%c0], %pad {in_bounds = [true]} : memref<4xf32, strided<[1], offset: 2>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A buffer accessed *only* through subviews (no whole-buffer transfer) still
+// promotes: the slot comes from the alloca, not from any transfer. The memref,
+// the subviews, and the transfers are all removed; the written slice is
+// inserted into the buffer value and read back out. (canonicalize/cse would
+// then fold this to a plain forward of the written vector.)
+
+// CHECK-LABEL: func.func @subview_only_write_read
+// CHECK-SAME: (%[[V:.*]]: vector<4xf32>, %[[PAD:.*]]: f32)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK-NOT: vector.transfer_write
+// CHECK-NOT: vector.transfer_read
+// CHECK: vector.insert_strided_slice %[[V]], %{{.*}} {offsets = [0], strides = [1]}
+func.func @subview_only_write_read(%v: vector<4xf32>, %pad: f32) -> vector<4xf32> {
----------------
banach-space wrote:
How does this test differ from `@subview_static_read` and `@subview_static_write`? Does it test a different code-path?
https://github.com/llvm/llvm-project/pull/211880
More information about the Mlir-commits
mailing list