MLIR-AIE 1.4.6.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 // Not op->emitWarning(): its "see current operation" note verifies and
186 // numbers the whole device for each warning, quadratic in the sequence.
187 return mlir::emitWarning(op->getLoc())
188 << "pushes a DMA task onto tile (" << col << "," << row << ") "
189 << AIE::stringifyDMAChannelDir(dir) << " channel " << channel
190 << ", whose task queue is only " << depth << " deep, with "
191 << outstanding
192 << " push(es) not yet known to have completed. A push onto a full "
193 "queue is dropped by hardware and its transfer never runs, so "
194 "whatever waits on it blocks forever. Whether it happens depends "
195 "on how fast the consumer drains, which is why this is a warning "
196 "and not an error. Drain the channel with an await on this tile, "
197 "direction and channel before pushing again";
198}
199
200/// Wording for a dma_free_task that releases a buffer descriptor id the
201/// runtime pool could hand straight back out before hardware has actually
202/// finished the freed transfer -- the rolled-loop counterpart to
203/// aie-assign-runtime-sequence-bd-ids's unsafe-BD-reuse warning.
204inline mlir::InFlightDiagnostic
205emitUnsafeBdReuseWarning(mlir::Operation *freeOp, int col, int row,
206 AIE::DMAChannelDir dir, uint32_t channel) {
207 // Not freeOp->emitWarning(), as in emitQueueOverflowWarning.
208 return mlir::emitWarning(freeOp->getLoc())
209 << "frees a task on tile (" << col << "," << row << ") "
210 << AIE::stringifyDMAChannelDir(dir) << " channel " << channel
211 << " with no completion guarantee, so its buffer descriptor id "
212 "goes back to the runtime pool while the DMA may still be "
213 "running; the pool may hand that id straight to the next task "
214 "drawn on this tile, reprogramming a BD still in flight. Await "
215 "this task, or a later one queued on the same tile, direction "
216 "and channel, before freeing it -- an await consumes the oldest "
217 "outstanding token, regardless of the task it names";
218}
219
220/// Address and mask for a poll that waits for a free task-queue slot.
221/// Unavailable when the target reports no pollable
222/// occupancy register, or when `depth` is not a power of two -- a maskpoll
223/// tests masked equality, not "<", so "occupancy < depth" has to be the single
224/// "depth bit is clear" test (for depth 4, bit 22 of Task_Queue_Size).
225inline std::optional<std::pair<uint32_t, uint32_t>>
226queueSpacePoll(const AIE::AIETargetModel &tm, int col, int row,
227 uint32_t channel, AIE::DMAChannelDir dir, uint32_t depth) {
228 uint32_t fieldMask = tm.getDmaTaskQueueSizeMask();
229 if (!fieldMask || depth == 0 || (depth & (depth - 1)) != 0)
230 return std::nullopt;
231 std::optional<uint32_t> statusAddr =
232 tm.getDmaStatusAddress(col, row, channel, dir);
233 if (!statusAddr)
234 return std::nullopt;
235 uint32_t depthBit = depth << llvm::countr_zero(fieldMask);
236 if ((depthBit & fieldMask) != depthBit)
237 return std::nullopt;
238 return std::make_pair(*statusAddr, depthBit);
239}
240
241inline mlir::LogicalResult
242insertQueueSpaceWait(mlir::Operation *before, const AIE::AIETargetModel &tm,
243 int col, int row, uint32_t channel, AIE::DMAChannelDir dir,
244 uint32_t depth) {
245 auto poll = queueSpacePoll(tm, col, row, channel, dir, depth);
246 if (!poll)
247 return mlir::failure();
248 mlir::OpBuilder b(before);
249 mlir::Location loc = before->getLoc();
250 auto i32 = b.getI32Type();
251 auto cst = [&](uint32_t v) {
252 return mlir::arith::ConstantOp::create(b, loc, i32, b.getI32IntegerAttr(v))
253 .getResult();
254 };
255 NpuMaskPollOp::create(b, loc, cst(poll->first), cst(0), cst(poll->second),
256 /*buffer=*/nullptr, /*column=*/nullptr,
257 /*row=*/nullptr);
258 return mlir::success();
259}
260
261inline constexpr llvm::StringLiteral queueDiagnosedAttr =
262 "aiex.dma_queue_overflow_diagnosed";
263
264/// Handle a push on `key` that would land on a full queue: poll where one can
265/// be emitted, warn where it cannot (see insertQueueSpaceWait above for when
266/// that is). Warning rather than failing keeps a default from rejecting
267/// designs that build today on a target the user can do nothing about; the
268/// note is what keeps it from declining quietly.
269inline void guardQueueOverflow(DmaQueueModel &queue, mlir::Operation *push,
270 const AIE::AIETargetModel &tm,
271 const DmaQueueModel::ChannelKey &key,
272 uint32_t depth, bool enforce) {
273 int col = key[0], row = key[1];
274 auto dir = static_cast<AIE::DMAChannelDir>(key[2]);
275 auto chan = static_cast<uint32_t>(key[3]);
276
277 if (enforce && mlir::succeeded(insertQueueSpaceWait(push, tm, col, row, chan,
278 dir, depth))) {
279 queue.noteSpaceGuaranteed(key, depth);
280 return;
281 }
282 if (!queue.shouldReport(key))
283 return;
284 // Keep diagnostics per sequence/channel across task and combined lowering.
285 // This marker suppresses only warnings, never queue accounting or guards.
286 if (auto seq = push->getParentOfType<AIE::RuntimeSequenceOp>()) {
287 auto channel = mlir::DenseI32ArrayAttr::get(push->getContext(), key);
288 llvm::SmallVector<mlir::Attribute> diagnosed;
289 if (auto previous =
290 seq->getAttrOfType<mlir::ArrayAttr>(queueDiagnosedAttr)) {
291 if (llvm::is_contained(previous, channel))
292 return;
293 llvm::append_range(diagnosed, previous);
294 }
295 diagnosed.push_back(channel);
296 seq->setAttr(queueDiagnosedAttr,
297 mlir::ArrayAttr::get(push->getContext(), diagnosed));
298 }
299 mlir::InFlightDiagnostic diag = emitQueueOverflowWarning(
300 push, col, row, dir, chan, depth, queue.outstanding(key));
301 if (enforce)
302 diag.attachNote() << "the compiler would have waited for a free slot here, "
303 "but this target reports no pollable task-queue "
304 "occupancy register for this channel";
305}
306
307/// What one op in a loop body does to a channel queue. The two lowering paths
308/// spell pushes and awaits with different ops, so the fixed point below takes
309/// the classification from its caller and keeps the FIFO rule to itself.
310///
311/// Free is a read-only check, not a hardware event: a dma_free_task returns a
312/// buffer descriptor id to the compiler's pool without touching the channel,
313/// so it neither pushes nor retires anything here. It asks whether `task`'s
314/// own push -- the one a later pop could still be waiting behind -- has
315/// already been proven complete.
317 enum class Kind { Ignore, Configure, Push, Await, Free };
320 bool issuesToken = false;
321 mlir::Value task;
322
324 mlir::Value task) {
325 return {Kind::Configure, key, false, task};
326 }
328 mlir::Value task = {}) {
329 return {Kind::Push, key, issuesToken, task};
330 }
332 return {Kind::Await, key, false, {}};
333 }
335 return {Kind::Free, key, false, task};
336 }
337};
338
339/// Completion is distinct from queue occupancy: Task_Queue_Size excludes the
340/// running task. Task identities are path-local and renamed at joins, so a
341/// configure executed again in a loop is distinct from the carried old task.
343 struct Push {
345 unsigned owner;
346 // Stands in for the newest push of an unknown region, whose owners are
347 // all in forgottenOwners: none of them is proven complete while it is.
348 bool unknownRegion = false;
349 bool operator==(const Push &other) const {
350 return issuesToken == other.issuesToken && owner == other.owner &&
352 }
353 };
354 llvm::SmallVector<unsigned> tasks;
355 llvm::SmallVector<Push, 8> pushes;
356 std::set<unsigned> forgottenOwners;
357 bool unknownTokens = false;
359
360 bool operator==(const DmaCompletionState &other) const {
361 return tasks == other.tasks && pushes == other.pushes &&
363 unknownTokens == other.unknownTokens &&
365 }
366
367 // Canonical identities keep loop fixed points finite. Instances no longer
368 // named by any SSA task need only retain their token and FIFO position.
369 void normalize() {
370 llvm::DenseMap<unsigned, unsigned> names;
371 unsigned next = 1;
372 for (unsigned &task : tasks)
373 if (task) {
374 auto [it, inserted] = names.try_emplace(task, next);
375 next += inserted;
376 task = it->second;
377 }
378 for (Push &push : pushes)
379 push.owner = names.lookup(push.owner);
380 std::set<unsigned> renamed;
381 for (unsigned owner : forgottenOwners)
382 if (unsigned name = names.lookup(owner))
383 renamed.insert(name);
384 forgottenOwners = std::move(renamed);
385 }
386
387 void awaitToken() {
388 if (unknownTokens)
389 return;
390 auto tok =
391 llvm::find_if(pushes, [](const Push &p) { return p.issuesToken; });
392 if (tok != pushes.end()) {
393 pushes.erase(pushes.begin(), std::next(tok));
395 }
396 }
397
398 // Forgotten owners were pushed no later than any remaining unknown-region
399 // stand-in; they are complete once every such stand-in has drained too.
401 if (llvm::none_of(pushes, [](const Push &p) { return p.unknownRegion; }))
402 forgottenOwners.clear();
403 }
404
405 void noteIdle() {
406 // Completion does not consume tokens; later awaits still see these pushes.
407 for (Push &push : pushes) {
408 push.owner = 0;
409 push.unknownRegion = false;
410 }
411 forgottenOwners.clear();
412 }
413
414 // A real space poll leaves at most depth-1 queued tasks AND one running.
415 // Abstract truncation instead retains every lost owner's uncertainty.
416 void bound(unsigned limit, bool spaceGuaranteed) {
417 // Attempted pushes no longer bound FIFO progress after a possible drop.
418 spaceGuaranteed &= !mayHaveDroppedPushes;
419 bool drained = spaceGuaranteed && pushes.size() >= limit;
420 if (pushes.size() > limit) {
421 auto end = pushes.end() - limit;
422 for (const Push &push : llvm::make_range(pushes.begin(), end)) {
423 unknownTokens |= push.issuesToken;
424 if (!spaceGuaranteed && push.owner)
425 forgottenOwners.insert(push.owner);
426 }
427 pushes.erase(pushes.begin(), end);
428 }
429 if (drained)
431 }
432
433 bool isProvenComplete(unsigned owner) const {
434 return owner && !forgottenOwners.count(owner) &&
435 !llvm::any_of(pushes,
436 [&](const Push &p) { return p.owner == owner; });
437 }
438};
439
440/// Guard every push in a runtime sequence that can land on a full queue, and
441/// (when `warnUnsafeBdReuse`) warn at every Free effect whose own push is not
442/// yet proven complete along some reachable path.
443///
444/// Retain all reachable states at branches and loop exits, including zero
445/// iterations. Nested loops are analyzed recursively, not walked once. Queue
446/// states are finite because overflowing pushes are capped at the target depth;
447/// targets without a queued-task model must be ignored rather than accumulated.
448/// Analyze each independent channel separately to avoid a Cartesian product
449/// of states across channels.
450template <typename EffectFn>
451inline void guardSequenceQueueDepth(mlir::Region &body, DmaQueueModel &queue,
452 const AIE::AIETargetModel &tm, bool enforce,
453 bool warnUnsafeBdReuse, EffectFn effectOf) {
454 auto depthOf = [&](const DmaQueueModel::ChannelKey &k) {
455 return tm.getDmaTaskQueueDepth(k[0], k[1], k[3],
456 static_cast<AIE::DMAChannelDir>(k[2]));
457 };
458
459 struct PathState {
461 DmaCompletionState completion;
462 bool operator==(const PathState &other) const {
463 return queue == other.queue && completion == other.completion;
464 }
465 };
466 using States = llvm::SmallVector<PathState, 8>;
467 llvm::DenseMap<mlir::Value, unsigned> taskIndices;
468 auto track = [&](mlir::Value value) {
469 if (value && value.getType().isIndex())
470 taskIndices.try_emplace(value, taskIndices.size());
471 };
472 std::set<DmaQueueModel::ChannelKey> keys;
473 for (const auto &[key, channel] : queue.state())
474 if (depthOf(key) != 0)
475 keys.insert(key);
476 llvm::DenseMap<mlir::Operation *, QueueEffect> effects;
477 body.walk([&](mlir::Operation *op) {
478 if (warnUnsafeBdReuse) {
479 for (mlir::Value value : op->getOperands())
480 track(value);
481 for (mlir::Value value : op->getResults())
482 track(value);
483 for (mlir::Region &region : op->getRegions())
484 for (mlir::Block &block : region)
485 for (mlir::Value arg : block.getArguments())
486 track(arg);
487 }
488 QueueEffect e = effectOf(op);
489 if (!warnUnsafeBdReuse) {
490 e.task = {};
493 return;
494 }
495 if (e.kind != QueueEffect::Kind::Ignore && depthOf(e.key) != 0) {
496 keys.insert(e.key);
497 effects[op] = e;
498 }
499 });
500
501 // Each channel's analysis visits only its own pushes, awaits and polls and
502 // the ops enclosing them, in block order, rather than the whole sequence.
503 std::map<uint32_t, DmaQueueModel::ChannelKey> keyByStatus;
504 for (const auto &key : keys)
505 if (auto status = tm.getDmaStatusAddress(
506 key[0], key[1], key[3], static_cast<AIE::DMAChannelDir>(key[2])))
507 keyByStatus[*status] = key;
508 using Visits =
509 llvm::DenseMap<mlir::Block *, llvm::SmallVector<mlir::Operation *>>;
510 std::map<DmaQueueModel::ChannelKey, Visits> visits;
511 auto visit = [&](mlir::Operation *op, const DmaQueueModel::ChannelKey &key) {
512 Visits &blocks = visits[key];
513 for (mlir::Operation *at = op; at != body.getParentOp();
514 at = at->getParentOp()) {
515 auto &ops = blocks[at->getBlock()];
516 if (!ops.empty() && ops.back() == at)
517 break;
518 ops.push_back(at);
519 }
520 };
521 body.walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *op) {
522 if (warnUnsafeBdReuse && mlir::isa<mlir::scf::ForOp, mlir::scf::IfOp>(op))
523 for (const auto &key : keys)
524 visit(op, key);
525 if (auto it = effects.find(op); it != effects.end()) {
526 visit(op, it->second.key);
527 } else if (auto poll = mlir::dyn_cast<NpuMaskPollOp>(op)) {
528 if (auto address = poll.getAbsoluteAddress())
529 if (auto it = keyByStatus.find(*address); it != keyByStatus.end())
530 visit(op, it->second);
531 }
532 });
533
534 DmaQueueModel::State entry = queue.state();
535 DmaQueueModel::State merged = entry;
536 for (const auto &key : keys) {
537 uint32_t depth = depthOf(key);
538 const Visits &channelVisits = visits[key];
539 auto channelEffectOf = [&](mlir::Operation *op) {
540 auto it = effects.find(op);
541 return it != effects.end() && it->second.key == key ? it->second
542 : QueueEffect{};
543 };
544 llvm::SetVector<mlir::Operation *> overflowing;
545 llvm::SetVector<mlir::Operation *> unsafeFrees;
546 std::map<mlir::Operation *, DmaQueueModel::ChannelState> witnesses;
547 bool trackCompletion = warnUnsafeBdReuse;
548 auto warnUnsafeFree = [&](mlir::Operation *op) {
549 if (warnUnsafeBdReuse && unsafeFrees.insert(op)) {
550 auto diag = emitUnsafeBdReuseWarning(
551 op, key[0], key[1], static_cast<AIE::DMAChannelDir>(key[2]),
552 key[3]);
553 if (!trackCompletion)
554 diag.attachNote() << "task ownership analysis budget exhausted; "
555 "completion is conservatively unproven";
556 }
557 };
558 size_t completionWork = 0;
559 auto equivalent = [&](const PathState &a, const PathState &b) {
560 return a.queue == b.queue &&
561 (!trackCompletion || a.completion == b.completion);
562 };
563 auto contains = [&](const States &states, const PathState &state) {
564 return llvm::any_of(states, [&](const PathState &other) {
565 return equivalent(state, other);
566 });
567 };
568 auto compact = [&](States &states) {
569 States unique;
570 for (const auto &state : states)
571 if (!contains(unique, state))
572 unique.push_back(state);
573 states = std::move(unique);
574 };
575 // Bound optional ownership analysis without weakening queue-depth guards.
576 // Beyond the budget, all frees are uncertain and only occupancy is tracked.
577 auto append = [&](States &to, const States &from) {
578 if (warnUnsafeBdReuse && !trackCompletion)
579 compact(to);
580 for (const auto &state : from)
581 if (!contains(to, state)) {
582 if (trackCompletion && to.size() >= 256) {
583 trackCompletion = false;
584 compact(to);
585 if (contains(to, state))
586 continue;
587 }
588 to.push_back(state);
589 }
590 };
591 auto dedupe = [&](States &states) {
592 if (trackCompletion)
593 for (auto &state : states)
594 state.completion.normalize();
595 if (states.size() < 2)
596 return;
597 States unique;
598 append(unique, states);
599 states = std::move(unique);
600 };
601 auto transfer = [&](States &states, mlir::ValueRange from,
602 mlir::ValueRange to) {
603 if (!trackCompletion)
604 return;
605 for (auto &state : states) {
606 auto previous = state.completion.tasks;
607 for (auto [source, dest] : llvm::zip_equal(from, to)) {
608 auto src = taskIndices.find(source), dst = taskIndices.find(dest);
609 if (src != taskIndices.end() && dst != taskIndices.end())
610 state.completion.tasks[dst->second] = previous[src->second];
611 }
612 }
613 dedupe(states);
614 };
615 auto forgetScope = [&](States &states, mlir::Region &region,
616 bool keepArguments = false) {
617 if (!trackCompletion)
618 return;
619 for (auto [value, index] : taskIndices) {
620 mlir::Region *parent = value.getParentRegion();
621 if (parent != &region && !region.isAncestor(parent))
622 continue;
623 if (keepArguments)
624 if (auto arg = mlir::dyn_cast<mlir::BlockArgument>(value))
625 if (arg.getOwner() == &region.front())
626 continue;
627 for (auto &state : states)
628 state.completion.tasks[index] = 0;
629 }
630 dedupe(states);
631 };
632 bool canPoll =
633 enforce &&
634 queueSpacePoll(tm, key[0], key[1], key[3],
635 static_cast<AIE::DMAChannelDir>(key[2]), depth)
636 .has_value();
637
638 // Earlier passes may already have guarded a start. Credit only polls whose
639 // constant address and mask prove space or idleness on a tracked channel.
640 enum class PollEffect { Ignore, Space, Idle };
641 auto pollEffectOf = [&](NpuMaskPollOp poll) {
642 auto address = poll.getAbsoluteAddress();
643 auto mask = getConstantIntOperand(poll.getMask());
644 auto value = getConstantIntOperand(poll.getValue());
645 if (!address || !mask || !value)
646 return PollEffect::Ignore;
647 auto status = tm.getDmaStatusAddress(
648 key[0], key[1], key[3], static_cast<AIE::DMAChannelDir>(key[2]));
649 if (status != address)
650 return PollEffect::Ignore;
651 uint32_t idleMask = tm.getDmaChannelIdleMask();
652 if (idleMask && (*mask & idleMask) == idleMask &&
653 (*value & idleMask) == 0)
654 return PollEffect::Idle;
655 uint32_t fieldMask = tm.getDmaTaskQueueSizeMask();
656 if (!fieldMask || (depth & (depth - 1)) != 0)
657 return PollEffect::Ignore;
658 uint32_t depthBit = depth << llvm::countr_zero(fieldMask);
659 return depthBit && (depthBit & fieldMask) == depthBit &&
660 (*mask & depthBit) == depthBit && (*value & depthBit) == 0
661 ? PollEffect::Space
662 : PollEffect::Ignore;
663 };
664
665 // Unknown region semantics (including unstructured CFGs) must not credit
666 // conditional waits. Guard their pushes and conservatively forget tokens.
667 auto unknownRegion = [&](mlir::Region &region, States states) {
668 region.walk([&](mlir::Operation *op) {
669 QueueEffect e = channelEffectOf(op);
671 warnUnsafeFree(op);
673 return;
674 for (auto &state : states) {
675 state.queue.pushes.assign(depth, false);
676 state.queue.unknownTokens = true;
677 if (trackCompletion) {
678 state.completion.unknownTokens = true;
679 state.completion.mayHaveDroppedPushes |= !canPoll;
680 for (unsigned owner : state.completion.tasks)
681 if (owner)
682 state.completion.forgottenOwners.insert(owner);
683 // Later polls must count this push before crediting its owners.
684 state.completion.pushes.push_back(
685 {/*issuesToken=*/false, /*owner=*/0, /*unknownRegion=*/true});
686 state.completion.bound(depth + 1, /*spaceGuaranteed=*/false);
687 }
688 if (overflowing.insert(op))
689 witnesses[op] = state.queue;
690 }
691 });
692 dedupe(states);
693 return states;
694 };
695
696 std::function<States(mlir::Region &, States)> analyze;
697 analyze = [&](mlir::Region &region, States states) -> States {
698 if (!region.hasOneBlock())
699 return unknownRegion(region, std::move(states));
700 auto blockVisits = channelVisits.find(&region.front());
701 if (blockVisits == channelVisits.end())
702 return states;
703 for (mlir::Operation *visited : blockVisits->second) {
704 if (trackCompletion) {
705 completionWork += states.size();
706 if (completionWork > 16384) {
707 trackCompletion = false;
708 compact(states);
709 }
710 }
711 mlir::Operation &op = *visited;
712 if (auto loop = mlir::dyn_cast<mlir::scf::ForOp>(&op)) {
713 auto lb = mlir::getConstantIntValue(loop.getLowerBound());
714 auto ub = mlir::getConstantIntValue(loop.getUpperBound());
715 if (lb && ub && *lb >= *ub &&
716 (!loop->hasAttr("unsignedCmp") || (*lb >= 0 && *ub >= 0))) {
717 transfer(states, loop.getInitArgs(), loop.getResults());
718 continue;
719 }
720 // Every loop header state is a possible exit for an unknown trip
721 // count. Include the entry: even an await-only loop might execute
722 // zero times.
723 transfer(states, loop.getInitArgs(), loop.getRegionIterArgs());
724 States reachable = states;
725 for (size_t i = 0; i < reachable.size();) {
726 bool wasTrackingCompletion = trackCompletion;
727 States next = analyze(loop.getRegion(), States{reachable[i]});
728 transfer(next, loop.getYieldedValues(), loop.getRegionIterArgs());
729 forgetScope(next, loop.getRegion(), /*keepArguments=*/true);
730 append(reachable, next);
731 if (wasTrackingCompletion && !trackCompletion) {
732 // Revisit earlier paths with uncertain ownership as well.
733 compact(reachable);
734 i = 0;
735 } else {
736 ++i;
737 }
738 }
739 transfer(reachable, loop.getRegionIterArgs(), loop.getResults());
740 forgetScope(reachable, loop.getRegion());
741 states = std::move(reachable);
742 continue;
743 }
744 if (auto branch = mlir::dyn_cast<mlir::scf::IfOp>(&op)) {
745 States alternatives = analyze(branch.getThenRegion(), states);
746 States otherwise = analyze(branch.getElseRegion(), states);
747 if (branch.getNumResults()) {
748 transfer(alternatives, branch.thenYield().getResults(),
749 branch.getResults());
750 transfer(otherwise, branch.elseYield().getResults(),
751 branch.getResults());
752 }
753 forgetScope(alternatives, branch.getThenRegion());
754 forgetScope(otherwise, branch.getElseRegion());
755 append(alternatives, otherwise);
756 states = std::move(alternatives);
757 continue;
758 }
759 for (mlir::Region &nested : op.getRegions())
760 states = unknownRegion(nested, std::move(states));
761
762 if (auto poll = mlir::dyn_cast<NpuMaskPollOp>(&op))
763 if (auto effect = pollEffectOf(poll); effect != PollEffect::Ignore) {
764 for (auto &state : states) {
765 DmaQueueModel::noteSpaceGuaranteed(state.queue, depth);
766 if (trackCompletion) {
767 if (effect == PollEffect::Idle)
768 state.completion.noteIdle();
769 else
770 state.completion.bound(depth, /*spaceGuaranteed=*/true);
771 }
772 }
773 dedupe(states);
774 }
775
776 QueueEffect e = channelEffectOf(&op);
778 continue;
779 if (e.kind == QueueEffect::Kind::Free) {
780 if (warnUnsafeBdReuse)
781 for (const auto &state : states)
782 if (!trackCompletion ||
783 !state.completion.isProvenComplete(
784 state.completion.tasks[taskIndices.lookup(e.task)]))
785 warnUnsafeFree(&op);
786 continue;
787 }
788 for (auto &state : states) {
790 if (trackCompletion)
791 state.completion.tasks[taskIndices.lookup(e.task)] =
792 taskIndices.size() + 1;
793 continue;
794 }
796 DmaQueueModel::awaitToken(state.queue);
797 if (trackCompletion)
798 state.completion.awaitToken();
799 continue;
800 }
801 if (DmaQueueModel::wouldOverflow(state.queue, depth)) {
802 if (overflowing.insert(&op))
803 witnesses[&op] = state.queue;
804 DmaQueueModel::noteSpaceGuaranteed(state.queue, depth);
805 if (trackCompletion && canPoll)
806 state.completion.bound(depth, /*spaceGuaranteed=*/true);
807 else if (trackCompletion)
808 state.completion.mayHaveDroppedPushes = true;
809 }
810 DmaQueueModel::push(state.queue, e.issuesToken);
811 if (trackCompletion) {
812 unsigned owner =
813 e.task ? state.completion.tasks[taskIndices.lookup(e.task)] : 0;
814 state.completion.pushes.push_back({e.issuesToken, owner});
815 state.completion.bound(depth + 1, /*spaceGuaranteed=*/false);
816 }
817 }
818 dedupe(states);
819 }
820 return states;
821 };
822
823 DmaQueueModel::ChannelState channelEntry;
824 if (auto it = entry.find(key); it != entry.end())
825 channelEntry = it->second;
826 PathState initial{channelEntry, {}};
827 initial.completion.tasks.resize(taskIndices.size());
828 States exits = analyze(body, States{initial});
829 for (mlir::Operation *push : overflowing) {
830 queue.setState({{key, witnesses[push]}});
831 guardQueueOverflow(queue, push, tm, key, depth, enforce);
832 }
833
834 // Preserve exact straight-line state for callers. If control-flow paths
835 // disagree, retain the largest occupancy without crediting ambiguous
836 // tokens.
837 DmaQueueModel::ChannelState channelExit = exits.front().queue;
838 for (const auto &state : exits)
839 if (!(channelExit == state.queue)) {
840 channelExit.pushes.assign(
841 std::max(channelExit.pushes.size(), state.queue.pushes.size()),
842 false);
843 channelExit.unknownTokens = true;
844 }
845 merged[key] = channelExit;
846 }
847 queue.setState(std::move(merged));
848}
849
850} // namespace xilinx::AIEX
851
852#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={})