Skip to content

Commit ecfc3c6

Browse files
Fixup device symbol ref with runtime sequence materialization (#2849)
Co-authored-by: Joseph Melber <jgmelber@gmail.com>
1 parent 31551e6 commit ecfc3c6

4 files changed

Lines changed: 174 additions & 0 deletions

File tree

include/aie/Dialect/AIEX/IR/AIEX.td

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,7 @@ def AIE_NpuLoadPdiOp: AIEX_Op<"npu.load_pdi", []> {
10141014

10151015
If a symbol reference is provided, the compiler driver (aiecc.py) will match it to a device symbol name and assign the PDI ID field based on it.
10161016
}];
1017+
let hasCanonicalizeMethod = 1;
10171018
}
10181019

10191020
def AIE_DMAConfigureTaskOp : AIEX_Op<"dma_configure_task", [HasParent<"AIE::RuntimeSequenceOp">, TileElement]>, Results<(outs Index:$result)> {

lib/Dialect/AIEX/IR/AIEXDialect.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,3 +1010,32 @@ LogicalResult AIEX::RunOp::verify() {
10101010

10111011
return success();
10121012
}
1013+
1014+
//===----------------------------------------------------------------------===//
1015+
// NpuLoadPdiOp
1016+
//===----------------------------------------------------------------------===//
1017+
1018+
LogicalResult AIEX::NpuLoadPdiOp::canonicalize(AIEX::NpuLoadPdiOp op,
1019+
PatternRewriter &rewriter) {
1020+
// Check for back-to-back identical load_pdi ops and remove duplicates
1021+
Operation *nextOp = op->getNextNode();
1022+
if (!nextOp)
1023+
return failure();
1024+
1025+
// Check if next op is also a NpuLoadPdiOp
1026+
auto nextLoadPdi = dyn_cast<AIEX::NpuLoadPdiOp>(nextOp);
1027+
if (!nextLoadPdi)
1028+
return failure();
1029+
1030+
// Check if they are identical (all attributes match)
1031+
if (op.getDeviceRefAttr() == nextLoadPdi.getDeviceRefAttr() &&
1032+
op.getId() == nextLoadPdi.getId() &&
1033+
op.getSize() == nextLoadPdi.getSize() &&
1034+
op.getAddress() == nextLoadPdi.getAddress()) {
1035+
// Erase the first one, keeping the second
1036+
rewriter.eraseOp(op);
1037+
return success();
1038+
}
1039+
1040+
return failure();
1041+
}

lib/Dialect/AIEX/Transforms/AIEMaterializeRuntimeSequences.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ struct InsertLoadPdiForConfigurePattern : RewritePattern {
129129
} else {
130130
configureBlock = &configureOp.getBody().front();
131131
}
132+
132133
rewriter.setInsertionPointToStart(configureBlock);
133134
AIEX::NpuLoadPdiOp::create(
134135
rewriter, configureOp.getLoc(),
@@ -344,12 +345,26 @@ static LogicalResult inlineReferencedSymbolDefinitions(
344345
previouslyInlinedSymbolMap[oldSymbolRef] = newSymbolRef;
345346

346347
// Add the new symbol definition
348+
// First try to look up from the lookupFrom operation (e.g., within the
349+
// callee device). If not found, try looking up from the module level
350+
// (for cross-device references).
347351
Operation *symbolDefOp =
348352
SymbolTable::lookupNearestSymbolFrom(lookupFrom, oldSymbolRef);
353+
if (!symbolDefOp) {
354+
if (ModuleOp moduleOp = lookupFrom->getParentOfType<ModuleOp>()) {
355+
symbolDefOp = SymbolTable::lookupSymbolIn(moduleOp, oldSymbolRef);
356+
}
357+
}
349358
if (!symbolDefOp) {
350359
return std::make_pair(newSymbolRef, WalkResult::interrupt());
351360
}
352361

362+
// If the symbol is a device, don't clone it - keep the original
363+
// reference. Device ops must stay at module level.
364+
if (llvm::isa<AIE::DeviceOp>(symbolDefOp)) {
365+
return std::make_pair(oldSymbolRef, WalkResult::advance());
366+
}
367+
353368
// Collect SSA values referenced by the symbol definition operation
354369
llvm::SetVector<Value> symbolReferencedValues;
355370
collectReferencedSSAValues(symbolDefOp, argMap, symbolReferencedValues);
@@ -561,6 +576,15 @@ struct AIEMaterializeRuntimeSequencesPass
561576
patterns_1.insert<InsertLoadPdiForConfigurePattern>(ctx);
562577
walkAndApplyPatterns(deviceOp, std::move(patterns_1));
563578

579+
// Canonicalize to remove duplicate back-to-back load_pdi ops
580+
RewritePatternSet canonicalize_patterns(ctx);
581+
AIEX::NpuLoadPdiOp::getCanonicalizationPatterns(canonicalize_patterns,
582+
ctx);
583+
if (failed(applyPatternsGreedily(
584+
deviceOp, std::move(canonicalize_patterns), rewriter_config))) {
585+
return signalPassFailure();
586+
}
587+
564588
// Flatten the IR: hoist all operations inside aiex.configure to be direct
565589
// children of the runtime sequence, preserving order
566590
for (AIE::RuntimeSequenceOp runtimeSequenceOp :
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//===- cross_device_inline_load_pdi.mlir -----------------------*- MLIR -*-===//
2+
//
3+
// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
// Copyright (C) 2026, Advanced Micro Devices, Inc.
8+
//
9+
//===----------------------------------------------------------------------===//
10+
11+
// RUN: aie-opt --aie-materialize-runtime-sequences --split-input-file %s | FileCheck %s
12+
13+
// Test cross-device inlining of runtime sequences:
14+
// 1. Inlined load_pdi operations are preserved with original device_ref
15+
// 2. InsertLoadPdiForConfigurePattern adds load_pdi at start only if needed
16+
// 3. When callee starts with load_pdi, no duplicate is added
17+
18+
//===----------------------------------------------------------------------===//
19+
// TEST 1: Callee does NOT start with load_pdi - one should be added at start
20+
//===----------------------------------------------------------------------===//
21+
22+
module {
23+
// The outer/caller device - anonymous (no symbol name)
24+
// CHECK: aie.device(npu2) {
25+
aie.device(npu2) {
26+
%tile00 = aie.tile(0, 0)
27+
28+
// CHECK: aie.runtime_sequence @caller_seq
29+
aie.runtime_sequence @caller_seq(%arg0: memref<16xi32>) {
30+
// After inlining, we should have:
31+
// 1. A load_pdi added by InsertLoadPdiForConfigurePattern (since the first inlined op is write32, not load_pdi)
32+
// 2. All operations from the callee sequence inlined
33+
// 3. The inlined load_pdi operations preserved with their original device_ref
34+
35+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device}
36+
// CHECK-NEXT: aiex.npu.write32 {address = 100 : ui32
37+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device}
38+
// CHECK-NEXT: aiex.npu.write32 {address = 200 : ui32
39+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device}
40+
// CHECK-NEXT: aiex.npu.write32 {address = 300 : ui32
41+
aiex.configure @callee_device {
42+
aiex.run @callee_seq(%arg0) : (memref<16xi32>)
43+
}
44+
}
45+
}
46+
47+
// The callee device that contains the runtime sequence to be inlined
48+
// CHECK: aie.device(npu2) @callee_device
49+
aie.device(npu2) @callee_device {
50+
%tile10 = aie.tile(1, 0)
51+
52+
// This sequence has load_pdi operations embedded in it for reconfiguration
53+
// between iterations. When inlined, these should be preserved.
54+
aie.runtime_sequence @callee_seq(%arg0: memref<16xi32>) {
55+
// First iteration
56+
aiex.npu.write32 {address = 100 : ui32, column = 0 : i32, row = 0 : i32, value = 1 : ui32}
57+
58+
// Reconfigure for second iteration
59+
aiex.npu.load_pdi {device_ref = @callee_device}
60+
aiex.npu.write32 {address = 200 : ui32, column = 0 : i32, row = 0 : i32, value = 2 : ui32}
61+
62+
// Reconfigure for third iteration
63+
aiex.npu.load_pdi {device_ref = @callee_device}
64+
aiex.npu.write32 {address = 300 : ui32, column = 0 : i32, row = 0 : i32, value = 3 : ui32}
65+
}
66+
}
67+
}
68+
69+
// -----
70+
71+
//===----------------------------------------------------------------------===//
72+
// TEST 2: Callee STARTS with load_pdi - no duplicate should be added
73+
//===----------------------------------------------------------------------===//
74+
75+
module {
76+
// CHECK: aie.device(npu2) {
77+
aie.device(npu2) {
78+
%tile00 = aie.tile(0, 0)
79+
80+
// CHECK: aie.runtime_sequence @caller_seq2
81+
aie.runtime_sequence @caller_seq2(%arg0: memref<16xi32>) {
82+
// After inlining, the callee's load_pdi is at the start of the configure block.
83+
// InsertLoadPdiForConfigurePattern should detect this and NOT add another one.
84+
// We should see exactly 3 load_pdi operations (from the callee), not 4.
85+
86+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device2}
87+
// CHECK-NEXT: aiex.npu.write32 {address = 100 : ui32
88+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device2}
89+
// CHECK-NEXT: aiex.npu.write32 {address = 200 : ui32
90+
// CHECK: aiex.npu.load_pdi {device_ref = @callee_device2}
91+
// CHECK-NEXT: aiex.npu.write32 {address = 300 : ui32
92+
aiex.configure @callee_device2 {
93+
aiex.run @callee_seq2(%arg0) : (memref<16xi32>)
94+
}
95+
}
96+
}
97+
98+
// The callee device - its runtime sequence starts with load_pdi
99+
// CHECK: aie.device(npu2) @callee_device2
100+
aie.device(npu2) @callee_device2 {
101+
%tile10 = aie.tile(1, 0)
102+
103+
// This sequence STARTS with a load_pdi operation.
104+
// When inlined into a configure block, InsertLoadPdiForConfigurePattern
105+
// should NOT add another load_pdi at the start.
106+
aie.runtime_sequence @callee_seq2(%arg0: memref<16xi32>) {
107+
// First load_pdi at the very start
108+
aiex.npu.load_pdi {device_ref = @callee_device2}
109+
aiex.npu.write32 {address = 100 : ui32, column = 0 : i32, row = 0 : i32, value = 1 : ui32}
110+
111+
// Second iteration
112+
aiex.npu.load_pdi {device_ref = @callee_device2}
113+
aiex.npu.write32 {address = 200 : ui32, column = 0 : i32, row = 0 : i32, value = 2 : ui32}
114+
115+
// Third iteration
116+
aiex.npu.load_pdi {device_ref = @callee_device2}
117+
aiex.npu.write32 {address = 300 : ui32, column = 0 : i32, row = 0 : i32, value = 3 : ui32}
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)