Environment setup
Same setup as lesson 01: LiteRT-LM 0.18.0 (com.google.ai.edge.litertlm:litertlm-android:0.18.0 on Android, the LiteRTLM Swift package on iOS), Xcode 27 or Android Studio, and a real device. Mac scripts run with uv run in the litert-ship project (Python 3.11+).
What maxNumTokens means
In EngineConfig, LiteRT-LM documents maxNumTokens as "the maximum number of the sum of input and output tokens. It is equivalent to the size of the kv-cache." When it is null, the engine uses the value from the model or the engine default. So this one number decides how long a chat can get before something must give.
Three ways it goes wrong
| Issue | Setup | What happened |
|---|---|---|
| LiteRT #6765 | Gemma 4 E4B, iOS, CPU, C API built from source | 4096 was stable. 8192 returned no output. 16384 crashed with SIGSEGV in reshape::Eval during prefill. The model card advertises 32K. |
| #2799 | Gemma 4 E2B, Android emulator, CPU, 0.13.1 and 0.14.0, maxNumTokens = 4096 | Third turn of a text and image chat crashed with SIGSEGV as the context got close to 4096. Setting 8192 made the crash go away. |
| #3508 | Gemma 3 270M and 1B bundles on Qualcomm and Intel NPUs | Only the first prefill chunk (128 tokens) was kept. The rest of the prompt, for example tool JSON, was dropped. No error. |
The NPU case in more detail
In #3508, contributors found that on a Qualcomm SM8750 (Galaxy S25), a Gemma 3 270M build with cache_length 1024 lost every chunk after the first, and the same export with cache_length 896 kept all chunks. They tied it to the prefill mask size going above about 1 MiB at prefill length 128. Another contributor later repeated the 896 vs 1280 pair for Gemma 3 1B on SM8550. Two points from the thread matter for you:
- The safe value is per model. 896 works for 4-head Gemma 3 models at prefill 128. It is not a general rule.
- The thread title says Gemma 4 is fine on both NPUs. On Intel, changing to 896 did not help, so it looks like a different cause there.
Practical choice: use Gemma 4 on the NPU, or run Gemma 3 on CPU or GPU. If you must use Gemma 3 on a Qualcomm NPU, use a bundle built with cache_length 896 and set maxNumTokens to the same value.
Rules for your config
- Find the value the bundle was built with. Ask the person who exported it, or read the export command (
--cache_lengthinlitert-torch export_hf). Do not use the model card's context length. - Set
maxNumTokensto that value, or lower. Do not leave it at a large guess. - Count tokens yourself and stop the chat before the limit, with room for the answer.
- Test a chat that gets close to the limit on each backend.
import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.EngineConfig
/** Values come from the export, not the model card. */
data class BundleInfo(val path: String, val builtCacheLength: Int)
fun engineConfig(bundle: BundleInfo, backend: Backend): EngineConfig =
EngineConfig(
modelPath = bundle.path,
backend = backend,
maxNumTokens = bundle.builtCacheLength,
)
/** Keep room for the reply. Start a new conversation (or summarize) before this. */
fun mustRollOver(usedTokens: Int, maxReplyTokens: Int, bundle: BundleInfo): Boolean =
usedTokens + maxReplyTokens > (bundle.builtCacheLength * 0.9).toInt()
On iOS, Conversation.getTokenCount() exists in the Swift API on main. Use it to know how full the context is.
import LiteRTLM
func mustRollOver(_ conversation: Conversation, maxReplyTokens: Int, builtCacheLength: Int) throws -> Bool {
let used = try conversation.getTokenCount()
return used + maxReplyTokens > Int(Double(builtCacheLength) * 0.9)
}
The 0.9 margin is our choice. #2799 crashed close to the limit, so do not plan to fill it.
Test for silent prompt loss
Put the important fact at the end of a long prompt, after 300 tokens of filler, and ask about it. If the answer ignores it on NPU but not on CPU, you have the #3508 problem. Add this prompt to the parity set in the next lesson.
Sources
Key concepts
maxNumTokensis the KV cache size for input plus output.- The model card context length is not the bundle's built cache length.
- Too high can crash or return nothing (LiteRT #6765). Close to the limit can crash (#2799).
- #3508: some Gemma 3 NPU bundles drop prompt chunks after the first, with no error. The safe cache length is per model.
Takeaways
- Set
maxNumTokensfrom the export, and roll the chat over before 90% of it. - Prefer Gemma 4 on NPUs, or Gemma 3 on CPU or GPU.
- Add a fact-at-the-end prompt to your tests to catch silent prompt loss.