argus-cluster/specs/mvp/v3.8/v3.8_dev_plan.md

267 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MVP v3.8 开发计划TDD细化版
> 目标:在 v3.7 基础上引入 Ray ServevLLM模型动态部署与管理多模型单 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 管理 APIFastAPI单测驱动
**目的**:先把管理 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_key404
### M3.4 单测
- [ ] `test_app_serving_api.py`
- happy pathcreate → 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.yamlH20
- [ ] `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 不足:不 applystate 仍 QUEUEDevent 写入
- apply 抛异常state FAILEDerror_summary 写入
**验收**
- reconciler 行为在纯单测环境可验证;失败可解释
---
## M6 - 真实集成h1Ray 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 - WebUIServing 页面)(单测驱动)
**目的**:给用户可视化的模型管理页面(最小必要功能)。
### 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 主链路可操作