📑 本页目录(点开跳转)
20 · 推理服务化
⏱ 48 分钟 | ⭐ 从"能跑"到"能上线"
🎯 一句话
前面五章讲的是怎么让单机跑得快。 这一章讲怎么把它变成一个能承诺 SLO、能扛住流量、能算清账的服务。
🧭 一、先选引擎:这一步常常就是 5-10 倍
第 15 章把推理优化的第一步写成了"用专门的推理引擎,别拿 generate() 上生产,光这一步常常 5-10 倍",
但没说换成哪个。这一节补上。
⚠️ 先说时效:推理框架半年一变,功能互相抄,性能榜半年就翻篇。 所以下面只写定位和取舍判据,不写版本号、不写 API、不写 benchmark 数字。 记结构,别记名字。
七个常见选项,各自的位置
| 引擎 | 一句话定位 | 最擅长 | 部署代价 |
|---|---|---|---|
| vLLM ⭐ | 通用的默认选项,社区最大、模型和量化格式支持面最广 | 通用在线服务 | 低:装上给一条命令,直接是 OpenAI 兼容接口 |
| SGLang ⭐ | 面向复杂调用结构的引擎,核心是 RadixAttention | 前缀高度重复的负载:Agent、多轮、多候选、约束解码 | 低,和 vLLM 同量级 |
| TensorRT-LLM | NVIDIA 官方的极致性能路线,要先把模型编译成引擎文件 | 单一模型、形状稳定、把最后一滴性能榨出来 | ⚠️ 高:要编译、要对齐版本,换模型 / 换卡 / 改并行度都要重编 |
| TGI | HuggingFace 的服务化方案,和 HF 生态贴得最紧 | 模型来源、权限、工具链都已经在 HF 那一套里的团队 | 低 |
| LMDeploy | 国内团队做的引擎,量化链路和国产模型支持完整 | 主力模型是国内开源系列时 | 低 |
| llama.cpp ⭐ | C++ 路线,没有 NVIDIA 卡也能跑(CPU / Apple Silicon / 各种边缘设备),GGUF 格式的发源地 | 本地、离线、消费级硬件 | 低,但服务层要自己包 |
| Ollama | llama.cpp 之上的一层"能用就行"的包装,一条命令拉模型就跑 | ⭐ 本地开发、demo、个人使用 | 极低 |
其实只有四个问题要问
① 你要的是"跑起来",还是"扛住流量"?
跑起来 → Ollama(或 LM Studio)→ ⭐ 到此为止,别往下看了
② 有 NVIDIA 卡吗?
没有(CPU / Mac / 边缘)→ llama.cpp + GGUF 路线
③ 模型和硬件会变吗?
不变,而且延迟/成本已经是核心 KPI → TensorRT-LLM
(⚠️ 你要接受"每次换东西都重编译"这个代价)
会变(还在选模型、还在调) → 别碰编译型的
④ 你的请求之间前缀重合度高吗?
高(Agent、多轮、大量共享 few-shot) → SGLang
一般 → vLLM ⭐ 默认答案
⭐ TGI 和 LMDeploy 属于"生态贴合"型的选择: 不是因为它们更快,而是因为你已经在那套生态里了,接起来省事。 选型的时候,"团队已经会什么"是一个合法的、而且经常被低估的判据。
⚠️ 一个反直觉但很重要的建议:先别去挑性能最高的那个。 主流引擎之间的差距通常在 20-50% 这个量级; 而你有没有开对连续批处理、前缀缓存、量化(第 17、16、18 章),差的是几倍。 💥 大部分"框架 A 比 B 快 3 倍"的评测,拆开看都是配置没对齐 —— 一边开了连续批处理一边没开,那测的不是引擎。
顺序:先把配置调对,再考虑换引擎。
🌳 RadixAttention:把前缀共享从"一条"推进到"一棵树"
这个机制值得单独讲,因为它和你已经学过的东西是同一条线上的下一步。
第 16 章的前缀缓存解决的是"所有请求共享同一个系统提示词", 第 17 章的块共享让这件事在实现上几乎免费。 但它们的匹配方式是线性的:从头比对,命中一条前缀。
而真实的 Agent 负载长这样:
RadixAttention 做的就是:把 KV Cache 的索引组织成一棵基数树(radix tree,也就是压缩过的前缀树)。
· 树上每条边 = 一段 token 序列,对应一批 KV 块
· 新请求来了,沿树往下走,能匹配多远就复用多远
→ 只有【分叉之后】的那部分才需要重新 Prefill
· 缓存满了按 LRU 淘汰【叶子】
⭐ 只淘汰叶子,因为父节点还被别的分支用着
| 线性前缀缓存(第 16、17 章) | RadixAttention | |
|---|---|---|
| 匹配结构 | 线性:从头比对,命中一条前缀 | 树:分叉点自动被发现 |
| 淘汰 | 按块 LRU | 按叶子 LRU,感知父子依赖 |
| 收益最大的场景 | 固定的系统提示词 | 多轮 + 分叉:Agent、多候选生成、树搜索、共享 few-shot ⭐ |
⭐ 两者不是互斥的,思路完全一样("算过的 KV 别重算"), RadixAttention 是它在数据结构上的推进,而且各家引擎都在往这个方向收敛。
⚠️ 所以真正的判断依据不是"哪个框架有这个功能", 而是【你的请求之间到底有多少共享前缀】 —— 一点都不共享,两者都白搭。 这件事你可以在换引擎之前就自己统计出来。
💡 顺带一条容易漏掉的能力:如果你要在一个基座上热挂多个 LoRA 适配器 (多租户各自微调一份),主流引擎大多支持这类"多 LoRA 服务" —— 它省的是"每个租户开一份完整实例"的显存。有这个需求的话,选型时要单独确认。
📏 二、先定 SLO,再谈优化
⚠️ 最常见的错误:不定目标就开始调优
→ 你不知道调到什么程度算够,也不知道该牺牲什么
⭐ 一个推理服务的 SLO 至少要包含:
· TTFT P95 < 500ms (首字延迟)
· TPOT P95 < 50ms (每字延迟 → 20 tok/s,比阅读速度快)
· 可用性 99.9%
· 吞吐 ≥ X tokens/秒
不同场景的典型要求:
| 场景 | TTFT | TPOT | 吞吐 |
|---|---|---|---|
| 代码补全 | < 200ms ⭐ | < 30ms | 中 |
| 聊天机器人 | < 1s | < 50ms | 中 |
| Agent / 长推理 | 宽松 | < 30ms ⭐(输出很长) | 中 |
| 批量处理 | 无所谓 | 无所谓 | 最大化 ⭐ |
| 语音对话 | < 300ms | < 40ms | 低 |
🔑 定 SLO 的意义:它把"越快越好"变成一个可以做取舍的工程问题。 知道 TPOT 只要 < 50ms,你就可以放心地把 batch 调大去换吞吐 —— 在不定 SLO 的情况下,你会盲目地优化延迟到远超需求的程度,白白损失吞吐。
📊 三、压测:画出你的延迟-吞吐曲线
⭐ 这是容量规划的唯一依据:
并发数 → 吞吐(tok/s) TPOT P95 是否满足 SLO
1 140 7ms ✅
8 1000 12ms ✅
32 3500 28ms ✅
64 5200 45ms ✅ ⭐ 这就是你的容量
128 6800 95ms ❌ 超了
256 7500 210ms ❌
→ 单实例最大并发 = 64
→ 需要的实例数 = 峰值并发 / 64
# vLLM 自带的压测工具
python -m vllm.entrypoints.openai.api_server --model <model> &
python benchmarks/benchmark_serving.py \
--model <model> \
--dataset-name sharegpt \ # ⭐ 用真实分布的数据集,别用固定长度
--request-rate 10 \
--num-prompts 1000
⚠️ 压测最常见的错误:用固定长度的输入输出。 真实流量的长度分布方差极大(第 17 章), 固定长度会让连续批处理的收益被严重高估。 一定要用 ShareGPT 这类真实分布,或者你自己的线上日志回放。
🏗️ 四、部署架构
路由策略的选择(比想象中重要):
| 策略 | 说明 |
|---|---|
| 轮询 | 最简单,但忽略了实例的实际负载 |
| 最少请求数 | 比轮询好 |
| 最少待处理 token ⭐ | 更准确——一个 4000 token 的请求 ≠ 一个 50 token 的请求 |
| 前缀感知路由 ⭐⭐ | 把有相同前缀的请求路由到同一实例 → 前缀缓存命中率大涨 |
⭐ 前缀感知路由是一个容易被忽略的大优化: 如果你有多个租户,每个租户有自己的长系统提示词, 把同一租户的请求固定路由到同一实例,前缀缓存命中率能从 ~30% 提到 90%+。
💰 五、算清成本
每百万 token 成本 = GPU 小时单价 ÷ (吞吐 tok/s × 3600) × 1e6
例:A100 按 $2/小时,实测吞吐 5000 tok/s
→ 2 / (5000 × 3600) × 1e6 = $0.11 / 百万 token
def cost_per_million(gpu_hourly, tokens_per_sec, n_gpus=1, utilization=0.6):
"""utilization: 平均负载率——⭐ 这个数字最容易被忽略"""
effective = tokens_per_sec * utilization
return gpu_hourly * n_gpus / (effective * 3600) * 1e6
print(f"${cost_per_million(2.0, 5000):.3f} / 百万 token") # $0.185
⭐ 注意
utilization那一项: 大部分服务的平均负载只有峰值的 30-60%(夜间几乎没流量)。 按峰值算容量、按平均算成本 —— 这两个数不能混。💡 降成本的三个方向: ① 提高吞吐(前面五章) ② 提高利用率(把离线批量任务填到低峰期)⭐ ③ 用更便宜的卡(量化后可能装得下)
🛡️ 六、生产必备的五件事
| 事项 | 做法 |
|---|---|
| 限流 | 按租户/API key 限 QPS 和 token 速率 ⭐ |
| 超时与取消 | 客户端断开要立刻停止生成并释放 KV Cache ⭐ |
| 优雅关闭 | 停止接新请求,等在跑的完成再退出 |
| 健康检查 | 不只是 ping,要真跑一次推理 |
| 降级 | 过载时切到小模型 / 降低 max_tokens / 排队 |
💥 "客户端断开不取消"是一个真实且昂贵的 bug: 用户关掉页面后,服务器还在为他生成 2000 个 token, 占着 KV Cache 槽位不放 —— 高并发下这会显著吃掉容量。 确认你的框架真的处理了 disconnect。
📈 七、该监控什么
⭐ 必须有的指标:
【延迟】TTFT / TPOT 的 P50、P95、P99 ← 看 P99,不是平均值
【吞吐】tokens/秒(输入和输出分开统计)⭐
【容量】正在运行的请求数 / 排队长度
【显存】KV Cache 使用率 ⭐⭐ ← 最重要的容量预警信号
【异常】抢占次数、OOM 次数、超时率
【业务】每租户的 token 消耗
⭐
KV Cache 使用率是最该盯的一个指标: 它持续 >90% 意味着你已经在抢占边缘 —— 再多一点流量就会开始踢请求,延迟会突然恶化。 它是比 CPU/GPU 利用率更早的预警信号。
⚠️ 为什么看 P99 不看平均:
推理延迟的分布是【长尾】的(长请求、抢占、排队)
平均值 30ms 可能对应 P99 = 800ms
→ 而用户体验是由 P99 决定的
🧭 八、上线检查清单
□ 引擎选过了(不是"团队一直用的那个",是按第一节四个问题选的)
□ SLO 定义清楚了(TTFT/TPOT/吞吐/可用性)
□ 用【真实分布】的数据压测过,画出了延迟-吞吐曲线
□ 根据曲线设了 max_num_seqs 上限(第 17 章)
□ 开了:连续批处理、前缀缓存、chunked prefill
□ 量化方案经过【业务数据】质量评估(第 18 章)
□ 客户端断开会取消生成
□ 限流和降级路径测试过
□ 监控里有 KV Cache 使用率和 P99 延迟告警
□ 做过一次故障演练(杀掉一个实例,看会怎样)
🔗 和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 第 15 章 TTFT/TPOT | SLO 的两个核心指标 ⭐ |
| 第 15 章 "换引擎常常 5-10 倍" | 换成哪个?本章第一节的横评和四问决策树就是答案 ⭐ |
| 第 16 章 前缀缓存 | 前缀感知路由让它命中率大涨;RadixAttention 是它在数据结构上的推进(本章第一节)⭐ |
| 第 17 章 max_num_seqs | 守 SLO 的旋钮 |
| 第 17 章 块共享 | 线性前缀共享的实现基础,和 RadixAttention 的树共享是同一个思路的两级 |
| 第 18 章 量化 | 降成本的主要手段 |
| 第 23 章 | 更系统的成本优化 |
| 《大模型全景导论》06 推理优化 | 那边是产品视角的取舍清单(该不该压、压到什么程度),本章是工程视角的选型与守 SLO。从那边过来的人,缺的正是这一章第一节的横评 |
| 《模型上线之后》05 该监控什么 三层监控 | 本章第七节只有引擎指标;"它给的答案还合理吗"这一层在那边,两层都要有 ⭐ |
| 《模型上线之后》18 成本与容量 容量规划 | 本章的成本公式只算 GPU;峰值容量、排队雪崩、"不做也是一个选项"在那边 ⭐ |
| ⭐⭐ 《AI 全栈》04 · 调用层 | ⭐ 分界:本章是「推理引擎怎么选、怎么守 SLO」,那一章是「应用怎么调它」 —— 超时(连接 vs 读取是两个数)、重试白名单、成本记账、缓存与不确定性、假客户端。引擎调好了,应用侧不做这些照样会 500 |
| 《AI 全栈》12 · 限流配额与成本护栏 | 本章算的是你自己跑引擎的成本;那一章管的是每个用户能花多少(限流 / 配额 / 全局熔断三层)。⭐ 用托管 API 时,那三层就是你唯一的成本闸门 |
| 《推荐算法》15 工程落地 降级策略 | 本章表里一行的"降级",在那边是完整一节 —— 降级路径必须设计,不是可选项 |
| 《大模型全景导论》07 本地部署与开源生态 该不该自建 | 那边回答"要不要自己部署",本章第一节回答"决定自建之后,用哪个引擎" ⭐ |
| 《智能体工程教程》11 检索与 RAG 提示词怎么拼 | 决定了你的请求之间有多少共享前缀 —— 而那正是 RadixAttention / 前缀缓存能不能生效的前提 ⭐ |
✅ 检查点
- 选推理引擎的四个问题是什么?默认答案是哪个?
- TensorRT-LLM 的代价是什么?什么情况下才值得付?
- 为什么说"先别去挑性能最高的那个"?大部分"框架 A 快 3 倍"的评测问题出在哪?
- RadixAttention 和普通前缀缓存的区别是什么?为什么它只淘汰叶子?
- 判断该不该关心 RadixAttention,真正要看的是什么?
- 为什么要先定 SLO 再优化?不定会怎样?
- 代码补全和 Agent 场景的 SLO 侧重有什么不同?
- 压测最常见的错误是什么?为什么它会误导你?
- 延迟-吞吐曲线怎么用来做容量规划?
- 四种路由策略里,哪个最容易被忽略但收益大?为什么?
- 成本公式里
utilization为什么重要? - "客户端断开不取消"为什么是昂贵的 bug?
- 最该盯的监控指标是哪个?为什么?
- 为什么看 P99 不看平均?
👀 答案
- ①要"跑起来"还是"扛住流量"(跑起来 → Ollama,到此为止)②有没有 NVIDIA 卡(没有 → llama.cpp + GGUF)③模型和硬件会不会变(不变且性能是 KPI → TensorRT-LLM)④请求之间前缀重合度高不高(高 → SGLang)。默认答案是 vLLM。TGI / LMDeploy 是"生态贴合"型的选择——"团队已经会什么"是一个合法且常被低估的判据。
- 它要先把模型编译成引擎文件,⚠️换模型、换卡、改并行度都要重编,还要对齐版本。只有当模型和硬件都稳定、而且延迟/成本已经是核心 KPI 时才值得付这个代价;还在选模型、还在调的阶段别碰编译型的。
- 因为主流引擎之间的差距通常只有 20-50%,而你有没有开对连续批处理、前缀缓存、量化,差的是几倍。💥 大部分"快 3 倍"的评测拆开看都是配置没对齐(一边开了连续批处理一边没开)。顺序:先把配置调对,再考虑换引擎。
- 普通前缀缓存的匹配是线性的(从头比对,命中一条前缀);RadixAttention 把 KV Cache 的索引组织成一棵基数树,分叉点是自动被发现的,沿树往下能匹配多远就复用多远,只有分叉之后的部分才重新 Prefill。只淘汰叶子是因为父节点还被别的分支用着。
- 看你的请求之间到底有多少共享前缀——不是"哪个框架有这个功能"。一点都不共享,两者都白搭;而这件事在换引擎之前就能自己统计出来。
- 因为不定目标就不知道调到什么程度算够、该牺牲什么。不定 SLO 会盲目优化延迟到远超需求的程度,白白损失吞吐。
- 代码补全 TTFT 极敏感(<200ms,用户等不了);Agent 输出很长,TPOT 是决定性的,TTFT 反而宽松。
- 用固定长度的输入输出。真实流量长度方差极大,固定长度会让连续批处理的收益被严重高估。要用 ShareGPT 这类真实分布或线上日志回放。
- 找到满足 SLO 的最大并发数(如 64),那就是单实例容量;需要的实例数 = 峰值并发 ÷ 单实例容量。
- 前缀感知路由——把有相同前缀的请求(如同一租户)路由到同一实例,前缀缓存命中率能从 ~30% 提到 90%+。
- 因为大部分服务的平均负载只有峰值的 30-60%(夜间几乎没流量)。按峰值算容量、按平均算成本,两个数不能混。
- 用户关掉页面后服务器还在生成 2000 个 token,占着 KV Cache 槽位不放——高并发下会显著吃掉容量。
- KV Cache 使用率。持续 >90% 意味着已经在抢占边缘,再多一点流量就开始踢请求、延迟突然恶化。它比 CPU/GPU 利用率更早预警。
- 因为推理延迟分布是长尾的(长请求、抢占、排队),平均 30ms 可能对应 P99 = 800ms,而用户体验由 P99 决定。
🛑 可以停在这里
⚡ 走神救援
🧭⭐第一件事是选引擎(第 15 章说"换引擎常常 5-10 倍",这里补上换成哪个):vLLM 是默认答案(通用、社区最大、支持面最广);SGLang 适合前缀重合度高的负载(Agent/多轮/多候选);TensorRT-LLM 性能上限最高但⚠️要编译,换模型/换卡/改并行度都得重编;TGI / LMDeploy 是生态贴合型(HF 生态 / 国产模型);llama.cpp 管没有 N 卡的一切场景;Ollama 只管"跑起来"。⭐四个问题就够:跑起来还是扛流量 → 有没有 N 卡 → 模型硬件会不会变 → 前缀重合度高不高。⚠️⭐反直觉但重要:先别挑性能最高的——引擎之间通常只差 20-50%,而有没有开对连续批处理/前缀缓存/量化差的是几倍;💥大部分"框架 A 快 3 倍"的评测拆开看都是配置没对齐。🌳RadixAttention(SGLang 的核心)是前缀共享从"一条"到"一棵树"的推进:把 KV Cache 索引组织成基数树,沿树往下能匹配多远就复用多远,只有分叉之后才重算;缓存满了只淘汰叶子(父节点还被别的分支用着);⭐判断该不该在意它,看的不是"哪个框架有这功能",而是你的请求之间到底有多少共享前缀——不共享就都白搭。⭐先定 SLO 再优化——不定目标就不知道调到什么程度算够,会盲目优化延迟到远超需求、白白损失吞吐。SLO 至少含 TTFT P95 / TPOT P95 / 可用性 / 吞吐;场景差异大:代码补全 TTFT<200ms 最敏感、Agent 输出长所以 TPOT 决定一切、批量处理只要吞吐。⭐压测画出"并发→吞吐→TPOT P95"曲线是容量规划的唯一依据(找到满足 SLO 的最大并发 = 单实例容量,实例数 = 峰值并发÷它);⚠️最常见的错误是用固定长度输入输出——真实流量方差极大,固定长度会严重高估连续批处理的收益,要用 ShareGPT 或线上日志回放。路由策略:轮询 < 最少请求数 < 最少待处理 token < ⭐⭐前缀感知路由(把同一租户的请求固定路由到同一实例,前缀缓存命中率从 ~30% 提到 90%+)。成本 = GPU 时价 ÷ (吞吐×3600) × 1e6;⭐别忘了 utilization——平均负载常只有峰值的 30-60%,按峰值算容量、按平均算成本;降成本三方向:提吞吐、⭐提利用率(把离线批量填到低峰期)、用更便宜的卡。生产五件事:限流、⭐超时与取消(💥客户端断开不取消是真实且昂贵的 bug——用户关页面后还在生成 2000 token 并占着 KV Cache 槽位)、优雅关闭、健康检查要真跑一次推理、降级。监控:⭐⭐KV Cache 使用率是最该盯的(持续 >90% = 已在抢占边缘,比 GPU 利用率更早预警);⭐看 P99 不看平均——延迟是长尾分布,平均 30ms 可能对应 P99 800ms,而用户体验由 P99 决定。
下一节 👉 21-训练稳定性与故障恢复.md