跳转至

第二十章:部署框架选型

20.1 部署框架到底解决什么问题

transformers 的 model.generate() 可以作为正确性基线,也支持多种缓存与 attention 优化;它本身不是一个完整的多租户在线调度服务。下面讨论朴素服务封装可能遇到的瓶颈,而不是把它们当成 Transformers 所有配置的固有缺陷。

20.1.1 三大痛点

痛点 表现
① KV Cache 显存碎片严重 朴素实现可能为请求预留接近最大长度的连续空间,而实际长度短得多,导致可用并发下降
② 批量推理调度低效 静态 batch 的成员通常保持不变;短请求完成后可能留下空槽或 padding,无法立刻纳入新请求,长度不均时利用率下降
③ 重复计算 例如用户共享同一段系统提示,但服务没有跨请求缓存,每次仍需计算相同前缀的 KV

因此常见优化方向是:内存高效(缓解碎片)+ 批量调度(提高利用率)+ 缓存复用(避免重复计算)。各框架的覆盖与实现随版本变化。

还要区分 prefill 与 decode:前者批量处理输入,通常有较高算术强度;低批量 decode 常受权重或 KV 带宽限制。高并发、长上下文和不同架构会改变瓶颈,不能把全部推理都称为 memory-bound。

服务选型最终要满足服务级别目标(SLO),例如首 token 延迟(TTFT)和相邻输出 token 的时间间隔(ITL)上限。后文比较缓存和调度时,既看总吞吐,也看用户等待和流式输出是否卡顿。

20.2 vLLM:PagedAttention + Continuous Batching

20.2.1 PagedAttention 的灵感来自操作系统虚拟内存

操作系统怎么管内存?不是给每个进程预分配大块连续物理内存(那会有大量碎片),而是把物理内存切成固定大小的「页」,进程拿到「逻辑地址」,通过页表映射到真实物理页。

PagedAttention 把这个思路搬到 KV Cache 上:

%%{init: {"flowchart": {"nodeSpacing": 16, "rankSpacing": 22, "padding": 8, "wrappingWidth": 160}}}%%
flowchart TB
    L["逻辑 KV 序列<br/>块 0 → 1 → 2"]
    L --> BT["Block Table<br/>逻辑 → 物理映射"]
    BT --> P["物理显存块池"]

    style BT fill:#e8f0fe

对一个请求,逻辑块 0、1、2 构成有序的 KV 序列。块表将每个逻辑块映射到物理显存块。物理块大小固定,可以分布在不连续的位置,并按引用情况回收;逻辑顺序不意味着物理存储位置相邻。

假设块大小为 16 token,一个请求的 200 token 需要 ceil(200/16)=13 块,总容量为 208 token,尾块空 8 个位置。它避免了为该请求预留整段 4096-token 连续空间;真实块大小受后端和配置约束。

请求结束后,引用计数为零的块可以回收到池中,也可能为前缀缓存保留到被逐出。回收不等于立刻将显存归还 CUDA,所以 nvidia-smi 占用不下降未必是泄漏。

实际收益取决于请求长度分布、block 大小、模型、并发限制和 KV-cache 预算;应使用生产形态的 trace 压测,而非套用固定百分比。

20.2.2 Continuous Batching:在迭代边界重组

Static batching:一批请求的成员在执行期间基本固定;短请求可以提前返回,但空出的执行槽未必能被新请求复用。

Continuous Batching:在调度迭代边界让已完成请求退出、让新请求加入。不是在任意 GPU kernel 执行到一半时插入请求;每轮可包含 decode token、prefill chunk 或推测验证等工作。

t1: 请求 A、B、C 同时在跑
t5: A 生成完退出 → 新请求 D 立刻加入
t8: B 生成完退出 → 新请求 E 加入

它可减少由长度不均造成的空槽时间;吞吐增益取决于到达率、输出长度和调度策略。

vLLM 同时提供 KV 管理、调度和 APC 等能力,不能只凭 PagedAttention 一个术语判定性能。最大 batch token 预算、并发限制和 KV 容量共同决定可调度的工作量。

20.2.3 长 prefill 会不会阻塞正在输出的请求

会,若长 prompt 独占一次调度迭代,现有请求的下一 token 可能被延后。Chunked prefill 把长输入拆成若干块,与 decode 工作交错,调节 TTFT 与逐 token 延迟之间的取舍。

