@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / da786e339e
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 13, 14:30 UTC

[Core] Rework handling of async scheduling config (#28250)

Signed-off-by: Nick Hill <nhill@redhat.com>

Nick Hill committed Nov 7, 2025 at 12:01 UTC da786e339e985cbf32f43474e7b88b8da65c76e5
6 files changed +121 -71
tests/v1/engine/test_engine_core.py
+17 -15
@@ -66,7 +66,7 @@ def test_engine_core():
66 assert len(engine_core.scheduler.waiting) == 1
67 assert len(engine_core.scheduler.running) == 0
68
69 - _ = engine_core.step()
69 + _ = engine_core.step_fn()
70 assert len(engine_core.scheduler.waiting) == 0
71 assert len(engine_core.scheduler.running) == 1
72
@@ -75,7 +75,7 @@ def test_engine_core():
75 assert len(engine_core.scheduler.waiting) == 1
76 assert len(engine_core.scheduler.running) == 1
77
78 - _ = engine_core.step()
78 + _ = engine_core.step_fn()
79 assert len(engine_core.scheduler.waiting) == 0
80 assert len(engine_core.scheduler.running) == 2
81
@@ -85,12 +85,12 @@ def test_engine_core():
85 assert len(engine_core.scheduler.waiting) == 2
86 assert len(engine_core.scheduler.running) == 2
87
88 - _ = engine_core.step()
88 + _ = engine_core.step_fn()
89 assert len(engine_core.scheduler.waiting) == 0
90 assert len(engine_core.scheduler.running) == 4
91
92 # Loop through until they are all done.
93 - while (outs := engine_core.step()[0].get(0)) and outs.outputs:
93 + while (outs := engine_core.step_fn()[0].get(0)) and outs.outputs:
94 pass
95
96 assert len(engine_core.scheduler.waiting) == 0
@@ -107,7 +107,7 @@ def test_engine_core():
107 assert engine_core.scheduler.has_unfinished_requests()
108 assert not engine_core.scheduler.has_finished_requests()
109
110 - _ = engine_core.step()
110 + _ = engine_core.step_fn()
111 assert len(engine_core.scheduler.waiting) == 0
112 assert len(engine_core.scheduler.running) == 1
113 assert engine_core.scheduler.has_unfinished_requests()
@@ -119,7 +119,7 @@ def test_engine_core():
119 assert not engine_core.scheduler.has_unfinished_requests()
120 assert engine_core.scheduler.has_finished_requests()
121
122 - _ = engine_core.step()
122 + _ = engine_core.step_fn()
123 assert not engine_core.scheduler.has_unfinished_requests()
124 assert not engine_core.scheduler.has_finished_requests()
125
@@ -133,7 +133,7 @@ def test_engine_core():
133 assert len(engine_core.scheduler.waiting) == 2
134 assert len(engine_core.scheduler.running) == 0
135
136 - _ = engine_core.step()
136 + _ = engine_core.step_fn()
137 assert len(engine_core.scheduler.waiting) == 0
138 assert len(engine_core.scheduler.running) == 2
139
@@ -141,7 +141,7 @@ def test_engine_core():
141 assert len(engine_core.scheduler.waiting) == 1
142 assert len(engine_core.scheduler.running) == 2
143
144 - _ = engine_core.step()
144 + _ = engine_core.step_fn()
145 assert len(engine_core.scheduler.waiting) == 0
146 assert len(engine_core.scheduler.running) == 3
147
@@ -150,7 +150,7 @@ def test_engine_core():
150 assert len(engine_core.scheduler.waiting) == 0
151 assert len(engine_core.scheduler.running) == 2
152
153 - _ = engine_core.step()
153 + _ = engine_core.step_fn()
154 assert len(engine_core.scheduler.waiting) == 0
155 assert len(engine_core.scheduler.running) == 2
156
@@ -165,12 +165,12 @@ def test_engine_core():
165 req0.request_id = req1.request_id = "test"
166 engine_core.add_request(*engine_core.preprocess_add_request(req0))
167
168 - while (outs := engine_core.step()[0].get(0)) and outs.outputs:
169 - pass
168 + while engine_core.scheduler.has_requests():
169 + engine_core.step_fn()
170
171 engine_core.add_request(*engine_core.preprocess_add_request(req1))
172 - while (outs := engine_core.step()[0].get(0)) and outs.outputs:
173 - pass
172 + while engine_core.scheduler.has_requests():
173 + engine_core.step_fn()
174
175 assert len(engine_core.scheduler.waiting) == 0
176 assert len(engine_core.scheduler.running) == 0
@@ -208,8 +208,8 @@ def test_engine_core_advanced_sampling():
208 assert len(engine_core.scheduler.waiting) == 1
209 assert len(engine_core.scheduler.running) == 0
210 # Loop through until they are all done.
211 - while (outs := engine_core.step()[0].get(0)) and outs.outputs:
212 - pass
211 + while engine_core.scheduler.has_requests():
212 + engine_core.step_fn()
213 assert len(engine_core.scheduler.waiting) == 0
214 assert len(engine_core.scheduler.running) == 0
215
@@ -297,6 +297,8 @@ def test_engine_core_concurrent_batches():
297 max_num_batched_tokens=10,
298 # Reduce startup time.
299 enforce_eager=True,
300 + # Test concurrent batch behaviour independently of async scheduling.
301 + async_scheduling=False,
302 )
303 vllm_config = engine_args.create_engine_config()
304 with set_default_torch_num_threads(1):
vllm/config/scheduler.py
+34 -9
@@ -4,7 +4,7 @@
4 import hashlib
5 from collections.abc import Callable
6 from dataclasses import InitVar
7 -from typing import Any, Literal
7 +from typing import TYPE_CHECKING, Any, Literal, cast
8
9 from pydantic import Field, field_validator, model_validator
10 from pydantic.dataclasses import dataclass
@@ -17,6 +17,10 @@ from vllm.utils import (
17 MULTIMODAL_MODEL_MAX_NUM_BATCHED_TOKENS,
18 POOLING_MODEL_MAX_NUM_BATCHED_TOKENS,
19 )
20 +from vllm.utils.import_utils import resolve_obj_by_qualname
21 +
22 +if TYPE_CHECKING:
23 + from vllm.v1.core.sched.interface import SchedulerInterface
24
25 logger = init_logger(__name__)
26
@@ -120,7 +124,7 @@ class SchedulerConfig:
124
125 # scheduler class or path. "vllm.v1.core.sched.scheduler.Scheduler"
126 # (default) or "mod.custom_class".
123 - scheduler_cls: str | type[object] = "vllm.v1.core.sched.scheduler.Scheduler"
127 + scheduler_cls: str | type[object] = Field(default=None)
128 """The scheduler class to use. "vllm.v1.core.sched.scheduler.Scheduler" is
129 the default scheduler. Can be a class directly or the path to a class of
130 form "mod.custom_class"."""
@@ -132,12 +136,34 @@ class SchedulerConfig:
136 """
137
138 async_scheduling: bool = False
135 - """EXPERIMENTAL: If set to True, perform async scheduling. This may help
136 - reduce the CPU overheads, leading to better latency and throughput. However,
137 - async scheduling is currently not supported with some features such as
138 - structured outputs, speculative decoding, and pipeline parallelism.
139 + """If set to True, perform async scheduling. This helps to avoid gaps in
140 + GPU utilization, leading to better latency and throughput.
141 + Async scheduling is currently not supported with some features such as
142 + speculative decoding and pipeline parallelism.
143 """
144
145 + def get_scheduler_cls(self) -> type["SchedulerInterface"]:
146 + if self.scheduler_cls is None:
147 + if self.async_scheduling:
148 + from vllm.v1.core.sched.async_scheduler import AsyncScheduler
149 +
150 + return AsyncScheduler
151 + from vllm.v1.core.sched.scheduler import Scheduler
152 +
153 + return Scheduler
154 +
155 + # This warning can be removed once the Scheduler interface is
156 + # finalized and we can maintain support for scheduler classes that
157 + # implement it
158 + logger.warning_once(
159 + "Using custom scheduler class %s. This scheduler interface is "
160 + "not public and compatibility may not be maintained.",
161 + self.scheduler_cls,
162 + )
163 + if not isinstance(self.scheduler_cls, str):
164 + return cast(type["SchedulerInterface"], self.scheduler_cls)
165 + return resolve_obj_by_qualname(self.scheduler_cls)
166 +
167 def compute_hash(self) -> str:
168 """
169 WARNING: Whenever a new field is added to this config,
@@ -161,6 +187,8 @@ class SchedulerConfig:
187 "max_num_seqs",
188 "max_model_len",
189 "enable_chunked_prefill",
190 + "scheduler_cls",
191 + "async_scheduling",
192 mode="wrap",
193 )
194 @classmethod
@@ -242,9 +270,6 @@ class SchedulerConfig:
270 self.long_prefill_token_threshold,
271 )
272
245 - if self.async_scheduling:
246 - self.scheduler_cls = "vllm.v1.core.sched.async_scheduler.AsyncScheduler"
247 -
273 @model_validator(mode="after")
274 def _verify_args(self) -> Self:
275 if (
vllm/config/vllm.py
+49 -2
@@ -353,6 +353,53 @@ class VllmConfig:
353 self.model_config, self.load_config
354 )
355
356 + executor_backend = self.parallel_config.distributed_executor_backend
357 + executor_supports_async_sched = executor_backend in (
358 + "mp",
359 + "uni",
360 + "external_launcher",
361 + )
362 +
363 + if self.scheduler_config.async_scheduling:
364 + # Async scheduling explicitly enabled, hard fail any incompatibilities.
365 + if self.parallel_config.pipeline_parallel_size > 1:
366 + raise ValueError(
367 + "Async scheduling is not yet compatible with "
368 + "pipeline_parallel_size > 1."
369 + )
370 + if self.speculative_config is not None:
371 + raise ValueError(
372 + "Async scheduling is not yet compatible with speculative decoding."
373 + )
374 + if not executor_supports_async_sched:
375 + raise ValueError(
376 + "Currently, async scheduling only supports `mp`, `uni`, or "
377 + "`external_launcher` distributed executor backend, but you chose "
378 + f"`{executor_backend}`."
379 + )
380 + elif self.scheduler_config.async_scheduling is None:
381 + # Enable async scheduling unless there is an incompatible option.
382 + # NOTE: we won't reach here until async scheduling is enabled by default.
383 + if (
384 + self.parallel_config.pipeline_parallel_size > 1
385 + or self.speculative_config is not None
386 + ):
387 + logger.warning(
388 + "Async scheduling is not yet supported with speculative decoding "
389 + " or pipeline_parallel_size > 1 and will be disabled."
390 + )
391 + self.scheduler_config.async_scheduling = False
392 + elif not executor_supports_async_sched:
393 + logger.warning(
394 + "Async scheduling will be disabled because it is not supported "
395 + "with the `%s` distributed executor backend (only `mp`, `uni`, and "
396 + "`external_launcher` are supported).",
397 + executor_backend,
398 + )
399 + self.scheduler_config.async_scheduling = False
400 + else:
401 + self.scheduler_config.async_scheduling = True
402 +
403 from vllm.platforms import current_platform
404
405 if (
@@ -467,7 +514,7 @@ class VllmConfig:
514 self.speculative_config is not None
515 and self.speculative_config.use_eagle()
516 ):
470 - raise NotImplementedError(
517 + raise ValueError(
518 "Fast prefill optimization for KV sharing is not "
519 "compatible with EAGLE as EAGLE requires correct logits "
520 "for all tokens while fast prefill gives incorrect logits "
@@ -491,7 +538,7 @@ class VllmConfig:
538 )
539 if not getattr(self.model_config.hf_config, "is_causal", True):
540 disable_chunked_prefill_reasons.append(
494 - "Only models using causal attention supports chunked "
541 + "Only models using causal attention support chunked "
542 "prefill and prefix caching; disabling both."
543 )
544 elif self.model_config.is_encoder_decoder:
vllm/engine/arg_utils.py
+2 -26
@@ -513,7 +513,7 @@ class EngineArgs:
513 ObservabilityConfig.collect_detailed_traces
514 )
515 scheduling_policy: SchedulerPolicy = SchedulerConfig.policy
516 - scheduler_cls: str | type[object] = SchedulerConfig.scheduler_cls
516 + scheduler_cls: str | type[object] | None = SchedulerConfig.scheduler_cls
517
518 pooler_config: PoolerConfig | None = ModelConfig.pooler_config
519 override_pooler_config: dict | PoolerConfig | None = (
@@ -552,7 +552,7 @@ class EngineArgs:
552 )
553 """Custom logitproc types"""
554
555 - async_scheduling: bool = SchedulerConfig.async_scheduling
555 + async_scheduling: bool | None = SchedulerConfig.async_scheduling
556
557 kv_sharing_fast_prefill: bool = CacheConfig.kv_sharing_fast_prefill
558
@@ -1479,20 +1479,6 @@ class EngineArgs:
1479 else ParallelConfig.data_parallel_rpc_port
1480 )
1481
1482 - if self.async_scheduling:
1483 - if self.pipeline_parallel_size > 1:
1484 - raise ValueError(
1485 - "Async scheduling is not supported with pipeline-parallel-size > 1."
1486 - )
1487 -
1488 - # Currently, async scheduling does not support speculative decoding.
1489 - # TODO(woosuk): Support it.
1490 - if self.speculative_config is not None:
1491 - raise ValueError(
1492 - "Currently, speculative decoding is not supported with "
1493 - "async scheduling."
1494 - )
1495 -
1482 # Forward the deprecated CLI args to the EPLB config.
1483 if self.num_redundant_experts is not None:
1484 self.eplb_config.num_redundant_experts = self.num_redundant_experts
@@ -1536,16 +1522,6 @@ class EngineArgs:
1522 _api_process_rank=self._api_process_rank,
1523 )
1524
1539 - if self.async_scheduling and (
1540 - parallel_config.distributed_executor_backend
1541 - not in ("mp", "uni", "external_launcher")
1542 - ):
1543 - raise ValueError(
1544 - "Currently, async scheduling only supports `mp`, `uni` or "
1545 - "`external_launcher` distributed executor backend, but you choose "
1546 - f"`{parallel_config.distributed_executor_backend}`."
1547 - )
1548 -
1525 speculative_config = self.create_speculative_config(
1526 target_model_config=model_config,
1527 target_parallel_config=parallel_config,
vllm/v1/core/sched/interface.py
+18
@@ -4,16 +4,34 @@ from abc import ABC, abstractmethod
4 from collections.abc import Iterable
5 from typing import TYPE_CHECKING, Optional
6
7 +from vllm.multimodal import MULTIMODAL_REGISTRY, MultiModalRegistry
8 +
9 if TYPE_CHECKING:
10 + from vllm.config import VllmConfig
11 from vllm.distributed.kv_transfer.kv_connector.v1 import KVConnectorBase_V1
12 from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
13 from vllm.v1.engine import EngineCoreOutputs
14 + from vllm.v1.kv_cache_interface import KVCacheConfig
15 from vllm.v1.metrics.stats import SchedulerStats
16 from vllm.v1.outputs import DraftTokenIds, ModelRunnerOutput
17 from vllm.v1.request import Request, RequestStatus
18 + from vllm.v1.structured_output import StructuredOutputManager
19
20
21 class SchedulerInterface(ABC):
22 + @abstractmethod
23 + def __init__(
24 + self,
25 + vllm_config: "VllmConfig",
26 + kv_cache_config: "KVCacheConfig",
27 + structured_output_manager: "StructuredOutputManager",
28 + block_size: int,
29 + mm_registry: MultiModalRegistry = MULTIMODAL_REGISTRY,
30 + include_finished_set: bool = False,
31 + log_stats: bool = False,
32 + ) -> None:
33 + raise NotImplementedError
34 +
35 @abstractmethod
36 def schedule(self) -> "SchedulerOutput":
37 """Schedule the requests to process in this scheduling step.
vllm/v1/engine/core.py
+1 -19
@@ -29,7 +29,6 @@ from vllm.tasks import POOLING_TASKS, SupportedTask
29 from vllm.transformers_utils.config import maybe_register_config_serialize_by_value
30 from vllm.utils.gc_utils import maybe_attach_gc_debug_callback
31 from vllm.utils.hashing import get_hash_fn_by_name
32 -from vllm.utils.import_utils import resolve_obj_by_qualname
32 from vllm.utils.network_utils import make_zmq_socket
33 from vllm.utils.system_utils import decorate_logs, set_process_title
34 from vllm.v1.core.kv_cache_utils import (
@@ -41,7 +40,6 @@ from vllm.v1.core.kv_cache_utils import (
40 )
41 from vllm.v1.core.sched.interface import SchedulerInterface
42 from vllm.v1.core.sched.output import SchedulerOutput
44 -from vllm.v1.core.sched.scheduler import Scheduler as V1Scheduler
43 from vllm.v1.engine import (
44 EngineCoreOutputs,
45 EngineCoreRequest,
@@ -117,23 +115,7 @@ class EngineCore:
115 self.structured_output_manager = StructuredOutputManager(vllm_config)
116
117 # Setup scheduler.
120 - if isinstance(vllm_config.scheduler_config.scheduler_cls, str):
121 - Scheduler = resolve_obj_by_qualname(
122 - vllm_config.scheduler_config.scheduler_cls
123 - )
124 - else:
125 - Scheduler = vllm_config.scheduler_config.scheduler_cls
126 -
127 - # This warning can be removed once the V1 Scheduler interface is
128 - # finalized and we can maintain support for scheduler classes that
129 - # implement it
130 - if Scheduler is not V1Scheduler:
131 - logger.warning(
132 - "Using configured V1 scheduler class %s. "
133 - "This scheduler interface is not public and "
134 - "compatibility may not be maintained.",
135 - vllm_config.scheduler_config.scheduler_cls,
136 - )
118 + Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
119
120 if len(kv_cache_config.kv_cache_groups) == 0:
121 # Encoder models without KV cache don't support