Results · all measurements

Every number we measured

Qwen3.8-27B on 2× RTX 3090, over five days. Speed, throughput, context scaling, correctness and agentic behaviour — with the provenance of each figure attached, because several of them are not what they first appeared to be.

212.1tok/s
best single-stream
7.7×
tuning ladder span
959.8tok/s
one GPU at C=64
204K
deepest context served
48.7%
Aider polyglot pass_2
−50%
thinking tokens with Swift, same accuracy
How to read this page. Numbers come from three eras of the study, and they were not all measured under equal conditions. Where a figure was taken while a second GPU was busy, or before the power cap, or with a different prompt corpus, it says so inline. The canonical table below supersedes any earlier figure it contradicts.

Swift against the base model, thinking on 13 SEP

ukisai/Swift-Qwen3.8-27B is a fine-tune of Qwen3.8-27B trained to penalise the reasoning-marker tokens its author associates with overthinking. The claim, made on bf16 weights over five seeds: LiveCodeBench v6 76.8% → 81.6% with 46% fewer median thinking tokens, and under one point lost across ten benchmarks. This section tests that claim on this hardware, under the author’s own conditions.

What was held equal. Both models as bartowski’s Q4_K_M GGUF: same quantizer, same llama.cpp release (b10896), a byte-identical imatrix calibration file, MTP layers at Q4_0 in both, identical 17.44 GB files. So the arms differ in the fine-tuned weights and nothing else. Served one after the other on the same card (GPU 1) by the same llama.cpp server: two slots of 40K, q8_0 KV, the model’s own MTP head as the speculator. Thinking on at the template default (xhigh), temperature 1.0, top_p 0.95, top_k 20, 32,768-token output cap, one sample per problem. A third arm runs the author’s own Q4_K_M (different imatrix, MTP at Q8_0), because that is the file people actually download.
Median output tokens per answer · thinking on · lower is better
Median output tokens per answer, base Qwen3.8-27B against Swift, thinking on 0 4,000 8,000 12,000 16,000 median output tokens per answer · thinking on · same 4-bit quantization LiveCodeBench v6 · base 14,190 LiveCodeBench v6 · Swift 7,035 Aider, per request · base 4,923 Aider, per request · Swift 2,337

LiveCodeBench v6 — 60 fixed problems from the v6 increment (Jan–Apr 2025)

BaseSwift (bartowski)Swift (author’s quant)
pass@163.3%71.7%76.7%
  easy (15) / medium (18) / hard (27)15 / 15 / 815 / 15 / 1315 / 15 / 16
Output tokens, mean16,47613,449−18%13,535−18%
Output tokens, median14,1907,035−50%8,467−40%
Swift shorter on the same problem40 of 6041 of 60
Answers cut off at the 32K cap191411
  of which passed000
Problems neither arm truncated (40)37 solved37 solved37 solved
  mean tokens on those8,4455,337−37%5,529−36%
Mean latency per problem623 s511 s527 s

Paired bootstrap on the pass@1 difference: bartowski arm [+0.0, +16.7] points, author’s quant [+3.3, +23.3]. Discordant pairs: 1 problem only the base solved, 6 (9) only Swift solved — every one of them a problem the base ran out of tokens on.

Aider polyglot — the fixed 36-exercise subset, two attempts, diff format

BaseSwift (bartowski)
Solved cold / after test feedback25.0% / 69.4%30.6% / 66.7%
Well-formed edits97.2%94.4%
Output tokens per exercise, mean / median18,555 / 17,86812,170 / 9,788−34% / −45%
Output tokens per request, mean / median9,864 / 4,9235,634 / 2,337−43% / −53%
Reasoning tokens per request, median (est.)4,6781,994−57%
Answer tokens per request, mean (est.)510487
Requests cut off at the cap, returning no answer7 of 942 of 93
Context windows exhausted72
Time spentsummed per exercise; two ran at once, so wall clock was about half9.6 h5.5 h−43%

Reasoning and answer tokens are estimated by splitting each request’s completion_tokens by the character share of reasoning_content, recorded by a logging proxy in front of the server. The answers are the same length in both arms; all of the saving is thinking.

What this does and does not confirm. Confirmed: the thinking reduction. Medians fall 40–57% against a claimed 46–58%; means fall less (18–43%) because the hardest problems run to the cap in both models. Not confirmed as stated: the accuracy gain. It is real on the scoreboard and it is an artefact of the output cap: no truncated answer ever passed, and on untruncated problems the two models score identically. The author’s +4.8 points on LiveCodeBench was measured under the same 32,768 cap and very likely has the same cause. Not resolvable here: “under 1% loss”. With 60 and 36 problems and one seed, pass rates carry ±12–15 points. What we can say is that no loss is visible. Not tested: bf16 weights, vision, the other eight benchmarks, and long-context behaviour of the quants (the author’s card warns that quality degrades below Q6_K past 32K, and Q6_K does not fit a 24 GB card with usable context).
Why the base run took three attempts. At thinking xhigh the base model writes 17,000 to 28,000 tokens for a single reply on the harder exercises. At llama.cpp’s ~25 tok/s per slot that is twelve to nineteen minutes, which is longer than Aider’s hard-coded 600-second request timeout — and its benchmark does not stream. The first run re-sent one prompt 60 times with exponential backoff and spent ten hours on two exercises. The full 225 exercises would take about three days per arm at that speed, which is why this is a 36-exercise subset. See Lessons.

