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.
-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.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.
| Config | Avg | code | explain | math | story | TTFT | Tokens/step | Power | VRAM |
|---|---|---|---|---|---|---|---|---|---|
| llama.cpp FastMTP, 2-GPU layer split (deployed) | 66.1 | 71.1 | 66.7 | 72.6 | 54.0 | 367 ms | ~2.0 | 245+249 W | 19.0+20.6 GB |
| llama.cpp 1-GPU, q8 KV, ub 1024 (my first-pass profile) | 53.1 | 57.8 | 53.0 | 58.9 | 43.0 | 237 ms | ~1.95 | 276 W | 21.9 GB |
| HyperQwen TP=2, MTP | 112.4 | 122.1 | 111.6 | 137.3 | 78.4 | 151 ms | 3.3–3.6 | 275+275 W | 23.2+23.2 GB |
| HyperQwen TP=2, DFlash2 | 110.9 | 118.9 | 106.8 | 148.6 | 69.4 | 165 ms | 3.9–4.4 | 274+269 W | 23.4+23.4 GB |
| HyperQwen GPU0 only, MTP | 135.6 | 148.4 | 136.5 | 162.9 | 94.7 | 114 ms | ~3.5 | 275 W | 22.6 GB |
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 calling | ✅ get_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.
| Part | Detail | Inference impact |
|---|---|---|
| CPU | Intel i5-10400F, 6C/12T, ~4.0 GHz all-core, no AVX-512 | Fine while the model is fully on GPU; weak for CPU/MoE offload |
| RAM | 4× 8 GB DDR4, two mismatched kits (CMW16GX4M2E3200C16 + M2Z2933C16), running at 2133 MT/s, so XMP is off | Only matters for offloaded layers or experts |
| Board | ASUS PRIME Z490-V, BIOS 0602 (Apr 2020) | Old BIOS; one CPU x16 slot, the second slot is chipset x4 |
| GPU0 | RTX 3090 · PCIe Gen2 x16 · PL 280/370 W · 67 °C, fan 95% | Healthy |
| GPU1 | RTX 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 / P2P | None (links inactive); topology PHB | No fast GPU-to-GPU path |
| Storage | Samsung 512 GB NVMe (93% full), Kingston 240 GB SATA, TEAM 128 GB SATA (OS) | Load time only; the full NVMe is a housekeeping issue |
| Software | HiveOS (Ubuntu 18.04, kernel 5.6, glibc 2.27), driver 580.159.04, docker; llama.cpp built on CUDA 12.6 with sm_86 | The old OS makes native builds painful; docker is the right approach |
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 |
|---|---|
| Generation | 51.4–55.5 tok/s (55 tok/s after a 122k-token prompt) |
| Prompt processing | 293–335 tok/s @ 1–2.6k tok · 641 tok/s @ 122k tok |
| Draft acceptance | 84–93%, mean accepted length 2.7–2.9 (max 3) |
| VRAM | GPU0 19.0 GB · GPU1 20.6 GB of 24 GB |
| Clocks under load | GPU0 ~1590–1755 MHz · GPU1 ~1455–1635 MHz · mem 9501 MHz both · 0 offsets |
| Throttle reasons | SW power cap active on both · SW thermal: GPU0 8.7 h, GPU1 3.6 h (cumulative) · HW thermal GPU1 24 s |
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.-sm layer this barely affects generation, but it does slow model loads, large prefills and any tensor-parallel option.-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%.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.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.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.
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
-np 1 when there's a single user: all KV goes to one stream and scheduling overhead drops. Keep 2–4 slots only if agents really call it concurrently.GGML_CUDA_GRAPH_OPT=1 enables concurrent CUDA streams, single-GPU only.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.
# 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
nvidia-oc.conf with the right 2-GPU arrays, or a systemd one-shot).nvtool --setcore 1700) to cut heat and power while memory stays high. Token generation doesn't use the extra core MHz.--spec-draft-n-max 2. Independent 3090 tests find n=2 beats n=3 for Qwen3.8 MTP (66.4 vs 63.3 tok/s). The August FastMTP test here used n=3, so an A/B on your real prompts is worth doing.--spec-draft-p-min 0.75 --spec-draft-n-max 6 reportedly gave +68% on a 3090 Ti. The gain comes from drafting deeper only when the drafter is confident.4df29be (Aug) on CUDA 12.6. Rebasing the FastMTP patch onto current master picks up DFlash2 (#27342), -sm tensor (#19378), and ongoing MTP + multi-GPU fixes. Build in the existing nvidia/cuda devel container with -DCMAKE_CUDA_ARCHITECTURES=86 (add -DGGML_CUDA_NCCL=ON only if you'll try tensor mode).ondemand → performance. It trims token-launch latency in the fully-offloaded, CUDA-graph path.qwen36-27b, qwen3vl-32b-heretic, qwen38-27b, dangling 7.5 GB images) cost load-time headroom and wear.| Drafter | For your model? | Engine support | 3090 result | Verdict |
|---|---|---|---|---|
| FastMTP (current) | Yes, HauhauCS sidecar | Patched llama.cpp only | ~52 (2-GPU) · +36% vs embedded MTP, measured here | keep |
| 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 fork | 62–63 vs MTP 58–66 (llama.cpp) · 163 greedy/8k in the EXL3 fork, 25 @ 150k | test later |
| DFlash v1 | Qwen3.5/3.6-27B, Gemma-4-31B, Gemma-4-26B-A4B | same | smaller gains on hybrid models (recurrent-state replay) | superseded by v2 |
| DSpark | No 27B drafter yet (Qwen3 4/8/14B only) | llama.cpp #25173, ik_llama | ~1.2× over DFlash on the small models | watch |
| Gemma-4-26B-A4B DFlash | For the Gemma service | llama.cpp master | not measured | Gemma 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.
| Option | What it brings | Reported on 3090s | Fit for epsilon |
|---|---|---|---|
| ik_llama.cpp | IQ4_KS/IQ_K quants (better quality per bit), -sm graph tensor-parallel, MTP for Qwen3.8 (#2369), DFlash, DSpark | 60–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-reduce | Measured here: 136 (1 GPU) · 112 (TP=2) via HyperQwen | tested, winner See HyperQwen. Gen2 x4 costs TP about 18%, as predicted. |
| ExLlamaV3 / TabbyAPI | EXL3 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 tensor | Mainline tensor parallelism (experimental) | 1.36–1.57× TG (2×4090, P2P) · VeroFess 2×3090 fork 64 tok/s, 1443 PP | blocked Needs P2P plus f16 KV, and crashes with MTP until draft PR #27858 lands. |
| SGLang | DFlash, fast scheduler | 15–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 DFlash | blocked Needs ReBAR, both cards on CPU lanes (not the chipset), and replacing HiveOS's driver. |
| TurboQuant KV forks | 3–4-bit KV with small quality loss | long-context memory, not speed | only if you need more than 262k context. Reports of slow MTP prefill. |
| # | Change | Cost | What it unlocks |
|---|---|---|---|
| 1 | Fix 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–40 | Stops thermal throttling; makes a memory OC on GPU1 safe (+5–10% TG on that card) |
| 2 | BIOS pass (one reboot): update from 0602 to latest; PCIe Gen3 instead of forced Gen2 (if no x1 risers); XMP / DDR4-2933; Above-4G + ReBAR | $0 | 2× link bandwidth (GPU1 4 GB/s), +37% RAM bandwidth, prerequisite for P2P |
| 3 | Board 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 used | Makes 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. |
| 4 | NVLink 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. |
| 5 | i5-11400F / i7-11700 (Rocket Lake, same socket) | ~$90–160 | PCIe 4.0 on CPU lanes (if the board supports it), AVX-512, DDR4-3200. Minor for fully-GPU inference. |
| 6 | 2× 32 GB DDR4-3200 matched kit | ~$90 | Only 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.
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.
nvidia-smi -lgc 210,<cap>)| Phase | GPU0 W / MHz / °C | GPU1 W / MHz / °C | TG tok/s* | PP tok/s* |
|---|---|---|---|---|
| baseline (3 runs) | 218–227 / 1613–1642 / 62–64 | 228–234 / 1590–1638 / 79–82 | 25–35 | 326–507 |
| cap 1700 | 205 / 1602 / 62 | 209 / 1569 / 77 | 49 | n/a |
| cap 1400 | 185 / 1377 / 57 | 187 / 1382 / 68 | 32 | 184 |
nvtool -i 0 --setmemoffset)nvtool.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.
--setmemoffset 0). Fix the stale 6-GPU HiveOS OC/fan arrays so the offsets survive a reboot.reasoning_content.-sm graph.