MLIR-AIE 1.4.5.dev
AIEGenerateColumnControlOverlay.cpp
Go to the documentation of this file.
1//===- AIEGenerateColumnControlOverlay.cpp ----------------------*- C++ -*-===//
2//
3// Copyright (C) 2024 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
12
13#include "mlir/IR/Attributes.h"
14#include "mlir/Pass/Pass.h"
15
16#include "llvm/ADT/SetVector.h"
17#include "llvm/ADT/SmallSet.h"
18
19#include <map>
20#include <optional>
21#include <set>
22#include <tuple>
23
24namespace xilinx::AIE {
25#define GEN_PASS_DEF_AIEGENERATECOLUMNCONTROLOVERLAY
26#define GEN_PASS_DEF_AIEASSIGNTILECTRLIDS
27#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
28} // namespace xilinx::AIE
29
30#define DEBUG_TYPE "aie-generate-column-control-overlay"
31
32using namespace mlir;
33using namespace xilinx;
34using namespace xilinx::AIE;
35
36// Delegate to AIETargetModel::getTileToControllerIdMap.
37DenseMap<AIE::TileID, int>
38getTileToControllerIdMap(bool clColumnWiseUniqueIDs,
39 const AIETargetModel &targetModel) {
40 return targetModel.getTileToControllerIdMap(clColumnWiseUniqueIDs);
41}
42
43// AIE arch-specific row id to shim dma mm2s channel mapping. All shim mm2s
44// channels were assumed to be available for control packet flow routing (i.e.
45// not reserved by any aie.flow circuit-switched routing).
46DenseMap<int, int> getRowToShimChanMap(const AIETargetModel &targetModel,
47 WireBundle bundle) {
48 DenseMap<int, int> rowMap;
49 SmallVector<int>
50 thresholdsToNextShimChannel; // a list of thresholds on the number of
51 // control ports that the ith shim channel
52 // could connect to, before advancing to
53 // the next shim channel in round robin
54 TileID shimTile = {0, 0};
55 while (!targetModel.isShimNOCTile(shimTile.col, shimTile.row)) {
56 shimTile.col++;
57 if (shimTile.col == targetModel.columns()) {
58 shimTile.col = 0;
59 shimTile.row++;
60 }
61 assert(shimTile.col != targetModel.columns() ||
62 shimTile.row != targetModel.rows());
63 }
64
65 int numShimChans = targetModel.getNumSourceShimMuxConnections(
66 shimTile.col, shimTile.row, AIE::WireBundle::DMA);
67 for (int i = 1; i < numShimChans + 1; i++)
68 thresholdsToNextShimChannel.push_back(targetModel.rows() / numShimChans *
69 i);
70
71 if (bundle == WireBundle::DMA) { // Ctrl packets
72 int shimChanIdx = 0;
73 for (int r = 0; r < targetModel.rows(); r++) {
74 if (r >= thresholdsToNextShimChannel[shimChanIdx])
75 shimChanIdx++;
76 rowMap[r] = shimChanIdx;
77 }
78 } else if (bundle == WireBundle::South) { // TCT
79 for (int r = 0; r < targetModel.rows(); r++)
80 rowMap[r] = 0;
81 }
82
83 return rowMap;
84}
85
87 : xilinx::AIE::impl::AIEAssignTileCtrlIDsBase<AIEAssignTileCtrlIDsPass> {
88 void getDependentDialects(DialectRegistry &registry) const override {
89 registry.insert<AIEDialect>();
90 }
91 void runOnOperation() override {
92 DeviceOp device = getOperation();
93 const auto &targetModel = device.getTargetModel();
94
95 if (targetModel.getTargetArch() == AIEArch::AIE1)
96 return; // Disable this pass for AIE1; AIE1 support NYI.
97
98 // Collect all TileOps in columns occupied by the design.
99 llvm::MapVector<AIE::TileID, AIE::TileOp> tiles;
100 llvm::SmallSet<int, 1> occupiedCols;
101 for (auto tile : device.getOps<AIE::TileOp>()) {
102 int colIndex = tile.colIndex();
103 int rowIndex = tile.rowIndex();
104 tiles[{colIndex, rowIndex}] = tile;
105 occupiedCols.insert(colIndex);
106 }
107
108 auto tileIDMap =
109 getTileToControllerIdMap(clColumnWiseUniqueIDs, targetModel);
110 for (int col : occupiedCols) {
111 SmallVector<AIE::TileOp> tilesOnCol;
112 for (auto &[tId, tOp] : tiles) {
113 if (tId.col != col)
114 continue;
115 tilesOnCol.push_back(tOp);
116 }
117
118 for (auto tOp : tilesOnCol) {
119 if (tOp->hasAttr("controller_id"))
120 continue;
121 auto pktInfoAttr = AIE::PacketInfoAttr::get(
122 tOp->getContext(), /*pkt_type*/ 0,
123 /*pkt_id*/ tileIDMap[{tOp.colIndex(), tOp.rowIndex()}]);
124 tOp->setAttr("controller_id", pktInfoAttr);
125 }
126 }
127 }
128};
129
132 AIEGenerateColumnControlOverlayPass> {
133 using Base::Base;
134
135 void getDependentDialects(DialectRegistry &registry) const override {
136 registry.insert<AIEDialect>();
137 registry.insert<memref::MemRefDialect>();
138 }
139 void runOnOperation() override {
140 ModuleOp module = getOperation();
141 OpBuilder builder(module.getContext());
142
143 // Gather source devices in module order. Skip a previously-generated
144 // overlay device so the pass is idempotent on its own output.
145 SmallVector<DeviceOp> sourceDevices;
146 for (auto dev : module.getOps<DeviceOp>()) {
147 if (dev.getSymName() == "ctrl_pkt_overlay")
148 continue;
149 sourceDevices.push_back(dev);
150 }
151
152 // Devices that receive the overlay: those that have not opted out via
153 // `needs_ctrl_pkt_overlay = false`.
154 SmallVector<DeviceOp> participating;
155 for (auto dev : sourceDevices) {
156 if (deviceOptedOut(dev)) {
157 if (clEmitStandaloneOverlay) {
158 dev.setHasCtrlPktOverlay(false);
159 }
160 continue;
161 }
162 // A device that already carries a control overlay must not receive a
163 // second one.
164 if (deviceHasControlOverlay(dev)) {
165 if (clEmitStandaloneOverlay) {
166 dev.setHasCtrlPktOverlay(true);
167 }
168 continue;
169 }
170 participating.push_back(dev);
171 }
172
173 // A standalone `@ctrl_pkt_overlay` device references a single overlay
174 // shape, so every participating device must expose the same set of tiles
175 // for that shape to be identical across them.
176 // The same holds for the tiles whose task-complete tokens need a route.
177 llvm::SmallSet<AIE::TileID, 4> sharedTokenTiles;
178 if (clEmitStandaloneOverlay) {
179 shareTilesAcrossDevices(participating);
180 sharedTokenTiles = collectTokenIssuingTiles(participating);
181 }
182
183 // Apply the overlay in-place to participating devices.
184 for (auto dev : participating) {
185 llvm::SmallSet<AIE::TileID, 4> tokenTiles =
186 clEmitStandaloneOverlay ? sharedTokenTiles
188 if (failed(applyOverlayToDevice(dev, tokenTiles)))
189 return signalPassFailure();
190 if (clEmitStandaloneOverlay)
191 dev.setHasCtrlPktOverlay(true);
192 }
193
194 // Emit standalone `@ctrl_pkt_overlay` device.
195 if (clEmitStandaloneOverlay) {
196 if (failed(createOverlayDevice(module, builder, participating,
197 sharedTokenTiles))) {
198 return signalPassFailure();
199 }
200 }
201 }
202
203 // Collect the union of tiles across `devices`, recording one prototype
204 // TileOp per tile so its attributes can be copied when the tile is cloned.
205 static void
206 collectTileUnion(ArrayRef<DeviceOp> devices,
207 llvm::SmallSetVector<AIE::TileID, 8> &unionTiles,
208 llvm::DenseMap<AIE::TileID, AIE::TileOp> &prototypeTile) {
209 for (auto dev : devices)
210 for (auto tOp : dev.getOps<AIE::TileOp>()) {
211 AIE::TileID id{tOp.colIndex(), tOp.rowIndex()};
212 unionTiles.insert(id);
213 if (!prototypeTile.contains(id))
214 prototypeTile[id] = tOp;
215 }
216 }
217
218 // Clone every tile in `unionTiles` not already present in `device`, copying
219 // the prototype's attributes so downstream passes that compare attribute
220 // dictionaries (e.g. AIEMaterializeRuntimeSequences) match.
221 static void cloneMissingTiles(
222 DeviceOp device, const llvm::SmallSetVector<AIE::TileID, 8> &unionTiles,
223 const llvm::DenseMap<AIE::TileID, AIE::TileOp> &prototypeTile) {
224 llvm::SmallSet<AIE::TileID, 8> existing;
225 for (auto tOp : device.getOps<AIE::TileOp>())
226 existing.insert({tOp.colIndex(), tOp.rowIndex()});
227 OpBuilder b = OpBuilder::atBlockBegin(device.getBody());
228 for (auto id : unionTiles) {
229 if (existing.contains(id))
230 continue;
231 b.clone(*prototypeTile.lookup(id).getOperation());
232 }
233 }
234
235 // Give every device in `devices` the union of their tiles, so an overlay
236 // routed onto any of them has the same shape (routes, shim_dma_allocations).
237 static void shareTilesAcrossDevices(ArrayRef<DeviceOp> devices) {
238 llvm::SmallSetVector<AIE::TileID, 8> unionTiles;
239 llvm::DenseMap<AIE::TileID, AIE::TileOp> prototypeTile;
240 collectTileUnion(devices, unionTiles, prototypeTile);
241 for (auto dev : devices)
242 cloneMissingTiles(dev, unionTiles, prototypeTile);
243 }
244
245 // Emit a standalone `@ctrl_pkt_overlay` device holding only the overlay and
246 // the union of tiles it references. Downstream consumers compile it on its
247 // own to ship a reconfigure-only PDI.
248 LogicalResult
249 createOverlayDevice(ModuleOp module, OpBuilder &builder,
250 ArrayRef<DeviceOp> participating,
251 const llvm::SmallSet<AIE::TileID, 4> &tokenTiles) {
252 if (participating.empty())
253 return success();
254
255 // All participating devices must share the same target.
256 DeviceOp firstDev = participating.front();
257 auto refDevice = firstDev.getDevice();
258 for (auto dev : llvm::drop_begin(participating)) {
259 if (dev.getDevice() != refDevice) {
260 return dev->emitOpError(
261 "cannot generate a single standalone ctrl_pkt_overlay device: "
262 "participating devices have mismatched target architectures.");
263 }
264 }
265
266 if (module.lookupSymbol("ctrl_pkt_overlay")) {
267 return module.emitOpError(
268 "a symbol named `ctrl_pkt_overlay` already exists in the module; "
269 "cannot create a standalone ctrl_pkt_overlay device.");
270 }
271
272 builder.setInsertionPointToEnd(module.getBody());
273 Location loc = firstDev.getLoc();
274 auto overlayDevice = AIE::DeviceOp::create(
275 builder, loc, refDevice, builder.getStringAttr("ctrl_pkt_overlay"));
276 overlayDevice.getRegion().emplaceBlock();
277 builder.setInsertionPointToEnd(&overlayDevice.getRegion().front());
278 AIE::EndOp::create(builder, loc);
279
280 // Populate the overlay device with the union of tiles referenced across
281 // participating devices, then route the overlay onto it.
282 llvm::SmallSetVector<AIE::TileID, 8> unionTiles;
283 llvm::DenseMap<AIE::TileID, AIE::TileOp> prototypeTile;
284 collectTileUnion(participating, unionTiles, prototypeTile);
285 cloneMissingTiles(overlayDevice, unionTiles, prototypeTile);
286
287 if (failed(applyOverlayToDevice(overlayDevice, tokenTiles)))
288 return failure();
289
290 overlayDevice.setHasCtrlPktOverlay(true);
291 return success();
292 }
293
294 // Apply the column-control overlay to `device` in place, routing the
295 // TileControl port of every tile in `tokenTiles` back to its shim. Returns
296 // failure on a routing conflict.
297 LogicalResult
298 applyOverlayToDevice(DeviceOp device,
299 const llvm::SmallSet<AIE::TileID, 4> &tokenTiles) {
300 const auto &targetModel = device.getTargetModel();
301 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
302
303 if (targetModel.getTargetArch() == AIEArch::AIE1)
304 return success(); // Disable this pass for AIE1; AIE1 support NYI.
305
306 // Collect existing TileOps
307 llvm::MapVector<AIE::TileID, AIE::TileOp> tiles;
308 for (auto tile : device.getOps<AIE::TileOp>())
309 tiles[{tile.colIndex(), tile.rowIndex()}] = tile;
310 if (tiles.empty())
311 return success();
312
313 int minOccupiedCol = tiles.front().first.col;
314 int maxOccupiedCol = minOccupiedCol;
315 int maxOccupiedRow = 0;
316 llvm::SmallSet<int, 4> declaredCols;
317 for (auto &[tId, tOp] : tiles) {
318 minOccupiedCol = std::min(minOccupiedCol, tId.col);
319 maxOccupiedCol = std::max(maxOccupiedCol, tId.col);
320 maxOccupiedRow = std::max(maxOccupiedRow, tId.row);
321 declaredCols.insert(tId.col);
322 }
323
324 // Both widenings below are scoped to the control-packet configuration path
325 // (`route-shim-to-tile-ctrl`); do not add tile declarations if the control
326 // overlay was not requested, as to not congest routing needlessly.
327 SmallVector<int> colsToCover;
328 if (clRouteShimDmaToTileCTRL) {
329 // Cover the full column range between the leftmost and rightmost occupied
330 // column, not just the occupied columns. This is required so that a shim
331 // DMA allocation is later emitted for the intermediate columns that a
332 // flow will route through.
333 for (int col = minOccupiedCol; col <= maxOccupiedCol; col++)
334 colsToCover.push_back(col);
335 } else {
336 for (auto &[tId, tOp] : tiles)
337 if (!llvm::is_contained(colsToCover, tId.col))
338 colsToCover.push_back(tId.col);
339 }
340
341 auto tileIDMap = getTileToControllerIdMap(true, targetModel);
342 for (int col : colsToCover) {
343 builder.setInsertionPointToStart(device.getBody());
344 AIE::TileOp shimTile = TileOp::getOrCreate(builder, device, col, 0);
345 if (clRouteShimDmaToTileCTRL)
346 tiles[{col, 0}] = shimTile;
347
348 if (clRouteShimCTRLToTCT == "all-tiles" ||
349 clRouteShimCTRLToTCT == "shim-only") {
350 // Get all tile ops on column col
351 SmallVector<AIE::TileOp> tilesOnCol;
352 for (auto &[tId, tOp] : tiles) {
353 if (tId.col != col)
354 continue;
355 if (clRouteShimCTRLToTCT == "shim-only" && !tOp.isShimNOCorPLTile() &&
356 !tokenTiles.contains(tId))
357 continue;
358 // A routed response only covers the tile it comes from.
359 if (hasRoutedResponse(device, tOp, tileIDMap))
360 continue;
361 tilesOnCol.push_back(tOp);
362 }
363
364 if (!tilesOnCol.empty() &&
365 failed(generatePacketFlowsForControl(
366 builder, device, shimTile, AIE::WireBundle::South, tilesOnCol,
367 AIE::WireBundle::TileControl, 0, tileIDMap, false)))
368 return failure();
369 }
370 if (clRouteShimDmaToTileCTRL) {
371 // Ensure tiles exist for the full range from shim (row 0) to the
372 // highest existing tile in the column. Intermediate tiles (e.g. mem
373 // tiles) are needed for control packet routing and will also need
374 // their switchboxes configured via control packets.
375 int maxRow = 0;
376 for (auto &[tId, tOp] : tiles) {
377 if (tId.col == col)
378 maxRow = std::max(maxRow, tId.row);
379 }
380 // A column the design declared no tile in is only in range because
381 // flows route through it, and such a flow can traverse it at any row up
382 // to the highest row in use. getRowToShimChanMap splits the rows into
383 // one contiguous range per shim channel, so covering only the shim row
384 // here would allocate just one of the channels its packets get
385 // addressed to. Test against the columns the design itself declared,
386 // not against `tiles`, which now also holds the shim materialized just
387 // above.
388 if (!declaredCols.contains(col))
389 maxRow = maxOccupiedRow;
390 SmallVector<AIE::TileOp> tilesOnCol;
391 for (int row = 0; row <= maxRow; row++) {
392 auto tOp = TileOp::getOrCreate(builder, device, col, row);
393 tilesOnCol.push_back(tOp);
394 }
395
396 if (failed(generatePacketFlowsForControl(
397 builder, device, shimTile, AIE::WireBundle::DMA, tilesOnCol,
398 AIE::WireBundle::TileControl, 0, tileIDMap, true)))
399 return failure();
400 }
401 }
402 return success();
403 }
404
405 // Tiles a runtime sequence asks a task-complete token of. The token leaves
406 // the tile through its TileControl port, so without a route from there to
407 // the shim an await on it never returns.
408 static llvm::SmallSet<AIE::TileID, 4>
409 collectTokenIssuingTiles(ArrayRef<DeviceOp> devices) {
410 llvm::SmallSet<AIE::TileID, 4> tiles;
411 auto addTile = [&](Value tileValue) {
412 if (!tileValue)
413 return;
414 if (auto tile = dyn_cast_or_null<TileOp>(tileValue.getDefiningOp()))
415 tiles.insert({tile.colIndex(), tile.rowIndex()});
416 };
417 for (DeviceOp device : devices)
418 device.walk([&](Operation *op) {
419 if (auto task = dyn_cast<AIEX::DMAConfigureTaskOp>(op)) {
420 if (task.getIssueToken())
421 addTile(task.getTile());
422 } else if (auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(op)) {
423 if (!task.getIssueToken())
424 return;
425 if (auto endpoint = dyn_cast_or_null<RouteEndpointOp>(
426 SymbolTable::lookupSymbolIn(device, task.getAlloc())))
427 addTile(endpoint.getTile());
428 } else if (auto push = dyn_cast<AIEX::NpuPushQueueOp>(op)) {
429 if (push.getIssueToken())
430 tiles.insert({static_cast<int>(push.getColumn()),
431 static_cast<int>(push.getRow())});
432 }
433 });
434 return tiles;
435 }
436
437 // Return true when the user has explicitly disabled overlay generation for
438 // this device via `needs_ctrl_pkt_overlay = false`.
439 static bool deviceOptedOut(DeviceOp device) {
440 auto attr = device->getAttrOfType<BoolAttr>("needs_ctrl_pkt_overlay");
441 return attr && !attr.getValue();
442 }
443
444 // A one-source, one-destination packet flow reduced to a comparable key:
445 // the packet ID plus both endpoints as (col, row, bundle, channel). This is
446 // the shape every control flow below has, and keying on coordinates rather
447 // than on the defining op is what makes it match DeviceOp::verify's notion
448 // of a duplicate.
449 using CtrlFlowKey = std::tuple<int, int, int, int, int, int, int, int, int>;
450
451 // Nullopt when either endpoint's coordinates are not known yet -- an
452 // aie.logical_tile that --aie-place-tiles has not placed. Such a flow cannot
453 // be told apart from the one about to be created, and the verifier skips it
454 // for the same reason. A pinned aie.logical_tile does have coordinates, so
455 // this goes through TileLike rather than TileOp to catch it.
456 static std::optional<CtrlFlowKey>
457 tryGetCtrlFlowKey(int id, Value srcTileValue, WireBundle srcBundle,
458 int srcChan, Value destTileValue, WireBundle destBundle,
459 int destChan) {
460 auto srcTile = dyn_cast_or_null<TileLike>(srcTileValue.getDefiningOp());
461 auto destTile = dyn_cast_or_null<TileLike>(destTileValue.getDefiningOp());
462 if (!srcTile || !destTile)
463 return std::nullopt;
464 std::optional<int> srcCol = srcTile.tryGetCol();
465 std::optional<int> srcRow = srcTile.tryGetRow();
466 std::optional<int> destCol = destTile.tryGetCol();
467 std::optional<int> destRow = destTile.tryGetRow();
468 if (!srcCol || !srcRow || !destCol || !destRow)
469 return std::nullopt;
470 return CtrlFlowKey{
471 id, *srcCol, *srcRow, static_cast<int>(srcBundle),
472 srcChan, *destCol, *destRow, static_cast<int>(destBundle),
473 destChan};
474 }
475
476 // The packet flows `device` already declares in the shape this pass emits,
477 // keyed so one can be recognised before it is created a second time. A flow
478 // with more than one source or destination is a different shape and is left
479 // out.
480 static std::map<CtrlFlowKey, AIE::PacketFlowOp>
481 collectExistingCtrlFlows(DeviceOp device) {
482 std::map<CtrlFlowKey, AIE::PacketFlowOp> flows;
483 for (auto flow : device.getOps<AIE::PacketFlowOp>()) {
484 auto sources = flow.getOps<AIE::PacketSourceOp>();
485 auto dests = flow.getOps<AIE::PacketDestOp>();
486 if (!llvm::hasSingleElement(sources) || !llvm::hasSingleElement(dests))
487 continue;
488 AIE::PacketSourceOp src = *sources.begin();
489 AIE::PacketDestOp dest = *dests.begin();
490 std::optional<CtrlFlowKey> key = tryGetCtrlFlowKey(
491 flow.IDInt(), src.getTile(), src.getBundle(), src.channelIndex(),
492 dest.getTile(), dest.getBundle(), dest.channelIndex());
493 if (key)
494 flows.try_emplace(*key, flow);
495 }
496 return flows;
497 }
498
499 // The packet ID of control packets to and from `tile`.
500 static int getControllerID(TileOp tile, DenseMap<TileID, int> &tileIDMap) {
501 if (auto id = tile->getAttrOfType<AIE::PacketInfoAttr>("controller_id"))
502 return id.assignedId();
503 return tileIDMap[{tile.colIndex(), tile.rowIndex()}];
504 }
505
506 // Name of the shim DMA allocation the runtime sequence issues or receives
507 // control packets through.
508 static std::string getCtrlPktAllocName(int col, bool isShimMM2S, int chan) {
509 std::string name = "ctrlpkt";
510 name += "_col" + std::to_string(col);
511 name += isShimMM2S ? "_mm2s" : "_s2mm";
512 name += "_chan" + std::to_string(chan);
513 return name;
514 }
515
516 // True when packets with ID `pktID` entering the switchbox of `from` on any
517 // of `startPorts` are routed to `toPort` of `to`'s switchbox, through a
518 // connect or master marked is_ctrl_pkt_overlay. The ID is traced through the
519 // routed switchboxes rather than inferred from the final rule.
520 static bool hasRoutedPacket(DeviceOp device, int pktID, TileID from,
521 ArrayRef<Port> startPorts, TileID to,
522 Port toPort) {
523 AIEDialect::IsCtrlPktOverlayAttrHelper overlay(device.getContext());
524 DenseMap<TileID, AIE::SwitchboxOp> switchboxes;
525 for (auto switchbox : device.getOps<AIE::SwitchboxOp>()) {
526 auto tile = dyn_cast<TileLike>(switchbox.getTile().getDefiningOp());
527 if (!tile)
528 continue;
529 std::optional<int> c = tile.tryGetCol(), r = tile.tryGetRow();
530 if (c && r)
531 switchboxes[{*c, *r}] = switchbox;
532 }
533
534 SmallVector<std::pair<TileID, Port>> worklist;
535 for (Port port : startPorts)
536 worklist.push_back({from, port});
537 std::set<std::tuple<int, int, int, int>> visited;
538 while (!worklist.empty()) {
539 auto [tileID, port] = worklist.pop_back_val();
540 if (!visited
541 .insert({tileID.col, tileID.row, static_cast<int>(port.bundle),
542 port.channel})
543 .second)
544 continue;
545 auto switchbox = switchboxes.lookup(tileID);
546 if (!switchbox)
547 continue;
548 Block &connections = switchbox.getConnections().front();
549
550 SmallVector<std::pair<Port, Operation *>> outputs;
551 for (auto connect : connections.getOps<AIE::ConnectOp>())
552 if (connect.sourcePort() == port)
553 outputs.push_back({connect.destPort(), connect});
554 // The first rule matching the packet ID picks the master. Pathfinder
555 // marks a slave's packet_rules only from the first group it routes, so
556 // the marker on the rules is not reliable.
557 for (auto rules : connections.getOps<AIE::PacketRulesOp>()) {
558 if (rules.sourcePort() != port)
559 continue;
560 for (auto rule : rules.getRules().front().getOps<AIE::PacketRuleOp>()) {
561 if ((pktID & rule.maskInt()) != rule.valueInt())
562 continue;
563 for (auto master : connections.getOps<AIE::MasterSetOp>())
564 if (llvm::is_contained(master.getAmsels(), rule.getAmsel()))
565 outputs.push_back({master.destPort(), master});
566 break;
567 }
568 }
569
570 for (auto [dest, op] : outputs) {
571 if (tileID == to && dest == toPort) {
572 if (overlay.isAttrPresent(op))
573 return true;
574 continue;
575 }
576 auto next =
577 getConnectingWireEndpoint(device.getTargetModel(), tileID, dest);
578 if (!next)
579 continue;
580 worklist.push_back(*next);
581 }
582 }
583 return false;
584 }
585
586 // A routed response must actually connect TileControl:0 of `src` to South:0
587 // on its column's shim. A priority request ending at TileControl is not a
588 // response, even though pathfinder marks it is_ctrl_pkt_overlay.
589 static bool hasRoutedResponse(DeviceOp device, TileOp src,
590 DenseMap<TileID, int> &tileIDMap) {
591 int col = src.colIndex();
592 return hasRoutedPacket(device, getControllerID(src, tileIDMap),
593 {col, src.rowIndex()},
594 {Port{WireBundle::TileControl, 0}}, {col, 0},
595 Port{WireBundle::South, 0});
596 }
597
598 // A routed request must connect DMA:`shimChan` of `shimTile` to
599 // TileControl:0 of `dest`. Pathfinder moves the shim DMA behind the shim
600 // mux, so the request enters the shim's switchbox on the South port the mux
601 // connects that DMA channel to.
602 static bool hasRoutedRequest(DeviceOp device, TileOp shimTile, int shimChan,
603 TileOp dest, DenseMap<TileID, int> &tileIDMap) {
604 SmallVector<Port> startPorts = {Port{WireBundle::DMA, shimChan}};
605 for (auto shimMux : device.getOps<AIE::ShimMuxOp>()) {
606 if (shimMux.getTile() != shimTile.getResult())
607 continue;
608 for (auto connect :
609 shimMux.getConnections().front().getOps<AIE::ConnectOp>())
610 if (connect.sourcePort() == Port{WireBundle::DMA, shimChan} &&
611 connect.destPort().bundle == WireBundle::North)
612 startPorts.push_back(
613 Port{WireBundle::South, connect.destPort().channel});
614 }
615 return hasRoutedPacket(device, getControllerID(dest, tileIDMap),
616 {shimTile.colIndex(), shimTile.rowIndex()},
617 startPorts, {dest.colIndex(), dest.rowIndex()},
618 Port{WireBundle::TileControl, 0});
619 }
620
621 // True when `tile` is used only by its routing, and its switchbox only
622 // passes streams between neighbouring switchboxes: none starts or ends at
623 // the tile itself.
624 static bool isRoutedThroughOnly(TileOp tile) {
625 auto isNeighbour = [](Port port) {
626 return port.bundle == WireBundle::North ||
627 port.bundle == WireBundle::South ||
628 port.bundle == WireBundle::East || port.bundle == WireBundle::West;
629 };
630 bool hasSwitchbox = false;
631 for (Operation *user : tile->getUsers()) {
632 if (isa<AIE::WireOp>(user))
633 continue;
634 auto switchbox = dyn_cast<AIE::SwitchboxOp>(user);
635 if (!switchbox)
636 return false;
637 hasSwitchbox = true;
638 for (Operation &op : switchbox.getConnections().front()) {
639 if (auto connect = dyn_cast<AIE::ConnectOp>(op)) {
640 if (!isNeighbour(connect.sourcePort()) ||
641 !isNeighbour(connect.destPort()))
642 return false;
643 } else if (auto rules = dyn_cast<AIE::PacketRulesOp>(op)) {
644 if (!isNeighbour(rules.sourcePort()))
645 return false;
646 } else if (auto master = dyn_cast<AIE::MasterSetOp>(op)) {
647 if (!isNeighbour(master.destPort()))
648 return false;
649 }
650 }
651 }
652 return hasSwitchbox;
653 }
654
655 // Every occupied column needs its own shim response route. An overlay
656 // routed on only one column must not suppress the remaining columns. With
657 // route-shim-to-tct=all-tiles, every tile also needs its own response route,
658 // except a tile that is only routed through. Pathfinder declares such a tile
659 // when a flow crosses it, and a first run of this pass never covered it.
660 bool hasRoutedResponses(DeviceOp device, DenseMap<TileID, int> &tileIDMap) {
661 llvm::SmallSet<int, 4> cols;
662 llvm::DenseMap<int, TileOp> shims;
663 for (auto tile : device.getOps<AIE::TileOp>()) {
664 cols.insert(tile.colIndex());
665 if (tile.rowIndex() == 0)
666 shims[tile.colIndex()] = tile;
667 }
668 if (cols.empty())
669 return false;
670 if (!llvm::all_of(cols, [&](int col) {
671 auto it = shims.find(col);
672 return it != shims.end() &&
673 hasRoutedResponse(device, it->second, tileIDMap);
674 }))
675 return false;
676 if (clRouteShimCTRLToTCT != "all-tiles")
677 return true;
678 return llvm::all_of(device.getOps<AIE::TileOp>(), [&](TileOp tile) {
679 return isRoutedThroughOnly(tile) ||
680 hasRoutedResponse(device, tile, tileIDMap);
681 });
682 }
683
684 // With route-shim-to-tile-ctrl, every tile applyOverlayToDevice sends
685 // requests to needs a routed request from its shim DMA channel, and that
686 // channel needs its shim DMA allocation. Tiles only routed through do not
687 // widen the columns and rows covered, as a first run never covered them.
688 static bool hasRoutedRequests(DeviceOp device,
689 DenseMap<TileID, int> &tileIDMap) {
690 llvm::DenseMap<TileID, TileOp> tiles;
691 llvm::DenseMap<int, int> maxRowOfCol;
692 std::optional<int> minCol, maxCol;
693 int maxOccupiedRow = 0;
694 for (auto tile : device.getOps<AIE::TileOp>()) {
695 int col = tile.colIndex(), row = tile.rowIndex();
696 tiles[{col, row}] = tile;
697 if (isRoutedThroughOnly(tile))
698 continue;
699 minCol = std::min(minCol.value_or(col), col);
700 maxCol = std::max(maxCol.value_or(col), col);
701 maxOccupiedRow = std::max(maxOccupiedRow, row);
702 maxRowOfCol[col] = std::max(maxRowOfCol.lookup(col), row);
703 }
704 if (!minCol || !maxCol)
705 return false;
706 auto rowToShimChanMap =
707 getRowToShimChanMap(device.getTargetModel(), WireBundle::DMA);
708 for (int col = *minCol; col <= *maxCol; col++) {
709 TileOp shimTile = tiles.lookup({col, 0});
710 if (!shimTile)
711 return false;
712 auto it = maxRowOfCol.find(col);
713 int maxRow = it != maxRowOfCol.end() ? it->second : maxOccupiedRow;
714 for (int row = 0; row <= maxRow; row++) {
715 TileOp tile = tiles.lookup({col, row});
716 int chan = rowToShimChanMap[row];
717 if (!tile ||
718 !device.lookupSymbol(getCtrlPktAllocName(col, true, chan)) ||
719 !hasRoutedRequest(device, shimTile, chan, tile, tileIDMap))
720 return false;
721 }
722 }
723 return true;
724 }
725
726 // True when `device` already carries every route this pass would add.
727 // Responses alone do not complete an overlay that also routes requests.
728 // Responses are only skipped when route-shim-to-tct=disable leaves requests
729 // as the sole routes requested; with neither requested, they still decide.
730 bool deviceHasControlOverlay(DeviceOp device) {
731 auto tileIDMap = getTileToControllerIdMap(true, device.getTargetModel());
732 if ((clRouteShimCTRLToTCT != "disable" || !clRouteShimDmaToTileCTRL) &&
733 !hasRoutedResponses(device, tileIDMap))
734 return false;
735 return !clRouteShimDmaToTileCTRL || hasRoutedRequests(device, tileIDMap);
736 }
737
738 AIE::PacketFlowOp createPacketFlowOp(OpBuilder &builder, Location loc,
739 int &flowID, Value source,
740 xilinx::AIE::WireBundle sourceBundle,
741 uint32_t sourceChannel, Value dest,
742 xilinx::AIE::WireBundle destBundle,
743 uint32_t destChannel,
744 mlir::BoolAttr keep_pkt_header = nullptr,
745 mlir::BoolAttr ctrl_pkt_flow = nullptr) {
746 OpBuilder::InsertionGuard guard(builder);
747
748 AIE::PacketFlowOp pktFlow = AIE::PacketFlowOp::create(
749 builder, loc, flowID++, keep_pkt_header, ctrl_pkt_flow);
750 Region &r_pktFlow = pktFlow.getPorts();
751 Block *b_pktFlow = builder.createBlock(&r_pktFlow);
752 builder.setInsertionPointToStart(b_pktFlow);
753 AIE::PacketSourceOp::create(builder, loc, source, sourceBundle,
754 sourceChannel);
755 AIE::PacketDestOp::create(builder, loc, dest, destBundle, destChannel);
756 AIE::EndOp::create(builder, loc);
757 return pktFlow;
758 }
759
760 // Get a vector of shim channels not reserved by any circuit-switched aie.flow
761 // op
762 SmallVector<int> getAvailableShimChans(DeviceOp device, TileOp shimTile,
763 WireBundle shimWireBundle,
764 bool isShimMM2S) {
765 SmallVector<int> availableShimChans;
766 DenseMap<int, AIE::FlowOp> flowOpUsers;
767 const auto &targetModel = device.getTargetModel();
768
769 for (auto *user : shimTile.getResult().getUsers()) {
770 auto fOp = dyn_cast<AIE::FlowOp>(user);
771 if (!fOp)
772 continue;
773 if (isShimMM2S && fOp.getSource() == shimTile &&
774 fOp.getSourceBundle() == shimWireBundle)
775 flowOpUsers[fOp.getSourceChannel()] = fOp;
776 else if (!isShimMM2S && fOp.getDest() == shimTile &&
777 fOp.getDestBundle() == shimWireBundle)
778 flowOpUsers[fOp.getDestChannel()] = fOp;
779 }
780 int numShimChans = 0;
781 if (isShimMM2S)
782 numShimChans = targetModel.getNumSourceShimMuxConnections(
783 shimTile.colIndex(), shimTile.rowIndex(), shimWireBundle);
784 else
785 numShimChans = targetModel.getNumDestShimMuxConnections(
786 shimTile.colIndex(), shimTile.rowIndex(), shimWireBundle);
787 for (int i = 0; i < numShimChans; i++) {
788 if (!flowOpUsers.count(i))
789 availableShimChans.push_back(i);
790 }
791
792 return availableShimChans;
793 }
794
795 // Create packet flows per col which moves control packets to and from shim
796 // dma
797 LogicalResult generatePacketFlowsForControl(
798 OpBuilder builder, DeviceOp device, TileOp shimTile,
799 WireBundle shimWireBundle, const SmallVector<AIE::TileOp> &ctrlTiles,
800 WireBundle ctrlWireBundle, int coreOrMemChanId,
801 DenseMap<TileID, int> tileIDMap, bool isShimMM2S) {
802 int ctrlPktFlowID = 0;
803 auto rowToShimChanMap =
804 getRowToShimChanMap(device.getTargetModel(), shimWireBundle);
805 // Get all available shim channels, to verify that the one being used is
806 // available
807 auto availableShimChans =
808 getAvailableShimChans(device, shimTile, shimWireBundle, isShimMM2S);
809 // The overlay a device already carries -- from a user-written control flow,
810 // or from an earlier run of this pass, which aiecc does whenever the input
811 // was already overlaid -- must not be laid down a second time. A flow
812 // declared twice is rejected by DeviceOp::verify.
813 std::map<CtrlFlowKey, AIE::PacketFlowOp> existingCtrlFlows =
814 collectExistingCtrlFlows(device);
815
816 builder.setInsertionPoint(device.getBody()->getTerminator());
817 for (auto tOp : ctrlTiles) {
818 if (tOp->hasAttr("controller_id"))
819 ctrlPktFlowID =
820 (int)tOp->getAttrOfType<AIE::PacketInfoAttr>("controller_id")
821 .assignedId();
822 else
823 ctrlPktFlowID = tileIDMap[{tOp.colIndex(), tOp.rowIndex()}];
824 // Check shim channel availability
825 if (!llvm::is_contained(availableShimChans,
826 rowToShimChanMap[tOp.rowIndex()])) {
827 device->emitOpError(
828 "failed to generate column control overlay from shim dma to tile "
829 "ctrl ports, because some shim mm2s dma channels were reserved "
830 "from routing control packets.");
831 return failure();
832 }
833
834 auto keep_pkt_header = builder.getBoolAttr(true);
835 auto ctrl_pkt_flow = builder.getBoolAttr(true);
836 int shimChan = rowToShimChanMap[tOp.rowIndex()];
837 std::optional<CtrlFlowKey> key =
838 isShimMM2S ? tryGetCtrlFlowKey(ctrlPktFlowID, shimTile,
839 shimWireBundle, shimChan, tOp,
840 ctrlWireBundle, coreOrMemChanId)
841 : tryGetCtrlFlowKey(ctrlPktFlowID, tOp, ctrlWireBundle,
842 coreOrMemChanId, shimTile,
843 shimWireBundle, shimChan);
844 // Only the flow itself is affected by this; the shim DMA allocation
845 // below is emitted either way, guarded by its own name lookup.
846 auto it = key ? existingCtrlFlows.find(*key) : existingCtrlFlows.end();
847 if (it != existingCtrlFlows.end()) {
848 // The device already declares this control flow -- written by hand, or
849 // laid down by an earlier run of this pass over the same input, which
850 // aiecc does whenever its input was already overlaid. Declaring it a
851 // second time is the duplicate DeviceOp::verify rejects, so adopt the
852 // overlay's attributes onto the flow that is already there instead.
853 // They are what tells --aie-create-pathfinder-flows this is a control
854 // flow; that pass takes the last writer per destination port, so
855 // before this pass deduplicated, the copy it appended is what set
856 // them. Dropping them here would silently reroute the switchbox.
857 it->second.setKeepPktHeader(keep_pkt_header.getValue());
858 it->second.setPriorityRoute(ctrl_pkt_flow.getValue());
859 } else if (!isShimMM2S || !hasRoutedRequest(device, shimTile, shimChan,
860 tOp, tileIDMap)) {
861 // A request the switchboxes already route is not declared again; its
862 // shim DMA allocation below is still emitted.
863 AIE::PacketFlowOp created =
864 isShimMM2S
865 ? createPacketFlowOp(builder, tOp.getLoc(), ctrlPktFlowID,
866 shimTile, shimWireBundle, shimChan, tOp,
867 ctrlWireBundle, coreOrMemChanId,
868 keep_pkt_header, ctrl_pkt_flow)
869 : createPacketFlowOp(builder, tOp.getLoc(), ctrlPktFlowID, tOp,
870 ctrlWireBundle, coreOrMemChanId, shimTile,
871 shimWireBundle, shimChan, keep_pkt_header,
872 ctrl_pkt_flow);
873 // Both endpoints are placed tiles here, so the key is always present;
874 // guarded rather than asserted so an unplaced one just means no
875 // deduplication, never a dropped flow.
876 if (key)
877 existingCtrlFlows.try_emplace(*key, created);
878 }
879
880 // Generate shim dma alloc ops as handle for runtime sequence to pickup,
881 // when issuing control packets
882 if (shimWireBundle != WireBundle::DMA)
883 continue;
884
885 AIE::DMAChannelDir dir =
886 isShimMM2S ? AIE::DMAChannelDir::MM2S : AIE::DMAChannelDir::S2MM;
887 int chan = rowToShimChanMap[tOp.rowIndex()];
888 std::string dma_name =
889 getCtrlPktAllocName(shimTile.colIndex(), isShimMM2S, chan);
890
891 // check to see if ShimDMAAllocationOp already exists
892 if (device.lookupSymbol(dma_name))
893 continue;
894
895 AIE::ShimDMAAllocationOp::create(
896 builder, tOp.getLoc(), StringRef(dma_name), shimTile.getResult(), dir,
897 rowToShimChanMap[tOp.rowIndex()], false, nullptr);
898 }
899 return success();
900 }
901
902 // Get packet-flow op with the same source or destination
903 AIE::PacketFlowOp getPktFlowWithSameSrcOrDst(DeviceOp device, TileOp srcTile,
904 WireBundle srcBundle,
905 int srcChan, TileOp destTile,
906 WireBundle destBundle,
907 int destChan) {
908 AIE::PacketFlowOp result = nullptr;
909 device.walk([&](AIE::PacketFlowOp fOp) {
910 for (auto srcOp : fOp.getOps<AIE::PacketSourceOp>()) {
911 if (srcOp.getTile() == srcTile && srcOp.getBundle() == srcBundle &&
912 srcOp.channelIndex() == srcChan) {
913 result = fOp;
914 return;
915 }
916 }
917 for (auto destOp : fOp.getOps<AIE::PacketDestOp>()) {
918 if (destOp.getTile() == destTile && destOp.getBundle() == destBundle &&
919 destOp.channelIndex() == destChan) {
920 result = fOp;
921 return;
922 }
923 }
924 });
925 return result;
926 }
927};
928
929std::unique_ptr<OperationPass<DeviceOp>> AIE::createAIEAssignTileCtrlIDsPass() {
930 return std::make_unique<AIEAssignTileCtrlIDsPass>();
931}
932
933std::unique_ptr<OperationPass<mlir::ModuleOp>>
934AIE::createAIEGenerateColumnControlOverlayPass() {
935 return std::make_unique<AIEGenerateColumnControlOverlayPass>();
936}
937
938std::unique_ptr<OperationPass<mlir::ModuleOp>>
939AIE::createAIEGenerateColumnControlOverlayPass(
940 const AIEGenerateColumnControlOverlayOptions &options) {
941 return std::make_unique<AIEGenerateColumnControlOverlayPass>(options);
942}
943
944void populateAIEColumnControlOverlay(DeviceOp &device) {}
DenseMap< int, int > getRowToShimChanMap(const AIETargetModel &targetModel, WireBundle bundle)
DenseMap< AIE::TileID, int > getTileToControllerIdMap(bool clColumnWiseUniqueIDs, const AIETargetModel &targetModel)
llvm::DenseMap< TileID, int > getTileToControllerIdMap(bool columnWiseUniqueIDs=true) const
Return a mapping from tile coordinates to controller IDs.
virtual uint32_t getNumSourceShimMuxConnections(int col, int row, WireBundle bundle) const =0
Return the number of sources of connections inside a shimmux.
virtual int rows() const =0
Return the number of rows in the device.
bool isShimNOCTile(int col, int row) const
Return true if the given tile is a ShimNOC tile.
virtual int columns() const =0
Return the number of columns in the device.
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
void getDependentDialects(DialectRegistry &registry) const override
void getDependentDialects(DialectRegistry &registry) const override
static void collectTileUnion(ArrayRef< DeviceOp > devices, llvm::SmallSetVector< AIE::TileID, 8 > &unionTiles, llvm::DenseMap< AIE::TileID, AIE::TileOp > &prototypeTile)
static void cloneMissingTiles(DeviceOp device, const llvm::SmallSetVector< AIE::TileID, 8 > &unionTiles, const llvm::DenseMap< AIE::TileID, AIE::TileOp > &prototypeTile)
static void shareTilesAcrossDevices(ArrayRef< DeviceOp > devices)
LogicalResult applyOverlayToDevice(DeviceOp device, const llvm::SmallSet< AIE::TileID, 4 > &tokenTiles)
static llvm::SmallSet< AIE::TileID, 4 > collectTokenIssuingTiles(ArrayRef< DeviceOp > devices)
LogicalResult createOverlayDevice(ModuleOp module, OpBuilder &builder, ArrayRef< DeviceOp > participating, const llvm::SmallSet< AIE::TileID, 4 > &tokenTiles)