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 9, 00:00 UTC
Turn `@config` into a `dataclass_transform` (#31541)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Harry Mellor committed
Feb 3, 2026 at 17:40 UTC
61e632aea15f76fd1c46354b00f9cac62cd28c4e
32 files changed
+155
-193
tests/engine/test_arg_utils.py
+12
-14
@@ -3,11 +3,11 @@
3
4
import json
5
from argparse import ArgumentError
6
-from contextlib import nullcontext
7
-from dataclasses import dataclass, field
6
+from contextlib import AbstractContextManager, nullcontext
7
from typing import Annotated, Literal
8
9
import pytest
10
+from pydantic import Field
11
12
from vllm.config import AttentionConfig, CompilationConfig, config
13
from vllm.engine.arg_utils import (
@@ -96,7 +96,7 @@ def test_get_type(type_hints, type, expected):
96
],
97
)
98
def test_literal_to_kwargs(type_hints, expected):
99
- context = nullcontext()
99
+ context: AbstractContextManager[object] = nullcontext()
100
if expected is Exception:
101
context = pytest.raises(expected)
102
with context:
@@ -104,14 +104,12 @@ def test_literal_to_kwargs(type_hints, expected):
104
105
106
@config
107
-@dataclass
107
class NestedConfig:
108
field: int = 1
109
"""field"""
110
111
112
@config
114
-@dataclass
113
class DummyConfig:
114
regular_bool: bool = True
115
"""Regular bool with default True"""
@@ -119,23 +117,23 @@ class DummyConfig:
117
"""Optional bool with default None"""
118
optional_literal: Literal["x", "y"] | None = None
119
"""Optional literal with default None"""
122
- tuple_n: tuple[int, ...] = field(default_factory=lambda: (1, 2, 3))
120
+ tuple_n: tuple[int, ...] = Field(default_factory=lambda: (1, 2, 3))
121
"""Tuple with variable length"""
124
- tuple_2: tuple[int, int] = field(default_factory=lambda: (1, 2))
122
+ tuple_2: tuple[int, int] = Field(default_factory=lambda: (1, 2))
123
"""Tuple with fixed length"""
126
- list_n: list[int] = field(default_factory=lambda: [1, 2, 3])
124
+ list_n: list[int] = Field(default_factory=lambda: [1, 2, 3])
125
"""List with variable length"""
128
- list_literal: list[Literal[1, 2]] = field(default_factory=list)
126
+ list_literal: list[Literal[1, 2]] = Field(default_factory=list)
127
"""List with literal choices"""
130
- list_union: list[str | type[object]] = field(default_factory=list)
128
+ list_union: list[str | type[object]] = Field(default_factory=list)
129
"""List with union type"""
132
- set_n: set[int] = field(default_factory=lambda: {1, 2, 3})
130
+ set_n: set[int] = Field(default_factory=lambda: {1, 2, 3})
131
"""Set with variable length"""
132
literal_literal: Literal[Literal[1], Literal[2]] = 1
133
"""Literal of literals with default 1"""
136
- json_tip: dict = field(default_factory=dict)
134
+ json_tip: dict = Field(default_factory=dict)
135
"""Dict which will be JSON in CLI"""
138
- nested_config: NestedConfig = field(default_factory=NestedConfig)
136
+ nested_config: NestedConfig = Field(default_factory=NestedConfig)
137
"""Nested config"""
138
139
@@ -195,7 +193,7 @@ def test_get_kwargs():
193
json_tip = "Should either be a valid JSON string or JSON keys"
194
assert json_tip in kwargs["json_tip"]["help"]
195
# nested config should construct the nested config
198
- assert kwargs["nested_config"]["type"]('{"field": 2}') == NestedConfig(2)
196
+ assert kwargs["nested_config"]["type"]('{"field": 2}') == NestedConfig(2) # type: ignore[call-arg]
197
198
199
@pytest.mark.parametrize(
tests/test_config.py
+1
-4
@@ -66,9 +66,6 @@ class _TestConfigFields:
66
67
68
def test_get_field():
69
- with pytest.raises(ValueError):
70
- get_field(_TestConfigFields, "a")
71
-
69
b = get_field(_TestConfigFields, "b")
70
assert isinstance(b, Field)
71
assert b.default is MISSING
@@ -188,7 +185,7 @@ def test_get_pooling_config():
185
)
186
def test_get_pooling_config_from_args():
187
model_id = "sentence-transformers/all-MiniLM-L12-v2"
191
- pooler_config = PoolerConfig(seq_pooling_type="CLS", normalize=True)
188
+ pooler_config = PoolerConfig(seq_pooling_type="CLS", use_activation=False)
189
model_config = ModelConfig(model_id, pooler_config=pooler_config)
190
191
assert asdict(model_config.pooler_config) == asdict(pooler_config)
tests/tools/test_config_validator.py
+8
-18
@@ -7,31 +7,22 @@ import pytest
7
8
from tools.pre_commit.validate_config import validate_ast
9
10
-_TestConfig1 = """
10
+_TestConfig1 = '''
11
@config
12
class _TestConfig1:
13
- pass
14
-"""
15
-
16
-_TestConfig2 = '''
17
-@config
18
-@dataclass
19
-class _TestConfig2:
13
a: int
14
"""docstring"""
15
'''
16
24
-_TestConfig3 = """
17
+_TestConfig2 = """
18
@config
26
-@dataclass
27
-class _TestConfig3:
19
+class _TestConfig2:
20
a: int = 1
21
"""
22
31
-_TestConfig4 = '''
23
+_TestConfig3 = '''
24
@config
33
-@dataclass
34
-class _TestConfig4:
25
+class _TestConfig3:
26
a: Union[Literal[1], Literal[2]] = 1
27
"""docstring"""
28
'''
@@ -40,10 +31,9 @@ class _TestConfig4:
31
@pytest.mark.parametrize(
32
("test_config", "expected_error"),
33
[
43
- (_TestConfig1, "must be a dataclass"),
44
- (_TestConfig2, "must have a default"),
45
- (_TestConfig3, "must have a docstring"),
46
- (_TestConfig4, "must use a single Literal"),
34
+ (_TestConfig1, "must have a default"),
35
+ (_TestConfig2, "must have a docstring"),
36
+ (_TestConfig3, "must use a single Literal"),
37
],
38
)
39
def test_config(test_config, expected_error):
tests/v1/e2e/test_spec_decode.py
+1
-1
@@ -766,8 +766,8 @@ def assert_draft_model_correctness(args: ArgsTest, enforce_eager: bool):
766
"max_model_len": args.max_model_len,
767
"enforce_eager": enforce_eager,
768
"draft_tensor_parallel_size": args.draft_tensor_parallel_size,
769
- "max_num_seqs": 100, # limit cudagraph capture runtime
769
},
770
+ max_num_seqs=100, # limit cudagraph capture runtime
771
max_model_len=args.max_model_len,
772
gpu_memory_utilization=args.gpu_memory_utilization,
773
tensor_parallel_size=args.target_tensor_parallel_size,
tests/v1/structured_output/test_backend_guidance.py
+2
-5
@@ -26,11 +26,8 @@ def test_backend_guidance_rollback_terminated():
26
# guidance backend. In that case we are in a stopped state, but
27
# it should be reverted in case EOS is not accepted by the target
28
# model.
29
- vllm_config = VllmConfig(
30
- decoding_config=StructuredOutputsConfig(
31
- backend="guidance",
32
- )
33
- )
29
+ structured_outputs_config = StructuredOutputsConfig(backend="guidance")
30
+ vllm_config = VllmConfig(structured_outputs_config=structured_outputs_config)
31
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER)
32
33
backend = GuidanceBackend(
tools/pre_commit/validate_config.py
+11
-17
@@ -54,24 +54,18 @@ class ConfigValidator(ast.NodeVisitor):
54
def __init__(self): ...
55
56
def visit_ClassDef(self, node):
57
- # Validate class with both @config and @dataclass decorators
58
- decorators = [
59
- id
60
- for d in node.decorator_list
61
- if (
62
- isinstance(d, ast.Name)
63
- and ((id := d.id) == "config" or id == "dataclass")
64
- )
65
- or (
66
- isinstance(d, ast.Call)
67
- and (isinstance(d.func, ast.Name) and (id := d.func.id) == "dataclass")
68
- )
69
- ]
70
-
71
- if set(decorators) == {"config", "dataclass"}:
57
+ # Validate classes with a @config decorator
58
+ decorators = set()
59
+ for decorator in node.decorator_list:
60
+ if isinstance(decorator, ast.Call):
61
+ decorator = decorator.func
62
+ if isinstance(decorator, ast.Name) and decorator.id == "config":
63
+ decorators.add(decorator.id)
64
+
65
+ if decorators == {"config"}:
66
validate_class(node)
73
- elif set(decorators) == {"config"}:
74
- fail(f"Class {node.name} with config decorator must be a dataclass.", node)
67
+ elif "config" in decorators:
68
+ fail(f"config decorator for {node.name} should be used alone", node)
69
70
self.generic_visit(node)
71
vllm/config/__init__.py
+2
@@ -36,6 +36,7 @@ from vllm.config.utils import (
36
config,
37
get_attr_docs,
38
is_init_field,
39
+ replace,
40
update_config,
41
)
42
from vllm.config.vllm import (
@@ -101,6 +102,7 @@ __all__ = [
102
"config",
103
"get_attr_docs",
104
"is_init_field",
105
+ "replace",
106
"update_config",
107
# From vllm.config.vllm
108
"VllmConfig",
vllm/config/attention.py
-2
@@ -4,14 +4,12 @@
4
from typing import Any, Literal
5
6
from pydantic import field_validator
7
-from pydantic.dataclasses import dataclass
7
8
from vllm.config.utils import config
9
from vllm.v1.attention.backends.registry import AttentionBackendEnum
10
11
12
@config
14
-@dataclass
13
class AttentionConfig:
14
"""Configuration for attention mechanisms in vLLM."""
15
vllm/config/cache.py
-2
@@ -6,7 +6,6 @@ from dataclasses import field
6
from typing import TYPE_CHECKING, Any, Literal
7
8
from pydantic import Field, SkipValidation, field_validator
9
-from pydantic.dataclasses import dataclass
9
10
from vllm.config.utils import config
11
from vllm.logger import init_logger
@@ -37,7 +36,6 @@ KVOffloadingBackend = Literal["native", "lmcache"]
36
37
38
@config
40
-@dataclass
39
class CacheConfig:
40
"""Configuration for the KV cache."""
41
vllm/config/compilation.py
+1
-5
@@ -8,8 +8,7 @@ from dataclasses import field
8
from pathlib import Path
9
from typing import TYPE_CHECKING, Any, ClassVar, Literal
10
11
-from pydantic import ConfigDict, Field, TypeAdapter, field_validator
12
-from pydantic.dataclasses import dataclass
11
+from pydantic import Field, TypeAdapter, field_validator
12
13
import vllm.envs as envs
14
from vllm.compilation.inductor_pass import CallableInductorPass, InductorPass
@@ -96,7 +95,6 @@ class CUDAGraphMode(enum.Enum):
95
96
97
@config
99
-@dataclass(config=ConfigDict(extra="forbid"))
98
class PassConfig:
99
"""Configuration for custom Inductor passes.
100
@@ -267,7 +265,6 @@ class DynamicShapesType(str, enum.Enum):
265
266
267
@config
270
-@dataclass(config=ConfigDict(extra="forbid"))
268
class DynamicShapesConfig:
269
"""Configuration to control/debug torch compile dynamic shapes."""
270
@@ -311,7 +308,6 @@ class DynamicShapesConfig:
308
309
310
@config
314
-@dataclass(config=ConfigDict(extra="forbid"))
311
class CompilationConfig:
312
"""Configuration for compilation.
313
vllm/config/device.py
+1
-3
@@ -6,7 +6,6 @@ from typing import Any, Literal
6
7
import torch
8
from pydantic import ConfigDict, SkipValidation
9
-from pydantic.dataclasses import dataclass
9
10
from vllm.config.utils import config
11
from vllm.utils.hashing import safe_hash
@@ -14,8 +13,7 @@ from vllm.utils.hashing import safe_hash
13
Device = Literal["auto", "cuda", "cpu", "tpu", "xpu"]
14
15
17
-@config
18
-@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
16
+@config(config=ConfigDict(arbitrary_types_allowed=True))
17
class DeviceConfig:
18
"""Configuration for the device to use for vLLM execution."""
19
vllm/config/ec_transfer.py
-3
@@ -5,8 +5,6 @@ import uuid
5
from dataclasses import field
6
from typing import Any, Literal, get_args
7
8
-from pydantic.dataclasses import dataclass
9
-
8
from vllm.config.utils import config
9
10
ECProducer = Literal["ec_producer"]
@@ -15,7 +13,6 @@ ECRole = Literal[ECProducer, ECConsumer]
13
14
15
@config
18
-@dataclass
16
class ECTransferConfig:
17
"""Configuration for distributed EC cache transfer."""
18
vllm/config/kv_events.py
-2
@@ -5,13 +5,11 @@
5
from typing import Literal
6
7
from pydantic import Field
8
-from pydantic.dataclasses import dataclass
8
9
from vllm.config.utils import config
10
11
12
@config
14
-@dataclass
13
class KVEventsConfig:
14
"""Configuration for KV event publishing."""
15
vllm/config/kv_transfer.py
-3
@@ -5,8 +5,6 @@ import uuid
5
from dataclasses import field
6
from typing import Any, Literal, get_args
7
8
-from pydantic.dataclasses import dataclass
9
-
8
from vllm.config.utils import config
9
from vllm.utils.hashing import safe_hash
10
@@ -16,7 +14,6 @@ KVRole = Literal[KVProducer, KVConsumer]
14
15
16
@config
19
-@dataclass
17
class KVTransferConfig:
18
"""Configuration for distributed KV cache transfer."""
19
vllm/config/load.py
-2
@@ -4,7 +4,6 @@
4
from typing import TYPE_CHECKING, Any
5
6
from pydantic import Field, field_validator
7
-from pydantic.dataclasses import dataclass
7
8
from vllm.config.utils import config
9
from vllm.logger import init_logger
@@ -21,7 +20,6 @@ logger = init_logger(__name__)
20
21
22
@config
24
-@dataclass
23
class LoadConfig:
24
"""Configuration for loading the model weights."""
25
vllm/config/lora.py
+1
-3
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Any, Literal
5
6
import torch
7
from pydantic import ConfigDict, Field, model_validator
8
-from pydantic.dataclasses import dataclass
8
from typing_extensions import Self
9
10
from vllm.config.utils import config
@@ -26,8 +25,7 @@ MaxLoRARanks = Literal[1, 8, 16, 32, 64, 128, 256, 320, 512]
25
LoRAExtraVocabSize = Literal[256, 512]
26
27
29
-@config
30
-@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
28
+@config(config=ConfigDict(arbitrary_types_allowed=True))
29
class LoRAConfig:
30
"""Configuration for LoRA."""
31
vllm/config/model.py
+1
-3
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any, Literal, cast, get_args
9
10
import torch
11
from pydantic import ConfigDict, Field, field_validator, model_validator
12
-from pydantic.dataclasses import dataclass
12
13
import vllm.envs as envs
14
from vllm.config.model_arch import (
@@ -97,8 +96,7 @@ AttnTypeStr = Literal[
96
]
97
98
100
-@config
101
-@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
99
+@config(config=ConfigDict(arbitrary_types_allowed=True))
100
class ModelConfig:
101
"""Configuration for the model."""
102
vllm/config/multimodal.py
-1
@@ -51,7 +51,6 @@ DummyOptions: TypeAlias = (
51
52
53
@config
54
-@dataclass
54
class MultiModalConfig:
55
"""Controls the behavior of multimodal models."""
56
vllm/config/observability.py
-2
@@ -6,7 +6,6 @@ from typing import Any, Literal, cast
6
7
from packaging.version import parse
8
from pydantic import Field, field_validator, model_validator
9
-from pydantic.dataclasses import dataclass
9
10
from vllm import version
11
from vllm.config.utils import config
@@ -16,7 +15,6 @@ DetailedTraceModules = Literal["model", "worker", "all"]
15
16
17
@config
19
-@dataclass
18
class ObservabilityConfig:
19
"""Configuration for observability - metrics and tracing."""
20
vllm/config/parallel.py
-7
@@ -3,12 +3,10 @@
3
4
import os
5
from collections.abc import Callable
6
-from dataclasses import replace
6
from typing import TYPE_CHECKING, Any, Literal
7
8
import torch
9
from pydantic import Field, field_validator, model_validator
11
-from pydantic.dataclasses import dataclass
10
from torch.distributed import ProcessGroup, ReduceOp
11
from typing_extensions import Self
12
@@ -50,7 +48,6 @@ All2AllBackend = Literal[
48
49
50
@config
53
-@dataclass
51
class EPLBConfig:
52
"""Configuration for Expert Parallel Load Balancing (EP)."""
53
@@ -94,7 +91,6 @@ class EPLBConfig:
91
92
93
@config
97
-@dataclass
94
class ParallelConfig:
95
"""Configuration for the distributed execution."""
96
@@ -715,6 +711,3 @@ class ParallelConfig:
711
)
712
713
return self
718
-
719
- def replace(self, **kwargs) -> Self:
720
- return replace(self, **kwargs)
vllm/config/pooler.py
-3
@@ -3,8 +3,6 @@
3
4
from typing import Any, Literal, get_args
5
6
-from pydantic.dataclasses import dataclass
7
-
6
from vllm.config.utils import config
7
from vllm.logger import init_logger
8
from vllm.utils.hashing import safe_hash
@@ -19,7 +17,6 @@ TOK_POOLING_TYPES: tuple[TokenPoolingType, ...] = get_args(TokenPoolingType)
17
18
19
@config
22
-@dataclass
20
class PoolerConfig:
21
"""Controls the behavior of output pooling in pooling models."""
22
vllm/config/profiler.py
-2
@@ -5,7 +5,6 @@ import os
5
from typing import Any, Literal
6
7
from pydantic import Field, model_validator
8
-from pydantic.dataclasses import dataclass
8
from typing_extensions import Self
9
10
from vllm.config.utils import config
@@ -32,7 +31,6 @@ def _is_uri_path(path: str) -> bool:
31
32
33
@config
35
-@dataclass
34
class ProfilerConfig:
35
"""Dataclass which contains profiler config for the engine."""
36
vllm/config/scheduler.py
-2
@@ -6,7 +6,6 @@ from dataclasses import InitVar
6
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
7
8
from pydantic import Field, field_validator
9
-from pydantic.dataclasses import dataclass
9
from typing_extensions import Self
10
11
from vllm.config.utils import config
@@ -24,7 +23,6 @@ SchedulerPolicy = Literal["fcfs", "priority"]
23
24
25
@config
27
-@dataclass
26
class SchedulerConfig:
27
"""Scheduler configuration."""
28
vllm/config/speculative.py
-2
@@ -5,7 +5,6 @@ import ast
5
from typing import TYPE_CHECKING, Any, Literal, get_args
6
7
from pydantic import Field, SkipValidation, model_validator
8
-from pydantic.dataclasses import dataclass
8
from typing_extensions import Self
9
10
from vllm.config.model import ModelConfig
@@ -55,7 +54,6 @@ SpeculativeMethod = Literal[
54
55
56
@config
58
-@dataclass
57
class SpeculativeConfig:
58
"""Configuration for speculative decoding."""
59
vllm/config/speech_to_text.py
-3
@@ -2,13 +2,10 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
-from pydantic.dataclasses import dataclass
6
-
5
from vllm.config.utils import config
6
7
8
@config
11
-@dataclass
9
class SpeechToTextConfig:
10
"""Configuration for speech-to-text models."""
11
vllm/config/structured_outputs.py
-2
@@ -4,7 +4,6 @@
4
from typing import Any, Literal
5
6
from pydantic import model_validator
7
-from pydantic.dataclasses import dataclass
7
from typing_extensions import Self
8
9
from vllm.config.utils import config
@@ -16,7 +15,6 @@ StructuredOutputsBackend = Literal[
15
16
17
@config
19
-@dataclass
18
class StructuredOutputsConfig:
19
"""Dataclass which contains structured outputs config for the engine."""
20
vllm/config/utils.py
+75
-38
@@ -10,14 +10,17 @@ import json
10
import pathlib
11
import textwrap
12
from collections.abc import Callable, Mapping, Sequence, Set
13
-from dataclasses import MISSING, Field, dataclass, field, fields, is_dataclass, replace
13
+from dataclasses import MISSING, Field, field, fields, is_dataclass
14
from itertools import pairwise
15
-from typing import TYPE_CHECKING, Any, Protocol, TypeVar
15
+from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast
16
17
import regex as re
18
import torch
19
+from pydantic import ConfigDict
20
+from pydantic.dataclasses import dataclass
21
+from pydantic.fields import Field as PydanticField
22
from pydantic.fields import FieldInfo
20
-from typing_extensions import runtime_checkable
23
+from typing_extensions import dataclass_transform, runtime_checkable
24
25
from vllm.logger import init_logger
26
@@ -29,23 +32,39 @@ else:
32
DataclassInstance = Any
33
34
ConfigType = type[DataclassInstance]
32
-ConfigT = TypeVar("ConfigT", bound=ConfigType)
35
+ConfigT = TypeVar("ConfigT", bound=DataclassInstance)
36
37
35
-def config(cls: ConfigT) -> ConfigT:
36
- """
37
- A decorator that ensures all fields in a dataclass have default values
38
- and that each field has a docstring.
38
+@dataclass_transform(field_specifiers=(PydanticField,))
39
+def config(
40
+ cls: type[ConfigT] | None = None,
41
+ *,
42
+ config: ConfigDict | None = None,
43
+ **kwargs: Any,
44
+) -> type[ConfigT] | Callable[[type[ConfigT]], type[ConfigT]]:
45
+ """Decorator to create a pydantic dataclass with default config. The default config
46
+ for the dataclass forbids extra fields.
47
40
- If a `ConfigT` is used as a CLI argument itself, the `type` keyword argument
41
- provided by `get_kwargs` will be
42
- `pydantic.TypeAdapter(ConfigT).validate_json(cli_arg)` which treats the
43
- `cli_arg` as a JSON string which gets validated by `pydantic`.
48
+ All config classes in vLLM should use this decorator.
49
45
- Config validation is performed by the tools/pre_commit/validate_config.py
46
- script, which is invoked during the pre-commit checks.
47
- """
48
- return cls
50
+ Args:
51
+ cls: The class to decorate
52
+ config: The pydantic ConfigDict to use. If provided, it will be merged with
53
+ the default config.
54
+ **kwargs: Additional arguments to pass to pydantic.dataclass."""
55
+ # Extra fields are forbidden by default
56
+ merged_config = ConfigDict(extra="forbid")
57
+ if config is not None:
58
+ merged_config.update(config)
59
+
60
+ def decorator(cls):
61
+ return dataclass(cls, config=merged_config, **kwargs)
62
+
63
+ # Called with arguments: @config(config=...)
64
+ if cls is None:
65
+ return decorator
66
+ # Called without arguments: @config
67
+ return decorator(cls)
68
69
70
def get_field(cls: ConfigType, name: str) -> Field:
@@ -53,24 +72,46 @@ def get_field(cls: ConfigType, name: str) -> Field:
72
default factory fields in `EngineArgs`."""
73
if not is_dataclass(cls):
74
raise TypeError("The given class is not a dataclass.")
56
- cls_fields = {f.name: f for f in fields(cls)}
57
- if name not in cls_fields:
58
- raise ValueError(f"Field '{name}' not found in {cls.__name__}.")
59
- named_field: Field = cls_fields[name]
60
- if (default_factory := named_field.default_factory) is not MISSING:
61
- return field(default_factory=default_factory)
62
- if (default := named_field.default) is not MISSING:
63
- if isinstance(default, FieldInfo):
64
- # Handle pydantic.Field defaults
65
- if default.default_factory is not None:
66
- return field(default_factory=default.default_factory)
67
- else:
68
- default = default.default
69
- return field(default=default)
70
-
71
- raise ValueError(
72
- f"{cls.__name__}.{name} must have a default value or default factory."
73
- )
75
+ try:
76
+ named_field = next(f for f in fields(cls) if f.name == name)
77
+ except StopIteration as e:
78
+ raise ValueError(f"Field '{name}' not found in {cls.__name__}.") from e
79
+
80
+ # The arguments to copy to the new field
81
+ default = named_field.default
82
+ default_factory = named_field.default_factory
83
+ init = named_field.init
84
+
85
+ # Handle pydantic.Field
86
+ if isinstance(default, FieldInfo):
87
+ if default.init is not None:
88
+ init = default.init
89
+ if default.default_factory is not None:
90
+ default_factory = cast(Callable[[], Any], default.default_factory)
91
+ default = MISSING
92
+ else:
93
+ default = default.default
94
+
95
+ if default is MISSING and default_factory is MISSING:
96
+ logger.warning_once(
97
+ "%s.%s has no default or default factory.", cls.__name__, name
98
+ )
99
+ return field(default=default, default_factory=default_factory, init=init)
100
+
101
+
102
+def is_init_field(cls: ConfigType, name: str) -> bool:
103
+ return get_field(cls, name).init
104
+
105
+
106
+def replace(dataclass_instance: ConfigT, /, **kwargs) -> ConfigT:
107
+ """Like [`dataclasses.replace`](https://docs.python.org/3/library/dataclasses.html#dataclasses.replace),
108
+ but compatible with Pydantic dataclasses which use `pydantic.fields.Field` instead
109
+ of `dataclasses.field`"""
110
+ cls = type(dataclass_instance)
111
+ dataclass_dict = dataclass_instance.__dict__
112
+ dataclass_dict = {k: v for k, v in dataclass_dict.items() if is_init_field(cls, k)}
113
+ dataclass_dict.update(kwargs)
114
+ return cls(**dataclass_dict)
115
116
117
def getattr_iter(
@@ -172,10 +213,6 @@ def get_attr_docs(cls: type[Any]) -> dict[str, str]:
213
return out
214
215
175
-def is_init_field(cls: ConfigType, name: str) -> bool:
176
- return next(f for f in fields(cls) if f.name == name).init
177
-
178
-
216
@runtime_checkable
217
class SupportsHash(Protocol):
218
def compute_hash(self) -> str: ...
vllm/config/vllm.py
+4
-15
@@ -9,7 +9,7 @@ import tempfile
9
import threading
10
import time
11
from contextlib import contextmanager
12
-from dataclasses import is_dataclass, replace
12
+from dataclasses import is_dataclass
13
from datetime import datetime
14
from enum import IntEnum
15
from functools import lru_cache
@@ -18,10 +18,8 @@ from typing import TYPE_CHECKING, Any, TypeVar, get_args
18
19
import torch
20
from pydantic import ConfigDict, Field, model_validator
21
-from pydantic.dataclasses import dataclass
21
22
import vllm.envs as envs
24
-from vllm.config.speculative import EagleModelTypes
23
from vllm.logger import enable_trace_function_call, init_logger
24
from vllm.transformers_utils.runai_utils import is_runai_obj_uri
25
from vllm.utils import random_uuid
@@ -41,9 +39,9 @@ from .observability import ObservabilityConfig
39
from .parallel import ParallelConfig
40
from .profiler import ProfilerConfig
41
from .scheduler import SchedulerConfig
44
-from .speculative import SpeculativeConfig
42
+from .speculative import EagleModelTypes, SpeculativeConfig
43
from .structured_outputs import StructuredOutputsConfig
46
-from .utils import SupportsHash, config
44
+from .utils import SupportsHash, config, replace
45
46
if TYPE_CHECKING:
47
from transformers import PretrainedConfig
@@ -187,8 +185,7 @@ OPTIMIZATION_LEVEL_TO_CONFIG = {
185
}
186
187
190
-@config
191
-@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
188
+@config(config=ConfigDict(arbitrary_types_allowed=True))
189
class VllmConfig:
190
"""Dataclass which contains all vllm-related configuration. This
191
simplifies passing around the distinct configurations in the codebase.
@@ -1395,14 +1392,6 @@ class VllmConfig:
1392
path = self.compilation_config.debug_dump_path / append_path
1393
return path
1394
1398
- def replace(self, **kwargs):
1399
- """
1400
- Replace attributes of the config, and 'recompute' the config.
1401
- dataclass.replace() calls __init__() and __post_init__(), source:
1402
- https://docs.python.org/3/library/dataclasses.html#dataclasses.replace
1403
- """
1404
- return replace(self, **kwargs)
1405
-
1395
def __str__(self):
1396
return (
1397
f"model={self.model_config.model!r}, "
vllm/entrypoints/openai/cli_args.py
-3
@@ -13,8 +13,6 @@ from collections.abc import Sequence
13
from dataclasses import field
14
from typing import Any, Literal
15
16
-from pydantic.dataclasses import dataclass
17
-
16
import vllm.envs as envs
17
from vllm.config import config
18
from vllm.engine.arg_utils import AsyncEngineArgs, optional_type
@@ -69,7 +67,6 @@ class LoRAParserAction(argparse.Action):
67
68
69
@config
72
-@dataclass
70
class FrontendArgs:
71
"""Arguments for the OpenAI-compatible frontend server."""
72
vllm/transformers_utils/configs/speculators/algos.py
+10
-7
@@ -13,9 +13,10 @@ def register_speculator(name):
13
14
15
@register_speculator("eagle3")
16
-def update_eagle3(config_dict: dict, vllm_config: dict) -> None:
16
+def update_eagle3(config_dict: dict, pre_trained_config: dict) -> None:
17
"""
18
- Apply Eagle-3 specific configuration transformations.
18
+ Apply Eagle-3 specific configuration transformations to the `dict` used to
19
+ construct the Transformers PreTrainedConfig.
20
21
Eagle-3 specific fields:
22
- draft_vocab_size: Size of the draft model's vocabulary
@@ -27,12 +28,14 @@ def update_eagle3(config_dict: dict, vllm_config: dict) -> None:
28
predictions. This is the standard field used in Eagle3 checkpoints.
29
"""
30
30
- vllm_config["draft_vocab_size"] = config_dict.get("draft_vocab_size")
31
+ pre_trained_config["draft_vocab_size"] = config_dict.get("draft_vocab_size")
32
if config_dict.get("target_hidden_size") is not None:
32
- vllm_config["target_hidden_size"] = config_dict["target_hidden_size"]
33
- vllm_config["norm_before_residual"] = config_dict.get("norm_before_residual", True)
34
- vllm_config["architectures"] = ["Eagle3LlamaForCausalLM"]
33
+ pre_trained_config["target_hidden_size"] = config_dict["target_hidden_size"]
34
+ pre_trained_config["norm_before_residual"] = config_dict.get(
35
+ "norm_before_residual", True
36
+ )
37
+ pre_trained_config["architectures"] = ["Eagle3LlamaForCausalLM"]
38
if config_dict.get("eagle_aux_hidden_state_layer_ids"):
36
- vllm_config["eagle_aux_hidden_state_layer_ids"] = config_dict[
39
+ pre_trained_config["eagle_aux_hidden_state_layer_ids"] = config_dict[
40
"eagle_aux_hidden_state_layer_ids"
41
]
vllm/transformers_utils/configs/speculators/base.py
+19
-15
@@ -24,13 +24,16 @@ class SpeculatorsConfig(PretrainedConfig):
24
"""Load speculators Eagle config and convert to vLLM format."""
25
config_dict, _ = cls.get_config_dict(pretrained_model_name_or_path, **kwargs)
26
27
- vllm_config = cls.extract_vllm_speculative_config(config_dict)
27
+ vllm_config = cls.extract_transformers_pre_trained_config(config_dict)
28
return cls(**vllm_config)
29
30
@classmethod
31
- def extract_vllm_speculative_config(
31
+ def extract_transformers_pre_trained_config(
32
cls, config_dict: dict[str, Any]
33
) -> dict[str, Any]:
34
+ """
35
+ Extract standard Transformers PreTrainedConfig config from speculators config.
36
+ """
37
speculators_model_type = config_dict.get("speculators_model_type")
38
if speculators_model_type not in SUPPORTED_SPECULATORS_TYPES:
39
raise ValueError(
@@ -38,15 +41,23 @@ class SpeculatorsConfig(PretrainedConfig):
41
"Please ensure you're loading a speculators-format model."
42
)
43
44
+ # Start with transformer layer configuration if present
45
+ pre_trained_config = config_dict.get("transformer_layer_config", {})
46
+ # Apply anything specific to the supported algorithm
47
+ algo_updater = SUPPORTED_SPECULATORS_TYPES[speculators_model_type]
48
+ algo_updater(config_dict=config_dict, pre_trained_config=pre_trained_config)
49
+ return pre_trained_config
50
+
51
+ @classmethod
52
+ def extract_vllm_speculative_config(
53
+ cls, config_dict: dict[str, Any]
54
+ ) -> dict[str, Any]:
55
+ """Extract vLLM speculative config from speculators config."""
56
# validate fields
57
# TODO: @dsikka - use speculators pydantic model to validate
58
cls.validate_speculators_config(config_dict=config_dict)
59
# Convert from speculators config -> format that can be ingested by vLLM
45
- vllm_config = cls.build_vllm_speculative_config(config_dict=config_dict)
46
- # Apply anything specific to the supported algorithm
47
- algo_updater = SUPPORTED_SPECULATORS_TYPES[speculators_model_type]
48
- algo_updater(config_dict=config_dict, vllm_config=vllm_config)
49
- return vllm_config
60
+ return cls.build_vllm_speculative_config(config_dict=config_dict)
61
62
@classmethod
63
def validate_speculators_config(cls, config_dict: dict[str, Any]) -> None:
@@ -101,14 +112,7 @@ class SpeculatorsConfig(PretrainedConfig):
112
)
113
114
# Build base vLLM speculative configuration
104
- vllm_config = {
115
+ return {
116
"method": config_dict.get("speculators_model_type"),
117
"num_speculative_tokens": num_speculative_tokens,
107
- "target_model": spec_config.get("verifier")["name_or_path"],
118
}
109
-
110
- # Merge transformer layer configuration if present
111
- transformer_config = config_dict.get("transformer_layer_config", {})
112
- vllm_config.update(transformer_config)
113
-
114
- return vllm_config
vllm/v1/spec_decode/draft_model.py
+6
-4
@@ -4,7 +4,7 @@ from typing import Any
4
5
import torch
6
7
-from vllm.config import VllmConfig, get_layers_from_vllm_config
7
+from vllm.config import VllmConfig, get_layers_from_vllm_config, replace
8
from vllm.logger import init_logger
9
from vllm.model_executor.layers.attention import Attention
10
from vllm.model_executor.model_loader import get_model
@@ -191,10 +191,12 @@ def create_vllm_config_for_draft_model(
191
old = target_model_vllm_config
192
assert old.speculative_config is not None, "speculative_config is not set"
193
old_spec_config = old.speculative_config
194
- new_parallel_config = old_spec_config.draft_parallel_config.replace(
195
- rank=old.parallel_config.rank
194
+ new_parallel_config = replace(
195
+ old_spec_config.draft_parallel_config,
196
+ rank=old.parallel_config.rank,
197
)
197
- new: VllmConfig = old.replace(
198
+ new: VllmConfig = replace(
199
+ old,
200
quant_config=None, # quant_config is recomputed in __init__()
201
model_config=old_spec_config.draft_model_config,
202
parallel_config=new_parallel_config,