Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1ac4281
Update
shoumikhin Jul 31, 2026
9415cc5
Update
shoumikhin Aug 1, 2026
448aec8
Update
shoumikhin Aug 1, 2026
c8cb1fc
Update
shoumikhin Aug 1, 2026
d843d61
Update
shoumikhin Aug 1, 2026
ee03285
Update
shoumikhin Aug 1, 2026
773d4ac
Update
shoumikhin Aug 1, 2026
3dc5dc5
Update
shoumikhin Aug 1, 2026
ca8f90d
Update
shoumikhin Aug 1, 2026
3c9845d
Update
shoumikhin Aug 1, 2026
52b926d
Update
shoumikhin Aug 1, 2026
d46bb95
Update
shoumikhin Aug 1, 2026
5c03937
Update
shoumikhin Aug 1, 2026
7b89a94
Update
shoumikhin Aug 1, 2026
4444cdf
Update
shoumikhin Aug 1, 2026
5279954
Update
shoumikhin Aug 1, 2026
1705c79
Update
shoumikhin Aug 1, 2026
c222b38
Update
shoumikhin Aug 1, 2026
52ed6a4
Update
shoumikhin Aug 1, 2026
f7d5065
Update
shoumikhin Aug 1, 2026
92c5102
Update
shoumikhin Aug 1, 2026
9a32371
Update
shoumikhin Aug 1, 2026
2fc3699
Update
shoumikhin Aug 1, 2026
f03af24
Update
shoumikhin Aug 1, 2026
d2ae3ff
Update
shoumikhin Aug 1, 2026
c3e5a4e
Update
shoumikhin Aug 2, 2026
f8f4e0f
Update
shoumikhin Aug 2, 2026
30117eb
Update
shoumikhin Aug 2, 2026
f0cc8d1
Update
shoumikhin Aug 2, 2026
e294878
Update
shoumikhin Aug 2, 2026
6e800b2
Update
shoumikhin Aug 2, 2026
f4599ab
Update
shoumikhin Aug 2, 2026
262166a
Update
shoumikhin Aug 2, 2026
ff43604
Update
shoumikhin Aug 2, 2026
db98248
Update
shoumikhin Aug 2, 2026
6ffb463
Update
shoumikhin Aug 2, 2026
207b1f6
Update
shoumikhin Aug 2, 2026
9fa91a3
Update
shoumikhin Aug 2, 2026
0ba529a
Update
shoumikhin Aug 2, 2026
a403f10
Update
shoumikhin Aug 2, 2026
41245b9
Update
shoumikhin Aug 2, 2026
aa96aa7
Update
shoumikhin Aug 2, 2026
5164d18
Update
shoumikhin Aug 2, 2026
d036cf8
Update
shoumikhin Aug 2, 2026
b498dad
Update
shoumikhin Aug 2, 2026
14dfdee
Update
shoumikhin Aug 2, 2026
658edef
Update
shoumikhin Aug 2, 2026
07fc10c
Update
shoumikhin Aug 2, 2026
c67cf7d
Update
shoumikhin Aug 2, 2026
9940166
Update
shoumikhin Aug 2, 2026
744c9e2
Update
shoumikhin Aug 2, 2026
404cc6b
Update
shoumikhin Aug 2, 2026
9e11594
Update
shoumikhin Aug 2, 2026
daec777
Update
shoumikhin Aug 2, 2026
7631bab
Update
shoumikhin Aug 3, 2026
e7b36dc
Update
shoumikhin Aug 3, 2026
235ec6a
Update
shoumikhin Aug 3, 2026
aee93c2
Update
shoumikhin Aug 3, 2026
1dbfad8
Update
shoumikhin Aug 3, 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
111 changes: 109 additions & 2 deletions .ci/scripts/wheel/test_cpp_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
"executorch::backends::xnnpack::XnnpackBackendOptions::workspace_manager",
)

# A representative symbol from the CUDA delegate's shim layer. The delegate's own
# methods are weak symbols, so this checks a strong one instead.
_CUDA_SYMBOLS = ("executorch::backends::cuda::clearCurrentCUDAStream",)

# `nm -DC` prints "<hexaddr> <kind> <name>" for a definition and
# " U <name>" for an undefined reference.
_DEFINED = re.compile(r"^[0-9a-fA-F]+\s+(?P<kind>[A-Za-z])\s+(?P<name>.+)$")
Expand Down Expand Up @@ -120,8 +124,100 @@ def _defines_symbol(library: Path, symbol: str) -> bool:
return False


