Skip to content

Qwen3.8-27B Q4_K_M on M1 Max 32 GB — llama-server benchmarks

bash
llama-server -hf bartowski/Qwen3.8-27B-GGUF:Q4_K_M \
  --alias qwen3.8-27b --no-mmproj \
  --spec-type draft-mtp --spec-draft-n-max 3 --parallel 1 \
  -ngl 999 -fa on -c 98304 \
  --jinja --port 8081
  • --spec-draft-n-max 3 is the clear decode-speed peak (16.8 py / 15.6 js tok/s vs 12.4 without MTP).
  • KV is a context/speed dial: f16 (lossless, marginally faster) up to 96K; q8_0 (near-lossless) up to 160K at ~1% decode cost — see the long-context variant section.
  • -c 98304 (96K) is the max stable context: 112K and 128K hit Metal OOM. RSS ~24.1 GB, ~7.9 GB left for macOS + DB.
  • For 2+ concurrent agents use --parallel 2 -c 90112 (2×44K slots, the max): MTP survives, RSS ~24.2 GB. --parallel 2 -c 98304 OOMs.
  • --reasoning-effort medium decodes ~21% faster than xhigh (higher MTP acceptance on medium-effort output).

Build: llama-server 0.3.0 (build 10621, commit c1d0e7a00), Metal. Wired limit: iogpu.wired_limit_mb=27000. All runs: temperature 0, n_predict 256 unless noted. Fresh server start per config.

Base startup command (flags that change per run are shown in each section):

bash
llama-server -hf bartowski/Qwen3.8-27B-GGUF:Q4_K_M \
  --alias qwen3.8-27b --no-mmproj --parallel 1 \
  -ngl 999 -fa on -c 32768 \
  --cache-type-k q8_0 --cache-type-v q8_0 \
  --jinja --port 8081

Prompts:

  • py: Write a Python function that parses ISO dates.
  • js: Write a JavaScript function that deep clones an object.
  • long: 1521-token English text, n_predict 64 (prompt-processing probe)

Baseline — no MTP (no --spec-type flags)

promptpp tok/sdecode tok/sdraft_naccepted
py22.412.47
long127.111.78
py (fresh start)23.512.44
js24.412.44

MTP --spec-type draft-mtp --spec-draft-n-max 1

promptpp tok/sdecode tok/sdraft_naccepted
py22.612.39133121 (91%)
js22.712.02137117 (85%)

MTP --spec-draft-n-max 2 (rerun of the handoff config)

promptpp tok/sdecode tok/sdraft_naccepted
py22.011.98186161 (87%)
js22.710.68209150 (72%)

MTP --spec-draft-n-max 3

promptpp tok/sdecode tok/sdraft_naccepted
py21.316.79230178 (77%)
py (repeat)21.916.77230178 (77%)
js22.015.58248172 (69%)

MTP --spec-draft-n-max 3 + f16 KV (no --cache-type-* flags, lossless)

promptpp tok/sdecode tok/sdraft_naccepted
py21.016.93230178 (77%)
js22.215.73248172 (69%)

Medium-effort depth sweep — chat endpoint, f16 KV, max_tokens 1024, warmup first

n-maxpy tok/spy acceptjs tok/sjs accept
317.52723/897 (81%)16.31702/962 (73%)
416.57767/1023 (75%)14.86738/1138 (65%)
613.44804/1311 (61%)11.60769/1518 (51%)

Peak stays at n-max 3 at medium effort: 17.5/16.3 tok/s.

Reasoning effort — chat endpoint, N=3 + f16 KV, max_tokens 1024

Both efforts hit the 1024-token cap while thinking; decode speed is the comparison.

effortpromptdecode tok/sdraft_naccepted
xhighpy14.441089659 (61%)
xhighjs13.961123648 (58%)
mediumpy17.50897723 (81%)
mediumjs16.30962702 (73%)

MTP --spec-draft-n-max 4

