MLIR-AIE 1.4.5.dev
TxnEncoding.h
Go to the documentation of this file.
1//===- TxnEncoding.h - Standalone TXN instruction encoding ------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Header-only library for encoding AI Engine TXN (transaction) instructions.
9// This has ZERO dependencies on MLIR or LLVM and can be used standalone in
10// host applications to generate TXN binaries at runtime. It is the single
11// in-tree source of truth for the instruction format, used by both the
12// compiler (AIETargetNPU.cpp) and generated host code.
13//
14//===----------------------------------------------------------------------===//
15
16#ifndef AIE_RUNTIME_TXNENCODING_H
17#define AIE_RUNTIME_TXNENCODING_H
18
19#include <cstddef>
20#include <cstdint>
21#include <optional>
22#include <vector>
23
24// Applied to the emitted entry points; see emitDispatchShimFuncs.
25#ifdef _WIN32
26#define AIE_DISPATCH_EXPORT __declspec(dllexport)
27#else
28#define AIE_DISPATCH_EXPORT
29#endif
30
31namespace aie_runtime {
32
33// Transaction opcodes for the firmware TXN format the compiler currently
34// targets.
35//
36// These DO NOT match the third_party/aie-rt xaie_txn.h enum, which is an older
37// layout (CONFIG_SHIMDMA_BD=5, no NOOP/PREEMPT/LOADPDI block). They match the
38// newer firmware opcodes the compiler emits (e.g. CREATE_SCRATCHPAD=0x0A,
39// UPDATE_REG=0x0C).
40//
41// DRIFT WARNING: when the firmware TXN format changes, update these values
42// here. Do not "correct" them against the submodule's xaie_txn.h — it tracks a
43// different (older) numbering and reconciling to it would break the emitted
44// transactions.
72
73// Upper bound on a tile's buffer-descriptor count across all tile types, sizing
74// the pool's fixed storage. This is a standalone header (no MLIR / target model
75// here), so the ACTUAL per-tile count is supplied at runtime by the compiler
76// via bd_pool_init_range -- this constant only bounds the array.
77// AIE2 memtiles have the most BDs (48); shim/core have 16. Keep this >= the
78// largest getNumBDs any target model returns.
79constexpr uint32_t kMaxBDsPerTile = 48;
80
81// Runtime buffer-descriptor free-list pool, for dynamic runtime sequences whose
82// BD IDs are chosen at runtime (a rolled scf.for the compiler cannot unroll).
83// The static allocator assigns BD IDs at compile time; this is its runtime
84// counterpart. `head` is the number of currently-free IDs at the front of
85// `free_ids`; pop takes from the top, push returns to it.
86struct BdPool {
88 int head;
89};
90
91// Initialize a pool over the half-open id range [lo, hi), the ids one BD
92// partition of a tile can submit (the compiler reads it from the target model).
93// A mem tile splits its BD table by channel parity, so each half gets its own
94// pool; one flat list would hand a channel ids it cannot submit. IDs are
95// stacked so that pop() hands out the LOWEST free id first, matching the static
96// allocator's lowest-free-first order -- so a first pop from a fresh pool
97// equals a pinned bd_id = lo.
98inline BdPool bd_pool_init_range(uint32_t lo, uint32_t hi) {
99 BdPool p;
100 p.head = 0;
101 if (hi > kMaxBDsPerTile)
102 hi = kMaxBDsPerTile;
103 for (uint32_t i = hi; i > lo; --i)
104 p.free_ids[p.head++] = i - 1; // top of stack is `lo`
105 return p;
106}
107
108// Initialize a pool with the `n` free IDs [0, n).
109inline BdPool bd_pool_init(uint32_t n) { return bd_pool_init_range(0, n); }
110
111// Withhold `id` from the pool -- a static BD already owns that slot in the
112// tile's shared BD table, and popping it here would silently overwrite it.
113// `free_ids[0..head)` is kept sorted highest-to-lowest (see bd_pool_init_range)
114// so pop keeps handing out the lowest remaining id first; shift the tail down
115// instead of swapping in the top, which would scramble that order.
116inline void bd_pool_reserve(BdPool &p, uint32_t id) {
117 for (int i = 0; i < p.head; ++i)
118 if (p.free_ids[i] == id) {
119 for (int j = i; j < p.head - 1; ++j)
120 p.free_ids[j] = p.free_ids[j + 1];
121 --p.head;
122 return;
123 }
124}
125
126// Pop a free BD ID into `out`. Returns false if the pool is empty -- the
127// generated builder turns that into a `return std::nullopt`, so a runtime
128// working set that exceeds the tile's BD count yields no stream rather than a
129// silently-corrupt one.
130inline bool bd_pool_pop(BdPool &p, uint32_t &out) {
131 if (p.head == 0)
132 return false;
133 out = p.free_ids[--p.head];
134 return true;
135}
136
137// Return a BD ID to the pool for reuse.
138inline void bd_pool_push(BdPool &p, uint32_t id) {
139 if (p.head < static_cast<int>(kMaxBDsPerTile))
140 p.free_ids[p.head++] = id;
141}
142
143// Device information for the TXN header.
145 uint8_t major = 0;
146 uint8_t minor = 1;
147 uint8_t devGen = 3; // 3 = NPU (PHX/HWK), 4 = NPU2 (STX/KRK)
148 uint8_t numRows = 6;
149 uint8_t numCols = 5;
150 uint8_t numMemTileRows = 1;
151};
152
153// Build a TxnDeviceInfo from target-model values. Assigns by field name, so a
154// caller that can only emit an expression (the generated C++ builder) states
155// the field order in exactly one place: here.
156inline TxnDeviceInfo txn_device_info(uint8_t dev_gen, uint8_t num_rows,
157 uint8_t num_cols,
158 uint8_t num_memtile_rows) {
159 TxnDeviceInfo info;
160 info.devGen = dev_gen;
161 info.numRows = num_rows;
162 info.numCols = num_cols;
163 info.numMemTileRows = num_memtile_rows;
164 return info;
165}
166
167// Append a 6-word write32 instruction.
168inline void txn_append_write32(std::vector<uint32_t> &txn, uint32_t addr,
169 uint32_t val) {
170 size_t pos = txn.size();
171 txn.resize(pos + 6, 0);
172 txn[pos + 0] = TXN_OPC_WRITE;
173 // txn[pos + 1] is reserved (0)
174 txn[pos + 2] = addr;
175 txn[pos + 3] = 0; // extra bits for reg offset
176 txn[pos + 4] = val;
177 txn[pos + 5] = 6 * sizeof(uint32_t); // operation size
178}
179
180// Append a 7-word maskwrite32 instruction.
181inline void txn_append_maskwrite32(std::vector<uint32_t> &txn, uint32_t addr,
182 uint32_t val, uint32_t mask) {
183 size_t pos = txn.size();
184 txn.resize(pos + 7, 0);
185 txn[pos + 0] = TXN_OPC_MASKWRITE;
186 // txn[pos + 1] is reserved (0)
187 txn[pos + 2] = addr;
188 txn[pos + 3] = 0;
189 txn[pos + 4] = val;
190 txn[pos + 5] = mask;
191 txn[pos + 6] = 7 * sizeof(uint32_t); // operation size
192}
193
194// Append a 7-word maskpoll instruction: block until (reg & mask) == val.
195// Same word layout as maskwrite32 -- aie-rt's XAie_MaskPoll32Hdr and
196// XAie_MaskWrite32Hdr agree field for field, and the poll timeout is not
197// serialized (the firmware forces its own default; see xaie_txn.c).
198inline void txn_append_maskpoll32(std::vector<uint32_t> &txn, uint32_t addr,
199 uint32_t val, uint32_t mask) {
200 size_t pos = txn.size();
201 txn.resize(pos + 7, 0);
202 txn[pos + 0] = TXN_OPC_MASKPOLL;
203 // txn[pos + 1] is reserved (0)
204 txn[pos + 2] = addr;
205 txn[pos + 3] = 0;
206 txn[pos + 4] = val;
207 txn[pos + 5] = mask;
208 txn[pos + 6] = 7 * sizeof(uint32_t); // operation size
209}
210
211// Append a 4-word sync (TCT) instruction.
212inline void txn_append_sync(std::vector<uint32_t> &txn, uint32_t col,
213 uint32_t row, uint32_t dir, uint32_t chan,
214 uint32_t ncol, uint32_t nrow) {
215 size_t pos = txn.size();
216 txn.resize(pos + 4, 0);
217 txn[pos + 0] = TXN_OPC_TCT;
218 txn[pos + 1] = 4 * sizeof(uint32_t); // operation size
219 txn[pos + 2] = (dir & 0xff) | ((row & 0xff) << 8) | ((col & 0xff) << 16);
220 txn[pos + 3] =
221 ((nrow & 0xff) << 8) | ((ncol & 0xff) << 16) | ((chan & 0xff) << 24);
222}
223
224// Append a variable-length blockwrite instruction.
225// `data` points to `count` uint32_t words of payload.
226//
227// The destination tile is encoded twice in the firmware blockwrite format: once
228// in the upper bits of the absolute `addr` (column/row folded in by the
229// compiler before this point) and once in the dedicated col/row field at
230// word[1]. Callers that build an absolute address from a tile pass that same
231// (col, row) here so both representations agree; callers with a purely flat
232// address pass 0/0.
233inline void txn_append_blockwrite(std::vector<uint32_t> &txn, uint32_t addr,
234 const uint32_t *data, size_t count,
235 uint32_t col = 0, uint32_t row = 0) {
236 const unsigned headerSize = 4;
237 size_t pos = txn.size();
238 txn.resize(pos + headerSize + count, 0);
239 txn[pos + 0] = TXN_OPC_BLOCKWRITE;
240 txn[pos + 1] = (col & 0xff) | ((row & 0xff) << 8);
241 txn[pos + 2] = addr;
242 txn[pos + 3] = static_cast<uint32_t>((headerSize + count) * sizeof(uint32_t));
243 for (size_t i = 0; i < count; ++i)
244 txn[pos + headerSize + i] = data[i];
245}
246
247// Under the xclbin + insts.bin runtime, NPU firmware adds kDDRAIEAddrOffset to
248// host buffer addresses for only the first kNumFirmwareTranslatedArgs args; a
249// DDR patch for a later arg must fold the offset into arg_plus itself. Full-ELF
250// and HRX translate every arg, so folding there would double-translate the 6th+
251// buffer: those pass fold = false.
252constexpr uint32_t kDDRAIEAddrOffset = 0x80000000;
253constexpr uint32_t kNumFirmwareTranslatedArgs = 5;
254
255// Append a 12-word address_patch (DDR_PATCH) instruction.
256//
257// The consumer casts this byte range to aie-rt's `XAie_CustomOpHdr` followed by
258// `patch_op_t` (xaie_txn.h), so the layout is that struct pair's under natural
259// alignment: an 8-byte header at words 0-1, op_base and action at 2-5, then
260// regaddr, argidx and argplus as u64 word pairs at 6-7, 8-9 and 10-11, low half
261// first.
262inline void txn_append_address_patch(std::vector<uint32_t> &txn, uint32_t addr,
263 int32_t arg_idx, uint64_t arg_plus) {
264 size_t pos = txn.size();
265 txn.resize(pos + 12, 0);
266 txn[pos + 0] = TXN_OPC_DDR_PATCH; // opcode
267 txn[pos + 1] = 12 * sizeof(uint32_t); // operation size
268 // pos+2..4 are reserved (zero)
269 txn[pos + 5] = 0; // action; the struct puts it at word 4, inert while zero
270 txn[pos + 6] = addr; // register address to patch
271 // pos+7 is reserved (zero)
272 txn[pos + 8] = static_cast<uint32_t>(arg_idx); // buffer argument index
273 // pos+9 is reserved (zero)
274 txn[pos + 10] = static_cast<uint32_t>(arg_plus & 0xFFFFFFFFull);
275 txn[pos + 11] = static_cast<uint32_t>(arg_plus >> 32);
276}
277
278// Append a DDR_PATCH for a host buffer argument, folding the aperture offset
279// when the target runtime needs it. Both the static binary emitter and the
280// generated C++ builder route through here, so the fold rule has one body.
281inline void txn_append_arg_patch(std::vector<uint32_t> &txn, uint32_t addr,
282 int32_t arg_idx, uint64_t arg_plus,
283 bool fold_ddr_addr_offset) {
284 if (fold_ddr_addr_offset &&
285 static_cast<uint32_t>(arg_idx) >= kNumFirmwareTranslatedArgs)
286 arg_plus += kDDRAIEAddrOffset;
287 txn_append_address_patch(txn, addr, arg_idx, arg_plus);
288}
289
290// Append a 4-word loadpdi instruction.
291inline void txn_append_loadpdi(std::vector<uint32_t> &txn, uint32_t id,
292 uint32_t size, uint64_t addr) {
293 size_t pos = txn.size();
294 txn.resize(pos + 4, 0);
295 txn[pos + 0] = TXN_OPC_LOADPDI | (id << 16);
296 txn[pos + 1] = size;
297 txn[pos + 2] = static_cast<uint32_t>(addr);
298 txn[pos + 3] = static_cast<uint32_t>(addr >> 32);
299}
300
301// Append a 1-word preempt instruction.
302inline void txn_append_preempt(std::vector<uint32_t> &txn, uint32_t level) {
303 txn.push_back(TXN_OPC_PREEMPT | (level << 8));
304}
305
306// Why the last generated builder on this thread declined (returned
307// std::nullopt): a shape guard, a buffer-descriptor field overflow or an
308// empty BD pool records a message here before returning, and the dispatch
309// shim reports it through dispatch_last_refusal(). Null after a successful
310// build. The pointer is to a string literal in the generated code.
311inline thread_local const char *txn_refusal = nullptr;
312// `return aie_runtime::txn_refused("why");` declines a build with a reason.
313inline std::nullopt_t txn_refused(const char *why) {
314 txn_refusal = why;
315 return std::nullopt;
316}
317
318// Reserve 4 placeholder words for the TXN header. Call this BEFORE appending
319// any instructions, so that the header space is already allocated.
320inline void txn_init(std::vector<uint32_t> &txn) { txn.resize(4, 0); }
321
322// Finalize the 4-word TXN header in-place. Call this AFTER all instructions
323// are appended. The first 4 words must have been reserved by txn_init().
324// `op_count` is the number of operations appended.
325inline void txn_prepend_header(std::vector<uint32_t> &txn, uint32_t op_count,
326 TxnDeviceInfo info = {}) {
327 if (txn.size() < 4)
328 txn.resize(4, 0);
329 txn[0] = (static_cast<uint32_t>(info.numRows) << 24) |
330 (static_cast<uint32_t>(info.devGen) << 16) |
331 (static_cast<uint32_t>(info.minor) << 8) |
332 static_cast<uint32_t>(info.major);
333 txn[1] = (static_cast<uint32_t>(info.numMemTileRows) << 8) |
334 static_cast<uint32_t>(info.numCols);
335 txn[2] = op_count;
336 txn[3] = static_cast<uint32_t>(txn.size() * sizeof(uint32_t));
337}
338
339} // namespace aie_runtime
340
341#endif // AIE_RUNTIME_TXNENCODING_H
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)
std::nullopt_t txn_refused(const char *why)
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 bd_pool_push(BdPool &p, uint32_t id)
void txn_append_address_patch(std::vector< uint32_t > &txn, uint32_t addr, int32_t arg_idx, uint64_t arg_plus)
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)
constexpr uint32_t kNumFirmwareTranslatedArgs
@ TXN_OPC_RECORD_TIMER
Definition TxnEncoding.h:66
@ TXN_OPC_UPDATE_SCRATCH
Definition TxnEncoding.h:59
@ TXN_OPC_CREATE_SCRATCHPAD
Definition TxnEncoding.h:56
@ TXN_OPC_MASKPOLL_BUSY
Definition TxnEncoding.h:53
@ TXN_OPC_UPDATE_STATE_TABLE
Definition TxnEncoding.h:57
@ TXN_OPC_CONFIG_SHIMDMA_DMABUF_BD
Definition TxnEncoding.h:61
@ TXN_OPC_LOAD_PM_END_INTERNAL
Definition TxnEncoding.h:69
@ TXN_OPC_CUSTOM_OP_NEXT
Definition TxnEncoding.h:68
@ TXN_OPC_CUSTOM_OP_MAX
Definition TxnEncoding.h:70
@ TXN_OPC_CONFIG_SHIMDMA_BD
Definition TxnEncoding.h:60
@ TXN_OPC_LOAD_PM_START
Definition TxnEncoding.h:55
@ TXN_OPC_CUSTOM_OP_BEGIN
Definition TxnEncoding.h:62
constexpr uint32_t kDDRAIEAddrOffset
void bd_pool_reserve(BdPool &p, uint32_t id)
void txn_append_maskwrite32(std::vector< uint32_t > &txn, uint32_t addr, uint32_t val, uint32_t mask)
bool bd_pool_pop(BdPool &p, uint32_t &out)
const char * txn_refusal
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={})
constexpr uint32_t kMaxBDsPerTile
Definition TxnEncoding.h:79
BdPool bd_pool_init(uint32_t n)
BdPool bd_pool_init_range(uint32_t lo, uint32_t hi)
Definition TxnEncoding.h:98
uint32_t free_ids[kMaxBDsPerTile]
Definition TxnEncoding.h:87