MLIR-AIE 1.4.6.dev
AIEObjectFifoAllocate.cpp
Go to the documentation of this file.
1//===- AIEObjectFifoAllocate.cpp --------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
18
19#include "mlir/IR/Attributes.h"
20#include "mlir/Interfaces/DataLayoutInterfaces.h"
21#include "mlir/Pass/Pass.h"
22#include "llvm/ADT/EquivalenceClasses.h"
23#include "llvm/ADT/MapVector.h"
24#include "llvm/ADT/StringMap.h"
25#include "llvm/Support/Debug.h"
26
27#include <memory>
28#include <set>
29
30#define DEBUG_TYPE "aie-objectfifo-allocate"
31
32using namespace mlir;
33using namespace xilinx;
34using namespace xilinx::AIE;
35
36namespace xilinx::AIE {
37#define GEN_PASS_DEF_AIEOBJECTFIFOALLOCATE
38#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
39} // namespace xilinx::AIE
40
41namespace {
42
43struct AIEObjectFifoAllocatePass
44 : public xilinx::AIE::impl::AIEObjectFifoAllocateBase<
45 AIEObjectFifoAllocatePass> {
46 using Base::Base;
47
48 DeviceOp device;
49 OpBuilder builder{static_cast<MLIRContext *>(nullptr)};
50 /// The last buffer or lock this pass placed on each tile.
51 DenseMap<Value, Operation *> lastPlaced;
52 /// Which pools get locks, and the resets re-arming each fifo.
53 std::shared_ptr<PoolLocks> poolLocks;
54 /// Flows already lowered. A route endpoint reads its direction off the
55 /// flow naming it, so these outlive the walk that replaces them.
56 SmallVector<Operation *> loweredFlows;
57 /// The flows each route endpoint is the source of, by its symbol name,
58 /// gathered before any flow is lowered.
59 DenseMap<StringAttr, SmallVector<RouteOp>> sourcedFlows;
60 llvm::MapVector<ObjectFifoDmaEndpointOp, llvm::StringMap<PacketInfoAttr>>
61 dispatchHeaders;
62 /// Ends that take another end's channel, by the end they take it from:
63 /// shim ends never in flight together, and ends taking turns.
64 llvm::MapVector<Operation *, Operation *> channelSharers;
65 DenseMap<Operation *, ShimDMAAllocationOp> allocationSharers;
66 DenseMap<Value, SmallVector<int64_t>> plannedMemory;
67 DenseMap<Operation *, SmallVector<Value>> bufferPlacements;
68 DenseMap<Operation *, int> channelAssignments;
69 DenseMap<Operation *, Value> localPools;
70 DenseMap<Operation *, SmallVector<Value>> poolUsers;
71 DenseMap<Operation *, SmallVector<Operation *>> lockUsers;
72 DenseMap<Operation *, Value> lockPlacements;
73 /// What runs on each channel the design programs itself, by the
74 /// `aie.route_endpoint` naming it: dma_starts in its tile's DMA program and
75 /// tasks the runtime sequence configures for it.
76 DenseMap<Operation *, SmallVector<DMAStartOp>> endpointStarts;
77 DenseMap<Operation *, SmallVector<AIEX::DMAConfigureTaskForOp>> endpointTasks;
78 /// BDs the chains DMA programs start by index hold on each channel of a
79 /// placed tile, by (col, row, channel). Channels are chosen from these and
80 /// the endpoints' own chains alone, so an image built for one runtime
81 /// sequence serves another's instruction stream.
82 DenseMap<std::tuple<int, int, int>, int64_t> fixedBDs;
83 /// The largest task the runtime sequence configures on each channel by
84 /// index, by (col, row, channel).
85 DenseMap<std::tuple<int, int, int>, int64_t> taskBDs;
86 /// The BDs every channel the last assignment handed out needs at once,
87 /// runtime tasks included.
88 DenseMap<std::tuple<int, int, int>, int64_t> channelDemand;
89 /// Where each tile, pool, endpoint and route falls in an order taken from
90 /// the design's structure alone, which settles the choices op order would.
91 DenseMap<Operation *, unsigned> structuralRank;
92 RouteEndpoint channelFailure;
93 /// The pool the buffer search could not place with the fewest left
94 /// unplaced, i.e. where it got furthest.
95 ObjectFifoPoolOp bufferFailure;
96 size_t unplacedAtFailure = 0;
97 /// Neighbors spillTargets created to weigh a spill. The ones no buffer or
98 /// lock ends up on are erased once allocation is done.
99 SmallVector<TileOp> spillTiles;
100 Value lockFailure;
101 Operation *lockAccessFailure = nullptr;
102
103 bool sameTile(Value a, Value b) {
104 if (a == b)
105 return true;
106 auto first = cast<TileLike>(a.getDefiningOp());
107 auto second = cast<TileLike>(b.getDefiningOp());
108 auto ac = first.tryGetCol(), ar = first.tryGetRow();
109 auto bc = second.tryGetCol(), br = second.tryGetRow();
110 return ac && ar && bc && br && ac == bc && ar == br;
111 }
112
113 int64_t memoryUsed(Value tile, int64_t extraSize = 0, int extraCount = 0) {
114 SmallVector<BufferAllocation> layout;
115 int64_t alignment =
116 device.getTargetModel().getMemTileLoadStoreBusWidth() / 8;
117 // Generated buffers are inserted immediately after their tile, before
118 // existing buffers. Preserve that order for equal-sized aligned/unaligned
119 // buffers, including coordinate-equivalent tile references.
120 for (Operation &op : *device.getBody()) {
121 if (auto placed = dyn_cast<TileLike>(op);
122 placed && sameTile(tile, placed->getResult(0))) {
123 Value value = placed->getResult(0);
124 for (int64_t size : plannedMemory[value])
125 layout.push_back({size, alignment, std::nullopt});
126 if (value == tile)
127 for (int i = 0; i < extraCount; ++i)
128 layout.push_back({extraSize, alignment, std::nullopt});
129 } else if (auto buffer = dyn_cast<BufferOp>(op);
130 buffer && sameTile(tile, buffer.getTile())) {
131 layout.push_back({buffer.getAllocationSize(),
132 buffer.getAligned() ? alignment : 1,
133 buffer.getAddress()});
134 }
135 }
136 return assignSequentialBufferAddresses(layout);
137 }
138
139 LogicalResult collectFixedMemory() {
140 plannedMemory.clear();
141 for (auto tile : device.getOps<TileLike>()) {
142 if (!tile.isMemTile())
143 continue;
144 int64_t bytes = memoryUsed(tile->getResult(0));
145 int64_t capacity = device.getTargetModel().getMemTileSize();
146 if (bytes > capacity)
147 return tile->emitOpError("existing buffers require ")
148 << bytes << " bytes, exceeding MemTile capacity of " << capacity
149 << " bytes";
150 }
151 return success();
152 }
153
154 bool canAccess(Value user, Value memory, bool localOnly = false) {
155 if (user == memory)
156 return true;
157 auto userTile = dyn_cast<TileLike>(user.getDefiningOp());
158 auto memoryTile = dyn_cast<TileLike>(memory.getDefiningOp());
159 if (!userTile || !memoryTile)
160 return false;
161 const auto &target = device.getTargetModel();
162 auto positions = [&](TileLike tile) {
163 SmallVector<TileID> compatible;
164 auto col = tile.tryGetCol(), row = tile.tryGetRow();
165 for (int c = 0; c < target.columns(); ++c) {
166 if (col && c != *col)
167 continue;
168 for (int r = 0; r < target.rows(); ++r) {
169 if ((!row || r == *row) &&
170 target.getTileType(c, r) == tile.getTileType())
171 compatible.push_back({c, r});
172 }
173 }
174 return compatible;
175 };
176 // Before placement, defer only accesses that some compatible physical
177 // positions could satisfy. Known coordinates can already disprove affinity.
178 auto userPositions = positions(userTile);
179 auto memoryPositions = positions(memoryTile);
180 for (TileID u : userPositions)
181 for (TileID m : memoryPositions) {
182 if (localOnly && u != m)
183 continue;
184 auto shared = target.getSharedMemory(u, m);
185 if (shared == AIETargetModel::SharedMemory::Second ||
186 shared == AIETargetModel::SharedMemory::Either)
187 return true;
188 }
189 return false;
190 }
191
192 bool canPlace(ObjectFifoPoolOp pool, Value tile, int64_t sizeBytes,
193 int count = 1) {
194 if (cast<TileLike>(tile.getDefiningOp()).isMemTile() &&
195 memoryUsed(tile, sizeBytes, count) >
196 device.getTargetModel().getMemTileSize())
197 return false;
198 return llvm::all_of(poolUsers[pool],
199 [&](Value user) { return canAccess(user, tile); });
200 }
201
202 Value lockPlacement(ObjectFifoPoolOp pool, Operation *group) {
203 if (Value placed = lockPlacements.lookup(group))
204 return placed;
205 Value local = localPools.lookup(pool);
206 return local ? local : pool.getTile();
207 }
208
209 /// A contiguous pool's objects are one buffer; others have one per object.
210 static int bufferCount(ObjectFifoPoolOp pool) {
211 return pool.getContiguous() ? 1 : pool.getDepth();
212 }
213 static int64_t bufferBytes(ObjectFifoPoolOp pool) {
214 return pool.getObjectSizeInBytes() * pool.getObjectsPerBuffer();
215 }
216
217 /// Reserve mandatory-local objects before any other pool can spill into
218 /// their memory. Existing buffers are fixed and counted by identity, not by
219 /// how many pools or endpoints reference them.
220 LogicalResult planBuffers(ArrayRef<ObjectFifoPoolOp> pools) {
221 plannedMemory.clear();
222 bufferPlacements.clear();
223 bufferFailure = nullptr;
224 for (auto pool : pools) {
225 if (!localPools.contains(pool))
226 continue;
227 Value tile = localPools.lookup(pool);
228 int count = pool.getBuffers() ? 0 : bufferCount(pool);
229 int64_t size = bufferBytes(pool);
230 if (!canAccess(pool.getTile(), tile) ||
231 !canPlace(pool, tile, size, count)) {
232 bufferFailure = pool;
233 return failure();
234 }
235 plannedMemory[tile].append(count, size);
236 }
237 SmallVector<ObjectFifoPoolOp> generated;
238 for (auto pool : pools) {
239 if (pool.getBuffers()) {
240 for (auto buffer : pool.getBufferOps()) {
241 Value tile = buffer.getBufferTile();
242 if (tile && ((localPools.contains(pool) &&
243 !sameTile(tile, localPools.lookup(pool))) ||
244 !llvm::all_of(poolUsers[pool], [&](Value user) {
245 return canAccess(user, tile);
246 }))) {
247 bufferFailure = pool;
248 return failure();
249 }
250 bufferPlacements[pool].push_back(tile);
251 }
252 continue;
253 }
254 if (pool.getTileLike().isShimTile())
255 continue;
256 if (localPools.contains(pool))
257 bufferPlacements[pool].append(bufferCount(pool),
258 localPools.lookup(pool));
259 else
260 generated.append(bufferCount(pool), pool);
261 }
262 int retries = kSpillRetries;
263 return success(placeBuffers(generated, retries));
264 }
265
266 /// The first layout tried is the greedy one. When a buffer fits nowhere,
267 /// an earlier buffer may have spilled to the one neighbor it could use, or
268 /// stayed home where a later buffer that only home can hold needed the
269 /// room, so earlier choices are revisited, latest first, a bounded number of
270 /// times.
271 static constexpr int kSpillRetries = 256;
272
273 bool placeBuffers(ArrayRef<ObjectFifoPoolOp> buffers, int &retries) {
274 if (buffers.empty())
275 return true;
276 ObjectFifoPoolOp pool = buffers.front();
277 int64_t size = bufferBytes(pool);
278 Value homeTile = pool.getTileLike()->getResult(0);
279 bool fitsHome = canPlace(pool, homeTile, size);
280 SmallVector<Value> candidates;
281 if (fitsHome)
282 candidates.push_back(homeTile);
283 else
284 candidates = spillTargets(pool, size);
285 if (candidates.empty() &&
286 (!bufferFailure || buffers.size() < unplacedAtFailure)) {
287 bufferFailure = pool;
288 unplacedAtFailure = buffers.size();
289 }
290 auto tryTile = [&](Value tile) {
291 plannedMemory[tile].push_back(size);
292 bufferPlacements[pool].push_back(tile);
293 if (placeBuffers(buffers.drop_front(), retries))
294 return true;
295 plannedMemory[tile].pop_back();
296 bufferPlacements[pool].pop_back();
297 return false;
298 };
299 for (auto [index, tile] : llvm::enumerate(candidates)) {
300 if (index > 0 && retries-- <= 0)
301 return false;
302 if (tryTile(tile))
303 return true;
304 }
305 if (!fitsHome)
306 return false;
307 // Spill targets are only looked up once home has failed, so a layout that
308 // keeps every buffer home creates no neighbor tiles.
309 for (Value tile : spillTargets(pool, size)) {
310 if (retries-- <= 0)
311 return false;
312 if (tryTile(tile))
313 return true;
314 }
315 return false;
316 }
317
318 /// FIXME: choosing which tile a buffer lives on is the buffer allocator's
319 /// job, not this pass's. Tracking used memory here to make that choice
320 /// breaks the separation of concerns; --aie-assign-buffer-addresses should
321 /// instead be free to move buffers between tiles that share a memory module.
322 ///
323 /// A MemTile buffer that does not fit at home spills to a neighbor reachable
324 /// by its DMAs, preferring the emptier one so adjacent MemTiles
325 /// keep room for their own spills. Which tiles neighbor an unplaced one is
326 /// not yet known, so its buffers stay at home.
327 SmallVector<Value> spillTargets(ObjectFifoPoolOp pool, int64_t sizeBytes) {
328 TileLike home = pool.getTileLike();
329 auto &target = device.getTargetModel();
330 if (!home.isMemTile())
331 return {};
332
333 auto homeOp = dyn_cast<TileOp>(home.getOperation());
334 if (!homeOp) {
335 return {};
336 }
337
338 SmallVector<TileOp> neighbors;
339 for (int col : {homeOp.getCol() - 1, homeOp.getCol() + 1}) {
340 if (col < 0 || col >= target.columns()) {
341 continue;
342 }
343 int row = homeOp.getRow();
344 bool declared = llvm::any_of(device.getOps<TileOp>(), [&](TileOp t) {
345 return t.getCol() == col && t.getRow() == row;
346 });
347 TileOp neighbor =
348 TileOp::getOrCreate(builder, device, col, row, pool.getLoc());
349 if (!declared)
350 spillTiles.push_back(neighbor);
351 using SharedMemory = AIETargetModel::SharedMemory;
352 SharedMemory shared = sharedMemory(homeOp, neighbor);
353 if (shared == SharedMemory::Second || shared == SharedMemory::Either) {
354 neighbors.push_back(neighbor);
355 }
356 }
357 llvm::stable_sort(neighbors, [&](TileOp a, TileOp b) {
358 return memoryUsed(a.getResult()) < memoryUsed(b.getResult());
359 });
360 SmallVector<Value> fitting;
361 for (TileOp neighbor : neighbors) {
362 if (canPlace(pool, neighbor.getResult(), sizeBytes)) {
363 fitting.push_back(neighbor.getResult());
364 }
365 }
366 return fitting;
367 }
368
369 /// Buffers and locks sit directly below the tile whose memory holds them,
370 /// after whatever this pass has already put there.
371 void setInsertionPointOn(Value tile) {
372 Operation *after = lastPlaced.lookup(tile);
373 if (!after) {
374 after = tile.getDefiningOp();
375 }
376 if (after) {
377 builder.setInsertionPointAfter(after);
378 } else {
379 builder.setInsertionPointToStart(device.getBody());
380 }
381 }
382
383 void allocateBuffers(ObjectFifoPoolOp pool) {
384 if (pool.getBuffers()) {
385 return;
386 }
387 TileLike home = pool.getTileLike();
388 if (home.isShimTile()) {
389 return;
390 }
391
392 ArrayRef<Attribute> initValues;
393 if (auto initAttr = pool.getInitValues()) {
394 initValues = initAttr->getValue();
395 }
396 int filled = initValues.size();
397 StringRef base = pool.getBaseName();
398
399 SmallVector<Attribute> names;
400 MemRefType type = pool.getElemType();
401 ElementsAttr zeros;
402 if (pool.getContiguous()) {
403 type =
404 MemRefType::get({pool.getObjectsPerBuffer() * type.getNumElements()},
405 type.getElementType());
406 if (pool.getZeroObjects())
407 zeros = DenseElementsAttr::get(
408 RankedTensorType::get(type.getShape(), type.getElementType()),
409 builder.getZeroAttr(type.getElementType()));
410 }
411 // Initial contents may fill only the first objects of a deeper pool.
412 for (int i = 0; i < bufferCount(pool); i++) {
413 ElementsAttr init =
414 i < filled ? cast<ElementsAttr>(initValues[i]) : zeros;
415 std::string name = (base + "__buf" + std::to_string(i)).str();
416 Value placement = bufferPlacements[pool][i];
417 setInsertionPointOn(placement);
418 lastPlaced[placement] = BufferOp::create(
419 builder, pool.getLoc(), type, placement, builder.getStringAttr(name),
420 /*address=*/nullptr, init,
421 /*mem_bank=*/nullptr, /*core_data=*/nullptr);
422 names.push_back(FlatSymbolRefAttr::get(builder.getContext(), name));
423 }
424 pool.setBuffersAttr(builder.getArrayAttr(names));
425 }
426
427 LockOp createLock(ObjectFifoPoolOp pool, Value tile, StringRef name,
428 int value) {
429 setInsertionPointOn(tile);
430 auto lock = LockOp::create(builder, pool.getLoc(), tile, value);
431 lastPlaced[tile] = lock;
432 lock->setAttr(SymbolTable::getSymbolAttrName(),
433 builder.getStringAttr(name));
434 return lock;
435 }
436
437 LogicalResult planLocks(ArrayRef<ObjectFifoPoolOp> pools) {
438 lockFailure = {};
439 lockAccessFailure = nullptr;
440 lockPlacements.clear();
441 DenseMap<std::pair<int, int>, int64_t> used;
442 auto reserve = [&](Value tile, int64_t count) {
443 auto like = cast<TileLike>(tile.getDefiningOp());
444 auto col = like.tryGetCol(), row = like.tryGetRow();
445 if (!col || !row)
446 return success();
447 if (used[{*col, *row}] + count >
448 device.getTargetModel().getNumLocks(*col, *row)) {
449 lockFailure = tile;
450 return failure();
451 }
452 used[{*col, *row}] += count;
453 return success();
454 };
455 for (auto lock : device.getOps<LockOp>())
456 if (failed(reserve(lock.getTile(), 1)))
457 return failure();
458 // Neighbors created to weigh a spill have no rank and come last.
459 SmallVector<TileLike> tiles(device.getOps<TileLike>());
460 llvm::stable_sort(tiles, [&](TileLike a, TileLike b) {
461 auto aRank = structuralRank.find(a), bRank = structuralRank.find(b);
462 if ((aRank == structuralRank.end()) != (bRank == structuralRank.end()))
463 return bRank == structuralRank.end();
464 return aRank != structuralRank.end() && aRank->second < bRank->second;
465 });
466 auto place = [&](ObjectFifoPoolOp pool, Operation *group, int count) {
467 SmallVector<Value> candidates{lockPlacement(pool, group)};
468 if (!localPools.contains(pool)) {
469 llvm::append_range(candidates, bufferPlacements[pool]);
470 for (Operation *user : lockUsers[group])
471 candidates.push_back(user->getOperand(0));
472 for (auto tile : tiles)
473 candidates.push_back(tile->getResult(0));
474 }
475 bool reachable = false;
476 for (Value tile : candidates) {
477 if (!tile || !llvm::all_of(lockUsers[group], [&](Operation *user) {
478 return canAccessLocks(user, tile);
479 }))
480 continue;
481 reachable = true;
482 if (succeeded(reserve(tile, count))) {
483 lockPlacements[group] = tile;
484 lockFailure = {};
485 return success();
486 }
487 }
488 if (!reachable && !lockUsers[group].empty()) {
489 lockFailure = {};
490 lockAccessFailure = lockUsers[group].front();
491 }
492 return failure();
493 };
494 for (auto pool : pools) {
495 if (!poolLocks->needsLocks(pool))
496 continue;
497 if (device.getTargetModel().getTargetArch() == AIEArch::AIE1) {
498 if (!pool.getLocks() && failed(place(pool, pool, pool.getDepth())))
499 return failure();
500 } else {
501 for (auto segment : pool.getSegmentOps())
502 if (!(segment.getProduceLock() && segment.getConsumeLock()) &&
503 failed(place(pool, segment, 2)))
504 return failure();
505 }
506 }
507 auto check = [&](auto endpoint) {
508 for (Value tile : endpointLockTiles(endpoint)) {
509 if (!canAccessLocks(endpoint, tile)) {
510 lockAccessFailure = endpoint;
511 return failure();
512 }
513 }
514 return success();
515 };
516 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>())
517 if (failed(check(endpoint)))
518 return failure();
519 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>())
520 if (failed(check(endpoint)))
521 return failure();
522 return success();
523 }
524
525 /// AIE1 guards each buffer with one binary lock that rotates with it; AIE2
526 /// gives each segment a counting pair, the producer's lock counting free
527 /// objects and the consumer's counting full ones.
528 void allocateLocks(ObjectFifoPoolOp pool) {
529 if (!poolLocks->needsLocks(pool))
530 return;
531 StringRef base = pool.getBaseName();
532 int depth = pool.getDepth();
533 auto initValues = pool.getInitValues();
534 int filled = initValues ? initValues->size() : 0;
535 int repeat = pool.getRepeatCount().value_or(1);
536
537 if (device.getTargetModel().getTargetArch() == AIEArch::AIE1) {
538 if (pool.getLocks()) {
539 return;
540 }
541 SmallVector<Attribute> names;
542 for (int i = 0; i < depth; i++) {
543 std::string name = (base + "__lock" + std::to_string(i)).str();
544 createLock(pool, lockPlacement(pool, pool), name, i < filled ? 1 : 0);
545 names.push_back(FlatSymbolRefAttr::get(builder.getContext(), name));
546 }
547 pool.setLocksAttr(builder.getArrayAttr(names));
548 return;
549 }
550
551 for (auto [index, segment] : llvm::enumerate(pool.getSegmentOps())) {
552 if (segment.getProduceLock() && segment.getConsumeLock()) {
553 continue;
554 }
555 std::string produce = (base + "__prodlock" + std::to_string(index)).str();
556 std::string consume = (base + "__conslock" + std::to_string(index)).str();
557 Value tile = lockPlacement(pool, segment);
558 createLock(pool, tile, produce, (depth - filled) * repeat);
559 createLock(pool, tile, consume, filled * repeat);
560 segment.setProduceLockAttr(
561 FlatSymbolRefAttr::get(builder.getContext(), produce));
562 segment.setConsumeLockAttr(
563 FlatSymbolRefAttr::get(builder.getContext(), consume));
564 }
565 }
566
567 bool canAccessLocks(Operation *endpoint, Value tile) {
568 Value user = endpoint->getOperand(0);
569 // Compute-tile DMA engines, unlike cores, only use their own lock module.
570 if (isa<ObjectFifoDmaEndpointOp>(endpoint) &&
571 !cast<TileLike>(user.getDefiningOp()).isMemTile())
572 return canAccess(user, tile, /*localOnly=*/true);
573 return canAccess(user, tile);
574 }
575
576 template <typename EndpointOp>
577 void collectLockUsers(EndpointOp endpoint) {
578 auto pool = endpoint.getPoolOp();
579 if (device.getTargetModel().getTargetArch() == AIEArch::AIE1) {
580 lockUsers[pool].push_back(endpoint);
581 return;
582 }
583 for (auto segment : endpoint.getSelectedSegments())
584 lockUsers[segment].push_back(endpoint);
585 }
586
587 /// Predict the locks core and DMA lowering will use, including hand-written
588 /// locks and those allocation will create. Only selected segments matter.
589 template <typename EndpointOp>
590 SmallVector<Value> endpointLockTiles(EndpointOp endpoint) {
591 SmallVector<Value> tiles;
592 auto pool = endpoint.getPoolOp();
593 if (pool.getDepth() == 0)
594 return tiles;
595 if (device.getTargetModel().getTargetArch() == AIEArch::AIE1) {
596 if (poolLocks->needsLocks(pool) && !pool.getLocks())
597 tiles.push_back(lockPlacement(pool, pool));
598 else
599 for (auto lock : pool.getLockOps())
600 tiles.push_back(lock.getTile());
601 return tiles;
602 }
603 for (auto segment : endpoint.getSelectedSegments()) {
604 if (poolLocks->needsLocks(pool) &&
605 !(segment.getProduceLock() && segment.getConsumeLock())) {
606 tiles.push_back(lockPlacement(pool, segment));
607 continue;
608 }
609 for (auto name :
610 {segment.getProduceLockAttr(), segment.getConsumeLockAttr()})
611 if (name)
612 if (auto lock = lookupNamedOp<LockOp>(device, name.getAttr()))
613 tiles.push_back(lock.getTile());
614 }
615 return tiles;
616 }
617
618 /// Both buffers and locks must be local to use the local-only channels.
619 /// Distinct unresolved tiles may be neighbors after placement, so they
620 /// cannot be assumed local when assigning a placed endpoint's channels.
621 bool reachesAdjacentTile(RouteEndpoint endpoint) {
622 if (auto route = dyn_cast<RouteEndpointOp>(endpoint.getOperation()))
623 return programReachesAdjacentTile(route);
624 auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation());
625 if (!dma || !dma.getTileLike().isMemTile()) {
626 return false;
627 }
628 ObjectFifoPoolOp pool = dma.getPoolOp();
629 auto remote = [&](Value tile) {
630 return tile && !sameTile(tile, endpoint.getTile());
631 };
632 return pool && (llvm::any_of(bufferPlacements[pool], remote) ||
633 llvm::any_of(endpointLockTiles(dma), remote));
634 }
635
636 TileLike tileOf(RouteEndpoint endpoint) {
637 return dyn_cast<TileLike>(endpoint.getTile().getDefiningOp());
638 }
639
640 /// A DMA channel the design programs itself, by naming this endpoint from a
641 /// dma_start or a runtime task, rather than one an objectFIFO lowers to.
642 static bool isProgramOwned(RouteEndpoint endpoint) {
643 auto route = dyn_cast<RouteEndpointOp>(endpoint.getOperation());
644 return route && route.getBundle() == WireBundle::DMA &&
645 !route.getTileLike().isShimTile();
646 }
647
648 /// Blocks of the BD chain `start` begins, following next_bd.
649 static SmallVector<Block *> chainOf(DMAStartOp start) {
650 SmallVector<Block *> chain;
651 SmallPtrSet<Block *, 8> seen;
652 SmallVector<Block *> work{start.getDest()};
653 while (!work.empty()) {
654 Block *block = work.pop_back_val();
655 if (!seen.insert(block).second || block->getOps<DMABDOp>().empty())
656 continue;
657 chain.push_back(block);
658 if (auto next = dyn_cast<NextBDOp>(block->getTerminator()))
659 work.push_back(next.getDest());
660 }
661 return chain;
662 }
663
664 static int64_t countBDs(DMAStartOp start) {
665 int64_t bds = 0;
666 for (Block *block : chainOf(start))
667 bds += llvm::range_size(block->getOps<DMABDOp>());
668 return bds;
669 }
670
671 static int64_t countBDs(Region &taskBody) {
672 int64_t bds = 0;
673 taskBody.walk([&](DMABDOp) { bds++; });
674 return bds;
675 }
676
677 /// BDs a program-owned endpoint's static chains hold; they stay configured.
678 int64_t endpointBDs(Operation *endpoint) {
679 int64_t bds = 0;
680 for (DMAStartOp start : endpointStarts.lookup(endpoint))
681 bds += countBDs(start);
682 return bds;
683 }
684
685 /// BDs the largest runtime task on a program-owned endpoint needs.
686 int64_t endpointTaskBDs(Operation *endpoint) {
687 int64_t largest = 0;
688 for (auto task : endpointTasks.lookup(endpoint))
689 largest = std::max(largest, countBDs(task.getBody()));
690 return largest;
691 }
692
693 /// Whether a program-owned MemTile channel's BDs touch a neighbor's buffers
694 /// or locks, which only the lower channels can.
695 bool programReachesAdjacentTile(RouteEndpointOp endpoint) {
696 if (!endpoint.getTileLike().isMemTile())
697 return false;
698 auto remote = [&](Operation *op) {
699 Value tile;
700 if (auto bd = dyn_cast<DMABDOp>(op)) {
701 if (auto buffer =
702 dyn_cast_or_null<BufferOp>(bd.getBuffer().getDefiningOp()))
703 tile = buffer.getTile();
704 } else if (auto use = dyn_cast<UseLockOp>(op)) {
705 if (auto lock = dyn_cast_or_null<LockOp>(use.getLock().getDefiningOp()))
706 tile = lock.getTile();
707 }
708 return tile && !sameTile(tile, endpoint.getTile());
709 };
710 for (DMAStartOp start : endpointStarts.lookup(endpoint))
711 for (Block *block : chainOf(start))
712 if (llvm::any_of(block->getOperations(),
713 [&](Operation &op) { return remote(&op); }))
714 return true;
715 for (auto task : endpointTasks.lookup(endpoint)) {
716 bool reaches = false;
717 task.getBody().walk([&](Operation *op) { reaches |= remote(op); });
718 if (reaches)
719 return true;
720 }
721 return false;
722 }
723
724 /// Find what programs each endpoint-named channel, and the BDs already spoken
725 /// for on channels named by index.
726 void collectChannelPrograms() {
727 endpointStarts.clear();
728 endpointTasks.clear();
729 fixedBDs.clear();
730 taskBDs.clear();
731 auto key = [](Value tile,
732 int channel) -> std::optional<std::tuple<int, int, int>> {
733 auto placed = cast<TileLike>(tile.getDefiningOp());
734 auto col = placed.tryGetCol(), row = placed.tryGetRow();
735 if (!col || !row)
736 return std::nullopt;
737 return std::make_tuple(*col, *row, channel);
738 };
739 for (auto program : device.getOps<DmaBody>())
740 for (Block &block : program.getDmaBody())
741 for (auto start : block.getOps<DMAStartOp>()) {
742 if (auto endpoint = start.getEndpointOp()) {
743 endpointStarts[endpoint].push_back(start);
744 } else if (auto at =
745 key(program.getTile(), start.getChannelIndex())) {
746 fixedBDs[*at] += countBDs(start);
747 }
748 }
749 device.walk([&](Operation *op) {
750 if (auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(op)) {
751 if (auto endpoint = dyn_cast_or_null<RouteEndpointOp>(
752 SymbolTable::lookupNearestSymbolFrom(
753 device, task.getAlloc().getRootReference())))
754 endpointTasks[endpoint].push_back(task);
755 } else if (auto task = dyn_cast<AIEX::DMAConfigureTaskOp>(op)) {
756 if (auto at = key(task.getTile(), task.getChannel())) {
757 int64_t &largest = taskBDs[*at];
758 largest = std::max(largest, countBDs(task.getBody()));
759 }
760 }
761 });
762 }
763
764 /// Rank tiles, pools, endpoints and routes by their attributes and then, a
765 /// round at a time until no class splits, by the ranks of what they connect
766 /// to. Symbol names and op order play no part, so a permuted or renamed
767 /// design ranks alike, and ops still tied cannot be told apart.
768 void rankStructurally() {
769 StructuralGraph graph;
770 // A placed tile is one node however many ops name it.
771 DenseMap<std::pair<int, int>, Operation *> placed;
772 DenseMap<Operation *, Operation *> tileNode;
773 for (auto tile : device.getOps<TileLike>()) {
774 auto col = tile.tryGetCol(), row = tile.tryGetRow();
775 if (!col || !row) {
776 graph.add(tileNode[tile] = tile);
777 continue;
778 }
779 auto [it, first] = placed.try_emplace({*col, *row}, tile);
780 tileNode[tile] = it->second;
781 if (first) {
782 graph.nodes.push_back(tile);
783 llvm::raw_string_ostream(graph.features[tile])
784 << "tile " << *col << ' ' << *row << ' '
785 << stringifyAIETileType(tile.getTileType());
786 }
787 }
788 auto link = [&](Operation *from, Operation *to, StringRef label) {
789 if (Operation *tile = tileNode.lookup(to))
790 to = tile;
791 graph.link(from, to, label);
792 };
793 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
794 llvm::raw_string_ostream os(graph.add(pool));
795 for (auto segment : pool.getSegmentOps())
796 os << " [" << segment.getOffset() << ", " << segment.getSize() << "]";
797 link(pool, pool.getTile().getDefiningOp(), "tile");
798 }
799 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
800 llvm::raw_string_ostream os(graph.add(endpoint));
801 for (auto segment : endpoint.getSelectedSegments())
802 os << " @" << segment.getOffset();
803 link(endpoint, endpoint.getTile().getDefiningOp(), "tile");
804 link(endpoint, endpoint.getPoolOp(), "pool");
805 }
806 for (auto endpoint : device.getOps<RouteEndpoint>()) {
807 llvm::raw_string_ostream os(graph.add(endpoint));
808 if (auto dma =
809 dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation())) {
810 for (auto segment : dma.getSelectedSegments())
811 os << " @" << segment.getOffset();
812 link(endpoint, dma.getPoolOp(), "pool");
813 }
814 link(endpoint, endpoint.getTile().getDefiningOp(), "tile");
815 }
816 for (auto route : device.getOps<RouteOp>()) {
817 graph.add(route);
818 for (StringRef source : route.getSourceNames())
819 link(route, lookupEndpoint(source), "source");
820 for (StringRef dest : route.getDestinationNames())
821 link(route, lookupEndpoint(dest), "dest");
822 }
823 DenseMap<Operation *, unsigned> rank = graph.rank();
824 for (auto [tile, node] : tileNode)
825 rank[tile] = rank[node];
826 structuralRank = std::move(rank);
827 }
828
829 /// BDs `channel` of the tile at (`col`, `row`) can use that `demand` leaves
830 /// over, counting every channel whose BD ids overlap its own. Negative when
831 /// overcommitted.
832 int64_t bdRoom(TileLike tile, int col, int row, int channel,
833 const DenseMap<std::tuple<int, int, int>, int64_t> &demand) {
834 const AIETargetModel &target = device.getTargetModel();
835 uint32_t numBDs = target.getNumBDs(col, row);
836 int numChannels = std::max(tile.getNumSourceConnections(WireBundle::DMA),
837 tile.getNumDestConnections(WireBundle::DMA));
838 auto sharesBDs = [&](int other) {
839 for (uint32_t bd = 0; bd < numBDs; ++bd)
840 if (target.isBdChannelAccessible(col, row, bd, channel) &&
841 target.isBdChannelAccessible(col, row, bd, other))
842 return true;
843 return false;
844 };
845 int64_t room = target.getNumBDsForChannel(col, row, channel);
846 for (int other = 0; other < numChannels; ++other)
847 if (sharesBDs(other))
848 if (auto it = demand.find({col, row, other}); it != demand.end())
849 room -= it->second;
850 return room;
851 }
852
853 /// On a tile whose BD ids are split between channels, the free channel whose
854 /// BDs have the most room left once every channel sharing them has its
855 /// demand; ties, and tiles without such a split, go to the lowest index.
856 /// Falls back to first-free where coordinates are unknown.
857 int chooseChannel(DMAChannelAnalysis &channels, RouteEndpoint endpoint,
858 const DenseMap<std::tuple<int, int, int>, int64_t> &demand,
859 bool adjacent) {
860 TileLike tile = tileOf(endpoint);
861 DMAChannelDir dir = endpoint.getRouteDirection();
862 std::optional<int> col = tile.tryGetCol(), row = tile.tryGetRow();
863 if (!col || !row)
864 return channels.getDMAChannelIndex(tile, dir, adjacent,
865 endpoint.getOperation());
866 int best = -1;
867 int64_t bestRoom = 0;
868 int limit = DMAChannelAnalysis::getDMAChannelLimit(tile, dir, adjacent);
869 for (int channel = 0; channel < limit; ++channel) {
870 if (!channels.isChannelFree(tile, dir, channel))
871 continue;
872 int64_t room = bdRoom(tile, *col, *row, channel, demand);
873 if (best < 0 || room > bestRoom) {
874 best = channel;
875 bestRoom = room;
876 }
877 }
878 if (best < 0)
879 return -1;
880 return channels.reservePinnedChannel(tile, dir, best,
881 endpoint.getOperation());
882 }
883
884 void noteChannelOwner(InFlightDiagnostic &diag, Operation *owner,
885 std::optional<int> channel = std::nullopt) {
886 auto &note = diag.attachNote(owner->getLoc());
887 if (auto endpoint = dyn_cast<RouteEndpoint>(owner)) {
888 note << "DMA endpoint @" << cast<SymbolOpInterface>(owner).getName();
889 if (auto fifo = owner->getAttrOfType<StringAttr>("fifoName"))
890 note << " for ObjectFifo @" << fifo.getValue();
891 if (reachesAdjacentTile(endpoint))
892 note << " requires adjacent MemTile access";
893 if (channel)
894 note << "; occupies channel " << *channel;
895 } else if (channel) {
896 note << "pre-existing " << owner->getName() << " reserves DMA channel "
897 << *channel;
898 }
899 }
900
901 // The endpoints of the routes that switch packets.
902 DenseSet<Operation *> packetEndpoints() {
903 DenseSet<Operation *> endpoints;
904 for (auto flow : device.getOps<RouteOp>()) {
905 if (!flow.getPacket())
906 continue;
907 for (StringRef source : flow.getSourceNames())
908 endpoints.insert(lookupEndpoint(source).getOperation());
909 for (StringRef dest : flow.getDestinationNames())
910 endpoints.insert(lookupEndpoint(dest).getOperation());
911 }
912 return endpoints;
913 }
914
915 // No allocation can move a stream port the design names, so a clash fails
916 // the pass before the search rather than surfacing inside it.
917 LogicalResult checkStreamPorts() {
918 DMAChannelAnalysis channels(device);
919 DenseSet<Operation *> packetEndpoints = this->packetEndpoints();
920 bool clash = false;
921 for (auto endpoint : device.getOps<RouteEndpoint>()) {
922 if (endpoint.getRouteBundle() != WireBundle::Core)
923 continue;
924 std::optional<int> channel = endpoint.getRouteChannel();
925 if (!channel)
926 return endpoint->emitOpError("a stream port names its own channel");
927 clash |= failed(channels.checkAIEStreamIndex(
928 tileOf(endpoint), {endpoint.getRouteDirection(), *channel},
929 endpoint.getOperation(),
930 packetEndpoints.contains(endpoint.getOperation())));
931 }
932 return failure(clash);
933 }
934
935 /// Whether this endpoint can share its pinned channel without a cyclic
936 /// DMA program monopolizing it.
937 bool sharesPinnedChannel(RouteEndpoint endpoint, DMAChannelDir dir) {
938 // An out-of-order receiver takes each object into the buffer its header
939 // names, so its channel keeps no order its fifos would have to agree on.
940 auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation());
941 if (dir == DMAChannelDir::S2MM)
942 return dma && dma.getOutOfOrder();
943 if (!tileOf(endpoint).isShimTile() &&
944 !(dma && dma.getReleaseStrategy() == ObjectFifoRelease::PushQueue))
945 return false;
946 bool hasPacketRoute = false;
947 for (auto flow : device.getOps<RouteOp>()) {
948 if (llvm::none_of(flow.getSourceNames(), [&](StringRef name) {
949 return lookupEndpoint(name).getOperation() ==
950 endpoint.getOperation();
951 }))
952 continue;
953 if (!flow.getPacket())
954 return false;
955 hasPacketRoute = true;
956 }
957 return hasPacketRoute;
958 }
959
960 LogicalResult assignChannels(DMAChannelAnalysis &channels,
961 bool diagnose = true) {
962 channelAssignments.clear();
963 channelFailure = nullptr;
964 SmallVector<RouteEndpoint> pending;
965 // BDs per placed (col, row, channel) as channels are handed out, for
966 // choosing among a tile's channels by the BD ids they can use.
967 DenseMap<std::tuple<int, int, int>, int64_t> demand = fixedBDs;
968 auto noteDemand = [&](RouteEndpoint endpoint, int channel) {
969 TileLike tile = tileOf(endpoint);
970 auto col = tile.tryGetCol(), row = tile.tryGetRow();
971 if (!col || !row)
972 return;
973 int64_t bds = 0;
974 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation()))
975 bds = dma.getNumBDs();
976 else if (isProgramOwned(endpoint))
977 bds = endpointBDs(endpoint.getOperation());
978 demand[{*col, *row, channel}] += bds;
979 };
980 for (auto endpoint : device.getOps<RouteEndpoint>()) {
981 DMAChannelDir dir = endpoint.getRouteDirection();
982 std::optional<int> channel = endpoint.getRouteChannel();
983 // A core's stream port is named by the design, not drawn from the tile's
984 // DMA channels; checkStreamPorts has claimed it.
985 if (endpoint.getRouteBundle() == WireBundle::Core ||
986 allocationSharers.count(endpoint.getOperation()) ||
987 channelSharers.count(endpoint.getOperation()))
988 continue;
989
990 if (channel) {
991 if (reachesAdjacentTile(endpoint) &&
992 *channel >= DMAChannelAnalysis::getDMAChannelLimit(tileOf(endpoint),
993 dir, true)) {
994 channelFailure = endpoint;
995 if (!diagnose)
996 return failure();
997 return endpoint->emitOpError("pinned ")
998 << stringifyDMAChannelDir(dir) << " DMA channel " << *channel
999 << " cannot access adjacent MemTile buffers or locks";
1000 }
1001 if (channels.reservePinnedChannel(
1002 tileOf(endpoint), dir, *channel, endpoint.getOperation(),
1003 sharesPinnedChannel(endpoint, dir)) < 0) {
1004 if (!diagnose)
1005 return failure();
1006 auto diag = endpoint->emitOpError("pinned ");
1007 diag << stringifyDMAChannelDir(dir) << " DMA channel " << *channel
1008 << " is out of range or already in use on this tile";
1009 if (Operation *owner =
1010 channels.getDMAChannelOwner(tileOf(endpoint), dir, *channel))
1011 noteChannelOwner(diag, owner, channel);
1012 return failure();
1013 }
1014 noteDemand(endpoint, *channel);
1015 continue;
1016 }
1017 pending.push_back(endpoint);
1018 }
1019
1020 // Endpoints reaching a spilled buffer draw from a restricted channel
1021 // range, so they are served before the unrestricted ones. Channels the
1022 // design programs itself come after the objectFIFOs', whose BDs they are
1023 // then chosen around, the most demanding first. The structural rank
1024 // settles the rest, so the order ops were written in never does.
1025 DenseMap<Operation *, int64_t> programBDs;
1026 for (auto endpoint : pending)
1027 if (isProgramOwned(endpoint))
1028 programBDs[endpoint.getOperation()] =
1029 endpointBDs(endpoint.getOperation());
1030 llvm::stable_sort(pending, [&](RouteEndpoint a, RouteEndpoint b) {
1031 bool aAdjacent = reachesAdjacentTile(a);
1032 bool bAdjacent = reachesAdjacentTile(b);
1033 if (aAdjacent != bAdjacent)
1034 return aAdjacent;
1035 bool aProgram = isProgramOwned(a), bProgram = isProgramOwned(b);
1036 if (aProgram != bProgram)
1037 return bProgram;
1038 int64_t aBDs = programBDs.lookup(a.getOperation());
1039 int64_t bBDs = programBDs.lookup(b.getOperation());
1040 if (aBDs != bBDs)
1041 return aBDs > bBDs;
1042 return structuralRank.lookup(a.getOperation()) <
1043 structuralRank.lookup(b.getOperation());
1044 });
1045
1046 for (auto endpoint : pending) {
1047 DMAChannelDir dir = endpoint.getRouteDirection();
1048 TileLike tile = tileOf(endpoint);
1049 bool adjacent = reachesAdjacentTile(endpoint);
1050 // An objectFIFO end takes the first free channel, unless that channel's
1051 // BD ids have too few left for it: then the one with the most left.
1052 bool roomy = true;
1053 auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation());
1054 std::optional<int> col = tile.tryGetCol(), row = tile.tryGetRow();
1055 if (dma && col && row) {
1056 int limit = DMAChannelAnalysis::getDMAChannelLimit(tile, dir, adjacent);
1057 for (int free = 0; free < limit; ++free) {
1058 if (!channels.isChannelFree(tile, dir, free))
1059 continue;
1060 roomy = bdRoom(tile, *col, *row, free, demand) >= dma.getNumBDs();
1061 break;
1062 }
1063 }
1064 int channel = isProgramOwned(endpoint) || !roomy
1065 ? chooseChannel(channels, endpoint, demand, adjacent)
1066 : channels.getDMAChannelIndex(tile, dir, adjacent,
1067 endpoint.getOperation());
1068 if (channel < 0) {
1069 channelFailure = endpoint;
1070 if (!diagnose)
1071 return failure();
1072 int capacity =
1073 DMAChannelAnalysis::getDMAChannelLimit(tile, dir, adjacent);
1074 auto diag =
1075 tile.emitOpError(dir == DMAChannelDir::MM2S
1076 ? "number of output DMA channel exceeded!"
1077 : "number of input DMA channel exceeded!");
1078 diag << " requires at least " << capacity + 1 << " "
1079 << stringifyDMAChannelDir(dir) << " channels, but capacity is "
1080 << capacity;
1081 if (adjacent) {
1082 diag << " for adjacent MemTile access";
1083 }
1084 for (int i = 0; i < capacity; ++i)
1085 if (Operation *owner = channels.getDMAChannelOwner(tile, dir, i))
1086 noteChannelOwner(diag, owner, i);
1087 noteChannelOwner(diag, endpoint.getOperation());
1088 return failure();
1089 }
1090 channelAssignments[endpoint.getOperation()] = channel;
1091 noteDemand(endpoint, channel);
1092 }
1093 DenseSet<Operation *> claimedAllocations;
1094 for (auto [endpoint, alloc] : allocationSharers) {
1095 auto end = cast<RouteEndpoint>(endpoint);
1096 int channel = alloc.getChannelIndex();
1097 if (claimedAllocations.insert(alloc).second &&
1098 channels.reservePinnedChannel(tileOf(end), alloc.getChannelDir(),
1099 channel, endpoint, true) < 0) {
1100 channelFailure = end;
1101 if (!diagnose)
1102 return failure();
1103 auto diag = endpoint->emitOpError("cannot share ");
1104 diag << stringifyDMAChannelDir(alloc.getChannelDir()) << " DMA channel "
1105 << channel << ": already in use or unavailable";
1106 if (Operation *owner = channels.getDMAChannelOwner(
1107 tileOf(end), alloc.getChannelDir(), channel))
1108 noteChannelOwner(diag, owner, channel);
1109 return failure();
1110 }
1111 channelAssignments[endpoint] = channel;
1112 noteDemand(end, channel);
1113 }
1114 for (auto [sharer, owner] : channelSharers)
1115 channelAssignments[sharer] = channelAssignments.lookup(owner);
1116 // Runtime tasks did not steer the choice, but they hold BDs while they
1117 // run, which the overcommit warning counts.
1118 for (auto &[at, bds] : taskBDs)
1119 demand[at] += bds;
1120 for (auto endpoint : device.getOps<RouteEndpoint>()) {
1121 TileLike tile = tileOf(endpoint);
1122 std::optional<int> col = tile.tryGetCol(), row = tile.tryGetRow();
1123 std::optional<int> channel = endpoint.getRouteChannel();
1124 if (auto it = channelAssignments.find(endpoint.getOperation());
1125 it != channelAssignments.end())
1126 channel = it->second;
1127 if (isProgramOwned(endpoint) && channel && col && row)
1128 demand[{*col, *row, *channel}] +=
1129 endpointTaskBDs(endpoint.getOperation());
1130 }
1131 channelDemand = std::move(demand);
1132 return success();
1133 }
1134
1135 /// Gives one more shim end of `exhausted` the channel of ends it is never in
1136 /// flight with, in the order ShimTransferSpans groups them, so only as many
1137 /// ends share as the tile needs. Packet headers tell a shared channel's ends
1138 /// apart.
1139 bool planShimSharing(TileLike exhausted, DMAChannelDir dir) {
1140 ShimTransferSpans spans(
1141 device,
1142 [&](StringAttr symbol) -> std::optional<ShimTransferSpans::End> {
1143 auto endpoint = SymbolTable::lookupNearestSymbolFrom<RouteEndpointOp>(
1144 device, symbol);
1145 if (!endpoint || endpoint.getBundle() != WireBundle::DMA ||
1146 !endpoint.getTileLike().isShimTile())
1147 return std::nullopt;
1148 ShimTransferSpans::End end{
1149 endpoint.getFifoNameAttr() ? endpoint.getFifoNameAttr()
1150 : endpoint.getSymNameAttr(),
1151 endpoint.getRouteDirection() == DMAChannelDir::MM2S};
1152 return end;
1153 });
1154 auto canShare = [&](RouteEndpointOp a, RouteEndpointOp b) {
1155 return spans.apart(
1156 a.getFifoNameAttr() ? a.getFifoNameAttr() : a.getSymNameAttr(),
1157 b.getFifoNameAttr() ? b.getFifoNameAttr() : b.getSymNameAttr());
1158 };
1159 SmallVector<Operation *> candidates;
1160 SmallVector<StringAttr> fifos;
1161 for (auto endpoint : device.getOps<RouteEndpointOp>()) {
1162 if (endpoint.getBundle() != WireBundle::DMA ||
1163 endpoint.getChannelIndex() ||
1164 allocationSharers.count(endpoint.getOperation()) ||
1165 endpoint.getTile() != exhausted->getResult(0) ||
1166 endpoint.getRouteDirection() != dir)
1167 continue;
1168 candidates.push_back(endpoint);
1169 fifos.push_back(endpoint.getFifoNameAttr() ? endpoint.getFifoNameAttr()
1170 : endpoint.getSymNameAttr());
1171 }
1172 for (auto alloc : device.getOps<ShimDMAAllocationOp>()) {
1173 auto allowed = AIEDialect::ShareableWithAttrHelper(alloc->getContext())
1174 .getAttr(alloc);
1175 if (!allowed || alloc.getTile() != exhausted->getResult(0) ||
1176 alloc.getChannelDir() != dir)
1177 continue;
1178 if (llvm::any_of(device.getOps<RouteEndpoint>(), [&](RouteEndpoint end) {
1179 return end.getRouteBundle() == WireBundle::DMA &&
1180 sameTile(end.getTile(), alloc.getTile()) &&
1181 end.getRouteDirection() == dir &&
1182 end.getRouteChannel() == alloc.getChannelIndex();
1183 }))
1184 continue;
1185 for (Operation *candidate : candidates) {
1186 auto endpoint = cast<RouteEndpointOp>(candidate);
1187 if (allocationSharers.count(candidate) ||
1188 channelSharers.count(candidate) ||
1189 llvm::none_of(allowed, [&](Attribute attr) {
1190 auto symbol = dyn_cast<FlatSymbolRefAttr>(attr);
1191 return symbol && symbol.getValue() == endpoint.getSymName();
1192 }))
1193 continue;
1194 if (llvm::any_of(allocationSharers, [&](const auto &entry) {
1195 if (entry.second != alloc)
1196 return false;
1197 auto other = cast<RouteEndpointOp>(entry.first);
1198 return !canShare(endpoint, other);
1199 }))
1200 continue;
1201 allocationSharers[candidate] = alloc;
1202 return true;
1203 }
1204 }
1205 SmallVector<unsigned> order;
1206 for (auto &group : spans.groups(fifos))
1207 llvm::append_range(order, group);
1208 SmallVector<SmallVector<unsigned>> groups;
1209 for (unsigned candidate : order) {
1210 auto *group = llvm::find_if(groups, [&](ArrayRef<unsigned> members) {
1211 return llvm::all_of(members, [&](unsigned member) {
1212 return canShare(cast<RouteEndpointOp>(candidates[candidate]),
1213 cast<RouteEndpointOp>(candidates[member]));
1214 });
1215 });
1216 if (group == groups.end())
1217 groups.push_back({candidate});
1218 else
1219 group->push_back(candidate);
1220 }
1221 for (auto &members : groups)
1222 for (unsigned sharer : llvm::drop_begin(members))
1223 if (channelSharers
1224 .insert({candidates[sharer], candidates[members.front()]})
1225 .second)
1226 return true;
1227 return false;
1228 }
1229
1230 /// A MemTile whose endpoints need more buffer descriptors than it has
1231 /// walks its deepest plain pools with one descriptor a side instead, their
1232 /// objects side by side in one buffer and counted by one lock pair: the
1233 /// deepest first, until its descriptors fit.
1234 /// Whether one iterating descriptor a side could walk `pool`: a plain mem
1235 /// tile pool of one segment, deeper than one object and no deeper than a
1236 /// lock counts, whose ends neither dispatch nor take turns, but those in
1237 /// `regrouped`, whose turns are being chosen again.
1238 bool canWalkWhole(ObjectFifoPoolOp pool,
1239 ArrayRef<ObjectFifoDmaEndpointOp> regrouped = {}) {
1240 const AIETargetModel &target = device.getTargetModel();
1241 TileLike tile =
1242 pool ? dyn_cast<TileLike>(pool.getTile().getDefiningOp()) : TileLike();
1243 return tile && tile.isMemTile() && !pool.getContiguous() &&
1244 pool.getSegmentOps().size() == 1 && !pool.getRepeatCount() &&
1245 !pool.getBuffers() && pool.getDepth() > 1 &&
1246 pool.getDepth() <= static_cast<int>(target.getMaxLockValue()) &&
1247 llvm::all_of(device.getOps<ObjectFifoDmaEndpointOp>(),
1248 [&](ObjectFifoDmaEndpointOp end) {
1249 return end.getPoolOp() != pool ||
1250 (!end.getOutOfOrder() &&
1251 end.getReleaseStrategy() !=
1252 ObjectFifoRelease::PushQueue &&
1253 !end.getDispatch() &&
1254 (!end.getTurns() ||
1255 llvm::is_contained(regrouped, end)));
1256 });
1257 }
1258
1259 void walkDeepPoolsWhole() {
1260 const AIETargetModel &target = device.getTargetModel();
1261 llvm::MapVector<Value, SmallVector<ObjectFifoDmaEndpointOp>> byTile;
1262 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>())
1263 if (dma.getTileLike() && dma.getTileLike().isMemTile())
1264 byTile[dma.getTile()].push_back(dma);
1265 for (SmallVector<ObjectFifoDmaEndpointOp> &ends :
1266 llvm::make_second_range(byTile)) {
1267 // A mem tile's BD ids are split between its even and odd channels, and
1268 // channels go to ends in an order of their own, each to the half with
1269 // room. That cannot fail while every end fits beside all the others in
1270 // either half: the ends' BDs and the largest of them within the BDs
1271 // plus one.
1272 TileLike tileLike = ends.front().getTileLike();
1273 std::optional<int> col = tileLike.tryGetCol(), row = tileLike.tryGetRow();
1274 int64_t numBDs = target.getNumBDs(AIETileType::MemTile);
1275 bool halved =
1276 col && row && target.getNumBDsForChannel(*col, *row, 0) < numBDs;
1277 auto fits = [&] {
1278 int64_t bds = 0, largest = 0;
1279 for (ObjectFifoDmaEndpointOp end : ends) {
1280 bds += end.getNumBDs();
1281 largest = std::max(largest, end.getNumBDs());
1282 }
1283 return halved ? bds + largest <= numBDs + 1 : bds <= numBDs;
1284 };
1285 SmallVector<ObjectFifoPoolOp> pools;
1286 for (ObjectFifoDmaEndpointOp end : ends) {
1287 ObjectFifoPoolOp pool = end.getPoolOp();
1288 if (!llvm::is_contained(pools, pool) && canWalkWhole(pool))
1289 pools.push_back(pool);
1290 }
1291 llvm::stable_sort(pools, [&](ObjectFifoPoolOp a, ObjectFifoPoolOp b) {
1292 if (a.getDepth() != b.getDepth())
1293 return a.getDepth() > b.getDepth();
1294 return structuralRank.lookup(a) < structuralRank.lookup(b);
1295 });
1296 for (ObjectFifoPoolOp pool : pools) {
1297 if (fits())
1298 break;
1299 pool.setContiguous(true);
1300 }
1301 }
1302 }
1303
1304 /// The other end of the one route naming `endpoint`, when that route is a
1305 /// circuit between two objectFIFO DMA endpoints.
1306 ObjectFifoDmaEndpointOp circuitPeer(ObjectFifoDmaEndpointOp endpoint) {
1307 ObjectFifoDmaEndpointOp peer;
1308 StringRef name = endpoint.getSymName();
1309 for (RouteOp route : device.getOps<RouteOp>()) {
1310 SmallVector<StringRef> sources = llvm::to_vector(route.getSourceNames());
1311 SmallVector<StringRef> dests =
1312 llvm::to_vector(route.getDestinationNames());
1313 bool source = llvm::is_contained(sources, name);
1314 if (!source && !llvm::is_contained(dests, name))
1315 continue;
1316 if (peer || route.getPacket() || sources.size() != 1 || dests.size() != 1)
1317 return {};
1318 RouteEndpoint other = lookupEndpoint(source ? dests[0] : sources[0]);
1319 peer = other ? dyn_cast<ObjectFifoDmaEndpointOp>(other.getOperation())
1320 : ObjectFifoDmaEndpointOp();
1321 if (!peer)
1322 return {};
1323 }
1324 return peer;
1325 }
1326
1327 /// The one core endpoint working through `endpoint`'s pool from the other
1328 /// side.
1329 ObjectFifoCoreEndpointOp coreEndOf(ObjectFifoDmaEndpointOp endpoint) {
1330 ObjectFifoCoreEndpointOp found;
1331 for (auto core : device.getOps<ObjectFifoCoreEndpointOp>()) {
1332 if (core.getPool() != endpoint.getPool())
1333 continue;
1334 if (found || core.drains() == endpoint.drains())
1335 return {};
1336 found = core;
1337 }
1338 return found;
1339 }
1340
1341 static bool canTakeTurns(ObjectFifoDmaEndpointOp endpoint) {
1342 return endpoint &&
1343 endpoint.getReleaseStrategy() != ObjectFifoRelease::PushQueue &&
1344 !endpoint.getChannelIndex() && !endpoint.getDispatch() &&
1345 !endpoint.getPacketAttr() && !endpoint.getIterCount() &&
1346 endpoint.getRepeat() == 1 && !endpoint.walksStream();
1347 }
1348
1349 /// Whether a chain moving `turns` over `ends` can walk each contiguous pool
1350 /// with one descriptor per turn of its unrolled passes, each stepping over
1351 /// the others' objects: they must divide the pool's objects.
1352 static bool contiguousWalkersFit(ArrayRef<ObjectFifoDmaEndpointOp> ends,
1353 ArrayRef<unsigned> turns) {
1354 SmallVector<std::pair<int64_t, int64_t>> counts;
1355 for (auto [i, constEnd] : llvm::enumerate(ends)) {
1356 ObjectFifoDmaEndpointOp end = constEnd;
1357 counts.push_back({llvm::count(turns, i), end.getPoolOp().getContiguous()
1358 ? 0
1359 : end.getNumDescriptors()});
1360 }
1361 int64_t passes = turnPasses(counts);
1362 for (auto [i, constEnd] : llvm::enumerate(ends)) {
1363 ObjectFifoDmaEndpointOp end = constEnd;
1364 if (end.getPoolOp().getContiguous() &&
1365 (end.walksStream() ||
1366 end.getPoolOp().getDepth() % (counts[i].first * passes)))
1367 return false;
1368 }
1369 return true;
1370 }
1371
1372 /// Gives `ends` one channel, the k-th object belonging to `ends[order[k]]`.
1373 void takeTurns(MutableArrayRef<ObjectFifoDmaEndpointOp> ends,
1374 ArrayRef<unsigned> order) {
1375 SmallVector<Attribute> names;
1376 for (unsigned i : order)
1377 names.push_back(FlatSymbolRefAttr::get(ends[i].getSymNameAttr()));
1378 ArrayAttr turns = builder.getArrayAttr(names);
1379 Operation *owner = ends[order.front()];
1380 channelSharers.erase(owner);
1381 for (ObjectFifoDmaEndpointOp end : ends) {
1382 end.setTurnsAttr(turns);
1383 if (end != owner)
1384 channelSharers[end] = owner;
1385 }
1386 }
1387
1388 /// Gives one more end of `exhausted` the channel of others whose objects
1389 /// the same core needs in a fixed order, read off its program, so only as
1390 /// many ends share as the tile needs. A circuit carries one stream, so the
1391 /// ends at the routes' other ends take the same turns.
1392 bool planTurnSharing(TileLike exhausted, DMAChannelDir dir) {
1393 // `coreEndOf`/`circuitPeer` each rescan the whole device; neither's
1394 // domain (core endpoints, routes) changes within this function (only
1395 // pool `contiguous` flags and endpoints' `turns` attrs do), so caching
1396 // them once here turns every lookup below from O(device size) into O(1).
1397 DenseMap<Operation *, ObjectFifoCoreEndpointOp> coreEndCache;
1398 DenseMap<Operation *, ObjectFifoDmaEndpointOp> circuitPeerCache;
1399 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>()) {
1400 coreEndCache[dma] = coreEndOf(dma);
1401 circuitPeerCache[dma] = circuitPeer(dma);
1402 }
1403 auto cachedCoreEnd = [&](ObjectFifoDmaEndpointOp dma) {
1404 return coreEndCache.lookup(dma);
1405 };
1406 auto cachedCircuitPeer = [&](ObjectFifoDmaEndpointOp dma) {
1407 return circuitPeerCache.lookup(dma);
1408 };
1409 SmallVector<ObjectFifoDmaEndpointOp> candidates;
1410 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>())
1411 if (sameTile(dma.getTile(), exhausted->getResult(0)) &&
1412 dma.getRouteDirection() == dir && cachedCoreEnd(dma) &&
1413 canTakeTurns(dma.getTurns() ? turnEndpoints(dma).front() : dma) &&
1414 canTakeTurns(cachedCircuitPeer(dma)))
1415 candidates.push_back(dma);
1416 llvm::stable_sort(candidates, [&](Operation *a, Operation *b) {
1417 return structuralRank.lookup(a) < structuralRank.lookup(b);
1418 });
1419 LLVM_DEBUG({
1420 llvm::dbgs() << "turns on " << exhausted->getLoc() << ": "
1421 << candidates.size() << " candidate ends\n";
1422 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>())
1423 if (sameTile(dma.getTile(), exhausted->getResult(0)) &&
1424 dma.getRouteDirection() == dir)
1425 llvm::dbgs() << " " << dma.getSymName() << ": core end "
1426 << bool(cachedCoreEnd(dma)) << ", takes turns "
1427 << canTakeTurns(dma) << ", peer takes turns "
1428 << canTakeTurns(cachedCircuitPeer(dma)) << "\n";
1429 });
1430 auto groupOf = [](ObjectFifoDmaEndpointOp end) {
1431 SmallVector<ObjectFifoDmaEndpointOp> group = turnEndpoints(end);
1432 return group.empty() ? SmallVector<ObjectFifoDmaEndpointOp>{end} : group;
1433 };
1434 for (auto [i, a] : llvm::enumerate(candidates)) {
1435 for (ObjectFifoDmaEndpointOp b : llvm::drop_begin(candidates, i + 1)) {
1436 SmallVector<ObjectFifoDmaEndpointOp> group = groupOf(a);
1437 if (llvm::is_contained(group, b))
1438 continue;
1439 llvm::append_range(group, groupOf(b));
1440 SmallVector<ObjectFifoCoreEndpointOp> cores;
1441 SmallVector<ObjectFifoDmaEndpointOp> peers;
1442 SmallVector<int64_t> depths;
1443 for (ObjectFifoDmaEndpointOp end : group) {
1444 cores.push_back(cachedCoreEnd(end));
1445 peers.push_back(cachedCircuitPeer(end));
1446 depths.push_back(end.getPoolOp().getDepth());
1447 }
1448 if (llvm::any_of(peers, [&](ObjectFifoDmaEndpointOp peer) {
1449 return !sameTile(peer.getTile(), peers.front().getTile()) ||
1450 llvm::any_of(turnEndpoints(peer), [&](auto other) {
1451 return !llvm::is_contained(peers, other);
1452 });
1453 })) {
1454 LLVM_DEBUG(llvm::dbgs()
1455 << " " << a.getSymName() << " + " << b.getSymName()
1456 << ": senders on different tiles\n");
1457 continue;
1458 }
1459 CoreTurns turns;
1460 std::string reason;
1461 if (succeeded(turns.read(cores, reason)) && !turns.keepsUp(depths))
1462 reason = "would stall";
1463 // Over plain pools the chain unrolls to whole passes over each; past
1464 // the peers' channel BDs, walking them whole takes one a turn.
1465 bool overflows = false;
1466 if (reason.empty()) {
1467 SmallVector<std::pair<int64_t, int64_t>> counts;
1468 for (auto [i, constPeer] : llvm::enumerate(peers)) {
1469 ObjectFifoDmaEndpointOp peer = constPeer;
1470 counts.push_back({llvm::count(turns.turns(), i),
1471 peer.getPoolOp().getContiguous()
1472 ? 0
1473 : peer.getNumDescriptors()});
1474 }
1475 TileLike tile = peers.front().getTileLike();
1476 std::optional<int> col = tile.tryGetCol(), row = tile.tryGetRow();
1477 overflows =
1478 col && row &&
1479 turnPasses(counts) * static_cast<int64_t>(turns.turns().size()) >
1480 device.getTargetModel().getNumBDsForChannel(*col, *row, 0);
1481 }
1482 // Beside a pool walked whole, the others' descriptors must come round
1483 // each pass, which walking them whole as well lines up.
1484 // A pool whose other end already takes turns may be walked whole too,
1485 // where those turns still line up.
1486 SmallVector<ObjectFifoPoolOp> walked;
1487 if (reason.empty() &&
1488 (overflows || !contiguousWalkersFit(peers, turns.turns())))
1489 for (ObjectFifoDmaEndpointOp peer : peers) {
1490 ObjectFifoPoolOp pool = peer.getPoolOp();
1491 SmallVector<ObjectFifoDmaEndpointOp> others;
1492 for (auto end : device.getOps<ObjectFifoDmaEndpointOp>())
1493 if (end.getPoolOp() == pool && end.getTurns() &&
1494 !llvm::is_contained(peers, end))
1495 others.push_back(end);
1496 SmallVector<ObjectFifoDmaEndpointOp> ignored(peers);
1497 llvm::append_range(ignored, others);
1498 if (!canWalkWhole(pool, ignored)) {
1499 LLVM_DEBUG(llvm::dbgs() << " " << pool.getSymName()
1500 << " cannot be walked whole\n");
1501 continue;
1502 }
1503 pool.setContiguous(true);
1504 bool linedUp =
1505 llvm::all_of(others, [&](ObjectFifoDmaEndpointOp end) {
1506 SmallVector<ObjectFifoDmaEndpointOp> ends =
1507 turnEndpoints(end);
1508 SmallVector<unsigned> order;
1509 for (auto name :
1510 end.getTurnsAttr().getAsRange<FlatSymbolRefAttr>())
1511 order.push_back(
1512 llvm::find_if(ends,
1513 [&](ObjectFifoDmaEndpointOp e) {
1514 return e.getSymNameAttr() ==
1515 name.getAttr();
1516 }) -
1517 ends.begin());
1518 return contiguousWalkersFit(ends, order);
1519 });
1520 LLVM_DEBUG(
1521 llvm::dbgs()
1522 << " " << pool.getSymName() << " walked whole"
1523 << (linedUp ? "" : ", but its other turns would not line up")
1524 << "\n");
1525 if (linedUp)
1526 walked.push_back(pool);
1527 else
1528 pool.setContiguous(false);
1529 }
1530 if (reason.empty() && (!contiguousWalkersFit(group, turns.turns()) ||
1531 !contiguousWalkersFit(peers, turns.turns()))) {
1532 reason = "a contiguous pool's objects do not divide among its turns";
1533 for (ObjectFifoPoolOp pool : walked)
1534 pool.setContiguous(false);
1535 }
1536 if (!reason.empty()) {
1537 LLVM_DEBUG(llvm::dbgs() << " " << a.getSymName() << " + "
1538 << b.getSymName() << ": " << reason << "\n");
1539 continue;
1540 }
1541 takeTurns(group, turns.turns());
1542 takeTurns(peers, turns.turns());
1543 return true;
1544 }
1545 }
1546 return false;
1547 }
1548
1549 /// A route reaching or leaving a shared shim channel carries a packet
1550 /// header, its id distinct from those of the routes it shares the channel
1551 /// with. Other shared channels reuse the same ids: the router keeps flows
1552 /// with one id apart wherever their destinations differ.
1553 LogicalResult headShimSharingRoutes() {
1554 DenseMap<Operation *, Operation *> channelOf;
1555 for (auto [sharer, owner] : channelSharers) {
1556 if (!tileOf(cast<RouteEndpoint>(owner)).isShimTile())
1557 continue;
1558 channelOf[sharer] = owner;
1559 channelOf[owner] = owner;
1560 }
1561 for (auto [endpoint, alloc] : allocationSharers)
1562 channelOf[endpoint] = alloc.getOperation();
1563 auto channelsOf = [&](RouteOp route) {
1564 SmallVector<Operation *> channels;
1565 auto note = [&](StringRef name) {
1566 if (auto endpoint = lookupEndpoint(name);
1567 endpoint && channelOf.count(endpoint.getOperation()))
1568 channels.push_back(channelOf.lookup(endpoint.getOperation()));
1569 };
1570 llvm::for_each(route.getSourceNames(), note);
1571 llvm::for_each(route.getDestinationNames(), note);
1572 return channels;
1573 };
1574 DenseMap<Operation *, DenseSet<int>> idsOn;
1575 for (auto route : device.getOps<RouteOp>())
1576 if (PacketInfoAttr packet = route.getPacketAttr();
1577 packet && packet.isAssigned())
1578 for (Operation *channel : channelsOf(route))
1579 idsOn[channel].insert(packet.assignedId());
1580 PacketIdSpace ids(device);
1581 SmallVector<int> reused;
1582 for (auto route : device.getOps<RouteOp>()) {
1583 SmallVector<Operation *> channels = channelsOf(route);
1584 if (route.getPacket() || channels.empty())
1585 continue;
1586 auto freeOnAll = [&](int id) {
1587 return llvm::none_of(channels, [&](Operation *channel) {
1588 return idsOn[channel].contains(id);
1589 });
1590 };
1591 std::optional<int> id;
1592 if (auto *it = llvm::find_if(reused, freeOnAll); it != reused.end())
1593 id = *it;
1594 else if ((id = ids.takeLowestFrom()))
1595 reused.push_back(*id);
1596 else
1597 return route.emitOpError("shares a shim channel, but no packet id is "
1598 "left to tell its transfers apart");
1599 for (Operation *channel : channels)
1600 idsOn[channel].insert(*id);
1601 route.setPacketAttr(PacketInfoAttr::get(builder.getContext(), 0,
1602 static_cast<uint16_t>(*id)));
1603 }
1604 return success();
1605 }
1606
1607 /// Channels are chosen by BD room but never refused for lack of it: a task
1608 /// that frees its BDs before the next starts lets demand exceed the BDs.
1609 /// Otherwise BD id assignment fails, so say where.
1610 void warnOvercommittedBDs() {
1611 const AIETargetModel &target = device.getTargetModel();
1612 for (auto endpoint : device.getOps<RouteEndpoint>()) {
1613 auto it = channelAssignments.find(endpoint.getOperation());
1614 TileLike tile = tileOf(endpoint);
1615 std::optional<int> col = tile.tryGetCol(), row = tile.tryGetRow();
1616 if (it == channelAssignments.end() || !isProgramOwned(endpoint) || !col ||
1617 !row)
1618 continue;
1619 int channel = it->second;
1620 int64_t room = bdRoom(tile, *col, *row, channel, channelDemand);
1621 if (room >= 0)
1622 continue;
1623 int64_t numBDs = target.getNumBDsForChannel(*col, *row, channel);
1624 endpoint->emitWarning("channel ")
1625 << channel << " can use " << numBDs
1626 << " BDs, but it and the channels sharing them need " << numBDs - room
1627 << " at once; BD id assignment fails unless tasks free their BDs "
1628 "before others start";
1629 }
1630 }
1631
1632 /// Keep a successful largest-first allocation unchanged. On channel
1633 /// exhaustion, try keeping an affected pool's resources local, cheapest
1634 /// first. Existing buffers and locks never move. Each DMA accesses every
1635 /// object in its pool, even when it selects only one segment. Reserving that
1636 /// pool once can therefore free several restricted channels. This is a
1637 /// bounded repair of the greedy buffer placement, not an exhaustive solver
1638 /// for tile placement or memory packing.
1639 LogicalResult planAllocation(ArrayRef<ObjectFifoPoolOp> pools,
1640 std::set<std::vector<unsigned>> &tried) {
1641 std::vector<unsigned> key;
1642 for (auto [index, pool] : llvm::enumerate(pools)) {
1643 if (auto tile = localPools.lookup(pool)) {
1644 auto placed = cast<TileLike>(tile.getDefiningOp());
1645 auto col = placed.tryGetCol(), row = placed.tryGetRow();
1646 assert(col && row && "locality repair requires resolved coordinates");
1647 key.push_back(index);
1648 key.push_back(*col);
1649 key.push_back(*row);
1650 }
1651 }
1652 if (tried.size() >= 64 || !tried.insert(key).second ||
1653 failed(planBuffers(pools)) || failed(planLocks(pools)))
1654 return failure();
1655 DMAChannelAnalysis channels(device);
1656 if (succeeded(assignChannels(channels, /*diagnose=*/false)))
1657 return success();
1658 if (!channelFailure || !tileOf(channelFailure).isMemTile() ||
1659 !tileOf(channelFailure).tryGetCol() ||
1660 !tileOf(channelFailure).tryGetRow())
1661 return failure();
1662
1663 Value localTile = channelFailure.getTile();
1664 SmallVector<ObjectFifoPoolOp> candidates;
1665 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>()) {
1666 auto endpoint = cast<RouteEndpoint>(dma.getOperation());
1667 auto pool = dma.getPoolOp();
1668 if (!pool || localPools.contains(pool) ||
1669 !sameTile(endpoint.getTile(), channelFailure.getTile()) ||
1670 endpoint.getRouteDirection() != channelFailure.getRouteDirection() ||
1671 (channelFailure.getRouteChannel() && endpoint != channelFailure) ||
1672 (endpoint.getRouteChannel() && endpoint != channelFailure) ||
1673 !reachesAdjacentTile(endpoint) ||
1674 llvm::is_contained(candidates, pool))
1675 continue;
1676 candidates.push_back(pool);
1677 }
1678 llvm::stable_sort(candidates, [&](ObjectFifoPoolOp a, ObjectFifoPoolOp b) {
1679 int64_t aBytes =
1680 a.getBuffers() ? 0 : a.getObjectSizeInBytes() * a.getDepth();
1681 int64_t bBytes =
1682 b.getBuffers() ? 0 : b.getObjectSizeInBytes() * b.getDepth();
1683 if (aBytes != bBytes)
1684 return aBytes < bBytes;
1685 return structuralRank.lookup(a) < structuralRank.lookup(b);
1686 });
1687 for (auto pool : candidates) {
1688 localPools[pool] = localTile;
1689 if (succeeded(planAllocation(pools, tried)))
1690 return success();
1691 localPools.erase(pool);
1692 }
1693 return failure();
1694 }
1695
1696 /// A packet-switched flow shares the stream with others, so every buffer
1697 /// descriptor the source emits has to carry the packet header.
1698 /// One packet flow carries every source of the route: each stamps the same
1699 /// header, and their packets share the destination channels.
1700 LogicalResult lowerPacketFlow(RouteOp flow, ArrayRef<RouteEndpoint> sources,
1701 PacketInfoAttr header) {
1702 int packetID = header.assignedId();
1703 for (auto [name, endpoint] : llvm::zip_equal(
1704 flow.getSources().getAsRange<FlatSymbolRefAttr>(), sources)) {
1705 RouteEndpoint source = endpoint;
1706 // Written out in turn order after the last route.
1707 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(*source);
1708 dma && dma.getDispatch()) {
1709 dispatchHeaders[dma][*flow.getDestinationNames().begin()] = header;
1710 continue;
1711 }
1712 if (sourcedFlows.at(name.getAttr()).size() == 1)
1713 source.setRoutePacket(header);
1714 }
1715
1716 // An out-of-order receiver reads the buffer it should fill out of the
1717 // header, so the header has to reach its DMA.
1718 bool keepHeader = flow.getKeepPktHeader();
1719 for (StringRef destName : flow.getDestinationNames())
1720 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(
1721 lookupEndpoint(destName).getOperation()))
1722 keepHeader |= dma.getOutOfOrder();
1723
1724 builder.setInsertionPoint(flow);
1725 auto packetFlow = PacketFlowOp::create(
1726 builder, flow.getLoc(),
1727 builder.getIntegerAttr(builder.getI8Type(), packetID),
1728 keepHeader ? builder.getBoolAttr(true) : nullptr, nullptr);
1729 OpBuilder::InsertionGuard g(builder);
1730 Block &ports = packetFlow.getRegion().emplaceBlock();
1731 builder.setInsertionPointToStart(&ports);
1732 EndOp::create(builder, flow.getLoc());
1733
1734 builder.setInsertionPointToStart(&ports);
1735 for (RouteEndpoint source : sources) {
1736 PacketSourceOp::create(builder, flow.getLoc(), source.getTile(),
1737 source.getRouteBundle(), channelOf(source));
1738 }
1739 for (StringRef destName : flow.getDestinationNames()) {
1740 auto dest = lookupEndpoint(destName);
1741 // An out-of-order receiver matches on the header rather than walking a
1742 // chain, so its descriptors carry the id too.
1743 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(dest.getOperation()))
1744 if (dma.getOutOfOrder())
1745 dest.setRoutePacket(header);
1746 PacketDestOp::create(builder, flow.getLoc(), dest.getTile(),
1747 dest.getRouteBundle(), channelOf(dest));
1748 }
1749 return success();
1750 }
1751
1752 /// Assigned before flows are lowered, so every endpoint has one by now.
1753 int channelOf(RouteEndpoint endpoint) {
1754 std::optional<int> channel = endpoint.getRouteChannel();
1755 assert(channel && "channels are assigned before flows are lowered");
1756 return *channel;
1757 }
1758
1759 RouteEndpoint lookupEndpoint(StringRef name) {
1760 return dyn_cast_or_null<RouteEndpoint>(SymbolTable::lookupNearestSymbolFrom(
1761 device, StringAttr::get(device.getContext(), name)));
1762 }
1763
1764 LogicalResult lowerFlows() {
1765 // Independent core-pushed senders need different routing IDs even when
1766 // their descriptors are submitted at different times.
1767 DenseMap<std::tuple<Value, int, int>, Operation *> pushedPacketSources;
1768 for (RouteOp flow : device.getOps<RouteOp>()) {
1769 auto packet = flow.getPacket();
1770 if (!packet || !packet->isAssigned())
1771 continue;
1772 for (StringRef name : flow.getSourceNames()) {
1773 auto source = dyn_cast<ObjectFifoDmaEndpointOp>(
1774 lookupEndpoint(name).getOperation());
1775 if (!source ||
1776 source.getReleaseStrategy() != ObjectFifoRelease::PushQueue)
1777 continue;
1778 auto [it, inserted] = pushedPacketSources.try_emplace(
1779 std::make_tuple(source.getTile(), channelOf(source),
1780 packet->assignedId()),
1781 source.getOperation());
1782 if (!inserted && it->second != source.getOperation()) {
1783 auto diag = source.emitOpError(
1784 "core-pushed senders sharing an MM2S channel require distinct "
1785 "packet IDs");
1786 diag.attachNote(it->second->getLoc()) << "other sender is here";
1787 return failure();
1788 }
1789 }
1790 }
1791 DenseSet<std::tuple<Value, int, Value, int>> circuits;
1792 for (RouteOp flow : device.getOps<RouteOp>())
1793 for (auto name : flow.getSources().getAsRange<FlatSymbolRefAttr>())
1794 sourcedFlows[name.getAttr()].push_back(flow);
1795 for (auto flow : device.getOps<RouteOp>()) {
1796 SmallVector<RouteEndpoint> sources;
1797 for (StringRef name : flow.getSourceNames()) {
1798 sources.push_back(lookupEndpoint(name));
1799 }
1800 loweredFlows.push_back(flow);
1801
1802 // Which switching a route gets was settled upstream of this pass: the
1803 // header is on the route, and --aie-assign-packet-ids gave it its id.
1804 if (std::optional<PacketInfoAttr> packet = flow.getPacket()) {
1805 if (!packet->isAssigned()) {
1806 return flow.emitOpError("has a packet header with no pkt_id; run "
1807 "--aie-assign-packet-ids before this pass");
1808 }
1809 if (failed(lowerPacketFlow(flow, sources, *packet))) {
1810 return failure();
1811 }
1812 continue;
1813 }
1814
1815 // The verifier keeps a circuit route to one source.
1816 RouteEndpoint source = sources.front();
1817
1818 int sourceChannel = channelOf(source);
1819 builder.setInsertionPoint(flow);
1820 for (StringRef destName : flow.getDestinationNames()) {
1821 auto dest = lookupEndpoint(destName);
1822 // Ends taking turns share one circuit for all their routes.
1823 if (!circuits
1824 .insert({source.getTile(), sourceChannel, dest.getTile(),
1825 channelOf(dest)})
1826 .second)
1827 continue;
1828 FlowOp::create(builder, flow.getLoc(), source.getTile(),
1829 source.getRouteBundle(), sourceChannel, dest.getTile(),
1830 dest.getRouteBundle(), channelOf(dest));
1831 }
1832 }
1833 for (auto &[endpoint, byDest] : dispatchHeaders) {
1834 SmallVector<Attribute> headers;
1835 for (StringRef dest :
1836 endpoint.getDispatchAttr().getAsValueRange<FlatSymbolRefAttr>())
1837 headers.push_back(byDest.lookup(dest));
1838 endpoint.setDispatchPacketsAttr(builder.getArrayAttr(headers));
1839 }
1840 return success();
1841 }
1842
1843 /// An `aiex.dma_channel_reset_for` outlives the fifo it names, so record the
1844 /// channels and locks it has to re-arm and point it at that record. Shim
1845 /// endpoints are left out: the host re-pushes those itself.
1846 LogicalResult bindRearmTargets() {
1847 if (poolLocks->rearms().empty()) {
1848 return success();
1849 }
1850
1851 builder.setInsertionPoint(device.getBody()->getTerminator());
1852 for (auto &[fifoName, users] : poolLocks->rearms()) {
1853 SmallVector<Value> channelTiles, lockValues;
1854 SmallVector<int32_t> channelDirs, channelIndices, lockInits;
1855 // The record stands for these endpoints and pools: located at each.
1856 SmallVector<Location> locs;
1857
1858 for (auto endpoint : device.getOps<RouteEndpoint>()) {
1859 std::optional<int> channel = endpoint.getRouteChannel();
1860 if (endpoint.getFifoName() != fifoName ||
1861 tileOf(endpoint).isShimTile() || !channel) {
1862 continue;
1863 }
1864 if (auto dma =
1865 dyn_cast<ObjectFifoDmaEndpointOp>(endpoint.getOperation());
1866 dma && dma.getReleaseStrategy() == ObjectFifoRelease::PushQueue) {
1867 for (AIEX::DmaChannelResetForOp user : users)
1868 user.emitOpError(
1869 "does not support core-pushed DMA channels; reset requires "
1870 "coordination with the issuing core and all shared senders");
1871 return failure();
1872 }
1873 channelTiles.push_back(endpoint.getTile());
1874 channelDirs.push_back(
1875 static_cast<int32_t>(endpoint.getRouteDirection()));
1876 channelIndices.push_back(*channel);
1877 locs.push_back(endpoint->getLoc());
1878 }
1879 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
1880 if (pool.getFifoName() != fifoName || pool.getTileLike().isShimTile()) {
1881 continue;
1882 }
1883 locs.push_back(pool.getLoc());
1884 for (LockOp lock : pool.getLockOps()) {
1885 lockValues.push_back(lock.getResult());
1886 lockInits.push_back(lock.getInit().value_or(0));
1887 }
1888 }
1889
1890 if (channelTiles.empty() && lockValues.empty()) {
1891 for (AIEX::DmaChannelResetForOp user : users) {
1892 user.emitOpError() << "objectFIFO '" << fifoName
1893 << "' has no resident core/mem DMA channels or "
1894 "locks to re-arm";
1895 }
1896 return failure();
1897 }
1898
1899 std::string name = (fifoName + "__rearm").str();
1900 for (unsigned suffix = 0; lookupNamedOpIn(device, StringRef(name));
1901 suffix++) {
1902 name = (fifoName + "__rearm" + std::to_string(suffix + 1)).str();
1903 }
1904
1905 // head_bd_ids and repeat_counts are filled in by --aie-assign-bd-ids.
1906 ObjectFifoRearmBindingOp::create(
1907 builder, FusedLoc::get(builder.getContext(), locs),
1908 builder.getStringAttr(name), ValueRange(channelTiles),
1909 ValueRange(lockValues), builder.getDenseI32ArrayAttr(channelDirs),
1910 builder.getDenseI32ArrayAttr(channelIndices),
1911 builder.getDenseI32ArrayAttr(lockInits),
1912 /*head_bd_ids=*/DenseI32ArrayAttr(),
1913 /*repeat_counts=*/DenseI32ArrayAttr());
1914 auto target = FlatSymbolRefAttr::get(builder.getContext(), name);
1915 for (AIEX::DmaChannelResetForOp user : users) {
1916 user.setObjfifoAttr(target);
1917 }
1918 }
1919 return success();
1920 }
1921
1922 /// Give each dma_start naming an endpoint the channel it was assigned.
1923 LogicalResult resolveStart(DMAStartOp start, RouteEndpointOp endpoint,
1924 ArrayRef<RouteOp> flows, ArrayRef<int> ids,
1925 int channel) {
1926 DMAChannelDir dir = endpoint.getRouteDirection();
1927 if (start.getChannelDir() != dir)
1928 return start.emitOpError("starts ")
1929 << stringifyDMAChannelDir(start.getChannelDir()) << " on "
1930 << start.getEndpoint() << ", but its route makes it "
1931 << stringifyDMAChannelDir(dir);
1932 // Only a runtime task's BDs are given the packet header, so a DMA
1933 // program states it in each BD; --aie-assign-packet-ids made it pin one.
1934 bool packet = !flows.empty() && RouteOp(flows.front()).getPacket();
1935 if (packet) {
1936 SmallVector<DMABDOp> bds;
1937 for (Block *block : chainOf(start))
1938 llvm::append_range(bds, block->getOps<DMABDOp>());
1939 if (failed(checkHeaders(endpoint, ids, bds)))
1940 return failure();
1941 }
1942 start.setChannelAttr(builder.getI32IntegerAttr(channel));
1943 return success();
1944 }
1945
1946 /// A packet source that no one header is stamped on needs each of `bds` to
1947 /// state the header of one of its flows, whose pinned ids are `ids`.
1948 LogicalResult checkHeaders(RouteEndpointOp endpoint, ArrayRef<int> ids,
1949 ArrayRef<DMABDOp> bds) {
1950 for (DMABDOp bd : bds) {
1951 std::optional<int> id;
1952 if (auto packet = bd.getPacket())
1953 id = packet->getPktId();
1954 else if (auto ops = bd->getBlock()->getOps<DMABDPACKETOp>(); !ops.empty())
1955 id = (*ops.begin()).getPacketId();
1956 if (id && llvm::is_contained(ids, *id))
1957 continue;
1958 return bd.emitOpError("sends on @")
1959 << endpoint.getSymName()
1960 << " without the packet header of one of its flows, ids " << ids;
1961 }
1962 return success();
1963 }
1964
1965 /// Each runtime task or transfer that sends on a shared source must state
1966 /// one of its flows' headers; a DMA program's start is resolveStart's, and
1967 /// other users send nothing.
1968 LogicalResult checkSharedSourceUsers(RouteEndpointOp endpoint,
1969 ArrayRef<int> ids) {
1970 auto uses = SymbolTable::getSymbolUses(endpoint, device);
1971 if (!uses)
1972 return success();
1973 for (const SymbolTable::SymbolUse &use : *uses) {
1974 Operation *user = use.getUser();
1975 if (auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(user)) {
1976 SmallVector<DMABDOp> bds;
1977 task.getBody().walk([&](DMABDOp bd) { bds.push_back(bd); });
1978 if (failed(checkHeaders(endpoint, ids, bds)))
1979 return failure();
1980 continue;
1981 }
1982 if (auto start = dyn_cast<AIEX::DMAStartBdChainForOp>(user)) {
1983 auto chain = device.lookupSymbol<BDChainOp>(start.getSymbol());
1984 if (!chain)
1985 return start.emitOpError("symbol does not reference valid BD chain");
1986 SmallVector<DMABDOp> bds;
1987 chain.getBody().walk([&](DMABDOp bd) { bds.push_back(bd); });
1988 if (failed(checkHeaders(endpoint, ids, bds)))
1989 return failure();
1990 continue;
1991 }
1992 auto memcpy = dyn_cast<AIEX::NpuDmaMemcpyNdOp>(user);
1993 if (!memcpy)
1994 continue;
1995 std::optional<PacketInfoAttr> packet = memcpy.getPacket();
1996 if (packet && llvm::is_contained(ids, packet->getPktId()))
1997 continue;
1998 return memcpy.emitOpError("sends on @")
1999 << endpoint.getSymName()
2000 << ", a source several packet routes share, which has no one "
2001 "header to stamp; it needs a packet header of its own, one of "
2002 "ids "
2003 << ids;
2004 }
2005 return success();
2006 }
2007
2008 /// A runtime task naming an endpoint is configured on the endpoint's tile
2009 /// and channel directly.
2010 void resolveTask(AIEX::DMAConfigureTaskForOp task, RouteEndpointOp endpoint,
2011 int channel) {
2012 builder.setInsertionPoint(task);
2013 auto configured = AIEX::DMAConfigureTaskOp::create(
2014 builder, task.getLoc(), builder.getIndexType(), endpoint.getTile(),
2015 DMAChannelDirAttr::get(builder.getContext(),
2016 endpoint.getRouteDirection()),
2017 builder.getI32IntegerAttr(channel),
2018 builder.getBoolAttr(task.getIssueToken()),
2019 builder.getI32IntegerAttr(task.getRepeatCount()),
2020 task.getRepeatCountVal(), endpoint.getPacketAttr());
2021 task.getResult().replaceAllUsesWith(configured.getResult());
2022 configured.getBody().takeBody(task.getBody());
2023 task.erase();
2024 }
2025
2026 /// Rewrite the programs and tasks naming an endpoint to its channel while
2027 /// the route giving its direction is still there. A shim end with a fifo
2028 /// name keeps its tasks, which emitShimAllocations points at the allocation.
2029 LogicalResult resolveEndpointUses() {
2030 for (auto endpoint : device.getOps<RouteEndpointOp>()) {
2031 auto starts = endpointStarts.lookup(endpoint);
2032 auto tasks = endpointTasks.lookup(endpoint);
2033 auto sourced = sourcedFlows.find(endpoint.getSymNameAttr());
2034 ArrayRef<RouteOp> flows;
2035 if (sourced != sourcedFlows.end())
2036 flows = sourced->second;
2037 SmallVector<int> ids;
2038 for (RouteOp flow : flows)
2039 if (std::optional<PacketInfoAttr> packet = flow.getPacket();
2040 packet && packet->isAssigned())
2041 ids.push_back(packet->assignedId());
2042 bool shared = flows.size() > 1;
2043 if (shared && failed(checkSharedSourceUsers(endpoint, ids)))
2044 return failure();
2045 if (starts.empty() && tasks.empty())
2046 continue;
2047 std::optional<int> channel = endpoint.getRouteChannel();
2048 assert(channel && "assignChannels gives every routed endpoint one");
2049 for (DMAStartOp start : starts)
2050 if (failed(resolveStart(start, endpoint, flows, ids, *channel)))
2051 return failure();
2052 if (endpoint.getTileLike().isShimTile() && endpoint.getFifoName())
2053 continue;
2054 for (auto task : tasks)
2055 resolveTask(task, endpoint, *channel);
2056 }
2057 return success();
2058 }
2059
2060 /// A shim endpoint has no memory of its own, so the runtime needs its channel
2061 /// spelled out under the name the sequence refers to.
2062 void emitShimAllocations() {
2063 builder.setInsertionPoint(device.getBody()->getTerminator());
2064 // A fifo's first shim end takes the name the runtime sequence was pointed
2065 // at. Any further shim end of the same fifo, as when a core broadcasts to
2066 // two shim tiles, is a different tile and channel and needs a record of
2067 // its own; sharing the first would send the runtime to the wrong tile.
2068 llvm::StringMap<unsigned> shimEndsSeen;
2069 for (auto endpoint : device.getOps<RouteEndpoint>()) {
2070 std::optional<StringRef> fifoName = endpoint.getFifoName();
2071 if (!fifoName)
2072 if (auto named = dyn_cast<RouteEndpointOp>(endpoint.getOperation()))
2073 fifoName = named.getSymName();
2074 std::optional<int> channel = endpoint.getRouteChannel();
2075 if (!tileOf(endpoint).isShimTile() || !fifoName || !channel) {
2076 continue;
2077 }
2078 std::string name = (*fifoName + "__shim_alloc").str();
2079 unsigned ordinal = shimEndsSeen[*fifoName]++;
2080 bool create = true;
2081 if (ordinal == 0) {
2082 // A record under this name may already be there, declared by hand.
2083 create = !SymbolTable::lookupNearestSymbolFrom<ShimDMAAllocationOp>(
2084 device, builder.getStringAttr(name));
2085 } else {
2086 for (unsigned suffix = ordinal - 1; device.lookupSymbol(name);
2087 suffix++) {
2088 name =
2089 (*fifoName + "__shim_alloc" + std::to_string(suffix + 1)).str();
2090 }
2091 }
2092 if (create) {
2093 ShimDMAAllocationOp::create(
2094 builder, endpoint.getLoc(), builder.getStringAttr(name),
2095 endpoint.getTile(),
2096 DMAChannelDirAttr::get(builder.getContext(),
2097 endpoint.getRouteDirection()),
2098 builder.getI64IntegerAttr(*channel),
2099 builder.getBoolAttr(endpoint.getRouteBundle() == WireBundle::PLIO),
2100 endpoint->getAttrOfType<PacketInfoAttr>("packet"));
2101 }
2102 // The runtime sequence reaches the fifo through this record.
2103 (void)SymbolTable::replaceAllSymbolUses(
2104 endpoint, builder.getStringAttr(name), device);
2105 }
2106 }
2107
2108 /// Alternative packing order: prioritize DMA demand within each home tile,
2109 /// but merge those lists by object size to avoid globally prioritizing small
2110 /// pools over large objects on unrelated tiles. This is only a heuristic;
2111 /// the same buffer, lock and channel planner validates both orders.
2112 SmallVector<ObjectFifoPoolOp>
2113 demandOrderedPools(ArrayRef<ObjectFifoPoolOp> pools) {
2114 SmallVector<ObjectFifoPoolOp> ordered(pools);
2115 SmallVector<size_t> memTileSlots;
2116 SmallVector<Value> memTileHomes;
2117 DenseMap<Value, SmallVector<ObjectFifoPoolOp>> memTilePools;
2118 DenseMap<Operation *, std::pair<int, int>> poolChannelDemand;
2119 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
2120 ObjectFifoPoolOp pool = endpoint.getPoolOp();
2121 if (!sameTile(endpoint.getTile(), pool.getTile()))
2122 continue;
2123 auto &demand = poolChannelDemand[pool];
2124 if (endpoint.getRouteDirection() == DMAChannelDir::S2MM)
2125 ++demand.first;
2126 else
2127 ++demand.second;
2128 }
2129 for (auto [index, pool] : llvm::enumerate(ordered)) {
2130 if (pool.getTileLike().isMemTile()) {
2131 memTileSlots.push_back(index);
2132 Value home = pool.getTile();
2133 for (Value known : memTileHomes)
2134 if (sameTile(home, known)) {
2135 home = known;
2136 break;
2137 }
2138 if (!memTilePools.count(home))
2139 memTileHomes.push_back(home);
2140 memTilePools[home].push_back(pool);
2141 }
2142 }
2143 DenseMap<Value, size_t> nextPool;
2144 auto demand = [&](ObjectFifoPoolOp pool) {
2145 auto [input, output] = poolChannelDemand.lookup(pool.getOperation());
2146 return std::max(input, output);
2147 };
2148 for (Value home : memTileHomes) {
2149 auto &tilePools = memTilePools[home];
2150 llvm::stable_sort(tilePools, [&](ObjectFifoPoolOp a, ObjectFifoPoolOp b) {
2151 if (demand(a) != demand(b))
2152 return demand(a) > demand(b);
2153 return a.getObjectSizeInBytes() > b.getObjectSizeInBytes();
2154 });
2155 }
2156 for (size_t slot : memTileSlots) {
2157 Value bestHome;
2158 ObjectFifoPoolOp bestPool;
2159 for (Value home : memTileHomes) {
2160 auto &tilePools = memTilePools[home];
2161 size_t index = nextPool[home];
2162 if (index >= tilePools.size())
2163 continue;
2164 ObjectFifoPoolOp pool = tilePools[index];
2165 if (!bestPool ||
2166 pool.getObjectSizeInBytes() > bestPool.getObjectSizeInBytes() ||
2167 (pool.getObjectSizeInBytes() == bestPool.getObjectSizeInBytes() &&
2168 demand(pool) > demand(bestPool))) {
2169 bestHome = home;
2170 bestPool = pool;
2171 }
2172 }
2173 ordered[slot] = bestPool;
2174 ++nextPool[bestHome];
2175 }
2176 return ordered;
2177 }
2178
2179 void runOnOperation() override {
2180 device = getOperation();
2181 builder = OpBuilder(device.getContext());
2182 // One pass instance serves every device in the module, and none of this
2183 // state means anything outside the device it was gathered from.
2184 channelSharers.clear();
2185 allocationSharers.clear();
2186 lastPlaced.clear();
2187 loweredFlows.clear();
2188 sourcedFlows.clear();
2189 localPools.clear();
2190 poolUsers.clear();
2191 lockUsers.clear();
2192 lockPlacements.clear();
2193 spillTiles.clear();
2194 dispatchHeaders.clear();
2195 channelSharers.clear();
2196 plannedMemory.clear();
2197 bufferPlacements.clear();
2198 channelAssignments.clear();
2199 endpointStarts.clear();
2200 endpointTasks.clear();
2201 fixedBDs.clear();
2202 taskBDs.clear();
2203 channelDemand.clear();
2204 structuralRank.clear();
2205 channelFailure = nullptr;
2206 bufferFailure = nullptr;
2207 unplacedAtFailure = 0;
2208 lockFailure = nullptr;
2209 lockAccessFailure = nullptr;
2210
2211 if (failed(collectFixedMemory()) || failed(checkStreamPorts()))
2212 return signalPassFailure();
2213 collectChannelPrograms();
2214 rankStructurally();
2215 walkDeepPoolsWhole();
2216
2217 // Preserve successful largest-first allocations, including their locality
2218 // repairs. Only try demand ordering when that search fails.
2219 SmallVector<ObjectFifoPoolOp> pools(device.getOps<ObjectFifoPoolOp>());
2220 llvm::stable_sort(pools, [&](ObjectFifoPoolOp a, ObjectFifoPoolOp b) {
2221 return structuralRank.lookup(a) < structuralRank.lookup(b);
2222 });
2223 SmallVector<size_t> memTileSlots;
2224 SmallVector<ObjectFifoPoolOp> memTilePools;
2225 for (auto [index, pool] : llvm::enumerate(pools)) {
2226 if (pool.getTileLike().isMemTile()) {
2227 memTileSlots.push_back(index);
2228 memTilePools.push_back(pool);
2229 }
2230 }
2231 llvm::stable_sort(memTilePools, [](ObjectFifoPoolOp a, ObjectFifoPoolOp b) {
2232 return a.getObjectSizeInBytes() > b.getObjectSizeInBytes();
2233 });
2234 for (auto [slot, pool] : llvm::zip(memTileSlots, memTilePools)) {
2235 pools[slot] = pool;
2236 }
2237
2238 poolLocks = std::make_shared<PoolLocks>(device);
2239 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
2240 poolUsers[endpoint.getPoolOp()].push_back(endpoint.getTile());
2241 collectLockUsers(endpoint);
2242 }
2243 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
2244 poolUsers[endpoint.getPoolOp()].push_back(endpoint.getTile());
2245 collectLockUsers(endpoint);
2246 }
2247 for (auto &[group, users] : lockUsers)
2248 llvm::stable_sort(users, [&](Operation *a, Operation *b) {
2249 return structuralRank.lookup(a) < structuralRank.lookup(b);
2250 });
2251
2252 std::set<std::vector<unsigned>> tried;
2253 LogicalResult allocated = planAllocation(pools, tried);
2254 if (failed(allocated)) {
2255 auto demandOrdered = demandOrderedPools(pools);
2256 if (!llvm::equal(pools, demandOrdered)) {
2257 // Memoized locality sets are specific to a packing order.
2258 localPools.clear();
2259 tried.clear();
2260 allocated = planAllocation(demandOrdered, tried);
2261 }
2262 }
2263 // Out of channels, ends that never need theirs at once take turns on one,
2264 // a tile and direction at a time: on a shim tile, ends never in flight
2265 // together; elsewhere, ends whose objects one core needs in a fixed order.
2266 while (failed(allocated) && channelFailure &&
2267 (tileOf(channelFailure).isShimTile()
2268 ? planShimSharing(tileOf(channelFailure),
2269 channelFailure.getRouteDirection())
2270 : planTurnSharing(tileOf(channelFailure),
2271 channelFailure.getRouteDirection()))) {
2272 localPools.clear();
2273 tried.clear();
2274 allocated = planAllocation(pools, tried);
2275 }
2276 if (succeeded(allocated) &&
2277 (!channelSharers.empty() || !allocationSharers.empty()) &&
2278 failed(headShimSharingRoutes()))
2279 return signalPassFailure();
2280 if (failed(allocated)) {
2281 localPools.clear();
2282 if (failed(planBuffers(pools))) {
2283 bufferFailure.emitOpError(
2284 "could not place buffers in accessible memory with available "
2285 "capacity");
2286 } else if (failed(planLocks(pools))) {
2287 if (lockAccessFailure)
2288 lockAccessFailure->emitOpError("cannot access pool locks");
2289 else
2290 lockFailure.getDefiningOp()->emitOpError(
2291 "could not place locks within tile lock capacity");
2292 } else {
2293 DMAChannelAnalysis channels(device);
2294 (void)assignChannels(channels);
2295 device.emitRemark(
2296 "could not find a spill-aware allocation with size-first and "
2297 "demand-first packing and bounded local-pool retries; tile "
2298 "placement and buffer packing remain greedy");
2299 }
2300 return signalPassFailure();
2301 }
2302 for (ObjectFifoPoolOp pool : pools) {
2303 allocateBuffers(pool);
2304 allocateLocks(pool);
2305 }
2306 for (TileOp tile : spillTiles)
2307 if (tile->use_empty())
2308 tile->erase();
2309 for (auto endpoint : device.getOps<RouteEndpoint>())
2310 if (auto it = channelAssignments.find(endpoint.getOperation());
2311 it != channelAssignments.end())
2312 endpoint.setRouteChannel(it->second);
2313 warnOvercommittedBDs();
2314
2315 // Routes still identify each sender's receiving buffers here, but hardware
2316 // BD IDs are assigned later. Preserve that pairing with temporary tags
2317 // before lowering erases the routes.
2318 llvm::EquivalenceClasses<Operation *> matchingPools;
2319 DenseMap<Operation *, int64_t> descriptorCounts;
2320 for (auto flow : device.getOps<RouteOp>()) {
2321 if (flow.getSources().size() != 1)
2322 continue;
2323 ObjectFifoPoolOp first;
2324 for (auto destName : flow.getDestinationNames()) {
2325 RouteEndpoint receiver = lookupEndpoint(destName);
2326 auto dst =
2327 receiver
2328 ? dyn_cast<ObjectFifoDmaEndpointOp>(receiver.getOperation())
2329 : nullptr;
2330 if (!dst || !dst.getOutOfOrder())
2331 continue;
2332 ObjectFifoPoolOp pool = dst.getPoolOp();
2333 if (!first)
2334 first = pool;
2335 matchingPools.unionSets(first.getOperation(), pool.getOperation());
2336 descriptorCounts[pool] = dst.getNumDescriptors();
2337 }
2338 }
2339 int32_t nextTag = 0;
2340 DenseMap<Operation *, DenseI32ArrayAttr> tagsOfGroup;
2341 for (ObjectFifoPoolOp pool : pools) {
2342 auto member = matchingPools.findLeader(pool.getOperation());
2343 if (member == matchingPools.member_end())
2344 continue;
2345 Operation *leader = *member;
2346 auto &tags = tagsOfGroup[leader];
2347 if (!tags) {
2348 SmallVector<int32_t> values;
2349 for (int64_t slot = 0; slot < descriptorCounts.lookup(pool); ++slot)
2350 values.push_back(nextTag++);
2351 tags = builder.getDenseI32ArrayAttr(values);
2352 }
2353 pool.setOutOfOrderTagsAttr(tags);
2354 }
2355
2356 // A sender references the next hop's receiving slots, independently of
2357 // any receiving descriptors that fill its own pool.
2358 for (auto flow : device.getOps<RouteOp>()) {
2359 if (flow.getSources().size() != 1)
2360 continue;
2361 RouteEndpoint sender = lookupEndpoint(*flow.getSourceNames().begin());
2362 auto src = sender
2363 ? dyn_cast<ObjectFifoDmaEndpointOp>(sender.getOperation())
2364 : nullptr;
2365 if (!src || !src.getOutOfOrder())
2366 continue;
2367 for (auto destName : flow.getDestinationNames()) {
2368 RouteEndpoint receiver = lookupEndpoint(destName);
2369 auto dst =
2370 receiver
2371 ? dyn_cast<ObjectFifoDmaEndpointOp>(receiver.getOperation())
2372 : nullptr;
2373 if (dst && dst.getOutOfOrder()) {
2374 src.setOutOfOrderTagsAttr(dst.getPoolOp().getOutOfOrderTagsAttr());
2375 break;
2376 }
2377 }
2378 }
2379
2380 if (failed(bindRearmTargets())) {
2381 return signalPassFailure();
2382 }
2383 if (failed(lowerFlows()) || failed(resolveEndpointUses())) {
2384 return signalPassFailure();
2385 }
2386 emitShimAllocations();
2387 for (Operation *flow : loweredFlows) {
2388 flow->erase();
2389 }
2390 }
2391};
2392
2393} // namespace
2394
2395std::unique_ptr<OperationPass<DeviceOp>>
2397 return std::make_unique<AIEObjectFifoAllocatePass>();
2398}
virtual bool isBdChannelAccessible(int col, int row, uint32_t bd_id, int channel) const =0
Return true iff buffer descriptor bd_id on tile (col, row) can be submitted on channel channel.
virtual uint32_t getNumBDs(AIETileType tileType) const =0
Return the number of buffer descriptors for a given tile type.
virtual uint32_t getMaxLockValue() const =0
Return the maximum value that can be stored in a lock register.
uint32_t getNumBDsForChannel(int col, int row, int channel) const
Return the number of buffer descriptors accessible on channel channel for the tile at (col,...
SharedMemory
Which of two tiles' memory modules both of them can address.
mlir::LogicalResult read(llvm::ArrayRef< ObjectFifoCoreEndpointOp > ends, std::string &reason)
Reads the order off the program of the core working through ends, all draining (the channel fills the...
bool keepsUp(llvm::ArrayRef< int64_t > depths)
Whether a channel moving objects in turns() order forever, through pools of depths,...
llvm::ArrayRef< unsigned > turns() const
Indices into ends, one per object the channel moves in a period of the core's steady state.
int getDMAChannelIndex(TileLike tile, DMAChannelDir dir, bool requiresAdjacentTileAccessChannels, mlir::Operation *owner=nullptr)
Next free channel of tile in dir, or -1 when the tile has none left.
mlir::Operation * getDMAChannelOwner(TileLike tile, DMAChannelDir dir, int channel)
Operation reserving or routing this channel; null if none.
bool isChannelFree(TileLike tile, DMAChannelDir dir, int channel)
Whether first-free assignment could hand out channel: nothing reserves it and no explicit flow routes...
static int getDMAChannelLimit(TileLike tile, DMAChannelDir dir, bool requiresAdjacentTileAccessChannels)
Exclusive upper bound on channel indices eligible for this transfer.
mlir::LogicalResult checkAIEStreamIndex(TileLike tile, DMAChannel chan, mlir::Operation *user, bool packet=false)
Claim a raw stream port for user, reporting on it when the port is taken; packet users may share it w...
int reservePinnedChannel(TileLike tile, DMAChannelDir dir, int channel, mlir::Operation *owner=nullptr, bool shareable=false)
Claim channel for (tile, dir) so first-free assignment cannot take it.
std::optional< int > takeLowestFrom(int from=0)
Takes and returns the lowest free id at or above from, or nothing when the field is exhausted.
llvm::SmallVector< ObjectFifoDmaEndpointOp > turnEndpoints(ObjectFifoDmaEndpointOp end)
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEObjectFifoAllocatePass()
mlir::Operation * lookupNamedOpIn(mlir::Operation *symbolTableOp, mlir::StringAttr name)
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
int64_t turnPasses(llvm::ArrayRef< std::pair< int64_t, int64_t > > ends)
mlir::Operation * lookupNamedOp(mlir::Operation *from, mlir::StringAttr name)
int64_t assignSequentialBufferAddresses(llvm::MutableArrayRef< BufferAllocation > buffers, int64_t start=0)
The basic address allocator and resource planner share this layout: retain address pins,...
AIETargetModel::SharedMemory sharedMemory(TileOp a, TileOp b)
Which of a's and b's memory modules both tiles can address.
void link(mlir::Operation *from, mlir::Operation *to, llvm::StringRef label)
Link from to to under label, and to back to from.
llvm::SmallVector< mlir::Operation * > nodes
std::string & add(mlir::Operation *op)
Add op as a node whose features are its name and attributes, symbol names and references left out.
llvm::DenseMap< mlir::Operation *, unsigned > rank() const
Rank the nodes by their features and then, a round at a time until no class splits,...
llvm::DenseMap< mlir::Operation *, std::string > features
What each node is, before anything it links to is considered.