Prhub

#1920 fix(ci): stabilize SGLang and FSDP E2E

原始 PR 作者 guapisolo 合并时间 2026-07-29 09:46 文件变更 9 提交数 7 评论 1 代码增减 +43 / -30

执行摘要

修复 SGLang E2E 失败不可见与 FSDP 长任务误入 nightly

PR body 明确列出 6 个根因:MODEL_PATH 把默认权重获取推迟到 SGLang 启动时;_wait_for_ready 在 fixture 清理前抛错导致服务进程无人回收;pytest.main 返回失败但 main 丢弃退出码;est_time=300 低估 60-rollout FSDP 负载,旧 runner 在 1800 秒超时时任务仍在推进;resolve_policy 把 long 纳入 nightly 标准范围;labels 把长 FSDP 用例分到 fsdp 或 short 范围。作者的目标是让 setup 失败在 CI 中对齐可见,并让长任务只能经显式 run-ci-long / run-ci-all 选择,同时保留显式请求的并集兜底语义。

值得精读。PR body 的 root cause 编号分析质量高,把「失败为什么不可见」拆成 6 个独立根因并逐一修复,是排查 CI 假性红绿的范本;resolve_policy 的「减法不是否决、显式请求并集兜底」语义值得作为 CI 策略设计参考。对需要在仓库中注册长时间 E2E 用例的工程师,本 PR 的标签分类与 est_time 预算规则有直接指导意义。建议顺带确认 run-ci-fsdp 标签当前是否仍有归属用例。

讨论亮点

本 PR 没有实质性的 review 讨论:审核人 Zhichenzzz 直接 APPROVED 且未留评论;仓库唯一一条评论来自 gemini-code-assist 机器人,声明其 GitHub 代码审查服务已停用。真正的设计权衡写在 PR body 的 Review Focus 与 7 个 commit 的演进中:作者要求重点审视 MODEL_PATH 解析、start_sglang_server 的就绪清理、以及 resolve_policy 对 long 的处理;2×GPU FSDP 用例的标签经历「加 fsdp 域标签 → 退回 short → 最终定为 long」三次调整,反映了对「能否被 run-ci-fsdp 选中」与「是否过长混入 short/nightly」之间的取舍,最终选择了语义更严格的 long。

实现拆解

  1. SGLang 失败可见化tests/e2e/sglang/test_chat_input_ids_equivalence.py 新增 _prepare_model(),在 sglang_server fixture 中启动服务前用 snapshot_downloadQwen/Qwen3-0.6B 预下载到 /root/models/Qwen3-0.6BMODEL_PATH 改为环境变量 SGLANG_E2E_MODEL_PATH 优先、默认本地路径兜底;__main__pytest.main(...) 改为 raise SystemExit(pytest.main(...)),让直接执行脚本时的失败传导到进程退出码。
  2. SGLang 服务清理tests/e2e/sglang/utils/sglang_server.pystart_sglang_servertry/except (RuntimeError, TimeoutError) 包住 _wait_for_ready,就绪失败时先 server.stop()raise,回收子进程与日志句柄;由于失败发生在 fixture yield 之前,fixture finally 中的停止逻辑不会重复触发。
  3. FSDP 长任务重分类tests/e2e/short/test_qwen3_0.6B_fsdp_colocated_2xGPU.py 的 est_time 从 300 提到 3000 秒(按 runner 规则获得约 3750 秒文件预算),标签从 short 改为 long,工作负载与 gsm8k 0.71 阈值不变;tests/e2e/fsdp/test_qwen3_0.6B_fsdp_distributed.py 标签从 fsdp 改为 long,使其只能经显式长任务请求选择。
  4. nightly 策略调整tests/ci/ci_policy.pyresolve_policy 中 nightly 分支从 _ALL - {"ft-long"} 改为 _ALL - {"long", "ft-long"};减法语义不变(不是逐用例否决),显式 run-ci-long / run-ci-all 仍通过并集重新引入被减标签,docstring 同步改写为作用域列表。
  5. 配套测试与文档tests/ci/test/test_run_suite.py 更新 TestResolvePolicy 参数化期望、在 nightly alias CLI 测试中新增 'long' not in alias_policy 断言,并把 nightly 范围用例重命名为 test_nightly_scope_excludes_long_and_ft_long_but_selects_ft_shortdocs/ci/00-stage.mddocs/ci/01-label.md 同步 nightly 排除项与显式 run-ci-long 加回语义;.github/workflows/pr-test.yml 仅更新 cron 注释。7 个 commit 展示了标签策略的反复:先加 fsdp 域标签、再退回 short、最终统一归入 long。
