inference/research

07 — Benchmarking methodology and its pitfalls

Every number in data/benchmarks/ is a measurement somebody took, under conditions they had to write down. This doc is about the conditions: which omissions make a number meaningless, and which mistakes this repo has actually recorded as gotchas because they happened in the wild.

Read this before quoting any throughput or latency figure from this repo, and before adding a new benchmark record.

The governing rule is in AGENTS.md: a benchmark is only useful with its methodology. The benchmark schema forces metric, value, unit, methodology and measured_by; everything below is about filling those fields in a way that survives contact with another engine.

1. Name the metric, and say whose it is

The single most common way a tok/s number misleads is that nobody says whether it is what one user experiences or what the whole machine produces. Those two differ by the concurrency, and the repo has a directly measured pair:

So one hardware configuration, one model, one engine: aggregate throughput rises 13.9x (57.6 -> 800) while the number the actual user feels falls 4.6x (57.6 -> 12.5). Any comparison that mixes the two conventions is meaningless, and in this case the error is not even conservative in one direction.

The schema encodes the distinction in the metric enum:

metric meaning
tok_s_per_user decode tokens/s one request experiences
tps_aggregate tokens/s summed over all in-flight requests
decode_tok_s usually per-request at batch 1; confirm in methodology
prefill_tok_s prompt-processing rate
ttft_ms time to first token
itl_ms inter-token latency, or end-to-end time in this repo's records
quality accuracy / perplexity delta vs a baseline
memory_gb resident footprint

Two live examples where the same deployment produced both conventions, and quoting either alone would be wrong:

These are two points on one concurrency curve, not one configuration. The record notes say so explicitly, and that framing is the point: a "5000 tok/s per GPU" headline and a "180 tok/s per user" headline describe the same rig under different load.

Same trap with hardware counts. vLLM disaggregated PD serving: Qwen3.5-397B-A17B-NVFP4 on GB200 NVL72, 25K total TPS per GPU is 25000 tokens/s per GPU across a rack, and TensorRT-LLM: Llama 3.1 8B FP8 on 1x H100 SXM 80GB, ISL/OSL 1000/1000 is 14991.62 tokens/s aggregate on one GPU. Dividing one by the other produces a nonsense ratio.

2. Offline/aggregate benchmarks hide the latency you will actually get

MLPerf Inference splits scenarios into Offline (all samples in one batch, no latency ceiling) and Server (a latency constraint per query). The repo's MLPerf records are all Offline, and several say so in their unit field:

The same caveat applies to the closed-loop setups. The paired AMD/NVIDIA records here (MI300X vs H100, vLLM, Llama 3.1 70B FP8 TP8: output token throughput at 128-in/2048-out and MI300X vs H100, vLLM, Llama 3.1 70B FP8 TP8: output token throughput at 128-in/2048-out) are explicitly labelled "closed-loop, infinite-rate client fed requests at an infinite rate: no delay between messages, no latency constraint". That maximizes throughput and discards the latency distribution entirely.

Latency at batch 1 is a different regime again, and the same AMD/NVIDIA pair shows the expected inversion: MI300X is 10x better on end-to-end time-to-last-token at batch 1 (MI300X vs H100, vLLM, Llama 3.1 70B FP8 TP8 batch 1: end-to-end time-to-last-token at 128-in/2048-out vs H100 reference column for MI300X vs H100, vLLM/TRT-LLM, Llama 3.1 70B FP8 TP8 batch 1: end-to-end time-to-last-token) while being behind on aggregate throughput at high batch. That is the signature of a bandwidth-bound decode path, and it is the whole reason you cannot pick hardware from one number.

3. Pin the thing that changed, or the comparison is not an experiment

An engine version bump, a driver bump, or a different quantization name is a different experiment. This repo has four separate records that exist only because someone pinned the commit:

That last one is the shape to internalise: the regression was visible only in decode throughput, invisible in TTFT, and invisible to any correctness check.

