Primary source · 20 sections

The measurement log

The complete record, in the order it happened — including the runs that were wrong, the corrections, and the one comparison that could not be made at all. Sections 1–9 are prior research; 10–20 are measurements from this machine.

Read section 19 first if you plan to cite anything. Conditions changed across the five days — power cap, reasoning setting, prompt corpus, single-tenancy — and two tables in this document can look comparable without being so. Section 19 is the provenance key; section 20 holds the canonical numbers, and wins wherever an earlier section disagrees.

1. Weight Quantizations

All formats available on HuggingFace for Qwen3.8-27B, ranked by accuracy. VRAM is weights-only; add 2–8 GB for KV cache depending on context length and cache dtype.

LOSSLESS >99% MINIMAL 97–99% MODERATE 94–97% LOSS <94%
Method Format Weights Fits 1 GPU? Accuracy Quality Best Framework
BF16 (baseline)Qwen/Qwen3.8-27B BF16 55.6 GB No (TP=2) 100% baseline BASELINE vLLM, SGLang
FP8 (official)Qwen/Qwen3.8-27B-FP8 FP8 E4M3 28.0 GB No (TP=2) ~99.9% >99% vLLM (storage only on Ampere—no compute speedup)
W8A16 INT8 + MTPlued/Qwen3.8-27B-INT8-W8A16-MTP INT8 sym RTN 31.6 GB No (TP=2) 99.36% top-1KLD 0.000894 nats/tok >99% vLLM (compressed-tensors, BF16 MTP drafter incl.)
AutoRound W4A16dbirks/Qwen3.8-27B-W4A16-AutoRound INT4 g128 19.5 GB Yes 99.4% avg recoveryGSM8K 91.7% vs 91.1% BF16 >99% vLLM (Marlin kernel)
MixedInt4 AutoRoundPilcothink/Qwen3.8-27B-MixedInt4-AutoRound INT4 g32 mixed ~19.5 GB Yes 99.4% avgVision layers kept at original precision >99% vLLM (Marlin)
AWQ INT4cyankiwi/Qwen3.8-27B-AWQ-INT4 AWQ INT4 ~19.5 GB Yes ~99%STEM+Agentic calibration set >99% vLLM (awq_marlin), SGLang
AutoRound GPTQVishva007/Qwen3.8-27B-W4A16-AutoRound-GPTQ GPTQ INT4 ~17 GB Yes ~99% (est.) >99% vLLM (gptq_marlin)
EXL3 3.0 bpwykarout/Qwen3.5-27B-exl3-3.0bpw EXL3 trellis ~12 GB Yes Best ppl/bit (no bench yet) TBD ExLlamaV3 + TabbyAPI only
NVFP4unsloth/Qwen3.8-27B-NVFP4 MXFP4 ~10.6 GB N/A 92–97% BLOCKED Blackwell GPUs only—cannot run on RTX 3090
Key finding: AutoRound W4A16 achieves near-lossless quality at 19.5 GB—fitting a single RTX 3090 with room for KV cache. This enables the 2-replica load-balanced pattern that proved fastest for Qwen3.6-27B. AWQ is comparably good but has less published accuracy data for Qwen3.8 specifically.

2. GGUF Variants (llama.cpp / Ollama)

Quality measured by KL divergence and top-1 token agreement against BF16 reference. Lower KLD is better; higher agreement is better. Highlighted row fits a single 3090 with good KV cache headroom. Unsloth “UD” prefix = Dynamic v3.0 quantization (per-layer quant type selection + 1.5M-token imatrix calibration); independently validated to have ~2× lower KLD than standard Q4_K_M at similar size. UD-Q4_K_XL and above include MTP heads in the GGUF; below Q2_K_XL, MTP is stripped (separate 1.37 GB MTP Q4_0 file available). Unsloth’s only RTX 3090-usable format is GGUF—their NVFP4 is Blackwell-only, and they don’t produce AWQ/GPTQ/AutoRound checkpoints.

Quant Size Top-1 Agree KL Divergence Quality Publisher Fits 24 GB?
Q8_0 28.89 GB 98.93% 0.00064 >99% AtomicChat No (TP=2)
Q6_K 22.43 GB 97.93% 0.00242 97–99% LM Studio Tight
UD-Q5_K_XL 20.22 GB 97.28% 0.00437 97–99% Unsloth Yes
UD-Q4_K_XL 17.56 GB 96.02% 0.00955 94–97% Unsloth Yes + headroom
AD-Q5_K_M/Q4_K_M 18.55 GB 96.43% 0.00730 94–97% AtomicChat Yes
IQ4_XS 16.51 GB 95.39% 0.01248 94–97% AtomicChat Yes
IQ3_S 13.84 GB 92.41% 0.03247 <94% AtomicChat Yes (very tight context)
IQ2_S 11.14 GB 87.18% 0.09832 <94% AtomicChat Yes
Unsloth agentic quality warning: Below UD-Q2_K_XL, 32-token prediction accuracy drops from 25% to <10%, causing tool-calling failures, excessive looping, and empty responses. For agentic coding, UD-Q4_K_XL is the minimum safe quant. Unsloth’s Dynamic v3.0 advantage is real—UD-Q4_K_XL has KLD 0.00955 vs standard Q4_K_M’s 0.02094 at comparable size—but the gain comes from smarter per-layer precision allocation, not a fundamentally different quantization method.

3. KV Cache Quantization

KV cache dtype affects context capacity and accuracy. On Ampere, FP8 KV is storage-only (dequants to BF16 for matmul) but still halves KV memory.

KV Cache Dtype Memory vs FP16 Accuracy Impact Quality Framework
FP16 / BF16 1.0× None (baseline) BASELINE All
FP8_E5M2 0.5× ≤0.7 pt across all tasks; near-lossless to 1M tokens >99% vLLM, SGLang
Q8_0 0.5× ~100% quality; +2.2% speed bonus >99% llama.cpp
Q4 keys only ~0.25× K ~0.4% perplexity increase 97–99% llama.cpp, vLLM (experimental)
Q4 keys + values 0.25× ~1.4% ppl + 1.5 pt reasoning drop CAUTION llama.cpp
Asymmetric Q4K+Q8V ~0.33× Better than Q4+Q4 97–99% llama.cpp
Recommendation: FP8_E5M2 KV cache is the safe default for vLLM/SGLang—halves KV memory with negligible quality loss. For llama.cpp, Q8_0 KV is effectively free quality.

4. MTP Speculative Decoding

Qwen3.8-27B ships with Multi-Token Prediction heads. Speculative decoding is mathematically exact—rejected drafts are discarded, so output quality is identical to non-speculative. The question is purely speed vs VRAM overhead.

MTP Depth Sweep (llama.cpp, single RTX 3090)

MTP Depth tok/s Speedup Draft Acceptance Notes
Off 73.6 1.00× Baseline
n=1 104.8 1.42× ~96% pos-0
n=2 125.5 1.70× ~88% pos-1
n=3 133.6 1.81× ~79% pos-2 PEAK on RTX 3090
n=4 119.5 1.62× Diminishing Overhead exceeds gains on Ampere
n=5 108.8 1.48× Regression vs n=3

Alternative Speculators