文件 模块 状态 重要度
tests/ci/ci_policy.py CI 策略 modified 4.92
tests/e2e/sglang/test_chat_input_ids_equivalence.py SGLang 用例 modified 5.09
tests/e2e/sglang/utils/sglang_server.py SGLang 服务 modified 4.4
tests/ci/test/test_run_suite.py 套件测试 modified 5.11
tests/e2e/short/test_qwen3_0.6B_fsdp_colocated_2xGPU.py FSDP 用例 modified 3.95
tests/e2e/fsdp/test_qwen3_0.6B_fsdp_distributed.py FSDP 用例 modified 3.62
docs/ci/01-label.md CI 文档 modified 2.83
docs/ci/00-stage.md CI 文档 modified 2.14
.github/workflows/pr-test.yml 工作流 modified 2.88

关键符号

resolve_policy _prepare_model start_sglang_server _wait_for_ready test_nightly_scope_excludes_long_and_ft_long_but_selects_ft_short test_nightly_alias_matches_explicit_cadence

关键源码片段

tests/ci/ci_policy.py core-logic

本 PR 的策略核心:resolve_policy 的 nightly 分支从排除 ft-long 扩展为同时排除 long,是「FSDP 长任务不进 nightly」的关键开关,subtraction 语义与显式请求并集优先级的 docstring 同步改写。

def resolve_policy(cadence: str, raw_labels: set[str]) -> RunPolicy:
    # 分支优先级从高到低为 run-ci-all > nightly > run-ci-image;
    # 显式请求的 run-ci-<x> 标签最后再参与并集,保证 run-ci-long
    # 与 run-ci-all 在 nightly 下仍能重新引入被减去的 long 标签
    requested = strip_run_ci_prefix(raw_labels) & set(KNOWN_LABELS)
    if "run-ci-all" in raw_labels:
        # run-ci-all 覆盖全部注册标签,不受任何减法影响
        scope = set(KNOWN_LABELS)
    elif cadence == NIGHTLY_CADENCE:
        # nightly 默认范围减去 long 与 ft-long:长耗时 FSDP 任务
        # 不再默认进入 nightly,只有显式 run-ci-long 才执行
        scope = set(KNOWN_LABELS) - {"long", "ft-long"}
    elif "run-ci-image" in raw_labels:
        # image 构建范围维持原语义:排除 long 与全部 FT 标签
        scope = set(KNOWN_LABELS) - {"long", "ft-short", "ft-long"}
    else:
        scope = set()
    return RunPolicy(
        cadence=cadence,
        include_labels=frozenset(scope | requested),
        bypass_fastfail=cadence == NIGHTLY_CADENCE or "bypass-fastfail" in raw_labels,
    )
tests/e2e/sglang/test_chat_input_ids_equivalence.py test-coverage

SGLang 失败可见化的主要载体:新增 _prepare_model 预下载默认模型到 /root/models/Qwen3-0.6B,并把 pytest.main 退出码通过 raise SystemExit 传播给 CI runner。

