Prhub

#32829 [CI] Graceful teardown for kv_canary and EAGLE spec fixtures

原始 PR 作者 alisonshao 合并时间 2026-08-02 16:10 文件变更 18 提交数 8 评论 7 代码增减 +58 / -46

执行摘要

测试夹具改优雅关闭,消除 GPU 残留误报

两次 CI 失败(test_self_e2e_perturb_real_kv_unused_cache.py 在 GPU-idle 30s 预检查超时、test_spec_eagle.py 的 TestEagle3Overlap 报 kill_process_tree 60s 未回收)均源于 fixture 用裸 SIGKILL 关停服务器:teardown 在 0.01s 内返回但 52 GiB(单服务器)或 295 GiB(四卡)内存仍被分配,是否在下个类的 30s gate 内回收完全取决于内核回收速度。与 #31746 / #31871 同根因:大 pinned host KV 池的 cudaHostUnregister 与内核回收极慢,实测 243 GB pinned host 池在裸 SIGKILL 后需 25.94 s 才完全干净。PR body 还指出全仓库 502 个杀掉服务器进程树的 teardown 中,只有来自 #31746 / #31871 的两个做了优雅关闭,因此需要系统性推广。

值得精读。这是一个罕见的"机制导向"CI 修复案例:作者没有止步于表面修好,而是用定量 A/B 证伪自己的初次结论,最终给出"减少 38% 但不消除"的诚实评估。对维护大型 GPU 测试集群的团队,terminate_and_kill_process_tree 加静态反汇编扫描的方法论可借鉴;同时它揭示了 SIGKILL 关停 CUDA 服务器时 pinned host 内存回收的耗时机制,对理解测试隔离设计有参考价值。

讨论亮点

最有价值的讨论是 alisonshao 在 issue 评论中对结论的两次修正:

  • 第一次 A/B 只测了 device memory(裸 kill 0.01s 返回、52/295 GiB 未释放;优雅关闭 5.4-6.0s、约 0 残留),作者随后承认该测量没有覆盖真正的失败机制。
  • 换上真实 pinned host pool 复测(Llama-3.1-8B、--enable-hierarchical-cache --hicache-ratio 4.0,243 GB pinned host + 67.5 GB device):裸 kill 后完全回收需 25.94 s,优雅关闭后仍需 16.01 s。作者总结:"This is the mechanism. The fix reduces the next class's exposure by ~38% but does not eliminate it."
  • 另有一个 follow-up 建议:抽象 test base 如 _PerturbRealKvUnusedCacheBase 仍会被 unittest 从 main 收集,在最后一次真实 teardown 后空跑一次 GPU-idle gate 才 SkipTest,移出测试模块可去掉这个多余检查点。

