Prhub

#36589 fix: kill_process_tree waits for the reap by default

原始 PR 作者 kpham-sgl 合并时间 2026-08-27 17:34 文件变更 3 提交数 1 评论 1 代码增减 +12 / -7

执行摘要

kill_process_tree 默认等待回收,修复 GPU 资源泄漏

在 CI 日志中,test_metrics 返回时服务器子进程仍占用 29.14 GiB GPU 显存,导致后续任务等待超时。作者指出 SIGKILL 不直接释放资源,只有进程退出时才在 exit_files 关闭 /dev/nvidia* 时释放,因此 kill_process_tree 必须等待回收。文档也已说明 '>30s observed on GB300',但默认 None 使大量调用点不安全。

值得精读,尤其是 kill_process_tree 的语义变更和进程生命周期管理逻辑,对理解 SGLang 的进程模型和资源清理有重要参考价值。

讨论亮点

作者在 PR body 中提出开放问题:_wait_for_reap_or_raise 在超时时抛出异常,而部分新等待点是 finally 块,会掩盖原始异常。作者认为大声报错更合适,但也愿意改为 log-and-continue。

实现拆解

  1. 修改默认等待:在 python/sglang/srt/utils/common.py 中,将 kill_process_treewait_timeout 参数默认值从 None 改为 60,并扩展 docstring 说明原因。
  2. 调整异常处理:将 children() 调用移入 NoSuchProcess 守卫内,避免目标进程在查找和遍历之间死亡导致异常逃逸。
  3. tokenizer_manager 适配:在 sigterm_watchdog 中改为 include_parent=False 并显式传递 wait_timeout=60,使子进程被收割后再退出,而不是交给 init 收养。
  4. 保持 del 不阻塞:在 runtime_endpoint.pyshutdown 中显式传递 wait_timeout=None,避免在垃圾回收时阻塞。
文件 模块 状态 重要度
python/sglang/srt/utils/common.py 工具库 modified 5.99
python/sglang/srt/managers/tokenizer_manager.py 管理器 modified 4.72
python/sglang/lang/backend/runtime_endpoint.py 后端 modified 4.5

关键符号

kill_process_tree sigterm_watchdog shutdown

关键源码片段

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

修改 kill_process_tree 默认等待,是本次变更核心。

# python/sglang/srt/utils/common.pydef kill_process_tree(
    parent_pid,
    include_parent: bool = True,
    skip_pid: int = None,
    wait_timeout: Optional[float] = 60, # 默认等待,避免资源残留
):
    """Kill the process and all its child processes.    `wait_timeout` (seconds) blocks until every killed process is reaped and
    raises `RuntimeError` on timeout. SIGKILL only queues the teardown, so
    returning without waiting leaves the GPU context, the pinned host memory
    and the ports held for seconds; pass `None` only where blocking is
    unacceptable, such as a `__del__`.
    """
    logger.info(
        f"kill_process_tree called: parent_pid={parent_pid}, "
        f"include_parent={include_parent}, pid={os.getpid()}"
    )
​
    if parent_pid is None:
        parent_pid = os.getpid()
        include_parent = False
​
    try:
        itself = psutil.Process(parent_pid)
        # 在守卫内获取 children,避免目标进程在查找和遍历之间死亡
        children = itself.children(recursive=True)
    except psutil.NoSuchProcess:
        return
​
    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)
python/sglang/srt/managers/tokenizer_manager.py core-logic

修改 sigterm_watchdog 的进程回收方式,使子进程被收割。

# python/sglang/srt/managers/tokenizer_manager.pyasync def sigterm_watchdog(self):
    # ... 健康检查和退出逻辑
    # 等待调度器释放资源
    self._dispatch_to_scheduler(ShutdownReq())
    deadline = time.monotonic() + 15
    while time.monotonic() < deadline and collect_scheduler_processes():
        time.sleep(0.1)
    # 收割所有子进程,再退出自身,避免进程被重新收养导致资源残留
    kill_process_tree(os.getpid(), include_parent=False, wait_timeout=60)
    sys.exit(0)
python/sglang/lang/backend/runtime_endpoint.py core-logic

显式保持 __del__ 路径不阻塞。

# python/sglang/lang/backend/runtime_endpoint.pydef shutdown(self):
    from sglang.srt.utils import kill_process_tree
​
    if self.pid is not None:
        # 注意:__del__ 会调用此方法,阻塞回收会卡住 GC 线程
        kill_process_tree(self.pid, wait_timeout=None)
        self.pid = None

评论区精华

超时异常处理 设计

作者提出开放问题:_wait_for_reap_or_raise 超时会抛出异常,而在 finally 块中会掩盖原始异常。

结论:作者倾向保留大声报错,但也愿意改为 log-and-continue,最终代码未显式处理,可能默认保留抛错。 · 待处理

风险与影响

风险点集中在默认行为变更可能影响调用方:kill_process_tree 现在默认阻塞,若调用方不期望等待可能挂起。但调用限定了超时,且有显式 None 用于 __del__。另外,tokenizer_managerinclude_parent 修改可能改变退出行为,需验证信号处理。

影响范围主要在 SGLang 内部进程管理和测试基础设施:默认等待能防止 GPU 显存和端口资源残留,减少 CI 超时和僵尸进程。对用户而言,尤其影响使用 kill_process_tree 的脚本和测试,可能有额外等待时间。

默认行为变更 可能掩盖异常

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论