MLIR-AIE 1.4.6.dev
AIELowerDmaChannelReset.cpp
Go to the documentation of this file.
1//===- AIELowerDmaChannelReset.cpp ------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
11
12#include "mlir/Dialect/Arith/IR/Arith.h"
13#include "mlir/IR/SymbolTable.h"
14#include "mlir/Pass/Pass.h"
15#include "mlir/Transforms/DialectConversion.h"
16
17namespace xilinx::AIEX {
18#define GEN_PASS_DEF_AIELOWERDMACHANNELRESET
19#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
20} // namespace xilinx::AIEX
21
22#define DEBUG_TYPE "aie-lower-dma-channel-reset"
23
24using namespace mlir;
25using namespace xilinx;
26using namespace xilinx::AIE;
27using namespace xilinx::AIEX;
28
29// Expand every aiex.dma_channel_reset_for in `device` into the resident re-arm
30// trio of its objectFIFO -- one aiex.dma_channel_reset + START_QUEUE re-push
31// (aiex.npu.push_queue) per non-shim channel and one aiex.set_lock per
32// producer/consumer lock -- resolved through the fifo's
33// aie.objectfifo_rearm_binding. The head BD id + repeat of each channel are
34// read straight off the binding (populated by --aie-assign-bd-ids), so this
35// does not re-scan the objectFIFO-emitted aie.mem chain. The emitted
36// dma_channel_reset ops are lowered to npu.maskwrite32 by the conversion below
37// in the same pass; the push_queue / set_lock ops are lowered by aie-dma-to-npu
38// / aie-lower-set- lock later in the pipeline, so this pass runs before them.
39static LogicalResult expandDmaChannelResetForOps(DeviceOp device) {
40 SymbolTable symbolTable(device);
41
42 // One resident endpoint resolved from the binding, ready to re-arm.
43 struct ResolvedEndpoint {
44 Value tile;
45 DMAChannelDir dir;
46 int channel;
47 int col;
48 int row;
49 uint32_t headBdId;
50 int32_t
51 repeatCount; // already the N-1 biased value on the resident dma_start
52 };
53
54 SmallVector<DMAStartOp> corePushStarts;
55 device.walk([&](DMAStartOp start) {
56 if (start.getCorePushes())
57 corePushStarts.push_back(start);
58 });
59
60 SmallVector<DmaChannelResetForOp> toErase;
61 WalkResult res = device.walk([&](DmaChannelResetForOp op) -> WalkResult {
62 auto binding =
63 symbolTable.lookup<ObjectFifoRearmBindingOp>(op.getObjfifo());
64 if (!binding) {
65 op.emitOpError("could not resolve '")
66 << op.getObjfifo()
67 << "' to an aie.objectfifo_rearm_binding; run "
68 "--aie-objectFifo-stateful-transform first";
69 return WalkResult::interrupt();
70 }
71
72 ArrayRef<int32_t> dirs = binding.getChannelDirs();
73 ArrayRef<int32_t> chans = binding.getChannelIndices();
74 ValueRange tiles = binding.getChannelTiles();
75
76 // The per-channel head BD id + repeat live on the binding, put there by
77 // --aie-assign-bd-ids (or set directly on a hand-authored binding). A
78 // binding still missing them cannot be re-pushed: either that pass has not
79 // run, or it could not resolve an endpoint's resident DMA channel.
80 std::optional<ArrayRef<int32_t>> headBdIds = binding.getHeadBdIds();
81 std::optional<ArrayRef<int32_t>> repeats = binding.getRepeatCounts();
82 if (!headBdIds || !repeats) {
83 op.emitOpError("objectFIFO re-arm binding '")
84 << op.getObjfifo()
85 << "' has no head_bd_ids/repeat_counts; run --aie-assign-bd-ids "
86 "before this pass, or set them on the binding";
87 return WalkResult::interrupt();
88 }
89
90 OpBuilder builder(op);
91 Location loc = op.getLoc();
92 MLIRContext *ctx = builder.getContext();
93
94 // Resolve every endpoint up front so a failure aborts before we emit
95 // anything for this op.
96 SmallVector<ResolvedEndpoint> endpoints;
97 for (unsigned i = 0; i < tiles.size(); ++i) {
98 auto tileOp = tiles[i].getDefiningOp<TileOp>();
99 if (!tileOp) {
100 op.emitOpError("re-arm binding channel tile is not an aie.tile");
101 return WalkResult::interrupt();
102 }
103 DMAChannelDir dir =
104 dirs[i] == 0 ? DMAChannelDir::S2MM : DMAChannelDir::MM2S;
105 for (DMAStartOp start : corePushStarts) {
106 if (start->getParentOfType<TileElement>().getTile() == tiles[i] &&
107 start.getChannelDir() == dir &&
108 start.getChannelIndex() == chans[i]) {
109 op.emitOpError(
110 "does not support core-pushed DMA channels; reset requires "
111 "coordination with the issuing core and all shared senders");
112 return WalkResult::interrupt();
113 }
114 }
115 ResolvedEndpoint ep;
116 ep.tile = tiles[i];
117 ep.dir = dir;
118 ep.channel = chans[i];
119 ep.col = tileOp.getCol();
120 ep.row = tileOp.getRow();
121 ep.headBdId = static_cast<uint32_t>((*headBdIds)[i]);
122 ep.repeatCount = (*repeats)[i];
123 endpoints.push_back(ep);
124 }
125
126 // Guard the lock operands (the verifier already requires them to be
127 // aie.lock, but stay defensive so a malformed binding cannot make the
128 // set_lock builder cast a non-lock operand and abort).
129 for (Value lock : binding.getLocks())
130 if (!lock.getDefiningOp<AIE::LockOp>()) {
131 op.emitOpError("re-arm binding lock operand is not an aie.lock");
132 return WalkResult::interrupt();
133 }
134
135 // The re-arm, ordered so a channel is only restarted once its locks are
136 // re-armed:
137 // 1. reset every channel (drain the queue, clear the run FSM),
138 // 2. re-arm the fifo locks to their init values,
139 // 3. re-push each channel's start queue (required on aie2p: a DMA
140 // channel has no enable bit, so the only way to restart it is a
141 // START_QUEUE write).
142
143 // 1. dma_channel_reset per endpoint (lowered to maskwrite32 below).
144 for (const ResolvedEndpoint &ep : endpoints)
145 DmaChannelResetOp::create(builder, loc, ep.tile,
146 DMAChannelDirAttr::get(ctx, ep.dir),
147 builder.getI32IntegerAttr(ep.channel));
148
149 // 2. set_lock per lock, to the value the fifo was armed with.
150 ValueRange locks = binding.getLocks();
151 ArrayRef<int32_t> lockInits = binding.getLockInits();
152 for (unsigned j = 0; j < locks.size(); ++j)
153 SetLockOp::create(builder, loc, locks[j],
154 createConstantI32(builder, loc, lockInits[j]));
155
156 // 3. START_QUEUE re-push per endpoint, emitted as aiex.npu.push_queue so
157 // the command-word encoding, the queue address, and the bd_id/repeat range
158 // checks all live in one place -- the aie-dma-to-npu lowering and
159 // NpuPushQueueOp::verify. The token bit is only set for a shim S2MM
160 // channel; these endpoints are core/mem, so it is always false.
161 for (const ResolvedEndpoint &ep : endpoints) {
162 bool issueToken = ep.row == 0 && ep.dir == DMAChannelDir::S2MM;
163 // Materialize the operands in fixed statements before the create() so
164 // their emission order is deterministic (function-argument evaluation
165 // order is unspecified in C++).
166 Value repeatVal = createConstantI32(
167 builder, loc, static_cast<uint32_t>(ep.repeatCount));
168 Value bdVal = createConstantI32(builder, loc, ep.headBdId);
169 NpuPushQueueOp::create(builder, loc, builder.getI32IntegerAttr(ep.col),
170 builder.getI32IntegerAttr(ep.row),
171 DMAChannelDirAttr::get(ctx, ep.dir),
172 builder.getI32IntegerAttr(ep.channel),
173 builder.getBoolAttr(issueToken), repeatVal, bdVal,
174 /*repeat_state_table_idx=*/nullptr);
175 }
176
177 toErase.push_back(op);
178 return WalkResult::advance();
179 });
180
181 if (res.wasInterrupted())
182 return failure();
183
184 for (DmaChannelResetForOp op : toErase)
185 op.erase();
186
187 // Drop re-arm bindings that are now unreferenced (their reset_for users were
188 // lowered). They are device-body metadata like aie.shim_dma_allocation;
189 // erasing the dead ones keeps the lowered module clean.
190 SmallVector<ObjectFifoRearmBindingOp> deadBindings;
191 for (auto binding : device.getOps<ObjectFifoRearmBindingOp>())
192 if (SymbolTable::symbolKnownUseEmpty(binding.getSymNameAttr(), device))
193 deadBindings.push_back(binding);
194 for (ObjectFifoRearmBindingOp binding : deadBindings)
195 binding.erase();
196
197 return success();
198}
199
201 : OpConversionPattern<DmaChannelResetOp> {
202 using OpConversionPattern<DmaChannelResetOp>::OpConversionPattern;
203
205 : OpConversionPattern(context) {}
206
207 LogicalResult
208 matchAndRewrite(DmaChannelResetOp op, OpAdaptor adaptor,
209 ConversionPatternRewriter &rewriter) const override {
210 AIE::DeviceOp dev = op->getParentOfType<AIE::DeviceOp>();
211 const AIE::AIETargetModel &tm = dev.getTargetModel();
212
213 AIE::TileOp tile = op.getTileOp();
214 int col = tile.getCol();
215 int row = tile.getRow();
216 int channel = op.getChannel();
217 AIE::DMAChannelDir dir = op.getDirection();
218
219 // NpuMaskWrite32Op re-folds col/row, so pass the local offset + col/row, as
220 // AIELowerSetLock does for the lock address.
221 uint32_t ctrlAddrLocal =
222 tm.getLocalDmaControlAddress(col, row, channel, dir);
223
224 // Resolve the channel's reset bit from the register database rather than
225 // restating the layout here. The DMA control registers live in a tile's
226 // memory module (mem tiles resolve to their own module regardless), so the
227 // lookup is isMem; shim tiles are rejected by the verifier.
228 std::string ctrlRegName =
229 (dir == AIE::DMAChannelDir::S2MM ? "DMA_S2MM_" : "DMA_MM2S_") +
230 std::to_string(channel) + "_Ctrl";
231 const AIE::RegisterInfo *ctrlReg =
232 tm.lookupRegister(ctrlRegName, tile.getTileID(), /*isMem=*/true);
233 if (!ctrlReg)
234 return op.emitOpError("target has no ")
235 << ctrlRegName << " register in its register database";
236 const AIE::BitFieldInfo *resetField = ctrlReg->getField("Reset");
237 if (!resetField)
238 return op.emitOpError()
239 << ctrlRegName << " has no Reset field in the register database";
240 std::optional<uint32_t> resetMask = tm.getFieldMask(*resetField);
241 if (!resetMask)
242 return op.emitOpError()
243 << ctrlRegName << " Reset field does not fit in a 32-bit register";
244 std::optional<uint32_t> resetValue = tm.encodeFieldValue(*resetField, 1);
245 if (!resetValue)
246 return op.emitOpError()
247 << ctrlRegName
248 << " Reset field value does not fit in its bit width";
249
250 Location loc = op.getLoc();
251 IntegerAttr colAttr = rewriter.getI32IntegerAttr(col);
252 IntegerAttr rowAttr = rewriter.getI32IntegerAttr(row);
253
254 // Reset pulse: assert the reset bit, then clear it. Both writes mask to the
255 // reset bit only, so the pulse preserves the other CTRL fields
256 // (DECOMPRESSION_ENABLE, ENABLE_OUT_OF_ORDER, CONTROLLER_ID, FOT_MODE)
257 // instead of clobbering them. This mirrors aie-rt's XAie_DmaChannelReset,
258 // which drives the reset bit with a MaskWrite32. Constants are materialized
259 // in named locals so the emitted IR order does not depend on unspecified
260 // C++ argument-evaluation order.
261 Value assertAddr = createConstantI32(rewriter, loc, ctrlAddrLocal);
262 Value assertVal = createConstantI32(rewriter, loc, *resetValue);
263 Value assertMask = createConstantI32(rewriter, loc, *resetMask);
264 NpuMaskWrite32Op::create(rewriter, loc, assertAddr, assertVal, assertMask,
265 nullptr, colAttr, rowAttr);
266
267 Value clearAddr = createConstantI32(rewriter, loc, ctrlAddrLocal);
268 Value clearVal = createConstantI32(rewriter, loc, 0u);
269 Value clearMask = createConstantI32(rewriter, loc, *resetMask);
270 rewriter.replaceOpWithNewOp<NpuMaskWrite32Op>(
271 op, clearAddr, clearVal, clearMask, nullptr, colAttr, rowAttr);
272
273 return success();
274 };
275};
276
279 AIELowerDmaChannelResetPass> {
280 void runOnOperation() override {
281 DeviceOp device = getOperation();
282
283 // First expand any aiex.dma_channel_reset_for into its re-arm trio; the
284 // dma_channel_reset ops it emits are then lowered by the conversion below,
285 // together with any standalone ones.
286 if (failed(expandDmaChannelResetForOps(device)))
287 return signalPassFailure();
288
289 ConversionTarget target(getContext());
290 target.addLegalOp<NpuMaskWrite32Op>();
291 target.addLegalDialect<arith::ArithDialect>();
292 target.addIllegalOp<DmaChannelResetOp>();
293
294 RewritePatternSet patterns(&getContext());
295 patterns.add<DmaChannelResetToMaskWrite32Pattern>(&getContext());
296
297 SmallVector<Operation *> roots;
298 device.walk([&](DmaChannelResetOp op) { roots.push_back(op); });
299 if (failed(applyPartialConversion(roots, target, std::move(patterns))))
300 signalPassFailure();
301 }
302};
303
304std::unique_ptr<OperationPass<DeviceOp>>
306 return std::make_unique<AIELowerDmaChannelResetPass>();
307}
uint32_t getLocalDmaControlAddress(int col, int row, int channel, AIE::DMAChannelDir direction) const
Return the DMA task-queue register address relative to its tile.
mlir::Value createConstantI32(mlir::OpBuilder &builder, mlir::Location loc, uint32_t value)
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIELowerDmaChannelResetPass()
LogicalResult matchAndRewrite(DmaChannelResetOp op, OpAdaptor adaptor, ConversionPatternRewriter &rewriter) const override
Bit field information for a register.
const BitFieldInfo * getField(llvm::StringRef fieldName) const