[llvm] [mlir] [MLIR][SPIRV] Split vector accesses under the Logical addressing model (PR #214173)
via llvm-commits
llvm-commits at lists.llvm.org
Wed Aug 5 02:51:26 PDT 2026
https://github.com/LouisLu060211 created https://github.com/llvm/llvm-project/pull/214173
`VectorLoadOpConverter` and `VectorStoreOpConverter` cast the element pointer returned by the access chain into a pointer to the vector type, so the whole vector can be moved in one step. Under the Logical addressing model pointers are opaque and can only be derived through `OpAccessChain`, so the resulting module is rejected by spirv-val even though the conversion itself succeeds.
Take that path only when the target allows pointer bitcasts. Otherwise address each element through its own access chain and assemble or disassemble the vector with `OpCompositeConstruct` and `OpCompositeExtract`.
The alignment on the original op is dropped on the split path, since it describes the vector access rather than the element accesses. Dropping an alignment claim is always valid, and each element access is naturally aligned.
Fixes #213192
>From 4126fb29ae14cf87117d8af8a36899b92c4f86fe Mon Sep 17 00:00:00 2001
From: LouisLu0602 <yaolu0602 at gmail.com>
Date: Wed, 5 Aug 2026 17:49:52 +0800
Subject: [PATCH] [MLIR][SPIRV] Split vector accesses under the Logical
addressing model
`VectorLoadOpConverter` and `VectorStoreOpConverter` cast the element pointer
returned by the access chain into a pointer to the vector type, so the whole
vector can be moved in one step. Under the Logical addressing model pointers are
opaque and can only be derived through `OpAccessChain`, so the resulting module
is rejected by spirv-val even though the conversion itself succeeds.
Take that path only when the target allows pointer bitcasts. Otherwise address
each element through its own access chain and assemble or disassemble the vector
with `OpCompositeConstruct` and `OpCompositeExtract`.
The alignment on the original op is dropped on the split path, since it
describes the vector access rather than the element accesses. Dropping an
alignment claim is always valid, and each element access is naturally aligned.
Fixes #213192
---
%1 | 0
.../VectorToSPIRV/VectorToSPIRV.cpp | 131 +++++++++++++++---
.../VectorToSPIRV/vector-to-spirv.mlir | 67 ++++++---
3 files changed, 157 insertions(+), 41 deletions(-)
create mode 100644 %1
diff --git a/%1 b/%1
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/mlir/lib/Conversion/VectorToSPIRV/VectorToSPIRV.cpp b/mlir/lib/Conversion/VectorToSPIRV/VectorToSPIRV.cpp
index 78693e924c4d9..d1f6ed767fe14 100644
--- a/mlir/lib/Conversion/VectorToSPIRV/VectorToSPIRV.cpp
+++ b/mlir/lib/Conversion/VectorToSPIRV/VectorToSPIRV.cpp
@@ -727,6 +727,33 @@ struct VectorDeinterleaveOpConvert final
}
};
+/// Returns true if a pointer `OpBitcast` is valid for the target. Under the
+/// Logical addressing model pointers are opaque and can only be derived through
+/// `OpAccessChain`, so a pointer to the element type cannot be cast to a
+/// pointer to the vector type.
+static bool allowsPointerBitcast(const SPIRVTypeConverter &typeConverter) {
+ return typeConverter.allows(spirv::Capability::Addresses) ||
+ typeConverter.allows(
+ spirv::Capability::PhysicalStorageBufferAddresses);
+}
+
+/// Returns `indices` with the innermost index advanced by `offset`, so that the
+/// consecutive elements covered by a vector access can be addressed one at a
+/// time.
+static SmallVector<Value> offsetInnermostIndex(ValueRange indices,
+ int64_t offset, Location loc,
+ OpBuilder &builder) {
+ SmallVector<Value> result(indices.begin(), indices.end());
+ if (offset == 0 || result.empty())
+ return result;
+ Value innermost = result.back();
+ Value offsetValue = spirv::ConstantOp::create(
+ builder, loc, innermost.getType(),
+ builder.getIntegerAttr(innermost.getType(), offset));
+ result.back() = spirv::IAddOp::create(builder, loc, innermost, offsetValue);
+ return result;
+}
+
struct VectorLoadOpConverter final
: public OpConversionPattern<vector::LoadOp> {
using Base::Base;
@@ -743,12 +770,6 @@ struct VectorLoadOpConverter final
const auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
auto loc = loadOp.getLoc();
- Value accessChain =
- spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
- adaptor.getIndices(), loc, rewriter);
- if (!accessChain)
- return rewriter.notifyMatchFailure(
- loadOp, "failed to get memref element pointer");
spirv::StorageClass storageClass = attr.getValue();
auto vectorType = loadOp.getVectorType();
@@ -758,8 +779,6 @@ struct VectorLoadOpConverter final
if (!spirvVectorType)
return rewriter.notifyMatchFailure(loadOp, "unsupported vector type");
- auto vectorPtrType = spirv::PointerType::get(spirvVectorType, storageClass);
-
std::optional<uint64_t> alignment = loadOp.getAlignment();
if (alignment > std::numeric_limits<uint32_t>::max()) {
return rewriter.notifyMatchFailure(loadOp,
@@ -776,14 +795,56 @@ struct VectorLoadOpConverter final
alignmentAttr = rewriter.getI32IntegerAttr(alignment.value());
}
+ int64_t numElements = vectorType.getNumElements();
+
+ // Without a pointer bitcast the vector cannot be loaded in one step. Load
+ // each element through its own access chain and assemble the vector. The
+ // alignment is dropped because it describes the vector access, not the
+ // individual element accesses.
+ if (numElements != 1 && !allowsPointerBitcast(typeConverter)) {
+ if (memrefType.getRank() == 0)
+ return rewriter.notifyMatchFailure(
+ loadOp, "cannot split a vector load from a rank-0 memref");
+ auto convertedVectorType = dyn_cast<VectorType>(spirvVectorType);
+ if (!convertedVectorType)
+ return rewriter.notifyMatchFailure(loadOp, "unsupported vector type");
+
+ Type elementType = convertedVectorType.getElementType();
+ SmallVector<Value> elements;
+ elements.reserve(numElements);
+ for (int64_t i = 0; i < numElements; ++i) {
+ SmallVector<Value> elementIndices =
+ offsetInnermostIndex(adaptor.getIndices(), i, loc, rewriter);
+ Value elementPtr =
+ spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
+ elementIndices, loc, rewriter);
+ if (!elementPtr)
+ return rewriter.notifyMatchFailure(
+ loadOp, "failed to get memref element pointer");
+ elements.push_back(
+ spirv::LoadOp::create(rewriter, loc, elementType, elementPtr));
+ }
+ rewriter.replaceOpWithNewOp<spirv::CompositeConstructOp>(
+ loadOp, spirvVectorType, elements);
+ return success();
+ }
+
+ Value accessChain =
+ spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
+ adaptor.getIndices(), loc, rewriter);
+ if (!accessChain)
+ return rewriter.notifyMatchFailure(
+ loadOp, "failed to get memref element pointer");
+
+ auto vectorPtrType = spirv::PointerType::get(spirvVectorType, storageClass);
+
// For single element vectors, we don't need to bitcast the access chain to
// the original vector type. Both is going to be the same, a pointer
// to a scalar.
Value castedAccessChain =
- (vectorType.getNumElements() == 1)
- ? accessChain
- : spirv::BitcastOp::create(rewriter, loc, vectorPtrType,
- accessChain);
+ (numElements == 1) ? accessChain
+ : spirv::BitcastOp::create(
+ rewriter, loc, vectorPtrType, accessChain);
rewriter.replaceOpWithNewOp<spirv::LoadOp>(loadOp, spirvVectorType,
castedAccessChain,
@@ -809,12 +870,6 @@ struct VectorStoreOpConverter final
const auto &typeConverter = *getTypeConverter<SPIRVTypeConverter>();
auto loc = storeOp.getLoc();
- Value accessChain =
- spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
- adaptor.getIndices(), loc, rewriter);
- if (!accessChain)
- return rewriter.notifyMatchFailure(
- storeOp, "failed to get memref element pointer");
std::optional<uint64_t> alignment = storeOp.getAlignment();
if (alignment > std::numeric_limits<uint32_t>::max()) {
@@ -830,16 +885,48 @@ struct VectorStoreOpConverter final
if (!spirvVectorType)
return rewriter.notifyMatchFailure(storeOp, "unsupported vector type");
+ int64_t numElements = vectorType.getNumElements();
+
+ // Mirror of the load path: without a pointer bitcast each element has to be
+ // extracted and stored through its own access chain.
+ if (numElements != 1 && !allowsPointerBitcast(typeConverter)) {
+ if (memrefType.getRank() == 0)
+ return rewriter.notifyMatchFailure(
+ storeOp, "cannot split a vector store to a rank-0 memref");
+ for (int64_t i = 0; i < numElements; ++i) {
+ SmallVector<Value> elementIndices =
+ offsetInnermostIndex(adaptor.getIndices(), i, loc, rewriter);
+ Value elementPtr =
+ spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
+ elementIndices, loc, rewriter);
+ if (!elementPtr)
+ return rewriter.notifyMatchFailure(
+ storeOp, "failed to get memref element pointer");
+ Value element = spirv::CompositeExtractOp::create(
+ rewriter, loc, adaptor.getValueToStore(),
+ ArrayRef<int32_t>{static_cast<int32_t>(i)});
+ spirv::StoreOp::create(rewriter, loc, elementPtr, element);
+ }
+ rewriter.eraseOp(storeOp);
+ return success();
+ }
+
+ Value accessChain =
+ spirv::getElementPtr(typeConverter, memrefType, adaptor.getBase(),
+ adaptor.getIndices(), loc, rewriter);
+ if (!accessChain)
+ return rewriter.notifyMatchFailure(
+ storeOp, "failed to get memref element pointer");
+
auto vectorPtrType = spirv::PointerType::get(spirvVectorType, storageClass);
// For single element vectors, we don't need to bitcast the access chain to
// the original vector type. Both is going to be the same, a pointer
// to a scalar.
Value castedAccessChain =
- (vectorType.getNumElements() == 1)
- ? accessChain
- : spirv::BitcastOp::create(rewriter, loc, vectorPtrType,
- accessChain);
+ (numElements == 1) ? accessChain
+ : spirv::BitcastOp::create(
+ rewriter, loc, vectorPtrType, accessChain);
auto memoryAccess = spirv::MemoryAccess::None;
spirv::MemoryAccessAttr memoryAccessAttr;
diff --git a/mlir/test/Conversion/VectorToSPIRV/vector-to-spirv.mlir b/mlir/test/Conversion/VectorToSPIRV/vector-to-spirv.mlir
index f904dd9d35c37..10a7027dd15db 100644
--- a/mlir/test/Conversion/VectorToSPIRV/vector-to-spirv.mlir
+++ b/mlir/test/Conversion/VectorToSPIRV/vector-to-spirv.mlir
@@ -1114,12 +1114,9 @@ module attributes {
// CHECK: %[[S0:.+]] = builtin.unrealized_conversion_cast %[[ARG0]] : memref<4xf32, #spirv.storage_class<StorageBuffer>> to !spirv.ptr<!spirv.struct<(!spirv.array<4 x f32, stride=4> [0])>, StorageBuffer>
// CHECK: %[[C0:.+]] = arith.constant 0 : index
// CHECK: %[[S1:.+]] = builtin.unrealized_conversion_cast %[[C0]] : index to i32
-// CHECK: %[[CST1:.+]] = spirv.Constant 0 : i32
-// CHECK: %[[CST2:.+]] = spirv.Constant 0 : i32
-// CHECK: %[[CST3:.+]] = spirv.Constant 1 : i32
-// CHECK: %[[S4:.+]] = spirv.AccessChain %[[S0]][%[[CST1]], %[[S1]]] : !spirv.ptr<!spirv.struct<(!spirv.array<4 x f32, stride=4> [0])>, StorageBuffer>, i32, i32
-// CHECK: %[[S5:.+]] = spirv.Bitcast %[[S4]] : !spirv.ptr<f32, StorageBuffer> to !spirv.ptr<vector<4xf32>, StorageBuffer>
-// CHECK: %[[R0:.+]] = spirv.Load "StorageBuffer" %[[S5]] : vector<4xf32>
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.Load "StorageBuffer" %{{.+}} : f32
+// CHECK: %[[R0:.+]] = spirv.CompositeConstruct %{{.+}}, %{{.+}}, %{{.+}}, %{{.+}} : (f32, f32, f32, f32) -> vector<4xf32>
// CHECK: return %[[R0]] : vector<4xf32>
func.func @vector_load(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer>>) -> vector<4xf32> {
%idx = arith.constant 0 : index
@@ -1151,8 +1148,10 @@ func.func @vector_load_single_elem(%arg0 : memref<4xf32, #spirv.storage_class<St
// CHECK-LABEL: @vector_load_aligned
func.func @vector_load_aligned(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer>>) -> vector<4xf32> {
%idx = arith.constant 0 : index
- // CHECK: spirv.Load
- // CHECK-SAME: ["Aligned", 8]
+ // The alignment describes the vector access and does not carry over to the
+ // element accesses used under the Logical addressing model.
+ // CHECK-COUNT-4: spirv.Load "StorageBuffer" %{{.+}} : f32
+ // CHECK: spirv.CompositeConstruct
%0 = vector.load %arg0[%idx] { alignment = 8 } : memref<4xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
return %0: vector<4xf32>
}
@@ -1171,8 +1170,9 @@ func.func @vector_load_aligned(%arg0 : memref<4xf32, #spirv.storage_class<Storag
// CHECK: %[[CST1:.+]] = spirv.Constant 1 : i32
// CHECK: %[[S6:.+]] = spirv.IAdd %[[S2]], %[[S3]] : i32
// CHECK: %[[S7:.+]] = spirv.AccessChain %[[S0]][%[[CST0_1]], %[[S6]]] : !spirv.ptr<!spirv.struct<(!spirv.array<16 x f32, stride=4> [0])>, StorageBuffer>, i32, i32
-// CHECK: %[[S8:.+]] = spirv.Bitcast %[[S7]] : !spirv.ptr<f32, StorageBuffer> to !spirv.ptr<vector<4xf32>, StorageBuffer>
-// CHECK: %[[R0:.+]] = spirv.Load "StorageBuffer" %[[S8]] : vector<4xf32>
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.Load "StorageBuffer" %{{.+}} : f32
+// CHECK: %[[R0:.+]] = spirv.CompositeConstruct %{{.+}}, %{{.+}}, %{{.+}}, %{{.+}} : (f32, f32, f32, f32) -> vector<4xf32>
// CHECK: return %[[R0]] : vector<4xf32>
func.func @vector_load_2d(%arg0 : memref<4x4xf32, #spirv.storage_class<StorageBuffer>>) -> vector<4xf32> {
%idx_0 = arith.constant 0 : index
@@ -1190,9 +1190,8 @@ func.func @vector_load_2d(%arg0 : memref<4x4xf32, #spirv.storage_class<StorageBu
// CHECK: %[[CST1:.+]] = spirv.Constant 0 : i32
// CHECK: %[[CST2:.+]] = spirv.Constant 0 : i32
// CHECK: %[[CST3:.+]] = spirv.Constant 1 : i32
-// CHECK: %[[S4:.+]] = spirv.AccessChain %[[S0]][%[[CST1]], %[[S1]]] : !spirv.ptr<!spirv.struct<(!spirv.array<4 x f32, stride=4> [0])>, StorageBuffer>, i32, i32
-// CHECK: %[[S5:.+]] = spirv.Bitcast %[[S4]] : !spirv.ptr<f32, StorageBuffer> to !spirv.ptr<vector<4xf32>, StorageBuffer>
-// CHECK: spirv.Store "StorageBuffer" %[[S5]], %[[ARG1]] : vector<4xf32>
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.CompositeExtract %[[ARG1]]
func.func @vector_store(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer>>, %arg1 : vector<4xf32>) {
%idx = arith.constant 0 : index
vector.store %arg1, %arg0[%idx] : memref<4xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
@@ -1202,8 +1201,9 @@ func.func @vector_store(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer
// CHECK-LABEL: @vector_store_aligned
func.func @vector_store_aligned(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer>>, %arg1 : vector<4xf32>) {
%idx = arith.constant 0 : index
- // CHECK: spirv.Store
- // CHECK-SAME: ["Aligned", 8]
+ // The alignment describes the vector access and does not carry over to the
+ // element accesses used under the Logical addressing model.
+ // CHECK-COUNT-4: spirv.Store "StorageBuffer" %{{.+}}, %{{.+}} : f32
vector.store %arg1, %arg0[%idx] { alignment = 8 } : memref<4xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
return
}
@@ -1241,8 +1241,8 @@ func.func @vector_store_single_elem(%arg0 : memref<4xf32, #spirv.storage_class<S
// CHECK: %[[CST1:.+]] = spirv.Constant 1 : i32
// CHECK: %[[S6:.+]] = spirv.IAdd %[[S2]], %[[S3]] : i32
// CHECK: %[[S7:.+]] = spirv.AccessChain %[[S0]][%[[CST0_1]], %[[S6]]] : !spirv.ptr<!spirv.struct<(!spirv.array<16 x f32, stride=4> [0])>, StorageBuffer>, i32, i32
-// CHECK: %[[S8:.+]] = spirv.Bitcast %[[S7]] : !spirv.ptr<f32, StorageBuffer> to !spirv.ptr<vector<4xf32>, StorageBuffer>
-// CHECK: spirv.Store "StorageBuffer" %[[S8]], %[[ARG1]] : vector<4xf32>
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.CompositeExtract %[[ARG1]]
func.func @vector_store_2d(%arg0 : memref<4x4xf32, #spirv.storage_class<StorageBuffer>>, %arg1 : vector<4xf32>) {
%idx_0 = arith.constant 0 : index
%idx_1 = arith.constant 1 : index
@@ -1255,8 +1255,8 @@ func.func @vector_store_2d(%arg0 : memref<4x4xf32, #spirv.storage_class<StorageB
// CHECK-SAME: %[[ARG1:.*]]: vector<4xindex>
// CHECK: %[[S0:.+]] = builtin.unrealized_conversion_cast %[[ARG0]] : memref<4xindex, #spirv.storage_class<StorageBuffer>> to !spirv.ptr<!spirv.struct<(!spirv.array<4 x i32, stride=4> [0])>, StorageBuffer>
// CHECK: %[[S1:.+]] = builtin.unrealized_conversion_cast %[[ARG1]] : vector<4xindex> to vector<4xi32>
-// CHECK: %[[S5:.+]] = spirv.Bitcast %{{.+}} : !spirv.ptr<i32, StorageBuffer> to !spirv.ptr<vector<4xi32>, StorageBuffer>
-// CHECK: spirv.Store "StorageBuffer" %[[S5]], %[[S1]] : vector<4xi32>
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.CompositeExtract %[[S1]]
func.func @vector_store_index(%arg0 : memref<4xindex, #spirv.storage_class<StorageBuffer>>, %arg1 : vector<4xindex>) {
%idx = arith.constant 0 : index
vector.store %arg1, %arg0[%idx] : memref<4xindex, #spirv.storage_class<StorageBuffer>>, vector<4xindex>
@@ -1276,3 +1276,32 @@ func.func @vector_load(%arg0 : memref<4xf32, #spirv.storage_class<StorageBuffer>
%0 = vector.load %arg0[%idx] : memref<4xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
return %0: vector<4xf32>
}
+
+// -----
+
+// Under the Logical addressing model a pointer cannot be bitcast, so a vector
+// access has to be split into element accesses. Emitting the bitcast produced a
+// module that spirv-val rejected.
+// See https://github.com/llvm/llvm-project/issues/213192.
+
+module attributes {
+ spirv.target_env = #spirv.target_env
+ #spirv.vce<v1.0, [Shader], [SPV_KHR_storage_buffer_storage_class]>,
+ #spirv.resource_limits<>>
+} {
+
+// CHECK-LABEL: @vector_load_store_logical_no_pointer_bitcast
+// CHECK-NOT: spirv.Bitcast
+// CHECK-COUNT-4: spirv.Load "StorageBuffer" %{{.+}} : f32
+// CHECK: spirv.CompositeConstruct
+// CHECK-COUNT-4: spirv.CompositeExtract
+func.func @vector_load_store_logical_no_pointer_bitcast(
+ %src : memref<16xf32, #spirv.storage_class<StorageBuffer>>,
+ %dst : memref<16xf32, #spirv.storage_class<StorageBuffer>>) {
+ %idx = arith.constant 0 : index
+ %0 = vector.load %src[%idx] : memref<16xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
+ vector.store %0, %dst[%idx] : memref<16xf32, #spirv.storage_class<StorageBuffer>>, vector<4xf32>
+ return
+}
+
+} // end module
More information about the llvm-commits
mailing list