Prhub

#30110 [diffusion] fix: shut down diffusion workers on serve exit

原始 PR 作者 mickqian 合并时间 2026-07-04 20:42 文件变更 4 提交数 2 评论 4 代码增减 +198 / -17

执行摘要

为 diffusion 服务添加优雅关闭 worker 进程的机制

在 SGLang Office Hour 7/2 讨论中,用户反馈 Ctrl-C 后后台 scheduler 进程仍然存活,需要手动 kill,有时会残留 GPU 内存占用导致 OOM(PR body 原文:"Ctrl-C often leaves the background scheduler process alive, requiring manual kill and sometimes leaving enough GPU memory occupied to OOM a large model")。本 PR 旨在通过优雅关闭机制解决该问题。

值得精读,尤其是 launch_server.py 中的层级式进程关闭模式(join → terminate → kill)设计和 kill_itself_when_parent_died 的强化实现。该 PR 为 diffusion 服务稳定性做出了重要改进。

讨论亮点

Review 建议:在请求 scheduler 关闭前检查 master 进程是否存活

gemini-code-assist[bot] 提出:如果 master 进程已经死亡(例如被直接 SIGKILL),则 scheduler 肯定未运行,此时 _request_monolithic_scheduler_shutdown 会超时等待 5 秒,造成不必要的延迟。建议增加 processes[0].is_alive() 检查。该建议未被采纳,作者未回复,PR 已合并。可能作者认为在正常关闭路径中 master 进程不会先于 scheduler 退出,或者该优化影响较小。

实现拆解

  1. launch_server.py 中添加关闭辅助函数:新增 _process_names_join_processes_with_deadline_terminate_alive_processes_kill_alive_processes_request_monolithic_scheduler_shutdownshutdown_scheduler_processes。这些函数实现了带超时的层级式进程关闭:先尝试 join,超时后 terminate,再超时后 kill。
  2. 在服务入口集成关闭逻辑:在 launch_server 函数的 finally 块以及 pool_disagg_launch_serverdisagg_role_launch_server 中调用 shutdown_scheduler_processes,确保无论正常退出还是异常中断都能清理进程。
  3. 重写 utils.py 中的 kill_itself_when_parent_died:移除平台分支,直接使用 Linux prctl(PR_SET_PDEATHSIG, SIGKILL) 并处理错误;如果父进程已死亡(pid 变为 1),则立即自杀,避免 worker 成为孤儿。
  4. gpu_worker.pyrun_scheduler_process 入口处调用 kill_itself_when_parent_died:使得每个 worker 进程在启动时绑定父进程死亡信号。
  5. 添加单元测试文件 test_launch_server_shutdown.py:使用 _FakeProcess 模拟进程行为,测试 monolithic 关闭流程(验证发送了 ShutdownReq,调用了进程的 terminate 和 kill),测试 scheduler 请求失败时仍强制执行关闭,以及非 monolithic 角色不发送关闭请求。
文件 模块 状态 重要度
python/sglang/multimodal_gen/runtime/launch_server.py 启动器 modified 8.56
python/sglang/multimodal_gen/test/unit/test_launch_server_shutdown.py 测试 added 7.39
python/sglang/multimodal_gen/utils.py 工具 modified 6.35
python/sglang/multimodal_gen/runtime/managers/gpu_worker.py GPU 工作器 modified 4.89

关键符号

_process_names _join_processes_with_deadline _terminate_alive_processes _kill_alive_processes _run_http_server_process _request_monolithic_scheduler_shutdown shutdown_scheduler_processes kill_itself_when_parent_died run_scheduler_process

关键源码片段

python/sglang/multimodal_gen/runtime/launch_server.py core-logic

核心实现文件,添加了 shutdown_scheduler_processes 及辅助函数,并在多个服务入口集成关闭逻辑。

# 带超时的进程连接,确保不会永久阻塞
def _join_processes_with_deadline(processes, timeout_s: float) -> None:
    deadline = time.monotonic() + timeout_s
    for process in processes:
        remaining_s = max(0.0, deadline - time.monotonic())
        process.join(timeout=remaining_s)# 终止仍然存活的进程,返回依然存活的进程列表