def _assert_single_definer(symbols, what: str) -> None:
"""Exactly one shipped library may define each of `symbols`."""
def report_wheel_composition() -> None:
"""Print what the wheel ships and what each library needs.

Not an assertion. A size jump or an unexpected external dependency is the
first visible sign that a component got statically duplicated again, so the
numbers are worth having in the log of every run.
"""
package_dir = _installed_package_dir()
libraries = _shipped_shared_objects(package_dir)

print("shipped libraries:")
total = 0
for library in sorted(libraries, key=lambda path: path.name):
size = library.stat().st_size
total += size
print(f" {size / 1024:9.1f} KiB {library.relative_to(package_dir)}")
print(f" {total / 1024:9.1f} KiB total")

if shutil.which("readelf") is None:
return
# Anything the libraries need that the wheel does not itself ship has to be
# present on the user's machine, so it belongs in the report. Compare against
# the shipped file names rather than guessing from name prefixes.
shipped = {library.name for library in libraries}
external = set()
for library in libraries:
dynamic = subprocess.run(
["readelf", "-d", str(library)],
capture_output=True,
text=True,
check=False,
).stdout
for line in dynamic.splitlines():
if "(NEEDED)" not in line or "[" not in line:
continue
name = line.split("[", 1)[1].rstrip("]").strip()
if name not in shipped:
external.add(name)
if external:
print("external dependencies expected from the environment:")
for name in sorted(external):
print(f" {name}")


def test_shipped_libraries_load() -> None:
"""Every shipped library must depend only on things that exist.

The symbol checks prove each component is defined exactly once, but a library
can still be unloadable if it needs something nothing provides, which is a
packaging bug rather than a duplication bug.

A dependency the wheel ships elsewhere is fine even when `ldd` cannot resolve
it: some extensions are loaded after `import torch` has already brought their
dependencies into the process, so they intentionally carry no path to them.
Only a name nothing in the wheel provides is a real problem.
"""
if shutil.which("ldd") is None:
print("- ldd not available, skipping the load check")
return

package_dir = _installed_package_dir()
libraries = _shipped_shared_objects(package_dir)
shipped = {library.name for library in libraries}

broken = {}
for library in libraries:
resolved = subprocess.run(
["ldd", str(library)], capture_output=True, text=True, check=False
).stdout
missing = [
name
for name in (
line.split("=>")[0].strip()
for line in resolved.splitlines()
if "not found" in line
)
if name not in shipped
]
if missing:
broken[str(library.relative_to(package_dir))] = missing

assert not broken, (
"shipped libraries need dependencies that nothing provides, so they will "
f"fail to load: {broken}"
)
print("✓ every shipped library depends only on things that exist")


def _assert_single_definer(symbols, what: str, optional: bool = False) -> None:
"""Exactly one shipped library may define each of `symbols`.

`optional` allows a component that is only present in some wheel flavors,
such as an accelerator delegate, to be absent without failing.
"""
assert shutil.which("nm") is not None, "nm is required to inspect the wheel"

