MLIR-AIE 1.4.5.dev
AIESAPlacer.cpp
Go to the documentation of this file.
1//===- AIESAPlacer.cpp ------------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
10#include "llvm/Support/Debug.h"
11
12#include <algorithm>
13#include <chrono>
14#include <cmath>
15#include <limits>
16#include <numeric>
17
18using namespace mlir;
19using namespace xilinx::AIE;
20
21#define DEBUG_TYPE "aie-sa-placer"
22
23// The std:: distributions and std::shuffle are implementation-defined, so the
24// same seed yields different placements under libstdc++ and MSVC. These
25// helpers consume raw std::mt19937 output, which the standard fixes.
26
27/// Draw an index in [0, upperExclusive).
28static size_t drawBoundedIndex(std::mt19937 &rng, size_t upperExclusive) {
29 return static_cast<size_t>(rng()) % upperExclusive;
30}
31
32/// Draw a double in [0, 1).
33static double drawUnitReal(std::mt19937 &rng) {
34 constexpr double denominator = static_cast<double>(std::mt19937::max()) + 1.0;
35 return static_cast<double>(rng()) / denominator;
36}
37
38/// Fisher-Yates shuffle.
39static void deterministicShuffle(MutableArrayRef<TileID> values,
40 std::mt19937 &rng) {
41 for (size_t i = values.size(); i > 1; --i)
42 std::swap(values[i - 1], values[drawBoundedIndex(rng, i)]);
43}
44
45/// Return neighboring tiles whose memory module `tilePos` can access.
46/// The direction matches the allocate op requirement: the source tile
47/// can store buffers in the neighbor's memory.
48static SmallVector<TileID>
49getMemAffinityNeighbors(const AIETargetModel &targetModel, TileID tilePos) {
50 SmallVector<TileID> neighbors;
51 for (TileID candidate : {TileID{tilePos.col - 1, tilePos.row},
52 TileID{tilePos.col + 1, tilePos.row},
53 TileID{tilePos.col, tilePos.row + 1},
54 TileID{tilePos.col, tilePos.row - 1}}) {
55 if (candidate.col < 0 || candidate.col >= targetModel.columns() ||
56 candidate.row < 0 || candidate.row >= targetModel.rows())
57 continue;
58 if (!targetModel.isLegalMemAffinity(tilePos.col, tilePos.row, candidate.col,
59 candidate.row))
60 continue;
61 neighbors.push_back(candidate);
62 }
63 return neighbors;
64}
65
66/// Bytes a buffer takes on a tile once assign-buffer-addresses aligns it.
67static int64_t alignedBufferBytes(const AIETargetModel &targetModel,
68 AIETileType tileType, int64_t bytes) {
69 TileMemoryLimits limits = tileMemoryLimits(targetModel, tileType);
70 return llvm::alignTo(bytes,
72 limits.maxVecAlignBits, bytes) /
73 8);
74}
75
76//===----------------------------------------------------------------------===//
77// SASchedule
78//===----------------------------------------------------------------------===//
79
80SASchedule::SASchedule(double startTemp, int movesPerIter, int maxIters,
81 int greedyIters, const SAConfig &cfg)
82 : temperature(startTemp), movesPerIter(movesPerIter), maxIters(maxIters),
83 greedyIters(greedyIters),
84 windowSize(std::max(movesPerIter, cfg.windowSize)), config(cfg) {}
85
87 return currIteration >= maxIters ||
88 (inGreedyStage && currGreedyIteration >= greedyIters);
89}
90
92 int total = acceptCount + rejectCount;
93 return (total == 0) ? 1.0 : static_cast<double>(acceptCount) / total;
94}
95
96void SASchedule::record(bool accepted) {
97 history.push_back(accepted ? 1 : 0);
98 ++(accepted ? acceptCount : rejectCount);
99 trimWindow();
100}
101
103 if (!inGreedyStage) {
104 double ratio = getAcceptanceRatio();
105 double adaptiveFactor;
106 if (ratio > 0.96)
107 adaptiveFactor = config.coolFast;
108 else if (ratio > 0.8)
109 adaptiveFactor = config.coolSteady;
110 else if (ratio > 0.15)
111 adaptiveFactor = config.coolBalanced;
112 else
113 adaptiveFactor = config.coolStuck;
114
115 temperature *= adaptiveFactor;
116
117 if (ratio < config.greedyThreshold || temperature < 1e-8) {
118 inGreedyStage = true;
119 temperature = 0.0;
120 }
121 } else {
122 currGreedyIteration++;
123 }
124 currIteration++;
125}
126
127void SASchedule::trimWindow() {
128 while (static_cast<int>(history.size()) > windowSize) {
129 if (history.front() == 1)
130 acceptCount--;
131 else
132 rejectCount--;
133 history.pop_front();
134 }
135}
136
137//===----------------------------------------------------------------------===//
138// Net model
139//===----------------------------------------------------------------------===//
140
141void SAPlacer::buildNetModel(SmallVector<ObjectFifoCreateOp> &objectFifos,
142 SmallVector<ObjectFifoLinkOp> &objectFifoLinks) {
143 nets.clear();
144 tileToNetIndices.clear();
145
146 for (auto ofOp : objectFifos) {
147 NetInfo net;
148 Value producerTile = ofOp.getProducerTile();
149 if (auto *prodOp = producerTile.getDefiningOp())
150 if (isa<LogicalTileOp>(prodOp))
151 net.endpoints.push_back(prodOp);
152
153 for (Value consumerTile : ofOp.getConsumerTiles()) {
154 if (auto *consOp = consumerTile.getDefiningOp())
155 if (isa<LogicalTileOp>(consOp))
156 net.endpoints.push_back(consOp);
157 }
158
159 // Skip degenerate nets (0-1 endpoints)
160 if (net.endpoints.size() < 2)
161 continue;
162
163 // Cost increase for large multicast
164 net.isMulticast = (ofOp.getConsumerTiles().size() >
165 static_cast<unsigned>(config.multicastThreshold));
166
167 size_t idx = nets.size();
168 nets.push_back(net);
169
170 // Build reverse index
171 for (auto *ep : net.endpoints)
172 tileToNetIndices[ep].push_back(idx);
173 }
174}
175
176// A route is a net over the tiles of its ends, and each DMA end is one channel
177// on its tile; a pool is bytes on its tile, and descriptors when the tile is a
178// mem tile. None of it needs the DMA-or-shared-memory guess a fifo needs: a
179// route already moves data by DMA.
180void SAPlacer::buildRouteModel(DeviceOp device, ArrayRef<RouteOp> routes) {
181 routeEnds.clear();
182 poolBDs.clear();
183 for (RouteOp route : routes) {
184 NetInfo net;
185 auto addEnd = [&](StringRef name, bool output) {
186 auto endpoint = SymbolTable::lookupNearestSymbolFrom<RouteEndpoint>(
187 route, StringAttr::get(route.getContext(), name));
188 if (!endpoint)
189 return;
190 Operation *tileOp = endpoint.getTile().getDefiningOp();
191 if (!isa_and_nonnull<LogicalTileOp>(tileOp))
192 return;
193 net.endpoints.push_back(tileOp);
194 if (endpoint.getRouteBundle() == WireBundle::DMA)
195 routeEnds.push_back({tileOp, output});
196 };
197 for (StringRef source : route.getSourceNames())
198 addEnd(source, /*output=*/true);
199 for (StringRef dest : route.getDestinationNames())
200 addEnd(dest, /*output=*/false);
201 if (net.endpoints.size() < 2)
202 continue;
203 unsigned threshold = static_cast<unsigned>(config.multicastThreshold);
204 net.isMulticast = route.getDestinations().size() > threshold ||
205 route.getSources().size() > threshold;
206 size_t idx = nets.size();
207 nets.push_back(net);
208 for (auto *ep : net.endpoints)
209 tileToNetIndices[ep].push_back(idx);
210 }
211
212 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
213 Operation *tileOp = pool.getTile().getDefiningOp();
214 if (!isa_and_nonnull<LogicalTileOp>(tileOp))
215 continue;
216 staticBufferSizes[tileOp] += pool.getObjectSizeInBytes() * pool.getDepth();
217 auto typeIt = tileTypes.find(tileOp);
218 if (typeIt != tileTypes.end() && typeIt->second == AIETileType::MemTile)
219 poolBDs[tileOp] += pool.getDepth();
220 }
221}
222
223int SAPlacer::computeNetHPWL(const NetInfo &net) const {
224 int spanCol = net.bb.maxCol - net.bb.minCol;
225 int spanRow = net.bb.maxRow - net.bb.minRow;
226 int hpwl = spanCol + spanRow;
227 if (net.isMulticast)
228 hpwl *= config.multicastMultiplier;
229 // Area penalty: penalizes spread-out nets that consume more routing tracks.
230 // A row-aligned broadcast (area=0) is much cheaper to route than a
231 // scattered one (area=cols*rows).
232 int area = spanCol * spanRow;
233 return hpwl + area;
234}
235
236int SAPlacer::computeTotalHPWL() const {
237 int total = 0;
238 for (const auto &net : nets)
239 total += computeNetHPWL(net);
240 return total;
241}
242
243void SAPlacer::initBoundingBoxes() {
244 for (auto &net : nets) {
245 if (net.endpoints.empty())
246 continue;
247
248 TileID first = currentPlacement[net.endpoints[0]];
249 net.bb = {first.col, first.col, first.row, first.row, 1, 1, 1, 1};
250
251 for (size_t i = 1; i < net.endpoints.size(); i++) {
252 TileID pos = currentPlacement[net.endpoints[i]];
253
254 if (pos.col < net.bb.minCol) {
255 net.bb.minCol = pos.col;
256 net.bb.countAtMinCol = 1;
257 } else if (pos.col == net.bb.minCol) {
258 net.bb.countAtMinCol++;
259 }
260
261 if (pos.col > net.bb.maxCol) {
262 net.bb.maxCol = pos.col;
263 net.bb.countAtMaxCol = 1;
264 } else if (pos.col == net.bb.maxCol) {
265 net.bb.countAtMaxCol++;
266 }
267
268 if (pos.row < net.bb.minRow) {
269 net.bb.minRow = pos.row;
270 net.bb.countAtMinRow = 1;
271 } else if (pos.row == net.bb.minRow) {
272 net.bb.countAtMinRow++;
273 }
274
275 if (pos.row > net.bb.maxRow) {
276 net.bb.maxRow = pos.row;
277 net.bb.countAtMaxRow = 1;
278 } else if (pos.row == net.bb.maxRow) {
279 net.bb.countAtMaxRow++;
280 }
281 }
282 }
283}
284
285//===----------------------------------------------------------------------===//
286// Incremental move evaluation
287//===----------------------------------------------------------------------===//
288
289int SAPlacer::evaluateMove(
290 Operation *tile, TileID newPos,
291 SmallVector<std::pair<size_t, NetBoundingBox>> &backups) {
292 backups.clear();
293 int deltaCost = 0;
294
295 auto it = tileToNetIndices.find(tile);
296 if (it == tileToNetIndices.end())
297 return 0;
298
299 for (size_t netIdx : it->second) {
300 NetInfo &net = nets[netIdx];
301 backups.push_back({netIdx, net.bb});
302 int oldCost = computeNetHPWL(net);
303
304 // Recompute BB from scratch for this net
305 int newMinCol = INT_MAX, newMaxCol = INT_MIN;
306 int newMinRow = INT_MAX, newMaxRow = INT_MIN;
307 int cMinCol = 0, cMaxCol = 0, cMinRow = 0, cMaxRow = 0;
308
309 for (auto *ep : net.endpoints) {
310 TileID pos = (ep == tile) ? newPos : currentPlacement[ep];
311
312 if (pos.col < newMinCol) {
313 newMinCol = pos.col;
314 cMinCol = 1;
315 } else if (pos.col == newMinCol) {
316 cMinCol++;
317 }
318 if (pos.col > newMaxCol) {
319 newMaxCol = pos.col;
320 cMaxCol = 1;
321 } else if (pos.col == newMaxCol) {
322 cMaxCol++;
323 }
324 if (pos.row < newMinRow) {
325 newMinRow = pos.row;
326 cMinRow = 1;
327 } else if (pos.row == newMinRow) {
328 cMinRow++;
329 }
330 if (pos.row > newMaxRow) {
331 newMaxRow = pos.row;
332 cMaxRow = 1;
333 } else if (pos.row == newMaxRow) {
334 cMaxRow++;
335 }
336 }
337
338 net.bb = {newMinCol, newMaxCol, newMinRow, newMaxRow,
339 cMinCol, cMaxCol, cMinRow, cMaxRow};
340
341 deltaCost += computeNetHPWL(net) - oldCost;
342 }
343
344 return deltaCost;
345}
346
347void SAPlacer::revertMove(
348 SmallVector<std::pair<size_t, NetBoundingBox>> &backups) {
349 for (auto &[netIdx, savedBB] : backups)
350 nets[netIdx].bb = savedBB;
351}
352
353//===----------------------------------------------------------------------===//
354// Move generation
355//===----------------------------------------------------------------------===//
356
357bool SAPlacer::isLegalPosition(Operation *tile, TileID pos) const {
358 auto it = tileTypes.find(tile);
359 if (it == tileTypes.end())
360 return false;
361
362 AIETileType needed = it->second;
363 AIETileType actual = targetModel->getTileType(pos.col, pos.row);
364 if (needed != actual)
365 return false;
366
367 // Compute tiles: must be unoccupied
368 if (needed == AIETileType::CoreTile) {
369 auto occIt = physToLogical.find(pos);
370 if (occIt != physToLogical.end() && occIt->second != tile)
371 return false;
372 }
373
374 return satisfiesConstraints(tile, pos);
375}
376
377//===----------------------------------------------------------------------===//
378// Memory capacity tracking
379//===----------------------------------------------------------------------===//
380
381std::optional<TileID> SAPlacer::positionOf(Operation *tile) const {
382 if (auto it = currentPlacement.find(tile);
383 tile && it != currentPlacement.end())
384 return it->second;
385 if (auto tileOp = dyn_cast_or_null<TileOp>(tile))
386 return tileOp.getTileID();
387 return std::nullopt;
388}
389
390// Add a single fifo's contributions to memory and DMA usage maps.
391void SAPlacer::addRouteEndContribution(size_t endIdx, int sign) {
392 const RouteEndInfo &end = routeEnds[endIdx];
393 auto posIt = currentPlacement.find(end.tile);
394 if (posIt == currentPlacement.end())
395 return;
396 if (end.output)
397 currentDMAUsage[posIt->second].second += sign;
398 else
399 currentDMAUsage[posIt->second].first += sign;
400}
401
402void SAPlacer::addFifoContribution(size_t fifoIdx, int sign) {
403 const auto &fb = fifoBuffers[fifoIdx];
404 Operation *prod = fb.producer;
405 if (!prod)
406 return;
407 auto prodPlaceIt = currentPlacement.find(prod);
408 if (prodPlaceIt == currentPlacement.end())
409 return;
410 TileID prodPos = prodPlaceIt->second;
411 auto prodTypeIt = tileTypes.find(prod);
412 bool prodIsCore = prodTypeIt != tileTypes.end() &&
413 prodTypeIt->second == AIETileType::CoreTile;
414 bool prodIsShim = prodTypeIt != tileTypes.end() &&
415 (prodTypeIt->second == AIETileType::ShimNOCTile ||
416 prodTypeIt->second == AIETileType::ShimPLTile);
417
418 // A fifo that already has an aie.objectfifo.allocate keeps its shared-memory
419 // objects on that delegate tile.
420 std::optional<TileID> delegatePos = positionOf(fb.delegate);
421
422 // Charge memory based on connection topology:
423 // intratile → max(sizes) * max(depths) on shared tile
424 // shared-mem → max(sizes) * max(depths) on chosen adjacent tile
425 // core↔core DMA → prod: prodSize*dmaDepth, cons: consSize*dmaDepth
426 // core→mem/shim → prod: dmaDepth, cons: declaredDepth (skip shim)
427 // mem/shim→core → prod: declaredDepth (skip shim/link), cons: dmaDepth
428 // mem↔mem → both declaredDepth (skip shim/link)
429 bool prodCharged = false;
430 bool prodNeedsDMA = false;
431
432 for (size_t ci = 0; ci < fb.consumers.size(); ci++) {
433 auto *cons = fb.consumers[ci];
434 if (!cons)
435 continue;
436 auto consPlaceIt = currentPlacement.find(cons);
437 if (consPlaceIt == currentPlacement.end())
438 continue;
439 TileID consPos = consPlaceIt->second;
440 auto consTypeIt = tileTypes.find(cons);
441 bool consIsCore = consTypeIt != tileTypes.end() &&
442 consTypeIt->second == AIETileType::CoreTile;
443 bool consIsShim = consTypeIt != tileTypes.end() &&
444 (consTypeIt->second == AIETileType::ShimNOCTile ||
445 consTypeIt->second == AIETileType::ShimPLTile);
446
447 // Declared depth (for shared-mem) and DMA depth (for DMA connections).
448 // The appropriate depth is selected in each branch below based on
449 // whether the connection uses DMA or shared memory.
450 int consDeclDepth = fb.consumerDepthAt(ci);
451 int consDMADepth = (ci < fb.consumerDMADepths.size())
452 ? fb.consumerDMADepths[ci]
453 : fb.producerDMADepth;
454
455 // Check shared memory between core tiles (skip if fifo forces DMA)
456 bool isSharedMem =
457 !fb.forcesDMA && prodIsCore && consIsCore && prodPos != consPos &&
458 (targetModel->isLegalMemAffinity(prodPos.col, prodPos.row, consPos.col,
459 consPos.row) ||
460 targetModel->isLegalMemAffinity(consPos.col, consPos.row, prodPos.col,
461 prodPos.row));
462
463 if (prodPos == consPos) {
464 // Intratile: shared-mem, use declared depths
465 int depth = std::max(fb.producerDepth, consDeclDepth);
466 int64_t maxSize = std::max(fb.producerSizeBytes, fb.consumerSizeBytes);
467 int64_t bytes = maxSize * depth;
468 currentMemUsage[delegatePos.value_or(prodPos)] += sign * bytes;
469 } else if (isSharedMem) {
470 // Shared memory: use declared depths
471 bool rightShared = targetModel->isLegalMemAffinity(
472 prodPos.col, prodPos.row, consPos.col, consPos.row);
473 bool leftShared = targetModel->isLegalMemAffinity(
474 consPos.col, consPos.row, prodPos.col, prodPos.row);
475 TileID bufTile;
476 if (delegatePos) {
477 bufTile = *delegatePos;
478 } else if (rightShared && leftShared) {
479 if (sign > 0) {
480 int64_t prodUsed =
481 currentMemUsage.count(prodPos) ? currentMemUsage[prodPos] : 0;
482 int64_t consUsed =
483 currentMemUsage.count(consPos) ? currentMemUsage[consPos] : 0;
484 bufTile = (prodUsed <= consUsed) ? prodPos : consPos;
485 // Only track shared-mem destination for single-consumer fifos.
486 // allocate ops require all endpoints share access to the delegate
487 // tile, which is only guaranteed for single-consumer fifos.
488 if (fb.consumers.size() == 1)
489 sharedMemDestination[fifoIdx] = bufTile;
490 } else {
491 auto it = sharedMemDestination.find(fifoIdx);
492 bufTile = (it != sharedMemDestination.end()) ? it->second : prodPos;
493 }
494 } else if (rightShared) {
495 bufTile = consPos;
496 } else {
497 bufTile = prodPos;
498 }
499 int depth = std::max(fb.producerDepth, consDeclDepth);
500 int64_t maxSize = std::max(fb.producerSizeBytes, fb.consumerSizeBytes);
501 currentMemUsage[bufTile] += sign * maxSize * depth;
502 } else if (prodIsCore && consIsCore) {
503 // Non-adjacent core-to-core: DMA, use DMA depths (maxAcquire+1)
504 if (!prodCharged) {
505 int64_t bytes = fb.producerSizeBytes * fb.producerDMADepth;
506 currentMemUsage[prodPos] += sign * bytes;
507 prodCharged = true;
508 }
509 int64_t consBytes = fb.consumerSizeBytes * consDMADepth;
510 currentMemUsage[consPos] += sign * consBytes;
511 } else if (prodIsCore && !consIsCore) {
512 // CoreTile producer → MemTile/Shim consumer
513 // Producer (core): DMA depth. Consumer (MemTile): declared depth.
514 if (!prodCharged) {
515 int64_t bytes = fb.producerSizeBytes * fb.producerDMADepth;
516 currentMemUsage[prodPos] += sign * bytes;
517 prodCharged = true;
518 }
519 if (!consIsShim) {
520 // MemTile consumer: use declared depth
521 int64_t consBytes = fb.consumerSizeBytes * consDeclDepth;
522 currentMemUsage[consPos] += sign * consBytes;
523 }
524 } else if (!prodIsCore && consIsCore) {
525 // MemTile/Shim producer → CoreTile consumer
526 // Producer (MemTile): declared depth. Consumer (core): DMA depth.
527 if (!prodCharged && !prodIsShim && !fb.linkSharedProd) {
528 currentMemUsage[prodPos] +=
529 sign * fb.producerSizeBytes * fb.producerDepth;
530 prodCharged = true;
531 }
532 int64_t consBytes = fb.consumerSizeBytes * consDMADepth;
533 currentMemUsage[consPos] += sign * consBytes;
534 } else if (!prodIsCore && !consIsCore) {
535 // MemTile/Shim → MemTile/Shim: both use declared depths
536 if (!prodCharged && !prodIsShim && !fb.linkSharedProd) {
537 currentMemUsage[prodPos] +=
538 sign * fb.producerSizeBytes * fb.producerDepth;
539 prodCharged = true;
540 }
541 if (!consIsShim) {
542 int64_t consBytes = fb.consumerSizeBytes * consDeclDepth;
543 currentMemUsage[consPos] += sign * consBytes;
544 }
545 }
546
547 // DMA contribution — shared-memory connections use no DMA. A mem tile has
548 // no core, so a fifo from one back to itself, between two links there,
549 // runs through its DMAs.
550 bool loopsBack = prodPos == consPos && !prodIsCore && !prodIsShim;
551 if ((prodPos != consPos || loopsBack) && !isSharedMem) {
552 currentDMAUsage[consPos].first += sign;
553 prodNeedsDMA = true;
554 }
555 }
556
557 if (prodNeedsDMA)
558 currentDMAUsage[prodPos].second += sign;
559}
560
561void SAPlacer::initResourceTracking() {
562 currentMemUsage.clear();
563 currentDMAUsage.clear();
564 sharedMemDestination.clear();
565
566 // Build reverse index: tile → fifo indices
567 tileToFifoIndices.clear();
568 for (size_t i = 0; i < fifoBuffers.size(); i++) {
569 const auto &fb = fifoBuffers[i];
570 if (fb.producer)
571 tileToFifoIndices[fb.producer].push_back(i);
572 for (auto *cons : fb.consumers)
573 if (cons)
574 tileToFifoIndices[cons].push_back(i);
575 if (fb.delegate)
576 tileToFifoIndices[fb.delegate].push_back(i);
577 }
578
579 // Static buffers and stacks go first: a shared-memory fifo picks the emptier
580 // of its two tiles as it is added, and must see what they already hold.
581 for (auto &[op, bufSize] : staticBufferSizes) {
582 auto posIt = currentPlacement.find(op);
583 if (posIt != currentPlacement.end())
584 currentMemUsage[posIt->second] += bufSize;
585 }
586 // Add core stack overhead (reserved by assign-buffer-addresses pass)
587 for (auto &[op, pos] : currentPlacement) {
588 auto typeIt = tileTypes.find(op);
589 if (typeIt == tileTypes.end() || typeIt->second != AIETileType::CoreTile)
590 continue;
591 auto it = stackSizes.find(op);
592 if (it != stackSizes.end())
593 currentMemUsage[pos] += it->second;
594 }
595
596 for (size_t i = 0; i < fifoBuffers.size(); i++)
597 addFifoContribution(i, +1);
598
599 tileToRouteEnds.clear();
600 for (size_t i = 0; i < routeEnds.size(); i++) {
601 tileToRouteEnds[routeEnds[i].tile].push_back(i);
602 addRouteEndContribution(i, +1);
603 }
604 cachedResourcePenalty = computePenalty();
605}
606
607int SAPlacer::computeMemoryPressure() const {
608 int64_t coreCapacity = targetModel->getLocalMemorySize();
609 int64_t memTileCapacity = targetModel->getMemTileSize();
610 int pressure = 0;
611
612 for (auto &[tilePos, used] : currentMemUsage) {
613 auto tileType = targetModel->getTileType(tilePos.col, tilePos.row);
614 if (tileType == AIETileType::ShimNOCTile ||
615 tileType == AIETileType::ShimPLTile)
616 continue;
617 int64_t capacity =
618 (tileType == AIETileType::MemTile) ? memTileCapacity : coreCapacity;
619 double utilization = static_cast<double>(used) / capacity;
620 if (utilization > config.pressureThreshold) {
621 double excess = (utilization - config.pressureThreshold) /
622 (1.0 - config.pressureThreshold);
623 pressure += static_cast<int>(config.pressurePerKB * excess * excess);
624 }
625 }
626 return pressure;
627}
628
629int SAPlacer::computePenalty() const {
630 return computeMemSpilloverPenalty() + computeCoreOverflowPenalty() +
631 computeDMAChannelPenalty() + computeBDCountPenalty() +
632 computeDelegatePenalty();
633}
634
635// Both ends of a fifo with an aie.objectfifo.allocate must reach the delegate
636// tile's memory module, or the objectfifo split pass rejects the allocate.
637int SAPlacer::computeDelegatePenalty() const {
638 int penalty = 0;
639 for (const auto &fb : fifoBuffers) {
640 std::optional<TileID> delegatePos = positionOf(fb.delegate);
641 if (!delegatePos)
642 continue;
643 SmallVector<Operation *, 2> ends = {fb.producer};
644 llvm::append_range(ends, fb.consumers);
645 for (Operation *end : ends) {
646 std::optional<TileID> endPos = positionOf(end);
647 if (!endPos)
648 continue;
649 auto shared = targetModel->getSharedMemory(*delegatePos, *endPos);
652 continue;
653 int dist = std::abs(delegatePos->col - endPos->col) +
654 std::abs(delegatePos->row - endPos->row);
655 penalty += config.delegateWeightPerDist * std::max(dist - 1, 1);
656 }
657 }
658 return penalty;
659}
660
661// Simulates per-buffer MemTile allocation with neighbor spillover.
662// Approximates the objectfifo allocator's strategy: buffers are
663// collected globally across all MemTile columns, sorted largest-first,
664// and each buffer is placed on its home column when possible, spilling
665// to the neighbor with the most remaining capacity when the home is full.
666// Equal-size buffers may be visited in a different order than the
667// allocator's, so the two can break spill ties differently; the
668// allocator backtracks over its spill choices to cover that.
669int SAPlacer::computeMemSpilloverPenalty() const {
670 int64_t memTileCapacity = targetModel->getMemTileSize();
671 int numCols = targetModel->columns();
672 int penalty = 0;
673
674 // Collect all MemTile buffers globally, each tagged with its home column.
675 // Pair: (bufferSize, homeColumn).
676 llvm::SmallVector<std::pair<int64_t, int>> allBufs;
677 llvm::SmallVector<int64_t> memTileBaseline(numCols, 0);
678
679 for (const auto &fb : fifoBuffers) {
680 if (!fb.producer)
681 continue;
682 auto prodIt = currentPlacement.find(fb.producer);
683 if (prodIt == currentPlacement.end())
684 continue;
685 TileID prodPos = prodIt->second;
686 auto prodTypeIt = tileTypes.find(fb.producer);
687 if (prodTypeIt == tileTypes.end())
688 continue;
689 if (prodTypeIt->second != AIETileType::MemTile)
690 continue;
691 if (fb.linkSharedProd)
692 continue;
693
694 int col = prodPos.col;
695 for (int d = 0; d < fb.producerDepth; d++)
696 allBufs.push_back({fb.producerSizeBytes, col});
697
698 for (size_t ci = 0; ci < fb.consumers.size(); ci++) {
699 auto *cons = fb.consumers[ci];
700 if (!cons)
701 continue;
702 auto consIt = currentPlacement.find(cons);
703 if (consIt == currentPlacement.end())
704 continue;
705 auto consTypeIt = tileTypes.find(cons);
706 if (consTypeIt == tileTypes.end() ||
707 consTypeIt->second != AIETileType::MemTile)
708 continue;
709 int consCol = consIt->second.col;
710 int consDepth = fb.consumerDepthAt(ci);
711 for (int d = 0; d < consDepth; d++)
712 allBufs.push_back({fb.consumerSizeBytes, consCol});
713 }
714 }
715
716 // Static buffers as baseline (non-spillable).
717 for (auto &[op, bufSize] : staticBufferSizes) {
718 auto posIt = currentPlacement.find(op);
719 if (posIt == currentPlacement.end())
720 continue;
721 auto typeIt = tileTypes.find(op);
722 if (typeIt == tileTypes.end() || typeIt->second != AIETileType::MemTile)
723 continue;
724 memTileBaseline[posIt->second.col] += bufSize;
725 }
726
727 // remaining[col] = capacity available after baseline.
728 llvm::SmallVector<int64_t> remaining(numCols, 0);
729 for (int col = 0; col < numCols; col++) {
730 if (targetModel->getTileType(col, 1) != AIETileType::MemTile)
731 continue;
732 remaining[col] =
733 std::max(memTileCapacity - memTileBaseline[col], int64_t{0});
734 }
735
736 // Sort globally by size descending (matching stateful transform).
737 llvm::sort(allBufs,
738 [](const auto &a, const auto &b) { return a.first > b.first; });
739
740 // Place each buffer: try home first, then neighbor with most capacity.
741 for (auto &[bufSize, homeCol] : allBufs) {
742 if (remaining[homeCol] >= bufSize) {
743 remaining[homeCol] -= bufSize;
744 } else {
745 // Pick neighbor with the most remaining capacity.
746 int bestNbr = -1;
747 int64_t bestCap = -1;
748 for (int nbr : {homeCol - 1, homeCol + 1}) {
749 if (nbr < 0 || nbr >= numCols)
750 continue;
751 if (targetModel->getTileType(nbr, 1) != AIETileType::MemTile)
752 continue;
753 if (remaining[nbr] >= bufSize && remaining[nbr] > bestCap) {
754 bestCap = remaining[nbr];
755 bestNbr = nbr;
756 }
757 }
758 if (bestNbr >= 0) {
759 remaining[bestNbr] -= bufSize;
760 } else {
761 // Ceiling division: bytes → KB.
762 penalty += ((bufSize + 1023) / 1024) * config.memPenaltyPerKB;
763 }
764 }
765 }
766
767 return penalty;
768}
769
770// Penalizes core tiles whose buffer usage exceeds local memory, accounting
771// for intratile fifos that can be relocated to a neighbor via allocate.
772int SAPlacer::computeCoreOverflowPenalty() const {
773 int64_t coreCapacity = targetModel->getLocalMemorySize();
774 int penalty = 0;
775 // Room a neighbour gives one overflowing tile is gone for the next, as
776 // generateAllocates hands it out.
777 llvm::DenseMap<TileID, int64_t> lent;
778
779 for (auto &[tilePos, used] : currentMemUsage) {
780 if (targetModel->getTileType(tilePos.col, tilePos.row) !=
781 AIETileType::CoreTile)
782 continue;
783 if (used <= coreCapacity)
784 continue;
785 int64_t overflow = used - coreCapacity;
786
787 // Build accessible neighbor spare capacities.
788 llvm::SmallVector<std::pair<int64_t, TileID>, 4> nbrSpares;
789 for (TileID nbr : getMemAffinityNeighbors(*targetModel, tilePos)) {
790 auto it = currentMemUsage.find(nbr);
791 int64_t nbrUsed = (it != currentMemUsage.end()) ? it->second : 0;
792 int64_t spare =
793 std::max(coreCapacity - nbrUsed - lent.lookup(nbr), int64_t{0});
794 if (spare > 0)
795 nbrSpares.push_back({spare, nbr});
796 }
797 llvm::sort(nbrSpares,
798 [](const auto &a, const auto &b) { return a.first > b.first; });
799
800 // Collect relocatable intratile fifo sizes on this tile.
801 int64_t unresolved = overflow;
802 auto opIt = physToLogical.find(tilePos);
803 if (opIt != physToLogical.end()) {
804 auto fifoIt = tileToFifoIndices.find(opIt->second);
805 if (fifoIt != tileToFifoIndices.end()) {
806 llvm::SmallVector<int64_t, 8> fifoSizes;
807 llvm::DenseSet<size_t> seen;
808 for (size_t fi : fifoIt->second) {
809 if (!seen.insert(fi).second)
810 continue;
811 const auto &fb = fifoBuffers[fi];
812 if (fb.consumers.size() != 1 || !fb.producer || !fb.consumers[0] ||
813 fb.delegate)
814 continue;
815 auto prodIt = currentPlacement.find(fb.producer);
816 auto consIt = currentPlacement.find(fb.consumers[0]);
817 if (prodIt == currentPlacement.end() ||
818 consIt == currentPlacement.end())
819 continue;
820 if (prodIt->second != tilePos || consIt->second != tilePos)
821 continue;
822 int consDepth = fb.consumerDepths.empty() ? fb.producerDepth
823 : fb.consumerDepths[0];
824 int64_t sz = std::max(fb.producerSizeBytes, fb.consumerSizeBytes) *
825 std::max(fb.producerDepth, consDepth);
826 fifoSizes.push_back(sz);
827 }
828 llvm::sort(fifoSizes, std::greater<int64_t>());
829
830 // Greedy packing: assign each fifo to the first neighbor that fits.
831 int64_t totalAbsorbed = 0;
832 for (int64_t sz : fifoSizes) {
833 for (auto &[spare, nbr] : nbrSpares) {
834 if (spare >= sz) {
835 spare -= sz;
836 lent[nbr] += sz;
837 totalAbsorbed += sz;
838 break;
839 }
840 }
841 }
842 unresolved = overflow - std::min(totalAbsorbed, overflow);
843 }
844 }
845
846 if (unresolved > 0)
847 penalty += ((unresolved + 1023) / 1024) * config.memPenaltyPerKB;
848 }
849
850 return penalty;
851}
852
853int SAPlacer::computeDMAChannelPenalty() const {
854 int penalty = 0;
855 for (auto &[tilePos, usage] : currentDMAUsage) {
856 auto [maxIn, maxOut] = getDMACapacity(*targetModel, tilePos);
857 int used[2] = {usage.first, usage.second};
858 int max[2] = {maxIn, maxOut};
859 for (int dir : {0, 1}) {
860 if (used[dir] <= max[dir])
861 continue;
862 if (shimSpans && targetModel->getTileType(tilePos.col, tilePos.row) ==
863 AIETileType::ShimNOCTile) {
864 SmallVector<StringAttr> ends;
865 for (auto &[lt, own] : shareableShimEnds) {
866 auto at = currentPlacement.find(lt);
867 if (at != currentPlacement.end() && at->second == tilePos)
868 llvm::append_range(ends, own[dir]);
869 }
870 used[dir] += static_cast<int>(shimSpans->groups(ends).size()) -
871 static_cast<int>(ends.size());
872 }
873 penalty +=
874 std::max(0, used[dir] - max[dir]) * config.dmaPenaltyPerChannel;
875 }
876 }
877 return penalty;
878}
879
880// Penalizes MemTiles that exceed their BD slot limit.
881int SAPlacer::computeBDCountPenalty() const {
882 int memTileBDMax = targetModel->getNumBDs(AIETileType::MemTile);
883 int penalty = 0;
884
885 llvm::DenseMap<TileID, int> memTileBDs;
886 for (const auto &fb : fifoBuffers) {
887 if (fb.producer) {
888 auto posIt = currentPlacement.find(fb.producer);
889 auto typeIt = tileTypes.find(fb.producer);
890 if (posIt != currentPlacement.end() && typeIt != tileTypes.end() &&
891 typeIt->second == AIETileType::MemTile && !fb.linkSharedProd)
892 memTileBDs[posIt->second] += fb.producerDepth;
893 }
894 for (size_t ci = 0; ci < fb.consumers.size(); ci++) {
895 auto *cons = fb.consumers[ci];
896 if (!cons)
897 continue;
898 auto posIt = currentPlacement.find(cons);
899 auto typeIt = tileTypes.find(cons);
900 if (posIt == currentPlacement.end() || typeIt == tileTypes.end() ||
901 typeIt->second != AIETileType::MemTile)
902 continue;
903 int depth = fb.consumerDepthAt(ci);
904 memTileBDs[posIt->second] += depth;
905 }
906 }
907 for (auto &[op, depth] : poolBDs) {
908 auto posIt = currentPlacement.find(op);
909 if (posIt != currentPlacement.end())
910 memTileBDs[posIt->second] += depth;
911 }
912 for (auto &[tilePos, totalBDs] : memTileBDs) {
913 if (totalBDs > memTileBDMax)
914 penalty += (totalBDs - memTileBDMax) * config.dmaPenaltyPerChannel;
915 }
916
917 return penalty;
918}
919
920int SAPlacer::computeAdjacencyPenalty(
921 const Adjacency &adj, ArrayRef<std::pair<int, int>> validOffsets,
922 int weight) const {
923 int penalty = 0;
924 for (auto [first, second] : adj.edges) {
925 auto firstIt = currentPlacement.find(first.getOperation());
926 auto secondIt = currentPlacement.find(second.getOperation());
927 if (firstIt == currentPlacement.end() || secondIt == currentPlacement.end())
928 continue;
929
930 TileID firstPos = firstIt->second;
931 TileID secondPos = secondIt->second;
932
933 // Find minimum Manhattan distance to any valid offset.
934 int minDist = INT_MAX;
935 for (auto [dc, dr] : validOffsets) {
936 int dist = std::abs(secondPos.col - (firstPos.col + dc)) +
937 std::abs(secondPos.row - (firstPos.row + dr));
938 minDist = std::min(minDist, dist);
939 }
940 if (minDist > 0)
941 penalty += weight * minDist;
942 }
943 return penalty;
944}
945
946int SAPlacer::computePairingPenalty() const {
947 int penalty = computeAdjacencyPenalty(cascadeAdjacency, kCascadeOffsets,
948 config.cascadeWeightPerDist);
949 for (auto [first, second] : sharedMemAdjacency.edges) {
950 auto firstIt = currentPlacement.find(first.getOperation());
951 auto secondIt = currentPlacement.find(second.getOperation());
952 if (firstIt == currentPlacement.end() || secondIt == currentPlacement.end())
953 continue;
954 TileID a = firstIt->second, b = secondIt->second;
955 if (a == b || targetModel->isLegalMemAffinity(a.col, a.row, b.col, b.row) ||
956 targetModel->isLegalMemAffinity(b.col, b.row, a.col, a.row))
957 continue;
958 int distance = std::abs(a.col - b.col) + std::abs(a.row - b.row);
959 penalty += config.sharedMemWeightPerDist * std::max(1, distance - 1);
960 }
961 return penalty;
962}
963
964void SAPlacer::generateAllocates() {
965 int64_t coreCapacity = targetModel->getLocalMemorySize();
966 DenseSet<size_t> emittedFifoIndices;
967
968 // Case A: Intratile fifo relocation for overflowing core tiles.
969 // Snapshot overflowing tiles first to avoid iterator invalidation
970 // (delegate updates modify currentMemUsage during packing).
971 SmallVector<TileID> overflowTiles;
972 for (auto &[tilePos, used] : currentMemUsage) {
973 if (targetModel->getTileType(tilePos.col, tilePos.row) ==
974 AIETileType::CoreTile &&
975 used > coreCapacity)
976 overflowTiles.push_back(tilePos);
977 }
978 for (auto &tilePos : overflowTiles) {
979
980 // Build accessible neighbor spare capacities: (tilePos, spare)
981 SmallVector<std::pair<TileID, int64_t>, 4> nbrSpares;
982 for (TileID nbr : getMemAffinityNeighbors(*targetModel, tilePos)) {
983 auto it = currentMemUsage.find(nbr);
984 int64_t nbrUsed = (it != currentMemUsage.end()) ? it->second : 0;
985 int64_t spare = std::max(coreCapacity - nbrUsed, int64_t{0});
986 if (spare > 0)
987 nbrSpares.push_back({nbr, spare});
988 }
989 llvm::sort(nbrSpares, [](const auto &a, const auto &b) {
990 return a.second > b.second;
991 });
992
993 // Collect relocatable intratile fifos on this tile
994 auto opIt = physToLogical.find(tilePos);
995 if (opIt == physToLogical.end())
996 continue;
997 auto fifoIt = tileToFifoIndices.find(opIt->second);
998 if (fifoIt == tileToFifoIndices.end())
999 continue;
1000
1001 // (fifoIndex, totalSize)
1002 SmallVector<std::pair<size_t, int64_t>, 8> relocFifos;
1003 DenseSet<size_t> seen;
1004 for (size_t fi : fifoIt->second) {
1005 if (!seen.insert(fi).second)
1006 continue;
1007 const auto &fb = fifoBuffers[fi];
1008 if (fb.consumers.size() != 1 || !fb.producer || !fb.consumers[0] ||
1009 fb.delegate)
1010 continue;
1011 auto prodIt = currentPlacement.find(fb.producer);
1012 auto consIt = currentPlacement.find(fb.consumers[0]);
1013 if (prodIt == currentPlacement.end() || consIt == currentPlacement.end())
1014 continue;
1015 if (prodIt->second != tilePos || consIt->second != tilePos)
1016 continue;
1017 int consDepth =
1018 fb.consumerDepths.empty() ? fb.producerDepth : fb.consumerDepths[0];
1019 int64_t sz = std::max(fb.producerSizeBytes, fb.consumerSizeBytes) *
1020 std::max(fb.producerDepth, consDepth);
1021 relocFifos.push_back({fi, sz});
1022 }
1023 llvm::sort(relocFifos, [](const auto &a, const auto &b) {
1024 return a.second > b.second;
1025 });
1026
1027 // Greedy: assign each whole fifo to the first neighbor that fits.
1028 // Verify against actual currentMemUsage to avoid overflowing delegate.
1029 for (auto &[fifoIdx, fifoSize] : relocFifos) {
1030 if (!emittedFifoIndices.insert(fifoIdx).second)
1031 continue;
1032 for (auto &[nbrPos, nbrSpare] : nbrSpares) {
1033 if (nbrSpare >= fifoSize) {
1034 // Double-check: delegate won't overflow after adding this fifo
1035 auto memIt = currentMemUsage.find(nbrPos);
1036 int64_t delegateUsed =
1037 (memIt != currentMemUsage.end()) ? memIt->second : 0;
1038 if (delegateUsed + fifoSize > coreCapacity)
1039 continue;
1040 nbrSpare -= fifoSize;
1041 currentMemUsage[nbrPos] += fifoSize;
1042 allocates.push_back({fifoBuffers[fifoIdx].fifoOp, nbrPos});
1043 break;
1044 }
1045 }
1046 }
1047 }
1048
1049 // Case B: Shared-mem destination communication.
1050 // sharedMemDestination is populated by addFifoContribution during
1051 // initResourceTracking, which runs on the final placement before this.
1052 for (auto &[fifoIdx, destTile] : sharedMemDestination) {
1053 if (emittedFifoIndices.contains(fifoIdx))
1054 continue;
1055 const auto &fb = fifoBuffers[fifoIdx];
1056 if (!fb.producer || fb.consumers.empty() || !fb.consumers[0])
1057 continue;
1058 auto prodIt = currentPlacement.find(fb.producer);
1059 auto consIt = currentPlacement.find(fb.consumers[0]);
1060 if (prodIt == currentPlacement.end() || consIt == currentPlacement.end())
1061 continue;
1062 if (prodIt->second == consIt->second)
1063 continue;
1064 if (destTile != prodIt->second)
1065 allocates.push_back({fb.fifoOp, destTile});
1066 }
1067}
1068
1069int SAPlacer::updateResourcePenalty(
1070 const SmallVector<std::pair<Operation *, TileID>> &oldPlacements) {
1071 // Collect all fifo indices affected by the moved tiles
1072 DenseSet<size_t> affectedFifos;
1073 for (auto &[op, _] : oldPlacements) {
1074 auto it = tileToFifoIndices.find(op);
1075 if (it != tileToFifoIndices.end())
1076 for (size_t idx : it->second)
1077 affectedFifos.insert(idx);
1078 }
1079
1080 // Update static buffer and stack contributions for moved tiles
1081 for (auto &[op, oldPos] : oldPlacements) {
1082 auto bufIt = staticBufferSizes.find(op);
1083 if (bufIt != staticBufferSizes.end())
1084 currentMemUsage[oldPos] -= bufIt->second;
1085 auto stackIt = stackSizes.find(op);
1086 if (stackIt != stackSizes.end())
1087 currentMemUsage[oldPos] -= stackIt->second;
1088 }
1089
1090 // Subtract old fifo contributions (positions already updated in
1091 // currentPlacement, but we need old positions for subtraction).
1092 // Temporarily swap back old positions for affected fifos.
1093 DenseMap<Operation *, TileID> savedPositions;
1094 for (auto &[op, oldPos] : oldPlacements) {
1095 savedPositions[op] = currentPlacement[op];
1096 currentPlacement[op] = oldPos;
1097 }
1098 for (size_t fi : affectedFifos)
1099 addFifoContribution(fi, -1);
1100 for (auto &[op, _] : oldPlacements)
1101 for (size_t ei : tileToRouteEnds.lookup(op))
1102 addRouteEndContribution(ei, -1);
1103
1104 // Restore new positions and add new contributions
1105 for (auto &[op, newPos] : savedPositions)
1106 currentPlacement[op] = newPos;
1107 for (size_t fi : affectedFifos)
1108 addFifoContribution(fi, +1);
1109 for (auto &[op, _] : oldPlacements)
1110 for (size_t ei : tileToRouteEnds.lookup(op))
1111 addRouteEndContribution(ei, +1);
1112
1113 // Add new static buffer and stack contributions
1114 for (auto &[op, _] : oldPlacements) {
1115 TileID newPos = currentPlacement[op];
1116 auto bufIt = staticBufferSizes.find(op);
1117 if (bufIt != staticBufferSizes.end())
1118 currentMemUsage[newPos] += bufIt->second;
1119 auto stackIt = stackSizes.find(op);
1120 if (stackIt != stackSizes.end())
1121 currentMemUsage[newPos] += stackIt->second;
1122 }
1123
1124 cachedResourcePenalty = computePenalty();
1125 return cachedResourcePenalty;
1126}
1127
1128//===----------------------------------------------------------------------===//
1129// Move generation
1130//===----------------------------------------------------------------------===//
1131
1132bool SAPlacer::generateMove(
1133 SmallVector<std::pair<Operation *, TileID>> &moves) {
1134 if (movableTiles.empty())
1135 return false;
1136
1137 for (int attempt = 0; attempt < config.maxMoveAttempts; attempt++) {
1138 // Pick a random movable tile.
1139 Operation *tile = movableTiles[drawBoundedIndex(rng, movableTiles.size())];
1140
1141 auto typeIt = tileTypes.find(tile);
1142 if (typeIt == tileTypes.end())
1143 continue;
1144 AIETileType type = typeIt->second;
1145
1146 // Pick a random target position of matching type.
1147 const auto &candidates = (type == AIETileType::CoreTile)
1148 ? availability.compTiles
1149 : availability.nonCompTiles;
1150 if (candidates.empty())
1151 continue;
1152
1153 TileID targetPos = candidates[drawBoundedIndex(rng, candidates.size())];
1154
1155 // Filter by tile type for non-compute (nonCompTiles has mixed types).
1156 if (type != AIETileType::CoreTile &&
1157 targetModel->getTileType(targetPos.col, targetPos.row) != type)
1158 continue;
1159
1160 TileID currentPos = currentPlacement[tile];
1161 if (targetPos == currentPos)
1162 continue;
1163
1164 if (!satisfiesConstraints(tile, targetPos))
1165 continue;
1166
1167 // Check if target is occupied by another movable tile.
1168 Operation *occupant = nullptr;
1169 for (auto *t : movableTiles) {
1170 if (t != tile && currentPlacement[t] == targetPos) {
1171 occupant = t;
1172 break;
1173 }
1174 }
1175
1176 if (occupant) {
1177 // Swap: both tiles must accept each other's position.
1178 if (tileTypes[occupant] != type)
1179 continue;
1180 if (!satisfiesConstraints(occupant, currentPos))
1181 continue;
1182 moves.clear();
1183 moves.push_back({tile, targetPos});
1184 moves.push_back({occupant, currentPos});
1185 } else {
1186 // Shift: target is empty.
1187 if (!isLegalPosition(tile, targetPos))
1188 continue;
1189 moves.clear();
1190 moves.push_back({tile, targetPos});
1191 }
1192 return true;
1193 }
1194 return false;
1195}
1196
1197//===----------------------------------------------------------------------===//
1198// Initial temperature estimation
1199//===----------------------------------------------------------------------===//
1200
1201double SAPlacer::estimateInitialTemperature(int numSamples) {
1202 // Collect delta costs from random moves
1203 std::vector<double> deltaCosts;
1204 deltaCosts.reserve(numSamples);
1205
1206 SmallVector<std::pair<size_t, NetBoundingBox>> backups;
1207
1208 for (int i = 0; i < numSamples; i++) {
1209 SmallVector<std::pair<Operation *, TileID>> moves;
1210 if (!generateMove(moves))
1211 continue;
1212
1213 // Use the first tile's HPWL delta as cost sample.
1214 int delta = evaluateMove(moves[0].first, moves[0].second, backups);
1215 deltaCosts.push_back(delta);
1216 revertMove(backups);
1217 }
1218
1219 if (deltaCosts.empty())
1220 return 1.0;
1221
1222 // Equilibrium binary search: find T where E[delta_cost] ≈ 0,
1223 // improving and worsening moves roughly cancel out and
1224 // SA accepts most moves freely - good starting point
1225 double maxDelta = 0.0;
1226 for (double d : deltaCosts)
1227 maxDelta = std::max(maxDelta, std::abs(d));
1228
1229 if (maxDelta == 0.0)
1230 return 1.0;
1231
1232 auto getExpectedValue = [&](double T) -> double {
1233 double ev = 0.0;
1234 for (double d : deltaCosts) {
1235 if (d <= 0)
1236 ev += d;
1237 else
1238 ev += d * std::exp(-d / T);
1239 }
1240 return ev;
1241 };
1242
1243 double lowT = 0.0;
1244 double highT = config.tempSearchCeiling * maxDelta;
1245 double T = 1.0;
1246
1247 for (int iter = 0; iter < config.tempSearchIters; iter++) {
1248 double ev = getExpectedValue(T);
1249 if (std::abs(highT - lowT) / std::max(T, config.tempSearchEpsilon) <
1250 config.tempSearchTolerance)
1251 break;
1252 if (ev > 0) {
1253 // Expected value positive → worsening moves dominate → T too high
1254 // (accepting too many bad moves). Lower the ceiling.
1255 highT = T;
1256 } else {
1257 // Expected value negative → improving moves dominate → T too low.
1258 // Raise the floor.
1259 lowT = T;
1260 }
1261 T = (lowT + highT) / 2.0;
1262 }
1263
1264 return T;
1265}
1266
1267//===----------------------------------------------------------------------===//
1268// Memory weight estimation (for initial placement sorting)
1269//===----------------------------------------------------------------------===//
1270
1271int64_t SAPlacer::computeTileMemoryWeight(Operation *tile) const {
1272 int64_t weight = 0;
1273
1274 // Static buffers (aie.buffer ops)
1275 auto bufIt = staticBufferSizes.find(tile);
1276 if (bufIt != staticBufferSizes.end())
1277 weight += bufIt->second;
1278
1279 // Core stack overhead
1280 auto stackIt = stackSizes.find(tile);
1281 if (stackIt != stackSizes.end())
1282 weight += stackIt->second;
1283
1284 // Fifo endpoint contributions
1285 for (const auto &fb : fifoBuffers) {
1286 bool isProd = (fb.producer == tile);
1287 int consIdx = -1;
1288 for (size_t ci = 0; ci < fb.consumers.size(); ci++) {
1289 if (fb.consumers[ci] == tile) {
1290 consIdx = static_cast<int>(ci);
1291 break;
1292 }
1293 }
1294 if (!isProd && consIdx < 0)
1295 continue;
1296
1297 if (isProd && consIdx >= 0) {
1298 // Intratile: max(sizes) * max(depths)
1299 int depth = std::max(fb.producerDepth, fb.consumerDepths[consIdx]);
1300 int64_t sz = std::max(fb.producerSizeBytes, fb.consumerSizeBytes);
1301 weight += sz * depth;
1302 } else if (isProd) {
1303 weight += fb.producerSizeBytes * fb.producerDepth;
1304 } else {
1305 weight += fb.consumerSizeBytes * fb.consumerDepths[consIdx];
1306 }
1307 }
1308 return weight;
1309}
1310
1311//===----------------------------------------------------------------------===//
1312// Post-SA mem/shim merge
1313//===----------------------------------------------------------------------===//
1314
1315LogicalResult SAPlacer::mergeMemShimTiles(DeviceOp device) {
1316 // After SA, each logical tile has its own physical position.
1317 // Mem/shim tiles in the same column and of the same type share a
1318 // physical tile. The SA's penalty model already enforces DMA capacity
1319 // and memory constraints, so merge unconditionally.
1320
1321 // Group non-compute logical tiles by (col, tileType)
1322 llvm::DenseMap<std::pair<int, AIETileType>, SmallVector<Operation *>> groups;
1323
1324 for (auto &[op, pos] : currentPlacement) {
1325 auto it = tileTypes.find(op);
1326 if (it == tileTypes.end())
1327 continue;
1328 if (it->second == AIETileType::CoreTile)
1329 continue;
1330 groups[{pos.col, it->second}].push_back(op);
1331 }
1332
1333 for (auto &[key, ops] : groups) {
1334 if (ops.size() <= 1)
1335 continue;
1336 TileID mergeTarget = currentPlacement[ops[0]];
1337 for (size_t i = 1; i < ops.size(); i++)
1338 currentPlacement[ops[i]] = mergeTarget;
1339 }
1340
1341 return success();
1342}
1343
1344//===----------------------------------------------------------------------===//
1345// SA placement
1346//===----------------------------------------------------------------------===//
1347
1348LogicalResult SAPlacer::place(DeviceOp device) {
1349 startTime = std::chrono::steady_clock::now();
1350
1351 // Seed RNG
1352 if (rngSeed == 0) {
1353 rng.seed(std::random_device{}());
1354 } else {
1355 rng.seed(rngSeed);
1356 }
1357
1358 // Phase 1-2: Collect IR ops, build net model, fifo info, cascade groups.
1359 if (failed(collectAndBuildModel(device)))
1360 return failure();
1361 // Phase 3: Classify tiles by type, generate initial placement.
1362 if (failed(generateInitialPlacement()))
1363 return failure();
1364 // Phase 4: Initialize bounding boxes, resource tracking, initial cost.
1365 initializeSAState();
1366 // Phase 5: SA main loop (skip if nothing to optimize).
1367 runSAMainLoop();
1368 // Phase 6: Restore best, generate allocates, merge mem/shim, stats.
1369 return finalizePlacement(device);
1370}
1371
1372LogicalResult SAPlacer::collectAndBuildModel(DeviceOp device) {
1373 collected = collectOperations(device);
1374 auto &logicalTiles = collected.logicalTiles;
1375 auto &objectFifos = collected.objectFifos;
1376 auto &objectFifoLinks = collected.objectFifoLinks;
1377
1378 if (logicalTiles.empty())
1379 return success();
1380
1381 // Cache tile types
1382 for (auto lt : logicalTiles)
1383 tileTypes[lt.getOperation()] = lt.getTileType();
1384
1385 // Collect static buffer sizes per logical tile
1386 device.walk([&](BufferOp bufOp) {
1387 auto *tileOp = bufOp.getTile().getDefiningOp();
1388 if (!tileOp || !isa<LogicalTileOp>(tileOp))
1389 return;
1390 int64_t bytes = bufOp.getAllocationSize();
1391 staticBufferSizes[tileOp] +=
1392 bufOp.getAligned()
1393 ? alignedBufferBytes(*targetModel, tileTypes.lookup(tileOp), bytes)
1394 : bytes;
1395 });
1396
1397 // Collect core stack and data regions (reserved by assign-buffer-addresses)
1398 device.walk([&](CoreOp coreOp) {
1399 auto *tileOp = coreOp.getTile().getDefiningOp();
1400 if (!tileOp || !isa<LogicalTileOp>(tileOp))
1401 return;
1402 // The data region a core sets aside for its own sections is as much its
1403 // memory as the stack.
1404 stackSizes[tileOp] = alignedBufferBytes(*targetModel, AIETileType::CoreTile,
1405 coreOp.getEffectiveStackSize()) +
1406 alignedBufferBytes(*targetModel, AIETileType::CoreTile,
1407 coreOp.getDataSize().value_or(
1408 config.coreSectionsBytes));
1409 });
1410
1411 // Build net model, fifo buffer info, and cascade groups
1412 buildNetModel(objectFifos, objectFifoLinks);
1413 buildFifoBufferInfo(device, objectFifos, objectFifoLinks);
1414 buildRouteModel(device, collected.routes);
1415 shareableShimEnds = collectShareableShimEnds(device, objectFifos);
1416 shimSpans.emplace(
1417 device, [&](StringAttr symbol) { return shimEndOf(device, symbol); });
1418 cascadeAdjacency = buildCascadeAdjacency(collected.cascadeFlows);
1419 LLVM_DEBUG(llvm::dbgs() << "[SA] Cascade adjacency: "
1420 << cascadeAdjacency.edges.size() << " edges\n");
1421 for (ObjectFifoCreateOp fifo : objectFifos)
1422 if (fifo.forcesSharedMemory() && fifo.getConsumerTiles().size() == 1)
1423 sharedMemAdjacency.addEdgeFromValues(fifo.getProducerTile(),
1424 fifo.getConsumerTiles()[0]);
1425
1426 return success();
1427}
1428
1429void SAPlacer::buildFifoBufferInfo(DeviceOp device,
1430 ArrayRef<ObjectFifoCreateOp> objectFifos,
1431 ArrayRef<ObjectFifoLinkOp> objectFifoLinks) {
1432 mlir::DataLayout dataLayout(device->getParentOfType<ModuleOp>());
1433
1434 llvm::StringMap<Operation *> delegates;
1435 for (auto alloc : device.getOps<ObjectFifoAllocateOp>())
1436 delegates[alloc.getObjFifoName()] = alloc.getDelegateTile().getDefiningOp();
1437
1438 for (auto ofOp : objectFifos) {
1439 FifoBufferInfo fb;
1440 fb.fifoOp = ofOp.getOperation();
1441 fb.delegate = delegates.lookup(ofOp.getSymName());
1442
1443 // Get producer
1444 auto *prodOp = ofOp.getProducerTile().getDefiningOp();
1445 fb.producer = (isa_and_nonnull<LogicalTileOp>(prodOp)) ? prodOp : nullptr;
1446
1447 // Get consumers
1448 for (Value ct : ofOp.getConsumerTiles()) {
1449 auto *consOp = ct.getDefiningOp();
1450 if (isa_and_nonnull<LogicalTileOp>(consOp))
1451 fb.consumers.push_back(consOp);
1452 }
1453
1454 // Compute buffer element size (producer side)
1455 auto fifoType = llvm::cast<AIEObjectFifoType>(ofOp.getElemType());
1456 auto elemType = llvm::cast<MemRefType>(fifoType.getElementType());
1457 int64_t elementBits =
1458 dataLayout.getTypeSizeInBits(elemType.getElementType());
1459 int64_t producerBytes = elemType.getNumElements() * elementBits / 8;
1460 AIETileType prodType =
1461 fb.producer ? tileTypes.lookup(fb.producer) : AIETileType::CoreTile;
1462 fb.producerSizeBytes =
1463 alignedBufferBytes(*targetModel, prodType, producerBytes);
1464
1465 // Consumer element size (may differ with consumerElemType)
1466 int64_t consumerBytes = producerBytes;
1467 if (auto consType = ofOp.getConsumerElemType()) {
1468 auto consOFType = llvm::cast<AIEObjectFifoType>(consType.value());
1469 auto consMemref = llvm::cast<MemRefType>(consOFType.getElementType());
1470 int64_t consBits =
1471 dataLayout.getTypeSizeInBits(consMemref.getElementType());
1472 consumerBytes = consMemref.getNumElements() * consBits / 8;
1473 }
1474 // One size serves every consumer, so align it for a core tile if any
1475 // consumer is one.
1476 bool coreConsumer = llvm::any_of(fb.consumers, [&](Operation *cons) {
1477 return tileTypes.lookup(cons) == AIETileType::CoreTile;
1478 });
1479 fb.consumerSizeBytes = alignedBufferBytes(
1480 *targetModel,
1481 coreConsumer ? AIETileType::CoreTile : AIETileType::MemTile,
1482 consumerBytes);
1483
1484 // Get per-endpoint depths: [producer, consumer0, consumer1, ...]
1485 if (auto arrayAttr = dyn_cast<ArrayAttr>(ofOp.getElemNumber())) {
1486 auto values = arrayAttr.getValue();
1487 fb.producerDepth =
1488 values.empty()
1489 ? 1
1490 : static_cast<int>(cast<IntegerAttr>(values[0]).getInt());
1491 for (size_t i = 0; i < fb.consumers.size(); i++) {
1492 size_t idx = i + 1; // consumer depths start at index 1
1493 int d = (idx < values.size())
1494 ? static_cast<int>(cast<IntegerAttr>(values[idx]).getInt())
1495 : fb.producerDepth;
1496 fb.consumerDepths.push_back(d);
1497 }
1498 } else {
1499 int d =
1500 static_cast<int>(cast<IntegerAttr>(ofOp.getElemNumber()).getInt());
1501 fb.producerDepth = d;
1502 for (size_t i = 0; i < fb.consumers.size(); i++)
1503 fb.consumerDepths.push_back(d);
1504 }
1505
1506 // Determine if this fifo forces DMA even when tiles are adjacent.
1507 // Must match dmaReason() in AIEObjectFifoSplit.cpp.
1508 bool isLinked = false;
1509 for (auto link : objectFifoLinks) {
1510 for (auto in : link.getInputObjectFifos())
1511 if (in.name() == ofOp.getSymName())
1512 isLinked = true;
1513 for (auto out : link.getOutputObjectFifos())
1514 if (out.name() == ofOp.getSymName())
1515 isLinked = true;
1516 }
1517 fb.forcesDMA = ofOp.forcesDMA() || ofOp.getRepeatCount().has_value() ||
1518 ofOp.getConsumerElemType().has_value() ||
1519 !ofOp.getDimensionsToStream().empty() || isLinked;
1520 // For linked output fifos, producer buffers share memory with
1521 // the link input fifo's consumer buffer on the MemTile.
1522 // Mark to skip producer-side MemTile memory charge.
1523 if (isLinked) {
1524 for (auto link : objectFifoLinks) {
1525 for (auto out : link.getOutputObjectFifos()) {
1526 if (out.name() == ofOp.getSymName())
1527 fb.linkSharedProd = true;
1528 }
1529 }
1530 }
1531 fifoBuffers.push_back(fb);
1532 }
1533
1534 LLVM_DEBUG(llvm::dbgs() << "[SA] seed=" << rngSeed << "\n");
1535 LLVM_DEBUG(llvm::dbgs() << "[SA] Collected " << fifoBuffers.size()
1536 << " ObjectFifo buffer requirements\n");
1537
1538 // Compute DMA depths per endpoint: maxAcquire + 1 from core body
1539 // acquire ops, matching findObjectFifoSize() in the stateful transform.
1540 // DMA depths are used when the connection requires DMA; declared
1541 // depths are used for shared-mem connections. The SA selects the
1542 // appropriate depth in addFifoContribution based on placement.
1543 DenseMap<std::pair<Operation *, Operation *>, int> endpointMaxAcquire;
1544 device.walk([&](CoreOp coreOp) {
1545 auto *tileOp = coreOp.getTile().getDefiningOp();
1546 if (!tileOp || !isa<LogicalTileOp>(tileOp))
1547 return;
1548 coreOp.walk([&](ObjectFifoAcquireOp acqOp) {
1549 auto fifoOp = acqOp.getObjectFifo();
1550 if (!fifoOp)
1551 return;
1552 auto endpoint = std::pair{static_cast<Operation *>(fifoOp), tileOp};
1553 int acqNum = acqOp.acqNumber();
1554 auto it = endpointMaxAcquire.find(endpoint);
1555 if (it == endpointMaxAcquire.end() || acqNum > it->second)
1556 endpointMaxAcquire[endpoint] = acqNum;
1557 });
1558 });
1559
1560 for (auto &fb : fifoBuffers) {
1561 // Producer DMA depth
1562 fb.producerDMADepth = fb.producerDepth;
1563 if (fb.producer) {
1564 auto it = endpointMaxAcquire.find(std::pair{fb.fifoOp, fb.producer});
1565 if (it != endpointMaxAcquire.end()) {
1566 int maxAcq = it->second;
1567 fb.producerDMADepth =
1568 (maxAcq == 1 && fb.producerDepth == 1) ? 1 : maxAcq + 1;
1569 }
1570 }
1571 // Consumer DMA depths
1572 for (size_t ci = 0; ci < fb.consumers.size(); ci++) {
1573 int dmaDepth =
1574 fb.consumerDepths.empty() ? fb.producerDepth : fb.consumerDepths[ci];
1575 if (fb.consumers[ci]) {
1576 auto it =
1577 endpointMaxAcquire.find(std::pair{fb.fifoOp, fb.consumers[ci]});
1578 if (it != endpointMaxAcquire.end()) {
1579 int maxAcq = it->second;
1580 int declared = fb.consumerDepthAt(ci);
1581 dmaDepth = (maxAcq == 1 && declared == 1) ? 1 : maxAcq + 1;
1582 }
1583 }
1584 fb.consumerDMADepths.push_back(dmaDepth);
1585 }
1586 }
1587}
1588
1589LogicalResult SAPlacer::generateInitialPlacement() {
1590 auto &logicalTiles = collected.logicalTiles;
1591
1592 // Classify available tiles by type
1593 std::vector<TileID> availComp = availability.compTiles;
1594 std::vector<TileID> availMem, availShim;
1595 for (auto &t : availability.nonCompTiles) {
1596 AIETileType type = targetModel->getTileType(t.col, t.row);
1597 if (type == AIETileType::MemTile)
1598 availMem.push_back(t);
1599 else if (type == AIETileType::ShimNOCTile)
1600 availShim.push_back(t);
1601 }
1602
1603 // Order compute positions in checkerboard pattern: phase-0 positions
1604 // ((col+row) even) first, then phase-1. No two phase-0 positions are
1605 // 4-connected neighbors, so the heaviest tiles placed first are guaranteed
1606 // non-adjacent. Within each phase, row-major order.
1607 llvm::sort(availComp, [](TileID a, TileID b) {
1608 int phaseA = (a.col + a.row) % 2;
1609 int phaseB = (b.col + b.row) % 2;
1610 if (phaseA != phaseB)
1611 return phaseA < phaseB;
1612 if (a.row != b.row)
1613 return a.row < b.row;
1614 return a.col < b.col;
1615 });
1616 // Order MemTile positions: even columns first (0,2,4,6,1,3,5,7).
1617 // Heavy MemTile tiles placed first get non-adjacent columns, avoiding
1618 // per-buffer spill collisions.
1619 llvm::sort(availMem, [](TileID a, TileID b) {
1620 int phaseA = a.col % 2;
1621 int phaseB = b.col % 2;
1622 if (phaseA != phaseB)
1623 return phaseA < phaseB;
1624 return a.col < b.col;
1625 });
1626 deterministicShuffle(availShim, rng);
1627
1628 size_t compIdx = 0, memIdx = 0, shimIdx = 0;
1629
1630 for (auto lt : logicalTiles) {
1631 Operation *op = lt.getOperation();
1632 auto col = lt.tryGetCol();
1633 auto row = lt.tryGetRow();
1634 AIETileType type = lt.getTileType();
1635
1636 if (col && row) {
1637 // Fully constrained
1638 TileID pos{*col, *row};
1639 currentPlacement[op] = pos;
1640 physToLogical[pos] = op;
1641 constrainedTiles.insert(op);
1642
1643 // Remove from available pool
1644 if (type == AIETileType::CoreTile)
1645 llvm::erase(availComp, pos);
1646 else if (type == AIETileType::MemTile)
1647 llvm::erase(availMem, pos);
1648 else if (type == AIETileType::ShimNOCTile)
1649 llvm::erase(availShim, pos);
1650 }
1651 }
1652
1653 // Place unconstrained core tiles sorted by memory weight (heaviest first)
1654 // into checkerboard-spread positions. Cascade tiles are placed independently;
1655 // the cascade penalty guides them to adjacent positions during SA.
1656 auto byMemWeightDesc = [this](Operation *a, Operation *b) {
1657 int64_t wa = computeTileMemoryWeight(a);
1658 int64_t wb = computeTileMemoryWeight(b);
1659 if (wa != wb)
1660 return wa > wb;
1661 return a->isBeforeInBlock(b);
1662 };
1663 {
1664 SmallVector<Operation *> sortedCoreTiles;
1665 for (auto lt : logicalTiles) {
1666 Operation *op = lt.getOperation();
1667 if (constrainedTiles.contains(op))
1668 continue;
1669 if (lt.getTileType() == AIETileType::CoreTile)
1670 sortedCoreTiles.push_back(op);
1671 }
1672 llvm::sort(sortedCoreTiles, byMemWeightDesc);
1673
1674 for (auto *op : sortedCoreTiles) {
1675 TileID pos;
1676 bool found = false;
1677 for (size_t i = compIdx; i < availComp.size(); i++) {
1678 if (isLegalPosition(op, availComp[i])) {
1679 pos = availComp[i];
1680 std::swap(availComp[i], availComp[compIdx]);
1681 compIdx++;
1682 found = true;
1683 break;
1684 }
1685 }
1686 if (!found)
1687 return cast<LogicalTileOp>(op).emitError(
1688 "no available physical tile for placement");
1689 currentPlacement[op] = pos;
1690 physToLogical[pos] = op;
1691 movableTiles.push_back(op);
1692 }
1693 }
1694
1695 // Place unconstrained MemTile tiles sorted by memory weight (heaviest
1696 // first into spread positions), then shim tiles.
1697 {
1698 SmallVector<Operation *> sortedMemTiles, shimTiles;
1699 for (auto lt : logicalTiles) {
1700 Operation *op = lt.getOperation();
1701 if (constrainedTiles.contains(op))
1702 continue;
1703 AIETileType type = lt.getTileType();
1704 if (type == AIETileType::MemTile)
1705 sortedMemTiles.push_back(op);
1706 else if (type == AIETileType::ShimNOCTile)
1707 shimTiles.push_back(op);
1708 }
1709 llvm::sort(sortedMemTiles, byMemWeightDesc);
1710
1711 for (auto *op : sortedMemTiles) {
1712 if (availMem.empty())
1713 return cast<LogicalTileOp>(op).emitError(
1714 "no available physical tile for placement");
1715 TileID pos = availMem[memIdx % availMem.size()];
1716 memIdx++;
1717 currentPlacement[op] = pos;
1718 movableTiles.push_back(op);
1719 }
1720
1721 for (auto *op : shimTiles) {
1722 if (availShim.empty())
1723 return cast<LogicalTileOp>(op).emitError(
1724 "no available physical tile for placement");
1725 TileID pos = availShim[shimIdx % availShim.size()];
1726 shimIdx++;
1727 if (!isLegalPosition(op, pos))
1728 return cast<LogicalTileOp>(op).emitError(
1729 "no available physical tile for placement");
1730 currentPlacement[op] = pos;
1731 movableTiles.push_back(op);
1732 }
1733 }
1734
1735 return success();
1736}
1737
1738void SAPlacer::initializeSAState() {
1739 initBoundingBoxes();
1740 initResourceTracking();
1741
1742 cascadePen = computePairingPenalty();
1743 int memPressure = computeMemoryPressure();
1744 totalCost =
1745 computeTotalHPWL() + getResourcePenalty() + cascadePen + memPressure;
1746
1747 deviceSlots = availability.compTiles.size() + movableTiles.size();
1748
1749 LLVM_DEBUG(llvm::dbgs() << "[SA] Initial cost: " << totalCost
1750 << " (HPWL=" << computeTotalHPWL()
1751 << ", resPenalty=" << getResourcePenalty()
1752 << "), movable tiles: " << movableTiles.size()
1753 << ", nets: " << nets.size()
1754 << ", fifos: " << fifoBuffers.size() << "\n");
1755}
1756
1757void SAPlacer::runSAMainLoop() {
1758 // Skip SA when there is nothing to optimize.
1759 bool hasCostTerms =
1760 !nets.empty() || !fifoBuffers.empty() || !cascadeAdjacency.edges.empty();
1761 if (movableTiles.empty() || !hasCostTerms)
1762 return;
1763
1764 // SA schedule parameters scaled by design and device.
1765 int numMovable = movableTiles.size();
1766 int numNets = nets.size();
1767
1768 int movesPerIter =
1769 std::clamp(static_cast<int>(numMovable * std::sqrt(std::max(numNets, 1))),
1770 config.minMovesPerIter, config.maxMovesPerIter);
1771
1772 int maxIters = std::max(
1773 config.minMaxIters,
1774 static_cast<int>(1000 * std::sqrt(static_cast<double>(numMovable) *
1775 std::max(numNets, 1))));
1776
1777 int greedyIters = config.greedyMultiplier * numMovable;
1778
1779 // A large effort saturates rather than overflowing the int conversion.
1780 auto scaled = [&](int budget) {
1781 double v = std::min(budget * config.effort,
1782 static_cast<double>(std::numeric_limits<int>::max()));
1783 return std::max(1, static_cast<int>(v));
1784 };
1785 movesPerIter = scaled(movesPerIter);
1786 // Same floor as movesPerIter above: a small positive effort must still
1787 // scale the greedy stage down, but never to zero -- skipping it entirely
1788 // is what leaves violations unresolved (see finalizePlacement's legality
1789 // check).
1790 greedyIters = scaled(greedyIters);
1791
1792 int numSamples = std::max(10 * numMovable, 50);
1793 double estimatedT = estimateInitialTemperature(numSamples);
1794 double initTemp =
1795 std::min(config.tempScaleEstimate * estimatedT,
1796 static_cast<double>(totalCost) * config.tempScaleCost);
1797 initTemp = std::max(initTemp, 1.0);
1798
1799 LLVM_DEBUG(llvm::dbgs() << "[SA] Schedule: movesPerIter=" << movesPerIter
1800 << " maxIters=" << maxIters
1801 << " initTemp=" << initTemp << "\n");
1802
1803 schedule = SASchedule(initTemp, movesPerIter, maxIters, greedyIters, config);
1804
1805 bestPlacement = currentPlacement;
1806 bestCost =
1807 (getResourcePenalty() == 0 && cascadePen == 0) ? totalCost : INT_MAX;
1808 bestOverallPlacement = currentPlacement;
1809 bestOverallCost = totalCost;
1810 zeroDeltaMoves = posDeltaMoves = negDeltaMoves = 0;
1811 acceptedUphill = rejectedMoves = 0;
1812
1813 while (!schedule.limitReached()) {
1814 for (int m = 0; m < schedule.getMovesPerIter(); m++) {
1815
1816 SmallVector<std::pair<Operation *, TileID>> moves;
1817 if (!generateMove(moves))
1818 continue;
1819 tryMove(moves);
1820 }
1821
1822 if (schedule.getIteration() % config.debugInterval == 0)
1823 LLVM_DEBUG(llvm::dbgs()
1824 << "[SA] Iter " << schedule.getIteration()
1825 << " T=" << llvm::format("%.3e", schedule.getTemperature())
1826 << " cost=" << totalCost << " best=" << bestCost << " accept="
1827 << llvm::format("%.3f", schedule.getAcceptanceRatio())
1828 << " d0=" << zeroDeltaMoves << " d+=" << posDeltaMoves
1829 << " d-=" << negDeltaMoves << " uphill=" << acceptedUphill
1830 << " rej=" << rejectedMoves
1831 << (schedule.isGreedy() ? " (greedy)" : "") << "\n");
1832
1833 // Periodic full recompute to reset incremental cost drift.
1834 if (schedule.getIteration() % config.fullRecomputeInterval == 0 &&
1835 schedule.getIteration() > 0) {
1836 initResourceTracking();
1837 cascadePen = computePairingPenalty();
1838 totalCost = computeTotalHPWL() + getResourcePenalty() + cascadePen +
1839 computeMemoryPressure();
1840 }
1841
1842 schedule.cool();
1843 }
1844}
1845
1846//===----------------------------------------------------------------------===//
1847// Move acceptance
1848//===----------------------------------------------------------------------===//
1849
1850void SAPlacer::tryMove(SmallVector<std::pair<Operation *, TileID>> &moves) {
1851 // Snapshot state for revert on rejection.
1852 SmallVector<std::pair<Operation *, TileID>> oldPlacements;
1853 for (auto &[t, _] : moves)
1854 oldPlacements.push_back({t, currentPlacement[t]});
1855 int oldResPenalty = cachedResourcePenalty;
1856 auto savedMemUsage = currentMemUsage;
1857 auto savedDMAUsage = currentDMAUsage;
1858 auto savedSharedMemDest = sharedMemDestination;
1859
1860 // Apply moves: update placement maps and compute HPWL delta.
1861 for (auto &[t, _] : moves)
1862 physToLogical.erase(currentPlacement[t]);
1863 SmallVector<SmallVector<std::pair<size_t, NetBoundingBox>>> allBackups(
1864 moves.size());
1865 int hpwlDelta = 0;
1866 for (size_t i = 0; i < moves.size(); i++) {
1867 hpwlDelta += evaluateMove(moves[i].first, moves[i].second, allBackups[i]);
1868 currentPlacement[moves[i].first] = moves[i].second;
1869 }
1870 for (auto &[t, pos] : moves)
1871 physToLogical[pos] = t;
1872
1873 // Compute total cost delta across all cost terms.
1874 int oldCascade = cascadePen;
1875 int oldPressure = computeMemoryPressure();
1876 int newResPenalty = updateResourcePenalty(oldPlacements);
1877 int newCascade = computePairingPenalty();
1878 int newPressure = computeMemoryPressure();
1879 int delta = hpwlDelta + (newResPenalty - oldResPenalty) +
1880 (newCascade - oldCascade) + (newPressure - oldPressure);
1881
1882 if (delta == 0)
1883 zeroDeltaMoves++;
1884 else if (delta > 0)
1885 posDeltaMoves++;
1886 else
1887 negDeltaMoves++;
1888
1889 // Accept improving moves unconditionally; accept worsening moves
1890 // with probability exp(-delta/T).
1891 bool accept =
1892 delta <= 0 ||
1893 (schedule.getTemperature() > 0 &&
1894 drawUnitReal(rng) < std::exp(-delta / schedule.getTemperature()));
1895
1896 if (accept) {
1897 if (delta > 0)
1898 acceptedUphill++;
1899 totalCost += delta;
1900 cascadePen = newCascade;
1901 schedule.record(true);
1902 if (totalCost < bestOverallCost) {
1903 bestOverallCost = totalCost;
1904 bestOverallPlacement = currentPlacement;
1905 }
1906 // Only update bestPlacement when fully legal (no resource or cascade
1907 // violations).
1908 if (totalCost < bestCost && newResPenalty == 0 && newCascade == 0) {
1909 bestCost = totalCost;
1910 bestPlacement = currentPlacement;
1911 }
1912 } else {
1913 // Revert: undo moves in reverse order and restore saved state.
1914 for (auto &[t, pos] : moves)
1915 physToLogical.erase(pos);
1916 for (int i = moves.size() - 1; i >= 0; i--) {
1917 revertMove(allBackups[i]);
1918 currentPlacement[moves[i].first] = oldPlacements[i].second;
1919 }
1920 for (auto &[op, oldPos] : oldPlacements)
1921 physToLogical[oldPos] = op;
1922 currentMemUsage = savedMemUsage;
1923 currentDMAUsage = savedDMAUsage;
1924 sharedMemDestination = savedSharedMemDest;
1925 cachedResourcePenalty = oldResPenalty;
1926 rejectedMoves++;
1927 schedule.record(false);
1928 }
1929}
1930
1931//===----------------------------------------------------------------------===//
1932// Debug output
1933//===----------------------------------------------------------------------===//
1934
1935void SAPlacer::printPlacementStats(int64_t elapsedMs) const {
1936 int finalHPWL = computeTotalHPWL();
1937 int finalHardPenalty = getResourcePenalty();
1938 int finalPressure = computeMemoryPressure();
1939 int finalCascade = computePairingPenalty();
1940 bool isLegal = (finalHardPenalty == 0 && finalCascade == 0);
1941
1942 llvm::dbgs() << "[SA] seed=" << rngSeed << " Final HPWL=" << finalHPWL
1943 << ", resPenalty=" << finalHardPenalty
1944 << ", pressure=" << finalPressure << ", cascade=" << finalCascade
1945 << ", legal=" << (isLegal ? "yes" : "NO") << "\n";
1946 llvm::dbgs() << "[SA] " << schedule.getIteration() << " iters, "
1947 << movableTiles.size() << " tiles, " << nets.size() << " nets, "
1948 << allocates.size() << " allocates, " << elapsedMs << " ms\n";
1949 llvm::dbgs() << "[SA] Moves: d-=" << negDeltaMoves << " d0=" << zeroDeltaMoves
1950 << " d+=" << posDeltaMoves << " uphill=" << acceptedUphill
1951 << " rej=" << rejectedMoves << "\n";
1952
1953 // Per-tile resource utilization (tiles above 75% capacity).
1954 int64_t coreCap = targetModel->getLocalMemorySize();
1955 int64_t memCap = targetModel->getMemTileSize();
1956 for (auto &[pos, used] : currentMemUsage) {
1957 auto type = targetModel->getTileType(pos.col, pos.row);
1958 int64_t cap = (type == AIETileType::MemTile) ? memCap : coreCap;
1959 if (used > cap * 3 / 4)
1960 llvm::dbgs() << "[SA-RES] mem(" << pos.col << "," << pos.row
1961 << "): " << used << "/" << cap
1962 << (used > cap ? " OVERFLOW" : "") << "\n";
1963 }
1964 for (auto &[pos, usage] : currentDMAUsage) {
1965 auto [maxIn, maxOut] = getDMACapacity(*targetModel, pos);
1966 if (usage.first >= maxIn || usage.second >= maxOut)
1967 llvm::dbgs() << "[SA-RES] dma(" << pos.col << "," << pos.row
1968 << "): in=" << usage.first << "/" << maxIn
1969 << " out=" << usage.second << "/" << maxOut
1970 << (usage.first > maxIn || usage.second > maxOut
1971 ? " OVERFLOW"
1972 : " AT-LIMIT")
1973 << "\n";
1974 }
1975
1976 // Detailed breakdown for overflowing core tiles: neighbor spare capacity.
1977 for (auto &[pos, used] : currentMemUsage) {
1978 auto type = targetModel->getTileType(pos.col, pos.row);
1979 if (type != AIETileType::CoreTile || used <= coreCap)
1980 continue;
1981 int64_t overflow = used - coreCap;
1982 for (TileID n : getMemAffinityNeighbors(*targetModel, pos)) {
1983 auto it = currentMemUsage.find(n);
1984 int64_t nbrUsed = it != currentMemUsage.end() ? it->second : 0;
1985 llvm::dbgs() << " nbr(" << n.col << "," << n.row << "): used=" << nbrUsed
1986 << " spare=" << std::max(coreCap - nbrUsed, int64_t{0})
1987 << "\n";
1988 }
1989 llvm::dbgs() << "[SA-PENALTY] tile(" << pos.col << "," << pos.row
1990 << "): used=" << used << " cap=" << coreCap
1991 << " overflow=" << overflow << "\n";
1992 }
1993}
1994
1995//===----------------------------------------------------------------------===//
1996// Post-SA finalization
1997//===----------------------------------------------------------------------===//
1998
1999LogicalResult SAPlacer::finalizePlacement(DeviceOp device) {
2000 // Restore best placement found during SA. Skip if SA loop didn't run
2001 // (bestOverallCost stays at INT_MAX when runSAMainLoop returns early).
2002 finalCost = totalCost;
2003 if (bestOverallCost < INT_MAX) {
2004 if (bestCost < INT_MAX) {
2005 currentPlacement = bestPlacement;
2006 finalCost = bestCost;
2007 } else {
2008 currentPlacement = bestOverallPlacement;
2009 finalCost = bestOverallCost;
2010 }
2011 physToLogical.clear();
2012 for (auto &[op, pos] : currentPlacement)
2013 physToLogical[pos] = op;
2014 initResourceTracking();
2015 }
2016
2017 // Generate allocates (state is fresh from initResourceTracking)
2018 generateAllocates();
2019
2020 // Merge mem/shim tiles that SA placed at the same position
2021 if (failed(mergeMemShimTiles(device)))
2022 return failure();
2023
2024 auto endTime = std::chrono::steady_clock::now();
2025 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
2026 endTime - startTime);
2027
2028 LLVM_DEBUG(printPlacementStats(elapsed.count()));
2029
2030 // SA optimizes for legality but is not guaranteed to reach it (e.g. a very
2031 // low --sa-effort can starve the greedy stage that normally drives hard
2032 // violations to zero). Cascade and shared memory pairs and delegate reach
2033 // are exact, and a placement breaking them would only fail later and less
2034 // clearly, so they fail here. Memory, DMA channel and BD use are this pass's
2035 // estimates of what later passes allocate, and those passes check them
2036 // exactly, so an overrun here is a warning.
2037 int finalPairing = computePairingPenalty();
2038 int finalDelegate = computeDelegatePenalty();
2039 if (finalPairing != 0 || finalDelegate != 0) {
2040 device.emitError("SA placer failed to find a legal placement (cascade and "
2041 "shared memory penalty=")
2042 << finalPairing << ", delegate penalty=" << finalDelegate
2043 << "); try increasing --sa-effort";
2044 return failure();
2045 }
2046 if (int estimated = getResourcePenalty() - finalDelegate; estimated != 0)
2047 device.emitWarning("SA placer's resource estimate is over capacity "
2048 "(penalty=")
2049 << estimated
2050 << "); later passes check exactly, and increasing --sa-effort may help";
2051
2052 result = currentPlacement;
2053 return success();
2054}
virtual int rows() const =0
Return the number of rows in the device.
virtual int columns() const =0
Return the number of columns in the device.
@ First
Only the first tile's module.
virtual bool isLegalMemAffinity(int coreCol, int coreRow, int memCol, int memRow) const =0
Return true if core can access the memory in mem.
static Adjacency buildCascadeAdjacency(llvm::ArrayRef< CascadeFlowOp > cascadeFlows)
static bool satisfiesConstraints(mlir::Operation *tile, TileID pos)
const AIETargetModel * targetModel
Definition AIEPlacer.h:184
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)
mlir::LogicalResult place(DeviceOp device) override
void record(bool accepted)
double getAcceptanceRatio() const
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
uint32_t requiredBufferAlignBits(uint32_t busAlignBits, uint32_t maxVecAlignBits, int64_t sizeBytes)
Return the alignment (in bits) a buffer of sizeBytes must satisfy.
TileMemoryLimits tileMemoryLimits(const AIETargetModel &targetModel, AIETileType tileType)
llvm::SmallVector< mlir::Operation * > endpoints
Definition AIEPlacer.h:717
NetBoundingBox bb
Definition AIEPlacer.h:718
llvm::SmallVector< RouteOp > routes
Definition AIEPlacer.h:132
llvm::SmallVector< ObjectFifoLinkOp > objectFifoLinks
Definition AIEPlacer.h:127
llvm::SmallVector< ObjectFifoCreateOp > objectFifos
Definition AIEPlacer.h:126
llvm::SmallVector< LogicalTileOp > logicalTiles
Definition AIEPlacer.h:125
A memtile is reached by DMA rather than by core vector load and store, so its bus width also covers t...