MLIR-AIE 1.4.5.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
10
11#include "mlir/Dialect/SCF/IR/SCF.h"
12#include "mlir/Pass/Pass.h"
13#include "llvm/ADT/SetVector.h"
14
15#include <limits>
16#include <optional>
17
18using namespace mlir;
19using namespace xilinx;
20using namespace xilinx::AIE;
21
22namespace xilinx::AIE {
23#define GEN_PASS_DEF_AIEOBJECTFIFOVERIFY
24#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
25} // namespace xilinx::AIE
26
27namespace {
28
29struct SegmentActors {
30 SmallVector<Operation *> fillers;
31 SmallVector<Operation *> drainers;
32};
33
34/// A DMA endpoint with an iteration count moves this many objects through its
35/// pool, then stops.
36struct TransferBudget {
37 ObjectFifoDmaEndpointOp endpoint;
38 int32_t iterations;
39 int64_t objects;
40};
41
42/// A loop the frontend means to run forever has a bound no design counts to:
43/// IRON writes sys.maxsize, hand-written designs 2^32 - 1. From the AIE's
44/// 24-bit BD-loop saturation value up, a trip count is read as "forever"; below
45/// it, as a real bound.
46static constexpr int64_t kRunsForever = (int64_t{1} << 24) - 2;
47
48static std::optional<int64_t> getStaticTripCount(scf::ForOp forOp) {
49 if (forOp.getSingleLowerBound() && forOp.getSingleUpperBound() &&
50 forOp.getSingleStep()) {
51 if (std::optional<llvm::APInt> tc = forOp.getStaticTripCount()) {
52 return tc->getSExtValue();
53 }
54 }
55 return std::nullopt;
56}
57
58/// How many times `op` runs when its core runs to completion, when static loop
59/// bounds alone decide that. Anything conditional between the op and the core,
60/// an `scf.if`, a loop with dynamic bounds or one meant to run forever, a block
61/// reached by branching, makes it unknown.
62static std::optional<int64_t> staticExecutions(Operation *op) {
63 int64_t count = 1;
64 for (Operation *cur = op; !isa<CoreOp>(cur); cur = cur->getParentOp()) {
65 if (cur->getBlock() != &cur->getParentRegion()->front()) {
66 return std::nullopt;
67 }
68 Operation *parent = cur->getParentOp();
69 if (isa<CoreOp>(parent)) {
70 break;
71 }
72 auto forOp = dyn_cast<scf::ForOp>(parent);
73 if (!forOp) {
74 return std::nullopt;
75 }
76 std::optional<int64_t> trip = getStaticTripCount(forOp);
77 if (!trip || *trip >= kRunsForever ||
78 (*trip != 0 && count > std::numeric_limits<int64_t>::max() / *trip)) {
79 return std::nullopt;
80 }
81 count *= *trip;
82 }
83 return count;
84}
85
86struct AIEObjectFifoVerifyPass
87 : public xilinx::AIE::impl::AIEObjectFifoVerifyBase<
88 AIEObjectFifoVerifyPass> {
89
90 /// Every segment needs an actor at each end, so that what one writes another
91 /// reads. A segment may go unfilled when the pool's objects start full, and
92 /// may name no endpoint at an end its `manualFill` or `manualDrain` hands to
93 /// a DMA program the author writes.
94 LogicalResult verifyActors(ObjectFifoPoolOp pool,
95 ArrayRef<SegmentActors> actors) {
96 bool external = llvm::any_of(pool.getBufferOps(), [](BufferLike buffer) {
97 return isa<ExternalBufferOp>(buffer.getOperation());
98 });
99 std::vector<ObjectFifoSegmentOp> segmentOps = pool.getSegmentOps();
100 for (auto [index, segment] : llvm::enumerate(actors)) {
101 ObjectFifoSegmentOp segmentOp = segmentOps[index];
102 bool manualFill = segmentOp.getManualFill();
103 bool manualDrain = segmentOp.getManualDrain();
104 if (segment.fillers.size() > 1 ||
105 (manualFill && !segment.fillers.empty())) {
106 return pool.emitOpError("segment ")
107 << index << " is filled by more than one endpoint";
108 }
109 if (segment.drainers.size() > 1 ||
110 (manualDrain && !segment.drainers.empty())) {
111 return pool.emitOpError("segment ")
112 << index << " is drained by more than one endpoint";
113 }
114 // The host works an external pool's buffers, so it stands in for the end
115 // this device does not program.
116 bool hostDrains = external && !segment.fillers.empty();
117 bool hostFills = external && !segment.drainers.empty();
118 if (segment.drainers.empty() && !hostDrains && !manualDrain) {
119 return pool.emitOpError("segment ") << index << " has no drainer";
120 }
121 if (segment.fillers.empty() && !pool.getInitValues() && !hostFills &&
122 !manualFill) {
123 return pool.emitOpError("segment ") << index << " has no filler";
124 }
125 }
126 return success();
127 }
128
129 /// An endpoint that is not connected carries data nowhere.
130 LogicalResult verifyFlows(DeviceOp device,
131 SymbolTableCollection &symbolTables) {
132 DenseMap<StringRef, int> appearances;
133 for (auto flow : device.getOps<RouteOp>()) {
134 for (StringRef source : flow.getSourceNames()) {
135 appearances[source]++;
136 }
137 for (StringRef dest : flow.getDestinationNames()) {
138 appearances[dest]++;
139 }
140 }
141
142 for (auto endpoint : device.getOps<RouteEndpoint>()) {
143 int count =
144 appearances.lookup(cast<SymbolOpInterface>(*endpoint).getName());
145 if (count == 0) {
146 return endpoint->emitOpError("is not connected by any flow");
147 }
148 if (auto dma = dyn_cast<ObjectFifoDmaEndpointOp>(*endpoint);
149 dma && dma.getDispatch()) {
150 if (failed(verifyDispatch(device, dma, symbolTables)))
151 return failure();
152 continue;
153 }
154 // An aie.route_endpoint's own verifier says which flows may share it.
155 if (count > 1 && !isa<RouteEndpointOp>(endpoint.getOperation())) {
156 return endpoint->emitOpError(
157 "drives one channel, so at most one flow may name it, but "
158 "it is named ")
159 << count << " times";
160 }
161 }
162 return success();
163 }
164
165 /// A dispatching endpoint is the one source of one packet-switched route per
166 /// destination it takes turns between, and of nothing else. A second route to
167 /// the same destination is refused there, by the count every endpoint gets.
168 LogicalResult verifyDispatch(DeviceOp device,
169 ObjectFifoDmaEndpointOp endpoint,
170 SymbolTableCollection &symbolTables) {
171 StringRef name = endpoint.getSymName();
172 llvm::SmallSetVector<StringRef, 4> turns;
173 for (StringRef dest :
174 endpoint.getDispatchAttr().getAsValueRange<FlatSymbolRefAttr>())
175 turns.insert(dest);
176 llvm::SmallDenseSet<StringRef, 4> reached;
177 for (auto flow : device.getOps<RouteOp>()) {
178 if (flow.namesAsDestination(name))
179 return flow.emitOpError("names dispatching endpoint @")
180 << name << " as a destination";
181 if (!flow.namesAsSource(name))
182 continue;
183 if (flow.getSources().size() != 1 || flow.getDestinations().size() != 1)
184 return flow.emitOpError("leaves dispatching endpoint @")
185 << name
186 << " for one destination, so it has one source and "
187 "one destination";
188 if (!flow.getPacket())
189 return flow.emitOpError("leaves dispatching endpoint @")
190 << name << " and needs a packet header to tell its turns apart";
191 StringRef dest = *flow.getDestinationNames().begin();
192 if (!turns.contains(dest))
193 return flow.emitOpError("reaches @")
194 << dest << ", which dispatching endpoint @" << name
195 << " never takes a turn for";
196 reached.insert(dest);
197 }
198 for (StringRef dest : turns)
199 if (!reached.contains(dest))
200 return endpoint.emitOpError("dispatches to @")
201 << dest << ", but no route from it reaches there";
202
203 // Every turn moves the same chain through the dispatching endpoint, one
204 // object per turn, so each turn's destination must move as many elements
205 // as the dispatching endpoint itself does.
206 int64_t elements = transferElements(endpoint);
207 for (StringRef dest : turns) {
208 auto destEndpoint = symbolTables.lookupSymbolIn<ObjectFifoDmaEndpointOp>(
209 device, StringAttr::get(device.getContext(), dest));
210 if (!destEndpoint)
211 continue;
212 int64_t destElements = transferElements(destEndpoint);
213 if (destElements != elements)
214 return endpoint.emitOpError("dispatches, so every turn moves ")
215 << elements << " elements, but @" << dest << " moves "
216 << destElements;
217 }
218 return success();
219 }
220
221 /// The elements one pass of an endpoint's chain moves: each selected segment
222 /// once, through its transform where it has one.
223 static int64_t transferElements(ObjectFifoDmaEndpointOp endpoint) {
224 int64_t elements = 0;
225 std::optional<ArrayRef<BDDimLayoutArrayAttr>> dimensions =
226 endpoint.getDimensions();
227 for (auto [index, segment] :
228 llvm::enumerate(endpoint.getSelectedSegments())) {
229 BDDimLayoutArrayAttr dims;
230 if (dimensions && index < dimensions->size()) {
231 dims = (*dimensions)[index];
232 }
233 if (dims && !dims.empty()) {
234 int64_t product = 1;
235 for (BDDimLayoutAttr dim : dims) {
236 product *= dim.getSize();
237 }
238 elements += product;
239 } else {
240 elements += segment.getSize();
241 }
242 }
243 return elements;
244 }
245
246 /// A fan-in's destination fills its next object with whatever packet
247 /// arrives, and a packet is one pass of its sender's chain, so every end has
248 /// to move the same number of elements or two senders share an object. A
249 /// route endpoint's transfers are the runtime's and are not checked here.
250 LogicalResult verifyFanIn(DeviceOp device,
251 SymbolTableCollection &symbolTables) {
252 for (auto flow : device.getOps<RouteOp>()) {
253 if (!flow.isFanIn()) {
254 continue;
255 }
256 std::optional<std::pair<StringRef, int64_t>> first;
257 auto check = [&](StringRef name) -> LogicalResult {
258 auto endpoint = symbolTables.lookupSymbolIn<ObjectFifoDmaEndpointOp>(
259 device, StringAttr::get(device.getContext(), name));
260 if (!endpoint) {
261 return success();
262 }
263 int64_t elements = transferElements(endpoint);
264 if (!first) {
265 first = {name, elements};
266 return success();
267 }
268 if (elements != first->second) {
269 return flow.emitOpError("fans in, so every end moves one object of "
270 "the same size, but @")
271 << first->first << " moves " << first->second
272 << " elements and @" << name << " moves " << elements;
273 }
274 return success();
275 };
276 for (StringRef name : flow.getSourceNames()) {
277 if (failed(check(name))) {
278 return failure();
279 }
280 }
281 for (StringRef name : flow.getDestinationNames()) {
282 if (failed(check(name))) {
283 return failure();
284 }
285 }
286 }
287 return success();
288 }
289
290 /// A loop body that releases more than it acquires underflows the held count
291 /// as it repeats, whatever the trip count. Accesses split across nesting
292 /// levels are excluded: balancing them needs the inner trip counts. This runs
293 /// before lowering because afterwards the imbalance survives only as
294 /// loop-carried lock values, which no scan can decide.
295 LogicalResult verifyOverRelease(DeviceOp device) {
296 for (auto coreOp : device.getOps<CoreOp>()) {
297 WalkResult result = coreOp.walk([&](Operation *loop) {
298 if (!isObjectFifoLoop(loop)) {
299 return WalkResult::advance();
300 }
301 auto directlyIn = [&](Operation *op) {
302 return getObjectFifoLoopParent(op) == loop;
303 };
304 DenseMap<StringRef, int64_t> acquired;
305 DenseMap<StringRef, int64_t> released;
306 DenseMap<StringRef, Operation *> blame;
307 DenseSet<StringRef> spansNestedLoop;
308
309 loop->walk([&](ObjectFifoAcquireOp a) {
310 StringRef key = a.getObjFifoName();
311 if (!directlyIn(a)) {
312 spansNestedLoop.insert(key);
313 } else {
314 acquired[key] += a.acqNumber();
315 blame.try_emplace(key, a);
316 }
317 });
318 loop->walk([&](ObjectFifoReleaseOp r) {
319 StringRef key = r.getObjFifoName();
320 if (!directlyIn(r)) {
321 spansNestedLoop.insert(key);
322 } else {
323 released[key] += r.relNumber();
324 blame.try_emplace(key, r);
325 }
326 });
327
328 for (auto &[key, count] : released) {
329 if (spansNestedLoop.contains(key) || count <= acquired.lookup(key)) {
330 continue;
331 }
332 blame.lookup(key)->emitOpError(
333 "cannot release more elements than are already acquired");
334 return WalkResult::interrupt();
335 }
336 return WalkResult::advance();
337 });
338 if (result.wasInterrupted()) {
339 return failure();
340 }
341 }
342 return success();
343 }
344
345 /// A DMA endpoint with an iteration count stops after `iterCount * depth`
346 /// objects, so a core that must release more through the same pool waits
347 /// forever for the rest. Only releases static loop bounds force are counted,
348 /// so what is reported is certain; a conditional release, or one in a loop
349 /// meant to run forever, is left out, since such a core stalling once the
350 /// data stops is how those designs end.
351 LogicalResult verifyTransferBudgets(DeviceOp device) {
352 DenseMap<Operation *, TransferBudget> budgets;
353 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
354 std::optional<int32_t> iterations = endpoint.getIterCount();
355 ObjectFifoPoolOp pool = endpoint.getPoolOp();
356 if (!iterations || !pool) {
357 continue;
358 }
359 budgets[pool] = {endpoint, *iterations,
360 int64_t{*iterations} * pool.getDepth()};
361 }
362 if (budgets.empty()) {
363 return success();
364 }
365
366 DenseMap<StringRef, int64_t> released;
367 DenseMap<StringRef, ObjectFifoReleaseOp> blame;
368 device.walk([&](ObjectFifoReleaseOp release) {
369 std::optional<int64_t> runs = staticExecutions(release);
370 if (!runs) {
371 return;
372 }
373 StringRef key = release.getObjFifoName();
374 released[key] += *runs * release.relNumber();
375 blame.try_emplace(key, release);
376 });
377
378 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
379 // A segment-selecting endpoint sees a share of the pool, not the whole.
380 auto budget = budgets.find(endpoint.getPoolOp());
381 if (budget == budgets.end() || endpoint.getSegments()) {
382 continue;
383 }
384 StringRef name = endpoint.getSymName();
385 int64_t count = released.lookup(name);
386 if (count <= budget->second.objects) {
387 continue;
388 }
389 ObjectFifoDmaEndpointOp dma = budget->second.endpoint;
390 InFlightDiagnostic diag =
391 blame.lookup(name).emitOpError("releases ")
392 << count << " objects through @" << name
393 << " over the run, but the pool's DMA endpoint stops after "
394 << budget->second.objects << " (iterCount "
395 << budget->second.iterations << " x depth "
396 << endpoint.getPoolOp().getDepth()
397 << "), so the core would wait forever for the rest";
398 diag.attachNote(dma.getLoc()) << "the DMA endpoint is here";
399 return failure();
400 }
401 return success();
402 }
403
404 void runOnOperation() override {
405 DeviceOp device = getOperation();
406
407 DenseMap<Operation *, SmallVector<SegmentActors>> actorsPerPool;
408 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
409 actorsPerPool[pool].resize(pool.getSegmentOps().size());
410 }
411
412 auto record = [&](auto endpoint) {
413 ObjectFifoPoolOp pool = endpoint.getPoolOp();
414 if (!pool) {
415 return;
416 }
417 auto &actors = actorsPerPool[pool];
418 std::vector<ObjectFifoSegmentOp> all = pool.getSegmentOps();
419 for (ObjectFifoSegmentOp segment : endpoint.getSelectedSegments()) {
420 size_t index = llvm::find(all, segment) - all.begin();
421 (endpoint.drains() ? actors[index].drainers : actors[index].fillers)
422 .push_back(endpoint);
423 }
424 };
425
426 for (auto endpoint : device.getOps<ObjectFifoCoreEndpointOp>()) {
427 record(endpoint);
428 }
429 for (auto endpoint : device.getOps<ObjectFifoDmaEndpointOp>()) {
430 record(endpoint);
431 }
432
433 for (auto pool : device.getOps<ObjectFifoPoolOp>()) {
434 if (failed(verifyActors(pool, actorsPerPool[pool]))) {
435 return signalPassFailure();
436 }
437 }
438
439 SymbolTableCollection symbolTables;
440 if (failed(verifyFlows(device, symbolTables)) ||
441 failed(verifyFanIn(device, symbolTables)) ||
442 failed(verifyOverRelease(device)) ||
443 failed(verifyTransferBudgets(device))) {
444 return signalPassFailure();
445 }
446 }
447};
448
449} // namespace
450
451std::unique_ptr<OperationPass<DeviceOp>>
453 return std::make_unique<AIEObjectFifoVerifyPass>();
454}
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEObjectFifoVerifyPass()
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...