package_dir = _installed_package_dir()
Expand All @@ -131,6 +227,9 @@ def _assert_single_definer(symbols, what: str) -> None:
for symbol in symbols:
definers = [lib for lib in libraries if _defines_symbol(lib, symbol)]
pretty = [str(lib.relative_to(package_dir)) for lib in definers]
if optional and not definers:
print(f"- no {what} in this wheel, skipping")
return
assert len(definers) == 1, (
Comment thread
shoumikhin marked this conversation as resolved.
Outdated
f"expected exactly one library to define {symbol}, found "
f"{len(definers)}: {pretty}. More than one definition means the "
Expand Down Expand Up @@ -159,6 +258,11 @@ def test_single_xnnpack_delegate() -> None:
_assert_single_definer(_XNNPACK_SYMBOLS, "XNNPACK delegate")


def test_single_cuda_delegate() -> None:
"""Exactly one shipped library may define the CUDA delegate, if present."""
_assert_single_definer(_CUDA_SYMBOLS, "CUDA delegate", optional=True)


def test_cpp_consumer(work_dir: Path) -> None:
"""A standalone C++ app builds and runs against the installed wheel."""
assert shutil.which("cmake") is not None, "cmake is required to build a consumer"
Expand Down Expand Up @@ -257,8 +361,11 @@ def _assert_runs_relocated(consumer, package_dir, work_dir, environment) -> None


def run_tests(work_dir: Path) -> None:
report_wheel_composition()
test_shipped_libraries_load()
test_single_backend_registry()
test_single_threadpool()
test_single_kernel_registration()
test_single_xnnpack_delegate()
test_single_cuda_delegate()
test_cpp_consumer(work_dir)
54 changes: 43 additions & 11 deletions backends/cuda/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,17 @@ target_compile_options(
PUBLIC "$<$<COMPILE_LANGUAGE:CXX>:${_cuda_cxx_compile_options}>"
)

# Link against ExecuTorch core libraries
target_link_libraries(cuda_platform PRIVATE executorch_core ${CMAKE_DL_LIBS})
# Link against ExecuTorch core libraries. Resolve them from the shared runtime
# when there is one, so this does not carry a second copy of the backend
# registry.
if(EXECUTORCH_BUILD_SHARED)
target_link_libraries(
cuda_platform PRIVATE executorch_shared ${CMAKE_DL_LIBS}
)
executorch_target_link_shared_runtime(cuda_platform)
else()
target_link_libraries(cuda_platform PRIVATE executorch_core ${CMAKE_DL_LIBS})
endif()

install(
TARGETS cuda_platform
Expand Down Expand Up @@ -169,14 +178,9 @@ if(_cuda_is_msvc_toolchain)
else()
target_link_libraries(
aoti_cuda_shims
PRIVATE cuda_platform
PUBLIC -Wl,--whole-archive
aoti_common_shims_slim
-Wl,--no-whole-archive
CUDA::cudart
CUDA::curand
extension_cuda
${CMAKE_DL_LIBS}
PRIVATE cuda_platform -Wl,--whole-archive aoti_common_shims_slim
-Wl,--no-whole-archive
PUBLIC CUDA::cudart CUDA::curand extension_cuda ${CMAKE_DL_LIBS}
)
endif()

Expand All @@ -200,7 +204,35 @@ if(_cuda_is_msvc_toolchain)
list(APPEND _aoti_cuda_backend_sources runtime/cuda_allocator.cpp)
endif()

add_library(aoti_cuda_backend STATIC ${_aoti_cuda_backend_sources})
# Build the delegate as a shared library for the wheel so a process has one copy
# of it, and keep it static everywhere else so no other build changes.
if(EXECUTORCH_BUILD_SHARED)
set(_aoti_cuda_backend_library_type SHARED)
else()
set(_aoti_cuda_backend_library_type STATIC)
endif()
add_library(
aoti_cuda_backend ${_aoti_cuda_backend_library_type}
${_aoti_cuda_backend_sources}
)
if(EXECUTORCH_BUILD_SHARED)
set_target_properties(
aoti_cuda_backend
PROPERTIES OUTPUT_NAME executorch_cuda_backend
VERSION "${PROJECT_VERSION}"
SOVERSION "${PROJECT_VERSION_MAJOR}"
)
if(NOT APPLE)
# Ships in the wheel's lib/ directory, but the CUDA shim library it links
# lives under backends/cuda, so both locations have to be searchable. The
# CUDA runtime itself comes from the environment and is not bundled.
set(_cuda_backend_rpath "$ORIGIN:$ORIGIN/../backends/cuda")
set_target_properties(
aoti_cuda_backend PROPERTIES BUILD_RPATH "${_cuda_backend_rpath}"
INSTALL_RPATH "${_cuda_backend_rpath}"
)
endif()
endif()
Comment thread
shoumikhin marked this conversation as resolved.

target_include_directories(
aoti_cuda_backend
Expand Down
17 changes: 17 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,23 @@ def run(self): # noqa C901
"EXECUTORCH_BUILD_XNNPACK",
],
),
# Install the CUDA delegate beside them when it is built. The CUDA
# runtime itself is not bundled; it comes from the environment.
BuiltFile(
src_dir="%CMAKE_CACHE_DIR%/backends/cuda/",
src_name=(
"libexecutorch_cuda_backend.so."
f"{get_runtime_soname_major()}.*"
),
dst=(
"executorch/lib/libexecutorch_cuda_backend.so."
f"{get_runtime_soname_major()}"
),
dependent_cmake_flags=[
"EXECUTORCH_BUILD_SHARED",
"EXECUTORCH_BUILD_CUDA",
],
),
# Install the prebuilt pybindings extension wrapper for the runtime,
# portable kernels, and a selection of backends. This lets users
# load and execute .pte files from python.
Expand Down
Loading