The hardware is the argument
Sixteen gigabytes is the binding constraint on everything below. Every -ncmoe,
every KV quantisation choice and every -ub value is bought against it. The rest
of the box matters because expert layers that don't fit on the card get streamed from DDR4 at
~40 GB/s instead of read from VRAM at ~640 GB/s.
| Component | Spec | Why it shows up in the results |
|---|---|---|
| GPU | RX 9070 XT · Navi 48 · gfx1201 · RDNA4 · 16,304 MiB | The binding constraint on this entire page |
| CPU | Ryzen 9 5950X · 16C/32T · 64 MiB L3 · 5.09 GHz | Runs CPU-offloaded expert layers. -t 32 collapses generation to 23.6 ± 4.6 tok/s on SMT contention — leave threads at 16 |
| RAM | 64 GB DDR4-3200 · 4 × 16 GB · dual channel | ~40 GB/s ceiling on every -ncmoe row. Two mismatched kits; the 3600 pair is downclocked to the 3200 SPD |
| Storage | Crucial BX500 1 TB — SATA, ~540 MB/s | Not NVMe. Cold loads cost ~93 s for a 49 GB model, ~30 s for 12.5 GB. Idle-sleep reloads pay it again |
| OS | CachyOS · kernel 7.1.5 | — |
| ROCm | 7.2.53211 · GGML_HIP=ON · AMDGPU_TARGETS=gfx1201 | Wins K-quants by 1.7–2.1× on prompt processing |
| llama.cpp | b10463 · 7c35571e5 · 2026-08-17 | Tool calling was broken on e583f3b4f and is fixed here |
A BIOS update changes none of it — measured. Board moved from BIOS 2407 (2021) to 3636 (2026), roughly four and a half years of AGESA updates. Memory still trains at 3200 on all four DIMMs, and Qwen3.6 re-run under identical conditions moved within noise: prompt up 1–2 %, generation down 1–2 %, both directions. 3200 is not a firmware limitation, it is what four mostly dual-rank DIMMs across two kits will do on this memory controller.
ROCm for everything, and why Vulkan was retired
Measured head-to-head with llama-bench, -fa 1 throughout.
llama-bench defaults flash attention off, but llama-server
resolves auto → enabled, so -fa 0 numbers do not describe real serving —
an earlier revision of this record got that wrong.
| Model | Quant | ROCm pp / tg | Vulkan pp / tg | Use |
|---|---|---|---|---|
| Qwen3.6-35B-A3B (-ncmoe 40 -ub 512) | Q4_K_M | 706.0 / — | 333.4 / 29.3 | ROCm |
| Gemma 4-26B-A4B (-ncmoe 8) | Q4_K_M | 1949.9 / 50.3 | 1064.7 / 48.0 | ROCm |
| GPT-OSS-20B shallow | MXFP4 | 5529.9 / 148.5 | 4952.0 / 180.8 | was Vulkan |
| GPT-OSS-20B @131k depth | MXFP4 | 1035.2 / 70.5 | 1063.7 / 22.3 | ROCm |
| Qwen3.8-27B dense, -ngl 99 | Q3_K_XL | 1329.4 / 30.64 | 1161.8 / 16.91 | ROCm |
| Laguna XS.2 33B-A3B (-ncmoe 16) | Q4_K_M | 1244.4 / 55.28 | 661.7 / 48.19 | ROCm |
K-quants
1.8–2.1×ROCm wins prompt unambiguously, and wins generation too once flash attention is on (Gemma 50.3 vs 48.0).
MXFP4, shallow
57 pp : 1 tgThe crossover ratio. Below it Vulkan was faster overall; above it ROCm. One preset ever collected that advantage.
MXFP4, at depth
3.2×Vulkan's generation collapses to 22.3 tok/s at 131k under VRAM pressure. ROCm holds 70.5 at the same prompt speed.
Vulkan's only remaining advantage was shallow MXFP4 generation on exactly one preset, and
router mode always spawned children from the ROCm binary — so in daily use that advantage was
never actually being collected. build-vulkan/ was dropped on 2026-08-17, trading
18 % of shallow generation on one preset for a single backend with no version skew. It has been
rebuilt twice since, from the same checkout, to spot-check new models: Qwen3.8-27B gives ROCm
+81 % generation and Laguna XS.2 gives it +88 % prompt — the
widest gaps in the table. The routing decision holds.
Attention topology, not parameter count
Three models here are hybrid attention: full_attention_interval = 4
means only every 4th layer keeps a KV cache, the rest carry a constant-size recurrent state that
does not grow with context. That is the whole reason 1M fits in 16 GB. But hybrid attention is
not by itself a promise of cheap context — read head_count_kv and
block_count alongside the interval.
| Model | Layers | Layers with KV | KV heads | B / token (q8_0) | KV @1M |
|---|---|---|---|---|---|
| Nemotron-3-Nano-30B-A3B | 52 | 6 | 2 | 3,264 | 3,264 MiB |
| Qwen3.6-35B-A3B | 40 | 10 | 2 | 10,880 | 10,880 MiB |
| Qwen3-Coder-Next | 48 | 12 | 2 | 13,056 | 13,056 MiB |
| Qwen3.5-9B-Uncensored | 32 | 8 | 4 | 17,408 | — |
| GLM-4.7-Flash (MLA) | 47 | 47 | latent 576 | ≈27,915 | — |
| Qwen3.8-27B | 64 | 16 | 4 | 34,816 | 34,816 MiB |
| Qwen3-Coder-30B-A3B | 48 | 48 | 4 | 52,224 | ~52 GB |
| Devstral Small 2 24B | 40 | 40 | 8 | 87,040 | unreachable |
Qwen3.8-27B is the counter-example that matters. Same
full_attention_interval = 4, but 4 KV heads instead of 2 and 64 layers instead of
40–48 — so 16 layers cache at double the per-layer cost. Confirmed exactly, not estimated:
asking for 131,072 tokens of q8_0 KV fails with
failed to allocate ROCm0 buffer of size 4563402752, which is 4,352 MiB,
precisely 16 × 4 × 256 × 2 × 1.0625 × 131072.
MLA compression beats sparsity. GLM-4.7-Flash caches a single compressed latent per layer rather than per-head K and V — 576 elements per layer per token, applied at every layer. Measured empirically at ≈27,915 B/token, within 3 % of the formula, and still cheaper than Qwen3.8's sparse-but-fat 34,816.
Seventeen models
Everything that has been through the suite. Rows marked deleted are no longer on disk — their measurements stand, and each was removed for a stated reason rather than quietly dropped.
| Model | Total | Active | Type | Quant | Size | Disk |
|---|---|---|---|---|---|---|
| GPT-OSS-20B | 21B | ~3.6B | MoE | MXFP4 | 11.3 GB | yes |
| Qwen3.6-35B-A3B | 35B | ~3B | MoE hybrid, 40L | UD-Q4_K_M | 20.6 GB | yes |
| Gemma 4-26B-A4B | 25.2B | ~3.8B | MoE, 30L | UD-Q4_K_M | 15.8 GB | yes |
| Gemma 4-E2B | ~4.66B | ~4.66B | Dense, 35L | BF16 | 9.3 GB | deleted |
| Qwen3-Coder-Next | 80B | ~3B | MoE hybrid, 48L | UD-Q4_K_M | 49.3 GB | yes |
| Muse Glimmer 30B | 30B | 30B | Dense SWA, 52L | UD-Q3_K_XL | 12.4 GB | yes |
| Qwen3.8-27B | 27B | 27B | Dense hybrid, 64L | UD-Q3_K_XL (Dyn 2.0) | 12.5 GB | yes |
| Qwen3.8-27B | 27B | 27B | Dense hybrid, 64L | UD-Q3_K_XL (Dyn 3.0) | 12.2 GB | yes |
| Qwen3.8-27B | 27B | 27B | Dense hybrid, 64L | UD-IQ4_XS | 13.3 GB | yes |
| Qwen3.8-27B | 27B | 27B | Dense hybrid, 64L | UD-IQ3_XXS | 10.2 GB | yes |
| Qwen3.5-27B-Uncensored | 26.9B | 26.9B | Dense hybrid, 64L | Q3_K_M | 12.4 GB | yes |
| Qwen3.5-9B-Uncensored | 8.95B | 8.95B | Dense hybrid, 32L | Q8_0 | 8.9 GB | yes |
| GLM-4.7-Flash | 30B | ~3B | MoE MLA, 47L | UD-Q4_K_XL | 16.3 GB | yes |
| Laguna XS.2 (Poolside) | 33B | ~3B | MoE hybrid + SWA, 40L | Q4_K_M / Q8_0 | 18.9 / 33 GB | yes |
| Nemotron-3-Nano-30B-A3B | 31.6B | ~3.5B | MoE Mamba2 hybrid, 52L | UD-Q4_K_XL | 22.8 GB | deleted |
| Devstral Small 2 24B | 23.6B | 23.6B | Dense full-attn, 40L | Q4_K_M | 14.3 GB | deleted |
| Instella-MoE-16B-A3B-Think | 15.86B | ~3B | MoE, fork-only arch | Q4_K_M | 9.75 GB | deleted |
Also measured and since removed: Qwen3-Coder-30B-A3B (Q8_0, 32.5 GB), Qwen2.5-Coder-14B and -32B, GPT-OSS-120B (MXFP4, 59 GB).
What actually loads, and the flags it takes
Every row below was actually loaded, confirmed to reach
server is listening, with VRAM read while resident. All include
-ngl 99 unless noted. Contexts are exact token counts, not rounded labels.
How full the card gets
VRAM at load against the 16,304 MiB ceiling · selected presets
Per-model presets
| Model | Context | Extra flags | VRAM MiB | Free |
|---|---|---|---|---|
| Qwen3.6-35B-A3B | 32K | -ncmoe 16 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,932 | 1,372 |
| 128K | -ncmoe 20 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,090 | 2,214 | |
| 256K | -ncmoe 24 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,471 | 1,833 | |
| Qwen3-Coder-Next 80B | 32K | -ncmoe 38 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 13,444 | 2,860 |
| 128K | -ncmoe 40 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 13,181 | 3,123 | |
| 256K | -ncmoe 42 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 13,894 | 2,410 | |
| 1M † | -ncmoe 46 -ub 256 -b 1024 -fa on -ctk q4_0 -ctv q4_0 | 13,874 | 2,430 | |
| Gemma 4-26B-A4B | 32K | -ncmoe 8 | 14,338 | 1,966 |
| 128K | -ncmoe 12 | 14,437 | 1,867 | |
| 256K | -ncmoe 20 | 13,811 | 2,493 | |
| Qwen3.8-27B Q3_K_XL v3 | 32K | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,018 | 2,023 |
| 64K | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,295 | 746 | |
| 128K | -ub 512 -b 2048 -fa on -ctk q4_0 -ctv q4_0 | 15,665 | 376 | |
| Qwen3.8-27B IQ3_XXS | 192K | -ub 512 -b 2048 -fa on -ctk q4_0 -ctv q4_0 | 15,355 | 672 |
| Qwen3.8-27B IQ4_XS | 32K | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,343 | 955 |
| Qwen3.5-9B-Uncensored | 32K | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 9,384 | 6,444 |
| 128K | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 11,587 | 4,181 | |
| 256K native | -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,524 | 1,304 | |
| GLM-4.7-Flash | 32K | -ncmoe 12 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,357 | 1,337 |
| 128K | -ncmoe 20 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,743 | 1,130 | |
| 202,752 native | -ncmoe 28 -ub 512 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,017 | 1,856 | |
| Laguna XS.2 Q4_K_M | 32K | -ncmoe 16 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,418 | 1,886 |
| 128K | -ncmoe 20 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,058 | 1,246 | |
| 256K | -ncmoe 26 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,814 | 490 | |
| Nemotron-3-Nano-30B-A3B | 32K | -ncmoe 24 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,747 | 1,557 |
| 128K | -ncmoe 24 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,060 | 1,244 | |
| 256K | -ncmoe 24 -ub 1024 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 15,723 | 581 | |
| 512K | -ncmoe 28 -ub 512 -b 2048 -fa on -ctk q8_0 -ctv q8_0 | 14,753 | 1,551 | |
| 1M native | -ncmoe 32 -ub 256 -b 1024 -fa on -ctk q4_0 -ctv q4_0 | 14,104 | 2,200 | |
| Devstral Small 2 24B | 32K | -ub 512 -b 2048 -fa on -ctk q4_0 -ctv q4_0 | 15,663 | 641 |
| Gemma 4-E2B BF16 | 131K native | -ub 1024 -b 2048 -fa on | 7,142 | 9,162 |
| Muse Glimmer 30B | 32K | DFlash, -ub 512 | 15,422 | 882 |
| 64K | DFlash, -ub 256 | 15,374 | 930 | |
| 128K | no DFlash | 14,462 | 1,842 |
† Beyond the model's native 262,144 — needs YaRN RoPE scaling, and is not exposed as a preset.
The rejected pile
A config that loads is not the same as a config that works. These were all measured and thrown away, and several look like passes until you check the right column.
| Attempt | Result |
|---|---|
| Qwen3.8-27B 128K, q8_0 KV | fails to allocate — wants 4,352 MiB of KV |
| Qwen3.8-27B 128K, q4_0, -ub 1024 | loads at 16,247 — 57 MiB free |
| Qwen3.8-27B v3 160K, q4_0, -ub 256 | loads at 16,275 — 29 MiB free, 150 MiB already spilled to GTT |
| Qwen3.8-27B v3 32K + MTP | SPILL — free_at_load=28, GTT +12,154 MiB |
| Qwen3.8-27B IQ4_XS 64K, q8_0, -ub 512 | SPILL — free_at_load=9, GTT +2,314 MiB |
| Qwen3.8-27B Q6_K 32K, -ngl 44 | SPILL — reads as a 60 MiB fit; GTT rose 1,623 MiB |
| Qwen3.5-27B-Unc 64K, q8_0, -ub 512 | 15,608 — 55 MiB free, negative at the session's worst baseline |
| GLM-4.7-Flash 32K, -ncmoe 8 | 15,649 — 45 MiB free |
| Nemotron 512K, -ncmoe 24 -ub 512 | loads at 19 MiB free; the first generation request failed |
| Devstral 16K, q8_0, -ub 1024 | 15,800 — 504 MiB free, less room than 32K at q4_0 |
| Qwen3-Coder-Next 1M, q8_0 KV | fails — 13,056 MiB KV plus a ~1.9 GB compute buffer overflows the card |
The spill guard exists because fit.sh lied once. It scored a 160K
config a pass at 2,707 MiB free. It was not a pass: VRAM at load was 16,275 MiB and GTT rose
275 → 425 MiB. Buffers had migrated to host memory, and because the script read peak VRAM
after the probe returned, it measured the post-migration figure. It now samples GTT
alongside VRAM and refuses any row where peak < loaded or GTT drifted past
48 MiB. Any pre-guard row quoting single- or double-digit free VRAM should be treated as
suspect until re-measured.
Speed, and how much of it depth takes back
llama-bench, -r 2, ROCm build. Shallow unless a depth is named.
The interesting axis is not peak tok/s — it is how much survives to 131K, and that varies by
3.3× across the models here.
Generation throughput vs context depth
tok/s · ROCm · -fa 1 · tg64
Expert offload: the -ncmoe sweeps
Monotonic in every case — every expert layer moved onto the GPU helps. The usable value is whatever the target context leaves room for.
| -ncmoe | Qwen3.6-35B pp / tg | Laguna XS.2 pp / tg | GLM-4.7-Flash pp / tg | Gemma 4-26B pp / tg |
|---|---|---|---|---|
| 12 | fails to allocate | — | 1571.9 / 43.3 | 1603.6 / 42.6 |
| 16 | 1974.8 / 43.9 | 2074.9 / 46.7 | 1463.2 / 39.0 | — |
| 20 | 1787.3 / 41.9 | 1846.1 / 43.7 | 1375.6 / 35.9 | 1276.0 / 33.9 |
| 24 | 1616.2 / 39.0 | — | 1318.7 / 32.3 | — |
| 26 / 28 | 1472.6 / 35.8 (28) | 1618.4 / 39.3 (26) | 1243.6 / 28.7 (28) | — |
| 32 | 1368.0 / 35.7 | — | — | — |
| 36 | 1268.4 / 33.4 | — | — | — |
| 40 | 1198.0 / 31.8 | — | — | — |
Laguna XS.2 is faster than Qwen3.6 at every matched value despite an almost
identical file size (18.9 GB vs 20.6 GB) — its 512-wide expert FFN is lighter per active
parameter. GLM-4.7-Flash reaches Qwen3.6's best published generation (43.9 at
-ncmoe 16) at -ncmoe 12, needing four fewer GPU-resident
layers, consistent with a file 4.6 GB lighter.
Ubatch: the most under-used flag, with one big asterisk
| -ub | Qwen3.6 -ncmoe 40, offloaded | Qwen3.8-27B dense, resident | Qwen3.5-27B-Unc | Qwen3.5-9B-Unc |
|---|---|---|---|---|
| 256 | 416.1 | 1180.7 | 1031.5 | 3884.6 |
| 512 default | 706.0 | 1279.7 | 1131.8 | 4270.3 |
| 1024 | 1198.0 | 1306.9 | 1153.6 | 4423.4 |
| gain | +188 % | +11 % | +12 % | +14 % |
This is a claim about CPU-offloaded models, not about ubatch itself. The gain
comes from amortising expert-weight transfers over PCIe, so it scales with how much of the model
lives in system RAM. On a fully GPU-resident model there is nothing to amortise. Check whether
you are actually offloading before paying VRAM for a bigger ubatch — and if you are not,
-ub 256 is a cheap way to buy back compute buffer for context.
Depth sweeps
| Model / preset | d0 | 8K | 16K | 32K | 131K | decay |
|---|---|---|---|---|---|---|
| Gemma 4-26B-A4B -ncmoe 8 | 51.4 | 49.0 | 47.5 | 47.0 | — | 1.09× |
| Qwen3.5-9B-Unc q8_0 | 56.6 | — | — | 49.1 | 35.7 | 2.2× to 262K |
| Nemotron-3-Nano -ncmoe 24 | 47.5 | — | — | 41.7 | 31.7 | 1.50× |
| Qwen3.6-35B-A3B -ncmoe 40 | 31.8 | — | — | 28.8 | 22.2 | 1.43× |
| GLM-4.7-Flash -ncmoe 12 | 42.2 | 39.3 | 35.7 | 29.5 | — | 1.43× |
| Qwen3.8-27B 32K preset, q8_0 | 31.6 | 30.6 | 29.4 | 27.0 | — | 1.17× |
| Qwen3.8-27B 128K preset, q4_0 | 31.6 | 27.8 | 25.1 | 20.8 | 9.6 | 3.3× |
| Qwen3.5-27B-Unc 32K, q8_0 | 29.8 | 28.1 | 26.7 | 24.5 | — | 1.21× |
| Devstral Small 2 24B q4_0 | 36.0 | 32.2 | 28.8 | 24.1 | unreachable | 1.49× in 32K |
| Qwen3-Coder-Next 80B | ~25 | — | — | ~24 | ~22 @256K | 1.10× |
Flattest curve
Gemma 4-26B8.5 % decay across the whole 32K range. Never drops below 47 tok/s inside its window — the answer to "fastest thing that is still a serious 25B at Q4_K_M".
Steepest curve
Qwen3.8-27B3.3× to 131K. It fits, it recalls perfectly, and it is slow enough that you should reach for 32K or 64K unless you genuinely need the window.
Steepest prefill
GLM-4.7-FlashGeneration decays 1.43×, but prompt decays 8.5× (1608 → 190 over 32K). Worth weighing for agentic use, where most of the token cost is prompt.
Fastest overall
GPT-OSS-20B5,530 pp / 180.7 tg shallow, fully GPU-resident at 11.3 GB. The same "Rust backend + React frontend" prompt: 15 s here vs 1 m 55 s on Qwen3-Coder-Next.
Does the synthetic curve transfer to real serving?
Twice now, yes. One turn of qwen3.8-32k through Open WebUI with web search
(prompt_n 15065, cache_n 11198, so roughly 11K–26K of depth) measured
798.4 tok/s prompt and 27.8 tok/s generation — both landing between the d0 and d32K rows exactly
where the sweep predicts. Separately, switching the 64K preset from q4_0 to q8_0 KV was predicted
at 29.39 tok/s and measured 29.15 at slightly greater depth: within 0.8 %.
llama-bench is not flattering itself here.
Recall is fine. Prefill is the bill.
The common warning is that long-context recall degrades well before the nominal window —
"lost in the middle". On these models, inside their native ranges, it does not happen.
Two probes: needle-test.py plants distinct verbatim facts at 5/25/50/75/95 % depth;
semantic-recall-test.py is the hard one — it plants real helper functions among
hundreds of similar utilities, then poses tasks describing what each helper does without
naming it, with a deliberate near-miss distractor for each, placed closer to the question
than its target so proximity cannot be the cue.
| Model / preset | Needle | at depth | Semantic | at depth | Cached follow-up |
|---|---|---|---|---|---|
| Qwen3.8-27B IQ3_XXS 192K | 5/5 | 189,482 | 5/5 | 186,695 | 5.3–8.8 s |
| Qwen3.5-9B-Unc 256K native | 5/5 | 216,208 | 5/5 | 225,148 | — |
| Qwen3-Coder-Next 256K | — | — | 5/5 | ~241,000 | 8.3 s |
| Qwen3-Coder-Next 128K | 5/5 | ~119,000 | 5/5 | ~119,000 | 6.5 s |
| Nemotron-3-Nano 128K | 5/5 | 127,569 | — | — | 1.1–1.2 s |
| Qwen3.8-27B Q3_K_XL 128K | 5/5 | 127,116 | 5/5 | 23,474 | 2.6–3.3 s |
| Qwen3.5-27B-Unc 32K | 5/5 | 31,596 | 5/5 | 28,149 | 2.4–3.7 s |
| GLM-4.7-Flash 32K, MLA | 5/5 | 29,817 | 5/5 | 27,185 | — |
| Gemma 4-26B-A4B 32K | — | — | 5/5 | 23,251 | 0.5–0.8 s |
The real cost is prefill, not forgetting
Prefill throughput nearly halves with depth — 552 tok/s at 38K down to 322 at 241K — so cost grows superlinearly. 119K → 241K is 2.02× the tokens but 2.8× the time.
| Preset | Cold | Warm (cached prefix) | Ratio |
|---|---|---|---|
| Qwen3-Coder-Next 128K | ~4.5 min | ~6.5 s | ~40× |
| Qwen3-Coder-Next 256K | ~12.5 min | ~8.3 s | ~90× |
| Qwen3.8-27B IQ3_XXS 192K | 686 s (~276 tok/s) | 4.9–8.8 s | ~100× |
Treat 256K as load-once-then-iterate. Dump a subsystem in, then ask twenty
questions against it. Any change to the prefix — reordering files, editing the system prompt,
inserting anything ahead of the bulk source — costs the full cold prefill again. Seen in the wild
on Qwen3-Coder-Next: a turn reporting cache_n 46967 against prompt_n 14,
the entire 47K prefix reused and fourteen tokens new. If your workflow rebuilds the prompt every
turn, 256K is unusable. Keep context lean because rebuilding it is expensive, not because
the model forgets.
Thinking models will eat your entire token budget
Qwen3.8-27B defaults reasoning_effort to xhigh. At
max_tokens: 1200 on one hard prompt, every setting returned zero
content — and raising the budget does not converge, it scales with whatever you give it.
| Setting | max_tokens | Reasoning chars | Content chars | finish |
|---|---|---|---|---|
| default (xhigh) | 1,200 | 4,684 | 0 | length |
| medium | 1,200 | 2,574 | 0 | length |
| low | 1,200 | 2,533 | 0 | length |
| low | 4,000 | 11,822 | 0 | length |
| low | 8,000 | 28,174 | 0 | length |
| --reasoning-budget 1024 | 1,200 | 3,135 | 776 | — |
| --reasoning-budget 1024 | 2,000 | 4,298 | 3,584 | — |
Nothing in reasoning_effort puts a ceiling on it. The fix is
llama.cpp's own flag, not a template kwarg — so every preset here sets
--reasoning-budget 1024. And note the second-order trap: that budget bounds the
thinking but does not stop it being billed against the client's
max_tokens, and the answer is written after the reasoning. A cap that is
comfortable for a non-thinking model silently truncates a thinking one mid-sentence, with
truncated = 0 in the server log and no error anywhere.
Rule: client max_tokens > reasoning-budget + the answer you actually want.
Both probes overshoot --depth, by different amounts.
semantic-recall-test.py plants a distractor per target and runs ~17 % over;
needle-test.py runs ~7.8 % over. A 199,478-token haystack against a 196,608 window
fails with a naked HTTPError: 400 at the client, which looks like a broken server
rather than a too-big prompt. Ask for ~80 % of the window on semantic, ~90 % on needle.
Executed, not read
code-quality-test.py scores a model by running its output. Seven tasks,
each a from-scratch reimplementation of a stdlib behaviour — fnmatch,
shlex.split, urljoin, textwrap.wrap,
csv.reader, parse_qsl, date arithmetic — with the model's function
compared against the real one over edge cases plus a seeded random batch. 50 checks total.
The first version of this suite was worthless, and the reason is worth keeping.
It used self-contained leetcode-style tasks — merge intervals, LRU, roman numerals — and every
model scored 50/50. Saturated, measuring nothing. Differential testing against stdlib is what
produced resolution: matching urljoin exactly is hard, matching it on the
happy path is easy, and the gap between those is the score. The suite is validated — with stdlib
wrappers substituted for the model, all 7 tasks score 50/50, so a failure belongs to the model
and not to the checks.
Differential stdlib reimplementation
checks passed of 50 · temperature 0.0 · exactly reproducible on re-run
urljoin dot-segment removal
(1–3 of 9 for everyone), textwrap whitespace collapsing, shlex
double-quote escapes, glob []] handling — so those tasks contribute
noise rather than signal. A different task selection could reorder the top two.
Two things this table says, and one it turned out not to
- The 80B coding specialist does not lead. Qwen3-Coder-Next scores below both Qwen3.8-27B v3 quants that ran cleanly, and it is also the slowest generator of the five at 24.69 tok/s against 29–30 for the dense 27Bs. No axis measured here shows it ahead.
- Gemma 4-E2B reads as simply too small. 17/50 at full BF16 precision, with two of seven tasks emitting code that did not even parse. Quantisation is ruled out as the explanation by construction.
- Laguna's 54 % is the model, not the quant — and the way that was established
is the interesting part. Re-run at Q8_0 it scores 27/50 again, but the totals match
while the distribution does not: twenty of the fifty checks changed hands, ten each way. The
SyntaxErrorat Q4_K_M was a quantisation artifact (add_monthsgoes 0/6 → 6/6), whilecsv_rowswent 7/7 → 0/7 at the higher quant. No single task's score should be quoted on its own.
Superseded on the quant question. Two conclusions once drawn from this table — "BPW does not order it" and "dropping to IQ3_XXS costs no coding quality" — did not survive a more sensitive instrument. KL-divergence orders the quants cleanly and monotonically by BPW and puts IQ4_XS 3.3× closer to the unquantised model than IQ3_XXS, the reverse of the check counts. The model comparison is untouched by this: KLD cannot compare different models, only a quant against its own reference.
Where the models actually differ: one chart, three outcomes
Asked conversationally for an SVG/HTML bar chart with axis, labels and title:
| Model | Billed as | Result |
|---|---|---|
| Nemotron-3-Nano-30B-A3B | general reasoning | 0/4 — computed height="-45"; a negative <rect> height is invalid SVG, so bars silently do not draw. Axis, ticks and labels all render. No error anywhere |
| Muse Glimmer 30B | agentic specialist | 7 attempts, and only succeeded after abandoning SVG for CSS <div> bars with percentage heights |
| Qwen3-Coder-Next | coding specialist | 3/3 on a strictly harder chart — ten bars, legend, rotated labels, axis title |
The confounds were eliminated one at a time: quantisation (it is UD-Q4_K_XL, not a Q3), reasoning
cap (identical at 1024 and 4096), temperature (still wrong at 0.25), and luck (four regenerations,
all four bars negative). The failure is specific to SVG coordinate maths, not to
charting — both generalists produced correct scaffolding, labels, axes and colours and
fell over only on mapping a value to a y origin and a positive height. If you want a
chart out of a non-coding model, ask for CSS bars; it routes around the part they get wrong.
Honest caveat: the failure reproduced reliably inside a long chat carrying web-search
history but did not reproduce in isolated single-turn API calls.
The other failure class, and the prompt that fixes it
Asked for the same chart using Recharts with no CDN URLs supplied, every model produced a blank page — but for different reasons, and only one reason is the model's fault. The React and Recharts code itself was correct in every case; they failed on dependency URLs. That is a recall failure, and it is the opposite of the SVG case: no amount of thinking recovers a package version you never saw, whereas pinning the URLs in the prompt fixes it completely.
| Attempt | Library | Scripts resolving | Peer deps found |
|---|---|---|---|
| Qwen3-Coder-Next 80B | Recharts | 2/4 | ✗ |
| Devstral Small 2 24B | Recharts | 3/4 | ✗ |
| Qwen3-Coder-30B Q8_0 | Recharts | 0/1 | ✗ |
| Gemma 4 + verify phase | MUI | 6/6 | ✓ |
Adding a verification phase — fetch each script URL and confirm HTTP 200 before writing it;
do not guess filenames or assume a .min build exists; read the library's own UMD docs
for peer dependencies — produced the first unambiguous success. Gemma 4 loaded React,
ReactDOM, Babel, @mui/material and both @emotion packages
— MUI v5's styling engine, the equivalent of the prop-types trap nobody found on
Recharts. All six URLs were independently re-checked and all six genuinely return 200.
Make the model verify; do not make it guess.
Note also that near-lossless precision on a smaller model lost decisively to 4-bit on bigger ones:
Qwen3-Coder-30B at Q8_0 emitted a single script tag, called ReactDOM.render (React 17)
on globals that were never loaded, and claimed data was sorted when it was not — failing on
knowledge questions the Q4 models got right, at 15.9 tok/s at 32K against Coder-Next's ~24.
Q4_K_M is not what limits coding here.
KL-divergence against BF16
The coding suite resolves 4 checks in 50 across a 68–76 % band. That is too coarse to rank quant tiers, and it produced an ordering that inverted under measurement. KLD is the right instrument for this specific question: how far has a quantised model's output distribution moved from the unquantised one, per token, with error bars — no task design, no scoring rubric, nothing to saturate.
Method. Reference is unsloth/Qwen3.8-27B-GGUF BF16,
54.66 GB across two shards, sha256-verified against the HF LFS oids. Corpus is wikitext-2
wiki.test.raw, the llama.cpp convention, so these are comparable to published
figures. -c 512 --chunks 200 = 102,400 tokens prefilled, 51,000 scored.
The 25.33 GB base logits file was generated CPU-only in 16 m 49 s — BF16 does not fit in 16 GB,
and running it on CPU also kept the router usable throughout.
Mean KL-divergence from BF16
lower is more faithful · Qwen3.8-27B · 51,000 scored tokens · every gap is 20–80 σ
| Quant | BPW | Mean KLD | 99th-pct | RMS Δp | Same top-1 |
|---|---|---|---|---|---|
| UD-IQ3_XXS v3 | 3.148 | 0.0589 ± 0.0007 | 0.577 | 6.88 % | 89.28 ± 0.14 % |
| UD-Q3_K_XL v2 | 3.932 | 0.0299 ± 0.0004 | 0.345 | 5.05 % | 92.56 ± 0.12 % |
| UD-Q3_K_XL v3 | 3.802 | 0.0263 ± 0.0003 | 0.276 | 4.61 % | 92.94 ± 0.11 % |
| UD-IQ4_XS v3 | 4.131 | 0.0179 ± 0.0002 | 0.189 | 3.80 % | 94.08 ± 0.10 % |
| UD-Q6_K v3 | 6.431 | 0.0020 ± 0.00005 | 0.020 | 1.25 % | 97.96 ± 0.06 % |
Q6_K is a different regime
13× lowerNot one more step. Top-1 disagreement falls from 7.1 % to 2.0 %. Nothing else in the table moves that much for one tier.
Dynamic 3.0 confirmed
better & smallerv3 beats v2 at the same nominal tier (0.0263 vs 0.0299) while spending fewer bits — 3.802 against 3.932. A real recipe improvement, not a size-for-quality trade.
But the vendor claim does not reproduce
+0.38 ppTop-1 goes 92.559 → 92.939 %, a 5.1 % relative reduction in disagreement — about half of the ">10 % top-1" claim under the reading most favourable to it.
The instrument was validated three ways
- Self-KLD is zero. A model scored against its own base gives KLD 4×10⁻⁵ and same-top-1 100.000 % — float noise, as it must be.
- Backend is not a confound. The base is computed on CPU while quants are scored on GPU, so some signal could have been CPU-vs-ROCm arithmetic. Scoring BF16 itself through the GPU path against the CPU base gives KLD 0.00000, worst single token 0.000068, same-top-1 100.000 % — the worst case is 30× smaller than Q6_K's mean.
- Mismatched runs cannot be compared by accident. The scorer reads
-cand--chunksback out of the base file header and refuses to run on a mismatch.
IQ4_XS is a strict upgrade over Q3_K_XL at 32K
| UD-Q3_K_XL v3 | UD-IQ4_XS v3 | |
|---|---|---|
| Generation, n=3 | 29.70 ± 0.06 tok/s | 30.27 ± 0.014 tok/s |
| Mean KLD | 0.0263 | 0.0179 |
| Same top-1 | 92.94 % | 94.08 % |
| VRAM at 32K | 14,280 MiB | 15,343 MiB |
| Free | ~2,000 MiB | 955 MiB |
Better on both axes at once — 32 % lower divergence and 1.9 % faster,
despite a file 1.06 GB larger. The reason the bigger file is faster is already in the record:
Dynamic 3.0 spends fourteen distinct ggml quant types against v2's five, and
that was measured costing 3.6 % of generation. IQ4_XS has fewer dequant paths, so
UD-Q3_K_XL-v3 pays a dequant tax for a quant that is also less faithful
than the one that does not pay it. The cost is headroom, not speed.
What Q6_K costs to actually serve
Q6_K's weights are 20,965 MiB against 16,304 MiB of VRAM, so unlike everything
else here it cannot run fully offloaded — -ngl becomes the variable and the
remainder sits in DDR4. At -ngl 42, 32K, f16 KV:
| Q3_K_XL v3 · -ngl 99 | Q6_K v3 · -ngl 42 | |
|---|---|---|
| Generation | 29.67 tok/s | 4.24 ± 0.005 tok/s |
| Prefill (batch 2048) | 1,164 tok/s | 470 tok/s |
| Mean KLD | 0.0263 | 0.0020 |
| Same top-1 | 92.94 % | 97.96 % |
The trade is 7× generation and 2.5× prefill for 13× lower divergence. Three runs returned 4.25 / 4.24 / 4.24 — a measurement, not a sample. At 4.24 tok/s a 700-token answer takes 165 s, which puts Q6_K in batch-and-come-back territory rather than interactive or agentic use. That is the honest shape of "highest quality practically" on this card: the quality is available, but not at a speed that survives an agent loop.
TurboQuant KV: tested and rejected, and only KLD shows why
TurboQuant's Walsh–Hadamard-rotated KV types compress the cache far past q4_0 and
really do change what fits — UD-IQ4_XS runs at 131,072 with turbo2
where q4_0 cannot allocate its compute buffers, and it passes 5/5 needle and 5/5
semantic at ~121K. It is still the wrong trade:
| KV cache type | Mean KLD vs f16 |
|---|---|
| q8_0 | 0.000744 |
| q4_0 | 0.003901 |
| turbo3 | 0.009823 |
| turbo2 | 0.030743 |
turbo2's cache costs more distribution quality (0.0307) than this
model's entire weight quantisation does (Q3_K_XL v3: 0.0263). Both retrieval
tests scored 5/5 on the rejected config and were blind to a 7.9× divergence gap —
needle and semantic recall are regression alarms, not quality instruments.
One genuinely free finding fell out of the same investigation, no fork required: the
gemma4:32k preset ships -ncmoe 8 but runs +14 % prompt / +12 %
generation at -ncmoe 4 on stock q8_0.
Freshness is measurable, and it did not separate the labs
cdn-freshness-test.py asks for a self-contained browser page, regexes every
src/href, sends a HEAD to each URL and counts what resolves. No
judgement calls. Three prompts (Recharts, MUI, Chart.js + D3), three runs each,
temperature 0.0.
CDN URLs that actually resolve
percentage of emitted script/link URLs returning a non-error HEAD
@mui/material@5 UMD path, Qwen3-Coder-Next always emits a dead
recharts@2.10.0/umd/recharts.min.js. So "the coder model's knowledge is outdated"
is not reproduced by this probe.
This is the one probe here that is sensitive to quant tier, and that is the useful
finding. Laguna moved from 18/36 to 27/36 purely by going Q4_K_M → Q8_0, recovering
nine URLs and dropping three of six hallucinated paths — while code-quality-test.py
scored 27/50 at both tiers on the same two files. Recall of specific version
strings and CDN paths degrades under quantisation well before procedural coding ability does,
which is consistent with the KLD result. A knowledge probe is the cheaper instrument for
detecting quant damage than a coding probe. Treat the cross-lab spread above as an upper
bound: some of every gap there is quantisation.
Native tool calls work — and llama.cpp broke them once per model
Prompt-based tool schemes inject descriptions into the prompt and parse text back, so they succeed even when the API path is broken. To test the real path you need Native function calling and a tool whose output cannot be guessed:
def get_probe_token(self, seed: int) -> str:
"""
Return the secret probe token for a given seed. There is no way to
derive this value without calling this function.
:param seed: Integer seed for the token.
"""
return f"PROBE-{random.Random(seed).randint(100000, 999999)}"
Expected: seed 42 → PROBE-770487, seed 7 → PROBE-439563,
seed 1234 → PROBE-915965. A model answering without calling cannot produce these.
| Model | Single | Parallel | Notes |
|---|---|---|---|
| GPT-OSS-20B @128K | ✓ | ✓ | two calls in one turn, did the arithmetic itself |
| Qwen3-Coder-Next @128K | ✓ | ✓ | three calls — delegated the addition to add_numbers |
| Qwen3.8-27B @32K | ✓ | ✓ | passes with thinking on and off |
| Qwen3.8-27B IQ4_XS @32K | ✓ | ✓ | same three-call delegation |
| Qwen3.8-27B IQ3_XXS @192K | ✓ | ✓ | structured output survives 3.148 BPW |
| Qwen3.5-27B-Uncensored @32K | ✓ | ✓ | same delegation as its Qwen3.8 twin |
| Qwen3.5-9B-Uncensored @256K | ✓ | ✓ | comma-formatted the sum — numerically identical, but it broke a naive exact-match probe |
| GLM-4.7-Flash @32K | ✓ | ✓ | tested at general-use sampling, not the vendor's tool-calling params |
| Laguna XS.2 @32K | ✓ | ✓ | the strongest axis measured on this model |
| Gemma 4-E2B @32K | ✓ | ✓ | two token calls plus an add_numbers delegation |
| Muse Glimmer 30B @32K | ✓ | untested | via Open WebUI's own write_note |
Three things that will waste your day
The parser breaks per model
llama.cpp's peg-native format derives a grammar from each model's chat template rather than using a hand-written parser, so it breaks per model and gets fixed per model. Qwen3-Coder-Next 500'd on every tool call on build e583f3b4f — Failed to parse input at pos 22, exactly len("<|im_start|>assistant\n"). Fixed by 153d324bc. GPT-OSS-20B still fails this way under Cline. Record it as a harness limitation, not a model limitation, and check the server build before blaming the model — retrying cannot help, since the model regenerates the same valid-but-rejected shape.
A result badge is not evidence a tool ran
With no tools installed or enabled, a model emitted a call to calculate_timestamp — a function that exists nowhere — and the UI rendered View Result from calculate_timestamp above a fabricated answer. llama.cpp converts native tool-call syntax into a structured tool_calls field regardless of whether the client sent a tools array. Every layer behaved as designed and the output was still invented. Check the tool name against the tools you actually installed.
Code execution fails silently when half-enabled
Open WebUI's Code Interpreter needs enabling in two places. With only the first done there is no error and no warning — the model simply answers from nothing. Same model, same three questions, twenty minutes apart: sha256 fabricated; primes below 987654 given as 77,597 against the true 77,614; digit sum of 7⁷⁷⁷ given as 3,519 against 2,989. With execution available it used it unprompted on all three and got every answer right. The prime count is the dangerous case — a wrong hash is obviously wrong, 77,597 passes a glance.
Pick test values that cannot be memorised. An earlier run of that test used sha256("hello world")
and the 5000th prime, and "passed" without executing anything because all three are in training
data. The tell was formatting: the sum came back as 4,543,965,37, comma-grouped
wrong — what recalled digits look like rather than a formatted integer.
Flags that matter, and one that is a trap
KV cache type — match them, never mix
| -ctk / -ctv | Prompt tok/s | Gen tok/s |
|---|---|---|
| q8_0 / q8_0 | 776.7 | 33.4 |
| f16 / f16 | 753.0 | 34.8 |
| f16 / q8_0 | 600.9 | 33.2 |
| q8_0 / f16 | 603.6 | 33.0 |
Mixed precision costs ~23 % prompt throughput for no memory benefit. q8_0/q8_0 is the right default: same speed as f16, half the memory.
q4_0 KV is not free, and the cost scales with depth
The table above invites assuming q4_0 is as cheap again. It is not. Qwen3.8-27B,
-ub 1024, tg64, matched depth, nothing else varied:
| Depth | q8_0 | q4_0 | q4_0 cost |
|---|---|---|---|
| 0 | 31.57 | 31.53 | 0 % |
| 8K | 30.64 | 27.80 | −9.3 % |
| 16K | 29.39 | 25.06 | −14.7 % |
| 32K | 27.04 | 20.78 | −23.1 % |
Zero at depth 0 — there is no cache to read, so the quant cannot matter — and 23 % by 32K. The penalty is per-token dequantisation proportional to cache size, so it grows exactly where you reached for q4_0 in the first place. Treat q4_0 KV as the price of admission, never an optimisation. Where both fit, q8_0 wins on speed and on precision.
One negative result worth recording: q4_0 is widely assumed to degrade output, and a truncated
response made that the obvious suspect. Tested directly at ~13,250-token depth, four samples per
quant, asking for five titled sections plus a literal end marker — no early stopping and
no structural loss from q4_0; if anything it ran long. The real cause of the truncation
was the client's max_tokens. That says nothing about factual accuracy, and nothing
about 100K+ depth where quantisation error has far more attention steps to compound through.
Threads and offload
Leave threads at 16
-t 32 → 23.6 ± 4.6Generation collapses with high variance on SMT contention across all 32 threads. 8 / 16 / 24 are within noise of each other (34.7 / 34.0 / 33.0).
--no-kv-offload is a trap
2–3× worseIt frees a lot of VRAM but moves KV to system RAM over PCIe. Qwen3.6 generation: 28.8 → 14.7 at 32K, 22.2 → 7.7 at 131K. Reduce context or KV quant instead.
Watch for other GPU consumers
10.4 GBDaVinci Resolve holds that much VRAM while open, silently forcing far more conservative -ncmoe. If a working config suddenly fails to allocate, check for it before re-tuning. Idle ollama serve holds nothing.
YaRN does not confirm itself
check by handprint_info: rope scaling = linear in the log reflects GGUF metadata, not the runtime flag, so it is not a reliable confirmation that YaRN engaged. Verify with a retrieval test at depth if it matters.
The MTP head llama.cpp throws away by default
Qwen3.8-27B's block_count is 65, not 64. blk.64 is a
Multi-Token-Prediction head, and llama.cpp discards it by default —
model has unused tensor blk.64.* -- ignoring. That message is the default-off path,
not a missing feature: --spec-type draft-mtp turns it on, and it is present in
both the v2 and v3 files, so this is testable on something you already have.
| Config | Generation | Interactive follow-ups |
|---|---|---|
| v2 @16K, no MTP | 30.82 tok/s | 2.3–2.6 s |
| v2 @16K, MTP | 74.02 tok/s (2.40×) | — |
| v3 @16K, no MTP | 29.70 tok/s | 2.3–2.6 s |
| v3 @16K, MTP | 69.47 tok/s (2.34×) | 0.9–1.1 s |
That beats Muse Glimmer's DFlash drafter (1.64×). The cost is ~2,212 MiB, fixed —
not the 198.8 MiB of blk.64 weights, because
common_speculative_init_result builds a second full context. Measured identical at
8K and 16K and unchanged by batch size, so it does not scale. On a 16 GB card holding 12.5 GB of
weights that caps MTP at 16K. If you cannot live inside 16K, MTP is not for you;
the 2.2 GB is not negotiable.
Everything here is reproducible
Model folders under benchmarks/ are generated, not hand-assembled —
a result you cannot regenerate is a claim, not a measurement. Every output file carries a header
naming the model, context, flags, llama.cpp build and timestamp, so it is interpretable on its own.
Rebuilding the ROCm backend
cd ~/llama.cpp
HIPCXX="$(hipconfig -l)/clang" HIP_PATH="$(hipconfig -R)" \
cmake -B build -DGGML_HIP=ON -DAMDGPU_TARGETS=gfx1201 -DCMAKE_BUILD_TYPE=Release
HIP_PATH=/opt/rocm cmake --build build -j16
HIP_PATH is needed at both configure and build time. Every guide
shows it only at configure, which gets you a clean cmake run and then fails every
.cu file with a misleading fatal error: 'hip/hip_fp16.h' file not found.
Two more traps, each of which costs a full build cycle: HIPCXX must point at
clang, not clang++ (auto-detection picks clang++, compiles
in CUDA language mode, and dies with unsupported CUDA gpu architecture: gfx1201);
and a stale CMakeCache.txt survives git pull. Back up the working
binary first — a failed build leaves you with no server at all.
Serving
nohup ~/llama.cpp/build/bin/llama-server -m models/<file>.gguf -ngl 99 [-ncmoe N] \
[-ub 1024 -b 2048] [-fa on -ctk q8_0 -ctv q8_0] -c <context> -np 1 \
--host 0.0.0.0 --port 8090 > /tmp/llama-server.log 2>&1 < /dev/null &
disown
Switching models — router mode is the default worth using
~/llama.cpp/switch-model.sh router # serve ALL presets, switch from the client
~/llama.cpp/switch-model.sh <model> <context> # pin one
~/llama.cpp/switch-model.sh stop # SIGTERM, then SIGKILL -- frees the GPU
~/llama.cpp/switch-model.sh status # what's running now
~/llama.cpp/switch-model.sh list # verified combinations
router starts one process over every preset in models-preset.ini.
Picking a model from the client's dropdown loads it on demand and unloads the previous one, and
each preset carries its full verified config rather than you retyping flags. It costs exactly one
thing: the router spawns children from /proc/self/exe, so every model runs on the
binary the router was launched with — there is no per-model backend in this mode. Run with
--models-max 1; a second resident model will not allocate on this card.
Getting the GPU back without stopping the server
# launches pass --sleep-idle-seconds 900
# after 15 idle minutes llama-server calls destroy() and releases the model
# measured: 12,464 MiB -> 595 MiB, process still listening on 8090
The next request calls load_model() and reloads it, so nothing breaks — it just pays
a cold start. Sleep state is not reported by any endpoint: /health,
/props and /v1/models all deliberately bypass sleep, so none of them
can tell you it is asleep, and polling them never triggers a reload.
Running the probe suite
./run-suite.sh qwen3.8-27B-UD-IQ3_XXS-v3 Qwen3.8-27B-UD-IQ3_XXS-v3.gguf 196608 \
"-ub 512 -b 2048 -fa on -ctk q4_0 -ctv q4_0"
benchmarks/needle-test.py --depth 119000 --no-think
benchmarks/semantic-recall-test.py --depth 20000 --no-think
benchmarks/kld-test.sh score # refuses to run on a -c/--chunks mismatch
benchmarks/fit.sh # VRAM sweep, with the GTT spill guard
Reading GGUF metadata before downloading anything large
~/llama.cpp/build/bin/llama-server -m models/<file>.gguf -c 512 --no-warmup 2>&1 \
| grep -iE "block_count|full_attention_interval|head_count_kv|key_length|context_length"
Attention topology decides long-context viability, so check it first — parameter count matters
far less. A related warning: Unsloth re-quantised Qwen3.8-27B-GGUF in place on
2026-08-19/20 under the same repo and the same filenames. A file pulled on
08-16 is Dynamic 2.0 and nothing in its name says so. The only reliable check is the byte size or
the LFS oid — and the filename does not identify the build: the same
UD-Q3_K_XL name reports Q4_K - Small at 3.93 BPW in v2 and
Q3_K - Large at 3.80 BPW in v3.
Can they actually do a job?
Throughput, VRAM and recall say nothing about whether a model can research live data, build something and find its own bugs. The task: research the top 10 open-weight coding LLMs from a published benchmark, then build a React + MUI dashboard that renders them — with a verification phase requiring every CDN URL to be fetched and the browser console read before declaring success.
Headline: feedback beats capability. The same models that produced four
undetected blank pages through a chat UI catch their own 404s once given a browser console.
Muse Glimmer is the clean demonstration — its dependency check returned
200 200 404 404, it reasoned toward the correct @emotion UMD path, and
it corrected before writing the file. Nothing about the model changed. What changed is
that it could check its work. The bottleneck was the absence of a feedback loop, not model
capability.
| Cline | Claude Code | aider | |
|---|---|---|---|
| Model compatibility | ~5/6 | 7/11 | 11/11 |
| Failure class | native tool-call parser format | grammar compile / message order | turn-boundary + confirmation-gate mismatches |
| Data layer | mixed (one model fabricated) | correct, verified | correct, verified |
| Frontend | rendered, minor bugs | rendered, minor bugs | did not render at all |
| Wall time | 40–70 min | 75 min | 11–20 min |
aider wins decisively on raw compatibility — it parses plain-text diffs and plain-text shell suggestions instead of native tool calls, so nothing is disqualified by a protocol quirk, including the two presets blocked under both other harnesses. But getting a real task through it took five structural fixes a human driving it interactively would never hit, because a human naturally supplies the "now run it" nudge and notices when the model says it cannot run shell commands.
Claude Code: 7 of 11 presets, and both failures are server-side
llama.cpp serves the Anthropic Messages API natively, so no translation proxy is involved. What
does block it: Claude Code sends a system-role message after the user
message, and the dense-Qwen family's Jinja template raises
System message must be at the beginning on that shape. That rules out
qwen3.8-27B — the model that scores best on code-quality in this
whole record. Nothing to fix client-side; it is the template. GPT-OSS fails differently
and earlier, rejecting the 30 built-in schemas at grammar compilation.
A probe that sends tools plus a plain user message is not sufficient to establish compatibility — it passed all three Qwen models that then failed in real use. The system-after-user shape has to be in the probe.
| Preset | Cold s | Warm s | TTFT s | Gen s |
|---|---|---|---|---|
| laguna-33B-A3B-128k | 101.7 | 21.9 | 21.5 | 0.4 |
| qwen3.6-35B-A3B-128k | 145.6 | 22.8 | 13.1 | 9.7 |
| gemma4-26B-A4B-128k | 43.7 | 32.7 | 28.6 | 4.1 |
| laguna-33B-A3B-q8-128k | 176.9 | 37.5 | 36.9 | 0.6 |
| glm-4.7-flash-30B-A3B-128k | 167.6 | 54.6 | 39.2 | 15.5 |
| qwen3-coder-80B-A3B-128k | 251.8 | 56.8 | 56.3 | 0.5 |
| muse-glimmer-30B-128k | 118.0 | 61.7 | 58.0 | 3.7 |
Time-to-first-token is 96–99 % of a fresh-session turn. Generation is a rounding error, so the throughput tables above predict almost nothing about how an agent feels. In a continued session it is far better because the prefix caches — Laguna goes 25.5 s → 3.1 s on turn two, Qwen3.6 58.6 s → 31.9 s, and a byte-identical repeat measured 65.3 s → 0.1 s.
That produces a genuinely awkward trade: the quality leader is disqualified by its chat template and the speed leader is the weakest model. Laguna scored 27/50 on coding and last on library knowledge, and it shows — asked to reply with an exact token it declined and explained what it was designed for instead. Correct tool calls, unreliable instruction-following. But 3.1 s per follow-up against 31.9 s is a tenfold difference in felt latency.
When the feedback loop gets gamed
The "feedback beats capability" claim has a boundary, and it is worth stating plainly. Nemotron-3-Nano — the model that wins on throughput, KV cost, native context and recall, and which showed the best research instinct of any model on the task by checking for a pre-aggregated endpoint first — fabricated a full dataset of pre-2024 models with real citation URLs attached to invented scores after its real data source failed to resolve, and separately falsified a "clean" verification report on two occasions, claiming files existed and checks passed when they had not. It was deleted from disk for that, not for anything in its benchmark numbers.
A feedback loop only works if the model's report of what it observed is honest. Verifying that the report is real — not just that a check exists — is the harder problem this record leaves open. It is the same shape as the chart that rendered perfectly and was titled "2024 Comparison" from stale search results: nothing malfunctions and the output is still wrong.
What to actually run
| Job | Take | Why |
|---|---|---|
| Code | qwen3-coder-80B-A3B | Correct SVG geometry 3/3 where two generalists failed, verified tool calling, ~24 tok/s out to 47K depth, flattest decay in the file |
| Max context on one card | qwen3.8-27B UD-IQ3_XXS @192K | 1.5× the old ceiling, 672 MiB free, 5/5 on both recall probes at ~187–189K, tool calling intact at 3.148 BPW |
| Max speed inside 16K | qwen3.8-27B Q3_K_XL v3 + --spec-type draft-mtp | 2.34× generation, ~1 s follow-ups, from a head already in the file you downloaded |
| Best weights ≤32K | qwen3.8-27B UD-IQ4_XS | 32 % lower KLD and 1.9 % faster than Q3_K_XL v3 — better on both axes at once |
| Beyond 262,144 | nemotron-1m | The only option natively trained there — but see the honesty caveat above |
| Fast general | gemma4-26B-A4B | 47 tok/s flat across its whole window, 8.5 % decay, cheapest cold start by 4× |
| Agentic loop, felt latency | laguna-33B-A3B-q8 | 3.1 s follow-up turns against 31.9 s. Same coding score as its Q4 and +25 points of library knowledge |
| Absolute fidelity, batch only | qwen3.8-27B UD-Q6_K | 13× lower divergence at 4.24 tok/s — a 700-token answer takes 165 s |
Six things that generalise past this box
- Attention topology beats parameter count for long context. Read
full_attention_interval,head_count_kvandblock_countbefore downloading anything large. A 27B dense hybrid can cost 2.7× the KV of an 80B, and a conventional full-attention coder needs ~52 GB of cache at 1M. - A probe that saturates is measuring nothing. Needle recall at 32K, the first version of the coding suite, and both retrieval tests against TurboQuant all returned perfect scores while real differences existed. Build the instrument until it resolves, then say what it cannot see.
- Match the instrument's resolution to the question. A 50-check coding score ranked quant tiers backwards; KLD ordered them monotonically at 20–80 σ. Knowledge probes catch quant damage that coding probes miss entirely.
- "It loads" is not "it fits." Buffers migrate silently to host memory and a naive peak-VRAM reading reports the migration as a comfortable fit. Sample GTT, and treat anything under ~300 MiB free as fragile.
- Check the harness before blaming the model. Tool calling was broken by llama.cpp's per-model derived grammar, not by any model. Retrying cannot help — the model regenerates the same valid-but-rejected output.
- Give the model a way to check its work, then check that it checked. A verification phase turns a silent 404 into something the model has to notice. It does not, on its own, stop a model from reporting a check it never ran.
What these numbers do not say
- Recall probes are saturated at 32K. Every quant scores 5/5. They confirm nothing broke; they cannot rank models. Only runs near the ceiling discriminate — and even those were blind to a 7.9× KLD gap.
- Coding scores are deterministic but narrow.
temperature 0.0is greedy and re-runs reproduce check-for-check, so these are measurements rather than samples. But the spread across the top three is 4 checks in 50, and all three fail on the same near-floor tasks. - KLD cannot compare different models, only a quant against its own unquantised reference. It is measured on wikitext, which is prose; a code corpus could shift the ordering. And low divergence from BF16 means faithful to the original model, not good.
- Long-context results are single-hop and n=5 per depth, with clean planted signal unlike real code. That rules out systematic collapse, not subtle degradation. Nothing covers the beyond-native 512K/1M configs, where YaRN and q4_0 KV both plausibly hurt.
- The output-quality section is one task, one client, three models. It is not a coding benchmark. Read it as: route code at the coder, and do not assume a model that wins on throughput and recall wins on code.
- Desktop VRAM drifts — the baseline moved between 263 and 442 MiB, and across 306 MiB within a single session. Older Free columns measure the machine's state as much as the model's. Newer rows report model-attributable VRAM against a stated baseline.
code-quality-test.pyexecutes model-written code. It runs in a temp directory under a wall-clock timeout, which is containment, not a sandbox.- Beyond-native context rows stack a second RoPE extension on models that are already stretched. GPT-OSS-20B's 131,072 is itself a 32× YaRN stretch from a 4,096 base. Those are the least trustworthy rows anywhere in this record.