Improve --help for enhanced user experience (#24903)

Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
This commit is contained in:
Harry Mellor 2025-09-25 00:08:18 +01:00 committed by GitHub
parent 1f29141258
commit e7f27ea648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 114 additions and 137 deletions

View File

@ -168,5 +168,5 @@ def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
doc_path = ARGPARSE_DOC_DIR / f"{stem}.md" doc_path = ARGPARSE_DOC_DIR / f"{stem}.md"
# Specify encoding for building on Windows # Specify encoding for building on Windows
with open(doc_path, "w", encoding="utf-8") as f: with open(doc_path, "w", encoding="utf-8") as f:
f.write(parser.format_help()) f.write(super(type(parser), parser).format_help())
logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR)) logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR))

View File

@ -156,8 +156,8 @@ def is_online_quantization(quantization: Any) -> bool:
NEEDS_HELP = ( NEEDS_HELP = (
"--help" in (argv := sys.argv) # vllm SUBCOMMAND --help any("--help" in arg for arg in sys.argv) # vllm SUBCOMMAND --help
or (argv0 := argv[0]).endswith("mkdocs") # mkdocs SUBCOMMAND or (argv0 := sys.argv[0]).endswith("mkdocs") # mkdocs SUBCOMMAND
or argv0.endswith("mkdocs/__main__.py") # python -m mkdocs SUBCOMMAND or argv0.endswith("mkdocs/__main__.py") # python -m mkdocs SUBCOMMAND
) )

View File

@ -8,8 +8,7 @@ import typing
from vllm.entrypoints.cli.benchmark.base import BenchmarkSubcommandBase from vllm.entrypoints.cli.benchmark.base import BenchmarkSubcommandBase
from vllm.entrypoints.cli.types import CLISubcommand from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG, from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
show_filtered_argument_or_group_from_help)
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from vllm.utils import FlexibleArgumentParser from vllm.utils import FlexibleArgumentParser
@ -33,9 +32,8 @@ class BenchmarkSubcommand(CLISubcommand):
subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser: subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
bench_parser = subparsers.add_parser( bench_parser = subparsers.add_parser(
self.name, self.name,
help=self.help,
description=self.help, description=self.help,
usage="vllm bench <bench_type> [options]") usage=f"vllm {self.name} <bench_type> [options]")
bench_subparsers = bench_parser.add_subparsers(required=True, bench_subparsers = bench_parser.add_subparsers(required=True,
dest="bench_type") dest="bench_type")
@ -44,13 +42,12 @@ class BenchmarkSubcommand(CLISubcommand):
cmd_cls.name, cmd_cls.name,
help=cmd_cls.help, help=cmd_cls.help,
description=cmd_cls.help, description=cmd_cls.help,
usage=f"vllm bench {cmd_cls.name} [options]", usage=f"vllm {self.name} {cmd_cls.name} [options]",
) )
cmd_subparser.set_defaults(dispatch_function=cmd_cls.cmd) cmd_subparser.set_defaults(dispatch_function=cmd_cls.cmd)
cmd_cls.add_cli_args(cmd_subparser) cmd_cls.add_cli_args(cmd_subparser)
show_filtered_argument_or_group_from_help(cmd_subparser, cmd_subparser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
["bench", cmd_cls.name]) subcmd=f"{self.name} {cmd_cls.name}")
cmd_subparser.epilog = VLLM_SUBCMD_PARSER_EPILOG
return bench_parser return bench_parser

View File

