MLIR-AIE 1.4.5.dev
AIEAssignRuntimeSequenceBDIDs.cpp
Go to the documentation of this file.
1//===- AIEAssignRuntimeSequenceBDIDs.cpp ------------------------*- C++ -*-===//
2//
3// Copyright (C) 2024 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Assigns buffer-descriptor (BD) IDs to the DMA tasks configured in a runtime
9// sequence.
10//
11// This pass runs on straight-line IR: --aie-unroll-runtime-sequence-loops has
12// unrolled every constant-trip scf.for, and canonicalization has folded every
13// constant-predicate scf.if, before this pass runs. So in the static path no
14// scf op survives to reach the allocator -- a rolled ping-pong over a constant
15// loop is just N straight-line configures whose ids ordinary liveness reuse
16// recycles. Any scf.for/scf.if still present is therefore runtime-valued, which
17// the static path cannot lower (the runtime sequence becomes a flat, branchless
18// NPU instruction stream); such forms are rejected here for the dynamic EmitC
19// path (Phase 2).
20//
21// aie-split-long-repeats has already made every start exactly one queue push.
22//
23// Before allocating, the pass also checks the per-channel hardware resources a
24// sequence can exhaust (see verifyChannelUsage): task-completion-token (TCT)
25// imbalance -- an await with no matching issue_token push on its channel would
26// deadlock the host -- and DMA task-queue overflow, where more transfers are
27// pushed onto a channel than its queue holds. On the straight-line IR the
28// allocator sees, both are single-pass per-channel counts.
29//
30// With reclaim-bds, when a tile runs out of ids, the pass takes them back from
31// a started task that was never released (see reclaimFor) rather than failing:
32// from one a status poll already proves finished, or else by inserting a poll
33// that does. Only allocations that would fail change, so a sequence that fits
34// compiles to the same instructions either way. It is opt-in because the poll
35// never returns if the task's completion depends on a later push.
36//
37//===----------------------------------------------------------------------===//
38
44
45#include "mlir/Dialect/Arith/IR/Arith.h"
46#include "mlir/Dialect/SCF/IR/SCF.h"
47#include "mlir/Pass/Pass.h"
48#include "llvm/ADT/DenseMap.h"
49#include "llvm/ADT/SmallPtrSet.h"
50
51#include <array>
52#include <map>
53#include <set>
54
55namespace xilinx::AIEX {
56#define GEN_PASS_DEF_AIEASSIGNRUNTIMESEQUENCEBDIDS
57#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
58} // namespace xilinx::AIEX
59
60using namespace mlir;
61using namespace xilinx;
62using namespace xilinx::AIEX;
63
64namespace {
65
66struct AIEAssignRuntimeSequenceBDIDsPass
67 : xilinx::AIEX::impl::AIEAssignRuntimeSequenceBDIDsBase<
68 AIEAssignRuntimeSequenceBDIDsPass> {
69 using Base = xilinx::AIEX::impl::AIEAssignRuntimeSequenceBDIDsBase<
70 AIEAssignRuntimeSequenceBDIDsPass>;
71 AIEAssignRuntimeSequenceBDIDsPass() = default;
72 AIEAssignRuntimeSequenceBDIDsPass(
73 const AIEAssignRuntimeSequenceBDIDsOptions &options)
74 : Base(options) {}
75
76 llvm::DenseMap<AIE::TileOp, BdIdGenerator> gens;
77
78 static std::optional<DmaQueueModel::ChannelKey>
79 otherTokenChannel(Operation *op) {
80 if (auto push = dyn_cast<NpuPushQueueOp>(op)) {
81 if (push.getIssueToken())
82 return DmaQueueModel::ChannelKey{static_cast<int>(push.getColumn()),
83 static_cast<int>(push.getRow()),
84 static_cast<int>(push.getDirection()),
85 static_cast<int>(push.getChannel())};
86 } else if (auto copy = dyn_cast<NpuDmaMemcpyNdOp>(op)) {
87 auto allocation = AIE::ShimDMAAllocationOp::getForSymbol(
88 op->getParentOfType<AIE::DeviceOp>(),
89 copy.getMetadata().getRootReference());
90 if (allocation &&
91 (copy.getIssueToken() ||
92 allocation.getChannelDir() == AIE::DMAChannelDir::S2MM)) {
93 if (AIE::TileOp tile = allocation.getTileOp())
94 return DmaQueueModel::keyOf(tile, allocation.getChannelDir(),
95 allocation.getChannelIndex());
96 }
97 }
98 return std::nullopt;
99 }
100
101 static std::optional<DmaQueueModel::ChannelKey>
102 otherAwaitChannel(Operation *op) {
103 if (auto sync = dyn_cast<NpuSyncOp>(op))
104 return syncChannelKey(sync);
105 if (auto wait = dyn_cast<NpuDmaWaitOp>(op)) {
106 auto allocation = AIE::ShimDMAAllocationOp::getForSymbol(
107 op->getParentOfType<AIE::DeviceOp>(), wait.getSymbol());
108 if (allocation)
109 if (AIE::TileOp tile = allocation.getTileOp())
110 return DmaQueueModel::keyOf(tile, allocation.getChannelDir(),
111 allocation.getChannelIndex());
112 }
113 return std::nullopt;
114 }
115
116 // The BD ids each device's static DMAs take, by tile. Generators restart
117 // at every PDI load, so this is read once per tile per load.
118 //
119 // Known limitation: after a load_pdi of another device, this and
120 // loopingStartsOf read that sibling device's DMA programs, which races with
121 // the sibling's own nested pipeline (aie-dma-to-npu adds globals to its
122 // block) once devices run in parallel. The MLIR aiecc builds against runs
123 // them one at a time today. Splitting the nest does not help, since the pass
124 // manager merges adjacent device nests; a parallel build needs these
125 // summaries computed at module scope before the nest.
126 llvm::DenseMap<Operation *,
127 llvm::DenseMap<AIE::TileID, SmallVector<uint32_t>>>
128 staticBdIds;
129
130 // Mark every BD id a static DMA already took on `tile`, so this allocator
131 // doesn't hand the same id to a runtime-sequence task.
132 void seedFromStaticBds(AIE::DeviceOp device, AIE::TileOp tile,
133 BdIdGenerator &gen) {
134 auto [it, inserted] = staticBdIds.try_emplace(device);
135 if (inserted)
136 for (AIE::DmaBody program : device.getOps<AIE::DmaBody>())
137 llvm::append_range(it->second[program.getTileID()],
138 AIE::getAssignedBdIds(program));
139 for (uint32_t id : it->second.lookup(tile.getTileID()))
140 if (!gen.bdIdAlreadyAssigned(id))
141 gen.assignBdId(id);
142 }
143
144 BdIdGenerator &getGeneratorForTile(AIE::TileOp tile) {
145 auto it = gens.find(tile);
146 if (it == gens.end()) {
147 AIE::DeviceOp device = tile->getParentOfType<AIE::DeviceOp>();
148 it = gens.insert({tile, BdIdGenerator(tile.getCol(), tile.getRow(),
149 device.getTargetModel())})
150 .first;
151 seedFromStaticBds(device, tile, it->second);
152 if (loadedDevice && loadedDevice != device)
153 seedFromStaticBds(loadedDevice, tile, it->second);
154 }
155 return it->second;
156 }
157
158 // Each device's static aie.dma_starts whose BD chain never ends, by channel.
159 llvm::DenseMap<Operation *,
160 std::map<DmaQueueModel::ChannelKey, AIE::DMAStartOp>>
161 loopingStarts;
162 // Channels an aiex.dma_channel_reset drained since the latest load.
163 std::set<DmaQueueModel::ChannelKey> resetChannels;
164
165 std::map<DmaQueueModel::ChannelKey, AIE::DMAStartOp> &
166 loopingStartsOf(AIE::DeviceOp device) {
167 auto [it, inserted] = loopingStarts.try_emplace(device);
168 auto &starts = it->second;
169 if (!inserted)
170 return starts;
171 for (AIE::DmaBody program : device.getOps<AIE::DmaBody>()) {
172 AIE::TileID tile = program.getTileID();
173 program->walk([&](AIE::DMAStartOp start) {
174 if (start.getEndpoint())
175 return;
176 llvm::SmallPtrSet<Block *, 8> visited;
177 Block *bd = start.getDest();
178 while (bd && visited.insert(bd).second)
179 bd = bd->getNumSuccessors() == 1 ? bd->getSuccessor(0) : nullptr;
180 if (bd)
181 starts.try_emplace({tile.col, tile.row,
182 static_cast<int>(start.getChannelDir()),
183 start.getChannelIndex()},
184 start);
185 });
186 }
187 return starts;
188 }
189
190 // A task pushed behind a static program that never ends never runs. Only
191 // a named load tells which programs run; the sequence's own device does
192 // until the first load.
193 LogicalResult checkNotBehindLoop(DMAStartTaskOp start,
194 DMAConfigureTaskOp cfg) {
195 if (opaqueLoad)
196 return success();
197 DmaQueueModel::ChannelKey key = channelOf(cfg);
198 if (resetChannels.count(key))
199 return success();
200 auto &starts =
201 loopingStartsOf(loadedDevice ? loadedDevice : getOperation());
202 auto it = starts.find(key);
203 if (it == starts.end())
204 return success();
205 InFlightDiagnostic diag =
206 start.emitOpError("starts a task on a channel whose static DMA "
207 "program loops forever, so the task never runs; "
208 "end that program's BD chain, or reset the channel "
209 "with aiex.dma_channel_reset first");
210 diag.attachNote(it->second.getLoc()) << "the program starts here";
211 return diag;
212 }
213
214 // The device the latest aiex.npu.load_pdi configured. A sequence that
215 // aie-materialize-runtime-sequences inlined runs on that device's static
216 // DMAs, which stay in it rather than in the device that holds the sequence.
217 AIE::DeviceOp loadedDevice;
218 // The latest aiex.npu.load_pdi, when it names no device in this module.
219 NpuLoadPdiOp opaqueLoad;
220 // Configures from before the latest aiex.npu.load_pdi.
221 llvm::SmallPtrSet<Operation *, 16> unloaded;
222
223 // A PDI load reconfigures the array, so the tasks configured so far no
224 // longer hold ids and allocation restarts around the new static BDs.
225 void noteLoadPdi(NpuLoadPdiOp load) {
226 loadedDevice = {};
227 if (FlatSymbolRefAttr ref = load.getDeviceRefAttr())
228 if (Operation *parent = getOperation()->getParentOp())
229 loadedDevice =
230 SymbolTable::lookupNearestSymbolFrom<AIE::DeviceOp>(parent, ref);
231 opaqueLoad = loadedDevice ? NpuLoadPdiOp() : load;
232 resetChannels.clear();
233 for (auto &[tile, tasks] : liveByTile)
234 for (DMAConfigureTaskOp task : tasks)
235 unloaded.insert(task);
236 gens.clear();
237 liveByTile.clear();
238 pendingAwaitReleases.clear();
239 freedInFlight.clear();
240 }
241
242 // Reject control flow the static path cannot lower. Constant-trip scf.for is
243 // unrolled and constant-predicate scf.if is folded before this pass, so any
244 // scf op here is runtime-valued and belongs to the dynamic EmitC path.
245 LogicalResult rejectRuntimeControlFlow(AIE::RuntimeSequenceOp seq) {
246 WalkResult wr = seq.walk([&](Operation *op) -> WalkResult {
247 if (isa<scf::ForOp, scf::IfOp, scf::WhileOp>(op)) {
248 op->emitOpError(
249 "Runtime-valued control flow in a runtime sequence is not "
250 "supported by this static BD-ID allocation pass. Either pass "
251 "only constant-valued predicates to scf.for and scf.if so "
252 "`aie-unroll-runtime-sequence-loops` can unroll/fold them, or "
253 "use the dynamic EmitC path instead.");
254 return WalkResult::interrupt();
255 }
256 return WalkResult::advance();
257 });
258 return failure(wr.wasInterrupted());
259 }
260
261 // Track the two per-channel resources a sequence can exhaust. Their failure
262 // modes are opposites: over-production is harmless for the TCT FIFO (leftover
263 // tokens cost nothing) but not for the task queue, where a push onto a full
264 // queue is dropped and its transfer never runs.
265 //
266 // Queue counting differs from token counting in three ways: every push takes
267 // a slot, not just issue_token ones; a non-token push is still retired
268 // implicitly, since in-order execution means awaiting a token drains
269 // everything queued ahead of it too; and a repeat_count does not multiply
270 // slots. rejectRuntimeControlFlow has already run, so one program-order pass
271 // over straight-line IR is exact.
272 LogicalResult verifyChannelUsage(AIE::RuntimeSequenceOp seq) {
273 using ChannelKey = DmaQueueModel::ChannelKey;
274
275 const AIETargetModel &tm =
276 seq->getParentOfType<AIE::DeviceOp>().getTargetModel();
277
278 std::map<ChannelKey, int> avail;
279 DmaQueueModel queue;
280
281 WalkResult wr = seq.walk([&](Operation *op) -> WalkResult {
282 if (auto start = dyn_cast<DMAStartTaskOp>(op)) {
283 DMAConfigureTaskOp cfg = start.getTaskOp();
284 if (!cfg)
285 return WalkResult::advance();
286 ChannelKey key = channelOf(cfg);
287 AIE::TileOp tile = cfg.getTileOp();
288 uint32_t depth = tm.getDmaTaskQueueDepth(
289 tile.getCol(), tile.getRow(), cfg.getChannel(), cfg.getDirection());
290 if (queue.wouldOverflow(key, depth))
291 guardQueueOverflow(queue, start, tm, key, depth, enforceQueueDepth);
292 bool issuesToken = start.getPushIssueToken(cfg);
293 queue.push(key, issuesToken);
294 if (issuesToken)
295 avail[key]++;
296 } else if (auto await = dyn_cast<DMAAwaitTaskOp>(op)) {
297 DMAConfigureTaskOp cfg = await.getTaskOp();
298 // A non-issue_token await is diagnosed later by aie-dma-tasks-to-npu;
299 // an unresolved task is diagnosed by the recycle path. Skip both here.
300 if (!cfg || !cfg.getIssueToken())
301 return WalkResult::advance();
302 ChannelKey key = channelOf(cfg);
303 int &tokens = avail[key];
304 if (tokens < 1) {
305 await.emitOpError(
306 "awaits a task-completion token on a channel where no "
307 "outstanding token is guaranteed to have been produced; the "
308 "runtime sequence would block here forever. Ensure a prior "
309 "issue_token dma_start_task on the same tile, direction and "
310 "channel reaches this await");
311 return WalkResult::interrupt();
312 }
313 tokens--;
314 queue.awaitToken(key);
315 } else if (auto key = otherAwaitChannel(op)) {
316 // Raw waits may also consume tokens from outside this sequence. Do not
317 // diagnose those here, but never reuse a token already consumed by one.
318 avail[*key] = std::max(0, avail[*key] - 1);
319 queue.awaitToken(*key);
320 } else if (auto key = otherTokenChannel(op)) {
321 avail[*key]++;
322 queue.push(*key, true);
323 }
324 return WalkResult::advance();
325 });
326 return failure(wr.wasInterrupted());
327 }
328
329 LogicalResult validate(AIE::RuntimeSequenceOp seq) {
330 WalkResult unplaced = seq.walk([](DMAConfigureTaskOp cfg) {
331 if (cfg.getTile().getDefiningOp<AIE::TileOp>())
332 return WalkResult::advance();
333 cfg.emitOpError("Cannot assign buffer descriptor ids to a DMA task "
334 "whose tile is not placed; run placement first.");
335 return WalkResult::interrupt();
336 });
337 if (unplaced.wasInterrupted())
338 return failure();
339 // Reject runtime control flow first, so the token-balance pass below runs
340 // on straight-line IR and needs no control-flow reasoning.
341 if (failed(rejectRuntimeControlFlow(seq)))
342 return failure();
343 if (failed(verifyChannelUsage(seq)))
344 return failure();
345 return success();
346 }
347
348 LogicalResult allocateConfigure(DMAConfigureTaskOp op) {
349 AIE::TileOp tile = op.getTileOp();
350 BdIdGenerator &gen = getGeneratorForTile(tile);
351
352 const AIETargetModel &targetModel =
353 tile->getParentOfType<AIE::DeviceOp>().getTargetModel();
354
355 // First, honor all the user-specified BD IDs.
356 WalkResult result = op.walk<WalkOrder::PreOrder>([&](AIE::DMABDOp bd_op) {
357 if (bd_op.getBdId().has_value()) {
358 if (!targetModel.isBdChannelAccessible(tile.getCol(), tile.getRow(),
359 bd_op.getBdId().value(),
360 op.getChannel())) {
361 bd_op.emitOpError("Buffer descriptor ID ")
362 << bd_op.getBdId().value() << " cannot be submitted on channel "
363 << op.getChannel() << " of tile (" << tile.getCol() << ","
364 << tile.getRow()
365 << "), which partitions its buffer descriptors by channel "
366 "parity: an even channel reaches only the low half of the "
367 "ids and an odd channel only the high half.";
368 return WalkResult::interrupt();
369 }
370 if (gen.bdIdAlreadyAssigned(bd_op.getBdId().value())) {
371 op.emitOpError("Specified buffer descriptor ID ")
372 << bd_op.getBdId().value()
373 << " is already in use. Release the earlier task first: "
374 "aiex.dma_await_task waits for hardware completion before its "
375 "BDs are reusable. aiex.dma_free_task also releases them but "
376 "does NOT wait, so it is only safe when some other "
377 "synchronization already guarantees that task has finished "
378 "(see programming_guide/section-2/section-2d/DMATasks.md).";
379 return WalkResult::interrupt();
380 }
381 checkReallocation(tile, bd_op.getBdId().value(), bd_op);
382 gen.assignBdId(bd_op.getBdId().value());
383 }
384 return WalkResult::advance();
385 });
386 if (result.wasInterrupted())
387 return failure();
388
389 // A start whose repeat count is a scratchpad parameter pushes the head
390 // BD with an update that clears the low two bits of its id.
391 AIE::DMABDOp alignedHead =
392 llvm::any_of(op->getUsers(),
393 [](Operation *user) {
394 auto start = dyn_cast<DMAStartTaskOp>(user);
395 return start && start.hasRepeatParameter();
396 })
397 ? op.getFirstBd()
398 : nullptr;
399
400 // Now allocate BD IDs for all unspecified BDs.
401 result = op.walk<WalkOrder::PreOrder>([&](AIE::DMABDOp bd_op) {
402 if (bd_op.getBdId().has_value())
403 return WalkResult::advance();
404 if (opaqueLoad) {
405 bd_op.emitOpError(
406 "needs a buffer descriptor ID, but the aiex.npu.load_pdi "
407 "before it does not name a device in this module, so the "
408 "compiler cannot tell which ids the loaded PDI's static "
409 "DMAs use. Name the loaded device with device_ref = "
410 "@<device>, or give this aie.dma_bd a bd_id that the PDI "
411 "leaves free.")
412 .attachNote(opaqueLoad.getLoc())
413 << "the PDI is loaded here";
414 return WalkResult::interrupt();
415 }
416 // channelIndex matters on a MemTile, where the AIE2 model partitions
417 // BDs by channel parity (isBdChannelAccessible: an even channel can
418 // only submit ids below 24, an odd channel only 24 and above).
419 uint32_t alignment = bd_op == alignedHead ? 4 : 1;
420 std::optional<int32_t> next_id = gen.nextBdId(op.getChannel(), alignment);
421 while (!next_id && reclaimBds && succeeded(reclaimFor(op)))
422 next_id = gen.nextBdId(op.getChannel(), alignment);
423 if (!next_id) {
424 auto diag =
425 op.emitOpError()
426 << "Too many simultaneously active buffer descriptors on tile ("
427 << tile.getCol() << "," << tile.getRow()
428 << "), which supports up to "
429 << targetModel.getNumBDsForChannel(tile.getCol(), tile.getRow(),
430 op.getChannel())
431 << ". Emit an aiex.dma_await_task to free BDs for reuse; it "
432 "waits for hardware completion, so the recycled ids are no "
433 "longer in flight. aiex.dma_free_task also recycles ids but "
434 "does NOT wait for completion -- using it before the task "
435 "has finished is a race -- so reach for it only when some "
436 "other synchronization already guarantees completion (see "
437 "programming_guide/section-2/section-2d/DMATasks.md).";
438 if (reclaimBds)
439 diag << " The compiler could not take any back either: every "
440 "task holding one is not started yet, is started again "
441 "later, or runs on a channel whose status it cannot "
442 "poll.";
443 else
444 diag << " Or let the compiler poll for finished tasks and take "
445 "their ids (reclaim-bds, aiecc --reclaim-runtime-bds), "
446 "provided no task depends on a push issued after it.";
447 return WalkResult::interrupt();
448 }
449 checkReallocation(tile, *next_id, bd_op);
450 bd_op.setBdId(next_id);
451 return WalkResult::advance();
452 });
453 if (result.wasInterrupted())
454 return failure();
455
456 liveByTile[tile].push_back(op);
457 return success();
458 }
459
460 // One push onto a channel: the task it started (null for a raw push or
461 // memcpy transfer) and whether that push issues a token. A task started more
462 // than once may issue a token on only some of its pushes.
463 struct StartedTask {
464 DMAConfigureTaskOp task;
465 bool issuesToken;
466 // Program order of the push, for picking the oldest task to reclaim.
467 uint64_t order;
468 };
469 // Pushes on a channel, in program order, whose tokens no await has consumed
470 // yet. A status poll proves a prefix of them finished, the first `finished`;
471 // their tokens, if any, still wait for an await, so they stay here until one
472 // consumes them.
473 struct ChannelPushes {
474 SmallVector<StartedTask, 8> tasks;
475 size_t finished = 0;
476 };
477 std::map<DmaQueueModel::ChannelKey, ChannelPushes> startedOnChannel;
478 uint64_t pushCount = 0;
479 // Per configure, its pushes in startedOnChannel and how many of them are not
480 // yet finished. A configure pushes onto its own channel alone.
481 llvm::DenseMap<Operation *, unsigned> startCount;
482 llvm::DenseMap<Operation *, unsigned> runningCount;
483
484 void notePush(const DmaQueueModel::ChannelKey &key, DMAConfigureTaskOp task,
485 bool issuesToken) {
486 startedOnChannel[key].tasks.push_back({task, issuesToken, pushCount++});
487 if (task) {
488 ++startCount[task];
489 ++runningCount[task];
490 }
491 }
492 // Configures whose completion an await has established.
493 llvm::SmallPtrSet<Operation *, 16> knownComplete;
494 // Configures whose completion a status poll has established. Kept apart from
495 // knownComplete so that the ids an await releases do not move: only
496 // reclaimFor reads this.
497 llvm::SmallPtrSet<Operation *, 16> pollProven;
498 // Configures whose ids reclaimFor took back.
499 llvm::SmallPtrSet<Operation *, 16> reclaimed;
500 // Configures whose ids went back to the pool by any route.
501 llvm::SmallPtrSet<Operation *, 16> releasedTasks;
502 // Configures allocated on each tile, in program order.
503 llvm::DenseMap<AIE::TileOp, SmallVector<DMAConfigureTaskOp, 16>> liveByTile;
504 // BD ids released by aiex.dma_free_task while the task could still have been
505 // in flight, keyed by tile, with the task and the free that released them.
506 struct ReleasedTask {
507 DMAConfigureTaskOp configure;
508 Operation *freeOp;
509 };
510 std::map<std::pair<int, int>, std::map<uint32_t, ReleasedTask>> freedInFlight;
511
512 static DmaQueueModel::ChannelKey channelOf(DMAConfigureTaskOp cfg) {
513 return DmaQueueModel::keyOf(cfg.getTileOp(), cfg.getDirection(),
514 cfg.getChannel());
515 }
516
517 // An await consumes the oldest outstanding token on the channel, regardless
518 // of the configure named by its SSA operand. Only that token-issuing task and
519 // the tasks queued ahead of it are known to have finished.
520 void noteAwaited(const DmaQueueModel::ChannelKey &key) {
521 ChannelPushes &channel = startedOnChannel[key];
522 auto &started = channel.tasks;
523 auto *it = llvm::find_if(
524 started, [](const StartedTask &s) { return s.issuesToken; });
525 if (it == started.end())
526 return;
527 size_t retired = std::distance(started.begin(), it) + 1;
528 for (size_t i = 0; i < retired; ++i)
529 if (DMAConfigureTaskOp task = started[i].task) {
530 --startCount[task];
531 if (i >= channel.finished)
532 --runningCount[task];
533 }
534 for (size_t i = 0; i < retired; ++i)
535 if (DMAConfigureTaskOp task = started[i].task)
536 if (startCount.lookup(task) == 0)
537 knownComplete.insert(task);
538 started.erase(started.begin(), std::next(it));
539 channel.finished -= std::min(channel.finished, retired);
540 }
541
542 // A status poll proved at most `unfinished` pushes on `key` are still queued
543 // or running. The channel runs its queue in order, so those are the newest.
544 void noteDrained(const DmaQueueModel::ChannelKey &key, size_t unfinished) {
545 ChannelPushes &channel = startedOnChannel[key];
546 if (channel.tasks.size() <= unfinished)
547 return;
548 for (size_t done = channel.tasks.size() - unfinished;
549 channel.finished < done; ++channel.finished)
550 if (DMAConfigureTaskOp task = channel.tasks[channel.finished].task)
551 if (--runningCount[task] == 0)
552 pollProven.insert(task);
553 }
554
555 // Credit a maskpoll on a channel's status register, whether the queue-depth
556 // guard, reclaimFor or the design emitted it. A poll bounding the queue size
557 // by b leaves at most b + 1 unfinished (see getDmaTaskQueueSizeMask); one
558 // that also clears the running and stall bits leaves none.
559 void notePoll(NpuMaskPollOp poll) {
560 std::optional<uint32_t> address = poll.getAbsoluteAddress();
561 std::optional<uint32_t> mask = getConstantIntOperand(poll.getMask());
562 std::optional<uint32_t> value = getConstantIntOperand(poll.getValue());
563 const AIETargetModel &tm =
564 poll->getParentOfType<AIE::DeviceOp>().getTargetModel();
565 uint32_t field = tm.getDmaTaskQueueSizeMask();
566 uint32_t idle = tm.getDmaChannelIdleMask();
567 if (!address || !mask || !value || !field)
568 return;
569 for (const auto &[key, channel] : startedOnChannel) {
570 if (tm.getDmaStatusAddress(key[0], key[1], key[3],
571 static_cast<AIE::DMAChannelDir>(key[2])) !=
572 address)
573 continue;
574 if (idle && (*mask & idle) == idle && (*value & idle) == 0)
575 return noteDrained(key, 0);
576 unsigned shift = llvm::countr_zero(field);
577 uint32_t bound = 0;
578 for (uint32_t size = 0; size <= field >> shift; ++size)
579 if (((size << shift) & *mask & field) == (*value & *mask & field))
580 bound = size;
581 return noteDrained(key, bound + 1);
582 }
583 }
584
585 // Record ids released without any completion guarantee. nextBdId scans upward
586 // from 0, so a just-freed low id is the first one handed out again -- the
587 // worst case for aliasing a BD that is still running.
588 void noteFreedInFlight(DMAConfigureTaskOp cfg, Operation *freeOp) {
589 if (!startCount.lookup(cfg))
590 return;
591 AIE::TileOp tile = cfg.getTileOp();
592 auto &ids = freedInFlight[{tile.getCol(), tile.getRow()}];
593 cfg.walk([&](AIE::DMABDOp bd) {
594 if (bd.getBdId().has_value())
595 ids[bd.getBdId().value()] = {cfg, freeOp};
596 });
597 }
598
599 // Warn where the hazard actually bites: reusing the id, not releasing it.
600 void checkReallocation(AIE::TileOp tile, uint32_t id, AIE::DMABDOp bd) {
601 if (!warnUnsafeBdReuse)
602 return;
603 auto tileIt = freedInFlight.find({tile.getCol(), tile.getRow()});
604 if (tileIt == freedInFlight.end())
605 return;
606 auto idIt = tileIt->second.find(id);
607 if (idIt == tileIt->second.end())
608 return;
609 ReleasedTask released = idIt->second;
610 tileIt->second.erase(idIt);
611 if (knownComplete.contains(released.configure) ||
612 pollProven.contains(released.configure))
613 return;
614 auto diag =
615 bd->emitWarning()
616 << "reuses buffer descriptor ID " << id << " on tile (" << tile.getCol()
617 << "," << tile.getRow()
618 << ") after it was released by an aiex.dma_free_task that had no "
619 "completion guarantee, so the DMA it belonged to may still be "
620 "running and this reprograms it underneath. Consume the outstanding "
621 "tokens on the same tile, direction and channel through a task "
622 "queued at or after this transfer -- each await consumes the oldest "
623 "token, regardless of the task it names";
624 diag.attachNote(released.freeOp->getLoc()) << "released here";
625 }
626
627 // Configures already completed by an aiex.dma_await_task. Awaiting a task
628 // returns its BD ids to the pool (like a free), but a subsequent
629 // aiex.dma_free_task of the same task is the common "wait, then release"
630 // idiom, not a double free -- so freeing an awaited task's already-returned
631 // ids is tolerated.
632 llvm::SmallPtrSet<Operation *, 8> awaitedConfigures;
633 // Awaited configures whose IDs remain live until their FIFO completion.
634 llvm::SmallPtrSet<Operation *, 8> pendingAwaitReleases;
635
636 // Return the ids of the configure's chain to the pool. `isAwait` records the
637 // configure so a later free of it is treated as a redundant release rather
638 // than a double free. Otherwise an already-freed id is a real double free (or
639 // a free of a task that was never started) and is an error.
640 LogicalResult recycle(DMAConfigureTaskOp task_op, Operation *freeOp,
641 bool isAwait) {
642 pendingAwaitReleases.erase(task_op);
643 // Those IDs may now belong to another configure. A redundant release must
644 // not inspect or change the generator's current ownership.
645 if (awaitedConfigures.contains(task_op) || reclaimed.contains(task_op) ||
646 unloaded.contains(task_op))
647 return success();
648 BdIdGenerator &gen = getGeneratorForTile(task_op.getTileOp());
649 WalkResult result = task_op.walk<WalkOrder::PreOrder>([&](AIE::DMABDOp bd) {
650 if (!bd.getBdId().has_value()) {
651 bd.emitOpError("Free called on BD chain with unassigned IDs.");
652 return WalkResult::interrupt();
653 }
654 if (gen.bdIdAlreadyAssigned(bd.getBdId().value())) {
655 gen.freeBdId(bd.getBdId().value());
656 } else {
657 freeOp->emitOpError("frees buffer descriptor ID ")
658 << bd.getBdId().value()
659 << ", which is not currently in use; it was already completed by "
660 "an "
661 "earlier aiex.dma_free_task or aiex.dma_await_task";
662 return WalkResult::interrupt();
663 }
664 return WalkResult::advance();
665 });
666 if (result.wasInterrupted())
667 return failure();
668 releasedTasks.insert(task_op);
669 if (isAwait)
670 awaitedConfigures.insert(task_op);
671 else
672 noteFreedInFlight(task_op, freeOp);
673 return success();
674 }
675
676 // Resolve a free/await to its configure. In straight-line IR the task value
677 // is defined directly by the configure op; anything else is an unlowered task
678 // op.
679 LogicalResult recycleTask(Value task, Operation *op, bool isAwait) {
680 auto cfg = dyn_cast_or_null<DMAConfigureTaskOp>(task.getDefiningOp());
681 if (!cfg) {
682 auto err = op->emitOpError(
683 "does not reference a valid configure_task operation.");
684 if (Operation *def = task.getDefiningOp()) {
685 if (isa<DMAStartBdChainOp>(def))
686 err.attachNote(def->getLoc())
687 << "Lower this operation first using the "
688 "--aie-materialize-bd-chains pass.";
689 if (isa<DMAConfigureTaskForOp>(def))
690 err.attachNote(def->getLoc())
691 << "Lower this operation first using the "
692 "--aie-substitute-shim-dma-allocations pass.";
693 }
694 return err;
695 }
696 if (!isAwait)
697 return recycle(cfg, op, /*isAwait=*/false);
698 pendingAwaitReleases.insert(cfg);
699 return recycleCompletedTasks(op);
700 }
701
702 LogicalResult recycleCompletedTasks(Operation *op) {
703 SmallVector<Operation *, 8> pending(pendingAwaitReleases.begin(),
704 pendingAwaitReleases.end());
705 for (Operation *pendingOp : pending) {
706 auto pendingCfg = cast<DMAConfigureTaskOp>(pendingOp);
707 // An earlier await may have named this task while consuming an older
708 // token. Revisit its release when a later await proves it complete.
709 if (!knownComplete.contains(pendingCfg))
710 continue;
711 // Retain ownership across any remaining starts of the same configure.
712 if (llvm::any_of(pendingCfg.getResult().getUsers(), [&](Operation *user) {
713 return isa<DMAStartTaskOp>(user) &&
714 user->getBlock() == op->getBlock() &&
715 op->isBeforeInBlock(user);
716 }))
717 continue;
718 if (failed(recycle(pendingCfg, op, /*isAwait=*/true)))
719 return failure();
720 }
721 return success();
722 }
723
724 static bool isStartedAfter(DMAConfigureTaskOp task, Operation *op) {
725 return llvm::any_of(task.getResult().getUsers(), [&](Operation *user) {
726 return isa<DMAStartTaskOp>(user) && user->getBlock() == op->getBlock() &&
727 op->isBeforeInBlock(user);
728 });
729 }
730
731 // Take back the ids of a started task that is not started again after `op`
732 // and holds an id `op`'s channel can use (on a mem tile, its parity's half).
733 // One a poll already proved finished is taken as is. Otherwise a poll before
734 // `op` proves one finished: the oldest with j >= 1 pushes queued behind it,
735 // for which Task_Queue_Size <= j - 1 is proof (as a masked equality, the
736 // largest 2^k - 1 <= j - 1), so it waits on no push not yet issued; failing
737 // that, the oldest, until its channel is idle. The poll never returns if that
738 // task's inputs come from a later push through a core, which the compiler
739 // cannot see; a design with such a dependence releases its own BDs.
740 LogicalResult reclaimFor(DMAConfigureTaskOp op) {
741 AIE::TileOp tile = op.getTileOp();
742 const AIETargetModel &tm =
743 tile->getParentOfType<AIE::DeviceOp>().getTargetModel();
744 auto isCandidate = [&](DMAConfigureTaskOp task) {
745 if (releasedTasks.contains(task) || isStartedAfter(task, op))
746 return false;
747 bool usable = false;
748 task.walk([&](AIE::DMABDOp bd) {
749 usable |= bd.getBdId() &&
750 tm.isBdChannelAccessible(tile.getCol(), tile.getRow(),
751 *bd.getBdId(), op.getChannel());
752 });
753 return usable;
754 };
755 auto take = [&](DMAConfigureTaskOp task) {
756 BdIdGenerator &gen = getGeneratorForTile(tile);
757 task.walk([&](AIE::DMABDOp bd) { gen.freeBdId(*bd.getBdId()); });
758 reclaimed.insert(task);
759 releasedTasks.insert(task);
760 pendingAwaitReleases.erase(task);
761 };
762
763 ArrayRef<DMAConfigureTaskOp> live = liveByTile[tile];
764 for (DMAConfigureTaskOp task : live)
765 if ((knownComplete.contains(task) || pollProven.contains(task)) &&
766 isCandidate(task)) {
767 take(task);
768 return success();
769 }
770
771 uint32_t field = tm.getDmaTaskQueueSizeMask();
772 uint32_t idle = tm.getDmaChannelIdleMask();
773 DMAConfigureTaskOp victim;
774 size_t victimQueuedBehind = 0;
775 uint64_t victimOrder = 0;
776 for (DMAConfigureTaskOp task : live) {
777 if (!isCandidate(task))
778 continue;
779 DmaQueueModel::ChannelKey key = channelOf(task);
780 if (!tm.getDmaStatusAddress(key[0], key[1], key[3],
781 static_cast<AIE::DMAChannelDir>(key[2])))
782 continue;
783 const ChannelPushes &channel = startedOnChannel[key];
784 ArrayRef<StartedTask> started = channel.tasks;
785 auto last =
786 llvm::find_if(llvm::reverse(started),
787 [&](const StartedTask &s) { return s.task == task; });
788 size_t queuedBehind = std::distance(started.rbegin(), last);
789 // Never started, or retired by an await and caught above.
790 if (last == started.rend() ||
791 started.size() - 1 - queuedBehind < channel.finished)
792 continue;
793 if (queuedBehind == 0 ? !idle : !field)
794 continue;
795 // Rule 1 before rule 2, then oldest.
796 if (victim && std::make_pair(queuedBehind == 0, last->order) >=
797 std::make_pair(victimQueuedBehind == 0, victimOrder))
798 continue;
799 victim = task;
800 victimQueuedBehind = queuedBehind;
801 victimOrder = last->order;
802 }
803 if (!victim)
804 return failure();
805
806 DmaQueueModel::ChannelKey key = channelOf(victim);
807 std::optional<uint32_t> status = tm.getDmaStatusAddress(
808 key[0], key[1], key[3], static_cast<AIE::DMAChannelDir>(key[2]));
809 if (!status)
810 return failure();
811 uint32_t mask = idle;
812 size_t unfinished = 0;
813 if (victimQueuedBehind > 0) {
814 unsigned shift = llvm::countr_zero(field);
815 uint32_t bound =
816 std::min<uint32_t>(llvm::bit_floor(victimQueuedBehind),
817 llvm::bit_floor((field >> shift) + 1) / 2) -
818 1;
819 mask = field & ~(bound << shift);
820 unfinished = bound + 1;
821 }
822 OpBuilder b(op);
823 auto cst = [&](uint32_t v) {
824 return arith::ConstantOp::create(b, op.getLoc(), b.getI32Type(),
825 b.getI32IntegerAttr(v))
826 .getResult();
827 };
828 Value maskValue = cst(mask);
829 Value compareValue = cst(0);
830 Value statusValue = cst(*status);
831 auto poll = NpuMaskPollOp::create(b, op.getLoc(), statusValue, compareValue,
832 maskValue,
833 /*buffer=*/nullptr, /*column=*/nullptr,
834 /*row=*/nullptr);
835 SmallVector<int32_t> ids;
836 victim.walk([&](AIE::DMABDOp bd) { ids.push_back(*bd.getBdId()); });
837 // Not poll.emitRemark(): its "see current operation" note verifies and
838 // numbers the whole device for each poll, quadratic in the sequence.
839 auto remark = mlir::emitRemark(poll.getLoc())
840 << "tile (" << tile.getCol() << "," << tile.getRow()
841 << ") has no free buffer descriptor id, so this poll waits "
842 << (victimQueuedBehind > 0
843 ? "until the queue behind an earlier task is short "
844 "enough to prove it finished"
845 : "until the channel of an earlier task is idle")
846 << " and takes back its ids ";
847 llvm::interleaveComma(ids, remark);
848 remark.attachNote(victim.getLoc()) << "the task it waits for";
849 noteDrained(key, unfinished);
850 take(victim);
851 return success();
852 }
853
854 // All of this is scoped to one runtime sequence. `gens` restarts BD id
855 // allocation per sequence, so hazard state left over from an earlier one
856 // describes ids that no longer name the same tasks. freedInFlight is the
857 // one that bites rather than merely misleads: it holds aiex.dma_free_task
858 // pointers, and those ops are erased once the sequence that owns them is
859 // done, so carrying an entry forward leaves a dangling note location.
860 void resetPerSequenceState() {
861 gens.clear();
862 awaitedConfigures.clear();
863 pendingAwaitReleases.clear();
864 startedOnChannel.clear();
865 pushCount = 0;
866 startCount.clear();
867 runningCount.clear();
868 knownComplete.clear();
869 pollProven.clear();
870 reclaimed.clear();
871 releasedTasks.clear();
872 liveByTile.clear();
873 freedInFlight.clear();
874 loadedDevice = {};
875 opaqueLoad = {};
876 unloaded.clear();
877 resetChannels.clear();
878 }
879
880 void runOnOperation() override {
881 AIE::DeviceOp device = getOperation();
882 staticBdIds.clear();
883 loopingStarts.clear();
884
885 WalkResult wr = device.walk([&](AIE::RuntimeSequenceOp seq) -> WalkResult {
886 // Skip sequences already handled by the dynamic free-list pool path
887 // (aie-lower-dynamic-bd-pool): they draw BD ids at runtime via
888 // dma_bd_pool_pop and keep their scf.for rolled, which the static
889 // straight-line allocator neither needs to touch nor can validate.
890 bool dynamicPool = false;
891 bool hasTasks = false;
892 seq.walk([&](Operation *op) {
893 dynamicPool |= isa<DMABdPoolPopOp>(op);
894 hasTasks |=
895 isa<DMAConfigureTaskOp, DMAConfigureTaskForOp, DMAStartBdChainOp,
896 DMAStartTaskOp, DMAAwaitTaskOp, DMAFreeTaskOp>(op);
897 });
898 // Already-lowered instruction-only control flow needs no BD allocation.
899 if (dynamicPool || !hasTasks)
900 return WalkResult::advance();
901
902 if (failed(validate(seq)))
903 return WalkResult::interrupt();
904 resetPerSequenceState();
905
906 // Straight-line walk. Collect frees to erase after (recycling reads the
907 // configure the free points at, so erase only once the walk is done).
908 // This allocation strategy works only for straight-line IR without
909 // branching or conditionals; the verifier of this pass ensures this is
910 // the case on the input IR.
911 SmallVector<DMAFreeTaskOp> frees;
912 WalkResult r = seq.walk([&](Operation *op) -> WalkResult {
913 if (auto cfg = dyn_cast<DMAConfigureTaskOp>(op)) {
914 if (failed(allocateConfigure(cfg)))
915 return WalkResult::interrupt();
916 } else if (auto start = dyn_cast<DMAStartTaskOp>(op)) {
917 if (DMAConfigureTaskOp cfg = start.getTaskOp()) {
918 if (unloaded.contains(cfg)) {
919 start.emitOpError(
920 "starts a task configured before an aiex.npu.load_pdi, "
921 "which reconfigured the array; its buffer descriptor ids "
922 "may now belong to the loaded device's static DMAs. "
923 "Configure the task after the load_pdi instead.");
924 return WalkResult::interrupt();
925 }
926 // Only an explicit aiex.dma_free_task releases a task that is
927 // started again (an await or a reclaim keeps the ids of one that
928 // is), so this start would push ids that may already describe
929 // another task's transfer.
930 if (releasedTasks.contains(cfg)) {
931 start.emitOpError(
932 "starts a task whose buffer descriptors an earlier "
933 "aiex.dma_free_task released; their ids may already be "
934 "reprogrammed for another task. Free the task after its "
935 "last start instead.");
936 return WalkResult::interrupt();
937 }
938 if (failed(checkNotBehindLoop(start, cfg)))
939 return WalkResult::interrupt();
940 knownComplete.erase(cfg);
941 pollProven.erase(cfg);
942 notePush(channelOf(cfg), cfg, start.getPushIssueToken(cfg));
943 }
944 } else if (auto load = dyn_cast<NpuLoadPdiOp>(op)) {
945 noteLoadPdi(load);
946 } else if (auto reset = dyn_cast<DmaChannelResetOp>(op)) {
947 resetChannels.insert(DmaQueueModel::keyOf(
948 reset.getTileOp(), reset.getDirection(), reset.getChannel()));
949 } else if (auto poll = dyn_cast<NpuMaskPollOp>(op)) {
950 notePoll(poll);
951 } else if (auto await = dyn_cast<DMAAwaitTaskOp>(op)) {
952 if (DMAConfigureTaskOp cfg = await.getTaskOp())
953 if (cfg.getIssueToken())
954 noteAwaited(channelOf(cfg));
955 if (failed(recycleTask(await.getTask(), await, /*isAwait=*/true)))
956 return WalkResult::interrupt();
957 } else if (auto key = otherAwaitChannel(op)) {
958 noteAwaited(*key);
959 if (failed(recycleCompletedTasks(op)))
960 return WalkResult::interrupt();
961 } else if (auto freeOp = dyn_cast<DMAFreeTaskOp>(op)) {
962 if (failed(recycleTask(freeOp.getTask(), freeOp, /*isAwait=*/false)))
963 return WalkResult::interrupt();
964 frees.push_back(freeOp);
965 } else if (auto key = otherTokenChannel(op)) {
966 notePush(*key, DMAConfigureTaskOp{}, true);
967 }
968 return WalkResult::advance();
969 });
970 if (r.wasInterrupted())
971 return WalkResult::interrupt();
972 for (DMAFreeTaskOp freeOp : frees)
973 freeOp.erase();
974 return WalkResult::advance();
975 });
976 if (wr.wasInterrupted())
977 return signalPassFailure();
978 }
979};
980
981} // namespace
982
983std::unique_ptr<OperationPass<AIE::DeviceOp>>
985 return std::make_unique<AIEAssignRuntimeSequenceBDIDsPass>();
986}
987
988std::unique_ptr<OperationPass<AIE::DeviceOp>>
990 const AIEAssignRuntimeSequenceBDIDsOptions &options) {
991 return std::make_unique<AIEAssignRuntimeSequenceBDIDsPass>(options);
992}
static void push(ChannelState &q, bool issuesToken)
Record a push.
static void awaitToken(ChannelState &q)
Retire what a task-completion-token await proves has drained.
std::array< int, 4 > ChannelKey
{col, row, direction, channel}.
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)
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 bool isBdChannelAccessible(int col, int row, uint32_t bd_id, int channel) const =0
Return true iff buffer descriptor bd_id on tile (col, row) can be submitted on channel channel.
virtual uint32_t getDmaTaskQueueSizeMask() const
Return the mask selecting the live task-queue occupancy field within the getDmaStatusAddress() regist...
uint32_t getNumBDsForChannel(int col, int row, int channel) const
Return the number of buffer descriptors accessible on channel channel for the tile at (col,...
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< DmaQueueModel::ChannelKey > syncChannelKey(NpuSyncOp sync)
The channel an aiex.npu.sync retires on, or nullopt when it cannot be pinned down.
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,...
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIEAssignRuntimeSequenceBDIDsPass()
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
llvm::SmallVector< uint32_t > getAssignedBdIds(DmaBody program)
const AIETargetModel & getTargetModel(mlir::Operation *op)
bool bdIdAlreadyAssigned(uint32_t bdId)
void assignBdId(uint32_t bdId)
void freeBdId(uint32_t bdId)
std::optional< uint32_t > nextBdId(int channelIndex, uint32_t alignment=1)