# MVP v2.0 开发计划（服务化入口 + 队列调度 + Ray Jobs SDK）

目标：在 v1.1（脚本 + Ray Jobs SDK）已验收通过的基础上，交付一个**可独立运行的最小“服务层”**：
- 用户通过 **HTTP API** 提交训练任务（PPO/GRPO/SFT）。
- 服务层分配一个**人类易读的任务 ID**（`task_id`），并把任务放入队列。
- 后台调度器在资源满足时再向 Ray 集群提交 Ray Job，并持续追踪 Ray Job 状态。
- 针对 `verl` 的 **fail-fast 资源预检查**（资源不足直接 `ValueError` 失败）做“服务级重试/排队”，避免用户反复手工提交。

> 约束继承 v1.1：head 不跑训练；driver 必须落到 worker；共享存储只考虑 NFS（容器内 `/private`）。

---

## 1. 背景：为什么 v2.0 需要“服务层调度”

在 v1.1 中我们通过 Ray Job 提交 `verl` 训练任务。`verl` PPO/GRPO 在初始化 worker 时会创建资源池，并做一次 fail-fast 的资源检查：
- 触发点：`ResourcePoolManager.create_resource_pool()` 末尾调用 `_check_resource_available()`
- `_check_resource_available()` 使用 `ray._private.state.available_resources_per_node()` 统计“可用 GPU/NPU”，如果不足则直接抛异常：
  - `ValueError: Total available GPUs 0 is less than total desired GPUs 8`

这是一种合理的选择（避免 Ray 层面无限 pending/卡死），但会带来一个平台侧问题：
- 当集群暂时没有足够资源时，用户提交会“立刻失败”，需要手动重试。

因此 v2.0 的服务层要提供：
- **队列 + gang 约束**：资源不满足则任务在服务层 pending（不提交到 Ray）。
- **状态追踪**：一旦提交到 Ray，持续获取 Ray Job 状态并回传给用户。
- **资源不足的“自动重试”**：即使发生 race（提交时资源够、启动时被抢走），也能识别该类失败并延迟重试。

---

## 2. v2.0 交付范围（Scope）

### 2.1 必做（MVP v2.0）

1) **HTTP API**（内部 token）：
   - 提交任务、查询任务、取消任务、拉取日志（最小可用）。
2) **任务队列与调度器**：
   - FIFO（先到先服务），无配额/公平性（留给 v3+）。
   - gang：按 `nnodes` + `n_gpus_per_node` 的固定资源需求“全有才提交”。
3) **Ray Jobs SDK 集成**（不使用 `requests` 自己拼 HTTP）：
   - 通过 `ray.job_submission.JobSubmissionClient` submit/status/stop/logs。
4) **可观测/可排障最小集**：
   - 每个 task/attempt 落盘配置、提交载荷、Ray 返回的 `submission_id`、关键日志。
5) **失败策略**：
   - 识别 “资源不足 fail-fast” 类失败 → 转为 `PENDING_RESOURCES` 并延迟重试。
   - 其他失败保持 `FAILED`（不自动重试，避免掩盖错误）。

### 2.2 不做（v2.0 不实现）

- 多租户/配额/优先级/公平性调度（v3）。
- Pipeline（多 job 串联）（v3+）。
- 完整 UI（v3+，v2.0 可只提供 OpenAPI/Swagger）。
- K8s 编排（明确不做，仍是 Native Ray）。

---

## 2.3 工程原则（开闭原则 / 复用 v1.1）

v2.0 研发遵循开闭原则（Open/Closed Principle）：
- **对扩展开放**：新增“服务层（API + scheduler + SQLite）”能力以支持排队、重试、状态聚合。
- **对修改关闭**：尽量不改动 v1.1 已经稳定可用的 Ray Jobs SDK 提交链路代码。

