[MLIR][Affine] affine-loop-fusion generates out-of-bounds loads from a private buffer
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Applying producer/maximal `affine-loop-fusion` to the verifier-valid program below generates a verifier-valid target containing out-of-bounds loads from a private buffer.
Without fusion, the consumer loads `%comm[%j]` from a `memref<32xf64>`. After fusion, the pass creates a `memref<1xf64>` private buffer and rewrites the same consumer access to load `%alloc[%j mod 2]`.
Over the consumer domain `0 <= j < 16`, the generated index set is `{0, 1}`, but the only valid index of `memref<1xf64>` is `0`.
Public `mlir-opt` reproducer:
https://godbolt.org/z/W5fh18zsW
## Version
Tested at `llvm/llvm-project` commit: `450cf0aafeab9fed6666b112b018d3913a36527c`
LLVM version: LLVM 24.0.0git
Release builds with assertions enabled and disabled generated the same transformed IR.
## Reproducer
```mlir
module {
func.func @domain(
%in: memref<32xf64>,
%comm: memref<32xf64>,
%out: memref<32xf64>) {
affine.for %i = 0 to 8 {
%a = affine.load %in[%i] : memref<32xf64>
%b = arith.addf %a, %a : f64
affine.store %b, %comm[2 * %i] : memref<32xf64>
}
affine.for %j = 0 to 16 {
%c = affine.load %comm[%j] : memref<32xf64>
%d = arith.addf %c, %c : f64
affine.store %d, %out[%j] : memref<32xf64>
}
return
}
}
```
## Commands
```sh
# Baseline.
mlir-opt input.mlir \
-o baseline.mlir
# Apply producer/maximal affine loop fusion to the same input.
mlir-opt input.mlir \
--pass-pipeline='builtin.module(func.func(affine-loop-fusion{mode=producer maximal}))' \
-o fused.mlir
# The generated target also passes MLIR verification.
mlir-opt fused.mlir \
-o /dev/null
```
## Actual behavior
Without `affine-loop-fusion`, the consumer access remains:
```mlir
affine.for %j = 0 to 16 {
%c = affine.load %comm[%j] : memref<32xf64>
// ...
}
```
All values of `%j` are within the bounds of `memref<32xf64>`.
With `affine-loop-fusion`, the generated function contains:
```mlir
#map = affine_map<(d0) -> (d0 floordiv 2)>
#map1 = affine_map<(d0) -> (d0 * 2)>
module {
func.func @domain(
%arg0: memref<32xf64>,
%arg1: memref<32xf64>,
%arg2: memref<32xf64>) {
%alloc = memref.alloc() : memref<1xf64>
affine.for %arg3 = 0 to 8 {
%0 = affine.load %arg0[%arg3] : memref<32xf64>
%1 = arith.addf %0, %0 : f64
affine.store %1, %arg1[%arg3 * 2] : memref<32xf64>
}
affine.for %arg3 = 0 to 16 {
%0 = affine.apply #map(%arg3)
%1 = affine.load %arg0[%0] : memref<32xf64>
%2 = arith.addf %1, %1 : f64
%3 = affine.apply #map1(%0)
affine.store %2, %alloc[0] : memref<1xf64>
%4 = affine.load %alloc[%arg3 mod 2] : memref<1xf64>
%5 = arith.addf %4, %4 : f64
affine.store %5, %arg2[%arg3] : memref<32xf64>
}
return
}
}
```
The relevant transformation is therefore:
```text
without affine-loop-fusion:
affine.load %comm[%j] : memref<32xf64>
with affine-loop-fusion:
%alloc = memref.alloc() : memref<1xf64>
affine.load %alloc[%j mod 2] : memref<1xf64>
```
The valid index range of `memref<1xf64>` is:
```text
[0, 1)
```
Over `0 <= j < 16`, the generated index `j mod 2` has the exact value set:
```text
{0, 1}
```
It evaluates to `1` for:
```text
j = 1, 3, 5, 7, 9, 11, 13, 15
```
Every one of those iterations performs an out-of-bounds load from the generated private buffer.
This is a static property of the transformed IR. Because the target contains out-of-bounds memory accesses, no target runtime value is used as the semantic oracle for this report.
## Expected behavior
`affine-loop-fusion` should ensure that every consumer access remapped to a private buffer is within the allocated private footprint and refers to a value that has been populated.
If the consumer access domain is not covered by the producer-derived private footprint, the pass should enlarge and correctly populate the private buffer, leave uncovered accesses on the original memref, or decline the fusion.
The pass must not introduce an out-of-bounds memory access.
## Validity
The source program contains no out-of-bounds memory accesses:
- `%in[%i]` uses indices 0 through 7;
- `%comm[2 * %i]` uses indices 0, 2, ..., 14;
- `%comm[%j]` and `%out[%j]` use indices 0 through 15.
All of these accesses are within their respective `memref<32xf64>` bounds.
Both the source and generated target pass MLIR verification. The out-of-bounds access is introduced only by `affine-loop-fusion`, when the consumer access is remapped to the generated `memref<1xf64>`.
Contributor guide
Research direction
Start with the input.mlir reproducer and run the two mlir-opt commands using affine-loop-fusion in producer maximal mode. Inspect fused.mlir, focusing on the generated memref.alloc and the consumer affine.load indices. Done means the pass no longer introduces an out-of-bounds private-buffer access while preserving valid producer and consumer accesses.
Written by the indexing model from the issue text.
Assessment
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100