v1/executor/multiproc_executor.py: MultiprocExecutor and WorkerProc
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/v1/executor/multiproc_executor.py implements the multiproc executor: the default executor when an engine core needs more than one worker on a node, or when DP > 1. It also defines WorkerProc, the code that runs inside each worker process.
| File | vllm/v1/executor/multiproc_executor.py, 1,147 lines of Python. |
|---|---|
| Main classes | MultiprocExecutor(Executor) (vllm/v1/executor/multiproc_executor.py:112), WorkerProc (vllm/v1/executor/multiproc_executor.py:608), FutureWrapper(Future) (vllm/v1/executor/multiproc_executor.py:79). |
| Runs in | MultiprocExecutor: the engine core process (main thread, plus the thread MultiprocWorkerMonitor). WorkerProc: each worker process (main thread, WorkerAsyncOutputCopy, DeathPipeMonitor). |
| Called by | EngineCore.__init__ makes it (vllm/v1/engine/core.py:147); the busy loop calls execute_model and sample_tokens; utility calls reach collective_rpc. |
| Calls | MessageQueue (shared memory), multiprocessing processes and pipes, WorkerWrapperBase and through it the GPU Worker. |
| Not in this file | The ring buffer itself (distributed/device_communicators/shm_broadcast.py), the worker methods (v1/worker/gpu_worker.py), the uni executor (uniproc_executor.py), the base interface (abstract.py), Ray (ray_executor_v2.py, which subclasses this file's classes). |
The page Executors and workers: from the engine core to the GPUs explains the ideas. This page reads the file method by method.
Where it sits#
Who calls it#
Executor.get_classreturnsMultiprocExecutorfor the backend"mp"(vllm/v1/executor/abstract.py:80-83). When you do not set a backend, the config picks"mp"whenworld_size_across_dp > 1, unless a Ray case applies (vllm/config/parallel.py:1055-1096).EngineCorebuilds it and registers the failure callback (vllm/v1/engine/core.py:147-150).RayExecutorV2andRayWorkerProcsubclass the two main classes (vllm/v1/executor/ray_executor_v2.py:79,vllm/v1/executor/ray_executor_v2.py:239).- The spawned child process runs
WorkerProc.worker_main(vllm/v1/executor/multiproc_executor.py:745-750).
What it calls#
MessageQueue(...),export_handle,create_from_handle,enqueue,dequeue(vllm/v1/executor/multiproc_executor.py:165-172,vllm/v1/executor/multiproc_executor.py:615-625).get_mp_context()for processes, locks and pipes (vllm/v1/executor/multiproc_executor.py:174-175,vllm/v1/executor/multiproc_executor.py:723-727).WorkerWrapperBase.init_worker, theninit_deviceandload_modelin the worker (vllm/v1/executor/multiproc_executor.py:659-688).set_multiprocessing_worker_envs, which forces spawn when needed and setsOMP_NUM_THREADS(vllm/v1/executor/multiproc_executor.py:1119-1147).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
FutureWrapper | vllm/v1/executor/multiproc_executor.py:79-110 | A future that reads replies in call order from a shared deque. |
MultiprocExecutor._init_executor | vllm/v1/executor/multiproc_executor.py:119-268 | Broadcast queue, worker processes, ready wait, monitor, reply queues, output rank. |
start_worker_monitor | vllm/v1/executor/multiproc_executor.py:300-334 | Thread that waits for any worker to exit. |
execute_model / sample_tokens | vllm/v1/executor/multiproc_executor.py:342-366 | One-reply RPCs with the step timeout and the KV/EC aggregators. |
collective_rpc | vllm/v1/executor/multiproc_executor.py:383-456 | Enqueue once, build the reply reader, return a future or the result. |
_ensure_worker_termination / shutdown | vllm/v1/executor/multiproc_executor.py:458-543 | Close the death pipes, wait, SIGTERM, SIGKILL; close the queues. |
_get_output_rank | vllm/v1/executor/multiproc_executor.py:549-563 | world_size - TP × PCP: first TP rank of the last PP stage. |
UnreadyWorkerProcHandle / WorkerProcHandle | vllm/v1/executor/multiproc_executor.py:570-605 | The parent's view of a worker before and after READY. |
WorkerProc.__init__ | vllm/v1/executor/multiproc_executor.py:647-710 | Build the worker, device, model, async copy thread, queues. |
make_worker_process / wait_for_ready | vllm/v1/executor/multiproc_executor.py:712-822 | Start the child with a ready pipe and a death pipe; collect READY. |
worker_main | vllm/v1/executor/multiproc_executor.py:861-989 | Process entry point of a worker. |
enqueue_output / handle_output / async_output_busy_loop | vllm/v1/executor/multiproc_executor.py:995-1039 | Turn a result into (status, value); wait for async outputs in a thread. |
worker_busy_loop / _execute_worker_rpc | vllm/v1/executor/multiproc_executor.py:1041-1080 | Dequeue a call, run it, reply if this rank must. |
setup_proc_title_and_log_prefix | vllm/v1/executor/multiproc_executor.py:1082-1116 | Process titles such as Worker_TP0. |
Walk through the main methods#
_init_executor: start the workers#
context = get_mp_context()
shared_worker_lock = context.Lock()
unready_workers: list[UnreadyWorkerProcHandle] = []
success = False
try:
global_start_rank = (
self.local_world_size * self.parallel_config.node_rank_within_dp
)
# When using fork, keep track of socket file descriptors that are
# inherited by the worker, so that we can close them in subsequent
# workers
inherited_fds: list[int] | None = (
[] if context.get_start_method() == "fork" else None
)
# For CPU backend only, to setup OpenMP threads affinity
cpu_omp_manager = OMPProcessManager(self.vllm_config)
for local_rank in range(self.local_world_size):
global_rank = global_start_rank + local_rank
is_driver_worker = self._is_driver_worker(global_rank)
with cpu_omp_manager.configure_omp_envs(
rank=global_rank, local_rank=local_rank
):
unready_worker_handle = WorkerProc.make_worker_process(
vllm_config=self.vllm_config,
local_rank=local_rank,
rank=global_rank,
distributed_init_method=distributed_init_method,
input_shm_handle=scheduler_output_handle,
shared_worker_lock=shared_worker_lock,
is_driver_worker=is_driver_worker,
inherited_fds=inherited_fds,
)
unready_workers.append(unready_worker_handle)
if inherited_fds is not None:
inherited_fds.append(unready_worker_handle.death_writer.fileno())
inherited_fds.append(unready_worker_handle.ready_pipe.fileno())
# Workers must be created before wait_for_ready to avoid
# deadlock, since worker.init_device() does a device sync.
# Wait for all local workers to be ready.
self.workers = WorkerProc.wait_for_ready(unready_workers)- Lines 174-175. One lock is shared by all workers of this executor; the worker uses it for the multimodal receiver cache (
vllm/v1/worker/worker_base.py:339-343). - Lines 191-193. One process per local rank.
is_driver_workeris true whenrank % TP == 0(vllm/v1/executor/multiproc_executor.py:297-298). - Lines 197-206. Each child gets the handle of the broadcast queue, not the queue object.
- Line 216. The executor blocks until every worker has loaded its model. The comment at lines 212-213 explains the order: all workers must exist first, because
init_devicesynchronizes the devices.
set_torch_threads_for_runtime()
# Start background thread to monitor worker health if not in headless mode.
if self.monitor_workers:
self.start_worker_monitor()
self.response_mqs = []
# Only leader node have remote response mqs
if self.parallel_config.node_rank_within_dp == 0:
for rank in range(self.world_size):
if rank < self.local_world_size:
local_message_queue = self.workers[rank].worker_response_mq
assert local_message_queue is not None
self.response_mqs.append(local_message_queue)
else:
remote_message_queue = self.workers[0].peer_worker_response_mqs[
rank
]
assert remote_message_queue is not None
self.response_mqs.append(remote_message_queue)
# Ensure message queues are ready. Will deadlock if re-ordered
# Must be kept consistent with the WorkerProc.
# Wait for all input mqs to be ready.
if self.rpc_broadcast_mq is not None:
self.rpc_broadcast_mq.wait_until_ready()
# Wait for all remote response mqs to be ready.
for response_mq in self.response_mqs:
response_mq.wait_until_ready()
self.futures_queue = deque[FutureWrapper]()
self._post_init_executor()
success = True
finally:
if not success:
# Clean up the worker procs if there was a failure.
# Close death_writers first to signal workers to exit
for uw in unready_workers:
if uw.death_writer is not None:
uw.death_writer.close()
uw.death_writer = None
self._ensure_worker_termination([uw.proc for uw in unready_workers])
self.output_rank = self._get_output_rank()- Line 222. The engine core process sets its torch thread count for run time. The comment gives the reason: this process only schedules, and more threads only compete with the workers for the CPU.
- Lines 229-241. One reply queue per rank. Local workers write to their own
MessageQueue(1, 1); remote ranks reach the leader node through worker 0. - Lines 247-251. Both sides must call
wait_until_ready()in the same order. The comment warns that another order deadlocks. - Lines 258-266. On any failure, the executor closes the death pipes and terminates the processes it started.
collective_rpc: one call to all workers#
def collective_rpc( # type: ignore[override]
self,
method: str | Callable,
timeout: float | None = None,
args: tuple = (),
kwargs: dict | None = None,
non_block: bool = False,
unique_reply_rank: int | None = None,
kv_output_aggregator: KVOutputAggregator | None = None,
ec_output_aggregator: ECOutputAggregator | None = None,
) -> Any:
"""Returns single result if unique_reply_rank and/or an output
aggregator is provided, otherwise list."""
assert self.rpc_broadcast_mq is not None, (
"collective_rpc should not be called on follower node"
)
if self.is_failed:
raise RuntimeError("Executor failed.")
deadline = None if timeout is None else time.monotonic() + timeout
kwargs = kwargs or {}
aggregators = [a for a in (kv_output_aggregator, ec_output_aggregator) if a]
aggregate: Callable[[Any], Any]
if aggregators:
output_rank = None
def _aggregate(outputs: Any) -> Any:
# Each aggregator merges its own connector's output onto
# outputs[output_rank] in place and returns it, so chaining is safe.
rank = unique_reply_rank or 0
result = outputs[rank]
for a in aggregators:
result = a.aggregate(outputs, output_rank=rank)
return result
aggregate = _aggregate
else:
output_rank = unique_reply_rank
aggregate = lambda x: x
- Lines 396-398. Only the leader node of a DP rank owns the broadcast queue.
- Lines 399-400. After a worker death, every call fails at once.
- Line 402. One deadline for the whole call; each reply read gets the time that is left.
- Lines 405-422. With a KV or EC connector aggregator,
output_rankbecomesNone: all workers reply, and the aggregator merges their outputs onto the reply ofunique_reply_rank.
if isinstance(method, str):
send_method = method
else:
send_method = cloudpickle.dumps(method, protocol=pickle.HIGHEST_PROTOCOL)
self.rpc_broadcast_mq.enqueue((send_method, args, kwargs, output_rank))
response_mqs: Sequence[MessageQueue] = self.response_mqs
if output_rank is not None:
response_mqs = (response_mqs[output_rank],)
def get_response():
responses = []
for mq in response_mqs:
dequeue_timeout = (
None if deadline is None else max(0.0, deadline - time.monotonic())
)
try:
status, result = mq.dequeue(timeout=dequeue_timeout)
except TimeoutError as e:
raise TimeoutError(f"RPC call to {method} timed out.") from e
if status != WorkerProc.ResponseStatus.SUCCESS:
raise RuntimeError(
f"Worker failed with error '{result}', please check the"
" stack trace above for the root cause"
)
responses.append(result)
return responses[0] if output_rank is not None else responses
future = FutureWrapper(
self.futures_queue, get_response=get_response, aggregate=aggregate
)
return future if non_block else future.result()- Lines 425-427. A callable is sent as cloudpickle bytes; the worker calls it with the worker object as the first argument (
vllm/v1/executor/multiproc_executor.py:1059-1060). - Line 428. The single write into shared memory.
- Lines 437-443. A reply that does not come in time becomes
TimeoutError. - Lines 452-456.
non_blockreturns theFutureWrapper; else the method waits at once.
FutureWrapper: replies in order#
class FutureWrapper(Future):
def __init__(
self,
futures_queue: deque["FutureWrapper"],
get_response: Callable[[], Any],
aggregate: Callable = lambda x: x,
):
self.futures_queue = futures_queue
self.get_response = get_response
self.aggregate = aggregate
super().__init__()
self.futures_queue.appendleft(self)
def result(self, timeout=None):
if timeout is not None:
raise RuntimeError("timeout not implemented")
# Drain any futures ahead of us in the queue.
while not self.done():
future = self.futures_queue.pop()
future._wait_for_response()
return super().result()
def _wait_for_response(self):
try:
response = self.aggregate(self.get_response())
with suppress(InvalidStateError):
self.set_result(response)
except Exception as e:
with suppress(InvalidStateError):
self.set_exception(e)
- Line 90. Each new future goes to the left end of the shared deque.
- Lines 97-99.
result()pops from the right end, the oldest future, and reads its reply, until its own future is done. The reply queues are FIFO, so the reply of the oldest call always comes first. - Lines 104-109. An error goes into the future that owns it, not into the caller's future.
WorkerProc.__init__: inside the worker process#
self.rank = rank
wrapper = WorkerWrapperBase(rpc_rank=local_rank, global_rank=rank)
# TODO: move `init_worker` to executor level as a collective rpc call
all_kwargs: list[dict] = [
{} for _ in range(vllm_config.parallel_config.world_size)
]
all_kwargs[local_rank] = {
"vllm_config": vllm_config,
"local_rank": local_rank,
"rank": rank,
"distributed_init_method": distributed_init_method,
"is_driver_worker": is_driver_worker,
"shared_worker_lock": shared_worker_lock,
}
wrapper.init_worker(all_kwargs)
self.worker = wrapper
self.setup_proc_title_and_log_prefix(
enable_ep=vllm_config.parallel_config.enable_expert_parallel
)
# Load model
self.worker.init_device()
# Update process title now that parallel groups are initialized
self.setup_proc_title_and_log_prefix(
enable_ep=vllm_config.parallel_config.enable_expert_parallel
)
if envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
self.worker.elastic_ep_execute("load_model")
else:
self.worker.load_model()
scheduler_config = vllm_config.scheduler_config
self.use_async_scheduling = scheduler_config.async_scheduling
if self.use_async_scheduling:
self.async_output_queue: queue.Queue = queue.Queue()
self.async_output_copy_thread = Thread(
target=self.async_output_busy_loop,
daemon=True,
name="WorkerAsyncOutputCopy",
)
self.async_output_copy_thread.start()
# Set block size based on the attention backends
current_platform.update_block_size_for_backend(vllm_config)
# Initialize message queues after init_device() since multi-node setups
# (nnodes_within_dp > 1) require distributed groups to be initialized
self._init_message_queues(input_shm_handle, vllm_config)
# Enable environment variable cache (e.g. assume no more
# environment variable overrides after this point)
enable_envs_cache()- Lines 659-672.
WorkerWrapperBase.init_workerloads the general plugins and makes the worker class. Only this rank's kwargs are filled in. - Line 680.
init_device: CUDA device, NCCL groups, memory snapshot, model runner. - Line 688. The weights load here, before the worker reports
READY. - Lines 691-699. With async scheduling, a second thread will wait for the GPU results.
- Line 702. The platform may change the block size for the chosen attention backend.
- Line 706. The queues come last, because a multi-node setup needs the process groups.
worker_busy_loop and _execute_worker_rpc#
def worker_busy_loop(self):
"""Main busy loop for Multiprocessing Workers."""
assert self.rpc_broadcast_mq is not None
while True:
with get_watch_dog().disable():
rpc_request = self.rpc_broadcast_mq.dequeue(indefinite=True)
self._execute_worker_rpc(rpc_request)
del rpc_request
def _execute_worker_rpc(
self,
rpc_request: tuple[str | bytes, tuple[Any, ...], dict[str, Any], int | None],
) -> None:
"""Execute one RPC in a separate frame from the dequeue loop."""
method, args, kwargs, output_rank = rpc_request
try:
if isinstance(method, str):
func = getattr(self.worker, method)
elif isinstance(method, bytes):
func = partial(cloudpickle.loads(method), self.worker)
watchdog = get_watch_dog()
watchdog.feed()
output = func(*args, **kwargs)
# Feed again: the RPC itself may run close to the watchdog
# timeout, so the pre-call feed can go stale while the result
# is serialized/enqueued in handle_output().
watchdog.feed()
if output_rank is None or self.rank == output_rank:
self.handle_output(output)
except Exception as e:
# Notes have been introduced in python 3.11
if hasattr(e, "add_note"):
e.add_note(traceback.format_exc())
logger.exception("WorkerProc hit an exception.")
# enqueue_output converts the exception to a FAILURE response
# containing its string representation before transport.
if output_rank is None or self.rank == output_rank:
self.handle_output(e)- Lines 1045-1046. Wait for the next call without a timeout. The stack-dump watchdog is off during this wait, so an idle worker does not look stuck.
- Lines 1057-1060. A name is looked up on the worker wrapper; bytes are a pickled function.
- Lines 1070-1071. Only the reply rank (or every rank when
output_rankisNone) sends the result. - Lines 1076-1080. An exception is logged with its traceback and sent as a
FAILUREreply. The loop goes on.
handle_output: the async copy thread#
def enqueue_output(self, output: Any):
"""Prepares output from the worker and enqueues it to the
worker_response_mq. If the output is an Exception, it is
converted to a FAILURE response.
"""
if isinstance(output, AsyncModelRunnerOutput):
try:
output = output.get_output()
except Exception as e:
logger.exception("Error getting async model runner output")
output = e
if isinstance(output, Exception):
result = (WorkerProc.ResponseStatus.FAILURE, str(output))
else:
result = (WorkerProc.ResponseStatus.SUCCESS, output)
if (response_mq := self.worker_response_mq) is not None:
response_mq.enqueue(result)
def handle_output(self, output: Any):
"""Handles output from the worker. If async scheduling is enabled,
it is passed to the async_output_busy_loop thread. Otherwise, it is
enqueued directly to the worker_response_mq.
"""
if self.use_async_scheduling:
self.async_output_queue.put(output)
else:
self.enqueue_output(output)
def async_output_busy_loop(self):
"""Entrypoint for the thread which handles outputs asynchronously."""
# set device to the worker device for the thread.
# a thread will not inherit the context of the main thread.
# when calling any cuda runtime functions, it will implicitly
# create a new cuda context on device 0, consuming extra memory.
# here we set the device to the worker device for the thread,
# enforcing the context to be the same as the main thread.
from vllm.platforms import current_platform
if hasattr(self.worker, "device"):
current_platform.set_device(self.worker.device)
while True:
output = self.async_output_queue.get()
self.enqueue_output(output)- Lines 1000-1005. An
AsyncModelRunnerOutputis resolved withget_output(), which waits for the copy to the CPU. - Lines 1019-1022. With async scheduling, this wait happens in the extra thread, not in the main thread.
- Lines 1034-1035. The thread sets its CUDA device first. A new thread does not inherit it, and a CUDA call on device 0 would make a second CUDA context.
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveData flow: types in and out
| Value | Type | Where |
|---|---|---|
| broadcast message | (method: str | bytes, args: tuple, kwargs: dict, output_rank: int | None), pickled | vllm/v1/executor/multiproc_executor.py:428, vllm/v1/executor/multiproc_executor.py:1050-1055 |
execute_model args | (SchedulerOutput,) | vllm/v1/executor/multiproc_executor.py:345-347 |
sample_tokens args | (GrammarOutput | None,) | vllm/v1/executor/multiproc_executor.py:358-360 |
| reply message | (ResponseStatus.SUCCESS, result) or (ResponseStatus.FAILURE, str(exception)) | vllm/v1/executor/multiproc_executor.py:1007-1012 |
execute_model reply (MRV2, last PP rank) | None: sampling follows in sample_tokens | vllm/v1/worker/gpu/model_runner.py:2103-2110 |
sample_tokens reply | ModelRunnerOutput (after get_output() in the worker) | vllm/v1/executor/multiproc_executor.py:1000-1002 |
| ready pipe message | {"status": "READY", "handle": Handle, "peer_response_handles": [...]} | vllm/v1/executor/multiproc_executor.py:931-937 |
Deep diveProcesses and threads
| Process | Thread | Code | Blocks in |
|---|---|---|---|
| engine core | main | collective_rpc, FutureWrapper.result | mq.dequeue of a reply queue |
| engine core | MultiprocWorkerMonitor | vllm/v1/executor/multiproc_executor.py:307-331 | multiprocessing.connection.wait(sentinels) |
| worker | main | worker_busy_loop | rpc_broadcast_mq.dequeue(indefinite=True) |
| worker | WorkerAsyncOutputCopy (async scheduling only) | async_output_busy_loop | async_output_queue.get(), then copy_event.synchronize() |
| worker | DeathPipeMonitor | vllm/v1/executor/multiproc_executor.py:835-859 | death_pipe.recv() |
The process name is VllmWorker-<rank> (vllm/v1/executor/multiproc_executor.py:748). After init_device, the worker renames itself by its parallel ranks, for example Worker_TP1 or, for an MoE model with DP = 2 and TP = 2, Worker_DP1_TP0 (vllm/v1/executor/multiproc_executor.py:1101-1116). A dense model's DP rank runs as DP = 1 (vllm/config/parallel.py:1192-1199), so its name has no DP part.
Deep diveWorked example: TP = 2, one step
- The busy loop calls
execute_model(so, non_block=True).collective_rpcenqueues("execute_model", (so,), {}, 0)and returns future F1 (vllm/v1/executor/multiproc_executor.py:342-353). - Both workers dequeue the same message. Both run
Worker.execute_model; their forward passes meet in the NCCL all-reduces of the TP layers. - Worker 0 is the output rank (
2 - 2 × 1 = 0). It replies(SUCCESS, None)through its copy thread. Worker 1 replies nothing. - The busy loop computes the grammar bitmask, then calls
sample_tokens(grammar, non_block=True): message("sample_tokens", (grammar,), {}, 0), future F2. - Both workers sample. Worker 0 gives its
AsyncOutputto the copy thread, which waits for the copy and replies(SUCCESS, ModelRunnerOutput). - One call later, the busy loop pops this step and calls
F2.result().FutureWrapperfirst reads the reply of F1 (None), then the reply of F2.
(Derived from the methods above and vllm/v1/engine/core.py:724-773.)
Deep diveError paths: what raises, what only logs
| Situation | Result | Where |
|---|---|---|
| world size is not TP × PP × PCP | AssertionError | vllm/v1/executor/multiproc_executor.py:126-132 |
a worker sends a status other than READY, or its pipe closes | Exception("WorkerProc initialization failed ...") | vllm/v1/executor/multiproc_executor.py:790-816 |
| a worker process exits | ERROR log, is_failed = True, shutdown, failure callback | vllm/v1/executor/multiproc_executor.py:314-326 |
| call after a failure | RuntimeError("Executor failed.") | vllm/v1/executor/multiproc_executor.py:399-400 |
| worker method raises | worker logs it. Only a replying rank sends FAILURE, and then the executor raises RuntimeError("Worker failed with error ..."). A failure on another rank is only logged | vllm/v1/executor/multiproc_executor.py:1072-1080, vllm/v1/executor/multiproc_executor.py:444-448 |
| reply timeout | TimeoutError("RPC call to ... timed out.") | vllm/v1/executor/multiproc_executor.py:440-443 |
get_output() fails in the copy thread | logged; sent as a FAILURE reply | vllm/v1/executor/multiproc_executor.py:1000-1005 |
| parent process exits | worker logs "Parent process exited, terminating worker queues" | vllm/v1/executor/multiproc_executor.py:841-849 |
| SIGTERM / SIGINT in a worker | SystemExit, raised once | vllm/v1/executor/multiproc_executor.py:873-884 |
Deep diveEnvironment variables
| Variable | Default | Read at | Effect |
|---|---|---|---|
VLLM_MQ_MAX_CHUNK_BYTES_MB | 16 (vllm/envs.py:1856-1858) | vllm/v1/executor/multiproc_executor.py:152 | Chunk size of the broadcast queue; larger messages go over ZMQ. |
VLLM_EXECUTE_MODEL_TIMEOUT_SECONDS | 300 (vllm/envs.py:1861-1863) | vllm/v1/executor/multiproc_executor.py:350, vllm/v1/executor/multiproc_executor.py:363 | Timeout of execute_model, sample_tokens, dummy batches and draft tokens. |
VLLM_WORKER_SHUTDOWN_TIMEOUT_SECONDS | 5 (vllm/envs.py:1865-1867) | vllm/v1/executor/multiproc_executor.py:484-486 | Grace period before SIGTERM; SIGKILL follows after 4 s more. |
VLLM_WORKER_MULTIPROC_METHOD | fork; vllm serve sets spawn | through get_mp_context() (vllm/v1/executor/multiproc_executor.py:723) | Start method of the worker processes. |
OMP_NUM_THREADS | set by vLLM if unset | vllm/v1/executor/multiproc_executor.py:1125-1135 | CPU threads per worker. |
Related concepts#
- Executors and workers: from the engine core to the GPUs: the backends, the call path and the worker start-up.
- Async scheduling: overlap the CPU and the GPU: why the worker has a copy thread.
- The engine core loop: threads, queues and the busy loop: what happens after
EXECUTOR_FAILED. - v1/worker/gpu_worker.py: the GPU Worker: the methods that the RPCs run.
- distributed/device_communicators/shm_broadcast.py: MessageQueue: the
MessageQueuering buffer.
Check yourself#
- 1What is the name of a worker process before it renames itself, and which method is its target?easy
Show answer
VllmWorker-<rank>, with the targetWorkerProc.worker_main(vllm/v1/executor/multiproc_executor.py:745-750). - 2With TP = 8 and PP = 4, which rank replies tomedium
execute_model?Show answer
Rank
world_size - TP × PCP = 32 - 8 = 24, the first TP rank of the last PP stage. The comment in_get_output_rankuses this example (vllm/v1/executor/multiproc_executor.py:549-563). - 3A worker method raisesmedium
ValueError("bad"). What does the engine core see, and where is the traceback?Show answer
The worker logs "WorkerProc hit an exception." with the traceback. If it is the reply rank (or the call has no
output_rank), it replies(FAILURE, "bad")(vllm/v1/executor/multiproc_executor.py:1072-1080). The executor raisesRuntimeError("Worker failed with error 'bad', ...")(vllm/v1/executor/multiproc_executor.py:444-448). The traceback is only in the worker's log. - 4Why doeshard
async_output_busy_loopcallcurrent_platform.set_devicebefore its loop, and what goes wrong without it?Show answer
A new thread does not inherit the current CUDA device of the main thread. The first CUDA call in the thread, here the event synchronize in
get_output(), would then make a new CUDA context on device 0, which costs extra memory. The comment says this (vllm/v1/executor/multiproc_executor.py:1026-1035).