promptpp tok/sdecode tok/sdraft_naccepted
py20.516.33253191 (75%)
js21.813.03319175 (55%)

MTP --spec-draft-n-max 6

promptpp tok/sdecode tok/sdraft_naccepted
py20.513.12330200 (61%)
js21.010.21422184 (44%)

MTP --spec-draft-n-max 7

promptpp tok/sdecode tok/sdraft_naccepted
py20.812.73377201 (53%)
js17.510.16471187 (40%)

MLX backend — mlx-lm 0.31.3 (brew), mlx_lm.generate, chat template applied

Model: mlx-community/Qwen3.8-27B-4bit. 256 tokens, temp 0. Peak memory 15.5 GB.

promptdecode tok/snotes
py19.69beats GGUF+MTP (16.8) with no MTP at all
js19.58pp 73 tok/s on 62-token prompt (warm)

MLX has no slot system: mlx_lm.server handles one request at a time, requests queue. No -c preallocation — KV grows per request up to the trained 262K window, memory permitting.

MLX context probe (mlx_lm API, 32-token generations, warmup first)

prompt depthpp tok/speak memory
8K10619.2 GB
16K10720.5 GB
32K10223.0 GB
48K9625.6 GB
64KMetal OOM

MLX max context ≈ 48K (vs 96K on llama-server). MLX pp (~105 tok/s) is no faster than llama.cpp (122.8 at 4K) — MLX wins decode only. Marginal memory ~0.16 GB per 1K tokens (≈3× llama-server's f16 KV slope): prefill activation buffers dominate.

MTP on MLX (mlx_vlm.generate, draft Qwen3.8-27B-MTP-4bit, depth fixed at 2)

promptdecode tok/sacceptancepeak memory
py20.2482.8% (2.66 tok/round)17.1 GB
js22.4997.1% (2.94 tok/round)17.1 GB

Best Qwen decode overall for js; py ties plain MLX. Needs the mlx-vlm package (pipx). Multi-instance MLX is impossible for this model: two weight copies (2×15.5 GB) exceed the wired limit.

Long-context variant — q8_0 KV (near-lossless, user-approved quality bar)

f16 KV was chosen for speed (+~1%), not because q8_0 failed quality. Halving KV doubles the context budget:

-cKVresultrss
131072q8_0OK, 14.7 tok/s (short probe)19.4 GB
163840q8_0OK — q8-KV maximum (160K)23.1 GB
180224q8_0Metal OOM
196608q8_0Metal OOM

96K (f16, lossless) vs 160K (q8_0, near-lossless) at ~1% decode cost.

Two-slot with q8_0 KV:

-cslotsresultrss
1474562×72KOK, 14.6 tok/s — recommended22.9 GB
1556482×76Kloads, but decode degrades to 10.1 tok/s (memory pressure)23.4 GB
1638402×80KMetal OOM

(f16 two-slot max was 2×44K.)

Context ramp — N=3, f16 KV, short probe (n_predict 64)

-cresultRSS
49152OK, 14.9 tok/s21.2 GB
65536OK, 14.8 tok/s22.0 GB
98304OK, 14.8 tok/s24.1 GB
106496Metal OOM
114688Metal OOM
131072Metal OOM

Final config validation — N=3, f16 KV, -c 98304

promptpp tok/sdecode tok/sdraft_naccepted
4086-token text, n_predict 128122.815.3412386 (70%)
py18.716.84230178 (77%)
js18.315.59248172 (69%)

No Metal errors. RSS 24.1 GB after the long prompt.

JS prompt check — Write a JavaScript function that debounces another function.

Run twice on the final config. Same speed as the deep-clone prompt (15.59), so the deep-clone prompt stays as the JS benchmark.

rundecode tok/sdraft_naccepted
115.33255170 (67%)
215.35255170 (67%)

Concurrency — N=3, f16 KV, --parallel 2

-cslotsresultRSS
655362×32KOK, MTP active, py 17.05 tok/s23.1 GB
819202×40KOK, MTP active23.9 GB
901122×44KOK, MTP active — max for 2 slots24.2 GB
983042×48KMetal OOM

Quality — EvalPlus HumanEval+ (2026-08-27, fair budget)

pass@1 0.982 base / 0.939 plus / 100% completion (mlx 4-bit, reasoning_effort=medium, output budget 8192, temperature 0). A 2026-08-26 pass under a flawed 3072-token cap had scored it 0.970/0.939/98% (superseded, see the historical page). Zero empty completions. The strongest HumanEval+ result of the models scored so far.

Quality — EvalPlus HumanEval+, 4-bit GGUF at effort xhigh (2026-09-12, wired limit 25000)

bartowski/Qwen3.8-27B-GGUF:Q4_K_M rev f0eec4a, drafter n-max 3, f16 KV, -c 32768, reasoning_effort: xhigh passed on every call, budget 30000 from a calibration where two of ten problems hit the cap, temperature 0.

metricvalue
pass@1 base0.957
pass@1 plus0.939
empty completions6/164
completion96%
active wall8h30

The six empties all ran to the 30000-token cap: HumanEval/2, 32, 91, 99, 132, 134. The same build at effort medium carries the MLX 0.982/0.939 score under the shared-score rule.

Real-text decode at the server's sampling (llama-benchy 0.4.0, 2026-09-10 and 2026-09-11, wired limit 25000)

llama-benchy sends 512 prompt tokens after a text conversation of the stated depth and reads 256 generated tokens, two runs after one warmup, no sampling parameter passed. Draft acceptance from the server log, per counted request. --cache-ram 0 on the server where stated.

buildconfigdepthcorpustok/ssdacceptancewired
Q4_K_M bartowskillama+MTP n-max 3, f16 KV, -c 73728, --cache-ram 04096code11.771.090.46–0.5425.0 GB
Q4_K_M bartowskillama+MTP n-max 3, f16 KV, -c 73728, --cache-ram 065536code8.571.390.37–0.6325.0 GB
IQ3_S-mtp ISTAllama, no drafter, f16 KV, -c 1638404096prose13.940.00no drafter24.5 GB
IQ3_S-mtp ISTAllama, no drafter, f16 KV, -c 16384049152prose11.360.05no drafter24.5 GB
IQ3_S-mtp ISTAllama, no drafter, f16 KV, -c 16384098304prose9.470.00no drafter24.5 GB
IQ3_S-mtp ISTAllama+MTP n-max 3, f16 KV, -c 13107298304prose6.030.370.41–0.5123.4 GB
IQ3_S-mtp ISTAllama+MTP n-max 3, f16 KV, -c 131072, --cache-ram 098304code5.620.180.38–0.4323.5 GB

The ISTA no-drafter cells match the creep within 2.3 percent. The ISTA drafter cells sit under the no-drafter reading at the same depth, and under the creep's drafter reading, which a memorised continuation inflated.

Depth sweeps (llama at limit 25000, 2026-08-28; mlx re-tested at limit 24000, slow creep, 2026-08-29)

Decode vs used context, append-only prompts, 8 tok/s early stop:

depthllama+MTP, q8_0 KV (32K alloc)mlx, f16 KV
4K14.1
8K12.817.1
16K8.616.4
22K10.23
24K14.79
24.5K7.3 — below floor
26K15.19
28K15.29 — last stable
~30KMetal OOM (server thread dies; /health stays 200; gfx-resident ~22 GB)

llama floor ~19K (speed), RSS 18.9 GB there; mlx never crosses the floor — its limit is a memory ceiling between 28K and ~30K at limit 24000. MLX wins this model's equilibrium: ~10-17 tok/s across its whole usable window. Suggested pi setup: mlx config with compaction threshold ~26K (below the known-good 28K). The 27000-era context maxima (160K single, 2×72K) are withdrawn pending re-probe.