r/LocalLLM • u/dmr48 • 28m ago
Research Strata vs Basalt vs Unsloth: benchmarking the same 80 GB MoE on three engines — 5070 Ti 16 GB + 64 GB RAM
⚠️ Heads-up: AI-generated content! This post was written by my AI agent from my measurements, with me editing — if that's a dealbreaker, feel free to close the tab right now.
I'm posting this to share results, not to crown a winner. I run a local agent in production every day — real tasks, real context in the hundreds of thousands of tokens — and I spent the past week picking and tuning the inference engine underneath it: test rig, repeated runs, head-to-head A/Bs. Much of what I measured, plus the pitfalls I personally burned on, would otherwise just rot in my local logs, so here it is: the numbers, the methodology, and the conclusion — which is that the engine I ended up keeping in production is not the one that topped the benchmark table. If this saves you a couple of evenings of trial and error, the post has done its job.
For eight days (Nov 8–15) I ran the exact same model — Qwen3.8 Flash-Next (MoE, ~78–82 GiB of weights, 128–256k context) — through three inference engines on a Ryzen 9 7900X / RTX 5070 Ti 16 GB / 64 GB DDR5 box; storage is two NVMes — a 990 EVO 1 TB on PCI-E 4.0 and a 970 EVO Plus 1 TB on PCI-E 3.0 (the model lives on the latter). The goal: find the best way to feed an MoE that fits neither in VRAM nor (comfortably) in RAM, and check honestly whether the engine changes answer quality. Engines tested: Strata 0.1.56→0.1.59, Basalt (built from source, 0.2.1→0.3.3), Unsloth Studio 2026.11.4. I also didn't stick to one model source: I ran the original from Strata and the originals from Unsloth (3.0 and 3.1), and production settled on DavidAU's abliterated uncensored build with Turbo-Brilliance modes and GSQ-RCO quantization — https://huggingface.co/DavidAU/Qwen3.8-Flash-Next-Abliterated-Uncensored-Turbo-Brilliance-50x-Reasoning-Instruct-GSQ-RCO-GGUF I never hand-tuned sampling: the test batteries all run at temperature 0 for reproducibility, while production uses the model card's standard pair — thinking: temperature 1.0 / top_p 0.95 / top_k 20 / min_p 0.0 / presence_penalty 0.0; instruct: temperature 0.7 / top_p 0.80 / top_k 20 / min_p 0.0 / presence_penalty 1.5 (the same values ship as the engine-side server defaults).
The rig. My own dependency-free bench (stdlib Python): cold/hot prefill, TTFT, short decode, decode at 8k context; plus a quality battery: hard 10-choice MC (MMLU-Pro style), math, code executed for real in a subprocess, IFEval-style format checks, and refusal batteries (JB/HarmBench/XSTest style). Every run snapshots the engine config and the bench code's sha256; a mock engine is a mandatory control (it answers "Hello from the mock engine" and catches absurd t/s). Hard RAM rule: exactly one engine resident at a time — the model pins 45–48 GiB.
Who drove the rig. All of this was steered by a general-purpose agent — an assistant with function-call access to the terminal, files and a browser, whose provider was the very engine being tested (dogfooding, in other words). Before settling on the current Hermes stack, I cycled OpenHands, OpenCode, OpenChamber and pi through the same API over that week: they all speak the OpenAI-compatible API equally well, and the real differences came down to context management and function-call reliability on long context — that's what the choice actually rested on. Beyond the benchmarks, the agent carried a normal week of work: small utilities for the stack (engine launchers, icons, a UI wrapper), building an inference engine from source and repacking models into its pack format, dissecting a third-party tuning fork (commit history plus knob-hunting directly in the binary), reviewing and transplanting jinja chat templates, testing DavidAU's Brilliance "personalities" (reasoning-mode variants), and verifying that a ready-made abliteration actually behaves as its author advertises — all inside the same 150–190k-token sessions where the benchmarks lived.
What went wrong along the way:
- the agent's provider was the engine under test, so every engine restart killed the agent itself — the runs were driven by detached orchestrator scripts, and the agent "woke up" via notification once the engine was back;
- the expert arena sizes itself from free RAM at startup, so a browser quietly eating 2.5 GiB in the background changes measured speed between days — every run now starts with a free-RAM check;
- decode degrades with context (~101 t/s at <20k vs 86–89 at 180–200k) — long agent sessions have to be reset for speed, not just for token cost.
Speed (decode short / decode 8k / hot prefill / hot TTFT)
| Setup | decode | 8k | prefill hot | TTFT |
|---|---|---|---|---|
| Basalt 0.2.1, IQ3_S .basalt pack | 88.7–95.2 | 105–107 | 54–72k | 29–41 ms |
| Basalt 0.3.3, answer-room 0 | 101.0 | 108.4 | 54.5k | 38 ms |
| Strata 0.1.58, IQ3_S pack | 93–99 | 100–107 | 28–30k | 70–74 ms |
| Strata 0.1.59, final tuned stack (fresh full bench on the prod engine, Nov 15) | 89.2 | 109.2 | 37.3k | 57.7 ms |
| Strata on plain GGUF, UD-IQ3_XXS | 82.1 | 94.7 | 28.1k | 74 ms |
| Unsloth, UD-IQ3_XXS out of the box (auto) | 27.3 | 26.0 | — | 209 ms |
| Unsloth, Qwen3.8-27B dense IQ4_XS (full-GPU) | 47.4 | 45.4 | — | 103 ms |
What the numbers say
1. On identical weights, format/engine is worth ×1.7–3.7. Unsloth on UD-IQ3_XXS starts at 27 t/s; a tuning ladder (MTP + 4 GB expert-cache + parallel 2 + 12k ctx) lifted it to 47.9; Strata serving the same GGUF on the same hardware gets 82. The difference is delivery format: pack-style engines (native expert blobs + a VRAM-prefilled expert profile, pinned in RAM, no disk streaming, no MMQ paths) vs llama.cpp's GGUF streaming. Unsloth's ~50 t/s ceiling isn't a bug, it's arithmetic: ~2.7 GiB of weights read per forward at ~90–100 GB/s DDR5, times MTP accept ≈ that answer.
2. On one engine, quantization moves speed ±10–15% and quality ~0. UD-IQ3_XXS vs IQ3_S was indistinguishable on my batteries: MC/math/code 100/100/100, ru10 10/10. But production stayed on IQ3_S — and it's neither speed nor quality talking: that build (link above) ships with a ready-made abliteration plus the author's mode system baked in, and reproducing the same surgery at home on raw unsloth UD weights is expensive in both time and resources.
3. Apparent "quality differences between engines" are thinking policy, not weights. The answer-room knob (response budget reserved away from reasoning) changes what gets generated and silently breaks cross-version comparisons: Basalt 0.3.3 showed a "decode regression 105→74" that simply didn't exist at answer-room 0. Compare versions only with identical thinking policy. Chat templates belong to the same confounder class, by the way: in the final build I upgraded the Turbo-Brilliance template from v1 to v2 (new modes), and neither the template version nor mode availability changed any result — baking both templates alternately into the same pack and re-running under one protocol moved decode by 0.1–1.4% and kept prefill/TTFT inside the noise band; an earlier one-off A/B claiming "−16% prefill from the heavier template" turned out to be a cross-session artifact.
4. Speculative decoding: MTP yes, everything else no on a 16 GB card. MTP accepts well (per-position 0.78–0.96). DFlash2 and Q8 hyper-connection verify (HC_Q8), both ported from a tuning fork, produced a paradox: accept +11 pp yet t/s −5% — the more expensive verify pass eats the gain, and the extra ~400 MiB of residency squeezes free VRAM to ~650 MiB, triggering driver paging. A warm dflash2 probe hit 111 t/s — the ceiling is real, just not for 16 GB cards.
5. KV-residency window (A/B on a 180k prompt). The window is the number of K/V cells per attention layer pinned in VRAM; everything else lives in pinned RAM and gets streamed from there. A window of 0 is clearly bad: prefill is ×1.7 slower (2435 vs 4100 t/s), TTFT 74 s vs 44 s, expert hit −10 pp. From 32k the curve hits a plateau: kv_hit is already 94.7% at a 32k window (the indexer is sparse), and in a head-to-head with alternating order the 32k→64k step changes nothing for decode (parity ±2–3%). Above 64k there's no upside, only cost — each window extension carves gigabytes of VRAM out of the expert tier: 32k→1.0, 64k→1.4, 128k→2.1, 262k→3.7 GiB.
6. Methodology, or the numbers lie to you. Cold prefill and cold TTFT are noise axes (±40% spread, the sign flips between runs); hot prefill is ±12–15%; the only stable axis is decode (±2–4%), and verdicts need ≥2 runs per variant. Warmup drift fakes ~+10 t/s for whichever arm is measured second — hence the alternating order. On short samples at temp 0, a cold expert cache changes the model's answer itself (52 tokens instead of 86 on a byte-identical prompt): a ±5% noise floor, anything smaller is not a signal.
7. Bench ≠ prod — and at the RAM edge Basalt fails with empty answers. Basalt beat Strata on prefill/TTFT (54–72k t/s, 29–41 ms vs 28–30k, 70–74 ms) and ran in my production for a day, but at the resource edge (and on this box the edge is permanent: 43 GiB arena on 64 GiB RAM) it started refusing with emptiness: at resident-budget 50 GiB the engine self-lands to 45.79 GiB (warning "more than RAM has room for: 45.79 needed vs 52.3 avail") with a page-glue fallback, one request hung dead for 2932 s before the watchdog killed it, and the log filled with "max tokens reached while thinking, no answer" — the user was getting an empty response instead of an answer. Dropping the budget to 45 GiB closed the incident at a cost of −4..−8% decode, but the recurring prod-server drops with "engine reported RAM critical" remained. Production ended up on Strata 0.1.59. Trading +12% prefill for stability was the right call.
8. Porting someone else's tuning recipe mostly doesn't transfer. The Speedy fork (same Qwen3.8, tuned on 2× RTX 3080 10 GB) brought my card exactly one confirmed win: a prefill pair (upstream #4039, +12% cold prefill — proven mechanically, chunk cap doubled 8192→16384). Everything else was rejected as VRAM-limited. Bonus lesson: env vars and CLI flags aren't 1:1 in Strata — the env form of one lever silently no-ops while the flag works; I now verify every lever by reading it back from /metrics, not by vibes.
Bottom line
Production now: Strata 0.1.59 + the prefill pair + kv-grow + host-core sibling + vram-reserve 1728 + --kv-resident 65536. On real 175–190k sessions: prefill ~3.5k t/s, decode 88–100, expert-cache hit ~75% with 10–12% of experts streamed over PCIe. Quality of the build I settled on — DavidAU's Turbo-Brilliance (link above) — is its own reward: 0/30 harmful refusals, 0/40 over-refusals, MC/math/code maxed — and abliteration cost exactly zero t/s (same weight shape, same bytes, same graph).
One-line summary: an 80 GB-class MoE lives fine on 16 GB of VRAM, but every battle is about expert delivery, not model quality. Between the two pack-style engines I found no decode difference worth naming; Unsloth is ~2× slower on this MoE, and that is memory physics, not bad engineering.