Skip to content

Commit f38d918

Browse files
atassishunhoffe
andauthored
[AIE] objectfifo-liveness: union-find cycles + structural coupling gate + slim tests (#3312)
Co-authored-by: Erika Hunhoff <erika.hunhoff@amd.com>
1 parent 719cc78 commit f38d918

4 files changed

Lines changed: 202 additions & 1424 deletions

File tree

lib/Dialect/AIE/Transforms/AIEObjectFifoLiveness.cpp

Lines changed: 112 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,32 @@
1515
// is a static, structural deadlock: the IR compiles clean and then hangs the
1616
// NPU at runtime.
1717
//
18-
// This pass builds the objectFIFO data + back-pressure dependency graph, finds
19-
// cyclic strongly-connected components (Tarjan), and applies the validated SDF
20-
// model PER coupled-multicast group (grouped by name base) so an unrelated
21-
// cycle elsewhere in the device cannot inflate a group's demand:
18+
// This pass builds the objectFIFO data + back-pressure dependency graph. The
19+
// graph is UNDIRECTED (a shared tile couples its producer and consumer fifos
20+
// with data and back-pressure edges in both directions), so cycle detection
21+
// degenerates to connected components: any fifo that shares a tile with another
22+
// sits in a back-pressure loop. We therefore group fifos with a union-find and
23+
// treat a component of size > 1 as a cycle. The validated SDF model is then
24+
// applied PER coupled-multicast group so an unrelated cycle elsewhere in the
25+
// device cannot inflate a group's demand:
2226
//
2327
// demand = array_fan * T (array_fan = the group's multicast
24-
// fan-out, summed across the SCCs it
25-
// spans; T = max repeat_count among the
26-
// fifos in those SCCs)
28+
// fan-out, summed across the components
29+
// it spans; T = max repeat_count among
30+
// the fifos in those components)
2731
// slack = 2 * depth (depth = min depth in the group)
2832
// DEADLOCK iff T >= 2 AND depth > 0 AND demand > slack
2933
//
34+
// Coupling (which fifos are halves of ONE logical broadcast tensor) is a
35+
// property the IR does not carry: the halves can live on different, unlinked
36+
// MemTiles, so neither aie.objectfifo.link nor a shared producer tile
37+
// identifies them -- only their shared name base does (see nameBase). Because a
38+
// name is a weak signal, the grouping is gated on structural agreement
39+
// (identical element type, depth and repeat_count, and disjoint consumer
40+
// tiles); a fifo that only shares the base name is analyzed on its own rather
41+
// than folded in. A durable fix would be a first-class coupling attribute on
42+
// the frontend ops; that is a tracked follow-up.
43+
//
3044
// The T >= 2 replay guard avoids false positives on single-trip broadcasts
3145
// (which fan out once and drain monotonically). The analysis is sound for this
3246
// static SDF class and conservative elsewhere (it never errors outside a proven
@@ -44,11 +58,11 @@
4458
#include "llvm/ADT/DenseMap.h"
4559
#include "llvm/ADT/DenseSet.h"
4660
#include "llvm/ADT/SmallVector.h"
61+
#include "llvm/ADT/StringMap.h"
4762
#include "llvm/ADT/StringRef.h"
4863

4964
#include <algorithm>
5065
#include <functional>
51-
#include <map>
5266
#include <set>
5367
#include <string>
5468
#include <vector>
@@ -92,6 +106,7 @@ struct AIEObjectFifoLivenessPass
92106
SmallVector<mlir::Value> cons;
93107
long repeat;
94108
StringRef name;
109+
mlir::Type elemTy; // coupling signature (see nameBase gating below)
95110
};
96111
SmallVector<F> fifos;
97112

@@ -112,6 +127,7 @@ struct AIEObjectFifoLivenessPass
112127
f.repeat = 1;
113128
if (auto rc = ofo.getRepeatCount())
114129
f.repeat = *rc;
130+
f.elemTy = ofo.getElemType();
115131
fifos.push_back(f);
116132
});
117133

