@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / c05596f1a3
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 15, 03:30 UTC

[Perf] Validate @config in pre-commit instead of dynamically (#20200)

Signed-off-by: Lionel Villard <villard@us.ibm.com>

Lionel Villard committed Jul 1, 2025 at 05:10 UTC c05596f1a350f3d993c467959ed02492141c2527
6 files changed +220 -57
.pre-commit-config.yaml
+7
@@ -160,6 +160,13 @@ repos:
160 types: [python]
161 pass_filenames: false
162 additional_dependencies: [pathspec, regex]
163 + - id: validate-config
164 + name: Validate configuration has default values and that each field has a docstring
165 + entry: python tools/validate_config.py
166 + language: python
167 + types: [python]
168 + pass_filenames: true
169 + files: vllm/config.py|tests/test_config.py
170 # Keep `suggestion` last
171 - id: suggestion
172 name: Suggestion
tests/test_config.py
+1 -34
@@ -2,49 +2,16 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from dataclasses import MISSING, Field, asdict, dataclass, field
5 -from typing import Literal, Union
5
6 import pytest
7
8 from vllm.compilation.backends import VllmBackend
9 from vllm.config import (LoadConfig, ModelConfig, PoolerConfig, VllmConfig,
11 - config, get_field)
10 + get_field)
11 from vllm.model_executor.layers.pooler import PoolingType
12 from vllm.platforms import current_platform
13
14
16 -class _TestConfig1:
17 - pass
18 -
19 -
20 -@dataclass
21 -class _TestConfig2:
22 - a: int
23 - """docstring"""
24 -
25 -
26 -@dataclass
27 -class _TestConfig3:
28 - a: int = 1
29 -
30 -
31 -@dataclass
32 -class _TestConfig4:
33 - a: Union[Literal[1], Literal[2]] = 1
34 - """docstring"""
35 -
36 -
37 -@pytest.mark.parametrize(("test_config", "expected_error"), [
38 - (_TestConfig1, "must be a dataclass"),
39 - (_TestConfig2, "must have a default"),
40 - (_TestConfig3, "must have a docstring"),
41 - (_TestConfig4, "must use a single Literal"),
42 -])
43 -def test_config(test_config, expected_error):
44 - with pytest.raises(Exception, match=expected_error):
45 - config(test_config)
46 -
47 -
15 def test_compile_config_repr_succeeds():
16 # setup: VllmBackend mutates the config object
17 config = VllmConfig()
tests/tools/__init__.py
tests/tools/test_config_validator.py new
+49
@@ -0,0 +1,49 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import ast
5 +
6 +import pytest
7 +
8 +from tools.validate_config import validate_ast
9 +
10 +_TestConfig1 = '''
11 +@config
12 +class _TestConfig1:
13 + pass
14 +'''
15 +
16 +_TestConfig2 = '''
17 +@config
18 +@dataclass
19 +class _TestConfig2:
20 + a: int
21 + """docstring"""
22 +'''
23 +
24 +_TestConfig3 = '''
25 +@config
26 +@dataclass
27 +class _TestConfig3:
28 + a: int = 1
29 +'''
30 +
31 +_TestConfig4 = '''
32 +@config
33 +@dataclass
34 +class _TestConfig4:
35 + a: Union[Literal[1], Literal[2]] = 1
36 + """docstring"""
37 +'''
38 +
39 +
40 +@pytest.mark.parametrize(("test_config", "expected_error"), [
41 + (_TestConfig1, "must be a dataclass"),
42 + (_TestConfig2, "must have a default"),
43 + (_TestConfig3, "must have a docstring"),
44 + (_TestConfig4, "must use a single Literal"),
45 +])
46 +def test_config(test_config, expected_error):
47 + tree = ast.parse(test_config)
48 + with pytest.raises(Exception, match=expected_error):
49 + validate_ast(tree)
tools/validate_config.py new
+158
@@ -0,0 +1,158 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""
4 +Ensures all fields in a config dataclass have default values
5 +and that each field has a docstring.
6 +"""
7 +
8 +import ast
9 +import inspect
10 +import sys
11 +
12 +
13 +def get_attr_docs(cls_node: ast.ClassDef) -> dict[str, str]:
14 + """
15 + Get any docstrings placed after attribute assignments in a class body.
16 +
17 + Adapted from https://davidism.com/attribute-docstrings/
18 + https://davidism.com/mit-license/
19 + """
20 +
21 + def pairwise(iterable):
22 + """
23 + Manually implement https://docs.python.org/3/library/itertools.html#itertools.pairwise
24 +
25 + Can be removed when Python 3.9 support is dropped.
26 + """
27 + iterator = iter(iterable)
28 + a = next(iterator, None)
29 +
30 + for b in iterator:
31 + yield a, b
32 + a = b
33 +
34 + out = {}
35 +
36 + # Consider each pair of nodes.
37 + for a, b in pairwise(cls_node.body):
38 + # Must be an assignment then a constant string.
39 + if (not isinstance(a, (ast.Assign, ast.AnnAssign))
40 + or not isinstance(b, ast.Expr)
41 + or not isinstance(b.value, ast.Constant)
42 + or not isinstance(b.value.value, str)):
43 + continue
44 +
45 + doc = inspect.cleandoc(b.value.value)
46 +
47 + # An assignment can have multiple targets (a = b = v), but an
48 + # annotated assignment only has one target.
49 + targets = a.targets if isinstance(a, ast.Assign) else [a.target]
50 +
51 + for target in targets:
52 + # Must be assigning to a plain name.
53 + if not isinstance(target, ast.Name):
54 + continue
55 +
56 + out[target.id] = doc
57 +
58 + return out
59 +
60 +
61 +class ConfigValidator(ast.NodeVisitor):
62 +
63 + def __init__(self):
64 + ...
65 +
66 + def visit_ClassDef(self, node):
67 + # Validate class with both @config and @dataclass decorators
68 + decorators = [
69 + id for d in node.decorator_list if (isinstance(d, ast.Name) and (
70 + (id := d.id) == 'config' or id == 'dataclass')) or
71 + (isinstance(d, ast.Call) and (isinstance(d.func, ast.Name) and
72 + (id := d.func.id) == 'dataclass'))
73 + ]
74 +
75 + if set(decorators) == {'config', 'dataclass'}:
76 + validate_class(node)
77 + elif set(decorators) == {'config'}:
78 + fail(
79 + f"Class {node.name} with config decorator must be a dataclass.",
80 + node)
81 +
82 + self.generic_visit(node)
83 +
84 +
85 +def validate_class(class_node: ast.ClassDef):
86 + attr_docs = get_attr_docs(class_node)
87 +
88 + for stmt in class_node.body:
89 + # A field is defined as a class variable that has a type annotation.
90 + if isinstance(stmt, ast.AnnAssign):
91 + # Skip ClassVar
92 + # see https://docs.python.org/3/library/dataclasses.html#class-variables
93 + if isinstance(stmt.annotation, ast.Subscript) and isinstance(
94 + stmt.annotation.value,
95 + ast.Name) and stmt.annotation.value.id == "ClassVar":
96 + continue
97 +
98 + if isinstance(stmt.target, ast.Name):
99 + field_name = stmt.target.id
100 + if stmt.value is None:
101 + fail(
102 + f"Field '{field_name}' in {class_node.name} must have "
103 + "a default value.", stmt)
104 +
105 + if field_name not in attr_docs:
106 + fail(
107 + f"Field '{field_name}' in {class_node.name} must have "
108 + "a docstring.", stmt)
109 +
110 + if isinstance(stmt.annotation, ast.Subscript) and \
111 + isinstance(stmt.annotation.value, ast.Name) \
112 + and stmt.annotation.value.id == "Union" and \
113 + isinstance(stmt.annotation.slice, ast.Tuple):
114 + args = stmt.annotation.slice.elts
115 + literal_args = [
116 + arg for arg in args
117 + if isinstance(arg, ast.Subscript) and isinstance(
118 + arg.value, ast.Name) and arg.value.id == "Literal"
119 + ]
120 + if len(literal_args) > 1:
121 + fail(
122 + f"Field '{field_name}' in {class_node.name} must "
123 + "use a single "
124 + "Literal type. Please use 'Literal[Literal1, "
125 + "Literal2]' instead of 'Union[Literal1, Literal2]'"
126 + ".", stmt)
127 +
128 +
129 +def validate_ast(tree: ast.stmt):
130 + ConfigValidator().visit(tree)
131 +
132 +
133 +def validate_file(file_path: str):
134 + try:
135 + print(f"validating {file_path} config dataclasses ", end="")
136 + with open(file_path, encoding="utf-8") as f:
137 + source = f.read()
138 +
139 + tree = ast.parse(source, filename=file_path)
140 + validate_ast(tree)
141 + except ValueError as e:
142 + print(e)
143 + SystemExit(2)
144 + else:
145 + print("✅")
146 +
147 +
148 +def fail(message: str, node: ast.stmt):
149 + raise ValueError(f"❌ line({node.lineno}): {message}")
150 +
151 +
152 +def main():
153 + for filename in sys.argv[1:]:
154 + validate_file(filename)
155 +
156 +
157 +if __name__ == "__main__":
158 + main()
vllm/config.py
+5 -23
@@ -18,7 +18,7 @@ from functools import cached_property
18 from importlib.util import find_spec
19 from pathlib import Path
20 from typing import (TYPE_CHECKING, Any, Callable, ClassVar, Literal, Optional,
21 - Protocol, TypeVar, Union, cast, get_args, get_origin)
21 + Protocol, TypeVar, Union, cast, get_args)
22
23 import regex as re
24 import torch
@@ -193,28 +193,10 @@ def config(cls: ConfigT) -> ConfigT:
193 (i.e. `ConfigT(**json.loads(cli_arg))`). However, if a particular `ConfigT`
194 requires custom construction from CLI (i.e. `CompilationConfig`), it can
195 have a `from_cli` method, which will be called instead.
196 - """
197 - if not is_dataclass(cls):
198 - raise TypeError("The decorated class must be a dataclass.")
199 - attr_docs = get_attr_docs(cls)
200 - for f in fields(cls):
201 - if f.init and f.default is MISSING and f.default_factory is MISSING:
202 - raise ValueError(
203 - f"Field '{f.name}' in {cls.__name__} must have a default value."
204 - )
196
206 - if f.name not in attr_docs:
207 - raise ValueError(
208 - f"Field '{f.name}' in {cls.__name__} must have a docstring.")
209 -
210 - if get_origin(f.type) is Union:
211 - args = get_args(f.type)
212 - literal_args = [arg for arg in args if get_origin(arg) is Literal]
213 - if len(literal_args) > 1:
214 - raise ValueError(
215 - f"Field '{f.name}' in {cls.__name__} must use a single "
216 - "Literal type. Please use 'Literal[Literal1, Literal2]' "
217 - "instead of 'Union[Literal1, Literal2]'.")
197 + Config validation is performed by the tools/validate_config.py
198 + script, which is invoked during the pre-commit checks.
199 + """
200 return cls
201
202
@@ -1798,7 +1780,7 @@ class ParallelConfig:
1780 eplb_step_interval: int = 3000
1781 """
1782 Interval for rearranging experts in expert parallelism.
1801 -
1783 +
1784 Note that if this is greater than the EPLB window size, only the metrics
1785 of the last `eplb_window_size` steps will be used for rearranging experts.
1786 """