# 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://: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=, route_prefix="/")` - `serve.delete(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": }] * 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//models/...` - 以及用户训练输出(例如 `jobs//checkpoints/.../huggingface`) 为保证 UI 易用,沿用平台已有的宏语义: - `$HOME` → `/private/users/` - `$HOME/common/hf` → `/private/hf` 并进行路径校验: - 允许前缀:`/private/hf`、`/private/users//` - 拒绝:越权访问其他用户目录、或访问系统敏感路径 ### 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 ` 的认证体系下,新增 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://: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/ 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)两类本地路径 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=` 字段上自然可读。