Skip to content

Android: export curated EXECUTORCH_1_0 ABI from libexecutorch.so (hot-pluggable backends, milestone 1) - #21945

Open
RanjithRagavan wants to merge 10 commits into
pytorch:mainfrom
RanjithRagavan:android-split-backend-abi
Open

RanjithRagavan wants to merge 10 commits into
pytorch:mainfrom
RanjithRagavan:android-split-backend-abi

Conversation

@RanjithRagavan

@RanjithRagavan RanjithRagavan commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

First step of the hot-pluggable backends plan (#10457), which lets Android apps opt out of unused backends to shrink binary size (#19329).

Today the Android AAR ships a single libexecutorch.so that links every backend via --whole-archive, and its linker version script exports only JNI_OnLoad and Java_*. A backend built as a standalone .so therefore can't reach the process-wide backend/kernel registries — its self-registration silently writes into a private copy, and delegation fails. This is why external attempts at per-backend .so files hit "duplicate kernel" errors.

The fix: add a versioned EXECUTORCH_1_0 node to extension/android/jni/version_script.txt that exports the curated surface an external .so needs to self-register at load time through normal ELF symbol interposition — the same mechanism the QNN backend already relies on:

  • Registration entry points — register_backend, get_backend_class, register_kernels
  • RTTI/vtables for polymorphic public types such as BackendInterface (its key function is emitted in the core .so, so subclasses in external .so files need these symbols)
  • The public executorch::runtime / executorch::aten API namespaces backend implementations are written against (Tensor, EValue, MemoryManager, FreeableBuffer, logging)
  • The et_pal_* C API so backend .so files share the process-wide logging/allocation hooks

The boundary is deliberately namespace-scoped rather than symbol-by-symbol so it stays maintainable as the public headers evolve; everything else stays hidden for LTO/--gc-sections. Additive symbols go into a new version node so external .so files get a stable ABI tag — answering the issue's open question in favor of a curated, versioned list.

What this does NOT change: the monolithic .so topology is untouched — no target, link, or loading-behavior change for existing consumers. Split build mode (milestone 2) and the first carved-out backend (milestone 3) build on top of this.

Test plan

  • Self-verifying build: scripts/build_android_library.sh now asserts — via the NDK's llvm-nm — that the built libexecutorch.so actually exports the required symbols (registration functions, BackendInterface RTTI/vtable, etensor::Tensor, EValue, et_pal_emit_log_message, JNI_OnLoad). If the ABI surface ever regresses, the Android AAR build fails loudly instead of shipping a silently broken artifact.
  • No-regression: existing Android CI exercises the monolithic path end to end (unit tests, instrumentation tests, demo app builds); exporting additional symbols does not alter code generation, only the dynamic symbol table.
  • This unblocks the milestone-1 acceptance criterion: a standalone backend .so can now resolve the core registry symbols at dlopen time; milestone 3's Vulkan pilot will demonstrate the end-to-end flow.

Refs #10457 (milestone 1 of 7), #19329

cc @kirklandsign @cbilgin

…-pluggable backends, milestone 1)

