Skip to content

Commit 9c1e32d

Browse files
hunhoffeclaude
andauthored
func level link_with (#2922)
Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent b4934f0 commit 9c1e32d

169 files changed

Lines changed: 3764 additions & 1684 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

include/aie/Dialect/AIE/IR/AIEOps.td

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,12 @@ def AIE_CoreOp: AIE_Op<"core", [
398398
let arguments = (
399399
ins Index:$tile,
400400
DefaultValuedAttr<AIEI32Attr, "0x400">:$stack_size,
401+
// Deprecated: attach link_with to func.func declarations instead and run
402+
// aie-assign-core-link-files to populate link_files.
401403
OptionalAttr<StrAttr>:$link_with,
404+
// Populated by aie-assign-core-link-files; consumed by BCF/ldscript emitters
405+
// and the aiecc driver. Specifying both link_with and link_files is an error.
406+
OptionalAttr<StrArrayAttr>:$link_files,
402407
OptionalAttr<StrAttr>:$elf_file,
403408
OptionalAttr<BoolAttr>:$dynamic_objfifo_lowering
404409
);
@@ -423,6 +428,18 @@ def AIE_CoreOp: AIE_Op<"core", [
423428
This op has an optional `dynamic_objfifo_lowering` attribute, to finely control whether the
424429
objectfifos in this core should be lowered using the dynamic runtime lowering.
425430

431+
**External object files.** The preferred mechanism is to attach a `link_with`
432+
string attribute to each `func.func` declaration for an externally-defined
433+
function, then run the `aie-assign-core-link-files` pass. That pass traces
434+
direct `func.call` edges from each core and writes the aggregated, de-duplicated
435+
list of object file paths into the `link_files` attribute on this op. The
436+
BCF/ldscript emitters and the aiecc driver consume `link_files`.
437+
438+
The core-level `link_with` attribute is deprecated and kept only for
439+
backward compatibility. It is migrated by `aie-assign-core-link-files`
440+
(its value is folded into `link_files` and then removed). Specifying both
441+
`link_with` and `link_files` on the same CoreOp is a verifier error.
442+
426443
Examples:
427444
```
428445
%tile = aie.tile(1, 1)

include/aie/Dialect/AIE/Transforms/AIEPasses.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ createAIEAssignBufferAddressesPass();
2929
std::unique_ptr<mlir::OperationPass<DeviceOp>>
3030
createAIEAssignBufferAddressesPass(
3131
const AIEAssignBufferAddressesOptions &options);
32+
std::unique_ptr<mlir::OperationPass<DeviceOp>>
33+
createAIEAssignCoreLinkFilesPass();
3234
std::unique_ptr<mlir::OperationPass<DeviceOp>> createAIEAssignLockIDsPass();
3335
std::unique_ptr<mlir::OperationPass<mlir::ModuleOp>>
3436
createAIECanonicalizeDevicePass();

include/aie/Dialect/AIE/Transforms/AIEPasses.td

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,35 @@
1313

1414
include "mlir/Pass/PassBase.td"
1515

16+
def AIEAssignCoreLinkFiles : Pass<"aie-assign-core-link-files", "DeviceOp"> {
17+
let summary =
18+
"Infer per-core link_files from func-level link_with attributes";
19+
let description = [{
20+
Walks each aie.core and collects the set of external object files it needs
21+
by tracing direct func.call edges to func.func declarations that carry a
22+
"link_with" string attribute. The result is stored in the CoreOp's
23+
"link_files" StrArrayAttr.
24+
25+
Only direct calls (func.call) are resolved. Indirect calls
26+
(func.call_indirect) inside a core body emit a warning and are not
27+
resolved; add a direct func.call to the required func.func declaration
28+
so the pass can trace the dependency.
29+
30+
Core-level "link_with" (deprecated) is also migrated: its value is
31+
folded into the set and the attribute is removed from the CoreOp.
32+
33+
func.func declarations that carry "link_with" but are never called from
34+
any core emit a warning; their object files will not appear in any
35+
core's link_files.
36+
}];
37+
38+
let constructor = "xilinx::AIE::createAIEAssignCoreLinkFilesPass()";
39+
let dependentDialects = [
40+
"mlir::func::FuncDialect",
41+
"xilinx::AIE::AIEDialect",
42+
];
43+
}
44+
1645
def AIEAssignBufferAddresses : Pass<"aie-assign-buffer-addresses", "DeviceOp"> {
1746
let summary = "Assign memory locations for buffers in each tile";
1847
let description = [{

lib/Dialect/AIE/IR/AIEDialect.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,6 +1741,10 @@ LogicalResult CoreOp::verify() {
17411741
"(consist of exactly one `aie.end` op).");
17421742
}
17431743
}
1744+
if (getLinkWith() && getLinkFiles())
1745+
return emitOpError(
1746+
"cannot specify both 'link_with' (deprecated) and 'link_files' "
1747+
"on the same core; run aie-assign-core-link-files to migrate");
17441748
return success();
17451749
}
17461750

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//===- AIEAssignCoreLinkFiles.cpp -------------------------------*- C++ -*-===//
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+
// (c) Copyright 2026 Advanced Micro Devices Inc.
8+
//
9+
//===----------------------------------------------------------------------===//
10+
//
11+
// This pass infers the per-core set of external object files required for
12+
// linking by tracing call edges from each core to func.func declarations that
13+
// carry a "link_with" attribute.
14+
//
15+
// After the pass runs, every CoreOp that needs external files will have a
16+
// "link_files" StrArrayAttr containing the (de-duplicated) list of .o paths.
17+
//
18+
// Core-level "link_with" (deprecated) is also migrated: its value is added to
19+
// the set and the attribute is removed from the CoreOp.
20+
//
21+
//===----------------------------------------------------------------------===//
22+
23+
#include "aie/Dialect/AIE/IR/AIEDialect.h"
24+
#define GEN_PASS_DEF_AIEASSIGNCORELINKFILES
25+
#include "aie/Dialect/AIE/Transforms/AIEPasses.h"
26+
27+
#include "mlir/Dialect/Func/IR/FuncOps.h"
28+
#include "mlir/IR/Builders.h"
29+
#include "mlir/Pass/Pass.h"
30+
31+
#include "llvm/ADT/DenseSet.h"
32+
#include "llvm/ADT/SetVector.h"
33+
34+
#define DEBUG_TYPE "aie-assign-core-link-files"
35+
36+
using namespace mlir;
37+
using namespace xilinx;
38+
using namespace xilinx::AIE;
39+
40+
struct AIEAssignCoreLinkFilesPass
41+
: xilinx::AIE::impl::AIEAssignCoreLinkFilesBase<
42+
AIEAssignCoreLinkFilesPass> {
43+
void runOnOperation() override {
44+
DeviceOp device = getOperation();
45+
// Builder is used only for attribute construction; no ops are inserted.
46+
Builder builder(device.getContext());
47+
48+
// Build a map from func name to the object file(s) it requires, sourced
49+
// from the "link_with" string attribute on func.func declarations.
50+
// StringRefs are views into MLIRContext-owned storage and remain valid
51+
// for the entire pass run.
52+
DenseMap<StringRef, SmallVector<StringRef, 2>> funcToObjs;
53+
for (auto funcOp : device.getOps<mlir::func::FuncOp>()) {
54+
if (auto attr = funcOp->getAttrOfType<mlir::StringAttr>("link_with")) {
55+
funcToObjs[funcOp.getName()].push_back(attr.getValue());
56+
}
57+
}
58+
59+
// Tracks which func.func symbols are directly called from at least one
60+
// core; used to warn about link_with-bearing functions that are never
61+
// called and whose object files would otherwise be silently omitted.
62+
llvm::DenseSet<StringRef> usedFuncs;
63+
64+
// Only direct func.call edges are traced. func.call_indirect ops and
65+
// calls through intermediate wrapper functions are not followed. To
66+
// handle transitive dependencies, attach link_with directly to every
67+
// func.func declaration that a core calls, even thin wrappers.
68+
// TODO: extend to transitive call resolution.
69+
device.walk([&](CoreOp core) {
70+
// De-duplicate while preserving insertion order.
71+
llvm::SetVector<StringRef> needed;
72+
73+
// Migrate deprecated core-level attr: warn, consume it, and add to set.
74+
if (auto lw = core.getLinkWith()) {
75+
core.emitWarning(
76+
"link_with on aie.core is deprecated; attach link_with to "
77+
"the func.func declaration instead");
78+
needed.insert(lw.value());
79+
core->removeAttr("link_with");
80+
}
81+
82+
// Single walk over the core body: collect required object files and
83+
// record called symbols (for the unused-func warning below).
84+
core.walk([&](Operation *op) {
85+
if (auto call = dyn_cast<mlir::func::CallOp>(op)) {
86+
usedFuncs.insert(call.getCallee());
87+
auto it = funcToObjs.find(call.getCallee());
88+
if (it != funcToObjs.end())
89+
for (StringRef obj : it->second)
90+
needed.insert(obj);
91+
} else if (auto indCall = dyn_cast<mlir::func::CallIndirectOp>(op)) {
92+
indCall.emitWarning(
93+
"indirect call in core body — link_with attributes on "
94+
"indirectly-called functions are not automatically resolved; "
95+
"add a direct func.call to the required func.func declaration "
96+
"so that aie-assign-core-link-files can trace the dependency");
97+
}
98+
});
99+
100+
if (!needed.empty()) {
101+
// builder is used only for attribute construction; its insertion
102+
// point is irrelevant and no ops are inserted.
103+
core.setLinkFilesAttr(builder.getStrArrayAttr(needed.getArrayRef()));
104+
}
105+
});
106+
107+
// Warn about funcs with link_with that are never called from any core.
108+
for (auto &[funcName, objs] : funcToObjs) {
109+
if (!usedFuncs.count(funcName)) {
110+
if (auto funcOp = device.lookupSymbol<mlir::func::FuncOp>(funcName))
111+
funcOp.emitWarning()
112+
<< "func '" << funcName
113+
<< "' has link_with but is never called from any core; "
114+
"its .o file will not be linked";
115+
}
116+
}
117+
}
118+
};
119+
120+
std::unique_ptr<OperationPass<DeviceOp>>
121+
AIE::createAIEAssignCoreLinkFilesPass() {
122+
return std::make_unique<AIEAssignCoreLinkFilesPass>();
123+
}

lib/Dialect/AIE/Transforms/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77

88
add_mlir_dialect_library(
99
AIETransforms
10-
AIEAssignBuffers.cpp
1110
AIEAssignBufferDescriptorIDs.cpp
11+
AIEAssignBuffers.cpp
12+
AIEAssignCoreLinkFiles.cpp
1213
AIEAssignLockIDs.cpp
1314
AIEFindFlows.cpp
1415
AIEPathFinder.cpp

lib/Targets/AIETargetBCF.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,18 @@ LogicalResult AIETranslateToBCF(ModuleOp module, raw_ostream &output,
139139
<< utohexstr(addressSpaceSize - dataMemoryEnd)
140140
<< " // And everything else the core can't see\n";
141141

142-
if (tile.getCoreOp() && tile.getCoreOp().getLinkWith())
143-
output << "_include _file "
144-
<< tile.getCoreOp().getLinkWith().value().str() << "\n";
142+
if (auto coreOp = tile.getCoreOp()) {
143+
if (auto filesAttr = coreOp.getLinkFiles()) {
144+
// Canonical path: link_files populated by aie-assign-core-link-files.
145+
for (auto f : filesAttr->getAsRange<mlir::StringAttr>())
146+
output << "_include _file " << f.getValue() << "\n";
147+
} else if (coreOp.getLinkWith()) {
148+
// Deprecated fallback: core-level link_with was not migrated by
149+
// aie-assign-core-link-files (e.g., the pass was not run).
150+
output << "_include _file " << coreOp.getLinkWith().value().str()
151+
<< "\n";
152+
}
153+
}
145154
output << "_resolve _main core_" << tile.getCol() << "_" << tile.getRow()
146155
<< "\n";
147156
}

lib/Targets/AIETargetLdScript.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,19 @@ SECTIONS
175175
targetModel.getMemEastBaseAddress(), std::string("east"));
176176

177177
output << " .bss : { *(.bss*) } > data\n";
178+
// INPUT() directives must follow the closing brace of SECTIONS; placing
179+
// them inside SECTIONS is invalid linker script syntax.
178180
output << "}\n";
179181
if (auto coreOp = tile.getCoreOp()) {
180-
if (auto fileAttr = coreOp.getLinkWith())
182+
if (auto filesAttr = coreOp.getLinkFiles()) {
183+
// Canonical path: link_files populated by aie-assign-core-link-files.
184+
for (auto f : filesAttr->getAsRange<mlir::StringAttr>())
185+
output << "INPUT(" << f.getValue() << ")\n";
186+
} else if (auto fileAttr = coreOp.getLinkWith()) {
187+
// Deprecated fallback: core-level link_with was not migrated by
188+
// aie-assign-core-link-files (e.g., the pass was not run).
181189
output << "INPUT(" << fileAttr.value().str() << ")\n";
190+
}
182191

183192
output << "PROVIDE(main = core_" << tile.getCol() << "_"
184193
<< tile.getRow() << ");\n";

mlir_exercises/tutorial-8/aie.mlir

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ module @tutorial_8 {
2929

3030
// declare 2 kernel functions name "extern_kernel1" and "extern_kernel2"
3131
// with one positional function argument, in this case mapped to a memref
32-
func.func private @extern_kernel1() -> ()
33-
func.func private @extern_kernel2(%b: memref<256xi32>) -> ()
32+
func.func private @extern_kernel1() -> () attributes {link_with = "kernel1.o"}
33+
func.func private @extern_kernel2(%b: memref<256xi32>) -> () attributes {link_with = "kernel2.o"}
3434

3535
// Declare shared lock (belonging to tile(2,4), lock ID=1)
3636
// %lock13_1 = aie.lock(%tile13, 1) { sym_name = "lock_13_1" }
@@ -49,7 +49,7 @@ module @tutorial_8 {
4949

5050
// aie.use_lock(%lock13_1, "Release", 1)
5151
aie.end
52-
} { link_with="kernel1.o" }
52+
}
5353

5454
// Define core algorithm for tile(2,4) which reads value set by tile(1,4)
5555
// buf[5] = buf[3] + 100
@@ -74,6 +74,6 @@ module @tutorial_8 {
7474
// This release means our 2nd core is done
7575
aie.use_lock(%lock13_2, "Release", 1)
7676
aie.end
77-
} { link_with="kernel2.o" }
77+
}
7878

7979
}

mlir_exercises/tutorial-8/answers/aie.mlir

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ module @tutorial_8 {
3030

3131
// declare 2 kernel functions name "extern_kernel1" and "extern_kernel2"
3232
// with one positional function argument, in this case mapped to a memref
33-
func.func private @extern_kernel1() -> ()
34-
func.func private @extern_kernel2(%b: memref<256xi32>) -> ()
33+
func.func private @extern_kernel1() -> () attributes {link_with = "kernel1.o"}
34+
func.func private @extern_kernel2(%b: memref<256xi32>) -> () attributes {link_with = "kernel2.o"}
3535

3636
// Declare shared lock (belonging to tile(2,4), lock ID=1), do not change symbolic name to allow reuse of test.cpp
3737

@@ -52,7 +52,7 @@ module @tutorial_8 {
5252

5353
// aie.use_lock(%lock23_1, "Release", 1)
5454
aie.end
55-
} { link_with="kernel2.o" }
55+
}
5656

5757
// Define core algorithm for tile(2,4) which reads value set by tile(1,4)
5858
// buf[5] = buf[3] + 100
@@ -73,6 +73,6 @@ module @tutorial_8 {
7373

7474
// aie.use_lock(%lock24_1, "Release", 0)
7575
aie.end
76-
} { link_with="kernel1.o" }
76+
}
7777

7878
}

0 commit comments

Comments
 (0)