From f932b9c7a5100db26691a4b00aaf39daeaf8e8f0 Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Fri, 31 Jul 2026 14:19:39 -0700 Subject: [PATCH 1/3] Update [ghstack-poisoned] --- .ci/scripts/wheel/test_cpp_sdk.py | 12 +++++++++++ CMakeLists.txt | 12 ++++++++--- backends/xnnpack/CMakeLists.txt | 34 +++++++++++++++++++++++++++++-- setup.py | 17 ++++++++++++++++ 4 files changed, 70 insertions(+), 5 deletions(-) diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index cae8715a343..0553777ad87 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -44,6 +44,12 @@ # the operators are registered twice, which aborts at startup. _KERNEL_SYMBOLS = ("torch::executor::native::abs_out",) +# A representative symbol from the XNNPACK delegate. A second definer means the +# process carries two copies of the delegate. +_XNNPACK_SYMBOLS = ( + "executorch::backends::xnnpack::XnnpackBackendOptions::workspace_manager", +) + # `nm -DC` prints " " for a definition and # " U " for an undefined reference. _DEFINED = re.compile(r"^[0-9a-fA-F]+\s+(?P[A-Za-z])\s+(?P.+)$") @@ -141,6 +147,11 @@ def test_single_kernel_registration() -> None: _assert_single_definer(_KERNEL_SYMBOLS, "set of CPU kernels") +def test_single_xnnpack_delegate() -> None: + """Exactly one shipped library may define the XNNPACK delegate.""" + _assert_single_definer(_XNNPACK_SYMBOLS, "XNNPACK delegate") + + 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" @@ -197,4 +208,5 @@ def run_tests(work_dir: Path) -> None: test_single_backend_registry() test_single_threadpool() test_single_kernel_registration() + test_single_xnnpack_delegate() test_cpp_consumer(work_dir) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33c466f7283..c7c65de0208 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1200,9 +1200,15 @@ if(EXECUTORCH_BUILD_PYBIND) endif() if(EXECUTORCH_BUILD_XNNPACK) - # need to explicitly specify XNNPACK and xnnpack-microkernels-prod here - # otherwise uses XNNPACK and microkernel-prod symbols from libtorch_cpu - list(APPEND _dep_libs xnnpack_backend XNNPACK xnnpack-microkernels-prod) + if(EXECUTORCH_BUILD_SHARED) + # The delegate bundles XNNPACK and its microkernels, so naming them again + # here would ship a second copy. + list(APPEND _dep_libs xnnpack_backend) + else() + # need to explicitly specify XNNPACK and xnnpack-microkernels-prod here + # otherwise uses XNNPACK and microkernel-prod symbols from libtorch_cpu + list(APPEND _dep_libs xnnpack_backend XNNPACK xnnpack-microkernels-prod) + endif() endif() if(EXECUTORCH_BUILD_VULKAN) diff --git a/backends/xnnpack/CMakeLists.txt b/backends/xnnpack/CMakeLists.txt index cd0d945a84f..b2adcd9ed0f 100644 --- a/backends/xnnpack/CMakeLists.txt +++ b/backends/xnnpack/CMakeLists.txt @@ -101,11 +101,41 @@ set(xnnpack_third_party pthreadpool extension_threadpool cpuinfo) include(cmake/Dependencies.cmake) list(TRANSFORM _xnnpack_backend__srcs PREPEND "${EXECUTORCH_ROOT}/") -add_library(xnnpack_backend ${_xnnpack_backend__srcs}) +# 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(_xnnpack_backend_library_type SHARED) +else() + set(_xnnpack_backend_library_type STATIC) +endif() +add_library( + xnnpack_backend ${_xnnpack_backend_library_type} ${_xnnpack_backend__srcs} +) target_link_libraries( - xnnpack_backend PUBLIC ${xnnpack_third_party} executorch_core xnnpack_schema + xnnpack_backend PUBLIC ${xnnpack_third_party} xnnpack_schema extension_threadpool ) +if(EXECUTORCH_BUILD_SHARED) + set_target_properties( + xnnpack_backend + PROPERTIES OUTPUT_NAME executorch_xnnpack_backend + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}" + ) + # XNNPACK and its microkernels are forced static, so bundle them inside this + # library instead of making every consumer supply them. + executorch_target_whole_archive(xnnpack_backend XNNPACK) + executorch_target_whole_archive(xnnpack_backend xnnpack-microkernels-prod) + target_link_libraries(xnnpack_backend PUBLIC executorch_shared) + if(NOT APPLE) + # Ships beside the runtime in the wheel's lib/ directory. + set_target_properties( + xnnpack_backend PROPERTIES BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN" + ) + endif() +else() + target_link_libraries(xnnpack_backend PUBLIC executorch_core) +endif() target_include_directories( xnnpack_backend PUBLIC ${_common_include_directories} ) diff --git a/setup.py b/setup.py index 79ec60bdf55..f55b37c38ed 100644 --- a/setup.py +++ b/setup.py @@ -1143,6 +1143,23 @@ def run(self): # noqa C901 ), dependent_cmake_flags=["EXECUTORCH_BUILD_SHARED"], ), + # Install the XNNPACK delegate beside them, so a process has one + # copy of it instead of one per component that uses it. + BuiltFile( + src_dir="%CMAKE_CACHE_DIR%/backends/xnnpack/", + src_name=( + "libexecutorch_xnnpack_backend.so." + f"{get_runtime_soname_major()}.*" + ), + dst=( + "executorch/lib/libexecutorch_xnnpack_backend.so." + f"{get_runtime_soname_major()}" + ), + dependent_cmake_flags=[ + "EXECUTORCH_BUILD_SHARED", + "EXECUTORCH_BUILD_XNNPACK", + ], + ), # 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. From a231092b98dbebcaeec6989e6c9ff07accac1cbe Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Fri, 31 Jul 2026 16:22:36 -0700 Subject: [PATCH 2/3] Update [ghstack-poisoned] --- CMakeLists.txt | 8 ++++++++ tools/cmake/Utils.cmake | 32 +++++++++++++++++++------------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c7c65de0208..e6cf275936f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1263,6 +1263,14 @@ if(EXECUTORCH_BUILD_PYBIND) target_compile_options(portable_lib PUBLIC ${_pybind_compile_options}) target_link_libraries(portable_lib PRIVATE ${_dep_libs}) executorch_target_link_shared_runtime(portable_lib) + # The delegates register themselves from a static initializer, so nothing here + # references their symbols and some linkers drop them from DT_NEEDED, leaving + # the backend unregistered at runtime. + foreach(_retained_backend xnnpack_backend aoti_cuda_backend) + if(TARGET ${_retained_backend}) + executorch_target_retain_shared_library(portable_lib ${_retained_backend}) + endif() + endforeach() # Set RPATH to find PyTorch and backend libraries relative to the installation # location. This goes from executorch/extension/pybindings up to diff --git a/tools/cmake/Utils.cmake b/tools/cmake/Utils.cmake index 8d14ec0770b..97966ab9cb5 100644 --- a/tools/cmake/Utils.cmake +++ b/tools/cmake/Utils.cmake @@ -247,29 +247,35 @@ endfunction() # the static archive further along the line would then supply the registry after # all. Other linkers keep the reference without it. function(executorch_target_link_shared_runtime target_name) + executorch_target_retain_shared_library(${target_name} executorch_shared) +endfunction() + +# Put a shared library on a consumer's link line and keep it there. +# +# A library whose only purpose is to run a static initializer, such as a backend +# that registers itself, has no symbol the consumer references directly, so the +# linker is free to drop it from DT_NEEDED. Some linkers do exactly that, and +# the initializer then never runs. This forces the reference to be recorded. +function(executorch_target_retain_shared_library target_name library_target) if(NOT EXECUTORCH_BUILD_SHARED) return() endif() if(APPLE OR MSVC) - set(_runtime_flags "SHELL:$") + set(_retain_flags "SHELL:$") else() - # --no-as-needed keeps the runtime in DT_NEEDED even though no symbol has - # been referenced yet at this point on the link line. It is wrapped in - # push-state/pop-state rather than closed with an explicit --as-needed so - # that whatever policy was in effect before is restored: closing with - # --as-needed would leave that in force for everything that follows, and - # would drop shared backends whose only purpose is static-init registration. - set(_runtime_flags - "SHELL:LINKER:--push-state,--no-as-needed $ LINKER:--pop-state" + # push-state/pop-state rather than closing with an explicit --as-needed: + # that would leave --as-needed in force for everything after it on the line + # and drop the next library that only exists for static-init registration. + set(_retain_flags + "SHELL:LINKER:--push-state,--no-as-needed $ LINKER:--pop-state" ) endif() - # The generator expression alone does not make the runtime get built first, so - # state the build-order dependency explicitly. - add_dependencies(${target_name} executorch_shared) + # The generator expression alone does not order the build, so say it outright. + add_dependencies(${target_name} ${library_target}) set_property( TARGET ${target_name} APPEND - PROPERTY LINK_OPTIONS "${_runtime_flags}" + PROPERTY LINK_OPTIONS "${_retain_flags}" ) endfunction() From 20af43e5fc20d4841ca0b2eff84ed210c163319a Mon Sep 17 00:00:00 2001 From: shoumikhin Date: Fri, 31 Jul 2026 22:37:45 -0700 Subject: [PATCH 3/3] Update [ghstack-poisoned] --- CMakeLists.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 973955dc56f..dff7383dc9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1267,9 +1267,7 @@ if(EXECUTORCH_BUILD_PYBIND) # initializer, so nothing here references a symbol from them and some linkers # drop them from DT_NEEDED. That surfaces at runtime as a missing kernel or an # unregistered backend rather than as a link error. - foreach(_retained_component optimized_native_cpu_ops_lib xnnpack_backend - aoti_cuda_backend - ) + foreach(_retained_component optimized_native_cpu_ops_lib xnnpack_backend) if(TARGET ${_retained_component}) executorch_target_retain_shared_library( portable_lib ${_retained_component}