Qwen 3.8 27B on 2 RTX 3090s + 128GB of RAM: QEngine, ULTRAMAX, vLLM or llama.cpp?
QEngine is the best overall choice based on our tests.
QEngine
- ✓ Decoding 142 → 138 tok/s from 2K to 128K: almost no loss
- ✓ 32 slots configured, 262K context
- ✓ 0.24 s before the 1st token on a cached prefix at 32K
- ✓ PolyForm Noncommercial license, INT4 quality not measured
ULTRAMAX (sous vLLM)
- ✓ 106 / 92 / 73 tok/s at 2K / 32K / 128K in FP8
- ✓ 262K context, DFlash2 drafter
- ✓ Pinned non-upstream stack, vLLM updates refused
- ✓ Known upstream bug: workers can die under agent load
TurboQuant (sous vLLM)
- ✓ Upstream, zero patches, a single flag (turboquant_k8v4)
- ✓ 70 / 53 / 52 tok/s median, 262K and vision
- ✓ Throughput highly scattered from run to run (2 runs per point)
- ✓ The KV format decides everything: 8.6 tok/s at 128K in bf16
llama.cpp
- ✓ 1.2 to 1.4 s before the 1st token on a cached prefix at 128K
- ✓ Simple engine, no traffic between cards per token
- ✓ 40.7 / 36.4 / 28.2 tok/s in Q8_K_XL, the slowest at decoding
- ✓ The balanced --tensor-split trap: silent OOM
Also available inEnglishFrench(original)
Translated with AI assistance and reviewed by the author.
The result fits in one sentence: QEngine decodes 1.3 to 1.9 times faster than ULTRAMAX and barely slows down as the context grows, but on an already-cached context, QEngine (0.24 s at 32K) and llama.cpp (1.3 s at 128K) respond almost instantly, whereas ULTRAMAX takes 4 to 7 s. The right choice therefore depends on what you do with the model, not just its advertised speed.
In 30 seconds
| You want… | Take… | Why | Good to know |
|---|---|---|---|
| Fast decoding, even on a very long context, or serving several agents | QEngine | 138 tok/s at 128K, 32 slots, 0.24 s on a cached prefix at 32K | non-commercial license, INT4 quality not measured by me, warm TTFT not measured at 128K |
| An agent that replays a long context, with a simple engine | llama.cpp | 1.3 s before the first token on a cached prefix at 128K | 3.5 to 5 times slower at decoding |
| Stay upstream, no patches | vLLM 0.30 + TurboQuant | 70 / 53 / 52 tok/s median, 262K, a single flag | throughput highly scattered from run to run, outputs to re-verify, warm TTFT not measured |
| Maximum throughput in FP8 | ULTRAMAX | 106 / 92 / 73 tok/s at 2K / 32K / 128K | pinned non-upstream stack, known upstream bug |
On my setup, QEngine is the default engine for the 27B (version 1.3.5.4).
Why this model changes the comparison
Qwen 3.8 27B is a dense model: its 27 billion parameters are read for every token. Unlike QFN, there are no experts to offload to RAM, so RAM matters little here. Everything plays out in VRAM, on 2 levers: weight quantization (what has to be read for every token) and KV cache quantization (which grows with the context).
The weights I served: 31.5 GB in Q8_K_XL (llama.cpp), about 27 GB in FP8 (vLLM, ULTRAMAX), 19.5 GB in INT4 (QEngine).
A few terms to know for what follows:
- tok/s: tokens generated per second (decoding).
- Prefill: how fast the engine reads your prompt. TTFT: the time before the first token. When cold, the context has never been seen. When warm, it is already cached: the two are completely different.
- KV cache: the attention memory. Its format (bf16, fp8, int8, TurboQuant) weighs very heavily at long context.
- Drafter / MTP / DFlash2: a small model proposes several tokens ahead, the big one validates them. This speeds things up without changing output quality. Acceptance is the average number of tokens obtained per verification.
- P2P: direct transfer from one card to the other, without going through RAM. My cards have none.
My machine
2 RTX 3090s with 24 GB each (a 300 W Ti and a 275 W 3090), an AMD EPYC 7401P with 24 cores, 128 GB of DDR4, PCIe 3.0 x16 on both cards, no NVLink and no P2P (my GIGABYTE MZ31-AR0 motherboard does not allow it). Two monitors are plugged into the cards, which eats up some VRAM. With 48 GB, I can only run one engine at a time.
Without P2P, vLLM in tensor parallel sends every exchange between cards through the machine's RAM, about 128 collectives per token. llama.cpp, with layer-wise splitting, has no traffic between cards per token. That is one of the reasons it remains competitive despite a more rustic engine.
The 4 engines in one table
| QEngine | ULTRAMAX | vLLM 0.30 stock | llama.cpp | |
|---|---|---|---|---|
| Version tested | 1.3.2 (A/B), 1.3.5.4 (daily use) | vLLM 0.29 + patched FA2 fp8-KV kernel | 0.30 | master build (11141) |
| Weights | INT4 W4A16, 19.5 GB | FP8 W8A16, ~27 GB | FP8 W8A16, ~27 GB | Q8_K_XL ≈ 8.5 bit, 31.5 GB |
| KV cache | FP8 | FP8 | TurboQuant k8v4 | q8_0 |
| Drafter | Fused MTP | DFlash2 W4A16 (n=7) | MTP | Built-in MTP (n=2) |
| Max context | 262K | 262K | 262K | 262K |
| Slots configured | 32 | 1 | — | 1 |
| Cold start | 54 s (1.3.2), 45 s (1.3.5.4) | 153 s (5 to 8 min the very first time) | — | — |
| License | PolyForm Noncommercial | Apache-2.0 (vLLM) + third-party binary artifact | Apache-2.0 | MIT |
QEngine vs ULTRAMAX: same session, same scripts
This is the only strictly controlled comparison: same evening, same prompts (code corpus of 2,132 / 32,132 / 128,132 tokens), 5 runs per prompt, median. Each prompt is preceded by a unique prefix to guarantee a cold prefill, and decoding is measured afterwards, so at the true context length.
| Context | QEngine 1.3.2 | ULTRAMAX | Gap | |
|---|---|---|---|---|
| Decoding (tok/s) | 2K | 142.2 | 106.5 | +33.5% |
| 32K | 136.9 | 92.5 | +48.0% | |
| 128K | 137.9 | 73.0 | +88.8% | |
| Prefill (tok/s) | 2K | 2,131 | 1,463 | +45.6% |
| 32K | 2,226 | 1,399 | +59.1% | |
| 128K | 1,980 | 1,146 | +72.8% | |
| Wait before the 1st reply at 128K | 64.7 s | 111.9 s | −47.1 s | |
| Cold start | 54 s | 153 s |
Gaps are expressed from QEngine's point of view. Medians come from 5 runs, except QEngine at 2K (4 runs, see "Sources and method").
Decoding. This is where QEngine stands out: it loses 3% between 2K and 128K (142.2 → 137.9), whereas ULTRAMAX loses 31% (106.5 → 73.0). The gap therefore widens with context. My hypotheses: INT4 weights (7.2 GB per card versus 13.5 GB, so about 2 times less to read per token), MTP fused into the engine, and a more generous KV pool. I did not isolate any of these causes.

