230 lines
9.7 KiB
Markdown
230 lines
9.7 KiB
Markdown
# MVP v2.5 开发计划(TDD 驱动)
|
||
|
||
本文是 v2.5 的**工程化开发计划**,强调“先写测试,再写实现”(TDD),并将每个里程碑拆成**可独立验收**的小闭环。
|
||
|
||
输入依据:
|
||
- 路线图:`specs/mvp/mvp_roadmap_v2.md`
|
||
- v2.5 设计:`specs/mvp/v2.5/v2.5_design.md`
|
||
- v2.5 API 草案:`specs/mvp/v2.5/v2.5_api.md`
|
||
- v2.5 验收:`specs/mvp/v2.5/v2.5_acceptance.md`
|
||
|
||
v2.5 约束(已确认):
|
||
- **不扩展 TaskSpec**:沿用 v2.0/v2.1 的 YAML 结构化字段与语义。
|
||
- **不支持自定义 reward function / 不支持用户自定义 verl 代码**。
|
||
- 训练输入(verl 代码、HF cache、datasets)统一使用 `/private/common/...`。
|
||
- 多用户隔离 v2.5 **先只隔离 jobs 输出目录**:`/private/users/<uid>/jobs/<ray_submission_id>/...`。
|
||
|
||
---
|
||
|
||
## 0. TDD 规范(所有功能都遵循)
|
||
|
||
### 0.1 测试分层
|
||
|
||
1) **单元测试(fast)**
|
||
- 纯 Python 逻辑:DB、鉴权、ID、路径派生、head.json 解析/TTL、watchdog 决策逻辑。
|
||
- 目标:不依赖真实 Ray、不依赖 docker、不依赖网络。
|
||
|
||
2) **组件测试(中等)**
|
||
- FastAPI 路由:使用 `fastapi.testclient.TestClient`(现有 v2.0 已采用)。
|
||
- 目标:验证 auth/权限隔离、API 行为、状态机。
|
||
|
||
3) **端到端(慢/手工或脚本)**
|
||
- 在 `argus@h1` 上通过 scripts/compose 跑一次“head publish → worker auto-connect → API submit”闭环。
|
||
- 目标:验证无状态 worker + watchdog 的真实行为。
|
||
|
||
### 0.2 测试约定
|
||
|
||
- 测试目录:`src/mvp/py/tests/`
|
||
- 新增功能必须先补齐测试用例,并让其在未实现时失败(红)。
|
||
- 实现最小改动让测试变绿(绿)。
|
||
- 重构/去重复(重构)。
|
||
|
||
> 注:现有测试通过 `src/mvp/py/tests/conftest.py` 注入 ray stub,确保单测不依赖真实 ray 包;v2.5 新增模块也应复用此模式。
|
||
|
||
---
|
||
|
||
## 1. 里程碑拆分(v2.5 = 4 个可验证闭环)
|
||
|
||
### M1:User 表/Token 表 + 基础鉴权(不影响现有内部 token 兼容)
|
||
|
||
**目标**
|
||
- 引入 user/token 的持久化与鉴权映射(token → user_id)。
|
||
- 兼容现有 `Authorization: Bearer <MVP_INTERNAL_TOKEN>` 的“单租户模式”,避免一次性破坏 v2.0 用法:
|
||
- v2.5 可以先支持两种 token 模式:
|
||
- legacy:环境变量 `MVP_INTERNAL_TOKEN`(全局单租户);
|
||
- user token:DB 内签发 token(多用户)。
|
||
- admin 能创建用户、签发 token、禁用用户。
|
||
|
||
**TDD 用例(先写测试)**
|
||
|
||
单测:
|
||
- `test_user_db_create_disable()`
|
||
- 创建用户 ACTIVE;禁用后状态变为 DISABLED;重复创建返回冲突或幂等(按最终约定)。
|
||
- `test_token_hashing()`
|
||
- 签发 token 时 DB 中只保存 hash,不保存明文。
|
||
|
||
API 测试(TestClient):
|
||
- `test_admin_create_user_and_issue_token()`
|
||
- admin token 可创建用户并签发 token(明文 token 只返回一次)。
|
||
- `test_disabled_user_token_rejected()`
|
||
- 用户被禁用后,使用旧 token 调用 API 返回 401/403。
|
||
|
||
**实现落点(建议模块)**
|
||
- `argus.service.auth`:token 校验与 user_id 解析(兼容 legacy 模式)
|
||
- `argus.service.db`:新增 `users`、`api_tokens` 表与 CRUD
|
||
- `argus.service.app`:新增 user 管理 endpoints(admin scope)
|
||
- `configs/dev.yaml`:补充 admin token/env 相关配置(保持 YAML 风格)
|
||
|
||
**验收点**
|
||
- `v2.5_acceptance.md`:U1 可通过自动化 API 测试覆盖。
|
||
|
||
---
|
||
|
||
### M2:Task 绑定 user_id + API 可见性隔离(仍不改 TaskSpec)
|
||
|
||
**目标**
|
||
- 提交 task 时由 token 推导 `user_id`,写入 `tasks.user_id`。
|
||
- task 查询/取消/日志默认只允许 owner;他人访问返回 404(避免泄露存在性)。
|
||
- queue 默认只返回当前用户队列;admin 可查询全局队列(可选)。
|
||
|
||
**TDD 用例(先写测试)**
|
||
|
||
单测:
|
||
- `test_tasks_table_has_user_id()`:创建任务必须落 `user_id`,且 `list_queue(user_id=...)` 只返回该用户任务。
|
||
|
||
API 测试:
|
||
- `test_task_visibility_isolated()`
|
||
- user A 创建 task;user B 查询 `/api/v2/tasks/{id}` 返回 404;
|
||
- user B cancel/logs 也返回 404。
|
||
- `test_queue_isolated()`
|
||
- A/B 各自创建 task;`GET /api/v2/queue` 只看到自己的。
|
||
|
||
**实现落点**
|
||
- `argus.service.app`:为 task endpoints 增加 user scope
|
||
- `argus.service.db`:tasks 表增加 user_id 字段、索引、按 user 过滤的查询方法
|
||
- `argus.service.scheduler`:pick_next_runnable_task 等仍按“全局 FIFO”或“按 user FIFO”
|
||
- v2.5 先保持“全局 FIFO”最简单(但 API queue 视角是按 user 过滤)。
|
||
|
||
**验收点**
|
||
- `v2.5_acceptance.md`:U2 可通过 API 测试覆盖。
|
||
|
||
---
|
||
|
||
### M3:Jobs 输出目录按 user 隔离(只改输出,不改输入)
|
||
|
||
**目标**
|
||
- Ray Job 的 job_root 目录由服务端统一计算到:
|
||
- `/private/users/<uid>/jobs/<ray_submission_id>/...`
|
||
- TaskSpec 内与输入相关的路径字段必须是 `/private/common/...`(v2.5 输入统一 common)。
|
||
- 任何用户无法通过 TaskSpec 指定输出写到非 user jobs 目录(避免越权写)。
|
||
|
||
**TDD 用例(先写测试)**
|
||
|
||
单测:
|
||
- `test_job_root_derivation_per_user()`
|
||
- 给定 user_id 与 ray_submission_id,派生 job_root 固定且正确。
|
||
- `test_reject_non_common_inputs()`
|
||
- TaskSpec 中 train_file / val_file / code_path / hf 路径等若不以 `/private/common/` 开头则拒绝(HTTP 400)。
|
||
|
||
API 测试:
|
||
- `test_job_dir_written_under_user_jobs()`
|
||
- 提交 task 后,在 DB 或 submit payload 中能看到 job_root 在 user jobs 下(可通过 mock RayJobTool.submit 捕获 spec)。
|
||
|
||
**实现落点(建议最小侵入)**
|
||
- 在 service 层派生 `job_root` 并注入到 RayJobTool/builders(而不是让用户从 TaskSpec 指定)。
|
||
- RayJobTool `_job_dir()` 改为接收“job_root 生成器”或直接接收 `job_root` 参数(由服务层提供)。
|
||
- 目标:保持 RayJobTool 的职责清晰:提交 Ray job;路径策略由 service 决定。
|
||
|
||
**验收点**
|
||
- `v2.5_acceptance.md`:U3/U4 可通过 API/单测覆盖。
|
||
|
||
---
|
||
|
||
### M4:Stateless Ray Node Pool(head.json + worker watchdog)+ 端到端脚本验证
|
||
|
||
**目标**
|
||
- head 启动后持续写入 `/private/ray/discovery/<cluster_name>/head.json`(包含 TTL)。
|
||
- worker 容器内运行 watchdog(或启动脚本 + watchdog),无需平台显式传 head 地址:
|
||
- 读取 head.json(存在且未过期)→ `ray start --address=<head_ip>:<gcs_port>`
|
||
- head.json 变化 → `ray stop` + `ray start` 重连
|
||
- 在 dev 环境(docker compose)提供一键脚本复现(e2e)。
|
||
|
||
**TDD 用例(先写测试)**
|
||
|
||
单测(不跑真实 ray):
|
||
- `test_head_json_read_validate_ttl()`
|
||
- 文件不存在/过期 → 返回“不可用”
|
||
- 未过期 → 返回 head 地址
|
||
- `test_watchdog_decision_on_change()`
|
||
- head_ip 变化 → 触发重连动作
|
||
- only updated_at 变化(地址不变)→ 不重连(或按策略重连,需确定)
|
||
|
||
组件/脚本级测试(可选):
|
||
- 如果 watchdog 用 Python 实现,可对“执行命令”层做 stub(不真正跑 `ray start`),只验证会调用什么命令。
|
||
|
||
端到端脚本(手工/慢):
|
||
- 提供脚本 `scripts/run_all_v25_stateless.sh`(命名示例):
|
||
1) 起 head(Ray head + API)
|
||
2) 启动 head publisher(写 head.json)
|
||
3) 起 2 个 worker(每个 4 GPU),worker 只跑 watchdog,不传 head 地址
|
||
4) `ray status` 显示 1 head + 2 worker 且 GPU=8
|
||
5) 通过 API 创建用户/签发 token,提交 PPO/GRPO/SFT
|
||
6) 重启 head(或更新 head.json 指向新地址)验证 worker 自动重连
|
||
|
||
**实现落点(建议实现策略)**
|
||
|
||
为了可测试性(TDD),推荐把“读 head.json/判定 TTL/生成 ray start 命令”做成 Python 模块:
|
||
- `argus.ray.discovery`:read/write head.json(原子写、TTL)
|
||
- `argus.ray.worker_watchdog`:watch loop(polling + change detection),执行命令可注入(便于单测 stub)
|
||
|
||
脚本层保持薄:
|
||
- `scripts/` 负责 docker exec / compose 编排与进程守护;
|
||
- watchdog 进程由容器内 python 模块运行(更可测、更易移植到生产平台的 entrypoint/command)。
|
||
|
||
**验收点**
|
||
- `v2.5_acceptance.md`:A1/A2/A3 主要通过 e2e 脚本 + dashboard/日志验证。
|
||
|
||
---
|
||
|
||
## 2. 回归策略(确保 v2.0 不被破坏)
|
||
|
||
在 v2.5 过程中保留并持续回归以下用例(至少单测覆盖):
|
||
- 旧的内部 token 模式仍可访问 `GET /api/v2/queue` 与提交 task(若决定保留兼容)。
|
||
- scheduler 的“资源不足 → PENDING_RESOURCES → 延迟重试”行为不变(现有 `test_scheduler.py` 覆盖)。
|
||
- `ray entrypoint_resources` 强制 driver 落 worker(继续使用 `worker_node` 自定义资源)。
|
||
|
||
---
|
||
|
||
## 3. 交付清单(代码/脚本/文档)
|
||
|
||
### 3.1 代码
|
||
- user/tokens:DB schema + auth + API endpoints
|
||
- tasks:绑定 user_id + 权限隔离
|
||
- job_root:按 user jobs 输出目录派生(输入仍 common)
|
||
- discovery/watchdog:head.json + worker 自愈
|
||
|
||
### 3.2 scripts(dev e2e)
|
||
- head:启动 Ray head + head publisher
|
||
- workers:以无状态方式启动(不传 head addr)+ watchdog
|
||
- `run_all`:一键跑通(含 API submit + 查询 + cancel + 观察队列)
|
||
|
||
### 3.3 文档
|
||
- 更新 `specs/mvp/v2.5/*`(设计/API/验收/开发计划)
|
||
- 补充 `src/mvp/README.md` 的 v2.5 使用方式(如需要)
|
||
|
||
---
|
||
|
||
## 4. 关键待确认点(开始实现前必须定稿)
|
||
|
||
1) **legacy token 是否继续兼容**
|
||
- 方案 A:保留 `MVP_INTERNAL_TOKEN`(单租户)+ 新增 user token(多租户)
|
||
- 方案 B:v2.5 直接切换到 user token(破坏兼容,但更清晰)
|
||
|
||
2) **调度公平性**
|
||
- v2.5 先全局 FIFO(简单);后续 v3 再引入 per-user 公平调度/配额。
|
||
|
||
3) **head.json 的生产写入者**
|
||
- 方案 A:与 API 同进程线程(最少组件)
|
||
- 方案 B:独立进程(更独立、易运维)
|
||
|