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, 14:45 UTC
Improve configs - `TokenizerPoolConfig` + `DeviceConfig` (#16603)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Harry Mellor committed
Apr 17, 2025 at 12:19 UTC
d27ea94034a9cf3383b2d4d340b9086fd7cebb82
3 files changed
+136
-81
tests/test_config.py
+24
-2
@@ -1,14 +1,36 @@
1
# SPDX-License-Identifier: Apache-2.0
2
3
-from dataclasses import asdict
3
+from dataclasses import MISSING, Field, asdict, dataclass, field
4
5
import pytest
6
7
-from vllm.config import ModelConfig, PoolerConfig
7
+from vllm.config import ModelConfig, PoolerConfig, get_field
8
from vllm.model_executor.layers.pooler import PoolingType
9
from vllm.platforms import current_platform
10
11
12
+def test_get_field():
13
+
14
+ @dataclass
15
+ class TestConfig:
16
+ a: int
17
+ b: dict = field(default_factory=dict)
18
+ c: str = "default"
19
+
20
+ with pytest.raises(ValueError):
21
+ get_field(TestConfig, "a")
22
+
23
+ b = get_field(TestConfig, "b")
24
+ assert isinstance(b, Field)
25
+ assert b.default is MISSING
26
+ assert b.default_factory is dict
27
+
28
+ c = get_field(TestConfig, "c")
29
+ assert isinstance(c, Field)
30
+ assert c.default == "default"
31
+ assert c.default_factory is MISSING
32
+
33
+
34
@pytest.mark.parametrize(
35
("model_id", "expected_runner_type", "expected_task"),
36
[
vllm/config.py
+51
-22
@@ -182,6 +182,23 @@ def config(cls: type[Config]) -> type[Config]:
182
return cls
183
184
185
+def get_field(cls: type[Config], name: str) -> Field:
186
+ """Get the default factory field of a dataclass by name. Used for getting
187
+ default factory fields in `EngineArgs`."""
188
+ if not is_dataclass(cls):
189
+ raise TypeError("The given class is not a dataclass.")
190
+ cls_fields = {f.name: f for f in fields(cls)}
191
+ if name not in cls_fields:
192
+ raise ValueError(f"Field '{name}' not found in {cls.__name__}.")
193
+ named_field: Field = cls_fields.get(name)
194
+ if (default_factory := named_field.default_factory) is not MISSING:
195
+ return field(default_factory=default_factory)
196
+ if (default := named_field.default) is not MISSING:
197
+ return field(default=default)
198
+ raise ValueError(
199
+ f"{cls.__name__}.{name} must have a default value or default factory.")
200
+
201
+
202
class ModelConfig:
203
"""Configuration for the model.
204
@@ -1364,20 +1381,26 @@ class CacheConfig:
1381
logger.warning("Possibly too large swap space. %s", msg)
1382
1383
1384
+PoolType = Literal["ray"]
1385
+
1386
+
1387
+@config
1388
@dataclass
1389
class TokenizerPoolConfig:
1369
- """Configuration for the tokenizer pool.
1390
+ """Configuration for the tokenizer pool."""
1391
1371
- Args:
1372
- pool_size: Number of tokenizer workers in the pool.
1373
- pool_type: Type of the pool.
1374
- extra_config: Additional config for the pool.
1375
- The way the config will be used depends on the
1376
- pool type.
1377
- """
1378
- pool_size: int
1379
- pool_type: Union[str, type["BaseTokenizerGroup"]]
1380
- extra_config: dict
1392
+ pool_size: int = 0
1393
+ """Number of tokenizer workers in the pool to use for asynchronous
1394
+ tokenization. If 0, will use synchronous tokenization."""
1395
+
1396
+ pool_type: Union[PoolType, type["BaseTokenizerGroup"]] = "ray"
1397
+ """Type of tokenizer pool to use for asynchronous tokenization. Ignored if
1398
+ tokenizer_pool_size is 0."""
1399
+
1400
+ extra_config: dict = field(default_factory=dict)
1401
+ """Additional config for the pool. The way the config will be used depends
1402
+ on the pool type. This should be a JSON string that will be parsed into a
1403
+ dictionary. Ignored if tokenizer_pool_size is 0."""
1404
1405
def compute_hash(self) -> str:
1406
"""
@@ -1408,7 +1431,7 @@ class TokenizerPoolConfig:
1431
@classmethod
1432
def create_config(
1433
cls, tokenizer_pool_size: int,
1411
- tokenizer_pool_type: Union[str, type["BaseTokenizerGroup"]],
1434
+ tokenizer_pool_type: Union[PoolType, type["BaseTokenizerGroup"]],
1435
tokenizer_pool_extra_config: Optional[Union[str, dict]]
1436
) -> Optional["TokenizerPoolConfig"]:
1437
"""Create a TokenizerPoolConfig from the given parameters.
@@ -1483,7 +1506,7 @@ class LoadConfig:
1506
download_dir: Optional[str] = None
1507
"""Directory to download and load the weights, default to the default
1508
cache directory of Hugging Face."""
1486
- model_loader_extra_config: Optional[Union[str, dict]] = None
1509
+ model_loader_extra_config: dict = field(default_factory=dict)
1510
"""Extra config for model loader. This will be passed to the model loader
1511
corresponding to the chosen load_format. This should be a JSON string that
1512
will be parsed into a dictionary."""
@@ -1514,10 +1537,6 @@ class LoadConfig:
1537
return hash_str
1538
1539
def __post_init__(self):
1517
- model_loader_extra_config = self.model_loader_extra_config or {}
1518
- if isinstance(model_loader_extra_config, str):
1519
- self.model_loader_extra_config = json.loads(
1520
- model_loader_extra_config)
1540
if isinstance(self.load_format, str):
1541
load_format = self.load_format.lower()
1542
self.load_format = LoadFormat(load_format)
@@ -2029,9 +2048,19 @@ class SchedulerConfig:
2048
return self.num_scheduler_steps > 1
2049
2050
2051
+Device = Literal["auto", "cuda", "neuron", "cpu", "tpu", "xpu", "hpu"]
2052
+
2053
+
2054
+@config
2055
+@dataclass
2056
class DeviceConfig:
2033
- device: Optional[torch.device]
2034
- device_type: str
2057
+ """Configuration for the device to use for vLLM execution."""
2058
+
2059
+ device: Union[Device, torch.device] = "auto"
2060
+ """Device type for vLLM execution."""
2061
+ device_type: str = field(init=False)
2062
+ """Device type from the current platform. This is set in
2063
+ `__post_init__`."""
2064
2065
def compute_hash(self) -> str:
2066
"""
@@ -2053,8 +2082,8 @@ class DeviceConfig:
2082
usedforsecurity=False).hexdigest()
2083
return hash_str
2084
2056
- def __init__(self, device: str = "auto") -> None:
2057
- if device == "auto":
2085
+ def __post_init__(self):
2086
+ if self.device == "auto":
2087
# Automated device type detection
2088
from vllm.platforms import current_platform
2089
self.device_type = current_platform.device_type
@@ -2065,7 +2094,7 @@ class DeviceConfig:
2094
"to turn on verbose logging to help debug the issue.")
2095
else:
2096
# Device type is assigned explicitly
2068
- self.device_type = device
2097
+ self.device_type = self.device
2098
2099
# Some device types require processing inputs on CPU
2100
if self.device_type in ["neuron"]:
vllm/engine/arg_utils.py
+61
-57
@@ -16,15 +16,15 @@ from typing_extensions import TypeIs
16
17
import vllm.envs as envs
18
from vllm import version
19
-from vllm.config import (CacheConfig, CompilationConfig, ConfigFormat,
20
- DecodingConfig, DeviceConfig,
19
+from vllm.config import (CacheConfig, CompilationConfig, Config, ConfigFormat,
20
+ DecodingConfig, Device, DeviceConfig,
21
DistributedExecutorBackend, HfOverrides,
22
KVTransferConfig, LoadConfig, LoadFormat, LoRAConfig,
23
ModelConfig, ModelImpl, ObservabilityConfig,
24
- ParallelConfig, PoolerConfig, PromptAdapterConfig,
25
- SchedulerConfig, SchedulerPolicy, SpeculativeConfig,
26
- TaskOption, TokenizerPoolConfig, VllmConfig,
27
- get_attr_docs)
24
+ ParallelConfig, PoolerConfig, PoolType,
25
+ PromptAdapterConfig, SchedulerConfig, SchedulerPolicy,
26
+ SpeculativeConfig, TaskOption, TokenizerPoolConfig,
27
+ VllmConfig, get_attr_docs, get_field)
28
from vllm.executor.executor_base import ExecutorBase
29
from vllm.logger import init_logger
30
from vllm.model_executor.layers.quantization import QUANTIZATION_METHODS
@@ -44,27 +44,17 @@ logger = init_logger(__name__)
44
45
ALLOWED_DETAILED_TRACE_MODULES = ["model", "worker", "all"]
46
47
-DEVICE_OPTIONS = [
48
- "auto",
49
- "cuda",
50
- "neuron",
51
- "cpu",
52
- "tpu",
53
- "xpu",
54
- "hpu",
55
-]
56
-
47
# object is used to allow for special typing forms
48
T = TypeVar("T")
49
TypeHint = Union[type[Any], object]
50
TypeHintT = Union[type[T], object]
51
52
63
-def optional_arg(val: str, return_type: type[T]) -> Optional[T]:
53
+def optional_arg(val: str, return_type: Callable[[str], T]) -> Optional[T]:
54
if val == "" or val == "None":
55
return None
56
try:
67
- return cast(Callable, return_type)(val)
57
+ return return_type(val)
58
except ValueError as e:
59
raise argparse.ArgumentTypeError(
60
f"Value {val} cannot be converted to {return_type}.") from e
@@ -82,8 +72,11 @@ def optional_float(val: str) -> Optional[float]:
72
return optional_arg(val, float)
73
74
85
-def nullable_kvs(val: str) -> Optional[Mapping[str, int]]:
86
- """Parses a string containing comma separate key [str] to value [int]
75
+def nullable_kvs(val: str) -> Optional[dict[str, int]]:
76
+ """NOTE: This function is deprecated, args should be passed as JSON
77
+ strings instead.
78
+
79
+ Parses a string containing comma separate key [str] to value [int]
80
pairs into a dictionary.
81
82
Args:
@@ -117,6 +110,17 @@ def nullable_kvs(val: str) -> Optional[Mapping[str, int]]:
110
return out_dict
111
112
113
+def optional_dict(val: str) -> Optional[dict[str, int]]:
114
+ try:
115
+ return optional_arg(val, json.loads)
116
+ except ValueError:
117
+ logger.warning(
118
+ "Failed to parse JSON string. Attempting to parse as "
119
+ "comma-separated key=value pairs. This will be deprecated in a "
120
+ "future release.")
121
+ return nullable_kvs(val)
122
+
123
+
124
@dataclass
125
class EngineArgs:
126
"""Arguments for vLLM engine."""
@@ -178,12 +182,14 @@ class EngineArgs:
182
enforce_eager: Optional[bool] = None
183
max_seq_len_to_capture: int = 8192
184
disable_custom_all_reduce: bool = ParallelConfig.disable_custom_all_reduce
181
- tokenizer_pool_size: int = 0
185
+ tokenizer_pool_size: int = TokenizerPoolConfig.pool_size
186
# Note: Specifying a tokenizer pool by passing a class
187
# is intended for expert use only. The API may change without
188
# notice.
185
- tokenizer_pool_type: Union[str, Type["BaseTokenizerGroup"]] = "ray"
186
- tokenizer_pool_extra_config: Optional[Dict[str, Any]] = None
189
+ tokenizer_pool_type: Union[PoolType, Type["BaseTokenizerGroup"]] = \
190
+ TokenizerPoolConfig.pool_type
191
+ tokenizer_pool_extra_config: dict[str, Any] = \
192
+ get_field(TokenizerPoolConfig, "extra_config")
193
limit_mm_per_prompt: Optional[Mapping[str, int]] = None
194
mm_processor_kwargs: Optional[Dict[str, Any]] = None
195
disable_mm_preprocessor_cache: bool = False
@@ -199,14 +205,14 @@ class EngineArgs:
205
long_lora_scaling_factors: Optional[Tuple[float]] = None
206
lora_dtype: Optional[Union[str, torch.dtype]] = 'auto'
207
max_cpu_loras: Optional[int] = None
202
- device: str = 'auto'
208
+ device: Device = DeviceConfig.device
209
num_scheduler_steps: int = SchedulerConfig.num_scheduler_steps
210
multi_step_stream_outputs: bool = SchedulerConfig.multi_step_stream_outputs
211
ray_workers_use_nsight: bool = ParallelConfig.ray_workers_use_nsight
212
num_gpu_blocks_override: Optional[int] = None
213
num_lookahead_slots: int = SchedulerConfig.num_lookahead_slots
208
- model_loader_extra_config: Optional[
209
- dict] = LoadConfig.model_loader_extra_config
214
+ model_loader_extra_config: dict = \
215
+ get_field(LoadConfig, "model_loader_extra_config")
216
ignore_patterns: Optional[Union[str,
217
List[str]]] = LoadConfig.ignore_patterns
218
preemption_mode: Optional[str] = SchedulerConfig.preemption_mode
@@ -294,14 +300,15 @@ class EngineArgs:
300
"""Check if the class is a custom type."""
301
return cls.__module__ != "builtins"
302
297
- def get_kwargs(cls: type[Any]) -> dict[str, Any]:
303
+ def get_kwargs(cls: type[Config]) -> dict[str, Any]:
304
cls_docs = get_attr_docs(cls)
305
kwargs = {}
306
for field in fields(cls):
307
name = field.name
302
- # One of these will always be present
303
- default = (field.default_factory
304
- if field.default is MISSING else field.default)
308
+ default = field.default
309
+ # This will only be True if default is MISSING
310
+ if field.default_factory is not MISSING:
311
+ default = field.default_factory()
312
kwargs[name] = {"default": default, "help": cls_docs[name]}
313
314
# Make note of if the field is optional and get the actual
@@ -331,8 +338,9 @@ class EngineArgs:
338
elif can_be_type(field_type, float):
339
kwargs[name][
340
"type"] = optional_float if optional else float
341
+ elif can_be_type(field_type, dict):
342
+ kwargs[name]["type"] = optional_dict
343
elif (can_be_type(field_type, str)
335
- or can_be_type(field_type, dict)
344
or is_custom_type(field_type)):
345
kwargs[name]["type"] = optional_str if optional else str
346
else:
@@ -674,25 +682,19 @@ class EngineArgs:
682
'Additionally for encoder-decoder models, if the '
683
'sequence length of the encoder input is larger '
684
'than this, we fall back to the eager mode.')
677
- parser.add_argument('--tokenizer-pool-size',
678
- type=int,
679
- default=EngineArgs.tokenizer_pool_size,
680
- help='Size of tokenizer pool to use for '
681
- 'asynchronous tokenization. If 0, will '
682
- 'use synchronous tokenization.')
683
- parser.add_argument('--tokenizer-pool-type',
684
- type=str,
685
- default=EngineArgs.tokenizer_pool_type,
686
- help='Type of tokenizer pool to use for '
687
- 'asynchronous tokenization. Ignored '
688
- 'if tokenizer_pool_size is 0.')
689
- parser.add_argument('--tokenizer-pool-extra-config',
690
- type=optional_str,
691
- default=EngineArgs.tokenizer_pool_extra_config,
692
- help='Extra config for tokenizer pool. '
693
- 'This should be a JSON string that will be '
694
- 'parsed into a dictionary. Ignored if '
695
- 'tokenizer_pool_size is 0.')
685
+
686
+ # Tokenizer arguments
687
+ tokenizer_kwargs = get_kwargs(TokenizerPoolConfig)
688
+ tokenizer_group = parser.add_argument_group(
689
+ title="TokenizerPoolConfig",
690
+ description=TokenizerPoolConfig.__doc__,
691
+ )
692
+ tokenizer_group.add_argument('--tokenizer-pool-size',
693
+ **tokenizer_kwargs["pool_size"])
694
+ tokenizer_group.add_argument('--tokenizer-pool-type',
695
+ **tokenizer_kwargs["pool_type"])
696
+ tokenizer_group.add_argument('--tokenizer-pool-extra-config',
697
+ **tokenizer_kwargs["extra_config"])
698
699
# Multimodal related configs
700
parser.add_argument(
@@ -784,11 +786,15 @@ class EngineArgs:
786
type=int,
787
default=EngineArgs.max_prompt_adapter_token,
788
help='Max number of PromptAdapters tokens')
787
- parser.add_argument("--device",
788
- type=str,
789
- default=EngineArgs.device,
790
- choices=DEVICE_OPTIONS,
791
- help='Device type for vLLM execution.')
789
+
790
+ # Device arguments
791
+ device_kwargs = get_kwargs(DeviceConfig)
792
+ device_group = parser.add_argument_group(
793
+ title="DeviceConfig",
794
+ description=DeviceConfig.__doc__,
795
+ )
796
+ device_group.add_argument("--device", **device_kwargs["device"])
797
+
798
parser.add_argument('--num-scheduler-steps',
799
type=int,
800
default=1,
@@ -1302,8 +1308,6 @@ class EngineArgs:
1308
1309
if self.qlora_adapter_name_or_path is not None and \
1310
self.qlora_adapter_name_or_path != "":
1305
- if self.model_loader_extra_config is None:
1306
- self.model_loader_extra_config = {}
1311
self.model_loader_extra_config[
1312
"qlora_adapter_name_or_path"] = self.qlora_adapter_name_or_path
1313