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 14, 05:30 UTC
Extract `CompilationConfig` from `config.py` (#22524)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Harry Mellor committed
Aug 9, 2025 at 00:34 UTC
e3edc0a7a8f015b938d5cd77a44638dde28ab3a9
5 files changed
+467
-480
tests/engine/test_arg_utils.py
-33
@@ -93,32 +93,6 @@ class NestedConfig:
93
"""field"""
94
95
96
-@config
97
-@dataclass
98
-class FromCliConfig1:
99
- field: int = 1
100
- """field"""
101
-
102
- @classmethod
103
- def from_cli(cls, cli_value: str):
104
- inst = cls(**json.loads(cli_value))
105
- inst.field += 1
106
- return inst
107
-
108
-
109
-@config
110
-@dataclass
111
-class FromCliConfig2:
112
- field: int = 1
113
- """field"""
114
-
115
- @classmethod
116
- def from_cli(cls, cli_value: str):
117
- inst = cls(**json.loads(cli_value))
118
- inst.field += 2
119
- return inst
120
-
121
-
96
@config
97
@dataclass
98
class DummyConfig:
@@ -144,10 +118,6 @@ class DummyConfig:
118
"""Dict which will be JSON in CLI"""
119
nested_config: NestedConfig = field(default_factory=NestedConfig)
120
"""Nested config"""
147
- from_cli_config1: FromCliConfig1 = field(default_factory=FromCliConfig1)
148
- """Config with from_cli method"""
149
- from_cli_config2: FromCliConfig2 = field(default_factory=FromCliConfig2)
150
- """Different config with from_cli method"""
121
122
123
@pytest.mark.parametrize(("type_hint", "expected"), [
@@ -199,9 +169,6 @@ def test_get_kwargs():
169
assert json_tip in kwargs["json_tip"]["help"]
170
# nested config should should construct the nested config
171
assert kwargs["nested_config"]["type"]('{"field": 2}') == NestedConfig(2)
202
- # from_cli configs should be constructed with the correct method
203
- assert kwargs["from_cli_config1"]["type"]('{"field": 2}').field == 3
204
- assert kwargs["from_cli_config2"]["type"]('{"field": 2}').field == 4
172
173
174
@pytest.mark.parametrize(
vllm/config/__init__.py
renamed
+7
-442
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+# ruff: noqa: F401
5
import ast
6
import copy
7
import enum
@@ -10,11 +11,9 @@ import json
11
import textwrap
12
import uuid
13
import warnings
13
-from collections import Counter
14
from collections.abc import Mapping
15
from contextlib import contextmanager
16
-from dataclasses import (MISSING, Field, asdict, field, fields, is_dataclass,
17
- replace)
16
+from dataclasses import MISSING, Field, field, fields, is_dataclass, replace
17
from functools import cached_property, lru_cache
18
from importlib.util import find_spec
19
from typing import (TYPE_CHECKING, Any, Callable, ClassVar, Literal, Optional,
@@ -22,7 +21,7 @@ from typing import (TYPE_CHECKING, Any, Callable, ClassVar, Literal, Optional,
21
22
import regex as re
23
import torch
25
-from pydantic import (ConfigDict, SkipValidation, TypeAdapter, field_validator,
24
+from pydantic import (ConfigDict, SkipValidation, field_validator,
25
model_validator)
26
from pydantic.dataclasses import dataclass
27
from safetensors.torch import _TYPES as _SAFETENSORS_TO_TORCH_DTYPE
@@ -31,7 +30,9 @@ from typing_extensions import Self, assert_never, runtime_checkable
30
31
import vllm.envs as envs
32
from vllm import version
34
-from vllm.compilation.inductor_pass import CallableInductorPass, InductorPass
33
+from vllm.config.compilation import (CompilationConfig, CompilationLevel,
34
+ PassConfig)
35
+from vllm.config.utils import ConfigType, config
36
from vllm.logger import init_logger
37
from vllm.model_executor.layers.quantization import QuantizationMethods
38
from vllm.platforms import current_platform
@@ -50,8 +51,7 @@ from vllm.utils import (DEFAULT_MAX_NUM_BATCHED_TOKENS,
51
POOLING_MODEL_MAX_NUM_BATCHED_TOKENS, GiB_bytes,
52
LayerBlockType, LazyLoader, common_broadcastable_dtype,
53
cuda_device_count_stateless, get_cpu_memory,
53
- get_open_port, is_torch_equal_or_newer, random_uuid,
54
- resolve_obj_by_qualname)
54
+ get_open_port, random_uuid)
55
56
# yapf: enable
57
@@ -70,7 +70,6 @@ if TYPE_CHECKING:
70
from vllm.model_executor.model_loader import LoadFormats
71
from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
72
73
- ConfigType = type[DataclassInstance]
73
HfOverrides = Union[dict, Callable[[type], type]]
74
else:
75
DataclassInstance = Any
@@ -83,7 +82,6 @@ else:
82
BaseModelLoader = Any
83
LoadFormats = Any
84
TensorizerConfig = Any
86
- ConfigType = type
85
HfOverrides = Union[dict[str, Any], Callable[[type], type]]
86
87
me_quant = LazyLoader("model_executor", globals(),
@@ -93,7 +91,6 @@ else:
91
92
logger = init_logger(__name__)
93
DataclassInstanceT = TypeVar("DataclassInstanceT", bound=DataclassInstance)
96
-ConfigT = TypeVar("ConfigT", bound=ConfigType)
94
95
TaskOption = Literal["auto", "generate", "embedding", "embed", "classify",
96
"score", "reward", "transcription", "draft"]
@@ -234,23 +231,6 @@ def get_attr_docs(cls: type[Any]) -> dict[str, str]:
231
return out
232
233
237
-def config(cls: ConfigT) -> ConfigT:
238
- """
239
- A decorator that ensures all fields in a dataclass have default values
240
- and that each field has a docstring.
241
-
242
- If a `ConfigT` is used as a CLI argument itself, the default value provided
243
- by `get_kwargs` will be the result parsing a JSON string as the kwargs
244
- (i.e. `ConfigT(**json.loads(cli_arg))`). However, if a particular `ConfigT`
245
- requires custom construction from CLI (i.e. `CompilationConfig`), it can
246
- have a `from_cli` method, which will be called instead.
247
-
248
- Config validation is performed by the tools/validate_config.py
249
- script, which is invoked during the pre-commit checks.
250
- """
251
- return cls
252
-
253
-
234
def get_field(cls: ConfigType, name: str) -> Field:
235
"""Get the default factory field of a dataclass by name. Used for getting
236
default factory fields in `EngineArgs`."""
@@ -4154,421 +4134,6 @@ class KVEventsConfig:
4134
"""
4135
4136
4157
-class CompilationLevel:
4158
- # constants for the levels of the compilation process
4159
- NO_COMPILATION = 0
4160
- DYNAMO_AS_IS = 1
4161
- DYNAMO_ONCE = 2
4162
- PIECEWISE = 3
4163
-
4164
-
4165
-@config
4166
-@dataclass
4167
-class PassConfig:
4168
- """Configuration for custom Inductor passes.
4169
-
4170
- This is separate from general `CompilationConfig` so that inductor passes
4171
- don't all have access to full configuration - that would create a cycle as
4172
- the `PassManager` is set as a property of config."""
4173
-
4174
- enable_fusion: bool = field(default_factory=lambda: not envs.VLLM_USE_V1)
4175
- """Whether to enable the custom fusion (RMSNorm/SiluMul+quant) pass."""
4176
- enable_attn_fusion: bool = False
4177
- """Whether to enable the custom attention+quant fusion pass."""
4178
- enable_noop: bool = field(default_factory=lambda: not envs.VLLM_USE_V1)
4179
- """Whether to enable the custom no-op elimination pass."""
4180
- enable_sequence_parallelism: bool = False
4181
- """Whether to enable sequence parallelism."""
4182
- enable_async_tp: bool = False
4183
- """Whether to enable async TP."""
4184
- enable_fi_allreduce_fusion: bool = False
4185
- """Whether to enable flashinfer allreduce fusion."""
4186
- fi_allreduce_fusion_max_token_num: int = 16384
4187
- """Max number of tokens to used in flashinfer allreduce fusion."""
4188
-
4189
- # TODO(luka) better pass enabling system.
4190
-
4191
- def uuid(self):
4192
- """
4193
- Produces a hash unique to the pass configuration.
4194
- Any new fields that affect compilation should be added to the hash.
4195
- Any future fields that don't affect compilation should be excluded.
4196
- """
4197
- return InductorPass.hash_dict(asdict(self))
4198
-
4199
- def __post_init__(self) -> None:
4200
- if not self.enable_noop:
4201
- if self.enable_fusion:
4202
- logger.warning_once(
4203
- "Fusion enabled but reshape elimination disabled. "
4204
- "RMSNorm/SiluMul + quant (fp8) fusion might not work")
4205
- if self.enable_attn_fusion:
4206
- logger.warning_once(
4207
- "Fusion enabled but reshape elimination disabled. "
4208
- "Attention + quant (fp8) fusion might not work")
4209
-
4210
-
4211
-@config
4212
-@dataclass
4213
-class CompilationConfig:
4214
- """Configuration for compilation. It has three parts:
4215
-
4216
- - Top-level Compilation control:
4217
- - [`level`][vllm.config.CompilationConfig.level]
4218
- - [`debug_dump_path`][vllm.config.CompilationConfig.debug_dump_path]
4219
- - [`cache_dir`][vllm.config.CompilationConfig.cache_dir]
4220
- - [`backend`][vllm.config.CompilationConfig.backend]
4221
- - [`custom_ops`][vllm.config.CompilationConfig.custom_ops]
4222
- - [`splitting_ops`][vllm.config.CompilationConfig.splitting_ops]
4223
- - CudaGraph capture:
4224
- - [`use_cudagraph`][vllm.config.CompilationConfig.use_cudagraph]
4225
- - [`cudagraph_capture_sizes`]
4226
- [vllm.config.CompilationConfig.cudagraph_capture_sizes]
4227
- - [`cudagraph_num_of_warmups`]
4228
- [vllm.config.CompilationConfig.cudagraph_num_of_warmups]
4229
- - [`cudagraph_copy_inputs`]
4230
- [vllm.config.CompilationConfig.cudagraph_copy_inputs]
4231
- - [`full_cuda_graph`][vllm.config.CompilationConfig.full_cuda_graph]
4232
- - Inductor compilation:
4233
- - [`use_inductor`][vllm.config.CompilationConfig.use_inductor]
4234
- - [`compile_sizes`][vllm.config.CompilationConfig.compile_sizes]
4235
- - [`inductor_compile_config`]
4236
- [vllm.config.CompilationConfig.inductor_compile_config]
4237
- - [`inductor_passes`][vllm.config.CompilationConfig.inductor_passes]
4238
- - custom inductor passes
4239
-
4240
- Why we have different sizes for cudagraph and inductor:
4241
- - cudagraph: a cudagraph captured for a specific size can only be used
4242
- for the same size. We need to capture all the sizes we want to use.
4243
- - inductor: a graph compiled by inductor for a general shape can be used
4244
- for different sizes. Inductor can also compile for specific sizes,
4245
- where it can have more information to optimize the graph with fully
4246
- static shapes. However, we find the general shape compilation is
4247
- sufficient for most cases. It might be beneficial to compile for
4248
- certain small batchsizes, where inductor is good at optimizing.
4249
- """
4250
- # Top-level Compilation control
4251
- level: Optional[int] = None
4252
- """The level of compilation:
4253
-
4254
- - None: If None, we will select the default compilation level.
4255
- For V1 engine this is 3, for V0 engine this is 0.
4256
- - 0: no compilation.
4257
- - 1: dynamo as is.
4258
- - 2: dynamo once.
4259
- - 3: piecewise compilation."""
4260
- debug_dump_path: str = ""
4261
- """The path to dump the debug information."""
4262
- cache_dir: str = ""
4263
- """The directory to store the compiled graph, to accelerate Inductor
4264
- compilation. By default, it will use model-related information to generate
4265
- a cache directory."""
4266
- backend: str = ""
4267
- """The backend for compilation. It needs to be a string:
4268
-
4269
- - "" (empty string): use the default backend.
4270
- - "eager"/"openxla"/...: use the specified backend registered in PyTorch.
4271
- - "full.module.name": a qualified name which can be used to import the
4272
-
4273
- backend function.
4274
- We use string to avoid serialization issues when using compilation in a
4275
- distributed setting. When the compilation level is 1 or 2, the backend is
4276
- used for the compilation directly (it sees the whole graph). When the
4277
- compilation level is 3, the backend is used for the piecewise compilation
4278
- (it sees a part of the graph)."""
4279
- custom_ops: list[str] = field(default_factory=list)
4280
- """Fine-grained control over which custom ops to enable/disable. Use 'all'
4281
- to enable all, 'none' to disable all. Also specify a list of custom op
4282
- names to enable (prefixed with a '+'), or disable (prefixed with a '-').
4283
- Examples:
4284
-
4285
- - 'all,-op1' to enable all except op1
4286
- - 'none,+op1,+op2' to enable only op1 and op2
4287
-
4288
- By default, all custom ops are enabled when running without Inductor and
4289
- disabled when running with Inductor: level>=PIECEWISE and use_inductor=True.
4290
- Inductor generates (fused) Triton kernels for disabled custom ops."""
4291
- splitting_ops: list[str] = field(default_factory=list)
4292
- """A list of ops to split the full graph into subgraphs, used in piecewise
4293
- compilation."""
4294
-
4295
- # Inductor capture
4296
- use_inductor: bool = True
4297
- """Whether to use inductor compilation:
4298
-
4299
- - False: inductor compilation is not used. graph runs in eager
4300
- (custom_ops enabled by default).
4301
- - True: inductor compilation is used (custom_ops disabled by default).
4302
- One graph for symbolic shape and one graph per size in compile_sizes
4303
- are compiled using configurations in inductor_compile_config.
4304
-
4305
- This setting is ignored if level<PIECEWISE."""
4306
- compile_sizes: Optional[list[Union[int, str]]] = None
4307
- """Sizes to compile for inductor. In addition
4308
- to integers, it also supports "cudagraph_capture_sizes" to
4309
- specify the sizes for cudagraph capture."""
4310
- inductor_compile_config: dict = field(default_factory=dict)
4311
- """Additional configurations for inductor.
4312
- - None: use default configurations."""
4313
- inductor_passes: dict[str, str] = field(default_factory=dict)
4314
- """Additional passes for inductor. It is a dictionary
4315
- from pass name to pass function qualified name. We use function
4316
- name because the config uses JSON format. If we pass the config
4317
- from Python, functions can also be passed directly via Python object
4318
- constructor, e.g. `CompilationConfig(inductor_passes={"a": func})`."""
4319
-
4320
- # CudaGraph compilation
4321
- use_cudagraph: bool = field(default_factory=lambda: envs.VLLM_USE_V1)
4322
- """Whether to use cudagraph inside compilation.
4323
- - False: cudagraph inside compilation is not used.
4324
- - True: cudagraph inside compilation is used. It requires
4325
- that all input buffers have fixed addresses, and all
4326
- splitting ops write their outputs to input buffers.
4327
- In the vLLM V1 Engine, this flag only applies for
4328
- CompilationLevel.PIECEWISE (aka -O3).
4329
- Note that this is orthogonal to the cudagraph capture logic
4330
- outside of compilation.
4331
- TODO: move outside cudagraph logic into compilation.
4332
- torch.compile will handle cudagraph capture logic in the future."""
4333
- cudagraph_num_of_warmups: int = 0
4334
- """Number of warmup runs for cudagraph.
4335
- It means the first several runs will be treated as warmup runs.
4336
- Only after that, the execution will be recorded, and the recorded
4337
- cudagraph will be used for subsequent runs."""
4338
- cudagraph_capture_sizes: Optional[list[int]] = None
4339
- """Sizes to capture cudagraph.
4340
- - None (default): capture sizes are inferred from vllm config.
4341
- - list[int]: capture sizes are specified as given."""
4342
- cudagraph_copy_inputs: bool = False
4343
- """Whether to copy input tensors for
4344
- cudagraph. If the caller can guarantee that the same input buffers
4345
- are always used, it can set this to False. Otherwise, it should
4346
- set this to True, and the compiler will copy the input to an
4347
- internally managed buffer. Default is False."""
4348
- full_cuda_graph: bool = False
4349
- """whether to use a full cuda graph for the entire forward pass rather than
4350
- splitting certain operations such as attention into subgraphs. Thus this
4351
- flag cannot be used together with splitting_ops. This may provide
4352
- performance benefits for smaller models."""
4353
-
4354
- pass_config: PassConfig = field(default_factory=PassConfig)
4355
- """Custom inductor passes, see PassConfig for more details"""
4356
-
4357
- max_capture_size: int = field(default=None, init=False) # type: ignore
4358
- """not configurable, computed after init"""
4359
- local_cache_dir: str = field(default=None, init=False) # type: ignore
4360
- """local cache dir for each rank"""
4361
- bs_to_padded_graph_size: list[int] = field(
4362
- default=None, # type: ignore
4363
- init=False)
4364
- """optimization:
4365
- Intuitively, bs_to_padded_graph_size should be dict[int, int].
4366
- since we know all keys are in a range [0, max_capture_size],
4367
- we can optimize it to list[int] for better lookup performance."""
4368
-
4369
- # keep track of enabled and disabled custom ops
4370
- enabled_custom_ops: Counter[str] = field(default_factory=Counter,
4371
- init=False)
4372
- """custom ops that are enabled"""
4373
- disabled_custom_ops: Counter[str] = field(default_factory=Counter,
4374
- init=False)
4375
- """custom ops that are disabled"""
4376
- traced_files: set[str] = field(default_factory=set, init=False)
4377
- """files that are traced for compilation"""
4378
- compilation_time: float = field(default=0.0, init=False)
4379
- """time taken for compilation"""
4380
-
4381
- static_forward_context: dict[str, Any] = field(default_factory=dict,
4382
- init=False)
4383
- """Per-model forward context
4384
- Map from layer name to layer objects that need to be accessed outside
4385
- model code, e.g., Attention, FusedMOE when dp_size>1."""
4386
-
4387
- def compute_hash(self) -> str:
4388
- """
4389
- WARNING: Whenever a new field is added to this config,
4390
- ensure that it is included in the factors list if
4391
- it affects the computation graph.
4392
-
4393
- Provide a hash that uniquely identifies all the configs
4394
- that affect the structure of the computation
4395
- graph from input ids/embeddings to the final hidden states,
4396
- excluding anything before input ids/embeddings and after
4397
- the final hidden states.
4398
- """
4399
- factors: list[Any] = []
4400
- factors.append(self.level)
4401
- factors.append(self.backend)
4402
- factors.append(self.custom_ops)
4403
- factors.append(self.splitting_ops)
4404
- factors.append(self.use_inductor)
4405
- factors.append(self.inductor_compile_config)
4406
- factors.append(self.inductor_passes)
4407
- factors.append(self.pass_config.uuid())
4408
- return hashlib.sha256(str(factors).encode()).hexdigest()
4409
-
4410
- def __repr__(self) -> str:
4411
- exclude = {
4412
- "static_forward_context": True,
4413
- "enabled_custom_ops": True,
4414
- "disabled_custom_ops": True,
4415
- "compilation_time": True,
4416
- "bs_to_padded_graph_size": True,
4417
- "traced_files": True,
4418
- "inductor_compile_config": {
4419
- "post_grad_custom_post_pass": True,
4420
- },
4421
- }
4422
-
4423
- # exclude default attr in pass_config
4424
- pass_config_exclude = {}
4425
- for attr, default_val in vars(PassConfig()).items():
4426
- if getattr(self.pass_config, attr) == default_val:
4427
- pass_config_exclude[attr] = True
4428
- if pass_config_exclude:
4429
- exclude["pass_config"] = pass_config_exclude
4430
-
4431
- # The cast to string is necessary because Pydantic is mocked in docs
4432
- # builds and sphinx-argparse doesn't know the return type of decode()
4433
- return str(
4434
- TypeAdapter(CompilationConfig).dump_json(
4435
- self,
4436
- exclude=exclude, # type: ignore[arg-type]
4437
- exclude_unset=True).decode())
4438
-
4439
- __str__ = __repr__
4440
-
4441
- @classmethod
4442
- def from_cli(cls, cli_value: str) -> "CompilationConfig":
4443
- """Parse the CLI value for the compilation config.
4444
- -O1, -O2, -O3, etc. is handled in FlexibleArgumentParser.
4445
- """
4446
- return TypeAdapter(CompilationConfig).validate_json(cli_value)
4447
-
4448
- def __post_init__(self) -> None:
4449
- count_none = self.custom_ops.count("none")
4450
- count_all = self.custom_ops.count("all")
4451
- assert count_none + count_all <= 1, "Can only specify 'none' or 'all'"
4452
-
4453
- # TODO(zou3519/luka): There are 2 issues with auto-functionalization V2:
4454
- # 1. A bug in PyTorch, fixed in 2.7:
4455
- # https://github.com/pytorch/pytorch/issues/147924
4456
- # 2. Custom passes (fusion) rely on auto-functionalization V1 and don't
4457
- # work with V2. Addressing this will take extra engineering effort
4458
- # and it is not yet a priority. RFC here:
4459
- # https://github.com/vllm-project/vllm/issues/14703
4460
-
4461
- if is_torch_equal_or_newer("2.6"):
4462
- KEY = 'enable_auto_functionalized_v2'
4463
- if KEY not in self.inductor_compile_config:
4464
- self.inductor_compile_config[KEY] = False
4465
-
4466
- for k, v in self.inductor_passes.items():
4467
- if not isinstance(v, str):
4468
- assert callable(v), (
4469
- f"pass {k} should be callable or a qualified name")
4470
- self.inductor_compile_config[k] = v if isinstance(
4471
- v, InductorPass) else CallableInductorPass(v)
4472
- continue
4473
-
4474
- # resolve function from qualified name
4475
- names = v.split(".")
4476
- module = ".".join(names[:-1])
4477
- func_name = names[-1]
4478
- func = __import__(module).__dict__[func_name]
4479
- self.inductor_compile_config[k] = func if isinstance(
4480
- func, InductorPass) else CallableInductorPass(func)
4481
-
4482
- if isinstance(self.pass_config, dict):
4483
- self.pass_config = PassConfig(**self.pass_config)
4484
-
4485
- def init_backend(self, vllm_config: "VllmConfig") -> Union[str, Callable]:
4486
- if self.level == CompilationLevel.NO_COMPILATION:
4487
- raise ValueError("No compilation level is set.")
4488
-
4489
- from torch._dynamo.backends.registry import list_backends
4490
- torch_backends = list_backends(exclude_tags=tuple())
4491
- if self.level in [
4492
- CompilationLevel.DYNAMO_AS_IS, CompilationLevel.DYNAMO_ONCE
4493
- ]:
4494
- if self.backend == "":
4495
- return "eager"
4496
- if self.backend in torch_backends:
4497
- return self.backend
4498
- return resolve_obj_by_qualname(self.backend)
4499
-
4500
- # TODO: pass user-specified backend to piecewise compilation
4501
- # merge with the config use_inductor
4502
- assert self.level == CompilationLevel.PIECEWISE
4503
-
4504
- from vllm.compilation.backends import VllmBackend
4505
- return VllmBackend(vllm_config)
4506
-
4507
- def init_with_cudagraph_sizes(self,
4508
- cudagraph_capture_sizes: list[int]) -> None:
4509
- """To complete the initialization of config,
4510
- we need to know the cudagraph sizes."""
4511
-
4512
- if self.cudagraph_capture_sizes is None:
4513
- self.cudagraph_capture_sizes = cudagraph_capture_sizes
4514
- else:
4515
- # de-duplicate the sizes provided by the config
4516
- dedup_sizes = list(set(self.cudagraph_capture_sizes))
4517
- if len(dedup_sizes) < len(self.cudagraph_capture_sizes):
4518
- logger.info(("cudagraph sizes specified by model runner"
4519
- " %s is overridden by config %s"),
4520
- cudagraph_capture_sizes, dedup_sizes)
4521
- self.cudagraph_capture_sizes = dedup_sizes
4522
-
4523
- computed_compile_sizes = []
4524
- if self.compile_sizes is not None:
4525
- # de-duplicate the sizes provided by the config
4526
- self.compile_sizes = list(set(self.compile_sizes))
4527
- for x in self.compile_sizes:
4528
- if isinstance(x, str):
4529
- assert x == "cudagraph_capture_sizes", \
4530
- "Unrecognized size type in compile_sizes, " \
4531
- f"expect 'cudagraph_capture_sizes', got {x}"
4532
- computed_compile_sizes.extend(self.cudagraph_capture_sizes)
4533
- else:
4534
- assert isinstance(x, int)
4535
- computed_compile_sizes.append(x)
4536
- self.compile_sizes = computed_compile_sizes # type: ignore
4537
-
4538
- # sort to make sure cudagraph capture sizes are in descending order
4539
- self.cudagraph_capture_sizes.sort(reverse=True)
4540
- self.max_capture_size = self.cudagraph_capture_sizes[
4541
- 0] if self.cudagraph_capture_sizes else 0
4542
-
4543
- # pre-compute the mapping from batch size to padded graph size
4544
- self.bs_to_padded_graph_size = [
4545
- 0 for i in range(self.max_capture_size + 1)
4546
- ]
4547
- for end, start in zip(self.cudagraph_capture_sizes,
4548
- self.cudagraph_capture_sizes[1:] + [0]):
4549
- for bs in range(start, end):
4550
- if bs == start:
4551
- self.bs_to_padded_graph_size[bs] = start
4552
- else:
4553
- self.bs_to_padded_graph_size[bs] = end
4554
- self.bs_to_padded_graph_size[
4555
- self.max_capture_size] = self.max_capture_size
4556
-
4557
- def set_splitting_ops_for_v1(self):
4558
- # NOTE: this function needs to be called
4559
- if self.splitting_ops and self.full_cuda_graph:
4560
- raise ValueError("full_cuda_graph cannot be used together with "
4561
- "splitting_ops, as Full CUDA graph will override "
4562
- f"the splitting_ops: {self.splitting_ops}")
4563
-
4564
- if not self.splitting_ops:
4565
- self.splitting_ops = [] if self.full_cuda_graph else [
4566
- "vllm.unified_attention",
4567
- "vllm.unified_attention_with_output",
4568
- "vllm.mamba_mixer2",
4569
- ]
4570
-
4571
-
4137
@config
4138
@dataclass(config=ConfigDict(arbitrary_types_allowed=True))
4139
class VllmConfig:
vllm/config/compilation.py
new
+428
@@ -0,0 +1,428 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import hashlib
5
+from collections import Counter
6
+from dataclasses import asdict, field
7
+from typing import TYPE_CHECKING, Any, Callable, Optional, Union
8
+
9
+from pydantic import TypeAdapter
10
+from pydantic.dataclasses import dataclass
11
+
12
+import vllm.envs as envs
13
+from vllm.compilation.inductor_pass import CallableInductorPass, InductorPass
14
+from vllm.config.utils import config
15
+from vllm.logger import init_logger
16
+from vllm.utils import is_torch_equal_or_newer, resolve_obj_by_qualname
17
+
18
+if TYPE_CHECKING:
19
+ from vllm.config.config import VllmConfig
20
+else:
21
+ VllmConfig = object
22
+
23
+logger = init_logger(__name__)
24
+
25
+
26
+class CompilationLevel:
27
+ # constants for the levels of the compilation process
28
+ NO_COMPILATION = 0
29
+ DYNAMO_AS_IS = 1
30
+ DYNAMO_ONCE = 2
31
+ PIECEWISE = 3
32
+
33
+
34
+@config
35
+@dataclass
36
+class PassConfig:
37
+ """Configuration for custom Inductor passes.
38
+
39
+ This is separate from general `CompilationConfig` so that inductor passes
40
+ don't all have access to full configuration - that would create a cycle as
41
+ the `PassManager` is set as a property of config."""
42
+
43
+ enable_fusion: bool = field(default_factory=lambda: not envs.VLLM_USE_V1)
44
+ """Whether to enable the custom fusion (RMSNorm/SiluMul+quant) pass."""
45
+ enable_attn_fusion: bool = False
46
+ """Whether to enable the custom attention+quant fusion pass."""
47
+ enable_noop: bool = field(default_factory=lambda: not envs.VLLM_USE_V1)
48
+ """Whether to enable the custom no-op elimination pass."""
49
+ enable_sequence_parallelism: bool = False
50
+ """Whether to enable sequence parallelism."""
51
+ enable_async_tp: bool = False
52
+ """Whether to enable async TP."""
53
+ enable_fi_allreduce_fusion: bool = False
54
+ """Whether to enable flashinfer allreduce fusion."""
55
+ fi_allreduce_fusion_max_token_num: int = 16384
56
+ """Max number of tokens to used in flashinfer allreduce fusion."""
57
+
58
+ # TODO(luka) better pass enabling system.
59
+
60
+ def uuid(self):
61
+ """
62
+ Produces a hash unique to the pass configuration.
63
+ Any new fields that affect compilation should be added to the hash.
64
+ Any future fields that don't affect compilation should be excluded.
65
+ """
66
+ return InductorPass.hash_dict(asdict(self))
67
+
68
+ def __post_init__(self) -> None:
69
+ if not self.enable_noop:
70
+ if self.enable_fusion:
71
+ logger.warning_once(
72
+ "Fusion enabled but reshape elimination disabled. "
73
+ "RMSNorm/SiluMul + quant (fp8) fusion might not work")
74
+ if self.enable_attn_fusion:
75
+ logger.warning_once(
76
+ "Fusion enabled but reshape elimination disabled. "
77
+ "Attention + quant (fp8) fusion might not work")
78
+
79
+
80
+@config
81
+@dataclass
82
+class CompilationConfig:
83
+ """Configuration for compilation. It has three parts:
84
+
85
+ - Top-level Compilation control:
86
+ - [`level`][vllm.config.CompilationConfig.level]
87
+ - [`debug_dump_path`][vllm.config.CompilationConfig.debug_dump_path]
88
+ - [`cache_dir`][vllm.config.CompilationConfig.cache_dir]
89
+ - [`backend`][vllm.config.CompilationConfig.backend]
90
+ - [`custom_ops`][vllm.config.CompilationConfig.custom_ops]
91
+ - [`splitting_ops`][vllm.config.CompilationConfig.splitting_ops]
92
+ - CudaGraph capture:
93
+ - [`use_cudagraph`][vllm.config.CompilationConfig.use_cudagraph]
94
+ - [`cudagraph_capture_sizes`]
95
+ [vllm.config.CompilationConfig.cudagraph_capture_sizes]
96
+ - [`cudagraph_num_of_warmups`]
97
+ [vllm.config.CompilationConfig.cudagraph_num_of_warmups]
98
+ - [`cudagraph_copy_inputs`]
99
+ [vllm.config.CompilationConfig.cudagraph_copy_inputs]
100
+ - [`full_cuda_graph`][vllm.config.CompilationConfig.full_cuda_graph]
101
+ - Inductor compilation:
102
+ - [`use_inductor`][vllm.config.CompilationConfig.use_inductor]
103
+ - [`compile_sizes`][vllm.config.CompilationConfig.compile_sizes]
104
+ - [`inductor_compile_config`]
105
+ [vllm.config.CompilationConfig.inductor_compile_config]
106
+ - [`inductor_passes`][vllm.config.CompilationConfig.inductor_passes]
107
+ - custom inductor passes
108
+
109
+ Why we have different sizes for cudagraph and inductor:
110
+ - cudagraph: a cudagraph captured for a specific size can only be used
111
+ for the same size. We need to capture all the sizes we want to use.
112
+ - inductor: a graph compiled by inductor for a general shape can be used
113
+ for different sizes. Inductor can also compile for specific sizes,
114
+ where it can have more information to optimize the graph with fully
115
+ static shapes. However, we find the general shape compilation is
116
+ sufficient for most cases. It might be beneficial to compile for
117
+ certain small batchsizes, where inductor is good at optimizing.
118
+ """
119
+ # Top-level Compilation control
120
+ level: Optional[int] = None
121
+ """The level of compilation:
122
+
123
+ - None: If None, we will select the default compilation level.
124
+ For V1 engine this is 3, for V0 engine this is 0.
125
+ - 0: no compilation.
126
+ - 1: dynamo as is.
127
+ - 2: dynamo once.
128
+ - 3: piecewise compilation."""
129
+ debug_dump_path: str = ""
130
+ """The path to dump the debug information."""
131
+ cache_dir: str = ""
132
+ """The directory to store the compiled graph, to accelerate Inductor
133
+ compilation. By default, it will use model-related information to generate
134
+ a cache directory."""
135
+ backend: str = ""
136
+ """The backend for compilation. It needs to be a string:
137
+
138
+ - "" (empty string): use the default backend.
139
+ - "eager"/"openxla"/...: use the specified backend registered in PyTorch.
140
+ - "full.module.name": a qualified name which can be used to import the
141
+
142
+ backend function.
143
+ We use string to avoid serialization issues when using compilation in a
144
+ distributed setting. When the compilation level is 1 or 2, the backend is
145
+ used for the compilation directly (it sees the whole graph). When the
146
+ compilation level is 3, the backend is used for the piecewise compilation
147
+ (it sees a part of the graph)."""
148
+ custom_ops: list[str] = field(default_factory=list)
149
+ """Fine-grained control over which custom ops to enable/disable. Use 'all'
150
+ to enable all, 'none' to disable all. Also specify a list of custom op
151
+ names to enable (prefixed with a '+'), or disable (prefixed with a '-').
152
+ Examples:
153
+
154
+ - 'all,-op1' to enable all except op1
155
+ - 'none,+op1,+op2' to enable only op1 and op2
156
+
157
+ By default, all custom ops are enabled when running without Inductor and
158
+ disabled when running with Inductor: level>=PIECEWISE and use_inductor=True.
159
+ Inductor generates (fused) Triton kernels for disabled custom ops."""
160
+ splitting_ops: list[str] = field(default_factory=list)
161
+ """A list of ops to split the full graph into subgraphs, used in piecewise
162
+ compilation."""
163
+
164
+ # Inductor capture
165
+ use_inductor: bool = True
166
+ """Whether to use inductor compilation:
167
+
168
+ - False: inductor compilation is not used. graph runs in eager
169
+ (custom_ops enabled by default).
170
+ - True: inductor compilation is used (custom_ops disabled by default).
171
+ One graph for symbolic shape and one graph per size in compile_sizes
172
+ are compiled using configurations in inductor_compile_config.
173
+
174
+ This setting is ignored if level<PIECEWISE."""
175
+ compile_sizes: Optional[list[Union[int, str]]] = None
176
+ """Sizes to compile for inductor. In addition
177
+ to integers, it also supports "cudagraph_capture_sizes" to
178
+ specify the sizes for cudagraph capture."""
179
+ inductor_compile_config: dict = field(default_factory=dict)
180
+ """Additional configurations for inductor.
181
+ - None: use default configurations."""
182
+ inductor_passes: dict[str, str] = field(default_factory=dict)
183
+ """Additional passes for inductor. It is a dictionary
184
+ from pass name to pass function qualified name. We use function
185
+ name because the config uses JSON format. If we pass the config
186
+ from Python, functions can also be passed directly via Python object
187
+ constructor, e.g. `CompilationConfig(inductor_passes={"a": func})`."""
188
+
189
+ # CudaGraph compilation
190
+ use_cudagraph: bool = field(default_factory=lambda: envs.VLLM_USE_V1)
191
+ """Whether to use cudagraph inside compilation.
192
+ - False: cudagraph inside compilation is not used.
193
+ - True: cudagraph inside compilation is used. It requires
194
+ that all input buffers have fixed addresses, and all
195
+ splitting ops write their outputs to input buffers.
196
+ In the vLLM V1 Engine, this flag only applies for
197
+ CompilationLevel.PIECEWISE (aka -O3).
198
+ Note that this is orthogonal to the cudagraph capture logic
199
+ outside of compilation.
200
+ TODO: move outside cudagraph logic into compilation.
201
+ torch.compile will handle cudagraph capture logic in the future."""
202
+ cudagraph_num_of_warmups: int = 0
203
+ """Number of warmup runs for cudagraph.
204
+ It means the first several runs will be treated as warmup runs.
205
+ Only after that, the execution will be recorded, and the recorded
206
+ cudagraph will be used for subsequent runs."""
207
+ cudagraph_capture_sizes: Optional[list[int]] = None
208
+ """Sizes to capture cudagraph.
209
+ - None (default): capture sizes are inferred from vllm config.
210
+ - list[int]: capture sizes are specified as given."""
211
+ cudagraph_copy_inputs: bool = False
212
+ """Whether to copy input tensors for
213
+ cudagraph. If the caller can guarantee that the same input buffers
214
+ are always used, it can set this to False. Otherwise, it should
215
+ set this to True, and the compiler will copy the input to an
216
+ internally managed buffer. Default is False."""
217
+ full_cuda_graph: bool = False
218
+ """whether to use a full cuda graph for the entire forward pass rather than
219
+ splitting certain operations such as attention into subgraphs. Thus this
220
+ flag cannot be used together with splitting_ops. This may provide
221
+ performance benefits for smaller models."""
222
+
223
+ pass_config: PassConfig = field(default_factory=PassConfig)
224
+ """Custom inductor passes, see PassConfig for more details"""
225
+
226
+ max_capture_size: int = field(default=None, init=False) # type: ignore
227
+ """not configurable, computed after init"""
228
+ local_cache_dir: str = field(default=None, init=False) # type: ignore
229
+ """local cache dir for each rank"""
230
+ bs_to_padded_graph_size: list[int] = field(
231
+ default=None, # type: ignore
232
+ init=False)
233
+ """optimization:
234
+ Intuitively, bs_to_padded_graph_size should be dict[int, int].
235
+ since we know all keys are in a range [0, max_capture_size],
236
+ we can optimize it to list[int] for better lookup performance."""
237
+
238
+ # keep track of enabled and disabled custom ops
239
+ enabled_custom_ops: Counter[str] = field(default_factory=Counter,
240
+ init=False)
241
+ """custom ops that are enabled"""
242
+ disabled_custom_ops: Counter[str] = field(default_factory=Counter,
243
+ init=False)
244
+ """custom ops that are disabled"""
245
+ traced_files: set[str] = field(default_factory=set, init=False)
246
+ """files that are traced for compilation"""
247
+ compilation_time: float = field(default=0.0, init=False)
248
+ """time taken for compilation"""
249
+
250
+ static_forward_context: dict[str, Any] = field(default_factory=dict,
251
+ init=False)
252
+ """Per-model forward context
253
+ Map from layer name to layer objects that need to be accessed outside
254
+ model code, e.g., Attention, FusedMOE when dp_size>1."""
255
+
256
+ def compute_hash(self) -> str:
257
+ """
258
+ WARNING: Whenever a new field is added to this config,
259
+ ensure that it is included in the factors list if
260
+ it affects the computation graph.
261
+
262
+ Provide a hash that uniquely identifies all the configs
263
+ that affect the structure of the computation
264
+ graph from input ids/embeddings to the final hidden states,
265
+ excluding anything before input ids/embeddings and after
266
+ the final hidden states.
267
+ """
268
+ factors: list[Any] = []
269
+ factors.append(self.level)
270
+ factors.append(self.backend)
271
+ factors.append(self.custom_ops)
272
+ factors.append(self.splitting_ops)
273
+ factors.append(self.use_inductor)
274
+ factors.append(self.inductor_compile_config)
275
+ factors.append(self.inductor_passes)
276
+ factors.append(self.pass_config.uuid())
277
+ return hashlib.sha256(str(factors).encode()).hexdigest()
278
+
279
+ def __repr__(self) -> str:
280
+ exclude = {
281
+ "static_forward_context": True,
282
+ "enabled_custom_ops": True,
283
+ "disabled_custom_ops": True,
284
+ "compilation_time": True,
285
+ "bs_to_padded_graph_size": True,
286
+ "traced_files": True,
287
+ "inductor_compile_config": {
288
+ "post_grad_custom_post_pass": True,
289
+ },
290
+ }
291
+
292
+ # exclude default attr in pass_config
293
+ pass_config_exclude = {}
294
+ for attr, default_val in vars(PassConfig()).items():
295
+ if getattr(self.pass_config, attr) == default_val:
296
+ pass_config_exclude[attr] = True
297
+ if pass_config_exclude:
298
+ exclude["pass_config"] = pass_config_exclude
299
+
300
+ return TypeAdapter(CompilationConfig).dump_json(
301
+ self,
302
+ exclude=exclude, # type: ignore[arg-type]
303
+ exclude_unset=True).decode()
304
+
305
+ __str__ = __repr__
306
+
307
+ def __post_init__(self) -> None:
308
+ count_none = self.custom_ops.count("none")
309
+ count_all = self.custom_ops.count("all")
310
+ assert count_none + count_all <= 1, "Can only specify 'none' or 'all'"
311
+
312
+ # TODO(zou3519/luka): There are 2 issues with auto-functionalization V2:
313
+ # 1. A bug in PyTorch, fixed in 2.7:
314
+ # https://github.com/pytorch/pytorch/issues/147924
315
+ # 2. Custom passes (fusion) rely on auto-functionalization V1 and don't
316
+ # work with V2. Addressing this will take extra engineering effort
317
+ # and it is not yet a priority. RFC here:
318
+ # https://github.com/vllm-project/vllm/issues/14703
319
+
320
+ if is_torch_equal_or_newer("2.6"):
321
+ KEY = 'enable_auto_functionalized_v2'
322
+ if KEY not in self.inductor_compile_config:
323
+ self.inductor_compile_config[KEY] = False
324
+
325
+ for k, v in self.inductor_passes.items():
326
+ if not isinstance(v, str):
327
+ assert callable(v), (
328
+ f"pass {k} should be callable or a qualified name")
329
+ self.inductor_compile_config[k] = v if isinstance(
330
+ v, InductorPass) else CallableInductorPass(v)
331
+ continue
332
+
333
+ # resolve function from qualified name
334
+ names = v.split(".")
335
+ module = ".".join(names[:-1])
336
+ func_name = names[-1]
337
+ func = __import__(module).__dict__[func_name]
338
+ self.inductor_compile_config[k] = func if isinstance(
339
+ func, InductorPass) else CallableInductorPass(func)
340
+
341
+ if isinstance(self.pass_config, dict):
342
+ self.pass_config = PassConfig(**self.pass_config)
343
+
344
+ def init_backend(self, vllm_config: VllmConfig) -> Union[str, Callable]:
345
+ if self.level == CompilationLevel.NO_COMPILATION:
346
+ raise ValueError("No compilation level is set.")
347
+
348
+ from torch._dynamo.backends.registry import list_backends
349
+ torch_backends = list_backends(exclude_tags=tuple())
350
+ if self.level in [
351
+ CompilationLevel.DYNAMO_AS_IS, CompilationLevel.DYNAMO_ONCE
352
+ ]:
353
+ if self.backend == "":
354
+ return "eager"
355
+ if self.backend in torch_backends:
356
+ return self.backend
357
+ return resolve_obj_by_qualname(self.backend)
358
+
359
+ # TODO: pass user-specified backend to piecewise compilation
360
+ # merge with the config use_inductor
361
+ assert self.level == CompilationLevel.PIECEWISE
362
+
363
+ from vllm.compilation.backends import VllmBackend
364
+ return VllmBackend(vllm_config)
365
+
366
+ def init_with_cudagraph_sizes(self,
367
+ cudagraph_capture_sizes: list[int]) -> None:
368
+ """To complete the initialization of config,
369
+ we need to know the cudagraph sizes."""
370
+
371
+ if self.cudagraph_capture_sizes is None:
372
+ self.cudagraph_capture_sizes = cudagraph_capture_sizes
373
+ else:
374
+ # de-duplicate the sizes provided by the config
375
+ dedup_sizes = list(set(self.cudagraph_capture_sizes))
376
+ if len(dedup_sizes) < len(self.cudagraph_capture_sizes):
377
+ logger.info(("cudagraph sizes specified by model runner"
378
+ " %s is overridden by config %s"),
379
+ cudagraph_capture_sizes, dedup_sizes)
380
+ self.cudagraph_capture_sizes = dedup_sizes
381
+
382
+ computed_compile_sizes = []
383
+ if self.compile_sizes is not None:
384
+ # de-duplicate the sizes provided by the config
385
+ self.compile_sizes = list(set(self.compile_sizes))
386
+ for x in self.compile_sizes:
387
+ if isinstance(x, str):
388
+ assert x == "cudagraph_capture_sizes", \
389
+ "Unrecognized size type in compile_sizes, " \
390
+ f"expect 'cudagraph_capture_sizes', got {x}"
391
+ computed_compile_sizes.extend(self.cudagraph_capture_sizes)
392
+ else:
393
+ assert isinstance(x, int)
394
+ computed_compile_sizes.append(x)
395
+ self.compile_sizes = computed_compile_sizes # type: ignore
396
+
397
+ # sort to make sure cudagraph capture sizes are in descending order
398
+ self.cudagraph_capture_sizes.sort(reverse=True)
399
+ self.max_capture_size = self.cudagraph_capture_sizes[
400
+ 0] if self.cudagraph_capture_sizes else 0
401
+
402
+ # pre-compute the mapping from batch size to padded graph size
403
+ self.bs_to_padded_graph_size = [
404
+ 0 for i in range(self.max_capture_size + 1)
405
+ ]
406
+ for end, start in zip(self.cudagraph_capture_sizes,
407
+ self.cudagraph_capture_sizes[1:] + [0]):
408
+ for bs in range(start, end):
409
+ if bs == start:
410
+ self.bs_to_padded_graph_size[bs] = start
411
+ else:
412
+ self.bs_to_padded_graph_size[bs] = end
413
+ self.bs_to_padded_graph_size[
414
+ self.max_capture_size] = self.max_capture_size
415
+
416
+ def set_splitting_ops_for_v1(self):
417
+ # NOTE: this function needs to be called
418
+ if self.splitting_ops and self.full_cuda_graph:
419
+ raise ValueError("full_cuda_graph cannot be used together with "
420
+ "splitting_ops, as Full CUDA graph will override "
421
+ f"the splitting_ops: {self.splitting_ops}")
422
+
423
+ if not self.splitting_ops:
424
+ self.splitting_ops = [] if self.full_cuda_graph else [
425
+ "vllm.unified_attention",
426
+ "vllm.unified_attention_with_output",
427
+ "vllm.mamba_mixer2",
428
+ ]
vllm/config/utils.py
new
+29
@@ -0,0 +1,29 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from typing import TYPE_CHECKING, TypeVar
5
+
6
+if TYPE_CHECKING:
7
+ from _typeshed import DataclassInstance
8
+
9
+ ConfigType = type[DataclassInstance]
10
+else:
11
+ ConfigType = type
12
+
13
+ConfigT = TypeVar("ConfigT", bound=ConfigType)
14
+
15
+
16
+def config(cls: ConfigT) -> ConfigT:
17
+ """
18
+ A decorator that ensures all fields in a dataclass have default values
19
+ and that each field has a docstring.
20
+
21
+ If a `ConfigT` is used as a CLI argument itself, the `type` keyword argument
22
+ provided by `get_kwargs` will be
23
+ `pydantic.TypeAdapter(ConfigT).validate_json(cli_arg)` which treats the
24
+ `cli_arg` as a JSON string which gets validated by `pydantic`.
25
+
26
+ Config validation is performed by the tools/validate_config.py
27
+ script, which is invoked during the pre-commit checks.
28
+ """
29
+ return cls
vllm/engine/arg_utils.py
+3
-5
@@ -193,8 +193,6 @@ Additionally, list elements can be passed individually using `+`:
193
194
def parse_dataclass(val: str, cls=dataclass_cls) -> Any:
195
try:
196
- if hasattr(cls, "from_cli"):
197
- return cls.from_cli(val)
196
return TypeAdapter(cls).validate_json(val)
197
except ValidationError as e:
198
raise argparse.ArgumentTypeError(repr(e)) from e
@@ -455,9 +453,9 @@ class EngineArgs:
453
# support `EngineArgs(compilation_config={...})`
454
# without having to manually construct a
455
# CompilationConfig object
458
- if isinstance(self.compilation_config, (int, dict)):
459
- self.compilation_config = CompilationConfig.from_cli(
460
- str(self.compilation_config))
456
+ if isinstance(self.compilation_config, dict):
457
+ self.compilation_config = CompilationConfig(
458
+ **self.compilation_config)
459
# Setup plugins
460
from vllm.plugins import load_general_plugins
461
load_general_plugins()