The ladder

Same model, same card, same prompt. The only variable is the serving software. Every rung is the same 4-bit checkpoint — nothing was quantized further between the bottom and the top.

Decode speed by configuration · single RTX 3090
Decode speed by configuration on a single RTX 3090, 1K-token prompt 0 50 100 150 200 decode tokens/sec · single RTX 3090 · 1K-token prompt llama.cpp, out of the box 27.6 Stock vLLM, no speculation 32.8 llama.cpp, tuned (FA + q8 KV + MTP) 42.6 Patched vLLM batch profile, no spec 43.1 Stock vLLM + MTP k=4 85.6 Patched vLLM + MTP k=4 167.8 Patched vLLM + DFlash2 k=7 212.1 BEST SINGLE-STREAM

Canonical single-GPU figures

Re-measured strictly single-tenant, one configuration at a time, mean of five runs with standard deviations. 512-token greedy completions, reasoning disabled, 250 W power cap. Where an earlier section of this study disagrees, these win.

Comparator correction (31 August) — read these as single-replica figures. Every number in this table was measured with the other replica stopped. On the deployed two-replica stack the same configuration measures ~149.6 tok/s, not 167.8: a resident idle neighbour costs roughly 11% single-stream. We found this while checking whether raising the context default had cost speed — a control at the original settings with the second replica also resident reproduced 149.6, which ruled out the context change and left the deployment shape as the cause.

The figures below are not wrong; they are correct for a machine serving one replica. But if you are running two replicas behind a load balancer, expect about 11% less single-stream than this table advertises. Which condition a number belongs to is part of the number.
Configurationsingle replica, other card idle~1K prompt~55 promptAcceptanceTokens/stepTTFTPeak VRAM
Patched vLLM + DFlash2 k=7212.1 ±1.25145.7 ±1.490.6615.630.88 s22,973
Patched vLLM + MTP k=4167.8 ±0.73129.8 ±1.300.7645.290.88 s23,217
Stock vLLM + MTP k=485.6 ±0.7766.5 ±0.150.9014.601.02 s20,833
Patched vLLM batch profile, no spec43.1 ±0.680.61 s23,843
llama.cpp + MTP n=1, tuned42.6 ±0.7940.6 ±0.051.09 s17,297
Stock vLLM, no speculation32.8 ±0.0832.6 ±0.090.98 s19,969
llama.cpp, out-of-box defaults27.6 ±0.5526.9 ±0.050.12 s cache-assisted17,323

Spreads are 0.1–2.0%, far tighter than the ~5% noise floor quoted elsewhere in this study. Both facts are true and they are not the same fact: repeated measurements against one running server agree closely, but restarting the server shifts the whole set — the same card and configuration produced 165.4 then 159.6 tok/s across a restart. Treat the restart spread as the floor for comparing two configurations.

One open discrepancy, stated rather than smoothed. DFlash2 at ~1K measures 212.1 here against 181.5 in the earlier coding suite — a 17% gap. Both runs were single-tenant and this one has a ±0.6% spread across five repeats. The cause is unidentified. If you need a conservative figure for this configuration, use the lower one.

A fourth engine was added later and is not in this table because it was measured on the other card under a different comparator: ExLlamaV3 1.4.5 at EXL3 3.00 bpw with MTP reaches 94.8 ±0.5 tok/s, against 43.6 with speculation off. That is 63% of the patched stack’s speed under the matching idle-neighbour condition, and it buys 128K–200K of context on one card. Full figures, and what it costs in fidelity, are in Long-context fidelity.

Prompt length changes speed only through acceptance

A control settles what looked like a measurement error. With speculation off, stock vLLM measures 32.6 tok/s at 55 tokens and 32.8 at 1K — a 0.6% difference, i.e. none. Turn speculation on and the same engine gains 29%; the patched stack with DFlash2 gains 46%, its acceptance climbing 0.420 → 0.661. A longer prefix of coherent context is simply easier to draft against.

Configuration55 tok1K tokGainAcceptance shift
Patched vLLM + DFlash2 k=7145.7212.1+46%0.420 → 0.661
Patched vLLM + MTP k=4129.8167.8+29%0.572 → 0.764
Stock vLLM + MTP k=466.585.6+29%0.642 → 0.901
Stock vLLM, no speculation32.632.8+0.6%n/a

The consequence: any speed comparison across different prompt lengths is meaningless unless acceptance is reported alongside it. This study made exactly that mistake once, “discovering” a 20% contention error that did not exist.

Throughput under concurrency

ConfigurationC=1C=4C=16C=32C=64
Batch profile, one GPU INT8 activations, fp8 KV, no speculation525.9761.8959.8
Two replicas + load balancer MTP k=4, round-robin151.1457.4905.6

Two replicas win up to about 16 concurrent requests while keeping every individual request fast; past that, the batch profile’s INT8 tensor-core GEMMs take over. An earlier run measured the single-GPU C=64 figure at 1,023 tok/s — that measurement overlapped other GPU work, but the card was already saturated at 100% utilisation and 249 W, so saturated-throughput figures are largely contention-insensitive and the two agree within 6%.

The 64K ceiling was wrong

