Update LlmMetadata for litert-lm 0.18.0: chat template with content parts, supports_thinking / supports_function_calling (weights unchanged)

#6
Files changed (3) hide show
  1. MiniCPM5-2B_int4.litertlm +1 -1
  2. README.md +55 -21
  3. litertlm_manifest.json +42 -8
MiniCPM5-2B_int4.litertlm CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9858563beafbc6d5e0d25fcee3827541515296a9302ed3d088b16a58d4fbe7b8
3
  size 1553670064
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05b78bb57dd5ded8a76907cc953cbc57d6261ee9fa66fbfca21ae677a897da45
3
  size 1553670064
README.md CHANGED
@@ -25,17 +25,17 @@ This repository hosts the [**LiteRT-LM**](https://ai.google.dev/edge/litert-lm)
25
 
26
  ## Available Models
27
 
28
- Recommend to use below both **CPU and GPU compatible** models. The int8 file is built with the LiteRT-LM [`models/minicpm5` conversion command](https://github.com/google-ai-edge/LiteRT-LM/blob/main/models/minicpm5/README.md#model-conversion) (`--quantization_recipe=dynamic_wi8_afp32`); the int4 file is originally from [mlboydaisuke/MiniCPM5-2B-LiteRT](https://huggingface.co/mlboydaisuke/MiniCPM5-2B-LiteRT).
29
- **Requires litert-lm ≥ 0.16** (thought channel + `ThinkingConfig`); measured here on litert-lm 0.17.0 (Mac) and the v0.16.0 release kit (Galaxy S26).
30
 
31
  | File | Recipe | Size |
32
  |---|---|---|
33
  | **`MiniCPM5-2B_int4.litertlm`** | int4 blockwise-32 + OCTAV on linears, int8 embedding | 1.55 GB |
34
  | **`MiniCPM5-2B_int8.litertlm`** | int8 dynamic (channelwise) on linears, embedding and lm_head — `dynamic_wi8_afp32` | 2.55 GB |
35
 
36
- The **int4 file is the phone file** (smaller, fastest GPU decode on every platform measured) — best used for direct answers or short reasoning; see the thinking-mode note below. **int8 is the file for reasoning that has to complete**: its thinking chains are ~3–4× shorter than int4's on the same questions and terminate where int4 runs into the token budget. int8's main weight section is 2.54 GB, more than an iOS app maps in one piece with the default entitlements; an app that carries the `com.apple.developer.kernel.increased-memory-limit` entitlement lifts that limit (not checked with this file).
37
 
38
- Both files declare the `thought` channel, so the runtime's `ThinkingConfig` and the reasoning/answer split work on both. They carry **different chat templates**, and that changes the thinking default: the int4 file embeds the checkpoint's own `chat_template.jinja` (the model decides, and in practice thinks); the int8 file embeds the LiteRT-LM [`models/minicpm5/chat_template.jinja`](https://github.com/google-ai-edge/LiteRT-LM/blob/b5e34ab1/models/minicpm5/chat_template.jinja) as of 2026-09-01 (`enable_thinking` defaults to **false**: direct answers unless the app turns thinking on). Details under *Controlling thinking* and *Conversion notes*.
39
 
40
  Further, below are some CPU-only models for exploration:
41
  * `minicpm_wi4c_wi8_afp32.litertlm`: This model features mixed INT4/INT8 weight-only quantization with FP32 activations (afp32). MLP projections use channelwise INT4 with Hadamard rotation; all remaining weights (attention, embedding, and lmhead) use channelwise INT8.
@@ -95,51 +95,61 @@ litert-lm run ./MiniCPM5-2B_int4.litertlm --backend gpu --cache no --prompt "...
95
 
96
  ### Controlling thinking
97
 
98
- The two files start from opposite defaults, because they carry different templates (see *Conversion notes*):
99
 
100
- - **int4** (the checkpoint's own template): thinking is the model's **default** — with no `ThinkingConfig` it decides for itself and, in practice, reasons before every answer (100–700 characters on trivial questions, thousands on math).
101
- - **int8** (the LiteRT-LM canonical template): with nothing set the template closes the think block itself, so the model gives **direct answers**; `enable_thinking=true` turns the reasoning on.
102
 
103
  For both files:
104
 
105
  - **Give a reasoning run a generous output budget (≥ 2048 tokens; 4096 for math).** Truncated mid-thought it produces no final answer at all.
106
- - Both bundles declare the `thought` channel (`<think>` / `</think>`), so the streamed text contains only the answer, and runtimes that expose `ThinkingConfig` can cap or read the reasoning separately.
107
- - `enable_thinking` set via `ThinkingConfig` or the conversation's extra context reaches either template: `false` switches to direct answers (two- to seven-token replies on the gate questions, ~10× faster turns, and the GSM8K numbers below); `true` pre-fills the think opener explicitly. On the CLI: `--thinking true|false` / `--thinking-budget N`.
108
  - Sampling: OpenBMB recommends `temperature 1.0, top_p 0.95`; the gates below are greedy.
109
  - Prompt format is ChatML (`<|im_start|>role\n…<|im_end|>\n`), 4096-token KV budget. Prefill signatures: int4 six (1024, 256, 64, 16, 4, 1); int8 one (128 — a longer prompt is prefilled in 128-token chunks).
110
 
111
  ## Correctness
112
 
113
- On an 8-question sanity gate (Apple M4 Max, greedy, CPU and GPU) the **int4 file scores 8/8 at its default** (the model thinks; the reasoning arrives on the thought channel, only the answer in the streamed text) and the **int8 file scores 6/8 at its default** (direct answers; measured with nothing set on the GPU and with thinking off on both backends) and **8/8 with thinking on**, on both backends. The two thinking-off misses are the bf16 PyTorch model's own: it scores 8/8 with thinking on or left to the model and **6/8 with thinking forced off** ("opposite of hot" → "Cool.", the rhyme line → "Green."), so read any thinking-off result against that, not against 8/8.
114
 
115
  **GSM8K** (first 100 test questions, greedy, 0-shot chain-of-thought prompt, **thinking off** — the protocol OpenBMB's own MiniCPM5 cards use, max 2048 new tokens, identical prompt and extraction on every row):
116
 
117
  | Configuration | GSM8K |
118
  |---|---|
119
  | bf16 PyTorch (MPS), upstream template | **92 %** |
120
- | int8, GPU (Metal, WebGPU delegate) | **93 %** |
121
  | int4, CPU | 86 % |
122
- | int4, GPU (Metal) | 87 % |
123
 
124
  int8 is at parity: 93 vs 92 is inside one standard error at n=100 (about ±3 points), and six of its seven misses are the bf16 model's own; the seventh (question 8) is a derivation that runs to the 2048-token budget mid-calculation on the GPU, so the extracted number is wrong, while the CPU run on the same file answers it in 2,204 characters. int4 costs about five points on this 42-layer model; the GPU's default fp16 activations cost nothing measurable in no-think mode.
125
 
126
- **Thinking mode is where int4 shows its damage.** On ten GSM8K questions with thinking on and a 3584-token budget, the bf16 model closes its reasoning on 9/10 with ~3,000-character chains, and int8 closes **10/10 with ~3,900-character chains (10/10 correct; Metal GPU through the WebGPU delegate, runtime-default fp16 activations)**, while **int4 closes 0/10 on CPU** (median ~13,700 characters — it keeps re-checking and runs into the budget; the answer is usually right inside the thought text but never gets emitted). On the Metal GPU with the runtime's default fp16 activations the same int4 file happened to close 10/10 with ~8,000-character chains, but that is fp16 rounding steering the trajectory, not a property to rely on. If your use needs the reasoning to finish, use int8 or turn thinking off on int4.
127
 
128
  On a **Galaxy S26 (SM-S942Q, Snapdragon SM8850, Adreno)** both files generate correctly on GPU and CPU; on the GPU with **full OpenCL delegation — every node of every prefill and decode signature, zero rejected ops** (int4: 1873/1873 on the 1024- to 4-token prefills, 1664/1664 on the 1-token prefill, 1692/1692 on decode; int8: 1836/1836 on the 128-token prefill, 1655/1655 on decode) — and the runtime separates the reasoning on-device (`[thought] … [/thought]`, then the answer).
129
 
130
- On an **iPhone 17 Pro** the int4 file passes the same 8-question gate on both backends (on-device byte count verified against the source file): **Metal GPU 7/8** (init 5.7 s) and **CPU 7/8** (init 2.2 s); the one miss on each leg is the rhyme line inside the 8-question composite prompt, answered "green" — the same answer the bf16 model gives with thinking off, so a prompt-format artifact rather than conversion damage.
131
 
132
- Multi-turn: three-turn conversations (introduce a name and a city, an arithmetic question, then "which city do I live in?") hold on both files under all three thinking modes with the name and city recalled — each template's history rendering stays consistent with what the runtime already streamed.
133
 
134
  ## Performance
135
 
 
 
 
 
 
 
 
 
 
 
 
136
  `litert-lm benchmark` (litert-lm 0.17.0), Apple M4 Max, `-p 256 -d 256 --runs 3 --cache no --max-num-tokens 1024`, quiet machine, serialized, ≥300 s rest before each GPU reading; each backend confirmed to generate real text before its number was recorded; all four rows from one session (2026-09-13):
137
 
138
  | File | Backend | Prefill (256) | Decode | TTFT | Init |
139
  |---|---|---|---|---|---|
140
- | int4 | **GPU (Metal)** | **1836 tok/s** | **93.0 tok/s** | 0.15 s | 3.8 s |
141
  | int4 | CPU | 148 tok/s | 31.4 tok/s | 1.77 s | 4.4 s |
142
- | int8 | **GPU (Metal)** | **2084 tok/s** | **124.2 tok/s** | 0.13 s | 1.8 s |
143
  | int8 | CPU | 175 tok/s | 37.1 tok/s | 1.49 s | 4.7 s |
144
 
145
  Galaxy S26 (SM-S942Q, Snapdragon SM8850, Adreno; `litert_lm_advanced_main` from the litert-lm v0.16.0 release kit, 205-token prompt with `--benchmark`, 2 runs per cell, ranges shown; a reasoning model decodes its own full response, so decode-turn lengths vary):
@@ -165,14 +175,38 @@ The two files come from two different conversion paths.
165
 
166
  **int4 — a blockwise recipe with two pieces of handling** (reproduction: [hf-to-litertlm](https://github.com/john-rocky/hf-to-litertlm), `bash scripts/reproduce_llm.sh minicpm5-2b`; litert-torch 0.9.3 / litert-converter 0.4.0 / ai-edge-quantizer 0.9.0 / litert-lm-builder 0.16.1):
167
 
168
- - **The chat template is the checkpoint's `chat_template.jinja`, byte for byte**, embedded on the runtime's jinja path — the same packaging as [litert-community/MiniCPM5-1B](https://huggingface.co/litert-community/MiniCPM5-1B). That is what keeps `enable_thinking` and the tool-calling format available to the app; the `thought` channel is declared alongside it.
169
- - **Start token `<s>`** is correct for this family: the template's own `{{ bos_token }}` renders empty at runtime and the engine prepends the metadata start token, so the model sees exactly one `<s>` as it does upstream. Stops are the model's `</s>` and `<|im_end|>`.
170
  - **int4 needed a zero-scale fix.** Decoder layer 0's MLP contains 13 dead rows (every weight below 3e-23); blockwise quantization emits a zero scale for each of their blocks, which the CPU (XNNPACK) path refuses to load (`unsupported scale value (0.000000) … for INT4 tensor`) while the GPU path silently accepts. The scales were replaced in place by a tiny positive epsilon — 3,328 bytes of a 1.55 GB file.
171
  - The 130560×2048 input-embedding table sits in its own section (embedder externalised, int8); linears are blockwise-32 int4 with OCTAV clipping; GPU activations at the runtime default (fp16). A block-128 int4 variant was built and rejected: it loses 8 GSM8K points (79 vs 87, thinking off), fails the 8-question gate on CPU, and does not shorten the thinking chains.
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  ## Machine-readable manifest
174
 
175
- `litertlm_manifest.json` in this repo describes both recommended files (sha256, size, sections, context length, verified backends, the measured rows above with their conditions, and known issues) for tooling that picks a file per device.
176
 
177
  ## Links
178
 
@@ -187,7 +221,7 @@ The two files come from two different conversion paths.
187
 
188
  Released under the **Apache-2.0 License**, consistent with the upstream [openbmb/MiniCPM5-2B](https://huggingface.co/openbmb/MiniCPM5-2B).
189
 
190
- **Changes from the original work** (for the two recommended files): weights converted from safetensors bf16 to LiteRT flatbuffers and quantized as described above; in the int4 file, 13 dead weight rows' quantization scales set to an epsilon (measured inert: GSM8K 87 with the identical miss list on a copy whose payload under those scales is zeroed); tokenizer and chat template repackaged into the `.litertlm` bundle with runtime metadata (thought channel, stop tokens; the int8 file carries the LiteRT-LM canonical template instead of the checkpoint's). No fine-tuning.
191
 
192
  ## Citation
193
 
 
25
 
26
  ## Available Models
27
 
28
+ Recommend to use below both **CPU and GPU compatible** models. The int8 file is built with the LiteRT-LM [`models/minicpm5` conversion command](https://github.com/google-ai-edge/LiteRT-LM/blob/main/models/minicpm5/README.md#model-conversion) (`--quantization_recipe=dynamic_wi8_afp32`); the int4 file is my conversion (originally from mlboydaisuke/MiniCPM5-2B-LiteRT), and its metadata is LiteRT-LM's official [models/minicpm5](https://github.com/google-ai-edge/LiteRT-LM/tree/4a363c07/models/minicpm5) configuration (weights unchanged).
29
+ The int4 file needs litert-lm 0.18.0 or newer (tested on 0.18.0), because its template reads message content as a list of parts. The int8 file needs litert-lm 0.16 or newer (thought channel + `ThinkingConfig`, the minicpm5 model type; 0.15.0 cannot create a conversation with it).
30
 
31
  | File | Recipe | Size |
32
  |---|---|---|
33
  | **`MiniCPM5-2B_int4.litertlm`** | int4 blockwise-32 + OCTAV on linears, int8 embedding | 1.55 GB |
34
  | **`MiniCPM5-2B_int8.litertlm`** | int8 dynamic (channelwise) on linears, embedding and lm_head — `dynamic_wi8_afp32` | 2.55 GB |
35
 
36
+ The **int4 file is the phone file** (the smaller one) — best used for direct answers or short reasoning; see the thinking-mode note below. **int8 is the file for reasoning that has to complete**: its thinking chains are ~3–4× shorter than int4's on the same questions and terminate where int4 runs into the token budget. int8's main weight section is 2.54 GB, more than an iOS app maps in one piece with the default entitlements; an app that carries the `com.apple.developer.kernel.increased-memory-limit` entitlement lifts that limit (not checked with this file).
37
 
38
+ Both files declare the `thought` channel as `<think>\n` / `</think>`, so the runtime's ThinkingConfig and the reasoning/answer split work on both. Both carry the LiteRT-LM models/minicpm5 chat template, so both answer directly unless `enable_thinking` is true. The int4 file used to leave the choice to the model and, in practice, thought before every answer. Both declare the model type minicpm5, so the runtime parses the model's `<function name=…>` tool calls. Details under Controlling thinking and Conversion notes.
39
 
40
  Further, below are some CPU-only models for exploration:
41
  * `minicpm_wi4c_wi8_afp32.litertlm`: This model features mixed INT4/INT8 weight-only quantization with FP32 activations (afp32). MLP projections use channelwise INT4 with Hadamard rotation; all remaining weights (attention, embedding, and lmhead) use channelwise INT8.
 
95
 
96
  ### Controlling thinking
97
 
98
+ Both files start from the same default, because both carry the LiteRT-LM models/minicpm5 template:
99
 
100
+ - int4 and int8: with nothing set the template closes the think block itself, so the model gives direct answers; `enable_thinking=true` pre-fills the think opener and turns the reasoning on.
 
101
 
102
  For both files:
103
 
104
  - **Give a reasoning run a generous output budget (≥ 2048 tokens; 4096 for math).** Truncated mid-thought it produces no final answer at all.
105
+ - Both bundles declare the thought channel (`<think>\n` / `</think>`), so the streamed text contains only the answer, and runtimes that expose ThinkingConfig can cap or read the reasoning separately.
106
+ - `enable_thinking` set via `ThinkingConfig` or the conversation's extra context reaches either template: `false` switches to direct answers (two- to nine-token replies on the gate questions, ~10× faster turns, and the GSM8K numbers below); `true` pre-fills the think opener explicitly. On the CLI: `--thinking true|false` / `--thinking-budget N`.
107
  - Sampling: OpenBMB recommends `temperature 1.0, top_p 0.95`; the gates below are greedy.
108
  - Prompt format is ChatML (`<|im_start|>role\n…<|im_end|>\n`), 4096-token KV budget. Prefill signatures: int4 six (1024, 256, 64, 16, 4, 1); int8 one (128 — a longer prompt is prefilled in 128-token chunks).
109
 
110
  ## Correctness
111
 
112
+ On an 8-question sanity gate (Apple M4 Max, greedy, CPU and GPU) the int4 file, on litert-lm 0.18.0, scores 6/8 at its default (direct answers) on both backends. With thinking on it scores 7/8 on CPU and 8/8 on GPU (WebGPU on Metal: every text-decoder node delegated, the two embedder graphs on CPU). The int8 file scores 6/8 at its default (direct answers) and 8/8 with thinking on, on both backends. At its default on litert-lm 0.18.0 the int4 file misses two questions on both backends ("opposite of hot" → "Cool.", the rhyme line → "Blues."). The two thinking-off misses are the bf16 PyTorch model's own: it scores 8/8 with thinking on or left to the model and 6/8 with thinking forced off ("opposite of hot" → "Cool.", the rhyme line → "Green."), so read any thinking-off result against that, not against 8/8.
113
 
114
  **GSM8K** (first 100 test questions, greedy, 0-shot chain-of-thought prompt, **thinking off** — the protocol OpenBMB's own MiniCPM5 cards use, max 2048 new tokens, identical prompt and extraction on every row):
115
 
116
  | Configuration | GSM8K |
117
  |---|---|
118
  | bf16 PyTorch (MPS), upstream template | **92 %** |
119
+ | int8, GPU (WebGPU on Metal) | **93 %** |
120
  | int4, CPU | 86 % |
121
+ | int4, GPU (WebGPU on Metal) | 87 % |
122
 
123
  int8 is at parity: 93 vs 92 is inside one standard error at n=100 (about ±3 points), and six of its seven misses are the bf16 model's own; the seventh (question 8) is a derivation that runs to the 2048-token budget mid-calculation on the GPU, so the extracted number is wrong, while the CPU run on the same file answers it in 2,204 characters. int4 costs about five points on this 42-layer model; the GPU's default fp16 activations cost nothing measurable in no-think mode.
124
 
125
+ **Thinking mode is where int4 shows its damage.** On ten GSM8K questions with thinking on and a 3584-token budget, the bf16 model closes its reasoning on 9/10 with ~3,000-character chains, and int8 closes **10/10 with ~3,900-character chains (10/10 correct; GPU, WebGPU on Metal, runtime-default fp16 activations)**, while **int4 closes 0/10 on CPU** (median ~13,700 characters — it keeps re-checking and runs into the budget; the answer is usually right inside the thought text but never gets emitted). On the GPU (WebGPU on Metal) with the runtime's default fp16 activations the same int4 file happened to close 10/10 with ~8,000-character chains, but that is fp16 rounding steering the trajectory, not a property to rely on. If your use needs the reasoning to finish, use int8 or turn thinking off on int4.
126
 
127
  On a **Galaxy S26 (SM-S942Q, Snapdragon SM8850, Adreno)** both files generate correctly on GPU and CPU; on the GPU with **full OpenCL delegation — every node of every prefill and decode signature, zero rejected ops** (int4: 1873/1873 on the 1024- to 4-token prefills, 1664/1664 on the 1-token prefill, 1692/1692 on decode; int8: 1836/1836 on the 128-token prefill, 1655/1655 on decode) — and the runtime separates the reasoning on-device (`[thought] … [/thought]`, then the answer).
128
 
129
+ On an **iPhone 17 Pro** the int4 file, with its earlier metadata, passed the same 8-question gate on both backends (on-device byte count verified against the source file): **Metal GPU 7/8** (init 5.7 s) and **CPU 7/8** (init 2.2 s); the one miss on each backend was the rhyme line inside the 8-question composite prompt, answered "green" — the same answer the bf16 model gives with thinking off, so a prompt-format artifact rather than conversion damage.
130
 
131
+ Multi-turn: in a three-turn conversation (introduce a name and a city, an arithmetic question, then "which city do I live in?") the int8 file recalls the city with thinking unset, off and on (litert-lm 0.18.0). At the default (no thinking), a two-turn exchange ("My name is Kenji." / "What is my name?") does not get the name back: on litert-lm 0.18.0 the int4 file calls itself Kenji. With thinking on (0.18.0, 300-token thinking budget), its second reply begins "Hello! You're named Kenji." The int8 file, at its default on 0.18.0, answers the second turn without the name.
132
 
133
  ## Performance
134
 
135
+ Galaxy S26 (SM-S942Q, Android 16), the int4 file as it is now: the prebuilt `litert_lm_advanced_main` from `gs://litert/binaries/latest` (2026-09-18 build), 256 prefill and 64 decode tokens, `--max_num_tokens=1280`. A warm-up process writes the caches, then a second process runs three iterations; each number is the median of iterations 2 and 3. Before each process the phone rested until no CPU frequency cap was in force (at most 90 s).
136
+
137
+ | File | Backend | Init, cache present | Init, first start (no cache) | Prefill (256) | Decode |
138
+ |---|---|---|---|---|---|
139
+ | int4 | GPU (OpenCL) | 8.97 s | 11.91 s | 525.1 tok/s | 24.40 tok/s |
140
+ | int4 | CPU | 5.05 s | 6.00 s | 100.7 tok/s | 19.70 tok/s |
141
+
142
+ Both runs answered "Paris" to a one-word question, and on the GPU every text-decoder subgraph ran on the OpenCL delegate. On the CPU the phone lowers the prime core's clock from about the second second of load, so the CPU row includes capped iterations.
143
+
144
+ The measurements below were taken before the int4 file's metadata update, with the same weights; the int8 file has not changed.
145
+
146
  `litert-lm benchmark` (litert-lm 0.17.0), Apple M4 Max, `-p 256 -d 256 --runs 3 --cache no --max-num-tokens 1024`, quiet machine, serialized, ≥300 s rest before each GPU reading; each backend confirmed to generate real text before its number was recorded; all four rows from one session (2026-09-13):
147
 
148
  | File | Backend | Prefill (256) | Decode | TTFT | Init |
149
  |---|---|---|---|---|---|
150
+ | int4 | **GPU (WebGPU on Metal)** | **1836 tok/s** | **93.0 tok/s** | 0.15 s | 3.8 s |
151
  | int4 | CPU | 148 tok/s | 31.4 tok/s | 1.77 s | 4.4 s |
152
+ | int8 | **GPU (WebGPU on Metal)** | **2084 tok/s** | **124.2 tok/s** | 0.13 s | 1.8 s |
153
  | int8 | CPU | 175 tok/s | 37.1 tok/s | 1.49 s | 4.7 s |
154
 
155
  Galaxy S26 (SM-S942Q, Snapdragon SM8850, Adreno; `litert_lm_advanced_main` from the litert-lm v0.16.0 release kit, 205-token prompt with `--benchmark`, 2 runs per cell, ranges shown; a reasoning model decodes its own full response, so decode-turn lengths vary):
 
175
 
176
  **int4 — a blockwise recipe with two pieces of handling** (reproduction: [hf-to-litertlm](https://github.com/john-rocky/hf-to-litertlm), `bash scripts/reproduce_llm.sh minicpm5-2b`; litert-torch 0.9.3 / litert-converter 0.4.0 / ai-edge-quantizer 0.9.0 / litert-lm-builder 0.16.1):
177
 
178
+ - The metadata is LiteRT-LM's official models/minicpm5 configuration at commit 4a363c07 (`LlmMetadataProto.pbtext` and `chat_template.jinja`, the template as last changed in 801acdef), put in place by the commands under Reproduce the metadata. `llm_model_type` is minicpm5, so the runtime parses the model's tool calls, and the `thought` channel is declared as `<think>\n` / `</think>`. The configuration sets neither capability flag: `describe` prints Supports Function Call NO and Supports Thinking NO for both files, and the tool-call, thinking and tool-result checks ran with that (litert-lm 0.18.0).
179
+ - **Start token `<s>`** is correct for this family: the template writes no BOS of its own and the engine prepends the metadata start token, so the model sees exactly one `<s>` as it does upstream. Stops are the token ids of `</s>` (1) and `<|im_end|>` (130073).
180
  - **int4 needed a zero-scale fix.** Decoder layer 0's MLP contains 13 dead rows (every weight below 3e-23); blockwise quantization emits a zero scale for each of their blocks, which the CPU (XNNPACK) path refuses to load (`unsupported scale value (0.000000) … for INT4 tensor`) while the GPU path silently accepts. The scales were replaced in place by a tiny positive epsilon — 3,328 bytes of a 1.55 GB file.
181
  - The 130560×2048 input-embedding table sits in its own section (embedder externalised, int8); linears are blockwise-32 int4 with OCTAV clipping; GPU activations at the runtime default (fp16). A block-128 int4 variant was built and rejected: it loses 8 GSM8K points (79 vs 87, thinking off), fails the 8-question gate on CPU, and does not shorten the thinking chains.
182
 
183
+ ## Reproduce the metadata
184
+
185
+ The commands below produced the int4 file's current metadata with litert-lm 0.18.0. Run them in bash or zsh with `WORK` set to an empty directory and `OFF` to a directory for the official files. They download the two official files and the int4 file as it was before the update, put the official pbtext and template in place, pack the file and check it.
186
+
187
+ ```sh
188
+ litert-lm() { uvx --from litert-lm==0.18.0 litert-lm "$@"; }
189
+ litert-lm --version
190
+ mkdir -p $OFF/minicpm5 $WORK/src $WORK/out
191
+ curl -fsSL -o $OFF/minicpm5/LlmMetadataProto.pbtext https://raw.githubusercontent.com/google-ai-edge/LiteRT-LM/4a363c0728d4461eaf05c4d87250ef3e90deb035/models/minicpm5/LlmMetadataProto.pbtext
192
+ curl -fsSL -o $OFF/minicpm5/chat_template.jinja https://raw.githubusercontent.com/google-ai-edge/LiteRT-LM/4a363c0728d4461eaf05c4d87250ef3e90deb035/models/minicpm5/chat_template.jinja
193
+ curl -fsSL -o $WORK/src/MiniCPM5-2B_int4.litertlm https://huggingface.co/litert-community/MiniCPM5-2B/resolve/8f5f487a72141710604742866bc6f67afdffc798/MiniCPM5-2B_int4.litertlm
194
+ shasum -a 256 $WORK/src/MiniCPM5-2B_int4.litertlm
195
+ litert-lm unpack $WORK/src/MiniCPM5-2B_int4.litertlm --output-dir $WORK/work
196
+ grep '^max_num_tokens' $WORK/work/LlmMetadataProto.pbtext
197
+ cp $OFF/minicpm5/LlmMetadataProto.pbtext $WORK/work/LlmMetadataProto.pbtext
198
+ grep '^max_num_tokens' $WORK/work/LlmMetadataProto.pbtext
199
+ litert-lm pack $WORK/work --chat-template $OFF/minicpm5/chat_template.jinja --output $WORK/out/MiniCPM5-2B_int4.litertlm
200
+ litert-lm unpack $WORK/out/MiniCPM5-2B_int4.litertlm --output-dir $WORK/chk --chat-template $WORK/chk.jinja && diff $WORK/chk.jinja $OFF/minicpm5/chat_template.jinja
201
+ diff -r -x LlmMetadataProto.pbtext -x model.toml $WORK/work $WORK/chk
202
+ shasum -a 256 $WORK/out/MiniCPM5-2B_int4.litertlm
203
+ ```
204
+
205
+ The two `diff` commands print nothing: the template in the new file equals the official one, and every other section equals the downloaded file's. Running the commands again gives a file whose sections match byte for byte while its sha256 differs: the packer writes a new uuid and creation timestamp into the container header (37 bytes in a test re-run).
206
+
207
  ## Machine-readable manifest
208
 
209
+ `litertlm_manifest.json` in this repo describes both recommended files (sha256, size, sections, context length, verified backends, the earlier measured rows above with their conditions, and known issues) for tooling that picks a file per device. Its int4 entry carries the current file's sha256 (05b78bb5…).
210
 
211
  ## Links
212
 
 
221
 
222
  Released under the **Apache-2.0 License**, consistent with the upstream [openbmb/MiniCPM5-2B](https://huggingface.co/openbmb/MiniCPM5-2B).
223
 
224
+ **Changes from the original work** (for the two recommended files): weights converted from safetensors bf16 to LiteRT flatbuffers and quantized as described above; in the int4 file, 13 dead weight rows' quantization scales set to an epsilon (measured inert: GSM8K 87 with the identical miss list on a copy whose payload under those scales is zeroed); tokenizer and chat template repackaged into the `.litertlm` bundle with runtime metadata (thought channel, stop tokens; both files carry the LiteRT-LM models/minicpm5 template instead of the checkpoint's). No fine-tuning.
225
 
226
  ## Citation
227
 
litertlm_manifest.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "manifest_schema": "0.1.3",
3
  "repo": "litert-community/MiniCPM5-2B",
4
- "generated": "2026-10-07",
5
  "generator": "make_manifest.py",
6
  "model": {
7
  "display_name": "MiniCPM5-2B",
8
  "base_model": "openbmb/MiniCPM5-2B",
9
- "architecture": "Dense LlamaForCausalLM (42L, hidden 2048, GQA 16:2, untied 130560-vocab embeddings, rope_theta 5e6, no rope scaling), 2.52B params; hybrid-reasoning chat template (enable_thinking) with a declared thought channel (the int4 file carries the checkpoint template verbatim, the int8 file the LiteRT-LM canonical one)",
10
  "parameters_b": 2.52,
11
  "license": "apache-2.0",
12
  "context_length": 4096,
@@ -16,7 +16,7 @@
16
  "thinking": {
17
  "declared": true,
18
  "channel": {
19
- "start": "<think>",
20
  "end": "</think>"
21
  },
22
  "control": "switch"
@@ -24,7 +24,7 @@
24
  "channels": [
25
  {
26
  "name": "thought",
27
- "start": "<think>",
28
  "end": "</think>"
29
  }
30
  ]
@@ -33,12 +33,12 @@
33
  "variants": [
34
  {
35
  "file": "MiniCPM5-2B_int4.litertlm",
36
- "sha256": "9858563beafbc6d5e0d25fcee3827541515296a9302ed3d088b16a58d4fbe7b8",
37
  "size_bytes": 1553670064,
38
  "sections": [
39
  {
40
  "type": "LlmMetadataProto",
41
- "size_bytes": 9346
42
  },
43
  {
44
  "type": "HF_Tokenizer_Zlib",
@@ -82,8 +82,8 @@
82
  "requirements": {
83
  "peak_ram_mb": 2122,
84
  "platform_notes": [
85
- "Requires litert-lm >= 0.16 (thought channel + ThinkingConfig); measured on 0.17.0",
86
- "Hybrid-reasoning model: with no ThinkingConfig it thinks before every answer; budget max output tokens >= 2048 (4096 for math). enable_thinking=false (ThinkingConfig or conversation extra_context) switches to direct answers",
87
  "Thinking-mode caveat measured on this file: on GSM8K-class questions its reasoning chains run ~4x longer than int8's and do not close inside 3584 tokens on the CPU reference (0/10 closed; int8 9/10, bf16 9/10). Use int8 when the reasoning must complete, or turn thinking off",
88
  "iPhone 17 Pro: passes the on-device gate on gpu and cpu (init 5.7 s / 2.2 s); the int8 file's 2.54 GB main section needs the com.apple.developer.kernel.increased-memory-limit entitlement on iOS (not checked with that file)",
89
  "The 13 all-zero MLP rows in decoder layer 0 make raw blockwise-int4 exports fail to load on XNNPACK (scale 0); this file carries the epsilon fix"
@@ -124,6 +124,40 @@
124
  "date": "2026-09-13",
125
  "source": "litert-lm benchmark -p 256 -d 256 --runs 3 --cache no --max-num-tokens 1024, >=300 s rest before the GPU cell, backend gated on a real generation first (same session as the int8 rows; the 2026-09-08 reading was 1699 / 92.79)"
126
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  {
128
  "device": "Galaxy S26 (SM-S942Q, Qualcomm SM8850, Adreno)",
129
  "backend": "gpu",
 
1
  {
2
  "manifest_schema": "0.1.3",
3
  "repo": "litert-community/MiniCPM5-2B",
4
+ "generated": "2026-10-10",
5
  "generator": "make_manifest.py",
6
  "model": {
7
  "display_name": "MiniCPM5-2B",
8
  "base_model": "openbmb/MiniCPM5-2B",
9
+ "architecture": "Dense LlamaForCausalLM (42L, hidden 2048, GQA 16:2, untied 130560-vocab embeddings, rope_theta 5e6, no rope scaling), 2.52B params; hybrid-reasoning chat template (enable_thinking) with a declared thought channel (both files carry LiteRT-LM's models/minicpm5 chat template: the int4 file the 4a363c07 revision, the int8 file the b5e34ab1 revision)",
10
  "parameters_b": 2.52,
11
  "license": "apache-2.0",
12
  "context_length": 4096,
 
16
  "thinking": {
17
  "declared": true,
18
  "channel": {
19
+ "start": "<think>\n",
20
  "end": "</think>"
21
  },
22
  "control": "switch"
 
24
  "channels": [
25
  {
26
  "name": "thought",
27
+ "start": "<think>\n",
28
  "end": "</think>"
29
  }
30
  ]
 
33
  "variants": [
34
  {
35
  "file": "MiniCPM5-2B_int4.litertlm",
36
+ "sha256": "05b78bb57dd5ded8a76907cc953cbc57d6261ee9fa66fbfca21ae677a897da45",
37
  "size_bytes": 1553670064,
38
  "sections": [
39
  {
40
  "type": "LlmMetadataProto",
41
+ "size_bytes": 4103
42
  },
43
  {
44
  "type": "HF_Tokenizer_Zlib",
 
82
  "requirements": {
83
  "peak_ram_mb": 2122,
84
  "platform_notes": [
85
+ "Requires litert-lm >= 0.18.0 (the template reads message content as a list of parts; on 0.17.x and older the user turn renders empty); measured on 0.18.0",
86
+ "Hybrid-reasoning model: with no ThinkingConfig it answers directly (the template's default); enable_thinking=true (ThinkingConfig or conversation extra_context) turns the reasoning on; with thinking on, budget max output tokens >= 2048 (4096 for math)",
87
  "Thinking-mode caveat measured on this file: on GSM8K-class questions its reasoning chains run ~4x longer than int8's and do not close inside 3584 tokens on the CPU reference (0/10 closed; int8 9/10, bf16 9/10). Use int8 when the reasoning must complete, or turn thinking off",
88
  "iPhone 17 Pro: passes the on-device gate on gpu and cpu (init 5.7 s / 2.2 s); the int8 file's 2.54 GB main section needs the com.apple.developer.kernel.increased-memory-limit entitlement on iOS (not checked with that file)",
89
  "The 13 all-zero MLP rows in decoder layer 0 make raw blockwise-int4 exports fail to load on XNNPACK (scale 0); this file carries the epsilon fix"
 
124
  "date": "2026-09-13",
125
  "source": "litert-lm benchmark -p 256 -d 256 --runs 3 --cache no --max-num-tokens 1024, >=300 s rest before the GPU cell, backend gated on a real generation first (same session as the int8 rows; the 2026-09-08 reading was 1699 / 92.79)"
126
  },
127
+ {
128
+ "device": "Galaxy S26 (SM-S942Q, Qualcomm SM8850, Adreno)",
129
+ "os": "Android 16",
130
+ "backend": "gpu",
131
+ "runtime": "litert-lm prebuilt litert_lm_advanced_main from gs://litert/binaries/latest (2026-09-18 build, sha256 adac974b...)",
132
+ "prompt_tokens": 256,
133
+ "decode_tokens": 64,
134
+ "prefill_tps": 525.1,
135
+ "decode_tps": 24.4,
136
+ "ttft_s": 0.53,
137
+ "load_s": 8.97,
138
+ "max_num_tokens": 1280,
139
+ "cache": "yes",
140
+ "runs": 3,
141
+ "date": "2026-10-10",
142
+ "source": "the litert benchmark --ddp steps (LiteRT-CLI) run over adb: caches removed, a warm-up process (cold init 11.91 s, writes the cache), then one process of 3 iterations; the row is the median of iterations 2-3 (iterations prefill/decode: 509.5 / 21.74; 528.5 / 24.30; 521.8 / 24.50); --max_num_tokens=1280; the phone rested until no CPU frequency cap was in force before each process; every text-decoder subgraph on the OpenCL delegate; peak RAM 1133 MB (the binary's Peak system ram usage)"
143
+ },
144
+ {
145
+ "device": "Galaxy S26 (SM-S942Q, Qualcomm SM8850, Adreno)",
146
+ "os": "Android 16",
147
+ "backend": "cpu",
148
+ "runtime": "litert-lm prebuilt litert_lm_advanced_main from gs://litert/binaries/latest (2026-09-18 build, sha256 adac974b...)",
149
+ "prompt_tokens": 256,
150
+ "decode_tokens": 64,
151
+ "prefill_tps": 100.7,
152
+ "decode_tps": 19.7,
153
+ "ttft_s": 2.65,
154
+ "load_s": 5.05,
155
+ "max_num_tokens": 1280,
156
+ "cache": "yes",
157
+ "runs": 3,
158
+ "date": "2026-10-10",
159
+ "source": "the litert benchmark --ddp steps (LiteRT-CLI) run over adb: caches removed, a warm-up process (cold init 6.0 s, writes the cache), then one process of 3 iterations; the row is the median of iterations 2-3 (iterations prefill/decode: 98.0 / 21.97; 115.6 / 20.21; 85.8 / 19.18); --max_num_tokens=1280; the phone rested until no CPU frequency cap was in force before each process; the prime core is capped by the phone from about the second second of load, so the CPU iterations include capped samples; peak RAM 2130 MB (the binary's Peak system ram usage)"
160
+ },
161
  {
162
  "device": "Galaxy S26 (SM-S942Q, Qualcomm SM8850, Adreno)",
163
  "backend": "gpu",