[Docs] Use `gen-files` for generated docs content (#49587)

Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>

Harry Mellor committed Jul 25, 2026 at 15:43 UTC 3e74c60b9c74b9bdb4a4adf9ebd010c7b22104db
34 files changed +522 -868
.gitignore
-3
@@ -173,9 +173,6 @@ venv.bak/
173
174 # mkdocs documentation
175 /site
176 -docs/argparse
177 -docs/examples/*
178 -!docs/examples/README.md
176
177 # mypy
178 .mypy_cache/
.markdownlint.yaml
+3
@@ -3,6 +3,9 @@ MD007:
3 MD013: false
4 MD024:
5 siblings_only: true
6 +MD025:
7 + # Allow front matter title to be different from the first heading in the document.
8 + front_matter_title: ""
9 MD031:
10 list_items: false
11 MD033: false
.pre-commit-config.yaml
-4
@@ -260,10 +260,6 @@ repos:
260 files: ^docker/(Dockerfile|versions\.json)$
261 pass_filenames: false
262 additional_dependencies: [dockerfile-parse]
263 - - id: attention-backend-docs
264 - name: Check attention backend documentation is up to date
265 - entry: python tools/pre_commit/generate_attention_backend_docs.py --check
266 - language: python
263 - id: check-boolean-context-manager
264 name: Check for boolean ops in with-statements
265 entry: python tools/pre_commit/check_boolean_context_manager.py
docs/.nav.yml
+3 -1
@@ -56,7 +56,9 @@ nav:
56 - API Reference:
57 - api/README.md
58 - api/vllm
59 - - CLI Reference: cli
59 + - CLI Reference:
60 + - cli/README.md
61 + - vllm: cli
62 - Community:
63 - community/*
64 - Governance: governance
docs/cli/.nav.yml
+7 -9
@@ -1,10 +1,8 @@
1 nav:
2 - - README.md
3 - - serve.md
4 - - chat.md
5 - - complete.md
6 - - run-batch.md
7 - - vllm bench:
8 - - bench/**/*.md
9 - - vllm launch:
10 - - launch/**/*.md
2 + - "*.md"
3 + - bench:
4 + - bench/*.md
5 + - sweep:
6 + - bench/sweep/*.md
7 + - launch:
8 + - launch/*.md
docs/cli/bench/latency.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench latency
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_latency.inc.md"
docs/cli/bench/mm_processor.md deleted
-55
@@ -1,55 +0,0 @@
1 -# vllm bench mm-processor
2 -
3 -## Overview
4 -
5 -`vllm bench mm-processor` profiles the multimodal input processor pipeline of
6 -vision-language models. It measures per-stage latency from the HuggingFace
7 -processor through to the encoder forward pass, helping you identify
8 -preprocessing bottlenecks and understand how different image resolutions or
9 -item counts affect end-to-end request time.
10 -
11 -The benchmark supports two data sources: synthetic random multimodal inputs
12 -(`random-mm`) and HuggingFace datasets (`hf`). Warmup requests are run before
13 -measurement to ensure stable results.
14 -
15 -## Quick Start
16 -
17 -```bash
18 -vllm bench mm-processor \
19 - --model Qwen/Qwen2-VL-7B-Instruct \
20 - --dataset-name random-mm \
21 - --num-prompts 50 \
22 - --random-input-len 300 \
23 - --random-output-len 40 \
24 - --random-mm-base-items-per-request 2 \
25 - --random-mm-limit-mm-per-prompt '{"image": 3, "video": 0}' \
26 - --random-mm-bucket-config '{(256, 256, 1): 0.7, (720, 1280, 1): 0.3}'
27 -```
28 -
29 -## Measured Stages
30 -
31 -| Stage | Description |
32 -| ----- | ----------- |
33 -| `get_mm_hashes_secs` | Time spent hashing multimodal inputs |
34 -| `get_cache_missing_items_secs` | Time spent looking up the processor cache |
35 -| `apply_hf_processor_secs` | Time spent in the HuggingFace processor |
36 -| `merge_mm_kwargs_secs` | Time spent merging multimodal kwargs |
37 -| `apply_prompt_updates_secs` | Time spent updating prompt tokens |
38 -| `preprocessor_total_secs` | Total preprocessing time |
39 -| `encoder_forward_secs` | Time spent in the encoder model forward pass |
40 -| `num_encoder_calls` | Number of encoder invocations per request |
41 -
42 -The benchmark also reports end-to-end latency (TTFT + decode time) per
43 -request. Use `--metric-percentiles` to select which percentiles to report
44 -(default: p99) and `--output-json` to save results.
45 -
46 -For more examples (HF datasets, warmup, JSON output), see
47 -[Benchmarking CLI — Multimodal Processor Benchmark](../../benchmarking/cli.md#multimodal-processor-benchmark).
48 -
49 -## JSON CLI Arguments
50 -
51 ---8<-- "docs/cli/json_tip.inc.md"
52 -
53 -## Arguments
54 -
55 ---8<-- "docs/generated/argparse/bench_mm_processor.inc.md"
docs/cli/bench/serve.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench serve
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_serve.inc.md"
docs/cli/bench/sweep/plot.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench sweep plot
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_sweep_plot.inc.md"
docs/cli/bench/sweep/plot_pareto.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench sweep plot_pareto
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_sweep_plot_pareto.inc.md"
docs/cli/bench/sweep/serve.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench sweep serve
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_sweep_serve.inc.md"
docs/cli/bench/sweep/serve_workload.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench sweep serve_workload
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_sweep_serve_workload.inc.md"
docs/cli/bench/throughput.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm bench throughput
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/bench_throughput.inc.md"
docs/cli/chat.md deleted
-5
@@ -1,5 +0,0 @@
1 -# vllm chat
2 -
3 -## Arguments
4 -
5 ---8<-- "docs/generated/argparse/chat.inc.md"
docs/cli/complete.md deleted
-5
@@ -1,5 +0,0 @@
1 -# vllm complete
2 -
3 -## Arguments
4 -
5 ---8<-- "docs/generated/argparse/complete.inc.md"
docs/cli/json_tip.inc.md deleted
-10
@@ -1,10 +0,0 @@
1 -<!-- markdownlint-disable MD041 -->
2 -When passing JSON CLI arguments, the following sets of arguments are equivalent:
3 -
4 -- `--json-arg '{"key1": "value1", "key2": {"key3": "value2"}}'`
5 -- `--json-arg.key1 value1 --json-arg.key2.key3 value2`
6 -
7 -Additionally, list elements can be passed individually using `+`:
8 -
9 -- `--json-arg '{"key4": ["value3", "value4", "value5"]}'`
10 -- `--json-arg.key4+ value3 --json-arg.key4+='value4,value5'`
docs/cli/launch/render.md deleted
-22
@@ -1,22 +0,0 @@
1 -# vllm launch render
2 -
3 -## Overview
4 -
5 -`vllm launch render` starts a GPU-less rendering server for preprocessing and
6 -postprocessing only.
7 -
8 -```bash
9 -vllm launch render meta-llama/Llama-3.2-1B-Instruct --port 8100
10 -```
11 -
12 -This command reuses the standard serving parser, so model, frontend,
13 -networking, and related CLI options follow the same conventions as
14 -[`vllm serve`](../serve.md).
15 -
16 -## JSON CLI Arguments
17 -
18 ---8<-- "docs/cli/json_tip.inc.md"
19 -
20 -## Arguments
21 -
22 ---8<-- "docs/generated/argparse/launch_render.inc.md"
docs/cli/run-batch.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm run-batch
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/run-batch.inc.md"
docs/cli/serve.md deleted
-9
@@ -1,9 +0,0 @@
1 -# vllm serve
2 -
3 -## JSON CLI Arguments
4 -
5 ---8<-- "docs/cli/json_tip.inc.md"
6 -
7 -## Arguments
8 -
9 ---8<-- "docs/generated/argparse/serve.inc.md"
docs/configuration/engine_args.md
+1 -9
@@ -11,12 +11,4 @@ Engine arguments control the behavior of the vLLM engine.
11
12 The engine argument classes, [EngineArgs][vllm.engine.arg_utils.EngineArgs] and [AsyncEngineArgs][vllm.engine.arg_utils.AsyncEngineArgs], are a combination of the configuration classes defined in [vllm.config][]. Therefore, if you are interested in developer documentation, we recommend looking at these configuration classes as they are the source of truth for types, defaults and docstrings.
13
14 ---8<-- "docs/cli/json_tip.inc.md"
15 -
16 -## `EngineArgs`
17 -
18 ---8<-- "docs/generated/argparse/engine_args.inc.md"
19 -
20 -## `AsyncEngineArgs`
21 -
22 ---8<-- "docs/generated/argparse/async_engine_args.inc.md"
14 +--8<-- "gen:engine-args"
docs/design/attention_backends.md
+11 -89
@@ -1,15 +1,9 @@
1 # Attention Backend Feature Support
2
3 -This document is auto-generated by `tools/pre_commit/generate_attention_backend_docs.py`.
4 -It shows the feature support for each registered attention backend
5 -based on the checks in `AttentionBackend.validate_configuration()`.
6 -
7 -**Do not edit this file manually.** Run the following command to
8 -regenerate it:
9 -
10 -```bash
11 -python tools/pre_commit/generate_attention_backend_docs.py
12 -```
3 +The priority and feature tables on this page are auto-generated from the
4 +attention backend registry by
5 +`docs/mkdocs/gen_files/generate_attention_backends.py`, based on the checks in
6 +`AttentionBackend.validate_configuration()`.
7
8 ## Setting the Attention Backend
9
@@ -98,40 +92,11 @@ Priority is **1 = highest** (tried first).
92
93 ### Standard Attention (MHA, MQA, GQA)
94
101 -**Blackwell (SM 10.x):**
102 -
103 -| Priority | Backend |
104 -| -------- | ------- |
105 -| 1 | `FLASHINFER` |
106 -| 2 | `FLASH_ATTN` |
107 -| 3 | `TRITON_ATTN` |
108 -| 4 | `FLEX_ATTENTION` |
109 -| 5 | `TURBOQUANT` |
110 -
111 -**Ampere/Hopper (SM 8.x-9.x):**
112 -
113 -| Priority | Backend |
114 -| -------- | ------- |
115 -| 1 | `FLASH_ATTN` |
116 -| 2 | `FLASHINFER` |
117 -| 3 | `TRITON_ATTN` |
118 -| 4 | `FLEX_ATTENTION` |
119 -| 5 | `TURBOQUANT` |
95 +--8<-- "gen:priority-standard"
96
97 ### MLA Attention (DeepSeek-style)
98
123 -**Blackwell (SM 10.x):**
124 -
125 -| Priority | Backend |
126 -| -------- | ------- |
127 -| 1 | `FLASHINFER_MLA` |
128 -| 2 | `TOKENSPEED_MLA` |
129 -| 3 | `CUTLASS_MLA` |
130 -| 4 | `FLASH_ATTN_MLA` |
131 -| 5 | `FLASHMLA` |
132 -| 6 | `TRITON_MLA` |
133 -| 7 | `FLASHINFER_MLA_SPARSE`**\*** |
134 -| 8 | `FLASHMLA_SPARSE` |
99 +--8<-- "gen:priority-mla"
100
101 > **\*** For sparse MLA, FP8 KV cache always prefers `FLASHINFER_MLA_SPARSE`. With BF16 KV cache, `FLASHINFER_MLA_SPARSE` is preferred for low query-head counts (<= 16), while `FLASHMLA_SPARSE` is preferred otherwise.
102 >
@@ -157,24 +122,7 @@ Priority is **1 = highest** (tried first).
122
123 ## Standard Attention (MHA, MQA, GQA) Backends
124
160 -| Backend | Version | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | MM Prefix | DCP | Attention Types | Compute Cap. |
161 -| ------- | ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | --------- | --- | --------------- | ------------ |
162 -| `CPU_ATTN` | | fp16, bf16, fp32 | `auto`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 112, 128, 160, 192, 224, 256, 512 | ❌ | ✅ | ❌ | ❌ | All | N/A |
163 -| `FLASHINFER` | Native† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ❌ | ✅ | ❌ | ✅ | Decoder | 8.x-9.x |
164 -| `FLASHINFER` | XQA† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ❌ | ❌ | ❌ | ✅ | Decoder | 9.0 |
165 -| `FLASHINFER` | trtllm-gen† | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `nvfp4` | 16, 32, 64, 128, 256, 512, 1024 | 64, 128, 256, 512 | ✅ | ✅ | ❌ | ✅ | Decoder | 10.x |
166 -| `FLASH_ATTN` | FA2* | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ❌ | ✅ | All | ≥8.0 |
167 -| `FLASH_ATTN` | FA3* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | 9.x |
168 -| `FLASH_ATTN` | FA4* | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ✅ | ✅ | ❌ | ✅ | All | ≥10.0 |
169 -| `FLASH_ATTN_DIFFKV` | | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
170 -| `FLEX_ATTENTION` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder Only | Any |
171 -| `HPC_ATTN` | | fp16, bf16 | `auto`, `bfloat16`, `fp8_e4m3` | 64 | 128 | ❌ | ❌ | ❌ | ❌ | Decoder | ≥9.0 |
172 -| `ROCM_AITER_FA` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 16, 32 | 64, 128, 256 | ✅ | ✅ | ❌ | ❌ | Decoder | N/A |
173 -| `ROCM_AITER_UNIFIED_ATTN` | | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | Any | ✅ | ❌ | ✅ | ❌ | All | N/A |
174 -| `ROCM_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %16 | 32, 64, 80, 96, 128, 160, 192, 224, 256 | ❌ | ✅ | ✅ | ❌ | Decoder, Encoder, Encoder Only | N/A |
175 -| `TRITON_ATTN` | | fp16, bf16, fp32 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2`, `int4_per_token_head`, `int8_per_token_head`, `fp8_per_token_head` | %16 | Any | ✅ | ✅ | ✅ | ❌ | All | Any |
176 -| `TRITON_ATTN_DIFFKV` | | fp16, bf16 | `auto`, `bfloat16` | Any | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
177 -| `TURBOQUANT` | | fp16, bf16 | `turboquant_k8v4`, `turboquant_4bit_nc`, `turboquant_k3v4_nc`, `turboquant_3bit_nc` | 16, 32, 64, 128 | Any | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
125 +--8<-- "gen:table-standard"
126
127 > **†** FlashInfer Native is the regular FlashInfer path. XQA is the SM90 decode path exposed through FlashInfer's TRTLLM decode API. trtllm-gen is used on SM100 and supports sinks. Disable XQA/trtllm-gen via `--attention-config.use_trtllm_attention=0`.
128 >
@@ -188,9 +136,7 @@ automatic priority lists above. A lightning indexer scores KV blocks, the
136 top-k blocks (plus fixed init/local blocks) are selected, and attention
137 attends only to those blocks; index keys live in a separate side cache.
138
191 -| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | MM Prefix | DCP | Attention Types | Compute Cap. |
192 -| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | --------- | --- | --------------- | ------------ |
193 -| `MINIMAX_M3_SPARSE` | bf16, fp16 | `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | 128 | 128 | ❌ | ❌ | ❌ | ❌ | Decoder | Any |
139 +--8<-- "gen:table-minimax"
140
141 ## MLA (Multi-head Latent Attention) Backends
142
@@ -203,12 +149,7 @@ To explicitly select a prefill backend, use
149 Otherwise, the prefill backend is selected automatically at runtime based on
150 hardware and configuration.
151
206 -| Backend | Description | Dtypes | Compute Cap. | Notes |
207 -| ------- | ----------- | ------ | ------------ | ----- |
208 -| `FLASH_ATTN`‡ | FlashAttention varlen (FA2/FA3/FA4) | fp16, bf16 | Any | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) (FA2/FA3/FA4) or (qk_nope_head_dim=64, qk_rope_head_dim=64, v_head_dim=128) (FA2/FA3/FA4) or (qk_nope_head_dim=192, qk_rope_head_dim=64, v_head_dim=256) (FA2/FA3 only) |
209 -| `TRTLLM_RAGGED` | TensorRT-LLM ragged attention | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) or (qk_nope_head_dim=192, qk_rope_head_dim=64, v_head_dim=256) only |
210 -| `FLASHINFER` | FlashInfer CUTLASS backend | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) only |
211 -| `TOKENSPEED_MLA` | | fp16, bf16 | 10.x | (qk_nope_head_dim=128, qk_rope_head_dim=64, v_head_dim=128) only |
152 +--8<-- "gen:table-mla-prefill"
153
154 > **‡** Automatic selection tries FlashAttention first. On Blackwell
155 > (SM100), the fallback order is TRT-LLM Ragged, FlashInfer, then
@@ -219,22 +160,7 @@ hardware and configuration.
160 MLA decode backends are selected using the standard
161 `-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).
162
222 -| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
223 -| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
224 -| `CUTLASS_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 128 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
225 -| `FLASHINFER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
226 -| `FLASHINFER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
227 -| `FLASHINFER_MLA_SPARSE_SM120` | bf16 | `auto`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 64, 256 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | 12.x |
228 -| `FLASHMLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x-10.x |
229 -| `FLASHMLA_SPARSE` | bf16 | `auto`, `bfloat16`, `fp8_ds_mla` | 64 | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
230 -| `FLASH_ATTN_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 9.x |
231 -| `FLASH_ATTN_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16` | 64 | Any | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x |
232 -| `ROCM_AITER_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_e5m2` | %1 | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
233 -| `ROCM_AITER_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | 1, 64 | Any | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | N/A |
234 -| `ROCM_AITER_TRITON_MLA` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
235 -| `TOKENSPEED_MLA` | fp16, bf16 | `fp8`, `fp8_e4m3` | 32, 64 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | 10.x |
236 -| `TRITON_MLA` | fp16, bf16 | `auto`, `float16`, `bfloat16`, `fp8`, `fp8_e4m3` | %16 | Any | ❌ | ❌ | ❌ | ❌ | ✅ | Decoder | Any |
237 -| `XPU_MLA_SPARSE` | fp16, bf16 | `auto`, `float16`, `bfloat16` | Any | 576 | ❌ | ❌ | ✅ | ❌ | ❌ | Decoder | Any |
163 +--8<-- "gen:table-mla-decode"
164
165 ### DeepSeek V4 Decode Backends
166
@@ -245,8 +171,4 @@ pipeline (compressor + SWA + indexer, 256-token blocks, head 512);
171 default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and
172 `FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.
173
248 -| Backend | Dtypes | KV Dtypes | Block Sizes | Head Sizes | Sink | Non-Causal | Sparse | MM Prefix | DCP | Attention Types | Compute Cap. |
249 -| ------- | ------ | --------- | ----------- | ---------- | ---- | ---------- | ------ | --------- | --- | --------------- | ------------ |
250 -| `FLASHINFER_MLA_SPARSE_DSV4` | bf16 | `auto`, `bfloat16`, `fp8`, `fp8_e4m3`, `fp8_ds_mla` | 256 | 512 | ✅ | ❌ | ✅ | ❌ | ❌ | Decoder | 10.x, 12.x |
251 -| `FLASHMLA_SPARSE_DSV4` | bf16 | `auto`, `fp8_ds_mla`, `fp8` | 256 | 512 | ✅ | ❌ | ✅ | ❌ | ❌ | Decoder | 9.x-10.x |
252 -| `ROCM_FLASHMLA_SPARSE_DSV4` | fp16, bf16 | `auto` | Any | Any | ❌ | ❌ | ❌ | ❌ | ❌ | Decoder | N/A |
174 +--8<-- "gen:table-mla-v4-decode"
docs/mkdocs/gen_files/generate_argparse.py renamed
+185 -50
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import importlib.metadata
4 import importlib.util
5 +import inspect
6 import logging
7 import sys
8 import textwrap
@@ -10,17 +11,21 @@ from argparse import SUPPRESS, Action, HelpFormatter
11 from collections.abc import Callable, Iterable
12 from importlib.machinery import ModuleSpec
13 from pathlib import Path
13 -from typing import TYPE_CHECKING, Literal
14 +from typing import TYPE_CHECKING
15 from unittest.mock import MagicMock, patch
16
17 +import mkdocs_gen_files
18 +import regex as re
19 from pydantic_core import core_schema
20
21 logger = logging.getLogger("mkdocs")
22
23 ROOT_DIR = Path(__file__).parent.parent.parent.parent
21 -ARGPARSE_DOC_DIR = ROOT_DIR / "docs/generated/argparse"
24
25 sys.path.insert(0, str(ROOT_DIR))
26 +sys.path.insert(0, str(Path(__file__).parent))
27 +
28 +from generated_content import fill_markers # noqa: E402
29
30
31 def mock_if_no_torch(mock_module: str, mock: MagicMock):
@@ -132,8 +137,8 @@ def auto_mock(module_name: str, attr: str, max_mocks: int = 100):
137
138
139 bench_latency = auto_mock("vllm.benchmarks", "latency")
135 -bench_mm_processor = auto_mock("vllm.benchmarks", "mm_processor")
140 bench_serve = auto_mock("vllm.benchmarks", "serve")
141 +bench_startup = auto_mock("vllm.benchmarks", "startup")
142 bench_sweep_plot = auto_mock("vllm.benchmarks.sweep.plot", "SweepPlotArgs")
143 bench_sweep_plot_pareto = auto_mock(
144 "vllm.benchmarks.sweep.plot_pareto", "SweepPlotParetoArgs"
@@ -142,12 +147,28 @@ bench_sweep_serve = auto_mock("vllm.benchmarks.sweep.serve", "SweepServeArgs")
147 bench_sweep_serve_workload = auto_mock(
148 "vllm.benchmarks.sweep.serve_workload", "SweepServeWorkloadArgs"
149 )
150 +bench_sweep_startup = auto_mock("vllm.benchmarks.sweep.startup", "SweepStartupArgs")
151 bench_throughput = auto_mock("vllm.benchmarks", "throughput")
152 AsyncEngineArgs = auto_mock("vllm.engine.arg_utils", "AsyncEngineArgs")
153 EngineArgs = auto_mock("vllm.engine.arg_utils", "EngineArgs")
154 ChatCommand = auto_mock("vllm.entrypoints.cli.openai", "ChatCommand")
155 CompleteCommand = auto_mock("vllm.entrypoints.cli.openai", "CompleteCommand")
156 +BenchmarkSubcommand = auto_mock(
157 + "vllm.entrypoints.cli.benchmark.main", "BenchmarkSubcommand"
158 +)
159 +import_bench_subcommands = auto_mock(
160 + "vllm.entrypoints.cli.benchmark.main", "_import_bench_subcommand_modules"
161 +)
162 +BenchmarkSubcommandBase = auto_mock(
163 + "vllm.entrypoints.cli.benchmark.base", "BenchmarkSubcommandBase"
164 +)
165 +BenchmarkMMProcessorSubcommand = auto_mock(
166 + "vllm.entrypoints.cli.benchmark.mm_processor", "BenchmarkMMProcessorSubcommand"
167 +)
168 +LaunchSubcommandBase = auto_mock("vllm.entrypoints.cli.launch", "LaunchSubcommandBase")
169 +launch_description = auto_mock("vllm.entrypoints.cli.launch", "DESCRIPTION")
170 RenderSubcommand = auto_mock("vllm.entrypoints.cli.launch", "RenderSubcommand")
171 +sweep_subcommands = auto_mock("vllm.benchmarks.sweep.cli", "SUBCOMMANDS")
172 openai_cli_args = auto_mock("vllm.entrypoints.openai", "cli_args")
173 openai_run_batch = auto_mock("vllm.entrypoints.openai", "run_batch")
174
@@ -179,7 +200,7 @@ class MarkdownFormatter(HelpFormatter):
200
201 def add_text(self, text: str):
202 if text:
182 - self._markdown_output.append(f"{text.strip()}\n\n")
203 + self._markdown_output.append(f"{inspect.cleandoc(text)}\n\n")
204
205 def add_usage(self, usage, actions, groups, prefix=None):
206 pass
@@ -241,49 +262,163 @@ def create_parser(add_cli_args, **kwargs) -> FlexibleArgumentParser:
262 return _parser or parser
263
264
244 -def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
245 - logger.info("Generating argparse documentation")
246 - logger.debug("Root directory: %s", ROOT_DIR.resolve())
247 - logger.debug("Output directory: %s", ARGPARSE_DOC_DIR.resolve())
248 -
249 - # Create the ARGPARSE_DOC_DIR if it doesn't exist
250 - if not ARGPARSE_DOC_DIR.exists():
251 - ARGPARSE_DOC_DIR.mkdir(parents=True)
252 -
253 - # Create parsers to document
254 - parsers = {
255 - # Engine args
256 - "engine_args": create_parser(EngineArgs.add_cli_args),
257 - "async_engine_args": create_parser(
258 - AsyncEngineArgs.add_cli_args, async_args_only=True
259 - ),
260 - # CLI
261 - "serve": create_parser(openai_cli_args.make_arg_parser),
262 - "chat": create_parser(ChatCommand.add_cli_args),
263 - "complete": create_parser(CompleteCommand.add_cli_args),
264 - "launch_render": create_parser(RenderSubcommand.add_cli_args),
265 - "run-batch": create_parser(openai_run_batch.make_arg_parser),
266 - # Benchmark CLI
267 - "bench_latency": create_parser(bench_latency.add_cli_args),
268 - "bench_mm_processor": create_parser(bench_mm_processor.add_cli_args),
269 - "bench_serve": create_parser(bench_serve.add_cli_args),
270 - "bench_sweep_plot": create_parser(bench_sweep_plot.add_cli_args),
271 - "bench_sweep_plot_pareto": create_parser(bench_sweep_plot_pareto.add_cli_args),
272 - "bench_sweep_serve": create_parser(bench_sweep_serve.add_cli_args),
273 - "bench_sweep_serve_workload": create_parser(
274 - bench_sweep_serve_workload.add_cli_args
275 - ),
276 - "bench_throughput": create_parser(bench_throughput.add_cli_args),
277 - }
278 -
279 - # Generate documentation for each parser
280 - for stem, parser in parsers.items():
281 - doc_path = ARGPARSE_DOC_DIR / f"{stem}.inc.md"
282 - # Specify encoding for building on Windows
283 - with open(doc_path, "w", encoding="utf-8") as f:
284 - f.write(super(type(parser), parser).format_help())
285 - logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR))
286 -
287 -
288 -if __name__ == "__main__":
289 - on_startup("build", False)
265 +def format_help(parser: FlexibleArgumentParser) -> str:
266 + """Format a parser's help as markdown using `MarkdownFormatter`."""
267 + return super(type(parser), parser).format_help()
268 +
269 +
270 +# Absolute docs URLs are kept in the help text because they are useful in the
271 +# terminal. Wrap them as markdown links so the `url_schemes` hook can rewrite
272 +# them into doc-relative links / cross-references at render time.
273 +_DOCS_URL = re.compile(r"https://docs\.vllm\.ai/en/[^/\s]+/[^\s)>]+")
274 +
275 +
276 +def linkify_docs_urls(text: str) -> str:
277 + """Wrap bare docs.vllm.ai URLs in help text as markdown links."""
278 + return _DOCS_URL.sub(lambda m: f"[{m.group()}]({m.group()})", text)
279 +
280 +
281 +logger.info("Generating argparse documentation")
282 +logger.debug("Root directory: %s", ROOT_DIR.resolve())
283 +
284 +# The JSON tip is always rendered immediately before generated argument content,
285 +# and the generator is its only consumer, so it lives here rather than in a
286 +# separate snippet file. (The runtime terminal equivalent is
287 +# `FlexibleArgumentParser._json_tip` in vllm/utils/argparse_utils.py.)
288 +JSON_TIP = """## JSON CLI Arguments
289 +
290 +When passing JSON CLI arguments, the following sets of arguments are equivalent:
291 +
292 +- `--json-arg '{"key1": "value1", "key2": {"key3": "value2"}}'`
293 +- `--json-arg.key1 value1 --json-arg.key2.key3 value2`
294 +
295 +Additionally, list elements can be passed individually using `+`:
296 +
297 +- `--json-arg '{"key4": ["value3", "value4", "value5"]}'`
298 +- `--json-arg.key4+ value3 --json-arg.key4+='value4,value5'`
299 +
300 +"""
301 +
302 +# Argument sections filled into `gen:` markers on handwritten pages
303 +engine_args = create_parser(EngineArgs.add_cli_args)
304 +async_engine_args = create_parser(AsyncEngineArgs.add_cli_args, async_args_only=True)
305 +fill_markers(
306 + "configuration/engine_args.md",
307 + {
308 + "engine-args": (
309 + f"{JSON_TIP}## `EngineArgs`\n\n"
310 + f"{linkify_docs_urls(format_help(engine_args))}"
311 + f"## `AsyncEngineArgs`\n\n"
312 + f"{linkify_docs_urls(format_help(async_engine_args))}"
313 + )
314 + },
315 +)
316 +
317 +# CLI reference pages generated entirely from their parser: page -> (parser, JSON tip)
318 +pages = {
319 + "cli/serve.md": (create_parser(openai_cli_args.make_arg_parser), True),
320 + "cli/chat.md": (create_parser(ChatCommand.add_cli_args), False),
321 + "cli/complete.md": (create_parser(CompleteCommand.add_cli_args), False),
322 + "cli/run-batch.md": (create_parser(openai_run_batch.make_arg_parser), True),
323 + "cli/launch/render.md": (create_parser(RenderSubcommand.add_cli_args), True),
324 + "cli/bench/latency.md": (create_parser(bench_latency.add_cli_args), True),
325 + # URL kept as `mm_processor` for back-compat; command name is `mm-processor`
326 + "cli/bench/mm_processor.md": (
327 + create_parser(BenchmarkMMProcessorSubcommand.add_cli_args),
328 + True,
329 + ),
330 + "cli/bench/serve.md": (create_parser(bench_serve.add_cli_args), True),
331 + "cli/bench/startup.md": (create_parser(bench_startup.add_cli_args), True),
332 + "cli/bench/throughput.md": (create_parser(bench_throughput.add_cli_args), True),
333 + "cli/bench/sweep/plot.md": (create_parser(bench_sweep_plot.add_cli_args), True),
334 + "cli/bench/sweep/plot_pareto.md": (
335 + create_parser(bench_sweep_plot_pareto.add_cli_args),
336 + True,
337 + ),
338 + "cli/bench/sweep/serve.md": (create_parser(bench_sweep_serve.add_cli_args), True),
339 + "cli/bench/sweep/serve_workload.md": (
340 + create_parser(bench_sweep_serve_workload.add_cli_args),
341 + True,
342 + ),
343 + "cli/bench/sweep/startup.md": (
344 + create_parser(bench_sweep_startup.add_cli_args),
345 + True,
346 + ),
347 +}
348 +
349 +# Command name for pages whose file stem differs (URL kept for back-compat).
350 +COMMAND_NAMES = {"cli/bench/mm_processor.md": "mm-processor"}
351 +
352 +for doc_path, (parser, json_tip) in pages.items():
353 + segments = Path(doc_path).relative_to("cli").with_suffix("").parts
354 + label = COMMAND_NAMES.get(doc_path, segments[-1])
355 + command = " ".join([*segments[:-1], label])
356 + # `title` frontmatter keeps the nav label to just this command's segment,
357 + # while the H1 stays the full `vllm ...` command for the page heading.
358 + content = f"---\ntitle: {label}\n---\n\n"
359 + content += f"# vllm {command}\n\n"
360 + if parser.description:
361 + content += f"## Overview\n\n{parser.description}\n\n"
362 + # Rendered above instead of at the top of the Arguments section
363 + parser.description = None
364 + if json_tip:
365 + content += JSON_TIP
366 + content += f"## Arguments\n\n{linkify_docs_urls(format_help(parser))}"
367 + with mkdocs_gen_files.open(doc_path, "w") as f:
368 + f.write(content)
369 + logger.debug("CLI reference generated: %s", doc_path)
370 +
371 +logger.info("Total argparse docs generated: %d", len(pages) + 2)
372 +
373 +
374 +# --- Bare subcommand (group) pages -------------------------------------------
375 +# Mirror `vllm <group> --help`: an overview plus a table of child subcommands,
376 +# each linked to its reference page. Children are read from the CLI registries
377 +# so the listing can never drift from the actual subcommands. Each page is the
378 +# `README.md` of its command directory so it becomes that section's index and is
379 +# picked up by the existing nav globs.
380 +import_bench_subcommands() # populate BenchmarkSubcommandBase.__subclasses__()
381 +bench_subcommands = BenchmarkSubcommandBase.__subclasses__()
382 +bench_children = [(cmd.name, cmd.help) for cmd in bench_subcommands]
383 +
384 +groups = {
385 + "cli/bench/README.md": (BenchmarkSubcommand.help, bench_children),
386 + "cli/launch/README.md": (
387 + launch_description,
388 + [(cmd.name, cmd.help) for cmd in LaunchSubcommandBase.__subclasses__()],
389 + ),
390 + "cli/bench/sweep/README.md": (
391 + dict(bench_children).get("sweep"),
392 + [(args.parser_name, args.parser_help) for args, _ in sweep_subcommands],
393 + ),
394 +}
395 +
396 +# Doc paths that exist, so we only link a child that has a reference page.
397 +existing_pages = set(pages) | set(groups)
398 +
399 +
400 +def child_link(group_doc: str, name: str) -> str | None:
401 + group_dir = Path(group_doc).parent # cli/bench/README.md -> cli/bench
402 + for stem in (name, name.replace("-", "_")):
403 + # A leaf page (bench/latency.md) or a nested group index (sweep/README.md)
404 + for candidate in (group_dir / f"{stem}.md", group_dir / stem / "README.md"):
405 + if candidate.as_posix() in existing_pages:
406 + return candidate.relative_to(group_dir).as_posix()
407 + return None
408 +
409 +
410 +for doc_path, (overview, children) in groups.items():
411 + title = "vllm " + Path(doc_path).parent.relative_to("cli").as_posix()
412 + lines = [f"# {title.replace('/', ' ')}", ""]
413 + if overview:
414 + lines += ["## Overview", "", overview.strip(), ""]
415 + lines += ["## Subcommands", "", "| Command | Description |", "| --- | --- |"]
416 + for name, summary in children:
417 + link = child_link(doc_path, name)
418 + command = f"[`{name}`]({link})" if link else f"`{name}`"
419 + lines.append(f"| {command} | {(summary or '').strip()} |")
420 + with mkdocs_gen_files.open(doc_path, "w") as f:
421 + f.write("\n".join(lines) + "\n")
422 + logger.debug("CLI group reference generated: %s", doc_path)
423 +
424 +logger.info("CLI group reference pages generated: %d", len(groups))
docs/mkdocs/gen_files/generate_attention_backends.py renamed
+58 -401
@@ -9,33 +9,28 @@ based on the checks in AttentionBackend.validate_configuration().
9
10 This approach avoids requiring CUDA/ROCm/GPU libraries to be installed.
11
12 -When used as a pre-commit hook, this script receives filenames as arguments
13 -and only runs the check if any of the relevant files were modified.
12 +It runs as an mkdocs-gen-files script, so the page is generated at docs build
13 +time rather than being committed to the repository.
14 """
15
16 -import argparse
16 import ast
18 -import fnmatch
17 +import logging
18 import sys
19 from collections.abc import Callable
20 from pathlib import Path
21 from typing import Any
22
23 +sys.path.insert(0, str(Path(__file__).parent))
24 +
25 +from generated_content import fill_markers # noqa: E402
26 +
27 +logger = logging.getLogger("mkdocs")
28 +
29 # ---------------------------------------------------------------------------
30 # Constants and file paths
31 # ---------------------------------------------------------------------------
32
28 -REPO_ROOT = Path(__file__).parent.parent.parent
29 -
30 -RELEVANT_PATTERNS = [
31 - "vllm/v1/attention/backends/*.py",
32 - "vllm/v1/attention/backends/**/*.py",
33 - "vllm/models/minimax_m3/common/sparse_attention.py",
34 - "vllm/model_executor/layers/attention/mla_attention.py",
35 - "vllm/platforms/cuda.py",
36 - "tools/pre_commit/generate_attention_backend_docs.py",
37 - "docs/design/attention_backends.md",
38 -]
33 +REPO_ROOT = Path(__file__).parent.parent.parent.parent
34
35 BACKENDS_DIR = REPO_ROOT / "vllm" / "v1" / "attention" / "backends"
36 REGISTRY_FILE = BACKENDS_DIR / "registry.py"
@@ -55,19 +50,6 @@ BACKEND_KV_DTYPE_EXCLUDES: dict[str, set[str]] = {
50 }
51
52
58 -def is_relevant_file(filepath: str) -> bool:
59 - """Check if a file matches any of the relevant patterns."""
60 - path = Path(filepath)
61 - if path.is_absolute():
62 - try:
63 - path = path.relative_to(REPO_ROOT)
64 - except ValueError:
65 - return False
66 - path_str = str(path)
67 -
68 - return any(fnmatch.fnmatch(path_str, pattern) for pattern in RELEVANT_PATTERNS)
69 -
70 -
53 MLA_PREFILL_DIR = BACKENDS_DIR / "mla" / "prefill"
54 MLA_PREFILL_REGISTRY_FILE = MLA_PREFILL_DIR / "registry.py"
55 MLA_PREFILL_SELECTOR_FILE = MLA_PREFILL_DIR / "selector.py"
@@ -960,7 +942,7 @@ def analyze_backend(backend_name: str, class_path: str) -> dict[str, Any] | None
942 try:
943 tree = ast.parse(file_path.read_text())
944 except Exception as e:
963 - print(f" Warning: Could not parse {file_path}: {e}", file=sys.stderr)
945 + logger.warning("Could not parse %s: %s", file_path, e)
946 return None
947
948 class_name = class_path.rsplit(".", 1)[1]
@@ -1657,113 +1639,12 @@ def _render_table(
1639 return lines
1640
1641
1660 -def generate_markdown_table(
1661 - backends: list[dict[str, Any]], title: str, is_mla_table: bool = False
1662 -) -> str:
1663 - """Generate a titled markdown table from backend info."""
1664 - if not backends:
1665 - return f"## {title}\n\nNo backends found.\n"
1666 - has_versions = any(b.get("version") for b in backends)
1667 - columns = _build_columns(is_mla_table, has_versions)
1668 - lines = [f"## {title}", ""]
1669 - lines.extend(_render_table(columns, backends))
1670 - lines.append("")
1671 - return "\n".join(lines)
1672 -
1673 -
1674 -# ---------------------------------------------------------------------------
1675 -# Markdown section generators (usage, priority, legend, MLA)
1676 -# ---------------------------------------------------------------------------
1677 -
1678 -
1679 -def generate_usage_section() -> str:
1680 - """Generate the usage documentation section."""
1681 - return """## Setting the Attention Backend
1682 -
1683 -### Command Line
1684 -
1685 -There are two ways to specify the backend from the command line:
1686 -
1687 -**Option 1: Using `--attention-backend` (simple)**
1688 -
1689 -```bash
1690 -vllm serve <model> --attention-backend FLASH_ATTN
1691 -```
1692 -
1693 -**Option 2: Using `--attention-config.backend` / `-ac.backend` (structured config)**
1694 -
1695 -```bash
1696 -# Dot notation
1697 -vllm serve <model> --attention-config.backend FLASH_ATTN
1698 -vllm serve <model> -ac.backend FLASH_ATTN
1699 -
1700 -# JSON format
1701 -vllm serve <model> --attention-config '{"backend": "FLASH_ATTN"}'
1702 -vllm serve <model> -ac '{"backend": "FLASH_ATTN"}'
1703 -```
1704 -
1705 -> **Note:** `--attention-backend` and `--attention-config.backend` are mutually
1706 -> exclusive. Use one or the other, not both.
1707 -
1708 -### Python API
1709 -
1710 -Use `AttentionConfig` with the `LLM` class:
1711 -
1712 -```python
1713 -from vllm import LLM
1714 -from vllm.config import AttentionConfig
1715 -from vllm.v1.attention.backends.registry import AttentionBackendEnum
1716 -
1717 -# Method 1: Using AttentionConfig with enum
1718 -llm = LLM(
1719 - model="Qwen/Qwen3-0.6B",
1720 - attention_config=AttentionConfig(backend=AttentionBackendEnum.FLASH_ATTN),
1721 -)
1722 -
1723 -# Method 2: Using attention_backend parameter with string
1724 -llm = LLM(
1725 - model="Qwen/Qwen3-0.6B",
1726 - attention_backend="FLASH_ATTN",
1727 -)
1728 -```
1729 -
1730 -## Backend Selection Behavior
1731 -
1732 -### Manual Selection
1733 -
1734 -When you explicitly set a backend via `--attention-backend` or `AttentionConfig`:
1735 -
1736 -1. The backend is **validated** against your configuration (model dtype, head
1737 - size, compute capability, etc.)
1738 -2. If the backend **doesn't support** your configuration, an error is raised
1739 - with the specific reason
1740 -3. If valid, the backend is used
1741 -
1742 -Example error when selecting an incompatible backend:
1743 -
1744 -```text
1745 -ValueError: Selected backend FLASHMLA is not valid for this configuration.
1746 -Reason: ['compute capability not supported']
1747 -```
1748 -
1749 -### Automatic Selection
1750 -
1751 -When no backend is specified (the default):
1752 -
1753 -1. vLLM iterates through backends in **priority order** (see tables below)
1754 -2. Each backend is validated against your configuration
1755 -3. The **first compatible backend** is selected
1756 -4. If no backend is compatible, an error is raised listing all backends and
1757 - their incompatibility reasons
1758 -"""
1759 -
1760 -
1642 def _priority_table(
1643 title: str,
1644 backends: list[str],
1645 annotations: dict[str, str] | None = None,
1646 ) -> list[str]:
1766 - """Generate a priority table for a list of backends."""
1647 + """Render a priority table for a list of backends."""
1648
1649 def _fmt(b: str) -> str:
1650 suffix = annotations.get(b, "") if annotations else ""
@@ -1779,102 +1660,38 @@ def _priority_table(
1660 ]
1661
1662
1782 -def generate_priority_section(priorities: dict[str, list[str]]) -> str:
1783 - """Generate the priority ranking section."""
1784 - lines = [
1785 - "## Backend Priority (CUDA)",
1786 - "",
1787 - "When no backend is explicitly selected, vLLM chooses the first",
1788 - "compatible backend from these priority-ordered lists.",
1789 - "",
1790 - "Priority is **1 = highest** (tried first).",
1791 - "",
1792 - "### Standard Attention (MHA, MQA, GQA)",
1793 - "",
1794 - ]
1795 -
1796 - sm100 = "Blackwell (SM 10.x)"
1797 - ampere = "Ampere/Hopper (SM 8.x-9.x)"
1798 -
1799 - if "standard_sm100" in priorities:
1800 - lines.extend(_priority_table(sm100, priorities["standard_sm100"]))
1801 - if "standard_default" in priorities:
1802 - lines.extend(_priority_table(ampere, priorities["standard_default"]))
1663 +_SM100 = "Blackwell (SM 10.x)"
1664 +_AMPERE = "Ampere/Hopper (SM 8.x-9.x)"
1665
1804 - lines.extend(["### MLA Attention (DeepSeek-style)", ""])
1666
1806 - mla_sm100_annotations = {
1807 - "FLASHINFER_MLA_SPARSE": "**\\***",
1808 - }
1809 - if "mla_sm100" in priorities:
1810 - lines.extend(
1811 - _priority_table(sm100, priorities["mla_sm100"], mla_sm100_annotations)
1812 - )
1813 - if "mla_default" in priorities:
1814 - lines.extend(_priority_table(ampere, priorities["mla_default"]))
1815 -
1816 - if "mla_sm100" in priorities:
1817 - lines.append(
1818 - "> **\\*** For sparse MLA, FP8 KV cache always prefers "
1819 - "`FLASHINFER_MLA_SPARSE`. With BF16 KV cache, `FLASHINFER_MLA_SPARSE` "
1820 - "is preferred for low query-head counts (<= 16), while "
1821 - "`FLASHMLA_SPARSE` is preferred otherwise."
1822 - )
1823 - lines.append(">")
1824 -
1825 - lines.append(
1826 - "> **Note:** ROCm and CPU platforms have their own selection logic. "
1827 - "See the platform-specific documentation for details."
1828 - )
1829 - lines.append("")
1830 -
1831 - return "\n".join(lines)
1667 +def _priority_block(
1668 + priorities: dict[str, list[str]],
1669 + sm100_key: str,
1670 + default_key: str,
1671 + sm100_annotations: dict[str, str] | None = None,
1672 +) -> str:
1673 + """Render whichever priority tables exist for one attention category."""
1674 + lines: list[str] = []
1675 + if sm100_key in priorities:
1676 + lines += _priority_table(_SM100, priorities[sm100_key], sm100_annotations)
1677 + if default_key in priorities:
1678 + lines += _priority_table(_AMPERE, priorities[default_key])
1679 + return "\n".join(lines).strip()
1680
1681
1834 -def generate_legend() -> str:
1835 - """Generate a legend explaining the table columns."""
1836 - return """## Legend
1837 -
1838 -| Column | Description |
1839 -| ------ | ----------- |
1840 -| **Dtypes** | Supported model data types (fp16, bf16, fp32) |
1841 -| **KV Dtypes** | Supported KV cache data types (`auto`, `fp8`, `fp8_e4m3`, etc.) |
1842 -| **Block Sizes** | Supported KV cache block sizes (%N means multiples of N) |
1843 -| **Head Sizes** | Supported attention head sizes |
1844 -| **Sink** | Attention sink support (for StreamingLLM) |
1845 -| **Non-Causal** | Non-causal (bidirectional) attention support for decoder models |
1846 -| **Sparse** | Sparse attention support (MLA only) |
1847 -| **MM Prefix** | Multimodal prefix full attention support |
1848 -| **DCP** | Decode Context Parallelism support (`--decode-context-parallel-size`) |
1849 -| **Attention Types** | Supported attention patterns (Decoder, Encoder, Enc-Dec) |
1850 -| **Compute Cap.** | Required CUDA compute capability (N/A for non-CUDA backends) |
1851 -
1852 -**Symbols:** ✅ = Supported, ❌ = Not supported
1853 -"""
1682 +def _feature_table(backends: list[dict[str, Any]], is_mla: bool) -> str:
1683 + """Render a backend feature table (header, separator, one row per backend)."""
1684 + has_versions = any(b.get("version") for b in backends)
1685 + columns = _build_columns(is_mla, has_versions)
1686 + return "\n".join(_render_table(columns, backends))
1687
1688
1856 -def generate_mla_section(
1857 - prefill_backends: list[dict[str, Any]],
1858 - decode_backends: list[dict[str, Any]],
1859 - v4_decode_backends: list[dict[str, Any]] | None = None,
1860 -) -> str:
1861 - """Generate the complete MLA section with prefill and decode tables."""
1689 +def _mla_prefill_table(prefill_backends: list[dict[str, Any]]) -> str:
1690 + """Render the MLA prefill backend table."""
1691 lines = [
1863 - "## MLA (Multi-head Latent Attention) Backends",
1864 - "",
1865 - "MLA uses separate backends for prefill and decode phases.",
1866 - "",
1867 - "### Prefill Backends",
1868 - "",
1869 - "To explicitly select a prefill backend, use",
1870 - "`-ac.mla_prefill_backend=<BACKEND>` (e.g., `FLASH_ATTN`, `FLASHINFER`).",
1871 - "Otherwise, the prefill backend is selected automatically at runtime based on",
1872 - "hardware and configuration.",
1873 - "",
1692 "| Backend | Description | Dtypes | Compute Cap. | Notes |",
1693 "| ------- | ----------- | ------ | ------------ | ----- |",
1694 ]
1877 -
1695 for backend in prefill_backends:
1696 row = "| `{}`{} | {} | {} | {} | {} |".format(
1697 backend["name"],
@@ -1885,87 +1702,21 @@ def generate_mla_section(
1702 backend.get("notes", ""),
1703 )
1704 lines.append(row.replace(" ", " "))
1888 -
1889 - lines.extend(
1890 - [
1891 - "",
1892 - "> **‡** Automatic selection tries FlashAttention first. On Blackwell",
1893 - "> (SM100), the fallback order is TRT-LLM Ragged, FlashInfer, then",
1894 - "> TokenSpeed MLA. On other GPUs, only FlashAttention is considered.",
1895 - "",
1896 - "### Decode Backends",
1897 - "",
1898 - "MLA decode backends are selected using the standard",
1899 - "`-ac.backend=<BACKEND>` argument (e.g., `FLASHMLA`, `TRITON_MLA`).",
1900 - "",
1901 - ]
1902 - )
1903 -
1904 - # Reuse data-driven table rendering for decode backends
1905 - columns = _build_columns(is_mla=True, has_versions=False)
1906 - lines.extend(_render_table(columns, decode_backends))
1907 -
1908 - if v4_decode_backends:
1909 - lines.extend(
1910 - [
1911 - "",
1912 - "### DeepSeek V4 Decode Backends",
1913 - "",
1914 - "DeepSeek V4 sparse MLA uses its own decode backends, selected via",
1915 - "`--attention-backend=<BACKEND>` (e.g., `FLASHMLA_SPARSE_DSV4`,",
1916 - "`FLASHINFER_MLA_SPARSE_DSV4`). They share the V4 sparse-index",
1917 - "pipeline (compressor + SWA + indexer, 256-token blocks, head 512);",
1918 - "default on NVIDIA is `FLASHINFER_MLA_SPARSE_DSV4` on SM12x and",
1919 - "`FLASHMLA_SPARSE_DSV4` on other supported CUDA architectures.",
1920 - "",
1921 - ]
1922 - )
1923 - lines.extend(_render_table(columns, v4_decode_backends))
1924 -
1925 - lines.append("")
1705 return "\n".join(lines)
1706
1707
1929 -def generate_minimax_section(backends: list[dict[str, Any]]) -> str:
1930 - """Generate the MiniMax M3 sparse attention section."""
1931 - lines = [
1932 - "## MiniMax M3 Sparse Attention Backends",
1933 - "",
1934 - 'Block-sparse GQA backend used by MiniMax M3 sparse ("lightning indexer")',
1935 - "layers. It is wired in directly by the model and is not part of the",
1936 - "automatic priority lists above. A lightning indexer scores KV blocks, the",
1937 - "top-k blocks (plus fixed init/local blocks) are selected, and attention",
1938 - "attends only to those blocks; index keys live in a separate side cache.",
1939 - "",
1940 - ]
1941 - columns = _build_columns(is_mla=False, has_versions=False)
1942 - lines.extend(_render_table(columns, backends))
1943 - lines.append("")
1944 - return "\n".join(lines)
1945 -
1946 -
1947 -# ---------------------------------------------------------------------------
1948 -# Top-level orchestration
1949 -# ---------------------------------------------------------------------------
1708 +def build_blocks() -> dict[str, str]:
1709 + """Build the generated table blocks keyed by their `gen:` marker name.
1710
1951 -
1952 -def generate_docs() -> str:
1953 - """Generate the complete documentation."""
1711 + Only the tables are generated here; the surrounding prose lives in the
1712 + handwritten ``docs/design/attention_backends.md`` page.
1713 + """
1714 attention_backends_map = parse_registry()
1955 -
1956 - # Parse priority lists from cuda.py
1715 priorities = parse_cuda_priority_lists()
1958 -
1959 - # Parse FlashAttention FA2/FA3 feature differences
1716 fa_features = parse_flash_attn_features()
1961 -
1962 - # Parse FlashInfer TRTLLM feature differences (native vs TRTLLM on Blackwell)
1717 fi_features = parse_flashinfer_trtllm_features()
1964 -
1965 - # Parse MLA prefill backends
1718 mla_prefill_backends = parse_mla_prefill_backends()
1719
1968 - # Collect backend info
1720 all_backends = []
1721 for backend_name, class_path in attention_backends_map.items():
1722 if backend_name in SKIP_BACKENDS:
@@ -1973,17 +1724,14 @@ def generate_docs() -> str:
1724 info = analyze_backend(backend_name, class_path)
1725 if info:
1726 all_backends.append(info)
1976 -
1977 - # Expand backends into version variants
1727 if fa_features:
1728 all_backends = _expand_flash_attn_variants(all_backends, fa_features)
1729 if fi_features:
1730 all_backends = _expand_flashinfer_variants(all_backends, fi_features)
1731
1983 - # DeepSeek V4 (*_DSV4) decode backends and MiniMax M3 sparse backends each
1984 - # get their own subsection rather than mixing into the main MLA / standard
1985 - # tables (the ROCm V4 backend isn't flagged is_mla by the AST heuristic, so
1986 - # filter purely on the name).
1732 + # DeepSeek V4 (*_DSV4) and MiniMax M3 sparse backends get their own tables
1733 + # rather than mixing into the main MLA / standard tables (the ROCm V4 backend
1734 + # isn't flagged is_mla by the AST heuristic, so filter purely on the name).
1735 def _is_v4(b: dict[str, Any]) -> bool:
1736 return b["name"].endswith("_DSV4")
1737
@@ -1999,112 +1747,21 @@ def generate_docs() -> str:
1747 if not b["is_mla"] and not _is_v4(b) and not _is_minimax(b)
1748 ]
1749
2002 - # Generate documentation
2003 - script_path = "tools/pre_commit/generate_attention_backend_docs.py"
2004 - doc_lines = [
2005 - "# Attention Backend Feature Support",
2006 - "",
2007 - f"This document is auto-generated by `{script_path}`.",
2008 - "It shows the feature support for each registered attention backend",
2009 - "based on the checks in `AttentionBackend.validate_configuration()`.",
2010 - "",
2011 - "**Do not edit this file manually.** Run the following command to",
2012 - "regenerate it:",
2013 - "",
2014 - "```bash",
2015 - f"python {script_path}",
2016 - "```",
2017 - "",
2018 - ]
2019 -
2020 - # Add usage documentation
2021 - doc_lines.append(generate_usage_section())
2022 -
2023 - # Add priority section
2024 - doc_lines.append(generate_priority_section(priorities))
2025 -
2026 - # Add legend and feature tables
2027 - doc_lines.append(generate_legend())
2028 - standard_title = "Standard Attention (MHA, MQA, GQA) Backends"
2029 - doc_lines.append(
2030 - generate_markdown_table(non_mla_backends, standard_title, is_mla_table=False)
2031 - )
2032 - # Add footnotes for version/variant distinctions (in table order)
2033 - footnotes = []
2034 - if fi_features:
2035 - footnotes.append(
2036 - "> **†** FlashInfer Native is the regular FlashInfer path. XQA is the "
2037 - "SM90 decode path exposed through FlashInfer's TRTLLM decode API. "
2038 - "trtllm-gen is used on SM100 and supports sinks. Disable XQA/trtllm-gen "
2039 - "via `--attention-config.use_trtllm_attention=0`."
2040 - )
2041 - if fa_features:
2042 - footnotes.append(
2043 - "> **\\*** Specify the FlashAttention version via "
2044 - "`--attention-config.flash_attn_version=2`, `3`, or `4`. "
2045 - "Default is FA4 on SM100+ (Blackwell), FA3 on SM90 (Hopper), "
2046 - "FA2 otherwise."
2047 - )
2048 - if footnotes:
2049 - doc_lines.append("\n>\n".join(footnotes) + "\n")
2050 -
2051 - # Add MiniMax M3 sparse section (separate category after standard GQA)
2052 - if minimax_backends:
2053 - doc_lines.append(generate_minimax_section(minimax_backends))
2054 -
2055 - # Add MLA section with prefill and decode backends
2056 - doc_lines.append(
2057 - generate_mla_section(mla_prefill_backends, mla_backends, v4_decode_backends)
2058 - )
2059 -
2060 - return "\n".join(doc_lines)
2061 -
2062 -
2063 -def main():
2064 - parser = argparse.ArgumentParser(
2065 - description="Generate attention backend documentation table"
2066 - )
2067 - parser.add_argument(
2068 - "--output",
2069 - "-o",
2070 - type=str,
2071 - default=str(REPO_ROOT / "docs" / "design" / "attention_backends.md"),
2072 - help="Output file path (default: docs/design/attention_backends.md)",
2073 - )
2074 - parser.add_argument(
2075 - "--check",
2076 - action="store_true",
2077 - help="Check if the documentation is up to date (for pre-commit)",
2078 - )
2079 - parser.add_argument(
2080 - "files",
2081 - nargs="*",
2082 - help="Files to check (passed by pre-commit). If none are relevant, skip.",
2083 - )
2084 - args = parser.parse_args()
2085 -
2086 - if args.files and not any(is_relevant_file(f) for f in args.files):
2087 - sys.exit(0)
2088 -
2089 - output_path = Path(args.output)
2090 - new_content = generate_docs()
2091 -
2092 - if args.check:
2093 - needs_update = (
2094 - not output_path.exists() or output_path.read_text() != new_content
2095 - )
2096 - if needs_update:
2097 - output_path.parent.mkdir(parents=True, exist_ok=True)
2098 - output_path.write_text(new_content)
2099 - print(f"🔄 Regenerated: {output_path}")
2100 - sys.exit(1)
2101 - print(f"✅ Up to date: {output_path}")
2102 - sys.exit(0)
2103 -
2104 - output_path.parent.mkdir(parents=True, exist_ok=True)
2105 - output_path.write_text(new_content)
2106 - print(f"Generated: {output_path}")
1750 + mla_sm100_annotations = {"FLASHINFER_MLA_SPARSE": "**\\***"}
1751 + return {
1752 + "priority-standard": _priority_block(
1753 + priorities, "standard_sm100", "standard_default"
1754 + ),
1755 + "priority-mla": _priority_block(
1756 + priorities, "mla_sm100", "mla_default", mla_sm100_annotations
1757 + ),
1758 + "table-standard": _feature_table(non_mla_backends, is_mla=False),
1759 + "table-minimax": _feature_table(minimax_backends, is_mla=False),
1760 + "table-mla-prefill": _mla_prefill_table(mla_prefill_backends),
1761 + "table-mla-decode": _feature_table(mla_backends, is_mla=True),
1762 + "table-mla-v4-decode": _feature_table(v4_decode_backends, is_mla=True),
1763 + }
1764
1765
2109 -if __name__ == "__main__":
2110 - main()
1766 +logger.info("Generating attention backend documentation")
1767 +fill_markers("design/attention_backends.md", build_blocks())
docs/mkdocs/gen_files/generate_examples.py renamed
+37 -44
@@ -5,16 +5,15 @@ import logging
5 from dataclasses import dataclass
6 from functools import cached_property
7 from pathlib import Path
8 -from typing import Literal
8
9 +import mkdocs_awesome_nav.nav.directory as _nav_dir
10 +import mkdocs_gen_files
11 import regex as re
12
13 logger = logging.getLogger("mkdocs")
14
15 ROOT_DIR = Path(__file__).parent.parent.parent.parent
15 -ROOT_DIR_RELATIVE = "../../../../.."
16 EXAMPLE_DIR = ROOT_DIR / "examples"
17 -EXAMPLE_DOC_DIR = ROOT_DIR / "docs/examples"
17
18
19 def title(text: str) -> str:
@@ -197,44 +196,38 @@ class Example:
196 return content
197
198
200 -def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
201 - # Monkey-patch dirname_to_title in awesome-nav so that sub-directory names are
202 - # title-cased (e.g. "Offline Inference" instead of "Offline inference").
203 - import mkdocs_awesome_nav.nav.directory as _nav_dir
204 -
205 - _nav_dir.dirname_to_title = title
206 - logger.info("Generating example documentation")
207 - logger.debug("Root directory: %s", ROOT_DIR.resolve())
208 - logger.debug("Example directory: %s", EXAMPLE_DIR.resolve())
209 - logger.debug("Example document directory: %s", EXAMPLE_DOC_DIR.resolve())
210 -
211 - # Create the EXAMPLE_DOC_DIR if it doesn't exist
212 - if not EXAMPLE_DOC_DIR.exists():
213 - EXAMPLE_DOC_DIR.mkdir(parents=True)
214 -
215 - categories = sorted(p for p in EXAMPLE_DIR.iterdir() if p.is_dir())
216 -
217 - examples = []
218 - glob_patterns = ["*.py", "*.md", "*.sh"]
219 - # Find categorised examples
220 - for category in categories:
221 - logger.info("Processing category: %s", category.stem)
222 - globs = [category.glob(pattern) for pattern in glob_patterns]
223 - for path in itertools.chain(*globs):
224 - examples.append(Example(path, category.stem))
225 - # Find examples in subdirectories
226 - globs = [category.glob(f"*/{pattern}") for pattern in glob_patterns]
227 - for path in itertools.chain(*globs):
228 - examples.append(Example(path.parent, category.stem))
229 -
230 - # Generate the example documentation
231 - for example in sorted(examples, key=lambda e: e.path.stem):
232 - example_name = f"{example.path.stem}.md"
233 - doc_path = EXAMPLE_DOC_DIR / example.category / example_name
234 - if not doc_path.parent.exists():
235 - doc_path.parent.mkdir(parents=True)
236 - # Specify encoding for building on Windows
237 - with open(doc_path, "w+", encoding="utf-8") as f:
238 - f.write(example.generate())
239 - logger.debug("Example generated: %s", doc_path.relative_to(ROOT_DIR))
240 - logger.info("Total examples generated: %d", len(examples))
199 +# Monkey-patch dirname_to_title in awesome-nav so that sub-directory names are
200 +# title-cased (e.g. "Offline Inference" instead of "Offline inference").
201 +_nav_dir.dirname_to_title = title
202 +logger.info("Generating example documentation")
203 +logger.debug("Root directory: %s", ROOT_DIR.resolve())
204 +logger.debug("Example directory: %s", EXAMPLE_DIR.resolve())
205 +
206 +categories = sorted(
207 + p for p in EXAMPLE_DIR.iterdir() if p.is_dir() and not p.name.startswith(".")
208 +)
209 +
210 +examples = []
211 +glob_patterns = ["*.py", "*.md", "*.sh"]
212 +# Find categorised examples
213 +for category in categories:
214 + logger.info("Processing category: %s", category.stem)
215 + globs = [category.glob(pattern) for pattern in glob_patterns]
216 + for path in itertools.chain(*globs):
217 + examples.append(Example(path, category.stem))
218 + # Find examples in subdirectories
219 + globs = [category.glob(f"*/{pattern}") for pattern in glob_patterns]
220 + for path in itertools.chain(*globs):
221 + examples.append(Example(path.parent, category.stem))
222 +
223 +# Generate the example documentation
224 +for example in sorted(examples, key=lambda e: e.path.stem):
225 + doc_path = f"examples/{example.category}/{example.path.stem}.md"
226 + with mkdocs_gen_files.open(doc_path, "w") as f:
227 + f.write(example.generate())
228 + if example.main_file is not None:
229 + # Point the edit button at the example's source file
230 + edit_path = Path("..") / example.main_file.relative_to(ROOT_DIR)
231 + mkdocs_gen_files.set_edit_path(doc_path, str(edit_path))
232 + logger.debug("Example generated: %s", doc_path)
233 +logger.info("Total examples generated: %d", len(examples))
docs/mkdocs/gen_files/generate_metrics.py renamed
+36 -49
@@ -2,27 +2,28 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import ast
4 import logging
5 +import sys
6 from pathlib import Path
6 -from typing import Literal
7 +
8 +sys.path.insert(0, str(Path(__file__).parent))
9 +
10 +from generated_content import fill_markers # noqa: E402
11
12 logger = logging.getLogger("mkdocs")
13
14 ROOT_DIR = Path(__file__).parent.parent.parent.parent
11 -DOCS_DIR = ROOT_DIR / "docs"
12 -GENERATED_METRICS_DIR = DOCS_DIR / "generated" / "metrics"
15
14 -# Files to scan for metric definitions - each will generate a separate table
16 +# Files to scan for metric definitions - each fills a `gen:` marker in
17 +# docs/usage/metrics.md with its table (the section heading and any preamble
18 +# live in the tracked page next to the marker).
19 METRIC_SOURCE_FILES = [
16 - {"path": "vllm/v1/metrics/loggers.py", "output": "general.inc.md"},
17 - {
18 - "path": "vllm/v1/spec_decode/metrics.py",
19 - "output": "spec_decode.inc.md",
20 - },
20 + {"path": "vllm/v1/metrics/loggers.py", "key": "metrics-general"},
21 + {"path": "vllm/v1/spec_decode/metrics.py", "key": "metrics-spec-decode"},
22 {
23 "path": "vllm/distributed/kv_transfer/kv_connector/v1/nixl/stats.py",
23 - "output": "nixl_connector.inc.md",
24 + "key": "metrics-nixl",
25 },
25 - {"path": "vllm/v1/metrics/perf.py", "output": "perf.inc.md"},
26 + {"path": "vllm/v1/metrics/perf.py", "key": "metrics-mfu"},
27 ]
28
29
@@ -110,41 +111,27 @@ def generate_markdown_table(metrics: list[dict[str, str]]) -> str:
111 return "\n".join(lines) + "\n"
112
113
113 -def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
114 - """Generate metrics documentation tables from source files."""
115 - logger.info("Generating metrics documentation")
116 -
117 - # Create generated directory if it doesn't exist
118 - GENERATED_METRICS_DIR.mkdir(parents=True, exist_ok=True)
119 -
120 - total_metrics = 0
121 - for source_config in METRIC_SOURCE_FILES:
122 - source_path = source_config["path"]
123 - output_file = source_config["output"]
124 -
125 - filepath = ROOT_DIR / source_path
126 - if not filepath.exists():
127 - raise FileNotFoundError(f"Metrics source file not found: {filepath}")
128 -
129 - logger.debug("Extracting metrics from: %s", source_path)
130 - metrics = extract_metrics_from_file(filepath)
131 - logger.debug("Found %d metrics in %s", len(metrics), source_path)
132 -
133 - # Generate and write the markdown table for this source
134 - table_content = generate_markdown_table(metrics)
135 - output_path = GENERATED_METRICS_DIR / output_file
136 - with open(output_path, "w", encoding="utf-8") as f:
137 - f.write(table_content)
138 -
139 - total_metrics += len(metrics)
140 - logger.info(
141 - "Generated metrics table: %s (%d metrics)",
142 - output_path.relative_to(ROOT_DIR),
143 - len(metrics),
144 - )
145 -
146 - logger.info(
147 - "Total metrics generated: %d across %d files",
148 - total_metrics,
149 - len(METRIC_SOURCE_FILES),
150 - )
114 +logger.info("Generating metrics documentation")
115 +
116 +blocks = {}
117 +total_metrics = 0
118 +for source_config in METRIC_SOURCE_FILES:
119 + source_path = source_config["path"]
120 +
121 + filepath = ROOT_DIR / source_path
122 + if not filepath.exists():
123 + raise FileNotFoundError(f"Metrics source file not found: {filepath}")
124 +
125 + logger.debug("Extracting metrics from: %s", source_path)
126 + metrics = extract_metrics_from_file(filepath)
127 + logger.debug("Found %d metrics in %s", len(metrics), source_path)
128 +
129 + blocks[source_config["key"]] = generate_markdown_table(metrics).strip()
130 + total_metrics += len(metrics)
131 +
132 +fill_markers("usage/metrics.md", blocks)
133 +logger.info(
134 + "Total metrics generated: %d across %d files",
135 + total_metrics,
136 + len(METRIC_SOURCE_FILES),
137 +)
docs/mkdocs/gen_files/generated_content.py new
+56
@@ -0,0 +1,56 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Inline build-time generated content into existing docs pages.
4 +
5 +Source pages mark where generated content goes with a snippet-style marker,
6 +`--8<-- "gen:<key>"`, so the insertion point is explicit and readable. The
7 +substitution happens here (at gen-files time, before mkdocs-gen-files shadows
8 +the page), not via pymdownx.snippets, so the content can be generated at build
9 +time without living in a real file on disk.
10 +
11 +The `gen:` prefix keeps these markers distinct from real pymdownx.snippets
12 +includes, and `fill_markers` fails loudly if a marker is missing or left behind
13 +(pymdownx.snippets would otherwise silently drop an unsubstituted marker).
14 +"""
15 +
16 +from pathlib import Path
17 +
18 +import mkdocs_gen_files
19 +import regex as re
20 +
21 +DOCS_DIR = Path(__file__).parent.parent.parent
22 +
23 +_MARKER = '--8<-- "gen:{key}"'
24 +_ANY_MARKER = re.compile(r'--8<-- "gen:[^"]*"')
25 +
26 +
27 +def fill_markers(doc_path: str, blocks: dict[str, str]) -> None:
28 + """Replace `--8<-- "gen:<key>"` markers in a docs page with generated content.
29 +
30 + Args:
31 + doc_path: Docs-relative path of the source page to fill.
32 + blocks: Mapping of marker key to the markdown to insert in its place.
33 +
34 + Raises:
35 + FileNotFoundError: If the source page does not exist.
36 + ValueError: If an expected marker is missing, or any `gen:` marker is
37 + left unsubstituted after filling.
38 + """
39 + source = DOCS_DIR / doc_path
40 + if not source.exists():
41 + raise FileNotFoundError(f"Cannot fill markers in missing page: {doc_path}")
42 +
43 + text = source.read_text()
44 + for key, content in blocks.items():
45 + marker = _MARKER.format(key=key)
46 + if marker not in text:
47 + raise ValueError(f"{doc_path}: missing marker {marker}")
48 + text = text.replace(marker, content)
49 +
50 + if leftover := _ANY_MARKER.search(text):
51 + raise ValueError(f"{doc_path}: unsubstituted marker {leftover.group()}")
52 +
53 + with mkdocs_gen_files.open(doc_path, "w") as f:
54 + f.write(text)
55 + # Keep the edit button pointing at the real source page
56 + mkdocs_gen_files.set_edit_path(doc_path, doc_path)
docs/mkdocs/hooks/url_schemes.py
+39 -4
@@ -19,6 +19,7 @@ The on_page_markdown hook passes the current page context to the preprocessor be
19 each page is converted.
20 """
21
22 +import posixpath
23 from pathlib import Path
24
25 import regex as re
@@ -38,18 +39,22 @@ TITLE = r"(?P<title>[^\[\]<>]+?)"
39 REPO = r"(?P<repo>.+?/.+?)"
40 TYPE = r"(?P<type>issues|pull|projects)"
41 NUMBER = r"(?P<number>\d+)"
42 +VERSION = r"[^/\s]+"
43 PATH = r"(?P<path>[^\s]+?)"
44 FRAGMENT = r"(?P<fragment>#[^\s]+)?"
43 -URL = f"https://github.com/{REPO}/{TYPE}/{NUMBER}{FRAGMENT}"
45 +URL_GITHUB = f"https://github.com/{REPO}/{TYPE}/{NUMBER}{FRAGMENT}"
46 RELATIVE = rf"(?!(https?|ftp)://|#){PATH}{FRAGMENT}"
47 +URL_DOCS = f"https://docs.vllm.ai/en/{VERSION}/{PATH}{FRAGMENT}"
48
49 # Common titles to use for GitHub links when none is provided in the link.
50 TITLES = {"issues": "Issue ", "pull": "Pull Request ", "projects": "Project "}
51
52 # Regex to match GitHub issue, PR, and project links with optional titles.
50 -github_link = re.compile(rf"(\[{TITLE}\]\(|<){URL}(\)|>)")
53 +github_link = re.compile(rf"(\[{TITLE}\]\(|<){URL_GITHUB}(\)|>)")
54 # Regex to match relative file links with optional titles.
55 relative_link = re.compile(rf"\[{TITLE}\]\({RELATIVE}\)")
56 +# Regex to match absolute docs.vllm.ai links (should only exist in CLI).
57 +docs_link = re.compile(rf"\[{TITLE}\]\({URL_DOCS}\)")
58
59
60 class UrlSchemesPreprocessor(Preprocessor):
@@ -61,7 +66,8 @@ class UrlSchemesPreprocessor(Preprocessor):
66
67 def run(self, lines):
68 page = self.ext.page
64 - if page is None or getattr(page.file, "abs_src_path", None) is None:
69 + files = self.ext.files
70 + if page is None:
71 return lines
72
73 def replace_relative_link(match: re.Match) -> str:
@@ -70,7 +76,7 @@ class UrlSchemesPreprocessor(Preprocessor):
76 """
77 title = match.group("title")
78 path = match.group("path")
73 - path = (Path(page.file.abs_src_path).parent / path).resolve()
79 + path = ((DOC_DIR / page.file.src_uri).parent / path).resolve()
80 fragment = match.group("fragment") or ""
81
82 # Check if the path exists and is outside the docs dir
@@ -105,9 +111,36 @@ class UrlSchemesPreprocessor(Preprocessor):
111 url = f"https://github.com/{repo}/{type}/{number}{fragment}"
112 return f"[{gh_icon} {title}]({url})"
113
114 + def replace_docs_link(match: re.Match) -> str:
115 + """Rewrite absolute docs.vllm.ai links as doc-relative links."""
116 + title = match.group("title")
117 + path = match.group("path").rstrip("/")
118 + fragment = match.group("fragment") or ""
119 +
120 + # vllm.config.<Class> API reference -> mkdocstrings cross-reference
121 + if path == "api/vllm/config" and re.fullmatch(
122 + r"#vllm\.config\.\w+", fragment
123 + ):
124 + ident = fragment[1:]
125 + return f"[`{ident}`][{ident}]"
126 +
127 + # Other docs pages -> link relative to the current page, but only
128 + # when the target is a known docs page (real or generated); leave
129 + # unknown/external URLs untouched. This is correct even when the same
130 + # docstring is also rendered on its API reference page.
131 + src = f"{path.removesuffix('.html')}.md"
132 + if files.get_file_from_path(src) is None:
133 + return match.group(0)
134 + rel = posixpath.relpath(src, posixpath.dirname(page.file.src_uri))
135 + # Auto-wrapped bare URLs use the URL as their title; make it readable.
136 + if title.startswith("http"):
137 + title = path.removesuffix(".html")
138 + return f"[{title}]({rel}{fragment})"
139 +
140 markdown = "\n".join(lines)
141 markdown = github_link.sub(replace_github_link, markdown)
142 markdown = relative_link.sub(replace_relative_link, markdown)
143 + markdown = docs_link.sub(replace_docs_link, markdown)
144 return markdown.split("\n")
145
146
@@ -116,6 +149,7 @@ class UrlSchemesExtension(Extension):
149
150 def __init__(self, **kwargs):
151 self.page = None
152 + self.files = None
153 super().__init__(**kwargs)
154
155 def extendMarkdown(self, md):
@@ -138,4 +172,5 @@ def on_page_markdown(
172 ) -> str:
173 """Pass the current page context to the preprocessor."""
174 _ext.page = page
175 + _ext.files = files
176 return markdown
docs/pre_run_check.sh
+1 -1
@@ -28,7 +28,7 @@ DOCS_PATHS=(
28 docs/ # Actual docs content
29 examples/ # Examples are rendered in docs
30 vllm/ # API & CLI reference
31 - requirements/test/cuda.txt # CLI reference (see docs/mkdocs/hooks/generate_argparse.py)
31 + requirements/test/cuda.txt # CLI reference (see docs/mkdocs/gen_files/generate_argparse.py)
32 mkdocs.yaml # Affects build process
33 .readthedocs.yaml # Affects build process
34 requirements/docs.txt # Affects build process
docs/usage/metrics.md
+4 -4
@@ -35,21 +35,21 @@ The following metrics are exposed:
35
36 ## General Metrics
37
38 ---8<-- "docs/generated/metrics/general.inc.md"
38 +--8<-- "gen:metrics-general"
39
40 ## Speculative Decoding Metrics
41
42 ---8<-- "docs/generated/metrics/spec_decode.inc.md"
42 +--8<-- "gen:metrics-spec-decode"
43
44 ## NIXL KV Connector Metrics
45
46 ---8<-- "docs/generated/metrics/nixl_connector.inc.md"
46 +--8<-- "gen:metrics-nixl"
47
48 ## Model Flops Utilization (MFU) Performance Metrics
49
50 These metrics are available via `--enable-mfu-metrics`:
51
52 ---8<-- "docs/generated/metrics/perf.inc.md"
52 +--8<-- "gen:metrics-mfu"
53
54 ## Deprecation Policy
55
mkdocs.yaml
+7 -10
@@ -3,7 +3,6 @@ site_url: !ENV READTHEDOCS_CANONICAL_URL
3 repo_url: https://github.com/vllm-project/vllm
4 edit_uri: edit/main/docs/
5 exclude_docs: |
6 - argparse
6 *.inc.md
7 *.template.md
8 theme:
@@ -50,24 +49,22 @@ theme:
49
50 hooks:
51 - docs/mkdocs/hooks/remove_announcement.py
53 - - docs/mkdocs/hooks/generate_examples.py
54 - - docs/mkdocs/hooks/generate_argparse.py
55 - - docs/mkdocs/hooks/generate_metrics.py
52 - docs/mkdocs/hooks/url_schemes.py
53 - docs/mkdocs/hooks/autoref_code.py
54
55 plugins:
56 - meta
57 - search
58 + - gen-files:
59 + scripts:
60 + - docs/mkdocs/gen_files/generate_examples.py
61 + - docs/mkdocs/gen_files/generate_argparse.py
62 + - docs/mkdocs/gen_files/generate_metrics.py
63 + - docs/mkdocs/gen_files/generate_attention_backends.py
64 - autorefs
65 - awesome-nav
66 - glightbox
65 - - git-revision-date-localized:
66 - # exclude autogenerated files
67 - exclude:
68 - - api/*
69 - - examples/*
70 - - generated/*
67 + - git-revision-date-localized
68 - minify:
69 minify_html: true
70 minify_js: true
vllm/config/cache.py
+4 -4
@@ -137,13 +137,13 @@ class CacheConfig:
137 still be controlled by mamba_cache_dtype). If set to 'auto', the data type
138 for the ssm state will be determined by mamba_cache_dtype."""
139 mamba_cache_mode: MambaCacheMode = "none"
140 - """The cache strategy for Mamba layers.
140 + """The cache strategy for Mamba layers:
141 +
142 - "none": set when prefix caching is disabled.
143 - "all": cache the mamba state of all tokens at position i * block_size. This is
143 - the default behavior (for models that support it) when prefix caching is
144 - enabled.
144 + the default behavior (for models that support it) when prefix caching is enabled.
145 - "align": only cache the mamba state of the last token of each scheduler step and
146 - when the token is at position i * block_size.
146 + when the token is at position i * block_size.
147 """
148 replayssm_buffer_len: int = Field(default=16, gt=0)
149 """ReplaySSM history buffer length B: with use_replayssm, standard decode
vllm/engine/arg_utils.py
+6 -4
@@ -249,17 +249,19 @@ def get_type_hints(type_hint: TypeHint) -> set[TypeHint]:
249
250 NEEDS_HELP = (
251 any("--help" in arg for arg in sys.argv) # vllm SUBCOMMAND --help
252 - or (argv0 := sys.argv[0]).endswith("mkdocs") # mkdocs SUBCOMMAND
253 - or argv0.endswith("mkdocs/__main__.py") # python -m mkdocs SUBCOMMAND
252 + or "mkdocs" in sys.modules # mkdocs SUBCOMMAND
253 )
254
255
256 def _maybe_add_docs_url(cls: Any) -> str:
257 """Generate API docs URL for a vllm config class."""
259 - if not cls.__module__.startswith("vllm.config"):
258 + import vllm.config
259 +
260 + name = cls.__name__
261 + if getattr(vllm.config, name, None) is not cls:
262 return ""
263 version = f"v{VLLM_VERSION}" if "dev" not in VLLM_VERSION else "latest"
262 - return f"\n\nAPI docs: https://docs.vllm.ai/en/{version}/api/vllm/config/#vllm.config.{cls.__name__}"
264 + return f"\n\nAPI docs: https://docs.vllm.ai/en/{version}/api/vllm/config/#vllm.config.{name}"
265
266
267 def _expand_json_human_readable_numbers(val: str) -> str:
vllm/entrypoints/cli/benchmark/mm_processor.py
+48 -1
@@ -1,6 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import argparse
4 +import inspect
5
6 from vllm.benchmarks.mm_processor import add_cli_args, main
7 from vllm.entrypoints.cli.benchmark.base import BenchmarkSubcommandBase
@@ -8,13 +9,59 @@ from vllm.utils.argparse_utils import FlexibleArgumentParser
9
10
11 class BenchmarkMMProcessorSubcommand(BenchmarkSubcommandBase):
11 - """The `mm-processor` subcommand for `vllm bench`."""
12 + r"""`vllm bench mm-processor` profiles the multimodal input processor pipeline of
13 + vision-language models. It measures per-stage latency from the HuggingFace
14 + processor through to the encoder forward pass, helping you identify
15 + preprocessing bottlenecks and understand how different image resolutions or
16 + item counts affect end-to-end request time.
17 +
18 + The benchmark supports two data sources: synthetic random multimodal inputs
19 + (`random-mm`) and HuggingFace datasets (`hf`). Warmup requests are run before
20 + measurement to ensure stable results.
21 +
22 + ## Quick Start
23 +
24 + ```bash
25 + vllm bench mm-processor \
26 + --model Qwen/Qwen2-VL-7B-Instruct \
27 + --dataset-name random-mm \
28 + --num-prompts 50 \
29 + --random-input-len 300 \
30 + --random-output-len 40 \
31 + --random-mm-base-items-per-request 2 \
32 + --random-mm-limit-mm-per-prompt '{"image": 3, "video": 0}' \
33 + --random-mm-bucket-config '{(256, 256, 1): 0.7, (720, 1280, 1): 0.3}'
34 + ```
35 +
36 + ## Measured Stages
37 +
38 + | Stage | Description |
39 + | ----- | ----------- |
40 + | `get_mm_hashes_secs` | Time spent hashing multimodal inputs |
41 + | `get_cache_missing_items_secs` | Time spent looking up the processor cache |
42 + | `apply_hf_processor_secs` | Time spent in the HuggingFace processor |
43 + | `merge_mm_kwargs_secs` | Time spent merging multimodal kwargs |
44 + | `apply_prompt_updates_secs` | Time spent updating prompt tokens |
45 + | `preprocessor_total_secs` | Total preprocessing time |
46 + | `encoder_forward_secs` | Time spent in the encoder model forward pass |
47 + | `num_encoder_calls` | Number of encoder invocations per request |
48 +
49 + The benchmark also reports end-to-end latency (TTFT + decode time) per
50 + request. Use `--metric-percentiles` to select which percentiles to report
51 + (default: p99) and `--output-json` to save results.
52 +
53 + For more examples (HF datasets, warmup, JSON output), see the
54 + [multimodal processor benchmark guide](https://docs.vllm.ai/en/latest/benchmarking/cli/#multimodal-processor-benchmark).
55 + """
56
57 name = "mm-processor"
58 help = "Benchmark multimodal processor latency across different configurations."
59
60 @classmethod
61 def add_cli_args(cls, parser: FlexibleArgumentParser) -> None:
62 + # The class docstring is the page overview / `--help` description.
63 + if cls.__doc__:
64 + parser.description = inspect.cleandoc(cls.__doc__)
65 add_cli_args(parser)
66
67 @staticmethod
vllm/entrypoints/cli/launch.py
+16 -3
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import argparse
5 +import inspect
6 import signal
7
8 import uvloop
@@ -36,9 +37,12 @@ class LaunchSubcommandBase(CLISubcommand):
37 def add_cli_args(cls, parser: FlexibleArgumentParser) -> None:
38 """Add the CLI arguments to the parser.
39
39 - By default, adds the standard vLLM serving arguments.
40 + By default, uses the subcommand's docstring as the description and adds
41 + the standard vLLM serving arguments.
42 Subclasses can override to add component-specific arguments.
43 """
44 + if cls.__doc__:
45 + parser.description = inspect.cleandoc(cls.__doc__)
46 make_arg_parser(parser)
47
48 @staticmethod
@@ -47,7 +51,17 @@ class LaunchSubcommandBase(CLISubcommand):
51
52
53 class RenderSubcommand(LaunchSubcommandBase):
50 - """The `render` subcommand for `vllm launch`."""
54 + """`vllm launch render` starts a GPU-less rendering server for preprocessing
55 + and postprocessing only.
56 +
57 + ```bash
58 + vllm launch render meta-llama/Llama-3.2-1B-Instruct --port 8100
59 + ```
60 +
61 + This command reuses the standard serving parser, so model, frontend,
62 + networking, and related CLI options follow the same conventions as
63 + [`vllm serve`](https://docs.vllm.ai/en/latest/cli/serve/).
64 + """
65
66 name = "render"
67 help = "Launch a GPU-less rendering server (preprocessing and postprocessing only)."
@@ -93,7 +107,6 @@ class LaunchSubcommand(CLISubcommand):
107 cmd_subparser = launch_subparsers.add_parser(
108 cmd_cls.name,
109 help=cmd_cls.help,
96 - description=cmd_cls.help,
110 usage=f"vllm {self.name} {cmd_cls.name} [options]",
111 )
112 cmd_subparser.set_defaults(launch_command=cmd_cls.cmd)