Correction (31 August). This page previously described ~64K as the practical bf16 limit on one card. It is not a limit — it is a config default we mistook for physics. The launcher reads MAX_LEN=${MAX_LEN:-65536} (line 74 of start_qwen.sh), an overridable variable. Meanwhile the server itself was reporting GPU KV cache size: 89,437 tokens at those very settings — we were serving 37% less context than the card already held.
Attemptgpu_utilmax_num_seqsKV capacityResult
65,5360.93889,437the old default
85,0000.96490,212loads & serves
90,0000.97293,616loads & serves
85,0000.968103,042APPLIED — now live
110,0000.964refused

vLLM states the constraint precisely when you exceed it — “7.56 GiB KV cache is needed, which is larger than the available KV cache memory (6.31 GiB)” — so the right behaviour is to surface that message, not to pre-emptively refuse. KV scales at roughly 13,700 tokens per GiB on this model, which is unusually generous because only 16 of its 64 layers hold attention KV; the other 48 use fixed-size GDN recurrent state.

The real single-card bf16 ceiling is ~90,000. Reading the middle rows of that table, we concluded more context had to be bought by cutting concurrency — max_num_seqs falling 8 → 4 → 2.

Correction (31 August) — the concurrency trade was not necessary. We assumed the extra context had to come out of max_num_seqs. It didn’t. Raising gpu_util from the launcher’s 0.93 to 0.96 lifts KV capacity to 103,042 tokens, which carries 85,000 with all eight concurrency slots intact. Even the untouched 0.93 config had a ceiling of 79,488 — +21% available for free before anything was tuned. The earlier reading mistook a knob we had happened to turn for a constraint we had to accept.

85,000 is now applied and live on both replicas, verified by serving rather than merely loading: a 78,652-token prompt returned a correct completion in 86 s, and /v1/models reports 85,000 on the load balancer and both replicas. The speed cost is nil — 148.5 ±0.1 tok/s at the new setting against a 149.6 ±0.2 control at the old one, a 0.7% difference well inside noise. The panel’s derived client guidance moved with it: max output at an 8K prompt reservation rose from 57,344 to 76,808.

Context scaling to 64K

Provenance. These curves were measured with two sweeps running concurrently on the two cards. Cross-GPU contention depresses batch-1 decode by up to 39%, so the vLLM curves and the llama.cpp 1×GPU curve read low. The llama.cpp 2×GPU curve owned both cards and is clean. The shapes survive; the cross-configuration ranking does not.
Decode speed versus prompt length
Decode speed versus prompt length 0 20 40 60 80 100 512 2K 8K 16K 32K prompt tokens (log scale) 61 vLLM DFlash2 41 vLLM MTP 37 llama.cpp 2×GPU 34 llama.cpp 1×GPU 30 llama.cpp DFlash2
Time to first token versus prompt length
Time to first token versus prompt length 0 20 40 60 80 100 512 2K 8K 16K 32K prompt tokens (log scale) 103 vLLM MTP 70 vLLM DFlash2 69 llama.cpp 1×GPU 61 llama.cpp 2×GPU

Time to first token is the honest cost of long context, and it is brutally linear: +1.14 seconds per 1,000 tokens, correlation 1.00. At 61K a request waits 69 seconds before its first token — roughly six times the entire decode time of a 500-token reply. Speculative decoding accelerates generation, not prefill, so the speculator is nearly irrelevant here and prefix caching is the only lever that matters.

Beyond 64K

The 64K ceiling above is the deployed profile’s, not the model’s — Qwen3.8 is 262K native. Reaching further means changing the KV cache, and each option pays differently.

Decode speed beyond 64K context
Decode speed beyond 64K context 0 10 20 30 40 50 60 70 64K 100K 150K 200K prompt tokens (log scale) 64 CTX=long MTP (fp8) 39 CTX=long DFlash2 (int8) 25 llama.cpp q8 KV 24 CTX=huge KVarN 4/2-bit
ProfileKV cacheMax servedDecode at maxProvenance
vLLM CTX=long + MTP k=3fp8 (FlashInfer)149,52563.9contended true value is higher
vLLM CTX=long + DFlash2int8 per-token-head128,01338.7partly contended
vLLM CTX=huge + MTPKVarN 4/2-bit192,51723.7clean
llama.cpp + MTP n=1q8_0151,53025.2contended

Two caveats on the KVarN row. Its documentation describes a 245,760-token ceiling, but the launcher resolved max-model-len to 200,000 on this host, so 192.5K is the deepest point measured rather than a hard limit. llama.cpp fails to allocate past roughly 160K on a single card — -c 200000 OOMs at load.

The fp8 profile gets faster with depth — 51.9 → 63.9 tok/s from 68K to 150K. That is backwards for attention cost, and it is the acceptance effect again: a longer prefix of coherent documentation is easier to draft against. Since this row is also the contended one, its true figures are higher still.

Correctness

Speed is half the question. Speculative decoding cannot answer the other half — it is mathematically exact, so a speculator can never change what the model would have said. Any quality difference comes from the quantization or the KV dtype.

Aider polyglot pass rate by language
Aider polyglot pass rate by language, first attempt versus after test feedback 0% 10% 20% 30% 40% 50% 60% 70% python n=34 59 cpp n=26 58 javascript n=49 55 java n=47 47 go n=38 45 rust n=30 27 first attempt after test feedback
RunCasespass_1pass_2Well-formeds/case
Hand-driven harness22412.5%48.7%90.6%97
Framework, independent repeat22514.7%49.8%90.2%138
The gap is the finding, not the score. Twenty-eight exercises were solved cold; eighty-one more were solved only after the model saw failing test output — a 3.9× improvement from feedback alone. For anything agentic, plumbing test output back to the model matters more than any serving-configuration choice on this page.

