diff --git a/.ci/scripts/wheel/test_cpp_sdk.py b/.ci/scripts/wheel/test_cpp_sdk.py index 310f6135df1..9f3e2914efc 100644 --- a/.ci/scripts/wheel/test_cpp_sdk.py +++ b/.ci/scripts/wheel/test_cpp_sdk.py @@ -56,6 +56,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.+)$") @@ -194,6 +200,11 @@ def test_single_kernel_registration() -> None: _assert_single_definer(_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" @@ -784,7 +795,7 @@ def test_no_absolute_runtime_paths() -> None: # Link every component this wheel offers, and report which ones those are so the test # can check the result. Guarded individually because the set depends on the wheel. -foreach(_component threadpool kernels_optimized kernels_quantized etdump) +foreach(_component threadpool kernels_optimized kernels_quantized etdump backend_xnnpack) if(TARGET executorch::${_component}) target_link_libraries(component_consumer PRIVATE executorch::${_component}) # Report the library file, not just the target name: the two differ, and the test @@ -953,6 +964,7 @@ def run_tests(work_dir: Path) -> None: test_no_absolute_runtime_paths() test_single_threadpool() test_single_kernel_registration() + test_single_xnnpack_delegate() test_cpp_consumer(work_dir) test_documented_example_compiles(work_dir) test_component_targets_link(work_dir) diff --git a/CMakeLists.txt b/CMakeLists.txt index c7752b2d507..a6c6207381b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1242,9 +1242,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) @@ -1299,14 +1305,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..1a800e0fde4 100644 --- a/backends/xnnpack/CMakeLists.txt +++ b/backends/xnnpack/CMakeLists.txt @@ -96,16 +96,54 @@ target_include_directories( $ ) -set(xnnpack_third_party pthreadpool extension_threadpool cpuinfo) +if(EXECUTORCH_BUILD_SHARED) + # extension_threadpool is a shared library here and already provides + # pthreadpool and cpuinfo. Naming the static archives as well would give this + # delegate its own second copy of both, so a process would end up with two + # thread pools rather than the one the shared library exists to provide. + set(xnnpack_third_party extension_threadpool) +else() + set(xnnpack_third_party pthreadpool extension_threadpool cpuinfo) +endif() 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_backend_xnnpack + 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/docs/source/using-executorch-cpp.md b/docs/source/using-executorch-cpp.md index d75c04e50d3..25d3043c579 100644 --- a/docs/source/using-executorch-cpp.md +++ b/docs/source/using-executorch-cpp.md @@ -108,7 +108,6 @@ each one is defined only when the installed wheel actually ships it: | --- | --- | | `executorch::runtime` | The core runtime. Always present. | | `executorch::threadpool` | The shared thread pool the kernels and backends use. | -| `executorch::kernels_optimized` | CPU operator kernels, for any operator not taken by a backend. | Each target already carries what it needs: the runtime dependency, the include directories, the runtime search paths, and the linker options that keep a diff --git a/extension/training/CMakeLists.txt b/extension/training/CMakeLists.txt index 20d4b1ae53f..89d5990f338 100644 --- a/extension/training/CMakeLists.txt +++ b/extension/training/CMakeLists.txt @@ -63,11 +63,17 @@ 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 _pybind_training_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 ask for a second copy of what that library already provides. + list(APPEND _pybind_training_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 _pybind_training_dep_libs xnnpack_backend XNNPACK + xnnpack-microkernels-prod + ) + endif() endif() pybind11_add_module( @@ -89,6 +95,17 @@ if(EXECUTORCH_BUILD_PYBIND) -fexceptions> ) target_link_libraries(_training_lib PRIVATE ${_pybind_training_dep_libs}) + + if(EXECUTORCH_BUILD_SHARED + AND EXECUTORCH_BUILD_XNNPACK + AND TARGET xnnpack_backend + ) + # The delegate registers itself from a static initializer, so nothing in + # this extension references a symbol from it and a normal link can drop it. + # Keeping it named on the link line is what makes an XNNPACK-delegated + # program usable from here. + executorch_target_retain_shared_library(_training_lib xnnpack_backend) + endif() executorch_target_link_shared_runtime(_training_lib) if(EXECUTORCH_BUILD_SHARED diff --git a/setup.py b/setup.py index 3f9ec8d02b6..c19e0244292 100644 --- a/setup.py +++ b/setup.py @@ -1374,6 +1374,23 @@ def run(self): # noqa C901 "EXECUTORCH_BUILD_KERNELS_QUANTIZED", ], ), + # 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_backend_xnnpack.so." + f"{get_runtime_soname_major()}.*" + ), + dst=( + "executorch/lib/libexecutorch_backend_xnnpack.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. diff --git a/tools/cmake/executorch-wheel-config.cmake b/tools/cmake/executorch-wheel-config.cmake index bec7658de28..e9a6796c952 100644 --- a/tools/cmake/executorch-wheel-config.cmake +++ b/tools/cmake/executorch-wheel-config.cmake @@ -44,9 +44,6 @@ # dependency and, for a registration-only library, the link options that keep it # from being dropped. The names, when present, are: # -# executorch::threadpool executorch::kernels_optimized -# executorch::xnnpack_backend executorch::cuda_backend -# # Check with if(TARGET executorch::) rather than assuming one exists. A # namespaced name that was never defined is a configure-time error that names # the component, so a consumer who links one unconditionally gets a clear @@ -399,6 +396,8 @@ if(TARGET executorch::threadpool) ) endif() +executorch_define_component(backend_xnnpack executorch_backend_xnnpack) + # Find prebuilt _portable_lib..so. This is the legacy contract used # to build custom-op extensions against the Python module, and is kept working # independently of the runtime target above.