Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
190ec66
[AIE] Add func-level link_with and AIEAssignCoreLinkFiles pass
hunhoffe Mar 5, 2026
46ac7c8
[aiecc] Wire AIEAssignCoreLinkFiles into driver; add atomicCopyFile
hunhoffe Mar 5, 2026
26aafad
[test] Add aiecc and npu-xrt tests for func-level link_with
hunhoffe Mar 5, 2026
496a1be
[aiecc] Fix unistd.h include causing link symbol collision
hunhoffe Mar 5, 2026
7326e89
[AIE] Polish AIEAssignCoreLinkFiles: declaration order, redundant ove…
hunhoffe Mar 5, 2026
2974552
[python] Post-migration audit: remove dead branches and redundant code
hunhoffe Mar 5, 2026
cb35c97
[aiecc] Fix Peano linker unable to find external .o in JIT flow
hunhoffe Mar 5, 2026
83f756a
[python] Remove merge_object_files; add link_with to external_func
hunhoffe Mar 5, 2026
a4e7c11
[aiecc] Replace Python aiecc with thin wrapper from PR #2925
hunhoffe Mar 6, 2026
4141cea
[aiecc/jit] Fix JIT external function path resolution in C++ aiecc path
hunhoffe Mar 6, 2026
2d3931f
[format] Apply black formatting to compile/utils.py
hunhoffe Mar 6, 2026
4063532
[quality] Production-level audit fixes across aiecc.cpp, jit.py, util…
hunhoffe Mar 6, 2026
3a09dbc
[fix] Fix two bugs exposed by running tests without warm cache
hunhoffe Mar 6, 2026
9f6a19d
[fix] Fix add_one_scale_func_link_with_chess output buffer mismatch
hunhoffe Mar 6, 2026
1a151fc
Merge branch 'main' into func-level-link-with
hunhoffe Mar 6, 2026
c9f4feb
[fix] Restore is_placed branch in jit.py for placed designs
hunhoffe Mar 6, 2026
19066a1
[fix] Fix Windows build: use file_t-free createUniqueFile in atomicCo…
hunhoffe Mar 6, 2026
0ffd6bc
Merge branch 'main' into func-level-link-with
hunhoffe Mar 9, 2026
df9a92f
try to update some tests
hunhoffe Mar 10, 2026
e64e254
Merge branch 'main' into func-level-link-with
hunhoffe Mar 10, 2026
55c03f8
[audit] Code quality pass: comments, docs, and minor cleanup
hunhoffe Mar 10, 2026
396b802
[audit] Fix correctness issues found in code review
hunhoffe Mar 10, 2026
4d6f0ad
[docs] Document link_with parameter in quick_reference.md
hunhoffe Mar 10, 2026
ef61e9a
[test] Fill three gaps in func-level link_with test coverage
hunhoffe Mar 10, 2026
b4711ed
[python] Rationalize external kernel Python API
hunhoffe Mar 11, 2026
038084e
[fix] Update Python tests broken by Core(link_with=...) removal
hunhoffe Mar 11, 2026
a61dee4
[fix] Migrate remaining tests from deprecated Core(link_with=...) API
hunhoffe Mar 11, 2026
d17eb69
Merge branch 'main' into func-level-link-with
hunhoffe Mar 11, 2026
c9b081b
[fix] Fix self-loop ObjectFifo in test_jit_two_extern_functions
hunhoffe Mar 11, 2026
c51d461
Merge remote-tracking branch 'origin/main' into func-level-link-with
hunhoffe Mar 11, 2026
0c453bd
Merge branch 'main' into func-level-link-with
hunhoffe Mar 11, 2026
64c8fb6
Update lit_config_helpers.py
hunhoffe Mar 11, 2026
f98478e
[examples] Replace artificial archives with individual .o files in vi…
hunhoffe Mar 11, 2026
ddddb0d
[audit] Tighten kernel.py and compile_mlir_module comments
hunhoffe Mar 11, 2026
aa18a70
Merge branch 'main' into func-level-link-with
hunhoffe Mar 11, 2026
9848373
Merge branch 'main' into func-level-link-with
hunhoffe Mar 12, 2026
1bbcefa
Merge branch 'main' into func-level-link-with
hunhoffe Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions include/aie/Dialect/AIE/IR/AIEOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,12 @@ def AIE_CoreOp: AIE_Op<"core", [
let arguments = (
ins Index:$tile,
DefaultValuedAttr<AIEI32Attr, "0x400">:$stack_size,
// Deprecated: attach link_with to func.func declarations instead and run
// aie-assign-core-link-files to populate link_files.
OptionalAttr<StrAttr>:$link_with,
// Populated by aie-assign-core-link-files; consumed by BCF/ldscript emitters
// and the aiecc driver. Specifying both link_with and link_files is an error.
OptionalAttr<StrArrayAttr>:$link_files,
OptionalAttr<StrAttr>:$elf_file,
OptionalAttr<BoolAttr>:$dynamic_objfifo_lowering
);
Expand All @@ -423,6 +428,18 @@ def AIE_CoreOp: AIE_Op<"core", [
This op has an optional `dynamic_objfifo_lowering` attribute, to finely control whether the
objectfifos in this core should be lowered using the dynamic runtime lowering.

**External object files.** The preferred mechanism is to attach a `link_with`
string attribute to each `func.func` declaration for an externally-defined
function, then run the `aie-assign-core-link-files` pass. That pass traces
direct `func.call` edges from each core and writes the aggregated, de-duplicated
list of object file paths into the `link_files` attribute on this op. The
BCF/ldscript emitters and the aiecc driver consume `link_files`.

The core-level `link_with` attribute is deprecated and kept only for
backward compatibility. It is migrated by `aie-assign-core-link-files`
(its value is folded into `link_files` and then removed). Specifying both
`link_with` and `link_files` on the same CoreOp is a verifier error.

Examples:
```
%tile = aie.tile(1, 1)
Expand Down
2 changes: 2 additions & 0 deletions include/aie/Dialect/AIE/Transforms/AIEPasses.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ createAIEAssignBufferAddressesPass();
std::unique_ptr<mlir::OperationPass<DeviceOp>>
createAIEAssignBufferAddressesPass(
const AIEAssignBufferAddressesOptions &options);
std::unique_ptr<mlir::OperationPass<DeviceOp>>
createAIEAssignCoreLinkFilesPass();
std::unique_ptr<mlir::OperationPass<DeviceOp>> createAIEAssignLockIDsPass();
std::unique_ptr<mlir::OperationPass<mlir::ModuleOp>>
createAIECanonicalizeDevicePass();
Expand Down
29 changes: 29 additions & 0 deletions include/aie/Dialect/AIE/Transforms/AIEPasses.td
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,35 @@

include "mlir/Pass/PassBase.td"

def AIEAssignCoreLinkFiles : Pass<"aie-assign-core-link-files", "DeviceOp"> {
let summary =
"Infer per-core link_files from func-level link_with attributes";
let description = [{
Walks each aie.core and collects the set of external object files it needs
by tracing direct func.call edges to func.func declarations that carry a
"link_with" string attribute. The result is stored in the CoreOp's
"link_files" StrArrayAttr.

Only direct calls (func.call) are resolved. Indirect calls
(func.call_indirect) inside a core body emit a warning and are not
resolved; add a direct func.call to the required func.func declaration
so the pass can trace the dependency.

Core-level "link_with" (deprecated) is also migrated: its value is
folded into the set and the attribute is removed from the CoreOp.

func.func declarations that carry "link_with" but are never called from
any core emit a warning; their object files will not appear in any
core's link_files.
}];

let constructor = "xilinx::AIE::createAIEAssignCoreLinkFilesPass()";
let dependentDialects = [
"mlir::func::FuncDialect",
"xilinx::AIE::AIEDialect",
];
}

def AIEAssignBufferAddresses : Pass<"aie-assign-buffer-addresses", "DeviceOp"> {
let summary = "Assign memory locations for buffers in each tile";
let description = [{
Expand Down
4 changes: 4 additions & 0 deletions lib/Dialect/AIE/IR/AIEDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1741,6 +1741,10 @@ LogicalResult CoreOp::verify() {
"(consist of exactly one `aie.end` op).");
}
}
if (getLinkWith() && getLinkFiles())
return emitOpError(
"cannot specify both 'link_with' (deprecated) and 'link_files' "
"on the same core; run aie-assign-core-link-files to migrate");
return success();
}

Expand Down
123 changes: 123 additions & 0 deletions lib/Dialect/AIE/Transforms/AIEAssignCoreLinkFiles.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//===- AIEAssignCoreLinkFiles.cpp -------------------------------*- C++ -*-===//
//
// This file is licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// (c) Copyright 2026 Advanced Micro Devices Inc.
//
//===----------------------------------------------------------------------===//
//
// This pass infers the per-core set of external object files required for
// linking by tracing call edges from each core to func.func declarations that
// carry a "link_with" attribute.
//
// After the pass runs, every CoreOp that needs external files will have a
// "link_files" StrArrayAttr containing the (de-duplicated) list of .o paths.
//
// Core-level "link_with" (deprecated) is also migrated: its value is added to
// the set and the attribute is removed from the CoreOp.
//
//===----------------------------------------------------------------------===//

#include "aie/Dialect/AIE/IR/AIEDialect.h"
#define GEN_PASS_DEF_AIEASSIGNCORELINKFILES
#include "aie/Dialect/AIE/Transforms/AIEPasses.h"

#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/IR/Builders.h"
#include "mlir/Pass/Pass.h"

#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/SetVector.h"

#define DEBUG_TYPE "aie-assign-core-link-files"

using namespace mlir;
using namespace xilinx;
using namespace xilinx::AIE;

struct AIEAssignCoreLinkFilesPass
: xilinx::AIE::impl::AIEAssignCoreLinkFilesBase<
AIEAssignCoreLinkFilesPass> {
void runOnOperation() override {
DeviceOp device = getOperation();
// Builder is used only for attribute construction; no ops are inserted.
Builder builder(device.getContext());

// Build a map from func name to the object file(s) it requires, sourced
// from the "link_with" string attribute on func.func declarations.
// StringRefs are views into MLIRContext-owned storage and remain valid
// for the entire pass run.
DenseMap<StringRef, SmallVector<StringRef, 2>> funcToObjs;
for (auto funcOp : device.getOps<mlir::func::FuncOp>()) {
if (auto attr = funcOp->getAttrOfType<mlir::StringAttr>("link_with")) {
funcToObjs[funcOp.getName()].push_back(attr.getValue());
}
}

// Tracks which func.func symbols are directly called from at least one
// core; used to warn about link_with-bearing functions that are never
// called and whose object files would otherwise be silently omitted.
llvm::DenseSet<StringRef> usedFuncs;

// Only direct func.call edges are traced. func.call_indirect ops and
// calls through intermediate wrapper functions are not followed. To
// handle transitive dependencies, attach link_with directly to every
// func.func declaration that a core calls, even thin wrappers.
// TODO: extend to transitive call resolution.
device.walk([&](CoreOp core) {
// De-duplicate while preserving insertion order.
llvm::SetVector<StringRef> needed;

// Migrate deprecated core-level attr: warn, consume it, and add to set.
if (auto lw = core.getLinkWith()) {
core.emitWarning(
"link_with on aie.core is deprecated; attach link_with to "
"the func.func declaration instead");
needed.insert(lw.value());
core->removeAttr("link_with");
}

// Single walk over the core body: collect required object files and
// record called symbols (for the unused-func warning below).
core.walk([&](Operation *op) {
if (auto call = dyn_cast<mlir::func::CallOp>(op)) {
usedFuncs.insert(call.getCallee());
auto it = funcToObjs.find(call.getCallee());
if (it != funcToObjs.end())
for (StringRef obj : it->second)
needed.insert(obj);
} else if (auto indCall = dyn_cast<mlir::func::CallIndirectOp>(op)) {
indCall.emitWarning(
"indirect call in core body — link_with attributes on "
"indirectly-called functions are not automatically resolved; "
"add a direct func.call to the required func.func declaration "
"so that aie-assign-core-link-files can trace the dependency");
}
});

if (!needed.empty()) {
// builder is used only for attribute construction; its insertion
// point is irrelevant and no ops are inserted.
core.setLinkFilesAttr(builder.getStrArrayAttr(needed.getArrayRef()));
}
});

// Warn about funcs with link_with that are never called from any core.
for (auto &[funcName, objs] : funcToObjs) {
if (!usedFuncs.count(funcName)) {
if (auto funcOp = device.lookupSymbol<mlir::func::FuncOp>(funcName))
funcOp.emitWarning()
<< "func '" << funcName
<< "' has link_with but is never called from any core; "
"its .o file will not be linked";
}
}
}
};

std::unique_ptr<OperationPass<DeviceOp>>
AIE::createAIEAssignCoreLinkFilesPass() {
return std::make_unique<AIEAssignCoreLinkFilesPass>();
}
3 changes: 2 additions & 1 deletion lib/Dialect/AIE/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

add_mlir_dialect_library(
AIETransforms
AIEAssignBuffers.cpp
AIEAssignBufferDescriptorIDs.cpp
AIEAssignBuffers.cpp
AIEAssignCoreLinkFiles.cpp
AIEAssignLockIDs.cpp
AIEFindFlows.cpp
AIEPathFinder.cpp
Expand Down
15 changes: 12 additions & 3 deletions lib/Targets/AIETargetBCF.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,18 @@ LogicalResult AIETranslateToBCF(ModuleOp module, raw_ostream &output,
<< utohexstr(addressSpaceSize - dataMemoryEnd)
<< " // And everything else the core can't see\n";

if (tile.getCoreOp() && tile.getCoreOp().getLinkWith())
output << "_include _file "
<< tile.getCoreOp().getLinkWith().value().str() << "\n";
if (auto coreOp = tile.getCoreOp()) {
if (auto filesAttr = coreOp.getLinkFiles()) {
// Canonical path: link_files populated by aie-assign-core-link-files.
for (auto f : filesAttr->getAsRange<mlir::StringAttr>())
output << "_include _file " << f.getValue() << "\n";
} else if (coreOp.getLinkWith()) {
// Deprecated fallback: core-level link_with was not migrated by
// aie-assign-core-link-files (e.g., the pass was not run).
output << "_include _file " << coreOp.getLinkWith().value().str()
<< "\n";
}
}
output << "_resolve _main core_" << tile.getCol() << "_" << tile.getRow()
<< "\n";
}
Expand Down
11 changes: 10 additions & 1 deletion lib/Targets/AIETargetLdScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,19 @@ SECTIONS
targetModel.getMemEastBaseAddress(), std::string("east"));

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

output << "PROVIDE(main = core_" << tile.getCol() << "_"
<< tile.getRow() << ");\n";
Expand Down
8 changes: 4 additions & 4 deletions mlir_exercises/tutorial-8/aie.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ module @tutorial_8 {

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

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

// aie.use_lock(%lock13_1, "Release", 1)
aie.end
} { link_with="kernel1.o" }
}

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

}
8 changes: 4 additions & 4 deletions mlir_exercises/tutorial-8/answers/aie.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ module @tutorial_8 {

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

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

Expand All @@ -52,7 +52,7 @@ module @tutorial_8 {

// aie.use_lock(%lock23_1, "Release", 1)
aie.end
} { link_with="kernel2.o" }
}

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

// aie.use_lock(%lock24_1, "Release", 0)
aie.end
} { link_with="kernel1.o" }
}

}
10 changes: 5 additions & 5 deletions mlir_exercises/tutorial-9/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ MLIR gives us the ability to leverage different dialects such as [arith](https:/

Specifically, to support external functions, we use the operators `func.func` and `func.call` as follows:
```
func.func private @extern_kernel(%b: memref<256xi32>) -> ()
func.func private @extern_kernel(%b: memref<256xi32>) -> () attributes {link_with = "kernel.o"}

%core14 = AIE.core(%tile14) {
func.call @extern_kernel(%buf) : (memref<256xi32>) -> ()
AIE.end
} { link_with="kernel.o"}
}
```
In this MLIR code snippet, we see that we first call `func.func` to declare a private function whose function signature matches that of the AIE C/C++ function. The function name after the @ (e.g. `@external_kernel`) should match the C function name and the number of arguments should match the number of C function arguments. C++ name mangling is not supported. Argument types are converted according to the MLIR ['bare pointer' calling convention](https://mlir.llvm.org/docs/TargetLLVMIR/#bare-pointer-calling-convention-for-ranked-memref) (see below).
In this MLIR code snippet, we see that we first call `func.func` to declare a private function whose function signature matches that of the AIE C/C++ function. The function name after the @ (e.g. `@external_kernel`) should match the C function name and the number of arguments should match the number of C function arguments. C++ name mangling is not supported. Argument types are converted according to the MLIR ['bare pointer' calling convention](https://mlir.llvm.org/docs/TargetLLVMIR/#bare-pointer-calling-convention-for-ranked-memref) (see below).

| MLIR type | C type |
| ----------- | ----------- |
Expand All @@ -31,9 +31,9 @@ In this MLIR code snippet, we see that we first call `func.func` to declare a pr
| Memref | C pointer |
| index | int64_t |

Then, within the `AIE.core` operator, we use `func.call` to call the previously defined function from within our core, being sure to pass the appropriate function arguments. In this case, we pass in the the `AIE.buffer` `%buf`.
Then, within the `AIE.core` operator, we use `func.call` to call the previously defined function from within our core, being sure to pass the appropriate function arguments. In this case, we pass in the the `AIE.buffer` `%buf`.

The final step is to tell our tools where to look for the object code that the function whose name we defined in `func.func`/ `func.call`. Using the additional operator definition `link_with="kernel.o"`, we point to the file `kernel.o` in the current directory and link it in to create the final kernel object file.
The final step is to tell our tools where to look for the object code that the function whose name we defined in `func.func`/ `func.call`. Using the `link_with` attribute on the `func.func` declaration (e.g. `attributes {link_with = "kernel.o"}`), we point to the file `kernel.o` in the current directory and link it in to create the final kernel object file.
> Note that this allows us to call the function multiple times within the `AIE.core` or even separate functions in the same `AIE.core` if they are both defined within the single linked object file.

## <ins>Kernel object file generation</ins>
Expand Down
4 changes: 2 additions & 2 deletions mlir_exercises/tutorial-9/aie.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ module @tutorial_9 {

// declare kernel function name "extern_kernel" with one positional
// function argument, in this case mapped to a memref
func.func private @extern_kernel(%b: memref<256xi32>) -> ()
func.func private @extern_kernel(%b: memref<256xi32>) -> () attributes {link_with = "kernel.o"}

// Define the algorithm for the core of tile(1, 4)
// buf[3] = 14
Expand All @@ -52,6 +52,6 @@ module @tutorial_9 {
// by acquiring this lock (with value 1).
aie.use_lock(%lock14_0, "Release", 1)
aie.end
} { link_with="kernel.o" } // indicate kernel object name used by this core
} // indicate kernel object name used by this core

}
4 changes: 2 additions & 2 deletions mlir_exercises/tutorial-9/answers/aie_matmul.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module @tutorial_9 {

// declare kernel function name "extern_kernel" with one positional
// function argument, in this case mapped to a memref
func.func private @extern_kernel(%a: memref<32xi32>, %b: memref<32xi32>, %acc: memref<32xi32>, %c: memref<32xi32>) -> ()
func.func private @extern_kernel(%a: memref<32xi32>, %b: memref<32xi32>, %acc: memref<32xi32>, %c: memref<32xi32>) -> () attributes {link_with = "kernel_matmul.o"}

// Define the algorithm for the core of tile(1, 4)
// buf[3] = 14
Expand All @@ -55,6 +55,6 @@ module @tutorial_9 {
// by acquiring this lock (with value 1).
aie.use_lock(%lock14_0, "Release", 1)
aie.end
} { link_with="kernel_matmul.o" } // indicate kernel object name used by this core
} // indicate kernel object name used by this core

}
Loading
Loading