MLIR-AIE 1.4.5.dev
DmaQueueModel.h
Go to the documentation of this file.
1//===- DmaQueueModel.h ------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Per-channel DMA task queue, shared by the dma_start_task and
9// npu.dma_memcpy_nd paths so the retirement rule cannot drift between them.
10//
11//===----------------------------------------------------------------------===//
12
13#ifndef AIE_DIALECT_AIEX_UTILS_DMAQUEUEMODEL_H
14#define AIE_DIALECT_AIEX_UTILS_DMAQUEUEMODEL_H
15
18
19#include "mlir/Dialect/Arith/IR/Arith.h"
20#include "mlir/Dialect/SCF/IR/SCF.h"
21#include "mlir/Dialect/Utils/StaticValueUtils.h"
22#include "mlir/IR/Builders.h"
23#include "llvm/ADT/DenseMap.h"
24#include "llvm/ADT/STLExtras.h"
25#include "llvm/ADT/SetVector.h"
26#include "llvm/ADT/SmallVector.h"
27
28#include <algorithm>
29#include <array>
30#include <cstdint>
31#include <functional>
32#include <map>
33#include <set>
34
35namespace xilinx::AIEX {
36
37/// Tracks, per DMA channel, the pushes not yet known to have completed along
38/// one execution path. The sequence analysis below joins control-flow paths.
40public:
41 /// {col, row, direction, channel}. Queues are per channel and per direction,
42 /// and shim tiles are one per column, so the same channel number in two
43 /// columns is two independent queues.
44 using ChannelKey = std::array<int, 4>;
45
46 static ChannelKey keyOf(AIE::TileOp tile, AIE::DMAChannelDir direction,
47 int64_t channel) {
48 return {tile.getCol(), tile.getRow(), static_cast<int>(direction),
49 static_cast<int>(channel)};
50 }
51
52 /// One channel's queue: its pushes not yet known to have completed, oldest
53 /// first, each true when it issues a completion token.
54 struct ChannelState {
55 llvm::SmallVector<bool, 8> pushes;
56 bool unknownTokens = false;
57 bool operator==(const ChannelState &other) const {
58 return pushes == other.pushes && unknownTokens == other.unknownTokens;
59 }
60 };
61
62 /// True when a push would land on a full queue. `depth` of 0 means the
63 /// target has no queued-task model and nothing can overflow.
64 static bool wouldOverflow(const ChannelState &q, uint32_t depth) {
65 return depth != 0 && q.pushes.size() >= depth;
66 }
67
68 /// Record a push. Every push occupies a slot, not just issue_token ones, and
69 /// one push occupies exactly one whatever its repeat_count.
70 static void push(ChannelState &q, bool issuesToken) {
71 q.pushes.push_back(issuesToken && !q.unknownTokens);
72 }
73
74 /// Retire what a task-completion-token await proves has drained. The sync
75 /// pops whichever token the channel produces first -- the oldest outstanding
76 /// token-issuing push, not necessarily the task the await names -- and
77 /// in-order execution means everything queued ahead of it has drained too.
78 /// With no token-issuing push queued, nothing can be retired.
79 static void awaitToken(ChannelState &q) {
80 if (q.unknownTokens)
81 return;
82 auto *tok = llvm::find(q.pushes, true);
83 if (tok != q.pushes.end())
84 q.pushes.erase(q.pushes.begin(), std::next(tok));
85 }
86
87 /// Record that a queue-space poll has guaranteed room. Keeps the newest
88 /// depth-1 entries rather than clearing. Polling does not consume completion
89 /// tokens: if it drops a token-bearing entry, later syncs cannot safely be
90 /// credited to newer pushes.
91 static void noteSpaceGuaranteed(ChannelState &q, uint32_t depth) {
92 if (depth > 0 && q.pushes.size() > depth - 1) {
93 auto end = q.pushes.end() - (depth - 1);
94 q.unknownTokens |=
95 llvm::is_contained(llvm::make_range(q.pushes.begin(), end), true);
96 q.pushes.erase(q.pushes.begin(), end);
97 if (q.unknownTokens)
98 llvm::fill(q.pushes, false);
99 }
100 }
101
102 bool wouldOverflow(const ChannelKey &key, uint32_t depth) const {
103 auto it = queued.find(key);
104 return it != queued.end() && wouldOverflow(it->second, depth);
105 }
106
107 void push(const ChannelKey &key, bool issuesToken) {
108 push(queued[key], issuesToken);
109 }
110
111 void awaitToken(const ChannelKey &key) {
112 if (auto it = queued.find(key); it != queued.end())
113 awaitToken(it->second);
114 }
115
116 void noteSpaceGuaranteed(const ChannelKey &key, uint32_t depth) {
117 noteSpaceGuaranteed(queued[key], depth);
118 }
119
120 /// True the first time a channel is reported. An over-subscribed channel
121 /// usually stays that way for every later push, and repeating the same
122 /// diagnostic per push buries it.
123 bool shouldReport(const ChannelKey &key) {
124 return reported.insert(key).second;
125 }
126
127 /// Outstanding pushes on `key`, for diagnostics.
128 size_t outstanding(const ChannelKey &key) const {
129 auto it = queued.find(key);
130 return it == queued.end() ? 0 : it->second.pushes.size();
131 }
132
133 /// Every channel's queue. `reported` is deliberately not part of it: it is
134 /// diagnostic bookkeeping, not queue state, and folding it in would stop
135 /// two otherwise identical states from comparing equal.
136 using State = std::map<ChannelKey, ChannelState>;
137 const State &state() const { return queued; }
138 void setState(State s) { queued = std::move(s); }
139
140private:
141 State queued;
142 std::set<ChannelKey> reported;
143};
144
145/// The channel an aiex.npu.sync retires on, or nullopt when it cannot be
146/// pinned down. Its six operands are exactly a ChannelKey, with direction
147/// numbered as AIE::DMAChannelDir because DmaWaitToSyncPattern builds that
148/// operand by casting the enum. Unresolvable retires nothing, which
149/// over-reports; guessing the other way would hide a real hang.
150inline std::optional<DmaQueueModel::ChannelKey> syncChannelKey(NpuSyncOp sync) {
151 std::optional<uint32_t> col = getConstantIntOperand(sync.getColumn());
152 std::optional<uint32_t> row = getConstantIntOperand(sync.getRow());
153 std::optional<uint32_t> dir = getConstantIntOperand(sync.getDirection());
154 std::optional<uint32_t> chan = getConstantIntOperand(sync.getChannel());
155 std::optional<uint32_t> colNum = getConstantIntOperand(sync.getColumnNum());
156 std::optional<uint32_t> rowNum = getConstantIntOperand(sync.getRowNum());
157 if (!col || !row || !dir || !chan || !colNum || !rowNum)
158 return std::nullopt;
159 // Nothing in tree settles whether a sync spanning a range of tiles waits for
160 // one token per tile or one for the range -- the cert lowering ignores both
161 // fields -- and crediting nothing is the only reading sound either way.
162 if (*colNum != 1 || *rowNum != 1)
163 return std::nullopt;
165 static_cast<int>(*col), static_cast<int>(*row), static_cast<int>(*dir),
166 static_cast<int>(*chan)};
167}
168
169/// Retire what an aiex.npu.sync proves has drained. This is the lowered form of
170/// npu.dma_wait and the same hardware event, so it retires by the same rule;
171/// sequences written or lowered down to raw syncs would otherwise look like
172/// they never drain a channel at all.
173inline void awaitSync(DmaQueueModel &queue, NpuSyncOp sync) {
174 if (std::optional<DmaQueueModel::ChannelKey> key = syncChannelKey(sync))
175 queue.awaitToken(*key);
176}
177
178/// Shared wording for the two lowering paths, so the explanation does not
179/// depend on whether the transfer came from dma_start_task or
180/// npu.dma_memcpy_nd.
181inline mlir::InFlightDiagnostic
182emitQueueOverflowWarning(mlir::Operation *op, int col, int row,
183 AIE::DMAChannelDir dir, uint32_t channel,
184 uint32_t depth, size_t outstanding) {
185 return op->emitWarning()
186 << "pushes a DMA task onto tile (" << col << "," << row << ") "
187 << AIE::stringifyDMAChannelDir(dir) << " channel " << channel
188 << ", whose task queue is only " << depth << " deep, with "
189 << outstanding
190 << " push(es) not yet known to have completed. A push onto a full "
191 "queue is dropped by hardware and its transfer never runs, so "
192 "whatever waits on it blocks forever. Whether it happens depends "
193 "on how fast the consumer drains, which is why this is a warning "
194 "and not an error. Drain the channel with an await on this tile, "
195 "direction and channel before pushing again";
196}
197
198/// Wording for a dma_free_task that releases a buffer descriptor id the
199/// runtime pool could hand straight back out before hardware has actually
200/// finished the freed transfer -- the rolled-loop counterpart to
201/// aie-assign-runtime-sequence-bd-ids's unsafe-BD-reuse warning.
202inline mlir::InFlightDiagnostic
203emitUnsafeBdReuseWarning(mlir::Operation *freeOp, int col, int row,
204 AIE::DMAChannelDir dir, uint32_t channel) {
205 return freeOp->emitWarning()
206 << "frees a task on tile (" << col << "," << row << ") "
207 << AIE::stringifyDMAChannelDir(dir) << " channel " << channel
208 << " with no completion guarantee, so its buffer descriptor id "
209 "goes back to the runtime pool while the DMA may still be "
210 "running; the pool may hand that id straight to the next task "
211 "drawn on this tile, reprogramming a BD still in flight. Await "
212 "this task, or a later one queued on the same tile, direction "
213 "and channel, before freeing it -- an await consumes the oldest "
214 "outstanding token, regardless of the task it names";
215}
216
217/// Address and mask for a poll that waits for a free task-queue slot.
218/// Unavailable when the target reports no pollable
219/// occupancy register, or when `depth` is not a power of two -- a maskpoll
220/// tests masked equality, not "<", so "occupancy < depth" has to be the single
221/// "depth bit is clear" test (for depth 4, bit 22 of Task_Queue_Size).
222inline std::optional<std::pair<uint32_t, uint32_t>>
223queueSpacePoll(const AIE::AIETargetModel &tm, int col, int row,
224 uint32_t channel, AIE::DMAChannelDir dir, uint32_t depth) {
225 uint32_t fieldMask = tm.getDmaTaskQueueSizeMask();
226 if (!fieldMask || depth == 0 || (depth & (depth - 1)) != 0)
227 return std::nullopt;
228 std::optional<uint32_t> statusAddr =
229 tm.getDmaStatusAddress(col, row, channel, dir);
230 if (!statusAddr)
231 return std::nullopt;
232 uint32_t depthBit = depth << llvm::countr_zero(fieldMask);
233 if ((depthBit & fieldMask) != depthBit)
234 return std::nullopt;
235 return std::make_pair(*statusAddr, depthBit);
236}
237
238inline mlir::LogicalResult
239insertQueueSpaceWait(mlir::Operation *before, const AIE::AIETargetModel &tm,
240 int col, int row, uint32_t channel, AIE::DMAChannelDir dir,
241 uint32_t depth) {
242 auto poll = queueSpacePoll(tm, col, row, channel, dir, depth);
243 if (!poll)
244 return mlir::failure();
245 mlir::OpBuilder b(before);
246 mlir::Location loc = before->getLoc();
247 auto i32 = b.getI32Type();
248 auto cst = [&](uint32_t v) {
249 return mlir::arith::ConstantOp::create(b, loc, i32, b.getI32IntegerAttr(v))
250 .getResult();
251 };
252 NpuMaskPollOp::create(b, loc, cst(poll->first), cst(0), cst(poll->second),
253 /*buffer=*/nullptr, /*column=*/nullptr,
254 /*row=*/nullptr);
255 return mlir::success();
256}
257
258inline constexpr llvm::StringLiteral queueDiagnosedAttr =
259 "aiex.dma_queue_overflow_diagnosed";
260
261/// Handle a push on `key` that would land on a full queue: poll where one can
262/// be emitted, warn where it cannot (see insertQueueSpaceWait above for when
263/// that is). Warning rather than failing keeps a default from rejecting
264/// designs that build today on a target the user can do nothing about; the
265/// note is what keeps it from declining quietly.
266inline void guardQueueOverflow(DmaQueueModel &queue, mlir::Operation *push,
267 const AIE::AIETargetModel &tm,
268 const DmaQueueModel::ChannelKey &key,
269 uint32_t depth, bool enforce) {
270 int col = key[0], row = key[1];
271 auto dir = static_cast<AIE::DMAChannelDir>(key[2]);
272 auto chan = static_cast<uint32_t>(key[3]);
273
274 if (enforce && mlir::succeeded(insertQueueSpaceWait(push, tm, col, row, chan,
275 dir, depth))) {
276 queue.noteSpaceGuaranteed(key, depth);
277 return;
278 }
279 if (!queue.shouldReport(key))
280 return;
281 // Keep diagnostics per sequence/channel across task and combined lowering.
282 // This marker suppresses only warnings, never queue accounting or guards.
283 if (auto seq = push->getParentOfType<AIE::RuntimeSequenceOp>()) {
284 auto channel = mlir::DenseI32ArrayAttr::get(push->getContext(), key);
285 llvm::SmallVector<mlir::Attribute> diagnosed;
286 if (auto previous =
287 seq->getAttrOfType<mlir::ArrayAttr>(queueDiagnosedAttr)) {
288 if (llvm::is_contained(previous, channel))
289 return;
290 llvm::append_range(diagnosed, previous);
291 }
292 diagnosed.push_back(channel);
293 seq->setAttr(queueDiagnosedAttr,
294 mlir::ArrayAttr::get(push->getContext(), diagnosed));
295 }
296 mlir::InFlightDiagnostic diag = emitQueueOverflowWarning(
297 push, col, row, dir, chan, depth, queue.outstanding(key));
298 if (enforce)
299 diag.attachNote() << "the compiler would have waited for a free slot here, "
300 "but this target reports no pollable task-queue "
301 "occupancy register for this channel";
302}
303
304/// What one op in a loop body does to a channel queue. The two lowering paths
305/// spell pushes and awaits with different ops, so the fixed point below takes
306/// the classification from its caller and keeps the FIFO rule to itself.
307///
308/// Free is a read-only check, not a hardware event: a dma_free_task returns a
309/// buffer descriptor id to the compiler's pool without touching the channel,
310/// so it neither pushes nor retires anything here. It asks whether `task`'s
311/// own push -- the one a later pop could still be waiting behind -- has
312/// already been proven complete.
314 enum class Kind { Ignore, Configure, Push, Await, Free };
317 bool issuesToken = false;
318 mlir::Value task;
319
321 mlir::Value task) {
322 return {Kind::Configure, key, false, task};
323 }
325 mlir::Value task = {}) {
326 return {Kind::Push, key, issuesToken, task};
327 }
329 return {Kind::Await, key, false, {}};
330 }
332 return {Kind::Free, key, false, task};
333 }
334};
335
336/// Completion is distinct from queue occupancy: Task_Queue_Size excludes the
337/// running task. Task identities are path-local and renamed at joins, so a
338/// configure executed again in a loop is distinct from the carried old task.
340 struct Push {
342 unsigned owner;
343 // Stands in for the newest push of an unknown region, whose owners are
344 // all in forgottenOwners: none of them is proven complete while it is.
345 bool unknownRegion = false;
346 bool operator==(const Push &other) const {
347 return issuesToken == other.issuesToken && owner == other.owner &&
349 }
350 };
351 llvm::SmallVector<unsigned> tasks;
352 llvm::SmallVector<Push, 8> pushes;
353 std::set<unsigned> forgottenOwners;
354 bool unknownTokens = false;
356
357 bool operator==(const DmaCompletionState &other) const {
358 return tasks == other.tasks && pushes == other.pushes &&
360 unknownTokens == other.unknownTokens &&
362 }
363
364 // Canonical identities keep loop fixed points finite. Instances no longer
365 // named by any SSA task need only retain their token and FIFO position.
366 void normalize() {
367 llvm::DenseMap<unsigned, unsigned> names;
368 unsigned next = 1;
369 for (unsigned &task : tasks)
370 if (task) {
371 auto [it, inserted] = names.try_emplace(task, next);
372 next += inserted;
373 task = it->second;
374 }
375 for (Push &push : pushes)
376 push.owner = names.lookup(push.owner);
377 std::set<unsigned> renamed;
378 for (unsigned owner : forgottenOwners)
379 if (unsigned name = names.lookup(owner))
380 renamed.insert(name);
381 forgottenOwners = std::move(renamed);
382 }
383
384 void awaitToken() {
385 if (unknownTokens)
386 return;
387 auto tok =
388 llvm::find_if(pushes, [](const Push &p) { return p.issuesToken; });
389 if (tok != pushes.end()) {
390 pushes.erase(pushes.begin(), std::next(tok));
392 }
393 }
394
395 // Forgotten owners were pushed no later than any remaining unknown-region
396 // stand-in; they are complete once every such stand-in has drained too.
398 if (llvm::none_of(pushes, [](const Push &p) { return p.unknownRegion; }))
399 forgottenOwners.clear();
400 }
401
402 void noteIdle() {
403 // Completion does not consume tokens; later awaits still see these pushes.
404 for (Push &push : pushes) {
405 push.owner = 0;
406 push.unknownRegion = false;
407 }
408 forgottenOwners.clear();
409 }
410
411 // A real space poll leaves at most depth-1 queued tasks AND one running.
412 // Abstract truncation instead retains every lost owner's uncertainty.
413 void bound(unsigned limit, bool spaceGuaranteed) {
414 // Attempted pushes no longer bound FIFO progress after a possible drop.
415 spaceGuaranteed &= !mayHaveDroppedPushes;
416 bool drained = spaceGuaranteed && pushes.size() >= limit;
417 if (pushes.size() > limit) {
418 auto end = pushes.end() - limit;
419 for (const Push &push : llvm::make_range(pushes.begin(), end)) {
420 unknownTokens |= push.issuesToken;
421 if (!spaceGuaranteed && push.owner)
422 forgottenOwners.insert(push.owner);
423 }
424 pushes.erase(pushes.begin(), end);
425 }
426 if (drained)
428 }
429
430 bool isProvenComplete(unsigned owner) const {
431 return owner && !forgottenOwners.count(owner) &&
432 !llvm::any_of(pushes,
433 [&](const Push &p) { return p.owner == owner; });
434 }
435};
436
437/// Guard every push in a runtime sequence that can land on a full queue, and
438/// (when `warnUnsafeBdReuse`) warn at every Free effect whose own push is not
439/// yet proven complete along some reachable path.
440///
441/// Retain all reachable states at branches and loop exits, including zero
442/// iterations. Nested loops are analyzed recursively, not walked once. Queue
443/// states are finite because overflowing pushes are capped at the target depth;
444/// targets without a queued-task model must be ignored rather than accumulated.
445/// Analyze each independent channel separately to avoid a Cartesian product
446/// of states across channels.
447template <typename EffectFn>
448inline void guardSequenceQueueDepth(mlir::Region &body, DmaQueueModel &queue,
449 const AIE::AIETargetModel &tm, bool enforce,
450 bool warnUnsafeBdReuse, EffectFn effectOf) {
451 auto depthOf = [&](const DmaQueueModel::ChannelKey &k) {
452 return tm.getDmaTaskQueueDepth(k[0], k[1], k[3],
453 static_cast<AIE::DMAChannelDir>(k[2]));
454 };
455
456 struct PathState {
458 DmaCompletionState completion;
459 bool operator==(const PathState &other) const {
460 return queue == other.queue && completion == other.completion;
461 }
462 };
463 using States = llvm::SmallVector<PathState, 8>;
464 llvm::DenseMap<mlir::Value, unsigned> taskIndices;
465 auto track = [&](mlir::Value value) {
466 if (value && value.getType().isIndex())
467 taskIndices.try_emplace(value, taskIndices.size());
468 };
469 std::set<DmaQueueModel::ChannelKey> keys;
470 for (const auto &[key, channel] : queue.state())
471 if (depthOf(key) != 0)
472 keys.insert(key);
473 llvm::DenseMap<mlir::Operation *, QueueEffect> effects;
474 body.walk([&](mlir::Operation *op) {
475 if (warnUnsafeBdReuse) {
476 for (mlir::Value value : op->getOperands())
477 track(value);
478 for (mlir::Value value : op->getResults())
479 track(value);
480 for (mlir::Region &region : op->getRegions())
481 for (mlir::Block &block : region)
482 for (mlir::Value arg : block.getArguments())
483 track(arg);
484 }
485 QueueEffect e = effectOf(op);
486 if (!warnUnsafeBdReuse) {
487 e.task = {};
490 return;
491 }
492 if (e.kind != QueueEffect::Kind::Ignore && depthOf(e.key) != 0) {
493 keys.insert(e.key);
494 effects[op] = e;
495 }
496 });
497
498 // Each channel's analysis visits only its own pushes, awaits and polls and
499 // the ops enclosing them, in block order, rather than the whole sequence.
500 std::map<uint32_t, DmaQueueModel::ChannelKey> keyByStatus;
501 for (const auto &key : keys)
502 if (auto status = tm.getDmaStatusAddress(
503 key[0], key[1], key[3], static_cast<AIE::DMAChannelDir>(key[2])))
504 keyByStatus[*status] = key;
505 using Visits =
506 llvm::DenseMap<mlir::Block *, llvm::SmallVector<mlir::Operation *>>;
507 std::map<DmaQueueModel::ChannelKey, Visits> visits;
508 auto visit = [&](mlir::Operation *op, const DmaQueueModel::ChannelKey &key) {
509 Visits &blocks = visits[key];
510 for (mlir::Operation *at = op; at != body.getParentOp();
511 at = at->getParentOp()) {
512 auto &ops = blocks[at->getBlock()];
513 if (!ops.empty() && ops.back() == at)
514 break;
515 ops.push_back(at);
516 }
517 };
518 body.walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *op) {
519 if (warnUnsafeBdReuse && mlir::isa<mlir::scf::ForOp, mlir::scf::IfOp>(op))
520 for (const auto &key : keys)
521 visit(op, key);
522 if (auto it = effects.find(op); it != effects.end()) {
523 visit(op, it->second.key);
524 } else if (auto poll = mlir::dyn_cast<NpuMaskPollOp>(op)) {
525 if (auto address = poll.getAbsoluteAddress())
526 if (auto it = keyByStatus.find(*address); it != keyByStatus.end())
527 visit(op, it->second);
528 }
529 });
530
531 DmaQueueModel::State entry = queue.state();
532 DmaQueueModel::State merged = entry;
533 for (const auto &key : keys) {
534 uint32_t depth = depthOf(key);
535 const Visits &channelVisits = visits[key];
536 auto channelEffectOf = [&](mlir::Operation *op) {
537 auto it = effects.find(op);
538 return it != effects.end() && it->second.key == key ? it->second
539 : QueueEffect{};
540 };
541 llvm::SetVector<mlir::Operation *> overflowing;
542 llvm::SetVector<mlir::Operation *> unsafeFrees;
543 std::map<mlir::Operation *, DmaQueueModel::ChannelState> witnesses;
544 bool trackCompletion = warnUnsafeBdReuse;
545 auto warnUnsafeFree = [&](mlir::Operation *op) {
546 if (warnUnsafeBdReuse && unsafeFrees.insert(op)) {
547 auto diag = emitUnsafeBdReuseWarning(
548 op, key[0], key[1], static_cast<AIE::DMAChannelDir>(key[2]),
549 key[3]);
550 if (!trackCompletion)
551 diag.attachNote() << "task ownership analysis budget exhausted; "
552 "completion is conservatively unproven";
553 }
554 };
555 size_t completionWork = 0;
556 auto equivalent = [&](const PathState &a, const PathState &b) {
557 return a.queue == b.queue &&
558 (!trackCompletion || a.completion == b.completion);
559 };
560 auto contains = [&](const States &states, const PathState &state) {
561 return llvm::any_of(states, [&](const PathState &other) {
562 return equivalent(state, other);
563 });
564 };
565 auto compact = [&](States &states) {
566 States unique;
567 for (const auto &state : states)
568 if (!contains(unique, state))
569 unique.push_back(state);
570 states = std::move(unique);
571 };
572 // Bound optional ownership analysis without weakening queue-depth guards.
573 // Beyond the budget, all frees are uncertain and only occupancy is tracked.
574 auto append = [&](States &to, const States &from) {
575 if (warnUnsafeBdReuse && !trackCompletion)
576 compact(to);
577 for (const auto &state : from)
578 if (!contains(to, state)) {
579 if (trackCompletion && to.size() >= 256) {
580 trackCompletion = false;
581 compact(to);
582 if (contains(to, state))
583 continue;
584 }
585 to.push_back(state);
586 }
587 };
588 auto dedupe = [&](States &states) {
589 if (trackCompletion)
590 for (auto &state : states)
591 state.completion.normalize();
592 if (states.size() < 2)
593 return;
594 States unique;
595 append(unique, states);
596 states = std::move(unique);
597 };
598 auto transfer = [&](States &states, mlir::ValueRange from,
599 mlir::ValueRange to) {
600 if (!trackCompletion)
601 return;
602 for (auto &state : states) {
603 auto previous = state.completion.tasks;
604 for (auto [source, dest] : llvm::zip_equal(from, to)) {
605 auto src = taskIndices.find(source), dst = taskIndices.find(dest);
606 if (src != taskIndices.end() && dst != taskIndices.end())
607 state.completion.tasks[dst->second] = previous[src->second];
608 }
609 }
610 dedupe(states);
611 };
612 auto forgetScope = [&](States &states, mlir::Region &region,
613 bool keepArguments = false) {
614 if (!trackCompletion)
615 return;
616 for (auto [value, index] : taskIndices) {
617 mlir::Region *parent = value.getParentRegion();
618 if (parent != &region && !region.isAncestor(parent))
619 continue;
620 if (keepArguments)
621 if (auto arg = mlir::dyn_cast<mlir::BlockArgument>(value))
622 if (arg.getOwner() == &region.front())
623 continue;
624 for (auto &state : states)
625 state.completion.tasks[index] = 0;
626 }
627 dedupe(states);
628 };
629 bool canPoll =
630 enforce &&
631 queueSpacePoll(tm, key[0], key[1], key[3],
632 static_cast<AIE::DMAChannelDir>(key[2]), depth)
633 .has_value();
634
635 // Earlier passes may already have guarded a start. Credit only polls whose
636 // constant address and mask prove space or idleness on a tracked channel.
637 enum class PollEffect { Ignore, Space, Idle };
638 auto pollEffectOf = [&](NpuMaskPollOp poll) {
639 auto address = poll.getAbsoluteAddress();
640 auto mask = getConstantIntOperand(poll.getMask());
641 auto value = getConstantIntOperand(poll.getValue());
642 if (!address || !mask || !value)
643 return PollEffect::Ignore;
644 auto status = tm.getDmaStatusAddress(
645 key[0], key[1], key[3], static_cast<AIE::DMAChannelDir>(key[2]));
646 if (status != address)
647 return PollEffect::Ignore;
648 uint32_t idleMask = tm.getDmaChannelIdleMask();
649 if (idleMask && (*mask & idleMask) == idleMask &&
650 (*value & idleMask) == 0)
651 return PollEffect::Idle;
652 uint32_t fieldMask = tm.getDmaTaskQueueSizeMask();
653 if (!fieldMask || (depth & (depth - 1)) != 0)
654 return PollEffect::Ignore;
655 uint32_t depthBit = depth << llvm::countr_zero(fieldMask);
656 return depthBit && (depthBit & fieldMask) == depthBit &&
657 (*mask & depthBit) == depthBit && (*value & depthBit) == 0
658 ? PollEffect::Space
659 : PollEffect::Ignore;
660 };
661
662 // Unknown region semantics (including unstructured CFGs) must not credit
663 // conditional waits. Guard their pushes and conservatively forget tokens.
664 auto unknownRegion = [&](mlir::Region &region, States states) {
665 region.walk([&](mlir::Operation *op) {
666 QueueEffect e = channelEffectOf(op);
668 warnUnsafeFree(op);
670 return;
671 for (auto &state : states) {
672 state.queue.pushes.assign(depth, false);
673 state.queue.unknownTokens = true;
674 if (trackCompletion) {
675 state.completion.unknownTokens = true;
676 state.completion.mayHaveDroppedPushes |= !canPoll;
677 for (unsigned owner : state.completion.tasks)
678 if (owner)
679 state.completion.forgottenOwners.insert(owner);
680 // Later polls must count this push before crediting its owners.
681 state.completion.pushes.push_back(
682 {/*issuesToken=*/false, /*owner=*/0, /*unknownRegion=*/true});
683 state.completion.bound(depth + 1, /*spaceGuaranteed=*/false);
684 }
685 if (overflowing.insert(op))
686 witnesses[op] = state.queue;
687 }
688 });
689 dedupe(states);
690 return states;
691 };
692
693 std::function<States(mlir::Region &, States)> analyze;
694 analyze = [&](mlir::Region &region, States states) -> States {
695 if (!region.hasOneBlock())
696 return unknownRegion(region, std::move(states));
697 auto blockVisits = channelVisits.find(&region.front());
698 if (blockVisits == channelVisits.end())
699 return states;
700 for (mlir::Operation *visited : blockVisits->second) {
701 if (trackCompletion) {
702 completionWork += states.size();
703 if (completionWork > 16384) {
704 trackCompletion = false;
705 compact(states);
706 }
707 }
708 mlir::Operation &op = *visited;
709 if (auto loop = mlir::dyn_cast<mlir::scf::ForOp>(&op)) {
710 auto lb = mlir::getConstantIntValue(loop.getLowerBound());
711 auto ub = mlir::getConstantIntValue(loop.getUpperBound());
712 if (lb && ub && *lb >= *ub &&
713 (!loop->hasAttr("unsignedCmp") || (*lb >= 0 && *ub >= 0))) {
714 transfer(states, loop.getInitArgs(), loop.getResults());
715 continue;
716 }
717 // Every loop header state is a possible exit for an unknown trip
718 // count. Include the entry: even an await-only loop might execute
719 // zero times.
720 transfer(states, loop.getInitArgs(), loop.getRegionIterArgs());
721 States reachable = states;
722 for (size_t i = 0; i < reachable.size();) {
723 bool wasTrackingCompletion = trackCompletion;
724 States next = analyze(loop.getRegion(), States{reachable[i]});
725 transfer(next, loop.getYieldedValues(), loop.getRegionIterArgs());
726 forgetScope(next, loop.getRegion(), /*keepArguments=*/true);
727 append(reachable, next);
728 if (wasTrackingCompletion && !trackCompletion) {
729 // Revisit earlier paths with uncertain ownership as well.
730 compact(reachable);
731 i = 0;
732 } else {
733 ++i;
734 }
735 }
736 transfer(reachable, loop.getRegionIterArgs(), loop.getResults());
737 forgetScope(reachable, loop.getRegion());
738 states = std::move(reachable);
739 continue;
740 }
741 if (auto branch = mlir::dyn_cast<mlir::scf::IfOp>(&op)) {
742 States alternatives = analyze(branch.getThenRegion(), states);
743 States otherwise = analyze(branch.getElseRegion(), states);
744 if (branch.getNumResults()) {
745 transfer(alternatives, branch.thenYield().getResults(),
746 branch.getResults());
747 transfer(otherwise, branch.elseYield().getResults(),
748 branch.getResults());
749 }
750 forgetScope(alternatives, branch.getThenRegion());
751 forgetScope(otherwise, branch.getElseRegion());
752 append(alternatives, otherwise);
753 states = std::move(alternatives);
754 continue;
755 }
756 for (mlir::Region &nested : op.getRegions())
757 states = unknownRegion(nested, std::move(states));
758
759 if (auto poll = mlir::dyn_cast<NpuMaskPollOp>(&op))
760 if (auto effect = pollEffectOf(poll); effect != PollEffect::Ignore) {
761 for (auto &state : states) {
762 DmaQueueModel::noteSpaceGuaranteed(state.queue, depth);
763 if (trackCompletion) {
764 if (effect == PollEffect::Idle)
765 state.completion.noteIdle();
766 else
767 state.completion.bound(depth, /*spaceGuaranteed=*/true);
768 }
769 }
770 dedupe(states);
771 }
772
773 QueueEffect e = channelEffectOf(&op);
775 continue;
776 if (e.kind == QueueEffect::Kind::Free) {
777 if (warnUnsafeBdReuse)
778 for (const auto &state : states)
779 if (!trackCompletion ||
780 !state.completion.isProvenComplete(
781 state.completion.tasks[taskIndices.lookup(e.task)]))
782 warnUnsafeFree(&op);
783 continue;
784 }
785 for (auto &state : states) {
787 if (trackCompletion)
788 state.completion.tasks[taskIndices.lookup(e.task)] =
789 taskIndices.size() + 1;
790 continue;
791 }
793 DmaQueueModel::awaitToken(state.queue);
794 if (trackCompletion)
795 state.completion.awaitToken();
796 continue;
797 }
798 if (DmaQueueModel::wouldOverflow(state.queue, depth)) {
799 if (overflowing.insert(&op))
800 witnesses[&op] = state.queue;
801 DmaQueueModel::noteSpaceGuaranteed(state.queue, depth);
802 if (trackCompletion && canPoll)
803 state.completion.bound(depth, /*spaceGuaranteed=*/true);
804 else if (trackCompletion)
805 state.completion.mayHaveDroppedPushes = true;
806 }
807 DmaQueueModel::push(state.queue, e.issuesToken);
808 if (trackCompletion) {
809 unsigned owner =
810 e.task ? state.completion.tasks[taskIndices.lookup(e.task)] : 0;
811 state.completion.pushes.push_back({e.issuesToken, owner});
812 state.completion.bound(depth + 1, /*spaceGuaranteed=*/false);
813 }
814 }
815 dedupe(states);
816 }
817 return states;
818 };
819
820 DmaQueueModel::ChannelState channelEntry;
821 if (auto it = entry.find(key); it != entry.end())
822 channelEntry = it->second;
823 PathState initial{channelEntry, {}};
824 initial.completion.tasks.resize(taskIndices.size());
825 States exits = analyze(body, States{initial});
826 for (mlir::Operation *push : overflowing) {
827 queue.setState({{key, witnesses[push]}});
828 guardQueueOverflow(queue, push, tm, key, depth, enforce);
829 }
830
831 // Preserve exact straight-line state for callers. If control-flow paths
832 // disagree, retain the largest occupancy without crediting ambiguous
833 // tokens.
834 DmaQueueModel::ChannelState channelExit = exits.front().queue;
835 for (const auto &state : exits)
836 if (!(channelExit == state.queue)) {
837 channelExit.pushes.assign(
838 std::max(channelExit.pushes.size(), state.queue.pushes.size()),
839 false);
840 channelExit.unknownTokens = true;
841 }
842 merged[key] = channelExit;
843 }
844 queue.setState(std::move(merged));
845}
846
847} // namespace xilinx::AIEX
848
849#endif // AIE_DIALECT_AIEX_UTILS_DMAQUEUEMODEL_H
Tracks, per DMA channel, the pushes not yet known to have completed along one execution path.
static void push(ChannelState &q, bool issuesToken)
Record a push.
void push(const ChannelKey &key, bool issuesToken)
size_t outstanding(const ChannelKey &key) const
Outstanding pushes on key, for diagnostics.
static void awaitToken(ChannelState &q)
Retire what a task-completion-token await proves has drained.
void noteSpaceGuaranteed(const ChannelKey &key, uint32_t depth)
std::array< int, 4 > ChannelKey
{col, row, direction, channel}.
static void noteSpaceGuaranteed(ChannelState &q, uint32_t depth)
Record that a queue-space poll has guaranteed room.
bool shouldReport(const ChannelKey &key)
True the first time a channel is reported.
void awaitToken(const ChannelKey &key)
static bool wouldOverflow(const ChannelState &q, uint32_t depth)
True when a push would land on a full queue.
static ChannelKey keyOf(AIE::TileOp tile, AIE::DMAChannelDir direction, int64_t channel)
bool wouldOverflow(const ChannelKey &key, uint32_t depth) const
const State & state() const
std::map< ChannelKey, ChannelState > State
Every channel's queue.
virtual uint32_t getDmaChannelIdleMask() const
Return the bits of the getDmaStatusAddress() register that are all clear exactly when the channel has...
virtual uint32_t getDmaTaskQueueDepth() const =0
Return how many DMA tasks a single channel's task queue holds (unsupported = 0).
virtual uint32_t getDmaTaskQueueSizeMask() const
Return the mask selecting the live task-queue occupancy field within the getDmaStatusAddress() regist...
virtual std::optional< uint32_t > getDmaStatusAddress(int, int, int, AIE::DMAChannelDir) const
Return the address of the DMA status register for a channel, or nullopt when this target's layout has...
std::optional< std::pair< uint32_t, uint32_t > > queueSpacePoll(const AIE::AIETargetModel &tm, int col, int row, uint32_t channel, AIE::DMAChannelDir dir, uint32_t depth)
Address and mask for a poll that waits for a free task-queue slot.
mlir::InFlightDiagnostic emitQueueOverflowWarning(mlir::Operation *op, int col, int row, AIE::DMAChannelDir dir, uint32_t channel, uint32_t depth, size_t outstanding)
Shared wording for the two lowering paths, so the explanation does not depend on whether the transfer...
constexpr llvm::StringLiteral queueDiagnosedAttr
std::optional< DmaQueueModel::ChannelKey > syncChannelKey(NpuSyncOp sync)
The channel an aiex.npu.sync retires on, or nullopt when it cannot be pinned down.
mlir::LogicalResult insertQueueSpaceWait(mlir::Operation *before, const AIE::AIETargetModel &tm, int col, int row, uint32_t channel, AIE::DMAChannelDir dir, uint32_t depth)
void guardQueueOverflow(DmaQueueModel &queue, mlir::Operation *push, const AIE::AIETargetModel &tm, const DmaQueueModel::ChannelKey &key, uint32_t depth, bool enforce)
Handle a push on key that would land on a full queue: poll where one can be emitted,...
mlir::InFlightDiagnostic emitUnsafeBdReuseWarning(mlir::Operation *freeOp, int col, int row, AIE::DMAChannelDir dir, uint32_t channel)
Wording for a dma_free_task that releases a buffer descriptor id the runtime pool could hand straight...
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
void guardSequenceQueueDepth(mlir::Region &body, DmaQueueModel &queue, const AIE::AIETargetModel &tm, bool enforce, bool warnUnsafeBdReuse, EffectFn effectOf)
Guard every push in a runtime sequence that can land on a full queue, and (when warnUnsafeBdReuse) wa...
void awaitSync(DmaQueueModel &queue, NpuSyncOp sync)
Retire what an aiex.npu.sync proves has drained.
bool operator==(const Push &other) const
Completion is distinct from queue occupancy: Task_Queue_Size excludes the running task.
llvm::SmallVector< Push, 8 > pushes
void bound(unsigned limit, bool spaceGuaranteed)
bool operator==(const DmaCompletionState &other) const
llvm::SmallVector< unsigned > tasks
std::set< unsigned > forgottenOwners
bool isProvenComplete(unsigned owner) const
One channel's queue: its pushes not yet known to have completed, oldest first, each true when it issu...
llvm::SmallVector< bool, 8 > pushes
bool operator==(const ChannelState &other) const
What one op in a loop body does to a channel queue.
static QueueEffect await(DmaQueueModel::ChannelKey key)
static QueueEffect configure(DmaQueueModel::ChannelKey key, mlir::Value task)
static QueueEffect free(DmaQueueModel::ChannelKey key, mlir::Value task)
DmaQueueModel::ChannelKey key
static QueueEffect push(DmaQueueModel::ChannelKey key, bool issuesToken, mlir::Value task={})