跪拜 Guibai
← All articles
AI Programming

Android On-Device LLM: 596s to 10s Without Touching the Model

By 张益达Yida ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

The 60x speedup came entirely from engineering around the model: build flags, a backend switch, GPU enabling, and fewer output tokens. The lesson is that the model is often the last thing to change, and that 'unsupported' and 'undeclared' can look identical in logs.

Summary

An auto-expense Android app reads payment screenshots with on-device OCR and a 0.6B text model, keeping all data local. The initial build took 596 seconds per screenshot on a MediaTek Dimensity 9000+ phone, making the feature unusable. Over four optimization rounds the time dropped to 10 seconds: first by fixing ARM build flags (NEON dotprod, disabling OpenMP) and switching to Q4_K_M, then by replacing llama.cpp with LiteRT-LM's dynamic INT8 backend, then by enabling OpenCL GPU, and finally by cutting the output contract from 7 fields to 4 and keeping the inference engine resident.

The 60x speedup came with a key surprise: the model itself was never the bottleneck. A 0.6B Qwen3 model beat a 1.7B model on accuracy (100% vs 96.7%) and speed (5 min vs 17 min) for this structured text-to-JSON task. The real work was in silent failure hunting: a missing native library declaration caused CPU fallback, a missing GPU sampler truncated outputs at 145 characters, and two INT4 quantization paths both failed with the same grammar-mask bug. A 30-image regression gate caught a prompt slimming attempt that looked faster but actually broke amount and merchant extraction.

The project is MIT-licensed with ADRs, timing docs, and the regression set published. For on-device AI, it shows that the gap between 'demo works' and 'actually usable' is filled with build flags, linker namespaces, output contracts, and deterministic fallbacks - not model architecture.

Takeaways
— Baseline llama.cpp Q8_0 with ARM default build took 596 seconds per screenshot; the final setup runs 8.4–11 seconds.
— Switching to Q4_K_M and enabling NEON dotprod while disabling OpenMP cut time from 596s to 208s.
— Moving from llama.cpp to LiteRT-LM with dynamic INT8 reduced time to 41.9s.
— Enabling OpenCL GPU and adding the proper sampler brought it to 17.4s.
— Cutting the output contract from 7 to 4 fields and keeping the engine resident reached 8.4–11s, a ~60x improvement.
— Qwen3-0.6B Q8_0 achieved 100% hard-field accuracy on 30 real screenshots; Qwen3-1.7B Q8_0 got 96.7% and took three times longer.
— INT4 quantization failed on two independent pipelines (wi4b32 and mixed_int4) due to a grammar-masking bug in runtime 0.17.0.
— A missing <uses-native-library> declaration for libOpenCL.so makes the GPU silently fall back to CPU; visible as slowness, not an error.
— The official LiteRT-LM AAR omits the GPU sampler .so, causing output to truncate at ~145 chars; extracting it from the prebuilt tag fixes it.
— Slimming the prompt from 410 to 255 tokens cut end-to-end time to 169s but dropped hard-field accuracy to 96.7%; the 30-image gate caught it.
Conclusions

For a structured text-to-JSON task, a 0.6B model beat a 1.7B model on both accuracy and speed; capability spillover is pure cost when the output is tightly constrained.

The biggest time sinks were silent: linker namespace isolation, a missing GPU sampler, and INT4 grammar masking. None produced an error, only incorrect or slow behavior.

A 30-image regression gate is what caught a prompt slimming that looked like a pure win but broke extraction accuracy; without it the trade would have shipped.

The framing 'is transmitting data acceptable?' determines whether on-device AI is a necessity; once that's set, the model itself is rarely the bottleneck.

Concepts & terms
LiteRT-LM
Google's on-device LLM runtime that supports multiple backends (CPU, GPU) and constrained decoding via a JSON Schema or grammar, replacing llama.cpp in this project.
LLGuidance
A constraint decoding library that enforces grammar rules during token generation; used by LiteRT-LM to guarantee valid JSON output.
GBNF grammar
A grammar format used by llama.cpp to define output structure; in this project it was translated to JSON Schema for LiteRT-LM.
Linker namespace isolation
Android's mechanism since API 24 that prevents native libraries outside the NDK public set from being loaded unless explicitly declared in the manifest via <uses-native-library>.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