Skip to content

Android: allow building Vulkan/XNNPACK backends as separate shared libraries - #21849

Open
msluszniak wants to merge 3 commits into
pytorch:mainfrom
msluszniak:ms/android-shared-backend-libs
Open

Android: allow building Vulkan/XNNPACK backends as separate shared libraries#21849
msluszniak wants to merge 3 commits into
pytorch:mainfrom
msluszniak:ms/android-shared-backend-libs

Conversation

@msluszniak

@msluszniak msluszniak commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

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 shipping only the CPU path still carries the
GPU backend.

This PR adds EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED and
EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED, both defaulting to OFF, so
existing builds are byte-for-byte 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 load only what they need.

Two details worth calling out for review:

  • extension/android/jni/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 on.
  • 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.

Test plan

  • Default build (both flags OFF) produces the same single libexecutorch.so as
    before.
  • With EXECUTORCH_BUILD_VULKAN=ON EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED=ON,
    the Vulkan backend builds as a separate .so and an app loading it runs
    Vulkan-delegated models on device (Galaxy S26 Ultra, Adreno).
  • This split is what react-native-executorch ships on Android today, so it has
    had real-world exercise across both backends.

cc @kirklandsign @cbilgin @SS-JIA @manuelcandales @digantdesai

Fixes #10457

…braries

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.
@pytorch-bot pytorch-bot Bot added the module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ label Aug 14, 2026
@pytorch-bot

pytorch-bot Bot commented Aug 14, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21849

Note: Links to docs will display an error until the docs builds have been completed.

⚠️ 21 Awaiting Approval

As of commit 69a5f51 with merge base 33f68d1 (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 14, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@nil-is-all nil-is-all added module: android Issues related to Android code, build, and execution and removed module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ labels Aug 18, 2026
@pytorch-bot pytorch-bot Bot added the module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/ label Aug 18, 2026
@nil-is-all

Copy link
Copy Markdown
Contributor

Thanks for the PR, @msluszniak. Once the rest of CI tests are done running, do address the lintrunner logs and other CI tests.

@msluszniak

Copy link
Copy Markdown
Contributor Author

Once the rest of CI tests are done running

My bad, sorry 😅 lint fixed

@kirklandsign

Copy link
Copy Markdown
Contributor

@claude review

@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

Claude finished @kirklandsign's task in 11m 56s —— View job


Code review

Nice, focused change — opt-in flags default OFF, so the default AAR is untouched. The mechanics (whole-archiving the backend static lib into a SHARED target seeded by an empty TU, dropping the transitive xnnpack_backend link from custom_ops) are reasonable. A few things worth resolving before merge, one of which is potentially load-bearing.


1. (Important) How does the backend registered in the standalone .so reach the runtime in libexecutorch_jni.so?

This is the part I'd most like the author/maintainers to confirm, because static analysis suggests the two libraries may end up with separate copies of the backend registry:

  • executorch_core is a static library and holds the registry state — registered_backends[], num_registered_backends, register_backend, get_backend_class (runtime/backend/interface.cpp:27-59).
  • Both libexecutorch_jni.so and libvulkan_executorch_backend.so link executorch_core statically, so each gets its own copy of that table.
  • executorch_jni is built with a version script that makes everything except JNI_OnLoad/Java_* local (extension/android/jni/version_script.txt + CMakeLists.txt:294-297). So the JNI lib does not export register_backend/get_backend_class.
  • The standalone backend .so has no version script, so it exports those symbols with default visibility.

The concern: the backend .so's static initializer calls register_backend(...), which (since the JNI lib doesn't export the symbol) binds to the backend .so's own copy of the table — while the runtime's get_backend_class(...) inside the JNI lib reads the JNI lib's copy. If that's what happens, the JNI runtime would never see the delegate → DelegateNotFound.

