MLIR-AIE 1.4.6.dev
AIEPlacer.cpp
Go to the documentation of this file.
1//===- AIEPlacer.cpp --------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
13#include "mlir/Interfaces/ViewLikeInterface.h"
14#include "llvm/ADT/DenseSet.h"
15#include "llvm/ADT/MapVector.h"
16#include "llvm/ADT/STLExtras.h"
17#include "llvm/ADT/TypeSwitch.h"
18#include "llvm/Support/Debug.h"
19
20#include <algorithm>
21#include <numeric>
22
23using namespace mlir;
24using namespace xilinx::AIE;
25
26#define DEBUG_TYPE "aie-placer"
27
28// Packet selection only matters when the transport actually uses DMA.
29// Keep auto FIFOs eligible for the shared-memory path in dmaReason().
30static bool outOfOrderNeedsDMA(ObjectFifoCreateOp fifo) {
31 if (!fifo.getOutOfOrder())
32 return false;
33 if (fifo.forcesDMA() || fifo.getRepeatCount() || fifo.usesStream() ||
34 fifo.getConsumerElemType() || !fifo.getDimensionsToStream().empty())
35 return true;
36 for (auto dims : fifo.getDimensionsFromStreamPerConsumer())
37 if (!dims.empty())
38 return true;
39 auto device = fifo->getParentOfType<DeviceOp>();
40 for (auto alloc : device.getOps<ObjectFifoAllocateOp>())
41 if (alloc.getObjectFifo() == fifo)
42 return false;
43 for (auto link : device.getOps<ObjectFifoLinkOp>()) {
44 if (llvm::is_contained(link.getInputObjectFifos(), fifo) ||
45 llvm::is_contained(link.getOutputObjectFifos(), fifo))
46 return true;
47 }
48 return false;
49}
50
51static std::optional<TileID> resolvePeerPosition(TileLike peer,
52 const PlacementResult &placed);
53
54void Placer::Adjacency::addEdge(TileLike first, TileLike second) {
55 if (!first || !second)
56 return;
57 unsigned idx = edges.size();
58 edges.push_back({first, second});
59 if (mlir::isa<LogicalTileOp>(first.getOperation()))
60 tileToEdges[first.getOperation()].push_back(idx);
61 if (mlir::isa<LogicalTileOp>(second.getOperation()))
62 tileToEdges[second.getOperation()].push_back(idx);
63}
64
66 if (!a || !b)
67 return;
68 auto aT = dyn_cast_or_null<TileLike>(a.getDefiningOp());
69 auto bT = dyn_cast_or_null<TileLike>(b.getDefiningOp());
70 if (aT && bT)
71 addEdge(aT, bT);
72}
73
75 targetModel = &tm;
76 availability.compTiles.clear();
77 availability.nonCompTiles.clear();
78 for (int col = 0; col < tm.columns(); col++) {
79 for (int row = 0; row < tm.rows(); row++) {
80 TileID id = {col, row};
81 switch (tm.getTileType(col, row)) {
82 case AIETileType::CoreTile:
83 availability.compTiles.push_back(id);
84 break;
85 case AIETileType::MemTile:
86 case AIETileType::ShimNOCTile:
87 availability.nonCompTiles.push_back(id);
88 break;
89 default:
90 break;
91 }
92 }
93 }
94}
95
97 CollectedOps ops;
98 device.walk([&](Operation *op) {
99 llvm::TypeSwitch<Operation *>(op)
100 .Case<LogicalTileOp>([&](auto lt) { ops.logicalTiles.push_back(lt); })
101 .Case<ObjectFifoCreateOp>(
102 [&](auto of) { ops.objectFifos.push_back(of); })
103 .Case<ObjectFifoLinkOp>(
104 [&](auto link) { ops.objectFifoLinks.push_back(link); })
105 .Case<ObjectFifoAllocateOp>(
106 [&](auto alloc) { ops.objectFifoAllocates.push_back(alloc); })
107 .Case<CascadeFlowOp>([&](auto cf) { ops.cascadeFlows.push_back(cf); })
108 .Case<FlowOp>([&](auto f) { ops.flows.push_back(f); })
109 .Case<PacketFlowOp>([&](auto pf) { ops.pktFlows.push_back(pf); })
110 .Case<RouteOp>([&](auto route) { ops.routes.push_back(route); });
111 });
112 // Every later tie between logical tiles falls back on this order, so it is
113 // the design's structure rather than the order the tiles were written in:
114 // shim tiles, which have the fewest places to go, then MemTiles, then cores.
115 DenseMap<Operation *, unsigned> rank = StructuralGraph::of(device).rank();
116 llvm::stable_sort(ops.logicalTiles, [&](LogicalTileOp a, LogicalTileOp b) {
117 if (a.getTileType() != b.getTileType())
118 return a.getTileType() > b.getTileType();
119 return rank.lookup(a) < rank.lookup(b);
120 });
121 return ops;
122}
123
124bool Placer::satisfiesConstraints(Operation *tile, TileID pos) {
125 if (auto lt = dyn_cast<LogicalTileOp>(tile)) {
126 if (auto c = lt.tryGetCol(); c && *c != pos.col)
127 return false;
128 if (auto r = lt.tryGetRow(); r && *r != pos.row)
129 return false;
130 }
131 return true;
132}
133
136
137 // Compute tiles iterate column-major (fill a column top-to-bottom before
138 // moving right); non-compute tiles iterate row-major.
139 auto compTileCmp = [](TileID a, TileID b) {
140 if (a.col != b.col)
141 return a.col < b.col;
142 return a.row < b.row;
143 };
144 auto rowMajorCmp = [](TileID a, TileID b) {
145 if (a.row != b.row)
146 return a.row < b.row;
147 return a.col < b.col;
148 };
149 std::sort(availability.compTiles.begin(), availability.compTiles.end(),
150 compTileCmp);
151 std::sort(availability.nonCompTiles.begin(), availability.nonCompTiles.end(),
152 rowMajorCmp);
153 availability.compTilesSet.clear();
154 availability.compTilesSet.insert(availability.compTiles.begin(),
155 availability.compTiles.end());
156
157 if (coresPerCol.has_value()) {
158 llvm::DenseMap<int, int> coresInColumn;
159 for (const auto &tile : availability.compTiles)
160 coresInColumn[tile.col]++;
161 int maxDeviceCoresPerCol = 0;
162 for (const auto &[col, count] : coresInColumn)
163 maxDeviceCoresPerCol = std::max(maxDeviceCoresPerCol, count);
164 deviceCoresPerCol = maxDeviceCoresPerCol;
165 limitCoresPerColumn(*coresPerCol, targetModel.columns());
166 }
167}
168
169void SequentialPlacer::limitCoresPerColumn(int maxCoresPerCol, int numColumns) {
170 llvm::DenseMap<int, std::vector<TileID>> tilesByColumn;
171 for (const auto &tile : availability.compTiles)
172 tilesByColumn[tile.col].push_back(tile);
173
174 // Keep the first maxCoresPerCol tiles from each column. The per-column
175 // vectors are already row-sorted because compTiles is column-major.
176 std::vector<TileID> limitedTiles;
177 for (int col = 0; col < numColumns; col++) {
178 auto it = tilesByColumn.find(col);
179 if (it == tilesByColumn.end())
180 continue;
181 const auto &tilesInCol = it->second;
182 size_t numToTake =
183 std::min(tilesInCol.size(), static_cast<size_t>(maxCoresPerCol));
184 limitedTiles.insert(limitedTiles.end(), tilesInCol.begin(),
185 tilesInCol.begin() + numToTake);
186 }
187
188 availability.compTiles = limitedTiles;
191 availability.compTiles.end());
192}
193
194LogicalResult SequentialPlacer::place(DeviceOp device) {
195 // Phase 0: Validate options
196 if (!targetModel)
197 return device.emitError() << "SequentialPlacer::place called before "
198 "initialize(); targetModel is null";
199 if (coresPerCol.has_value() && *coresPerCol > deviceCoresPerCol) {
200 return device.emitError() << "requested cores-per-col (" << *coresPerCol
201 << ") exceeds device capacity ("
202 << deviceCoresPerCol << " cores per column)";
203 }
204
205 // Phase 1: Collect operations needed for placement
206 auto collected = collectOperations(device);
207 auto &logicalTiles = collected.logicalTiles;
208 auto &objectFifos = collected.objectFifos;
209 auto &objectFifoLinks = collected.objectFifoLinks;
210 auto &cascadeFlows = collected.cascadeFlows;
211 auto &flows = collected.flows;
212 auto &pktFlows = collected.pktFlows;
213 auto &routes = collected.routes;
214
215 // Phase 2a: Build placement constraints
216 auto bufferAdjacency =
217 buildBufferAdjacency(logicalTiles, collected.objectFifoAllocates);
218 auto computePeerAdjacency = buildComputePeerAdjacency(objectFifos);
219 auto sharedMemAdjacency = buildSharedMemAdjacency(objectFifos);
220
221 // Phase 2b: Build channel requirements from ObjectFifo and Flow connectivity,
222 // and cascade adjacency constraints from CascadeFlow connectivity.
223 auto channelRequirements =
224 buildChannelRequirements(objectFifos, objectFifoLinks);
225 addChannelRequirementsFromFlows(flows, pktFlows, routes, channelRequirements);
226 countTurns(device, objectFifos, channelRequirements);
227 buildStreamDemand(objectFifos);
228 buildRuntimeBdProfiles(device, objectFifos);
229 shareableShimEnds = collectShareableShimEnds(device, objectFifos);
230 shimSpans.emplace(
231 device, [&](StringAttr symbol) { return shimEndOf(device, symbol); });
232 shimEndsAt.clear();
233
234 auto cascadeAdjacency = buildCascadeAdjacency(cascadeFlows);
235
236 // A forced-shared-mem peer is already in `computePeerAdjacency`; this
237 // tallies which of a tile's peers are forced, to floor its neighbor
238 // demand below regardless of DMA channel headroom.
239 llvm::DenseMap<Operation *, int> forcedSharedMemIn, forcedSharedMemOut;
240 for (auto ofOp : objectFifos) {
241 if (!ofOp.forcesSharedMemory() || ofOp.getConsumerTiles().size() != 1)
242 continue;
243 auto producer =
244 dyn_cast_or_null<LogicalTileOp>(ofOp.getProducerTile().getDefiningOp());
245 auto consumer = dyn_cast_or_null<LogicalTileOp>(
246 ofOp.getConsumerTiles()[0].getDefiningOp());
247 if (!producer || producer.getTileType() != AIETileType::CoreTile)
248 continue;
249 if (!consumer || consumer.getTileType() != AIETileType::CoreTile)
250 continue;
251 if (producer.getOperation() == consumer.getOperation())
252 continue;
253 ++forcedSharedMemOut[producer.getOperation()];
254 ++forcedSharedMemIn[consumer.getOperation()];
255 }
256
257 // Phase 2c: For every compute LTO, compute the minimum number of its
258 // compute-peer producers/consumers that must land on physical neighbors so
259 // the LTO fits its DMA channel budget. A compute→compute ObjectFifo can use
260 // shared neighbor memory (no DMA) when the two endpoints sit on physical-
261 // neighbor compute tiles; otherwise it consumes one DMA channel on each
262 // side. Non-compute-peer fifos (from/to memtile or shim) always need a DMA
263 // channel and already appear in `channelRequirements`, so the headroom left
264 // for compute-peer fifos is (DMA-budget − channelRequirements). If a
265 // Worker has more compute-peer fifos than that headroom, the surplus MUST
266 // come from neighbors -- and a peer forcing shared memory always must,
267 // budget or not.
268 llvm::DenseMap<Operation *, int> needNeighborIn, needNeighborOut;
269 for (auto lt : logicalTiles) {
270 if (lt.getTileType() != AIETileType::CoreTile)
271 continue;
272 auto [peerIn, peerOut] =
273 totalComputePeers(lt.getOperation(), computePeerAdjacency);
274 auto [inBudget, outBudget] =
275 getDMACapacity(*targetModel, lt.getCanonicalTileID());
276 auto [nonPeerIn, nonPeerOut] =
277 channelRequirements.lookup(lt.getOperation());
278 needNeighborIn[lt.getOperation()] =
279 std::max({0, peerIn - (inBudget - nonPeerIn),
280 forcedSharedMemIn.lookup(lt.getOperation())});
281 needNeighborOut[lt.getOperation()] =
282 std::max({0, peerOut - (outBudget - nonPeerOut),
283 forcedSharedMemOut.lookup(lt.getOperation())});
284 LLVM_DEBUG(llvm::dbgs()
285 << DEBUG_TYPE << ": LTO " << lt.getLoc() << " peerIn=" << peerIn
286 << " peerOut=" << peerOut << " nonPeerIn=" << nonPeerIn
287 << " nonPeerOut=" << nonPeerOut
288 << " => needNeighborIn=" << needNeighborIn[lt.getOperation()]
289 << " needNeighborOut=" << needNeighborOut[lt.getOperation()]
290 << "\n");
291 }
292 PlacementContext ctx{*targetModel, computePeerAdjacency, needNeighborIn,
293 needNeighborOut};
294
295 // Phase 2d: group cores by the non-core tile they share the most flows
296 // with, so each group can be placed in one column and its hub's centroid
297 // lands there too.
298 FlowMembership flowIndex =
299 buildFlowMembership(flows, pktFlows, routes, objectFifos);
300 CoreGroups coreGroups = buildCoreGroups(logicalTiles, flowIndex);
301
302 // Per-kind predicates / labelers shared by all phase-3 call sites below.
303 // Buffer: consumer LTO (edge.first) must satisfy isLegalMemAffinity to the
304 // owner tile (edge.second).
305 auto bufferPred = [this](TileID consumerPos, TileID ownerPos) {
306 return targetModel->isLegalMemAffinity(consumerPos.col, consumerPos.row,
307 ownerPos.col, ownerPos.row);
308 };
309 auto bufferLabel = [](bool thisIsConsumer) -> StringRef {
310 return thisIsConsumer ? "shared-L1 buffer owner"
311 : "shared-L1 buffer consumer";
312 };
313 // Cascade: same predicate as AIELowerCascadeFlowsPass -- src (edge.first)
314 // must be one row North or one column West of dst (edge.second); rows
315 // increase upward, so isSouth(src,dst) means dst is south of src.
316 auto cascadePred = [this](TileID srcPos, TileID dstPos) {
317 return targetModel->isSouth(srcPos.col, srcPos.row, dstPos.col,
318 dstPos.row) ||
319 targetModel->isEast(srcPos.col, srcPos.row, dstPos.col, dstPos.row);
320 };
321 auto cascadeLabel = [](bool thisIsSrc) -> StringRef {
322 return thisIsSrc ? "cascade destination" : "cascade source";
323 };
324 // Shared memory: unlike buffer ownership, either end satisfying
325 // isLegalMemAffinity to the other is enough -- a shared_mem objectFifo
326 // has no owner/consumer asymmetry, it just needs both ends on tiles that
327 // share a memory module. Same bidirectional check as SAPlacer's own
328 // shared-memory penalty.
329 auto sharedMemPred = [this](TileID aPos, TileID bPos) {
330 return targetModel->isLegalMemAffinity(aPos.col, aPos.row, bPos.col,
331 bPos.row) ||
332 targetModel->isLegalMemAffinity(bPos.col, bPos.row, aPos.col,
333 aPos.row);
334 };
335 auto sharedMemLabel = [](bool thisIsProducer) -> StringRef {
336 return thisIsProducer ? "shared-memory consumer" : "shared-memory producer";
337 };
338
339 AdjacencyKind bufferKind{
340 bufferAdjacency, bufferPred, bufferLabel, "shared-L1 buffer",
341 "shared-L1 buffer adjacency requires this LTO to be on a tile "
342 "whose L1 is shared with the buffer owner's tile (N/S neighbor, "
343 "or W neighbor per the device's checkerboard rule)"};
344 AdjacencyKind cascadeKind{
345 cascadeAdjacency, cascadePred, cascadeLabel, "cascade",
346 "cascade adjacency requires the destination tile to be one row "
347 "South or one column East of the source tile"};
348 AdjacencyKind sharedMemKind{
349 sharedMemAdjacency, sharedMemPred, sharedMemLabel,
350 "shared-memory transport",
351 "a `shared_mem` objectFifo requires its producer and consumer to "
352 "land on tiles that share a memory module (N/S neighbor, or W "
353 "neighbor per the device's checkerboard rule)"};
354
355 // Phase 3: Place constrained tiles then compute tiles.
356 //
357 // Process LogicalTileOps in order of how constrained they are: fully pinned
358 // (both col and row) first, partially constrained (col xor row) next, fully
359 // unpinned last. This guarantees every pinned coordinate is recorded — and
360 // for CoreTiles, removed from availability — before any unpinned tile gets
361 // to pick from `availability.compTiles`, so unpinned tiles cannot land on a
362 // coordinate that a later-iterated pinned tile claims.
363 //
364 // Within the unpinned bucket, place compute LTOs with the highest
365 // neighbor-demand first so a high-fanin Worker grabs an interior tile (with
366 // two compute neighbors) before its producers consume the surrounding
367 // slots. Producers and consumers that the heavy Worker needs as physical
368 // neighbors are then steered to those slots by `computePeerAdjacency`.
369 //
370 // Before sorting, each core group is made contiguous at its first member,
371 // so the order in which the program creates cores does not decide which
372 // of them share a column.
373 SmallVector<LogicalTileOp> orderedTiles;
374 llvm::DenseSet<unsigned> emittedGroups;
375 for (auto lt : logicalTiles) {
376 auto group = coreGroups.groupOf.find(lt.getOperation());
377 if (group == coreGroups.groupOf.end())
378 orderedTiles.push_back(lt);
379 else if (emittedGroups.insert(group->second).second)
380 llvm::append_range(orderedTiles, coreGroups.members[group->second]);
381 }
382 llvm::stable_sort(orderedTiles, [&](LogicalTileOp a, LogicalTileOp b) {
383 auto rank = [](LogicalTileOp lt) {
384 bool hasCol = lt.tryGetCol().has_value();
385 bool hasRow = lt.tryGetRow().has_value();
386 if (hasCol && hasRow)
387 return 0; // fully pinned
388 if (hasCol || hasRow)
389 return 1; // partially constrained
390 return 2; // fully unpinned
391 };
392 int ra = rank(a), rb = rank(b);
393 if (ra != rb)
394 return ra < rb;
395 // Same constraint level: high priority first. Priority
396 // = max(self demand, highest peer demand) so a Worker's
397 // compute-peer producers/consumers also rise to the
398 // front and claim the Worker's neighbor tiles before
399 // unrelated LTOs consume them.
400 int pa = ctx.placementPriority(a.getOperation());
401 int pb = ctx.placementPriority(b.getOperation());
402 if (pa != pb)
403 return pa > pb;
404 // Among equal priority, place the high-demand LTO
405 // itself first so its peers can be steered to its
406 // neighbors immediately afterward.
407 return ctx.neighborDemand(a.getOperation()) >
408 ctx.neighborDemand(b.getOperation());
409 });
410
411 // An unpinned tile sharing L1 with another unpinned tile is placed right
412 // after it, while that tile's neighbors are still free.
413 auto unpinned = [](LogicalTileOp lt) {
414 return !lt.tryGetCol() && !lt.tryGetRow();
415 };
416 SmallVector<LogicalTileOp> sequenced;
417 llvm::DenseSet<Operation *> queued;
418 for (auto start : orderedTiles) {
419 if (!queued.insert(start.getOperation()).second)
420 continue;
421 size_t next = sequenced.size();
422 sequenced.push_back(start);
423 for (; next < sequenced.size(); ++next) {
424 LogicalTileOp lt = sequenced[next];
425 if (!unpinned(lt))
426 continue;
427 forEachPeer(lt.getOperation(), bufferAdjacency,
428 [&](TileLike peer, bool /*thisIsFirst*/) {
429 auto peerLt = dyn_cast<LogicalTileOp>(peer.getOperation());
430 if (peerLt && unpinned(peerLt) &&
431 queued.insert(peerLt.getOperation()).second)
432 sequenced.push_back(peerLt);
433 });
434 }
435 }
436 orderedTiles = std::move(sequenced);
437
438 for (auto logicalTile : orderedTiles) {
439 // Place fully constrained tiles at their specified coordinates
440 auto col = logicalTile.tryGetCol();
441 auto row = logicalTile.tryGetRow();
442 if (col && row) {
443 TileID tile{*col, *row};
444 if (failed(enforceAdjacency(logicalTile, tile, bufferKind)))
445 return failure();
446 if (failed(enforceAdjacency(logicalTile, tile, cascadeKind)))
447 return failure();
448 if (failed(enforceAdjacency(logicalTile, tile, sharedMemKind)))
449 return failure();
450 if (failed(validateAndUpdateChannelUsage(
451 logicalTile, tile, channelRequirements, PlacementOrigin::Pinned)))
452 return failure();
453
454 recordPlacement(logicalTile, tile, ctx, "pinned");
455 continue;
456 }
457
458 // Place compute tiles with partial constraint support
459 if (logicalTile.getTileType() == AIETileType::CoreTile) {
460 auto isReservedForOtherBound = [&](Operation *lto, TileID candidate) {
461 return ctx.isReservedForOther(lto, candidate);
462 };
463 SmallVector<int> preferredCols =
464 preferredGroupColumns(logicalTile, coreGroups);
465 UnpinnedPlacementInputs inputs{
466 bufferAdjacency, bufferPred,
467 cascadeAdjacency, cascadePred,
468 sharedMemAdjacency, sharedMemPred,
469 computePeerAdjacency, needNeighborIn,
470 needNeighborOut, isReservedForOtherBound,
471 preferredCols};
472 auto search =
473 findUnconstrainedCoreCandidate(logicalTile, col, row, inputs);
474 std::optional<TileID> placement = search.placement;
475 bool sawConstraintMatch = search.sawConstraintMatch;
476 bool allConstraintMatchesFailedAdjacency =
477 search.allConstraintMatchesFailedAdjacency;
478 bool computePeerWasCause = search.computePeerWasCause;
479
480 if (!placement) {
481 bool adjacencyWasCause =
482 sawConstraintMatch && allConstraintMatchesFailedAdjacency &&
483 bufferAdjacency.hasEdges(logicalTile.getOperation());
484 bool hasCascade = cascadeAdjacency.hasEdges(logicalTile.getOperation());
485 bool hasSharedMem =
486 sharedMemAdjacency.hasEdges(logicalTile.getOperation());
487 InFlightDiagnostic diag = logicalTile.emitError();
488 if (col || row) {
489 diag << "no compute tile available matching constraint ("
490 << (col ? std::to_string(*col) : "?") << ", "
491 << (row ? std::to_string(*row) : "?") << ")"
492 << (adjacencyWasCause ? " and shared-L1 buffer adjacency" : "")
493 << (hasCascade ? " and cascade adjacency" : "")
494 << (hasSharedMem ? " and shared-memory transport" : "")
495 << (computePeerWasCause ? " and compute-peer DMA budget" : "");
496 } else {
497 diag << "no available compute tiles for placement"
498 << (adjacencyWasCause
499 ? " (shared-L1 buffer adjacency unsatisfiable)"
500 : "")
501 << (hasCascade ? " (cascade adjacency unsatisfiable)" : "")
502 << (hasSharedMem ? " (shared-memory transport unsatisfiable)"
503 : "")
504 << (computePeerWasCause
505 ? " (compute-peer DMA budget unsatisfiable)"
506 : "");
507 }
508 // A cores-per-col budget leaves no trace in the message above: the
509 // tiles it removed are simply absent from `availability.compTiles`.
510 if (coresPerCol.has_value() && deviceCoresPerCol > 0) {
511 int columns = targetModel->columns();
512 diag.attachNote()
513 << "cores-per-col=" << *coresPerCol << " leaves "
514 << columns * *coresPerCol << " of this device's "
515 << columns * deviceCoresPerCol
516 << " compute tiles placeable and may be limiting placement; "
517 "consider raising it or reducing the design's core count";
518 }
519 if (adjacencyWasCause)
520 attachPeerNotes(diag, logicalTile, bufferAdjacency, bufferLabel);
521 if (hasCascade)
522 attachPeerNotes(diag, logicalTile, cascadeAdjacency, cascadeLabel);
523 if (hasSharedMem)
524 attachPeerNotes(diag, logicalTile, sharedMemAdjacency,
525 sharedMemLabel);
526 if (computePeerWasCause) {
527 attachPeerNotes(diag, logicalTile, computePeerAdjacency,
528 [](bool thisIsProducer) -> StringRef {
529 return thisIsProducer ? "compute-peer consumer"
530 : "compute-peer producer";
531 });
532 diag.attachNote()
533 << "to fix, pin this LTO to a position that is shared-L1 with "
534 "the placed compute-peers above, or pin one of those peers "
535 "to a different coordinate so this LTO's constraints become "
536 "satisfiable";
537 }
538 return failure();
539 }
540
541 if (failed(validateAndUpdateChannelUsage(logicalTile, *placement,
542 channelRequirements,
543 PlacementOrigin::Selected)))
544 return failure();
545
546 recordPlacement(logicalTile, *placement, ctx, "unconstrained");
547 }
548
549 if (logicalTile.getTileType() == AIETileType::ShimPLTile) {
550 return logicalTile.emitError(
551 "DMA channel-based SequentialPlacer does not support unplaced "
552 "ShimPLTiles (no DMAs).");
553 }
554 }
555
556 // Phase 4: place every still-unplaced non-core (mem/shim) LTO at the
557 // centroid column of its placed core peers.
558 return placeNonCoreLogicalTiles(logicalTiles, flowIndex, channelRequirements);
559}
560
561SequentialPlacer::CoreGroups
562SequentialPlacer::buildCoreGroups(ArrayRef<LogicalTileOp> logicalTiles,
563 const FlowMembership &flowIndex) const {
564 // Flows from each core to each non-core tile, in first-seen order.
565 SmallVector<std::pair<LogicalTileOp, llvm::MapVector<Operation *, int>>>
566 coreFlows;
567 llvm::DenseMap<Operation *, int> coresOnHub;
568 for (auto lt : logicalTiles) {
569 if (lt.getTileType() != AIETileType::CoreTile)
570 continue;
571 llvm::MapVector<Operation *, int> counts;
572 auto it = flowIndex.ltoFlows.find(lt.getResult());
573 if (it != flowIndex.ltoFlows.end())
574 for (auto &peers : it->second)
575 for (Value p : peers) {
576 auto tile = dyn_cast_or_null<TileLike>(p.getDefiningOp());
577 if (tile && !tile.isCoreTile())
578 ++counts[p.getDefiningOp()];
579 }
580 for (auto &entry : counts)
581 ++coresOnHub[entry.first];
582 coreFlows.emplace_back(lt, std::move(counts));
583 }
584
585 // A core's hub is the tile it has the most flows with; on a tie, the one
586 // fewer cores touch (the more specific one), then the first seen.
587 CoreGroups groups;
588 llvm::DenseMap<Operation *, unsigned> groupOfHub;
589 for (auto &[lt, counts] : coreFlows) {
590 Operation *hub = nullptr;
591 int best = 0;
592 for (auto &[peer, n] : counts)
593 if (!hub || n > best ||
594 (n == best && coresOnHub[peer] < coresOnHub[hub])) {
595 hub = peer;
596 best = n;
597 }
598 unsigned idx = groups.members.size();
599 if (hub) {
600 auto [it, inserted] = groupOfHub.try_emplace(hub, idx);
601 idx = it->second;
602 if (!inserted) {
603 groups.members[idx].push_back(lt);
604 groups.groupOf[lt.getOperation()] = idx;
605 continue;
606 }
607 }
608 groups.members.push_back({lt});
609 groups.groupOf[lt.getOperation()] = idx;
610 }
611 return groups;
612}
613
614SmallVector<int>
615SequentialPlacer::preferredGroupColumns(LogicalTileOp logicalTile,
616 const CoreGroups &groups) const {
617 auto group = groups.groupOf.find(logicalTile.getOperation());
618 if (group == groups.groupOf.end())
619 return {};
620 SmallVector<std::pair<int, int>> placedCols; // (column, members there)
621 int unplaced = 0;
622 for (auto member : groups.members[group->second]) {
623 auto placed = result.find(member.getOperation());
624 if (placed == result.end()) {
625 ++unplaced;
626 continue;
627 }
628 auto *entry = llvm::find_if(
629 placedCols, [&](auto &e) { return e.first == placed->second.col; });
630 if (entry == placedCols.end())
631 placedCols.push_back({placed->second.col, 1});
632 else
633 ++entry->second;
634 }
635 llvm::stable_sort(placedCols, [](auto &a, auto &b) {
636 return a.second != b.second ? a.second > b.second : a.first < b.first;
637 });
638
639 SmallVector<int> cols;
640 for (auto &[col, n] : placedCols)
641 cols.push_back(col);
642 // compTiles is column-major, so this visits columns in order.
643 llvm::MapVector<int, int> freeCores;
644 for (TileID t : availability.compTiles)
645 ++freeCores[t.col];
646 for (auto &[col, n] : freeCores)
647 if (n >= unplaced && !llvm::is_contained(cols, col))
648 cols.push_back(col);
649 return cols;
650}
651
652LogicalResult SequentialPlacer::placeNonCoreLogicalTiles(
653 ArrayRef<LogicalTileOp> logicalTiles, const FlowMembership &flowIndex,
654 const llvm::DenseMap<Operation *, std::pair<int, int>>
655 &channelRequirements) {
656 // Sort the unplaced non-core LTOs by descending channel demand so the
657 // heaviest consumers (e.g. memtile joins that need many input channels)
658 // get first pick of physical tiles before lighter pass-through fifos
659 // crowd into the same column and leave no room.
660 SmallVector<LogicalTileOp> nonCoreOrdered;
661 for (auto logicalTile : logicalTiles) {
662 if (result.count(logicalTile.getOperation()))
663 continue;
664 AIETileType tileType = logicalTile.getTileType();
665 if (tileType == AIETileType::CoreTile ||
666 tileType == AIETileType::ShimPLTile)
667 continue;
668 nonCoreOrdered.push_back(logicalTile);
669 }
670 llvm::stable_sort(nonCoreOrdered, [&](LogicalTileOp a, LogicalTileOp b) {
671 auto demand = [&](LogicalTileOp lt) {
672 auto chans = channelRequirements.lookup(lt.getOperation());
673 return chans.first + chans.second;
674 };
675 return demand(a) > demand(b);
676 });
677
678 for (auto logicalTile : nonCoreOrdered) {
679 if (failed(placeNonCoreTileByCentroid(logicalTile, flowIndex,
680 channelRequirements)))
681 return failure();
682 }
683 return success();
684}
685
686int SequentialPlacer::PlacementContext::placementPriority(Operation *op) const {
687 int self = neighborDemand(op);
688 int best = 0;
689 forEachPeer(op, computePeerAdjacency,
690 [&](TileLike peer, bool /*thisIsFirst*/) {
691 best = std::max(best, neighborDemand(peer.getOperation()));
692 });
693 return std::max(self, best);
694}
695
696void SequentialPlacer::PlacementContext::reserveNeighborSlots(
697 Operation *placedOp, TileID at) {
698 if (neighborDemand(placedOp) <= 0)
699 return;
701 targetModel, at, [&](TileID nb) { reservedFor[nb].push_back(placedOp); });
702}
703
704bool SequentialPlacer::PlacementContext::isReservedForOther(
705 Operation *lto, TileID candidate) const {
706 auto it = reservedFor.find(candidate);
707 if (it == reservedFor.end())
708 return false;
709 for (Operation *owner : it->second) {
710 if (owner == lto)
711 continue; // The LTO's own slot reservation never counts as "other".
712 bool isPeer = false;
713 forEachPeer(lto, computePeerAdjacency,
714 [&](TileLike peer, bool /*thisIsFirst*/) {
715 if (peer.getOperation() == owner)
716 isPeer = true;
717 });
718 if (!isPeer)
719 return true;
720 }
721 return false;
722}
723
724SequentialPlacer::UnpinnedSearchResult
725SequentialPlacer::findUnconstrainedCoreCandidate(
726 LogicalTileOp logicalTile, std::optional<int> col, std::optional<int> row,
727 const UnpinnedPlacementInputs &inputs) {
728 UnpinnedSearchResult result;
729 auto neighborDemand = [&](Operation *op) {
730 return inputs.needNeighborIn.lookup(op) + inputs.needNeighborOut.lookup(op);
731 };
732
733 // compTiles is sorted column-major top-to-bottom. For high-fanin LTOs
734 // (neighborDemand > 0) the placer needs an INTERIOR tile (with two
735 // compute neighbors) so its compute-peer producers/consumers can sit
736 // on either side. Reorder the candidate iteration for THIS LTO only
737 // to prefer high-neighbor-count rows first; LTOs with no neighbor
738 // demand fall through to the default column-major order so existing
739 // single-worker tests keep landing on (col, 2).
740 auto computeNeighborCount = [&](TileID t) {
741 int n = 0;
742 if (t.row > 0 &&
743 targetModel->getTileType(t.col, t.row - 1) == AIETileType::CoreTile)
744 ++n;
745 if (t.row + 1 < targetModel->rows() &&
746 targetModel->getTileType(t.col, t.row + 1) == AIETileType::CoreTile)
747 ++n;
748 return n;
749 };
750 SmallVector<TileID> orderedCandidates(availability.compTiles.begin(),
751 availability.compTiles.end());
752 if (neighborDemand(logicalTile.getOperation()) > 0) {
753 llvm::stable_sort(orderedCandidates, [&](TileID a, TileID b) {
754 if (a.col != b.col)
755 return a.col < b.col;
756 int na = computeNeighborCount(a);
757 int nb = computeNeighborCount(b);
758 if (na != nb)
759 return na > nb;
760 return a.row < b.row;
761 });
762 } else if (!inputs.preferredCols.empty()) {
763 // Keep the LTO with its core group: preferred columns first, in
764 // preference order; the rest keep the column-major order after them.
765 auto rankOf = [&](TileID t) {
766 return std::distance(inputs.preferredCols.begin(),
767 llvm::find(inputs.preferredCols, t.col));
768 };
769 llvm::stable_sort(orderedCandidates, [&](TileID a, TileID b) {
770 return rankOf(a) < rankOf(b);
771 });
772 }
773
774 // A candidate that leaves no free core tile for one of this LTO's
775 // unplaced shared-L1 peers is tried only in the final pass. Each peer
776 // claims its own tile; claiming greedily is exact when the peers share one
777 // candidate set, as one owner's consumers do.
778 auto leavesPeerRoom = [&](TileID candidate) {
779 llvm::DenseSet<TileID> claimed;
780 bool room = true;
781 forEachPeer(logicalTile.getOperation(), inputs.bufferAdjacency,
782 [&](TileLike peer, bool thisIsFirst) {
783 if (!room || resolvePeerPosition(peer, this->result))
784 return;
785 // thisIsFirst: this tile is the consumer, peer the owner.
786 auto free =
787 llvm::find_if(availability.compTiles, [&](TileID t) {
788 return t != candidate && !claimed.contains(t) &&
789 (thisIsFirst ? inputs.bufferPred(candidate, t)
790 : inputs.bufferPred(t, candidate));
791 });
792 room = free != availability.compTiles.end();
793 if (room)
794 claimed.insert(*free);
795 });
796 return room;
797 };
798
799 auto fitsColumnStreams = [&](TileID candidate) {
800 StreamDemand demand = streamDemand.lookup(logicalTile);
801 StreamDemand used = columnStreamsUsed.lookup(candidate.col);
802 auto incoming = columnIncomingFifos.find(candidate.col);
803 if (incoming != columnIncomingFifos.end())
804 for (Operation *fifo : incomingFifos.lookup(logicalTile))
805 demand.southIn -= incoming->second.contains(fifo);
806 // Measure the bottleneck at the bottom compute row, not at a candidate
807 // in the middle of the column. Overflow can still route via other columns.
808 int bottomRow = candidate.row;
809 while (bottomRow > 0 &&
810 targetModel->isCoreTile(candidate.col, bottomRow - 1))
811 --bottomRow;
812 return (!demand.southIn ||
813 used.southIn + demand.southIn <=
814 static_cast<int>(targetModel->getNumSourceSwitchboxConnections(
815 candidate.col, bottomRow, WireBundle::South))) &&
816 (!demand.southOut ||
817 used.southOut + demand.southOut <=
818 static_cast<int>(targetModel->getNumDestSwitchboxConnections(
819 candidate.col, bottomRow, WireBundle::South)));
820 };
821
822 // First prefer columns with stream capacity, then allow overloaded columns
823 // while preserving peer room and reservations. Relax those only as a last
824 // resort.
825 for (int pass = 0; pass < 3; ++pass) {
826 for (const TileID &candidate : orderedCandidates) {
827 if (col && candidate.col != *col)
828 continue;
829 if (row && candidate.row != *row)
830 continue;
831 if (pass < 2 &&
832 (inputs.isReservedForOther(logicalTile.getOperation(), candidate) ||
833 !leavesPeerRoom(candidate)))
834 continue;
835 if (pass == 0 && !fitsColumnStreams(candidate))
836 continue;
837 result.sawConstraintMatch = true;
838 if (!satisfiesAdjacency(logicalTile, candidate, inputs.bufferAdjacency,
839 inputs.bufferPred))
840 continue;
841 result.allConstraintMatchesFailedAdjacency = false;
842 if (!satisfiesAdjacency(logicalTile, candidate, inputs.cascadeAdjacency,
843 inputs.cascadePred))
844 continue;
845 if (!satisfiesAdjacency(logicalTile, candidate, inputs.sharedMemAdjacency,
846 inputs.sharedMemPred))
847 continue;
848 if (!satisfiesComputePeer(
849 logicalTile, candidate, inputs.computePeerAdjacency,
850 inputs.needNeighborIn, inputs.needNeighborOut)) {
851 result.computePeerWasCause = true;
852 continue;
853 }
854 result.placement = candidate;
855 return result;
856 }
857 }
858 return result;
859}
860
861LogicalResult SequentialPlacer::validateAndUpdateChannelUsage(
862 LogicalTileOp logicalTile, TileID tile,
863 const llvm::DenseMap<Operation *, std::pair<int, int>> &channelRequirements,
864 PlacementOrigin origin) {
865 auto [inChannels, outChannels] =
866 channelRequirements.lookup(logicalTile.getOperation());
867
868 if (!hasAvailableChannels(tile, inChannels, outChannels) &&
869 !fitsSharingShim(tile, logicalTile, {inChannels, outChannels})) {
870 // The same capacity as hasAvailableChannels, so the message matches it.
871 auto [maxIn, maxOut] = getDMACapacity(*targetModel, tile);
872 int availIn = maxIn - availability.inputChannelsUsed[tile];
873 int availOut = maxOut - availability.outputChannelsUsed[tile];
874
875 auto diag = logicalTile.emitError();
876 diag << "tile (" << tile.col << ", " << tile.row << ") requires "
877 << inChannels << " input/" << outChannels
878 << " output DMA channels, but only " << availIn << " input/"
879 << availOut << " output available";
880 if (origin == PlacementOrigin::Selected)
881 diag.attachNote() << "placer selected this tile; to fix, pin this LTO "
882 "to a tile with more spare DMA capacity, or reduce "
883 "the LTO's DMA fanin (e.g. via memtile staging)";
884 return failure();
885 }
886
887 if (inChannels > 0)
888 updateChannelUsage(tile, DmaDir::In, inChannels);
889 if (outChannels > 0)
890 updateChannelUsage(tile, DmaDir::Out, outChannels);
891 recordStreamAndBdUsage(logicalTile, tile);
892 recordShimEnds(tile, logicalTile);
893
894 return success();
895}
896
897llvm::DenseMap<Operation *, std::pair<int, int>>
898SequentialPlacer::buildChannelRequirements(
899 SmallVector<ObjectFifoCreateOp> &objectFifos,
900 SmallVector<ObjectFifoLinkOp> &objectFifoLinks) {
901 llvm::DenseMap<Operation *, std::pair<int, int>> channelRequirements;
902
903 // Build map of ObjectFifo name -> CreateOp for link processing
904 llvm::StringMap<ObjectFifoCreateOp> fifoNameToOp;
905 for (auto ofOp : objectFifos) {
906 fifoNameToOp[ofOp.getSymName()] = ofOp;
907 }
908
909 // A link's tile is the one producing its dest fifos (each fifo has one
910 // producer). The link loop below credits it with (#sources input + #dests
911 // output), so the per-fifo loop skips the side of each linked fifo on a tile
912 // of a link reading it, and only those: a linked source fifo can have other
913 // consumers, such as a shim that also drains it, and those still need their
914 // own channels.
915 llvm::DenseMap<Operation *, Operation *> linkTileOf;
916 llvm::StringMap<SmallVector<Operation *>> linkTilesOfSource;
917 llvm::DenseSet<llvm::StringRef> linkedAsDest;
918 for (auto linkOp : objectFifoLinks) {
919 Operation *linkTileOp = nullptr;
920 for (auto dstFifoAttr : linkOp.getFifoOuts())
921 if (auto sym = dyn_cast<FlatSymbolRefAttr>(dstFifoAttr)) {
922 linkedAsDest.insert(sym.getValue());
923 auto it = fifoNameToOp.find(sym.getValue());
924 if (!linkTileOp && it != fifoNameToOp.end())
925 linkTileOp = it->second.getProducerTile().getDefiningOp();
926 }
927 linkTileOf[linkOp.getOperation()] = linkTileOp;
928 if (!linkTileOp)
929 continue;
930 for (auto srcFifoAttr : linkOp.getFifoIns())
931 if (auto sym = dyn_cast<FlatSymbolRefAttr>(srcFifoAttr))
932 linkTilesOfSource[sym.getValue()].push_back(linkTileOp);
933 }
934
935 // Endpoints that pin the same channel share it, so the tile pays for that
936 // channel once. AIEObjectFifoAllocate's sharesChannel() states which
937 // channels may be shared, and why.
938 //
939 // Fully pinned logical tiles at the same coordinates share a physical
940 // channel. Otherwise only endpoints on the very same logical tile share:
941 // that identity constrains them to be placed together.
942 using ChannelKey = std::tuple<Operation *, int, int, int>;
943 auto channelKey = [](LogicalTileOp tile, int channel) -> ChannelKey {
944 auto col = tile.getCol();
945 auto row = tile.getRow();
946 if (col && row)
947 return {nullptr, static_cast<int>(*col), static_cast<int>(*row), channel};
948 return {tile.getOperation(), -1, -1, channel};
949 };
950 llvm::DenseMap<ChannelKey, ObjectFifoCreateOp> sharedOut;
951 auto sharesProducerChannel = [&](ObjectFifoCreateOp fifo,
952 LogicalTileOp logicalTile) -> bool {
953 ObjectFifoEndPortAttr port = fifo.getProdPortAttr();
954 std::optional<int> pinned =
955 port && !port.isCore() ? port.channelIndex() : std::nullopt;
956 if (!pinned || !logicalTile)
957 return false;
958 AIETileType type = logicalTile.getTileType();
959 bool coreSender = type == AIETileType::CoreTile &&
960 !fifo.streamsFromProducer() &&
961 targetModel->getDmaTaskQueueDepth() > 0;
962 if (type != AIETileType::ShimNOCTile && type != AIETileType::ShimPLTile &&
963 !coreSender)
964 return false;
965 auto [it, fresh] =
966 sharedOut.try_emplace(channelKey(logicalTile, *pinned), fifo);
967 if (fresh)
968 return false;
969 ObjectFifoCreateOp other = it->second;
970 auto reject = [&](const Twine &why) {
971 fifo.emitError() << "shares MM2S channel " << *pinned << " with @"
972 << other.getSymName() << ", but " << why;
973 return false;
974 };
975 if (!fifo.packetHeader() || !other.packetHeader())
976 return reject("a shared channel carries both at once, which only "
977 "packet-switched routes tell apart");
978 std::optional<uint16_t> id =
979 fifo.packetHeader() ? fifo.packetHeader().getPktId() : std::nullopt;
980 std::optional<uint16_t> otherId =
981 other.packetHeader() ? other.packetHeader().getPktId() : std::nullopt;
982 if (id && otherId && *id == *otherId)
983 return reject(Twine("both pin packet id ") +
984 Twine(static_cast<unsigned>(*id)));
985 return true;
986 };
987
988 // The receiving counterpart: out-of-order fifos pinned to one S2MM channel
989 // of a tile share it, and the packet ids keep their objects apart.
990 llvm::DenseMap<ChannelKey, ObjectFifoCreateOp> sharedIn;
991 auto sharesConsumerChannel = [&](ObjectFifoCreateOp fifo, Operation *tileOp,
992 unsigned consumer) -> bool {
993 auto logicalTile = dyn_cast_or_null<LogicalTileOp>(tileOp);
994 ObjectFifoEndPortAttr port = fifo.getConsumerPort(consumer);
995 if (!logicalTile || !port || port.isCore() || !port.channelIndex())
996 return false;
997 int channel = *port.channelIndex();
998 if (channel < 0)
999 return false;
1000 auto [it, fresh] =
1001 sharedIn.try_emplace(channelKey(logicalTile, channel), fifo);
1002 if (fresh)
1003 return false;
1004 ObjectFifoCreateOp other = it->second;
1005 auto reject = [&](const Twine &why) {
1006 fifo.emitError() << "shares S2MM channel " << channel << " with @"
1007 << other.getSymName() << ", but " << why;
1008 return false;
1009 };
1010 if (!fifo.getOutOfOrder() || !other.getOutOfOrder())
1011 return reject("a channel its fifos receive in turn holds their order; "
1012 "out_of_order puts that order in each object's header");
1013 std::optional<uint16_t> id =
1014 fifo.packetHeader() ? fifo.packetHeader().getPktId() : std::nullopt;
1015 std::optional<uint16_t> otherId =
1016 other.packetHeader() ? other.packetHeader().getPktId() : std::nullopt;
1017 if (id && otherId && *id == *otherId)
1018 return reject(Twine("both pin packet id ") +
1019 Twine(static_cast<unsigned>(*id)));
1020 return true;
1021 };
1022
1023 // Count channels for every ObjectFifo, omitting only the link-tile side of
1024 // linked fifos. The off-link-tile endpoint still needs DMA: a shim that
1025 // produces a linked source fifo consumes an MM2S channel; a core that
1026 // consumes a linked dest fifo consumes an S2MM channel.
1027 for (auto ofOp : objectFifos) {
1028 SmallVector<Operation *> linkConsumers =
1029 linkTilesOfSource.lookup(ofOp.getSymName());
1030 bool skipProducerSide = linkedAsDest.count(ofOp.getSymName());
1031
1032 Value producerTile = ofOp.getProducerTile();
1033 auto *producerOp = producerTile.getDefiningOp();
1034 auto producerLogicalTile = dyn_cast_or_null<LogicalTileOp>(producerOp);
1035
1036 // Check if ANY consumer is a different tile type (needs DMA channel)
1037 bool producerNeedsDMA = false;
1038
1039 for (auto [consumerIndex, consumerTile] :
1040 llvm::enumerate(ofOp.getConsumerTiles())) {
1041 auto *consumerOp = consumerTile.getDefiningOp();
1042 auto consumerLogicalTile = dyn_cast_or_null<LogicalTileOp>(consumerOp);
1043
1044 // Skip a core-to-core fifo of one consumer: a compute peer, which
1045 // neighbor memory may carry instead of DMA (buildComputePeerAdjacency).
1046 // A broadcast always moves by DMA, so it is counted here.
1047 if (producerLogicalTile && consumerLogicalTile &&
1048 producerLogicalTile.getTileType() == AIETileType::CoreTile &&
1049 consumerLogicalTile.getTileType() == AIETileType::CoreTile &&
1050 ofOp.getConsumerTiles().size() == 1 && !outOfOrderNeedsDMA(ofOp))
1051 continue;
1052
1053 // Linked consumers and shared channels are counted separately.
1054 if (consumerOp && !llvm::is_contained(linkConsumers, consumerOp) &&
1055 !sharesConsumerChannel(ofOp, consumerOp, consumerIndex))
1056 channelRequirements[consumerOp].first++; // input++
1057
1058 producerNeedsDMA = true;
1059 }
1060
1061 // Producer needs ONE output channel if any consumer needs DMA (unless the
1062 // producer IS the link tile of an outgoing linked dest fifo, or it shares
1063 // an already-counted shared channel).
1064 if (producerNeedsDMA && producerOp && !skipProducerSide &&
1065 !sharesProducerChannel(ofOp, producerLogicalTile))
1066 channelRequirements[producerOp].second++; // output++
1067 }
1068
1069 // For linked ObjectFifos, count channels based on the link structure
1070 // Link tiles need channels for all sources and destinations
1071 for (auto linkOp : objectFifoLinks) {
1072
1073 Operation *linkTileOp = linkTileOf.lookup(linkOp.getOperation());
1074 if (!linkTileOp)
1075 continue;
1076
1077 // Link tile needs one input channel per source ObjectFifo and one output
1078 // channel per dest ObjectFifo, except that a time link's turns share one.
1079 int numInputChannels = linkOp.isMerge() ? 1 : linkOp.getFifoIns().size();
1080 int numOutputChannels =
1081 linkOp.isDispatch() ? 1 : linkOp.getFifoOuts().size();
1082
1083 channelRequirements[linkTileOp].first += numInputChannels;
1084 channelRequirements[linkTileOp].second += numOutputChannels;
1085 }
1086
1087 return channelRequirements;
1088}
1089
1090void SequentialPlacer::countTurns(
1091 DeviceOp device, ArrayRef<ObjectFifoCreateOp> objectFifos,
1092 llvm::DenseMap<Operation *, std::pair<int, int>> &requirements) {
1093 for (CoreOp core : device.getOps<CoreOp>()) {
1094 auto tile = dyn_cast_or_null<LogicalTileOp>(core.getTile().getDefiningOp());
1095 if (!tile || !requirements.count(tile))
1096 continue;
1097 auto &[in, out] = requirements[tile];
1098 for (bool coreDrains : {true, false}) {
1099 int &need = coreDrains ? in : out;
1100 auto [inBudget, outBudget] =
1101 getDMACapacity(*targetModel, tile.getCanonicalTileID());
1102 int budget = coreDrains ? inBudget : outBudget;
1103 if (need <= budget)
1104 continue;
1105 llvm::MapVector<Operation *, SmallVector<ObjectFifoCreateOp>> byTile;
1106 for (ObjectFifoCreateOp fifo : objectFifos) {
1107 auto consumers = fifo.getConsumerTiles();
1108 if (consumers.size() != 1)
1109 continue;
1110 Value mine = coreDrains ? consumers[0] : fifo.getProducerTile();
1111 Value other = coreDrains ? fifo.getProducerTile() : consumers[0];
1112 auto peer = dyn_cast_or_null<LogicalTileOp>(other.getDefiningOp());
1113 if (mine == tile.getResult() && peer &&
1114 peer.getTileType() == AIETileType::MemTile)
1115 byTile[peer].push_back(fifo);
1116 }
1117 for (auto &[peer, fifos] : byTile) {
1118 if (fifos.size() < 2 || need <= budget)
1119 continue;
1120 SmallVector<StringAttr> names;
1121 SmallVector<int64_t> depths;
1122 for (ObjectFifoCreateOp fifo : fifos) {
1123 names.push_back(fifo.getSymNameAttr());
1124 depths.push_back(fifo.size(coreDrains ? 1 : 0));
1125 }
1126 CoreTurns turns;
1127 std::string reason;
1128 if (succeeded(turns.read(core, names, coreDrains, reason)) &&
1129 turns.keepsUp(depths))
1130 need -= static_cast<int>(fifos.size()) - 1;
1131 }
1132 }
1133 }
1134}
1135
1136// Walk view-like aliasing memref ops back to the underlying BufferOp.
1137static BufferOp traceToBuffer(Value val) {
1138 for (Operation *op = val.getDefiningOp(); op;) {
1139 if (auto buf = dyn_cast<BufferOp>(op))
1140 return buf;
1141 if (auto view = dyn_cast<ViewLikeOpInterface>(op)) {
1142 val = view.getViewSource();
1143 op = val.getDefiningOp();
1144 continue;
1145 }
1146 return nullptr;
1147 }
1148 return nullptr;
1149}
1150
1151static std::optional<TileID>
1152resolvePeerPosition(TileLike peer, const PlacementResult &placed) {
1153 if (!peer)
1154 return std::nullopt;
1155 auto it = placed.find(peer.getOperation());
1156 if (it != placed.end())
1157 return it->second;
1158 auto col = peer.tryGetCol();
1159 auto row = peer.tryGetRow();
1160 if (col && row)
1161 return TileID{*col, *row};
1162 return std::nullopt;
1163}
1164
1165Placer::Adjacency SequentialPlacer::buildBufferAdjacency(
1166 ArrayRef<LogicalTileOp> logicalTiles,
1167 ArrayRef<ObjectFifoAllocateOp> allocates) {
1168 Adjacency adjacency;
1169 // One edge per (user, owner) pair: a duplicate would book a second free
1170 // tile for the same peer in leavesPeerRoom.
1171 llvm::DenseSet<std::pair<Operation *, Operation *>> seen;
1172 auto addOnce = [&](TileLike user, TileLike owner) {
1173 if (seen.insert({user.getOperation(), owner.getOperation()}).second)
1174 adjacency.addEdge(user, owner);
1175 };
1176 // An allocated fifo's objects live in the delegate's memory, which each of
1177 // its ends then reaches as it would a buffer the delegate owns.
1178 for (auto alloc : allocates) {
1179 auto delegate =
1180 dyn_cast_or_null<TileLike>(alloc.getDelegateTile().getDefiningOp());
1181 ObjectFifoCreateOp fifo = alloc.getObjectFifo();
1182 if (!delegate || !fifo || delegate.getTileType() != AIETileType::CoreTile)
1183 continue;
1184 SmallVector<Value> ends{fifo.getProducerTile()};
1185 llvm::append_range(ends, fifo.getConsumerTiles());
1186 for (Value end : ends) {
1187 auto tile = dyn_cast_or_null<TileLike>(end.getDefiningOp());
1188 if (tile && tile.getTileType() == AIETileType::CoreTile &&
1189 tile.getOperation() != delegate.getOperation())
1190 addOnce(tile, delegate);
1191 }
1192 }
1193 for (auto consumer : logicalTiles) {
1194 if (consumer.getTileType() != AIETileType::CoreTile)
1195 continue;
1196 CoreOp core = nullptr;
1197 for (Operation *user : consumer.getResult().getUsers())
1198 if (auto c = dyn_cast<CoreOp>(user)) {
1199 core = c;
1200 break;
1201 }
1202 if (!core)
1203 continue;
1204
1205 core.getBody().walk([&](Operation *op) {
1206 for (Value operand : op->getOperands()) {
1207 BufferOp buf = traceToBuffer(operand);
1208 if (!buf)
1209 continue;
1210 auto owner = dyn_cast_or_null<TileLike>(buf.getTile().getDefiningOp());
1211 if (owner && owner.getOperation() != consumer.getOperation())
1212 addOnce(TileLike(consumer), owner);
1213 }
1214 });
1215 }
1216 return adjacency;
1217}
1218
1219Placer::Adjacency
1220Placer::buildCascadeAdjacency(ArrayRef<CascadeFlowOp> cascadeFlows) {
1221 Adjacency adjacency;
1222 for (auto cf : cascadeFlows) {
1223 TileLike src = cf.getSourceTileLike();
1224 TileLike dst = cf.getDestTileLike();
1225 if (!src || !dst)
1226 continue;
1227 adjacency.addEdge(src, dst);
1228 }
1229 return adjacency;
1230}
1231
1233Placer::buildComputePeerAdjacency(ArrayRef<ObjectFifoCreateOp> objectFifos) {
1234 Adjacency adjacency;
1235 for (auto ofOp : objectFifos) {
1236 auto producer =
1237 dyn_cast_or_null<LogicalTileOp>(ofOp.getProducerTile().getDefiningOp());
1238 if (!producer || producer.getTileType() != AIETileType::CoreTile)
1239 continue;
1240 // A broadcast always moves by DMA (dmaReason in AIEObjectFifoSplit).
1241 if (ofOp.getConsumerTiles().size() != 1 || outOfOrderNeedsDMA(ofOp))
1242 continue;
1243 for (Value consumerVal : ofOp.getConsumerTiles()) {
1244 auto consumer =
1245 dyn_cast_or_null<LogicalTileOp>(consumerVal.getDefiningOp());
1246 if (!consumer || consumer.getTileType() != AIETileType::CoreTile)
1247 continue;
1248 // Self-loops live entirely in the tile's own L1 and consume no DMA
1249 // channel, so they should not push the LTO toward a neighbor-heavy
1250 // placement.
1251 if (producer.getOperation() == consumer.getOperation())
1252 continue;
1253 adjacency.addEdge(TileLike(producer), TileLike(consumer));
1254 }
1255 }
1256 return adjacency;
1257}
1258
1260Placer::buildSharedMemAdjacency(ArrayRef<ObjectFifoCreateOp> objectFifos) {
1261 Adjacency adjacency;
1262 for (auto ofOp : objectFifos)
1263 if (ofOp.forcesSharedMemory() && ofOp.getConsumerTiles().size() == 1)
1264 adjacency.addEdgeFromValues(ofOp.getProducerTile(),
1265 ofOp.getConsumerTiles()[0]);
1266 return adjacency;
1267}
1268
1269bool SequentialPlacer::satisfiesAdjacency(
1270 LogicalTileOp logicalTile, TileID candidate, const Adjacency &adjacency,
1271 llvm::function_ref<bool(TileID firstPos, TileID secondPos)> pred) const {
1272 bool ok = true;
1273 forEachPeer(logicalTile.getOperation(), adjacency,
1274 [&](TileLike peer, bool thisIsFirst) {
1275 if (!ok)
1276 return;
1277 auto peerPos = resolvePeerPosition(peer, result);
1278 if (!peerPos)
1279 return;
1280 TileID firstPos = thisIsFirst ? candidate : *peerPos;
1281 TileID secondPos = thisIsFirst ? *peerPos : candidate;
1282 if (!pred(firstPos, secondPos))
1283 ok = false;
1284 });
1285 return ok;
1286}
1287
1288LogicalResult SequentialPlacer::enforceAdjacency(LogicalTileOp logicalTile,
1289 TileID tile,
1290 const AdjacencyKind &kind) {
1291 if (satisfiesAdjacency(logicalTile, tile, kind.adjacency, kind.pred))
1292 return success();
1293 auto diag = logicalTile.emitError()
1294 << "tile (" << tile.col << ", " << tile.row << ") violates "
1295 << kind.name << " adjacency";
1296 attachPeerNotes(diag, logicalTile, kind.adjacency, kind.peerLabel);
1297 diag.attachNote() << kind.constraintHint;
1298 return failure();
1299}
1300
1301void SequentialPlacer::recordPlacement(LogicalTileOp logicalTile, TileID tile,
1302 PlacementContext &ctx,
1303 StringRef debugLabel) {
1304 result[logicalTile] = tile;
1305 LLVM_DEBUG(llvm::dbgs() << DEBUG_TYPE << ": placed " << debugLabel << " LTO "
1306 << logicalTile.getLoc() << " at (" << tile.col << ", "
1307 << tile.row << ")\n");
1308 if (logicalTile.getTileType() == AIETileType::CoreTile) {
1309 recordColumnStreamUsage(logicalTile, tile.col);
1310 availability.removeTile(tile, AIETileType::CoreTile);
1311 ctx.reserveNeighborSlots(logicalTile.getOperation(), tile);
1312 }
1313}
1314
1315std::pair<int, int>
1316SequentialPlacer::totalComputePeers(Operation *op, const Adjacency &adjacency) {
1317 int in = 0, out = 0;
1318 // Compute-peer convention: edge.first is the producer (OUT for `op`),
1319 // edge.second is the consumer (IN for `op`).
1320 forEachPeer(op, adjacency, [&](TileLike /*peer*/, bool thisIsFirst) {
1321 if (thisIsFirst)
1322 ++out;
1323 else
1324 ++in;
1325 });
1326 return {in, out};
1327}
1328
1329bool SequentialPlacer::satisfiesComputePeer(
1330 LogicalTileOp logicalTile, TileID candidate,
1331 const Adjacency &computePeerAdjacency,
1332 const llvm::DenseMap<Operation *, int> &needNeighborIn,
1333 const llvm::DenseMap<Operation *, int> &needNeighborOut) const {
1334 // No compute-peer edges => the compute-peer DMA budget constraint is
1335 // vacuous for this LTO. (Phase 2c can still populate a non-zero
1336 // needNeighborIn/Out for a tile whose non-peer channel count exceeds
1337 // its DMA budget, but no neighbor placement can fix that and the
1338 // channel validator emits the right error later.)
1339 if (!computePeerAdjacency.hasEdges(logicalTile.getOperation()))
1340 return true;
1341
1342 // Self side: count placed peers that would land non-neighbor of
1343 // candidate, and reject if the count exceeds the slack budget.
1344 int selfNonNeighborIn = 0, selfNonNeighborOut = 0;
1345 int selfNeighborIn = 0, selfNeighborOut = 0;
1346 forEachPeer(logicalTile.getOperation(), computePeerAdjacency,
1347 [&](TileLike peer, bool thisIsFirst) {
1348 auto peerPos = resolvePeerPosition(peer, result);
1349 if (!peerPos)
1350 return;
1351 bool isNeighbor =
1352 isSharedComputeMemory(*targetModel, candidate, *peerPos);
1353 bool thisIsConsumer = !thisIsFirst;
1354 if (isNeighbor) {
1355 if (thisIsConsumer)
1356 ++selfNeighborIn;
1357 else
1358 ++selfNeighborOut;
1359 } else {
1360 if (thisIsConsumer)
1361 ++selfNonNeighborIn;
1362 else
1363 ++selfNonNeighborOut;
1364 }
1365 });
1366 auto [selfTotalIn, selfTotalOut] =
1367 totalComputePeers(logicalTile.getOperation(), computePeerAdjacency);
1368 int selfNeedIn = needNeighborIn.lookup(logicalTile.getOperation());
1369 int selfNeedOut = needNeighborOut.lookup(logicalTile.getOperation());
1370 int selfSlackIn = selfTotalIn - selfNeedIn;
1371 int selfSlackOut = selfTotalOut - selfNeedOut;
1372 if (selfNonNeighborIn > selfSlackIn)
1373 return false;
1374 if (selfNonNeighborOut > selfSlackOut)
1375 return false;
1376
1377 // Forward-look: even if no placed peer violates slack at this
1378 // candidate, future peers still to be placed must land on a physical
1379 // compute neighbor of candidate. Count candidate's free shared-L1
1380 // compute neighbors (N, S, and the checkerboard W neighbor on AIE2)
1381 // and reject if the remaining demand exceeds that count. A free slot
1382 // can host an IN-peer OR an OUT-peer (not both); the sum-check is a
1383 // conservative approximation.
1384 if (selfNeedIn > 0 || selfNeedOut > 0) {
1385 int freeNeighborSlots = 0;
1386 forEachMemAffinityNeighbor(*targetModel, candidate, [&](TileID nb) {
1387 if (availability.compTilesSet.contains(nb))
1388 ++freeNeighborSlots;
1389 });
1390 int remainingInNeed = std::max(0, selfNeedIn - selfNeighborIn);
1391 int remainingOutNeed = std::max(0, selfNeedOut - selfNeighborOut);
1392 if (remainingInNeed + remainingOutNeed > freeNeighborSlots)
1393 return false;
1394 }
1395
1396 // Symmetric side: for each already-placed compute peer p, placing
1397 // logicalTile at candidate adds one non-neighbor entry to p's count if
1398 // candidate isn't a neighbor of p. Re-derive p's count using the current
1399 // `result` plus this candidate, then check p's slack.
1400 bool symmetricOk = true;
1402 logicalTile.getOperation(), computePeerAdjacency,
1403 [&](TileLike peer, bool thisIsFirst) {
1404 if (!symmetricOk)
1405 return;
1406 auto peerLTO = dyn_cast_or_null<LogicalTileOp>(peer.getOperation());
1407 if (!peerLTO)
1408 return;
1409 auto peerPos = resolvePeerPosition(peer, result);
1410 if (!peerPos)
1411 return;
1412 // From peer's POV, logicalTile is one of peer's compute peers.
1413 // If logicalTile is the consumer here (thisIsFirst == false),
1414 // peer is the producer and logicalTile sits on peer's OUT side.
1415 bool logicalIsPeerOut = !thisIsFirst;
1416 int peerNeedDir = logicalIsPeerOut
1417 ? needNeighborOut.lookup(peerLTO.getOperation())
1418 : needNeighborIn.lookup(peerLTO.getOperation());
1419 auto [peerTotalIn, peerTotalOut] =
1420 totalComputePeers(peerLTO.getOperation(), computePeerAdjacency);
1421 int peerTotalDir = logicalIsPeerOut ? peerTotalOut : peerTotalIn;
1422 int peerSlackDir = peerTotalDir - peerNeedDir;
1423
1424 // Count peer's currently placed non-neighbor peers in this direction.
1425 int peerNonNeighborDir = 0;
1426 forEachPeer(
1427 peerLTO.getOperation(), computePeerAdjacency,
1428 [&](TileLike peerPeer, bool peerLTOIsFirst) {
1429 // peerLTO is the consumer of this edge when it sits on
1430 // edge.second (compute-peer convention is producer=first,
1431 // consumer=second). logicalIsPeerOut selects peer's OUT
1432 // edges (peer = producer), which is the opposite role.
1433 bool peerIsConsumerHere = !peerLTOIsFirst;
1434 if (logicalIsPeerOut == peerIsConsumerHere)
1435 return;
1436 if (peerPeer.getOperation() == logicalTile.getOperation()) {
1437 // For the edge currently being decided, score candidate
1438 // against peerPos.
1439 if (!isSharedComputeMemory(*targetModel, *peerPos, candidate))
1440 ++peerNonNeighborDir;
1441 return;
1442 }
1443 auto pp = resolvePeerPosition(peerPeer, result);
1444 if (!pp)
1445 return;
1446 if (!isSharedComputeMemory(*targetModel, *peerPos, *pp))
1447 ++peerNonNeighborDir;
1448 });
1449 if (peerNonNeighborDir > peerSlackDir)
1450 symmetricOk = false;
1451 });
1452 return symmetricOk;
1453}
1454
1455void SequentialPlacer::attachPeerNotes(
1456 InFlightDiagnostic &diag, LogicalTileOp logicalTile,
1457 const Adjacency &adjacency,
1458 llvm::function_ref<StringRef(bool thisIsFirst)> labelPeer) const {
1459 int unplacedCount = 0;
1460 forEachPeer(logicalTile.getOperation(), adjacency,
1461 [&](TileLike peer, bool thisIsFirst) {
1462 auto peerPos = resolvePeerPosition(peer, result);
1463 if (!peerPos) {
1464 ++unplacedCount;
1465 return;
1466 }
1467 diag.attachNote(peer.getLoc())
1468 << labelPeer(thisIsFirst) << " peer placed at ("
1469 << peerPos->col << ", " << peerPos->row << ")";
1470 });
1471 if (unplacedCount > 0)
1472 diag.attachNote() << unplacedCount
1473 << " additional peer(s) on this LTO are not yet placed "
1474 "(placed after this LTO; their positions may "
1475 "subsequently constrain or unblock this placement)";
1476}
1477
1478Placer::Adjacency
1479Placer::buildObjectFifoAdjacency(ArrayRef<ObjectFifoCreateOp> objectFifos) {
1480 Adjacency adjacency;
1481 for (auto ofOp : objectFifos)
1482 for (Value consumer : ofOp.getConsumerTiles())
1483 adjacency.addEdgeFromValues(ofOp.getProducerTile(), consumer);
1484 return adjacency;
1485}
1486
1489 ArrayRef<ObjectFifoCreateOp> objectFifos) {
1490 llvm::DenseSet<std::pair<Operation *, Value>> registered;
1491 for (auto reg : device.getOps<ObjectFifoRegisterExternalBuffersOp>())
1492 registered.insert({reg.getObjectFifo(), reg.getTile()});
1493 ShareableShimEnds ends;
1494 auto shareable = [&](ObjectFifoCreateOp fifo, Value tile,
1495 ObjectFifoEndPortAttr port) -> Operation * {
1496 auto lt = dyn_cast_or_null<LogicalTileOp>(tile.getDefiningOp());
1497 if (!lt || lt.getTileType() != AIETileType::ShimNOCTile || fifo.getPlio() ||
1498 (port && port.channelIndex()) || registered.contains({fifo, tile}))
1499 return nullptr;
1500 return lt;
1501 };
1502 for (auto fifo : objectFifos) {
1503 if (Operation *lt =
1504 shareable(fifo, fifo.getProducerTile(), fifo.getProdPortAttr()))
1505 ends[lt][1].push_back(fifo.getSymNameAttr());
1506 for (auto [i, consumer] : llvm::enumerate(fifo.getConsumerTiles()))
1507 if (Operation *lt = shareable(fifo, consumer, fifo.getConsumerPort(i)))
1508 ends[lt][0].push_back(fifo.getSymNameAttr());
1509 }
1510 for (auto endpoint : device.getOps<RouteEndpointOp>()) {
1511 auto tile = dyn_cast<LogicalTileOp>(endpoint.getTile().getDefiningOp());
1512 if (tile && tile.isShimTile() && endpoint.getBundle() == WireBundle::DMA &&
1513 !endpoint.getChannelIndex())
1514 ends[tile][endpoint.getRouteDirection() == DMAChannelDir::MM2S ? 1 : 0]
1515 .push_back(endpoint.getSymNameAttr());
1516 }
1517 return ends;
1518}
1519
1520std::optional<ShimTransferSpans::End> Placer::shimEndOf(DeviceOp device,
1521 StringAttr symbol) {
1522 if (auto endpoint = dyn_cast_or_null<RouteEndpointOp>(
1523 SymbolTable::lookupNearestSymbolFrom(device, symbol)))
1524 return ShimTransferSpans::End{symbol, endpoint.getRouteDirection() ==
1525 DMAChannelDir::MM2S};
1526 auto fifo = dyn_cast_or_null<ObjectFifoCreateOp>(
1527 SymbolTable::lookupNearestSymbolFrom(device, symbol));
1528 if (!fifo)
1529 return std::nullopt;
1530 auto producer =
1531 dyn_cast_or_null<TileLike>(fifo.getProducerTile().getDefiningOp());
1532 return ShimTransferSpans::End{symbol, producer && producer.isShimTile()};
1533}
1534
1536 ArrayRef<PacketFlowOp> pktFlows) {
1537 Adjacency adjacency;
1538 for (auto flow : flows)
1539 adjacency.addEdgeFromValues(flow.getSource(), flow.getDest());
1540
1541 for (auto pktFlow : pktFlows) {
1542 SmallVector<Value> srcs, dsts;
1543 pktFlow.walk([&](Operation *op) {
1544 if (auto src = dyn_cast<PacketSourceOp>(op))
1545 srcs.push_back(src.getTile());
1546 else if (auto dst = dyn_cast<PacketDestOp>(op))
1547 dsts.push_back(dst.getTile());
1548 });
1549 for (auto s : srcs)
1550 for (auto d : dsts)
1551 adjacency.addEdgeFromValues(s, d);
1552 }
1553 return adjacency;
1554}
1555
1556// Already-resolved TileOps have no placer-visible budget, so only flows whose
1557// endpoints are LogicalTileOps contribute to channelRequirements. A DMA
1558// channel is a hardware resource: a broadcast (one source channel feeding
1559// multiple destinations) consumes one MM2S channel on the producer regardless
1560// of the number of `aie.flow` ops it lowers to, and a merge (multiple sources
1561// landing on one destination channel) consumes one S2MM channel on the
1562// consumer. Dedup by (tile, channel) so the producer-of-broadcast and
1563// consumer-of-merge sides are each counted once. An `aie.route` end whose
1564// channel allocation has yet to pick holds a channel of its own.
1565void SequentialPlacer::addChannelRequirementsFromFlows(
1566 ArrayRef<FlowOp> flows, ArrayRef<PacketFlowOp> pktFlows,
1567 ArrayRef<RouteOp> routes,
1568 llvm::DenseMap<Operation *, std::pair<int, int>> &channelRequirements) {
1569
1570 llvm::DenseSet<std::tuple<Operation *, int>> seenSrc, seenDst;
1571 namedChannels.clear();
1572
1573 auto incIfDMA = [&](Operation *tileOp, WireBundle bundle,
1574 std::optional<int> channel, bool isOutput) {
1575 if (!tileOp || !isa<LogicalTileOp>(tileOp))
1576 return;
1577 if (bundle != WireBundle::DMA)
1578 return;
1579 auto &seen = isOutput ? seenSrc : seenDst;
1580 if (channel && !seen.insert({tileOp, *channel}).second)
1581 return;
1582 if (channel)
1583 namedChannels[tileOp].push_back({isOutput, *channel});
1584 if (isOutput)
1585 channelRequirements[tileOp].second++;
1586 else
1587 channelRequirements[tileOp].first++;
1588 };
1589
1590 for (auto flow : flows) {
1591 incIfDMA(flow.getSource().getDefiningOp(), flow.getSourceBundle(),
1592 flow.getSourceChannel(), /*isOutput=*/true);
1593 incIfDMA(flow.getDest().getDefiningOp(), flow.getDestBundle(),
1594 flow.getDestChannel(), /*isOutput=*/false);
1595 }
1596
1597 for (auto pktFlow : pktFlows) {
1598 pktFlow.walk([&](Operation *op) {
1599 if (auto src = dyn_cast<PacketSourceOp>(op)) {
1600 incIfDMA(src.getTile().getDefiningOp(), src.getBundle(),
1601 src.getChannel(), /*isOutput=*/true);
1602 } else if (auto dst = dyn_cast<PacketDestOp>(op)) {
1603 incIfDMA(dst.getTile().getDefiningOp(), dst.getBundle(),
1604 dst.getChannel(), /*isOutput=*/false);
1605 }
1606 });
1607 }
1608
1609 // Packet routes may share an endpoint, which holds one channel however many
1610 // routes name it, and a fan-in's sources share one destination channel.
1611 llvm::DenseSet<Operation *> countedEndpoints;
1612 for (auto route : routes) {
1613 auto count = [&](StringRef name, bool isOutput) {
1614 auto endpoint = SymbolTable::lookupNearestSymbolFrom<RouteEndpoint>(
1615 route, StringAttr::get(route.getContext(), name));
1616 if (endpoint && countedEndpoints.insert(endpoint).second)
1617 incIfDMA(endpoint.getTile().getDefiningOp(), endpoint.getRouteBundle(),
1618 endpoint.getRouteChannel(), isOutput);
1619 };
1620 for (StringRef source : route.getSourceNames())
1621 count(source, /*isOutput=*/true);
1622 for (StringRef dest : route.getDestinationNames())
1623 count(dest, /*isOutput=*/false);
1624 }
1625}
1626
1627SequentialPlacer::FlowMembership SequentialPlacer::buildFlowMembership(
1628 ArrayRef<FlowOp> flows, ArrayRef<PacketFlowOp> pktFlows,
1629 ArrayRef<RouteOp> routes, ArrayRef<ObjectFifoCreateOp> objectFifos) {
1630 // packet_flow connectivity is sources x destinations -- destinations
1631 // are never each other's peers. Same asymmetry for route and objectfifo
1632 // producer/consumers.
1633 FlowMembership idx;
1634 auto isLto = [](Value v) {
1635 return v && mlir::isa_and_nonnull<LogicalTileOp>(v.getDefiningOp());
1636 };
1637 auto addEntry = [&](Value lto, ArrayRef<Value> peers) {
1638 if (!isLto(lto) || peers.empty())
1639 return;
1640 idx.ltoFlows[lto].push_back(SmallVector<Value>(peers.begin(), peers.end()));
1641 };
1642
1643 for (auto flow : flows) {
1644 Value s = flow.getSource(), d = flow.getDest();
1645 addEntry(s, {d});
1646 addEntry(d, {s});
1647 }
1648 for (auto pf : pktFlows) {
1649 SmallVector<Value> srcs, dsts;
1650 pf.walk([&](Operation *op) {
1651 if (auto src = dyn_cast<PacketSourceOp>(op))
1652 srcs.push_back(src.getTile());
1653 else if (auto dst = dyn_cast<PacketDestOp>(op))
1654 dsts.push_back(dst.getTile());
1655 });
1656 for (Value s : srcs)
1657 addEntry(s, dsts);
1658 for (Value d : dsts)
1659 addEntry(d, srcs);
1660 }
1661 for (auto route : routes) {
1662 auto tileOf = [&](StringRef name) -> Value {
1663 auto endpoint = SymbolTable::lookupNearestSymbolFrom<RouteEndpoint>(
1664 route, StringAttr::get(route.getContext(), name));
1665 return endpoint ? endpoint.getTile() : Value();
1666 };
1667 SmallVector<Value> srcs, dsts;
1668 for (StringRef source : route.getSourceNames())
1669 if (Value tile = tileOf(source))
1670 srcs.push_back(tile);
1671 for (StringRef dest : route.getDestinationNames())
1672 if (Value tile = tileOf(dest))
1673 dsts.push_back(tile);
1674 for (Value s : srcs)
1675 addEntry(s, dsts);
1676 for (Value d : dsts)
1677 addEntry(d, srcs);
1678 }
1679 for (auto of : objectFifos) {
1680 Value prod = of.getProducerTile();
1681 auto cons = of.getConsumerTiles();
1682 SmallVector<Value> consVec(cons.begin(), cons.end());
1683 addEntry(prod, consVec);
1684 for (Value c : consVec)
1685 addEntry(c, {prod});
1686 }
1687 return idx;
1688}
1689
1690std::optional<int>
1691SequentialPlacer::computeCentroidColumn(LogicalTileOp logicalTile,
1692 const FlowMembership &flowIndex) {
1693 // Per-flow routing cost as a function of the LTO's column S:
1694 // single-dest flow to col D : cost(S) = |S - D|
1695 // N-dest flow spanning [lo, hi]:
1696 // cost(S) = 0 if lo <= S <= hi
1697 // = lo - S if S < lo
1698 // = S - hi if S > hi
1699 // Inside a broadcast's span the cost is flat -- broadcasts constrain
1700 // the LTO to stay within their reach but don't pull it to any column.
1701 // Tiebreak by closest-to-mean-of-dests so a pure broadcast lands mid
1702 // and two single-dest flows average to their midpoint.
1703 Value ltoVal = logicalTile.getResult();
1704 auto ltoIt = flowIndex.ltoFlows.find(ltoVal);
1705 if (ltoIt == flowIndex.ltoFlows.end())
1706 return std::nullopt;
1707
1708 // Column of a flow endpoint, or nullopt if not yet fixed. A memtile that
1709 // already knows its column answers here; looking past it to its cores
1710 // would pick up the other flows it carries.
1711 auto resolvePeerCol = [&](Value v) -> std::optional<int> {
1712 Operation *defOp = v.getDefiningOp();
1713 if (!defOp)
1714 return std::nullopt;
1715 // A physical tile's column is fixed by construction, whatever its type.
1716 if (auto tileOp = dyn_cast<TileOp>(defOp))
1717 return tileOp.getCol();
1718 auto lto = dyn_cast<LogicalTileOp>(defOp);
1719 if (!lto)
1720 return std::nullopt;
1721 if (auto c = lto.tryGetCol())
1722 return c;
1723 auto it = result.find(defOp);
1724 if (it != result.end())
1725 return it->second.col;
1726 return std::nullopt;
1727 };
1728
1729 // One level of indirection for shim->memtile->core, used only when the
1730 // memtile's own column is still unknown. Multi-level memtile chains aren't
1731 // currently produced by mlir-aie.
1732 auto resolveLtoCoreCols = [&](Value v) {
1733 SmallVector<int> out;
1734 auto it = flowIndex.ltoFlows.find(v);
1735 if (it == flowIndex.ltoFlows.end())
1736 return out;
1737 for (auto &peerList : it->second)
1738 for (Value p : peerList)
1739 if (auto col = resolvePeerCol(p))
1740 out.push_back(*col);
1741 return out;
1742 };
1743
1744 SmallVector<SmallVector<int>> perFlowDests;
1745 for (auto &peers : ltoIt->second) {
1746 SmallVector<int> dests;
1747 for (Value p : peers) {
1748 if (auto col = resolvePeerCol(p)) {
1749 dests.push_back(*col);
1750 continue;
1751 }
1752 if (p == ltoVal)
1753 continue;
1754 for (int c : resolveLtoCoreCols(p))
1755 dests.push_back(c);
1756 }
1757 if (!dests.empty())
1758 perFlowDests.push_back(std::move(dests));
1759 }
1760
1761 if (perFlowDests.empty())
1762 return std::nullopt;
1763
1764 double meanDestCol = 0.0;
1765 int destCount = 0;
1766 for (auto &dests : perFlowDests)
1767 for (int d : dests) {
1768 meanDestCol += d;
1769 ++destCount;
1770 }
1771 meanDestCol /= destCount;
1772
1773 int numCols = targetModel->columns();
1774 int bestCol = 0;
1775 int bestCost = std::numeric_limits<int>::max();
1776 double bestTiebreak = std::numeric_limits<double>::infinity();
1777 for (int S = 0; S < numCols; ++S) {
1778 int cost = 0;
1779 for (auto &dests : perFlowDests) {
1780 if (dests.size() == 1) {
1781 cost += std::abs(S - dests[0]);
1782 } else {
1783 int lo = *llvm::min_element(dests);
1784 int hi = *llvm::max_element(dests);
1785 if (S < lo)
1786 cost += lo - S;
1787 else if (S > hi)
1788 cost += S - hi;
1789 }
1790 }
1791 double tiebreak = std::abs(S - meanDestCol);
1792 if (cost < bestCost || (cost == bestCost && tiebreak < bestTiebreak)) {
1793 bestCost = cost;
1794 bestTiebreak = tiebreak;
1795 bestCol = S;
1796 }
1797 }
1798 return bestCol;
1799}
1800
1801LogicalResult SequentialPlacer::placeNonCoreTileByCentroid(
1802 LogicalTileOp logicalTile, const FlowMembership &flowIndex,
1803 const llvm::DenseMap<Operation *, std::pair<int, int>>
1804 &channelRequirements) {
1805 std::optional<int> centroidCol =
1806 computeCentroidColumn(logicalTile, flowIndex);
1807 auto colConstraint = logicalTile.tryGetCol();
1808
1809 // With spread-unanchored-tiles off, an LTO with no CoreTile peer targets
1810 // column 0, which ranks every candidate at the same distance and so pins the
1811 // whole design to one column until it fills up.
1812 std::optional<int> targetCol = colConstraint ? colConstraint : centroidCol;
1813 if (!targetCol && !spreadUnanchoredTiles)
1814 targetCol = 0;
1815
1816 auto [numInputChannels, numOutputChannels] =
1817 channelRequirements.lookup(logicalTile.getOperation());
1818
1819 auto maybeTile = findTileWithCapacity(targetCol, availability.nonCompTiles,
1820 numInputChannels, numOutputChannels,
1821 logicalTile.getTileType(), logicalTile,
1822 /*withinBudgets=*/true);
1823 if (!maybeTile)
1824 maybeTile = findTileWithCapacity(targetCol, availability.nonCompTiles,
1825 numInputChannels, numOutputChannels,
1826 logicalTile.getTileType(), logicalTile,
1827 /*withinBudgets=*/false);
1828 // Exhausted shim tiles are no longer in availability, so look at them all.
1829 if (!maybeTile && mergeLogicalTiles &&
1830 logicalTile.getTileType() == AIETileType::ShimNOCTile) {
1831 std::optional<std::pair<int, int>> bestKey;
1832 for (int col = 0, numCols = targetModel->columns(); col < numCols; ++col) {
1833 TileID tile{col, 0};
1834 if ((colConstraint && col != *colConstraint) ||
1835 !fitsSharingShim(tile, logicalTile,
1836 {numInputChannels, numOutputChannels}))
1837 continue;
1838 std::pair<int, int> key{targetCol ? std::abs(col - *targetCol) : 0, col};
1839 if (!bestKey || key < *bestKey) {
1840 bestKey = key;
1841 maybeTile = tile;
1842 }
1843 }
1844 }
1845 if (!maybeTile) {
1846 AIETileType requestedType = logicalTile.getTileType();
1847 StringRef tileTypeName = stringifyAIETileType(requestedType);
1848 auto diag = logicalTile.emitError();
1849 CapacityDiagnosis diagnosis = diagnoseCapacityExhaustion(
1850 requestedType, numInputChannels, numOutputChannels);
1851 if (!diagnosis.capacityExistsSomewhere) {
1852 diag << "no " << tileTypeName << " on the device has " << numInputChannels
1853 << " input/" << numOutputChannels
1854 << " output DMA channel(s) free: all " << diagnosis.tilesOfType
1855 << " " << tileTypeName << "(s) are at " << diagnosis.inUsed << "/"
1856 << diagnosis.inMax << " input, " << diagnosis.outUsed << "/"
1857 << diagnosis.outMax << " output channels used";
1858 diag.attachNote() << "this is the device's total " << tileTypeName
1859 << " DMA budget, not a placement choice -- no column "
1860 "has spare capacity to pin to";
1861 int shortIn = diagnosis.closestShortIn;
1862 int shortOut = diagnosis.closestShortOut;
1863 auto &remedy = diag.attachNote();
1864 remedy << "the closest " << tileTypeName << " is ";
1865 if (shortIn > 0 && shortOut > 0)
1866 remedy << "short " << shortIn << " input/" << shortOut
1867 << " output channel(s); ";
1868 else if (shortIn > 0)
1869 remedy << "short " << shortIn << " input channel(s); ";
1870 else if (shortOut > 0)
1871 remedy << "short " << shortOut << " output channel(s); ";
1872 remedy << "fan traffic through a MemTile via aie.objectfifo.link, or "
1873 "reduce the number of DMA-attached ObjectFIFOs feeding this "
1874 "tile";
1875 } else {
1876 diag << "no " << tileTypeName << " has sufficient DMA capacity for "
1877 << numInputChannels << " input/" << numOutputChannels
1878 << " output channels ";
1879 if (colConstraint)
1880 diag << "at column " << *colConstraint;
1881 else if (centroidCol)
1882 diag << "near centroid column " << *centroidCol;
1883 else
1884 diag << "on any column";
1885 diag.attachNote() << "every " << tileTypeName
1886 << " with spare DMA capacity already hosts a "
1887 "different logical tile (merge-logical-tiles is "
1888 "off); enable merging, or pin logical tiles "
1889 "one-per-column if fewer of them than physical "
1890 << tileTypeName << "s remain unclaimed";
1891 }
1892 return failure();
1893 }
1894
1895 result[logicalTile] = *maybeTile;
1896 LLVM_DEBUG({
1897 auto showCol = [](std::optional<int> c) {
1898 return c ? std::to_string(*c) : std::string("none");
1899 };
1900 llvm::dbgs() << DEBUG_TYPE << ": placed non-core LTO "
1901 << logicalTile.getLoc() << " at (" << maybeTile->col << ", "
1902 << maybeTile->row << ") (centroid col=" << showCol(centroidCol)
1903 << ", target col=" << showCol(targetCol) << ")\n";
1904 });
1905 if (!mergeLogicalTiles)
1906 assignedNonCoreTiles.insert(*maybeTile);
1907 if (numInputChannels > 0)
1908 updateChannelUsage(*maybeTile, DmaDir::In, numInputChannels);
1909 if (numOutputChannels > 0)
1910 updateChannelUsage(*maybeTile, DmaDir::Out, numOutputChannels);
1911 recordStreamAndBdUsage(logicalTile, *maybeTile);
1912 recordShimEnds(*maybeTile, logicalTile);
1913 return success();
1914}
1915
1916bool SequentialPlacer::fitsSharingShim(TileID tile, Operation *logicalTile,
1917 std::pair<int, int> required) {
1918 if (targetModel->getTileType(tile.col, tile.row) !=
1919 AIETileType::ShimNOCTile ||
1920 !shimSpans)
1921 return false;
1922 auto [maxIn, maxOut] = getDMACapacity(*targetModel, tile);
1923 int used[2] = {availability.inputChannelsUsed[tile],
1924 availability.outputChannelsUsed[tile]};
1925 int need[2] = {required.first, required.second};
1926 int max[2] = {maxIn, maxOut};
1927 auto own = shareableShimEnds.lookup(logicalTile);
1928 for (int dir : {0, 1}) {
1929 if (used[dir] + need[dir] <= max[dir])
1930 continue;
1931 SmallVector<StringAttr> ends(shimEndsAt[tile][dir]);
1932 llvm::append_range(ends, own[dir]);
1933 int fixed = used[dir] + need[dir] - static_cast<int>(ends.size());
1934 if (fixed + static_cast<int>(shimSpans->groups(ends).size()) > max[dir])
1935 return false;
1936 }
1937 return true;
1938}
1939
1940void SequentialPlacer::recordShimEnds(TileID tile, Operation *logicalTile) {
1941 auto own = shareableShimEnds.find(logicalTile);
1942 if (own == shareableShimEnds.end())
1943 return;
1944 for (int dir : {0, 1})
1945 llvm::append_range(shimEndsAt[tile][dir], own->second[dir]);
1946}
1947
1948std::optional<TileID> SequentialPlacer::findTileWithCapacity(
1949 std::optional<int> targetCol, llvm::ArrayRef<TileID> tiles,
1950 int requiredInputChannels, int requiredOutputChannels,
1951 AIETileType requestedType, Operation *logicalTile, bool withinBudgets) {
1952 // Choose a physical tile by lexicographic minimum of
1953 // (|col - targetCol| + broadcast pressure, current load, col, row).
1954 // A memtile's extra broadcast destinations penalize sharing its bandwidth,
1955 // even when an idle tile is slightly farther from the centroid. Without
1956 // broadcasts, and for shims or explicit column hints, distance still leads.
1957 // (col, row) is the final tiebreaker for determinism.
1958 //
1959 // Without a targetCol there is no routing anchor to measure against, so the
1960 // distance term is dropped and load leads. Load is then counted only in the
1961 // directions this LTO actually needs, matching hasAvailableChannels, which
1962 // already filters per direction: summing both would push a chain's read and
1963 // write shims onto different columns and need 2N columns for N chains.
1964 std::optional<TileID> best;
1965 std::tuple<int, int, int, int> bestKey;
1966
1967 for (auto &tile : tiles) {
1968 if (targetModel->getTileType(tile.col, tile.row) != requestedType)
1969 continue;
1970 if (!hasAvailableChannels(tile, requiredInputChannels,
1971 requiredOutputChannels))
1972 continue;
1973 // When merge-logical-tiles is disabled, a tile that already hosts a
1974 // non-core aie.logical_tile is off-limits even if it has spare DMA
1975 // capacity.
1976 if (!mergeLogicalTiles && assignedNonCoreTiles.contains(tile))
1977 continue;
1978 if (withinBudgets && !fitsStreamAndBdBudgets(logicalTile, tile))
1979 continue;
1980 if (llvm::any_of(namedChannels.lookup(logicalTile), [&](auto named) {
1981 return availability.namedChannelsUsed[tile].contains(named);
1982 }))
1983 continue;
1984 int dist = targetCol ? std::abs(tile.col - *targetCol) : 0;
1985 if (requestedType == AIETileType::MemTile &&
1986 !cast<LogicalTileOp>(logicalTile).tryGetCol())
1987 dist += broadcastFanoutUsed.lookup(tile);
1988 int load;
1989 if (targetCol ||
1990 (requiredInputChannels == 0 && requiredOutputChannels == 0)) {
1991 load = availability.inputChannelsUsed[tile] +
1992 availability.outputChannelsUsed[tile];
1993 } else {
1994 load = 0;
1995 if (requiredInputChannels > 0)
1996 load += availability.inputChannelsUsed[tile];
1997 if (requiredOutputChannels > 0)
1998 load += availability.outputChannelsUsed[tile];
1999 }
2000 std::tuple<int, int, int, int> key{dist, load, tile.col, tile.row};
2001 if (!best || key < bestKey) {
2002 best = tile;
2003 bestKey = key;
2004 }
2005 }
2006
2007 return best;
2008}
2009
2010SequentialPlacer::CapacityDiagnosis
2011SequentialPlacer::diagnoseCapacityExhaustion(AIETileType requestedType,
2012 int requiredInputChannels,
2013 int requiredOutputChannels) {
2014 // Walk the device directly rather than `availability.nonCompTiles`: a
2015 // fully-exhausted tile is meant to stay enumerable here even if a future
2016 // fix makes `updateChannelUsage`'s prune (see its `removeTile` call)
2017 // actually fire, which today it structurally cannot.
2018 CapacityDiagnosis diagnosis;
2019 for (int col = 0, numCols = targetModel->columns(); col < numCols; ++col) {
2020 for (int row = 0, numRows = targetModel->rows(); row < numRows; ++row) {
2021 if (targetModel->getTileType(col, row) != requestedType)
2022 continue;
2023 TileID tile{col, row};
2024 ++diagnosis.tilesOfType;
2025 auto [maxIn, maxOut] = getDMACapacity(*targetModel, tile);
2026 diagnosis.inMax += maxIn;
2027 diagnosis.outMax += maxOut;
2028 int usedIn = availability.inputChannelsUsed.lookup(tile);
2029 int usedOut = availability.outputChannelsUsed.lookup(tile);
2030 diagnosis.inUsed += usedIn;
2031 diagnosis.outUsed += usedOut;
2032 int shortIn = std::max(0, requiredInputChannels - (maxIn - usedIn));
2033 int shortOut = std::max(0, requiredOutputChannels - (maxOut - usedOut));
2034 if (diagnosis.tilesOfType == 1 ||
2035 shortIn + shortOut <
2036 diagnosis.closestShortIn + diagnosis.closestShortOut) {
2037 diagnosis.closestShortIn = shortIn;
2038 diagnosis.closestShortOut = shortOut;
2039 }
2040 if (hasAvailableChannels(tile, requiredInputChannels,
2041 requiredOutputChannels))
2042 diagnosis.capacityExistsSomewhere = true;
2043 }
2044 }
2045 return diagnosis;
2046}
2047
2048void SequentialPlacer::updateChannelUsage(TileID tile, DmaDir direction,
2049 int numChannels) {
2050 if (direction == DmaDir::Out)
2051 availability.outputChannelsUsed[tile] += numChannels;
2052 else
2053 availability.inputChannelsUsed[tile] += numChannels;
2054
2055 if (!hasAvailableChannels(tile, 0, 0)) {
2056 AIETileType type = targetModel->getTileType(tile.col, tile.row);
2057 availability.removeTile(tile, type);
2058 }
2059}
2060
2061bool SequentialPlacer::hasAvailableChannels(TileID tile, int inputChannels,
2062 int outputChannels) {
2063 auto [maxIn, maxOut] = getDMACapacity(*targetModel, tile);
2064
2065 int currentIn = availability.inputChannelsUsed[tile];
2066 int currentOut = availability.outputChannelsUsed[tile];
2067
2068 return (currentIn + inputChannels <= maxIn) &&
2069 (currentOut + outputChannels <= maxOut);
2070}
2071
2072void SequentialPlacer::recordColumnStreamUsage(Operation *tile, int col) {
2073 auto &incoming = columnIncomingFifos[col];
2074 for (Operation *fifo : incomingFifos.lookup(tile))
2075 incoming.insert(fifo);
2076 auto &used = columnStreamsUsed[col];
2077 used += streamDemand.lookup(tile);
2078 used.southIn = incoming.size();
2079}
2080
2081void SequentialPlacer::buildStreamDemand(
2082 ArrayRef<ObjectFifoCreateOp> objectFifos) {
2083 streamDemand.clear();
2084 columnStreamsUsed.clear();
2085 incomingFifos.clear();
2086 columnIncomingFifos.clear();
2087 broadcastFanout.clear();
2088 broadcastFanoutUsed.clear();
2089 // Rows rise from shims through memtiles to cores, so a stream to a higher
2090 // rank leaves through the North port and arrives through the South port.
2091 auto rank = [](Operation *op) -> std::optional<int> {
2092 auto tile = dyn_cast_or_null<TileLike>(op);
2093 if (!tile)
2094 return std::nullopt;
2095 if (tile.isShimTile())
2096 return 0;
2097 return tile.isMemTile() ? 1 : 2;
2098 };
2099 for (auto of : objectFifos) {
2100 Operation *producer = of.getProducerTile().getDefiningOp();
2101 std::optional<int> producerRank = rank(producer);
2102 if (!producerRank)
2103 continue;
2104 if (*producerRank == 1)
2105 broadcastFanout[producer] +=
2106 std::max(0, static_cast<int>(of.getConsumerTiles().size()) - 1);
2107 bool north = false, south = false;
2108 for (Value consumerTile : of.getConsumerTiles()) {
2109 Operation *consumer = consumerTile.getDefiningOp();
2110 std::optional<int> consumerRank = rank(consumer);
2111 if (!consumerRank || *consumerRank == *producerRank)
2112 continue;
2113 if (*consumerRank > *producerRank) {
2114 north = true;
2115 streamDemand[consumer].southIn++;
2116 if (*consumerRank == 2)
2117 incomingFifos[consumer].push_back(of.getOperation());
2118 } else {
2119 south = true;
2120 streamDemand[consumer].northIn++;
2121 }
2122 }
2123 if (north)
2124 streamDemand[producer].northOut++;
2125 if (south)
2126 streamDemand[producer].southOut++;
2127 }
2128 for (auto &[op, demand] : streamDemand)
2129 if (auto tile = dyn_cast<TileOp>(op))
2130 if (tile.isCoreTile())
2131 recordColumnStreamUsage(op, tile.getCol());
2132 for (auto &[op, fanout] : broadcastFanout)
2133 if (auto tile = dyn_cast<TileOp>(op))
2134 broadcastFanoutUsed[{tile.getCol(), tile.getRow()}] += fanout;
2135}
2136
2137void SequentialPlacer::buildRuntimeBdProfiles(
2138 DeviceOp device, ArrayRef<ObjectFifoCreateOp> objectFifos) {
2139 llvm::StringMap<Operation *> shimOfFifo;
2140 for (auto of : objectFifos) {
2141 SmallVector<Value> ends{of.getProducerTile()};
2142 llvm::append_range(ends, of.getConsumerTiles());
2143 for (Value end : ends) {
2144 auto lt = dyn_cast_or_null<LogicalTileOp>(end.getDefiningOp());
2145 if (lt && lt.getTileType() == AIETileType::ShimNOCTile)
2146 shimOfFifo[of.getSymName()] = lt;
2147 }
2148 }
2149 if (shimOfFifo.empty())
2150 return;
2151 // How a descriptor splits depends on the tile type, not on its column.
2152 auto shimTile = llvm::find_if(availability.nonCompTiles, [&](TileID t) {
2153 return targetModel->getTileType(t.col, t.row) == AIETileType::ShimNOCTile;
2154 });
2155 if (shimTile == availability.nonCompTiles.end())
2156 return;
2157
2158 // (shim LTO, change in live BDs) in program order; a task holds its BDs from
2159 // configure to await or free, else to the end of its sequence.
2160 SmallVector<std::pair<Operation *, int>> events;
2161 device.walk([&](RuntimeSequenceOp seq) {
2162 llvm::MapVector<Value, std::pair<Operation *, int>> live;
2163 seq.walk([&](Operation *op) {
2164 if (auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(op)) {
2165 Operation *shim =
2166 shimOfFifo.lookup(task.getAlloc().getRootReference().getValue());
2167 if (!shim)
2168 return;
2169 int bds = 0;
2170 task.getBody().walk([&](DMABDOp bd) {
2171 auto plan = AIEX::decomposeTaskBdPattern(
2172 bd, *targetModel, shimTile->col, shimTile->row);
2173 bds += succeeded(plan) && !plan->pieces.empty() ? plan->pieces.size()
2174 : 1;
2175 });
2176 live[task.getResult()] = {shim, bds};
2177 events.push_back({shim, bds});
2178 return;
2179 }
2180 Value released;
2181 if (auto await = dyn_cast<AIEX::DMAAwaitTaskOp>(op))
2182 released = await.getTask();
2183 else if (auto free = dyn_cast<AIEX::DMAFreeTaskOp>(op))
2184 released = free.getTask();
2185 auto *it = live.find(released);
2186 if (it == live.end())
2187 return;
2188 events.push_back({it->second.first, -it->second.second});
2189 live.erase(it);
2190 });
2191 for (auto &[task, held] : live)
2192 events.push_back({held.first, -held.second});
2193 });
2194
2195 llvm::MapVector<Operation *, int> running;
2196 for (auto &[shim, delta] : events)
2197 if (running.insert({shim, 0}).second)
2198 runtimeBdProfiles[shim].assign(events.size(), 0);
2199 for (auto [idx, event] : llvm::enumerate(events)) {
2200 running[event.first] += event.second;
2201 for (auto &[shim, n] : running)
2202 runtimeBdProfiles[shim][idx] = n;
2203 }
2204}
2205
2206bool SequentialPlacer::fitsStreamAndBdBudgets(Operation *logicalTile,
2207 TileID tile) const {
2208 // Only the ports this LTO adds to: a tile a fallback already overfilled
2209 // stays open to LTOs that use none of its streams.
2210 StreamDemand demand = streamDemand.lookup(logicalTile);
2211 StreamDemand streams = availability.streamsUsed.lookup(tile);
2212 streams += demand;
2213 const AIETargetModel &tm = *targetModel;
2214 if ((demand.northIn &&
2215 streams.northIn > static_cast<int>(tm.getNumSourceSwitchboxConnections(
2216 tile.col, tile.row, WireBundle::North))) ||
2217 (demand.southIn &&
2218 streams.southIn > static_cast<int>(tm.getNumSourceSwitchboxConnections(
2219 tile.col, tile.row, WireBundle::South))) ||
2220 (demand.northOut &&
2221 streams.northOut > static_cast<int>(tm.getNumDestSwitchboxConnections(
2222 tile.col, tile.row, WireBundle::North))) ||
2223 (demand.southOut &&
2224 streams.southOut > static_cast<int>(tm.getNumDestSwitchboxConnections(
2225 tile.col, tile.row, WireBundle::South))))
2226 return false;
2227
2228 auto profile = runtimeBdProfiles.find(logicalTile);
2229 if (profile == runtimeBdProfiles.end())
2230 return true;
2231 SmallVector<int> used = availability.runtimeBdsUsed.lookup(tile);
2232 used.resize(profile->second.size(), 0);
2233 int numBDs = tm.getNumBDs(tile.col, tile.row);
2234 return llvm::all_of(llvm::zip(profile->second, used), [&](auto live) {
2235 return !std::get<0>(live) ||
2236 std::get<0>(live) + std::get<1>(live) <= numBDs;
2237 });
2238}
2239
2240void SequentialPlacer::recordStreamAndBdUsage(Operation *logicalTile,
2241 TileID tile) {
2242 broadcastFanoutUsed[tile] += broadcastFanout.lookup(logicalTile);
2243 for (auto named : namedChannels.lookup(logicalTile))
2244 availability.namedChannelsUsed[tile].insert(named);
2245 if (auto it = streamDemand.find(logicalTile); it != streamDemand.end())
2246 availability.streamsUsed[tile] += it->second;
2247 auto profile = runtimeBdProfiles.find(logicalTile);
2248 if (profile == runtimeBdProfiles.end())
2249 return;
2250 SmallVector<int> &used = availability.runtimeBdsUsed[tile];
2251 used.resize(profile->second.size(), 0);
2252 for (auto [idx, live] : llvm::enumerate(profile->second))
2253 used[idx] += live;
2254}
2255
2256void TileAvailability::removeTile(TileID tile, AIETileType type) {
2257 auto removeFromVector = [&](std::vector<TileID> &vec) {
2258 vec.erase(std::remove(vec.begin(), vec.end(), tile), vec.end());
2259 };
2260
2261 switch (type) {
2262 case AIETileType::CoreTile:
2263 removeFromVector(compTiles);
2264 compTilesSet.erase(tile);
2265 break;
2266 case AIETileType::MemTile:
2267 case AIETileType::ShimNOCTile:
2268 case AIETileType::ShimPLTile:
2269 removeFromVector(nonCompTiles);
2270 break;
2271 }
2272}
#define DEBUG_TYPE
virtual AIETileType getTileType(int col, int row) const =0
Return the tile type for the given tile coordinates.
virtual int rows() const =0
Return the number of rows in the device.
virtual uint32_t getNumBDs(AIETileType tileType) const =0
Return the number of buffer descriptors for a given tile type.
virtual int columns() const =0
Return the number of columns in the device.
virtual uint32_t getNumDestSwitchboxConnections(int col, int row, WireBundle bundle) const =0
Return the number of destinations of connections inside a switchbox.
virtual uint32_t getNumSourceSwitchboxConnections(int col, int row, WireBundle bundle) const =0
Return the number of sources of connections inside a switchbox.
mlir::LogicalResult read(llvm::ArrayRef< ObjectFifoCoreEndpointOp > ends, std::string &reason)
Reads the order off the program of the core working through ends, all draining (the channel fills the...
bool keepsUp(llvm::ArrayRef< int64_t > depths)
Whether a channel moving objects in turns() order forever, through pools of depths,...
llvm::DenseMap< mlir::Operation *, std::array< llvm::SmallVector< mlir::StringAttr >, 2 > > ShareableShimEnds
Definition AIEPlacer.h:165
static Adjacency buildCascadeAdjacency(llvm::ArrayRef< CascadeFlowOp > cascadeFlows)
static bool satisfiesConstraints(mlir::Operation *tile, TileID pos)
static Adjacency buildSharedMemAdjacency(llvm::ArrayRef< ObjectFifoCreateOp > objectFifos)
static Adjacency buildObjectFifoAdjacency(llvm::ArrayRef< ObjectFifoCreateOp > objectFifos)
static Adjacency buildFlowAdjacency(llvm::ArrayRef< FlowOp > flows, llvm::ArrayRef< PacketFlowOp > pktFlows)
const AIETargetModel * targetModel
Definition AIEPlacer.h:180
virtual void initialize(const AIETargetModel &tm)
Definition AIEPlacer.cpp:74
static Adjacency buildComputePeerAdjacency(llvm::ArrayRef< ObjectFifoCreateOp > objectFifos)
llvm::SmallVector< AllocateInfo > allocates
Definition AIEPlacer.h:179
PlacementResult result
Definition AIEPlacer.h:178
TileAvailability availability
Definition AIEPlacer.h:181
static ShareableShimEnds collectShareableShimEnds(DeviceOp device, llvm::ArrayRef< ObjectFifoCreateOp > objectFifos)
static CollectedOps collectOperations(DeviceOp device)
Definition AIEPlacer.cpp:96
static std::optional< ShimTransferSpans::End > shimEndOf(DeviceOp device, mlir::StringAttr symbol)
void initialize(const AIETargetModel &targetModel) override
mlir::LogicalResult place(DeviceOp device) override
mlir::FailureOr< TaskBdDecomposition > decomposeTaskBdPattern(xilinx::AIE::DMABDOp bd, const xilinx::AIE::AIETargetModel &targetModel, int tileCol, int tileRow)
Plan the split of bd, a runtime task's descriptor, on tile (tileCol, tileRow).
void forEachPeer(mlir::Operation *op, const Placer::Adjacency &adjacency, F &&fn)
Definition AIEPlacer.h:188
llvm::DenseMap< mlir::Operation *, TileID > PlacementResult
Definition AIEPlacer.h:46
void forEachMemAffinityNeighbor(const AIETargetModel &targetModel, TileID at, F &&fn)
Definition AIEPlacer.h:217
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
std::pair< int, int > getDMACapacity(const AIETargetModel &tm, TileID tile)
Get DMA channel capacity (maxIn, maxOut) for a tile position: its S2MM and MM2S channel counts.
Definition AIEPlacer.h:26
void addEdge(TileLike first, TileLike second)
Definition AIEPlacer.cpp:54
llvm::DenseMap< mlir::Operation *, llvm::SmallVector< unsigned, 2 > > tileToEdges
Definition AIEPlacer.h:106
void addEdgeFromValues(mlir::Value a, mlir::Value b)
Definition AIEPlacer.cpp:65
llvm::SmallVector< std::pair< TileLike, TileLike >, 4 > edges
Definition AIEPlacer.h:104
llvm::SmallVector< RouteOp > routes
Definition AIEPlacer.h:128
llvm::SmallVector< ObjectFifoLinkOp > objectFifoLinks
Definition AIEPlacer.h:123
llvm::SmallVector< CascadeFlowOp > cascadeFlows
Definition AIEPlacer.h:125
llvm::SmallVector< PacketFlowOp > pktFlows
Definition AIEPlacer.h:127
llvm::SmallVector< FlowOp > flows
Definition AIEPlacer.h:126
llvm::SmallVector< ObjectFifoCreateOp > objectFifos
Definition AIEPlacer.h:122
llvm::SmallVector< ObjectFifoAllocateOp > objectFifoAllocates
Definition AIEPlacer.h:124
llvm::SmallVector< LogicalTileOp > logicalTiles
Definition AIEPlacer.h:121
A shim end a runtime sequence transfers through.
llvm::DenseMap< mlir::Operation *, unsigned > rank() const
Rank the nodes by their features and then, a round at a time until no class splits,...
static StructuralGraph of(DeviceOp device)
The ops directly in device but its runtime sequences, each with the ops nested in it,...
std::vector< TileID > compTiles
Definition AIEPlacer.h:50
void removeTile(TileID tile, AIETileType type)
std::vector< TileID > nonCompTiles
Definition AIEPlacer.h:51
llvm::DenseSet< TileID > compTilesSet
Definition AIEPlacer.h:60