@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / e7f27ea648
Fork 0

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)