Prhub

#2221 fix(ci): split CPU and GPU reusable workflows

原始 PR 作者 guapisolo 合并时间 2026-08-07 04:00 文件变更 5 提交数 1 评论 0 代码增减 +134 / -112

执行摘要

拆分 CPU/GPU CI 可复用工作流,消除冗余 skipped job

PR body 描述症状与根因:每个硬件 stage 都把另一硬件的 job 暴露为 skipped;根本原因是 _run-ci.yml 同时声明 run 与 run-cpu,而 jobs..if 在两个可复用 job 展开后才求值。修复还保留了另一层动机:CPU 快速测试继续跑在 GitHub-hosted ubuntu-latest 上,不占用 GPU fleet runner 槽位;同时 job 列表变干净,方便定位真正执行的任务。commit message 也明确说明要保留 runner、command、secret、gating 与 check-name 契约。

值得快速浏览。本 PR 展示了 GitHub Actions 可复用工作流拆分的一个干净范式:用“一个工作流只声明本硬件一个 job”替代“单工作流 + job 级 if”,从根上消除展开语义导致的冗余 skipped job;同时用结构锁测试固定 job id、路由计数与 cpu_runner 残留,防止回归。关注 _run-cpu-ci.yml 与 _run-ci.yml 的步骤对等性,以及 test_run_suite.py 中基于正则提取 job id 的断言方式——这是用测试守护 YAML 结构的好例子。

讨论亮点

本 PR 没有任何 review 评论,yushengsu-thu 以空 body 直接 APPROVED,因此没有产生争议线程。有效讨论信息浓缩在 commit message 中:GitHub expands every job in a reusable workflow before evaluating job-level conditions,即根因是可复用工作流的展开语义早于 job 级 if 求值。评审通过即认可该诊断与“一硬件一工作流”的拆分方向。测试承担了行为一致性论证:新锁测试从 YAML 文本层面断言路由计数与 job 单一性。

实现拆解

