16 KiB
MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径)
本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出开发改造 → 部署测试 → 验收的可复现流程。
v1.1 的核心约束(来自讨论结论)
- 仍然必须通过 head 节点执行
ray job submit提交任务。- 训练/driver 必须落在 worker(head 不跑训练)。
- 多版本
verl共存:同一镜像不变,必须通过 Ray Jobruntime_env.env_vars注入PYTHONPATH让 job 粒度选择代码版本。- 存储只考虑 NFS:dev 环境我们自己 mount;生产环境容器内统一看到
/private/。
1. 目标与非目标
1.1 目标(v1.1 必须做到)
- 可回归:同一环境连续跑多次 PPO 回归,不互相覆盖,输出按 submission id 归档。
- 可扩展:新增并验收通过 2 个 workload(GRPO + SFT)并跑通闭环。
- 可排障:每个 job 目录包含完整的提交快照、关键 env、Ray 状态快照与日志入口。
- 可多版本共存:同一 Ray 集群内,不同 job 通过
PYTHONPATH选择不同verl代码快照。
1.2 非目标(v1.1 不做)
- 不做平台 API/队列/多租户/RBAC(这是 v2/v3)。
- 不做复杂调度(拓扑、IB 域、NUMA、Gang 等自动化策略)。
2. 运行环境约定(dev / prod 一致抽象)
2.1 拓扑(单机 3 容器)
mvp-ray-head:无 GPU,ray start --head --num-cpus=0 --num-gpus=0(控制面 only)mvp-ray-worker-0:4 GPUmvp-ray-worker-1:4 GPU
2.2 “head 不跑训练”的硬约束实现(必须)
- head CPU=0:从资源层面阻断默认 task/driver 落到 head。
- worker 自定义资源标签:worker 启动时带
--resources='{"worker_node": 100}'。 - ray job submit 强制 entrypoint 落 worker:提交时必须带:
--entrypoint-resources='{"worker_node": 1}'--entrypoint-num-cpus=1(显式声明 driver 需要的 CPU)
验证口径:
ray job list的driver_info.node_ip_address必须是 worker 的 IP,而不是 head IP。
2.3 共享存储(NFS)与路径(关键)
- 生产环境:容器内共享根路径固定为
/private/(算力平台统一挂载 NFS)。 - 开发环境:docker compose 也应把宿主机共享目录挂载到容器内的
/private/,从而做到 dev/prod 一致。
统一约定(容器内视角):
SHARED_ROOT=/private- Job 输出:
${SHARED_ROOT}/jobs/<submission_id>/
建议的共享目录结构(v1.1 新增 common/ 与 user/):
${SHARED_ROOT}/datasets/:通用数据(例如 gsm8k parquet)${SHARED_ROOT}/hf/:HuggingFace cache(模型/分词器/权重)${SHARED_ROOT}/jobs/:按 submission id 归档的作业目录(强制)${SHARED_ROOT}/outputs/:临时/非强约束输出(不建议长期依赖)${SHARED_ROOT}/ray/:Ray 调试痕迹(可选,通常 Ray 默认写/tmp/ray)${SHARED_ROOT}/common/:所有用户可读写共享区(模型/数据/代码快照)${SHARED_ROOT}/common/models/:可复用基础模型(可用软链指向 hf cache 或 snapshot)${SHARED_ROOT}/common/datasets/:共享数据(或与datasets/统一规划)${SHARED_ROOT}/common/code/:代码快照(多版本verl/ 自定义 reward)
${SHARED_ROOT}/user/:用户自定义内容(默认所有用户可写)${SHARED_ROOT}/user/code/:reward_fn 等自定义 Python 代码
3. 开发实施方案(代码改造清单)
v1.1 建议新增
src/mvp/v1.1/(保持 v1 可回归不被破坏)。
3.1 JobSpec(最小标准化)
v1.1 的工程化目标是把“提交机制”迁移到 Ray Python SDK,因此输入拆为两份 YAML:
- Ray 基础配置(YAML):address / entrypoint resources / runtime_env 等
- 训练 JobSpec(YAML):workload 语义与训练参数(仍由 Hydra overrides 组织)
训练 JobSpec(YAML)至少包含:
submission_id:可空;为空时由 submitter 生成(但最终必须显式传给ray job submit --submission-id)workload:ppo/grpo/sft(v1.1 必须ppo+grpo+sft)shared_root:默认/private(容器内路径)code_path:verl代码快照目录(用于多版本共存)reward_fn_path(可选):指向${shared_root}/user/code/...下的 Python 文件或模块入口model/dataset:必须指向共享存储的持久化路径(避免每次下载/生成)ray:address=http://127.0.0.1:8265(从 head 容器内部视角)resources:entrypoint_resources={"worker_node":1}entrypoint_num_cpus=1
trainer_overrides:训练参数覆盖(v1.1 默认total_epochs=1、save_freq=10)env_vars:会被透传到runtime_env.env_vars(必须包含PYTHONPATH注入)
交付物(v1.1 SDK 方式):
src/mvp/v1.1/py/configs/dev.yaml(Ray 基础配置示例)src/mvp/v1.1/py/jobspecs/{ppo,grpo,sft}.yaml(训练 JobSpec 示例)src/mvp/v1.1/py/run.py(入口:使用 Ray Python SDK 提交/查询/停止/拉日志)- 设计文档:
specs/mvp/v1.1/sdk_submit_refactor.md
3.2 多版本 verl 共存(必须)
原则:镜像固定不变;job 粒度通过 PYTHONPATH 选择 verl 代码快照。
提交时必须注入(runtime_env):
PYTHONPATH="<CODE_PATH>:$PYTHONPATH"(CODE_PATH放最前面)
并要求 job 在日志中打印一行确认 import 来源,例如:
python -c "import verl,inspect; print(verl.__file__)"(或训练入口启动时打印)
v1.1 具体实现(可复现):
- 先用
src/mvp/v1.1/scripts/31_snapshot_verl_code.sh生成代码快照目录${SHARED_ROOT}/common/code/verl/<code_id>/- 该目录里会包含一个
mvp_marker.py(MARKER=<code_id>)
- 该目录里会包含一个
- 提交 job 时让
code_path指向该快照目录;submitter 会在 entrypoint 前打印:MVP_PRECHECK_VERL_FILE(验证 import 来源)MVP_PRECHECK_MARKER(验证选择的 code_path)
3.3 submit_job 工具(组装 ray job submit)
新增一个提交器(建议 Python,避免复杂 bash quoting):
- 输入:JobSpec JSON
- 产物:
- 生成/确定
submission_id - 创建
${SHARED_ROOT}/jobs/<id>/config/、logs/、checkpoints/ - 写入
config/job_spec.json(原样快照) - 写入
config/runtime_env.json(最终用于 submit 的 JSON) - 写入
config/submit_cmd.txt(最终命令行)
- 生成/确定
- 执行:在 head 容器内运行
ray job submit ...
3.4 可排障:debug bundle(强制落盘)
在 job 生命周期的关键节点收集并落盘(至少 2 类):
ray statusray job listray list nodesray list actors
建议落盘到:
${SHARED_ROOT}/jobs/<id>/debug/(每次收集带时间戳文件名)
3.5 Workload 扩展:GRPO(v1.1 新增闭环)
优先用与 PPO 相同入口 python -m verl.trainer.main_ppo,仅通过配置切换算法:
algorithm.adv_estimator=grpo- 其余保持最小可跑:
total_epochs=1、save_freq=10
3.6 Workload 扩展:SFT on Ray(v1.1 必须新增闭环)
3.6.1 入口与参考实现
- 入口:
python -m verl.trainer.sft_trainer_ray - 参考代码:
verl/verl/trainer/sft_trainer.py(非 Ray 版本)与verl/verl/trainer/sft_trainer_ray.py(Ray 版本)
v1.1 要验收的是 “SFT on Ray”,因此默认使用
sft_trainer_ray.py。
3.6.2 连接已有 Ray 集群(必须)
sft_trainer_ray.py 内部直接调用 ray.init(),为了确保它连接到已有集群(head+workers),v1.1 约定:
- 提交 job 时通过
runtime_env.env_vars注入:RAY_ADDRESS=auto
如果发现 ray.init() 未按预期读取 RAY_ADDRESS(Ray 版本差异风险),v1.1 需要提供一个 launcher 兜底:
- 由 launcher 先显式
ray.init(address="auto"),再调用 SFT trainer 逻辑
3.6.3 SFT 数据格式(parquet schema)
sft_trainer_ray 默认使用 MultiTurnSFTDataset,parquet 中至少需要:
messages列:list[dict],dict 至少含role/content
v1.1 的 prepare 阶段需要生成并持久化 SFT 数据,例如:
${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet${SHARED_ROOT}/datasets/gsm8k_sft/val.parquet(可选)
单条样本的 messages 形态示例:
[{ "role": "user", "content": "<question>" }, { "role": "assistant", "content": "<answer>" }]
注意:SFT parquet 不能直接复用 PPO/RL 的 parquet(schema 不同)。
3.6.4 重要细节:SFT Ray Driver 不应依赖 GPU
在 ray job submit 模式下,我们的 entrypoint(driver)默认 不会分配 GPU(我们只指定了 --entrypoint-num-cpus=1,没有指定 --entrypoint-num-gpus)。
而 verl/verl/trainer/sft_trainer_ray.py 的 driver 逻辑里会用 trainer.device 来创建 torch.tensor(..., device=...) 做统计,如果设置为 cuda 且 driver 没有 GPU,会触发:
RuntimeError: No CUDA GPUs are available
因此 v1.1 的 SFT on Ray 验收默认要求:
trainer.device=cpu(driver 只做 orchestration;真正训练仍由 Ray 的 TrainingWorker/资源池占用 GPU)
3.7 v1.1 脚本化交付(必须独立完整)
src/mvp/v1.1/ 需要像 v1 一样提供一套完整脚本,确保 v1.1 可独立运行、可回归:
src/mvp/v1.1/docker-compose.yaml(容器名建议与 v1 区分,避免冲突)src/mvp/v1.1/scripts/00_prereq_check.sh(含 GPU/目录/NFS/verl 代码检查)src/mvp/v1.1/scripts/01_up.sh/02_down.sh(起停)src/mvp/v1.1/scripts/20_start_head.sh/21_start_workers.shsrc/mvp/v1.1/scripts/30_prepare_data_and_model.sh(包含 PPO 数据 + SFT 数据)src/mvp/v1.1/scripts/40_submit_ppo_epoch1.shsrc/mvp/v1.1/scripts/41_submit_grpo_epoch1.shsrc/mvp/v1.1/scripts/42_submit_sft_minimal.shsrc/mvp/v1.1/scripts/50_status.shsrc/mvp/v1.1/scripts/31_snapshot_verl_code.sh(多版本 code snapshot)src/mvp/v1.1/scripts/43_submit_jobspec.sh(通过 JobSpec 提交)src/mvp/v1.1/scripts/12_install_py_deps.sh(安装 PyYAML 等依赖)src/mvp/v1.1/scripts/44_submit_sdk.sh(通过 Ray Python SDK + YAML 提交)
4. 部署与测试流程(dev 环境)
dev 环境以远程机目录为例:
argus@h1:/home2/argus/infra/mvp。v1.1 的所有内容要求放在:
argus@h1:/home2/argus/infra/mvp/v1.1/并在该目录中通过脚本使用
docker exec协调容器。
4.0 清理 v1 环境(必须先做)
v1 已在 argus@h1 部署过容器与 Ray。为保证 v1.1 的可重复测试,开始 v1.1 前必须清理 v1:
- 停止并删除 v1 容器(推荐用 v1 的 down 脚本)
- 确认
docker ps中不再有 v1 的mvp-ray-head/mvp-ray-worker-*
v1.1 的脚本里也提供了一个 best-effort 清理脚本:src/mvp/v1.1/scripts/03_cleanup_v1_legacy.sh(远程目录中同名脚本)。
4.1 环境准备(一次性 / 幂等)
- 目录检查(远程机):
${WORKDIR}/shared/存在并具备上述子目录(含common/、user/)
verl代码目录检查:${WORKDIR}/verl不存在则执行git clone https://github.com/volcengine/verl.git
- GPU 可用性检查:
- 设备存在(例如 0-7 可见),并按 worker 容器分配(每个 worker 4 GPU)
- 模型与数据持久化路径:
- 模型与数据必须落在
${SHARED_ROOT}下;若已存在则跳过下载/生成 - SFT parquet 同样必须落在
${SHARED_ROOT}下;若已存在则跳过生成
- 模型与数据必须落在
4.2 启动 Ray 集群(每次测试)
docker compose up -d- head:
ray start --head --num-cpus=0 --num-gpus=0 ... - workers:
ray start --address=<head>:6379 --resources='{"worker_node":100}' ... - 验证:
ray status显示 1 head + 2 worker,且 headCPU:0 GPU:0
4.3 提交 PPO 回归(必须跑 2 次)
- 生成 JobSpec(可用模板 + 覆盖项)
- 在 head 容器内执行 submitter(或直接
ray job submit) - 验证要点:
ray job list:driver node 是 worker${SHARED_ROOT}/jobs/<id>/下存在config/、logs/、checkpoints/- checkpoint 每 10 step 产生(例如
global_step_10)
4.4 提交 GRPO(新增 workload 验收)
同 PPO,但覆盖 algorithm.adv_estimator=grpo,确保能进入 RUNNING 并完成最小步数。
4.5 提交 SFT on Ray(新增 workload 验收,必须)
- 确认
${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet已存在(由 v1.1 prepare 生成)。 - 通过 head 容器执行
ray job submit提交python -m verl.trainer.sft_trainer_ray。 - 关键约束:
runtime_env.env_vars.RAY_ADDRESS=auto(连接已有集群)--entrypoint-resources='{"worker_node": 1}'(driver 落 worker)PYTHONPATH=<code_path>:$PYTHONPATH(多版本 verl)
- 最小化训练配置建议(避免 OOM/耗时过长):
trainer.total_epochs=1trainer.total_training_steps=10~30trainer.save_freq=10trainer.nnodes=2、trainer.n_gpus_per_node=4(用满 8 卡做一次最小分布式验证)data.train_files=${SHARED_ROOT}/datasets/gsm8k_sft/train.parquettrainer.default_local_dir=${SHARED_ROOT}/jobs/<id>/checkpoints
4.6 工程化验证:JobSpec + 多版本共存(v1.1 必须)
- 生成两个 code snapshot(不同
CODE_ID):CODE_ID=codeA ./scripts/31_snapshot_verl_code.shCODE_ID=codeB ./scripts/31_snapshot_verl_code.sh
- 分别修改/复制 JobSpec 模板,使
code_path指向不同 snapshot:${SHARED_ROOT}/common/code/verl/codeA${SHARED_ROOT}/common/code/verl/codeB
- 用 JobSpec 提交(必须从 head):
./scripts/43_submit_jobspec.sh /workspace/mvp/v1.1/templates/ppo.json(示例)
- 在 Ray job logs 中验证:
MVP_PRECHECK_MARKER打印为对应的codeA/codeBMVP_PRECHECK_VERL_FILE指向${SHARED_ROOT}/common/code/verl/...而不是镜像内 site-packages
5. 验收标准(Definition of Done)
5.1 Hardening DoD(全部必选)
- 提交必须来自 head:能在 head 容器内看到
ray job submit ...的提交记录 - driver 不在 head:
ray job list的driver_info.node_ip_address∈ worker IP,且 ≠ head IP - 输出目录按 submission id 隔离:
${SHARED_ROOT}/jobs/<submission_id>/不复用、不覆盖 - 数据/模型持久化:再次提交时不重复下载/生成(有 “skip if exists” 的日志)
- checkpoint 策略有效:默认
save_freq=10,不会每 step 保存爆盘 - debug bundle 落盘:
${SHARED_ROOT}/jobs/<id>/debug/至少包含 2 类 Ray 状态快照 - 多版本共存验证通过:日志中能确认
verlimport 来源来自 JobSpec 指定的code_path
5.2 Workload DoD(GRPO + SFT 都必须)
- GRPO job 能提交、RUNNING、完成最小训练步数
- GRPO job 产物目录满足与 PPO 相同的目录规范与 debug 规范
- SFT job 能提交、连接已有集群并跑到至少 1 个 step(建议最小步数/epoch)
- SFT job 产物目录满足与 PPO 相同的目录规范与 debug 规范
6. 生产环境部署注意事项(v1.1 需要考虑但不强制在 dev 全量模拟)
- 容器由算力平台创建:我们只负责 SSH 进去纳管(启动 ray / 提交 job / 收集产物)。
- 容器内共享路径为
/private:所有脚本必须以SHARED_ROOT=/private工作,不得写死/mnt/shared。 - 认证仅内部 token:在 submitter 中把 token 作为 env var 透传(不写入日志明文)。