MLIR-AIE 1.4.6.dev
AIELowerCoreIssuedConfiguration.cpp
Go to the documentation of this file.
1//===- AIELowerCoreIssuedConfiguration.cpp ----------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// Lower the configuration a core makes onto the accesses it makes itself.
9//
10// The writes that push a task are not spelled out here. aie-rt is asked to
11// push inside a transaction, and the transaction it records is read back and
12// replayed as stores the core makes over its processor bus. A restatement of
13// the register map would be one more copy to keep in step with aie-rt.
14
15#include "TransactionBinary.h"
16
19#include "aie/Targets/AIERT.h"
20
21#include "mlir/Dialect/Arith/IR/Arith.h"
22#include "mlir/Dialect/Func/IR/FuncOps.h"
23#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
24#include "mlir/Dialect/SCF/IR/SCF.h"
25#include "mlir/IR/Builders.h"
26#include "mlir/Pass/Pass.h"
27
28namespace xilinx::AIE {
29#define GEN_PASS_DEF_AIELOWERCOREISSUEDCONFIGURATION
30#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
31} // namespace xilinx::AIE
32
33using namespace mlir;
34using namespace xilinx;
35using namespace xilinx::AIE;
36
37namespace {
38
39/// The core reaches its own tile's registers through a window at this offset;
40/// see the write_tm intrinsic, whose own default says the same.
41constexpr uint64_t kTileMemoryMappedBase = 0x80000;
42
43/// The names of the store and load intrinsics for this architecture.
44struct TileAccessIntrinsics {
45 std::string write, read;
46};
47
48static std::optional<TileAccessIntrinsics> tileAccessIntrinsics(AIEArch arch) {
49 switch (arch) {
50 case AIEArch::AIE2:
51 return TileAccessIntrinsics{"llvm.aie2.write.tm", "llvm.aie2.read.tm"};
52 case AIEArch::AIE2p:
53 return TileAccessIntrinsics{"llvm.aie2p.write.tm", "llvm.aie2p.read.tm"};
54 default:
55 return std::nullopt;
56 }
57}
58
59/// The writes aie-rt makes to push `op`'s task, as (tile offset, value).
60static LogicalResult
61pushWrites(DmaPushQueueOp op, int col, int row,
62 const AIETargetModel &targetModel,
63 SmallVectorImpl<std::pair<uint64_t, uint32_t>> &writes) {
64 if (!op.getBdId())
65 return op.emitOpError("names its descriptor by tag; run "
66 "--aie-assign-bd-ids first");
67 AIERTControl ctl(targetModel);
68 if (failed(ctl.setIOBackend(/*aieSim=*/false, /*xaieDebug=*/false)))
69 return op.emitOpError("could not start aie-rt to collect its writes");
70 ctl.startTransaction();
71 // Only the task queue. Rewriting the channel's control register while the
72 // channel runs loses the state it keeps between packets, and the channel
73 // this pushes onto is already configured and running.
74 if (failed(ctl.pushToBdQueueAndEnable(
75 *op.getOperation(), col, row, op.getChannel(), op.getDirection(),
76 op.getBdId().value_or(0), op.getRepeatCount(), /*padValue=*/0,
77 op.getOutOfOrder(), /*configureChannel=*/false)))
78 return failure();
79
80 std::vector<uint8_t> txn = ctl.exportSerializedTransaction();
81 std::vector<TransactionBinaryOperation> ops;
82 if (!parseTransactionBinary(txn, ops))
83 return op.emitOpError("could not read back what aie-rt recorded");
84
85 // Only this tile's registers are the core's to write, and only a plain
86 // store is. Anything else means aie-rt pushes a task in a way this lowering
87 // does not carry, and silently dropping it would leave the channel without
88 // the task.
89 uint64_t tileBase =
90 (static_cast<uint64_t>(col) << targetModel.getColumnShift()) |
91 (static_cast<uint64_t>(row) << targetModel.getRowShift());
92 uint64_t tileSpan = 1ull << targetModel.getRowShift();
93 for (const TransactionBinaryOperation &txnOp : ops) {
94 if (txnOp.cmd.Opcode != XAie_TxnOpcode::XAIE_IO_WRITE)
95 return op.emitOpError("pushing this task takes a ")
96 << static_cast<int>(txnOp.cmd.Opcode)
97 << " transaction command, which a core cannot make; only plain "
98 "register writes lower here";
99 uint64_t addr = txnOp.cmd.RegOff;
100 if (addr < tileBase || addr >= tileBase + tileSpan)
101 return op.emitOpError("pushing this task writes ")
102 << addr << ", outside tile (" << col << ", " << row
103 << "); a core reaches only its own tile's registers";
104 writes.emplace_back(addr - tileBase, txnOp.cmd.Value);
105 }
106 if (writes.empty())
107 return op.emitOpError("aie-rt recorded no writes for this push");
108 return success();
109}
110
111struct AIELowerCoreIssuedConfigurationPass
112 : public xilinx::AIE::impl::AIELowerCoreIssuedConfigurationBase<
113 AIELowerCoreIssuedConfigurationPass> {
114
115 /// A declaration of `name`, inside the device, which is a symbol table of
116 /// its own, so that the calls resolve here; outlining the cores later takes
117 /// the declaration out to the module along with them.
118 func::FuncOp declare(DeviceOp device, StringRef name, TypeRange argTypes,
119 TypeRange resultTypes) {
120 if (auto decl = device.lookupSymbol<func::FuncOp>(name))
121 return decl;
122 OpBuilder builder(device.getContext());
123 builder.setInsertionPointToStart(device.getBody());
124 auto decl = func::FuncOp::create(
125 builder, device.getLoc(), name,
126 FunctionType::get(device.getContext(), argTypes, resultTypes));
127 decl.setPrivate();
128 return decl;
129 }
130
131 /// A pointer to `offset` in this tile's register window.
132 Value tilePointer(OpBuilder &builder, Location loc, uint64_t offset) {
133 Type i32 = builder.getI32Type();
134 Type ptr = LLVM::LLVMPointerType::get(builder.getContext());
135 Value addr = arith::ConstantOp::create(
136 builder, loc, i32,
137 builder.getI32IntegerAttr(
138 static_cast<int32_t>(kTileMemoryMappedBase + offset)));
139 return LLVM::IntToPtrOp::create(builder, loc, ptr, addr);
140 }
141
142 LogicalResult lowerPush(DmaPushQueueOp op, const AIETargetModel &targetModel,
143 func::FuncOp write) {
144 CoreOp core = op->getParentOfType<CoreOp>();
145 TileOp tile = core.getTileOp();
146 SmallVector<std::pair<uint64_t, uint32_t>> writes;
147 if (failed(
148 pushWrites(op, tile.getCol(), tile.getRow(), targetModel, writes)))
149 return failure();
150 // The configuration enables the bus for this core before it runs.
151 core.setProcessorBus(true);
152
153 OpBuilder builder(op);
154 Type i32 = builder.getI32Type();
155 for (auto [offset, value] : writes) {
156 Value valueOp = arith::ConstantOp::create(
157 builder, op.getLoc(), i32,
158 builder.getI32IntegerAttr(static_cast<int32_t>(value)));
159 Value addrPtr = tilePointer(builder, op.getLoc(), offset);
160 // A call to a declaration, not llvm.call_intrinsic: the intrinsic is
161 // the AIE backend's, and translation to LLVM IR runs against an LLVM
162 // that does not have it. A named callee is what the other AIE
163 // intrinsics the cores use are reached through too.
164 func::CallOp::create(builder, op.getLoc(), write,
165 ValueRange{valueOp, addrPtr});
166 }
167 op.erase();
168 return success();
169 }
170
171 LogicalResult lowerPoll(DmaPollForQueueSpaceOp op,
172 const AIETargetModel &targetModel,
173 func::FuncOp read) {
174 CoreOp core = op->getParentOfType<CoreOp>();
175 TileOp tile = core.getTileOp();
176 std::optional<uint32_t> statusAddr = targetModel.getDmaStatusAddress(
177 tile.getCol(), tile.getRow(), op.getChannel(), op.getDirection());
178 if (!statusAddr)
179 return op.emitOpError("this tile reports no DMA status register to read");
180 uint32_t mask = targetModel.getDmaTaskQueueSizeMask();
181 if (mask == 0)
182 return op.emitOpError(
183 "this tile reports no task-queue field in its DMA status");
184 core.setProcessorBus(true);
185
186 OpBuilder builder(op);
187 Location loc = op.getLoc();
188 Type i32 = builder.getI32Type();
189 uint32_t localAddr = *statusAddr & ((1u << targetModel.getRowShift()) - 1);
190 Value addrPtr = tilePointer(builder, loc, localAddr);
191 Value maskOp = arith::ConstantOp::create(
192 builder, loc, i32,
193 builder.getI32IntegerAttr(static_cast<int32_t>(mask)));
194 // The field counts the tasks the queue holds, so it reads the depth
195 // exactly when the queue has no room left.
196 uint32_t full = targetModel.getDmaTaskQueueDepth()
197 << llvm::countr_zero(mask);
198 Value fullOp = arith::ConstantOp::create(
199 builder, loc, i32,
200 builder.getI32IntegerAttr(static_cast<int32_t>(full)));
201
202 // Read the field until it falls below the depth. The core does nothing
203 // else meanwhile.
204 auto loop = scf::WhileOp::create(builder, loc, TypeRange{}, ValueRange{});
205 OpBuilder::InsertionGuard guard(builder);
206 Block *before = builder.createBlock(&loop.getBefore());
207 builder.setInsertionPointToStart(before);
208 Value status = func::CallOp::create(builder, loc, read, ValueRange{addrPtr})
209 .getResult(0);
210 Value size = arith::AndIOp::create(builder, loc, status, maskOp);
211 Value isFull = arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::eq,
212 size, fullOp);
213 scf::ConditionOp::create(builder, loc, isFull, ValueRange{});
214
215 Block *after = builder.createBlock(&loop.getAfter());
216 builder.setInsertionPointToStart(after);
217 scf::YieldOp::create(builder, loc, ValueRange{});
218
219 op.erase();
220 return success();
221 }
222
223 void runOnOperation() override {
224 DeviceOp device = getOperation();
225 const AIETargetModel &targetModel = device.getTargetModel();
226
227 SmallVector<DmaPushQueueOp> pushes;
228 SmallVector<DmaPollForQueueSpaceOp> polls;
229 device.walk([&](Operation *op) {
230 if (auto push = dyn_cast<DmaPushQueueOp>(op))
231 pushes.push_back(push);
232 else if (auto poll = dyn_cast<DmaPollForQueueSpaceOp>(op))
233 polls.push_back(poll);
234 });
235 if (pushes.empty() && polls.empty())
236 return;
237
238 std::optional<TileAccessIntrinsics> intrinsics =
239 tileAccessIntrinsics(targetModel.getTargetArch());
240 if (!intrinsics) {
241 Operation *first = pushes.empty() ? polls.front().getOperation()
242 : pushes.front().getOperation();
243 first->emitOpError("this device has no access a core can make to its "
244 "own tile's registers");
245 return signalPassFailure();
246 }
247
248 OpBuilder builder(device.getContext());
249 Type i32 = builder.getI32Type();
250 Type ptr = LLVM::LLVMPointerType::get(device.getContext());
251
252 if (!polls.empty()) {
253 func::FuncOp read = declare(device, intrinsics->read, {ptr}, {i32});
254 for (DmaPollForQueueSpaceOp op : polls)
255 if (failed(lowerPoll(op, targetModel, read)))
256 return signalPassFailure();
257 }
258 if (!pushes.empty()) {
259 func::FuncOp write =
260 declare(device, intrinsics->write, {i32, ptr}, TypeRange{});
261 for (DmaPushQueueOp op : pushes)
262 if (failed(lowerPush(op, targetModel, write)))
263 return signalPassFailure();
264 }
265 }
266};
267
268} // namespace
269
270std::unique_ptr<OperationPass<DeviceOp>>
272 return std::make_unique<AIELowerCoreIssuedConfigurationPass>();
273}
virtual AIEArch getTargetArch() const =0
Return the target architecture.
virtual uint32_t getDmaTaskQueueDepth() const =0
Return how many DMA tasks a single channel's task queue holds (unsupported = 0).
virtual uint32_t getDmaTaskQueueSizeMask() const
Return the mask selecting the live task-queue occupancy field within the getDmaStatusAddress() regist...
virtual uint32_t getColumnShift() const =0
virtual std::optional< uint32_t > getDmaStatusAddress(int, int, int, AIE::DMAChannelDir) const
Return the address of the DMA status register for a channel, or nullopt when this target's layout has...
virtual uint32_t getRowShift() const =0
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIELowerCoreIssuedConfigurationPass()
std::optional< int > parseTransactionBinary(const std::vector< uint8_t > &data, std::vector< TransactionBinaryOperation > &ops)
Parse an exported transaction.
AIEArch
Definition Passes.h:19