def _terminate_alive_processes(processes, timeout_s: float) -> list:
    alive = [p for p in processes if p.is_alive()]
    if not alive:
        return []
    logger.warning("Worker process(es) did not exit in time; terminating: %s", _process_names(alive))
    for process in alive:
        process.terminate()
    _join_processes_with_deadline(alive, timeout_s)
    return [p for p in alive if p.is_alive()]# 强制杀死仍然存活的进程
def _kill_alive_processes(processes, timeout_s: float) -> None:
    alive = [p for p in processes if p.is_alive()]
    if not alive:
        return
    logger.warning("Worker process(es) did not terminate in time; killing: %s", _process_names(alive))
    for process in alive:
        process.kill()
    _join_processes_with_deadline(alive, timeout_s)# 向 monolithic scheduler 发送关闭请求
def _request_monolithic_scheduler_shutdown(server_args: ServerArgs) -> None:
    if server_args.disagg_role != RoleType.MONOLITHIC:
        return
    client = SchedulerClient()
    try:
        client.initialize(server_args)
        client.forward(ShutdownReq(), timeout_ms=_SCHEDULER_SHUTDOWN_TIMEOUT_MS)
    except Exception as e:
        logger.warning("Failed to request graceful scheduler shutdown: %s", e)
    finally:
        client.close()# 主关闭入口:先尝试优雅关闭 scheduler,再逐步强制退出 workers
def shutdown_scheduler_processes(
    server_args: ServerArgs | None,
    processes: list,
    *,
    request_shutdown: bool = True,
) -> None:
    if not processes:
        return
​
    # 1. 尝试优雅关闭 monolithic scheduler
    if request_shutdown and server_args is not None:
        _request_monolithic_scheduler_shutdown(server_args)
​
    # 2. 等待 worker 进程正常退出(最长 _WORKER_JOIN_TIMEOUT_S 秒)
    _join_processes_with_deadline(processes, _WORKER_JOIN_TIMEOUT_S)
​
    # 3. 对未退出的 worker 发送 SIGTERM
    still_alive = _terminate_alive_processes(processes, _WORKER_TERMINATE_TIMEOUT_S)
​
    # 4. 对仍然未退出的 worker 发送 SIGKILL
    if still_alive:
        _kill_alive_processes(processes, _WORKER_KILL_TIMEOUT_S)

评论区精华

检查 master 进程存活以避免不必要的 scheduler shutdown 超时 性能

gemini-code-assist[bot] 在 `launch_server.py` 第 164 行建议:如果 master 进程已经死亡,scheduler 必然未运行,此时调用 `_request_monolithic_scheduler_shutdown` 会超时 5 秒。建议在 `shutdown_scheduler_processes` 中添加 `if processes[0].is_alive()` 检查。

结论:作者未回应,PR 已合并,未采纳该建议。 · not-addressed

风险与影响

  1. 进程关闭超时风险:如果 worker 进程在 terminate 或 kill 后仍不退出(例如陷入死循环或内核态阻塞),_join_processes_with_deadline 会超时返回,可能导致关闭不彻底。已在代码中添加日志警告,但无法强制恢复。
  2. 依赖 SchedulerClient 网络调用:在 _request_monolithic_scheduler_shutdown 中会初始化 SchedulerClient 并发送请求,如果网络异常或 shcheduler 未监听,会触发异常并被 catch 后继续关闭流程,不会阻塞过程,但可能留下未处理的连接(finally 中已调用 close())。
  3. kill_itself_when_parent_died 在非 Linux 平台直接返回:原代码已通过 if sys.platform != "linux": return 保护,不会误杀,但 worker 失去该保护。

用户影响:使用 diffusion 服务的用户在执行 Ctrl-C 或正常关闭服务时,不再需要手动清理残留进程,避免 GPU 内存泄漏导致 OOM。
系统影响:关闭流程增加最多约 (join 10s + terminate 1s + kill 1s) = 12s 的额外延迟,但通常在正常关闭时 worker 能快速退出,只有异常情况会触发更长等待。
团队影响:新增的 shutdown_scheduler_processes 函数和测试为后续维护提供了清晰的关闭契约,有助于防止进程泄漏。

进程关闭超时风险 依赖网络调用 非 Linux 平台保护缺失

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论