DEFAULT_MODEL_ID = "Qwen/Qwen3-0.6B"
DEFAULT_MODEL_PATH = "/root/models/Qwen3-0.6B"
# 环境变量提供本地权重路径时优先使用,便于 CI 复用已缓存的模型
MODEL_PATH_OVERRIDE = os.environ.get("SGLANG_E2E_MODEL_PATH")
MODEL_PATH = MODEL_PATH_OVERRIDE if MODEL_PATH_OVERRIDE is not None else DEFAULT_MODEL_PATH
​
​
def _prepare_model() -> None:
    # 只有使用默认模型路径时才预下载权重;显式设置
    # SGLANG_E2E_MODEL_PATH 时假定环境已备好权重,不重复拉取
    if MODEL_PATH_OVERRIDE is None:
        snapshot_download(DEFAULT_MODEL_ID, local_dir=DEFAULT_MODEL_PATH)
​
​
@pytest.fixture(scope="module")
def sglang_server():
    # 在启动 SGLang 之前完成权重准备,把网络下载失败提前暴露在
    # setup 阶段,而不是让服务在启动超时后才发现模型缺失
    _prepare_model()
    server = start_sglang_server(model_path=MODEL_PATH)
    try:
        yield server
    finally:
        server.stop()
​
​
if __name__ == "__main__":
    # pytest.main 返回退出码但不会自动影响进程退出状态;
    # raise SystemExit 让直接执行脚本时的失败能正确传给 CI runner
    raise SystemExit(pytest.main([__file__, "-v"]))
tests/e2e/sglang/utils/sglang_server.py test-coverage

就绪失败清理逻辑:start_sglang_server 捕获 RuntimeError / TimeoutError 后先 server.stop() 再抛出,修复服务进程与日志句柄泄漏。

    process = subprocess.Popen(cmd, stdout=log_file, stderr=subprocess.STDOUT, env=env)
    server = SGLangServer(process=process, host=host, port=port, log_path=log_path, _log_file=log_file)
​
    try:
        _wait_for_ready(server, timeout_secs=startup_timeout_secs)
    except (RuntimeError, TimeoutError):
        # 就绪失败时主动回收子进程与日志句柄,避免 setup 报错后
        # 残留 SGLang 服务进程拖住后续用例或污染其他测试
        server.stop()
        raise
    return server

评论区精华

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

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

风险与影响

  1. 长任务预算未实测:est_time=3000 是估算值,PR 明确说明未在 2×H200 上重跑完整 60-rollout 负载;若真实耗时仍超过约 3750 秒,用例依旧会被 runner 超时终止,只是把失败点推后。
  2. run-ci-fsdp 可能失配:两个 FSDP 用例标签都改为 long 后,run-ci-fsdp 可能不再命中任何用例(仓库中未见其他 fsdp 标签用例),依赖该标签的 PR 会静默失去 FSDP 覆盖。
  3. nightly 默认覆盖收窄:nightly 范围排除 long 后,依靠 nightly 做长任务回归的场景需要显式 run-ci-long;新用例若不遵守「长任务只打 long 标签」的约定,也可能漏出覆盖。
  4. SGLang 启动路径变更:snapshot_download 依赖 CI 节点网络与 HuggingFace 可达性,且 /root/models 需可写并持久化;环境异常会在 setup 阶段快速失败,这是本次预期行为,但失败原因指向网络而非代码。
  5. 清理逻辑安全性:就绪失败发生在 fixture yield 之前,fixture finally 不会重复 stop,逻辑上安全;若未来有人把 stop 逻辑提前,需注意幂等性。
  1. CI 可靠性:SGLang 用例从「等待约 900 秒后假成功」变为「setup 阶段快速失败并清理进程」,直接文件运行与 pytest 运行的失败行为对齐。
  2. 覆盖范围:nightly 与 run-ci-image 默认范围都排除 long,两个 FSDP 长任务只经显式 run-ci-long / run-ci-all 执行;对测试注册标签的语义要求更严格。
  3. 协作与文档:docs/ci/00-stage.md 与 01-label.md 同步了新语义,后续注册用例的工程师以文档为准。
  4. 影响面:全部为测试与 CI 配置变更,无产品代码、schema 或部署变更,对用户侧无直接影响。
FSDP 长任务预算未实测 run-ci-fsdp 可能失配用例 nightly 默认覆盖收窄 SGLang 启动路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论