# MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径) 本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出**开发改造 → 部署测试 → 验收**的可复现流程。 > v1.1 的核心约束(来自讨论结论) > - 仍然必须通过 **head 节点执行 `ray job submit`** 提交任务。 > - 训练/driver **必须落在 worker**(head 不跑训练)。 > - 多版本 `verl` 共存:同一镜像不变,必须通过 **Ray Job `runtime_env.env_vars` 注入 `PYTHONPATH`** 让 job 粒度选择代码版本。 > - 存储只考虑 NFS:dev 环境我们自己 mount;生产环境容器内统一看到 `/private/`。 --- ## 1. 目标与非目标 ### 1.1 目标(v1.1 必须做到) 1) **可回归**:同一环境连续跑多次 PPO 回归,不互相覆盖,输出按 submission id 归档。 2) **可扩展**:新增并验收通过 2 个 workload(**GRPO + SFT**)并跑通闭环。 3) **可排障**:每个 job 目录包含完整的提交快照、关键 env、Ray 状态快照与日志入口。 4) **可多版本共存**:同一 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 GPU - `mvp-ray-worker-1`:4 GPU ### 2.2 “head 不跑训练”的硬约束实现(必须) 1) **head CPU=0**:从资源层面阻断默认 task/driver 落到 head。 2) **worker 自定义资源标签**:worker 启动时带 `--resources='{"worker_node": 100}'`。 3) **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//` 建议的共享目录结构(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: 1) Ray 基础配置(YAML):address / entrypoint resources / runtime_env 等 2) 训练 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=":$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//` - 该目录里会包含一个 `mvp_marker.py`(`MARKER=`) - 提交 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//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 status` - `ray job list` - `ray list nodes` - `ray list actors` 建议落盘到: - `${SHARED_ROOT}/jobs//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": "" }, { "role": "assistant", "content": "" }]` > 注意: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.sh` - `src/mvp/v1.1/scripts/30_prepare_data_and_model.sh`(包含 PPO 数据 + SFT 数据) - `src/mvp/v1.1/scripts/40_submit_ppo_epoch1.sh` - `src/mvp/v1.1/scripts/41_submit_grpo_epoch1.sh` - `src/mvp/v1.1/scripts/42_submit_sft_minimal.sh` - `src/mvp/v1.1/scripts/50_status.sh` - `src/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: 1) 停止并删除 v1 容器(推荐用 v1 的 down 脚本) 2) 确认 `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 环境准备(一次性 / 幂等) 1) 目录检查(远程机): - `${WORKDIR}/shared/` 存在并具备上述子目录(含 `common/`、`user/`) 2) `verl` 代码目录检查: - `${WORKDIR}/verl` 不存在则执行 `git clone https://github.com/volcengine/verl.git` 3) GPU 可用性检查: - 设备存在(例如 0-7 可见),并按 worker 容器分配(每个 worker 4 GPU) 4) 模型与数据持久化路径: - 模型与数据必须落在 `${SHARED_ROOT}` 下;若已存在则跳过下载/生成 - SFT parquet 同样必须落在 `${SHARED_ROOT}` 下;若已存在则跳过生成 ### 4.2 启动 Ray 集群(每次测试) 1) `docker compose up -d` 2) head:`ray start --head --num-cpus=0 --num-gpus=0 ...` 3) workers:`ray start --address=:6379 --resources='{"worker_node":100}' ...` 4) 验证:`ray status` 显示 1 head + 2 worker,且 head `CPU:0 GPU:0` ### 4.3 提交 PPO 回归(必须跑 2 次) 1) 生成 JobSpec(可用模板 + 覆盖项) 2) 在 head 容器内执行 submitter(或直接 `ray job submit`) 3) 验证要点: - `ray job list`:driver node 是 worker - `${SHARED_ROOT}/jobs//` 下存在 `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 验收,必须) 1) 确认 `${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` 已存在(由 v1.1 prepare 生成)。 2) 通过 head 容器执行 `ray job submit` 提交 `python -m verl.trainer.sft_trainer_ray`。 3) 关键约束: - `runtime_env.env_vars.RAY_ADDRESS=auto`(连接已有集群) - `--entrypoint-resources='{"worker_node": 1}'`(driver 落 worker) - `PYTHONPATH=:$PYTHONPATH`(多版本 verl) 4) 最小化训练配置建议(避免 OOM/耗时过长): - `trainer.total_epochs=1` - `trainer.total_training_steps=10~30` - `trainer.save_freq=10` - `trainer.nnodes=2`、`trainer.n_gpus_per_node=4`(用满 8 卡做一次最小分布式验证) - `data.train_files=${SHARED_ROOT}/datasets/gsm8k_sft/train.parquet` - `trainer.default_local_dir=${SHARED_ROOT}/jobs//checkpoints` ### 4.6 工程化验证:JobSpec + 多版本共存(v1.1 必须) 1) 生成两个 code snapshot(不同 `CODE_ID`): - `CODE_ID=codeA ./scripts/31_snapshot_verl_code.sh` - `CODE_ID=codeB ./scripts/31_snapshot_verl_code.sh` 2) 分别修改/复制 JobSpec 模板,使 `code_path` 指向不同 snapshot: - `${SHARED_ROOT}/common/code/verl/codeA` - `${SHARED_ROOT}/common/code/verl/codeB` 3) 用 JobSpec 提交(必须从 head): - `./scripts/43_submit_jobspec.sh /workspace/mvp/v1.1/templates/ppo.json`(示例) 4) 在 Ray job logs 中验证: - `MVP_PRECHECK_MARKER` 打印为对应的 `codeA`/`codeB` - `MVP_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//` 不复用、不覆盖 - [ ] 数据/模型持久化:再次提交时不重复下载/生成(有 “skip if exists” 的日志) - [ ] checkpoint 策略有效:默认 `save_freq=10`,不会每 step 保存爆盘 - [ ] debug bundle 落盘:`${SHARED_ROOT}/jobs//debug/` 至少包含 2 类 Ray 状态快照 - [ ] 多版本共存验证通过:日志中能确认 `verl` import 来源来自 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 透传(不写入日志明文)。