落地方式：
- 将 `src/mvp/v1.1/py/mvp_v11/` 作为“成熟可用提交层”，原样拷贝到 `src/mvp/v2.0/py/mvp_v11/` 供 v2.0 复用。
- v2.0 的新增功能全部在新模块实现（例如 `src/mvp/v2.0/py/mvp_v2/`），通过组合/封装来调用 `mvp_v11`，避免在旧代码中掺杂平台逻辑。

---

## 3. 总体架构（v2.0）

### 3.1 组件

- **mvp-api**（HTTP Server）
  - 接收 JobSpec（结构化字段保持与 v1.1 一致的语义）
  - 生成 `task_id` 并写入持久化
  - 提供 query/cancel/logs

- **mvp-scheduler**（后台调度器，可与 api 同进程也可拆进程）
  - 轮询队列：对 `PENDING_RESOURCES` 的任务做资源判断
  - 资源满足 → 调用 Ray Jobs SDK 提交 → 记录 `ray_submission_id`
  - 对 `SUBMITTED/RUNNING` 的任务持续同步 Ray Job 状态
  - 如果 Ray Job 失败且命中资源不足模式 → 延迟重试

> 部署建议：v2.0 先在 **head 容器**内运行该服务（dev/prod 行为一致；生产环境只能 ssh 进入容器纳管）。

### 3.4 dev 环境目录约定（示例）

以当前远程开发机为例（`argus@h1`）：
- 宿主机目录：`/home2/argus/infra/mvp/v2/`
- 容器内挂载：`/workspace/mvp/v2/`
- 共享 NFS：容器内统一为 `/private/`（与 v1.1 保持一致）

> 注意：服务脚本（`v2/scripts/*.sh`）应在**宿主机**执行，通过 `docker exec` 控制 head 容器；训练 driver 仍通过 Ray entrypoint_resources 强制落到 worker。

### 3.2 与 Ray/容器的关系

- 服务进程运行在 head（或等价能访问 head 的 Job server 地址）。
- 提交时仍使用 v1.1 的强约束：
  - head：`--num-cpus=0 --num-gpus=0`
  - worker：`--resources='{\"worker_node\": 100}'`
  - job entrypoint：`entrypoint_resources={\"worker_node\": 1}` 强制 driver 落 worker

---

## 3.3 配置约定（复用 v1.1 dev.yaml 并扩展）

v2.0 的服务层（API + scheduler）建议复用 v1.1 已存在的 RayConfig 文件：
- `src/mvp/v1.1/py/configs/dev.yaml`

原因：
- 其中已包含 v1.1 运行所需的 Ray 基础配置（Ray Job server address、entrypoint_resources、runtime_env 等），v2.0 也需要同样的信息来提交 Ray Jobs。

扩展方式：
- 在该 YAML 中新增一个顶层 `v2:` section，存放 v2 服务专属配置（API 监听、SQLite 路径、scheduler 间隔等）。
- v1.1 submitter 只读取 `address/shared_root/entrypoint_* /runtime_env/user_code_path`，会忽略 `v2:` 之类的额外字段；因此不会破坏 v1.1。

最小新增项建议（示例）：
- `v2.api.host` / `v2.api.port`
- `v2.auth.token_env`（内部 token 环境变量名）
- `v2.sqlite.db_path`（建议 `/private/common/db/mvp_v2.sqlite3`）
- `v2.scheduler.tick_s` / `v2.scheduler.retry_interval_s` / `v2.scheduler.max_running_tasks`

---

## 4. 核心数据模型（Task / Attempt）

### 4.1 Task（用户视角的任务）

- `task_id`：**人类易读**且唯一，例如：
  - `mvp2-ppo-20251223-143201-7f3a`
- `workload`：`ppo|grpo|sft`
- `jobspec`：提交参数（**保持 v1.1 的 jobspec YAML 字段与语义**；服务端解析 YAML 后入库）
- `state`：见第 5 节状态机
- `created_at` / `updated_at`
- `latest_attempt`：指向当前 attempt
- `attempts[]`：历史尝试列表
- `error_summary`：面向用户的简短错误（最后一次失败原因）

