MLIR-AIE 1.4.5.dev
AIEAssignCoreLinkFiles.cpp
Go to the documentation of this file.
1//===- AIEAssignCoreLinkFiles.cpp -------------------------------*- C++ -*-===//
2//
3// Copyright (C) 2026 Advanced Micro Devices, Inc.
4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
5//
6//===----------------------------------------------------------------------===//
7//
8// This pass infers the per-core set of external link artifacts required by
9// each core by tracing call edges from that core to func.func declarations
10// that carry a "link_with" attribute.
11//
12// How an artifact is linked is taken from metadata, never from the file name:
13// an optional "link_with_mode" attribute on the same func.func declaration
14// selects the policy.
15//
16// * absent -> ordinary input to the core's final link
17// * "merge" -> merged into the core's LLVM module (llvm-link)
18// before codegen; never handed to the final link
19//
20// After the pass runs, every CoreOp that needs external artifacts will have a
21// "link_files" and/or a "link_merge_files" StrArrayAttr containing the
22// (de-duplicated) list of paths for that policy.
23//
24// Core-level "link_with" (deprecated) is also migrated: its value is added to
25// the ordinary "link_files" set and the attribute is removed from the CoreOp.
26// There is no core-level way to request merging.
27//
28//===----------------------------------------------------------------------===//
29
31#define GEN_PASS_DEF_AIEASSIGNCORELINKFILES
33
34#include "mlir/Dialect/Func/IR/FuncOps.h"
35#include "mlir/IR/Builders.h"
36#include "mlir/Pass/Pass.h"
37
38#include "llvm/ADT/DenseSet.h"
39#include "llvm/ADT/MapVector.h"
40#include "llvm/ADT/SetVector.h"
41
42#define DEBUG_TYPE "aie-assign-core-link-files"
43
44using namespace mlir;
45using namespace xilinx;
46using namespace xilinx::AIE;
47
48namespace {
49
50/// One external artifact required by a func.func declaration, together with
51/// the policy that decides how it reaches the core binary.
52struct LinkArtifact {
53 StringRef path;
54 bool merge;
55};
56
57/// The artifacts a single core needs, split by policy. Computed for every
58/// core before any IR is mutated, so that the device-scope consistency check
59/// below can fail the pass without leaving half-written attributes behind.
60struct CoreLinkFiles {
61 CoreOp core;
62 bool hadDeprecatedLinkWith = false;
63 // De-duplicated, insertion-ordered.
64 llvm::SetVector<StringRef> linkFiles;
65 llvm::SetVector<StringRef> mergeFiles;
66};
67
68} // namespace
69
72 AIEAssignCoreLinkFilesPass> {
73 void runOnOperation() override {
74 DeviceOp device = getOperation();
75 // Builder is used only for attribute construction; no ops are inserted.
76 Builder builder(device.getContext());
77
78 // Build a map from func name to the artifact(s) it requires, sourced from
79 // the "link_with"/"link_with_mode" attributes on func.func declarations.
80 // StringRefs are views into MLIRContext-owned storage and remain valid
81 // for the entire pass run.
82 DenseMap<StringRef, SmallVector<LinkArtifact, 2>> funcToObjs;
83 bool badMetadata = false;
84 for (auto funcOp : device.getOps<mlir::func::FuncOp>()) {
85 auto linkWith = funcOp->getAttrOfType<mlir::StringAttr>("link_with");
86 auto modeAttr = funcOp->getAttrOfType<mlir::StringAttr>("link_with_mode");
87 if (modeAttr && !linkWith) {
88 funcOp.emitError()
89 << "func '" << funcOp.getName()
90 << "' has link_with_mode but no link_with; link_with_mode only "
91 "describes how the link_with artifact is linked";
92 badMetadata = true;
93 continue;
94 }
95 if (!linkWith)
96 continue;
97 bool merge = false;
98 if (modeAttr) {
99 // "merge" is the only non-default policy; everything else is a typo.
100 if (modeAttr.getValue() != "merge") {
101 funcOp.emitError()
102 << "func '" << funcOp.getName()
103 << "' has unknown link_with_mode '" << modeAttr.getValue()
104 << "'; the only supported value is 'merge'";
105 badMetadata = true;
106 continue;
107 }
108 merge = true;
109 }
110 funcToObjs[funcOp.getName()].push_back({linkWith.getValue(), merge});
111 }
112 if (badMetadata)
113 return signalPassFailure();
114
115 // Tracks which func.func symbols are directly called from at least one
116 // core; used to warn about link_with-bearing functions that are never
117 // called and whose artifacts would otherwise be silently omitted.
118 llvm::DenseSet<StringRef> usedFuncs;
119
120 // Only direct func.call edges are traced. func.call_indirect ops and
121 // calls through intermediate wrapper functions are not followed. To
122 // handle transitive dependencies, attach link_with directly to every
123 // func.func declaration that a core calls, even thin wrappers.
124 // TODO: extend to transitive call resolution.
125 SmallVector<CoreLinkFiles> coreFiles;
126 device.walk([&](CoreOp core) {
127 CoreLinkFiles files;
128 files.core = core;
129
130 // Migrate deprecated core-level attr: warn and add to the ordinary set.
131 // The attribute itself is removed in the mutation phase below. There is
132 // deliberately no core-level way to request merging.
133 if (auto lw = core.getLinkWith()) {
134 core.emitWarning(
135 "link_with on aie.core is deprecated; attach link_with to "
136 "the func.func declaration instead");
137 files.linkFiles.insert(lw.value());
138 files.hadDeprecatedLinkWith = true;
139 }
140
141 // Single walk over the core body: collect required artifacts and
142 // record called symbols (for the unused-func warning below).
143 core.walk([&](Operation *op) {
144 if (auto call = dyn_cast<mlir::func::CallOp>(op)) {
145 usedFuncs.insert(call.getCallee());
146 auto it = funcToObjs.find(call.getCallee());
147 if (it != funcToObjs.end())
148 for (const LinkArtifact &artifact : it->second)
149 (artifact.merge ? files.mergeFiles : files.linkFiles)
150 .insert(artifact.path);
151 } else if (auto indCall = dyn_cast<mlir::func::CallIndirectOp>(op)) {
152 indCall.emitWarning(
153 "indirect call in core body — link_with attributes on "
154 "indirectly-called functions are not automatically resolved; "
155 "add a direct func.call to the required func.func declaration "
156 "so that aie-assign-core-link-files can trace the dependency");
157 }
158 });
159
160 coreFiles.push_back(std::move(files));
161 });
162
163 // Device-scope consistency check. aiecc has a "unified" mode in which all
164 // cores of a device share one LLVM module that is llvm-linked once; if one
165 // core merges an artifact while another object-links it, the second core's
166 // ELF would define the artifact's symbols twice. A per-core check is
167 // therefore not enough: the policy must agree across the whole device.
168 llvm::MapVector<StringRef, CoreOp> linkedBy;
169 for (CoreLinkFiles &files : coreFiles)
170 for (StringRef path : files.linkFiles)
171 linkedBy.insert({path, files.core});
172 bool conflict = false;
173 for (CoreLinkFiles &files : coreFiles)
174 for (StringRef path : files.mergeFiles) {
175 auto *it = linkedBy.find(path);
176 if (it == linkedBy.end())
177 continue;
178 conflict = true;
179 InFlightDiagnostic diag =
180 files.core.emitError()
181 << "artifact '" << path
182 << "' is merged into an LLVM module here but object-linked in the "
183 "same aie.device; an artifact must use a single link mode "
184 "across all cores of a device";
185 diag.attachNote(it->second.getLoc())
186 << "artifact '" << path << "' is object-linked here";
187 }
188 if (conflict)
189 return signalPassFailure();
190
191 // Mutation phase.
192 for (CoreLinkFiles &files : coreFiles) {
193 if (files.hadDeprecatedLinkWith)
194 files.core->removeAttr("link_with");
195 // builder is used only for attribute construction; its insertion
196 // point is irrelevant and no ops are inserted.
197 if (!files.linkFiles.empty())
198 files.core.setLinkFilesAttr(
199 builder.getStrArrayAttr(files.linkFiles.getArrayRef()));
200 if (!files.mergeFiles.empty())
201 files.core.setLinkMergeFilesAttr(
202 builder.getStrArrayAttr(files.mergeFiles.getArrayRef()));
203 }
204
205 // Warn about funcs with link_with that are never called from any core.
206 for (auto &[funcName, objs] : funcToObjs) {
207 if (!usedFuncs.count(funcName)) {
208 if (auto funcOp = device.lookupSymbol<mlir::func::FuncOp>(funcName))
209 funcOp.emitWarning()
210 << "func '" << funcName
211 << "' has link_with but is never called from any core; "
212 "its artifact will not be linked or merged";
213 }
214 }
215 }
216};
217
218std::unique_ptr<OperationPass<DeviceOp>>
220 return std::make_unique<AIEAssignCoreLinkFilesPass>();
221}
std::unique_ptr< mlir::OperationPass< DeviceOp > > createAIEAssignCoreLinkFilesPass()