The same applies on the quantization side. Two models quantized at 'the same Q4_K_M level' can land at 4.75 and 4.30 bpw, so named-quant comparisons across models are not controlled experiments is the clearest statement in the repo that named levels are not budgets: llama.cpp's per-tensor mixed-precision rules mean two models quantized at "Q4_K_M" land at 4.75 and 4.30 bpw respectively. Comparing across models at a matched name is not a controlled experiment. Normalize to matched bpw.

4. A single sweep tells you about one point, not a curve

A benchmark run at one concurrency level cannot tell you whether a configuration is good or merely impressive. EAGLE/MTP speculative decoding costs 6-10 points of prefix-cache hit rate, and hybrid layouts make the recompute 100x larger is the clearest example: EAGLE/MTP speculative decoding costs 6-10 points of prefix-cache hit rate (97.5% -> 86.9-91.7%), and on one hybrid layout the recompute is large enough to cost 30-40% of batch throughput — but only on prefix-reusing workloads. The record's own workaround is to measure hit rate with and without speculation before enabling it, i.e. sweep the axis.

The GB300 pair in section 1 is the same lesson in the other direction: 5000 tok/s/GPU and 180 tok/s/user are both correct, and only the sweep between them tells you which one your deployment lives at.

For vendor-published curves, the repo records the shape explicitly rather than the headline. Silex (third-party genai-perf): Llama 3.3 70B Instruct bf16 on 4x H100 80GB SXM, ~7000 total tokens/s at 200/200 is 7,000 TPS "peak at 500 concurrent users" — and its notes flag both that the figure is an author's "" approximation read off a chart image, and that the same post's H100-vs-A100 gap conflates architecture with form factor because the A100 used PCIe rather than SXM. Do not quote the ratio.

5. Correct output does not mean correct measurement

This is the failure class that motivates the whole doc. Most of it is silent.

Silent numerical corruption

The pattern: CUDA graphs, kernel selection, and quantization paths can all be broken while every output check passes. A benchmark that reports throughput without a quality gate over the same configuration is measuring the speed of a possibly-wrong model.

Silent accuracy loss that looks like a win

Silent loss of losslessness

A warning that is not a capability statement

If you see this quoted as evidence about hardware, the citation chain is broken.

6. Benchmark harnesses lie about what they measured

7. When the harness itself is the variable

Two config flags are load-bearing enough to be benchmark methodology:

And one that is not a config at all — memory accounting. vLLM's --gpu-memory-utilization is a fraction of TOTAL VRAM, so KV cache block allocation fails even with memory visibly free records that --gpu-memory-utilization is a fraction of total VRAM, so allocation fails while memory is visibly free. The number of blocks you actually got is in the startup log, and that log line is part of the methodology.

8. Checklist for a new benchmark record

  1. Which convention? tok_s_per_user or tps_aggregate. If aggregate, state the concurrency. If per-user, state the batch size.
  2. What is the baseline, and is it like-for-like? Same quantization, same scenario class (Offline vs Server), same harness version.
  3. Is the engine version pinned to a commit, not a release name? Same for the driver, and for any env var the result depends on.
  4. Does the output pass a quality check under the same configuration? If the result is speed-only, cite the record that establishes the output is coherent, or mark it as unverified.
  5. Is it a chart read or a counter read? Set reproducible accordingly and say so in notes.
  6. measured_by — vendor, third_party, or self. A vendor number on open tooling is still vendor; see vLLM disaggregated PD serving: Qwen3.5-397B-A17B-NVFP4 on GB200 NVL72, 25K total TPS per GPU, which says exactly that.
  7. Every number in notes cites a record or a source id. No bare figures.

Terms with no coverage yet

These concepts show up in real benchmark methodology but this repo has no record that discusses them. Do not cite this doc as if it did.

Related

  • AGENTS.md — the ingest contract, including why vendor_claim and *_basis fields exist.
  • SCHEMA.md — the benchmark type, field by field.
  • 06-glossary.md — the domain vocabulary these pitfalls are phrased in.
  • 10-testing-methodology.md — the correctness side: differential oracles for silent corruption, where this doc covers performance measurement.