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, 02:00 UTC
[BugFix] Fix PP performance and PP kv connector output regression (#28768)
Signed-off-by: Nick Hill <nhill@redhat.com>
Nick Hill committed
Nov 17, 2025 at 14:08 UTC
7765e5ba75c0b5caa8f372bfa20ab3de2c6b3aac
4 files changed
+105
-104
vllm/v1/engine/core.py
+66
-84
@@ -63,7 +63,6 @@ from vllm.v1.outputs import ModelRunnerOutput
63
from vllm.v1.request import Request, RequestStatus
64
from vllm.v1.serial_utils import MsgpackDecoder, MsgpackEncoder
65
from vllm.v1.structured_output import StructuredOutputManager
66
-from vllm.v1.utils import record_function_or_nullcontext
66
from vllm.version import __version__ as VLLM_VERSION
67
68
logger = init_logger(__name__)
@@ -181,11 +180,13 @@ class EngineCore:
180
logger.info("Batch queue is enabled with size %d", self.batch_queue_size)
181
self.batch_queue = deque(maxlen=self.batch_queue_size)
182
183
+ self.ec_producer = (
184
+ vllm_config.ec_transfer_config is not None
185
+ and vllm_config.ec_transfer_config.is_ec_producer
186
+ )
187
+
188
self.request_block_hasher: Callable[[Request], list[BlockHash]] | None = None
185
- if (
186
- self.vllm_config.cache_config.enable_prefix_caching
187
- or kv_connector is not None
188
- ):
189
+ if vllm_config.cache_config.enable_prefix_caching or kv_connector is not None:
190
caching_hash_fn = get_hash_fn_by_name(
191
vllm_config.cache_config.prefix_caching_hash_algo
192
)
@@ -246,7 +247,7 @@ class EngineCore:
247
248
elapsed = time.time() - start
249
logger.info_once(
249
- ("init engine (profile, create kv cache, warmup model) took %.2f seconds"),
250
+ "init engine (profile, create kv cache, warmup model) took %.2f seconds",
251
elapsed,
252
scope="local",
253
)
@@ -312,6 +313,16 @@ class EngineCore:
313
)
314
raise err
315
316
+ def _log_err_callback(self, scheduler_output: SchedulerOutput):
317
+ """Log error details of a future that's not expected to return a result."""
318
+
319
+ def callback(f, sched_output=scheduler_output):
320
+ with self.log_error_detail(sched_output):
321
+ result = f.result()
322
+ assert result is None
323
+
324
+ return callback
325
+
326
def step(self) -> tuple[dict[int, EngineCoreOutputs], bool]:
327
"""Schedule, execute, and make output.
328
@@ -323,21 +334,17 @@ class EngineCore:
334
# or finished and not yet removed from the batch.
335
if not self.scheduler.has_requests():
336
return {}, False
326
- with record_function_or_nullcontext("core step: schedule"):
327
- scheduler_output = self.scheduler.schedule()
328
-
329
- with record_function_or_nullcontext("core step: execute_model"):
330
- future = self.model_executor.execute_model(scheduler_output, non_block=True)
331
- grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
332
- with self.log_error_detail(scheduler_output):
333
- model_output = future.result()
334
- if model_output is None:
335
- model_output = self.model_executor.sample_tokens(grammar_output)
336
-
337
- with record_function_or_nullcontext("core step: update_from_output"):
338
- engine_core_outputs = self.scheduler.update_from_output(
339
- scheduler_output, model_output
340
- )
337
+ scheduler_output = self.scheduler.schedule()
338
+ future = self.model_executor.execute_model(scheduler_output, non_block=True)
339
+ grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
340
+ with self.log_error_detail(scheduler_output):
341
+ model_output = future.result()
342
+ if model_output is None:
343
+ model_output = self.model_executor.sample_tokens(grammar_output)
344
+
345
+ engine_core_outputs = self.scheduler.update_from_output(
346
+ scheduler_output, model_output
347
+ )
348
349
return engine_core_outputs, scheduler_output.total_num_scheduled_tokens > 0
350
@@ -378,52 +385,34 @@ class EngineCore:
385
model_executed = False
386
deferred_scheduler_output = None
387
if self.scheduler.has_requests():
381
- with record_function_or_nullcontext("core step_with_batch_queue: schedule"):
382
- scheduler_output = self.scheduler.schedule()
383
- with record_function_or_nullcontext(
384
- "core step_with_batch_queue: execute_model"
385
- ):
386
- exec_future = self.model_executor.execute_model(
387
- scheduler_output, non_block=True
388
- )
389
- model_executed = scheduler_output.total_num_scheduled_tokens > 0
388
+ scheduler_output = self.scheduler.schedule()
389
+ exec_future = self.model_executor.execute_model(
390
+ scheduler_output, non_block=True
391
+ )
392
+ if not self.ec_producer:
393
+ model_executed = scheduler_output.total_num_scheduled_tokens > 0
394
391
- if scheduler_output.pending_structured_output_tokens:
392
- with record_function_or_nullcontext(
393
- "core step_with_batch_queue: pending_structured_output_tokens"
394
- ):
395
- # We need to defer sampling until we have processed the model output
396
- # from the prior step.
397
- deferred_scheduler_output = scheduler_output
398
- # Block-wait for execute to return
399
- # (continues running async on the GPU).
400
- with self.log_error_detail(scheduler_output):
401
- exec_result = exec_future.result()
402
- assert exec_result is None
395
+ if not model_executed:
396
+ # No sampling required (no requests scheduled).
397
+ future = cast(Future[ModelRunnerOutput], exec_future)
398
else:
404
- with record_function_or_nullcontext(
405
- "core step_with_batch_queue: get_grammar_bitmask"
406
- ):
407
- # We aren't waiting for any tokens, get any grammar
408
- # output immediately.
399
+ exec_future.add_done_callback(self._log_err_callback(scheduler_output))
400
+
401
+ if not scheduler_output.pending_structured_output_tokens:
402
+ # We aren't waiting for any tokens, get any grammar output
403
+ # and sample immediately.
404
grammar_output = self.scheduler.get_grammar_bitmask(
405
scheduler_output
406
)
412
- # Block-wait for execute to return (continues running async on the GPU).
413
- with self.log_error_detail(scheduler_output):
414
- exec_result = exec_future.result()
415
-
416
- if exec_result is None:
417
- with record_function_or_nullcontext(
418
- "core step_with_batch_queue: sample_tokens"
419
- ):
420
- # Call sample tokens.
421
- future = self.model_executor.sample_tokens(
422
- grammar_output, non_block=True
423
- )
407
+ future = self.model_executor.sample_tokens(
408
+ grammar_output, non_block=True
409
+ )
410
else:
425
- # No sampling required (e.g. all requests finished).
426
- future = cast(Future[ModelRunnerOutput], exec_future)
411
+ # We need to defer sampling until we have processed the model output
412
+ # from the prior step.
413
+ deferred_scheduler_output = scheduler_output
414
+
415
+ if not deferred_scheduler_output:
416
# Add this step's future to the queue.
417
batch_queue.appendleft((future, scheduler_output))
418
if (
@@ -440,34 +429,27 @@ class EngineCore:
429
# only be called when the scheduler contains requests or the queue
430
# is non-empty.
431
return None, False
443
- with record_function_or_nullcontext("core step_with_batch_queue: model_output"):
444
- # Block until the next result is available.
445
- future, scheduler_output = batch_queue.pop()
446
- with self.log_error_detail(scheduler_output):
447
- model_output = future.result()
448
- with record_function_or_nullcontext(
449
- "core step_with_batch_queue: update_from_output"
450
- ):
451
- engine_core_outputs = self.scheduler.update_from_output(
452
- scheduler_output, model_output
453
- )
432
+
433
+ # Block until the next result is available.
434
+ future, scheduler_output = batch_queue.pop()
435
+ with self.log_error_detail(scheduler_output):
436
+ model_output = future.result()
437
+
438
+ engine_core_outputs = self.scheduler.update_from_output(
439
+ scheduler_output, model_output
440
+ )
441
442
# NOTE(nick): We can either handle the deferred tasks here or save
443
# in a field and do it immediately once step_with_batch_queue is
444
# re-called. The latter slightly favors TTFT over TPOT/throughput.
445
if deferred_scheduler_output:
459
- with record_function_or_nullcontext(
460
- "core step_with_batch_queue: deferred_scheduler_output"
461
- ):
462
- # We now have the tokens needed to compute the bitmask for the
463
- # deferred request. Get the bitmask and call sample tokens.
464
- grammar_output = self.scheduler.get_grammar_bitmask(
465
- deferred_scheduler_output
466
- )
467
- future = self.model_executor.sample_tokens(
468
- grammar_output, non_block=True
469
- )
470
- batch_queue.appendleft((future, deferred_scheduler_output))
446
+ # We now have the tokens needed to compute the bitmask for the
447
+ # deferred request. Get the bitmask and call sample tokens.
448
+ grammar_output = self.scheduler.get_grammar_bitmask(
449
+ deferred_scheduler_output
450
+ )
451
+ future = self.model_executor.sample_tokens(grammar_output, non_block=True)
452
+ batch_queue.appendleft((future, deferred_scheduler_output))
453
454
return engine_core_outputs, model_executed
455
vllm/v1/executor/ray_executor.py
+20
-1
@@ -99,6 +99,11 @@ class RayDistributedExecutor(Executor):
99
# KV connector setup
100
self.has_connector = self.vllm_config.kv_transfer_config is not None
101
102
+ self.ec_producer = (
103
+ self.vllm_config.ec_transfer_config is not None
104
+ and self.vllm_config.ec_transfer_config.is_ec_producer
105
+ )
106
+
107
self.scheduler_output: SchedulerOutput | None = None
108
109
@property
@@ -395,6 +400,12 @@ class RayDistributedExecutor(Executor):
400
"State error: sample_tokens() must be called "
401
"after execute_model() returns None."
402
)
403
+
404
+ if self.ec_producer or not scheduler_output.total_num_scheduled_tokens:
405
+ # Model will not execute, call model runner immediately.
406
+ return self._execute_dag(scheduler_output, None, non_block)
407
+
408
+ # Model will execute, defer to sample_tokens() call.
409
self.scheduler_output = scheduler_output
410
return COMPLETED_NONE_FUTURE if non_block else None
411
@@ -417,10 +428,18 @@ class RayDistributedExecutor(Executor):
428
"""
429
scheduler_output = self.scheduler_output
430
if scheduler_output is None:
420
- return None # noqa
431
+ return COMPLETED_NONE_FUTURE if non_block else None # noqa
432
433
self.scheduler_output = None
434
435
+ return self._execute_dag(scheduler_output, grammar_output, non_block)
436
+
437
+ def _execute_dag(
438
+ self,
439
+ scheduler_output: SchedulerOutput,
440
+ grammar_output: "GrammarOutput | None",
441
+ non_block: bool = False,
442
+ ) -> ModelRunnerOutput | Future[ModelRunnerOutput]:
443
# Build the compiled DAG for the first time.
444
if self.forward_dag is None: # type: ignore
445
self.forward_dag = self._compiled_ray_dag(enable_asyncio=False)
vllm/v1/worker/gpu_model_runner.py
+18
-5
@@ -7,7 +7,7 @@ import time
7
from collections import defaultdict
8
from collections.abc import Iterator
9
from contextlib import contextmanager
10
-from copy import deepcopy
10
+from copy import copy, deepcopy
11
from functools import reduce
12
from itertools import product
13
from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast
@@ -250,7 +250,6 @@ class ExecuteModelState(NamedTuple):
250
hidden_states: torch.Tensor
251
sample_hidden_states: torch.Tensor
252
aux_hidden_states: list[torch.Tensor] | None
253
- kv_connector_output: KVConnectorOutput | None
253
ec_connector_output: ECConnectorOutput | None
254
255
@@ -573,6 +572,7 @@ class GPUModelRunner(
572
573
# Ephemeral state transferred between execute_model() and sample_tokens().
574
self.execute_model_state: ExecuteModelState | None = None
575
+ self.kv_connector_output: KVConnectorOutput | None = None
576
577
def reset_mm_cache(self) -> None:
578
if self.mm_budget:
@@ -2803,6 +2803,7 @@ class GPUModelRunner(
2803
# Return the intermediate tensors.
2804
assert isinstance(hidden_states, IntermediateTensors)
2805
hidden_states.kv_connector_output = kv_connector_output
2806
+ self.kv_connector_output = kv_connector_output
2807
return hidden_states
2808
2809
if self.is_pooling_model:
@@ -2853,18 +2854,31 @@ class GPUModelRunner(
2854
hidden_states,
2855
sample_hidden_states,
2856
aux_hidden_states,
2856
- kv_connector_output,
2857
ec_connector_output,
2858
)
2859
+ self.kv_connector_output = kv_connector_output
2860
return None
2861
2862
@torch.inference_mode
2863
def sample_tokens(
2864
self, grammar_output: "GrammarOutput | None"
2865
) -> ModelRunnerOutput | AsyncModelRunnerOutput | IntermediateTensors:
2866
+ kv_connector_output = self.kv_connector_output
2867
+ self.kv_connector_output = None
2868
+
2869
if self.execute_model_state is None:
2870
# Nothing to do (PP non-final rank case), output isn't used.
2867
- return None # noqa
2871
+ if not kv_connector_output:
2872
+ return None # noqa
2873
+
2874
+ # In case of PP with kv transfer, we need to pass through the
2875
+ # kv_connector_output
2876
+ if kv_connector_output.is_empty():
2877
+ return EMPTY_MODEL_RUNNER_OUTPUT
2878
+
2879
+ output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
2880
+ output.kv_connector_output = kv_connector_output
2881
+ return output
2882
2883
# Unpack ephemeral state.
2884
(
@@ -2875,7 +2889,6 @@ class GPUModelRunner(
2889
hidden_states,
2890
sample_hidden_states,
2891
aux_hidden_states,
2878
- kv_connector_output,
2892
ec_connector_output,
2893
) = self.execute_model_state
2894
# Clear ephemeral state.
vllm/v1/worker/gpu_worker.py
+1
-14
@@ -2,7 +2,6 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""A GPU worker class."""
4
5
-import copy
5
import gc
6
import os
7
from contextlib import AbstractContextManager, nullcontext
@@ -45,7 +44,6 @@ from vllm.v1.core.sched.output import GrammarOutput
44
from vllm.v1.engine import ReconfigureDistributedRequest, ReconfigureRankType
45
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
46
from vllm.v1.outputs import (
48
- EMPTY_MODEL_RUNNER_OUTPUT,
47
AsyncModelRunnerOutput,
48
DraftTokenIds,
49
ModelRunnerOutput,
@@ -581,18 +579,7 @@ class Worker(WorkerBase):
579
all_gather_tensors=all_gather_tensors,
580
)
581
584
- kv_connector_output = output.kv_connector_output
585
- if not kv_connector_output:
586
- return None
587
-
588
- # In case of PP with kv transfer, we need to pass through the
589
- # kv_connector_output
590
- if kv_connector_output.is_empty():
591
- return EMPTY_MODEL_RUNNER_OUTPUT
592
-
593
- output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT)
594
- output.kv_connector_output = kv_connector_output
595
- return output
582
+ return None
583
584
def take_draft_token_ids(self) -> DraftTokenIds | None:
585
return self.model_runner.take_draft_token_ids()