llvm / llvm/llvm-project

[MLIR][Affine] affine-loop-fusion violates a producer-read/consumer-write dependence

Open
#212,032 1 comment 0 reactions 0 assignees View on GitHub
mlir
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.