The two runs above are the same suite measured twice, and they disagree by 2.2 points on pass_1 and by 13 points on Rust (26.7% versus 40.0%, n=30). Both used greedy decoding at temperature zero, which is nominally deterministic — but at four concurrent threads, batch composition varies and reduction order flips near-tie tokens. Aggregate scores resolve to roughly ±2 points; per-language scores only to about ±10. Read the per-language bars as indicative.

Three of the 225 cases (1.3%) failed for infrastructure reasons rather than model capability: one context exhaustion, one test timeout, and one HTTP 413 caused by an unset client_max_body_size on the reverse proxy. The remaining 24 malformed responses across 21 cases are genuine diff-format failures, concentrated in C++.

Agentic behaviour

Everything above is single-turn against a clean context. To reach the multi-turn regime, a coding agent built Tetris from scratch across 68 assistant turns, context growing 5.4K → 61.5K.

Acceptance and decode speed versus accumulated context
Draft acceptance and decode speed versus accumulated context 0 20 40 60 80 100 120 140 160 5K 15K 25K 35K 45K 60K accumulated context (band midpoint) 108 decode tok/s 68 acceptance %
Context bandTurnsAcceptanceDecode tok/sTTFTTokens/step
0–10K765.3%137.35.9 s3.61
10–20K1173.3%140.812.5 s3.93
20–30K577.3%137.222.2 s4.09
30–40K772.3%124.633.1 s3.89
40–50K1669.1%113.848.6 s3.76
50–70K868.5%108.561.3 s3.74
Acceptance does not decay with context. We expected the 78–81% single-turn repo-edit acceptance to collapse by turn 8 with 40K of history. It doesn’t — the slope is −0.03 points per 1K tokens, r=−0.05, i.e. flat. An independent cross-check with a completely different agent harness reproduced it (−0.063 pts/1K, r=−0.03). What degrades is attention cost over a growing KV cache, not speculation. The instinctive fix — reducing draft depth — does nothing.

A natural experiment confirms it: the agent compacted its context at turn 52, cutting 61.5K → 30.4K. The twelve turns before averaged 70.8% acceptance and 113.1 tok/s; after, 67.4% and 112.2. Freeing context bought nothing — exactly what you would predict if acceptance was never context-limited.

Why acceptance varies so much

Draft acceptance is the causal variable behind almost all of the tok/s spread on this page. It is determined by the task, far more than by the configuration.

Draft acceptance by task type · patched vLLM, MTP k=4
Speculative draft acceptance by task type, MTP k=4 0% 20% 40% 60% 80% draft acceptance · patched vLLM, MTP k=4 Repo edit, 32K ctx 81% Repo edit, 1K–8K 78% Code review, 32K 76% Boilerplate CLI 67% Canonical (π, fizzbuzz) 61% Code review, 1K 56% Summarize, 1K 54% Summarize, 8K 52% Summarize, 32K 44%

Repo edits accept highest — emitting a diff means reproducing code already in the context, and acceptance there rises with more surrounding code. Prose summarization is the floor. Computing π was predicted to be the memorized ceiling and turned out to be near the bottom of the coding classes, because dense numeric code is not stereotyped boilerplate.

Draft acceptance by position within the speculated block
Draft acceptance by draft position, code versus prose 0% 25% 50% 75% 100% 0 1 2 3 4 5 6 draft position (0 = first speculated token) 69% MTP k=4 · code 55% DFlash2 k=7 · code 30% MTP k=4 · prose 12% DFlash2 k=7 · prose

Each speculated token is conditioned on the ones before it, so acceptance falls with draft depth — and the rate of that fall is task-dependent, not a property of the hardware. On a repo edit, DFlash2 still lands 55% of its seventh draft token, so deep drafting pays. On prose the same speculator collapses to 12%, meaning positions 5–7 are verify compute spent on tokens that will be discarded. This is the mechanism behind every optimal-draft-depth result in this study — and behind the earlier finding that n=4 beat a model card’s recommended n=7, which was measured on prose.

Which numbers to trust

SectionVerdictNote
Canonical single-GPU figurescleanSingle-tenant, mean of 5, supersedes all earlier speed numbers
Correctness · both runscleanCorrectness is contention-immune
Agentic profile & cross-checkcleanSingle-tenant throughout
Acceptance by task & by positioncleanSequential runs, one config at a time
Throughput, two-replicacleanBoth GPUs is the configuration
Context scaling ≤64KmixedvLLM and llama.cpp 1×GPU curves contended; 2×GPU clean
Beyond 64KmixedOnly the KVarN row is clean; see the per-row column
Single-card context ceilingcleanServer-reported KV capacity; supersedes the ~64K claim
TP=2 figurescleanSingle-tenant — but the penalty is specific to this board’s PCIe wiring
KV-quantization armssmall-n72 cases; arm-to-arm only, and probed only at ≤64K
Long-context fidelity (NLL & probes)cleanDedicated single-request container, determinism floor zero; supersedes the divergence table