First step of the hot-pluggable backends plan (pytorch#10457) to let Android
apps opt out of unused backends (pytorch#19329).

Today libexecutorch.so exports only JNI_OnLoad and Java_*. A backend
built as a standalone .so cannot reach the process-wide backend/kernel
registries, so its static registration writes into a private copy and
delegation fails with 'backend not found' / duplicate-kernel errors.

This change adds a versioned EXECUTORCH_1_0 node to
extension/android/jni/version_script.txt exporting the curated surface
an external .so needs to self-register at load time via ELF symbol
interposition:

- registration entry points: register_backend, get_backend_class,
  register_kernels
- RTTI/vtables for polymorphic public types such as BackendInterface
  (its key function is emitted in the core .so)
- the public executorch::runtime / executorch::aten API namespaces that
  backend implementations are written against (Tensor, EValue,
  MemoryManager, FreeableBuffer, logging)
- the et_pal_* C API so backend .so files share process-wide
  logging/allocation hooks

The boundary is namespace-scoped instead of symbol-by-symbol so it
stays maintainable as public headers evolve; everything else stays
hidden for LTO/gc-sections. Future additive symbols go into a new
version node so external .so files can depend on a stable ABI tag.

The monolithic .so topology is unchanged: no target, link, or loading
behavior changes for existing consumers.

Verification: build_android_library.sh now asserts the built
libexecutorch.so actually exports the required symbols (via NDK
llvm-nm), failing the build if the ABI surface regresses. Existing
Android CI exercises the monolithic path end to end.
@pytorch-bot

pytorch-bot Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 1 New Failure, 1 Unrelated Failure

As of commit 2b51364 with merge base fb60708 (image):

NEW FAILURE - The following job has failed:

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

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 19, 2026
@RanjithRagavan

Copy link
Copy Markdown
Contributor Author

@pytorchbot label "module: android"

@RanjithRagavan

Copy link
Copy Markdown
Contributor Author

@pytorchbot label "release notes: none"

@pytorch-bot pytorch-bot Bot added module: android Issues related to Android code, build, and execution release notes: none Do not include this in the release notes labels Aug 19, 2026
@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 4m 49s —— View job


Code Review: EXECUTORCH_1_0 ABI export

Reviewed the version script, the new check_exported_symbols.sh, and the build wiring. The mechanism is sound — a versioned node exporting the curated surface via ELF interposition is the right primitive for hot-pluggable backends, and gating the AAR build on an llvm-nm assertion is a nice self-verifying touch. A few substantive points below.


1. The executorch::aten::* export lines are dead — they match no symbols

executorch::aten is composed entirely of using-aliases (exec_aten.h:73-197): using Tensor = torch::executor::Tensor;, etc. No symbol is ever emitted into that namespace — the real portable types live in executorch::runtime::etensor (already covered by executorch::runtime::*). The PR's own check_exported_symbols.sh:37-40 even acknowledges this and deliberately checks executorch::runtime::etensor::Tensor instead.

So these four lines in version_script.txt (39/44/46 counterparts) never bind anything:

"executorch::aten::*";
"typeinfo for executorch::aten::*";
"vtable for executorch::aten::*";

Not harmful, but they — and the comment block at lines 16-18 describing the executorch::aten::* surface — are misleading about what's actually exported. Recommend dropping them (or replacing with executorch::runtime::etensor::* if you want to be explicit about the data model) so the script documents reality.

2. Broad executorch::runtime::* export works against the size goal it serves

Exported (global) symbols in a version script become GC roots: --gc-sections (Utils.cmake:143) and LTO can no longer strip or internalize anything reachable from executorch::runtime::*. The PR body states this "does not alter code generation, only the dynamic symbol table" — that isn't quite accurate. Making the whole runtime namespace externally visible defeats internal dead-stripping/inlining for those symbols and inflates .dynsym/.dynstr.

Given the motivating issue (#19329) is binary size reduction, it would be worth quantifying the libexecutorch.so size delta from this change (before/after ls -l + size). If it's material, a more surgical export list would keep milestone 1 from regressing the very metric milestones 2-3 aim to improve. Worth at least a sentence in the PR acknowledging the tradeoff.

3. NM_BIN glob is fragile (build_android_library.sh:76)

NM_BIN=$(echo "${ANDROID_NDK}"/toolchains/llvm/prebuilt/*/bin/llvm-nm)

If the glob matches nothing (unusual NDK layout), NM_BIN becomes the literal unexpanded pattern and the check fails with a confusing "no such file" rather than a clear diagnostic. If it matches more than one host dir, NM_BIN holds two space-separated paths and breaks the invocation. A guard makes the failure legible:

NM_BIN=$(ls "${ANDROID_NDK}"/toolchains/llvm/prebuilt/*/bin/llvm-nm 2>/dev/null | head -n1)
[[ -x "${NM_BIN}" ]] || { echo "llvm-nm not found under ${ANDROID_NDK}" >&2; exit 1; }

4. The check assumes a portable (non-ATen) build

In ATen mode ET_RUNTIME_NAMESPACE is runtime::aten (exec_aten.h:66), so register_backend et al. would be executorch::runtime::aten::register_backend and the hard-coded substrings in check_exported_symbols.sh:29-30 wouldn't match. The Android path is portable, so this is fine today — but a one-line comment noting the check is portable-mode-specific would save a future ATen-mode reader some confusion. (The version-script globs themselves are unaffected — * spans ::, so both namespaces are covered.)

Minor

  • The required-symbol list in check_exported_symbols.sh duplicates intent from version_script.txt and can drift. That's an acceptable smoke-test design, but a comment cross-linking the two (you already reference the script from the version file — the reverse link exists too) keeps them in sync mentally.
  • Placement of the check is correct: it runs on the pre-strip staged .so, and llvm-strip (build_aar) removes local/debug symbols but not dynamic exports, so the verified ABI survives into the AAR. 👍

Overall: The approach is correct and the guardrail is well-designed. Nothing blocking, but I'd (a) remove the dead executorch::aten::* lines + comment, and (b) measure/acknowledge the .so size impact of exporting the full runtime namespace, since that's in tension with the parent issue's goal. Items 3-4 are polish.
· branch android-split-backend-abi

@nil-is-all nil-is-all added release notes: android Android Java and JNI code and removed release notes: none Do not include this in the release notes labels Sep 10, 2026
…sing-aliases only, no symbols bind) and document actual export surface
…ed patterns

lld matches quoted version-script patterns literally, so the quoted
"vtable for executorch::runtime::*"-style entries were treated as exact
symbol names and failed the link with 'symbol not defined' on NDK r29.
Switch to the unquoted Itanium-mangled wildcard forms
(_ZN/_ZTI/_ZTVN10executorch7runtimeE*), which glob correctly on both
GNU ld and lld.
@executorch-triage executorch-triage Bot added the community: contribution PRs coming from community (excluding hardware partners) label Sep 22, 2026
@meta-codesync

meta-codesync Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

@psiddh has imported this pull request. If you are a Meta employee, you can view this in D122178024.

@psiddh

psiddh commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for this! Please take a look at #21849 , which splits XNNPACK/Vulkan into same-release backend libraries and should address the size problem this PR is going after. My concern with this hot-swap approach is that EXECUTORCH_1_0 promises a stable cross-version C++ ABI we may not guarantee (for example BackendInterface/EValue layouts, TensorImpl may change often.).

This branch has not been deployed

No deployments
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. community: contribution PRs coming from community (excluding hardware partners) module: android Issues related to Android code, build, and execution release notes: android Android Java and JNI code

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants