[Mlir-commits] [mlir] [mlir][vector] Promote in-bounds sub-vector transfer reads with Mem2Reg (PR #236320)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Wed Oct 28 23:21:11 PDT 2026
https://github.com/meinie0826 updated https://github.com/llvm/llvm-project/pull/236320
>From f99ef255158cf880c04806a2421cf932c626083b Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Wed, 28 Oct 2026 13:24:58 +0800
Subject: [PATCH 1/6] [mlir][vector] Promote in-bounds sub-vector transfer
reads with Mem2Reg
Extend the read model to extract a fixed-size, same-rank sub-vector from the slot reaching definition. Require constant contained indices, an identity permutation, no mask, and in-bounds dimensions. Preserve write promotion and existing dynamic subview paths.
---
.../Transforms/MemorySlotInterfacesImpl.cpp | 52 ++-
mlir/test/Transforms/Mem2Reg/vector.mlir | 334 +++++++++++++++++-
2 files changed, 381 insertions(+), 5 deletions(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index 33abbf2644f0fc..5422e041ceff61 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -12,7 +12,8 @@
// and definitions of a vector SSA value. The models for `memref.subview`
// live in the MemRef dialect.
//
-// A read uses the slot's current value; a write defines its next value.
+// A read uses the slot's current value or extracts an in-bounds sub-vector;
+// a write defines its next value.
// Static subviews use a vector matching the subview's shape: reads extract that
// sub-vector from the parent, and writes insert it back into the parent.
// Supported transfers through dynamic subviews use a vector type matching the
@@ -36,6 +37,7 @@
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/Interfaces/MemorySlotInterfaces.h"
+#include "llvm/ADT/STLExtras.h"
using namespace mlir;
using namespace mlir::vector;
@@ -88,6 +90,40 @@ isPromotableTransfer(VectorTransferOpInterface xferOp, const MemorySlot &slot,
return true;
}
+/// Return offsets for an unmasked, in-bounds read of a fixed-size sub-vector
+/// of the slot. Dynamic subview slots hold the entire parent vector, not the
+/// runtime extent of the view, so containment in that vector is insufficient.
+static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
+ TransferReadOp readOp, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses) {
+ if (blockingUses.size() != 1 || (*blockingUses.begin())->get() != slot.ptr ||
+ readOp.getBase() != slot.ptr || !isa<MemRefType>(slot.ptr.getType()))
+ return failure();
+ if (readOp.getMask() || readOp.isMasked() || readOp.hasOutOfBoundsDim() ||
+ !readOp.getPermutationMap().isIdentity() ||
+ memref::isDynamicSubViewSlot(slot.ptr))
+ return failure();
+
+ auto slotType = dyn_cast<VectorType>(slot.valueType);
+ VectorType readType = readOp.getVectorType();
+ if (!slotType || slotType.isScalable() || readType.isScalable() ||
+ readType.getRank() != slotType.getRank() ||
+ readOp.getIndices().size() != static_cast<size_t>(readType.getRank()) ||
+ readType.getElementType() != slotType.getElementType())
+ return failure();
+
+ SmallVector<int64_t> offsets;
+ for (auto [index, size, extent] : llvm::zip_equal(
+ readOp.getIndices(), readType.getShape(), slotType.getShape())) {
+ auto offset = getConstantIntValue(index);
+ // Subtract first to avoid overflowing offset + size in the bounds check.
+ if (!offset || *offset < 0 || size > extent || *offset > extent - size)
+ return failure();
+ offsets.push_back(*offset);
+ }
+ return offsets;
+}
+
//===----------------------------------------------------------------------===//
// PromotableMemOpInterface models
//===----------------------------------------------------------------------===//
@@ -113,7 +149,9 @@ struct TransferReadOpMemOpModel
SmallVectorImpl<OpOperand *> &newBlockingUses,
const DataLayout &dataLayout) const {
return isPromotableTransfer(cast<VectorTransferOpInterface>(op), slot,
- blockingUses);
+ blockingUses) ||
+ succeeded(getPromotableReadSliceOffsets(cast<TransferReadOp>(op),
+ slot, blockingUses));
}
// Replace the read with the reaching definition, selecting padding for lanes
@@ -129,6 +167,16 @@ struct TransferReadOpMemOpModel
const DataLayout &dataLayout) const {
auto readOp = cast<vector::TransferReadOp>(op);
Location loc = op->getLoc();
+ if (readOp.getVectorType() != slot.valueType) {
+ auto offsets = getPromotableReadSliceOffsets(readOp, slot, blockingUses);
+ assert(succeeded(offsets) && "expected a promotable read slice");
+ SmallVector<int64_t> strides(offsets->size(), 1);
+ Value result = vector::ExtractStridedSliceOp::create(
+ builder, loc, reachingDefinition, *offsets,
+ readOp.getVectorType().getShape(), strides);
+ readOp.getVector().replaceAllUsesWith(result);
+ return DeletionKind::Delete;
+ }
Value mask = memref::buildDynamicSubViewMask(builder, loc, slot.ptr);
if (Value opMask = readOp.getMask())
mask = mask
diff --git a/mlir/test/Transforms/Mem2Reg/vector.mlir b/mlir/test/Transforms/Mem2Reg/vector.mlir
index 62a35c90d324e3..02e4a9f1ab778b 100644
--- a/mlir/test/Transforms/Mem2Reg/vector.mlir
+++ b/mlir/test/Transforms/Mem2Reg/vector.mlir
@@ -1,8 +1,7 @@
// 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.
+// Whole-buffer transfers and supported sub-vector reads can be promoted to a
+// single vector SSA value.
// CHECK-LABEL: func.func @whole_buffer_write_read
// CHECK-SAME: (%[[PAD:.*]]: f32)
@@ -690,3 +689,332 @@ func.func @negative_masked_write(%v: vector<8xf32>, %w: vector<8xf32>, %m: vecto
%r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<8xf32>
return %r : vector<8xf32>
}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_origin
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [0], sizes = [4], strides = [1]
+// CHECK: return %[[R]] : vector<4xf32>
+func.func @partial_read_origin(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_offset
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: return %[[R]] : vector<4xf32>
+func.func @partial_read_offset(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_at_end
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [4], sizes = [4], strides = [1]
+// CHECK: return %[[R]] : vector<4xf32>
+func.func @partial_read_at_end(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c4], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_2d
+// CHECK-SAME: (%[[V:.*]]: vector<4x8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [1, 2], sizes = [2, 4], strides = [1, 1]
+// CHECK: return %[[R]] : vector<2x4xf32>
+func.func @partial_read_2d(%v: vector<4x8xf32>, %pad: f32) -> vector<2x4xf32> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<4x8xf32>
+ vector.transfer_write %v, %a[%c0, %c0] {in_bounds = [true, true]} : vector<4x8xf32>, memref<4x8xf32>
+ %r = vector.transfer_read %a[%c1, %c2], %pad {in_bounds = [true, true]} : memref<4x8xf32>, vector<2x4xf32>
+ return %r : vector<2x4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_dynamic_index
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_dynamic_index(%v: vector<8xf32>, %pad: f32, %offset: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%offset], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_negative_index
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_negative_index(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c-1 = arith.constant -1 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c-1], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_exceeds_extent
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_exceeds_extent(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c6 = arith.constant 6 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c6], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_large_index
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_large_index(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c9223372036854775807 = arith.constant 9223372036854775807 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c9223372036854775807], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_unproven_bounds
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_unproven_bounds(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c2], %pad : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_masked
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_masked(%v: vector<8xf32>, %pad: f32, %mask: vector<4xi1>) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c2], %pad, %mask {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_transpose
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_transpose(%v: vector<4x8xf32>, %pad: f32) -> vector<2x4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<4x8xf32>
+ vector.transfer_write %v, %a[%c0, %c0] {in_bounds = [true, true]} : vector<4x8xf32>, memref<4x8xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {permutation_map = affine_map<(d0, d1) -> (d1, d0)>, in_bounds = [true, true]} : memref<4x8xf32>, vector<2x4xf32>
+ return %r : vector<2x4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_broadcast
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_broadcast(%v: vector<4x8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<4x8xf32>
+ vector.transfer_write %v, %a[%c0, %c0] {in_bounds = [true, true]} : vector<4x8xf32>, memref<4x8xf32>
+ %r = vector.transfer_read %a[%c0, %c0], %pad {permutation_map = affine_map<(d0, d1) -> (d1)>, in_bounds = [true]} : memref<4x8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A read projects the definition reaching that program point, not a later write.
+// CHECK-LABEL: func.func @partial_read_before_write
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK: %[[POISON:.*]] = ub.poison : vector<8xf32>
+// CHECK: %[[FIRST:.*]] = vector.extract_strided_slice %[[POISON]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[SECOND:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: return %[[FIRST]], %[[SECOND]]
+func.func @partial_read_before_write(%v: vector<8xf32>, %pad: f32) -> (vector<4xf32>, vector<4xf32>) {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ %r0 = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r1 = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r0, %r1 : vector<4xf32>, vector<4xf32>
+}
+
+// -----
+
+// Static aliases provide their own sub-vector; the read extracts within it.
+// CHECK-LABEL: func.func @partial_read_static_alias
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[ALIAS:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[ALIAS]] offsets = [1], sizes = [2], strides = [1]
+// CHECK: return %[[R]]
+func.func @partial_read_static_alias(%v: vector<8xf32>, %pad: f32) -> vector<2xf32> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %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[%c1], %pad {in_bounds = [true]} : memref<4xf32, strided<[1], offset: 2>>, vector<2xf32>
+ return %r : vector<2xf32>
+}
+
+// -----
+
+// Each slice reads the current loop-carried vector.
+// CHECK-LABEL: func.func @partial_read_loop
+// CHECK-NOT: memref.alloca
+// CHECK: %[[LOOP:.*]] = scf.for {{.*}} iter_args(%[[IT:.*]] = %{{.*}}) -> (vector<8xf32>)
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[IT]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[NEXT:.*]] = arith.addf %[[SLICE]], %[[SLICE]]
+// CHECK: %[[FULL:.*]] = vector.insert_strided_slice %[[NEXT]], %[[IT]] offsets = [2], strides = [1]
+// CHECK: scf.yield %[[FULL]]
+// CHECK: %[[RESULT:.*]] = vector.extract_strided_slice %[[LOOP]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: return %[[RESULT]]
+func.func @partial_read_loop(%v: vector<8xf32>, %pad: f32, %lb: index, %ub: index, %step: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ scf.for %iv = %lb to %ub step %step {
+ %whole = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<8xf32>
+ %part = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ %next = arith.addf %part, %part : vector<4xf32>
+ %full = vector.insert_strided_slice %next, %whole offsets = [2], strides = [1] : vector<4xf32> into vector<8xf32>
+ vector.transfer_write %full, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ }
+ %r = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_branch
+// CHECK-NOT: memref.alloca
+// CHECK: %[[DEF:.*]] = scf.if {{.*}} -> (vector<8xf32>)
+// CHECK: scf.yield %{{.*}} : vector<8xf32>
+// CHECK: scf.yield %{{.*}} : vector<8xf32>
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[DEF]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: return %[[R]]
+func.func @partial_read_branch(%a0: vector<8xf32>, %a1: vector<8xf32>, %pad: f32, %cond: i1) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ scf.if %cond {
+ vector.transfer_write %a0, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ } else {
+ vector.transfer_write %a1, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ }
+ %r = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// A dynamic subview's slot value represents its parent, not its runtime shape.
+// CHECK-LABEL: func.func @partial_read_dynamic_alias
+// CHECK: memref.alloca
+// CHECK: memref.subview
+// CHECK: vector.transfer_read
+func.func @partial_read_dynamic_alias(%v: vector<8xf32>, %pad: f32, %size: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[0] [%size] [1] : memref<8xf32> to memref<?xf32, strided<[1]>>
+ %r = vector.transfer_read %sv[%c0], %pad {in_bounds = [true]} : memref<?xf32, strided<[1]>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// Scalable transfers keep the whole-buffer requirement.
+// CHECK-LABEL: func.func @partial_read_scalable
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_scalable(%v: vector<[8]xf32>, %pad: f32) -> vector<[4]xf32> {
+ %c0 = arith.constant 0 : index
+ %c8 = arith.constant 8 : index
+ %scale = vector.vscale
+ %size = arith.muli %scale, %c8 : index
+ %a = memref.alloca(%size) : memref<?xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<[8]xf32>, memref<?xf32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<?xf32>, vector<[4]xf32>
+ return %r : vector<[4]xf32>
+}
+
+// -----
+
+// A slice may span lanes written through an alias and untouched parent lanes.
+// CHECK-LABEL: func.func @partial_read_after_alias_write
+// CHECK-SAME: (%[[INIT:.*]]: vector<8xf32>, %[[V:.*]]: vector<4xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK: %[[MERGED:.*]] = vector.insert_strided_slice %[[V]], %[[INIT]] offsets = [2], strides = [1]
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[MERGED]] offsets = [1], sizes = [4], strides = [1]
+// CHECK: return %[[R]]
+func.func @partial_read_after_alias_write(%init: vector<8xf32>, %v: vector<4xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : 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[%c1], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %r : vector<4xf32>
+}
>From 58bf91cd1d2262dc6932fce9e9709bb0fc87b835 Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Wed, 28 Oct 2026 13:45:09 +0800
Subject: [PATCH 2/6] [mlir][vector] Share Mem2Reg transfer slot access checks
---
.../Transforms/MemorySlotInterfacesImpl.cpp | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index 5422e041ceff61..85446807b6ff9c 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -46,6 +46,15 @@ using namespace mlir::vector;
// Utilities
//===----------------------------------------------------------------------===//
+/// Return whether the transfer's only blocking use is its memref slot base.
+static bool
+isTransferSlotAccess(VectorTransferOpInterface xferOp, const MemorySlot &slot,
+ const SmallPtrSetImpl<OpOperand *> &blockingUses) {
+ return blockingUses.size() == 1 &&
+ (*blockingUses.begin())->get() == slot.ptr &&
+ xferOp.getBase() == slot.ptr && isa<MemRefType>(slot.ptr.getType());
+}
+
/// Returns whether `xferOp` can be promoted to use or define `slot`'s vector
/// value. The transfer's memref base must be the slot pointer, its vector type
/// must equal `slot.valueType`, its indices must be zero, and its permutation
@@ -57,15 +66,7 @@ using namespace mlir::vector;
static bool
isPromotableTransfer(VectorTransferOpInterface xferOp, const MemorySlot &slot,
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
- // The transfer's memref base must be the slot pointer.
- if (blockingUses.size() != 1)
- return false;
- Value blockingUse = (*blockingUses.begin())->get();
- if (blockingUse != slot.ptr || xferOp.getBase() != slot.ptr)
- return false;
-
- // Reject the tensor form (already implied, since slot pointers are memrefs).
- if (!isa<MemRefType>(xferOp.getBase().getType()))
+ if (!isTransferSlotAccess(xferOp, slot, blockingUses))
return false;
// Exact type match pins rank/extents/element type/scalable dims.
@@ -96,8 +97,9 @@ isPromotableTransfer(VectorTransferOpInterface xferOp, const MemorySlot &slot,
static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
TransferReadOp readOp, const MemorySlot &slot,
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
- if (blockingUses.size() != 1 || (*blockingUses.begin())->get() != slot.ptr ||
- readOp.getBase() != slot.ptr || !isa<MemRefType>(slot.ptr.getType()))
+ if (!isTransferSlotAccess(
+ cast<VectorTransferOpInterface>(readOp.getOperation()), slot,
+ blockingUses))
return failure();
if (readOp.getMask() || readOp.isMasked() || readOp.hasOutOfBoundsDim() ||
!readOp.getPermutationMap().isIdentity() ||
>From a6cf92eade58cdaab36b2f40e8ff4a1afaa611f8 Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Wed, 28 Oct 2026 13:53:38 +0800
Subject: [PATCH 3/6] [mlir][vector] Promote masked sub-vector reads through
dynamic subviews
---
.../Transforms/MemorySlotInterfacesImpl.cpp | 39 +++--
mlir/test/Transforms/Mem2Reg/vector.mlir | 157 ++++++++++++++++--
2 files changed, 166 insertions(+), 30 deletions(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index 85446807b6ff9c..746fef2a40b75f 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -16,8 +16,7 @@
// a write defines its next value.
// Static subviews use a vector matching the subview's shape: reads extract that
// sub-vector from the parent, and writes insert it back into the parent.
-// Supported transfers through dynamic subviews use a vector type matching the
-// whole static parent, and the subview's slot holds that whole vector. Reads
+// Dynamic subview slots hold the whole static parent vector. Reads
// pad lanes outside the runtime extent, and writes preserve the previous value
// of those lanes. `arith.select` implements this behavior and also handles
// lanes disabled by a transfer's mask operand.
@@ -91,9 +90,9 @@ isPromotableTransfer(VectorTransferOpInterface xferOp, const MemorySlot &slot,
return true;
}
-/// Return offsets for an unmasked, in-bounds read of a fixed-size sub-vector
-/// of the slot. Dynamic subview slots hold the entire parent vector, not the
-/// runtime extent of the view, so containment in that vector is insufficient.
+/// Return constant offsets for a fixed-size read contained in the slot vector.
+/// Static slots must cover their memref extent. Dynamic subview slots hold the
+/// whole parent vector; reads use an extent mask to pad lanes outside the view.
static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
TransferReadOp readOp, const MemorySlot &slot,
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
@@ -101,9 +100,7 @@ static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
cast<VectorTransferOpInterface>(readOp.getOperation()), slot,
blockingUses))
return failure();
- if (readOp.getMask() || readOp.isMasked() || readOp.hasOutOfBoundsDim() ||
- !readOp.getPermutationMap().isIdentity() ||
- memref::isDynamicSubViewSlot(slot.ptr))
+ if (readOp.isMasked() || !readOp.getPermutationMap().isIdentity())
return failure();
auto slotType = dyn_cast<VectorType>(slot.valueType);
@@ -114,6 +111,12 @@ static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
readType.getElementType() != slotType.getElementType())
return failure();
+ auto memrefType = cast<MemRefType>(slot.ptr.getType());
+ if (!memref::isDynamicSubViewSlot(slot.ptr) &&
+ (!memrefType.hasStaticShape() ||
+ memrefType.getShape() != slotType.getShape()))
+ return failure();
+
SmallVector<int64_t> offsets;
for (auto [index, size, extent] : llvm::zip_equal(
readOp.getIndices(), readType.getShape(), slotType.getShape())) {
@@ -169,28 +172,30 @@ struct TransferReadOpMemOpModel
const DataLayout &dataLayout) const {
auto readOp = cast<vector::TransferReadOp>(op);
Location loc = op->getLoc();
+ Value result = reachingDefinition;
+ Value mask = memref::buildDynamicSubViewMask(builder, loc, slot.ptr);
if (readOp.getVectorType() != slot.valueType) {
auto offsets = getPromotableReadSliceOffsets(readOp, slot, blockingUses);
assert(succeeded(offsets) && "expected a promotable read slice");
SmallVector<int64_t> strides(offsets->size(), 1);
- Value result = vector::ExtractStridedSliceOp::create(
- builder, loc, reachingDefinition, *offsets,
- readOp.getVectorType().getShape(), strides);
- readOp.getVector().replaceAllUsesWith(result);
- return DeletionKind::Delete;
+ auto extractSlice = [&](Value value) -> Value {
+ return vector::ExtractStridedSliceOp::create(
+ builder, loc, value, *offsets, readOp.getVectorType().getShape(),
+ strides);
+ };
+ result = extractSlice(result);
+ if (mask)
+ mask = extractSlice(mask);
}
- Value mask = memref::buildDynamicSubViewMask(builder, loc, slot.ptr);
if (Value opMask = readOp.getMask())
mask = mask
? arith::AndIOp::create(builder, loc, mask, opMask).getResult()
: opMask;
- Value result = reachingDefinition;
if (mask) {
Value padSplat = vector::BroadcastOp::create(
builder, loc, readOp.getVectorType(), readOp.getPadding());
- result = arith::SelectOp::create(builder, loc, mask, reachingDefinition,
- padSplat);
+ result = arith::SelectOp::create(builder, loc, mask, result, padSplat);
}
readOp.getVector().replaceAllUsesWith(result);
return DeletionKind::Delete;
diff --git a/mlir/test/Transforms/Mem2Reg/vector.mlir b/mlir/test/Transforms/Mem2Reg/vector.mlir
index 02e4a9f1ab778b..bfed63b61ed099 100644
--- a/mlir/test/Transforms/Mem2Reg/vector.mlir
+++ b/mlir/test/Transforms/Mem2Reg/vector.mlir
@@ -819,25 +819,29 @@ func.func @partial_read_large_index(%v: vector<8xf32>, %pad: f32) -> vector<4xf3
// -----
-// CHECK-LABEL: func.func @partial_read_unproven_bounds
-// CHECK: memref.alloca
-// CHECK: vector.transfer_write
-// CHECK: vector.transfer_read
-func.func @partial_read_unproven_bounds(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+// CHECK-LABEL: func.func @partial_read_static_bounds
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: return %[[R]]
+func.func @partial_read_static_bounds(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
%a = memref.alloca() : memref<8xf32>
vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
- %r = vector.transfer_read %a[%c2], %pad : memref<8xf32>, vector<4xf32>
+ %r = vector.transfer_read %a[%c2], %pad : memref<8xf32>, vector<4xf32>
return %r : vector<4xf32>
}
// -----
// CHECK-LABEL: func.func @partial_read_masked
-// CHECK: memref.alloca
-// CHECK: vector.transfer_write
-// CHECK: vector.transfer_read
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>, %[[PAD:.*]]: f32, %[[MASK:.*]]: vector<4xi1>)
+// CHECK-NOT: memref.alloca
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[PADDING:.*]] = vector.broadcast %[[PAD]] : f32 to vector<4xf32>
+// CHECK: %[[R:.*]] = arith.select %[[MASK]], %[[SLICE]], %[[PADDING]] : vector<4xi1>, vector<4xf32>
+// CHECK: return %[[R]]
func.func @partial_read_masked(%v: vector<8xf32>, %pad: f32, %mask: vector<4xi1>) -> vector<4xf32> {
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
@@ -969,15 +973,21 @@ func.func @partial_read_branch(%a0: vector<8xf32>, %a1: vector<8xf32>, %pad: f32
// A dynamic subview's slot value represents its parent, not its runtime shape.
// CHECK-LABEL: func.func @partial_read_dynamic_alias
-// CHECK: memref.alloca
-// CHECK: memref.subview
-// CHECK: vector.transfer_read
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>, %[[PAD:.*]]: f32, %[[SIZE:.*]]: index)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[EXTENT:.*]] = vector.create_mask %[[SIZE]] : vector<8xi1>
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[V]] offsets = [0], sizes = [4], strides = [1]
+// CHECK: %[[MASK:.*]] = vector.extract_strided_slice %[[EXTENT]] offsets = [0], sizes = [4], strides = [1]
+// CHECK: %[[PADDING:.*]] = vector.broadcast %[[PAD]] : f32 to vector<4xf32>
+// CHECK: %[[R:.*]] = arith.select %[[MASK]], %[[SLICE]], %[[PADDING]] : vector<4xi1>, vector<4xf32>
+// CHECK: return %[[R]]
func.func @partial_read_dynamic_alias(%v: vector<8xf32>, %pad: f32, %size: index) -> vector<4xf32> {
%c0 = arith.constant 0 : index
%a = memref.alloca() : memref<8xf32>
vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
%sv = memref.subview %a[0] [%size] [1] : memref<8xf32> to memref<?xf32, strided<[1]>>
- %r = vector.transfer_read %sv[%c0], %pad {in_bounds = [true]} : memref<?xf32, strided<[1]>>, vector<4xf32>
+ %r = vector.transfer_read %sv[%c0], %pad : memref<?xf32, strided<[1]>>, vector<4xf32>
return %r : vector<4xf32>
}
@@ -1018,3 +1028,124 @@ func.func @partial_read_after_alias_write(%init: vector<8xf32>, %v: vector<4xf32
%r = vector.transfer_read %a[%c1], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
return %r : vector<4xf32>
}
+
+// -----
+
+// Static containment also proves bounds for a whole-slot read.
+// CHECK-LABEL: func.func @whole_read_static_bounds
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: vector.transfer
+// CHECK: return %[[V]]
+func.func @whole_read_static_bounds(%v: vector<8xf32>, %pad: f32) -> vector<8xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.transfer_read %a[%c0], %pad : memref<8xf32>, vector<8xf32>
+ return %r : vector<8xf32>
+}
+
+// -----
+
+// The view may end inside the slice; combine its extent with the read mask.
+// CHECK-LABEL: func.func @partial_read_dynamic_alias_masked
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>, %[[PAD:.*]]: f32, %[[SIZE:.*]]: index, %[[OPMASK:.*]]: vector<4xi1>)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[EXTENT:.*]] = vector.create_mask %[[SIZE]] : vector<8xi1>
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[SUBMASK:.*]] = vector.extract_strided_slice %[[EXTENT]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[MASK:.*]] = arith.andi %[[SUBMASK]], %[[OPMASK]] : vector<4xi1>
+// CHECK: %[[PADDING:.*]] = vector.broadcast %[[PAD]] : f32 to vector<4xf32>
+// CHECK: %[[R:.*]] = arith.select %[[MASK]], %[[SLICE]], %[[PADDING]] : vector<4xi1>, vector<4xf32>
+// CHECK: return %[[R]]
+func.func @partial_read_dynamic_alias_masked(%v: vector<8xf32>, %pad: f32, %size: index, %mask: vector<4xi1>) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[0] [%size] [1] : memref<8xf32> to memref<?xf32, strided<[1]>>
+ %r = vector.transfer_read %sv[%c2], %pad, %mask : memref<?xf32, strided<[1]>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// CHECK-LABEL: func.func @partial_read_dynamic_alias_2d
+// CHECK-SAME: (%[[V:.*]]: vector<4x8xf32>, %[[PAD:.*]]: f32, %[[SIZE:.*]]: index)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[C4:.*]] = arith.constant 4 : index
+// CHECK: %[[EXTENT:.*]] = vector.create_mask %[[C4]], %[[SIZE]] : vector<4x8xi1>
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[V]] offsets = [1, 2], sizes = [2, 4], strides = [1, 1]
+// CHECK: %[[MASK:.*]] = vector.extract_strided_slice %[[EXTENT]] offsets = [1, 2], sizes = [2, 4], strides = [1, 1]
+// CHECK: %[[PADDING:.*]] = vector.broadcast %[[PAD]] : f32 to vector<2x4xf32>
+// CHECK: %[[R:.*]] = arith.select %[[MASK]], %[[SLICE]], %[[PADDING]] : vector<2x4xi1>, vector<2x4xf32>
+// CHECK: return %[[R]]
+func.func @partial_read_dynamic_alias_2d(%v: vector<4x8xf32>, %pad: f32, %size: index) -> vector<2x4xf32> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<4x8xf32>
+ vector.transfer_write %v, %a[%c0, %c0] {in_bounds = [true, true]} : vector<4x8xf32>, memref<4x8xf32>
+ %sv = memref.subview %a[0, 0] [4, %size] [1, 1] : memref<4x8xf32> to memref<4x?xf32, strided<[8, 1]>>
+ %r = vector.transfer_read %sv[%c1, %c2], %pad : memref<4x?xf32, strided<[8, 1]>>, vector<2x4xf32>
+ return %r : vector<2x4xf32>
+}
+
+// -----
+
+// A constant-zero size still has a dynamic memref type and pads every lane.
+// CHECK-LABEL: func.func @partial_read_empty_dynamic_alias
+// CHECK-SAME: (%[[V:.*]]: vector<8xf32>, %[[PAD:.*]]: f32)
+// CHECK-NOT: memref.alloca
+// CHECK-NOT: memref.subview
+// CHECK: %[[ZERO:.*]] = arith.constant 0 : index
+// CHECK: %[[EXTENT:.*]] = vector.create_mask %[[ZERO]] : vector<8xi1>
+// CHECK: %[[SLICE:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[MASK:.*]] = vector.extract_strided_slice %[[EXTENT]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[PADDING:.*]] = vector.broadcast %[[PAD]] : f32 to vector<4xf32>
+// CHECK: %[[R:.*]] = arith.select %[[MASK]], %[[SLICE]], %[[PADDING]] : vector<4xi1>, vector<4xf32>
+// CHECK: return %[[R]]
+func.func @partial_read_empty_dynamic_alias(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[0] [%c0] [1] : memref<8xf32> to memref<?xf32, strided<[1]>>
+ %r = vector.transfer_read %sv[%c2], %pad : memref<?xf32, strided<[1]>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// Runtime padding does not allow extracting beyond the parent slot vector.
+// CHECK-LABEL: func.func @partial_read_dynamic_alias_exceeds_parent
+// CHECK: memref.alloca
+// CHECK: memref.subview
+// CHECK: vector.transfer_read
+func.func @partial_read_dynamic_alias_exceeds_parent(%v: vector<8xf32>, %pad: f32, %size: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c6 = arith.constant 6 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[0] [%size] [1] : memref<8xf32> to memref<?xf32, strided<[1]>>
+ %r = vector.transfer_read %sv[%c6], %pad : memref<?xf32, strided<[1]>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// Nonzero offsets in a dynamic subview are not supported by the alias model.
+// CHECK-LABEL: func.func @partial_read_offset_dynamic_alias
+// CHECK: memref.alloca
+// CHECK: memref.subview
+// CHECK: vector.transfer_read
+func.func @partial_read_offset_dynamic_alias(%v: vector<8xf32>, %pad: f32, %size: index) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %sv = memref.subview %a[2] [%size] [1] : memref<8xf32> to memref<?xf32, strided<[1], offset: 2>>
+ %r = vector.transfer_read %sv[%c0], %pad : memref<?xf32, strided<[1], offset: 2>>, vector<4xf32>
+ return %r : vector<4xf32>
+}
>From 1dda2f9aed9a554b1dfc7dbd9b4e683d45865288 Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Wed, 28 Oct 2026 14:01:19 +0800
Subject: [PATCH 4/6] [mlir][vector] Rely on verifier and region promotion
guarantees for read slices
---
.../Transforms/MemorySlotInterfacesImpl.cpp | 10 ++----
mlir/test/Transforms/Mem2Reg/vector.mlir | 32 +++++++++++++++++++
2 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index 746fef2a40b75f..c6d8f50e14be46 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -98,23 +98,19 @@ static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
if (!isTransferSlotAccess(
cast<VectorTransferOpInterface>(readOp.getOperation()), slot,
- blockingUses))
- return failure();
- if (readOp.isMasked() || !readOp.getPermutationMap().isIdentity())
+ blockingUses) ||
+ !readOp.getPermutationMap().isIdentity())
return failure();
auto slotType = dyn_cast<VectorType>(slot.valueType);
VectorType readType = readOp.getVectorType();
if (!slotType || slotType.isScalable() || readType.isScalable() ||
readType.getRank() != slotType.getRank() ||
- readOp.getIndices().size() != static_cast<size_t>(readType.getRank()) ||
readType.getElementType() != slotType.getElementType())
return failure();
- auto memrefType = cast<MemRefType>(slot.ptr.getType());
if (!memref::isDynamicSubViewSlot(slot.ptr) &&
- (!memrefType.hasStaticShape() ||
- memrefType.getShape() != slotType.getShape()))
+ readOp.getShapedType().getShape() != slotType.getShape())
return failure();
SmallVector<int64_t> offsets;
diff --git a/mlir/test/Transforms/Mem2Reg/vector.mlir b/mlir/test/Transforms/Mem2Reg/vector.mlir
index bfed63b61ed099..76ba0c6587e349 100644
--- a/mlir/test/Transforms/Mem2Reg/vector.mlir
+++ b/mlir/test/Transforms/Mem2Reg/vector.mlir
@@ -1149,3 +1149,35 @@ func.func @partial_read_offset_dynamic_alias(%v: vector<8xf32>, %pad: f32, %size
%r = vector.transfer_read %sv[%c0], %pad : memref<?xf32, strided<[1], offset: 2>>, vector<4xf32>
return %r : vector<4xf32>
}
+
+// -----
+
+// Mem2Reg cannot promote accesses through the vector.mask region.
+// CHECK-LABEL: func.func @partial_read_mask_region
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.mask
+// CHECK-SAME: vector.transfer_read
+func.func @partial_read_mask_region(%v: vector<8xf32>, %pad: f32, %mask: vector<4xi1>) -> vector<4xf32> {
+ %c0 = arith.constant 0 : index
+ %c2 = arith.constant 2 : index
+ %a = memref.alloca() : memref<8xf32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
+ %r = vector.mask %mask { vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32> } : vector<4xi1> -> vector<4xf32>
+ return %r : vector<4xf32>
+}
+
+// -----
+
+// Transfers can change element width, but extracting a slice cannot.
+// CHECK-LABEL: func.func @partial_read_element_width
+// CHECK: memref.alloca
+// CHECK: vector.transfer_write
+// CHECK: vector.transfer_read
+func.func @partial_read_element_width(%v: vector<8xi32>, %pad: i32) -> vector<4xi64> {
+ %c0 = arith.constant 0 : index
+ %a = memref.alloca() : memref<8xi32>
+ vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xi32>, memref<8xi32>
+ %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xi32>, vector<4xi64>
+ return %r : vector<4xi64>
+}
>From a97e37b42b95b7ebbeae51575b95e7d4e164a0a4 Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Thu, 29 Oct 2026 14:14:16 +0800
Subject: [PATCH 5/6] [mlir][vector] Clarify read slice requirements and
consolidate position tests
---
.../Transforms/MemorySlotInterfacesImpl.cpp | 18 +++++--
mlir/test/Transforms/Mem2Reg/vector.mlir | 48 ++++---------------
2 files changed, 23 insertions(+), 43 deletions(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index c6d8f50e14be46..2fe4e073e60999 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -12,8 +12,8 @@
// and definitions of a vector SSA value. The models for `memref.subview`
// live in the MemRef dialect.
//
-// A read uses the slot's current value or extracts an in-bounds sub-vector;
-// a write defines its next value.
+// A read uses the slot's current value or extracts a constant-offset sub-vector
+// from the slot vector; a write defines its next value.
// Static subviews use a vector matching the subview's shape: reads extract that
// sub-vector from the parent, and writes insert it back into the parent.
// Dynamic subview slots hold the whole static parent vector. Reads
@@ -90,9 +90,17 @@ isPromotableTransfer(VectorTransferOpInterface xferOp, const MemorySlot &slot,
return true;
}
-/// Return constant offsets for a fixed-size read contained in the slot vector.
-/// Static slots must cover their memref extent. Dynamic subview slots hold the
-/// whole parent vector; reads use an extent mask to pad lanes outside the view.
+/// Return offsets for a constant-offset sub-vector read from the slot vector.
+/// Requires:
+/// - The transfer's only blocking use is its memref slot base.
+/// - Read and slot vectors have fixed sizes, equal ranks and element types.
+/// - The permutation map is identity and indices are constant and nonnegative.
+/// - The read slice is contained in the slot vector.
+/// - The memref shape matches the slot vector, or it is a zero-offset,
+/// unit-stride, same-rank dynamic subview of a static parent.
+///
+/// For dynamic subviews, containment is in the whole parent vector, not the
+/// runtime extent. Reads use an extent mask to select padding outside the view.
static FailureOr<SmallVector<int64_t>> getPromotableReadSliceOffsets(
TransferReadOp readOp, const MemorySlot &slot,
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
diff --git a/mlir/test/Transforms/Mem2Reg/vector.mlir b/mlir/test/Transforms/Mem2Reg/vector.mlir
index 76ba0c6587e349..b742dcfccb0664 100644
--- a/mlir/test/Transforms/Mem2Reg/vector.mlir
+++ b/mlir/test/Transforms/Mem2Reg/vector.mlir
@@ -692,52 +692,24 @@ func.func @negative_masked_write(%v: vector<8xf32>, %w: vector<8xf32>, %m: vecto
// -----
-// CHECK-LABEL: func.func @partial_read_origin
+// CHECK-LABEL: func.func @partial_read_positions
// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
// CHECK-NOT: memref.alloca
// CHECK-NOT: vector.transfer
-// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [0], sizes = [4], strides = [1]
-// CHECK: return %[[R]] : vector<4xf32>
-func.func @partial_read_origin(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
- %c0 = arith.constant 0 : index
- %a = memref.alloca() : memref<8xf32>
- vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
- %r = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
- return %r : vector<4xf32>
-}
-
-// -----
-
-// CHECK-LABEL: func.func @partial_read_offset
-// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
-// CHECK-NOT: memref.alloca
-// CHECK-NOT: vector.transfer
-// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
-// CHECK: return %[[R]] : vector<4xf32>
-func.func @partial_read_offset(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
+// CHECK: %[[ORIGIN:.*]] = vector.extract_strided_slice %[[V]] offsets = [0], sizes = [4], strides = [1]
+// CHECK: %[[MIDDLE:.*]] = vector.extract_strided_slice %[[V]] offsets = [2], sizes = [4], strides = [1]
+// CHECK: %[[END:.*]] = vector.extract_strided_slice %[[V]] offsets = [4], sizes = [4], strides = [1]
+// CHECK: return %[[ORIGIN]], %[[MIDDLE]], %[[END]] : vector<4xf32>, vector<4xf32>, vector<4xf32>
+func.func @partial_read_positions(%v: vector<8xf32>, %pad: f32) -> (vector<4xf32>, vector<4xf32>, vector<4xf32>) {
%c0 = arith.constant 0 : index
%c2 = arith.constant 2 : index
- %a = memref.alloca() : memref<8xf32>
- vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
- %r = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
- return %r : vector<4xf32>
-}
-
-// -----
-
-// CHECK-LABEL: func.func @partial_read_at_end
-// CHECK-SAME: (%[[V:.*]]: vector<8xf32>,
-// CHECK-NOT: memref.alloca
-// CHECK-NOT: vector.transfer
-// CHECK: %[[R:.*]] = vector.extract_strided_slice %[[V]] offsets = [4], sizes = [4], strides = [1]
-// CHECK: return %[[R]] : vector<4xf32>
-func.func @partial_read_at_end(%v: vector<8xf32>, %pad: f32) -> vector<4xf32> {
- %c0 = arith.constant 0 : index
%c4 = arith.constant 4 : index
%a = memref.alloca() : memref<8xf32>
vector.transfer_write %v, %a[%c0] {in_bounds = [true]} : vector<8xf32>, memref<8xf32>
- %r = vector.transfer_read %a[%c4], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
- return %r : vector<4xf32>
+ %origin = vector.transfer_read %a[%c0], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ %middle = vector.transfer_read %a[%c2], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ %end = vector.transfer_read %a[%c4], %pad {in_bounds = [true]} : memref<8xf32>, vector<4xf32>
+ return %origin, %middle, %end : vector<4xf32>, vector<4xf32>, vector<4xf32>
}
// -----
>From f78b2504857fbb65b2b99bc1699d7b01da7cf673 Mon Sep 17 00:00:00 2001
From: meinie0826 <929449726 at qq.com>
Date: Thu, 29 Oct 2026 14:20:54 +0800
Subject: [PATCH 6/6] [mlir][vector] Remove redundant MemRef check from
transfer slot access
---
mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
index 2fe4e073e60999..3308a4c8d45a81 100644
--- a/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
+++ b/mlir/lib/Dialect/Vector/Transforms/MemorySlotInterfacesImpl.cpp
@@ -51,7 +51,7 @@ isTransferSlotAccess(VectorTransferOpInterface xferOp, const MemorySlot &slot,
const SmallPtrSetImpl<OpOperand *> &blockingUses) {
return blockingUses.size() == 1 &&
(*blockingUses.begin())->get() == slot.ptr &&
- xferOp.getBase() == slot.ptr && isa<MemRefType>(slot.ptr.getType());
+ xferOp.getBase() == slot.ptr;
}
/// Returns whether `xferOp` can be promoted to use or define `slot`'s vector
More information about the Mlir-commits
mailing list