From 224699000582691dd8ad0d73d17ab87ac2e348a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E7=9A=AE=E5=B9=BC=E9=B8=9F?= <2960474346@qq.com> Date: Wed, 12 Aug 2026 18:16:09 +0800 Subject: [PATCH] [Bugfix] Release worker RPC payload before next dequeue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Execute each worker RPC in a separate stack frame so deserialized arguments and outputs are released before the next message is deserialized. Preserve exception handling behavior and add deterministic lifetime regression coverage. Fixes #43639 Co-authored-by: OpenAI Codex Signed-off-by: 石皮幼鸟 <2960474346@qq.com> --- tests/v1/executor/test_multiproc_executor.py | 67 ++++++++++++++++++++ vllm/v1/executor/multiproc_executor.py | 45 +++++++------ 2 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 tests/v1/executor/test_multiproc_executor.py diff --git a/tests/v1/executor/test_multiproc_executor.py b/tests/v1/executor/test_multiproc_executor.py new file mode 100644 index 000000000000..4ebaba5f1177 --- /dev/null +++ b/tests/v1/executor/test_multiproc_executor.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project + +import weakref +from types import SimpleNamespace +from typing import Any + +import pytest + +from vllm.v1.executor.multiproc_executor import WorkerProc + + +class _ExitWorkerLoop(RuntimeError): + pass + + +class _RpcPayload: + pass + + +class _PayloadLifetimeCheckingQueue: + def __init__(self) -> None: + self.payload_ref: weakref.ReferenceType[_RpcPayload] | None = None + self.dequeue_count = 0 + + def dequeue(self, *, indefinite: bool): + assert indefinite + self.dequeue_count += 1 + if self.dequeue_count == 1: + payload = _RpcPayload() + self.payload_ref = weakref.ref(payload) + return "consume", (payload,), {}, None + + assert self.payload_ref is not None + assert self.payload_ref() is None + raise _ExitWorkerLoop + + +def test_worker_rpc_payload_released_before_next_dequeue(): + queue = _PayloadLifetimeCheckingQueue() + worker_proc: Any = WorkerProc.__new__(WorkerProc) + worker_proc.rpc_broadcast_mq = queue + worker_proc.rank = 0 + worker_proc.worker = SimpleNamespace(consume=lambda payload: payload) + worker_proc.handle_output = lambda output: None + + with pytest.raises(_ExitWorkerLoop): + worker_proc.worker_busy_loop() + + assert queue.dequeue_count == 2 + + +def test_execute_worker_rpc_returns_worker_exception(): + def fail(): + raise RuntimeError("test error") + + worker_proc: Any = WorkerProc.__new__(WorkerProc) + worker_proc.rank = 0 + worker_proc.worker = SimpleNamespace(fail=fail) + outputs: list[Any] = [] + worker_proc.handle_output = outputs.append + + worker_proc._execute_worker_rpc(("fail", (), {}, None)) + + assert len(outputs) == 1 + assert isinstance(outputs[0], RuntimeError) + assert str(outputs[0]) == "test error" diff --git a/vllm/v1/executor/multiproc_executor.py b/vllm/v1/executor/multiproc_executor.py index afc333723d0f..dbc17a1ee96a 100644 --- a/vllm/v1/executor/multiproc_executor.py +++ b/vllm/v1/executor/multiproc_executor.py @@ -1013,28 +1013,33 @@ def worker_busy_loop(self): """Main busy loop for Multiprocessing Workers""" assert self.rpc_broadcast_mq is not None while True: - method, args, kwargs, output_rank = self.rpc_broadcast_mq.dequeue( - indefinite=True - ) - try: - if isinstance(method, str): - func = getattr(self.worker, method) - elif isinstance(method, bytes): - func = partial(cloudpickle.loads(method), self.worker) + self._execute_worker_rpc(self.rpc_broadcast_mq.dequeue(indefinite=True)) + + 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) - output = func(*args, **kwargs) + output = func(*args, **kwargs) - 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.") - # exception might not be serializable, so we convert it to - # string, only for logging purpose. - if output_rank is None or self.rank == output_rank: - self.handle_output(e) + 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) @staticmethod def setup_proc_title_and_log_prefix(enable_ep: bool) -> None: