Skip to content

Extract native library candidates and rank Linux arm64 CPU variants - #1459

Merged
leehack merged 5 commits into
mainfrom
refactor/354-native-library-candidates
Oct 27, 2026
Merged

leehack merged 5 commits into
mainfrom
refactor/354-native-library-candidates

Conversation

@leehack

@leehack leehack commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Commit 1 (S5 of #354): moves native library-candidate naming out of llama_cpp_service.dart into lib/src/backends/llama_cpp/native_library_candidates.dart (LlamaCppLibraryCandidates, internal). Backend/ggml/wrapper/mtmd file names, patterns, asset URIs, wrapper lookup paths and the Android CPU-variant ranking now take host OS, ABI, module directory and directory listing as inputs. The open/probe loops and every guarded native call stay in LlamaCppService. One deliberate behavior change: when the Android module directory can't be read, the code used to throw UnsupportedError (it called sort on a const []). It now returns no names.
  • Commit 2 (for #675, with leehack/llamadart-native#127): when a module directory exists on Linux arm64, the CPU candidates try libggml-cpu-linux_armv8.6_1.so, then libggml-cpu-linux_armv8.2_2.so, then the baseline libggml-cpu.so. ggml_backend_load returns NULL for a variant whose score is 0, so the loop falls through to the baseline.

Why this matters beyond explicit modes: initializeBackend always loads the CPU module through these candidates before any model load. On main, a Linux arm64 bundle from #127 therefore registers the baseline first, and auto (ggml_backend_load_all_from_path) then adds the variant as a second CPU backend. ggml/llama.cpp use the first CPU device, so even auto would stay on the baseline. The new order matches descending ggml_backend_score (aarch64 score: 8.2_2 = 1+2+4 = 7, 8.6_1 = 7+16 = 23). The first module that loads is the same file load_best picks, and ggml's register_backend drops the repeated reg. The result is one CPU backend whichever order auto and explicit loads run in.

Production-readiness scope

  • User-facing scope: Linux arm64 apps whose native bundle ships the feat(tools): add function and tool calling helpers #127 variants use the best supported CPU variant in every backend mode (startup, cpu, vulkan, blas, auto). No current release bundle ships them yet, so nothing changes until the native pin moves.
  • Supported platforms/paths: native Linux arm64 when a module directory is resolved. Android ranking, Linux x64/arm32/riscv64, Apple and Windows naming are unchanged (pinned by tables and a differential run, see below).
  • Unsupported or intentionally unavailable paths: with no module directory, Linux arm64 still names only the bare libggml-cpu.so. Variants not in the list (for example a future linux_armv9.*) sort after the baseline until llamadart adds their rank. That keeps an unscored or unaudited module from being tried first. No asset-URI variants are added for Linux.
  • Out of scope / follow-ups: native pin bump to a release containing feat(tools): add function and tool calling helpers #127 and a real Neoverse-N1/V1 run (tracked by #675). The rest of #354's slices.

Completeness checklist

  • Declared scope is fully implemented, or explicitly reduced above.
  • Unsupported platform/option combinations fail loudly with actionable diagnostics or are clearly documented as unavailable. (A variant that fails to load is recorded in the existing backend probe failures; the baseline still loads.)
  • Public API docs, README/website docs, examples, support matrices, and changelog entries are updated where relevant (native-build-hooks.md gets a Linux arm64 CPU variants section, and one Unreleased bullet in both changelogs. There is no public API.)
  • Regression coverage covers the original issue plus key negative/version-skew paths where relevant (absent variants, no baseline, unlisted variants, other ABIs, no module directory, Android unchanged, unreadable directory).
  • Public capability changes update the validation coverage registry: N/A. This is internal module ordering with no new public capability.
  • Security/privacy review completed: no secrets or paths added to logs, errors or keys.
  • Follow-up work is tracked: #675, #354.

PR type guidance

  • Feature PR: N/A. This is a refactor plus a runtime-selection fix.
  • Bugfix PR: root cause: on Linux arm64 the baseline CPU module sorts first in the explicit and startup candidates, so ggml's variant selection never takes effect, and mixed loads register two CPU backends (audit finding 5 on #127). The regression tests are the table tests in native_library_candidates_test.dart.
  • Docs-only PR: N/A

Test Plan

  • dart run tool/prepare_workspace.dart (clean tree afterwards)
  • dart format --output=none --set-exit-if-changed .
  • dart analyze
  • dart run tool/testing/check_platform_boundaries.dart
  • TMPDIR="$(mktemp -d)" dart test -p vm -j 1 --exclude-tags local-only test/unit/backends/llama_cpp/ test/unit/test_structure/ test/unit/tooling/ test/unit/hook/ (2703 passed, macOS). The full VM and Chrome suites run in CI.
  • dart test -p chrome --exclude-tags local-only: CI only. No web-reachable file changed.
  • Other: characterization against unchanged main (see Matrix Evidence)

Matrix Evidence

Matrix row Scope covered Platform / model / backend Result Evidence / notes
characterization vs main Every builder on main's own code: an oracle generated from origin/main's methods, with only Platform.isX replaced by an injected OS macOS host; os ∈ {linux, android, macos, ios, windows, other} × 5 ABIs × 11 real-filesystem fixtures × 8 backends PASS 300 cases. Commit 1: identical except Android+unreadable dir, where main throws. Commit 2: the only extra diffs are linux/linuxArm64/cpu reorders of the variant fixtures. The committed table tests run against the same oracle: 15/16 pass, and the one failure is the documented Android throw. Oracle is scratch-only, not committed
host wrapper candidates debugLlamadartWrapperLibraryCandidatesForTesting() main vs branch macOS arm64 PASS 19 candidates, identical
mutations native_library_candidates.dart VM 15/15 killed dropping the Linux sort, swapping priorities, unlisted variants ahead of the baseline, ignoring ABI, Android unknown rank, removing the listing catch, always adding the baseline, Windows wrapper alias, wrapper dedupe, ggml-base asset, backend regex escape, Android static variants, baseline rank, Apple framework names, mtmd suffix pattern
native selection semantics load-or-fallthrough and dedupe QEMU Linux arm64 N1/A72, #127 bundle PASS (from #127 audit) ggml_backend_load of an unsupported variant returns NULL. Loading the same file twice gives one CPU device. Explicit baseline plus auto on N1 gives two devices with the baseline first, which is what this PR removes. The ranked-then-auto sequence itself has not been run on Linux arm64
high-risk-exact-head-independent-qa independent blocking-only review of exact head 3003c25a2 / 8dede4429 PASS First audit at c470d3e BLOCKED on one P1 (test missing @TestOn('vm'), broke Test Web (Chrome)); fixed in 3031a099d. Delta re-audit CLEAR at 3003c25a2: 12,600-case differential vs main, ggml dedupe/score analysis, mutants killed. Evidence llamadart-evidence/pr-1459/r2/evidence-1459-3003c25.json; high_risk_readiness.dart exits 2.
release-metadata-verification N/A no release metadata touched
structured-output-adversarial N/A no grammar or tool-call surface
litert-lm-json-schema N/A LiteRT-LM untouched

Review Notes

High-risk regression review

  • Classification: high-risk (backendRuntime)
  • Implementation task: this PR's author session
  • Independent blocking QA task: independent auditor sessions; c470d3e BLOCKED (P1 fixed in 3031a09), 3003c25 CLEAR
  • Exact head / current base: 3003c25a2 / 8dede4429
  • Production-branch deletion, bypass, or miswire proof: the mutations above. Service wiring (os: Platform.operatingSystem, abi: _abi) has no unit test that can see it, because ggml_backend_load is native. The host wrapper-candidate comparison and the differential oracle cover the moved call sites.
  • Affected-family real-model/artifact evidence: none for the Dart change on Linux arm64. No released bundle ships the variants, and Docker or arm64 Linux hardware wasn't available to this lane. Native primitives come from feat(tools): add function and tool calling helpers #127's QEMU audit.
  • Explicit unavailable-family or other N/A evidence: Linux arm64 hardware run deferred to Qwen3-TTS: cancellation takes ~800 ms on Linux arm64 CPU, over the tts pack's 500 ms budget #675 with the pin bump.
  • Known PR-caused P1 regressions: 0
  • Unresolved review threads: 0

Library file names, patterns, asset URIs and the Android CPU-variant
ranking now take the host OS, module directory and directory listing as
inputs, so every platform's naming is table-tested on any host. Open and
probe loops stay in LlamaCppService.

The only behavior change: an unreadable Android module directory no
longer throws from sorting a const empty list while ranking CPU modules.

Refs #354
With a module directory on Linux arm64, the CPU module candidates try
libggml-cpu-linux_armv8.6_1.so, then linux_armv8.2_2, then the baseline.
ggml_backend_load rejects a variant whose score is 0, so an unsupported
CPU falls through to the baseline. The order matches descending
ggml_backend_score, so the startup and explicit-mode load picks the module
ggml_backend_load_all_from_path picks and ggml registers one CPU backend.

Refs #354, #675
@leehack
leehack marked this pull request as ready for review October 27, 2026 14:36
@leehack
leehack merged commit 9612aa4 into main Oct 27, 2026
37 of 38 checks passed
@leehack
leehack deleted the refactor/354-native-library-candidates branch October 27, 2026 14:36
@github-actions

Copy link
Copy Markdown
Contributor

Chat app preview removed for leehack/llamadart-chat-pr-1459.

@leehack leehack mentioned this pull request Oct 27, 2026
12 of 13 tasks
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.

1 participant