[Mlir-commits] [mlir] [MLIR][Affine] Handle aliases and effects in fusion dependencies (PR #213413)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Sun Aug 2 07:30:55 PDT 2026
https://github.com/1sgtpepper updated https://github.com/llvm/llvm-project/pull/213413
>From 8de53cd7a3c3064ea89e5c454b869e6a660410e2 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 13:59:03 +0800
Subject: [PATCH 01/15] [MLIR][Affine] Handle cast aliases in fusion
dependencies
---
.../Dialect/Affine/Analysis/CMakeLists.txt | 1 +
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 85 +++++++++++--------
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 41 ++++++++-
3 files changed, 91 insertions(+), 36 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/CMakeLists.txt b/mlir/lib/Dialect/Affine/Analysis/CMakeLists.txt
index 3a1996349dbed..6c52f7751f1f0 100644
--- a/mlir/lib/Dialect/Affine/Analysis/CMakeLists.txt
+++ b/mlir/lib/Dialect/Affine/Analysis/CMakeLists.txt
@@ -18,6 +18,7 @@ add_mlir_dialect_library(MLIRAffineAnalysis
MLIRControlFlowInterfaces
MLIRDialectUtils
MLIRInferTypeOpInterface
+ MLIRMemRefUtils
MLIRSideEffectInterfaces
MLIRPresburger
MLIRSCFDialect
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 321c8e34d907c..11e412f8d8704 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -19,6 +19,7 @@
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Affine/IR/AffineValueMap.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/IR/IntegerSet.h"
#include "llvm/ADT/SetVector.h"
@@ -38,6 +39,49 @@ using llvm::SmallDenseMap;
using Node = MemRefDependenceGraph::Node;
+/// Returns the values that `op` may have a memref effect of type `EffectTys`
+/// on, not considering recursive effects. An op with unknown memory effects
+/// (e.g. a call to an external function without a memory-effect interface) is
+/// conservatively assumed to affect all its memref operands. Fully aliasing
+/// views are canonicalized so the MDG uses one key for the view and its source.
+template <typename... EffectTys>
+static void getMayEffectedValues(Operation *op,
+ SmallVectorImpl<Value> &values) {
+ auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
+ if (!memOp) {
+ if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
+ // No effects.
+ return;
+ // Memref operands have to be considered as being affected.
+ for (Value operand : op->getOperands()) {
+ if (isa<MemRefType>(operand.getType()))
+ values.push_back(memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(operand)));
+ }
+ return;
+ }
+ SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
+ memOp.getEffects(effects);
+ for (auto &effect : effects) {
+ Value effectVal = effect.getValue();
+ if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
+ isa<MemRefType>(effectVal.getType()))
+ values.push_back(memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(effectVal)));
+ };
+}
+
+/// Returns true if `op` may have a memory effect of type `EffectTys` on
+/// `memref`, i.e., whether `memref` is among the values returned by
+/// `getMayEffectedValues` for `op`.
+template <typename... EffectTys>
+static bool mayHaveEffect(Operation *op, Value memref) {
+ SmallVector<Value> values;
+ getMayEffectedValues<EffectTys...>(op, values);
+ return llvm::is_contained(
+ values, memref::skipFullyAliasingOperations(cast<MemrefValue>(memref)));
+}
+
// LoopNestStateCollector walks loop nests and collects load and store
// operations, and whether or not a region holding op other than ForOp and IfOp
// was encountered in the loop nest.
@@ -83,7 +127,7 @@ unsigned Node::getLoadOpCount(Value memref) const {
if (auto affineLoad = dyn_cast<AffineReadOpInterface>(loadOp)) {
if (memref == affineLoad.getMemRef())
++loadOpCount;
- } else if (hasEffect<MemoryEffects::Read>(loadOp, memref)) {
+ } else if (mayHaveEffect<MemoryEffects::Read>(loadOp, memref)) {
++loadOpCount;
}
}
@@ -98,8 +142,7 @@ unsigned Node::getStoreOpCount(Value memref) const {
if (auto affineStore = dyn_cast<AffineWriteOpInterface>(storeOp)) {
if (memref == affineStore.getMemRef())
++storeOpCount;
- } else if (hasEffect<MemoryEffects::Write>(const_cast<Operation *>(storeOp),
- memref)) {
+ } else if (mayHaveEffect<MemoryEffects::Write>(storeOp, memref)) {
++storeOpCount;
}
}
@@ -114,7 +157,7 @@ unsigned Node::hasStore(Value memref) const {
if (auto affineStore = dyn_cast<AffineWriteOpInterface>(storeOp)) {
if (memref == affineStore.getMemRef())
return true;
- } else if (hasEffect<MemoryEffects::Write>(storeOp, memref)) {
+ } else if (mayHaveEffect<MemoryEffects::Write>(storeOp, memref)) {
return true;
}
return false;
@@ -123,7 +166,7 @@ unsigned Node::hasStore(Value memref) const {
unsigned Node::hasFree(Value memref) const {
return llvm::any_of(memrefFrees, [&](Operation *freeOp) {
- return hasEffect<MemoryEffects::Free>(freeOp, memref);
+ return mayHaveEffect<MemoryEffects::Free>(freeOp, memref);
});
}
@@ -160,32 +203,6 @@ void Node::getLoadAndStoreMemrefSet(
}
}
-/// Returns the values that this op has a memref effect of type `EffectTys` on,
-/// not considering recursive effects.
-template <typename... EffectTys>
-static void getEffectedValues(Operation *op, SmallVectorImpl<Value> &values) {
- auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
- if (!memOp) {
- if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
- // No effects.
- return;
- // Memref operands have to be considered as being affected.
- for (Value operand : op->getOperands()) {
- if (isa<MemRefType>(operand.getType()))
- values.push_back(operand);
- }
- return;
- }
- SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
- memOp.getEffects(effects);
- for (auto &effect : effects) {
- Value effectVal = effect.getValue();
- if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
- isa<MemRefType>(effectVal.getType()))
- values.push_back(effectVal);
- };
-}
-
/// Add `op` to MDG creating a new node and adding its memory accesses (affine
/// or non-affine to memrefAccesses (memref -> list of nodes with accesses) map.
static Node *
@@ -210,7 +227,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefLoads) {
SmallVector<Value> effectedValues;
- getEffectedValues<MemoryEffects::Read>(op, effectedValues);
+ getMayEffectedValues<MemoryEffects::Read>(op, effectedValues);
if (llvm::any_of(((ValueRange)effectedValues).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
// We do not know the interaction here.
@@ -221,7 +238,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefStores) {
SmallVector<Value> effectedValues;
- getEffectedValues<MemoryEffects::Write>(op, effectedValues);
+ getMayEffectedValues<MemoryEffects::Write>(op, effectedValues);
if (llvm::any_of((ValueRange(effectedValues)).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
return nullptr;
@@ -231,7 +248,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefFrees) {
SmallVector<Value> effectedValues;
- getEffectedValues<MemoryEffects::Free>(op, effectedValues);
+ getMayEffectedValues<MemoryEffects::Free>(op, effectedValues);
if (llvm::any_of((ValueRange(effectedValues)).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
return nullptr;
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index cf530016c201a..980e68e6b9cfa 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -583,11 +583,15 @@ func.func @zero_tolerance(%arg0: memref<65536xcomplex<f64>>, %arg1: memref<30x13
affine.store %18, %2[%arg2] : memref<131072xi128>
affine.store %13, %1[%arg2] : memref<131072xi1>
}
- // The next two nests are fused.
+ // The next nest cannot fuse with the one following it across the opaque
+ // external call below, which may write to its memref operand in place.
// ZERO-TOLERANCE: affine.for %{{.*}} = 0 to 30
// ZERO-TOLERANCE-NEXT: affine.for %{{.*}} = 0 to 131072
// ZERO-TOLERANCE: func.call @__external_reduce_barrett
// ZERO-TOLERANCE: affine.store
+ // ZERO-TOLERANCE: call @__external_levelwise_forward_ntt
+ // ZERO-TOLERANCE-NEXT: affine.for %{{.*}} = 0 to 30
+ // ZERO-TOLERANCE-NEXT: affine.for %{{.*}} = 0 to 131072
// ZERO-TOLERANCE: affine.load
// ZERO-TOLERANCE-NEXT: affine.store
affine.for %arg2 = 0 to 30 {
@@ -611,9 +615,14 @@ func.func @zero_tolerance(%arg0: memref<65536xcomplex<f64>>, %arg1: memref<30x13
affine.store %7, %arg1[%arg2, %arg3] : memref<30x131072xi64>
}
}
- // Under maximal fusion, just one nest.
+ // Under maximal fusion, the first two nests fuse, but the last nest cannot
+ // fuse into them across the opaque external call, which may write to its
+ // memref operand in place.
// PRODUCER-CONSUMER-MAXIMAL: affine.for %{{.*}} = 0 to 30
// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for %{{.*}} = 0 to 131072
+ // PRODUCER-CONSUMER-MAXIMAL: call @__external_levelwise_forward_ntt
+ // PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for %{{.*}} = 0 to 30
+ // PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for %{{.*}} = 0 to 131072
// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for %{{.*}}
memref.dealloc %2 : memref<131072xi128>
memref.dealloc %1 : memref<131072xi1>
@@ -884,3 +893,31 @@ func.func @high_trip_count(%arg0: memref<1024x4096xf32>, %arg1: memref<8192x4096
}
return %alloc : memref<1024x8192xf32>
}
+
+// -----
+
+// The external call receives a fully aliasing cast of the producer's memref.
+// Fusion must preserve the call between the producer and consumer loops.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_external_call
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: memref.cast
+// PRODUCER-CONSUMER-MAXIMAL: call @escape
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @cast_alias_external_call(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ %view = memref.cast %comm : memref<32xf64> to memref<?xf64>
+ func.call @escape(%view) : (memref<?xf64>) -> ()
+ 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
+}
+func.func private @escape(memref<?xf64>)
>From ba43cc3744d3fee08e720ca8fa97ace576c368b7 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 14:13:28 +0800
Subject: [PATCH 02/15] Format Affine analysis changes
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 11e412f8d8704..793214e87f021 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -55,8 +55,8 @@ static void getMayEffectedValues(Operation *op,
// Memref operands have to be considered as being affected.
for (Value operand : op->getOperands()) {
if (isa<MemRefType>(operand.getType()))
- values.push_back(memref::skipFullyAliasingOperations(
- cast<MemrefValue>(operand)));
+ values.push_back(
+ memref::skipFullyAliasingOperations(cast<MemrefValue>(operand)));
}
return;
}
@@ -66,8 +66,8 @@ static void getMayEffectedValues(Operation *op,
Value effectVal = effect.getValue();
if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
isa<MemRefType>(effectVal.getType()))
- values.push_back(memref::skipFullyAliasingOperations(
- cast<MemrefValue>(effectVal)));
+ values.push_back(
+ memref::skipFullyAliasingOperations(cast<MemrefValue>(effectVal)));
};
}
>From 8a2d6150d4d468d73c91a16efdb978db9de9a82c Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 15:38:29 +0800
Subject: [PATCH 03/15] [MLIR][Affine] Make fusion alias handling symmetric
Use one canonical identity for trivial memref aliases throughout the dependence graph and loop fusion consumers. Keep raw views for precise affine accesses, and conservatively handle differing views and arbitrary memory-effecting operations.
---
.../mlir/Dialect/Affine/Analysis/Utils.h | 6 +-
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 152 ++++++++++--------
.../Dialect/Affine/Transforms/LoopFusion.cpp | 61 +++++--
mlir/lib/Dialect/Affine/Utils/CMakeLists.txt | 1 +
.../Dialect/Affine/Utils/LoopFusionUtils.cpp | 54 +++++--
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 88 ++++++++++
mlir/test/Dialect/Affine/loop-fusion.mlir | 41 ++++-
7 files changed, 310 insertions(+), 93 deletions(-)
diff --git a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
index df4145db90a61..6a03fabd274fd 100644
--- a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
+++ b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
@@ -128,9 +128,9 @@ struct MemRefDependenceGraph {
// 'Node.outEdges[i].id' is the identifier of the dest node of the edge.
unsigned id;
// The SSA value on which this edge represents a dependence.
- // If the value is a memref, then the dependence is between graph nodes
- // which contain accesses to the same memref 'value'. If the value is a
- // non-memref value, then the dependence is between a graph node which
+ // If the value is a memref, then it is the canonical representative of
+ // the trivial alias class on which the dependence is based. If the value
+ // is a non-memref value, then the dependence is between a graph node which
// defines an SSA value and another graph node which uses the SSA value
// (e.g. a constant or load operation defining a value which is used inside
// a loop nest).
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 793214e87f021..bbc2fd0888d7c 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -39,13 +39,23 @@ using llvm::SmallDenseMap;
using Node = MemRefDependenceGraph::Node;
+static Value canonicalizeMemref(Value value) {
+ if (!value || !isa<MemRefType>(value.getType()))
+ return value;
+ return memref::skipFullyAliasingOperations(cast<MemrefValue>(value));
+}
+
+static bool isSameMemref(Value lhs, Value rhs) {
+ return canonicalizeMemref(lhs) == canonicalizeMemref(rhs);
+}
+
/// Returns the values that `op` may have a memref effect of type `EffectTys`
/// on, not considering recursive effects. An op with unknown memory effects
/// (e.g. a call to an external function without a memory-effect interface) is
/// conservatively assumed to affect all its memref operands. Fully aliasing
/// views are canonicalized so the MDG uses one key for the view and its source.
template <typename... EffectTys>
-static void getMayEffectedValues(Operation *op,
+static void getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
if (!memOp) {
@@ -55,8 +65,7 @@ static void getMayEffectedValues(Operation *op,
// Memref operands have to be considered as being affected.
for (Value operand : op->getOperands()) {
if (isa<MemRefType>(operand.getType()))
- values.push_back(
- memref::skipFullyAliasingOperations(cast<MemrefValue>(operand)));
+ values.push_back(canonicalizeMemref(operand));
}
return;
}
@@ -66,20 +75,18 @@ static void getMayEffectedValues(Operation *op,
Value effectVal = effect.getValue();
if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
isa<MemRefType>(effectVal.getType()))
- values.push_back(
- memref::skipFullyAliasingOperations(cast<MemrefValue>(effectVal)));
+ values.push_back(canonicalizeMemref(effectVal));
};
}
/// Returns true if `op` may have a memory effect of type `EffectTys` on
/// `memref`, i.e., whether `memref` is among the values returned by
-/// `getMayEffectedValues` for `op`.
+/// `getMayAffectedValues` for `op`.
template <typename... EffectTys>
static bool mayHaveEffect(Operation *op, Value memref) {
SmallVector<Value> values;
- getMayEffectedValues<EffectTys...>(op, values);
- return llvm::is_contained(
- values, memref::skipFullyAliasingOperations(cast<MemrefValue>(memref)));
+ getMayAffectedValues<EffectTys...>(op, values);
+ return llvm::is_contained(values, canonicalizeMemref(memref));
}
// LoopNestStateCollector walks loop nests and collects load and store
@@ -99,11 +106,11 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
// This op itself is memory-effect free.
return;
- // Check operands. Eg. ops like the `call` op are handled here.
- for (Value v : op->getOperands()) {
- if (!isa<MemRefType>(v.getType()))
- continue;
- // Conservatively, we assume the memref is read and written to.
+ // Check operands. E.g., ops like the `call` op are handled here.
+ if (llvm::any_of(op->getOperands(), [](Value value) {
+ return isa<MemRefType>(value.getType());
+ })) {
+ // Conservatively, assume all memref operands are read and written.
memrefLoads.push_back(op);
memrefStores.push_back(op);
}
@@ -124,12 +131,9 @@ unsigned Node::getLoadOpCount(Value memref) const {
unsigned loadOpCount = 0;
for (Operation *loadOp : loads) {
// Common case: affine reads.
- if (auto affineLoad = dyn_cast<AffineReadOpInterface>(loadOp)) {
- if (memref == affineLoad.getMemRef())
+ if (auto affineLoad = dyn_cast<AffineReadOpInterface>(loadOp))
+ if (isSameMemref(memref, affineLoad.getMemRef()))
++loadOpCount;
- } else if (mayHaveEffect<MemoryEffects::Read>(loadOp, memref)) {
- ++loadOpCount;
- }
}
return loadOpCount;
}
@@ -140,7 +144,7 @@ unsigned Node::getStoreOpCount(Value memref) const {
for (auto *storeOp : llvm::concat<Operation *const>(stores, memrefStores)) {
// Common case: affine writes.
if (auto affineStore = dyn_cast<AffineWriteOpInterface>(storeOp)) {
- if (memref == affineStore.getMemRef())
+ if (isSameMemref(memref, affineStore.getMemRef()))
++storeOpCount;
} else if (mayHaveEffect<MemoryEffects::Write>(storeOp, memref)) {
++storeOpCount;
@@ -155,7 +159,7 @@ unsigned Node::hasStore(Value memref) const {
llvm::concat<Operation *const>(stores, memrefStores),
[&](Operation *storeOp) {
if (auto affineStore = dyn_cast<AffineWriteOpInterface>(storeOp)) {
- if (memref == affineStore.getMemRef())
+ if (isSameMemref(memref, affineStore.getMemRef()))
return true;
} else if (mayHaveEffect<MemoryEffects::Write>(storeOp, memref)) {
return true;
@@ -174,7 +178,8 @@ unsigned Node::hasFree(Value memref) const {
void Node::getStoreOpsForMemref(Value memref,
SmallVectorImpl<Operation *> *storeOps) const {
for (Operation *storeOp : stores) {
- if (memref == cast<AffineWriteOpInterface>(storeOp).getMemRef())
+ if (isSameMemref(memref,
+ cast<AffineWriteOpInterface>(storeOp).getMemRef()))
storeOps->push_back(storeOp);
}
}
@@ -183,7 +188,8 @@ void Node::getStoreOpsForMemref(Value memref,
void Node::getLoadOpsForMemref(Value memref,
SmallVectorImpl<Operation *> *loadOps) const {
for (Operation *loadOp : loads) {
- if (memref == cast<AffineReadOpInterface>(loadOp).getMemRef())
+ if (isSameMemref(memref,
+ cast<AffineReadOpInterface>(loadOp).getMemRef()))
loadOps->push_back(loadOp);
}
}
@@ -194,10 +200,12 @@ void Node::getLoadAndStoreMemrefSet(
DenseSet<Value> *loadAndStoreMemrefSet) const {
llvm::SmallDenseSet<Value, 2> loadMemrefs;
for (Operation *loadOp : loads) {
- loadMemrefs.insert(cast<AffineReadOpInterface>(loadOp).getMemRef());
+ loadMemrefs.insert(canonicalizeMemref(
+ cast<AffineReadOpInterface>(loadOp).getMemRef()));
}
for (Operation *storeOp : stores) {
- auto memref = cast<AffineWriteOpInterface>(storeOp).getMemRef();
+ auto memref = canonicalizeMemref(
+ cast<AffineWriteOpInterface>(storeOp).getMemRef());
if (loadMemrefs.count(memref) > 0)
loadAndStoreMemrefSet->insert(memref);
}
@@ -217,42 +225,44 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
Node &node = nodes.insert({newNodeId, Node(newNodeId, nodeOp)}).first->second;
for (Operation *op : collector.loadOpInsts) {
node.loads.push_back(op);
- auto memref = cast<AffineReadOpInterface>(op).getMemRef();
+ auto memref = canonicalizeMemref(
+ cast<AffineReadOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
}
for (Operation *op : collector.storeOpInsts) {
node.stores.push_back(op);
- auto memref = cast<AffineWriteOpInterface>(op).getMemRef();
+ auto memref = canonicalizeMemref(
+ cast<AffineWriteOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
}
for (Operation *op : collector.memrefLoads) {
- SmallVector<Value> effectedValues;
- getMayEffectedValues<MemoryEffects::Read>(op, effectedValues);
- if (llvm::any_of(((ValueRange)effectedValues).getTypes(),
+ SmallVector<Value> affectedValues;
+ getMayAffectedValues<MemoryEffects::Read>(op, affectedValues);
+ if (llvm::any_of(((ValueRange)affectedValues).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
// We do not know the interaction here.
return nullptr;
- for (Value memref : effectedValues)
+ for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
node.memrefLoads.push_back(op);
}
for (Operation *op : collector.memrefStores) {
- SmallVector<Value> effectedValues;
- getMayEffectedValues<MemoryEffects::Write>(op, effectedValues);
- if (llvm::any_of((ValueRange(effectedValues)).getTypes(),
+ SmallVector<Value> affectedValues;
+ getMayAffectedValues<MemoryEffects::Write>(op, affectedValues);
+ if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
return nullptr;
- for (Value memref : effectedValues)
+ for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
node.memrefStores.push_back(op);
}
for (Operation *op : collector.memrefFrees) {
- SmallVector<Value> effectedValues;
- getMayEffectedValues<MemoryEffects::Free>(op, effectedValues);
- if (llvm::any_of((ValueRange(effectedValues)).getTypes(),
+ SmallVector<Value> affectedValues;
+ getMayAffectedValues<MemoryEffects::Free>(op, affectedValues);
+ if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
[](Type type) { return !isa<MemRefType>(type); }))
return nullptr;
- for (Value memref : effectedValues)
+ for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
node.memrefFrees.push_back(op);
}
@@ -260,17 +270,16 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
return &node;
}
-/// Returns the memref being read/written by a memref/affine load/store op.
-static Value getMemRef(Operation *memOp) {
- if (auto memrefLoad = dyn_cast<memref::LoadOp>(memOp))
- return memrefLoad.getMemRef();
- if (auto affineLoad = dyn_cast<AffineReadOpInterface>(memOp))
- return affineLoad.getMemRef();
- if (auto memrefStore = dyn_cast<memref::StoreOp>(memOp))
- return memrefStore.getMemRef();
- if (auto affineStore = dyn_cast<AffineWriteOpInterface>(memOp))
- return affineStore.getMemRef();
- llvm_unreachable("unexpected op");
+/// Returns true if `op` may access `memref`, including through a fully aliasing
+/// view. Unknown operations are handled conservatively through their memory
+/// effects rather than assuming a particular operation class.
+static bool mayAccessMemRef(Operation *op, Value memref) {
+ if (auto affineRead = dyn_cast<AffineReadOpInterface>(op))
+ return isSameMemref(affineRead.getMemRef(), memref);
+ if (auto affineWrite = dyn_cast<AffineWriteOpInterface>(op))
+ return isSameMemref(affineWrite.getMemRef(), memref);
+ return mayHaveEffect<MemoryEffects::Read>(op, memref) ||
+ mayHaveEffect<MemoryEffects::Write>(op, memref);
}
/// Returns true if there may be a dependence on `memref` from srcNode's
@@ -289,16 +298,15 @@ static bool mayDependence(const Node &srcNode, const Node &dstNode,
// true if there exists a conflicting read/write access involving such.
// Check whether there is a dependence from a source read/write op to a
- // destination read/write one; all expected to be memref/affine load/store.
+ // destination read/write one.
auto hasNonAffineDep = [&](ArrayRef<Operation *> srcMemOps,
ArrayRef<Operation *> dstMemOps) {
return llvm::any_of(srcMemOps, [&](Operation *srcOp) {
- Value srcMemref = getMemRef(srcOp);
- if (srcMemref != memref)
+ if (!mayAccessMemRef(srcOp, memref))
return false;
- return llvm::find_if(dstMemOps, [&](Operation *dstOp) {
- return srcMemref == getMemRef(dstOp);
- }) != dstMemOps.end();
+ return llvm::any_of(dstMemOps, [&](Operation *dstOp) {
+ return mayAccessMemRef(dstOp, memref);
+ });
});
};
@@ -335,13 +343,18 @@ static bool mayDependence(const Node &srcNode, const Node &dstNode,
for (auto *srcMemOp :
llvm::concat<Operation *const>(srcNode.stores, srcNode.loads)) {
MemRefAccess srcAcc(srcMemOp);
- if (srcAcc.memref != memref)
+ if (!isSameMemref(srcAcc.memref, memref))
continue;
for (auto *destMemOp :
llvm::concat<Operation *const>(dstNode.stores, dstNode.loads)) {
MemRefAccess destAcc(destMemOp);
- if (destAcc.memref != memref)
+ if (!isSameMemref(destAcc.memref, memref))
+ continue;
+ if (srcAcc.memref != destAcc.memref) {
+ if (srcAcc.isStore() || destAcc.isStore())
+ return true;
continue;
+ }
// Check for a top-level dependence between srcNode and destNode's ops.
if (!noDependence(checkMemrefAccessDependence(
srcAcc, destAcc, getNestingDepth(srcNode.op) + 1)))
@@ -354,7 +367,7 @@ static bool mayDependence(const Node &srcNode, const Node &dstNode,
bool MemRefDependenceGraph::init(bool fullAffineDependences) {
LDBG() << "--- Initializing MDG ---";
// Map from a memref to the set of ids of the nodes that have ops accessing
- // the memref.
+ // the memref. Fully aliasing views use their canonical source value here.
DenseMap<Value, SetVector<unsigned>> memrefAccesses;
// Create graph nodes.
@@ -369,14 +382,16 @@ bool MemRefDependenceGraph::init(bool fullAffineDependences) {
// Create graph node for top-level load op.
Node node(nextNodeId++, &op);
node.loads.push_back(&op);
- auto memref = cast<AffineReadOpInterface>(op).getMemRef();
+ auto memref = canonicalizeMemref(
+ cast<AffineReadOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
nodes.insert({node.id, node});
} else if (isa<AffineWriteOpInterface>(op)) {
// Create graph node for top-level store op.
Node node(nextNodeId++, &op);
node.stores.push_back(&op);
- auto memref = cast<AffineWriteOpInterface>(op).getMemRef();
+ auto memref = canonicalizeMemref(
+ cast<AffineWriteOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
nodes.insert({node.id, node});
} else if (op.getNumResults() > 0 && !op.use_empty()) {
@@ -537,6 +552,7 @@ bool MemRefDependenceGraph::hasEdge(unsigned srcId, unsigned dstId,
if (!outEdges.contains(srcId) || !inEdges.contains(dstId)) {
return false;
}
+ value = canonicalizeMemref(value);
bool hasOutEdge = llvm::any_of(outEdges.lookup(srcId), [=](const Edge &edge) {
return edge.id == dstId && (!value || edge.value == value);
});
@@ -549,6 +565,7 @@ bool MemRefDependenceGraph::hasEdge(unsigned srcId, unsigned dstId,
// Adds an edge from node 'srcId' to node 'dstId' for 'value'.
void MemRefDependenceGraph::addEdge(unsigned srcId, unsigned dstId,
Value value) {
+ value = canonicalizeMemref(value);
if (!hasEdge(srcId, dstId, value)) {
outEdges[srcId].push_back({dstId, value});
inEdges[dstId].push_back({srcId, value});
@@ -562,6 +579,7 @@ void MemRefDependenceGraph::removeEdge(unsigned srcId, unsigned dstId,
Value value) {
assert(inEdges.count(dstId) > 0);
assert(outEdges.count(srcId) > 0);
+ value = canonicalizeMemref(value);
if (isa<MemRefType>(value.getType())) {
assert(memrefEdgeCount.count(value) > 0);
memrefEdgeCount[value]--;
@@ -624,7 +642,7 @@ unsigned MemRefDependenceGraph::getIncomingMemRefAccesses(unsigned id,
Value memref) const {
unsigned inEdgeCount = 0;
for (const Edge &inEdge : inEdges.lookup(id)) {
- if (inEdge.value == memref) {
+ if (isSameMemref(inEdge.value, memref)) {
const Node *srcNode = getNode(inEdge.id);
// Only count in edges from 'srcNode' if 'srcNode' accesses 'memref'
if (srcNode->getStoreOpCount(memref) > 0)
@@ -640,7 +658,7 @@ unsigned MemRefDependenceGraph::getOutEdgeCount(unsigned id,
Value memref) const {
unsigned outEdgeCount = 0;
for (const auto &outEdge : outEdges.lookup(id))
- if (!memref || outEdge.value == memref)
+ if (!memref || isSameMemref(outEdge.value, memref))
++outEdgeCount;
return outEdgeCount;
}
@@ -743,7 +761,9 @@ void MemRefDependenceGraph::updateEdges(unsigned srcId, unsigned dstId,
SmallVector<Edge, 2> oldInEdges = inEdges[srcId];
for (auto &inEdge : oldInEdges) {
// Add edge from 'inEdge.id' to 'dstId' if it's not a private memref.
- if (!privateMemRefs.contains(inEdge.value))
+ if (!llvm::any_of(privateMemRefs, [&](Value privateMemRef) {
+ return isSameMemref(privateMemRef, inEdge.value);
+ }))
addEdge(inEdge.id, dstId, inEdge.value);
}
}
@@ -767,7 +787,9 @@ void MemRefDependenceGraph::updateEdges(unsigned srcId, unsigned dstId,
if (inEdges.count(dstId) > 0 && !privateMemRefs.empty()) {
SmallVector<Edge, 2> oldInEdges = inEdges[dstId];
for (auto &inEdge : oldInEdges)
- if (privateMemRefs.count(inEdge.value) > 0)
+ if (llvm::any_of(privateMemRefs, [&](Value privateMemRef) {
+ return isSameMemref(privateMemRef, inEdge.value);
+ }))
removeEdge(inEdge.id, dstId, inEdge.value);
}
}
diff --git a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
index 1ec5fbfef50c3..4c6b94c26a24a 100644
--- a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
+++ b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
@@ -19,6 +19,7 @@
#include "mlir/Dialect/Affine/LoopUtils.h"
#include "mlir/Dialect/Affine/Utils.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/IR/AffineExpr.h"
#include "mlir/IR/AffineMap.h"
#include "mlir/IR/Builders.h"
@@ -154,7 +155,11 @@ static void getProducerCandidates(unsigned dstId,
if (any_of(srcNode->stores, [&](Operation *op) {
auto storeOp = cast<AffineWriteOpInterface>(op);
- return consumedMemrefs.count(storeOp.getMemRef()) > 0;
+ return llvm::any_of(consumedMemrefs, [&](Value consumedMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(consumedMemref),
+ cast<MemrefValue>(storeOp.getMemRef()));
+ });
}))
srcIdCandidates.push_back(srcNode->id);
}
@@ -218,7 +223,10 @@ static void gatherEscapingMemrefs(unsigned id, const MemRefDependenceGraph &mdg,
auto *node = mdg.getNode(id);
for (Operation *storeOp : node->stores) {
auto memref = cast<AffineWriteOpInterface>(storeOp).getMemRef();
- if (escapingMemRefs.count(memref))
+ if (llvm::any_of(escapingMemRefs, [&](Value escapingMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(escapingMemref), cast<MemrefValue>(memref));
+ }))
continue;
if (isEscapingMemref(memref, &mdg.block))
escapingMemRefs.insert(memref);
@@ -852,7 +860,10 @@ struct GreedyFusion {
// 1. The source is to be removed after fusion,
// OR
// 2. The destination writes to `memref`.
- if (srcEscapingMemRefs.count(memref) > 0 &&
+ if (llvm::any_of(srcEscapingMemRefs, [&](Value escapingMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(escapingMemref), cast<MemrefValue>(memref));
+ }) &&
(removeSrcNode || consumerNode->getStoreOpCount(memref) > 0))
return false;
@@ -867,7 +878,10 @@ struct GreedyFusion {
// cannot create a private memref.
if (removeSrcNode &&
any_of(mdg->outEdges[producerId], [&](const auto &edge) {
- return edge.value == memref && edge.id != consumerId;
+ return edge.id != consumerId && isa<MemRefType>(edge.value.getType()) &&
+ memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(edge.value),
+ cast<MemrefValue>(memref));
}))
return false;
@@ -972,12 +986,20 @@ struct GreedyFusion {
// producer-consumer loads/stores.
SmallVector<Operation *, 2> dstMemrefOps;
for (Operation *op : dstNode->loads)
- if (producerConsumerMemrefs.count(
- cast<AffineReadOpInterface>(op).getMemRef()) > 0)
+ if (llvm::any_of(producerConsumerMemrefs, [&](Value producerMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(producerMemref),
+ cast<MemrefValue>(
+ cast<AffineReadOpInterface>(op).getMemRef()));
+ }))
dstMemrefOps.push_back(op);
for (Operation *op : dstNode->stores)
- if (producerConsumerMemrefs.count(
- cast<AffineWriteOpInterface>(op).getMemRef()))
+ if (llvm::any_of(producerConsumerMemrefs, [&](Value producerMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(producerMemref),
+ cast<MemrefValue>(
+ cast<AffineWriteOpInterface>(op).getMemRef()));
+ }))
dstMemrefOps.push_back(op);
if (dstMemrefOps.empty())
continue;
@@ -1050,8 +1072,12 @@ struct GreedyFusion {
// Retrieve producer stores from the src loop.
SmallVector<Operation *, 2> producerStores;
for (Operation *op : srcNode->stores)
- if (producerConsumerMemrefs.count(
- cast<AffineWriteOpInterface>(op).getMemRef()))
+ if (llvm::any_of(producerConsumerMemrefs, [&](Value producerMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(producerMemref),
+ cast<MemrefValue>(
+ cast<AffineWriteOpInterface>(op).getMemRef()));
+ }))
producerStores.push_back(op);
assert(!producerStores.empty() && "Expected producer store");
@@ -1112,7 +1138,11 @@ struct GreedyFusion {
DenseMap<Value, SmallVector<Operation *, 4>> privateMemRefToStores;
dstAffineForOp.walk([&](AffineWriteOpInterface storeOp) {
Value storeMemRef = storeOp.getMemRef();
- if (privateMemrefs.count(storeMemRef) > 0)
+ if (llvm::any_of(privateMemrefs, [&](Value privateMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(privateMemref),
+ cast<MemrefValue>(storeMemRef));
+ }))
privateMemRefToStores[storeMemRef].push_back(storeOp);
});
@@ -1386,8 +1416,8 @@ struct GreedyFusion {
// Check that all stores are to the same memref if any.
DenseSet<Value> storeMemrefs;
for (auto *storeOpInst : sibNode->stores) {
- storeMemrefs.insert(
- cast<AffineWriteOpInterface>(storeOpInst).getMemRef());
+ storeMemrefs.insert(memref::skipFullyAliasingOperations(cast<MemrefValue>(
+ cast<AffineWriteOpInterface>(storeOpInst).getMemRef())));
}
return storeMemrefs.size() <= 1;
};
@@ -1457,7 +1487,10 @@ struct GreedyFusion {
if (visitedSibNodeIds->count(sibNodeId) > 0)
return;
// Skip output edge if not a sibling using the same memref.
- if (outEdge.id == dstNode->id || outEdge.value != inEdge.value)
+ if (outEdge.id == dstNode->id ||
+ !memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(outEdge.value),
+ cast<MemrefValue>(inEdge.value)))
return;
auto *sibNode = mdg->getNode(sibNodeId);
if (!isa<AffineForOp>(sibNode->op))
diff --git a/mlir/lib/Dialect/Affine/Utils/CMakeLists.txt b/mlir/lib/Dialect/Affine/Utils/CMakeLists.txt
index ef6e0dbf45d3a..efeac83a098e7 100644
--- a/mlir/lib/Dialect/Affine/Utils/CMakeLists.txt
+++ b/mlir/lib/Dialect/Affine/Utils/CMakeLists.txt
@@ -14,6 +14,7 @@ add_mlir_dialect_library(MLIRAffineUtils
MLIRArithUtils
MLIRFuncDialect
MLIRMemRefDialect
+ MLIRMemRefUtils
MLIRTransformUtils
MLIRViewLikeInterface
)
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index 68296ea3368a1..4e4f8fd696ffd 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -18,6 +18,7 @@
#include "mlir/Dialect/Affine/Analysis/Utils.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Affine/LoopUtils.h"
+#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/IR/IRMapping.h"
#include "mlir/IR/Operation.h"
#include "mlir/IR/PatternMatch.h"
@@ -37,10 +38,14 @@ static void getLoadAndStoreMemRefAccesses(Operation *opA,
DenseMap<Value, bool> &values) {
opA->walk([&](Operation *op) {
if (auto loadOp = dyn_cast<AffineReadOpInterface>(op)) {
- if (values.count(loadOp.getMemRef()) == 0)
- values[loadOp.getMemRef()] = false;
+ Value memref = memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(loadOp.getMemRef()));
+ if (values.count(memref) == 0)
+ values[memref] = false;
} else if (auto storeOp = dyn_cast<AffineWriteOpInterface>(op)) {
- values[storeOp.getMemRef()] = true;
+ Value memref = memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(storeOp.getMemRef()));
+ values[memref] = true;
}
});
}
@@ -50,10 +55,16 @@ static void getLoadAndStoreMemRefAccesses(Operation *opA,
/// Returns false otherwise.
static bool isDependentLoadOrStoreOp(Operation *op,
DenseMap<Value, bool> &values) {
- if (auto loadOp = dyn_cast<AffineReadOpInterface>(op))
- return values.count(loadOp.getMemRef()) > 0 && values[loadOp.getMemRef()];
- if (auto storeOp = dyn_cast<AffineWriteOpInterface>(op))
- return values.count(storeOp.getMemRef()) > 0;
+ if (auto loadOp = dyn_cast<AffineReadOpInterface>(op)) {
+ Value memref = memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(loadOp.getMemRef()));
+ return values.count(memref) > 0 && values[memref];
+ }
+ if (auto storeOp = dyn_cast<AffineWriteOpInterface>(op)) {
+ Value memref = memref::skipFullyAliasingOperations(
+ cast<MemrefValue>(storeOp.getMemRef()));
+ return values.count(memref) > 0;
+ }
return false;
}
@@ -200,7 +211,10 @@ static unsigned getMaxLoopDepth(ArrayRef<Operation *> srcOps,
auto loadOp = dyn_cast<AffineReadOpInterface>(dstOp);
Value memref = loadOp ? loadOp.getMemRef()
: cast<AffineWriteOpInterface>(dstOp).getMemRef();
- if (producerConsumerMemrefs.count(memref) > 0)
+ if (llvm::any_of(producerConsumerMemrefs, [&](Value producerMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(producerMemref), cast<MemrefValue>(memref));
+ }))
targetDstOps.push_back(dstOp);
}
@@ -223,6 +237,19 @@ static unsigned getMaxLoopDepth(ArrayRef<Operation *> srcOps,
auto *dstOpInst = targetDstOps[j];
MemRefAccess dstAccess(dstOpInst);
+ if (!memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(srcAccess.memref),
+ cast<MemrefValue>(dstAccess.memref)))
+ continue;
+ // Affine maps are expressed in the raw view coordinates. Do not run
+ // precise dependence analysis across different views without a
+ // translation between those coordinate systems.
+ if (srcAccess.memref != dstAccess.memref) {
+ if (srcAccess.isStore() || dstAccess.isStore())
+ return 0;
+ continue;
+ }
+
unsigned numCommonLoops =
getNumCommonSurroundingLoops(*srcOpInst, *dstOpInst);
for (unsigned d = 1; d <= numCommonLoops + 1; ++d) {
@@ -328,7 +355,10 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
// to 'memref' in 'srcForOp' to compute the slice union.
for (Operation *op : opsA) {
auto load = dyn_cast<AffineReadOpInterface>(op);
- if (load && load.getMemRef() == fusionStrategy.getSiblingFusionMemRef())
+ if (load && memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(load.getMemRef()),
+ cast<MemrefValue>(
+ fusionStrategy.getSiblingFusionMemRef())))
strategyOpsA.push_back(op);
}
break;
@@ -652,6 +682,10 @@ void mlir::affine::gatherProducerConsumerMemrefs(
// memrefs from loads in 'dstOps'.
for (Operation *op : dstOps)
if (auto loadOp = dyn_cast<AffineReadOpInterface>(op))
- if (srcStoreMemRefs.count(loadOp.getMemRef()) > 0)
+ if (llvm::any_of(srcStoreMemRefs, [&](Value storeMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(storeMemref),
+ cast<MemrefValue>(loadOp.getMemRef()));
+ }))
producerConsumerMemrefs.insert(loadOp.getMemRef());
}
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 980e68e6b9cfa..471e2f4a9bb64 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -921,3 +921,91 @@ func.func @cast_alias_external_call(
return
}
func.func private @escape(memref<?xf64>)
+
+// -----
+
+// Affine accesses may use the fully aliasing view while the external call uses
+// the source value. The call must remain between the two loop nests.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_reverse
+// PRODUCER-CONSUMER-MAXIMAL: memref.cast
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_static
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @cast_alias_reverse(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ %view = memref.cast %comm : memref<32xf64> to memref<?xf64>
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %view[%i] : memref<?xf64>
+ }
+ func.call @escape_static(%comm) : (memref<32xf64>) -> ()
+ affine.for %j = 0 to 16 {
+ %c = affine.load %view[%j] : memref<?xf64>
+ %d = arith.addf %c, %c : f64
+ affine.store %d, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @escape_static(memref<32xf64>)
+
+// -----
+
+// Two distinct fully aliasing views must be placed in the same dependence
+// class as their source value.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_sibling
+// PRODUCER-CONSUMER-MAXIMAL: memref.cast
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: call @escape
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @cast_alias_sibling(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ %view0 = memref.cast %comm : memref<32xf64> to memref<?xf64>
+ %view1 = memref.cast %comm : memref<32xf64> to memref<?xf64>
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %view0[%i] : memref<?xf64>
+ }
+ func.call @escape(%view1) : (memref<?xf64>) -> ()
+ affine.for %j = 0 to 16 {
+ %c = affine.load %view0[%j] : memref<?xf64>
+ %d = arith.addf %c, %c : f64
+ affine.store %d, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @escape(memref<?xf64>)
+
+// -----
+
+// An external call on a distinct memref must not block an otherwise legal
+// producer-consumer fusion.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_non_alias_call
+// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_other
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: return
+func.func @cast_alias_non_alias_call(
+ %in: memref<32xf64>, %out: memref<32xf64>) {
+ %comm = memref.alloc() : memref<32xf64>
+ %other = memref.alloc() : memref<32xf64>
+ %view = memref.cast %other : memref<32xf64> to memref<?xf64>
+ %cst = arith.constant 1.0 : f64
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %cst : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ func.call @escape_other(%view) : (memref<?xf64>) -> ()
+ affine.for %j = 0 to 16 {
+ %c = affine.load %comm[%j] : memref<32xf64>
+ affine.store %c, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @escape_other(memref<?xf64>)
diff --git a/mlir/test/Dialect/Affine/loop-fusion.mlir b/mlir/test/Dialect/Affine/loop-fusion.mlir
index 1ea42517988c3..0784e079adf5d 100644
--- a/mlir/test/Dialect/Affine/loop-fusion.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion.mlir
@@ -1574,5 +1574,44 @@ func.func @producer_consumer_with_outmost_user(%arg0 : f16) {
return
}
-// Add further tests in mlir/test/Transforms/loop-fusion-4.mlir
+// -----
+
+// Unknown operations nested in an affine loop may access their memref
+// operands. Dependence checking must handle them without assuming a load/store
+// operation class.
+// CHECK-LABEL: func @nested_unknown_call
+// CHECK: func.call @escape_nested
+// CHECK: return
+func.func @nested_unknown_call(%m: memref<8xf32>, %out: memref<8xf32>) {
+ affine.for %i = 0 to 8 {
+ func.call @escape_nested(%m) : (memref<8xf32>) -> ()
+ }
+ affine.for %i = 0 to 8 {
+ %v = affine.load %m[%i] : memref<8xf32>
+ affine.store %v, %out[%i] : memref<8xf32>
+ }
+ return
+}
+func.func private @escape_nested(memref<8xf32>)
+
+// -----
+
+// Multi-memref operations must follow the same arbitrary-operation path.
+
+// CHECK-LABEL: func @nested_memref_copy
+// CHECK: memref.copy
+// CHECK: return
+func.func @nested_memref_copy(
+ %src: memref<8xf32>, %dst: memref<8xf32>, %out: memref<8xf32>) {
+ affine.for %i = 0 to 8 {
+ memref.copy %src, %dst : memref<8xf32> to memref<8xf32>
+ }
+ affine.for %i = 0 to 8 {
+ %v = affine.load %dst[%i] : memref<8xf32>
+ affine.store %v, %out[%i] : memref<8xf32>
+ }
+ return
+}
+
+// Add further tests in mlir/test/Transforms/loop-fusion-4.mlir
>From b2345892ea76e5761d091e447d92a17a62a49e49 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 15:45:44 +0800
Subject: [PATCH 04/15] Format Affine alias handling changes
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 30 ++++++++--------
.../Dialect/Affine/Transforms/LoopFusion.cpp | 36 ++++++++++---------
.../Dialect/Affine/Utils/LoopFusionUtils.cpp | 8 ++---
3 files changed, 38 insertions(+), 36 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index bbc2fd0888d7c..ed9d2d3a49c80 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -178,8 +178,7 @@ unsigned Node::hasFree(Value memref) const {
void Node::getStoreOpsForMemref(Value memref,
SmallVectorImpl<Operation *> *storeOps) const {
for (Operation *storeOp : stores) {
- if (isSameMemref(memref,
- cast<AffineWriteOpInterface>(storeOp).getMemRef()))
+ if (isSameMemref(memref, cast<AffineWriteOpInterface>(storeOp).getMemRef()))
storeOps->push_back(storeOp);
}
}
@@ -188,8 +187,7 @@ void Node::getStoreOpsForMemref(Value memref,
void Node::getLoadOpsForMemref(Value memref,
SmallVectorImpl<Operation *> *loadOps) const {
for (Operation *loadOp : loads) {
- if (isSameMemref(memref,
- cast<AffineReadOpInterface>(loadOp).getMemRef()))
+ if (isSameMemref(memref, cast<AffineReadOpInterface>(loadOp).getMemRef()))
loadOps->push_back(loadOp);
}
}
@@ -200,12 +198,12 @@ void Node::getLoadAndStoreMemrefSet(
DenseSet<Value> *loadAndStoreMemrefSet) const {
llvm::SmallDenseSet<Value, 2> loadMemrefs;
for (Operation *loadOp : loads) {
- loadMemrefs.insert(canonicalizeMemref(
- cast<AffineReadOpInterface>(loadOp).getMemRef()));
+ loadMemrefs.insert(
+ canonicalizeMemref(cast<AffineReadOpInterface>(loadOp).getMemRef()));
}
for (Operation *storeOp : stores) {
- auto memref = canonicalizeMemref(
- cast<AffineWriteOpInterface>(storeOp).getMemRef());
+ auto memref =
+ canonicalizeMemref(cast<AffineWriteOpInterface>(storeOp).getMemRef());
if (loadMemrefs.count(memref) > 0)
loadAndStoreMemrefSet->insert(memref);
}
@@ -225,14 +223,14 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
Node &node = nodes.insert({newNodeId, Node(newNodeId, nodeOp)}).first->second;
for (Operation *op : collector.loadOpInsts) {
node.loads.push_back(op);
- auto memref = canonicalizeMemref(
- cast<AffineReadOpInterface>(op).getMemRef());
+ auto memref =
+ canonicalizeMemref(cast<AffineReadOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
}
for (Operation *op : collector.storeOpInsts) {
node.stores.push_back(op);
- auto memref = canonicalizeMemref(
- cast<AffineWriteOpInterface>(op).getMemRef());
+ auto memref =
+ canonicalizeMemref(cast<AffineWriteOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
}
for (Operation *op : collector.memrefLoads) {
@@ -382,16 +380,16 @@ bool MemRefDependenceGraph::init(bool fullAffineDependences) {
// Create graph node for top-level load op.
Node node(nextNodeId++, &op);
node.loads.push_back(&op);
- auto memref = canonicalizeMemref(
- cast<AffineReadOpInterface>(op).getMemRef());
+ auto memref =
+ canonicalizeMemref(cast<AffineReadOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
nodes.insert({node.id, node});
} else if (isa<AffineWriteOpInterface>(op)) {
// Create graph node for top-level store op.
Node node(nextNodeId++, &op);
node.stores.push_back(&op);
- auto memref = canonicalizeMemref(
- cast<AffineWriteOpInterface>(op).getMemRef());
+ auto memref =
+ canonicalizeMemref(cast<AffineWriteOpInterface>(op).getMemRef());
memrefAccesses[memref].insert(node.id);
nodes.insert({node.id, node});
} else if (op.getNumResults() > 0 && !op.use_empty()) {
diff --git a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
index 4c6b94c26a24a..c957c52504a6a 100644
--- a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
+++ b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
@@ -860,10 +860,12 @@ struct GreedyFusion {
// 1. The source is to be removed after fusion,
// OR
// 2. The destination writes to `memref`.
- if (llvm::any_of(srcEscapingMemRefs, [&](Value escapingMemref) {
- return memref::isSameViewOrTrivialAlias(
- cast<MemrefValue>(escapingMemref), cast<MemrefValue>(memref));
- }) &&
+ if (llvm::any_of(srcEscapingMemRefs,
+ [&](Value escapingMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(escapingMemref),
+ cast<MemrefValue>(memref));
+ }) &&
(removeSrcNode || consumerNode->getStoreOpCount(memref) > 0))
return false;
@@ -878,10 +880,10 @@ struct GreedyFusion {
// cannot create a private memref.
if (removeSrcNode &&
any_of(mdg->outEdges[producerId], [&](const auto &edge) {
- return edge.id != consumerId && isa<MemRefType>(edge.value.getType()) &&
- memref::isSameViewOrTrivialAlias(
- cast<MemrefValue>(edge.value),
- cast<MemrefValue>(memref));
+ return edge.id != consumerId &&
+ isa<MemRefType>(edge.value.getType()) &&
+ memref::isSameViewOrTrivialAlias(cast<MemrefValue>(edge.value),
+ cast<MemrefValue>(memref));
}))
return false;
@@ -1072,12 +1074,13 @@ struct GreedyFusion {
// Retrieve producer stores from the src loop.
SmallVector<Operation *, 2> producerStores;
for (Operation *op : srcNode->stores)
- if (llvm::any_of(producerConsumerMemrefs, [&](Value producerMemref) {
- return memref::isSameViewOrTrivialAlias(
- cast<MemrefValue>(producerMemref),
- cast<MemrefValue>(
- cast<AffineWriteOpInterface>(op).getMemRef()));
- }))
+ if (llvm::any_of(
+ producerConsumerMemrefs, [&](Value producerMemref) {
+ return memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(producerMemref),
+ cast<MemrefValue>(
+ cast<AffineWriteOpInterface>(op).getMemRef()));
+ }))
producerStores.push_back(op);
assert(!producerStores.empty() && "Expected producer store");
@@ -1416,8 +1419,9 @@ struct GreedyFusion {
// Check that all stores are to the same memref if any.
DenseSet<Value> storeMemrefs;
for (auto *storeOpInst : sibNode->stores) {
- storeMemrefs.insert(memref::skipFullyAliasingOperations(cast<MemrefValue>(
- cast<AffineWriteOpInterface>(storeOpInst).getMemRef())));
+ storeMemrefs.insert(
+ memref::skipFullyAliasingOperations(cast<MemrefValue>(
+ cast<AffineWriteOpInterface>(storeOpInst).getMemRef())));
}
return storeMemrefs.size() <= 1;
};
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index 4e4f8fd696ffd..c34bfe40da89e 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -355,10 +355,10 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
// to 'memref' in 'srcForOp' to compute the slice union.
for (Operation *op : opsA) {
auto load = dyn_cast<AffineReadOpInterface>(op);
- if (load && memref::isSameViewOrTrivialAlias(
- cast<MemrefValue>(load.getMemRef()),
- cast<MemrefValue>(
- fusionStrategy.getSiblingFusionMemRef())))
+ if (load &&
+ memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(load.getMemRef()),
+ cast<MemrefValue>(fusionStrategy.getSiblingFusionMemRef())))
strategyOpsA.push_back(op);
}
break;
>From 03d0fdee46a86513d4fcb395fb18c854486670c6 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 15:57:47 +0800
Subject: [PATCH 05/15] [MLIR][Affine] Preserve memref SSA edge identity
---
.../mlir/Dialect/Affine/Analysis/Utils.h | 14 +++++++-----
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 13 ++++++-----
.../Dialect/Affine/Utils/LoopFusionUtils.cpp | 22 +++++++++++++++++++
3 files changed, 37 insertions(+), 12 deletions(-)
diff --git a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
index 6a03fabd274fd..8c95630edcbf1 100644
--- a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
+++ b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
@@ -128,12 +128,14 @@ struct MemRefDependenceGraph {
// 'Node.outEdges[i].id' is the identifier of the dest node of the edge.
unsigned id;
// The SSA value on which this edge represents a dependence.
- // If the value is a memref, then it is the canonical representative of
- // the trivial alias class on which the dependence is based. If the value
- // is a non-memref value, then the dependence is between a graph node which
- // defines an SSA value and another graph node which uses the SSA value
- // (e.g. a constant or load operation defining a value which is used inside
- // a loop nest).
+ // If the value is a memref and this is a memory dependence, then it is the
+ // canonical representative of the trivial alias class on which the
+ // dependence is based. Memref SSA dependences retain the defining value so
+ // that the defining operation remains observable to graph clients. If the
+ // value is a non-memref value, then the dependence is between a graph node
+ // which defines an SSA value and another graph node which uses the SSA
+ // value (e.g. a constant or load operation defining a value which is used
+ // inside a loop nest).
Value value;
};
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index ed9d2d3a49c80..d061a7d0da2f0 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -550,7 +550,6 @@ bool MemRefDependenceGraph::hasEdge(unsigned srcId, unsigned dstId,
if (!outEdges.contains(srcId) || !inEdges.contains(dstId)) {
return false;
}
- value = canonicalizeMemref(value);
bool hasOutEdge = llvm::any_of(outEdges.lookup(srcId), [=](const Edge &edge) {
return edge.id == dstId && (!value || edge.value == value);
});
@@ -563,12 +562,14 @@ bool MemRefDependenceGraph::hasEdge(unsigned srcId, unsigned dstId,
// Adds an edge from node 'srcId' to node 'dstId' for 'value'.
void MemRefDependenceGraph::addEdge(unsigned srcId, unsigned dstId,
Value value) {
- value = canonicalizeMemref(value);
+ // Keep memref SSA edges in their raw form so their defining operation stays
+ // available to graph clients. Memory-dependence callers provide the
+ // canonical representative; count both kinds by canonical identity.
if (!hasEdge(srcId, dstId, value)) {
outEdges[srcId].push_back({dstId, value});
inEdges[dstId].push_back({srcId, value});
if (isa<MemRefType>(value.getType()))
- memrefEdgeCount[value]++;
+ memrefEdgeCount[canonicalizeMemref(value)]++;
}
}
@@ -577,10 +578,10 @@ void MemRefDependenceGraph::removeEdge(unsigned srcId, unsigned dstId,
Value value) {
assert(inEdges.count(dstId) > 0);
assert(outEdges.count(srcId) > 0);
- value = canonicalizeMemref(value);
if (isa<MemRefType>(value.getType())) {
- assert(memrefEdgeCount.count(value) > 0);
- memrefEdgeCount[value]--;
+ Value canonicalValue = canonicalizeMemref(value);
+ assert(memrefEdgeCount.count(canonicalValue) > 0);
+ memrefEdgeCount[canonicalValue]--;
}
// Remove 'srcId' from 'inEdges[dstId]'.
for (auto *it = inEdges[dstId].begin(); it != inEdges[dstId].end(); ++it) {
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index c34bfe40da89e..1bfc3af59fd03 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -364,6 +364,28 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
break;
}
+ // Affine access maps are expressed in the raw view coordinates. Reject a
+ // fusion that would pair different views from one trivial alias class until
+ // a coordinate translation is available.
+ auto getMemref = [](Operation *op) -> Value {
+ if (auto load = dyn_cast<AffineReadOpInterface>(op))
+ return load.getMemRef();
+ return cast<AffineWriteOpInterface>(op).getMemRef();
+ };
+ if (llvm::any_of(strategyOpsA, [&](Operation *srcOp) {
+ return llvm::any_of(opsB, [&](Operation *dstOp) {
+ Value srcMemref = getMemref(srcOp);
+ Value dstMemref = getMemref(dstOp);
+ return srcMemref != dstMemref &&
+ memref::isSameViewOrTrivialAlias(
+ cast<MemrefValue>(srcMemref),
+ cast<MemrefValue>(dstMemref));
+ });
+ })) {
+ LDBG() << "Fusion across different trivial alias views is unsupported";
+ return FusionResult::FailFusionDependence;
+ }
+
// Compute union of computation slices computed between all pairs of ops
// from 'forOpA' and 'forOpB'.
SliceComputationResult sliceComputationResult = affine::computeSliceUnion(
>From 43d392a12e9322d3f214146715cc7d5ae7bcccb1 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 16:00:42 +0800
Subject: [PATCH 06/15] Format alias view guard
---
mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index 1bfc3af59fd03..e51b9ff5dea27 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -377,9 +377,8 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
Value srcMemref = getMemref(srcOp);
Value dstMemref = getMemref(dstOp);
return srcMemref != dstMemref &&
- memref::isSameViewOrTrivialAlias(
- cast<MemrefValue>(srcMemref),
- cast<MemrefValue>(dstMemref));
+ memref::isSameViewOrTrivialAlias(cast<MemrefValue>(srcMemref),
+ cast<MemrefValue>(dstMemref));
});
})) {
LDBG() << "Fusion across different trivial alias views is unsupported";
>From 9d59f1c77ca4e79e69d035c0f15ae49d7fc15775 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 18:46:57 +0800
Subject: [PATCH 07/15] [MLIR][Affine] Handle unranked memref aliases
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 22 +--
.../Dialect/Affine/Transforms/LoopFusion.cpp | 2 +-
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 125 ++++++++++++++++++
mlir/test/Dialect/Affine/loop-fusion.mlir | 10 +-
4 files changed, 145 insertions(+), 14 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index d061a7d0da2f0..97adf11743321 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -40,7 +40,7 @@ using llvm::SmallDenseMap;
using Node = MemRefDependenceGraph::Node;
static Value canonicalizeMemref(Value value) {
- if (!value || !isa<MemRefType>(value.getType()))
+ if (!value || !isa<BaseMemRefType>(value.getType()))
return value;
return memref::skipFullyAliasingOperations(cast<MemrefValue>(value));
}
@@ -64,7 +64,7 @@ static void getMayAffectedValues(Operation *op,
return;
// Memref operands have to be considered as being affected.
for (Value operand : op->getOperands()) {
- if (isa<MemRefType>(operand.getType()))
+ if (isa<BaseMemRefType>(operand.getType()))
values.push_back(canonicalizeMemref(operand));
}
return;
@@ -74,7 +74,7 @@ static void getMayAffectedValues(Operation *op,
for (auto &effect : effects) {
Value effectVal = effect.getValue();
if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
- isa<MemRefType>(effectVal.getType()))
+ isa<BaseMemRefType>(effectVal.getType()))
values.push_back(canonicalizeMemref(effectVal));
};
}
@@ -108,7 +108,7 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
return;
// Check operands. E.g., ops like the `call` op are handled here.
if (llvm::any_of(op->getOperands(), [](Value value) {
- return isa<MemRefType>(value.getType());
+ return isa<BaseMemRefType>(value.getType());
})) {
// Conservatively, assume all memref operands are read and written.
memrefLoads.push_back(op);
@@ -237,7 +237,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
SmallVector<Value> affectedValues;
getMayAffectedValues<MemoryEffects::Read>(op, affectedValues);
if (llvm::any_of(((ValueRange)affectedValues).getTypes(),
- [](Type type) { return !isa<MemRefType>(type); }))
+ [](Type type) { return !isa<BaseMemRefType>(type); }))
// We do not know the interaction here.
return nullptr;
for (Value memref : affectedValues)
@@ -248,7 +248,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
SmallVector<Value> affectedValues;
getMayAffectedValues<MemoryEffects::Write>(op, affectedValues);
if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
- [](Type type) { return !isa<MemRefType>(type); }))
+ [](Type type) { return !isa<BaseMemRefType>(type); }))
return nullptr;
for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
@@ -258,7 +258,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
SmallVector<Value> affectedValues;
getMayAffectedValues<MemoryEffects::Free>(op, affectedValues);
if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
- [](Type type) { return !isa<MemRefType>(type); }))
+ [](Type type) { return !isa<BaseMemRefType>(type); }))
return nullptr;
for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
@@ -568,7 +568,7 @@ void MemRefDependenceGraph::addEdge(unsigned srcId, unsigned dstId,
if (!hasEdge(srcId, dstId, value)) {
outEdges[srcId].push_back({dstId, value});
inEdges[dstId].push_back({srcId, value});
- if (isa<MemRefType>(value.getType()))
+ if (isa<BaseMemRefType>(value.getType()))
memrefEdgeCount[canonicalizeMemref(value)]++;
}
}
@@ -578,7 +578,7 @@ void MemRefDependenceGraph::removeEdge(unsigned srcId, unsigned dstId,
Value value) {
assert(inEdges.count(dstId) > 0);
assert(outEdges.count(srcId) > 0);
- if (isa<MemRefType>(value.getType())) {
+ if (isa<BaseMemRefType>(value.getType())) {
Value canonicalValue = canonicalizeMemref(value);
assert(memrefEdgeCount.count(canonicalValue) > 0);
memrefEdgeCount[canonicalValue]--;
@@ -669,7 +669,7 @@ void MemRefDependenceGraph::gatherDefiningNodes(
// By definition of edge, if the edge value is a non-memref value,
// then the dependence is between a graph node which defines an SSA value
// and another graph node which uses the SSA value.
- if (!isa<MemRefType>(edge.value.getType()))
+ if (!isa<BaseMemRefType>(edge.value.getType()))
definingNodes.insert(edge.id);
}
@@ -861,7 +861,7 @@ void MemRefDependenceGraph::forEachMemRefEdge(
ArrayRef<Edge> edges, const std::function<void(Edge)> &callback) {
for (const auto &edge : edges) {
// Skip if 'edge' is not a memref dependence edge.
- if (!isa<MemRefType>(edge.value.getType()))
+ if (!isa<BaseMemRefType>(edge.value.getType()))
continue;
assert(nodes.count(edge.id) > 0);
// Visit current input edge 'edge'.
diff --git a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
index c957c52504a6a..a22a6639fd314 100644
--- a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
+++ b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
@@ -881,7 +881,7 @@ struct GreedyFusion {
if (removeSrcNode &&
any_of(mdg->outEdges[producerId], [&](const auto &edge) {
return edge.id != consumerId &&
- isa<MemRefType>(edge.value.getType()) &&
+ isa<BaseMemRefType>(edge.value.getType()) &&
memref::isSameViewOrTrivialAlias(cast<MemrefValue>(edge.value),
cast<MemrefValue>(memref));
}))
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 471e2f4a9bb64..600c1d1d484db 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -924,6 +924,66 @@ func.func private @escape(memref<?xf64>)
// -----
+// A ranked source cast to an unranked memref must retain the dependence on the
+// source used by the affine producer and consumer.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_ranked_to_unranked
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+// PRODUCER-CONSUMER-MAXIMAL: memref.cast
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_unranked
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+func.func @cast_alias_ranked_to_unranked(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ %view = memref.cast %comm : memref<32xf64> to memref<*xf64>
+ func.call @escape_unranked(%view) : (memref<*xf64>) -> ()
+ 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
+}
+func.func private @escape_unranked(memref<*xf64>)
+
+// -----
+
+// An unranked source cast to a ranked memref must retain the dependence on the
+// source passed to the opaque call.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_unranked_to_ranked
+// PRODUCER-CONSUMER-MAXIMAL: memref.cast
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_ranked
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+func.func @cast_alias_unranked_to_ranked(
+ %in: memref<32xf64>, %comm: memref<*xf64>, %out: memref<32xf64>) {
+ %view = memref.cast %comm : memref<*xf64> to memref<32xf64>
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %view[%i] : memref<32xf64>
+ }
+ func.call @escape_ranked(%comm) : (memref<*xf64>) -> ()
+ affine.for %j = 0 to 16 {
+ %c = affine.load %view[%j] : memref<32xf64>
+ %d = arith.addf %c, %c : f64
+ affine.store %d, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @escape_ranked(memref<*xf64>)
+
+// -----
+
// Affine accesses may use the fully aliasing view while the external call uses
// the source value. The call must remain between the two loop nests.
@@ -1009,3 +1069,68 @@ func.func @cast_alias_non_alias_call(
return
}
func.func private @escape_other(memref<?xf64>)
+
+// -----
+
+// A zero-offset, unit-stride subview is a fully aliasing view and must retain
+// the same dependence as its source memref.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @subview_alias_external_call
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+// PRODUCER-CONSUMER-MAXIMAL: memref.subview
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_subview
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+func.func @subview_alias_external_call(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ %view = memref.subview %comm[0] [32] [1]
+ : memref<32xf64> to memref<32xf64, strided<[1], offset: 0>>
+ func.call @escape_subview(%view)
+ : (memref<32xf64, strided<[1], offset: 0>>) -> ()
+ 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
+}
+func.func private @escape_subview(memref<32xf64, strided<[1], offset: 0>>)
+
+// -----
+
+// A non-fully-aliasing subview of an unrelated memref must not block an
+// otherwise legal producer-consumer fusion.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @subview_non_alias_call
+// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: call @escape_subview_non_alias
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+// PRODUCER-CONSUMER-MAXIMAL: return
+// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
+func.func @subview_non_alias_call(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ %other = memref.alloc() : memref<32xf64>
+ %view = memref.subview %other[1] [16] [1]
+ : memref<32xf64> to memref<16xf64, strided<[1], offset: 1>>
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ func.call @escape_subview_non_alias(%view)
+ : (memref<16xf64, strided<[1], offset: 1>>) -> ()
+ affine.for %j = 0 to 16 {
+ %c = affine.load %comm[%j] : memref<32xf64>
+ affine.store %c, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @escape_subview_non_alias(
+ memref<16xf64, strided<[1], offset: 1>>)
diff --git a/mlir/test/Dialect/Affine/loop-fusion.mlir b/mlir/test/Dialect/Affine/loop-fusion.mlir
index 0784e079adf5d..96a8f0c6218ee 100644
--- a/mlir/test/Dialect/Affine/loop-fusion.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion.mlir
@@ -1581,7 +1581,10 @@ func.func @producer_consumer_with_outmost_user(%arg0 : f16) {
// operation class.
// CHECK-LABEL: func @nested_unknown_call
-// CHECK: func.call @escape_nested
+// CHECK: affine.for
+// CHECK: func.call @escape_nested
+// CHECK: affine.for
+// CHECK: affine.load
// CHECK: return
func.func @nested_unknown_call(%m: memref<8xf32>, %out: memref<8xf32>) {
affine.for %i = 0 to 8 {
@@ -1600,7 +1603,10 @@ func.func private @escape_nested(memref<8xf32>)
// Multi-memref operations must follow the same arbitrary-operation path.
// CHECK-LABEL: func @nested_memref_copy
-// CHECK: memref.copy
+// CHECK: affine.for
+// CHECK: memref.copy
+// CHECK: affine.for
+// CHECK: affine.load
// CHECK: return
func.func @nested_memref_copy(
%src: memref<8xf32>, %dst: memref<8xf32>, %out: memref<8xf32>) {
>From 8c35ea405bed0740549e20cb364c8c67c03a36f0 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sat, 1 Aug 2026 21:41:51 +0800
Subject: [PATCH 08/15] [MLIR][Affine] Preserve addressable memory effects
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 47 +++++++++-----
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 70 +++++++++++++++++++++
2 files changed, 100 insertions(+), 17 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 97adf11743321..d0a4710e359e2 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -54,38 +54,58 @@ static bool isSameMemref(Value lhs, Value rhs) {
/// (e.g. a call to an external function without a memory-effect interface) is
/// conservatively assumed to affect all its memref operands. Fully aliasing
/// views are canonicalized so the MDG uses one key for the view and its source.
+/// Returns false if an addressable effect cannot be represented by a memref
+/// value, in which case the MDG must not be used for fusion.
template <typename... EffectTys>
-static void getMayAffectedValues(Operation *op,
+static bool getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
if (!memOp) {
if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
// No effects.
- return;
+ return true;
// Memref operands have to be considered as being affected.
for (Value operand : op->getOperands()) {
if (isa<BaseMemRefType>(operand.getType()))
values.push_back(canonicalizeMemref(operand));
}
- return;
+ return true;
}
SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
memOp.getEffects(effects);
for (auto &effect : effects) {
+ if (!isa<EffectTys...>(effect.getEffect()))
+ continue;
Value effectVal = effect.getValue();
- if (isa<EffectTys...>(effect.getEffect()) && effectVal &&
- isa<BaseMemRefType>(effectVal.getType()))
+ if (!effectVal) {
+ // A value-less or symbol-associated effect on an addressable resource
+ // cannot be represented by a per-memref graph edge. Refuse to fuse the
+ // block rather than silently dropping the effect.
+ if (effect.getResource()->isAddressable())
+ return false;
+ continue;
+ }
+ if (isa<BaseMemRefType>(effectVal.getType())) {
values.push_back(canonicalizeMemref(effectVal));
+ continue;
+ }
+ // An addressable effect on a non-memref value (for example, a pointer) is
+ // equally unrepresentable by the memref dependence graph.
+ if (effect.getResource()->isAddressable())
+ return false;
};
+ return true;
}
/// Returns true if `op` may have a memory effect of type `EffectTys` on
/// `memref`, i.e., whether `memref` is among the values returned by
-/// `getMayAffectedValues` for `op`.
+/// `getMayAffectedValues` for `op`. An unrepresentable addressable effect is
+/// conservatively treated as affecting every memref.
template <typename... EffectTys>
static bool mayHaveEffect(Operation *op, Value memref) {
SmallVector<Value> values;
- getMayAffectedValues<EffectTys...>(op, values);
+ if (!getMayAffectedValues<EffectTys...>(op, values))
+ return true;
return llvm::is_contained(values, canonicalizeMemref(memref));
}
@@ -235,10 +255,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefLoads) {
SmallVector<Value> affectedValues;
- getMayAffectedValues<MemoryEffects::Read>(op, affectedValues);
- if (llvm::any_of(((ValueRange)affectedValues).getTypes(),
- [](Type type) { return !isa<BaseMemRefType>(type); }))
- // We do not know the interaction here.
+ if (!getMayAffectedValues<MemoryEffects::Read>(op, affectedValues))
return nullptr;
for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
@@ -246,9 +263,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefStores) {
SmallVector<Value> affectedValues;
- getMayAffectedValues<MemoryEffects::Write>(op, affectedValues);
- if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
- [](Type type) { return !isa<BaseMemRefType>(type); }))
+ if (!getMayAffectedValues<MemoryEffects::Write>(op, affectedValues))
return nullptr;
for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
@@ -256,9 +271,7 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
}
for (Operation *op : collector.memrefFrees) {
SmallVector<Value> affectedValues;
- getMayAffectedValues<MemoryEffects::Free>(op, affectedValues);
- if (llvm::any_of((ValueRange(affectedValues)).getTypes(),
- [](Type type) { return !isa<BaseMemRefType>(type); }))
+ if (!getMayAffectedValues<MemoryEffects::Free>(op, affectedValues))
return nullptr;
for (Value memref : affectedValues)
memrefAccesses[memref].insert(node.id);
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 600c1d1d484db..469f9088e5017 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1134,3 +1134,73 @@ func.func @subview_non_alias_call(
}
func.func private @escape_subview_non_alias(
memref<16xf64, strided<[1], offset: 1>>)
+
+// -----
+
+// An addressable effect without an SSA memory value cannot be represented by
+// the per-memref dependence graph. Fusion must be skipped for the block.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_addressable_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @value_less_addressable_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ }
+ %effect = "test.side_effect_op"() {effects = [{effect = "write"}]} : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// A symbol-associated addressable effect is also not representable by a
+// per-memref dependence graph. Fusion must be skipped for the block.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @symbol_addressable_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @symbol_addressable_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ }
+ "test.side_effect_op"() {
+ effects = [{effect = "write", on_reference = @effect_target}]
+ } : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ }
+ return
+}
+func.func private @effect_target()
+
+// A value-less effect on a non-addressable resource is disjoint from memref
+// accesses, so the otherwise legal producer-consumer fusion remains enabled.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_nonaddressable_effect
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
+func.func @value_less_nonaddressable_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ }
+ %effect = "test.side_effect_op"() {
+ effects = [{effect = "write", test_nonaddressable_resource}]
+ } : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ }
+ return
+}
>From 19f57849890548c75474c79b174bb07bac850ea4 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 10:03:41 +0800
Subject: [PATCH 09/15] [MLIR][Affine] Preserve unrepresentable memory effects
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 29 +++----
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 92 +++++++++++++++++++--
2 files changed, 96 insertions(+), 25 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index d0a4710e359e2..c4dcf258ba928 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -54,8 +54,9 @@ static bool isSameMemref(Value lhs, Value rhs) {
/// (e.g. a call to an external function without a memory-effect interface) is
/// conservatively assumed to affect all its memref operands. Fully aliasing
/// views are canonicalized so the MDG uses one key for the view and its source.
-/// Returns false if an addressable effect cannot be represented by a memref
-/// value, in which case the MDG must not be used for fusion.
+/// Returns false if a selected effect cannot be represented by a memref value.
+/// The MDG has no resource-level edges, so dropping such an effect would make
+/// fusion unsound even when its resource is non-addressable.
template <typename... EffectTys>
static bool getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
@@ -77,29 +78,16 @@ static bool getMayAffectedValues(Operation *op,
if (!isa<EffectTys...>(effect.getEffect()))
continue;
Value effectVal = effect.getValue();
- if (!effectVal) {
- // A value-less or symbol-associated effect on an addressable resource
- // cannot be represented by a per-memref graph edge. Refuse to fuse the
- // block rather than silently dropping the effect.
- if (effect.getResource()->isAddressable())
- return false;
- continue;
- }
- if (isa<BaseMemRefType>(effectVal.getType())) {
- values.push_back(canonicalizeMemref(effectVal));
- continue;
- }
- // An addressable effect on a non-memref value (for example, a pointer) is
- // equally unrepresentable by the memref dependence graph.
- if (effect.getResource()->isAddressable())
+ if (!effectVal || !isa<BaseMemRefType>(effectVal.getType()))
return false;
+ values.push_back(canonicalizeMemref(effectVal));
};
return true;
}
/// Returns true if `op` may have a memory effect of type `EffectTys` on
/// `memref`, i.e., whether `memref` is among the values returned by
-/// `getMayAffectedValues` for `op`. An unrepresentable addressable effect is
+/// `getMayAffectedValues` for `op`. An unrepresentable effect is
/// conservatively treated as affecting every memref.
template <typename... EffectTys>
static bool mayHaveEffect(Operation *op, Value memref) {
@@ -135,7 +123,10 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
memrefStores.push_back(op);
}
} else {
- // Non-affine loads and stores.
+ // Non-affine loads, stores, and frees. Allocation effects are
+ // intentionally omitted: they do not access existing memory, and
+ // allocation results are handled by existing SSA and local-allocation
+ // analysis instead of the memref access graph.
if (hasEffect<MemoryEffects::Read>(op))
memrefLoads.push_back(op);
if (hasEffect<MemoryEffects::Write>(op))
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 469f9088e5017..01967ad414335 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1182,21 +1182,101 @@ func.func @symbol_addressable_effect(
}
func.func private @effect_target()
-// A value-less effect on a non-addressable resource is disjoint from memref
-// accesses, so the otherwise legal producer-consumer fusion remains enabled.
+// An addressable effect on a non-memref SSA value is not representable by the
+// per-memref dependence graph. Fusion must be skipped for the block.
-// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_nonaddressable_effect
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_addressable_nonmemref_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
// PRODUCER-CONSUMER-MAXIMAL: affine.for
-// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
-func.func @value_less_nonaddressable_effect(
+func.func @value_addressable_nonmemref_effect(
%in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
affine.for %i = 0 to 16 {
%a = affine.load %in[%i] : memref<32xf64>
affine.store %a, %comm[%i] : memref<32xf64>
}
%effect = "test.side_effect_op"() {
- effects = [{effect = "write", test_nonaddressable_resource}]
+ effects = [{effect = "write", on_result}]
+ } : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// A value-less effect on a non-addressable resource cannot be represented by
+// the per-memref dependence graph. Effects on the same resource must still be
+// ordered, so fusion must be skipped for the block.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_nonaddressable_write_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @value_less_nonaddressable_write_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ "test.side_effect_op"() {
+ effects = [{effect = "write", test_nonaddressable_resource}]
+ } : () -> i32
+ }
+ "test.side_effect_op"() {
+ effects = [{effect = "read", test_nonaddressable_resource}]
+ } : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// Value-less reads on a non-addressable resource are also unrepresentable.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_nonaddressable_read_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+func.func @value_less_nonaddressable_read_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ }
+ "test.side_effect_op"() {
+ effects = [{effect = "read", test_nonaddressable_resource}]
+ } : () -> i32
+ affine.for %j = 0 to 16 {
+ %a = affine.load %comm[%j] : memref<32xf64>
+ affine.store %a, %out[%j] : memref<32xf64>
+ "test.side_effect_op"() {
+ effects = [{effect = "write", test_nonaddressable_resource}]
+ } : () -> i32
+ }
+ return
+}
+
+// Free effects follow the same conservative rule.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @value_less_nonaddressable_free_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: test.side_effect_op
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @value_less_nonaddressable_free_effect(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ affine.store %a, %comm[%i] : memref<32xf64>
+ "test.side_effect_op"() {
+ effects = [{effect = "free", test_nonaddressable_resource}]
+ } : () -> i32
+ }
+ "test.side_effect_op"() {
+ effects = [{effect = "read", test_nonaddressable_resource}]
} : () -> i32
affine.for %j = 0 to 16 {
%a = affine.load %comm[%j] : memref<32xf64>
>From 6f2aaefb86d23be48a3ddeacb0f6b3f2f2d45b98 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 17:31:30 +0800
Subject: [PATCH 10/15] [MLIR][Affine] Fail closed on incomplete fusion effects
---
.../mlir/Dialect/Affine/Analysis/Utils.h | 7 +-
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 57 ++++----
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 136 ++++++++++++------
mlir/test/Dialect/Affine/loop-fusion.mlir | 6 +-
4 files changed, 128 insertions(+), 78 deletions(-)
diff --git a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
index 8c95630edcbf1..f78fe772d77e9 100644
--- a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
+++ b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
@@ -129,7 +129,7 @@ struct MemRefDependenceGraph {
unsigned id;
// The SSA value on which this edge represents a dependence.
// If the value is a memref and this is a memory dependence, then it is the
- // canonical representative of the trivial alias class on which the
+ // canonical representative of the view-like storage class on which the
// dependence is based. Memref SSA dependences retain the defining value so
// that the defining operation remains observable to graph clients. If the
// value is a non-memref value, then the dependence is between a graph node
@@ -160,8 +160,9 @@ struct MemRefDependenceGraph {
// side-effect-free operations with zero results and no regions. Assigns each
// node in the graph a node id based on the order in block. Fails if certain
// kinds of operations, for which `Node` creation isn't supported, are
- // encountered (unknown region holding ops). If `fullAffineDependences` is
- // set, affine memory dependence analysis is performed before concluding that
+ // encountered (unknown effects or region holding ops). If
+ // `fullAffineDependences` is set, affine memory dependence analysis is
+ // performed before concluding that
// conflicting affine memory accesses lead to a dependence check; otherwise, a
// pair of conflicting affine memory accesses (where one of them is a store
// and they are to the same memref) always leads to an edge (conservatively).
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index c4dcf258ba928..3a731193bd887 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -42,7 +42,10 @@ using Node = MemRefDependenceGraph::Node;
static Value canonicalizeMemref(Value value) {
if (!value || !isa<BaseMemRefType>(value.getType()))
return value;
- return memref::skipFullyAliasingOperations(cast<MemrefValue>(value));
+ // Use the storage source for graph identity. Keep the original view in
+ // MemRefAccess so affine maps remain expressed in their original
+ // coordinate systems.
+ return memref::skipViewLikeOps(cast<MemrefValue>(value));
}
static bool isSameMemref(Value lhs, Value rhs) {
@@ -50,28 +53,19 @@ static bool isSameMemref(Value lhs, Value rhs) {
}
/// Returns the values that `op` may have a memref effect of type `EffectTys`
-/// on, not considering recursive effects. An op with unknown memory effects
-/// (e.g. a call to an external function without a memory-effect interface) is
-/// conservatively assumed to affect all its memref operands. Fully aliasing
-/// views are canonicalized so the MDG uses one key for the view and its source.
+/// on, not considering recursive effects. View-like values are canonicalized
+/// to their storage source so the MDG uses one key for a view chain while raw
+/// views remain available for affine-coordinate analysis. Unknown operations
+/// cannot be represented by the memref-keyed graph, so return false for them.
/// Returns false if a selected effect cannot be represented by a memref value.
-/// The MDG has no resource-level edges, so dropping such an effect would make
-/// fusion unsound even when its resource is non-addressable.
+/// The MDG has no resource-level or all-memory edges, so dropping such an
+/// effect would make fusion unsound.
template <typename... EffectTys>
static bool getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
- if (!memOp) {
- if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
- // No effects.
- return true;
- // Memref operands have to be considered as being affected.
- for (Value operand : op->getOperands()) {
- if (isa<BaseMemRefType>(operand.getType()))
- values.push_back(canonicalizeMemref(operand));
- }
- return true;
- }
+ if (!memOp)
+ return !hasUnknownEffects(op);
SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
memOp.getEffects(effects);
for (auto &effect : effects) {
@@ -111,17 +105,12 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
} else {
auto memInterface = dyn_cast<MemoryEffectOpInterface>(op);
if (!memInterface) {
- if (op->hasTrait<OpTrait::HasRecursiveMemoryEffects>())
- // This op itself is memory-effect free.
+ if (!hasUnknownEffects(op))
return;
- // Check operands. E.g., ops like the `call` op are handled here.
- if (llvm::any_of(op->getOperands(), [](Value value) {
- return isa<BaseMemRefType>(value.getType());
- })) {
- // Conservatively, assume all memref operands are read and written.
- memrefLoads.push_back(op);
- memrefStores.push_back(op);
- }
+ // Unknown effects may reach memory through globals or other state not
+ // represented by SSA memref operands. Keep the graph fail-closed.
+ memrefLoads.push_back(op);
+ memrefStores.push_back(op);
} else {
// Non-affine loads, stores, and frees. Allocation effects are
// intentionally omitted: they do not access existing memory, and
@@ -272,9 +261,9 @@ addNodeToMDG(Operation *nodeOp, MemRefDependenceGraph &mdg,
return &node;
}
-/// Returns true if `op` may access `memref`, including through a fully aliasing
-/// view. Unknown operations are handled conservatively through their memory
-/// effects rather than assuming a particular operation class.
+/// Returns true if `op` may access `memref`, including through a view-like
+/// operation. Unknown operations are handled conservatively through their
+/// memory effects rather than assuming a particular operation class.
static bool mayAccessMemRef(Operation *op, Value memref) {
if (auto affineRead = dyn_cast<AffineReadOpInterface>(op))
return isSameMemref(affineRead.getMemRef(), memref);
@@ -295,6 +284,10 @@ static bool mayDependence(const Node &srcNode, const Node &dstNode,
assert(srcNode.op->getBlock() == dstNode.op->getBlock());
if (!isa<AffineForOp>(srcNode.op) || !isa<AffineForOp>(dstNode.op))
return true;
+ // Deallocation invalidates the whole storage object. Affine access
+ // relations cannot prove a free harmless by comparing indexed accesses.
+ if (srcNode.hasFree(memref) || dstNode.hasFree(memref))
+ return true;
// Conservatively handle dependences involving non-affine load/stores. Return
// true if there exists a conflicting read/write access involving such.
@@ -369,7 +362,7 @@ static bool mayDependence(const Node &srcNode, const Node &dstNode,
bool MemRefDependenceGraph::init(bool fullAffineDependences) {
LDBG() << "--- Initializing MDG ---";
// Map from a memref to the set of ids of the nodes that have ops accessing
- // the memref. Fully aliasing views use their canonical source value here.
+ // the memref. View-like values use their canonical storage source here.
DenseMap<Value, SetVector<unsigned>> memrefAccesses;
// Create graph nodes.
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 01967ad414335..fc82b34f870bf 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1041,37 +1041,6 @@ func.func private @escape(memref<?xf64>)
// -----
-// An external call on a distinct memref must not block an otherwise legal
-// producer-consumer fusion.
-
-// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @cast_alias_non_alias_call
-// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
-// PRODUCER-CONSUMER-MAXIMAL: call @escape_other
-// PRODUCER-CONSUMER-MAXIMAL: affine.for
-// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
-// PRODUCER-CONSUMER-MAXIMAL: return
-func.func @cast_alias_non_alias_call(
- %in: memref<32xf64>, %out: memref<32xf64>) {
- %comm = memref.alloc() : memref<32xf64>
- %other = memref.alloc() : memref<32xf64>
- %view = memref.cast %other : memref<32xf64> to memref<?xf64>
- %cst = arith.constant 1.0 : f64
- affine.for %i = 0 to 16 {
- %a = affine.load %in[%i] : memref<32xf64>
- %b = arith.addf %a, %cst : f64
- affine.store %b, %comm[%i] : memref<32xf64>
- }
- func.call @escape_other(%view) : (memref<?xf64>) -> ()
- affine.for %j = 0 to 16 {
- %c = affine.load %comm[%j] : memref<32xf64>
- affine.store %c, %out[%j] : memref<32xf64>
- }
- return
-}
-func.func private @escape_other(memref<?xf64>)
-
-// -----
-
// A zero-offset, unit-stride subview is a fully aliasing view and must retain
// the same dependence as its source memref.
@@ -1104,36 +1073,123 @@ func.func private @escape_subview(memref<32xf64, strided<[1], offset: 0>>)
// -----
-// A non-fully-aliasing subview of an unrelated memref must not block an
-// otherwise legal producer-consumer fusion.
+// A non-zero-offset subview of the producer's storage must retain a
+// dependence even though its affine coordinates use a different view.
-// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @subview_non_alias_call
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @subview_overlap_memref_store
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: memref.subview
+// PRODUCER-CONSUMER-MAXIMAL: memref.store
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @subview_overlap_memref_store(
+ %in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant 1.0 : f64
+ affine.for %i = 0 to 16 {
+ %a = affine.load %in[%i] : memref<32xf64>
+ %b = arith.addf %a, %a : f64
+ affine.store %b, %comm[%i] : memref<32xf64>
+ }
+ %view = memref.subview %comm[1] [16] [1]
+ : memref<32xf64> to memref<16xf64, strided<[1], offset: 1>>
+ memref.store %cst, %view[%c0]
+ : memref<16xf64, strided<[1], offset: 1>>
+ affine.for %j = 0 to 16 {
+ %c = affine.load %comm[%j] : memref<32xf64>
+ affine.store %c, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// -----
+
+// A non-zero-offset subview of unrelated storage must not block an otherwise
+// legal producer-consumer fusion.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @subview_non_alias_memref_store
// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
-// PRODUCER-CONSUMER-MAXIMAL: call @escape_subview_non_alias
+// PRODUCER-CONSUMER-MAXIMAL: memref.store
// PRODUCER-CONSUMER-MAXIMAL: affine.for
// PRODUCER-CONSUMER-MAXIMAL: affine.load
// PRODUCER-CONSUMER-MAXIMAL: return
// PRODUCER-CONSUMER-MAXIMAL-NOT: affine.for
-func.func @subview_non_alias_call(
+func.func @subview_non_alias_memref_store(
%in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
%other = memref.alloc() : memref<32xf64>
%view = memref.subview %other[1] [16] [1]
: memref<32xf64> to memref<16xf64, strided<[1], offset: 1>>
+ %c0 = arith.constant 0 : index
+ %cst = arith.constant 1.0 : f64
affine.for %i = 0 to 16 {
%a = affine.load %in[%i] : memref<32xf64>
%b = arith.addf %a, %a : f64
affine.store %b, %comm[%i] : memref<32xf64>
}
- func.call @escape_subview_non_alias(%view)
- : (memref<16xf64, strided<[1], offset: 1>>) -> ()
+ memref.store %cst, %view[%c0]
+ : memref<16xf64, strided<[1], offset: 1>>
affine.for %j = 0 to 16 {
%c = affine.load %comm[%j] : memref<32xf64>
affine.store %c, %out[%j] : memref<32xf64>
}
return
}
-func.func private @escape_subview_non_alias(
- memref<16xf64, strided<[1], offset: 1>>)
+
+// -----
+
+// A represented free must remain a dependence through full affine filtering.
+// Otherwise fusing the first and third loops would move a store to %a past
+// its deallocation.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @representable_free_between_loops
+// PRODUCER-CONSUMER-MAXIMAL: %[[A:.*]] = memref.alloc
+// PRODUCER-CONSUMER-MAXIMAL: affine.store {{.*}}, %[[A]][
+// PRODUCER-CONSUMER-MAXIMAL: memref.dealloc %[[A]]
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+func.func @representable_free_between_loops(
+ %in: memref<32xf64>, %out: memref<32xf64>) {
+ %a = memref.alloc() : memref<32xf64>
+ %b = memref.alloc() : memref<32xf64>
+ affine.for %i = 0 to 16 {
+ %v = affine.load %in[%i] : memref<32xf64>
+ affine.store %v, %a[%i] : memref<32xf64>
+ affine.store %v, %b[%i] : memref<32xf64>
+ }
+ affine.for %k = 0 to 1 {
+ memref.dealloc %a : memref<32xf64>
+ }
+ affine.for %j = 0 to 16 {
+ %v = affine.load %b[%j] : memref<32xf64>
+ affine.store %v, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// -----
+
+// An unknown call without memref operands can access a global memref and must
+// not become an isolated node that fusion can cross.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @unknown_call_global_effect
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: call @touch_global
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+func.func @unknown_call_global_effect(
+ %in: memref<32xf64>, %out: memref<32xf64>) {
+ %global = memref.get_global @fusion_global : memref<32xf64>
+ affine.for %i = 0 to 16 {
+ %v = affine.load %in[%i] : memref<32xf64>
+ affine.store %v, %global[%i] : memref<32xf64>
+ }
+ func.call @touch_global() : () -> ()
+ affine.for %j = 0 to 16 {
+ %v = affine.load %global[%j] : memref<32xf64>
+ affine.store %v, %out[%j] : memref<32xf64>
+ }
+ return
+}
+memref.global "private" @fusion_global : memref<32xf64>
+func.func private @touch_global()
// -----
diff --git a/mlir/test/Dialect/Affine/loop-fusion.mlir b/mlir/test/Dialect/Affine/loop-fusion.mlir
index 96a8f0c6218ee..79d9b55cf6e1b 100644
--- a/mlir/test/Dialect/Affine/loop-fusion.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion.mlir
@@ -1576,9 +1576,9 @@ func.func @producer_consumer_with_outmost_user(%arg0 : f16) {
// -----
-// Unknown operations nested in an affine loop may access their memref
-// operands. Dependence checking must handle them without assuming a load/store
-// operation class.
+// Unknown operations nested in an affine loop may access memory outside their
+// explicit operands. Fusion must leave the block unchanged when their effects
+// cannot be represented by the memref dependence graph.
// CHECK-LABEL: func @nested_unknown_call
// CHECK: affine.for
>From c3c459a8eeedda0f9e2407473d33bbc01e5a1fd6 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 17:47:49 +0800
Subject: [PATCH 11/15] [MLIR][Affine] Restrict unknown-effect bailout to calls
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 34 ++++++++++++++++-----
mlir/test/Dialect/Affine/loop-fusion-3.mlir | 15 ++++-----
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 9 ++----
3 files changed, 37 insertions(+), 21 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 3a731193bd887..009b0a9126e85 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -22,6 +22,7 @@
#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/IR/IntegerSet.h"
+#include "mlir/Interfaces/CallInterfaces.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallVectorExtras.h"
#include "llvm/Support/Debug.h"
@@ -55,8 +56,10 @@ static bool isSameMemref(Value lhs, Value rhs) {
/// Returns the values that `op` may have a memref effect of type `EffectTys`
/// on, not considering recursive effects. View-like values are canonicalized
/// to their storage source so the MDG uses one key for a view chain while raw
-/// views remain available for affine-coordinate analysis. Unknown operations
-/// cannot be represented by the memref-keyed graph, so return false for them.
+/// views remain available for affine-coordinate analysis. Unknown calls cannot
+/// be represented by the memref-keyed graph because their effects are not
+/// limited to explicit memref operands. Other unknown operations retain the
+/// existing operand-based fallback.
/// Returns false if a selected effect cannot be represented by a memref value.
/// The MDG has no resource-level or all-memory edges, so dropping such an
/// effect would make fusion unsound.
@@ -64,8 +67,16 @@ template <typename... EffectTys>
static bool getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
- if (!memOp)
- return !hasUnknownEffects(op);
+ if (!memOp) {
+ if (!hasUnknownEffects(op))
+ return true;
+ if (isa<CallOpInterface>(op))
+ return false;
+ for (Value operand : op->getOperands())
+ if (isa<BaseMemRefType>(operand.getType()))
+ values.push_back(canonicalizeMemref(operand));
+ return true;
+ }
SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
memOp.getEffects(effects);
for (auto &effect : effects) {
@@ -107,10 +118,17 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
if (!memInterface) {
if (!hasUnknownEffects(op))
return;
- // Unknown effects may reach memory through globals or other state not
- // represented by SSA memref operands. Keep the graph fail-closed.
- memrefLoads.push_back(op);
- memrefStores.push_back(op);
+ if (isa<CallOpInterface>(op)) {
+ // Calls may access memory not represented by explicit operands.
+ memrefLoads.push_back(op);
+ memrefStores.push_back(op);
+ } else if (llvm::any_of(op->getOperands(), [](Value value) {
+ return isa<BaseMemRefType>(value.getType());
+ })) {
+ // Conservatively, assume all memref operands are read and written.
+ memrefLoads.push_back(op);
+ memrefStores.push_back(op);
+ }
} else {
// Non-affine loads, stores, and frees. Allocation effects are
// intentionally omitted: they do not access existing memory, and
diff --git a/mlir/test/Dialect/Affine/loop-fusion-3.mlir b/mlir/test/Dialect/Affine/loop-fusion-3.mlir
index 70d6c82105543..d8204d0afc376 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-3.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-3.mlir
@@ -869,7 +869,8 @@ func.func @call_op_prevents_fusion(%arg0: memref<16xf32>){
// -----
func.func private @some_function()
-func.func @call_op_does_not_prevent_fusion(%arg0: memref<16xf32>){
+func.func @call_op_without_memref_operands_prevents_fusion(
+ %arg0: memref<16xf32>) {
%A = memref.alloc() : memref<16xf32>
%cst_1 = arith.constant 1.000000e+00 : f32
affine.for %arg1 = 0 to 16 {
@@ -885,9 +886,10 @@ func.func @call_op_does_not_prevent_fusion(%arg0: memref<16xf32>){
}
return
}
-// CHECK-LABEL: func @call_op_does_not_prevent_fusion
+// CHECK-LABEL: func @call_op_without_memref_operands_prevents_fusion
+// CHECK: affine.for
+// CHECK: call @some_function() : () -> ()
// CHECK: affine.for
-// CHECK-NOT: affine.for
// -----
@@ -1281,14 +1283,13 @@ func.func @unknown_memref_def_op() {
affine.for %i1 = 0 to 10 {
%0 = affine.load %may_alias[%i1] : memref<10xf32>
}
- // Fusion happens, but memref isn't privatized since %may_alias's origin is
- // unknown.
+ // The unknown call prevents fusion because its memory effects are not
+ // limited to the returned memref.
// CHECK: call
// CHECK-NEXT: affine.for
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[%{{.*}}] : memref<10xf32>
+ // CHECK: affine.for
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}}] : memref<10xf32>
- // CHECK-NEXT: }
- // CHECK-NOT: affine.for
return
}
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index fc82b34f870bf..7fa411ae6c134 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1137,19 +1137,16 @@ func.func @subview_non_alias_memref_store(
// -----
// A represented free must remain a dependence through full affine filtering.
-// Otherwise fusing the first and third loops would move a store to %a past
-// its deallocation.
+// Otherwise fusing the first and third loops could move a store to %a past its
+// deallocation.
// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @representable_free_between_loops
// PRODUCER-CONSUMER-MAXIMAL: %[[A:.*]] = memref.alloc
// PRODUCER-CONSUMER-MAXIMAL: affine.store {{.*}}, %[[A]][
// PRODUCER-CONSUMER-MAXIMAL: memref.dealloc %[[A]]
-// PRODUCER-CONSUMER-MAXIMAL: affine.for
-// PRODUCER-CONSUMER-MAXIMAL: affine.load
func.func @representable_free_between_loops(
- %in: memref<32xf64>, %out: memref<32xf64>) {
+ %in: memref<32xf64>, %b: memref<32xf64>, %out: memref<32xf64>) {
%a = memref.alloc() : memref<32xf64>
- %b = memref.alloc() : memref<32xf64>
affine.for %i = 0 to 16 {
%v = affine.load %in[%i] : memref<32xf64>
affine.store %v, %a[%i] : memref<32xf64>
>From df1b83d3b665cc51006a395ce2e336f92e536005 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 17:55:52 +0800
Subject: [PATCH 12/15] [MLIR][Affine] Reuse effect classification in fusion
collection
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 009b0a9126e85..5e02828b4714f 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -116,19 +116,14 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
} else {
auto memInterface = dyn_cast<MemoryEffectOpInterface>(op);
if (!memInterface) {
- if (!hasUnknownEffects(op))
+ SmallVector<Value> affectedValues;
+ if (getMayAffectedValues<MemoryEffects::Read>(op, affectedValues) &&
+ affectedValues.empty())
return;
- if (isa<CallOpInterface>(op)) {
- // Calls may access memory not represented by explicit operands.
- memrefLoads.push_back(op);
- memrefStores.push_back(op);
- } else if (llvm::any_of(op->getOperands(), [](Value value) {
- return isa<BaseMemRefType>(value.getType());
- })) {
- // Conservatively, assume all memref operands are read and written.
- memrefLoads.push_back(op);
- memrefStores.push_back(op);
- }
+ // Unknown calls may access memory not represented by explicit
+ // operands; other unknown operations reach here with memref operands.
+ memrefLoads.push_back(op);
+ memrefStores.push_back(op);
} else {
// Non-affine loads, stores, and frees. Allocation effects are
// intentionally omitted: they do not access existing memory, and
>From 970ada299ed3921bd69da4b9ce7d778891f4869d Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 20:11:42 +0800
Subject: [PATCH 13/15] [MLIR][Affine] Fail closed on unknown fusion effects
---
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 28 ++++---------
mlir/test/Dialect/Affine/loop-fusion-2.mlir | 24 +++++------
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 45 +++++++++++++++++++++
mlir/test/Dialect/Affine/loop-fusion.mlir | 34 ++++++++--------
4 files changed, 82 insertions(+), 49 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 5e02828b4714f..70a11acb8b8e0 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -22,7 +22,6 @@
#include "mlir/Dialect/MemRef/Utils/MemRefUtils.h"
#include "mlir/Dialect/Utils/StaticValueUtils.h"
#include "mlir/IR/IntegerSet.h"
-#include "mlir/Interfaces/CallInterfaces.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/SmallVectorExtras.h"
#include "llvm/Support/Debug.h"
@@ -56,27 +55,17 @@ static bool isSameMemref(Value lhs, Value rhs) {
/// Returns the values that `op` may have a memref effect of type `EffectTys`
/// on, not considering recursive effects. View-like values are canonicalized
/// to their storage source so the MDG uses one key for a view chain while raw
-/// views remain available for affine-coordinate analysis. Unknown calls cannot
-/// be represented by the memref-keyed graph because their effects are not
-/// limited to explicit memref operands. Other unknown operations retain the
-/// existing operand-based fallback.
-/// Returns false if a selected effect cannot be represented by a memref value.
-/// The MDG has no resource-level or all-memory edges, so dropping such an
-/// effect would make fusion unsound.
+/// views remain available for affine-coordinate analysis. Unknown effects
+/// cannot be represented by the memref-keyed graph.
+/// Returns false if a selected effect cannot be represented by a memref value
+/// or if the operation's effects are unknown. The MDG has no resource-level or
+/// all-memory edges, so dropping such an effect would make fusion unsound.
template <typename... EffectTys>
static bool getMayAffectedValues(Operation *op,
SmallVectorImpl<Value> &values) {
auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
- if (!memOp) {
- if (!hasUnknownEffects(op))
- return true;
- if (isa<CallOpInterface>(op))
- return false;
- for (Value operand : op->getOperands())
- if (isa<BaseMemRefType>(operand.getType()))
- values.push_back(canonicalizeMemref(operand));
- return true;
- }
+ if (!memOp)
+ return !hasUnknownEffects(op);
SmallVector<SideEffects::EffectInstance<MemoryEffects::Effect>, 4> effects;
memOp.getEffects(effects);
for (auto &effect : effects) {
@@ -120,8 +109,7 @@ void LoopNestStateCollector::collect(Operation *opToWalk) {
if (getMayAffectedValues<MemoryEffects::Read>(op, affectedValues) &&
affectedValues.empty())
return;
- // Unknown calls may access memory not represented by explicit
- // operands; other unknown operations reach here with memref operands.
+ // Unknown effects cannot be represented by the memref-keyed graph.
memrefLoads.push_back(op);
memrefStores.push_back(op);
} else {
diff --git a/mlir/test/Dialect/Affine/loop-fusion-2.mlir b/mlir/test/Dialect/Affine/loop-fusion-2.mlir
index b26a539b2f7d5..2ef55b01164e2 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-2.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-2.mlir
@@ -20,15 +20,15 @@ func.func @should_fuse_at_depth_above_loop_carried_dependence(%arg0: memref<64x4
affine.for %i3 = 0 to 4 {
affine.for %i4 = 0 to 16 {
%v = affine.load %arg1[16 * %i3 - %i4 + 15, %i2] : memref<64x4xf32>
- "op0"(%v) : (f32) -> ()
+ %unused0 = arith.addf %v, %v : f32
}
affine.for %i5 = 0 to 4 {
affine.for %i6 = 0 to 16 {
%v = affine.load %arg0[16 * %i5 - %i6 + 15, %i3] : memref<64x4xf32>
- "op1"(%v) : (f32) -> ()
+ %unused1 = arith.addf %v, %v : f32
}
affine.for %i7 = 0 to 16 {
- %r = "op2"() : () -> (f32)
+ %r = arith.constant 0.0 : f32
%v = affine.load %out[16 * %i5 + %i7, %i2] : memref<64x4xf32>
%s = arith.addf %v, %r : f32
affine.store %s, %out[16 * %i5 + %i7, %i2] : memref<64x4xf32>
@@ -57,15 +57,15 @@ func.func @should_fuse_at_depth_above_loop_carried_dependence(%arg0: memref<64x4
// CHECK-NEXT: affine.for %{{.*}} = 0 to 4 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}} * 16 - %{{.*}} + 15, %{{.*}}] : memref<64x4xf32>
- // CHECK-NEXT: "op0"(%{{.*}}) : (f32) -> ()
+ // CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 4 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}} * 16 - %{{.*}} + 15, %{{.*}}] : memref<64x4xf32>
- // CHECK-NEXT: "op1"(%{{.*}}) : (f32) -> ()
+ // CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
- // CHECK-NEXT: %{{.*}} = "op2"() : () -> f32
+ // CHECK-NEXT: %{{.*}} = arith.constant {{.*}} : f32
// CHECK: affine.load %{{.*}}[%{{.*}} * 16 + %{{.*}}, %{{.*}}] : memref<64x4xf32>
// CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK: affine.store %{{.*}}, %{{.*}}[%{{.*}} * 16 + %{{.*}}, %{{.*}}] : memref<64x4xf32>
@@ -244,7 +244,7 @@ func.func @slice_tile(%arg0: memref<128x8xf32>, %arg1: memref<32x8xf32>, %0 : f3
affine.for %k = 0 to 8 {
affine.for %kk = 0 to 16 {
%v = affine.load %arg0[16 * %k + %kk, %j] : memref<128x8xf32>
- %r = "foo"(%v) : (f32) -> f32
+ %r = arith.addf %v, %v : f32
}
affine.for %ii = 0 to 16 {
%v = affine.load %arg1[16 * %i + %ii, %j] : memref<32x8xf32>
@@ -264,7 +264,7 @@ func.func @slice_tile(%arg0: memref<128x8xf32>, %arg1: memref<32x8xf32>, %0 : f3
// CHECK-NEXT: affine.for %{{.*}} = 0 to 8 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}} * 16 + %{{.*}}, %{{.*}}] : memref<128x8xf32>
-// CHECK-NEXT: "foo"(%{{.*}}) : (f32) -> f32
+// CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}} * 16 + %{{.*}}, %{{.*}}] : memref<32x8xf32>
@@ -463,26 +463,26 @@ func.func @should_not_slice_past_slice_barrier() {
%0 = memref.alloc() : memref<100x16xf32>
affine.for %i0 = 0 to 100 {
affine.for %i1 = 0 to 16 {
- %1 = "op1"() : () -> f32
+ %1 = arith.constant 0.0 : f32
affine.store %1, %0[%i0, %i1] : memref<100x16xf32>
} {slice_fusion_barrier = true}
}
affine.for %i2 = 0 to 100 {
affine.for %i3 = 0 to 16 {
%2 = affine.load %0[%i2, %i3] : memref<100x16xf32>
- "op2"(%2) : (f32) -> ()
+ %unused = arith.addf %2, %2 : f32
}
}
// The 'slice_fusion_barrier' attribute on '%i1' prevents slicing the
// iteration space of '%i1' and any enclosing loop nests.
// CHECK: affine.for %{{.*}} = 0 to 100 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
-// CHECK-NEXT: %{{.*}} = "op1"() : () -> f32
+// CHECK-NEXT: %{{.*}} = arith.constant {{.*}} : f32
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0, %{{.*}}] : memref<1x16xf32>
// CHECK-NEXT: } {slice_fusion_barrier = true}
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[0, %{{.*}}] : memref<1x16xf32>
-// CHECK-NEXT: "op2"(%{{.*}}) : (f32) -> ()
+// CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: }
return
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 7fa411ae6c134..9b13cbc969823 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1188,6 +1188,51 @@ func.func @unknown_call_global_effect(
memref.global "private" @fusion_global : memref<32xf64>
func.func private @touch_global()
+// An unknown non-call operation can access memory not represented by its
+// operands and must not become an isolated node that fusion can cross.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @unknown_non_call_without_operands_prevents_fusion
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: }
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: "unknown.touch_global"() : () -> ()
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for
+func.func @unknown_non_call_without_operands_prevents_fusion(
+ %in: memref<32xf64>, %out: memref<32xf64>) {
+ %global = memref.get_global @fusion_global : memref<32xf64>
+ affine.for %i = 0 to 16 {
+ %v = affine.load %in[%i] : memref<32xf64>
+ affine.store %v, %global[%i] : memref<32xf64>
+ }
+ "unknown.touch_global"() : () -> ()
+ affine.for %j = 0 to 16 {
+ %v = affine.load %global[%j] : memref<32xf64>
+ affine.store %v, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
+// Explicit operands do not make the effects of an unknown operation complete.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @unknown_non_call_with_memref_operand_may_have_implicit_effects
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: }
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: "unknown.touch_global"(%{{.*}}) : (memref<32xf64>) -> ()
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for
+func.func @unknown_non_call_with_memref_operand_may_have_implicit_effects(
+ %in: memref<32xf64>, %unrelated: memref<32xf64>, %out: memref<32xf64>) {
+ %global = memref.get_global @fusion_global : memref<32xf64>
+ affine.for %i = 0 to 16 {
+ %v = affine.load %in[%i] : memref<32xf64>
+ affine.store %v, %global[%i] : memref<32xf64>
+ }
+ "unknown.touch_global"(%unrelated) : (memref<32xf64>) -> ()
+ affine.for %j = 0 to 16 {
+ %v = affine.load %global[%j] : memref<32xf64>
+ affine.store %v, %out[%j] : memref<32xf64>
+ }
+ return
+}
+
// -----
// An addressable effect without an SSA memory value cannot be represented by
diff --git a/mlir/test/Dialect/Affine/loop-fusion.mlir b/mlir/test/Dialect/Affine/loop-fusion.mlir
index 79d9b55cf6e1b..5e4064d9c54ae 100644
--- a/mlir/test/Dialect/Affine/loop-fusion.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion.mlir
@@ -596,7 +596,7 @@ func.func @permute_and_fuse() {
affine.for %i4 = 0 to 10 {
affine.for %i5 = 0 to 20 {
%v0 = affine.load %m[%i4, %i5, %i3] : memref<10x20x30xf32>
- "foo"(%v0) : (f32) -> ()
+ %unused = arith.addf %v0, %v0 : f32
}
}
}
@@ -605,7 +605,7 @@ func.func @permute_and_fuse() {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 20 {
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0, 0, 0] : memref<1x1x1xf32>
// CHECK-NEXT: affine.load %{{.*}}[0, 0, 0] : memref<1x1x1xf32>
-// CHECK-NEXT: "foo"(%{{.*}}) : (f32) -> ()
+// CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }
@@ -633,7 +633,7 @@ func.func @fuse_reshape_64_16_4(%in : memref<64xf32>) {
affine.for %i1 = 0 to 16 {
affine.for %i2 = 0 to 4 {
%w = affine.load %out[%i1, %i2] : memref<16x4xf32>
- "foo"(%w) : (f32) -> ()
+ %unused = arith.addf %w, %w : f32
}
}
return
@@ -665,7 +665,7 @@ func.func @fuse_reshape_16_4_64() {
affine.for %i2 = 0 to 64 {
%w = affine.load %out[%i2] : memref<64xf32>
- "foo"(%w) : (f32) -> ()
+ %unused = arith.addf %w, %w : f32
}
// CHECK: affine.for %{{.*}} = 0 to 64 {
// CHECK-NEXT: affine.apply [[$MAP0]](%{{.*}})
@@ -674,7 +674,7 @@ func.func @fuse_reshape_16_4_64() {
// CHECK-NEXT: affine.apply [[$MAP2]](%{{.*}}, %{{.*}})
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0] : memref<1xf32>
// CHECK-NEXT: affine.load %{{.*}}[0] : memref<1xf32>
-// CHECK-NEXT: "foo"(%{{.*}}) : (f32) -> ()
+// CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: return
return
@@ -697,7 +697,7 @@ func.func @R6_to_R2_reshape_square() -> memref<64x9xi32> {
affine.for %i3 = 0 to 3 {
affine.for %i4 = 0 to 16 {
affine.for %i5 = 0 to 1 {
- %val = "foo"(%i0, %i1, %i2, %i3, %i4, %i5) : (index, index, index, index, index, index) -> i32
+ %val = arith.constant 0 : i32
affine.store %val, %in[%i0, %i1, %i2, %i3, %i4, %i5] : memref<2x2x3x3x16x1xi32>
}
}
@@ -758,7 +758,7 @@ func.func @R6_to_R2_reshape_square() -> memref<64x9xi32> {
// CHECK-NEXT: affine.apply [[$MAP2]](%{{.*}}, %{{.*}})
// CHECK-NEXT: affine.apply [[$MAP3]](%{{.*}}, %{{.*}})
// CHECK-NEXT: affine.apply [[$MAP4]](%{{.*}}, %{{.*}})
-// CHECK-NEXT: "foo"(%{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}}) : (index, index, index, index, index, index) -> i32
+// CHECK-NEXT: %{{.*}} = arith.constant {{.*}} : i32
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0, 0, 0, 0, 0, 0] : memref<1x1x1x1x1x1xi32>
// CHECK-NEXT: affine.apply [[$MAP11]](%{{.*}}, %{{.*}})
// CHECK-NEXT: affine.apply [[$MAP12]](%{{.*}})
@@ -812,7 +812,7 @@ func.func @should_fuse_reduction_at_depth_of_one() {
affine.for %i1 = 0 to 100 {
%v0 = affine.load %b[%i0] : memref<10xf32>
%v1 = affine.load %a[%i0, %i1] : memref<10x100xf32>
- %v2 = "maxf"(%v0, %v1) : (f32, f32) -> f32
+ %v2 = arith.addf %v0, %v1 : f32
affine.store %v2, %b[%i0] : memref<10xf32>
}
}
@@ -832,7 +832,7 @@ func.func @should_fuse_reduction_at_depth_of_one() {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 100 {
// CHECK-NEXT: affine.load %{{.*}}[0] : memref<1xf32>
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}}, %{{.*}}] : memref<10x100xf32>
- // CHECK-NEXT: "maxf"(%{{.*}}, %{{.*}}) : (f32, f32) -> f32
+ // CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0] : memref<1xf32>
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 100 {
@@ -856,10 +856,10 @@ func.func @should_fuse_at_src_depth1_and_dst_depth1() {
affine.for %i0 = 0 to 100 {
affine.for %i1 = 0 to 16 {
%v0 = affine.load %a[%i0, %i1] : memref<100x16xf32>
- "op0"(%v0) : (f32) -> ()
+ %unused0 = arith.addf %v0, %v0 : f32
}
affine.for %i2 = 0 to 16 {
- %v1 = "op1"() : () -> (f32)
+ %v1 = arith.constant 0.0 : f32
affine.store %v1, %b[%i0, %i2] : memref<100x16xf32>
}
}
@@ -867,7 +867,7 @@ func.func @should_fuse_at_src_depth1_and_dst_depth1() {
affine.for %i3 = 0 to 100 {
affine.for %i4 = 0 to 16 {
%v2 = affine.load %b[%i3, %i4] : memref<100x16xf32>
- "op2"(%v2) : (f32) -> ()
+ %unused2 = arith.addf %v2, %v2 : f32
}
}
// We can slice iterations of the '%i0' and '%i1' loops in the source
@@ -878,15 +878,15 @@ func.func @should_fuse_at_src_depth1_and_dst_depth1() {
// CHECK: affine.for %{{.*}} = 0 to 100 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[%{{.*}}, %{{.*}}] : memref<100x16xf32>
- // CHECK-NEXT: "op0"(%{{.*}}) : (f32) -> ()
+ // CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
- // CHECK-NEXT: %{{.*}} = "op1"() : () -> f32
+ // CHECK-NEXT: %{{.*}} = arith.constant {{.*}} : f32
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0, %{{.*}}] : memref<1x16xf32>
// CHECK-NEXT: }
// CHECK-NEXT: affine.for %{{.*}} = 0 to 16 {
// CHECK-NEXT: affine.load %{{.*}}[0, %{{.*}}] : memref<1x16xf32>
- // CHECK-NEXT: "op2"(%{{.*}}) : (f32) -> ()
+ // CHECK-NEXT: arith.addf %{{.*}}, %{{.*}} : f32
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: return
@@ -1304,7 +1304,7 @@ func.func @R3_to_R2_reshape() {
affine.for %i0 = 0 to 2 {
affine.for %i1 = 0 to 3 {
affine.for %i2 = 0 to 16 {
- %val = "foo"(%i0, %i1, %i2) : (index, index, index) -> i32
+ %val = arith.constant 0 : i32
affine.store %val, %in[%i0, %i1, %i2] : memref<2x3x16xi32>
}
}
@@ -1329,7 +1329,7 @@ func.func @R3_to_R2_reshape() {
// CHECK: affine.for %{{.*}} = 0 to 32 {
// CHECK-NEXT: affine.for %{{.*}} = 0 to 3 {
// CHECK-NEXT: affine.apply [[$MAP0]](%{{.*}}, %{{.*}})
-// CHECK-NEXT: "foo"(%{{.*}}, %{{.*}}, %{{.*}}) : (index, index, index) -> i32
+// CHECK-NEXT: %{{.*}} = arith.constant {{.*}} : i32
// CHECK-NEXT: affine.store %{{.*}}, %{{.*}}[0, 0, 0] : memref<1x1x1xi32>
// CHECK-NEXT: affine.apply [[$MAP1]](%{{.*}}, %{{.*}})
// CHECK-NEXT: affine.apply [[$MAP2]](%{{.*}})
>From 6a8f8f19e5d92ce381a839da064702612d038dd5 Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 22:27:34 +0800
Subject: [PATCH 14/15] [MLIR][Affine] Guard fusion against unmodeled loop
effects
---
.../mlir/Dialect/Affine/Analysis/Utils.h | 10 +-
mlir/lib/Dialect/Affine/Analysis/Utils.cpp | 7 +-
.../Dialect/Affine/Transforms/LoopFusion.cpp | 8 +-
.../Dialect/Affine/Utils/LoopFusionUtils.cpp | 49 ++++++++++
mlir/test/Dialect/Affine/loop-fusion-4.mlir | 93 +++++++++++++++++++
mlir/test/Dialect/Affine/loop-fusion.mlir | 18 +++-
6 files changed, 169 insertions(+), 16 deletions(-)
diff --git a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
index f78fe772d77e9..32ce462f3b552 100644
--- a/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
+++ b/mlir/include/mlir/Dialect/Affine/Analysis/Utils.h
@@ -32,9 +32,8 @@ class AffineForOp;
class AffineValueMap;
struct MemRefAccess;
-// LoopNestStateCollector walks loop nests and collects load and store
-// operations, and whether or not a region holding op other than ForOp and IfOp
-// was encountered in the loop nest.
+// LoopNestStateCollector walks loop nests and collects affine and non-affine
+// memory operations.
struct LoopNestStateCollector {
SmallVector<AffineForOp, 4> forOps;
// Affine loads.
@@ -48,8 +47,7 @@ struct LoopNestStateCollector {
// Free operations.
SmallVector<Operation *, 4> memrefFrees;
- // Collects load and store operations, and whether or not a region holding op
- // other than ForOp and IfOp was encountered in the loop nest.
+ // Collects affine and non-affine memory operations in the loop nest.
void collect(Operation *opToWalk);
};
@@ -248,7 +246,7 @@ struct MemRefDependenceGraph {
ArrayRef<Operation *> memrefStores,
ArrayRef<Operation *> memrefFrees);
- void clearNodeLoadAndStores(unsigned id);
+ void clearNodeMemoryOps(unsigned id);
// Calls 'callback' for each input edge incident to node 'id' which carries a
// memref dependence.
diff --git a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
index 70a11acb8b8e0..e273c3f40e799 100644
--- a/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
+++ b/mlir/lib/Dialect/Affine/Analysis/Utils.cpp
@@ -817,7 +817,7 @@ void MemRefDependenceGraph::updateEdges(unsigned sibId, unsigned dstId) {
}
}
-// Adds ops in 'loads' and 'stores' to node at 'id'.
+// Adds all collected memory operations to node at 'id'.
void MemRefDependenceGraph::addToNode(unsigned id, ArrayRef<Operation *> loads,
ArrayRef<Operation *> stores,
ArrayRef<Operation *> memrefLoads,
@@ -831,10 +831,13 @@ void MemRefDependenceGraph::addToNode(unsigned id, ArrayRef<Operation *> loads,
llvm::append_range(node->memrefFrees, memrefFrees);
}
-void MemRefDependenceGraph::clearNodeLoadAndStores(unsigned id) {
+void MemRefDependenceGraph::clearNodeMemoryOps(unsigned id) {
Node *node = getNode(id);
node->loads.clear();
node->stores.clear();
+ node->memrefLoads.clear();
+ node->memrefStores.clear();
+ node->memrefFrees.clear();
}
// Calls 'callback' for each input edge incident to node 'id' which carries a
diff --git a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
index a22a6639fd314..8a6dd4804a06b 100644
--- a/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
+++ b/mlir/lib/Dialect/Affine/Transforms/LoopFusion.cpp
@@ -1175,8 +1175,8 @@ struct GreedyFusion {
LoopNestStateCollector dstLoopCollector;
dstLoopCollector.collect(dstAffineForOp);
- // Clear and add back loads and stores.
- mdg->clearNodeLoadAndStores(dstNode->id);
+ // Clear and add back all memory operations.
+ mdg->clearNodeMemoryOps(dstNode->id);
mdg->addToNode(
dstId, dstLoopCollector.loadOpInsts, dstLoopCollector.storeOpInsts,
dstLoopCollector.memrefLoads, dstLoopCollector.memrefStores,
@@ -1527,8 +1527,8 @@ struct GreedyFusion {
auto dstForInst = cast<AffineForOp>(dstNode->op);
LoopNestStateCollector dstLoopCollector;
dstLoopCollector.collect(dstForInst);
- // Clear and add back loads and stores
- mdg->clearNodeLoadAndStores(dstNode->id);
+ // Clear and add back all memory operations.
+ mdg->clearNodeMemoryOps(dstNode->id);
mdg->addToNode(dstNode->id, dstLoopCollector.loadOpInsts,
dstLoopCollector.storeOpInsts, dstLoopCollector.memrefLoads,
dstLoopCollector.memrefStores, dstLoopCollector.memrefFrees);
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index e51b9ff5dea27..5126c5d317f8e 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -191,6 +191,43 @@ gatherLoadsAndStores(AffineForOp forOp,
return !hasIfOp;
}
+// The fusion transformation clones the complete source loop body into the
+// destination schedule. The affine slice and dependence analysis below does
+// not model the order of non-affine memory effects within that schedule, so
+// keep such effects out of candidate loops until that analysis is extended.
+static bool hasUnmodeledMemoryEffects(AffineForOp forOp) {
+ LoopNestStateCollector collector;
+ collector.collect(forOp);
+ return !collector.memrefLoads.empty() || !collector.memrefStores.empty() ||
+ !collector.memrefFrees.empty();
+}
+
+// Returns true when accesses in two loop bodies use different views of the
+// same storage and at least one access writes. The raw view remains important
+// for affine coordinate analysis, but a storage-level conflict is enough to
+// reject fusion because this check protects the frame condition of the
+// complete loop bodies before strategy-specific filtering.
+static bool hasConflictingStorageAccesses(ArrayRef<Operation *> firstOps,
+ ArrayRef<Operation *> secondOps) {
+ for (Operation *firstOp : firstOps) {
+ MemRefAccess firstAccess(firstOp);
+ Value firstStorage = memref::skipViewLikeOps(
+ cast<MemrefValue>(firstAccess.memref));
+ for (Operation *secondOp : secondOps) {
+ MemRefAccess secondAccess(secondOp);
+ if (firstAccess.memref == secondAccess.memref)
+ continue;
+ Value secondStorage = memref::skipViewLikeOps(
+ cast<MemrefValue>(secondAccess.memref));
+ if (firstStorage != secondStorage)
+ continue;
+ if (firstAccess.isStore() || secondAccess.isStore())
+ return true;
+ }
+ }
+ return false;
+}
+
/// Returns the maximum loop depth at which we could fuse producer loop
/// 'srcForOp' into consumer loop 'dstForOp' without violating data dependences.
// TODO: Generalize this check for sibling and more generic fusion scenarios.
@@ -289,6 +326,12 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
return FusionResult::FailPrecondition;
}
+ if (hasUnmodeledMemoryEffects(srcForOp) ||
+ hasUnmodeledMemoryEffects(dstForOp)) {
+ LDBG() << "Cannot fuse loop nests with unmodeled non-affine memory effects";
+ return FusionResult::FailFusionDependence;
+ }
+
// Return 'failure' if no valid insertion point for fused loop nest in 'block'
// exists which would preserve dependences.
if (!getFusedLoopNestInsertionPoint(srcForOp, dstForOp)) {
@@ -316,6 +359,12 @@ FusionResult mlir::affine::canFuseLoops(AffineForOp srcForOp,
return FusionResult::FailPrecondition;
}
+ if (hasConflictingStorageAccesses(opsA, opsB)) {
+ LDBG() << "Fusion would change ordering of accesses through different "
+ "views of the same storage";
+ return FusionResult::FailFusionDependence;
+ }
+
// Return 'failure' if fusing loops at depth 'dstLoopDepth' wouldn't preserve
// loop dependences.
// TODO: Enable this check for sibling and more generic loop fusion
diff --git a/mlir/test/Dialect/Affine/loop-fusion-4.mlir b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
index 9b13cbc969823..c78a1b97fb330 100644
--- a/mlir/test/Dialect/Affine/loop-fusion-4.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion-4.mlir
@@ -1382,3 +1382,96 @@ func.func @value_less_nonaddressable_free_effect(
}
return
}
+
+// Non-affine memory effects inside candidate loops are not included in the
+// affine slice analysis, so fusion must preserve the two complete loops.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @non_affine_effects_inside_candidate_loops
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: memref.load
+// PRODUCER-CONSUMER-MAXIMAL: memref.store
+// PRODUCER-CONSUMER-MAXIMAL: }
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: memref.load
+func.func @non_affine_effects_inside_candidate_loops(
+ %in: memref<8xi32>, %out: memref<8xi32>) {
+ %tmp = memref.alloc() : memref<8xi32>
+ %counter = memref.alloc() : memref<1xi32>
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : i32
+ affine.for %i = 0 to 8 {
+ %v = affine.load %in[%i] : memref<8xi32>
+ %old = memref.load %counter[%c0] : memref<1xi32>
+ %next = arith.addi %old, %c1 : i32
+ memref.store %next, %counter[%c0] : memref<1xi32>
+ affine.store %v, %tmp[%i] : memref<8xi32>
+ }
+ affine.for %j = 0 to 8 {
+ %v = affine.load %tmp[%j] : memref<8xi32>
+ %current = memref.load %counter[%c0] : memref<1xi32>
+ %result = arith.addi %v, %current : i32
+ affine.store %result, %out[%j] : memref<8xi32>
+ }
+ return
+}
+
+// A source read and destination write through overlapping non-trivial views
+// must not be interleaved by producer-consumer fusion.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @overlapping_subviews_inside_loops
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+// PRODUCER-CONSUMER-MAXIMAL: }
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+func.func @overlapping_subviews_inside_loops(%root: memref<32xf32>) {
+ %a = memref.subview %root[0] [16] [1]
+ : memref<32xf32> to memref<16xf32, strided<[1], offset: 0>>
+ %b = memref.subview %root[1] [16] [1]
+ : memref<32xf32> to memref<16xf32, strided<[1], offset: 1>>
+ %tmp = memref.alloc() : memref<16xf32>
+ affine.for %i = 0 to 16 {
+ %v = affine.load %a[%i]
+ : memref<16xf32, strided<[1], offset: 0>>
+ affine.store %v, %tmp[%i] : memref<16xf32>
+ }
+ affine.for %j = 0 to 16 {
+ %v = affine.load %tmp[%j] : memref<16xf32>
+ affine.store %v, %b[%j]
+ : memref<16xf32, strided<[1], offset: 1>>
+ }
+ return
+}
+
+// The reverse read/write direction is covered independently so the guard is
+// not accidentally limited to source-read/destination-write pairs.
+
+// PRODUCER-CONSUMER-MAXIMAL-LABEL: func @overlapping_subviews_write_read
+// PRODUCER-CONSUMER-MAXIMAL: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.store
+// PRODUCER-CONSUMER-MAXIMAL: }
+// PRODUCER-CONSUMER-MAXIMAL-NEXT: affine.for
+// PRODUCER-CONSUMER-MAXIMAL: affine.load
+func.func @overlapping_subviews_write_read(
+ %root: memref<32xf32>, %out: memref<16xf32>) {
+ %a = memref.subview %root[0] [16] [1]
+ : memref<32xf32> to memref<16xf32, strided<[1], offset: 0>>
+ %b = memref.subview %root[1] [16] [1]
+ : memref<32xf32> to memref<16xf32, strided<[1], offset: 1>>
+ %tmp = memref.alloc() : memref<16xf32>
+ %cst = arith.constant 1.0 : f32
+ affine.for %i = 0 to 16 {
+ affine.store %cst, %a[%i]
+ : memref<16xf32, strided<[1], offset: 0>>
+ affine.store %cst, %tmp[%i] : memref<16xf32>
+ }
+ affine.for %j = 0 to 16 {
+ %v = affine.load %tmp[%j] : memref<16xf32>
+ %other = affine.load %b[%j]
+ : memref<16xf32, strided<[1], offset: 1>>
+ %sum = arith.addf %v, %other : f32
+ affine.store %sum, %out[%j] : memref<16xf32>
+ }
+ return
+}
diff --git a/mlir/test/Dialect/Affine/loop-fusion.mlir b/mlir/test/Dialect/Affine/loop-fusion.mlir
index 5e4064d9c54ae..b55eee5c27b6e 100644
--- a/mlir/test/Dialect/Affine/loop-fusion.mlir
+++ b/mlir/test/Dialect/Affine/loop-fusion.mlir
@@ -1583,15 +1583,20 @@ func.func @producer_consumer_with_outmost_user(%arg0 : f16) {
// CHECK-LABEL: func @nested_unknown_call
// CHECK: affine.for
// CHECK: func.call @escape_nested
-// CHECK: affine.for
+// CHECK: affine.store
+// CHECK: }
+// CHECK-NEXT: affine.for
// CHECK: affine.load
// CHECK: return
func.func @nested_unknown_call(%m: memref<8xf32>, %out: memref<8xf32>) {
+ %tmp = memref.alloc() : memref<8xf32>
affine.for %i = 0 to 8 {
+ %v = affine.load %m[%i] : memref<8xf32>
func.call @escape_nested(%m) : (memref<8xf32>) -> ()
+ affine.store %v, %tmp[%i] : memref<8xf32>
}
affine.for %i = 0 to 8 {
- %v = affine.load %m[%i] : memref<8xf32>
+ %v = affine.load %tmp[%i] : memref<8xf32>
affine.store %v, %out[%i] : memref<8xf32>
}
return
@@ -1605,16 +1610,21 @@ func.func private @escape_nested(memref<8xf32>)
// CHECK-LABEL: func @nested_memref_copy
// CHECK: affine.for
// CHECK: memref.copy
-// CHECK: affine.for
+// CHECK: affine.store
+// CHECK: }
+// CHECK-NEXT: affine.for
// CHECK: affine.load
// CHECK: return
func.func @nested_memref_copy(
%src: memref<8xf32>, %dst: memref<8xf32>, %out: memref<8xf32>) {
+ %tmp = memref.alloc() : memref<8xf32>
affine.for %i = 0 to 8 {
memref.copy %src, %dst : memref<8xf32> to memref<8xf32>
+ %v = affine.load %src[%i] : memref<8xf32>
+ affine.store %v, %tmp[%i] : memref<8xf32>
}
affine.for %i = 0 to 8 {
- %v = affine.load %dst[%i] : memref<8xf32>
+ %v = affine.load %tmp[%i] : memref<8xf32>
affine.store %v, %out[%i] : memref<8xf32>
}
return
>From dd0c490fd9d3d6e8067dc50c019613c20f443ecf Mon Sep 17 00:00:00 2001
From: 1sgtpepper <cynejarviszarceno at gmail.com>
Date: Sun, 2 Aug 2026 22:30:36 +0800
Subject: [PATCH 15/15] [MLIR][Affine] Format fusion legality guard
---
mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
index 5126c5d317f8e..ef2a70e8a850b 100644
--- a/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
+++ b/mlir/lib/Dialect/Affine/Utils/LoopFusionUtils.cpp
@@ -211,14 +211,14 @@ static bool hasConflictingStorageAccesses(ArrayRef<Operation *> firstOps,
ArrayRef<Operation *> secondOps) {
for (Operation *firstOp : firstOps) {
MemRefAccess firstAccess(firstOp);
- Value firstStorage = memref::skipViewLikeOps(
- cast<MemrefValue>(firstAccess.memref));
+ Value firstStorage =
+ memref::skipViewLikeOps(cast<MemrefValue>(firstAccess.memref));
for (Operation *secondOp : secondOps) {
MemRefAccess secondAccess(secondOp);
if (firstAccess.memref == secondAccess.memref)
continue;
- Value secondStorage = memref::skipViewLikeOps(
- cast<MemrefValue>(secondAccess.memref));
+ Value secondStorage =
+ memref::skipViewLikeOps(cast<MemrefValue>(secondAccess.memref));
if (firstStorage != secondStorage)
continue;
if (firstAccess.isStore() || secondAccess.isStore())
More information about the Mlir-commits
mailing list