### 4.2 Attempt（一次真实的 Ray Job 提交）

- `attempt_no`：从 1 开始递增
- `ray_submission_id`：建议派生自 task_id：
  - `ray_submission_id = <task_id>--a01`
  - 好处：Ray 侧输出目录天然可读、可追溯
- `status`：Ray Job 状态（PENDING/RUNNING/SUCCEEDED/FAILED/STOPPED）
- `start_time` / `end_time`
- `exit_code`（如可取）
- `failure_kind`（枚举）：
  - `INSUFFICIENT_RESOURCES`（匹配 “Total available GPUs … less than total desired …”）
  - `USER_ERROR`（配置/数据路径错误等）
  - `RUNTIME_ERROR`（代码异常）
  - `UNKNOWN`

---

## 5. 状态机（服务侧）

建议最小状态集：

- `QUEUED`：已入队，尚未进行资源判断
- `PENDING_RESOURCES`：资源不足，等待（服务侧 pending，不提交 Ray）
- `SUBMITTING`：正在向 Ray 提交 attempt
- `SUBMITTED`：Ray 已接受 submission（拿到 `ray_submission_id`）
- `RUNNING`：Ray Job RUNNING
- `SUCCEEDED`：任务成功（终态）
- `FAILED`：任务失败（终态，除非命中“资源不足重试策略”）
- `CANCELED`：用户取消（终态）

关键转换：
- `QUEUED -> PENDING_RESOURCES`：资源不足
- `QUEUED/PENDING_RESOURCES -> SUBMITTING`：资源满足
- `SUBMITTING -> SUBMITTED`：提交成功
- `SUBMITTED -> RUNNING`：Ray 状态推进
- `SUBMITTED/RUNNING -> SUCCEEDED|FAILED`：Ray 终态
- `FAILED (INSUFFICIENT_RESOURCES) -> PENDING_RESOURCES`：进入延迟重试（attempt_no+1）

---

## 6. 调度策略（v2.0）

### 6.1 资源计算（对齐 verl 的“可用资源”口径）

由于 verl 使用 `ray._private.state.available_resources_per_node()` 做“可用资源”统计，
v2.0 的 scheduler 应该尽量使用相同口径，避免：
- 我们认为够了 → 实际 verl 认为不够（仍 fail-fast）
- 我们认为不够 → 实际够了（浪费）

策略（建议）：
1) scheduler 周期性获取 per-node 可用 GPU
2) 计算 total_available_gpus = sum(node_gpu_available)
3) 任务需求 total_required_gpus = nnodes * n_gpus_per_node
4) 如果 `total_available_gpus < total_required_gpus` → `PENDING_RESOURCES`

注意：v2.0 先只做总量判断；节点级分配（保证每个 node 恰好 n_gpus_per_node）可作为 v2.1+（资源池/标签/节点纳管）增强点。

### 6.2 排队与并发

- 默认 FIFO。
- 并发度：允许同时跑多个任务，但必须保证资源足够。
  - 简化实现：如果任务默认都吃满 8 卡，则 scheduler 实际上一次只能跑一个。
  - 若未来支持小任务（1*1、1*4），可以自然并发。

### 6.3 重试策略（资源不足）

当出现下面模式时判定为 `INSUFFICIENT_RESOURCES`：
- Ray Job `status=FAILED`
- `JobDetails.message` 或 `job logs` 中匹配：
  - `Total available GPUs` 且 `less than total desired`

处理：
- 将 task 置为 `PENDING_RESOURCES`
- `next_run_at = now + 60s`（固定间隔；v2.1 可改指数退避）
- attempt_no++ 后重提（新 submission id）

---

## 7. SQLite 持久化（队列/状态/attempt）

