@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 61e632aea1
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 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,