![【Bug已解决】[Bug]: WorkerProc initialization failure swallows root cause traceback (raise e from None) 解](http://pic.xiahunao.cn/yaotu/【Bug已解决】[Bug]: WorkerProc initialization failure swallows root cause traceback (raise e from None) 解)
【Bug已解决】[Bug]: WorkerProc initialization failure swallows root cause traceback (raise e from None) 解决方案一、现象长什么样vLLM 启动多 worker张量并行 / 流水线并行时某个 worker 进程初始化失败但父进程拿到的报错完全没有根因RuntimeError: WorkerProc failed to initialize而 worker 进程里真正的错误比如CUDA error: out of memory或KeyError: ...被吞掉了。栈里只有父进程的RuntimeError: WorkerProc failed to initialize看不到 worker 内部到底哪行炸的。几个典型表征父进程报错极简worker 内部栈丢失想排查却只能看到worker 初始化失败没有内部 traceback。代码里有raise e from None这是元凶。from None会显式切断异常的__cause__链并把原始 traceback 替换成当前位置于是父进程收到的异常只剩WorkerProc failed这一层。多进程场景特有的跨进程吞栈worker 是子进程异常要 pickle 回父进程。如果 worker 在raise new_err from None不仅链断了连原始异常类型/栈都可能被换成泛化错误。这不是 worker 真的无法初始化而是初始化失败时错误没有被正确跨进程传递根因被from None抹掉。下面给出定位与修复保留跨进程 traceback。二、背景vLLM 的 worker 是独立进程multiprocessing/fork/spawn。初始化流程父进程 spawn worker → worker 跑 __init__建模型/绑卡/加载权重 → 若 worker 内部抛 e如 OOM → worker 捕获后 raise WorkerInitError(...) from None ← 问题在这 → 异常 pickle 回父进程 → 父进程只看到 WorkerInitError无 e 的栈raise new_err from None的语义是我这个新异常和之前那个异常无关Python 于是隐藏__cause__和原始 traceback。在单进程里这最多让调试麻烦但在多进程里因为异常要跨进程 pickle 重建原始异常很可能被完全丢掉父进程只剩泛化的WorkerInitError。正确做法raise new_err from e保留链或干脆raise不另起异常让原始异常原样 pickle 回父进程。这样父进程能看到完整根因栈。下面用可运行代码复现并修复。三、根因拆成两条根因raise e from None切断了 cause 链worker 捕获内部错误e后raise WorkerInitError(...) from None显式丢弃e的上下文父进程收不到根因。根因是错误的异常重抛方式。跨进程异常 pickle 丢失原始栈多进程下子进程异常要序列化回父进程。若 worker 用from None起了个泛化异常原始e的类型/栈没被带上父进程重建后只有泛化层。根因是异常没有原样透传或带链透传。修复方向worker 初始化失败时要么raise让原始异常原样回传父进程要么raise WorkerInitError(细节) from e保留根因链。绝不用from None。四、最小可运行复现下面复现from None吞掉根因def worker_init_bad(): 现状捕获内部错误后 raise ... from None吞掉根因。 try: raise ValueError(CUDA out of memory at layer 7) # 真正的根因 except ValueError as e: raise RuntimeError(WorkerProc failed to initialize) from None def worker_init_good(): 修复保留 cause 链。 try: raise ValueError(CUDA out of memory at layer 7) except ValueError as e: raise RuntimeError(WorkerProc failed to initialize) from e # 复现bad 版本看不到 ValueError 栈 try: worker_init_bad() except RuntimeError as err: has_root err.__cause__ is not None print(bad 版本保留根因:, has_root) # False —— 根因被吞 try: worker_init_good() except RuntimeError as err: print(good 版本根因:, repr(err.__cause__)) # ValueError 栈保留bad 版本保留根因: False即复现了worker 初始化失败吞根因。下面修复成跨进程也可保留。五、解决方案第一层最小直接修复最小修复worker 初始化失败时用raise new_err from e保留链或raise原样透传并附上 worker rank 等上下文。import traceback def worker_init_safe(rank: int): try: # 真正可能失败的初始化建模型/绑卡/加载 raise ValueError(frank{rank}: CUDA out of memory at layer 7) except Exception as e: # 保留根因链 附上下文绝不用 from None wrapped RuntimeError( fWorkerProc rank{rank} 初始化失败根因见 __cause__ ) wrapped.__cause__ e # 等价于 raise ... from e # 同时把原始栈也带出来方便跨进程排查 wrapped.__traceback__ e.__traceback__ raise wrapped # 复现修复 try: worker_init_safe(1) except RuntimeError as err: print(根因:, repr(err.__cause__)) # ValueError 完整保留 print(栈片段:, traceback.format_tb(err.__cause__.__traceback__)[0].strip())这一层改动让 worker 初始化失败时父进程能拿到完整根因哪张卡、哪层 OOM而不是只看到 WorkerProc failed。六、解决方案第二层结构化改进把worker 初始化错误传播做成结构化组件统一异常类型WorkerInitError 跨进程安全 pickle 根因链保留 结构化上下文字段。from dataclasses import dataclass, field from typing import Optional, Dict, Any class WorkerInitError(Exception): def __init__(self, rank: int, message: str, cause: Optional[BaseException] None): super().__init__(message) self.rank rank self.context: Dict[str, Any] {rank: rank} if cause is not None: self.__cause__ cause # 保留链不用 from None def init_worker_guarded(rank: int, init_fn): worker 初始化统一入口失败则带根因抛出 WorkerInitError。 try: return init_fn() except Exception as e: # 不吞根因把原始异常作为 cause并附 rank 上下文 raise WorkerInitError(rank, frank {rank} 初始化失败, causee) from e # 跨进程父进程收到后还原完整信息 def parent_handle(worker_exc: WorkerInitError): print(f[父进程] rank {worker_exc.rank} 初始化失败) if worker_exc.__cause__: print( 根因类型:, type(worker_exc.__cause__).__name__) print( 根因信息:, worker_exc.__cause__) # 用法 def fake_init(): raise ValueError(CUDA OOM at layer 7) try: init_worker_guarded(1, fake_init) except WorkerInitError as e: parent_handle(e)WorkerInitError把 rank 上下文和根因链一起带上跨进程 pickle 后父进程仍能看到哪个 rank、什么根因排查从盲猜变看根因。七、解决方案第三层断言 / CI 守护worker 错误传播最怕又用 from None 吞根因。用断言守两条不变量import ast, os def check_no_raise_from_none(path: str): 静态扫描禁止 raise ... from None会吞根因。 violations [] for root, _, files in os.walk(path): for f in files: if not f.endswith(.py): continue src open(os.path.join(root, f)).read() tree ast.parse(src) for node in ast.walk(tree): if isinstance(node, ast.Raise) and node.cause is not None: # cause 是 Name None → 吞根因 if isinstance(node.cause, ast.Constant) and node.cause.value is None: violations.append(f{f}:{node.lineno} raise ... from None) if violations: raise RuntimeError(发现吞根因的 from None:\n \n.join(violations)) return True def test_worker_error_keeps_cause(): try: worker_init_safe(1) except WorkerInitError as e: assert e.__cause__ is not None, 根因被吞 assert isinstance(e.__cause__, ValueError) # 静态扫描 check_no_raise_from_none(.) # 对整个代码树示例 print(OK: worker 初始化错误传播不变量通过) if __name__ __main__: test_worker_error_keeps_cause()把test_worker_error_keeps_cause接进 CI运行时断言根因链保留 静态扫描禁止from None任何吞根因的改动立即红。八、排查清单worker 初始化失败报 WorkerProc failed 却看不到根因按序查先 grepfrom None在 worker 初始化路径里找raise ... from None。找到就是元凶——它切断了__cause__链。改成from e或raise捕获内部异常e后raise WorkerInitError(...) from e保留链或干脆不包装、raise让原始异常原样回父进程。附 rank / gpu 上下文WorkerInitError(rank..., gpu...)带上结构化上下文跨进程后父进程能直接定位哪张卡。跨进程 pickle 限制异常要能被 pickle 回父进程。自定义异常如WorkerInitError必须是可 pickle 的不持有不可序列化对象否则父进程重建失败反而更糟。保留原始 tracebackwrapped.__traceback__ e.__traceback__让父进程traceback.format_tb能打出 worker 内部的栈而非只有父进程这一层。CI 接test_worker_error_keeps_cause运行时断言根因链 静态扫描禁止from None锁死不吞根因。区分初始化失败与运行期失败初始化失败的报错要能直接指向建模型/绑卡/加载权重哪步用根因链 上下文实现别用泛化 RuntimeError 掩盖。九、小结worker 初始化失败报 WorkerProc failed 却吞掉根因根因是worker 捕获内部错误后用raise new_err from None切断了__cause__链加上多进程异常需 pickle 回父进程原始栈彻底丢失。三层修复第一层worker_init_safe失败时raise WorkerInitError(...) from e保留链 附 rank 上下文 带原始 traceback父进程能看到完整根因第二层WorkerInitError结构化异常rank 上下文 cause 链跨进程 pickle 后仍可还原哪个 rank、什么根因第三层CI 运行时断言根因链 静态扫描禁止raise ... from None任何吞根因立即红。落实后vLLM 任一 worker 初始化失败父进程都能拿到带 rank 上下文和完整根因栈的错误如rank1 CUDA OOM at layer 7而不是只剩一句无信息的 WorkerProc failed。