Which speculator to serve: MTP vs DFlash2, both cards

Single-GPU DFlash2 is the fastest configuration we measured (212.1 tok/s). The obvious next question — run it on both replicas and get that speed plus concurrency — went unanswered for most of this study, and shipped in the control panel as an untested profile. This settles it.

Concurrent requestsbalancedMTP k=4, both replicasfast-pairDFlash2 k=7, both replicasVerdict
1123.1 ±2.8129.2 ±5.3noise
4327.0336.2noise
8464.9410.7balanced +12%
16610.3401.6balanced +34%
24432.5441.0noise
32443.5402.9balanced +9%
48401.7387.3noise
64348.6426.9fast-pair +22%

Aggregate tok/s through the load balancer, ~1K-token prompts, 512-token completions. C=1 is the mean of five with standard deviation; the rest are single runs.

Keep MTP. DFlash2 on both cards is not the upgrade it looks like. At the concurrency that matters for a shared endpoint — eight to sixteen simultaneous requests, i.e. a handful of people with coding assistants — MTP delivers 12–34% more throughput. DFlash2 saturates around 400–440 tok/s from C=8 onward and stops scaling entirely, while MTP keeps climbing.

The mechanism is visible in the replicas' own counters. Under this load DFlash2 ran at 46.9% draft acceptance against MTP's ~76%. It proposes seven tokens per step and discards most of them, and that wasted verification work is precisely what a saturated GPU cannot spare. The same property that makes it excellent for one user — drafting deep and often being right — becomes a liability once the verify batches are already full.

It also answers the other half of the question: pairing DFlash2 does not preserve its single-GPU advantage. At C=1 both configurations land near 125 tok/s through the load balancer, nowhere near the 212 a single dedicated GPU delivers. One request goes to one replica; the second card contributes nothing but its presence.

No failure at any load. We pushed to C=64 expecting an out-of-memory kill, since DFlash2 keeps a separate drafter resident and leaves only ~1.6 GB of headroom per card. It never came. Zero failed requests across every level, VRAM steady at 23.2 GB, no Xid events. The configuration is safe — it simply is not faster.
Why the high-concurrency rows are not trustworthy. Switching between these profiles requires recreating the replicas, so every cross-arm comparison crosses a server restart — and restart drift on this machine is worth 4–6%. C=1 through C=16 were measured within a single session and compare cleanly. C=24–64 for balanced were measured afterwards, so the C=64 result favouring fast-pair is unproven. balanced peaking at C=16 (610) and then falling at C=24 (432) is also non-monotonic in a way that suggests that 610 may itself be optimistic. The C=8 and C=16 gaps are large enough to survive the doubt; nothing else here is.
An operational trap this surfaced. Recreating the replicas gives them new addresses on the Docker network, but nginx resolves its upstreams once, at config load. Every request returned 502 Bad Gateway until the load balancer was restarted too. Any tooling that switches profiles must restart the balancer after recreating a replica, or the first switch will look like a total outage.

Tensor parallel: the full native window, at a price

Two cards had only ever been used as two independent replicas. Splitting one model across both — tensor parallelism — was untested until now, and it changes the context picture entirely.

MeasureSingle GPUTP=2Change
KV cache capacity89,437377,518×4.2
Max context verified served~90,000204,058
Decode, C=1167.8119.0 ±3.4−29%
Throughput, C=16610234−62%
Draft acceptance~76%48.8%

That 4.2× exceeds the model’s own 262,144 native window, so bf16 under TP reaches the architectural ceiling — and the fp8 and KVarN profiles become pointless there, since they exist only to stretch a single card. A 204,058-token prompt was verified served, not merely loaded, completing in 311 s at roughly 656 tok/s of prefill.

Pipeline parallelism is not available at all. PP=2 raises NotImplementedError: Pipeline parallelism is not supported for this model. Qwen3_5ForConditionalGeneration does not implement vLLM’s SupportsPP interface, so it is unreachable regardless of settings — a model-implementation gap in vLLM core, not a tuning problem.
The TP penalty is this machine’s wiring, not a general result. Do not generalise the −29% / −62% figures. GPU 0 sits on a CPU root port at x8; GPU 1 sits behind the PCH/chipset at x4. With Custom allreduce is disabled because your platform lacks GPU P2P capability, NCCL staged through host memory, so every allreduce ran GPU0 → RAM → DMI → PCH → GPU1. On a board with both cards CPU-attached at x8 or x16 this would very likely look different. We also do not recommend running it here at all — see the fault analysis in Lessons.

KV-cache quantization is not free

Every quality number in this study before now was measured on bf16 KV. The weights have always been 4-bit; the cache dtype was the untested variable, and it is the one that governs how much context fits on a single card. Measured on a fixed, deterministic 72-exercise subset.

KV dtypepass_1pass_2Well-formedDecode tok/sMax contextVRAM
bf1620.8%41.7%95.8%158.8 ±3.265,53622.7 GB
fp8 e4m3fn18.1%36.1%95.8%124.4 ±2.0150,00021.7 GB
KVarN 4/2-bit n=3613.9%50.0%91.7%117.6 ±2.3200,00021.8 GB

These scores are comparable to each other only. They are a 72-exercise subset and must not be read against the 225-case reference of 12.5 / 48.7 / 90.2.

