main
py 1,129 lines 44.4 KB
Raw
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
8 import threading
9 import time
10 import traceback
11 import weakref
12 from collections import deque
13 from collections.abc import Callable, Sequence
14 from concurrent.futures import Future, InvalidStateError
15 from contextlib import suppress
16 from dataclasses import dataclass
17 from enum import Enum, auto
18 from functools import partial
19 from multiprocessing.connection import Connection
20 from multiprocessing.process import BaseProcess
21 from multiprocessing.synchronize import Lock as LockType
22 from threading import Thread
23 from typing import Any, cast
24
25 import cloudpickle
26 import torch
27
28 import vllm.envs as envs
29 from vllm.config import VllmConfig
30 from vllm.distributed import destroy_distributed_environment, destroy_model_parallel
31 from vllm.distributed.device_communicators.shm_broadcast import Handle, MessageQueue
32 from vllm.distributed.ec_transfer.ec_connector.utils import ECOutputAggregator
33 from vllm.distributed.kv_transfer.kv_connector.utils import KVOutputAggregator
34 from vllm.distributed.parallel_state import (
35 get_dcp_group,
36 get_dp_group,
37 get_ep_group,
38 get_inner_dp_world_group,
39 get_pcp_group,
40 get_pp_group,
41 get_tp_group,
42 model_parallel_is_initialized,
43 )
44 from vllm.envs import enable_envs_cache
45 from vllm.logger import configure_logging, init_logger
46 from vllm.platforms import current_platform
47 from vllm.tracing import instrument, maybe_init_worker_tracer
48 from vllm.utils import numa_utils
49 from vllm.utils.network_utils import (
50 aiter_requires_tcp_store,
51 get_distributed_init_method,
52 get_file_store_init_method,
53 get_ip,
54 get_loopback_ip,
55 get_open_port,
56 )
57 from vllm.utils.ompmultiprocessing import OMPProcessManager
58 from vllm.utils.system_utils import (
59 _maybe_force_spawn,
60 decorate_logs,
61 get_mp_context,
62 set_process_title,
63 )
64 from vllm.utils.torch_utils import (
65 OMP_NUM_THREADS_SET_BY_VLLM,
66 set_torch_threads_for_runtime,
67 startup_omp_num_threads,
68 )
69 from vllm.v1.core.sched.output import GrammarOutput, SchedulerOutput
70 from vllm.v1.executor.abstract import Executor, FailureCallback
71 from vllm.v1.executor.vllm_net_devices import set_worker_net_device
72 from vllm.v1.outputs import AsyncModelRunnerOutput, DraftTokenIds, ModelRunnerOutput
73 from vllm.v1.worker.worker_base import WorkerWrapperBase
74
75 logger = init_logger(__name__)
76
77
78 class FutureWrapper(Future):
79 def __init__(
80 self,
81 futures_queue: deque["FutureWrapper"],
82 get_response: Callable[[], Any],
83 aggregate: Callable = lambda x: x,
84 ):
85 self.futures_queue = futures_queue
86 self.get_response = get_response
87 self.aggregate = aggregate
88 super().__init__()
89 self.futures_queue.appendleft(self)
90
91 def result(self, timeout=None):
92 if timeout is not None:
93 raise RuntimeError("timeout not implemented")
94
95 # Drain any futures ahead of us in the queue.
96 while not self.done():
97 future = self.futures_queue.pop()
98 future._wait_for_response()
99 return super().result()
100
101 def _wait_for_response(self):
102 try:
103 response = self.aggregate(self.get_response())
104 with suppress(InvalidStateError):
105 self.set_result(response)
106 except Exception as e:
107 with suppress(InvalidStateError):
108 self.set_exception(e)
109
110
111 class MultiprocExecutor(Executor):
112 supports_pp: bool = True
113
114 def __init__(self, vllm_config: VllmConfig, monitor_workers: bool = True):
115 self.monitor_workers = monitor_workers
116 super().__init__(vllm_config)
117
118 def _init_executor(self) -> None:
119 # Call self.shutdown at exit to clean up
120 # and ensure workers will be terminated.
121 self._finalizer = weakref.finalize(self, self.shutdown)
122 self.is_failed = False
123 self.failure_callback: FailureCallback | None = None
124
125 tp_size, pp_size, pcp_size = self._get_parallel_sizes()
126 assert self.world_size == tp_size * pp_size * pcp_size, (
127 f"world_size ({self.world_size}) must be equal to the "
128 f"tensor_parallel_size ({tp_size}) x pipeline"
129 f"_parallel_size ({pp_size}) x prefill_context"
130 f"_parallel_size ({pcp_size}). "
131 )
132
133 num_local_procs = self.local_world_size * max(
134 1, self.parallel_config.data_parallel_size_local
135 )
136 set_multiprocessing_worker_envs(num_local_procs)
137
138 if aiter_requires_tcp_store():
139 distributed_init_method = get_distributed_init_method(
140 get_loopback_ip(), get_open_port()
141 )
142 else:
143 distributed_init_method = get_file_store_init_method()
144 self.rpc_broadcast_mq: MessageQueue | None = None
145 scheduler_output_handle: Handle | None = None
146 # Initialize worker and set up message queues for SchedulerOutputs
147 # and ModelRunnerOutputs
148 if self.parallel_config.node_rank_within_dp == 0:
149 # For leader node within each dp rank,
150 # each dp will have its own leader multiproc executor.
151 max_chunk_bytes = envs.VLLM_MQ_MAX_CHUNK_BYTES_MB * 1024 * 1024
152 mq_connect_ip = get_ip()
153 logger.info(
154 "DP group leader: node_rank=%d, node_rank_within_dp=%d, "
155 "master_addr=%s, mq_connect_ip=%s (local), "
156 "world_size=%d, local_world_size=%d",
157 self.parallel_config.node_rank,
158 self.parallel_config.node_rank_within_dp,
159 self.parallel_config.master_addr,
160 mq_connect_ip,
161 self.world_size,
162 self.local_world_size,
163 )
164 self.rpc_broadcast_mq = MessageQueue(
165 self.world_size,
166 self.local_world_size,
167 max_chunk_bytes=max_chunk_bytes,
168 connect_ip=mq_connect_ip,
169 )
170 scheduler_output_handle = self.rpc_broadcast_mq.export_handle()
171 # Create workers
172 context = get_mp_context()
173 shared_worker_lock = context.Lock()
174 unready_workers: list[UnreadyWorkerProcHandle] = []
175 success = False
176 try:
177 global_start_rank = (
178 self.local_world_size * self.parallel_config.node_rank_within_dp
179 )
180 # When using fork, keep track of socket file descriptors that are
181 # inherited by the worker, so that we can close them in subsequent
182 # workers
183 inherited_fds: list[int] | None = (
184 [] if context.get_start_method() == "fork" else None
185 )
186
187 # For CPU backend only, to setup OpenMP threads affinity
188 cpu_omp_manager = OMPProcessManager(self.vllm_config)
189 for local_rank in range(self.local_world_size):
190 global_rank = global_start_rank + local_rank
191 is_driver_worker = self._is_driver_worker(global_rank)
192 with cpu_omp_manager.configure_omp_envs(
193 rank=global_rank, local_rank=local_rank
194 ):
195 unready_worker_handle = WorkerProc.make_worker_process(
196 vllm_config=self.vllm_config,
197 local_rank=local_rank,
198 rank=global_rank,
199 distributed_init_method=distributed_init_method,
200 input_shm_handle=scheduler_output_handle,
201 shared_worker_lock=shared_worker_lock,
202 is_driver_worker=is_driver_worker,
203 inherited_fds=inherited_fds,
204 )
205 unready_workers.append(unready_worker_handle)
206 if inherited_fds is not None:
207 inherited_fds.append(unready_worker_handle.death_writer.fileno())
208 inherited_fds.append(unready_worker_handle.ready_pipe.fileno())
209
210 # Workers must be created before wait_for_ready to avoid
211 # deadlock, since worker.init_device() does a device sync.
212
213 # Wait for all local workers to be ready.
214 self.workers = WorkerProc.wait_for_ready(unready_workers)
215
216 # The workers have inherited their thread count (see
217 # set_multiprocessing_worker_envs); this process only schedules, so
218 # it gets no benefit from torch intra-op parallelism, just CPU
219 # contention with them.
220 set_torch_threads_for_runtime()
221
222 # Start background thread to monitor worker health if not in headless mode.
223 if self.monitor_workers:
224 self.start_worker_monitor()
225
226 self.response_mqs = []
227 # Only leader node have remote response mqs
228 if self.parallel_config.node_rank_within_dp == 0:
229 for rank in range(self.world_size):
230 if rank < self.local_world_size:
231 local_message_queue = self.workers[rank].worker_response_mq
232 assert local_message_queue is not None
233 self.response_mqs.append(local_message_queue)
234 else:
235 remote_message_queue = self.workers[0].peer_worker_response_mqs[
236 rank
237 ]
238 assert remote_message_queue is not None
239 self.response_mqs.append(remote_message_queue)
240
241 # Ensure message queues are ready. Will deadlock if re-ordered
242 # Must be kept consistent with the WorkerProc.
243
244 # Wait for all input mqs to be ready.
245 if self.rpc_broadcast_mq is not None:
246 self.rpc_broadcast_mq.wait_until_ready()
247 # Wait for all remote response mqs to be ready.
248 for response_mq in self.response_mqs:
249 response_mq.wait_until_ready()
250
251 self.futures_queue = deque[FutureWrapper]()
252
253 self._post_init_executor()
254
255 success = True
256 finally:
257 if not success:
258 # Clean up the worker procs if there was a failure.
259 # Close death_writers first to signal workers to exit
260 for uw in unready_workers:
261 if uw.death_writer is not None:
262 uw.death_writer.close()
263 uw.death_writer = None
264 self._ensure_worker_termination([uw.proc for uw in unready_workers])
265
266 self.output_rank = self._get_output_rank()
267
268 def get_response_mqs(self, unique_reply_rank: int = -1) -> list[MessageQueue]:
269 assert unique_reply_rank >= -1 and unique_reply_rank < self.world_size, (
270 f"unique_reply_rank must be -1 or < world_size,"
271 f"unique_reply_rank = {unique_reply_rank}, "
272 f"world_size={self.world_size}"
273 )
274 ranks = (
275 [unique_reply_rank] if unique_reply_rank != -1 else range(self.world_size)
276 )
277 return [self.workers[rank].worker_response_mq for rank in ranks]
278
279 def _get_parallel_sizes(self) -> tuple[int, int, int]:
280 self.world_size = self.parallel_config.world_size
281 assert self.world_size % self.parallel_config.nnodes_within_dp == 0, (
282 f"global world_size ({self.parallel_config.world_size}) must be "
283 f"divisible by nnodes_within_dp "
284 f"({self.parallel_config.nnodes_within_dp}). "
285 )
286 self.local_world_size = self.parallel_config.local_world_size
287 tp_size = self.parallel_config.tensor_parallel_size
288 pp_size = self.parallel_config.pipeline_parallel_size
289 pcp_size = self.parallel_config.prefill_context_parallel_size
290 return tp_size, pp_size, pcp_size
291
292 def _post_init_executor(self) -> None:
293 pass
294
295 def _is_driver_worker(self, rank: int) -> bool:
296 return rank % self.parallel_config.tensor_parallel_size == 0
297
298 def start_worker_monitor(self, inline=False) -> None:
299 workers = self.workers
300 self_ref = weakref.ref(self)
301
302 # Monitors worker process liveness. If any die unexpectedly,
303 # logs an error, shuts down the executor and invokes the failure
304 # callback to inform the engine.
305 def monitor_workers():
306 sentinels = [h.proc.sentinel for h in workers]
307 died = multiprocessing.connection.wait(sentinels)
308 _self = self_ref()
309 if not _self or getattr(_self, "shutting_down", False):
310 logger.debug("MultiprocWorkerMonitor: shutdown already initiated")
311 return
312 _self.is_failed = True
313 proc = next(h.proc for h in workers if h.proc.sentinel == died[0])
314 logger.error(
315 "Worker proc %s died unexpectedly (exit code: %s), "
316 "shutting down executor.",
317 proc.name,
318 proc.exitcode,
319 )
320 _self.shutdown()
321 callback = _self.failure_callback
322 if callback is not None:
323 _self.failure_callback = None
324 callback()
325
326 if not inline:
327 Thread(
328 target=monitor_workers, daemon=True, name="MultiprocWorkerMonitor"
329 ).start()
330 return
331
332 monitor_workers()
333
334 def register_failure_callback(self, callback: FailureCallback):
335 if self.is_failed:
336 callback()
337 else:
338 self.failure_callback = callback
339
340 def execute_model( # type: ignore[override]
341 self, scheduler_output: SchedulerOutput, non_block: bool = False
342 ) -> ModelRunnerOutput | None | Future[ModelRunnerOutput | None]:
343 return self.collective_rpc(
344 "execute_model",
345 args=(scheduler_output,),
346 unique_reply_rank=self.output_rank,
347 non_block=non_block,
348 timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS,
349 kv_output_aggregator=self.kv_output_aggregator,
350 ec_output_aggregator=self.ec_output_aggregator,
351 )
352
353 def sample_tokens( # type: ignore[override]
354 self, grammar_output: GrammarOutput | None, non_block: bool = False
355 ) -> ModelRunnerOutput | Future[ModelRunnerOutput]:
356 return self.collective_rpc(
357 "sample_tokens",
358 args=(grammar_output,),
359 unique_reply_rank=self.output_rank,
360 non_block=non_block,
361 timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS,
362 kv_output_aggregator=self.kv_output_aggregator,
363 ec_output_aggregator=self.ec_output_aggregator,
364 )
365
366 def execute_dummy_batch(self) -> None:
367 self.collective_rpc("execute_dummy_batch", unique_reply_rank=self.output_rank)
368
369 def take_draft_token_ids(self) -> DraftTokenIds | None:
370 # OPTIMIZATION: Get output only from a single worker (output_rank)
371 return self.collective_rpc(
372 "take_draft_token_ids",
373 unique_reply_rank=self.output_rank,
374 timeout=envs.VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS,
375 )
376
377 def collective_rpc( # type: ignore[override]
378 self,
379 method: str | Callable,
380 timeout: float | None = None,
381 args: tuple = (),
382 kwargs: dict | None = None,
383 non_block: bool = False,
384 unique_reply_rank: int | None = None,
385 kv_output_aggregator: KVOutputAggregator | None = None,
386 ec_output_aggregator: ECOutputAggregator | None = None,
387 ) -> Any:
388 """Returns single result if unique_reply_rank and/or an output
389 aggregator is provided, otherwise list."""
390 assert self.rpc_broadcast_mq is not None, (
391 "collective_rpc should not be called on follower node"
392 )
393 if self.is_failed:
394 raise RuntimeError("Executor failed.")
395
396 deadline = None if timeout is None else time.monotonic() + timeout
397 kwargs = kwargs or {}
398
399 aggregators = [a for a in (kv_output_aggregator, ec_output_aggregator) if a]
400 aggregate: Callable[[Any], Any]
401 if aggregators:
402 output_rank = None
403
404 def _aggregate(outputs: Any) -> Any:
405 # Each aggregator merges its own connector's output onto
406 # outputs[output_rank] in place and returns it, so chaining is safe.
407 rank = unique_reply_rank or 0
408 result = outputs[rank]
409 for a in aggregators:
410 result = a.aggregate(outputs, output_rank=rank)
411 return result
412
413 aggregate = _aggregate
414 else:
415 output_rank = unique_reply_rank
416 aggregate = lambda x: x
417
418 if isinstance(method, str):
419 send_method = method
420 else:
421 send_method = cloudpickle.dumps(method, protocol=pickle.HIGHEST_PROTOCOL)
422 self.rpc_broadcast_mq.enqueue((send_method, args, kwargs, output_rank))
423
424 response_mqs: Sequence[MessageQueue] = self.response_mqs
425 if output_rank is not None:
426 response_mqs = (response_mqs[output_rank],)
427
428 def get_response():
429 responses = []
430 for mq in response_mqs:
431 dequeue_timeout = (
432 None if deadline is None else max(0.0, deadline - time.monotonic())
433 )
434 try:
435 status, result = mq.dequeue(timeout=dequeue_timeout)
436 except TimeoutError as e:
437 raise TimeoutError(f"RPC call to {method} timed out.") from e
438 if status != WorkerProc.ResponseStatus.SUCCESS:
439 raise RuntimeError(
440 f"Worker failed with error '{result}', please check the"
441 " stack trace above for the root cause"
442 )
443 responses.append(result)
444 return responses[0] if output_rank is not None else responses
445
446 future = FutureWrapper(
447 self.futures_queue, get_response=get_response, aggregate=aggregate
448 )
449
450 return future if non_block else future.result()
451
452 @staticmethod
453 def _ensure_worker_termination(worker_procs: list[BaseProcess]):
454 """Ensure that all worker processes are terminated. Assumes workers have
455 received termination requests. Waits for processing, then sends
456 termination and kill signals if needed."""
457
458 def wait_for_termination(procs, timeout):
459 if not time:
460 # If we are in late stage shutdown, the interpreter may replace
461 # `time` with `None`.
462 return all(not proc.is_alive() for proc in procs)
463 start_time = time.time()
464 while time.time() - start_time < timeout:
465 if all(not proc.is_alive() for proc in procs):
466 return True
467 time.sleep(0.1)
468 return False
469
470 active_procs = lambda: [proc for proc in worker_procs if proc.is_alive()]
471 initial_count = len(active_procs())
472
473 # Give processes time to clean themselves up properly first
474 logger.info(
475 "[shutdown] Executor: waiting for worker exit count=%d",
476 initial_count,
477 )
478 if wait_for_termination(
479 active_procs(), timeout=envs.VLLM_WORKER_SHUTDOWN_TIMEOUT_SECONDS
480 ):
481 logger.info_once("[shutdown] Executor: all workers exited gracefully")
482 return
483
484 # Send SIGTERM if still running
485 remaining = active_procs()
486 logger.warning(
487 "[shutdown] Executor: workers still running after grace period; "
488 "sending SIGTERM count=%d",
489 len(remaining),
490 )
491 for p in remaining:
492 p.terminate()
493 if not wait_for_termination(active_procs(), 4):
494 # Send SIGKILL if still running
495 remaining = active_procs()
496 logger.warning(
497 "[shutdown] Executor: workers still running after SIGTERM; "
498 "sending SIGKILL count=%d",
499 len(remaining),
500 )
501 for p in remaining:
502 p.kill()
503
504 def shutdown(self):
505 """Properly shut down the executor and its workers."""
506 if not getattr(self, "shutting_down", False):
507 worker_count = len(getattr(self, "workers", None) or [])
508 logger.debug(
509 "[shutdown] Executor: start worker_count=%d",
510 worker_count,
511 )
512 self.shutting_down = True
513
514 # Make sure all the worker processes are terminated first.
515 if workers := getattr(self, "workers", None):
516 for w in workers:
517 # Close death_writer to signal child processes to exit
518 if w.death_writer is not None:
519 w.death_writer.close()
520 w.death_writer = None
521 self._ensure_worker_termination([w.proc for w in workers])
522
523 for w in workers:
524 # Shutdown response queues
525 if w.worker_response_mq is not None:
526 w.worker_response_mq.shutdown()
527 w.worker_response_mq = None
528
529 if rpc_broadcast_mq := getattr(self, "rpc_broadcast_mq", None):
530 rpc_broadcast_mq.shutdown()
531 self.rpc_broadcast_mq = None
532 if response_mqs := getattr(self, "response_mqs", None):
533 for mq in response_mqs:
534 mq.shutdown()
535 self.response_mqs = []
536
537 logger.debug_once("[shutdown] Executor: complete")
538
539 def check_health(self) -> None:
540 self.collective_rpc("check_health", timeout=10)
541 return
542
543 def _get_output_rank(self) -> int:
544 # Only returns ModelRunnerOutput from TP rank=0 and PP rank=-1
545 # (the first TP worker of the last PP stage).
546 # Example:
547 # Assuming TP=8, PP=4, then the world_size=32
548 # 0-7, PP rank 0
549 # 8-15, PP rank 1
550 # 16-23, PP rank 2
551 # 24-31, PP rank 3
552 # so world_size - tp_size = 32 - 8 = 24 should be PP rank = -1 (i.e. 3)
553 return (
554 self.world_size
555 - self.parallel_config.tensor_parallel_size
556 * self.parallel_config.prefill_context_parallel_size
557 )
558
559 @classmethod
560 def supports_async_scheduling(cls) -> bool:
561 return True
562
563
564 @dataclass
565 class UnreadyWorkerProcHandle:
566 """WorkerProcess handle before READY."""
567
568 proc: BaseProcess
569 rank: int
570 ready_pipe: Connection
571 death_writer: Connection | None = None
572
573
574 @dataclass
575 class WorkerProcHandle:
576 proc: BaseProcess
577 rank: int
578 # The worker process writes to this MQ in single-node mode
579 worker_response_mq: MessageQueue | None
580 # This is only non empty on driver node,
581 # the peer worker process i writes to MQ
582 # `peer_worker_response_mqs[i]`
583 peer_worker_response_mqs: list[MessageQueue | None]
584 death_writer: Connection | None = None
585
586 @classmethod
587 def from_unready_handle(
588 cls,
589 unready_handle: UnreadyWorkerProcHandle,
590 worker_response_mq: MessageQueue | None,
591 peer_worker_response_mqs: list[MessageQueue | None],
592 ) -> "WorkerProcHandle":
593 return cls(
594 proc=unready_handle.proc,
595 rank=unready_handle.rank,
596 worker_response_mq=worker_response_mq,
597 peer_worker_response_mqs=peer_worker_response_mqs,
598 death_writer=unready_handle.death_writer,
599 )
600
601
602 class WorkerProc:
603 """Wrapper that runs one Worker in a separate process."""
604
605 READY_STR = "READY"
606 rpc_broadcast_mq: MessageQueue | None
607 worker_response_mq: MessageQueue | None
608
609 def _init_message_queues(
610 self, input_shm_handle: Handle, vllm_config: VllmConfig
611 ) -> None:
612 if vllm_config.parallel_config.nnodes_within_dp == 1:
613 # Initialize MessageQueue for receiving SchedulerOutput
614 self.rpc_broadcast_mq = MessageQueue.create_from_handle(
615 input_shm_handle, self.worker.rank
616 )
617
618 # Initializes a message queue for sending the model output
619 self.worker_response_mq = MessageQueue(1, 1)
620 self.peer_response_handles = []
621 else:
622 # Initialize remote MessageQueue for receiving SchedulerOutput across nodes
623 self.rpc_broadcast_mq = get_inner_dp_world_group().create_mq_broadcaster(
624 external_writer_handle=input_shm_handle,
625 # Since there is external_writer_handle from executor proc,
626 # where the ready signal from actual writer is sent out of the
627 # create_mq_broadcaster method and after this setup, we make it
628 # non blocking. The handshake will be triggered when
629 # worker.rpc_broadcast_mq.wait_until_ready() is called
630 blocking=False,
631 )
632 # Initializes remote message queue for sending the model output to the
633 # driver worker, exposing peer_response_handles for driver worker
634 # that include handles for all ranks
635 self.worker_response_mq, self.peer_response_handles = (
636 get_inner_dp_world_group().create_single_reader_mq_broadcasters(
637 reader_rank_in_group=0
638 )
639 )
640
641 @instrument(span_name="Worker init")
642 def __init__(
643 self,
644 vllm_config: VllmConfig,
645 local_rank: int,
646 rank: int,
647 distributed_init_method: str,
648 input_shm_handle: Handle,
649 shared_worker_lock: LockType,
650 is_driver_worker: bool,
651 ):
652 self.rank = rank
653 wrapper = WorkerWrapperBase(rpc_rank=local_rank, global_rank=rank)
654 # TODO: move `init_worker` to executor level as a collective rpc call
655 all_kwargs: list[dict] = [
656 {} for _ in range(vllm_config.parallel_config.world_size)
657 ]
658 all_kwargs[local_rank] = {
659 "vllm_config": vllm_config,
660 "local_rank": local_rank,
661 "rank": rank,
662 "distributed_init_method": distributed_init_method,
663 "is_driver_worker": is_driver_worker,
664 "shared_worker_lock": shared_worker_lock,
665 }
666 wrapper.init_worker(all_kwargs)
667 self.worker = wrapper
668
669 self.setup_proc_title_and_log_prefix(
670 enable_ep=vllm_config.parallel_config.enable_expert_parallel
671 )
672
673 # Load model
674 self.worker.init_device()
675 # Update process title now that parallel groups are initialized
676 self.setup_proc_title_and_log_prefix(
677 enable_ep=vllm_config.parallel_config.enable_expert_parallel
678 )
679 if envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
680 self.worker.elastic_ep_execute("load_model")
681 else:
682 self.worker.load_model()
683
684 scheduler_config = vllm_config.scheduler_config
685 self.use_async_scheduling = scheduler_config.async_scheduling
686 if self.use_async_scheduling:
687 self.async_output_queue: queue.Queue = queue.Queue()
688 self.async_output_copy_thread = Thread(
689 target=self.async_output_busy_loop,
690 daemon=True,
691 name="WorkerAsyncOutputCopy",
692 )
693 self.async_output_copy_thread.start()
694
695 # Set block size based on the attention backends
696 current_platform.update_block_size_for_backend(vllm_config)
697
698 # Initialize message queues after init_device() since multi-node setups
699 # (nnodes_within_dp > 1) require distributed groups to be initialized
700 self._init_message_queues(input_shm_handle, vllm_config)
701
702 # Enable environment variable cache (e.g. assume no more
703 # environment variable overrides after this point)
704 enable_envs_cache()
705
706 @staticmethod
707 def make_worker_process(
708 vllm_config: VllmConfig,
709 local_rank: int,
710 rank: int,
711 distributed_init_method: str,
712 input_shm_handle, # Receive SchedulerOutput
713 shared_worker_lock: LockType,
714 is_driver_worker: bool,
715 inherited_fds: list[int] | None = None,
716 ) -> UnreadyWorkerProcHandle:
717 context = get_mp_context()
718 # Ready pipe to communicate readiness from child to parent
719 ready_reader, ready_writer = context.Pipe(duplex=False)
720 # Death pipe to let child detect parent process exit
721 death_reader, death_writer = context.Pipe(duplex=False)
722 if inherited_fds is not None:
723 inherited_fds = inherited_fds.copy()
724 inherited_fds.extend((ready_reader.fileno(), death_writer.fileno()))
725 process_kwargs = {
726 "vllm_config": vllm_config,
727 "local_rank": local_rank,
728 "rank": rank,
729 "distributed_init_method": distributed_init_method,
730 "input_shm_handle": input_shm_handle,
731 "ready_pipe": ready_writer,
732 "death_pipe": death_reader,
733 "shared_worker_lock": shared_worker_lock,
734 "is_driver_worker": is_driver_worker,
735 # Have the worker close parent end of this worker's pipes too
736 "inherited_fds": inherited_fds if inherited_fds is not None else [],
737 }
738 # Run EngineCore busy loop in background process.
739 proc = context.Process(
740 target=WorkerProc.worker_main,
741 kwargs=process_kwargs,
742 name=f"VllmWorker-{rank}",
743 daemon=True,
744 )
745
746 # Apply NUMA binding if configured
747 with numa_utils.configure_subprocess(
748 vllm_config, local_rank, process_kind="worker"
749 ):
750 proc.start()
751
752 # Close child ends of pipes here in the parent
753 ready_writer.close()
754 death_reader.close()
755 # Keep death_writer open in parent - when parent exits,
756 # death_reader in child will get EOFError
757 return UnreadyWorkerProcHandle(proc, rank, ready_reader, death_writer)
758
759 @staticmethod
760 def wait_for_response_handle_ready(
761 handles: dict[str, Any], proc_handle: UnreadyWorkerProcHandle
762 ) -> WorkerProcHandle:
763 response_handle = handles["handle"]
764 worker_response_mq: MessageQueue | None = None
765 if len(response_handle.local_reader_ranks) > 0:
766 worker_response_mq = MessageQueue.create_from_handle(response_handle, 0)
767 peer_response_handles = handles["peer_response_handles"]
768 peer_worker_response_mqs = [
769 MessageQueue.create_from_handle(handle, -1)
770 if handle.remote_subscribe_addr is not None
771 else None
772 for handle in peer_response_handles
773 ]
774 return WorkerProcHandle.from_unready_handle(
775 proc_handle,
776 worker_response_mq,
777 peer_worker_response_mqs=peer_worker_response_mqs,
778 )
779
780 @staticmethod
781 def wait_for_ready(
782 unready_proc_handles: list[UnreadyWorkerProcHandle],
783 ) -> list[WorkerProcHandle]:
784 e = Exception(
785 "WorkerProc initialization failed due to an exception in a "
786 "background process. See stack trace for root cause."
787 )
788
789 pipes = {handle.ready_pipe: handle for handle in unready_proc_handles}
790 ready_proc_handles: list[WorkerProcHandle | None] = [None] * len(
791 unready_proc_handles
792 )
793 while pipes:
794 ready = multiprocessing.connection.wait(pipes.keys())
795 for pipe in ready:
796 assert isinstance(pipe, Connection)
797 try:
798 # Wait until the WorkerProc is ready.
799 unready_proc_handle = pipes.pop(pipe)
800 response: dict[str, Any] = pipe.recv()
801 if response["status"] != "READY":
802 raise e
803
804 idx = unready_proc_handle.rank % len(ready_proc_handles)
805 ready_proc_handles[idx] = WorkerProc.wait_for_response_handle_ready(
806 response, unready_proc_handle
807 )
808 except EOFError:
809 e.__suppress_context__ = True
810 raise e from None
811
812 finally:
813 # Close connection.
814 pipe.close()
815
816 return cast(list[WorkerProcHandle], ready_proc_handles)
817
818 def shutdown(self):
819 if self.rpc_broadcast_mq is not None:
820 self.rpc_broadcast_mq.shutdown()
821 if self.worker_response_mq is not None:
822 self.worker_response_mq.shutdown()
823 self.worker.shutdown()
824 self.rpc_broadcast_mq = None
825 self.worker_response_mq = None
826 destroy_model_parallel()
827 destroy_distributed_environment()
828
829 def monitor_death_pipe(self, death_pipe, shutdown_requested: threading.Event):
830 if death_pipe is None:
831 return
832
833 def death_pipe_monitor(queues_to_shutdown: list[MessageQueue]):
834 try:
835 # This will block until parent process exits (pipe closes)
836 death_pipe.recv()
837 except EOFError:
838 logger.info_once("Parent process exited, terminating worker queues")
839 shutdown_requested.set()
840 for mq in queues_to_shutdown:
841 if mq is not None:
842 mq.shutdown()
843 except Exception as e:
844 logger.warning("Death monitoring error: %s", e)
845
846 # Pass queue references directly to avoid gc issues if passing self
847 Thread(
848 target=death_pipe_monitor,
849 args=([self.rpc_broadcast_mq, self.worker_response_mq],),
850 daemon=True,
851 name="DeathPipeMonitor",
852 ).start()
853
854 @staticmethod
855 def worker_main(*args, **kwargs):
856 """Worker initialization and execution loops.
857 This runs a background process"""
858 if logging_config := getattr(kwargs["vllm_config"], "logging_config", None):
859 configure_logging(logging_config)
860
861 # Signal handler used for graceful termination.
862 # SystemExit exception is only raised once to allow this and worker
863 # processes to terminate without error
864 shutdown_requested = threading.Event()
865
866 def signal_handler(signum, frame):
867 nonlocal shutdown_requested
868 if not shutdown_requested.is_set():
869 shutdown_requested.set()
870 logger.debug(
871 "WorkerProc handling signal %d, raising SystemExit", signum
872 )
873 raise SystemExit()
874
875 # Either SIGTERM or SIGINT will terminate the worker
876 signal.signal(signal.SIGTERM, signal_handler)
877 signal.signal(signal.SIGINT, signal_handler)
878
879 # Publish the logical-to-physical mapping early so topology helpers
880 # work before init_device (needed by set_worker_net_device below).
881 assigned_physical_gpu_ids = kwargs[
882 "vllm_config"
883 ].parallel_config.assigned_physical_gpu_ids
884 if assigned_physical_gpu_ids is not None:
885 from vllm.platforms.interface import set_assigned_physical_gpu_ids
886
887 set_assigned_physical_gpu_ids(assigned_physical_gpu_ids)
888
889 # Set net device env vars for the worker if VLLM_GPU_NIC_PCIE_MAPPING is set
890 set_worker_net_device(kwargs.get("local_rank", 0), kwargs["vllm_config"])
891
892 worker = None
893 ready_writer = kwargs.pop("ready_pipe")
894 death_pipe = kwargs.pop("death_pipe", None)
895
896 # Close inherited pipes from parent (incl. other worker pipes)
897 # Explicitly passing in existing pipes and closing them makes the pipe
898 # behave when using fork. Otherwise, a hidden reference to the pipes
899 # exist in the child process and prevents EOF closure.
900 for fd in kwargs.pop("inherited_fds", []):
901 try:
902 os.close(fd)
903 except Exception as e:
904 logger.warning("Error closing inherited connection: %s: %s", type(e), e)
905
906 try:
907 # Initialize tracer
908 rank = kwargs.get("rank", 0)
909 maybe_init_worker_tracer(
910 instrumenting_module_name="vllm.worker",
911 process_kind="worker",
912 process_name=f"Worker_{rank}",
913 )
914
915 worker = WorkerProc(*args, **kwargs)
916 assert worker.worker_response_mq is not None
917 if kwargs["vllm_config"].parallel_config.numa_bind:
918 numa_utils.log_current_affinity_state(f"Worker_{worker.rank}")
919
920 worker.monitor_death_pipe(death_pipe, shutdown_requested)
921
922 # Send READY once we know everything is loaded
923 ready_writer.send(
924 {
925 "status": WorkerProc.READY_STR,
926 "handle": worker.worker_response_mq.export_handle(),
927 "peer_response_handles": worker.peer_response_handles,
928 }
929 )
930
931 # Ensure message queues are ready. Will deadlock if re-ordered.
932 # Must be kept consistent with the Executor
933 if worker.rpc_broadcast_mq is not None:
934 worker.rpc_broadcast_mq.wait_until_ready()
935 worker.worker_response_mq.wait_until_ready()
936 ready_writer.close()
937 ready_writer = None
938
939 worker.worker_busy_loop()
940
941 except Exception:
942 # NOTE: if an Exception arises in busy_loop, we send
943 # a FAILURE message over the MQ RPC to notify the Executor,
944 # which triggers system shutdown.
945 # TODO(rob): handle case where the MQ itself breaks.
946
947 if ready_writer is not None:
948 logger.exception("WorkerProc failed to start.")
949 elif shutdown_requested.is_set():
950 logger.debug_once(
951 "[shutdown] WorkerProc: exiting after shutdown request"
952 )
953 else:
954 logger.exception("WorkerProc failed.")
955
956 # The parent sends a SIGTERM to all worker processes if
957 # any worker dies. Set this value so we don't re-throw
958 # SystemExit() to avoid zmq exceptions in __del__.
959 shutdown_requested.set()
960
961 except SystemExit as e:
962 # SystemExit is raised on SIGTERM or SIGKILL, which usually indicates that
963 # the graceful shutdown process did not succeed
964 if shutdown_requested.is_set():
965 logger.debug_once(
966 "[shutdown] WorkerProc: terminated by shutdown signal"
967 )
968 else:
969 logger.warning("WorkerProc was terminated")
970 # SystemExit must never be ignored
971 raise e
972
973 finally:
974 if ready_writer is not None:
975 ready_writer.close()
976 if death_pipe is not None:
977 death_pipe.close()
978 # Clean up once worker exits busy loop
979 if worker is not None:
980 worker.shutdown()
981
982 class ResponseStatus(Enum):
983 SUCCESS = auto()
984 FAILURE = auto()
985
986 def enqueue_output(self, output: Any):
987 """Prepares output from the worker and enqueues it to the
988 worker_response_mq. If the output is an Exception, it is
989 converted to a FAILURE response.
990 """
991 if isinstance(output, AsyncModelRunnerOutput):
992 try:
993 output = output.get_output()
994 except Exception as e:
995 logger.exception("Error getting async model runner output")
996 output = e
997
998 if isinstance(output, Exception):
999 result = (WorkerProc.ResponseStatus.FAILURE, str(output))
1000 else:
1001 result = (WorkerProc.ResponseStatus.SUCCESS, output)
1002 if (response_mq := self.worker_response_mq) is not None:
1003 response_mq.enqueue(result)
1004
1005 def handle_output(self, output: Any):
1006 """Handles output from the worker. If async scheduling is enabled,
1007 it is passed to the async_output_busy_loop thread. Otherwise, it is
1008 enqueued directly to the worker_response_mq.
1009 """
1010 if self.use_async_scheduling:
1011 self.async_output_queue.put(output)
1012 else:
1013 self.enqueue_output(output)
1014
1015 def async_output_busy_loop(self):
1016 """Entrypoint for the thread which handles outputs asynchronously."""
1017 # set device to the worker device for the thread.
1018 # a thread will not inherit the context of the main thread.
1019 # when calling any cuda runtime functions, it will implicitly
1020 # create a new cuda context on device 0, consuming extra memory.
1021 # here we set the device to the worker device for the thread,
1022 # enforcing the context to be the same as the main thread.
1023 from vllm.platforms import current_platform
1024
1025 if hasattr(self.worker, "device"):
1026 current_platform.set_device(self.worker.device)
1027
1028 while True:
1029 output = self.async_output_queue.get()
1030 self.enqueue_output(output)
1031
1032 def worker_busy_loop(self):
1033 """Main busy loop for Multiprocessing Workers."""
1034 assert self.rpc_broadcast_mq is not None
1035 while True:
1036 self._execute_worker_rpc(self.rpc_broadcast_mq.dequeue(indefinite=True))
1037
1038 def _execute_worker_rpc(
1039 self,
1040 rpc_request: tuple[str | bytes, tuple[Any, ...], dict[str, Any], int | None],
1041 ) -> None:
1042 """Execute one RPC in a separate frame from the dequeue loop."""
1043 method, args, kwargs, output_rank = rpc_request
1044 try:
1045 if isinstance(method, str):
1046 func = getattr(self.worker, method)
1047 elif isinstance(method, bytes):
1048 func = partial(cloudpickle.loads(method), self.worker)
1049
1050 output = func(*args, **kwargs)
1051
1052 if output_rank is None or self.rank == output_rank:
1053 self.handle_output(output)
1054 except Exception as e:
1055 # Notes have been introduced in python 3.11
1056 if hasattr(e, "add_note"):
1057 e.add_note(traceback.format_exc())
1058 logger.exception("WorkerProc hit an exception.")
1059 # enqueue_output converts the exception to a FAILURE response
1060 # containing its string representation before transport.
1061 if output_rank is None or self.rank == output_rank:
1062 self.handle_output(e)
1063
1064 @staticmethod
1065 def setup_proc_title_and_log_prefix(enable_ep: bool) -> None:
1066 # Check if parallel groups are initialized first
1067 if not model_parallel_is_initialized():
1068 # Parallel groups not yet initialized, use default process name
1069 set_process_title(name="Worker")
1070 decorate_logs("Worker")
1071 return
1072
1073 dp_size = get_dp_group().world_size
1074 dp_rank = get_dp_group().rank_in_group
1075 pp_size = get_pp_group().world_size
1076 pp_rank = get_pp_group().rank_in_group
1077 pcp_size = get_pcp_group().world_size
1078 pcp_rank = get_pcp_group().rank_in_group
1079 tp_size = get_tp_group().world_size
1080 tp_rank = get_tp_group().rank_in_group
1081 dcp_size = get_dcp_group().world_size
1082 dcp_rank = get_dcp_group().rank_in_group
1083 process_name = "Worker"
1084 if dp_size > 1:
1085 process_name += f"_DP{dp_rank}"
1086 if pp_size > 1:
1087 process_name += f"_PP{pp_rank}"
1088 if pcp_size > 1:
1089 process_name += f"_PCP{pcp_rank}"
1090 if tp_size > 1:
1091 process_name += f"_TP{tp_rank}"
1092 if dcp_size > 1:
1093 process_name += f"_DCP{dcp_rank}"
1094 if enable_ep:
1095 ep_rank = get_ep_group().rank_in_group
1096 process_name += f"_EP{ep_rank}"
1097 set_process_title(name=process_name)
1098 decorate_logs(process_name)
1099
1100
1101 def set_multiprocessing_worker_envs(local_world_size: int = 1):
1102 """Set up environment variables that should be used when there are workers
1103 in a multiprocessing environment. This should be called by the parent
1104 process before worker processes are created"""
1105 _maybe_force_spawn()
1106
1107 if current_platform.is_cpu() or "OMP_NUM_THREADS" in os.environ:
1108 return
1109
1110 # Choose the workers' thread count here, before they start, since a worker
1111 # must not set its own: `torch.set_num_threads()` spawns the thread pool
1112 # eagerly, and doing that part way through a worker's startup either races
1113 # the dlopen of shared objects or, in a forked worker, deadlocks (libgomp
1114 # is not fork-safe).
1115 num_threads = startup_omp_num_threads(local_world_size)
1116 os.environ["OMP_NUM_THREADS"] = str(num_threads)
1117 os.environ[OMP_NUM_THREADS_SET_BY_VLLM] = "1"
1118
1119 # A spawned worker picks the count up from the environment when it imports
1120 # torch. A forked worker instead inherits it from this process, so set it
1121 # here too. This is safe as long as we don't *use* the pool before forking:
1122 # a forked child whose parent had run a parallel region deadlocks, whereas
1123 # one whose parent merely sized the pool does not.
1124 torch.set_num_threads(num_threads)
1125 logger.debug(
1126 "Set OMP_NUM_THREADS=%d for %d worker process(es).",
1127 num_threads,
1128 local_world_size,
1129 )