Prefill/decode 分离则将两阶段放在不同执行实例或设备池,分别扩缩容,但需要传输 KV 并管理跨实例路由。传输成本、带宽和负载规模不合适时,分离反而更慢;它不等于连续批处理或前缀缓存。

20.3 SGLang:用 RadixAttention 复用前缀

SGLang 的 RadixAttention 是解释跨请求前缀复用的一个实现实例。SGLang 也包含调度、量化、分布式等能力,不能仅凭是否有共享前缀判定它与其他框架的胜负。

20.3.1 哪些场景前缀重复率高

  • System Prompt 共享:所有用户调同一个 API,System Prompt 完全一样;
  • Few-shot 示例共享:Prompt 里有 5–10 个固定示例;
  • 多轮对话历史:每轮都包含前 N 轮的完整历史;
  • Agent 工作流:Agent 多次调用 LLM,每次上下文都从同一个 System Prompt 开始。

vLLM 不只有 PagedAttention,也提供 Automatic Prefix Caching(APC),可复用相同 token 前缀的 KV block。是否开启、命中和收益依版本、配置及缓存压力而定;不能把「跨请求前缀复用」说成 SGLang 独有。

20.3.2 RadixAttention:用基数树组织 KV Cache

%%{init: {"flowchart": {"nodeSpacing": 16, "rankSpacing": 22, "padding": 8, "wrappingWidth": 160}}}%%
flowchart TB
    ROOT["根节点(空)"] --> SP["共享的 System Prompt<br/>1000 token —— 只存一份"]
    SP --> U1["问题 A"]
    SP --> U2["问题 B"]
    SP --> U3["问题 C"]

    style SP fill:#e6f4ea

问题 A、B、C 来自不同用户。各分支共享只存储一份的 1000 token 系统提示,不共享彼此的问题后缀。

多个请求可复用相同的 token 前缀路径,在第一个不同 token 处分叉。Radix tree 的一条边可压缩存储一段 token,不必一个 token 对应树的一层。

前缀树可让共享部分只保留一份;实际节省由前缀重合、驱逐策略和并发共同决定。

20.3.3 自动复用历史请求

若相同前缀的 KV block 尚未被逐出,后来的请求可复用它们;缓存驻留时长和首 token 延迟收益都由部署配置与负载决定。

SGLang 的 RadixAttention 对高前缀复用工作负载尤其值得评估;vLLM APC、SGLang 与其他运行时都应在同一模型、硬件、并发和 trace 下比较。

无共享前缀时,只能说前缀缓存这一项不能带来收益;其他 kernel、调度和模型实现仍可能产生差异。SGLang 与 vLLM 可以作为同一服务的候选运行时,既不是必须一起部署的互补组件,也没有脱离负载的固定排序。

20.4 TGI:处于维护模式的 HuggingFace 服务方案

Hugging Face 已在官方文档中声明 TGI 进入维护模式:接受小型修复、文档和轻量维护工作,并推荐后续评估 vLLM、SGLang 或本地兼容运行时。现有 TGI 用户仍应按自身版本的支持矩阵维护部署;维护模式不等于既有服务立即不可用。

维度 说明
生态集成 可通过 HF Hub 模型 ID 加载支持的模型;需核对架构、权重格式、量化配置、tokenizer 和 chat template
服务与观测 HTTP 服务与 SSE 流式输出、Prometheus 指标、OpenTelemetry tracing;内部组件的 gRPC 通信不等于通用的外部双协议 API
能力 提供连续批处理、量化和 SSE 流式输出;新项目需将维护状态和目标模型支持纳入选型

既有部署应评估继续维护与迁移成本。HF Hub 集成和指标能力不是 TGI 独有,新项目不应只因为模型来自 Hub 就选 TGI。鉴权、JWT、配额、TLS 和审计需要分别确认运行时与网关的职责,不能用「企业级」一词概括为开箱即用。

20.5 llama.cpp:CPU / 边缘设备的常用方案

使用 C/C++ 实现轻量推理栈,支持 CPU、Metal、CUDA 等后端及 CPU/GPU 混合执行,常用于本地、Apple Silicon 和边缘场景;并非只支持 CPU。

这类设备的内存、带宽、功耗与部署环境差异很大,轻量依赖和多后端支持比单一服务器 GPU 的最优吞吐更重要。

20.5.1 三个关键技术

① GGUF 文件格式

把张量及 tokenizer 等元数据存入 GGUF,也可分片存储。常见量化预设:

