Build a Repeatable LLM Inference Benchmark
A reproducible inference harness fixes the model, dataset, server, workload, and metrics so latency and throughput remain comparable across serving stacks.
A repeatable LLM inference benchmark is a controlled experiment: freeze the model, dataset, serving configuration, request pattern, and metric definitions, then repeat the same run without changing them. For a self hosted llm, drive a running endpoint with a documented workload, retain raw results and the run manifest, and change only the variable under test. Compare results only when the timing boundaries and workload settings match.
Prepare a fixed system under test
If you intend to report an MLPerf result, the MLPerf Inference Rules require every submission to use LoadGen, provided in C++ with Python bindings. LoadGen generates queries for each scenario, tracks latency, validates accuracy, and computes final metrics. The rules define latency from the moment LoadGen schedules a query for the system under test until it receives a reply.
The same rules distinguish these load patterns:
| Scenario | Documented arrival behavior | Documented result |
|---|---|---|
| Server | Queries follow a Poisson distribution | Maximum Poisson throughput supported under a benchmark-specific latency constraint |
| Offline | All samples are sent at the start in a single query | Measured throughput, with no latency constraint |
| Single stream | The next query starts after the previous one completes | A 90th-percentile early-stopping latency estimate |
For LLM benchmarks, MLPerf collects time to first token (TTFT) and time per output token (TPOT). Server latency constraints are expressed as category-specific TTFT/TPOT pairs. The system under test must also be a defined set of hardware and software resources, and it should not retain information associated with query indices.
Whether or not you use MLPerf, create a run manifest before sending traffic. It should contain:
- System: GPU model, CPU, memory, driver and runtime identifiers, inference-server version, server flags, model revision, quantization choice, endpoint, and streaming mode.
- Workload: dataset revision, request rate, concurrency limit, traffic distribution, ramp strategy, and generation settings sent by the client.
- Execution: exact benchmark command, client version, run order, server lifecycle, relevant cache state, raw report, and any client errors.
For components that use semantic versioning, record exact versions rather than moving labels. Semantic Versioning 2.0.0 says a released version must not be modified, and it defines MAJOR for incompatible API changes, MINOR for backward-compatible functionality, and PATCH for backward-compatible fixes. That makes a version change an explicit benchmark variable rather than an invisible system change.
For file-based benchmark data, the DVC data-versioning guide documents that dvc add records a small .dvc metadata file for Git, the data hash determines its cache path, and dvc push uploads cached data to a configured remote. Its restoration workflow checks out the recorded .dvc revision with Git and then runs dvc checkout.
dvc add YOUR_VALUE
dvc push
Store the .dvc metadata revision with the run manifest. That lets another operator identify the intended data even when the data itself remains in a separate cache or remote.
Run the self hosted llm benchmark
Use the vLLM Benchmark CLI documentation as the contract for the following workflow. It positions vllm bench for feature evaluation and regression testing and recommends GuideLLM for production vLLM servers.
For the documented ShareGPT workflow, download the dataset with the published command:
wget https://huggingface.co/datasets/anon8231489123/ShareGPT_Vicuna_unfiltered/resolve/main/ShareGPT_V3_unfiltered_cleaned_split.json
With the model server already running, the documented online benchmark command is:
vllm bench serve \
--backend vllm \
--model NousResearch/Hermes-3-Llama-3.1-8B \
--endpoint /v1/completions \
--dataset-name sharegpt
Keep the model identifier identical to the model exposed by the server. The command uses the documented /v1/completions endpoint path; the server-start configuration remains a separate prerequisite and should come from the current vLLM serving documentation.
ShareGPT is documented for online and offline benchmarks. If you use a private corpus instead, the documented contract is a .jsonl file with a prompt field in each entry, selected with --dataset-name custom. Check the current CLI page for the file-location option rather than inventing an undocumented flag.
For a controlled-load variant, append the documented load flags to the command:
--request-rate YOUR_VALUE \
--max-concurrency YOUR_VALUE \
--burstiness YOUR_VALUE \
--ramp-up-strategy YOUR_VALUE
--request-rate controls requests per second. Its documented default is inf, which sends all requests immediately for maximum-throughput testing; a finite value creates controlled load. --max-concurrency caps outstanding requests, while omitting it leaves concurrency unlimited. --burstiness controls variability through a Gamma distribution, with 1.0 representing Poisson traffic. --ramp-up-strategy accepts linear or exponential. Resolve these placeholders before the run and preserve the values in the manifest.
Verify the metrics and preserve the evidence
Do not treat a throughput value without its timing definition as a complete result. The vLLM report lists request throughput, output-token throughput, and total-token throughput, together with the following latency fields:
| Metric | Documented measurement boundary |
|---|---|
| TTFT | Client-side time from sending the request to receiving its first streamed output |
| ITL | Time between consecutive streamed outputs, aggregated over successful requests |
| TPOT | Calculated once per request, excluding the first token, and then aggregated across requests |
| Throughput | Request throughput in requests per second, plus output-token and total-token throughput |
With speculative decoding, one streamed output can contain several tokens. The vLLM documentation explains that ITL records gaps only between streamed outputs and does not insert zero-duration gaps for tokens delivered in the same output, so ITL and TPOT can diverge. Preserve that distinction instead of renaming one metric to match another.
A run is ready for comparison when the client finishes without a reported error, the report contains the expected throughput and latency fields, and the saved manifest identifies the same model, dataset revision, endpoint, server configuration, and load profile. Archive the raw report with the command and manifest. Then repeat the identical procedure and inspect variation across runs; a single result is an observation, not evidence of repeatability.
Where the benchmark usually becomes misleading
- Cache-assisted reruns: Repeating
vllm bench serveagainst the same server can reuse prompts left in the prefix cache and inflate throughput. Record server lifecycle and run order, and do not describe an uncontrolled warm rerun as an independent baseline. - A moving system: A model revision, server flag, runtime, hardware change, or client update can alter the system under test. Record each change and rerun the complete comparison rather than combining older and newer results.
- Different arrival patterns: A maximum-throughput run with
--request-rate infis not comparable to a finite-rate run. Concurrency, burstiness, and ramp strategy are workload variables, not interchangeable report labels. - Metric-label confusion: Benchmarking tools do not standardize metric terminology. Compare TTFT, ITL, and TPOT only after confirming where each timer starts and stops.
- A moving client boundary: TTFT is measured by the client. If the endpoint is remote, the network path is part of that observation, so keep client location and connection path stable.
Another operator should be able to use the saved manifest and raw outputs to reconstruct the same system, dataset, request pattern, and measurement interpretation. If any of those elements is missing, report the result as a diagnostic observation rather than a performance comparison.
These steps are compiled from the official documentation linked in the text, nothing was run or measured for this article, and the commands and definitions should be checked against the current version before use.