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

16 KiB
Raw Blame History

MVP v1.1 行动文档(实施方案 / 部署测试 / 验收口径)

本文档面向“把 v1 跑通的实验脚本,升级为可长期回归的 v1.1 最小系统”,并给出开发改造 → 部署测试 → 验收的可复现流程。

v1.1 的核心约束(来自讨论结论)

  • 仍然必须通过 head 节点执行 ray job submit 提交任务。
  • 训练/driver 必须落在 workerhead 不跑训练)。
  • 多版本 verl 共存:同一镜像不变,必须通过 Ray Job runtime_env.env_vars 注入 PYTHONPATH 让 job 粒度选择代码版本。
  • 存储只考虑 NFSdev 环境我们自己 mount生产环境容器内统一看到 /private/

1. 目标与非目标

1.1 目标v1.1 必须做到)

  1. 可回归:同一环境连续跑多次 PPO 回归,不互相覆盖,输出按 submission id 归档。
  2. 可扩展:新增并验收通过 2 个 workloadGRPO + 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:无 GPUray start --head --num-cpus=0 --num-gpus=0(控制面 only
  • mvp-ray-worker-04 GPU
  • mvp-ray-worker-14 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 listdriver_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
  • workloadppo / grpo / sftv1.1 必须 ppo + grpo + sft
  • shared_root:默认 /private(容器内路径)
  • code_pathverl 代码快照目录(用于多版本共存)
  • reward_fn_path(可选):指向 ${shared_root}/user/code/... 下的 Python 文件或模块入口
  • model / dataset:必须指向共享存储的持久化路径(避免每次下载/生成)
  • rayaddress=http://127.0.0.1:8265(从 head 容器内部视角)
  • resources
    • entrypoint_resources={"worker_node":1}
    • entrypoint_num_cpus=1
  • trainer_overrides训练参数覆盖v1.1 默认 total_epochs=1save_freq=10
  • env_vars:会被透传到 runtime_env.env_vars(必须包含 PYTHONPATH 注入)

交付物v1.1 SDK 方式):

  • src/mvp/v1.1/py/configs/dev.yamlRay 基础配置示例)
  • 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.pyMARKER=<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=1save_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.pyRay 版本)

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_ADDRESSRay 版本差异风险v1.1 需要提供一个 launcher 兜底:

  • 由 launcher 先显式 ray.init(address="auto"),再调用 SFT trainer 逻辑

3.6.3 SFT 数据格式parquet schema

sft_trainer_ray 默认使用 MultiTurnSFTDatasetparquet 中至少需要:

  • messageslist[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=cpudriver 只做 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. headray start --head --num-cpus=0 --num-gpus=0 ...
  3. workersray 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 listdriver 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=2trainer.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 不在 headray job listdriver_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 透传(不写入日志明文)。