[Mlir-commits] [mlir] [mlir][VectorToLLVM] emit `inbounds|nuw` GEP flags when lowering `vector.load/store` (PR #202118)
Federico Bruzzone
llvmlistbot at llvm.org
Thu Jun 11 02:15:09 PDT 2026
================
@@ -256,10 +256,19 @@ class VectorLoadStoreConversion : public ConvertOpToLLVMPattern<LoadOrStoreOp> {
"could not resolve alignment");
// Resolve address.
+ // Per vector.load/store spec, indices must be in-bounds (0 <= idx <
+ // dim_size). Emit inbounds|nuw so LLVM can apply no-wrap optimizations on
----------------
FedericoBruzzone wrote:
I apologize if this response took a little while to arrive.
Thanks again. I've been new to this world for about two weeks, and your comments are very helpful.
However, I'm a little confused.
The spec doesn't require in-bounds indices. It explicitly permits OOB reads (e.g., `vector.load %memref[%c0] : memref<7xf32>, vector<8xf32>`).
But I think it's worth separating two cases:
1. `inbounds` (and the implied nusw/nsw on the index arithmetic) reduce to a single assumption: the addressed element lies within the allocation.
The [GEP](https://llvm.org/docs/LangRef.html#getelementptr-instruction) only computes the start address, it doesn't access memory. The `inbounds` rules constrain the computed pointer, not the width of any later access through it (I think `inrange` attribute is for this case). With a valid memref descriptor, all the `nusw` rules are "discharged" by the in-bounds assumption via the [allocated-objects rules](https://llvm.org/docs/LangRef.html#allocated-objects). That is, the object size cannot exceed the largest signed value of the index type and objects cannot cross the address-space boundary.
This covers the vector spec's ["Explicit out-of-bound vector load"](https://mlir.llvm.org/docs/Dialects/Vector/#vectorload-vectorloadop), e.g., `vector.load %memref[%c0] : memref<7xf32>, vector<8xf32>`. The start address is in bounds, so the GEP flags are not violated (If I understood correctly). The OOB part is the plain `llvm.load` itself, that is already UB today, independently of this patch. The [Pointer Aliasing Rules](https://llvm.org/docs/LangRef.html#pointer-aliasing-rules) say: "any memory access must be done through a pointer value associated with an address range of the memory access, otherwise the behavior is undefined", where the address range of a `load <8 x float>` is all 32 bytes accessed.
Note the vector.load spec is careful to distinguish two failure modes: OOB elements have undefined values, and only the alignment clause triggers immediate undefined behavior, but the lowering to a plain load doesn't preserve that distinction. Again, it might be possibile that the current conversion already doesn't honor the documented OOB semantics, with or without the GEP flags (prob a pre-existing inconsistency between the spec and the lowering). Consider:
```mlir
%buf = memref.alloca() : memref<7xf32>
// fill the 7 elements
%v = vector.load %buf[%c0] : memref<7xf32>, vector<8xf32>
```
the vector spec essentially says: "Ehi, this op is valid and lane 0–6 contains your data, on lane 7 no assumptions should be made on the value". It's lowered to:
```llvm
%a = alloca [7 x float]
%v = load <8 x float>, ptr %a ; reads 32 byte from a 28-byte object
```
In LLVM IR this is **not** "the lane 7 is undefined": for the Pointer Aliasing Rules it is simply UB (if I understood correctly).
2. `nuw` needs strictly more than in-bounds (not the flag :'D). The `nuw` rules require every term of the offset computation to be non-negative when interpreted as unsigned, meaning non-negative indices and non-negative strides. This is a strictly stronger condition. Correct me if I'm wrong, but we can land `inbounds` while violating `nuw` if you get there by walking backwards (also in memref maybe). E.g., a flipped view (from `[0, 1000)` -> `[900, 1000)`):
```mlir
%flip = memref.reinterpret_cast %base to offset: [1000], sizes: [100], strides: [-1]
: memref<2000xf32> to memref<100xf32, strided<[-1], offset: 1000>>
%v = vector.load %flip[%c5] : memref<100xf32, strided<[-1], offset: 1000>>, vector<1xf32>
```
Index 5 is in `[0, 100)`. The addressed element is at `base + 1000 + 5*(−1) = base + 995` inside the allocation. `inbounds` is satisfied. `nsw` is satisfied. But `mul nuw 5, −1` (read as unsigned `−1` is `2^64 − 1`), so the product wraps (poison) and the load is UB (again, if I understood correctly). See the second bullet under the `nuw` flag explanation of [GEP docs](https://llvm.org/docs/LangRef.html#getelementptr-instruction).
That's to say, it's not correct to put a priori `nuw` here. But then the same goes for `memref`, am I missing something?
I think that we should clarify the `vector.load/vector.store` index semantics. E.g., requiring the start address to be in-bounds, and stating the non-negativity conditions under which lowerings may emit inbounds/nuw, mirroring the memref.load/memref.store wording.
https://github.com/llvm/llvm-project/pull/202118
More information about the Mlir-commits
mailing list