MLIR-AIE 1.4.5.dev
AIEStreamDependencyAnalysis.cpp
Go to the documentation of this file.
1//===- AIEStreamDependencyAnalysis.cpp --------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
11
12#include "mlir/Dialect/Utils/StaticValueUtils.h"
13#include "mlir/IR/Matchers.h"
14#include "mlir/Interfaces/ControlFlowInterfaces.h"
15#include "mlir/Interfaces/LoopLikeInterface.h"
16
17#include "llvm/ADT/DenseSet.h"
18#include "llvm/ADT/GraphTraits.h"
19#include "llvm/ADT/SCCIterator.h"
20#include "llvm/ADT/STLExtras.h"
21#include "llvm/ADT/ScopeExit.h"
22#include "llvm/ADT/SetVector.h"
23#include "llvm/ADT/SparseBitVector.h"
24#include "llvm/Support/Debug.h"
25#include "llvm/Support/FormatVariadic.h"
26
27#include <algorithm>
28#include <deque>
29#include <set>
30#include <variant>
31
32#define DEBUG_TYPE "aie-stream-dependency"
33
34using namespace mlir;
35using namespace xilinx;
36using namespace xilinx::AIE;
37
38namespace {
39
40std::optional<TileID> tileOf(Value tile) {
41 if (auto tileOp = dyn_cast_or_null<TileOp>(tile.getDefiningOp()))
42 return tileOp.getTileID();
43 return std::nullopt;
44}
45
46std::optional<DmaChannelProgram> makeProgram(Operation *op, DeviceOp device) {
47 auto parentTile = [](Operation *op) -> std::optional<TileID> {
48 if (auto element = op->getParentOfType<TileElement>())
49 return tileOf(element.getTile());
50 return std::nullopt;
51 };
52 if (auto start = dyn_cast<DMAStartOp>(op)) {
53 std::optional<TileID> tile = parentTile(op);
54 if (!tile)
55 return std::nullopt;
57 op, {*tile, start.getChannelDir(), start.getChannelIndex()}};
58 p.passes += start.getRepeatCount();
59 llvm::SmallPtrSet<Block *, 8> seen;
60 for (Block *b = start.getDest(); b;) {
61 if (!seen.insert(b).second) {
62 p.loops = true;
63 p.loopStart = llvm::find(p.bds, b) - p.bds.begin();
64 break;
65 }
66 p.bds.push_back(b);
67 auto next = dyn_cast<NextBDOp>(b->getTerminator());
68 b = next ? next.getDest() : nullptr;
69 }
70 return p;
71 }
72 if (auto dma = dyn_cast<DMAOp>(op)) {
73 std::optional<TileID> tile = parentTile(op);
74 if (!tile)
75 return std::nullopt;
77 {*tile, dma.getChannelDir(), dma.getChannelIndex()}};
78 p.passes += dma.getRepeatCount();
79 for (Region &r : dma.getBds())
80 p.regions.push_back(&r);
81 p.loops = dma.getLoop();
82 return p;
83 }
84 if (auto task = dyn_cast<AIEX::DMAConfigureTaskOp>(op)) {
85 std::optional<TileID> tile = tileOf(task.getTile());
86 if (!tile)
87 return std::nullopt;
89 op, {*tile, task.getDirection(), static_cast<int>(task.getChannel())}};
90 p.regions.push_back(&task.getBody());
91 p.passes += task.getRepeatCount();
92 return p;
93 }
94 if (auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(op)) {
95 auto alloc = ShimDMAAllocationOp::getForSymbol(
96 device, task.getAlloc().getRootReference());
97 if (!alloc)
98 return std::nullopt;
99 std::optional<TileID> tile = tileOf(alloc.getTile());
100 if (!tile)
101 return std::nullopt;
103 {*tile, alloc.getChannelDir(),
104 static_cast<int>(alloc.getChannelIndex())}};
105 p.regions.push_back(&task.getBody());
106 p.passes += task.getRepeatCount();
107 return p;
108 }
109 return std::nullopt;
110}
111
112// Every DMA channel program: aie.dma_start and aie.dma inside the tile DMA
113// ops, and runtime-configured tasks.
114SmallVector<DmaChannelProgram> collectDmaPrograms(DeviceOp device) {
115 SmallVector<DmaChannelProgram> programs;
116 device.walk([&](Operation *op) {
117 if (isa<DMAStartOp, DMAOp, AIEX::DMAConfigureTaskOp,
118 AIEX::DMAConfigureTaskForOp>(op))
119 if (std::optional<DmaChannelProgram> p = makeProgram(op, device))
120 programs.push_back(std::move(*p));
121 });
122 return programs;
123}
124
125template <typename OpT>
126void forEachInProgram(const DmaChannelProgram &p,
127 llvm::function_ref<void(OpT)> fn) {
128 for (Block *b : p.bds)
129 for (auto op : b->getOps<OpT>())
130 fn(op);
131 for (Region *r : p.regions)
132 r->walk([&](OpT op) { fn(op); });
133}
134
135std::optional<TileDMAChannel> channelOfSymbol(DeviceOp device,
136 StringRef symbol) {
137 auto alloc = ShimDMAAllocationOp::getForSymbol(device, symbol);
138 if (!alloc)
139 return std::nullopt;
140 std::optional<TileID> tile = tileOf(alloc.getTile());
141 if (!tile)
142 return std::nullopt;
143 return TileDMAChannel{*tile, alloc.getChannelDir(),
144 static_cast<int>(alloc.getChannelIndex())};
145}
146
147// The ops that can create `task`, following it back through the results and
148// block arguments of runtime control flow (scf.for iter_args, scf.if results).
149// Empty when some value it can come from has no creator here.
150SmallVector<Operation *> taskCreators(Value task) {
151 SmallVector<Operation *> creators;
152 llvm::SmallPtrSet<Value, 8> seen;
153 SmallVector<Value> worklist{task};
154 while (!worklist.empty()) {
155 Value v = worklist.pop_back_val();
156 if (!seen.insert(v).second)
157 continue;
158 Operation *op = v.getDefiningOp();
159 if (isa_and_nonnull<AIEX::DMAConfigureTaskOp, AIEX::DMAConfigureTaskForOp,
160 AIEX::DMAStartBdChainOp, AIEX::DMAStartBdChainForOp>(
161 op)) {
162 creators.push_back(op);
163 continue;
164 }
165 Operation *owner =
166 op ? op : cast<BlockArgument>(v).getOwner()->getParentOp();
167 auto branch = dyn_cast_or_null<RegionBranchOpInterface>(owner);
168 if (!branch)
169 return {};
170 RegionBranchInverseSuccessorMapping mapping;
171 branch.getSuccessorInputOperandMapping(mapping);
172 auto it = mapping.find(v);
173 if (it == mapping.end())
174 return {};
175 for (OpOperand *operand : it->second)
176 worklist.push_back(operand->get());
177 }
178 return creators;
179}
180
181// The channels `task` can run on; empty when that is not known here.
182SmallVector<TileDMAChannel> channelsOfTask(DeviceOp device, Value task) {
183 SmallVector<TileDMAChannel> keys;
184 for (Operation *op : taskCreators(task)) {
185 std::optional<TileDMAChannel> key;
186 auto onTile = [&](Value tileValue, DMAChannelDir dir, int channel) {
187 if (std::optional<TileID> tile = tileOf(tileValue))
188 key = TileDMAChannel{*tile, dir, channel};
189 };
190 if (auto configure = dyn_cast<AIEX::DMAConfigureTaskOp>(op))
191 onTile(configure.getTile(), configure.getDirection(),
192 configure.getChannel());
193 else if (auto chain = dyn_cast<AIEX::DMAStartBdChainOp>(op))
194 onTile(chain.getTile(), chain.getDirection(), chain.getChannel());
195 else if (auto configure = dyn_cast<AIEX::DMAConfigureTaskForOp>(op))
196 key = channelOfSymbol(device, configure.getAlloc().getRootReference());
197 else if (auto chain = dyn_cast<AIEX::DMAStartBdChainForOp>(op))
198 key = channelOfSymbol(device, chain.getAlloc());
199 if (!key)
200 return {};
201 if (!llvm::is_contained(keys, *key))
202 keys.push_back(*key);
203 }
204 return keys;
205}
206
207// Visits the ops in `region` in program order, going through the body of
208// each loop twice, so that what one iteration does follows all the previous
209// iteration did.
210void walkLoopsTwice(Region &region, function_ref<void(Operation *)> fn) {
211 for (Operation &op : region.getOps()) {
212 fn(&op);
213 auto branch = dyn_cast<RegionBranchOpInterface>(op);
214 int passes = branch && branch.hasLoop() ? 2 : 1;
215 for (int pass = 0; pass < passes; pass++)
216 for (Region &r : op.getRegions())
217 walkLoopsTwice(r, fn);
218 }
219}
220
221// Packet ids each sending channel is programmed with.
222std::map<TileDMAChannel, std::set<int>> collectSentPacketIDs(DeviceOp device) {
223 std::map<TileDMAChannel, std::set<int>> ids;
224 for (const DmaChannelProgram &p : collectDmaPrograms(device)) {
225 if (p.dma.dir != DMAChannelDir::MM2S)
226 continue;
227 forEachInProgram<DMABDOp>(p, [&](DMABDOp bd) {
228 if (std::optional<PacketInfoAttr> packet = bd.getPacket())
229 ids[p.dma].insert(packet->assignedId());
230 });
231 }
232 for (auto alloc : device.getOps<ShimDMAAllocationOp>()) {
233 std::optional<PacketInfoAttr> packet = alloc.getPacket();
234 std::optional<TileID> tile = tileOf(alloc.getTile());
235 if (packet && tile)
236 ids[{*tile, alloc.getChannelDir(),
237 static_cast<int>(alloc.getChannelIndex())}]
238 .insert(packet->assignedId());
239 }
240 device.walk([&](AIEX::NpuDmaMemcpyNdOp memcpy) {
241 std::optional<PacketInfoAttr> packet = memcpy.getPacket();
242 if (!packet)
243 return;
244 if (std::optional<TileDMAChannel> key =
245 channelOfSymbol(device, memcpy.getMetadata().getRootReference()))
246 ids[*key].insert(packet->assignedId());
247 });
248 return ids;
249}
250
251class StreamTracer {
252public:
253 explicit StreamTracer(DeviceOp device)
254 : maxPacketID(getTargetModel(device).getMaxPacketId()) {
255 for (auto sb : device.getOps<SwitchboxOp>())
256 switchboxes[sb.getTileOp().getTileID()] = sb;
257 for (auto mux : device.getOps<ShimMuxOp>())
258 shimMuxes[mux.getTileOp().getTileID()] = mux;
259 sentIDs = collectSentPacketIDs(device);
260 }
261
262 std::vector<RoutedStream> trace() {
263 for (auto &[tile, mux] : shimMuxes)
264 for (Port p : inputPorts(mux.getConnections()))
265 if (p.bundle != WireBundle::North)
266 traceFrom({tile, p}, mux, p);
267 for (auto &[tile, sb] : switchboxes)
268 for (Port p : inputPorts(sb.getConnections()))
269 if (!isDirectional(p.bundle))
270 traceFrom({tile, p}, sb, p);
271 return std::move(streams);
272 }
273
274private:
275 struct Hop {
276 Interconnect interconnect;
277 Port input;
278 };
279
280 static SmallVector<Port> inputPorts(Region &connections) {
281 SmallVector<Port> ports;
282 for (Operation &op : connections.front()) {
283 std::optional<Port> p;
284 if (auto connect = dyn_cast<ConnectOp>(op))
285 p = connect.sourcePort();
286 else if (auto rules = dyn_cast<PacketRulesOp>(op))
287 p = rules.sourcePort();
288 if (p && !llvm::is_contained(ports, *p))
289 ports.push_back(*p);
290 }
291 return ports;
292 }
293
294 void traceFrom(StreamEndpoint src, Interconnect interconnect, Port input) {
295 std::optional<std::set<int>> ids;
296 // A tile sends from its MM2S channel on the port of the same number.
297 if (src.port.bundle == WireBundle::DMA) {
298 auto it = sentIDs.find({src.tile, DMAChannelDir::MM2S, src.port.channel});
299 if (it != sentIDs.end())
300 ids = it->second;
301 }
302 if (!ids) {
303 step(src, {interconnect, input}, std::nullopt);
304 return;
305 }
306 for (int id : *ids)
307 step(src, {interconnect, input}, id);
308 }
309
310 // The switchbox input a master port of `from` drives, or the tile port it
311 // leaves the fabric at.
312 std::variant<Hop, StreamEndpoint> follow(Interconnect from, Port out) {
313 TileID here{from.colIndex(), from.rowIndex()};
314 if (isa<ShimMuxOp>(from)) {
315 if (out.bundle == WireBundle::North)
316 if (auto sb = switchboxes.find(here); sb != switchboxes.end())
317 return Hop{sb->second, {WireBundle::South, out.channel}};
318 return StreamEndpoint{here, out};
319 }
320 if (!isDirectional(out.bundle))
321 return StreamEndpoint{here, out};
322 if (out.bundle == WireBundle::South)
323 if (auto mux = shimMuxes.find(here); mux != shimMuxes.end())
324 return Hop{mux->second, {WireBundle::North, out.channel}};
325 TileID next = here;
326 WireBundle in = WireBundle::South;
327 switch (out.bundle) {
328 case WireBundle::North:
329 next.row++;
330 in = WireBundle::South;
331 break;
332 case WireBundle::South:
333 next.row--;
334 in = WireBundle::North;
335 break;
336 case WireBundle::East:
337 next.col++;
338 in = WireBundle::West;
339 break;
340 default:
341 next.col--;
342 in = WireBundle::East;
343 break;
344 }
345 if (auto sb = switchboxes.find(next); sb != switchboxes.end())
346 return Hop{sb->second, {in, out.channel}};
347 return StreamEndpoint{here, out};
348 }
349
350 // Follows the stream `src` sends with `id` on from `hop`, along every
351 // branch that does not come back to an input it already took.
352 void step(StreamEndpoint src, Hop hop, std::optional<int> id) {
353 std::pair<Operation *, Port> input{hop.interconnect, hop.input};
354 if (!onPath.insert(input).second)
355 return;
356 llvm::scope_exit leave([&] { onPath.erase(input); });
357 auto sb = dyn_cast<SwitchboxOp>(hop.interconnect.getOperation());
358 Block &b = hop.interconnect.getConnections().front();
359
360 auto next = [&](Port out, std::optional<int> nextID,
361 std::optional<int> arbiter, bool keepsPktHeader = false) {
362 if (sb)
363 path.push_back({sb.getTileOp().getTileID(), hop.input, arbiter});
364 llvm::scope_exit backtrack([&] {
365 if (sb)
366 path.pop_back();
367 });
368 std::variant<Hop, StreamEndpoint> to = follow(hop.interconnect, out);
369 if (auto *endpoint = std::get_if<StreamEndpoint>(&to))
370 streams.push_back({src, *endpoint, nextID, keepsPktHeader, path});
371 else
372 step(src, std::get<Hop>(to), nextID);
373 };
374
375 for (auto connect : b.getOps<ConnectOp>())
376 if (connect.sourcePort() == hop.input)
377 next(connect.destPort(), id, std::nullopt);
378
379 for (auto rules : b.getOps<PacketRulesOp>()) {
380 if (rules.sourcePort() != hop.input)
381 continue;
382 auto route = [&](PacketRuleOp rule, int ruleID) {
383 std::optional<int> arbiter;
384 if (auto amsel = rule.getAmsel().getDefiningOp<AMSelOp>())
385 arbiter = amsel.arbiterIndex();
386 for (auto masterSet : b.getOps<MasterSetOp>())
387 if (llvm::is_contained(masterSet.getAmsels(), rule.getAmsel()))
388 next(masterSet.destPort(), ruleID, arbiter,
389 masterSet.keepsPktHeader());
390 };
391 for (int packetID = id.value_or(0); packetID <= id.value_or(maxPacketID);
392 ++packetID)
393 for (auto rule : rules.getRules().front().getOps<PacketRuleOp>())
394 if ((packetID & rule.maskInt()) ==
395 (rule.valueInt() & rule.maskInt())) {
396 route(rule, packetID);
397 break;
398 }
399 }
400 }
401
402 std::map<TileID, SwitchboxOp> switchboxes;
403 std::map<TileID, ShimMuxOp> shimMuxes;
404 std::map<TileDMAChannel, std::set<int>> sentIDs;
405 std::vector<RoutedStream> streams;
406 // The interconnect inputs the stream `step` follows has taken, and the
407 // switchboxes it has passed.
408 llvm::DenseSet<std::pair<Operation *, Port>> onPath;
409 SmallVector<StreamHop, 8> path;
410 int maxPacketID;
411};
412
413} // namespace
414
415std::vector<RoutedStream> AIE::traceRoutedStreams(DeviceOp device) {
416 return StreamTracer(device).trace();
417}
418
419std::vector<RoutedStream> AIE::requestedStreams(DeviceOp device) {
420 std::vector<RoutedStream> streams;
421 for (auto flow : device.getOps<FlowOp>()) {
422 std::optional<TileID> srcTile = tileOf(flow.getSource());
423 std::optional<TileID> dstTile = tileOf(flow.getDest());
424 if (srcTile && dstTile)
425 streams.push_back(
426 {{*srcTile, {flow.getSourceBundle(), flow.sourceIndex()}},
427 {*dstTile, {flow.getDestBundle(), flow.destIndex()}},
428 std::nullopt});
429 }
430 // The router writes one keep_pkt_header per destination port, the last
431 // packet flow's into it.
432 DenseMap<std::pair<TileID, Port>, std::optional<bool>> keepAt;
433 for (auto flow : device.getOps<PacketFlowOp>())
434 for (auto dst : flow.getPorts().front().getOps<PacketDestOp>())
435 if (std::optional<TileID> dstTile = tileOf(dst.getTile()))
436 keepAt[{*dstTile, dst.port()}] = flow.getKeepPktHeader();
437 for (auto flow : device.getOps<PacketFlowOp>()) {
438 Block &b = flow.getPorts().front();
439 for (auto src : b.getOps<PacketSourceOp>()) {
440 std::optional<TileID> srcTile = tileOf(src.getTile());
441 if (!srcTile)
442 continue;
443 for (auto dst : b.getOps<PacketDestOp>())
444 if (std::optional<TileID> dstTile = tileOf(dst.getTile()))
445 streams.push_back(
446 {{*srcTile, src.port()},
447 {*dstTile, dst.port()},
448 flow.IDInt(),
449 keepsPktHeader(*dstTile, dst.port(),
450 keepAt.lookup({*dstTile, dst.port()})),
451 {},
452 static_cast<int>(flow.getMask().value_or(~0))});
453 }
454 }
455 return streams;
456}
457
459 ArrayRef<RoutedStream> streams)
460 : device(device), streams(streams) {
461 for (DmaChannelProgram &p : collectDmaPrograms(device))
462 programs[p.dma].push_back(std::move(p));
463 for (const auto &[dma, channelPrograms] : programs)
464 for (const DmaChannelProgram &p : channelPrograms)
465 forEachInProgram<UseLockOp>(
466 p, [&](UseLockOp use) { lockUseProgram[use] = &p; });
467 device.walk([&](AIEX::NpuDmaMemcpyNdOp memcpy) {
468 if (std::optional<TileDMAChannel> dma =
469 channelOfSymbol(device, memcpy.getMetadata().getRootReference()))
470 memcpys[*dma].push_back(memcpy);
471 });
472}
473
474// The BD blocks of a program in the order its channel runs them.
475static SmallVector<Block *> chainBlocks(const DmaChannelProgram &p) {
476 SmallVector<Block *> sequence(p.bds);
477 for (Region *r : p.regions)
478 sequence.push_back(&r->front());
479 return sequence;
480}
481
482// Index into `sequence` of the block a program runs at `step`: the blocks
483// before the cycle once, then the cycle over and over. A looping chain cycles
484// from where it returns to, a repeated one over the whole chain.
485static size_t blockAt(const DmaChannelProgram &p, size_t size, uint64_t step) {
486 size_t start = p.loops ? p.loopStart : 0;
487 if (step < start)
488 return step;
489 return start + (step - start) % (size - start);
490}
491
492// The value `use` acquires or releases, if it is a constant.
493static std::optional<int64_t> lockAmount(UseLockOp use) {
494 APInt amount;
495 if (!use.getLock().getDefiningOp<LockOp>() ||
496 !matchPattern(use.getValue(), m_ConstantInt(&amount)))
497 return std::nullopt;
498 return amount.getSExtValue();
499}
500
501// The tokens `use` takes or gives, if that is a known count.
502static std::optional<uint64_t> lockTokens(UseLockOp use) {
503 std::optional<int64_t> n = lockAmount(use);
504 if (!n || *n < 0)
505 return std::nullopt;
506 return *n;
507}
508
509constexpr int maxBDSteps = 1024;
510
511std::optional<uint64_t>
513 if (stream.src.port.bundle != WireBundle::DMA)
514 return std::nullopt;
515 TileDMAChannel dma{stream.src.tile, DMAChannelDir::MM2S,
516 stream.src.port.channel};
517 constexpr uint64_t pktHeaderBytes = 4;
518 auto headerBytes = [&](std::optional<PacketInfoAttr> packet) -> uint64_t {
519 return packet && stream.keepsPktHeader ? pktHeaderBytes : 0;
520 };
521 auto bytesOf = [&](DMABDOp bd) -> uint64_t {
522 if (!stream.carries(bd.getPacket()))
523 return 0;
524 return bd.getLenInBytes() + headerBytes(bd.getPacket());
525 };
526 bool known = false;
527 uint64_t total = 0;
528 if (auto it = programs.find(dma); it != programs.end()) {
529 for (const DmaChannelProgram &p : it->second) {
530 bool carried = false;
531 uint64_t bytes = 0;
532 forEachInProgram<DMABDOp>(p, [&](DMABDOp bd) {
533 if (!stream.carries(bd.getPacket()))
534 return;
535 carried = true;
536 bytes += bytesOf(bd);
537 });
538 known = true;
539 if (!carried)
540 continue;
541 if (p.loops) {
542 std::optional<uint64_t> looped = loopedVolume(p, bytesOf);
543 if (!looped)
544 return std::nullopt;
545 total += *looped;
546 continue;
547 }
548 uint64_t runs = p.passes;
549 if (isa<AIEX::DMAConfigureTaskOp, AIEX::DMAConfigureTaskForOp>(p.op)) {
550 auto task = dyn_cast<AIEX::DMAConfigureTaskOp>(p.op);
551 auto taskFor = dyn_cast<AIEX::DMAConfigureTaskForOp>(p.op);
552 if ((task && task.getRepeatCountVal()) ||
553 (taskFor && taskFor.getRepeatCountVal()))
554 return std::nullopt;
555 runs = 0;
556 for (Operation *user : p.op->getUsers()) {
557 if (isa<AIEX::DMAAwaitTaskOp, AIEX::DMAFreeTaskOp>(user))
558 continue;
559 // Any other user, such as an scf.yield or an scf.for init, can
560 // forward the task to starts this does not count.
561 auto start = dyn_cast<AIEX::DMAStartTaskOp>(user);
562 if (!start || start.hasRepeatParameter())
563 return std::nullopt;
564 if (user->getParentOfType<LoopLikeOpInterface>())
565 return std::nullopt;
566 runs += p.passes;
567 }
568 }
569 total += bytes * runs;
570 }
571 }
572 if (auto it = memcpys.find(dma); it != memcpys.end()) {
573 for (Operation *op : it->second) {
574 auto memcpy = cast<AIEX::NpuDmaMemcpyNdOp>(op);
575 std::optional<PacketInfoAttr> packet = memcpy.getPacket();
576 if (!packet)
577 packet = ShimDMAAllocationOp::getForSymbol(
578 device, memcpy.getMetadata().getRootReference())
579 .getPacket();
580 if (!stream.carries(packet))
581 continue;
582 auto type = dyn_cast<BaseMemRefType>(memcpy.getMemref().getType());
583 if (memcpy->getParentOfType<LoopLikeOpInterface>() || !type ||
584 !type.getElementType().isIntOrFloat())
585 return std::nullopt;
586 std::optional<SmallVector<int64_t>> sizes =
587 getConstantIntValues(memcpy.getMixedSizes());
588 if (!sizes)
589 return std::nullopt;
590 uint64_t elements = 1;
591 for (int64_t n : *sizes)
592 elements *= n;
593 // The outermost dimension re-runs the BD (as iterations, or as repeats
594 // when its stride is 0), and each run sends its own header.
595 uint64_t runs = sizes->front();
596 known = true;
597 total += elements * type.getElementTypeBitWidth() / 8 +
598 runs * headerBytes(packet);
599 }
600 }
601 if (!known)
602 return std::nullopt;
603 return total;
604}
605
606std::optional<uint64_t> StreamVolumeAnalysis::loopedVolume(
607 const DmaChannelProgram &program,
608 function_ref<uint64_t(DMABDOp)> bytesOf) const {
609 uint64_t bytes = 0;
610 if (!walkLoop(program, [&](DMABDOp bd) { bytes += bytesOf(bd); }))
611 return std::nullopt;
612 return bytes;
613}
614
615bool StreamVolumeAnalysis::walkLoop(const DmaChannelProgram &p,
616 function_ref<void(DMABDOp)> visit) const {
617 // AIE1 locks hold a state rather than count tokens.
618 if (getTargetModel(device).getTargetArch() == AIEArch::AIE1 ||
619 !visiting.insert(&p).second)
620 return false;
621 llvm::scope_exit done([&] { visiting.erase(&p); });
622 SmallVector<Block *> sequence = chainBlocks(p);
623 if (sequence.empty())
624 return false;
625 DenseMap<Operation *, uint64_t> tokens;
626 for (int step = 0; step < maxBDSteps; step++) {
627 for (Operation &bdOp : *sequence[blockAt(p, sequence.size(), step)]) {
628 if (auto use = dyn_cast<UseLockOp>(bdOp)) {
629 auto lock = use.getLock().getDefiningOp<LockOp>();
630 std::optional<uint64_t> n = lockTokens(use);
631 if (!n || (!use.release() && !use.acquireGE()))
632 return false;
633 auto it = tokens.find(lock);
634 if (it == tokens.end()) {
635 std::optional<uint64_t> others = tokensFromOthers(lock, &p);
636 if (!others)
637 return false;
638 it = tokens.try_emplace(lock, lock.getInit().value_or(0) + *others)
639 .first;
640 }
641 if (use.release())
642 it->second += *n;
643 else if (it->second < *n)
644 return true;
645 else
646 it->second -= *n;
647 } else if (auto bd = dyn_cast<DMABDOp>(bdOp)) {
648 visit(bd);
649 }
650 }
651 }
652 return false;
653}
654
655// Whether `op` runs at most once each time its core runs its body.
656static bool onceInCore(Operation *op) {
657 for (Operation *parent = op->getParentOp(); parent;
658 op = parent, parent = parent->getParentOp()) {
659 if (!op->getParentRegion()->hasOneBlock() ||
660 isa<LoopLikeOpInterface>(parent))
661 return false;
662 if (isa<CoreOp>(parent))
663 return true;
664 }
665 return false;
666}
667
668std::optional<uint64_t>
669StreamVolumeAnalysis::tokensFromOthers(LockOp lock,
670 const DmaChannelProgram *self) const {
671 llvm::SetVector<const DmaChannelProgram *> releasers;
672 uint64_t tokens = 0;
673 for (Operation *user : lock->getUsers()) {
674 auto use = dyn_cast<UseLockOp>(user);
675 if (!use)
676 return std::nullopt;
677 if (!use.release())
678 continue;
679 auto owner = lockUseProgram.find(use);
680 if (owner == lockUseProgram.end()) {
681 std::optional<uint64_t> n = lockTokens(use);
682 if (!onceInCore(use) || !n)
683 return std::nullopt;
684 tokens += *n;
685 continue;
686 }
687 if (owner->second != self)
688 releasers.insert(owner->second);
689 }
690 for (const DmaChannelProgram *program : releasers) {
691 std::optional<uint64_t> n = releasesOver(*program, lock);
692 if (!n)
693 return std::nullopt;
694 tokens += *n;
695 }
696 return tokens;
697}
698
699std::optional<bool>
701 const RoutedStream &then) const {
702 if (first.src != then.src)
703 return std::nullopt;
704 if (!first.packetID || !then.packetID ||
705 ((*first.packetID ^ *then.packetID) & first.packetMask &
706 then.packetMask) == 0)
707 return true;
708 if (first.src.port.bundle != WireBundle::DMA)
709 return std::nullopt;
710 TileDMAChannel dma{first.src.tile, DMAChannelDir::MM2S,
711 first.src.port.channel};
712 auto it = programs.find(dma);
713 if (it == programs.end() || it->second.size() != 1 ||
714 !isa<DMAStartOp, DMAOp>(it->second.front().op) || memcpys.count(dma))
715 return std::nullopt;
716 const DmaChannelProgram &p = it->second.front();
717 bool sent = false, after = false;
718 auto visit = [&](DMABDOp bd) {
719 after |= sent && then.carries(bd.getPacket());
720 sent |= first.carries(bd.getPacket());
721 };
722 if (p.loops) {
723 if (!walkLoop(p, visit))
724 return std::nullopt;
725 return after;
726 }
727 SmallVector<Block *> sequence = chainBlocks(p);
728 for (uint64_t pass = 0; pass < p.passes && !after; pass++)
729 for (Block *b : sequence)
730 for (DMABDOp bd : b->getOps<DMABDOp>())
731 visit(bd);
732 return after;
733}
734
735std::optional<uint64_t>
736StreamVolumeAnalysis::releasesOver(const DmaChannelProgram &p,
737 LockOp lock) const {
738 if (!isa<DMAStartOp, DMAOp>(p.op))
739 return std::nullopt;
740 SmallVector<Block *> sequence = chainBlocks(p);
741 if (!p.loops) {
742 uint64_t perPass = 0;
743 for (Block *b : sequence)
744 for (auto use : b->getOps<UseLockOp>())
745 if (use.release() && use.getLock().getDefiningOp() == lock) {
746 std::optional<uint64_t> n = lockTokens(use);
747 if (!n)
748 return std::nullopt;
749 perPass += *n;
750 }
751 return perPass * p.passes;
752 }
753 // A looping receiver finishes only the BDs what it is sent fills.
754 if (p.dma.dir != DMAChannelDir::S2MM || sequence.empty())
755 return std::nullopt;
756 StreamEndpoint endpoint{p.dma.tile, {WireBundle::DMA, p.dma.channel}};
757 uint64_t received = 0;
758 for (const RoutedStream &s : streams) {
759 if (s.dst != endpoint)
760 continue;
761 std::optional<uint64_t> bytes = sendVolume(s);
762 if (!bytes)
763 return std::nullopt;
764 received += *bytes;
765 }
766 uint64_t filled = 0, tokens = 0;
767 for (int step = 0; step < maxBDSteps; step++) {
768 for (Operation &bdOp : *sequence[blockAt(p, sequence.size(), step)]) {
769 if (auto bd = dyn_cast<DMABDOp>(bdOp)) {
770 if (filled + bd.getLenInBytes() > received)
771 return tokens;
772 filled += bd.getLenInBytes();
773 } else if (auto use = dyn_cast<UseLockOp>(bdOp)) {
774 if (!use.release() || use.getLock().getDefiningOp() != lock)
775 continue;
776 std::optional<uint64_t> n = lockTokens(use);
777 if (!n)
778 return std::nullopt;
779 tokens += *n;
780 }
781 }
782 }
783 return std::nullopt;
784}
785
786static std::optional<uint64_t> programCapacity(const DmaChannelProgram &p,
787 DeviceOp device) {
788 if (!isa<DMAStartOp, DMAOp>(p.op))
789 return 0;
790
791 // Run the BD chain from its initial lock values until an acquire blocks.
792 SmallVector<Block *> sequence = chainBlocks(p);
793 if (sequence.empty())
794 return 0;
795 // AIE1 locks hold a state an acquire waits to equal and a release sets; later
796 // locks count, and an acquire waiting for an exact count is taken to block.
797 bool stateLocks = getTargetModel(device).getTargetArch() == AIEArch::AIE1;
798 DenseMap<Operation *, int64_t> lockValues;
799 uint64_t bytes = 0;
800 // Lock values and bytes when the cycle last began. A cycle that leaves every
801 // lock where it found it, or with more tokens, runs again the same way.
802 std::optional<DenseMap<Operation *, int64_t>> cycleLocks;
803 uint64_t cycleBytes = 0;
804 size_t start = p.loops ? p.loopStart : 0;
805 for (uint64_t step = 0;; step++) {
806 if (!p.loops && step >= p.passes * sequence.size())
807 return bytes;
808 size_t i = blockAt(p, sequence.size(), step);
809 if (i == start) {
810 if (cycleLocks && cycleLocks->size() == lockValues.size() &&
811 llvm::all_of(lockValues, [&](const auto &lv) {
812 int64_t before = cycleLocks->at(lv.first);
813 return stateLocks ? lv.second == before : lv.second >= before;
814 })) {
815 if (p.loops)
816 return std::nullopt;
817 return bytes +
818 (p.passes - step / sequence.size()) * (bytes - cycleBytes);
819 }
820 cycleLocks = lockValues;
821 cycleBytes = bytes;
822 }
823 // Past the analysis limit, what it took in so far is a safe capacity.
824 if (step >= static_cast<uint64_t>(maxBDSteps))
825 return bytes;
826 for (Operation &bdOp : *sequence[i]) {
827 if (auto use = dyn_cast<UseLockOp>(bdOp)) {
828 std::optional<int64_t> n = lockAmount(use);
829 if (!n)
830 return 0;
831 auto lock = use.getLock().getDefiningOp<LockOp>();
832 int64_t &value =
833 lockValues.try_emplace(lock, lock.getInit().value_or(0))
834 .first->second;
835 if (stateLocks) {
836 if (use.release())
837 value = *n;
838 else if (value != *n)
839 return bytes;
840 } else if (use.release()) {
841 value += *n;
842 } else if (!use.acquireGE() || value < *n) {
843 return bytes;
844 } else {
845 value -= *n;
846 }
847 } else if (auto bd = dyn_cast<DMABDOp>(bdOp)) {
848 bytes += bd.getLenInBytes();
849 }
850 }
851 }
852}
853
854std::optional<uint64_t>
856 if (endpoint.port.bundle != WireBundle::DMA)
857 return 0;
858 auto it = programs.find(
859 {endpoint.tile, DMAChannelDir::S2MM, endpoint.port.channel});
860 if (it == programs.end())
861 return 0;
862 std::optional<uint64_t> capacity;
863 for (const DmaChannelProgram &p : it->second)
864 if (std::optional<uint64_t> c = programCapacity(p, device))
865 capacity = std::min(*c, capacity.value_or(*c));
866 return capacity;
867}
868
870 ArrayRef<RoutedStream> streams) const {
871 std::optional<uint64_t> capacity = receiveCapacity(endpoint);
872 if (!capacity)
873 return false;
874 uint64_t sent = 0;
875 for (const RoutedStream &s : streams) {
876 if (s.dst != endpoint)
877 continue;
878 std::optional<uint64_t> bytes = sendVolume(s);
879 if (!bytes)
880 return true;
881 sent += *bytes;
882 }
883 return sent > *capacity;
884}
885
887 ArrayRef<RoutedStream> streams,
888 const StreamVolumeAnalysis &volumes) {
889 std::set<TileDMAChannel> neverFull;
890 for (const RoutedStream &s : streams)
891 if (s.dst.port.bundle == WireBundle::DMA &&
892 !volumes.canFill(s.dst, streams))
893 neverFull.insert({s.dst.tile, DMAChannelDir::S2MM, s.dst.port.channel});
894 auto waitsOnLocks = [&](unsigned agent) {
895 const Agent &a = agents[agent];
896 return a.kind == Agent::Kind::Core || !neverFull.count(a.dma);
897 };
898
899 // Who acquires and who releases each lock.
900 DenseMap<Operation *, llvm::SetVector<unsigned>> acquirers, releasers;
901 auto noteLock = [&](UseLockOp use, unsigned agent) {
902 Operation *lock = use.getLock().getDefiningOp();
903 if (!lock)
904 return;
905 if (use.getAction() == LockAction::Release)
906 releasers[lock].insert(agent);
907 else
908 acquirers[lock].insert(agent);
909 };
910
911 // Agents whose waits the design spells out; see the end of this constructor
912 // for the rest.
913 for (auto core : device.getOps<CoreOp>()) {
914 unsigned agent = getOrCreate(Agent::core(core.getTileOp().getTileID()));
915 modeled.insert(agent);
916 core.walk([&](UseLockOp use) { noteLock(use, agent); });
917 }
918 for (const DmaChannelProgram &p : collectDmaPrograms(device)) {
919 unsigned agent = getOrCreate(Agent::channel(p.dma));
920 modeled.insert(agent);
921 forEachInProgram<UseLockOp>(p,
922 [&](UseLockOp use) { noteLock(use, agent); });
923 }
924 // Document order, not pointer order: edge order picks the chain a
925 // diagnostic reports.
926 for (auto lock : device.getOps<LockOp>()) {
927 auto waiting = acquirers.find(lock), it = releasers.find(lock);
928 if (waiting == acquirers.end() || it == releasers.end())
929 continue;
930 for (unsigned p : waiting->second)
931 for (unsigned q : it->second)
932 if (p != q && waitsOnLocks(p))
933 addEdge(p, q, EdgeKind::Lock);
934 }
935
936 for (const RoutedStream &s : streams) {
937 std::optional<unsigned> from, to;
938 if (std::optional<Agent> agent = Agent::at(s.src, true))
939 from = getOrCreate(*agent);
940 if (std::optional<Agent> agent = Agent::at(s.dst, false))
941 to = getOrCreate(*agent);
942 if (!from || !to || *from == *to)
943 continue;
944 addEdge(*from, *to, EdgeKind::Stream, /*sends=*/true);
945 addEdge(*to, *from, EdgeKind::Stream, /*sends=*/false, /*receives=*/true);
946 }
947
948 // The host issues a channel's transfer only after every wait before it in
949 // the runtime sequence completes. Each issue is modeled: a later one can
950 // follow waits the first did not. A wait on a channel not known here may be
951 // on any the sequence issues, and an issue on one, or a raw register write,
952 // may start any channel.
953 unsigned numStreamAgents = agents.size();
954 for (auto sequence : device.getOps<RuntimeSequenceOp>()) {
955 auto bySymbol = [&](SymbolRefAttr symbol) -> SmallVector<TileDMAChannel> {
956 if (std::optional<TileDMAChannel> key =
957 channelOfSymbol(device, symbol.getRootReference()))
958 return {*key};
959 return {};
960 };
961 auto issued =
962 [&](Operation *op) -> std::optional<SmallVector<TileDMAChannel>> {
963 if (auto memcpy = dyn_cast<AIEX::NpuDmaMemcpyNdOp>(op))
964 return bySymbol(memcpy.getMetadata());
965 if (auto start = dyn_cast<AIEX::DMAStartTaskOp>(op))
966 return channelsOfTask(device, start.getTask());
967 if (isa<AIEX::DMAStartBdChainOp, AIEX::DMAStartBdChainForOp>(op))
968 return channelsOfTask(device, op->getResult(0));
969 if (auto push = dyn_cast<AIEX::NpuPushQueueOp>(op))
970 return SmallVector<TileDMAChannel>{
971 {{static_cast<int>(push.getColumn()),
972 static_cast<int>(push.getRow())},
973 push.getDirection(),
974 static_cast<int>(push.getChannel())}};
975 if (isa<AIEX::NpuWrite32Op, AIEX::NpuMaskWrite32Op, AIEX::NpuBlockWriteOp,
976 AIEX::NpuWriteConfigOp, AIEX::NpuWriteTeardownOp>(op))
977 return SmallVector<TileDMAChannel>{};
978 return std::nullopt;
979 };
980 auto awaited =
981 [&](Operation *op) -> std::optional<SmallVector<TileDMAChannel>> {
982 if (auto dmaWait = dyn_cast<AIEX::NpuDmaWaitOp>(op))
983 return bySymbol(dmaWait.getSymbolAttr());
984 if (auto await = dyn_cast<AIEX::DMAAwaitTaskOp>(op))
985 return channelsOfTask(device, await.getTask());
986 if (auto sync = dyn_cast<AIEX::NpuSyncOp>(op)) {
987 std::optional<int64_t> col = getConstantIntValue(sync.getColumn()),
988 row = getConstantIntValue(sync.getRow()),
989 dir = getConstantIntValue(sync.getDirection()),
990 channel = getConstantIntValue(sync.getChannel()),
991 cols = getConstantIntValue(sync.getColumnNum()),
992 rows = getConstantIntValue(sync.getRowNum());
993 if (!col || !row || !dir || !channel || !cols || !rows)
994 return SmallVector<TileDMAChannel>{};
995 SmallVector<TileDMAChannel> keys;
996 for (int64_t c = *col; c < *col + *cols; c++)
997 for (int64_t r = *row; r < *row + *rows; r++)
998 keys.push_back({{static_cast<int>(c), static_cast<int>(r)},
999 *dir ? DMAChannelDir::MM2S : DMAChannelDir::S2MM,
1000 static_cast<int>(*channel)});
1001 return keys;
1002 }
1003 if (isa<AIEX::NpuMaskPollOp>(op))
1004 return SmallVector<TileDMAChannel>{};
1005 return std::nullopt;
1006 };
1007 llvm::SetVector<TileDMAChannel, SmallVector<TileDMAChannel>,
1008 std::set<TileDMAChannel>>
1009 issuedKeys;
1010 sequence.walk([&](Operation *op) {
1011 if (std::optional<SmallVector<TileDMAChannel>> keys = issued(op))
1012 issuedKeys.insert(keys->begin(), keys->end());
1013 });
1014
1015 llvm::SetVector<unsigned> waited;
1016 walkLoopsTwice(sequence.getBody(), [&](Operation *op) {
1017 if (std::optional<SmallVector<TileDMAChannel>> keys = issued(op)) {
1018 for (const TileDMAChannel &key : *keys) {
1019 unsigned agent = getOrCreate(Agent::channel(key));
1020 modeled.insert(agent);
1021 for (unsigned w : waited)
1022 if (w != agent)
1023 addEdge(agent, w, EdgeKind::Host);
1024 }
1025 if (keys->empty())
1026 for (unsigned a = 0; a < numStreamAgents; a++)
1027 if (agents[a].kind == Agent::Kind::Channel)
1028 for (unsigned w : waited)
1029 if (w != a)
1030 addEdge(a, w, EdgeKind::Host);
1031 } else if (std::optional<SmallVector<TileDMAChannel>> keys =
1032 awaited(op)) {
1033 for (const TileDMAChannel &key :
1034 keys->empty() ? issuedKeys.getArrayRef() : ArrayRef(*keys)) {
1035 waited.insert(getOrCreate(Agent::channel(key)));
1036 for (unsigned a = 0; a < numStreamAgents; a++)
1037 if (agents[a].kind == Agent::Kind::Controller &&
1038 agents[a].dma.tile.col == key.tile.col)
1039 waited.insert(a);
1040 }
1041 }
1042 });
1043 }
1044
1045 // A channel nothing programs here is programmed elsewhere, in ways this
1046 // cannot see, so it may wait on anything else on its tile. One on a shim
1047 // tile is driven by the host, which may wait on any other shim tile first.
1048 const AIETargetModel &targetModel = getTargetModel(device);
1049 for (Agent &a : agents)
1050 a.onShim = a.kind == Agent::Kind::Channel &&
1051 targetModel.isShimNOCorPLTile(a.dma.tile.col, a.dma.tile.row);
1052 for (unsigned a = 0; a < agents.size(); a++) {
1053 if (modeled.contains(a) || agents[a].kind == Agent::Kind::Controller ||
1054 !waitsOnLocks(a))
1055 continue;
1056 for (unsigned b = 0; b < agents.size(); b++)
1057 if (b != a && agents[b].kind != Agent::Kind::Controller &&
1058 (agents[b].dma.tile == agents[a].dma.tile ||
1059 (agents[a].onShim && agents[b].onShim)))
1060 addEdge(a, b, EdgeKind::Lock);
1061 }
1062
1063 LLVM_DEBUG({
1064 for (unsigned a = 0; a < agents.size(); a++)
1065 for (const Edge &e : edges[a])
1066 llvm::dbgs() << "Wait: " << describe(a) << " on " << describe(e.to)
1067 << (e.kind == EdgeKind::Lock ? " (lock)"
1068 : e.kind == EdgeKind::Stream ? " (stream)"
1069 : " (host)")
1070 << (e.kind == EdgeKind::Lock && !modeled.contains(a)
1071 ? ", assumed"
1072 : "")
1073 << "\n";
1074 });
1075}
1076
1077std::optional<StreamWaitGraph::Agent>
1078StreamWaitGraph::Agent::at(const StreamEndpoint &endpoint, bool sending) {
1079 if (endpoint.port.bundle == WireBundle::Core)
1080 return core(endpoint.tile);
1081 if (endpoint.port.bundle == WireBundle::TileControl && sending)
1082 return controller(endpoint.tile);
1083 if (endpoint.port.bundle == WireBundle::DMA)
1084 return channel({endpoint.tile,
1085 sending ? DMAChannelDir::MM2S : DMAChannelDir::S2MM,
1086 endpoint.port.channel});
1087 // Other ports (PLIO, NOC, a receiving TileControl) are taken to always
1088 // drain, so the streams into them never block.
1089 return std::nullopt;
1090}
1091
1092unsigned StreamWaitGraph::getOrCreate(const Agent &agent) {
1093 auto [it, inserted] =
1094 agentIDs.try_emplace({agent.kind, agent.dma}, agents.size());
1095 if (inserted) {
1096 agents.push_back(agent);
1097 edges.emplace_back();
1098 }
1099 return it->second;
1100}
1101
1102std::optional<unsigned> StreamWaitGraph::lookup(const Agent &agent) const {
1103 auto it = agentIDs.find({agent.kind, agent.dma});
1104 if (it == agentIDs.end())
1105 return std::nullopt;
1106 return it->second;
1107}
1108
1109void StreamWaitGraph::addEdge(unsigned from, unsigned to, EdgeKind kind,
1110 bool sends, bool receives) {
1111 for (Edge &e : edges[from]) {
1112 if (e.to == to && e.kind == kind) {
1113 e.sends |= sends;
1114 e.receives |= receives;
1115 return;
1116 }
1117 }
1118 edges[from].push_back({to, kind, sends, receives});
1119}
1120
1121std::optional<unsigned> StreamWaitGraph::agentAt(const StreamEndpoint &endpoint,
1122 bool sending) const {
1123 if (std::optional<Agent> agent = Agent::at(endpoint, sending))
1124 return lookup(*agent);
1125 return std::nullopt;
1126}
1127
1128SmallVector<unsigned> StreamWaitGraph::drainersOf(unsigned agent) const {
1129 if (agents[agent].kind == Agent::Kind::Core)
1130 return {agent};
1131 SmallVector<unsigned> drainers;
1132 for (const Edge &e : edges[agent])
1133 if (e.kind != EdgeKind::Stream)
1134 drainers.push_back(e.to);
1135 return drainers;
1136}
1137
1138SmallVector<unsigned>
1139StreamWaitGraph::waitChain(ArrayRef<unsigned> from, ArrayRef<unsigned> targets,
1140 ArrayRef<unsigned> avoid) const {
1141 // A state is an agent plus `took`, the subset of channel agents already
1142 // waited on to reach it, and `taking`, whether it was reached as a DMA
1143 // receiver still taking data. A state is dropped once a prior state at the
1144 // same agent is at least as free and took a subset of the same agents: it
1145 // leads nowhere new. Live states per agent therefore form an antichain in
1146 // that subset lattice, so by Sperner's theorem their count can be
1147 // exponential in the worst case -- a known, understood limitation, not an
1148 // oversight. No smaller *exact* representation preserves this dominance
1149 // search's soundness.
1150 struct State {
1151 unsigned agent;
1152 bool taking;
1153 llvm::SparseBitVector<> took;
1154 std::optional<size_t> parent;
1155 };
1156 std::vector<State> states;
1157 DenseMap<unsigned, SmallVector<size_t>> seen;
1158 std::deque<size_t> worklist;
1159 auto visit = [&](State next) {
1160 for (size_t i : seen.lookup(next.agent)) {
1161 const State &s = states[i];
1162 if ((next.taking || !s.taking) && next.took.contains(s.took))
1163 return;
1164 }
1165 seen[next.agent].push_back(states.size());
1166 worklist.push_back(states.size());
1167 states.push_back(std::move(next));
1168 };
1169 for (unsigned a : from)
1170 if (!llvm::is_contained(avoid, a))
1171 visit({a, false, {}, std::nullopt});
1172 while (!worklist.empty()) {
1173 size_t index = worklist.front();
1174 worklist.pop_front();
1175 unsigned a = states[index].agent;
1176 if (llvm::is_contained(targets, a)) {
1177 SmallVector<unsigned> chain;
1178 for (std::optional<size_t> at = index; at; at = states[*at].parent)
1179 chain.push_back(states[*at].agent);
1180 std::reverse(chain.begin(), chain.end());
1181 return chain;
1182 }
1183 for (const Edge &e : edges[a]) {
1184 const State &state = states[index];
1185 if (state.taking && e.receives && !e.sends &&
1186 agents[e.to].kind == Agent::Kind::Channel)
1187 continue;
1188 if (llvm::is_contained(avoid, e.to) || state.took.test(e.to))
1189 continue;
1190 bool takes = e.kind == EdgeKind::Stream && e.sends &&
1191 agents[e.to].kind == Agent::Kind::Channel;
1192 llvm::SparseBitVector<> took = state.took;
1193 if (takes)
1194 took.set(e.to);
1195 visit({e.to, takes, std::move(took), index});
1196 }
1197 }
1198 return {};
1199}
1200
1201std::string StreamWaitGraph::describe(unsigned id) const {
1202 const Agent &a = agents[id];
1203 std::string s;
1204 llvm::raw_string_ostream os(s);
1205 os << "(" << a.dma.tile.col << ", " << a.dma.tile.row << ") ";
1206 if (a.kind == Agent::Kind::Core)
1207 os << "core";
1208 else if (a.kind == Agent::Kind::Controller)
1209 os << "TileControl";
1210 else
1211 os << stringifyDMAChannelDir(a.dma.dir) << " " << a.dma.channel;
1212 return s;
1213}
1214
1216 ArrayRef<RoutedStream> streams)
1217 : streams(streams), volumes(device, streams),
1218 graph(device, streams, volumes), stalls(streams.size()),
1219 silence(streams.size()) {}
1220
1221bool StreamDeadlockAnalysis::canStall(size_t f) const {
1222 std::optional<bool> &stall = stalls[f];
1223 if (!stall)
1224 stall = volumes.canFill(streams[f].dst, streams);
1225 return *stall;
1226}
1227
1228SmallVector<unsigned> StreamDeadlockAnalysis::blockingChain(size_t f,
1229 size_t g) const {
1230 const RoutedStream &fs = streams[f], &gs = streams[g];
1231 std::optional<unsigned> fDst = graph.agentAt(fs.dst, false);
1232 if (!fDst)
1233 return {};
1234 SmallVector<unsigned> own;
1235 if (std::optional<unsigned> fSrc = graph.agentAt(fs.src, true))
1236 own.push_back(*fSrc);
1237 own.push_back(*fDst);
1238 SmallVector<unsigned> drainers = graph.drainersOf(*fDst);
1239 SmallVector<unsigned> avoid, targets;
1240 for (unsigned a : own)
1241 if (!llvm::is_contained(drainers, a))
1242 avoid.push_back(a);
1243 for (std::optional<unsigned> a :
1244 {graph.agentAt(gs.src, true), graph.agentAt(gs.dst, false)})
1245 if (a && !llvm::is_contained(own, *a))
1246 targets.push_back(*a);
1247 if (targets.empty())
1248 return {};
1249 return graph.waitChain(drainers, targets, avoid);
1250}
1251
1252bool StreamDeadlockAnalysis::canBlock(size_t f, size_t g) const {
1253 if (auto it = blocks.find({f, g}); it != blocks.end())
1254 return it->second;
1255 return blocks[{f, g}] = canStall(f) && !silent(f) && !silent(g) &&
1256 volumes.maySendAfter(streams[f], streams[g]) !=
1257 std::optional(false) &&
1258 !blockingChain(f, g).empty();
1259}
1260
1262 std::optional<bool> &quiet = silence[f];
1263 if (!quiet)
1264 quiet = volumes.sendVolume(streams[f]) == std::optional<uint64_t>(0);
1265 return *quiet;
1266}
1267
1268std::string StreamDeadlockAnalysis::explainBlock(size_t f, size_t g) const {
1269 const RoutedStream &fs = streams[f], &gs = streams[g];
1270 SmallVector<unsigned> chain = blockingChain(f, g);
1271 std::string s;
1272 llvm::raw_string_ostream os(s);
1273 os << describeStream(fs) << " can fill its receiver, and draining that "
1274 << "waits on ";
1275 for (auto [i, a] : llvm::enumerate(chain))
1276 os << (i ? ", then " : "") << graph.describe(a);
1277 os << (graph.agentAt(gs.dst, false) == chain.back() ? ", which receives "
1278 : ", which sends ")
1279 << describeStream(gs) << '.';
1280 for (const std::string &a : assumptions(f, g))
1281 os << ' ' << a;
1282 return s;
1283}
1284
1285SmallVector<std::string> StreamDeadlockAnalysis::assumptions(size_t f,
1286 size_t g) const {
1287 const RoutedStream &fs = streams[f], &gs = streams[g];
1288 SmallVector<std::string> assumed;
1289 for (const RoutedStream &other : streams)
1290 if (other.dst == fs.dst && !volumes.sendVolume(other)) {
1291 assumed.push_back("The volume " + describeStream(other) +
1292 " carries is unknown, so it is assumed to overrun its "
1293 "receiver.");
1294 break;
1295 }
1296 if (fs.src == gs.src && !volumes.maySendAfter(fs, gs))
1297 assumed.push_back("Both come from " +
1298 describeTilePort(fs.src.tile, fs.src.port) +
1299 ", and the order it sends in is not modeled.");
1300 SmallVector<unsigned> chain = blockingChain(f, g);
1301 std::optional<unsigned> receiver = graph.agentAt(fs.dst, false);
1302 assert(receiver && "a stream that can block has a receiver");
1303 SmallVector<unsigned> waiters{*receiver};
1304 waiters.append(chain.begin(), std::prev(chain.end()));
1305 for (auto [i, a] : llvm::enumerate(waiters))
1306 if (!graph.isModeled(a) &&
1307 !llvm::is_contained(ArrayRef(waiters).take_front(i), a))
1308 assumed.push_back(
1309 "Nothing in the design programs " + graph.describe(a) +
1310 ", so it is assumed to wait on anything on its tile" +
1311 (graph.getAgent(a).onShim ? " or on another shim tile." : "."));
1312 return assumed;
1313}
1314
1315std::string AIE::describeStream(const RoutedStream &stream) {
1316 std::string s;
1317 llvm::raw_string_ostream os(s);
1318 os << (stream.packetID ? "packet flow " : "flow ")
1319 << describeTilePort(stream.src.tile, stream.src.port) << " -> "
1320 << describeTilePort(stream.dst.tile, stream.dst.port);
1321 if (stream.packetID)
1322 os << " (id " << *stream.packetID << ")";
1323 return s;
1324}
1325
1327 : device(device), streams(requestedStreams(device)),
1328 numRequested(streams.size()) {
1329 for (RoutedStream &s : traceRoutedStreams(device))
1330 streams.push_back(std::move(s));
1331 std::map<TreeKey, size_t> treeIDs;
1332 for (size_t i = 0; i < streams.size(); i++) {
1333 size_t tree = treeMembers.size();
1334 if (std::optional<TreeKey> key = treeKey(i))
1335 tree = treeIDs.try_emplace(*key, tree).first->second;
1336 if (tree == treeMembers.size())
1337 treeMembers.emplace_back();
1338 treeMembers[tree].push_back(i);
1339 treeOf.push_back(tree);
1340 }
1341 waits.resize(treeMembers.size());
1342}
1343
1344std::optional<StreamConflicts::TreeKey>
1345StreamConflicts::treeKey(size_t s) const {
1346 const RoutedStream &stream = streams[s];
1347 if (!stream.packetID)
1348 return std::nullopt;
1349 return TreeKey{stream.src.tile, stream.src.port, *stream.packetID,
1350 s < numRequested};
1351}
1352
1353const StreamDeadlockAnalysis &StreamConflicts::getAnalysis() const {
1354 if (!analysis)
1355 analysis.emplace(device, streams);
1356 return *analysis;
1357}
1358
1359bool StreamConflicts::blocks(size_t s, size_t t) const {
1360 const RoutedStream &a = streams[s], &b = streams[t];
1361 if (a.src == b.src || a.dst == b.dst)
1362 return false;
1363 return getAnalysis().canBlock(s, t);
1364}
1365
1366// Trees from one source, or into one receiver, already wait on each other
1367// there whatever the routing.
1368bool StreamConflicts::related(size_t s, size_t t) const {
1369 if (streams[s].src == streams[t].src)
1370 return true;
1371 for (size_t m : treeMembers[treeOf[s]])
1372 for (size_t n : treeMembers[treeOf[t]])
1373 if (streams[m].dst == streams[n].dst)
1374 return true;
1375 return false;
1376}
1377
1378bool StreamConflicts::conflict(size_t s, size_t t) const {
1379 return !related(s, t) && (blocks(s, t) || blocks(t, s));
1380}
1381
1382// The chains holdCycle follows from a tree stuck at a receiver to the trees
1383// it waits on, which hold for any routing.
1384const DenseMap<size_t, std::pair<size_t, size_t>> &
1385StreamConflicts::waitsFrom(size_t a) const {
1386 std::optional<DenseMap<size_t, std::pair<size_t, size_t>>> &cached = waits[a];
1387 if (cached)
1388 return *cached;
1389 DenseMap<size_t, std::pair<size_t, size_t>> &reached = cached.emplace();
1390 std::deque<size_t> work{a};
1391 while (!work.empty()) {
1392 size_t u = work.front();
1393 work.pop_front();
1394 for (size_t g = 0; g < treeMembers.size(); g++) {
1395 if (g == a || reached.contains(g) ||
1396 !streams[treeMembers[g].front()].packetID)
1397 continue;
1398 auto by = [&]() -> std::optional<std::pair<size_t, size_t>> {
1399 for (size_t x : treeMembers[u])
1400 for (size_t m : treeMembers[g])
1401 if (blocks(x, m))
1402 return std::pair{x, m};
1403 return std::nullopt;
1404 }();
1405 if (!by)
1406 continue;
1407 reached[g] = *by;
1408 work.push_back(g);
1409 }
1410 }
1411 return reached;
1412}
1413
1414bool StreamConflicts::mustSeparate(size_t s, size_t t) const {
1415 if (related(s, t))
1416 return false;
1417 return blocks(s, t) || blocks(t, s) ||
1418 waitsFrom(treeOf[s]).contains(treeOf[t]) ||
1419 waitsFrom(treeOf[t]).contains(treeOf[s]);
1420}
1421
1422std::string StreamConflicts::explain(size_t s, size_t t) const {
1423 const StreamDeadlockAnalysis &a = getAnalysis();
1424 if (a.canBlock(s, t))
1425 return a.explainBlock(s, t);
1426 if (a.canBlock(t, s))
1427 return a.explainBlock(t, s);
1428 if (!waitsFrom(treeOf[s]).contains(treeOf[t]))
1429 std::swap(s, t);
1430 SmallVector<std::string> steps;
1431 for (size_t g = treeOf[t]; g != treeOf[s];) {
1432 auto [x, m] = waitsFrom(treeOf[s]).at(g);
1433 steps.push_back(a.explainBlock(x, m));
1434 g = treeOf[x];
1435 }
1436 std::reverse(steps.begin(), steps.end());
1437 return llvm::join(steps, " ");
1438}
1439
1440SmallVector<std::pair<size_t, size_t>> StreamConflicts::unavoidable() const {
1441 SmallVector<std::pair<size_t, size_t>> pairs;
1442 const StreamDeadlockAnalysis &a = getAnalysis();
1443 for (size_t s = 0; s < numRequested; s++)
1444 for (size_t t = 0; t < numRequested; t++) {
1445 if (s == t || !(streams[s].packetID || streams[t].packetID) ||
1446 !related(s, t) || !a.canBlock(s, t))
1447 continue;
1448 if (a.assumptions(s, t).empty())
1449 pairs.push_back({s, t});
1450 else
1451 LLVM_DEBUG(llvm::dbgs() << "Unavoidable only by assumption: "
1452 << a.explainBlock(s, t) << "\n");
1453 }
1454 return pairs;
1455}
1456
1457namespace {
1458// A wait in the graph holdCycle searches: a head stuck at one node waits on
1459// one at `to`, for the reason `step` gives, if any.
1460struct WaitEdge {
1461 size_t to;
1462 std::optional<HoldCycle::Step> step;
1463 // Where the trees a link wait is between merged above the link: the
1464 // switchbox and the arbiter that granted them the link in turn.
1465 std::optional<std::pair<TileID, int>> merged = std::nullopt;
1466};
1467// The graph holdCycle searches, built as it is walked. Node `entry` leads to
1468// every root.
1469struct WaitGraph {
1470 llvm::function_ref<ArrayRef<WaitEdge>(size_t)> successors;
1471 size_t entry;
1472};
1473using WaitNode = std::pair<const WaitGraph *, size_t>;
1474} // namespace
1475
1476namespace llvm {
1477template <>
1478struct GraphTraits<const WaitGraph *> {
1479 using NodeRef = WaitNode;
1480 struct ToNode {
1481 const WaitGraph *graph;
1482 NodeRef operator()(const WaitEdge &e) const { return {graph, e.to}; }
1483 };
1484 using ChildIteratorType = mapped_iterator<const WaitEdge *, ToNode>;
1485 static NodeRef getEntryNode(const WaitGraph *g) { return {g, g->entry}; }
1487 return map_iterator(n.first->successors(n.second).begin(), ToNode{n.first});
1488 }
1490 return map_iterator(n.first->successors(n.second).end(), ToNode{n.first});
1491 }
1492};
1493} // namespace llvm
1494
1495std::optional<HoldCycle>
1496StreamConflicts::holdCycle(ArrayRef<SmallVector<StreamHop, 8>> routes,
1497 bool forcedWaits, bool definite) const {
1498 // The packets one source sends with one id move down every branch as one.
1499 struct Tree {
1500 SmallVector<size_t, 2> members;
1501 SmallVector<std::pair<TileID, Port>, 8> hops;
1502 SmallVector<int, 8> parent;
1503 SmallVector<std::optional<int>, 8> arbiter;
1504 size_t base = 0;
1505 };
1506 std::vector<Tree> trees;
1507 std::map<TreeKey, size_t> treeIDs;
1508 for (size_t i = 0; i < streams.size(); i++) {
1509 std::optional<TreeKey> key = treeKey(i);
1510 if (!key || getAnalysis().silent(i))
1511 continue;
1512 auto [it, inserted] = treeIDs.try_emplace(*key, trees.size());
1513 if (inserted)
1514 trees.emplace_back();
1515 Tree &tree = trees[it->second];
1516 tree.members.push_back(i);
1517 int prev = -1;
1518 for (const StreamHop &hop : routes[i]) {
1519 std::pair<TileID, Port> key{hop.tile, hop.input};
1520 int h = llvm::find(tree.hops, key) - tree.hops.begin();
1521 if (h == static_cast<int>(tree.hops.size())) {
1522 tree.hops.push_back(key);
1523 tree.parent.push_back(prev);
1524 tree.arbiter.push_back(hop.arbiter);
1525 }
1526 prev = h;
1527 }
1528 }
1529
1530 // One source sends one tree's packet at a time. Trees into one receiver are
1531 // both in flight at once, and wait on each other at its master port however
1532 // they are routed, where both enter its switchbox for it; and trees with one
1533 // id wait on each other where they meet and below, going on as one. Those
1534 // waits count only with `forcedWaits`.
1535 auto sameSource = [&](size_t a, size_t b) {
1536 return streams[trees[a].members.front()].src ==
1537 streams[trees[b].members.front()].src;
1538 };
1539 auto forced = [&](size_t a, int ha, size_t b, int hb, TileID tile) {
1540 Port ina = trees[a].hops[ha].second, inb = trees[b].hops[hb].second;
1541 auto into = [&](size_t m, Port in) {
1542 return streams[m].dst.tile == tile && !routes[m].empty() &&
1543 routes[m].back().tile == tile && routes[m].back().input == in;
1544 };
1545 for (size_t m : trees[a].members)
1546 for (size_t n : trees[b].members)
1547 if (streams[m].dst == streams[n].dst && into(m, ina) && into(n, inb))
1548 return true;
1549 if (streams[trees[a].members.front()].packetID !=
1550 streams[trees[b].members.front()].packetID)
1551 return false;
1552 for (auto above : llvm::enumerate(trees[a].parent))
1553 if (above.value() == ha &&
1554 llvm::any_of(llvm::enumerate(trees[b].parent), [&](auto below) {
1555 return below.value() == hb &&
1556 trees[b].hops[below.index()] == trees[a].hops[above.index()];
1557 }))
1558 return true;
1559 return false;
1560 };
1561
1562 // Per tree, a node for a head of it stuck entering each hop or at each
1563 // receiver, one for it stuck anywhere, and one per hop for it stuck
1564 // anywhere past that hop, so still holding what it took there.
1565 enum class Kind { Hop, Receiver, Anywhere, Past };
1566 struct Node {
1567 size_t tree;
1568 Kind kind;
1569 size_t index;
1570 };
1571 std::vector<Node> nodes;
1572 for (size_t t = 0; t < trees.size(); t++) {
1573 Tree &tree = trees[t];
1574 tree.base = nodes.size();
1575 for (size_t h = 0; h < tree.hops.size(); h++)
1576 nodes.push_back({t, Kind::Hop, h});
1577 for (size_t m = 0; m < tree.members.size(); m++)
1578 nodes.push_back({t, Kind::Receiver, m});
1579 nodes.push_back({t, Kind::Anywhere, 0});
1580 for (size_t h = 0; h < tree.hops.size(); h++)
1581 nodes.push_back({t, Kind::Past, h});
1582 }
1583 auto receiverNode = [&](size_t t, size_t m) {
1584 return trees[t].base + trees[t].hops.size() + m;
1585 };
1586 auto anywhereNode = [&](size_t t) {
1587 return receiverNode(t, trees[t].members.size());
1588 };
1589 auto pastNode = [&](size_t t, size_t h) { return anywhereNode(t) + 1 + h; };
1590
1591 std::map<std::pair<TileID, Port>, SmallVector<std::pair<size_t, size_t>, 4>>
1592 entering;
1593 std::map<TileID, SmallVector<std::pair<size_t, size_t>, 8>> passing;
1594 for (size_t t = 0; t < trees.size(); t++)
1595 for (size_t h = 0; h < trees[t].hops.size(); h++) {
1596 entering[trees[t].hops[h]].push_back({t, h});
1597 passing[trees[t].hops[h].first].push_back({t, h});
1598 }
1599
1600 // The arbiter both trees took to leave the switchbox above a hop they share
1601 // where they came into it apart.
1602 using Grant = std::pair<TileID, int>;
1603 auto mergedAt = [&](size_t a, int ha, size_t b,
1604 int hb) -> std::optional<Grant> {
1605 for (;;) {
1606 int pa = trees[a].parent[ha], pb = trees[b].parent[hb];
1607 if (pa < 0 || pb < 0)
1608 return std::nullopt;
1609 if (trees[a].hops[pa] != trees[b].hops[pb]) {
1610 std::optional<int> arbiter = trees[a].arbiter[pa];
1611 if (trees[a].hops[pa].first != trees[b].hops[pb].first || !arbiter ||
1612 arbiter != trees[b].arbiter[pb])
1613 return std::nullopt;
1614 return Grant{trees[a].hops[pa].first, *arbiter};
1615 }
1616 ha = pa;
1617 hb = pb;
1618 }
1619 };
1620
1621 using Edge = WaitEdge;
1622 SmallVector<size_t> roots;
1623 std::vector<std::optional<SmallVector<Edge, 4>>> edges(nodes.size() + 1);
1624 auto successors = [&](size_t n) -> ArrayRef<Edge> {
1625 if (edges[n])
1626 return *edges[n];
1627 SmallVector<Edge, 4> out;
1628 if (n == nodes.size()) {
1629 for (size_t root : roots)
1630 out.push_back({root, std::nullopt});
1631 return *(edges[n] = std::move(out));
1632 }
1633 const Node &node = nodes[n];
1634 const Tree &u = trees[node.tree];
1635 switch (node.kind) {
1636 case Kind::Hop: {
1637 auto [tile, input] = u.hops[node.index];
1638 size_t waiting = u.members.front();
1639 for (auto [t, ht] : entering.at({tile, input})) {
1640 if (t != node.tree && sameSource(node.tree, t))
1641 continue;
1642 size_t sharer = trees[t].members.front();
1643 bool sameId = streams[waiting].packetID == streams[sharer].packetID;
1644 if (t != node.tree && (!sameId || forcedWaits))
1645 out.push_back(
1646 {pastNode(t, ht),
1647 HoldCycle::Step{HoldCycle::Wait::Link, waiting, sharer, sharer,
1648 tile, input, input, -1, sameId},
1649 mergedAt(node.tree, node.index, t, ht)});
1650 std::optional<int> arbiter = trees[t].arbiter[ht];
1651 if (!arbiter)
1652 continue;
1653 // A tree's branches move as one, so where two come into a switchbox
1654 // apart onto one arbiter, the one holding it waits on the other.
1655 for (auto [v, hv] : passing.at(tile)) {
1656 Port holderInput = trees[v].hops[hv].second;
1657 bool self = v == node.tree && t == node.tree;
1658 if ((!self && (v == node.tree || v == t || sameSource(t, v))) ||
1659 holderInput == input || trees[v].arbiter[hv] != arbiter)
1660 continue;
1661 bool isForced = !self && forced(t, ht, v, hv, tile);
1662 if (isForced && !forcedWaits)
1663 continue;
1664 out.push_back(
1665 {pastNode(v, hv),
1667 trees[v].members.front(), tile, input,
1668 holderInput, *arbiter, isForced}});
1669 }
1670 }
1671 break;
1672 }
1673 case Kind::Receiver: {
1674 // A tree into the full receiver is stuck there however it is routed.
1675 size_t s = u.members[node.index];
1676 for (size_t g = 0; g < trees.size(); g++) {
1677 if (g == node.tree || llvm::any_of(trees[g].members, [&](size_t m) {
1678 return streams[m].dst == streams[s].dst;
1679 }))
1680 continue;
1681 for (size_t m : trees[g].members)
1682 if (blocks(s, m) &&
1683 (!definite || getAnalysis().assumptions(s, m).empty())) {
1684 out.push_back({anywhereNode(g),
1686 TileID{}, Port{}, Port{}, -1}});
1687 break;
1688 }
1689 }
1690 break;
1691 }
1692 case Kind::Anywhere:
1693 case Kind::Past: {
1694 llvm::SmallDenseSet<size_t, 8> behind;
1695 if (node.kind == Kind::Past)
1696 for (int h = node.index; h >= 0; h = u.parent[h])
1697 behind.insert(h);
1698 for (size_t h = 0; h < u.hops.size(); h++)
1699 if (!behind.contains(h))
1700 out.push_back({u.base + h, std::nullopt});
1701 for (size_t m = 0; m < u.members.size(); m++)
1702 out.push_back({receiverNode(node.tree, m), std::nullopt});
1703 break;
1704 }
1705 }
1706 return *(edges[n] = std::move(out));
1707 };
1708
1709 // A cycle of receivers alone waiting on each other is the design's own, so
1710 // only cycles through a wait some requested routing adds count.
1711 auto counts = [&](const Edge &e) {
1712 if (!e.step || e.step->wait == HoldCycle::Wait::Drain)
1713 return false;
1714 return e.step->waiting < numRequested || e.step->sharer < numRequested ||
1715 e.step->holding < numRequested;
1716 };
1717 for (size_t n = 0; n < nodes.size(); n++)
1718 if (nodes[n].kind == Kind::Hop && llvm::any_of(successors(n), counts))
1719 roots.push_back(n);
1720
1721 const WaitGraph graph{successors, nodes.size()};
1722 std::vector<int> component(nodes.size() + 1, -1);
1723 int components = 0;
1724 for (auto scc = llvm::scc_begin(&graph); !scc.isAtEnd(); ++scc, ++components)
1725 for (WaitNode n : *scc)
1726 component[n.second] = components;
1727
1728 // A counted wait within one component lies on a closed walk. No state has two
1729 // trees holding one arbiter (see HoldCycle), so a walk that needs that is no
1730 // deadlock. Nor is one where each packet is behind the next on links below
1731 // one arbiter they merged at, which granted them those links in one order, so
1732 // a walk from a link wait below one must also wait otherwise. The search
1733 // fixes or rules out a holder per arbiter until the shortest walk left
1734 // agrees. Each clash splits it in two, so past maxWalkSearches it keeps the
1735 // first walk, which at worst steers the router off a routing that cannot
1736 // deadlock.
1737 struct Holders {
1738 std::optional<size_t> fixed;
1739 SmallVector<size_t, 2> excluded;
1740 };
1741 using Constraints = std::map<Grant, Holders>;
1742 auto allowed = [](const Edge &edge, const Constraints &c) {
1743 if (!edge.step || edge.step->wait != HoldCycle::Wait::Arbiter)
1744 return true;
1745 auto it = c.find({edge.step->tile, edge.step->arbiter});
1746 if (it == c.end())
1747 return true;
1748 size_t holder = edge.step->holding;
1749 return it->second.fixed ? *it->second.fixed == holder
1750 : !llvm::is_contained(it->second.excluded, holder);
1751 };
1752 auto closeWalk =
1753 [&](size_t x, const Edge &e,
1754 const Constraints &c) -> std::optional<SmallVector<const Edge *>> {
1755 if (!allowed(e, c))
1756 return std::nullopt;
1757 // A state is a node and whether the walk there has waited other than on
1758 // a link below e's merge.
1759 auto ordered = [&](const Edge &edge) {
1760 return !edge.step || edge.merged == e.merged;
1761 };
1762 auto state = [](size_t node, bool waited) { return 2 * node + waited; };
1763 size_t start = state(e.to, !e.merged), end = state(x, true);
1764 DenseMap<size_t, std::pair<size_t, const Edge *>> via;
1765 via[start] = {start, nullptr};
1766 std::deque<size_t> worklist{start};
1767 while (!via.contains(end)) {
1768 if (worklist.empty())
1769 return std::nullopt;
1770 size_t n = worklist.front();
1771 worklist.pop_front();
1772 for (const Edge &next : successors(n / 2)) {
1773 size_t to = state(next.to, n % 2 || !ordered(next));
1774 if (component[next.to] == component[x] && allowed(next, c) &&
1775 via.try_emplace(to, n, &next).second)
1776 worklist.push_back(to);
1777 }
1778 }
1779 SmallVector<const Edge *> path;
1780 for (size_t n = end; n != start; n = via.at(n).first)
1781 path.push_back(via.at(n).second);
1782 path.push_back(&e);
1783 std::reverse(path.begin(), path.end());
1784 return path;
1785 };
1786 auto toCycle = [](ArrayRef<const Edge *> path) {
1787 HoldCycle cycle;
1788 for (const Edge *edge : path)
1789 if (edge->step)
1790 cycle.steps.push_back(*edge->step);
1791 return cycle;
1792 };
1793 constexpr int maxWalkSearches = 1024;
1794 for (size_t x : roots)
1795 for (const Edge &e : successors(x)) {
1796 if (!counts(e) || component[e.to] != component[x])
1797 continue;
1798 std::optional<SmallVector<const Edge *>> first;
1799 SmallVector<Constraints> pending{{}};
1800 int search = 0;
1801 for (; !pending.empty(); search++) {
1802 if (search == maxWalkSearches) {
1803 assert(first && "only a walk found adds pending constraints");
1804 LLVM_DEBUG(llvm::dbgs() << "Hold cycle search gave up after "
1805 << search << " walks\n");
1806 return toCycle(*first);
1807 }
1808 Constraints c = pending.pop_back_val();
1809 std::optional<SmallVector<const Edge *>> path = closeWalk(x, e, c);
1810 if (!path)
1811 continue;
1812 if (!first)
1813 first = path;
1814 std::map<Grant, size_t> held;
1815 std::optional<std::pair<Grant, size_t>> clash;
1816 for (const Edge *edge : *path) {
1817 if (!edge->step || edge->step->wait != HoldCycle::Wait::Arbiter)
1818 continue;
1819 auto [it, inserted] = held.try_emplace(
1820 {edge->step->tile, edge->step->arbiter}, edge->step->holding);
1821 if (!inserted && it->second != edge->step->holding) {
1822 clash = *it;
1823 break;
1824 }
1825 }
1826 if (!clash) {
1827 LLVM_DEBUG(llvm::dbgs() << "Hold cycle search closed a walk after "
1828 << search + 1 << " walks\n");
1829 return toCycle(*path);
1830 }
1831 Constraints fix = c, exclude = c;
1832 fix[clash->first].fixed = clash->second;
1833 exclude[clash->first].excluded.push_back(clash->second);
1834 pending.push_back(std::move(exclude));
1835 pending.push_back(std::move(fix));
1836 }
1837 LLVM_DEBUG(llvm::dbgs() << "Hold cycle search closed no walk after "
1838 << search << " walks\n");
1839 }
1840 return std::nullopt;
1841}
1842
1843std::string StreamConflicts::explain(const HoldCycle &cycle) const {
1844 std::string s;
1845 llvm::raw_string_ostream os(s);
1846 for (auto [i, step] : llvm::enumerate(cycle.steps)) {
1847 if (i)
1848 os << ' ';
1849 switch (step.wait) {
1851 os << describeStream(streams[step.waiting]) << " can queue behind "
1852 << describeStream(streams[step.holding]) << " on "
1853 << describePort(step.sharerInput) << " into tile (" << step.tile.col
1854 << ", " << step.tile.row << ").";
1855 break;
1857 os << describeStream(streams[step.holding]) << " can hold arbiter "
1858 << step.arbiter << " at tile (" << step.tile.col << ", "
1859 << step.tile.row << ") that " << describeStream(streams[step.sharer])
1860 << " needs";
1861 if (step.waiting != step.sharer)
1862 os << ", and " << describeStream(streams[step.waiting])
1863 << " can queue behind it";
1864 os << ".";
1865 break;
1867 os << getAnalysis().explainBlock(step.waiting, step.holding);
1868 break;
1869 }
1870 }
1871 return s;
1872}
constexpr int maxBDSteps
virtual AIEArch getTargetArch() const =0
Return the target architecture.
bool isShimNOCorPLTile(int col, int row) const
Return true if the given tile is either a ShimNOC or ShimPL tile.
llvm::SmallVector< std::pair< size_t, size_t > > unavoidable() const
The requested streams where the first can hold up the second however they are routed,...
bool mustSeparate(size_t s, size_t t) const
Whether a routing that puts packet streams s and t on one arbiter anywhere has a hold cycle: they con...
bool conflict(size_t s, size_t t) const
Whether s and t can deadlock if they share an arbiter or a link.
std::optional< HoldCycle > holdCycle(llvm::ArrayRef< llvm::SmallVector< StreamHop, 8 > > routes, bool forcedWaits=false, bool definite=false) const
A cycle of waits the packet streams can deadlock in when routed along routes, indexed like getStreams...
std::string explain(size_t s, size_t t) const
Why s and t conflict.
Which streams can deadlock against each other if they share an arbiter or a link.
bool canBlock(size_t f, size_t g) const
Whether stream f, stalled at its receiver, can keep stream g from ever arriving: f carries more than ...
std::string explainBlock(size_t f, size_t g) const
Why f can block g: the chain of waits from f's receiver to g, and which links of it are assumed for l...
bool silent(size_t f) const
Whether stream f carries nothing, so it neither holds nor waits.
llvm::SmallVector< std::string > assumptions(size_t f, size_t g) const
What explainBlock(f, g) takes on trust rather than reads off the design, one sentence each.
StreamDeadlockAnalysis(DeviceOp device, llvm::ArrayRef< RoutedStream > streams)
streams must outlive the analysis.
How much data a stream carries over a run and how much a receiver takes in before it has to wait on a...
StreamVolumeAnalysis(DeviceOp device, llvm::ArrayRef< RoutedStream > streams)
std::optional< uint64_t > sendVolume(const RoutedStream &stream) const
Bytes the stream's source sends with the stream's packet id, headers included where the receiver keep...
std::optional< uint64_t > receiveCapacity(const StreamEndpoint &endpoint) const
Bytes the receiver at endpoint accepts before it waits on another agent, or nullopt when it never doe...
bool canFill(const StreamEndpoint &endpoint, llvm::ArrayRef< RoutedStream > streams) const
Whether streams can send the receiver at endpoint more than it takes in before it waits on another ag...
std::optional< bool > maySendAfter(const RoutedStream &first, const RoutedStream &then) const
Whether the source both streams come from can send a packet of then after one of first; nullopt if it...
std::optional< unsigned > agentAt(const StreamEndpoint &endpoint, bool sending) const
The agent pushing data into (sending) or pulling data out of the fabric at endpoint,...
std::string describe(unsigned id) const
llvm::SmallVector< unsigned > waitChain(llvm::ArrayRef< unsigned > from, llvm::ArrayRef< unsigned > targets, llvm::ArrayRef< unsigned > avoid) const
A shortest chain of waits from an agent in from to one in targets that avoids avoid,...
llvm::SmallVector< unsigned > drainersOf(unsigned agent) const
Agents whose progress frees room in agent for more incoming data: a core frees itself,...
StreamWaitGraph(DeviceOp device, llvm::ArrayRef< RoutedStream > streams, const StreamVolumeAnalysis &volumes)
struct TileID { friend std::ostream &operator<<(std::ostream &os, const TileID &s) { os<< "TileID("<< s.col<< ", "<< s.row<< ")"; return os; } friend std::string to_string(const TileID &s) { std::ostringstream ss; ss<< s; return ss.str(); } friend llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const TileID &s) { os<< to_string(s); return os; } inline bool operator<(const TileID &rhs) const { return std::tie(col, row)< std::tie(rhs.col, rhs.row); } bool operator==(const TileID &rhs) const { return std::tie(col, row)==std::tie(rhs.col, rhs.row); } bool operator!=(const TileID &rhs) const { return !(*this==rhs);} int col, row;} TileID
bool isDirectional(WireBundle bundle)
Definition AIEDialect.h:447
std::string describeStream(const RoutedStream &stream)
Names the stream by its endpoints and packet id, e.g.
const AIETargetModel & getTargetModel(mlir::Operation *op)
bool keepsPktHeader(TileID tile, Port master, std::optional< bool > keep)
std::vector< RoutedStream > requestedStreams(DeviceOp device)
The streams the aie.flow and aie.packet_flow ops ask for, one per source, destination and packet id,...
std::vector< RoutedStream > traceRoutedStreams(DeviceOp device)
Follows every stream from its source tile port through the configured switchboxes,...
std::string describeTilePort(TileID tile, Port port)
Names a tile port the way diagnostics do, e.g. "(0, 2) DMA:1".
struct Port { WireBundle bundle; int channel; bool operator==(const Port &rhs) const { return std::tie(bundle, channel)==std::tie(rhs.bundle, rhs.channel); } bool operator!=(const Port &rhs) const { return !(*this==rhs);} bool operator<(const Port &rhs) const { return std::tie(bundle, channel)< std::tie(rhs.bundle, rhs.channel); } friend std::ostream &operator<<(std::ostream &os, const Port &port) { os<< "("; os<< stringifyWireBundle(port.bundle).str(); os<< ": "<< std::to_string(port.channel)<< ")"; return os; } friend llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const Port &port) { os<< to_string(port); return os; } } Port
Definition AIEDialect.h:265
std::string describePort(Port port)
Names a port the way diagnostics do, e.g. "DMA:1".
static ChildIteratorType child_begin(NodeRef n)
mapped_iterator< const WaitEdge *, ToNode > ChildIteratorType
The BD chain a DMA channel runs, as one aie.dma_start, aie.dma or runtime task programs it.
llvm::SmallVector< mlir::Region * > regions
uint64_t passes
Times one start of the channel runs the chain: its repeat count plus one.
bool loops
Whether the BD chain runs forever.
size_t loopStart
The first of bds a looping chain returns to once it has run them all.
llvm::SmallVector< mlir::Block * > bds
A cycle of waits packet streams can deadlock in, given their routes.
llvm::SmallVector< Step, 4 > steps
@ Drain
waiting fills its receiver, and draining that waits on holding.
@ Link
waiting queues behind holding on the port both enter tile by.
@ Arbiter
waiting, or sharer it queues behind, needs the arbiter holding holds at tile; sharer enters by sharer...
One stream from a source tile port to a destination tile port, recovered from the physical switchbox ...
bool carries(std::optional< PacketInfoAttr > packet) const
Whether what a BD sends with header packet, or with none, goes on this stream.
int packetMask
The stream carries every id that agrees with packetID on these bits.
bool keepsPktHeader
The destination stores each packet's header along with its payload.
std::optional< int > packetID
The packet id carried, if any.
A tile port at the edge of the stream fabric.
A switchbox a stream passes: the port it enters by and, where it is packet switched there,...
A core, a DMA channel, or a tile's controller.
static std::optional< Agent > at(const StreamEndpoint &endpoint, bool sending)
Each agent at a stream endpoint: the core, DMA channel or controller pushing data into (sending) or p...
static Agent channel(const TileDMAChannel &dma)