256 lines
9.6 KiB
Markdown
256 lines
9.6 KiB
Markdown
# 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;任务失败可归因并排队重试。
|