267 lines
10 KiB
Markdown
267 lines
10 KiB
Markdown
# MVP v3.8 开发计划(TDD,细化版)
|
||
|
||
> 目标:在 v3.7 基础上引入 Ray Serve(vLLM)模型动态部署与管理(多模型单 app),并提供 WebUI + API 管理闭环。
|
||
> 约束(已确认):
|
||
> - 推理端口固定 `8000`(Serve HTTP)。
|
||
> - 推理侧不接入现有 token 鉴权(对外 OpenAI endpoint 无鉴权)。
|
||
> - 对外 `model_id` 统一加前缀:`<user_id>-<YYYYMMDDHHMM>-<suffix>`(用户只填 suffix)。
|
||
> - `LLMConfig.accelerator_type` 从 `dev.yaml` 读取(dev/h1: `H20`)。
|
||
|
||
本计划按“测试先行 → 实现 → 回归”的节奏拆分到可验证粒度;每个 milestone 都能单独验收。
|
||
|
||
---
|
||
|
||
## M0 - 基线与依赖探测(不改行为)
|
||
|
||
**目的**:确认 v3.7 baseline 稳定,并明确 Ray Serve LLM 依赖是否已具备(否则后续会卡在镜像/依赖)。
|
||
|
||
### M0.1 本地回归
|
||
- [ ] `.venv/bin/python -m pytest` 通过(coverage ≥ 90%)
|
||
|
||
### M0.2 远端回归(h1)
|
||
- [ ] `src/mvp/scripts/run_all_v30_api.sh` 可跑通(确认训练闭环未回退)
|
||
|
||
### M0.3 head 容器内依赖探测(记录结论)
|
||
- [ ] `python3 -c "import ray; import ray.serve; print(ray.__version__)"`
|
||
- [ ] `python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"`
|
||
- [ ] 若失败(例如缺 `gymnasium`):记录缺失项,并在 M6 通过补齐 `ray[llm]` 解决
|
||
|
||
### M0.4 配置探测
|
||
- [ ] `configs/dev.yaml` 中存在:
|
||
- `serving.llm.accelerator_type: H20`
|
||
- `serving.serve.http_port: 8000`
|
||
- `serving.serve.proxy_location: HeadOnly`
|
||
|
||
**验收**:
|
||
- baseline 无回退;依赖探测结论明确(可用/不可用)
|
||
|
||
---
|
||
|
||
## M1 - ServingSpec(解析/校验/宏替换/路径校验)(单测驱动)
|
||
|
||
**目的**:先把“输入”这层彻底固化(API/UI 复用),避免后期反复改 schema。
|
||
|
||
### M1.1 新增/扩展数据模型
|
||
- [ ] `ServingSpec`(输入)
|
||
- `model_id`(suffix)
|
||
- `model_source`(支持 `$HOME` 宏)
|
||
- `num_replicas`(default=1)
|
||
- `gpus_per_replica`(default=1)
|
||
- `engine_kwargs`(可选 dict,先原样存 DB;实现阶段再做白名单/黑名单)
|
||
- [ ] `ResolvedServingSpec`(内部)
|
||
- `model_id_suffix`
|
||
- `model_id_prefix`(由平台生成:`user_id-YYYYMMDDHHMM`)
|
||
- `model_id`(对外:`<prefix>-<suffix>`)
|
||
- `model_source`(resolved path)
|
||
|
||
### M1.2 规则(写成纯函数,便于测)
|
||
- [ ] `validate_model_id_suffix(suffix)`:长度/字符集限制(建议:`[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}`)
|
||
- [ ] `$HOME` 宏替换:`$HOME`、`$HOME/common/hf`、`$HOME/common/datasets`
|
||
- [ ] 路径校验(强制本地路径):
|
||
- 允许:`/private/hf/...`、`/private/users/<user_id>/...`
|
||
- 拒绝:`..`、空、其它用户路径、非 `/private` 路径
|
||
- [ ] `make_model_id_prefix(user_id, now_utc)`:`YYYYMMDDHHMM`(UTC)+ user_id
|
||
|
||
### M1.3 单测(先写失败用例,再补实现)
|
||
- [ ] `test_serving_spec_validation.py`
|
||
- suffix 合法/非法
|
||
- replicas/gpus 边界:0、负数、小数、超大值(按实现决定是否限制上限)
|
||
- [ ] `test_serving_spec_paths.py`
|
||
- `$HOME` 替换正确
|
||
- 越权路径返回 403/ValueError(按接口层映射)
|
||
- `/private/hf` 与 `/private/users/<user>` 均可
|
||
- [ ] `test_serving_model_id_prefix.py`
|
||
- 固定时间输入 → prefix 输出一致(避免时区/格式问题)
|
||
|
||
**验收**:
|
||
- 输入 spec 规则稳定;核心校验/替换均有单测覆盖
|
||
|
||
---
|
||
|
||
## M2 - SQLite 表结构与 Db 接口(单测驱动)
|
||
|
||
**目的**:Serving 的声明式状态必须持久化,可审计、可恢复。
|
||
|
||
### M2.1 DB schema
|
||
- [ ] `serve_models`
|
||
- 主键:`model_key`(平台生成)
|
||
- unique:`(user_id, model_id_suffix)`(实现 upsert)
|
||
- 存储:resolved spec(包含 prefix/full model_id、resolved model_source)
|
||
- 状态:`QUEUED/DEPLOYING/RUNNING/FAILED/DELETING/DELETED`
|
||
- `error_summary`
|
||
- [ ] `serve_events`(append-only)
|
||
|
||
### M2.2 Db 方法
|
||
- [ ] `upsert_serve_model(user_id, spec_yaml, now)` → (model_key, state)
|
||
- [ ] `list_serve_models(user_id, include_deleted=False, limit/offset?)`
|
||
- [ ] `get_serve_model(model_key)`
|
||
- [ ] `set_serve_model_state(model_key, state, error_summary=None)`
|
||
- [ ] `append_serve_event(model_key, event_type, payload_json=None)`
|
||
- [ ] `pick_next_runnable_serve_change()`(给 reconciler 用)
|
||
|
||
### M2.3 单测
|
||
- [ ] `test_db_serving.py`
|
||
- upsert 行为(同 suffix 更新不产生新 model_key 或产生新版本——此处需在实现前明确策略)
|
||
- state 流转 + 事件记录
|
||
- list 的过滤与排序(按 updated_at)
|
||
|
||
**验收**:
|
||
- DB 行为可预测;upsert/unique 语义确定并测试覆盖
|
||
|
||
---
|
||
|
||
## M3 - Serving 管理 API(FastAPI)(单测驱动)
|
||
|
||
**目的**:先把管理 API 跑通,Ray Serve 先不接真实(reconciler 之后再接)。
|
||
|
||
### M3.1 API 路由(用户)
|
||
- [ ] `POST /api/v2/serve/models`(Content-Type: application/yaml)
|
||
- 入参:ServingSpec YAML
|
||
- 出参:`{model_key,state}`(202)
|
||
- [ ] `GET /api/v2/serve/models`
|
||
- 返回 items + `openai_base_url=http://<host>:8000/v1`
|
||
- [ ] `GET /api/v2/serve/models/{model_key}`
|
||
- 返回 model + resolved_spec_yaml + events(分页可后置)+ serve_status(先空/占位)
|
||
- [ ] `PATCH /api/v2/serve/models/{model_key}`(JSON)
|
||
- 支持 `num_replicas`(最小闭环)
|
||
- [ ] `DELETE /api/v2/serve/models/{model_key}`
|
||
|
||
### M3.2 API 路由(admin,可选)
|
||
- [ ] `GET /api/v2/serve/status`(仅 admin token)
|
||
|
||
### M3.3 错误映射(必须测试)
|
||
- [ ] YAML 解析失败:400
|
||
- [ ] spec 校验失败:422
|
||
- [ ] 越权路径:403
|
||
- [ ] 不存在 model_key:404
|
||
|
||
### M3.4 单测
|
||
- [ ] `test_app_serving_api.py`
|
||
- happy path:create → list → get → patch → delete
|
||
- 多用户隔离:用户只能看到自己的 model
|
||
- 错误码覆盖:400/403/404/422
|
||
|
||
**验收**:
|
||
- API reference (`v3.8_api.md`) 中所有管理接口可返回预期结构(Serve 未接入也能工作)
|
||
|
||
---
|
||
|
||
## M4 - ServeClient 抽象 + LLMConfig builder(单测驱动)
|
||
|
||
**目的**:将“如何从 ResolvedServingSpec 构造 LLMConfig”固化,并把 Ray Serve 的依赖隔离到 client 里,便于 mock。
|
||
|
||
### M4.1 `ServeClient` 接口(可 mock)
|
||
- [ ] `ensure_started(http_port=8000, proxy_location="HeadOnly")`
|
||
- [ ] `apply_app(app_name, llm_configs)`(multi-model)
|
||
- [ ] `get_status()`(serve.status 摘要)
|
||
|
||
### M4.2 `build_llm_config(resolved_spec, accelerator_type, runtime_env_defaults)` 纯函数
|
||
- [ ] 写入 `LLMConfig.accelerator_type`(来自 dev.yaml:H20)
|
||
- [ ] `deployment_config.num_replicas`
|
||
- [ ] `engine_kwargs.tensor_parallel_size = gpus_per_replica`
|
||
- [ ] `placement_group_config` bundles 按 GPU 张数生成
|
||
- [ ] `runtime_env.env_vars` 注入(至少包含 HF cache + `HF_HUB_OFFLINE=1`)
|
||
|
||
### M4.3 单测
|
||
- [ ] `test_llm_config_builder.py`
|
||
- gpus_per_replica=1/2/4 → tensor_parallel_size 与 bundles 数量正确
|
||
- accelerator_type 注入正确
|
||
- runtime_env 含 HF_HUB_OFFLINE 等关键 env
|
||
|
||
**验收**:
|
||
- 从平台 spec 到 Ray Serve LLMConfig 的映射规则稳定,有单测锁定
|
||
|
||
---
|
||
|
||
## M5 - Serving Reconciler(状态机 + 资源预检查)(单测驱动)
|
||
|
||
**目的**:实现声明式对齐:DB → Serve;同时提供可解释的 QUEUED/FAILED 状态。
|
||
|
||
### M5.1 状态机(最小闭环)
|
||
- [ ] `QUEUED`:等待 apply
|
||
- [ ] `DEPLOYING`:已触发 apply,等待 Serve running/healthy
|
||
- [ ] `RUNNING`:Serve status running
|
||
- [ ] `FAILED`:apply 或 status 失败(写 error_summary + event)
|
||
- [ ] `DELETING`:等待从 app 中移除
|
||
- [ ] `DELETED`:完成删除(可选保留记录)
|
||
|
||
### M5.2 资源预检查
|
||
- [ ] `needed_total_gpus = sum(num_replicas*gpus_per_replica)`(最小可用预检查)
|
||
- [ ] `ray.available_resources()["GPU"]`(或更稳健的 per-node 统计)不足时:
|
||
- 保持 `QUEUED`
|
||
- 记录 `PENDING_RESOURCES` event
|
||
|
||
### M5.3 reconcile 策略(multi-model app)
|
||
- [ ] tick 读取 active models,构建全量 `llm_configs`
|
||
- [ ] 处理 deleting:从 configs 中移除对应 model,再 apply
|
||
|
||
### M5.4 单测(mock ServeClient + mock ray resources)
|
||
- [ ] `test_serving_reconciler.py`
|
||
- 新增模型:apply_app 被调用;state 进入 DEPLOYING
|
||
- 删除模型:apply_app configs 不包含该模型
|
||
- GPU 不足:不 apply;state 仍 QUEUED;event 写入
|
||
- apply 抛异常:state FAILED;error_summary 写入
|
||
|
||
**验收**:
|
||
- reconciler 行为在纯单测环境可验证;失败可解释
|
||
|
||
---
|
||
|
||
## M6 - 真实集成(h1):Ray Serve 启动 + 推理闭环(E2E)
|
||
|
||
**目的**:在 dev/h1 环境真正跑通:部署模型 → `/v1/models` 可见 → `chat/completions` 成功 → 删除后消失。
|
||
|
||
### M6.1 compose/端口
|
||
- [ ] `src/mvp/docker-compose.yaml`:`ray_head` 增加 `8000:8000`
|
||
|
||
### M6.2 镜像依赖(若 M0 发现缺失)
|
||
- [ ] 在 `argus-ray-node` 镜像中补齐 `ray[serve,llm]`(版本与现有 Ray 对齐,避免升级 Ray 导致不兼容)
|
||
- 推荐优先补齐 `ray[llm]`(包含 `ray.serve.llm` 依赖闭包,如 `gymnasium`),再按需补 `ray[serve]`
|
||
- 验证点:`python3 -c "from ray.serve.llm import LLMConfig, build_openai_app; print('serve_llm_ok')"`
|
||
|
||
### M6.3 E2E 脚本(幂等)
|
||
- [ ] 新增 `scripts/run_all_v38_serving.sh`:
|
||
- 起 compose(确保 Serve 端口可用)
|
||
- 起 API
|
||
- 创建 user + token
|
||
- `POST /api/v2/serve/models` 创建 1GPU 模型
|
||
- 轮询模型 state 到 RUNNING
|
||
- `curl http://127.0.0.1:8000/v1/models` 验证包含 `<prefix>-<suffix>`
|
||
- `curl http://127.0.0.1:8000/v1/chat/completions` 进行最小推理
|
||
- `DELETE /api/v2/serve/models/{model_key}` 下线
|
||
- 再轮询 `/v1/models` 不包含
|
||
|
||
**验收**:
|
||
- E2E 可重复跑通(至少两次连续跑不需要人工清理)
|
||
|
||
---
|
||
|
||
## M7 - WebUI(Serving 页面)(单测驱动)
|
||
|
||
**目的**:给用户可视化的模型管理页面(最小必要功能)。
|
||
|
||
### M7.1 页面
|
||
- [ ] Sidebar 增加 Serving
|
||
- [ ] `/ui/serving`:列表 + 状态 + 操作(delete/scale)
|
||
- [ ] `/ui/serving/new`:YAML 输入 + submit
|
||
- [ ] `/ui/serving/{model_key}`:详情(resolved spec、events、OpenAI 调用示例)
|
||
|
||
### M7.2 单测
|
||
- [ ] `test_ui_serving.py`:路由 200、关键链接存在、包含 openai_base_url=8000
|
||
|
||
**验收**:
|
||
- WebUI 覆盖 create/list/detail/scale/delete 的主链路
|
||
|
||
---
|
||
|
||
## M8 - 文档与验收用例(交付)
|
||
|
||
**目的**:给用户/运维一套可复用的运行方式与排障路径。
|
||
|
||
- [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`(按 milestone 记录)
|
||
- [ ] 补充 README(可选):端口说明、推理 API 无鉴权警示、模型路径约定
|
||
- [ ] 验收清单(checklist):
|
||
- 单测通过
|
||
- h1 E2E 通过
|
||
- UI 主链路可操作
|