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