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
[P/D] Dynamic `kv_output_aggregator` collect size (#26734)
Signed-off-by: NickLucche <nlucches@redhat.com>
Nicolò Lucchesi committed
Oct 22, 2025 at 18:07 UTC
4dfdb821c8e46781ba6a37d828a7c76bcd401ae7
7 files changed
+90
-19
tests/v1/kv_connector/unit/test_nixl_connector.py
+2
-2
@@ -703,7 +703,7 @@ def test_kv_connector_stats_aggregation():
703
704
# Create KVOutputAggregator for 3 workers (simulating TP=3), same thing
705
# done in MultiprocExecutor.execute_model
706
- aggregator = KVOutputAggregator(world_size=3)
706
+ aggregator = KVOutputAggregator(expected_finished_count=3)
707
708
# Create stats for multiple workers with different transfer patterns
709
worker1_stats = NixlKVConnectorStats()
@@ -768,7 +768,7 @@ def test_multi_kv_connector_stats_aggregation():
768
KVOutputAggregator (used by MultiprocExecutor).
769
"""
770
771
- aggregator = KVOutputAggregator(world_size=3)
771
+ aggregator = KVOutputAggregator(expected_finished_count=3)
772
773
from dataclasses import dataclass
774
tests/v1/kv_connector/unit/test_output_aggregator.py
renamed
+41
-2
@@ -16,11 +16,13 @@ class DummyModelRunnerOutput(ModelRunnerOutput):
16
finished_sending: set[str] | None = None,
17
finished_recving: set[str] | None = None,
18
invalid_block_ids: set[int] | None = None,
19
+ expected_finished_count: int = 0,
20
):
21
self.kv_connector_output = KVConnectorOutput(
22
finished_sending=finished_sending,
23
finished_recving=finished_recving,
24
invalid_block_ids=invalid_block_ids or set(),
25
+ expected_finished_count=expected_finished_count,
26
)
27
28
def __repr__(self):
@@ -33,7 +35,7 @@ class DummyModelRunnerOutput(ModelRunnerOutput):
35
36
37
def test_aggregate_workers_output():
36
- aggregator = KVOutputAggregator(world_size=2)
38
+ aggregator = KVOutputAggregator(expected_finished_count=2)
39
40
output1 = DummyModelRunnerOutput()
41
output2 = DummyModelRunnerOutput()
@@ -85,7 +87,7 @@ def test_aggregate_workers_output():
87
88
89
def test_async_aggregate_workers_output():
88
- aggregator = KVOutputAggregator(world_size=2)
90
+ aggregator = KVOutputAggregator(expected_finished_count=2)
91
92
future1: Future[DummyModelRunnerOutput] = Future()
93
future2: Future[DummyModelRunnerOutput] = Future()
@@ -158,3 +160,40 @@ def test_async_aggregate_workers_output():
160
assert aggregated.finished_sending is None
161
assert aggregated.finished_recving == {"req2"}
162
assert aggregated.invalid_block_ids == {3, 4, 5}
163
+
164
+
165
+def test_aggregate_workers_output_with_expected_finished_count():
166
+ # We create the aggregator expecting to collect from 4 workers
167
+ aggregator = KVOutputAggregator(expected_finished_count=4)
168
+ assert aggregator._expected_finished_count == 4
169
+ # Some request with default expected finished requests
170
+ output1 = DummyModelRunnerOutput(finished_sending={"req1"})
171
+ aggregated = aggregator.aggregate([output1])
172
+ # still expecting to collect from 4 workers
173
+ assert aggregator._send_remaining_count["req1"] == 3
174
+ assert not aggregated.kv_connector_output.finished_sending
175
+ assert not aggregated.kv_connector_output.finished_recving
176
+
177
+ # Workers discover and find that in this setup they only need to
178
+ # collect from 2
179
+ output1 = DummyModelRunnerOutput(
180
+ finished_sending={"req1"}, expected_finished_count=2
181
+ )
182
+ output2 = DummyModelRunnerOutput(
183
+ finished_recving={"req2"}, expected_finished_count=2
184
+ )
185
+ output3 = DummyModelRunnerOutput(finished_recving={"req2"})
186
+ # Req2 only needs 2 acks
187
+ aggregated = aggregator.aggregate([output1, output2, output3])
188
+ assert aggregated.kv_connector_output.expected_finished_count == 2
189
+
190
+ assert not aggregated.kv_connector_output.finished_sending
191
+
192
+ # Req2 is finished
193
+ assert "req2" not in aggregator._recv_remaining_count
194
+ assert aggregated.kv_connector_output.finished_recving == {"req2"}
195
+
196
+ # Req1 is still waiting for 2 more acks (expected_finished_count has no effect)
197
+ # NOTE: This is to showcase dynamic update. Workers are responsible for
198
+ # ensuring "req1" termination in this case
199
+ assert aggregator._send_remaining_count["req1"] == 2
vllm/distributed/kv_transfer/kv_connector/utils.py
+30
-6
@@ -4,10 +4,9 @@
4
KV cache helper for store.
5
"""
6
7
-from collections import defaultdict
7
from collections.abc import Sequence
8
from concurrent.futures import CancelledError, Future
10
-from typing import Literal, cast
9
+from typing import TYPE_CHECKING, Literal, cast
10
11
import torch
12
@@ -18,6 +17,9 @@ from vllm.distributed.kv_transfer.kv_connector.factory import KVConnectorFactory
17
from vllm.logger import init_logger
18
from vllm.v1.outputs import KVConnectorOutput, ModelRunnerOutput
19
20
+if TYPE_CHECKING:
21
+ from vllm.distributed.kv_transfer.kv_connector.base import KVConnectorBase
22
+
23
logger = init_logger(__name__)
24
25
@@ -124,11 +126,16 @@ class KVOutputAggregator:
126
"""Utility class to aggregate the output of all workers into a single
127
output corresponding to Rank 0 for scheduler."""
128
127
- def __init__(self, world_size: int):
129
+ def __init__(self, expected_finished_count: int):
130
# Complete transfer tracker. Used to track finished requests
131
# [req_id -> n_remaining_workers]
130
- self._recv_remaining_count = defaultdict[str, int](lambda: world_size)
131
- self._send_remaining_count = defaultdict[str, int](lambda: world_size)
132
+ self._recv_remaining_count = dict[str, int]()
133
+ self._send_remaining_count = dict[str, int]()
134
+ self._expected_finished_count = expected_finished_count
135
+
136
+ @classmethod
137
+ def from_connector(cls, connector: "KVConnectorBase", world_size: int):
138
+ return cls(connector.get_finished_count() or world_size)
139
140
def aggregate(
141
self, outputs: list[ModelRunnerOutput], output_rank: int = 0
@@ -141,7 +148,10 @@ class KVOutputAggregator:
148
finished_set: set[str],
149
) -> None:
150
for req_id in req_ids or ():
144
- remaining_count_dict[req_id] -= 1
151
+ remaining_count = remaining_count_dict.get(
152
+ req_id, self._expected_finished_count
153
+ )
154
+ remaining_count_dict[req_id] = remaining_count - 1
155
if remaining_count_dict[req_id] == 0:
156
finished_set.add(req_id)
157
del remaining_count_dict[req_id]
@@ -154,6 +164,19 @@ class KVOutputAggregator:
164
kv_output = model_runner_output.kv_connector_output
165
if not kv_output:
166
continue
167
+ # Allow the worker to dynamically update the expected number of
168
+ # finished sending/recving for new requests.
169
+ if (
170
+ kv_output.expected_finished_count > 0
171
+ and kv_output.expected_finished_count != self._expected_finished_count
172
+ ):
173
+ logger.debug(
174
+ "Expected finished requests updated from %d to %d",
175
+ self._expected_finished_count,
176
+ kv_output.expected_finished_count,
177
+ )
178
+ self._expected_finished_count = kv_output.expected_finished_count
179
+
180
update_finished_set(
181
kv_output.finished_sending, self._send_remaining_count, finished_sending
182
)
@@ -186,6 +209,7 @@ class KVOutputAggregator:
209
finished_recving=finished_recving or None,
210
kv_connector_stats=aggregated_kv_connector_stats or None,
211
invalid_block_ids=invalid_block_ids,
212
+ expected_finished_count=self._expected_finished_count,
213
)
214
215
return output
vllm/distributed/kv_transfer/kv_connector/v1/base.py
+2
-1
@@ -413,7 +413,8 @@ class KVConnectorBase_V1(ABC):
413
def get_finished_count(self) -> int | None:
414
"""
415
Get the count of requests expected to complete send/receive operations
416
- via this connector.
416
+ via this connector. This method is used to initialize the
417
+ KVOutputAggregator, overwriting the default world_size.
418
419
Returns:
420
int: expected sending or receiving completion count.
vllm/v1/engine/core.py
+1
-3
@@ -160,9 +160,7 @@ class EngineCore:
160
)
161
self.use_spec_decode = vllm_config.speculative_config is not None
162
if self.scheduler.connector is not None: # type: ignore
163
- self.model_executor.init_kv_output_aggregator(
164
- self.scheduler.connector.get_finished_count() # type: ignore
165
- )
163
+ self.model_executor.init_kv_output_aggregator(self.scheduler.connector) # type: ignore
164
165
self.mm_registry = mm_registry = MULTIMODAL_REGISTRY
166
self.mm_receiver_cache = engine_receiver_cache_from_config(
vllm/v1/executor/abstract.py
+7
-4
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
5
from collections.abc import Callable
6
from concurrent.futures import Future
7
from functools import cached_property
8
-from typing import Literal, TypeVar, overload
8
+from typing import TYPE_CHECKING, Literal, TypeVar, overload
9
10
from vllm.config import VllmConfig
11
from vllm.distributed.kv_transfer.kv_connector.utils import KVOutputAggregator
@@ -19,6 +19,9 @@ from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
19
from vllm.v1.outputs import DraftTokenIds, ModelRunnerOutput
20
from vllm.v1.worker.worker_base import WorkerBase
21
22
+if TYPE_CHECKING:
23
+ from vllm.distributed.kv_transfer.kv_connector.base import KVConnectorBase
24
+
25
logger = init_logger(__name__)
26
27
_R = TypeVar("_R")
@@ -233,10 +236,10 @@ class Executor(ABC):
236
"""Shutdown the executor."""
237
self.collective_rpc("shutdown")
238
236
- def init_kv_output_aggregator(self, finished_count: int | None) -> None:
239
+ def init_kv_output_aggregator(self, connector: "KVConnectorBase") -> None:
240
"""Init KVOutputAggregator"""
238
- self.kv_output_aggregator = KVOutputAggregator(
239
- finished_count or self.parallel_config.world_size
241
+ self.kv_output_aggregator = KVOutputAggregator.from_connector(
242
+ connector, self.parallel_config.world_size
243
)
244
245
@cached_property # Avoid unnecessary RPC calls
vllm/v1/outputs.py
+7
-1
@@ -86,8 +86,14 @@ class KVConnectorOutput:
86
finished_recving: set[str] | None = None
87
kv_connector_stats: KVConnectorStats | None = None
88
# IDs of externally computed KV blocks that failed to load.
89
- # Requests referencing these blocks should be rescheduled to recompute them.
89
+ # Requests referencing these blocks should be rescheduled to recompute them
90
invalid_block_ids: set[int] = field(default_factory=set)
91
+ # Configuration describing how many finished sending/receiving
92
+ # notifications should be expected for each request. This allows
93
+ # handshake-based connectors like Nixl to update the KVOutputAggregator.
94
+ # It captures a static setup info and should almost always remain constant
95
+ # for a given connector after discovery. Default value entails no change.
96
+ expected_finished_count: int = 0
97
98
def is_empty(self):
99
return (