TL;DR: for a big-vocab decoder the fp16 lm_head is ~half the per-token memory read — untie it (if
tied) and quantize to int8 (per-block-32 **symmetric / absmax, never clipping) for a large decode win
at no quality cost. Then measure the win on the bundle you actually ship, in the actual app, as a
same-conditions back-to-back A/B — controlled benchmarks, VLM image buffers, thermal, and the app’s own
per-token UI work each move the number by tens of percent.**
A 100k–250k-row lm_head is read on every decoded token, and on a sub-2B transformer that matrix is
roughly half of the per-token weight traffic. Quantizing it int8 ≈ halves that half.
model.lm_head.weight = nn.Parameter(model.lm_head.weight.detach().clone()) → quant config
module_name_configs = {r".*lm_head$": head_quant_spec()}.symmetric (absmax), NOT symmetric_with_clipping. Big-vocab head rows are fat-tailed;
clipping the outliers flips next-token argmax (measured: qwen3.5-2B 6/16 oracle flips with clipping;
absmax keeps cos ≈ 0.9999, argmax == HF). Per-block-32.int8hu mode; most zoo decode exports already carry a head_quant_spec(). See
compression-reference.md and pipelined-engine.md.Worked example: MiniCPM-V-4.6 (qwen3.5-0.8B class), iPhone 17 Pro, int8 head.
image_embeds[64,1024]
buffer every step (extra per-token traffic). Same head, VLM bundle, same-conditions in-app A/B:
51.5 → 70.0 tok/s = +36%. → Measure the bundle you ship, not the text-core proxy.A SwiftUI chat that re-decodes the whole token list with the tokenizer on every token
(tokenizer.decode(fullSequence)) and pushes a view update is O(n²) and serializes against the model.
It dragged both the measured and the experienced rate well below the model’s true decode speed, and got
worse the longer the output (a 343-token reasoning trace measured ~10 tok/s low).
�
mid-character glitches for CJK — a character spans multiple BPE tokens — so the throttle is the safe pick.After a day of repeated 1 GB+ bundle loads + GPU-heavy work, an iPhone can wedge: a model load hangs at
cold-compile and devicectl reports the console connection invalidated. Reboot the device — it clears
the Metal/GPU state and the load returns to normal. Before blaming code, confirm the load path is unchanged (here the only edits
were UI-layer; the engine/backend were untouched) — heavy on-device sessions need an occasional reboot.