🏠 总目录📚 本教程 推理服务化
📑 本页目录(点开跳转)

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 负载长这样:

真实 Agent 负载不是一条链,是一棵树 系统提示词 2000 token 所有请求共用 用户问题 A 用户问题 B 第一次分叉 工具调用 → 结果 1 工具调用 → 结果 2 继续追问 第二次分叉 ⭐ 沿树往下走,能匹配多远就复用多远 —— 只有分叉之后的部分才要重新算 ⚠️ 缓存满了只能淘汰叶子:父节点还被别的分支用着
线性的前缀缓存只认得"从头开始一模一样的那一段";RadixAttention 把 KV Cache 的索引直接组织成一棵基数树,分叉点是自动被发现的,不需要你告诉它哪些请求该配对。

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 这类真实分布,或者你自己的线上日志回放。


🏗️ 四、部署架构

部署架构:一层负载均衡 + 多个模型实例 请求 负载均衡 ⭐ 路由策略很重要 实例 1 TP=2 实例 2 TP=2 实例 3 TP=2 每个实例内部自己做张量并行,实例之间彼此独立
请求先进负载均衡,再被分发到若干个各自 TP=2 的推理实例上 —— 分发怎么分,比实例本身跑多快还要影响吞吐。

路由策略的选择(比想象中重要):

策略 说明
轮询 最简单,但忽略了实例的实际负载
最少请求数 比轮询好
最少待处理 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 / 前缀缓存能不能生效的前提 ⭐

✅ 检查点

  1. 选推理引擎的四个问题是什么?默认答案是哪个?
  2. TensorRT-LLM 的代价是什么?什么情况下才值得付?
  3. 为什么说"先别去挑性能最高的那个"?大部分"框架 A 快 3 倍"的评测问题出在哪?
  4. RadixAttention 和普通前缀缓存的区别是什么?为什么它只淘汰叶子?
  5. 判断该不该关心 RadixAttention,真正要看的是什么?
  6. 为什么要先定 SLO 再优化?不定会怎样?
  7. 代码补全和 Agent 场景的 SLO 侧重有什么不同?
  8. 压测最常见的错误是什么?为什么它会误导你?
  9. 延迟-吞吐曲线怎么用来做容量规划?
  10. 四种路由策略里,哪个最容易被忽略但收益大?为什么?
  11. 成本公式里 utilization 为什么重要?
  12. "客户端断开不取消"为什么是昂贵的 bug?
  13. 最该盯的监控指标是哪个?为什么?
  14. 为什么看 P99 不看平均?
👀 答案
  1. 要"跑起来"还是"扛住流量"(跑起来 → Ollama,到此为止)②有没有 NVIDIA 卡(没有 → llama.cpp + GGUF)③模型和硬件会不会变(不变且性能是 KPI → TensorRT-LLM)④请求之间前缀重合度高不高(高 → SGLang)。默认答案是 vLLM。TGI / LMDeploy 是"生态贴合"型的选择——"团队已经会什么"是一个合法且常被低估的判据
  2. 它要先把模型编译成引擎文件,⚠️换模型、换卡、改并行度都要重编,还要对齐版本。只有当模型和硬件都稳定、而且延迟/成本已经是核心 KPI 时才值得付这个代价;还在选模型、还在调的阶段别碰编译型的。
  3. 因为主流引擎之间的差距通常只有 20-50%,而你有没有开对连续批处理、前缀缓存、量化,差的是几倍。💥 大部分"快 3 倍"的评测拆开看都是配置没对齐(一边开了连续批处理一边没开)。顺序:先把配置调对,再考虑换引擎。
  4. 普通前缀缓存的匹配是线性的(从头比对,命中一条前缀);RadixAttention 把 KV Cache 的索引组织成一棵基数树分叉点是自动被发现的,沿树往下能匹配多远就复用多远,只有分叉之后的部分才重新 Prefill。只淘汰叶子是因为父节点还被别的分支用着
  5. 你的请求之间到底有多少共享前缀——不是"哪个框架有这个功能"。一点都不共享,两者都白搭;而这件事在换引擎之前就能自己统计出来。
  6. 因为不定目标就不知道调到什么程度算够、该牺牲什么。不定 SLO 会盲目优化延迟到远超需求的程度,白白损失吞吐
  7. 代码补全 TTFT 极敏感(<200ms,用户等不了);Agent 输出很长,TPOT 是决定性的,TTFT 反而宽松。
  8. 用固定长度的输入输出。真实流量长度方差极大,固定长度会让连续批处理的收益被严重高估。要用 ShareGPT 这类真实分布或线上日志回放。
  9. 找到满足 SLO 的最大并发数(如 64),那就是单实例容量;需要的实例数 = 峰值并发 ÷ 单实例容量
  10. 前缀感知路由——把有相同前缀的请求(如同一租户)路由到同一实例,前缀缓存命中率能从 ~30% 提到 90%+
  11. 因为大部分服务的平均负载只有峰值的 30-60%(夜间几乎没流量)。按峰值算容量、按平均算成本,两个数不能混。
  12. 用户关掉页面后服务器还在生成 2000 个 token,占着 KV Cache 槽位不放——高并发下会显著吃掉容量。
  13. KV Cache 使用率。持续 >90% 意味着已经在抢占边缘,再多一点流量就开始踢请求、延迟突然恶化。它比 CPU/GPU 利用率更早预警
  14. 因为推理延迟分布是长尾的(长请求、抢占、排队),平均 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

打卡记录保存在你的浏览器里,首页能看到总进度