Repository navigation
[Benchmark] RTX 4090 48 GB + Qwen3.8-Flash-Next IQ3_S: long-output decode 158-263 tok/s, driven by MTP acceptance #4768
Description
Problem
tools/strata_bench.py never exercises long generation. The caps are 100 new tokens for short chat, 24 for the 4K/16K/32K prompt tests and 64 per agent turn; --long only makes the prompt longer (32K). Most of the work people do with this model — writing a module, emitting a large JSON document, reasoning for thousands of tokens — happens above those caps, and the decode speed there is not the same as in short chat.
I added a small long-output bench (four cases, 1.9K–7K generated tokens, with a check where the answer is verifiable) and ran it 3× per case against two launch configurations on one RTX 4090 48 GB. The result is that long-output decode ranges from 158 to 263 tok/s and follows MTP draft acceptance almost exactly, while the same server reports 165–168 tok/s for 100-token chat.
Hardware and software
- GPU: NVIDIA GeForce RTX 4090, 49,140 MiB VRAM, driver 595.91.07, PCIe gen4 x16, power limit 450 W
- CPU: AMD EPYC 9B14, 96 cores / 192 threads; RAM 282.8 GiB; NVMe storage
- OS: Linux 7.0.0-42-generic; CUDA 13.2 (driver)
- Strata Terminal 0.2.7 (build 0.1.53), engine 0.1.52, Linux CUDA build
- Model: Qwen3.8-Flash-Next, ISTA-DASLab GSQ-RCO IQ3_S (two GGUF shards, 84.5 GB), MTP draft layer
- Context 131,072 tokens, KV int8,
kv-resident 32768, temperature 0 throughout
Configurations
Both were launched from the release Terminal against the same downloaded model, on the same machine, within the same hour. Paths replaced with <data-dir> / <feed-dir>.
A — stock launch (what strata launch --family qwen --model iq3_s picked by itself):
strata launch --family qwen --model iq3_s --context 128k --vision gpu --yes --no-tuiEngine reported: parallel_slots=4, spec=6, mtp_max=4, spec_min_p=0.50, pcie_frac=0.55, prefill_cap=8192, expert cache 18,615 slots / 36,305 MiB (35.45 GiB), 435 MiB VRAM free.
B — tuned launch (text only):
strata launch --family qwen --model iq3_s --context 128k \
--vision off --parallel 1 \
--set spec=6 --set spec-min-p=0.70 --set pcie-frac=0.35 \
--set vram-reserve-mib=1024 --set prefill=auto:32768 \
--set STRATA_HC_Q8=1 \
--yes --no-tuiEngine reported: parallel_slots=1, spec=8, mtp_max=6, spec_min_p=0.70, pcie_frac=0.35, prefill_cap=32768, expert cache 20,936 slots / 40,853 MiB (39.90 GiB), 746 MiB VRAM free.
Why these settings, following the 48 GB reports in this repo: turning vision off frees the vision reader's VRAM for the expert cache (I did not measure the split); one parallel slot avoids the batch slots the stock launch reserved (4 slots, ~2.9 GiB) and keeps VRAM headroom above the low-VRAM warning; a higher reserve (1024 MiB) leaves room for the ring buffer; a 32K prefill cap removes the 8K chunking seen at the start of long prompts; the deeper draft (spec=6, engine reports mtp_max=6) and a stricter spec-min-p were chosen to push draft acceptance. STRATA_HC_Q8=1 was enabled as an opt-in; I did not isolate its effect. B has no image input. For vision, use A.
Standard suite (python tools/strata_bench.py --long, 3 runs, medians with ranges)
| Test | A decode tok/s | B decode tok/s | A prompt tok/s | B prompt tok/s |
|---|---|---|---|---|
| short chat, 100 tok | 164.9 (124.5–207.7) | 168.3 (133.7–247.9) | not measured | not measured |
| 4K prompt, 24 tok | not measured | not measured | 3234.3 (3210.8–3290.2) | 3130.3 (3123.5–3175.8) |
| 16K prompt, 24 tok | not measured | not measured | 4372.1 (4345.9–4461.9) | 4202.1 (4160.4–4268.5) |
| 32K prompt, 24 tok | not measured | not measured | 4510.3 (4410.3–4540.9) | 4439.4 (4336.7–4466.9) |
| agent turn, 64 tok | 166.1 (150.9–181.8) | 164.5 (137.7–191.9) | — | — |
Chat +2.1 %, agent −1.0 %, 4K prompt −3.2 %, 16K −3.9 %, 32K −1.6 %. On the standard suite the tuned launch is roughly neutral for decode and slightly slower for prompt processing.
Long-output bench
Method: streaming /v1/chat/completions, temperature=0, reasoning_effort=none except the thinking case; each case 3 runs. Speeds are the engine's timings.predicted_per_second; TTFT is measured by the client. Draft acceptance is accepted/offered from timings, reported per request then medianed.
| Case (max_tokens) | Config | Generated tokens | Decode tok/s | Draft acceptance | TTFT s | Total s | Check |
|---|---|---|---|---|---|---|---|
| code module + tests (4096) | A | 2360 | 206.6 (195.9–208.3) | 81.5 % (81.2–82.9) | 0.19 | 11.7 | 3/3 |
| B | 2431 | 227.1 (227.0–228.2) | 82.7 % (82.2–82.8) | 0.16 | 10.8 | 2/3 | |
| math, no thinking (3072) | A | 3072 | 179.6 (176.4–185.5) | 68.8 % (67.5–71.3) | 0.22 | 17.3 | 0/3 |
| B | 3072 | 183.5 (181.7–187.9) | 72.5 % (71.4–73.0) | 0.06 | 17.0 | 0/3 | |
| math, thinking (8192) | A | 6807 | 163.2 (162.1–163.4) | 69.8 % (69.5–69.8) | 0.20 | 41.8 | 0/3 |
| B | 6860 | 158.4 (156.9–158.9) | 76.4 % (75.6–76.6) | 0.07 | 43.6 | 0/3 | |
| 400-line log → JSON (4096) | A | 2865 | 227.4 (216.1–229.2) | 95.1 % (93.9–95.5) | 0.36 | 13.0 | 3/3 |
| B | 2845 | 263.2 (260.5–265.6) | 96.6 % (96.0–96.7) | 0.13 | 10.9 | 3/3 |
Notes:
- The JSON case has a 16,277-token prompt. Its first run in each set shows the cold TTFT (3.5–3.8 s); the second and third hit the conversation cache (16,270 reused tokens) and start in 0.08–0.36 s.
- The code and math cases have 124–245-token prompts, so their TTFT is mostly model overhead (0.06–0.60 s).
- Case definitions: (1) code — a thread-safe
TTLCachewith O(1) operations, TTL,purge(), 8+ unittest methods; the check extracts the code block, runs it and its own unittest suite, and requires exit 0 and ≥8 tests. (2) math — count 6-digit integers with distinct nonzero digits divisible by 7, answer on aFINAL:line; expected 8544, computed by brute force over the 60,480 candidates. (3) same with thinking on. (4) JSON — 400 deterministic synthetic log lines containing exactly 40ERROR ... incident=INC-xxxxlines; the check parses the JSON, requires all 40 IDs and a summary over 20 words.
What the numbers say
- Draft acceptance, not token count, sets long-output speed. The two structured cases (code 82.7 %, JSON 96.6 %) decode at 227–263 tok/s; math at 68–76 % acceptance decodes at 158–184 tok/s; short chat sits at 165–168 tok/s. The JSON case is about 56 % faster than short chat on the same server with the same model state.
- Long structured output is where the tuned launch pays. +15.7 % JSON, +9.9 % code, +2.2 % math-without-thinking over stock, with roughly neutral short chat and −1 to −4 % prompt throughput.
- The 6.8K-token thinking case is the exception: B raises acceptance from 69.8 % to 76.4 % but decodes 3 % slower than A (158.4 vs 163.2 tok/s). I did not isolate the cause; my guess is longer per-sequence KV traffic and more draft tokens verified per step, but that is unconfirmed.
- Long outputs also expose quality failures that a 100-token suite cannot: the math problem fails in both configurations (see below), and one tuned code run generates a module whose own tests fail.
Failed checks (kept separate from the speed results)
- Math, 0/3 in both configurations. With thinking off, all six requests hit the 3072 cap before printing
FINAL:— the problem needs more room than the prompt's "under 2,500 tokens" asks for. With thinking on, the model always finishes but gives 8640 (all three B runs, two A runs) or 8537 (one A run) instead of 8544. The failure is consistent, not noise. - Code, 2/3 in B, 3/3 in A. B run 0 produced a module that fails its own
test_negative_ttl_does_not_store(exit 1 on both the direct run and unittest); the other five code runs pass. - No request errored: all 24 long-output requests returned
finish_reasonstoporlengthwith engine timings.
Not tested / limits
- One GPU, one model file, one machine, one day. No multi-GPU, no concurrency/batch throughput, no other quantization, no other model family.
- B is text-only (vision off). A was measured with vision on but no image was sent, so this is not a vision benchmark.
- Background load was not recorded; the machine was otherwise idle.
- The standard suite for A used the launch's automatic
parallel=4; I did not run A withparallel=1alone, so the prompt-throughput difference mixes several changes. - The checks are functional smoke tests (code executes, JSON parses, a number matches). They are not a quality evaluation, and generated code is executed in a temp directory without a sandbox review.
spec=6is the launch flag; the engine reportsspec=8, mtp_max=6for B andspec=6, mtp_max=4for A, so the flag and the reported field do not use the same scale.
Proposal
Add an opt-in long-output mode to tools/strata_bench.py — four cases like the above, 3K–8K caps, engine timings, draft acceptance, and the code/JSON/math checks — so this workload is measured the same way on every GPU report. The script and both results.json sets are ready; I can open a PR that adds bench/long_output_bench.py unchanged (no default path touched) if that fits the project. Happy to re-run on the final engine build or with any configuration you want compared.
Related: #3265 (expert lookahead on RTX 5070) is the other recent 48 GB-class decode experiment; this report is about long generation and launch flags, not expert lookahead.
Activity
udqe commentedon Oct 31, 2026
A second box for the acceptance story: Windows, 2x RTX 4090 24 GB, CUDA, engine 0.1.54, our production config (the one in #4573),
UD-Q4_K_XLnative pack, T=0, 21 requests this morning in two blocks. Source for every number: the engine's own/metricsper-request fields - which carry your acceptance metric on CUDA too:drafts_offered,drafts_accepted,decode_tok_s,hit_rate,first_engine_token_s.Four things this adds rather than repeats:
The monotone holds across content types within one session. Between our own chat traffic (751 tok, 82.6 % acceptance, 179.8 tok/s) and these cases, decode tracks acceptance with no visible second factor - the two-card version of your point 1, where expert-cache misses ride on top.
The math failure is quantization-independent and bigger than 0/3. You saw 8640/8537 (IQ3_S). We raised to 13 runs (UD-Q4_K_XL): 0 correct, nine distinct wrong answers - 8640 x3, 7200 x2, then one each of 8537, 8532, 8376, 8748, 7560, 8628, 7776 - plus one length-cap miss. The expected value was brute-forced independently here: 8544, as you said. Two quantizations, two machines, one consistent arithmetic weakness at ~3K-token outputs, not a quant artifact. Worth detaching from this thread's speed conclusion.
A short-output failure your suite would only catch by luck. Second block, same session, same prompt as the first block's three good short runs:
message.contentcame back EMPTY 3/3 (finishlength, 100-200 tokens all consumed byreasoning_content- one debug run captured 801 chars of reasoning and zero answer). The 100-token budget fits either the reasoning or the answer, not both. You setreasoning_effort=nonein your suite; we did not, and that is exactly the difference - so pin it in the proposed long-output mode, because a 100-token short run on this default silently returns nothing at all.Your flag-vs-report scale note reproduces on CUDA/Windows. Our launch passes
--spec 4;/metricsreports"spec":6, "mtp_max":4. Different scales on your Linux launch and our CUDA one - whatever the long-output mode prints, name the scale.One axis caution: same box, same hour, your JSON-style case reads ~210 tok/s while our standing cold-prompt bench (150K tokens,
0 reused, arena deliberately cold) reads 95-110. Acceptance and cache-state both move this number; pin the reuse state the way the issue already pins acceptance.Limits kept honest: one session, one day, one box; prompts reworded, ours not yours; the code-execution case skipped by policy (we do not run model-generated code here); non-streaming client (TTFT = the engine's own field only); math answers counted from the
FINAL:line where present. This box carries the +-10 % day-to-day decode spread we documented in #4610 - deltas under ~10 % are noise; the acceptance-decode gap (91 % vs 78 %, ~1.5x) is far above it.Happy to re-run any configuration. +1 for
bench/long_output_bench.pylanding as-is, with the fields wired to/metricsinstead of the response body - fewer build-dependent JSON paths.(AI-assisted again on our side - the model served by this repo's engine ran the suite and read its own metrics; all figures local.)
wangminle commentedon Oct 31, 2026
Follow-up: pass-only rerun (8 runs per case)
I re-ran the two cases whose checks can pass, 8 runs each on both launch configurations, and summarised only the requests whose check passed. The table in the report above includes every request that returned, including code runs whose own tests failed; this one separates the two.
Same machine, same day, same commands as the report; the server was restarted between the stock and tuned sets. Medians with ranges over passing requests.
Pass rate differs on the code case
The checks are the same as in the report: extract the Python block, run it, run its unittest suite, require exit 0 and at least 8 test methods.
unittest.TestCase/unittest.main()but omitimport unittest(NameErroron both the direct run andpython -m unittest); 1 hastest_default_ttlandtest_ttl_expiryfailing withAssertionError: 1 is not None(the default TTL / expiry behaviour is wrong).STRATA_HC_Q8, and I did not isolate which of them, if any, is responsible. A larger paired run would be needed to call it a property of the configuration.What this changes in the report
Raw
results.jsonfor both sets is available if useful.bench/long_output_bench.pynow records both all-run and*_passingstatistics and accepts--casesfor a focused rerun.wangminle commentedon Oct 31, 2026
Same rig, same tuned launch as this report, another workload: I kept the engine timings of a prefill-heavy real task (static review of a 330-file repository) and posted the record in #4813.
Not the standard protocol (no fixed prompts, no repeats, one run per pass); the limits are in #4813.