Skip to content

Link LiteRT into the Linux and Windows v0.19 hosts - #112

Merged
leehack merged 1 commit into
mainfrom
fix/desktop-litert-same-runtime
Nov 8, 2026
Merged

leehack merged 1 commit into
mainfrom
fix/desktop-litert-same-runtime

Conversation

@leehack

@leehack leehack commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

Outcome

Fixes #111. From v0.19, Linux (x64, arm64) and Windows x64 compile LiteRT from the pinned LITERT_REF into libLiteRtLm / LiteRtLm.dll, as Apple and Android already do. EmbeddingGemma-2 engine creation now succeeds on all three desktop targets and produces correct vectors. The same change also repairs desktop explicit-GPU generation, which aborts on the published v0.19.1 Linux and Windows bundles. The C ABI and bundle layout are unchanged. Nothing is tagged, released or dispatched; the follow-up stable rebuild would be v0.19.1-1.

Why

LiteRT-LM hands external and in-memory weights to LiteRT by writing C++ members of the opaque options struct (litert::Options::SetExternalWeightScopedFile / SetWeightInMemoryMap, header-only, reinterpret_cast<LiteRtOptionsT*>). On Linux and Windows the host was source-built against libstdc++ / the MSVC STL but, with --define=litert_runtime_link_mode=dynamic, linked upstream's prebuilt libLiteRt, which carries its own static libc++. That write is not ABI-valid, so Linux dropped the weights (Failed to open tflite_weights) and Windows faulted (libLiteRt.dll+0x7a8d, libc++ unordered_map lookup).

v0.19 made it worse for GPU: TopK samplers are now source-built into the host, while the WebGPU accelerator was the prebuilt libc++ one. On an NVIDIA L4 the published Linux x64 bundle aborts at the first GPU decode with free(): invalid size in ml_drift::TensorDescriptor::~TensorDescriptor <- TopKWebGpuSampler::SetInferenceFuncAndInputTensors (core-dump backtrace in the evidence), and Windows D3D12 crashes the same way (0xC0000409).

In static mode upstream's default_static_gpu_accelerator compiles ml_drift_webgpu_accelerator into the host, so core, accelerator, samplers and LiteRT-LM share one C++ runtime, the same configuration as macOS and Android.

