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, 15:45 UTC
Revert "[PerfFix] Avoid separate thread for MP executor shm spin (#28012)" (#28289)
Signed-off-by: NickLucche <nlucches@redhat.com>
Nicolò Lucchesi committed
Nov 7, 2025 at 16:07 UTC
68a72a5cc1e29198730d1b2471e23675d9b964dd
9 files changed
+131
-143
tests/v1/executor/test_executor.py
+1
-2
@@ -4,7 +4,6 @@
4
import asyncio
5
import os
6
from collections.abc import Callable
7
-from concurrent.futures import Future
7
from typing import Any
8
9
import pytest
@@ -28,7 +27,7 @@ class CustomMultiprocExecutor(MultiprocExecutor):
27
kwargs: dict | None = None,
28
non_block: bool = False,
29
unique_reply_rank: int | None = None,
31
- ) -> Any | list[Any] | Future[Any | list[Any]]:
30
+ ) -> list[Any]:
31
# Drop marker to show that this was run
32
with open(".marker", "w"):
33
...
tests/v1/kv_connector/unit/test_output_aggregator.py
+20
-12
@@ -89,12 +89,14 @@ def test_aggregate_workers_output():
89
def test_async_aggregate_workers_output():
90
aggregator = KVOutputAggregator(expected_finished_count=2)
91
92
- future: Future[list[DummyModelRunnerOutput]] = Future()
93
- result_future = aggregator.async_aggregate(future)
92
+ future1: Future[DummyModelRunnerOutput] = Future()
93
+ future2: Future[DummyModelRunnerOutput] = Future()
94
+ result_future = aggregator.async_aggregate([future1, future2])
95
96
output1 = DummyModelRunnerOutput()
97
output2 = DummyModelRunnerOutput()
97
- future.set_result([output1, output2])
98
+ future1.set_result(output1)
99
+ future2.set_result(output2)
100
101
assert result_future.done()
102
aggregated = result_future.result()
@@ -104,14 +106,16 @@ def test_async_aggregate_workers_output():
106
assert aggregated.finished_recving is None
107
assert not aggregated.invalid_block_ids
108
107
- future = Future()
108
- result_future = aggregator.async_aggregate(future)
109
+ future1 = Future()
110
+ future2 = Future()
111
+ result_future = aggregator.async_aggregate([future1, future2])
112
113
output1 = DummyModelRunnerOutput(
114
finished_sending={"req1"}, finished_recving={"req2"}
115
)
116
output2 = DummyModelRunnerOutput(invalid_block_ids={1})
114
- future.set_result([output1, output2])
117
+ future1.set_result(output1)
118
+ future2.set_result(output2)
119
120
assert result_future.done()
121
aggregated = result_future.result()
@@ -121,12 +125,14 @@ def test_async_aggregate_workers_output():
125
assert aggregated.finished_recving is None
126
assert aggregated.invalid_block_ids == {1}
127
124
- future = Future()
125
- result_future = aggregator.async_aggregate(future)
128
+ future1 = Future()
129
+ future2 = Future()
130
+ result_future = aggregator.async_aggregate([future1, future2])
131
132
output1 = DummyModelRunnerOutput(invalid_block_ids={2})
133
output2 = DummyModelRunnerOutput(finished_sending={"req1"})
129
- future.set_result([output1, output2])
134
+ future1.set_result(output1)
135
+ future2.set_result(output2)
136
137
assert result_future.done()
138
aggregated = result_future.result()
@@ -136,14 +142,16 @@ def test_async_aggregate_workers_output():
142
assert aggregated.finished_recving is None
143
assert aggregated.invalid_block_ids == {2}
144
139
- future = Future()
140
- result_future = aggregator.async_aggregate(future)
145
+ future1 = Future()
146
+ future2 = Future()
147
+ result_future = aggregator.async_aggregate([future1, future2])
148
149
output1 = DummyModelRunnerOutput(invalid_block_ids={3, 4})
150
output2 = DummyModelRunnerOutput(
151
finished_recving={"req2"}, invalid_block_ids={4, 5}
152
)
146
- future.set_result([output1, output2])
153
+ future1.set_result(output1)
154
+ future2.set_result(output2)
155
156
assert result_future.done()
157
aggregated = result_future.result()
vllm/distributed/kv_transfer/kv_connector/utils.py
+29
-14
@@ -221,24 +221,39 @@ class KVOutputAggregator:
221
222
def async_aggregate(
223
self,
224
- output_future: Future[Sequence[ModelRunnerOutput | None]],
224
+ output_futures: Sequence[Future[ModelRunnerOutput | None]],
225
output_rank: int = 0,
226
) -> Future[ModelRunnerOutput | None]:
227
- """Takes a future that resolves to a list of outputs and returns a future
228
- which resolves to a single aggregated output."""
227
+ """Takes a list of futures and returns a single future which resolves
228
+ to the respective list of outputs."""
229
result_future: Future[ModelRunnerOutput | None] = Future()
230
231
- def callback(fut):
232
- if result_future.done():
233
- return
234
- try:
235
- result_future.set_result(self.aggregate(fut.result(), output_rank))
236
- except CancelledError:
237
- result_future.cancel()
238
- except Exception as e:
239
- result_future.set_exception(e)
240
-
241
- output_future.add_done_callback(callback)
231
+ outputs: list[ModelRunnerOutput | None] = [None] * len(output_futures)
232
+ remaining = len(output_futures)
233
+
234
+ def make_callback(idx):
235
+ def callback(fut):
236
+ if result_future.done():
237
+ return
238
+
239
+ try:
240
+ outputs[idx] = fut.result()
241
+ except CancelledError:
242
+ result_future.cancel()
243
+ except Exception as e:
244
+ result_future.set_exception(e)
245
+
246
+ # this check assumes io_thread_pool uses a single thread
247
+ nonlocal remaining
248
+ remaining -= 1
249
+ if not remaining:
250
+ result_future.set_result(self.aggregate(outputs, output_rank))
251
+
252
+ return callback
253
+
254
+ for i, output_future in enumerate(output_futures):
255
+ output_future.add_done_callback(make_callback(i))
256
+
257
return result_future
258
259
vllm/v1/executor/abstract.py
+2
-2
@@ -171,7 +171,7 @@ class Executor(ABC):
171
args: tuple = (),
172
kwargs: dict | None = None,
173
non_block: Literal[True] = True,
174
- ) -> Future[list[_R]]:
174
+ ) -> list[Future[_R]]:
175
pass
176
177
@abstractmethod
@@ -219,7 +219,7 @@ class Executor(ABC):
219
220
def sample_tokens(
221
self, grammar_output: GrammarOutput | None, non_block: bool = False
222
- ) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
222
+ ) -> ModelRunnerOutput | Future[ModelRunnerOutput]:
223
output = self.collective_rpc( # type: ignore[call-overload]
224
"sample_tokens", args=(grammar_output,), non_block=non_block
225
)
vllm/v1/executor/multiproc_executor.py
+61
-67
@@ -9,10 +9,8 @@ import threading
9
import time
10
import traceback
11
import weakref
12
-from collections import deque
12
from collections.abc import Callable
14
-from concurrent.futures import Future, InvalidStateError
15
-from contextlib import suppress
13
+from concurrent.futures import Future, ThreadPoolExecutor
14
from dataclasses import dataclass
15
from enum import Enum, auto
16
from functools import cached_property, partial
@@ -56,30 +54,6 @@ from vllm.v1.worker.worker_base import WorkerWrapperBase
54
logger = init_logger(__name__)
55
56
59
-class FutureWrapper(Future):
60
- def __init__(self, futures_queue: deque[tuple["FutureWrapper", Callable]]):
61
- self.futures_queue = futures_queue
62
- super().__init__()
63
-
64
- def result(self, timeout=None):
65
- if timeout is not None:
66
- raise RuntimeError("timeout not implemented")
67
- # Drain any futures ahead of us in the queue.
68
- while not self.done():
69
- future, get_response = self.futures_queue.pop()
70
- future.wait_for_response(get_response)
71
- return super().result()
72
-
73
- def wait_for_response(self, get_response: Callable):
74
- try:
75
- response = get_response()
76
- with suppress(InvalidStateError):
77
- self.set_result(response)
78
- except Exception as e:
79
- with suppress(InvalidStateError):
80
- self.set_exception(e)
81
-
82
-
57
class MultiprocExecutor(Executor):
58
supports_pp: bool = True
59
@@ -90,6 +64,7 @@ class MultiprocExecutor(Executor):
64
self.is_failed = False
65
self.shutdown_event = threading.Event()
66
self.failure_callback: FailureCallback | None = None
67
+ self.io_thread_pool: ThreadPoolExecutor | None = None
68
69
self.world_size = self.parallel_config.world_size
70
tensor_parallel_size = self.parallel_config.tensor_parallel_size
@@ -157,7 +132,12 @@ class MultiprocExecutor(Executor):
132
uw.death_writer.close()
133
self._ensure_worker_termination([uw.proc for uw in unready_workers])
134
160
- self.futures_queue = deque[tuple[FutureWrapper, Callable]]()
135
+ # Note: must use only 1 IO thread to keep dequeue sequence
136
+ # from the response queue.
137
+ # _async_aggregate_workers_output also assumes a single IO thread.
138
+ self.io_thread_pool = ThreadPoolExecutor(
139
+ max_workers=1, thread_name_prefix="mp_exec_io"
140
+ )
141
142
self.output_rank = self._get_output_rank()
143
self.has_connector = self.vllm_config.kv_transfer_config is not None
@@ -215,13 +195,14 @@ class MultiprocExecutor(Executor):
195
) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
196
if not self.has_connector:
197
# get output only from a single worker (output_rank)
218
- return self.collective_rpc(
198
+ (output,) = self.collective_rpc(
199
method,
200
args=args,
201
unique_reply_rank=self.output_rank,
202
non_block=non_block,
203
timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS,
204
)
205
+ return output
206
207
# get output from all workers
208
outputs = self.collective_rpc(
@@ -242,11 +223,12 @@ class MultiprocExecutor(Executor):
223
224
def take_draft_token_ids(self) -> DraftTokenIds | None:
225
# OPTIMIZATION: Get output only from a single worker (output_rank)
245
- return self.collective_rpc(
226
+ outputs = self.collective_rpc(
227
"take_draft_token_ids", unique_reply_rank=self.output_rank
228
)
229
+ return outputs[0]
230
249
- def collective_rpc( # type: ignore[override]
231
+ def collective_rpc(
232
self,
233
method: str | Callable,
234
timeout: float | None = None,
@@ -254,9 +236,7 @@ class MultiprocExecutor(Executor):
236
kwargs: dict | None = None,
237
non_block: bool = False,
238
unique_reply_rank: int | None = None,
257
- ) -> Any | list[Any] | Future[Any | list[Any]]:
258
- """Returns single result if unique_reply_rank is provided, otherwise list."""
259
-
239
+ ) -> list[Any]:
240
if self.is_failed:
241
raise RuntimeError("Executor failed.")
242
@@ -266,52 +246,63 @@ class MultiprocExecutor(Executor):
246
# NOTE: If the args are heterogeneous, then we pack them into a list,
247
# and unpack them in the method of every worker, because every worker
248
# knows their own rank.
249
+ try:
250
+ if isinstance(method, str):
251
+ send_method = method
252
+ else:
253
+ send_method = cloudpickle.dumps(
254
+ method, protocol=pickle.HIGHEST_PROTOCOL
255
+ )
256
+ self.rpc_broadcast_mq.enqueue(
257
+ (send_method, args, kwargs, unique_reply_rank)
258
+ )
259
270
- if isinstance(method, str):
271
- send_method = method
272
- else:
273
- send_method = cloudpickle.dumps(method, protocol=pickle.HIGHEST_PROTOCOL)
274
- self.rpc_broadcast_mq.enqueue((send_method, args, kwargs, unique_reply_rank))
260
+ workers = (
261
+ (self.workers[unique_reply_rank],)
262
+ if unique_reply_rank is not None
263
+ else self.workers
264
+ )
265
+ responses = []
266
276
- workers = (
277
- (self.workers[unique_reply_rank],)
278
- if unique_reply_rank is not None
279
- else self.workers
280
- )
267
+ def get_response(
268
+ w: WorkerProcHandle,
269
+ dequeue_timeout: float | None = None,
270
+ cancel_event: threading.Event | None = None,
271
+ ):
272
+ status, result = w.worker_response_mq.dequeue(
273
+ timeout=dequeue_timeout, cancel=cancel_event
274
+ )
275
282
- shutdown_event = self.shutdown_event
276
+ if status != WorkerProc.ResponseStatus.SUCCESS:
277
+ raise RuntimeError(
278
+ f"Worker failed with error '{result}', please check the"
279
+ " stack trace above for the root cause"
280
+ )
281
+ return result
282
284
- def get_response():
285
- responses = []
283
for w in workers:
284
dequeue_timeout = (
285
None if deadline is None else (deadline - time.monotonic())
286
)
290
- try:
291
- status, result = w.worker_response_mq.dequeue(
292
- timeout=dequeue_timeout, cancel=shutdown_event
287
+
288
+ if self.io_thread_pool is not None:
289
+ # We must consume worker_response_mq from a single thread.
290
+ result = self.io_thread_pool.submit( # type: ignore
291
+ get_response, w, dequeue_timeout, self.shutdown_event
292
)
294
- except TimeoutError as e:
295
- raise TimeoutError(f"RPC call to {method} timed out.") from e
296
- if status != WorkerProc.ResponseStatus.SUCCESS:
293
+ if not non_block:
294
+ result = result.result()
295
+ elif not non_block:
296
+ result = get_response(w, dequeue_timeout, self.shutdown_event)
297
+ else:
298
raise RuntimeError(
298
- f"Worker failed with error '{result}', please check the"
299
- " stack trace above for the root cause"
299
+ "non_block can only be used when max_concurrent_batches > 1"
300
)
301
responses.append(result)
302
- return responses[0] if unique_reply_rank is not None else responses
303
-
304
- if non_block:
305
- future = FutureWrapper(self.futures_queue)
306
- self.futures_queue.appendleft((future, get_response))
307
- return future
308
-
309
- # First drain any pending futures in the queue.
310
- while self.futures_queue:
311
- future, get_fut_response = self.futures_queue.pop()
312
- future.wait_for_response(get_fut_response)
302
314
- return get_response()
303
+ return responses
304
+ except TimeoutError as e:
305
+ raise TimeoutError(f"RPC call to {method} timed out.") from e
306
307
@staticmethod
308
def _ensure_worker_termination(worker_procs: list[BaseProcess]):
@@ -357,6 +348,9 @@ class MultiprocExecutor(Executor):
348
self._ensure_worker_termination([w.proc for w in workers])
349
350
self.shutdown_event.set()
351
+ if self.io_thread_pool is not None:
352
+ self.io_thread_pool.shutdown(wait=False, cancel_futures=True)
353
+ del self.io_thread_pool
354
355
self.rpc_broadcast_mq = None
356
vllm/v1/executor/ray_executor.py
+6
-5
@@ -435,25 +435,26 @@ class RayDistributedExecutor(Executor):
435
436
# When PP is used, we return a FutureWrapper immediately so that
437
# the scheduler can yield to the next batch.
438
- return FutureWrapper(refs[0])
438
+ return FutureWrapper(refs)
439
440
# Get output from all workers when connector is present
441
assert self.kv_output_aggregator is not None
442
if not non_block:
443
# Block and get results from all workers
444
- return self.kv_output_aggregator.aggregate(ray.get(refs))
444
+ outputs = [ref.get() for ref in refs]
445
+ return self.kv_output_aggregator.aggregate(outputs)
446
447
# Return a future that will aggregate outputs from all workers
448
return FutureWrapper(refs, self.kv_output_aggregator)
449
449
- def collective_rpc( # type: ignore[override]
450
+ def collective_rpc(
451
self,
452
method: str | Callable,
453
timeout: float | None = None,
454
args: tuple = (),
455
kwargs: dict[str, Any] | None = None,
456
non_block: bool = False,
456
- ) -> list[Any] | Future[list[Any]]:
457
+ ) -> list[Any]:
458
"""Runs the given method on all workers."""
459
sent_method = method if isinstance(method, str) else cloudpickle.dumps(method)
460
del method
@@ -469,7 +470,7 @@ class RayDistributedExecutor(Executor):
470
471
# Get the results of the ray workers.
472
if non_block:
472
- return FutureWrapper(ray_worker_outputs)
473
+ return [FutureWrapper((output,)) for output in ray_worker_outputs]
474
475
return ray.get(ray_worker_outputs, timeout=timeout)
476
vllm/v1/executor/ray_utils.py
+4
-4
@@ -141,19 +141,19 @@ class FutureWrapper(Future):
141
the result() call. If not only the first worker's output is returned.
142
"""
143
144
- def __init__(self, ref_or_refs, aggregator: KVOutputAggregator | None = None):
144
+ def __init__(self, refs, aggregator: KVOutputAggregator | None = None):
145
super().__init__()
146
- self.ref_or_refs = ref_or_refs
146
+ self.refs = refs
147
self.aggregator = aggregator
148
149
def result(self, timeout=None):
150
if timeout is not None:
151
raise NotImplementedError("timeout is not supported")
152
153
- outputs = ray.get(self.ref_or_refs, timeout=timeout)
153
if self.aggregator is None:
155
- return outputs
154
+ return self.refs[0].get()
155
156
+ outputs = [ref.get() for ref in self.refs]
157
return self.aggregator.aggregate(outputs, output_rank=0)
158
159
vllm/v1/executor/uniproc_executor.py
+7
-36
@@ -13,10 +13,9 @@ import torch.distributed as dist
13
import vllm.envs as envs
14
from vllm.logger import init_logger
15
from vllm.utils.network_utils import get_distributed_init_method, get_ip, get_open_port
16
-from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
16
from vllm.v1.engine import ReconfigureDistributedRequest, ReconfigureRankType
17
from vllm.v1.executor.abstract import Executor
19
-from vllm.v1.outputs import AsyncModelRunnerOutput, DraftTokenIds, ModelRunnerOutput
18
+from vllm.v1.outputs import AsyncModelRunnerOutput
19
from vllm.v1.serial_utils import run_method
20
from vllm.v1.worker.worker_base import WorkerWrapperBase
21
@@ -59,60 +58,32 @@ class UniProcExecutor(Executor):
58
def max_concurrent_batches(self) -> int:
59
return 2 if self.scheduler_config.async_scheduling else 1
60
62
- def collective_rpc( # type: ignore[override]
61
+ def collective_rpc(
62
self,
63
method: str | Callable,
64
timeout: float | None = None,
65
args: tuple = (),
66
kwargs: dict | None = None,
67
non_block: bool = False,
69
- single_value: bool = False,
70
- ) -> Any | list[Any] | Future[Any | list[Any]]:
68
+ ) -> list[Any]:
69
if kwargs is None:
70
kwargs = {}
71
72
if not non_block:
75
- result = run_method(self.driver_worker, method, args, kwargs)
76
- return result if single_value else [result]
73
+ return [run_method(self.driver_worker, method, args, kwargs)]
74
75
try:
76
result = run_method(self.driver_worker, method, args, kwargs)
77
if isinstance(result, AsyncModelRunnerOutput):
78
if (async_thread := self.async_output_thread) is not None:
82
- get_output = result.get_output
83
- if not single_value:
84
- get_output = lambda: [get_output()]
85
- return async_thread.submit(get_output)
79
+ return [async_thread.submit(result.get_output)]
80
result = result.get_output()
81
future = Future[Any]()
88
- future.set_result(result if single_value else [result])
82
+ future.set_result(result)
83
except Exception as e:
84
future = Future[Any]()
85
future.set_exception(e)
92
- return future
93
-
94
- def execute_model( # type: ignore[override]
95
- self, scheduler_output: SchedulerOutput, non_block: bool = False
96
- ) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
97
- return self.collective_rpc(
98
- "execute_model",
99
- args=(scheduler_output,),
100
- non_block=non_block,
101
- single_value=True,
102
- )
103
-
104
- def sample_tokens( # type: ignore[override]
105
- self, grammar_output: GrammarOutput | None, non_block: bool = False
106
- ) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
107
- return self.collective_rpc(
108
- "sample_tokens",
109
- args=(grammar_output,),
110
- non_block=non_block,
111
- single_value=True,
112
- )
113
-
114
- def take_draft_token_ids(self) -> DraftTokenIds | None:
115
- return self.collective_rpc("take_draft_token_ids", single_value=True)
86
+ return [future]
87
88
def check_health(self) -> None:
89
# UniProcExecutor will always be healthy as long as
vllm/v1/worker/gpu_worker.py
+1
-1
@@ -524,7 +524,7 @@ class Worker(WorkerBase):
524
525
@torch.inference_mode()
526
def sample_tokens(
527
- self, grammar_output: "GrammarOutput | None"
527
+ self, grammar_output: "GrammarOutput"
528
) -> ModelRunnerOutput | AsyncModelRunnerOutput:
529
return self.model_runner.sample_tokens(grammar_output)
530