fp8 is 22% slower than bf16, not faster. 124.4 against 158.8 tok/s. Quantizing the cache saves memory but adds dequantization work to every attention operation. Its only benefit is context. The −5.6-point pass_2 gap sits inside the resolution limit of a 72-case run, so it is not a proven regression — but “changes outputs, scores directionally worse, runs 22% slower” is not a case for making it the default.

Both quantized caches change what the model says

Each arm is internally deterministic. Compared against the bf16 reference, however, both diverge within the first ~100–200 characters at every length tested — early, not merely at depth.

Contextfp8 diverges atKVarN diverges at
8Kchar 224char 93
32Kchar 104char 204
64Kchar 105char 105
KVarN’s 50.0% is not evidence that it is better. It is n=36 on a different subset, and it carries the worst secondary indicators of the three: well-formed falls to 91.7%, error cases double, and it logged 3 test timeouts against bf16’s zero. More errors, more timeouts, lower well-formed is what degradation looks like; the headline number is noise on a small sample. If you offer this profile to users, it needs a quality caveat attached.
Scope limit, stated plainly. All three arms were probed at ≤64K, because the bf16 reference cannot exceed 65,536. This demonstrates that KV quantization changes output at lengths bf16 can also serve. It does not test fidelity at the 150K and 200K depths those profiles exist for — which is precisely where anyone would use them. That remains unmeasured, and we have no clean way to measure it without a reference of the same size.

Superseded, 3 September. There was a clean way, and it does not need a reference of the same size — see Long-context fidelity below. The conclusion reverses: measured properly, fp8 KV is indistinguishable from bf16 all the way to 150K.

Long-context fidelity: the measurement we said could not be made

The scope limit above stood for three days: the fp8 and KVarN profiles exist for 150K and 200K, and every fidelity number we had was taken at ≤64K, because the bf16 reference cannot go further. The way out was not a bigger reference. It was a different instrument.

Correction — “both quantized caches change what the model says” was measured the wrong way. Free-running greedy divergence generates from the same prompt under two profiles and counts characters until they differ. That number compounds: one near-tie token flipped by rounding sends the two continuations down different paths, so “diverges at char 105” records where the first coin-flip landed, not how wrong the cache is. It also cannot exist above the reference’s ceiling. The divergence table is not wrong, but it does not measure what we read into it.

The replacement is teacher-forced excess negative log-likelihood. Feed a long document as the prompt, ask the server for the log-probability of every prompt token (prompt_logprobs), and report mean NLL per 8K-token bucket of position. The ground truth is the document itself, so no reference model is needed and the metric is defined at 200K exactly as at 8K. Teacher forcing pins the context, so nothing compounds: a rounding error at position p affects the prediction at p+1 and nothing else. And because prefill is chunked, almost everything a token at position 150K attends to is read back through the quantized cache — which is the thing under test.

Memorised text would defeat it, so the document set is five frozen documents of ≥200K tokens each: three the model wrote itself (202,296 / 214,368 / 212,578 tokens — a novel, an engineering design document, an essay sequence), and two built from 22 arXiv papers apiece submitted between 20 July and 1 September 2026, after the model’s assumed training cut (207,877 and 206,061 tokens). Repeat runs of the harness are bit-identical, so the determinism floor is exactly zero and any excess NLL is signal.

Excess NLL against bf16, 0–76K

Mean and worst 8K bucket, in nats per token, against the patched stack at bf16 KV. The threshold, written down before the run, was 0.02 nats.

Profilegen1gen2gen3nat1nat2top-1 agreeminimum
vLLM fp8 e4m3fn+0.008 / +0.025−0.005 / +0.001+0.001 / +0.001−0.033 / +0.006−0.026 / +0.0220.921
vLLM KVarN 4/2-bit+0.011 / +0.030+0.027 / +0.116+0.005 / +0.007+0.025 / +0.084−0.043 / +0.0080.830
EXL3 3.0 bpw, fp16 cache+0.025 / +0.067+0.011 / +0.036+0.034 / +0.043+0.127 / +0.213+0.026 / +0.1370.744
EXL3 3.0 bpw, q8 cache+0.025 / +0.067+0.011 / +0.036+0.034 / +0.043+0.130 / +0.215+0.028 / +0.1480.743
EXL3 4.0 bpw, q8 cache+0.024 / +0.039−0.013 / +0.020+0.017 / +0.022+0.280 / +0.385+0.256 / +0.4250.761

gen1–gen3 are the model’s own text (mean NLL 0.2–0.5); nat1–nat2 are recent arXiv prose (mean NLL ~4.7). Negative numbers are not evidence that a quantized cache is better than bf16 — they are the two attention paths (FlashAttention bf16 against FlashInfer fp8) differing by kernel numerics.

The document is the experiment. On text the model wrote itself, every profile looks fine — the predictions are so easy that a degraded cache has nothing to lose. Natural prose the model has never seen is where quantization shows up, and it makes the effect several times larger: EXL3 at 3.0 bpw costs +0.011 to +0.034 nats on the generated documents and +0.127 on arXiv prose. Any long-context fidelity claim measured on self-generated or memorised text is mostly measuring how easy the text is.
fp8 KV is indistinguishable from bf16. Worst bucket anywhere: +0.025 nats, on the model’s own text; on both natural documents the mean is negative and the worst bucket is +0.006 and +0.022. Top-1 agreement never drops below 92%. The single bucket that grazes the 0.02 threshold sits on the model’s own text, where the absolute NLL is 0.2–0.5 and every profile looks clean anyway.

