MLIR-AIE 1.4.5.dev
AIEDMATasksToNPU.cpp
Go to the documentation of this file.
1//===- AIEDMATasksToNPU.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#include <algorithm>
9#include <iterator>
10#include <limits>
11
17
18#include "mlir/Dialect/Arith/IR/Arith.h"
19#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
20#include "mlir/Dialect/MemRef/IR/MemRef.h"
21#include "mlir/Dialect/SCF/IR/SCF.h"
22#include "mlir/Dialect/UB/IR/UBOps.h"
23#include "mlir/Dialect/Utils/StaticValueUtils.h"
24#include "mlir/Interfaces/ControlFlowInterfaces.h"
25#include "mlir/Interfaces/SideEffectInterfaces.h"
26#include "mlir/Pass/Pass.h"
27#include "mlir/Pass/PassManager.h"
28#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
29#include "mlir/Transforms/LoopInvariantCodeMotionUtils.h"
30#include "mlir/Transforms/Passes.h"
31#include "mlir/Transforms/WalkPatternRewriteDriver.h"
32#include "llvm/ADT/STLExtras.h"
33#include "llvm/ADT/TypeSwitch.h"
34
35namespace xilinx::AIEX {
36#define GEN_PASS_DEF_AIEDMATASKSTONPU
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
44// This pass emits absolute tile coordinates, so it cannot run before placement
45// has substituted a concrete `aie.tile` for an `aie.logical_tile`. Report that
46// instead of calling getTileOp(), which fatally errors on an unplaced tile.
47static LogicalResult emitUnplacedTileError(Operation *op,
48 DMAConfigureTaskOp task_op) {
49 auto err = op->emitOpError(
50 "Cannot lower a DMA task whose tile is not placed; run placement first.");
51 if (task_op.getOperation() != op)
52 err.attachNote(task_op.getLoc()) << "Task configured here.";
53 return err;
54}
55
56struct DMAStartTaskOpPattern : OpRewritePattern<DMAStartTaskOp> {
57 DMAStartTaskOpPattern(MLIRContext *context,
58 llvm::DenseMap<Operation *, bool> &onlyPushedOnce)
60
61 // Whether every start of a configure pushes a constant repeat count of 0.
62 llvm::DenseMap<Operation *, bool> &onlyPushedOnce;
63
64 LogicalResult matchAndRewrite(DMAStartTaskOp op,
65 PatternRewriter &rewriter) const override {
66 DMAConfigureTaskOp task_op = op.getTaskOp();
67 if (!task_op && !op.getTask().getDefiningOp<DMAConfigureTaskForOp>()) {
68 // A task carried through runtime control flow. The push names one head
69 // BD, so it lowers only if every value the task can carry comes from
70 // the same configure.
71 task_op = getUniqueReachableConfigure(op.getTask());
72 if (!task_op)
73 return op.emitOpError(
74 "starts a task carried through control flow that does not come "
75 "from exactly one aiex.dma_configure_task, so its queue push has "
76 "no single buffer descriptor ID. Start the task where it is "
77 "configured.");
78 }
79 if (!task_op) {
80 // Cannot rewrite this; probably points to a DMAStartTaskForOp,
81 // which we will lower once it has been rewritten into a DMAStartTaskOp.
82 return failure();
83 }
84 AIE::TileOp tile = task_op.tryGetTileOp();
85 if (!tile)
86 return emitUnplacedTileError(op, task_op);
87 Location loc = op.getLoc();
88
89 // The bd_id for the queue push: the head BD's runtime pool value if it
90 // carries one, else its static value.
91 AIE::DMABDOp head = task_op.getFirstBd();
92 OpFoldResult idOfr = head ? head.getBdIdValue() : OpFoldResult();
93 if (!idOfr) {
94 auto err = op.emitOpError(
95 "First buffer descriptor in chain has not been assigned an ID");
96 err.attachNote() << "Run the `aie-assign-runtime-buffer-descriptor-ids` "
97 "pass first or manually assign an ID.";
98 return failure();
99 }
100 Value bdIdVal = getAsValue(rewriter, loc, idOfr, rewriter.getI32Type());
101 if (op.getRepeatParameterAttr())
102 return op.emitOpError("has a repeat_parameter; run "
103 "--aie-lower-scratchpad-parameters first");
104 // A runtime repeat count keeps its width so the push lowering can
105 // range-check it before narrowing. A scratchpad one is added to a push
106 // of 0.
107 IntegerAttr repeatIdx = op.getRepeatStateTableIdxAttr();
108 OpFoldResult repeatOfr = repeatIdx ? rewriter.getI32IntegerAttr(0)
109 : op.getPushRepeatCount(task_op);
110 onlyPushedOnce.try_emplace(task_op, true).first->second &=
111 !repeatIdx && isConstantIntValue(repeatOfr, 0);
112 Value repeatCount = dyn_cast<Value>(repeatOfr);
113 if (!repeatCount)
114 repeatCount = getAsValue(rewriter, loc, repeatOfr, rewriter.getI32Type());
115 rewriter.replaceOpWithNewOp<NpuPushQueueOp>(
116 op, tile.getCol(), tile.getRow(), task_op.getDirection(),
117 task_op.getChannel(), op.getPushIssueToken(task_op), repeatCount,
118 bdIdVal, repeatIdx);
119 return success();
120 }
121};
122
123// Resolve a task value to a configure that gives the sync its physical channel.
124// The value is usually a configure result, but under the dynamic BD pool path a
125// task threaded through runtime control flow surfaces as a region-branch
126// successor input instead: an scf.for/scf.if result (the task carried out of
127// the construct), or a loop's own iter_arg block argument (when the task is
128// awaited from inside the loop body rather than after it -- e.g. a
129// software-pipelined sequence awaiting the previous iteration's task before
130// reusing its BD). Every reachable configure targets the same physical channel
131// -- the pool pass verified both branches of an scf.if agree, and a loop's
132// init and per-iteration reconfiguration agree -- so the first one found
133// gives the right channel.
134//
135// getReachableConfigures walks such a value back to its configures through
136// RegionBranchOpInterface, at any nesting depth.
137static DMAConfigureTaskOp resolveConfigureThroughCF(Value task) {
138 SmallVector<DMAConfigureTaskOp> configures;
139 (void)getReachableConfigures(task, configures);
140 return configures.empty() ? nullptr : configures.front();
141}
142
143struct DMAAwaitTaskOpPattern : OpRewritePattern<DMAAwaitTaskOp> {
144 using OpRewritePattern::OpRewritePattern;
145
146 LogicalResult matchAndRewrite(DMAAwaitTaskOp op,
147 PatternRewriter &rewriter) const override {
148 DMAConfigureTaskOp task_op = op.getTaskOp();
149 if (!task_op) {
150 // The awaited task threads through runtime control flow (an scf result),
151 // so it has no directly-defining configure; walk the SSA carry to one.
152 task_op = resolveConfigureThroughCF(op.getTask());
153 }
154 if (!task_op) {
155 return failure();
156 }
157 if (!task_op.getIssueToken()) {
158 auto err = op.emitOpError(
159 "Cannot wait on a BD that is not configured to issue a token.");
160 err.attachNote(task_op.getLoc())
161 << "Consider adding attribute `issue_token=true` here.";
162 return err;
163 }
164 AIE::TileOp tile = task_op.tryGetTileOp();
165 if (!tile)
166 return emitUnplacedTileError(op, task_op);
167 Location loc = op.getLoc();
168 rewriter.replaceOpWithNewOp<NpuSyncOp>(
169 op, createConstantI32(rewriter, loc, tile.getCol()),
170 createConstantI32(rewriter, loc, tile.getRow()),
171 createConstantI32(rewriter, loc, (uint32_t)task_op.getDirection()),
172 createConstantI32(rewriter, loc, task_op.getChannel()),
173 createConstantI32(rewriter, loc, 1),
174 createConstantI32(rewriter, loc, 1));
175 return success();
176 }
177};
178
180 : xilinx::AIEX::impl::AIEDMATasksToNPUBase<AIEDMATasksToNPUPass> {
181
182 // A BD whose aie.next_bd successor draws its id from the dynamic pool, mapped
183 // to that runtime id; hoistNextBdOpsIntoAttrs fills it in place of the
184 // next_bd_id attribute.
185 llvm::DenseMap<AIE::DMABDOp, Value> runtimeNextBdId;
186
187 bool shouldSkipBlock(Block &block) {
188 // Allow blocks in the input IR that contain nothing but a next_bd operation
189 // as the entry block. We will skip these blocks and not lower them to
190 // anything.
191 auto it = block.without_terminator();
192 return block.isEntryBlock() && it.begin() == it.end();
193 }
194
195 LogicalResult verifyBdInBlock(Block &block) {
196 auto bd_ops = block.getOps<AIE::DMABDOp>();
197 // Exactly one BD op per block
198 int n_bd_ops = std::distance(bd_ops.begin(), bd_ops.end());
199 if (n_bd_ops < 1) {
200 auto error = block.getTerminator()->emitError(
201 "Block ending in this terminator does not contain a required "
202 "aie.dma_bd operation.");
203 error.attachNote(block.getParentOp()->getLoc())
204 << "Error encountered while lowering this BD configuration.";
205 return failure();
206 }
207 if (n_bd_ops > 1) {
208 auto error = block.getTerminator()->emitOpError(
209 "This block contains multiple aie.dma_bd operations. Exactly one "
210 " is "
211 "required.");
212 auto it = bd_ops.begin();
213 ++it;
214 for (; it != bd_ops.end(); ++it) {
215 error.attachNote((*it)->getLoc()) << "Extra aie.dma_bd operation here.";
216 }
217 return failure();
218 }
219 AIE::DMABDOp bd_op = *bd_ops.begin();
220 bool hasRuntimeBdId = bd_op.getBdIdVal() != nullptr;
221 if (!hasRuntimeBdId && !bd_op.getBdId().has_value()) {
222 auto error = bd_op.emitOpError(
223 "Cannot lower buffer descriptor without assigned ID.");
224 error.attachNote()
225 << "Run the `--aie-assign-runtime-sequence-bd-ids` pass first or "
226 "manually assign an ID to this buffer descriptor.";
227 error.attachNote(block.getParentOp()->getLoc())
228 << "Error encountered while lowering this BD configuration.";
229 return failure();
230 }
231 return success();
232 }
233
234 // A builder that derives a BD operand from runtime scalars (a width cast, a
235 // transfer-length product, a cf.assert guard) emits that arithmetic where it
236 // is called, inside the BD block. None of it depends on the BD, so hoist it
237 // ahead of the task. What stays (an op with other effects, or one reading a
238 // value only the block defines) is left for verifyNoUnsupportedOpsInBlock to
239 // reject.
240 void hoistScalarOpsOutOfBdBlocks(DMAConfigureTaskOp op) {
241 moveLoopInvariantCode(
242 {&op.getBody()},
243 [](Value v, Region *body) {
244 return !body->isAncestor(v.getParentRegion());
245 },
246 [](Operation *inner, Region *) {
247 return inner->getNumRegions() == 0 &&
248 (isPure(inner) || isa<cf::AssertOp>(inner));
249 },
250 [&](Operation *inner, Region *) { inner->moveBefore(op); });
251 }
252
253 LogicalResult verifyNoUnsupportedOpsInBlock(Block &block) {
254 WalkResult unsupported_ops = block.walk([&](Operation *inner_op) {
255 return llvm::TypeSwitch<Operation *, WalkResult>(inner_op)
256 .Case<AIE::DMABDOp>(
257 [&](AIE::DMABDOp bd_op) { return WalkResult::advance(); })
258 .Case<AIE::UseLockOp>(
259 [&](AIE::UseLockOp lock_op) { return WalkResult::advance(); })
260 .Case<arith::ConstantOp>(
261 [&](arith::ConstantOp const_op) { return WalkResult::advance(); })
262 .Case<AIE::NextBDOp>(
263 [&](AIE::NextBDOp lock_op) { return WalkResult::advance(); })
264 .Case<AIE::EndOp>(
265 [&](AIE::EndOp lock_op) { return WalkResult::advance(); })
266 .Default([&](Operation *inner_op) {
267 auto error = block.getParentOp()->emitOpError(
268 "Unsupported operation within BD block.");
269 error.attachNote(inner_op->getLoc())
270 << "No lowering to NPU instructions available for this "
271 "operation.";
272 return WalkResult::interrupt();
273 });
274 });
275 if (unsupported_ops.wasInterrupted()) {
276 return failure();
277 }
278 return success();
279 }
280
281 AIE::DMABDOp getBdForBlock(Block &block) {
282 auto bd_ops = block.getOps<AIE::DMABDOp>();
283 AIE::DMABDOp bd_op = *bd_ops.begin(); // Dereference first (and only, after
284 // previous checks) bd op iterator
285 return bd_op;
286 }
287
288 LogicalResult setAddressForSingleBD(OpBuilder &builder, AIE::DMABDOp &bd_op,
289 AIE::TileOp &tile,
290 OpFoldResult bdId = {}) {
291 const AIE::AIETargetModel &target_model = AIE::getTargetModel(bd_op);
292 auto buf = bd_op.getBuffer();
293 auto col = tile.getCol();
294 auto row = tile.getRow();
295 // A foldable SSA bd_id must name the same register as the block write.
296 auto constBdId = bdId ? getConstantIntValue(bdId) : std::nullopt;
297 uint32_t bd_id = constBdId ? *constBdId : bd_op.getBdId().value_or(0);
298 uint64_t register_addr = target_model.getDmaBdAddress(col, row, bd_id) +
299 target_model.getDmaBdAddressOffset(col, row);
300 // On the runtime-bd_id path the patched register (BD buffer-address word)
301 // is itself runtime: getBdRegisterBase(bd_id) + getDmaBdAddressOffset.
302 // Emitted as an SSA operand on the address patch; null keeps the static
303 // constant.
304 Value runtimeRegisterAddr;
305 if (bdId && !getConstantIntValue(bdId)) {
306 Value base = getBdRegisterBase(builder, bd_op.getLoc(), target_model, col,
307 row, bdId);
308 runtimeRegisterAddr = arith::AddIOp::create(
309 builder, bd_op.getLoc(), base,
310 createConstantI32(builder, bd_op.getLoc(),
311 target_model.getDmaBdAddressOffset(col, row)));
312 }
313
314 // emitUpdateBdAddressFromOffsetParameter bakes a compile-time register
315 // address into npu.update_from_scratchpad, so pairing it with a BD drawn
316 // from the runtime pool would patch whichever BD that literal names rather
317 // than the one just configured.
318 if (bd_op.getOffsetStateTableIdxAttr() && runtimeRegisterAddr)
319 return bd_op->emitOpError(
320 "offset_state_table_idx is not supported with a runtime bd_id; the "
321 "scratchpad update targets a compile-time BD register address.");
322
323 // A buffer descriptor can refer to a statically allocated aie.buffer, or to
324 // a DDR buffer which will be passed as a runtime argument (block
325 // argument). Try to find the root block argument, either directly or
326 // through supported subview/view/cast chains.
327 mlir::BlockArgument buf_arg = nullptr;
328 int64_t offset = 0;
329
330 if (auto directArg = llvm::dyn_cast<mlir::BlockArgument>(buf)) {
331 buf_arg = directArg;
332 offset = 0;
333 } else if (auto traceResult = traceSubviewToBlockArgument(buf)) {
334 buf_arg = traceResult->rootArg;
335 offset = traceResult->offsetInBytes;
336 }
337
338 if (buf_arg) {
339 if (!target_model.isShimNOCTile(tile.getCol(), tile.getRow())) {
340 return bd_op->emitOpError("DDR memory (runtime input arguments) can "
341 "only be referred to on shim tiles.");
342 }
343
344 std::optional<unsigned> hostIdx = getHostBufferArgIndex(buf_arg);
345 if (!hostIdx)
346 return bd_op->emitOpError(
347 "buffer must resolve to a memref argument of the runtime sequence");
348 unsigned arg_idx = *hostIdx;
349 // arg_plus = buffer byte offset. The dma_bd offset is a single element
350 // offset (stride 1); a constant folds to a constant (byte-identical to
351 // before), a runtime offset operand is built with arith. `offset` here is
352 // the constant subview base in bytes.
353 OpFoldResult offsetOfr =
354 bd_op.getOffset() ? OpFoldResult(bd_op.getOffset())
355 : OpFoldResult(builder.getI32IntegerAttr(
356 bd_op.getConstantOffset().value_or(0)));
357 OpFoldResult oneStride = builder.getI32IntegerAttr(1);
358 Value argPlus =
359 buildArgPlusValue(builder, bd_op.getLoc(), {offsetOfr}, {oneStride},
360 bd_op.getBufferElementTypeWidthInBytes(), offset,
361 target_model.getAddressGenGranularity() / 8);
362 if (!argPlus)
363 return failure();
364 NpuAddressPatchOp::create(builder, bd_op.getLoc(),
365 /*addr*/ register_addr,
366 /*addr_val*/ runtimeRegisterAddr,
367 /*arg_idx*/ arg_idx, argPlus);
368 } else if (AIE::BufferOp buffer =
369 llvm::dyn_cast<AIE::BufferOp>(buf.getDefiningOp())) {
370 std::optional<uint32_t> bufferAddr = buffer.getAddress();
371 if (!bufferAddr.has_value()) {
372 return bd_op->emitOpError(
373 "Cannot lower buffer without associated address. Run pass "
374 "--aie-assign-buffer-addresses first or manually assign an "
375 "address.");
376 }
377 uint64_t buf_addr = *bufferAddr;
378 // A memtile DMA addresses its west neighbour, itself and its east
379 // neighbour as three windows (west=0, own=getMemTileSize, east=2x), as
380 // the static lowering in AIERT.cpp does.
381 if (target_model.isMemTile(col, row)) {
382 auto addrOffset = target_model.getMemLocalBaseAddress(
383 col, row, buffer.getTileOp().getCol(), buffer.getTileOp().getRow());
384 if (addrOffset)
385 buf_addr += addrOffset.value();
386 }
387
388 // The BD's address field, from the target model's layout. Shim NOC BDs
389 // address bytes; mem and core tile BDs address 32-bit words.
390 const AIE::DmaBdLayout *layout = target_model.getDmaBdLayout(col, row);
391 if (!layout)
392 return bd_op->emitOpError(
393 "has no buffer descriptor layout on this tile");
394 const AIE::DmaBdField &addrBits = layout->bufferOffset;
395 uint64_t divisor = target_model.isShimNOCTile(col, row) ? 1 : 4;
396 uint64_t maxByteAddr = (uint64_t(addrBits.mask()) + 1) * divisor - 1;
397 if (buf_addr > maxByteAddr)
398 return bd_op->emitOpError("buffer address exceeds the tile's DMA "
399 "address field.");
400
401 // getOffsetInBytes() folds a runtime offset to 0, so it may only be read
402 // on the constant path; a runtime offset is added with arith instead.
403 bool constOffset = !bd_op.getOffset() || bd_op.getConstantOffset();
404 Location loc = bd_op.getLoc();
405 Value addrField;
406 if (constOffset) {
407 buf_addr += bd_op.getOffsetInBytes();
408 if (buf_addr > maxByteAddr)
409 return bd_op->emitOpError("buffer address exceeds the tile's DMA "
410 "address field.");
411 addrField =
412 createConstantI32(builder, loc, addrBits.place(buf_addr / divisor));
413 } else {
414 Value byteAddr =
415 buildArgPlusValue(builder, loc, {OpFoldResult(bd_op.getOffset())},
416 {OpFoldResult(builder.getI32IntegerAttr(1))},
417 bd_op.getBufferElementTypeWidthInBytes(),
418 (int64_t)buf_addr, divisor);
419 if (!byteAddr)
420 return failure();
421 Type i64 = builder.getI64Type();
422 Value fits = builder.createOrFold<arith::CmpIOp>(
423 loc, arith::CmpIPredicate::ule, byteAddr,
424 arith::ConstantOp::create(builder, loc,
425 IntegerAttr::get(i64, maxByteAddr)));
426 if (failed(emitRuntimeCheck(builder, loc, fits,
427 "a runtime DMA offset puts the buffer "
428 "address past the tile's DMA address "
429 "field")))
430 return failure();
431 if (divisor != 1)
432 byteAddr = arith::DivUIOp::create(
433 builder, loc, byteAddr,
434 arith::ConstantOp::create(builder, loc,
435 IntegerAttr::get(i64, divisor)));
436 addrField = arith::TruncIOp::create(builder, loc, builder.getI32Type(),
437 byteAddr);
438 if (addrBits.shift != 0)
439 addrField = arith::ShLIOp::create(
440 builder, loc, addrField,
441 createConstantI32(builder, loc, addrBits.shift));
442 }
443 // The register holding the address word. A runtime bd_id makes it
444 // runtime too; a pinned one folds to the literal the static path uses.
445 Value regAddrVal =
446 runtimeRegisterAddr
447 ? runtimeRegisterAddr
448 : createConstantI32(builder, loc,
449 static_cast<uint32_t>(register_addr));
450 uint32_t addrMask = addrBits.place(0xFFFFFFFF);
451 if (addrMask == 0xFFFFFFFF)
452 NpuWrite32Op::create(builder, loc, regAddrVal, addrField, nullptr,
453 nullptr, nullptr);
454 else
455 NpuMaskWrite32Op::create(builder, loc, regAddrVal, addrField,
456 createConstantI32(builder, loc, addrMask),
457 nullptr, nullptr, nullptr);
458 } else {
459 return bd_op->emitOpError(
460 "Buffer argument must be a constant aie.buffer, a runtime sequence "
461 "input argument, or a supported chain of memref.subview, "
462 "memref.view, memref.cast, or memref.reinterpret_cast operations "
463 "rooted at a block argument. Subviews must be static and contiguous; "
464 "views must have a constant byte shift and no dynamic result sizes.");
465 }
466
467 // If this BD has an offset_state_table_idx, emit update_from_scratchpad to
468 // add the runtime offset to the BD address register. This is applied after
469 // the base address is set (by either NpuAddressPatchOp for DDR buffers or
470 // NpuMaskWrite32Op/NpuWrite32Op for on-chip buffers), since the hardware
471 // update_from_scratchpad instruction is additive -- it reads the existing
472 // register value and adds a computed delta to it.
473 if (bd_op.getOffsetStateTableIdxAttr()) {
474 auto bufType = llvm::cast<BaseMemRefType>(bd_op.getBuffer().getType());
475 if (failed(emitUpdateBdAddressFromOffsetParameter(builder, bd_op, bufType,
476 register_addr)))
477 return failure();
478 }
479
480 return success();
481 }
482
483 // The lock / packet / next_bd fields for a BD block. A runtime lock value is
484 // guarded and encoded with `builder`; only the dynamic path reaches that.
485 FailureOr<BdTemplateFields>
486 gatherBdTemplateFields(OpBuilder &builder, Block &block, AIE::DMABDOp bd_op,
487 AIE::TileOp &tile,
488 const AIE::AIETargetModel &target_model,
489 std::optional<xilinx::AIE::PacketInfoAttr> packet,
490 bool outOfOrder = false) {
492 if (std::optional<uint32_t> nextBdId = bd_op.getNextBdId()) {
493 f.next_bd_id = *nextBdId;
494 f.use_next_bd = 1;
495 } else if (Value nextBdIdVal = runtimeNextBdId.lookup(bd_op)) {
496 f.next_bd_id_val = nextBdIdVal;
497 f.use_next_bd = 1;
498 }
499
500 auto info = bd_op.getPacket().value_or(packet.value_or(nullptr));
501 if (info) {
502 if (!info.isAssigned()) {
503 bd_op.emitOpError("packet has no pkt_id; allocation assigns one, so a "
504 "header at this level must carry it");
505 return failure();
506 }
507 f.enable_packet = 1;
508 f.packet_type = info.getPktType();
509 f.packet_id = info.assignedId();
510 }
511
512 if (std::optional<int32_t> oooId = bd_op.getOutOfOrderId())
513 f.out_of_order_id = *oooId;
514
515 if (std::optional<AIE::BDIterationAttr> iter = bd_op.getIteration())
516 f.iteration_current = iter->getCurrent();
517
518 // AcquireGreaterEqual is encoded negated, so a runtime one must be >= 1:
519 // a field of 0 would mean "acquire == 0".
520 auto lockValue = [&](AIE::UseLockOp op, int32_t &constOut,
521 Value &valueOut) -> LogicalResult {
522 if (std::optional<int64_t> c = getConstantIntValue(op.getValue())) {
523 constOut = op.acquireGE() ? -static_cast<int32_t>(*c)
524 : static_cast<int32_t>(*c);
525 return success();
526 }
527 Location loc = op.getLoc();
528 int32_t minValue = op.acquireGE() ? 1 : 0;
529 int32_t maxValue = target_model.getMaxLockValue();
530 Value fromMin = builder.createOrFold<arith::SubIOp>(
531 loc, op.getValue(), createConstantI32(builder, loc, minValue));
532 Value inRange = builder.createOrFold<arith::CmpIOp>(
533 loc, arith::CmpIPredicate::ule, fromMin,
534 createConstantI32(builder, loc, maxValue - minValue));
535 if (failed(emitRuntimeCheck(builder, loc, inRange,
536 "a runtime lock value must be in [" +
537 Twine(minValue) + ":" + Twine(maxValue) +
538 "]")))
539 return failure();
540 valueOut = op.getValue();
541 if (op.acquireGE())
542 valueOut = arith::SubIOp::create(
543 builder, loc, createConstantI32(builder, loc, 0), valueOut);
544 return success();
545 };
546
547 AIE::UseLockOp acquire_op, release_op;
548 if (failed(
549 AIE::verifyBdLockPair(block, outOfOrder, acquire_op, release_op)))
550 return failure();
551 if (release_op) {
552 AIE::LockOp rel_lock = release_op.getLockOp();
553
554 // Acquire is optional under out-of-order (release-only completion).
555 if (acquire_op) {
556 AIE::LockOp acq_lock = acquire_op.getLockOp();
557 if (std::optional<int32_t> acqLockId = acq_lock.getLockID()) {
558 f.lock_acq_id = *acqLockId;
559 if (failed(lockValue(acquire_op, f.lock_acq_val, f.lock_acq_val_val)))
560 return failure();
561 f.lock_acq_enable = 1;
562 }
563 }
564
565 if (std::optional<int32_t> relLockId = rel_lock.getLockID()) {
566 f.lock_rel_id = *relLockId;
567 if (failed(lockValue(release_op, f.lock_rel_val, f.lock_rel_val_val)))
568 return failure();
569 }
570
571 // For memtile, add lock offset using getLockLocalBaseIndex. This matches
572 // AIERT.cpp implementation.
573 if (target_model.isMemTile(tile.getCol(), tile.getRow())) {
574 auto lockOffset = target_model.getLockLocalBaseIndex(
575 tile.getCol(), tile.getRow(), rel_lock.colIndex(),
576 rel_lock.rowIndex());
577 if (lockOffset && acquire_op &&
578 acquire_op.getLockOp().getLockID().has_value())
579 f.lock_acq_id += lockOffset.value();
580 if (lockOffset && rel_lock.getLockID().has_value())
581 f.lock_rel_id += lockOffset.value();
582 }
583 }
584 return f;
585 }
586
587 // Dynamic (runtime SSA size/stride/len/bd_id) BD lowering, the dma_task
588 // sibling of DmaToNpuPattern::lowerDynamic. Reaching this function at all
589 // means the design targets the EmitC (C++ TXN) builder, never the static
590 // binary target: a design meant for the binary target is unrolled to
591 // all-constant before this pass runs. Scope (a tile type with a BD register
592 // layout, no padding, realizability) is enforced by the caller;
593 // buildBdWords picks the layout from the tile.
594 LogicalResult
595 rewriteSingleBDDynamic(OpBuilder &builder, Block &block, AIE::DMABDOp bd_op,
596 AIE::TileOp &tile,
597 std::optional<xilinx::AIE::PacketInfoAttr> packet,
598 Value runtimeBdId = nullptr, bool outOfOrder = false) {
599 const auto &target_model = AIE::getTargetModel(bd_op);
600 Location loc = bd_op.getLoc();
601 int col = tile.getCol();
602 int row = tile.getRow();
603
604 auto fieldsOr = gatherBdTemplateFields(builder, block, bd_op, tile,
605 target_model, packet, outOfOrder);
606 if (failed(fieldsOr))
607 return failure();
608 // failed() above guards the deref; FailureOr hides the std::optional base's
609 // has_value(), so the checker cannot see the guard (suppressed below).
610 BdTemplateFields f = *fieldsOr; // NOLINT
611
612 // The bd_id as an OpFoldResult: the runtime pool value if present, else the
613 // pinned constant attribute. On the non-runtime path verifyBdInBlock (run
614 // for every block before any lowering, with hasRuntimeBdId == !runtimeBdId)
615 // has already required the attribute, so the access is guaranteed set here.
616 OpFoldResult bdIdOfr;
617 if (runtimeBdId) {
618 bdIdOfr = runtimeBdId;
619 } else {
620 std::optional<uint32_t> staticBdId = bd_op.getBdId();
621 assert(staticBdId && "static bd_id must be assigned (checked by "
622 "verifyBdInBlock before lowering)");
623 bdIdOfr = builder.getI32IntegerAttr(*staticBdId);
624 }
625
626 // Normalize sizes/strides to a 4-element outermost-first mixed list (the
627 // shared emitter's contract, matching memcpy_nd's always-4D operands),
628 // padding absent leading dims with size 1 / stride 0. dma_bd's mixed lists
629 // are outermost-first and variable-length (0..4 dims). A BD with no dims is
630 // a contiguous scan, so its innermost stride is 1, which keeps it linear
631 // like the static path.
632 SmallVector<OpFoldResult> sizes(bd_op.getMixedSizes());
633 SmallVector<OpFoldResult> strides(bd_op.getMixedStrides());
634 if (sizes.size() > 4 || strides.size() > 4)
635 return bd_op->emitOpError(
636 "At most four data layout transformation dimensions may be "
637 "provided; aie-decompose-large-dma-bd splits a longer one.");
638 OpFoldResult one = builder.getI64IntegerAttr(1);
639 OpFoldResult zeroOfr = builder.getI64IntegerAttr(0);
640 SmallVector<OpFoldResult, 4> sizes4(4, one), strides4(4, zeroOfr);
641 if (strides.empty())
642 strides4.back() = one;
643 for (size_t i = 0; i < sizes.size(); i++) {
644 sizes4[4 - sizes.size() + i] = sizes[i];
645 strides4[4 - strides.size() + i] = strides[i];
646 }
647 // The iteration attribute takes the outermost (iteration) slot, which
648 // verifyTaskBDDimensions has kept free of sizes/strides.
649 if (std::optional<AIE::BDIterationAttr> iter = bd_op.getIteration()) {
650 sizes4.front() = builder.getI64IntegerAttr(iter->getSize());
651 strides4.front() = builder.getI64IntegerAttr(iter->getStride());
652 }
653 // len as OpFoldResult: the runtime operand if present, else the static_len
654 // attr (a constant BD with runtime sizes/strides still reaches here). With
655 // runtime dims and no len, the transfer is their d0*d1*d2 extent; with
656 // constant dims or none, it is the whole buffer, as on the static path.
657 // That default needs a constant offset, or it could run past the buffer.
658 OpFoldResult lenOfr;
659 if (Value lenOperand = bd_op.getLen())
660 lenOfr = lenOperand;
661 else if (std::optional<int32_t> constLen = bd_op.getConstantLen())
662 lenOfr = builder.getI32IntegerAttr(*constLen);
663 else if (bd_op.getSizes().empty() && bd_op.getStrides().empty()) {
664 auto bufTy = cast<BaseMemRefType>(bd_op.getBuffer().getType());
665 if (!bufTy.hasStaticShape() ||
666 (bd_op.getOffset() && !bd_op.getConstantOffset()))
667 return bd_op->emitOpError(
668 "runtime-valued BD requires an explicit transfer length; provide a "
669 "`len` for this buffer descriptor.");
670 lenOfr = builder.getI64IntegerAttr(bufTy.getNumElements());
671 }
672
673 // As on the static path, the lowest three dims (d3 repeats the BD) must
674 // cover a constant length exactly.
675 std::optional<int64_t> constLen =
676 lenOfr ? getConstantIntValue(lenOfr) : std::nullopt;
677 std::optional<int64_t> dimsLen = 1;
678 for (OpFoldResult s : llvm::drop_begin(sizes4)) {
679 std::optional<int64_t> c = getConstantIntValue(s);
680 if (!c) {
681 dimsLen.reset();
682 break;
683 }
684 *dimsLen *= *c;
685 }
686 uint64_t elemWidth =
687 static_cast<uint64_t>(bd_op.getBufferElementTypeWidthInBytes()) * 8;
688 if (!sizes.empty() && constLen && dimsLen && *dimsLen != *constLen) {
689 auto err = bd_op->emitOpError(
690 "Buffer descriptor length does not match length of "
691 "transfer expressed by lowest three dimensions of data "
692 "layout transformation strides/wraps. ")
693 << "BD length is " << (*constLen * elemWidth / 8)
694 << " bytes. Lowest three dimensions of data layout "
695 "transformation would result in transfer of "
696 << (*dimsLen * elemWidth / 8) << " bytes. ";
697 err.attachNote() << "Do not include the highest dimension size in "
698 "transfer length, as this is the BD repeat count.";
699 return failure();
700 }
701
702 // A BD without addressing dims, or whose dims are a unit-stride d0 alone,
703 // is a plain linear transfer of `len` elements (canonicalization strips a
704 // contiguous [1, 1, 1, N] to this form, and the static path encodes it
705 // linear from `len` alone), so give it exactly that shape. With any other
706 // dims, `len` must agree with their d0*d1*d2 extent.
707 bool linear =
708 sizes.empty() ||
709 (lenOfr && isConstantIntValue(strides4[3], 1) &&
710 llvm::all_of(ArrayRef(sizes4).drop_back(),
711 [](OpFoldResult v) { return isConstantIntValue(v, 1); }));
712 if (linear) {
713 sizes4[3] = lenOfr;
714 strides4[3] = one;
715 }
716
717 // The BD-level repeat_count (encoder output) is unused here: the dma_task
718 // queue push is emitted separately by DMAStartTaskOpPattern from the task
719 // op's repeat_count, not the BD's outer dim.
720 SmallVector<Value> bdWords;
721 Value bdRepeatCount;
722 if (failed(buildBdWords(
723 builder, loc, target_model, col, row, f, sizes4, strides4,
724 elemWidth, bd_op.getBurstLength(), bd_op.getAxcacheOrDefault(),
725 linear ? OpFoldResult() : lenOfr, bdRepeatCount, bdWords)))
726 return failure();
727
728 // The walk must stay inside the host buffer it reads or writes.
729 if (auto trace = traceSubviewToBlockArgument(bd_op.getBuffer())) {
730 OpFoldResult offsetOfr =
731 bd_op.getOffset() ? OpFoldResult(bd_op.getOffset())
732 : OpFoldResult(builder.getI32IntegerAttr(
733 bd_op.getConstantOffset().value_or(0)));
734 if (failed(guardWithinHostBuffer(
735 builder, loc, cast<BaseMemRefType>(trace->rootArg.getType()),
736 trace->offsetInBytes, elemWidth / 8, {offsetOfr}, {one}, sizes4,
737 strides4)))
738 return failure();
739 }
740
741 // One blockwrite carries the whole register block for BD configuration.
742 Value bdBase =
743 getBdRegisterBase(builder, loc, target_model, col, row, bdIdOfr);
744 NpuBlockWriteValuesOp::create(builder, loc, bdBase, bdWords);
745 return setAddressForSingleBD(builder, bd_op, tile, bdIdOfr);
746 }
747
748 LogicalResult
749 rewriteSingleBD(OpBuilder &builder, Block &block, AIE::TileOp &tile,
750 AIE::DMAChannelDir channelDir,
751 std::optional<xilinx::AIE::PacketInfoAttr> packet,
752 bool outOfOrder = false) {
753 AIE::DMABDOp bd_op = getBdForBlock(block);
754 Value runtimeBdId = bd_op.getBdIdVal();
755 const auto &target_model = AIE::getTargetModel(bd_op);
756 auto buffer_type = llvm::cast<BaseMemRefType>(bd_op.getBuffer().getType());
757 uint32_t addr_granularity = target_model.getAddressGenGranularity();
758
759 // Runtime (SSA) sizes/strides/len/offset take the dynamic BD-word encoder
760 // path; a runtime bd_id (dynamic free-list pool) also forces it, since the
761 // BD register addresses are then runtime and cannot fold into a blockwrite,
762 // and so does a runtime next_bd, which is a runtime field in the words.
763 // A fully-constant descriptor with a pinned bd_id takes the static path
764 // below unchanged. The encoder covers every tile type that has a BD
765 // register layout (see buildBdWords), so anything the dynamic path can't
766 // represent stays a clean diagnostic.
767 bool runtimeLen = bd_op.getLen() && !bd_op.getConstantLen();
768 bool runtimeOffset = bd_op.getOffset() && !bd_op.getConstantOffset();
769 bool runtimeDims =
770 llvm::any_of(bd_op.getMixedSizes(),
771 [](OpFoldResult s) { return !getConstantIntValue(s); }) ||
772 llvm::any_of(bd_op.getMixedStrides(),
773 [](OpFoldResult s) { return !getConstantIntValue(s); });
774 bool runtimeNextBd = runtimeNextBdId.contains(bd_op);
775 bool runtimeLock =
776 llvm::any_of(block.getOps<AIE::UseLockOp>(), [](AIE::UseLockOp op) {
777 return !getConstantIntValue(op.getValue());
778 });
779 if (runtimeLen || runtimeDims || runtimeOffset || runtimeBdId ||
780 runtimeNextBd || runtimeLock) {
781 // The verifier leaves a runtime offset, bd_id, next_bd or lock value as
782 // the only ways here. An offset that depends on a runtime-sequence loop
783 // is constant by now if the loop was unrolled.
784 if (bd_op.getLengthStateTableIdxAttr())
785 return bd_op->emitOpError("length_parameter requires a constant "
786 "offset, bd_id, next_bd and lock value");
787 int col = tile.getCol(), row = tile.getRow();
788 if (!target_model.isShimNOCTile(col, row) &&
789 !target_model.isMemTile(col, row) &&
790 !target_model.isCoreTile(col, row))
791 return bd_op->emitOpError(
792 "runtime-valued BD size/stride/len/bd_id/next_bd/lock value is "
793 "only supported on tiles with a DMA buffer descriptor layout (shim "
794 "NOC, mem and "
795 "core tiles); use compile-time constants here.");
796 if (bd_op.getPadDimensions().has_value())
797 return bd_op->emitOpError(
798 "zero padding is not supported with runtime sizes/strides/len.");
799 // Realizability of the constant size/stride operands (runtime ones are
800 // guarded at lowering by the shared encoder). Mixed lists are
801 // outermost-first; the helper wants innermost-first.
802 uint64_t elemWidth =
803 static_cast<uint64_t>(bd_op.getBufferElementTypeWidthInBytes()) * 8;
804 SmallVector<OpFoldResult, 4> sizesRev(
805 llvm::reverse(bd_op.getMixedSizes()));
806 SmallVector<OpFoldResult, 4> stridesRev(
807 llvm::reverse(bd_op.getMixedStrides()));
809 bd_op, sizesRev, stridesRev, elemWidth,
810 target_model.getAddressGenGranularity())))
811 return failure();
812 return rewriteSingleBDDynamic(builder, block, bd_op, tile, packet,
813 runtimeBdId, outOfOrder);
814 }
815
816 // Static path: bd_id is a pinned attribute (the dynamic/runtime-bd_id path
817 // returned above) and the offset is constant (runtime offset routed above).
818 // verifyBdInBlock (run for every block before lowering, with the static
819 // path implying hasRuntimeBdId == false) has already required the
820 // attribute.
821 std::optional<uint32_t> bdIdAttr = bd_op.getBdId();
822 assert(bdIdAttr && "static bd_id must be assigned (checked by "
823 "verifyBdInBlock before lowering)");
824 uint32_t bd_id = *bdIdAttr;
825 int64_t offset = bd_op.getOffsetInBytes();
826 uint64_t len = bd_op.getLenInBytes();
827 uint64_t len_addr_granularity = len * 8 / addr_granularity;
828
829 if (offset * 8 % addr_granularity != 0) {
830 return bd_op->emitOpError("Offset must be aligned to ")
831 << (addr_granularity / 8) << " byte boundary.";
832 }
833
834 // A runtime length may start from zero: the update below adds to it.
835 if (len < addr_granularity / 8 && !bd_op.getLengthStateTableIdxAttr()) {
836 return bd_op->emitOpError("Transfer size of ")
837 << len << " bytes falls below minimum hardware transfer unit of "
838 << (addr_granularity / 8) << " bytes.";
839 }
840 // The owning storage must outlive the ArrayRef view below.
841 std::optional<llvm::SmallVector<AIE::BDDimLayoutAttr>> dimsStorage =
842 bd_op.getConstantDimensions();
843 if (!dimsStorage)
844 return bd_op->emitOpError("internal error folding BD dimensions");
845 // The iteration attribute rides in the outermost (iteration) slot of the
846 // dimensioned form, so a BD without dims becomes one contiguous dimension.
847 std::optional<AIE::BDIterationAttr> iter = bd_op.getIteration();
848 if (iter && dimsStorage->empty())
849 dimsStorage->push_back(AIE::BDDimLayoutAttr::get(
850 builder.getContext(), len / bd_op.getBufferElementTypeWidthInBytes(),
851 1));
852 std::optional<llvm::ArrayRef<AIE::BDDimLayoutAttr>> dims;
853 if (!dimsStorage->empty())
854 dims = llvm::ArrayRef<AIE::BDDimLayoutAttr>(*dimsStorage);
855 llvm::SmallVector<int64_t, 4> sizes = llvm::SmallVector<int64_t, 4>(4, 0);
856 llvm::SmallVector<int64_t, 4> strides = llvm::SmallVector<int64_t, 4>(4, 0);
857
858 // Padding
859 std::optional<llvm::ArrayRef<AIE::BDPadLayoutAttr>> padDims =
860 bd_op.getPadDimensions();
861 llvm::SmallVector<int64_t, 4> padBefore =
862 llvm::SmallVector<int64_t, 4>(4, 0);
863 llvm::SmallVector<int64_t, 4> padAfter =
864 llvm::SmallVector<int64_t, 4>(4, 0);
865 llvm::fill(padBefore, 0);
866 llvm::fill(padAfter, 0);
867
868 auto d0size = 0;
869 auto d0stride = 0;
870 auto d1size = 0;
871 auto d1stride = 0;
872 auto d2size = 0;
873 auto d2stride = 0;
874 auto iteration_size = 0;
875 auto iteration_stride = 0;
876
877 if (dims && !dims->empty()) {
878 llvm::SmallVector<int64_t, 4> input_sizes =
879 llvm::SmallVector<int64_t, 4>(4, 1);
880 llvm::SmallVector<int64_t, 4> input_strides =
881 llvm::SmallVector<int64_t, 4>(4, 0);
882 if (dims->size() > 4) {
883 return bd_op->emitOpError(
884 "At most four data layout transformation dimensions may be "
885 "provided; aie-decompose-large-dma-bd splits a longer one.");
886 }
887
888 for (size_t i = 0; i < dims->size(); i++) {
889 // Pass down dimensions in reverse order; in the MLIR, this allows
890 // us to specify step sizes/wraps in the same order as we would
891 // access a multi-dim C array, with the highest dimension first.
892 int j = dims->size() - i - 1;
893 input_sizes[i] = (*dims)[j].getSize();
894 input_strides[i] = (*dims)[j].getStride();
895 }
896 if (iter) {
897 input_sizes[3] = iter->getSize();
898 input_strides[3] = iter->getStride();
899 }
900
901 // d3 (repeat) is excluded; a repeated linear transfer is still linear.
902 // A contiguous row-major ND access on a shim NOC tile is also lowered
903 // using the wide buffer_length register, exempt from the 10-bit ND
904 // wrap-size limit. Canonicalization zeroes size-1 strides before this
905 // pass runs, so isContiguousTransfer is sufficient.
906 bool treatAsLinear =
907 isLinearModeTransfer(target_model, tile.getCol(), tile.getRow(),
908 input_sizes, input_strides);
909 if (!treatAsLinear && bd_op.getLengthStateTableIdxAttr())
910 AIE::placeRuntimeLengthDimension(input_sizes, input_strides);
911
912 if (padDims.has_value()) {
913 if (!target_model.isMemTile(tile.getCol(), tile.getRow()))
914 return bd_op->emitOpError()
915 << "Padding is only supported by memtile dma bds.";
916 if (padDims->size() > dims->size())
917 return bd_op->emitOpError()
918 << "Mismatch number of dimensions between padding(s)"
919 << " and wrap(s) and stride(s).";
920 if (channelDir == AIE::DMAChannelDir::MM2S) {
921 for (size_t i = 0; i < padDims->size(); i++) {
922 int j = padDims->size() - i - 1;
923 padBefore[i] = (*padDims)[j].getConstPadBefore();
924 padAfter[i] = (*padDims)[j].getConstPadAfter();
925 }
926 for (size_t i = padDims->size(); i < dims->size(); i++) {
927 padBefore[i] = 0;
928 padAfter[i] = 0;
929 }
930 } else
931 return bd_op->emitOpError()
932 << "supports padding only for MM2S direction on MemTiles.";
933 }
934 getHardwareStridesWraps(target_model, bd_op, buffer_type, input_sizes,
935 input_strides, sizes, strides);
936
937 if (failed(verifyStridesWraps(
938 bd_op, [&] { return bd_op->emitOpError(); }, buffer_type,
939 tile.getCol(), tile.getRow(), input_sizes, input_strides, sizes,
940 strides, treatAsLinear))) {
941 return failure();
942 }
943
944 iteration_size = sizes[3];
945 iteration_stride = strides[3];
946
947 if (!treatAsLinear) {
948 // d0_size, d0_stride
949 d0size = sizes[0];
950 d0stride = strides[0];
951
952 // d1_size, d1_stride
953 d1size = sizes[1];
954 d1stride = strides[1];
955
956 // d2_stride
957 d2stride = strides[2];
958
959 // TODO: d2_size is a dead field; AIEDmaToNpu.cpp memtile word-packing
960 // never writes it (see its `// TODO: D2Size`); the real D2 repeat
961 // count is carried entirely by buffer_length, same as on shim tiles.
962 d2size = (target_model.isMemTile(tile.getCol(), tile.getRow()))
963 ? sizes[2]
964 : 0;
965 }
966 if (input_sizes[3] > 1 && input_strides[3] == 0) {
967 // We allow users to encode the repeat_count as a dimension 3 stride
968 // of 0. This must lower to a iteration wrap of 0, so no stride is
969 // ever added. We then repeat the BD using the repeat_count in
970 // NpuPushQueueOp.
971 iteration_size = 0;
972 iteration_stride = 0;
973 }
974
975 // Ensure the total transfer length and the length expressed in the lowest
976 // three dimensions of strides/wraps agree. (Fourth dimension is
977 // iteration/repeat count and repeats the whole BD, so should not be
978 // incorporated in length of a single BD invocation.)
979 uint64_t len_dims_addr_granularity = 1;
980 for (size_t i = 0; i < 3; i++) {
981 len_dims_addr_granularity *= sizes[i];
982 }
983 // A runtime length from zero keeps the pattern for the units it adds.
984 bool runtimeLengthFromZero =
985 len_addr_granularity == 0 && bd_op.getLengthStateTableIdxAttr();
986 if (len_dims_addr_granularity != len_addr_granularity &&
987 !runtimeLengthFromZero) {
988 auto err =
989 bd_op->emitOpError(
990 "Buffer descriptor length does not match length of transfer "
991 "expressed by lowest three dimensions of data layout "
992 "transformation strides/wraps. ")
993 << "BD length is " << (len_addr_granularity * addr_granularity / 8)
994 << " bytes. "
995 << "Lowest three dimensions of data layout transformation would "
996 "result in transfer of "
997 << (len_dims_addr_granularity * addr_granularity / 8) << " bytes. ";
998 err.attachNote() << "Do not include the highest dimension size in "
999 "transfer length, as this is the BD repeat count.";
1000 return failure();
1001 }
1002 } else {
1003 if (padDims && target_model.isMemTile(tile.getCol(), tile.getRow()) &&
1004 channelDir == AIE::DMAChannelDir::MM2S) {
1005 return bd_op->emitOpError()
1006 << "Padding requires n-d data layouts expressed as "
1007 << "wrap(s) and stride(s).";
1008 }
1009 if (padDims) {
1010 return bd_op->emitOpError()
1011 << " Padding is supported only on MemTiles.";
1012 }
1013 }
1014 auto fieldsOr = gatherBdTemplateFields(builder, block, bd_op, tile,
1015 target_model, packet, outOfOrder);
1016 if (failed(fieldsOr))
1017 return failure();
1018 // failed() above guards the deref; see note in rewriteSingleBDDynamic.
1019 BdTemplateFields f = *fieldsOr; // NOLINT
1020
1021 NpuWriteBdOp::create(
1022 builder, bd_op.getLoc(), tile.getCol(), bd_id, len_addr_granularity,
1023 offset,
1024 /*enable_packet=*/f.enable_packet,
1025 /*out_of_order_id=*/f.out_of_order_id,
1026 /*packet_id=*/f.packet_id,
1027 /*packet_type=*/f.packet_type,
1028 /*d0_size=*/d0size, /*d0_stride=*/d0stride,
1029 /*d1_size=*/d1size, /*d1_stride=*/d1stride,
1030 /*d2_size=*/d2size, /*d2_stride=*/d2stride,
1031 /*iteration_current=*/f.iteration_current,
1032 /*iteration_size=*/iteration_size,
1033 /*iteration_stride=*/iteration_stride,
1034 /*next_bd=*/f.next_bd_id,
1035 /*row=*/tile.getRow(),
1036 /*use_next_bd=*/f.use_next_bd,
1037 /*valid_bd=*/1,
1038 /*lock_rel_val=*/f.lock_rel_val, /*lock_rel_id=*/f.lock_rel_id,
1039 /*lock_acq_enable=*/f.lock_acq_enable,
1040 /*lock_acq_val=*/f.lock_acq_val, /*lock_acq_id=*/f.lock_acq_id,
1041 /*d0_zero_before=*/padBefore[0],
1042 /*d1_zero_before=*/padBefore[1], /*d2_zero_before=*/padBefore[2],
1043 /*d0_zero_after=*/padAfter[0], /*d1_zero_after=*/padAfter[1],
1044 /*d2_zero_after=*/padAfter[2],
1045 /*burst_length=*/bd_op.getBurstLength(),
1046 /*axcache=*/
1047 target_model.isShimNOCTile(tile.getCol(), tile.getRow())
1048 ? builder.getI32IntegerAttr(bd_op.getAxcacheOrDefault())
1049 : IntegerAttr());
1050 if (failed(setAddressForSingleBD(builder, bd_op, tile)))
1051 return failure();
1052
1053 // A length_state_table_idx adds the runtime length to the BD's
1054 // Buffer_Length, which the BD write has already set to the static length.
1055 // The verifier requires a length_unit with it.
1056 auto lengthUnit = bd_op.getLengthUnit();
1057 if (bd_op.getLengthStateTableIdxAttr() && lengthUnit)
1059 builder, bd_op, buffer_type, *lengthUnit, target_model, tile.getCol(),
1060 tile.getRow(), bd_id);
1061 return success();
1062 }
1063
1064 LogicalResult hoistNextBdOpsIntoAttrs(DMAConfigureTaskOp op) {
1065 Region &body = op.getBody();
1066 for (auto &block : body) {
1067 if (shouldSkipBlock(block)) {
1068 continue;
1069 }
1070 AIE::DMABDOp bd_op = getBdForBlock(block);
1071 if (AIE::NextBDOp next_bd_op =
1072 llvm::dyn_cast<AIE::NextBDOp>(block.getTerminator())) {
1073 if (bd_op.getNextBdId().has_value()) {
1074 auto error =
1075 bd_op.emitOpError("Cannot specify both next_bd_id attribute and "
1076 "aie.next_bd operation.");
1077 error.attachNote(next_bd_op.getLoc())
1078 << "Potentially conflicting next buffer descriptor ID specified "
1079 "here.";
1080 return failure();
1081 }
1082 Block &next_bd_block = *next_bd_op.getDest();
1083 AIE::DMABDOp next_dma_bd_op = getBdForBlock(next_bd_block);
1084 // verifyBdInBlock has required a pinned or a runtime id on every BD.
1085 if (Value nextBdIdVal = next_dma_bd_op.getBdIdVal())
1086 runtimeNextBdId[bd_op] = nextBdIdVal;
1087 else
1088 bd_op.setNextBdId(next_dma_bd_op.getBdId());
1089 OpBuilder builder(next_bd_op);
1090 AIE::EndOp::create(builder, next_bd_op.getLoc());
1091 next_bd_op.erase();
1092 }
1093 }
1094 return success();
1095 }
1096
1097 LogicalResult emitOutOfOrderChannelEnable(OpBuilder &builder,
1098 DMAConfigureTaskOp op,
1099 AIE::TileOp tile) {
1101 int col = tile.getCol();
1102 int row = tile.getRow();
1103 int channel = op.getChannel();
1104
1105 uint32_t ctrlAddrLocal = tm.getLocalDmaControlAddress(
1106 col, row, channel, AIE::DMAChannelDir::S2MM);
1107
1108 std::string ctrlRegName = "DMA_S2MM_" + std::to_string(channel) + "_Ctrl";
1109 // DMA control regs live in the memory module; memtile ignores isMem, shim
1110 // is rejected by the verifier.
1111 const AIE::RegisterInfo *ctrlReg =
1112 tm.lookupRegister(ctrlRegName, tile.getTileID(), /*isMem=*/true);
1113 if (!ctrlReg)
1114 return op.emitOpError("target has no ")
1115 << ctrlRegName << " register in its register database";
1116 const AIE::BitFieldInfo *oooField =
1117 ctrlReg->getField("Enable_Out_of_Order");
1118 if (!oooField)
1119 return op.emitOpError()
1120 << ctrlRegName
1121 << " has no Enable_Out_of_Order field in the register database";
1122 std::optional<uint32_t> oooMask = tm.getFieldMask(*oooField);
1123 if (!oooMask)
1124 return op.emitOpError()
1125 << ctrlRegName
1126 << " Enable_Out_of_Order field does not fit in a 32-bit register";
1127 std::optional<uint32_t> oooValue = tm.encodeFieldValue(*oooField, 1);
1128 if (!oooValue)
1129 return op.emitOpError()
1130 << ctrlRegName
1131 << " Enable_Out_of_Order field value does not fit in its bit "
1132 "width";
1133
1134 Location loc = op.getLoc();
1135 IntegerAttr colAttr = builder.getI32IntegerAttr(col);
1136 IntegerAttr rowAttr = builder.getI32IntegerAttr(row);
1137 Value addr = createConstantI32(builder, loc, ctrlAddrLocal);
1138 Value val = createConstantI32(builder, loc, *oooValue);
1139 Value mask = createConstantI32(builder, loc, *oooMask);
1140 NpuMaskWrite32Op::create(builder, loc, addr, val, mask, nullptr, colAttr,
1141 rowAttr);
1142 return success();
1143 }
1144
1146 DMAConfigureTaskOp op,
1147 const llvm::DenseMap<Operation *, bool> &onlyPushedOnce) {
1148 OpBuilder builder(op);
1149 AIE::TileOp tile = op.tryGetTileOp();
1150 if (!tile)
1151 return emitUnplacedTileError(op, op);
1152
1153 if (!op.use_empty()) {
1154 auto err = op.emitOpError("Cannot lower while op still has uses.");
1155 mlir::Operation::use_range uses = op.getOperation()->getUses();
1156 for (auto &use : uses) {
1157 err.attachNote(use.getOwner()->getLoc()) << "Used here.";
1158 }
1159 return failure();
1160 }
1161
1162 Region &body = op.getBody();
1164
1165 // Verify each BD block first; subsequent functions rely on them being
1166 // well-formed
1167 for (auto &it : body) {
1168 if (shouldSkipBlock(it)) {
1169 continue;
1170 }
1171 if (failed(verifyNoUnsupportedOpsInBlock(it))) {
1172 return failure();
1173 }
1174 if (failed(verifyBdInBlock(it))) {
1175 return failure();
1176 }
1177 AIE::UseLockOp acquire, release;
1178 if (failed(
1179 AIE::verifyBdLockPair(it, op.getOutOfOrder(), acquire, release)))
1180 return failure();
1181 }
1182
1183 if (op.getOutOfOrder()) {
1184 if (failed(emitOutOfOrderChannelEnable(builder, op, tile)))
1185 return failure();
1186 }
1187
1188 // Hoist next_bd operations into next_bd_id attribute of the dma_bd
1189 runtimeNextBdId.clear();
1190 if (failed(hoistNextBdOpsIntoAttrs(op))) {
1191 return failure();
1192 }
1193
1194 auto channelDir = op.getDirection();
1195 auto packet = op.getPacket();
1196
1197 // One push onto the task queue runs one iteration of a BD's iteration
1198 // dimension; only a push's repeat_count runs the rest. Warn once the
1199 // BDs are known to lower, so an invalid one gets only its error.
1200 SmallVector<std::pair<Location, int64_t>> unrepeatedIterations;
1201 if (onlyPushedOnce.lookup(op)) {
1202 for (auto bd_op : body.getOps<AIE::DMABDOp>()) {
1203 SmallVector<OpFoldResult> sizes = bd_op.getMixedSizes();
1204 std::optional<int64_t> iterations;
1205 if (std::optional<AIE::BDIterationAttr> iter = bd_op.getIteration())
1206 iterations = iter->getSize();
1207 else if (sizes.size() == 4)
1208 iterations = getConstantIntValue(sizes.front());
1209 if (iterations && *iterations > 1)
1210 unrepeatedIterations.emplace_back(bd_op.getLoc(), *iterations);
1211 }
1212 }
1213
1214 // Lower all BDs
1215 for (auto &block : body) {
1216 if (shouldSkipBlock(block)) {
1217 continue;
1218 }
1219 if (failed(rewriteSingleBD(builder, block, tile, channelDir, packet,
1220 op.getOutOfOrder()))) {
1221 return failure();
1222 }
1223 }
1224
1225 for (auto [loc, iterations] : unrepeatedIterations)
1226 mlir::emitWarning(loc)
1227 << "iteration dimension of size " << iterations
1228 << " is pushed with repeat_count 0, so only its first iteration "
1229 "runs; set repeat_count = "
1230 << iterations - 1;
1231
1232 op.erase();
1233
1234 return success();
1235 }
1236
1238 AIE::DeviceOp device,
1239 const llvm::DenseMap<Operation *, bool> &onlyPushedOnce) {
1240 WalkResult result = device.walk([&](DMAConfigureTaskOp op) {
1241 if (failed(rewriteSingleDMAConfigureTaskOp(op, onlyPushedOnce))) {
1242 return WalkResult::interrupt();
1243 }
1244 return WalkResult::advance();
1245 });
1246 if (result.wasInterrupted()) {
1247 return failure();
1248 }
1249 return success();
1250 }
1251
1252 // Drop the dead task-index carries left by the dynamic BD pool path once
1253 // awaits have lowered to npu.sync. scf.for/scf.if carry the task value as a
1254 // result purely to hold the await's data dependence on the configure; with
1255 // the await gone that carry is dead but still counts as a use of the
1256 // branch-local configure, blocking its use_empty lowering. scf's own
1257 // canonicalizations remove dead iter-args/results and prune the yields
1258 // feeding them. Run them device-wide but with folding and constant-CSE
1259 // DISABLED, so the static path's byte-golden constant emission is untouched
1260 // -- only the dead scf carries go.
1261 //
1262 // scf's canonicalizations only see one level at a time: a task carried
1263 // through NESTED loops (an outer iter_arg feeding an inner loop's init) is
1264 // never provably dead to either loop in isolation, since each still sees a
1265 // genuine SSA use feeding the other -- even once the whole chain is
1266 // unobserved end to end. RemoveDeadValues performs the cross-region
1267 // liveness analysis (via RegionBranchOpInterface, generic over nesting
1268 // depth) needed to prune that fully; run it as a nested pipeline afterward
1269 // to pick up what the local canonicalizations left behind.
1270 LogicalResult dropDeadTaskCarries(AIE::DeviceOp device) {
1271 // Only a region-carried value can be a dead carry, and a static sequence
1272 // has none; the analysis costs time proportional to the whole device.
1273 if (llvm::none_of(device.getOps<AIE::RuntimeSequenceOp>(), [](auto seq) {
1274 return seq
1275 .walk([](RegionBranchOpInterface) {
1276 return WalkResult::interrupt();
1277 })
1278 .wasInterrupted();
1279 }))
1280 return success();
1281
1282 RewritePatternSet patterns(&getContext());
1283 scf::ForOp::getCanonicalizationPatterns(patterns, &getContext());
1284 scf::IfOp::getCanonicalizationPatterns(patterns, &getContext());
1285 GreedyRewriteConfig config;
1286 config.enableFolding(false).enableConstantCSE(false);
1287 if (failed(applyPatternsGreedily(device, std::move(patterns), config)))
1288 return failure();
1289
1290 OpPassManager pm(AIE::DeviceOp::getOperationName());
1291 pm.addPass(createRemoveDeadValuesPass());
1292 return runPipeline(pm, device);
1293 }
1294
1295 void runOnOperation() override {
1296 AIE::DeviceOp device = getOperation();
1297
1298 // Lower the starts and awaits. Each rewrites one op into ops that need no
1299 // further lowering, so one walk does it.
1300 RewritePatternSet patterns(&getContext());
1301 llvm::DenseMap<Operation *, bool> onlyPushedOnce;
1302 patterns.insert<DMAStartTaskOpPattern>(&getContext(), onlyPushedOnce);
1303 patterns.insert<DMAAwaitTaskOpPattern>(&getContext());
1304 walkAndApplyPatterns(device, std::move(patterns));
1305 // A start or await left unlowered still uses its configure, so lowering
1306 // the configures would only add errors.
1307 bool unlowered = false;
1308 device.walk([&](Operation *op) {
1309 if (isa<DMAStartTaskOp, DMAAwaitTaskOp>(op)) {
1310 op->emitError() << "failed to legalize operation '" << op->getName()
1311 << "'";
1312 unlowered = true;
1313 }
1314 });
1315 if (unlowered)
1316 return signalPassFailure();
1317
1318 // Drop the now-dead task-index carries the awaits held, so the branch-local
1319 // configures they used can reach use_empty and lower below.
1320 if (failed(dropDeadTaskCarries(device)))
1321 signalPassFailure();
1322
1323 // Lower the configuration for the BDs
1324 if (failed(rewriteDMAConfigureTaskOp(device, onlyPushedOnce))) {
1325 signalPassFailure();
1326 }
1327
1328 eraseDeadArith(device);
1329 }
1330};
1331
1332std::unique_ptr<OperationPass<AIE::DeviceOp>>
1334 return std::make_unique<AIEDMATasksToNPUPass>();
1335}
std::optional< uint32_t > encodeFieldValue(const BitFieldInfo &field, uint32_t value) const
Encode a field value with proper bit shifting.
std::optional< uint32_t > getMemLocalBaseAddress(int localCol, int localRow, int memCol, int memRow) const
Return the memory base address (or offset) in the local tile when accessing a neighbor's memory or an...
uint32_t getLocalDmaControlAddress(int col, int row, int channel, AIE::DMAChannelDir direction) const
Return the DMA task-queue register address relative to its tile.
bool isCoreTile(int col, int row) const
Return true if the given tile is a Core tile.
virtual const DmaBdLayout * getDmaBdLayout(AIETileType tileType) const
Return the register layout of a buffer descriptor on the given tile type, or null if the target has n...
bool isMemTile(int col, int row) const
Return true if the given tile is a Mem tile.
std::optional< uint32_t > getLockLocalBaseIndex(int localCol, int localRow, int lockCol, int lockRow) const
Return the lock base index (or offset) in the local tile when accessing a neighbor's lock or an empty...
std::optional< uint32_t > getFieldMask(const BitFieldInfo &field) const
Compute a 32-bit mask for a register field.
const RegisterInfo * lookupRegister(llvm::StringRef name, TileID tile, bool isMem=false) const
Register Database API - provides access to register and event information for trace configuration and...
virtual uint64_t getDmaBdAddress(int col, int row, uint32_t bd_id, int channel=-1, AIE::DMAChannelDir direction=AIE::DMAChannelDir::MM2S) const =0
Return the array address of the dma buffer descriptor for the given col, row, buffer descriptor id,...
virtual uint32_t getDmaBdAddressOffset(int col, int row) const =0
Return the offset of the base address field within the shim dma buffer descriptor.
bool isShimNOCTile(int col, int row) const
Return true if the given tile is a ShimNOC tile.
virtual uint32_t getMaxLockValue() const =0
Return the maximum value that can be stored in a lock register.
virtual uint32_t getAddressGenGranularity() const =0
Return the data bus width of the device.
mlir::LogicalResult verifyStridesWraps(mlir::Operation *forOp, llvm::function_ref< mlir::InFlightDiagnostic()> emitError, mlir::BaseMemRefType referencedBufType, int tileCol, int tileRow, llvm::SmallVector< int64_t, 4 > inputSizes, llvm::SmallVector< int64_t, 4 > inputStrides, llvm::SmallVector< int64_t, 4 > hardwareSizes, llvm::SmallVector< int64_t, 4 > hardwareStrides, bool skipTransformationChecks=false)
DMAConfigureTaskOp getUniqueReachableConfigure(Value task)
Definition AIEUtils.cpp:354
std::optional< SubviewTraceResult > traceSubviewToBlockArgument(Value value)
Definition AIEUtils.cpp:18
mlir::Value buildArgPlusValue(mlir::OpBuilder &builder, mlir::Location loc, llvm::ArrayRef< mlir::OpFoldResult > elementOffsets, llvm::ArrayRef< mlir::OpFoldResult > strides, int64_t elemWidthBytes, int64_t baseByteOffset, uint32_t granuleBytes)
mlir::Value getAsValue(mlir::OpBuilder &builder, mlir::Location loc, mlir::OpFoldResult ofr, mlir::Type intType)
mlir::LogicalResult guardWithinHostBuffer(mlir::OpBuilder &builder, mlir::Location loc, mlir::BaseMemRefType hostBufferType, int64_t baseByteOffset, int64_t elemWidthBytes, llvm::ArrayRef< mlir::OpFoldResult > offsets, llvm::ArrayRef< mlir::OpFoldResult > offsetStrides, llvm::ArrayRef< mlir::OpFoldResult > sizes, llvm::ArrayRef< mlir::OpFoldResult > strides)
mlir::LogicalResult buildBdWords(mlir::OpBuilder &builder, mlir::Location loc, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow, const BdTemplateFields &fields, llvm::ArrayRef< mlir::OpFoldResult > mixedSizes, llvm::ArrayRef< mlir::OpFoldResult > mixedStrides, uint64_t elemWidth, uint32_t burstLength, uint32_t axcache, mlir::OpFoldResult lenElems, mlir::Value &repeatCountOut, llvm::SmallVectorImpl< mlir::Value > &wordsOut)
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIEDMATasksToNPUPass()
void eraseDeadArith(mlir::Operation *root)
void getHardwareStridesWraps(const AIE::AIETargetModel &targetModel, mlir::Operation *op, mlir::BaseMemRefType referencedBufType, llvm::SmallVector< int64_t, 4 > inputSizes, llvm::SmallVector< int64_t, 4 > inputStrides, llvm::SmallVector< int64_t, 4 > &sizes, llvm::SmallVector< int64_t, 4 > &strides)
bool isLinearModeTransfer(const AIE::AIETargetModel &targetModel, int col, int row, llvm::ArrayRef< int64_t > sizes, llvm::ArrayRef< int64_t > strides)
mlir::Value getBdRegisterBase(mlir::OpBuilder &builder, mlir::Location loc, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow, mlir::OpFoldResult bdId)
mlir::LogicalResult emitRuntimeCheck(mlir::OpBuilder &builder, mlir::Location loc, mlir::Value cond, const llvm::Twine &message)
mlir::LogicalResult verifyConstBdRealizability(mlir::Operation *op, llvm::ArrayRef< mlir::OpFoldResult > sizes, llvm::ArrayRef< mlir::OpFoldResult > strides, uint64_t elemWidth, uint32_t gran)
Definition BdLowering.h:77
bool getReachableConfigures(Value task, SmallVectorImpl< DMAConfigureTaskOp > &configures)
Definition AIEUtils.cpp:318
LogicalResult emitUpdateBdAddressFromOffsetParameter(OpBuilder &builder, Operation *bdOp, BaseMemRefType bufType, uint64_t registerAddr)
Definition AIEUtils.cpp:200
LogicalResult emitUpdateBdLengthFromParameter(OpBuilder &builder, Operation *bdOp, BaseMemRefType bufType, int64_t lengthUnit, const AIE::AIETargetModel &targetModel, int col, int row, int bdId)
Definition AIEUtils.cpp:221
std::optional< unsigned > getHostBufferArgIndex(BlockArgument arg)
Definition AIEUtils.cpp:142
mlir::Value createConstantI32(mlir::OpBuilder &builder, mlir::Location loc, uint32_t value)
mlir::LogicalResult verifyBdLockPair(mlir::Block &block, bool outOfOrder, UseLockOp &acquire, UseLockOp &release)
const AIETargetModel & getTargetModel(mlir::Operation *op)
void placeRuntimeLengthDimension(llvm::MutableArrayRef< int64_t > sizes, llvm::MutableArrayRef< int64_t > strides)
void runOnOperation() override
LogicalResult dropDeadTaskCarries(AIE::DeviceOp device)
bool shouldSkipBlock(Block &block)
LogicalResult rewriteSingleBDDynamic(OpBuilder &builder, Block &block, AIE::DMABDOp bd_op, AIE::TileOp &tile, std::optional< xilinx::AIE::PacketInfoAttr > packet, Value runtimeBdId=nullptr, bool outOfOrder=false)
void hoistScalarOpsOutOfBdBlocks(DMAConfigureTaskOp op)
LogicalResult rewriteDMAConfigureTaskOp(AIE::DeviceOp device, const llvm::DenseMap< Operation *, bool > &onlyPushedOnce)
LogicalResult verifyNoUnsupportedOpsInBlock(Block &block)
LogicalResult rewriteSingleBD(OpBuilder &builder, Block &block, AIE::TileOp &tile, AIE::DMAChannelDir channelDir, std::optional< xilinx::AIE::PacketInfoAttr > packet, bool outOfOrder=false)
LogicalResult emitOutOfOrderChannelEnable(OpBuilder &builder, DMAConfigureTaskOp op, AIE::TileOp tile)
AIE::DMABDOp getBdForBlock(Block &block)
LogicalResult hoistNextBdOpsIntoAttrs(DMAConfigureTaskOp op)
LogicalResult verifyBdInBlock(Block &block)
llvm::DenseMap< AIE::DMABDOp, Value > runtimeNextBdId
FailureOr< BdTemplateFields > gatherBdTemplateFields(OpBuilder &builder, Block &block, AIE::DMABDOp bd_op, AIE::TileOp &tile, const AIE::AIETargetModel &target_model, std::optional< xilinx::AIE::PacketInfoAttr > packet, bool outOfOrder=false)
LogicalResult rewriteSingleDMAConfigureTaskOp(DMAConfigureTaskOp op, const llvm::DenseMap< Operation *, bool > &onlyPushedOnce)
LogicalResult setAddressForSingleBD(OpBuilder &builder, AIE::DMABDOp &bd_op, AIE::TileOp &tile, OpFoldResult bdId={})
LogicalResult matchAndRewrite(DMAAwaitTaskOp op, PatternRewriter &rewriter) const override
llvm::DenseMap< Operation *, bool > & onlyPushedOnce
LogicalResult matchAndRewrite(DMAStartTaskOp op, PatternRewriter &rewriter) const override
DMAStartTaskOpPattern(MLIRContext *context, llvm::DenseMap< Operation *, bool > &onlyPushedOnce)
Bit field information for a register.
uint32_t place(uint64_t value) const
uint32_t mask() const
const BitFieldInfo * getField(llvm::StringRef fieldName) const