变更入口是 pr-test.yml 的 stage 路由,核心是工作流文件拆分,并配套结构锁测试与文档更新。

  1. 新建 .github/workflows/_run-cpu-ci.yml:把原 _run-ci.yml 中的 run-cpu job 整体迁出,job id 保持 run-cpu,runner 仍是 ubuntu-latest。文件开头注释记录了拆分动机:GitHub 会先展开可复用工作流内的所有 job 再评估 if,同文件双 job 必然产生跨硬件 skipped 兄弟。迁移内容包含 resolve-refs 依赖 ref 解析(支持 PR body 的 ci-megatron-pr / ci-sglang-pr 指令与 refs/pull/#N/head 转换)、sglang 与 Megatron-LM checkout、PYTHONPATH 配置、requirements-ci-cpu.txt 裸 pip 安装、verify_source_resolution.py 校验、--list-only 计划打印与 HF_TOKEN 注入。workflow_call 输入只保留 execute_command。

  2. 精简 .github/workflows/_run-ci.yml:删除 cpu_runner input、run job 的 if 条件以及整个 run-cpu job,runs_on 描述同步去掉 cpu_runner: false 字样。文件现在只承担 GPU 路径,run job id 不变,既有 gate 与 check-name 契约不受影响。

  3. 调整 pr-test.yml 路由:stage-a-cpu 与 stage-b-cpu 的 uses 改为 ./.github/workflows/_run-cpu-ci.yml 并删除 cpu_runner: true 传参;GPU 五个 stage 仍指向 _run-ci.yml 并继续传递 runs_on 与 container_image。CPU 与 GPU 的 gating(needs: [resolve-ci-policy, resolve-ci-image]、CPU 失败短路 GPU)原样保留。

  4. 测试配套:tests/ci/test/test_run_suite.py 新增 _reusable_workflow 辅助与 test_cpu_and_gpu_stages_use_dedicated_reusable_workflows。断言 pr-test.yml 中 _run-cpu-ci.yml 恰好出现 2 次、_run-ci.yml 恰好出现 5 次;再用正则提取两个工作流 jobs 下的 job id,断言 GPU 工作流只有 run、CPU 工作流只有 run-cpu,并且 cpu_runner 开关彻底消失。这把 job 结构契约锁死,防止 stage 增删或开关回退导致问题复发。

  5. 文档配套:docs/ci/00-stage.md 更新 Runner selection 与 Launch 段落,说明 CPU stage 调用 _run-cpu-ci.yml、CUDA stage 调用 _run-ci.yml,每个工作流只声明本硬件 job,GitHub 不再为另一硬件追加 skipped 兄弟。验证方面,tests/ci/test/test_run_suite.py 本地 70 个测试通过,Ruff、check-yaml、git diff --check 与 commit hooks 均通过。

文件 模块 状态 重要度
.github/workflows/_run-cpu-ci.yml CI 工作流 added 5.8
.github/workflows/_run-ci.yml CI 工作流 modified 5.16
tests/ci/test/test_run_suite.py CI 测试 modified 5.12
.github/workflows/pr-test.yml CI 工作流 modified 3.8
docs/ci/00-stage.md CI 文档 modified 2.02

关键符号

_reusable_workflow test_cpu_and_gpu_stages_use_dedicated_reusable_workflows

关键源码片段

tests/ci/test/test_run_suite.py test-coverage

新增锁测试,断言 pr-test.yml 的路由数量与每个可复用工作流的 job 结构,防止回归。

    @staticmethod
    def _reusable_workflow(name: str) -> str:
        # 读取 .github/workflows 下的可复用工作流,供结构锁测试使用
        return (
            Path(__file__).resolve().parents[3] / ".github" / "workflows" / name
        ).read_text()
​
    def test_cpu_and_gpu_stages_use_dedicated_reusable_workflows(self):
        # 锁住 pr-test.yml 的路由:CPU 两个 stage 走 _run-cpu-ci.yml,GPU 五个 stage 走 _run-ci.yml
        workflow = self._workflow()
        assert workflow.count("uses: ./.github/workflows/_run-cpu-ci.yml") == 2
        assert workflow.count("uses: ./.github/workflows/_run-ci.yml") == 5
        assert "cpu_runner" not in workflow
​
        # 每个可复用工作流只能声明本硬件对应的一个 job,
        # 否则 GitHub 展开后仍会给另一硬件 stage 追加 skipped 兄弟 job
        gpu_workflow = self._reusable_workflow("_run-ci.yml")
        cpu_workflow = self._reusable_workflow("_run-cpu-ci.yml")
        job_id_pattern = r"^  ([A-Za-z_][A-Za-z0-9_-]*):$"
        gpu_jobs = re.findall(job_id_pattern, gpu_workflow.split("\njobs:\n", 1)[1], re.MULTILINE)
        cpu_jobs = re.findall(job_id_pattern, cpu_workflow.split("\njobs:\n", 1)[1], re.MULTILINE)
        assert gpu_jobs == ["run"]
        assert cpu_jobs == ["run-cpu"]
        # 迁移后应彻底移除 cpu_runner 开关,避免新旧双路径并存
        assert "cpu_runner" not in gpu_workflow
        assert "cpu_runner" not in cpu_workflow

评论区精华

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

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

风险与影响

主要风险来自双工作流复制带来的行为漂移。resolve-refs 依赖解析、依赖安装、verify_source_resolution.py 与 HF_TOKEN 注入等步骤现在同时存在于 _run-cpu-ci.yml 与 _run-ci.yml,后续若只修改一处,CPU 与 GPU 测试环境会逐渐不一致,而锁测试只检查 job id 与 uses 计数,不校验步骤内容,步骤级漂移无法被 CI 拦截。其次,路由计数被锁测试固定为 2 次 CPU、5 次 GPU,未来增减 stage 必须同步更新断言,否则 PR 会被测试拦住(这也是设计意图)。风险仅限 CI 工作流,不影响训练、rollout、模型等运行时路径;若 CPU job 迁移时遗漏某个步骤,通常表现为 CPU 测试环境显式报错,而不是静默失败。

对 PR 提交者:GitHub 展开的 job 列表不再混入另一硬件的 skipped 兄弟 job,可读性和可排查性明显提升。对 CI 维护者:引入第二个专用可复用工作流,公共逻辑修改时需要同步两处。对算力:CPU 快速测试仍运行在 GitHub-hosted ubuntu-latest,不占用 GPU fleet runner 槽位;CPU 失败短路 GPU 的 gating 语义完全保留。对仓库产品代码:零影响。

双工作流步骤复制,存在漂移风险 锁测试仅覆盖结构不校验步骤内容 stage 数量变化需同步更新锁测试 CPU/GPU 工作流行为一致性依赖手工维护

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论