档位 说明
Q8_0 8-bit,通常比更低比特格式保留更多质量
Q5_K_M 以 5-bit 为主的混合量化预设,实际平均位宽包含元数据和不同张量格式
Q4_K_M 以 4-bit 为主的混合量化预设,不表示文件中每个参数恰好占 4 bit
Q3_K_S 3-bit,极端压缩,精度有损

再强调一次:GGUF 是文件格式不是量化算法(见 第十五章)。

② SIMD 优化

针对 AVX2、AVX512、ARM NEON 等指令集提供优化 kernel。CPU 与 GPU 的速度比较高度依赖模型、量化、内存带宽和批量大小,不应使用固定比例。

③ Metal 后端(Apple Silicon)

Metal 可利用 Apple GPU;统一内存减少部分 CPU/GPU 间显式复制,但不消除带宽限制。能否运行 70B 要结合量化、可用内存、KV 与系统预留判断,不能用「统一内存」承诺速度。

20.5.2 适用边界

常见用途 需要额外验证
本地或嵌入式推理 高并发时的排队、KV 容量和目标设备吞吐
Mac 上使用 Metal 模型格式、量化与硬件带宽
资源受限的 CPU/GPU 混合运行 offload 与跨设备传输的延迟代价
离线或隐私敏感场景 核查模型下载、遥测、日志与外部工具调用;本地推理本身不保证整个应用的数据都不出设备

官方 llama-server 已列出多用户并行解码和 continuous batching,也提供多 GPU 分配选项。因此「不支持 batch」「不能多卡」都不准确。分布式规模和运维能力仍要按具体版本验证,不能从支持某个选项推成适合所有集群。

20.6 TensorRT-LLM:针对 NVIDIA 的推理栈

TensorRT-LLM 针对 NVIDIA GPU 的 kernel、运行时与服务调度进行优化。要区分传统 TensorRT engine 路线与 PyTorch backend / 高层 LLM API,不能继续概括成「所有模型都必须先手动编译 engine」。

特点 代价
提供针对硬件和模型的 kernel、量化及并行优化 需核对 CUDA、GPU、模型、精度与后端支持矩阵
集成 NVIDIA 自家内核库 只支持 NVIDIA GPU
支持若干 FP8、INT4 等路径,依模型与 GPU 而异 硬件支持某格式不意味着所有算子和模型都支持
trtllm-serve 和 LLM API 可简化加载与部署 engine 路线仍需考虑构建配置与兼容性,高层 API 也有加载和优化成本

适合在 NVIDIA 环境中评估,不限于大集群。是否值得采用,取决于模型支持、实际 SLO 收益和团队承担的运维成本,而不是框架名称暗示的「极致性能」。

20.7 怎样按工作负载筛选运行时?

先用模型、硬件和维护要求排除不支持的组合,再用真实请求比较满足 SLO 的吞吐。下面是候选方向,不是性能排名:

框架 可重点评估的机制 常见评估场景与限制
vLLM PagedAttention、连续批处理、APC 高吞吐 LLM API 与重复前缀工作负载
SGLang RadixAttention 与服务调度 Agent、多轮、Few-shot;高前缀复用时尤其值得评估
TGI HF 服务生态 既有部署维护与迁移;已进入维护模式
llama.cpp C/C++ 多后端推理与 GGUF CPU、Mac、边缘;性能受具体设备约束
TensorRT-LLM NVIDIA 运行时优化 NVIDIA 环境;按后端和模型支持矩阵选型

这些运行时都提供公开实现,但生态归属或活跃程度不能替代目标版本的支持矩阵;最终性能仍需在相同硬件、质量和负载下测量。

20.7.1 四个常见误用

误用 后果 正确做法
认为 vLLM 没有前缀复用 忽略 APC,导致错误的架构判断 查目标版本 APC 配置,并用真实 trace 比较 vLLM 与 SGLang
把 llama.cpp 当成没有批处理的 CPU 工具 漏掉 GPU 后端与服务器能力 用目标设备和并发压测,而非按标签排除
新部署忽略 TGI 维护模式 新模型或功能支持不符合预期 比较维护成本和替代运行时,保留迁移与回退方案
假设 TensorRT-LLM 必须手工构建 engine 错估 POC 与维护成本 明确使用的后端、LLM API 或 engine 工作流

20.8 三大隐藏陷阱

20.8.1 分页没有解决所有容量问题

固定块分配主要缓解外部碎片和过度预留;每条序列可能留下未填满的尾块。长上下文还会真实增加 KV 容量,不能把所有 OOM 都叫「碎片」。

