Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
30960c5
feat(iron): inline external kernels into the core (no func.call, no s…
fifield Jul 23, 2026
adfe70d
fix(iron): validate inline external kernel artifacts
fifield Jul 27, 2026
2c16a50
test(iron): reproduce inline collision suffix bug
fifield Jul 27, 2026
6ff737b
fix(iron): preserve inline suffix on collisions
fifield Jul 27, 2026
f62015c
fix(iron): insert alwaysinline per the LLVM define grammar
fifield Jul 28, 2026
a66a277
fix(aiecc): reject LLVM IR link artifacts on the chess path
fifield Jul 28, 2026
1ec4ed5
test(aiecc): cover .ll/.bc filtering in the ldscript/BCF emitters
fifield Jul 28, 2026
414c6cc
test(examples): run the inline_kernel example under lit
fifield Jul 28, 2026
3685760
Apply suggestions from code review
fifield Jul 28, 2026
b1967ca
fix(iron): restore identity attributes dropped in review autofix
fifield Jul 28, 2026
3473f0d
fix(iron): bind params_at unconditionally in _make_ir_inlinable
fifield Jul 28, 2026
52f9375
fix(iron): assemble inline .bc with clang++, not llvm-as
fifield Jul 28, 2026
f1fdbe3
[AIE] Add link_merge_files to aie.core
fifield Jul 28, 2026
1ca84d8
[AIE] Route link artifacts by link_with_mode, not by file suffix
fifield Jul 28, 2026
ed7ab9e
[AIE] Switch emitters, aiecc and IRON to the link mode contract
fifield Jul 28, 2026
4ea46aa
[example] Time inline_kernel with aie.utils.benchmark.run_iters
fifield Jul 28, 2026
c6ce33f
[test] De-duplicate the aiecc link-mode tests
fifield Jul 29, 2026
48d9805
[iron] Adopt the eager-callback Runtime API
fifield Jul 29, 2026
e863731
review changes
fifield Jul 29, 2026
6a2bcdc
[example] Copy tensor data out before the buffer is unmapped
fifield Jul 29, 2026
9880369
[example] Quote relative speedups
fifield Jul 29, 2026
066c991
tweak readme
fifield Jul 29, 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
48 changes: 39 additions & 9 deletions include/aie/Dialect/AIE/IR/AIEOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,14 @@ def AIE_CoreOp: AIE_Op<"core", [
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.
// These artifacts are ordinary inputs to the core's final link.
OptionalAttr<StrArrayAttr>:$link_files,
// Populated by aie-assign-core-link-files from func.func declarations that
// carry `link_with_mode = "merge"`. These artifacts are merged into the
// core's LLVM module (llvm-link) before codegen instead of being handed to
// the final link. May coexist with link_files, but a given path must not
// appear in both.
OptionalAttr<StrArrayAttr>:$link_merge_files,
OptionalAttr<StrAttr>:$elf_file,
OptionalAttr<BoolAttr>:$dynamic_objfifo_lowering,
// If set to false, suppresses automatic insertion of the parameter-sync
Expand Down Expand Up @@ -408,17 +415,40 @@ 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`.
**External link artifacts.** 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 artifact paths into the `link_files` and
`link_merge_files` attributes on this op. The BCF/ldscript emitters and the
aiecc driver consume both.

How an artifact is linked is a property of the metadata, never of the file
or file extension. A `func.func` declaration may carry an optional
`link_with_mode` string attribute alongside `link_with`:

* absent -- the artifact is an ordinary input to the core's final link,
and the pass records it in `link_files`;
* `"merge"` -- the artifact is an LLVM IR module (e.g. as emitted by
`ExternalFunction(inline=True)`) that must be merged into the core's
LLVM module with llvm-link before codegen, so that the callee can be
inlined and no separate object is linked. The pass records it in
`link_merge_files`.

`"merge"` is currently the only accepted `link_with_mode` value, and
`link_with_mode` without `link_with` is an error. Both lists may be present
on the same core, but a given path must not appear in both -- an artifact is
either merged or linked, not both. Because the aiecc "unified" flow builds
one LLVM module for all cores of a device, the pass additionally rejects a
path that is merged by one core and object-linked by another core of the
same device.

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.
backward compatibility. It always means an ordinary final-link input;
there is no core-level way to request merging. It is migrated by
`aie-assign-core-link-files` (its value is folded into `link_files` and then
removed). Specifying both `link_with` and either `link_files` or
`link_merge_files` on the same CoreOp is a verifier error.

Examples:
```
Expand Down
30 changes: 22 additions & 8 deletions include/aie/Dialect/AIE/Transforms/AIEPasses.td
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,38 @@ 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";
"Infer per-core link_files/link_merge_files from func-level link_with";
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.
Walks each aie.core and collects the external artifacts it needs by
tracing direct func.call edges to func.func declarations that carry a
"link_with" string attribute.

Each artifact is routed by its optional "link_with_mode" attribute:

* no "link_with_mode" -> the CoreOp's "link_files"
* "link_with_mode" = "merge" -> the CoreOp's "link_merge_files"

Both attributes are StrArrayAttrs and may coexist on one core. "merge"
is currently the only supported mode; any other value, or a
"link_with_mode" without a "link_with", is an error.

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.
folded into "link_files" -- it has no way to request a mode -- 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.
any core emit a warning; their artifacts will not appear in any core's
list.

The pass rejects a device in which one artifact is merged by
one core and object-linked by another. aiecc's unified mode compiles
all cores of a device from a single llvm-linked module, so such an
artifact would have its symbols defined twice.
}];

