Prhub

#41943 [CI][Bugfix] Surface subprocess output in spawn_new_process_for_each_test

原始 PR 作者 haosdent 合并时间 2026-05-08 16:39 文件变更 1 提交数 1 评论 3 代码增减 +18 / -5

执行摘要

子进程输出实时显示,崩溃原因不再被吞

在 CI 构建(如 #64813、#64851、#64859)中频繁出现 Failed core proc(s): {} 且无任何 engine 子进程输出的情况,导致无法定位崩溃原因。PR body 明确指出:Engine-core, NCCL and CUDA output is hidden, which is where the actual cause of crashes lives

建议精读此 PR,尤其是 _format_subprocess_exit 的设计和 capture_output 的移除决策。对于需要处理子进程诊断的开发者,该模式值得参考。

讨论亮点

ZJY0516 在 issue 评论中询问此 PR 是否只是将错误消息从 Failed core proc(s): {} 改为 Failed core proc(s): {killed by SIGSEGV (-11)},作者 haosdent 澄清核心改动是让子进程输出实时显示,而非仅修改错误消息格式。

自动化 bot 评论(claude、gemini-code-assist)均给出正面评价,指出该改动是纯粹的 CI 诊断改进,无安全风险。最终 ZJY0516 批准(LGTM)。

实现拆解

  1. 移除 capture_output=True:在 tests/utils.pysubprocess.run 调用中删除 capture_output=True,使子进程的 stdout/stderr 直接继承父进程文件描述符,从而实时输出到测试运行器,与 fork_new_process_for_each_test 行为一致。

  2. 简化异常回溯读取逻辑:删除原有优先读取 tb_file 再 fallback 到 result.stderr.decode() 的复杂逻辑,改为始终尝试读取 tb_file;若文件不可读或为空,则给出提示性消息 "<no Python traceback; see subprocess output above>",将诊断职责交给实时输出的子进程流。

  3. 新增 _format_subprocess_exit 辅助函数:此函数将子进程返回码转为人类可读字符串。对于负返回码(由信号终止),通过 signal.Signals(-returncode).name 获取信号名称,如 killed by SIGSEGV (-11);非负返回码则直接输出 exit code {returncode}。当 signal.Signals 查找失败时,fallback 为通用 exit code 格式。

  4. 更新错误消息:将 RuntimeError 中原有的 (exit code {returncode}) 替换为调用 _format_subprocess_exit(result.returncode) 的结果,使信号引起的退出更易识别。

文件 模块 状态 重要度
tests/utils.py 测试工具 modified 5.74

关键符号

_format_subprocess_exit spawn_new_process_for_each_test

关键源码片段

tests/utils.py test-coverage

唯一修改的文件,核心变更所在。删除了 `capture_output=True` 以暴露子进程实时输出,新增 `_format_subprocess_exit` 辅助函数改善信号退出提示,并简化异常回溯读取路径。

import signal# 辅助函数:将子进程返回码转为人类可读的描述
# 对信号引起的负退出码(如 -11 对应 SIGSEGV)给出明确提示
def _format_subprocess_exit(returncode: int) -> str:
    """渲染子进程退出码,对负值(信号)给出名称。"""
    if returncode >= 0:
        return f"exit code {returncode}"
    try:
        # 信号编号是负数,取绝对值后查找信号名称
        return f"killed by {signal.Signals(-returncode).name} ({returncode})"
    except ValueError:
        # 以防某些平台信号编号不在 Signals 枚举中
        return f"exit code {returncode}"
​
​
# 原函数的关键变更部分:移除 capture_output=True
# 并在错误消息中使用 _format_subprocess_exit
result = subprocess.run(
    [sys.executable, "-c", child_script],
    input=payload,
    # capture_output=True, # 已移除:不再捕获子进程输出
    env=env,
)if result.returncode != 0:
    with open(tb_file) as fp:
        tb = fp.read()
    if not tb:
        tb = "<no Python traceback; see subprocess output above>"
    raise RuntimeError(
        f"Test subprocess '{f.__name__}' failed "
        f"({_format_subprocess_exit(result.returncode)}):\n{tb}"
    )

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

  1. 回归风险:移除 capture_output=True 后,子进程输出直接写入父进程 stdout/stderr,可能影响测试输出解析(如 pytest 捕获机制),但该行为与 fork_new_process_for_each_test 一致,且 CI 场景下通常欢迎实时输出。
  2. 兼容性风险:新增的 _format_subprocess_exit 函数使用 signal.Signals,在不同平台上信号编号可能不同,但 try/except ValueError 包裹了查找失败的情况,fallback 安全。
  3. 测试覆盖:该函数本身未被测试,但其逻辑简单,风险可控。

影响范围:仅影响 tests/utils.pyspawn_new_process_for_each_test 装饰器使用场景。
影响程度:中等。该装饰器用于需要在独立进程中运行的测试(如 CUDA/multi-GPU 相关),本改动使这些测试在发生 native crash 或异常时更易诊断,直接提升 CI 调试效率。无用户或生产环境影响。

测试工具变更 低风险

关联 Issue

#41423 [Bugfix] Fix spawn_new_process_for_each_test silently swallowing test failures

完整报告

参与讨论