# MVP v1.1 工程化重构方案：Ray Python SDK 提交层（YAML Config + YAML JobSpec）

本文档把 v1.1 的“代码工程化”目标落到一个明确的设计：**保留现有 scripts**（Ray 集群构建、数据准备、模型准备、代码快照），将“任务提交机制”重构为 **Ray Python SDK**（`ray.job_submission.JobSubmissionClient`）驱动的 Python 工具层。

> 约束（已确认）
> 1) 基础配置用 YAML，JobSpec 也用 YAML。  
> 2) 工具必须在 **head 容器**执行（从 head 发起提交，满足“在 head 提交”的要求）。  
> 3) 训练参数组织保持与现在一致：仍然使用 **Hydra overrides** 方式构造 entrypoint。  
> 4) 不使用 `requests` 直连 HTTP API（只用 Ray SDK）。  

---

## 1. 当前 Ray SDK 能力验证（关键前提）

在 head 容器（`mvp11-ray-head`）中验证：

- Ray 版本：`2.51.1`
- `JobSubmissionClient.submit_job` 支持以下关键字段：
  - `submission_id`
  - `runtime_env`
  - `entrypoint_num_cpus`
  - `entrypoint_num_gpus`
  - `entrypoint_resources`（用于强制 driver 落 worker）

因此 v1.1 可以“纯 SDK”完成提交，不需要 `requests` fallback。

---

## 2. 系统分层（不动 scripts，只重构提交层）

### 2.1 scripts（保留）

`src/mvp/v1.1/scripts/` 继续负责：

- 容器生命周期：`01_up.sh` / `02_down.sh`
- Ray 启动：`20_start_head.sh` / `21_start_workers.sh`
- 数据/模型准备：`30_prepare_data_and_model.sh`
- 代码快照：`31_snapshot_verl_code.sh`（生成 `${SHARED_ROOT}/common/code/verl/<code_id>/`）

scripts 可以新增一个“薄封装”脚本，负责 `docker exec` 进 head 容器并运行 Python 提交器，但 scripts 不再拼 `ray job submit ...` CLI 字符串。

### 2.2 Python 工具层（新增）

在 `src/mvp/v1.1/py/` 新增提交工具层：

- 读取 Ray 基础配置（YAML）
- 读取训练 JobSpec（YAML）
- 用 Ray Python SDK 提交/查询/停止/拉日志
- 将 job 级别产物落盘到：`${SHARED_ROOT}/jobs/<submission_id>/...`

---

## 3. 输入定义：两份 YAML

### 3.1 Ray 基础配置（RayConfig YAML）

这份配置是“稳定可复用”的，描述 cluster 与 driver placement 等通用信息。

字段建议：

- `address`: `http://127.0.0.1:8265`（从 head 容器内部视角）
- `shared_root`: `/private`
- `entrypoint_num_cpus`: `1`
- `entrypoint_resources`: `{"worker_node": 1}`（强制 driver 使用 worker 才有的资源）
- `runtime_env.env_vars`: HF cache / endpoint 等通用环境变量
- `user_code_path`: `${shared_root}/user/code`（可选，默认值也可）

### 3.2 训练 JobSpec（JobSpec YAML）

这份配置是“一次训练”语义，描述 workload + 训练参数 + code_path 多版本等。

字段建议：

- `workload`: `ppo|grpo|sft`
- `submission_id`: 可选（不填则生成；但最终必须显式传给 SDK）
- `code_path`: `${shared_root}/common/code/verl/<code_id>`（多版本关键字段）
- `model_id`
- 数据路径：`train_file` / `val_file`（按 workload）
- 训练参数：`nnodes` / `n_gpus_per_node` / `total_training_steps` / `save_freq` / `test_freq`

注意（SFT 的 driver 设备选择）：

- Ray job 的 entrypoint（driver）默认不分配 GPU（我们通常不设置 `entrypoint_num_gpus`）。
- `sft_trainer_ray.py` 的 driver 会用 `trainer.device` 做张量统计；若设置为 `cuda` 且 driver 无 GPU，会报：
  - `RuntimeError: No CUDA GPUs are available`
- 因此 v1.1 的 SFT JobSpec 默认应设置：`trainer.device=cpu`（训练 workers 仍会占用 GPU）。

---

## 4. Python 提交器的职责（tool class）

建议实现 `RayJobTool`（或类似命名），能力：

### 4.1 submit（核心）

输入：`RayConfig + JobSpec`  
输出：`submission_id`

实现要点：

- `client = JobSubmissionClient(address)`
- 生成/确定 `submission_id`
- `runtime_env` 合并逻辑：
  - 合并 config 与 jobspec 的 `env_vars`
  - 强制注入多版本：
    - `PYTHONPATH = "<code_path>:<user_code_path>:$PYTHONPATH"`
- 构造 entrypoint（保持 hydra overrides 风格）：
  - PPO/GRPO：`python3 -m verl.trainer.main_ppo ...`
  - SFT：`python3 -m verl.trainer.sft_trainer_ray ...`
- 强制 driver 落 worker：
  - `entrypoint_resources=config.entrypoint_resources`
  - `entrypoint_num_cpus=config.entrypoint_num_cpus`
- 落盘产物：
  - `${shared_root}/jobs/<id>/config/{ray_config.yaml,jobspec.yaml,submit_payload.json}`
  - `${shared_root}/jobs/<id>/logs/submit.out`
  - `${shared_root}/jobs/<id>/debug/{ray_status_pre,ray_job_list_post}.txt`（可用 SDK 或 `ray status` 采集）

### 4.2 status / stop / logs / list

- `status(submission_id)`
- `stop(submission_id)`
- `logs(submission_id)`（可支持 tail）
- `list()`

---

## 5. `run.py` 入口（必须在 head 容器执行）

建议入口：

- `python3 /workspace/mvp/v1.1/py/run.py --config <ray_config.yaml> --jobspec <jobspec.yaml> --action submit`
- `--action` 支持：`submit|status|stop|logs|list`

host 侧执行方式（由 scripts 薄封装）：

- `docker exec mvp11-ray-head python3 /workspace/mvp/v1.1/py/run.py ...`

---

## 6. 验收口径（工程化部分）

1) **SDK 提交**：不使用 `ray job submit` CLI，改用 `JobSubmissionClient.submit_job`。  
2) **driver 仍强制在 worker**：SDK 提交时 `entrypoint_resources={"worker_node":1}` 生效。  
3) **多版本共存验证**：
   - 通过 `31_snapshot_verl_code.sh` 生成 `codeA/codeB` 两份 code_path
   - 通过两份 JobSpec 分别指向不同 `code_path`
   - 在 job logs 中看到不同的 marker（例如 `mvp_marker.MARKER`）

