MLIR-AIE 1.4.6.dev
AIEPlacer.h
Go to the documentation of this file.
1//===- AIEPlacer.h ----------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
8#ifndef AIE_PLACER_H
9#define AIE_PLACER_H
10
14
15#include <array>
16#include <deque>
17#include <random>
18
19namespace xilinx::AIE {
20
21/// Placement algorithm type for pass option
23
24/// Get DMA channel capacity (maxIn, maxOut) for a tile position: its S2MM and
25/// MM2S channel counts.
26inline std::pair<int, int> getDMACapacity(const AIETargetModel &tm,
27 TileID tile) {
28 return {tm.getNumDMAChannels(tile.col, tile.row, DMAChannelDir::S2MM),
29 tm.getNumDMAChannels(tile.col, tile.row, DMAChannelDir::MM2S)};
30}
31
32// Streams through the North and South ports of a tile's stream switch.
34 int northIn = 0, northOut = 0, southIn = 0, southOut = 0;
35
37 northIn += other.northIn;
38 northOut += other.northOut;
39 southIn += other.southIn;
40 southOut += other.southOut;
41 return *this;
42 }
43};
44
45// maps logical tile operations to physical coordinates
46using PlacementResult = llvm::DenseMap<mlir::Operation *, TileID>;
47
48// Track available tiles and resource usage
50 std::vector<TileID> compTiles;
51 std::vector<TileID> nonCompTiles; // Memory and shim tiles
52
53 // O(1) membership shadow of `compTiles` used by SequentialPlacer's
54 // satisfiesComputePeer forward-look, which checks whether each of a
55 // candidate's physical compute neighbors is still free. Without it the
56 // forward-look would do an O(|compTiles|) std::find per direction per
57 // candidate per LTO, which scales as O(N * M^2) on large arrays. Must be
58 // kept in sync with `compTiles` by any code that mutates the vector
59 // (initialize(), limitCoresPerColumn(), removeTile()).
60 llvm::DenseSet<TileID> compTilesSet;
61
62 llvm::DenseMap<TileID, int> inputChannelsUsed;
63 llvm::DenseMap<TileID, int> outputChannelsUsed;
64 llvm::DenseMap<TileID, StreamDemand> streamsUsed;
65 // Live runtime-sequence BDs after each task event, as in
66 // SequentialPlacer::runtimeBdProfiles.
67 llvm::DenseMap<TileID, llvm::SmallVector<int>> runtimeBdsUsed;
68 // DMA channels, as (output, index), that flows of the logical tiles placed
69 // here name by index.
70 llvm::DenseMap<TileID, llvm::DenseSet<std::pair<int, int>>> namedChannelsUsed;
71
72 void removeTile(TileID tile, AIETileType type);
73};
74
75// Abstract placer interface
76class Placer {
77public:
78 Placer() = default;
79 virtual ~Placer() = default;
80
81 virtual void initialize(const AIETargetModel &tm);
82
83 virtual mlir::LogicalResult place(DeviceOp device) = 0;
84
85 virtual llvm::StringRef getName() const = 0;
86
87 std::optional<TileID> getPlacement(mlir::Operation *logicalTile) const {
88 auto it = result.find(logicalTile);
89 if (it != result.end())
90 return it->second;
91 return std::nullopt;
92 }
93
94 // Buffer allocation redirects: (ObjectFifoCreateOp, delegate TileID)
95 // Generated when a core tile's buffers exceed local memory and must
96 // spill to a neighbor tile's memory module.
97 using AllocateInfo = std::pair<mlir::Operation *, TileID>;
98 llvm::SmallVector<AllocateInfo> &getAllocates() { return allocates; }
99
100 // Per-LTO peer edges indexed by either endpoint. `tileToEdges` only
101 // indexes `LogicalTileOp` endpoints; `TileOp` peers carry their own
102 // coords.
103 struct Adjacency {
104 llvm::SmallVector<std::pair<TileLike, TileLike>, 4> edges;
105 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<unsigned, 2>>
107
108 void addEdge(TileLike first, TileLike second);
109
110 // Convenience for IR walkers: skip if either Value isn't a TileLike.
111 void addEdgeFromValues(mlir::Value a, mlir::Value b);
112
113 // True if `op` has at least one edge in this adjacency.
114 bool hasEdges(mlir::Operation *op) const {
115 return tileToEdges.count(op) != 0;
116 }
117 };
118
119 // IR operations collected from a DeviceOp for placement.
121 llvm::SmallVector<LogicalTileOp> logicalTiles;
122 llvm::SmallVector<ObjectFifoCreateOp> objectFifos;
123 llvm::SmallVector<ObjectFifoLinkOp> objectFifoLinks;
124 llvm::SmallVector<ObjectFifoAllocateOp> objectFifoAllocates;
125 llvm::SmallVector<CascadeFlowOp> cascadeFlows;
126 llvm::SmallVector<FlowOp> flows;
127 llvm::SmallVector<PacketFlowOp> pktFlows;
128 llvm::SmallVector<RouteOp> routes;
129 };
130
131 // Collect all placement-relevant operations from the device.
132 static CollectedOps collectOperations(DeviceOp device);
133
134 // Check if a logical tile's col/row constraints are satisfied at a position.
135 static bool satisfiesConstraints(mlir::Operation *tile, TileID pos);
136
137 // Adjacency builders: pure IR walkers with no placer-specific state.
138
139 // Edge: (cascade source, dest).
140 static Adjacency
141 buildCascadeAdjacency(llvm::ArrayRef<CascadeFlowOp> cascadeFlows);
142
143 // Edge: (producer LTO, consumer LTO) for every ObjectFifo whose producer
144 // AND consumer are CoreTile LTOs.
145 static Adjacency
146 buildComputePeerAdjacency(llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
147
148 // Edge: (producer, consumer) per forcesSharedMemory() ObjectFifo with one
149 // consumer.
150 static Adjacency
151 buildSharedMemAdjacency(llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
152
153 // Edge: (producer, consumer_i) per fifo.
154 static Adjacency
155 buildObjectFifoAdjacency(llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
156
157 // Edge: (src, dst) per `aie.flow`; cross-product per `aie.packet_flow`.
158 static Adjacency buildFlowAdjacency(llvm::ArrayRef<FlowOp> flows,
159 llvm::ArrayRef<PacketFlowOp> pktFlows);
160
161 // The objectFIFO ends each shim LTO hosts that may take turns on a channel
162 // with ends never in flight with them (ShimTransferSpans): DMA ends naming
163 // no channel, [0] receiving and [1] sending, as objectFIFO allocation sees
164 // them.
166 llvm::DenseMap<mlir::Operation *,
167 std::array<llvm::SmallVector<mlir::StringAttr>, 2>>;
168 static ShareableShimEnds
169 collectShareableShimEnds(DeviceOp device,
170 llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
171
172 // The objectFIFO end a runtime sequence names `symbol` for, before split:
173 // the fifo itself, sending when its producer is a shim tile.
174 static std::optional<ShimTransferSpans::End>
175 shimEndOf(DeviceOp device, mlir::StringAttr symbol);
176
177protected:
179 llvm::SmallVector<AllocateInfo> allocates;
180 const AIETargetModel *targetModel = nullptr;
182};
183
184// Invoke `fn(peer, thisIsFirst)` for each edge in `adjacency` that
185// mentions `op`. `peer` is the OTHER endpoint of the edge; `thisIsFirst`
186// is true when `op` is `edge.first`.
187template <typename F>
188inline void forEachPeer(mlir::Operation *op, const Placer::Adjacency &adjacency,
189 F &&fn) {
190 auto it = adjacency.tileToEdges.find(op);
191 if (it == adjacency.tileToEdges.end())
192 return;
193 for (unsigned idx : it->second) {
194 auto [first, second] = adjacency.edges[idx];
195 bool thisIsFirst = first.getOperation() == op;
196 TileLike peer = thisIsFirst ? second : first;
197 fn(peer, thisIsFirst);
198 }
199}
200
201// Whether two compute tiles can share one memory module: unlike buffer
202// ownership (where only the consumer must reach the owner's module),
203// sharing has no owner/consumer asymmetry, so either tile reaching the
204// other's module is enough. isLegalMemAffinity itself is directional (see
205// AIETargetModel::getSharedMemory's None/First/Second/Either), so compute-
206// peer neighbor-ness -- what `--aie-objectfifo-split`'s auto transport
207// actually tests -- has to check both.
208inline bool isSharedComputeMemory(const AIETargetModel &targetModel, TileID a,
209 TileID b) {
210 return targetModel.isLegalMemAffinity(a.col, a.row, b.col, b.row) ||
211 targetModel.isLegalMemAffinity(b.col, b.row, a.col, a.row);
212}
213
214// Invoke `fn(TileID)` on every CoreTile neighbor of `at` that can share
215// compute-peer memory with it per the target model.
216template <typename F>
217inline void forEachMemAffinityNeighbor(const AIETargetModel &targetModel,
218 TileID at, F &&fn) {
219 for (auto [dc, dr] :
220 {std::pair{0, -1}, std::pair{0, 1}, std::pair{-1, 0}, std::pair{1, 0}}) {
221 int nc = at.col + dc;
222 int nr = at.row + dr;
223 if (nc < 0 || nc >= targetModel.columns())
224 continue;
225 if (nr < 0 || nr >= targetModel.rows())
226 continue;
227 if (targetModel.getTileType(nc, nr) != AIETileType::CoreTile)
228 continue;
229 if (!isSharedComputeMemory(targetModel, at, TileID{nc, nr}))
230 continue;
231 fn(TileID{nc, nr});
232 }
233}
234
235// Sequential placement algorithm
236//
237// Greedy, single-pass placer that maps each `aie.logical_tile` (LTO) to a
238// physical (col, row). Four phases:
239//
240// 1. Collect LogicalTileOp / ObjectFifo* / Cascade* / Flow* / PacketFlow* /
241// Route from the device.
242// 2. Build per-LTO constraints: buffer/cascade/compute-peer adjacencies,
243// channel requirements, and needNeighborIn/Out (the minimum number of
244// compute peers per LTO that MUST land on a shared-L1 neighbor to fit
245// the per-tile DMA budget). Group the compute LTOs by the non-core
246// tile each shares the most flows with (buildCoreGroups).
247// 3. Place compute tiles. Make each core group contiguous, then sort by
248// constraint level (pinned -> partial -> unpinned) and by descending
249// placementPriority. For each LTO: validate buffer/cascade/compute-peer
250// constraints and channel budget, then claim the tile, trying first
251// the columns that keep its group together. Unpinned candidates are
252// tried in two passes: first the slots with column stream capacity and
253// not soft-reserved for another demand-bearing LTO's peers, then a
254// fallback accepting overloaded columns and reserved slots. Placing a
255// demand-bearing LTO soft-reserves its mem-affinity neighbor slots for
256// that LTO's compute peers.
257// 4. Place each remaining non-core (mem/shim) LTO near the column
258// centroid of its placed-core peers. With mergeLogicalTiles == true
259// (default) several non-core LTOs may share one physical tile when
260// DMA capacity allows, preferring tiles whose North/South stream ports
261// and runtime-sequence BDs also fit; with mergeLogicalTiles == false
262// each non-core LTO claims its own physical tile.
263//
264// Greedy: once a tile is chosen the decision is final, with no backtracking.
265// If a later LTO's constraints become unsatisfiable given prior placements,
266// the placer emits an error pointing at the LTOs the user can manually pin
267// to break the deadlock.
268//
269// Compute-to-compute ObjectFifos consume no DMA channel when their endpoints
270// land on shared-L1 neighbor tiles, so they enter the placer as a separate
271// compute-peer adjacency rather than as channel demand.
272class SequentialPlacer : public Placer {
273public:
274 SequentialPlacer(std::optional<int> coresPerCol = std::nullopt,
275 bool mergeLogicalTiles = true,
276 bool spreadUnanchoredTiles = false)
277 : coresPerCol(coresPerCol), mergeLogicalTiles(mergeLogicalTiles),
278 spreadUnanchoredTiles(spreadUnanchoredTiles) {}
279
280 void initialize(const AIETargetModel &targetModel) override;
281
282 mlir::LogicalResult place(DeviceOp device) override;
283
284 llvm::StringRef getName() const override { return "sequential_placer"; }
285
286private:
287 std::optional<int> coresPerCol;
288 bool mergeLogicalTiles;
289 // When a non-core LTO has no CoreTile peers, rank candidates by DMA load
290 // instead of by distance to column 0. See placeNonCoreTileByCentroid.
291 bool spreadUnanchoredTiles;
292 // Physical tiles already assigned to a non-core aie.logical_tile. Used
293 // only when mergeLogicalTiles == false to forbid mapping a second
294 // non-core aie.logical_tile onto a tile that already hosts one.
295 llvm::DenseSet<TileID> assignedNonCoreTiles;
296 int deviceCoresPerCol = 0; // Actual cores per column in device
297
298 // Shim channels are counted one per end until a shim tile runs out; then
299 // ends never in flight together count once, as allocation will share them.
300 std::optional<ShimTransferSpans> shimSpans;
301 ShareableShimEnds shareableShimEnds;
302 llvm::DenseMap<TileID, std::array<llvm::SmallVector<mlir::StringAttr>, 2>>
303 shimEndsAt;
304 bool fitsSharingShim(TileID tile, mlir::Operation *logicalTile,
305 std::pair<int, int> required);
306 void recordShimEnds(TileID tile, mlir::Operation *logicalTile);
307
308 // Counts once in `requirements` each group of fifos between a core and one
309 // mem tile that allocation's planTurnSharing will put on one channel.
310 void countTurns(
311 DeviceOp device, llvm::ArrayRef<ObjectFifoCreateOp> objectFifos,
312 llvm::DenseMap<mlir::Operation *, std::pair<int, int>> &requirements);
313
314 // DMA channel direction selector.
315 enum class DmaDir { In, Out };
316
317 // Whether an LTO's placement was pinned by the user (via col/row
318 // attributes) or chosen by the placer.
319 enum class PlacementOrigin { Pinned, Selected };
320
321 void limitCoresPerColumn(int maxCoresPerCol, int numColumns);
322
323 // targetCol == nullopt means no routing anchor is known, so candidates are
324 // ranked by DMA load rather than by distance to a column. `withinBudgets`
325 // also requires fitsStreamAndBdBudgets.
326 std::optional<TileID>
327 findTileWithCapacity(std::optional<int> targetCol,
328 llvm::ArrayRef<TileID> tiles, int requiredInputChannels,
329 int requiredOutputChannels, AIETileType requestedType,
330 mlir::Operation *logicalTile, bool withinBudgets);
331
332 // Diagnosis for a failed findTileWithCapacity() call. `findTileWithCapacity`
333 // scans every tile of `requestedType` on the device -- `targetCol` orders
334 // candidates, it does not exclude any -- so a failure is either (a) no
335 // tile of this type has the requested channels free ANYWHERE (device-wide
336 // DMA exhaustion: pinning a column cannot create capacity), or (b) at
337 // least one tile has them free but is already claimed by a different
338 // non-core logical tile (only possible under merge-logical-tiles=false).
339 // `capacityExistsSomewhere` selects between the two; the totals let the
340 // caller report the real budget instead of guessing at it.
341 struct CapacityDiagnosis {
342 bool capacityExistsSomewhere = false;
343 int tilesOfType = 0;
344 int inUsed = 0, inMax = 0, outUsed = 0, outMax = 0;
345 // Both shortfalls belong to one tile, minimizing their sum.
346 int closestShortIn = 0, closestShortOut = 0;
347 };
348 CapacityDiagnosis diagnoseCapacityExhaustion(AIETileType requestedType,
349 int requiredInputChannels,
350 int requiredOutputChannels);
351
352 void updateChannelUsage(TileID tile, DmaDir direction, int numChannels);
353
354 // North/South streams of each tile's ObjectFifos: a memtile has 6
355 // DMA inputs but only 4 streams from the core above, and no East/West.
356 llvm::DenseMap<mlir::Operation *, StreamDemand> streamDemand;
357 // Aggregate traffic between a column's cores and the tiles below them.
358 llvm::DenseMap<int, StreamDemand> columnStreamsUsed;
359 // Broadcast consumers in one column share a single incoming stream.
360 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<mlir::Operation *>>
361 incomingFifos;
362 llvm::DenseMap<int, llvm::DenseSet<mlir::Operation *>> columnIncomingFifos;
363 void recordColumnStreamUsage(mlir::Operation *tile, int col);
364 // Extra broadcast destinations beyond the first, which uses one DMA channel.
365 llvm::DenseMap<mlir::Operation *, int> broadcastFanout;
366 llvm::DenseMap<TileID, int> broadcastFanoutUsed;
367 void buildStreamDemand(llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
368
369 // Live runtime-sequence BDs of each ShimNOCTile LTO after every task event,
370 // counting each descriptor as aie-decompose-large-dma-bd splits it.
371 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<int>> runtimeBdProfiles;
372
373 // DMA channels, as (output, index), that each logical tile's flows name by
374 // index. Two logical tiles naming the same one cannot share a physical tile.
375 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<std::pair<int, int>>>
376 namedChannels;
377 void buildRuntimeBdProfiles(DeviceOp device,
378 llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
379
380 // True if `tile`'s stream ports and BDs still fit `logicalTile`'s demand on
381 // top of what earlier placements use.
382 bool fitsStreamAndBdBudgets(mlir::Operation *logicalTile, TileID tile) const;
383 // Records the streams, BDs and named DMA channels `logicalTile` takes on
384 // `tile`.
385 void recordStreamAndBdUsage(mlir::Operation *logicalTile, TileID tile);
386
387 bool hasAvailableChannels(TileID tile, int inputChannels, int outputChannels);
388
389 mlir::LogicalResult validateAndUpdateChannelUsage(
390 LogicalTileOp logicalTile, TileID tile,
391 const llvm::DenseMap<mlir::Operation *, std::pair<int, int>>
392 &channelRequirements,
393 PlacementOrigin origin);
394
395 llvm::DenseMap<mlir::Operation *, std::pair<int, int>>
396 buildChannelRequirements(
397 llvm::SmallVector<ObjectFifoCreateOp> &objectFifos,
398 llvm::SmallVector<ObjectFifoLinkOp> &objectFifoLinks);
399
400 // Per-place() neighbor-demand maps and soft-reservation table.
401 struct PlacementContext {
402 const AIETargetModel &targetModel;
403 const Adjacency &computePeerAdjacency;
404 const llvm::DenseMap<mlir::Operation *, int> &needNeighborIn;
405 const llvm::DenseMap<mlir::Operation *, int> &needNeighborOut;
406 // Mutable: each placement of an LTO with neighbor demand reserves
407 // its mem-affinity neighbor slots for that LTO's compute peers.
408 llvm::DenseMap<TileID, llvm::SmallVector<mlir::Operation *, 2>>
409 reservedFor{};
410
411 int neighborDemand(mlir::Operation *op) const {
412 return needNeighborIn.lookup(op) + needNeighborOut.lookup(op);
413 }
414 // Priority = max(self demand, highest peer demand). A peer of a
415 // high-fanin Worker rises to the front of the sort so its neighbor
416 // slots aren't consumed by unrelated LTOs.
417 int placementPriority(mlir::Operation *op) const;
418 // Soft-reserve `at`'s mem-affinity compute-tile neighbors for
419 // `placedOp`'s peers, if `placedOp` has non-zero neighbor demand.
420 void reserveNeighborSlots(mlir::Operation *placedOp, TileID at);
421 // True if `candidate` is reserved by any LTO that is NOT a compute
422 // peer of `lto` (i.e. would compete for the slot).
423 bool isReservedForOther(mlir::Operation *lto, TileID candidate) const;
424 };
425
426 // A pairwise-legality adjacency constraint (buffer, cascade, ...)
427 // bundled with everything the placer needs to check it and report a
428 // useful diagnostic when it fails: the edge set, the predicate, the
429 // peer-role label for notes, the short name for the error message,
430 // and the educational hint text appended after the peer notes.
431 struct AdjacencyKind {
432 const Adjacency &adjacency;
433 llvm::function_ref<bool(TileID firstPos, TileID secondPos)> pred;
434 llvm::function_ref<llvm::StringRef(bool thisIsFirst)> peerLabel;
435 llvm::StringRef name; // e.g. "shared-L1 buffer", "cascade"
436 llvm::StringRef constraintHint; // appended as a final attachNote
437 };
438
439 // Check `logicalTile` at `tile` against `kind`'s adjacency. On
440 // failure, emit a fully-formed diagnostic (error + peer notes +
441 // constraint hint) and return failure.
442 mlir::LogicalResult enforceAdjacency(LogicalTileOp logicalTile, TileID tile,
443 const AdjacencyKind &kind);
444
445 // Record that `logicalTile` is placed at `tile`. For CoreTile LTOs
446 // this also removes the tile from availability and asks `ctx` to
447 // soft-reserve the tile's mem-affinity neighbor slots for the LTO's
448 // compute peers. `debugLabel` ("pinned" / "unconstrained") names the
449 // placement path in the LLVM_DEBUG trace.
450 void recordPlacement(LogicalTileOp logicalTile, TileID tile,
451 PlacementContext &ctx, llvm::StringRef debugLabel);
452
453 // Per-call inputs for `findUnconstrainedCoreCandidate`. Bundles the
454 // adjacency + demand state that Phase 3 builds for each candidate
455 // filter.
456 struct UnpinnedPlacementInputs {
457 const Adjacency &bufferAdjacency;
458 llvm::function_ref<bool(TileID, TileID)> bufferPred;
459 const Adjacency &cascadeAdjacency;
460 llvm::function_ref<bool(TileID, TileID)> cascadePred;
461 const Adjacency &sharedMemAdjacency;
462 llvm::function_ref<bool(TileID, TileID)> sharedMemPred;
463 const Adjacency &computePeerAdjacency;
464 const llvm::DenseMap<mlir::Operation *, int> &needNeighborIn;
465 const llvm::DenseMap<mlir::Operation *, int> &needNeighborOut;
466 llvm::function_ref<bool(mlir::Operation *, TileID)> isReservedForOther;
467 // Columns to try first, in order, to keep the LTO with its core group
468 // (see buildCoreGroups). Ignored for LTOs with neighbor demand.
469 llvm::ArrayRef<int> preferredCols;
470 };
471
472 // Outputs of `findUnconstrainedCoreCandidate`. `placement` is set on
473 // success; on failure the three boolean flags describe which filter
474 // rejected the most candidates so place() can construct an actionable
475 // diagnostic.
476 struct UnpinnedSearchResult {
477 std::optional<TileID> placement;
478 bool sawConstraintMatch = false;
479 bool allConstraintMatchesFailedAdjacency = true;
480 bool computePeerWasCause = false;
481 };
482
483 // Two-pass candidate search for an unpinned (or partially-pinned)
484 // CoreTile LTO. First pass prefers candidates that aren't
485 // soft-reserved by an unrelated demand-bearing LTO's peers; second
486 // pass falls back to reserved candidates. Each candidate is filtered
487 // by buffer adjacency, cascade adjacency, and the compute-peer DMA
488 // budget. Does not mutate placer state.
489 UnpinnedSearchResult
490 findUnconstrainedCoreCandidate(LogicalTileOp logicalTile,
491 std::optional<int> col, std::optional<int> row,
492 const UnpinnedPlacementInputs &inputs);
493
494 // Edge: (consumer LTO, owner tile). Predicate: `isLegalMemAffinity`.
495 // SequentialPlacer-only: walks CoreOps and BufferOps, and takes each end of
496 // an allocated fifo as a consumer of its delegate tile.
498 buildBufferAdjacency(llvm::ArrayRef<LogicalTileOp> logicalTiles,
499 llvm::ArrayRef<ObjectFifoAllocateOp> allocates);
500
501 // Per-LTO peer-Value lists, one list per flow that mentions the LTO.
502 // Built once and shared so each centroid lookup is O(#flows on the LTO).
503 struct FlowMembership {
504 llvm::DenseMap<mlir::Value,
505 llvm::SmallVector<llvm::SmallVector<mlir::Value>>>
506 ltoFlows;
507 };
508
509 // Phase 4 driver: for every still-unplaced non-core LTO, place it near
510 // the centroid column of its core peers, iterating non-core LTOs in
511 // descending channel-demand order so the heaviest consumers get first
512 // pick of physical tiles.
513 mlir::LogicalResult placeNonCoreLogicalTiles(
514 llvm::ArrayRef<LogicalTileOp> logicalTiles,
515 const FlowMembership &flowIndex,
516 const llvm::DenseMap<mlir::Operation *, std::pair<int, int>>
517 &channelRequirements);
518
519 // Core LTOs grouped by the non-core LTO each shares the most flows with
520 // (its hub), members in program order. A core with no non-core peer is a
521 // group of its own. Placing a group in one column lets the hub's
522 // centroid land on that column.
523 struct CoreGroups {
524 llvm::DenseMap<mlir::Operation *, unsigned> groupOf;
525 llvm::SmallVector<llvm::SmallVector<LogicalTileOp>> members;
526 };
527
528 CoreGroups buildCoreGroups(llvm::ArrayRef<LogicalTileOp> logicalTiles,
529 const FlowMembership &flowIndex) const;
530
531 // Columns to try first for `logicalTile`: those already holding members
532 // of its group, most members first; for a group's first member, the
533 // columns with a free core for every member, in column order. Empty
534 // means no preference.
535 llvm::SmallVector<int> preferredGroupColumns(LogicalTileOp logicalTile,
536 const CoreGroups &groups) const;
537
538 FlowMembership
539 buildFlowMembership(llvm::ArrayRef<FlowOp> flows,
540 llvm::ArrayRef<PacketFlowOp> pktFlows,
541 llvm::ArrayRef<RouteOp> routes,
542 llvm::ArrayRef<ObjectFifoCreateOp> objectFifos);
543
544 // Pick the column that minimizes total routing cost across the LTO's
545 // flows. See AIEPlacer.cpp for the per-flow cost formulas and tiebreak.
546 // Returns nullopt when the LTO has no CoreTile peer to measure against.
547 std::optional<int> computeCentroidColumn(LogicalTileOp logicalTile,
548 const FlowMembership &flowIndex);
549
550 mlir::LogicalResult placeNonCoreTileByCentroid(
551 LogicalTileOp logicalTile, const FlowMembership &flowIndex,
552 const llvm::DenseMap<mlir::Operation *, std::pair<int, int>>
553 &channelRequirements);
554
555 // Pairwise legality check. `pred(firstPos, secondPos)` is evaluated for
556 // every edge mentioning `logicalTile`; unplaced peers defer.
557 bool satisfiesAdjacency(
558 LogicalTileOp logicalTile, TileID candidate, const Adjacency &adjacency,
559 llvm::function_ref<bool(TileID firstPos, TileID secondPos)> pred) const;
560
561 // Compute-peer DMA budget check. Returns true if placing `logicalTile` at
562 // `candidate` keeps the compute-peer neighbor demand satisfiable for BOTH
563 // the LTO being placed AND every already-placed compute peer of the LTO.
564 // A compute LTO with peer demand N (N compute-peer fifos must use shared
565 // neighbor memory to fit DMA budget) has slack (totalComputePeers − N): it
566 // can afford at most that many non-neighbor compute peers. The check also
567 // forward-looks: unplaced peers still need a free physical compute-tile
568 // neighbor of `candidate` to land on.
569 bool satisfiesComputePeer(
570 LogicalTileOp logicalTile, TileID candidate,
571 const Adjacency &computePeerAdjacency,
572 const llvm::DenseMap<mlir::Operation *, int> &needNeighborIn,
573 const llvm::DenseMap<mlir::Operation *, int> &needNeighborOut) const;
574
575 // Compute-peer in/out edge counts for `op`, looked up from a prebuilt
576 // `computePeerAdjacency`. Returns {0, 0} if `op` has no entry. Producer
577 // is edges[idx].first, consumer is .second (per buildComputePeerAdjacency).
578 static std::pair<int, int> totalComputePeers(mlir::Operation *op,
579 const Adjacency &adjacency);
580
581 // Diagnostic peer notes. `labelPeer(thisIsFirst)` names the peer endpoint
582 // role; the attached note reads "<label> peer placed at (col, row)".
583 void attachPeerNotes(
584 mlir::InFlightDiagnostic &diag, LogicalTileOp logicalTile,
585 const Adjacency &adjacency,
586 llvm::function_ref<llvm::StringRef(bool thisIsFirst)> labelPeer) const;
587
588 void addChannelRequirementsFromFlows(
589 llvm::ArrayRef<FlowOp> flows, llvm::ArrayRef<PacketFlowOp> pktFlows,
590 llvm::ArrayRef<RouteOp> routes,
591 llvm::DenseMap<mlir::Operation *, std::pair<int, int>>
592 &channelRequirements);
593};
594
595// Configuration for the SA placer's cost model and schedule.
596//
597// The SA placer minimizes a total cost with four components:
598//
599// totalCost = HPWL + resourcePenalty + cascadePenalty + memoryPressure
600//
601// 1. HPWL (half-perimeter wire length): sum of bounding-box perimeters
602// across all nets.
603//
604// 2. Resource penalty (hard, blocks legality): penalizes MemTile buffer
605// overflow, core tile overflow, DMA channel overuse, BD count
606// overuse, and fifo ends that can't reach their allocate's delegate.
607//
608// 3. Cascade penalty (hard, blocks legality): penalizes cascade put/get
609// pairs that aren't adjacent.
610//
611// 4. Memory pressure (soft, guides optimization): quadratic penalty when
612// tile utilization exceeds a threshold.
613struct SAConfig {
614 // HPWL (cost 1)
615 int multicastThreshold = 4; // consumer count above which penalty applies
616 int multicastMultiplier = 2; // HPWL multiplier for multicast nets
617
618 // Resource penalty (cost 2)
619 int memPenaltyPerKB = 30; // per KB of unresolved memory overflow
620 int dmaPenaltyPerChannel = 20; // per DMA channel or BD over limit
621 int delegateWeightPerDist = 30; // per distance from an allocate's delegate
622
623 // Cascade penalty (cost 3)
624 int cascadeWeightPerDist = 30; // per Manhattan distance to valid position
625 // A shared-memory pair apart, per distance past a neighbour: above a
626 // fifo's worth of memory overflow, which a neighbour's spare room absorbs.
628
629 // The core's own sections (tables, constants) a core that declares no
630 // data_size is taken to need, before aiecc measures them after placement.
631 int coreSectionsBytes = 3 * 1024;
632
633 // Memory pressure (cost 4)
634 int pressurePerKB = 3; // quadratic pressure above threshold
635 double pressureThreshold = 0.75; // utilization fraction triggering pressure
636
637 // Adaptive cooling
638 double coolFast = 0.995; // accept ratio > 96%: cool faster
639 double coolSteady = 0.998; // accept ratio > 80%: steady progress
640 double coolBalanced = 0.999; // accept ratio > 15%: balanced exploration
641 double coolStuck = 0.9998; // accept ratio <= 15%: barely cool
642 double greedyThreshold = 0.005; // switch to greedy (T=0) below this
643
644 // Schedule sizing
646 int maxMovesPerIter = 2000;
647 int minMaxIters = 10000;
648 int greedyMultiplier = 50; // greedyIters = multiplier * numMovable
649 double effort = 1.0; // scales movesPerIter and greedyIters
650 double tempScaleEstimate = 10.0; // initTemp cap: scale * estimated T
651 double tempScaleCost = 2.0; // initTemp cap: scale * totalCost
652
653 // Temperature estimation
654 int tempSearchIters = 100; // binary search iterations
655 double tempSearchCeiling = 5.0; // highT = ceiling * maxDelta
656 double tempSearchEpsilon = 1e-10; // zero guard in binary search
657 double tempSearchTolerance = 1e-6; // convergence tolerance
658
659 // SA loop intervals
660 int debugInterval = 5000; // iterations between debug log lines
661 int fullRecomputeInterval = 10000; // iterations between full cost recompute
662
663 // Move generation
664 int maxMoveAttempts = 20; // retries per move before giving up
665
666 // Acceptance tracking
667 int windowSize = 100; // sliding window for acceptance ratio
668};
669
670// SA temperature schedule with windowed acceptance tracking.
672public:
673 SASchedule() = default;
674 SASchedule(double startTemp, int movesPerIter, int maxIters, int greedyIters,
675 const SAConfig &cfg = {});
676
677 double getTemperature() const { return temperature; }
678 int getIteration() const { return currIteration; }
679 int getMovesPerIter() const { return movesPerIter; }
680 bool isGreedy() const { return inGreedyStage; }
681 bool limitReached() const;
682 double getAcceptanceRatio() const;
683 void record(bool accepted);
684 void cool();
685
686private:
687 double temperature = 0.0;
688 int currIteration = 0;
689 int currGreedyIteration = 0;
690 int movesPerIter = 100;
691 int maxIters = 5000;
692 int greedyIters = 100;
693 int windowSize = 100;
694 SAConfig config;
695 bool inGreedyStage = false;
696 std::deque<int> history;
697 int acceptCount = 0;
698 int rejectCount = 0;
699
700 void trimWindow();
701};
702
703// Per-net bounding box state for incremental HPWL updates.
704// countAt* tracks how many endpoints sit at each extreme so we can avoid
705// full rescans when an endpoint moves away from a non-unique extreme.
707 int minCol = 0, maxCol = 0, minRow = 0, maxRow = 0;
710};
711
712struct NetInfo {
713 llvm::SmallVector<mlir::Operation *> endpoints; // producer + all consumers
715 bool isMulticast = false; // >4 consumers triggers 2x HPWL penalty
716};
717
718// Simulated annealing placement algorithm
719//
720// Uses HPWL (half-perimeter wire length) cost with incremental bounding-box
721// updates. Nets are derived from ObjectFifo producer/consumer connectivity.
722// All tile types (compute, mem, shim) participate in SA moves. After SA
723// converges, a merge pass collapses mem/shim logical tiles that landed in the
724// same column onto a shared physical tile when DMA capacity permits.
725class SAPlacer : public Placer {
726public:
727 explicit SAPlacer(unsigned seed = 0, SAConfig config = {})
728 : rngSeed(seed), config(config) {}
729
730 mlir::LogicalResult place(DeviceOp device) override;
731 llvm::StringRef getName() const override { return "sa_placer"; }
732 int getFinalCost() const { return finalCost; }
733
734private:
735 // Configuration
736 unsigned rngSeed;
737 SAConfig config;
738 int finalCost = 0;
739 std::mt19937 rng;
740 SASchedule schedule;
741
742 // Collected IR state
743 CollectedOps collected;
744 llvm::DenseMap<mlir::Operation *, AIETileType> tileTypes;
745 llvm::DenseMap<mlir::Operation *, int64_t> staticBufferSizes;
746 llvm::DenseMap<mlir::Operation *, int64_t> stackSizes;
747
748 // Net model
749 struct FifoBufferInfo {
750 mlir::Operation *fifoOp = nullptr;
751 mlir::Operation *producer = nullptr;
752 llvm::SmallVector<mlir::Operation *> consumers;
753 mlir::Operation *delegate = nullptr;
754 int64_t producerSizeBytes = 0;
755 int64_t consumerSizeBytes = 0;
756 int producerDepth = 0;
757 llvm::SmallVector<int> consumerDepths;
758 int producerDMADepth = 0;
759 llvm::SmallVector<int> consumerDMADepths;
760 bool forcesDMA = false;
761 bool linkSharedProd = false;
762
763 int consumerDepthAt(size_t ci) const {
764 return (ci < consumerDepths.size()) ? consumerDepths[ci] : producerDepth;
765 }
766 };
767 llvm::SmallVector<NetInfo> nets;
768 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<size_t>> tileToNetIndices;
769 llvm::SmallVector<FifoBufferInfo> fifoBuffers;
770
771 // A route's DMA end on a logical tile: one channel, in or out, that moves
772 // with the tile.
773 struct RouteEndInfo {
774 mlir::Operation *tile;
775 bool output;
776 };
777 llvm::SmallVector<RouteEndInfo> routeEnds;
778 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<size_t>> tileToRouteEnds;
779 // Descriptors the pools on each logical tile need, for the mem-tile budget.
780 llvm::DenseMap<mlir::Operation *, int> poolBDs;
781
782 // Placement state
783 llvm::DenseMap<mlir::Operation *, TileID> currentPlacement;
784 llvm::DenseMap<TileID, mlir::Operation *> physToLogical;
785 llvm::SmallVector<mlir::Operation *> movableTiles;
786 llvm::DenseSet<mlir::Operation *> constrainedTiles;
787
788 // Resource tracking
789 llvm::DenseMap<mlir::Operation *, llvm::SmallVector<size_t>>
790 tileToFifoIndices;
791 llvm::DenseMap<TileID, int64_t> currentMemUsage;
792 llvm::DenseMap<TileID, std::pair<int, int>> currentDMAUsage;
793 llvm::DenseMap<size_t, TileID> sharedMemDestination;
794 int cachedResourcePenalty = 0;
795 // An over-full shim tile is charged only for the channels allocation
796 // still needs once ends never in flight together share them.
797 std::optional<ShimTransferSpans> shimSpans;
798 ShareableShimEnds shareableShimEnds;
799
800 // Cascade
801 Adjacency cascadeAdjacency;
802 static constexpr std::pair<int, int> kCascadeOffsets[] = {{1, 0}, {0, -1}};
803 // Ends of fifos that ask for shared memory, which must share a module.
804 Adjacency sharedMemAdjacency;
805
806 // SA loop state
807 int totalCost = 0;
808 int cascadePen = 0;
809 int bestCost = INT_MAX; // best legal (resPenalty==0 && cascade==0)
810 int bestOverallCost = INT_MAX; // best regardless of legality (fallback)
811 PlacementResult bestPlacement; // corresponds to bestCost
812 PlacementResult bestOverallPlacement; // corresponds to bestOverallCost
813 int deviceSlots = 0;
814 int zeroDeltaMoves = 0, posDeltaMoves = 0, negDeltaMoves = 0;
815 int acceptedUphill = 0, rejectedMoves = 0;
816 std::chrono::steady_clock::time_point startTime;
817
818 // Phase methods
819 mlir::LogicalResult collectAndBuildModel(DeviceOp device);
820 mlir::LogicalResult generateInitialPlacement();
821 void initializeSAState();
822 void runSAMainLoop();
823 mlir::LogicalResult finalizePlacement(DeviceOp device);
824 mlir::LogicalResult mergeMemShimTiles(DeviceOp device);
825
826 // Cost and resource methods
827 void initResourceTracking();
828 void addFifoContribution(size_t fifoIdx, int sign);
829 void addRouteEndContribution(size_t endIdx, int sign);
830 int updateResourcePenalty(
831 const llvm::SmallVector<std::pair<mlir::Operation *, TileID>>
832 &oldPlacements);
833 int getResourcePenalty() const { return cachedResourcePenalty; }
834 int computePenalty() const;
835 int computeMemSpilloverPenalty() const;
836 int computeCoreOverflowPenalty() const;
837 int computeDMAChannelPenalty() const;
838 int computeBDCountPenalty() const;
839 int computeDelegatePenalty() const;
840 std::optional<TileID> positionOf(mlir::Operation *tile) const;
841 int computeMemoryPressure() const;
842 int computeAdjacencyPenalty(const Adjacency &adj,
843 llvm::ArrayRef<std::pair<int, int>> validOffsets,
844 int weight) const;
845 /// Cascade neighbours placed apart, and shared-memory ends placed on
846 /// tiles sharing no memory module, by how far.
847 int computePairingPenalty() const;
848 void generateAllocates();
849
850 // Move methods
851 void tryMove(llvm::SmallVector<std::pair<mlir::Operation *, TileID>> &moves);
852 bool
853 generateMove(llvm::SmallVector<std::pair<mlir::Operation *, TileID>> &moves);
854 int evaluateMove(
855 mlir::Operation *tile, TileID newPos,
856 llvm::SmallVector<std::pair<size_t, NetBoundingBox>> &backups);
857 void
858 revertMove(llvm::SmallVector<std::pair<size_t, NetBoundingBox>> &backups);
859
860 // Utility methods
861 void buildNetModel(llvm::SmallVector<ObjectFifoCreateOp> &objectFifos,
862 llvm::SmallVector<ObjectFifoLinkOp> &objectFifoLinks);
863 void buildFifoBufferInfo(DeviceOp device,
864 llvm::ArrayRef<ObjectFifoCreateOp> objectFifos,
865 llvm::ArrayRef<ObjectFifoLinkOp> objectFifoLinks);
866 void buildRouteModel(DeviceOp device, llvm::ArrayRef<RouteOp> routes);
867 int computeNetHPWL(const NetInfo &net) const;
868 int computeTotalHPWL() const;
869 void initBoundingBoxes();
870 double estimateInitialTemperature(int numSamples);
871 bool isLegalPosition(mlir::Operation *tile, TileID pos) const;
872 int64_t computeTileMemoryWeight(mlir::Operation *tile) const;
873 void printPlacementStats(int64_t elapsedMs) const;
874};
875
876} // namespace xilinx::AIE
877
878#endif // AIE_PLACER_H
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.
uint32_t getNumDMAChannels(int col, int row, AIE::DMAChannelDir dir) const
Return the number of DMA channels a tile has in one direction.
virtual int columns() const =0
Return the number of columns in the device.
virtual bool isLegalMemAffinity(int coreCol, int coreRow, int memCol, int memRow) const =0
Return true if core can access the memory in mem.
virtual mlir::LogicalResult place(DeviceOp device)=0
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
std::pair< mlir::Operation *, TileID > AllocateInfo
Definition AIEPlacer.h:97
virtual void initialize(const AIETargetModel &tm)
Definition AIEPlacer.cpp:74
static Adjacency buildComputePeerAdjacency(llvm::ArrayRef< ObjectFifoCreateOp > objectFifos)
virtual ~Placer()=default
llvm::SmallVector< AllocateInfo > allocates
Definition AIEPlacer.h:179
virtual llvm::StringRef getName() const =0
PlacementResult result
Definition AIEPlacer.h:178
std::optional< TileID > getPlacement(mlir::Operation *logicalTile) const
Definition AIEPlacer.h:87
llvm::SmallVector< AllocateInfo > & getAllocates()
Definition AIEPlacer.h:98
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)
SAPlacer(unsigned seed=0, SAConfig config={})
Definition AIEPlacer.h:727
llvm::StringRef getName() const override
Definition AIEPlacer.h:731
int getFinalCost() const
Definition AIEPlacer.h:732
mlir::LogicalResult place(DeviceOp device) override
void record(bool accepted)
double getTemperature() const
Definition AIEPlacer.h:677
int getIteration() const
Definition AIEPlacer.h:678
double getAcceptanceRatio() const
int getMovesPerIter() const
Definition AIEPlacer.h:679
bool isGreedy() const
Definition AIEPlacer.h:680
SequentialPlacer(std::optional< int > coresPerCol=std::nullopt, bool mergeLogicalTiles=true, bool spreadUnanchoredTiles=false)
Definition AIEPlacer.h:274
llvm::StringRef getName() const override
Definition AIEPlacer.h:284
void initialize(const AIETargetModel &targetModel) override
mlir::LogicalResult place(DeviceOp device) override
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
PlacerType
Placement algorithm type for pass option.
Definition AIEPlacer.h:22
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
bool isSharedComputeMemory(const AIETargetModel &targetModel, TileID a, TileID b)
Definition AIEPlacer.h:208
llvm::SmallVector< mlir::Operation * > endpoints
Definition AIEPlacer.h:713
NetBoundingBox bb
Definition AIEPlacer.h:714
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
bool hasEdges(mlir::Operation *op) const
Definition AIEPlacer.h:114
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
StreamDemand & operator+=(const StreamDemand &other)
Definition AIEPlacer.h:36
std::vector< TileID > compTiles
Definition AIEPlacer.h:50
llvm::DenseMap< TileID, int > outputChannelsUsed
Definition AIEPlacer.h:63
void removeTile(TileID tile, AIETileType type)
std::vector< TileID > nonCompTiles
Definition AIEPlacer.h:51
llvm::DenseSet< TileID > compTilesSet
Definition AIEPlacer.h:60
llvm::DenseMap< TileID, int > inputChannelsUsed
Definition AIEPlacer.h:62
llvm::DenseMap< TileID, StreamDemand > streamsUsed
Definition AIEPlacer.h:64
llvm::DenseMap< TileID, llvm::DenseSet< std::pair< int, int > > > namedChannelsUsed
Definition AIEPlacer.h:70
llvm::DenseMap< TileID, llvm::SmallVector< int > > runtimeBdsUsed
Definition AIEPlacer.h:67