You report it runs Vulkan-delegated models on device, so empirically something ties them together (load order, Android's RTLD_GLOBAL interposition, etc.). Could you confirm the tables are actually shared — e.g. call executorch::runtime::get_num_registered_backends() from the JNI side after loading the backend .so and confirm the count includes the delegate? A one-line note on the load-order/linker contract that makes this work would also help future maintainers, since the version-script interaction is very non-obvious.

2. pthreadpool is duplicated across the two libraries

pthreadpool is built with -fvisibility=hidden (CMakeLists.txt:421). In the shared-XNNPACK path, libexecutorch_jni.so still pulls pthreadpool in via extension_threadpool, and libxnnpack_executorch_backend.so links its own copy. Hidden visibility means these are two independent thread pools. Thread-count configuration applied through the JNI/extension_threadpool side would then not affect the pool XNNPACK actually runs on. Is that intentional / does it match what RNExecuTorch ships? Worth a comment if so.

3. jni_noop.cpp is missing the BSD license header

extension/android/jni/jni_noop.cpp has only descriptive comments. Every other source file in this dir carries the Meta BSD header (see jni/log.cpp). LICENSELINT only greps for the "Confidential and proprietary" string so it won't flag this, but it's the repo convention. Fix this →

4. rpath='$ORIGIN' quoting and use of the legacy LINK_FLAGS property

set_target_properties(
  vulkan_executorch_backend PROPERTIES LINK_FLAGS "-Wl,-rpath='$ORIGIN'"
)

Two nits:

  • The single quotes are likely passed literally to the linker (CMake doesn't shell-expand LINK_FLAGS), producing an rpath of '$ORIGIN' rather than $ORIGIN. Prefer target_link_options(<tgt> PRIVATE "LINKER:-rpath,$ORIGIN") (or set BUILD_RPATH/INSTALL_RPATH). On Android this is also largely redundant — co-located jniLibs/<abi>/*.so are found by the app's native lib search path — so it may be droppable entirely.
  • Mixing the string-based LINK_FLAGS property with target_link_options(...) on the same target is inconsistent; consolidating on target_link_options matches the rest of this file.

5. -fopenmp -static-openmp only on the shared XNNPACK target

The single-lib path never adds these, so why does the split need OpenMP? XNNPACK normally threads via pthreadpool, not OpenMP. If this is working around an undefined omp_* symbol that used to be satisfied transitively, a one-line comment naming the dependency would help; otherwise it may be dead flags.

6. New options aren't declared in the preset system

EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED / EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED are only ever read (here and in extension/llm/custom_ops/CMakeLists.txt). Unlike EXECUTORCH_BUILD_XNNPACK/_VULKAN they're not registered via define_overridable_option(...) in tools/cmake/preset/default.cmake. Registering them (with REQUIRES EXECUTORCH_BUILD_XNNPACK/_VULKAN) would give them documentation, validation, and guard against silently doing nothing when the base backend flag is off.


Minor: the two rationale comment blocks (lines 127-133 and 161-168) are near-duplicates — fine to keep, just noting.

Overall the approach is sound and the default-OFF gating is safe. Item #1 is the only blocker in my view — please confirm the registry is genuinely shared at runtime.
· branch ms/android-shared-backend-libs

A split backend links executorch_core statically, so it holds a second
copy of the registry and only reaches the runtime's copy by binding its
register_backend PLT slot to the first definition in the global lookup
scope. version_script.txt makes that definition local in
libexecutorch_jni.so, so on the AAR path the backend would register into
its own table and the runtime would report DelegateNotFound. Use a
version script that exports the registry entry points (and pthreadpool,
so a split XNNPACK shares the configured pool) whenever a backend is
split out, and write the load-order contract down next to the option.

Also drop the dead -fopenmp/-static-openmp flags and the $ORIGIN rpath
(the .so has no non-system DT_NEEDED), register both options in the
preset system, give jni_noop.cpp the license header, and trim the
duplicated rationale comments.
@msluszniak

msluszniak commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author
  • Yeah, the first point was indeed thing that I need to fix. I wasn't a problem on our fork, but here it would be.
  • Regarding 3 and 6, applied as suggested.
  • Regarding 2: both libraries export pthreadpool, so the backend binds to the runtime's copy. The new version script exports pthreadpool explicitly.
  • In 4, I dropped rpath.
  • In 5, I dropped since there are no openmp symbols.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: android Issues related to Android code, build, and execution module: vulkan Issues related to the Vulkan delegate and code under backends/vulkan/

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Android] Make libraries hot pluggable

3 participants