MLIR-AIE 1.4.5.dev
AIEExpandLoadPdi.cpp
Go to the documentation of this file.
1//===- AIEExpandLoadPdi.cpp -------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// This pass expands `npu.load_pdi` operations that reference a device. There
9// are two output modes, controlled by the `ctrl-pkt` option:
10//
11// 1. Default (ctrl-pkt=false): replaces each `load_pdi @device` with
12// a. an empty device PDI load (`load_pdi @empty_N`), which causes the
13// firmware to reset the device, and
14// b. an `aiex.npu.write_config @device`, whose register writes the NPU
15// translation emits (or, with inline-config, those writes as ops).
16// c. with teardown, an `aiex.npu.write_teardown @device` where it ends.
17// 2. With ctrl-pkt=true: replaces each `load_pdi @device` with
18// a. a `load_pdi @ctrl_pkt_overlay`, which configures the NPU to stream
19// further configuration as control packets, and
20// b. a sequence of `aiex.npu.control_packet` ops carrying the device's
21// configuration.
22//
23//===----------------------------------------------------------------------===//
24
30
31#include "mlir/Dialect/Arith/IR/Arith.h"
32#include "mlir/Dialect/MemRef/IR/MemRef.h"
33#include "mlir/Pass/Pass.h"
34
35#include "llvm/ADT/DenseMap.h"
36#include "llvm/ADT/MapVector.h"
37#include "llvm/ADT/SetVector.h"
38#include "llvm/ADT/StringRef.h"
39#include "llvm/Support/FormatVariadic.h"
40#include "llvm/Support/raw_ostream.h"
41
42#include <array>
43#include <map>
44#include <set>
45
46namespace xilinx::AIEX {
47#define GEN_PASS_DEF_AIEEXPANDLOADPDI
48#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
49} // namespace xilinx::AIEX
50
51#define DEBUG_TYPE "aie-expand-load-pdi"
52
53using namespace mlir;
54using namespace xilinx;
55using namespace xilinx::AIEX;
56using namespace xilinx::AIE;
57
58namespace {
59
60// Name of the overlay device loaded ahead of streaming configuration as
61// control packets. The device is expected to exist in the module (e.g.,
62// emitted by the `aie-generate-column-control-overlay` pass).
63static constexpr llvm::StringLiteral kCtrlPktOverlayName = "ctrl_pkt_overlay";
64// Name of an alternating copy of the overlay device, used to avoid PDI
65// address caching when the same overlay would otherwise be loaded twice in a
66// row (the firmware would treat the second load as a no-op).
67static constexpr llvm::StringLiteral kCtrlPktOverlayCopyName =
68 "ctrl_pkt_overlay_copy";
69
70// Look up `kCtrlPktOverlayName` and return a clone of it named
71// `kCtrlPktOverlayCopyName`, creating it once if needed. Returns nullptr on
72// error.
73static AIE::DeviceOp getOrCreateCtrlPktOverlayCopy(ModuleOp moduleOp,
74 Operation *errLocOp) {
75 if (auto existing =
76 moduleOp.lookupSymbol<AIE::DeviceOp>(kCtrlPktOverlayCopyName))
77 return existing;
78
79 auto orig = moduleOp.lookupSymbol<AIE::DeviceOp>(kCtrlPktOverlayName);
80 if (!orig) {
81 errLocOp->emitError("ctrl-pkt mode requires a `@")
82 << kCtrlPktOverlayName << "` device in the module";
83 return nullptr;
84 }
85
86 OpBuilder builder(moduleOp.getContext());
87 builder.setInsertionPointAfter(orig);
88 auto *cloned = builder.clone(*orig.getOperation());
89 auto clonedDev = cast<AIE::DeviceOp>(cloned);
90 clonedDev.setSymName(kCtrlPktOverlayCopyName);
91 return clonedDev;
92}
93
94// A stream-switch setting: a port, or with `slot` >= 0 one of its packet rule
95// slots.
96struct SwitchPort {
97 AIE::TileID tile;
98 bool master;
99 AIE::Port port;
100 int slot;
101
102 bool operator<(const SwitchPort &other) const {
103 return std::tie(tile, master, port, slot) <
104 std::tie(other.tile, other.master, other.port, other.slot);
105 }
106
107 std::string describe() const {
108 std::string s =
109 llvm::formatv("tile ({0}, {1}) {2} {3} : {4}", tile.col, tile.row,
110 master ? "master" : "slave",
111 AIE::stringifyWireBundle(port.bundle), port.channel);
112 if (slot >= 0)
113 s += llvm::formatv(" slot {0}", slot).str();
114 return s;
115 }
116};
117
118struct PortSetting {
119 SwitchPort port;
120 std::string value;
121 Operation *op;
122 bool skipped;
123};
124
125// The settings `op` writes, and whether a reload skips each. Each packet rule
126// fills one slot, and a reload skips the rules marked
127// is_ctrl_pkt_overlay. It skips enabling the port if it skips any of them.
128static SmallVector<PortSetting> portSettings(Operation &op,
129 AIE::SwitchboxOp sb) {
130 auto key = [&](bool master, AIE::Port port, int slot = -1) {
131 return SwitchPort{{sb.colIndex(), sb.rowIndex()}, master, port, slot};
132 };
133 auto amsel = [](Value v) {
134 auto a = v.getDefiningOp<AIE::AMSelOp>();
135 return llvm::formatv("{0}.{1}", a.arbiterIndex(), a.getMselValue()).str();
136 };
137 AIE::AIEDialect::IsCtrlPktOverlayAttrHelper overlay(op.getContext());
138 bool skipped = overlay.isAttrPresent(&op);
139 SmallVector<PortSetting> settings;
140 if (auto connect = dyn_cast<AIE::ConnectOp>(op)) {
141 AIE::Port src = connect.sourcePort();
142 settings.push_back(
143 {key(true, connect.destPort()),
144 llvm::formatv("circuit {0} : {1}",
145 AIE::stringifyWireBundle(src.bundle), src.channel),
146 &op, skipped});
147 settings.push_back({key(false, src), "circuit", &op, skipped});
148 } else if (auto masterSet = dyn_cast<AIE::MasterSetOp>(op)) {
149 int arbiter = -1;
150 int mask = 0;
151 for (Value v : masterSet.getAmsels()) {
152 auto a = cast<AIE::AMSelOp>(v.getDefiningOp());
153 arbiter = a.arbiterIndex();
154 mask |= 1 << a.getMselValue();
155 }
156 std::string value =
157 llvm::formatv("packet {0}/{1} {2}", arbiter, mask,
158 masterSet.keepsPktHeader() ? "keep" : "drop")
159 .str();
160 settings.push_back({key(true, masterSet.destPort()), value, &op, skipped});
161 } else if (auto rules = dyn_cast<AIE::PacketRulesOp>(op)) {
162 // An empty packet_rules op writes nothing to the hardware.
163 auto ruleOps = rules.getRules().front().getOps<AIE::PacketRuleOp>();
164 if (ruleOps.empty())
165 return settings;
166 bool anySkipped = skipped;
167 for (auto [slot, rule] : llvm::enumerate(ruleOps)) {
168 bool ruleSkipped = skipped || overlay.isAttrPresent(rule);
169 anySkipped |= ruleSkipped;
170 settings.push_back({key(false, rules.sourcePort(), slot),
171 llvm::formatv("{0}/{1}>{2}", rule.valueInt(),
172 rule.maskInt(), amsel(rule.getAmsel())),
173 rule, ruleSkipped});
174 }
175 settings.insert(settings.begin(), {key(false, rules.sourcePort()), "packet",
176 &op, anySkipped});
177 }
178 return settings;
179}
180
181// A control-packet reload streams `device`'s configuration over the routes
182// `overlay` set up, skipping what is marked is_ctrl_pkt_overlay. So the
183// overlay must already write every setting the reload skips, the same way,
184// and the reload may write nothing the overlay writes.
185static LogicalResult verifyReloadKeepsOverlay(AIE::DeviceOp device,
186 AIE::DeviceOp overlay) {
187 StringRef overlayName = overlay.getSymName();
188 std::map<SwitchPort, std::string> overlaySettings;
189 overlay.walk([&](AIE::SwitchboxOp sb) {
190 for (Operation &op : sb.getConnections().front())
191 for (PortSetting &setting : portSettings(op, sb))
192 overlaySettings[setting.port] = setting.value;
193 });
194 WalkResult result = device.walk([&](AIE::SwitchboxOp sb) {
195 for (Operation &op : sb.getConnections().front()) {
196 for (auto &[port, value, setter, skipped] : portSettings(op, sb)) {
197 auto it = overlaySettings.find(port);
198 if (skipped && it == overlaySettings.end()) {
199 setter->emitError()
200 << "a control-packet reload skips this op, but @" << overlayName
201 << " does not set " << port.describe();
202 return WalkResult::interrupt();
203 }
204 if (skipped && it->second != value) {
205 setter->emitError()
206 << "a control-packet reload skips this op, but @" << overlayName
207 << " sets " << port.describe() << " differently";
208 return WalkResult::interrupt();
209 }
210 if (!skipped && it != overlaySettings.end()) {
211 setter->emitError()
212 << "a control-packet reload rewrites " << port.describe()
213 << ", which the control packets of @" << overlayName
214 << " route through";
215 return WalkResult::interrupt();
216 }
217 }
218 }
219 return WalkResult::advance();
220 });
221 return failure(result.wasInterrupted());
222}
223
224// A control-packet reload reaches a tile only if `overlay` routes control
225// packets to its TileControl port. `packets` are the reload's.
226static LogicalResult
227verifyOverlayReachesTiles(NpuLoadPdiOp loadPdiOp, AIE::DeviceOp overlay,
228 iterator_range<Block::iterator> packets) {
229 std::set<AIE::TileID> reached;
230 overlay.walk([&](AIE::SwitchboxOp sb) {
231 for (Operation &op : sb.getConnections().front()) {
232 std::optional<AIE::Port> dest;
233 if (auto master = dyn_cast<AIE::MasterSetOp>(op))
234 dest = master.destPort();
235 else if (auto connect = dyn_cast<AIE::ConnectOp>(op))
236 dest = connect.destPort();
237 if (dest == AIE::Port{AIE::WireBundle::TileControl, 0})
238 reached.insert({sb.colIndex(), sb.rowIndex()});
239 }
240 });
241 for (Operation &op : packets) {
242 auto packet = dyn_cast<NpuControlPacketOp>(op);
243 if (!packet)
244 continue;
245 AIE::TileID tile{static_cast<int>(packet.getColumnFromAddr()),
246 static_cast<int>(packet.getRowFromAddr())};
247 if (reached.count(tile))
248 continue;
249 return loadPdiOp.emitError()
250 << "a control-packet reload configures tile (" << tile.col << ", "
251 << tile.row << "), but @" << overlay.getSymName()
252 << " routes no control packets to it";
253 }
254 return success();
255}
256
257// The empty device whose PDI load makes the firmware reset the array. `parity`
258// alternates so that two consecutive loads never name the same PDI (see
259// transformLoadPdi).
260static AIE::DeviceOp getOrCreateEmptyDevice(ModuleOp moduleOp,
261 AIE::AIEDevice deviceType,
262 unsigned parity, Location loc) {
263 std::string emptyName = "empty_" + std::to_string(parity);
264 if (auto existing = moduleOp.lookupSymbol<AIE::DeviceOp>(emptyName)) {
265 // Shared reset PDIs retain every load and trailing sequence's provenance.
266 loc = FusedLoc::get(moduleOp.getContext(), {existing.getLoc(), loc});
267 existing->setLoc(loc);
268 existing.getRegion().front().getTerminator()->setLoc(loc);
269 return existing;
270 }
271
272 OpBuilder builder(moduleOp.getContext());
273 builder.setInsertionPointToStart(moduleOp.getBody());
274 auto emptyDevice = AIE::DeviceOp::create(builder, loc, deviceType,
275 builder.getStringAttr(emptyName));
276 emptyDevice.getRegion().emplaceBlock();
277 builder.setInsertionPointToEnd(&emptyDevice.getRegion().front());
278 AIE::EndOp::create(builder, loc);
279 return emptyDevice;
280}
281
282// A control-packet reload: the load_pdi it replaces, the device it streams,
283// and the overlay load ahead of it.
284struct CtrlPktReload {
285 NpuLoadPdiOp load;
286 AIE::DeviceOp device;
287 NpuLoadPdiOp preload;
288 AIE::DeviceOp overlay;
289};
290
291// Set `builder` at the next load_pdi, or at the block's end if there is none.
292static void setInsertionPointToSegmentEnd(OpBuilder &builder,
293 NpuLoadPdiOp loadPdiOp) {
294 Operation *op = loadPdiOp->getNextNode();
295 while (op && !isa<NpuLoadPdiOp>(op) && !op->hasTrait<OpTrait::IsTerminator>())
296 op = op->getNextNode();
297 if (op)
298 builder.setInsertionPoint(op);
299 else
300 builder.setInsertionPointToEnd(loadPdiOp->getBlock());
301}
302
303// Helper to transform a single load_pdi operation. Without `keepPreload` the
304// empty-device reset is left out. With `teardown` the configuration's teardown
305// goes where it ends.
306static LogicalResult
307transformLoadPdi(NpuLoadPdiOp loadPdiOp, ModuleOp moduleOp, unsigned index,
308 AIEX::ExpandMode defaultMode, bool keepPreload, bool teardown,
309 SmallVectorImpl<CtrlPktReload> &ctrlPktReloads) {
310 OpBuilder builder(loadPdiOp);
311
312 // Only process load_pdi ops that reference a device
313 auto deviceRefAttr = loadPdiOp.getDeviceRefAttr();
314 if (!deviceRefAttr) {
315 return success();
316 }
317
318 // Per-op annotation takes precedence; fall back to pass default
319 AIEX::ExpandMode mode = loadPdiOp.getExpandMode().value_or(defaultMode);
320 if (mode == AIEX::ExpandMode::none)
321 return success();
322 bool ctrlPkt = (mode == AIEX::ExpandMode::ctrlpkt);
323
324 auto referencedDevice = moduleOp.lookupSymbol<AIE::DeviceOp>(deviceRefAttr);
325 if (!referencedDevice) {
326 loadPdiOp.emitError("Referenced symbol '")
327 << deviceRefAttr.getValue() << "' is not a device";
328 return failure();
329 }
330
331 FlatSymbolRefAttr preloadRef;
332 AIE::DeviceOp overlay;
333 if (ctrlPkt) {
334 // Overlay device PDI
335 // Alternate between the original overlay and a clone of it on every
336 // other load. Loading the same PDI twice in a row gets cached by the
337 // firmware (the second load becomes a no-op), so we need two distinct
338 // PDI addresses that carry the same overlay configuration.
339 StringRef overlayName =
340 (index % 2 == 0) ? kCtrlPktOverlayName : kCtrlPktOverlayCopyName;
341 if (index % 2 != 0) {
342 overlay =
343 getOrCreateCtrlPktOverlayCopy(moduleOp, loadPdiOp.getOperation());
344 if (!overlay)
345 return failure();
346 } else {
347 overlay = moduleOp.lookupSymbol<AIE::DeviceOp>(kCtrlPktOverlayName);
348 if (!overlay) {
349 loadPdiOp.emitError("ctrl-pkt mode requires a `@")
350 << kCtrlPktOverlayName << "` device in the module";
351 return failure();
352 }
353 }
354 if (failed(verifyReloadKeepsOverlay(referencedDevice, overlay)))
355 return failure();
356 preloadRef = FlatSymbolRefAttr::get(builder.getContext(), overlayName);
357 } else if (keepPreload) {
358 // Empty device PDI (triggers firmware reset)
359 AIE::DeviceOp emptyDevice = getOrCreateEmptyDevice(
360 moduleOp, referencedDevice.getDevice(), index % 2, loadPdiOp.getLoc());
361 preloadRef = FlatSymbolRefAttr::get(emptyDevice.getSymNameAttr());
362 }
363
364 builder.setInsertionPoint(loadPdiOp);
365
366 // Emit the preload load_pdi (either empty-device reset or ctrl_pkt_overlay).
367 if (ctrlPkt) {
368 NpuLoadPdiOp preload =
369 NpuLoadPdiOp::create(builder, loadPdiOp.getLoc(), preloadRef,
370 /*id=*/nullptr, /*size=*/nullptr,
371 /*address=*/nullptr,
372 /*expand_mode=*/
373 AIEX::ExpandModeAttr::get(builder.getContext(),
374 AIEX::ExpandMode::none));
375 // The control packets go in once every reload is known, so that a
376 // sequence's reloads share their constants.
377 ctrlPktReloads.push_back({loadPdiOp, referencedDevice, preload, overlay});
378 return success();
379 }
380 if (keepPreload)
381 NpuLoadPdiOp::create(builder, loadPdiOp.getLoc(), preloadRef,
382 loadPdiOp.getIdAttr(), loadPdiOp.getSizeAttr(),
383 loadPdiOp.getAddressAttr(),
384 /*expand_mode=*/
385 AIEX::ExpandModeAttr::get(builder.getContext(),
386 AIEX::ExpandMode::none));
387 NpuWriteConfigOp::create(builder, loadPdiOp.getLoc(), deviceRefAttr);
388 if (teardown) {
389 setInsertionPointToSegmentEnd(builder, loadPdiOp);
390 NpuWriteTeardownOp::create(builder, loadPdiOp.getLoc(), deviceRefAttr);
391 }
392 loadPdiOp.erase();
393 return success();
394}
395
396// The checks that need no expansion, so the pass fails before it changes the
397// IR.
398static LogicalResult
399verifyExpansion(ModuleOp module, ArrayRef<NpuLoadPdiOp> loadPdiOps,
400 function_ref<AIEX::ExpandMode(NpuLoadPdiOp)> expandMode,
401 bool teardown, bool initEntry) {
402 if (initEntry && !teardown)
403 return module.emitError(
404 "aie-expand-load-pdi: init-entry requires teardown");
405 if (initEntry)
406 for (auto device : module.getOps<AIE::DeviceOp>())
407 if (Operation *existing = device.lookupSymbol(initEntrySequenceName))
408 return existing->emitOpError()
409 << "is named '" << initEntrySequenceName
410 << "', which aie-expand-load-pdi reserves under init-entry";
411 if (!teardown)
412 return success();
413 for (NpuLoadPdiOp loadPdiOp : loadPdiOps) {
414 AIEX::ExpandMode mode = expandMode(loadPdiOp);
415 if (mode == AIEX::ExpandMode::ctrlpkt)
416 return loadPdiOp.emitError(
417 "teardown is not implemented for control packet reconfiguration");
418 if (mode == AIEX::ExpandMode::none)
419 continue;
420 // write_teardown's verifier checks this too, but inline-config leaves no
421 // write_teardown to verify.
422 auto dev = module.lookupSymbol<AIE::DeviceOp>(loadPdiOp.getDeviceRefAttr());
423 if (Operation *start = dev ? AIEX::findShimDmaStart(dev) : nullptr)
424 return start->emitOpError(
425 "starts a shim DMA channel, which teardown cannot stop");
426 }
427 return success();
428}
429
430struct AIEExpandLoadPdiPass
431 : public xilinx::AIEX::impl::AIEExpandLoadPdiBase<AIEExpandLoadPdiPass> {
432 using AIEExpandLoadPdiBase::AIEExpandLoadPdiBase;
433
434 void getDependentDialects(DialectRegistry &registry) const override {
435 registry.insert<arith::ArithDialect, memref::MemRefDialect, AIE::AIEDialect,
436 AIEX::AIEXDialect>();
437 }
438
439 void runOnOperation() override {
440 auto module = getOperation();
441
442 // Collect all load_pdi operations in program order;
443 // need to collect once, then transform all collected ops;
444 // since the transform inserts a new preload load_pdi, we can't transform
445 // as we walk or it'd infinitely recurse.
446 SmallVector<NpuLoadPdiOp> loadPdiOps;
447
448 module.walk(
449 [&](NpuLoadPdiOp loadPdiOp) { loadPdiOps.push_back(loadPdiOp); });
450
451 // Map the legacy bool option to the new ExpandMode enum.
452 AIEX::ExpandMode defaultMode =
453 clCtrlPkt ? AIEX::ExpandMode::ctrlpkt : AIEX::ExpandMode::write32;
454
455 auto expandMode = [&](NpuLoadPdiOp loadPdiOp) {
456 return loadPdiOp.getDeviceRefAttr()
457 ? loadPdiOp.getExpandMode().value_or(defaultMode)
458 : AIEX::ExpandMode::none;
459 };
460 // Per enclosing runtime sequence, the parity of the FIRST and LAST of its
461 // empty-device resets and of its overlay loads, plus the device type, and
462 // the kind of its last load, which leaves the array as the next dispatch
463 // finds it. `transformLoadPdi` picks the preload as `index % 2` over the
464 // module-wide op order, and that order includes ops this pass skips, so a
465 // sequence's first preload is not necessarily the even one and its parity
466 // cannot be recovered from a count. Captured BEFORE transforming, which
467 // erases the ops.
468 struct PreloadParity {
469 unsigned firstParity = 0;
470 unsigned lastParity = 0;
471 bool seen = false;
472 AIE::AIEDevice device = {};
473 };
474 struct SequenceLoads {
475 std::array<PreloadParity, 2> preloads;
476 AIEX::ExpandMode last = AIEX::ExpandMode::none;
477 };
478 llvm::MapVector<AIE::RuntimeSequenceOp, SequenceLoads> sequences;
479 for (auto [index, loadPdiOp] : llvm::enumerate(loadPdiOps)) {
480 auto seq = loadPdiOp->getParentOfType<AIE::RuntimeSequenceOp>();
481 if (!seq)
482 continue;
483 SequenceLoads &loads = sequences[seq];
484 AIEX::ExpandMode mode = loads.last = expandMode(loadPdiOp);
485 if (mode == AIEX::ExpandMode::none)
486 continue;
487 auto dev =
488 module.lookupSymbol<AIE::DeviceOp>(loadPdiOp.getDeviceRefAttr());
489 if (!dev)
490 continue;
491 auto &entry = loads.preloads[mode == AIEX::ExpandMode::ctrlpkt];
492 if (!entry.seen) {
493 entry.firstParity = index % 2;
494 entry.seen = true;
495 }
496 entry.lastParity = index % 2;
497 entry.device = dev.getDevice();
498 }
499
500 // Teardown leaves the array ready for the next configuration, so only a
501 // sequence's first load needs a reset. A load this pass leaves alone sets
502 // up a configuration no teardown undoes, so the load after it keeps its
503 // reset. The host may dispatch a device's sequences in any order, so with
504 // an init entry they start without a reset only if each ends torn down,
505 // and `reconfig_init` holds that reset.
506 llvm::SetVector<AIE::DeviceOp> initEntries;
507 if (clInitEntry) {
508 for (AIE::RuntimeSequenceOp seq : llvm::make_first_range(sequences))
509 initEntries.insert(seq->getParentOfType<AIE::DeviceOp>());
510 for (auto &[seq, loads] : sequences)
511 if (loads.last != AIEX::ExpandMode::write32)
512 initEntries.remove(seq->getParentOfType<AIE::DeviceOp>());
513 }
514
515 if (failed(verifyExpansion(module, loadPdiOps, expandMode, clTeardown,
516 clInitEntry)))
517 return signalPassFailure();
518
519 SmallVector<CtrlPktReload> ctrlPktReloads;
520 // The kind of each sequence's last load, which left the array as it is.
521 llvm::DenseMap<Operation *, std::optional<AIEX::ExpandMode>> arrayState;
522 for (auto [idx, loadPdiOp] : llvm::enumerate(loadPdiOps)) {
523 auto seq = loadPdiOp->getParentOfType<AIE::RuntimeSequenceOp>();
524 std::optional<AIEX::ExpandMode> start;
525 if (seq && initEntries.contains(seq->getParentOfType<AIE::DeviceOp>()))
526 start = AIEX::ExpandMode::write32;
527 std::optional<AIEX::ExpandMode> &state =
528 arrayState.try_emplace(seq, start).first->second;
529 // We need to insert a reset/overlay `load_pdi` if the last configuration
530 // was configured using a different mode, or if we did not insert a
531 // teardown.
532 bool keepPreload = !clTeardown || state != expandMode(loadPdiOp);
533 state = expandMode(loadPdiOp);
534 if (failed(transformLoadPdi(loadPdiOp, module, idx, defaultMode,
535 keepPreload, clTeardown, ctrlPktReloads))) {
536 signalPassFailure();
537 return;
538 }
539 }
540
541 SmallVector<std::pair<Operation *, AIE::DeviceOp>> ctrlPktSites;
542 for (CtrlPktReload &reload : ctrlPktReloads)
543 ctrlPktSites.emplace_back(reload.load, reload.device);
544 if (failed(AIE::insertConfigOps(
546 signalPassFailure();
547 return;
548 }
549 for (CtrlPktReload &reload : ctrlPktReloads) {
550 if (failed(verifyOverlayReachesTiles(
551 reload.load, reload.overlay,
552 {std::next(reload.preload->getIterator()),
553 reload.load->getIterator()}))) {
554 signalPassFailure();
555 return;
556 }
557 reload.load.erase();
558 }
559
560 for (AIE::DeviceOp device : initEntries) {
561 OpBuilder builder(device.getContext());
562 builder.setInsertionPoint(
563 *device.getOps<AIE::RuntimeSequenceOp>().begin());
564 auto init = AIE::RuntimeSequenceOp::create(
565 builder, device.getLoc(),
566 builder.getStringAttr(initEntrySequenceName), BoolAttr{},
567 AIE::TraceBufferAttr{}, ArrayAttr{}, ArrayAttr{});
568 builder.setInsertionPointToStart(&init.getBody().emplaceBlock());
569 AIE::DeviceOp emptyDevice = getOrCreateEmptyDevice(
570 module, device.getDevice(), 0, device.getLoc());
571 NpuLoadPdiOp::create(builder, device.getLoc(),
572 FlatSymbolRefAttr::get(emptyDevice.getSymNameAttr()),
573 /*id=*/nullptr, /*size=*/nullptr,
574 /*address=*/nullptr,
575 /*expand_mode=*/
576 AIEX::ExpandModeAttr::get(builder.getContext(),
577 AIEX::ExpandMode::none));
578 }
579
580 if (clInlineConfig && failed(AIE::inlineWriteConfigs(module)))
581 return signalPassFailure();
582
583 // Under teardown a skipped reset follows only torn-down configurations, as
584 // write_config's verifier checks, so nothing needs the append.
585 if (clTeardown)
586 return;
587
588 // The `index % 2` alternation above keeps two consecutive preloads on
589 // different PDIs WITHIN a sequence. But the host re-runs the whole sequence
590 // on every dispatch, so the alternation has to hold across that boundary
591 // too. When a sequence's last preload lands on the same PDI as its first,
592 // the firmware caches the address and no-ops the next dispatch's first
593 // load, and the configuration that follows lands on a device that was never
594 // reset. That is silent -- the first dispatch is correct and later ones
595 // return non-deterministic garbage.
596 //
597 // Append one more preload, of the parity opposite the last one, so the
598 // sequence ends somewhere its own start will not repeat. The empty PDI is a
599 // few hundred bytes, so the cost is negligible next to the configuration it
600 // guards.
601 for (auto &[seq, loads] : sequences) {
602 for (auto [ctrlPkt, info] : llvm::enumerate(loads.preloads)) {
603 if (!info.seen || info.firstParity != info.lastParity)
604 continue;
605 unsigned parity = 1 - info.lastParity;
606 FlatSymbolRefAttr preloadRef;
607 if (ctrlPkt) {
608 if (parity == 1 && !getOrCreateCtrlPktOverlayCopy(module, seq)) {
609 signalPassFailure();
610 return;
611 }
612 preloadRef = FlatSymbolRefAttr::get(
613 seq.getContext(),
614 parity == 0 ? kCtrlPktOverlayName : kCtrlPktOverlayCopyName);
615 } else {
616 AIE::DeviceOp emptyDevice =
617 getOrCreateEmptyDevice(module, info.device, parity, seq.getLoc());
618 preloadRef = FlatSymbolRefAttr::get(emptyDevice.getSymNameAttr());
619 }
620 OpBuilder builder(seq.getContext());
621 Block &body = seq.getBody().front();
622 if (!body.empty() && body.back().hasTrait<OpTrait::IsTerminator>())
623 builder.setInsertionPoint(&body.back());
624 else
625 builder.setInsertionPointToEnd(&body);
626 NpuLoadPdiOp::create(builder, seq.getLoc(), preloadRef,
627 /*id=*/nullptr, /*size=*/nullptr,
628 /*address=*/nullptr,
629 /*expand_mode=*/
630 AIEX::ExpandModeAttr::get(seq.getContext(),
631 AIEX::ExpandMode::none));
632 }
633 }
634 }
635};
636
637} // namespace
638
639std::unique_ptr<mlir::OperationPass<mlir::ModuleOp>>
641 return std::make_unique<AIEExpandLoadPdiPass>();
642}
constexpr llvm::StringLiteral initEntrySequenceName
Name of the runtime sequence aie-expand-load-pdi{init-entry} adds.
Definition AIEXPasses.h:100
mlir::Operation * findShimDmaStart(AIE::DeviceOp device)
std::unique_ptr< mlir::OperationPass< mlir::ModuleOp > > createAIEExpandLoadPdiPass()
mlir::LogicalResult insertConfigOps(llvm::ArrayRef< std::pair< mlir::Operation *, xilinx::AIE::DeviceOp > > sites, AIEToConfigurationOutputType outputType)
mlir::LogicalResult inlineWriteConfigs(mlir::ModuleOp module)
struct TileID { friend std::ostream &operator<<(std::ostream &os, const TileID &s) { os<< "TileID("<< s.col<< ", "<< s.row<< ")"; return os; } friend std::string to_string(const TileID &s) { std::ostringstream ss; ss<< s; return ss.str(); } friend llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const TileID &s) { os<< to_string(s); return os; } inline bool operator<(const TileID &rhs) const { return std::tie(col, row)< std::tie(rhs.col, rhs.row); } bool operator==(const TileID &rhs) const { return std::tie(col, row)==std::tie(rhs.col, rhs.row); } bool operator!=(const TileID &rhs) const { return !(*this==rhs);} int col, row;} TileID
struct Port { WireBundle bundle; int channel; bool operator==(const Port &rhs) const { return std::tie(bundle, channel)==std::tie(rhs.bundle, rhs.channel); } bool operator!=(const Port &rhs) const { return !(*this==rhs);} bool operator<(const Port &rhs) const { return std::tie(bundle, channel)< std::tie(rhs.bundle, rhs.channel); } friend std::ostream &operator<<(std::ostream &os, const Port &port) { os<< "("; os<< stringifyWireBundle(port.bundle).str(); os<< ": "<< std::to_string(port.channel)<< ")"; return os; } friend llvm::raw_ostream &operator<<(llvm::raw_ostream &os, const Port &port) { os<< to_string(port); return os; } } Port
Definition AIEDialect.h:265