@@ -142,75 +158,106 @@ struct AIEObjectFifoLivenessPass
142158
}
143159
}
144160

145-
// find_cycles: Tarjan SCC; assign an SCC id to every node in a cycle
146-
// (component of size > 1). Nodes not in any cycle keep sccId = -1.
147-
SmallVector<int> idx(n, -1), low(n, 0);
148-
SmallVector<char> onStack(n, 0);
149-
SmallVector<unsigned> stk;
150-
int counter = 0;
151-
SmallVector<int> sccId(n, -1);
152-
int sccCount = 0;
153-
std::function<void(unsigned)> dfs = [&](unsigned v) {
154-
idx[v] = low[v] = counter++;
155-
stk.push_back(v);
156-
onStack[v] = 1;
157-
for (unsigned w : adj[v]) {
158-
if (idx[w] == -1) {
159-
dfs(w);
160-
low[v] = std::min(low[v], low[w]);
161-
} else if (onStack[w]) {
162-
low[v] = std::min(low[v], idx[w]);
163-
}
164-
}
165-
if (low[v] == idx[v]) {
166-
SmallVector<unsigned> comp;
167-
while (true) {
168-
unsigned w = stk.back();
169-
stk.pop_back();
170-
onStack[w] = 0;
171-
comp.push_back(w);
172-
if (w == v)
173-
break;
174-
}
175-
if (comp.size() > 1) {
176-
for (unsigned w : comp)
177-
sccId[w] = sccCount;
178-
++sccCount;
179-
}
161+
// find_cycles: the graph is undirected, so a directed SCC search would just
162+
// recover connected components (every shared-tile edge is a 2-cycle). Use a
163+
// union-find and treat a component of size > 1 as a cycle; a lone fifo that
164+
// shares no tile is acyclic. sccId is the component root for cyclic nodes
165+
// and -1 otherwise (the downstream group logic only keys off sccId
166+
// identity, so a root index is as good as a dense SCC id).
167+
SmallVector<int> uf(n);
168+
for (unsigned i = 0; i < n; ++i)
169+
uf[i] = i;
170+
std::function<int(int)> find = [&](int x) {
171+
while (uf[x] != x) {
172+
uf[x] = uf[uf[x]]; // path halving
173+
x = uf[x];
180174
}
175+
return x;
181176
};
182177
for (unsigned v = 0; v < n; ++v)
183-
if (idx[v] == -1)
184-
dfs(v);
178+
for (unsigned w : adj[v])
179+
uf[find(v)] = find(w);
180+
SmallVector<int> compSize(n, 0);
181+
for (unsigned i = 0; i < n; ++i)
182+
++compSize[find(i)];
183+
SmallVector<int> sccId(n, -1);
184+
for (unsigned i = 0; i < n; ++i)
185+
if (compSize[find(i)] > 1)
186+
sccId[i] = find(i);
185187

186188
// SDF check, scoped PER coupled-multicast group -- NOT globally. A coupled
187-
// broadcast is a name-base group of cyclic multicast objectFIFOs (e.g.
188-
// memW1_0 + memW1_1 = one weight broadcast, split across MemTiles); the
189-
// halves can land in different SCCs, so the group -- not the SCC -- is the
190-
// unit of coupling, and array_fan SUMS the group's fan-out across the SCCs
191-
// it spans. The trip-count T is taken ONLY from the SCC(s) this group lives
192-
// in, so an unrelated high-repeat_count (or a wide broadcast) elsewhere in
193-
// the device cannot inflate this group's demand and false-positive.
189+
// broadcast is a group of cyclic multicast objectFIFOs that are one logical
190+
// broadcast tensor (e.g. memW1_0 + memW1_1 = one weight broadcast, split
191+
// across MemTiles); the halves can land in different components, so the
192+
// group
193+
// -- not the component -- is the unit of coupling, and array_fan SUMS the
194+
// group's fan-out across the components it spans. The trip-count T is taken
195+
// ONLY from the component(s) this group lives in, so an unrelated
196+
// high-repeat_count (or a wide broadcast) elsewhere in the device cannot
197+
// inflate this group's demand and false-positive.
198+
//
199+
// The IR carries no signal for "these fifos are one logical tensor", so the
200+
// grouping keys off the shared name base (nameBase). Because a name is a
201+
// weak signal, it is GATED on structural agreement before two fifos are
202+
// coupled: identical element type, depth and repeat_count, and DISJOINT
203+
// consumer tiles (two halves of a broadcast reach different cores). A fifo
204+
// that shares the base name but fails any gate is analyzed as its own
205+
// standalone group rather than folded in -- so a stray name collision
206+
// cannot fabricate a coupling, and the summation is never applied to fifos
207+
// that are not actually halves.
194208
struct Group {
195209
long fan = 0, depth = 0;
196-
bool haveDepth = false, haveRep = false;
197210
unsigned rep = 0;
211+
std::string name; // display name (nameBase of the representative)
212+
mlir::Type elemTy; // coupling signature: element type ...
213+
long sigDepth = 0; // ... depth ...
214+
long sigRepeat = 0; // ... and repeat_count must all match to couple
215+
llvm::DenseSet<mlir::Value> cons; // consumer tiles (must stay disjoint)
198216
std::set<int> sccs;
199217
};
200-
std::map<std::string, Group> groups;
218+
SmallVector<Group> groupList;
219+
llvm::StringMap<unsigned>
220+
groupIdx; // name base -> owning group in groupList
201221
for (unsigned i = 0; i < n; ++i) {
202222
if (sccId[i] < 0 || fifos[i].cons.size() <= 1)
203223
continue; // only a cyclic multicast couples a broadcast
204-
Group &g = groups[nameBase(fifos[i].name).str()];
205-
g.fan += (long)fifos[i].cons.size();
206-
g.depth =
207-
g.haveDepth ? std::min(g.depth, fifos[i].depth) : fifos[i].depth;
208-
g.haveDepth = true;
209-
g.sccs.insert(sccId[i]);
210-
if (!g.haveRep) {
224+
StringRef base = nameBase(fifos[i].name);
225+
int slot = -1;
226+
auto it = groupIdx.find(base);
227+
if (it != groupIdx.end()) {
228+
Group &cand = groupList[it->second];
229+
bool consistent = cand.elemTy == fifos[i].elemTy &&
230+
cand.sigDepth == fifos[i].depth &&
231+
cand.sigRepeat == fifos[i].repeat;
232+
for (mlir::Value c : fifos[i].cons)
233+
if (consistent && cand.cons.contains(c))
234+
consistent = false;
235+
if (consistent)
236+
slot = it->second;
237+
}
238+
if (slot < 0) {
239+
// First fifo for this base, or a same-name fifo that failed a gate:
240+
// open a fresh group. Only the first owns the name-base map slot; a
241+
// later mismatching fifo becomes an unindexed standalone group.
242+
Group g;
211243
g.rep = i;
212-
g.haveRep = true;
244+
g.name = base.str();
245+
g.elemTy = fifos[i].elemTy;
246+
g.sigDepth = fifos[i].depth;
247+
g.sigRepeat = fifos[i].repeat;
248+
g.depth = fifos[i].depth;
249+
groupList.push_back(std::move(g));
250+
slot = (int)groupList.size() - 1;
251+
if (it == groupIdx.end())
252+
groupIdx[base] = slot;
253+
} else {
254+
groupList[slot].depth = std::min(groupList[slot].depth, fifos[i].depth);
213255
}
256+
Group &g = groupList[slot];
257+
g.fan += (long)fifos[i].cons.size();
258+
for (mlir::Value c : fifos[i].cons)
259+
g.cons.insert(c);
260+
g.sccs.insert(sccId[i]);
214261
}
215262

216263
// Replay guard (mirrors sdf_checker.py): the multicast back-pressure cycle
@@ -220,9 +267,8 @@ struct AIEObjectFifoLivenessPass
220267
// it never deadlocks regardless of fan-out (confirmed: bundled whole_array
221268
// matmul examples broadcast to the whole array at depth 2, T == 1, and RUN
222269
// on device).
223-
for (auto &kv : groups) {
224-
Group &g = kv.second;
225-
long T = 1; // trip-count from the SCC(s) coupled to THIS broadcast only
270+
for (Group &g : groupList) {
271+
long T = 1; // trip-count from the component(s) coupled to THIS broadcast
226272
for (unsigned i = 0; i < n; ++i)
227273
if (sccId[i] >= 0 && g.sccs.count(sccId[i]))
228274
T = std::max(T, fifos[i].repeat);
@@ -232,7 +278,7 @@ struct AIEObjectFifoLivenessPass
232278
continue;
233279
long reqDepth = (demand + 1) / 2; // ceil(demand / 2)
234280
fifos[g.rep].op->emitError()
235-
<< "objectFIFO @" << kv.first
281+
<< "objectFIFO @" << g.name
236282
<< " in a cyclic dependency requires depth >= " << reqDepth
237283
<< " for deadlock-free execution; allocated depth = " << g.depth
238284
<< ". (Coupled broadcast fan-out " << g.fan << " x trip-count " << T
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===- namebase_gate.mlir ---------------------------------*- MLIR -*-===//
2+
//
3+
// Copyright (C) 2026 Advanced Micro Devices, Inc.
4+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5+
//
6+
//===----------------------------------------------------------------------===//
7+
8+
// RUN: aie-opt --aie-objectfifo-liveness --verify-diagnostics %s
9+
//
10+
// Coupling gate. The pass groups a coupled broadcast by the shared name base of
11+
// its halves (memX_0, memX_1 -> "memX"), but a name is a weak signal, so the
12+
// grouping is gated on structural agreement before the fan-out is summed. Here
13+
// two cyclic multicast fifos merely COLLIDE on the base name "memX" -- they are
14+
// unrelated broadcasts with different element types (32x32xbf16 vs 16x16xbf16).
15+
// The gate must keep them in separate groups, so each is checked on its own:
16+
// fan = 2, T = 2 -> demand = 4 == slack 2 * depth = 4, no error. If the name
17+
// alone were trusted (no gate) they would be folded into one group with
18+
// fan = 4 -> demand = 8 > 4 and the pass would emit a FALSE positive. So this
19+
// file must stay silent; it pins that name-base grouping is structurally gated.
20+
module {
21+
aie.device(npu2) {
22+
%mem0 = aie.logical_tile<MemTile>(?, ?)
23+
%mem1 = aie.logical_tile<MemTile>(?, ?)
24+
%c0 = aie.logical_tile<CoreTile>(?, ?)
25+
%c1 = aie.logical_tile<CoreTile>(?, ?)
26+
%c2 = aie.logical_tile<CoreTile>(?, ?)
27+
%c3 = aie.logical_tile<CoreTile>(?, ?)
28+
// Same base name "memX", but different element types -> not one tensor.
29+
aie.objectfifo @memX_0(%mem0, {%c0, %c1}, 2 : i32) : !aie.objectfifo<memref<32x32xbf16>>
30+
aie.objectfifo @memX_1(%mem1, {%c2, %c3}, 2 : i32) : !aie.objectfifo<memref<16x16xbf16>>
31+
// A back-pressure output per half closes its cycle and sets T = 2.
32+
aie.objectfifo @memC0(%c0, {%mem0}, 2 : i32) {repeat_count = 2 : i32} : !aie.objectfifo<memref<8x8xf32>>
33+
aie.objectfifo @memC1(%c2, {%mem1}, 2 : i32) {repeat_count = 2 : i32} : !aie.objectfifo<memref<8x8xf32>>
34+
}
35+
}

0 commit comments

Comments
 (0)