MLIR-AIE
AIEAssignBuffers.cpp
Go to the documentation of this file.
1//===- AIEAssignBuffers.cpp -------------------------------------*- C++ -*-===//
2//
3// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7// (c) Copyright 2019 Xilinx Inc.
8//
9//===----------------------------------------------------------------------===//
10
13
14#include "mlir/IR/Attributes.h"
15
16namespace xilinx::AIE {
17#define GEN_PASS_DEF_AIEASSIGNBUFFERADDRESSES
18#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
19} // namespace xilinx::AIE
20
21#define DEBUG_TYPE "aie-assign-buffers"
22
23using namespace mlir;
24using namespace xilinx;
25using namespace xilinx::AIE;
26
27//===----------------------------------------------------------------------===//
28// BasicAllocation : sequential alloc from largest to smallest
29//===----------------------------------------------------------------------===//
30bool checkAndPrintOverflow(TileOp tile, int address, int maxDataMemorySize,
31 int stacksize, SmallVector<BufferOp> &buffers) {
32 if (address > maxDataMemorySize) {
33 InFlightDiagnostic error =
34 tile.emitOpError("allocated buffers exceeded available memory\n");
35 auto &note = error.attachNote() << "MemoryMap:\n";
36 auto printbuffer = [&](StringRef name, int address, int size) {
37 note << "\t" << name << " \t"
38 << ": 0x" << llvm::utohexstr(address) << "-0x"
39 << llvm::utohexstr(address + size - 1) << " \t(" << size
40 << " bytes)\n";
41 };
42 if (stacksize > 0)
43 printbuffer("(stack)", 0, stacksize);
44 else
45 error << "(no stack allocated)\n";
46
47 for (auto buffer : buffers) {
48 assert(buffer.getAddress().has_value() &&
49 "buffer must have address assigned");
50 printbuffer(buffer.name(), buffer.getAddress().value(),
51 buffer.getAllocationSize());
52 }
53 return false;
54 }
55 return true;
56}
57
58bool basicAllocation(TileOp tile) {
59 auto device = tile->getParentOfType<AIE::DeviceOp>();
60 if (!device)
61 return false;
62
63 const auto &targetModel = getTargetModel(tile);
64 int maxDataMemorySize = 0;
65 if (tile.isMemTile())
66 maxDataMemorySize = targetModel.getMemTileSize();
67 else
68 maxDataMemorySize = targetModel.getLocalMemorySize();
69
70 SmallVector<BufferOp> buffers;
71 SmallVector<BufferOp> allocated_buffers;
72 // Collect all the buffers for this tile. If the buffer has an address, add
73 // it to allocated_buffers. Otherwise, add it to buffers.
74 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
75 if (buffer.getTileOp() == tile) {
76 if (buffer.getAddress())
77 allocated_buffers.push_back(buffer);
78 else
79 buffers.push_back(buffer);
80 }
81 });
82
83 // Sort buffers by allocation size.
84 std::sort(buffers.begin(), buffers.end(), [](BufferOp a, BufferOp b) {
85 return a.getAllocationSize() > b.getAllocationSize();
86 });
87
88 // Sort allocated_buffers by address
89 std::sort(allocated_buffers.begin(), allocated_buffers.end(),
90 [](BufferOp a, BufferOp b) {
91 return a.getAddress().value() < b.getAddress().value();
92 });
93
94 // Address range owned by the MemTile is 0x80000.
95 // Address range owned by the tile is 0x8000 in
96 // AIE1 and 0x10000 in AIE2, but we need room at
97 // the bottom for stack.
98 int stacksize = 0;
99 int address = 0;
100 if (auto core = tile.getCoreOp()) {
101 stacksize = core.getStackSize();
102 address += stacksize;
103 }
104
105 // As the next address to allocate is assigned, skip over any buffers
106 // from the allocated_buffers list.
107 auto current_alloc = allocated_buffers.begin();
108 for (auto buffer : buffers) {
109 assert(!buffer.getAddress());
110 while (current_alloc != allocated_buffers.end() &&
111 address + buffer.getAllocationSize() >
112 current_alloc->getAddress().value()) {
113 address = current_alloc->getAddress().value() +
114 current_alloc->getAllocationSize();
115 current_alloc++;
116 }
117 buffer.setAddress(address);
118 address += buffer.getAllocationSize();
119 }
120
121 // Sort by smallest address before printing memory map.
122 std::sort(buffers.begin(), buffers.end(), [](BufferOp a, BufferOp b) {
123 assert(a.getAddress().has_value() && "buffer must have address assigned");
124 assert(b.getAddress().has_value() && "buffer must have address assigned");
125 return a.getAddress().value() < b.getAddress().value();
126 });
127 // Check if memory was exceeded on any bank and print debug info.
128 return checkAndPrintOverflow(tile, address, maxDataMemorySize, stacksize,
129 buffers);
130}
131
132//===----------------------------------------------------------------------===//
133// SimpleBankAwareAllocation : round-robin each alloc over available banks
134//===----------------------------------------------------------------------===//
135typedef struct BankLimits {
136 int64_t startAddr;
137 int64_t endAddr;
139
140// Function that given a number of banks and their size, computes
141// the start and end addresses for each bank and fills in the entry
142// in the bankLimits vector.
143void fillBankLimits(int numBanks, int bankSize,
144 std::vector<BankLimits> &bankLimits) {
145 for (int i = 0; i < numBanks; i++) {
146 auto startAddr = bankSize * i;
147 auto endAddr = bankSize * (i + 1);
148 bankLimits.push_back({startAddr, endAddr});
149 }
150}
151
152// Function that sets the address attribute of the given buffer to
153// the given start_addr. It also updates the entry in the
154// nextAddrInBanks for the corresponding bank.
155void setAndUpdateAddressInBank(BufferOp buffer, int64_t start_addr,
156 int64_t end_addr,
157 std::vector<int64_t> &nextAddrInBanks) {
158 // Fixme: alignment
159 buffer.setAddress(start_addr);
160 nextAddrInBanks[buffer.getMemBank().value()] = end_addr;
161}
162
163// Function that checks whether the given buffer already has a set address
164// attribute. If it does, it finds in which bank the buffer is and checks
165// whether there is enough space left for it. If there is the function
166// returns true and if not, the function emits a warning that the address
167// will be overwritten and returns false (which will cause the buffer to be
168// added to the list of buffers without addresses, to be completed later on).
169FailureOr<bool>
170checkAndAddBufferWithAddress(BufferOp buffer, int numBanks,
171 std::vector<int64_t> &nextAddrInBanks,
172 std::vector<BankLimits> &bankLimits) {
173 auto addrAttr = buffer->getAttrOfType<IntegerAttr>("address");
174 if (!addrAttr)
175 return false;
176
177 int addr = addrAttr.getInt();
178 for (int i = 0; i < numBanks; i++) {
179 // if the address is not within the bank, continue
180 if (addr < bankLimits[i].startAddr || addr >= bankLimits[i].endAddr)
181 continue;
182
183 // if the allocator already allocated this address, fail
184 if (addr < nextAddrInBanks[i])
185 return buffer->emitOpError("would override allocated address");
186
187 // the allocator can accomadate this existing allocation
188 nextAddrInBanks[i] = addr + buffer.getAllocationSize();
189 buffer.setMemBank(i);
190 }
191 return true;
192}
193
194// Function that checks whether the given buffer already has a set mem_bank
195// attribute. If it does, it checks whether there is enough space left for
196// it. If there is, it sets the buffer's address field and if not, the
197// function emits a warning that the mem_bank will be overwritten and returns
198// false (which will cause the buffer to be added to the list of buffers
199// without addresses, to be completed later on).
200FailureOr<bool>
201checkAndAddBufferWithMemBank(BufferOp buffer, int numBanks,
202 std::vector<int64_t> &nextAddrInBanks,
203 std::vector<BankLimits> &bankLimits) {
204 auto memBankAttr = buffer->getAttrOfType<IntegerAttr>("mem_bank");
205 if (!memBankAttr)
206 return false;
207
208 int mem_bank = memBankAttr.getInt();
209 int64_t startAddr = nextAddrInBanks[mem_bank];
210 int64_t endAddr = startAddr + buffer.getAllocationSize();
211 if (endAddr > bankLimits[mem_bank].endAddr)
212 return buffer->emitOpError("would override existing mem_bank");
213 setAndUpdateAddressInBank(buffer, startAddr, endAddr, nextAddrInBanks);
214 return true;
215}
216
217// Prints the memory map across banks
218void printMemMap(TileOp tile, SmallVector<BufferOp> &allocatedBuffers,
219 SmallVector<BufferOp> &preAllocatedBuffers, int numBanks,
220 std::vector<BankLimits> &bankLimits, int stacksize) {
221 InFlightDiagnostic error = tile.emitWarning(
222 "Not all requested buffers fit in the available memory.\n");
223 auto &note = error.attachNote()
224 << "Current configuration of buffers in bank(s) : ";
225 note << "MemoryMap:\n";
226 auto printbuffer = [&](StringRef name, int address, int size) {
227 note << "\t"
228 << "\t" << name << " \t"
229 << ": 0x" << llvm::utohexstr(address) << "-0x"
230 << llvm::utohexstr(address + size - 1) << " \t(" << size
231 << " bytes)\n";
232 };
233 for (int i = 0; i < numBanks; i++) {
234 if (i == 0) {
235 if (stacksize > 0)
236 printbuffer("(stack)", 0, stacksize);
237 else
238 note << "(no stack allocated)\n";
239 }
240 note << "\t"
241 << "bank : " << i << "\t"
242 << "0x" << llvm::utohexstr(bankLimits[i].startAddr) << "-0x"
243 << llvm::utohexstr(bankLimits[i].endAddr - 1) << "\n";
244 for (auto buffer : preAllocatedBuffers) {
245 auto addr = buffer.getAddress().value();
246 auto mem_bank = buffer.getMemBank().value();
247 if (mem_bank == i)
248 printbuffer(buffer.name(), addr, buffer.getAllocationSize());
249 }
250 for (auto buffer : allocatedBuffers) {
251 auto addr = buffer.getAddress().value();
252 auto mem_bank = buffer.getMemBank().value();
253 if (mem_bank == i)
254 printbuffer(buffer.name(), addr, buffer.getAllocationSize());
255 }
256 }
257}
258
259// Function that given a buffer will iterate over all the memory banks
260// starting from the given index to try and find a bank with enough
261// space. If it does, it will set the buffer's address and mem_bank
262// attributes and update the nextAddrInBanks vector.
263// If it does not find one with enough space, it will throw an error.
264// Returns true if the buffer was successfully allocated, false otherwise.
265// If no bank has enough space to accommodate the buffer, an error is emitted.
266
267int setBufferAddress(BufferOp buffer, int numBanks, int &startBankIndex,
268 std::vector<int64_t> &nextAddrInBanks,
269 std::vector<BankLimits> &bankLimits) {
270 assert(startBankIndex < numBanks &&
271 "Unexpected input value for startBankIndex");
272 int bankIndex = startBankIndex;
273 bool allocated = false;
274 for (int i = 0; i < numBanks; i++) {
275 int64_t startAddr = nextAddrInBanks[bankIndex];
276 int64_t endAddr = startAddr + buffer.getAllocationSize();
277 if (endAddr <= bankLimits[bankIndex].endAddr) {
278 buffer.setMemBank(bankIndex);
279 setAndUpdateAddressInBank(buffer, startAddr, endAddr, nextAddrInBanks);
280 allocated = true;
281 bankIndex = (bankIndex + 1) % numBanks;
282 startBankIndex = bankIndex;
283 break;
284 }
285 // Move to the next bank
286 bankIndex = (bankIndex + 1) % numBanks;
287 }
288 // If no bank has enough space, throws error
289 if (!allocated) {
290 buffer.emitWarning("Failed to allocate buffer: ")
291 << buffer.name() << " with size: " << buffer.getAllocationSize()
292 << " bytes.";
293 return false;
294 }
295 return true;
296}
297
298bool checkAndPrintOverflow(TileOp tile, int numBanks, int stacksize,
299 SmallVector<BufferOp> &allBuffers,
300 std::vector<int64_t> &nextAddrInBanks,
301 std::vector<BankLimits> &bankLimits) {
302 bool foundOverflow = false;
303 std::vector<int> overflow_banks;
304 for (int i = 0; i < numBanks; i++) {
305 if (nextAddrInBanks[i] > bankLimits[i].endAddr) {
306 foundOverflow = true;
307 overflow_banks.push_back(i);
308 }
309 }
310 if (foundOverflow) {
311 InFlightDiagnostic error =
312 tile.emitWarning("allocated buffers exceeded available memory\n");
313 auto &note = error.attachNote() << "Error in bank(s) : ";
314 for (auto bank : overflow_banks)
315 note << bank << " ";
316 note << "\n";
317 note << "MemoryMap:\n";
318 auto printbuffer = [&](StringRef name, int address, int size) {
319 note << "\t"
320 << "\t" << name << " \t"
321 << ": 0x" << llvm::utohexstr(address) << "-0x"
322 << llvm::utohexstr(address + size - 1) << " \t(" << size
323 << " bytes)\n";
324 };
325 for (int i = 0; i < numBanks; i++) {
326 note << "\t"
327 << "bank : " << i << "\t"
328 << "0x" << llvm::utohexstr(bankLimits[i].startAddr) << "-0x"
329 << llvm::utohexstr(bankLimits[i].endAddr - 1) << "\n";
330 if (i == 0) {
331 if (stacksize > 0)
332 printbuffer("(stack)", 0, stacksize);
333 else
334 error << "(no stack allocated)\n";
335 }
336 for (auto buffer : allBuffers) {
337 auto addr = buffer.getAddress().value();
338 auto mem_bank = buffer.getMemBank().value();
339 if (mem_bank == i)
340 printbuffer(buffer.name(), addr, buffer.getAllocationSize());
341 }
342 }
343 return false;
344 }
345 return true;
346}
347
348// Function to deallocate attributes of buffers in case of a failure
349void deAllocationBuffers(SmallVector<BufferOp> &buffers) {
350 for (auto buffer : buffers) {
351 buffer->removeAttr("address");
352 buffer->removeAttr("mem_bank");
353 }
354}
355
356bool simpleBankAwareAllocation(TileOp tile) {
357 auto device = tile->getParentOfType<AIE::DeviceOp>();
358 if (!device)
359 return false;
360
361 std::vector<int64_t>
362 nextAddrInBanks; // each entry is the next address available for use
363 // for that bank
364 // (e.g. nextAddrInBanks[tile_0][1] = next available
365 // address in bank 1 for tile_0)
366 std::vector<BankLimits> bankLimits; // the entries contain pairs of start and
367 // end addresses for each bank
368
369 const auto &targetModel = getTargetModel(tile);
370 int maxDataMemorySize = 0;
371 if (tile.isMemTile())
372 maxDataMemorySize = targetModel.getMemTileSize();
373 else
374 maxDataMemorySize = targetModel.getLocalMemorySize();
375
376 int numBanks = targetModel.getNumBanks(tile.getCol(), tile.getRow());
377 int bankSize = maxDataMemorySize / numBanks;
378
379 // Address range owned by the MemTile is 0x80000.
380 // Address range owned by the tile is 0x8000 in
381 // AIE1 and 0x10000 in AIE2, but we need room at
382 // the bottom for stack.
383 int stacksize = 0;
384 for (int i = 0; i < numBanks; i++)
385 nextAddrInBanks.push_back(bankSize * i);
386 if (auto core = tile.getCoreOp()) {
387 stacksize = core.getStackSize();
388 nextAddrInBanks[0] += stacksize;
389 }
390 fillBankLimits(numBanks, bankSize, bankLimits);
391
392 SmallVector<BufferOp> preAllocatedBuffers;
393 SmallVector<BufferOp> buffersToAlloc;
394 SmallVector<BufferOp> allBuffers;
395 // Collect all the buffers for this tile.
396 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
397 if (buffer.getTileOp() == tile)
398 allBuffers.push_back(buffer);
399 });
400 // If possible, the buffers with an already specified address will not
401 // be overwritten (the available address range of the bank the buffers
402 // are in will start AFTER the specified adress + buffer size).
403 // Buffers with a specified mem_bank will be assigned first, after
404 // the above.
405 for (auto buffer : allBuffers) {
406 if (buffer.getTileOp() == tile) {
407 auto has_addr = checkAndAddBufferWithAddress(buffer, numBanks,
408 nextAddrInBanks, bankLimits);
409 auto has_bank = checkAndAddBufferWithMemBank(buffer, numBanks,
410 nextAddrInBanks, bankLimits);
411 if (failed(has_addr) || failed(has_bank))
412 return false;
413 if (!has_addr.value() && !has_bank.value())
414 buffersToAlloc.push_back(buffer);
415 else
416 preAllocatedBuffers.push_back(buffer);
417 }
418 }
419
420 // Sort by largest allocation size before allocating.
421 std::sort(buffersToAlloc.begin(), buffersToAlloc.end(),
422 [](BufferOp a, BufferOp b) {
423 return a.getAllocationSize() > b.getAllocationSize();
424 });
425
426 // Set addresses for remaining buffers.
427 SmallVector<BufferOp> allocatedBuffers;
428 int bankIndex = 0;
429 for (auto buffer : buffersToAlloc) {
430 // If the buffer doesn't fit in any of the bank space then
431 // it prints the current memory map of the banks,
432 // deallocates all the buffers, and
433 // returns a failure.
434 if (!setBufferAddress(buffer, numBanks, bankIndex, nextAddrInBanks,
435 bankLimits)) {
436
437 printMemMap(tile, allocatedBuffers, preAllocatedBuffers, numBanks,
438 bankLimits, stacksize);
439 deAllocationBuffers(allocatedBuffers);
440 return false;
441 } else {
442 allocatedBuffers.push_back(buffer);
443 }
444 }
445
446 // Sort by smallest address before printing memory map.
447 std::sort(allBuffers.begin(), allBuffers.end(), [](BufferOp a, BufferOp b) {
448 assert(a.getAddress().has_value() && "buffer must have address assigned");
449 assert(b.getAddress().has_value() && "buffer must have address assigned");
450 return a.getAddress().value() < b.getAddress().value();
451 });
452 // Check if memory was exceeded on any bank and print debug info.
453 return checkAndPrintOverflow(tile, numBanks, stacksize, allBuffers,
454 nextAddrInBanks, bankLimits);
455}
456
457LogicalResult checkBufferScope(BufferOp buffer, DeviceOp device) {
458 // Buffers are not allowed to be inside the core without being statically
459 // initialized.
460 Operation *parent = buffer->getParentOp();
461 // Allowed to be in MemTile
462 if (!isa<DeviceOp>(parent) && !isa<MemTileDMAOp>(parent) &&
463 !buffer.getInitialValue().has_value()) {
464 auto tile = buffer.getTileOp();
465 tile->emitOpError("Buffer '")
466 << buffer.name()
467 << "' must be defined directly under the device scope. Currently it "
468 "is nested inside a core tile.";
469 return failure();
470 }
471 return success();
472}
473
476 AIEAssignBufferAddressesPass> {
477
479
480 AIEAssignBufferAddressesPass(const AIEAssignBufferAddressesOptions &options) {
481 clAllocScheme = options.clAllocScheme;
482 }
483
484 void getDependentDialects(DialectRegistry &registry) const override {
485 registry.insert<func::FuncDialect>();
486 registry.insert<AIEDialect>();
487 }
488
489 void runOnOperation() override {
490 DeviceOp device = getOperation();
491 OpBuilder builder = OpBuilder::atBlockTerminator(device.getBody());
492 // Ensure all BufferOps are globally defined at the device level.
493 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
494 if (failed(checkBufferScope(buffer, device)))
495 return signalPassFailure();
496 });
497 // Make sure all the buffers have a name
498 int counter = 0;
499 device.walk<WalkOrder::PreOrder>([&](BufferOp buffer) {
500 if (!buffer.hasName()) {
501 std::string name = "_anonymous";
502 name += std::to_string(counter++);
503 buffer->setAttr(SymbolTable::getSymbolAttrName(),
504 builder.getStringAttr(name));
505 }
506 });
507
508 // Select allocation scheme per tile
509 for (auto tile : device.getOps<TileOp>()) {
510 auto tileAllocationScheme = tile.getAllocationScheme();
511
512 if (!tileAllocationScheme)
513 tileAllocationScheme = clAllocScheme;
514
515 if (tileAllocationScheme == "basic-sequential") {
516 if (!basicAllocation(tile)) {
517 tile.emitOpError("Basic sequential allocation failed.");
518 return signalPassFailure();
519 }
520 } else if (tileAllocationScheme == "bank-aware") {
521 if (!simpleBankAwareAllocation(tile)) {
522 tile.emitOpError("Bank-aware allocation failed.");
523 return signalPassFailure();
524 }
525 } else {
526 if (!simpleBankAwareAllocation(tile)) {
527 tile.emitWarning("Bank-aware allocation failed, trying basic "
528 "sequential allocation.");
529 if (!basicAllocation(tile)) {
530 tile.emitOpError("Basic sequential allocation also failed.");
531 return signalPassFailure();
532 }
533 }
534 }
535 }
536 }
537};
538
539std::unique_ptr<OperationPass<DeviceOp>>
541 return std::make_unique<AIEAssignBufferAddressesPass>();
542}
543
544std::unique_ptr<OperationPass<DeviceOp>>
546 const AIEAssignBufferAddressesOptions &options) {
547 return std::make_unique<AIEAssignBufferAddressesPass>(options);
548}
LogicalResult checkBufferScope(BufferOp buffer, DeviceOp device)
void deAllocationBuffers(SmallVector< BufferOp > &buffers)
void setAndUpdateAddressInBank(BufferOp buffer, int64_t start_addr, int64_t end_addr, std::vector< int64_t > &nextAddrInBanks)
bool basicAllocation(TileOp tile)
int setBufferAddress(BufferOp buffer, int numBanks, int &startBankIndex, std::vector< int64_t > &nextAddrInBanks, std::vector< BankLimits > &bankLimits)
FailureOr< bool > checkAndAddBufferWithMemBank(BufferOp buffer, int numBanks, std::vector< int64_t > &nextAddrInBanks, std::vector< BankLimits > &bankLimits)
void fillBankLimits(int numBanks, int bankSize, std::vector< BankLimits > &bankLimits)
bool simpleBankAwareAllocation(TileOp tile)
void printMemMap(TileOp tile, SmallVector< BufferOp > &allocatedBuffers, SmallVector< BufferOp > &preAllocatedBuffers, int numBanks, std::vector< BankLimits > &bankLimits, int stacksize)
bool checkAndPrintOverflow(TileOp tile, int address, int maxDataMemorySize, int stacksize, SmallVector< BufferOp > &buffers)
FailureOr< bool > checkAndAddBufferWithAddress(BufferOp buffer, int numBanks, std::vector< int64_t > &nextAddrInBanks, std::vector< BankLimits > &bankLimits)
Include the generated interface declarations.
const AIETargetModel & getTargetModel(mlir::Operation *op)
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEAssignBufferAddressesPass()
AIEAssignBufferAddressesPass(const AIEAssignBufferAddressesOptions &options)
void getDependentDialects(DialectRegistry &registry) const override