Orkun Soylu

3 min read

Qwen3.8-27B Q8_0 vs Q4_K_M

Dropping from Q8_0 to Q4_K_M on my W7800 bought 29% faster generation and 12 GB of VRAM — far less than the bandwidth maths promised — and made long-prompt prefill 7% slower.

Tested
Setup

Radeon Pro W7800 48 GB — bare-metal workstationBare metal, Linux, ROCm (Ollama) · AMD Ryzen 5 7600 (6C/12T, AVX-512) · 64 GB DDR5-6400

Runtime
Ollama 0.32.14, ROCm 7.2.4, flash attention on, q8_0 KV cache, 262K context
Models

qwen3.8:27bQ8_0baseline

qwen3.8:27bQ4_K_Mtested

Verdict

Q4_K_M is worth it for VRAM, not for speed: generation gains 24–29%, prefill gets slightly worse, and the advantage shrinks as the context fills. 20 tok/s on Q8_0 is simply this card's ceiling, not a fault.

Works

  • −12 GB VRAM (26 GB vs 38 GB at 262K)
  • +29% generation on short prompts, +24% at 31K tokens

Doesn’t

  • Predicted ~35 tok/s, measured 25.7 — bandwidth efficiency falls from 69% to 53%
  • Prefill is 7% slower than Q8_0 (498 vs 534 tok/s)
  • Loses more speed as context fills (−19% vs −16%)
Generation, Q8_0 → Q4_K_M
19.9 → 25.7tok/s
Bandwidth efficiency
69% → 53%
Prefill 31K tokens
534 → 498tok/s
VRAM at 262K
38 → 26GB

Generation speed

higher is better
  • Q8_0
  • Q4_K_M
Short prompt (21 tok)
19.85 tok/sQ8_0 · 19.85 tok/s
25.68 tok/sQ4_K_M · 25.68 tok/s
Long prompt (31,073 tok)
16.77 tok/sQ8_0 · 16.77 tok/s
20.75 tok/sQ4_K_M · 20.75 tok/s
Theoretical ceiling (864 GB/s ÷ weights)
28.80 tok/sQ8_0 · 28.80 tok/s
48.70 tok/sQ4_K_M · 48.70 tok/s
Data
Q8_0Q4_K_M
Short prompt (21 tok)19.85 tok/s25.68 tok/s
Long prompt (31,073 tok)16.77 tok/s20.75 tok/s
Theoretical ceiling (864 GB/s ÷ weights)28.80 tok/s48.70 tok/s

Prefill, 31,073-token prompt

higher is better
  • Q8_0
  • Q4_K_M
Prefill
533.6 tok/sQ8_0 · 533.6 tok/s
498.0 tok/sQ4_K_M · 498.0 tok/s
Data
Q8_0Q4_K_M
Prefill533.6 tok/s498.0 tok/s

Why I measured

In daily use qwen3.8:27b-q8_0 felt slow at ~20 tok/s. Was something wrong, and would Q4_K_M fix it?

How I measured

  • Short prompt: 21 tokens (“write me a Python code example”), 2–3 repetitions.
  • Long prompt: 99,000 characters of Turkish filler plus an instruction — 31,073 tokens (Turkish tokenises worse than English).
  • /api/chat, stream: false, think: false. I unloaded both variants with keep_alive: 0 before every measurement (30 + 18 GB don’t fit together).
  • Both use a q8_0 KV cache. The KV type doesn’t depend on the weight quantization, so at 31K tokens both models move the same number of KV bytes. Keep that in mind for point 5.

Results

Q8_0 Q4_K_M
Weights on disk 29.98 GB 17.74 GB
VRAM (ollama ps, ctx 262,144) 38 GB 26 GB
Theoretical ceiling (864 GB/s ÷ weights) 28.8 tok/s 48.7 tok/s
Generation, short (21 tok) 19.85 (19.9 / 19.8) 25.68 (26.00 / 25.59 / 25.45)
Bandwidth efficiency (short) 69% 53%
Generation, long (31,073 tok) 16.77 20.75
Long-context slowdown −15.5% −19.2%
Prefill, long prompt 533.6 tok/s (58.2 s) 498.0 tok/s (62.4 s)
Total time (long) 67.8 s 69.5 s

What I found

  1. 20 tok/s is not a fault. 100% GPU, no offload, normal temperatures. A 27B at Q8 reads ~30 GB per token; 864 GB/s caps that at 28.8 tok/s, and 69% efficiency is normal for llama.cpp on ROCm.
  2. My Q4 prediction missed: I expected ~35 and got 25.7. My mistake was assuming Q8’s 69% efficiency would carry over. As the weights shrink, the work stops being bandwidth-bound and shifts to dequantization — unpacking the weights — and K-quants (per-block scales and mins) are far more expensive to unpack than Q8_0’s int8 × scale.
  3. Efficiency drops with the quant. Same card, same model, same runtime: Q8_0 69%, Q4_K_M 53%. This was my first clean measurement that isolates the quantization alone.
  4. Prefill is actually slower on Q4_K_M. Prefill is compute-bound (matrix × matrix: the weights are read once and reused for thousands of tokens), so smaller weights don’t help, and the costlier unpacking is still there. Generation is bandwidth-bound (matrix × vector: every weight is reread for every token) — that’s where Q4 wins.
  5. Q4’s advantage erodes as the context grows. The q8_0 KV traffic is the same number of bytes for both models, so it takes a bigger share of Q4’s smaller per-token read. A cross-check with July: Qwen3.6-27B Q8_0 lost 14% at ~25K, Qwen3.8 loses 15.5% at 31K — the card and the stack behave consistently.
  6. The wait is prefill. 58–62 s before the first token at 31K, then 85 tokens in 5 s. No quant change fixes that; a shorter prompt or prefix caching does.
  7. think changes how many tokens you get, not how fast. Same question: think:false → 572 tokens / 28.8 s, think:true → 1,263 tokens / 64.0 s, both at ~19.8 tok/s.

A projection (not measured): 2× W7800 with BF16

This came up while I was planning hardware. llama.cpp/Ollama split multiple GPUs by layer, so the cards work one after another — bandwidths don’t add up, times do.

Setup Weights Effective bandwidth Theoretical Expected
1× W7800, Q8_0 30 GB 864 GB/s 28.8 19.9 (measured)
1× W7800, Q4_K_M 17.7 GB 864 GB/s 48.7 25.7 (measured)
2× W7800, BF16, layer split ~56 GB 864 GB/s 15.4 ~10.5
2× W7800, BF16, tensor parallel (vLLM) ~56 GB 1,728 GB/s 31 ~20–25, if it runs on RDNA3

If I add a second card, it should be for capacity, not precision: for a 27B, BF16 vs Q8_0 is within noise on quality.