A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
Improve `--help` for enhanced user experience (#24903)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Harry Mellor committed
Sep 25, 2025 at 00:08 UTC
e7f27ea648fd234ec44566925dfe9548a002d6c0
8 files changed
+114
-137
docs/mkdocs/hooks/generate_argparse.py
+1
-1
@@ -168,5 +168,5 @@ def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
168
doc_path = ARGPARSE_DOC_DIR / f"{stem}.md"
169
# Specify encoding for building on Windows
170
with open(doc_path, "w", encoding="utf-8") as f:
171
- f.write(parser.format_help())
171
+ f.write(super(type(parser), parser).format_help())
172
logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR))
vllm/engine/arg_utils.py
+2
-2
@@ -156,8 +156,8 @@ def is_online_quantization(quantization: Any) -> bool:
156
157
158
NEEDS_HELP = (
159
- "--help" in (argv := sys.argv) # vllm SUBCOMMAND --help
160
- or (argv0 := argv[0]).endswith("mkdocs") # mkdocs SUBCOMMAND
159
+ any("--help" in arg for arg in sys.argv) # vllm SUBCOMMAND --help
160
+ or (argv0 := sys.argv[0]).endswith("mkdocs") # mkdocs SUBCOMMAND
161
or argv0.endswith("mkdocs/__main__.py") # python -m mkdocs SUBCOMMAND
162
)
163
vllm/entrypoints/cli/benchmark/main.py
+5
-8
@@ -8,8 +8,7 @@ import typing
8
9
from vllm.entrypoints.cli.benchmark.base import BenchmarkSubcommandBase
10
from vllm.entrypoints.cli.types import CLISubcommand
11
-from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG,
12
- show_filtered_argument_or_group_from_help)
11
+from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
12
13
if typing.TYPE_CHECKING:
14
from vllm.utils import FlexibleArgumentParser
@@ -33,9 +32,8 @@ class BenchmarkSubcommand(CLISubcommand):
32
subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
33
bench_parser = subparsers.add_parser(
34
self.name,
36
- help=self.help,
35
description=self.help,
38
- usage="vllm bench <bench_type> [options]")
36
+ usage=f"vllm {self.name} <bench_type> [options]")
37
bench_subparsers = bench_parser.add_subparsers(required=True,
38
dest="bench_type")
39
@@ -44,13 +42,12 @@ class BenchmarkSubcommand(CLISubcommand):
42
cmd_cls.name,
43
help=cmd_cls.help,
44
description=cmd_cls.help,
47
- usage=f"vllm bench {cmd_cls.name} [options]",
45
+ usage=f"vllm {self.name} {cmd_cls.name} [options]",
46
)
47
cmd_subparser.set_defaults(dispatch_function=cmd_cls.cmd)
48
cmd_cls.add_cli_args(cmd_subparser)
51
- show_filtered_argument_or_group_from_help(cmd_subparser,
52
- ["bench", cmd_cls.name])
53
- cmd_subparser.epilog = VLLM_SUBCMD_PARSER_EPILOG
49
+ cmd_subparser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
50
+ subcmd=f"{self.name} {cmd_cls.name}")
51
return bench_parser
52
53
vllm/entrypoints/cli/main.py
+1
-1
@@ -30,7 +30,7 @@ def main():
30
31
parser = FlexibleArgumentParser(
32
description="vLLM CLI",
33
- epilog=VLLM_SUBCMD_PARSER_EPILOG,
33
+ epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"),
34
)
35
parser.add_argument(
36
'-v',
vllm/entrypoints/cli/run_batch.py
+4
-6
@@ -9,8 +9,7 @@ import importlib.metadata
9
import typing
10
11
from vllm.entrypoints.cli.types import CLISubcommand
12
-from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG,
13
- show_filtered_argument_or_group_from_help)
12
+from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
13
from vllm.logger import init_logger
14
15
if typing.TYPE_CHECKING:
@@ -50,7 +49,7 @@ class RunBatchSubcommand(CLISubcommand):
49
from vllm.entrypoints.openai.run_batch import make_arg_parser
50
51
run_batch_parser = subparsers.add_parser(
53
- "run-batch",
52
+ self.name,
53
help="Run batch prompts and write results to file.",
54
description=(
55
"Run batch prompts using vLLM's OpenAI-compatible API.\n"
@@ -59,9 +58,8 @@ class RunBatchSubcommand(CLISubcommand):
58
"vllm run-batch -i INPUT.jsonl -o OUTPUT.jsonl --model <model>",
59
)
60
run_batch_parser = make_arg_parser(run_batch_parser)
62
- show_filtered_argument_or_group_from_help(run_batch_parser,
63
- ["run-batch"])
64
- run_batch_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG
61
+ run_batch_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
62
+ subcmd=self.name)
63
return run_batch_parser
64
65
vllm/entrypoints/cli/serve.py
+13
-7
@@ -14,8 +14,7 @@ from vllm.entrypoints.openai.api_server import (run_server, run_server_worker,
14
setup_server)
15
from vllm.entrypoints.openai.cli_args import (make_arg_parser,
16
validate_parsed_serve_args)
17
-from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG,
18
- show_filtered_argument_or_group_from_help)
17
+from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
18
from vllm.logger import init_logger
19
from vllm.usage.usage_lib import UsageContext
20
from vllm.utils import (FlexibleArgumentParser, decorate_logs, get_tcp_uri,
@@ -29,6 +28,14 @@ from vllm.v1.utils import (APIServerProcessManager,
28
29
logger = init_logger(__name__)
30
31
+DESCRIPTION = """Launch a local OpenAI-compatible API server to serve LLM
32
+completions via HTTP. Defaults to Qwen/Qwen3-0.6B if no model is specified.
33
+
34
+Search by using: `--help=<ConfigGroup>` to explore options by section (e.g.,
35
+--help=ModelConfig, --help=Frontend)
36
+ Use `--help=all` to show all available flags at once.
37
+"""
38
+
39
40
class ServeSubcommand(CLISubcommand):
41
"""The `serve` subcommand for the vLLM CLI. """
@@ -56,14 +63,13 @@ class ServeSubcommand(CLISubcommand):
63
self,
64
subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
65
serve_parser = subparsers.add_parser(
59
- "serve",
60
- help="Start the vLLM OpenAI Compatible API server.",
61
- description="Start the vLLM OpenAI Compatible API server.",
66
+ self.name,
67
+ description=DESCRIPTION,
68
usage="vllm serve [model_tag] [options]")
69
70
serve_parser = make_arg_parser(serve_parser)
65
- show_filtered_argument_or_group_from_help(serve_parser, ["serve"])
66
- serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG
71
+ serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
72
+ subcmd=self.name)
73
return serve_parser
74
75
vllm/entrypoints/utils.py
+9
-104
@@ -1,13 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-import argparse
4
import asyncio
5
import dataclasses
6
import functools
7
import os
9
-import subprocess
10
-import sys
8
+from argparse import Namespace
9
from typing import Any, Optional, Union
10
11
from fastapi import Request
@@ -25,13 +23,10 @@ from vllm.utils import FlexibleArgumentParser
23
logger = init_logger(__name__)
24
25
VLLM_SUBCMD_PARSER_EPILOG = (
28
- "Tip: Use `vllm [serve|run-batch|bench <bench_type>] "
29
- "--help=<keyword>` to explore arguments from help.\n"
30
- " - To view a argument group: --help=ModelConfig\n"
31
- " - To view a single argument: --help=max-num-seqs\n"
32
- " - To search by keyword: --help=max\n"
33
- " - To list all groups: --help=listgroup\n"
34
- " - To view help with pager: --help=page")
26
+ "For full list: vllm {subcmd} --help=all\n"
27
+ "For a section: vllm {subcmd} --help=ModelConfig (case-insensitive)\n" # noqa: E501
28
+ "For a flag: vllm {subcmd} --help=max-model-len (_ or - accepted)\n" # noqa: E501
29
+ "Documentation: https://docs.vllm.ai\n")
30
31
32
async def listen_for_disconnect(request: Request) -> None:
@@ -196,96 +191,6 @@ def _validate_truncation_size(
191
return truncate_prompt_tokens
192
193
199
-def _output_with_pager(text: str):
200
- """Output text using scrolling view if available and appropriate."""
201
-
202
- pagers = ['less -R', 'more']
203
- for pager_cmd in pagers:
204
- try:
205
- proc = subprocess.Popen(pager_cmd.split(),
206
- stdin=subprocess.PIPE,
207
- text=True)
208
- proc.communicate(input=text)
209
- return
210
- except (subprocess.SubprocessError, OSError, FileNotFoundError):
211
- continue
212
-
213
- # No pager worked, fall back to normal print
214
- print(text)
215
-
216
-
217
-def show_filtered_argument_or_group_from_help(parser: argparse.ArgumentParser,
218
- subcommand_name: list[str]):
219
-
220
- # Only handle --help=<keyword> for the current subcommand.
221
- # Since subparser_init() runs for all subcommands during CLI setup,
222
- # we skip processing if the subcommand name is not in sys.argv.
223
- # sys.argv[0] is the program name. The subcommand follows.
224
- # e.g., for `vllm bench latency`,
225
- # sys.argv is `['vllm', 'bench', 'latency', ...]`
226
- # and subcommand_name is "bench latency".
227
- if len(sys.argv) <= len(subcommand_name) or sys.argv[
228
- 1:1 + len(subcommand_name)] != subcommand_name:
229
- return
230
-
231
- for arg in sys.argv:
232
- if arg.startswith('--help='):
233
- search_keyword = arg.split('=', 1)[1]
234
-
235
- # Enable paged view for full help
236
- if search_keyword == 'page':
237
- help_text = parser.format_help()
238
- _output_with_pager(help_text)
239
- sys.exit(0)
240
-
241
- # List available groups
242
- if search_keyword == 'listgroup':
243
- output_lines = ["\nAvailable argument groups:"]
244
- for group in parser._action_groups:
245
- if group.title and not group.title.startswith(
246
- "positional arguments"):
247
- output_lines.append(f" - {group.title}")
248
- if group.description:
249
- output_lines.append(" " +
250
- group.description.strip())
251
- output_lines.append("")
252
- _output_with_pager("\n".join(output_lines))
253
- sys.exit(0)
254
-
255
- # For group search
256
- formatter = parser._get_formatter()
257
- for group in parser._action_groups:
258
- if group.title and group.title.lower() == search_keyword.lower(
259
- ):
260
- formatter.start_section(group.title)
261
- formatter.add_text(group.description)
262
- formatter.add_arguments(group._group_actions)
263
- formatter.end_section()
264
- _output_with_pager(formatter.format_help())
265
- sys.exit(0)
266
-
267
- # For single arg
268
- matched_actions = []
269
-
270
- for group in parser._action_groups:
271
- for action in group._group_actions:
272
- # search option name
273
- if any(search_keyword.lower() in opt.lower()
274
- for opt in action.option_strings):
275
- matched_actions.append(action)
276
-
277
- if matched_actions:
278
- header = f"\nParameters matching '{search_keyword}':\n"
279
- formatter = parser._get_formatter()
280
- formatter.add_arguments(matched_actions)
281
- _output_with_pager(header + formatter.format_help())
282
- sys.exit(0)
283
-
284
- print(f"\nNo group or parameter matching '{search_keyword}'")
285
- print("Tip: use `--help=listgroup` to view all groups.")
286
- sys.exit(1)
287
-
288
-
194
def get_max_tokens(max_model_len: int, request: Union[ChatCompletionRequest,
195
CompletionRequest],
196
input_length: int, default_sampling_params: dict) -> int:
@@ -301,11 +206,11 @@ def get_max_tokens(max_model_len: int, request: Union[ChatCompletionRequest,
206
if val is not None)
207
208
304
-def log_non_default_args(args: Union[argparse.Namespace, EngineArgs]):
209
+def log_non_default_args(args: Union[Namespace, EngineArgs]):
210
non_default_args = {}
211
307
- # Handle argparse.Namespace
308
- if isinstance(args, argparse.Namespace):
212
+ # Handle Namespace
213
+ if isinstance(args, Namespace):
214
parser = make_arg_parser(FlexibleArgumentParser())
215
for arg, default in vars(parser.parse_args([])).items():
216
if default != getattr(args, arg):
@@ -323,6 +228,6 @@ def log_non_default_args(args: Union[argparse.Namespace, EngineArgs]):
228
non_default_args["model"] = default_args.model
229
else:
230
raise TypeError("Unsupported argument type. " \
326
- "Must be argparse.Namespace or EngineArgs instance.")
231
+ "Must be Namespace or EngineArgs instance.")
232
233
logger.info("non-default args: %s", non_default_args)
vllm/utils/__init__.py
+79
-8
@@ -1720,6 +1720,7 @@ class FlexibleArgumentParser(ArgumentParser):
1720
"Additionally, list elements can be passed individually using +:\n"
1721
' --json-arg \'{"key4": ["value3", "value4", "value5"]}\'\n'
1722
" --json-arg.key4+ value3 --json-arg.key4+=\'value4,value5\'\n\n")
1723
+ _search_keyword: Optional[str] = None
1724
1725
def __init__(self, *args, **kwargs):
1726
# Set the default "formatter_class" to SortedHelpFormatter
@@ -1768,13 +1769,79 @@ class FlexibleArgumentParser(ArgumentParser):
1769
self._action_groups.append(group)
1770
return group
1771
1771
- def format_help(self) -> str:
1772
- # Add tip about JSON arguments to the epilog
1773
- epilog = self.epilog or ""
1774
- if (self.add_json_tip
1775
- and not epilog.startswith(FlexibleArgumentParser._json_tip)):
1776
- self.epilog = FlexibleArgumentParser._json_tip + epilog
1777
- return super().format_help()
1772
+ def format_help(self):
1773
+ # Only use custom help formatting for bottom level parsers
1774
+ if self._subparsers is not None:
1775
+ return super().format_help()
1776
+
1777
+ formatter = self._get_formatter()
1778
+
1779
+ # Handle keyword search of the args
1780
+ if (search_keyword := self._search_keyword) is not None:
1781
+ # Normalise the search keyword
1782
+ search_keyword = search_keyword.lower().replace("_", "-")
1783
+ # Return full help if searching for 'all'
1784
+ if search_keyword == 'all':
1785
+ self.epilog = self._json_tip
1786
+ return super().format_help()
1787
+
1788
+ # Return group help if searching for a group title
1789
+ for group in self._action_groups:
1790
+ if group.title and group.title.lower() == search_keyword:
1791
+ formatter.start_section(group.title)
1792
+ formatter.add_text(group.description)
1793
+ formatter.add_arguments(group._group_actions)
1794
+ formatter.end_section()
1795
+ formatter.add_text(self._json_tip)
1796
+ return formatter.format_help()
1797
+
1798
+ # Return matched args if searching for an arg name
1799
+ matched_actions = []
1800
+ for group in self._action_groups:
1801
+ for action in group._group_actions:
1802
+ # search option name
1803
+ if any(search_keyword in opt.lower()
1804
+ for opt in action.option_strings):
1805
+ matched_actions.append(action)
1806
+ if matched_actions:
1807
+ formatter.start_section(
1808
+ f"Arguments matching '{search_keyword}'")
1809
+ formatter.add_arguments(matched_actions)
1810
+ formatter.end_section()
1811
+ formatter.add_text(self._json_tip)
1812
+ return formatter.format_help()
1813
+
1814
+ # No match found
1815
+ formatter.add_text(
1816
+ f"No group or arguments matching '{search_keyword}'.\n"
1817
+ "Use '--help' to see available groups or "
1818
+ "'--help=all' to see all available parameters.")
1819
+ return formatter.format_help()
1820
+
1821
+ # usage
1822
+ formatter.add_usage(self.usage, self._actions,
1823
+ self._mutually_exclusive_groups)
1824
+
1825
+ # description
1826
+ formatter.add_text(self.description)
1827
+
1828
+ # positionals, optionals and user-defined groups
1829
+ formatter.start_section("Config Groups")
1830
+ config_groups = ""
1831
+ for group in self._action_groups:
1832
+ if not group._group_actions:
1833
+ continue
1834
+ title = group.title
1835
+ description = group.description or ""
1836
+ config_groups += f"{title: <24}{description}\n"
1837
+ formatter.add_text(config_groups)
1838
+ formatter.end_section()
1839
+
1840
+ # epilog
1841
+ formatter.add_text(self.epilog)
1842
+
1843
+ # determine help from format above
1844
+ return formatter.format_help()
1845
1846
def parse_args( # type: ignore[override]
1847
self,
@@ -1807,7 +1874,11 @@ class FlexibleArgumentParser(ArgumentParser):
1874
# Convert underscores to dashes and vice versa in argument names
1875
processed_args = list[str]()
1876
for i, arg in enumerate(args):
1810
- if arg.startswith('--'):
1877
+ if arg.startswith("--help="):
1878
+ FlexibleArgumentParser._search_keyword = arg.split(
1879
+ '=', 1)[-1].lower()
1880
+ processed_args.append("--help")
1881
+ elif arg.startswith('--'):
1882
if '=' in arg:
1883
key, value = arg.split('=', 1)
1884
key = pattern.sub(repl, key, count=1)