# MVP v3.8 开发计划(TDD,细化版) > 目标:在 v3.7 基础上引入 Ray Serve(vLLM)模型动态部署与管理(多模型单 app),并提供 WebUI + API 管理闭环。 > 约束(已确认): > - 推理端口固定 `8000`(Serve HTTP)。 > - 推理侧不接入现有 token 鉴权(对外 OpenAI endpoint 无鉴权)。 > - 对外 `model_id` 统一加前缀:`--`(用户只填 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`(对外:`-`) - `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//...` - 拒绝:`..`、空、其它用户路径、非 `/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/` 均可 - [ ] `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://: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` 验证包含 `-` - `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 主链路可操作