# MVP V1（Ray + VERL PPO）实验脚本

本目录用于在“宿主机 + Docker 容器”环境下，**用宿主机脚本（`docker exec`）**协调启动 Ray 集群，并通过 **`ray job submit`（在 head 提交）**跑通一次 `verl` 的 PPO 训练闭环（`total_epochs=1`），且数据/模型/日志/ckpt 都持久化到宿主机目录。

## 1. 运行环境与拓扑

### 1.1 依赖

- 宿主机：Linux
- 必需工具：`docker`、`docker compose`（Compose v2 插件）、`git`
- GPU：至少 8 张可用 GPU（索引 `0-7`），Docker 的 NVIDIA runtime 可用

### 1.2 集群拓扑（3 个容器）

- `mvp-ray-head`（Ray Head）
  - **不挂 GPU**（容器内 `nvidia-smi` 不可用）
  - `ray start --head --num-cpus=0 --num-gpus=0`：head 只做控制面，不参与计算调度
  - 暴露 dashboard：宿主机端口 `8265`
- `mvp-ray-worker-0`（Ray Worker）
  - 4 GPU：`0,1,2,3`
  - `ray start ... --resources='{"worker_node": 100}'`
- `mvp-ray-worker-1`（Ray Worker）
  - 4 GPU：`4,5,6,7`
  - `ray start ... --resources='{"worker_node": 100}'`

**关键点：driver 不在 head**

- 作业通过 head 提交：`ray job submit ...`
- 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 entrypoint/driver 只能调度到 worker（head 没有该资源）

## 2. 持久化目录（宿主机 <-> 容器）

在宿主机项目根目录（运行脚本时的 `${PWD}`）下使用 `./shared` 做持久化根目录，并 bind mount 到容器内 `/mnt/shared`：

- 宿主机：`./shared`
- 容器：`/mnt/shared`

主要内容：

- 数据集：`/mnt/shared/datasets/gsm8k/`
- HF 缓存：`/mnt/shared/hf/`（脚本会设置 `HF_HOME`，并尽量幂等跳过重复下载）
- 每个 Ray Job 的输出（按 submission id 分目录）：
  - `/mnt/shared/jobs/<submission_id>/logs/`
  - `/mnt/shared/jobs/<submission_id>/checkpoints/`

## 3. 整体流程（代码逻辑）

脚本都在 `src/mvp/v1/scripts/`，整体顺序如下：

1) `00_prereq_check.sh`
   - 检查 `docker/docker compose/git`
2) `05_ensure_verl_repo.sh`
   - 若项目根目录下没有 `./verl`，自动 `git clone https://github.com/volcengine/verl.git`
3) `01_up.sh`
   - 创建持久化目录（`./shared/...`）
   - `docker compose up -d` 启动 3 个容器
4) `10_install_verl_editable.sh`
   - 在 3 个容器内执行 `pip install -e /workspace/verl`（确保 `python -m verl...` 可用且代码与 `./verl` 同步）
5) `20_start_head.sh`
   - 在 `mvp-ray-head` 内启动 Ray head（CPU=0、GPU=0）
6) `21_start_workers.sh`
   - 在两个 worker 内启动 Ray worker 加入集群
   - 同时给 worker 打 `worker_node` 自定义资源标签
7) `30_prepare_data_and_model.sh`
   - 数据集：若 `train.parquet/test.parquet` 已存在则跳过，否则生成
   - 模型：使用 HF cache（`HF_HOME=/mnt/shared/hf`），存在则跳过，不存在才下载
8) `40_submit_ppo_epoch1.sh`
   - 在 head 容器里执行 `ray job submit`
   - 显式指定 `--submission-id=$SUBMISSION_ID`
   - 通过 `--entrypoint-resources='{"worker_node": 1}'` 强制 driver 在 worker
   - 训练参数：
     - `trainer.total_epochs=1`
     - `trainer.total_training_steps=29`（GSM8K 该配置下对应 29 steps）
     - `trainer.save_freq=10`（每 10 step 保存一次 checkpoint，避免磁盘爆炸）
     - `trainer.default_local_dir=/mnt/shared/jobs/$SUBMISSION_ID/checkpoints`
     - `hydra.run.dir=/mnt/shared/jobs/$SUBMISSION_ID/logs/hydra`
9) `50_status.sh`
   - 打印 `ray status` / `ray job list` / `ray job status` / `ray job logs | tail`

## 4. 运行方法

### 4.1 一键执行

在项目根目录执行：

- `./src/mvp/v1/scripts/run_all.sh`

### 4.2 分步执行（推荐）

按顺序执行：

- `./src/mvp/v1/scripts/01_up.sh`
- `./src/mvp/v1/scripts/10_install_verl_editable.sh`
- `./src/mvp/v1/scripts/20_start_head.sh`
- `./src/mvp/v1/scripts/21_start_workers.sh`
- `./src/mvp/v1/scripts/30_prepare_data_and_model.sh`
- `SUBMISSION_ID=ppo_h20_8g_$(date +%Y%m%d_%H%M%S) ./src/mvp/v1/scripts/40_submit_ppo_epoch1.sh`
- `./src/mvp/v1/scripts/50_status.sh`

### 4.3 查看与停止

- Dashboard：`http://<宿主机IP>:8265`
- 列出作业（在 head 容器内）：
  - `docker exec mvp-ray-head bash -lc "ray job list --address=http://127.0.0.1:8265"`
- 停止某个 submission id：
  - `docker exec mvp-ray-head bash -lc "ray job stop --address=http://127.0.0.1:8265 <submission_id>"`

### 4.4 清理

- 停止并删除容器：`./src/mvp/v1/scripts/02_down.sh`
- 清理输出（谨慎，数据量可能很大）：删除 `./shared/jobs/<submission_id>/`

## 5. 常见坑

- **不传 `--submission-id` 会导致“输出目录难以等于 submission id”**：因为 hydra/ckpt 目录需要在提交前确定。当前脚本会显式传 `--submission-id=$SUBMISSION_ID`，并使用同名目录。
- **checkpoint 太大**：PPO 的 checkpoint 非常占空间。当前脚本默认 `save_freq=10`，如仍过大，可调大 `save_freq` 或减少保存内容/频率。

更多分步操作与验收标准见：`specs/mvp/v1_action.md`
