Prhub

#33329 [CI] Size the CPU stage from the live partition model

原始 PR 作者 hnyls2002 合并时间 2026-08-03 13:26 文件变更 2 提交数 5 评论 2 代码增减 +46 / -8

执行摘要

CPU CI 阶段改用实测分区模型动态扩容

base-a-test-cpu 内联在 pr-test.yml 中,从未经过 _pr-test-stage.yml 分发,所以没有像其他套件那样拉取实时分区模型,run_suite.py 只能回退到 LPT: no live est (None); using in-source est_time。这些写死的 est_time 比实测 p90 低约 1.5 倍,例如 test_cargo_workspace.pyest_time=300 对应实测 p90 仅 80,为一个文件独占整个分片;而最差 LPT 分片实测约 600 秒,与 10 分钟预算几乎重合——最近一次运行 47/47 passed 却以 1.45 秒之差触发步骤超时。PR 的诉求正是让分片按实测 p90 而不是手写常量切分,从根上消除这类“测试全部通过但 CI 超时标红”的假阴性。

值得 CI/基础设施维护者精读,核心看三点:一是把步骤 timeout-minutes 作为分区驱动信号而非硬性上限的“预算反馈”设计;二是 _INLINE_SUITE_JOBS 的硬失败策略,用显式错误替代静默降级;三是 CPU(托管、弹性)与 GPU(自托管、稀缺)在 max_parallel 节流上的差异化处理。若团队后续继续推进“实时分区模型”这条线,本 PR 的字段规范与读取逻辑是很好的参照。

讨论亮点

该 PR 没有任何人工 review 评论(review_comments_count = 0),Issue 评论仅剩 Gemini Code Assist 的停用公告和作者触发的 /tag-and-rerun-ci 重跑命令。核心设计说明都在 PR body 的 Notes for review 中:compute_partitions 会把 Run test 步骤的 timeout-minutes 读回为每分片预算,因此它是“驱动扇出”的信号而不是兜底上限——缩短预算会得到更多分片而不是更短分片;动态套件的 max_parallel = size // 3 节流只适合稀缺的自托管 GPU runner,对托管的 ubuntu-latest 会串行化阶段,所以所有 CPU 套件保持 max_parallel = size。PR 还声明分区输出对其他套件字节一致,无未解决的技术争议。

