google-ai-edge/litert-samples/on-device-verification
Claude Code and agent skill by google-ai-edge/litert-samples
- community
Checking that the SKILL.md file is still in the repository.
Prove a converted or quantized LiteRT model on the actual device via the CompiledModel API - confirm GPU residency, compare device output against the source model, and diagnose device-only failures such as silent CPU fallback, whole-graph compile ceilings, and fp16 range breaks. Use after conversion or quantization, when device output is wrong or NaN, when a clean graph fails to compile only on device, or when GPU and CPU outputs are suspiciously identical.
Install
npx vskill@latest install google-ai-edge/litert-samples/on-device-verificationSKILL.md
On-device verification
A device result is done when three things hold:
- the model compiles and runs on the accelerator you claim it runs on,
- the output matches the source model numerically and on a task-level
gate,
- the record names the device, the runtime version, and the residency
line. A number without those is not reproducible and not a result.
Host-side checks (the CompiledModel checker used in gpu-clean-conversion) exercise the host GPU. The device has its own shader compiler, its own precision behavior, and its own memory ceiling — every failure mode in the table below was hit by a model that had already passed on the host. The Python API below runs on the host only (ai-edge-litert ships wheels for Linux, macOS and Windows); on a phone the same inputs go through the app's inference class or a push-run-pull binary (last bullet of "Watch for").
Loop
1. Dump references from the source model, once. Fixed inputs — one real sample plus fixed-seed random — saved as .npy next to the recipe (dump_*_ref.py). These are ground truth for every later step; regenerate them only when the source model changes.
2. Run the same inputs on the host runtime, CPU then GPU, then on the device. One argument switches the accelerator (ai-edge-litert 2.2.0, 2.3.0):
The device-CPU run is the control. If it already diverges from the source dump, the problem is the conversion, not the GPU — go back to gpu-clean-conversion. A full worked example of the A/B lives in this repo at samples/litert/speech_recognition/convert/verify_tflite.py (its ai_edge_torch.compiled_model import no longer resolves with litert-torch 0.9.4; read it as ai_edge_litert.compiled_model).
Ask for the strict accelerator. Compiling with HardwareAccelerator.CPU | HardwareAccelerator.GPU permits partial delegation and hides fallback; use the combined mode only to discover which ops fell back after a strict GPU compile fails. Strict is per graph, not per op: when the GPU cannot compile the graph, from_file raises (Some ops are not accelerated. Add kLiteRtHwAcceleratorCpu … in 2.3.0); when the delegate declines one node while building (TopK requires src tensor C dimension to be divisible by 4), that node runs on the CPU with only the log line, the call returns, and is_fully_accelerated() still reads true (measured with the 2.2.0 and 2.3.0 wheels on a Mac). The log is the residency signal.
3. Read the delegate log before reading any numbers. The CompiledModel runtime prints, per op it declines and then as a total:
Record it as N/T (N on the GPU of T nodes; benchmark_model prints the same as Replacing N out of T node(s) with delegate plus a partition count). Any op left on the CPU means part of the graph runs there — decide whether that is acceptable before quoting any accuracy or latency number.
4. Gate the output three ways.
- Numeric: correlation and max abs diff against the source dump.
Expect genuine GPU fp16 to drift in the last digits — exact equality is a symptom, not a pass (see below).
- Task: argmax match, IoU, token-for-token greedy decode — whatever
the model is for.
- Artifact, for generative models: the decoded text / audio / image.
An artifact gate catches argmax ties that numeric tolerance misses.
5. Record it: device, runtime version, N/T (and the partition count where benchmark_model gave one), correlation, max abs diff, task result — one row per device in the recipe README.
The silent CPU fallback
A recurring false positive: everything runs and the numbers match perfectly. If the GPU output is bit-identical to the device CPU fp32 output, re-read the residency line before believing the GPU ran — a full N/T decides, not the equality; fp16 GPU runs usually drift in the last digits, and an fp32-forced or integer graph may not.
When the comparison is between two bundles of the same weights (a re-pack against its source, external against inline weights), byte-compare the text on single-thread CPU (--cpu-thread-count 1) or on the GPU: at the default thread count the CPU greedy text of one file moves between fresh processes, so a byte difference there is not a file difference. Long replies in several fresh processes per file are the test; short answers agree even when long ones do not.
Device-only failures
| What you see | What it is, what to do | |
|---|---|---|
| GPU compile fails on device for a graph that is op-clean and passed the host check | A whole-graph compile ceiling, not a bad op — a fused graph can fail where each half compiles. Split at a natural block boundary (conv frontend / transformer encoder), verify split == monolith bit-exact on the host, ship the halves | |
| The file refuses to load at all | The >2 GB flatbuffer limit. Split, or quantize below it | |
| Full residency, wrong numbers | Bisect by intermediates: re-export with block-boundary tensors as extra outputs and find the first one that diverges. In the cases on record the cause was a reduction (mean, variance, Σx²) in fp16, not the op suspected. If materializing a tap fixes the numbers, that localizes the bug to a fusion boundary — record it as a finding, not a nuisance | |
| Wrong-but-plausible output, and every hypothesis costs a slow re-export | Micro-probe instead: export ~1 KB graphs — the suspect subexpression and each candidate fix — and run them through the device harness you already have. Minutes per hypothesis instead of a re-export per hypothesis | |
| NaN or garbage only on the GPU, in a model with large-magnitude residuals or deep modulation paths | An fp16 range break. Confirm the attribution by forcing fp32 on the delegate (CompiledModel.GpuOptions(precision = …) in Kotlin; ~2× memory, slower); then fix it properly with the fp16-safe rewrites in gpu-clean-conversion, or keep the offending block on CPU and run the rest on GPU | |
| The process dies while loading or running a large float model | A memory ceiling, not a model bug. Weights + activations + delegate buffers must fit in available memory. Do not run a multi-GiB fp32 build in-process on a phone "just to check" — quantize or split first, then verify the smaller thing | |
| A deep transformer shipped as an fp16 graph is bit-exact on desktop and noise on the device CPU | Android ARM XNNPACK computes fp16 activations natively; desktop XNNPACK upcasts to fp32. Deep residual streams compound the difference to collapse. Ship fp32 activations for CPU inference on device (fp16 weights that dequantize on load are a size route, not this case) | |
| Attention quality collapses only for a small-head submodule | head_dim is the fp16-fragile axis, not token count: the same graph at head_dim 64 held corr 0.998 where head_dim 16 fell to 0.86. Pad heads to ≥32 or keep the small-head module on CPU | |
A [1,N,C] token tensor that is a graph output and feeds other consumers comes back corrupted | 3-D fan-out corruption — the later branch is clobbered, it cascades, and it reads exactly like an fp16 wall downstream (4-D NCHW maps with the same fan-out are fine). Keep token tensors as sole leaf outputs (or keep them 4-D) and push per-token heads to the host — exact, since per-token ops commute with the gather | |
| An output that is one of the graph's own inputs, passed through with no op writing it, comes back as stale bytes; a graph that also reads that tensor computes on that unwritten buffer (zeros in the case on record) | The CompiledModel API binds the output to its own buffer and the graph reads the tensor from that output binding, which nothing writes. Measured on one speech encoder with LiteRT 2.2.0 (Android, CPU) and ai-edge-litert 2.4.0.dev20261011 (macOS, CPU and Metal); the Interpreter API keeps one tensor and gives the right answer, so a host Interpreter check passes. Export without the pass-through output (or produce it with a real op), or write the value into the output buffer as well, or bind one buffer to both; when a host hands streaming state back, copy pass-through states from the inputs | |
| A fused-LSTM-style graph with variable tensors (state kept inside the graph) will not load | The CompiledModel loader rejects it: model_load.cc: Variable tensors not yet supported (2.2.0 and 2.3.0, from_file and from_buffer). Re-export the graph stateless — the recurrent state as explicit inputs and outputs that the host feeds back each call — and verify the per-call parity on that form |
Watch for
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/on-device-verification/SKILL.md
Popularity
- Repository stars
- 405
- Repository forks
- 114
- 7-day trend score
- 30
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/google-ai-edge/litert-samples/on-device-verification)