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 2, 12:15 UTC
[Core] Cleanup KVConnector handling with PP + fix MRV2 (#43732)
Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Nick Hill committed
May 28, 2026 at 13:12 UTC
7e53283b1c28868ea01d88dd20504b61ea971bae
9 files changed
+38
-48
tests/distributed/test_comm_ops.py
+1
-1
@@ -292,7 +292,7 @@ def test_async_intermediate_tensors_lazy_wait() -> None:
292
)
293
294
# accessing non-tensor attributes should not trigger wait.
295
- assert it.kv_connector_output is None
295
+ assert it._comm_handles is not None
296
assert work.wait_calls == 0
297
assert post_calls["n"] == 0
298
tests/v1/worker/test_gpu_model_runner.py
+5
-2
@@ -39,6 +39,7 @@ from vllm.v1.kv_cache_interface import (
39
KVCacheGroupSpec,
40
KVCacheTensor,
41
)
42
+from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT
43
from vllm.v1.sample.metadata import SamplingMetadata
44
from vllm.v1.spec_decode.metadata import SpecDecodeMetadata
45
from vllm.v1.worker.gpu_input_batch import InputBatch
@@ -279,7 +280,8 @@ def test_sample_tokens_receives_pp_sampled_ids_only_on_non_last_rank(
280
lambda: SimpleNamespace(world_size=world_size, is_last_rank=is_last_rank),
281
)
282
282
- assert GPUModelRunner.sample_tokens(runner, None) is None
283
+ output = GPUModelRunner.sample_tokens(runner, None)
284
+ assert output in (EMPTY_MODEL_RUNNER_OUTPUT, None)
285
assert receive_calls == expected_calls
286
287
@@ -298,7 +300,8 @@ def test_sample_tokens_skips_pp_group_lookup_without_async_scheduling(
300
pytest.fail,
301
)
302
301
- assert GPUModelRunner.sample_tokens(runner, None) is None
303
+ output = GPUModelRunner.sample_tokens(runner, None)
304
+ assert output in (EMPTY_MODEL_RUNNER_OUTPUT, None)
305
306
307
def test_select_common_block_size_no_valid_option():
tests/v1/worker/test_gpu_model_runner_v2_eplb.py
+7
-2
@@ -7,6 +7,7 @@ from typing import Any
7
8
import torch
9
10
+from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT
11
from vllm.v1.worker.gpu import eplb_utils as eplb
12
from vllm.v1.worker.gpu import model_runner as mrv2
13
@@ -76,7 +77,10 @@ def _make_runner(**overrides: Any) -> Any:
77
runner.max_num_reqs = 8
78
runner.max_num_tokens = 16
79
runner.decode_query_len = 1
79
- runner.kv_connector = SimpleNamespace(set_disabled=lambda *_: None)
80
+ runner.kv_connector = SimpleNamespace(
81
+ set_disabled=lambda *_: None,
82
+ post_forward=lambda *_, **__: None,
83
+ )
84
runner.eplb = eplb.EPLBController(runner.parallel_config, runner.device)
85
runner.pooling_runner = None
86
runner.execute_model_state = None
@@ -183,5 +187,6 @@ def test_v2_sample_tokens_runs_eplb_on_non_last_pp_rank(monkeypatch):
187
),
188
)
189
186
- assert mrv2.GPUModelRunner.sample_tokens(runner, None) is None
190
+ output = mrv2.GPUModelRunner.sample_tokens(runner, None)
191
+ assert output in (EMPTY_MODEL_RUNNER_OUTPUT, None)
192
assert events == ["postprocess", "eplb"]
vllm/sequence.py
-11
@@ -3,15 +3,9 @@
3
"""Sequence and its related classes."""
4
5
from dataclasses import dataclass
6
-from typing import TYPE_CHECKING, Any
6
7
import torch
8
10
-if TYPE_CHECKING:
11
- from vllm.v1.worker.kv_connector_model_runner_mixin import KVConnectorOutput
12
-else:
13
- KVConnectorOutput = Any
14
-
9
10
# cannot use msgspec.Struct here because Dynamo does not support it
11
@dataclass
@@ -19,24 +13,19 @@ class IntermediateTensors:
13
"""For all pipeline stages except the last, we need to return the hidden
14
states and residuals to be sent to the next stage. This data structure
15
contains the hidden states and residuals for a request.
22
-
23
- Each stage also needs to handle its own kv_connector_output.
16
"""
17
18
tensors: dict[str, torch.Tensor]
27
- kv_connector_output: KVConnectorOutput | None
19
20
def __init__(
21
self,
22
tensors: dict[str, torch.Tensor],
32
- kv_connector_output: KVConnectorOutput | None = None,
23
) -> None:
24
# manually define this function, so that
25
# Dynamo knows `IntermediateTensors()` comes from this file.
26
# Otherwise, dataclass will generate this function by evaluating
27
# a string, and we will lose the information about the source file.
28
self.tensors = tensors
39
- self.kv_connector_output = kv_connector_output
29
30
def __getitem__(self, key: str | slice):
31
if isinstance(key, str):
vllm/v1/outputs.py
+14
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from abc import ABC, abstractmethod
5
+from copy import copy
6
from dataclasses import dataclass, field
7
from typing import TYPE_CHECKING, NamedTuple, TypeAlias
8
@@ -279,6 +280,19 @@ class ModelRunnerOutput:
280
# ``None`` when ``enable_return_routed_experts`` is off.
281
routed_experts: RoutedExpertsLists | None = None
282
283
+ @staticmethod
284
+ def with_kv_conn_output_only(
285
+ kv_connector_output: KVConnectorOutput | None,
286
+ ) -> "ModelRunnerOutput":
287
+ """Return ModelRunnerOutput containing the provided KVConnectorOutput,
288
+ otherwise empty. Returns None if kv_connector_output is passed as None.
289
+ """
290
+ if kv_connector_output is None or kv_connector_output.is_empty():
291
+ return EMPTY_MODEL_RUNNER_OUTPUT
292
+ output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
293
+ output.kv_connector_output = kv_connector_output
294
+ return output
295
+
296
297
# ModelRunnerOutput wrapper for async scheduling.
298
class AsyncModelRunnerOutput(ABC):
vllm/v1/worker/gpu/kv_connector.py
+1
-6
@@ -1,6 +1,5 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-import copy
3
from typing import TYPE_CHECKING
4
5
import torch
@@ -103,11 +102,7 @@ class ActiveKVConnector(KVConnector):
102
self.pre_forward(scheduler_output)
103
finished_req_ids = scheduler_output.finished_req_ids
104
kv_connector_output = self.post_forward(finished_req_ids, wait_for_save=False)
106
- if kv_connector_output is None or kv_connector_output.is_empty():
107
- return EMPTY_MODEL_RUNNER_OUTPUT
108
- output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT)
109
- output.kv_connector_output = kv_connector_output
110
- return output
105
+ return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
106
107
def set_disabled(self, disabled: bool) -> None:
108
# Ensure that layer-wise connector hooks aren't called when disabled.
vllm/v1/worker/gpu/model_runner.py
+8
-8
@@ -1218,9 +1218,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1218
1219
if not self.is_last_pp_rank:
1220
# Non-last PP rank: return IntermediateTensors for sending.
1221
- assert output_intermediate_tensors is not None
1222
- kv_connector_output = self.kv_connector.post_forward(finished_req_ids)
1223
- output_intermediate_tensors.kv_connector_output = kv_connector_output
1221
return output_intermediate_tensors
1222
return None
1223
@@ -1249,7 +1246,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1246
input_batch.num_reqs, max_sample_len=self.num_speculative_steps + 1
1247
)
1248
self.postprocess(input_batch, sampled, num_sampled, num_rejected)
1252
- return None
1249
+
1250
+ # Post-step KV connector related operations.
1251
+ kv_connector_output = self.kv_connector.post_forward(finished_req_ids)
1252
+ return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
1253
1254
# Last rank: sample tokens
1255
sampler_output, num_sampled, num_rejected = self.sample(
@@ -1367,18 +1367,18 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1367
finished_req_ids = self.execute_model_state.finished_req_ids
1368
self.execute_model_state = None
1369
1370
+ # Post-step KV connector related operations.
1371
+ kv_connector_output = self.kv_connector.post_forward(finished_req_ids)
1372
+
1373
if not self.is_last_pp_rank:
1374
self.postprocess_pool(input_batch)
1372
- return None
1375
+ return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
1376
1377
assert self.pooling_runner is not None
1378
pooler_output, is_valid = self.pooling_runner.pool(
1379
hidden_states, input_batch, self.req_states
1380
)
1381
1379
- # Post-step KV connector related operations.
1380
- kv_connector_output = self.kv_connector.post_forward(finished_req_ids)
1381
-
1382
# Build the model runner output.
1383
model_runner_output = ModelRunnerOutput(
1384
req_ids=input_batch.req_ids,
vllm/v1/worker/gpu_model_runner.py
+1
-10
@@ -4250,7 +4250,6 @@ class GPUModelRunner(
4250
if not get_pp_group().is_last_rank:
4251
# Return the intermediate tensors.
4252
assert isinstance(hidden_states, IntermediateTensors)
4253
- hidden_states.kv_connector_output = kv_connector_output
4253
self.kv_connector_output = kv_connector_output
4254
return hidden_states
4255
@@ -4326,17 +4325,9 @@ class GPUModelRunner(
4325
# receive sampled token ids from the last PP rank.
4326
if self.use_async_scheduling and not get_pp_group().is_last_rank:
4327
self._pp_receive_prev_sampled_token_ids_to_input_batch()
4329
- if not kv_connector_output:
4330
- return None # type: ignore[return-value]
4331
-
4328
# In case of PP with kv transfer, we need to pass through the
4329
# kv_connector_output
4334
- if kv_connector_output.is_empty():
4335
- return EMPTY_MODEL_RUNNER_OUTPUT
4336
-
4337
- output = copy(EMPTY_MODEL_RUNNER_OUTPUT)
4338
- output.kv_connector_output = kv_connector_output
4339
- return output
4330
+ return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
4331
4332
# Unpack ephemeral state.
4333
(
vllm/v1/worker/kv_connector_model_runner_mixin.py
+1
-8
@@ -4,7 +4,6 @@
4
Define KV connector functionality mixin for model runners.
5
"""
6
7
-import copy
7
from collections.abc import Generator
8
from contextlib import AbstractContextManager, contextmanager, nullcontext
9
from typing import TYPE_CHECKING
@@ -20,7 +19,6 @@ from vllm.logger import init_logger
19
from vllm.v1.attention.backend import AttentionBackend
20
from vllm.v1.kv_cache_interface import AttentionSpec, KVCacheConfig
21
from vllm.v1.outputs import (
23
- EMPTY_MODEL_RUNNER_OUTPUT,
22
KVConnectorOutput,
23
ModelRunnerOutput,
24
)
@@ -47,12 +45,7 @@ class KVConnectorModelRunnerMixin:
45
):
46
pass
47
50
- if kv_connector_output.is_empty():
51
- return EMPTY_MODEL_RUNNER_OUTPUT
52
-
53
- output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT)
54
- output.kv_connector_output = kv_connector_output
55
- return output
48
+ return ModelRunnerOutput.with_kv_conn_output_only(kv_connector_output)
49
50
@staticmethod
51
def maybe_get_kv_connector_output(