v2.0 引入一个**最小但可恢复的持久化层**：使用 SQLite 保存任务队列与状态，确保：
- api/scheduler 进程重启后，队列不丢；
- task/attempt 历史可追溯；
- 能实现“服务侧 pending + 延迟重试”的确定性行为。

### 7.1 存放位置

建议路径（容器内）：
- `DB_PATH=/private/common/db/mvp_v2.sqlite3`

说明：
- v2.0 默认单实例服务（单 writer），SQLite 足够。
- 生产环境若 NFS 上的 SQLite 有锁/性能风险，v2.1+ 再演进到 Postgres/Redis；v2.0 先以“可回放/可恢复”为第一目标。

### 7.2 表设计（建议最小集合）

- `tasks`
  - `task_id` (PK)
  - `workload`
  - `state`（服务侧状态机）
  - `jobspec_yaml`（原始 YAML 文本，原样落盘便于审计/复现）
  - `created_at`, `updated_at`
  - `next_run_at`（用于 `PENDING_RESOURCES` 的延迟重试）
  - `error_summary`
  - `latest_attempt_no`

- `attempts`
  - `task_id` (FK)
  - `attempt_no`
  - `ray_submission_id`
  - `ray_status`
  - `failure_kind`
  - `message`（截断后的关键信息）
  - `start_time`, `end_time`

- `events`（可选，但非常利于排障）
  - `id` (PK)
  - `task_id`
  - `ts`
  - `event_type`（STATE_TRANSITION / SUBMIT / RAY_STATUS_SYNC / RETRY_SCHEDULED 等）
  - `payload_json`

### 7.3 调度循环（与 SQLite 的交互）

scheduler 每个 tick 做三件事：
1) **挑选可运行任务**（FIFO + next_run_at）：
   - `state IN ('QUEUED','PENDING_RESOURCES') AND next_run_at <= now`
2) **资源判断**（对齐 verl 的可用资源口径）：
   - 不满足：更新 `state='PENDING_RESOURCES'`，并写入 `next_run_at=now+60s`
3) **提交 Ray Job 并追踪**：
   - 提交成功：写入 `attempts` 并更新 `tasks.latest_attempt_no`、`state='SUBMITTED'`
   - 周期性同步 Ray 状态：`SUBMITTED/RUNNING -> SUCCEEDED/FAILED`
   - 若失败命中资源不足模式：`FAILED -> PENDING_RESOURCES` + 计划下次重试

---

## 8. 接口与验收（DoD）

### 8.1 API 能力（最小集合）

详见 `specs/mvp/v2.0/v2_api.md`。

### 8.2 验收口径（DoD）

1) API 提交 PPO/GRPO/SFT，返回 `task_id`，并在 NFS 上创建任务目录。
2) 当集群忙（GPU 不足）时：
   - task 状态为 `PENDING_RESOURCES`（不是 FAILED）
   - 一旦资源释放，任务自动变为 `SUBMITTED/RUNNING`
3) 当 race 导致触发 verl fail-fast：
   - attempt 标记为 `INSUFFICIENT_RESOURCES`
   - task 回到 `PENDING_RESOURCES`，并在 60s 后自动重试
4) 通过 API 查询 task 能看到：
   - 当前 state
   - 最新 attempt 的 `ray_submission_id`
   - attempt 历史（至少包含开始/结束/失败原因）
5) Cancel 能停止正在运行的 Ray Job（调用 Ray Jobs SDK stop）

---

## 9. v2.0 交付物建议（目录）

`specs/mvp/v2.0/`（本目录）：
- `v2_plan.md`：总体设计与开发计划（本文件）
- `v2_api.md`：API 详细定义（请求/响应/字段/错误码）

代码建议位置（后续实现时）：
- `src/mvp/v2.0/`
  - `py/`：API server + scheduler
  - `scripts/`：启动/停止/查看状态（仍沿用 v1.1 的 compose/cluster 逻辑）
