# MVP v3.8(变更)开发计划:Per-Model Serve App(TDD) > 目标:按 `specs/mvp/v3.8/v3.8_per_model_app.md` 将 v3.8 从“单 app 多模型(全量重建)”改为“**每个模型一个 Ray Serve app + 独立 route_prefix**”,实现增删/缩放某个模型不触发其它模型重启与重调度。 ## 约束与结论 - 推理端口固定:`8000` - 推理 endpoint **不做鉴权** - `model_id` 前缀规则:`--` - `LLMConfig.accelerator_type` 由 `configs/dev.yaml` 决定(dev/h1: `H20`) - 路由方案(本迭代固定): - `app_name = model_key` - `route_prefix = /serve/` - `openai_base_url = http://:8000/serve//v1` ## 非目标(明确不做) - 不提供统一 `/v1` 的“跨模型聚合路由”(如要,需要额外 router 层;可在后续迭代做) - 不改 ServingSpec 语义(输入仍为 `model_id/model_source/num_replicas/gpus_per_replica/engine_kwargs`) --- ## M0 - 基线回归与分支保护 **目的**:确保切换架构前训练/现有功能不回退。 ### 测试 - [ ] 本地:`.venv/bin/python -m pytest` 全绿(coverage ≥ 90%) ### 验收 - [ ] 基线可用;进入 M1 --- ## M1 - API 输出与 endpoint 语义调整(单测驱动) **目的**:API/DB/前端都统一 per-model 的 `openai_base_url` 语义;避免 UI/脚本继续使用 `/v1` 根路径。 ### 变更点 - `GET /api/v2/serve/models`: - 保持返回 `items[]`,但每个 item 的 `endpoint.openai_base_url` 必须是 per-model base url - `openai_base_url`(列表层级字段)处理策略二选一: - A(推荐):移除该字段(breaking,需同步 UI/脚本) - B(兼容):保留但改为 `null` 或提示字符串(不再保证可用) - `GET /api/v2/serve/models/{model_key}`: - `model.endpoint.openai_base_url` 改为 per-model base url ### 单测(先写) - [ ] 更新/新增 `src/mvp/py/tests/test_app_serving_api.py` - 断言 `endpoint.openai_base_url` 包含 `/serve//v1` - 断言多条 models 的 base_url 不相同(随 model_key 变化) ### 实现 - [ ] 更新 `src/mvp/py/argus/service/app.py`: - `_serve_model_public()` 的 `endpoint.openai_base_url` 拼接 per-model prefix - 如选择移除/调整 list 层的 `openai_base_url`,同步实现 ### 验收 - [ ] API 单测通过;返回结构可被 UI/脚本消费 --- ## M2 - ServeClient 扩展(delete_app)+ Reconciler 改造成 per-model(单测驱动) **目的**:核心行为变更:每次 tick 只部署/删除一个模型对应的 app,不重建全量 app。 ### 变更点(行为) - `QUEUED`: - 对该 `model_key` 执行 `serve.run(app, name=model_key, route_prefix=/serve/)` - 状态:`DEPLOYING → RUNNING` - `DELETING`: - 对该 `model_key` 执行 `serve.delete(model_key)` - 状态:`DELETED` - 资源预检查从“全量 needed_total_gpus”改为“本次变更模型所需 GPU” ### 单测(先写) - [ ] 更新 `src/mvp/py/tests/test_serving_reconciler.py` - `create A` 后,reconciler 只 `apply_app(app_name=A.model_key, route_prefix=/serve/A)` - `create B` 后,reconciler 只 `apply_app(app_name=B.model_key, route_prefix=/serve/B)`(不再对 A 触发 apply) - `delete B` 后,reconciler 只 `delete_app(B.model_key)`(不触发 A) - GPU 不足时:保持 `QUEUED` 且 event=SERVE_PENDING_RESOURCES ### 实现 - [ ] `src/mvp/py/argus/service/serve_client.py` - 增加 `delete_app(app_name: str)`(封装 `serve.delete`) - [ ] `src/mvp/py/argus/service/serving_reconciler.py` - 移除“全量 app apply”逻辑 - 每个 model_key 独立部署:`app_name=model_key`、`route_prefix=/serve/` - 删除路径走 `delete_app` ### 验收 - [ ] reconciler 单测全绿;逻辑可解释(events/state 正确) --- ## M3 - WebUI Serving 页面适配 per-model base_url(单测驱动) **目的**:用户从 UI 复制的示例命令必须可用;不再指向根 `/v1`。 ### 变更点 - `/ui/serving` 列表: - “OpenAI /v1/models”按钮改为: - A:移除(因为没有聚合 `/v1/models`) - B:保留但文案改为“OpenAI base 需进入详情页” - `/ui/serving/{model_key}` 详情页: - `curl` 示例使用 per-model `openai_base_url` - 增加一键打开:`/serve//v1/models` ### 单测(先写) - [ ] 更新/新增 `src/mvp/py/tests/test_ui_serving.py` - 断言页面包含 `/serve/` 前缀 - 断言详情页示例里包含 `/serve//v1/chat/completions` ### 实现 - [ ] `src/mvp/py/argus/service/ui.py` 更新 Serving UI ### 验收 - [ ] UI 单测全绿;页面内容与 API 语义一致 --- ## M4 - E2E 脚本更新(v3.8 serving) **目的**:在 dev/h1 一键验证 per-model app:A/B 增删不互相影响,且推理可用。 ### 变更点 - 更新 `src/mvp/scripts/run_all_v38_serving.sh`: - `/v1/models` 与 `chat/completions` 均改用 per-model base_url(`/serve//v1`) - 增加“隔离验证”步骤: - 部署 A → 记录 A 的 serve replica actor_id/node_id - 部署 B → 再次记录 A 的 actor_id/node_id,必须一致 - 删除 B → 再次记录 A 的 actor_id/node_id,必须一致 - 最后删除 A ### 验收 - [ ] E2E 脚本能跑通且输出明确断言(一致/不一致) --- ## M5 - h1 端到端验证与回归 **目的**:确认实际 Ray Serve 行为满足“其它模型不滚动更新”的核心目标。 ### 操作 - [ ] 同步代码到:`argus@h1:/home2/argus/infra/mvp/src/mvp` - [ ] 重启 API:`scripts/61_stop_api.sh && scripts/60_start_api.sh` - [ ] 执行:`MVP_INTERNAL_TOKEN=... scripts/run_all_v38_serving.sh` ### 验收标准(必须满足) - [ ] 新增/删除 B 时,A 的 `LLMServer` replica actor_id/node_id 不变 - [ ] A/B 的 OpenAI endpoint 均可完成 `chat/completions` - [ ] 删除 B 后 A 仍可推理 --- ## M6 - 文档与迁移说明 **目的**:明确“路由语义变化”和“如何使用”。 - [ ] 更新 `src/mvp/README.md`: - 新增 per-model base_url 说明(`/serve//v1`) - 提示不再提供聚合 `/v1/models` - [ ] 更新 `specs/mvp/v3.8/v3.8_progress.md`: - 记录 per-model app 变更与验收结论 --- ## 风险与缓解 - **风险:旧 `argus_llm_app` 残留** - 缓解:在 E2E/迁移步骤里增加一次 best-effort `serve.delete("argus_llm_app")`(可选) - **风险:用户仍按旧方式访问 `/v1`** - 缓解:UI/文档/脚本统一切换到 per-model base_url,并在列表页给出明显提示