9.7 KiB
Per-platform on-device inference backend strategy — CoreML / LiteRT-LM / Metal
Status: decision record + current-state evidence (2026-06-24). Answers the recurring question "should iOS/Mac LLM inference move to CoreML or LiteRT-LM, and does llama.cpp give us per-platform backends for max battery/GPU efficiency?"
Short answer: the per-platform backend strategy already exists and is correct.
One elizaOS/llama.cpp fork compiles into libelizainference with the right GGML
backend per platform. CoreML and LiteRT-LM are not the levers for Apple LLM
decode — Metal is, and it is already the shipped, optimized, verified path.
1. Per-platform backend table (what is actually built/used)
| Platform | GGML backend | Source of truth |
|---|---|---|
| macOS (Apple Silicon) | Metal (TurboQuant + eliza kernels embedded) | packages/app-core/scripts/build-llama-cpp-mtp.mjs (-DGGML_METAL=ON, embed metallib) |
| iOS (arm64) | Metal (static archives) | same build script, ios-arm64-metal / …-metal-fused targets |
| Android arm64 | Vulkan (GPU) + CPU fallback | packages/app-core/scripts/aosp/compile-libllama.mjs |
| Android x86_64 | CPU only | same (Vulkan wired for arm64 only) |
| Linux / Windows desktop | CUDA (NVIDIA) / CPU | desktop dylib build + cuda_verify |
All from one fork → one managed library (libelizainference) → one FFI pipe
(native/AGENTS.md §11). This is "per-platform backends for max battery/GPU
efficiency." There is no separate per-platform runtime to add.
2. CoreML — wrong tool for LLM decode; correct where it is already used
- LLM autoregressive decode: CoreML/ANE is not viable. CoreML compiles a static graph with fixed tensor shapes; LLM decode grows a KV cache token by token with per-position masks. ANE targets fixed-graph CV/encoder workloads, not streaming text. There is no CoreML LLM-decode backend in llama.cpp, and we have none. The correct Apple LLM backend is Metal/MPS, which handles dynamic KV shapes natively.
- Where CoreML IS used (correctly): fixed-graph models — Kokoro TTS on iOS
(
kokoro-coreml/*.mlmodelc) and image generation (src/services/imagegen/CoreML backend). These are encoder/diffusion graphs, the right shape for CoreML. - Apple Foundation Models (
src/backends/apple-foundation.ts): an opportunistic iOS-26 text adapter, never the owned backend (native/AGENTS.md §11). Out-of-process OS model services cannot satisfy the single-runtime contract.
2a. Every voice/vision model is already on its optimal backend — MEASURED, no ANE win to capture
A natural follow-up: the always-on gate models (Silero VAD, openWakeWord) run continuously, so wouldn't the ANE save battery vs the GPU? Measured on this Apple Silicon Mac (ANE present, CoreML native) — the answer is no, on two counts:
- They don't run on the GPU — both ship on native CPU (pure scalar C, "no
ggml link"):
silero_vad_runtime.c/wakeword_runtime.c, whose*_active_backend()returns"native-cpu". - The ANE is slower for a model this tiny. Ran Silero VAD v5 ONNX (2.3 MB LSTM)
200×32 ms windows, CoreML EP (ANE) vs CPU EP: CPU 0.227 ms/window vs ANE
0.798 ms/window — the ANE is 3.5× slower (identical outputs). ANE dispatch
overhead dominates a few-layer LSTM; the ANE wins on large fixed-graph models
(Kokoro), not tiny per-frame gates. Evidence:
native/verify/evidence/platform/coreml-ane-vs-cpu-voice-gate-2026-06-24.md.
So the architecture already routes every model to its optimal backend by compute profile, and it's verified:
| Model | Cadence | Backend | Right call |
|---|---|---|---|
| LLM (Gemma) decode | per-token, dynamic KV | Metal | ANE can't do dynamic decode |
| Vision mmproj / local ASR | bursty | Metal | GPU-sized bursty work |
| Kokoro TTS | per-utterance | CoreML (kokoro_5s.mlmodelc) |
larger fixed-graph → ANE helps |
| Silero VAD | always-on | native CPU | tiny LSTM → CPU fastest+lowest-power (measured; ANE 3.5× slower) |
| openWakeWord | always-on | native CPU | same |
Bottom line for "optimize voice for CoreML on iOS": there is no un-captured CoreML/ANE win — Kokoro already uses CoreML where it pays off, and the tiny always-on gates are correctly on the CPU (the ANE would regress them, measured). The system is already optimal. LiteRT-LM (the Android-NPU analogue) would hit the same tiny-model dispatch-overhead wall for the gate models.
3. LiteRT-LM — not used; permitted only as a future in-process Android-NPU backend
- Not present in the resolved code path. No LiteRT/TFLite/MediaPipe LLM runtime is wired.
- The architecture permits LiteRT-LM only as an in-process backend that links
into
libelizainferencebehind the same FFI symbols, for the Android NPU path (native/AGENTS.md §11). It is additive to llama.cpp, not a replacement, and is unimplemented. - It does nothing for Apple. Adopting it would not touch the iOS/Mac story. It is a future Android-NPU optimization, gated on real product demand.
4. The shipped model is Gemma 4 — its optimization set is applied + verified
The current Eliza-1 tiers ship Gemma 4 bases (E2B/E4B/12B/31B → 2b/4b/9b/27b),
not the retired Qwen3.5/3.6 line (packages/shared/src/local-inference/catalog.ts).
Gemma's KV is already minimal (MQA + windowed-SWA + shared-KV, dual head dims
512/256), so:
- Mandatory set (applied): TurboQuant weight-quant (
turbo3/turbo4,+turbo3_tcqon big/long-ctx tiers) + MTP separate-drafter speculative decode- Gemma-native memory settings + stock minimal KV (f16/q8_0).
- MTP wiring (applied on the Apple/desktop path):
catalog.ts runtimeForTier→active-model.ts(draftMin/draftMax/speculativeSamples) →desktop-fused-ffi-backend-runtime.ts→ the FFI's separate-drafter MTP engine (tools/omnivoice/src/eliza-inference-ffi.cpp). - NOT applicable on Gemma: the legacy head_dim=128 QJL K-cache /
PolarQuant(TBQ) V-cache fused-attn kernels. They still ship and pass
verification, but the decode graph never routes a Gemma KV through them — the
dequant-to-F16 hop in
src/llama-graph.cpp:1990only fires forQJL1_256/TBQ3_TCQ/Q4_POLARcache types, which Gemma never allocates.catalog.test.ts("advertises only safe runtime optimizations for the shipped gemma4 tiers") pins this contract.
Implication for issue #8848
#8848's headline item — "fused QJL/TBQ attention is implemented but never routed,
leaving a win on the table" — is a legacy-Qwen concern, filed from a Pixel
audit of the Qwen-shaped eliza-1-0_8b. It is not on the shipped Gemma critical
path: routing it would not speed up the Gemma model (Gemma has no QJL/TBQ cache
to route). The live Gemma audit (TurboQuant + MTP + stock KV) is green and tested.
The Mali/Vulkan items in #8848 (#4 generic-FA race, #5 prefill matmul) remain real
for Android GPU but are independent of the fused-attn routing.
5. Host-side verification evidence (Apple M-series, this machine, 2026-06-24)
Run from plugins/plugin-local-inference/native/verify/ (Metal framework runtime
shader compilation — no full Xcode required):
make reference-test → self-test: turbo3/turbo4/turbo3_tcq/qjl/polar/fused all finite, parity OK
make metal-verify → turbo3 8/8 · turbo4 8/8 · turbo3_tcq 8/8 · qjl 8/8 · polar PASS (tol 1e-3)
make metal-verify-fused → fused_attn_qjl_polar 1920/1920 PASS (max_diff 4.8e-7)
fused_attn_qjl_polar causal-prefix 1536/1536 PASS (max_diff 9.5e-7)
polar_preht use_qjl=0/1 8/8 PASS
Cross-check: verify/PLATFORM_MATRIX.md records the 2026-06-23 Gemma-4 Metal
cutover — §8 kernel gate 40/40 PASS incl. fused-attention, and real
google/gemma-4-E2B (head_dim 512, MQA, SWA) generation + llama-bench pp512 636
/ tg128 23 t/s (FA=1) on the Metal FA path.
Conclusion: the Apple/Metal Gemma path is optimized (TurboQuant + MTP + minimal KV), and every shipped kernel is bit-exact on Apple Silicon. No CoreML/LiteRT pivot is warranted or beneficial for iOS/Mac LLM decode.
6. Genuinely-remaining gated items (not code-fixable from a Mac host)
| Item | Issue | Why gated |
|---|---|---|
--spec-type draft-mtp gatedrafter-2b.gguf), validated on M-series Metal, and the fast-tier draft window fixed (draftMax 4→1 = 1.37–1.66× decode win on the 2B; the prior "regression" was the mistuned window, not the head). A from-scratch H200 head is now only a possible large/slow-tier optimization, not a fast-tier unlock. |
#8848 / #9172 | n/a — shipped; see docs/gemma4-mtp-drafter-conversion.md (2026-06-24 correction) |
eliza-archivelibelizainference.so is built from source in CI by .github/workflows/build-llama-ffi-android.yml (android-arm64-vulkan-fused via compile-libllama.mjs, zig 0.13 pin from #9598) and consumed directly as a workflow_call artifact — there is no eliza-archive prebuilt dependency and nothing to republish. The Mali mitigation is in source; the Linux/CUDA + Mali-G715 kernel gates are verified on real hardware (#9584/#9715). |
#9508 | n/a — CI-built |
| Real-audio GPU CI lane (DER/WER/echo/owner/impostor numbers) | #9454 | needs a gpu-cuda-12.6 self-hosted runner + ELEVENLABS_API_KEY |
| PCM-level AEC3 sample-level echo cancellation (turn-level self-voice gate already shipped) | #9455 | net-new DSP feature (adaptive filter + double-talk detect + ERLE corpus) |