MLIR-AIE 1.4.6.dev
AIEToConfiguration.cpp
Go to the documentation of this file.
1//===- AIEToConfiguration.h -------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2024 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
8#include "../PassDetail.h"
9#include "TransactionBinary.h"
10
15#include "aie/Targets/AIERT.h"
16
17#include "mlir/Dialect/Arith/IR/Arith.h"
18#include "mlir/Dialect/MemRef/IR/MemRef.h"
19#include "mlir/IR/IRMapping.h"
20
21#include "llvm/ADT/APInt.h"
22#include "llvm/ADT/DenseMap.h"
23#include "llvm/Support/Debug.h"
24
25extern "C" {
26#include "xaiengine/xaiegbl.h"
27// above needs to go first for u32/u64 typedefs, AieRC, and XAIE_AIG_EXPORT
28// used by xaie_txn.h
29#include "xaiengine/xaie_txn.h"
30}
31
32#include <array>
33#include <cstring>
34#include <optional>
35#include <utility>
36#include <vector>
37
38namespace xilinx {
39#define GEN_PASS_DEF_CONVERTAIETOCONTROLPACKETS
40#define GEN_PASS_DEF_CONVERTAIETOTRANSACTION
41#include "aie/Conversion/Passes.h.inc"
42} // namespace xilinx
43
44#define DEBUG_TYPE "aie-convert-to-config"
45
46using namespace mlir;
47using namespace xilinx;
48using namespace xilinx::AIE;
49
50namespace {
51
52constexpr size_t kTxnHeaderBytes = 16;
53
54struct TxnPreemptHeader {
55 uint8_t opcode;
56 uint8_t level;
57 uint16_t reserved;
58};
59
60struct TxnLoadPdiHeader {
61 uint8_t opcode;
62 uint8_t padding;
63 uint16_t id;
64 uint32_t size;
65 uint64_t address;
66};
67} // namespace
68
69// Parse a TXN binary blob. On success return the number of columns from the
70// header and a vector of parsed operations. On failure return std::nullopt.
72 const std::vector<uint8_t> &data,
73 std::vector<TransactionBinaryOperation> &ops) {
74
75 if (data.size() < kTxnHeaderBytes) {
76 llvm::errs() << "Transaction binary is too small for header\n";
77 return std::nullopt;
78 }
79
80 uint32_t major = data[0];
81 uint32_t minor = data[1];
82 uint32_t num_cols = data[4];
83
84 uint32_t num_ops, txn_size;
85 std::memcpy(&num_ops, &data[8], 4);
86 std::memcpy(&txn_size, &data[12], 4);
87
88 LLVM_DEBUG(llvm::dbgs() << "Major: " << major << "\n");
89 LLVM_DEBUG(llvm::dbgs() << "Minor: " << minor << "\n");
90 LLVM_DEBUG(llvm::dbgs() << "DevGen: " << data[2] << "\n");
91 LLVM_DEBUG(llvm::dbgs() << "NumRows: " << data[3] << "\n");
92 LLVM_DEBUG(llvm::dbgs() << "NumCols: " << num_cols << "\n");
93 LLVM_DEBUG(llvm::dbgs() << "NumMemTileRows: " << data[5] << "\n");
94 LLVM_DEBUG(llvm::dbgs() << "NumOps: " << num_ops << "\n");
95 LLVM_DEBUG(llvm::dbgs() << "TxnSize: " << txn_size << " bytes\n");
96
97 size_t i = kTxnHeaderBytes;
98
99 auto requireBytes = [&](size_t offset, size_t length) -> bool {
100 if (offset + length > data.size()) {
101 llvm::errs() << "Transaction binary truncated while parsing opcode\n";
102 return false;
103 }
104 return true;
105 };
106
107 auto read32 = [&](size_t offset) -> uint32_t {
108 uint32_t value;
109 std::memcpy(&value, data.data() + offset, sizeof(uint32_t));
110 return value;
111 };
112
113 // Convert opcode from uint8 to a validated opcode byte
114 auto convertOpcode = [](uint8_t opc) -> std::optional<uint8_t> {
115 switch (opc) {
116 case static_cast<uint8_t>(XAie_TxnOpcode::XAIE_IO_WRITE):
117 case static_cast<uint8_t>(XAie_TxnOpcode::XAIE_IO_BLOCKWRITE):
118 case static_cast<uint8_t>(XAie_TxnOpcode::XAIE_IO_MASKWRITE):
119 case 0x6: // XAie_TxnOpcode::XAIE_IO_PREEMPT
120 case 0x8: // XAie_TxnOpcode::XAIE_IO_LOAD_PDI
121 case static_cast<uint8_t>(XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_TCT):
122 case static_cast<uint8_t>(XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_DDR_PATCH):
123 return opc;
124 default:
125 llvm::errs() << "Unhandled opcode: " << std::to_string(opc) << "\n";
126 return std::nullopt;
127 }
128 };
129
130 // Parse the binary blob. There are two versions supported, 0.1 and 1.0.
131 // For both versions, build a list of TransactionBinaryOperation objects
132 // representing the parsed operations.
133 if (major == 0 && minor == 1) {
134 while (i < data.size()) {
135 auto maybeOpcode = convertOpcode(data[i]);
136 if (!maybeOpcode)
137 return std::nullopt;
138 XAie_TxnOpcode opcode = static_cast<XAie_TxnOpcode>(*maybeOpcode);
139 LLVM_DEBUG(llvm::dbgs() << "opcode: " + std::to_string(opcode) << "\n");
140
142 op.cmd.Opcode = opcode;
143
144 switch (opcode) {
145 case XAie_TxnOpcode::XAIE_IO_WRITE: {
146 LLVM_DEBUG(llvm::dbgs() << "opcode: WRITE (0x00)\n");
147 if (!requireBytes(i, 24))
148 return std::nullopt;
149 uint32_t addrLo = read32(i + 8);
150 uint32_t addrHi = read32(i + 12);
151 uint32_t value = read32(i + 16);
152 uint32_t opSize = read32(i + 20);
153 if (!requireBytes(i, opSize))
154 return std::nullopt;
155 uint64_t addr = (static_cast<uint64_t>(addrHi) << 32) | addrLo;
156 op.cmd.RegOff = addr;
157 op.cmd.Value = value;
158 op.cmd.Size = 0;
159 i += opSize;
160 break;
161 }
162 case XAie_TxnOpcode::XAIE_IO_BLOCKWRITE: {
163 LLVM_DEBUG(llvm::dbgs() << "opcode: BLOCKWRITE (0x01)\n");
164 if (!requireBytes(i, 16))
165 return std::nullopt;
166 uint32_t addr = read32(i + 8);
167 uint32_t opSize = read32(i + 12);
168 if (opSize < 16 || !requireBytes(i, opSize))
169 return std::nullopt;
170 const uint8_t *payload = data.data() + i + 16;
171 uint32_t payloadBytes = opSize - 16;
172 op.cmd.RegOff = addr;
173 op.cmd.DataPtr = reinterpret_cast<uint64_t>(payload);
174 op.cmd.Size = payloadBytes;
175 i += opSize;
176 break;
177 }
178 case XAie_TxnOpcode::XAIE_IO_MASKWRITE: {
179 LLVM_DEBUG(llvm::dbgs() << "opcode: MASKWRITE (0x03)\n");
180 if (!requireBytes(i, 28))
181 return std::nullopt;
182 uint32_t addrLo = read32(i + 8);
183 uint32_t addrHi = read32(i + 12);
184 uint32_t value = read32(i + 16);
185 uint32_t mask = read32(i + 20);
186 uint32_t opSize = read32(i + 24);
187 if (!requireBytes(i, opSize))
188 return std::nullopt;
189 uint64_t addr = (static_cast<uint64_t>(addrHi) << 32) | addrLo;
190 op.cmd.RegOff = addr;
191 op.cmd.Value = value;
192 op.cmd.Mask = mask;
193 op.cmd.Size = opSize;
194 i += opSize;
195 break;
196 }
197 case XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_TCT: {
198 uint32_t opSize = read32(i + 4);
199 if (opSize < 16 || !requireBytes(i, opSize))
200 return std::nullopt;
201 uint32_t descriptor = read32(i + 8);
202 uint32_t config = read32(i + 12);
204 /*column=*/static_cast<int32_t>((descriptor >> 16) & 0xff),
205 /*row=*/static_cast<int32_t>((descriptor >> 8) & 0xff),
206 /*direction=*/static_cast<int32_t>(descriptor & 0xff),
207 /*channel=*/static_cast<int32_t>((config >> 24) & 0xff),
208 /*columnCount=*/static_cast<int32_t>((config >> 16) & 0xff),
209 /*rowCount=*/static_cast<int32_t>((config >> 8) & 0xff)};
210 op.sync = payload;
211 op.cmd.Size = opSize;
212 i += opSize;
213 break;
214 }
215 // Not in the vendored third_party/aie-rt enum (though the driver
216 // protocol defines it); can't add it without forking upstream.
217 case 0x8: { // NOLINT(clang-diagnostic-switch):
218 // XAie_TxnOpcode::XAIE_IO_LOAD_PDI
219 LLVM_DEBUG(llvm::dbgs() << "opcode: LOAD_PDI (0x08)\n");
220 constexpr size_t opSize = sizeof(TxnLoadPdiHeader);
221 if (!requireBytes(i, opSize))
222 return std::nullopt;
223 TxnLoadPdiHeader header;
224 std::memcpy(&header, data.data() + i, opSize);
226 header.id, header.size, header.address};
227 op.loadPdi = payload;
228 op.cmd.Size = opSize;
229 i += opSize;
230 break;
231 }
232 case XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_DDR_PATCH: {
233 uint32_t opSize = read32(i + 4);
234 if (opSize < 44 || !requireBytes(i, opSize))
235 return std::nullopt;
236 uint32_t action = read32(i + 20);
237 uint32_t addr = read32(i + 24);
238 int32_t argIdx = static_cast<int32_t>(read32(i + 32));
239 uint64_t argPlus = static_cast<uint64_t>(read32(i + 40)) |
240 (static_cast<uint64_t>(read32(i + 44)) << 32);
242 action, addr, argIdx, argPlus};
243 op.addressPatch = payload;
244 op.cmd.Size = opSize;
245 i += opSize;
246 break;
247 }
248 case 0x6: { // XAie_TxnOpcode::XAIE_IO_PREEMPT
249 LLVM_DEBUG(llvm::dbgs() << "opcode: PREEMPT (0x06)\n");
250 constexpr size_t opSize = sizeof(TxnPreemptHeader);
251 if (!requireBytes(i, opSize))
252 return std::nullopt;
253 const auto *header =
254 reinterpret_cast<const TxnPreemptHeader *>(data.data() + i);
255 op.cmd.Value = header->level;
256 op.cmd.Size = opSize;
257 i += opSize;
258 break;
259 }
260 default:
261 llvm::errs() << "Unhandled opcode: " << std::to_string(opcode)
262 << " for v0.1 transaction\n";
263 return std::nullopt;
264 }
265
266 ops.push_back(op);
267 }
268 } else if (major == 1 && minor == 0) {
269 while (i < data.size()) {
270 auto maybeOpcode = convertOpcode(data[i]);
271 if (!maybeOpcode)
272 return std::nullopt;
273 XAie_TxnOpcode opcode = static_cast<XAie_TxnOpcode>(*maybeOpcode);
274 LLVM_DEBUG(llvm::dbgs() << "opcode: " + std::to_string(opcode) << "\n");
275
277 op.cmd.Opcode = opcode;
278
279 switch (opcode) {
280 case XAie_TxnOpcode::XAIE_IO_WRITE: {
281 LLVM_DEBUG(llvm::dbgs() << "opcode: WRITE (0x00)\n");
282 if (!requireBytes(i, 12))
283 return std::nullopt;
284 uint32_t addr = read32(i + 4);
285 uint32_t value = read32(i + 8);
286 op.cmd.RegOff = addr;
287 op.cmd.Value = value;
288 op.cmd.Size = 0;
289 i += 12;
290 break;
291 }
292 case XAie_TxnOpcode::XAIE_IO_BLOCKWRITE: {
293 LLVM_DEBUG(llvm::dbgs() << "opcode: BLOCKWRITE (0x01)\n");
294 if (!requireBytes(i, 12))
295 return std::nullopt;
296 uint32_t addr = read32(i + 4);
297 uint32_t opSize = read32(i + 8);
298 if (opSize < 12 || !requireBytes(i, opSize))
299 return std::nullopt;
300 const uint8_t *payload = data.data() + i + 12;
301 uint32_t payloadBytes = opSize - 12;
302 op.cmd.RegOff = addr;
303 op.cmd.DataPtr = reinterpret_cast<uint64_t>(payload);
304 op.cmd.Size = payloadBytes;
305 i += opSize;
306 break;
307 }
308 case XAie_TxnOpcode::XAIE_IO_MASKWRITE: {
309 LLVM_DEBUG(llvm::dbgs() << "opcode: MASKWRITE (0x03)\n");
310 if (!requireBytes(i, 16))
311 return std::nullopt;
312 uint32_t addr = read32(i + 4);
313 uint32_t value = read32(i + 8);
314 uint32_t mask = read32(i + 12);
315 op.cmd.RegOff = addr;
316 op.cmd.Value = value;
317 op.cmd.Mask = mask;
318 op.cmd.Size = 0;
319 i += 16;
320 break;
321 }
322 case XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_TCT: {
323 uint32_t opSize = read32(i + 4);
324 if (opSize < 16 || !requireBytes(i, opSize))
325 return std::nullopt;
326 uint32_t descriptor = read32(i + 8);
327 uint32_t config = read32(i + 12);
329 /*column=*/static_cast<int32_t>((descriptor >> 16) & 0xff),
330 /*row=*/static_cast<int32_t>((descriptor >> 8) & 0xff),
331 /*direction=*/static_cast<int32_t>(descriptor & 0xff),
332 /*channel=*/static_cast<int32_t>((config >> 24) & 0xff),
333 /*columnCount=*/static_cast<int32_t>((config >> 16) & 0xff),
334 /*rowCount=*/static_cast<int32_t>((config >> 8) & 0xff)};
335 op.sync = payload;
336 op.cmd.Size = opSize;
337 i += opSize;
338 break;
339 }
340 // Not in the vendored third_party/aie-rt enum (though the driver
341 // protocol defines it); can't add it without forking upstream.
342 case 0x8: { // NOLINT(clang-diagnostic-switch):
343 // XAie_TxnOpcode::XAIE_IO_LOAD_PDI
344 LLVM_DEBUG(llvm::dbgs() << "opcode: LOAD_PDI (0x08)\n");
345 constexpr size_t opSize = sizeof(TxnLoadPdiHeader);
346 if (!requireBytes(i, opSize))
347 return std::nullopt;
348 TxnLoadPdiHeader header;
349 std::memcpy(&header, data.data() + i, opSize);
351 header.id, header.size, header.address};
352 op.loadPdi = payload;
353 op.cmd.Size = opSize;
354 i += opSize;
355 break;
356 }
357 case XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_DDR_PATCH: {
358 uint32_t opSize = read32(i + 4);
359 if (opSize < 44 || !requireBytes(i, opSize))
360 return std::nullopt;
361 uint32_t action = read32(i + 20);
362 uint32_t addr = read32(i + 24);
363 int32_t argIdx = static_cast<int32_t>(read32(i + 32));
364 uint64_t argPlus = static_cast<uint64_t>(read32(i + 40)) |
365 (static_cast<uint64_t>(read32(i + 44)) << 32);
367 action, addr, argIdx, argPlus};
368 op.addressPatch = payload;
369 op.cmd.Size = opSize;
370 i += opSize;
371 break;
372 }
373 case 0x6: { // XAie_TxnOpcode::XAIE_IO_PREEMPT
374 LLVM_DEBUG(llvm::dbgs() << "opcode: PREEMPT (0x06)\n");
375 constexpr size_t opSize = sizeof(TxnPreemptHeader);
376 if (!requireBytes(i, opSize))
377 return std::nullopt;
378 const auto *header =
379 reinterpret_cast<const TxnPreemptHeader *>(data.data() + i);
380 op.cmd.Value = header->level;
381 op.cmd.Size = opSize;
382 i += opSize;
383 break;
384 }
385 default:
386 llvm::errs() << "Unhandled opcode: " << std::to_string(opcode)
387 << " for v1.0 transaction\n";
388 return std::nullopt;
389 }
390
391 ops.push_back(op);
392 }
393 } else {
394 llvm::errs() << "Unsupported TXN binary version: " << major << "." << minor
395 << "\n";
396 return std::nullopt;
397 }
398
399 return num_cols;
400}
401
402static LogicalResult generateTransactions(AIERTControl &ctl,
403 const StringRef workDirPath,
404 DeviceOp &targetOp, bool aieSim,
405 bool enableElfs, bool enableInit,
406 bool enableCores,
407 bool skipCtrlPktOverlay = false) {
408 if (enableElfs && !targetOp.getOps<CoreOp>().empty() &&
409 failed(ctl.addAieElfs(targetOp, workDirPath, aieSim)))
410 return failure();
411 if (enableInit && failed(ctl.addInitConfig(targetOp, skipCtrlPktOverlay)))
412 return failure();
413 if (enableCores && !targetOp.getOps<CoreOp>().empty() &&
414 failed(ctl.addCoreEnable(targetOp)))
415 return failure();
416 return success();
417}
418
419// Translate vector of TransactionBinaryOperation to a sequence of transaction
420// ops (npu.write32, npu.maskwrite32, npu.blockwrite). Each emitted op gets the
421// per-op `sourceLoc` from AIERT's instruction-range bracketing if available;
422// otherwise it falls back to `fallbackLoc` (typically the device location).
423static LogicalResult
424emitTransactionOps(OpBuilder &builder, Location fallbackLoc,
425 std::vector<TransactionBinaryOperation> &operations,
426 std::vector<memref::GlobalOp> &global_data) {
427
428 // One constant per distinct value. They all land in the same block through
429 // `builder`, so the first use dominates every later one.
430 //
431 // Because the first use is what creates the op, callers must bind the
432 // operands to locals rather than call this inside an argument list: the
433 // evaluation order of function arguments is unspecified, and it decides the
434 // order the constants are emitted in.
435 llvm::DenseMap<uint32_t, Value> constantCache;
436 auto constant = [&](Location loc, uint32_t value) {
437 Value &cached = constantCache[value];
438 if (!cached)
439 cached = AIEX::createConstantI32(builder, loc, value);
440 return cached;
441 };
442
443 // create the txn ops
444 for (auto [op, payload] : llvm::zip(operations, global_data)) {
445 Location loc = op.sourceLoc.value_or(fallbackLoc);
446
447 if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_WRITE) {
448 Value address = constant(loc, op.cmd.RegOff);
449 Value value = constant(loc, op.cmd.Value);
450 AIEX::NpuWrite32Op::create(builder, loc, address, value, nullptr, nullptr,
451 nullptr);
452 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_BLOCKWRITE) {
453 auto memref = memref::GetGlobalOp::create(builder, loc, payload.getType(),
454 payload.getName());
455 AIEX::NpuBlockWriteOp::create(
456 builder, loc, builder.getUI32IntegerAttr(op.cmd.RegOff),
457 memref.getResult(), nullptr, nullptr, nullptr);
458 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_MASKWRITE) {
459 Value address = constant(loc, op.cmd.RegOff);
460 Value value = constant(loc, op.cmd.Value);
461 Value mask = constant(loc, op.cmd.Mask);
462 AIEX::NpuMaskWrite32Op::create(builder, loc, address, value, mask,
463 nullptr, nullptr, nullptr);
464 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_TCT) {
465 if (!op.sync) {
466 llvm::errs() << "Missing sync payload while emitting transaction\n";
467 return failure();
468 }
469 const TransactionBinaryOperation::SyncPayload &sync = *op.sync;
470 Value column = constant(loc, sync.column);
471 Value row = constant(loc, sync.row);
472 Value direction = constant(loc, sync.direction);
473 Value channel = constant(loc, sync.channel);
474 Value columnCount = constant(loc, sync.columnCount);
475 Value rowCount = constant(loc, sync.rowCount);
476 AIEX::NpuSyncOp::create(builder, loc, column, row, direction, channel,
477 columnCount, rowCount);
478 } else if (op.cmd.Opcode == 0x8 /* XAie_TxnOpcode::XAIE_IO_LOAD_PDI */) {
479 if (!op.loadPdi) {
480 llvm::errs() << "Missing load_pdi payload while emitting transaction\n";
481 return failure();
482 }
484 *op.loadPdi;
485 auto idAttr =
486 builder.getI32IntegerAttr(static_cast<int32_t>(payloadInfo.id));
487 IntegerAttr sizeAttr =
488 builder.getI32IntegerAttr(static_cast<int32_t>(payloadInfo.size));
489
490 auto ui64Ty =
491 IntegerType::get(builder.getContext(), 64, IntegerType::Unsigned);
492 IntegerAttr addressAttr =
493 IntegerAttr::get(ui64Ty, llvm::APInt(64, payloadInfo.address));
494
495 AIEX::NpuLoadPdiOp::create(builder, loc, /*device_ref=*/nullptr, idAttr,
496 sizeAttr, addressAttr,
497 /*expand_mode=*/nullptr);
498 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_CUSTOM_OP_DDR_PATCH) {
499 if (!op.addressPatch) {
500 llvm::errs()
501 << "Missing address_patch payload while emitting transaction\n";
502 return failure();
503 }
505 *op.addressPatch;
506 AIEX::NpuAddressPatchOp::create(
507 builder, loc, patch.addr,
508 /*addr_val=*/mlir::Value(), static_cast<int32_t>(patch.argIdx),
509 AIEX::createConstantArgPlus(builder, loc, patch.argPlus));
510 } else if (op.cmd.Opcode == 0x6 /* XAie_TxnOpcode::XAIE_IO_PREEMPT */) {
511 auto ui8Ty =
512 IntegerType::get(builder.getContext(), 8, IntegerType::Unsigned);
513 auto levelAttr = IntegerAttr::get(ui8Ty, llvm::APInt(8, op.cmd.Value));
514 AIEX::NpuPreemptOp::create(builder, loc, levelAttr);
515 } else {
516 llvm::errs() << "Unhandled txn opcode: " << op.cmd.Opcode << "\n";
517 return failure();
518 }
519 }
520 return success();
521}
522
523// Scans back from the insertion point to the last PDI load, which reprograms
524// the array.
525static llvm::DenseMap<uint32_t, uint32_t>
526registersWrittenSinceLoad(OpBuilder &builder) {
527 llvm::DenseMap<uint32_t, uint32_t> registers;
528 Block *block = builder.getInsertionBlock();
529 if (!block)
530 return registers;
531 Block::iterator it = builder.getInsertionPoint();
532 while (it != block->begin()) {
533 Operation &op = *--it;
534 if (isa<AIEX::NpuLoadPdiOp>(op))
535 break;
536 auto packet = dyn_cast<AIEX::NpuControlPacketOp>(op);
537 std::optional<ArrayRef<int32_t>> data =
538 packet ? packet.getData() : std::nullopt;
539 if (!data)
540 continue;
541 for (auto [i, word] : llvm::enumerate(*data))
542 registers.try_emplace(packet.getAddress() + i * sizeof(int32_t), word);
543 }
544 return registers;
545}
546
547// Translate vector of TransactionBinaryOperation to a sequence of control
548// packet ops. Each emitted op gets the per-op `sourceLoc` from AIERT's
549// instruction-range bracketing if available; otherwise it falls back to
550// `fallbackLoc`.
551static LogicalResult emitControlPacketOps(
552 OpBuilder &builder, Location fallbackLoc,
553 std::vector<TransactionBinaryOperation> &operations,
554 std::vector<memref::GlobalOp> &global_data,
555 const llvm::DenseMap<uint32_t, uint32_t> &initialRegisters) {
556
557 auto *ctx = builder.getContext();
558
559 // A control packet writes whole words, so a masked write becomes a write of
560 // the value the register would hold after it.
561 llvm::DenseMap<uint32_t, uint32_t> registers =
562 registersWrittenSinceLoad(builder);
563 for (auto [address, value] : initialRegisters)
564 registers.try_emplace(address, value);
565
566 // create the control packet ops
567 for (auto [op, payload] : llvm::zip(operations, global_data)) {
568 Location loc = op.sourceLoc.value_or(fallbackLoc);
569
570 if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_WRITE) {
571 registers[op.cmd.RegOff] = op.cmd.Value;
572 AIEX::NpuControlPacketOp::create(
573 builder, loc, builder.getUI32IntegerAttr(op.cmd.RegOff), nullptr,
574 /*opcode*/ builder.getI32IntegerAttr(0),
575 /*stream_id*/ builder.getI32IntegerAttr(0),
576 DenseI32ArrayAttr::get(ctx, ArrayRef<int32_t>(op.cmd.Value)));
577 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_BLOCKWRITE) {
578 auto initialValue = payload.getInitialValue();
579 if (!initialValue)
580 continue;
581 auto blockWriteData = dyn_cast<DenseIntElementsAttr>(*initialValue);
582 if (!blockWriteData) {
583 payload.emitError(
584 "Global symbol initial value is not a dense int array");
585 break;
586 }
587 auto blockWriteDataValues = blockWriteData.getValues<int32_t>();
588 // Split block write data into beats of 4 or less, in int32_t.
589 int currAddr = op.cmd.RegOff;
590 for (size_t i = 0; i < blockWriteDataValues.size(); i += 4) {
591 auto last = std::min(blockWriteDataValues.size(), i + 4);
592 SmallVector<int32_t> splitData =
593 SmallVector<int32_t>(blockWriteDataValues.begin() + i,
594 blockWriteDataValues.begin() + last);
595 for (auto [j, word] : llvm::enumerate(splitData))
596 registers[currAddr + j * sizeof(int32_t)] = word;
597 AIEX::NpuControlPacketOp::create(
598 builder, loc, builder.getUI32IntegerAttr(currAddr), nullptr,
599 /*opcode*/ builder.getI32IntegerAttr(0),
600 /*stream_id*/ builder.getI32IntegerAttr(0),
601 DenseI32ArrayAttr::get(ctx, ArrayRef<int32_t>(splitData)));
602 currAddr += splitData.size() * sizeof(int32_t);
603 }
604
605 } else if (op.cmd.Opcode == XAie_TxnOpcode::XAIE_IO_MASKWRITE) {
606 uint32_t &reg = registers[op.cmd.RegOff];
607 reg = (reg & ~op.cmd.Mask) | (op.cmd.Value & op.cmd.Mask);
608 int32_t value = reg;
609 AIEX::NpuControlPacketOp::create(
610 builder, loc, builder.getUI32IntegerAttr(op.cmd.RegOff), nullptr,
611 /*opcode*/ builder.getI32IntegerAttr(0),
612 /*stream_id*/ builder.getI32IntegerAttr(0),
613 DenseI32ArrayAttr::get(ctx, ArrayRef<int32_t>(value)));
614 } else {
615 llvm::errs() << "Unhandled txn opcode: " << op.cmd.Opcode << "\n";
616 return failure();
617 }
618 }
619 return success();
620}
621
622// Take transaction operations and insert them at the _current_ insertion point
623// of the supplied builder.
624static LogicalResult convertTransactionOpsToMLIR(
625 OpBuilder builder, AIE::AIEToConfigurationOutputType outputType,
626 std::vector<TransactionBinaryOperation> &operations,
627 AIEX::BlockwriteData &blockwriteData,
628 const llvm::DenseMap<uint32_t, uint32_t> &initialRegisters = {}) {
629
630 // for each blockwrite in the binary, find or create a GlobalOp with the data
631 // at the device level
632 std::vector<memref::GlobalOp> global_data;
633 DeviceOp device = blockwriteData.getDevice();
634 Location loc = device.getLoc();
635 {
636 OpBuilder::InsertionGuard guard(builder);
637 builder.setInsertionPointToStart(device.getBody());
638 for (auto &op : operations) {
639 if (op.cmd.Opcode != XAIE_IO_BLOCKWRITE) {
640 global_data.emplace_back(nullptr);
641 continue;
642 }
643 const uint32_t *d = reinterpret_cast<const uint32_t *>(op.cmd.DataPtr);
644 global_data.push_back(blockwriteData.getOrCreate(
645 builder, loc, ArrayRef<uint32_t>(d, op.cmd.Size / 4)));
646 }
647 }
648
649 // create the txn ops
651 if (failed(emitTransactionOps(builder, loc, operations, global_data)))
652 return failure();
653 } else if (outputType == AIE::AIEToConfigurationOutputType::ControlPacket) {
654 if (failed(emitControlPacketOps(builder, loc, operations, global_data,
655 initialRegisters)))
656 return failure();
657 } else {
658 llvm_unreachable("bad output type");
659 }
660
661 return success();
662}
663
664// Convert (disassemble) a transaction binary to MLIR. On success return a new
665// ModuleOp containing a DeviceOp containing a runtime sequence with the
666// transaction binary encoded as a sequence of npu.write32, npu.maskwrite32 and
667// npu.blockwrite operations. On failure return std::nullopt.
668std::optional<mlir::ModuleOp>
670 std::vector<uint8_t> &binary) {
671
672 // parse the binary
673 std::vector<TransactionBinaryOperation> operations;
674 auto c = parseTransactionBinary(binary, operations);
675 if (!c) {
676 llvm::errs() << "Failed to parse binary\n";
677 return std::nullopt;
678 }
679 int columns = *c;
680
681 // write32/maskwrite32/sync/address_patch now carry their integer fields as
682 // SSA operands materialized via arith.constant, so ensure the dialect is
683 // loaded before emitting them.
684 ctx->getOrLoadDialect<mlir::arith::ArithDialect>();
685
686 auto loc = mlir::UnknownLoc::get(ctx);
687
688 // create a new ModuleOp and set the insertion point
689 auto module = ModuleOp::create(loc);
690 OpBuilder builder(module.getBodyRegion());
691 builder.setInsertionPointToStart(module.getBody());
692
693 // create aie.device
694 std::vector<AIEDevice> devices{AIEDevice::npu1_1col, AIEDevice::npu1_2col,
695 AIEDevice::npu1_3col, AIEDevice::npu1};
696 auto device = DeviceOp::create(builder, loc, devices[columns - 1],
697 DeviceOp::getDefaultDeviceName(), nullptr);
698 device.getRegion().emplaceBlock();
699 DeviceOp::ensureTerminator(device.getBodyRegion(), builder, loc);
700 builder.setInsertionPointToStart(device.getBody());
701
702 // convert the parsed ops to MLIR
703 AIEX::BlockwriteData blockwriteData(device, "config_blockwrite_data_");
704 if (failed(convertTransactionOpsToMLIR(
706 blockwriteData)))
707 return std::nullopt;
708
709 return module;
710}
711
712// Insert the ops of the transaction `ctl` recorded, as `outputType`.
713static LogicalResult
714insertRecordedTransaction(AIERTControl &ctl, OpBuilder &builder,
716 AIEX::BlockwriteData &blockwriteData,
717 DeviceOp configured = {}) {
718 // Export the transactions to a binary buffer
719 std::vector<uint8_t> txn_data = ctl.exportSerializedTransaction();
720
721 // parse the binary data
722 std::vector<TransactionBinaryOperation> operations;
723 if (!parseTransactionBinary(txn_data, operations)) {
724 llvm::errs() << "Failed to parse binary\n";
725 return failure();
726 }
727
728 // Attach per-op source locations from AIERT's instruction-range bracketing.
729 // AIERTControl already projected those onto the serialized transaction's
730 // operations, so the indices line up with what the parser reproduced here.
731 // Ops with no bracketed location keep std::nullopt and inherit the device
732 // fallback location at emit time.
733 const std::vector<mlir::Location> &opLocs = ctl.getTxnOpLocs();
734 // Sharp edge: that projection models how aie-rt's serializer maps recorded
735 // commands to binary operations. A count disagreement means the model has
736 // drifted from the pinned aie-rt and every index past the divergence is
737 // mislabeled -- fail loudly instead of silently. An empty vector just means
738 // nothing was bracketed.
739 assert((opLocs.empty() || opLocs.size() == operations.size()) &&
740 "txn loc/op count mismatch: AIERTControl's command-to-operation "
741 "projection disagrees with the transaction parser; aie-rt's "
742 "serializer has drifted from projectCmdLocsOntoSerializedOps");
743 for (size_t i = 0, e = std::min(opLocs.size(), operations.size()); i < e; ++i)
744 if (!isa<UnknownLoc>(opLocs[i]))
745 operations[i].sourceLoc = opLocs[i];
746
747 llvm::DenseMap<uint32_t, uint32_t> initialRegisters;
748 if (outputType == AIEToConfigurationOutputType::ControlPacket && configured &&
749 configured.getOverlayAttr()) {
750 auto overlay = SymbolTable::lookupNearestSymbolFrom<DeviceOp>(
751 configured->getParentOp(), configured.getOverlayAttr());
752 if (!overlay)
753 return configured.emitOpError("references an unknown overlay device");
754 AIERTControl overlayCtl(overlay.getTargetModel());
755 if (failed(overlayCtl.setIOBackend(false, false)))
756 return failure();
757 overlayCtl.startTransaction();
758 if (failed(overlayCtl.configureSwitches(overlay)))
759 return failure();
760 auto overlayData = overlayCtl.exportSerializedTransaction();
761 std::vector<TransactionBinaryOperation> overlayOps;
762 if (!parseTransactionBinary(overlayData, overlayOps))
763 return failure();
764 // Masked writes must preserve fields installed by the overlay's PDI.
765 for (const auto &op : overlayOps) {
766 uint32_t &value = initialRegisters[op.cmd.RegOff];
767 if (op.cmd.Opcode == XAIE_IO_WRITE)
768 value = op.cmd.Value;
769 else if (op.cmd.Opcode == XAIE_IO_MASKWRITE)
770 value = (value & ~op.cmd.Mask) | (op.cmd.Value & op.cmd.Mask);
771 else
772 return overlay.emitOpError("has a non-register switch configuration");
773 }
774 }
775 return convertTransactionOpsToMLIR(builder, outputType, operations,
776 blockwriteData, initialRegisters);
777}
778
780 OpBuilder &builder, xilinx::AIE::DeviceOp device,
781 AIEX::BlockwriteData &blockwriteData, llvm::StringRef clElfDir,
782 AIE::AIEToConfigurationOutputType outputType, bool skipCtrlPktOverlay) {
783 const AIETargetModel &targetModel =
784 (const AIETargetModel &)device.getTargetModel();
785
786 if (!targetModel.hasProperty(AIETargetModel::IsNPU))
787 return failure();
788
789 bool aieSim = false;
790 bool xaieDebug = false;
791
792 AIERTControl ctl(targetModel);
793 if (failed(ctl.setIOBackend(aieSim, xaieDebug)))
794 return failure();
795
796 // start collecting transactions
797 ctl.startTransaction();
798
799 bool generateElfs = true;
800 if (failed(generateTransactions(ctl, clElfDir, device, aieSim, generateElfs,
801 true, true, skipCtrlPktOverlay)))
802 return failure();
803
804 return insertRecordedTransaction(ctl, builder, outputType, blockwriteData,
805 skipCtrlPktOverlay ? device : DeviceOp{});
806}
807
809 OpBuilder &builder, xilinx::AIE::DeviceOp device,
810 AIEX::BlockwriteData &blockwriteData,
811 AIE::AIEToConfigurationOutputType outputType, bool skipCtrlPktOverlay) {
812 const AIETargetModel &targetModel =
813 (const AIETargetModel &)device.getTargetModel();
814 if (!targetModel.hasProperty(AIETargetModel::IsNPU))
815 return failure();
816
817 AIERTControl ctl(targetModel);
818 if (failed(ctl.setIOBackend(/*aieSim=*/false, /*xaieDebug=*/false)))
819 return failure();
820 ctl.startTransaction();
821 if (failed(ctl.teardownSwitches(device, skipCtrlPktOverlay)) ||
822 failed(ctl.disableCores(device)) || failed(ctl.resetDmaChannels(device)))
823 return failure();
824 return insertRecordedTransaction(ctl, builder, outputType, blockwriteData,
825 skipCtrlPktOverlay ? device : DeviceOp{});
826}
827
828LogicalResult
829xilinx::AIE::insertConfigOps(ArrayRef<std::pair<Operation *, DeviceOp>> sites,
831 bool ctrlPkt = outputType == AIE::AIEToConfigurationOutputType::ControlPacket;
832 // Indexed by whether the site is a teardown.
833 std::array<llvm::DenseMap<DeviceOp, AIEX::BlockwriteData>, 2> blockwriteData;
834 std::array<llvm::DenseMap<std::pair<Operation *, Operation *>,
835 std::unique_ptr<Block>>,
836 2>
837 configs;
838 struct ConstantPool {
839 Operation *last = nullptr;
840 llvm::DenseMap<Attribute, Value> values;
841 };
842 llvm::DenseMap<Operation *, ConstantPool> pools;
843 for (auto [at, configured] : sites) {
844 auto device = at->getParentOfType<DeviceOp>();
845 if (!device)
846 return at->emitError("expected to be nested under an aie.device op");
847 bool teardown = isa<AIEX::NpuWriteTeardownOp>(at);
848 std::unique_ptr<Block> &config = configs[teardown][{configured, device}];
849 if (!config) {
850 AIEX::BlockwriteData &payloads =
851 blockwriteData[teardown]
852 .try_emplace(device, device, teardown ? "teardown_" : "loadpdi_")
853 .first->second;
854 auto generated = std::make_unique<Block>();
855 OpBuilder configBuilder(at->getContext());
856 configBuilder.setInsertionPointToEnd(generated.get());
857 if (teardown ? failed(generateAndInsertTeardownOps(
858 configBuilder, configured, payloads, outputType))
860 configBuilder, configured, payloads,
861 /*clElfDir=*/"", outputType,
862 /*skipCtrlPktOverlay=*/ctrlPkt)))
863 return at->emitError(teardown ? "Failed to generate teardown operations"
864 : "Failed to generate configuration "
865 "operations");
866 config = std::move(generated);
867 }
868 auto seq = at->getParentOfType<RuntimeSequenceOp>();
869 ConstantPool *pool = seq ? &pools[seq] : nullptr;
870 OpBuilder builder(at);
871 IRMapping mapping;
872 for (Operation &op : *config) {
873 Attribute key;
874 if (auto constant = dyn_cast<arith::ConstantOp>(op))
875 key = constant.getValue();
876 else if (auto global = dyn_cast<memref::GetGlobalOp>(op))
877 key = global.getNameAttr();
878 if (!pool || !key) {
879 builder.clone(op, mapping);
880 continue;
881 }
882 Value &pooled = pool->values[key];
883 if (!pooled) {
884 OpBuilder::InsertionGuard guard(builder);
885 if (pool->last)
886 builder.setInsertionPointAfter(pool->last);
887 else
888 builder.setInsertionPointToStart(&seq.getBody().front());
889 pool->last = builder.clone(op);
890 pooled = pool->last->getResult(0);
891 }
892 mapping.map(op.getResult(0), pooled);
893 }
894 }
895 return success();
896}
897
898LogicalResult xilinx::AIE::inlineWriteConfigs(ModuleOp module) {
899 SmallVector<std::pair<Operation *, DeviceOp>> sites;
900 WalkResult walk = module.walk([&](Operation *write) {
901 FlatSymbolRefAttr ref;
902 if (auto config = dyn_cast<AIEX::NpuWriteConfigOp>(write))
903 ref = config.getDeviceRefAttr();
904 else if (auto teardown = dyn_cast<AIEX::NpuWriteTeardownOp>(write))
905 ref = teardown.getDeviceRefAttr();
906 else
907 return WalkResult::advance();
908 auto configured = module.lookupSymbol<DeviceOp>(ref);
909 if (!configured) {
910 write->emitOpError() << ref << " is not a device";
911 return WalkResult::interrupt();
912 }
913 sites.emplace_back(write, configured);
914 return WalkResult::advance();
915 });
916 if (walk.wasInterrupted() ||
918 return failure();
919 for (auto [write, configured] : sites)
920 write->erase();
921 return success();
922}
923
924static LogicalResult
925convertAIEToConfiguration(AIE::DeviceOp device, StringRef clElfDir,
927
928 OpBuilder builder(device.getBodyRegion());
929 // search for aiex.configure ops in runtime sequences by walking the device
930 // and collect them in a vector. If there are none, create a new runtime
931 // sequence. Otherwise assume the insertion point is the first
932 // aiex.configure op.
933 auto loc = device.getLoc();
934 SmallVector<AIEX::ConfigureOp> configureOps;
935 device.walk([&](AIEX::ConfigureOp op) { configureOps.push_back(op); });
936
937 if (configureOps.empty()) {
938 // create aiex.runtime_sequence
939 int id = 0;
940 std::string seq_name = "configure";
941 while (device.lookupSymbol(seq_name))
942 seq_name = "configure" + std::to_string(id++);
943 StringAttr seq_sym_name = builder.getStringAttr(seq_name);
944 auto seq = AIE::RuntimeSequenceOp::create(
945 builder, loc, seq_sym_name, BoolAttr{}, AIE::TraceBufferAttr{},
946 ArrayAttr{}, ArrayAttr{});
947 seq.getBody().push_back(new Block);
948 builder.setInsertionPointToStart(&seq.getBody().front());
949 } else {
950 builder.setInsertionPoint(configureOps.front());
951 }
952
953 // convert the parsed ops to MLIR
954 AIEX::BlockwriteData blockwriteData(device, "config_blockwrite_data_");
955 if (failed(generateAndInsertConfigOps(builder, device, blockwriteData,
956 clElfDir, outputType)))
957 return failure();
958
959 // If we chose the first aiex.configure as insertion point, erase it
960 // and inline its child operations.
961 if (!configureOps.empty()) {
962 // splice the body into the current insertion point
963 builder.getBlock()->getOperations().splice(
964 builder.getInsertionPoint(),
965 configureOps.front().getBody().front().getOperations());
966 configureOps.front().erase();
967 }
968
969 return success();
970}
971
972namespace {
973
974template <typename BaseClass, AIE::AIEToConfigurationOutputType MyOutputType>
975struct ConvertAIEToConfigurationPass : BaseClass {
976 std::string &ref_clElfDir;
977 std::string &ref_clDeviceName;
978 ConvertAIEToConfigurationPass(std::string &clElfDir,
979 std::string &clDeviceName)
980 : ref_clElfDir(clElfDir), ref_clDeviceName(clDeviceName) {}
981
982 void getDependentDialects(DialectRegistry &registry) const override {
983 registry.insert<memref::MemRefDialect, AIEX::AIEXDialect,
984 arith::ArithDialect>();
985 }
986
987 void runOnOperation() override {
988 AIE::DeviceOp deviceOp = BaseClass::getOperation();
989 if (!ref_clDeviceName.empty() &&
990 deviceOp.getSymName() != ref_clDeviceName) {
991 return;
992 }
993 if (failed(
994 convertAIEToConfiguration(deviceOp, ref_clElfDir, MyOutputType))) {
995 return BaseClass::signalPassFailure();
996 }
997 }
998};
999
1000struct ConvertAIEToTransactionPass
1001 : ConvertAIEToConfigurationPass<
1002 xilinx::impl::ConvertAIEToTransactionBase<
1003 ConvertAIEToTransactionPass>,
1004 AIE::AIEToConfigurationOutputType::Transaction> {
1005 ConvertAIEToTransactionPass()
1006 : ConvertAIEToConfigurationPass<
1007 xilinx::impl::ConvertAIEToTransactionBase<
1008 ConvertAIEToTransactionPass>,
1010 clDeviceName) {}
1011};
1012
1013struct ConvertAIEToControlPacketsPass
1014 : ConvertAIEToConfigurationPass<
1015 xilinx::impl::ConvertAIEToControlPacketsBase<
1016 ConvertAIEToControlPacketsPass>,
1017 AIE::AIEToConfigurationOutputType::ControlPacket> {
1018 ConvertAIEToControlPacketsPass()
1019 : ConvertAIEToConfigurationPass<
1020 xilinx::impl::ConvertAIEToControlPacketsBase<
1021 ConvertAIEToControlPacketsPass>,
1023 clDeviceName) {}
1024};
1025
1026} // end anonymous namespace
1027
1028std::unique_ptr<mlir::OperationPass<xilinx::AIE::DeviceOp>>
1030 return std::make_unique<ConvertAIEToTransactionPass>();
1031}
1032
1033std::unique_ptr<mlir::OperationPass<xilinx::AIE::DeviceOp>>
1035 return std::make_unique<ConvertAIEToControlPacketsPass>();
1036}
memref::GlobalOp getOrCreate(OpBuilder &builder, mlir::Location loc, ArrayRef< uint32_t > words)
Definition AIEUtils.cpp:180
AIE::DeviceOp getDevice() const
Definition AIEUtils.h:38
bool hasProperty(ModelProperty Prop) const
mlir::Value createConstantArgPlus(mlir::OpBuilder &builder, mlir::Location loc, uint64_t value)
mlir::Value createConstantI32(mlir::OpBuilder &builder, mlir::Location loc, uint32_t value)
mlir::LogicalResult insertConfigOps(llvm::ArrayRef< std::pair< mlir::Operation *, xilinx::AIE::DeviceOp > > sites, AIEToConfigurationOutputType outputType)
mlir::LogicalResult inlineWriteConfigs(mlir::ModuleOp module)
std::optional< int > parseTransactionBinary(const std::vector< uint8_t > &data, std::vector< TransactionBinaryOperation > &ops)
Parse an exported transaction.
std::unique_ptr< mlir::OperationPass< xilinx::AIE::DeviceOp > > createConvertAIEToTransactionPass()
mlir::LogicalResult generateAndInsertConfigOps(mlir::OpBuilder &builder, xilinx::AIE::DeviceOp device, xilinx::AIEX::BlockwriteData &blockwriteData, llvm::StringRef clElfDir="", AIEToConfigurationOutputType outputType=AIEToConfigurationOutputType::Transaction, bool skipCtrlPktOverlay=false)
mlir::LogicalResult generateAndInsertTeardownOps(mlir::OpBuilder &builder, xilinx::AIE::DeviceOp device, xilinx::AIEX::BlockwriteData &blockwriteData, AIEToConfigurationOutputType outputType=AIEToConfigurationOutputType::Transaction, bool skipCtrlPktOverlay=false)
std::optional< mlir::ModuleOp > convertTransactionBinaryToMLIR(mlir::MLIRContext *ctx, std::vector< uint8_t > &binary)
std::unique_ptr< mlir::OperationPass< xilinx::AIE::DeviceOp > > createConvertAIEToControlPacketsPass()
mlir::LogicalResult setIOBackend(bool aieSim, bool xaieDebug)
Definition AIERT.cpp:307
std::vector< uint8_t > exportSerializedTransaction()
Definition AIERT.cpp:1336
mlir::LogicalResult disableCores(DeviceOp &targetOp)
Definition AIERT.cpp:920
mlir::LogicalResult addCoreEnable(DeviceOp &targetOp)
Definition AIERT.cpp:1062
const std::vector< mlir::Location > & getTxnOpLocs() const
Definition AIERT.cpp:1355
mlir::LogicalResult addInitConfig(DeviceOp &targetOp, bool skipCtrlPktOverlay=false)
Definition AIERT.cpp:971
mlir::LogicalResult resetDmaChannels(DeviceOp &targetOp)
Definition AIERT.cpp:930
mlir::LogicalResult addAieElfs(DeviceOp &targetOp, const mlir::StringRef elfPath, bool aieSim)
Definition AIERT.cpp:1235
mlir::LogicalResult teardownSwitches(DeviceOp &targetOp, bool skipCtrlPktOverlay=false)
Definition AIERT.cpp:868
std::optional< LoadPdiPayload > loadPdi
std::optional< AddressPatchPayload > addressPatch