Prhub

#23213 wait for reap in kill_process_tree

原始 PR 作者 hnyls2002 合并时间 2026-04-20 14:36 文件变更 7 提交数 2 评论 3 代码增减 +57 / -9

执行摘要

为 kill_process_tree 添加可选超时等待,修复引擎关闭后的 GPU 资源竞争条件。

根据PR body,动机是'Fix race between Engine.shutdown() / fixture teardown and the next GPU allocation: after SIGKILL, children still hold GPU context until the kernel reaps them.' 即修复关闭和GPU分配间的竞争条件,确保资源及时释放。

值得精读,特别是_wait_for_reap_or_raise函数的实现,展示了如何处理异步进程回收、超时控制和竞态避免,对于涉及多进程管理或资源清理的代码有借鉴意义。

讨论亮点

该PR未经过正式review讨论,作者直接合并;评论中仅包含测试重跑命令和机器人响应,无技术性争议或设计权衡。

实现拆解

  1. 新增等待函数:在python/sglang/srt/utils/common.py中新增_wait_for_reap_or_raise函数,实现带警告和超时检查的进程等待逻辑,使用psutil.wait_procs监控进程退出,超时则抛出RuntimeError
  2. 扩展kill_process_tree:修改同一文件中的kill_process_tree函数,添加wait_timeout可选参数,默认None保持向后兼容;当wait_timeout不为None时,调用_wait_for_reap_or_raise等待被杀进程回收。
  3. 更新引擎关闭路径:在python/sglang/srt/entrypoints/engine.pyhttp_server_engine.pyshutdown方法中,调用kill_process_tree时传递wait_timeout=60,确保引擎关闭时等待子进程释放GPU上下文。
  4. 同步测试fixture:更新四个测试fixture文件(如default_fixture.py)的tearDownClass方法,同样添加wait_timeout=60,保证测试清理时等待进程回收,避免测试环境竞态。
文件 模块 状态 重要度
python/sglang/srt/utils/common.py 工具函数 modified 7.64
python/sglang/srt/entrypoints/engine.py 引擎入口 modified 5.63
python/sglang/srt/entrypoints/http_server_engine.py 服务器入口 modified 4.93

关键符号

kill_process_tree _wait_for_reap_or_raise

关键源码片段

python/sglang/srt/utils/common.py core-logic

修改了核心的 kill_process_tree 函数,新增 _wait_for_reap_or_raise 辅助函数,是实现超时等待的核心逻辑所在。

def _wait_for_reap_or_raise(procs, wait_timeout: float) -> None:
    """Wait for `procs` to exit; warn at ~10s, raise on `wait_timeout`.    SIGKILL is asynchronous -- children hold GPU context, pinned memory and
    fds until the kernel reaps them. Raise on timeout so a stuck process
    surfaces instead of leaving a latent race.
    """
    warn_at = min(10.0, wait_timeout / 2) # 设置警告时间点,避免长时间无反馈
    gone, alive = psutil.wait_procs(procs, timeout=warn_at) # 首次等待,检查进程是否退出
    if not alive:
        return # 所有进程已退出,直接返回
    logger.warning(
        "kill_process_tree: %d process(es) still alive after %.1fs SIGKILL; "
        "continuing to wait up to %.1fs total. pids=%s",
        len(alive),
        warn_at,
        wait_timeout,
        [p.pid for p in alive],
    ) # 记录警告日志,提示进程未及时回收
    remaining = wait_timeout - warn_at
    if remaining > 0:
        _, alive = psutil.wait_procs(alive, timeout=remaining) # 继续等待剩余时间
    if alive:
        raise RuntimeError( # 超时后抛出异常,避免静默失败
            f"kill_process_tree: {len(alive)} process(es) not reaped within "
            f"{wait_timeout}s after SIGKILL; pids={[p.pid for p in alive]}"
        )def kill_process_tree(
    parent_pid,
    include_parent: bool = True,
    skip_pid: int = None,
    wait_timeout: Optional[float] = None,
):
    """Kill the process and all its child processes.    `wait_timeout` (seconds) blocks until every killed process is reaped and
    raises `RuntimeError` on timeout; `None` is fire-and-forget. The
    `parent_pid == os.getpid()` branch calls `sys.exit(0)` and cannot wait
    for itself -- use `include_parent=False` if child reap must finish first.
    """
    if parent_pid is None:
        parent_pid = os.getpid()
        include_parent = False # 默认不杀死父进程以避免自等待
    try:
        itself = psutil.Process(parent_pid)
    except psutil.NoSuchProcess:
        return # 进程不存在则直接返回
    children = itself.children(recursive=True)
    killed = [] # 记录已杀死的进程,用于后续等待
    for child in children:
        if child.pid == skip_pid:
            continue
        try:
            child.kill()
            killed.append(child) # 杀死子进程并记录
        except psutil.NoSuchProcess:
            pass
    if include_parent:
        try:
            if parent_pid == os.getpid():
                itself.kill()
                sys.exit(0) # 特殊分支:杀死自身进程并退出,无法等待
            itself.kill()
            itself.send_signal(signal.SIGQUIT) # 发送额外信号确保杀死顽固进程
            killed.append(itself) # 记录父进程
        except psutil.NoSuchProcess:
            pass
    if wait_timeout is not None and killed:
        _wait_for_reap_or_raise(killed, wait_timeout) # 如果有超时设置,调用等待函数

评论区精华

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

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

风险与影响

技术风险包括:

1) 超时设置风险:若wait_timeout过短(如低于进程回收时间),可能无法完全避免竞争条件;过长则增加关闭延迟,影响测试执行效率。
2) 异常路径风险:新函数_wait_for_reap_or_raise可能引入未处理的异常(如psutil库错误),需确保错误处理健壮。
3) 兼容性风险:默认wait_timeout=None保持向后兼容,但调用方显式设置超时后,若依赖旧行为(如火警式退出)可能被破坏。

对用户:提高系统关闭后GPU资源可立即重用的可靠性,减少因资源泄漏导致的测试失败或生产环境问题。对系统:关闭过程可能增加最多60秒延迟,但避免了潜在竞态和资源泄漏,提升稳定性。对团队:增强基础设施的健壮性,尤其在高频测试场景中减少调试开销。

核心路径变更 超时配置风险 测试覆盖调整

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论