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.
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.
| 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 |
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 |
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 |
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 |
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 | — |
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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) | — | — |
DFlash2 Depth Sweep (llama.cpp, GPU 1, 250 W)
| Draft n | tok/s | Speedup | Draft acceptance | Mean accepted len | Notes |
|---|---|---|---|---|---|
| Off | 33.3 | 1.00× | — | — | 250 W baseline (38.7 at 350 W) |
| n=3 | 55.1 | 1.65× | 70.7% | 3.12 | |
| n=4 | 60.7 | 1.82× | 63.9% | 3.55 | PEAK on RTX 3090 |
| n=5 | 60.2 | 1.81× | 60.2% | 3.99 | Within noise of n=4 |
| n=7 | 54.2 | 1.63× | 47.6% | 4.33 | Model-card recommendation—worse here; 8-token verify batch costs more than it returns |
| n=10 | 53.4 | 1.60× | 47.6% | 4.33 | Identical 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)
| Config | KV dtype | pp512 tok/s | tg128 tok/s |
|---|---|---|---|
| 1 GPU | f16 | 1,027 | 38.8 |
| 1 GPU | q8_0 | 1,275 | 38.7 |
| 1 GPU | q4_0 | 1,280 | 38.1 |
| 2 GPU, layer split | f16 | 1,391 | 42.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)
| Check | MTP k=4 | DFlash2 k=7 | Reference | Verdict |
|---|---|---|---|---|
| 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
- Xid 79 “GPU has fallen off the bus” on GPU 0 during stock vLLM’s Triton GDN autotuner (rapid idle↔350 W transients). Not recoverable without a reboot. Diagnosed as a 12 V-rail brownout; both cards now run at
nvidia-smi -pl 250 -pm 1and nothing has faulted since, including 32 s at 249.8 W pinned under C=64. The cap costs ~14% on unassisted llama.cpp decode and is invisible on the vLLM winner. - GPU 0 vs GPU 1 gap in MTP mode: 135 vs 150 tok/s, same image, model, prompt, and power cap, reproduced twice. Unexplained—candidates are PCIe lane width on slot 1 vs slot 2, or silicon lottery. DFlash2 on GPU 0 (153.9) was not re-run on GPU 1; it may go higher.
- DFlash2 in llama.cpp needs an unmerged PR (#27342,
z-lab:dflash2); the stock--spec-type draft-dflashonly loads v1 drafters. Built separately ata separate build directory. - Not yet run: two batch replicas behind the LB; DFlash2 on GPU 1; a SGLang DP=2 retry at 250 W; DSpark (research #4); any TP=2 config; long-context (>4K) throughput.
11. Speed vs Context Length THIS MACHINE
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.
Table view
| Config | 504 | 507 | 2K | 2K | 7K | 8K | 16K | 16K | 31K | 31K | 56K | 58K |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| vLLM patched + DFlash2 k=7 | — | 94.80.71s | — | 66.22.69s | — | 50.611.56s | 38.524.91s | — | — | 34.852.85s | — | 61.170.10s |
| vLLM patched + MTP k=4 | — | 67.40.81s | — | 57.03.05s | — | 49.712.22s | 52.019.82s | — | — | 36.952.89s | — | 40.7103.24s |
| llama.cpp MTP n=1, 2 GPU | 61.70.55s | — | 60.91.65s | — | 55.65.63s | — | — | 54.511.77s | 45.525.65s | — | 37.161.17s | — |
| llama.cpp DFlash2 n=4 | — | 44.70.64s | — | 41.92.38s | — | 41.08.56s | 32.717.67s | — | — | 34.337.58s | 29.572.80s | — |
| llama.cpp MTP n=1, 1 GPU | 46.90.57s | — | 46.32.09s | — | 42.67.68s | — | — | 42.016.39s | 37.233.85s | — | 34.268.56s | — |
| Long-context profile | 68K | 99K | 128K | 150K | 152K | 193K |
|---|---|---|---|---|---|---|
| vLLM CTX=long (fp8 KV) + MTP k=3 | 51.9104s | 61.3136s | — | 63.9239s | — | — |
| vLLM CTX=long (fp8 KV) + DFlash2 | 29.1214s | 39.2313s | 38.7421s | — | — | — |
| vLLM CTX=huge (KVarN 4/2-bit) + MTP | — | 34.8132s | — | — | 27.1234s | 23.7331s |
| llama.cpp MTP n=1, q8 KV, 262K ctx | 32.786s | 30.1145s | — | — | 25.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.
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.
| Profile | KV cache | Max reached | Decode at max | Verdict |
|---|---|---|---|---|
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) |
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.
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.
Decode tok/s by task class
| Task class | Context | vLLM MTP k=4 | vLLM DFlash2 k=7 | llama.cpp 2-GPU |
|---|---|---|---|---|
| CanonicalChudnovsky π | 55 | 144.361% acc | 135.642% | 63.31.93 tok/step |
| Boilerplateargparse CLI | 64 | 152.167% | 160.959% | 63.81.95 |
| Repo editreal source + “add --dry-run, show diff” | 1,007 | 169.778% | 181.570% | 64.21.98 |
| 8,281 | 159.678% | 109.946% | 60.61.98 | |
| 32,463 | 136.281% | 120.351% | 47.82.00 | |
| Code reviewsame files, “find bugs” | 987 | 121.056% | 144.350% | 59.11.88 |
| 8,280 | 138.471% | 132.546% | 47.81.90 | |
| 32,457 | 131.076% | 158.862% | 38.41.88 | |
| Summarize§11’s task, verbatim | 1,042 | 117.054% | 126.239% | 54.91.86 |
| 8,075 | 110.352% | 99.629% | 43.71.79 | |
| 31,833 | 88.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
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.
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.
| Context band | Turns | Acceptance | Decode tok/s | TTFT | Tokens/step | Acceptance by position 0–3 |
|---|---|---|---|---|---|---|
| 0–10K | 7 | 65.3% | 137.3 | 5.9 s | 3.61 | 0.86 0.71 0.57 0.47 |
| 10–20K | 11 | 73.3% | 140.8 | 12.5 s | 3.93 | 0.90 0.79 0.67 0.58 |
| 20–30K | 5 | 77.3% | 137.2 | 22.2 s | 4.09 | 0.89 0.81 0.73 0.66 |
| 30–40K | 7 | 72.3% | 124.6 | 33.1 s | 3.89 | 0.88 0.77 0.67 0.58 |
| 40–50K | 16 | 69.1% | 113.8 | 48.6 s | 3.76 | 0.86 0.74 0.63 0.53 |
| 50–70K | 8 | 68.5% | 108.5 | 61.3 s | 3.74 | 0.85 0.73 0.63 0.53 |
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.
--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.
| Harness | Mean acceptance | Acceptance slope | r | Decode slope | Context span |
|---|---|---|---|---|---|
| Pi (baseline) | ~70% | −0.006 pts/1K | −0.01 | −0.571 tok/s/1K | 5–61K |
| Qwen Code (cross-check) | 76.1% | −0.063 pts/1K | −0.03 | −0.593 tok/s/1K | 21–37K |
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.
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
| Format | pass_1 | pass_2 | Well-formed | s/case |
|---|---|---|---|---|
| whole | 0.0% | 40.0% | 100% | 88.9 |
| diff | 20.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.
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.
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.
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.
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:
| Component | cyankiwi “AWQ-INT4” | AutoRound-fast (deployed) | Affects correctness? |
|---|---|---|---|
| body weights | 4-bit asymmetric, group-32 | 4-bit symmetric, group-128 | yes |
lm_head | BF16 | int4 | yes |
embed_tokens | BF16 | int8 | marginal |
| MTP head | BF16 | int4 | no — speed only |
| draft vocabulary | none | 40k tokens | no — 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.
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.
A useful by-product: the noise floor
| Arm (identical checkpoint & config) | Decode tok/s | TTFT | VRAM |
|---|---|---|---|
| AutoRound-fast, GPU 0 | 165.4 | 0.96 s | 23,208 MiB |
| AutoRound-fast, GPU 1 | 172.0 | 0.91 s | 23,130 MiB |
| AutoRound-fast, GPU 0 after restart | 159.6 | 1.00 s | 22,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.
| Checkpoint | Quantization | Patched stack | Stock vLLM 0.27.1 |
|---|---|---|---|
| dbirks/…AutoRound | 4-bit symmetric, g128 | serves | serves |
| cyankiwi/…AWQ-INT4 | 4-bit asymmetric, g32 | fails at load | serves |
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
| Checkpoint | Decode tok/s | Weights VRAM |
|---|---|---|
| AutoRound (g128, symmetric) | 32.7 | 17.45 GiB |
| AWQ (g32, asymmetric) | 32.9 | 18.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
| Configuration | Decode tok/s | Note |
|---|---|---|
| Patched stack, MTP k=4 (deployed) | 160–172 | split-KV verify attention, int4 lm_head, 40k draft vocab |
| Stock vLLM, MTP k=4 | 71.8 | 61.3% acceptance, 3.45 tok/step — speculation works correctly |
| Stock vLLM, CUDA graphs, no spec | 39.6 | graphs worth ~+20% over eager |
| Stock vLLM, eager, no spec | 32.7 | baseline |
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.
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.
Framework output vs the hand-driven reference
| Measurement | Hand-run (§15, §11–12) | Framework card | Verdict |
|---|---|---|---|
| Aider pass_1 | 12.5% (224 cases) | 14.7% (225) | +2.2 pt — ~5 cases |
| Aider pass_2 | 48.7% | 49.8% | +1.1 pt — ~2 cases |
| Well-formed edits | 90.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 mean | 144.0 tok/s | 138.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
| Axis | Result |
|---|---|
| Speed | coding 138.1 mean / 160.1 peak · summarize 104.2 · acceptance 65.3% · 3.61 tok/step |
| Throughput | C1 123 → C16 402 → C32 404 (peak) → C64 402 agg tok/s |
| Context | 57,349 served · decode 119.6→84.8 · TTFT 65.7 s at max |
| Correctness | 225 scored · pass_1 14.7% · pass_2 49.8% · 90.2% well-formed |
| Efficiency | 0.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.
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.
| Condition | Changed when | Size of effect | Affects |
|---|---|---|---|
| Cross-GPU contention | Campaigns run in parallel on the two cards | up to −39% | §11 (corrected), §17 “Beyond 64K” rows, ~6% of §10 |
| 250 W power cap | Applied 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 enabled | On by default until discovered in §15 | ~5× tokens | Every token count before §15 is inflated |
| Prompt corpus | Random filler → real prose, mid-§11 | large, on acceptance | Never compare across the change |
| Prompt length | Varies by campaign (46 tok → 192K) | acceptance 0.61→0.78 | The error that produced a wrong correction to §10 |
| Pinned vs load-balanced | Per campaign | ~4–6% | §§13–14, 17 pinned to the slower card; §§15, 18 balanced |
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 prompt | Acceptance | VRAM |
|---|---|---|---|---|
| Patched vLLM + DFlash2 k=7 | 212.1 ±1.2 | 145.7 | 0.661 | 22.4 G |
| Patched vLLM + MTP k=4 | 167.8 ±0.7 | 129.8 | 0.764 | 22.7 G |
| Stock vLLM + MTP k=4 | 85.6 ±0.8 | 66.5 | 0.901 | 20.3 G |
| Patched batch profile, no spec | 43.1 ±0.7 | — | — | 23.3 G |
| llama.cpp + MTP n=1, tuned | 42.6 ±0.8 | 40.6 | — | 16.9 G |
| Stock vLLM, no speculation | 32.8 ±0.1 | 32.6 | — | 19.5 G |
| llama.cpp, out-of-box defaults | 27.6 ±0.6 | 26.9 | — | 16.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.
Throughput
| Configuration | C=1 | C=4 | C=16 | C=32 | C=64 |
|---|---|---|---|---|---|
| Batch profile, one GPU | — | — | 525.9 | 761.8 | 959.8 |
| Two replicas + load balancer | 151.1 | 457.4 | 905.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_len | 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 and serves |
| 90,000 | 0.97 | 2 | 93,616 | loads and serves |
| 110,000 | 0.96 | 4 | — | refused 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.
| Measure | Single GPU | TP=2 |
|---|---|---|
| KV cache capacity | 89,437 | 377,518 ×4.2 |
| Max context verified served | ~90,000 | 204,058 311 s, ~656 tok/s prefill |
| Decode, C=1 | 167.8 | 119.0 ±3.4 −29% |
| Throughput, C=16 | 610 | 234 −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 model — Qwen3_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.
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.
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 dtype | pass_1 | pass_2 | Well-formed | Decode tok/s | Max ctx | 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 |
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.
| Context | fp8 diverges from bf16 at | KVarN diverges at |
|---|---|---|
| 8K | char 224 | char 93 |
| 32K | char 104 | char 204 |
| 64K | char 105 | char 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.
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.
| Check | Result |
|---|---|
| Near-limit request | 78,652-token prompt → correct completion, 86 s |
/v1/models, LB and both replicas | 85,000 |
| KV capacity per replica | 103,042 |
| Decode at new setting | 148.5 ±0.1 tok/s |
| Control at old setting | 149.6 ±0.2 tok/s |
| Client guidance, max output | 57,344 → 76,808 |
+30% context at no concurrency cost and no measurable speed cost (0.7%, inside noise).
~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.
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.
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 config | tok/s | Draft accept | 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× 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.
| Piece | What it is |
|---|---|
| Harness | nll.py, nll_compare.py, probes.py against vLLM; determinism floor is exactly zero, so any excess NLL is signal |
| Documents | Five frozen documents of ≥200K tokens with sources, hashes and prompts recorded |
| EXL3 backend | nll_exl3.py, writing the same schema; TabbyAPI on port 18031 with draft_mode: mtp |
| Document | Source | Tokens |
|---|---|---|
gen1 | Model-generated: a literary-SF novel about a gravitational-wave collaboration | 202,296 |
gen2 | Model-generated: an engineering design document for a time-series database | 214,368 |
gen3 | Model-generated: an interlinked essay sequence | 212,578 |
nat1 | 22 arXiv papers submitted 2026-07-22 to 2026-09-01 | 207,877 |
nat2 | 22 arXiv papers submitted 2026-07-23 to 2026-09-01 | 206,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.
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.
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.
| Profile | gen1 | gen2 | gen3 | nat1 | nat2 | top-1 min |
|---|---|---|---|---|---|---|
| vLLM fp8 | +0.008 / +0.025 | −0.005 / +0.001 | +0.001 / +0.001 | −0.033 / +0.006 | −0.026 / +0.022 | 0.921 |
| vLLM KVarN | +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 fp16 | +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 q8 | +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 q8 | +0.024 / +0.039 | −0.013 / +0.020 | +0.017 / +0.022 | +0.280 / +0.385 | +0.256 / +0.425 | 0.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:
| Document | KVarN vs fp8 | EXL3 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.
Known-answer probes at depth
| Profile | Depths | Multi-key | Verbatim | 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 n=5 | 64K / 123K | 40/40 each | 100% each | 5/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.
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.
| Control | Result |
|---|---|
| 3.0 q8 repeated a day later | Bit-identical |
| Plain 4.0, fp16 vs q8 cache, nat1 at 32K | 0.01 nats, 98.6% top-1 — cache quantization irrelevant |
Plain 4.0 vs self-calibrated SC_4.00bpw_H5, nat1 at 64K | SC 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 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.
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).
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.