[Mlir-commits] [mlir] [MLIR][Affine] Reject affine-loop-unroll-jam on dependence-violating nests (PR #196554)
Soowon Jeong
llvmlistbot at llvm.org
Fri May 8 08:57:53 PDT 2026
https://github.com/swjng updated https://github.com/llvm/llvm-project/pull/196554
>From 8eb30e69d7c25024ad16078aa41e51dae99475da Mon Sep 17 00:00:00 2001
From: Soowon Jeong <soowon1106 at gmail.com>
Date: Fri, 8 May 2026 22:37:02 +0900
Subject: [PATCH] [MLIR][Affine] Reject affine-loop-unroll-jam on
dependence-violating nests
loopUnrollJamByFactor performs no memory-dependence legality check;
loops whose distance vector becomes negative-leading under unroll-and-jam's
new iteration order are silently miscompiled. Add a factor-aware gate: for
each dependence, if the outer (band[0]) component's lower bound is >= F the
stripe never spans the dep and the inner-iv interchange does not reorder
it; otherwise refuse when any inner component is negative-leading. This is
strictly more precise than reusing isTilingValid, e.g. distance (2, -1) at
factor 2 is now correctly accepted.
Self-contained: no dependency on the in-flight isTilingValid hardening.
Assisted-by: Claude
Co-Authored-By: Claude <noreply at anthropic.com>
---
mlir/lib/Dialect/Affine/Utils/LoopUtils.cpp | 41 +++++++++++++++++++++
mlir/test/Dialect/Affine/unroll-jam.mlir | 40 ++++++++++++++++++++
2 files changed, 81 insertions(+)
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopUtils.cpp
index 8f1249e3afaf0..20b1bc4a7a0fe 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopUtils.cpp
@@ -12,6 +12,7 @@
#include "mlir/Dialect/Affine/LoopUtils.h"
#include "mlir/Analysis/SliceAnalysis.h"
+#include "mlir/Dialect/Affine/Analysis/AffineAnalysis.h"
#include "mlir/Dialect/Affine/Analysis/LoopAnalysis.h"
#include "mlir/Dialect/Affine/Analysis/Utils.h"
#include "mlir/Dialect/Affine/IR/AffineValueMap.h"
@@ -1107,6 +1108,46 @@ LogicalResult mlir::affine::loopUnrollJamByFactor(AffineForOp forOp,
if (!areInnerBoundsInvariant(forOp))
return failure();
+ // Refuse if the jammed iteration order would violate a memory dep. UJ at
+ // factor F reorders dep `d` iff `0 < d_outer < F` and some `d_inner_k < 0`.
+ SmallVector<AffineForOp, 4> band;
+ getPerfectlyNestedLoops(band, forOp);
+ if (band.size() > 1) {
+ SmallVector<Operation *, 8> loadAndStoreOps;
+ band[0]->walk([&](Operation *op) {
+ if (isa<AffineReadOpInterface, AffineWriteOpInterface>(op))
+ loadAndStoreOps.push_back(op);
+ });
+ unsigned numLoops = band.size();
+ for (unsigned d = 1; d <= numLoops + 1; ++d) {
+ for (Operation *srcOp : loadAndStoreOps) {
+ MemRefAccess srcAccess(srcOp);
+ for (Operation *dstOp : loadAndStoreOps) {
+ MemRefAccess dstAccess(dstOp);
+ SmallVector<DependenceComponent, 2> depComps;
+ DependenceResult result = checkMemrefAccessDependence(
+ srcAccess, dstAccess, d,
+ /*dependenceConstraints=*/nullptr, &depComps);
+ if (!hasDependence(result))
+ continue;
+ // d_outer >= F: stripe never spans this dep.
+ if (!depComps.empty() && depComps[0].lb.has_value() &&
+ *depComps[0].lb >= static_cast<int64_t>(unrollJamFactor))
+ continue;
+ for (unsigned k = 1; k < depComps.size(); ++k) {
+ const DependenceComponent &c = depComps[k];
+ if (c.lb.has_value() && c.ub.has_value() && *c.lb <= *c.ub &&
+ *c.ub < 0) {
+ LDBG() << "[failed] backward inner dep at depth " << d
+ << " (k=" << k << "); factor " << unrollJamFactor;
+ return failure();
+ }
+ }
+ }
+ }
+ }
+ }
+
// Gather all sub-blocks to jam upon the loop being unrolled.
JamBlockGatherer<AffineForOp> jbg;
jbg.walk(forOp);
diff --git a/mlir/test/Dialect/Affine/unroll-jam.mlir b/mlir/test/Dialect/Affine/unroll-jam.mlir
index 8ed7fccf7d251..aa29c42772001 100644
--- a/mlir/test/Dialect/Affine/unroll-jam.mlir
+++ b/mlir/test/Dialect/Affine/unroll-jam.mlir
@@ -550,3 +550,43 @@ func.func @unroll_jam_iter_args_addi(%arg0: memref<21xi32, 1>, %init : i32) {
// CHECK-NEXT: [[LOAD3:%[0-9]+]] = affine.load {{.*}}[%[[CONST0]]]
// CHECK-NEXT: [[ADD4:%[0-9]+]] = arith.addi [[ADD3]], [[LOAD3]] : i32
// CHECK-NEXT: return
+
+// Verify that affine-loop-unroll-jam refuses to transform a perfectly nested
+// band when its dependence components would be violated by the new iteration
+// order. Mirrors the gating already used by `affine-loop-tile`.
+
+// A flow dependence with distance vector (1, -1): unroll-and-jam at factor 2
+// would interchange the intra-stripe iv with the inner iv and turn the
+// (1, -1) dep into a backward distance. The pass must leave the nest
+// unchanged.
+
+// CHECK-LABEL: func @unroll_jam_illegal_flow_dep_1_neg1
+// CHECK: affine.for %{{.*}} = 1 to 5 {
+// CHECK-NEXT: affine.for %{{.*}} = 0 to 4 {
+// CHECK-NOT: affine.for %{{.*}} = 1 to 5 step 2
+func.func @unroll_jam_illegal_flow_dep_1_neg1(%arr: memref<5x5xi32>) {
+ affine.for %i = 1 to 5 {
+ affine.for %j = 0 to 4 {
+ %v = affine.load %arr[%i - 1, %j + 1] : memref<5x5xi32>
+ affine.store %v, %arr[%i, %j] : memref<5x5xi32>
+ }
+ }
+ return
+}
+
+// Distance vector (2, -1) at factor 2: d_outer >= F, so the unroll-and-jam
+// stripe never spans the dep. The pass must still fire even though an
+// `isTilingValid`-style check (which ignores the factor) would refuse it.
+
+// CHECK-LABEL: func @unroll_jam_legal_outer_distance_geq_factor
+// CHECK: affine.for %{{.*}} = 2 to 6 step 2
+// CHECK-NEXT: affine.for %{{.*}} = 0 to 5 {
+func.func @unroll_jam_legal_outer_distance_geq_factor(%arr: memref<6x6xi32>) {
+ affine.for %i = 2 to 6 {
+ affine.for %j = 0 to 5 {
+ %v = affine.load %arr[%i - 2, %j + 1] : memref<6x6xi32>
+ affine.store %v, %arr[%i, %j] : memref<6x6xi32>
+ }
+ }
+ return
+}
More information about the Mlir-commits
mailing list