Debugging LiteRT / LiteRT-LM On-Device GPU Accuracy & ML Drift Bugs
Use this 6-phase funnel to isolate why a LiteRT-LM model produces correct
output on CPU (--backend=cpu) but generates garbage, NaNs, or empty/corrupted
state on Android GPU (--backend=gpu, OpenCL or WebGPU).
Phase 1: Unpack & diff model metadata (.litertlm) against a GPU-good model │ Phase 2: On-device repro matrix (CPU vs OpenCL vs WebGPU + prompt sweep) │ Phase 3: Op-by-op accuracy_debugger on extracted TFLite subgraph │ ├─ Op diverges in isolation ──► Fix op kernel / precision │ └─ All ops pass (maxdiff≈0) ──► Composite / graph-lowering / state bug ▼ Phase 4: Host-side CPU vs GPU state-buffer dump after prefill_128 & decode │ (Pinpoints exact corrupted KV cache layers: e.g. local ring vs global) ▼ Phase 5: ML Drift descriptor dump & in-shader sentinel readback probes │ (Bisects runtime param chain & TensorStorageType mismatches) ▼ Phase 6: Fix in ml_drift / delegate, unit-test GpuModel invariants, & validate
Critical Environment Gotchas (Read First)
-
Delete the on-device ML Drift program cache before every GPU run after editing any kernel, parser, or selector code. Otherwise the device silently reuses the cached compiled OpenCL/WebGPU program and your C++ edits never execute:
bashadb shell "rm -f /data/local/tmp/{model_dir}/*mldrift_program_cache.bin"Keep
*_mldrift_weight_cache.binand*.xnnpack_cacheso weight conversion stays fast. -
Injecting device environment variables:
run_litert_lm.shdoes not forward custom host env vars or provide a build-only flag. Runrun_litert_lm.sh --skip_model_pushonce to build and pushlibLiteRtOpenClAccelerator.soandlitert_lm_advanced_main, then invokeadb shelldirectly with your probe env vars:bashOUT=/data/local/tmp/{model_dir} adb shell "rm -f ${OUT}/*mldrift_program_cache.bin" adb shell "ADSP_LIBRARY_PATH=${OUT} LD_LIBRARY_PATH=${OUT} \ LITERT_LM_DEBUG_DUMP_STATE=1 MLDRIFT_CACHE_PROBE=1 \ ${OUT}/litert_lm_advanced_main \ --model_path=${OUT}/model.litertlm --backend=gpu \ --async=false --clear_kv_cache_before_prefill=true \ --convert_weights_on_gpu=true --minloglevel=0 \ --input_prompt=\"hi\"" -
Layering check in
third_party/ml_driftanddelegate/composite: Adding#include "third_party/absl/log/absl_log.h"to kernel files fails Bazel/Blazelayering_check. Use#include <cstdio>+fprintf(stderr, ...)and#include <cstdlib>+std::getenv(...)for temporary probes.
Phase 1: Unpack & Diff Model Metadata Against a Known-Good Model
Unpack the failing .litertlm bundle and a known-good GPU model to compare
their ExecutorMetadataProto, LlmMetadataProto, and embedded .tflite
subgraphs:
bash/google/bin/releases/arca9-local-blaze-cli/blaze-for-agents run \ //third_party/odml/litert_lm/runtime/util:litertlm_builder_cli -- \ unpack --input_file=/tmp/dbg/failing.litertlm --output_dir=/tmp/dbg/unpacked
Check ExecutorMetadataProto.pbtext and LlmMetadataProto.pbtext for:
- Hybrid KV cache topology: Does the model mix
TYPE_LOCAL_*_CACHE(sliding-window / ring buffer, e.g.minimum_sequence_length: 1024,maximum_sequence_length: 1280) with global caches (maximum_sequence_length: 32771)? attention_mask_settings: Presence ofsliding_window_sizetriggers thegpu_optimized_single_buffer_cache_ring-buffer path in LiteRT-LM andring_buffer_sdpain the exported graph.
Phase 2: Reproduce & Narrow the Failure Surface on Device
Run the repro prompt across backends and configurations to eliminate whole subsystems up front:
-
CPU vs OpenCL GPU vs WebGPU:
bash# OpenCL GPU ./third_party/odml/litert_lm/runtime/engine/run_litert_lm.sh \ --models={model_name} --target_os=android --backend=gpu \ --skip_model_push --input_prompt="{prompt}" # WebGPU (--use_webgpu) ./third_party/odml/litert_lm/runtime/engine/run_litert_lm.sh \ --models={model_name} --target_os=android --backend=gpu --use_webgpu \ --skip_model_push --input_prompt="{prompt}"- If both OpenCL and WebGPU fail identically, the bug is in
backend-agnostic code: the exported graph, LiteRT-LM host tensor
filling, or shared
ml_drift/common/delegate/compositegraph lowering (such asLiteRtOpSelector), not an OpenCL-specific driver or shader bug.
- If both OpenCL and WebGPU fail identically, the bug is in
backend-agnostic code: the exported graph, LiteRT-LM host tensor
filling, or shared
-
Prompt-length sweep: Test a minimal prompt (
"hi"— note that system prompt wrapping still yields ~14 tokens) vs >128 tokens (multi-chunk prefill) vs >sliding_window_size. If even a 14-token prompt fails on the very first generated token, wrap-around and multi-chunk boundary math are ruled out.
Phase 3: Run Op-by-Op accuracy_debugger (and Know Its Blind Spots)
Run accuracy_debugger on the unpacked
Section3_TFLiteModel_tf_lite_prefill_decode.tflite (prefill_128 subgraph) to
compare GPU vs CPU reference op-by-op:
bash./litert/tools/accuracy_debugger/google/debug_accuracy_qc.sh \ --model_path=/tmp/dbg/unpacked/Section3_TFLiteModel_tf_lite_prefill_decode.tflite \ --signature_name=prefill_128 \ --num_ops=400
Inspect the generated CSV (Index, Op Code, Tensor Name, Max Diff, MSE, Cos Sim, SNR, PSNR, NaN, Min Ref, Max Ref, Min Accel, Max Accel, Status):
- If a standard op shows low
Cos Sim/ highMax Diff/NaN: You have an isolated op numerical/precision bug. - Blind spot to remember:
accuracy_debuggerverifies each op in isolation and reportsACCEL_COMPILE_FAILEDonshlo.composite(odml.runtime_bmm)andshlo.composite(odml.cache_update). Consequently, an entire parameter computation chain (e.g.SLICE→FLOOR_MOD→CONCAT) can pass withmaxdiff = 0inaccuracy_debuggerwhile still producing garbage in the full compiled graph due to cross-op storage-type / producer-rewiring bugs triggered only when composite ops are lowered alongside standard ops.
Phase 4: Diff CPU vs GPU State Buffers After prefill_128 & decode
When accuracy_debugger shows standard ops are healthy, instrument
google3/third_party/odml/litert_lm/runtime/executor/llm_litert_compiled_model_executor.cc
(see references/mldrift_shader_probes.md
for the drop-in snippet) to dump n, sum, sumabs, min, max, nan, and
the first 6 elements (head=[...]) of every input and output buffer in
BindTensorsAndRunPrefill and BindTensorsAndRunDecode under --async=false.
Run once with --backend=cpu and once with --backend=gpu on "hi" and diff:
- Check
param_tensor(prefill_in): Verify the 7-channelint32runtime parameter tensor[start, end, end, ...]matches on host. - Check
kv_cache_k_{layer}/kv_cache_v_{layer}(prefill_out):- If global layers (e.g. layers 3, 7, 11, ...) match CPU
(
sumabs > 0) while local/sliding-window layers (e.g. layers 0, 1, 2, ...) havesumabs = 0(min=0, max=0), the ring-buffer branch ofadd_values_to_cache_kernel.ccis early-exiting without writing.
- If global layers (e.g. layers 3, 7, 11, ...) match CPU
(
Phase 5: Bisect with ML Drift Descriptor & Shader Readback Probes
Understand the parameter contract before probing:
| Slot | Meaning | Consumer |
|---|---|---|
0 | token_index_offset (start or start % S) | add_values_to_cache (both branches) |
1 | active_tokens (end or min(end, S)) | add_values_to_cache non-ring bound |
2 | kActiveTokensAlignedIndex | runtime_batched_matmul (src_end_ch_index / dst_end_ch_index) & sdpa_transposed |
3 | Ring update_length (end - start) | add_values_to_cache ring branch only (if (X >= update_length) return;) |
Use two targeted probes (full code in references/mldrift_shader_probes.md):
- In-shader param readback (
MLDRIFT_CACHE_PROBE): Inml_drift/delegate/composite/add_values_to_cache_kernel.cc, bypass theX >= update_lengthgate and overwritefinal_value_kwithfloat4(params.Read(0..3))andfinal_value_vwithfloat4(params.Read(4..7)). The host[DBG]dump ofkv_cache_k_0 head=[...]andkv_cache_v_0 head=[...]then directly prints the exactint32values the GPU shader read fromargs.params! - Host-side
OperationDef/TensorStorageTypedump (MLDRIFT_SLICE_PROBE): Ingoogle3/third_party/ml_drift/common/kernels/strided_slice.cc(insideGetStridedSliceCode), printstarts.c,ends.c,src.Channels(), andsrc.GetStorageType()(1 = BUFFER,3 = TEXTURE_2D) for small int32 tensors (c <= 16).
Classic Root-Cause Signature (b/565413009)
If MLDRIFT_CACHE_PROBE shows params[0..2] are valid (0, 14, 14) while
params[3..6] are garbage (±inf in fp16), and [SLICEDBG] shows:
textstarts.c=0 ends.c=3 | src c=7 storage=1 (BUFFER) -> valid starts.c=3 ends.c=7 | src c=7 storage=3 (TEXTURE_2D) -> GARBAGE (unproduced tensor!)
Look at LiteRtOpSelector::ParamTensorToBuffer in
ml_drift/delegate/composite/litert_op_selector.cc:
- Composite ops (
add_values_to_cache,runtime_batched_matmul,sdpa_transposed) declareparamsviaAddSrcBuffer(BUFFERstorage), whereas standard ops likeStridedSlice(rest = param'[3:7]) consume the original tensor viaAddSrcTensor(TEXTURE_2D) without consultingreplaced_tensors_. - Calling
model_builder->UpdateOutputTensor(param_tensor, new_param_tensor.id)re-points the producer node's output fromparam_idtonew_param_tensor.id, leavingparam_idwith no producer in theGpuModelso any non-buffer consumer reads uninitialized GPU memory. - Correct fix: Emit
model_builder->Copy(param_tensor, new_param_tensor)instead ofUpdateOutputTensor. Whenparam_tensorhas no other consumers,LinkNodes()ingoogle3/third_party/ml_drift/common/merge_nodes.ccautomatically fuses the single-consumer copy back into the producer.
Phase 6: Unit-Test the GpuModel Invariant & Validate End-to-End
-
Revert all temporary probes (
hg reverton kernel/executor files) before running final validation. -
Write a hermetic
cc_test(seeml_drift/delegate/composite/litert_op_selector_test.cc) that builds aGpuModelviaGpuModelBuilder+LiteRtOpSelectorand asserts that everyin_idof everyGpuNodeingpu_model.nodesis present indefined_tensors(graph inputs ∪ const tensors ∪ node outputs):bash/google/bin/releases/arca9-local-blaze-cli/blaze-for-agents test \ //ml_drift/delegate/composite:litert_op_selector_test -
Validate on device across:
- Failing model on OpenCL (
--backend=gpu) - Failing model on WebGPU (
--backend=gpu --use_webgpu) - A baseline non-sliding-window model
(
--models=kanana_i4_107_kv_1024 --backend=gpu)
- Failing model on OpenCL (
References
- Drop-in C++ probes (Executor state dumper, Shader param readback, Slice storage dumper): mldrift_shader_probes.md
Contributions
To contribute or modify this skill or its references, follow the contribution guidelines before making changes.


