执行摘要
- 一句话:子进程输出实时显示,崩溃原因不再被吞
- 推荐动作:建议精读此 PR,尤其是
_format_subprocess_exit 的设计和 capture_output 的移除决策。对于需要处理子进程诊断的开发者,该模式值得参考。
功能与动机
在 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。
实现拆解
-
移除 capture_output=True:在 tests/utils.py 的 subprocess.run 调用中删除 capture_output=True,使子进程的 stdout/stderr 直接继承父进程文件描述符,从而实时输出到测试运行器,与 fork_new_process_for_each_test 行为一致。
-
简化异常回溯读取逻辑:删除原有优先读取 tb_file 再 fallback 到 result.stderr.decode() 的复杂逻辑,改为始终尝试读取 tb_file;若文件不可读或为空,则给出提示性消息 "<no Python traceback; see subprocess output above>",将诊断职责交给实时输出的子进程流。
-
新增 _format_subprocess_exit 辅助函数:此函数将子进程返回码转为人类可读字符串。对于负返回码(由信号终止),通过 signal.Signals(-returncode).name 获取信号名称,如 killed by SIGSEGV (-11);非负返回码则直接输出 exit code {returncode}。当 signal.Signals 查找失败时,fallback 为通用 exit code 格式。
-
更新错误消息:将 RuntimeError 中原有的 (exit code {returncode}) 替换为调用 _format_subprocess_exit(result.returncode) 的结果,使信号引起的退出更易识别。
关键文件:
tests/utils.py(模块 测试工具;类别 test;类型 test-coverage;符号 _format_subprocess_exit): 唯一修改的文件,核心变更所在。删除了 capture_output=True 以暴露子进程实时输出,新增 _format_subprocess_exit 辅助函数改善信号退出提示,并简化异常回溯读取路径。
关键符号:_format_subprocess_exit, spawn_new_process_for_each_test
关键源码片段
tests/utils.py
唯一修改的文件,核心变更所在。删除了 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}"
)
评论区精华
ZJY0516 在 issue 评论中询问此 PR 是否只是将错误消息从 Failed core proc(s): {} 改为 Failed core proc(s): {killed by SIGSEGV (-11)},作者 haosdent 澄清核心改动是让子进程输出实时显示,而非仅修改错误消息格式。
自动化 bot 评论(claude、gemini-code-assist)均给出正面评价,指出该改动是纯粹的 CI 诊断改进,无安全风险。最终 ZJY0516 批准(LGTM)。
风险与影响
-
风险:
- 回归风险:移除
capture_output=True 后,子进程输出直接写入父进程 stdout/stderr,可能影响测试输出解析(如 pytest 捕获机制),但该行为与 fork_new_process_for_each_test 一致,且 CI 场景下通常欢迎实时输出。
- 兼容性风险:新增的
_format_subprocess_exit 函数使用 signal.Signals,在不同平台上信号编号可能不同,但 try/except ValueError 包裹了查找失败的情况,fallback 安全。
- 测试覆盖:该函数本身未被测试,但其逻辑简单,风险可控。
- 影响:影响范围:仅影响 tests/utils.py 中 spawn_new_process_for_each_test 装饰器使用场景。
影响程度:中等。该装饰器用于需要在独立进程中运行的测试(如 CUDA/multi-GPU 相关),本改动使这些测试在发生 native crash 或异常时更易诊断,直接提升 CI 调试效率。无用户或生产环境影响。
-
风险标记:测试工具变更, 低风险
关联脉络
- PR #41423 [Bugfix] Fix spawn_new_process_for_each_test silently swallowing test failures: 此 PR 的关联 issue #41423 记录了之前修复测试异常传播的问题,本次 PR 是其后续改进,解决输出隐藏问题。
- PR #41895 [Bugfix] Fix XPU/ROCm compatibility in spawn_new_process_for_each_test: 同一文件
tests/utils.py 的近期修改,涉及相同装饰器的兼容性修复。
参与讨论