[mlir][vector] Negative transfer starts are treated as in-bounds: canonicalization sets in_bounds=true and VectorToSCF guards only the upper bound
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
`vector.transfer_read`/`transfer_write` document that with `in_bounds = false`
the accesses, **including the starting point**, may run out of bounds. Two
independent paths assume only the *upper* end can overrun, so a negative
starting index is treated as in-bounds.
**1. The canonicalizer asserts a safety attribute that is false.**
`isInBounds` (`mlir/lib/Dialect/Vector/IR/VectorOps.cpp:5391`) ends with:
```c++
int64_t sourceSize = op.getShapedType().getDimSize(indicesIdx);
int64_t vectorSize = op.getVectorType().getDimSize(resultIdx);
return cstOp.value() + vectorSize <= sourceSize;
```
There is no `cstOp.value() >= 0` conjunct. For a start of `-1` into a
`memref<4xf32>` with `vector<3xf32>`, this computes `-1 + 3 = 2 <= 4` and
returns true, so `foldTransferInBoundsAttribute` rewrites `in_bounds = [false]`
to `[true]` under plain `-canonicalize`.
**2. `VectorToSCF` guards only the upper bound.** `generateInBoundsCheck`
(`mlir/lib/Conversion/VectorToSCF/VectorToSCF.cpp`) emits
```mlir
%3 = arith.cmpi slt, %2, %c4 : index
scf.if %3 { ... } else { ... }
```
with no `%2 >= 0` conjunct, so a negative index enters the `then` branch and
performs a scalar access below the allocation instead of padding the read or
suppressing the write.
Both reproduce at `31a1c03fe67c`.
## Reproducers
### Canonicalization flips the attribute
```mlir
func.func @negative_start(%a: memref<4xf32>) -> vector<3xf32> {
%cm1 = arith.constant -1 : index
%pad = arith.constant -42.0 : f32
%v = vector.transfer_read %a[%cm1], %pad {in_bounds = [false]}
: memref<4xf32>, vector<3xf32>
return %v : vector<3xf32>
}
```
```
mlir-opt -canonicalize repro.mlir
```
```mlir
%0 = vector.transfer_read %arg0[%c-1], %cst {in_bounds = [true]} : memref<4xf32>, vector<3xf32>
```
The transfer starts one element *below* the buffer, and is now marked as
guaranteed in-bounds. Any consumer that trusts the attribute will skip the
bounds handling this transfer requires.
### The lowered guard misses the lower bound
```mlir
func.func @neg2d(%a: memref<4x8xf32>, %i: index) -> vector<2x8xf32> {
%pad = arith.constant -42.0 : f32
%v = vector.transfer_read %a[%i, %i], %pad {in_bounds = [false, true]}
: memref<4x8xf32>, vector<2x8xf32>
return %v : vector<2x8xf32>
}
```
```
mlir-opt -convert-vector-to-scf repro2.mlir
```
```mlir
scf.for %arg2 = %c0 to %c2 step %c1 {
%2 = affine.apply #map(%arg2)[%arg1]
%3 = arith.cmpi slt, %2, %c4 : index // upper bound only
scf.if %3 {
%5 = vector.transfer_read %arg0[%4, %arg1], %cst_0 {in_bounds = [true]} ...
```
For a negative `%i` the condition holds and the guarded access runs below the
source.
## Why this is a bug and not intended behaviour
The op documentation is explicit that the starting point is covered:
> For every vector dimension, the boolean array attribute `in_bounds` specifies
> if the transfer is guaranteed to be within the source bounds. If set to
> "false", accesses (**including the starting point**) may run out-of-bounds
> along the respective vector dimension as the index increases.
So a negative start is a legal input for `in_bounds = false`, and both the
`in_bounds` inference and the lowered guard must establish `start >= 0` before
treating the access as safe.
## Environment
* llvm-project at `31a1c03fe67c`
* Release build with assertions, Linux x86-64
Contributor guide
Research direction
Start with isInBounds and foldTransferInBoundsAttribute in mlir/lib/Dialect/Vector/IR/VectorOps.cpp, then inspect generateInBoundsCheck in mlir/lib/Conversion/VectorToSCF/VectorToSCF.cpp. Run the two supplied mlir-opt reproducers first. Done means negative transfer starts are not marked in-bounds and lowered reads or writes do not access below the source bounds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100