实现拆解

  1. 提取公共 teardown 工具:在 python/sglang/test/test_utils.py 新增 terminate_and_kill_process_tree(process, terminate_timeout=60, **kill_kwargs),序列为 SIGTERM → 最多等 60s → kill_process_tree(SIGKILL)兜底,并把 wait_timeout 等关键字透传给后者的 kill 等待逻辑。
  2. 收敛本 PR 触发失败的夹具:python/sglang/test/kv_canary/e2e_base.py 的 CapturedServerE2EBase(CanaryE2EBase 基类)与 python/sglang/test/server_fixtures/spec_eagle_fixture.py 的 SpecEagleServerBase,这两个是 run 30448736325 中分别在 GPU-idle 30s 超时和 SIGKILL 后 60s 未回收的现场。
  3. 推广到其余单服务器夹具:default_fixture(约 26 个测试模块)、chunked_prefill_test_utils(约 12 个)、streaming_session_fixture、pcg_spec_fixture、ngram_fixture、mmmu_fixture、dsa_mtp_fixture、standalone_fixture、hybrid_attn_backend_fixture、eagle_fixture、vlm_utils、test_deterministic_utils。其中 default_fixture 与 eagle_fixture 原本就传 wait_timeout=60,语义从"SIGKILL 后等 60s"变成"SIGTERM 后再 SIGKILL 并等 60s";kl_mamba 测试的 HiCache L2/L3 原本有内联的 terminate + wait 序列,现在删除重复代码统一走公共函数。
  4. 修复连锁 import 回归:vlm_utils 不再 import kill_process_tree 后,test/registered/vlm/test_vision_openai_server_a.py(通过 from sglang.test.vlm_utils import * 间接依赖)与 test/registered/xpu/test_gemma_4_e2b.py(直接 from sglang.test.vlm_utils import kill_process_tree)会分别触发 NameError / ImportError;PR 将这两个文件的 teardown 也一并转换为 terminate_and_kill_process_tree。
  5. 有意保留的例外:disaggregation_fixture 因涉及三个进程加活跃的 mooncake 传输引擎,SIGTERM 行为需单独 PR 验证;ascend/xpu 专用 fixtures 因非 CUDA runner、GPU-idle gate 不适用,未纳入本次修改。
  6. 验证手段:本地 1×H100 与 4×H100 devbox 跑全部转换后的 fixture(含 TestEagle3Overlap、4×H100 TP4 的 kl_mamba 12 个用例)全部通过;另用静态方法反汇编 522 个受影响测试模块中的 3284 个 setUp/tearDown 方法排查不可解析的 globals,提前捕获了上述两个 import 回归。
文件 模块 状态 重要度
python/sglang/test/test_utils.py 测试工具 modified 5.99
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_mamba.py 缓存测试 modified 5.05
python/sglang/test/server_fixtures/default_fixture.py 默认夹具 modified 4.39
python/sglang/test/kv_canary/e2e_base.py 端到端基座 modified 3.99
python/sglang/test/server_fixtures/spec_eagle_fixture.py 推测解码 modified 3.99

关键符号

terminate_and_kill_process_tree

关键源码片段

python/sglang/test/test_utils.py test-utility

新增 terminate_and_kill_process_tree(),是全部转换夹具共用的 teardown 工具:先 SIGTERM 等待最多 60s,再 SIGKILL 兜底,让服务器在用户态释放 CUDA 上下文与 pinned host 内存。

def terminate_and_kill_process_tree(
    process,
    terminate_timeout: float = 60,
    **kill_kwargs,
) -> None:
    """先优雅关闭服务器,再对剩余进程树 SIGKILL 兜底。    直接 kill_process_tree 时,内核需要在进程回收阶段解构 CUDA 上下文、
    解钉 host 内存;实测大 pinned 池(243 GB 量级)会让 GPU 残留约 26 秒,
    足以让下一个测试类的 GPU-idle 预检查(30s gate)误报失败。
    先发 SIGTERM,让服务器在用户态 unregister 这些资源,再走 SIGKILL。
    """
    # 第一步:SIGTERM 请求优雅退出,最多等 terminate_timeout 秒
    process.terminate()
    try:
        process.wait(timeout=terminate_timeout)
    except subprocess.TimeoutExpired:
        pass # 超时不阻塞,直接进入下面的 SIGKILL 兜底
    # 第二步:无论是否优雅退出,都确保整棵进程树被清掉
    kill_process_tree(process.pid, **kill_kwargs)
test/registered/radix_cache/unified_radix_tree/test_unified_radix_cache_kl_mamba.py test-coverage

#31871 的延续:三个类(普通、HiCache L2、HiCache L3)teardown 统一收敛到公共函数,删除各自内联的 terminate + wait 重复代码;其中 HiCache L2/L3 正是 pinned host 池回收最慢的场景。

    @classmethod
    def tearDownClass(cls):
        # HiCache L2 场景:服务器持有 243 GB 量级的 pinned host KV 池,
        # 裸 SIGKILL 时内核回收曾让下一个测试类的 GPU-idle 预检查超时;
        # 先 SIGTERM 让用户态 unregister,实测将残留窗口从约 25.94 s
        # 缩短到 16.01 s(减少约 38%),仍未完全消除但显著降低误报概率
        terminate_and_kill_process_tree(cls.process, wait_timeout=60)

