diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 7905873fca9..7f72167b044 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -53,6 +53,12 @@ "executorch::runtime::get_registered_kernels", ) +# 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.+)$") @@ -183,6 +189,11 @@ def test_single_kernel_registration() -> None: _report_definers(_KERNEL_REGISTRY_SYMBOLS, "operator registry") +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" @@ -284,4 +295,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 7dcfef1ffa8..1816ba41d9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1206,9 +1206,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) @@ -1263,14 +1269,17 @@ 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 operators register themselves from a static initializer, so nothing here - # references a symbol from the kernels library and some linkers drop it, which - # surfaces at runtime as a missing kernel rather than a link error. - if(TARGET optimized_native_cpu_ops_lib) - executorch_target_retain_shared_library( - portable_lib optimized_native_cpu_ops_lib - ) - endif() + # These libraries register their operators or their backend from a static + # 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) + if(TARGET ${_retained_component}) + executorch_target_retain_shared_library( + portable_lib ${_retained_component} + ) + 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/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 4a862be1d7d..cf08272eb7c 100644 --- a/setup.py +++ b/setup.py @@ -1157,6 +1157,23 @@ def run(self): # noqa C901 "EXECUTORCH_BUILD_KERNELS_OPTIMIZED", ], ), + # 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.