Past the reference: cross-profile agreement at 80–150K

Above 76K there is no bf16 to compare against, so the profiles are compared to each other, with fp8 as the anchor because it matched bf16 everywhere it could be checked. Figures are “other minus fp8”, mean / worst bucket, then minimum top-1 agreement.

DocumentKVarN vs fp8EXL3 3.0 bpw q8 vs fp8
gen1+0.005 / +0.008 · 0.962+0.045 / +0.057 · 0.915
gen2+0.005 / +0.009 · 0.976+0.028 / +0.036 · 0.947
gen3+0.004 / +0.006 · 0.972+0.032 / +0.042 · 0.929
nat1+0.072 / +0.129 · 0.833+0.008 / +0.158 · 0.748
nat2+0.011 / +0.096 · 0.846+0.083 / +0.257 · 0.757

fp8 shows no inflection with depth on any of the five documents. Its 80–150K bucket means track the difficulty of the text, and on the generated documents they are flat. KVarN drifts. Its gap to fp8 on nat1 roughly triples between the sub-76K band (+0.025 mean) and the 80–150K band (+0.072 mean, +0.129 worst); on generated text it stays inside 0.01. So it degrades with depth, mildly, and only where the text is hard. Its 150–200K band could be cross-checked against nothing but EXL3 at q8, which is itself worse. EXL3’s gap at depth is the same size as its gap to bf16 in the overlap band — a weight-quantization cost, flat with depth, not a cache effect.

One of our own pass/fail criteria had to be retired. The proposal said “no bucket past 80K may rise more than 0.05 nats above the 57–76K mean”. On gen3, fp8 and KVarN both rise by +0.075 — while agreeing with each other to 0.005 nats. The rise is the document getting harder at that point, not the cache getting worse. An absolute-threshold criterion on a metric that tracks content is content-confounded; the cross-profile comparison is the one that survives.

Known-answer probes at depth

NLL says whether the distribution moved. It does not say whether a user gets a wrong answer. Three probes with known answers — eight-needle key/value retrieval, verbatim copy of a marked paragraph, and a three-step cross-reference chain — run at depth against filler drawn from the frozen documents.

ProfileDepthsMulti-keyVerbatim copyCross-reference
bf16 n=1016K / 32K / 64K80/80100%10/10
fp8 n=564K / 128K / 147K40/40 each100% each5/5 each
KVarN n=564K / 128K / 196K40/40 each100% each5/5 each
EXL3 3.0 bpw n=564K / 123K40/40 each100% each5/5 each

Every probe instance succeeded at every depth for every profile — KVarN at 196K and EXL3 3.0 bpw at 123K included. So the fidelity cost measured above does not surface as a retrieval, copying or multi-hop failure at these depths. It is a distribution shift, not a capability loss — which is exactly why it needed a distributional instrument to see at all.

The first EXL3 probe run scored 0 on every instance, and the model was fine. probes.py assumed a non-null usage field on the completion response; TabbyAPI returns usage: null, so the scorer raised on every row and recorded a zero. The server’s own log showed HTTP 200s and sensible generations throughout. A harness that turns its own exception into a score of zero will report a catastrophic model failure with total confidence — which is trap 1 in a new costume. Fixed and rerun; the corrected numbers are the row above.

ExLlamaV3: the context is real, the speed is not

The other route to long context on one card is a smaller checkpoint on a different engine. EXL3 at 3.0 bpw fits far more cache than the 4-bit compressed-tensors weights do, and — contrary to what we assumed when we closed the sub-4-bit line — ExLlamaV3 does support MTP speculative decoding on this model. Measured on GPU 1 with the GPU 0 replica idle, the same condition as the 149.6 comparator.

ExLlamaV3 1.4.5, 3.00 bpwDecode tok/sDraft acceptanceVRAM free after load
MTP k=4, fp16 cache 32K94.8 ±0.570.5%6.42 GiB
No speculation, 32K43.6 ±0.36.43 GiB
MTP k=4, fp16 cache 128K93.8 ±0.670.3%1.03 GiB
MTP k=4, q8 cache 200K91.6 ±0.969.2%2.23 GiB

MTP is worth 2.2× here, and acceptance matches vLLM’s 70–76%, so EXL3 avoids the wall llama.cpp hit. What it does not do is close the gap: 94.8 against the patched stack’s 149.6 under the same conditions is 63% of the speed. What it buys is context on one card — 128K at fp16 KV, 200K at q8 — and decode is flat across cache sizes. The fp16 ceiling is sharp: with no draft model resident, 131,072 tokens loads with 0.3 GiB of headroom and 135,168 already fails autosplit.

