← All courses ← Course home

Lesson 03 of 05

Match maxNumTokens to the model

The model card may say 32K context, but the file was built for a fixed KV cache. Too high can crash or return nothing. Near the limit can crash. On some NPUs, too large a cache drops part of the prompt with no error.

Agent brief (llms.md)

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

IssueSetupWhat happened
LiteRT #6765Gemma 4 E4B, iOS, CPU, C API built from source4096 was stable. 8192 returned no output. 16384 crashed with SIGSEGV in reshape::Eval during prefill. The model card advertises 32K.
#2799Gemma 4 E2B, Android emulator, CPU, 0.13.1 and 0.14.0, maxNumTokens = 4096Third turn of a text and image chat crashed with SIGSEGV as the context got close to 4096. Setting 8192 made the crash go away.
#3508Gemma 3 270M and 1B bundles on Qualcomm and Intel NPUsOnly 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:

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

  1. Find the value the bundle was built with. Ask the person who exported it, or read the export command (--cache_length in litert-torch export_hf). Do not use the model card's context length.
  2. Set maxNumTokens to that value, or lower. Do not leave it at a large guess.
  3. Count tokens yourself and stop the chat before the limit, with room for the answer.
  4. 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

  • maxNumTokens is 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 maxNumTokens from 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.