Method Source Draft Tokens Mean Acceptance Framework
Built-in MTP Ships with model weights 1–5 56–77% vLLM, SGLang, llama.cpp
DSparkRadixArk/Qwen3.8-27B-DSpark 1.36B param speculator 7 3.39 mean tokens SGLang
DFlash2 Block drafter (NInfer/syv-ai) 7–15 75% llama.cpp (PR #27342, unmerged), vLLM (syv-ai patches)
Qwen3-0.6B draft External small model 3–5 25–35% lower than MTP vLLM, SGLang

5. Serving Frameworks

Evaluated for Qwen3.8-27B on Ampere RTX 3090s specifically. “Day-one” means confirmed support at Qwen3.8 launch (Aug 14, 2026).

Framework Quant Formats MTP Multi-GPU Continuous Batching Strengths Weaknesses for 3090
vLLMv0.26.x AutoRound, AWQ, GPTQ (Marlin), FP8, INT8, BF16 Yesnative MTP support TP, PP Yes Largest ecosystem, CUDAGraphs, prefix caching, chunked prefill, PagedAttention MTP loader can silently load zero params (Qwen3_5MTP bug); CUDAGraph+spec-decode OOM on some configs
SGLangv0.4.x AWQ, GPTQ, AutoRound (all Marlin), FP8, GGUF, compressed-tensors, BF16 Yesnative MTP + DSpark + EAGLE + DFlash + n-gram TP, DP (native, cache-aware), EP Yes RadixAttention (up to 5× on agentic prefix-heavy workloads); native DP=2 with cache-aware routing (no nginx needed); overlap scheduling; constrained decoding (faster warm grammars than vLLM) MTP adaptive bug on GDN models (fixed PR #23331—need recent build); can’t load FP8 lm_head; no RTX 3090 benchmarks published
llama.cpp+ MTP PR #22673 All GGUF K-quants Yes--draft-mtp (July 2026) Tensor split No GGUF flexibility, Q4 KV cache, excellent single-GPU utilization, simplest deployment No continuous batching, lower throughput at concurrency
NInfer-3090community INT8, INT4 (custom) YesMTP3, ReplaySSM Single GPU Cohort batching Purpose-built for 3090; 171K INT8 context; DFlash2 block drafter Narrow support, community project, single GPU only
ExLlamaV3+ TabbyAPI EXL3 (2–8 bpw) Unknown gpu-split Limited Best perplexity-per-bit (trellis-coded quant); variable bpw per layer Newest format, smallest ecosystem, batch inference less mature
TensorRT-LLM AWQ INT4, INT8 Unknown TP Yes Optimized NVIDIA kernels; claimed 70% faster than llama.cpp (generic) Complex build pipeline, no Qwen3.8 benchmarks found, low consumer adoption

6. Dual-3090 Deployment Strategies

Four viable patterns for 2× RTX 3090 without NVLink. “C=1” is single-stream latency; “Aggregate” is total throughput under concurrent load.

Strategy C=1 tok/s Aggregate tok/s Max Context Requires Best For
vLLM 2-Replica + nginx LB1 vLLM per GPU, nginx least_conn ~114 ~400+ (C≥4) 64K per replica INT4 model (≤20 GB) Proven pattern from Qwen3.6
SGLang DP=2 + Cache-Aware RouterSingle launch cmd, built-in SMG router ~114 ~400+ (C≥4)3 64K per replica INT4 model (≤20 GB) AGENTIC prefix-heavy workloads
TP=2 Single InstanceBoth GPUs via tensor-parallel ~120–1701 ~270 (C=4) 100–200K Any quant ≤48 GB total Long context, quality-first (FP8/INT8)
Single GPUOne replica, second GPU free ~114–1342 ~1,000 (C=64) 64–240K INT4 model (≤20 GB) Simplest; high batch throughput; leaves GPU 1 free
SGLang DP=2 vs vLLM 2-replica: SGLang’s native data-parallel mode with cache-aware routing is architecturally superior to the nginx LB pattern used in Qwen3.6. The SGLang Model Gateway (SMG) achieves +92% throughput and +275% cache hit rate vs round-robin on prefix-heavy workloads. For agentic coding with repeated system prompts (~1,500–2,000 tokens), RadixAttention delivers 75–95% cache hit rates. One launch command vs three containers (2× vLLM + nginx).
Verdict from Qwen3.6 work: For single-user latency, TP=2 and single-GPU are roughly equivalent (~66–68 tok/s without MTP)—PCIe allreduce overhead nearly cancels the bandwidth doubling. For concurrent agentic throughput, 2-replica/DP wins decisively. For long context (>64K), TP=2 is necessary to pool VRAM.

7. Published Performance Numbers

Numbers from community benchmarks. Sources marked with framework and hardware. 3.6 = measured on Qwen3.6-27B (same architecture, directly comparable).

Single RTX 3090

Config Framework C=1 tok/s C=8 tok/s Notes Source
W4A16 baseline (no spec) vLLM 46 No optimizations syv-ai
+ MTP-2 (BF16 drafter) vLLM 66 65% acceptance syv-ai
+ MTP-4 + probabilistic + split-KV vLLM 93 69% acceptance syv-ai
+ INT4 lm_head + INT4 MTP (full stack) vLLM 114 74% acceptance syv-ai
+ DFlash2 block drafter vLLM (custom) 118–130 75% acceptance syv-ai
W4A16, INT8 GEMMs, batch mode vLLM 45.5 324.9 ~1,035 tok/s @ C=64 syv-ai
Q4_K_M + MTP n=3 + Q4 KV llama.cpp 136.7 17.4 GB VRAM kgptalkie
Q4_K_M baseline (no MTP) llama.cpp 40.3 17.7 GB VRAM kgptalkie
INT8 + MTP3 + ReplaySSM NInfer-3090 70.2 161.3 56–61% acceptance Don-Chad

Dual RTX 3090

Config Framework C=1 tok/s Aggregate Notes Source
W8A16 INT8, TP=2, 1K prompt vLLM 65.6 49.7 tok/s @ 32K prompt lued (HF)
TP=2, single GPU baseline3.6 vLLM 117–118 Narrative text tfriedel
TP=2 + MTP k=3, code gen3.6 vLLM 264 Code (high acceptance) tfriedel
2 replicas + nginx LB, C=43.6 vLLM ~100 225 AutoRound INT4, MTP n=3 This project

8. Accuracy Deep-Dive

Qwen3.8-27B baseline scores and measured degradation under quantization. Focus on coding benchmarks since the target use case is agentic coding.

Qwen3.8-27B Baseline (BF16)

Benchmark Qwen3.8-27B Qwen3.6-27B Delta
Terminal-Bench 2.1 73.0 63.4 +9.6
SWE-bench Pro 61.7 53.5 +8.2
DeepSWE 1.1 42.2 13.3 +28.9
LiveCodeBench v6 90.3 83.9 +6.4
GPQA Diamond 89.2 87.8 +1.4
IFBench 79.5 69.1 +10.4
OSWorld-Verified 84.3 63.9 +20.4

AutoRound MixedInt4 Recovery (Pilcothink)

Benchmark BF16 INT4 Recovery
MMLU 83.49% 83.07% 99.50%
GSM8K (flex) 72.86% 76.12% 104.5%
ARC-Challenge 58.87% 58.87% 100.0%
HellaSwag 82.82% 82.40% 99.49%
BoolQ 86.64% 80.49% 92.90%
Average recovery 99.38%

dbirks AutoRound W4A16 (Coding Benchmarks)

Benchmark BF16 W4A16 INT4 Delta
GSM8K 91.1% 91.7% +0.6 (noise)
HumanEval 93.9% 95.7% +1.8 (noise)
MMLU-Pro 81.9% 82.6% +0.7 (noise)

syv-ai Full Optimization Stack Quality

Benchmark Unquantized Full Stack (INT4+MTP+DFlash2) Delta
IFBench 79.5 78.3 −1.2
GSM8K (exact match) 96.5%
Perplexity 8.09
Coding quality warning: One Qwen3.6 study (heyneo) found FP8 preserved HumanEval (39.02% = BF16) while INT4 dropped 8 points (31.10%). If coding quality is paramount, consider FP8 or INT8 on TP=2 over INT4. However, the dbirks Qwen3.8 benchmarks show no HumanEval drop with AutoRound INT4—calibration method matters significantly.

9. Recommended Benchmark Configurations

Ranked by expected performance × quality on our dual RTX 3090s. All configs should be benchmarked with the same prompt shape and concurrency levels for fair comparison.

1

vLLM + AutoRound INT4 + MTP n=3 + 2-Replica LB

Proven pattern from Qwen3.6. Two independent vLLM instances (one per GPU) behind nginx least_conn. AutoRound INT4 via Marlin kernel. FP8 KV cache. Maximum aggregate throughput for concurrent agentic requests.

~114 tok/s C=1 ~400+ tok/s C=4 agg 99.4% accuracy 64K ctx/replica
2

SGLang DP=2 + AutoRound INT4 + MTP n=3 + Cache-Aware Router

SGLang’s native data-parallel mode with SMG cache-aware routing. Architecturally superior to nginx LB—coordinated prefix routing gives +92% throughput on agentic workloads with shared system prompts. One launch command replaces three containers. RadixAttention delivers 75–95% cache hit rate on repeated ~1,500-token system prompts.

~114 tok/s C=1 ~400+ tok/s C=4 agg 99.4% accuracy Single launch cmd Needs PR #23331 fix
3

vLLM + AutoRound INT4 + MTP n=3 + TP=2

Single instance across both GPUs. Trades aggregate throughput for double the context window and simpler deployment. Good baseline comparison against the DP/LB patterns.

~120–170 tok/s C=1 ~270 tok/s C=4 99.4% accuracy 128K+ context
4

SGLang DP=2 + AWQ INT4 + DSpark Speculator

Tests DSpark (1.36B, 7 tokens/step, mean 3.39 acceptance) against built-in MTP. DSpark claims superior throughput-latency at higher batch sizes. Note: DSpark + INT4 target compatibility is unconfirmed (tested with FP8 target only).

TBD tok/s 3.39 mean acceptance ~99% accuracy INT4 compat unconfirmed
5

vLLM + AWQ INT4 + MTP n=3 + 2-Replica LB

Same pattern as #1 but with cyankiwi’s pre-made AWQ checkpoint. Tests whether AWQ vs AutoRound makes a measurable speed or quality difference. Falls behind SGLang DP on agentic prefix-heavy workloads but is the most battle-tested config.

~100–114 tok/s C=1 ~400+ tok/s C=4 agg ~99% accuracy
6

llama.cpp + Unsloth UD-Q4_K_XL + MTP n=3

Simplest deployment. Unsloth Dynamic v3.0 GGUF (17.56 GB per the repository manifest — this document previously said 17.92 GB; KLD 0.00955) with built-in MTP heads and Q4 KV cache. No continuous batching but excellent single-stream performance. Minimum safe quant for agentic use per Unsloth’s own guidance.

~134–151 tok/s C=1 No batching 96% top-1 17.4 GB VRAM
7

vLLM + W8A16 INT8 + MTP + TP=2 (Quality-First)

lued’s INT8 checkpoint with BF16 MTP drafter included. 99.36% top-1 accuracy. Higher quality than INT4 at the cost of requiring both GPUs for one instance.

~65 tok/s C=1 99.36% accuracy 128K+ context
8

vLLM + Official FP8 + TP=2 (Maximum Quality)

Qwen’s official FP8 checkpoint. Highest fidelity (∼99.9%) at the cost of no compute advantage on Ampere. Tests whether quality difference vs INT4 matters for agentic coding in practice.

~65–70 tok/s C=1 ~99.9% accuracy 128K+ context
9

vLLM + syv-ai Full Stack (INT4+INT8 GEMMs+DFlash2) MEASURED WINNER

Aggressive custom optimization stack from syv-ai. Requires their patches (INT4 lm_head, probabilistic sampling, split-KV verify). Maximum single-GPU throughput.

~114–130 tok/s C=1 ~1,000 tok/s C=64 IFBench −1.2 pt Requires custom patches

10. Measured Results THIS MACHINE

Everything below was run on this host, August 22–24, 2026: 2× RTX 3090, driver 580.173, CUDA 13.0, 250 W power cap unless marked 350 W. Single-stream numbers are 512-token greedy chat completions on a ~46-token prose prompt, mean of 3. Sections 1–9 are the research; this is what the research predicted versus what happened.

153.9tok/s
Best single-stream · vLLM + DFlash2
880tok/s
2 replicas + LB · C=16
1,023tok/s
Batch profile · 1 GPU · C=64
97.0%
GSM8K n=200 · MTP k=4 greedy
Verdict: The research picked the right framework and the wrong speculator. The syv-ai patched vLLM stack (research rank #9) is the clear winner at 153.9 tok/s with DFlash2, or 135–150 with MTP—2.5× anything llama.cpp or SGLang produced here, and within noise of the published quality reference. Two of the research’s top-ranked configs never ran: stock vLLM 0.27.1 crashed on Ampere (and took GPU 0 off the bus), and SGLang DP=2 died during scheduler init. Deploy as two MTP replicas behind the LB for concurrent agentic work, one DFlash2 replica for a single user, or the batch profile for bulk.
Provenance note (audited August 26). These rows were measured while another campaign ran on the second card. Contention depresses batch-1 decode, so the natural worry is that the whole table reads low. Checked against §12’s provably single-tenant coding suite at matched prompt length, a later canonical re-measurement (mean of 5, single-tenant) settled it: MTP k=4 is 3.9% off and DFlash2 5.3% — both at or inside the noise floor. No correction is needed to this table. An earlier draft of this note claimed a ~20% understatement; that was wrong, and worth recording as an error of method: it compared this table’s ~46-token prompt against 1.1K-context measurements elsewhere. Draft acceptance rises with context (0.61 → 0.78), so those figures differ for reasons that have nothing to do with contention. Prompt length is a condition like any other; comparing across it silently is exactly the mistake the fairness contract in §18 exists to prevent.

Single-Stream Leaderboard (C=1)

Rank Config GPUs Power tok/s vs baseline Accept / mean len Quality
1 vLLM patched + AutoRound W4A16 + DFlash2 k=7syv-ai/qwen38-27b-rtx3090 · fast int4 lm_head 1 250 W 153.9 4.0× 4.35 tok/step GSM8K 96.0%
2 vLLM patched + AutoRound W4A16 + MTP k=4same stack, Qwen’s own MTP head 1 250 W 135.0 / 150.0GPU 0 / GPU 1 — repeatable gap 3.5–3.9× 3.25–3.4 tok/step GSM8K 97.0%
3 llama.cpp + UD-Q4_K_XL + MTP n=1layer split across both GPUs 2 350 W 61.4 1.6× 96% top-1 (published)
4 llama.cpp + UD-Q4_K_XL + DFlash2-Q8_0 n=4incoai/Qwen3.8-27B-DFlash2-GGUF · needs PR #27342 build 1 250 W 60.7 1.8× (vs 250 W base) 63.9% / 3.55 96% top-1 (published)
5 llama.cpp + UD-Q4_K_XL + MTP n=1 1 350 W 54.5 1.4× 96% top-1 (published)
6 vLLM patched, batch profileINT8 activations, fp8 KV, no spec 1 250 W 47.6 1.2× >99% (published)
7 SGLang + AutoRound W4A16lmsysorg/sglang:latest, no spec 1 350 W 47.4 1.2× >99% (published)
8 llama.cpp + UD-Q4_K_XL, no spec (baseline) 1 350 / 250 W 38.7 / 33.3 1.0× 96% top-1 (published)
vLLM stock 0.27.1 (research #1/#3/#5)CUDA-graph OOM at 0.85; Triton GDN autotuner crash under --enforce-eager; NVML error with --language-model-only 1 350 W FAILED — third attempt ended in Xid 79
SGLang DP=2 (research #2)--dp-size 2: rank 0 scheduler died during init (exit −3) 2 350 W FAILED — not retried after the power cap

Concurrency Scaling

Deployment C=1 C=2 C=4 C=8 C=16 C=32 C=64
2× vLLM MTP replicas + nginx least_connaggregate tok/s · per-request in parentheses · 512 tok 151 (151) 276 (145) 414 (115) 620 (85) 880 (59)
1× vLLM batch profileINT8 activations, 64 slots · 256 tok (C=64: 512) 48 537 (34) 755 (24) 1,023 (16)1,165–1,184 steady-state; GPU was already saturated at 100% util / 249 W, so this figure is contention-insensitive
1× vLLM MTP single-user8 slots · 256 tok 135 377 (35)
1× vLLM DFlash2 single-user8 slots, C=16 queues · 256 tok 154 300 (48) 323 (35)
Reading the scaling: the two-replica LB is the best shape up to C≈16 (880 agg with every request still above 59 tok/s). Past that, speculation stops paying and the batch profile’s INT8 tensor-core GEMMs take over—at C=64 one GPU alone delivers 1,023 tok/s, so two batch replicas would clear ~2,000. GPU 1 sat at 249.8 W, 23.1 GiB, 98% KV utilisation at C=64: that is the ceiling at 4K context.

DFlash2 Depth Sweep (llama.cpp, GPU 1, 250 W)

Draft n tok/s Speedup Draft acceptance Mean accepted len Notes
Off33.31.00×250 W baseline (38.7 at 350 W)
n=355.11.65×70.7%3.12
n=460.71.82×63.9%3.55PEAK on RTX 3090
n=560.21.81×60.2%3.99Within noise of n=4
n=754.21.63×47.6%4.33Model-card recommendation—worse here; 8-token verify batch costs more than it returns
n=1053.41.60×47.6%4.33Identical to n=7: drafter is trained for a 7-token block

The same DFlash2 drafter is 2.5× faster inside vLLM (153.9) than inside llama.cpp (60.7). The multiplier is the framework’s verify path—split-KV verify attention and a CUDA-graphed spec step—not the drafter. Accepted length on this prose/greedy prompt (3.5–4.3) is well under the card’s 5.3, which is GSM8K with sampling; expect closer to that on code and math.

llama.cpp KV-Cache Dtype (llama-bench, 350 W)

ConfigKV dtypepp512 tok/stg128 tok/s
1 GPUf161,02738.8
1 GPUq8_01,27538.7
1 GPUq4_01,28038.1
2 GPU, layer splitf161,39142.6

Quantised KV is a free +24% on prompt processing and neutral on generation, confirming section 3. A second GPU buys +35% prefill and only +10% decode—PCIe layer split is not where the wins are.

Quality Gate (patched vLLM, GPU 0)

CheckMTP k=4DFlash2 k=7ReferenceVerdict
GSM8K n=200, greedy, thinking off, C=8 97.0% (194/200) 96.0% (192/200) 96.5% (repo, same setup) PASS — int4 lm_head/MTP requant costs nothing measurable
Determinism 20 prompts, greedy, C=1, same mode run twice 20/20 identical PASS
Same mode, C=1 vs C=8 5/20 identical EXPECTED bf16 batch-shape nondeterminism (reduction order flips near-tie argmax), not spec-decode error. All 11 divergences are equivalent phrasings (“functional approach” vs “functional programming approach”, array vs object wrapper); none truncate, corrupt, or loop.
DFlash2 vs MTP, C=1 9/20 identical MTP vs itself at C=8: 10/20

No non-speculative greedy reference was available from this stack (the launcher has no SPEC=none path), so exactness is established by self-consistency rather than against a plain decode.

Incidents & Open Questions

11. Speed vs Context Length THIS MACHINE

Correction (August 26, 2026) — these curves were measured under cross-GPU contention. The two sweeps that produced this section ran concurrently, one per card, on the assumption that separate GPUs are independent. They are not: the cards share PCIe, host memory bandwidth and CPU, and batch-1 decode is launch-latency sensitive. Re-running the identical summarization task on an otherwise idle machine gives 95.8 tok/s where this section reports 36.9; deliberately loading the second GPU pulls the same measurement back down to 58.0. Section 12 has the isolated numbers.

What is affected: every vLLM curve and the llama.cpp 1-GPU curve above ran against a busy neighbour. The llama.cpp 2-GPU curve is the one clean line in the ≤64K sweep — it owned both cards. A later provenance audit narrowed this further: the llama.cpp 64K–250K long-context line is also contended in both its original and resumed windows, and the “Beyond 64K” table below inherits that. Because the contamination is uneven, the ranking in this section is unreliable, not merely its absolute values: the configs that look competitive here are partly just the ones that were measured while the other card was quiet. The shapes of the curves (decay with context, linear TTFT) survive; the cross-config comparison does not. It has not been re-run — a clean sweep is roughly a day of GPU time.

Section 10’s leaderboard used a 46-token prompt. That flatters every speculative config, because a short open-ended prompt is the easiest possible draft target. These curves re-run the top five configs across the full context window on one fixed task — “write a detailed technical summary of the documents above, then list ten concrete recommendations” over a growing prefix of real, non-repeating technical prose (llama.cpp and vLLM documentation, ~1.9 tokens/word). 256-token greedy completions, mean of 2 after a warm run, prompt caching disabled on both frameworks so time-to-first-token is comparable. Prompt length is calibrated per request against each server’s own tokenizer.

The ranking inverts with context. vLLM + DFlash2 leads the field at 512 tokens (94.8 tok/s, half again the next config) and has fallen behind both llama.cpp MTP configs by 16K — fourth of five. llama.cpp across both GPUs is the flattest curve in the set — 61.7 → 37.1 tok/s from 512 to 57K, a 40% decay where DFlash2 loses 60% by 32K. If your workload is long-context, the config that wins the benchmark table is not the config to deploy.
vLLM patched + DFlash2 k=7 vLLM patched + MTP k=4 llama.cpp MTP n=1, 2 GPU llama.cpp DFlash2 n=4 llama.cpp MTP n=1, 1 GPU
Decode speed vs prompt length
Decode tok/s vs prompt tokens 0 20 40 60 80 100 512 2K 8K 16K 32K 64K prompt tokens (log scale) 61 vLLM DFlash2 41 vLLM MTP 37 llama.cpp MTP 2×GPU 34 llama.cpp MTP 1×GPU 30 llama.cpp DFlash2
Time to first token vs prompt length
Time to first token (s) vs prompt tokens 0 20 40 60 80 100 512 2K 8K 16K 32K 64K prompt tokens (log scale) 103 vLLM MTP 73 llama.cpp DFlash2 70 vLLM DFlash2 69 llama.cpp MTP 1×GPU 61 llama.cpp MTP 2×GPU
vLLM CTX=long (fp8 KV) + MTP k=3 vLLM CTX=long (fp8 KV) + DFlash2 vLLM CTX=huge (KVarN 4/2-bit) + MTP llama.cpp MTP n=1, q8 KV, 262K ctx
Beyond 64K — long-context profiles, decode speed vs prompt length
Decode tok/s vs prompt tokens 0 10 20 30 40 50 60 64K 100K 150K 200K prompt tokens (log scale) 64 vLLM long MTP 39 vLLM long DFlash2 25 llama.cpp MTP 262K 24 vLLM huge MTP
Table view
Config5045072K2K7K8K16K16K31K31K56K58K
vLLM patched + DFlash2 k=794.80.71s66.22.69s50.611.56s38.524.91s34.852.85s61.170.10s
vLLM patched + MTP k=467.40.81s57.03.05s49.712.22s52.019.82s36.952.89s40.7103.24s
llama.cpp MTP n=1, 2 GPU61.70.55s60.91.65s55.65.63s54.511.77s45.525.65s37.161.17s
llama.cpp DFlash2 n=444.70.64s41.92.38s41.08.56s32.717.67s34.337.58s29.572.80s
llama.cpp MTP n=1, 1 GPU46.90.57s46.32.09s42.67.68s42.016.39s37.233.85s34.268.56s
Long-context profile68K99K128K150K152K193K
vLLM CTX=long (fp8 KV) + MTP k=351.9104s61.3136s63.9239s
vLLM CTX=long (fp8 KV) + DFlash229.1214s39.2313s38.7421s
vLLM CTX=huge (KVarN 4/2-bit) + MTP34.8132s27.1234s23.7331s
llama.cpp MTP n=1, q8 KV, 262K ctx32.786s30.1145s25.2278s

Cells: decode tok/s, with TTFT beneath. 256-token greedy completions, mean of 2 after a warm run; prompt is a growing prefix of real technical documentation, calibrated to the target token count by each serverprompt is filler prose calibrated to the target token count by each server’s own tokenizer.rsquo;s own tokenizer.

Time to first token is linear and unassisted. Speculative decoding accelerates generation, not prefill, so TTFT tracks prompt length at roughly 1.7 ms/token on vLLM and 1.1 ms/token on llama.cpp’s two-GPU split — the one place the second card clearly earns its keep (61 s vs 69–73 s at 57K, and 26 s vs 34–38 s at 32K). At 150K a single request waits about four minutes before its first token; at 192K, five and a half. Prefix caching, off here for measurement, is what makes those numbers survivable in a real chat or RAG front-end — the repo reports a 24K document dropping from ~23 s to ~1 s on turn two.

Beyond 64K — what each profile costs

The 64K ceiling in the charts 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.

ProfileKV cacheMax reachedDecode at maxVerdict
vLLM CTX=long + MTP k=3 fp8 (FlashInfer) 149,525 63.9 tok/s Best long-context config by a wide margin — and it gets faster with depth (51.9 → 63.9)
vLLM CTX=long + DFlash2 int8 per-token-head 128,013 38.7 tok/s Its own launcher caps this path at 128K; slower than MTP at every depth
vLLM CTX=huge + MTP KVarN 4/2-bit 192,517 23.7 tok/s Deepest context on one 24 GB card. Take it when the request would not otherwise fit, not for speed
llama.cpp MTP n=1 q8_0 151,530 25.2 tok/s Decays steadily; 200K+ fails to allocate on a single card (-c 200000 OOMs at load)
Per-row provenance. A later audit established that these four rows were not measured under equal conditions. CTX=long + MTP (the apparent winner) and CTX=long + DFlash2 were both measured while a llama.cpp server was live on the other card; the llama.cpp row is contended throughout. Only CTX=huge KVarN is clean. The direction matters: contention depresses results, so the winner’s true figure is higher than 63.9 — the table understates the gap between the fp8 profile and the rest rather than inventing one. No clean replacement has been measured.

Two caveats on the KVarN row. The repo documents CTX=huge at 245,760 tokens, 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 ceiling. And 240K was never reached: the harness’s length calibrator refused to build a prompt past the resolved limit.

Read the bumps as content, not context. Several curves rise where they should fall — vLLM MTP from 8K to 16K, DFlash2 from 32K to 60K, the whole CTX=long MTP line. Draft acceptance depends on how predictable the text is, and a longer prefix of coherent documentation is easier to draft against than a short one. The 60K points on both vLLM curves were also measured with a separate script over different source text after the main harness overshot the token limit, so that segment compares two prompts as much as two lengths. Treat every number here as specific to summarization over technical prose; on code, where draft acceptance is highest, expect the whole field to shift up and the gaps to widen.

12. Coding Workloads, Measured Clean THIS MACHINE

Everything above is summarization. That matters because speculative decoding’s speed is a function of draft acceptance — how predictable the next tokens are — and prose summarization is a hard draft target. This section runs an 11-cell suite of coding tasks built from real files on this machine, against three configs, single-tenant with the other GPU idle. 256-token greedy completions, prompt caching off, 250 W cap, acceptance read from each server’s own counters.

Coding is ~1.4× summarization, and the two vLLM speculators are tied. Averaged over the coding classes: vLLM MTP k=4 144.0 tok/s, vLLM DFlash2 k=7 143.0, llama.cpp 2-GPU MTP 55.6. MTP takes 6 of 11 cells and DFlash2 5 — a statistical tie, so there is no case for switching away from MTP, which is already the default. Peak observed: 181.5 tok/s (DFlash2, repo edit at 1K context).

Decode tok/s by task class

Task classContextvLLM MTP k=4vLLM DFlash2 k=7llama.cpp 2-GPU
CanonicalChudnovsky π55144.361% acc135.642%63.31.93 tok/step
Boilerplateargparse CLI64152.167%160.959%63.81.95
Repo editreal source + “add --dry-run, show diff”1,007169.778%181.570%64.21.98
8,281159.678%109.946%60.61.98
32,463136.281%120.351%47.82.00
Code reviewsame files, “find bugs”987121.056%144.350%59.11.88
8,280138.471%132.546%47.81.90
32,457131.076%158.862%38.41.88
Summarize§11’s task, verbatim1,042117.054%126.239%54.91.86
8,075110.352%99.629%43.71.79
31,83388.544%82.126%34.61.81

Acceptance percentages are only comparable within an engine: llama.cpp reads 88–100% but at n=1 its ceiling is 2.0 tokens per step, so the honest cross-engine metric is tokens/step — vLLM 2.8–5.9 against llama.cpp’s 1.8–2.0. That gap, not the acceptance rate, is why llama.cpp runs at a third of vLLM’s speed here.

Why k=4 beats k=7 — and when it doesn’t

Draft acceptance by position — code vs prose
Draft acceptance by draft position, code vs prose 0% 25% 50% 75% 100% 0 1 2 3 4 5 6 draft position (0 = first speculated token) 69% MTP k=4 · repo edit (code) 55% DFlash2 k=7 · repo edit (code) 30% MTP k=4 · summarize (prose) 12% DFlash2 k=7 · summarize (prose)

Each speculated token is conditioned on the ones before it, so acceptance falls with draft depth. The rate of that fall is what decides the optimal draft count — and it 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 thrown away. This is the mechanism behind every k-sweep in this document, including the earlier finding that n=4 beat the model card’s recommended n=7 — that sweep was run on prose.

Two predictions the data killed. Computing π was supposed to be the memorized ceiling; it is the floor of the coding classes (61% acceptance, 144 tok/s), because Chudnovsky is dense numeric code rather than stereotyped boilerplate — a π-based benchmark would have understated coding by about 20%. And repo edits were predicted to sit mid-range on unfamiliar identifiers; they are the highest (78–81%), because emitting a diff means reproducing code that is already in the context. Acceptance there rises with context length, since more surrounding code makes the edit more determined.
Unresolved. llama.cpp 2-GPU is the one config measured cleanly in both rounds, and it reports 45.5 tok/s at 32K in §11 against 34.6 here on the nominally identical summarization task. Contention does not explain it — both runs owned the machine. The likely cause is that the two harnesses build the prompt from different source text, which changes acceptance; it has not been isolated. Treat the two rounds as separate experiments rather than a controlled comparison.

13. Multi-Turn Agentic Profile THIS MACHINE

Every measurement above is single-turn against a clean context. Real agentic coding is not: context accumulates across turns of tool calls, file reads and edits. To reach that regime we pointed the Pi coding agent (v0.84.3, MIT) at a single pinned replica and had Qwen3.8-27B build Tetris from scratch — core loop, then hold-piece, SRS wall kicks, scoring — each feature requiring it to re-read and extend its own earlier code. 68 assistant turns, context growing 5.4K → 61.5K against a 65,536 ceiling. Decode rate and TTFT come from stamping pi’s streaming JSON deltas on arrival; acceptance from the replica’s own spec-decode counters over the same windows.

The prediction was wrong: 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, on the reasoning that later turns are novel reasoning rather than reproducing context. It doesn’t. Acceptance is flat — −0.03 points per 1K tokens, r=−0.05. It in fact rises from 65% to 77% through the first 30K, then settles around 69%, within a few points of the single-turn baseline.
Indexed to the first context band (=100) — three measures, one axis
Acceptance, decode rate and TTFT vs accumulated context, indexed to the first band 0 200 400 600 800 1,000 5K 15K 25K 35K 45K 60K accumulated context (band midpoint) 1,039 Time to first token 105 Draft acceptance 79 Decode tok/s
Context bandTurnsAcceptanceDecode tok/sTTFTTokens/stepAcceptance by position 0–3
0–10K765.3%137.35.9 s3.610.86 0.71 0.57 0.47
10–20K1173.3%140.812.5 s3.930.90 0.79 0.67 0.58
20–30K577.3%137.222.2 s4.090.89 0.81 0.73 0.66
30–40K772.3%124.633.1 s3.890.88 0.77 0.67 0.58
40–50K1669.1%113.848.6 s3.760.86 0.74 0.63 0.53
50–70K868.5%108.561.3 s3.740.85 0.73 0.63 0.53
What actually degrades is attention, not speculation. Decode rate falls −0.70 tok/s per 1K tokens (r=−0.70) — 137 → 108, about −21% across the run. With acceptance flat, that cost is the growing KV cache, not the drafter losing its grip. The practical consequence: reducing draft depth would not help here. A slowdown caused by attention looks identical to one caused by speculation if you only watch tok/s, and the two call for opposite fixes.
Prefill is the real tax, and it is brutally linear: +1.14 s per 1K tokens, r=+1.00. At 61K context a request waits 69 seconds before its first token — roughly six times the entire decode time of a 500-token reply. For agentic work at depth the speculator is close to irrelevant and prefix caching is the lever that matters. It was disabled here for measurement; the upstream repo reports a 24K document dropping from ~23 s to ~1 s on the second turn.

Compaction bought nothing. Pi compacted at turn 52, cutting context 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 did not restore speed — exactly what you would predict if acceptance was never context-limited to begin with, and a useful independent check on the headline result.

The build worked. 618 lines of Python: curses rendering, SRS wall kicks, hold-piece, a 7-bag queue. It parses and imports cleanly. The ladder reached 4 of 11 planned steps before a turn hit the 900 s timeout — the agent proved far more iterative than designed for, spending 30 assistant messages on step one alone. That is a limitation of the ladder, not of the model: context still reached 61.5K, past the 58K target.

Two instrumentation traps worth repeating. Pi’s session files record ISO-8601 completion timestamps only, so token-count ÷ timestamp-delta yields end-to-end throughput with prefill baked in — 37–49 tok/s against a true decode rate near 144. Anything derived from session files alone understates decode by roughly threefold. Separately, --thinking off was a silent no-op: declaring the model reasoning: false made the agent skip its thinking branch entirely and send no chat_template_kwargs, so the server’s own default (thinking on) applied and reasoning tokens inflated every count. Both failures are silent and neither raises an error.

14. Independent Cross-Check THIS MACHINE

Section 13’s flat-acceptance result contradicted our own prediction, which is exactly when a finding deserves a second instrument. We repeated the Tetris ladder with Qwen Code v0.22.1 — a different codebase, a different prompt construction, and the Qwen-native XML tool-call format rather than Pi’s. The /metrics acceptance join is harness-agnostic and carried over untouched.

HarnessMean acceptanceAcceptance sloperDecode slopeContext span
Pi (baseline)~70%−0.006 pts/1K−0.01−0.571 tok/s/1K5–61K
Qwen Code (cross-check)76.1%−0.063 pts/1K−0.03−0.593 tok/s/1K21–37K
Confirmed — the finding is about the model, not the scaffold. Two harnesses with nothing in common but the endpoint agree that acceptance is flat against context and that decode decays at essentially the same rate. Qwen Code’s narrower context span (21–37K) gives it less leverage on the trend, so treat it as corroboration rather than a replacement for the Pi run.

Two corrections were needed before the comparison meant anything. The raw aggregate read 93.7% acceptance against Pi’s 70% — because templated tool-call messages, which stream as a single delta and are trivially predictable, were pooled with real generation. Separated: real generation 74.5%, tool calls 94.0%. Worse, the rows carrying context and the rows carrying streaming deltas were disjoint — the join silently never happened, so the first fit was computed entirely on tool-call messages. Merging by adjacency yields the 33 properly paired rows above. Neither fault raised an error; both produced plausible-looking numbers.

15. Correctness — Aider Polyglot THIS MACHINE

Every section before this measures speed. Speed is only half the question, and 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 must come from the quantization or the KV dtype. This is the first correctness measurement on this stack: 224 of 225 Exercism exercises across six languages, two attempts with test feedback after a failure, diff edit format, four threads across both replicas, ~2 h wall.

12.5%
solved first attempt
48.7%
solved after test feedback
90.6%
well-formed edit blocks
97s
per case
Pass rate by language — cold vs with test feedback
Aider polyglot pass rate by language, first attempt vs 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
The headline is the gap, not the score. 28 exercises were solved cold; 81 more were solved only after the model saw failing test output — a 3.9× improvement from feedback alone. That is the most actionable result in this document: for agentic use, plumbing test output back to the model matters more than any serving-configuration choice measured here. It also coheres with §12, where repo-edit tasks — reproducing code already in context — showed the highest draft acceptance at 78–81%.

Rust is the clear weak spot at 26.7%, less than half of Python’s rate, and it is weak cold as well (3.3% first attempt). C++ shows the opposite profile — near-worst cold at 3.8% but second-best with feedback at 57.7% — and it is also where most malformed edit blocks concentrated.

Edit format was settled by measurement, not assumption

Formatpass_1pass_2Well-formeds/case
whole0.0%40.0%100%88.9
diff20.0%50.0%80.0%121.0

On a 10-exercise smoke test, diff won both pass rates despite whole being perfectly well-formed — and whole bypasses the malformed-edit check that the benchmark partly exists to apply. The pass_2 gap is a single case at n=10; the pass_1 gap is the stronger signal. diff also matches Aider’s own shipped setting for every Qwen coder model.

Infrastructure failures, separated from capability. Three cases (1.3%) failed for reasons that are not the model’s fault: one context exhaustion, one test timeout, and one HTTP 413 — the last of which was our bug, an unset client_max_body_size leaving nginx at its 1 MB default and rejecting large prompts before they ever reached vLLM. Now set to 32 m and verified. The remaining 24 malformed responses across 21 cases are genuine diff-format capability misses. Conflating the two categories would have overstated failure by roughly a quarter.
Do not compare this 48.7% to a published leaderboard. It measures this quantization (W4A16-AutoRound-fast, int4-GPTQ lm_head) of Qwen3.8-27B, through this harness version, with thinking disabled, on this serving stack. It is not a measurement of the model’s capability. A genuinely comparable number would require matching the reference harness, checkpoint precision and sampling settings — none of which we did, because the question here was how our deployment behaves.
The trap that nearly invalidated the whole run. The first smoke test reported context exhaustion on 10 of 10 cases. Probing directly showed something stranger: 24 of 24 requests generated their full token budget with completely empty content. Thinking is on by default server-side, and this deployment runs --reasoning-parser qwen3, which extracts reasoning into a separate field — while Aider expects <think> inline in content and therefore saw nothing at all. Disabling it took errors from 10 to 0, seconds-per-case from 183.5 to 88.9, and cut tokens roughly fivefold for the same answer. This is the fourth time the same default silently corrupted a measurement in this project: it defeated Pi’s --thinking off, needed a specific incantation in Qwen Code, and here produced a benchmark scoring a model whose output the harness could not read. It never raises an error.

16. A Comparison That Could Not Be Run THIS MACHINE

§15 measured correctness for one quantization. The obvious follow-up — the same 225 exercises against a second 4-bit checkpoint, isolating quantization method at equal bit width — looked free: cyankiwi/Qwen3.8-27B-AWQ-INT4 was already cached, no download required. It could not be run, and why it could not is more useful than the number would have been.

The predicted obstacle wasn’t the real one. We expected the AWQ checkpoint to lack an MTP head, which would have confounded quantization with speculator presence. It ships 15 MTP tensors; that confound never existed. Two different problems did.

It is not one variable, it is five

The checkpoint is not classic AWQ at all — it is compressed-tensors / pack-quantized. Against the deployed checkpoint:

Componentcyankiwi “AWQ-INT4”AutoRound-fast (deployed)Affects correctness?
body weights4-bit asymmetric, group-324-bit symmetric, group-128yes
lm_headBF16int4yes
embed_tokensBF16int8marginal
MTP headBF16int4no — speed only
draft vocabularynone40k tokensno — speed only

Three of those five were closed by running the serving stack’s own prep on a copy (quant_lm_head.py rel. error 0.0064, quant_embed.py 0.0056, quant_mtp.py ~0.007), which would have left body quantization as the sole correctness-affecting variable. The clean experiment was within reach.

Then the stack structurally rejected it. Loading fails in vocab_parallel_embedding.py with size of tensor a (160) must match tensor b (40) — that is 5120/32 against 5120/128. The syv-ai qwen3_5-embed-quant.patch makes the embedding layer quantization-aware from the checkpoint’s declared group size, while the same repo’s requant scripts hard-code GROUP = 128. Restoring the original BF16 embedding shard and retrying reproduced the identical failure, ruling out the requant as the cause.
The consequence for reusable benchmarking. This serving stack is checkpoint-specific, not a general harness. It assumes group-128 and patches vLLM’s embedding path around that assumption, so any future checkpoint quantized at a different group size fails the same way — and fails as an opaque tensor-shape error at load rather than an honest “unsupported quantization”. A framework intended to evaluate arbitrary new models cannot use it as the serving layer. Either stock vLLM serves that role (at the cost of the syv-ai speed patches), or the prep must be rewritten to honour each checkpoint’s declared group size.

A useful by-product: the noise floor

Arm (identical checkpoint & config)Decode tok/sTTFTVRAM
AutoRound-fast, GPU 0165.40.96 s23,208 MiB
AutoRound-fast, GPU 1172.00.91 s23,130 MiB
AutoRound-fast, GPU 0 after restart159.61.00 s22,911 MiB

The same card and configuration spans 165.4 → 159.6 tok/s across restarts. Single-stream differences below roughly 5% on this box are not meaningful. That threshold applies retroactively: the GPU 0 / GPU 1 gap reported earlier in this document reproduced here at ~4%, which sits at the edge of noise — treat it as suggestive rather than established.

17. Choosing a Serving Layer for Comparison THIS MACHINE

§16 established that the patched stack is checkpoint-specific and cannot serve a group-32 checkpoint. That is disqualifying for an instrument meant to evaluate arbitrary new models, so we retested the obvious alternative — stock vllm/vllm-openai, the same image that previously crashed this machine.

Stock vLLM serves what the patched stack cannot — and the crash did not recur. The historical failure sequence (CUDA-graph OOM → Triton GDN autotuner crash → Xid 79, GPU off the bus) was deliberately re-run. The OOM reproduced, being a genuine memory-accounting issue. The autotuner crash and the Xid did not, across roughly eight server starts. That is strong confirmation that Xid 79 was the 12 V brownout diagnosed earlier, not a software fault, and that the 250 W cap resolved it.
CheckpointQuantizationPatched stackStock vLLM 0.27.1
dbirks/…AutoRound4-bit symmetric, g128servesserves
cyankiwi/…AWQ-INT44-bit asymmetric, g32fails at loadserves

The distinction is categorical, not marginal: where stock vLLM fails on the group-32 checkpoint it fails with OOM, a resource problem, never a shape error. It accepts the format; the patched stack structurally cannot.

The first actual quantization comparison

CheckpointDecode tok/sWeights VRAM
AutoRound (g128, symmetric)32.717.45 GiB
AWQ (g32, asymmetric)32.918.91 GiB

Matched conditions — eager, no speculation, 4096 context. Indistinguishable: 0.6% apart, far inside the ~5% noise floor established in §16. AWQ costs +1.46 GiB because its lm_head and MTP head remain BF16 where AutoRound-fast’s are int4. This is speed only; a correctness comparison still needs the two-hour Aider run.

What going stock costs

ConfigurationDecode tok/sNote
Patched stack, MTP k=4 (deployed)160–172split-KV verify attention, int4 lm_head, 40k draft vocab
Stock vLLM, MTP k=471.861.3% acceptance, 3.45 tok/step — speculation works correctly
Stock vLLM, CUDA graphs, no spec39.6graphs worth ~+20% over eager
Stock vLLM, eager, no spec32.7baseline
Two serving layers, two purposes. Stock vLLM is 2.3× slower on the same checkpoint — the gap is entirely the missing syv-ai patches, not a defect. That cost is acceptable for comparison, because it applies uniformly: every model is measured on equal, unoptimized footing, so differences are attributable to the model rather than to how well one repository’s patches happen to fit it. It is not acceptable for production, which is why the deployment stays patched. The serving stack is already one of the fairness fingerprint’s eleven fields, so a comparison tool will refuse to mix them — but it bears saying plainly: framework numbers are comparison-grade, production numbers are not the same measurement. ~72 tok/s in a report card and ~165 tok/s in production describe the same model honestly under different instruments.

Two non-obvious requirements

gpu_memory_utilization does not reserve for CUDA graph capture. At 0.93, vLLM consumed 22.99 GiB against a 22.1 GiB budget and then OOM’d needing 784 MiB for graphs. Pin the KV pool explicitly with --kv-cache-memory instead of trusting the utilization fraction. And max_num_seqs is bounded by Mamba cache blocks — hybrid GDN models need one block per decode sequence, so pinning KV to 1.5 GiB left 31 blocks against a default of 256 and raised a hard ValueError. Both belong in a preflight check, not in a troubleshooting log.

One comparison remains blocked, for a new reason. AWQ with MTP speculation does not fit on a single 24 GB card — OOM by 2.37 GiB, because its BF16 lm_head and MTP head dwarf AutoRound-fast’s int4 equivalents. A speculation-on comparison of these two checkpoints would require TP=2 for the AWQ arm alone, which changes the conditions and breaks the fairness contract. The speculation-off comparison above is clean; the speculation-on one cannot be made fair on this hardware.

18. The Framework’s First Report Card THIS MACHINE

Everything in §§10–17 was measured by hand-driven scripts. Those results are only reusable if the same measurements can be re-run on a new model without re-deriving the method each time, so they were consolidated into a benchmark framework: six axes, a declarative model config, and a machine-checked fairness contract. This is its first execution against live hardware — and the point of the exercise is not the numbers, which we already knew, but whether the instrument reproduces them.

It does. The correctness axis lands within noise of the hand-run reference. That axis had never executed end-to-end before: a mock endpoint could not reach it, because the Aider harness needs Docker and real code execution. Its results-directory globbing and score parsing ran for the first time here, against a known answer.

Framework output vs the hand-driven reference

MeasurementHand-run (§15, §11–12)Framework cardVerdict
Aider pass_112.5% (224 cases)14.7% (225)+2.2 pt — ~5 cases
Aider pass_248.7%49.8%+1.1 pt — ~2 cases
Well-formed edits90.6%90.2%−0.4 pt
TTFT slope+1.14 s/1K (r=1.00)+1.14 s/1K (r=1.00)exact
Coding decode mean144.0 tok/s138.1 tok/s−4% — inside noise
Decode slope vs context−0.70 tok/s/1K−0.54 (r=−0.91)same sign, weaker

The TTFT slope reproducing to three significant figures across two independently written harnesses is the strongest single signal that the instrument measures what it claims to. Prefill cost is the least noisy thing on this box, so it makes a good calibration target.

Full card — deployed stack, core tier

AxisResult
Speedcoding 138.1 mean / 160.1 peak · summarize 104.2 · acceptance 65.3% · 3.61 tok/step
ThroughputC1 123 → C16 402 → C32 404 (peak) → C64 402 agg tok/s
Context57,349 served · decode 119.6→84.8 · TTFT 65.7 s at max
Correctness225 scored · pass_1 14.7% · pass_2 49.8% · 90.2% well-formed
Efficiency0.514 tok/s/W single · 1.504 batch · 23,157 MiB peak

Throughput saturates at C≈32 (404 tok/s) and does not improve at C=64 — consistent with MAX_SEQS=8 per replica across two replicas, so beyond ~16 concurrent requests the excess queues rather than adding throughput.

The instrument’s resolution limit, quantified. The two runs used greedy decoding at temperature 0, which is nominally deterministic — yet Rust scored 26.7% by hand and 40.0% through the framework, a four-case swing on n=30. The cause is the batch-shape nondeterminism established in §10: at four concurrent threads, reduction order varies with batch composition and flips near-tie tokens. The practical consequence is that a single 225-case run resolves the aggregate to roughly ±2 points and per-language figures only to ±10. Per-language numbers should be read as indicative; a real per-language claim needs repeated runs, and any model-to-model difference smaller than a few points is not a difference.
The fairness gate passed its live test — two cards of the same model on the same stack fingerprinted identically (cdf5261e…) and were correctly reported comparable. The negative path (differing conditions → refuse, exit 1) was proven earlier against mock cards, after that gate was found to be silently broken: it printed “untrustworthy”, then “comparable”, then exited 0. A stock-vLLM card would exercise the negative path on real data and has not yet been run.

Peak power reached 276.8 W against a 250 W cap. The cap governs sustained draw rather than instantaneous excursions, so this is expected — but it is worth recording given that uncapped transients were what took a GPU off the bus earlier in this project. No Xid events occurred during the run.

19. Reading These Numbers

This document accumulated over five days, and its measurement conditions changed as we learned what mattered. Two tables in it can look directly comparable and not be. This section is the provenance key — the result of auditing every campaign’s wall-clock window against every other, using server logs and sampler files to establish what was running when.

ConditionChanged whenSize of effectAffects
Cross-GPU contentionCampaigns run in parallel on the two cardsup to −39%§11 (corrected), §17 “Beyond 64K” rows, ~6% of §10
250 W power capApplied Aug 22 after the Xid 79 fault~−14%Pre-cap: §10’s llama.cpp depth sweep and 350 W baselines. §§12–18 all post-cap
Thinking enabledOn by default until discovered in §15~5× tokensEvery token count before §15 is inflated
Prompt corpusRandom filler → real prose, mid-§11large, on acceptanceNever compare across the change
Prompt lengthVaries by campaign (46 tok → 192K)acceptance 0.61→0.78The error that produced a wrong correction to §10
Pinned vs load-balancedPer campaign~4–6%§§13–14, 17 pinned to the slower card; §§15, 18 balanced
Which sections are clean. §§12, 13, 14, 15, 16, 18 are provably single-tenant. §11 is contended and carries a correction banner. §17’s beyond-64K table inherits contended rows from §11 and is annotated per row. §10 is marginal — about 6% on one line, inside the noise floor.
The general lesson, which cost us three separate errors to learn. Every one of these hazards produced numbers that looked entirely plausible. None raised an error. Contention did not announce itself; thinking-on returned empty content with a normal HTTP 200; comparing across prompt lengths yielded a confident 20% “correction” that was itself wrong. On a machine where a measurement can be quietly wrong in half a dozen ways, the conditions a number was taken under are part of the number — which is why the framework in §18 hashes eleven of them into every result and refuses to compare across a mismatch. If you take one thing from this document for your own benchmarking, take that.

20. Canonical Numbers THIS MACHINE

The definitive single-GPU figures, re-measured strictly single-tenant, one configuration at a time, mean of five with spreads. 512-token greedy completions, thinking disabled, 250 W cap. Where earlier sections and this one disagree, this section wins.

Configuration (single RTX 3090)~1K prompt~55 promptAcceptanceVRAM
Patched vLLM + DFlash2 k=7212.1 ±1.2145.70.66122.4 G
Patched vLLM + MTP k=4167.8 ±0.7129.80.76422.7 G
Stock vLLM + MTP k=485.6 ±0.866.50.90120.3 G
Patched batch profile, no spec43.1 ±0.723.3 G
llama.cpp + MTP n=1, tuned42.6 ±0.840.616.9 G
Stock vLLM, no speculation32.8 ±0.132.619.5 G
llama.cpp, out-of-box defaults27.6 ±0.626.916.9 G

Spreads are 0.1–2.0% — far tighter than the ~5% noise floor quoted elsewhere. That floor is a restart phenomenon, not run-to-run variance: repeated measurements against one live server agree closely, while a server restart shifts the whole set. Both facts matter, and they are not the same fact.

Prompt length affects speed only through acceptance — proven by control. Stock vLLM without speculation 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. This is the mechanism behind every “faster at longer context” oddity in this document, and it is why comparing two speeds taken at different prompt lengths — an error made and corrected in §10 — is meaningless without stating acceptance.

Throughput

ConfigurationC=1C=4C=16C=32C=64
Batch profile, one GPU525.9761.8959.8
Two replicas + load balancer151.1457.4905.6

One open discrepancy, stated rather than smoothed: DFlash2 at ~1K measures 212.1 here against §12’s 181.5, a 17% gap. Both runs were single-tenant and this one has a ±0.6% spread across five repeats. The cause is unidentified. The patched-versus-stock advantage is 1.96× at both prompt lengths, revising the 2.3× quoted in §17.

21. The 64K Ceiling Was a Default — 30 August

Sections 11 and 17 treat ~64K as the practical bf16 limit on a single card. It is not a limit. start_qwen.sh line 74 reads MAX_LEN=${MAX_LEN:-65536}, an overridable default that was mistaken for a hardware constraint — while the running server was reporting GPU KV cache size: 89,437 tokens at those same settings.

max_model_lengpu_utilmax_num_seqsKV capacityResult
65,5360.93889,437the old default
85,0000.96490,212loads and serves
90,0000.97293,616loads and serves
110,0000.964refused at load

vLLM names the constraint exactly when exceeded: “7.56 GiB KV cache is needed, which is larger than the available KV cache memory (6.31 GiB)”. KV scales at roughly 13,700 tokens per GiB here — generous because only 16 of 64 layers hold attention KV. The real ceiling is ~90,000, and we had been serving 37% less than the card held. The cost of more context is concurrency: max_num_seqs falls 8 → 4 → 2.

22. Tensor and Pipeline Parallelism — 29 August

Both cards had only ever been used as independent replicas. Six configurations were planned; four ran, one was blocked by a model-implementation gap, one was abandoned as redundant.

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

The 4.2× capacity exceeds the model’s own 262,144 native window, so bf16 under TP reaches the architectural ceiling and the fp8/KVarN profiles become pointless there. PP=2 is unreachable: NotImplementedError: Pipeline parallelism is not supported for this modelQwen3_5ForConditionalGeneration does not implement vLLM’s SupportsPP. The fp8-at-native configuration was abandoned after ten minutes of FlashInfer JIT compilation, being redundant once bf16 reached 262,144.

The penalty is this board’s wiring, not a general PCIe result. GPU 0 is on a CPU root port at x8; GPU 1 is behind the PCH 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. Do not generalise these figures.

23. Two GPU Faults, One Broken Explanation — 22 and 29 August

Aug 22 16:24:03  NVRM: Xid (PCI:0000:01:00): 79, GPU has fallen off the bus.
Aug 29 21:49:55  NVRM: Xid (PCI:0000:01:00): 79, GPU has fallen off the bus.
                 NVRM: Xid 154, GPU recovery action ... 0x1 (GPU Reset Required)

Same card, identical signature, seven days apart. No AER errors in either boot — no correctable or uncorrectable PCIe errors, nothing preceding the fault.

Correction. Section 10 and the guide attribute the first fault to a 12 V brownout and present the 250 W cap as the fix, on the evidence of eight clean re-runs of the trigger. The second fault occurred with both cards verified at 250 W. Xid 79 without AER is consistent with either a power transient or a link fault, and these logs cannot separate them. A cap bounds average board power, not microsecond transients — and TP=2 is uniquely bad there, because synchronised allreduce barriers make both cards peak simultaneously, where every other workload here has one card working while the other idles. The cap rules out sustained over-draw; it does not exonerate power.

Verdict: do not retry TP=2 on this machine. The topology makes it a poor fit on the merits; it is the only workload twice associated with a dead GPU; and its payoff is weakly motivated now that ~90K is reachable on one card. NCCL_P2P_DISABLE buys nothing, since P2P was already unavailable. The real remedy is hardware — moving GPU 1 to a CPU-attached slot.

24. KV Quantization Is Not Free — 30 August

Every quality figure in this document before this section was measured on bf16 KV. The weights were always 4-bit; the cache dtype was the untested variable. Fixed deterministic 72-exercise subset; arm-to-arm comparable only, not against the 225-case 12.5 / 48.7 / 90.2 reference.

KV dtypepass_1pass_2Well-formedDecode tok/sMax ctxVRAM
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

fp8 is 22% slower than bf16, not faster. Its only benefit is context. The −5.6-point pass_2 gap sits inside the resolution limit of a 72-case run and is not a proven regression, but the direction is unhelpful and the speed cost is not in doubt.

Contextfp8 diverges from bf16 atKVarN diverges at
8Kchar 224char 93
32Kchar 104char 204
64Kchar 105char 105

Each arm is internally deterministic, yet both quantized caches diverge from the bf16 reference within the first ~100–200 characters at every length. KV quantization measurably changes what the model says, early rather than only at depth.

KVarN’s 50.0% is not evidence of superiority. n=36 on a different subset, and it carries the worst secondary indicators of the three: well-formed 91.7%, doubled error cases, and 3 test timeouts against bf16’s zero. That profile is what degradation looks like; the headline number is small-sample noise.
Scope limit. All arms were probed at ≤64K, because the bf16 reference cannot exceed 65,536. This shows KV quantization changes output at lengths bf16 can also serve. It does not test fidelity at the 150K/200K depths those profiles exist for. That remains unmeasured.

Two harness traps found en route. The divergence harness initially called the load balancer, so two “identical” calls hit different replicas and bf16 falsely appeared non-deterministic. And a probe run concurrently with a benchmark had batch-shape variation flip near-ties, producing another false non-determinism result. Both produced plausible-looking wrong answers; both were fixed by pinning to one replica and running serially.

25. Context Raised to 85,000; Sub-4-bit Closed 31 AUG

Two pieces of work: bank the context headroom §21 identified but never applied, and test the quantization survey’s standing recommendation.

The concurrency trade was not necessary

§21 measured a ~90,000-token ceiling and read the probe table as showing that extra context must be bought by cutting max_num_seqs from 8 to 4 to 2. Testing the alternatives first showed otherwise. At the launcher’s default gpu_util of 0.93 the ceiling was already 79,488 — +21% free. Raising it to 0.96 lifted KV capacity to 103,042 tokens, carrying 85,000 with all eight slots intact.

CheckResult
Near-limit request78,652-token prompt → correct completion, 86 s
/v1/models, LB and both replicas85,000
KV capacity per replica103,042
Decode at new setting148.5 ±0.1 tok/s
Control at old setting149.6 ±0.2 tok/s
Client guidance, max output57,344 → 76,808

+30% context at no concurrency cost and no measurable speed cost (0.7%, inside noise).

The comparator was wrong, and it affects the headline numbers. The first reading looked like −11.5% against the 167.8 baseline. It is not the context change: a control at the original settings with the second replica also resident reproduced ~149.6. So 167.8 and 212.1 describe a machine running one replica, and a resident idle neighbour costs about 11% single-stream. Both numbers are right for their conditions; the deployed two-replica stack simply is not the machine the ladder was measured on.

~3.5-bit compressed-tensors: not feasible

The survey’s recommendation was to quantize the model ourselves to ~3.5 bits in compressed-tensors, the only format the fast stack accepts — every published sub-4-bit build requires leaving it. Read from the running container, the blocker is upstream:

marlin_utils.py:56   return [scalar_types.uint4, scalar_types.uint4b8]
marlin_utils.py:85   res = [scalar_types.uint4b8, scalar_types.uint8b128]
compressed_tensors.py:504  is_weight_4_bits = weight_quant.num_bits == 4

Marlin supports 4- and 8-bit weights only. uint3b4 exists in the scalar-type registry but is absent from Marlin’s supported list, and no num_bits == 3 path exists in the quantization layer. A 3.5-bit checkpoint would be refused by the stack that was the entire reason for building it, so we stopped at the feasibility gate rather than spending a 56 GB download and hours of calibration.

Incidental correction: group-32 was never vLLM’s fault. MARLIN_SUPPORTED_GROUP_SIZES = [-1, 32, 64, 128]. The cyankiwi load failure in §16 was the syv-ai prep script’s hard-coded GROUP = 128, not a kernel limitation — so prepare/_groupsize.py targets the right thing, though it is still unverified against a live serve.

That leaves EXL3 (3.0 bpw ≈ 12.89 GiB, implying ~150K context on one card) as the only remaining route to a smaller checkpoint, on a different serving stack, with MTP support unknown.

26. Group-Size Fix Verified; ExLlamaV3 Measured 1 SEP

Two loose ends from §25 closed in one session, and a third opened.

The prep pipeline is checkpoint-agnostic

§25 recorded that the cyankiwi load failure was our own hard-coded GROUP = 128, not vLLM. The fix now derives the group size from the checkpoint’s own config.json, and it is verified live rather than by inspection: all three prep scripts derived group 32 from config_groups.group_0 and passed their round-trip gates (lm_head 0.53%, embed_tokens 0.49%, MTP linears 0.54–0.84%). vLLM loaded the checkpoint in 12 s with no scale-shape error, detected MTP, and served coherently at 139.5 ±0.2 tok/s greedy with 72.8% draft acceptance (per-position 0.93 / 0.79 / 0.66 / 0.54, mean accepted length 3.91). The gap to the deployed ~149.6 is the missing -fast variant — int4 head plus own-output draft vocabulary, worth about 15% on its own — not the group size. One further checkpoint assumption fell out: build_draft_vocab.py expected a model_extra_tensors.safetensors shard that only our own checkpoint ships, and now creates it when absent.

But cyankiwi is the wrong checkpoint for this card. Its ignore list leaves every GDN layer’s linear_attn.in_proj_a/in_proj_b (48 each) in bf16. Weights took 16.46 GiB against AutoRound’s 13.97 GiB, and group-32 scales are 4× the size of group-128 scales, so it could not start at the 64K default — “4.68 GiB KV cache is needed, 2.9 GiB available, estimated maximum model length 38016”. A finer group size is not a smaller file. It is a larger checkpoint that buys less context. Do not deploy it; the value of the run was the pipeline, not the weights.

ExLlamaV3 does support MTP on this model

§25 left EXL3 as the only remaining route to a smaller checkpoint, with MTP support unknown. Read from ExLlamaV3 1.4.5 source: exllamav3/architecture/qwen3_5_mtp.py loads the mtp.* head as a draft component, with GDN state rewind on rejection, and turboderp/Qwen3.8-27B-exl3 ships 39 mtp.* tensors in every branch. Measured on GPU 1 with the GPU 0 replica idle — the same condition as the 149.6 comparator — at 3.00 bpw, greedy, fixed 512-token probe:

ExLlamaV3 1.4.5 configtok/sDraft acceptVRAM 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× and acceptance matches vLLM’s 70–76%, so EXL3 avoids the llama.cpp wall — but 94.8 is 63% of the patched stack’s 149.6. What it buys is context on one card: 128K at fp16 KV, 200K at q8, against vLLM’s 85K bf16. The fp16 ceiling is sharp — with no draft model resident, 131,072 loads with 0.3 GiB spare and 135,168 fails autosplit. Caveats recorded: a separate recurrent draft model is rejected outright, stock exllamav3 has no DFlash2, and upstream issue #317 reports cache_mode 8,5 crashing on long prefill for this model, so keep V-bits even. Quality at 3.0 bpw was not measured here — that is §28.

Free-running divergence was the wrong instrument

§24’s scope limit — “fidelity at 150K/200K remains unmeasured, and we have no clean way to measure it” — was accepted too readily. The method is written up in longctx-fidelity-proposal.md: stop using free-running divergence, which compounds coin-flips and cannot exist above the reference ceiling, and use teacher-forced per-position NLL on unmemorisable documents plus known-answer probes at depth. Pass/fail thresholds written down before running anything.

27. Building the Fidelity Harness 2 SEP

Three pieces, built in parallel, all of which landed.

PieceWhat it is
Harnessnll.py, nll_compare.py, probes.py against vLLM; determinism floor is exactly zero, so any excess NLL is signal
DocumentsFive frozen documents of ≥200K tokens with sources, hashes and prompts recorded
EXL3 backendnll_exl3.py, writing the same schema; TabbyAPI on port 18031 with draft_mode: mtp
DocumentSourceTokens
gen1Model-generated: a literary-SF novel about a gravitational-wave collaboration202,296
gen2Model-generated: an engineering design document for a time-series database214,368
gen3Model-generated: an interlinked essay sequence212,578
nat122 arXiv papers submitted 2026-07-22 to 2026-09-01207,877
nat222 arXiv papers submitted 2026-07-23 to 2026-09-01206,061

The natural documents are dated after the model’s assumed training cut, so neither set can be predicted from weights alone. All five are frozen with sha256 recorded and must not be regenerated: a changed document invalidates every comparison against it.

Generating 200K tokens is its own study. The model does not fail by producing nonsense. It winds down into one of four modes, each of which is self-seeding — leave it in the document and the next continuation, handed that tail as context, does more of it. Chant (a closing run of one-line paragraphs), staccato (sentences collapse to four to six words), anaphora (“He thought about the X. He thought about the Y.” — lexically varied, so n-gram tests miss it), and replay (reproducing the handed excerpt at length before continuing, starting thousands of tokens back so the naive overlap check misses it). Rejection rates at the same temperature and top_p: the novel kept 167 of 225 rounds and the essay sequence 128 of 170; the technical design document kept 47 of 48. Structure to fill in is what prevents wind-down.
Trap: prompt_logprobs OOM-killed a production replica. The first such request killed the GPU 0 engine and Docker restarted it. vLLM log-softmaxes the full 248K vocabulary in fp32 for every scheduled prompt token, and at gpu_util 0.96 there is roughly 250 MiB spare. Fidelity runs now use a dedicated one-off container on GPU 1 — same image, GPU_UTIL=0.92 MAX_LEN=76800 EXTRA_ARGS="--max-num-batched-tokens 256", prefix cache off, one request at a time. That headroom is why the bf16 reference ceiling in §28 is 76.8K, not 85K.
Trap: docker compose up -d <service> recreates its dependencies. lb depends on both replicas. Bringing up only the balancer while the one-off fidelity container held port 18021 failed on GPU 1 as expected — and silently recreated the healthy qwen38-gpu0 on the way. Three minutes of outage, in-flight requests lost, from a command naming one service. Use --no-deps whenever a test container occupies a card.

Also recorded: nll_exl3.py tokenises with the contract tokenizer rather than the EXL3 model directory’s own, because the two are not identical — the EXL3 copy’s pre-tokenizer regex handles \p{M} differently and the two disagree on text with combining marks. The vocabularies are byte-identical, so contract ids feed the EXL3 model directly. On a synthetic 32,768-token document the harness measured 3.00 bpw costing +0.0182 nats/token over 4.00 bpw, confirming it resolves differences of exactly the size the study was looking for.

28. Long-Context Fidelity Results 3 SEP

Excess NLL against bf16 over 0–76K, mean / worst 8K bucket, nats per token. Threshold 0.02, set before the run.

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

Natural text (nat1/nat2, mean NLL ~4.7) is where quantization shows; the model’s own text (gen*, mean NLL 0.2–0.5) is easy and hides it. Negative means on nat1/nat2 are kernel numerics — FlashAttention bf16 against FlashInfer fp8 — not fp8 being better.

Above 76K no reference exists, so profiles are compared to each other with fp8 as the anchor. “Other minus fp8”, mean / worst bucket, then minimum top-1 agreement, over 80–150K:

DocumentKVarN vs fp8EXL3 3.0 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 document. KVarN drifts — its nat1 gap roughly triples between the sub-76K band (+0.025 mean) and 80–150K (+0.072 mean, +0.129 worst), while staying inside 0.01 on generated text. 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.

Criterion B is retired. The proposal said no bucket past 80K may exceed the 57–76K mean by more than 0.05 nats. On gen3 fp8 and KVarN both rise by +0.075 while agreeing with each other to 0.005 nats — the rise is the document, not the cache. An absolute threshold on a metric that tracks content is content-confounded. The reference-free cross-profile comparison is immune, because both arms read the same words.

Known-answer probes at depth

ProfileDepthsMulti-keyVerbatimCross-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 n=564K / 123K40/40 each100% each5/5 each

Every instance passed at every depth for every profile — KVarN at 196K and EXL3 3.0 bpw at 123K included. The fidelity cost above does not surface as retrieval, copying or multi-hop failure — it is a distribution shift, not a capability loss, which is why it took a distributional instrument to see.

The EXL3 row took two attempts, and the first was a harness bug. The first run scored 0 on every instance because probes.py assumed a non-null usage field and TabbyAPI returns usage: null; the scorer raised on every row and recorded the exception as a zero, while the server’s own log showed 200s and sensible generations. A harness that scores its own exceptions will report a catastrophic model failure with complete confidence. Fixed and rerun clean at both depths.

Why 4.0 bpw does not rescue EXL3

Plain 4.0 bpw scoring worse than 3.0 on natural text is surprising enough to chase down.

ControlResult
3.0 q8 repeated a day laterBit-identical
Plain 4.0, fp16 vs q8 cache, nat1 at 32K0.01 nats, 98.6% top-1 — cache quantization irrelevant
Plain 4.0 vs self-calibrated SC_4.00bpw_H5, nat1 at 64KSC 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 6.99 · plain 4.0 7.03 · SC 4.0 7.00

The harness is not at fault. By ExLlamaV3’s own perplexity tool the two published 4.0 bpw files are no better than the 3.0 bpw file, and on recent arXiv prose they are clearly worse. Why remains unexplained — calibration-set mismatch or a bad upstream quantization run are the candidates, both outside our control.

Decision: for context past 85K on this box, patched vLLM with fp8 KV at 150K. No measurable fidelity cost against bf16, no inflection to 150K, 100% on every probe to 147K. The only price is the known 22%: 124.4 against 158.8 tok/s. KVarN at 200K costs up to ~0.12 nats on hard text and drifts with depth — the answer to “it must fit”, not a default. EXL3 at 3.0 bpw buys 200K on one card for ~0.13 nats on natural text, 74% top-1, and 37% of the speed given up; it loses to fp8 at 150K on every axis but raw length, and to KVarN at 200K on fidelity.

Operational cost, for anyone repeating this. Prefill on the 256-token chunk the dedicated container requires: 100 s at 76K, 290 s at 150K, 465 s at 200K. ExLlamaV3: 234 s for 131K at fp16, 465 s for 205K at q8. Five documents times five profiles is measured in days, not hours, and it holds a card the whole time.

29. Swift-Qwen3.8-27B: Does the Shorter-Thinking Claim Hold? 11–18 SEP

A Reddit post and model card (ukisai/Swift-Qwen3.8-27B) claimed 46–58% fewer thinking tokens at under a point of accuracy cost, with LiveCodeBench v6 rising 76.8 → 81.6%. Plan A was to requantize the bf16 weights with the base checkpoint’s AutoRound recipe and serve both through patched vLLM. It was abandoned before running: the 55 GB download died once on a DNS outage, the link ran at 4–10 MB/s, and GGUFs already existed. Plan B: bartowski had quantized both models with the same release and a byte-identical calibration file, which is a cleaner pairing than anything we could have produced.

Served through the official llama.cpp CUDA image on GPU 1: Q4_K_M, two slots of 40K, q8_0 KV, --spec-type draft-mtp (85 of 94 drafts accepted on a probe), 56 tok/s single-stream, 20.5 GB. First attempt at the full 225-exercise Aider set ran 17 hours and completed nine exercises — the timeout loop described in Lessons. Restarted as a chain on one card, arm after arm: LiveCodeBench base (5.2 h), LiveCodeBench Swift (4.3 h), Aider base (4.9 h of requests, 9.6 h summed exercise time), Aider Swift (2.8 h), LiveCodeBench with the author’s own quant (4.4 h).

Decision: Swift is the model to run when thinking is on. Median output tokens −50% on LiveCodeBench and −53% per Aider request, accuracy at parity (identical on every problem neither model truncated), 43% less time on agentic work even at llama.cpp speed. Both models now sit behind one endpoint with an njs router picking the card from the request’s model field. The base model on patched vLLM stays the answer for thinking-off and latency-bound work. Numbers and caveats: Results.

Router bug found on the way. The first njs router used js_set + proxy_pass http://$var. nginx evaluates that variable before reading the request body, so requestText was empty, every request went to the base card, and a Swift request came back 404 from vLLM. Fixed with a js_content handler and internalRedirect.

1 TP=2 + MTP k=3 hit 264 tok/s on code generation (high draft acceptance) in tfriedel’s lab; narrative was ~179 tok/s. Numbers are from Qwen3.6-27B (same architecture).

2 Range spans baseline vLLM INT4 (~114, syv-ai) to llama.cpp Q4+MTP n=3 (~134–151, kgptalkie).

3 SGLang SMG cache-aware routing claims +92% throughput vs round-robin on prefix-heavy workloads; actual 3090 numbers are untested. Unique-prompt throughput should match vLLM within ~2–4%.

3.6 Measured on Qwen3.6-27B. Same hybrid architecture (GDN+softmax), same parameter count. Directly comparable for throughput; Qwen3.8 is a training improvement, not an architecture change.

Key sources: syv-ai/qwen38-27b-rtx3090, tfriedel/qwen3.6-rtx3090-lab, dbirks & Pilcothink HF repos, lued/Qwen3.8-27B-INT8-W8A16-MTP, kingy.ai GGUF benchmarks, kgptalkie llama.cpp guide, AtomicChat/Unsloth GGUF repos, RadixArk/Qwen3.8-27B-DSpark, SGLang GitHub (sgl-project/sglang, PR #23331), Qwen official blog.

Research compiled August 22, 2026; measured results added August 24 (section 10) and context-scaling curves August 25 (section 11, since corrected for measurement contention) and clean coding results (section 12) a multi-turn agentic profile (13), an independent cross-check (14) and first correctness results (15) and a blocked quantization comparison (16) serving-layer selection (17) and the framework’s first report card (18) August 26, 2026. Sections 21–24 were added 29–31 August 2026 (context ceiling, tensor parallelism, GPU fault forensics, KV quantization), section 25 on 31 August (context raised to 85,000; sub-4-bit closed) and sections 26–28 on 1–3 September (group-size fix and ExLlamaV3, the fidelity harness, and the long-context fidelity results). Sections 1–9 are community numbers and may vary with driver, build, and prompt shape; sections 10–28 are this host, at a 250 W power cap.