MLIR-AIE 1.4.5.dev
AIETargetNPU.cpp
Go to the documentation of this file.
1//===- AIETargetNPU.cpp -----------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2023-2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
9
17
18#include "mlir/Dialect/Arith/IR/Arith.h"
19#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
20#include "mlir/Dialect/Func/IR/FuncOps.h"
21#include "mlir/Dialect/MemRef/IR/MemRef.h"
22#include "mlir/IR/SymbolTable.h"
23#include "mlir/Interfaces/DataLayoutInterfaces.h"
24#include "mlir/Interfaces/SideEffectInterfaces.h"
25
26#include "mlir/Tools/mlir-translate/MlirTranslateMain.h"
27#include "llvm/ADT/DenseMap.h"
28
29#include "mlir/Dialect/Utils/StaticValueUtils.h"
30#include "llvm/ADT/ArrayRef.h"
31#include "llvm/ADT/TypeSwitch.h"
32#include "llvm/Support/Format.h"
33#include "llvm/Support/FormatVariadic.h"
34#include "llvm/Support/JSON.h"
35#include "llvm/Support/raw_ostream.h"
36
37#include <memory>
38#include <vector>
39
40using namespace mlir;
41using namespace xilinx;
42using namespace xilinx::AIE;
43using namespace xilinx::AIEX;
44
45namespace {
46
47// Example:
48// - instructions = {3,4,5}
49// - tailSize = 2
50// instructions becomes {3,4,5,0,0} and
51// a mutable reference to the tail {0,0} is returned.
52llvm::MutableArrayRef<uint32_t>
53reserveAndGetTail(std::vector<uint32_t> &instructions, uint64_t tailSize) {
54 auto oldSize = instructions.size();
55 auto newSize = oldSize + tailSize;
56 instructions.resize(newSize, 0);
57 return llvm::MutableArrayRef<uint32_t>(instructions.data() + oldSize,
58 tailSize);
59}
60
61// Thin wrappers that extract MLIR attributes and delegate to TxnEncoding.h,
62// the single source of truth for the TXN word layout.
63
64LogicalResult appendSync(std::vector<uint32_t> &instructions, NpuSyncOp op) {
65 std::optional<uint32_t> column = AIEX::getConstantIntOperand(op.getColumn());
66 std::optional<uint32_t> row = AIEX::getConstantIntOperand(op.getRow());
67 std::optional<uint32_t> direction =
68 AIEX::getConstantIntOperand(op.getDirection());
69 std::optional<uint32_t> channel =
70 AIEX::getConstantIntOperand(op.getChannel());
71 std::optional<uint32_t> columnNum =
72 AIEX::getConstantIntOperand(op.getColumnNum());
73 std::optional<uint32_t> rowNum = AIEX::getConstantIntOperand(op.getRowNum());
74 if (!column || !row || !direction || !channel || !columnNum || !rowNum)
75 return op.emitOpError("Cannot translate sync with non-constant operands to "
76 "a static TXN binary");
77 aie_runtime::txn_append_sync(instructions, *column, *row, *direction,
78 *channel, *columnNum, *rowNum);
79 return success();
80}
81
82LogicalResult appendWrite32(std::vector<uint32_t> &instructions,
83 NpuWrite32Op op) {
84 if (op.getBuffer())
85 return op.emitOpError("Cannot translate symbolic address");
86 std::optional<uint32_t> address = op.getAbsoluteAddress();
87 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
88 if (!address || !value)
89 return op.emitOpError(
90 "Cannot translate write32 with non-constant address or value "
91 "to a static TXN binary");
92 aie_runtime::txn_append_write32(instructions, *address, *value);
93 return success();
94}
95
96LogicalResult appendMaskWrite32(std::vector<uint32_t> &instructions,
97 NpuMaskWrite32Op op) {
98 if (op.getBuffer())
99 return op.emitOpError("Cannot translate symbolic address");
100 std::optional<uint32_t> address = op.getAbsoluteAddress();
101 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
102 std::optional<uint32_t> mask = AIEX::getConstantIntOperand(op.getMask());
103 if (!address || !value || !mask)
104 return op.emitOpError("Cannot translate maskwrite32 with non-constant "
105 "address, value, or mask to a static TXN binary");
106 aie_runtime::txn_append_maskwrite32(instructions, *address, *value, *mask);
107 return success();
108}
109
110LogicalResult appendMaskPoll(std::vector<uint32_t> &instructions,
111 NpuMaskPollOp op) {
112 if (op.getBuffer())
113 return op.emitOpError("Cannot translate symbolic address");
114 std::optional<uint32_t> address = op.getAbsoluteAddress();
115 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
116 std::optional<uint32_t> mask = AIEX::getConstantIntOperand(op.getMask());
117 if (!address || !value || !mask)
118 return op.emitOpError("Cannot translate maskpoll with non-constant "
119 "address, value, or mask to a static TXN binary");
120 aie_runtime::txn_append_maskpoll32(instructions, *address, *value, *mask);
121 return success();
122}
123
124void appendLoadPdi(std::vector<uint32_t> &instructions, NpuLoadPdiOp op) {
125 aie_runtime::txn_append_loadpdi(instructions, op.getId(), op.getSize(),
126 op.getAddress());
127}
128
129LogicalResult appendAddressPatch(std::vector<uint32_t> &instructions,
130 NpuAddressPatchOp op, bool foldDDRAddrOffset) {
131 if (op.getAddrVal())
132 return op.emitOpError("Cannot translate address_patch with a runtime "
133 "register address (addr_val) to a static TXN binary; "
134 "the runtime-bd_id pool path targets the C++ TXN "
135 "target only");
136 std::optional<uint64_t> argPlus =
137 AIEX::getConstantInt64Operand(op.getArgPlus());
138 if (!argPlus)
139 return op.emitOpError("Cannot translate address_patch with non-constant "
140 "arg_plus to a static TXN binary");
141 std::optional<uint32_t> argIdx = op.getArgIdx();
142 if (!argIdx)
143 return op.emitOpError("address_patch still names its host buffer by SSA "
144 "value; run -aie-resolve-address-patch-buffers");
145 aie_runtime::txn_append_arg_patch(instructions, op.getAddr(), *argIdx,
146 *argPlus, foldDDRAddrOffset);
147 return success();
148}
149
150// Cached resolution of NpuBlockWriteOp's data words.
151// NpuBlockWriteOp::getDataWords() resolves the data memref.global via
152// device.lookupSymbol (a LINEAR symbol scan over the device's symbols) +
153// global.getInitialValue() on EVERY call — perf shows this
154// (SymbolTable::lookupSymbolIn + memref::GlobalOp::getInherentAttr) is ~47% of
155// AIETranslateNpuToBinary on a B=128 runtime sequence (~10^5 block-writes),
156// i.e. O(block-writes x globals). Resolve via a prebuilt per-device SymbolTable
157// (O(1)) and memoize per global symbol (the B-unroll's streams reuse the same
158// data globals), so the returned attribute is byte-identical but the work
159// collapses. Falls back to the canonical method on any non-global memref /
160// lookup miss (identical behavior).
161static DenseIntElementsAttr cachedBlockWriteData(
162 NpuBlockWriteOp op, mlir::SymbolTable &symTab,
163 llvm::DenseMap<mlir::StringAttr, DenseIntElementsAttr> &cache) {
164 auto getGlobal = op.getData().getDefiningOp<mlir::memref::GetGlobalOp>();
165 if (!getGlobal)
166 return op.getDataWords();
167 // A hit already passed the width check: get_global's type is its global's.
168 mlir::StringAttr key = getGlobal.getNameAttr().getRootReference();
169 auto it = cache.find(key);
170 if (it != cache.end())
171 return it->second;
172 // Honor getDataWords()'s 32-bit-element contract exactly: a non-32-bit memref
173 // must take the canonical path (which emits "Only 32-bit data type is
174 // supported" + returns nullptr), not the cached fast path.
175 mlir::DataLayout dataLayout = mlir::DataLayout::closest(op);
176 if (dataLayout.getTypeSizeInBits(
177 mlir::cast<mlir::MemRefType>(op.getData().getType())
178 .getElementType()) != 32)
179 return op.getDataWords();
180 DenseIntElementsAttr data;
181 if (auto global =
182 dyn_cast_if_present<mlir::memref::GlobalOp>(symTab.lookup(key)))
183 if (auto initVal = global.getInitialValue())
184 data = dyn_cast<DenseIntElementsAttr>(*initVal);
185 if (!data)
186 data = op.getDataWords(); // preserve original error/edge behavior exactly
187 cache[key] = data;
188 return data;
189}
190
191LogicalResult appendBlockWrite(
192 std::vector<uint32_t> &instructions, NpuBlockWriteOp op,
193 std::optional<uint32_t> address, mlir::SymbolTable &symTab,
194 llvm::DenseMap<mlir::StringAttr, DenseIntElementsAttr> &dataCache) {
195 if (!address)
196 return op.emitOpError(
197 "Cannot translate blockwrite with unresolved address to a static TXN "
198 "binary");
199 DenseIntElementsAttr data = cachedBlockWriteData(op, symTab, dataCache);
200
201 // Resolve the payload words via the (cached) data attribute, then hand off to
202 // the encoder which owns the word layout.
203 std::vector<uint32_t> payload;
204 payload.reserve(data.size());
205 if (auto words = data.tryGetValues<uint32_t>(); succeeded(words))
206 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
207 payload.assign(words->begin(), words->end());
208 else
209 for (auto d : data)
210 payload.push_back(d.getZExtValue());
211
212 // The col/row word is only populated when BOTH are present (matching the
213 // historical behavior); otherwise it stays 0 (a flat address).
214 auto col = op.getColumn();
215 auto row = op.getRow();
216 uint32_t colVal = 0, rowVal = 0;
217 if (col && row) {
218 colVal = *col;
219 rowVal = *row;
220 }
221 aie_runtime::txn_append_blockwrite(instructions, *address, payload.data(),
222 payload.size(), colVal, rowVal);
223 return success();
224}
225
226void appendPreempt(std::vector<uint32_t> &instructions, NpuPreemptOp op) {
227 aie_runtime::txn_append_preempt(instructions, op.getLevel());
228}
229
230void appendCreateScratchpad(std::vector<uint32_t> &instructions,
231 NpuCreateScratchpadOp op) {
232 // TXN_OPC_CREATE_SCRATCHPAD encoding (4 words = 16 bytes):
233 // Byte 0: Opcode (10)
234 // Byte 1: Usage Type
235 // Bytes 2-3: padding
236 // Bytes 4-7: Size
237 // Bytes 8-15: DDR Address (patched at runtime by XRT)
238 auto words = reserveAndGetTail(instructions, 4);
239
241 words[0] |= (static_cast<uint32_t>(op.getUsageType()) << 8);
242 words[1] = op.getSize();
243 // DDR address words[2] and words[3] are left as 0;
244 // they will be patched at runtime by XRT/aiebu based on the
245 // .ctrl.scratchpad section.
246 words[2] = 0;
247 words[3] = 0;
248}
249
250LogicalResult appendUpdateRegFromScratchpad(std::vector<uint32_t> &instructions,
251 NpuUpdateFromScratchpadOp op,
252 const NamedOpTable &names) {
253 std::optional<uint32_t> address = op.getAbsoluteAddress(&names);
254 if (!address)
255 return op.emitOpError("Cannot translate update_from_scratchpad with "
256 "unresolved address to a static TXN binary");
257 // TXN_OPC_UPDATE_REG encoding (3 words = 12 bytes):
258 // Byte 0: Opcode (12)
259 // Byte 1: StateTableIdx
260 // Byte 2: Func
261 // Byte 3: padding
262 // Bytes 4-7: FuncArg
263 // Bytes 8-11: RegOff (absolute offset from AIE array base to register pair)
264 auto words = reserveAndGetTail(instructions, 3);
265
267 words[0] |= (static_cast<uint32_t>(op.getStateTableIdx()) << 8);
268 words[0] |= (static_cast<uint32_t>(op.getFunc()) << 16);
269 words[1] = op.getFuncArg();
270 words[2] = *address;
271 return success();
272}
273
274} // namespace
275
276namespace {
277
278// Lazy-loaded regdb for register-name decoration. Loaded once per process,
279// returns nullptr if the JSON files cannot be located (e.g. install dir not
280// set). Decoration is best-effort.
281static const xilinx::AIE::RegisterDatabase *getRegDB() {
282 static std::unique_ptr<xilinx::AIE::RegisterDatabase> db = []() {
284 }();
285 return db.get();
286}
287
288// Decompose an absolute device address into (col, row, offset) using the
289// target model's column/row shifts, then derive the regdb module name from
290// the row's tile type. Looks the (module, offset) up in regdb. Returns the
291// register name on success and writes the module name to *outModule. On any
292// failure returns empty StringRef.
293static llvm::StringRef
294lookupRegisterByAddress(uint64_t address, const AIETargetModel &tm,
296 std::string &outModule) {
297 outModule.clear();
298 if (!regdb)
299 return {};
300 uint32_t colShift = tm.getColumnShift();
301 uint32_t rowShift = tm.getRowShift();
302 uint8_t col = static_cast<uint8_t>((address >> colShift) & 0xFF);
303 uint8_t row = static_cast<uint8_t>((address >> rowShift) & 0xFF);
304 uint64_t baseMask = (uint64_t{1} << rowShift) - 1;
305 uint32_t offset = static_cast<uint32_t>(address & baseMask);
306
307 // Determine module from tile type. Module names must match the JSON keys in
308 // aie_registers_aie2.json: "core", "memory", "memory_tile", "shim".
309 StringRef moduleName;
310 if (tm.isShimNOCTile(col, row) || tm.isShimPLTile(col, row)) {
311 moduleName = "shim";
312 } else if (tm.isMemTile(col, row)) {
313 moduleName = "memory_tile";
314 } else {
315 // For aie tiles, regdb has both core and memory modules. The offset alone
316 // disambiguates. Try core first then memory.
317 if (auto *r = regdb->lookupRegisterByOffset(offset, "core")) {
318 outModule = "core";
319 return r->name;
320 }
321 if (auto *r = regdb->lookupRegisterByOffset(offset, "memory")) {
322 outModule = "memory";
323 return r->name;
324 }
325 return {};
326 }
327 if (auto *r = regdb->lookupRegisterByOffset(offset, moduleName)) {
328 outModule = moduleName.str();
329 return r->name;
330 }
331 return {};
332}
333
334static void pushLocEntry(std::vector<TxnLocEntry> *locmap,
335 uint32_t byteOffsetBefore, uint32_t byteOffsetAfter,
336 StringRef opcodeName, StringRef sourceOpName,
337 std::optional<uint64_t> address, mlir::Operation *op,
338 const AIETargetModel &tm) {
339 if (!locmap)
340 return;
341 TxnLocEntry e;
342 e.byteOffset = byteOffsetBefore;
343 e.byteSize = byteOffsetAfter - byteOffsetBefore;
344 e.opcodeName = opcodeName.str();
345 e.sourceOpName = sourceOpName.str();
346 e.address = address;
347 e.loc = op->getLoc();
348 if (address) {
349 if (auto regName =
350 lookupRegisterByAddress(*address, tm, getRegDB(), e.registerModule);
351 !regName.empty()) {
352 e.registerName = regName.str();
353 }
354 }
355 locmap->push_back(std::move(e));
356}
357
358// Appends the TXN encoding of a runtime sequence's ops, one op at a time.
359struct TxnEncoder {
360 TxnEncoder(DeviceOp device, std::vector<uint32_t> &instructions,
361 std::vector<TxnLocEntry> *locmap, bool foldDDRAddrOffset,
362 NpuConfigCache &configs)
363 : device(device), tm(device.getTargetModel()),
364 symTab(device.getOperation()), names(device.getOperation()),
365 configs(configs), instructions(instructions), locmap(locmap),
366 foldDDRAddrOffset(foldDDRAddrOffset) {}
367
368 void encode(Operation &o);
369 LogicalResult encodeConfig(DeviceOp configured, bool teardown,
370 NpuConfigCache::Config &config);
371 void appendConfig(Operation *op, FlatSymbolRefAttr deviceRef, bool teardown);
372
373 DeviceOp device;
374 const AIETargetModel &tm;
375 // Built once, with a per-global data cache, so block-write data resolution
376 // is O(1)+memoized instead of a per-op linear symbol scan
377 // (cachedBlockWriteData). ~47% of the translation on a B=128 sequence.
378 mlir::SymbolTable symTab;
379 NamedOpTable names;
380 llvm::DenseMap<mlir::StringAttr, DenseIntElementsAttr> blockWriteDataCache;
381 NpuConfigCache &configs;
382 std::vector<uint32_t> &instructions;
383 std::vector<TxnLocEntry> *locmap;
384 bool foldDDRAddrOffset;
385 uint32_t count = 0;
386 // Accumulates failure from the per-op append helpers (e.g. a non-constant
387 // operand that cannot be encoded into a static TXN binary). The walk
388 // finishes so every offending op is diagnosed, then the translation fails
389 // rather than emit a silently incomplete binary.
390 LogicalResult result = success();
391};
392
393// Generates `configured`'s configuration ops, or with `teardown` its teardown
394// ops, as the expand-load-pdi pass would inline them, in a scratch device of
395// its own, and encodes them as a sequence's own ops are.
396LogicalResult TxnEncoder::encodeConfig(DeviceOp configured, bool teardown,
397 NpuConfigCache::Config &config) {
398 MLIRContext *ctx = device.getContext();
399 ctx->getOrLoadDialect<arith::ArithDialect>();
400 ctx->getOrLoadDialect<memref::MemRefDialect>();
401 OpBuilder builder(ctx);
402 OwningOpRef<ModuleOp> scratch = ModuleOp::create(device.getLoc());
403 builder.setInsertionPointToStart(scratch->getBody());
404 auto holder =
405 DeviceOp::create(builder, device.getLoc(), configured.getDevice(),
406 configured.getSymName());
407 holder.getRegion().emplaceBlock();
408 DeviceOp::ensureTerminator(holder.getBodyRegion(), builder, device.getLoc());
409 builder.setInsertionPointToStart(holder.getBody());
410 auto seq = RuntimeSequenceOp::create(
411 builder, device.getLoc(), builder.getStringAttr("configure"), BoolAttr{},
412 TraceBufferAttr{}, ArrayAttr{}, ArrayAttr{});
413 Block *body = new Block;
414 seq.getBody().push_back(body);
415 builder.setInsertionPointToStart(body);
416 BlockwriteData payloads(holder, teardown ? "teardown_" : "loadpdi_");
417 if (teardown
418 ? failed(generateAndInsertTeardownOps(builder, configured, payloads))
419 : failed(generateAndInsertConfigOps(builder, configured, payloads)))
420 return failure();
421 // The locmap is kept whether or not this translation writes one: another
422 // sharing the cache may.
423 TxnEncoder encoder(holder, config.words, &config.locmap, foldDDRAddrOffset,
424 configs);
425 for (Operation &op : *body)
426 encoder.encode(op);
427 config.count = encoder.count;
428 return encoder.result;
429}
430
431void TxnEncoder::appendConfig(Operation *op, FlatSymbolRefAttr deviceRef,
432 bool teardown) {
433 auto configured = dyn_cast_or_null<DeviceOp>(
434 SymbolTable::lookupSymbolIn(device->getParentOp(), deviceRef));
435 const NpuConfigCache::Config *config =
436 configured
437 ? configs.getOrEncode(configured, teardown,
438 [&](NpuConfigCache::Config &c) {
439 return encodeConfig(configured, teardown, c);
440 })
441 : nullptr;
442 if (!config) {
443 op->emitOpError(teardown ? "cannot generate the teardown of "
444 : "cannot generate the configuration of ")
445 << deviceRef;
446 result = failure();
447 return;
448 }
449 uint32_t base = static_cast<uint32_t>(instructions.size() * sizeof(uint32_t));
450 llvm::append_range(instructions, config->words);
451 count += config->count;
452 if (locmap)
453 for (TxnLocEntry entry : config->locmap) {
454 entry.byteOffset += base;
455 locmap->push_back(std::move(entry));
456 }
457}
458
459void TxnEncoder::encode(Operation &o) {
460 auto byteOffset = [&]() -> uint32_t {
461 return static_cast<uint32_t>(instructions.size() * sizeof(uint32_t));
462 };
463 llvm::TypeSwitch<Operation *>(&o)
464 .Case<cf::AssertOp>([&](auto op) {
465 // A static sequence only ever carries a constant-true guard
466 // (canonicalization erases it); a constant-false one failed at
467 // compile time, and anything unresolved means a runtime value
468 // reached the binary path.
469 auto c = getConstantIntValue(op.getArg());
470 if (c && *c == 0) {
471 op.emitOpError("is violated at compile time: ") << op.getMsg();
472 result = failure();
473 } else if (!c) {
474 op.emitOpError("runtime check cannot be encoded in a static TXN "
475 "binary; use the C++ builder (--aie-npu-to-cpp) "
476 "or specialize the value: ")
477 << op.getMsg();
478 result = failure();
479 }
480 })
481 .Case<NpuSyncOp>([&](auto op) {
482 count++;
483 uint32_t before = byteOffset();
484 if (failed(appendSync(instructions, op)))
485 result = failure();
486 pushLocEntry(locmap, before, byteOffset(), "TCT",
487 op->getName().getStringRef(), std::nullopt, op, tm);
488 })
489 .Case<NpuWrite32Op>([&](auto op) {
490 count++;
491 uint32_t before = byteOffset();
492 uint64_t addr = op.getAbsoluteAddress().value_or(0);
493 if (failed(appendWrite32(instructions, op)))
494 result = failure();
495 pushLocEntry(locmap, before, byteOffset(), "WRITE32",
496 op->getName().getStringRef(), addr, op, tm);
497 })
498 .Case<NpuBlockWriteOp>([&](auto op) {
499 count++;
500 uint32_t before = byteOffset();
501 std::optional<uint32_t> addr = op.getAbsoluteAddress(&names);
502 if (failed(appendBlockWrite(instructions, op, addr, symTab,
503 blockWriteDataCache)))
504 result = failure();
505 pushLocEntry(locmap, before, byteOffset(), "BLOCKWRITE",
506 op->getName().getStringRef(), addr.value_or(0), op, tm);
507 })
508 .Case<NpuBlockWriteValuesOp>([&](auto op) {
509 // A runtime-computed blockwrite payload has no static encoding;
510 // this op only reaches the EmitC (C++ TXN) target.
511 op.emitOpError("cannot translate a runtime-valued blockwrite "
512 "payload to a static TXN binary; this op is "
513 "supported only by the C++ TXN target "
514 "(--aie-npu-to-cpp)");
515 result = failure();
516 })
517 .Case<NpuMaskWrite32Op>([&](auto op) {
518 count++;
519 uint32_t before = byteOffset();
520 uint64_t addr = op.getAbsoluteAddress().value_or(0);
521 if (failed(appendMaskWrite32(instructions, op)))
522 result = failure();
523 pushLocEntry(locmap, before, byteOffset(), "MASKWRITE",
524 op->getName().getStringRef(), addr, op, tm);
525 })
526 .Case<NpuMaskPollOp>([&](auto op) {
527 count++;
528 uint32_t before = byteOffset();
529 uint64_t addr = op.getAbsoluteAddress().value_or(0);
530 if (failed(appendMaskPoll(instructions, op)))
531 result = failure();
532 pushLocEntry(locmap, before, byteOffset(), "MASKPOLL",
533 op->getName().getStringRef(), addr, op, tm);
534 })
535 .Case<NpuLoadPdiOp>([&](auto op) {
536 count++;
537 uint32_t before = byteOffset();
538 appendLoadPdi(instructions, op);
539 pushLocEntry(locmap, before, byteOffset(), "LOAD_PDI",
540 op->getName().getStringRef(), std::nullopt, op, tm);
541 })
542 .Case<NpuAddressPatchOp>([&](auto op) {
543 count++;
544 uint32_t before = byteOffset();
545 if (failed(appendAddressPatch(instructions, op, foldDDRAddrOffset)))
546 result = failure();
547 pushLocEntry(locmap, before, byteOffset(), "ADDRESS_PATCH",
548 op->getName().getStringRef(), op.getAddr(), op, tm);
549 })
550 .Case<NpuPreemptOp>([&](auto op) {
551 count++;
552 uint32_t before = byteOffset();
553 appendPreempt(instructions, op);
554 pushLocEntry(locmap, before, byteOffset(), "PREEMPT",
555 op->getName().getStringRef(), std::nullopt, op, tm);
556 })
557 .Case<NpuCreateScratchpadOp>([&](auto op) {
558 count++;
559 uint32_t before = byteOffset();
560 appendCreateScratchpad(instructions, op);
561 pushLocEntry(locmap, before, byteOffset(), "CREATE_SCRATCHPAD",
562 op->getName().getStringRef(), std::nullopt, op, tm);
563 })
564 .Case<NpuUpdateFromScratchpadOp>([&](auto op) {
565 count++;
566 uint32_t before = byteOffset();
567 if (failed(appendUpdateRegFromScratchpad(instructions, op, names)))
568 result = failure();
569 pushLocEntry(locmap, before, byteOffset(), "UPDATE_FROM_SCRATCHPAD",
570 op->getName().getStringRef(), std::nullopt, op, tm);
571 })
572 .Case<NpuWriteConfigOp>([&](auto op) {
573 appendConfig(op, op.getDeviceRefAttr(), /*teardown=*/false);
574 })
575 .Case<NpuWriteTeardownOp>([&](auto op) {
576 appendConfig(op, op.getDeviceRefAttr(), /*teardown=*/true);
577 })
578 .Default([&](Operation *op) {
579 // Pure values (constants, the operands above) need no encoding;
580 // control packets have their own translation. Anything else would
581 // be dropped from the binary without a trace.
582 if (isMemoryEffectFree(op) || op->hasTrait<OpTrait::IsTerminator>() ||
583 isa<NpuControlPacketOp>(op))
584 return;
585 op->emitOpError("has no static TXN encoding");
586 result = failure();
587 });
588}
589
590} // namespace
591
593 Operation *device, bool teardown,
594 llvm::function_ref<LogicalResult(Config &)> encode) {
595 std::lock_guard<std::mutex> lock(mutex);
596 auto [it, inserted] = configs[teardown].try_emplace(device);
597 if (inserted) {
598 auto config = std::make_unique<Config>();
599 if (succeeded(encode(*config)))
600 it->second = std::move(config);
601 }
602 return it->second.get();
603}
604
606 mlir::ModuleOp moduleOp, std::vector<uint32_t> &instructions,
607 StringRef deviceName, StringRef sequenceName,
608 std::vector<TxnLocEntry> *locmap, bool foldDDRAddrOffset,
609 NpuConfigCache *configs) {
610
611 DeviceOp deviceOp =
612 DeviceOp::getForSymbolInModuleOrError(moduleOp, deviceName);
613 if (!deviceOp) {
614 return failure();
615 }
616
617 const AIETargetModel &tm = deviceOp.getTargetModel();
618
619 // Reserve the 4-word header up front; finalized in-place by
620 // txn_prepend_header once all instructions are appended.
621 aie_runtime::txn_init(instructions);
622
624 txnDeviceGen(tm), tm.rows(), tm.columns(), tm.getNumMemTileRows());
625
626 AIE::RuntimeSequenceOp seq =
627 AIE::RuntimeSequenceOp::getForSymbolInDeviceOrError(deviceOp,
628 sequenceName);
629 if (!seq) {
630 return failure();
631 }
632
633 NpuConfigCache ownConfigs;
634 TxnEncoder encoder(deviceOp, instructions, locmap, foldDDRAddrOffset,
635 configs ? *configs : ownConfigs);
636 for (Block &block : seq.getBody())
637 for (Operation &o : block)
638 encoder.encode(o);
639 if (failed(encoder.result))
640 return failure();
641
642 // Finalize the TXN header (overwrites the 4 reserved words).
643 aie_runtime::txn_prepend_header(instructions, encoder.count, devInfo);
644 return success();
645}
646
648 ModuleOp module, std::vector<uint32_t> &instructions, StringRef deviceName,
649 StringRef sequenceName, std::vector<TxnLocEntry> *locmap) {
650 DeviceOp deviceOp =
651 AIE::DeviceOp::getForSymbolInModuleOrError(module, deviceName);
652 if (!deviceOp) {
653 return failure();
654 }
655 AIE::RuntimeSequenceOp seq =
656 AIE::RuntimeSequenceOp::getForSymbolInDeviceOrError(deviceOp,
657 sequenceName);
658 if (!seq) {
659 return failure();
660 }
661
662 const AIETargetModel &tm = deviceOp.getTargetModel();
663 Block &entry = seq.getBody().front();
664 for (auto &o : entry) {
665 auto packetOp = dyn_cast<AIEX::NpuControlPacketOp>(o);
666 if (!packetOp)
667 continue;
668
669 uint32_t before =
670 static_cast<uint32_t>(instructions.size() * sizeof(uint32_t));
671
672 uint32_t size = 0;
673 auto data = packetOp.getData();
674 if (data)
675 size = data->size();
676
677 // Emit: stream_header (1 word) + ctrl_header (1 word) + data (size words).
678 // The stream header contains the routing pkt_id so the stream switch can
679 // route each control packet to the correct tile's TileControl port.
680 // The DMA BD does NOT use enable_packet -- it sends raw data, and the
681 // stream switch parses the embedded packet headers from the data stream.
682 auto words = reserveAndGetTail(instructions, 2 + size);
683
684 std::optional<uint32_t> length = packetOp.getLength();
685 if (!data && length)
686 size = *length;
687
688 auto parity = [](uint32_t n) {
689 uint32_t p = 0;
690 while (n) {
691 p += n & 1;
692 n >>= 1;
693 }
694 return (p % 2) == 0;
695 };
696
697 // stream header with routing pkt_id from target tile's controller_id
698 int col = packetOp.getColumnFromAddr();
699 int row = packetOp.getRowFromAddr();
700 DeviceOp deviceOp2 = packetOp->getParentOfType<AIE::DeviceOp>();
701 OpBuilder builder2 = OpBuilder::atBlockBegin(deviceOp2.getBody());
702 auto destTile = TileOp::getOrCreate(builder2, deviceOp2, col, row);
703 auto info = destTile->getAttrOfType<AIE::PacketInfoAttr>("controller_id");
704 uint32_t hdr = 0;
705 if (info)
706 hdr = (info.getPktType() & 0x7) << 12 | (info.assignedId() & 0xff);
707 words[0] = hdr | (0x1 & parity(hdr)) << 31;
708
709 // `beats` gets two bits, directly above the address, so an oversized
710 // payload corrupts the address instead of truncating, and `size - 1`
711 // underflows the same way when size is 0. Enforced here (not by a
712 // verifier) since ops may carry more before
713 // --aie-legalize-control-packet splits them.
714 bool sizeFromLength = !data && length;
715 const char *what = sizeFromLength ? "length" : "payload";
716 if (size == 0)
717 return packetOp.emitOpError()
718 << what
719 << " is empty; a control packet must carry at least 1 "
720 "word on the wire";
721 if (size > AIEX::NpuControlPacketOp::getMaxDataWords())
722 return packetOp.emitOpError()
723 << what << " is " << size
724 << " words; a control packet carries at most "
725 << AIEX::NpuControlPacketOp::getMaxDataWords()
726 << " on the wire. Run --aie-legalize-control-packet before "
727 "translating.";
728
729 uint32_t addr = packetOp.getAddress() & 0xFFFFF;
730 uint32_t beats = size - 1;
731 uint32_t opc = packetOp.getOpcode();
732 uint32_t id = packetOp.getStreamId();
733 hdr = id << 24 | opc << 22 | beats << 20 | addr;
734 words[1] = hdr | (0x1 & parity(hdr)) << 31;
735
736 // configuration data
737 if (opc == 0x0 || opc == 0x2) {
738 if (!data)
739 return packetOp.emitOpError(
740 "control packet with a write opcode requires a data payload");
741 for (unsigned i = 0; i < size; i++)
742 words[i + 2] = (*data)[i];
743 }
744
745 uint32_t after =
746 static_cast<uint32_t>(instructions.size() * sizeof(uint32_t));
747 pushLocEntry(locmap, before, after, "CONTROL_PACKET",
748 packetOp->getName().getStringRef(), packetOp.getAddress(),
749 packetOp, tm);
750 }
751 return success();
752}
753
754// Render an mlir::Location as a JSON object, recursing into NameLoc, FusedLoc,
755// and CallSiteLoc so a consumer can reconstruct the file:line:col + named
756// location structure carried by the source op.
757static llvm::json::Value locToJSON(mlir::Location loc) {
758 using namespace llvm;
759 if (auto f = dyn_cast<mlir::FileLineColLoc>(loc))
760 return json::Value(json::Object{
761 {"kind", "file"},
762 {"file", f.getFilename().str()},
763 {"line", static_cast<int64_t>(f.getLine())},
764 {"col", static_cast<int64_t>(f.getColumn())},
765 });
766 if (auto n = dyn_cast<mlir::NameLoc>(loc)) {
767 json::Object o{{"kind", "name"}, {"name", n.getName().str()}};
768 o["child"] = locToJSON(n.getChildLoc());
769 return json::Value(std::move(o));
770 }
771 if (auto fused = dyn_cast<mlir::FusedLoc>(loc)) {
772 json::Array children;
773 for (mlir::Location c : fused.getLocations())
774 children.push_back(locToJSON(c));
775 return json::Value(
776 json::Object{{"kind", "fused"}, {"children", std::move(children)}});
777 }
778 if (auto cs = dyn_cast<mlir::CallSiteLoc>(loc))
779 return json::Value(json::Object{
780 {"kind", "callsite"},
781 {"callee", locToJSON(cs.getCallee())},
782 {"caller", locToJSON(cs.getCaller())},
783 });
784 if (isa<mlir::OpaqueLoc>(loc))
785 return json::Value(json::Object{{"kind", "opaque"}});
786 return json::Value(json::Object{{"kind", "unknown"}});
787}
788
789void xilinx::AIE::emitNpuLocmapJSON(llvm::raw_ostream &output,
790 llvm::StringRef deviceName,
791 llvm::StringRef binaryName,
792 const std::vector<TxnLocEntry> &locmap) {
793 llvm::json::Object root;
794 root["version"] = 1;
795 root["device"] = deviceName.str();
796 root["binary"] = binaryName.str();
797
798 llvm::json::Array opsArr;
799 for (const auto &e : locmap) {
800 llvm::json::Object o;
801 o["byte_offset"] = static_cast<int64_t>(e.byteOffset);
802 o["byte_size"] = static_cast<int64_t>(e.byteSize);
803 o["opcode"] = e.opcodeName;
804 o["source_op"] = e.sourceOpName;
805 if (e.address)
806 o["address"] = llvm::formatv("{0:X}", *e.address).str();
807 if (!e.registerName.empty()) {
808 o["register"] = e.registerName;
809 o["register_module"] = e.registerModule;
810 }
811 if (e.loc)
812 o["loc"] = locToJSON(*e.loc);
813 opsArr.push_back(std::move(o));
814 }
815 root["operations"] = std::move(opsArr);
816
817 output << llvm::formatv("{0:2}\n", llvm::json::Value(std::move(root)));
818}
bool isMemTile(int col, int row) const
Return true if the given tile is a Mem tile.
bool isShimPLTile(int col, int row) const
Return true if the given tile is a ShimPL tile.
virtual int rows() const =0
Return the number of rows in the device.
bool isShimNOCTile(int col, int row) const
Return true if the given tile is a ShimNOC tile.
virtual uint32_t getColumnShift() const =0
virtual int columns() const =0
Return the number of columns in the device.
virtual uint32_t getNumMemTileRows() const =0
virtual uint32_t getRowShift() const =0
const Config * getOrEncode(mlir::Operation *device, bool teardown, llvm::function_ref< mlir::LogicalResult(Config &)> encode)
Register and event database for a specific architecture.
static std::unique_ptr< RegisterDatabase > loadAIE2()
Load database for AIE2 architecture.
const RegisterInfo * lookupRegisterByOffset(uint32_t offset, llvm::StringRef module) const
Lookup register by tile-relative offset and module name.
void txn_append_maskpoll32(std::vector< uint32_t > &txn, uint32_t addr, uint32_t val, uint32_t mask)
void txn_append_loadpdi(std::vector< uint32_t > &txn, uint32_t id, uint32_t size, uint64_t addr)
void txn_append_write32(std::vector< uint32_t > &txn, uint32_t addr, uint32_t val)
void txn_init(std::vector< uint32_t > &txn)
void txn_append_sync(std::vector< uint32_t > &txn, uint32_t col, uint32_t row, uint32_t dir, uint32_t chan, uint32_t ncol, uint32_t nrow)
void txn_append_preempt(std::vector< uint32_t > &txn, uint32_t level)
void txn_append_arg_patch(std::vector< uint32_t > &txn, uint32_t addr, int32_t arg_idx, uint64_t arg_plus, bool fold_ddr_addr_offset)
TxnDeviceInfo txn_device_info(uint8_t dev_gen, uint8_t num_rows, uint8_t num_cols, uint8_t num_memtile_rows)
@ TXN_OPC_CREATE_SCRATCHPAD
Definition TxnEncoding.h:56
void txn_append_maskwrite32(std::vector< uint32_t > &txn, uint32_t addr, uint32_t val, uint32_t mask)
void txn_append_blockwrite(std::vector< uint32_t > &txn, uint32_t addr, const uint32_t *data, size_t count, uint32_t col=0, uint32_t row=0)
void txn_prepend_header(std::vector< uint32_t > &txn, uint32_t op_count, TxnDeviceInfo info={})
std::optional< uint64_t > getConstantInt64Operand(mlir::Value v)
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
void emitNpuLocmapJSON(llvm::raw_ostream &output, llvm::StringRef deviceName, llvm::StringRef binaryName, const std::vector< TxnLocEntry > &locmap)
uint8_t txnDeviceGen(const AIETargetModel &tm)
Device generation ID written into the TXN header (aie_runtime::TxnDeviceInfo::devGen).
mlir::LogicalResult AIETranslateControlPacketsToUI32Vec(mlir::ModuleOp, std::vector< uint32_t > &, llvm::StringRef deviceName="", llvm::StringRef sequenceName="", std::vector< TxnLocEntry > *locmap=nullptr)
mlir::LogicalResult AIETranslateNpuToBinary(mlir::ModuleOp, std::vector< uint32_t > &, llvm::StringRef deviceName="", llvm::StringRef sequenceName="", std::vector< TxnLocEntry > *locmap=nullptr, bool foldDDRAddrOffset=true, NpuConfigCache *configs=nullptr)
mlir::LogicalResult generateAndInsertConfigOps(mlir::OpBuilder &builder, xilinx::AIE::DeviceOp device, xilinx::AIEX::BlockwriteData &blockwriteData, llvm::StringRef clElfDir="", AIEToConfigurationOutputType outputType=AIEToConfigurationOutputType::Transaction, bool skipCtrlPktOverlay=false)
const AIETargetModel & getTargetModel(mlir::Operation *op)
mlir::LogicalResult generateAndInsertTeardownOps(mlir::OpBuilder &builder, xilinx::AIE::DeviceOp device, xilinx::AIEX::BlockwriteData &blockwriteData, AIEToConfigurationOutputType outputType=AIEToConfigurationOutputType::Transaction)
std::vector< uint32_t > words
Definition AIETargets.h:80
std::vector< TxnLocEntry > locmap
Definition AIETargets.h:83
std::optional< uint64_t > address
Definition AIETargets.h:46
std::string registerModule
Definition AIETargets.h:49
std::optional< mlir::Location > loc
Definition AIETargets.h:50