epsilon · inference audit · 23 Sep 2026

Getting more tokens/sec out of epsilon's llama.cpp

A read-only audit of the 2× RTX 3090 box and the live Qwen3.8-27B server, with a ranked list of software, config, drafter and hardware changes. The first pass was read-only. Later the same day, with approval, I ran live clock tests and a controlled benchmark window that briefly stopped the server; those results are in HyperQwen and Live tests. Other numbers come from epsilon's live logs and telemetry, or from published community benchmarks where marked.

i5-10400F · 32 GB DDR42× RTX 3090 24 GBASUS PRIME Z490-VHiveOS · driver 580.159 · CUDA 13llama.cpp fork 4df29be + FastMTP
66
tok/s: llama.cpp FastMTP as deployed (clean, 1 stream)
136
tok/s: HyperQwen vLLM on GPU0 alone (2.05×)
112
tok/s: HyperQwen on both GPUs, 262k context
~320
tok/s prompt processing on 1–3k prompts (llama.cpp)

TL;DR: what matters, after testing

01
Switch the engine: HyperQwen (patched vLLM) doubles generation speedSame Qwen3.8-27B family (an uncensored AWQ-W4A16 build, since Hauhau publishes only GGUF), same box, same prompts. llama.cpp FastMTP: 66 tok/s. HyperQwen on GPU0 alone: 136 tok/s. On both GPUs with the full 262k context: 112 tok/s. Output quality checked by hand. It is now a one-click Nova dashboard entry that serves on :8094 under the same model name.
1.7–2.05× TG
02
On this board, two GPUs are slower than one for tensor parallelismHyperQwen TP=2 (112) lost to TP=1 (136). GPU1 sits on a chipset PCIe Gen2 x4 link with no P2P, so every layer's all-reduce goes through host RAM. Use TP=2 only when you need more than about 64k context; otherwise run one model per GPU.
−18% at TP2
03
Fix GPU1's coolingGPU1 reports its fan at 0% while at 86–87 °C, and is the only card with hardware thermal-slowdown events. Capping the core at 1400 MHz dropped it to 68 °C. This is a stability risk first and a speed problem second.
stability
04
Cap the core clock at 1700 and overclock GPU0's memoryMeasured live: the 1700 MHz cap saved about 20 W per card with no measurable generation loss. GPU0 memory at +1500 gave +7.8% bandwidth (884 → 953 GB/s) with 0 errors.
−9% W · +4–8% TG
05
Correction: the single-GPU llama.cpp profile did not helpMy first-pass recommendation (GPU0 only, 64k, q8 KV, -ub 1024) measured 53 vs 66 tok/s, i.e. slower. Under clean conditions the 2-GPU layer split costs nothing; the ~52 tok/s seen in live logs was concurrent traffic. The likely culprit is the q8_0 KV cache. Keep the current llama.cpp config if you stay on llama.cpp.
−20% (refuted)
llama.cpp 1-GPU q8 KV (my profile)
53
llama.cpp FastMTP 2-GPU (deployed)
66
HyperQwen TP=2 + DFlash2
111
HyperQwen TP=2 + MTP, 262k ctx
112
HyperQwen GPU0 only + MTP
136
ExLlamaV3 + DFlash2, greedy*
163*
tok/s generation, one request at a time, 512-token answers over 4 prompt types × greedy and sampled, 2 repetitions. Solid bars = measured on epsilon on 23 Sep with the live server stopped. Striped* = self-reported by others.

HyperQwen (patched vLLM) vs llama.cpp: measured 23 Sep

