372 lines
14 KiB
Markdown
372 lines
14 KiB
Markdown
# MVP v3.8 详细设计方案:Ray Serve(vLLM)模型动态部署与管理
|
||
|
||
> 基线:当前已具备 v3.7 能力(训练平台 + W&B + SFTPGo + WebUI/API + Ray stateless pool,训练侧默认 rollout=vllm)。
|
||
> v3.8 目标:在同一套 Ray 集群上,引入 **Ray Serve LLM(后端 vLLM)** 的模型推理服务能力,并通过 WebUI/API 动态管理模型生命周期。
|
||
|
||
## 0. 需求范围(来自 requirements.md)
|
||
|
||
1) 通过 Ray Serve(后端 vLLM)动态拉起 LLM,支持**多模型 application** 部署
|
||
2) 默认一个模型 1 个 replica,用户可配置多个
|
||
3) 用户可删除(下线)模型
|
||
4) 用户可指定模型使用几张 GPU
|
||
5) WebUI 可配置、查看模型列表、查看详情
|
||
6) 模型路径可用 common,也可用 user 路径(本地路径)
|
||
|
||
## 1. 总体架构
|
||
|
||
### 1.1 组件关系
|
||
|
||
v3.8 在现有“训练平台”之上新增一个 **Serving 子系统**:
|
||
|
||
- **API server(现有)**
|
||
- 新增 Serving API(模型部署/删除/扩缩容/状态)
|
||
- 新增 Serving 后台线程(reconciler):周期性对齐 DB 与 Ray Serve 实际状态
|
||
- **SQLite(现有)**
|
||
- 新增 `serve_models`、`serve_events` 等表,保存声明式配置与状态
|
||
- **Ray 集群(现有 stateless pool)**
|
||
- 复用现有 head/worker 容器
|
||
- 在集群内启动 Ray Serve(controller + proxy + deployments)
|
||
- **Ray Serve LLM(新增)**
|
||
- 通过 `ray.serve.llm.build_openai_app` 构建一个 OpenAI-compatible app
|
||
- app 内包含多个 `LLMConfig`(每个对应一个模型)
|
||
|
||
### 1.2 为什么选择“单个 multi-model application”
|
||
|
||
Ray Serve 支持 multi-app,但在 dev/docker 场景下多个 app 的 route_prefix 管理更复杂;同时 requirements 要求“多模型 application 部署”,因此 v3.8 采用:
|
||
|
||
- 一个固定的 app:`argus_llm_app`(名字可配置)
|
||
- route_prefix 固定为 `/`(对外暴露 `/v1/...` OpenAI 接口)
|
||
- 每个模型对应一个 `LLMConfig`,通过 `model_id` 区分(即 OpenAI API 里的 `model` 字段)
|
||
|
||
这样对用户而言最直观:
|
||
|
||
- base_url 固定:`http://<host>:8000/v1`
|
||
- `model=` 选择不同模型(`/v1/models` 自动列出)
|
||
|
||
## 2. Ray Serve 部署策略(dev/h1 约束)
|
||
|
||
### 2.1 HTTP 入口端口与 docker compose
|
||
|
||
Ray Serve 默认 HTTP 端口是 `8000`。v3.8 约定:
|
||
|
||
- 在 **head 容器** 映射 `8000:8000`
|
||
- API server 仍在 `8080`
|
||
- Ray Dashboard 在 `8265`
|
||
|
||
原因:在单机多容器 docker 环境里,如果让 proxy “每个节点都起”,会出现多个容器同时想绑定同一个 host 端口的问题(不可行)。因此 v3.8 推荐:
|
||
|
||
- Serve proxy 位置设为 **HeadOnly**(只在 head 上提供 HTTP 入口)
|
||
- GPU replica 仍运行在 worker 上(proxy 只转发,不跑推理)
|
||
|
||
> 需要注意:
|
||
> - Serve 的 HTTP 配置(host/port/proxy_location)是 **Ray 集群全局配置**,启动后无法动态修改,因此应当在平台启动时一次性设定并持久化。
|
||
> - proxy Actor 需要 CPU 资源;head 节点的 `num-cpus=0` 策略可能需要在 v3.8 做小幅调整(例如给 head 保留少量 CPU),但仍通过 `entrypoint_resources` 确保训练 driver 不会被调度到 head。
|
||
|
||
#### 2.1.1 compose 预期改动(v3.8 实现时落地)
|
||
|
||
- `src/mvp/docker-compose.yaml`(ray_head)新增:
|
||
- `ports: - "8000:8000"`
|
||
|
||
> worker 容器不暴露 8000(避免 host 端口冲突),由 head proxy 统一对外提供入口。
|
||
|
||
### 2.2 启动/配置方式(Python SDK 优先)
|
||
|
||
v3.8 采用 Ray Serve Python SDK:
|
||
|
||
- `ray.init(address="auto")`
|
||
- `serve.start(proxy_location="HeadOnly", http_options={"host":"0.0.0.0","port":8000})`(一次性全局配置)
|
||
- `serve.run(app, name=<app_name>, route_prefix="/")`
|
||
- `serve.delete(name=<app_name>)`(必要时)
|
||
- `serve.status()` 查询集群/应用状态
|
||
|
||
理由:
|
||
|
||
- 避免在平台内部引入额外 REST client 依赖(并减少跨版本 REST schema 不稳定风险)
|
||
- API server 本身运行在 head 容器内,可直接 `ray.init(address="auto")` 连接现有集群
|
||
|
||
> 另:Ray Dashboard 暴露 Serve REST API(`PUT /api/serve/applications/` 等)可作为备选方案,但 v3.8 先不以它为主通路。
|
||
|
||
### 2.3 依赖与镜像假设
|
||
|
||
v3.8 依赖:
|
||
|
||
- `ray[serve]`(Serve Controller/Proxy)
|
||
- `ray[llm]`(Ray Serve LLM 的 `ray.serve.llm` 模块)
|
||
- vLLM(推理引擎)
|
||
|
||
由于 v3.7 已切换到 `verlai/verl:vllm011.latest`,预期镜像内包含 vLLM;但 `ray.serve.llm` 是否开箱即用需要在实现阶段确认。
|
||
若缺失,v3.8 将在 `argus-ray-node` 镜像构建阶段补充 `pip install "ray[serve,llm]"`(或按官方建议的最小依赖)并做版本锁定。
|
||
|
||
### 2.4 Serving 配置(dev.yaml)
|
||
|
||
v3.8 新增一段 serving 配置,至少包含:
|
||
|
||
```yaml
|
||
serving:
|
||
serve:
|
||
http_port: 8000 # 固定 8000
|
||
proxy_location: HeadOnly # dev/docker 下推荐
|
||
llm:
|
||
accelerator_type: H20 # dev 环境填写 H20(对应 ray.serve.llm.LLMConfig.accelerator_type)
|
||
```
|
||
|
||
说明:
|
||
- `accelerator_type` 是 Ray Serve LLM 的 `LLMConfig.accelerator_type` 字段,用于表达“该模型运行在哪类加速卡上”。在 dev/h1 环境我们固定为 `H20`。
|
||
- v3.8 不把 `accelerator_type` 暴露给普通用户编辑(避免误配);由部署环境配置统一决定。
|
||
|
||
## 3. 模型配置与资源映射
|
||
|
||
### 3.1 关键配置对象:`ray.serve.llm.LLMConfig`
|
||
|
||
每个模型部署由一个 `LLMConfig` 描述,关键字段(v3.8 用到的子集):
|
||
|
||
- `model_loading_config`
|
||
- `model_id`: 对外展示/请求时用的模型名(唯一 key)
|
||
- `model_source`: HF repo id / S3 / **local path**
|
||
- `accelerator_type`
|
||
- 从 `dev.yaml` 的 `serving.llm.accelerator_type` 读取(dev/h1: `H20`)
|
||
- `deployment_config`
|
||
- `num_replicas` 或 `autoscaling_config`(v3.8 先用固定 `num_replicas`)
|
||
- `ray_actor_options`(CPU/资源约束)
|
||
- `engine_kwargs`
|
||
- vLLM 相关参数(`max_model_len`、`gpu_memory_utilization` 等)
|
||
- `placement_group_config`
|
||
- 控制 vLLM engine workers 使用的资源 bundle(用于多 GPU / 跨节点)
|
||
- `runtime_env`
|
||
- 注入 HF cache、离线开关等环境变量
|
||
|
||
### 3.2 GPU 张数(gpus_per_replica)如何落到 LLMConfig
|
||
|
||
v3.8 把用户输入的:
|
||
|
||
- `gpus_per_replica = N`
|
||
|
||
映射为:
|
||
|
||
- `engine_kwargs.tensor_parallel_size = N`(单机/跨机张量并行,Ray Serve LLM 官方示例写法)
|
||
- `placement_group_config = {"bundles": [{"GPU": 1, "CPU": <cpu_per_gpu>}] * N, "strategy": "PACK"}`
|
||
|
||
并在 `engine_kwargs` 中保留 vLLM 其他参数(`max_model_len`、`gpu_memory_utilization` 等)。
|
||
|
||
> 兼容性说明:Ray Serve LLM/Serve LLM 仍处于快速演进阶段;v3.8 会以我们线上实际 Ray 版本为准做最小适配与回归测试。
|
||
|
||
### 3.2.1 跨节点场景(N > 单机 GPU)
|
||
|
||
Ray Serve LLM 默认使用 `PACK` 策略,优先把 GPU worker 放在尽量少的节点上;如果单机放不下,会自动 spill 到其它节点,从而支持跨节点张量并行(TP)部署。
|
||
|
||
### 3.3 replica 数(num_replicas)
|
||
|
||
v3.8 默认:
|
||
|
||
- `num_replicas = 1`
|
||
|
||
允许用户在 UI 中设置为 `>=1`。
|
||
多 replica 会线性消耗 GPU(`num_replicas * gpus_per_replica`),需要做资源预检查。
|
||
|
||
### 3.4 模型路径与宏替换(common / user)
|
||
|
||
v3.8 支持两类模型来源:
|
||
|
||
1) **common**
|
||
- 典型为 `/private/hf/...`(共享 HF cache / snapshot)
|
||
|
||
2) **user**
|
||
- `/private/users/<user_id>/models/...`
|
||
- 以及用户训练输出(例如 `jobs/<sid>/checkpoints/.../huggingface`)
|
||
|
||
为保证 UI 易用,沿用平台已有的宏语义:
|
||
|
||
- `$HOME` → `/private/users/<user_id>`
|
||
- `$HOME/common/hf` → `/private/hf`
|
||
|
||
并进行路径校验:
|
||
|
||
- 允许前缀:`/private/hf`、`/private/users/<user_id>/`
|
||
- 拒绝:越权访问其他用户目录、或访问系统敏感路径
|
||
|
||
### 3.5 离线模式(避免 HF mirror 429)
|
||
|
||
v3.7 训练侧已验证 `HF_HUB_OFFLINE=1` 的必要性。v3.8 Serving 侧同样默认注入:
|
||
|
||
- `HF_HOME=/private/hf`
|
||
- `HUGGINGFACE_HUB_CACHE=/private/hf/hub`
|
||
- `TRANSFORMERS_CACHE=/private/hf/transformers`
|
||
- `HF_HUB_OFFLINE=1`
|
||
- `HF_ENDPOINT=https://hf-mirror.com`(可保留,但离线模式下不应触发网络)
|
||
|
||
并建议用户在 ServingSpec 中尽量填写 **local path** 作为 `model_source`,而不是直接 repo id。
|
||
|
||
## 4. 平台数据模型(SQLite)
|
||
|
||
新增两张主表:
|
||
|
||
### 4.1 `serve_models`
|
||
|
||
每一行代表一个“声明式模型部署”:
|
||
|
||
- `model_key`(平台内部唯一 ID,便于重命名/去重)
|
||
- `user_id`
|
||
- `model_id`(对外 OpenAI model 名称,要求 per-app 唯一)
|
||
- `model_source`(本地路径或 repo id,存 resolved 后的结果)
|
||
- `num_replicas`
|
||
- `gpus_per_replica`
|
||
- `engine_kwargs_json`(可选)
|
||
- `state`:`QUEUED | DEPLOYING | RUNNING | FAILED | DELETING | DELETED`
|
||
- `serve_app_name`(默认 `argus_llm_app`)
|
||
- `created_at / updated_at`
|
||
- `error_summary`
|
||
|
||
### 4.2 `serve_events`
|
||
|
||
记录关键事件与排障信息(类似 task_events):
|
||
|
||
- `id`
|
||
- `model_key`
|
||
- `event_type`(DEPLOY_REQUESTED/DEPLOY_APPLIED/STATUS_SYNC/DELETE_REQUESTED/...)
|
||
- `payload_json`
|
||
- `created_at`
|
||
|
||
## 5. API 设计(新增)
|
||
|
||
在现有 `Authorization: Bearer <user_token>` 的认证体系下,新增 Serving API(路径仅示意,具体在实现时与现有 `api/v2` 对齐)。
|
||
|
||
### 5.1 用户接口
|
||
|
||
- `POST /api/v2/serve/models`
|
||
- body: YAML 或 JSON(v3.8 先用 YAML 与现有 TaskSpec 一致)
|
||
- 创建/更新(upsert)一个模型配置,进入 `QUEUED`
|
||
- `GET /api/v2/serve/models`
|
||
- 列出当前用户的模型列表(含 state、资源、endpoint)
|
||
- `GET /api/v2/serve/models/{model_key}`
|
||
- 详情:完整 spec + 最近事件 + Serve status 摘要
|
||
- `PATCH /api/v2/serve/models/{model_key}`
|
||
- 修改 `num_replicas`、或 engine_kwargs(可选)
|
||
- `DELETE /api/v2/serve/models/{model_key}`
|
||
- 下线模型(进入 `DELETING`)
|
||
|
||
### 5.2 系统接口(admin)
|
||
|
||
- `GET /api/v2/serve/status`(admin)
|
||
- 返回 `serve.status()` 的摘要(集群级 / app 级)
|
||
|
||
### 5.3 对外推理 endpoint
|
||
|
||
固定输出到 UI/接口中:
|
||
|
||
- `openai_base_url = http://<host>:8000/v1`
|
||
- 支持:
|
||
- `/v1/chat/completions`
|
||
- `/v1/completions`
|
||
- `/v1/embeddings`
|
||
- `/v1/models`
|
||
|
||
> v3.8 不做额外网关与鉴权(保持与现有 dev 环境一致);若后续需要,可在 v3.9+ 引入 token 校验/反向代理。
|
||
|
||
### 5.4 `model_id` 前缀策略(user_id-)
|
||
|
||
为避免多用户冲突并保持可读性:
|
||
|
||
v3.8 采用“**user_id + 日期小时分钟**”作为稳定前缀,以降低冲突并便于快速定位创建时间:
|
||
|
||
- 用户在 UI/API 中仅填写 `model_id_suffix`(或仍用字段名 `model_id`,但语义为 suffix)
|
||
- 平台计算实际对外 `model_id`:
|
||
- `prefix = f"{user_id}-{YYYYMMDDHHMM}"`
|
||
- `model_id = f"{prefix}-{model_id_suffix}"`
|
||
- 在列表/详情中同时展示:
|
||
- `model_id_suffix`(用户输入)
|
||
- `model_id_prefix`(平台生成,例如 `alice-202601061235`)
|
||
- `model_id`(对外 OpenAI 名称)
|
||
|
||
## 6. 后台执行模型(Serving Reconciler)
|
||
|
||
v3.8 参考任务 scheduler 的模式,引入一个轻量的 reconciler:
|
||
|
||
- tick 周期(例如 5s)
|
||
- 每次 tick:
|
||
1) 拉取 DB 中 `QUEUED/DEPLOYING/RUNNING/DELETING` 的模型
|
||
2) 调用 `serve.status()` 读取当前 app 及 deployments 状态
|
||
3) 若存在 `QUEUED` 或需要变更的模型:构建新的 multi-model app(包含全部 `RUNNING/DEPLOYING/QUEUED` 的模型配置)并 `serve.run(...)`
|
||
4) 若存在 `DELETING`:从 app 配置中移除对应模型,并 `serve.run(...)` 应用变更
|
||
5) 更新每个模型的 state(依据 Serve status)
|
||
|
||
重要行为说明(multi-model app 的代价):
|
||
- 每次“新增/删除/改 replicas”都会触发对同一个 app 的一次 `serve.run(...)` 更新;
|
||
- Ray Serve 会尽量做增量更新,但在某些版本/配置下可能导致 ingress/router 短暂重启;
|
||
- v3.8 先接受该代价(满足需求闭环优先);若后续需要“删除某模型不影响其它模型”,可演进为“每模型一个 app + 单独 route_prefix”的方案。
|
||
|
||
资源预检查:
|
||
- 在 apply 前使用 `ray.available_resources()` 做粗粒度 GPU 预检查:
|
||
- 需要 GPU 总量 = `sum(num_replicas * gpus_per_replica)`(仅对“新增/扩容的差量”更精确)
|
||
- 若不足:
|
||
- 模型保持 `QUEUED`,记录事件 `PENDING_RESOURCES`
|
||
- 用户 UI 显示“资源不足,等待释放”
|
||
|
||
> v3.8 不引入更复杂的抢占/优先级。Serving 与 Training 会竞争 GPU;用户需要自行规划资源(或后续版本引入统一调度)。
|
||
|
||
## 7. WebUI 设计(新增 Serving 页面)
|
||
|
||
新增侧边栏入口:**Serving**
|
||
|
||
### 7.1 Serving 列表页
|
||
|
||
- 展示字段:
|
||
- model_id
|
||
- user_id(仅 admin 可见)
|
||
- replicas / gpus_per_replica / total_gpus
|
||
- state(RUNNING/DEPLOYING/QUEUED/FAILED)
|
||
- 操作:Scale(修改 replicas)、Delete
|
||
|
||
### 7.2 Serving 创建/编辑页
|
||
|
||
两种模式(与 New Task 类似,先做 YAML 模式即可):
|
||
|
||
示例 YAML(v3.8):
|
||
|
||
```yaml
|
||
model_id: qwen-0.5b
|
||
model_source: $HOME/common/hf/hub/models--Qwen--Qwen2.5-0.5B-Instruct/snapshots/<sha>
|
||
num_replicas: 1
|
||
gpus_per_replica: 1
|
||
# engine_kwargs:
|
||
# max_model_len: 8192
|
||
# gpu_memory_utilization: 0.9
|
||
```
|
||
|
||
### 7.3 Serving 详情页
|
||
|
||
- 完整配置(resolved spec)
|
||
- Serve status 摘要(deployments 状态、replica 健康)
|
||
- OpenAI 调用示例(python openai client)
|
||
|
||
## 8. 验收标准(v3.8)
|
||
|
||
1) 部署:
|
||
- 一键部署一个模型(1 replica、1 GPU)成功,状态变为 RUNNING
|
||
- `/v1/models` 可列出该模型
|
||
|
||
2) 扩缩容:
|
||
- 修改 `num_replicas` 生效(Serve status 看到副本数变化)
|
||
|
||
3) 多模型:
|
||
- 同一个 app 内能同时部署 2 个模型(不同 model_id)
|
||
- 通过 OpenAI 接口用不同 `model=` 请求可得到响应
|
||
|
||
4) 下线:
|
||
- 删除某模型后 `/v1/models` 不再出现
|
||
|
||
5) 模型路径:
|
||
- 支持 `/private/hf/...`(common)与 `/private/users/<user>/...`(user)两类本地路径
|
||
|
||
6) 资源不足可解释:
|
||
- 当 GPU 不足时,模型进入 `QUEUED` 并在 UI/详情中提示“资源不足”
|
||
|
||
## 9. 待确认点(请你评审时确认)
|
||
|
||
已确认(来自评审):
|
||
|
||
1) 推理端口固定使用 `8000`(Ray Serve 默认端口)。
|
||
2) 对外暴露的 OpenAI 接口 **不与现有 token 体系绑定**(v3.8 不做推理侧鉴权)。
|
||
3) `model_id` 命名规则:平台统一加 `user_id + 日期小时分钟` 前缀,用户在 UI 里只填写后缀部分。
|
||
|
||
> 说明:这样可以避免跨用户 model_id 冲突,同时在 OpenAI API 的 `model=` 字段上自然可读。
|