6.4 KiB
6.4 KiB
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
- 只构建该 model 的
- 对于状态
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/v1endpoint.model = <model_id>
建议改为(字段不变,值变化):
endpoint.openai_base_url = http://<host>:8000/serve/<model_key>/v1endpoint.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)
- 通过 UI/或 API 创建模型 A,等待 RUNNING
- 记录 A 的:
model_key_Aendpoint.openai_base_url_A
- 再创建模型 B,等待 RUNNING
- 确认:
- A 的 endpoint 仍可用(对 A 的 base_url 发 chat completion)
- B 的 endpoint 可用
- 删除模型 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/modelslist/get 的 openai_base_url 语义调整(可返回“该模型的 base_url”,列表里每条都有)
argus.service.ui(Serving 页面):- “OpenAI /v1/models” 需要调整为“选择某个模型后打开该模型的 /v1/models”
- 详情页 curl 示例使用 per-model base_url