diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index c94b95286d0..f1073fe7a0c 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -115,25 +115,83 @@ if(TARGET qnn_executorch_backend) list(APPEND link_libraries qnn_executorch_backend) endif() +# EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED / +# EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED emit a backend as its own .so so a +# consumer can drop the ones it does not use. A split backend still links +# executorch_core statically, so it carries a second copy of the backend +# registry (registered_backends[] in runtime/backend/interface.cpp). +# Registration reaches the runtime's copy only through PLT interposition: the +# register_backend call in the backend .so is a JUMP_SLOT relocation, so the +# dynamic linker binds it to the first definition in the global lookup scope. +# Two things have to hold for that to be this library's copy: +# +# * libexecutorch_jni.so has to export register_backend. The default version +# script hides everything but JNI_OnLoad/Java_*, so jni/version_script.txt is +# swapped for jni/version_script_shared_backends.txt below whenever a backend +# is split out. +# * The consumer has to load libexecutorch_jni.so ahead of the backend .so in +# the same lookup scope. Naming both in the link line of the library that uses +# them, runtime first, does that: the runtime lands earlier in DT_NEEDED and +# wins the lookup. dlopen()ing a backend .so on its own does not, and the +# backend then registers into its own table while the runtime reports +# DelegateNotFound. +# +# The same interposition is what keeps a split XNNPACK on one thread pool: the +# pthreadpool symbols are exported from here too, so the backend .so binds to +# the pool extension_threadpool configures instead of a second, unconfigured +# copy of its own. if(TARGET xnnpack_backend) - executorch_target_link_options_shared_lib(xnnpack_backend) - list( - APPEND - link_libraries - xnnpack_backend - XNNPACK - pthreadpool - cpuinfo - xnnpack-microkernels-prod + set(_xnnpack_so_link_libs xnnpack_backend XNNPACK pthreadpool cpuinfo + xnnpack-microkernels-prod ) if(TARGET kleidiai) - list(APPEND link_libraries kleidiai) + list(APPEND _xnnpack_so_link_libs kleidiai) + endif() + + if(EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED) + # Whole-archive the backend so its register_backend static initializer + # fires, and take only the XNNPACK third-party libs with it. The kernel + # registration archives stay in libexecutorch_jni.so, so loading both + # libraries together does not register a kernel twice + # (Error::RegistrationAlreadyRegistered, 0x16). + add_library( + xnnpack_executorch_backend SHARED + ${EXECUTORCH_ROOT}/extension/android/jni/jni_noop.cpp + ) + executorch_target_link_options_shared_lib(xnnpack_backend) + target_link_options_gc_sections(xnnpack_executorch_backend) + target_link_libraries( + xnnpack_executorch_backend PRIVATE ${_xnnpack_so_link_libs} + executorch_core log + ) + # executorch_jni still needs cpuinfo headers / lib for thread count + # detection. + list(APPEND link_libraries cpuinfo) + else() + executorch_target_link_options_shared_lib(xnnpack_backend) + list(APPEND link_libraries ${_xnnpack_so_link_libs}) endif() endif() if(TARGET vulkan_backend) - executorch_target_link_options_shared_lib(vulkan_backend) - list(APPEND link_libraries vulkan_backend) + if(EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED) + # Same shape as the XNNPACK split above: whole-archive vulkan_backend and + # take only vulkan_schema with it, leaving the kernel registration archives + # in libexecutorch_jni.so. + add_library( + vulkan_executorch_backend SHARED + ${EXECUTORCH_ROOT}/extension/android/jni/jni_noop.cpp + ) + executorch_target_link_options_shared_lib(vulkan_backend) + target_link_options_gc_sections(vulkan_executorch_backend) + target_link_libraries( + vulkan_executorch_backend PRIVATE vulkan_backend vulkan_schema + executorch_core log + ) + else() + executorch_target_link_options_shared_lib(vulkan_backend) + list(APPEND link_libraries vulkan_backend) + endif() endif() if(EXECUTORCH_BUILD_KERNELS_LLM) @@ -242,9 +300,21 @@ target_include_directories( target_compile_options(executorch_jni PUBLIC ${_common_compile_options}) +if(EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED + OR EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED +) + set(_executorch_jni_version_script + "${CMAKE_CURRENT_SOURCE_DIR}/jni/version_script_shared_backends.txt" + ) +else() + set(_executorch_jni_version_script + "${CMAKE_CURRENT_SOURCE_DIR}/jni/version_script.txt" + ) +endif() + target_link_options( executorch_jni PRIVATE - "LINKER:--version-script,${CMAKE_CURRENT_SOURCE_DIR}/jni/version_script.txt" + "LINKER:--version-script,${_executorch_jni_version_script}" ) target_link_options_gc_sections(executorch_jni) diff --git a/extension/android/jni/jni_noop.cpp b/extension/android/jni/jni_noop.cpp new file mode 100644 index 00000000000..b5b7cf76388 --- /dev/null +++ b/extension/android/jni/jni_noop.cpp @@ -0,0 +1,11 @@ +/* + * 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. + */ + +// Empty translation unit so SHARED libraries that only need to whole-archive +// other static archives (e.g. libvulkan_executorch_backend.so) have at least +// one source file for CMake to invoke the linker on. diff --git a/extension/android/jni/version_script_shared_backends.txt b/extension/android/jni/version_script_shared_backends.txt new file mode 100644 index 00000000000..7deb0ca8006 --- /dev/null +++ b/extension/android/jni/version_script_shared_backends.txt @@ -0,0 +1,23 @@ +{ + global: + JNI_OnLoad; + Java_*; + # A backend built as its own .so (EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED / + # EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED) carries its own statically linked + # copy of executorch_core, and therefore its own backend registry. Its call + # to register_backend goes through the PLT, so exporting the registry entry + # points here lets the dynamic linker bind that call to this library's + # registry - the one the runtime actually reads from. + extern "C++" { + executorch::runtime*::register_backend*; + executorch::runtime*::get_backend_class*; + executorch::runtime*::get_backend_name*; + executorch::runtime*::get_num_registered_backends*; + }; + # Same reason, for the thread pool the split XNNPACK backend runs on: bind + # its pthreadpool calls to the pool extension_threadpool configures here + # rather than to a second, unconfigured copy inside the backend .so. + pthreadpool_*; + local: + *; +}; diff --git a/extension/llm/custom_ops/CMakeLists.txt b/extension/llm/custom_ops/CMakeLists.txt index 8a43a5ddf5c..4d3050dc919 100644 --- a/extension/llm/custom_ops/CMakeLists.txt +++ b/extension/llm/custom_ops/CMakeLists.txt @@ -67,7 +67,11 @@ endif() list(TRANSFORM _custom_ops__srcs PREPEND "${EXECUTORCH_ROOT}/") -if(NOT EXECUTORCH_BUILD_XNNPACK) +if(NOT EXECUTORCH_BUILD_XNNPACK OR EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED) + # custom_ops doesn't actually call into xnnpack_backend (no XNNPACK refs in + # its sources). The transitive PUBLIC link is what was previously dragging + # XNNCompiler/XNNExecutor into libexecutorch_jni.so via the WHOLE_ARCHIVE on + # custom_ops, which defeats EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED. Drop it. list(APPEND custom_ops_libs extension_threadpool) else() list(APPEND custom_ops_libs extension_threadpool xnnpack_backend) diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index 5363c64b87c..02e7e3b8403 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -33,6 +33,8 @@ build_android_native_library() { fi EXECUTORCH_BUILD_VULKAN="${EXECUTORCH_BUILD_VULKAN:-OFF}" + EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED="${EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED:-OFF}" + EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED="${EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED:-OFF}" XNNPACK_ENABLE_ARM_SME2="${XNNPACK_ENABLE_ARM_SME2:-ON}" cmake . -DCMAKE_INSTALL_PREFIX="${CMAKE_OUT}" \ @@ -52,6 +54,8 @@ build_android_native_library() { -DEXECUTORCH_BUILD_QNN="${EXECUTORCH_BUILD_QNN}" \ -DQNN_SDK_ROOT="${QNN_SDK_ROOT}" \ -DEXECUTORCH_BUILD_VULKAN="${EXECUTORCH_BUILD_VULKAN}" \ + -DEXECUTORCH_BUILD_VULKAN_BACKEND_SHARED="${EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED}" \ + -DEXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED="${EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED}" \ -DXNNPACK_ENABLE_ARM_SME2="${XNNPACK_ENABLE_ARM_SME2}" \ -DFLATCC_ALLOW_WERROR=OFF \ -DSUPPORT_REGEX_LOOKAHEAD=ON \ @@ -68,7 +72,18 @@ build_android_native_library() { # Copy artifacts to ABI specific directory local SO_STAGE_DIR="cmake-out-android-so/${ANDROID_ABI}" mkdir -p ${SO_STAGE_DIR} - cp "${CMAKE_OUT}"/extension/android/*.so "${SO_STAGE_DIR}/libexecutorch.so" + cp "${CMAKE_OUT}"/extension/android/libexecutorch_jni.so "${SO_STAGE_DIR}/libexecutorch.so" + + # Copy standalone Vulkan backend shared library if built. Used by React + # Native Executorch as an opt-in artifact when the app enables Vulkan. + if [ -f "${CMAKE_OUT}"/extension/android/libvulkan_executorch_backend.so ]; then + cp "${CMAKE_OUT}"/extension/android/libvulkan_executorch_backend.so "${SO_STAGE_DIR}/" + fi + + # Same for XNNPACK as a standalone shared library. + if [ -f "${CMAKE_OUT}"/extension/android/libxnnpack_executorch_backend.so ]; then + cp "${CMAKE_OUT}"/extension/android/libxnnpack_executorch_backend.so "${SO_STAGE_DIR}/" + fi # Copy QNN related so library if [ -n "$QNN_SDK_ROOT" ] && [ "$ANDROID_ABI" == "arm64-v8a" ]; then diff --git a/tools/cmake/preset/default.cmake b/tools/cmake/preset/default.cmake index ae5437ea443..979c9d0c953 100644 --- a/tools/cmake/preset/default.cmake +++ b/tools/cmake/preset/default.cmake @@ -171,6 +171,18 @@ define_overridable_option( define_overridable_option( EXECUTORCH_BUILD_VULKAN "Build the Vulkan backend" BOOL OFF ) +define_overridable_option( + EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED + "Build the XNNPACK backend as its own shared library instead of linking it into the Android JNI library" + BOOL + OFF +) +define_overridable_option( + EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED + "Build the Vulkan backend as its own shared library instead of linking it into the Android JNI library" + BOOL + OFF +) define_overridable_option( EXECUTORCH_BUILD_WEBGPU "Build the WebGPU backend" BOOL OFF ) @@ -357,6 +369,15 @@ check_required_options_on( IF_ON EXECUTORCH_BUILD_QNN REQUIRES EXECUTORCH_BUILD_EXTENSION_TENSOR ) +check_required_options_on( + IF_ON EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED REQUIRES + EXECUTORCH_BUILD_XNNPACK +) + +check_required_options_on( + IF_ON EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED REQUIRES EXECUTORCH_BUILD_VULKAN +) + check_required_options_on( IF_ON EXECUTORCH_ENABLE_BUNDLE_IO REQUIRES EXECUTORCH_BUILD_DEVTOOLS )