MLIR-AIE 1.4.5.dev
AIELowerDynamicBDPool.cpp
Go to the documentation of this file.
1//===- AIELowerDynamicBDPool.cpp --------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// The dynamic counterpart to AIEAssignRuntimeSequenceBDIDs: where that pass
9// rejects a runtime-bound scf.for, this keeps the loop rolled and draws bd_ids
10// from a runtime free-list pool per tile and BD partition. Each configure gets
11// a dma_bd_pool_pop (its SSA id feeding bd_id_val); each free gets a
12// dma_bd_pool_push. An await is only a completion sync (npu_sync), never a
13// push.
14//
15// A popped id must be reachable at its push. Since a task value can cross a
16// loop back edge and exit as a result, the ids are carried in lockstep: every
17// task iter_arg gets parallel i32 id iter_args, and a push looks up the paired
18// ids.
19//
20// A multi-BD chain pops one id per BD, in body order, all from the channel's
21// partition. AIEDMATasksToNPU links each BD's next_bd to its successor's
22// runtime id. The ids share the task's lifetime, so a free pushes all of them.
23//
24//===----------------------------------------------------------------------===//
25
30
31#include "mlir/Dialect/Arith/IR/Arith.h"
32#include "mlir/Dialect/SCF/IR/SCF.h"
33#include "mlir/Pass/Pass.h"
34#include "llvm/ADT/DenseMap.h"
35#include "llvm/ADT/DenseSet.h"
36#include "llvm/ADT/SetVector.h"
37
38namespace xilinx::AIEX {
39#define GEN_PASS_DEF_AIELOWERDYNAMICBDPOOL
40#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
41} // namespace xilinx::AIEX
42
43using namespace mlir;
44using namespace xilinx;
45using namespace xilinx::AIEX;
46
47namespace {
48
49struct AIELowerDynamicBDPoolPass
50 : xilinx::AIEX::impl::AIELowerDynamicBDPoolBase<AIELowerDynamicBDPoolPass> {
51 using Base =
52 xilinx::AIEX::impl::AIELowerDynamicBDPoolBase<AIELowerDynamicBDPoolPass>;
53 AIELowerDynamicBDPoolPass() = default;
54 AIELowerDynamicBDPoolPass(const AIELowerDynamicBDPoolOptions &options)
55 : Base(options) {}
56
57 // Maps a task value (the Index result of a configure, or any Index the carry
58 // propagates it into) to the i32 pool ids available at that same program
59 // point, one per BD of the task's chain in body order.
60 llvm::DenseMap<Value, SmallVector<Value, 2>> pairedId;
61 // The (col,row) of the tile each task's id belongs to, so a push names the
62 // right pool even when the id is a loop result rather than a direct pop.
63 llvm::DenseMap<Value, std::pair<int, int>> tileForTask;
64 // A representative (loop-invariant) configure per task, so a post-loop await
65 // on a loop result -- which has no defining configure -- can be redirected to
66 // one with the right (tile,dir,channel).
67 llvm::DenseMap<Value, DMAConfigureTaskOp> originConfigure;
68
69 // Every SSA value that carries a DMA task: configure results plus everything
70 // the task flows into (for iter_args/results, if results). Computed by
71 // forward closure from configures, so it is independent of the order in which
72 // control flow is later rewritten.
73 llvm::DenseSet<Value> taskValues;
74 // The number of ids each task value carries (its chain length). Known for
75 // every task value up front, unlike pairedId, whose entry for an scf
76 // result/iter_arg only exists once that op is grown.
77 llvm::DenseMap<Value, unsigned> taskIdCount;
78
79 // A grown scf.for/scf.if whose appended i32 yield operands are still
80 // placeholders. carried[i] is the original task position whose idCount[i]
81 // ids were appended, laid out contiguously from nOrig; the yields are wired
82 // once every position is paired.
83 struct Fixup {
84 Operation *op;
85 SmallVector<unsigned> carried;
86 SmallVector<unsigned> idCount;
87 unsigned nOrig;
88 SmallVector<Operation *> placeholders; // dead consts to erase (scf.if only)
89 };
90 SmallVector<Fixup> fixups;
91
92 // The BD ids a configure's channel can submit. Pools are keyed by this range,
93 // so channels that share a partition share a pool.
94 static std::pair<uint32_t, uint32_t> bdPartition(DMAConfigureTaskOp cfg) {
95 AIE::TileOp tile = cfg.getTileOp();
97 tile.getCol(), tile.getRow(), cfg.getChannel());
98 }
99
100 // The BDs of a configure's body, in block order: the order their ids are
101 // popped and carried.
102 static SmallVector<AIE::DMABDOp, 2> chainBds(DMAConfigureTaskOp cfg) {
103 SmallVector<AIE::DMABDOp, 2> bds;
104 cfg.walk([&](AIE::DMABDOp bd) { bds.push_back(bd); });
105 return bds;
106 }
107
108 void markTask(Value v, unsigned idCount, SmallVectorImpl<Value> &worklist) {
109 if (taskValues.insert(v).second) {
110 taskIdCount[v] = idCount;
111 worklist.push_back(v);
112 }
113 }
114
115 // Forward-propagate task-ness from every configure result through scf.for
116 // (init -> iter_arg, yield -> result) and scf.if (yield -> result). A value
117 // that is never derived from a configure (e.g. an unrelated index yielded on
118 // one branch) stays out of the set, which is what lets the divergent-yield
119 // check below reject it.
120 void computeTaskValues(AIE::RuntimeSequenceOp seq) {
121 taskValues.clear();
122 taskIdCount.clear();
123 SmallVector<Value> worklist;
124 seq.walk([&](DMAConfigureTaskOp c) {
125 markTask(c.getResult(), chainBds(c).size(), worklist);
126 });
127 while (!worklist.empty()) {
128 Value v = worklist.pop_back_val();
129 unsigned idCount = taskIdCount.lookup(v);
130 for (OpOperand &use : v.getUses()) {
131 Operation *user = use.getOwner();
132 if (auto f = dyn_cast<scf::ForOp>(user)) {
133 for (unsigned k = 0; k < f.getInitArgs().size(); ++k)
134 if (f.getInitArgs()[k] == v)
135 markTask(f.getRegionIterArg(k), idCount, worklist);
136 } else if (isa<scf::YieldOp>(user)) {
137 unsigned k = use.getOperandNumber();
138 Operation *parent = user->getParentOp();
139 if (auto pf = dyn_cast<scf::ForOp>(parent))
140 markTask(pf.getResult(k), idCount, worklist);
141 else if (auto pi = dyn_cast<scf::IfOp>(parent))
142 markTask(pi.getResult(k), idCount, worklist);
143 }
144 }
145 }
146 }
147
148 // Every position a task flows through carries one fixed number of ids, so
149 // both branches of an scf.if, and an scf.for's init and per-iteration yield,
150 // must agree on the chain length.
151 LogicalResult verifyChainLengths(AIE::RuntimeSequenceOp seq) {
152 auto agree = [&](Operation *op, Value a, Value b,
153 StringRef what) -> LogicalResult {
154 if (!taskValues.contains(a) || !taskValues.contains(b) ||
155 taskIdCount.lookup(a) == taskIdCount.lookup(b))
156 return success();
157 return op->emitOpError()
158 << what << " a " << taskIdCount.lookup(a) << "-BD chain and a "
159 << taskIdCount.lookup(b)
160 << "-BD chain at the same position; a pool-allocated task "
161 "carries one buffer descriptor id per BD, so every task "
162 "flowing through one position must have the same chain length";
163 };
164 WalkResult wr = seq.walk([&](Operation *op) -> WalkResult {
165 if (auto ifOp = dyn_cast<scf::IfOp>(op)) {
166 if (ifOp.getNumResults() == 0 || ifOp.elseBlock() == nullptr)
167 return WalkResult::advance();
168 auto thenYield = cast<scf::YieldOp>(ifOp.thenBlock()->getTerminator());
169 auto elseYield = cast<scf::YieldOp>(ifOp.elseBlock()->getTerminator());
170 for (unsigned k = 0; k < ifOp.getNumResults(); ++k)
171 if (failed(agree(ifOp, thenYield.getOperand(k),
172 elseYield.getOperand(k),
173 "yields from its branches")))
174 return WalkResult::interrupt();
175 } else if (auto forOp = dyn_cast<scf::ForOp>(op)) {
176 auto yield = cast<scf::YieldOp>(forOp.getBody()->getTerminator());
177 for (unsigned k = 0; k < forOp.getInitArgs().size(); ++k)
178 if (failed(agree(forOp, forOp.getInitArgs()[k], yield.getOperand(k),
179 "carries from its init and its yield")))
180 return WalkResult::interrupt();
181 }
182 return WalkResult::advance();
183 });
184 return failure(wr.wasInterrupted());
185 }
186
187 LogicalResult lowerConfigure(DMAConfigureTaskOp cfg) {
188 SmallVector<AIE::DMABDOp, 2> bds = chainBds(cfg);
189 if (llvm::any_of(bds, [](AIE::DMABDOp bd) { return bd.getBdIdVal(); }))
190 return success(); // already lowered
191
192 // The pool owns all id allocation here, so a hand-pinned bd_id would
193 // collide with a runtime pop. Allocation is all-pool or all-static (for a
194 // straight-line sequence), never mixed.
195 for (AIE::DMABDOp bd : bds)
196 if (bd.getBdId().has_value())
197 return bd.emitOpError(
198 "pins a buffer descriptor ID inside a runtime-bound "
199 "sequence that draws IDs from the dynamic pool; a pinned "
200 "ID would collide with a runtime-allocated one. Allocate "
201 "every BD in this sequence from the pool (drop the "
202 "bd_id), or make the sequence straight-line so the static "
203 "allocator assigns all IDs.");
204
205 AIE::TileOp tile = cfg.getTileOp();
206 auto [begin, end] = bdPartition(cfg);
207 if (bds.size() > end - begin)
208 return cfg.emitOpError()
209 << "chains " << bds.size() << " buffer descriptors, but channel "
210 << cfg.getChannel() << " of tile (" << tile.getCol() << ","
211 << tile.getRow() << ") draws from a pool of only " << end - begin
212 << " ids";
213 OpBuilder b(cfg);
214 SmallVector<Value, 2> ids;
215 for (AIE::DMABDOp bd : bds) {
216 Value bdId =
217 DMABdPoolPopOp::create(b, cfg.getLoc(), b.getI32Type(), tile.getCol(),
218 tile.getRow(), begin, end)
219 .getBdId();
220 bd.getBdIdValMutable().assign(bdId);
221 ids.push_back(bdId);
222 }
223 pairedId[cfg.getResult()] = ids;
224 return success();
225 }
226
227 // Grow an scf.for with a parallel i32 iter_arg for every task-carrying
228 // iter_arg, so the popped id can flow in lockstep with the task Index. The
229 // yields are left as placeholders (the new id block arg itself) and wired to
230 // the real per-iteration id later, once every position is paired -- a body
231 // may yield an id defined by a not-yet-grown inner op. Pairs the new
232 // iter_args/results in `pairedId` and records a Fixup.
233 scf::ForOp carryIdsThroughLoop(scf::ForOp forOp) {
234 SmallVector<unsigned> carried; // iter_arg positions carrying a task
235 SmallVector<unsigned> idCount; // ids per carried position (chain length)
236 SmallVector<Value> newInits;
237 for (unsigned k = 0; k < forOp.getInitArgs().size(); ++k) {
238 Value init = forOp.getInitArgs()[k];
239 if (!taskValues.contains(init))
240 continue;
241 SmallVector<Value, 2> ids = pairedId.lookup(init);
242 assert(ids.size() == taskIdCount.lookup(init) &&
243 "for init carries a task but its ids are not all paired");
244 carried.push_back(k);
245 idCount.push_back(ids.size());
246 newInits.append(ids.begin(), ids.end());
247 }
248 if (carried.empty())
249 return forOp;
250
251 unsigned nOrig = forOp.getInitArgs().size();
252 IRRewriter rewriter(forOp);
253 auto newYields = [&](OpBuilder &b, Location loc,
254 ArrayRef<BlockArgument> newBBArgs) {
255 // Placeholder: yield each new id iter_arg straight back. Same type,
256 // dominates the yield, and is a no-op for an identity (task-reuse) carry.
257 // wireFixups overwrites it with the real per-iteration id.
258 return SmallVector<Value>(newBBArgs.begin(), newBBArgs.end());
259 };
260
261 FailureOr<LoopLikeOpInterface> newLoop = forOp.replaceWithAdditionalYields(
262 rewriter, newInits,
263 /*replaceInitOperandUsesInLoop=*/false, newYields);
264 assert(succeeded(newLoop) && "scf.for additional-yields rewrite failed");
265 // The assert above already guards this; the checker just doesn't
266 // associate succeeded() with FailureOr's std::optional base.
267 auto newFor = cast<scf::ForOp>(
268 newLoop->getOperation()); // NOLINT(bugprone-unchecked-optional-access)
269
270 unsigned off = nOrig;
271 for (auto [i, k] : llvm::enumerate(carried)) {
272 // The rewrite gave the loop new block args/results; migrate task-ness
273 // onto them so a later-grown consumer of this loop's result still sees a
274 // task.
275 taskValues.insert(newFor.getRegionIterArg(k));
276 taskValues.insert(newFor.getResult(k));
277 taskIdCount[newFor.getRegionIterArg(k)] = idCount[i];
278 taskIdCount[newFor.getResult(k)] = idCount[i];
279 SmallVector<Value, 2> argIds, resIds;
280 for (unsigned j = 0; j < idCount[i]; ++j) {
281 argIds.push_back(newFor.getRegionIterArg(off + j));
282 resIds.push_back(newFor.getResult(off + j));
283 }
284 pairedId[newFor.getRegionIterArg(k)] = argIds;
285 pairedId[newFor.getResult(k)] = resIds;
286 off += idCount[i];
287 }
288 fixups.push_back({newFor, carried, idCount, nOrig, {}});
289 return newFor;
290 }
291
292 // Grow an scf.if with a parallel i32 result per task it yields out (its
293 // branch-local id), so a task freed after the if finds its id. Yields start
294 // as placeholders, wired later. A result carries a task iff BOTH branches
295 // yield one; a divergent yield (only one branch) is rejected -- no id to push
296 // on the other path.
297 LogicalResult carryIdsThroughIf(scf::IfOp ifOp) {
298 if (ifOp.getNumResults() == 0 || ifOp.elseBlock() == nullptr)
299 return success();
300
301 auto thenYield = cast<scf::YieldOp>(ifOp.thenBlock()->getTerminator());
302 auto elseYield = cast<scf::YieldOp>(ifOp.elseBlock()->getTerminator());
303
304 SmallVector<unsigned> carried; // result positions carrying a task
305 SmallVector<unsigned> idCount; // ids per carried position (chain length)
306 for (unsigned k = 0; k < ifOp.getNumResults(); ++k) {
307 bool thenT = taskValues.contains(thenYield.getOperand(k));
308 bool elseT = taskValues.contains(elseYield.getOperand(k));
309 if (thenT && elseT) {
310 carried.push_back(k);
311 idCount.push_back(taskIdCount.lookup(thenYield.getOperand(k)));
312 } else if (thenT || elseT)
313 return ifOp.emitOpError(
314 "yields a pool-allocated task on only one branch of an scf.if; "
315 "both "
316 "branches must yield the task (and its buffer descriptor id) so it "
317 "can be freed after the if");
318 }
319 if (carried.empty())
320 return success();
321
322 unsigned nOrig = ifOp.getNumResults();
323 OpBuilder b(ifOp);
324 SmallVector<Type> resultTypes(ifOp.getResultTypes());
325 unsigned numIds = llvm::sum_of(idCount);
326 resultTypes.append(numIds, b.getI32Type());
327
328 auto newIf =
329 scf::IfOp::create(b, ifOp.getLoc(), resultTypes, ifOp.getCondition(),
330 /*withElseRegion=*/true);
331 newIf.getThenRegion().takeBody(ifOp.getThenRegion());
332 newIf.getElseRegion().takeBody(ifOp.getElseRegion());
333
334 // Append a placeholder i32 per carried id to each branch's yield;
335 // wireFixups replaces them with the branch's real ids (which may be defined
336 // by a not-yet-grown inner op). The placeholders are dead constants erased
337 // during wiring.
338 SmallVector<Operation *> placeholders;
339 auto appendPlaceholders = [&](scf::YieldOp y) {
340 OpBuilder yb(y);
341 SmallVector<Value> ids;
342 for (unsigned j = 0; j < numIds; ++j) {
343 auto c =
344 arith::ConstantOp::create(yb, y.getLoc(), yb.getI32IntegerAttr(0));
345 placeholders.push_back(c);
346 ids.push_back(c);
347 }
348 y->insertOperands(y->getNumOperands(), ids);
349 };
350 appendPlaceholders(cast<scf::YieldOp>(newIf.thenBlock()->getTerminator()));
351 appendPlaceholders(cast<scf::YieldOp>(newIf.elseBlock()->getTerminator()));
352
353 unsigned off = nOrig;
354 for (auto [i, k] : llvm::enumerate(carried)) {
355 // Migrate task-ness onto the rebuilt result so a later-grown consumer of
356 // this if's result still sees a task.
357 taskValues.insert(newIf.getResult(k));
358 taskIdCount[newIf.getResult(k)] = idCount[i];
359 SmallVector<Value, 2> resIds;
360 for (unsigned j = 0; j < idCount[i]; ++j)
361 resIds.push_back(newIf.getResult(off + j));
362 pairedId[newIf.getResult(k)] = resIds;
363 off += idCount[i];
364 }
365 for (unsigned k = 0; k < nOrig; ++k)
366 ifOp.getResult(k).replaceAllUsesWith(newIf.getResult(k));
367 ifOp.erase();
368 fixups.push_back({newIf, carried, idCount, nOrig, placeholders});
369 return success();
370 }
371
372 // Sweep 2: with every position paired, set each grown op's appended yield
373 // operand(s) to the real id yielded at that position. Runs after all growth,
374 // so an id defined by an inner op (grown later than its enclosing op) is
375 // resolvable, and an identity carry resolves to its own iter_arg's id.
376 void wireFixups() {
377 for (Fixup &fx : fixups) {
378 SmallVector<scf::YieldOp, 2> yields;
379 if (auto forOp = dyn_cast<scf::ForOp>(fx.op)) {
380 yields.push_back(cast<scf::YieldOp>(forOp.getBody()->getTerminator()));
381 } else {
382 auto ifOp = cast<scf::IfOp>(fx.op);
383 for (Block *blk : {ifOp.thenBlock(), ifOp.elseBlock()})
384 yields.push_back(cast<scf::YieldOp>(blk->getTerminator()));
385 }
386 for (scf::YieldOp yield : yields) {
387 unsigned off = fx.nOrig;
388 for (auto [i, k] : llvm::enumerate(fx.carried)) {
389 SmallVector<Value, 2> ids = pairedId.lookup(yield.getOperand(k));
390 assert(ids.size() == fx.idCount[i] &&
391 "carried task yields the wrong number of ids");
392 for (auto [j, id] : llvm::enumerate(ids))
393 yield.setOperand(off + j, id);
394 off += fx.idCount[i];
395 }
396 }
397 }
398 for (Fixup &fx : fixups)
399 for (Operation *p : fx.placeholders)
400 p->erase();
401 }
402
403 // Sweep 3: over the final IR, give every task value its tile (for the push)
404 // and a representative origin configure (for an await that names a loop
405 // result with no defining configure -- a loop's pre-loop init, which
406 // dominates a post-loop await). Order-independent forward closure:
407 // - scf.for: the init's tile/origin flow to the iter_arg AND the result, so
408 // a result takes the loop-invariant init configure (which dominates a
409 // post-loop await), not the per-iteration body configure.
410 // - scf.if: the then-branch yield's tile/origin flow to the result.
411 // A push only reads the tile, so both branches must agree; a genuinely
412 // cross-tile scf.if is rejected, and so is a for-loop whose per-iteration
413 // reconfiguration disagrees with its init.
414 LogicalResult computeMetadata(AIE::RuntimeSequenceOp seq) {
415 tileForTask.clear();
416 originConfigure.clear();
417 SmallVector<Value> worklist;
418 auto setMeta = [&](Value v, std::pair<int, int> tile,
419 DMAConfigureTaskOp origin) {
420 if (tileForTask.count(v))
421 return;
422 tileForTask[v] = tile;
423 originConfigure[v] = origin;
424 worklist.push_back(v);
425 };
426 seq.walk([&](DMAConfigureTaskOp c) {
427 AIE::TileOp t = c.getTileOp();
428 setMeta(c.getResult(), {t.getCol(), t.getRow()}, c);
429 });
430 while (!worklist.empty()) {
431 Value v = worklist.pop_back_val();
432 auto tile = tileForTask[v];
433 DMAConfigureTaskOp origin = originConfigure[v];
434 for (OpOperand &use : v.getUses()) {
435 Operation *user = use.getOwner();
436 if (auto f = dyn_cast<scf::ForOp>(user)) {
437 for (unsigned k = 0; k < f.getInitArgs().size(); ++k)
438 if (f.getInitArgs()[k] == v) {
439 setMeta(f.getRegionIterArg(k), tile, origin);
440 setMeta(f.getResult(k), tile, origin);
441 }
442 } else if (isa<scf::YieldOp>(user)) {
443 auto ifOp = dyn_cast<scf::IfOp>(user->getParentOp());
444 if (ifOp && user->getBlock() == ifOp.thenBlock())
445 setMeta(ifOp.getResult(use.getOperandNumber()), tile, origin);
446 }
447 }
448 }
449 // A task carried out of an scf.if and freed after it resolves to one BD id
450 // and one push, so both branches must agree on the full physical channel
451 // (tile,dir,channel) -- otherwise the single push would be ambiguous.
452 WalkResult wr = seq.walk([&](scf::IfOp ifOp) -> WalkResult {
453 if (ifOp.getNumResults() == 0 || ifOp.elseBlock() == nullptr)
454 return WalkResult::advance();
455 auto thenYield = cast<scf::YieldOp>(ifOp.thenBlock()->getTerminator());
456 auto elseYield = cast<scf::YieldOp>(ifOp.elseBlock()->getTerminator());
457 for (unsigned k = 0; k < ifOp.getNumResults(); ++k)
458 if (failed(checkChannelAgreement(
459 ifOp, thenYield.getOperand(k), elseYield.getOperand(k),
460 "yields tasks on different physical channels "
461 "(tile/direction/channel) from its two branches at "
462 "the same result; a pooled buffer descriptor id and "
463 "its completion sync belong to one channel")))
464 return WalkResult::interrupt();
465 return WalkResult::advance();
466 });
467 if (wr.wasInterrupted())
468 return failure();
469
470 // A task carried around an scf.for must likewise agree on the physical
471 // channel between the loop's init and its per-iteration reconfiguration
472 // -- the push after the loop uses the loop-invariant init's tile
473 // (computeMetadata never adopts the body's), and an await reachable
474 // through the loop (resolveConfigureThroughCF in AIEDMATasksToNPU) may
475 // resolve to either one.
476 WalkResult forWr = seq.walk([&](scf::ForOp forOp) -> WalkResult {
477 auto yield = cast<scf::YieldOp>(forOp.getBody()->getTerminator());
478 for (unsigned k = 0; k < forOp.getInitArgs().size(); ++k)
479 if (failed(checkChannelAgreement(
480 forOp, forOp.getInitArgs()[k], yield.getOperand(k),
481 "carries a task on different physical channels "
482 "(tile/direction/channel) between its init and its "
483 "per-iteration reconfiguration; a pooled buffer "
484 "descriptor id and its completion sync belong to one "
485 "channel")))
486 return WalkResult::interrupt();
487 return WalkResult::advance();
488 });
489 return forWr.wasInterrupted() ? failure() : success();
490 }
491
492 // If both `a` and `b` resolve (via originConfigure) to a configure, and
493 // those configures target different physical channels, emit `msg` on `op`
494 // and return failure. Shared by the scf.if and scf.for channel-agreement
495 // checks in computeMetadata -- only the pair of values being compared
496 // differs between them.
497 LogicalResult checkChannelAgreement(Operation *op, Value a, Value b,
498 StringRef msg) {
499 DMAConfigureTaskOp originA = originConfigure.lookup(a);
500 DMAConfigureTaskOp originB = originConfigure.lookup(b);
501 if (originA && originB && channelKey(originA) != channelKey(originB)) {
502 op->emitOpError(msg);
503 return failure();
504 }
505 return success();
506 }
507
508 static DmaQueueModel::ChannelKey channelKey(DMAConfigureTaskOp cfg) {
509 return DmaQueueModel::keyOf(cfg.getTileOp(), cfg.getDirection(),
510 cfg.getChannel());
511 }
512
513 // Every value that may carry `task`'s BD id after it: the region arguments
514 // and results it is forwarded to, including the next iteration of a loop
515 // that yields it. The forward counterpart of getReachableConfigures.
516 static llvm::SetVector<Value> aliasesAfter(Value task) {
517 llvm::SetVector<Value> aliases;
518 aliases.insert(task);
519 for (unsigned i = 0; i < aliases.size(); ++i) {
520 for (OpOperand &use : aliases[i].getUses()) {
521 Operation *user = use.getOwner();
522 auto branch = dyn_cast<RegionBranchOpInterface>(user);
523 if (!branch && user->hasTrait<OpTrait::IsTerminator>())
524 branch =
525 dyn_cast_or_null<RegionBranchOpInterface>(user->getParentOp());
526 if (!branch)
527 continue;
528 RegionBranchSuccessorMapping mapping;
529 branch.getSuccessorOperandInputMapping(mapping);
530 aliases.insert_range(mapping.lookup(&use));
531 }
532 }
533 return aliases;
534 }
535
536 // A start that can run after `freeOp` pushes an id the pool may already
537 // have handed to another task: one later in the block the two share, or one
538 // in a loop around both whose next iteration still names the freed id.
539 static LogicalResult verifyNoStartAfterFree(Value task, Operation *freeOp) {
540 llvm::SetVector<Value> aliases = aliasesAfter(task);
541 auto startsAfterFree = [&](DMAStartTaskOp start) {
542 for (Block *block = freeOp->getBlock(); block;
543 block = block->getParentOp()->getBlock()) {
544 Operation *startAt = block->findAncestorOpInBlock(*start);
545 if (!startAt)
546 continue;
547 Operation *freeAt = block->findAncestorOpInBlock(*freeOp);
548 if (freeAt != startAt && freeAt->isBeforeInBlock(startAt))
549 return true;
550 break;
551 }
552 Value started = start.getTask();
553 for (auto loop = freeOp->getParentOfType<scf::ForOp>(); loop;
554 loop = loop->getParentOfType<scf::ForOp>()) {
555 if (!loop->isAncestor(start))
556 continue;
557 if (!loop.getBody()->getParent()->isAncestor(started.getParentRegion()))
558 return true;
559 auto arg = dyn_cast<BlockArgument>(started);
560 if (arg && arg.getOwner() == loop.getBody() &&
561 arg.getArgNumber() >= loop.getNumInductionVars()) {
562 unsigned k = arg.getArgNumber() - loop.getNumInductionVars();
563 if (aliases.contains(loop.getYieldedValues()[k]))
564 return true;
565 }
566 }
567 return false;
568 };
569 for (Value alias : aliases)
570 for (Operation *user : alias.getUsers()) {
571 auto start = dyn_cast<DMAStartTaskOp>(user);
572 if (!start || !startsAfterFree(start))
573 continue;
574 auto diag = start.emitOpError(
575 "starts a task whose buffer descriptor ID was already returned "
576 "to the runtime pool; the pool may have handed it to another "
577 "task. Free the task after its last start instead.");
578 diag.attachNote(freeOp->getLoc()) << "returned here";
579 return diag;
580 }
581 return success();
582 }
583
584 LogicalResult lowerRelease(Value task, Operation *op) {
585 SmallVector<Value, 2> ids = pairedId.lookup(task);
586 if (ids.empty())
587 return op->emitOpError(
588 "does not resolve to a task allocated from the runtime pool; cannot "
589 "return its buffer descriptor ID");
590 if (failed(verifyNoStartAfterFree(task, op)))
591 return failure();
592 auto tile = tileForTask.lookup(task);
593 auto [begin, end] = bdPartition(originConfigure.lookup(task));
594 OpBuilder b(op);
595 for (Value id : ids)
596 DMABdPoolPushOp::create(b, op->getLoc(), tile.first, tile.second, begin,
597 end, id);
598 return success();
599 }
600
601 // Guard pushes that can land on a full task queue, and (opt-in) warn at a
602 // dma_free_task whose own push is not yet proven complete. Must run before
603 // frees are erased below -- it is the only point where this pass still has
604 // both the dma_free_task ops and the origin-configure metadata they resolve
605 // through.
606 //
607 // A sequence lowered here keeps its scf.for rolled, so the static allocator
608 // skips it. Guard while task metadata is available; aie-dma-to-npu later
609 // checks the combined pushes from tasks, memcpy and channel rearm operations.
610 void guardQueueDepth(AIE::RuntimeSequenceOp seq) {
611 const AIE::AIETargetModel &tm =
612 seq->getParentOfType<AIE::DeviceOp>().getTargetModel();
613
614 auto effectOf = [&](Operation *op) -> QueueEffect {
615 if (auto push = dyn_cast<NpuPushQueueOp>(op))
616 return QueueEffect::push({static_cast<int>(push.getColumn()),
617 static_cast<int>(push.getRow()),
618 static_cast<int>(push.getDirection()),
619 static_cast<int>(push.getChannel())},
620 push.getIssueToken());
621 if (auto sync = dyn_cast<NpuSyncOp>(op)) {
622 if (auto key = syncChannelKey(sync))
623 return QueueEffect::await(*key);
624 return {};
625 }
626 if (auto copy = dyn_cast<NpuDmaMemcpyNdOp>(op)) {
627 auto allocation = AIE::ShimDMAAllocationOp::getForSymbol(
628 seq->getParentOfType<AIE::DeviceOp>(),
629 copy.getMetadata().getRootReference());
630 if (allocation)
631 if (AIE::TileOp tile = allocation.getTileOp())
632 return QueueEffect::push(
633 DmaQueueModel::keyOf(tile, allocation.getChannelDir(),
634 allocation.getChannelIndex()),
635 copy.getIssueToken() ||
636 allocation.getChannelDir() == AIE::DMAChannelDir::S2MM);
637 return {};
638 }
639 if (auto wait = dyn_cast<NpuDmaWaitOp>(op)) {
640 auto allocation = AIE::ShimDMAAllocationOp::getForSymbol(
641 seq->getParentOfType<AIE::DeviceOp>(), wait.getSymbol());
642 if (allocation)
643 if (AIE::TileOp tile = allocation.getTileOp())
644 return QueueEffect::await(
645 DmaQueueModel::keyOf(tile, allocation.getChannelDir(),
646 allocation.getChannelIndex()));
647 return {};
648 }
649 DMAConfigureTaskOp cfg;
650 Value task;
651 bool isConfigure = false;
652 bool isPush = false;
653 bool issuesToken = false;
654 if (auto start = dyn_cast<DMAStartTaskOp>(op)) {
655 task = start.getTask();
656 cfg = originConfigure.lookup(task);
657 isPush = true;
658 issuesToken = cfg && start.getPushIssueToken(cfg);
659 } else if (auto configure = dyn_cast<DMAConfigureTaskOp>(op)) {
660 if (!warnUnsafeBdReuse)
661 return {};
662 cfg = configure;
663 task = configure.getResult();
664 isConfigure = true;
665 } else if (auto await = dyn_cast<DMAAwaitTaskOp>(op)) {
666 cfg = originConfigure.lookup(await.getTask());
667 // Only a token-issuing await retires anything.
668 if (cfg && !cfg.getIssueToken())
669 return {};
670 } else if (auto freeOp = dyn_cast<DMAFreeTaskOp>(op)) {
671 if (!warnUnsafeBdReuse)
672 return {};
673 cfg = originConfigure.lookup(freeOp.getTask());
674 if (!cfg)
675 return {};
676 AIE::TileOp tile = cfg.tryGetTileOp();
677 if (!tile)
678 return {};
679 return QueueEffect::free(
680 DmaQueueModel::keyOf(tile, cfg.getDirection(), cfg.getChannel()),
681 freeOp.getTask());
682 } else {
683 return {};
684 }
685 if (!cfg)
686 return {};
687 AIE::TileOp tile = cfg.tryGetTileOp();
688 if (!tile)
689 return {};
691 DmaQueueModel::keyOf(tile, cfg.getDirection(), cfg.getChannel());
692 if (isConfigure)
693 return QueueEffect::configure(key, task);
694 return isPush ? QueueEffect::push(key, issuesToken,
695 warnUnsafeBdReuse ? task : Value{})
696 : QueueEffect::await(key);
697 };
698
699 DmaQueueModel queue;
700 guardSequenceQueueDepth(seq.getBody(), queue, tm, enforceQueueDepth,
701 warnUnsafeBdReuse, effectOf);
702 }
703
704 void runOnOperation() override {
705 AIE::DeviceOp device = getOperation();
706
707 WalkResult wr = device.walk([&](AIE::RuntimeSequenceOp seq) -> WalkResult {
708 bool hasRuntimeCF = false;
709 seq.walk([&](Operation *op) {
710 if (isa<scf::ForOp, scf::IfOp, scf::WhileOp>(op))
711 hasRuntimeCF = true;
712 });
713 if (!hasRuntimeCF)
714 return WalkResult::advance();
715
716 // Idempotency: a sequence already carrying pool pops has been lowered by
717 // a prior run of this pass. Its awaits may have dropped their task
718 // operand (sync_* attrs) and its scf ops already carry i32 ids, so
719 // re-running the carry logic would misfire. Skip it.
720 bool alreadyLowered = false;
721 seq.walk([&](DMABdPoolPopOp) { alreadyLowered = true; });
722 if (alreadyLowered)
723 return WalkResult::advance();
724
725 // scf.while has no bounded trip count, so a BD id popped in its body has
726 // no place to be pushed back deterministically; only scf.for and scf.if
727 // are supported.
728 WalkResult whileCheck = seq.walk([&](scf::WhileOp w) {
729 w.emitOpError("scf.while in a runtime sequence is not supported by the "
730 "dynamic BD pool path; only scf.for and scf.if are "
731 "supported");
732 return WalkResult::interrupt();
733 });
734 if (whileCheck.wasInterrupted())
735 return WalkResult::interrupt();
736
737 pairedId.clear();
738 tileForTask.clear();
739 originConfigure.clear();
740 fixups.clear();
741
742 // 1. Every configure pops an id (program order: a pop dominates every use
743 // of its id).
744 WalkResult r = seq.walk([&](DMAConfigureTaskOp cfg) -> WalkResult {
745 return failed(lowerConfigure(cfg)) ? WalkResult::interrupt()
746 : WalkResult::advance();
747 });
748 if (r.wasInterrupted())
749 return WalkResult::interrupt();
750
751 // 2. Thread the id alongside the task everywhere the task flows. The task
752 // Index already threads (it is valid IR); shadowing every position it
753 // occupies with a parallel i32 makes the id in-scope at every push.
754 // a. Task closure: which SSA values carry a task (order-independent).
755 // b. Grow structure outermost-first (pre-order): an enclosing op's new
756 // iter_arg/result exists before an inner op yields through it. A
757 // divergent scf.if is rejected here.
758 // c. Wire the placeholder yields now that every position is paired --
759 // handles both siblings (producer grown before consumer) and
760 // parent-defines/child-consumes (inner id resolvable after growth).
761 computeTaskValues(seq);
762 if (failed(verifyChainLengths(seq)))
763 return WalkResult::interrupt();
764 SmallVector<Operation *> cf;
765 seq.walk<WalkOrder::PreOrder>([&](Operation *o) {
766 if (isa<scf::ForOp, scf::IfOp>(o))
767 cf.push_back(o);
768 });
769 for (Operation *o : cf) {
770 if (auto f = dyn_cast<scf::ForOp>(o)) {
771 carryIdsThroughLoop(f);
772 } else if (failed(carryIdsThroughIf(cast<scf::IfOp>(o)))) {
773 return WalkResult::interrupt();
774 }
775 }
776 wireFixups();
777
778 // 3. Metadata over the final IR: each task's tile (for the push) and a
779 // dominating origin configure (for an await on a loop/if result).
780 if (failed(computeMetadata(seq)))
781 return WalkResult::interrupt();
782
783 // 4. Return ids to the pool. A free is the ONLY push: it returns the id
784 // to the free-list. An await is a TCT sync (npu_sync), never a release
785 // -- it emits no push. In the rolled pool one physical id is live
786 // across iterations, so the static path's "await = release" (safe only
787 // because unrolling gives each await a distinct id) would push the
788 // same live id once per iteration and corrupt the free-list. Leaving a
789 // task unfreed is valid: the pool is stack-local and re-initialized
790 // every invocation, so an unfreed id merely holds a slot for the rest
791 // of this one run (the await-only idiom, live BDs <= pool size).
792 //
793 // The await keeps its natural SSA operand -- the task value it names,
794 // whether that is a direct configure, a loop result, or an scf.if
795 // result (the phi of the task in flight, whichever branch ran). The
796 // npu_sync lowering (AIEDMATasksToNPU) walks that value to the
797 // physical channel; both branches of an scf.if were verified to agree
798 // on it (computeMetadata). This preserves the sync's data dependence
799 // on the task rather than breaking it.
800 SmallVector<Operation *> toErase;
801 r = seq.walk([&](Operation *op) -> WalkResult {
802 if (auto freeOp = dyn_cast<DMAFreeTaskOp>(op)) {
803 if (failed(lowerRelease(freeOp.getTask(), freeOp)))
804 return WalkResult::interrupt();
805 toErase.push_back(freeOp);
806 }
807 return WalkResult::advance();
808 });
809 if (r.wasInterrupted())
810 return WalkResult::interrupt();
811 // Only once this sequence has lowered cleanly: guarding or reporting a
812 // queue on a design that is already being rejected just buries the
813 // error. Guard before erasing the frees above -- it still needs to see
814 // them, resolved through originConfigure, to check reuse safety.
815 guardQueueDepth(seq);
816 for (Operation *op : toErase)
817 op->erase();
818 return WalkResult::advance();
819 });
820 if (wr.wasInterrupted())
821 return signalPassFailure();
822 }
823};
824
825} // namespace
826
827std::unique_ptr<OperationPass<AIE::DeviceOp>>
829 return std::make_unique<AIELowerDynamicBDPoolPass>();
830}
831
832std::unique_ptr<OperationPass<AIE::DeviceOp>>
834 const AIELowerDynamicBDPoolOptions &options) {
835 return std::make_unique<AIELowerDynamicBDPoolPass>(options);
836}
std::array< int, 4 > ChannelKey
{col, row, direction, channel}.
static ChannelKey keyOf(AIE::TileOp tile, AIE::DMAChannelDir direction, int64_t channel)
std::pair< uint32_t, uint32_t > getBdIdRangeForChannel(int col, int row, int channel) const
Return the half-open range [first, last) of buffer descriptor ids that channel channel of the tile at...
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIELowerDynamicBDPoolPass()
std::optional< DmaQueueModel::ChannelKey > syncChannelKey(NpuSyncOp sync)
The channel an aiex.npu.sync retires on, or nullopt when it cannot be pinned down.
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...
const AIETargetModel & getTargetModel(mlir::Operation *op)
static QueueEffect await(DmaQueueModel::ChannelKey key)
static QueueEffect configure(DmaQueueModel::ChannelKey key, mlir::Value task)
static QueueEffect free(DmaQueueModel::ChannelKey key, mlir::Value task)
static QueueEffect push(DmaQueueModel::ChannelKey key, bool issuesToken, mlir::Value task={})