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
"""