Repository navigation
Proposal: reproducible end-to-end benchmark / regression harness #281
Description
While testing Qwen3.6 GGUF support in #131 together with Turing support from #24, I ended up building a small end-to-end benchmark harness to answer questions that the existing kernel/microbenchmarks don't fully capture.
Before cleaning it up into a PR, I'd like to check whether something along these lines would be useful upstream.
Motivation
FreeToken has excellent low-level benchmarks and tests, but comparing real serving configurations currently requires users to assemble their own methodology.
This makes results from different hardware, quantizations and MoE backends difficult to compare.
For example, on my test machine:
- Quadro RTX 4000 (sm_75, 8 GB)
- Ryzen 9 7945HX
- 96 GB DDR5
- PCIe Gen3 x16 (~13.17 GB/s measured H2D/D2H)
- Qwen3.6-35B-A3B
ft bench bw considered hybrid viable/recommended for several formats, but actual fixed-prompt inference consistently favored offload.
For NVFP4 in CPU Boost/performance mode:
ft bench bw:
CPU MoE 41.17 GB/s
PCIe gather 13.27 GB/s
ratio 3.10
recommended hybrid
actual Qwen3.6:
offload .93 37.16 tok/s
best hybrid 25.39 tok/s
Q4_K showed the same general pattern:
offload .93 39.16 tok/s
hybrid cap8 26.60 tok/s
The microbenchmark is still useful as a viability signal, but the full pipeline adds scheduling, synchronization, expert routing and GPU starvation that aren't represented by bandwidth alone.
Proposed performance suite
A model/backend-independent CLI or script that runs a reproducible set of serving tests against a FreeToken server:
Fixed decode
- deterministic prompt
- fixed output budget
- multiple repetitions
- wall-clock end-to-end tok/s
- optional decode-window telemetry
Prefill/context sweep
For example:
- ~512 tokens
- ~1K
- ~2K
- ~4K
Record TTFT and effective prompt throughput.
Prefix cache
Measure:
- cold prefix
- identical cached prefix
- cached prefix + small suffix
MoE backend comparison
Where supported:
- offload
- hybrid with configurable caps
- optionally cpu/fused
Cache/residency sweep
Optionally test several memory-ratio / expert-cache configurations rather than relying on one default.
Hardware metadata
Automatically record enough information to make community results comparable:
- FreeToken commit/version
- model/checkpoint
- quant format / GGUF bank types
- GPU and compute capability
- VRAM
- CPU
- RAM
- PCIe benchmark
- governor / boost state where available
- backend flags
Machine-readable output
A stable JSON schema would be especially useful.
For example:
{
"schema_version": 1,
"hardware": {},
"model": {},
"configuration": {},
"decode": {},
"prefill": {},
"prefix_cache": {},
"hybrid": {}
}This could make benchmark reports from issues/PRs directly comparable rather than requiring manually formatted tables.
Optional behavioral/API regression suite
I also built a separate deterministic suite while comparing quantizations.
This turned out to catch several categories of issues that pure tensor/kernel correctness does not:
- instruction following
- extraction
- reasoning completion
- valid JSON
- tool calling
- executable coding tasks
- long-context retrieval
I would not propose using arbitrary "LLM-as-judge" quality scores as CI gates.
Instead, a smaller upstream version could focus on deterministic API behavior, for example:
- requested JSON parses as JSON
- tool call parser returns the expected function and arguments
- reasoning does not consume the entire output budget without returning visible content
- protocol/control markers do not leak into semantic output
- code produced for a deterministic task passes fixed unit tests
- long-context needle is returned
finish_reasonis appropriate
This could run against any sufficiently capable model as an optional end-to-end regression suite rather than claiming to measure general intelligence.
Why I think this could be useful
During my own testing, the harness caught several cases where a raw throughput number or "the model generates fluent text" would have given the wrong conclusion:
- parser selection made valid Qwen tool calls appear broken;
- reasoning could find the answer but hit
finish_reason=lengthbefore exposing it; - strict string scoring confused correct answers with formatting violations;
- a server process could leave a CUDA-owning child behind and invalidate the next benchmark;
- synthetic hybrid bandwidth did not predict end-to-end hybrid performance.
A standardized harness would also make hardware/PR reports more reproducible.
Current prototype
I currently have two shell-based prototypes:
- performance suite: decode + prefill + prefix cache + offload/hybrid + JSON summaries
- behavioral suite: 28 deterministic API tasks with semantic vs strict scoring
They were developed for Qwen3.6 testing, so I would want to remove model-specific assumptions and simplify them before proposing code upstream.
If maintainers think this direction is useful, I'm happy to clean up the performance portion first and submit it as a small, separate PR.
The behavioral/API conformance suite could then be discussed separately rather than bundling both concepts into one change.
Activity
tuxevil commentedon Aug 29, 2026
A useful external baseline came out of the same testing, and I think it strengthens the case for keeping prefill and decode as separate first-class measurements in the proposed harness.
I ran the same Qwen3.6-35B-A3B family through Ollama/llama.cpp on the same machine:
qwen3.6:35bQ4_K_MI used a deterministic needle-retrieval prompt containing 3,226 input tokens and requesting only a short identifier as the answer.
Ollama reported:
The retrieval itself was correct.
For comparison, the FreeToken context sweep on the same hardware used a very similar ~3.3K-token prompt:
The GGUFs are not identical (
Q4_K_MvsQ4_K), so I would not treat this as a strict engine-vs-engine benchmark.However, the magnitude and direction of the result are interesting:
The Ollama decode number also appears stable: an earlier long-generation run on the same CPU/GPU placement generated 4,055 tokens at 16.07 tok/s, versus 15.59 tok/s here.
I also forced Ollama CPU-only for the same 3,226-token prompt:
So partial GPU placement helps llama.cpp substantially, but it still produces a very different performance profile from FreeToken expert offload.
This seems like a good concrete example of why a single aggregate "tokens/sec" result is insufficient for comparing serving configurations.
Depending on the workload, the conclusion can reverse:
It also suggests that an optional external-baseline mode could eventually be useful in a community benchmark format — not necessarily something FreeToken itself has to run, but a JSON schema capable of representing measurements from another engine would make reports much easier to compare.
One additional observation: during Ollama autoregressive decode I observed the RTX 4000 staying below roughly 50 W in
nvidia-smi, whereas FreeToken offload in my earlier runs held the GPU near full utilization and roughly 116-119 W. I did not collect synchronized telemetry for the Ollama run, so I would treat that as qualitative rather than benchmark data, but it is consistent with the very different decode behavior.For me this external baseline reinforced that the proposed harness should keep at least these measurements independent:
Collapsing them into one throughput number would hide exactly the trade-off seen here.
benwilson commentedon Aug 31, 2026
+1, and one requirement such a harness should build in from the start: greedy decoding is not reproducible when thinking is on. Three identical
temperature: 0requests produced three different reasoning traces (206 / 218 / 288 completion tokens); the first divergence was 210 characters in, on a near-tie ("Function reverse linked list"vs"Function to reverse linked list"). The same prompt withchat_template_kwargs: {"enable_thinking": false}was byte-identical across runs (91 tokens each), and explicittemperature/top_k/seedare honoured over--sampling-defaults model(omitting them samples, as documented).Environment: FreeToken git
4b94bdc(HEAD 2026-08-30, no local patches), installed withpip install --no-build-isolation "freetoken[accel] @ git+…@4b94bdc"; torch 2.11.0+cu130, CUDA toolkit 13.0, driver 595.71.05; RTX 3090 24 GB (sm_86) on a rented vast.ai host (503 GiB RAM); imagepytorch/pytorch:2.11.0-cuda13.0-cudnn9-devel; checkpointdealignai/Qwen3.8-Flash-Next-ABLITERATED-NVFP4@be794b99(standard compressed-tensors NVFP4 layout, byte-identical quant config to the RadixArk stock checkpoint — not the FTW repack).Reads like a nondeterministic kernel (atomics in the offload MoE combine, or the Triton NVFP4 path) flipping near-tied argmax picks — content-heavy reasoning text has many near-ties, peaked code output few. For a regression harness that means either thinking off, or comparing on scores rather than token hashes, and a note in the docs that temp-0 ≠ reproducible here. Happy to re-run the repro on
--moe-backend hybridor another backend if that helps localise it.benwilson commentedon Aug 31, 2026
A second data point for the harness question, this time the exact case in your motivation —
ft bench bwrecommendedhybrid; here real inference agreed at short context and could not tell the difference at long context, for a reason the harness would want to measure explicitly.Box: RTX 3090 (sm_86) on a rented host, PCIe 4.0 x16 (
bw: linear H2D 23.8 / D2H 20.1 GB/s), 32-core CPU (STREAM read 80.6 GB/s), 503 GiB RAM; FreeToken4b94bdc, torch 2.11.0+cu130;dealignai/Qwen3.8-Flash-Next-ABLITERATED-NVFP4(compressed-tensors NVFP4),--nvfp4-backend triton --moe-cache-auto --max-seq-len-override 262144 --kv-reserve-tokens 262144 --max-running-requests 1.ft bench bw --dtype nvfp4: CPU-MoE 65.8 GB/s vs PCIe-gather 25.1 → ratio 2.62× →hybrid; overlapped 43.4 + 21.6 → 33.2% of misses fetched over PCIe.Probe (same on both backends; the ad-hoc thing your harness would replace): streaming
/v1/chat/completions,temperature 0,enable_thinking: false, 400-token answer, a fresh nonce on every prompt so the radix cache cannot help; decode = tokens/(last − first token time).Two observations for the harness design:
Decode batch … gen throughputlines average 21.3 tok/s (offload) vs 26.5 (hybrid) — the MoE gain is real inside the batch — but the client sees ~12.5 on both. Short prompts agree within 5%. So something per decode step outside the batch grows with context and dominates at 75k; a harness that reports only the engine counter would call hybrid a win here, one that reports wall-clock would call it a wash. Both numbers are needed, and the gap itself is worth a row.bwwith-oand not settingFREETOKEN_BENCHBW_PATHgave--moe-hybrid-max-fetch auto: no usable ft bench bw profile … fixed fetch cap of 1— one WARNING line, then a different backend configuration than the one benchmarked. A harness should assert which profile (if any) the serve loaded.Raw probe logs and the
bwJSON are kept; happy to re-run under whatever protocol the harness settles on.benwilson commentedon Sep 1, 2026
Concurrency data for the harness proposed here, since the field this issue asks for (
concurrent requests/ throughput scaling) has no published values for the Flash-Next class that I can find — the paper's numbers and #324's are single-stream, and #257's 5090/4090 figures are--max-running-requests 1.Box: RTX 5090 (sm_120, 32 GB), driver 590.48.01, torch 2.11.0+cu130, PCIe 4.0 x16, 501 GiB host RAM, rented vast.ai. FreeToken
4b94bdc.RadixArk/Qwen3.8-Flash-Next-NVFP4@7b71922.--moe-backend offload --moe-cache-auto --nvfp4-backend triton --max-seq-len-override 262144 --kv-reserve-tokens 262144 --max-running-requests 8 --cuda-graph-max-bs 8. Graphs captured[1,2,4,8];moe_cache_size=2606,num_pages=4097, KV 262,208 tok = 6.19 GiB, 2.88 GiB free after capture.Method: N streams launched together, each with its own nonce prefix so the radix cache cannot serve one stream from another;
enable_thinking:false; per-stream tok/s measured first-token→last-token so TTFT is excluded from decode; aggregate is total tokens over the whole batch's wall clock. 8/8 streams completed at every level, zero failures.2000-token prompts, 400-token outputs:
Aggregate keeps climbing to 8 (2.1×) but per-stream falls 4.5× and TTFT more than doubles.
Two methodology points I'd suggest the harness bake in, both learned by getting them wrong first:
b12xbackend the first request took 2m20s wall for 10 output tokens (input throughput (token/s): 0.18on a 28-token prompt). A harness that measures cold will report compilation as inference.Also relevant to scope: I could not get
--nvfp4-backend flashinfer(b12x) to serve more than one request at all on this card — filed as #335 — so any benchmark matrix should record the NVFP4 backend alongside the MoE backend, since on sm_120 they are not interchangeable.tuxevil commentedon Sep 2, 2026
Thanks — these are very useful data points, and they actually help narrow the scope rather than expand it.
A few things I would now consider requirements for the first performance harness:
bench bwprofile was found and used;temperature: 0+ thinking as byte-level deterministic. Exact-output regression cases should probably run with thinking disabled, while reasoning-enabled cases should use semantic/protocol invariants instead.The long-context result is especially interesting. I just ran into a related effect on the 8 GB Turing box from the original post: increasing Qwen3.6 KV capacity from ~32k to ~64k kept decode reasonably healthy, but reduced expert-cache residency enough that fresh-context TTFT for a ~12k prompt went from roughly tens of seconds to ~100 s. Cached-prefix turns were a completely different regime again.
That reinforces the idea that
context length,expert-cache residency,TTFT, and decode throughput need to be recorded together — a single tok/s number can give a very misleading picture.For the first PR I would still keep the scope small: performance/serving measurements + machine-readable output. Concurrency and behavioral/API regression can build on the same schema afterwards.
Thanks for running these — this is exactly the kind of independent hardware data I hoped the harness would make easier to compare.
benwilson commentedon Sep 2, 2026
Your Turing observation replicates at the other end of the hardware range, which I think makes the "record context length + residency + TTFT + decode together" requirement worth treating as non-negotiable.
Same checkpoint and engine as my last comment, but on a 96 GB RTX PRO 6000, changing only
--kv-reserve-tokens(524288 -> 2097152, i.e. 8 seats at the model's full 262,144 window):moe_cache_sizeSo: 4x the KV cost 64% of expert residency but only 24% of shallow throughput — and then showed up almost entirely as cold TTFT at depth, exactly the regime split you saw at 8 GB. Cached-prefix turns are a different regime again, and the log is explicit about why:
Prefill batch, #new-seq: 7, #new-token: 8192, #cached-token: 751744(~99% hit).One hard number for your first bullet, since it is worse than it sounds: at 8 streams x 245,867 tokens my client-side probe measured 1.4 tok/s per stream while
Decode batch, #running-req: 8 ... gen throughput (token/s): 325-360for the same requests — ~30x disagreement. The client span is first-token-to-last-token, which at depth includes the GPU chunk-prefilling the other streams; it reports queueing as slow decode. Neither number is wrong, they answer different questions, and a harness that reports only one will mislead in whichever direction it picked. Worth stating in the schema which one a field is.Realistic agentic shape on the same box, 8 sessions with their own ~110k contexts, 3 turns each: TTFT 139 s cold (234 s worst), then 5.7 s per turn, decode ~30-36 tok/s per session from the scheduler lines. The cold fill is a once-per-session admission cost bounded by ~3,300 tok/s aggregate prefill.
Agreed on keeping the first PR to performance + machine-readable output.
tuxevil commentedon Sep 2, 2026
That replication across such different hardware is very useful. I think it settles two schema decisions for the first pass:
client_wall_*measurements explicitly fromengine_*scheduler telemetry rather than calling either simply “decode throughput”;The ~30x client/engine divergence at deep concurrent context is a particularly strong example of why the field semantics need to be explicit.
And the 287 s cold -> 5.7 s cached TTFT result makes me think cold admission and cached continuation should probably be separate benchmark cases rather than statistics of the same case.
I think we have enough methodology convergence now to stop growing the issue and turn the performance portion into a small first PR.