[MLIR][Affine] affine-loop-fusion violates a producer-read/consumer-write dependence
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Applying `affine-loop-fusion` to the verifier-valid program below changes the result from `3` to `6`.
The producer reads `a[1]` in every iteration. The consumer, which originally runs only after the producer loop has completed, writes `a[p]`. After fusion, the consumer write for `p = 1` is executed before the producer read for `p = 2`, changing the value observed by that later producer iteration.
Public `mlir-opt` reproducer: https://godbolt.org/z/dv58YjqK4
## Version
Tested at `llvm/llvm-project` commit `450cf0aafeab9fed6666b112b018d3913a36527c` (`LLVM 24.0.0git`).
## Reproducer
```mlir
module {
func.func @kernel(
%a: memref<3xf64>,
%c: memref<3xf64>,
%scratch: memref<3xf64>) {
affine.for %p = 1 to 3 {
%av = affine.load %a[1] : memref<3xf64>
%cv = affine.load %c[%p] : memref<3xf64>
%value = arith.mulf %av, %cv : f64
affine.store %value, %scratch[%p] : memref<3xf64>
}
affine.for %p = 1 to 3 {
%value = affine.load %scratch[%p] : memref<3xf64>
affine.store %value, %a[%p] : memref<3xf64>
}
return
}
func.func @main() -> i64 {
%a = memref.alloc() : memref<3xf64>
%c = memref.alloc() : memref<3xf64>
%scratch = memref.alloc() : memref<3xf64>
%zero = arith.constant 0.0 : f64
%one = arith.constant 1.0 : f64
%two = arith.constant 2.0 : f64
%three = arith.constant 3.0 : f64
affine.for %i = 0 to 3 {
affine.store %zero, %a[%i] : memref<3xf64>
affine.store %zero, %c[%i] : memref<3xf64>
affine.store %zero, %scratch[%i] : memref<3xf64>
}
affine.store %one, %a[1] : memref<3xf64>
affine.store %two, %c[1] : memref<3xf64>
affine.store %three, %c[2] : memref<3xf64>
func.call @kernel(%a, %c, %scratch)
: (memref<3xf64>, memref<3xf64>, memref<3xf64>) -> ()
%result = affine.load %a[2] : memref<3xf64>
%readable = arith.fptosi %result : f64 to i64
return %readable : i64
}
}
```
## Commands
```sh
LLVM_BUILD=/path/to/llvm-build
LOWER='builtin.module(lower-affine,convert-scf-to-cf,expand-strided-metadata,finalize-memref-to-llvm,convert-func-to-llvm,convert-arith-to-llvm,convert-cf-to-llvm,reconcile-unrealized-casts)'
SHARED_LIBS="$LLVM_BUILD/lib/libmlir_runner_utils.so,$LLVM_BUILD/lib/libmlir_c_runner_utils.so"
# Baseline.
"$LLVM_BUILD/bin/mlir-opt" input.mlir \
-pass-pipeline="$LOWER" \
-o baseline.mlir
"$LLVM_BUILD/bin/mlir-runner" baseline.mlir \
-e main \
--entry-point-result=i64 \
--shared-libs="$SHARED_LIBS"
# Apply affine-loop-fusion to the same input, then lower and execute it.
"$LLVM_BUILD/bin/mlir-opt" input.mlir \
--affine-loop-fusion \
-o fused.mlir
"$LLVM_BUILD/bin/mlir-opt" fused.mlir \
-pass-pipeline="$LOWER" \
-o fused-lowered.mlir
"$LLVM_BUILD/bin/mlir-runner" fused-lowered.mlir \
-e main \
--entry-point-result=i64 \
--shared-libs="$SHARED_LIBS"
```
## Actual behavior
The baseline and optimized executions produce:
```text
without affine-loop-fusion: 3
with affine-loop-fusion: 6
```
The generated function interleaves the producer and consumer bodies:
```mlir
affine.for %p = 1 to 3 {
%av = affine.load %a[1] : memref<3xf64>
%cv = affine.load %c[%p] : memref<3xf64>
%value = arith.mulf %av, %cv : f64
affine.store %value, %scratch[%p] : memref<3xf64>
%stored = affine.load %scratch[%p] : memref<3xf64>
affine.store %stored, %a[%p] : memref<3xf64>
}
```
At `p = 1`, the fused consumer writes `2.0` to `a[1]`. The producer iteration
at `p = 2` then reads that modified value and computes `2.0 * 3.0 = 6.0`.
In the source program, both producer iterations read the original `a[1] = 1.0`
before the consumer loop starts, so `a[2]` is `1.0 * 3.0 = 3.0`.
## Expected behavior
`affine-loop-fusion` should preserve the original read-before-write ordering between the producer and consumer loops, or decline the fusion.
Applying the pass must not change the result of this program.
## Validity
Both the input and generated target pass MLIR verification and lower successfully. All memrefs are fully initialized, all memory accesses are in bounds, and the arithmetic and runner entry point are defined. No undefined behavior explains the result difference.
Contributor guide
Research direction
Start by running the supplied input.mlir through mlir-opt with and without --affine-loop-fusion, then lower and execute both outputs with mlir-runner to reproduce 3 versus 6. Investigate the affine-loop-fusion pass and its handling of the producer read from a[1] and consumer write to a[p]. Done means fusion preserves the original result or declines fusion for this dependence.
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