MLIR-AIE 1.4.5.dev
AIEXDialect.cpp
Go to the documentation of this file.
1//===- AIEXDialect.cpp ------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2019-2022 Xilinx, Inc.
4// Copyright (C) 2022-2026 Advanced Micro Devices, Inc.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
12
13#include "mlir/Dialect/Arith/IR/Arith.h"
14#include "mlir/Dialect/Func/IR/FuncOps.h"
15#include "mlir/IR/DialectImplementation.h"
16#include "mlir/IR/Matchers.h"
17#include "mlir/IR/Operation.h"
18#include "mlir/IR/RegionGraphTraits.h"
19#include "mlir/IR/SymbolTable.h"
20#include "mlir/IR/TypeUtilities.h"
21#include "mlir/Interfaces/DataLayoutInterfaces.h"
22#include "mlir/Interfaces/FoldInterfaces.h"
23#include "mlir/Transforms/InliningUtils.h"
24#include "llvm/ADT/SCCIterator.h"
25#include "llvm/ADT/STLExtras.h"
26
27#include "llvm/ADT/DenseMap.h"
28#include "llvm/ADT/TypeSwitch.h"
29#include "llvm/Support/CheckedArithmetic.h"
30#include "llvm/Support/TypeSize.h"
31
32#include <cstdint>
33#include <limits>
34#include <numeric>
35
36using namespace mlir;
37using namespace xilinx;
38
39#include "aie/Dialect/AIEX/IR/AIEXDialect.cpp.inc"
40
41#include "aie/Dialect/AIEX/IR/AIEXEnums.cpp.inc"
42
43#include "aie/Dialect/AIEX/IR/AIEXInterfaces.cpp.inc"
44
45#define GET_TYPEDEF_CLASSES
46#include "aie/Dialect/AIEX/IR/AIEXTypes.cpp.inc"
47
48#define GET_ATTRDEF_CLASSES
49#include "aie/Dialect/AIEX/IR/AIEXAttrDefs.cpp.inc"
50
51namespace xilinx::AIEX {
52
53// FIXME: use Tablegen'd dialect class
54void AIEXDialect::initialize() {
55 addOperations<
56#define GET_OP_LIST
57#include "aie/Dialect/AIEX/IR/AIEX.cpp.inc"
58 >();
59 addTypes<
60#define GET_TYPEDEF_LIST
61#include "aie/Dialect/AIEX/IR/AIEXTypes.cpp.inc"
62 >();
63 addAttributes<
64#define GET_ATTRDEF_LIST
65#include "aie/Dialect/AIEX/IR/AIEXAttrDefs.cpp.inc"
66 >();
67}
68
69} // namespace xilinx::AIEX
70
73
74#define GET_OP_CLASSES
75#include "aie/Dialect/AIEX/IR/AIEX.cpp.inc"
76
77/* Return the correct values to write to the hardware registers to configure
78 strides and wraps given the input user-facing strides and wraps.
79
80 In the IR, we express strides in units of element data type, but the hardware
81 requires it in units of address granularity. Address granularity currently is
82 4 bytes for all hardware.
83
84
85 User-facing strides/wraps relate to hardware as follows:
86
87 - By default, stride 0 and size 1 is assumed if unspecified.
88 - If only N strides/wraps are defined, those define the lowest N dimensions.
89
90 inputStride[3] == iteration_stride / elemSizeFac + 1
91 inputWrap[3] == iteration_size + 1
92 Highest-dimension stride/wrap is iteration count / iteration stride.
93 inputStride[2] == d2_stride / elemSizeFac + 1
94 Note: d2_size is not specified in hardware as it is
95 implicit from the total buffer transfer length
96 inputStride[1] == d1_stride / elemSizeFac + 1
97 inputSize[1] == d1_size
98 inputStride[0] == d0_stride / elemSizeFac + 1
99 inputSize[0] == d0_size / elemSizeFac
100
101 where elemSizeFac == bufferElementSize / addressGranularity
102 where bufferElementSize == size in bytes of elements in buffer,
103 e.g. 4 for int32
104 where addressGranularity == transfer granularity in hardware, which is
105 4 bytes for all current hardware
106
107 Note: strides are expressed offset by one from user input strides, because the
108 hardware does not support a 0 stride (repeat).
109 */
111 mlir::Operation *op,
112 mlir::BaseMemRefType referencedBufType,
113 llvm::SmallVector<int64_t, 4> inputSizes,
114 llvm::SmallVector<int64_t, 4> inputStrides,
115 llvm::SmallVector<int64_t, 4> &sizes,
116 llvm::SmallVector<int64_t, 4> &strides) {
117 assert(inputSizes.size() == inputStrides.size());
118 assert(sizes.size() == 4);
119 assert(strides.size() == 4);
120
121 DataLayout dataLayout = DataLayout::closest(op);
122 auto elemWidth =
123 dataLayout.getTypeSizeInBits(referencedBufType.getElementType());
124 auto addressGranularity = targetModel.getAddressGenGranularity();
125
126 // Output strides and sizes are default-initialized to 0
127 llvm::fill(sizes, 0);
128 llvm::fill(strides, 0);
129
130 if (inputSizes[0] == 0) {
131 // Illegal input, this won't transfer anything at all.
132 // Leave it to the verification functions to complain to the user.
133 return;
134 }
135
136 ConstStridePolicy policy;
137 int64_t inS[4] = {inputSizes[0], inputSizes[1], inputSizes[2], inputSizes[3]};
138 int64_t inT[4] = {inputStrides[0], inputStrides[1], inputStrides[2],
139 inputStrides[3]};
140 int64_t outS[4], outT[4];
141 encodeHardwareStridesWraps(policy, elemWidth, addressGranularity, inS, inT,
142 outS, outT);
143 for (int i = 0; i < 4; i++) {
144 sizes[i] = outS[i];
145 strides[i] = outT[i];
146 }
147}
148
149mlir::LogicalResult AIEX::verifyStridesWraps(
150 mlir::Operation *forOp,
151 llvm::function_ref<mlir::InFlightDiagnostic()> emitError,
152 mlir::BaseMemRefType referencedBufType, int tileCol, int tileRow,
153 llvm::SmallVector<int64_t, 4> inputSizes,
154 llvm::SmallVector<int64_t, 4> inputStrides,
155 llvm::SmallVector<int64_t, 4> hardwareSizes,
156 llvm::SmallVector<int64_t, 4> hardwareStrides,
157 bool skipTransformationChecks) {
158 const auto &targetModel = AIE::getTargetModel(forOp);
159 auto addressGranularity = targetModel.getAddressGenGranularity();
160 DataLayout dataLayout = DataLayout::closest(forOp);
161 auto elemWidth =
162 dataLayout.getTypeSizeInBits(referencedBufType.getElementType());
163
164 // ShimPLTiles have no ShimDMA, so BD field widths are meaningless here.
165 if (!targetModel.isCoreTile(tileCol, tileRow) &&
166 !targetModel.isMemTile(tileCol, tileRow) &&
167 !targetModel.isShimNOCTile(tileCol, tileRow))
168 return emitError() << "Unsupported tile type at (" << tileCol << ", "
169 << tileRow << ") Must be ShimNOC, Mem or Core.";
170
171 uint32_t wrap_bits = targetModel.getDmaBdWrapBits(tileCol, tileRow);
172 uint32_t step_bits = targetModel.getDmaBdStepBits(tileCol, tileRow);
173
174 for (int i = 0; i < 4; i++) {
175 if (inputSizes[i] <= 0) {
176 return emitError() << "Size " << i << " must be a positive integer.";
177 }
178 }
179
180 if (!isConstMultipleOfGranule(inputSizes[0], elemWidth, addressGranularity)) {
181 std::stringstream msg;
182 msg << "Transfer sizes must be multiples of " << (addressGranularity / 8)
183 << " bytes. " << inputSizes[0] << " elements at " << (elemWidth / 8)
184 << " bytes each equal " << (inputSizes[0] * elemWidth / 8)
185 << " bytes, which is not divisible by " << (addressGranularity / 8)
186 << ". ";
187 return emitError() << msg.str();
188 }
189
190 for (int i = 0; i < 3; i++) {
191 if (inputSizes[i] > 1 && inputStrides[i] < 1) {
192 // If inputSize[i] == 1, anything is allowable in the stride, since that
193 // stride will never be applied. For any larger size, we must verify that
194 // the stride is positive.
195 return emitError() << "Stride " << i << " must be a positive integer.";
196 }
197 }
198 // A value of zero is allowable for the fourth-dimension stride
199 // (this indicates an interation stride for the repeat of 0)
200 if (inputSizes[3] > 1 && inputStrides[3] < 0) {
201 return emitError() << "Stride 3 must be a non-negative integer.";
202 }
203
204 for (int i = 0; i < 4; i++) {
205 // strides[0] == 1 is ok iff the transfer size is a multiple of
206 // addressGranularity, which is checked below
207 if (i == 0 && inputStrides[i] == 1)
208 continue;
209 if (!isConstMultipleOfGranule(inputStrides[i], elemWidth,
210 addressGranularity)) {
211 std::stringstream msg;
212 msg << "Stride " << i << " is " << inputStrides[i] << " elements * "
213 << (elemWidth / 8) << " bytes = " << (inputStrides[i] * elemWidth / 8)
214 << " bytes, which is not divisible by " << (addressGranularity / 8)
215 << ". ";
216 return emitError() << msg.str();
217 }
218 }
219 // The innermost dimension steps whole granules, so for elements of another
220 // width it can only be contiguous.
221 if (elemWidth != addressGranularity && inputSizes[0] > 1 &&
222 inputStrides[0] != 1)
223 return emitError() << "Stride 0 is " << inputStrides[0]
224 << " elements, but must be 1 for " << (elemWidth / 8)
225 << "-byte elements: the DMA moves whole "
226 << (addressGranularity / 8) << "-byte words.";
227
228 auto outOfRange = [&](const char *what, int dim, int64_t written,
229 int64_t encoded, int64_t lo, int64_t hi) {
230 return emitError() << what << " " << dim << " is " << written
231 << " (encoded as " << encoded << "), which exceeds the ["
232 << lo << ":" << hi << "] range.";
233 };
234
235 int64_t maxWrap = (1 << wrap_bits) - 1;
236 int64_t maxStep = 1 << step_bits;
237
238 for (int dim : {0, 1}) {
239 if (!skipTransformationChecks && hardwareSizes[dim] > maxWrap)
240 return outOfRange("Size", dim, inputSizes[dim], hardwareSizes[dim], 0,
241 maxWrap);
242 }
243 // A zero d3 stride is a pure repeat: the lowerings leave the iteration fields
244 // 0 and carry the count on the queue push, so the repeat limit applies.
245 bool pureRepeat = inputStrides[3] == 0;
246 int64_t maxCount =
247 pureRepeat ? targetModel.getMaxRepeatCount() + 1
248 : 1LL << targetModel.getDmaBdIterBits(tileCol, tileRow);
249 if (inputSizes[3] > maxCount)
250 return emitError() << (pureRepeat ? "repeat count " : "iteration count ")
251 << inputSizes[3] << " exceeds the [1:" << maxCount
252 << "] range.";
253 for (int dim : {0, 1, 2}) {
254 if (hardwareStrides[dim] > maxStep - 1)
255 return outOfRange("Stride", dim, inputStrides[dim], hardwareStrides[dim],
256 1, maxStep);
257 }
258 // strides[3] exceeding the range is ok iff the sizes[3] is one, which is
259 // checked below
260 if (hardwareStrides[3] > maxStep - 1 && hardwareSizes[3] > 0)
261 return outOfRange("Stride", 3, inputStrides[3], hardwareStrides[3], 1,
262 maxStep);
263
264 return success();
265}
266
267//===----------------------------------------------------------------------===//
268// UseTokenOp
269//===----------------------------------------------------------------------===//
270
271LogicalResult AIEX::UseTokenOp::verify() {
272 auto *parentOp = (*this)->getParentOp();
273 if (isa<func::FuncOp>(parentOp) || isa<AIE::CoreOp>(parentOp) ||
274 isa<AIE::MemOp>(parentOp) || isa<AIE::ShimDMAOp>(parentOp))
275 return success();
276 return failure();
277}
278
279//===----------------------------------------------------------------------===//
280// MulticastOp
281//===----------------------------------------------------------------------===//
282
283LogicalResult AIEX::MulticastOp::verify() {
284 Region &body = getPorts();
285 assert(getOperation()->getNumRegions());
286 assert(!body.empty());
287 for (auto &ops : body.front())
288 if (!isa<MultiDestOp, AIE::EndOp>(ops))
289 return ops.emitOpError("cannot be contained in a Multicast op");
290
291 return success();
292}
293
294//===----------------------------------------------------------------------===//
295// BroadcastPacketOp
296//===----------------------------------------------------------------------===//
297
298LogicalResult AIEX::BroadcastPacketOp::verify() {
299 Region &body = getPorts();
300 assert(getOperation()->getNumRegions());
301 assert(!body.empty());
302 for (auto &ops : body.front())
303 if (!isa<BPIDOp, AIE::EndOp>(ops))
304 return ops.emitOpError("cannot be contained in a BroadcastPacket op");
305
306 return success();
307}
308
309//===----------------------------------------------------------------------===//
310// NpuDmaMemcpyNdOp
311//===----------------------------------------------------------------------===//
312
313/* Calculates the offset value to be written to the
314 */
315uint32_t AIEX::NpuDmaMemcpyNdOp::getAxcacheOrDefault() {
316 return getAxcache().value_or(AIE::getTargetModel(*this).getDefaultAxCache());
317}
318
319int64_t AIEX::NpuDmaMemcpyNdOp::getOffsetInBytes() {
320 llvm::SmallVector<int64_t, 4> offsets =
321 llvm::map_to_vector(llvm::reverse(getMixedOffsets()), [](OpFoldResult s) {
322 return getConstantIntValue(s).value();
323 });
324 auto strides = llvm::to_vector<4>(llvm::reverse(getMixedStrides()));
325 size_t offset = 0;
326 size_t R = offsets.size();
327 size_t el_bit_width = getElementTypeBitwidth();
328 assert(el_bit_width % 8 == 0 &&
329 "Expected Memref element bitwidth to be multiple of 8.");
330 size_t S = el_bit_width / 8;
331 // A dimension only contributes to the byte offset when its (constant) offset
332 // is non-zero; a runtime stride paired with a zero offset is fine and must
333 // not be forced to a constant. The verifier requires any stride multiplied by
334 // a non-zero offset to be constant.
335 for (size_t i = 0; i < R; i++) {
336 if (offsets[i] == 0)
337 continue;
338 auto strideConst = getConstantIntValue(strides[i]);
339 assert(strideConst &&
340 "verifier requires a stride paired with a non-zero offset to be "
341 "constant");
342 offset += offsets[i] * (*strideConst) * S;
343 }
344 return offset;
345}
346
347// Returns true when sizes/strides describe a plain contiguous transfer with
348// no data layout transformation (d1/d2 sizes == 1, d0 stride == 1).
349// d3 (repeat) is intentionally excluded.
350bool AIEX::isLinearTransfer(llvm::ArrayRef<int64_t> sizes,
351 llvm::ArrayRef<int64_t> strides) {
352 return sizes[1] == 1 && sizes[2] == 1 && strides[0] == 1 && strides[1] == 0 &&
353 strides[2] == 0;
354}
355
356// Returns true when sizes/strides (innermost-first) describe a contiguous
357// row-major scan: innermost stride == 1 and each outer stride equals the
358// product of all inner sizes. The repeat dimension (index 3) is excluded.
359// Size-1 dimensions are allowed to carry any stride value because that stride
360// is never applied during the transfer (the loop runs only once).
361// This is the vector-form counterpart of AIE::isContiguousBDTransfer.
362bool AIEX::isContiguousTransfer(llvm::ArrayRef<int64_t> sizes,
363 llvm::ArrayRef<int64_t> strides) {
364 if (strides[0] != 1)
365 return false;
366 if (sizes[1] > 1 && strides[1] != sizes[0])
367 return false;
368 if (sizes[2] > 1 && strides[2] != sizes[0] * sizes[1])
369 return false;
370 return true;
371}
372
373bool AIEX::isLinearModeTransfer(const AIE::AIETargetModel &targetModel, int col,
374 int row, llvm::ArrayRef<int64_t> sizes,
375 llvm::ArrayRef<int64_t> strides) {
376 return isLinearTransfer(sizes, strides) ||
377 (targetModel.isShimNOCTile(col, row) &&
378 isContiguousTransfer(sizes, strides));
379}
380
381// dma_memcpy_nd transfers of the form [*, 1, 1, len][*, 0, 0, 1] do not
382// specify any data layout transformation, but simply express a contiguous
383// transfer of `len`. The 4th dimension is excluded because a repeat count
384// is still compatible with a linear transfer.
385bool AIEX::NpuDmaMemcpyNdOp::isLinearTransferWithoutTransformation() {
386 llvm::SmallVector<int64_t, 4> inputSizes =
387 llvm::map_to_vector(llvm::reverse(getMixedSizes()), [](OpFoldResult s) {
388 return getConstantIntValue(s).value();
389 });
390 llvm::SmallVector<int64_t, 4> inputStrides =
391 llvm::map_to_vector(llvm::reverse(getMixedStrides()), [](OpFoldResult s) {
392 return getConstantIntValue(s).value();
393 });
394 return isLinearTransfer(inputSizes, inputStrides);
395}
396
397// Canonicalization pattern: rewrite a contiguous row-major access pattern to
398// the canonical linear form [s3, 1, 1, N][st3, 0, 0, 1].
399//
400// Using outermost-first index notation (matching the IR syntax), a 4D access
401// [s3, s2, s1, s0][st3, st2, st1, st0] is a contiguous linear scan when:
402// st0 == 1
403// s1 == 1 || st1 == s0 (stride irrelevant when size is 1)
404// s2 == 1 || st2 == s0 * s1
405// yielding a total of N = s0 * s1 * s2 contiguous elements. The repeat
406// dimension s3 / stride st3 is unchanged by the fold.
407//
408// Note: this pattern applies only to NpuDmaMemcpyNdOp. The analogous
409// dimensionsToStream / dimensionsFromStreamPerConsumer attributes on
410// ObjectFifoCreateOp are not canonicalized here; they are lowered separately
411// by the ObjectFifo stateful transform pass.
412//
413// This fold is always semantically valid and never introduces new hardware
414// limit violations: in the resulting linear form, isLinearTransferWithout-
415// Transformation() returns true, so verifyStridesWraps() skips the 10-bit
416// d0 wrap-size check. The hardware uses a wider buffer_length register in
417// linear mode (32-bit on shim tiles, 17-bit on mem tiles, 14-bit on core
418// tiles), so N can be much larger than the 10-bit ND wrap limit.
419namespace {
420struct LinearizeContiguousTransfer
421 : public mlir::OpRewritePattern<AIEX::NpuDmaMemcpyNdOp> {
422 using OpRewritePattern::OpRewritePattern;
423
424 mlir::LogicalResult
425 matchAndRewrite(AIEX::NpuDmaMemcpyNdOp op,
426 mlir::PatternRewriter &rewriter) const override {
427 // Only constant sizes/strides/offsets can be analysed statically.
428 if (!llvm::all_of(op.getMixedSizes(), [](mlir::OpFoldResult s) {
429 return mlir::getConstantIntValue(s).has_value();
430 }))
431 return mlir::failure();
432 if (!llvm::all_of(op.getMixedStrides(), [](mlir::OpFoldResult s) {
433 return mlir::getConstantIntValue(s).has_value();
434 }))
435 return mlir::failure();
436 if (!llvm::all_of(op.getMixedOffsets(), [](mlir::OpFoldResult s) {
437 return mlir::getConstantIntValue(s).has_value();
438 }))
439 return mlir::failure();
440
441 // Skip ops that are already in canonical linear form.
442 if (op.isLinearTransferWithoutTransformation())
443 return mlir::failure();
444
445 // getMixedSizes/Strides/Offsets return outermost-first; reverse to
446 // innermost-first so index 0 = d0 (innermost) and index 3 = repeat.
447 llvm::SmallVector<int64_t, 4> sizes = llvm::map_to_vector(
448 llvm::reverse(op.getMixedSizes()), [](mlir::OpFoldResult s) {
449 return mlir::getConstantIntValue(s).value();
450 });
451 llvm::SmallVector<int64_t, 4> strides = llvm::map_to_vector(
452 llvm::reverse(op.getMixedStrides()), [](mlir::OpFoldResult s) {
453 return mlir::getConstantIntValue(s).value();
454 });
455 llvm::SmallVector<int64_t, 4> offsets = llvm::map_to_vector(
456 llvm::reverse(op.getMixedOffsets()), [](mlir::OpFoldResult s) {
457 return mlir::getConstantIntValue(s).value();
458 });
459
460 // Require a contiguous row-major scan.
461 if (!AIEX::isContiguousTransfer(sizes, strides))
462 return mlir::failure();
463
464 // Fold d0/d1/d2 into one linear count; keep the repeat dimension intact.
465 // Build directly in outermost-first order for the replacement op.
466 int64_t N = sizes[0] * sizes[1] * sizes[2];
467 llvm::SmallVector<int64_t, 4> newSizesOuter = {sizes[3], 1, 1, N};
468 llvm::SmallVector<int64_t, 4> newStridesOuter = {strides[3], 0, 0, 1};
469
470 // getOffsetInBytes() computes: sum(offsets[i] * strides[i] * elemSize).
471 // After folding, the intermediate strides become 0, so any non-zero offset
472 // in those dimensions would silently contribute 0 bytes. Preserve the
473 // correct start address by collapsing the innermost three offset/stride
474 // pairs into a single linear element index at d0.
475 int64_t linearOffset = offsets[0] * strides[0] + offsets[1] * strides[1] +
476 offsets[2] * strides[2];
477 llvm::SmallVector<int64_t, 4> newOffsetsOuter = {offsets[3], 0, 0,
478 linearOffset};
479
480 rewriter.replaceOpWithNewOp<AIEX::NpuDmaMemcpyNdOp>(
481 op, op, newOffsetsOuter, newSizesOuter, newStridesOuter);
482 return mlir::success();
483 }
484};
485} // namespace
486
487void AIEX::NpuDmaMemcpyNdOp::getCanonicalizationPatterns(
488 mlir::RewritePatternSet &results, mlir::MLIRContext *context) {
489 results.add<LinearizeContiguousTransfer>(context);
490}
491
492// Helper method to check if a requested burst length is supported by the target
493// model. Returns an error message if the burst length is not supported or an
494// empty option otherwise.
495static std::optional<std::string>
496checkBurstLength(const xilinx::AIE::AIETargetModel &targetModel,
497 uint32_t requestedBurstLength) {
498 if (requestedBurstLength != 0) {
499 auto bel = targetModel.getShimBurstEncodingsAndLengths();
500 auto pair = llvm::find_if(bel, [=](const std::pair<uint32_t, uint32_t> &p) {
501 return p.second == requestedBurstLength;
502 });
503
504 if (pair == bel.end()) {
505 std::string errorMessage =
506 "Requested burst length is not supported by the target. "
507 "Supported burst lengths:";
508
509 errorMessage =
510 std::accumulate(bel.begin(), bel.end(), errorMessage,
511 [](const std::string &a, auto b) {
512 return a + " " + std::to_string(b.second);
513 });
514
515 return errorMessage;
516 }
517 }
518
519 return std::nullopt;
520}
521
522// Verify the supported scope for a dma_memcpy_nd carrying runtime (SSA)
523// offsets/sizes/strides, hard-erroring on any statically-provable violation.
524// The scope here is exactly what the dynamic BD encoder (AIEDmaToNpu.cpp
525// lowerDynamic) can lower; runtime values are never silently masked.
526LogicalResult AIEX::NpuDmaMemcpyNdOp::verifyDynamicSizesStrides(
527 const AIE::AIETargetModel &targetModel, mlir::BaseMemRefType buffer,
528 AIE::ShimDMAAllocationOp allocOp) {
529 // Shim NOC only.
530 if (!allocOp)
531 return emitOpError(
532 "runtime sizes/strides require a shim_dma_allocation to resolve the "
533 "tile; none found.");
534 AIE::TileOp tile = allocOp.getTileOp();
535 if (!tile)
536 return emitOpError("shim DMA allocation must reference a valid TileOp");
537 if (!targetModel.isShimNOCTile(tile.getCol(), tile.getRow()))
538 return emitOpError(
539 "runtime sizes/strides are only supported for shim NOC tile DMAs.");
540
541 // No zero-padding with runtime sizes/strides.
542 if (getD0ZeroBefore() || getD1ZeroBefore() || getD2ZeroBefore() ||
543 getD0ZeroAfter() || getD1ZeroAfter() || getD2ZeroAfter())
544 return emitOpError(
545 "zero padding is not supported with runtime sizes/strides.");
546
547 // Per-field runtime values are allowed: any individual size/stride may be an
548 // SSA value while others stay constant. Only the runtime-dependent fields
549 // need runtime handling; the encoder produces the same word for a constant
550 // operand either way, so the static ≡ dynamic byte-identity holds field by
551 // field.
552 auto sizes = getMixedSizes();
553 auto strides = getMixedStrides();
554
555 // The innermost stride may be runtime like any other dimension: the encoder
556 // resolves its collapse-to-zero case with a select, and its realizability
557 // (unit stride, or granule-aligned) is enforced below for constants and by a
558 // host-side check (buildBdWords) for runtime values.
559
560 // (The memref must also trace to a runtime-sequence block argument through
561 // static subview/cast offsets; that structural check, with the same clean
562 // diagnostic, is enforced by the lowering in AIEDmaToNpu.cpp, which already
563 // owns the trace utility. It is not duplicated here to keep the dialect
564 // verifier free of the analysis-layer dependency.)
565
566 // A constant size must fit its hardware wrap field (an out-of-range constant
567 // is a hard error, never a silent truncation); runtime sizes are guarded at
568 // lowering. Bounds are checked on the ENCODED value, matching
569 // verifyStridesWraps: d0 wrap scaled to granules (size * elemWidth / gran),
570 // iteration wrap biased by -1, d1 a raw element count. Sizes outermost-first.
571 DataLayout dataLayout = DataLayout::closest(getOperation());
572 uint64_t elemWidth = dataLayout.getTypeSizeInBits(buffer.getElementType());
573 uint32_t gran = targetModel.getAddressGenGranularity();
574 llvm::SmallVector<mlir::OpFoldResult, 4> sizesRev(llvm::reverse(sizes));
575 auto checkSize = [&](mlir::OpFoldResult ofr, int64_t hwVal, int64_t hi,
576 llvm::StringRef what) -> LogicalResult {
577 if (!getConstantIntValue(ofr))
578 return success();
579 if (hwVal < 0 || hwVal > hi)
580 return emitOpError(what) << " hardware value " << hwVal
581 << " exceeds hardware range [0:" << hi << "].";
582 return success();
583 };
584 auto hwSize = [&](mlir::OpFoldResult ofr) {
585 return getConstantIntValue(ofr).value_or(0);
586 };
587 int64_t d0Hw = (int64_t)(hwSize(sizesRev[0]) * elemWidth / gran);
588 int64_t d1Hw = hwSize(sizesRev[1]);
589 int64_t iterRaw = hwSize(sizesRev[3]);
590 int64_t iterHw = iterRaw > 1 ? iterRaw - 1 : 0;
591 std::optional<int64_t> outerStride = getConstantIntValue(strides.front());
592 bool pureRepeat = outerStride && *outerStride == 0;
593 AIE::AIETileType tileType =
594 targetModel.getTileType(tile.getCol(), tile.getRow());
595 int64_t wrapMax = (1LL << targetModel.getDmaBdWrapBits(tileType)) - 1;
596 int64_t maxCount = pureRepeat ? targetModel.getMaxRepeatCount() + 1
597 : 1LL << targetModel.getDmaBdIterBits(tileType);
598 if (failed(checkSize(sizesRev[0], d0Hw, wrapMax, "d0 size")) ||
599 failed(checkSize(sizesRev[1], d1Hw, wrapMax, "d1 size")) ||
600 failed(checkSize(sizesRev[3], iterHw, maxCount - 1,
601 pureRepeat ? "repeat count" : "iteration size")))
602 return failure();
603
604 // Realizability of the CONSTANT size/stride operands (divisibility +
605 // positivity, innermost-first). Runtime operands get a host-side check at
606 // lowering time. Shared with the dma_task path.
607 llvm::SmallVector<mlir::OpFoldResult, 4> stridesRev(llvm::reverse(strides));
608 if (failed(verifyConstBdRealizability(getOperation(), sizesRev, stridesRev,
609 elemWidth, gran)))
610 return failure();
611
612 // Runtime fields are checked at dispatch instead; see buildBdWords.
613
614 auto errorMessage = checkBurstLength(targetModel, getBurstLength());
615 if (errorMessage.has_value())
616 return emitOpError(errorMessage.value());
617
618 return success();
619}
620
621LogicalResult AIEX::NpuDmaMemcpyNdOp::verify() {
622 BaseMemRefType buffer = getMemref().getType();
623 const auto &targetModel = AIE::getTargetModel(*this);
624 auto addressGranularity = targetModel.getAddressGenGranularity();
625
626 if (getOffsetParameterAttr() || getOffsetStateTableIdxAttr()) {
627 uint64_t elemBitWidth = buffer.getElementTypeBitWidth();
628 if (elemBitWidth == 0 || (elemBitWidth % 8) != 0)
629 return emitOpError("offset_parameter requires a whole-byte element type");
630 }
631
632 if (getElementTypeBitwidth() > addressGranularity) {
633 return emitOpError("Maximum element bit width allowed is ")
634 << addressGranularity << "bits. ";
635 }
636 if (buffer.hasStaticShape() &&
637 (buffer.getNumElements() * getElementTypeBitwidth()) <
638 addressGranularity) {
639 return emitOpError("Minimum data transfer size required is ")
640 << addressGranularity << "bits. ";
641 }
642 bool allStridesConstant = llvm::all_of(getMixedStrides(), [](OpFoldResult s) {
643 return getConstantIntValue(s).has_value();
644 });
645 bool allSizesConstant = llvm::all_of(getMixedSizes(), [](OpFoldResult s) {
646 return getConstantIntValue(s).has_value();
647 });
648 bool allOffsetsConstant = llvm::all_of(getMixedOffsets(), [](OpFoldResult s) {
649 return getConstantIntValue(s).has_value();
650 });
651
652 bool hasLengthParameter =
653 getLengthParameterAttr() || getLengthStateTableIdxAttr();
654 // Runtime offsets, sizes and strides lower only through EmitC, which has no
655 // scratchpad.
656 if (hasLengthParameter &&
657 (!allStridesConstant || !allSizesConstant || !allOffsetsConstant))
658 return emitOpError(
659 "length_parameter requires constant offsets, sizes and strides");
660
661 // The dynamic scope check needs the allocation resolved by verifySymbolUses.
662 if (!allStridesConstant || !allSizesConstant || !allOffsetsConstant)
663 return success();
664
665 llvm::SmallVector<int64_t, 4> inputSizes =
666 llvm::map_to_vector(llvm::reverse(getMixedSizes()), [](OpFoldResult s) {
667 return getConstantIntValue(s).value();
668 });
669 llvm::SmallVector<int64_t, 4> inputStrides =
670 llvm::map_to_vector(llvm::reverse(getMixedStrides()), [](OpFoldResult s) {
671 return getConstantIntValue(s).value();
672 });
673 int64_t offset = getOffsetInBytes();
674
675 auto errorMessage = checkBurstLength(targetModel, getBurstLength());
676 if (errorMessage.has_value()) {
677 return emitOpError(errorMessage.value());
678 }
679
680 if (hasLengthParameter) {
682 *this, getLengthUnit(), buffer,
683 inputSizes[0] * inputSizes[1] * inputSizes[2],
684 AIEX::isContiguousTransfer(inputSizes, inputStrides),
685 llvm::ArrayRef<int64_t>(inputSizes).take_front(3))))
686 return failure();
687 }
688
689 // The experimental HSA target uses this op on AIE1, skip all the AIE2
690 // specific checks
691 if (targetModel.getTargetArch() == AIE::AIEArch::AIE1)
692 return success();
693
694 if (offset % 4 != 0) {
695 return emitOpError("Offset must be 4-byte-aligned.");
696 }
697
698 // packet header
699 if (auto packetInfo = getPacket()) {
700 if (failed(AIE::verifyAssignedPacket(*this, *packetInfo)))
701 return failure();
702 if (packetInfo->getPktType() > 7)
703 return emitOpError("Packet type field can only hold 3 bits.");
704 if (packetInfo->assignedId() > 31)
705 return emitOpError("Packet ID field can only hold 5 bits.");
706 }
707
708 return success();
709}
710
711// The verifier-owned collection shares the device symbol table across memcpy
712// checks without retaining allocation pointers between verification calls.
713LogicalResult
714AIEX::NpuDmaMemcpyNdOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
715 BaseMemRefType buffer = getMemref().getType();
716 const auto &targetModel = AIE::getTargetModel(*this);
717 bool allStridesConstant = llvm::all_of(getMixedStrides(), [](OpFoldResult s) {
718 return getConstantIntValue(s).has_value();
719 });
720 bool allSizesConstant = llvm::all_of(getMixedSizes(), [](OpFoldResult s) {
721 return getConstantIntValue(s).has_value();
722 });
723 bool allOffsetsConstant = llvm::all_of(getMixedOffsets(), [](OpFoldResult s) {
724 return getConstantIntValue(s).has_value();
725 });
726 bool isDynamic =
727 !allStridesConstant || !allSizesConstant || !allOffsetsConstant;
728
729 bool hasLengthParameter =
730 getLengthParameterAttr() || getLengthStateTableIdxAttr();
731 // Static AIE1 transfers without a length parameter skip allocation checks,
732 // including on the compatibility target used outside a device.
733 if (!isDynamic && !hasLengthParameter &&
734 targetModel.getTargetArch() == AIE::AIEArch::AIE1)
735 return success();
736
737 AIE::DeviceOp dev = getOperation()->getParentOfType<AIE::DeviceOp>();
738 auto allocOp = symbolTable.lookupSymbolIn<AIE::ShimDMAAllocationOp>(
739 dev, getMetadata().getRootReference());
740 if (isDynamic)
741 return verifyDynamicSizesStrides(targetModel, buffer, allocOp);
742
743 // Static transfers can precede allocation lowering, so defer tile checks
744 // until metadata names a shim DMA allocation.
745 if (!allocOp)
746 return success();
747 AIE::TileOp tile = allocOp.getTileOp();
748 if (!tile)
749 return emitOpError("shim DMA allocation must reference a valid TileOp");
750 int col = tile.getCol();
751 int row = tile.getRow();
752 if (hasLengthParameter &&
753 failed(AIE::verifyLengthParameterTile(*this, targetModel, col, row)))
754 return failure();
755 if (targetModel.getTargetArch() == AIE::AIEArch::AIE1)
756 return success();
757
758 llvm::SmallVector<int64_t, 4> inputSizes =
759 llvm::map_to_vector(llvm::reverse(getMixedSizes()), [](OpFoldResult s) {
760 return getConstantIntValue(s).value();
761 });
762 llvm::SmallVector<int64_t, 4> inputStrides =
763 llvm::map_to_vector(llvm::reverse(getMixedStrides()), [](OpFoldResult s) {
764 return getConstantIntValue(s).value();
765 });
766 llvm::SmallVector<int64_t, 4> hardwareSizes(4);
767 llvm::SmallVector<int64_t, 4> hardwareStrides(4);
768 getHardwareStridesWraps(targetModel, getOperation(), buffer, inputSizes,
769 inputStrides, hardwareSizes, hardwareStrides);
770
771 // dma_memcpy_nd transfers of the form [1, 1, 1, len][0, 0, 0, 1] do not
772 // specify any data layout transformation, but simply express a contiguous
773 // transfer of `len`. For backwards compatibility, we allow this to proceed
774 // even if it exceeds the maximum stride/wrap size of any one dimension,
775 // and simply do not lower any data layout transformations, since there is
776 // no other way to express this at the dma_memcpy_nd interface otherwise.
777 // A contiguous row-major ND access is also exempt from the ND wrap-size
778 // limit: aie-dma-to-npu lowers it to linear mode (d0_size=d1_size=0),
779 // and LinearizeContiguousTransfer canonicalizes it to explicit linear form.
780 bool skipTransformationChecks = AIEX::isLinearModeTransfer(
781 targetModel, col, row, inputSizes, inputStrides);
782 // An oversized non-contiguous pattern that aie-decompose-large-dma-bd can
783 // split into hardware-legal sub-transfers is also allowed to verify: the
784 // pass rewrites it before BD lowering. Truly undecomposable patterns (e.g.
785 // oversized strides) still fail below. isDecomposableNdDmaPattern
786 // suppresses its own diagnostics, so no stray error is emitted for the
787 // accepted case.
788 llvm::SmallVector<int64_t, 4> inputOffsets =
789 llvm::map_to_vector(llvm::reverse(getMixedOffsets()), [](OpFoldResult s) {
790 return getConstantIntValue(s).value();
791 });
792 // aie-decompose-large-dma-bd leaves a transfer with a runtime length
793 // alone, so it must be legal as written.
794 if (!hasLengthParameter && AIEX::isDecomposableNdDmaPattern(
795 getOperation(), buffer, targetModel, col, row,
796 inputOffsets, inputSizes, inputStrides)) {
797 // ok: will be decomposed before lowering
798 } else if (failed(verifyStridesWraps(
799 *this, [&] { return emitOpError(); }, buffer, col, row,
800 inputSizes, inputStrides, hardwareSizes, hardwareStrides,
801 skipTransformationChecks))) {
802 return failure();
803 }
804
805 return success();
806}
807
808//===----------------------------------------------------------------------===//
809// NpuDmaWaitOp
810//===----------------------------------------------------------------------===//
811
812LogicalResult AIEX::NpuDmaWaitOp::verify() {
813 AIE::DeviceOp dev = (*this)->getParentOfType<AIE::DeviceOp>();
814 // Some passes (e.g. aie-standard-lowering) use aiex ops outside a DeviceOp,
815 // so we can't expect the device to always exist.
816 if (dev && !dev.lookupSymbol(getSymbol()))
817 return emitOpError("couldn't find symbol in parent device");
818 return success();
819}
820
821//===----------------------------------------------------------------------===//
822// NpuPushQueueOp
823//===----------------------------------------------------------------------===//
824
825LogicalResult AIEX::NpuPushQueueOp::verify() {
826 const auto &targetModel = AIE::getTargetModel(*this);
827 auto numBds = targetModel.getNumBDs(getColumn(), getRow());
828 // bd_id and repeat_count are SSA operands; range-check them only when they
829 // are compile-time constants. A runtime repeat_count is checked on the host
830 // when lowered (AIEDmaToNpu); a runtime bd_id comes from the tile's BD pool.
831 if (std::optional<uint32_t> bdId = getConstantIntOperand(getBdId());
832 bdId && *bdId >= numBds)
833 return emitOpError("BD ID exceeds the maximum ID.");
834 uint32_t maxRepeat = targetModel.getMaxRepeatCount();
835 if (std::optional<uint64_t> repeatCount =
836 getConstantInt64Operand(getRepeatCount());
837 repeatCount && *repeatCount > maxRepeat)
838 return emitOpError("Repeat count exceeds the [0:")
839 << maxRepeat << "] range.";
840 if (getRepeatStateTableIdxAttr()) {
841 AIE::AIEArch arch = targetModel.getTargetArch();
842 if (arch != AIE::AIEArch::AIE2 && arch != AIE::AIEArch::AIE2p)
843 return emitOpError("repeat_parameter is only supported on AIE2 and "
844 "AIE2P devices");
845 if (!targetModel.isShimNOCTile(getColumn(), getRow()) &&
846 !targetModel.isMemTile(getColumn(), getRow()))
847 return emitOpError("repeat_parameter is only supported on shim NOC "
848 "and mem tiles, got tile (")
849 << getColumn() << ", " << getRow() << ")";
850 if (getConstantInt64Operand(getRepeatCount()) != uint64_t{0})
851 return emitOpError("takes its repeat count from the scratchpad, so its "
852 "repeat_count operand must be 0");
853 std::optional<uint32_t> bdId = getConstantIntOperand(getBdId());
854 if (!bdId)
855 return emitOpError("takes its repeat count from the scratchpad, so its "
856 "bd_id must be a constant");
857 if (*bdId % 4 != 0)
858 return emitOpError("takes its repeat count from the scratchpad, whose "
859 "update clears the low two bits of the queue word, "
860 "so its bd_id must be a multiple of 4, got ")
861 << *bdId;
862 }
863 return success();
864}
865
866//===----------------------------------------------------------------------===//
867// NpuWriteBdOp
868//===----------------------------------------------------------------------===//
869
870LogicalResult AIEX::NpuWriteBdOp::verify() {
871 const auto &targetModel = AIE::getTargetModel(*this);
872 auto numBds = targetModel.getNumBDs(getColumn(), getRow());
873 bool isLinearTransfer =
874 (getD0Size() >= 1) && (getD1Size() == 1) && (getIterationSize() == 0);
875 if (getBdId() >= numBds)
876 return emitOpError("BD ID exceeds the maximum ID.");
877 if (getPacketId() > 31)
878 return emitOpError("Packet ID exceeds the maximum supported by 5 bits.");
879 if (getPacketType() > 7)
880 return emitOpError("Packet Type exceeds the maximum supported by 3 bits.");
881 int64_t oooId = getOutOfOrderId();
882 if (oooId < 0 ||
883 static_cast<uint64_t>(oooId) > targetModel.getMaxOutOfOrderId())
884 return emitOpError("out_of_order_id must be in [0, ")
885 << targetModel.getMaxOutOfOrderId() << "].";
886 if (oooId != 0 && getEnablePacket() == 0)
887 return emitOpError("out_of_order_id requires a packet-enabled BD");
888
889 // Every value on this op is already the hardware-encoded field value (wrap
890 // fields unbiased, stepsize/iteration fields biased actual-1), so the
891 // legal encoded range for a B-bit field is simply [0, 2^B - 1].
892 AIE::AIETileType tileType = targetModel.getTileType(getColumn(), getRow());
893 uint32_t wrapBits = targetModel.getDmaBdWrapBits(tileType);
894 uint32_t stepBits = targetModel.getDmaBdStepBits(tileType);
895 uint32_t iterBits = targetModel.getDmaBdIterBits(tileType);
896 uint32_t maxWrap = wrapBits > 0 ? (1u << wrapBits) - 1 : 0;
897 uint32_t maxStep = stepBits > 0 ? (1u << stepBits) - 1 : 0;
898 uint32_t maxIter = iterBits > 0 ? (1u << iterBits) - 1 : 0;
899
900 if (!isLinearTransfer && getD0Size() > maxWrap)
901 return emitOpError() << "D0 Size exceeds the [0:" << maxWrap << "] range.";
902 if (getD0Stride() > maxStep)
903 return emitOpError() << "D0 Stride exceeds the [0:" << maxStep
904 << "] range.";
905 if (getD1Size() > maxWrap)
906 return emitOpError() << "D1 Size exceeds the [0:" << maxWrap << "] range.";
907 if (getD1Stride() > maxStep)
908 return emitOpError() << "D1 Stride exceeds the [0:" << maxStep
909 << "] range.";
910 if (getD2Stride() > maxStep)
911 return emitOpError() << "D2 Stride exceeds the [0:" << maxStep
912 << "] range.";
913 if (getIterationSize() > maxIter)
914 return emitOpError() << "Iteration Size exceeds the [0:" << maxIter
915 << "] range.";
916 if (static_cast<uint32_t>(getIterationStride()) > maxStep)
917 return emitOpError() << "Iteration Stride exceeds the [0:" << maxStep
918 << "] range.";
919 // buffer_length is the full 32-bit word on a shim NOC tile but only 17 bits
920 // on a mem tile and 14 on a core tile, where the packer masks it.
921 uint64_t maxLen = targetModel.getDmaBdMaxLen(tileType);
922 if (getBufferLength() > maxLen)
923 return emitOpError() << "Buffer length exceeds the [0:" << maxLen
924 << "] range.";
925 if (targetModel.isShimNOCTile(getColumn(), getRow()) && getD2Size() != 0)
926 return emitOpError("ShimTile only supports 3 dimensions of sizes.");
927 if (targetModel.isShimNOCTile(getColumn(), getRow()) &&
928 (getD0ZeroBefore() != 0 || getD0ZeroAfter() != 0 ||
929 getD1ZeroBefore() != 0 || getD1ZeroAfter() != 0 ||
930 getD2ZeroBefore() != 0 || getD2ZeroAfter() != 0))
931 return emitOpError("ShimTile doesn't support zero padding.");
932
933 // Pad field widths (AIE2P ArchSpec Table 3-34, in 32-bit words): D0 is a
934 // 6-bit field (max 63), D1 a 5-bit field (max 31), D2 a 4-bit field (max
935 // 15).
936 //
937 // Placement is a deliberate compromise. DMABDOp::verify() would be the
938 // better home for this check, since its diagnostic would point at the
939 // user's own `aie.dma_bd`. But DMABDOp::verify() early-returns for BDs
940 // nested inside `aiex.dma_configure_task`, and the only case with
941 // hardware evidence for this defect is on that task path -- a check
942 // placed only in DMABDOp::verify() would miss it. NpuWriteBdOp is where
943 // both the static and task runtime paths converge, so putting it here
944 // covers both, at the cost of the diagnostic pointing at this
945 // compiler-generated op rather than the user-written BD.
946 //
947 // TODO: migrate this check to DMABDOp::verify() once BD iteration becomes
948 // an explicit `aie.dma_bd` field and that early-return can be removed.
949 if (getD0ZeroBefore() > 0x3F || getD0ZeroAfter() > 0x3F)
950 return emitOpError("D0 pad_before/pad_after exceeds the [0:63] range.");
951 if (getD1ZeroBefore() > 0x1F || getD1ZeroAfter() > 0x1F)
952 return emitOpError("D1 pad_before/pad_after exceeds the [0:31] range.");
953 if (getD2ZeroBefore() > 0xF || getD2ZeroAfter() > 0xF)
954 return emitOpError("D2 pad_before/pad_after exceeds the [0:15] range.");
955
956 if (!targetModel.isShimNOCTile(getColumn(), getRow()) &&
957 getBurstLength() != 0)
958 return emitOpError("Only ShimTiles support burst length.");
959 auto errorMessage = checkBurstLength(targetModel, getBurstLength());
960 if (errorMessage.has_value()) {
961 return emitOpError(errorMessage.value());
962 }
963 if (!targetModel.isShimNOCTile(getColumn(), getRow()) && getAxcache())
964 return emitOpError("Only ShimTiles support AxCACHE configuration.");
965
966 return success();
967}
968
969uint32_t AIEX::NpuWriteBdOp::getAxcacheOrDefault() {
970 return getAxcache().value_or(AIE::getTargetModel(*this).getDefaultAxCache());
971}
972
973std::optional<uint32_t> AIEX::getConstantIntOperand(mlir::Value v) {
974 if (std::optional<uint64_t> cst = getConstantInt64Operand(v))
975 return static_cast<uint32_t>(*cst);
976 return std::nullopt;
977}
978
979// Widthwise counterpart of getConstantIntOperand; see createConstantArgPlus.
980std::optional<uint64_t> AIEX::getConstantInt64Operand(mlir::Value v) {
981 // An arith.constant is read in place: the generic match folds the op, which
982 // dominates translating a sequence of a million writes.
983 if (auto constant = v.getDefiningOp<arith::ConstantOp>())
984 if (auto attr = dyn_cast<IntegerAttr>(constant.getValue()))
985 return attr.getValue().getZExtValue();
986 mlir::APInt cst;
987 if (!mlir::matchPattern(v, mlir::m_ConstantInt(&cst)))
988 return std::nullopt;
989 return cst.getZExtValue();
990}
991
992mlir::Value AIEX::createConstantI32(mlir::OpBuilder &builder,
993 mlir::Location loc, uint32_t value) {
994 return arith::ConstantOp::create(
995 builder, loc, builder.getI32IntegerAttr(static_cast<int32_t>(value)));
996}
997
998mlir::Value AIEX::createConstantArgPlus(mlir::OpBuilder &builder,
999 mlir::Location loc, uint64_t value) {
1000 if (value <= std::numeric_limits<uint32_t>::max())
1001 return createConstantI32(builder, loc, static_cast<uint32_t>(value));
1002 return arith::ConstantOp::create(
1003 builder, loc,
1004 IntegerAttr::get(builder.getIntegerType(64),
1005 static_cast<int64_t>(value)));
1006}
1007
1008//===----------------------------------------------------------------------===//
1009// NpuWrite32Op
1010//===----------------------------------------------------------------------===//
1011
1012template <typename T>
1013static std::optional<uint32_t>
1014getAbsoluteAddress(T *op, uint32_t addressOffset,
1015 const AIE::NamedOpTable *names) {
1016 AIE::DeviceOp device =
1017 op->getOperation()->template getParentOfType<AIE::DeviceOp>();
1018 if (!device) {
1019 op->emitError("Must be inside a device.");
1020 return std::nullopt;
1021 }
1022 const AIE::AIETargetModel &tm = device.getTargetModel();
1023
1024 uint32_t address = 0;
1025
1026 // If blockwrite references a buffer, the given address is understood to be
1027 // relative to the buffer's start address.
1028 if (auto bufferSym = op->getBuffer()) {
1029 AIE::BufferOp buffer =
1030 names ? names->lookup<AIE::BufferOp>(op->getBufferAttr().getAttr())
1031 : AIE::lookupNamedOpIn<AIE::BufferOp>(device, *bufferSym);
1032 if (!buffer) {
1033 op->emitError() << "buffer '" << *bufferSym << "' not found in device";
1034 return std::nullopt;
1035 }
1036
1037 auto bufferAddress = buffer.getAddress();
1038 if (!bufferAddress) {
1039 mlir::InFlightDiagnostic err =
1040 op->emitError("referenced buffer must have address assigned");
1041 err.attachNote(buffer.getLoc()) << "This buffer must have an address.";
1042 return std::nullopt;
1043 }
1044
1045 uint32_t col = buffer.getTileOp().getCol();
1046 uint32_t row = buffer.getTileOp().getRow();
1047 address = static_cast<uint32_t>(*bufferAddress) +
1048 addressOffset * sizeof(uint32_t);
1049 address = ((col & 0xff) << tm.getColumnShift()) |
1050 ((row & 0xff) << tm.getRowShift()) | (address & 0xfffff);
1051 } else { // otherwise, the given address is absolute
1052 address = addressOffset;
1053 std::optional<uint32_t> col = op->getColumn();
1054 std::optional<uint32_t> row = op->getRow();
1055 if (col && row) {
1056 // If col and row are set, only the lower 20 bits of the address are
1057 // used, and col and row dictate the upper bits (ignored)
1058 address = ((*col & 0xff) << tm.getColumnShift()) |
1059 ((*row & 0xff) << tm.getRowShift()) | (address & 0xfffff);
1060 }
1061 }
1062
1063 return address;
1064}
1065
1066std::optional<uint32_t>
1067AIEX::NpuWrite32Op::getAbsoluteAddress(const AIE::NamedOpTable *names) {
1068 std::optional<uint32_t> addressOffset = getConstantIntOperand(getAddress());
1069 if (!addressOffset)
1070 return std::nullopt;
1071 return ::getAbsoluteAddress(this, *addressOffset, names);
1072}
1073
1074//===----------------------------------------------------------------------===//
1075// NpuAddressPatchOp
1076//===----------------------------------------------------------------------===//
1077
1078LogicalResult AIEX::NpuAddressPatchOp::verify() {
1079 // A runtime register address (addr_val) is meaningful only on the EmitC (C++
1080 // TXN) target; the constant `addr` still carries the fallback / static value.
1081 // The static binary target checks for the operand and diagnoses it there.
1082 if (getAddrVal() && !getAddrVal().getType().isInteger(32))
1083 return emitOpError("addr_val must be an i32 value");
1084 if (getArgIdx().has_value() == static_cast<bool>(getBuffer()))
1085 return emitOpError("must name the host buffer either by the 'arg_idx' "
1086 "attribute or by the 'buffer' operand, not both and not "
1087 "neither");
1088 return success();
1089}
1090
1091//===----------------------------------------------------------------------===//
1092// NpuUpdateFromScratchpadOp
1093//===----------------------------------------------------------------------===//
1094
1095std::optional<uint32_t> AIEX::NpuUpdateFromScratchpadOp::getAbsoluteAddress(
1096 const AIE::NamedOpTable *names) {
1097 return ::getAbsoluteAddress(this, getAddress(), names);
1098}
1099
1100LogicalResult AIEX::NpuUpdateFromScratchpadOp::verify() {
1101 // StateTable has at most 32 entries (32-bit words).
1102 constexpr uint32_t kMaxStateTableEntries = 32;
1103 if (getStateTableIdx() >= kMaxStateTableEntries)
1104 return emitOpError("state_table_idx ")
1105 << static_cast<uint32_t>(getStateTableIdx())
1106 << " exceeds maximum StateTable index ("
1107 << (kMaxStateTableEntries - 1) << ").";
1108
1109 // NpuCreateScratchpadOp::verify() bounds the state_table_idx.
1110 return success();
1111}
1112
1113//===----------------------------------------------------------------------===//
1114// NpuCreateScratchpadOp
1115//===----------------------------------------------------------------------===//
1116
1117LogicalResult AIEX::NpuCreateScratchpadOp::verify() {
1118 // Only usage_type == 0 is currently supported by firmware.
1119 if (getUsageType() != 0) {
1120 return emitOpError("usage_type must be 0 (got ")
1121 << static_cast<uint32_t>(getUsageType())
1122 << "); other layouts are not supported.";
1123 }
1124
1125 // The StateTable layout (usage_type 0) has a max of 32 32-bit-word entries,
1126 // i.e. a maximum total scratchpad size of 128 bytes.
1127 constexpr uint32_t kMaxScratchpadSizeBytes = 128;
1128 if (getSize() == 0) {
1129 return emitOpError("size must be greater than 0.");
1130 }
1131 if (getSize() % 4 != 0) {
1132 return emitOpError("size (")
1133 << getSize() << ") must be a multiple of 4 bytes.";
1134 }
1135 if (getSize() > kMaxScratchpadSizeBytes) {
1136 return emitOpError("size (")
1137 << getSize() << " bytes) exceeds maximum scratchpad size of "
1138 << kMaxScratchpadSizeBytes << " bytes.";
1139 }
1140
1141 // Scratchpads are far rarer than the updates they bound, and an op verifier
1142 // runs after every pass, so the block scan belongs on this side.
1143 uint32_t numEntries = getSize() / 4;
1144 if (Block *block = (*this)->getBlock()) {
1145 for (auto updateOp : block->getOps<AIEX::NpuUpdateFromScratchpadOp>()) {
1146 if (updateOp.getStateTableIdx() < numEntries)
1147 continue;
1148 return updateOp.emitOpError("state_table_idx ")
1149 << static_cast<uint32_t>(updateOp.getStateTableIdx())
1150 << " is out of bounds for scratchpad of size " << getSize()
1151 << " bytes (" << numEntries << " entries) created by "
1152 << (*this)->getName() << ".";
1153 }
1154 }
1155
1156 // At most one create_scratchpad may appear per runtime sequence. Walk the
1157 // parent RuntimeSequenceOp to check; only report from the duplicate (i.e.
1158 // the op that is NOT the first occurrence) to avoid emitting the same error
1159 // twice.
1160 auto runtimeSeq = getOperation()->getParentOfType<AIE::RuntimeSequenceOp>();
1161 if (!runtimeSeq) {
1162 return success();
1163 }
1164
1165 NpuCreateScratchpadOp firstSeen;
1166 runtimeSeq.walk([&](NpuCreateScratchpadOp op) {
1167 firstSeen = op;
1168 return WalkResult::interrupt();
1169 });
1170 if (firstSeen != *this) {
1171 InFlightDiagnostic diag =
1172 emitOpError("only one 'aiex.npu.create_scratchpad' is allowed per "
1173 "runtime sequence");
1174 diag.attachNote(firstSeen.getLoc())
1175 << "previous 'aiex.npu.create_scratchpad' here";
1176 return diag;
1177 }
1178
1179 return success();
1180}
1181
1182//===----------------------------------------------------------------------===//
1183// NpuMaskWrite32Op
1184//===----------------------------------------------------------------------===//
1185
1186std::optional<uint32_t>
1187AIEX::NpuMaskWrite32Op::getAbsoluteAddress(const AIE::NamedOpTable *names) {
1188 std::optional<uint32_t> addressOffset = getConstantIntOperand(getAddress());
1189 if (!addressOffset)
1190 return std::nullopt;
1191 return ::getAbsoluteAddress(this, *addressOffset, names);
1192}
1193
1194//===----------------------------------------------------------------------===//
1195// NpuMaskPollOp
1196//===----------------------------------------------------------------------===//
1197
1198std::optional<uint32_t>
1199AIEX::NpuMaskPollOp::getAbsoluteAddress(const AIE::NamedOpTable *names) {
1200 std::optional<uint32_t> addressOffset = getConstantIntOperand(getAddress());
1201 if (!addressOffset)
1202 return std::nullopt;
1203 return ::getAbsoluteAddress(this, *addressOffset, names);
1204}
1205
1206//===----------------------------------------------------------------------===//
1207// NpuBlockWriteOp
1208//===----------------------------------------------------------------------===//
1209
1210std::optional<uint32_t>
1211AIEX::NpuBlockWriteOp::getAbsoluteAddress(const AIE::NamedOpTable *names) {
1212 return ::getAbsoluteAddress(this, getAddress(), names);
1213}
1214
1215DenseIntElementsAttr AIEX::NpuBlockWriteOp::getDataWords() {
1216 Value memref = this->getData();
1217 DataLayout dataLayout = DataLayout::closest(*this);
1218 int64_t width = dataLayout.getTypeSizeInBits(
1219 cast<MemRefType>(memref.getType()).getElementType());
1220 if (width != 32) {
1221 emitWarning("Only 32-bit data type is supported for now");
1222 return nullptr;
1223 }
1224
1225 memref::GetGlobalOp getGlobal = memref.getDefiningOp<memref::GetGlobalOp>();
1226 if (!getGlobal) {
1227 emitError("Only MemRefs from memref.get_global are supported");
1228 return nullptr;
1229 }
1230
1231 auto global = dyn_cast_if_present<memref::GlobalOp>(
1232 (*this)->getParentOfType<AIE::DeviceOp>().lookupSymbol(
1233 getGlobal.getName()));
1234 if (!global) {
1235 emitError("Global symbol not found");
1236 return nullptr;
1237 }
1238
1239 auto initVal = global.getInitialValue();
1240 if (!initVal) {
1241 emitError("Global symbol has no initial value");
1242 return nullptr;
1243 }
1244
1245 auto data = dyn_cast<DenseIntElementsAttr>(*initVal);
1246 if (!data) {
1247 emitError("Global symbol initial value is not a dense int array");
1248 return nullptr;
1249 }
1250
1251 return data;
1252}
1253
1254//===----------------------------------------------------------------------===//
1255// DMAConfigureTaskOp
1256//===----------------------------------------------------------------------===//
1257
1258AIE::DMABDOp AIEX::DMAConfigureTaskOp::getFirstBd() {
1259 Region &body = getBody();
1260 if (body.empty()) {
1261 return nullptr;
1262 }
1263 auto bd_ops = body.front().getOps<AIE::DMABDOp>();
1264 if (bd_ops.empty() && body.front().getNumSuccessors() == 1) {
1265 // Allow the first block to be empty and point to the entry point of the
1266 // chain. This allows for specifying cyclying BD chains (infinite loops)
1267 // within the constraints of MLIR syntax.
1268 Block &chain_entry = *body.front().getSuccessor(0);
1269 bd_ops = chain_entry.getOps<AIE::DMABDOp>();
1270 }
1271 if (bd_ops.empty()) {
1272 return nullptr;
1273 }
1274 return *bd_ops.begin();
1275}
1276
1277LogicalResult
1278AIEX::DMAConfigureTaskOp::canonicalize(AIEX::DMAConfigureTaskOp op,
1279 PatternRewriter &rewriter) {
1280 // Remove blocks that contain nothing but a terminator
1281 Region &body = op.getBody();
1282 bool did_rewrite = false;
1283 for (auto &block : body) {
1284 if (block.empty()) {
1285 continue;
1286 }
1287 auto ops_it = block.without_terminator();
1288 if (std::distance(ops_it.begin(), ops_it.end()) == 0) {
1289 rewriter.eraseOp(block.getTerminator());
1290 did_rewrite = true;
1291 }
1292 }
1293 if (did_rewrite) {
1294 return success();
1295 }
1296 return failure();
1297}
1298
1299// Enforce the per-BD ND access-pattern limit for BDs nested inside a
1300// runtime-sequence DMA task. The AIE::DMABDOp verifier skips these BDs (their
1301// parent is a DMA task op, not a *DMAOp), so this is the only check of the BD
1302// dimension count on the runtime-sequence path.
1303//
1304// Every AIE2/AIE2P DMA BD register file carries getBDMaxDims ND address
1305// dimensions (D0..) plus one separate iteration/repeat dimension: a core/shim
1306// BD has D0..D2 + iteration, a MemTile BD has D0..D3 + iteration. On this path
1307// aiex.shim_dma_single_bd_task hoists the leading tap dimension into that
1308// iteration register, so a shim/core BD may carry one dimension beyond its ND
1309// access limit (3 + 1). A MemTile is not given the +1: AIEDMATasksToNPU maps
1310// the 4th task dimension onto the iteration register for every tile type and
1311// caps the total at 4, which the MemTile's 4 ND dimensions already reach. Both
1312// branches therefore land on the same uniform 4-dimension cap enforced later by
1313// AIEDMATasksToNPU.
1314//
1315// A BD whose sizes and strides are all constant may give more: every dimension
1316// past the third is an iteration dimension, and aie-decompose-large-dma-bd
1317// splits them off into descriptors of 4. With a runtime value among them it
1318// cannot, so the cap applies here.
1319//
1320// A BD's `iteration` attribute claims that iteration register itself, so its
1321// sizes/strides must leave it free: one dimension fewer, with no splitting.
1322static LogicalResult
1323verifyTaskBDDimensions(const AIE::AIETargetModel &targetModel, int col, int row,
1324 Region &body) {
1325 size_t maxNDims = targetModel.getBDMaxDims(col, row);
1326 if (!targetModel.isMemTile(col, row))
1327 ++maxNDims; // leading dim is hoisted into the iteration/repeat register
1328 LogicalResult result = success();
1329 body.walk([&](AIE::DMABDOp bd) {
1330 // The BD's own verifier skips it here, so nothing has yet established that
1331 // its mixed sizes/strides lists are safe to read.
1332 if (failed(bd.verifyMixedSizesAndStrides())) {
1333 result = failure();
1334 return;
1335 }
1336 if (failed(
1337 AIE::verifyDMABDIteration(bd, targetModel.getTileType(col, row)))) {
1338 result = failure();
1339 return;
1340 }
1341 size_t numDims = bd.getMixedSizes().size();
1342 if (bd.getIteration()) {
1343 if (numDims + 1 > maxNDims) {
1344 bd.emitOpError() << "Cannot give more than "
1345 << std::to_string(maxNDims - 1)
1346 << " dimensions for step sizes and wraps alongside "
1347 "the iteration attribute on this tile (got "
1348 << std::to_string(numDims) << " dimensions).";
1349 result = failure();
1350 }
1351 return;
1352 }
1353 auto isConstant = [](OpFoldResult v) {
1354 return getConstantIntValue(v).has_value();
1355 };
1356 if (numDims > maxNDims && llvm::all_of(bd.getMixedSizes(), isConstant) &&
1357 llvm::all_of(bd.getMixedStrides(), isConstant))
1358 return;
1359 if (numDims > maxNDims) {
1360 bd.emitOpError() << "Cannot give more than " << std::to_string(maxNDims)
1361 << " dimensions for step sizes and wraps on this tile "
1362 "(got "
1363 << std::to_string(numDims) << " dimensions).";
1364 result = failure();
1365 }
1366 });
1367 return result;
1368}
1369
1370LogicalResult AIEX::DMAConfigureTaskOp::verify() {
1371 const AIE::AIETargetModel &targetModel = AIE::getTargetModel(getOperation());
1372 // Skip the per-BD dimension check on an unplaced (logical) tile: the ND limit
1373 // is a function of the tile's placed coordinates, which are not yet known.
1374 // The verifier runs again on the concrete tile once placement resolves it.
1375 std::optional<int> col = getTileLike().tryGetCol();
1376 std::optional<int> row = getTileLike().tryGetRow();
1377 if (col && row &&
1378 failed(verifyTaskBDDimensions(targetModel, *col, *row, getBody())))
1379 return failure();
1380 Region &body = getBody();
1381 // This is a layering violation on the DMABDOps, but they are never verified
1382 // otherwise Because DMAConfigureTaskOps are not yet merged into the AIE
1383 // dialect. The normal DMABDOp verify operation will skip over any BD inside
1384 // a DMAConfigureTaskOp
1385 if (failed(AIE::verifyAssignedPacket(*this, getPacketAttr())))
1386 return failure();
1387 LogicalResult result = success();
1388 bool taskHasPacket = getPacket().has_value();
1389 llvm::SmallVector<AIE::DMABDOp> bds;
1390 for (auto &block : body) {
1391 if (block.empty()) {
1392 continue;
1393 }
1394 if (block.hasNoPredecessors() && !block.isEntryBlock()) {
1395 auto error = block.getTerminator()->emitError(
1396 "Block ending in this terminator does not form a chain with "
1397 "entry block.");
1398 return failure();
1399 }
1400
1401 const AIE::AIETargetModel &targetModel =
1402 AIE::getTargetModel(getOperation());
1403
1404 block.walk([&](AIE::DMABDOp bd) {
1405 if (bd.getBurstLength() != 0 &&
1406 !targetModel.isShimNOCTile(getTileID().col, getTileID().row)) {
1407 bd.emitOpError("Burst length is only supported in Shim NOC tiles that "
1408 "are connected to the memory-mapped NOC.");
1409 result = failure();
1410 }
1411 // DMABDOp::verify skips task BDs, so validate their packet header and
1412 // out_of_order_id here too.
1413 if (failed(AIE::verifyAssignedPacket(
1414 bd, bd.getPacket().value_or(AIE::PacketInfoAttr()))) ||
1415 failed(AIE::verifyDMABDOutOfOrderId(bd, taskHasPacket)) ||
1416 failed(bd.verifyTransferInBounds()))
1417 result = failure();
1418 bds.push_back(bd);
1419 });
1420 }
1421 if (getOutOfOrder()) {
1422 // Out-of-order mode rejects task completion token (bits are aliased).
1423 if (getIssueToken()) {
1424 auto err =
1425 emitOpError("out_of_order channel cannot issue a completion token");
1426 err.attachNote() << "set issue_token = false on this out-of-order task";
1427 result = failure();
1428 }
1430 getOperation(), getDirection(), getOutOfOrder(),
1431 llvm::ArrayRef<AIE::DMABDOp>(bds),
1432 /*packetEnabledByContext=*/taskHasPacket)))
1433 result = failure();
1434 }
1435 return result;
1436}
1437
1438LogicalResult AIEX::DMAStartTaskOp::verify() {
1439 if (getRepeatCountAttr() && getRepeatCountVal())
1440 return emitOpError(
1441 "takes repeat_count or a runtime repeat count, not both");
1442 if (IntegerAttr rc = getRepeatCountAttr(); rc && rc.getInt() < 0)
1443 return emitOpError("repeat_count must be non-negative, got ")
1444 << rc.getInt();
1445 if (hasRepeatParameter() && (getRepeatCountAttr() || getRepeatCountVal()))
1446 return emitOpError("takes a repeat_parameter or a repeat count, not both");
1447 if (getRepeatParameterAttr() && getRepeatStateTableIdxAttr())
1448 return emitOpError("takes repeat_parameter or repeat_state_table_idx, "
1449 "not both");
1450 return success();
1451}
1452
1453// A BD's static length in bytes: the explicit or implicit (whole-buffer)
1454// length, scaled by the element width. Nullopt for a runtime length, or one a
1455// scratchpad parameter lengthens at run time (AIEOps.td's "Runtime length").
1456static std::optional<int64_t> staticBdLenBytes(AIE::DMABDOp bd) {
1457 if (bd.getLengthParameterAttr() || bd.getLengthStateTableIdxAttr())
1458 return std::nullopt;
1459 int64_t elems;
1460 if (std::optional<int32_t> len = bd.getConstantLen()) {
1461 // Zero-extended, as DMABDOp::getLenInBytes() reads the length.
1462 elems = static_cast<uint32_t>(*len);
1463 } else {
1464 // With runtime sizes/strides, an omitted len is their inner extent
1465 // (AIEDMATasksToNPU), not the whole buffer.
1466 if (bd.hasLen() || !bd.getSizes().empty() || !bd.getStrides().empty())
1467 return std::nullopt;
1468 auto buffer = dyn_cast<MemRefType>(bd.getBuffer().getType());
1469 if (!buffer || !buffer.hasStaticShape())
1470 return std::nullopt;
1471 elems = buffer.getNumElements();
1472 }
1473 return llvm::checkedMul(
1474 elems, static_cast<int64_t>(bd.getBufferElementTypeWidthInBytes()));
1475}
1476
1477// Bytes a task streams per dispatch, or nullopt when any extent, repeat or
1478// start count is not static, or the total overflows.
1479static std::optional<int64_t>
1480staticBytesStreamed(AIEX::DMAConfigureTaskForOp task) {
1481 int64_t starts = 0;
1482 for (Operation *user : task.getResult().getUsers()) {
1483 auto start = dyn_cast<AIEX::DMAStartTaskOp>(user);
1484 if (!start) {
1485 if (isa<AIEX::DMAAwaitTaskOp, AIEX::DMAFreeTaskOp>(user))
1486 continue;
1487 // The task value is forwarded (a branch operand, a call, ...), so its
1488 // starts elsewhere can't be counted.
1489 return std::nullopt;
1490 }
1491 // A start outside the task's own block may run under a loop or a
1492 // mutually-exclusive branch, either of which this static count can't see.
1493 if (start->getBlock() != task->getBlock() || start.getRepeatCountAttr() ||
1494 start.getRepeatCountVal() || start.hasRepeatParameter())
1495 return std::nullopt;
1496 ++starts;
1497 }
1498 if (starts == 0)
1499 return 0;
1500 std::optional<int64_t> repeat =
1501 getConstantIntValue(task.getRepeatCountValue());
1502 if (!repeat || *repeat < 0)
1503 return std::nullopt;
1504
1505 Region &body = task.getBody();
1506 if (body.empty())
1507 return std::nullopt;
1508 // The queue starts the chain at the entry block, so only blocks reachable
1509 // from it stream. A cycle runs indefinitely, and a next_bd_id continues the
1510 // chain outside the body.
1511 std::optional<int64_t> moved = 0;
1512 for (auto scc = llvm::scc_begin(&body.front()); !scc.isAtEnd(); ++scc) {
1513 if (scc.hasCycle())
1514 return std::nullopt;
1515 for (AIE::DMABDOp bd : scc->front()->getOps<AIE::DMABDOp>()) {
1516 std::optional<int64_t> len = staticBdLenBytes(bd);
1517 if (!len || bd.getIteration() || bd.getNextBdId())
1518 return std::nullopt;
1519 moved = llvm::checkedAdd(*moved, *len);
1520 if (!moved)
1521 return std::nullopt;
1522 }
1523 }
1524 std::optional<int64_t> runs = llvm::checkedAdd(*repeat, int64_t{1});
1525 if (runs)
1526 runs = llvm::checkedMul(*runs, starts);
1527 return runs ? llvm::checkedMul(*moved, *runs) : std::nullopt;
1528}
1529
1530namespace {
1531// A far end's cyclic chain of BDs over one object: each segment's BD, in
1532// order, sent `copies` times in a row.
1533struct BdChain {
1534 SmallVector<int64_t> segmentBytes;
1535 int64_t copies;
1536 int64_t objectBytes;
1537
1538 // Nullopt when the object's bytes overflow or a segment is empty.
1539 static std::optional<BdChain> get(ArrayRef<int64_t> segmentBytes,
1540 int64_t copies) {
1541 if (segmentBytes.size() == 1)
1542 copies = 1;
1543 std::optional<int64_t> objectBytes = 0;
1544 for (int64_t bytes : segmentBytes) {
1545 std::optional<int64_t> run = llvm::checkedMul(bytes, copies);
1546 if (bytes <= 0 || copies <= 0 || !run)
1547 return std::nullopt;
1548 objectBytes = llvm::checkedAdd(*objectBytes, *run);
1549 if (!objectBytes)
1550 return std::nullopt;
1551 }
1552 if (segmentBytes.empty())
1553 return std::nullopt;
1554 return BdChain{SmallVector<int64_t>(segmentBytes), copies, *objectBytes};
1555 }
1556
1557 // True when `bytes` through the chain stop where one of its BDs ends.
1558 bool endsOnBd(int64_t bytes) const {
1559 int64_t rest = bytes % objectBytes;
1560 int64_t start = 0;
1561 for (int64_t segment : segmentBytes) {
1562 if (rest < start + segment * copies)
1563 return (rest - start) % segment == 0;
1564 start += segment * copies;
1565 }
1566 return true;
1567 }
1568
1569 bool operator==(const BdChain &other) const {
1570 return segmentBytes == other.segmentBytes && copies == other.copies;
1571 }
1572};
1573} // namespace
1574
1575// The far end of a shim transfer on an objectFIFO receives (or sends) whole
1576// objects through BDs sized by the object type, or by its link pool's
1577// segments, so transfers ending mid-BD never complete it and the fifo hangs.
1578// Several transfers may share an object (concatenating host buffers into it, or
1579// splitting it across them), so it is the runtime sequence's static total
1580// through the fifo that must stop at a BD boundary.
1581static LogicalResult
1582verifyTaskCoversWholeObjects(AIEX::DMAConfigureTaskForOp task,
1583 AIE::ObjectFifoCreateOp fifo) {
1584 if (fifo.getPadDimensions() || fifo.usesStream())
1585 return success();
1586 std::optional<int64_t> moved = staticBytesStreamed(task);
1587 if (!moved || *moved == 0)
1588 return success();
1589 DataLayout layout = DataLayout::closest(task);
1590 // Bytes an object holds, or nullopt when that overflows.
1591 auto objectBytesOf = [&](Type fifoType) -> std::optional<int64_t> {
1592 MemRefType elem = cast<AIE::AIEObjectFifoType>(fifoType).getElementType();
1593 return llvm::checkedMul(
1594 elem.getNumElements(),
1595 static_cast<int64_t>(layout.getTypeSize(elem.getElementType())));
1596 };
1597 // A linked fifo's far end on the link's shared tile is a chain of BDs over
1598 // the link's pool, one per pool segment, sized by the pool owner whatever
1599 // this fifo's own (or consumer) type. A join's or distribute's combined end
1600 // steps through every segment in order, each small end through its own, and
1601 // a plain link's single segment is the whole pool object. The link's
1602 // repeat_count sends each of its output's segment BDs that many times in a
1603 // row.
1604 Value linkTile;
1605 std::optional<BdChain> linkChain;
1606 if (AIE::DeviceOp device = fifo->getParentOfType<AIE::DeviceOp>())
1607 for (AIE::ObjectFifoLinkOp link : device.getOps<AIE::ObjectFifoLinkOp>()) {
1608 std::vector<AIE::ObjectFifoCreateOp> ins = link.getInputObjectFifos();
1609 std::vector<AIE::ObjectFifoCreateOp> outs = link.getOutputObjectFifos();
1610 if (!llvm::is_contained(ins, fifo) && !llvm::is_contained(outs, fifo))
1611 continue;
1612 std::optional<Value> sharedTile = link.getOptionalSharedTile();
1613 if (ins.empty() || outs.empty() || !sharedTile)
1614 return success();
1615 linkTile = *sharedTile;
1616 AIE::ObjectFifoCreateOp owner = link.getPoolOwner();
1617 MemRefType pool =
1618 cast<AIE::AIEObjectFifoType>(owner.getElemType()).getElementType();
1619 auto width =
1620 static_cast<int64_t>(layout.getTypeSize(pool.getElementType()));
1621 SmallVector<std::pair<int64_t, int64_t>> segments =
1622 link.getPoolSegments();
1623 if (fifo != owner && (link.isJoin() || link.isDistribute())) {
1624 std::vector<AIE::ObjectFifoCreateOp> &many = link.isJoin() ? ins : outs;
1625 segments = {segments[llvm::find(many, fifo) - many.begin()]};
1626 }
1627 SmallVector<int64_t> segmentBytes;
1628 for (auto [offset, size] : segments) {
1629 std::optional<int64_t> bytes = llvm::checkedMul(size, width);
1630 if (!bytes)
1631 break;
1632 segmentBytes.push_back(*bytes);
1633 }
1634 int64_t copies = llvm::is_contained(outs, fifo)
1635 ? link.getRepeatCount().value_or(1)
1636 : 1;
1637 if (segmentBytes.size() == segments.size())
1638 linkChain = BdChain::get(segmentBytes, copies);
1639 break;
1640 }
1641 // Every far-end BD chain this transfer must leave at a BD boundary: the
1642 // producer's, for a drain, and each consumer's other than the shim's own. A
1643 // broadcast stalls on its slowest consumer, so a drain bounds what the
1644 // others receive too. An end off the link tile is one BD over its own
1645 // object.
1646 SmallVector<BdChain> chains;
1647 auto addChain = [&](Value tile, Type fifoType) {
1648 std::optional<BdChain> chain;
1649 if (linkTile && tile == linkTile)
1650 chain = linkChain;
1651 else if (std::optional<int64_t> bytes = objectBytesOf(fifoType))
1652 chain = BdChain::get({*bytes}, 1);
1653 if (chain && !llvm::is_contained(chains, *chain))
1654 chains.push_back(*chain);
1655 };
1656 auto producer =
1657 dyn_cast_or_null<AIE::TileLike>(fifo.getProducerTile().getDefiningOp());
1658 if (!producer)
1659 return success();
1660 if (!producer.isShimTile())
1661 addChain(fifo.getProducerTile(), fifo.getElemType());
1662 for (Value consumer : fifo.getConsumerTiles()) {
1663 auto tile = dyn_cast_or_null<AIE::TileLike>(consumer.getDefiningOp());
1664 if (!tile || !tile.isShimTile())
1665 addChain(consumer, fifo.getConsumerElemTypeOrDefault());
1666 }
1667 if (chains.empty())
1668 return success();
1669
1670 // Transfers in a loop or branch have no static count to sum.
1671 auto sequence = task->getParentOfType<AIE::RuntimeSequenceOp>();
1672 if (!sequence || task->getBlock() != &sequence.getBody().front())
1673 return success();
1674 // A dma_channel_reset_for re-arms the fifo's far ends and drops a partly
1675 // filled object. It doesn't block, so which bytes land before it isn't
1676 // static, and the transfers around it can't be summed.
1677 if (llvm::any_of(task->getBlock()->getOps<AIEX::DmaChannelResetForOp>(),
1678 [&](AIEX::DmaChannelResetForOp reset) {
1679 return reset.getObjfifoAttr() == task.getAlloc();
1680 }))
1681 return success();
1682 // Segment boundaries don't add up the way whole objects do, so the
1683 // sequence's total is checked whatever this transfer moves, once: by the
1684 // first transfer through the fifo.
1685 for (Operation *prev = task->getPrevNode(); prev; prev = prev->getPrevNode())
1686 if (auto other = dyn_cast<AIEX::DMAConfigureTaskForOp>(prev);
1687 other && other.getAlloc() == task.getAlloc() &&
1688 staticBytesStreamed(other) != 0)
1689 return success();
1690 std::optional<int64_t> total = 0;
1691 int64_t sharing = 0;
1692 WalkResult walk = sequence.walk([&](Operation *op) {
1693 // A dma_memcpy_nd or BD chain through the fifo also shares its objects;
1694 // its extent is not summed here, so the sequence's total is unknown.
1695 if (auto memcpy = dyn_cast<AIEX::NpuDmaMemcpyNdOp>(op))
1696 return memcpy.getMetadata() == task.getAlloc() ? WalkResult::interrupt()
1697 : WalkResult::advance();
1698 if (auto chain = dyn_cast<AIEX::DMAStartBdChainForOp>(op))
1699 return chain.getAllocAttr() == task.getAlloc() ? WalkResult::interrupt()
1700 : WalkResult::advance();
1701 auto other = dyn_cast<AIEX::DMAConfigureTaskForOp>(op);
1702 if (!other || other.getAlloc() != task.getAlloc())
1703 return WalkResult::advance();
1704 std::optional<int64_t> bytes = staticBytesStreamed(other);
1705 // A task that is never started streams nothing, wherever it is.
1706 if (bytes == 0)
1707 return WalkResult::advance();
1708 if (!bytes || other->getBlock() != task->getBlock())
1709 return WalkResult::interrupt();
1710 total = llvm::checkedAdd(*total, *bytes);
1711 if (!total)
1712 return WalkResult::interrupt();
1713 ++sharing;
1714 return WalkResult::advance();
1715 });
1716 if (walk.wasInterrupted())
1717 return success();
1718 const BdChain *partial = llvm::find_if(
1719 chains, [&](const BdChain &chain) { return !chain.endsOnBd(*total); });
1720 if (partial == chains.end())
1721 return success();
1722
1723 InFlightDiagnostic error =
1724 task.emitOpError() << "moves " << *moved << " bytes through objectFIFO @"
1725 << fifo.getSymName() << ", which";
1726 if (sharing > 1)
1727 error << ", with this runtime sequence's other transfers through it ("
1728 << *total << " bytes in all),";
1729 error << " is not a whole number of its " << partial->objectBytes
1730 << "-byte objects";
1731 if (partial->segmentBytes.size() == 1)
1732 return error << "; the far end's buffer descriptor is sized by the "
1733 "object, so a partial one never completes";
1734 std::string segments;
1735 llvm::raw_string_ostream os(segments);
1736 llvm::interleaveComma(partial->segmentBytes, os);
1737 error << " and doesn't end on a segment boundary (segments of " << segments
1738 << " bytes";
1739 if (partial->copies > 1)
1740 error << ", each sent " << partial->copies << " times in a row";
1741 return error << "); the far end's buffer descriptors are sized by the "
1742 "segments, so a partial one never completes";
1743}
1744
1745// Resolving the allocation symbol through the collection keeps the lookup off
1746// the device's linear symbol scan, which a per-op verifier repeats after every
1747// pass.
1748LogicalResult AIEX::DMAConfigureTaskForOp::verifySymbolUses(
1749 SymbolTableCollection &symbolTable) {
1750 AIE::DeviceOp dev = getOperation()->getParentOfType<AIE::DeviceOp>();
1751 if (!dev)
1752 return success();
1753 Operation *target =
1754 symbolTable.lookupSymbolIn(dev, getAlloc().getRootReference());
1755 if (!target)
1756 return emitOpError() << getAlloc() << " does not name a symbol";
1757 // The tile is known once the name is a shim DMA allocation or a route
1758 // endpoint, so the per-BD dimension limit can be enforced before the task
1759 // becomes a concrete dma_configure_task. Before the objectFIFO lowering
1760 // the name is the fifo's own, and the check waits.
1761 Value tileValue;
1762 LogicalResult named =
1763 TypeSwitch<Operation *, LogicalResult>(target)
1764 .Case([&](AIE::ShimDMAAllocationOp alloc) {
1765 tileValue = alloc.getTile();
1766 return success();
1767 })
1768 .Case([&](AIE::RouteEndpointOp endpoint) -> LogicalResult {
1769 tileValue = endpoint.getTile();
1770 if (endpoint.getBundle() == AIE::WireBundle::DMA)
1771 return success();
1772 return emitOpError() << getAlloc() << " names a "
1773 << stringifyWireBundle(endpoint.getBundle())
1774 << " port, not a DMA channel";
1775 })
1776 .Case([&](AIE::ObjectFifoCreateOp fifo) {
1777 return verifyTaskCoversWholeObjects(*this, fifo);
1778 })
1779 .Default([&](Operation *) -> LogicalResult {
1780 return emitOpError() << getAlloc()
1781 << " must name an aie.shim_dma_allocation, an "
1782 "aie.route_endpoint or an aie.objectfifo";
1783 });
1784 if (failed(named))
1785 return failure();
1786 // An allocation or endpoint on an unplaced (logical) tile waits for
1787 // placement to substitute a real one.
1788 auto tile = tileValue ? tileValue.getDefiningOp<AIE::TileOp>() : nullptr;
1789 if (!tile)
1790 return success();
1791 const AIE::AIETargetModel &targetModel = AIE::getTargetModel(getOperation());
1792 return verifyTaskBDDimensions(targetModel, tile.getCol(), tile.getRow(),
1793 getBody());
1794}
1795
1796//===----------------------------------------------------------------------===//
1797// DMAStartBdChainOp
1798//===----------------------------------------------------------------------===//
1799
1800AIE::BDChainOp AIEX::DMAStartBdChainOp::getBDChainOp() {
1801 AIE::DeviceOp device = (*this)->getParentOfType<AIE::DeviceOp>();
1802 AIE::BDChainOp chain = device.lookupSymbol<AIE::BDChainOp>(getSymbol());
1803 return chain;
1804}
1805
1806LogicalResult AIEX::DMAStartBdChainOp::verify() {
1807 AIE::BDChainOp chain = getBDChainOp();
1808 if (!chain) {
1809 return emitOpError("symbol does not reference valid BD chain");
1810 }
1811
1812 auto actualArgTypes = getArgs().getTypes();
1813 auto expectedArgTypes = chain.getRegion().getArgumentTypes();
1814 if (actualArgTypes.size() != expectedArgTypes.size()) {
1815 return emitOpError("Number of arguments mismatches.");
1816 }
1817 for (unsigned i = 0, n = expectedArgTypes.size(); i < n; i++) {
1818 if (actualArgTypes[i] != expectedArgTypes[i]) {
1819 return emitOpError("Argument ") << (i + 1) << " types mismatch: "
1820 << "expected " << expectedArgTypes[i]
1821 << " but got " << actualArgTypes[i];
1822 }
1823 }
1824 return success();
1825}
1826
1827//===----------------------------------------------------------------------===//
1828// NpuControlPacketOp
1829//===----------------------------------------------------------------------===//
1830
1831uint32_t AIEX::NpuControlPacketOp::getRowFromAddr() {
1832 const auto &targetModel = AIE::getTargetModel(*this);
1833 uint32_t addr = getAddress();
1834 uint32_t rowInt = (addr >> targetModel.getRowShift()) & 0x1f;
1835 return rowInt;
1836}
1837
1838uint32_t AIEX::NpuControlPacketOp::getColumnFromAddr() {
1839 const auto &targetModel = AIE::getTargetModel(*this);
1840 uint32_t addr = getAddress();
1841 uint32_t colInt = (addr >> targetModel.getColumnShift()) & 0x1f;
1842 return colInt;
1843}
1844
1845//===----------------------------------------------------------------------===//
1846// SetLockOp
1847//===----------------------------------------------------------------------===//
1848
1849LogicalResult AIEX::SetLockOp::verify() {
1850 const auto &targetModel = AIE::getTargetModel(*this);
1851
1852 if (targetModel.getTargetArch() == AIE::AIEArch::AIE1)
1853 return emitOpError("SetLockOp is not supported on AIE1.");
1854
1855 if (std::optional<int64_t> value = getConstantIntValue(getValue())) {
1856 if (*value < 0)
1857 return emitOpError("Lock value must be non-negative");
1858 if (*value > targetModel.getMaxLockValue())
1859 return emitOpError("Lock value exceeds the maximum value of " +
1860 std::to_string(targetModel.getMaxLockValue()));
1861 }
1862
1863 auto lockOp = getLockOp();
1864 auto lockIDOpt = getLockOp().getLockID();
1865 // Note that the lockID may not be assigned initially, so lets wait until it
1866 // is to verify the lockID dependent conditions
1867 if (!lockIDOpt) {
1868 return success();
1869 }
1870 // A logical tile with a coordinate still unpinned waits for the
1871 // post-placement re-verify for its range and address checks, as
1872 // UsesAreAccessible's do.
1873 std::optional<int> col = lockOp.getTileLike().tryGetCol();
1874 std::optional<int> row = lockOp.getTileLike().tryGetRow();
1875 if (!col || !row)
1876 return success();
1877
1878 uint32_t lockID = lockOp.getLockIDValue();
1879
1880 if (lockID >= targetModel.getNumLocks(*col, *row)) {
1881 return emitOpError("Lock ID out of range for given tile. Max ID: " +
1882 std::to_string(targetModel.getNumLocks(*col, *row) - 1));
1883 }
1884
1885 if (!targetModel.getLocalLockAddress(lockID, AIE::TileID{*col, *row})) {
1886 return emitOpError("Invalid lock ID and tile combination when trying to "
1887 "retrieve the local lock address.");
1888 }
1889
1890 return success();
1891}
1892
1893//===----------------------------------------------------------------------===//
1894// DMABdPoolPopOp / DMABdPoolPushOp
1895//===----------------------------------------------------------------------===//
1896
1897static LogicalResult verifyBdPoolPartition(Operation *op, int col, int row,
1898 uint32_t begin, uint32_t end) {
1899 uint32_t numBds = AIE::getTargetModel(op).getNumBDs(col, row);
1900 if (begin >= end || end > numBds)
1901 return op->emitOpError()
1902 << "partition [" << begin << ", " << end
1903 << ") is not a nonempty range of the " << numBds
1904 << " buffer descriptors on tile (" << col << ", " << row << ")";
1905 return success();
1906}
1907
1908LogicalResult AIEX::DMABdPoolPopOp::verify() {
1909 return verifyBdPoolPartition(*this, getColumn(), getRow(), getBdBegin(),
1910 getBdEnd());
1911}
1912
1913LogicalResult AIEX::DMABdPoolPushOp::verify() {
1914 return verifyBdPoolPartition(*this, getColumn(), getRow(), getBdBegin(),
1915 getBdEnd());
1916}
1917
1918//===----------------------------------------------------------------------===//
1919// DmaChannelResetOp
1920//===----------------------------------------------------------------------===//
1921
1922LogicalResult AIEX::DmaChannelResetOp::verify() {
1923 const auto &targetModel = AIE::getTargetModel(*this);
1924
1925 auto tile = dyn_cast_or_null<AIE::TileOp>(getTile().getDefiningOp());
1926 if (!tile)
1927 return emitOpError() << "tile operand must be produced by an aie.tile op";
1928 int col = tile.getCol();
1929 int row = tile.getRow();
1930 // Only core and mem tiles have a per-channel DMA reset bit. The shim NOC DMA
1931 // control register has no reset field (aie-rt's Aie2PShimDmaChProp sets
1932 // Reset.Mask = 0); bit 1 there is PAUSE_MEM, not RESET. Rejecting shim keeps
1933 // the op honest about what it can lower, matching aie-rt's own
1934 // XAie_DmaChannelReset, which errors on SHIMNOC/SHIMPL tiles.
1935 if (!targetModel.isCoreTile(col, row) && !targetModel.isMemTile(col, row))
1936 return emitOpError() << "tile (" << col << ", " << row
1937 << ") has no DMA channel reset (only core and mem "
1938 "tiles do; shim NOC DMA has no reset bit)";
1939
1940 // Number of DMA channels on this tile in this direction. Mirrors
1941 // TileOp::getNumSource/DestConnections(WireBundle::DMA): the switchbox
1942 // direction is reversed relative to the DMA direction.
1943 uint32_t numChannels = getDirection() == AIE::DMAChannelDir::S2MM
1944 ? targetModel.getNumDestSwitchboxConnections(
1945 col, row, AIE::WireBundle::DMA)
1947 col, row, AIE::WireBundle::DMA);
1948 if (getChannel() >= numChannels)
1949 return emitOpError() << "channel " << getChannel()
1950 << " out of range for this tile and direction (tile "
1951 "has "
1952 << numChannels << " DMA channel(s) in this direction)";
1953
1954 return success();
1955}
1956
1957LogicalResult AIEX::DmaChannelResetForOp::verify() {
1958 // Deferring verifier, in the shape of DMAConfigureTaskForOp::verify: the
1959 // referenced symbol is only resolvable once the objectFIFO lowering has run.
1960 // It names an aie.objectfifo, then that fifo's shim endpoint while the
1961 // lowering is in flight, and finally its aie.objectfifo_rearm_binding. If
1962 // none is resolvable yet, defer -- a later pass will resolve it.
1963 AIE::DeviceOp dev = getOperation()->getParentOfType<AIE::DeviceOp>();
1964 if (!dev)
1965 return success();
1966 // The resident re-arm relies on the aie2p behavior that a DMA channel has no
1967 // enable bit, so the only way to restart it is a START_QUEUE push. AIE1 DMA
1968 // channels have an enable bit and are armed differently, so the trio this op
1969 // lowers to would not re-arm them correctly.
1970 if (AIE::getTargetModel(*this).getTargetArch() == AIE::AIEArch::AIE1)
1971 return emitOpError() << "is not supported on AIE1 devices (the resident "
1972 "re-arm relies on the aie2p start-queue push)";
1973 Operation *target = dev.lookupSymbol(getObjfifo());
1974 if (!target)
1975 return success(); // symbol resolved during a later pass; defer the check
1976 if (!isa<AIE::ObjectFifoCreateOp, AIE::RouteEndpoint,
1977 AIE::ObjectFifoRearmBindingOp>(target))
1978 return emitOpError() << "'" << getObjfifo()
1979 << "' must reference an aie.objectfifo (or its "
1980 "aie.objectfifo_rearm_binding)";
1981 return success();
1982}
1983
1984//===----------------------------------------------------------------------===//
1985// CoreResetOp
1986//===----------------------------------------------------------------------===//
1987
1988LogicalResult AIEX::CoreResetOp::verify() {
1989 const auto &targetModel = AIE::getTargetModel(*this);
1990
1991 // The op lowers to an NPU control-packet write; the runtime sequence has no
1992 // meaning on AIE1. Reject it explicitly, as SetLockOp does.
1993 if (targetModel.getTargetArch() == AIE::AIEArch::AIE1)
1994 return emitOpError("aiex.core_reset is not supported on AIE1.");
1995
1996 auto tile = dyn_cast_or_null<AIE::TileOp>(getTile().getDefiningOp());
1997 if (!tile)
1998 return emitOpError() << "tile operand must be produced by an aie.tile op";
1999 int col = tile.getCol();
2000 int row = tile.getRow();
2001 // The tile coordinates are bounded by aie.tile's own verifier, so this op
2002 // does not re-check them for range.
2003
2004 // Only core tiles have a CORE_CONTROL register with a reset bit. Mem and shim
2005 // tiles have no compute core, so there is nothing valid to lower to. This
2006 // matches aie-rt's XAie_CoreReset, which errors on any tile that is not an
2007 // AIE (core) tile.
2008 if (!targetModel.isCoreTile(col, row))
2009 return emitOpError() << "tile (" << col << ", " << row
2010 << ") has no core to reset (only core tiles have a "
2011 "CORE_CONTROL register)";
2012
2013 return success();
2014}
2015
2016//===----------------------------------------------------------------------===//
2017// BlockFloatingPointType
2018//===----------------------------------------------------------------------===//
2019uint64_t AIEX::BlockFloatType::getTotalSizeInBits() const {
2020 return getBlockSize() * getMantissaBits() + getExponentBits() +
2021 getSubtileShiftBits();
2022}
2023
2024llvm::TypeSize AIEX::BlockFloatType::getTypeSizeInBits(
2025 const mlir::DataLayout &dataLayout,
2026 mlir::DataLayoutEntryListRef params) const {
2027 return llvm::TypeSize::getFixed(getTotalSizeInBits());
2028}
2029
2030uint64_t AIEX::BlockFloatType::getABIAlignment(
2031 const mlir::DataLayout &dataLayout,
2032 mlir::DataLayoutEntryListRef params) const {
2033 // For the purposes of the data movement operations, we want all types to be
2034 // packed <=> ABI alignment is 1.
2035 return 1;
2036}
2037
2038std::optional<AIEX::BlockFloatType::BlockFormat>
2039AIEX::BlockFloatType::getBlockFormat(StringRef blockType) {
2040 static const llvm::StringMap<AIEX::BlockFloatType::BlockFormat>
2041 blockFormatsMap = {
2042 {"v8bfp16ebs8", {8, 8, 8, 0}},
2043 {"v16bfp16ebs16", {16, 8, 8, 0}},
2044 };
2045
2046 auto it = blockFormatsMap.find(blockType);
2047 if (it != blockFormatsMap.end()) {
2048 return it->second;
2049 }
2050
2051 return std::nullopt;
2052}
2053
2054LogicalResult
2055AIEX::BlockFloatType::verify(function_ref<InFlightDiagnostic()> emitError,
2056 StringRef block_type) {
2057 if (!getBlockFormat(block_type))
2058 return emitError() << "Invalid block type: " << block_type
2059 << ". Known types are: v8bfp16ebs8, v16bfp16ebs16.";
2060
2061 return success();
2062}
2063
2064//===----------------------------------------------------------------------===//
2065// ConfigureOp
2066//===----------------------------------------------------------------------===//
2067
2068AIE::DeviceOp AIEX::ConfigureOp::getReferencedDeviceOp() {
2069 ModuleOp moduleOp = this->getOperation()->getParentOfType<ModuleOp>();
2070 if (!moduleOp) {
2071 emitError("aiex.configure must be inside of a module");
2072 return nullptr;
2073 }
2074 Operation *maybeReferencedDevice =
2075 SymbolTable::lookupSymbolIn(moduleOp.getOperation(), getSymbolAttr());
2076 if (!maybeReferencedDevice) {
2077 emitError("No such device: '") << getSymbolAttr() << "'";
2078 return nullptr;
2079 }
2080 AIE::DeviceOp referencedDevice =
2081 llvm::dyn_cast<AIE::DeviceOp>(maybeReferencedDevice);
2082 if (!referencedDevice) {
2083 emitError("Not a device: '") << getSymbolAttr() << "'";
2084 return nullptr;
2085 }
2086 return referencedDevice;
2087}
2088
2089//===----------------------------------------------------------------------===//
2090// ReadScratchpadParameterOp
2091//===----------------------------------------------------------------------===//
2092
2093LogicalResult AIEX::ReadScratchpadParameterOp::verify() {
2094 auto device = (*this)->getParentOfType<AIE::DeviceOp>();
2095 if (!device) {
2096 return emitOpError("must be inside an aie.device");
2097 }
2098 if (!(*this)->getParentOfType<AIE::CoreOp>()) {
2099 return emitOpError("must be inside an aie.core");
2100 }
2101 auto moduleOp = (*this)->getParentOfType<ModuleOp>();
2102 if (!moduleOp ||
2103 !moduleOp.lookupSymbol<AIEX::ScratchpadParameterOp>(getParameter())) {
2104 return emitOpError("references unknown parameter '")
2105 << getParameter()
2106 << "' (aiex.scratchpad_parameter ops are declared at module scope)";
2107 }
2108 if (getResult().getType().isF32()) {
2109 return emitOpError(
2110 "f32 parameters are not supported: the scratchpad encoding zeroes "
2111 "the top 2 bits, which clobbers the sign bit and top exponent bit "
2112 "of an f32. Use bf16 or an integer type up to i32 instead.");
2113 }
2114 return success();
2115}
2116
2117LogicalResult AIEX::WaitScratchpadParametersOp::verify() {
2118 if (!(*this)->getParentOfType<AIE::CoreOp>()) {
2119 return emitOpError("must be inside an aie.core");
2120 }
2121 return success();
2122}
2123
2124LogicalResult AIEX::ConfigureOp::verify() {
2125 AIE::DeviceOp parentDev = getOperation()->getParentOfType<AIE::DeviceOp>();
2126 AIE::DeviceOp referencedDev = getReferencedDeviceOp();
2127 if (!referencedDev) {
2128 return failure();
2129 }
2130 if (parentDev.getDevice() != referencedDev.getDevice()) {
2131 emitError("Device types do not match: '")
2132 << AIE::stringifyAIEDevice(parentDev.getDevice()) << "' vs. '"
2133 << AIE::stringifyAIEDevice(referencedDev.getDevice()) << "'";
2134 return failure();
2135 }
2136 return success();
2137}
2138
2139//===----------------------------------------------------------------------===//
2140// RunOp
2141//===----------------------------------------------------------------------===//
2142
2143AIE::DeviceOp AIEX::RunOp::getCalleeDeviceOp() {
2144 AIEX::ConfigureOp configureOp =
2145 getOperation()->getParentOfType<AIEX::ConfigureOp>();
2146 if (!configureOp) {
2147 return nullptr;
2148 }
2149 AIE::DeviceOp referencedDevice = configureOp.getReferencedDeviceOp();
2150 return referencedDevice;
2151}
2152
2153AIE::RuntimeSequenceOp
2154AIEX::RunOp::getCalleeRuntimeSequenceOp(AIE::NamedOpTableCollection *tables) {
2155 AIEX::ConfigureOp configureOp =
2156 getOperation()->getParentOfType<AIEX::ConfigureOp>();
2157 if (!configureOp) {
2158 return nullptr;
2159 }
2160 AIE::DeviceOp referencedDevice = configureOp.getReferencedDeviceOp();
2161 if (!referencedDevice) {
2162 return nullptr;
2163 }
2164
2165 StringAttr name = getRuntimeSequenceSymbolAttr().getAttr();
2166 if (!tables) {
2167 return AIE::lookupNamedOpIn<AIE::RuntimeSequenceOp>(referencedDevice, name);
2168 }
2169 return tables->lookupIn<AIE::RuntimeSequenceOp>(referencedDevice, name);
2170}
2171
2172//===----------------------------------------------------------------------===//
2173// NpuLoadPdiOp
2174//===----------------------------------------------------------------------===//
2175
2176LogicalResult AIEX::NpuLoadPdiOp::canonicalize(AIEX::NpuLoadPdiOp op,
2177 PatternRewriter &rewriter) {
2178 // Check for back-to-back identical load_pdi ops and remove duplicates
2179 Operation *nextOp = op->getNextNode();
2180 if (!nextOp)
2181 return failure();
2182
2183 // Check if next op is also a NpuLoadPdiOp
2184 auto nextLoadPdi = dyn_cast<AIEX::NpuLoadPdiOp>(nextOp);
2185 if (!nextLoadPdi)
2186 return failure();
2187
2188 // Check if they are identical (all attributes match)
2189 if (op.getDeviceRefAttr() == nextLoadPdi.getDeviceRefAttr() &&
2190 op.getId() == nextLoadPdi.getId() &&
2191 op.getSize() == nextLoadPdi.getSize() &&
2192 op.getAddress() == nextLoadPdi.getAddress()) {
2193 // Erase the first one, keeping the second
2194 rewriter.eraseOp(op);
2195 return success();
2196 }
2197
2198 return failure();
2199}
2200
2201//===----------------------------------------------------------------------===//
2202// NpuWriteConfigOp
2203//===----------------------------------------------------------------------===//
2204
2205static bool writesConfig(Operation *op) {
2206 return isa<AIEX::NpuLoadPdiOp, AIEX::NpuWriteConfigOp,
2207 AIEX::NpuWriteTeardownOp>(op);
2208}
2209
2210// Visits the load_pdi, write_config and write_teardown ops before `op` in its
2211// block, nearest first, then those after it from the block's end, as the host
2212// runs the sequence again on each dispatch, then `op` itself. Returns the first
2213// for which `visit` returns true.
2214static Operation *findConfigOpBefore(Operation *op,
2215 function_ref<bool(Operation *)> visit) {
2216 for (Operation *prev = op->getPrevNode(); prev; prev = prev->getPrevNode())
2217 if (writesConfig(prev) && visit(prev))
2218 return prev;
2219 for (Operation &next : llvm::reverse(*op->getBlock())) {
2220 if (&next == op)
2221 break;
2222 if (writesConfig(&next) && visit(&next))
2223 return &next;
2224 }
2225 return visit(op) ? op : nullptr;
2226}
2227
2228// The load_pdi that resets the array for `op`, or the write_teardown that
2229// leaves it so, or null.
2230static Operation *precedingReset(Operation *op) {
2231 return findConfigOpBefore(op, [&](Operation *prev) {
2232 return prev != op &&
2233 isa<AIEX::NpuLoadPdiOp, AIEX::NpuWriteTeardownOp>(prev);
2234 });
2235}
2236
2237static bool loadsSamePdi(AIEX::NpuLoadPdiOp a, AIEX::NpuLoadPdiOp b) {
2238 return a.getDeviceRefAttr() == b.getDeviceRefAttr() &&
2239 a.getId() == b.getId() && a.getSize() == b.getSize() &&
2240 a.getAddress() == b.getAddress();
2241}
2242
2243LogicalResult AIEX::NpuWriteConfigOp::verify() {
2244 Operation *found = precedingReset(*this);
2245 if (!found)
2246 return emitOpError("must follow the load_pdi that resets the array");
2247 auto reset = dyn_cast<AIEX::NpuLoadPdiOp>(found);
2248 if (!reset)
2249 return success();
2250 // The firmware skips the load if it loaded the same PDI last. That leaves
2251 // the array as a reset does only if every configuration since is torn down.
2252 bool tornDown = true;
2253 Operation *later = reset;
2254 auto before =
2255 cast<AIEX::NpuLoadPdiOp>(findConfigOpBefore(reset, [&](Operation *op) {
2256 if (isa<AIEX::NpuWriteConfigOp>(op) &&
2257 !isa<AIEX::NpuWriteTeardownOp>(later))
2258 tornDown = false;
2259 later = op;
2260 return isa<AIEX::NpuLoadPdiOp>(op);
2261 }));
2262 if (!loadsSamePdi(before, reset) || tornDown)
2263 return success();
2264 InFlightDiagnostic diag = emitOpError(
2265 "follows a load_pdi the firmware skips, as it loads the PDI loaded "
2266 "just before it, so the array is not reset");
2267 diag.attachNote(before.getLoc()) << "the PDI is loaded here";
2268 return diag;
2269}
2270
2271LogicalResult
2272AIEX::NpuLoadPdiOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
2273 FlatSymbolRefAttr ref = getDeviceRefAttr();
2274 if (!ref)
2275 return success();
2276 auto device = (*this)->getParentOfType<AIE::DeviceOp>();
2277 Operation *scope = device ? device->getParentOp()
2278 : SymbolTable::getNearestSymbolTable(*this);
2279 if (!isa_and_present<AIE::DeviceOp>(symbolTable.lookupSymbolIn(scope, ref)))
2280 return emitOpError() << ref << " is not a device";
2281 return success();
2282}
2283
2284LogicalResult
2285AIEX::NpuWriteConfigOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
2286 Operation *scope = (*this)->getParentOfType<AIE::DeviceOp>()->getParentOp();
2287 if (!isa_and_present<AIE::DeviceOp>(
2288 symbolTable.lookupSymbolIn(scope, getDeviceRefAttr())))
2289 return emitOpError() << getDeviceRefAttr() << " is not a device";
2290 auto reset = dyn_cast_or_null<AIEX::NpuLoadPdiOp>(precedingReset(*this));
2291 FlatSymbolRefAttr resetRef = reset ? reset.getDeviceRefAttr() : nullptr;
2292 auto empty = resetRef ? dyn_cast_or_null<AIE::DeviceOp>(
2293 symbolTable.lookupSymbolIn(scope, resetRef))
2294 : nullptr;
2295 // The transaction conversion gives every device a `@configure` sequence; an
2296 // empty one still configures nothing.
2297 if (reset &&
2298 (!empty ||
2299 !llvm::all_of(empty.getBody()->without_terminator(), [](Operation &op) {
2300 return isa<AIE::RuntimeSequenceOp>(op) &&
2301 llvm::all_of(op.getRegion(0),
2302 [](Block &block) { return block.empty(); });
2303 })))
2304 return emitOpError("must follow a load_pdi of an empty device, which "
2305 "resets the array");
2306 return success();
2307}
2308
2309//===----------------------------------------------------------------------===//
2310// NpuWriteTeardownOp
2311//===----------------------------------------------------------------------===//
2312
2313LogicalResult AIEX::NpuWriteTeardownOp::verify() {
2314 Operation *prev = (*this)->getPrevNode();
2315 while (prev && !writesConfig(prev))
2316 prev = prev->getPrevNode();
2317 auto config = dyn_cast_or_null<AIEX::NpuWriteConfigOp>(prev);
2318 if (!config || config.getDeviceRefAttr() != getDeviceRefAttr())
2319 return emitOpError("must follow the write_config of ")
2320 << getDeviceRefAttr() << ", with no load_pdi between";
2321 return success();
2322}
2323
2324LogicalResult
2325AIEX::NpuWriteTeardownOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
2326 Operation *scope = (*this)->getParentOfType<AIE::DeviceOp>()->getParentOp();
2327 auto device = dyn_cast_or_null<AIE::DeviceOp>(
2328 symbolTable.lookupSymbolIn(scope, getDeviceRefAttr()));
2329 if (!device)
2330 return emitOpError() << getDeviceRefAttr() << " is not a device";
2331 Operation *shimStart = AIEX::findShimDmaStart(device);
2332 if (!shimStart)
2333 return success();
2334 InFlightDiagnostic diag =
2335 emitOpError() << getDeviceRefAttr()
2336 << " starts a shim DMA channel, which teardown cannot stop";
2337 diag.attachNote(shimStart->getLoc()) << "started here";
2338 return diag;
2339}
2340
2341Operation *AIEX::findShimDmaStart(AIE::DeviceOp device) {
2342 Operation *found = nullptr;
2343 device.walk([&](Operation *op) {
2344 if (!isa<AIE::DMAStartOp, AIE::DMAOp>(op) ||
2345 !isa<AIE::ShimDMAOp>(op->getParentOp()))
2346 return WalkResult::advance();
2347 found = op;
2348 return WalkResult::interrupt();
2349 });
2350 return found;
2351}
bool isCoreTile(int col, int row) const
Return true if the given tile is a Core tile.
virtual uint64_t getDmaBdMaxLen(AIETileType tileType) const =0
Return the maximum transfer length, measured in address-generation granules (see getAddressGenGranula...
virtual AIEArch getTargetArch() const =0
Return the target architecture.
bool isMemTile(int col, int row) const
Return true if the given tile is a Mem tile.
virtual uint32_t getDmaBdWrapBits(AIETileType tileType) const =0
Return the bit width of the wrap (size) field in a DMA buffer descriptor for the given tile type.
virtual uint32_t getNumLocks(AIETileType tileType) const =0
Return the number of lock objects for a given tile type.
virtual uint32_t getDmaBdIterBits(AIETileType tileType) const =0
Return the bit width of the iteration (repeat) wrap/stride fields in a DMA buffer descriptor for the ...
virtual uint32_t getBDMaxDims(AIETileType tileType) const =0
Return the maximum number of addressing dimensions (wraps/strides) a single buffer descriptor support...
virtual std::vector< std::pair< uint32_t, uint32_t > > getShimBurstEncodingsAndLengths() const =0
virtual uint32_t getDmaBdStepBits(AIETileType tileType) const =0
Return the bit width of the step (stride) field in a DMA buffer descriptor for the given tile type.
virtual std::optional< uint32_t > getLocalLockAddress(uint32_t lockId, TileID tile) const =0
virtual AIETileType getTileType(int col, int row) const =0
Return the tile type for the given tile coordinates.
virtual uint32_t getMaxRepeatCount() const =0
Return the largest DMA task repeat count (unsupported = 0).
bool isShimNOCTile(int col, int row) const
Return true if the given tile is a ShimNOC tile.
virtual uint32_t getNumBDs(AIETileType tileType) const =0
Return the number of buffer descriptors for a given tile type.
virtual uint32_t getMaxLockValue() const =0
Return the maximum value that can be stored in a lock register.
virtual uint32_t getMaxOutOfOrderId() const =0
Return the largest out-of-order BD id (unsupported = 0).
virtual uint32_t getColumnShift() const =0
virtual uint32_t getNumDestSwitchboxConnections(int col, int row, WireBundle bundle) const =0
Return the number of destinations of connections inside a switchbox.
virtual uint32_t getRowShift() const =0
virtual uint32_t getAddressGenGranularity() const =0
Return the data bus width of the device.
virtual uint32_t getNumSourceSwitchboxConnections(int col, int row, WireBundle bundle) const =0
Return the number of sources of connections inside a switchbox.
OpTy lookupIn(mlir::Operation *symbolTableOp, mlir::StringAttr name)
Definition AIEDialect.h:179
mlir::Operation * lookup(mlir::StringAttr name) const
Definition AIEDialect.h:153
const V & get(const std::map< K, V > &pa, K k)
Definition d_ary_heap.h:70
mlir::Operation * findShimDmaStart(AIE::DeviceOp device)
bool isContiguousTransfer(llvm::ArrayRef< int64_t > sizes, llvm::ArrayRef< int64_t > strides)
void encodeHardwareStridesWraps(Policy &p, uint64_t elemWidth, uint32_t addressGranularity, typename Policy::V inputSizes[4], typename Policy::V inputStrides[4], typename Policy::V sizes[4], typename Policy::V strides[4])
Definition BdLowering.h:145
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)
std::optional< uint64_t > getConstantInt64Operand(mlir::Value v)
bool isDecomposableNdDmaPattern(mlir::Operation *forOp, mlir::BaseMemRefType referencedBufType, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow, llvm::ArrayRef< int64_t > offsetsInnermostFirst, llvm::ArrayRef< int64_t > sizesInnermostFirst, llvm::ArrayRef< int64_t > stridesInnermostFirst)
True when the pattern is fully constant, non-contiguous, currently illegal, and decomposeNdDmaPattern...
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
mlir::Value createConstantArgPlus(mlir::OpBuilder &builder, mlir::Location loc, uint64_t value)
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)
bool isConstMultipleOfGranule(int64_t value, uint64_t elemWidth, uint32_t addressGranularity)
Definition BdLowering.h:65
bool isLinearTransfer(llvm::ArrayRef< int64_t > sizes, llvm::ArrayRef< int64_t > strides)
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
mlir::Value createConstantI32(mlir::OpBuilder &builder, mlir::Location loc, uint32_t value)
void printTypedDynamicIndexList(mlir::OpAsmPrinter &printer, mlir::Operation *op, mlir::OperandRange values, llvm::ArrayRef< int64_t > integers, mlir::TypeRange types)
mlir::LogicalResult verifyLengthParameterTile(mlir::Operation *op, const AIETargetModel &targetModel, int col, int row)
mlir::LogicalResult verifyAssignedPacket(mlir::Operation *op, PacketInfoAttr packet, llvm::StringRef what="packet")
mlir::ParseResult parseTypedDynamicIndexList(mlir::OpAsmParser &parser, llvm::SmallVectorImpl< mlir::OpAsmParser::UnresolvedOperand > &values, mlir::DenseI64ArrayAttr &integers, llvm::SmallVectorImpl< mlir::Type > &types)
mlir::Operation * lookupNamedOpIn(mlir::Operation *symbolTableOp, mlir::StringAttr name)
mlir::LogicalResult verifyDMABDOutOfOrderId(DMABDOp bd, bool packetEnabledByContext=false)
mlir::LogicalResult verifyOutOfOrderChannel(mlir::Operation *op, DMAChannelDir dir, bool outOfOrder, llvm::ArrayRef< DMABDOp > bds, bool packetEnabledByContext=false)
mlir::LogicalResult verifyLengthParameter(mlir::Operation *op, std::optional< int64_t > lengthUnit, mlir::BaseMemRefType buffer, std::optional< int64_t > lenElems, bool contiguous, llvm::ArrayRef< int64_t > innerSizes)
struct TileID { friend std::ostream &operator<<(std::ostream &os, const TileID &s) { os<< "TileID("<< s.col<< ", "<< s.row<< ")"; return os; } friend std::string to_string(const TileID &s) { std::ostringstream ss; ss<< s; return ss.str(); } friend llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const TileID &s) { os<< to_string(s); return os; } inline bool operator<(const TileID &rhs) const { return std::tie(col, row)< std::tie(rhs.col, rhs.row); } bool operator==(const TileID &rhs) const { return std::tie(col, row)==std::tie(rhs.col, rhs.row); } bool operator!=(const TileID &rhs) const { return !(*this==rhs);} int col, row;} TileID
const AIETargetModel & getTargetModel(mlir::Operation *op)
mlir::LogicalResult verifyDMABDIteration(DMABDOp bd, AIETileType tileType)