let constructor = "xilinx::AIE::createAIEAssignCoreLinkFilesPass()";
Expand Down
18 changes: 18 additions & 0 deletions lib/Dialect/AIE/IR/AIEDialect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1811,6 +1811,24 @@ LogicalResult CoreOp::verify() {
return emitOpError(
"cannot specify both 'link_with' (deprecated) and 'link_files' "
"on the same core; run aie-assign-core-link-files to migrate");
if (getLinkWith() && getLinkMergeFiles())
return emitOpError(
"cannot specify both 'link_with' (deprecated) and 'link_merge_files' "
"on the same core; run aie-assign-core-link-files to migrate");
// An artifact is either merged into the core's LLVM module or handed to the
// final link, never both: doing both would define its symbols twice.
if (auto linkFiles = getLinkFiles())
if (auto mergeFiles = getLinkMergeFiles()) {
llvm::SmallSet<StringRef, 8> linked;
for (auto f : linkFiles->getAsRange<StringAttr>())
linked.insert(f.getValue());
for (auto f : mergeFiles->getAsRange<StringAttr>())
if (linked.count(f.getValue()))
return emitOpError("artifact '")
<< f.getValue()
<< "' appears in both 'link_files' and 'link_merge_files'; an "
"artifact must be either merged or linked, not both";
}
return success();
}

Expand Down
153 changes: 127 additions & 26 deletions lib/Dialect/AIE/Transforms/AIEAssignCoreLinkFiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,25 @@
//
//===----------------------------------------------------------------------===//
//
// 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.
// This pass infers the per-core set of external link artifacts required by
// each core by tracing call edges from that 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.
// How an artifact is linked is taken from metadata, never from the file name:
// an optional "link_with_mode" attribute on the same func.func declaration
// selects the policy.
//
// * absent -> ordinary input to the core's final link
// * "merge" -> merged into the core's LLVM module (llvm-link)
// before codegen; never handed to the final link
//
// After the pass runs, every CoreOp that needs external artifacts will have a
// "link_files" and/or a "link_merge_files" StrArrayAttr containing the
// (de-duplicated) list of paths for that policy.
//
// Core-level "link_with" (deprecated) is also migrated: its value is added to
// the set and the attribute is removed from the CoreOp.
// the ordinary "link_files" set and the attribute is removed from the CoreOp.
// There is no core-level way to request merging.
//
//===----------------------------------------------------------------------===//

Expand All @@ -26,6 +36,7 @@
#include "mlir/Pass/Pass.h"

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

#define DEBUG_TYPE "aie-assign-core-link-files"
Expand All @@ -34,6 +45,28 @@ using namespace mlir;
using namespace xilinx;
using namespace xilinx::AIE;

namespace {

/// One external artifact required by a func.func declaration, together with
/// the policy that decides how it reaches the core binary.
struct LinkArtifact {
StringRef path;
bool merge;
};

/// The artifacts a single core needs, split by policy. Computed for every
/// core before any IR is mutated, so that the device-scope consistency check
/// below can fail the pass without leaving half-written attributes behind.
struct CoreLinkFiles {
CoreOp core;
bool hadDeprecatedLinkWith = false;
// De-duplicated, insertion-ordered.
llvm::SetVector<StringRef> linkFiles;
llvm::SetVector<StringRef> mergeFiles;
};

} // namespace