Changes

  • tools/build_upstream_runtime.py: uses_shared_desktop_litert() keeps litert_runtime_link_mode=dynamic only for Linux/Windows v0.16–v0.18; v0.19+ desktop uses the same litert_link_capi_so=true command as Apple/Android. Older tags build exactly as before.
  • New fail-closed check validate_desktop_litert_linkage(): a v0.19+ Linux/Windows host that still imports libLiteRt.so / libLiteRt.dll (ELF DT_NEEDED, PE imports + delay imports) fails the build. CI logs show Validated in-process LiteRT linkage on linux-x64, linux-arm64 and windows-x64.
  • Tests: link-mode command test now covers v0.18.1, v0.19.0 and v0.19.1; new linkage-check test (Linux/Windows reject, pre-0.19 and Android skip).
  • Docs: docs/platform_strategy.md (v0.19 desktop linkage and why), docs/upstream_patch_audit.md (Linux arm64 v0.18.1-2 ignores litert_lm_engine_settings_set_num_threads: CPU runs on one thread #89 context limited to v0.16–v0.18).

Bundle layout and C ABI: unchanged. Bundles still ship upstream libLiteRt.* and libLiteRtWebGpuAccelerator.* (llamadart's hook requires and preloads them), but the v0.19 host neither links nor dlopens them (Statically linked GPU accelerator registered. in every run). Exported C API / bridge symbols pass validate_exported_symbols and the full-platform verify job.

Trade-off vs building libLiteRt from source with the host's C++ runtime: that keeps two binaries, still pairs a source core with the prebuilt libc++ WebGPU accelerator (the #47/#50 L4 crash class) and the source-built samplers, and would need the accelerator rebuilt too. Linking in-process removes every cross-runtime boundary with one define. Cost:

Target Host before → after Runtime tarball
Linux arm64 libLiteRtLm.so 49.9 → 59.9 MB 31.6 → ~35.0 MB (gzip)
Linux x64 libLiteRtLm.so 51.5 → 56.5 MB
Windows x64 LiteRtLm.dll 19.1 → 27.1 MB

Dropping the now-unused libLiteRt + accelerator (~12–15 MB per target) would need a llamadart change and is out of scope here. CI build time rises (linux-x64 50 → 60 min of a 90 min limit, windows-x64 57 → 84 min of 120).

Validation

CI at head d1a95f19: PR Release Qualification run 42430614932, 19/19 checks pass: all 8 builds, full-platform verify (runtime matrix, dependencies, SwiftPM), ASR / Whisper routing / finish-reason smokes and Qwen3-0.6B CPU inference + thread probe on Linux x64, Windows x64 and macOS arm64. Local: python3 -m unittest discover -s tools -p 'test_*.py' 441 OK, 3 skipped.

Real hosts (GCE, account-scoped project), exact CI artifacts from this head. llamadart main 60b616f1 (pins v0.19.1) with the #1369 guard bypassed locally and LLAMADART_LITERT_LM_LIB_DIR pointing at the PR bundle. Models: EmbeddingGemma-2 24d962e9 (e7a8a220…7f4c), Qwen3-0.6B 8414150f (555579ff…b0b4). Control: macOS arm64 (M4 Max) on published v0.19.1, which reproduces the earlier control bit for bit.

PR runtime hashes: linux-arm64 libLiteRtLm.so b582d717…, linux-x64 libLiteRtLm.so b8594f05…, windows-x64 LiteRtLm.dll 518412b8….

Host Published v0.19.1 This PR
Linux arm64, c4a-standard-8 (Neoverse-V2) embedding create fails (tflite_weights) 2 passes OK; llamadart litert-lm-multimodal-embedding e2e 3/3 pass
Linux x64, c3-standard-8 (Xeon 8481C) — first native-hardware run embedding create fails (tflite_weights) 2 passes OK; e2e 3/3 pass
Windows x64, c3-standard-8 (Xeon 8481C) process crash after Flatbuffer model initialized (0xC0000409) 2 passes OK; e2e 3 tests pass, suite teardown fails on a pre-existing Windows cache lock (below)
Linux x64 + NVIDIA L4, Vulkan (driver 580.178.04) GPU chat aborts 3/3 (free(): invalid size); CPU dispose failed 1/3 GPU chat 3/3, CPU 3/3; L4 Vulkan adapter selected, 1.7 GB VRAM, 95% util
Windows x64 + NVIDIA L4, D3D12 (driver 596.86) GPU chat crashes (0xC0000409) GPU chat 2 passes + 3 repeats OK; L4 D3D12 adapter selected, 1.8 GB VRAM, 95% util

Embedding values (unnormalized, 768-d, vs macOS arm64 control; batch equals single-input on every host and pass; pass 2 equals pass 1):

Input Linux arm64 Linux x64 Windows x64
text bit-exact cos 1.000000, max abs 3e-5 cos 1.000000, max abs 3e-5
black / checker image bit-exact cos 0.99895 / 0.99863 same as Linux x64 (bit-exact)
silence bit-exact cos 0.99559 same as Linux x64 (bit-exact)
tone / mixed cos 0.99645 / 0.99948 cos 0.99754 / 0.99847 cos 0.99586 / 0.99838

The earlier ./tflite_weights workaround produced garbage (text head 279.6 vs -30.0); now the text head is -29.99, 25.88, 62.18, -30.39 everywhere. Linux x64 and Windows x64 (different OS, compiler and STL, same CPU model) are bit-identical on text, both images and silence, so the x86 vs arm64 gap is ISA kernel numerics, not the weight handoff. Only non-silent audio differs between OSes on the same ISA; the Dart tone PCM hashes identically on macOS and Linux (3f895001…), so that difference is in the native audio front end (likely libm). The pairwise similarity matrix of the six inputs matches the macOS one within 0.008.

Text generation (greedy, 3 prompts × 2 passes): CPU output is identical to the published v0.19.1 output on the same host (Linux x64, Linux arm64, Windows x64). GPU output is identical across Linux Vulkan and Windows D3D12 and across repeats; there is no published GPU baseline because it crashes.

Not covered / follow-ups

  • Windows keeps *.xnnpack_cache files locked in-process after dispose (Access denied on delete). The published v0.19.1 runtime does the same for Qwen chat, so it predates this PR; it makes the llamadart e2e suite's temp-dir teardown fail on Windows.
  • Android x86_64 (also guarded by llamadart#1369) is unchanged and was not tested.
  • No macOS/iOS/Android behaviour change: their build commands are identical (unit-tested) and CI built them unchanged.
  • Release: after merge, a v0.19.1-1 stable rebuild needs separate maintainer approval; then llamadart repins and removes the #1369 guard for Linux and Windows.

Evidence (logs, JSON vectors, scripts, GPU core-dump backtrace): local llamadart-evidence/i111-pr112-d1a95f1/. All GCE VMs, the firewall rule and buckets were deleted.

LiteRT-LM hands external weights to LiteRT by writing C++ members of its
options struct. The desktop hosts linked upstream's prebuilt libLiteRt,
which carries its own libc++, so Linux lost the weights and Windows
crashed. Build v0.19+ desktop hosts with LiteRT compiled in, as on Apple
and Android, and fail the build if the host still imports libLiteRt.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux/Windows: external-weight handoff crosses the libLiteRt C++ ABI boundary (embeddings fail/crash)

1 participant