Skip to content

Commit c7a73e6

Browse files
mattiasbrandmeta-codesync[bot]
authored andcommitted
Load AOTI plugins into hosts that don't share their C++ runtime
Summary: An AOTInductor plugin is compiled separately from the program that loads it, so the two don't have to share a C++ standard library. Two places in the CUDA backend assume they do. - **Constants pass through a `std::unordered_map`.** Cross-method weight sharing, mutable-state sessions and the CUDA graph host-constant check read a method's constants with `AOTInductorModelContainerExtractConstantsMap`, which hands the plugin a pointer to the host's map to fill in. That's only safe when both sides use the same standard library. It isn't safe for a Windows plugin cross-compiled with MinGW and loaded by an MSVC host, or a plugin loaded by a libc++ host. PyTorch's AOTI interface marks that call deprecated in favour of `AOTInductorModelContainerExtractConstantsMapEntries`, which returns a container-owned array of `{name, handle}` C structs. The backend now reads constants only through that call, so no C++ container crosses the plugin boundary. - **The plugin's C++ runtime has to already be in the process.** On Linux an x86 plugin references libstdc++ (`std::cerr`, exception handling, `operator new`) without listing `libstdc++.so.6` as a dependency. Loaded from a process that doesn't link libstdc++, `dlopen` fails with `undefined symbol: _ZSt4cerr`. When the host does link libstdc++, the plugin loads only if the host's dynamic symbol table happens to include every libstdc++ symbol the plugin uses. `load_library` now opens `libstdc++.so.6` with `RTLD_GLOBAL` once before the first plugin, and returns `AccessFailed` if it can't, since such a plugin can't load anyway. The preload adds libstdc++ to the global lookup scope after the host, so symbols the host already defines still win. Plugins that don't export the entries call still run, with reduced features: - each method loads its own weights from the blob, without cross-method sharing; - mutable-state sessions report `NotSupported`; - CUDA graphs stay off, since their constants can't be checked for host memory. Malformed results are never dereferenced: a null array with a nonzero count, a null name or handle, or an extraction error is rejected with `InvalidProgram`, or keeps graphs off. `test_cuda_mutable_state` gains cases for the graph check (no entries call, no constants, a host constant, a device constant, malformed results) and for mutable-state extraction failures. It also builds outside fbcode as `test_cuda_mutable_state_native`. Differential Revision: D129654474
1 parent 2b4739c commit c7a73e6

7 files changed

Lines changed: 331 additions & 44 deletions

File tree

‎backends/aoti/aoti_delegate_handle.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ using AOTInductorModelContainerExtractConstantsMapFunc = AOTIRuntimeError (*)(
114114
AOTInductorConstantMapHandle constant_map_handle,
115115
bool use_inactive);
116116

117+
// Extracts borrowed entries through a flat C ABI; the container owns the array.
118+
using AOTInductorModelContainerExtractConstantsMapEntriesFunc =
119+
AOTIRuntimeError (*)(
120+
AOTInductorModelContainerHandle container_handle,
121+
const AOTInductorConstantMapEntry** entries,
122+
size_t* num_entries,
123+
bool use_inactive);
124+
117125
// Updates the container's constants with user-managed tensor handles.
118126
// DLL-boundary safe — uses a flat C array instead of std::unordered_map.
119127
using AOTInductorModelContainerUpdateUserManagedConstantBufferPairsFunc =
@@ -146,6 +154,8 @@ struct AOTIDelegateHandle {
146154
AOTInductorModelContainerGetConstantNameFunc get_constant_name;
147155
AOTInductorModelContainerGetConstantOriginalFQNFunc get_constant_original_fqn;
148156
AOTInductorModelContainerExtractConstantsMapFunc extract_constants_map;
157+
AOTInductorModelContainerExtractConstantsMapEntriesFunc
158+
extract_constants_map_entries{nullptr};
149159
AOTInductorModelContainerUpdateUserManagedConstantBufferPairsFunc
150160
update_user_managed_constant_buffer_pairs;
151161
};

‎backends/cuda/runtime/cuda_backend.cpp‎

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ class ET_EXPERIMENTAL CudaBackend final
233233
Error load_function_pointers_into_handle(
234234
void* so_handle,
235235
cuda::CudaDelegateHandle* handle) const {
236+
if (handle == nullptr) {
237+
return Error::InvalidArgument;
238+
}
236239
#define LOAD_SYMBOL(member, name) \
237240
do { \
238241
auto symbol_res = get_function(so_handle, #name); \
@@ -299,6 +302,9 @@ class ET_EXPERIMENTAL CudaBackend final
299302
AOTInductorModelContainerGetConstantFromFolded);
300303
LOAD_OPTIONAL_SYMBOL(
301304
extract_constants_map, AOTInductorModelContainerExtractConstantsMap);
305+
LOAD_OPTIONAL_SYMBOL(
306+
extract_constants_map_entries,
307+
AOTInductorModelContainerExtractConstantsMapEntries);
302308
LOAD_OPTIONAL_SYMBOL(
303309
update_user_managed_constant_buffer_pairs,
304310
AOTInductorModelContainerUpdateUserManagedConstantBufferPairs);
@@ -628,7 +634,8 @@ class ET_EXPERIMENTAL CudaBackend final
628634
// Initialize CUDA graph state if enabled for this method. Not on a device
629635
// without memory pools, where the method runs without a graph.
630636
if (should_use_cuda_graph_for_method(method_name)) {
631-
const std::string host_constants = describe_host_constants(handle);
637+
const std::string host_constants =
638+
cuda::describe_host_constants_for_graph(*handle);
632639
if (!host_constants.empty()) {
633640
// A graph records only the device work of the capture call. Host code,
634641
// such as the choice of a torch.cond branch on a flag in host memory,
@@ -948,29 +955,6 @@ class ET_EXPERIMENTAL CudaBackend final
948955
}
949956

950957
private:
951-
// Names a constant of the program in host memory, or says that the
952-
// constants could not be listed, since unknown counts as host memory.
953-
// Empty when every constant is on the device.
954-
static std::string describe_host_constants(cuda::CudaDelegateHandle* handle) {
955-
const char* unlisted = "its constants could not be listed";
956-
if (handle->extract_constants_map == nullptr) {
957-
return unlisted;
958-
}
959-
std::unordered_map<std::string, AtenTensorHandle> constants;
960-
if (handle->extract_constants_map(
961-
handle->container_handle,
962-
reinterpret_cast<AOTInductorConstantMapHandle>(&constants),
963-
/*use_inactive=*/false) != Error::Ok) {
964-
return unlisted;
965-
}
966-
for (const auto& [fqn, tensor] : constants) {
967-
if (reinterpret_cast<SlimTensor*>(tensor)->is_cpu()) {
968-
return "constant '" + fqn + "' is in host memory";
969-
}
970-
}
971-
return "";
972-
}
973-
974958
// Runs the program once. On the capture step it captures the CUDA graph
975959
// instead, then launches it once, and sets graph_launched once that launch
976960
// is queued.
@@ -1513,9 +1497,13 @@ class ET_EXPERIMENTAL CudaBackend final
15131497
const NamedDataMap* named_data_map,
15141498
const std::string& method_name,
15151499
const std::string& weights_blob_key) const {
1500+
if (handle == nullptr || named_data_map == nullptr) {
1501+
return Error::InvalidArgument;
1502+
}
15161503
// Check if the required APIs are available
15171504
if (!handle->get_num_constants || !handle->get_constant_name ||
1518-
!handle->get_constant_original_fqn || !handle->extract_constants_map ||
1505+
!handle->get_constant_original_fqn ||
1506+
!handle->extract_constants_map_entries ||
15191507
!handle->update_user_managed_constant_buffer_pairs) {
15201508
// Fall back to the legacy path
15211509
return load_constants_legacy(handle, named_data_map, weights_blob_key);
@@ -1581,6 +1569,13 @@ class ET_EXPERIMENTAL CudaBackend final
15811569
std::unordered_map<std::string, AtenTensorHandle> extracted_map;
15821570

15831571
if (!uncached_fqns.empty()) {
1572+
const auto extract = handle->extract_constants_map_entries;
1573+
if (extract == nullptr) {
1574+
ET_LOG(
1575+
Error,
1576+
"CUDA blob weight sharing requires C-ABI constants extraction");
1577+
return Error::NotSupported;
1578+
}
15841579
// Need to load from blob — use update_constants_from_blob for all,
15851580
// then extract the new constants into the cache.
15861581
auto buffer_res = named_data_map->get_data(weights_blob_key.c_str());
@@ -1606,14 +1601,33 @@ class ET_EXPERIMENTAL CudaBackend final
16061601
ET_CUDA_CHECK_OR_RETURN_ERROR(cudaDeviceSynchronize());
16071602
buffer_res->Free();
16081603

1609-
// Extract all constants from the freshly-loaded container.
1604+
const AOTInductorConstantMapEntry* entries = nullptr;
1605+
size_t num_entries = 0;
16101606
ET_CHECK_OK_OR_RETURN_ERROR(
1611-
handle->extract_constants_map(
1607+
extract(
16121608
handle->container_handle,
1613-
reinterpret_cast<AOTInductorConstantMapHandle>(&extracted_map),
1609+
&entries,
1610+
&num_entries,
16141611
/*use_inactive=*/false),
16151612
"Failed to extract constants from '%s'",
16161613
method_name.c_str());
1614+
if (entries == nullptr && num_entries != 0) {
1615+
ET_LOG(
1616+
Error, "CUDA constants extraction returned a null entries array");
1617+
return Error::InvalidProgram;
1618+
}
1619+
if (entries != nullptr) {
1620+
for (size_t i = 0; i < num_entries; ++i) {
1621+
const auto entry = entries[i];
1622+
if (entry.name == nullptr || entry.handle == nullptr) {
1623+
ET_LOG(
1624+
Error,
1625+
"CUDA constants extraction returned a null name or tensor handle");
1626+
return Error::InvalidProgram;
1627+
}
1628+
extracted_map.emplace(entry.name, entry.handle);
1629+
}
1630+
}
16171631

16181632
// Validate cache hits against the freshly-extracted tensors, and
16191633
// populate the cache with newly-loaded entries.

‎backends/cuda/runtime/cuda_delegate_handle.h‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,35 @@ struct CudaDelegateHandle : public aoti::AOTIDelegateHandle {
356356
std::unordered_map<std::string, std::vector<int64_t>> offgraph_kv_sizes;
357357
};
358358

359+
// An unknown or host-resident constant prevents safe CUDA graph capture.
360+
// Compiled plugins return flat entries so C++ containers stay host-owned.
361+
inline std::string describe_host_constants_for_graph(
362+
const CudaDelegateHandle& handle) {
363+
const char* unlisted = "its constants could not be listed";
364+
if (handle.extract_constants_map_entries == nullptr) {
365+
return unlisted;
366+
}
367+
const aoti::AOTInductorConstantMapEntry* entries = nullptr;
368+
size_t count = 0;
369+
if (handle.extract_constants_map_entries(
370+
handle.container_handle, &entries, &count, /*use_inactive=*/false) !=
371+
executorch::runtime::Error::Ok ||
372+
(entries == nullptr && count != 0)) {
373+
return unlisted;
374+
}
375+
for (size_t i = 0; i < count; ++i) {
376+
if (entries[i].name == nullptr || entries[i].handle == nullptr) {
377+
return unlisted;
378+
}
379+
if (reinterpret_cast<aoti::slim::SlimTensor*>(entries[i].handle)
380+
->is_cpu()) {
381+
return std::string("constant '") + entries[i].name +
382+
"' is in host memory";
383+
}
384+
}
385+
return "";
386+
}
387+
359388
} // namespace cuda
360389
} // namespace backends
361390
} // namespace executorch

‎backends/cuda/runtime/cuda_mutable_state.cpp‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ thread_local MutableStateContext tl_active_ctx = kInvalidMutableContext;
101101
thread_local int tl_active_token = kNoMutableSession;
102102

103103
bool handle_has_symbols(CudaDelegateHandle* h) {
104-
return h->get_num_constants && h->get_constant_name &&
105-
h->get_constant_original_fqn && h->extract_constants_map &&
104+
return h != nullptr && h->get_num_constants && h->get_constant_name &&
105+
h->get_constant_original_fqn && h->extract_constants_map_entries &&
106106
h->update_user_managed_constant_buffer_pairs;
107107
}
108108

@@ -255,6 +255,13 @@ Error validate_coverage_locked(Context& c) {
255255
// Captures descriptors and initial templates while the container still owns its
256256
// default mutable buffers.
257257
Error build_descriptors(Context& c, CudaDelegateHandle* h) {
258+
if (h == nullptr) {
259+
return Error::InvalidArgument;
260+
}
261+
const auto extract = h->extract_constants_map_entries;
262+
if (extract == nullptr) {
263+
return Error::NotSupported;
264+
}
258265
auto container = h->container_handle;
259266

260267
size_t n = 0;
@@ -278,12 +285,27 @@ Error build_descriptors(Context& c, CudaDelegateHandle* h) {
278285
}
279286

280287
std::unordered_map<std::string, aoti::AtenTensorHandle> extracted;
288+
const aoti::AOTInductorConstantMapEntry* entries = nullptr;
289+
size_t num_entries = 0;
281290
ET_CHECK_OK_OR_RETURN_ERROR(
282-
h->extract_constants_map(
283-
container,
284-
reinterpret_cast<aoti::AOTInductorConstantMapHandle>(&extracted),
285-
/*use_inactive=*/false),
286-
"mutable_state: extract_constants_map failed");
291+
extract(container, &entries, &num_entries, /*use_inactive=*/false),
292+
"mutable_state: extract_constants_map_entries failed");
293+
if (entries == nullptr && num_entries != 0) {
294+
ET_LOG(
295+
Error,
296+
"mutable_state: constants extraction returned a null entries array");
297+
return Error::InvalidProgram;
298+
}
299+
if (entries != nullptr) {
300+
for (size_t i = 0; i < num_entries; ++i) {
301+
const auto entry = entries[i];
302+
if (entry.name == nullptr || entry.handle == nullptr) {
303+
ET_LOG(Error, "mutable_state: null constant name or tensor handle");
304+
return Error::InvalidProgram;
305+
}
306+
extracted.emplace(entry.name, entry.handle);
307+
}
308+
}
287309

288310
auto& table = c.desc[h];
289311
for (const auto& fqn : c.fqns) {

‎backends/cuda/runtime/platform/platform.cpp‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ executorch::runtime::Result<void*> load_library(
7676
}
7777

7878
#else
79+
#if defined(__linux__)
80+
// AOTI plugins use a C ABI but can leave GNU C++ runtime references
81+
// unresolved. Native hosts may use a different C++ standard library.
82+
static void* const gnu_cpp_runtime = []() {
83+
void* handle = dlopen("libstdc++.so.6", RTLD_NOW | RTLD_GLOBAL);
84+
if (handle == nullptr) {
85+
ET_LOG(Error, "Failed to load the AOTI GNU C++ runtime: %s", dlerror());
86+
}
87+
return handle;
88+
}();
89+
if (gnu_cpp_runtime == nullptr) {
90+
return executorch::runtime::Error::AccessFailed;
91+
}
92+
#endif
7993
// Before loading the delegate .so, we need to ensure symbols from the current
8094
// process (e.g. this backend library) are globally visible. Python loads
8195
// modules with RTLD_LOCAL by default, so we re-open the current module with

‎backends/cuda/runtime/targets.bzl‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,31 @@ def define_common_targets(is_fbcode = False):
164164
)
165165

166166
if not is_fbcode:
167+
runtime.genrule(
168+
name = "test_cuda_runtime_libraries",
169+
out = "libs",
170+
platforms = [CXX],
171+
compatible_with = ["ovr_config//cuda:has_cuda"],
172+
cmd = """
173+
mkdir -p "$OUT"
174+
cp $(location fbsource//third-party/cuda:cublas) "$OUT/"
175+
cp $(location fbsource//third-party/cuda:cublasLt) "$OUT/"
176+
cp $(location fbsource//third-party/cuda:cudart) "$OUT/"
177+
cp $(location fbsource//third-party/cuda:curand) "$OUT/"
178+
""",
179+
)
180+
runtime.cxx_test(
181+
name = "test_cuda_mutable_state_native",
182+
srcs = ["test/test_cuda_mutable_state.cpp"],
183+
env = {"LD_LIBRARY_PATH": "$(location :test_cuda_runtime_libraries)"},
184+
platforms = [CXX],
185+
compatible_with = ["ovr_config//cuda:has_cuda"],
186+
deps = [
187+
":cuda_backend",
188+
"//executorch/backends/aoti/slim/factory:from_blob",
189+
"fbsource//third-party/googletest:gtest_main",
190+
],
191+
)
167192
return
168193

169194
cpp_unittest(

0 commit comments

Comments
 (0)