MLIR-AIE 1.4.6.dev
AIEObjectFifoVerify.cpp
Go to the documentation of this file.
1//===- AIEObjectFifoVerify.cpp ----------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
13
14#include "mlir/Dialect/SCF/IR/SCF.h"
15#include "mlir/Pass/Pass.h"
16#include "llvm/ADT/MapVector.h"
17#include "llvm/ADT/SetVector.h"
18#include "llvm/Support/Debug.h"
19
20#include <algorithm>
21#include <limits>
22#include <memory>
23#include <optional>
24
25using namespace mlir;
26using namespace xilinx;
27using namespace xilinx::AIE;
28
29#define DEBUG_TYPE "aie-objectfifo-verify"
30
31namespace xilinx::AIE {
32#define GEN_PASS_DEF_AIEOBJECTFIFOVERIFY
33#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
34} // namespace xilinx::AIE
35
36namespace {
37
38struct SegmentActors {
39 SmallVector<Operation *> fillers;
40 SmallVector<Operation *> drainers;
41};
42
43/// A DMA endpoint with an iteration count moves this many objects through its
44/// pool, then stops.
45struct TransferBudget {
46 ObjectFifoDmaEndpointOp endpoint;
47 int32_t iterations;
48 int64_t objects;
49};
50
51/// IRON's forever loop is an `scf.while` on a constant true condition. A
52/// hand-written `scf.for` meant to run forever has a bound no design counts
53/// to, such as sys.maxsize or 2^32 - 1. From the AIE's 24-bit BD-loop
54/// saturation value up, a trip count is read as "forever"; below it, as a real
55/// bound.
56static constexpr int64_t kRunsForever = (int64_t{1} << 24) - 2;
57
58static std::optional<int64_t> getStaticTripCount(scf::ForOp forOp) {
59 if (forOp.getSingleLowerBound() && forOp.getSingleUpperBound() &&
60 forOp.getSingleStep()) {
61 if (std::optional<llvm::APInt> tc = forOp.getStaticTripCount()) {
62 return tc->getSExtValue();
63 }
64 }
65 return std::nullopt;
66}
67
68/// How many times `op` runs when its core, or runtime sequence, runs to
69/// completion, when static loop bounds alone decide that. Anything conditional
70/// between the op and the core, an `scf.if`, a loop with dynamic bounds or one
71/// meant to run forever, a block reached by branching, makes it unknown.
72static std::optional<int64_t> staticExecutions(Operation *op) {
73 int64_t count = 1;
74 for (Operation *cur = op; !isa<CoreOp, RuntimeSequenceOp>(cur);
75 cur = cur->getParentOp()) {
76 if (cur->getBlock() != &cur->getParentRegion()->front()) {
77 return std::nullopt;
78 }
79 Operation *parent = cur->getParentOp();
80 if (isa<CoreOp, RuntimeSequenceOp>(parent)) {
81 break;
82 }
83 auto forOp = dyn_cast<scf::ForOp>(parent);
84 if (!forOp) {
85 return std::nullopt;
86 }
87 std::optional<int64_t> trip = getStaticTripCount(forOp);
88 if (!trip || *trip >= kRunsForever ||
89 (*trip != 0 && count > std::numeric_limits<int64_t>::max() / *trip)) {
90 return std::nullopt;
91 }
92 count *= *trip;
93 }
94 return count;
95}
96
97/// Objects of the fifo `name` still held after `block` runs, entered holding
98/// `held`: an acquire holds at least its count, a release gives its count
99/// back. Unknown when an access sits under anything but a static loop, or a
100/// loop's count has not settled after two passes.
101static std::optional<int64_t> heldAfter(Block &block, StringRef name,
102 int64_t held) {
103 for (Operation &op : block) {
104 if (auto acquire = dyn_cast<ObjectFifoAcquireOp>(op)) {
105 if (acquire.getObjFifoName() == name)
106 held = std::max<int64_t>(held, acquire.acqNumber());
107 continue;
108 }
109 if (auto release = dyn_cast<ObjectFifoReleaseOp>(op)) {
110 if (release.getObjFifoName() == name)
111 held -= release.relNumber();
112 if (held < 0)
113 return std::nullopt;
114 continue;
115 }
116 bool touches = op.getNumRegions() != 0 &&
117 op.walk([&](Operation *inner) {
118 auto acquire = dyn_cast<ObjectFifoAcquireOp>(inner);
119 auto release = dyn_cast<ObjectFifoReleaseOp>(inner);
120 if ((acquire && acquire.getObjFifoName() == name) ||
121 (release && release.getObjFifoName() == name))
122 return WalkResult::interrupt();
123 return WalkResult::advance();
124 }).wasInterrupted();
125 if (!touches)
126 continue;
127 auto forOp = dyn_cast<scf::ForOp>(op);
128 std::optional<int64_t> trip =
129 forOp ? getStaticTripCount(forOp) : std::nullopt;
130 if (!trip)
131 return std::nullopt;
132 if (*trip == 0)
133 continue;
134 std::optional<int64_t> once = heldAfter(*forOp.getBody(), name, held);
135 if (!once)
136 return std::nullopt;
137 if (*trip > 1 && heldAfter(*forOp.getBody(), name, *once) != once)
138 return std::nullopt;
139 held = *once;
140 }
141 return held;
142}
143
144struct AIEObjectFifoVerifyPass
145 : public xilinx::AIE::impl::AIEObjectFifoVerifyBase<
146 AIEObjectFifoVerifyPass> {
147
148 /// Which pools get locks. Without locks nothing waits on a release, and
149 /// nothing a core holds stays taken.
150 std::shared_ptr<PoolLocks> poolLocks;
151
152 /// Every segment needs an actor at each end, so that what one writes another
153 /// reads. A segment may go unfilled when the pool's objects start full, and
154 /// may name no endpoint at an end its `manualFill` or `manualDrain` hands to
155 /// a DMA program the author writes.
156 LogicalResult verifyActors(ObjectFifoPoolOp pool,
157 ArrayRef<SegmentActors> actors) {
158 bool external = llvm::any_of(pool.getBufferOps(), [](BufferLike buffer) {
159 return isa<ExternalBufferOp>(buffer.getOperation());
160 });
161 std::vector<ObjectFifoSegmentOp> segmentOps = pool.getSegmentOps();
162 for (auto [index, segment] : llvm::enumerate(actors)) {
163 ObjectFifoSegmentOp segmentOp = segmentOps[index];
164 bool manualFill = segmentOp.getManualFill();
165 bool manualDrain = segmentOp.getManualDrain();
166 if (segment.fillers.size() > 1 ||
167 (manualFill && !segment.fillers.empty())) {
168 return pool.emitOpError("segment ")
169 << index << " is filled by more than one endpoint";
170 }
171 if (segment.drainers.size() > 1 ||
172 (manualDrain && !segment.drainers.empty())) {
173 return pool.emitOpError("segment ")
174 << index << " is drained by more than one endpoint";
175 }
176 // The host works an external pool's buffers, so it stands in for the end
177 // this device does not program.
178 bool hostDrains = external && !segment.fillers.empty();
179 bool hostFills = external && !segment.drainers.empty();
180 if (segment.drainers.empty() && !hostDrains && !manualDrain) {
181 return pool.emitOpError("segment ") << index << " has no drainer";
182 }
183 if (segment.fillers.empty() && !pool.getInitValues() && !hostFills &&
184 !manualFill) {
185 return pool.emitOpError("segment ") << index << " has no filler";
186 }
187 }
188 return success();
189 }
190
191 /// An endpoint that is not connected carries data nowhere.
192 LogicalResult verifyFlows(DeviceOp device,
193 SymbolTableCollection &symbolTables) {
194 DenseMap<StringRef, int> appearances;
195 for (auto flow : device.getOps<RouteOp>()) {
196 for (StringRef source : flow.getSourceNames()) {
197 appearances[source]++;
198 }
199 for (StringRef dest : flow.getDestinationNames()) {
200 appearances[dest]++;
201 }
202 }
203
204 for (auto endpoint : device.getOps<RouteEndpoint>()) {
205 int count =
206 appearances.lookup(cast<SymbolOpInterface>(*endpoint).getName());
207 if (count == 0) {
208 return endpoint->emitOpError("is not connected by any flow");
209 }
210 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(*endpoint);
211 dma && dma.getDispatch()) {
212 if (failed(verifyDispatch(device, dma, symbolTables)))
213 return failure();
214 continue;
215 }
216 // An aie.route_endpoint's own verifier says which flows may share it.
217 if (count > 1 && !isa<RouteEndpointOp>(endpoint.getOperation())) {
218 return endpoint->emitOpError(
219 "drives one channel, so at most one flow may name it, but "
220 "it is named ")
221 << count << " times";
222 }
223 }
224 return success();
225 }
226
227 /// A dispatching endpoint is the one source of one packet-switched route per
228 /// destination it takes turns between, and of nothing else. A second route to
229 /// the same destination is refused there, by the count every endpoint gets.
230 LogicalResult verifyDispatch(DeviceOp device,
231 ObjectFifoDmaEndpointOp endpoint,
232 SymbolTableCollection &symbolTables) {
233 StringRef name = endpoint.getSymName();
234 llvm::SmallSetVector<StringRef, 4> turns;
235 for (StringRef dest :
236 endpoint.getDispatchAttr().getAsValueRange<FlatSymbolRefAttr>())
237 turns.insert(dest);
238 llvm::SmallDenseSet<StringRef, 4> reached;
239 for (auto flow : device.getOps<RouteOp>()) {
240 if (flow.namesAsDestination(name))
241 return flow.emitOpError("names dispatching endpoint @")
242 << name << " as a destination";
243 if (!flow.namesAsSource(name))
244 continue;
245 if (flow.getSources().size() != 1 || flow.getDestinations().size() != 1)
246 return flow.emitOpError("leaves dispatching endpoint @")
247 << name
248 << " for one destination, so it has one source and "
249 "one destination";
250 if (!flow.getPacket())
251 return flow.emitOpError("leaves dispatching endpoint @")
252 << name << " and needs a packet header to tell its turns apart";
253 StringRef dest = *flow.getDestinationNames().begin();
254 if (!turns.contains(dest))
255 return flow.emitOpError("reaches @")
256 << dest << ", which dispatching endpoint @" << name
257 << " never takes a turn for";
258 reached.insert(dest);
259 }
260 for (StringRef dest : turns)
261 if (!reached.contains(dest))
262 return endpoint.emitOpError("dispatches to @")
263 << dest << ", but no route from it reaches there";
264
265 // Every turn moves the same chain through the dispatching endpoint, one
266 // object per turn, so each turn's destination must move as many elements
267 // as the dispatching endpoint itself does.
268 int64_t elements = transferElements(endpoint);
269 for (StringRef dest : turns) {
270 auto destEndpoint = symbolTables.lookupSymbolIn<ObjectFifoDmaEndpointOp>(
271 device, StringAttr::get(device.getContext(), dest));
272 if (!destEndpoint)
273 continue;
274 int64_t destElements = transferElements(destEndpoint);
275 if (destElements != elements)
276 return endpoint.emitOpError("dispatches, so every turn moves ")
277 << elements << " elements, but @" << dest << " moves "
278 << destElements;
279 }
280 return success();
281 }
282
283 /// The elements one pass of an endpoint's chain moves: each selected segment
284 /// once, through its transform where it has one.
285 static int64_t transferElements(ObjectFifoDmaEndpointOp endpoint) {
286 int64_t elements = 0;
287 std::optional<ArrayRef<BDDimLayoutArrayAttr>> dimensions =
288 endpoint.getDimensions();
289 for (auto [index, segment] :
290 llvm::enumerate(endpoint.getSelectedSegments())) {
291 BDDimLayoutArrayAttr dims;
292 if (dimensions && index < dimensions->size()) {
293 dims = (*dimensions)[index];
294 }
295 if (dims && !dims.empty()) {
296 int64_t product = 1;
297 for (BDDimLayoutAttr dim : dims) {
298 product *= dim.getSize();
299 }
300 elements += product;
301 } else {
302 elements += segment.getSize();
303 }
304 }
305 return elements;
306 }
307
308 /// A fan-in's destination fills its next object with whatever packet
309 /// arrives, and a packet is one pass of its sender's chain, so every end has
310 /// to move the same number of elements or two senders share an object. A
311 /// route endpoint's transfers are the runtime's and are not checked here.
312 LogicalResult verifyFanIn(DeviceOp device,
313 SymbolTableCollection &symbolTables) {
314 for (auto flow : device.getOps<RouteOp>()) {
315 if (!flow.isFanIn()) {
316 continue;
317 }
318 std::optional<std::pair<StringRef, int64_t>> first;
319 auto check = [&](StringRef name) -> LogicalResult {
320 auto endpoint = symbolTables.lookupSymbolIn<ObjectFifoDmaEndpointOp>(
321 device, StringAttr::get(device.getContext(), name));
322 if (!endpoint) {
323 return success();
324 }
325 int64_t elements = transferElements(endpoint);
326 if (!first) {
327 first = {name, elements};
328 return success();
329 }
330 if (elements != first->second) {
331 return flow.emitOpError("fans in, so every end moves one object of "
332 "the same size, but @")
333 << first->first << " moves " << first->second
334 << " elements and @" << name << " moves " << elements;
335 }
336 return success();
337 };
338 for (StringRef name : flow.getSourceNames()) {
339 if (failed(check(name))) {
340 return failure();
341 }
342 }
343 for (StringRef name : flow.getDestinationNames()) {
344 if (failed(check(name))) {
345 return failure();
346 }
347 }
348 }
349 return success();
350 }
351
352 /// A loop body that releases more than it acquires underflows the held count
353 /// as it repeats, whatever the trip count. Accesses split across nesting
354 /// levels are excluded: balancing them needs the inner trip counts. This runs
355 /// before lowering because afterwards the imbalance survives only as
356 /// loop-carried lock values, which no scan can decide.
357 LogicalResult verifyOverRelease(DeviceOp device) {
358 for (auto coreOp : device.getOps<CoreOp>()) {
359 WalkResult result = coreOp.walk([&](Operation *loop) {
360 if (!isObjectFifoLoop(loop)) {
361 return WalkResult::advance();
362 }
363 auto directlyIn = [&](Operation *op) {
364 return getObjectFifoLoopParent(op) == loop;
365 };
366 DenseMap<StringRef, int64_t> acquired;
367 DenseMap<StringRef, int64_t> released;
368 DenseMap<StringRef, Operation *> blame;
369 DenseSet<StringRef> spansNestedLoop;
370
371 loop->walk([&](ObjectFifoAcquireOp a) {
372 StringRef key = a.getObjFifoName();
373 if (!directlyIn(a)) {
374 spansNestedLoop.insert(key);
375 } else {
376 acquired[key] += a.acqNumber();
377 blame.try_emplace(key, a);
378 }
379 });
380 loop->walk([&](ObjectFifoReleaseOp r) {
381 StringRef key = r.getObjFifoName();
382 if (!directlyIn(r)) {
383 spansNestedLoop.insert(key);
384 } else {
385 released[key] += r.relNumber();
386 blame.try_emplace(key, r);
387 }
388 });
389
390 for (auto &[key, count] : released) {
391 if (spansNestedLoop.contains(key) || count <= acquired.lookup(key)) {
392 continue;
393 }
394 blame.lookup(key)->emitOpError(
395 "cannot release more elements than are already acquired");
396 return WalkResult::interrupt();
397 }
398 return WalkResult::advance();
399 });
400 if (result.wasInterrupted()) {
401 return failure();
402 }
403 }
404 return success();
405 }
406
407 /// A DMA endpoint with an iteration count stops after `iterCount * depth`
408 /// objects, so a core that must release more through the same pool waits
409 /// forever for the rest, or leaves objects it filled unsent. Only
410 /// releases static loop bounds force are counted, so what is reported is
411 /// certain; a conditional release, or one in a loop meant to run forever, is
412 /// left out, since such a core stalling once the data stops is how those
413 /// designs end. When all of a core's releases are counted, the DMA endpoint
414 /// is bounded too: it cannot drain more than the core hands over, nor fill
415 /// more than the pool's depth plus what the core releases.
416 LogicalResult verifyTransferBudgets(DeviceOp device) {
417 DenseMap<Operation *, TransferBudget> budgets;
418 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
419 std::optional<int32_t> iterations = endpoint.getIterCount();
420 ObjectFifoPoolOp pool = endpoint.getPoolOp();
421 if (!iterations || !pool) {
422 continue;
423 }
424 budgets[pool] = {endpoint, *iterations,
425 int64_t{*iterations} * pool.getDepth()};
426 }
427 if (budgets.empty()) {
428 return success();
429 }
430
431 DenseMap<StringRef, int64_t> released;
432 DenseMap<StringRef, ObjectFifoReleaseOp> blame;
433 DenseSet<StringRef> uncounted;
434 device.walk([&](ObjectFifoReleaseOp release) {
435 StringRef key = release.getObjFifoName();
436 std::optional<int64_t> runs = staticExecutions(release);
437 if (!runs) {
438 uncounted.insert(key);
439 return;
440 }
441 released[key] += *runs * release.relNumber();
442 blame.try_emplace(key, release);
443 });
444 DenseSet<StringRef> acquired;
445 device.walk([&](ObjectFifoAcquireOp acquire) {
446 acquired.insert(acquire.getObjFifoName());
447 });
448
449 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
450 // A segment-selecting endpoint sees a share of the pool, not the whole.
451 auto budget = budgets.find(endpoint.getPoolOp());
452 if (budget == budgets.end() || endpoint.getSegments()) {
453 continue;
454 }
455 StringRef name = endpoint.getSymName();
456 int64_t count = released.lookup(name);
457 ObjectFifoDmaEndpointOp dma = budget->second.endpoint;
458 ObjectFifoPoolOp pool = endpoint.getPoolOp();
459 int64_t depth = pool.getDepth();
460 if (count > budget->second.objects) {
461 InFlightDiagnostic diag =
462 blame.lookup(name).emitOpError("releases ")
463 << count << " objects through @" << name
464 << " over the run, but the pool's DMA endpoint stops after "
465 << budget->second.objects << " (iterCount "
466 << budget->second.iterations << " x depth " << depth << ")";
467 // A producer finishes while the pool still has free objects to fill.
468 if (!endpoint.drains() && count <= budget->second.objects + depth)
469 diag << ", leaving " << count - budget->second.objects
470 << " in the pool unsent";
471 else
472 diag << ", so the core would wait forever for the rest";
473 diag.attachNote(dma.getLoc()) << "the DMA endpoint is here";
474 return failure();
475 }
476
477 if (!acquired.contains(name) || uncounted.contains(name) ||
478 dma.getSegments() || pool.getInitValues() ||
479 dma.drains() == endpoint.drains() || !poolLocks->needsLocks(pool)) {
480 continue;
481 }
482 int64_t reachable = endpoint.drains() ? count + depth : count;
483 if (budget->second.objects <= reachable) {
484 continue;
485 }
486 InFlightDiagnostic diag =
487 dma.emitOpError(dma.drains() ? "drains " : "fills ")
488 << budget->second.objects << " objects (iterCount "
489 << budget->second.iterations << " x depth " << depth
490 << ") before it stops, but the core releases exactly " << count
491 << " through @" << name;
492 if (endpoint.drains())
493 diag << ", leaving room for only " << reachable;
494 diag << ", so the DMA endpoint would wait forever for the rest";
495 diag.attachNote(endpoint.getLoc()) << "the core endpoint is here";
496 return failure();
497 }
498 return success();
499 }
500
501 /// A core that reaches its end still holding objects never gives their locks
502 /// back, and locks keep their counts until a PDI load, so the next run of
503 /// the configuration finds those objects taken. Only cores certain to end
504 /// are checked: one block, and no loop that may run forever.
505 LogicalResult verifyHeldAtEnd(DeviceOp device) {
506 for (auto core : device.getOps<CoreOp>()) {
507 Region &body = core.getBody();
508 if (!body.hasOneBlock())
509 continue;
510 bool mayNotEnd =
511 core.walk([](Operation *op) {
512 auto forOp = dyn_cast<scf::ForOp>(op);
513 std::optional<int64_t> trip =
514 forOp ? getStaticTripCount(forOp) : std::nullopt;
515 if (isa<scf::WhileOp>(op) || (trip && *trip >= kRunsForever))
516 return WalkResult::interrupt();
517 return WalkResult::advance();
518 })
519 .wasInterrupted();
520 if (mayNotEnd)
521 continue;
522 llvm::MapVector<StringRef, ObjectFifoAcquireOp> firstAcquire;
523 core.walk([&](ObjectFifoAcquireOp acquire) {
524 firstAcquire.insert({acquire.getObjFifoName(), acquire});
525 });
526 for (auto &[name, acquire] : firstAcquire) {
527 auto endpoint =
528 SymbolTable::lookupNearestSymbolFrom<ObjectFifoCoreEndpointOp>(
529 device, acquire.getObjFifoNameAttr());
530 if (endpoint && endpoint.getPoolOp() &&
531 !poolLocks->needsLocks(endpoint.getPoolOp()))
532 continue;
533 std::optional<int64_t> held = heldAfter(body.front(), name, 0);
534 if (!held || *held == 0)
535 continue;
536 InFlightDiagnostic diag =
537 core.emitOpError("ends with ")
538 << *held << " acquired from @" << name
539 << " never released, so their locks stay taken into the next run "
540 "of this configuration";
541 diag.attachNote(acquire.getLoc()) << "acquired here";
542 return failure();
543 }
544 }
545 return success();
546 }
547
548 /// A core whose loop repeats forever, and whose every repetition acquires
549 /// objects of a pool but releases none, keeps the same objects for good. The
550 /// end opposite it then stops once the pool is full, if it does not stop by
551 /// itself first: checked where that end is a DMA endpoint without an
552 /// iteration count, or a core that moves objects each time its own loop
553 /// repeats forever. A core holding an object acquired before its loop, for
554 /// the whole configuration, is left alone.
555 LogicalResult verifyAdvances(DeviceOp device) {
556 auto repeatsForever = [](Operation *loop) {
557 for (Operation *op = loop; op && !isa<CoreOp>(op);
558 op = op->getParentOp()) {
559 auto whileOp = dyn_cast<scf::WhileOp>(op);
560 auto forOp = dyn_cast<scf::ForOp>(op);
561 std::optional<int64_t> trip =
562 forOp ? getStaticTripCount(forOp) : std::nullopt;
563 if ((whileOp && isForeverLoop(whileOp)) ||
564 (trip && *trip >= kRunsForever))
565 return true;
566 }
567 return false;
568 };
569
570 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
571 ObjectFifoPoolOp pool = endpoint.getPoolOp();
572 if (!pool || pool.getInitValues() || endpoint.getSegments() ||
573 !poolLocks->needsLocks(pool))
574 continue;
575 Operation *keepsMoving = nullptr;
576 for (auto dma : device.getOps<ObjectFifoDmaEndpointOp>())
577 if (dma.getPoolOp() == pool && dma.drains() != endpoint.drains() &&
578 !dma.getIterCount() && !dma.getSegments())
579 keepsMoving = dma;
580 for (auto other : device.getOps<ObjectFifoCoreEndpointOp>()) {
581 if (keepsMoving || other.getPoolOp() != pool ||
582 other.drains() == endpoint.drains() || other.getSegments())
583 continue;
584 CoreTurns otherTurns;
585 std::string otherReason;
586 if (succeeded(otherTurns.read({other}, otherReason)) &&
587 repeatsForever(otherTurns.periodLoop()))
588 keepsMoving = other;
589 }
590 if (!keepsMoving)
591 continue;
592
593 CoreTurns turns;
594 std::string reason;
595 if (succeeded(turns.read({endpoint}, reason)) || !turns.movesNone() ||
596 !repeatsForever(turns.periodLoop()))
597 continue;
598 auto acquire =
599 dyn_cast_or_null<ObjectFifoAcquireOp>(turns.periodAcquire());
600 if (!acquire)
601 continue;
602 InFlightDiagnostic diag =
603 acquire.emitOpError("takes from @")
604 << endpoint.getSymName()
605 << " each time its loop repeats, but the loop releases none of "
606 "them, so ";
607 if (endpoint.drains())
608 diag << "the core keeps the same objects forever and the end "
609 "filling the pool stops once it is full";
610 else
611 diag << "nothing the core writes there is ever sent";
612 diag.attachNote(keepsMoving->getLoc()) << "the other end is here";
613 return failure();
614 }
615 return success();
616 }
617
618 /// Bytes one run of `sequence` moves through the shim endpoint `name`, when
619 /// every use of it is a DMA task whose executions, repeats and lengths are
620 /// static.
621 static std::optional<int64_t> hostBytes(RuntimeSequenceOp sequence,
622 StringAttr name) {
623 std::optional<SymbolTable::UseRange> uses =
624 SymbolTable::getSymbolUses(name, &sequence.getBody());
625 if (!uses)
626 return std::nullopt;
627 int64_t bytes = 0;
628 for (const SymbolTable::SymbolUse &use : *uses) {
629 auto task = dyn_cast<AIEX::DMAConfigureTaskForOp>(use.getUser());
630 std::optional<int64_t> runs =
631 task ? staticExecutions(task) : std::nullopt;
632 if (!runs || task.getRepeatCountVal())
633 return std::nullopt;
634 int64_t perRun = 0;
635 WalkResult result = task.getBody().walk([&](DMABDOp bd) {
636 if (bd.getLengthParameter() || !bd.getConstantLen())
637 return WalkResult::interrupt();
638 perRun += bd.getLenInBytes();
639 return WalkResult::advance();
640 });
641 if (result.wasInterrupted())
642 return std::nullopt;
643 bytes += *runs * (task.getRepeatCount() + 1) * perRun;
644 }
645 return bytes;
646 }
647
648 /// Bytes one object of `endpoint` carries over its channel.
649 static int64_t objectBytes(ObjectFifoDmaEndpointOp endpoint) {
650 auto type = cast<MemRefType>(endpoint.getPoolOp().getElemType());
651 return transferElements(endpoint) * type.getElementTypeBitWidth() / 8;
652 }
653
654 /// Objects the DMA endpoint `endpoint` moves in one run of `sequence`, traced
655 /// to the shim directly or through one pool with one DMA endpoint toward the
656 /// shim.
657 std::optional<int64_t> hostObjects(DeviceOp device,
658 RuntimeSequenceOp sequence,
659 ObjectFifoDmaEndpointOp endpoint,
660 bool throughPool) {
661 if (endpoint.getIterCount() || endpoint.getDispatch())
662 return std::nullopt;
663 StringRef name = endpoint.getSymName();
664 SmallVector<StringRef> peers;
665 for (auto route : device.getOps<RouteOp>()) {
666 if (endpoint.drains() ? route.namesAsSource(name)
667 : route.namesAsDestination(name)) {
668 auto names = endpoint.drains() ? route.getDestinationNames()
669 : route.getSourceNames();
670 peers.append(names.begin(), names.end());
671 }
672 }
673 if (peers.size() != 1)
674 return std::nullopt;
675 Operation *peer = SymbolTable::lookupNearestSymbolFrom(
676 device, StringAttr::get(device.getContext(), peers.front()));
677 // A draining endpoint sends each object `getRepeat` times.
678 int64_t sent = objectBytes(endpoint) * endpoint.getRepeat();
679 int64_t perObject = endpoint.drains() ? sent : objectBytes(endpoint);
680
681 int64_t bytes;
682 if (auto shim = dyn_cast_or_null<RouteEndpointOp>(peer)) {
683 std::optional<int64_t> host = hostBytes(sequence, shim.getSymNameAttr());
684 if (!host)
685 return std::nullopt;
686 bytes = *host;
687 } else if (auto other = dyn_cast_or_null<ObjectFifoDmaEndpointOp>(peer);
688 other && throughPool && other.drains() != endpoint.drains()) {
689 ObjectFifoDmaEndpointOp toward;
690 for (auto candidate : device.getOps<ObjectFifoDmaEndpointOp>()) {
691 if (candidate == other || candidate.getPoolOp() != other.getPoolOp() ||
692 candidate.drains() == other.drains())
693 continue;
694 if (toward)
695 return std::nullopt;
696 toward = candidate;
697 }
698 if (!toward)
699 return std::nullopt;
700 std::optional<int64_t> objects =
701 hostObjects(device, sequence, toward, false);
702 if (!objects)
703 return std::nullopt;
704 bytes =
705 *objects * (other.drains() ? objectBytes(other) * other.getRepeat()
706 : objectBytes(other));
707 } else {
708 return std::nullopt;
709 }
710 if (perObject == 0 || bytes % perObject)
711 return std::nullopt;
712 return bytes / perObject;
713 }
714
715 /// A Worker whose forever loop waits for each dispatch's parameters runs
716 /// one pass of that loop per dispatch, so the objects one pass moves through
717 /// a fifo must be the objects one run of the runtime sequence moves through
718 /// it, or the core drifts out of step with the dispatches. Checked when one
719 /// runtime sequence syncs the parameters, so every dispatch runs it, and its
720 /// transfers on the fifo are static.
721 LogicalResult verifyDispatchVolumes(DeviceOp device) {
722 RuntimeSequenceOp sequence;
723 int64_t syncs = 0;
724 for (auto candidate : device.getOps<RuntimeSequenceOp>()) {
725 auto ops = candidate.getBody()
726 .getOps<AIEX::SyncScratchpadParametersFromHostOp>();
727 int64_t count = std::distance(ops.begin(), ops.end());
728 // With a core waiting, --aie-lower-scratchpad-parameters syncs once in a
729 // sequence that has no sync of its own, unless the sequence opts out.
730 if (count == 0) {
731 BoolAttr preamble = candidate.getEmitParameterSyncPreambleAttr();
732 count = !preamble || preamble.getValue();
733 }
734 if (count == 0)
735 continue;
736 if (sequence)
737 return success();
738 sequence = candidate;
739 syncs = count;
740 }
741 if (!sequence)
742 return success();
743
744 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
745 ObjectFifoPoolOp pool = endpoint.getPoolOp();
746 if (!pool || pool.getInitValues() || endpoint.getSegments() ||
747 !poolLocks->needsLocks(pool))
748 continue;
749 ObjectFifoDmaEndpointOp dma;
750 bool single = true;
751 for (auto candidate : device.getOps<ObjectFifoDmaEndpointOp>()) {
752 if (candidate.getPoolOp() != pool ||
753 candidate.drains() == endpoint.drains())
754 continue;
755 single = !dma;
756 dma = candidate;
757 }
758 if (!dma || !single || dma.getSegments())
759 continue;
760
761 CoreTurns turns;
762 std::string reason;
763 if (failed(turns.read({endpoint}, reason)))
764 continue;
765 auto loop = dyn_cast<scf::WhileOp>(turns.periodLoop());
766 if (!loop || !isForeverLoop(loop) ||
767 loop.getAfterBody()
768 ->getOps<AIEX::WaitScratchpadParametersOp>()
769 .empty())
770 continue;
771 std::optional<int64_t> objects = hostObjects(device, sequence, dma, true);
772 int64_t perDispatch = syncs * static_cast<int64_t>(turns.turns().size());
773 LLVM_DEBUG(llvm::dbgs()
774 << "dispatch volume of @" << endpoint.getSymName()
775 << ": the core moves " << perDispatch << ", @"
776 << sequence.getSymName() << " moves "
777 << (objects ? std::to_string(*objects) : "an unknown count")
778 << "\n");
779 if (!objects || *objects == perDispatch)
780 continue;
781
782 ObjectFifoAcquireOp acquire;
783 loop.walk([&](ObjectFifoAcquireOp op) {
784 if (!acquire && op.getObjFifoName() == endpoint.getSymName())
785 acquire = op;
786 });
787 if (!acquire)
788 continue;
789 InFlightDiagnostic diag =
790 acquire.emitOpError()
791 << (endpoint.drains() ? "takes " : "hands over ") << perDispatch
792 << " objects through @" << endpoint.getSymName()
793 << " for each dispatch, one pass of a loop that waits for each "
794 "dispatch's parameters";
795 if (syncs > 1)
796 diag << " (synced " << syncs << " times per run)";
797 diag << ", but each run of runtime sequence @" << sequence.getSymName()
798 << " moves " << *objects
799 << ", so the core drifts out of step with the dispatches";
800 diag.attachNote(sequence.getLoc()) << "the runtime sequence is here";
801 return failure();
802 }
803 return success();
804 }
805
806 /// An out-of-order sender names the buffer each object lands in, and one BD
807 /// carries one such name, so the ends have to hold the same buffers in the
808 /// same order. Naming a slice of a larger buffer would need the sender split
809 /// into one descriptor per slice, which nothing does yet.
810 LogicalResult verifyOutOfOrderPools(DeviceOp device) {
811 auto lookup = [&](FlatSymbolRefAttr name) {
812 return dyn_cast_or_null<ObjectFifoDmaEndpointOp>(
813 SymbolTable::lookupNearestSymbolFrom(device, name.getAttr()));
814 };
815 auto objectBytes = [](ObjectFifoPoolOp pool) -> int64_t {
816 auto type = dyn_cast<MemRefType>(pool.getElemType());
817 if (!type)
818 return 0;
819 return (type.getNumElements() * type.getElementTypeBitWidth() + 7) / 8;
820 };
821 for (auto flow : device.getOps<RouteOp>()) {
822 if (flow.getSources().size() != 1)
823 continue;
824 ObjectFifoDmaEndpointOp src =
825 lookup(cast<FlatSymbolRefAttr>(flow.getSources()[0]));
826 ObjectFifoDmaEndpointOp firstReceiver;
827 for (auto destName :
828 flow.getDestinations().getAsRange<FlatSymbolRefAttr>()) {
829 ObjectFifoDmaEndpointOp dst = lookup(destName);
830 if (!dst || !dst.getOutOfOrder())
831 continue;
832 if (!firstReceiver)
833 firstReceiver = dst;
834 if (firstReceiver.getNumDescriptors() != dst.getNumDescriptors() ||
835 objectBytes(firstReceiver.getPoolOp()) !=
836 objectBytes(dst.getPoolOp()))
837 return flow.emitOpError(
838 "out-of-order broadcast destinations require matching buffer "
839 "counts and object sizes");
840 if (!src || !src.getOutOfOrder())
841 continue;
842 ObjectFifoPoolOp from = src.getPoolOp(), to = dst.getPoolOp();
843 if (!from || !to)
844 continue;
845 if (src.getNumDescriptors() != dst.getNumDescriptors() ||
846 objectBytes(from) != objectBytes(to))
847 return src.emitOpError("sends out of order from ")
848 << src.getNumDescriptors() << " buffers of "
849 << objectBytes(from) << " bytes into "
850 << dst.getNumDescriptors() << " of " << objectBytes(to)
851 << "; an out-of-order fifo needs the same buffers at both "
852 "ends, because one descriptor names one whole buffer";
853 }
854 }
855 return success();
856 }
857
858 void runOnOperation() override {
859 DeviceOp device = getOperation();
860 poolLocks = std::make_shared<PoolLocks>(device);
861
862 DenseMap<Operation *, SmallVector<SegmentActors>> actorsPerPool;
863 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
864 actorsPerPool[pool].resize(pool.getSegmentOps().size());
865 }
866
867 auto record = [&](auto endpoint) {
868 ObjectFifoPoolOp pool = endpoint.getPoolOp();
869 if (!pool) {
870 return;
871 }
872 auto &actors = actorsPerPool[pool];
873 std::vector<ObjectFifoSegmentOp> all = pool.getSegmentOps();
874 for (ObjectFifoSegmentOp segment : endpoint.getSelectedSegments()) {
875 size_t index = llvm::find(all, segment) - all.begin();
876 (endpoint.drains() ? actors[index].drainers : actors[index].fillers)
877 .push_back(endpoint);
878 }
879 };
880
881 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
882 record(endpoint);
883 }
884 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
885 record(endpoint);
886 }
887
888 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
889 if (failed(verifyActors(pool, actorsPerPool[pool]))) {
890 return signalPassFailure();
891 }
892 }
893
894 SymbolTableCollection symbolTables;
895 if (failed(verifyFlows(device, symbolTables)) ||
896 failed(verifyFanIn(device, symbolTables)) ||
897 failed(verifyOverRelease(device)) ||
898 failed(verifyTransferBudgets(device)) ||
899 failed(verifyHeldAtEnd(device)) || failed(verifyAdvances(device)) ||
900 failed(verifyDispatchVolumes(device)) ||
901 failed(verifyOutOfOrderPools(device))) {
902 return signalPassFailure();
903 }
904 }
905};
906
907} // namespace
908
909std::unique_ptr<OperationPass<DeviceOp>>
911 return std::make_unique<AIEObjectFifoVerifyPass>();
912}
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 movesNone() const
Whether read found a steady state in which the period moves none of the objects: the core keeps what ...
mlir::Operation * periodAcquire() const
The first acquire the period runs, with its branches folded; null when it runs none.
llvm::ArrayRef< unsigned > turns() const
Indices into ends, one per object the channel moves in a period of the core's steady state.
mlir::Operation * periodLoop() const
The loop read took as the core's period, once it found one.
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEObjectFifoVerifyPass()
bool isForeverLoop(mlir::scf::WhileOp whileOp)
Whether whileOp repeats forever: its condition is a constant true, and its condition region does noth...
mlir::Operation * getObjectFifoLoopParent(mlir::Operation *op)
The innermost loop around op that isObjectFifoLoop accepts, or null.
bool isObjectFifoLoop(mlir::Operation *op)
Whether op is a loop that objectFifo lowering unrolls by the rotation period of the objectFifos acces...