argus-cluster/specs/mvp/v1.1/v1.1_action.md
2025-12-23 14:22:15 +08:00

334 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径)
本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出**开发改造 → 部署测试 → 验收**的可复现流程。
> v1.1 的核心约束(来自讨论结论)
> - 仍然必须通过 **head 节点执行 `ray job submit`** 提交任务。
> - 训练/driver **必须落在 worker**head 不跑训练)。
> - 多版本 `verl` 共存:同一镜像不变,必须通过 **Ray Job `runtime_env.env_vars` 注入 `PYTHONPATH`** 让 job 粒度选择代码版本。
> - 存储只考虑 NFSdev 环境我们自己 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 基础配置YAMLaddress / entrypoint resources / runtime_env 等
2) 训练 JobSpecYAMLworkload 语义与训练参数(仍由 Hydra overrides 组织)
训练 JobSpecYAML至少包含
- `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 扩展GRPOv1.1 新增闭环)
优先用与 PPO 相同入口 `python -m verl.trainer.main_ppo`,仅通过配置切换算法:
- `algorithm.adv_estimator=grpo`
- 其余保持最小可跑:`total_epochs=1``save_freq=10`
### 3.6 Workload 扩展SFT on Rayv1.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+workersv1.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 的 parquetschema 不同)。
#### 3.6.4 重要细节SFT Ray Driver 不应依赖 GPU
`ray job submit` 模式下,我们的 entrypointdriver默认 **不会分配 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 DoDGRPO + 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 透传(不写入日志明文)。