MLIR-AIE 1.4.6.dev
AIEChannelTurns.cpp
Go to the documentation of this file.
1//===- AIEChannelTurns.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/Dialect/Utils/StaticValueUtils.h"
13#include "mlir/IR/Matchers.h"
14#include "mlir/Interfaces/LoopLikeInterface.h"
15#include "mlir/Interfaces/SideEffectInterfaces.h"
16#include "llvm/ADT/STLExtras.h"
17#include "llvm/Support/Debug.h"
18#include "llvm/Support/MathExtras.h"
19
20#include <map>
21#include <set>
22
23#define DEBUG_TYPE "aie-channel-turns"
24
25using namespace mlir;
26using namespace xilinx;
27using namespace xilinx::AIE;
28
29namespace {
30/// Events past this many in a period are not worth one chain.
31constexpr size_t maxPeriodEvents = 4096;
32/// Periods simulated before giving up on a steady state.
33constexpr int maxPeriods = 256;
34/// From the AIE's 24-bit BD-loop saturation value up, an `scf.for` trip count
35/// is read as "forever", as aie-objectfifo-verify reads it.
36constexpr int64_t runsForever = (int64_t{1} << 24) - 2;
37} // namespace
38
39LogicalResult CoreTurns::read(ArrayRef<ObjectFifoCoreEndpointOp> ends,
40 std::string &reason) {
41 assert(!ends.empty() && "turns between no endpoints");
42 ObjectFifoCoreEndpointOp first = ends.front();
43 Value tile = first.getTile();
44 if (llvm::any_of(ends, [&](ObjectFifoCoreEndpointOp end) {
45 return end.drains() != first.drains() || end.getTile() != tile;
46 })) {
47 reason = "the endpoints are not all one core's, one way";
48 return failure();
49 }
50 CoreOp core;
51 for (Operation *user : tile.getUsers())
52 if (auto candidate = dyn_cast<CoreOp>(user))
53 core = candidate;
54 if (!core) {
55 reason = "no core works through them";
56 return failure();
57 }
58 SmallVector<StringAttr> names;
59 for (ObjectFifoCoreEndpointOp end : ends)
60 names.push_back(end.getSymNameAttr());
61 return read(core, names, first.drains(), reason);
62}
63
64LogicalResult CoreTurns::read(CoreOp core, ArrayRef<StringAttr> names,
65 bool coreDrains, std::string &reason) {
66 this->coreDrains = coreDrains;
67 ends = names.size();
68 loop = nullptr;
69 idle = false;
70 DenseMap<StringAttr, unsigned> index;
71 for (auto [i, name] : llvm::enumerate(names))
72 index[name] = i;
73 // An objectFIFO's acquires name its port; an endpoint's are one way.
74 ObjectFifoPort port =
75 coreDrains ? ObjectFifoPort::Consume : ObjectFifoPort::Produce;
76 auto endOf = [&](Operation *op) -> std::optional<unsigned> {
77 FlatSymbolRefAttr name;
78 std::optional<ObjectFifoPort> used;
79 if (auto acquire = dyn_cast<ObjectFifoAcquireOp>(op)) {
80 name = acquire.getObjFifoNameAttr();
81 used = acquire.getPort();
82 } else if (auto release = dyn_cast<ObjectFifoReleaseOp>(op)) {
83 name = release.getObjFifoNameAttr();
84 used = release.getPort();
85 }
86 if (!name || (used && *used != port))
87 return std::nullopt;
88 auto it = index.find(name.getAttr());
89 if (it == index.end())
90 return std::nullopt;
91 return it->second;
92 };
93
94 // A draining core's release outside every loop, with no acquire after it,
95 // only gives objects back; the channel's order counts acquires, so it does
96 // not decide the period.
97 auto onceAfterLoops = [&](Operation *op) {
98 if (!coreDrains || !isa<ObjectFifoReleaseOp>(op) ||
99 op->getParentOfType<LoopLikeOpInterface>())
100 return false;
101 return llvm::none_of(
102 llvm::make_range(std::next(op->getIterator()), op->getBlock()->end()),
103 [&](Operation &later) {
104 return later
105 .walk([&](ObjectFifoAcquireOp acquire) {
106 return endOf(acquire) ? WalkResult::interrupt()
107 : WalkResult::advance();
108 })
109 .wasInterrupted();
110 });
111 };
112 SmallVector<Operation *> uses, tails;
113 core.walk([&](Operation *op) {
114 if (endOf(op))
115 (onceAfterLoops(op) ? tails : uses).push_back(op);
116 });
117 if (uses.empty()) {
118 reason = "the core never acquires them";
119 return failure();
120 }
121 // The period: the innermost loop around every use.
122 auto loopsAround = [&](Operation *op) {
123 SmallVector<Operation *> loops;
124 for (Operation *parent = op->getParentOp(); parent != core;
125 parent = parent->getParentOp())
126 if (isa<LoopLikeOpInterface>(parent))
127 loops.push_back(parent);
128 return loops;
129 };
130 SmallVector<Operation *> candidates;
131 for (Operation *candidate : loopsAround(uses.front()))
132 if (llvm::all_of(uses, [&](Operation *use) {
133 return candidate->isProperAncestor(use);
134 }))
135 candidates.push_back(candidate);
136 if (candidates.empty()) {
137 reason = "no loop repeats the core's use of them";
138 return failure();
139 }
140
141 // The counters of the unrolled loops, so that branches on them fold.
142 DenseMap<Value, Attribute> counters;
143 std::function<Attribute(Value)> evaluate = [&](Value value) -> Attribute {
144 Attribute attr = counters.lookup(value);
145 Operation *def = value.getDefiningOp();
146 if (attr || !def || matchPattern(value, m_Constant(&attr)))
147 return attr;
148 if (!isPure(def) || def->getNumResults() != 1)
149 return {};
150 SmallVector<Attribute> operands;
151 for (Value operand : def->getOperands())
152 if (!operands.emplace_back(evaluate(operand)))
153 return {};
154 SmallVector<OpFoldResult> folded;
155 if (failed(def->fold(operands, folded)) || folded.size() != 1)
156 return {};
157 if (auto result = dyn_cast<Attribute>(folded.front()))
158 return result;
159 return evaluate(cast<Value>(folded.front()));
160 };
161
162 std::function<LogicalResult(Block &)> unroll = [&](Block &block) {
163 for (Operation &op : block) {
164 if (std::optional<unsigned> end = endOf(&op)) {
165 if (auto acquire = dyn_cast<ObjectFifoAcquireOp>(op))
166 period.push_back({*end, acquire.acqNumber(), true, &op});
167 else
168 period.push_back(
169 {*end, cast<ObjectFifoReleaseOp>(op).relNumber(), false, &op});
170 if (period.size() > maxPeriodEvents) {
171 reason = "the core's period uses them too many times";
172 return failure();
173 }
174 continue;
175 }
176 if (!llvm::any_of(
177 uses, [&](Operation *use) { return op.isProperAncestor(use); }))
178 continue;
179 if (auto ifOp = dyn_cast<scf::IfOp>(op)) {
180 if (auto taken =
181 dyn_cast_or_null<BoolAttr>(evaluate(ifOp.getCondition()))) {
182 for (Block &branch :
183 taken.getValue() ? ifOp.getThenRegion() : ifOp.getElseRegion())
184 if (failed(unroll(branch)))
185 return failure();
186 continue;
187 }
188 }
189 auto forOp = dyn_cast<scf::ForOp>(op);
190 std::optional<APInt> trips =
191 forOp ? forOp.getStaticTripCount() : std::nullopt;
192 if (!trips) {
193 reason = "the core uses them under control flow that depends on data";
194 return failure();
195 }
196 if (trips->getSExtValue() > static_cast<int64_t>(maxPeriodEvents)) {
197 reason = "the core's period uses them too many times";
198 return failure();
199 }
200 std::optional<int64_t> lower = getConstantIntValue(forOp.getLowerBound());
201 std::optional<int64_t> step = getConstantIntValue(forOp.getStep());
202 for (int64_t trip = 0; trip < trips->getSExtValue(); trip++) {
203 if (lower && step)
204 counters[forOp.getInductionVar()] = IntegerAttr::get(
205 forOp.getInductionVar().getType(), *lower + trip * *step);
206 if (failed(unroll(*forOp.getBody())))
207 return failure();
208 }
209 }
210 return success();
211 };
212 // The innermost loop whose body unrolls, branches folded, is the period. An
213 // scf.while's condition region holds no uses.
214 for (Operation *candidate : candidates) {
215 period.clear();
216 counters.clear();
217 if (llvm::all_of(cast<LoopLikeOpInterface>(candidate).getLoopRegions(),
218 [&](Region *region) {
219 return llvm::all_of(*region, [&](Block &block) {
220 return succeeded(unroll(block));
221 });
222 })) {
223 loop = candidate;
224 reason.clear();
225 break;
226 }
227 }
228 if (!loop)
229 return failure();
230 LLVM_DEBUG({
231 llvm::dbgs() << "period of " << names.size() << " ends in "
232 << loop->getName() << " at " << loop->getLoc() << ": "
233 << period.size() << " events:";
234 for (const Event &event : period)
235 llvm::dbgs() << " " << (event.acquire ? "a" : "r") << event.end << "x"
236 << event.count;
237 llvm::dbgs() << "\n";
238 });
239
240 // Objects the channel moves in a period: those each acquire takes beyond the
241 // ones held, where the channel fills; those released, where it drains. The
242 // first periods may differ while the core builds up what it holds, so the
243 // order is that of the first period starting from a state seen before.
244 SmallVector<int64_t> held(ends, 0);
245 std::map<SmallVector<int64_t>, int> seen;
246 for (int round = 0; round < maxPeriods; round++) {
247 order.clear();
248 SmallVector<int64_t> start = held;
249 for (const Event &event : period) {
250 int64_t &h = held[event.end];
251 if (event.acquire) {
252 if (coreDrains)
253 order.append(std::max<int64_t>(0, event.count - h), event.end);
254 h = std::max(h, event.count);
255 } else {
256 if (event.count > h) {
257 reason = "the core releases objects it does not hold";
258 return failure();
259 }
260 h -= event.count;
261 if (!coreDrains)
262 order.append(event.count, event.end);
263 }
264 }
265 if (!seen.emplace(start, round).second) {
266 if (order.empty()) {
267 idle = true;
268 reason = "the core's period moves none of their objects";
269 return failure();
270 }
271 if (tails.empty())
272 return success();
273 SmallVector<std::pair<ObjectFifoReleaseOp, unsigned>> releases;
274 for (Operation *op : llvm::concat<Operation *const>(uses, tails))
275 if (auto release = dyn_cast<ObjectFifoReleaseOp>(op))
276 if (std::optional<unsigned> end = endOf(op))
277 releases.push_back({release, *end});
278 return wholeRunKeepsTurns(core, releases, reason);
279 }
280 }
281 reason = "the core never settles into a steady order";
282 return failure();
283}
284
285LogicalResult CoreTurns::wholeRunKeepsTurns(
286 CoreOp core, ArrayRef<std::pair<ObjectFifoReleaseOp, unsigned>> releases,
287 std::string &reason) const {
288 // A tail release behind a loop that runs forever is never reached.
289 for (Operation *op = loop; op != core; op = op->getParentOp()) {
290 auto whileOp = dyn_cast<scf::WhileOp>(op);
291 auto forOp = dyn_cast<scf::ForOp>(op);
292 std::optional<APInt> trips =
293 forOp ? forOp.getStaticTripCount() : std::nullopt;
294 if ((whileOp && isForeverLoop(whileOp)) ||
295 (trips && trips->getSExtValue() >= runsForever))
296 return success();
297 }
298
299 // The channel keeps taking its turns from one run of the core to the next,
300 // so a finite core must move each pool's objects a whole number of turns'
301 // worth over its run; otherwise a later run starts out of step with the
302 // turns, and the channel can wait on a full pool while the core waits on
303 // another.
304 SmallVector<int64_t> moved(ends, 0);
305 for (auto [release, end] : releases) {
306 int64_t runs = release.relNumber();
307 for (Operation *parent = release->getParentOp(); parent != core;
308 parent = parent->getParentOp()) {
309 auto forOp = dyn_cast<scf::ForOp>(parent);
310 std::optional<APInt> trips =
311 forOp ? forOp.getStaticTripCount() : std::nullopt;
312 if (!trips || llvm::MulOverflow(runs, trips->getSExtValue(), runs)) {
313 reason = "the core's whole run does not move a static count of them";
314 return failure();
315 }
316 }
317 moved[end] += runs;
318 }
319 SmallVector<int64_t> counts(ends, 0);
320 for (unsigned end : order)
321 counts[end]++;
322 std::optional<int64_t> whole;
323 for (size_t i = 0; i < ends; i++) {
324 bool fits = counts[i] ? moved[i] % counts[i] == 0 &&
325 (!whole || *whole == moved[i] / counts[i])
326 : moved[i] == 0;
327 if (!fits) {
328 reason = "the core's whole run moves them out of step with the turns";
329 return failure();
330 }
331 if (counts[i])
332 whole = moved[i] / counts[i];
333 }
334 return success();
335}
336
337bool CoreTurns::keepsUp(ArrayRef<int64_t> depths) {
338 assert(depths.size() == ends && "one depth per endpoint");
339 // The shortest run of the period's order, in the same proportions, that the
340 // channel can repeat, as a loop's steps do; the core's first and last steps
341 // may move them in another order, which the pools' depths then absorb.
342 SmallVector<int64_t> counts(ends, 0);
343 for (unsigned end : order)
344 counts[end]++;
345 for (size_t length = 1; length < order.size(); length++) {
346 if (order.size() % length)
347 continue;
348 size_t repeats = order.size() / length;
349 if (llvm::any_of(counts, [&](int64_t c) { return c % repeats; }))
350 continue;
351 std::set<SmallVector<unsigned>> tried;
352 // Counts for the window order[start, start + length), maintained
353 // incrementally across start (one element leaves, one enters per step)
354 // instead of recomputed from scratch, so the cheap proportionality
355 // filter below runs before a window is ever materialized, deduped or
356 // simulated, not after.
357 SmallVector<int64_t> runCounts(ends, 0);
358 for (size_t i = 0; i < length; i++)
359 runCounts[order[i]]++;
360 for (size_t start = 0; start + length <= order.size(); start++) {
361 if (start > 0) {
362 runCounts[order[start - 1]]--;
363 runCounts[order[start + length - 1]]++;
364 }
365 bool proportional = true;
366 for (size_t i = 0; i < ends; i++)
367 proportional &=
368 runCounts[i] * static_cast<int64_t>(repeats) == counts[i];
369 if (!proportional)
370 continue;
371 SmallVector<unsigned> run(order.begin() + start,
372 order.begin() + start + length);
373 if (!tried.insert(run).second || !keepsUp(run, depths))
374 continue;
375 order = run;
376 return true;
377 }
378 }
379 return keepsUp(order, depths);
380}
381
382Operation *CoreTurns::periodAcquire() const {
383 for (const Event &event : period)
384 if (event.acquire)
385 return event.op;
386 return nullptr;
387}
388
389bool CoreTurns::keepsUp(ArrayRef<unsigned> order,
390 ArrayRef<int64_t> depths) const {
391 // Objects ready for the core (full where it drains, free where it fills)
392 // and for the channel (the other kind); the core holds what it acquired.
393 SmallVector<int64_t> forCore(ends, 0), forChannel(ends, 0), held(ends, 0);
394 for (size_t i = 0; i < ends; i++)
395 (coreDrains ? forChannel : forCore)[i] = depths[i];
396 size_t turn = 0, event = 0;
397 std::map<SmallVector<int64_t>, int> seen;
398 for (int round = 0; round < maxPeriods * 4;) {
399 bool moved = false;
400 while (forChannel[order[turn]] > 0) {
401 forChannel[order[turn]]--;
402 forCore[order[turn]]++;
403 turn = (turn + 1) % order.size();
404 moved = true;
405 }
406 while (true) {
407 const Event &e = period[event];
408 if (e.acquire) {
409 int64_t take = std::max<int64_t>(0, e.count - held[e.end]);
410 if (forCore[e.end] < take)
411 break;
412 forCore[e.end] -= take;
413 held[e.end] = std::max(held[e.end], e.count);
414 } else {
415 held[e.end] -= e.count;
416 forChannel[e.end] += e.count;
417 }
418 moved = true;
419 if (++event == period.size()) {
420 event = 0;
421 round++;
422 SmallVector<int64_t> state{static_cast<int64_t>(turn)};
423 llvm::append_range(state, forCore);
424 llvm::append_range(state, forChannel);
425 llvm::append_range(state, held);
426 if (!seen.emplace(state, round).second)
427 return true;
428 }
429 }
430 if (!moved)
431 return false;
432 }
433 return false;
434}
mlir::LogicalResult read(llvm::ArrayRef< ObjectFifoCoreEndpointOp > ends, std::string &reason)
Reads the order off the program of the core working through ends, all draining (the channel fills the...
bool keepsUp(llvm::ArrayRef< int64_t > depths)
Whether a channel moving objects in turns() order forever, through pools of depths,...
mlir::Operation * periodAcquire() const
The first acquire the period runs, with its branches folded; null when it runs none.
bool isForeverLoop(mlir::scf::WhileOp whileOp)
Whether whileOp repeats forever: its condition is a constant true, and its condition region does noth...