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