And the published 4.0 bpw files do not rescue it. EXL3 at 3.0 bpw costs +0.127 nats on nat1 with 74% top-1 agreement, so the obvious move is to spend the VRAM on 4.0 bpw. It does not work, and we chased the anomaly to the end before saying so.
ControlResult
3.0 bpw q8, repeated a day laterBit-identical — the harness is deterministic
Plain 4.0, fp16 vs q8 cache, nat1 at 32K0.01 nats, 98.6% top-1 — cache quantization is irrelevant here
Plain 4.0 vs self-calibrated SC_4.00bpw_H5, nat1 at 64KSC is worse still: +0.12 to +0.28 per bucket, 84–88% top-1
Excess vs bf16 on nat1, per 8K bucket3.0: +0.08 to +0.22 · plain 4.0: +0.19 to +0.39 · SC 4.0: +0.35 to +0.54
ExLlamaV3’s own eval/ppl.py, wikitext-2, 100 rows × 20483.0 bpw 6.99 · plain 4.0 7.03 · SC 4.0 7.00
By ExLlamaV3’s own perplexity tool the two published 4.0 bpw files are no better than the 3.0 bpw file on wikitext-2, and on recent arXiv prose they are clearly worse. The 3.0 file is the best of the three as published. Why 4.0 is so much worse specifically on the arXiv documents is unexplained — calibration-set mismatch or a bad quantization run upstream are the candidates, and both are outside our control. The practical consequence is what matters: EXL3 cannot improve its fidelity case by spending more bits.

The decision: fp8 KV at 150K

For context past 85,000 tokens on this hardware, the answer is the patched vLLM stack with fp8 KV at 150K. It has no measurable fidelity cost against bf16 anywhere the two overlap, no inflection with depth to 150K, and 100% on every known-answer probe to 147K. The only price is the one we already knew about: 22% slower than bf16, 124.4 against 158.8 tok/s.

Two operational facts, if you reproduce this. prompt_logprobs cannot be served by a production replica: the first such request OOM-killed the GPU 0 engine, because vLLM log-softmaxes the full 248K vocabulary in fp32 for every scheduled prompt token and there is about 250 MiB spare at gpu_util 0.96. Fidelity runs need a dedicated container at 0.92 with --max-num-batched-tokens 256, one request at a time — which is also why the bf16 reference ceiling here is 76.8K rather than 85K. And it is slow: prefill on that 256-token chunk takes 100 s at 76K, 290 s at 150K, 465 s at 200K; ExLlamaV3 takes 234 s for 131K at fp16 and 465 s for 205K at q8.

Unsloth: resolved, and the answer is no

Verified from the repository manifest in the local cache rather than from documentation: Unsloth ships GGUF only for Qwen3.8-27B — 30 variants, no AWQ, GPTQ or AutoRound exports, and their NVFP4 build is Blackwell-only and unusable on Ampere.

The decisive constraint is vLLM’s own documentation, which describes its GGUF support as “highly experimental and under-optimized”, notes it has been moved out-of-tree to a plugin, and states that “the only confirmed use case is memory footprint reduction”. So Unsloth’s genuine quality-per-byte advantage stays locked inside llama.cpp, at 27.6–42.6 tok/s against the patched stack’s 167.8 — roughly 4× the speed to buy it.

Minor correction. This study previously listed UD-Q4_K_XL at 17.92 GB. The repository manifest gives 17.56 GB.

Sub-4-bit on the fast stack is closed

Weights and KV cache compete for the same 24 GB, and KV scales at ~13,700 tokens per GiB, so every GiB shaved off the weights buys roughly 13,700 tokens of context at full bf16 fidelity and full speed. Our weights are ~17.5 GiB. That arithmetic makes a smaller checkpoint the most attractive remaining route to more context — and it is why we looked at quantizing the model ourselves to ~3.5 bits in compressed-tensors format, the only format the fast stack accepts.

It cannot work, and the blocker is upstream of any effort we could make. vLLM’s Marlin kernel supports 4-bit and 8-bit weights only. Read from the running container: marlin_utils.py returns uint4 / uint4b8 / uint8b128, and there is no num_bits == 3 path anywhere in the quantization layer. uint3b4 exists in the scalar-type registry but is absent from Marlin’s supported list. A 3.5-bit compressed-tensors checkpoint would be refused by the stack that was the entire reason for making it — so we stopped at the feasibility gate rather than spending a 56 GB download and hours of calibration to produce a file nothing here can load.

That leaves EXL3 (3.0 bpw ≈ 12.89 GiB, which would imply roughly 150K of context on one card) as the only remaining route to a smaller checkpoint — and it requires leaving the fast stack for ExLlamaV3. Whether that stack supports MTP speculative decoding on this GDN hybrid is unknown, and worth settling before committing to a 15 GB download: without speculation it would hit precisely the wall llama.cpp did.

Correction — group-32 was never vLLM’s fault. Earlier we recorded a group-32 checkpoint (cyankiwi) failing to load, and treated the group size as the obstacle. Inspecting the running container gives MARLIN_SUPPORTED_GROUP_SIZES = [-1, 32, 64, 128]vLLM supports group-32 perfectly well. The failure was the syv-ai prep script’s hard-coded GROUP = 128, not a kernel limitation. Our prepare/_groupsize.py fix is therefore aimed at the right target, though it remains unverified against a live serve.

Conditions. All figures: Qwen3.8-27B at 4-bit (AutoRound W4A16 “fast” variant with int4 lm_head), 2× RTX 3090 capped at 250 W with persistence mode, reasoning disabled via chat_template_kwargs, prompt caching off, greedy decoding at temperature 0, 512-token completions unless stated.

Not comparable to published leaderboards. Different quantization, different harness versions, reasoning disabled and a different serving stack all move these numbers independently. They are honest measurements of one deployment, not claims about the model’s capability. See Lessons for the fairness contract, and Log for the full chronological record including the wrong turns.