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
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
0 commit comments