vLLM benchmarking
Target audience: operators producing defensible latency/throughput numbers against production or pre-production vLLM deployments, on datacenter GPUs, often in containerized or air-gapped environments.
This skill measures; it does not tune. Once a number is trusted and the
verdict is "too slow", the knobs live elsewhere in the vllm plugin:
vllm-performance-tuning (scheduler, MoE kernels, CUDA graphs, parallelism),
vllm-caching (KV tiering when the bottleneck is prefill or cache capacity),
vllm-nvidia-hardware (the SKU's own ceiling). Measure → change one thing →
re-measure with the same methodology; a tuning change compared against a
differently-shaped benchmark run is not evidence.
Why this matters
Bad benchmarks are worse than no benchmarks — they drive the wrong decisions with false confidence. The three common failure modes:
- Wrong methodology.
--request-rate inf answers "saturation throughput," not "TTFT my users see." Mixing those up leads to buying GPUs to solve a latency problem, or shipping a latency regression because total throughput looked fine.
- Wrong workload.
--dataset-name random has zero prefix structure. Real coding-agent or RAG traffic has heavy prefix reuse. Benchmarking caching wins on random produces numbers that don't survive contact with prod.
- No warmup / wrong tokenizer. First N requests hit cold CUDA graphs. Token counts are fiction unless
--tokenizer matches the served model exactly.
The cost of getting this right is small; the cost of getting it wrong is buying the wrong hardware.
Decision tree — which subcommand
| Question |
Command |
Why |
| "Saturation throughput of this offline batch" |
vllm bench throughput |
Submits N prompts at once, measures tok/s. No server. |
| "Single-batch generation latency" |
vllm bench latency |
Fixed batch size, repeated N times. Warmup included. Good for kernel-level regression. |
| "Production serving performance" |
vllm bench serve |
HTTP-level, Poisson arrivals, percentile metrics, honors concurrency caps. Use this for serving. |
| "Find best config under SLO" |
vllm bench sweep |
Parameter sweep + auto-tune. Finds max throughput subject to P99 < X ms. |
| "Cold-start / container boot latency" |
vllm bench startup |
Time from process launch to first-token-ready. |
| "Multimodal processor overhead" |
vllm bench mm-processor |
Image/video preprocessing cost before decode. |
Most production questions route to vllm bench serve. Reach for the others only when the question is specifically kernel-level (latency), offline-batch (throughput), or SLO auto-tuning (sweep).
The two methodologies operators actually need
Methodology A: health check / SLO validation
Question: "Does my running deployment meet the latency SLO under realistic load?"
- Fixed
--max-concurrency matching the production in-flight ceiling (NOT --request-rate).
- Realistic input/output length distribution — ideally replayed from production logs via
--dataset-name custom with a JSONL file.
- Sustained 10+ minute run to cover warmup + steady state.
- Report: P50/P95/P99 TTFT, P95/P99 ITL, throughput (tok/s, req/s).
- Compare against the SLO. Pass/fail.
vllm bench serve \
--model <served-model> \
--base-url http://<endpoint> \
--dataset-name custom \
--dataset-path /data/captured-prod-prompts.jsonl \
--max-concurrency 32 \
--num-prompts 2000 \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,95,99 \
--save-result --output-json health-check.json
Methodology B: change comparison / A/B
Question: "Does config change X make it faster, and at what cost?"
- Request-rate sweep, not a single rate: e.g. 1, 2, 4, 8, 16, 32, inf req/s.
- Plot throughput vs P99 latency — the knee of the curve is the usable operating point. A config that shifts the knee right is a win.
- Same seeds, same
--num-prompts (≥500), same dataset on both sides.
- Run A and B back-to-back on the same hardware in the same session to avoid thermal/neighbor noise.
See scripts/bench-sweep.sh for a parametrized sweep runner that emits one JSON file per rate for plotting.
Critical pitfalls
- No warmup. First 30–60 s hit cold CUDA graphs / torch.compile caches.
vllm bench serve does not auto-warm (v0.11–v0.27; --num-warmups default re-read as 0 on 2026-08-11) — pre-flight the server with a few requests, or set --num-prompts large enough (≥500) to amortize. latency does warm up via --num-iters-warmup (default 10).
- Wrong tokenizer.
--tokenizer defaults to --model, but if they differ (e.g., served via a local path while benching with a HF ID), every token count in the output is fiction. Always specify explicitly.
--dataset-name random as a proxy for production traffic. Random has zero prefix structure, overstates prefill work, understates prefix-cache hit rate, makes chunked prefill look worse than reality. For anything involving caching claims, use custom with a real-traffic JSONL, or prefix_repetition for synthetic prefix-heavy tests.
--request-rate inf alone. Measures saturation throughput, not the latency regime users experience. Always include a concurrency sweep for serving comparisons.
--endpoint-type is removed. Deprecated in v0.11.0, still absent from the v0.27.0 parser. Use --backend. Current full value set (ASYNC_REQUEST_FUNCS in vllm/benchmarks/lib/endpoint_request_func.py at v0.27.0, matches docs.vllm.ai, verified 2026-08-11): openai, openai-chat, openai-audio, openai-embeddings, openai-embeddings-chat, openai-embeddings-clip, openai-embeddings-vlm2vec, vllm, vllm-pooling, vllm-rerank, infinity-embeddings, infinity-embeddings-clip. (vllm-chat is a bench throughput backend only — it is not accepted by serve.) The flag is gone but the output JSON still emits an endpoint_type key as a backward-compat alias of backend — see output-schema.md.
- Conflating tok/s with req/s. High total-tokens/sec can coexist with terrible TTFT. Always report both plus P99 ITL.
- Noisy neighbor. Shared GPU, unrelated container load, MIG partition changes mid-run — check
nvidia-smi dmon for unrelated activity before trusting numbers.
latency subcommand disables prefix caching by default (to keep numbers clean). If benchmarking prefix-cache behavior, use serve with the prefix_repetition dataset.
For the full flag reference for each subcommand, see references/commands.md. For the dataset catalog and when to use each, see references/datasets.md.
Air-gapped environments
Operators who can't reach huggingface.co have three working patterns:
- Reroute to a mirror — set
HF_ENDPOINT=https://hf-mirror.com (or an internal reverse-proxy URL). huggingface_hub treats it transparently.
- ModelScope — set
VLLM_USE_MODELSCOPE=True plus trust_remote_code=True. Historical gap: LoRA adapter loading through ModelScope (vLLM issue #32841, closed 2026-01-23). Re-verify on your vLLM version before relying on LoRA-via-ModelScope; issue closure without a linked PR means status is unclear — test first.
- Fully offline with pre-seeded cache —
HF_HUB_OFFLINE=1 + TRANSFORMERS_OFFLINE=1, HF_HOME pointing at a pre-populated directory (NFS, PVC, or JuiceFS/S3).
For benchmark datasets specifically: sonnet is in-tree at vllm/benchmarks/sonnet.txt — never downloads. random is synthetic — never downloads. sharegpt must be pre-staged: wget the JSON on a connected host, rsync into the enclave, point --dataset-path at it.
For the full air-gapped recipe (HF proxy setup, gated model tokens, MinIO-as-HF-cache, transformer cache warming), see references/air-gapped.md.
Measuring the outcomes that matter
Default metrics (--percentile-metrics ttft,tpot,itl,e2el):
- TTFT — time-to-first-token. User-facing responsiveness. Dominated by prefill.
- TPOT — time-per-output-token (averaged across decode). Steady-state perceived speed.
- ITL — inter-token latency (per-step). Catches stalls that TPOT averages away.
- E2EL — end-to-end request latency. Only one that matters for pooling/embedding models.
Reporting guideline: always P50 and P99 together. Either in isolation is misleading. Add P95 if ITL has a long tail.
Goodput SLO — --goodput KEY:VALUE (milliseconds) tracks requests that completed within an SLO budget. Example: --goodput ttft:500 itl:50. Goodput is what actually matters in production; raw throughput that violates SLO is useless.
For methodology detail (warmup protocols, sweep design, SLO-constrained auto-tune, how to capture real-traffic prompts for replay), see references/methodology.md.
When numbers look wrong or a run crashes
See references/troubleshooting.md for the failure modes: tokenizer mismatch (numbers off 20–40%), cold-cache contamination (suspiciously fast), air-gapped hang (incomplete HF_HUB_OFFLINE setup), goodput=0 (unit error), noisy-neighbor ITL variance, and the full "what to include in a bug report" checklist.
Parsing the output JSON
See references/output-schema.md for the field layout in --output-json — top-level fields (request_throughput, output_throughput, total_token_throughput), the mean_/median_/std_/p<N>_<metric>_ms pattern, speculative decoding fields, and which names are stable across versions vs renamed.
External references
1---2name: vllm-benchmarking3description: Run production vLLM benchmarks — `vllm bench` (serve, throughput, latency, sweep, startup, mm-processor), request-rate vs max-concurrency semantics, TTFT/TPOT/ITL/E2EL percentiles, goodput SLO measurement, prefix-cache workloads, air-gapped operation (HF_ENDPOINT, ModelScope, hf-mirror, offline cache). Methodology split — SLO health checks vs A/B change sweeps — plus pitfalls that produce misleading numbers (no warmup, wrong tokenizer, random-as-prod, `--request-rate inf` alone).4---56# vLLM benchmarking78Target audience: operators producing defensible latency/throughput numbers against production or pre-production vLLM deployments, on datacenter GPUs, often in containerized or air-gapped environments.910**This skill measures; it does not tune.** Once a number is trusted and the11verdict is "too slow", the knobs live elsewhere in the `vllm` plugin:12**`vllm-performance-tuning`** (scheduler, MoE kernels, CUDA graphs, parallelism),13**`vllm-caching`** (KV tiering when the bottleneck is prefill or cache capacity),14**`vllm-nvidia-hardware`** (the SKU's own ceiling). Measure → change one thing →15re-measure with the same methodology; a tuning change compared against a16differently-shaped benchmark run is not evidence.1718## Why this matters1920Bad benchmarks are worse than no benchmarks — they drive the wrong decisions with false confidence. The three common failure modes:21221. **Wrong methodology.** `--request-rate inf` answers "saturation throughput," not "TTFT my users see." Mixing those up leads to buying GPUs to solve a latency problem, or shipping a latency regression because total throughput looked fine.232. **Wrong workload.** `--dataset-name random` has zero prefix structure. Real coding-agent or RAG traffic has heavy prefix reuse. Benchmarking caching wins on random produces numbers that don't survive contact with prod.243. **No warmup / wrong tokenizer.** First N requests hit cold CUDA graphs. Token counts are fiction unless `--tokenizer` matches the served model exactly.2526The cost of getting this right is small; the cost of getting it wrong is buying the wrong hardware.2728## Decision tree — which subcommand2930| Question | Command | Why |31|---|---|---|32| "Saturation throughput of this offline batch" | `vllm bench throughput` | Submits N prompts at once, measures tok/s. No server. |33| "Single-batch generation latency" | `vllm bench latency` | Fixed batch size, repeated N times. Warmup included. Good for kernel-level regression. |34| "Production serving performance" | `vllm bench serve` | HTTP-level, Poisson arrivals, percentile metrics, honors concurrency caps. Use this for serving. |35| "Find best config under SLO" | `vllm bench sweep` | Parameter sweep + auto-tune. Finds max throughput subject to P99 < X ms. |36| "Cold-start / container boot latency" | `vllm bench startup` | Time from process launch to first-token-ready. |37| "Multimodal processor overhead" | `vllm bench mm-processor` | Image/video preprocessing cost before decode. |3839Most production questions route to `vllm bench serve`. Reach for the others only when the question is specifically kernel-level (latency), offline-batch (throughput), or SLO auto-tuning (sweep).4041## The two methodologies operators actually need4243### Methodology A: health check / SLO validation4445**Question:** "Does my running deployment meet the latency SLO under realistic load?"4647- Fixed `--max-concurrency` matching the production in-flight ceiling (NOT `--request-rate`).48- Realistic input/output length distribution — ideally replayed from production logs via `--dataset-name custom` with a JSONL file.49- Sustained 10+ minute run to cover warmup + steady state.50- Report: P50/P95/P99 TTFT, P95/P99 ITL, throughput (tok/s, req/s).51- Compare against the SLO. Pass/fail.5253```bash54vllm bench serve \55 --model <served-model> \56 --base-url http://<endpoint> \57 --dataset-name custom \58 --dataset-path /data/captured-prod-prompts.jsonl \59 --max-concurrency 32 \60 --num-prompts 2000 \61 --percentile-metrics ttft,tpot,itl,e2el \62 --metric-percentiles 50,95,99 \63 --save-result --output-json health-check.json64```6566### Methodology B: change comparison / A/B6768**Question:** "Does config change X make it faster, and at what cost?"6970- **Request-rate sweep**, not a single rate: e.g. 1, 2, 4, 8, 16, 32, inf req/s.71- Plot throughput vs P99 latency — the **knee of the curve is the usable operating point**. A config that shifts the knee right is a win.72- Same seeds, same `--num-prompts` (≥500), same dataset on both sides.73- Run A and B back-to-back on the same hardware in the same session to avoid thermal/neighbor noise.7475See `scripts/bench-sweep.sh` for a parametrized sweep runner that emits one JSON file per rate for plotting.7677## Critical pitfalls78791. **No warmup.** First 30–60 s hit cold CUDA graphs / torch.compile caches. `vllm bench serve` does not auto-warm (v0.11–v0.27; `--num-warmups` default re-read as `0` on 2026-08-11) — pre-flight the server with a few requests, or set `--num-prompts` large enough (≥500) to amortize. `latency` does warm up via `--num-iters-warmup` (default 10).802. **Wrong tokenizer.** `--tokenizer` defaults to `--model`, but if they differ (e.g., served via a local path while benching with a HF ID), every token count in the output is fiction. Always specify explicitly.813. **`--dataset-name random` as a proxy for production traffic.** Random has zero prefix structure, overstates prefill work, understates prefix-cache hit rate, makes chunked prefill look worse than reality. For anything involving caching claims, use `custom` with a real-traffic JSONL, or `prefix_repetition` for synthetic prefix-heavy tests.824. **`--request-rate inf` alone.** Measures saturation throughput, not the latency regime users experience. Always include a concurrency sweep for serving comparisons.835. **`--endpoint-type` is removed.** Deprecated in v0.11.0, still absent from the v0.27.0 parser. Use `--backend`. Current full value set (`ASYNC_REQUEST_FUNCS` in `vllm/benchmarks/lib/endpoint_request_func.py` at v0.27.0, matches docs.vllm.ai, verified 2026-08-11): `openai`, `openai-chat`, `openai-audio`, `openai-embeddings`, `openai-embeddings-chat`, `openai-embeddings-clip`, `openai-embeddings-vlm2vec`, `vllm`, `vllm-pooling`, `vllm-rerank`, `infinity-embeddings`, `infinity-embeddings-clip`. (`vllm-chat` is a `bench throughput` backend only — it is not accepted by `serve`.) The *flag* is gone but the output JSON still emits an `endpoint_type` key as a backward-compat alias of `backend` — see `output-schema.md`.846. **Conflating tok/s with req/s.** High total-tokens/sec can coexist with terrible TTFT. Always report both plus P99 ITL.857. **Noisy neighbor.** Shared GPU, unrelated container load, MIG partition changes mid-run — check `nvidia-smi dmon` for unrelated activity before trusting numbers.868. **`latency` subcommand disables prefix caching by default** (to keep numbers clean). If benchmarking prefix-cache behavior, use `serve` with the `prefix_repetition` dataset.8788For the full flag reference for each subcommand, see `references/commands.md`. For the dataset catalog and when to use each, see `references/datasets.md`.8990## Air-gapped environments9192Operators who can't reach `huggingface.co` have three working patterns:93941. **Reroute to a mirror** — set `HF_ENDPOINT=https://hf-mirror.com` (or an internal reverse-proxy URL). `huggingface_hub` treats it transparently.952. **ModelScope** — set `VLLM_USE_MODELSCOPE=True` plus `trust_remote_code=True`. Historical gap: LoRA adapter loading through ModelScope (vLLM issue #32841, closed 2026-01-23). Re-verify on your vLLM version before relying on LoRA-via-ModelScope; issue closure without a linked PR means status is unclear — test first.963. **Fully offline with pre-seeded cache** — `HF_HUB_OFFLINE=1` + `TRANSFORMERS_OFFLINE=1`, `HF_HOME` pointing at a pre-populated directory (NFS, PVC, or JuiceFS/S3).9798For benchmark datasets specifically: `sonnet` is **in-tree** at `vllm/benchmarks/sonnet.txt` — never downloads. `random` is synthetic — never downloads. `sharegpt` must be pre-staged: `wget` the JSON on a connected host, `rsync` into the enclave, point `--dataset-path` at it.99100For the full air-gapped recipe (HF proxy setup, gated model tokens, MinIO-as-HF-cache, transformer cache warming), see `references/air-gapped.md`.101102## Measuring the outcomes that matter103104Default metrics (`--percentile-metrics ttft,tpot,itl,e2el`):105106- **TTFT** — time-to-first-token. User-facing responsiveness. Dominated by prefill.107- **TPOT** — time-per-output-token (averaged across decode). Steady-state perceived speed.108- **ITL** — inter-token latency (per-step). Catches stalls that TPOT averages away.109- **E2EL** — end-to-end request latency. Only one that matters for pooling/embedding models.110111Reporting guideline: **always P50 and P99 together**. Either in isolation is misleading. Add P95 if ITL has a long tail.112113**Goodput SLO** — `--goodput KEY:VALUE` (milliseconds) tracks requests that completed within an SLO budget. Example: `--goodput ttft:500 itl:50`. Goodput is what actually matters in production; raw throughput that violates SLO is useless.114115For methodology detail (warmup protocols, sweep design, SLO-constrained auto-tune, how to capture real-traffic prompts for replay), see `references/methodology.md`.116117## When numbers look wrong or a run crashes118119See `references/troubleshooting.md` for the failure modes: tokenizer mismatch (numbers off 20–40%), cold-cache contamination (suspiciously fast), air-gapped hang (incomplete `HF_HUB_OFFLINE` setup), goodput=0 (unit error), noisy-neighbor ITL variance, and the full "what to include in a bug report" checklist.120121## Parsing the output JSON122123See `references/output-schema.md` for the field layout in `--output-json` — top-level fields (`request_throughput`, `output_throughput`, `total_token_throughput`), the `mean_/median_/std_/p<N>_<metric>_ms` pattern, speculative decoding fields, and which names are stable across versions vs renamed.124125## External references126127- vLLM bench CLI docs: https://docs.vllm.ai/en/latest/benchmarking/cli/128- `vllm bench serve` reference: https://docs.vllm.ai/en/stable/cli/bench/serve/129- Performance dashboard (nightly reference numbers): https://docs.vllm.ai/en/latest/benchmarking/dashboard/130- In-tree benchmarks dir: https://github.com/vllm-project/vllm/tree/main/benchmarks131- Air-gapped discussion thread: https://discuss.vllm.ai/t/setting-up-vllm-in-an-airgapped-environment/916132- vLLM env vars (including `VLLM_USE_MODELSCOPE`): https://docs.vllm.ai/en/stable/configuration/env_vars/133- Blog: Anatomy of a High-Throughput LLM Inference System (2025-09-05): https://blog.vllm.ai/2025/09/05/anatomy-of-vllm.html134- Blog: Large Scale Serving — DeepSeek @ 2.2k tok/s/H200 (2025-12-17): https://blog.vllm.ai/2025/12/17/large-scale-serving.html