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