MLIR-AIE 1.4.5.dev
AIEMaterializeRuntimeSequences.cpp
Go to the documentation of this file.
1//===- AIEMaterializeRuntimeSequences.cpp -----------------------*- C++ -*-===//
2//
3// Copyright (C) 2025 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7
13#include "aie/Targets/AIERT.h"
14
15#include "mlir/Dialect/Arith/IR/Arith.h"
16#include "mlir/Dialect/MemRef/IR/MemRef.h"
17#include "mlir/IR/IRMapping.h"
18#include "mlir/IR/PatternMatch.h"
19#include "mlir/Pass/Pass.h"
20#include "mlir/Transforms/DialectConversion.h"
21#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
22#include "mlir/Transforms/WalkPatternRewriteDriver.h"
23#include "llvm/ADT/STLExtras.h"
24
25namespace xilinx::AIEX {
26#define GEN_PASS_DEF_AIEMATERIALIZERUNTIMESEQUENCES
27#include "aie/Dialect/AIEX/Transforms/AIEXPasses.h.inc"
28} // namespace xilinx::AIEX
29
30#define DEBUG_TYPE "aie-materialize-runtime-sequence"
31
32using namespace mlir;
33using namespace xilinx;
34using namespace xilinx::AIEX;
35
37 AnalysisManager &analysisManager;
38
39 // if invalid, analysis failed and results should not be considered
40 bool isValid = false;
41
42 // Call graph is cyclic
43 bool isCyclic = false;
44
45 RuntimeCallGraphCyclicityAnalysis(Operation *op, AnalysisManager &am)
46 : analysisManager(am) {
47 AIE::RuntimeSequenceOp runtimeSequenceOp =
48 llvm::dyn_cast<AIE::RuntimeSequenceOp>(op);
49 if (!runtimeSequenceOp) {
50 op->emitError("RuntimeCallGraphCyclicityAnalysis can only be called on "
51 "aiex.runtime_sequence operations.");
52 return;
53 }
54
55 // Use DFS with a stack to detect cycles
56 // A cycle exists if we encounter a sequence already on the current path
57 llvm::DenseSet<AIE::RuntimeSequenceOp> callStack;
58 llvm::DenseSet<AIE::RuntimeSequenceOp> visited;
60
61 std::function<bool(AIE::RuntimeSequenceOp)> hasCycle =
62 [&](AIE::RuntimeSequenceOp seq) -> bool {
63 if (callStack.contains(seq)) {
64 return true; // Found a cycle
65 }
66 if (visited.contains(seq)) {
67 return false; // Already checked this sequence
68 }
69
70 callStack.insert(seq);
71 visited.insert(seq);
72
73 // Check all sequences called by this one
74 bool foundCycle = false;
75 seq.walk([&](RunOp runOp) {
76 if (AIE::RuntimeSequenceOp callee =
77 runOp.getCalleeRuntimeSequenceOp(&calleeNames)) {
78 if (hasCycle(callee)) {
79 foundCycle = true;
80 return WalkResult::interrupt();
81 }
82 }
83 return WalkResult::advance();
84 });
85
86 callStack.erase(seq);
87 return foundCycle;
88 };
89
90 if (hasCycle(runtimeSequenceOp)) {
91 isCyclic = true;
92 isValid = true;
93 return;
94 }
95 isCyclic = false;
96 isValid = true;
97 }
98};
99
100// Turn aie.configure @device into aie.run %.. @configure
101// TODO: add check that liveness of two aie.configures do not overlap
102// (i.e., when we configure A, then configure B, cannot call runtime sequence of
103// A after configuring B)
104// TODO: add code to remove repeated @configure ops
105struct InsertLoadPdiForConfigurePattern : RewritePattern {
106
108 PatternBenefit benefit = 1)
109 : RewritePattern(ConfigureOp::getOperationName(), benefit, context) {}
110
111 LogicalResult matchAndRewrite(Operation *op,
112 PatternRewriter &rewriter) const override {
113 ConfigureOp configureOp = llvm::dyn_cast<ConfigureOp>(op);
114 if (!configureOp) {
115 return failure();
116 }
117
118 // LoadPDI resets the whole device, hence cannot do partial reconfiguration;
119 // therefore, this only supports top-level configure ops
120 if (!llvm::isa<AIE::RuntimeSequenceOp>(configureOp->getParentOp())) {
121 return failure();
122 }
123
124 AIE::DeviceOp referencedDevice = configureOp.getReferencedDeviceOp();
125 if (!referencedDevice) {
126 configureOp.emitError("Referenced symbol is not a device");
127 return failure();
128 }
129
130 Block *configureBlock;
131 if (configureOp.getBody().empty()) {
132 configureBlock = rewriter.createBlock(&configureOp.getBody());
133 } else {
134 configureBlock = &configureOp.getBody().front();
135 }
136
137 rewriter.setInsertionPointToStart(configureBlock);
138 AIEX::NpuLoadPdiOp::create(
139 rewriter, configureOp.getLoc(),
140 FlatSymbolRefAttr::get(referencedDevice.getSymNameAttr()),
141 /*id=*/nullptr, /*size=*/nullptr, /*address=*/nullptr,
142 /*expand_mode=*/configureOp.getExpandModeAttr());
143
144 return success();
145 }
146};
147
148// Collects all external SSA values referenced by an operation (and its nested
149// operations).
150// 1. Collects SSA values from the operation's operands.
151// 2. Recursively walks through all operations in the operation's regions.
152// 3. For each nested operation, collects SSA values from its operands.
153// 4. Skips values that are already in argMap or defined within the operation.
154// 5. For memref.subview operations, traces to the root block argument
155static void
156collectReferencedSSAValues(Operation *op, const IRMapping &argMap,
157 llvm::SetVector<Value> &referencedValues) {
158
159 auto processValue = [&](Value operand) {
160 if (argMap.contains(operand)) {
161 return;
162 }
163
164 // If this is a subview, trace to the root block argument
165 if (auto traceResult = traceSubviewToBlockArgument(operand)) {
166 // Check if the root argument is already mapped
167 if (!argMap.contains(traceResult->rootArg)) {
168 referencedValues.insert(traceResult->rootArg);
169 }
170 return;
171 }
172
173 // Not a subview chain leading to block arg, add as-is
174 referencedValues.insert(operand);
175 };
176
177 // Collect SSA values from the operation's direct operands.
178 for (Value operand : op->getOperands()) {
179 processValue(operand);
180 }
181
182 // Recursively collect SSA values from nested operations in all regions.
183 for (Region &region : op->getRegions()) {
184 region.walk([&](Operation *nestedOp) {
185 for (Value operand : nestedOp->getOperands()) {
186 if (argMap.contains(operand)) {
187 continue;
188 }
189
190 // Check if defined within the parent operation.
191 if (Operation *defOp = operand.getDefiningOp()) {
192 if (op->isProperAncestor(defOp)) {
193 continue;
194 }
195 } else if (auto blockArg = llvm::dyn_cast<BlockArgument>(operand)) {
196 // A block argument has no defining op, so the check above cannot see
197 // it. One belonging to a region nested inside `op` -- an scf.for
198 // induction variable, most commonly -- is nonetheless defined within
199 // `op` and must not be collected as an external reference: it would
200 // reach copyReferencedSSAValues, whose getDefiningOp() is null, and
201 // fail with "Referenced value is not defined by an operation".
202 Operation *owner = blockArg.getOwner()->getParentOp();
203 if (owner && (owner == op || op->isProperAncestor(owner))) {
204 continue;
205 }
206 }
207
208 processValue(operand);
209 }
210 });
211 }
212}
213
214// Returns `base`, or `base_<n>` if the caller device already holds `base`, and
215// records the returned name in `allSymbolNames`.
216static std::string
217uniqueSymbolName(MLIRContext *ctx, llvm::StringRef base,
218 llvm::SetVector<SymbolRefAttr> &allSymbolNames) {
219 std::string uniqueName = base.str();
220 unsigned uniquingCounter = 0;
221 while (allSymbolNames.count(SymbolRefAttr::get(ctx, uniqueName))) {
222 uniqueName = base.str() + "_" + std::to_string(uniquingCounter);
223 uniquingCounter++;
224 }
225 allSymbolNames.insert(SymbolRefAttr::get(ctx, uniqueName));
226 return uniqueName;
227}
228
229// Return the operation in the caller device that stands for `op`, cloning `op`
230// on first use. `clonedDefs` spans the aiex.run calls of one caller device, so
231// several calls that name one definition share one clone and one symbol.
232static Operation *
233getOrClone(PatternRewriter &rewriter, Operation *op, IRMapping &argMap,
234 llvm::DenseMap<Operation *, Operation *> &clonedDefs,
235 mlir::OpBuilder::InsertPoint &insertPoint,
236 llvm::SetVector<SymbolRefAttr> &allSymbolNames) {
237 auto it = clonedDefs.find(op);
238 if (it == clonedDefs.end()) {
239 rewriter.restoreInsertionPoint(insertPoint);
240 Operation *clone = rewriter.clone(*op, argMap);
241 if (auto name = clone->getAttrOfType<StringAttr>(
242 SymbolTable::getSymbolAttrName())) {
243 clone->setAttr(SymbolTable::getSymbolAttrName(),
244 rewriter.getStringAttr(uniqueSymbolName(
245 op->getContext(), name.getValue(), allSymbolNames)));
246 }
247 it = clonedDefs.try_emplace(op, clone).first;
248 insertPoint = rewriter.saveInsertionPoint();
249 }
250 argMap.map(op->getResults(), it->second->getResults());
251 return it->second;
252}
253
254// Copies SSA value definitions into the caller device.
255// Supports `aie.tile`, `aie.lock` and `aie.buffer` operations.
256// Updates argMap to map old values to new/existing values.
257static LogicalResult copyReferencedSSAValues(
258 PatternRewriter &rewriter, const llvm::SetVector<Value> &referencedValues,
259 AIE::DeviceOp callerDevice, IRMapping &argMap,
260 llvm::DenseMap<Operation *, Operation *> &clonedDefs,
261 mlir::OpBuilder::InsertPoint &clonedSSAInsertPoint,
262 llvm::SetVector<SymbolRefAttr> &allSymbolNames, Operation *errorReportOp) {
263
264 llvm::SetVector<Value> referencedValuesToVisit = referencedValues;
265 std::vector<Operation *> referencedOpsToClone = {};
266 while (!referencedValuesToVisit.empty()) {
267 Value referencedValue = referencedValuesToVisit.pop_back_val();
268 Operation *definingOp = referencedValue.getDefiningOp();
269 if (!definingOp) {
270 return errorReportOp->emitError()
271 << "Referenced value is not defined by an operation";
272 }
273 if (llvm::find(referencedOpsToClone, definingOp) !=
274 referencedOpsToClone.end()) {
275 continue;
276 }
277
278 if (auto tileOp = llvm::dyn_cast<AIE::TileOp>(definingOp)) {
279 referencedOpsToClone.insert(referencedOpsToClone.begin(), definingOp);
280 } else if (auto lockOp = llvm::dyn_cast<AIE::LockOp>(definingOp)) {
281 Value lockTile = lockOp.getTile();
282 if (lockTile) {
283 referencedValuesToVisit.insert(lockTile);
284 }
285 referencedOpsToClone.push_back(definingOp);
286 } else if (auto bufferOp = llvm::dyn_cast<AIE::BufferOp>(definingOp)) {
287 referencedValuesToVisit.insert(bufferOp.getTile());
288 referencedOpsToClone.push_back(definingOp);
289 } else {
290 return errorReportOp->emitError()
291 << "Referenced SSA value defined by unsupported operation type: "
292 << definingOp->getName().getStringRef()
293 << ". Currently only aie.tile, aie.lock and aie.buffer "
294 "operations are supported.";
295 }
296 }
297
298 for (Operation *definingOp : referencedOpsToClone) {
299 if (auto tileOp = llvm::dyn_cast<AIE::TileOp>(definingOp)) {
300 int col = tileOp.getCol();
301 int row = tileOp.getRow();
302
303 // A tile is its coordinates, so a tile the caller already declares stands
304 // for the callee's tile.
305 AIE::TileOp existingTile = nullptr;
306 for (AIE::TileOp tile : callerDevice.getOps<AIE::TileOp>()) {
307 if (tile.getCol() == col && tile.getRow() == row) {
308 existingTile = tile;
309 break;
310 }
311 }
312
313 if (existingTile) {
314 // Verify that all attributes match
315 if (tileOp->getAttrDictionary() != existingTile->getAttrDictionary()) {
316 // Filter out result type attributes and symbol attributes for
317 // comparison
318 auto filterAttrs = [](DictionaryAttr dict) -> DictionaryAttr {
319 SmallVector<NamedAttribute> filteredAttrs;
320 for (auto namedAttr : dict) {
321 StringRef name = namedAttr.getName().getValue();
322 if (name != "col" && name != "row") {
323 filteredAttrs.push_back(namedAttr);
324 }
325 }
326 return DictionaryAttr::get(dict.getContext(), filteredAttrs);
327 };
328
329 DictionaryAttr tileAttrs = filterAttrs(tileOp->getAttrDictionary());
330 DictionaryAttr existingAttrs =
331 filterAttrs(existingTile->getAttrDictionary());
332
333 if (tileAttrs != existingAttrs) {
334 return errorReportOp->emitError()
335 << "aie.tile(" << col << ", " << row
336 << ") already exists in the device with different "
337 "attributes";
338 }
339 }
340 clonedDefs[definingOp] = existingTile.getOperation();
341 }
342 }
343
344 Operation *clonedOp = getOrClone(rewriter, definingOp, argMap, clonedDefs,
345 clonedSSAInsertPoint, allSymbolNames);
346 rewriter.replaceOpUsesWithIf(
347 definingOp, clonedOp->getResult(0), [&](OpOperand &operand) {
348 return operand.getOwner()->getParentOfType<AIE::DeviceOp>() ==
349 callerDevice;
350 });
351 }
352
353 return success();
354}
355
356// Inlines the definitions of all symbols referenced in the given operation
357// at the current insertion point in the given rewriter, unless the symbol
358// definition is in the "previouslyInlinedSymbolMap" map. While inlining,
359// symbols will be renamed to have a unique name.
360// Also copies in SSA values referenced by the inlined symbol definitions.
361static LogicalResult inlineReferencedSymbolDefinitions(
362 PatternRewriter &rewriter, Operation *op, Operation *lookupFrom,
363 IRMapping &argMap,
364 llvm::DenseMap<SymbolRefAttr, SymbolRefAttr> &previouslyInlinedSymbolMap,
365 AIE::DeviceOp callerDevice,
366 llvm::DenseMap<Operation *, Operation *> &clonedDefs,
367 AIE::NamedOpTableCollection &calleeNames,
368 mlir::OpBuilder::InsertPoint &clonedDefOpsInsertionPoint,
369 llvm::SetVector<SymbolRefAttr> &allSymbolNames) {
370 for (NamedAttribute namedAttr : op->getAttrs()) {
371 Attribute attr = namedAttr.getValue();
372 auto newAttr = attr.replace([&](SymbolRefAttr oldSymbolRef) {
373 auto inlined = previouslyInlinedSymbolMap.find(oldSymbolRef);
374 if (inlined != previouslyInlinedSymbolMap.end())
375 return std::make_pair(inlined->second, WalkResult::advance());
376
377 // Add the new symbol definition
378 // First try to look up from the lookupFrom operation (e.g., within the
379 // callee device). If not found, try looking up from the module level
380 // (for cross-device references).
381 // The caller device gains clones as calls are inlined; a callee device
382 // keeps its names while the caller's calls are inlined, so it is indexed
383 // once.
384 Operation *symbolDefOp = nullptr;
385 Operation *calleeTable = SymbolTable::getNearestSymbolTable(lookupFrom);
386 if (calleeTable && calleeTable != callerDevice &&
387 oldSymbolRef.getNestedReferences().empty()) {
388 symbolDefOp = calleeNames.getTable(calleeTable)
389 .lookup(oldSymbolRef.getRootReference());
390 } else {
391 symbolDefOp =
392 SymbolTable::lookupNearestSymbolFrom(lookupFrom, oldSymbolRef);
393 if (!symbolDefOp && oldSymbolRef.getNestedReferences().empty()) {
394 symbolDefOp =
395 AIE::lookupNamedOp(lookupFrom, oldSymbolRef.getRootReference());
396 }
397 }
398 if (!symbolDefOp) {
399 if (ModuleOp moduleOp = lookupFrom->getParentOfType<ModuleOp>()) {
400 symbolDefOp = SymbolTable::lookupSymbolIn(moduleOp, oldSymbolRef);
401 }
402 }
403 if (!symbolDefOp) {
404 return std::make_pair(oldSymbolRef, WalkResult::interrupt());
405 }
406
407 // If the symbol is a device, don't clone it - keep the original
408 // reference. Device ops must stay at module level.
409 if (llvm::isa<AIE::DeviceOp>(symbolDefOp)) {
410 previouslyInlinedSymbolMap[oldSymbolRef] = oldSymbolRef;
411 return std::make_pair(oldSymbolRef, WalkResult::advance());
412 }
413
414 // Collect SSA values referenced by the symbol definition operation
415 llvm::SetVector<Value> symbolReferencedValues;
416 collectReferencedSSAValues(symbolDefOp, argMap, symbolReferencedValues);
417
418 // Copy SSA values referenced by the symbol definition
419 // This updates clonedDefOpsInsertionPoint to be after the copied SSA
420 // values
421 if (failed(copyReferencedSSAValues(
422 rewriter, symbolReferencedValues, callerDevice, argMap,
423 clonedDefs, clonedDefOpsInsertionPoint, allSymbolNames, op))) {
424 return std::make_pair(oldSymbolRef, WalkResult::interrupt());
425 }
426
427 // Insert the cloned symbol at the device level, after its SSA
428 // dependencies. A definition also named by SSA shares that clone.
429 Operation *clonedSymbolDefOp =
430 getOrClone(rewriter, symbolDefOp, argMap, clonedDefs,
431 clonedDefOpsInsertionPoint, allSymbolNames);
432 SymbolRefAttr newSymbolRef =
433 FlatSymbolRefAttr::get(clonedSymbolDefOp->getAttrOfType<StringAttr>(
434 SymbolTable::getSymbolAttrName()));
435 previouslyInlinedSymbolMap[oldSymbolRef] = newSymbolRef;
436 return std::make_pair(newSymbolRef, WalkResult::advance());
437 });
438 if (!newAttr) {
439 return failure();
440 }
441 op->setAttr(namedAttr.getName(), newAttr);
442 }
443 return success();
444}
445
446struct InlineRuntimeCallsPattern : RewritePattern {
447
448 mlir::OpBuilder::InsertPoint &ssaDefInsertPoint;
449 mlir::OpBuilder::InsertPoint &symbolDefInsertPoint;
450 llvm::SetVector<SymbolRefAttr> &allSymbolNames;
451 llvm::DenseMap<Operation *, Operation *> &clonedDefs;
453
455 MLIRContext *ctx, mlir::OpBuilder::InsertPoint &ssaDefInsertPoint,
456 mlir::OpBuilder::InsertPoint &symbolDefInsertPoint,
457 llvm::SetVector<SymbolRefAttr> &allSymbolNames,
458 llvm::DenseMap<Operation *, Operation *> &clonedDefs,
460 : RewritePattern(RunOp::getOperationName(), PatternBenefit(1), ctx),
465
466 LogicalResult matchAndRewrite(Operation *op,
467 PatternRewriter &rewriter) const override {
468 llvm::DenseMap<SymbolRefAttr, SymbolRefAttr> previouslyInlinedSymbolMap;
469
470 RunOp runOp = llvm::dyn_cast<RunOp>(op);
471 if (!runOp) {
472 return failure();
473 }
474
475 AIE::DeviceOp calleeDevice = runOp.getCalleeDeviceOp();
476 AIE::RuntimeSequenceOp calleeRuntimeSequence =
477 runOp.getCalleeRuntimeSequenceOp(&calleeNames);
478 if (!calleeDevice || !calleeRuntimeSequence) {
479 return failure();
480 }
481
482 // rewrite logic
483
484 // Get caller and callee bodies. The callee body will be inlined into the
485 // caller body at the point of the RunOp.
486 Region &calleeBody = calleeRuntimeSequence.getBody();
487 AIE::DeviceOp callerDevice =
488 runOp.getOperation()->getParentOfType<AIE::DeviceOp>();
489 if (!callerDevice) {
490 runOp.emitError() << "needs to be in a DeviceOp";
491 return failure();
492 }
493
494 // The argMap maps callee arguments to caller SSA values.
495 IRMapping argMap;
496 ValueRange values = runOp.getArgs();
497 for (unsigned i = 0, n = calleeBody.getNumArguments(); i < n; i++) {
498 BlockArgument arg = calleeBody.getArgument(i);
499 Value val = values[i];
500 argMap.map(arg, val);
501 }
502
503 // The callee body may reference SSA values and symbols that are defined
504 // in the callee device (outside the callee runtime sequence). We will
505 // inline a supported set of these and error otherwise.
506
507 // Collect SSA values referenced in the callee not defined by the callee and
508 // not in the argMap.
509 llvm::SetVector<Value> referencedValues;
510 for (Operation &op : calleeBody.getOps()) {
511 collectReferencedSSAValues(&op, argMap, referencedValues);
512 }
513 llvm::SetVector<Value> filteredValues;
514 for (Value val : referencedValues) {
515 if (val.getParentRegion() != &calleeBody) {
516 filteredValues.insert(val);
517 }
518 }
519 referencedValues = std::move(filteredValues);
520
521 // Copy the operations that define these SSA values into the caller device
522 if (failed(copyReferencedSSAValues(rewriter, referencedValues, callerDevice,
524 allSymbolNames, runOp))) {
525 return failure();
526 }
527
528 // Find the calling runtime sequence so we can hoist certain ops to its
529 // start instead of placing them at the call site.
530 AIE::RuntimeSequenceOp callerRuntimeSequence =
531 runOp.getOperation()->getParentOfType<AIE::RuntimeSequenceOp>();
532 if (!callerRuntimeSequence) {
533 runOp.emitError() << "needs to be (transitively) inside a "
534 "aie.runtime_sequence operation";
535 return failure();
536 }
537 Block &callerBodyBlock = callerRuntimeSequence.getBody().front();
538 mlir::Block::iterator hoistPos = callerBodyBlock.begin();
539
540 // Now, also inline symbol definitions referenced in the callee body;
541 // this may pull in additional SSA values referenced by the symbol
542 // definitions.
543 //
544 // npu.create_scratchpad ops are hoisted to the start of the calling
545 // runtime sequence instead of being placed at the call site, so a single
546 // scratchpad is created per sequence regardless of how many callees were
547 // inlined.
548 rewriter.setInsertionPoint(runOp);
549 mlir::OpBuilder::InsertPoint clonedOpInsertionPoint =
550 rewriter.saveInsertionPoint();
551 for (Operation &op : calleeBody.getOps()) {
552 bool shouldHoist = llvm::isa<NpuCreateScratchpadOp>(&op);
553 if (shouldHoist) {
554 rewriter.setInsertionPoint(&callerBodyBlock, hoistPos);
555 rewriter.clone(op, argMap);
556 continue;
557 }
558
559 rewriter.restoreInsertionPoint(clonedOpInsertionPoint);
560 Operation *clonedOp = rewriter.clone(op, argMap);
561 clonedOpInsertionPoint = rewriter.saveInsertionPoint();
562
563 // Inline symbol references in all nested ops.
564 WalkResult symbolWalk = clonedOp->walk([&](Operation *nestedOp) {
565 if (failed(inlineReferencedSymbolDefinitions(
566 rewriter, nestedOp, calleeRuntimeSequence.getOperation(),
567 argMap, previouslyInlinedSymbolMap, callerDevice, clonedDefs,
569 return WalkResult::interrupt();
570 }
571 return WalkResult::advance();
572 });
573 if (symbolWalk.wasInterrupted()) {
574 return failure();
575 }
576 }
577
578 // The aiex.run op has been inlined; erase it.
579 rewriter.eraseOp(runOp);
580
581 return success();
582 }
583};
584
585/// Validate all aiex.run ops inside a aiex.configure op against the referenced
586/// device. This must be called sequentially (before any parallel pass
587/// execution) because it performs cross-DeviceOp symbol table lookups.
588/// Placing this validation in RunOp::verify() is unsafe: MLIR's pass manager
589/// may invoke op verifiers concurrently on sibling DeviceOps, causing a data
590/// race on the referenced device's symbol table.
591static LogicalResult
592verifyRunOpsInConfigureOp(ConfigureOp configureOp, AIE::DeviceOp referencedDev,
593 AIE::NamedOpTableCollection &referencedDevNames) {
594 if (configureOp.getBody().empty())
595 return success();
596 AIE::NamedOpTable &devNames = referencedDevNames.getTable(referencedDev);
597 for (RunOp runOp : configureOp.getBody().front().getOps<RunOp>()) {
598 auto seqName = runOp.getRuntimeSequenceSymbol();
599 Operation *maybeSeq =
600 devNames.lookup(runOp.getRuntimeSequenceSymbolAttr().getAttr());
601 if (!maybeSeq) {
602 auto err = runOp.emitError()
603 << "No such runtime sequence for device '"
604 << referencedDev.getSymName() << "': '" << seqName << "'";
605 err.attachNote(referencedDev.getLoc())
606 << "This device does not have a '" << seqName << "' runtime sequence";
607 return failure();
608 }
609 auto runtimeSeq = llvm::dyn_cast<AIE::RuntimeSequenceOp>(maybeSeq);
610 if (!runtimeSeq) {
611 return runOp.emitError()
612 << "'" << seqName << "' is not a runtime sequence";
613 }
614
615 // Validate argument count and types against the callee signature.
616 // An empty body region (no blocks) means the sequence takes no arguments.
617 Region &calleeRegion = runtimeSeq.getBody();
618 unsigned numCalleeArgs =
619 calleeRegion.empty() ? 0 : calleeRegion.getNumArguments();
620 ValueRange args = runOp.getArgs();
621 if (numCalleeArgs != args.size())
622 return runOp.emitOpError() << "argument count mismatch: expected "
623 << numCalleeArgs << " but got " << args.size();
624 for (unsigned i = 0; i < numCalleeArgs; i++) {
625 Type expected = calleeRegion.front().getArgument(i).getType();
626 Type actual = args[i].getType();
627 if (expected != actual)
628 return runOp.emitOpError()
629 << "argument " << i << " type mismatch: expected " << expected
630 << " but got " << actual;
631 }
632 }
633 return success();
634}
635
638 AIEMaterializeRuntimeSequencesPass> {
639
640 // After inlining, a runtime sequence may contain multiple
641 // npu.create_scratchpad ops (one from the sequence itself and one from each
642 // inlined callee). Keep only the first; all must agree on size because the
643 // scratchpad size is a module-level property. Returns failure if a size
644 // mismatch is detected.
645 LogicalResult
646 deduplicateCreateScratchpadOps(AIE::RuntimeSequenceOp runtimeSeqOp) {
647 if (runtimeSeqOp.getBody().empty())
648 return success();
649
650 Block &body = runtimeSeqOp.getBody().front();
651 NpuCreateScratchpadOp firstScratchpad;
652 SmallVector<NpuCreateScratchpadOp> duplicates;
653 for (NpuCreateScratchpadOp scratchpadOp :
654 body.getOps<NpuCreateScratchpadOp>()) {
655 if (!firstScratchpad) {
656 firstScratchpad = scratchpadOp;
657 continue;
658 }
659 if (firstScratchpad.getSize() != scratchpadOp.getSize()) {
660 scratchpadOp.emitError(
661 "create_scratchpad size mismatch after inlining: ")
662 << scratchpadOp.getSize() << " != " << firstScratchpad.getSize();
663 return failure();
664 }
665 duplicates.push_back(scratchpadOp);
666 }
667 for (NpuCreateScratchpadOp dup : duplicates)
668 dup.erase();
669
670 return success();
671 }
672
673 void runOnOperation() override {
674 ModuleOp moduleOp = getOperation();
675
676 AIE::NamedOpTableCollection referencedDevNames;
677
678 // Process each device in the module
679 for (AIE::DeviceOp deviceOp : moduleOp.getOps<AIE::DeviceOp>()) {
680
681 // Verify all runtime sequences before materialization
682 for (AIE::RuntimeSequenceOp runtimeSequenceOp :
683 deviceOp.getOps<AIE::RuntimeSequenceOp>()) {
684 if (failed(runtimeSequenceOp.verifyBeforeMaterialization())) {
685 return signalPassFailure();
686 }
687
688 // Validate aiex.run ops inside aiex.configure ops. This cross-device
689 // check is performed here (sequentially) rather than in RunOp::verify()
690 // to avoid a race condition: MLIR's pass manager runs verifiers on
691 // sibling DeviceOps concurrently, and looking up symbols in a sibling
692 // DeviceOp from a verifier causes a data race on its symbol table.
693 for (ConfigureOp configureOp :
694 runtimeSequenceOp.getOps<ConfigureOp>()) {
695 AIE::DeviceOp referencedDev = configureOp.getReferencedDeviceOp();
696 if (!referencedDev) {
697 // ConfigureOp::verify() already reported the error (no such
698 // device, not a device, or device type mismatch) at parse time;
699 // this is a safety net if verification was disabled.
700 return signalPassFailure();
701 }
702 if (failed(verifyRunOpsInConfigureOp(configureOp, referencedDev,
703 referencedDevNames)))
704 return signalPassFailure();
705 }
706 }
707
708 // Check for cycles in runtime sequence calls
709 for (AIE::RuntimeSequenceOp runtimeSequenceOp :
710 deviceOp.getOps<AIE::RuntimeSequenceOp>()) {
711 AnalysisManager am =
712 getAnalysisManager().nest(deviceOp).nest(runtimeSequenceOp);
714 am.getAnalysis<RuntimeCallGraphCyclicityAnalysis>();
715 if (!cyclicity.isValid) {
716 return signalPassFailure();
717 }
718 if (cyclicity.isCyclic) {
719 runtimeSequenceOp.emitError(
720 "Runtime sequence call graph contains a cycle");
721 return signalPassFailure();
722 }
723 }
724
725 // Greedily inline all runtime sequences that can be inlined;
726 // this will start with runtime sequences that do not call other runtime
727 // sequences (leaves); once their callers inline them, the callers can
728 // be inlined as well, and so on
729 mlir::Block &deviceBodyFirstBlock = deviceOp.getBodyRegion().front();
730 auto runtimeSequenceOps = deviceOp.getOps<AIE::RuntimeSequenceOp>();
731 if (runtimeSequenceOps.begin() == runtimeSequenceOps.end()) {
732 // No runtime sequences to materialize
733 continue;
734 }
735 AIE::RuntimeSequenceOp firstRuntimeSequenceOp =
736 *runtimeSequenceOps.begin();
737 mlir::OpBuilder::InsertPoint ssaDefInsertPoint(
738 &deviceBodyFirstBlock, deviceBodyFirstBlock.begin());
739 mlir::OpBuilder::InsertPoint symbolDefInsertPoint(
740 &deviceBodyFirstBlock, mlir::Block::iterator(firstRuntimeSequenceOp));
741 llvm::SetVector<SymbolRefAttr> allSymbolNames = {};
742 for (Operation &op : deviceBodyFirstBlock) {
743 if (auto symbolName = op.getAttrOfType<StringAttr>(
744 SymbolTable::getSymbolAttrName())) {
745 allSymbolNames.insert(SymbolRefAttr::get(symbolName));
746 }
747 }
748
749 MLIRContext *ctx = &getContext();
750 GreedyRewriteConfig rewriter_config = GreedyRewriteConfig();
751 rewriter_config.setRegionSimplificationLevel(
752 GreedySimplifyRegionLevel::Disabled);
753
754 RewritePatternSet patterns_0(ctx);
755 llvm::DenseMap<Operation *, Operation *> clonedDefs;
756 AIE::NamedOpTableCollection calleeNames;
757 patterns_0.insert<InlineRuntimeCallsPattern>(
758 ctx, ssaDefInsertPoint, symbolDefInsertPoint, allSymbolNames,
759 clonedDefs, calleeNames);
760 if (failed(applyPatternsGreedily(deviceOp, std::move(patterns_0),
761 rewriter_config))) {
762 return signalPassFailure();
763 }
764
765 // Deduplicate create_scratchpad ops that were hoisted during inlining.
766 for (AIE::RuntimeSequenceOp runtimeSeqOp :
767 deviceOp.getOps<AIE::RuntimeSequenceOp>()) {
768 if (failed(deduplicateCreateScratchpadOps(runtimeSeqOp)))
769 return signalPassFailure();
770 }
771
772 // Insert LoadPDI ops for each aiex.configure op
773 RewritePatternSet patterns_1(ctx);
774 patterns_1.insert<InsertLoadPdiForConfigurePattern>(ctx);
775 walkAndApplyPatterns(deviceOp, std::move(patterns_1));
776
777 // Canonicalize to remove duplicate back-to-back load_pdi ops, visiting
778 // only the load_pdi ops rather than every op of the sequences.
779 RewritePatternSet canonicalize_patterns(ctx);
780 AIEX::NpuLoadPdiOp::getCanonicalizationPatterns(canonicalize_patterns,
781 ctx);
782 SmallVector<Operation *> loadPdis;
783 deviceOp.walk([&](NpuLoadPdiOp op) { loadPdis.push_back(op); });
784 GreedyRewriteConfig loadPdiConfig = rewriter_config;
785 loadPdiConfig.setStrictness(GreedyRewriteStrictness::ExistingOps);
786 if (failed(applyOpPatternsGreedily(
787 loadPdis, std::move(canonicalize_patterns), loadPdiConfig))) {
788 return signalPassFailure();
789 }
790
791 // Flatten the IR: hoist all operations inside aiex.configure to be direct
792 // children of the runtime sequence, preserving order
793 for (AIE::RuntimeSequenceOp runtimeSequenceOp :
794 deviceOp.getOps<AIE::RuntimeSequenceOp>()) {
795 SmallVector<ConfigureOp> configureOps;
796
797 for (ConfigureOp configureOp :
798 runtimeSequenceOp.getOps<ConfigureOp>()) {
799 configureOps.push_back(configureOp);
800 }
801
802 IRRewriter rewriter(ctx);
803 for (ConfigureOp configureOp : configureOps) {
804 Block &configureBlock = configureOp.getBody().front();
805
806 // Collect all operations in the configure block
807 SmallVector<Operation *> opsToHoist;
808 for (Operation &op : configureBlock) {
809 opsToHoist.push_back(&op);
810 }
811
812 // Hoist operations to be right before the configure op
813 rewriter.setInsertionPoint(configureOp);
814 for (Operation *op : opsToHoist) {
815 op->moveBefore(configureOp);
816 }
817
818 // Erase the now-empty configure op
819 rewriter.eraseOp(configureOp);
820 }
821 }
822
823 } // end for each device
824 }
825};
826
827std::unique_ptr<OperationPass<ModuleOp>>
829 return std::make_unique<AIEMaterializeRuntimeSequencesPass>();
830}
NamedOpTable & getTable(mlir::Operation *symbolTableOp)
Definition AIEDialect.h:175
mlir::Operation * lookup(mlir::StringAttr name) const
Definition AIEDialect.h:153
std::optional< SubviewTraceResult > traceSubviewToBlockArgument(Value value)
Definition AIEUtils.cpp:18
std::unique_ptr< mlir::OperationPass< mlir::ModuleOp > > createAIEMaterializeRuntimeSequencesPass()
mlir::Operation * lookupNamedOp(mlir::Operation *from, mlir::StringAttr name)
LogicalResult deduplicateCreateScratchpadOps(AIE::RuntimeSequenceOp runtimeSeqOp)
LogicalResult matchAndRewrite(Operation *op, PatternRewriter &rewriter) const override
mlir::OpBuilder::InsertPoint & ssaDefInsertPoint
InlineRuntimeCallsPattern(MLIRContext *ctx, mlir::OpBuilder::InsertPoint &ssaDefInsertPoint, mlir::OpBuilder::InsertPoint &symbolDefInsertPoint, llvm::SetVector< SymbolRefAttr > &allSymbolNames, llvm::DenseMap< Operation *, Operation * > &clonedDefs, AIE::NamedOpTableCollection &calleeNames)
llvm::DenseMap< Operation *, Operation * > & clonedDefs
mlir::OpBuilder::InsertPoint & symbolDefInsertPoint
llvm::SetVector< SymbolRefAttr > & allSymbolNames
AIE::NamedOpTableCollection & calleeNames
InsertLoadPdiForConfigurePattern(MLIRContext *context, PatternBenefit benefit=1)
LogicalResult matchAndRewrite(Operation *op, PatternRewriter &rewriter) const override
RuntimeCallGraphCyclicityAnalysis(Operation *op, AnalysisManager &am)