diff --git a/docs/platform_strategy.md b/docs/platform_strategy.md index d511c42..2c34d4c 100644 --- a/docs/platform_strategy.md +++ b/docs/platform_strategy.md @@ -168,7 +168,23 @@ Downstream packages should not infer platform support from filenames alone. ## Desktop WebGPU runtime linkage -For Linux and Windows builds using LiteRT-LM v0.16 or newer, pass +From v0.19, Linux and Windows link LiteRT from the pinned `LITERT_REF` into +`libLiteRtLm`/`LiteRtLm.dll`, as Apple and Android do, which also compiles in +the WebGPU accelerator +([#111](https://github.com/leehack/litert-lm-native/issues/111)). LiteRT-LM +hands external and in-memory weights to LiteRT by writing C++ members of the +opaque options struct (`SetExternalWeightScopedFile`, `SetWeightInMemoryMap`). +The host is built against libstdc++ or the MSVC STL, while upstream's prebuilt +`libLiteRt` carries its own libc++, so that write was not ABI-valid: Linux lost +the weights and Windows crashed, which broke every bundle with external-weight +sections, such as EmbeddingGemma-2. The build fails if a v0.19+ desktop host +still imports `libLiteRt`. Bundles keep shipping the upstream `libLiteRt` and +`libLiteRtWebGpuAccelerator` files so the consumer layout is unchanged, but the +host loads neither. + +The rest of this section covers v0.16 to v0.18. + +For Linux and Windows builds using LiteRT-LM v0.16 to v0.18, pass `--define=litert_runtime_link_mode=dynamic` so the host uses the same shared LiteRT runtime as its prebuilt accelerators. `litert_link_capi_so=true` is a legacy setting and does not select the modern runtime dependency path. Keep diff --git a/docs/upstream_patch_audit.md b/docs/upstream_patch_audit.md index d05748d..6370a82 100644 --- a/docs/upstream_patch_audit.md +++ b/docs/upstream_patch_audit.md @@ -175,7 +175,8 @@ google/XNNPACK#11793, verified on multithreaded SME2 decode. ## #89 workaround tracking: LiteRT CPU options identifier -Linux and Windows builds use `--define=litert_runtime_link_mode=dynamic`, so +v0.16 to v0.18 Linux and Windows builds use +`--define=litert_runtime_link_mode=dynamic`, so `libLiteRtLm` creates compiled models through the upstream prebuilt `libLiteRt` (`kLiteRtRuntimeBuiltin`). The host serializes CPU options under `LrtGetCpuOptionsIdentifier()`, which LiteRT `ca21dc99` returns as diff --git a/tools/build_upstream_runtime.py b/tools/build_upstream_runtime.py index 54ea37e..601ef3a 100644 --- a/tools/build_upstream_runtime.py +++ b/tools/build_upstream_runtime.py @@ -166,6 +166,18 @@ LIB_SUFFIXES = (".so", ".dylib", ".dll", ".lib", ".a") +def uses_shared_desktop_litert(platform: str, upstream_tag: str) -> bool: + """Whether a desktop host links upstream's prebuilt libLiteRt (v0.16-v0.18). + + LiteRT-LM hands external weights over by writing C++ members of LiteRT's + opaque options struct, which is ABI-valid only when LiteRT is compiled into + the host with the same C++ runtime, as on Apple and Android (#111). + """ + return (platform in {"linux", "windows"} + and is_at_least(upstream_tag, (0, 16, 0)) + and not is_at_least(upstream_tag, (0, 19, 0))) + + def patch_asr_untimed_overlap(source_root: Path, upstream_tag: str) -> None: """Carry #673 only on an audited source; require review on drift.""" require_asr_patch_decision(upstream_tag) @@ -555,10 +567,8 @@ def build_runtime( *configs, *target.get("bazel_options", []), target["bazel_target"], - # Modern desktop accelerators require the shared LiteRT runtime. The - # legacy define is ignored by their current dependency selection. ("--define=litert_runtime_link_mode=dynamic" - if platform in {"linux", "windows"} and is_at_least(upstream_tag, (0, 16, 0)) + if uses_shared_desktop_litert(platform, upstream_tag) else "--define=litert_link_capi_so=true"), "--define=resolve_symbols_in_exec=false", ] @@ -853,6 +863,22 @@ def validate_exported_symbols(output: Path, upstream_tag: str) -> None: print(f"Validated LiteRT-LM and bridge symbols in {output}", flush=True) +def validate_desktop_litert_linkage(output: Path, platform: str, upstream_tag: str) -> None: + """Fails if a v0.19+ desktop host still loads LiteRT from another binary.""" + if platform not in {"linux", "windows"} or not is_at_least(upstream_tag, (0, 19, 0)): + return + if platform == "linux": + imported = set(elf_needed_libraries(output)) + else: + imported = set(windows_runtime.read_pe(output).all_imports()) + shared = sorted(name for name in imported if name.lower() in {"liblitert.so", "liblitert.dll"}) + if shared: + raise RuntimeError( + f"{output.name} imports {', '.join(shared)}; v0.19+ desktop hosts must " + "compile LiteRT in so external-weight options stay ABI-valid (#111)") + print(f"Validated in-process LiteRT linkage in {output}", flush=True) + + def validate_android_global_visibility(output: Path, platform: str) -> None: if platform != "android": return @@ -909,6 +935,7 @@ def main() -> int: upstream_ref, ) validate_exported_symbols(output, args.upstream_tag) + validate_desktop_litert_linkage(output, args.platform, args.upstream_tag) validate_android_global_visibility(output, args.platform) validate_android_protobuf_isolation(output, args.platform, args.upstream_tag) validate_android_gpu_environment_teardown(output, args.platform) @@ -943,6 +970,7 @@ def main() -> int: upstream_ref, ) validate_exported_symbols(output, args.upstream_tag) + validate_desktop_litert_linkage(output, args.platform, args.upstream_tag) validate_android_global_visibility(output, args.platform) validate_android_protobuf_isolation(output, args.platform, args.upstream_tag) validate_android_gpu_environment_teardown(output, args.platform) diff --git a/tools/test_build_upstream_runtime.py b/tools/test_build_upstream_runtime.py index c77c647..6db6762 100644 --- a/tools/test_build_upstream_runtime.py +++ b/tools/test_build_upstream_runtime.py @@ -3,6 +3,7 @@ import subprocess import tempfile import unittest +from types import SimpleNamespace from pathlib import Path from unittest.mock import patch @@ -130,9 +131,9 @@ def test_runtime_selects_capabilities_owner_by_upstream_version(self) -> None: command = run.call_args.args[0] self.assertEqual("--define=litert_lm_capabilities_in_c_engine=true" in command, expected) - def test_desktop_gpu_build_selects_shared_runtime_without_legacy_define(self) -> None: + def test_desktop_build_selects_shared_runtime_only_for_v016_to_v018(self) -> None: for platform, arch in build_upstream_runtime.RUNTIME_TARGETS: - for tag in ("v0.15.0", "v0.16.0", "v0.17.0", "v0.17.1"): + for tag in ("v0.15.0", "v0.16.0", "v0.17.0", "v0.17.1", "v0.18.1", "v0.19.0", "v0.19.1"): with self.subTest(platform=platform, arch=arch, tag=tag), tempfile.TemporaryDirectory() as temp: root = Path(temp) gate = root / "runtime/conversation/model_data_processor/BUILD" @@ -140,17 +141,43 @@ def test_desktop_gpu_build_selects_shared_runtime_without_legacy_define(self) -> gate.write_bytes((Path(__file__).parent / "fixtures/ios_fst_gate.BUILD").read_bytes()) with patch.object(build_upstream_runtime, "materialize_git_lfs_libraries", return_value=0), \ patch.object(build_upstream_runtime, "patch_upstream_ios_sampler_path"), \ + patch.object(build_upstream_runtime, "patch_asr_untimed_overlap"), \ + patch.object(build_upstream_runtime, "patch_asr_litertlm_container"), \ + patch.object(build_upstream_runtime, "patch_stream_finish_reason"), \ + patch.object(build_upstream_runtime, "patch_huggingface_token_bytes"), \ + patch.object(build_upstream_runtime, "apply_android_gpu_safety"), \ + patch.object(build_upstream_runtime, "verify_dynamic_runtime"), \ patch.object(build_upstream_runtime, "bazel_command", return_value=["bazel"]), \ patch.object(build_upstream_runtime, "run", side_effect=RuntimeError("captured build")) as run: with self.assertRaisesRegex(RuntimeError, "captured build"): build_upstream_runtime.build_runtime(root, platform, arch, tag, "3") command = run.call_args.args[0] - self.assertEqual("--define=LITERT_LM_FST_CONSTRAINTS_DISABLED=1" in command, platform == "ios" and tag in {"v0.17.0", "v0.17.1"}) - dynamic = platform in {"linux", "windows"} and tag != "v0.15.0" + self.assertEqual("--define=LITERT_LM_FST_CONSTRAINTS_DISABLED=1" in command, platform == "ios" and tag not in {"v0.15.0", "v0.16.0"}) + dynamic = platform in {"linux", "windows"} and tag not in {"v0.15.0", "v0.19.0", "v0.19.1"} self.assertEqual("--define=litert_runtime_link_mode=dynamic" in command, dynamic) self.assertEqual("--define=litert_link_capi_so=true" in command, not dynamic) self.assertIn("--define=resolve_symbols_in_exec=false", command) + def test_v019_desktop_host_must_not_import_shared_litert(self) -> None: + cases = (("linux", ["libLiteRt.so", "libc.so.6"], ["libwebgpu_dawn.so", "libc.so.6"]), + ("windows", {"libLiteRt.dll": frozenset()}, {"KERNEL32.dll": frozenset()})) + for platform, shared, static in cases: + with self.subTest(platform=platform), tempfile.TemporaryDirectory() as temp: + output = Path(temp) / "host" + with patch.object(build_upstream_runtime, "elf_needed_libraries", return_value=shared), \ + patch.object(build_upstream_runtime.windows_runtime, "read_pe", + return_value=SimpleNamespace(all_imports=lambda: shared)): + with self.assertRaisesRegex(RuntimeError, "#111"): + build_upstream_runtime.validate_desktop_litert_linkage(output, platform, "v0.19.1") + build_upstream_runtime.validate_desktop_litert_linkage(output, platform, "v0.18.1") + with patch.object(build_upstream_runtime, "elf_needed_libraries", return_value=static), \ + patch.object(build_upstream_runtime.windows_runtime, "read_pe", + return_value=SimpleNamespace(all_imports=lambda: static)): + build_upstream_runtime.validate_desktop_litert_linkage(output, platform, "v0.19.1") + with patch.object(build_upstream_runtime, "elf_needed_libraries") as needed: + build_upstream_runtime.validate_desktop_litert_linkage(Path("libLiteRtLm.so"), "android", "v0.19.1") + needed.assert_not_called() + def test_linux_stages_matching_prebuilt_core_even_over_stale_copy(self) -> None: for arch, target in (("x64", "linux_x86_64"), ("arm64", "linux_arm64")): with self.subTest(arch=arch), tempfile.TemporaryDirectory() as temp: