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
[V0 Deprecation] Remove V0 MP executor (#25329)
Signed-off-by: Woosuk Kwon <woosuk.kwon@berkeley.edu>
Woosuk Kwon committed
Sep 20, 2025 at 21:26 UTC
7ed82d1974837957a3bfb6d576b9cffba24d31ae
3 files changed
+33
-530
vllm/executor/mp_distributed_executor.py
deleted
-244
@@ -1,244 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-
4
-import asyncio
5
-import os
6
-from typing import Any, Callable, List, Optional, Union
7
-
8
-import cloudpickle
9
-
10
-from vllm.executor.executor_base import DistributedExecutorBase
11
-from vllm.executor.multiproc_worker_utils import (
12
- ProcessWorkerWrapper, ResultHandler, WorkerMonitor,
13
- set_multiprocessing_worker_envs)
14
-from vllm.logger import init_logger
15
-from vllm.model_executor.layers.sampler import SamplerOutput
16
-from vllm.sequence import ExecuteModelRequest
17
-from vllm.utils import (_run_task_with_lock, cuda_device_count_stateless,
18
- get_distributed_init_method, get_ip, get_open_port,
19
- make_async, run_method, update_environment_variables)
20
-from vllm.worker.worker_base import WorkerWrapperBase
21
-
22
-logger = init_logger(__name__)
23
-
24
-
25
-class MultiprocessingDistributedExecutor(DistributedExecutorBase):
26
- """Python multiprocessing-based distributed executor"""
27
-
28
- uses_ray: bool = False
29
-
30
- def _check_cuda(self) -> None:
31
- """Check that the number of GPUs is sufficient for the parallel
32
- configuration. Separate from _init_executor to reduce the number of
33
- indented blocks.
34
- """
35
- parallel_config = self.parallel_config
36
- world_size = parallel_config.world_size
37
- tensor_parallel_size = parallel_config.tensor_parallel_size
38
-
39
- cuda_device_count = cuda_device_count_stateless()
40
- # Use confusing message for more common TP-only case.
41
- if tensor_parallel_size > cuda_device_count:
42
- raise RuntimeError(
43
- f"please set tensor_parallel_size ({tensor_parallel_size}) "
44
- f"to less than max local gpu count ({cuda_device_count})")
45
-
46
- if world_size > cuda_device_count:
47
- raise RuntimeError(
48
- f"please ensure that world_size ({world_size}) "
49
- f"is less than than max local gpu count ({cuda_device_count})")
50
-
51
- # Set CUDA_VISIBLE_DEVICES for the driver, inherited by workers
52
- if "CUDA_VISIBLE_DEVICES" not in os.environ:
53
- update_environment_variables({
54
- "CUDA_VISIBLE_DEVICES": (",".join(map(str, range(world_size))))
55
- })
56
-
57
- def _init_executor(self) -> None:
58
-
59
- from vllm.platforms import current_platform
60
- if current_platform.is_cuda_alike():
61
- self._check_cuda()
62
-
63
- # Create the parallel GPU workers.
64
- world_size = self.parallel_config.world_size
65
- tensor_parallel_size = self.parallel_config.tensor_parallel_size
66
-
67
- # Set multiprocessing envs that are common to V0 and V1
68
- set_multiprocessing_worker_envs(self.parallel_config)
69
-
70
- # Multiprocessing-based executor does not support multi-node setting.
71
- # Since it only works for single node, we can use the loopback address
72
- # 127.0.0.1 for communication.
73
- distributed_init_method = get_distributed_init_method(
74
- "127.0.0.1", get_open_port())
75
-
76
- self.workers: List[ProcessWorkerWrapper] = []
77
- # This is the list of workers that are rank 0 of each TP group EXCEPT
78
- # global rank 0. These are the workers that will broadcast to the
79
- # rest of the workers.
80
- self.tp_driver_workers: List[ProcessWorkerWrapper] = []
81
- # This is the list of workers that are not drivers and not the first
82
- # worker in a TP group. These are the workers that will be
83
- # broadcasted to.
84
- self.non_driver_workers: List[ProcessWorkerWrapper] = []
85
-
86
- if world_size == 1:
87
- self.worker_monitor = None
88
- else:
89
- result_handler = ResultHandler()
90
- for rank in range(1, world_size):
91
- worker = ProcessWorkerWrapper(result_handler,
92
- WorkerWrapperBase,
93
- self.vllm_config, rank)
94
- self.workers.append(worker)
95
- if rank % tensor_parallel_size == 0:
96
- self.tp_driver_workers.append(worker)
97
- else:
98
- self.non_driver_workers.append(worker)
99
-
100
- self.worker_monitor = WorkerMonitor(self.workers, result_handler)
101
- result_handler.start()
102
- self.worker_monitor.start()
103
-
104
- # Set up signal handlers to shut down the executor cleanly
105
- # sometimes gc does not work well
106
-
107
- self.driver_worker = WorkerWrapperBase(self.vllm_config, 0)
108
-
109
- all_kwargs = []
110
- distributed_init_method = get_distributed_init_method(
111
- get_ip(), get_open_port())
112
- for i in range(world_size):
113
- local_rank = i
114
- rank = i
115
- kwargs = dict(
116
- vllm_config=self.vllm_config,
117
- local_rank=local_rank,
118
- rank=rank,
119
- distributed_init_method=distributed_init_method,
120
- is_driver_worker=(not self.parallel_config)
121
- or (rank % self.parallel_config.tensor_parallel_size == 0),
122
- )
123
- all_kwargs.append(kwargs)
124
- self._run_workers("init_worker", all_kwargs)
125
- self._run_workers("init_device")
126
- self._run_workers("load_model",
127
- max_concurrent_workers=self.parallel_config.
128
- max_parallel_loading_workers)
129
- self.driver_exec_model = make_async(self.driver_worker.execute_model)
130
- self.pp_locks: Optional[List[asyncio.Lock]] = None
131
-
132
- def shutdown(self):
133
- if (worker_monitor := getattr(self, "worker_monitor",
134
- None)) is not None:
135
- worker_monitor.close()
136
-
137
- def _driver_execute_model(
138
- self, execute_model_req: Optional[ExecuteModelRequest]
139
- ) -> Optional[List[SamplerOutput]]:
140
- """Run execute_model in the driver worker.
141
-
142
- Passing None will cause the driver to stop the model execution
143
- loop running in each of the remote workers.
144
- """
145
- return self.driver_worker.execute_model(execute_model_req)
146
-
147
- def _run_workers(
148
- self,
149
- method: Union[str, Callable],
150
- *args,
151
- async_run_tensor_parallel_workers_only: bool = False,
152
- max_concurrent_workers: Optional[int] = None,
153
- **kwargs,
154
- ) -> List[Any]:
155
- """Runs the given method on all workers.
156
-
157
- Args:
158
- async_run_tensor_parallel_workers_only: If True the method will be
159
- run only in the remote TP workers, not the driver worker.
160
- It will also be run asynchronously and return a list of futures
161
- rather than blocking on the results.
162
- """
163
- if isinstance(method, str):
164
- sent_method = method
165
- else:
166
- sent_method = cloudpickle.dumps(method)
167
- del method
168
-
169
- if max_concurrent_workers:
170
- raise NotImplementedError(
171
- "max_concurrent_workers is not supported yet.")
172
-
173
- if async_run_tensor_parallel_workers_only:
174
- # Run only non-driver workers and just return futures.
175
- return [
176
- worker.execute_method(sent_method, *args, **kwargs)
177
- for worker in self.non_driver_workers
178
- ]
179
-
180
- # Start all remote workers first.
181
- worker_outputs = [
182
- worker.execute_method(sent_method, *args, **kwargs)
183
- for worker in self.workers
184
- ]
185
-
186
- driver_worker_output = run_method(self.driver_worker, sent_method,
187
- args, kwargs)
188
-
189
- # Get the results of the workers.
190
- return [driver_worker_output
191
- ] + [output.get() for output in worker_outputs]
192
-
193
- def check_health(self) -> None:
194
- """Raises an error if engine is unhealthy."""
195
- if self.worker_monitor is not None and not self.worker_monitor.is_alive(
196
- ):
197
- raise RuntimeError("Worker processes are not running")
198
-
199
- def _wait_for_tasks_completion(self, parallel_worker_tasks: Any) -> None:
200
- """Wait for futures returned from _run_workers() with
201
- async_run_remote_workers_only to complete."""
202
- for result in parallel_worker_tasks:
203
- result.get()
204
-
205
- async def _driver_execute_model_async(
206
- self,
207
- execute_model_req: Optional[ExecuteModelRequest] = None
208
- ) -> List[SamplerOutput]:
209
- if not self.tp_driver_workers:
210
- return await self.driver_exec_model(execute_model_req)
211
-
212
- if self.pp_locks is None:
213
- # This locks each pipeline parallel stage so multiple virtual
214
- # engines can't execute on the same stage at the same time
215
- # We create the locks here to avoid creating them in the constructor
216
- # which uses a different asyncio loop.
217
- self.pp_locks = [
218
- asyncio.Lock()
219
- for _ in range(self.parallel_config.pipeline_parallel_size)
220
- ]
221
-
222
- tasks = [
223
- asyncio.create_task(
224
- _run_task_with_lock(self.driver_exec_model, self.pp_locks[0],
225
- execute_model_req))
226
- ]
227
- for pp_rank, driver_worker in enumerate(self.tp_driver_workers,
228
- start=1):
229
- tasks.append(
230
- asyncio.create_task(
231
- _run_task_with_lock(driver_worker.execute_method_async,
232
- self.pp_locks[pp_rank],
233
- "execute_model", execute_model_req)))
234
- results = await asyncio.gather(*tasks)
235
-
236
- # Only the last PP stage has the final results.
237
- return results[-1]
238
-
239
- async def _start_worker_execution_loop(self):
240
- coros = [
241
- worker.execute_method_async("start_worker_execution_loop")
242
- for worker in self.non_driver_workers
243
- ]
244
- return await asyncio.gather(*coros)
vllm/executor/multiproc_worker_utils.py
deleted
-279
@@ -1,279 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-
4
-import asyncio
5
-import os
6
-import threading
7
-import uuid
8
-from dataclasses import dataclass
9
-from multiprocessing import Queue
10
-from multiprocessing.connection import wait
11
-from multiprocessing.process import BaseProcess
12
-from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union
13
-
14
-import torch
15
-
16
-from vllm.config import VllmConfig
17
-from vllm.logger import init_logger
18
-from vllm.utils import (_maybe_force_spawn, decorate_logs, get_mp_context,
19
- run_method)
20
-
21
-logger = init_logger(__name__)
22
-
23
-T = TypeVar('T')
24
-
25
-_TERMINATE = "TERMINATE" # sentinel
26
-
27
-JOIN_TIMEOUT_S = 2
28
-
29
-
30
-@dataclass
31
-class Result(Generic[T]):
32
- """Result of task dispatched to worker"""
33
-
34
- task_id: uuid.UUID
35
- value: Optional[T] = None
36
- exception: Optional[BaseException] = None
37
-
38
-
39
-class ResultFuture(threading.Event, Generic[T]):
40
- """Synchronous future for non-async case"""
41
-
42
- def __init__(self):
43
- super().__init__()
44
- self.result: Optional[Result[T]] = None
45
-
46
- def set_result(self, result: Result[T]):
47
- self.result = result
48
- self.set()
49
-
50
- def get(self) -> T:
51
- self.wait()
52
- assert self.result is not None
53
- if self.result.exception is not None:
54
- raise self.result.exception
55
- return self.result.value # type: ignore[return-value]
56
-
57
-
58
-def _set_future_result(future: Union[ResultFuture, asyncio.Future],
59
- result: Result):
60
- if isinstance(future, ResultFuture):
61
- future.set_result(result)
62
- return
63
- loop = future.get_loop()
64
- if not loop.is_closed():
65
- if result.exception is not None:
66
- loop.call_soon_threadsafe(future.set_exception, result.exception)
67
- else:
68
- loop.call_soon_threadsafe(future.set_result, result.value)
69
-
70
-
71
-class ResultHandler(threading.Thread):
72
- """Handle results from all workers (in background thread)"""
73
-
74
- def __init__(self) -> None:
75
- super().__init__(daemon=True)
76
- self.result_queue = get_mp_context().Queue()
77
- self.tasks: Dict[uuid.UUID, Union[ResultFuture, asyncio.Future]] = {}
78
-
79
- def run(self):
80
- for result in iter(self.result_queue.get, _TERMINATE):
81
- future = self.tasks.pop(result.task_id)
82
- _set_future_result(future, result)
83
- # Ensure that all waiters will receive an exception
84
- for task_id, future in self.tasks.items():
85
- _set_future_result(
86
- future,
87
- Result(task_id=task_id,
88
- exception=ChildProcessError("worker died")))
89
-
90
- def close(self):
91
- self.result_queue.put(_TERMINATE)
92
-
93
-
94
-class WorkerMonitor(threading.Thread):
95
- """Monitor worker status (in background thread)"""
96
-
97
- def __init__(self, workers: List['ProcessWorkerWrapper'],
98
- result_handler: ResultHandler):
99
- super().__init__(daemon=True)
100
- self.workers = workers
101
- self.result_handler = result_handler
102
- self._close = False
103
-
104
- def run(self) -> None:
105
- # Blocks until any worker exits
106
- dead_sentinels = wait([w.process.sentinel for w in self.workers])
107
- if not self._close:
108
- self._close = True
109
-
110
- # Kill / cleanup all workers
111
- for worker in self.workers:
112
- process = worker.process
113
- if process.sentinel in dead_sentinels:
114
- process.join(JOIN_TIMEOUT_S)
115
- if process.exitcode is not None and process.exitcode != 0:
116
- logger.error("Worker %s pid %s died, exit code: %s",
117
- process.name, process.pid, process.exitcode)
118
- # Cleanup any remaining workers
119
- if logger:
120
- logger.info("Killing local vLLM worker processes")
121
- for worker in self.workers:
122
- worker.kill_worker()
123
- # Must be done after worker task queues are all closed
124
- self.result_handler.close()
125
-
126
- for worker in self.workers:
127
- worker.process.join(JOIN_TIMEOUT_S)
128
-
129
- def close(self):
130
- if self._close:
131
- return
132
- self._close = True
133
- logger.info("Terminating local vLLM worker processes")
134
- for worker in self.workers:
135
- worker.terminate_worker()
136
- # Must be done after worker task queues are all closed
137
- self.result_handler.close()
138
-
139
-
140
-class ProcessWorkerWrapper:
141
- """Local process wrapper for vllm.worker.Worker,
142
- for handling single-node multi-GPU tensor parallel."""
143
-
144
- def __init__(self, result_handler: ResultHandler,
145
- worker_factory: Callable[[VllmConfig, int], Any],
146
- vllm_config: VllmConfig, rank: int) -> None:
147
- self.mp = get_mp_context()
148
- self._task_queue = self.mp.Queue()
149
- self.result_queue = result_handler.result_queue
150
- self.tasks = result_handler.tasks
151
- self.process: BaseProcess = self.mp.Process( # type: ignore[attr-defined]
152
- target=_run_worker_process,
153
- name="VllmWorkerProcess",
154
- kwargs=dict(
155
- worker_factory=worker_factory,
156
- task_queue=self._task_queue,
157
- result_queue=self.result_queue,
158
- vllm_config=vllm_config,
159
- rank=rank,
160
- ),
161
- daemon=True)
162
-
163
- self.process.start()
164
-
165
- def _enqueue_task(self, future: Union[ResultFuture, asyncio.Future],
166
- method: Union[str, bytes], args, kwargs):
167
- task_id = uuid.uuid4()
168
- self.tasks[task_id] = future
169
- try:
170
- self._task_queue.put((task_id, method, args, kwargs))
171
- except SystemExit:
172
- raise
173
- except BaseException as e:
174
- del self.tasks[task_id]
175
- raise ChildProcessError("worker died") from e
176
-
177
- def execute_method(self, method: Union[str, bytes], *args, **kwargs):
178
- future: ResultFuture = ResultFuture()
179
- self._enqueue_task(future, method, args, kwargs)
180
- return future
181
-
182
- async def execute_method_async(self, method: Union[str, bytes], *args,
183
- **kwargs):
184
- future = asyncio.get_running_loop().create_future()
185
- self._enqueue_task(future, method, args, kwargs)
186
- return await future
187
-
188
- def terminate_worker(self):
189
- try:
190
- self._task_queue.put(_TERMINATE)
191
- except ValueError:
192
- self.process.kill()
193
- self._task_queue.close()
194
-
195
- def kill_worker(self):
196
- self._task_queue.close()
197
- self.process.kill()
198
-
199
-
200
-def _run_worker_process(
201
- worker_factory: Callable[[VllmConfig, int], Any],
202
- task_queue: Queue,
203
- result_queue: Queue,
204
- vllm_config: VllmConfig,
205
- rank: int,
206
-) -> None:
207
- """Worker process event loop"""
208
-
209
- # Add process-specific prefix to stdout and stderr
210
- process_name = get_mp_context().current_process().name
211
- decorate_logs(process_name)
212
-
213
- # Initialize worker
214
- worker = worker_factory(vllm_config, rank)
215
- del worker_factory
216
-
217
- # Accept tasks from the engine in task_queue
218
- # and return task output in result_queue
219
- logger.info("Worker ready; awaiting tasks")
220
- try:
221
- for items in iter(task_queue.get, _TERMINATE):
222
- output = None
223
- exception = None
224
- task_id, method, args, kwargs = items
225
- try:
226
- output = run_method(worker, method, args, kwargs)
227
- except SystemExit:
228
- raise
229
- except KeyboardInterrupt:
230
- break
231
- except BaseException as e:
232
- logger.exception(
233
- "Exception in worker %s while processing method %s.",
234
- process_name, method)
235
- exception = e
236
- result_queue.put(
237
- Result(task_id=task_id, value=output, exception=exception))
238
- except KeyboardInterrupt:
239
- pass
240
- except Exception:
241
- logger.exception("Worker failed")
242
-
243
- # Flush TunableOp results when TunableOp is enabled and
244
- # online (in situ) tuning is enabled.
245
- # Offline tuning API (record_untuned_is_enabled()) only
246
- # available in PyTorch 2.6 or later.
247
- if torch.cuda.is_available():
248
- import torch.cuda.tunable as tunable
249
- if (tunable.is_enabled() and tunable.tuning_is_enabled()
250
- and not tunable.record_untuned_is_enabled()):
251
- tunable.write_file()
252
-
253
- logger.info("Worker exiting")
254
-
255
-
256
-def set_multiprocessing_worker_envs(parallel_config):
257
- """ Set up environment variables that should be used when there are workers
258
- in a multiprocessing environment. This should be called by the parent
259
- process before worker processes are created"""
260
-
261
- _maybe_force_spawn()
262
-
263
- # Configure thread parallelism if OMP_NUM_THREADS isn't set
264
- #
265
- # Helps to avoid CPU contention. The default of spawning a thread per
266
- # core combined with multiprocessing for each GPU can have a negative
267
- # impact on performance. The contention is amplified when running in a
268
- # container where CPU limits can cause throttling.
269
- default_omp_num_threads = 1
270
- if "OMP_NUM_THREADS" not in os.environ and (
271
- current_parallelism :=
272
- torch.get_num_threads()) > default_omp_num_threads:
273
- logger.warning(
274
- "Reducing Torch parallelism from %d threads to %d to avoid "
275
- "unnecessary CPU contention. Set OMP_NUM_THREADS in the "
276
- "external environment to tune this value as needed.",
277
- current_parallelism, default_omp_num_threads)
278
- os.environ["OMP_NUM_THREADS"] = str(default_omp_num_threads)
279
- torch.set_num_threads(default_omp_num_threads)
vllm/v1/executor/multiproc_executor.py
+33
-7
@@ -1,6 +1,7 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
import multiprocessing
4
+import os
5
import pickle
6
import queue
7
import signal
@@ -19,6 +20,7 @@ from threading import Thread
20
from typing import Any, Callable, Optional, Union, cast
21
22
import cloudpickle
23
+import torch
24
25
import vllm.envs as envs
26
from vllm.config import VllmConfig
@@ -28,14 +30,12 @@ from vllm.distributed.device_communicators.shm_broadcast import (Handle,
30
MessageQueue)
31
from vllm.distributed.parallel_state import (get_dp_group, get_ep_group,
32
get_pp_group, get_tp_group)
31
-from vllm.executor.multiproc_worker_utils import (
32
- set_multiprocessing_worker_envs)
33
from vllm.logger import init_logger
34
from vllm.multimodal import MULTIMODAL_REGISTRY
35
from vllm.multimodal.cache import worker_receiver_cache_from_config
36
-from vllm.utils import (decorate_logs, get_distributed_init_method,
37
- get_loopback_ip, get_mp_context, get_open_port,
38
- set_process_title)
36
+from vllm.utils import (_maybe_force_spawn, decorate_logs,
37
+ get_distributed_init_method, get_loopback_ip,
38
+ get_mp_context, get_open_port, set_process_title)
39
from vllm.v1.core.sched.output import SchedulerOutput
40
from vllm.v1.executor.abstract import Executor, FailureCallback
41
from vllm.v1.executor.utils import get_and_update_mm_cache
@@ -67,8 +67,8 @@ class MultiprocExecutor(Executor):
67
f"tensor_parallel_size ({tensor_parallel_size}) x pipeline"
68
f"_parallel_size ({pp_parallel_size}). ")
69
70
- # Set multiprocessing envs that are common to V0 and V1
71
- set_multiprocessing_worker_envs(self.parallel_config)
70
+ # Set multiprocessing envs
71
+ set_multiprocessing_worker_envs()
72
73
# Multiprocessing-based executor does not support multi-node setting.
74
# Since it only works for single node, we can use the loopback address
@@ -698,3 +698,29 @@ class WorkerProc:
698
process_name += f"_EP{ep_rank}"
699
set_process_title(name=process_name)
700
decorate_logs(process_name)
701
+
702
+
703
+def set_multiprocessing_worker_envs():
704
+ """ Set up environment variables that should be used when there are workers
705
+ in a multiprocessing environment. This should be called by the parent
706
+ process before worker processes are created"""
707
+
708
+ _maybe_force_spawn()
709
+
710
+ # Configure thread parallelism if OMP_NUM_THREADS isn't set
711
+ #
712
+ # Helps to avoid CPU contention. The default of spawning a thread per
713
+ # core combined with multiprocessing for each GPU can have a negative
714
+ # impact on performance. The contention is amplified when running in a
715
+ # container where CPU limits can cause throttling.
716
+ default_omp_num_threads = 1
717
+ if "OMP_NUM_THREADS" not in os.environ and (
718
+ current_parallelism :=
719
+ torch.get_num_threads()) > default_omp_num_threads:
720
+ logger.warning(
721
+ "Reducing Torch parallelism from %d threads to %d to avoid "
722
+ "unnecessary CPU contention. Set OMP_NUM_THREADS in the "
723
+ "external environment to tune this value as needed.",
724
+ current_parallelism, default_omp_num_threads)
725
+ os.environ["OMP_NUM_THREADS"] = str(default_omp_num_threads)
726
+ torch.set_num_threads(default_omp_num_threads)