MLIR-AIE 1.4.6.dev
AIECertVerify.cpp
Go to the documentation of this file.
1//===- AIECertVerify.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/Pass/Pass.h"
13
14#include "llvm/ADT/STLExtras.h"
15#include "llvm/ADT/SmallVector.h"
16#include "llvm/ADT/StringRef.h"
17
18#include <algorithm>
19#include <map>
20#include <set>
21#include <tuple>
22#include <utility>
23
24namespace xilinx::AIEX {
25#define GEN_PASS_DEF_AIECERTVERIFY
26#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
27} // namespace xilinx::AIEX
28
29using namespace mlir;
30using namespace xilinx;
31
32#define DEBUG_TYPE "cert-verify"
33
34namespace {
35
36// The uC (CERT group) id of a cert op is the group_id of its nearest enclosing
37// cert.attach_to_group, or 0 if it is at device level.
38static int getUcGroupId(Operation *op) {
39 auto group = op->getParentOfType<AIEX::CertAttachToGroupOp>();
40 return group ? group.getGroupID() : 0;
41}
42
43// G-localbar: within each uC scope, all cert.local_barrier ops that share a
44// local_barrier_id must reside in the same cert.page (their participants must
45// be co-resident on one page, else the firmware hangs).
46static LogicalResult checkLocalBarrierColocation(AIE::DeviceOp device) {
47 LogicalResult result = success();
48 // Before page formation, a standalone job is its own co-location scope.
49 std::map<std::pair<int, int>, Operation *> firstScope;
50 device.walk(
51 [&](AIEX::CertLocalBarrierOp barrier) {
52 int uc = getUcGroupId(barrier);
53 int id = static_cast<int>(barrier.getLocalBarrierId());
54 Operation *scope = barrier->getParentOfType<AIEX::CertPageOp>();
55 if (!scope)
56 scope = barrier->getParentOfType<AIEX::CertJobOp>();
57 auto key = std::make_pair(uc, id);
58 auto it = firstScope.find(key);
59 if (it == firstScope.end()) {
60 firstScope[key] = scope;
61 return;
62 }
63 if (it->second != scope) {
64 barrier.emitError()
65 << "cert.local_barrier "
66 << stringifyCertLocalBarrierId(barrier.getLocalBarrierId())
67 << " in uC " << uc
68 << " must be on the same page as its other participants";
69 result = failure();
70 }
71 });
72 return result;
73}
74
75// Firmware keeps one outstanding TCT count per actor. Concurrent jobs waiting
76// on the same (tile, channel) would over-wait; sequential waits within a job or
77// on different pages are safe. Bare jobs use their own scope until
78// legalization.
79static LogicalResult checkWaitTctsUniqueness(AIE::DeviceOp device) {
80 LogicalResult result = success();
81 // (page scope, tile, channel) -> the job that already waits on that actor.
82 std::map<std::tuple<Operation *, int, int>, Operation *> seen;
83 device.walk([&](AIEX::CertWaitTCTSOp wait) {
84 int tile = wait.getTileId();
85 int channel = wait.getChannelId();
86 Operation *job = wait->getParentOfType<AIEX::CertJobOp>();
87 Operation *scope = wait->getParentOfType<AIEX::CertPageOp>();
88 if (!scope)
89 scope = job;
90 auto key = std::make_tuple(scope, tile, channel);
91 auto it = seen.find(key);
92 if (it == seen.end()) {
93 seen[key] = job;
94 return;
95 }
96 // Same job: sequential within one thread of control, so legal.
97 if (it->second == job)
98 return;
99 wait.emitError() << "more than one job on this cert.page waits on tile "
100 << tile << " channel " << channel << " in uC "
101 << getUcGroupId(wait)
102 << "; at most one job per page may wait_tcts on a given "
103 "(tile, channel)";
104 result = failure();
105 });
106 return result;
107}
108
109static std::set<int> computePresentUcs(AIE::DeviceOp device) {
110 std::set<int> present;
111 device.walk([&](AIEX::CertAttachToGroupOp group) {
112 present.insert(group.getGroupID());
113 });
114 if (!device.getBody()->getOps<AIEX::CertPageOp>().empty() ||
115 !device.getBody()->getOps<AIEX::CertJobOp>().empty() ||
116 !device.getBody()->getOps<AIEX::CertSectionOp>().empty())
117 present.insert(0);
118 return present;
119}
120
121// Firmware requires each controller to expose the same preemption IDs,
122// including multiplicity. Compare against the lowest present controller ID.
123static LogicalResult checkPreemptConsistency(AIE::DeviceOp device) {
124 std::map<int, std::multiset<int>> preemptIds;
125 std::map<int, AIEX::CertAttachToGroupOp> repAttach;
126 std::set<int> present;
127
128 // Every attach_to_group establishes a present uC.
129 device.walk([&](AIEX::CertAttachToGroupOp group) {
130 int uc = group.getGroupID();
131 present.insert(uc);
132 if (!repAttach.count(uc))
133 repAttach[uc] = group;
134 });
135
136 // Device-level cert content lives on uC 0.
137 if (!device.getBody()->getOps<AIEX::CertPageOp>().empty() ||
138 !device.getBody()->getOps<AIEX::CertJobOp>().empty() ||
139 !device.getBody()->getOps<AIEX::CertSectionOp>().empty())
140 present.insert(0);
141
142 // Collect the preempt ids per uC.
143 device.walk([&](AIEX::CertPreemptOp preempt) {
144 int uc = getUcGroupId(preempt);
145 present.insert(uc);
146 preemptIds[uc].insert(preempt.getId());
147 });
148
149 if (present.size() < 2)
150 return success();
151
152 int ref = *present.begin();
153 const std::multiset<int> &refSet = preemptIds[ref];
154 LogicalResult result = success();
155 for (int uc : present) {
156 if (uc == ref)
157 continue;
158 if (preemptIds[uc] != refSet) {
159 Operation *loc = repAttach.count(uc) ? repAttach[uc].getOperation()
160 : device.getOperation();
161 loc->emitError()
162 << "cert.preempt ids on uC " << uc << " differ from uC " << ref
163 << "; all uCs must have the same preempt ids (G-preempt)";
164 result = failure();
165 }
166 }
167 return result;
168}
169
170// Group/placement ids must be valid microcontroller indices for the device:
171// nonnegative, less than the number of uCs, and (defensively) < 32 so they are
172// representable in a party mask. Rejecting invalid ids up front keeps later
173// checks (e.g. the remote-barrier mask shift) free of undefined behavior.
174static LogicalResult checkGroupIds(AIE::DeviceOp device) {
175 const AIE::AIETargetModel &tm = device.getTargetModel();
176 int64_t numUcs = std::min<int64_t>(tm.getNumControllers(), 32);
177 LogicalResult result = success();
178 auto checkId = [&](Operation *op, int64_t id, llvm::StringRef what) {
179 if (id < 0 || id >= numUcs) {
180 op->emitError() << what << " " << id << " is not a valid uC id [0, "
181 << numUcs << ")";
182 result = failure();
183 }
184 };
185 device.walk([&](AIEX::CertAttachToGroupOp g) {
186 checkId(g, g.getGroupID(), "cert.attach_to_group group id");
187 });
188 // The verifier may run before placement lowering; check any leftover attrs.
189 device.walk([&](AIEX::CertPageOp p) {
190 if (auto pl = p.getPlacementAttr())
191 checkId(p, pl.getInt(), "cert.page placement");
192 });
193 return result;
194}
195
196// G-uC / rendezvous: cross-uC remote barriers must actually rendezvous. Group
197// occurrences by remote_barrier_id and require: all occurrences agree on
198// party_mask; every mask bit names a present uC that has a matching occurrence
199// (so nobody waits forever); each participating uC sets its own bit; and at
200// most one occurrence per uC per id (one participant per column). Assumes group
201// ids are already validated by checkGroupIds (shifts are still guarded).
202static LogicalResult checkRemoteBarrierParties(AIE::DeviceOp device) {
203 std::set<int> present = computePresentUcs(device);
204 LogicalResult result = success();
205
206 std::map<AIEX::CertRemoteBarrierId, SmallVector<AIEX::CertRemoteBarrierOp, 4>>
207 byId;
208 device.walk([&](AIEX::CertRemoteBarrierOp b) {
209 byId[b.getRemoteBarrierId()].push_back(b);
210 });
211
212 for (auto &entry : byId) {
213 StringRef id = stringifyCertRemoteBarrierId(entry.first);
214 SmallVector<AIEX::CertRemoteBarrierOp, 4> &occs = entry.second;
215
216 // All occurrences of an id must describe the same rendezvous.
217 uint32_t mask = occs.front().getPartyMask();
218 bool masksAgree = llvm::all_of(occs, [&](AIEX::CertRemoteBarrierOp b) {
219 return b.getPartyMask() == mask;
220 });
221 if (!masksAgree) {
222 for (auto b : occs)
223 b.emitError() << "cert.remote_barrier " << id
224 << " occurrences disagree on party_mask";
225 result = failure();
226 continue;
227 }
228
229 // Participating uCs (from occurrences); own-bit + one-per-uC checks.
230 std::map<int, int> occPerUc;
231 for (auto b : occs) {
232 int own = getUcGroupId(b);
233 occPerUc[own]++;
234 if (own >= 0 && own < 32 && !((mask >> own) & 1u)) {
235 b.emitError() << "cert.remote_barrier " << id << " on uC " << own
236 << " excludes its own uC from party_mask";
237 result = failure();
238 }
239 }
240 for (auto &[uc, count] : occPerUc)
241 if (count > 1) {
242 occs.front().emitError()
243 << "cert.remote_barrier " << id << " has " << count
244 << " occurrences on uC " << uc
245 << " (at most one participant per uC per rendezvous)";
246 result = failure();
247 }
248
249 // Every mask bit must be a present uC that has a matching barrier.
250 for (int bit = 0; bit < 32; ++bit) {
251 if (!((mask >> bit) & 1u))
252 continue;
253 if (!present.count(bit)) {
254 occs.front().emitError()
255 << "cert.remote_barrier " << id << " party_mask references uC "
256 << bit << " which is not present in the design";
257 result = failure();
258 } else if (!occPerUc.count(bit)) {
259 occs.front().emitError()
260 << "cert.remote_barrier " << id << " party_mask includes uC " << bit
261 << " but that uC has no matching remote_barrier(" << id
262 << ") to rendezvous with";
263 result = failure();
264 }
265 }
266 }
267 return result;
268}
269
270// The register `op` writes, if any. cert.write32_d only reads registers.
271static std::optional<uint32_t> registerWrittenBy(Operation &op) {
272 if (auto o = dyn_cast<AIEX::CertUcDmaWriteDesOp>(op))
273 return o.getWaitHandle();
274 if (auto o = dyn_cast<AIEX::CertRead32Op>(op))
275 return o.getValue();
276 if (auto o = dyn_cast<AIEX::CertRead32DOp>(op))
277 return o.getValue();
278 return std::nullopt;
279}
280
281// A cert.wait_uc_dma handle must be a local register r0..r7 whose last write
282// earlier in the same job is a cert.uc_dma_write_des. A read into that register
283// in between replaces the handle.
284static LogicalResult checkUcDmaWaitHandles(AIE::DeviceOp device) {
285 LogicalResult result = success();
286 device.walk([&](AIEX::CertJobOp job) {
287 llvm::SmallDenseMap<uint32_t, Operation *> lastWrite;
288 for (Operation &o : job.getBody().front()) {
289 if (auto wait = dyn_cast<AIEX::CertWaitUcDmaOp>(o)) {
290 uint32_t r = wait.getWaitHandle();
291 auto it = lastWrite.find(r);
292 if (r >= 8) {
293 wait.emitError() << "cert.wait_uc_dma handle r" << r
294 << " is a shared register; the handle must be a "
295 "local register r0..r7 written by a "
296 "cert.uc_dma_write_des in the same job";
297 result = failure();
298 } else if (it == lastWrite.end()) {
299 wait.emitError() << "cert.wait_uc_dma waits on r" << r
300 << ", which no earlier cert.uc_dma_write_des in "
301 "this job writes";
302 result = failure();
303 } else if (!isa<AIEX::CertUcDmaWriteDesOp>(it->second)) {
304 wait.emitError() << "cert.wait_uc_dma waits on r" << r
305 << ", but its last write is a '"
306 << it->second->getName().getStringRef()
307 << "', which replaced the handle";
308 result = failure();
309 }
310 }
311 if (auto r = registerWrittenBy(o))
312 lastWrite[*r] = &o;
313 }
314 });
315 return result;
316}
317
318struct AIECertVerifyPass
319 : xilinx::AIEX::impl::AIECertVerifyBase<AIECertVerifyPass> {
320 void runOnOperation() override {
321 AIE::DeviceOp device = getOperation();
322 if (failed(checkGroupIds(device)))
323 signalPassFailure();
324 if (failed(checkLocalBarrierColocation(device)))
325 signalPassFailure();
326 if (failed(checkWaitTctsUniqueness(device)))
327 signalPassFailure();
328 if (failed(checkPreemptConsistency(device)))
329 signalPassFailure();
330 if (failed(checkRemoteBarrierParties(device)))
331 signalPassFailure();
332 if (failed(checkUcDmaWaitHandles(device)))
333 signalPassFailure();
334 }
335};
336
337} // namespace
338
339std::unique_ptr<OperationPass<AIE::DeviceOp>> AIEX::createAIECertVerifyPass() {
340 return std::make_unique<AIECertVerifyPass>();
341}
virtual uint32_t getNumControllers() const
Total number of microcontrollers (uC) in the device.
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIECertVerifyPass()