# MVP v2.5 详细设计方案（User Management + Stateless Ray Node Pool）

本文目标：把 `mvp_roadmap_v2.md` 中 v2.5 的思路落到**可工程化实现**的设计层，包括：
- API Server 内新增 user management；
- Ray node pool 变为无状态（worker 自发现 head、自动加入、watchdog 自愈）；
- 仍保持 v2.0 的“任务管理层”语义：Task/Attempt、队列、资源判断、Ray Job 提交与状态同步；
- 所有共享数据/状态统一落在 GPFS（dev 环境可先用 NFS），容器内路径统一为 `/private/`。

> 术语说明：文中“GPFS”代表生产共享存储；dev 环境可用 NFS，但容器内仍以 `/private/` 访问。

---

## 1. 目标与非目标

### 1.1 v2.5 目标（Must）

1) **User Management（最小多租户）**
- 支持创建/禁用用户；
- 为每个用户签发内部 token（API key），用于认证与隔离；
- 用户隔离（v2.5 先做最小闭环，仅隔离 **jobs 输出** 与 API 可见性）：
  - 用户只能看到/操作自己的 Task；
  - 训练输出（job root、checkpoints、日志归档等）按 user 目录落盘；
  - 训练输入（verl 代码、HF cache、datasets）统一使用 `common/`（v2.5 不支持用户自定义代码/模型/数据集隔离）。

2) **Stateless Ray Worker Node Pool**
- worker 容器启动时无需被平台告知 head 地址；
- worker 通过 GPFS 读取 **Head IP File** 自动连接 Ray head；
- worker 内部 watchdog 监控 head 地址变化，发生变化时自动 `ray stop` + `ray start` 重连；
- worker 尽量不依赖本地持久化状态（宕机/替换后可无感重建）。

3) **保持 v2.0 的 Task 管理行为**
- Task/Attempt 模型不变（或向后兼容扩展）；
- 对齐 verl 的 fail-fast 行为：资源不足时服务侧 pending + 重试；
- Ray Job 提交仍通过 Ray Python SDK（JobSubmissionClient）。

### 1.2 v2.5 非目标（Not Now）

- 完整 WebUI（留到 v3.0）。
- 公平调度/配额/优先级（留到 v3.x）。
- 完整生产级 IAM（留到 v4+），v2.5 仅内部 token。
- K8s 原生编排（本阶段不要求，但设计需能适配“算力平台拉起容器，只能 ssh 进去纳管”的模式）。

---

## 2. 总体架构（对应 roadmap v2.5）

### 2.1 组件划分

**控制面（Control Plane）**
- **API Server**
  - user management
  - task management（队列/调度/重试/状态聚合）
  - Ray Job Tool（Ray Client）
  - VerlTaskSpec（TaskSpec YAML，沿用 v2.0/v2.1 格式）
  - 与 Ray head 在同一台/同一容器是推荐形态（便于访问 dashboard / job server）
- **Ray Head（有状态）**
  - 启动后把 head 地址写入 GPFS 的 Head IP File，用于 worker 服务发现

**数据面（Data Plane）**
- **Ray Workers（无状态节点池）**
  - stateless bootstrap：从 GPFS 读取 head 地址自动加入集群
  - watchdog：持续 watch head 地址文件变化并自愈重连

**共享存储（GPFS）**
- 统一数据路径：数据、模型 cache、代码、任务输出、以及 head 服务发现文件。

### 2.2 v2.5 的控制反转（IoC）

与 v2.0/手工集群的关键差异：
- v2.0：平台脚本/运维显式启动 worker 并指定 `--address=<head>`。
- v2.5：worker 自己从 GPFS 读取 `head_ip_file`，无需平台维持 worker 列表与 SSH 连接池。

---

## 3. GPFS 目录结构（容器内 `/private`）

建议在 v2.5 固化以下目录（与现有 v2.0 兼容扩展）：

```
/private/
  ray/
    discovery/
      <cluster_name>/
        head.json              # Head IP File（服务发现）
        head.json.lock         # 可选：写入锁（v2.5 可先不实现）
  users/
    <user_id>/
      jobs/                    # /private/users/<uid>/jobs/<ray_submission_id>/*
      outputs/                 # 训练输出聚合（按需要）
  common/
    code/                      # 平台/公共代码快照（verl code snapshot 等）
    datasets/                  # 公共数据集
    hf/                        # 公共 HF cache（dev 复用）
    db/                        # sqlite
    logs/                      # API 日志、平台日志
```

说明：
- `common/`：平台默认目录（v2.5 先默认所有用户可写；后续再加 ACL/只读）。
- `users/<user_id>/...`：用户隔离主边界（最小多租户的关键）。

---

## 4. Head IP File（服务发现）设计

### 4.1 文件路径

- `head_ip_file = /private/ray/discovery/<cluster_name>/head.json`
- `<cluster_name>`：由配置指定（例如 `argus-ray`），允许同一 GPFS 上存在多个环境/集群。

### 4.2 文件内容（JSON）

建议采用 JSON（易扩展）：

```json
{
  "cluster_name": "argus-ray",
  "head_ip": "10.0.0.12",
  "gcs_port": 6379,
  "dashboard_port": 8265,
  "job_server_url": "http://10.0.0.12:8265",
  "updated_at": "2025-12-25T17:00:00Z",
  "expires_at": "2025-12-25T17:01:00Z"
}
```

关键点：
- `updated_at`：便于排障与可观测；
- `expires_at`：避免 worker 读取到“陈旧 head 地址”后无限重连；
- `job_server_url`：对外可直接用于 Ray Job Tool 配置（便于无脑接入）。

### 4.3 写入策略（原子更新）

Head 写入时必须保证 worker 读取不会读到半文件：
- 写临时文件 `head.json.tmp`；
- `fsync`（可选）；
- `rename(head.json.tmp -> head.json)`（原子替换）。

### 4.4 心跳与 TTL

Head 进程需周期性刷新 `head.json`：
- 建议 `ttl_s=60`，刷新周期 `refresh_s=10`；
- 若 head 进程异常退出，worker 读取到过期文件可进入“等待模式”而非无限重连。

---

## 5. Stateless Worker Bootstrap + Watchdog

### 5.1 启动序列（worker 容器内）

1) 启动脚本读取 `head.json`：
   - 若文件不存在：sleep + 重试（直到存在）
   - 若存在但 `expires_at` 已过期：sleep + 重试（直到变为新鲜）
2) 解析 `head_ip:gcs_port` 并执行：
   - `ray stop --force || true`
   - `ray start --address=<head_ip>:<gcs_port> --resources='{"worker_node": 100, ...}' ...`
3) 启动 watchdog 进程（同容器）：
   - 轮询/监听 `head.json` 的内容变化
   - 一旦 `head_ip` 或 `gcs_port` 改变，触发 `ray stop` + `ray start` 重连

### 5.2 Watchdog 策略（最小可用）

v2.5 推荐“简单且稳”的实现：
- polling 间隔 `watch_s=5`；
- 对比 `head.json` 的 `updated_at` 或 hash；
- 若发现变更：执行重连；
- 若连续多次重连失败：指数退避（v2.5 可先固定退避，v2.6 再增强）。

### 5.3 资源标签（driver 强制落 worker）

继续沿用 v2.0 的思路：
- worker 启动时 `--resources='{"worker_node": 100}'`
- head 不包含 `worker_node` 资源
- Ray job submit 时设置 entrypoint_resources：`{"worker_node": 1}`

### 5.4 GPU/CPU 的“无状态”约束

- worker 是否有 GPU 由底层算力平台决定（生产上平台会为容器挂载 GPU）；
- worker 启动脚本不应硬编码 GPU 编号，只依赖 `NVIDIA_VISIBLE_DEVICES`/平台注入；
- head 推荐 `--num-gpus=0 --num-cpus=0`，避免训练调度到 head。

---

## 6. User Management 设计（最小多租户）

### 6.1 数据模型（SQLite）

新增两张表（示意）：
- `users`
  - `user_id`（PK）
  - `display_name`
  - `state`（ACTIVE/DISABLED）
  - `created_at`
- `api_tokens`
  - `token_hash`（PK）
  - `user_id`（FK）
  - `created_at`
  - `last_used_at`

并在 `tasks` 表增加：
- `user_id`（FK）

### 6.2 鉴权策略

内部 token 模式：
- `Authorization: Bearer <token>`
- 服务端将 token 映射到 `user_id`
- 后续所有 task 查询/取消/日志默认 scope 到该 `user_id`

管理员能力（v2.5 最小实现）：
- 额外配置一个 admin token（或把特定 user 标记为 admin）
- admin 可 list all users/tasks（用于运维排障）。

### 6.3 用户目录隔离（路径约束）

核心原则（v2.5 版）：
- **输出**：必须落在 `/private/users/<uid>/jobs/...`（服务端统一计算，不允许用户任意指定输出根）
- **输入**：统一使用 `/private/common/...`（v2.5 不支持用户自定义 verl 代码、也不做 hf/datasets 的用户隔离）

服务端处理策略（最小可用）：
- 解析 TaskSpec 后，对输入路径字段做白名单前缀校验（必须是 `/private/common/...`；拒绝 `../` 与越界路径）；
- 输出目录统一由服务端计算：`job_root = /private/users/<uid>/jobs/<ray_submission_id>/`。

---

## 7. TaskSpec（VerlTaskSpec YAML）在 v2.5 的扩展点

v2.5 **不扩展 TaskSpec**：保持与 v2.0/v2.1 的 YAML 结构化字段与语义一致。

v2.5 的“用户语义”仅体现在服务端的补齐/约束：
- user_id 由 token 推导（用户不需要在 YAML 里写 user_id）；
- 服务端派生 `ray_submission_id`（由 task_id/attempt 派生）；
- 服务端统一计算输出目录 `job_root=/private/users/<uid>/jobs/<ray_submission_id>/...`；
- v2.5 不支持用户自定义 verl 代码路径（因此 runtime_env 不需要注入用户 code 目录）。

---

## 8. 迁移与兼容性

v2.5 设计需满足：
- 现有 v2.0 的“手工启动 worker”仍可运行（作为 dev fallback）；
- 在不改镜像的前提下，worker watchdog 可以以“容器启动命令/entrypoint”方式注入（dev 用 scripts；生产由算力平台指定 command）。

---

## 9. 风险与对策（v2.5）

1) **GPFS 上 head.json 一致性/延迟**
- 对策：原子 rename + TTL；watchdog polling。

2) **Ray head 重启后 job server URL 变化**
- 对策：head.json 内写入 `job_server_url`，Ray Job Tool 可读取该文件更新 address（v2.6 可做动态 reload）。

3) **Worker 重连期间任务波动**
- 对策：服务侧调度器对齐 verl 的资源 fail-fast；任务失败可归因并排队重试。