Context and slots. QEngine's KV pool is 343,040 tokens versus 267,493 for ULTRAMAX, and it is shared by 32 slots. Note: ULTRAMAX is configured with 1 slot. I did not run a concurrent load test, so "32 slots" is a configured capacity, not a measured throughput.

The license. QEngine is under PolyForm Noncommercial 1.0.0: read it closely if your use goes beyond personal use.
The other 2, briefly
llama.cpp. September figures, with the final setup (Q8_K_XL, MTP n=2): 40.7 / 36.4 / 28.2 tok/s at 2K / 32K / 128K. It is the slowest of the bunch, but it is the only one to win on an already-cached prefix: 1.2 to 1.4 s before the first token at 128K, versus 6.9 s for ULTRAMAX, because it only re-evaluates 4 tokens. Cold, it is a tie: 119 s for llama.cpp (1,074 tok/s), 120 s for ULTRAMAX (1,063 tok/s) during the September campaign. For an agent that continues the same conversation, this is a criterion that matters a lot. QEngine seems to do just as well, but I only measured it up to 32K (see below).
vLLM 0.30 + TurboQuant. --kv-cache-dtype turboquant_k8v4 gives 70.5 / 52.6 / 52.0 tok/s median at 2K / 32K / 128K, with 262K, vision and 344,926 tokens of KV capacity. Zero patches, a single flag. The KV cache weighs enormously: the same vLLM with a bf16 KV drops to 8.6 tok/s at 128K (and caps out at 131K), and to 5.9 with an int8 KV. Beware of these medians: they come from 2 runs per point, very scattered (36.9 and 68.3 tok/s at 32K; 37.4 and 66.6 at 128K). The outputs of these runs also show repetition loops from the very first tokens, which can inflate a drafter's throughput; I did not re-verify them. I did not measure its warm TTFT.
Warm TTFT: what I have, and what is missing
Warm TTFT is the time before the first token when the prefix is already cached: the case of an agent continuing a conversation.
| Engine | 2K | 32K | 128K |
|---|---|---|---|
| QEngine 1.3.2 | 0.12 s | 0.24 s | not measured |
| ULTRAMAX | 2.0 s* | 4.4 s | 6.9 s |
| llama.cpp | not measured | not measured | 1.2 to 1.4 s |
| vLLM + TurboQuant | not measured | not measured | not measured |
*At 2K, the displayed throughput (1,050 tok/s) is that of a cold prefill: it is not a true warm TTFT.
QEngine's figures come from my first pass of the A/B, with the same prompt on every run: I had discarded it for prefill, but it is valid for the cache. At 2K, 4 runs out of 5 are cached (the first is cold, at 1.0 s); at 32K, all 5 are. The other columns come from September, so from a different session.
At 32K, QEngine is about 18 times faster than ULTRAMAX on a cached prefix (0.24 s versus 4.4 s). QEngine's slope (0.12 → 0.24 s between 2K and 32K) is encouraging, but two points do not make a measurement at 128K: I therefore cannot say whether it beats llama.cpp (1.2 to 1.4 s) at very long context.
The comparison is not at equal quantization
QEngine runs in INT4: that is part of its speed. The right comparison point on the llama.cpp side is therefore Q4_K_XL (4.5 bit), measured in September: 60.7 / 52.3 / 34.7 tok/s, i.e. +49% at 2K compared to Q8. QEngine remains far above (142 / 137 / 138), but it is not the same session, so take the gap as an order of magnitude.
What each setting costs, with measured figures when I have them and published ones otherwise:
- Speculative decoding (MTP, DFlash2): no effect on output quality, the drafter proposes and the target model validates. It changes the speed, never the result.
- 8-bit KV (q8_0, fp8, int8): almost identical to bf16 (perplexity 7.14 versus 7.15). The output is not bit-for-bit reproducible, but the quality holds.
- TurboQuant k8v4: 2 times less memory for +1.17% perplexity according to the vLLM blog, and 0.973 versus 1.000 on MRCR-16 according to an independent reproduction.
- Symmetric 4-bit KV: catastrophe, perplexity of 1,573 versus 7.15. Avoid at all costs.
- 4-bit weights: real but gentle degradation, highly task-dependent. On my 5-task probe, Q4_K_XL scores 4/5 versus 5/5 for Q8. This probe is too short to settle the question.
The hole in my comparison: I did not measure the quality of QEngine's INT4. It passes my functional tests without problems (see below), but I have no bench comparing its fidelity to FP8 or Q8. If your task is sensitive to precision (long reasoning, extraction), test it on your own cases before relying on it.
QEngine 1.3.5.4: the engine I actually use
The A/B above is on 1.3.2. Day to day, I moved to 1.3.5.4, measured in a separate session, with a different harness (bench.sh): this is not an A/B, so take these figures as an order of magnitude.
| Measure (1.3.5.4) | Value |
|---|---|
| Narrative / code decoding, short context | 191.1 / 265.2 tok/s |
| Prefill at 10K / 90K | 2,311 / 2,124 tok/s |
| Drafter acceptance (MTP) | 6.05 |
| VRAM of the most loaded card | 21,690 MiB |
| Functional / vision tests | 10/10 · 4/4 |
| Long-context stress tests | 8/8, up to 240,659 tokens (91% of 262K) |
| Free VRAM per card at 240K | 1,117 MiB |
The gain over 1.3.2 (142 tok/s) cannot be compared cleanly: version, session and harness all changed at once, and it is not the same type of prompt (narrative and code here, a code corpus there). Acceptance also depends on the generated content.

