MLIR-AIE 1.4.6.dev
AIEShimSharing.cpp
Go to the documentation of this file.
1//===- AIEShimSharing.cpp ---------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
11
12#include "mlir/IR/Matchers.h"
13#include "mlir/Interfaces/LoopLikeInterface.h"
14#include "llvm/ADT/STLExtras.h"
15
16#include <tuple>
17
18using namespace mlir;
19using namespace xilinx;
20using namespace xilinx::AIE;
21
22namespace xilinx::AIE {
23#define GEN_PASS_DEF_AIEANNOTATESHIMSHARING
24#include "aie/Dialect/AIE/Transforms/AIEPasses.h.inc"
25} // namespace xilinx::AIE
26
28 DeviceOp device, function_ref<std::optional<End>(StringAttr)> endOf) {
29 analyze(device, [&](DeviceOp owner, StringAttr symbol) {
30 return owner == device ? endOf(symbol) : std::nullopt;
31 });
32}
33
35 ModuleOp module,
36 function_ref<std::optional<End>(DeviceOp, StringAttr)> endOf) {
37 analyze(module, endOf);
38}
39
40void ShimTransferSpans::analyze(
41 Operation *root,
42 function_ref<std::optional<End>(DeviceOp, StringAttr)> endOf) {
43 struct Span {
44 StringAttr endpoint;
45 int64_t first, last;
46 };
47 struct Summary {
48 SmallVector<Span> spans;
49 int64_t duration = 0;
50 };
51 DenseMap<Operation *, Summary> summaries;
52 DenseSet<Operation *> active;
53 NamedOpTableCollection names;
54 std::function<Summary(RuntimeSequenceOp)> summarize =
55 [&](RuntimeSequenceOp sequence) -> Summary {
56 if (active.contains(sequence)) {
57 unknown = true;
58 return {};
59 }
60 if (auto found = summaries.find(sequence); found != summaries.end())
61 return found->second;
62 active.insert(sequence);
63 DeviceOp device = sequence->getParentOfType<DeviceOp>();
64 Summary result;
65 DenseMap<Operation *, int64_t> position, end;
66 std::function<void(Operation *)> number = [&](Operation *op) {
67 position[op] = result.duration++;
68 if (auto run = dyn_cast<AIEX::RunOp>(op)) {
69 if (auto callee = run.getCalleeRuntimeSequenceOp(&names))
70 result.duration += summarize(callee).duration;
71 else
72 unknown = true;
73 }
74 for (Region &region : op->getRegions())
75 for (Block &block : region)
76 for (Operation &nested : block)
77 number(&nested);
78 end[op] = result.duration++;
79 };
80 number(sequence);
81 auto interval = [&](Operation *op, int64_t from, int64_t to) {
82 for (Operation *parent = op->getParentOp(); parent != sequence;
83 parent = parent->getParentOp())
84 if (isa<LoopLikeOpInterface>(parent)) {
85 from = position[parent];
86 to = end[parent];
87 }
88 return std::make_pair(from, to);
89 };
90 // Completion names a specific start, so a callee cannot retire caller work.
91 auto note = [&](StringAttr endpoint, int64_t at,
92 std::optional<size_t> completion = std::nullopt) {
93 if (completion) {
94 result.spans[*completion].last = at;
95 return *completion;
96 }
97 used.insert(endpoint);
98 result.spans.push_back({endpoint, at, end[sequence]});
99 return result.spans.size() - 1;
100 };
101 DenseMap<StringAttr, SmallVector<size_t>> pending;
102 DenseMap<Value, StringAttr> tasks;
103 DenseMap<size_t, Value> owners;
104 auto transfer = [&](Operation *op, StringAttr symbol, bool starts,
105 bool all = false, Value owner = {}) {
106 auto endpoint = endOf(device, symbol);
107 if (!endpoint)
108 return;
109 auto [from, to] = interval(op, position[op], position[op]);
110 auto &queue = pending[symbol];
111 if (starts) {
112 size_t start = note(endpoint->fifo, from);
113 owners[start] = owner;
114 queue.push_back(start);
115 } else if (!endpoint->sends) {
116 for (auto it = queue.begin(); it != queue.end();) {
117 if (owner && owners.lookup(*it) != owner) {
118 ++it;
119 continue;
120 }
121 note(endpoint->fifo, to, *it);
122 it = queue.erase(it);
123 if (!all)
124 break;
125 }
126 }
127 };
128 auto synchronous = [&](Operation *op, StringAttr endpoint, int64_t from,
129 int64_t to) {
130 std::tie(from, to) = interval(op, from, to);
131 auto start = note(endpoint, from);
132 note(endpoint, to, start);
133 };
134 auto configuration = [&](Operation *op, DeviceOp configured,
135 std::optional<int> column = {}) {
136 if (!configured)
137 return;
138 for (auto alloc : configured.getOps<ShimDMAAllocationOp>()) {
139 if (!AIEDialect::IsCtrlPktOverlayAttrHelper(root->getContext())
140 .isAttrPresent(alloc) ||
141 (column && alloc.getTileOp().colIndex() != *column))
142 continue;
143 if (auto endpoint = endOf(configured, alloc.getSymNameAttr()))
144 synchronous(op, endpoint->fifo, position[op], position[op]);
145 }
146 };
147 sequence.walk<WalkOrder::PreOrder>([&](Operation *op) {
148 if (auto run = dyn_cast<AIEX::RunOp>(op)) {
149 if (auto callee = run.getCalleeRuntimeSequenceOp(&names))
150 for (const Span &span : summarize(callee).spans)
151 synchronous(op, span.endpoint, position[op] + 1 + span.first,
152 position[op] + 1 + span.last);
153 } else if (auto configure = dyn_cast<AIEX::ConfigureOp>(op)) {
154 configuration(op, configure.getReferencedDeviceOp());
155 } else if (isa<AIEX::NpuLoadPdiOp, AIEX::NpuWriteConfigOp,
156 AIEX::NpuWriteTeardownOp>(op)) {
157 if (isa<AIEX::NpuLoadPdiOp>(op) && op->getParentOp() == sequence) {
158 for (auto &[symbol, queue] : pending) {
159 for (size_t start : queue)
160 note(result.spans[start].endpoint, position[op] - 1, start);
161 queue.clear();
162 }
163 }
164 auto ref = op->getAttrOfType<FlatSymbolRefAttr>("device_ref");
165 configuration(
166 op, ref ? SymbolTable::lookupNearestSymbolFrom<DeviceOp>(op, ref)
167 : device);
168 } else if (auto packet = dyn_cast<AIEX::NpuControlPacketOp>(op)) {
169 configuration(op, device, packet.getColumnFromAddr());
170 } else if (auto sync = dyn_cast<AIEX::NpuSyncOp>(op)) {
171 SmallVector<int> values;
172 for (Value operand : sync->getOperands()) {
173 APInt value;
174 if (!matchPattern(operand, m_ConstantInt(&value)))
175 return;
176 values.push_back(value.getSExtValue());
177 }
178 if (values[4] != 1 || values[5] != 1)
179 return;
180 StringAttr symbol;
181 size_t first = std::numeric_limits<size_t>::max();
182 for (const auto &[name, queue] : pending) {
183 if (queue.empty() || queue.front() >= first)
184 continue;
185 Operation *owner = device.lookupSymbol(name);
186 TileOp tile;
187 std::optional<int> channel;
188 DMAChannelDir direction;
189 if (auto alloc = dyn_cast_or_null<ShimDMAAllocationOp>(owner)) {
190 tile = alloc.getTileOp();
191 channel = alloc.getChannelIndex();
192 direction = alloc.getChannelDir();
193 } else if (auto endpoint = dyn_cast_or_null<RouteEndpointOp>(owner)) {
194 tile = endpoint.getTile().getDefiningOp<TileOp>();
195 channel = endpoint.getChannelIndex();
196 direction = endpoint.getRouteDirection();
197 }
198 if (tile && channel && tile.colIndex() == values[0] &&
199 tile.rowIndex() == values[1] &&
200 static_cast<int>(direction) == values[2] &&
201 *channel == values[3]) {
202 first = queue.front();
203 symbol = name;
204 }
205 }
206 if (symbol)
207 transfer(op, symbol, false);
208 } else if (auto memcpy = dyn_cast<AIEX::NpuDmaMemcpyNdOp>(op)) {
209 transfer(op, memcpy.getMetadata().getRootReference(), true);
210 } else if (auto wait = dyn_cast<AIEX::NpuDmaWaitOp>(op)) {
211 transfer(op, wait.getSymbolAttr().getAttr(), false, true);
212 } else if (auto configure = dyn_cast<AIEX::DMAConfigureTaskForOp>(op)) {
213 tasks[configure.getResult()] = configure.getAlloc().getRootReference();
214 } else if (isa<AIEX::DMAStartTaskOp, AIEX::DMAAwaitTaskOp,
215 AIEX::DMAFreeTaskOp>(op)) {
216 if (auto symbol = tasks.lookup(op->getOperand(0)))
217 transfer(op, symbol, isa<AIEX::DMAStartTaskOp>(op),
218 isa<AIEX::DMAFreeTaskOp>(op),
219 isa<AIEX::DMAAwaitTaskOp>(op) ? Value{} : op->getOperand(0));
220 }
221 });
222 llvm::stable_sort(result.spans, [](const Span &a, const Span &b) {
223 return a.first < b.first;
224 });
225 DenseMap<StringAttr, int64_t> live;
226 for (const Span &span : result.spans) {
227 lifetimes[span.endpoint].emplace_back(summaries.size(), span.first,
228 span.last);
229 for (const auto &[other, last] : live) {
230 if (last < span.first)
231 continue;
232 overlaps.insert({span.endpoint, other});
233 overlaps.insert({other, span.endpoint});
234 }
235 live[span.endpoint] = std::max(live.lookup(span.endpoint), span.last);
236 }
237 active.erase(sequence);
238 summaries[sequence] = result;
239 return result;
240 };
241 root->walk([&](Operation *op) {
242 if (!isa<ObjectFifoCreateOp, RouteEndpointOp, ShimDMAAllocationOp>(op))
243 return;
244 DeviceOp device = op->getParentOfType<DeviceOp>();
245 auto endpoint = endOf(device, SymbolTable::getSymbolName(op));
246 if (!endpoint)
247 return;
248 bool sends = endpoint->sends;
249 if (auto alloc = dyn_cast<ShimDMAAllocationOp>(op)) {
250 sends = alloc.getChannelDir() == DMAChannelDir::MM2S;
251 used.insert(endpoint->fifo);
252 } else if (auto allowed =
253 AIEDialect::ShareableWithAttrHelper(op->getContext())
254 .getAttr(op)) {
255 auto &peers = permissions[endpoint->fifo];
256 for (Attribute attr : allowed)
257 if (auto ref = dyn_cast<FlatSymbolRefAttr>(attr))
258 if (auto other = endOf(device, ref.getAttr()))
259 peers.push_back(other->fifo);
260 }
261 if (auto fifo = dyn_cast<ObjectFifoCreateOp>(op))
262 if (llvm::any_of(fifo.getConsumerTiles(), [](Value tile) {
263 return cast<TileLike>(tile.getDefiningOp()).isShimTile();
264 }))
265 sends = false;
266 // Receive ordering does not constrain when independent producers send.
267 if (!sends)
268 permissions.try_emplace(endpoint->fifo);
269 });
270 root->walk([&](RuntimeSequenceOp sequence) { summarize(sequence); });
271}
272
273bool ShimTransferSpans::isUsed(StringAttr endpoint) const {
274 return used.contains(endpoint);
275}
276
277bool ShimTransferSpans::apart(StringAttr a, StringAttr b) const {
278 auto permits = [&](StringAttr from, StringAttr to) {
279 if (auto it = permissions.find(from); it != permissions.end())
280 return llvm::is_contained(it->second, to);
281 return !unknown && isUsed(from) && isUsed(to) &&
282 !overlaps.contains({from, to});
283 };
284 return a != b && permits(a, b) && permits(b, a);
285}
286
287SmallVector<SmallVector<unsigned>>
288ShimTransferSpans::groups(ArrayRef<StringAttr> ends) const {
289 auto when = llvm::map_to_vector(
290 ends, [&](StringAttr end) { return lifetimes.lookup(end); });
291 auto order = llvm::to_vector(llvm::seq<unsigned>(0, ends.size()));
292 llvm::stable_sort(order,
293 [&](unsigned x, unsigned y) { return when[x] < when[y]; });
294 SmallVector<SmallVector<unsigned>> result;
295 for (unsigned end : order) {
296 auto *group = llvm::find_if(result, [&](ArrayRef<unsigned> members) {
297 return llvm::all_of(
298 members, [&](unsigned m) { return apart(ends[m], ends[end]); });
299 });
300 if (group == result.end())
301 result.push_back({end});
302 else
303 group->push_back(end);
304 }
305 return result;
306}
307
308namespace {
309struct AIEAnnotateShimSharingPass
310 : xilinx::AIE::impl::AIEAnnotateShimSharingBase<
311 AIEAnnotateShimSharingPass> {
312 void runOnOperation() override {
313 ModuleOp module = getOperation();
314 AIEDialect::IsCtrlPktOverlayAttrHelper overlay(&getContext());
315 AIEDialect::ShareableWithAttrHelper sharing(&getContext());
316 DenseMap<Operation *, StringAttr> keys;
317 auto key = [&](Operation *op) {
318 auto [it, inserted] = keys.try_emplace(op);
319 if (inserted)
320 it->second =
321 StringAttr::get(&getContext(), std::to_string(keys.size()));
322 return it->second;
323 };
324 ShimTransferSpans spans(
325 module,
326 [&](DeviceOp device,
327 StringAttr symbol) -> std::optional<ShimTransferSpans::End> {
328 Operation *op = device.lookupSymbol(symbol);
329 if (auto endpoint = dyn_cast_or_null<RouteEndpointOp>(op)) {
330 if (endpoint.getBundle() != WireBundle::DMA ||
331 !endpoint.getTileLike().isShimTile())
332 return std::nullopt;
333 return ShimTransferSpans::End{key(endpoint),
334 endpoint.getRouteDirection() ==
335 DMAChannelDir::MM2S};
336 }
337 auto alloc = dyn_cast_or_null<ShimDMAAllocationOp>(op);
338 if (!alloc)
339 return std::nullopt;
340 if (overlay.isAttrPresent(alloc)) {
341 if (auto ref = device.getOverlayAttr())
342 if (auto definition =
343 module.lookupSymbol<DeviceOp>(ref.getValue()))
344 if (Operation *original = definition.lookupSymbol(symbol))
345 op = original;
346 return ShimTransferSpans::End{key(op), false};
347 }
348 return ShimTransferSpans::End{key(op), alloc.getChannelDir() ==
349 DMAChannelDir::MM2S};
350 });
351 for (DeviceOp device : module.getOps<DeviceOp>()) {
352 auto annotate = [&](Operation *op, Value tile, DMAChannelDir direction,
353 StringAttr identity) {
354 if (sharing.isAttrPresent(op))
355 return;
356 SmallVector<Attribute> allowed;
357 for (auto other : device.getOps<RouteEndpointOp>())
358 if (other != op && other.getBundle() == WireBundle::DMA &&
359 other.getTile() == tile &&
360 other.getRouteDirection() == direction &&
361 spans.apart(identity, keys.lookup(other)))
362 allowed.push_back(FlatSymbolRefAttr::get(other.getSymNameAttr()));
363 sharing.setAttr(op, ArrayAttr::get(&getContext(), allowed));
364 };
365 for (auto endpoint : device.getOps<RouteEndpointOp>())
366 if (endpoint.getBundle() == WireBundle::DMA &&
367 endpoint.getTileLike().isShimTile())
368 annotate(endpoint, endpoint.getTile(), endpoint.getRouteDirection(),
369 keys.lookup(endpoint));
370 for (auto alloc : device.getOps<ShimDMAAllocationOp>())
371 if (overlay.isAttrPresent(alloc)) {
372 Operation *original = alloc;
373 if (auto ref = device.getOverlayAttr())
374 if (auto definition = module.lookupSymbol<DeviceOp>(ref.getValue()))
375 if (Operation *source =
376 definition.lookupSymbol(alloc.getSymName()))
377 original = source;
378 annotate(alloc, alloc.getTile(), alloc.getChannelDir(),
379 keys.lookup(original));
380 }
381 }
382 }
383};
384} // namespace
385
386std::unique_ptr<OperationPass<ModuleOp>>
388 return std::make_unique<AIEAnnotateShimSharingPass>();
389}
Shim endpoint lifetimes, including calls to self-contained runtime sequences.
ShimTransferSpans(DeviceOp device, llvm::function_ref< std::optional< End >(mlir::StringAttr)> endOf)
endOf maps a symbol a runtime sequence transfers through to the objectFIFO end it belongs to,...
bool isUsed(mlir::StringAttr end) const
bool apart(mlir::StringAttr a, mlir::StringAttr b) const
Whether both ends permit sharing, explicitly or by MM2S lifetime analysis.
llvm::SmallVector< llvm::SmallVector< unsigned > > groups(llvm::ArrayRef< mlir::StringAttr > ends) const
Splits ends into groups whose members are pairwise apart, each group taking one channel: first fit,...
std::unique_ptr< mlir::OperationPass< mlir::ModuleOp > > createAIEAnnotateShimSharingPass()
A shim end a runtime sequence transfers through.