MLIR-AIE 1.4.5.dev
AIEAssignBuffers.cpp
Go to the documentation of this file.
1//===- AIEAssignBuffers.cpp -------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2019-2022 Xilinx, Inc.
4// Copyright (C) 2022-2026 Advanced Micro Devices, Inc.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
14
15#include "mlir/IR/Attributes.h"
16
17#include "llvm/ADT/BitVector.h"
18
19#include <limits>
20#include <optional>
21#include <set>
22
23namespace xilinx::AIE {
24#define GEN_PASS_DEF_AIEASSIGNBUFFERADDRESSES
25#define GEN_PASS_DEF_AIEPREPAREBUFFERS
26#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
27} // namespace xilinx::AIE
28
29#define DEBUG_TYPE "aie-assign-buffers"
30
31using namespace mlir;
32using namespace xilinx;
33using namespace xilinx::AIE;
34
35// Absent unless aiecc measured the core.
36static std::optional<int64_t> getMeasuredStackSize(TileOp tile) {
37 CoreOp core = tile.getCoreOp();
38 if (!core) {
39 return std::nullopt;
40 }
41 if (auto measured = core.getMeasuredStackSize()) {
42 return static_cast<int64_t>(*measured);
43 }
44 return std::nullopt;
45}
46
47// Every buffer must already have an address.
48static void sortBuffersByAddress(SmallVectorImpl<BufferOp> &buffers) {
49 llvm::sort(buffers, [](BufferOp a, BufferOp b) {
50 assert(a.getAddress().has_value() && "buffer must have address assigned");
51 assert(b.getAddress().has_value() && "buffer must have address assigned");
52 return a.getAddress().value() < b.getAddress().value();
53 });
54}
55
56// How a diagnostic names an extent. The allocator creates the core_data and
57// bank_reserved buffers itself, so their symbols appear in no user source and
58// naming them would send the reader looking for something they never wrote.
59static std::string bufferLabel(BufferOp buffer) {
60 if (buffer.getCoreData()) {
61 return "this core's data sections (data_size)";
62 }
63 if (buffer.getBankReserved()) {
64 return "this core's static data pinned to bank " +
65 std::to_string(buffer.getMemBank().value_or(0));
66 }
67 return ("buffer \"" + buffer.name().getValue() + "\"").str();
68}
69
70// The name a memory map prints for an extent.
71static StringRef mapLabel(BufferOp buffer) {
72 if (buffer.getCoreData()) {
73 return StringRef("(core data sections)");
74 }
75 if (buffer.getBankReserved()) {
76 return StringRef("(bank-pinned static data)");
77 }
78 return buffer.name().getValue();
79}
80
81// Give each core's `data_size` an aie.buffer, so placement treats the core's
82// own sections as one more extent to fit. Idempotent: this pass runs standalone
83// and inside larger pipelines.
84static void materializeCoreDataBuffers(DeviceOp device) {
85 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
86 for (auto core : llvm::to_vector(device.getOps<CoreOp>())) {
87 // A declared `data_size` still wins: it is a reservation the design asked
88 // for, and may deliberately exceed what this build happens to measure.
89 // Without one, the probe's measurement stands in, so the region is an
90 // extent placement fits rather than whatever space is left over.
91 int64_t size = core.getDataSize().value_or(
92 static_cast<int64_t>(core.getMeasuredDataSize().value_or(0)));
93 if (size <= 0) {
94 continue;
95 }
96 auto tile = cast<TileOp>(core.getTile().getDefiningOp());
97 bool present = false;
98 device.walk([&](BufferOp buffer) {
99 if (buffer.getCoreData() && buffer.getTileOp() == tile) {
100 present = true;
101 }
102 });
103 if (present) {
104 continue;
105 }
106 builder.setInsertionPoint(core);
107 auto type = MemRefType::get({size}, builder.getI8Type());
108 auto buffer =
109 BufferOp::create(builder, core.getLoc(), type, tile.getResult(),
110 /*sym_name=*/nullptr, /*address=*/nullptr,
111 /*initial_value=*/nullptr,
112 /*mem_bank=*/nullptr,
113 /*core_data=*/builder.getUnitAttr(),
114 /*bank_reserved=*/nullptr,
115 /*aligned=*/nullptr);
116 buffer->setAttr(SymbolTable::getSymbolAttrName(),
117 builder.getStringAttr("core_data_" +
118 std::to_string(tile.getCol()) + "_" +
119 std::to_string(tile.getRow())));
120 }
121}
122
123// Give each bank a core pins static data to an aie.buffer of its own, so the
124// room the linker will need there is held before unconstrained buffers take it.
125// Sizes come from `measured_bank_sizes`, which aiecc fills from a probe link.
126// Idempotent, like materializeCoreDataBuffers.
127static void materializeBankReservations(DeviceOp device) {
128 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
129 for (auto core : llvm::to_vector(device.getOps<CoreOp>())) {
130 auto sizes = core.getMeasuredBankSizes();
131 if (!sizes) {
132 continue;
133 }
134 auto tile = cast<TileOp>(core.getTile().getDefiningOp());
135 for (auto [bank, size] : llvm::enumerate(*sizes)) {
136 if (size <= 0) {
137 continue;
138 }
139 std::string name = "bank_reserved_" + std::to_string(tile.getCol()) +
140 "_" + std::to_string(tile.getRow()) + "_" +
141 std::to_string(bank);
142 bool present = false;
143 device.walk([&](BufferOp buffer) {
144 if (buffer.name() == name) {
145 present = true;
146 }
147 });
148 if (present) {
149 continue;
150 }
151 builder.setInsertionPoint(core);
152 auto type = MemRefType::get({size}, builder.getI8Type());
153 auto buffer =
154 BufferOp::create(builder, core.getLoc(), type, tile.getResult(),
155 /*sym_name=*/nullptr, /*address=*/nullptr,
156 /*initial_value=*/nullptr,
157 /*mem_bank=*/builder.getI32IntegerAttr(bank),
158 /*core_data=*/nullptr,
159 /*bank_reserved=*/builder.getUnitAttr(),
160 /*aligned=*/nullptr);
161 buffer->setAttr(SymbolTable::getSymbolAttrName(),
162 builder.getStringAttr(name));
163 }
164 }
165}
166
167// Give a prebaked `elf_file` core's occupied extents an aie.buffer each, so
168// placement keeps the tile's buffers clear of memory that ELF already holds.
169// aiecc fills `measured_data_ranges` by reading the ELF; without it the
170// allocator has no way to know a prebaked core owns anything at all.
171//
172// The same shape as materializeBankReservations, pinned by address rather than
173// by bank: the addresses in a prebaked image are already final. Idempotent.
174static void materializePrebakedRanges(DeviceOp device) {
175 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
176 for (auto core : llvm::to_vector(device.getOps<CoreOp>())) {
177 auto ranges = core.getMeasuredDataRanges();
178 if (!ranges) {
179 continue;
180 }
181 auto tile = cast<TileOp>(core.getTile().getDefiningOp());
182 for (size_t i = 0; i + 1 < ranges->size(); i += 2) {
183 int32_t address = (*ranges)[i], size = (*ranges)[i + 1];
184 if (size <= 0) {
185 continue;
186 }
187 std::string name = "prebaked_" + std::to_string(tile.getCol()) + "_" +
188 std::to_string(tile.getRow()) + "_" +
189 std::to_string(address);
190 bool present = false;
191 device.walk([&](BufferOp buffer) {
192 if (buffer.name() == name) {
193 present = true;
194 }
195 });
196 if (present) {
197 continue;
198 }
199 builder.setInsertionPoint(core);
200 auto type = MemRefType::get({size}, builder.getI8Type());
201 auto buffer =
202 BufferOp::create(builder, core.getLoc(), type, tile.getResult(),
203 /*sym_name=*/nullptr,
204 /*address=*/builder.getI32IntegerAttr(address),
205 /*initial_value=*/nullptr,
206 /*mem_bank=*/nullptr,
207 /*core_data=*/nullptr,
208 /*bank_reserved=*/builder.getUnitAttr(),
209 /*aligned=*/builder.getBoolAttr(false));
210 buffer->setAttr(SymbolTable::getSymbolAttrName(),
211 builder.getStringAttr(name));
212 }
213 }
214}
215
216// Peano models a bank as an address space (`aiebase_resources.h`: a..d are
217// 5..8, 9..14 are pairs) and schedules loads on the strength of the qualifier,
218// while nothing placed the buffer to match. Reading it here makes that
219// assumption true rather than merely asserted.
220static SmallVector<int, 2> banksFromMemorySpace(Type type) {
221 auto memref = dyn_cast<MemRefType>(type);
222 if (!memref) {
223 return {};
224 }
225 auto space = dyn_cast_or_null<IntegerAttr>(memref.getMemorySpace());
226 if (!space) {
227 return {};
228 }
229 int64_t value = space.getInt();
230 if (value >= 5 && value <= 8) {
231 return {static_cast<int>(value - 5)};
232 }
233 switch (value) {
234 case 9:
235 return {0, 1};
236 case 10:
237 return {0, 2};
238 case 11:
239 return {0, 3};
240 case 12:
241 return {1, 2};
242 case 13:
243 return {1, 3};
244 case 14:
245 return {2, 3};
246 default:
247 return {};
248 }
249}
250
251// Check a buffer's bank pin against its address space. A singleton space also
252// supplies the pin; pairs stay flexible until placement.
253//
254// The buffer carries the space rather than the kernel's declaration doing so
255// alone, because `func.call` already requires operand types to match the
256// callee's signature exactly. Agreement between the buffer and the kernel it is
257// passed to is therefore checked by the verifier, not here; all that is left is
258// to place the buffer where its type says it lives.
259static LogicalResult applySignatureBankConstraints(DeviceOp device) {
260 auto result = success();
261 device.walk([&](BufferOp buffer) {
262 auto banks = banksFromMemorySpace(buffer.getType());
263 if (banks.empty()) {
264 return;
265 }
266 if (auto existing = buffer.getMemBank();
267 existing && !llvm::is_contained(banks, *existing)) {
268 auto diag = buffer.emitOpError("has address space for bank ");
269 diag << banks.front();
270 if (banks.size() > 1)
271 diag << " or " << banks.back();
272 diag << " but is pinned to bank " << *existing
273 << ". These constraints disagree";
274 result = failure();
275 return;
276 }
277 if (banks.size() == 1)
278 buffer.setMemBankAttr(IntegerAttr::get(
279 IntegerType::get(device.getContext(), 32), banks.front()));
280 });
281 return result;
282}
283
284static bool isBufferPreAllocated(BufferOp buffer) {
285 auto addr = buffer.getAddress();
286 auto memBank = buffer.getMemBank();
287 return (addr != std::nullopt || memBank != std::nullopt);
288}
289
290static int64_t getMeasuredDataAlignBytes(BufferOp buffer) {
291 if (!buffer.getCoreData() && !buffer.getBankReserved())
292 return 1;
293 CoreOp core = buffer.getTileOp().getCoreOp();
294 if (!core)
295 return 1;
296 if (buffer.getCoreData())
297 return core.getMeasuredDataAlignment().value_or(1);
298 auto bank = buffer.getMemBank();
299 auto alignments = core.getMeasuredBankAlignments();
300 if (bank && alignments && *bank >= 0 &&
301 static_cast<size_t>(*bank) < alignments->size())
302 return std::max<int64_t>((*alignments)[*bank], 1);
303 return 1;
304}
305
306// Check alignment (when `aligned` is set) and that no two buffers overlap. The
307// input vector must be sorted by ascending address. Returns false and emits an
308// error on the first offending buffer; true otherwise.
309static bool checkAndPrintBufferOverlap(ArrayRef<BufferOp> sortedBuffers,
310 uint32_t tileAlignBitWidth,
311 uint32_t maxVecAlignBits) {
312 BufferOp prev = nullptr;
313 for (auto cur : sortedBuffers) {
314 auto curAddrOpt = cur.getAddress();
315 assert(curAddrOpt.has_value() && "buffer must have address assigned");
316 int64_t curAddr = *curAddrOpt;
317
318 // A pinned buffer must satisfy the bus width. The stricter vector
319 // requirement applies to the addresses this pass chooses.
320 uint32_t reqAlignBits =
321 isBufferPreAllocated(cur)
322 ? tileAlignBitWidth
323 : requiredBufferAlignBits(tileAlignBitWidth, maxVecAlignBits,
324 cur.getAllocationSize());
325 uint32_t alignByteWidth = reqAlignBits / 8;
326 if (cur.getAligned() && alignByteWidth != 0 &&
327 curAddr % alignByteWidth != 0) {
328 cur.emitOpError("") << bufferLabel(cur) << " at address 0x"
329 << llvm::utohexstr(curAddr)
330 << " is not aligned to the required " << reqAlignBits
331 << " bits";
332 return false;
333 }
334
335 // A zero-sized buffer covers no bytes; skip it to avoid a false overlap
336 // when it shares an address.
337 if (cur.getAllocationSize() == 0) {
338 continue;
339 }
340
341 if (prev) {
342 auto prevAddrOpt = prev.getAddress();
343 assert(prevAddrOpt.has_value() && "buffer must have address assigned");
344 int64_t prevAddr = *prevAddrOpt;
345 int64_t prevEnd = prevAddr + prev.getAllocationSize();
346 if (curAddr < prevEnd) {
347 cur.emitOpError("")
348 << bufferLabel(cur) << " at address 0x" << llvm::utohexstr(curAddr)
349 << " overlaps with " << bufferLabel(prev) << " at address 0x"
350 << llvm::utohexstr(prevAddr)
351 << " (size: " << prev.getAllocationSize() << " bytes)";
352 return false;
353 }
354 }
355 prev = cur;
356 }
357 return true;
358}
359
360static bool checkAndPrintOverlapStackframe(MemoryRun stackRun,
361 ArrayRef<BufferOp> buffers) {
362 for (auto buf : buffers) {
363 // A zero-sized buffer covers no bytes, so it cannot overlap the stack.
364 if (buf.getAllocationSize() == 0) {
365 continue;
366 }
367 auto bufAddrOpt = buf.getAddress();
368 assert(bufAddrOpt.has_value() && "buffer must have address assigned");
369 int64_t bufAddr = *bufAddrOpt;
370 int64_t bufEnd = bufAddr + buf.getAllocationSize();
371 if (bufAddr < stackRun.end() && stackRun.start < bufEnd) {
372 buf.emitOpError("") << bufferLabel(buf) << " at address 0x"
373 << llvm::utohexstr(bufAddr) << " overlaps the stack ("
374 << stackRun.size << " bytes at 0x"
375 << llvm::utohexstr(stackRun.start) << ")";
376 return false;
377 }
378 }
379 return true;
380}
381
382// One line of a memory-map diagnostic. Both diagnostics below call this, so
383// they format their lines identically.
384static void printMemoryMapEntry(Diagnostic &note, StringRef name,
385 int64_t address, int64_t size, int indent,
386 StringRef suffix = "") {
387 for (int i = 0; i < indent; ++i) {
388 note << "\t";
389 }
390 int64_t end = size == 0 ? address : address + size - 1;
391 note << name << " \t"
392 << ": 0x" << llvm::utohexstr(address) << "-0x" << llvm::utohexstr(end)
393 << " \t(" << size << " bytes)" << suffix << "\n";
394}
395
396//===----------------------------------------------------------------------===//
397// SimpleBankAwareAllocation : round-robin each alloc over available banks
398//===----------------------------------------------------------------------===//
399// Compute the extent of each bank.
400static void fillBankLimits(int numBanks, int64_t bankSize,
401 std::vector<MemoryRun> &bankLimits) {
402 for (int i = 0; i < numBanks; i++) {
403 bankLimits.push_back({bankSize * i, bankSize});
404 }
405}
406
407namespace {
408// Which bytes of one tile's data memory are taken. Byte-granular, because a
409// buffer marked `aligned = false` is packed at an unaligned offset.
410class MemoryOccupancy {
411public:
412 explicit MemoryOccupancy(int64_t size) : occupied(size, false) {}
413
414 int64_t size() const { return occupied.size(); }
415
416 // True when [start, end) lies inside the tile and no byte of it is taken.
417 bool isRangeFree(int64_t start, int64_t end) const {
418 if (start < 0 || end > size() || start > end) {
419 return false;
420 }
421 return start == end || occupied.find_first_in(start, end) == -1;
422 }
423
424 void markOccupied(int64_t start, int64_t end) {
425 assert(start >= 0 && end <= size() && start <= end &&
426 "range must lie inside the tile");
427 if (start < end) {
428 occupied.set(start, end);
429 }
430 }
431
432 // Undo a markOccupied. The caller lifts the core's data-region placeholder
433 // once every buffer is placed. No buffer lies inside the placeholder's range,
434 // so clearing exactly that range is exact.
435 void markFree(int64_t start, int64_t end) {
436 assert(start >= 0 && end <= size() && start <= end &&
437 "range must lie inside the tile");
438 if (start < end) {
439 occupied.reset(start, end);
440 }
441 }
442
443 // Placement for `size` bytes in [lo, hi) that leaves the largest single free
444 // run behind anywhere in [0, size()), or nullopt when nothing fits. An object
445 // placed before the unconstrained buffers -- the core's data region, a
446 // bank-pinned buffer -- keeps more contiguous space when it sits flush
447 // against occupied space. Only the two flush positions of each hole are
448 // candidates, because an interior placement leaves strictly less contiguous
449 // space.
450 std::optional<int64_t> findLeastFragmentingGap(int64_t lo, int64_t hi,
451 int64_t size,
452 int64_t alignBytes) const {
453 assert(alignBytes > 0 && "alignment must be positive");
454 std::optional<int64_t> best;
455 int64_t bestRun = -1;
456 auto consider = [&](int64_t start) {
457 MemoryOccupancy trial = *this;
458 trial.markOccupied(start, start + size);
459 int64_t run = trial.largestGap(0, this->size());
460 // Ties to the lowest address, for determinism.
461 if (run > bestRun || (run == bestRun && best && start < *best)) {
462 bestRun = run;
463 best = start;
464 }
465 };
466 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
467 int64_t low = llvm::alignTo(gapStart, alignBytes);
468 if (low + size > gapEnd) {
469 return;
470 }
471 consider(low);
472 // Flush to the top, aligned down but never below `low`.
473 consider(
474 std::max<int64_t>(llvm::alignDown(gapEnd - size, alignBytes), low));
475 });
476 return best;
477 }
478
479 // Start of the tightest gap in [lo, hi) that holds `size` bytes, or nullopt.
480 // Each candidate start is aligned up before the fit test, so a misaligned
481 // first free byte does not reject a gap.
482 std::optional<int64_t> findGap(int64_t lo, int64_t hi, int64_t size,
483 int64_t alignBytes) const {
484 assert(alignBytes > 0 && "alignment must be positive");
485 std::optional<int64_t> best;
486 int64_t bestSlack = 0;
487 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
488 int64_t start = llvm::alignTo(gapStart, alignBytes);
489 if (start + size <= gapEnd) {
490 // Waste measured across the whole gap, so front-alignment padding
491 // counts and a padded hole does not rank as a tight fit.
492 int64_t slack = (gapEnd - gapStart) - size;
493 if (!best || slack < bestSlack) {
494 best = start;
495 bestSlack = slack;
496 }
497 }
498 });
499 return best;
500 }
501
502 // Total free bytes in [lo, hi).
503 int64_t freeBytes(int64_t lo, int64_t hi) const {
504 int64_t total = 0;
505 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
506 total += gapEnd - gapStart;
507 });
508 return total;
509 }
510
511 // Bytes of the enclosing free run that `size` bytes at `addr` would leave
512 // unused; placement scoring prefers a tight fit to keep large runs whole.
513 int64_t slackAt(int64_t addr, int64_t size) const {
514 int64_t slack = 0;
515 forEachGap(0, this->size(), [&](int64_t gapStart, int64_t gapEnd) {
516 if (addr >= gapStart && addr + size <= gapEnd) {
517 slack = (gapEnd - gapStart) - size;
518 }
519 });
520 return slack;
521 }
522
523 // The free run enclosing `addr`, or a zero-length run if it is taken.
524 MemoryRun gapAt(int64_t addr) const {
525 MemoryRun found;
526 forEachGap(0, this->size(), [&](int64_t gapStart, int64_t gapEnd) {
527 if (addr >= gapStart && addr < gapEnd) {
528 found = {gapStart, gapEnd - gapStart};
529 }
530 });
531 return found;
532 }
533
534 // The two largest free runs by size, duplicates kept, so two equal runs give
535 // {n, n}. A placement consumes one run and leaves the other whole, which lets
536 // a candidate score the run it leaves behind in constant time.
537 std::pair<int64_t, int64_t> topTwoGaps(int64_t lo, int64_t hi) const {
538 int64_t first = 0, second = 0;
539 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
540 int64_t run = gapEnd - gapStart;
541 if (run > first) {
542 second = first;
543 first = run;
544 } else if (run > second) {
545 second = run;
546 }
547 });
548 return {first, second};
549 }
550
551 // Every maximal free run in [lo, hi), lowest first. The search enumerates
552 // candidate placements from these rather than taking the single best run.
553 SmallVector<MemoryRun> gapsIn(int64_t lo, int64_t hi) const {
554 SmallVector<MemoryRun> runs;
555 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
556 runs.push_back({gapStart, gapEnd - gapStart});
557 });
558 return runs;
559 }
560
561 // Largest single free run in [lo, hi). A reservation needs one contiguous
562 // run, which freeBytes does not measure.
563 int64_t largestGap(int64_t lo, int64_t hi) const {
564 int64_t best = 0;
565 forEachGap(lo, hi, [&](int64_t gapStart, int64_t gapEnd) {
566 best = std::max(best, gapEnd - gapStart);
567 });
568 return best;
569 }
570
571private:
572 // Calls fn(gapStart, gapEnd) for every maximal free run in [lo, hi).
573 template <typename Fn>
574 void forEachGap(int64_t lo, int64_t hi, Fn fn) const {
575 lo = std::max<int64_t>(lo, 0);
576 hi = std::min(hi, size());
577 for (int64_t cursor = lo; cursor < hi;) {
578 int gapStart = occupied.find_first_unset_in(cursor, hi);
579 if (gapStart == -1) {
580 break;
581 }
582 int nextTaken = occupied.find_first_in(gapStart, hi);
583 // gapStart is clear, so gapEnd > gapStart >= cursor: the cursor advances.
584 int64_t gapEnd = nextTaken == -1 ? hi : nextTaken;
585 fn(gapStart, gapEnd);
586 cursor = gapEnd;
587 }
588 }
589
590 llvm::BitVector occupied;
591};
592} // namespace
593
594// Alignment a buffer must be placed at, in bytes.
595static int64_t getBufferAlignBytes(BufferOp buffer, uint32_t tileAlignBitWidth,
596 uint32_t maxVecAlignBits) {
597 // Linker section alignment still applies when vector alignment is disabled.
598 int64_t measuredAlign = getMeasuredDataAlignBytes(buffer);
599 if (!buffer.getAligned()) {
600 return measuredAlign;
601 }
602 return std::max<int64_t>(requiredBufferAlignBits(tileAlignBitWidth,
603 maxVecAlignBits,
604 buffer.getAllocationSize()) /
605 8,
606 measuredAlign);
607}
608
609// Index of the bank owning `addr`, or -1 when it falls outside every bank.
610static int getBankContaining(int64_t addr, int numBanks,
611 ArrayRef<MemoryRun> bankLimits) {
612 for (int i = 0; i < numBanks; i++) {
613 if (bankLimits[i].contains(addr)) {
614 return i;
615 }
616 }
617 return -1;
618}
619
620// Sets the buffer's address and mem_bank attributes and marks the bytes it
621// covers as taken.
622static void placeBuffer(BufferOp buffer, int64_t startAddr, int bank,
623 MemoryOccupancy &occupancy) {
624 buffer.setAddress(startAddr);
625 buffer.setMemBank(bank);
626 occupancy.markOccupied(startAddr, startAddr + buffer.getAllocationSize());
627}
628
629// Tile-level constants shared by every bank-aware helper below.
634 ArrayRef<MemoryRun> bankLimits;
637 // Whether this tile has a core, and so compiled sections that need a
638 // contiguous region. A memtile has none.
640 // Bytes at the start of the tile an initialized buffer must not cover; see
641 // allocateTile. Other buffers may still use them.
642 int64_t initGuardBytes = 0;
643};
644
645// Places a buffer carrying an explicit `address`, and checks that the space is
646// free and that any `mem_bank` agrees. Returns false when the buffer has no
647// address, which leaves it to the mem_bank or free-placement path. Failure is
648// terminal: this emits an error and the caller must not retry another scheme
649static FailureOr<bool>
650checkAndAddBufferWithAddress(BufferOp buffer, const BankAwareContext &ctx,
651 MemoryOccupancy &occupancy) {
652 auto addrOpt = buffer.getAddress();
653 if (!addrOpt) {
654 return false;
655 }
656
657 int64_t addr = *addrOpt;
658 int64_t measuredAlign = getMeasuredDataAlignBytes(buffer);
659 if (addr % measuredAlign != 0)
660 return buffer.emitOpError("data reservation address must be aligned to ")
661 << measuredAlign
662 << " bytes to satisfy its measured section alignment";
663
664 // A pinned address must satisfy the bus width. The stricter vector alignment
665 // applies to the addresses this pass chooses.
666 if (buffer.getAligned() && addr % (ctx.tileAlignBitWidth / 8) != 0) {
667 return buffer->emitOpError("address attribute value must be aligned to ")
668 << ctx.tileAlignBitWidth
669 << " bits when the aligned attribute is set";
670 }
671
672 int bank = getBankContaining(addr, ctx.numBanks, ctx.bankLimits);
673 if (bank < 0) {
674 // A zero-sized buffer pinned one past the last bank is legal. Assign it the
675 // last bank, where nothing can conflict with it.
676 if (buffer.getAllocationSize() == 0 &&
677 addr == ctx.bankLimits.back().end()) {
678 bank = ctx.numBanks - 1;
679 } else {
680 return buffer->emitOpError(
681 "address attribute does not fall within any bank range");
682 }
683 }
684
685 int64_t endAddr = addr + buffer.getAllocationSize();
686 if (endAddr > occupancy.size()) {
687 return buffer->emitOpError("address attribute would place the buffer past "
688 "the end of the tile's memory");
689 }
690
691 auto banks = banksFromMemorySpace(buffer.getType());
692 int lastBank =
693 buffer.getAllocationSize() == 0
694 ? bank
695 : getBankContaining(endAddr - 1, ctx.numBanks, ctx.bankLimits);
696 for (int touched = bank; !banks.empty() && touched <= lastBank; ++touched) {
697 if (!llvm::is_contained(banks, touched))
698 return buffer.emitOpError("address attribute places the buffer in bank ")
699 << touched << ", which is excluded by its memory space";
700 }
701 if (auto requested = buffer.getMemBank(); requested && *requested != bank)
702 return buffer.emitOpError("address attribute lies in bank ")
703 << bank << ", but mem_bank requests bank " << *requested;
704
705 // Only a real collision is invalid. The hardware lets a buffer straddle a
706 // bank boundary, so a pinned buffer may extend past `bank`.
707 if (!occupancy.isRangeFree(addr, endAddr)) {
708 return buffer->emitOpError("would override allocated address");
709 }
710
711 placeBuffer(buffer, addr, bank, occupancy);
712 return true;
713}
714
715// Banks a buffer may live in because the user requested them. Held separately
716// from the `mem_bank` attribute, which the allocator writes for the buffers it
717// places and clears when it rolls back.
718using RequiredBanks = DenseMap<Operation *, SmallVector<int, 2>>;
719
720// Record the allowed banks, intersected with any explicit mem_bank request.
721// applySignatureBankConstraints has already checked that the intersection is
722// nonempty.
723static LogicalResult recordRequiredBank(BufferOp buffer, int numBanks,
724 RequiredBanks &requiredBanks) {
725 auto memBankOpt = buffer.getMemBank();
726 if (memBankOpt && (*memBankOpt < 0 || *memBankOpt >= numBanks)) {
727 return buffer->emitOpError("mem_bank attribute value is out of range");
728 }
729 auto banks = banksFromMemorySpace(buffer.getType());
730 if (memBankOpt)
731 banks = {*memBankOpt};
732 if (!banks.empty()) {
733 if (banks.back() >= numBanks)
734 return buffer.emitOpError(
735 "memory space requests a bank that does not exist");
736 requiredBanks[buffer] = std::move(banks);
737 }
738 return success();
739}
740
741static void printMemMap(TileOp tile, ArrayRef<BufferOp> allocatedBuffers,
742 ArrayRef<BufferOp> preAllocatedBuffers,
743 const BankAwareContext &ctx) {
744 InFlightDiagnostic error = tile.emitWarning(
745 "Not all requested buffers fit in the available memory.\n");
746 auto &note = error.attachNote()
747 << "Current configuration of buffers in bank(s) : ";
748 note << "MemoryMap:\n";
749 auto printbuffer = [&](StringRef name, int64_t address, int64_t size,
750 StringRef suffix = "") {
751 printMemoryMapEntry(note, name, address, size, /*indent=*/2, suffix);
752 };
753 if (ctx.stackRun.size == 0) {
754 note << "\t(no stack allocated)\n";
755 }
756 if (auto measured = getMeasuredStackSize(tile)) {
757 note << "\t(aiecc measured this core's stack requirement as " << *measured
758 << " bytes)\n";
759 }
760 for (int i = 0; i < ctx.numBanks; i++) {
761 note << "\t" << "bank : " << i << "\t" << "0x"
762 << llvm::utohexstr(ctx.bankLimits[i].start) << "-0x"
763 << llvm::utohexstr(ctx.bankLimits[i].end() - 1) << "\n";
764 // Under whichever bank holds it, rather than always under bank 0.
765 if (ctx.stackRun.size > 0 &&
766 ctx.bankLimits[i].contains(ctx.stackRun.start)) {
767 printbuffer("(stack)", ctx.stackRun.start, ctx.stackRun.size);
768 }
769 // This runs on the failure path, where some buffers have no address yet.
770 // A mem_bank-only buffer appears in both lists -- it is pre-allocated in
771 // the sense that its bank was given, but this pass still chose its address
772 // -- so listing it once takes a filter.
773 SmallPtrSet<Operation *, 8> listed;
774 auto printPlaced = [&](ArrayRef<BufferOp> buffers) {
775 for (auto buffer : buffers) {
776 auto addrOpt = buffer.getAddress();
777 auto memBankOpt = buffer.getMemBank();
778 if (!addrOpt || !memBankOpt || *memBankOpt != i ||
779 !listed.insert(buffer).second) {
780 continue;
781 }
782 int64_t size = buffer.getAllocationSize();
783 // Listed under its start bank. A buffer too big for one bank straddles,
784 // so its range can extend past the bank printed above it.
785 std::string suffix;
786 if (*addrOpt + size > ctx.bankLimits[i].end()) {
787 suffix = (" (straddles into bank " + llvm::Twine(i + 1) + ")").str();
788 }
789 printbuffer(mapLabel(buffer), *addrOpt, size, suffix);
790 }
791 };
792 printPlaced(preAllocatedBuffers);
793 printPlaced(allocatedBuffers);
794 }
795}
796
797namespace {
798// Sentinel first key for an unscored fallback, so it sorts behind every
799// scored candidate however those score.
800constexpr int64_t kFallbackRank = std::numeric_limits<int64_t>::max();
801
802// Candidate address ordering, lower ranks first:
803// 1. banks touched, fewest first (each spanned bank costs DMA bandwidth);
804// 2. largest free run left behind, biggest first, capped by contiguityCap --
805// the bytes still to place. A run wider than that serves nothing, so past
806// the cap every candidate ties and criterion 3 takes over;
807// 3. round-robin distance from the cursor, nearest first (spreads for DMA);
808// 4. slack, tightest first, which leaves the large runs unbroken;
809// 5. address, lowest first, for determinism.
810// A memtile has no core, so criterion 2 is neutralized there and round-robin
811// governs throughout.
812struct Placement {
813 int64_t addr;
814 int bank;
815 int64_t banksTouched;
816 int64_t negLargestRunLeft;
817 int64_t rrDistance;
818 int64_t slack;
819
820 std::tuple<int64_t, int64_t, int64_t, int64_t, int64_t> rank() const {
821 return {banksTouched, negLargestRunLeft, rrDistance, slack, addr};
822 }
823};
824} // namespace
825
826// Every address `buffer` could take in the current occupancy, best first.
827//
828// The head of this list is the address a single-pass greedy would have chosen,
829// so a design that placed before places identically; the tail is where the
830// search goes when a later buffer is left with nowhere to sit.
831static SmallVector<Placement>
832rankedPlacements(BufferOp buffer, const BankAwareContext &ctx,
833 int startBankIndex, const RequiredBanks &requiredBanks,
834 const MemoryOccupancy &tileOccupancy, int64_t contiguityCap) {
835 assert(startBankIndex < ctx.numBanks &&
836 "Unexpected input value for startBankIndex");
837 // An initialized buffer sees the guarded bytes as taken, so none of its
838 // candidates cover them. Every other buffer may still be placed there.
839 std::optional<MemoryOccupancy> guarded;
840 if (ctx.initGuardBytes > 0 && buffer.getInitialValue().has_value()) {
841 guarded = tileOccupancy;
842 guarded->markOccupied(0, std::min(ctx.initGuardBytes, guarded->size()));
843 }
844 const MemoryOccupancy &occupancy = guarded ? *guarded : tileOccupancy;
845 int64_t size = buffer.getAllocationSize();
846 int64_t alignBytes =
847 getBufferAlignBytes(buffer, ctx.tileAlignBitWidth, ctx.maxVecAlignBits);
848 SmallVector<Placement> out;
849
850 // A requested bank set is a hard constraint, and the round-robin cursor stays
851 // put. Only the two flush ends of each hole are worth trying, because an
852 // interior start leaves strictly less behind.
853 auto required = requiredBanks.find(buffer);
854 if (required != requiredBanks.end()) {
855 auto &banks = required->second;
856 SmallVector<MemoryRun> ranges;
857 for (int bank : banks)
858 ranges.push_back(ctx.bankLimits[bank]);
859 // Adjacent allowed banks can hold one spanning buffer; a disjoint pair
860 // cannot use the intervening, forbidden bank.
861 if (banks.size() == 2 && banks.back() == banks.front() + 1)
862 ranges.push_back({ctx.bankLimits[banks.front()].start,
863 ctx.bankLimits[banks.front()].size +
864 ctx.bankLimits[banks.back()].size});
865 for (MemoryRun limits : ranges) {
866 for (MemoryRun gap : occupancy.gapsIn(limits.start, limits.end())) {
867 int64_t low = llvm::alignTo(gap.start, alignBytes);
868 if (low + size > gap.end()) {
869 continue;
870 }
871 for (int64_t addr :
872 {low, std::max<int64_t>(
873 llvm::alignDown(gap.end() - size, alignBytes), low)}) {
874 MemoryOccupancy trial = occupancy;
875 trial.markOccupied(addr, addr + size);
876 int bank = getBankContaining(addr, ctx.numBanks, ctx.bankLimits);
877 if (!llvm::is_contained(banks, bank))
878 continue;
879 int lastBank = size == 0
880 ? bank
881 : getBankContaining(addr + size - 1, ctx.numBanks,
882 ctx.bankLimits);
883 out.push_back({addr, bank, /*banksTouched=*/lastBank - bank + 1,
884 -trial.largestGap(0, trial.size()), /*rrDistance=*/0,
885 /*slack=*/0});
886 }
887 }
888 }
889 // A zero-sized buffer needs no free byte, so every bank holds it.
890 if (out.empty() && size == 0) {
891 int bank = banks.front();
892 out.push_back({ctx.bankLimits[bank].start, bank, 1, 0, 0, 0});
893 }
894 } else {
895 int64_t bankedEnd = ctx.bankLimits.back().end();
896 int64_t bankSize = bankedEnd / ctx.numBanks;
897 // Computed once per buffer, not per candidate: every candidate splits one
898 // run, so the largest run left elsewhere is one of these two. Named
899 // separately, because C++17 cannot capture a structured binding in the
900 // lambda below.
901 std::pair<int64_t, int64_t> topGaps =
902 occupancy.topTwoGaps(0, ctx.maxDataMemorySize);
903 int64_t largestRun = topGaps.first, secondRun = topGaps.second;
904 auto consider = [&](std::optional<int64_t> addr) {
905 if (!addr) {
906 return;
907 }
908 int bank = getBankContaining(*addr, ctx.numBanks, ctx.bankLimits);
909 if (bank < 0) {
910 return;
911 }
912 int64_t touched =
913 size == 0 ? 1 : (*addr + size - 1) / bankSize - *addr / bankSize + 1;
914 // This placement splits one run into the piece below and the piece above.
915 // Every other run stays whole, so the biggest of those is whichever of
916 // the top two this candidate does not consume.
917 MemoryRun gap = occupancy.gapAt(*addr);
918 int64_t elsewhere = gap.size == largestRun ? secondRun : largestRun;
919 int64_t leftPiece = *addr - gap.start;
920 int64_t rightPiece = gap.end() - (*addr + size);
921 int64_t runLeft =
922 std::min(std::max({elsewhere, leftPiece, rightPiece}), contiguityCap);
923 out.push_back({*addr, bank, touched, ctx.hasCore ? -runLeft : 0,
924 (bank - startBankIndex + ctx.numBanks) % ctx.numBanks,
925 occupancy.slackAt(*addr, size)});
926 };
927
928 for (int i = 0; i < ctx.numBanks; i++) {
929 consider(occupancy.findGap(ctx.bankLimits[i].start,
930 ctx.bankLimits[i].end(), size, alignBytes));
931 }
932 // Allow straddling when nothing fits one bank. The banked region maps to a
933 // bank, so the result always has one.
934 consider(occupancy.findGap(0, bankedEnd, size, alignBytes));
935 // Try bank-boundary starts only when a boundary also satisfies the buffer's
936 // own alignment, because otherwise this searches on an unintended stride.
937 if (bankSize % alignBytes == 0) {
938 consider(occupancy.findGap(0, bankedEnd, size, bankSize));
939 }
940 // A zero-sized buffer needs no free byte, so a full tile still holds it.
941 if (out.empty() && size == 0) {
942 out.push_back({0, 0, 1, 0, 0, 0});
943 }
944 }
945
946 llvm::stable_sort(out, [](const Placement &a, const Placement &b) {
947 return a.rank() < b.rank();
948 });
949
950 // The probes above ask each hole only for its tightest fit, so a layout
951 // needing some other position in one is unreachable -- and unreachable reads
952 // as "no room". Both flush ends of every hole cover that. Appended after the
953 // sort, never scored: ranking them would move addresses that already work.
954 // Unpinned only; the mem_bank branch above already does this within its bank,
955 // and widening it here would place the buffer outside that bank.
956 if (size > 0 && requiredBanks.find(buffer) == requiredBanks.end()) {
957 for (MemoryRun gap : occupancy.gapsIn(0, ctx.maxDataMemorySize)) {
958 int64_t low = llvm::alignTo(gap.start, alignBytes);
959 if (low + size > gap.end()) {
960 continue;
961 }
962 for (int64_t addr :
963 {low, std::max<int64_t>(
964 llvm::alignDown(gap.end() - size, alignBytes), low)}) {
965 int bank = getBankContaining(addr, ctx.numBanks, ctx.bankLimits);
966 if (bank >= 0) {
967 // Ranks last by construction, so it is only ever reached by
968 // backtracking past every scored candidate.
969 out.push_back({addr, bank, kFallbackRank, 0, 0, 0});
970 }
971 }
972 }
973 }
974
975 // The probes overlap: a gap that is the tightest in its bank is often also
976 // the tightest overall, and the fallbacks repeat whichever of those was a
977 // flush end. Equal addresses are the same placement, and trying one twice
978 // only costs budget. Stable, so the ranked entry survives its duplicate.
979 llvm::stable_sort(out, [](const Placement &a, const Placement &b) {
980 return a.addr < b.addr;
981 });
982 out.erase(
983 llvm::unique(out, [](const Placement &a,
984 const Placement &b) { return a.addr == b.addr; }),
985 out.end());
986 llvm::stable_sort(out, [](const Placement &a, const Placement &b) {
987 return a.rank() < b.rank();
988 });
989 return out;
990}
991
992namespace {
993// What the search cost on one device, for --mlir-pass-statistics.
994struct PlacementStats {
995 int64_t backtracks = 0; // placements undone because a later buffer had none
996 int64_t tilesSearched = 0; // tiles needing more than the ranked first try
997 int64_t budgetExhausted = 0;
998 int64_t tilesCompacted = 0; // placed only by the exhaustive search
999};
1000} // namespace
1001
1002// Depth-first placement over `order`, trying each buffer's addresses in rank
1003// order and undoing a choice that leaves a later buffer nowhere to go.
1004//
1005// The first descent is exactly what a single-pass greedy produces, so this only
1006// does extra work where that pass used to fail outright. On failure `deepest`
1007// names the buffer that ran out of addresses furthest into the order, which is
1008// the one whose demand the tile could not meet.
1009namespace {
1010struct PlacementSearch {
1011 ArrayRef<BufferOp> order;
1012 const BankAwareContext &ctx;
1013 const RequiredBanks &requiredBanks;
1014 MemoryOccupancy &occupancy;
1015 SmallVectorImpl<BufferOp> &placed;
1016 PlacementStats &stats;
1017 // How many placements the search may still try on this tile before giving
1018 // up and reporting the deepest failure it reached; the pass's
1019 // placement-budget option. Packing around fixed obstacles is NP-hard, so the
1020 // tree has no useful worst-case bound; this keeps a design the search cannot
1021 // solve to bounded compile time instead of exponential. A node count rather
1022 // than a time limit, so a build stays reproducible.
1023 int64_t budget;
1024 // Set when the node budget ran out with candidates still untried, so the
1025 // caller can say "gave up" rather than "no such layout exists".
1026 bool exhausted = false;
1027
1028 // The furthest the search reached, kept so a failure can report the layout it
1029 // got closest with rather than the empty tile backtracking leaves behind.
1030 // First arrangement to reach a given depth wins, not the last: the first
1031 // comes down the highest-ranked path, so the map a user sees is the one the
1032 // ranking preferred rather than whichever branch happened to be explored
1033 // last.
1034 size_t deepestIndex = 0;
1035 BufferOp deepest = nullptr;
1036 SmallVector<std::pair<BufferOp, Placement>> deepestLayout = {};
1037
1038 // Subproblems already searched in full without a layout, keyed by the next
1039 // buffer's index and the free runs left. The candidates a buffer gets depend
1040 // on nothing else -- the cursor only orders them -- so one of these can never
1041 // succeed. Same-sized buffers reach the same free runs in every order they
1042 // are placed in; without this the search retries each order.
1043 std::set<SmallVector<int64_t>> failedStates = {};
1044
1045 bool run(size_t index, int startBankIndex, int64_t remaining) {
1046 if (index == order.size()) {
1047 return true;
1048 }
1049 SmallVector<int64_t> state = {static_cast<int64_t>(index)};
1050 for (MemoryRun gap : occupancy.gapsIn(0, occupancy.size())) {
1051 state.append({gap.start, gap.size});
1052 }
1053 if (failedStates.count(state)) {
1054 return false;
1055 }
1056 BufferOp buffer = order[index];
1057 int64_t size = buffer.getAllocationSize();
1058 int64_t rest = remaining - size; // Placement's criterion 2 cap.
1059
1060 SmallVector<Placement> candidates = rankedPlacements(
1061 buffer, ctx, startBankIndex, requiredBanks, occupancy, rest);
1062 if (candidates.empty() && (!deepest || index > deepestIndex)) {
1063 recordDeepest(index, buffer);
1064 }
1065 for (const Placement &candidate : candidates) {
1066 if (budget <= 0) {
1067 // Out of budget before this buffer had an address: it is where the
1068 // search stopped, so it is the one to report.
1069 if (!deepest || index > deepestIndex) {
1070 recordDeepest(index, buffer);
1071 }
1072 exhausted = true;
1073 break;
1074 }
1075 --budget;
1076 placeBuffer(buffer, candidate.addr, candidate.bank, occupancy);
1077 placed.push_back(buffer);
1078 // A pinned buffer leaves the cursor alone; see rankedPlacements.
1079 int nextBank = requiredBanks.count(buffer)
1080 ? startBankIndex
1081 : (candidate.bank + 1) % ctx.numBanks;
1082 if (run(index + 1, nextBank, rest)) {
1083 return true;
1084 }
1085 placed.pop_back();
1086 ++stats.backtracks;
1087 occupancy.markFree(candidate.addr, candidate.addr + size);
1088 buffer->removeAttr("address");
1089 auto required = requiredBanks.find(buffer);
1090 if (required == requiredBanks.end() || required->second.size() != 1) {
1091 buffer->removeAttr("mem_bank");
1092 } else {
1093 buffer.setMemBank(required->second.front());
1094 }
1095 }
1096 if (!exhausted) {
1097 failedStates.insert(std::move(state));
1098 }
1099 return false;
1100 }
1101
1102 // Re-apply the best partial layout so the caller's memory map describes the
1103 // closest the search came, not the tile it unwound to.
1104 void restoreDeepest() {
1105 for (auto &[buffer, at] : deepestLayout) {
1106 placeBuffer(buffer, at.addr, at.bank, occupancy);
1107 placed.push_back(buffer);
1108 }
1109 }
1110
1111private:
1112 void recordDeepest(size_t index, BufferOp buffer) {
1113 deepestIndex = index;
1114 deepest = buffer;
1115 deepestLayout.clear();
1116 for (BufferOp done : placed) {
1117 auto address = done.getAddress();
1118 auto bank = done.getMemBank();
1119 assert(address && bank && "placed buffer must have an address and bank");
1120 deepestLayout.push_back({done, Placement{*address, *bank, 0, 0, 0, 0}});
1121 }
1122 }
1123};
1124} // namespace
1125
1126// Exhaustive placement, for when the ranked search fails. The ranked search
1127// only tries positions flush against what is already placed, so a buffer that
1128// must sit against one placed after it is out of its reach in any order.
1129//
1130// Any layout can be slid down, one buffer at a time in address order, until
1131// each buffer sits at the lowest address its alignment and banks allow above
1132// the one before it. Walking the free runs upward and choosing which buffer
1133// comes next therefore reaches every layout that exists, and a tile this
1134// rejects within budget has none. What can follow an address depends only on
1135// the address and the buffers left, so a failed pair is never tried twice,
1136// which collapses the orders of one set of buffers into a single visit.
1137namespace {
1138struct CompactionSearch {
1139 ArrayRef<BufferOp> order;
1140 const BankAwareContext &ctx;
1141 SmallVector<MemoryRun> gaps;
1142 // Per buffer in `order`: size, alignment, the ranges it may lie in, and a
1143 // class shared with every buffer it could swap with.
1144 SmallVector<int64_t> sizes, aligns;
1145 SmallVector<SmallVector<MemoryRun, 2>> ranges;
1146 SmallVector<unsigned> classes;
1147 SmallVector<int64_t> addrs;
1148 // Free bytes and the largest run from each gap to the top of the tile.
1149 SmallVector<int64_t> freeAbove, runAbove;
1150 llvm::DenseSet<std::pair<int64_t, llvm::BitVector>> failed;
1151 int64_t budget;
1152 bool exhausted = false;
1153
1154 CompactionSearch(ArrayRef<BufferOp> order, const BankAwareContext &ctx,
1155 const RequiredBanks &requiredBanks,
1156 const MemoryOccupancy &occupancy, int64_t budget)
1157 : order(order), ctx(ctx),
1158 gaps(occupancy.gapsIn(0, ctx.maxDataMemorySize)),
1159 addrs(order.size(), -1), budget(budget) {
1160 for (size_t i = 0; i < order.size(); ++i) {
1161 BufferOp buffer = order[i];
1162 sizes.push_back(buffer.getAllocationSize());
1163 aligns.push_back(getBufferAlignBytes(buffer, ctx.tileAlignBitWidth,
1164 ctx.maxVecAlignBits));
1165 SmallVector<MemoryRun, 2> allowed;
1166 auto required = requiredBanks.find(buffer);
1167 if (required == requiredBanks.end()) {
1168 allowed.push_back({0, ctx.maxDataMemorySize});
1169 } else {
1170 auto &banks = required->second;
1171 for (int bank : banks)
1172 allowed.push_back(ctx.bankLimits[bank]);
1173 if (banks.size() == 2 && banks.back() == banks.front() + 1)
1174 allowed = {{ctx.bankLimits[banks.front()].start,
1175 ctx.bankLimits[banks.front()].size +
1176 ctx.bankLimits[banks.back()].size}};
1177 }
1178 // An initialized buffer must dodge the guarded bytes at the foot of
1179 // the tile, the same restriction rankedPlacements enforces (see the
1180 // npu2 column-0 memtile comment in allocateTile). Clamping the ranges
1181 // here, before the equivalence classes below are computed, keeps a
1182 // guarded buffer out of any class an unguarded one of the same size
1183 // could also join.
1184 if (ctx.initGuardBytes > 0 && buffer.getInitialValue().has_value()) {
1185 SmallVector<MemoryRun, 2> clamped;
1186 for (MemoryRun range : allowed) {
1187 int64_t start = std::max(range.start, ctx.initGuardBytes);
1188 if (start < range.end())
1189 clamped.push_back({start, range.end() - start});
1190 }
1191 allowed = std::move(clamped);
1192 }
1193 ranges.push_back(allowed);
1194 unsigned cls = i;
1195 for (size_t j = 0; j < i; ++j) {
1196 if (sizes[j] == sizes[i] && aligns[j] == aligns[i] &&
1197 llvm::equal(ranges[j], ranges[i], [](MemoryRun a, MemoryRun b) {
1198 return a.start == b.start && a.size == b.size;
1199 })) {
1200 cls = classes[j];
1201 break;
1202 }
1203 }
1204 classes.push_back(cls);
1205 }
1206 freeAbove.assign(gaps.size() + 1, 0);
1207 runAbove.assign(gaps.size() + 1, 0);
1208 for (size_t g = gaps.size(); g-- > 0;) {
1209 freeAbove[g] = freeAbove[g + 1] + gaps[g].size;
1210 runAbove[g] = std::max(runAbove[g + 1], gaps[g].size);
1211 }
1212 }
1213
1214 // Lowest address at or above `from` in gap `g` where buffer `i` fits.
1215 std::optional<int64_t> lowest(size_t i, size_t g, int64_t from) const {
1216 std::optional<int64_t> best;
1217 for (MemoryRun range : ranges[i]) {
1218 int64_t at = llvm::alignTo(std::max(from, range.start), aligns[i]);
1219 if (at + sizes[i] <= std::min(gaps[g].end(), range.end()) &&
1220 getBankContaining(at, ctx.numBanks, ctx.bankLimits) >= 0 &&
1221 (!best || at < *best))
1222 best = at;
1223 }
1224 return best;
1225 }
1226
1227 bool run() {
1228 llvm::BitVector left(order.size());
1229 int64_t bytes = 0;
1230 for (size_t i = 0; i < order.size(); ++i) {
1231 if (sizes[i] == 0) {
1232 // Covers no byte, so it goes in the first gap its banks reach.
1233 for (size_t g = 0; g < gaps.size() && addrs[i] < 0; ++g)
1234 if (auto at = lowest(i, g, gaps[g].start))
1235 addrs[i] = *at;
1236 if (addrs[i] < 0)
1237 addrs[i] = ranges[i].front().start;
1238 continue;
1239 }
1240 left.set(i);
1241 bytes += sizes[i];
1242 }
1243 return gaps.empty() ? left.none()
1244 : place(0, gaps.front().start, left, bytes);
1245 }
1246
1247private:
1248 bool place(size_t g, int64_t from, llvm::BitVector &left, int64_t bytes) {
1249 if (left.none())
1250 return true;
1251 int64_t largest = 0;
1252 for (int i : left.set_bits())
1253 largest = std::max(largest, sizes[i]);
1254 int64_t here = gaps[g].end() - from;
1255 if (bytes > here + freeAbove[g + 1] ||
1256 largest > std::max(here, runAbove[g + 1]) ||
1257 failed.contains({from, left}))
1258 return false;
1259 SmallVector<unsigned> tried;
1260 for (int i : left.set_bits()) {
1261 if (llvm::is_contained(tried, classes[i]))
1262 continue;
1263 tried.push_back(classes[i]);
1264 std::optional<int64_t> at = lowest(i, g, from);
1265 if (!at)
1266 continue;
1267 if (budget-- <= 0) {
1268 exhausted = true;
1269 return false;
1270 }
1271 addrs[i] = *at;
1272 left.reset(i);
1273 bool done = place(g, *at + sizes[i], left, bytes - sizes[i]);
1274 left.set(i);
1275 if (done)
1276 return true;
1277 if (exhausted)
1278 return false;
1279 }
1280 if (g + 1 < gaps.size() && place(g + 1, gaps[g + 1].start, left, bytes))
1281 return true;
1282 if (!exhausted)
1283 failed.insert({from, left});
1284 return false;
1285 }
1286};
1287} // namespace
1288
1289// Places every buffer in `buffersToAlloc`. Returns the buffer that could not be
1290// placed, or nullptr when they all were; `placed` collects what was assigned so
1291// a failed attempt can be rolled back.
1292static BufferOp
1293placeFreeBuffers(ArrayRef<BufferOp> buffersToAlloc, const BankAwareContext &ctx,
1294 const RequiredBanks &requiredBanks, MemoryOccupancy &occupancy,
1295 SmallVectorImpl<BufferOp> &placed, int64_t budget,
1296 bool &exhausted, PlacementStats &stats) {
1297 int64_t remaining = 0;
1298 for (auto buffer : buffersToAlloc) {
1299 remaining += buffer.getAllocationSize();
1300 }
1301 PlacementSearch search{buffersToAlloc, ctx, requiredBanks, occupancy,
1302 placed, stats, budget};
1303 int64_t before = stats.backtracks;
1304 bool solved = search.run(/*index=*/0, /*startBankIndex=*/0, remaining);
1305 if (stats.backtracks > before) {
1306 ++stats.tilesSearched;
1307 }
1308 if (solved) {
1309 return nullptr;
1310 }
1311 // The ranked search unwinds everything it tried, so the tile is back to its
1312 // pinned buffers alone.
1313 CompactionSearch complete(buffersToAlloc, ctx, requiredBanks, occupancy,
1314 budget);
1315 if (complete.run()) {
1316 ++stats.tilesCompacted;
1317 for (auto [buffer, addr] : llvm::zip(buffersToAlloc, complete.addrs)) {
1318 placeBuffer(buffer, addr,
1319 getBankContaining(addr, ctx.numBanks, ctx.bankLimits),
1320 occupancy);
1321 placed.push_back(buffer);
1322 }
1323 return nullptr;
1324 }
1325 search.restoreDeepest();
1326 exhausted = complete.exhausted;
1327 if (complete.exhausted) {
1328 ++stats.budgetExhausted;
1329 }
1330 return search.deepest;
1331}
1332
1333// Rolls back what the allocator wrote, and leaves a mem_bank the user requested
1334// in place.
1335static void deAllocationBuffers(SmallVectorImpl<BufferOp> &buffers,
1336 const RequiredBanks &requiredBanks) {
1337 for (auto buffer : buffers) {
1338 buffer->removeAttr("address");
1339 auto required = requiredBanks.find(buffer);
1340 if (required == requiredBanks.end() || required->second.size() != 1) {
1341 buffer->removeAttr("mem_bank");
1342 } else {
1343 buffer.setMemBank(required->second.front());
1344 }
1345 }
1346}
1347
1348// Places every buffer carrying an explicit `address`, address pins first, so a
1349// mem_bank-only buffer cannot carve up space an address pin needs. A
1350// mem_bank-only buffer enters `requiredBanks` and queues into
1351// `buffersToAlloc`. Failure is terminal.
1352static LogicalResult placePreAllocatedBuffers(
1353 SmallVectorImpl<BufferOp> &preAllocatedBuffers, const BankAwareContext &ctx,
1354 MemoryOccupancy &occupancy, RequiredBanks &requiredBanks,
1355 SmallVectorImpl<BufferOp> &buffersToAlloc) {
1356 // Address buffers first (ascending), then mem_bank-only buffers. The sort is
1357 // stable, so the mem_bank-only tail, whose elements all compare equal, keeps
1358 // program order and places identically from one run to the next.
1359 llvm::stable_sort(preAllocatedBuffers, [](BufferOp a, BufferOp b) -> bool {
1360 auto a_addr = a.getAddress();
1361 auto b_addr = b.getAddress();
1362 if (a_addr.has_value() && b_addr.has_value()) {
1363 return a_addr.value() < b_addr.value();
1364 }
1365 return a_addr.has_value() && !b_addr.has_value();
1366 });
1367
1368 for (auto buffer : preAllocatedBuffers) {
1369 auto has_addr = checkAndAddBufferWithAddress(buffer, ctx, occupancy);
1370 if (failed(has_addr)) {
1371 return failure();
1372 }
1373 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
1374 if (*has_addr) {
1375 continue;
1376 }
1377 // Only a mem_bank: this pass still chooses the address, so queue the
1378 // buffer with the rest.
1379 if (failed(recordRequiredBank(buffer, ctx.numBanks, requiredBanks))) {
1380 return failure();
1381 }
1382 buffersToAlloc.push_back(buffer);
1383 }
1384 return success();
1385}
1386
1387// Order buffers for placement: fewest allowed banks first, then largest first,
1388// so a small buffer does not split the run a large one needs.
1389static SmallVector<BufferOp>
1390placementOrder(ArrayRef<BufferOp> buffersToAlloc,
1391 const RequiredBanks &requiredBanks) {
1392 SmallVector<BufferOp> order(buffersToAlloc.begin(), buffersToAlloc.end());
1393 llvm::stable_sort(order, [&](BufferOp a, BufferOp b) {
1394 if (requiredBanks.count(a) != requiredBanks.count(b)) {
1395 return requiredBanks.count(a) > requiredBanks.count(b);
1396 }
1397 if (requiredBanks.count(a) &&
1398 requiredBanks.lookup(a).size() != requiredBanks.lookup(b).size())
1399 return requiredBanks.lookup(a).size() < requiredBanks.lookup(b).size();
1400 return a.getAllocationSize() > b.getAllocationSize();
1401 });
1402 return order;
1403}
1404
1405static LogicalResult allocateTile(TileOp tile, int64_t budget,
1406 PlacementStats &stats) {
1407 auto device = tile->getParentOfType<AIE::DeviceOp>();
1408 if (!device) {
1409 return failure();
1410 }
1411
1412 std::vector<MemoryRun> bankLimits;
1413
1414 const auto &targetModel = getTargetModel(tile);
1415 auto [maxDataMemorySize, tileAlignBitWidth, maxVecAlignBits] =
1416 tileMemoryLimits(targetModel, tile.getTileType());
1417
1418 int numBanks = targetModel.getNumBanks(tile.getCol(), tile.getRow());
1419 int64_t bankSize = maxDataMemorySize / numBanks;
1420
1421 fillBankLimits(numBanks, bankSize, bankLimits);
1422
1423 // A stack_bank-only core has no address yet. Its bank is a hard constraint,
1424 // so reserve inside that bank before anything unconstrained is placed, and
1425 // record the address the way a mem_bank-only buffer gets one.
1426 MemoryRun stackRun;
1427 MemoryOccupancy occupancy(maxDataMemorySize);
1428 CoreOp coreOp = tile.getCoreOp();
1429 if (coreOp) {
1430 stackRun = coreOp.getStackRun();
1431 auto stackBank = coreOp.getStackBank();
1432 if (!coreOp.getStackAddress() && stackBank) {
1433 int bank = *stackBank;
1434 int64_t align = targetModel.getCoreStackAlignment();
1435 // Address-pinned buffers cannot move, whereas a bank-only stack can.
1436 // Account for those pins before choosing the stack's address. The normal
1437 // buffer placement below still validates each pin and records occupancy.
1438 MemoryOccupancy stackOccupancy(maxDataMemorySize);
1439 device.walk([&, maxDataMemorySize = maxDataMemorySize](BufferOp buffer) {
1440 if (buffer.getTileOp() != tile || !buffer.getAddress())
1441 return;
1442 int64_t start = *buffer.getAddress();
1443 int64_t end = start + buffer.getAllocationSize();
1444 if (start >= 0 && end <= maxDataMemorySize)
1445 stackOccupancy.markOccupied(start, end);
1446 });
1447 std::optional<int64_t> at = stackOccupancy.findLeastFragmentingGap(
1448 bankLimits[bank].start, bankLimits[bank].end(), stackRun.size, align);
1449 if (!at) {
1450 coreOp.emitOpError("requires a ")
1451 << stackRun.size << "-byte stack in bank " << bank
1452 << ", but no contiguous aligned space remains after address-pinned "
1453 "buffers";
1454 return failure();
1455 }
1456 stackRun.start = *at;
1457 coreOp.setStackAddress(*at);
1458 }
1459 occupancy.markOccupied(stackRun.start,
1460 std::min(stackRun.end(), maxDataMemorySize));
1461 }
1462 BankAwareContext ctx{
1463 numBanks, tileAlignBitWidth, maxVecAlignBits, bankLimits,
1464 maxDataMemorySize, stackRun, (bool)tile.getCoreOp()};
1465
1466 RequiredBanks requiredBanks;
1467 SmallVector<BufferOp> preAllocatedBuffers;
1468 SmallVector<BufferOp> buffersToAlloc;
1469 SmallVector<BufferOp> allBuffers_on_tile;
1470 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
1471 if (buffer.getTileOp() == tile) {
1472 if (!isBufferPreAllocated(buffer) &&
1473 banksFromMemorySpace(buffer.getType()).empty()) {
1474 buffersToAlloc.push_back(buffer);
1475 } else {
1476 preAllocatedBuffers.push_back(buffer);
1477 }
1478 allBuffers_on_tile.push_back(buffer);
1479 }
1480 });
1481
1482 if (failed(placePreAllocatedBuffers(preAllocatedBuffers, ctx, occupancy,
1483 requiredBanks, buffersToAlloc))) {
1484 return failure();
1485 }
1486
1487 // On npu2, the first word of the column-0 memtile reads back as 0x00CD0CD0
1488 // on every dispatch after the first, though nothing in the design writes it.
1489 // A buffer that DMA refills each dispatch is unaffected, but an initialized
1490 // one is loaded only once and keeps the bad word, so keep initialized buffers
1491 // off that word. Other buffers may still take it, and a buffer the user
1492 // pinned there keeps its address.
1493 //
1494 // TODO: Investigate. This is a low-confidence workaround for a cause nobody
1495 // has found: the word may be reserved by the firmware or driver, or clobbered
1496 // by something else. The value predates initialized buffers dropping their
1497 // locks (#3608 saw it first), so that change is not the cause.
1498 if (tile.isMemTile() && tile.getCol() == 0 &&
1499 targetModel.hasProperty(AIETargetModel::IsNPU) &&
1500 targetModel.getTargetArch() == AIEArch::AIE2p) {
1501 ctx.initGuardBytes = tileAlignBitWidth / 8;
1502 }
1503
1504 // Buffers this pass placed (not the pre-allocated ones), for rollback and
1505 // diagnostics on failure.
1506 SmallVector<BufferOp> allocatedBuffers;
1507
1508 // Bank-pinned buffers lead the order, then the core's data region, then the
1509 // unconstrained ones -- most-constrained first, since a pin has one candidate
1510 // bank. They are searched together rather than in separate passes: where a
1511 // pin's position inside its bank decides whether a later buffer fits, only a
1512 // single search can revisit it.
1513 SmallVector<BufferOp> order = placementOrder(buffersToAlloc, requiredBanks);
1514 bool searchExhausted = false;
1515 BufferOp failedBuffer =
1516 placeFreeBuffers(order, ctx, requiredBanks, occupancy, allocatedBuffers,
1517 budget, searchExhausted, stats);
1518
1519 if (BufferOp failed = failedBuffer) {
1520 // A buffer pinned to a bank that cannot hold it is a user constraint, not
1521 // an out-of-room tile: give it its own error and no memory map. Unless the
1522 // search merely ran out of budget, in which case the bank may have room.
1523 if (!searchExhausted && requiredBanks.count(failed)) {
1524 auto banks = requiredBanks.lookup(failed);
1525 if (banks.size() > 1) {
1526 failed.emitOpError("")
1527 << bufferLabel(failed) << " requires " << failed.getAllocationSize()
1528 << " bytes, but no contiguous aligned space remains within allowed "
1529 "banks "
1530 << banks.front() << " and " << banks.back();
1531 deAllocationBuffers(allocatedBuffers, requiredBanks);
1532 return failure();
1533 }
1534 int bank = banks.front();
1535 int64_t need = failed.getAllocationSize();
1536 int64_t bankCapacity = bankLimits[bank].size;
1537 if (need > bankCapacity) {
1538 failed->emitOpError("") << bufferLabel(failed) << " requires " << need
1539 << " bytes, which cannot fit in bank " << bank
1540 << " (" << bankCapacity << " bytes total)";
1541 } else {
1542 failed->emitOpError("")
1543 << bufferLabel(failed) << " requires " << need << " bytes in bank "
1544 << bank << ", but only "
1545 << occupancy.freeBytes(bankLimits[bank].start,
1546 bankLimits[bank].end())
1547 << " of " << bankCapacity << " bytes are free there";
1548 }
1549 deAllocationBuffers(allocatedBuffers, requiredBanks);
1550 return failure();
1551 }
1552 InFlightDiagnostic diag = failed.emitOpError("could not be placed: ")
1553 << bufferLabel(failed) << " needs "
1554 << failed.getAllocationSize()
1555 << " bytes and this tile has no room left for it";
1556 if (searchExhausted) {
1557 diag.attachNote() << "the search hit its " << budget
1558 << "-placement budget with arrangements still untried, "
1559 "so a layout may exist that it did not reach (raise "
1560 "it with placement-budget)";
1561 }
1562 // Print before rollback, while the addresses are still set.
1563 printMemMap(tile, allocatedBuffers, preAllocatedBuffers, ctx);
1564 deAllocationBuffers(allocatedBuffers, requiredBanks);
1565 return failure();
1566 }
1567 assert(allocatedBuffers.size() == buffersToAlloc.size());
1568
1569 sortBuffersByAddress(allBuffers_on_tile);
1570 // Every placement came from free space in the tile, so overflow cannot happen
1571 // here. The stack and overlap checks remain as a backstop.
1572 if (!checkAndPrintOverlapStackframe(stackRun, allBuffers_on_tile) ||
1573 !checkAndPrintBufferOverlap(allBuffers_on_tile, tileAlignBitWidth,
1574 maxVecAlignBits)) {
1575 return failure();
1576 }
1577 return success();
1578}
1579
1580static LogicalResult checkBufferScope(BufferOp buffer, DeviceOp device) {
1581 // collectBuffers walks the device's own operations, so the linker script and
1582 // the BCF describe a buffer only at that depth. A buffer nested anywhere else
1583 // reaches the core as an undefined symbol. MemTileDMA is the exception: a
1584 // memtile links no core, so nothing refers to the buffer by name.
1585 Operation *parent = buffer->getParentOp();
1586 if (!isa<DeviceOp>(parent) && !isa<MemTileDMAOp>(parent)) {
1587 auto tile = buffer.getTileOp();
1588 tile->emitOpError("Buffer '")
1589 << buffer.name()
1590 << "' must be defined directly under the device scope. Currently it "
1591 "is nested inside a core tile.";
1592 return failure();
1593 }
1594 return success();
1595}
1596
1597namespace {
1598struct AIEAssignBufferAddressesPass
1599 : xilinx::AIE::impl::AIEAssignBufferAddressesBase<
1600 AIEAssignBufferAddressesPass> {
1601
1602 AIEAssignBufferAddressesPass() = default;
1603
1604 AIEAssignBufferAddressesPass(const AIEAssignBufferAddressesOptions &options)
1605 : AIEAssignBufferAddressesBase(options) {}
1606
1607 void getDependentDialects(DialectRegistry &registry) const override {
1608 registry.insert<func::FuncDialect>();
1609 registry.insert<AIEDialect>();
1610 }
1611
1612 void runOnOperation() override {
1613 DeviceOp device = getOperation();
1614 int64_t budget = clPlacementBudget;
1615 if (budget <= 0) {
1616 device.emitError("placement-budget must be positive, got ") << budget;
1617 return signalPassFailure();
1618 }
1619 if (failed(applySignatureBankConstraints(device))) {
1620 return signalPassFailure();
1621 }
1622 materializeCoreDataBuffers(device);
1623 materializeBankReservations(device);
1624 materializePrebakedRanges(device);
1625
1626 // One allocator, one answer. There used to be two schemes and a fallback
1627 // between them, which meant a tile that ran out of room reported twice and
1628 // explained itself once, and a `mem_bank` pin could be dropped on the way
1629 // from one scheme to the other. Placement now either finds a layout or says
1630 // why it could not.
1631 PlacementStats stats;
1632 bool ok = llvm::all_of(device.getOps<TileOp>(), [&](TileOp tile) {
1633 return succeeded(allocateTile(tile, budget, stats));
1634 });
1635 // Recorded on failure too, where budget-exhausted is the one that matters.
1636 numTilesSearched += stats.tilesSearched;
1637 numBacktracks += stats.backtracks;
1638 numBudgetExhausted += stats.budgetExhausted;
1639 numTilesCompacted += stats.tilesCompacted;
1640 if (!ok)
1641 signalPassFailure();
1642 }
1643};
1644} // namespace
1645
1646namespace {
1647struct AIEPrepareBuffersPass
1648 : xilinx::AIE::impl::AIEPrepareBuffersBase<AIEPrepareBuffersPass> {
1649 void getDependentDialects(DialectRegistry &registry) const override {
1650 registry.insert<func::FuncDialect>();
1651 registry.insert<AIEDialect>();
1652 }
1653
1654 void runOnOperation() override {
1655 DeviceOp device = getOperation();
1656 // A buffer's name becomes a symbol in its core's object, so every buffer
1657 // needs a name before the cores are lowered. Naming runs first, because a
1658 // diagnostic below names the buffer it rejects.
1659 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
1660 unsigned counter = 0;
1661 NamedOpTable names(device);
1662 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
1663 if (!buffer.hasName()) {
1664 buffer->setAttr(
1665 SymbolTable::getSymbolAttrName(),
1666 builder.getStringAttr(names.uniqueName("_anonymous", counter)));
1667 }
1668 });
1669 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
1670 if (failed(checkBufferScope(buffer, device))) {
1671 return signalPassFailure();
1672 }
1673 });
1674 }
1675};
1676} // namespace
1677
1678std::unique_ptr<OperationPass<DeviceOp>> AIE::createAIEPrepareBuffersPass() {
1679 return std::make_unique<AIEPrepareBuffersPass>();
1680}
1681
1682std::unique_ptr<OperationPass<DeviceOp>>
1684 return std::make_unique<AIEAssignBufferAddressesPass>();
1685}
1686
1687std::unique_ptr<OperationPass<DeviceOp>>
1689 const AIEAssignBufferAddressesOptions &options) {
1690 return std::make_unique<AIEAssignBufferAddressesPass>(options);
1691}
DenseMap< Operation *, SmallVector< int, 2 > > RequiredBanks
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEPrepareBuffersPass()
const AIETargetModel & getTargetModel(mlir::Operation *op)
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEAssignBufferAddressesPass()
uint32_t requiredBufferAlignBits(uint32_t busAlignBits, uint32_t maxVecAlignBits, int64_t sizeBytes)
Return the alignment (in bits) a buffer of sizeBytes must satisfy.
TileMemoryLimits tileMemoryLimits(const AIETargetModel &targetModel, AIETileType tileType)
ArrayRef< MemoryRun > bankLimits
int64_t end() const