执行摘要
- 一句话:为kill_process_tree添加可选超时等待,修复引擎关闭后的GPU资源竞争条件。
- 推荐动作:值得精读,特别是
_wait_for_reap_or_raise函数的实现,展示了如何处理异步进程回收、超时控制和竞态避免,对于涉及多进程管理或资源清理的代码有借鉴意义。
功能与动机
根据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分配间的竞争条件,确保资源及时释放。
实现拆解
- 新增等待函数:在
python/sglang/srt/utils/common.py中新增_wait_for_reap_or_raise函数,实现带警告和超时检查的进程等待逻辑,使用psutil.wait_procs监控进程退出,超时则抛出RuntimeError。
- 扩展kill_process_tree:修改同一文件中的
kill_process_tree函数,添加wait_timeout可选参数,默认None保持向后兼容;当wait_timeout不为None时,调用_wait_for_reap_or_raise等待被杀进程回收。
- 更新引擎关闭路径:在
python/sglang/srt/entrypoints/engine.py和http_server_engine.py的shutdown方法中,调用kill_process_tree时传递wait_timeout=60,确保引擎关闭时等待子进程释放GPU上下文。
- 同步测试fixture:更新四个测试fixture文件(如
default_fixture.py)的tearDownClass方法,同样添加wait_timeout=60,保证测试清理时等待进程回收,避免测试环境竞态。
关键文件:
python/sglang/srt/utils/common.py(模块 工具函数;类别 source;类型 core-logic;符号 kill_process_tree, _wait_for_reap_or_raise): 修改了核心的kill_process_tree函数,新增_wait_for_reap_or_raise辅助函数,是实现超时等待的核心逻辑所在。
python/sglang/srt/entrypoints/engine.py(模块 引擎入口;类别 source;类型 core-logic): 引擎的shutdown方法调用kill_process_tree,添加wait_timeout=60确保关闭时等待进程回收,是核心调用点之一。
python/sglang/srt/entrypoints/http_server_engine.py(模块 服务器入口;类别 source;类型 core-logic): HTTP服务器引擎的shutdown方法同样添加wait_timeout=60,扩展超时等待到服务器关闭路径。
关键符号:kill_process_tree, _wait_for_reap_or_raise
关键源码片段
python/sglang/srt/utils/common.py
修改了核心的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) # 如果有超时设置,调用等待函数
评论区精华
该PR未经过正式review讨论,作者直接合并;评论中仅包含测试重跑命令和机器人响应,无技术性争议或设计权衡。
风险与影响
- 风险:技术风险包括:
1) 超时设置风险:若wait_timeout过短(如低于进程回收时间),可能无法完全避免竞争条件;过长则增加关闭延迟,影响测试执行效率。
2) 异常路径风险:新函数_wait_for_reap_or_raise可能引入未处理的异常(如psutil库错误),需确保错误处理健壮。
3) 兼容性风险:默认wait_timeout=None保持向后兼容,但调用方显式设置超时后,若依赖旧行为(如火警式退出)可能被破坏。
- 影响:对用户:提高系统关闭后GPU资源可立即重用的可靠性,减少因资源泄漏导致的测试失败或生产环境问题。对系统:关闭过程可能增加最多60秒延迟,但避免了潜在竞态和资源泄漏,提升稳定性。对团队:增强基础设施的健壮性,尤其在高频测试场景中减少调试开销。
- 风险标记:核心路径变更, 超时配置风险, 测试覆盖调整
关联脉络
参与讨论