diff --git a/.ci/scripts/test_cpp_sdk_wheel.sh b/.ci/scripts/test_cpp_sdk_wheel.sh new file mode 100755 index 00000000000..a17cf16dd86 --- /dev/null +++ b/.ci/scripts/test_cpp_sdk_wheel.sh @@ -0,0 +1,357 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Builds the Linux wheel with the prebuilt C++ SDK, installs it into a clean +# environment, and builds a standalone C++ program against it via +# find_package(executorch). This is the signal that the shipped +# libexecutorch.so, headers, and CMake package config actually let a C++ +# consumer link the ExecuTorch runtime with no source checkout, and that a +# separately built "coreless" backend shared library can register into the one +# runtime registry (the mechanism coalesced multi-backend execution relies on). + +set -euxo pipefail + +PYTHON_EXECUTABLE="${PYTHON_EXECUTABLE:-python}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +BUILD_VENV="${REPO_ROOT}/.venv-sdk-build" +TEST_VENV="${REPO_ROOT}/.venv-sdk-test" +WORK_DIR="${REPO_ROOT}/cpp_sdk_consumer" + +rm -rf "${BUILD_VENV}" "${TEST_VENV}" "${WORK_DIR}" "${REPO_ROOT}/dist" \ + "${REPO_ROOT}/pip-out" + +# --------------------------------------------------------------------------- +# Build the wheel. +# --------------------------------------------------------------------------- +"${PYTHON_EXECUTABLE}" -m venv "${BUILD_VENV}" +# shellcheck source=/dev/null +source "${BUILD_VENV}/bin/activate" +python -m pip install --upgrade pip +python -m pip install \ + "cmake>=3.24,<4.0.0" \ + "numpy>=2.0.0" \ + packaging \ + pyyaml \ + setuptools \ + wheel \ + zstd \ + certifi \ + torch \ + torchvision \ + --index-url "${TORCH_INDEX_URL:-https://download.pytorch.org/whl/cpu}" \ + --extra-index-url https://pypi.org/simple + +( + cd "${REPO_ROOT}" + CMAKE_ARGS="${SDK_WHEEL_CMAKE_ARGS:-}" python setup.py bdist_wheel +) + +WHEEL_FILE="$(find "${REPO_ROOT}/dist" -maxdepth 1 -name 'executorch-*.whl' | head -1)" +test -n "${WHEEL_FILE}" + +# --------------------------------------------------------------------------- +# Verify the SDK payload is present in the wheel. +# --------------------------------------------------------------------------- +python - "${WHEEL_FILE}" <<'PY' +import sys +import zipfile + +wheel_file = sys.argv[1] +with zipfile.ZipFile(wheel_file) as wheel: + names = set(wheel.namelist()) + +required = [ + "executorch/share/cmake/executorch-config.cmake", + "executorch/utils/__init__.py", + "executorch/include/executorch/runtime/executor/program.h", + "executorch/include/executorch/extension/module/module.h", +] +missing = [name for name in required if name not in names] +if missing: + raise AssertionError(f"{wheel_file} is missing SDK files: {missing}") + +# The loader resolves the SONAME (libexecutorch.so.), which is the only +# runtime library shipped. Without it the wheel has no linkable runtime. +versioned = [ + name + for name in names + if name.startswith("executorch/lib/libexecutorch.so.") + and name.split("libexecutorch.so.")[1][:1].isdigit() +] +if not versioned: + raise AssertionError( + f"{wheel_file} has no versioned libexecutorch.so.; the SONAME " + "library is missing and the runtime cannot load." + ) + +# If the kernels lib is shipped, the standalone threadpool lib must be too: +# get_threadpool() is a process-wide singleton, so the kernels lib links it +# dynamically instead of embedding its own pool. +has_kernels = any( + n.startswith("executorch/lib/libexecutorch_kernels.so.") for n in names +) +has_threadpool = any( + n.startswith("executorch/lib/libexecutorch_threadpool.so.") for n in names +) +if has_kernels and not has_threadpool: + raise AssertionError( + f"{wheel_file} ships the kernels lib but not the standalone " + "libexecutorch_threadpool.so; the thread pool would be duplicated." + ) + +# Headers whose implementation is not shipped must not be advertised. +forbidden = [ + "executorch/include/executorch/extension/module/bundled_module.h", + "executorch/include/executorch/extension/flat_tensor/serialize/serialize.h", +] +present = [name for name in forbidden if name in names] +if present: + raise AssertionError(f"{wheel_file} advertises unshipped APIs: {present}") + +print("SDK payload OK") +PY + +deactivate + +# --------------------------------------------------------------------------- +# Install into a clean environment and link a C++ consumer against it. +# --------------------------------------------------------------------------- +"${PYTHON_EXECUTABLE}" -m venv "${TEST_VENV}" +# shellcheck source=/dev/null +source "${TEST_VENV}/bin/activate" +python -m pip install --upgrade pip +python -m pip install "cmake>=3.24,<4.0.0" +# --no-deps: the C++ SDK link test does not need torch, proving the runtime is +# linkable standalone. (A plain install pulls declared deps; not needed here.) +python -m pip install --no-deps "${WHEEL_FILE}" + +CMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')" +export CMAKE_PREFIX_PATH + +mkdir -p "${WORK_DIR}" +cd "${WORK_DIR}" + +cat > main.cpp <<'CPP' +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using executorch::extension::from_blob; +using executorch::extension::FlatTensorDataMap; +using executorch::extension::Module; +using executorch::extension::TensorPtr; +using executorch::runtime::get_backend_class; +using executorch::runtime::get_num_registered_backends; +using executorch::runtime::runtime_init; + +int main() { + runtime_init(); + printf("registered backends: %zu\n", get_num_registered_backends()); + // The delegate-only SDK ships no backends, so this must be null. What matters + // is that the symbol links and resolves from libexecutorch.so. + printf("stock backend lookup resolves: %d\n", + get_backend_class("NonexistentBackend") == nullptr); + + // Exercise the advertised SDK surface so a broken header or missing symbol in + // Module / Tensor / DataLoader is caught at compile+link time, not just the + // runtime/backend headers. + std::vector data = {1.0f, 2.0f, 3.0f, 4.0f}; + TensorPtr tensor = from_blob(data.data(), {2, 2}); + printf("tensor numel: %zu\n", tensor->numel()); + + // Exercise the .ptd data-map surface (FlatTensorDataMap) so those advertised + // headers/symbols are compiled and linked from libexecutorch.so. Loading a + // bogus buffer is expected to fail; we only need the symbol to resolve. + { + const uint8_t bogus_ptd[8] = {0}; + auto data_map_result = FlatTensorDataMap::load( + std::make_unique( + bogus_ptd, sizeof(bogus_ptd)) + .get()); + printf(".ptd data-map load attempted (ok=%d)\n", + data_map_result.ok()); + } + + // odr-use MergedDataMap so a dropped symbol fails the link, not just + // a header include. An empty span is a valid no-op merge. + { + executorch::runtime::Span + maps; + auto merged = executorch::extension::MergedDataMap::load(maps); + printf("merged data-map load attempted (ok=%d)\n", merged.ok()); + } + + // Construct a Module from a tiny in-memory buffer via the buffer data loader. + // Loading is expected to fail (not a real .pte); we only need this to + // compile and link against the Module/DataLoader symbols in libexecutorch.so. + const uint8_t fake_pte[8] = {0}; + Module module(std::make_unique( + fake_pte, sizeof(fake_pte))); + (void)module.method_names(); + + printf("PASS: linked executorch runtime + Module/Tensor/DataLoader from the " + "wheel\n"); + return 0; +} +CPP + +cat > CMakeLists.txt <<'CMAKE' +cmake_minimum_required(VERSION 3.19) +project(executorch_cpp_sdk_consumer LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +find_package(executorch CONFIG REQUIRED) +if(NOT EXECUTORCH_SDK_FOUND) + message(FATAL_ERROR "EXECUTORCH_SDK_FOUND is false; the wheel C++ SDK is missing") +endif() +add_executable(consumer main.cpp) +target_link_libraries(consumer PRIVATE executorch::runtime) +CMAKE + +cmake -S . -B build +cmake --build build +./build/consumer + +# The runtime and the consumer must not pull in libtorch. +if ldd ./build/consumer | grep -Eiq "libtorch|libc10"; then + echo "ERROR: consumer unexpectedly links libtorch/libc10" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# Prove cross-shared-object registration: a "coreless" backend .so (no bundled +# runtime, register_backend undefined) registers into the runtime inside +# libexecutorch.so when loaded. This is the mechanism coalesced multi-backend +# execution depends on. +# --------------------------------------------------------------------------- +cat > mybackend.cpp <<'CPP' +#include +using namespace executorch::runtime; +namespace { +struct MyBackend final : public BackendInterface { + bool is_available() const override { return true; } + Result init(BackendInitContext&, FreeableBuffer*, + ArrayRef) const override { + return nullptr; + } + Error execute(BackendExecutionContext&, DelegateHandle*, + Span) const override { + return Error::Ok; + } + void destroy(DelegateHandle*) const override {} +}; +MyBackend g_backend; +Backend g_id{"MyTestBackend", &g_backend}; +static auto g_registered = register_backend(g_id); +} // namespace +CPP + +cat >> CMakeLists.txt <<'CMAKE' +# Coreless: undefined ExecuTorch symbols resolve from libexecutorch.so at load. +add_library(mybackend SHARED mybackend.cpp) +target_link_options(mybackend PRIVATE "LINKER:--unresolved-symbols=ignore-all") +target_include_directories(mybackend PRIVATE + $) +target_compile_definitions(mybackend PRIVATE C10_USING_CUSTOM_GENERATED_MACROS) + +add_executable(reg_consumer reg_main.cpp) +target_link_libraries(reg_consumer PRIVATE executorch::runtime ${CMAKE_DL_LIBS}) +CMAKE + +cat > reg_main.cpp <<'CPP' +#include +#include +#include +#include + +using executorch::runtime::get_backend_class; +using executorch::runtime::runtime_init; + +int main(int argc, char** argv) { + runtime_init(); + if (get_backend_class("MyTestBackend") != nullptr) { + fprintf(stderr, "backend registered before load\n"); + return 1; + } + void* handle = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL); + if (handle == nullptr) { + fprintf(stderr, "dlopen failed: %s\n", dlerror()); + return 1; + } + if (get_backend_class("MyTestBackend") == nullptr) { + fprintf(stderr, "backend NOT registered after load\n"); + return 1; + } + printf("PASS: coreless backend .so registered into libexecutorch.so\n"); + return 0; +} +CPP + +cmake -S . -B build +cmake --build build +./build/reg_consumer "$(find build -name 'libmybackend.so' | head -1)" + +# --------------------------------------------------------------------------- +# CUDA delegate check. Only runs when the wheel actually shipped the CUDA +# backend (i.e. a CUDA-enabled wheel); a CPU wheel skips this cleanly. Verifies +# the REAL shipped artifact: dlopen libexecutorch_cuda_backend.so and assert it +# registers "CudaBackend" into the runtime in libexecutorch.so. +SDK_LIB_DIR="$(python -c 'import os, executorch.utils as u; print(os.path.join(os.path.dirname(os.path.dirname(u.cmake_prefix_path)), "lib"))')" +CUDA_BACKEND_SO="${SDK_LIB_DIR}/libexecutorch_cuda_backend.so" +if [ ! -f "${CUDA_BACKEND_SO}" ] && [ "${EXPECT_CUDA:-0}" = "1" ]; then + echo "EXPECT_CUDA=1 but libexecutorch_cuda_backend.so is missing" >&2 + exit 1 +fi +if [ -f "${CUDA_BACKEND_SO}" ]; then + echo "CUDA backend present; verifying it registers CudaBackend" + cat > cuda_reg.cpp <<'CPP' +#include +#include +#include +#include + +using executorch::runtime::get_backend_class; +using executorch::runtime::runtime_init; + +int main(int argc, char** argv) { + runtime_init(); + if (get_backend_class("CudaBackend") != nullptr) { + fprintf(stderr, "CudaBackend registered before load\n"); + return 1; + } + void* handle = dlopen(argv[1], RTLD_NOW | RTLD_GLOBAL); + if (handle == nullptr) { + fprintf(stderr, "dlopen failed: %s\n", dlerror()); + return 1; + } + if (get_backend_class("CudaBackend") == nullptr) { + fprintf(stderr, "CudaBackend NOT registered after load\n"); + return 1; + } + printf("PASS: shipped libexecutorch_cuda_backend.so registered CudaBackend\n"); + return 0; +} +CPP + cat >> CMakeLists.txt <<'CMAKE' +add_executable(cuda_reg cuda_reg.cpp) +target_link_libraries(cuda_reg PRIVATE executorch::runtime ${CMAKE_DL_LIBS}) +CMAKE + cmake -S . -B build + cmake --build build + # dlopen by absolute path; its $ORIGIN rpath resolves the co-shipped deps. + ./build/cuda_reg "${CUDA_BACKEND_SO}" +else + echo "CUDA backend not in wheel (CPU wheel); skipping CudaBackend check" +fi + +echo "ALL C++ SDK WHEEL CHECKS PASSED" diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index ddad7eebf61..7d5d91cafce 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -116,6 +116,29 @@ jobs: # Build and test ExecuTorch with the add model on portable backend. PYTHON_EXECUTABLE=python bash .ci/scripts/test_model.sh "add" "${BUILD_TOOL}" "portable" + test-cpp-sdk-wheel-linux: + name: test-cpp-sdk-wheel-linux + uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + with: + runner: linux.2xlarge + docker-image: ci-image:executorch-ubuntu-22.04-gcc11 + submodules: 'recursive' + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + timeout: 90 + script: | + # The generic Linux job chooses to use base env, not the one setup by the image + CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]") + conda activate "${CONDA_ENV}" + + # Build the wheel, install it clean, and link a C++ consumer against the + # shipped libexecutorch.so via find_package(executorch). + PYTHON_EXECUTABLE=python bash .ci/scripts/test_cpp_sdk_wheel.sh + test-models-linux-basic: name: test-models-linux-basic uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main diff --git a/CMakeLists.txt b/CMakeLists.txt index ff3b9e86f7e..7d88dd514e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1420,6 +1420,33 @@ else() endif() target_link_libraries(executorch_kernels INTERFACE ${_executorch_kernels}) +# Standalone CPU kernels library for the pip C++ SDK, shipped separately from +# libexecutorch.so so a fully delegated model links no kernels (mirrors iOS +# kernels_optimized). PRIVATE executorch_shared so runtime symbols resolve from +# the one libexecutorch.so at load rather than embedding a second registry. +if(EXECUTORCH_BUILD_SHARED AND _executorch_kernels) + executorch_add_shared_library(executorch_kernels_shared) + set_target_properties( + executorch_kernels_shared + PROPERTIES OUTPUT_NAME executorch_kernels + ARCHIVE_OUTPUT_NAME executorch_kernels_shared + EXPORT_NAME executorch-kernels-shared + ) + target_include_directories( + executorch_kernels_shared PUBLIC ${_common_include_directories} + ) + target_compile_definitions( + executorch_kernels_shared PUBLIC C10_USING_CUSTOM_GENERATED_MACROS + ) + # Link the shared threadpool first so its exported get_threadpool()/ + # parallel_for satisfy the kernels' references, and the static + # extension_threadpool archive is not pulled in (which would embed a second + # singleton pool). executorch_shared provides the runtime core symbols. + target_link_libraries( + executorch_kernels_shared PRIVATE executorch_shared ${_executorch_kernels} + ) +endif() + install(TARGETS executorch_backends executorch_extensions executorch_kernels EXPORT ExecuTorchTargets ) diff --git a/extension/threadpool/CMakeLists.txt b/extension/threadpool/CMakeLists.txt index 3b9c7c66ddb..5deb57c3df1 100644 --- a/extension/threadpool/CMakeLists.txt +++ b/extension/threadpool/CMakeLists.txt @@ -30,10 +30,31 @@ else() set(_threadpool_size_flag "EXECUTORCH_THREADPOOL_USE_PERFORMANCE_CORES") endif() -add_library( - extension_threadpool threadpool.cpp threadpool_guard.cpp thread_parallel.cpp - cpuinfo_utils.cpp -) +# get_threadpool() is a process-wide singleton, so in a shared (pip C++ SDK) +# build it must live in exactly one shared object that every consumer (the +# kernels lib, XNNPACK and other backends, _portable_lib) links dynamically. +# Otherwise each consumer that statically links extension_threadpool embeds its +# own pool. Build it SHARED for EXECUTORCH_BUILD_SHARED and keep it STATIC +# everywhere else (iOS/Android/embedded are unaffected). Mirrors iOS's +# standalone threadpool framework. cpuinfo and pthreadpool are static and +# bundled in; executorch_core resolves from libexecutorch.so at load. +if(EXECUTORCH_BUILD_SHARED) + add_library( + extension_threadpool SHARED threadpool.cpp threadpool_guard.cpp + thread_parallel.cpp cpuinfo_utils.cpp + ) + set_target_properties( + extension_threadpool + PROPERTIES OUTPUT_NAME executorch_threadpool + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}" + ) +else() + add_library( + extension_threadpool threadpool.cpp threadpool_guard.cpp + thread_parallel.cpp cpuinfo_utils.cpp + ) +endif() target_link_libraries( extension_threadpool PUBLIC executorch_core cpuinfo pthreadpool ) diff --git a/setup.py b/setup.py index 8331deec8ad..9d4387686a8 100644 --- a/setup.py +++ b/setup.py @@ -322,6 +322,37 @@ def get_dynamic_lib_name(name: str) -> str: return f"lib{name}.so" +def _read_soname(lib_path: Path) -> str: + """Return the ELF SONAME of a shared library. + + Used to recreate the loader-visible SONAME symlink for a versioned .so in + the wheel. Raises if the SONAME cannot be read: without it the wheel would + link at build time but fail to load at runtime (the SONAME recorded in + dependents would have no matching file), so this must fail the build loudly + rather than silently ship a broken wheel. + """ + try: + out = subprocess.run( + ["readelf", "-d", os.fspath(lib_path)], + capture_output=True, + text=True, + check=True, + ).stdout + except (OSError, subprocess.CalledProcessError) as e: + raise RuntimeError( + f"Could not read the SONAME of {lib_path} via readelf: {e}. " + "The shared-library SONAME symlink cannot be created, which would " + "produce a wheel that fails to load at runtime." + ) from e + for line in out.splitlines(): + if "SONAME" in line and "[" in line and "]" in line: + return line[line.index("[") + 1 : line.index("]")] + raise RuntimeError( + f"{lib_path} has no ELF SONAME; cannot create the SONAME symlink for " + "the wheel. Expected a versioned shared library (e.g. libfoo.so.1.2.3)." + ) + + def get_executable_name(name: str) -> str: if _is_windows(): return name + ".exe" @@ -510,6 +541,49 @@ def inplace_dir(self, installer: "InstallerBuildExt") -> Path: return Path(package_dir) +class BuiltSharedLib(BuiltFile): + """Installs a SONAME-versioned shared library. + + A normal ``BuiltFile`` copies one file. For a shared library like + ``libexecutorch.so.1.4.0`` the wheel ships only the loader-visible SONAME + file (e.g. ``libexecutorch.so.1``), which is what dependents record via + DT_NEEDED. pip does not preserve symlinks, so a dev-name link + (``libexecutorch.so``) would become a second full copy; consumers link + through the CMake package's imported targets (full path), so it is omitted. + """ + + def __init__( + self, + src_dir: str, + src_name: str, + dst: str, + dependent_cmake_flags: Optional[List[str]] = None, + ): + # src_name is the base library name (e.g. "executorch"); the real file + # is libexecutorch.so., resolved by glob in src_path(). + super().__init__( + src_dir=src_dir, + src_name=f"lib{src_name}.so.*", + dst=dst, + dependent_cmake_flags=dependent_cmake_flags or [], + ) + + def src_path(self, installer: "InstallerBuildExt") -> Path: + # The glob matches the versioned real file and any symlinks; pick the + # regular file (the real library), not the symlinks. + build_dir = self._get_build_dir(installer) + pattern = self.src.replace("%CMAKE_CACHE_DIR%/", "") + matches = [ + p for p in build_dir.glob(pattern) if p.is_file() and not p.is_symlink() + ] + if len(matches) != 1: + raise ValueError( + f"Expecting exactly 1 real shared library matching {self.src} " + f"in {build_dir}, found {matches}." + ) + return matches[0] + + class BuiltExtension(_BaseExtension): """An extension that installs a python extension that was built by cmake.""" @@ -665,13 +739,37 @@ def build_extension(self, ext: _BaseExtension) -> None: # Copy the file. self.copy_file(os.fspath(src_file), os.fspath(dst_file)) - # Ensure that the destination file is writable, even if the source was - # not. build_py does this by passing preserve_mode=False to copy_file, - # but that would clobber the X bit on any executables. TODO(dbort): This - # probably won't work on Windows. - if not os.access(src_file, os.W_OK): - # Make the file writable. This should respect the umask. - os.chmod(src_file, os.stat(src_file).st_mode | 0o222) + # For a SONAME-versioned shared library, ship exactly one file: the + # SONAME (e.g. libexecutorch.so.1), which the loader resolves via + # DT_NEEDED. pip does not preserve symlinks (it materializes every wheel + # entry as a regular file), so a dev-name symlink would become a second + # full copy and double the shipped size. Consumers link through the + # CMake package's imported targets, which reference the SONAME by full + # path, so the dev name is not needed. + if isinstance(ext, BuiltSharedLib): + self._normalize_shared_lib(src_file, dst_file) + + def _normalize_shared_lib(self, src_file: Path, dst_file: Path) -> None: + lib_dir = dst_file.parent + # SONAME (e.g. libexecutorch.so.1) read from the built library. Required, + # not best-effort: it is what the loader resolves at runtime. + # _read_soname raises if it cannot be determined. + soname = _read_soname(src_file) + soname_path = lib_dir / soname + # Make the SONAME file the real library. dst_file is the fully versioned + # name (e.g. libexecutorch.so.1.4.0) that copy_file just wrote; rename it + # to the SONAME so we do not keep a third redundant copy. + if dst_file.name != soname: + if soname_path.exists() or soname_path.is_symlink(): + soname_path.unlink() + os.replace(os.fspath(dst_file), os.fspath(soname_path)) + + # Ensure the shipped library is writable (the CMake output may be + # read-only), so a later rebuild/repack can overwrite it. Mirrors the + # writability fix build_py applies via copy_file(preserve_mode=False), + # but scoped to this file so it does not clobber X bits elsewhere. + if not os.access(soname_path, os.W_OK): + os.chmod(soname_path, os.stat(soname_path).st_mode | 0o222) class CustomBuildPy(build_py): @@ -702,6 +800,60 @@ def analyze_manifest(self): if os.path.isfile(os.path.join(_root, _f)) ] + def _is_cuda_wheel_build(self) -> bool: + # Read EXECUTORCH_BUILD_CUDA from the CMake cache the build command + # produced (it runs before build_py copies files). Gates CUDA-only + # headers so they ship only in a CUDA wheel. + try: + build_temp = self.get_finalized_command("build").build_temp + cache_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + build_temp, + "cmake-out", + "CMakeCache.txt", + ) + if not os.path.exists(cache_path): + return False + return CMakeCache(cache_path=cache_path).is_enabled("EXECUTORCH_BUILD_CUDA") + except Exception: + return False + + def _sdk_headers(self) -> List[str]: + # Delegate-only C++ SDK headers: the Program/Module/Tensor/DataLoader/ + # .ptd APIs a standalone C++ runner needs to link the prebuilt runtime + # without an ExecuTorch source tree. Listed explicitly (not rglob) so we + # advertise only APIs whose implementation archive is shipped in + # executorch/lib/. Excluded on purpose: bundled_module.h, + # flat_tensor/serialize/serialize.h, file_descriptor_data_loader.h, and + # cpu_caching_malloc_allocator.h (their impls are not shipped). + # flat_tensor_header.h IS shipped: it is the .ptd reader. Linux only. + if sys.platform != "linux": + return [] + headers = [ + "extension/module/module.h", + "extension/data_loader/buffer_data_loader.h", + "extension/data_loader/file_data_loader.h", + "extension/data_loader/mmap_data_loader.h", + "extension/data_loader/mman.h", + "extension/data_loader/mman_windows.h", + "extension/data_loader/shared_ptr_data_loader.h", + "extension/flat_tensor/flat_tensor_data_map.h", + "extension/flat_tensor/serialize/flat_tensor_header.h", + "extension/named_data_map/merged_data_map.h", + "extension/memory_allocator/malloc_memory_allocator.h", + "extension/memory_allocator/memory_allocator_utils.h", + ] + # CUDA caller-stream headers ship only in a CUDA wheel, alongside the + # matching libextension_cuda.so and the executorch::extension_cuda / + # executorch::cuda_backend targets, so a CPU wheel does not advertise + # headers whose library and targets it does not contain. + if self._is_cuda_wheel_build(): + headers += [ + "extension/cuda/caller_stream.h", + "extension/cuda/export.h", + ] + return headers + def run(self): # Copy python files to the output directory. This set of files is # defined by the py_module list and package_data patterns. @@ -767,6 +919,8 @@ def run(self): src_to_dst.append( (str(src), os.path.join("include/executorch", str(src))) ) + for src in self._sdk_headers(): + src_to_dst.append((src, os.path.join("include/executorch", src))) for src, dst in src_to_dst: dst = os.path.join(dst_root, dst) @@ -945,6 +1099,22 @@ def run(self): # noqa C901 ): cmake_configuration_args += ["-DEXECUTORCH_BUILD_OPENVINO=ON"] + # Build the consolidated shared runtime libexecutorch.so so the wheel can + # ship a linkable C++ SDK (see the executorch_shared build target and the + # packaging step). Gate on an actual wheel build: EXECUTORCH_BUILD_SHARED + # also forces global PIC and changes link behavior, so it must not leak + # into editable/develop builds (pip install -e ., setup.py develop) that + # other CI (unittest, arm, etc.) rely on. A non-editable `pip install .` + # does go through bdist_wheel (PEP 517) and correctly gets the SDK. Linux + # only; the SDK is not shipped on Windows/macOS. + building_wheel = "bdist_wheel" in self.distribution.commands + if not minimal_build and sys.platform == "linux" and building_wheel: + cmake_configuration_args += ["-DEXECUTORCH_BUILD_SHARED=ON"] + # Ship optimized CPU kernels (with portable fallback for uncovered + # ops) in the standalone kernels .so, so the SDK can run non- or + # partially-delegated models. Mirrors iOS kernels_optimized. + cmake_configuration_args += ["-DEXECUTORCH_BUILD_KERNELS_OPTIMIZED=ON"] + with Buck2EnvironmentFixer(): # Generate the cmake cache from scratch to ensure that the cache state # is predictable. @@ -999,6 +1169,35 @@ def run(self): # noqa C901 # list explicitly rather than relying on each flag being OFF. cmake_build_args += ["--target", "flatbuffers_ep"] else: + # Delegate-only C++ SDK: ship the consolidated shared runtime + # libexecutorch.so (the executorch_shared target), which bundles the + # runtime core plus the common extensions (module, tensor, + # data_loader, flat_tensor, named_data_map). Shared is required so a + # separately distributed backend/delegate .so can register into the + # one process-global registry inside libexecutorch.so. Built only for + # a wheel build (guarded by the EXECUTORCH_BUILD_SHARED cache entry, + # which is set just above only for bdist_wheel), so editable/develop + # builds are unaffected and the target always exists when requested. + if sys.platform == "linux" and cmake_cache.is_enabled( + "EXECUTORCH_BUILD_SHARED" + ): + cmake_build_args += ["--target", "executorch_shared"] + # Standalone CPU kernels library for the C++ SDK, shipped + # separately from libexecutorch.so so a fully delegated model + # links only the runtime while a partial model links this too. + cmake_build_args += [ + "--target", + "executorch_kernels_shared", + ] + # Standalone threadpool library: get_threadpool() is a + # process-wide singleton, so it ships in one .so (extension_ + # threadpool built SHARED) that kernels and future delegates link + # dynamically to share one pool. + cmake_build_args += [ + "--target", + "extension_threadpool", + ] + if cmake_cache.is_enabled("EXECUTORCH_BUILD_PYBIND"): cmake_build_args += ["--target", "portable_lib"] cmake_build_args += ["--target", "data_loader"] @@ -1016,6 +1215,11 @@ def run(self): # noqa C901 if cmake_cache.is_enabled("EXECUTORCH_BUILD_CUDA"): cmake_build_args += ["--target", "aoti_cuda_backend"] cmake_build_args += ["--target", "aoti_common_shims_slim"] + # Loadable CUDA delegate + caller-stream shared libs for the C++ + # SDK, built only for a wheel that also builds the shared runtime. + if cmake_cache.is_enabled("EXECUTORCH_BUILD_SHARED"): + cmake_build_args += ["--target", "executorch_cuda_backend"] + cmake_build_args += ["--target", "extension_cuda"] if cmake_cache.is_enabled("EXECUTORCH_BUILD_EXTENSION_MODULE"): cmake_build_args += ["--target", "extension_module"] @@ -1188,6 +1392,98 @@ def run(self): # noqa C901 modpath="executorch.backends.qualcomm.python.PyQnnManagerAdaptor", dependent_cmake_flags=["EXECUTORCH_BUILD_QNN"], ), + # Delegate-only C++ SDK: the consolidated shared runtime + # libexecutorch.so (core + module/tensor/data_loader/flat_tensor/ + # named_data_map), plus its SONAME symlink chain. Shipping it + # shared (not static archives) lets a separately distributed + # backend/delegate .so register into the one process-global + # registry inside libexecutorch.so, which is what coalesced + # multi-backend .pte execution requires. Paired with + # executorch-config.cmake (executorch::runtime target) and + # executorch.utils.cmake_prefix_path. Linux only: the .so naming + # and symlink chain are Unix specific, so Windows/macOS wheels are + # unchanged. + *( + [ + BuiltSharedLib( + src_dir="%CMAKE_CACHE_DIR%/", + src_name="executorch", + dst="executorch/lib/", + dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"], + ) + ] + if sys.platform == "linux" + else [] + ), + # Standalone CPU kernels library, shipped SEPARATELY from + # libexecutorch.so so a fully delegated model pays no kernel + # weight while a partial model links executorch::kernels. + *( + [ + BuiltSharedLib( + src_dir="%CMAKE_CACHE_DIR%/", + src_name="executorch_kernels", + dst="executorch/lib/", + dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"], + ) + ] + if sys.platform == "linux" + else [] + ), + # Standalone threadpool library: one process-wide get_threadpool + # singleton shared by the kernels lib (and future delegates), so + # they do not each embed a separate pool. + *( + [ + BuiltSharedLib( + src_dir="%CMAKE_CACHE_DIR%/extension/threadpool/", + src_name="executorch_threadpool", + dst="executorch/lib/", + dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"], + ) + ] + if sys.platform == "linux" + else [] + ), + # CUDA delegate binaries for the C++ SDK, shipped only in a + # CUDA-enabled wheel (PyTorch-style: the default wheel has the core + # runtime, the CUDA-index wheel adds these). Co-located with + # libexecutorch.so in executorch/lib/ so a C++ consumer, or a + # coalesced TensorRT+CUDA .pte, can load and register the CUDA + # delegate. executorch_cuda_backend whole-archives the backend so + # its "CudaBackend" registration runs on load; extension_cuda + # carries the single process-wide caller-stream TLS. Both are + # unversioned .so, so a plain dynamic-lib copy is enough. Gated on + # BUILD_SHARED as well as BUILD_CUDA: executorch_cuda_backend links + # the shared runtime and is only defined/built when SHARED is on, + # so packaging must require the same to avoid referencing an + # unbuilt artifact. + *( + [ + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/backends/cuda/", + src_name="executorch_cuda_backend", + dst="executorch/lib/", + is_dynamic_lib=True, + dependent_cmake_flags=[ + "EXECUTORCH_BUILD_CUDA", + "EXECUTORCH_BUILD_SHARED", + ], + ), + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/extension/cuda/", + src_name="extension_cuda", + dst="executorch/lib/", + is_dynamic_lib=True, + dependent_cmake_flags=[ + "EXECUTORCH_BUILD_CUDA", + "EXECUTORCH_BUILD_SHARED", + ], + ), + ] + if sys.platform == "linux" + else [] + ), ] ), ], diff --git a/src/executorch/utils/__init__.py b/src/executorch/utils/__init__.py new file mode 100644 index 00000000000..3d5919dcc5c --- /dev/null +++ b/src/executorch/utils/__init__.py @@ -0,0 +1,23 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""Utilities for locating ExecuTorch's packaged assets. + +``cmake_prefix_path`` points at the directory that contains the installed +ExecuTorch CMake package config, so a C++ project can discover it with: + + cmake -DCMAKE_PREFIX_PATH="$(python -c 'import executorch.utils as u; print(u.cmake_prefix_path)')" +""" + +import os as _os + +# Mirror torch.utils.cmake_prefix_path: /share/cmake. This file +# lives at /utils/__init__.py, so go up one level. +cmake_prefix_path = _os.path.join( + _os.path.dirname(_os.path.dirname(__file__)), "share", "cmake" +) + +__all__ = ["cmake_prefix_path"] diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index 1d6096a2e96..fe216891b04 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -19,10 +19,27 @@ # EXECUTORCH_INCLUDE_DIRS -- The include directories for ExecuTorch # EXECUTORCH_LIBRARIES -- Libraries to link against # +# In addition to the legacy variables above, this config defines namespaced +# imported targets for the prebuilt delegate-only C++ SDK when the shared +# runtime library is shipped in the wheel (see the "C++ SDK targets" section +# below): +# +# executorch::runtime -- libexecutorch.so (runtime core + the +# bundled common extensions below) executorch::core -- alias of +# executorch::runtime executorch::extension_module -- alias of +# executorch::runtime executorch::extension_tensor -- alias of +# executorch::runtime executorch::extension_data_loader -- alias of +# executorch::runtime executorch::extension_flat_tensor -- alias of +# executorch::runtime executorch::extension_named_data_map -- alias of +# executorch::runtime +# cmake_minimum_required(VERSION 3.19) -# Find prebuilt _portable_lib..so. This file should be installed -# under /executorch/share/cmake +# --------------------------------------------------------------------------- +# Legacy: discover the CPython _portable_lib extension for custom-op authors. +# This keeps `find_package(executorch)` working for prebuilt custom-op +# extensions that link the Python runtime module, unchanged from before. +# --------------------------------------------------------------------------- # Find python if(DEFINED ENV{CONDA_DEFAULT_ENV} AND NOT $ENV{CONDA_DEFAULT_ENV} STREQUAL @@ -43,36 +60,287 @@ execute_process( OUTPUT_STRIP_TRAILING_WHITESPACE ) +set(EXECUTORCH_INCLUDE_DIRS + "${CMAKE_CURRENT_LIST_DIR}/../../include" + "${CMAKE_CURRENT_LIST_DIR}/../../include/executorch/runtime/core/portable_type/c10" +) +set(EXECUTORCH_LIBRARIES) +set(EXECUTORCH_FOUND OFF) + +# Only discover the portable Python module when we could read EXT_SUFFIX; +# probing with an empty suffix would match a wrong/generic file. A missing +# suffix is not fatal because a pure-C++ consumer (see the C++ SDK section +# below) does not need the Python extension at all. if(SYSCONFIG_RESULT EQUAL 0) message(STATUS "Sysconfig extension suffix: ${EXT_SUFFIX}") + find_library( + _portable_lib_LIBRARY + NAMES _portable_lib${EXT_SUFFIX} + PATHS "${CMAKE_CURRENT_LIST_DIR}/../../extension/pybindings/" + ) else() message( - FATAL_ERROR - "Failed to retrieve sysconfig config var EXT_SUFFIX: ${SYSCONFIG_ERROR}" + WARNING + "Failed to retrieve sysconfig config var EXT_SUFFIX: ${SYSCONFIG_ERROR}. " + "The _portable_lib Python runtime target will not be available; the C++ " + "SDK targets (executorch::*) are unaffected." ) endif() -find_library( - _portable_lib_LIBRARY - NAMES _portable_lib${EXT_SUFFIX} - PATHS "${CMAKE_CURRENT_LIST_DIR}/../../extension/pybindings/" -) - -set(EXECUTORCH_LIBRARIES) -set(EXECUTORCH_FOUND OFF) if(_portable_lib_LIBRARY) set(EXECUTORCH_FOUND ON) message( STATUS "ExecuTorch portable library is found at ${_portable_lib_LIBRARY}" ) list(APPEND EXECUTORCH_LIBRARIES _portable_lib) - add_library(_portable_lib STATIC IMPORTED) - set(EXECUTORCH_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/../../include) - # PyTorch requires C++20, so pybindings must be compiled with C++20. - set_target_properties( - _portable_lib - PROPERTIES IMPORTED_LOCATION "${_portable_lib_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" - CXX_STANDARD 20 + if(NOT TARGET _portable_lib) + add_library(_portable_lib STATIC IMPORTED) + # PyTorch requires C++20, so pybindings must be compiled with C++20. + set_target_properties( + _portable_lib + PROPERTIES IMPORTED_LOCATION "${_portable_lib_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + CXX_STANDARD 20 + ) + endif() +endif() + +# --------------------------------------------------------------------------- +# C++ SDK targets (delegate-only). Defined only when the prebuilt shared runtime +# library is present in the wheel (shipped alongside this config under +# ../../lib). This lets a C++ application link the ExecuTorch runtime and the +# common runtime extensions without an ExecuTorch source checkout: +# +# find_package(executorch REQUIRED) if(TARGET executorch::runtime) # only +# defined by the Linux C++ SDK wheel target_link_libraries(app PRIVATE +# executorch::runtime) endif() Installed (not build-tree) consumers set +# LD_LIBRARY_PATH to the wheel lib dir, matching PyTorch's TorchConfig.cmake. +# +# The set is intentionally libtorch-free and excludes CPU operator/kernel +# libraries; delegates (e.g. TensorRT, CUDA) supply their own compute. If your +# model needs portable CPU operators, link a kernel library in addition. +# --------------------------------------------------------------------------- + +get_filename_component( + _executorch_sdk_root "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE +) +set(_executorch_sdk_libdir "${_executorch_sdk_root}/lib") + +# EXECUTORCH_SDK_FOUND is separate from EXECUTORCH_FOUND on purpose: the legacy +# EXECUTORCH_FOUND / EXECUTORCH_LIBRARIES contract describes the _portable_lib +# Python runtime for custom-op authors. Overloading it here would let existing +# `if(EXECUTORCH_FOUND) link(${EXECUTORCH_LIBRARIES})` code enter its branch +# with an empty library list. C++ SDK consumers should check the imported target +# (e.g. `if(TARGET executorch::runtime)`) or EXECUTORCH_SDK_FOUND. +# +# The C++ SDK ships one shared library, libexecutorch.so, which bundles the +# runtime core plus the common runtime extensions (module, tensor, data_loader, +# flat_tensor, named_data_map). Shared (not static archives) is required so that +# a separately distributed backend/delegate shared library can register into the +# one process-global registry that lives in libexecutorch.so. A backend .so is +# built "coreless" (its register_backend reference is undefined and resolves +# against libexecutorch.so at load), then force-loaded so its static-init +# registration runs. +set(EXECUTORCH_SDK_FOUND OFF) +# Discover the versioned SONAME (libexecutorch.so.) rather than +# hardcoding a major, so this keeps working across ABI bumps. The wheel ships +# only the real SONAME file (no dev-name symlink), so find_library(NAMES +# executorch) alone would miss it. +file(GLOB _executorch_shared_candidates + "${_executorch_sdk_libdir}/libexecutorch.so.*" +) +if(_executorch_shared_candidates) + list(GET _executorch_shared_candidates 0 _executorch_shared_LIBRARY) +endif() +if(_executorch_shared_LIBRARY) + set(EXECUTORCH_SDK_FOUND ON) + if(NOT TARGET executorch::runtime) + add_library(executorch::runtime SHARED IMPORTED) + set_target_properties( + executorch::runtime + PROPERTIES IMPORTED_LOCATION "${_executorch_shared_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + INTERFACE_COMPILE_DEFINITIONS + "C10_USING_CUSTOM_GENERATED_MACROS" + ) + endif() + + # Convenience aliases. libexecutorch.so already contains the core and these + # extensions, so all names resolve to the one shared library. Provided so + # consumer CMake can name what it uses without depending on the bundling + # layout. + foreach(_alias core extension_module extension_tensor extension_data_loader + extension_flat_tensor extension_named_data_map + ) + if(NOT TARGET executorch::${_alias}) + add_library(executorch::${_alias} INTERFACE IMPORTED) + set_property( + TARGET executorch::${_alias} PROPERTY INTERFACE_LINK_LIBRARIES + executorch::runtime + ) + endif() + endforeach() + + # Optional standalone CPU kernels library. Shipped separately from + # libexecutorch.so so a fully delegated model links only executorch::runtime + # and carries no kernel weight; a model with non-delegated ops additionally + # links executorch::kernels for a full CPU operator set (optimized + portable + # fallback). Defined only when the kernels .so is present. Loaded purely for + # its op-registration static initializers, so force it to stay linked. + # Discover the versioned SONAME rather than hardcoding a major (see the + # runtime lookup above). + file(GLOB _executorch_threadpool_candidates + "${_executorch_sdk_libdir}/libexecutorch_threadpool.so.*" + ) + if(_executorch_threadpool_candidates) + list(GET _executorch_threadpool_candidates 0 _executorch_threadpool_LIBRARY) + endif() + if(_executorch_threadpool_LIBRARY AND NOT TARGET executorch::threadpool) + # One process-wide get_threadpool() singleton lives here; kernels (and + # future delegates) link it so they share a single pool. + add_library(executorch::threadpool SHARED IMPORTED) + set_target_properties( + executorch::threadpool + PROPERTIES IMPORTED_LOCATION "${_executorch_threadpool_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + ) + set_property( + TARGET executorch::threadpool + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES executorch::runtime + ) + endif() + + file(GLOB _executorch_kernels_candidates + "${_executorch_sdk_libdir}/libexecutorch_kernels.so.*" + ) + if(_executorch_kernels_candidates) + list(GET _executorch_kernels_candidates 0 _executorch_kernels_LIBRARY) + endif() + if(_executorch_kernels_LIBRARY AND NOT TARGET executorch::kernels) + add_library(executorch::kernels SHARED IMPORTED) + set_target_properties( + executorch::kernels + PROPERTIES IMPORTED_LOCATION "${_executorch_kernels_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + ) + set_property( + TARGET executorch::kernels + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES executorch::runtime + ) + # The kernels lib has a DT_NEEDED on the shared threadpool; expose it so a + # consumer's rpath/link resolves the one pool. + if(TARGET executorch::threadpool) + set_property( + TARGET executorch::kernels + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES executorch::threadpool + ) + endif() + if(NOT APPLE AND NOT WIN32) + set_property( + TARGET executorch::kernels + APPEND + PROPERTY + INTERFACE_LINK_OPTIONS + "SHELL:-Wl,--push-state,--no-as-needed,${_executorch_kernels_LIBRARY},--pop-state" + ) + endif() + endif() + + # CUDA delegate targets. Present only in a CUDA-enabled wheel, so each target + # is defined only when its shared library is shipped in executorch/lib/. These + # are real separate shared libraries (not aliases of libexecutorch.so): + # extension_cuda holds the one process-wide caller-stream TLS, and + # cuda_backend whole-archives the CUDA delegate so loading it registers + # "CudaBackend" into the runtime. + find_library( + _executorch_extension_cuda_LIBRARY + NAMES extension_cuda + PATHS "${_executorch_sdk_libdir}" + NO_DEFAULT_PATH + ) + if(_executorch_extension_cuda_LIBRARY AND NOT TARGET + executorch::extension_cuda + ) + # caller_stream.h includes and the real target links + # CUDA::cudart PUBLIC, so reproduce that usage requirement. Use a QUIET, + # non-REQUIRED lookup: a consumer that only wants executorch::runtime from a + # CUDA-built wheel must still be able to find_package(executorch) on a + # machine that has the CUDA runtime but no development toolkit. If the + # toolkit is absent we simply skip defining the optional CUDA targets rather + # than failing the whole package. + find_package(CUDAToolkit QUIET) + if(CUDAToolkit_FOUND) + add_library(executorch::extension_cuda SHARED IMPORTED) + set_target_properties( + executorch::extension_cuda + PROPERTIES IMPORTED_LOCATION "${_executorch_extension_cuda_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + INTERFACE_LINK_LIBRARIES CUDA::cudart + ) + else() + # The CUDA delegate libraries are shipped in this wheel, but no CUDA + # development toolkit was found, so the executorch::extension_cuda / + # executorch::cuda_backend targets are intentionally not defined. Warn so + # a consumer that expected them gets a clear reason instead of an opaque + # "unknown target executorch::cuda_backend" error later. + message( + WARNING + "ExecuTorch: the CUDA delegate libraries are present in this wheel " + "(${_executorch_extension_cuda_LIBRARY}), but find_package(CUDAToolkit) " + "failed, so the executorch::extension_cuda and executorch::cuda_backend " + "targets are NOT defined. Install the CUDA toolkit to use them." + ) + endif() + endif() + + find_library( + _executorch_cuda_backend_LIBRARY + NAMES executorch_cuda_backend + PATHS "${_executorch_sdk_libdir}" + NO_DEFAULT_PATH + ) + if(_executorch_cuda_backend_LIBRARY + AND TARGET executorch::extension_cuda + AND NOT TARGET executorch::cuda_backend ) + # Requires executorch::extension_cuda (only defined when the CUDA toolkit + # was found above), since the backend links it. If the toolkit is + # unavailable the optional CUDA targets are simply not defined. + add_library(executorch::cuda_backend SHARED IMPORTED) + set_target_properties( + executorch::cuda_backend + PROPERTIES IMPORTED_LOCATION "${_executorch_cuda_backend_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${EXECUTORCH_INCLUDE_DIRS}" + INTERFACE_COMPILE_FEATURES cxx_std_17 + ) + set_property( + TARGET executorch::cuda_backend + APPEND + PROPERTY INTERFACE_LINK_LIBRARIES executorch::runtime + executorch::extension_cuda + ) + # This library is loaded purely for its side effect: its static initializer + # registers "CudaBackend". A consumer references no symbol from it, so under + # -Wl,--as-needed (or -lexecutorch_cuda_backend) the linker would drop it + # from DT_NEEDED and the registration would never run. Force it to stay + # linked on ELF toolchains. Use --push-state/--pop-state so we restore the + # consumer's prior --as-needed/--no-as-needed state instead of hardcoding + # --as-needed for everything that follows this library on the link line. + if(NOT APPLE AND NOT WIN32) + set_property( + TARGET executorch::cuda_backend + APPEND + PROPERTY + INTERFACE_LINK_OPTIONS + "SHELL:-Wl,--push-state,--no-as-needed,${_executorch_cuda_backend_LIBRARY},--pop-state" + ) + endif() + endif() endif()