syv-ai/HyperQwen is a 38-patch series on vLLM 0.29 tuned for Qwen3.8-27B on 24 GB cards: requantized heads, a calibrated MTP draft vocabulary, and DFlash2. It cannot load GGUF, and HauhauCS publishes only GGUF, so the test used Ar4ikov/Qwen3.8-27B-Uncensored-AWQ-W4A16-ASYM-HyperQwen-fast. That is the same architecture and parameter count with a different uncensoring (abliteration, not Hauhau's fine-tune), so speed transfers but behaviour may differ.

Method: haufastmtp stopped for 23 min, each config benched alone, and haufastmtp auto-restored. 4 prompts (code, explanation, math, story) × greedy and sampled (T=0.7) × 2 reps, 512-token answers, thinking off, one request at a time, after an untimed warm-up. Decode = tokens ÷ (last − first token time). Both llama.cpp configs ran on a private port so agent traffic couldn't touch them.

ConfigAvgcodeexplainmathstoryTTFTTokens/stepPowerVRAM
llama.cpp FastMTP, 2-GPU layer split (deployed)66.171.166.772.654.0367 ms~2.0245+249 W19.0+20.6 GB
llama.cpp 1-GPU, q8 KV, ub 1024 (my first-pass profile)53.157.853.058.943.0237 ms~1.95276 W21.9 GB
HyperQwen TP=2, MTP112.4122.1111.6137.378.4151 ms3.3–3.6275+275 W23.2+23.2 GB
HyperQwen TP=2, DFlash2110.9118.9106.8148.669.4165 ms3.9–4.4274+269 W23.4+23.4 GB
HyperQwen GPU0 only, MTP135.6148.4136.5162.994.7114 ms~3.5275 W22.6 GB

Nova dashboard: one-click drop-in on :8094

New entry "HyperQwen vLLM" (bolt icon), next to "HauhauCS FastMTP". Clicking Start stops haufastmtp (same port and GPUs) and boots HyperQwen on :8094 under the same model name, qwen38-hauhau, so clients need no changes. To switch back: Stop it, then Start "HauhauCS FastMTP". Wiring follows the existing pattern: ~/hyperqwen-run.sh + conf/hyperqwen.conf, editable in the dashboard; use recreate after edits. Both entries now show status by their own container rather than by port, so only the active one lights up.

Drop-in check (TP=2, 262k, vision on)Result
Model name qwen38-hauhau, max_model_len✅ served as qwen38-hauhau + qwen3.8-27b, 262,144 ctx
Chat, thinking off (enable_thinking:false)✅ exact reply, 0.3 s
Thinking on⚠️ works, but reasoning arrives in message.reasoning; llama.cpp uses reasoning_content. Clients that display thinking may need that field name; the final content is unaffected.
Tool callingget_weather({"city":"Qatif"})
Vision (image_url)✅ identified red-left / blue-right, 0.7 s
Long context, 90,039-token prompt✅ needle retrieved, 167 s

Profile choice (in the conf): the default is TP=2 / 262k, which matches today's endpoint (agents send 66–122k prompts) at 112 tok/s. For about 136 tok/s set GPUS="0" TP=1 MAX_LEN=65536 and recreate; that also frees GPU1 for Gemma. Requests over 64k would then be rejected.

Current state (what's actually running)

Hardware

PartDetailInference impact
CPUIntel i5-10400F, 6C/12T, ~4.0 GHz all-core, no AVX-512Fine while the model is fully on GPU; weak for CPU/MoE offload
RAM4× 8 GB DDR4, two mismatched kits (CMW16GX4M2E3200C16 + M2Z2933C16), running at 2133 MT/s, so XMP is offOnly matters for offloaded layers or experts
BoardASUS PRIME Z490-V, BIOS 0602 (Apr 2020)Old BIOS; one CPU x16 slot, the second slot is chipset x4
GPU0RTX 3090 · PCIe Gen2 x16 · PL 280/370 W · 67 °C, fan 95%Healthy
GPU1RTX 3090 · PCIe Gen2 x4 (~2 GB/s, via chipset) · PL 280/370 W · 87 °C, fan 0%Thermal throttling; the slow link rules out tensor parallelism
NVLink / P2PNone (links inactive); topology PHBNo fast GPU-to-GPU path
StorageSamsung 512 GB NVMe (93% full), Kingston 240 GB SATA, TEAM 128 GB SATA (OS)Load time only; the full NVMe is a housekeeping issue
SoftwareHiveOS (Ubuntu 18.04, kernel 5.6, glibc 2.27), driver 580.159.04, docker; llama.cpp built on CUDA 12.6 with sm_86The old OS makes native builds painful; docker is the right approach

Live llama.cpp server (container haufastmtp)

llama-server -m Qwen3.8-27B-Uncensored-HauhauCS-Aggressive-Q4_K_P.gguf \
  -ngl 999 -c 262144 -fa on --jinja --tools all --mmproj …BF16.gguf \
  --spec-draft-model …FastMTP-32K.gguf --spec-draft-ngl 99 \
  --spec-type draft-mtp --spec-draft-n-max 2 --spec-draft-p-min 0 \
  -ub 256 -sm layer          # 4 slots, unified KV, f16 KV cache
Measured (live logs)Value
Generation51.4–55.5 tok/s (55 tok/s after a 122k-token prompt)
Prompt processing293–335 tok/s @ 1–2.6k tok · 641 tok/s @ 122k tok
Draft acceptance84–93%, mean accepted length 2.7–2.9 (max 3)
VRAMGPU0 19.0 GB · GPU1 20.6 GB of 24 GB
Clocks under loadGPU0 ~1590–1755 MHz · GPU1 ~1455–1635 MHz · mem 9501 MHz both · 0 offsets
Throttle reasonsSW power cap active on both · SW thermal: GPU0 8.7 h, GPU1 3.6 h (cumulative) · HW thermal GPU1 24 s

Red flags found

criticalGPU1 fan reports 0% at 86–87 °C under a ~245 W load. Either the fan has stopped, or fan control is not being applied: HiveOS is in maintenance mode, and its OC/fan config still lists six GPUs (FAN="98 98 100 …"), left over from an old mining setup. Check the fan physically. GDDR6X on a 3090 commonly reaches 100–110 °C when the core is at 87 °C, and hot memory throttles bandwidth, which is exactly what generation speed depends on.
highPCIe forced to Gen2 on both cards (the cards report a 5 GT/s maximum, typical of a mining BIOS profile). GPU1 is also in the chipset x4 slot. With -sm layer this barely affects generation, but it does slow model loads, large prefills and any tensor-parallel option.
highPrefill is slowed by the config: -ub 256, plus MTP combined with -sm layer. Known issue (#27428/#27306): the MTP step runs synchronously after each prefill batch across the split, which roughly halves prompt throughput on multi-GPU. On a single GPU the cost is about 3%.
mediumMemory overclock not applied, and the power limit is 280 W. nvtool shows 0 MHz memory and core offsets on both cards, and the memory runs at 9501 MHz (CUDA P2 state) instead of 9751 MHz or higher. The OC from the July tuning session did not survive a reboot.
lowRAM at 2133 MT/s with mixed kits, CPU governor ondemand, NVMe 93% full, and 4 parallel slots × 262k context. None of these limit generation today, but each is a cheap fix or a trap for later.

Config & software changes (no hardware, no money)

Ranked by expected gain on epsilon. All of these need a container restart later. Test them on a spare port or while the model is idle; nothing here has been applied.

1 · "Fast" profile: one GPU, bigger batches tested: −20% TG

Measured 23 Sep: this exact profile gave 53.1 tok/s vs 66.1 for the deployed 2-GPU config, so it is not recommended as written. Most likely cause: the q8_0 KV cache (dequant cost in flash-attention). If you retry, keep f16 KV and change one knob at a time. The text below is the original first-pass reasoning, kept for the record.

The Q4_K_P model (17.9 GB), the FastMTP sidecar (0.9 GB) and the mmproj fit on one 3090. Earlier on this box, 64k context with q8 KV fit at 23.6 GB. Use -sm none (or --device CUDA0) so there's no cross-GPU hop and the MTP prefill penalty disappears:

CUDA_VISIBLE_DEVICES=0 GGML_CUDA_GRAPH_OPT=1 \
llama-server -m …Q4_K_P.gguf -ngl 999 -sm none -c 65536 -np 1 \
  -fa on -ub 1024 -b 2048 --jinja --tools all --mmproj …BF16.gguf \
  --spec-draft-model …FastMTP-32K.gguf --spec-draft-ngl 99 \
  --spec-type draft-mtp --spec-draft-n-max 2 --spec-draft-p-min 0

2 · Batch sizes for prefill +50–100% PP

Even if you stay on two GPUs: -ub 1024 -b 2048, or -ub 2048 -b 4096 if VRAM allows. The compute buffer grows by roughly 1–2 GB, and there is 4–5 GB free per card now. Large-batch prefill on a 3090 is compute-bound, so this is where the power limit (below) starts to matter.

3 · GPU clocks (live, no restart) +5–10% TG

# GPU0 values proven on this box in July (Gemma decode 145→158 tok/s)
sudo nvtool -i 0 --setmemoffset 1500 --setcoreoffset 200
sudo nvidia-smi -i 0 -pl 330        # helps prefill; TG is bandwidth-bound
# GPU1: only after its cooling is fixed; start at +800 mem (it hung with OC before)
# P2-state memory clock: driver ≥580 honours CUDA_DISABLE_PERF_BOOST=1 in the container env

4 · Speculative-decoding knobs ±5%

5 · KV cache & context avoid regressions

6 · Rebuild on a newer base enabler

7 · Minor host tweaks 1–3%

DFlash & other drafters

DrafterFor your model?Engine support3090 resultVerdict
FastMTP (current)Yes, HauhauCS sidecarPatched llama.cpp only~52 (2-GPU) · +36% vs embedded MTP, measured herekeep
DFlash2 (z-lab)Yes: z-lab/Qwen3.8-27B-DFlash2-GGUF (1.14 GB, τ≈5.3)llama.cpp master (#22105, #27342), ik_llama, vLLM, SGLang, ExLlamaV3 fork62–63 vs MTP 58–66 (llama.cpp) · 163 greedy/8k in the EXL3 fork, 25 @ 150ktest later
DFlash v1Qwen3.5/3.6-27B, Gemma-4-31B, Gemma-4-26B-A4Bsamesmaller gains on hybrid models (recurrent-state replay)superseded by v2
DSparkNo 27B drafter yet (Qwen3 4/8/14B only)llama.cpp #25173, ik_llama~1.2× over DFlash on the small modelswatch
Gemma-4-26B-A4B DFlashFor the Gemma servicellama.cpp masternot measuredGemma already has a strong MTP head (~217 tok/s)

Why DFlash doesn't win here yet: it drafts a whole block in one diffusion step, so acceptance length is higher (~5 vs ~2.7). But Qwen3.8's linear-attention layers must replay their recurrent state for every rejected draft token, which eats the gain in llama.cpp. Engines that handle this better (ExLlamaV3 fork, vLLM) show much larger numbers, though mostly on greedy, short-context benchmarks.

Other engines & community mods

OptionWhat it bringsReported on 3090sFit for epsilon
ik_llama.cppIQ4_KS/IQ_K quants (better quality per bit), -sm graph tensor-parallel, MTP for Qwen3.8 (#2369), DFlash, DSpark60–69 tok/s single 3090 (IQ4_KS + MTP)best drop-in to try Same GGUF workflow. -sm graph needs P2P to shine.
vLLM (image already on box)TP=2, AutoRound INT4 with a BF16 MTP head, DFlash, NCCL all-reduceMeasured here: 136 (1 GPU) · 112 (TP=2) via HyperQwentested, winner See HyperQwen. Gen2 x4 costs TP about 18%, as predicted.
ExLlamaV3 / TabbyAPIEXL3 4.0 bpw, qwen3_5 + MTP, TP, DFlash2 fork (r0b0tlab)163 tok/s greedy / 8k (self-reported)experiment Separate model format; collapses at long context.
llama.cpp -sm tensorMainline tensor parallelism (experimental)1.36–1.57× TG (2×4090, P2P) · VeroFess 2×3090 fork 64 tok/s, 1443 PPblocked Needs P2P plus f16 KV, and crashes with MTP until draft PR #27858 lands.
SGLangDFlash, fast scheduler15–18 tok/s (graphs disabled)skip CUDA-graph capture hangs on Ampere for GDN hybrids.
P2P driver hack (aikitoria/open-gpu-kernel-modules)GeForce PCIe peer-to-peer≈0.2% for -sm layer · +2–9% vLLM TP · +19–22% with DFlashblocked Needs ReBAR, both cards on CPU lanes (not the chipset), and replacing HiveOS's driver.
TurboQuant KV forks3–4-bit KV with small quality losslong-context memory, not speedonly if you need more than 262k context. Reports of slow MTP prefill.

Hardware options (ranked by value)

#ChangeCostWhat it unlocks
1Fix GPU1 cooling: check/replace the fan, fix HiveOS fan control, and repad the memory (GDDR6X on the back) with a backplate heatsink or fan$0–40Stops thermal throttling; makes a memory OC on GPU1 safe (+5–10% TG on that card)
2BIOS pass (one reboot): update from 0602 to latest; PCIe Gen3 instead of forced Gen2 (if no x1 risers); XMP / DDR4-2933; Above-4G + ReBAR$02× link bandwidth (GPU1 4 GB/s), +37% RAM bandwidth, prerequisite for P2P
3Board with CPU x8/x8 bifurcation (a Z490 that splits the CPU x16 into two x8 slots with 3/4-slot spacing), so both 3090s are off the chipset~$80–150 usedMakes P2P possible, and with it vLLM TP=2, ik -sm graph and llama.cpp -sm tensor. This is where 90–130 tok/s becomes realistic.
4NVLink bridge for the 3090s (3- or 4-slot; must match slot spacing on the new board)~$80–200~56 GB/s GPU-to-GPU; best-case tensor parallelism. Only worth it after #3.
5i5-11400F / i7-11700 (Rocket Lake, same socket)~$90–160PCIe 4.0 on CPU lanes (if the board supports it), AVX-512, DDR4-3200. Minor for fully-GPU inference.
62× 32 GB DDR4-3200 matched kit~$90Only for CPU/MoE offload of models larger than 48 GB. No gain for the current workload.

The honest ceiling: one 3090 generates at ≤ 936 GB/s ÷ ~17 GB of weights ≈ 55 tok/s before speculation. Spec decoding multiplies that by roughly the accepted draft length. Two 3090s only add speed with true tensor parallelism, and that needs a fast GPU-to-GPU path, which the chipset x4 slot can't provide. So the cheapest real gains are: one GPU per model, prefill batch tuning, clocks and cooling.

Live tests (23 Sep): core-clock cap & GPU0 memory offset

Both tests ran on the live server under real agent traffic, one setting at a time, in 3-minute phases with a baseline between each. Safety measures: automatic revert on exit, a detached watchdog that forced defaults after 15–20 min, and a dmesg Xid + health check every 10 s. Both reverted cleanly (offsets 0, clocks unlocked) with zero Xid errors.

A · Core-clock cap, both GPUs (nvidia-smi -lgc 210,<cap>)

PhaseGPU0 W / MHz / °CGPU1 W / MHz / °CTG tok/s*PP tok/s*
baseline (3 runs)218–227 / 1613–1642 / 62–64228–234 / 1590–1638 / 79–8225–35326–507
cap 1700205 / 1602 / 62209 / 1569 / 7749n/a
cap 1400185 / 1377 / 57187 / 1382 / 6832184

B · GPU0 memory offset (nvtool -i 0 --setmemoffset)

+0 · 9501 MHz
884
+500 · 9751 MHz
906
+1000 · 10001 MHz
930
+1500 · 10251 MHz
953
GB/s measured read bandwidth on GPU0: a 512 MB streaming-read CUDA probe (the same access pattern as weight streaming during decode), median of ~60 samples per step. Minimum and maximum stayed within ±0.5% once each step settled.

Recommended persistent settings: nvidia-smi -lgc 210,1700 on both cards, plus nvtool -i 0 --setmemoffset 1500 on GPU0 after a soak test. Leave GPU1's memory at stock until its cooling is fixed. Put them in a boot one-shot, and fix the stale 6-GPU arrays in HiveOS nvidia-oc.conf so HiveOS doesn't overwrite them.

Suggested order

P0
Now, zero-downtimeCheck GPU1's fan physically. Apply the GPU0 memory/core offset live (reversible with --setmemoffset 0). Fix the stale 6-GPU HiveOS OC/fan arrays so the offsets survive a reboot.
free
P1
Switch to HyperQwenOne click in Nova. Decide TP=2 (262k, 112 tok/s) vs GPU0-only (64k, 136 tok/s, GPU1 free). Check whether any client reads reasoning_content.
1.7–2.05× TG
P2
Side experiments on GPU1ik_llama.cpp with IQ4_KS + MTP. vLLM single-GPU with an AutoRound INT4 that keeps the MTP head. Optionally the DFlash2 drafter on a rebased llama.cpp. Keep whichever wins on your own benchmark.
maybe 60–120
P3
One rebootBIOS update, Gen3, XMP, Above-4G/ReBAR.
enabler
P4
If you want true 2-GPU speedx8/x8 board (+ NVLink), then vLLM TP=2 or ik -sm graph.
~$100–350

Sources