Pitfalls encountered
The prefix cache distorts any prefill. My first pass with an identical prompt on every run displayed "18,000 tok/s" of prefill: it was a cache hit, not a prefill. You need a unique prefix in front of each prompt, and the script checks that no suspicious hit slips through (0 across the 30 retained runs).
The KV cache format decides long context. At 128K, with the same engine: bf16 8.6 tok/s, int8 5.9, TurboQuant 52. "It fits in 262K" does not mean "it is usable".
A p-min that costs a lot. My old llama.cpp unit had MTP at n=6 and --spec-draft-p-min 0.75. Switching to n=2 without p-min gives +10% at 2K in median (38.5 → 42.4), and p-min alone costs 17% at 2K and 13% at 32K. Note: 3 runs per setting and a scatter of ±10% from run to run, so I consider it probable rather than demonstrated. The unit also had no explicit sampler, so the server was running with llama.cpp defaults, outside what the model card recommends.
GPU split on llama.cpp. A balanced --tensor-split causes a silent OOM: the server starts anyway, in fallback (retrying without pipeline parallelism). You have to unbalance it (55/45) to leave room for the drafter, and check the logs after every change.
ULTRAMAX is fragile. A mixed CUDA toolchain (nvcc, headers and nvidia-nvvm at different versions) gives successive errors up to a ptxas: Unsupported .version. The artifact's ABI check also refuses any vLLM update, and there is an upstream bug (club-3090#50021): with a drafter, workers can die under sustained agent traffic. I installed it without Docker, for lack of the NVIDIA Container Toolkit.
vLLM does not allocate if VRAM is occupied. An old orphaned process is enough to make startup fail with Engine core initialization failed. Stop everything cleanly before switching engines.
My QEngine settings
To copy as-is if you start from the 27B preset:
QE_MAX_SEQS=32
QE_KV_ARCHIVE_GB=24
QE_RESERVE_GB=2,2
QE_PRESENCE_PENALTY=0.3
QE_FREQUENCY_PENALTY=0
frequency_penalty at 0.3 caused lexical drift, so I leave it at 0. In 1.3.5.4, reasoning budgets are native: 8,192 tokens in low, 16,384 in medium, 32,768 in xhigh. I also keep the dashboard and metrics on loopback only.
If you stay on llama.cpp, the base that gave me the best result: -c 262144 -fa on -np 1 -ngl 999 -b 4096 -ub 512 --split-mode layer --tensor-split 55,45 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 2, plus the model card's sampler (--temp 1.0 --top-p 0.95 --top-k 20 --min-p 0.0).
Sources and method
Everything quantified comes from my bench, except the published quality figures (KV format perplexity, TurboQuant), cited with their source.
- September campaign (llama.cpp, vLLM, ULTRAMAX): 1 warm-up then 2 runs, median, cold prefill.
- October 31 A/B (QEngine 1.3.2 vs ULTRAMAX):
cold_prefill.pyharness, 5 runs per prompt, 256 tokens generated withignore_eos. One QEngine run at 2K is excluded (TTFT of 20 s after a 128K request interrupted by akill), hence n=4. - QEngine 1.3.5.4: club-3090's
bench.sh, 3 warm-ups then N measurements, a single session. - Sampling identical everywhere:
temperature 1.0, top_p 0.95, top_k 20.reasoning_effortwas not identical (low for ULTRAMAX, xhigh by default for QEngine), with no effect on tok/s since the output is forced to 256 tokens.
Comparing across campaigns calls for caution. My A/B reproduces September at +9% for ULTRAMAX decoding at 2K, but at +19% at 32K (92.5 versus 78.0) and +16% at 128K (73.0 versus 63.2). Probable explanation: kernel 6.8.0-117 and lighter monitors, which I did not isolate. September's figures for llama.cpp and vLLM are therefore probably a bit low compared with today's. Only the QEngine / ULTRAMAX gap is strictly comparable.
What I did not measure: the quality of QEngine's INT4 (neither GSM8K nor IFEval), the warm TTFT of QEngine at 128K and of vLLM + TurboQuant at all lengths, concurrent load (c=8/16/32), stability beyond an hour, and the "with gpu-unlock" arm of the A/B.
Published sources: club-3090 (2×3090 reference configs), vLLM blog on TurboQuant, MRCR reproduction of TurboQuant, kvbits (key/value asymmetry of the KV), gguf-bench (quality of GGUF quantizations). A throughput figure without quantization or context length is worthless: always compare at equal weights and context.
and download the source files
👤 Which product for whom?
QEngine
138 tok/s at 128K, i.e. 1.9 times ULTRAMAX, with a loss of only 3% between 2K and 128K. Test the INT4 on your own cases if precision matters.
llama.cpp (or QEngine)
1.2 to 1.4 s before the first token at 128K on a cached prefix, with a simple engine. QEngine seems just as good (0.24 s at 32K) but is not measured at 128K.
QEngine
32 slots configured and a KV pool of 343,040 tokens. This is a configured capacity: I did not run a concurrent load test.
TurboQuant
A single flag, 262K and vision, about 52 tok/s at 128K. Scattered throughput and outputs to re-verify: validate it on your own setup.
ULTRAMAX
106 / 92 / 73 tok/s at 2K / 32K / 128K, but a pinned non-upstream stack and a known upstream bug under sustained agent traffic.
llama.cpp or vLLM
MIT and Apache-2.0 licenses. QEngine is under PolyForm Noncommercial: read it closely beyond personal use.
❤️ Like this comparison? Leave a star to support fabz !
