# 从学习到生产:本地 AI 服务的正式部署 ```{contents} ``` 前两篇搭好的服务——[本地 AI 服务](./local-ai-service.md) 和 [Agent](./agent.md)——都是"学习形态":单用户、无鉴权、手动启动、挂了重启。这一篇回答:**把它正式部署出去,交给很多用户持续使用时,应该怎么做。** ```{toctree} :maxdepth: 2 ``` ## 学习环境与生产环境的差别 | 维度 | 学习环境 | 生产环境 | |------|---------|---------| | 用户 | 你自己 | 多人并发,可能全天候 | | 并发 | 一次一个请求 | 数十上百请求同时 | | 可用性 | 挂了重启就是 | SLA,如 99.9% | | 安全 | 无鉴权,绑 localhost | 鉴权、内网、审计 | | 性能目标 | "能用" | TTFT/TPOT 有明确指标 | | 变更 | 随便折腾 | 灰度发布、可回滚 | | 监控 | 看终端日志 | 指标、报警、可视化 | 生产化的核心矛盾只有一个:**GPU 一次只能算一份活,而用户是同时来的。** 后面所有架构设计,本质都是在回答"怎么把有限的 GPU 算力公平、高效、可靠地分给所有用户"。 ## 为什么学习用的服务不能直接上生产 llama.cpp / Ollama 默认**一次只服务一个请求**(可以开并行参数,但只是简单的请求级调度)。第二个用户到达时只能排队: ``` 请求1: |-------- 生成中 --------| 请求2: |-------- 生成中 --------| 请求3: |-------- 生成中 --------| ``` 排队本身还能忍,真正浪费的是:**decode 阶段 GPU 计算单元大部分时间在等显存搬运数据,算力利用率只有个位数**。一个请求慢悠悠地吐 token 时,GPU 绝大部分算力闲置着。 ```{important} 生产推理引擎的核心思路:**让几十个请求同时 decode,用并发的请求把空闲算力填满**。这叫 continuous batching(连续批处理)——新请求随到随加,完成的请求立刻让位,不互相等待。 ``` ### continuous batching 与传统批处理 传统批处理等一批请求全部完成才换下一批(短的等长的);continuous batching 则: ``` 时间轴 → t1 t2 t3 t4 t5 请求A: [======================] 请求B: [====================] 请求C: [========] ↑ C 完成立即让出位置, 新请求 D 立刻加入 ``` > 图示:请求 C 完成后它的显存和算力马上让给新来的请求,不需要等 A、B 结束。批的边界消失了,"批"变成了一个持续流动的池子。 ## 生产推理引擎:vLLM [vLLM](https://docs.vllm.ai/) 是当前生产部署最主流的开源引擎,内置 continuous batching,还解决了另一个生产问题——KV cache 的显存管理。 ### PagedAttention:KV cache 的分页管理 回忆第一篇:每个请求的 KV cache 大小由上下文长度决定。如果每个请求都按最大上下文预分配,大量显存被浪费,而且频繁分配/释放会产生碎片。 PagedAttention 的解法类似操作系统的虚拟内存分页: 1. 把 KV cache 切成固定大小的 page; 2. 请求需要多少就分配多少页(不按最大上下文预分配); 3. 释放时按页回收,不产生碎片。 这样显存利用率显著提高,`--gpu-memory-utilization` 也敢安全地拉高。 ### 启动 vLLM ```bash pip install vllm vllm serve Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.90 \ --max-num-seqs 128 \ --api-key sk-prod-xxxxxxxx ``` 参数逐个解释: | 参数 | 含义 | |------|------| | `--max-model-len` | 允许的最大上下文长度。它和 `--max-num-seqs` 共同决定 KV cache 能容纳多少并发请求 | | `--gpu-memory-utilization` | 显存可用比例(0.90 = 90%)。PagedAttention 让这个值可以安全设高 | | `--max-num-seqs` | 最大并发请求数。超过则排队 | | `--api-key` | 鉴权密钥,请求时必须带 `Authorization: Bearer sk-prod-xxxx` | | `--tensor-parallel-size` | 模型切分到几张卡(单卡 1,双卡 2)。大模型(70B+)必须多卡 | vLLM 同样暴露 OpenAI 兼容接口,上一篇的 Agent 代码把 `base_url` 换过来即可无缝切换——这正是"OpenAI 兼容"标准的价值。 ### 引擎选型对比 | 引擎 | 定位 | 适合场景 | |------|------|---------| | llama.cpp / Ollama | 单机、低门槛、消费级硬件 | 学习、个人工具、边缘设备 | | vLLM | 高吞吐生产推理,生态最成熟 | 标准生产环境(本篇主线) | | SGLang | 与 vLLM 同档,调度更激进 | 极致吞吐场景 | | TGI(HuggingFace) | 老牌生产推理 | 深度绑定 HF 生态的团队 | ```{note} 技术选型不用一上来就纠结:学习/单机用 Ollama,生产默认 vLLM,遇到吞吐瓶颈再对比 SGLang。三者接口几乎一致,切换成本主要是部署成本,不是代码成本。 ``` ## 生产架构 生产环境的分层架构: ```{mermaid} flowchart TB subgraph 客户端 U1[Web 应用] U2[Agent 进程] U3[内部脚本] end subgraph 接入层 GW[API 网关
nginx / LiteLLM
鉴权 限流 路由] end subgraph 推理层 V1[vLLM 副本 1
GPU 0] V2[vLLM 副本 2
GPU 1] V3[vLLM 副本 3
GPU 2] end subgraph 支撑层 MON[Prometheus + Grafana
监控报警] REG[模型仓库
权重版本管理] end U1 --> GW U2 --> GW U3 --> GW GW --> V1 GW --> V2 GW --> V3 V1 -. 指标 .-> MON V2 -. 指标 .-> MON V3 -. 指标 .-> MON REG -. 权重只读挂载 .-> V1 REG -. 权重只读挂载 .-> V2 REG -. 权重只读挂载 .-> V3 ``` > 图示:三层职责分明——网关管"谁能进来、进来多少",推理层管"生成 token",支撑层管"看得见、换得了"。推理层无状态(权重只读挂载),所以可以随意扩缩容。 各层职责: | 层 | 职责 | |----|------| | 接入层 | 鉴权(API key)、限流、路由、重试、TLS 终结 | | 推理层 | 纯推理。无状态,可水平扩展 | | 支撑层 | 监控报警、模型版本管理、日志 | ## 容量规划:一台 GPU 能服务多少用户 ### 显存怎么分 第一篇给过公式,生产场景把它细化: ``` 显存 = 模型权重 + 单请求 KV cache × 并发数 + 激活值/碎片 ``` 以 24GB 显卡 + 7B 模型(fp16)为例: ``` 权重: 14 GB KV cache 可用: 24 × 0.90 - 14 - 约2GB(激活/碎片) ≈ 5.6 GB 单请求 KV cache(8192 上下文): 约 450 MB 最大并发 ≈ 5.6 GB / 450 MB ≈ 12 ``` 所以 `--max-num-seqs` 不是拍脑袋设的,是显存算出来的。想提高并发,三个方向: 1. **权重量化**(AWQ/GPTQ 等 4bit 方案):权重从 14GB 压到 ~4.4GB,KV cache 可用量从 5.6GB 涨到 ~15GB,并发直接翻三倍; 2. **减小 `--max-model-len`**:短问答场景很有效; 3. **换大显存卡**:A100/H100 的 80GB 相比 24GB,并发翻几倍。 ### 吞吐估算 ``` 吞吐 ≈ 单请求生成速度 × 并发数 ``` 7B 模型 fp16 在 24GB 卡上,单请求约 30~50 token/s。按 10 个并发、平均每次回答 1000 token 算: ``` 10 × 40 token/s = 400 token/s 400 / 1000 = 0.4 次对话/秒 ≈ 每天 3.5 万次对话 ``` 这是"能不能扛住"的粗算。**真实容量必须压测**——用并发请求打满服务,实测 TTFT、TPOT、吞吐,而不是信估算。 ### TTFT 与 TPOT:生产环境的两个 SLA 指标 第一篇埋过这两个概念,生产环境里它们是服务等级目标: - **TTFT**(Time To First Token):从请求到第一个 token,由 prefill 决定,受输入长度影响最大。典型目标:< 1 秒(2K token 输入); - **TPOT**(Time Per Output Token):生成阶段每个 token 的间隔,由并发压力决定。典型目标:< 100ms。 ```{tip} 流式输出在生产环境不只是体验优化:TTFT 达标时,用户立刻看到内容在生成,"在动"本身就是可用性的一部分。非流式的等待体感是 1+1=3。 ``` ## 安全 生产 AI 服务的安全有几层,从外到内: 1. **网络**:推理服务只绑内网,不直接暴露公网。TLS 终结在网关; 2. **鉴权**:vLLM `--api-key` 是最后一道闸,生产一般由网关统一做 API key 管理、配额; 3. **提示词注入**:Agent 场景特有(上篇讲过原理)。生产对策:工具最小权限 + 沙箱 + 高危操作人审 + 工具输出校验; 4. **数据**:本地部署的最大价值就是数据不出内网。仍然要做的:日志脱敏(用户输入进入日志前先过滤敏感信息)、保留审计记录。 ## 监控与可观测性 vLLM 自带 Prometheus 格式的 `/metrics` 端点,配一套 Prometheus + Grafana 即可看到服务全貌: | 指标 | 含义 | 建议报警条件 | |------|------|-------------| | `vllm:time_to_first_token_seconds` | TTFT | P95 > 2s | | `vllm:time_per_output_token_seconds` | TPOT | P95 > 100ms | | `vllm:num_requests_waiting` | 排队中的请求 | 持续 > 0 | | `vllm:num_requests_running` | 正在处理的请求 | 持续等于上限 | | `vllm:gpu_cache_usage_perc` | KV cache 使用率 | > 90% | | `vllm:prompt_tokens_total` / `vllm:generation_tokens_total` | 输入/输出流量 | 容量规划依据 | 网关层(nginx)再补上请求数、错误率、P99 延迟。两层数据放一起,出问题时才能分清:是模型慢了(TTFT/TPOT 上升),还是网关挡了(限流、连接问题)。 ```{note} 报警阈值是压测出来的,不是抄来的。上线前先打一轮压测,记录饱和点,再把报警设在饱和点的 70%~80%。 ``` ## 高可用与发布 - **多副本**:推理服务无状态,天然适合多副本 + 负载均衡。一台 GPU 挂了,网关自动踢掉它,流量走其余副本; - **健康检查**:vLLM 提供 `/health` 端点,容器编排用它做存活/就绪探测; - **灰度发布**:换新模型时,先切 10% 流量到新版本,对比 TTFT/TPOT 和输出质量,没问题再逐步全量。模型的"质量回归"靠指标看不太出来,灰度期要人工抽查; - **回滚**:权重以只读方式挂载、模型名版本化。回滚 = 换回旧挂载,一分钟的事。永远不要在发布新模型时删除旧模型文件。 ## Kubernetes 部署示例 生产环境一般跑在 Kubernetes 上,最小部署长这样: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: vllm-qwen spec: replicas: 2 # 两个 GPU 副本 selector: matchLabels: app: vllm-qwen template: metadata: labels: app: vllm-qwen spec: containers: - name: vllm image: vllm/vllm-openai:latest args: - "Qwen/Qwen2.5-7B-Instruct" - "--max-model-len" - "8192" - "--gpu-memory-utilization" - "0.90" - "--max-num-seqs" - "128" env: - name: VLLM_API_KEY valueFrom: secretKeyRef: name: vllm-secret key: api-key ports: - containerPort: 8000 resources: limits: nvidia.com/gpu: 1 # 每副本一张卡 readinessProbe: httpGet: path: /health port: 8000 --- apiVersion: v1 kind: Service metadata: name: vllm-qwen spec: selector: app: vllm-qwen ports: - port: 8000 targetPort: 8000 ``` 要点: - 权重由镜像内置或启动时拉取到共享存储,推理层不落任何可变数据; - 扩容方式与普通无状态服务相同(replicas +1)。区别是 **GPU 机器贵且少,扩缩容通常手动做**——小规模用 HPA 按 GPU 利用率自动扩容反而危险; - 网关、监控、模型仓库作为独立组件部署,不随推理副本变动。 ## 成本 生产部署的账要算清楚。以 7B 模型为例: | 方案 | 硬件 | 单卡约 | 适合 | |------|------|--------|------| | 消费级 GPU 自建 | RTX 4090(24GB) | 购买约 1.5~2 万(一次性) | 小团队内部使用 | | 云 GPU 按需 | A10/A100(24~80GB) | 每小时数元~数十元 | 波动负载、快速验证 | | 云 GPU 包月/年 | A100/H100 | 每月数千~数万 | 稳定生产负载 | 省钱的两个方向: - **量化**:4bit 权重(AWQ/GPTQ)把 7B 权重从 14GB 压到 ~4.4GB,同样的卡能服务更多并发,相当于省钱; - **投机解码**(speculative decoding):小模型"起草"、大模型"校对",生成速度可翻倍。vLLM/SGLang 都支持,作为优化项后置。 ```{warning} 消费级显卡(4090 等)与数据中心卡(A100/H100)的差距不只是算力:数据中心卡显存更大、支持虚拟化、厂商稳定性更好,且云厂商对消费卡的 SLA 几乎为 0。内部小规模使用消费卡没问题,对外服务请用数据中心卡。 ``` ## 上线检查清单 - [ ] 鉴权开启,服务只绑内网 - [ ] 监控与报警就位,阈值来自压测 - [ ] 至少两个副本,验证过故障切换 - [ ] 灰度发布流程演练过,旧模型未删除 - [ ] 压测出真实的 TTFT/TPOT/吞吐,与容量规划核对 - [ ] Agent 工具权限已按最小权限收口,沙箱就位 - [ ] 日志脱敏、审计开启 - [ ] 成本预估与预算对齐 ## 总结 三篇文章串起来是一条完整的路: 1. [从0搭建本地 AI 服务](./local-ai-service.md):理解服务是什么——模型文件、推理引擎、HTTP 接口; 2. [搭建 Agent](./agent.md):给模型装上工具和循环,让它从"会说话"到"会做事"; 3. 本篇:把学习形态推向生产——换连续批处理引擎、分层架构、监控、安全、成本。 原理上,生产环境没有新魔法:还是那套"权重 + 引擎 + OpenAI 兼容接口",变化的只是并发、可靠性和治理。