From 8167cb1bd90a79013ab5006ebc71d6be632a5d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82uszniak?= Date: Fri, 14 Aug 2026 19:39:51 +0200 Subject: [PATCH] Android: allow building Vulkan/XNNPACK backends as separate shared libraries The Android AAR links every enabled backend into libexecutorch_jni.so, so an app pays for all of them even when it uses one. Vulkan and XNNPACK are the largest contributors, and an app that ships only the CPU path still carries the GPU backend. Add EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED and EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED, both defaulting to OFF so existing builds are unchanged. When set, the backend is emitted as its own .so that whole-archives the corresponding static archive, and the JNI library no longer links it. Consumers then load only the backends they need. Notes: * jni_noop.cpp is an empty translation unit. A SHARED target whose only content is a whole-archived static library still needs one source file for CMake to invoke the linker. * custom_ops links xnnpack_backend directly, which would pull the backend back into the JNI library and defeat the split, so that link is dropped when the shared XNNPACK build is selected. * build_android_library.sh forwards both variables so they can be driven from the environment. --- extension/android/CMakeLists.txt | 59 ++++++++++++++++++++++--- extension/android/jni/jni_noop.cpp | 3 ++ extension/llm/custom_ops/CMakeLists.txt | 6 ++- scripts/build_android_library.sh | 17 ++++++- 4 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 extension/android/jni/jni_noop.cpp diff --git a/extension/android/CMakeLists.txt b/extension/android/CMakeLists.txt index c94b95286d0..3da6c8f3a64 100644 --- a/extension/android/CMakeLists.txt +++ b/extension/android/CMakeLists.txt @@ -116,10 +116,7 @@ if(TARGET qnn_executorch_backend) endif() if(TARGET xnnpack_backend) - executorch_target_link_options_shared_lib(xnnpack_backend) - list( - APPEND - link_libraries + set(_xnnpack_so_link_libs xnnpack_backend XNNPACK pthreadpool @@ -127,13 +124,61 @@ if(TARGET xnnpack_backend) 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) + # Same rationale as EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED below: ship + # xnnpack_backend as its own libxnnpack_executorch_backend.so so the + # consumer can opt out at install time. Link only the backend itself + # (whole-archived so the register_backend static initializer fires) and + # the XNNPACK third-party libs + executorch_core. Kernel registration + # archives stay in libexecutorch_jni.so so loading both shared libraries + # together does not trigger 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(xnnpack_executorch_backend PRIVATE -fopenmp -static-openmp) + target_link_options_gc_sections(xnnpack_executorch_backend) + target_link_libraries(xnnpack_executorch_backend + PRIVATE ${_xnnpack_so_link_libs} executorch_core log + ) + set_target_properties( + xnnpack_executorch_backend PROPERTIES LINK_FLAGS "-Wl,-rpath='$ORIGIN'" + ) + # 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) + # Build vulkan_backend as a standalone shared library that React Native + # Executorch can ship as an opt-in artifact (libvulkan_executorch_backend.so). + # The .so links only vulkan_backend (with --whole-archive so its + # register_backend static initializer fires) plus executorch_core/vulkan_schema. + # Kernel registration libs (optimized_native_cpu_ops_lib, custom_ops, etc.) + # stay in libexecutorch_jni.so to avoid duplicate Kernel registration when + # both .so are loaded together (Error::RegistrationAlreadyRegistered, 0x16). + 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 + ) + set_target_properties( + vulkan_executorch_backend PROPERTIES LINK_FLAGS "-Wl,-rpath='$ORIGIN'" + ) + else() + executorch_target_link_options_shared_lib(vulkan_backend) + list(APPEND link_libraries vulkan_backend) + endif() endif() if(EXECUTORCH_BUILD_KERNELS_LLM) diff --git a/extension/android/jni/jni_noop.cpp b/extension/android/jni/jni_noop.cpp new file mode 100644 index 00000000000..4084d021d89 --- /dev/null +++ b/extension/android/jni/jni_noop.cpp @@ -0,0 +1,3 @@ +// 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/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