MLIR-AIE 1.4.5.dev
AIEObjectFifoLiveness.cpp
Go to the documentation of this file.
1//===- AIEObjectFifoLiveness.cpp --------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Flag under-buffered cyclic objectFIFO dependencies that deadlock the array.
9//
10// A coupled multicast (broadcast) objectFIFO whose consumers back-pressure the
11// producer forms a dependency cycle. When the producer must re-acquire a slot
12// on a later trip (replay, T >= 2) while the prior trip's tokens are still
13// outstanding, the round-trip slack (2 * depth) can be smaller than the number
14// of outstanding tokens demanded by the coupled fan-out (array_fan * T). That
15// is a static, structural deadlock: the IR compiles clean and then hangs the
16// NPU at runtime.
17//
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:
26//
27// demand = array_fan * T (array_fan = the group's multicast
28// fan-out, summed across the components
29// it spans; T = max repeat_count among
30// the fifos in those components)
31// slack = 2 * depth (depth = min depth in the group)
32// DEADLOCK iff T >= 2 AND depth > 0 AND demand > slack
33//
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//
44// The T >= 2 replay guard avoids false positives on single-trip broadcasts
45// (which fan out once and drain monotonically). The analysis is sound for this
46// static SDF class and conservative elsewhere (it never errors outside a proven
47// cyclic under-buffered multicast); it is not a general deadlock detector.
48//
49//===----------------------------------------------------------------------===//
50
53
54#include "mlir/IR/BuiltinAttributes.h"
55#include "mlir/IR/BuiltinOps.h"
56#include "mlir/Pass/Pass.h"
57
58#include "llvm/ADT/DenseMap.h"
59#include "llvm/ADT/DenseSet.h"
60#include "llvm/ADT/SmallVector.h"
61#include "llvm/ADT/StringMap.h"
62#include "llvm/ADT/StringRef.h"
63
64#include <algorithm>
65#include <functional>
66#include <set>
67#include <string>
68#include <vector>
69
70namespace xilinx::AIE {
71#define GEN_PASS_DEF_AIEOBJECTFIFOLIVENESS
72#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
73} // namespace xilinx::AIE
74
75#define DEBUG_TYPE "aie-objectfifo-liveness"
76
77using namespace mlir;
78using namespace xilinx;
79using namespace xilinx::AIE;
80
81namespace {
82
83// Strip a trailing "_<digits>" group from a fifo name, mirroring the Phase-0
84// Python `re.sub(r'_\d+$', '', name)`. Requires at least one digit after the
85// final '_'; otherwise the name is returned unchanged.
86static StringRef nameBase(StringRef name) {
87 size_t us = name.rfind('_');
88 if (us == StringRef::npos || us + 1 >= name.size())
89 return name;
90 StringRef suffix = name.substr(us + 1);
91 for (char c : suffix)
92 if (c < '0' || c > '9')
93 return name;
94 return name.substr(0, us);
95}
96
97struct AIEObjectFifoLivenessPass
98 : xilinx::AIE::impl::AIEObjectFifoLivenessBase<AIEObjectFifoLivenessPass> {
99 void runOnOperation() override {
100 DeviceOp mod = getOperation();
101
102 struct F {
103 Operation *op;
104 long depth;
105 mlir::Value prod;
106 SmallVector<mlir::Value> cons;
107 long repeat;
108 StringRef name;
109 mlir::Type elemTy; // coupling signature (see nameBase gating below)
110 };
111 SmallVector<F> fifos;
112
113 mod.walk([&](xilinx::AIE::ObjectFifoCreateOp ofo) {
114 // Mirror the Phase-0 parser: it only matches a single-integer depth
115 // (`}, N : i32`); array-form per-tile depths are skipped entirely, so
116 // skip them here too to keep the dependency graph identical.
117 auto depthInt = dyn_cast<IntegerAttr>(ofo.getElemNumber());
118 if (!depthInt)
119 return;
120 F f;
121 f.op = ofo;
122 f.name = ofo.getSymName();
123 f.depth = depthInt.getInt();
124 f.prod = ofo.getProducerTile();
125 for (mlir::Value c : ofo.getConsumerTiles())
126 f.cons.push_back(c);
127 f.repeat = 1;
128 if (auto rc = ofo.getRepeatCount())
129 f.repeat = *rc;
130 f.elemTy = ofo.getElemType();
131 fifos.push_back(f);
132 });
133
134 unsigned n = fifos.size();
135
136 // build_dep_graph: group fifos by shared tile Value; for each tile, ins =
137 // fifos it consumes, outs = fifos it produces; add edges o<->i (data +
138 // back-pressure). Use Value identity as the tile key.
139 mlir::DenseMap<mlir::Value, SmallVector<unsigned>> consumes, produces;
140 for (unsigned i = 0; i < n; ++i) {
141 produces[fifos[i].prod].push_back(i);
142 for (mlir::Value c : fifos[i].cons)
143 consumes[c].push_back(i);
144 }
145 std::vector<std::set<unsigned>> adj(n);
146 mlir::DenseSet<mlir::Value> tiles;
147 for (auto &kv : consumes)
148 tiles.insert(kv.first);
149 for (auto &kv : produces)
150 tiles.insert(kv.first);
151 for (mlir::Value tile : tiles) {
152 auto &ins = consumes[tile];
153 auto &outs = produces[tile];
154 for (unsigned o : outs)
155 for (unsigned i : ins) {
156 adj[o].insert(i);
157 adj[i].insert(o);
158 }
159 }
160
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];
174 }
175 return x;
176 };
177 for (unsigned v = 0; v < n; ++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);
187
188 // SDF check, scoped PER coupled-multicast group -- NOT globally. A coupled
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.
208 struct Group {
209 long fan = 0, depth = 0;
210 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)
216 std::set<int> sccs;
217 };
218 SmallVector<Group> groupList;
219 llvm::StringMap<unsigned>
220 groupIdx; // name base -> owning group in groupList
221 for (unsigned i = 0; i < n; ++i) {
222 if (sccId[i] < 0 || fifos[i].cons.size() <= 1)
223 continue; // only a cyclic multicast couples a broadcast
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;
243 g.rep = i;
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);
255 }
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]);
261 }
262
263 // Replay guard (mirrors sdf_checker.py): the multicast back-pressure cycle
264 // only forms when the broadcast producer must RE-acquire a slot (trip t+1)
265 // while the prior trip's tokens are still outstanding -- i.e. T >= 2. A
266 // single-trip (T == 1) broadcast fans out once and drains monotonically, so
267 // it never deadlocks regardless of fan-out (confirmed: bundled whole_array
268 // matmul examples broadcast to the whole array at depth 2, T == 1, and RUN
269 // on device).
270 for (Group &g : groupList) {
271 long T = 1; // trip-count from the component(s) coupled to THIS broadcast
272 for (unsigned i = 0; i < n; ++i)
273 if (sccId[i] >= 0 && g.sccs.count(sccId[i]))
274 T = std::max(T, fifos[i].repeat);
275 long demand = g.fan * T;
276 long slack = 2 * g.depth;
277 if (g.depth <= 0 || T < 2 || demand <= slack)
278 continue;
279 long reqDepth = (demand + 1) / 2; // ceil(demand / 2)
280 fifos[g.rep].op->emitError()
281 << "objectFIFO @" << g.name
282 << " in a cyclic dependency requires depth >= " << reqDepth
283 << " for deadlock-free execution; allocated depth = " << g.depth
284 << ". (Coupled broadcast fan-out " << g.fan << " x trip-count " << T
285 << " = " << demand
286 << " outstanding tokens exceeds round-trip slack 2*depth = " << slack
287 << "; raise depth or reduce coupled consumers / output tiles.)";
288 signalPassFailure();
289 return;
290 }
291 }
292};
293
294} // namespace
295
296std::unique_ptr<OperationPass<DeviceOp>>
298 return std::make_unique<AIEObjectFifoLivenessPass>();
299}
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEObjectFifoLivenessPass()