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 15, 02:15 UTC
[KVConnector] Aggregate finished requests on the scheduler (#19555)
Signed-off-by: Or Ozeri <oro@il.ibm.com>
Or Ozeri committed
Jul 10, 2025 at 11:22 UTC
cc876d0f2909ea62f8e96a9fe384162a1680a9c8
5 files changed
+139
-110
vllm/distributed/kv_transfer/kv_connector/v1/base.py
+3
-1
@@ -190,7 +190,9 @@ class KVConnectorBase_V1(ABC):
190
) -> tuple[Optional[set[str]], Optional[set[str]]]:
191
"""
192
Notifies worker-side connector ids of requests that have
193
- finished generating tokens.
193
+ finished generating tokens on the worker.
194
+ The scheduler process (via the MultiprocExecutor) will use this output
195
+ to track which workers are done.
196
197
Returns:
198
ids of requests that have finished asynchronous transfer
vllm/distributed/kv_transfer/kv_connector/v1/nixl_connector.py
+4
-61
@@ -408,14 +408,6 @@ class NixlConnectorWorker:
408
# Track the expiration time of requests that are waiting to be sent.
409
self._reqs_to_send: dict[ReqId, float] = {}
410
411
- # Complete transfer tracker. Used by the rank 0 to track finished
412
- # transactions on ranks 1 to N-1.
413
- # [req_id -> count]
414
- self._done_recving_count: defaultdict[ReqId,
415
- int] = defaultdict(lambda: 0)
416
- self._done_sending_count: defaultdict[ReqId,
417
- int] = defaultdict(lambda: 0)
418
-
411
# Background thread for handling new handshake requests.
412
self._nixl_handshake_listener_t: Optional[threading.Thread] = None
413
# Background thread for initializing new NIXL handshakes.
@@ -830,15 +822,9 @@ class NixlConnectorWorker:
822
823
def get_finished(self) -> tuple[set[str], set[str]]:
824
"""
833
- Get requests that are done sending or recving.
834
-
835
- In TP>1 setup, each rank exchanges KVs with its counterpart
836
- ranks independently. get_finished() runs in a worker creates
837
- the done_sending and done_recving sets that are sent to the
838
- scheduler via ModelRunnerOutput by Rank 0. To ensure trnxs
839
- are done before adding to finished, Ranks 1 to N-1 communicate
840
- to Rank 0 once their transaction is done + Rank 0 returns
841
- finished sets to Scheduler only once all ranks are done.
825
+ Get requests that are done sending or recving on this specific worker.
826
+ The scheduler process (via the MultiprocExecutor) will use this output
827
+ to track which workers are done.
828
"""
829
done_sending = self._get_new_notifs()
830
done_recving = self._pop_done_transfers(self._recving_transfers)
@@ -858,50 +844,7 @@ class NixlConnectorWorker:
844
del self._reqs_to_send[req_id]
845
done_sending.add(req_id)
846
861
- if self.world_size == 1:
862
- return done_sending, done_recving
863
-
864
- # Rank 0: get finished from all other ranks.
865
- if self.tp_rank == 0:
866
- for req_id in done_sending:
867
- self._done_sending_count[req_id] += 1
868
- for req_id in done_recving:
869
- self._done_recving_count[req_id] += 1
870
-
871
- # Keep track of how many other ranks have finished.
872
- other_ranks_finished_ids: list[str] = []
873
- for i in range(1, self.world_size):
874
- other_ranks_finished_ids.extend(
875
- self.tp_group.recv_object(src=i))
876
- for req_id in other_ranks_finished_ids:
877
- if (req_id in self._done_recving_count
878
- or req_id in self._recving_transfers):
879
- self._done_recving_count[req_id] += 1
880
- else:
881
- self._done_sending_count[req_id] += 1
882
-
883
- # Return ids that finished on all ranks to the scheduler.
884
- all_done_recving: set[str] = set()
885
- for req_id in list(self._done_recving_count.keys()):
886
- if self._done_recving_count[req_id] == self.world_size:
887
- del self._done_recving_count[req_id]
888
- all_done_recving.add(req_id)
889
-
890
- all_done_sending: set[str] = set()
891
- for req_id in list(self._done_sending_count.keys()):
892
- if self._done_sending_count[req_id] >= self.world_size:
893
- del self._done_sending_count[req_id]
894
- all_done_sending.add(req_id)
895
-
896
- return all_done_sending, all_done_recving
897
-
898
- # Ranks 1 to N-1: send finished ids to Rank 0.
899
- else:
900
- finished_req_ids = list(done_recving.union(done_sending))
901
- self.tp_group.send_object(finished_req_ids, dst=0)
902
-
903
- # Unused as only Rank 0 results are sent to scheduler.
904
- return done_sending, done_recving
847
+ return done_sending, done_recving
848
849
def _get_new_notifs(self) -> set[str]:
850
"""
vllm/v1/executor/multiproc_executor.py
+105
-5
@@ -9,7 +9,8 @@ import threading
9
import time
10
import traceback
11
import weakref
12
-from concurrent.futures import Future, ThreadPoolExecutor
12
+from collections import defaultdict
13
+from concurrent.futures import CancelledError, Future, ThreadPoolExecutor
14
from dataclasses import dataclass
15
from enum import Enum, auto
16
from functools import partial
@@ -111,10 +112,19 @@ class MultiprocExecutor(Executor):
112
if self.max_concurrent_batches > 1:
113
# Note: must use only 1 IO thread to keep dequeue sequence
114
# from the response queue
115
+ # _async_aggregate_workers_output also assumes a single IO thread
116
self.io_thread_pool = ThreadPoolExecutor(
117
max_workers=1, thread_name_prefix="mp_exec_io")
118
119
self.output_rank = self._get_output_rank()
120
+ self.has_connector = self.vllm_config.kv_transfer_config is not None
121
+
122
+ # Complete transfer tracker. Used by to track finished requests
123
+ # [req_id -> n_finished_workers]
124
+ self._recv_remaining_count = defaultdict[str,
125
+ int](lambda: self.world_size)
126
+ self._send_remaining_count = defaultdict[str,
127
+ int](lambda: self.world_size)
128
129
def start_worker_monitor(self):
130
workers = self.workers
@@ -155,13 +165,29 @@ class MultiprocExecutor(Executor):
165
self,
166
scheduler_output,
167
) -> Union[ModelRunnerOutput, Future[ModelRunnerOutput]]:
158
- (output, ) = self.collective_rpc(
168
+ non_block = self.max_concurrent_batches > 1
169
+
170
+ if not self.has_connector:
171
+ # get output only from a single worker (output_rank)
172
+ (output, ) = self.collective_rpc(
173
+ "execute_model",
174
+ args=(scheduler_output, ),
175
+ unique_reply_rank=self.output_rank,
176
+ non_block=non_block,
177
+ timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS)
178
+ return output
179
+
180
+ # get output from all workers
181
+ outputs = self.collective_rpc(
182
"execute_model",
183
args=(scheduler_output, ),
161
- unique_reply_rank=self.output_rank,
162
- non_block=self.max_concurrent_batches > 1,
184
+ non_block=non_block,
185
timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS)
164
- return output
186
+
187
+ # aggregate all workers output to a single output
188
+ if non_block:
189
+ return self._async_aggregate_workers_output(outputs)
190
+ return self._aggregate_workers_output(outputs)
191
192
def collective_rpc(self,
193
method: Union[str, Callable],
@@ -220,6 +246,80 @@ class MultiprocExecutor(Executor):
246
except TimeoutError as e:
247
raise TimeoutError(f"RPC call to {method} timed out.") from e
248
249
+ def _aggregate_workers_output(
250
+ self, outputs: list[ModelRunnerOutput]) -> ModelRunnerOutput:
251
+ # aggregate finished_sending, finished_recving from all workers
252
+
253
+ finished_sending = set[str]()
254
+ finished_recving = set[str]()
255
+ for output in outputs:
256
+ # update finished_sending
257
+ for req_id in output.finished_sending or []:
258
+ new_count = self._send_remaining_count[req_id] - 1
259
+ if new_count == 0:
260
+ # got response from all workers, report back to scheduler
261
+ finished_sending.add(req_id)
262
+ del self._send_remaining_count[req_id]
263
+ else:
264
+ self._send_remaining_count[req_id] = new_count
265
+
266
+ # update finished_recving
267
+ for req_id in output.finished_recving or []:
268
+ new_count = self._recv_remaining_count[req_id] - 1
269
+ if new_count == 0:
270
+ # got response from all workers, report back to scheduler
271
+ finished_recving.add(req_id)
272
+ del self._recv_remaining_count[req_id]
273
+ else:
274
+ self._recv_remaining_count[req_id] = new_count
275
+
276
+ # select output of the worker specified by output_rank
277
+ output = outputs[self.output_rank]
278
+
279
+ # set the aggregated finished_sending / finished_recving
280
+ if finished_sending:
281
+ output.finished_sending = finished_sending
282
+ if finished_recving:
283
+ output.finished_recving = finished_recving
284
+
285
+ return output
286
+
287
+ def _async_aggregate_workers_output(
288
+ self, output_futures: list[Future[ModelRunnerOutput]]
289
+ ) -> (Future[ModelRunnerOutput]):
290
+ """Takes a list of futures and returns a single future which resolves
291
+ to the respective list of outputs."""
292
+ result_future: Future[ModelRunnerOutput] = Future()
293
+
294
+ outputs: list[Optional[ModelRunnerOutput]] = [None
295
+ ] * len(output_futures)
296
+
297
+ def make_callback(idx):
298
+
299
+ def callback(fut):
300
+ if result_future.done():
301
+ return
302
+
303
+ try:
304
+ outputs[idx] = fut.result()
305
+ except CancelledError:
306
+ result_future.cancel()
307
+ except Exception as e:
308
+ result_future.set_exception(e)
309
+
310
+ # this check assumes io_thread_pool uses a single thread
311
+ if all(outputs):
312
+ result_future.set_result(
313
+ self._aggregate_workers_output(
314
+ cast(list[ModelRunnerOutput], outputs)))
315
+
316
+ return callback
317
+
318
+ for i, output_future in enumerate(output_futures):
319
+ output_future.add_done_callback(make_callback(i))
320
+
321
+ return result_future
322
+
323
@staticmethod
324
def _ensure_worker_termination(worker_procs: list[BaseProcess]):
325
"""Ensure that all worker processes are terminated. Assumes workers have
vllm/v1/worker/gpu_model_runner.py
+6
-40
@@ -1,7 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-import copy
4
import gc
5
import time
6
import weakref
@@ -1234,8 +1233,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1233
hidden_states: torch.Tensor,
1234
num_scheduled_tokens: int,
1235
num_scheduled_tokens_np: np.ndarray,
1237
- finished_sending: Optional[set[str]],
1238
- finished_recving: Optional[set[str]],
1236
) -> ModelRunnerOutput:
1237
assert self.input_batch.num_reqs ==\
1238
len(self.input_batch.pooling_params), \
@@ -1270,8 +1267,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1267
logprobs=None,
1268
prompt_logprobs_dict={},
1269
pooler_output=pooler_output,
1273
- finished_sending=finished_sending,
1274
- finished_recving=finished_recving,
1270
)
1271
1272
@torch.inference_mode()
@@ -1282,11 +1277,12 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1277
) -> Union[ModelRunnerOutput, IntermediateTensors]:
1278
self._update_states(scheduler_output)
1279
if not scheduler_output.total_num_scheduled_tokens:
1285
- if not has_kv_transfer_group():
1286
- # Return empty ModelRunnerOutput if there's no work to do.
1287
- return EMPTY_MODEL_RUNNER_OUTPUT
1280
+ if has_kv_transfer_group():
1281
+ with set_forward_context(None, self.vllm_config):
1282
+ self.maybe_setup_kv_connector(scheduler_output)
1283
1289
- return self.kv_connector_no_forward(scheduler_output)
1284
+ # Return empty ModelRunnerOutput if there's no work to do.
1285
+ return EMPTY_MODEL_RUNNER_OUTPUT
1286
1287
# Prepare the decoder inputs.
1288
(attn_metadata, attention_cuda_graphs, logits_indices,
@@ -1379,8 +1375,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1375
)
1376
1377
self.maybe_wait_for_kv_save()
1382
- finished_sending, finished_recving = (
1383
- self.get_finished_kv_transfers(scheduler_output))
1378
1379
if self.use_aux_hidden_state_outputs:
1380
hidden_states, aux_hidden_states = model_output
@@ -1406,8 +1400,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1400
else:
1401
if self.input_batch.pooling_params:
1402
return self._pool(hidden_states, num_scheduled_tokens,
1409
- num_scheduled_tokens_np, finished_sending,
1410
- finished_recving)
1403
+ num_scheduled_tokens_np)
1404
1405
sample_hidden_states = hidden_states[logits_indices]
1406
logits = self.model.compute_logits(sample_hidden_states, None)
@@ -1560,8 +1553,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1553
logprobs=logprobs_lists,
1554
prompt_logprobs_dict=prompt_logprobs_dict,
1555
pooler_output=[],
1563
- finished_sending=finished_sending,
1564
- finished_recving=finished_recving,
1556
num_nans_in_logits=num_nans_in_logits,
1557
)
1558
@@ -1686,22 +1677,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1677
spec_token_ids = draft_token_ids.tolist()
1678
return spec_token_ids
1679
1689
- def kv_connector_no_forward(
1690
- self, scheduler_output: "SchedulerOutput") -> ModelRunnerOutput:
1691
- # KV send/recv even if no work to do.
1692
- with set_forward_context(None, self.vllm_config):
1693
- self.maybe_setup_kv_connector(scheduler_output)
1694
- finished_sending, finished_recving = (
1695
- self.get_finished_kv_transfers(scheduler_output))
1696
-
1697
- if not finished_sending and not finished_recving:
1698
- return EMPTY_MODEL_RUNNER_OUTPUT
1699
-
1700
- output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT)
1701
- output.finished_sending = finished_sending
1702
- output.finished_recving = finished_recving
1703
- return output
1704
-
1680
@staticmethod
1681
def maybe_setup_kv_connector(scheduler_output: "SchedulerOutput"):
1682
# Update KVConnector with the KVConnector metadata forward().
@@ -1723,15 +1698,6 @@ class GPUModelRunner(LoRAModelRunnerMixin):
1698
if has_kv_transfer_group():
1699
get_kv_transfer_group().wait_for_save()
1700
1726
- @staticmethod
1727
- def get_finished_kv_transfers(
1728
- scheduler_output: "SchedulerOutput",
1729
- ) -> tuple[Optional[set[str]], Optional[set[str]]]:
1730
- if has_kv_transfer_group():
1731
- return get_kv_transfer_group().get_finished(
1732
- scheduler_output.finished_req_ids)
1733
- return None, None
1734
-
1701
def propose_ngram_draft_token_ids(
1702
self,
1703
sampled_token_ids: list[list[int]],
vllm/v1/worker/gpu_worker.py
+21
-3
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
"""A GPU worker class."""
4
+import copy
5
import gc
6
import os
7
from typing import TYPE_CHECKING, Optional
@@ -14,7 +15,9 @@ from vllm.config import VllmConfig
15
from vllm.distributed import (ensure_model_parallel_initialized,
16
init_distributed_environment,
17
set_custom_all_reduce)
17
-from vllm.distributed.kv_transfer import ensure_kv_transfer_initialized
18
+from vllm.distributed.kv_transfer import (ensure_kv_transfer_initialized,
19
+ get_kv_transfer_group,
20
+ has_kv_transfer_group)
21
from vllm.distributed.parallel_state import get_pp_group, get_tp_group
22
from vllm.logger import init_logger
23
from vllm.lora.request import LoRARequest
@@ -23,7 +26,7 @@ from vllm.platforms import current_platform
26
from vllm.sequence import IntermediateTensors
27
from vllm.utils import GiB_bytes, MemorySnapshot, memory_profiling
28
from vllm.v1.kv_cache_interface import KVCacheConfig, KVCacheSpec
26
-from vllm.v1.outputs import ModelRunnerOutput
29
+from vllm.v1.outputs import EMPTY_MODEL_RUNNER_OUTPUT, ModelRunnerOutput
30
from vllm.v1.utils import report_usage_stats
31
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
32
from vllm.v1.worker.worker_base import WorkerBase
@@ -316,14 +319,29 @@ class Worker(WorkerBase):
319
320
output = self.model_runner.execute_model(scheduler_output,
321
intermediate_tensors)
322
+
323
parallel_config = self.vllm_config.parallel_config
324
if parallel_config.distributed_executor_backend != "external_launcher" \
325
and not get_pp_group().is_last_rank:
326
assert isinstance(output, IntermediateTensors)
327
get_pp_group().send_tensor_dict(output.tensors,
328
all_gather_group=get_tp_group())
325
- return None
329
+ output = EMPTY_MODEL_RUNNER_OUTPUT
330
+
331
assert isinstance(output, ModelRunnerOutput)
332
+ if has_kv_transfer_group():
333
+ finished_sending, finished_recving = (
334
+ get_kv_transfer_group().get_finished(
335
+ scheduler_output.finished_req_ids))
336
+ if finished_sending or finished_recving:
337
+ if output is EMPTY_MODEL_RUNNER_OUTPUT:
338
+ output = copy.copy(EMPTY_MODEL_RUNNER_OUTPUT)
339
+ output.finished_sending = finished_sending
340
+ output.finished_recving = finished_recving
341
+ # with a connector, the scheduler expects output from all workers
342
+ return output
343
+
344
+ # return output only from the driver worker
345
return output if self.is_driver_worker else None
346
347
def profile(self, is_start: bool = True):