ValueError: the real cause

当调用 accelerate launch --cpu (走 simple_launcher )启动的子脚本非零退出时,父进程抛出的 subprocess.CalledProcessError 只带返回码和命令, e.stderr 与 e.output 均为 None ,真正的 ValueError

快速结论:当调用 accelerate launch --cpu(走 simple_launcher)启动的子脚本非零退出时,父进程抛出的 subprocess.CalledProcessError 只带返回码和命令,e.stderre.output 均为 None,真正的 ValueError: the real cause 无法被程序化捕获。优先排查是否在用无 torchrun 的简单启动路径,以及是否已使用包含修复的 accelerate 版本。

适用环境:accelerate 1.12.0(Issue 与 main 分支 b795b4838 行为一致);触发路径为 --cpu + --num_processes 1simple_launcher。Issue 未提供操作系统、Python、CUDA、显卡等具体版本信息。

最快修复方案:暂无确认的一步修复方案(Issue 报告时修复尚未发布)。Issue 中已有 PR #4292 尝试捕获 stdout/stderr 并传入 CalledProcessError,可优先尝试升级到包含该 PR 的 accelerate 版本;否则需要自行在调用侧读取子进程输出。

注意事项:若自行按“选项 1”接管 stderr 管道,必须使用 communicate() 而非 wait() 之后再 read():Issue 评论确认后者会在子进程刷屏 stdout 后死锁,communicate() 在同一场景下 41ms 正常结束并带回完整 traceback。这与多进程路径 multi_gpu_launcher(由 torch.distributed.run 抛出携带根因的 ChildFailedError)的行为差异,是本次问题的核心。

问题场景

用户通过 accelerate launch 使用 simple_launcher 启动自己的训练脚本(复现命令为 --cpu --num_processes 1),并希望在外层代码里 except subprocess.CalledProcessError 后根据异常内容做判断,例如 TRL CLI 测试中按异常类型和消息匹配并重试已知的瞬时基础设施故障。当子脚本因 GPU 显存压力等原因崩溃时,父进程拿到的只有命令和退出码,无法区分临时故障和真实回归。

报错原文

Traceback (most recent call last):
  File "boom.py", line 1, in <module>
    raise ValueError("the real cause")
ValueError: the real cause

parent sees : CalledProcessError: Command '['.../bin/python', 'boom.py']' returned non-zero exit status 1.
e.stderr    : None
e.output    : None

原因分析

simple_launchersubprocess.Popen 拉起子进程,子进程 stderr 被继承而不是读取,父进程在非零退出时只用返回码和命令构造 CalledProcessError,既没有传 output 也没有传 stderr,因此调用方只能知道“有东西非零退出”。与之相对,multi_gpu_launcher 委托给 torch.distributed.run,torch elastic 通过 error file 机制把子进程根异常传回父进程,抛出带有 “Root Cause” 段的 ChildFailedError。Issue 认为 simple_launcher 扮演的是同一个 “nanny process” 角色,理应给出同样的保证。子进程 traceback 确实会打到终端,但位置在失败报告上方很多行、且与其他 worker 输出交错,对程序化调用者没有帮助。

环境排查

  • 确认 accelerate 版本:Issue 在 1.12.0 和 main(b795b4838eb33bde60eb398649c4ab006874e3e9)上均可复现。
  • 确认是否走 simple_launcher:使用 --cpu(无 torchrun 多进程分发)时命中该路径。
  • 确认是否命中 PR #4292 的修复:检查所用版本是否已包含“捕获 stdout/stderr 并传入 CalledProcessError”的改动。
  • Issue 未提供 Python、CUDA、PyTorch、显卡型号等信息,无法据此进一步缩小范围。

解决步骤

  1. 先用 Issue 提供的最小脚本确认现象:准备一个只含 raise ValueError("the real cause")boom.py,用 launch_command_parser().parse_args(["--cpu", "--num_processes", "1", sys.argv[1]]) 并捕获 subprocess.CalledProcessError,打印 e.stderre.output;若两者均为 None,即为此问题。
  2. 优先尝试升级到包含 PR #4292 的 accelerate 版本,让 simple_launcher 把子进程输出写入 CalledProcessError;升级后重复上一步验证。
  3. 若暂时无法升级,需要在调用侧自行取得子进程输出:可参考 Issue 提出的“选项 1”思路(将子进程 stderr 经管道转发并保留尾部内容)。实现时必须用 communicate() 读取,不要用 wait() 然后再 read()
  4. 若需要与 multi_gpu_launcher 完全一致的语义(携带 “Root Cause” 的 ChildFailedError),则需要“选项 2”:把启动脚本包进 torch elastic 的 error-file 机制,这属于更大的改动,Issue 中列为工作量更高的方案。

验证方法

重新运行上述最小复现:修复生效时,捕获到的 CalledProcessErrore.stderr 应包含子进程的完整 traceback 与 ValueError: the real cause,而不是 None;同时父进程终端仍应看到实时输出。Issue 评论提到,用 communicate() 的实现可在 41ms 内结束并带回完整 traceback,可作为参考基准。

参考来源

huggingface/accelerate #4277

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 25331

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注