@ -30,7 +30,7 @@ def main():
parser = FlexibleArgumentParser( parser = FlexibleArgumentParser(
description="vLLM CLI", description="vLLM CLI",
epilog=VLLM_SUBCMD_PARSER_EPILOG, epilog=VLLM_SUBCMD_PARSER_EPILOG.format(subcmd="[subcommand]"),
) )
parser.add_argument( parser.add_argument(
'-v', '-v',

View File

@ -9,8 +9,7 @@ import importlib.metadata
import typing import typing
from vllm.entrypoints.cli.types import CLISubcommand from vllm.entrypoints.cli.types import CLISubcommand
from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG, from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
show_filtered_argument_or_group_from_help)
from vllm.logger import init_logger from vllm.logger import init_logger
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -50,7 +49,7 @@ class RunBatchSubcommand(CLISubcommand):
from vllm.entrypoints.openai.run_batch import make_arg_parser from vllm.entrypoints.openai.run_batch import make_arg_parser
run_batch_parser = subparsers.add_parser( run_batch_parser = subparsers.add_parser(
"run-batch", self.name,
help="Run batch prompts and write results to file.", help="Run batch prompts and write results to file.",
description=( description=(
"Run batch prompts using vLLM's OpenAI-compatible API.\n" "Run batch prompts using vLLM's OpenAI-compatible API.\n"
@ -59,9 +58,8 @@ class RunBatchSubcommand(CLISubcommand):
"vllm run-batch -i INPUT.jsonl -o OUTPUT.jsonl --model <model>", "vllm run-batch -i INPUT.jsonl -o OUTPUT.jsonl --model <model>",
) )
run_batch_parser = make_arg_parser(run_batch_parser) run_batch_parser = make_arg_parser(run_batch_parser)
show_filtered_argument_or_group_from_help(run_batch_parser, run_batch_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
["run-batch"]) subcmd=self.name)
run_batch_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG
return run_batch_parser return run_batch_parser

View File

@ -14,8 +14,7 @@ from vllm.entrypoints.openai.api_server import (run_server, run_server_worker,
setup_server) setup_server)
from vllm.entrypoints.openai.cli_args import (make_arg_parser, from vllm.entrypoints.openai.cli_args import (make_arg_parser,
validate_parsed_serve_args) validate_parsed_serve_args)
from vllm.entrypoints.utils import (VLLM_SUBCMD_PARSER_EPILOG, from vllm.entrypoints.utils import VLLM_SUBCMD_PARSER_EPILOG
show_filtered_argument_or_group_from_help)
from vllm.logger import init_logger from vllm.logger import init_logger
from vllm.usage.usage_lib import UsageContext from vllm.usage.usage_lib import UsageContext
from vllm.utils import (FlexibleArgumentParser, decorate_logs, get_tcp_uri, from vllm.utils import (FlexibleArgumentParser, decorate_logs, get_tcp_uri,
@ -29,6 +28,14 @@ from vllm.v1.utils import (APIServerProcessManager,
logger = init_logger(__name__) logger = init_logger(__name__)
DESCRIPTION = """Launch a local OpenAI-compatible API server to serve LLM
completions via HTTP. Defaults to Qwen/Qwen3-0.6B if no model is specified.
Search by using: `--help=<ConfigGroup>` to explore options by section (e.g.,
--help=ModelConfig, --help=Frontend)
Use `--help=all` to show all available flags at once.
"""
class ServeSubcommand(CLISubcommand): class ServeSubcommand(CLISubcommand):
"""The `serve` subcommand for the vLLM CLI. """ """The `serve` subcommand for the vLLM CLI. """
@ -56,14 +63,13 @@ class ServeSubcommand(CLISubcommand):
self, self,
subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser: subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
serve_parser = subparsers.add_parser( serve_parser = subparsers.add_parser(
"serve", self.name,
help="Start the vLLM OpenAI Compatible API server.", description=DESCRIPTION,
description="Start the vLLM OpenAI Compatible API server.",
usage="vllm serve [model_tag] [options]") usage="vllm serve [model_tag] [options]")
serve_parser = make_arg_parser(serve_parser) serve_parser = make_arg_parser(serve_parser)
show_filtered_argument_or_group_from_help(serve_parser, ["serve"]) serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG.format(
serve_parser.epilog = VLLM_SUBCMD_PARSER_EPILOG subcmd=self.name)
return serve_parser return serve_parser

View File

@ -1,13 +1,11 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import argparse
import asyncio import asyncio
import dataclasses import dataclasses
import functools import functools
import os import os
import subprocess from argparse import Namespace
import sys
from typing import Any, Optional, Union from typing import Any, Optional, Union
from fastapi import Request from fastapi import Request
@ -25,13 +23,10 @@ from vllm.utils import FlexibleArgumentParser
logger = init_logger(__name__) logger = init_logger(__name__)
VLLM_SUBCMD_PARSER_EPILOG = ( VLLM_SUBCMD_PARSER_EPILOG = (
"Tip: Use `vllm [serve|run-batch|bench <bench_type>] " "For full list: vllm {subcmd} --help=all\n"
"--help=<keyword>` to explore arguments from help.\n" "For a section: vllm {subcmd} --help=ModelConfig (case-insensitive)\n" # noqa: E501
" - To view a argument group: --help=ModelConfig\n" "For a flag: vllm {subcmd} --help=max-model-len (_ or - accepted)\n" # noqa: E501
" - To view a single argument: --help=max-num-seqs\n" "Documentation: https://docs.vllm.ai\n")
" - To search by keyword: --help=max\n"
" - To list all groups: --help=listgroup\n"
" - To view help with pager: --help=page")
async def listen_for_disconnect(request: Request) -> None: async def listen_for_disconnect(request: Request) -> None:
@ -196,96 +191,6 @@ def _validate_truncation_size(
return truncate_prompt_tokens return truncate_prompt_tokens
def _output_with_pager(text: str):
"""Output text using scrolling view if available and appropriate."""
pagers = ['less -R', 'more']
for pager_cmd in pagers:
try:
proc = subprocess.Popen(pager_cmd.split(),
stdin=subprocess.PIPE,
text=True)
proc.communicate(input=text)
return
except (subprocess.SubprocessError, OSError, FileNotFoundError):
continue
# No pager worked, fall back to normal print
print(text)
def show_filtered_argument_or_group_from_help(parser: argparse.ArgumentParser,
subcommand_name: list[str]):
# Only handle --help=<keyword> for the current subcommand.
# Since subparser_init() runs for all subcommands during CLI setup,
# we skip processing if the subcommand name is not in sys.argv.
# sys.argv[0] is the program name. The subcommand follows.
# e.g., for `vllm bench latency`,
# sys.argv is `['vllm', 'bench', 'latency', ...]`
# and subcommand_name is "bench latency".
if len(sys.argv) <= len(subcommand_name) or sys.argv[
1:1 + len(subcommand_name)] != subcommand_name:
return
for arg in sys.argv:
if arg.startswith('--help='):
search_keyword = arg.split('=', 1)[1]
# Enable paged view for full help
if search_keyword == 'page':
help_text = parser.format_help()
_output_with_pager(help_text)
sys.exit(0)
# List available groups
if search_keyword == 'listgroup':
output_lines = ["\nAvailable argument groups:"]
for group in parser._action_groups:
if group.title and not group.title.startswith(
"positional arguments"):
output_lines.append(f" - {group.title}")
if group.description:
output_lines.append(" " +
group.description.strip())
output_lines.append("")
_output_with_pager("\n".join(output_lines))
sys.exit(0)
# For group search
formatter = parser._get_formatter()
for group in parser._action_groups:
if group.title and group.title.lower() == search_keyword.lower(
):
formatter.start_section(group.title)
formatter.add_text(group.description)
formatter.add_arguments(group._group_actions)
formatter.end_section()
_output_with_pager(formatter.format_help())
sys.exit(0)
# For single arg
matched_actions = []
for group in parser._action_groups:
for action in group._group_actions:
# search option name
if any(search_keyword.lower() in opt.lower()
for opt in action.option_strings):
matched_actions.append(action)
if matched_actions:
header = f"\nParameters matching '{search_keyword}':\n"
formatter = parser._get_formatter()
formatter.add_arguments(matched_actions)
_output_with_pager(header + formatter.format_help())
sys.exit(0)
print(f"\nNo group or parameter matching '{search_keyword}'")
print("Tip: use `--help=listgroup` to view all groups.")
sys.exit(1)
def get_max_tokens(max_model_len: int, request: Union[ChatCompletionRequest, def get_max_tokens(max_model_len: int, request: Union[ChatCompletionRequest,
CompletionRequest], CompletionRequest],
input_length: int, default_sampling_params: dict) -> int: input_length: int, default_sampling_params: dict) -> int:
@ -301,11 +206,11 @@ def get_max_tokens(max_model_len: int, request: Union[ChatCompletionRequest,
if val is not None) if val is not None)
def log_non_default_args(args: Union[argparse.Namespace, EngineArgs]): def log_non_default_args(args: Union[Namespace, EngineArgs]):
non_default_args = {} non_default_args = {}
# Handle argparse.Namespace # Handle Namespace
if isinstance(args, argparse.Namespace): if isinstance(args, Namespace):
parser = make_arg_parser(FlexibleArgumentParser()) parser = make_arg_parser(FlexibleArgumentParser())
for arg, default in vars(parser.parse_args([])).items(): for arg, default in vars(parser.parse_args([])).items():
if default != getattr(args, arg): if default != getattr(args, arg):
@ -323,6 +228,6 @@ def log_non_default_args(args: Union[argparse.Namespace, EngineArgs]):
non_default_args["model"] = default_args.model non_default_args["model"] = default_args.model
else: else:
raise TypeError("Unsupported argument type. " \ raise TypeError("Unsupported argument type. " \
"Must be argparse.Namespace or EngineArgs instance.") "Must be Namespace or EngineArgs instance.")
logger.info("non-default args: %s", non_default_args) logger.info("non-default args: %s", non_default_args)

View File

@ -1720,6 +1720,7 @@ class FlexibleArgumentParser(ArgumentParser):
"Additionally, list elements can be passed individually using +:\n" "Additionally, list elements can be passed individually using +:\n"
' --json-arg \'{"key4": ["value3", "value4", "value5"]}\'\n' ' --json-arg \'{"key4": ["value3", "value4", "value5"]}\'\n'
" --json-arg.key4+ value3 --json-arg.key4+=\'value4,value5\'\n\n") " --json-arg.key4+ value3 --json-arg.key4+=\'value4,value5\'\n\n")
_search_keyword: Optional[str] = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Set the default "formatter_class" to SortedHelpFormatter # Set the default "formatter_class" to SortedHelpFormatter
@ -1768,13 +1769,79 @@ class FlexibleArgumentParser(ArgumentParser):
self._action_groups.append(group) self._action_groups.append(group)
return group return group
def format_help(self) -> str: def format_help(self):
# Add tip about JSON arguments to the epilog # Only use custom help formatting for bottom level parsers
epilog = self.epilog or "" if self._subparsers is not None:
if (self.add_json_tip return super().format_help()
and not epilog.startswith(FlexibleArgumentParser._json_tip)):
self.epilog = FlexibleArgumentParser._json_tip + epilog formatter = self._get_formatter()
return super().format_help()
# Handle keyword search of the args
if (search_keyword := self._search_keyword) is not None:
# Normalise the search keyword
search_keyword = search_keyword.lower().replace("_", "-")
# Return full help if searching for 'all'
if search_keyword == 'all':
self.epilog = self._json_tip
return super().format_help()
# Return group help if searching for a group title
for group in self._action_groups:
if group.title and group.title.lower() == search_keyword:
formatter.start_section(group.title)
formatter.add_text(group.description)
formatter.add_arguments(group._group_actions)
formatter.end_section()
formatter.add_text(self._json_tip)
return formatter.format_help()
# Return matched args if searching for an arg name
matched_actions = []
for group in self._action_groups:
for action in group._group_actions:
# search option name
if any(search_keyword in opt.lower()
for opt in action.option_strings):
matched_actions.append(action)
if matched_actions:
formatter.start_section(
f"Arguments matching '{search_keyword}'")
formatter.add_arguments(matched_actions)
formatter.end_section()
formatter.add_text(self._json_tip)
return formatter.format_help()
# No match found
formatter.add_text(
f"No group or arguments matching '{search_keyword}'.\n"
"Use '--help' to see available groups or "
"'--help=all' to see all available parameters.")
return formatter.format_help()
# usage
formatter.add_usage(self.usage, self._actions,
self._mutually_exclusive_groups)
# description
formatter.add_text(self.description)
# positionals, optionals and user-defined groups
formatter.start_section("Config Groups")
config_groups = ""
for group in self._action_groups:
if not group._group_actions:
continue
title = group.title
description = group.description or ""
config_groups += f"{title: <24}{description}\n"
formatter.add_text(config_groups)
formatter.end_section()
# epilog
formatter.add_text(self.epilog)
# determine help from format above
return formatter.format_help()
def parse_args( # type: ignore[override] def parse_args( # type: ignore[override]
self, self,
@ -1807,7 +1874,11 @@ class FlexibleArgumentParser(ArgumentParser):
# Convert underscores to dashes and vice versa in argument names # Convert underscores to dashes and vice versa in argument names
processed_args = list[str]() processed_args = list[str]()
for i, arg in enumerate(args): for i, arg in enumerate(args):
if arg.startswith('--'): if arg.startswith("--help="):
FlexibleArgumentParser._search_keyword = arg.split(
'=', 1)[-1].lower()
processed_args.append("--help")
elif arg.startswith('--'):
if '=' in arg: if '=' in arg:
key, value = arg.split('=', 1) key, value = arg.split('=', 1)
key = pattern.sub(repl, key, count=1) key = pattern.sub(repl, key, count=1)