190 lines
6.4 KiB
Markdown
190 lines
6.4 KiB
Markdown
# v3.8 方案补充:每个模型一个 Ray Serve App(隔离增删影响)
|
||
|
||
## 背景与问题复现
|
||
|
||
当前 v3.8 的实现采用 **单 application + 多模型** 的方式:
|
||
|
||
- 服务层每次 reconcile 都会构造“全量 llm_configs”并调用一次 `serve.run(app, name="argus_llm_app", route_prefix="/")`
|
||
- **新增/删除一个模型**会触发对同一个 app 的“整体更新”
|
||
- Ray Serve 在 app 更新时会对该 app 内的 deployments/replicas 做滚动更新与重新调度
|
||
|
||
因此你在 Ray Dashboard 中观察到:
|
||
|
||
- 添加/删除一个模型时,其他模型的 Serve deployment 也进入更新状态
|
||
- 内存/显存占用重新变化,甚至出现 GPU 卡位变化(replica 重新调度到不同 node/GPU)
|
||
|
||
这与“其他未变更 model 不受影响”的期望不一致。
|
||
|
||
---
|
||
|
||
## 目标
|
||
|
||
将 serving 架构调整为:
|
||
|
||
- **每个模型一个 Serve App(独立 app name)**
|
||
- 每个模型一个独立 `route_prefix`
|
||
- 新增/删除/缩放某个模型只更新该模型对应的 app,不影响其他模型 app
|
||
|
||
约束保持不变:
|
||
|
||
- 推理端口固定 `8000`
|
||
- 推理侧不接入现有 token 鉴权(OpenAI endpoint 无鉴权)
|
||
- `model_id` 前缀规则:`<user_id>-<YYYYMMDDHHMM>-<suffix>`
|
||
- `LLMConfig.accelerator_type` 由 `configs/dev.yaml` 配置(dev/h1: `H20`)
|
||
|
||
---
|
||
|
||
## 总体设计
|
||
|
||
### 1) 命名与路由
|
||
|
||
为每个 model 生成:
|
||
|
||
- `app_name`:建议直接使用 `model_key`(天然唯一且 URL-safe),例如:
|
||
- `app_name = "mvp2-alice-serve-20260106-060203-aad8"`
|
||
- `route_prefix`:建议使用 model_key,避免 model_id 中的 `.`、`_` 等带来的 URL/编码歧义:
|
||
- `route_prefix = f"/serve/{model_key}"`
|
||
|
||
于是该模型的 OpenAI base url 为:
|
||
|
||
- `openai_base_url = http://<host>:8000/serve/<model_key>/v1`
|
||
|
||
说明:
|
||
|
||
- 仍然是 **OpenAI-compatible**,只是 base_url 不再是根路径 `/v1`,而是每个模型一个前缀。
|
||
- 这样可以做到“每个模型的 OpenAI endpoint 互不影响”,也更容易做按模型的观测/下线。
|
||
|
||
### 2) 运行方式(Ray Serve)
|
||
|
||
单模型 app 的创建/更新:
|
||
|
||
- `app = build_openai_app({"llm_configs":[LLMConfig(...)]})`
|
||
- `serve.run(app, name=app_name, route_prefix=route_prefix)`
|
||
|
||
单模型 app 的删除:
|
||
|
||
- `serve.delete(app_name)`
|
||
|
||
关键点:
|
||
|
||
- **更新/删除只作用于对应 app_name**;其它 app 不会被 serve.run “整体重建”触发滚动更新。
|
||
|
||
### 3) 服务层(Scheduler/Reconciler)改造点(高层)
|
||
|
||
现状:`ServingReconciler.tick()` 每次对“全量模型集合” apply 一次 app。
|
||
|
||
目标:改成按 model_key 的“局部 reconcile”:
|
||
|
||
- 对于状态 `QUEUED` 的 model:
|
||
- 只构建该 model 的 `LLMConfig`
|
||
- `serve.run(app, name=model_key, route_prefix="/serve/<model_key>")`
|
||
- 状态:`DEPLOYING` →(probe 成功)→ `RUNNING`
|
||
- 对于状态 `DELETING` 的 model:
|
||
- `serve.delete(model_key)`
|
||
- 状态:`DELETED`
|
||
|
||
资源预检查:
|
||
|
||
- 只需要预检查“本次变更模型”需要的 GPU(`num_replicas * gpus_per_replica`)
|
||
- 不需要把其他模型资源都算入 needed_total_gpus(因为不再重建全量 app)
|
||
|
||
### 4) API/UI 返回的 endpoint 结构
|
||
|
||
现状 API 返回:
|
||
|
||
- `endpoint.openai_base_url = http://<host>:8000/v1`
|
||
- `endpoint.model = <model_id>`
|
||
|
||
建议改为(字段不变,值变化):
|
||
|
||
- `endpoint.openai_base_url = http://<host>:8000/serve/<model_key>/v1`
|
||
- `endpoint.model = <model_id>`(保持)
|
||
|
||
UI 的示例 curl 也应使用上面的 base_url。
|
||
|
||
---
|
||
|
||
## 行为变化与兼容性影响
|
||
|
||
### 1) `/v1/models` 聚合能力变化(重要)
|
||
|
||
采用“每模型一个 route_prefix”后:
|
||
|
||
- `http://<host>:8000/v1/models` **不再是“所有模型的总览”**(除非我们再提供一个聚合层)
|
||
- 每个模型的 models list 在它自己的前缀下:
|
||
- `http://<host>:8000/serve/<model_key>/v1/models`
|
||
|
||
如果仍然希望保留一个统一入口(可选增强,非本方案必做):
|
||
|
||
- 额外引入一个“稳定不重建”的 **OpenAI Router**(可以是:
|
||
- FastAPI(8080) 侧做反向代理;或
|
||
- 一个单独 Ray Serve app 只负责路由,不随模型变更重建)
|
||
- Router 读取 SQLite/内存缓存的 model 映射:
|
||
- `model_id -> route_prefix`
|
||
- 将 `/v1/chat/completions` 转发到对应 model 的 prefix
|
||
|
||
这可以作为 v3.9+ 的增强项;v3.8 的核心目标是“变更隔离”,优先保证稳定性。
|
||
|
||
### 2) 资源与调度稳定性
|
||
|
||
改为 per-app 后:
|
||
|
||
- 新增模型 B 不再引起模型 A 的 replica 重新调度 → **A 的 GPU/内存占用更稳定**
|
||
- 删除模型 B 也不会触发 A 的滚动更新
|
||
|
||
但仍需注意:
|
||
|
||
- 如果 Ray 集群发生节点故障/资源回收,Serve 本身仍可能重启个别 replica(这是系统层行为)
|
||
|
||
---
|
||
|
||
## 验证与验收流程(建议)
|
||
|
||
### A. 功能验收(API/UI)
|
||
|
||
1. 通过 UI/或 API 创建模型 A,等待 RUNNING
|
||
2. 记录 A 的:
|
||
- `model_key_A`
|
||
- `endpoint.openai_base_url_A`
|
||
3. 再创建模型 B,等待 RUNNING
|
||
4. 确认:
|
||
- A 的 endpoint 仍可用(对 A 的 base_url 发 chat completion)
|
||
- B 的 endpoint 可用
|
||
5. 删除模型 B,确认:
|
||
- B endpoint 404/不可用
|
||
- A endpoint 仍可用
|
||
|
||
### B. “不影响其它模型”的强验证(Ray actor 级别)
|
||
|
||
在 Ray 上抓取 A 对应 `LLMServer` replica 的 actor_id/node_id:
|
||
|
||
- 创建 B 前:`actor_id_A_before`
|
||
- 创建 B 后:`actor_id_A_after`
|
||
- 删除 B 后:`actor_id_A_after_delete`
|
||
|
||
预期:
|
||
|
||
- `actor_id_A_before == actor_id_A_after == actor_id_A_after_delete`
|
||
|
||
(允许 `LLMRouter` 变化,但 **LLMServer for A 不应变化**)
|
||
|
||
---
|
||
|
||
## 需要修改的代码点(清单级)
|
||
|
||
> 这里只列“改哪里”,不展开具体实现(实现时按 TDD 补单测)。
|
||
|
||
- `argus.service.serving_reconciler`:
|
||
- 从“全量 apply 单 app”改为“按 model_key 局部 apply/delete 单 app”
|
||
- GPU 预检查改为 per-model
|
||
- `argus.service.serve_client`:
|
||
- 增加 `delete_app(app_name)`(封装 `serve.delete(app_name)`)
|
||
- `apply_app` 传入 `app_name/route_prefix`(已存在,但将不再传固定 app_name)
|
||
- `argus.service.app`(Serving API 输出):
|
||
- `_serve_model_public().endpoint.openai_base_url` 改为 per-model 前缀
|
||
- `/api/v2/serve/models` list/get 的 openai_base_url 语义调整(可返回“该模型的 base_url”,列表里每条都有)
|
||
- `argus.service.ui`(Serving 页面):
|
||
- “OpenAI /v1/models” 需要调整为“选择某个模型后打开该模型的 /v1/models”
|
||
- 详情页 curl 示例使用 per-model base_url
|
||
|