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