struct AIEAssignCoreLinkFilesPass
: xilinx::AIE::impl::AIEAssignCoreLinkFilesBase<
AIEAssignCoreLinkFilesPass> {
Expand All @@ -42,49 +75,79 @@ struct AIEAssignCoreLinkFilesPass
// 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.
// Build a map from func name to the artifact(s) it requires, sourced from
// the "link_with"/"link_with_mode" attributes 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;
DenseMap<StringRef, SmallVector<LinkArtifact, 2>> funcToObjs;
bool badMetadata = false;
for (auto funcOp : device.getOps<mlir::func::FuncOp>()) {
if (auto attr = funcOp->getAttrOfType<mlir::StringAttr>("link_with")) {
funcToObjs[funcOp.getName()].push_back(attr.getValue());
auto linkWith = funcOp->getAttrOfType<mlir::StringAttr>("link_with");
auto modeAttr = funcOp->getAttrOfType<mlir::StringAttr>("link_with_mode");
if (modeAttr && !linkWith) {
funcOp.emitError()
<< "func '" << funcOp.getName()
<< "' has link_with_mode but no link_with; link_with_mode only "
"describes how the link_with artifact is linked";
badMetadata = true;
continue;
}
if (!linkWith)
continue;
bool merge = false;
if (modeAttr) {
// "merge" is the only non-default policy; everything else is a typo.
if (modeAttr.getValue() != "merge") {
funcOp.emitError()
<< "func '" << funcOp.getName()
<< "' has unknown link_with_mode '" << modeAttr.getValue()
<< "'; the only supported value is 'merge'";
badMetadata = true;
continue;
}
merge = true;
}
funcToObjs[funcOp.getName()].push_back({linkWith.getValue(), merge});
}
if (badMetadata)
return signalPassFailure();

// 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.
// called and whose artifacts 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.
SmallVector<CoreLinkFiles> coreFiles;
device.walk([&](CoreOp core) {
// De-duplicate while preserving insertion order.
llvm::SetVector<StringRef> needed;
CoreLinkFiles files;
files.core = core;

// Migrate deprecated core-level attr: warn, consume it, and add to set.
// Migrate deprecated core-level attr: warn and add to the ordinary set.
// The attribute itself is removed in the mutation phase below. There is
// deliberately no core-level way to request merging.
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");
files.linkFiles.insert(lw.value());
files.hadDeprecatedLinkWith = true;
}

// Single walk over the core body: collect required object files and
// Single walk over the core body: collect required artifacts 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);
for (const LinkArtifact &artifact : it->second)
(artifact.merge ? files.mergeFiles : files.linkFiles)
.insert(artifact.path);
} else if (auto indCall = dyn_cast<mlir::func::CallIndirectOp>(op)) {
indCall.emitWarning(
"indirect call in core body — link_with attributes on "
Expand All @@ -94,21 +157,59 @@ struct AIEAssignCoreLinkFilesPass
}
});

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()));
}
coreFiles.push_back(std::move(files));
});

// Device-scope consistency check. aiecc has a "unified" mode in which all
// cores of a device share one LLVM module that is llvm-linked once; if one
// core merges an artifact while another object-links it, the second core's
// ELF would define the artifact's symbols twice. A per-core check is
// therefore not enough: the policy must agree across the whole device.
llvm::MapVector<StringRef, CoreOp> linkedBy;
for (CoreLinkFiles &files : coreFiles)
for (StringRef path : files.linkFiles)
linkedBy.insert({path, files.core});
bool conflict = false;
for (CoreLinkFiles &files : coreFiles)
for (StringRef path : files.mergeFiles) {
auto it = linkedBy.find(path);
if (it == linkedBy.end())
continue;
conflict = true;
InFlightDiagnostic diag =
files.core.emitError()
<< "artifact '" << path
<< "' is merged into an LLVM module here but object-linked in the "
"same aie.device; an artifact must use a single link mode "
"across all cores of a device";
diag.attachNote(it->second.getLoc())
<< "artifact '" << path << "' is object-linked here";
}
if (conflict)
return signalPassFailure();

// Mutation phase.
for (CoreLinkFiles &files : coreFiles) {
if (files.hadDeprecatedLinkWith)
files.core->removeAttr("link_with");
// builder is used only for attribute construction; its insertion
// point is irrelevant and no ops are inserted.
if (!files.linkFiles.empty())
files.core.setLinkFilesAttr(
builder.getStrArrayAttr(files.linkFiles.getArrayRef()));
if (!files.mergeFiles.empty())
files.core.setLinkMergeFilesAttr(
builder.getStrArrayAttr(files.mergeFiles.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";
"its artifact will not be linked or merged";
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion lib/Targets/AIETargetBCF.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,15 @@ LogicalResult AIETranslateToBCF(ModuleOp module, raw_ostream &output,
<< " // And everything else the core can't see\n";

if (auto coreOp = tile.getCoreOp()) {
// `link_files` holds the final-link inputs (object files)
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).
// aie-assign-core-link-files (e.g., the pass was not run). It carries
// no mode, so it is always an ordinary link input.
output << "_include _file " << coreOp.getLinkWith().value().str()
<< "\n";
}
Expand Down
4 changes: 3 additions & 1 deletion lib/Targets/AIETargetLdScript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,15 @@ SECTIONS
// them inside SECTIONS is invalid linker script syntax.
output << "}\n";
if (auto coreOp = tile.getCoreOp()) {
// `link_files` holds the ordinary final-link inputs (object files)
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).
// aie-assign-core-link-files (e.g., the pass was not run). It carries
// no mode, so it is always an ordinary link input.
output << "INPUT(" << fileAttr.value().str() << ")\n";
}

Expand Down
Loading
Loading