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.
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.
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.
LiveCodeBench v6 — 60 fixed problems from the v6 increment (Jan–Apr 2025)
| Base | Swift (bartowski) | Swift (author’s quant) | |
|---|---|---|---|
| pass@1 | 63.3% | 71.7% | 76.7% |
| easy (15) / medium (18) / hard (27) | 15 / 15 / 8 | 15 / 15 / 13 | 15 / 15 / 16 |
| Output tokens, mean | 16,476 | 13,449−18% | 13,535−18% |
| Output tokens, median | 14,190 | 7,035−50% | 8,467−40% |
| Swift shorter on the same problem | — | 40 of 60 | 41 of 60 |
| Answers cut off at the 32K cap | 19 | 14 | 11 |
| of which passed | 0 | 0 | 0 |
| Problems neither arm truncated (40) | 37 solved | 37 solved | 37 solved |
| mean tokens on those | 8,445 | 5,337−37% | 5,529−36% |
| Mean latency per problem | 623 s | 511 s | 527 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
| Base | Swift (bartowski) | |
|---|---|---|
| Solved cold / after test feedback | 25.0% / 69.4% | 30.6% / 66.7% |
| Well-formed edits | 97.2% | 94.4% |
| Output tokens per exercise, mean / median | 18,555 / 17,868 | 12,170 / 9,788−34% / −45% |
| Output tokens per request, mean / median | 9,864 / 4,923 | 5,634 / 2,337−43% / −53% |
| Reasoning tokens per request, median (est.) | 4,678 | 1,994−57% |
| Answer tokens per request, mean (est.) | 510 | 487 |
| Requests cut off at the cap, returning no answer | 7 of 94 | 2 of 93 |
| Context windows exhausted | 7 | 2 |
| Time spentsummed per exercise; two ran at once, so wall clock was about half | 9.6 h | 5.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.
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.
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.
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 prompt | Acceptance | Tokens/step | TTFT | Peak VRAM |
|---|---|---|---|---|---|---|
| Patched vLLM + DFlash2 k=7 | 212.1 ±1.25 | 145.7 ±1.49 | 0.661 | 5.63 | 0.88 s | 22,973 |
| Patched vLLM + MTP k=4 | 167.8 ±0.73 | 129.8 ±1.30 | 0.764 | 5.29 | 0.88 s | 23,217 |
| Stock vLLM + MTP k=4 | 85.6 ±0.77 | 66.5 ±0.15 | 0.901 | 4.60 | 1.02 s | 20,833 |
| Patched vLLM batch profile, no spec | 43.1 ±0.68 | — | — | — | 0.61 s | 23,843 |
| llama.cpp + MTP n=1, tuned | 42.6 ±0.79 | 40.6 ±0.05 | — | — | 1.09 s | 17,297 |
| Stock vLLM, no speculation | 32.8 ±0.08 | 32.6 ±0.09 | — | — | 0.98 s | 19,969 |
| llama.cpp, out-of-box defaults | 27.6 ±0.55 | 26.9 ±0.05 | — | — | 0.12 s cache-assisted | 17,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.
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.
| Configuration | 55 tok | 1K tok | Gain | Acceptance shift |
|---|---|---|---|---|
| Patched vLLM + DFlash2 k=7 | 145.7 | 212.1 | +46% | 0.420 → 0.661 |
| Patched vLLM + MTP k=4 | 129.8 | 167.8 | +29% | 0.572 → 0.764 |
| Stock vLLM + MTP k=4 | 66.5 | 85.6 | +29% | 0.642 → 0.901 |
| Stock vLLM, no speculation | 32.6 | 32.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
| Configuration | C=1 | C=4 | C=16 | C=32 | C=64 |
|---|---|---|---|---|---|
| Batch profile, one GPU INT8 activations, fp8 KV, no speculation | — | — | 525.9 | 761.8 | 959.8 |
| Two replicas + load balancer MTP k=4, round-robin | 151.1 | 457.4 | 905.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
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.
| Attempt | gpu_util | max_num_seqs | KV capacity | Result |
|---|---|---|---|---|
| 65,536 | 0.93 | 8 | 89,437 | the old default |
| 85,000 | 0.96 | 4 | 90,212 | loads & serves |
| 90,000 | 0.97 | 2 | 93,616 | loads & serves |
| 85,000 | 0.96 | 8 | 103,042 | APPLIED — now live |
| 110,000 | 0.96 | 4 | — | refused |
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.
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
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.
| Profile | KV cache | Max served | Decode at max | Provenance |
|---|---|---|---|---|
| vLLM CTX=long + MTP k=3 | fp8 (FlashInfer) | 149,525 | 63.9 | contended true value is higher |
| vLLM CTX=long + DFlash2 | int8 per-token-head | 128,013 | 38.7 | partly contended |
| vLLM CTX=huge + MTP | KVarN 4/2-bit | 192,517 | 23.7 | clean |
| llama.cpp + MTP n=1 | q8_0 | 151,530 | 25.2 | contended |
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.
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.
| Run | Cases | pass_1 | pass_2 | Well-formed | s/case |
|---|---|---|---|---|---|
| Hand-driven harness | 224 | 12.5% | 48.7% | 90.6% | 97 |
| Framework, independent repeat | 225 | 14.7% | 49.8% | 90.2% | 138 |
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.
| Context band | Turns | Acceptance | Decode tok/s | TTFT | Tokens/step |
|---|---|---|---|---|---|
| 0–10K | 7 | 65.3% | 137.3 | 5.9 s | 3.61 |
| 10–20K | 11 | 73.3% | 140.8 | 12.5 s | 3.93 |
| 20–30K | 5 | 77.3% | 137.2 | 22.2 s | 4.09 |
| 30–40K | 7 | 72.3% | 124.6 | 33.1 s | 3.89 |
| 40–50K | 16 | 69.1% | 113.8 | 48.6 s | 3.76 |
| 50–70K | 8 | 68.5% | 108.5 | 61.3 s | 3.74 |
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.
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.
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
| Section | Verdict | Note |
|---|---|---|
| Canonical single-GPU figures | clean | Single-tenant, mean of 5, supersedes all earlier speed numbers |
| Correctness · both runs | clean | Correctness is contention-immune |
| Agentic profile & cross-check | clean | Single-tenant throughout |
| Acceptance by task & by position | clean | Sequential runs, one config at a time |
| Throughput, two-replica | clean | Both GPUs is the configuration |
| Context scaling ≤64K | mixed | vLLM and llama.cpp 1×GPU curves contended; 2×GPU clean |
| Beyond 64K | mixed | Only the KVarN row is clean; see the per-row column |
| Single-card context ceiling | clean | Server-reported KV capacity; supersedes the ~64K claim |
| TP=2 figures | clean | Single-tenant — but the penalty is specific to this board’s PCIe wiring |
| KV-quantization arms | small-n | 72 cases; arm-to-arm only, and probed only at ≤64K |
| Long-context fidelity (NLL & probes) | clean | Dedicated 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 requests | balancedMTP k=4, both replicas | fast-pairDFlash2 k=7, both replicas | Verdict |
|---|---|---|---|
| 1 | 123.1 ±2.8 | 129.2 ±5.3 | noise |
| 4 | 327.0 | 336.2 | noise |
| 8 | 464.9 | 410.7 | balanced +12% |
| 16 | 610.3 | 401.6 | balanced +34% |
| 24 | 432.5 | 441.0 | noise |
| 32 | 443.5 | 402.9 | balanced +9% |
| 48 | 401.7 | 387.3 | noise |
| 64 | 348.6 | 426.9 | fast-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.
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.
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.
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.
| Measure | Single GPU | TP=2 | Change |
|---|---|---|---|
| KV cache capacity | 89,437 | 377,518 | ×4.2 |
| Max context verified served | ~90,000 | 204,058 | — |
| Decode, C=1 | 167.8 | 119.0 ±3.4 | −29% |
| Throughput, C=16 | 610 | 234 | −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.
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.
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 dtype | pass_1 | pass_2 | Well-formed | Decode tok/s | Max context | VRAM |
|---|---|---|---|---|---|---|
| bf16 | 20.8% | 41.7% | 95.8% | 158.8 ±3.2 | 65,536 | 22.7 GB |
fp8 e4m3fn | 18.1% | 36.1% | 95.8% | 124.4 ±2.0 | 150,000 | 21.7 GB |
| KVarN 4/2-bit n=36 | 13.9% | 50.0% | 91.7% | 117.6 ±2.3 | 200,000 | 21.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.
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.
| Context | fp8 diverges at | KVarN diverges at |
|---|---|---|
| 8K | char 224 | char 93 |
| 32K | char 104 | char 204 |
| 64K | char 105 | char 105 |
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.
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.
| Profile | gen1 | gen2 | gen3 | nat1 | nat2 | top-1 agreeminimum |
|---|---|---|---|---|---|---|
vLLM fp8 e4m3fn | +0.008 / +0.025 | −0.005 / +0.001 | +0.001 / +0.001 | −0.033 / +0.006 | −0.026 / +0.022 | 0.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.008 | 0.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.137 | 0.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.148 | 0.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.425 | 0.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.
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.
| Document | KVarN vs fp8 | EXL3 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.
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.
| Profile | Depths | Multi-key | Verbatim copy | Cross-reference |
|---|---|---|---|---|
| bf16 n=10 | 16K / 32K / 64K | 80/80 | 100% | 10/10 |
| fp8 n=5 | 64K / 128K / 147K | 40/40 each | 100% each | 5/5 each |
| KVarN n=5 | 64K / 128K / 196K | 40/40 each | 100% each | 5/5 each |
| EXL3 3.0 bpw n=5 | 64K / 123K | 40/40 each | 100% each | 5/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.
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 bpw | Decode tok/s | Draft acceptance | VRAM free after load |
|---|---|---|---|
| MTP k=4, fp16 cache 32K | 94.8 ±0.5 | 70.5% | 6.42 GiB |
| No speculation, 32K | 43.6 ±0.3 | — | 6.43 GiB |
| MTP k=4, fp16 cache 128K | 93.8 ±0.6 | 70.3% | 1.03 GiB |
| MTP k=4, q8 cache 200K | 91.6 ±0.9 | 69.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.
| Control | Result |
|---|---|
| 3.0 bpw q8, repeated a day later | Bit-identical — the harness is deterministic |
| Plain 4.0, fp16 vs q8 cache, nat1 at 32K | 0.01 nats, 98.6% top-1 — cache quantization is irrelevant here |
Plain 4.0 vs self-calibrated SC_4.00bpw_H5, nat1 at 64K | SC is worse still: +0.12 to +0.28 per bucket, 84–88% top-1 |
| Excess vs bf16 on nat1, per 8K bucket | 3.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 × 2048 | 3.0 bpw 6.99 · plain 4.0 7.03 · SC 4.0 7.00 |
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.
- KVarN at 200K costs up to ~0.12 nats per token on hard text and drifts further with depth. It is the right answer to “it must fit” and the wrong default.
- EXL3 at 3.0 bpw buys 200K on one card for ~0.13 nats on natural text, 74% top-1 agreement, and 37% of the speed given up. It loses to fp8 at 150K on every axis except raw length, and to KVarN at 200K on fidelity.
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.
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.
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.
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.