评论区精华

优雅关闭 vs 裸 SIGKILL 的 GPU 内存回收 A/B 验证 测试

alisonshao 在 1×H100 与 4×H100 devbox 上做 A/B:裸 kill_process_tree 0.01s 返回但仍有 52 GiB(单服务器)或 295 GiB(四卡)未释放;terminate_and_kill_process_tree 5.4-6.0s 返回、残留约 0。所有转换后的 fixture 运行时全部通过。

结论:转换后的 fixture 全部验证通过;但作者随后承认首次测量只覆盖了 device memory,未覆盖真实失败机制。 · 已解决

根因校正:pinned host KV 池的回收耗时才是关键 正确性

作者复现真实机制:大 pinned host KV 池的 cudaHostUnregister 远超释放 device 内存。243 GB pinned host + 67.5 GB device 时,裸 kill 后完全回收需 25.94 s,优雅关闭后仍要 16.01 s。结论是修复把下一测试类的暴露窗口减少约 38%(25.9 s → 16.0 s),但没有消除。

结论:接受作者结论:机制确认,修复是缓解而非根治;总回收时间约 26 s 在繁忙共享 runner 上仍可能超过 30 s gate。 · 已解决

抽象 test base 仍被 unittest 收集并空跑 GPU-idle gate 设计

alisonshao 指出 _PerturbRealKvUnusedCacheBase 等抽象基类仍会被 unittest 从 __main__ 收集,在最后一次真实 teardown 后空跑一次 GPU-idle gate 才 SkipTest,属于多余检查点。

结论:留作 follow-up,不在本 PR 处理。 · 待处理

分组测试下的 rerun-test 行为待验证 question

作者自记 todo:test grouped tests with rerun test,关注 grouped 场景下 rerun-test 命令的行为是否与单测一致。

结论:未在本 PR 处理,作者标记为待办。 · 待处理

风险与影响

  1. GPU 残留窗口未完全消除:作者实测优雅关闭后仍需约 16 s 才完全干净,在繁忙共享 runner 上仍可能逼近 30 s gate,只是概率降低。
  2. SIGTERM 不响应的服务器会让每个 teardown 先等满 terminate_timeout 60s,再叠加 kill 阶段的 wait_timeout,最坏情况每个类多出最多约 120 s,CI 总时长可能上升。
  3. vlm_utils 不再 re-export kill_process_tree 属于符号级破坏性变更,任何在其外部依赖该符号的测试或工具代码会立即 ImportError(本 PR 修了仓库内两个,仓库外测试可能受影响)。
  4. PD disaggregation 路径未覆盖:ChunkedTestPDBase 复用 PDDisaggregationServerBase.tearDownClass(),仍为裸 SIGKILL,相关场景的 GPU 残留问题依旧存在。
  5. 涉及约 100 个测试模块的 teardown 行为变更,虽经静态扫描验证,但个别 fixture 可能有自己的进程管理假设,需要 CI 多轮观察。

对 CI 稳定性:直接消除两类失败现场(GPU-idle 30s 超时、SIGKILL 后 60s 未回收),并把约 100 个测试模块的 teardown 从"依赖内核回收"改为"用户态释放优先",降低跨测试类污染(失败的类不再毒化下一个类的 gate)。对成本:每次 teardown 增加数秒,CI 全量耗时上升,但换来的是更稳定的调度。对团队:确立了"先 SIGTERM 再 SIGKILL"的 teardown 标准模式,后续新 fixture 应直接使用公共工具而非各自内联序列。

GPU 残留窗口未完全消除 SIGTERM 超时可能拖慢 CI vlm_utils 符号移除引发连锁 disaggregation 路径未覆盖 涉及约 100 个测试模块

关联 Issue

#31746 [Fix] Release hierarchical cache host pool on graceful shutdown
#31871 [CI] Graceful teardown in kl_mamba hicache tests to release pinned host pool

完整报告

参与讨论