---
license: apache-2.0
base_model: Qwen/Qwen3-4B-Thinking-2507
tags:
- litert
- litert-lm
- litertlm
- on-device
- edge
- qwen3
- reasoning
- thinking
pipeline_tag: text-generation
library_name: litert-lm
base_model_relation: quantized
---
LiteRT is Google's on-device runtime, the new name for TensorFlow Lite (Android: `com.google.ai.edge.litert:litert`), and `litert-torch`, the renamed `ai-edge-torch`, is its PyTorch converter: a PyTorch model converted unmodified with `litert_torch.convert` matched the original to 4e-7 on a Galaxy S26 ([measured](https://github.com/john-rocky/apple-silicon-llm-bench/blob/main/results/android/tinyhybridnet-three-runtimes.md), LiteRT 2.2.0, Android 16, 2026-09-05).
Measured on device (edge-compat, qwen3-4b-thinking): Galaxy S26 · LiteRT-LM prebuilt-adac974b · GPU · decode 18.1 tok/s · prefill 263 tok/s · TTFT 3.95 s · all 1707 ops delegated (2026-10-05); Mac Studio M4 Max · LiteRT-LM 0.17.1 · GPU · decode 92.1 tok/s · prefill 1100 tok/s · TTFT 244 ms · all 1707 ops delegated (2026-10-05); Galaxy S26 · LiteRT-LM prebuilt-adac974b · CPU · decode 7.3 tok/s · prefill 56 tok/s · TTFT 18.95 s (2026-10-05). Record: https://github.com/john-rocky/edge-compat/blob/main/cards/qwen3-4b-thinking/CARD.md
Measured on device (edge-compat, qwen3-4b-thinking-dynamic-wi4b32-afp32): Galaxy S26 · LiteRT-LM 0.16.0 · GPU · decode 11.2 tok/s · prefill 129 tok/s · TTFT 1.65 s · all 1282 ops delegated (2026-08-24); Raspberry Pi 5 · LiteRT-LM 0.16.1 · CPU, 4 threads · decode 1.4 tok/s · prefill 9 tok/s · TTFT 29.40 s (2026-09-01). Record: https://github.com/john-rocky/edge-compat/blob/main/cards/qwen3-4b-thinking-dynamic-wi4b32-afp32/CARD.md
# Qwen3-4B-Thinking-2507 — LiteRT-LM (blockwise int4)
[Qwen/Qwen3-4B-Thinking-2507](https://huggingface.co/Qwen/Qwen3-4B-Thinking-2507) converted to
the **LiteRT-LM** (`.litertlm`) format for on-device inference with Google's
[LiteRT-LM](https://github.com/google-ai-edge/litert-lm) runtime (the engine behind the official
`litert-community/*` models).
Qwen3-4B-Thinking-2507 is a dense 4B **reasoning model** (`Qwen3ForCausalLM`, 36 layers) that
operates exclusively in thinking mode — it emits a `…` chain before its answer —
so it rides the existing Qwen3 converter and runtime directly.
## Artifact (Block 128)
| | |
|---|---|
| **File** | `model.litertlm` — int4 **block 128** (~2.3 GB) |
| **Quantization** | int4 weights (symmetric) + **OCTAV** optimal-clipping; embeddings INT8 (externalized section) |
| **Compute** | integer |
| **Context (KV cache)** | 4096 |
| **Base model** | Qwen/Qwen3-4B-Thinking-2507 |
**2026-10-07: chat template made parts-safe.** `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` was replaced by a copy whose embedded Jinja template reads message content both as a string (what litert-lm 0.17.1 passes) and as a list of typed parts (what 0.18.0 passes). On litert-lm 0.18.0, the previous file stopped at the template with `tried to use + operator on unsupported types string and sequence`; on 0.17.1 it answered. Only the template section changed: the TFLite graph and weights, the tokenizer and the executor metadata are byte-identical, and for string content the new template renders the same prompt as the old one, byte for byte. The new file is still 2,274,193,168 bytes, sha256 `110098cf…` (full value in `litertlm_manifest.json`); the previous file (Hub revision `6c4863d9`) had sha256 `a412d77d…`. Tested on macOS: with litert-lm 0.18.0 the new file answers on CPU and scores 8/8 sanity-gate questions on the Mac GPU; with 0.17.1 it gives the same text as the previous file. Tested on a Galaxy S26 with `litert_lm_main` built from the v0.18.0 tag: the new file answers on the GPU (OpenCL) and on the CPU.
**2026-10-06: GPU graph rewrite.** `model.litertlm` was replaced by a file with a rewritten prefill/decode graph. In each layer, the two DYNAMIC_UPDATE_SLICE KV-cache writes became one `odml.cache_update` composite, and the BATCH_MATMUL attention products became `odml.runtime_bmm` composites. This is the graph shape litert-torch exports with `--apply_gpu_composites`; the previous file was exported without that flag. The graph also gains a signature input, `param_tensor` (INT32[1,1,1,7]), whose start/end values the runtime fills. The attention mask stays a FLOAT32 input. A 16-token prefill signature, `prefill_16`, was added as a copy of `prefill_128` that shares its weight buffers. Every other bundle section, and the weight region of the graph, is byte-identical to the previous file (checked per section and per buffer). The new file is 2,477,192,112 bytes, sha256 `8a8a6a419a5570b3f4814d358fb795c0dcb6931206c7dae29480f47564c3f04d`. The previous file (Hub revision `92751be3c031`) was 2,474,357,680 bytes, sha256 `356b2d7778d27d55fbb0d2e0c5e8801e9de136db59849c41c882bd97333840d5`. On CPU, `prefill_16` gives output bit-identical to `prefill_128` on the same tokens. With and without `prefill_16`, the answers to 9 gate prompts (8 sanity-gate questions plus one long prompt) are byte-identical, on CPU and on the Mac GPU with fp32 activations. On the Mac GPU with fp32 activations, the rewritten graph without `prefill_16` also matches the previous file byte for byte on all 8 questions. On the Mac GPU with the default fp16 activations, 5 of 30 GSM8K answer texts stay byte-identical. The scores hold: both files answer 8/8 sanity-gate questions and 26/30 GSM8K questions (max tokens 2048, greedy), and each solves one GSM8K question the other misses. The Raspberry Pi 5 rows and the iPhone 17 Pro note further down were measured on the previous file. The commands are under Conversion.
Mac Studio M4 Max, litert-lm 0.17.1 `benchmark --cache no`, both files measured in the same window (median of 3 processes on GPU, 2 on CPU; GPU is WebGPU on Metal with fp16 activations, CPU is XNNPACK):
| Measure | Previous file | New file | New / previous |
|---|---:|---:|---:|
| GPU decode (256-token prompt, 256 decode tokens) | 66.6 tok/s | 92.1 tok/s | ×1.383 |
| GPU prefill (256-token prompt) | 984 tok/s | 1100 tok/s | ×1.118 |
| GPU TTFT (16-token prompt) | 0.152 s | 0.037 s | ×0.247 |
| CPU decode (256-token prompt, 256 decode tokens) | 18.5 tok/s | 18.4 tok/s | ×0.997 |
| CPU TTFT (16-token prompt) | 1.444 s | 0.478 s | ×0.331 |
## Artifact (Block 32)
| File | Quantization Recipe | Context | Size |
|---|---|---:|---:|
| `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` | dynamic_wi4b32_afp32 (block-32) | 4096 | 2.1 GB |
## Conversion Notes
`Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` is a dynamic INT4 variant (block-32 weights, FP32 activations). It was converted through the LiteRT Torch (`litert-torch`) path and quantized with AI Edge Quantizer. This artifact incorporates LiteRT-LM GPU graph optimizations, including composite ops for RoPE, fused QKV, and fused Gate/Up projections, and is configured with static prefill memory allocation.
> **Update (2026-08-31) — thought channel and think pre-fill repaired in place.** Both bundles received two metadata-only repairs today (weights byte-identical each time, verified section by section). First, the `thought` channel (`\n` / `\n`) was declared in the bundle metadata — without it the runtime streams raw reasoning inline into the answer and silently ignores any thinking budget. Second, the generation prompt now **pre-opens the think block exactly as the upstream Qwen3-4B-Thinking-2507 template does** (`<|im_start|>assistant\n\n`): `model.litertlm`'s structured assistant prefix and the block-32 file's embedded template both previously ended at `assistant\n`, leaving the model to emit `` on its own — a discipline quantized reasoning models lose first. After the repair `model.litertlm` scores 8/8 on the 8-question sanity gate on both macOS backends (CPU and Metal GPU, max-tokens 2048), and the block-32 file scores 8/8 on CPU.
## ⚠️ It's a reasoning model — give it room to think
This model generates a `…` reasoning chain, then the answer. **Run it with
`max_tokens` ≥ 2048** — at a short limit it gets cut off mid-thought and never reaches the answer.
(All quality numbers below were measured at 2048.)
**Thinking cannot be switched off.** On litert-lm 0.18.0 `ThinkingConfig(enable_thinking=False)` has no effect on both files: with a 400-token cap the thought channel uses the whole cap and no answer text appears (measured 2026-10-13). Set `thinking_token_budget` (300 answered 17 × 23 under a 700-token cap) and a cap above it.
## Performance
All rows are for the current `model.litertlm` (the 2026-10-06 file).
**Mac Studio M4 Max.** litert-lm 0.17.1 (pip) CLI, `litert-lm benchmark --cache no`, 256 prompt / 256 decode tokens. Each value is the median of separate processes: 3 on GPU, 2 on CPU. The 16-token TTFT comes from separate runs with a 16-token prompt and 32 decode tokens.
| Backend | Prefill (256 tokens) | Decode (256 tokens) | TTFT (256-token prompt) | TTFT (16-token prompt) | Init |
|---|---:|---:|---:|---:|---:|
| GPU (WebGPU on Metal, fp16 activations) | 1100 tok/s | 92.1 tok/s | 0.24 s | 0.037 s | 2.2 s |
| CPU (XNNPACK) | 113 tok/s | 18.4 tok/s | 2.57 s | 0.478 s | 2.1 s |
Init is the `Init time` the CLI prints; that value includes both `Init Total` and the phases inside it, so it counts engine creation twice ([LiteRT-LM #4057](https://github.com/google-ai-edge/LiteRT-LM/issues/4057)).
**Samsung Galaxy S26** (SM-S942Q, Android 16). `litert_lm_advanced_main` from a LiteRT-LM main build of 2026-09-18, 1024 prompt / 256 decode tokens. The GPU runs use `--disable_cache=true` (cold init); the CPU runs use the XNNPACK weight cache, which init writes. Each run is one process: 3 iterations on GPU, 2 on CPU, and 3 in each separate 16-token run (16 prompt / 32 decode tokens). Each cell is the range over those iterations. In the 1024-token GPU run the first iteration is the fastest and the third the slowest (cold to warm). In the 16-token runs the first iteration has the longest TTFT. Init and peak memory (the process high-water mark, `VmHWM`) are from the 1024-token run.
| Backend | Prefill (1024 tokens) | Decode (256 tokens) | TTFT (1024-token prompt) | TTFT (16-token prompt) | Init | Peak |
|---|---:|---:|---:|---:|---:|---:|
| GPU (OpenCL, fp16 activations) | 130–264 tok/s | 13.9–20.2 tok/s | 3.93–7.95 s | 0.14–0.15 s | 7.9 s | 1.17 GB |
| CPU (XNNPACK, 4 threads) | 47–65 tok/s | 7.2–7.4 tok/s | 15.78–22.13 s | 0.40–1.29 s | 2.8 s | 4.08 GB |
### Accuracy note
Measured on GSM8K (n=100, greedy, 0-shot chain-of-thought, **max_tokens 2048**, identical prompt
and answer-extraction for every row).
| Configuration | GSM8K |
|---|---|
| bf16 (reference) | 90.0% |
| **LiteRT int4 — block 128** | **86.0%** (−4 pt) |
int4 is at parity (−4 pt). Note: evaluating a reasoning model at a short token budget badly
understates int4 — the longer int4 reasoning chains get truncated before the answer; benchmark
reasoning models with `max_tokens` ≥ 2048.
**Why block 128 (and not block 32)?** For this reasoning model the block-32 build degraded more
(−9 pt) and produced corrupted output under the iPhone GPU delegate, while **block 128 is robust
on every backend, ~40 % faster to decode** (¼ the dequant scales — which matters when generating
long `` chains), and stays at −4 pt parity. So block 128 is the build we recommend; the block-32 file is also published (see **Artifact (Block 32)** above), and both bundles run fully delegated on the Galaxy S26 GPU.
The block-32 file's GPU corruption is not iPhone-specific: on **macOS Metal** it degenerates too (question-echo loops, truncated reasoning; measured 2026-08-31 on the file both before and after the metadata repair, so it is pre-existing and unrelated to the repair). **Use the block-32 file on CPU**; on Android GPU it delegated and generated in the Galaxy S26 gate above.
## Galaxy S26 — GPU backend
Both published bundles run on the Android GPU backend and generate.
**`model.litertlm` (the 2026-10-06 file).** On a **Samsung Galaxy S26** (SM-S942Q, Android 16) with `litert_lm_advanced_main` from a LiteRT-LM main build of 2026-09-18, the three transformer signatures run fully on the OpenCL GPU delegate (`LITERT_CL`), one partition each:
| signature | nodes on `LITERT_CL` |
|---|---:|
| `prefill_128` | 1707 / 1707 |
| `prefill_16` | 1707 / 1707 |
| `decode` | 1631 / 1631 |
XNNPACK takes 1 of the 4 nodes in `decode_embedder` and 1 of the 4 nodes in `prefill_embedder_128`. On the GPU the file answered 5 of 5 gate prompts correctly. Speed and peak memory are in the Galaxy S26 table under Performance.
**Block-32 file.**
| file | GPU backend | delegation | peak |
|---|---|---|---:|
| `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` | runs | 3764 / 3764 ops across 3 subgraphs on `LiteRT GPU` | 1627 MB |
Measured on a **Samsung Galaxy S26** (SM-S942Q / SM8850, Android 16) with `litert_lm_advanced_main` from litert-lm 0.16.0, `--backend=gpu --sampler_backend=cpu`, prompt `What is the capital of France?`. Peak is the process high-water mark (`VmHWM`) sampled during that same run. Gated 2026-08-24.
The op counts above are the `LiteRT GPU` partitions.
GPU wiring, including the Gallery import toggle: [GPU guide](https://github.com/john-rocky/hf-to-litertlm/blob/main/docs/android-gpu.md).
## Usage
```bash
# build litert-lm from https://github.com/google-ai-edge/litert-lm, then:
litert_lm_main \
--model_path model.litertlm \
--backend gpu \
--input_prompt "A bat and a ball cost \$1.10. The bat costs \$1.00 more than the ball. How much is the ball?"
```
The `.litertlm` bundle carries the tokenizer and prompt template (Qwen3 ChatML —
`<|im_start|>role\n…<|im_end|>`, stop token `<|im_end|>`), so no separate tokenizer files are
needed. The model produces a `…` block followed by its answer.
## Run on Android
> **Update (July 2026):** [Google AI Edge Gallery](https://github.com/google-ai-edge/gallery) **v1.0.16+** can import litert-lm models **directly from Hugging Face** inside the app (tap **+**) — no computer or `adb` needed. The manual steps below are only required on older builds or for sideloading a local file.
The official **[Google AI Edge Gallery](https://github.com/google-ai-edge/gallery)** app runs
`.litertlm` models on-device:
1. Install a **recent** Gallery (package `com.google.ai.edge.gallery`, 1.0.15+ supports `.litertlm`).
2. Download `model.litertlm` and push it: `adb push model.litertlm /sdcard/Download/`
3. In the app tap **+**, pick the file, choose the **GPU** backend, and raise the max-tokens setting (≥2048).
4. Chat — the bundle already carries the tokenizer and Qwen3 chat template.
A 4B int4 build needs ~2.5 GB free RAM; reboot the phone first if memory is tight.
In a Flutter app, [flutter_edge_ai_litertlm](https://pub.dev/packages/flutter_edge_ai_litertlm) (1.14.0) installs this repo's bundle through `FlutterEdgeAi.resolveHuggingFace('litert-community/Qwen3-4B-Thinking-2507')`, which reads the repo's `litertlm_manifest.json` and picks the file and backend for the device; in React Native, [react-native-litert-lm](https://www.npmjs.com/package/react-native-litert-lm) (0.8.0) does the same through `resolveFromManifest()`. Neither route is device-verified by this card; each package's README states its tested platforms.
## Run on desktop (LiteRT-LM CLI)
The same `.litertlm` bundle runs on macOS / Linux / Windows with the official
[LiteRT-LM CLI](https://github.com/google-ai-edge/LiteRT-LM) — including as a
local **OpenAI-compatible API server**:
```bash
pip install litert-lm
litert-lm import --from-huggingface-repo litert-community/Qwen3-4B-Thinking-2507 model.litertlm qwen3-4b-thinking-2507
litert-lm run qwen3-4b-thinking-2507 # interactive chat in the terminal
litert-lm serve
```
## Run on iPhone
Verified on **iPhone 17 Pro** (LiteRT-LM Swift runtime): loads and generates at ~14 tok/s (previous file).
## Conversion
Converted with the **official** [`litert-torch`](https://github.com/google-ai-edge/litert-torch)
converter — a standard `Qwen3ForCausalLM`, so it uses the existing Qwen3 path with no custom graph
code. Recipe: **blockwise-128 int4 + OCTAV** (INT4 weights, block 128, symmetric, OCTAV
optimal-clipping), embeddings INT8, KV cache 4096.
```python
from litert_torch.generative.export_hf.export import export
export(
model="Qwen/Qwen3-4B-Thinking-2507",
output_dir="out",
quantization_recipe="qwen3_int4_block128_octav.json", # blockwise-128 int4 + OCTAV, int8 embeddings
cache_length=4096,
externalize_embedder=True,
)
```
**Graph rewrite (2026-10-06).** The current `model.litertlm` was built from the previous file (Hub revision `92751be3c031`) with two tools in `tools/gpu_graph/` of [hf-to-litertlm](https://github.com/john-rocky/hf-to-litertlm):
```bash
python tools/gpu_graph/gpu_graph_retrofit.py previous.litertlm retrofit.litertlm --decomp exporter --mask add_bcast
python tools/gpu_graph/prefill_bucket_clone.py build retrofit.litertlm new.litertlm --lengths 16 --source prefill_128 --report bucket.json
```
`previous.litertlm` is the previous file, `new.litertlm` the result published here as `model.litertlm`; `LITERT_LM_CLI` names the `litert-lm` CLI the scripts use to unpack and pack the bundle.
`gpu_graph_retrofit.py` changes only the prefill/decode TFLite graph, and every original weight buffer keeps its bytes. With `--mask add_bcast`, the attention mask is applied as an ADD on a [bk, g, T, C] view of the logits, with the FLOAT32 mask broadcast. `prefill_bucket_clone.py` copies an existing prefill signature (`prefill_128`) at a new length (16); the copy shares every weight buffer, so the file grows by graph structure only. A rebuild with these tools has every bundle section byte-identical to the shipped file. Only the bundle header (uuid and timestamp written by `litert-lm pack`) differs, so the whole-file sha256 differs.
## Raspberry Pi 5 (CPU) (previous file)
The `model.litertlm` row was measured on the previous file (sha256 `356b2d7778d27d55fbb0d2e0c5e8801e9de136db59849c41c882bd97333840d5`) and not re-measured after the 2026-10-06 graph rewrite; the weights are byte-identical in the new file, and the new file's Mac CPU speed is ×0.997 (decode) and ×1.002 (prefill) of the previous file's, so the row is expected to hold.
Measured on a Raspberry Pi 5 Model B Rev 1.1 (8 GB, Raspberry Pi OS 64-bit) with [`litert-lm benchmark`](https://github.com/google-ai-edge/LiteRT-LM) 0.16.1: CPU backend, 4 threads, 256 prefill + 256 decode tokens, `--cache memory` (the compile cache lives and dies with the process, so every invocation compiles the model from scratch; nothing is reused between runs), one warm-up plus one timed iteration per invocation, 3 invocations per file with cooldown in between. Values are the median across invocations (min–max in parentheses). No thermal throttling occurred during these runs (`vcgencmd get_throttled` stayed `0x0`). Every file listed produced coherent text in a real generation on this backend before its numbers were recorded.
| File | Prefill (tok/s) | Decode (tok/s) | TTFT | Peak RSS |
|---|---:|---:|---:|---:|
| `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` | 8.9 (8.8–9.0) | 1.4 (1.4–1.4) | 29.4 s | 4.4 GB |
| `model.litertlm` | 10.6 (10.5–10.7) | 1.5 (1.5–1.5) | 25.5 s | 3.9 GB |
## Known runtime issues
**External weights (since 2026-10-30).** In `model.litertlm` and `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm`, the weights sit in TFLiteWeights sections at the end of the file (LiteRT's external weights) instead of inside the model graph. On Android, the GPU path needs LiteRT-LM 0.19.0 or later: the GPU engine of the v0.18.0 and v0.18.1 Android builds crashes (SIGSEGV) on such a file ([LiteRT-LM#3986](https://github.com/google-ai-edge/LiteRT-LM/issues/3986)). The earlier files with the weights inside the graph stay at revision [`9df72e0`](https://huggingface.co/litert-community/Qwen3-4B-Thinking-2507/tree/9df72e073feaaeaa00911d9288ddd39f74608272).
The bundle's chat template opens a thought in every answer, so after each answer the runtime rebuilds the cache from the re-rendered history at the next turn: the second turn is free of the defect below when the conversation opens with a user message, and the same source shows a zero embedding at another token from the third turn on (not measured). The defect: after an answer without a thought (no `` … `` block), from the second turn of a conversation on, LiteRT-LM puts the previous answer's stop token into the KV cache with a zero embedding, because `model.litertlm` holds the embedding table in its own section and the decoder takes embeddings, so later answers in a conversation can differ from the answer the model gives to the same messages in one prompt (read in the source of LiteRT-LM 0.16.0 to 0.18.1; measured with 0.18.1 on WeVisOne-VL-2B and Qwen2.5-Coder-1.5B-Instruct, not on this file). `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` has no embedder section (the decoder looks the tokens up itself) and is not affected. One question per conversation avoids both, as does a new conversation that starts with the earlier messages and the new question.
From a conversation preface (the system message, initial messages, tools and extra context a conversation is created with) that has tools and no messages, LiteRT-LM renders the `Qwen3_4b_thinking_dynamic_wi4b32_afp32.litertlm` file's chat template with an empty message list, and the template reads `messages[0]` without checking that the list is non-empty, so the render fails with `Failed to apply template: undefined value`. This happens in two places: when the preface is prefilled at creation (`prefillPrefaceOnInit = true` in Kotlin, `prefillPrefaceOnInit: true` on the web, `SetPrefillPrefaceOnInit(true)` in C++; the default is false), where creating the conversation fails with that error, and in the preface render (`renderPrefaceIntoString()` in Kotlin and Swift, `RenderPrefaceIntoString()` in C++, `litert_lm_conversation_render_preface_to_string` in the C API). The same happens with extra context (such as `enable_thinking`) and no messages, and in the preface render with an empty preface (run in the runtime on one Qwen3 template; the other templates rendered with minijinja 2.14 and jinja2). Read in the source of LiteRT-LM 0.17.0, 0.18.1 and main; measured with LiteRT-LM 0.18.1 (the litert-lm package from PyPI) through its C API on this file's template text. The default path, where the first message is rendered together with the preface, is not affected: the Python API and the CLI of 0.18.1 expose neither the flag nor the preface render, and Google AI Edge Gallery sets neither (its source as read on 2026-10-25). A system message or any initial message in the conversation config avoids it. `model.litertlm` carries no Jinja chat template and is not affected: the runtime's qwen3 template only loops over the messages.
## License
Apache-2.0, inherited from the base model
[Qwen/Qwen3-4B-Thinking-2507](https://huggingface.co/Qwen/Qwen3-4B-Thinking-2507).