334 lines
16 KiB
Markdown
334 lines
16 KiB
Markdown
# 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/<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:
|
||
|
||
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="<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 status`
|
||
- `ray job list`
|
||
- `ray list nodes`
|
||
- `ray 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.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=<head>: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/<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 验收,必须)
|
||
|
||
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=<code_path>:$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/<id>/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/<submission_id>/` 不复用、不覆盖
|
||
- [ ] 数据/模型持久化:再次提交时不重复下载/生成(有 “skip if exists” 的日志)
|
||
- [ ] checkpoint 策略有效:默认 `save_freq=10`,不会每 step 保存爆盘
|
||
- [ ] debug bundle 落盘:`${SHARED_ROOT}/jobs/<id>/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 透传(不写入日志明文)。
|