MLIR-AIE 1.4.5.dev
AIEXToEmitC.cpp
Go to the documentation of this file.
1//===- AIEXToEmitC.cpp - AIEX runtime sequence to EmitC --------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Converts npu transaction ops in an aie.runtime_sequence into EmitC calls to
9// aie/Runtime/TxnEncoding.h; translateToCpp() then yields a standalone C++
10// function assembling the same TXN words as the binary emitter
11// (AIETargetNPU.cpp), but runtime-parameterizable. A runtime-bound scf.for
12// (dynamic BD pool path) is preserved and lowered to emitc.for, keeping the
13// loop rolled at runtime.
14//
15//===----------------------------------------------------------------------===//
16
19
24
25#include "mlir/Conversion/ArithToEmitC/ArithToEmitC.h"
26#include "mlir/Conversion/SCFToEmitC/SCFToEmitC.h"
27#include "mlir/Dialect/Arith/IR/Arith.h"
28#include "mlir/Dialect/Arith/Transforms/Passes.h"
29#include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
30#include "mlir/Dialect/EmitC/IR/EmitC.h"
31#include "mlir/Dialect/EmitC/Transforms/TypeConversions.h"
32#include "mlir/Dialect/MemRef/IR/MemRef.h"
33#include "mlir/Dialect/SCF/IR/SCF.h"
34#include "mlir/IR/Builders.h"
35#include "mlir/IR/BuiltinOps.h"
36#include "mlir/Interfaces/SideEffectInterfaces.h"
37#include "mlir/Pass/Pass.h"
38#include "mlir/Transforms/DialectConversion.h"
39#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
40#include "mlir/Transforms/RegionUtils.h"
41
42#include "llvm/ADT/StringSet.h"
43#include "llvm/ADT/TypeSwitch.h"
44#include "llvm/Support/Format.h"
45
46namespace xilinx {
47// DECL as well as DEF: the ConvertAIEXToEmitCPass(Options) constructor below
48// needs ConvertAIEXToEmitCOptions declared in this translation unit.
49#define GEN_PASS_DECL_CONVERTAIEXTOEMITC
50#define GEN_PASS_DEF_CONVERTAIEXTOEMITC
51#include "aie/Conversion/Passes.h.inc"
52} // namespace xilinx
53
54using namespace mlir;
55
56// The C++ that declines a build with a reason. `why` becomes a string
57// literal (quotes and backslashes escaped, line breaks flattened). When the
58// snippet goes through an emitc.verbatim with operands, `{}` is a
59// placeholder and `{{` its escape, so a `{` in the message is doubled; a
60// verbatim without operands is emitted literally.
61static std::string refuse(StringRef why, bool inFormat) {
62 std::string lit;
63 for (char ch : why) {
64 if (ch == '\n' || ch == '\r')
65 lit += ' ';
66 else if (ch == '"' || ch == '\\')
67 (lit += '\\') += ch;
68 else if (ch == '{' && inFormat)
69 lit += "{{";
70 else
71 lit += ch;
72 }
73 return "return aie_runtime::txn_refused(\"" + lit + "\");";
74}
75
76using namespace xilinx;
77
78namespace {
79
80// The C++ type of the TXN accumulator the generated function builds.
81constexpr llvm::StringLiteral kTxnVecType = "std::vector<uint32_t>";
82
83emitc::OpaqueType getU32Type(MLIRContext *ctx) {
84 return emitc::OpaqueType::get(ctx, "uint32_t");
85}
86
87// The C++ variable name of a runtime BD pool, declared in the generated
88// function's prologue. One pool per tile and BD partition: channels sharing a
89// partition must share one pool, or they would hand out the same id twice.
90template <typename PoolOp>
91std::string bdPoolName(PoolOp op) {
92 return "bd_pool_" + std::to_string(op.getColumn()) + "_" +
93 std::to_string(op.getRow()) + "_" + std::to_string(op.getBdBegin()) +
94 "_" + std::to_string(op.getBdEnd());
95}
96
97// BD ids the static allocator has already placed on (col, row), read off the
98// placed aie.mem / aie.memtile_dma / aie.shim_dma chains. Sorted so the emitted
99// stream is deterministic.
100llvm::SmallVector<uint32_t, 8> staticBdIdsOnTile(AIE::DeviceOp device,
101 uint32_t col, uint32_t row) {
102 llvm::SmallVector<uint32_t, 8> ids;
103 for (AIE::DmaBody program : device.getOps<AIE::DmaBody>()) {
104 auto id = program.getTileID();
105 if (id.col != static_cast<int>(col) || id.row != static_cast<int>(row))
106 continue;
107 llvm::append_range(ids, AIE::getAssignedBdIds(program));
108 }
109 llvm::sort(ids);
110 ids.erase(llvm::unique(ids), ids.end());
111 return ids;
112}
113
114// A uint32_t literal operand (e.g. `42u`) for a txn_append_* argument. Used for
115// the compile-time-resolved address fields (buffer/col/row are folded in).
116Value u32Literal(OpBuilder &b, Location loc, uint32_t val) {
117 return emitc::ConstantOp::create(
118 b, loc, getU32Type(b.getContext()),
119 emitc::OpaqueAttr::get(b.getContext(), std::to_string(val) + "u"));
120}
121
122// Emit a call to a TxnEncoding.h free function on the txn vector. Scalar
123// operands pass through as-is; convert-arith-to-emitc later lowers them to C++
124// literals or function parameters, byte-identical to the static path.
125void emitTxnCall(OpBuilder &b, Location loc, StringRef fn, Value txnVec,
126 ValueRange args) {
127 SmallVector<Value> callArgs;
128 callArgs.push_back(txnVec);
129 callArgs.append(args.begin(), args.end());
130 emitc::CallOpaqueOp::create(b, loc, TypeRange{}, ("aie_runtime::" + fn).str(),
131 callArgs);
132}
133
134// Device-relative values (absolute addr, blockwrite data) resolved before the
135// ops are cloned out of the aie.device, keyed by their clone in the emitc.func.
136struct DeviceResolved {
137 llvm::DenseMap<Operation *, uint32_t> absoluteAddr;
138 llvm::DenseMap<Operation *, DenseIntElementsAttr> blockWriteData;
139};
140
141class AIEXToEmitCConverter {
142public:
143 AIEXToEmitCConverter(emitc::FuncOp funcOp, Value txnVec,
144 const DeviceResolved &resolved, Value opCountVar,
145 bool foldDDRAddrOffset)
146 : funcOp(funcOp), txnVec(txnVec), resolved(resolved),
147 opCountVar(opCountVar), foldDDRAddrOffset(foldDDRAddrOffset) {}
148
149 // Convert every npu/pool op in the body, recursing into scf regions. Returns
150 // the compile-time op count for a straight-line body (the literal op_count),
151 // or nullopt on error. With a runtime op-count var (a loop is present) each
152 // op increments it instead and the returned count is unused.
153 std::optional<uint32_t> run() {
154 // DMA lowering consumes buffer views but can leave dead subview/cast
155 // chains behind (notably after configure/run materialization). They are
156 // not transaction operations and must not reach the C++ converter.
157 bool erased;
158 do {
159 erased = false;
160 funcOp.walk([&](Operation *op) {
161 if (isa<memref::MemRefDialect>(op->getDialect()) &&
162 isOpTriviallyDead(op)) {
163 op->erase();
164 erased = true;
165 }
166 });
167 } while (erased);
168 uint32_t count = 0;
169 SmallVector<Operation *> consumed;
170 convertBlockRecursive(funcOp.getBlocks().front(), count, consumed);
171 if (!ok)
172 return std::nullopt;
173
174 // Erase the converted npu ops, then DCE their now-dead operand clones (a
175 // get_global feeding a blockwrite, a folded-away address constant). Region-
176 // scoped runRegionDCE, not a module canonicalizer: the latter would also
177 // drop the shared memref.global the blockwrite data was inlined from, which
178 // other sequences in the module still reference.
179 for (Operation *op : llvm::reverse(consumed))
180 op->erase();
181 IRRewriter rewriter(funcOp.getContext());
182 (void)runRegionDCE(rewriter, funcOp.getFunctionBody());
183 return count;
184 }
185
186private:
187 // Convert the npu/pool ops in `block`, recursing through scf op regions so a
188 // rolled loop's body is lowered in place (the scf.for itself is left for the
189 // later convert-scf-to-emitc step). Collects converted ops into `consumed`.
190 void convertBlockRecursive(Block &block, uint32_t &count,
191 SmallVector<Operation *> &consumed) {
192 SmallVector<Operation *> work;
193 for (Operation &op : block)
194 work.push_back(&op);
195 for (Operation *op : work) {
196 if (!ok)
197 return;
198 // Structural emitc ops and arith (constant / runtime-value math) are left
199 // for the later arith-to-emitc step. Recurse into scf ops (and any other
200 // region-carrying control flow) to convert their bodies; the control-flow
201 // op itself is handled by convert-scf-to-emitc afterwards.
202 if (isa<emitc::EmitCDialect, arith::ArithDialect>(op->getDialect()))
203 continue;
204 if (isa<scf::SCFDialect>(op->getDialect())) {
205 for (Region &r : op->getRegions())
206 for (Block &b : r)
207 convertBlockRecursive(b, count, consumed);
208 continue;
209 }
210 convertOne(op, count);
211 if (!isa<memref::GetGlobalOp>(op))
212 consumed.push_back(op);
213 }
214 }
215
216private:
217 // Record one emitted txn op: bump the compile-time count and, when a runtime
218 // op-count variable exists (the sequence has a loop, so the count is not
219 // known at compile time), emit a `++__opcount;` so the header gets the true
220 // runtime total.
221 void countOp(OpBuilder &b, Location loc, uint32_t &count) {
222 ++count;
223 if (opCountVar)
224 emitc::VerbatimOp::create(b, loc, "++{};", ValueRange{opCountVar});
225 }
226
227 // Convert one op, appending to the txn vector and incrementing `count`.
228 // Scalar operands flow through as SSA (lowered later by
229 // convert-arith-to-emitc); only the address field is replaced by its
230 // device-resolved literal. Unsupported ops set `ok` false.
231 void convertOne(Operation *op, uint32_t &count) {
232 OpBuilder b(op);
233 Location loc = op->getLoc();
234
235 llvm::TypeSwitch<Operation *, void>(op)
236 .Case<AIEX::NpuWrite32Op>([&](auto w) {
237 // A compile-time address folds to a literal; a runtime address (the
238 // dynamic BD pool's bdBase + wordIdx, arith over a popped id) is
239 // passed through as SSA and lowered by convert-arith-to-emitc.
240 Value addrV = runtimeOrResolvedAddr(b, loc, w, w.getAddress());
241 if (!addrV)
242 return fail(w, "cannot convert a symbolic/unresolved write32 "
243 "address to the C++ TXN target");
244 emitTxnCall(b, loc, "txn_append_write32", txnVec,
245 {addrV, w.getValue()});
246 countOp(b, loc, count);
247 })
248 .Case<AIEX::NpuMaskWrite32Op>([&](auto mw) {
249 Value addrV = runtimeOrResolvedAddr(b, loc, mw, mw.getAddress());
250 if (!addrV)
251 return fail(mw, "cannot convert a symbolic/unresolved maskwrite32 "
252 "address to the C++ TXN target");
253 emitTxnCall(b, loc, "txn_append_maskwrite32", txnVec,
254 {addrV, mw.getValue(), mw.getMask()});
255 countOp(b, loc, count);
256 })
257 .Case<AIEX::NpuMaskPollOp>([&](auto mp) {
258 Value addrV = runtimeOrResolvedAddr(b, loc, mp, mp.getAddress());
259 if (!addrV)
260 return fail(mp, "cannot convert a symbolic/unresolved maskpoll "
261 "address to the C++ TXN target");
262 emitTxnCall(b, loc, "txn_append_maskpoll32", txnVec,
263 {addrV, mp.getValue(), mp.getMask()});
264 countOp(b, loc, count);
265 })
266 .Case<AIEX::NpuSyncOp>([&](auto s) {
267 emitTxnCall(b, loc, "txn_append_sync", txnVec,
268 {s.getColumn(), s.getRow(), s.getDirection(),
269 s.getChannel(), s.getColumnNum(), s.getRowNum()});
270 countOp(b, loc, count);
271 })
272 .Case<AIEX::NpuLoadPdiOp>([&](auto pdi) {
273 Value address = emitc::ConstantOp::create(
274 b, loc, emitc::OpaqueType::get(b.getContext(), "uint64_t"),
275 emitc::OpaqueAttr::get(b.getContext(),
276 std::to_string(pdi.getAddress()) + "ULL"));
277 emitTxnCall(b, loc, "txn_append_loadpdi", txnVec,
278 {u32Literal(b, loc, pdi.getId()),
279 u32Literal(b, loc, pdi.getSize()), address});
280 countOp(b, loc, count);
281 })
282 .Case<AIEX::NpuPreemptOp>([&](auto preempt) {
283 emitTxnCall(b, loc, "txn_append_preempt", txnVec,
284 {u32Literal(b, loc, preempt.getLevel())});
285 countOp(b, loc, count);
286 })
287 .Case<AIEX::NpuAddressPatchOp>([&](auto ap) {
288 // A runtime bd_id makes the patched register address runtime: prefer
289 // the SSA addr_val (flows through arith-to-emitc) over the constant.
290 Value addrV = ap.getAddrVal() ? ap.getAddrVal()
291 : u32Literal(b, loc, ap.getAddr());
292 std::optional<uint32_t> argIdx = ap.getArgIdx();
293 if (!argIdx)
294 return fail(ap, "address_patch still names its host buffer by SSA "
295 "value; run -aie-resolve-address-patch-buffers");
296 Value idxV = emitc::ConstantOp::create(
297 b, loc, emitc::OpaqueType::get(b.getContext(), "int32_t"),
298 emitc::OpaqueAttr::get(b.getContext(), std::to_string(*argIdx)));
299 // txn_append_arg_patch applies the DDR-aperture fold itself, so the
300 // rule lives only in TxnEncoding.h. Both operands are literals here,
301 // so -O2 folds the branch away.
302 Value foldV = emitc::ConstantOp::create(
303 b, loc, emitc::OpaqueType::get(b.getContext(), "bool"),
304 emitc::OpaqueAttr::get(b.getContext(),
305 foldDDRAddrOffset ? "true" : "false"));
306 Value argPlus = ap.getArgPlus();
307 // i32 offsets are unsigned bit patterns in the static emitter.
308 // Cast before widening so bit 31 is not sign-extended to 64 bits.
309 if (argPlus.getType().isInteger(32))
310 argPlus = emitc::CastOp::create(b, loc, getU32Type(b.getContext()),
311 argPlus);
312 emitTxnCall(b, loc, "txn_append_arg_patch", txnVec,
313 {addrV, idxV, argPlus, foldV});
314 countOp(b, loc, count);
315 })
316 .Case<AIEX::NpuBlockWriteOp>([&](auto bw) {
317 convertBlockWrite(b, loc, bw);
318 countOp(b, loc, count);
319 })
320 .Case<AIEX::NpuBlockWriteValuesOp>([&](auto bw) {
321 convertBlockWriteValues(b, loc, bw);
322 countOp(b, loc, count);
323 })
324 .Case<cf::AssertOp>([&](auto a) {
325 // Host-side guard: a violated condition yields no stream
326 // (std::nullopt), and its message is what the host reports. Appends
327 // nothing, so not counted.
328 emitc::VerbatimOp::create(
329 b, loc, "if (!({})) " + refuse(a.getMsg(), /*inFormat=*/true),
330 ValueRange{a.getArg()});
331 })
332 .Case<AIEX::DMABdPoolPopOp>([&](AIEX::DMABdPoolPopOp pop) {
333 // Draw a BD id from the tile's runtime pool (declared in the
334 // prologue) into a fresh C++ variable; nullopt if the pool is empty.
335 // Downstream ops consume the id, so replace the SSA result with a
336 // literal naming the variable.
337 std::string var = "bd_" + std::to_string(nextPoolVar++);
338 emitc::VerbatimOp::create(
339 b, loc,
340 "uint32_t " + var + "; if (!aie_runtime::bd_pool_pop(" +
341 bdPoolName(pop) + ", " + var + ")) " +
342 refuse("no free buffer descriptor on tile (" +
343 std::to_string(pop.getColumn()) + ", " +
344 std::to_string(pop.getRow()) +
345 "): the sequence keeps more transfers in flight "
346 "than the tile has descriptors",
347 /*inFormat=*/false));
348 Value ref =
349 emitc::LiteralOp::create(b, loc, pop.getBdId().getType(), var);
350 pop.getBdId().replaceAllUsesWith(ref);
351 })
352 .Case<AIEX::DMABdPoolPushOp>([&](AIEX::DMABdPoolPushOp push) {
353 // Return a BD id to the tile's runtime pool.
354 emitc::CallOpaqueOp::create(
355 b, loc, TypeRange{}, "aie_runtime::bd_pool_push",
356 ValueRange{poolRef(b, loc, bdPoolName(push)), push.getBdId()});
357 })
358 // XRT allocates the scratchpad only for a full-ELF context, which has
359 // no per-call instruction buffer to carry a generated stream.
360 .Case<AIEX::NpuCreateScratchpadOp, AIEX::NpuUpdateFromScratchpadOp>(
361 [&](Operation *sp) {
362 fail(sp, "needs the scratchpad of a full-ELF context, which the "
363 "C++ TXN target's per-call streams run without; use a "
364 "runtime sequence argument instead of a scratchpad "
365 "parameter");
366 })
367 // memref.get_global feeding a blockwrite is consumed by
368 // convertBlockWrite (data inlined); the now-dead op is erased later.
369 .Case<memref::GetGlobalOp>([&](auto) {})
370 .Default([&](Operation *unsupported) {
371 if (isa<scf::SCFDialect>(unsupported->getDialect()))
372 fail(unsupported,
373 "control flow in runtime sequences is not yet supported by "
374 "the C++ TXN target");
375 else
376 fail(unsupported, "is not supported by the C++ TXN target");
377 });
378 }
379
380 // The compile-time-resolved absolute address (folding buffer/col/row) as a
381 // uint32_t literal, or null if it could not be resolved statically. Address
382 // resolution depends on the parent device, so it is precomputed before the op
383 // is cloned out (see recordDeviceResolved).
384 Value resolvedAddr(OpBuilder &b, Location loc, Operation *clone) {
385 auto it = resolved.absoluteAddr.find(clone);
386 if (it == resolved.absoluteAddr.end())
387 return {};
388 return u32Literal(b, loc, it->second);
389 }
390
391 // The address for a write32/maskwrite32: the device-resolved literal, else a
392 // genuine runtime address (arith-derived, e.g. the pool's bdBase + wordIdx).
393 // A bare block argument selects no register and is rejected; null for a
394 // symbolic/unresolved address.
395 Value runtimeOrResolvedAddr(OpBuilder &b, Location loc, Operation *clone,
396 Value addrOperand) {
397 if (Value lit = resolvedAddr(b, loc, clone))
398 return lit;
399 Operation *def = addrOperand ? addrOperand.getDefiningOp() : nullptr;
400 if (def && !isa<arith::ConstantOp>(def))
401 return addrOperand;
402 return {};
403 }
404
405 // Emit a diagnostic on `op` and mark the conversion failed.
406 void fail(Operation *op, const llvm::Twine &msg) {
407 op->emitOpError(msg);
408 ok = false;
409 }
410
411 // An emitc.literal referencing a pool variable (for pass-by-reference into
412 // bd_pool_push).
413 Value poolRef(OpBuilder &b, Location loc, StringRef name) {
414 return emitc::LiteralOp::create(
415 b, loc, emitc::OpaqueType::get(b.getContext(), "aie_runtime::BdPool"),
416 name);
417 }
418
419 void convertBlockWrite(OpBuilder &b, Location loc, AIEX::NpuBlockWriteOp bw) {
420 auto addrIt = resolved.absoluteAddr.find(bw);
421 auto dataIt = resolved.blockWriteData.find(bw);
422 if (addrIt == resolved.absoluteAddr.end() ||
423 dataIt == resolved.blockWriteData.end() || !dataIt->second) {
424 bw.emitOpError("cannot convert blockwrite without a constant address and "
425 "data to the C++ TXN target");
426 ok = false;
427 return;
428 }
429 uint32_t addr = addrIt->second;
430 DenseIntElementsAttr data = dataIt->second;
431
432 // Emit the payload as a typed EmitC array variable rather than a verbatim
433 // C++ string, so the generated code carries real types.
434 MLIRContext *ctx = b.getContext();
435 int64_t n = data.size();
436 auto arrTy =
437 emitc::ArrayType::get(ctx, SmallVector<int64_t>{n}, getU32Type(ctx));
438 std::string init = "{";
439 llvm::raw_string_ostream ss(init);
440 bool first = true;
441 for (APInt word : data.getValues<APInt>()) {
442 if (!first)
443 ss << ", ";
444 ss << llvm::format("0x%08xu", static_cast<unsigned>(word.getZExtValue()));
445 first = false;
446 }
447 ss << "}";
448 auto arrVar = emitc::VariableOp::create(b, loc, arrTy,
449 emitc::OpaqueAttr::get(ctx, init));
450
451 uint32_t colVal = 0, rowVal = 0;
452 if (auto col = bw.getColumn(), row = bw.getRow(); col && row) {
453 colVal = *col;
454 rowVal = *row;
455 }
456 emitTxnCall(b, loc, "txn_append_blockwrite", txnVec,
457 {u32Literal(b, loc, addr), arrVar.getResult(),
458 u32Literal(b, loc, static_cast<uint32_t>(n)),
459 u32Literal(b, loc, colVal), u32Literal(b, loc, rowVal)});
460 }
461
462 // A blockwrite whose payload is computed at TXN-build time:
463 // stage the words into a local C++ array, then hand that array
464 // to one txn_append_blockwrite. Unlike convertBlockWrite there is no constant
465 // memref to inline, so the slots are filled by assignment from the operand
466 // values, which convert-arith-to-emitc lowers in place afterwards.
467 void convertBlockWriteValues(OpBuilder &b, Location loc,
468 AIEX::NpuBlockWriteValuesOp bw) {
469 MLIRContext *ctx = b.getContext();
470 Value addrV = runtimeOrResolvedAddr(b, loc, bw, bw.getAddress());
471 if (!addrV) {
472 fail(bw, "cannot convert a symbolic/unresolved blockwrite_values address "
473 "to the C++ TXN target");
474 return;
475 }
476 ValueRange words = bw.getValues();
477 int64_t n = static_cast<int64_t>(words.size());
478
479 // Declare the staging array (zero-initialized; every slot is assigned
480 // below). Typed emitc.variable, matching convertBlockWrite's array.
481 auto arrTy =
482 emitc::ArrayType::get(ctx, SmallVector<int64_t>{n}, getU32Type(ctx));
483 auto arrVar = emitc::VariableOp::create(b, loc, arrTy,
484 emitc::OpaqueAttr::get(ctx, "{}"));
485
486 // Fill the slots. Emitted as verbatim assignments (the same mechanism the
487 // runtime guards above use to reference a not-yet-lowered SSA value): the
488 // i32 words convert to the array's uint32_t elementwise, which is the
489 // intended two's-complement reinterpretation of the packed BD fields.
490 for (int64_t i = 0; i < n; i++)
491 emitc::VerbatimOp::create(b, loc, "{}[" + std::to_string(i) + "] = {};",
492 ValueRange{arrVar.getResult(), words[i]});
493
494 // col/row are 0: the address is already flat (as for npu.blockwrite).
495 emitTxnCall(b, loc, "txn_append_blockwrite", txnVec,
496 {addrV, arrVar.getResult(),
497 u32Literal(b, loc, static_cast<uint32_t>(n)),
498 u32Literal(b, loc, 0), u32Literal(b, loc, 0)});
499 }
500
501 emitc::FuncOp funcOp;
502 Value txnVec;
503 const DeviceResolved &resolved;
504 // Runtime op-count variable (the C++ `__opcount`), or null when the sequence
505 // is straight-line and the count is a compile-time literal.
506 Value opCountVar;
507 bool ok = true;
508 // Counter for unique popped-BD-id C++ variable names.
509 unsigned nextPoolVar = 0;
510 bool foldDDRAddrOffset;
511};
512
513struct ConvertAIEXToEmitCPass
514 : xilinx::impl::ConvertAIEXToEmitCBase<ConvertAIEXToEmitCPass> {
515 ConvertAIEXToEmitCPass() = default;
516 // foldDDRAddrOffset is `protected` on the generated base, settable only via
517 // a -pass-pipeline string; this constructor is the programmatic equivalent.
518 explicit ConvertAIEXToEmitCPass(const ConvertAIEXToEmitCOptions &options)
519 : ConvertAIEXToEmitCBase(options) {}
520
521 void runOnOperation() override {
522 ModuleOp moduleOp = getOperation();
523 MLIRContext *ctx = &getContext();
524 if (failed(AIE::inlineWriteConfigs(moduleOp)))
525 return signalPassFailure();
526
527 struct SeqInfo {
528 AIE::RuntimeSequenceOp seq;
529 AIE::DeviceOp device;
530 };
531 SmallVector<SeqInfo> sequences;
532 moduleOp.walk([&](AIE::RuntimeSequenceOp seq) {
533 sequences.push_back({seq, seq->getParentOfType<AIE::DeviceOp>()});
534 });
535 if (emitDispatchShim && sequences.size() != 1) {
536 moduleOp.emitOpError()
537 << "emit-dispatch-shim needs exactly one aie.runtime_sequence to "
538 "wrap, found "
539 << sequences.size();
540 return signalPassFailure();
541 }
542 if (sequences.empty())
543 return;
544
545 OpBuilder builder(ctx);
546 for (auto &[seqOp, deviceOp] : sequences) {
547 if (!deviceOp) {
548 seqOp.emitOpError("must be nested inside an aie.device");
549 return signalPassFailure();
550 }
551 std::optional<emitc::FuncOp> gen =
552 emitFunction(builder, moduleOp, seqOp, deviceOp);
553 if (!gen)
554 return signalPassFailure();
555 if (emitDispatchShim) {
556 emitc::FuncOp generated = *gen;
557 if (failed(emitDispatchShimFuncs(builder, moduleOp, generated)))
558 return signalPassFailure();
559 }
560 }
561
562 // Replace the module body with just the generated emitc funcs + includes.
563 SmallVector<Operation *> toErase;
564 for (Operation &op : *moduleOp.getBody())
565 if (!isa<emitc::FuncOp, emitc::IncludeOp>(op))
566 toErase.push_back(&op);
567 for (Operation *op : llvm::reverse(toErase))
568 op->erase();
569
570 // Lower the arith ops (constant scalar fields, runtime-value arithmetic
571 // feeding npu ops) and any scf control flow (a rolled dynamic loop) left in
572 // the function bodies to emitc.
573 if (failed(lowerArithAndScfToEmitC(moduleOp)))
574 return signalPassFailure();
575
576 builder.setInsertionPointToStart(moduleOp.getBody());
577 emitc::IncludeOp::create(builder, moduleOp.getLoc(),
578 "aie/Runtime/TxnEncoding.h",
579 /*is_standard_include=*/false);
580 emitc::IncludeOp::create(builder, moduleOp.getLoc(), "cstdint",
581 /*is_standard_include=*/true);
582 emitc::IncludeOp::create(builder, moduleOp.getLoc(), "vector",
583 /*is_standard_include=*/true);
584 // The builder returns std::optional: a runtime scalar that would overflow a
585 // narrow BD field yields std::nullopt instead of a truncated stream.
586 emitc::IncludeOp::create(builder, moduleOp.getLoc(), "optional",
587 /*is_standard_include=*/true);
588 }
589
590 // Run the upstream arith-to-emitc and scf-to-emitc conversions over the
591 // generated functions in one pass: a rolled dynamic loop is scf.for, and its
592 // bounds / iter_args are arith, so both must lower together.
593 LogicalResult lowerArithAndScfToEmitC(ModuleOp moduleOp) {
594 // ArithToEmitC has no patterns for the integer ops the Python bindings
595 // emit for `//`, ceildiv, min and max on staged scalars. Expand them to
596 // the divsi/cmpi/select forms it does lower before converting.
597 {
598 RewritePatternSet expand(moduleOp.getContext());
599 arith::populateCeilFloorDivExpandOpsPatterns(expand);
600 arith::populateExpandMinMaxIPatterns(expand);
601 // Rewrite only: folding or CSE here would reshape the sequence the
602 // per-op conversion below expects (and the C++ its tests check).
603 GreedyRewriteConfig config;
604 config.enableFolding(false).enableConstantCSE(false);
605 if (failed(applyPatternsGreedily(moduleOp, std::move(expand), config)))
606 return failure();
607 }
608
609 TypeConverter typeConverter;
610 typeConverter.addConversion([](Type t) { return t; });
611 populateEmitCSizeTTypeConversions(typeConverter);
612
613 RewritePatternSet patterns(moduleOp.getContext());
614 populateArithToEmitCPatterns(typeConverter, patterns);
615 populateSCFToEmitCConversionPatterns(patterns, typeConverter);
616
617 ConversionTarget target(*moduleOp.getContext());
618 target.addLegalDialect<emitc::EmitCDialect>();
619 target.addIllegalDialect<arith::ArithDialect>();
620 target.addIllegalDialect<scf::SCFDialect>();
621 return applyPartialConversion(moduleOp, target, std::move(patterns));
622 }
623
624private:
625 // The C spelling emitc's translator emits for a parameter type, for the ABI
626 // string only. Must mirror CppEmitter::emitType down to its signedness rule,
627 // or the reported ABI disagrees with the emitted signature.
628 static std::optional<std::string> cTypeName(Type t) {
629 if (isa<emitc::SizeTType>(t))
630 return std::string("size_t");
631 auto intTy = dyn_cast<IntegerType>(t);
632 if (!intTy || !emitc::isSupportedIntegerType(intTy))
633 return std::nullopt;
634 if (intTy.getWidth() == 1)
635 return std::string("bool");
636 return (intTy.isUnsigned() ? "uint" : "int") +
637 std::to_string(intTy.getWidth()) + "_t";
638 }
639
640 // Emit the ctypes-callable entry points: dispatch_abi() reporting the
641 // parameter types, and dispatch_generate() handing back a pointer + word
642 // count into thread-local storage, or -2 when the builder declined.
643 LogicalResult emitDispatchShimFuncs(OpBuilder &builder, ModuleOp moduleOp,
644 emitc::FuncOp gen) {
645 MLIRContext *ctx = moduleOp.getContext();
646 Location loc = gen.getLoc();
647 TypeRange genParams = gen.getFunctionType().getInputs();
648
649 SmallVector<std::string> abiTypes;
650 for (Type t : genParams) {
651 std::optional<std::string> name = cTypeName(t);
652 if (!name)
653 return gen.emitOpError()
654 << "runtime-sequence parameter type " << t
655 << " has no dispatch ABI spelling; it cannot be passed from the "
656 "host at dispatch time";
657 abiTypes.push_back(*name);
658 }
659
660 builder.setInsertionPointToEnd(moduleOp.getBody());
661 // Python resolves these two by name out of the built shared library. ELF
662 // exports every extern symbol unless told otherwise, but MSVC-target
663 // linking exports nothing without an explicit marker, so on Windows the
664 // DLL would load and then fail to resolve dispatch_abi. The macro comes
665 // from TxnEncoding.h, which the emitted file already includes, and expands
666 // to nothing off Windows.
667 auto externC =
668 builder.getStrArrayAttr({"extern \"C\"", "AIE_DISPATCH_EXPORT"});
669
670 // Pointers must be emitc.ptr: an opaque type may not have a pointer as its
671 // outer type.
672 auto charPtrTy =
673 emitc::PointerType::get(emitc::OpaqueType::get(ctx, "const char"));
674 auto abiFn = emitc::FuncOp::create(builder, loc, "dispatch_abi",
675 FunctionType::get(ctx, {}, {charPtrTy}));
676 abiFn.setSpecifiersAttr(externC);
677 OpBuilder ab = OpBuilder::atBlockBegin(abiFn.addEntryBlock());
678 Value abiStr = emitc::LiteralOp::create(
679 ab, loc, charPtrTy, "\"" + llvm::join(abiTypes, ",") + "\"");
680 emitc::ReturnOp::create(ab, loc, abiStr);
681
682 SmallVector<Type> shimParams(genParams);
683 shimParams.push_back(
684 emitc::PointerType::get(emitc::PointerType::get(getU32Type(ctx))));
685 auto i64Ty = emitc::OpaqueType::get(ctx, "int64_t");
686 auto shimFn =
687 emitc::FuncOp::create(builder, loc, "dispatch_generate",
688 FunctionType::get(ctx, shimParams, {i64Ty}));
689 shimFn.setSpecifiersAttr(externC);
690 Block *shimBlock = shimFn.addEntryBlock();
691 OpBuilder sb = OpBuilder::atBlockBegin(shimBlock);
692
693 SmallVector<Value> genArgs(shimBlock->getArguments().drop_back());
694 SmallVector<StringRef> holes(genArgs.size(), "{}");
695 std::string call = "auto __result = " + gen.getSymName().str() + "(" +
696 llvm::join(holes, ", ") + ");";
697 emitc::VerbatimOp::create(
698 sb, loc, "static thread_local " + kTxnVecType.str() + " __txn;");
699 emitc::VerbatimOp::create(sb, loc, "aie_runtime::txn_refusal = nullptr;");
700 emitc::VerbatimOp::create(sb, loc, call, genArgs);
701 emitc::VerbatimOp::create(sb, loc, "if (!__result) return -2;");
702 emitc::VerbatimOp::create(sb, loc, "__txn = std::move(*__result);");
703 emitc::VerbatimOp::create(sb, loc, "*{} = __txn.data();",
704 ValueRange{shimBlock->getArguments().back()});
705 emitc::ReturnOp::create(
706 sb, loc,
707 emitc::LiteralOp::create(sb, loc, i64Ty,
708 "static_cast<int64_t>(__txn.size())"));
709
710 // dispatch_last_refusal(): why the last dispatch_generate() on this
711 // thread returned -2 (a string literal in this library), or null.
712 auto whyFn = emitc::FuncOp::create(builder, loc, "dispatch_last_refusal",
713 FunctionType::get(ctx, {}, {charPtrTy}));
714 whyFn.setSpecifiersAttr(externC);
715 OpBuilder wb = OpBuilder::atBlockBegin(whyFn.addEntryBlock());
716 emitc::ReturnOp::create(
717 wb, loc,
718 emitc::LiteralOp::create(wb, loc, charPtrTy,
719 "aie_runtime::txn_refusal"));
720 return success();
721 }
722
723 // Resolve `orig`'s device-dependent values (still under the device) keyed by
724 // its detached `clone`.
725 static void recordDeviceResolved(Operation *orig, Operation *clone,
726 DeviceResolved &resolved) {
727 if (auto w = dyn_cast<AIEX::NpuWrite32Op>(orig)) {
728 if (auto a = w.getAbsoluteAddress())
729 resolved.absoluteAddr[clone] = *a;
730 } else if (auto mw = dyn_cast<AIEX::NpuMaskWrite32Op>(orig)) {
731 if (auto a = mw.getAbsoluteAddress())
732 resolved.absoluteAddr[clone] = *a;
733 } else if (auto mp = dyn_cast<AIEX::NpuMaskPollOp>(orig)) {
734 if (auto a = mp.getAbsoluteAddress())
735 resolved.absoluteAddr[clone] = *a;
736 } else if (auto bw = dyn_cast<AIEX::NpuBlockWriteOp>(orig)) {
737 if (auto a = bw.getAbsoluteAddress())
738 resolved.absoluteAddr[clone] = *a;
739 if (auto d = bw.getDataWords())
740 resolved.blockWriteData[clone] = d;
741 } else if (auto bwv = dyn_cast<AIEX::NpuBlockWriteValuesOp>(orig)) {
742 // Already absolute: no buffer/col/row to fold in.
743 if (auto a = AIEX::getConstantIntOperand(bwv.getAddress()))
744 resolved.absoluteAddr[clone] = *a;
745 }
746 }
747
748 FailureOr<emitc::FuncOp> emitFunction(OpBuilder &builder, ModuleOp moduleOp,
749 AIE::RuntimeSequenceOp seqOp,
750 AIE::DeviceOp deviceOp) {
751 Location loc = seqOp.getLoc();
752 Block &entry = seqOp.getBody().front();
753
754 // The generated function takes the sequence's non-memref args (the runtime
755 // scalars) and returns the assembled txn vector. Memref args address host
756 // buffers resolved by address_patch; they are not function parameters here.
757 SmallVector<Type> paramTypes;
758 auto txnVecType =
759 emitc::OpaqueType::get(moduleOp.getContext(), kTxnVecType);
760 // The builder can fail at TXN-build time (a runtime scalar overflowing a
761 // narrow BD field), so it returns std::optional<std::vector<uint32_t>>:
762 // std::nullopt on a failed guard, the assembled vector otherwise.
763 auto txnRetType = emitc::OpaqueType::get(
764 moduleOp.getContext(),
765 (llvm::Twine("std::optional<") + kTxnVecType + ">").str());
766 // A runtime scalar of type `index` (e.g. a loop trip count) has no C++
767 // spelling on its own; emit it as emitc.size_t so it flows straight into a
768 // converted scf.for bound with no leftover cast. Other scalar types pass
769 // through unchanged.
770 auto sizeT = emitc::SizeTType::get(moduleOp.getContext());
771 auto paramTypeFor = [&](Type t) -> Type {
772 return isa<IndexType>(t) ? Type(sizeT) : t;
773 };
774 for (BlockArgument arg : entry.getArguments())
775 if (!isa<BaseMemRefType>(arg.getType()))
776 paramTypes.push_back(paramTypeFor(arg.getType()));
777
778 builder.setInsertionPointToEnd(moduleOp.getBody());
779 std::string funcName = "generate_txn_" + deviceOp.getSymName().str() + "_" +
780 seqOp.getSymName().str();
781 auto funcOp = emitc::FuncOp::create(
782 builder, loc, funcName,
783 FunctionType::get(moduleOp.getContext(), paramTypes,
784 TypeRange{txnRetType}));
785 funcOp.setSpecifiersAttr(builder.getStrArrayAttr({"inline"}));
786 Block *funcBlock = funcOp.addEntryBlock();
787 OpBuilder fb = OpBuilder::atBlockBegin(funcBlock);
788
789 // EmitC has no native std::vector construction, so declare the accumulator
790 // with a verbatim statement and reference it by name via an emitc.literal
791 // (which translates inline, no extra copy). All txn_append_* calls take
792 // this value, so the generated C++ operates on the single `txn` vector.
793 emitc::VerbatimOp::create(fb, loc, "std::vector<uint32_t> txn;");
794 Value txnVec = emitc::LiteralOp::create(fb, loc, txnVecType, "txn");
795 emitTxnCall(fb, loc, "txn_init", txnVec, {});
796
797 // Declare each runtime BD free-list pool the sequence draws ids from,
798 // with the bounds its ops name.
799 llvm::StringSet<> declaredPools;
800 auto declarePool = [&](auto poolOp) {
801 std::string name = bdPoolName(poolOp);
802 if (!declaredPools.insert(name).second)
803 return;
804 uint32_t lo = poolOp.getBdBegin(), hi = poolOp.getBdEnd();
805 emitc::VerbatimOp::create(fb, loc,
806 "aie_runtime::BdPool " + name +
807 " = aie_runtime::bd_pool_init_range(" +
808 std::to_string(lo) + ", " +
809 std::to_string(hi) + ");");
810 // Hand back the ids the static allocator already placed on this tile.
811 for (uint32_t id :
812 staticBdIdsOnTile(deviceOp, poolOp.getColumn(), poolOp.getRow()))
813 if (id >= lo && id < hi)
814 emitc::VerbatimOp::create(fb, loc,
815 "aie_runtime::bd_pool_reserve(" + name +
816 ", " + std::to_string(id) + ");");
817 };
818 seqOp.walk([&](Operation *op) {
819 if (auto pop = dyn_cast<AIEX::DMABdPoolPopOp>(op))
820 declarePool(pop);
821 else if (auto push = dyn_cast<AIEX::DMABdPoolPushOp>(op))
822 declarePool(push);
823 });
824
825 // A rolled loop makes the op count a runtime quantity: declare a __opcount
826 // accumulator the converter bumps per op. A straight-line sequence keeps
827 // the compile-time literal count (byte-identical golden).
828 bool hasControlFlow = false;
829 seqOp.walk([&](Operation *op) {
830 if (isa<scf::SCFDialect>(op->getDialect()))
831 hasControlFlow = true;
832 });
833 Value opCountVar;
834 if (hasControlFlow) {
835 emitc::VerbatimOp::create(fb, loc, "uint32_t __opcount = 0;");
836 opCountVar = emitc::LiteralOp::create(
837 fb, loc, getU32Type(moduleOp.getContext()), "__opcount");
838 }
839
840 // Clone the sequence body (whole regions, so a rolled scf.for comes along),
841 // mapping non-memref block args to function params. Resolve device-relative
842 // values (address, blockwrite data) while the originals are still under the
843 // device, keyed by clone, since the accessors don't work once detached.
844 IRMapping mapping;
845 unsigned p = 0;
846 for (BlockArgument arg : entry.getArguments())
847 if (!isa<BaseMemRefType>(arg.getType()))
848 mapping.map(arg, funcBlock->getArgument(p++));
849 DeviceResolved resolved;
850 for (Operation &op : entry.without_terminator()) {
851 fb.clone(op, mapping);
852 // Record device-resolved values for the clone and any nested op (a BD
853 // inside a rolled loop resolves the same way as a top-level one).
854 op.walk([&](Operation *orig) {
855 Operation *c = mapping.lookupOrNull(orig);
856 if (c)
857 recordDeviceResolved(orig, c, resolved);
858 });
859 }
860
861 AIEXToEmitCConverter conv(funcOp, txnVec, resolved, opCountVar,
862 foldDDRAddrOffset);
863 std::optional<uint32_t> count = conv.run();
864 if (!count)
865 return failure();
866
867 // Finalize the header with the op count + device info. The count is the
868 // runtime `__opcount` when the sequence has a loop, else the compile-time
869 // literal.
870 const AIE::AIETargetModel &tm = deviceOp.getTargetModel();
871 OpBuilder eb(funcBlock, funcBlock->end());
872 std::string countStr =
873 hasControlFlow ? "__opcount" : (std::to_string(*count) + "u");
874 // txn_device_info assigns TxnDeviceInfo by field name, so a field added or
875 // reordered there cannot silently mis-encode this generated header.
876 std::string header = "aie_runtime::txn_prepend_header(txn, " + countStr +
877 ", aie_runtime::txn_device_info(" +
878 std::to_string(AIE::txnDeviceGen(tm)) + ", " +
879 std::to_string(tm.rows()) + ", " +
880 std::to_string(tm.columns()) + ", " +
881 std::to_string(tm.getNumMemTileRows()) + "));";
882 emitc::VerbatimOp::create(eb, loc, header);
883 // Return the vector as the optional result. The literal text differs from
884 // the "txn" accumulator literal (so the two are not deduplicated) and is
885 // typed as the optional return so emitc's return-type check passes; it
886 // relies on the implicit std::vector -> std::optional conversion in C++.
887 Value ret = emitc::LiteralOp::create(eb, loc, txnRetType, "std::move(txn)");
888 emitc::ReturnOp::create(eb, loc, ret);
889 return funcOp;
890 }
891};
892
893} // namespace
894
895std::unique_ptr<OperationPass<ModuleOp>>
897 return std::make_unique<ConvertAIEXToEmitCPass>();
898}
899
900std::unique_ptr<OperationPass<ModuleOp>>
902 bool emitDispatchShim) {
903 ConvertAIEXToEmitCOptions options;
904 options.foldDDRAddrOffset = foldDDRAddrOffset;
905 options.emitDispatchShim = emitDispatchShim;
906 return std::make_unique<ConvertAIEXToEmitCPass>(options);
907}
virtual int rows() const =0
Return the number of rows in the device.
virtual int columns() const =0
Return the number of columns in the device.
virtual uint32_t getNumMemTileRows() const =0
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
uint8_t txnDeviceGen(const AIETargetModel &tm)
Device generation ID written into the TXN header (aie_runtime::TxnDeviceInfo::devGen).
mlir::LogicalResult inlineWriteConfigs(mlir::ModuleOp module)
llvm::SmallVector< uint32_t > getAssignedBdIds(DmaBody program)
std::unique_ptr< mlir::OperationPass< mlir::ModuleOp > > createConvertAIEXToEmitCPass()