# PR #41943 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[CI][Bugfix] Surface subprocess output in spawn_new_process_for_each_test
- 合并时间：2026-05-08 16:39
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/41943

---

# 执行摘要

- 一句话：子进程输出实时显示，崩溃原因不再被吞
- 推荐动作：建议精读此 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`。

# 实现拆解

1. **移除 `capture_output=True`**：在 `tests/utils.py` 的 `subprocess.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`（模块 测试工具；类别 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` 辅助函数改善信号退出提示，并简化异常回溯读取路径。

```python
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）。

- 暂无高价值评论线程

# 风险与影响

- 风险：
 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.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` 的近期修改，涉及相同装饰器的兼容性修复。