MLIR-AIE 1.4.5.dev
AIEDecomposeLargeDmaBd.cpp
Go to the documentation of this file.
1//===- AIEDecomposeLargeDmaBd.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// Decomposes oversized non-contiguous aiex.npu.dma_memcpy_nd ops and task-path
9// aie.dma_bd ops into one or more hardware-legal ND transfers before
10// aie-dma-to-npu / aie-dma-tasks-to-npu lowering.
11//
12//===----------------------------------------------------------------------===//
13
19
20#include "mlir/Dialect/MemRef/IR/MemRef.h"
21#include "mlir/Dialect/SCF/IR/SCF.h"
22#include "mlir/Dialect/Utils/StaticValueUtils.h"
23#include "mlir/IR/PatternMatch.h"
24#include "mlir/Interfaces/SideEffectInterfaces.h"
25#include "mlir/Pass/Pass.h"
26#include "llvm/ADT/DenseSet.h"
27#include "llvm/ADT/SetVector.h"
28#include "llvm/Support/raw_ostream.h"
29
30#include <limits>
31#include <numeric>
32#include <set>
33#include <string>
34
35namespace xilinx::AIEX {
36#define GEN_PASS_DEF_AIEDECOMPOSELARGEDMABD
37#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
38} // namespace xilinx::AIEX
39
40using namespace mlir;
41using namespace xilinx;
42using namespace xilinx::AIEX;
43
44namespace {
45
46// `op`'s pattern, innermost first, when its offsets, sizes and strides are
47// constant.
48static std::optional<NdDmaPattern> constantPattern(NpuDmaMemcpyNdOp op) {
49 std::optional<SmallVector<int64_t>> offsets =
50 getConstantIntValues(op.getMixedOffsets());
51 std::optional<SmallVector<int64_t>> sizes =
52 getConstantIntValues(op.getMixedSizes());
53 std::optional<SmallVector<int64_t>> strides =
54 getConstantIntValues(op.getMixedStrides());
55 if (!offsets || !sizes || !strides)
56 return std::nullopt;
57 NdDmaPattern pattern;
58 pattern.offsets.assign(offsets->rbegin(), offsets->rend());
59 pattern.sizes.assign(sizes->rbegin(), sizes->rend());
60 pattern.strides.assign(strides->rbegin(), strides->rend());
61 return pattern;
62}
63
64static SmallVector<int64_t, 4> toOuter(ArrayRef<int64_t> inner) {
65 return llvm::map_to_vector(llvm::reverse(inner), [](int64_t v) { return v; });
66}
67
68static int64_t flatOffsetFromPattern(int64_t baseFlatOffset,
69 const NdDmaPattern &pattern) {
70 int64_t flat = baseFlatOffset + pattern.baseOffset;
71 for (unsigned k = 0; k < 4; ++k)
72 flat += pattern.offsets[k] * pattern.strides[k];
73 return flat;
74}
75
76static int64_t lenFromInnermost3(ArrayRef<int64_t> sizesInnermostFirst) {
77 int64_t len = 1;
78 for (unsigned i = 0; i < 3; ++i)
79 len *= sizesInnermostFirst[i];
80 return len;
81}
82
83static AIE::BDDimLayoutArrayAttr outerDimsAttr(MLIRContext *ctx,
84 ArrayRef<int64_t> outerSizes,
85 ArrayRef<int64_t> outerStrides) {
86 SmallVector<AIE::BDDimLayoutAttr> dims;
87 dims.reserve(outerSizes.size());
88 for (auto [s, t] : llvm::zip(outerSizes, outerStrides))
89 dims.push_back(AIE::BDDimLayoutAttr::get(ctx, static_cast<uint32_t>(s),
90 static_cast<uint32_t>(t)));
91 return AIE::BDDimLayoutArrayAttr::get(ctx, dims);
92}
93
94static void updateTaskBdInPlace(AIE::DMABDOp bd, int32_t offset, int32_t len,
95 ArrayRef<int64_t> outerSizes,
96 ArrayRef<int64_t> outerStrides) {
97 bd.getOffsetMutable().clear();
98 bd.setStaticOffset(offset);
99 bd.getLenMutable().clear();
100 bd.setStaticLen(len);
101 bd.getSizesMutable().clear();
102 bd.getStridesMutable().clear();
103 bd.setStaticSizes(DenseI64ArrayAttr::get(bd.getContext(), outerSizes));
104 bd.setStaticStrides(DenseI64ArrayAttr::get(bd.getContext(), outerStrides));
105}
106
107static AIE::DMABDOp createTaskBd(RewriterBase &rewriter, Location loc,
108 AIE::DMABDOp tmpl, int32_t offset, int32_t len,
109 ArrayRef<int64_t> outerSizes,
110 ArrayRef<int64_t> outerStrides) {
111 auto dims = outerDimsAttr(rewriter.getContext(), outerSizes, outerStrides);
112 auto bd =
113 AIE::DMABDOp::create(rewriter, loc, tmpl.getBuffer(), offset, len, dims);
114 if (tmpl.getPacketAttr())
115 bd.setPacketAttr(tmpl.getPacketAttr());
116 if (tmpl.getBurstLengthAttr())
117 bd.setBurstLengthAttr(tmpl.getBurstLengthAttr());
118 if (tmpl.getAxcacheAttr())
119 bd.setAxcacheAttr(tmpl.getAxcacheAttr());
120 if (tmpl.getOffsetParameterAttr())
121 bd.setOffsetParameterAttr(tmpl.getOffsetParameterAttr());
122 // out_of_order_id is not copied because slicing an OoO BD is rejected above.
123 return bd;
124}
125
126// How many executions one pass over a task BD's pattern takes: one per index
127// of its iteration dimensions, all of them past d2.
128static int64_t iterationCount(const NdDmaPattern &pattern) {
129 int64_t count = 1;
130 for (int64_t size : llvm::drop_begin(pattern.sizes, 3))
131 count *= size;
132 return count;
133}
134
135// The factor by which decomposition scaled the executions of a pass, num/den
136// in lowest terms: a pass over the original takes `iterations` executions,
137// and over `after` one per index of its iteration dimension. Below one, the
138// executions merged, `after`'s inner dimensions taking in one the original
139// iterated over.
140struct IterationScale {
141 int64_t num = 1;
142 int64_t den = 1;
143
144 bool isOne() const { return num == den; }
145 // Whether `runs` executions of the original are whole executions of the
146 // rewrite.
147 bool divides(int64_t runs) const { return runs % den == 0; }
148 int64_t apply(int64_t runs) const { return runs / den * num; }
149 std::string str() const {
150 return den == 1 ? std::to_string(num)
151 : std::to_string(num) + "/" + std::to_string(den);
152 }
153};
154
155static IterationScale iterationScale(int64_t iterations,
156 const NdDmaPattern &after) {
157 int64_t common = std::gcd(after.sizes[3], iterations);
158 return {after.sizes[3] / common, iterations / common};
159}
160
161static unsigned countTaskBds(DMAConfigureTaskLike taskOp) {
162 unsigned n = 0;
163 taskOp->walk([&](AIE::DMABDOp) { ++n; });
164 return n;
165}
166
167static bool isUnderRuntimeControlFlow(AIE::DMABDOp op) {
168 auto seq = op->getParentOfType<AIE::RuntimeSequenceOp>();
169 if (!seq)
170 return false;
171 for (Operation *parent = op->getParentOp(); parent && parent != seq;
172 parent = parent->getParentOp()) {
173 if (isa<scf::ForOp, scf::WhileOp, scf::IfOp>(parent))
174 return true;
175 }
176 return false;
177}
178
179static std::optional<std::pair<AIE::TileOp, DMAConfigureTaskLike>>
180resolveTaskAndTile(AIE::DMABDOp op) {
181 if (auto cfg = op->getParentOfType<DMAConfigureTaskOp>()) {
182 // Decomposition is a shape rewrite, so an unplaced tile is not an error
183 // here: skip the descriptor until placement has run.
184 AIE::TileOp tile = cfg.tryGetTileOp();
185 if (!tile)
186 return std::nullopt;
187 return std::make_pair(tile, cast<DMAConfigureTaskLike>(*cfg));
188 }
189 if (auto cfgFor = op->getParentOfType<DMAConfigureTaskForOp>()) {
190 AIE::DeviceOp dev = op->getParentOfType<AIE::DeviceOp>();
191 if (!dev)
192 return std::nullopt;
193 auto allocOp = AIE::ShimDMAAllocationOp::getForSymbol(
194 dev, cfgFor.getAlloc().getRootReference());
195 if (!allocOp)
196 return std::nullopt;
197 AIE::TileOp tile = allocOp.getTileOp();
198 if (!tile)
199 return std::nullopt;
200 return std::make_pair(tile, cast<DMAConfigureTaskLike>(*cfgFor));
201 }
202 return std::nullopt;
203}
204
205static NpuDmaMemcpyNdOp createDecomposedOp(RewriterBase &rewriter,
206 NpuDmaMemcpyNdOp op,
207 const NdDmaPattern &pattern,
208 int64_t id, bool issueToken) {
209 assert(pattern.baseOffset == 0 &&
210 "a memcpy pattern has no peeled dimensions");
211 auto newOp = NpuDmaMemcpyNdOp::create(
212 rewriter, op.getLoc(), op, toOuter(pattern.offsets),
213 toOuter(pattern.sizes), toOuter(pattern.strides));
214 newOp.setIdAttr(rewriter.getI64IntegerAttr(id));
215 newOp.setIssueTokenAttr(rewriter.getBoolAttr(issueToken));
216 return newOp;
217}
218
219static int64_t allocateNextId(NpuDmaMemcpyNdOp op, int64_t startId,
220 llvm::DenseSet<int64_t> &usedIds) {
221 int64_t id = startId;
222 while (usedIds.contains(id))
223 ++id;
224 usedIds.insert(id);
225 return id;
226}
227
228// The starts splitIntoTasks emits, until orderSlices has placed them.
229struct Slices {
230 struct Start {
231 int64_t group, index, count;
232 };
233 llvm::DenseMap<Operation *, Start> starts;
234 // The configures of the slices after the first, which orderSlices moves up
235 // to their first start.
236 llvm::DenseSet<Operation *> lateTasks;
237 llvm::SetVector<Block *> blocks;
238 int64_t nextGroup = 0;
239};
240
241// Why the slices of `bd` cannot go out as one task each, or nullopt if they
242// can. Every use of the task has to be one splitIntoTasks rewrites, and every
243// start has to run whole passes over the pattern, a pass being one execution
244// per index of the `iterations`-long iteration dimension: the slices of a pass
245// are issued in turn, so a partial pass has no slice boundary to stop at.
246static std::optional<std::string>
247whyNotSeparateTasks(AIE::DMABDOp bd, DMAConfigureTaskLike taskOp,
248 int64_t iterations) {
249 std::string why;
250 llvm::raw_string_ostream os(why);
251 if (bd.getBdIdVal()) {
252 os << "the descriptor's bd_id is a runtime value";
253 return why;
254 }
255 if (taskOp.getRepeatCountVal()) {
256 os << "the task's repeat count is a runtime value";
257 return why;
258 }
259 for (Operation *user : taskOp->getResult(0).getUsers()) {
260 if (!isa<DMAStartTaskOp, DMAAwaitTaskOp, DMAFreeTaskOp>(user)) {
261 os << "the task is used by '" << user->getName() << "'";
262 return why;
263 }
264 if (user->getBlock() != taskOp->getBlock()) {
265 os << "the task is used outside the block that configures it";
266 return why;
267 }
268 auto start = dyn_cast<DMAStartTaskOp>(user);
269 if (!start)
270 continue;
271 if (start.getRepeatCountVal()) {
272 os << "a start's repeat count is a runtime value";
273 return why;
274 }
275 if (start.hasRepeatParameter()) {
276 os << "a start's repeat count is a scratchpad parameter";
277 return why;
278 }
279 std::optional<uint32_t> rc = start.getRepeatCount();
280 int64_t runs = static_cast<int64_t>(rc ? *rc : taskOp.getRepeatCount());
281 ++runs;
282 if (runs % iterations != 0) {
283 os << "a start runs it " << runs
284 << " times, not a whole number of passes over its " << iterations
285 << "-long iteration dimension";
286 return why;
287 }
288 }
289 return std::nullopt;
290}
291
292// Issue the slices of `bd` as one single-descriptor task each. The task
293// becomes the first slice and the others are configured right after it, until
294// orderSlices moves each up to its first start. Only the last slice issues the
295// task's token, so an await of the task becomes an await of the last slice,
296// and a free frees every slice. Each start becomes a
297// group of consecutive starts, every slice once per pass, which orderSlices
298// then interleaves with the starts around it.
299static void splitIntoTasks(RewriterBase &rewriter, AIE::DMABDOp bd,
300 DMAConfigureTaskLike taskOp,
301 ArrayRef<NdDmaPattern> slices,
302 int64_t baseFlatOffset, int64_t iterations,
303 Slices &state) {
304 bool token = taskOp.getIssueToken();
305 int64_t taskRuns = static_cast<int64_t>(taskOp.getRepeatCount()) + 1;
306 SmallVector<Operation *> users(taskOp->getResult(0).getUsers());
307
308 SmallVector<DMAConfigureTaskLike> tasks{taskOp};
309 rewriter.setInsertionPointAfter(taskOp);
310 for (size_t i = 1; i < slices.size(); ++i) {
311 tasks.push_back(cast<DMAConfigureTaskLike>(rewriter.clone(*taskOp)));
312 state.lateTasks.insert(tasks.back());
313 }
314
315 for (auto it : llvm::enumerate(slices)) {
316 size_t i = it.index();
317 const NdDmaPattern &sub = it.value();
318 DMAConfigureTaskLike task = tasks[i];
319 AIE::DMABDOp sliceBd = bd;
320 if (i > 0)
321 task->walk([&](AIE::DMABDOp b) { sliceBd = b; });
322 int64_t flatOffset = flatOffsetFromPattern(baseFlatOffset, sub);
323 int32_t len = static_cast<int32_t>(lenFromInnermost3(sub.sizes));
324 rewriter.modifyOpInPlace(sliceBd, [&]() {
325 updateTaskBdInPlace(sliceBd, static_cast<int32_t>(flatOffset), len,
326 toOuter(sub.sizes), toOuter(sub.strides));
327 // A pinned id stays with the first slice, as in a chain.
328 if (i > 0)
329 sliceBd.removeBdIdAttr();
330 });
331 rewriter.modifyOpInPlace(task, [&]() {
332 // One pass over the slice is one execution per index of its own
333 // iteration dimension.
334 task.setRepeatCount(static_cast<uint32_t>(sub.sizes[3] - 1));
335 task.setIssueToken(token && i + 1 == slices.size());
336 });
337 }
338
339 for (Operation *user : users) {
340 if (auto await = dyn_cast<DMAAwaitTaskOp>(user)) {
341 rewriter.modifyOpInPlace(await, [&]() {
342 await.getTaskMutable().assign(tasks.back()->getResult(0));
343 });
344 continue;
345 }
346 if (auto free = dyn_cast<DMAFreeTaskOp>(user)) {
347 rewriter.setInsertionPointAfter(free);
348 for (DMAConfigureTaskLike task : llvm::drop_begin(tasks))
349 DMAFreeTaskOp::create(rewriter, free.getLoc(), task->getResult(0));
350 continue;
351 }
352 auto start = cast<DMAStartTaskOp>(user);
353 std::optional<uint32_t> rc = start.getRepeatCount();
354 int64_t runs = rc ? static_cast<int64_t>(*rc) + 1 : taskRuns;
355 int64_t count = runs / iterations * static_cast<int64_t>(slices.size());
356 int64_t group = state.nextGroup++;
357 // Only the last start of the last slice may issue the token: an earlier
358 // pass's would complete an await before the transfer has.
359 bool startToken = token && !start.getNoToken();
360 auto mark = [&](DMAStartTaskOp s, int64_t index) {
361 state.starts[s] = {group, index, count};
362 };
363 rewriter.modifyOpInPlace(start, [&]() { start.removeRepeatCountAttr(); });
364 mark(start, 0);
365 state.blocks.insert(start->getBlock());
366 rewriter.setInsertionPointAfter(start);
367 for (int64_t k = 1; k < count; ++k) {
368 size_t i = static_cast<size_t>(k) % slices.size();
369 bool withholds =
370 token && i + 1 == slices.size() && (!startToken || k + 1 != count);
371 auto s = DMAStartTaskOp::create(
372 rewriter, start.getLoc(), tasks[i]->getResult(0),
373 /*repeat_count=*/nullptr, /*repeat_count_val=*/nullptr,
374 /*no_token=*/withholds ? rewriter.getUnitAttr() : nullptr,
375 /*repeat_parameter=*/nullptr, /*repeat_state_table_idx=*/nullptr);
376 mark(s, k);
377 }
378 }
379}
380
381using ChannelKey = DmaQueueModel::ChannelKey;
382
383// The channel a start pushes onto, or nullopt when its task is not a configure
384// on a placed tile this pass can read.
385static std::optional<ChannelKey> channelOf(DMAStartTaskOp start) {
386 Operation *def = start.getTask().getDefiningOp();
387 if (auto cfg = dyn_cast_or_null<DMAConfigureTaskOp>(def)) {
388 if (AIE::TileOp tile = cfg.tryGetTileOp())
389 return DmaQueueModel::keyOf(tile, cfg.getDirection(), cfg.getChannel());
390 return std::nullopt;
391 }
392 auto cfgFor = dyn_cast_or_null<DMAConfigureTaskForOp>(def);
393 if (!cfgFor)
394 return std::nullopt;
395 auto dev = cfgFor->getParentOfType<AIE::DeviceOp>();
396 auto alloc = dev ? AIE::ShimDMAAllocationOp::getForSymbol(
397 dev, cfgFor.getAlloc().getRootReference())
398 : AIE::ShimDMAAllocationOp();
399 AIE::TileOp tile = alloc ? alloc.getTileOp() : AIE::TileOp();
400 if (!tile)
401 return std::nullopt;
402 return DmaQueueModel::keyOf(tile, alloc.getChannelDir(),
403 alloc.getChannelIndex());
404}
405
406// Interleave the slices splitIntoTasks emitted with the starts around them.
407// Later slices wait for earlier ones to finish (see
408// aie-assign-runtime-sequence-bd-ids), so a counterpart transfer left behind
409// all of them (a core's input for an output, say) would deadlock.
410//
411// A round runs from one start up to the next start on a channel it already
412// used. Each start's first slice keeps its place; later ones follow the
413// round's last start, ordered by index / size, ties in program order. Slices
414// move past configures, RTP writes and lock sets, which never wait. Any other
415// effect ends a round: an await, sync, poll, untracked push, free of an
416// unplaced slice, or a write32/blockwrite (either may be a push).
417static void orderSlices(Block &block, Slices &state) {
418 struct Pending {
419 DMAStartTaskOp start;
420 int64_t index, count, head;
421 };
422 SmallVector<Pending> pending;
423 llvm::DenseSet<Value> pendingTasks;
424 std::set<ChannelKey> round;
425 llvm::DenseMap<int64_t, int64_t> headOfGroup;
426 Operation *anchor = nullptr;
427 int64_t heads = 0;
428
429 auto flush = [&]() {
430 llvm::stable_sort(pending, [](const Pending &a, const Pending &b) {
431 int64_t lhs = a.index * b.count;
432 int64_t rhs = b.index * a.count;
433 return lhs != rhs ? lhs < rhs : a.head < b.head;
434 });
435 Operation *after = anchor;
436 for (Pending &p : pending) {
437 p.start->moveAfter(after);
438 after = p.start;
439 // Configure a slice only once it is due, so that the descriptors it
440 // takes are not held while earlier slices wait: the BD-ID pass can take
441 // descriptors back only from started tasks.
442 Operation *task = p.start.getTask().getDefiningOp();
443 if (task && state.lateTasks.erase(task))
444 task->moveBefore(p.start);
445 }
446 pending.clear();
447 pendingTasks.clear();
448 round.clear();
449 headOfGroup.clear();
450 anchor = nullptr;
451 };
452
453 for (Operation &op : llvm::make_early_inc_range(block)) {
454 if (auto start = dyn_cast<DMAStartTaskOp>(op)) {
455 std::optional<ChannelKey> channel = channelOf(start);
456 auto it = state.starts.find(start);
457 std::optional<Slices::Start> slice;
458 if (it != state.starts.end())
459 slice = it->second;
460 if (channel && slice && slice->index > 0) {
461 auto head = headOfGroup.find(slice->group);
462 if (head != headOfGroup.end()) {
463 pending.push_back({start, slice->index, slice->count, head->second});
464 pendingTasks.insert(start.getTask());
465 continue;
466 }
467 }
468 if (!channel || round.count(*channel))
469 flush();
470 if (!channel)
471 continue;
472 round.insert(*channel);
473 if (slice)
474 headOfGroup[slice->group] = heads;
475 ++heads;
476 anchor = &op;
477 continue;
478 }
479 if (auto free = dyn_cast<DMAFreeTaskOp>(op)) {
480 if (pendingTasks.contains(free.getTask()))
481 flush();
482 continue;
483 }
484 if (isa<DMAConfigureTaskOp, DMAConfigureTaskForOp, NpuWriteRTPOp,
485 SetLockOp>(op) ||
486 isMemoryEffectFree(&op))
487 continue;
488 flush();
489 }
490 flush();
491}
492
493static void decomposeMemcpy(RewriterBase &rewriter, NpuDmaMemcpyNdOp op) {
494 std::optional<NdDmaPattern> constant = constantPattern(op);
495 if (!constant)
496 return;
497 // A runtime length stays whole; see DMABDOp::verify.
498 if (op.getLengthParameterAttr() || op.getLengthStateTableIdxAttr())
499 return;
500
501 const NdDmaPattern &pattern = *constant;
502 if (isContiguousTransfer(pattern.sizes, pattern.strides))
503 return;
504
505 AIE::DeviceOp dev = op->getParentOfType<AIE::DeviceOp>();
506 if (!dev)
507 return;
508
509 auto allocOp = AIE::ShimDMAAllocationOp::getForSymbol(
510 dev, op.getMetadata().getRootReference());
511 if (!allocOp)
512 return;
513
514 AIE::TileOp tile = allocOp.getTileOp();
515 if (!tile)
516 return;
517
518 int col = tile.getCol();
519 int row = tile.getRow();
520 const AIE::AIETargetModel &targetModel = AIE::getTargetModel(op);
521 auto bufferType = cast<BaseMemRefType>(op.getMemref().getType());
522
523 if (patternPassesVerification(op, bufferType, targetModel, col, row, pattern))
524 return;
525
526 auto decomposed =
527 decomposeNdDmaPattern(op, bufferType, pattern, targetModel, col, row);
528 // failed() already guards both dereferences below via short-circuit /
529 // prior control flow; the checker just doesn't associate FailureOr's
530 // failed()/succeeded() idiom with the std::optional base it derives from.
531 if (failed(decomposed) ||
532 decomposed->empty()) // NOLINT(bugprone-unchecked-optional-access)
533 return;
534 // Bind a plain reference now that decomposed is known non-failed and
535 // non-empty, so nothing past this point looks like an optional access.
536 SmallVector<NdDmaPattern> &bds =
537 *decomposed; // NOLINT(bugprone-unchecked-optional-access)
538 if (bds.size() > targetModel.getNumBDs(col, row))
539 return;
540
541 rewriter.setInsertionPoint(op);
542 if (bds.size() == 1) {
543 rewriter.replaceOpWithNewOp<NpuDmaMemcpyNdOp>(
544 op, op, toOuter(bds.front().offsets), toOuter(bds.front().sizes),
545 toOuter(bds.front().strides));
546 return;
547 }
548
549 llvm::DenseSet<int64_t> usedIds;
550 if (auto seq = op->getParentOfType<AIE::RuntimeSequenceOp>()) {
551 seq.walk([&](NpuDmaMemcpyNdOp other) {
552 if (other == op)
553 return;
554 if (other.getMetadata() == op.getMetadata())
555 usedIds.insert(other.getId());
556 });
557 }
558
559 int64_t nextId = op.getId();
560 for (auto [idx, subPattern] : llvm::enumerate(bds)) {
561 bool last = idx + 1 == bds.size();
562 int64_t id = allocateNextId(op, nextId, usedIds);
563 nextId = id + 1;
564 createDecomposedOp(rewriter, op, subPattern, id,
565 last && op.getIssueToken());
566 }
567 rewriter.eraseOp(op);
568}
569
570// Rewrites a task's descriptor into legal ones. Fails only after reporting why
571// it cannot.
572static LogicalResult decomposeTaskBd(RewriterBase &rewriter, AIE::DMABDOp op,
573 Slices &slices) {
574 auto taskAndTile = resolveTaskAndTile(op);
575 if (!taskAndTile)
576 return success();
577
578 AIE::TileOp tile = taskAndTile->first;
579 DMAConfigureTaskLike taskOp = taskAndTile->second;
580
581 // A descriptor with more dimensions than a BD has cannot be lowered as it
582 // is, so where this pass cannot reduce it, it says why.
583 bool tooManyDims = op.getMixedSizes().size() > kNdDmaDims;
584 auto cannotReduce = [&]() {
585 return op.emitOpError()
586 << "has " << op.getMixedSizes().size()
587 << " dimensions, and a buffer descriptor holds " << kNdDmaDims
588 << "; the extra ones can only be split off ";
589 };
590 int col = tile.getCol();
591 int row = tile.getRow();
592 const AIE::AIETargetModel &targetModel = AIE::getTargetModel(op);
593 FailureOr<TaskBdDecomposition> plan =
594 decomposeTaskBdPattern(op, targetModel, col, row);
595 if (failed(plan)) {
596 if (tooManyDims)
597 return cannotReduce()
598 << "a descriptor whose offset, length, sizes and strides are "
599 "all constant and that has no padding";
600 return success();
601 }
602 if (countTaskBds(taskOp) != 1) {
603 if (tooManyDims)
604 return cannotReduce() << "a task's only descriptor";
605 return success();
606 }
607 // A descriptor takes its locks once per execution, which decomposition
608 // regroups, and a slice of its own would take none.
609 bool takesLocks = false;
610 taskOp->walk([&](AIE::UseLockOp) { takesLocks = true; });
611 if (takesLocks) {
612 if (tooManyDims)
613 return cannotReduce() << "a descriptor that takes no locks";
614 return success();
615 }
616 // A runtime length stays whole; see DMABDOp::verify.
617 if (op.getLengthParameterAttr() || op.getLengthStateTableIdxAttr())
618 return success();
619
620 if (plan->legal) // NOLINT(bugprone-unchecked-optional-access)
621 return success();
622
623 // Iteration uses the outermost ND slot, so there is no free dimension to
624 // factor into, and chain-slicing has no defined iteration semantics.
625 if (op.getIteration())
626 return op.emitOpError()
627 << "buffer descriptor with the iteration attribute is too large "
628 "to lower and cannot be decomposed";
629
630 // One pass over the pattern, in executions of the descriptor.
631 int64_t iterations =
632 iterationCount(plan->pass); // NOLINT(bugprone-unchecked-optional-access)
633 // Bind a plain reference now that plan is known non-failed, so nothing past
634 // this point looks like an optional access.
635 SmallVector<NdDmaPattern> &bds =
636 plan->pieces; // NOLINT(bugprone-unchecked-optional-access)
637 if (bds.empty()) {
638 if (tooManyDims)
639 return op.emitOpError()
640 << "has " << op.getMixedSizes().size()
641 << " dimensions, and no split into at most " << kMaxNdDmaPieces
642 << " descriptors of " << kNdDmaDims << " fits this tile";
643 return success();
644 }
645
646 int64_t baseFlatOffset = op.getConstantOffset().value_or(0);
647
648 if (bds.size() == 1) {
649 const NdDmaPattern &sub = bds.front();
650 int64_t flatOffset = flatOffsetFromPattern(baseFlatOffset, sub);
651 auto outerSizes = toOuter(sub.sizes);
652 auto outerStrides = toOuter(sub.strides);
653 // len covers one BD invocation, so it tracks the innermost three
654 // dimensions only. Rewriting in place can move extent into the fourth
655 // (repeat) dimension -- e.g. an innermost run too long for the wrap
656 // field factors out an extra dimension and pushes the outermost one into
657 // the repeat slot -- so len has to be recomputed alongside the shape.
658 int32_t len = static_cast<int32_t>(lenFromInnermost3(sub.sizes));
659
660 IterationScale scale = iterationScale(iterations, sub);
661 int64_t runs = 0;
662 if (!scale.isOne()) {
663 if (taskOp.getRepeatCountVal())
664 return op.emitOpError()
665 << "cannot decompose a buffer descriptor whose repeat count "
666 "is a runtime value: decomposition needs to scale it by "
667 << scale.str();
668 // A scaled count past what one queue push carries is fine: the BD-ID
669 // pass issues it as several starts. Only the attribute width limits
670 // it. Widen before multiplying: the accessor returns int32_t, so the
671 // addition alone would overflow in int and wrap past this check.
672 // Merged executions need every start to run whole ones.
673 auto check = [&](Operation *at, int64_t repeat, StringRef runner,
674 StringRef count) -> LogicalResult {
675 int64_t before = repeat + 1;
676 if (!scale.divides(before))
677 return at->emitOpError()
678 << "cannot decompose: it merges every " << scale.den
679 << " executions of the buffer descriptor into " << scale.num
680 << ", and " << runner << " runs it " << before << " times";
681 int64_t after = scale.apply(before);
682 if (after - 1 > std::numeric_limits<int32_t>::max())
683 return at->emitOpError()
684 << "decomposition scales " << count << " by " << scale.str()
685 << " to " << (after - 1) << ", beyond a 32-bit repeat_count";
686 return success();
687 };
688 runs = taskOp.getRepeatCount() + int64_t{1};
689 if (failed(check(op, runs - 1, "the task", "the repeat count")))
690 return failure();
691 // A start that overrides the task's count repeats the same BD, so it
692 // scales by the same factor.
693 for (Operation *user : taskOp->getResult(0).getUsers()) {
694 auto start = dyn_cast<DMAStartTaskOp>(user);
695 if (!start)
696 continue;
697 if (start.getRepeatCountVal() || start.hasRepeatParameter())
698 return start.emitOpError()
699 << "cannot decompose a buffer descriptor this start repeats "
700 "a runtime number of times: decomposition needs to scale "
701 "the count by "
702 << scale.str();
703 if (IntegerAttr rc = start.getRepeatCountAttr())
704 if (failed(check(start, rc.getInt(), "this start",
705 "this start's repeat count")))
706 return failure();
707 }
708 runs = scale.apply(runs);
709 }
710
711 rewriter.modifyOpInPlace(op, [&]() {
712 updateTaskBdInPlace(op, static_cast<int32_t>(flatOffset), len, outerSizes,
713 outerStrides);
714 });
715 if (!scale.isOne()) {
716 rewriter.modifyOpInPlace(taskOp, [&]() {
717 taskOp.setRepeatCount(static_cast<uint32_t>(runs - 1));
718 });
719 for (Operation *user : taskOp->getResult(0).getUsers())
720 if (auto start = dyn_cast<DMAStartTaskOp>(user))
721 if (IntegerAttr rc = start.getRepeatCountAttr())
722 rewriter.modifyOpInPlace(start, [&]() {
723 start.setRepeatCountAttr(
724 rewriter.getI32IntegerAttr(scale.apply(rc.getInt() + 1) - 1));
725 });
726 }
727 return success();
728 }
729
730 // A split would make every slice reuse the one out_of_order_id slot.
731 // TODO: BD iteration plus padding may enable this feature.
732 if (op.getOutOfOrderId().has_value())
733 return op.emitOpError() << "splitting an out-of-order buffer descriptor "
734 "into multiple descriptors is not implemented";
735
736 // Every member of a chain runs once per task execution, so the one
737 // queue-push repeat count is shared by all of them. A slice that also wants
738 // its own iteration factor needs a private repeat, which only a task of
739 // its own has. A chain also holds all of its descriptors until it
740 // finishes, so past the queue depth the slices go out as separate tasks,
741 // whose descriptors aie-assign-runtime-sequence-bd-ids can recycle one by
742 // one.
743 bool sharedRepeat = llvm::all_of(bds, [&](const NdDmaPattern &sub) {
744 return iterationScale(iterations, sub).isOne();
745 });
746 bool chainFits =
747 sharedRepeat && bds.size() <= targetModel.getNumBDs(col, row);
748 uint32_t depth = targetModel.getDmaTaskQueueDepth();
749 if (!chainFits || (depth > 0 && bds.size() > depth)) {
750 // The dynamic BD pool allocates a chain under runtime control flow, but
751 // separate tasks there are left whole.
752 if (isUnderRuntimeControlFlow(op)) {
753 if (tooManyDims)
754 return cannotReduce()
755 << "outside runtime control flow, since its " << bds.size()
756 << " pieces have to be separate tasks";
757 op.emitRemark() << "deferring decomposition into " << bds.size()
758 << " separate tasks under runtime control flow";
759 return success();
760 }
761 std::optional<std::string> why =
762 whyNotSeparateTasks(op, taskOp, iterations);
763 if (!why) {
764 splitIntoTasks(rewriter, op, taskOp, bds, baseFlatOffset, iterations,
765 slices);
766 return success();
767 }
768 if (!sharedRepeat)
769 return op.emitOpError()
770 << "cannot split this buffer descriptor: its slices need "
771 "per-descriptor repeat counts, which a chain shares, and "
772 "they cannot be separate tasks because "
773 << *why;
774 if (!chainFits)
775 return op.emitOpError()
776 << "cannot split this buffer descriptor: its " << bds.size()
777 << " slices outnumber the tile's "
778 << targetModel.getNumBDs(col, row)
779 << " buffer descriptors, and they cannot be separate tasks "
780 "because "
781 << *why;
782 }
783
784 Region *body = &taskOp.getBody();
785 if (body->empty())
786 return success();
787
788 SmallVector<Block *> blocks;
789 blocks.push_back(op->getBlock());
790 for (unsigned i = 1; i < bds.size(); ++i)
791 blocks.push_back(rewriter.createBlock(body));
792
793 for (auto [idx, subPattern] : llvm::enumerate(bds)) {
794 Block *block = blocks[idx];
795 int64_t flatOffset = flatOffsetFromPattern(baseFlatOffset, subPattern);
796 auto outerSizes = toOuter(subPattern.sizes);
797 auto outerStrides = toOuter(subPattern.strides);
798 int32_t len = static_cast<int32_t>(lenFromInnermost3(subPattern.sizes));
799
800 if (idx == 0) {
801 rewriter.modifyOpInPlace(op, [&]() {
802 updateTaskBdInPlace(op, static_cast<int32_t>(flatOffset), len,
803 outerSizes, outerStrides);
804 });
805 Operation *oldTerm = block->getTerminator();
806 rewriter.setInsertionPoint(oldTerm);
807 if (idx + 1 < bds.size())
808 AIE::NextBDOp::create(rewriter, op.getLoc(), blocks[idx + 1]);
809 else
810 AIE::EndOp::create(rewriter, op.getLoc());
811 rewriter.eraseOp(oldTerm);
812 } else {
813 rewriter.setInsertionPointToStart(block);
814 createTaskBd(rewriter, op.getLoc(), op, static_cast<int32_t>(flatOffset),
815 len, outerSizes, outerStrides);
816 rewriter.setInsertionPointToEnd(block);
817 if (idx + 1 < bds.size())
818 AIE::NextBDOp::create(rewriter, op.getLoc(), blocks[idx + 1]);
819 else
820 AIE::EndOp::create(rewriter, op.getLoc());
821 }
822 }
823
824 return success();
825}
826
827struct AIEDecomposeLargeDmaBdPass
828 : xilinx::AIEX::impl::AIEDecomposeLargeDmaBdBase<
829 AIEDecomposeLargeDmaBdPass> {
830 void runOnOperation() override {
831 AIE::DeviceOp device = getOperation();
832 IRRewriter rewriter(&getContext());
833 SmallVector<NpuDmaMemcpyNdOp> memcpys;
834 SmallVector<AIE::DMABDOp> bds;
835 device.walk([&](Operation *op) {
836 if (auto memcpy = dyn_cast<NpuDmaMemcpyNdOp>(op))
837 memcpys.push_back(memcpy);
838 else if (auto bd = dyn_cast<AIE::DMABDOp>(op))
839 bds.push_back(bd);
840 });
841 for (NpuDmaMemcpyNdOp memcpy : memcpys)
842 decomposeMemcpy(rewriter, memcpy);
843 Slices slices;
844 bool failed = false;
845 for (AIE::DMABDOp bd : bds)
846 failed |= mlir::failed(decomposeTaskBd(rewriter, bd, slices));
847 if (failed)
848 return signalPassFailure();
849 for (Block *block : slices.blocks)
850 orderSlices(*block, slices);
851 }
852};
853
854} // namespace
855
856std::unique_ptr<OperationPass<AIE::DeviceOp>>
858 return std::make_unique<AIEDecomposeLargeDmaBdPass>();
859}
std::array< int, 4 > ChannelKey
{col, row, direction, channel}.
static ChannelKey keyOf(AIE::TileOp tile, AIE::DMAChannelDir direction, int64_t channel)
virtual uint32_t getDmaTaskQueueDepth() const =0
Return how many DMA tasks a single channel's task queue holds (unsupported = 0).
virtual uint32_t getNumBDs(AIETileType tileType) const =0
Return the number of buffer descriptors for a given tile type.
bool isContiguousTransfer(llvm::ArrayRef< int64_t > sizes, llvm::ArrayRef< int64_t > strides)
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIEDecomposeLargeDmaBdPass()
mlir::FailureOr< llvm::SmallVector< NdDmaPattern > > decomposeNdDmaPattern(mlir::Operation *forOp, mlir::BaseMemRefType referencedBufType, const NdDmaPattern &pattern, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow)
Decompose an illegal pattern into one or more legal sub-patterns that move the same data.
mlir::FailureOr< TaskBdDecomposition > decomposeTaskBdPattern(xilinx::AIE::DMABDOp bd, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow)
Plan the split of bd, a runtime task's descriptor, on tile (tileCol, tileRow).
bool patternPassesVerification(mlir::Operation *forOp, mlir::BaseMemRefType referencedBufType, const xilinx::AIE::AIETargetModel &tm, int tileCol, int tileRow, const NdDmaPattern &pattern, bool skipTransformationChecks=false)
Returns true when the pattern passes verifyStridesWraps for the given tile and memref element type.
const AIETargetModel & getTargetModel(mlir::Operation *op)
Innermost-first ND access pattern (d0..d3 / repeat), matching the convention used by verifyStridesWra...
llvm::SmallVector< int64_t, kNdDmaDims > sizes
int64_t baseOffset
Elements added to the address the offsets give.
llvm::SmallVector< int64_t, kNdDmaDims > offsets
llvm::SmallVector< int64_t, kNdDmaDims > strides