generate_argparse.py 6.65 KB
Newer Older
1
2
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import importlib
4
5
6
7
8
9
10
import logging
import sys
from argparse import SUPPRESS, HelpFormatter
from pathlib import Path
from typing import Literal
from unittest.mock import MagicMock, patch

11
12
13
14
from pydantic_core import core_schema

logger = logging.getLogger("mkdocs")

15
16
17
18
19
20
21
ROOT_DIR = Path(__file__).parent.parent.parent.parent
ARGPARSE_DOC_DIR = ROOT_DIR / "docs/argparse"

sys.path.insert(0, str(ROOT_DIR))
sys.modules["vllm._C"] = MagicMock()


22
23
24
class PydanticMagicMock(MagicMock):
    """`MagicMock` that's able to generate pydantic-core schemas."""

25
26
27
28
29
    def __init__(self, *args, **kwargs):
        name = kwargs.pop("name", None)
        super().__init__(*args, **kwargs)
        self.__spec__ = importlib.machinery.ModuleSpec(name, None)

30
31
32
33
34
35
36
37
38
39
    def __get_pydantic_core_schema__(self, source_type, handler):
        return core_schema.any_schema()


def auto_mock(module, attr, max_mocks=50):
    """Function that automatically mocks missing modules during imports."""
    logger.info("Importing %s from %s", attr, module)
    for _ in range(max_mocks):
        try:
            # First treat attr as an attr, then as a submodule
40
            with patch("importlib.metadata.version", return_value="0.0.0"):
41
42
43
44
45
                return getattr(
                    importlib.import_module(module),
                    attr,
                    importlib.import_module(f"{module}.{attr}"),
                )
46
47
48
49
        except importlib.metadata.PackageNotFoundError as e:
            raise e
        except ModuleNotFoundError as e:
            logger.info("Mocking %s for argparse doc generation", e.name)
50
51
52
            sys.modules[e.name] = PydanticMagicMock(name=e.name)
        except Exception as e:
            logger.warning("Failed to import %s.%s: %s", module, attr, e)
53
54

    raise ImportError(
55
56
        f"Failed to import {module}.{attr} after mocking {max_mocks} imports"
    )
57
58
59
60
61
62
63
64
65
66
67
68


latency = auto_mock("vllm.benchmarks", "latency")
serve = auto_mock("vllm.benchmarks", "serve")
throughput = auto_mock("vllm.benchmarks", "throughput")
AsyncEngineArgs = auto_mock("vllm.engine.arg_utils", "AsyncEngineArgs")
EngineArgs = auto_mock("vllm.engine.arg_utils", "EngineArgs")
ChatCommand = auto_mock("vllm.entrypoints.cli.openai", "ChatCommand")
CompleteCommand = auto_mock("vllm.entrypoints.cli.openai", "CompleteCommand")
cli_args = auto_mock("vllm.entrypoints.openai", "cli_args")
run_batch = auto_mock("vllm.entrypoints.openai", "run_batch")
FlexibleArgumentParser = auto_mock("vllm.utils", "FlexibleArgumentParser")
69
70
71
72
73


class MarkdownFormatter(HelpFormatter):
    """Custom formatter that generates markdown for argument groups."""

74
    def __init__(self, prog, starting_heading_level=3):
75
        super().__init__(prog, max_help_position=float("inf"), width=float("inf"))
76
77
        self._section_heading_prefix = "#" * starting_heading_level
        self._argument_heading_prefix = "#" * (starting_heading_level + 1)
78
79
80
81
        self._markdown_output = []

    def start_section(self, heading):
        if heading not in {"positional arguments", "options"}:
82
83
            heading_md = f"\n{self._section_heading_prefix} {heading}\n\n"
            self._markdown_output.append(heading_md)
84
85
86
87
88
89
90
91
92
93
94
95
96

    def end_section(self):
        pass

    def add_text(self, text):
        if text:
            self._markdown_output.append(f"{text.strip()}\n\n")

    def add_usage(self, usage, actions, groups, prefix=None):
        pass

    def add_arguments(self, actions):
        for action in actions:
97
            if len(action.option_strings) == 0 or "--help" in action.option_strings:
98
                continue
99

100
            option_strings = f"`{'`, `'.join(action.option_strings)}`"
101
102
            heading_md = f"{self._argument_heading_prefix} {option_strings}\n\n"
            self._markdown_output.append(heading_md)
103
104

            if choices := action.choices:
105
106
107
108
109
                choices = f"`{'`, `'.join(str(c) for c in choices)}`"
                self._markdown_output.append(f"Possible choices: {choices}\n\n")
            elif (metavar := action.metavar) and isinstance(metavar, (list, tuple)):
                metavar = f"`{'`, `'.join(str(m) for m in metavar)}`"
                self._markdown_output.append(f"Possible choices: {metavar}\n\n")
110

111
112
            if action.help:
                self._markdown_output.append(f"{action.help}\n\n")
113
114
115
116
117
118
119
120
121

            if (default := action.default) != SUPPRESS:
                self._markdown_output.append(f"Default: `{default}`\n\n")

    def format_help(self):
        """Return the formatted help as markdown."""
        return "".join(self._markdown_output)


122
def create_parser(add_cli_args, **kwargs) -> FlexibleArgumentParser:
123
    """Create a parser for the given class with markdown formatting.
124

125
126
127
128
129
130
131
    Args:
        cls: The class to create a parser for
        **kwargs: Additional keyword arguments to pass to `cls.add_cli_args`.

    Returns:
        FlexibleArgumentParser: A parser with markdown formatting for the class.
    """
132
    parser = FlexibleArgumentParser(add_json_tip=False)
133
134
    parser.formatter_class = MarkdownFormatter
    with patch("vllm.config.DeviceConfig.__post_init__"):
135
136
137
        _parser = add_cli_args(parser, **kwargs)
    # add_cli_args might be in-place so return parser if _parser is None
    return _parser or parser
138
139


140
141
142
143
144
145
146
147
148
149
150
def on_startup(command: Literal["build", "gh-deploy", "serve"], dirty: bool):
    logger.info("Generating argparse documentation")
    logger.debug("Root directory: %s", ROOT_DIR.resolve())
    logger.debug("Output directory: %s", ARGPARSE_DOC_DIR.resolve())

    # Create the ARGPARSE_DOC_DIR if it doesn't exist
    if not ARGPARSE_DOC_DIR.exists():
        ARGPARSE_DOC_DIR.mkdir(parents=True)

    # Create parsers to document
    parsers = {
151
152
153
154
155
156
157
158
159
160
161
        "engine_args": create_parser(EngineArgs.add_cli_args),
        "async_engine_args": create_parser(
            AsyncEngineArgs.add_cli_args, async_args_only=True
        ),
        "serve": create_parser(cli_args.make_arg_parser),
        "chat": create_parser(ChatCommand.add_cli_args),
        "complete": create_parser(CompleteCommand.add_cli_args),
        "bench_latency": create_parser(latency.add_cli_args),
        "bench_throughput": create_parser(throughput.add_cli_args),
        "bench_serve": create_parser(serve.add_cli_args),
        "run-batch": create_parser(run_batch.make_arg_parser),
162
163
164
165
166
    }

    # Generate documentation for each parser
    for stem, parser in parsers.items():
        doc_path = ARGPARSE_DOC_DIR / f"{stem}.md"
167
168
        # Specify encoding for building on Windows
        with open(doc_path, "w", encoding="utf-8") as f:
169
            f.write(super(type(parser), parser).format_help())
170
        logger.info("Argparse generated: %s", doc_path.relative_to(ROOT_DIR))