slime 项目 CI 体系深度解析:双层测试架构、GPU E2E 工作流与测试编写实战
【免费下载链接】slimeslime is an LLM post-training framework for RL Scaling.项目地址: https://gitcode.com/GitHub_Trending/slime12/slime
本篇技术指南聚焦 slime(LLM post-training / RL Scaling 训练框架)仓库的持续集成体系:它如何以「默认运行的 CPU 正确性测试 + label 触发的 GPU end-to-end 测试」双层结构,在快速反馈与真实 Megatron + SGLang 训练路径覆盖之间取得平衡。读完本文,你将掌握 slime CI 的完整工作原理、每个 label 与 job 的触发关系、prepare()/execute()e2e 测试编写范式、GPU 锁申请机制,以及如何通过 Jinja2 模板安全地修改 CI matrix。
为什么 CI 要拆成两层
slime 的训练与 rollout 涉及 Megatron 训练后端、SGLang 推理引擎、Ray 调度、分布式 checkpoint 等大量组件,完整跑一次训练会消耗大量 GPU 时间。因此 CI 被有意拆成两层,这个设计决策贯穿整个仓库:
- 默认运行的 CPU 正确性测试:每个 PR、每次 push 到
main、以及手动workflow_dispatch都会运行。它覆盖纯逻辑层面的正确性不变量(correctness invariant),不等待 GPU 集群即可快速检查。 - 通过 label 触发的 GPU end-to-end 测试:在自托管 GPU runner 上验证真实的 Megatron + SGLang training/rollout 路径。
正如文档所述,这个拆分是刻意的:大部分 correctness invariant 应该在不等待 GPU 集群的情况下快速检查;真正依赖完整训练和 rollout 的行为,则由 GPU e2e job 覆盖。前者的目标是"快",后者的目标是"真",两者互补而非替代。
工作原理:模板驱动的工作流
slime 的 CI workflow 定义在 .github/workflows/pr-test.yml,但不要直接编辑它——该文件由 Jinja2 模板 .github/workflows/pr-test.yml.j2 自动生成。
CPU Jobs
CPU job 运行在 GitHub-hosted 的ubuntu-latestrunner 上,有两个 job:
cpu-unittest:安装 CPU 版 PyTorch 和轻量依赖,然后通过python tests/<test_file>.py运行注册的 unit/contract tests。agent-adapter-test:以同样方式运行 agent adapter tests,但额外安装openai、openai-agents、anthropic等 provider SDK 依赖(这部分测试被单独拆出,正是因为它们有额外的第三方依赖)。
从模板源码(pr-test.yml.j2)可以看到 CPU job 的依赖安装细节:通过pip install torch --index-url https://download.pytorch.org/whl/cpu安装纯 CPU PyTorch,再安装pytest numpy packaging pyyaml omegaconf tqdm httpx requests ray pybase64 pylatexenc sympy aiohttp pillow safetensors psutil等轻量依赖,最后pip install -e . --no-deps安装 slime 本体。
CPU job不使用 Docker、不申请 GPU、也不会调用tests/ci/gpu_lock_exec.py。执行时通过matrix.info.num_gpus判断:为0时直接运行,否则走 GPU 锁路径。
GPU E2E Jobs
GPU job 运行在自托管 GPU runner上,每个 job 依次执行四个步骤:
- 启动 Docker container,通常使用
slimerl/slime:latest;镜像验证使用slimerl/slime-test:latest。 - 在容器内通过
pip install -e . --no-deps --break-system-packages安装 slime。 - 通过
python tests/ci/gpu_lock_exec.py --count <num_gpus>申请所需 GPU。 - 执行注册的测试文件:
python tests/<test_file>.py。
模板中的 Docker 启动参数值得注意,它完整反映了 GPU 训练环境的需求(见 pr-test.yml.j2):
docker run --pull=always --rm \ --privileged \ --cap-add SYS_NICE \ --security-opt seccomp=unconfined \ --network host \ --gpus all \ --ipc=host \ --shm-size=16g \ --ulimit memlock=-1 \ --ulimit stack=67108864 \ --memory=0 \ --memory-swap=0 \ ...其中--network host用于 Ray/SGLang 的分布式通信、--shm-size=16g保证共享内存充足、--ulimit memlock=-1取消内存锁定限制(Megatron/ NCCL 常用)、--memory=0 --memory-swap=0不限制容器内存。容器还会挂载宿主机共享目录-v /mnt/nvme0n1/slime_ci/models:/root/models和-v /mnt/nvme0n1/slime_ci/datasets:/root/datasets,用于缓存已下载的模型与数据集,避免每个 job 重复下载。
GPU 测试通常遵循e2e 模式:prepare()下载模型和数据集,execute()构建 CLI 参数并调用U.execute_train(...)(U是 slime/utils/external_utils/command_utils.py 的别名)。U.execute_train内部会先清理残留的 sglang/ray/redis/slime 进程,随后ray start --head拉起 Ray 集群,最后通过ray job submit将训练任务提交到集群执行,并注入PYTHONPATH、CUDA_DEVICE_MAX_CONNECTIONS、NCCL_NVLS_ENABLE(按是否检测到 NVLink 自动设置)等运行时环境变量。
Changed-Test Job
run-ci-changed是一个动态检测机制:它相对origin/main找出新增或修改的tests/test_*.py和tests/plugin_contracts/test_*.py文件,并为每个文件构建 matrix。
关键逻辑在模板的e2e-test-changed-detectjob 中:
CHANGED=$(git diff --name-only --diff-filter=AM origin/main...HEAD -- 'tests/test_*.py' 'tests/plugin_contracts/test_*.py' || true) # 对每个文件提取 NUM_GPUS,缺省时默认为 8 NGPU=$(grep -oP '^NUM_GPUS\s*=\s*\K\d+' "$filepath" | head -1) NGPU=${NGPU:-8}也就是说:如果测试文件缺少NUM_GPUS常量,CI 会默认使用8,因此纯 CPU 测试文件必须显式声明:
NUM_GPUS = 0changed-test job 本身走 self-hosted Docker 路径;当NUM_GPUS = 0时,它直接运行测试、不申请 GPU。
CI Jobs 与触发方式一览
| Trigger | Job | 类型 | 说明 |
|---|---|---|---|
| 自动运行 | cpu-unittest | CPU | 默认运行的 unit/contract tests,覆盖 argument validation、schedule、reward、sample、rollout validation、checkpoint utilities 和 plugin contracts。 |
| 自动运行 | agent-adapter-test | CPU | 默认运行的 agent adapter tests,包含额外 provider SDK 依赖。 |
run-ci-sglang-config | e2e-test-sglang-config | GPU | SGLang config 测试,覆盖高级 rollout engine deployment 和 mixed/offload 场景。 |
run-ci-megatron | e2e-test-megatron | GPU | 核心 Megatron 训练测试,覆盖 dense、MoE、PPO、MTP、OPD、async rollout、PD/Mooncake 和 debug replay 路径。 |
run-ci-precision | e2e-test-precision | GPU | 数值精度和并行一致性检查。 |
run-ci-ckpt | e2e-test-ckpt | GPU | Checkpoint save/load 正确性,包括 CPU/GPU optimizer state 和 async save。 |
run-ci-image | e2e-test-image | GPU | 在slimerl/slime-test:latest上运行与run-ci-megatron相同的 matrix。 |
run-ci-changed | e2e-test-changed | Mixed | 只运行 changed tests,并使用每个文件中的NUM_GPUS。 |
也可以在 Actions 页面通过workflow_dispatch手动验证,它会按照 workflow 条件运行注册的 jobs。workflow_dispatch还提供一个infinite_run布尔输入,配合环境变量SLIME_TEST_ENABLE_INFINITE_RUN可以让训练无限运行,用于长时间稳定性验证。
从模板源码可以进一步确认 label 的判定逻辑(pr-test.yml.j2):
if: (github.event_name == 'workflow_dispatch') || (github.event.pull_request && contains(github.event.pull_request.labels.*.name, '<label>'))即 GPU job 只在两种情况下触发:手动 workflow_dispatch,或 PR 被打上对应 label。而自动 job 的条件是github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || github.event_name == 'push'。此外 workflow 声明了concurrency分组和cancel-in-progress: true,同一 PR/ref 的新提交会取消旧运行;push 到main专门触发默认的 CPU job,用于捕获"两个 PR 单独通过、合并后 main 损坏"的 PR-pair 回归(见模板顶部注释)。
CPU Unit Tests:正确性第一道防线
CPU suite 是 correctness 的第一道防线,用来在进入昂贵 GPU run 之前捕获 silent RL infrastructure bugs。
当前注册的 CPU suite 覆盖(对应 pr-test.yml.j2 中cpu-unittest的 tests 列表,共 50+ 个文件):
- Megatron argument 和 HF config validation(如 tests/test_megatron_argument_validation.py);
- DP/CP scheduling utilities 和 CP loss invariance(tests/test_dp_schedule.py、tests/test_cp_utils.py、tests/test_loss_cp_invariance.py);
- metric reporting 和 distributed metric aggregation(tests/test_metric_report.py、tests/test_metric_report_dist.py);
- math、GPQA、F1、DeepScaler、DAPO-style math 等 reward-model grading utilities(tests/test_rm_math.py、tests/test_rm_gpqa.py、tests/test_rm_f1.py、tests/test_rm_deepscaler.py、tests/test_rm_math_dapo.py);
Sample行为、rollout validation 和 agent trajectory merging(tests/test_sample.py、tests/test_process_rollout_data.py);- HF checkpoint saver 行为(tests/utils/test_hf_checkpoint_saver.py);
- rollout function、generate function、runtime hook 和 path loading 的 customization hook contracts(tests/plugin_contracts/test_plugin_generate_contracts.py 等四个 contract 测试);
- 另有 PPO loss/advantage/whiten、FP8、stateless Adam、layerwise alignment、reloadable process group、placement group、expert routing 等基础设施测试。
Agent adapter tests(tests/test_agent/ 下的 5 个文件)单独放在agent-testjob 中,因为它们需要额外 SDK 依赖。
常用本地命令(在仓库根目录直接执行):
python tests/test_agent/test_trajectory_manager_branching.py python -m pytest tests/test_megatron_argument_validation.py tests/plugin_contracts/test_plugin_generate_contracts.py值得说明的是,CPU 测试文件普遍以顶层NUM_GPUS = 0声明自身不需要 GPU(见 tests/test_megatron_argument_validation.py 第 9 行),这样即便被run-ci-changed动态拾取,也只会走纯 CPU 路径。
GPU E2E Tests:验证 CPU 测不到的真实训练路径
GPU e2e tests 验证 CPU tests 无法覆盖的集成训练/rollout 行为:
run-ci-sglang-config:高级 SGLang deployment path,包括 config-based engine layouts(tests/test_sglang_config_mixed_offload.py、tests/test_qwen2.5_0.5B_sglang_config_distributed.py 等 6 个文件)。run-ci-megatron:主要 Megatron backend coverage。从模板中的megatron_tests变量可以看到其矩阵规模——约 25 条测试条目,覆盖 dense/MoE recipe(tests/test_qwen3_30B_A3B.py、tests/test_moonlight_16B_A3B.py)、async rollout(tests/test_qwen2.5_0.5B_fully_async_short.py)、OPD(tests/test_qwen2.5_0.5B_opd_sglang.py)、PPO-style path(tests/test_qwen3_4B_ppo.py、tests/test_qwen3_4B_ppo_disaggregate.py)、PD/Mooncake(tests/test_qwen3.6_35B_A3B_pd_mooncake.py)和 debug rollout-then-train replay(tests/test_qwen2.5_0.5B_debug_rollout_then_train.py)。单个 job 还可以携带test_args(如 checkpoint 测试的 optimizer state 组合)、use_deepep、use_fp8_rollout、enable_eval等 matrix 附加字段。run-ci-precision:不同并行设置下的数值一致性(tests/test_qwen3_0.6B_parallel_check.py)。run-ci-ckpt:checkpoint save/load 组合和 async save——同一测试文件 tests/test_qwen3_4B_ckpt.py 以不同test_args注册 5 次:--save-optimizer gpu --load-optimizer gpu、gpu/cpu、cpu/cpu、cpu/gpu四种 optimizer state 迁移组合,外加--async-save异步保存。run-ci-image:与run-ci-megatron相同的 matrix,但运行在slimerl/slime-test:latestrelease/test image 上,用于验证新镜像没有破坏既有测试。
日常 PR 优先使用 targeted labels。run-ci-image消耗 GPU 时间较多(相当于把整个 megatron matrix 重跑一遍),应谨慎使用。
GPU 锁机制:gpu_lock_exec 深入
GPU job 的核心步骤之一是 tests/ci/gpu_lock_exec.py,它在共享的自托管 GPU 机器上实现基于fcntl.flock的文件锁调度。其命令行接口:
python tests/ci/gpu_lock_exec.py --count <num_gpus> [--devices 0,1] [--total-gpus 8] [--timeout 86400] \ [--target-env-name CUDA_VISIBLE_DEVICES] [--lock-path-pattern "/dev/shm/custom_gpu_lock_{gpu_id}.lock"] \ [--print-only] -- <command>主要参数与行为:
--count:申请任意N个空闲 GPU;--devices则指定具体的 GPU id 列表(二者互斥)。--total-gpus:宿主机 GPU 总数,默认8(即单台 H100 节点的标准配置)。--timeout:等待锁的超时秒数,默认 24 小时;超时抛TimeoutError。--target-env-name:默认将获取到的设备列表写入CUDA_VISIBLE_DEVICES环境变量(模板中的默认用法),也可以改成其他变量名。--lock-path-pattern:锁文件路径模式,默认/dev/shm/custom_gpu_lock_{gpu_id}.lock(放在/dev/shm是 tmpfs,进程退出自动清理,避免残留锁文件)。--print-only:只探测并打印当前空闲 GPU,不持有锁。
获取锁后,工具将 GPU 列表写入指定环境变量(如CUDA_VISIBLE_DEVICES=0,2,3,5),再以子进程方式执行--之后的命令。获取锁采用非阻塞flock(LOCK_EX | LOCK_NB)+ 指数退避重试(SLEEP_BACKOFF = 5.0乘以随机因子)的策略;工具还会接管SIGINT/SIGTERM/SIGHUP,在收到信号时先终止子进程再退出,并归一化负 returncode(128 - returncode)。获取到--devices指定的 GPU 时会按顺序逐个阻塞等待。该工具支持lslocks排查锁状态(源码注释中明确提示)。
在 workflow 中的典型用法(CPU 为 0 时不加锁,直接运行):
if [ "$NUM_GPUS" = "0" ]; then python "$TEST_PATH" "${TEST_ARGS_ARRAY[@]}" else python tests/ci/gpu_lock_exec.py --count "$NUM_GPUS" -- python "$TEST_PATH" "${TEST_ARGS_ARRAY[@]}" fi编写新测试
CPU Tests
对于 CPU-only tests,遵循 4 个步骤:
- 按照附近文件的模式,将测试放在
tests/test_*.py、tests/utils/test_*.py或tests/plugin_contracts/test_*.py下。 - 如果这个文件可能被
run-ci-changed运行,添加顶层NUM_GPUS = 0。 - 让文件可以直接执行:
if __name__ == "__main__": raise SystemExit(pytest.main([__file__]))- 如果测试需要永久进入 CI matrix,在 .github/workflows/pr-test.yml.j2 的
cpu-unittest或agent-adapter-testjob 中注册,然后重新生成 workflow(见下文"Workflow 生成")。
GPU E2E Tests
对于 GPU e2e tests:
- 创建
tests/test_<your_test_name>.py,遵循已有的prepare()/execute()模式。 - 用
NUM_GPUS = <N>声明所需 GPU 数量。 - 在
prepare()中下载所需模型和数据集。 - 在
execute()中构建参数并调用U.execute_train(...)。 - 在 .github/workflows/pr-test.yml.j2 的合适 GPU job 中注册测试,然后重新生成 workflow。
官方示例骨架(含清理代理环境变量的细节——因为容器内下载需要代理,而训练进程通常不应带代理):
import os import slime.utils.external_utils.command_utils as U MODEL_NAME = "Qwen2.5-0.5B-Instruct" MODEL_TYPE = "qwen2.5-0.5B" NUM_GPUS = 4 def prepare(): U.exec_command("mkdir -p /root/models /root/datasets") U.exec_command(f"hf download Qwen/{MODEL_NAME} --local-dir /root/models/{MODEL_NAME}") def execute(): # 构建参数字符串并调用 U.execute_train(...) ... if __name__ == "__main__": prepare() for proxy_var in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY"): os.environ.pop(proxy_var, None) execute()一个真实参照是 tests/test_qwen2.5_0.5B_debug_rollout_then_train.py:它声明MODEL_NAME = "Qwen2.5-0.5B-Instruct"、MODEL_TYPE = "qwen2.5-0.5B"、NUM_GPUS = 8,prepare()中下载模型和 GSM8K 数据集;execute()拆成两阶段——execute_rollout_only用--debug-rollout-only启动 SGLang 生成两轮 rollout 并保存到临时目录,execute_train_only则用--load-debug-rollout-data跳过 SGLang、加载已保存数据直接训练两步,验证了 rollout-then-train 的两阶段调试链路。
U(slime/utils/external_utils/command_utils.py)还提供了convert_checkpoint(HF → torch_dist 权重转换,自动读取scripts/models/<type>.sh中的MODEL_ARGS)、hf_download_dataset、fp8_cast_bf16、get_default_wandb_args(自动根据测试文件名生成--use-wandb --wandb-project slime-<test_name>参数)等便捷函数,以及NUM_GPUS_OF_HARDWARE = {"H100": 8, "GB200": 4, "GB300": 4}等硬件配置常量,可用于编写多硬件适配的测试。
Workflow 生成
pr-test.yml由 Jinja2 模板pr-test.yml.j2自动生成,不要直接编辑pr-test.yml——生成的 YAML 文件头部带有"auto-generated"警告注释,直接修改会在下次生成时被覆盖。
如果要修改固定 CI matrix:
- 编辑 .github/workflows/pr-test.yml.j2。
- 运行生成脚本:
python .github/workflows/generate_github_workflows.py- 同时提交
.github/workflows/pr-test.yml.j2和生成的.github/workflows/pr-test.yml。
生成器 .github/workflows/generate_github_workflows.py 使用自定义定界符的 Jinja2 Environment(<% %>为块、<< >>为变量),扫描*.yml.j2文件渲染后写入同名.yml,并在头部写入自动生成声明。模板中还使用了<< config.tests | tojson(indent=2) | indent(10, true) >>将 Python 侧的测试矩阵数据序列化为 YAML,因此修改矩阵时只需增删模板顶部的tests列表条目,无需手写大段 YAML。当前目录下的其他 workflow(如conda-ci.yml、pre-commit.yml)也遵循同一套 .j2 模板 + 生成器模式。
PR 应该选择哪些检查
文档给出了清晰的决策路径,按改动类型对号入座:
- 纯 argument parsing、reward、schedule、sample、trajectory 或 hook-contract 改动:优先依赖 CPU tests,它们默认运行且反馈最快。
- SGLang topology 或 rollout engine deployment 改动:使用
run-ci-sglang-config,其矩阵同时包含 GPU 为 0 的纯配置校验(tests/utils/test_sglang_arguments.py、tests/utils/test_sglang_config.py)和真实部署测试。 - Megatron training、loss、checkpoint conversion 或 model recipe 改动:使用
run-ci-megatron;必要时加run-ci-precision(数值一致性)或run-ci-ckpt(checkpoint 组合)。 - Docker image 或 dependency 改动:使用
run-ci-image在slimerl/slime-test:latest上验证。 - 新增或修改测试:使用
run-ci-changed做 targeted validation——它只运行你改动的测试文件,并把NUM_GPUS作为 GPU 数量,是最省资源的回归手段。
附:自托管 runner 的部署要点
GPU e2e job 依赖自托管 runner,部署说明见 tests/ci/README.md 与 tests/ci/github_runner/docker-compose.yml:
- 需在仓库 Secrets 中配置
WANDB_API_KEY(用于 e2e 测试的指标上报)。 - runner 以 Docker 方式部署,compose 文件启动 8 个
ghcr.io/actions/actions-runner副本,挂载/var/run/docker.sock(使 runner 能启动 CI 所需的训练容器)、/data/slime_ci(与 CI 容器共享模型/数据集缓存)和/home/runner/externals,并通过GITHUB_RUNNER_URL/GITHUB_RUNNER_TOKEN环境变量完成注册;token 会定期失效,需要及时更新。 - runner 以
privileged: true、user: root运行,entrypoint 中先config.sh注册、再run.sh启动。
总结
slime 的 CI 体系是一套为"RL 训练框架"量身定制的工程实践:CPU 层用 50+ 个轻量测试守住参数校验、奖励计算、采样、checkpoint 工具与插件契约等纯逻辑不变量;GPU 层用 label 门控的自托管 e2e 测试覆盖 dense/MoE 训练、PPO/OPD、async rollout、PD/Mooncake、数值精度与 checkpoint 组合等真实训练路径;run-ci-changed则提供了按文件粒度的动态回归。理解这套分层与触发机制,是在该仓库贡献代码时快速定位"该跑哪个 label、怎么写一个新测试"的关键。对开发者而言,把测试放进tests/test_*.py、声明正确的NUM_GPUS、遵循prepare()/execute()模式,再在 .j2 模板中注册并重新生成 workflow,即可让新功能获得完整的 CI 保护。
【免费下载链接】slimeslime is an LLM post-training framework for RL Scaling.项目地址: https://gitcode.com/GitHub_Trending/slime12/slime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考