MLIR-AIE 1.4.5.dev
AIENpuToCert.cpp
Go to the documentation of this file.
1//===- AIENpuToCert.cpp ----------------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
12
13#include "mlir/Dialect/Arith/IR/Arith.h"
14#include "mlir/Dialect/MemRef/IR/MemRef.h"
15#include "mlir/IR/IRMapping.h"
16#include "mlir/Pass/Pass.h"
17#include "mlir/Transforms/DialectConversion.h"
18#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
19
20#include <map>
21#include <set>
22#include <type_traits>
23#include <utility>
24#include <vector>
25
26namespace xilinx::AIEX {
27#define GEN_PASS_DEF_AIENPUTOCERT
28#define GEN_PASS_DEF_AIECERTPAGES
29#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
30} // namespace xilinx::AIEX
31
32using namespace mlir;
33using namespace xilinx;
34
35#define DEBUG_TYPE "npu-to-cert"
36
37namespace {
38
39// Trigger splitting below the 8 KB page limit to allow for estimated padding.
40// An unsplittable page is an error only if it exceeds cert_page_limit.
41static constexpr uint32_t cert_page_size = 8000;
42static constexpr uint32_t cert_page_limit = 8192;
43
44// Per-job control-code overhead: START_JOB (ISA_OPSIZE_START_JOB = 0x08) plus
45// END_JOB (ISA_OPSIZE_END_JOB = 0x04), emitted around every job's body by
46// emitJob.
47static constexpr uint32_t cert_job_start_cost = 8;
48static constexpr uint32_t cert_job_end_cost = 4;
49
50// Returns the PDI id to use for a cert.load_pdi that references `deviceSymName`
51// within `parentDevice`. If a load_pdi for that symbol already exists, its id
52// is reused so repeated loads of the same PDI share an id; otherwise a fresh id
53// (1-based, one past the current maximum) is assigned.
54static uint32_t getOrAssignPdiId(AIE::DeviceOp parentDevice,
55 StringRef deviceSymName) {
56 uint32_t pdiId = 1;
57 bool foundExisting = false;
58 parentDevice.walk([&](AIEX::CertLoadPdiOp loadPdiOp) {
59 if (loadPdiOp.getSymbol() == deviceSymName) {
60 pdiId = loadPdiOp.getPdiId();
61 foundExisting = true;
62 } else if (!foundExisting) {
63 pdiId = std::max(pdiId, loadPdiOp.getPdiId() + 1);
64 }
65 });
66 return pdiId;
67}
68
69struct RuntimeSequenceToCertJob : OpConversionPattern<AIE::RuntimeSequenceOp> {
70 using OpConversionPattern::OpConversionPattern;
71
72 RuntimeSequenceToCertJob(MLIRContext *context, PatternBenefit benefit = 1)
73 : OpConversionPattern(context, benefit) {}
74
75 LogicalResult
76 matchAndRewrite(AIE::RuntimeSequenceOp op, OpAdaptor adaptor,
77 ConversionPatternRewriter &rewriter) const override {
78
79 auto symName = op.getSymName();
80 uint32_t newJobId = 1;
81 if (symName != "configure") {
82 uint32_t maxJobId = 1;
83 op->getParentOp()->walk([&](AIEX::CertJobOp certJobOp) {
84 maxJobId = std::max(maxJobId, certJobOp.getJobId());
85 });
86 newJobId = maxJobId + 1;
87 }
88
89 // Replace the runtime sequence with a job directly inside aie.device.
90 // CertJobOp lists DeviceOp as an allowed parent in its ParentOneOf trait.
91 // cert-legalize-pages later groups contiguous jobs into implicit pages.
92 rewriter.setInsertionPoint(op);
93 auto jobOp = AIEX::CertJobOp::create(rewriter, op.getLoc(), newJobId);
94
95 // Tag the implicit configuration job so a later step can force it to be the
96 // first page on uC0 and keep it out of user implicit pages.
97 if (symName == "configure")
98 jobOp->setAttr("cert.configure", rewriter.getUnitAttr());
99
100 // Clone runtime sequence body into job
101 // Note: This preserves block arguments from the runtime sequence, which
102 // will be present in the MLIR IR but are not emitted in the final assembly
103 IRMapping remap;
104 op.getRegion().cloneInto(&jobOp.getBody(), remap);
105 AIEX::CertJobOp::ensureTerminator(jobOp.getBody(), rewriter, op->getLoc());
106
107 // Erase the original runtime sequence
108 rewriter.eraseOp(op);
109
110 return success();
111 }
112};
113
114struct NpuWrite32ToCertWrite32 : OpConversionPattern<AIEX::NpuWrite32Op> {
115 using OpConversionPattern::OpConversionPattern;
116
117 LogicalResult
118 matchAndRewrite(AIEX::NpuWrite32Op op, OpAdaptor adaptor,
119 ConversionPatternRewriter &rewriter) const override {
120
121 std::optional<uint32_t> address =
122 AIEX::getConstantIntOperand(op.getAddress());
123 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
124 if (!address || !value)
125 return op.emitOpError(
126 "cannot lower to cert.write32 with non-constant address or value");
127
128 // Get the absolute address, which properly handles row/col if present
129 std::optional<uint32_t> absAddress = op.getAbsoluteAddress();
130 if (!absAddress)
131 return failure();
132
133 uint32_t absoluteAddr = *absAddress;
134
135 // If row and col are specified, validate that the address upper bits match
136 std::optional<uint32_t> col = op.getColumn();
137 std::optional<uint32_t> row = op.getRow();
138 if (col && row) {
139 const auto &tm = AIE::getTargetModel(op);
140 uint32_t expectedUpperBits = ((*col & 0xff) << tm.getColumnShift()) |
141 ((*row & 0xff) << tm.getRowShift());
142
143 // Warn if the original address had non-zero upper bits that don't match
144 uint32_t origAddress = *address;
145 uint32_t origUpperBits = origAddress & ~0xfffff;
146 if (origUpperBits != 0 && origUpperBits != expectedUpperBits) {
147 op.emitWarning() << "address upper bits (0x"
148 << llvm::utohexstr(origUpperBits)
149 << ") don't match row=" << *row << " col=" << *col
150 << " computed bits (0x"
151 << llvm::utohexstr(expectedUpperBits) << ")";
152 }
153 }
154
155 rewriter.replaceOpWithNewOp<AIEX::CertWrite32Op>(op, absoluteAddr, *value);
156 return success();
157 }
158};
159
160struct NpuMaskWrite32ToCertMaskWrite32
161 : OpConversionPattern<AIEX::NpuMaskWrite32Op> {
162 using OpConversionPattern::OpConversionPattern;
163
164 LogicalResult
165 matchAndRewrite(AIEX::NpuMaskWrite32Op op, OpAdaptor adaptor,
166 ConversionPatternRewriter &rewriter) const override {
167
168 std::optional<uint32_t> address =
169 AIEX::getConstantIntOperand(op.getAddress());
170 std::optional<uint32_t> mask = AIEX::getConstantIntOperand(op.getMask());
171 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
172 if (!address || !mask || !value)
173 return op.emitOpError(
174 "cannot lower to cert.maskwrite32 with non-constant "
175 "address, mask, or value");
176
177 // Get the absolute address, which properly handles row/col if present
178 std::optional<uint32_t> absAddress = op.getAbsoluteAddress();
179 if (!absAddress)
180 return failure();
181
182 uint32_t absoluteAddr = *absAddress;
183
184 // If row and col are specified, validate that the address upper bits match
185 std::optional<uint32_t> col = op.getColumn();
186 std::optional<uint32_t> row = op.getRow();
187 if (col && row) {
188 const auto &tm = AIE::getTargetModel(op);
189 uint32_t expectedUpperBits = ((*col & 0xff) << tm.getColumnShift()) |
190 ((*row & 0xff) << tm.getRowShift());
191
192 // Warn if the original address had non-zero upper bits that don't match
193 uint32_t origAddress = *address;
194 uint32_t origUpperBits = origAddress & ~0xfffff;
195 if (origUpperBits != 0 && origUpperBits != expectedUpperBits) {
196 op.emitWarning() << "address upper bits (0x"
197 << llvm::utohexstr(origUpperBits)
198 << ") don't match row=" << *row << " col=" << *col
199 << " computed bits (0x"
200 << llvm::utohexstr(expectedUpperBits) << ")";
201 }
202 }
203
204 rewriter.replaceOpWithNewOp<AIEX::CertMaskWrite32Op>(op, absoluteAddr,
205 *mask, *value);
206 return success();
207 }
208};
209
210struct NpuMaskPollToCertMaskPoll32 : OpConversionPattern<AIEX::NpuMaskPollOp> {
211 using OpConversionPattern::OpConversionPattern;
212
213 LogicalResult
214 matchAndRewrite(AIEX::NpuMaskPollOp op, OpAdaptor adaptor,
215 ConversionPatternRewriter &rewriter) const override {
216 std::optional<uint32_t> address =
217 AIEX::getConstantIntOperand(op.getAddress());
218 std::optional<uint32_t> mask = AIEX::getConstantIntOperand(op.getMask());
219 std::optional<uint32_t> value = AIEX::getConstantIntOperand(op.getValue());
220 if (!address || !mask || !value)
221 return op.emitOpError("cannot lower to cert.maskpoll32 with non-constant "
222 "address, mask, or value");
223
224 std::optional<uint32_t> absAddress = op.getAbsoluteAddress();
225 if (!absAddress)
226 return failure();
227
228 rewriter.replaceOpWithNewOp<AIEX::CertMaskPoll32Op>(op, *absAddress, *mask,
229 *value);
230 return success();
231 }
232};
233
234struct NpuBlockWriteToCertUcDma : OpConversionPattern<AIEX::NpuBlockWriteOp> {
235 using OpConversionPattern::OpConversionPattern;
236
237 LogicalResult
238 matchAndRewrite(AIEX::NpuBlockWriteOp op, OpAdaptor adaptor,
239 ConversionPatternRewriter &rewriter) const override {
240
241 memref::GetGlobalOp dataOperand =
242 dyn_cast_or_null<memref::GetGlobalOp>(op.getData().getDefiningOp());
243 if (!dataOperand)
244 return failure();
245 MemRefType dataType = cast<MemRefType>(dataOperand.getResult().getType());
246 uint32_t dataSize = dataType.getNumElements();
247
248 int id = 0;
249 std::string symbolName = "chain_" + std::to_string(id);
250 while (op->getParentOfType<AIE::DeviceOp>().lookupSymbol(symbolName))
251 symbolName = "chain_" + std::to_string(++id);
252
253 // Create a new uc_dma_write_des_sync operation
254 rewriter.replaceOpWithNewOp<AIEX::CertUcDmaWriteDesSyncOp>(op, symbolName);
255
256 // Create the uc_dma_chain operation
257 // Find the nearest device to insert the chain
258 auto parentDevice = op->getParentOfType<AIE::DeviceOp>();
259 if (!parentDevice) {
260 // No parent device - this shouldn't happen but handle gracefully
261 return failure();
262 }
263
264 // Insert after the last existing uc_dma_chain (before any pages/jobs),
265 // preserving the order of blockwrite ops. Globals are allowed to precede
266 // the chains, so skip over them too.
267 Block *deviceBody = parentDevice.getBody();
268 Operation *insertAfter = nullptr;
269 for (Operation &bodyOp : *deviceBody) {
270 if (isa<AIEX::CertUcDmaChainOp>(bodyOp) || isa<memref::GlobalOp>(bodyOp))
271 insertAfter = &bodyOp;
272 else
273 break;
274 }
275 if (insertAfter)
276 rewriter.setInsertionPointAfter(insertAfter);
277 else
278 rewriter.setInsertionPointToStart(deviceBody);
279
280 auto symbolAttr = rewriter.getStringAttr(symbolName);
281 auto chainOp =
282 AIEX::CertUcDmaChainOp::create(rewriter, op.getLoc(), symbolAttr);
283
284 Block *bb = new Block();
285 chainOp.getRegion().push_back(bb);
286 rewriter.setInsertionPointToStart(bb);
287 AIEX::CertUcDmaBdOp::create(rewriter, op.getLoc(), dataOperand.getName(),
288 op.getAddress(), dataSize, false);
289
290 AIEX::CertUcDmaChainOp::ensureTerminator(chainOp.getBody(), rewriter,
291 op->getLoc());
292 return success();
293 }
294};
295
296struct NpuSyncToCertWaitTCTS : OpConversionPattern<AIEX::NpuSyncOp> {
297 using OpConversionPattern::OpConversionPattern;
298
299 LogicalResult
300 matchAndRewrite(AIEX::NpuSyncOp op, OpAdaptor adaptor,
301 ConversionPatternRewriter &rewriter) const override {
302 std::optional<uint32_t> rowOpt = AIEX::getConstantIntOperand(op.getRow());
303 std::optional<uint32_t> colOpt =
304 AIEX::getConstantIntOperand(op.getColumn());
305 std::optional<uint32_t> channelOpt =
306 AIEX::getConstantIntOperand(op.getChannel());
307 std::optional<uint32_t> directionOpt =
308 AIEX::getConstantIntOperand(op.getDirection());
309 if (!rowOpt || !colOpt || !channelOpt || !directionOpt)
310 return op.emitOpError(
311 "cannot lower to cert.wait_tcts with non-constant sync parameters");
312 uint32_t row = *rowOpt;
313 uint32_t col = *colOpt;
314
315 // These are the shift amounts from the tct packet format in the
316 // architecture spec. The firmware expects the row and column packed and
317 // shifted down to zero.
318 const int row_id_shift = 16;
319 const int col_id_shift = 21;
320 uint16_t tile_id = col << (col_id_shift - row_id_shift) | row;
321 uint32_t channel = *channelOpt;
322 uint32_t direction = *directionOpt;
323
324 const std::vector<int> chan2actor_shim_s2mm = {0, 2, 3, 4};
325 const std::vector<int> chan2actor_shim_mm2s = {6, 7, 8, 9, 10, 11, 12, 13};
326 const std::vector<int> chan2actor_mem_s2mm = {1, 2, 3, 4, 5, 6, 7};
327 const std::vector<int> chan2actor_mem_mm2s = {16, 17, 18, 19, 20,
328 22, 23, 24, 25, 26};
329 const std::vector<int> chan2actor_tile_s2mm = {0, 1};
330 const std::vector<int> chan2actor_tile_mm2s = {6};
331
332 const auto &tm = AIE::getTargetModel(op);
333 const bool isS2MM =
334 direction == static_cast<std::underlying_type_t<AIE::DMAChannelDir>>(
335 AIE::DMAChannelDir::S2MM);
336
337 const std::vector<int> *chan2actor = nullptr;
338 if (tm.isCoreTile(col, row))
339 chan2actor = isS2MM ? &chan2actor_tile_s2mm : &chan2actor_tile_mm2s;
340 else if (tm.isMemTile(col, row))
341 chan2actor = isS2MM ? &chan2actor_mem_s2mm : &chan2actor_mem_mm2s;
342 else
343 chan2actor = isS2MM ? &chan2actor_shim_s2mm : &chan2actor_shim_mm2s;
344
345 size_t chanIdx = static_cast<size_t>(channel);
346 if (chanIdx >= chan2actor->size()) {
347 op.emitError("invalid DMA channel ")
348 << channel << " for " << (isS2MM ? "S2MM" : "MM2S")
349 << " direction in NpuSyncToCertWaitTCTS conversion";
350 return failure();
351 }
352
353 uint8_t actor_id = static_cast<uint8_t>((*chan2actor)[chanIdx]);
354 uint8_t num_tcts = 1;
355 rewriter.replaceOpWithNewOp<AIEX::CertWaitTCTSOp>(op, tile_id, actor_id,
356 num_tcts);
357 return success();
358 }
359};
360
361struct NpuLoadPdiToCertLoadPdi : OpConversionPattern<AIEX::NpuLoadPdiOp> {
362 using OpConversionPattern::OpConversionPattern;
363
364 LogicalResult
365 matchAndRewrite(AIEX::NpuLoadPdiOp op, OpAdaptor adaptor,
366 ConversionPatternRewriter &rewriter) const override {
367 // Get the device reference
368 auto deviceRef = op.getDeviceRef();
369 if (!deviceRef)
370 return failure();
371
372 StringRef deviceSymName = *deviceRef;
373
374 // Find parent device to get unique PDI ID
375 auto parentDevice = op->getParentOfType<AIE::DeviceOp>();
376 if (!parentDevice)
377 return failure();
378
379 // Assign a PDI ID, reusing an existing one for this device if present.
380 uint32_t pdiId = getOrAssignPdiId(parentDevice, deviceSymName);
381
382 // Replace with cert.load_pdi
383 rewriter.replaceOpWithNewOp<AIEX::CertLoadPdiOp>(
384 op, rewriter.getUI32IntegerAttr(pdiId),
385 FlatSymbolRefAttr::get(rewriter.getContext(), deviceSymName));
386
387 return success();
388 }
389};
390
391struct RunOpInlining : OpRewritePattern<AIEX::RunOp> {
392 using OpRewritePattern::OpRewritePattern;
393
394 LogicalResult matchAndRewrite(AIEX::RunOp op,
395 PatternRewriter &rewriter) const override {
396 // Get the callee runtime sequence
397 AIE::RuntimeSequenceOp calleeRuntimeSequence =
398 op.getCalleeRuntimeSequenceOp();
399 if (!calleeRuntimeSequence)
400 return failure();
401
402 // Get the callee body region
403 Region &calleeBody = calleeRuntimeSequence.getBody();
404
405 // Create argument mapping from run op arguments to callee parameters
406 IRMapping argMap;
407 ValueRange values = op.getArgs();
408 if (values.size() != calleeBody.getNumArguments()) {
409 op.emitError("number of run op arguments (")
410 << values.size()
411 << ") does not match number of callee runtime sequence "
412 "arguments ("
413 << calleeBody.getNumArguments() << ")";
414 return failure();
415 }
416 for (unsigned i = 0, n = calleeBody.getNumArguments(); i < n; i++) {
417 BlockArgument arg = calleeBody.getArgument(i);
418 Value val = values[i];
419 argMap.map(arg, val);
420 }
421
422 // Clone operations from callee into current location
423 rewriter.setInsertionPoint(op);
424 for (Operation &o : calleeBody.front().getOperations()) {
425 // Skip the terminator
426 if (isa<AIE::EndOp>(o))
427 continue;
428 rewriter.clone(o, argMap);
429 }
430
431 // Erase the run op
432 rewriter.eraseOp(op);
433
434 return success();
435 }
436};
437
438struct ConfigureOpToCertSection : OpRewritePattern<AIEX::ConfigureOp> {
439 using OpRewritePattern::OpRewritePattern;
440
441 LogicalResult matchAndRewrite(AIEX::ConfigureOp op,
442 PatternRewriter &rewriter) const override {
443 // Get the referenced device
444 AIE::DeviceOp referencedDevice = op.getReferencedDeviceOp();
445 if (!referencedDevice)
446 return failure();
447
448 // Get the device's symbol name for the section
449 StringRef deviceSymName = referencedDevice.getSymName();
450
451 // Find the parent DeviceOp to insert the section into
452 auto parentDevice = op->getParentOfType<AIE::DeviceOp>();
453 if (!parentDevice)
454 return failure();
455
456 // Check if section already exists (avoid creating duplicates)
457 auto existingSection = dyn_cast_if_present<AIEX::CertSectionOp>(
458 parentDevice.lookupSymbol(deviceSymName));
459 if (existingSection) {
460 // Section already exists, just create load_pdi at call site, reusing the
461 // PDI ID already assigned to this section (or assigning a new one).
462 uint32_t pdiId = getOrAssignPdiId(parentDevice, deviceSymName);
463
464 rewriter.setInsertionPoint(op);
465 AIEX::CertLoadPdiOp::create(
466 rewriter, op.getLoc(), rewriter.getUI32IntegerAttr(pdiId),
467 FlatSymbolRefAttr::get(rewriter.getContext(), deviceSymName));
468
469 // Clone configure body operations after the load_pdi
470 IRMapping bodyMapper;
471 for (Operation &o : op.getRegion().front().getOperations()) {
472 if (isa<AIE::EndOp>(o))
473 continue;
474 rewriter.clone(o, bodyMapper);
475 }
476
477 rewriter.eraseOp(op);
478 return success();
479 }
480
481 // Create cert.section with the device symbol name
482 rewriter.setInsertionPoint(parentDevice.getBody()->getTerminator());
483 auto sectionOp = AIEX::CertSectionOp::create(
484 rewriter, op.getLoc(), rewriter.getStringAttr(deviceSymName));
485
486 // Create the section body with a page containing a job
487 Block *sectionBlock = new Block();
488 sectionOp.getBody().push_back(sectionBlock);
489 rewriter.setInsertionPointToStart(sectionBlock);
490
491 // Create page within section
492 auto pageOp = AIEX::CertPageOp::create(rewriter, op.getLoc());
493 Block *pageBlock = new Block();
494 pageOp.getBody().push_back(pageBlock);
495 rewriter.setInsertionPointToStart(pageBlock);
496
497 // Create job within page - assign unique job ID across all devices
498 // Find the maximum job ID in the parent device
499 uint32_t maxJobId = 0;
500 parentDevice.walk([&](AIEX::CertJobOp certJobOp) {
501 maxJobId = std::max(maxJobId, certJobOp.getJobId());
502 });
503 uint32_t sectionJobId = maxJobId + 1;
504
505 auto jobOp = AIEX::CertJobOp::create(rewriter, op.getLoc(), sectionJobId);
506 Block *jobBlock = new Block();
507 jobOp.getBody().push_back(jobBlock);
508 rewriter.setInsertionPointToStart(jobBlock);
509
510 // Find the "configure" sequence content from the referenced device
511 // It could be either:
512 // 1. Still a RuntimeSequenceOp named "configure" (if that device hasn't
513 // been processed yet)
514 // 2. Already converted to cert.page/cert.job (if that device was processed
515 // first)
516
517 AIE::RuntimeSequenceOp configureSeq = nullptr;
518 referencedDevice.walk([&](AIE::RuntimeSequenceOp seq) {
519 if (seq.getSymName() == "configure") {
520 configureSeq = seq;
521 }
522 });
523
524 if (configureSeq) {
525 // Clone operations from the "configure" runtime_sequence
526 IRMapping mapper;
527 for (Operation &o : configureSeq.getRegion().front().getOperations()) {
528 if (isa<AIE::EndOp>(o))
529 continue;
530 rewriter.clone(o, mapper);
531 }
532 } else {
533 // "configure" was already converted - find the first cert.page
534 // (configuration page)
535 AIEX::CertPageOp configPage = nullptr;
536 for (auto &op : referencedDevice.getBody()->getOperations()) {
537 if (auto page = dyn_cast<AIEX::CertPageOp>(op)) {
538 configPage = page;
539 break; // Take first page (configure)
540 }
541 }
542
543 if (configPage) {
544 // Clone operations from the config page's job
545 IRMapping mapper;
546 for (auto &op : configPage.getBody().front().getOperations()) {
547 if (auto job = dyn_cast<AIEX::CertJobOp>(op)) {
548 for (Operation &jobOp : job.getBody().front().getOperations()) {
549 if (isa<AIE::EndOp>(jobOp))
550 continue;
551 rewriter.clone(jobOp, mapper);
552 }
553 break; // Only clone first job
554 }
555 }
556 }
557 }
558
559 // Ensure terminators
560 AIEX::CertJobOp::ensureTerminator(jobOp.getBody(), rewriter, op.getLoc());
561 AIEX::CertPageOp::ensureTerminator(pageOp.getBody(), rewriter, op.getLoc());
562 AIEX::CertSectionOp::ensureTerminator(sectionOp.getBody(), rewriter,
563 op.getLoc());
564
565 // At the configure call site, replace with load_pdi, reusing the PDI ID
566 // already assigned to this device if present (or assigning a new one).
567 uint32_t pdiId = getOrAssignPdiId(parentDevice, deviceSymName);
568
569 rewriter.setInsertionPoint(op);
570 AIEX::CertLoadPdiOp::create(
571 rewriter, op.getLoc(), rewriter.getUI32IntegerAttr(pdiId),
572 FlatSymbolRefAttr::get(rewriter.getContext(), deviceSymName));
573
574 // Clone configure body operations after the load_pdi
575 // Operations that reference external values were not cloned into the
576 // section, so we clone them here into the main control flow
577 IRMapping bodyMapper;
578 for (Operation &o : op.getRegion().front().getOperations()) {
579 if (isa<AIE::EndOp>(o))
580 continue;
581 rewriter.clone(o, bodyMapper);
582 }
583
584 // Erase the original configure op
585 rewriter.eraseOp(op);
586 return success();
587 }
588};
589
590struct NpuAddressPatchToCertApplyOffset57
591 : OpConversionPattern<AIEX::NpuAddressPatchOp> {
592 using OpConversionPattern::OpConversionPattern;
593
594 LogicalResult
595 matchAndRewrite(AIEX::NpuAddressPatchOp op, OpAdaptor adaptor,
596 ConversionPatternRewriter &rewriter) const override {
597 // find the previous blockwrite operation
598 Block::iterator it(op);
599 while (it != op->getBlock()->begin()) {
600 --it;
601 auto blockWriteOp = dyn_cast<AIEX::NpuBlockWriteOp>(*it);
602 if (!blockWriteOp)
603 continue;
604
605 const auto &tm = AIE::getTargetModel(op);
606 uint32_t addr = op.getAddr();
607 int col = (addr >> tm.getColumnShift()) & 0x1f;
608 int row = (addr >> tm.getRowShift()) & 0x1f;
609 if (!tm.isValidTile({col, row}))
610 return failure();
611
612 // if it's not a matching blockwrite, give up.
613 if (blockWriteOp.getAddress() + tm.getDmaBdAddressOffset(col, row) !=
614 addr)
615 break;
616
617 Value data = blockWriteOp.getData();
618 auto getGlobalOp = dyn_cast<memref::GetGlobalOp>(data.getDefiningOp());
619 if (!getGlobalOp)
620 break;
621
622 // replace the address with the new address
623 std::optional<uint32_t> argIdx = op.getArgIdx();
624 if (!argIdx)
625 break;
626 rewriter.setInsertionPoint(blockWriteOp);
627 rewriter.replaceOpWithNewOp<AIEX::CertApplyOffset57Op>(
628 op, getGlobalOp.getName(), 1, *argIdx);
629 return success();
630 }
631
632 return failure();
633 }
634};
635
636struct MergeConsecutiveCertUcDmaWriteDesSyncOps
637 : OpRewritePattern<AIEX::CertUcDmaWriteDesSyncOp> {
638 using OpRewritePattern::OpRewritePattern;
639
640 LogicalResult matchAndRewrite(AIEX::CertUcDmaWriteDesSyncOp op,
641 PatternRewriter &rewriter) const override {
642 // Fusing the two enqueues sinks the earlier one to the later one's position
643 // in the job, so every op scanned over is an op the earlier DMA is moved
644 // past. Only ops with no architectural effect at all may be crossed; this
645 // is an allowlist rather than a denylist so a cert op added to this dialect
646 // later blocks the merge by default instead of silently becoming
647 // reorderable with respect to a DMA enqueue. cert.nop is the only such op
648 // today: it emits padding and carries no ordering, sync or side effect.
649 auto isTransparentToMerge = [](Operation *o) {
650 return isa<AIEX::CertNopOp>(o);
651 };
652
653 // Get the previous operation in the block
654 Block::iterator it(op);
655 AIEX::CertUcDmaWriteDesSyncOp prevWriteDesSync = nullptr;
656 while (it != op->getBlock()->begin() && !prevWriteDesSync) {
657 --it;
658 Operation *prevOp = &*it;
659 prevWriteDesSync = dyn_cast<AIEX::CertUcDmaWriteDesSyncOp>(prevOp);
660 if (!prevWriteDesSync && !isTransparentToMerge(prevOp))
661 return failure();
662 }
663 if (!prevWriteDesSync)
664 return failure();
665
666 // find the uc_dma_chain
667 StringRef sym_name = op.getSymbol();
668 StringRef prev_sym_name = prevWriteDesSync.getSymbol();
669 auto chain = dyn_cast_if_present<AIEX::CertUcDmaChainOp>(
670 op->getParentOfType<AIE::DeviceOp>().lookupSymbol(sym_name));
671 auto prevChain = dyn_cast_if_present<AIEX::CertUcDmaChainOp>(
672 prevWriteDesSync->getParentOfType<AIE::DeviceOp>().lookupSymbol(
673 prev_sym_name));
674 if (!chain || !prevChain)
675 return failure();
676
677 // The merge rewrites @chain's BD list in place and deletes @prevChain, so
678 // both symbols must be private to this pair of enqueues. Cert IR that is
679 // hand-authored rather than lowered from npu ops may enqueue one chain from
680 // several places: folding into a shared @chain would silently change those
681 // other enqueues, and erasing a shared @prevChain would leave a dangling
682 // symbol reference that neither the verifier nor the page cost model
683 // notices. Decline the merge instead.
684 auto deviceOp = op->getParentOfType<AIE::DeviceOp>();
685 auto isSingleUse = [&](AIEX::CertUcDmaChainOp c) {
686 auto uses = SymbolTable::getSymbolUses(c, deviceOp);
687 return uses && llvm::hasSingleElement(*uses);
688 };
689 if (!isSingleUse(chain) || !isSingleUse(prevChain))
690 return failure();
691
692 // Compute the size of the current and previous chains. If their combined
693 // data size is greater than the cert page size, then we cannot merge them.
694 //
695 // A chain costs 16 bytes of descriptor per BD on top of the BD's payload --
696 // the same accounting updateCostForOp (the page splitter's authoritative
697 // cost model) uses. Charging payload only under-reports a chain by 16 bytes
698 // per BD, which for many small BDs is most of its real size.
699 auto chainSize = [](AIEX::CertUcDmaChainOp c) {
700 uint32_t size = 0;
701 for (auto bdOp : c.getBody().front().getOps<AIEX::CertUcDmaBdOp>())
702 size += bdOp.getLength() * sizeof(int) + 16; // payload + bd descriptor
703 return size;
704 };
705 if ((chainSize(chain) + chainSize(prevChain)) >= cert_page_size)
706 return failure();
707
708 IRMapping map;
709 rewriter.setInsertionPointToStart(&chain.getBody().front());
710 for (auto &o : prevChain.getBody().front().getOperations()) {
711 auto bdOp = dyn_cast<AIEX::CertUcDmaBdOp>(o);
712 if (!bdOp)
713 continue;
714 AIEX::CertUcDmaBdOp::create(
715 rewriter, bdOp.getLoc(), bdOp.getRemoteAddress(),
716 bdOp.getLocalAddress(), bdOp.getLength(), true);
717 }
718 rewriter.eraseOp(prevChain);
719 rewriter.eraseOp(prevWriteDesSync);
720 return success();
721 }
722};
723
724struct SplitNpuBlockWriteOpPattern : OpRewritePattern<AIEX::NpuBlockWriteOp> {
725 using OpRewritePattern::OpRewritePattern;
726
727 LogicalResult matchAndRewrite(AIEX::NpuBlockWriteOp op,
728 PatternRewriter &rewriter) const override {
729
730 memref::GetGlobalOp dataOperand =
731 dyn_cast_or_null<memref::GetGlobalOp>(op.getData().getDefiningOp());
732 if (!dataOperand)
733 return failure();
734
735 MemRefType dataType = cast<MemRefType>(dataOperand.getResult().getType());
736 uint32_t dataSize = dataType.getNumElements();
737
738 uint32_t dataSizeBytes = dataSize * sizeof(int);
739 if (dataSizeBytes < cert_page_size)
740 return failure();
741
742 auto loc = op.getLoc();
743
744 // Calculate split point (split roughly in half)
745 uint32_t splitElements = dataSize / 2;
746 uint32_t firstChunkSize = splitElements;
747 uint32_t secondChunkSize = dataSize - splitElements;
748
749 // Find the original memref.global operation
750 auto deviceOp = op->getParentOfType<AIE::DeviceOp>();
751 auto originalGlobal = dyn_cast_if_present<memref::GlobalOp>(
752 deviceOp.lookupSymbol(dataOperand.getName()));
753 if (!originalGlobal)
754 return failure();
755
756 // Get the original data attribute
757 auto originalData = originalGlobal.getInitialValue();
758 if (!originalData)
759 return failure();
760
761 auto denseData = dyn_cast<DenseIntElementsAttr>(*originalData);
762 if (!denseData)
763 return failure();
764
765 // Split the data into two chunks
766 auto dataValues = denseData.getValues<APInt>();
767 std::vector<APInt> firstChunkData(dataValues.begin(),
768 dataValues.begin() + firstChunkSize);
769 std::vector<APInt> secondChunkData(dataValues.begin() + firstChunkSize,
770 dataValues.end());
771
772 // Create new global operations for the split data
773 auto elementType = rewriter.getI32Type();
774 auto firstChunkType = MemRefType::get({firstChunkSize}, elementType);
775 auto secondChunkType = MemRefType::get({secondChunkSize}, elementType);
776 TensorType firstTensorType =
777 RankedTensorType::get({firstChunkSize}, elementType);
778 TensorType secondTensorType =
779 RankedTensorType::get({secondChunkSize}, elementType);
780
781 auto firstChunkAttr =
782 DenseIntElementsAttr::get(firstTensorType, firstChunkData);
783 auto secondChunkAttr =
784 DenseIntElementsAttr::get(secondTensorType, secondChunkData);
785
786 // Generate unique names for the new globals
787 std::string firstName = dataOperand.getName().str() + "_split_0";
788 std::string secondName = dataOperand.getName().str() + "_split_1";
789
790 // Ensure unique names
791 int counter = 0;
792 while (deviceOp.lookupSymbol(firstName)) {
793 firstName =
794 dataOperand.getName().str() + "_split_0_" + std::to_string(counter++);
795 }
796 counter = 0;
797 while (deviceOp.lookupSymbol(secondName)) {
798 secondName =
799 dataOperand.getName().str() + "_split_1_" + std::to_string(counter++);
800 }
801
802 // Create the new global operations
803 rewriter.setInsertionPoint(originalGlobal);
804 memref::GlobalOp::create(rewriter, loc, firstName,
805 rewriter.getStringAttr("private"), firstChunkType,
806 firstChunkAttr, true, nullptr);
807
808 memref::GlobalOp::create(rewriter, loc, secondName,
809 rewriter.getStringAttr("private"), secondChunkType,
810 secondChunkAttr, true, nullptr);
811
812 // Create get_global operations for the new data
813 rewriter.setInsertionPoint(op);
814
815 auto firstGetGlobal =
816 memref::GetGlobalOp::create(rewriter, loc, firstChunkType, firstName);
817 auto secondGetGlobal =
818 memref::GetGlobalOp::create(rewriter, loc, secondChunkType, secondName);
819
820 uint32_t baseAddr = op.getAddress();
821
822 AIEX::NpuBlockWriteOp::create(rewriter, loc, baseAddr,
823 firstGetGlobal.getResult(), nullptr, nullptr,
824 nullptr);
825
826 AIEX::NpuBlockWriteOp::create(rewriter, loc, baseAddr + firstChunkSize * 4,
827 secondGetGlobal.getResult(), nullptr, nullptr,
828 nullptr);
829
830 // Replace the original operation
831 rewriter.eraseOp(op);
832
833 LLVM_DEBUG(llvm::outs()
834 << "Split NpuBlockWriteOp with data size: " << dataSizeBytes
835 << " bytes into chunks of " << firstChunkSize << " and "
836 << secondChunkSize << " elements\n");
837
838 return success();
839 }
840};
841
842struct AIENpuToCertPass
843 : xilinx::AIEX::impl::AIENpuToCertBase<AIENpuToCertPass> {
844 void runOnOperation() override {
845 auto moduleOp = getOperation();
846 if (failed(AIE::inlineWriteConfigs(moduleOp)))
847 return signalPassFailure();
848
849 // Collect all devices
850 llvm::SmallVector<AIE::DeviceOp, 4> devices;
851 for (auto &op : moduleOp.getBody()->getOperations()) {
852 if (auto deviceOp = dyn_cast<AIE::DeviceOp>(op)) {
853 devices.push_back(deviceOp);
854 }
855 }
856
857 // Process main device first (named "main" or specified by --device-name)
858 // so referenced devices' "configure" sequences can be found before they
859 // get converted to cert.job
860 AIE::DeviceOp mainDevice = nullptr;
861 for (auto dev : devices) {
862 if (dev.getSymName() == deviceName) {
863 mainDevice = dev;
864 processDevice(dev);
865 break;
866 }
867 }
868
869 if (!mainDevice) {
870 moduleOp.emitError("no device found matching --device-name \"")
871 << deviceName << "\"";
872 signalPassFailure();
873 return;
874 }
875
876 // Then process other devices
877 for (auto dev : devices) {
878 if (dev != mainDevice) {
879 processDevice(dev);
880 }
881 }
882
883 // Remove absorbed devices (keep only main device)
884 // Referenced devices have been absorbed as cert.sections, so remove them
885 llvm::SmallVector<AIE::DeviceOp, 4> devicesToRemove;
886 for (auto dev : devices) {
887 if (dev != mainDevice) {
888 devicesToRemove.push_back(dev);
889 }
890 }
891
892 for (auto dev : devicesToRemove) {
893 dev.erase();
894 }
895 }
896
897 void processDevice(AIE::DeviceOp currentDevice) {
898 auto moduleOp = currentDevice->getParentOfType<ModuleOp>();
899
900 // Identify and convert referenced "configure" sequences early, before the
901 // RuntimeSequenceOp conversion runs on other devices.
902 if (moduleOp) {
903
904 // Collect npu.load_pdi operations (before they are converted below)
905 llvm::SmallVector<StringRef, 4> referencedDeviceSyms;
906 currentDevice.walk([&](AIEX::NpuLoadPdiOp loadPdiOp) {
907 if (loadPdiOp.getDeviceRef())
908 referencedDeviceSyms.push_back(*loadPdiOp.getDeviceRef());
909 });
910
911 if (!referencedDeviceSyms.empty()) {
912 OpBuilder builder(&getContext());
913
914 for (StringRef refSymName : referencedDeviceSyms) {
915 // Skip if section already exists
916 if (currentDevice.lookupSymbol(refSymName))
917 continue;
918
919 // Find referenced device
920 AIE::DeviceOp refDevice = nullptr;
921 for (auto &op : moduleOp.getBody()->getOperations()) {
922 if (auto dev = dyn_cast<AIE::DeviceOp>(op)) {
923 if (dev.getSymName() == refSymName) {
924 refDevice = dev;
925 break;
926 }
927 }
928 }
929
930 if (!refDevice)
931 continue;
932
933 // Find "configure" runtime sequence
934 AIE::RuntimeSequenceOp configureSeq = nullptr;
935 refDevice.walk([&](AIE::RuntimeSequenceOp seq) {
936 if (seq.getSymName() == "configure") {
937 configureSeq = seq;
938 }
939 });
940
941 if (!configureSeq)
942 continue;
943
944 // Create cert.section in current device
945 builder.setInsertionPoint(currentDevice.getBody()->getTerminator());
946 auto sectionOp =
947 AIEX::CertSectionOp::create(builder, configureSeq.getLoc(),
948 builder.getStringAttr(refSymName));
949
950 Block *sectionBlock = new Block();
951 sectionOp.getBody().push_back(sectionBlock);
952 builder.setInsertionPointToStart(sectionBlock);
953
954 // Create page
955 auto pageOp =
956 AIEX::CertPageOp::create(builder, configureSeq.getLoc());
957 Block *pageBlock = new Block();
958 pageOp.getBody().push_back(pageBlock);
959 builder.setInsertionPointToStart(pageBlock);
960
961 // Get unique job ID
962 uint32_t maxJobId = 0;
963 currentDevice.walk([&](AIEX::CertJobOp certJobOp) {
964 maxJobId = std::max(maxJobId, certJobOp.getJobId());
965 });
966
967 // Create job
968 auto jobOp = AIEX::CertJobOp::create(builder, configureSeq.getLoc(),
969 maxJobId + 1);
970
971 // Clone configure sequence body into job
972 IRMapping mapper;
973 configureSeq.getRegion().cloneInto(&jobOp.getBody(), mapper);
974
975 // Ensure terminators
976 AIEX::CertJobOp::ensureTerminator(jobOp.getBody(), builder,
977 configureSeq.getLoc());
978 AIEX::CertPageOp::ensureTerminator(pageOp.getBody(), builder,
979 configureSeq.getLoc());
980 AIEX::CertSectionOp::ensureTerminator(sectionOp.getBody(), builder,
981 configureSeq.getLoc());
982 }
983 }
984 }
985
986 // Inline RunOps first
987 RewritePatternSet p_run_inline(&getContext());
988 p_run_inline.insert<RunOpInlining>(&getContext());
989 if (failed(applyPatternsGreedily(currentDevice, std::move(p_run_inline))))
990 return signalPassFailure();
991
992 // Then convert ConfigureOps to cert.section + cert.load_pdi
993 RewritePatternSet p_configure(&getContext());
994 p_configure.insert<ConfigureOpToCertSection>(&getContext());
995 if (failed(applyPatternsGreedily(currentDevice, std::move(p_configure))))
996 return signalPassFailure();
997
998 ConversionTarget target(getContext());
999 target.addIllegalOp<AIE::RuntimeSequenceOp>();
1000
1001 target.addLegalOp<AIEX::CertApplyOffset57Op>();
1002 target.addLegalOp<AIEX::CertJobOp>();
1003 target.addLegalOp<AIEX::CertPageOp>();
1004 target.addLegalOp<AIEX::CertSectionOp>();
1005 target.addLegalOp<AIEX::CertLoadPdiOp>();
1006 target.addLegalOp<AIEX::CertMaskWrite32Op>();
1007 target.addLegalOp<AIEX::CertMaskPoll32Op>();
1008 target.addLegalOp<AIEX::CertUcDmaWriteDesSyncOp>();
1009 target.addLegalOp<AIEX::CertUcDmaChainOp>();
1010 target.addLegalOp<AIEX::CertUcDmaBdOp>();
1011 target.addLegalOp<AIEX::CertWrite32Op>();
1012 target.addLegalOp<AIEX::CertWaitTCTSOp>();
1013 target.addLegalOp<AIEX::ConfigureOp>(); // TODO: Convert in separate pass
1014 target.addLegalDialect<AIE::AIEDialect>();
1015
1016 RewritePatternSet p0(&getContext());
1017 p0.insert<RuntimeSequenceToCertJob>(&getContext());
1018
1019 if (failed(applyPartialConversion(currentDevice, target, std::move(p0))))
1020 return signalPassFailure();
1021
1022 target.addIllegalOp<AIEX::NpuAddressPatchOp>();
1023
1024 // patch conversion must come before blockwrite conversion
1025 RewritePatternSet p1(&getContext());
1026 p1.insert<NpuAddressPatchToCertApplyOffset57>(&getContext());
1027
1028 if (failed(applyPartialConversion(currentDevice, target, std::move(p1))))
1029 return signalPassFailure();
1030
1031 // Split oversized NpuBlockWriteOps before lowering them to cert ops
1032 {
1033 RewritePatternSet p(&getContext());
1034 p.insert<SplitNpuBlockWriteOpPattern>(&getContext());
1035 if (failed(applyPatternsGreedily(getOperation(), std::move(p))))
1036 signalPassFailure();
1037 }
1038
1039 target.addIllegalOp<AIEX::NpuBlockWriteOp>();
1040 target.addIllegalOp<AIEX::NpuBlockWriteValuesOp>();
1041 target.addIllegalOp<AIEX::NpuMaskWrite32Op>();
1042 target.addIllegalOp<AIEX::NpuMaskPollOp>();
1043 target.addIllegalOp<AIEX::NpuSyncOp>();
1044 target.addIllegalOp<AIEX::NpuWrite32Op>();
1045 target.addIllegalOp<AIEX::NpuLoadPdiOp>();
1046
1047 RewritePatternSet p2(&getContext());
1048 p2.insert<NpuBlockWriteToCertUcDma>(&getContext());
1049 p2.insert<NpuMaskWrite32ToCertMaskWrite32>(&getContext());
1050 p2.insert<NpuMaskPollToCertMaskPoll32>(&getContext());
1051 p2.insert<NpuWrite32ToCertWrite32>(&getContext());
1052 p2.insert<NpuSyncToCertWaitTCTS>(&getContext());
1053 p2.insert<NpuLoadPdiToCertLoadPdi>(&getContext());
1054
1055 if (failed(applyPartialConversion(currentDevice, target, std::move(p2))))
1056 return signalPassFailure();
1057
1058 // Run the greedy driver with no patterns, purely for its DCE: p2
1059 // leaves each converted blockwrite's memref.get_global (and any constant
1060 // feeding it) dead, since the new op references the global by symbol
1061 // rather than by SSA value. CertJobOp's verifier rejects those ops
1062 RewritePatternSet cleanup(&getContext());
1063 if (failed(applyPatternsGreedily(getOperation(), std::move(cleanup))))
1064 return signalPassFailure();
1065
1066 // Convert referenced devices to cert.sections
1067 // Now that cert.load_pdi ops exist, find referenced devices and convert
1068 // their cert jobs into sections in this device
1069 {
1070 OpBuilder builder(&getContext());
1071
1072 // Find all cert.load_pdi in current device
1073 llvm::SetVector<StringRef> referencedDeviceNames;
1074 currentDevice.walk([&](AIEX::CertLoadPdiOp loadPdiOp) {
1075 StringRef refSymName = loadPdiOp.getSymbol();
1076 if (!currentDevice.lookupSymbol(refSymName))
1077 referencedDeviceNames.insert(refSymName);
1078 });
1079
1080 // For each referenced device, create a section with its cert jobs
1081 for (StringRef refSymName : referencedDeviceNames) {
1082 // Find the referenced device
1083 AIE::DeviceOp refDevice = nullptr;
1084 for (auto &op : moduleOp.getBody()->getOperations()) {
1085 if (auto dev = dyn_cast<AIE::DeviceOp>(op)) {
1086 if (dev.getSymName() == refSymName) {
1087 refDevice = dev;
1088 break;
1089 }
1090 }
1091 }
1092
1093 if (!refDevice)
1094 continue;
1095
1096 // Find all cert.page ops in the referenced device that are direct
1097 // children
1098 llvm::SmallVector<AIEX::CertPageOp, 4> certPages;
1099 for (auto &op : refDevice.getBody()->getOperations()) {
1100 if (auto pageOp = dyn_cast<AIEX::CertPageOp>(op)) {
1101 certPages.push_back(pageOp);
1102 }
1103 }
1104
1105 if (certPages.empty())
1106 continue;
1107
1108 // Create cert.section in current device
1109 builder.setInsertionPoint(currentDevice.getBody()->getTerminator());
1110 auto sectionOp = AIEX::CertSectionOp::create(
1111 builder, refDevice.getLoc(), builder.getStringAttr(refSymName));
1112
1113 Block *sectionBlock = new Block();
1114 sectionOp.getBody().push_back(sectionBlock);
1115
1116 // Clone all cert.page operations into the section
1117 IRMapping mapper;
1118 for (auto pageOp : certPages) {
1119 builder.setInsertionPointToEnd(sectionBlock);
1120 builder.clone(*pageOp.getOperation(), mapper);
1121 }
1122
1123 // Ensure section terminator
1124 AIEX::CertSectionOp::ensureTerminator(sectionOp.getBody(), builder,
1125 refDevice.getLoc());
1126 }
1127 }
1128
1129 // Clean up unused block arguments from cert.job operations
1130 currentDevice.walk([&](AIEX::CertJobOp jobOp) {
1131 Block &jobBlock = jobOp.getBody().front();
1132
1133 // Remove unused block arguments (in reverse order)
1134 for (int i = jobBlock.getNumArguments() - 1; i >= 0; --i) {
1135 BlockArgument arg = jobBlock.getArgument(i);
1136 if (arg.use_empty()) {
1137 jobBlock.eraseArgument(i);
1138 }
1139 }
1140 });
1141 }
1142};
1143
1144} // namespace
1145
1146// Instruction sizes follow the AIEBU CERT ISA specification so the running
1147// total here matches the bytes emitJob actually produces. Keep the cost model
1148// and emitter in sync when adding a cert op:
1149// an op the emitter can emit but this function ignores is counted as free,
1150// which makes the page look smaller than it is.
1151static void updateCostForOp(Operation &o, AIE::DeviceOp deviceOp,
1152 uint32_t &text_cost, uint32_t &data_cost) {
1153 // Several distinct op kinds share an encoded instruction size below --
1154 // an instruction-size table, not a copy-paste clone.
1155 // NOLINTBEGIN(bugprone-branch-clone)
1156 if (isa<AIEX::CertLocalBarrierOp>(o)) {
1157 text_cost += 4; // ISA_OPSIZE_LOCAL_BARRIER
1158 } else if (isa<AIEX::CertRemoteBarrierOp>(o)) {
1159 text_cost += 8; // ISA_OPSIZE_REMOTE_BARRIER
1160 } else if (isa<AIEX::CertRelAcqSyncOp>(o)) {
1161 text_cost += 12; // encoded REL_ACQ_SYNC instruction size
1162 } else if (isa<AIEX::CertWaitTCTSOp>(o)) {
1163 text_cost += 8; // wait tct
1164 } else if (isa<AIEX::CertMaskWrite32Op, AIEX::CertMaskPoll32Op>(o)) {
1165 text_cost += 16; // mask write / poll
1166 } else if (isa<AIEX::CertWrite32Op>(o)) {
1167 text_cost += 12; // ISA_OPSIZE_WRITE_32
1168 } else if (isa<AIEX::CertApplyOffset57Op>(o)) {
1169 text_cost += 8; // ISA_OPSIZE_APPLY_OFFSET_57
1170 } else if (isa<AIEX::CertNopOp>(o)) {
1171 text_cost += 4; // ISA_OPSIZE_NOP
1172 } else if (isa<AIEX::CertPreemptOp>(o)) {
1173 text_cost += 8; // ISA_OPSIZE_PREEMPT
1174 } else if (isa<AIEX::CertLoadPdiOp>(o)) {
1175 text_cost += 12; // ISA_OPSIZE_LOAD_PDI
1176 // NOLINTEND(bugprone-branch-clone)
1177 } else if (auto syncOp = dyn_cast<AIEX::CertUcDmaWriteDesSyncOp>(o)) {
1178 text_cost += 4; // ISA_OPSIZE_UC_DMA_WRITE_DES_SYNC
1179 // find the uc_dma_chain
1180 StringRef sym_name = syncOp.getSymbol();
1181 auto chain = dyn_cast_if_present<AIEX::CertUcDmaChainOp>(
1182 deviceOp.lookupSymbol(sym_name));
1183 if (!chain)
1184 return;
1185 for (auto bdOp : chain.getBody().front().getOps<AIEX::CertUcDmaBdOp>()) {
1186 data_cost += 16; // bd op
1187 StringRef data_sym_name = bdOp.getRemoteAddress();
1188 auto global = dyn_cast_if_present<memref::GlobalOp>(
1189 deviceOp.lookupSymbol(data_sym_name));
1190 if (!global)
1191 continue;
1192 auto initVal = global.getInitialValue();
1193 if (!initVal)
1194 continue;
1195 auto data = dyn_cast<DenseIntElementsAttr>(*initVal);
1196 if (!data)
1197 continue;
1198 data_cost += data.getNumElements() * 4; // 4 bytes per element
1199 }
1200 }
1201}
1202
1203static uint32_t estimateCost(AIEX::CertPageOp op, uint32_t split_target,
1204 AIEX::CertJobOp &split_job,
1205 Block::iterator &split_iter,
1206 bool &found_split_point) {
1207 uint32_t text_cost = 32; // page header: `.align 16` padding plus `.eop`
1208 uint32_t data_cost = 0;
1209 found_split_point = false;
1210 AIE::DeviceOp deviceOp = op->getParentOfType<AIE::DeviceOp>();
1211
1212 for (auto job : op.getBody().front().getOps<AIEX::CertJobOp>()) {
1213 // START_JOB precedes the body, so it is already spent at every candidate
1214 // split point inside this job; END_JOB is charged after the body below.
1215 text_cost += cert_job_start_cost;
1216 for (auto &o : job.getBody().front().getOperations()) {
1217 Block::iterator current(&o);
1218 if (!found_split_point && !isa<AIE::EndOp>(o) &&
1219 current != job.getBody().front().begin() &&
1220 (text_cost + data_cost) >= split_target) {
1221 split_job = job;
1222 split_iter = current;
1223 found_split_point = true;
1224 }
1225
1226 updateCostForOp(o, deviceOp, text_cost, data_cost);
1227
1228 if (!found_split_point && (text_cost + data_cost) >= split_target) {
1229 Block::iterator next = current;
1230 ++next;
1231 if (next != job.getBody().front().end() && !isa<AIE::EndOp>(*next)) {
1232 split_job = job;
1233 split_iter = next;
1234 found_split_point = true;
1235 } else if (!isa<AIE::EndOp>(o) &&
1236 current != job.getBody().front().begin()) {
1237 // Cutting after the last real op would leave an empty second half.
1238 // Cut before it; exclude the terminator to guarantee progress.
1239 split_job = job;
1240 split_iter = current;
1241 found_split_point = true;
1242 }
1243 }
1244 }
1245 text_cost += cert_job_end_cost;
1246 }
1247 return text_cost + data_cost;
1248}
1249
1250// A cut inside a barrier's [min, max] textual indices would separate its
1251// participants, which must remain co-resident (see
1252// checkLocalBarrierColocation).
1253static void computeBarrierSpans(AIEX::CertPageOp page,
1254 std::map<Operation *, int> &flatIndex,
1255 std::map<int, std::pair<int, int>> &spans) {
1256 int idx = 0;
1257 for (auto job : page.getBody().front().getOps<AIEX::CertJobOp>()) {
1258 for (auto &o : job.getBody().front().getOperations()) {
1259 if (isa<AIE::EndOp>(o))
1260 continue;
1261 flatIndex[&o] = idx;
1262 if (auto bar = dyn_cast<AIEX::CertLocalBarrierOp>(o)) {
1263 int id = bar.getLocalBarrierId();
1264 auto it = spans.find(id);
1265 if (it == spans.end())
1266 spans[id] = {idx, idx};
1267 else {
1268 it->second.first = std::min(it->second.first, idx);
1269 it->second.second = std::max(it->second.second, idx);
1270 }
1271 }
1272 ++idx;
1273 }
1274 }
1275}
1276
1277// A cut before flat index `p` (ops with index < p -> earlier page) separates a
1278// local_barrier group iff some participant is before and some at/after the cut.
1279static bool cutSeparatesBarrier(const std::map<int, std::pair<int, int>> &spans,
1280 int p) {
1281 return llvm::any_of(spans, [p](const auto &entry) {
1282 auto [lo, hi] = entry.second;
1283 return lo < p && hi >= p;
1284 });
1285}
1286
1287// Choose the barrier-safe cut closest to split_target, allowing both interior
1288// and job-boundary cuts. Each half needs content so repeated splitting
1289// progresses.
1290static bool findLegalSplitPoint(AIEX::CertPageOp page, uint32_t split_target,
1291 const std::map<Operation *, int> &flatIndex,
1292 const std::map<int, std::pair<int, int>> &spans,
1293 AIEX::CertJobOp &split_job,
1294 Block::iterator &split_iter) {
1295 AIE::DeviceOp deviceOp = page->getParentOfType<AIE::DeviceOp>();
1296 uint32_t text_cost = 32; // page header, mirrors estimateCost
1297 uint32_t data_cost = 0;
1298 bool found = false;
1299 uint32_t best_delta = 0;
1300 // Whether some job already visited holds a real op, i.e. whether cutting at
1301 // the current job's first op would leave content on the earlier page.
1302 bool sawOpInEarlierJob = false;
1303 for (auto job : page.getBody().front().getOps<AIEX::CertJobOp>()) {
1304 text_cost += cert_job_start_cost; // mirrors estimateCost
1305 bool sawOpInJob = false;
1306 for (auto &o : job.getBody().front().getOperations()) {
1307 if (isa<AIE::EndOp>(o))
1308 continue;
1309 Block::iterator current(&o);
1310 // Candidate: cut before `o`, legal as an interior cut when an op of this
1311 // job precedes it, or as a job-boundary cut when an earlier job does.
1312 // Admitting the latter matters for a page built of small jobs whose sizes
1313 // only sum over the limit: no job has an interior point worth cutting at,
1314 // yet a cut between two whole jobs splits it perfectly well.
1315 if (sawOpInJob || sawOpInEarlierJob) {
1316 auto fi = flatIndex.find(&o);
1317 int p = fi == flatIndex.end() ? 0 : fi->second;
1318 if (!cutSeparatesBarrier(spans, p)) {
1319 uint32_t cost = text_cost + data_cost;
1320 uint32_t delta =
1321 cost > split_target ? cost - split_target : split_target - cost;
1322 if (!found || delta < best_delta) {
1323 found = true;
1324 best_delta = delta;
1325 split_job = job;
1326 split_iter = current;
1327 }
1328 }
1329 }
1330 updateCostForOp(o, deviceOp, text_cost, data_cost);
1331 sawOpInJob = true;
1332 }
1333 text_cost += cert_job_end_cost; // mirrors estimateCost
1334 sawOpInEarlierJob |= sawOpInJob;
1335 }
1336 return found;
1337}
1338
1339namespace {
1340
1341// Pattern to isolate load_pdi and preempt operations into their own job and
1342// page According to CERT spec: "load_pdi and preempt should take one whole job
1343// which in turn should take one whole page"
1344struct IsolateFullPageOpsPattern : OpRewritePattern<AIEX::CertJobOp> {
1345 using OpRewritePattern::OpRewritePattern;
1346
1347 // Create an empty, terminated cert.page directly after `after`, in the same
1348 // block. Staying in `after`'s block is what keeps a page that was moved into
1349 // a cert.attach_to_group by placement lowering on its uC.
1350 static AIEX::CertPageOp makePageAfter(PatternRewriter &rewriter,
1351 Operation *after, Location loc) {
1352 rewriter.setInsertionPointAfter(after);
1353 auto page = AIEX::CertPageOp::create(rewriter, loc);
1354 page.getBody().push_back(new Block());
1355 AIEX::CertPageOp::ensureTerminator(page.getBody(), rewriter, loc);
1356 return page;
1357 }
1358
1359 LogicalResult matchAndRewrite(AIEX::CertJobOp jobOp,
1360 PatternRewriter &rewriter) const override {
1361
1362 // Find first load_pdi or preempt in this job
1363 Operation *fullPageOp = nullptr;
1364 Block::iterator fullPageOpIter;
1365 for (Block::iterator it = jobOp.getBody().front().begin();
1366 it != jobOp.getBody().front().end(); ++it) {
1367 if (isa<AIEX::CertLoadPdiOp, AIEX::CertPreemptOp>(*it)) {
1368 fullPageOp = &*it;
1369 fullPageOpIter = it;
1370 break;
1371 }
1372 }
1373
1374 if (!fullPageOp)
1375 return failure(); // No full-page op in this job
1376
1377 auto parentPage = jobOp->getParentOfType<AIEX::CertPageOp>();
1378 if (!parentPage)
1379 return failure(); // No parent page (unusual but skip)
1380
1381 // Real ops in this job (the terminator does not count).
1382 size_t opCount = 0;
1383 for (Operation &op : jobOp.getBody().front().getOperations())
1384 if (!isa<AIE::EndOp>(op))
1385 opCount++;
1386
1387 // Pages execute in order, so the rewrite has to preserve this job's
1388 // position among its *siblings*, not just relative to the parent page:
1389 // anchoring every new page on the parent page's boundary hoists the jobs
1390 // that surround this one across the isolated load_pdi/preempt. Nothing
1391 // downstream checks op order, so that reordering is silent.
1392 SmallVector<Operation *> leadingSiblings, trailingSiblings;
1393 bool pastSelf = false;
1394 for (Operation &op : parentPage.getBody().front()) {
1395 if (isa<AIE::EndOp>(op))
1396 continue;
1397 if (&op == jobOp.getOperation()) {
1398 pastSelf = true;
1399 continue;
1400 }
1401 (pastSelf ? trailingSiblings : leadingSiblings).push_back(&op);
1402 }
1403
1404 // Already properly isolated: sole op of the sole job of its page.
1405 if (opCount == 1 && leadingSiblings.empty() && trailingSiblings.empty())
1406 return failure();
1407
1408 auto loc = jobOp.getLoc();
1409
1410 // The parent page stays where it is and keeps the leading siblings; every
1411 // page created below is chained after it, in program order.
1412 Operation *anchor = parentPage.getOperation();
1413
1414 if (opCount == 1) {
1415 // The job is exactly the full-page op: it just needs a page of its own.
1416 auto isolatedPage = makePageAfter(rewriter, anchor, loc);
1417 jobOp->moveBefore(isolatedPage.getBody().front().getTerminator());
1418 anchor = isolatedPage;
1419 } else {
1420 // Mixed job: split it into up to three jobs, each on its own page, in
1421 // source order (ops before the full-page op, it, then the ops after).
1422 auto parentDevice = jobOp->getParentOfType<AIE::DeviceOp>();
1423 uint32_t maxJobId = 0;
1424 parentDevice.walk([&](AIEX::CertJobOp certJobOp) {
1425 maxJobId = std::max(maxJobId, certJobOp.getJobId());
1426 });
1427 uint32_t beforeJobId = jobOp.getJobId();
1428 uint32_t fullPageJobId = maxJobId + 1;
1429 uint32_t afterJobId = maxJobId + 2;
1430
1431 SmallVector<Operation *> beforeOps, afterOps;
1432 for (Block::iterator it = jobOp.getBody().front().begin();
1433 it != fullPageOpIter; ++it)
1434 if (!isa<AIE::EndOp>(*it))
1435 beforeOps.push_back(&*it);
1436 Block::iterator afterStart = std::next(fullPageOpIter);
1437 for (Block::iterator it = afterStart; it != jobOp.getBody().front().end();
1438 ++it)
1439 if (!isa<AIE::EndOp>(*it))
1440 afterOps.push_back(&*it);
1441
1442 auto emitJobPage = [&](uint32_t jobId, ArrayRef<Operation *> ops) {
1443 auto page = makePageAfter(rewriter, anchor, loc);
1444 rewriter.setInsertionPoint(page.getBody().front().getTerminator());
1445 auto job = AIEX::CertJobOp::create(rewriter, loc, jobId);
1446 job.getBody().push_back(new Block());
1447 AIEX::CertJobOp::ensureTerminator(job.getBody(), rewriter, loc);
1448 for (Operation *op : ops)
1449 op->moveBefore(job.getBody().front().getTerminator());
1450 anchor = page;
1451 };
1452
1453 if (!beforeOps.empty())
1454 emitJobPage(beforeJobId, beforeOps);
1455 emitJobPage(fullPageJobId, {fullPageOp});
1456 if (!afterOps.empty())
1457 emitJobPage(afterJobId, afterOps);
1458
1459 rewriter.eraseOp(jobOp);
1460 }
1461
1462 // The trailing siblings cannot stay on the parent page, which now precedes
1463 // the isolated page, so they move to a page of their own behind it.
1464 if (!trailingSiblings.empty()) {
1465 auto tailPage = makePageAfter(rewriter, anchor, loc);
1466 for (Operation *op : trailingSiblings)
1467 op->moveBefore(tailPage.getBody().front().getTerminator());
1468 }
1469
1470 if (llvm::all_of(parentPage.getBody().front(),
1471 [](Operation &op) { return isa<AIE::EndOp>(op); }))
1472 rewriter.eraseOp(parentPage);
1473
1474 return success();
1475 }
1476};
1477
1478struct SplitCertPageOpPattern : OpRewritePattern<AIEX::CertPageOp> {
1479 // `hadError` is raised when the pattern emits an error diagnostic. A rewrite
1480 // pattern returning failure() is ordinary control flow -- it does NOT fail
1481 // the pass -- so without this the "cannot split" error below would print and
1482 // the oversized page would still reach codegen. The pass checks the flag
1483 // after applyPatternsGreedily and calls signalPassFailure().
1484 SplitCertPageOpPattern(MLIRContext *context, bool *hadError)
1485 : OpRewritePattern(context), hadError(hadError) {}
1486
1487 LogicalResult matchAndRewrite(AIEX::CertPageOp op,
1488 PatternRewriter &rewriter) const override {
1489
1490 constexpr uint32_t split_threshold = cert_page_size;
1491
1492 AIEX::CertJobOp split_job;
1493 Block::iterator split_iter;
1494 bool found_split_point = false;
1495 uint32_t cost = estimateCost(op, cert_page_size / 2, split_job, split_iter,
1496 found_split_point);
1497 LLVM_DEBUG(llvm::outs()
1498 << "Estimate cost for page: " << " is " << cost << "\n");
1499
1500 if (cost < split_threshold)
1501 return failure();
1502
1503 bool isImplicit = op->hasAttr("cert.implicit");
1504
1505 std::map<Operation *, int> flatIndex;
1506 std::map<int, std::pair<int, int>> barrierSpans;
1507 computeBarrierSpans(op, flatIndex, barrierSpans);
1508
1509 // The initial estimate can miss a job-boundary cut or an earlier candidate;
1510 // search all positions before declaring the page unsplittable.
1511 if (!found_split_point)
1512 found_split_point =
1513 findLegalSplitPoint(op, cert_page_size / 2, flatIndex, barrierSpans,
1514 split_job, split_iter);
1515
1516 // Over the split trigger but with nowhere to cut. Every cut has to leave at
1517 // least one op on each side, so a page whose bulk sits in a single one-op
1518 // job offers none. Leave the page alone -- but if it is also over the hard
1519 // limit, it will not load, so say so instead of emitting a page the
1520 // firmware will reject.
1521 if (!found_split_point) {
1522 if (cost > cert_page_limit) {
1523 op.emitError() << "cert.page is an estimated " << cost
1524 << " bytes, over the " << cert_page_limit
1525 << "-byte microcontroller page limit, and offers no "
1526 "split point (a split must leave at least one op on "
1527 "each page); break the oversized job into smaller "
1528 "jobs";
1529 *hadError = true;
1530 }
1531 return failure();
1532 }
1533
1534 // never split a local_barrier participant set across an .eop
1535 // (G-localbar). If the cost-chosen split point would separate a barrier
1536 // group, retarget to a legal split point; if none exists the page cannot be
1537 // made legal, so fail the compile rather than emit something broken.
1538 // an implicit page (formed by formImplicitPages) groups jobs meant to
1539 // be cooperatively scheduled together; an explicit page is a user-authored
1540 // boundary. This governs the wording of the diagnostics below (the remedy
1541 // differs), not whether they fire.
1542 if (!barrierSpans.empty()) {
1543 auto fi = flatIndex.find(&*split_iter);
1544 int p = fi == flatIndex.end() ? 0 : fi->second;
1545 if (cutSeparatesBarrier(barrierSpans, p)) {
1546 if (!findLegalSplitPoint(op, cert_page_size / 2, flatIndex,
1547 barrierSpans, split_job, split_iter)) {
1548 // No split keeps every local_barrier group intact. Leave the
1549 // page whole: severing a barrier hangs the firmware, so an oversized
1550 // page is the better of the two. That is only survivable while the
1551 // page still fits, though -- past cert_page_limit neither option
1552 // works, and both fail silently at runtime, so diagnose and fail the
1553 // compile. The remedy differs by page kind: an implicit page just
1554 // needs a user-authored boundary in the right place, while an
1555 // explicit page is already where the user put it and has to be made
1556 // smaller or its participants regrouped. Raise `hadError` so the pass
1557 // actually fails -- returning failure() alone only tells the greedy
1558 // driver this pattern did not apply.
1559 if (cost > cert_page_limit) {
1560 if (isImplicit)
1561 op.emitError()
1562 << "implicit cert.page is an estimated " << cost
1563 << " bytes, over the " << cert_page_limit
1564 << "-byte microcontroller page limit, and cannot be split "
1565 "without separating a local_barrier participant set "
1566 "across pages; insert an explicit cert.page boundary to "
1567 "co-locate the barrier participants";
1568 else
1569 op.emitError()
1570 << "cert.page is an estimated " << cost << " bytes, over the "
1571 << cert_page_limit
1572 << "-byte microcontroller page limit, and cannot be split "
1573 "without separating a local_barrier participant set "
1574 "across pages; reduce the page's contents or regroup its "
1575 "local_barrier participants so that a legal split point "
1576 "exists";
1577 *hadError = true;
1578 }
1579 return failure();
1580 }
1581 }
1582 }
1583
1584 // splitting an implicit page turns jobs that were meant to run
1585 // cooperatively on one page into strictly sequential pages. Warn so the
1586 // user can insert an explicit boundary if the serialization is unintended.
1587 if (isImplicit)
1588 op.emitRemark()
1589 << "auto-split of implicit cert.page serializes cooperatively-"
1590 "scheduled jobs into separate sequential pages; insert an "
1591 "explicit "
1592 "cert.page boundary to control where the split occurs";
1593
1594 auto loc = op.getLoc();
1595
1596 // Preserve source order across both pages so cross-page dependencies stay
1597 // backward. A job-boundary cut moves split_job whole onto the later page.
1598 bool jobBoundaryCut = split_iter == split_job.getBody().front().begin();
1599 [[maybe_unused]] auto earlierJobHasRealOp = [&]() {
1600 for (auto job : op.getBody().front().getOps<AIEX::CertJobOp>()) {
1601 if (job == split_job)
1602 break;
1603 for (auto &o : job.getBody().front().getOperations())
1604 if (!isa<AIE::EndOp>(o))
1605 return true;
1606 }
1607 return false;
1608 };
1609 assert((!jobBoundaryCut || earlierJobHasRealOp()) &&
1610 "a job-boundary cut needs a preceding job with content, else the "
1611 "earlier page is empty and the split makes no progress");
1612
1613 // NOTE: the device-wide job-id shift below only keeps job ids unique per
1614 // page; job_id is a label, not an order, so it does not affect
1615 // emission/execution order, which is textual. A device-wide job-id renumber
1616 // is left for a follow-up. A job-boundary cut creates no new job -- every
1617 // job keeps its id, just on a different page -- so there is nothing to
1618 // renumber and the walk is skipped.
1619 if (!jobBoundaryCut)
1620 op->getParentOfType<AIE::DeviceOp>().walk([&](AIEX::CertJobOp certJobOp) {
1621 if (certJobOp.getJobId() > split_job.getJobId())
1622 certJobOp.setJobId(certJobOp.getJobId() + 1);
1623 });
1624
1625 auto cloneJobRange = [&](AIEX::CertJobOp sourceJob, uint32_t jobId,
1626 Block::iterator begin, Block::iterator end) {
1627 auto newJobOp = AIEX::CertJobOp::create(rewriter, loc, jobId);
1628 Block *newJobBlock = new Block();
1629 newJobOp.getBody().push_back(newJobBlock);
1630
1631 IRMapping mapper;
1632 Block &sourceBlock = sourceJob.getBody().front();
1633 for (BlockArgument arg : sourceBlock.getArguments()) {
1634 BlockArgument newArg =
1635 newJobBlock->addArgument(arg.getType(), arg.getLoc());
1636 mapper.map(arg, newArg);
1637 }
1638
1639 rewriter.setInsertionPointToStart(newJobBlock);
1640 for (Block::iterator oi = begin; oi != end; ++oi) {
1641 if (!isa<AIE::EndOp>(*oi))
1642 rewriter.clone(*oi, mapper);
1643 }
1644 AIEX::CertJobOp::ensureTerminator(newJobOp.getBody(), rewriter, loc);
1645 };
1646
1647 rewriter.setInsertionPoint(op);
1648 auto newPageOp0 = AIEX::CertPageOp::create(rewriter, loc);
1649 Block *newPageBlock0 = new Block();
1650 newPageOp0.getBody().push_back(newPageBlock0);
1651
1652 auto newPageOp1 = AIEX::CertPageOp::create(rewriter, loc);
1653 Block *newPageBlock1 = new Block();
1654 newPageOp1.getBody().push_back(newPageBlock1);
1655
1656 // propagate the implicit tag so a later re-split of either fragment
1657 // still knows it came from an implicit page and diagnoses consistently.
1658 if (isImplicit) {
1659 newPageOp0->setAttr("cert.implicit", rewriter.getUnitAttr());
1660 newPageOp1->setAttr("cert.implicit", rewriter.getUnitAttr());
1661 }
1662
1663 for (auto job : op.getBody().front().getOps<AIEX::CertJobOp>()) {
1664 if (job == split_job) {
1665 if (jobBoundaryCut) {
1666 // Whole job to the later page: partitioning it at its own first op
1667 // would leave an empty half behind on the earlier page.
1668 rewriter.setInsertionPointToEnd(newPageBlock1);
1669 cloneJobRange(job, job.getJobId(), job.getBody().front().begin(),
1670 job.getBody().front().end());
1671 continue;
1672 }
1673 rewriter.setInsertionPointToEnd(newPageBlock0);
1674 cloneJobRange(job, job.getJobId(), job.getBody().front().begin(),
1675 split_iter);
1676
1677 rewriter.setInsertionPointToEnd(newPageBlock1);
1678 cloneJobRange(job, job.getJobId() + 1, split_iter,
1679 job.getBody().front().end());
1680 continue;
1681 }
1682
1683 if (job->isBeforeInBlock(split_job)) {
1684 rewriter.setInsertionPointToEnd(newPageBlock0);
1685 cloneJobRange(job, job.getJobId(), job.getBody().front().begin(),
1686 job.getBody().front().end());
1687 } else {
1688 rewriter.setInsertionPointToEnd(newPageBlock1);
1689 cloneJobRange(job, job.getJobId(), job.getBody().front().begin(),
1690 job.getBody().front().end());
1691 }
1692 }
1693
1694 AIEX::CertPageOp::ensureTerminator(newPageOp0.getBody(), rewriter, loc);
1695 AIEX::CertPageOp::ensureTerminator(newPageOp1.getBody(), rewriter, loc);
1696
1697 rewriter.eraseOp(op);
1698 return success();
1699 }
1700
1701 bool *hadError;
1702};
1703
1704// Explicit group 0 shares the default stream. Flatten it before page formation
1705// so it does not introduce an artificial scheduling boundary.
1706static void inlineDefaultGroup(AIE::DeviceOp dev) {
1707 Block *body = dev.getBody();
1708 SmallVector<AIEX::CertAttachToGroupOp> zeroGroups;
1709 for (auto grp : body->getOps<AIEX::CertAttachToGroupOp>())
1710 if (grp.getGroupId() == 0)
1711 zeroGroups.push_back(grp);
1712 for (auto grp : zeroGroups) {
1713 SmallVector<Operation *> inner;
1714 for (Operation &op : grp.getBody().front())
1715 if (!isa<AIE::EndOp>(op))
1716 inner.push_back(&op);
1717 for (Operation *op : inner)
1718 op->moveBefore(grp.getOperation());
1719 grp.erase();
1720 }
1721}
1722
1723// Group a block's maximal runs of contiguous top-level cert.job ops into
1724// implicit cert.page ops (only an explicit cert.page delimits; the
1725// {cert.configure} job stays isolated in its own page). Applied per uC stream.
1726static void formImplicitPagesInBlock(Block *body, OpBuilder &builder) {
1727 // Snapshot top-level ops; the body is mutated while iterating.
1728 SmallVector<Operation *> topOps;
1729 for (Operation &op : *body)
1730 topOps.push_back(&op);
1731
1732 // `cert.implicit` marks a page formed from a run of *user* jobs -- a
1733 // boundary the author could have drawn but didn't. The splitter's diagnostics
1734 // are keyed off it and all advise editing page boundaries, so it must not be
1735 // set on a page the user cannot restructure. The compiler-synthesized
1736 // @configure page is exactly that case, and it is isolated by construction,
1737 // so the tag would buy nothing there anyway.
1738 auto openPage = [&](Location loc, Operation *before,
1739 bool implicit) -> AIEX::CertPageOp {
1740 builder.setInsertionPoint(before);
1741 auto page = AIEX::CertPageOp::create(builder, loc);
1742 if (implicit)
1743 page->setAttr("cert.implicit", builder.getUnitAttr());
1744 Block *b = new Block();
1745 page.getBody().push_back(b);
1746 AIEX::CertPageOp::ensureTerminator(page.getBody(), builder, loc);
1747 return page;
1748 };
1749
1750 // The (tile, channel) actors a job wait_tcts on, keyed exactly as
1751 // AIECertVerify's G-tct check (checkWaitTctsUniqueness) keys them so the two
1752 // stay consistent.
1753 auto waitTctsActors = [](AIEX::CertJobOp job) {
1754 std::set<std::pair<int, int>> actors;
1755 job.walk([&](AIEX::CertWaitTCTSOp wait) {
1756 actors.insert({wait.getTileId(), wait.getChannelId()});
1757 });
1758 return actors;
1759 };
1760
1761 AIEX::CertPageOp currentPage; // null => no open implicit page
1762 // Actors already waited on by the jobs accumulated into `currentPage`.
1763 std::set<std::pair<int, int>> pageActors;
1764 auto closePage = [&]() {
1765 currentPage = nullptr;
1766 pageActors.clear();
1767 };
1768
1769 for (Operation *op : topOps) {
1770 if (auto job = dyn_cast<AIEX::CertJobOp>(op)) {
1771 if (job->hasAttr("cert.configure")) {
1772 // Keep the configuration job isolated in its own page. Not tagged
1773 // implicit: the user did not author this job and cannot add a boundary
1774 // inside it, so the auto-split remark would be noise on every compile
1775 // whose
1776 // config transaction is large enough to split (and it usually is).
1777 auto page = openPage(job.getLoc(), job, /*implicit=*/false);
1778 job->moveBefore(page.getBody().front().getTerminator());
1779 closePage();
1780 continue;
1781 }
1782 // G-tct: at most one job per page may wait on a given actor. Two jobs
1783 // that each legally wait on the same (tile, channel) -- an ordinary shape
1784 // for two runtime sequences sharing a shim channel -- are only legal
1785 // while they sit on separate pages, so grouping them would manufacture a
1786 // verifier error out of valid input. Cut a page boundary instead, exactly
1787 // as an over-size job would.
1788 auto actors = waitTctsActors(job);
1789 if (currentPage &&
1790 llvm::any_of(actors, [&](const std::pair<int, int> &actor) {
1791 return pageActors.count(actor);
1792 }))
1793 closePage();
1794 if (!currentPage)
1795 currentPage = openPage(job.getLoc(), job, /*implicit=*/true);
1796 job->moveBefore(currentPage.getBody().front().getTerminator());
1797 pageActors.insert(actors.begin(), actors.end());
1798 } else if (isa<AIEX::CertPageOp>(op)) {
1799 // An explicit page delimits the current implicit run.
1800 closePage();
1801 }
1802 // All other ops (structural / control containers) do not delimit a run.
1803 }
1804}
1805
1806static void formImplicitPages(AIE::DeviceOp dev) {
1807 OpBuilder builder(dev.getContext());
1808 formImplicitPagesInBlock(dev.getBody(), builder);
1809 // Each other uC (attach_to_group) forms implicit pages within its own stream,
1810 // so adjacent jobs on that uC are cooperatively co-located too.
1811 SmallVector<AIEX::CertAttachToGroupOp> groups;
1812 for (auto grp : dev.getBody()->getOps<AIEX::CertAttachToGroupOp>())
1813 groups.push_back(grp);
1814 for (auto grp : groups)
1815 formImplicitPagesInBlock(&grp.getBody().front(), builder);
1816}
1817
1818// Force the implicit configuration page to be the first page on uC0:
1819// @configure sets up the device (DMA/lock/core config), so it must strictly
1820// precede all other uC0 pages regardless of where it appeared textually. The
1821// config job is already isolated in its own page by formImplicitPages; here we
1822// just hoist that page to the front of the device body (structural ops are not
1823// control code, so position relative to them does not matter).
1824static void moveConfigPageFirst(AIE::DeviceOp dev) {
1825 Block *body = dev.getBody();
1826 AIEX::CertPageOp configPage;
1827 for (auto page : body->getOps<AIEX::CertPageOp>()) {
1828 for (auto job : page.getBody().front().getOps<AIEX::CertJobOp>()) {
1829 if (job->hasAttr("cert.configure")) {
1830 configPage = page;
1831 break;
1832 }
1833 }
1834 if (configPage)
1835 break;
1836 }
1837 if (configPage && &body->front() != configPage.getOperation())
1838 configPage->moveBefore(&body->front());
1839}
1840
1841// Move nonzero placements into their controller containers, retaining order.
1842static void lowerPagePlacementToGroups(AIE::DeviceOp dev) {
1843 Block *body = dev.getBody();
1844
1845 // Collect placed top-level pages by resolved group id, in first-seen order.
1846 std::map<int32_t, SmallVector<AIEX::CertPageOp, 4>> byGroup;
1847 for (auto page : body->getOps<AIEX::CertPageOp>()) {
1848 if (auto placement = page.getPlacementAttr()) {
1849 auto gid = static_cast<int32_t>(placement.getInt());
1850 if (gid != 0)
1851 byGroup[gid].push_back(page);
1852 }
1853 }
1854
1855 OpBuilder builder(dev.getContext());
1856 for (auto &entry : byGroup) {
1857 int32_t gid = entry.first;
1858 SmallVector<AIEX::CertPageOp, 4> &pages = entry.second;
1859
1860 // Create the group op where the first placed page currently lives (avoids
1861 // assuming anything about a device terminator).
1862 builder.setInsertionPoint(pages.front());
1863 auto grp = AIEX::CertAttachToGroupOp::create(builder, dev.getLoc(), gid);
1864 Block *grpBlock = new Block();
1865 grp.getBody().push_back(grpBlock);
1866
1867 // Move the placed pages into the group, in order, dropping the
1868 // now-redundant placement attribute (the group encodes it).
1869 for (AIEX::CertPageOp page : pages) {
1870 page->removeAttr("placement");
1871 page->moveBefore(grpBlock, grpBlock->end());
1872 }
1873 AIEX::CertAttachToGroupOp::ensureTerminator(grp.getBody(), builder,
1874 dev.getLoc());
1875 }
1876}
1877
1878struct AIECertPagesPass
1879 : xilinx::AIEX::impl::AIECertPagesBase<AIECertPagesPass> {
1880 void runOnOperation() override {
1881 inlineDefaultGroup(getOperation());
1882
1883 formImplicitPages(getOperation());
1884
1885 // Force the @configure page to be first on uC0.
1886 moveConfigPageFirst(getOperation());
1887
1888 // Lower cert.page placement to cert.attach_to_group BEFORE isolate
1889 // and split. Those rewrites create replacement pages adjacent to the
1890 // original (same parent block) with the no-placement builder; running the
1891 // placement grouping first means placed pages already live inside their
1892 // cert.attach_to_group, so any pages the later rewrites spawn stay in the
1893 // same group by position. (Doing this last silently dropped placement from
1894 // split/isolated pages, emitting them on group 0.)
1895 lowerPagePlacementToGroups(getOperation());
1896
1897 // First, isolate load_pdi and preempt operations into their own job/page
1898 RewritePatternSet isolatePatterns(&getContext());
1899 isolatePatterns.insert<IsolateFullPageOpsPattern>(&getContext());
1900 if (failed(
1901 applyPatternsGreedily(getOperation(), std::move(isolatePatterns))))
1902 signalPassFailure();
1903
1904 // Then apply the page splitting pattern. `splitError` catches diagnostics
1905 // the pattern emits while declining to split (see SplitCertPageOpPattern):
1906 // applyPatternsGreedily reports only whether the rewrite converged, so an
1907 // illegal-to-split page would otherwise print an error and still compile.
1908 bool splitError = false;
1909 RewritePatternSet p1(&getContext());
1910 p1.insert<SplitCertPageOpPattern>(&getContext(), &splitError);
1911 if (failed(applyPatternsGreedily(getOperation(), std::move(p1))))
1912 signalPassFailure();
1913 if (splitError)
1914 signalPassFailure();
1915
1916 // Add the merge pattern for CertUcDmaWriteDesSyncOps
1917 RewritePatternSet p3(&getContext());
1918 p3.insert<MergeConsecutiveCertUcDmaWriteDesSyncOps>(&getContext());
1919 if (failed(applyPatternsGreedily(getOperation(), std::move(p3))))
1920 return signalPassFailure();
1921 }
1922};
1923
1924} // namespace
1925
1926std::unique_ptr<OperationPass<mlir::ModuleOp>> AIEX::createAIENpuToCertPass() {
1927 return std::make_unique<AIENpuToCertPass>();
1928}
1929
1930std::unique_ptr<OperationPass<AIE::DeviceOp>> AIEX::createAIECertPagesPass() {
1931 return std::make_unique<AIECertPagesPass>();
1932}
std::unique_ptr< mlir::OperationPass< mlir::ModuleOp > > createAIENpuToCertPass()
std::optional< uint32_t > getConstantIntOperand(mlir::Value v)
std::unique_ptr< mlir::OperationPass< AIE::DeviceOp > > createAIECertPagesPass()
mlir::LogicalResult inlineWriteConfigs(mlir::ModuleOp module)
const AIETargetModel & getTargetModel(mlir::Operation *op)