应对:分别监控活动 KV 块、缓存驻留、预留池、抢占/重算、GPU 峰值和排队延迟。再结合 trace 调整上下文限制、batch token 预算与并发;不要把总显存利用率等同于有效工作量。

20.8.2 KV Cache 量化的支持差异大

权重量化很多框架都支持,但 KV Cache 量化的支持差异很大,而且版本迭代很快。

如果你的瓶颈是长上下文 KV Cache 显存,选型前一定要查当前版本文档,别只听别人说「支持」。

20.8.3 MoE 模型的部署支持差异

跨设备 MoE 部署需检查专家并行与 dispatch/combine 通信(见第十九章);单设备 MoE 不一定需要跨卡 All-to-All:

  • 目标模型的具体路由、共享专家与 attention 结构是否支持;
  • 量化格式、专家并行拓扑、通信后端能否一起使用;
  • 热点专家、offload 与 batch 变化下的实际性能。

没有「TensorRT-LLM 支持最好」或「llama.cpp 性能一般」的跨模型结论;功能能运行与符合服务 SLO 是两项不同验证。

20.9 如何设计能用于选型的压测

20.9.1 先统一条件

固定模型/tokenizer 修订、chat template、量化精度、输入输出长度分布、采样、硬件拓扑与实际并发。记录框架版本、attention 后端、batch token 预算、KV 配置和是否开启推测解码,不能把不同质量或不同输出长度的结果直接排榜。

20.9.2 分清延迟与吞吐指标

指标 含义与读法
TTFT 从发出请求到第一个输出 token;需说明是否含网络、排队和 prefill
ITL 相邻输出 token 的时间间隔;可反映流式输出卡顿
TPOT 常按首 token 后耗时除以其余输出 token 数计算;均值会掩盖停顿
端到端延迟 完整请求耗时,受输出长度显著影响
输出 tokens/s 总输出吞吐,与单用户生成速度不是同一个数
SLO goodput 在 TTFT、ITL/TPOT 等约束内完成的有效请求或 token 吞吐

20.9.3 覆盖真实到达流量与缓存状态

只固定并发的闭环压测,会在服务变慢时自动降低请求到达率,可能掩盖过载排队。还应使用带到达时间的 trace 或开放式负载,覆盖冷/热缓存、长短混合输入输出、多租户和突发流量,并报告 p50/p95/p99 与失败率。

20.9.4 为什么优化可能让另一项指标变差

增加 batch 往往提高吞吐,但可能增大 TTFT 和 ITL;保留更多前缀能减少重算,却可能挤压活动 KV;分离 prefill/decode 可以减少干扰,却会增加 KV 传输。需要沿 SLO 约束寻找可接受配置,而不是最大化一个离线 tokens/s 数字。

20.9.5 从实验走向上线还缺什么

验证取消与超时能否释放资源、过载限流与背压、模型加载就绪探针、多副本路由、租户配额及版本回滚。API “OpenAI-compatible” 通常只覆盖部分接口语义,还要测试流式结束、工具调用、错误码和 usage 字段,而不是仅测请求返回 200。

20.10 本章总结

  1. 部署框架解决三大痛点:KV Cache 显存碎片、批量调度低效、共享前缀重复计算;
  2. vLLM 的 PagedAttention 借鉴操作系统虚拟内存,Block Table 映射逻辑到物理;APC 可自动复用匹配前缀的 KV block;
  3. Continuous Batching 让请求异步进出,可改善长度不均工作负载的利用率;
  4. SGLang 的 RadixAttention 用基数树共享前缀,高前缀复用时可减少缓存和 prefill 重算;
  5. vLLM APC 与 SGLang 都应按实际 trace 评估,不存在脱离版本与负载的固定胜负;
  6. TGI 已进入维护模式;新项目需优先检查目标模型与运行时的当前支持;
  7. llama.cpp 是 C/C++ 多后端推理栈 + GGUF,包含连续批处理与多 GPU 选项;适用规模应实测;
  8. TensorRT-LLM 针对 NVIDIA 硬件优化,应区分 PyTorch/LLM API 与 engine 工作流;并非都要手工预编译;
  9. 三大隐藏陷阱:长上下文仍有碎片与调度开销、KV Cache 量化支持差异大、MoE 部署复杂度通常高于 Dense。

参考资料

本章的参考资料、阅读建议与来源说明见集中参考资料章节。