实现拆解

  1. 获取实时分区模型(pr-test.yml:在缓存 restore 之后新增 Fetch live partition model 步骤,仅在 partition_model_sha 非空时执行;用固定 SHA 从 raw.githubusercontent.com/sgl-project/sglang-ci-stats 拉取 model.json/tmp/partition-model.json,并在 Run test 步骤追加 --partition-model-file /tmp/partition-model.json,保证所有分片基于同一份模型快照划分。
  2. 内联套件预算接入(compute_partitions.py:新增 _INLINE_SUITE_JOBS = {"base-a-test-cpu"}load_run_timeouts 除了扫描 uses: _pr-test-stage.yml 的作业外,还从内联作业中查找唯一的 Run test 步骤并读取 timeout-minutes;找不到或数量不对时抛 RuntimeError,避免静默退回旧行为。这样 compute_partitions 可以把该步骤超时读回为每分片预算——预算驱动扇出,而不是只做硬性兜底。
  3. 解除 CPU 套件固定扇出(compute_partitions.py:把 base-a-test-cpu_BASE_A_OVERRIDES 移除,其扇出改由实时模型计算:{size: 8, max_parallel: 8} 变为 {size: 10, max_parallel: 10},并随 CPU 测试量增长自动扩容。
  4. 节流策略按 runner 类型区分(compute_partitions.pymax_parallel 改为 unthrottled = full_parallel or all(t.backend == HWBackend.CPU for t in group);纯 CPU 套件不做 size // 3 节流,避免把托管的 ubuntu-latest 阶段串行化,自托管 GPU runner 仍保持旧节流。
  5. 配套预算与回归说明(pr-test.ymlRun test 步骤 timeout-minutes 从 10 提升到 15,匹配最差分片约 600 秒的实测;PR body 确认其他所有套件的分区输出字节一致,回归面仅限 base-a-test-cpu。本 PR 未新增单元测试,正确性由后续 pr-test 运行本身验证。
文件 模块 状态 重要度
scripts/ci/utils/compute_partitions.py CI 脚本 modified 4.97
.github/workflows/pr-test.yml 工作流 modified 4.14

关键符号

load_run_timeouts compute_partitions

关键源码片段

scripts/ci/utils/compute_partitions.py infrastructure

核心逻辑变更:移除 `base-a-test-cpu` 固定扇出、新增 `_INLINE_SUITE_JOBS` 预算读取、按 runner 类型调整 `max_parallel` 节流规则。

# 内联在 pr-test.yml 中的套件不走可复用 stage,因此没有
# `with.run_timeout_minutes` 输入,只能从 `Run test` 步骤兜底读取。
_INLINE_SUITE_JOBS = {"base-a-test-cpu"}def load_run_timeouts(pr_test_yml_path: str) -> dict:
    """把 pr-test*.yml 中的步骤预算映射为每个套件的分片预算。"""
    with open(pr_test_yml_path) as f:
        wf = yaml.safe_load(f)
    jobs = wf.get("jobs") or {}
    timeouts = {}
​
    # 常规套件:直接读可复用 stage 的 `run_timeout_minutes` 输入
    for job_id, job in jobs.items():
        if not isinstance(job, dict) or job.get("uses") != _REUSABLE_STAGE_USES:
            continue
        with_ = job.get("with") or {}
        suite = with_.get("self_name", job_id)
        timeouts[suite] = int(with_["run_timeout_minutes"])
​
    # 内联套件:必须且只能有一个带 timeout-minutes 的 `Run test` 步骤;
    # 找不到就抛错,宁可 CI 显式失败,也不静默退回源码内陈旧 est_time。
    for suite in _INLINE_SUITE_JOBS:
        budgets = [
            step["timeout-minutes"]
            for step in ((jobs.get(suite) or {}).get("steps") or [])
            if isinstance(step, dict)
            and step.get("name") == "Run test"
            and "timeout-minutes" in step
        ]
        if len(budgets) != 1:
            raise RuntimeError(
                f"load_run_timeouts: inline suite {suite!r} needs exactly one "
                f"`Run test` step with `timeout-minutes` in {pr_test_yml_path}."
            )
        timeouts[suite] = int(budgets[0])
​
    if not timeouts:
        raise RuntimeError(
            f"load_run_timeouts: no jobs matched uses={_REUSABLE_STAGE_USES!r} "
            f"in {pr_test_yml_path}"
        )
    return timeouts
​
​
def compute_partitions(...):
    ...
    size = max(1, ideal_size)
    # 节流只用于稀缺的自托管 GPU runner;托管 ubuntu-latest 是弹性的,
    # 纯 CPU 套件若被 `max_parallel` 节流,反而会把整个阶段串行化。
    unthrottled = full_parallel or all(
        t.backend == HWBackend.CPU for t in group
    )
    max_parallel = size if unthrottled else compute_max_parallel(size)
    result[suite] = {
        "size": size,
        "arr": list(range(size)),
        "max_parallel": max_parallel,
        ...
    }

评论区精华

PR 评论仅含机器人停用公告与重跑命令 other

`review_comments_count` 为 0;Issue 评论只有 Gemini Code Assist 停用公告和作者触发的 `/tag-and-rerun-ci`。技术说明集中在 PR body 的 `Notes for review` 段落。

结论:无未解决的技术争议;作者直接合并。 · 已解决

风险与影响

  • load_run_timeouts 从“找不到就跳过”变成“找不到就抛 RuntimeError”:任何未来新增的内联套件若缺 Run test 步骤,会直接让 CI 解析失败而非静默回退。这是有意为之,但改变了失败模式,后续工作流改动需同步维护 _INLINE_SUITE_JOBS
  • 新步骤依赖 raw.githubusercontent.com/sgl-project/sglang-ci-stats 的可用性:虽然固定 SHA 并带 --retry 3 --retry-delay 2,但外网拉取仍有偶发失败窗口;失败时 run_suite.py 退回源码 est_time,分区质量会回退到修复前水平。
  • 扇出从 8 增至 10 意味着每次多消耗 2 个弹性 CPU runner 的并发与 Actions 分钟数,且随 CPU 测试增多还会继续上涨,CI 成本需要留意。
  • 15 分钟预算比最差实测 600 秒多出约 50% 余量,可能掩盖后续测试耗时回归(不会立刻超时,而是分片缓慢变慢后再靠扩容消化)。

影响面集中在 CI 基础设施的 CPU 测试阶段:base-a-test-cpu 分片从固定 8 变为模型驱动的 10(max_parallel 保持 10),步骤预算从 10 分钟提到 15 分钟,且随测试数量增加会自动扩容。其他所有套件的分区输出保持字节一致,说明该变更不会扰动 GPU/分发套件的调度。对日常贡献者的直接收益是 CPU 门禁更稳定,不再出现“全部测试通过却因超时被标红”的假阴性;对团队而言,CPU 测试的扩容路径变为数据驱动,后续新增测试无需手调扇出参数。

外部分区模型拉取 预算读回改为硬失败 CPU 分片扩容成本

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论