在推理工程领域工作,最令人兴奋的地方之一在于:与许多行业中新学术研究需要数年乃至数十年才能被产业界采用不同,新论文中的技术往往在数月甚至数周内就能在生产环境中投入使用。
从研究到生产之间存在一道鸿沟,而业界最引人瞩目的推理工程工作,往往正是在弥合这一鸿沟的过程中诞生的。
推理工程的一个核心原则是:在推理系统中引入的约束越多,所能实现的性能就越好。这一原则在本章中将持续体现,例如解耦(Disaggregation)技术——它允许你将各个引擎分别约束为专注于预填充(Prefill)和解码(Decode)的角色。
在这些模型性能优化技术中,有一个新原则值得牢记:流量越大,可以实施的性能优化手段就越多(同时保持合理的单位经济效益)。更高的模型并行度、跨更多GPU的部署、KV感知路由以及动态解耦,只有在拥有大量GPU(通常是多个节点)、以垂直扩展和水平复制方式服务同一模型时,才具有实际意义。
真实世界的流量难以被约束。但随着规模的增长,你可以随着时间推移不断调整系统,以适应使用模式的变化。调整推理引擎、推测算法和模型服务器的参数并非一次性任务,而是需要通过迭代部署或动态运行时调整,持续提升推理系统的性能。
找到技术与配置的最佳组合需要耐心地反复试验。我记得在一次内部黑客马拉松中,Baseten的一位推理工程师正在为代码自动补全模型进行优化,他通过一个手写脚本尝试了77种不同的配置,最终找到了一个出人意料的解决方案,将某客户模型的TPS(每秒Token数)提升了一倍。
推理优化的复杂性还体现在:有些技术之间相辅相成,而有些则相互冲突。例如,对KV缓存进行量化可以缓解解耦中的瓶颈,但增大批处理规模则会减少可用于推测的计算资源。
推理工程师的目标,始终是构建一套均衡的优化组合,使整体效果超越各部分之和。
本章将介绍推理加速应用研究的五大核心类别:量化(Quantization)、推测(Speculation)、缓存(Caching)、并行(Parallelism)和解耦(Disaggregation)。在阅读每个章节时,请特别关注各项技术的适用场景,以及每种技术可能带来的潜在瓶颈或权衡取舍。
5.1 量化
量化可以改善延迟(包括 TTFT 和 TPS),提高系统吞吐量,并为其他优化(如分离、推测和前缀缓存)提供更大的发挥空间,使其更加有效。但一旦出现问题,量化可能会显著降低模型的输出质量。
模型在训练时,其权重、激活值及其他组件以特定的原生数字格式表示。通常为 BF16 或 FP16,但 8 位和 4 位原生精度在训练中正变得越来越普遍。
训练后量化的工作原理是将模型权重和其他值从其原生数字格式转换为更低精度的格式。将精度减半可以改善推理两个阶段的性能:
-
预填充:受计算限制的预填充阶段现在可在具有两倍 FLOPS 的低精度张量核心上运行。
-
解码:受内存限制的解码阶段现在每个值加载的数据量减少了一半,相当于将内存带宽提升了一倍。
处理量化数据确实会引入额外开销,因此从 16 位降至 8 位并不会带来线性的两倍加速。在实践中,每降低一个精度级别的量化通常可为大语言模型带来 30% 至 50% 的性能提升。
| Pi 精度 | Pi 的平方 | Pi 的立方 |
|---|---|---|
| 3.14159 | 9.869588 | 31.006198 |
| 3.14 | 9.8596 | 30.959144 |
| 3 | 9 | 27 |
量化的问题在于它存在降低模型输出质量的风险。量化有可能在支撑推理的整个计算过程中引入精度误差。
精度误差会随时间不断累积。考虑对不同精度的 Pi 进行平方和立方运算时会发生什么:
量化工作的重点在于既要防止精度误差的产生,又要尽量减少其对最终模型输出的影响。
5.1.1 数字格式
量化引入了一批重要的术语和缩写。最需要了解的是常见的数字格式:
| 名称 | 缩写 | 首次应用架构 |
|---|---|---|
| 64位浮点数 | FP64 | Fermi(2010) |
| 32位浮点数 | FP32 | Kepler(2012) |
| 16位浮点数 | FP16 | Pascal(2016) |
| Brain 16位浮点数 | BF16 | Ampere(2020) |
| 8位浮点数 | FP8 | Hopper(2022) |
| 混合精度FP8 | MXFP8 | Blackwell(2024) |
| 8位整数 | INT8 | Pascal(2016) |
| 6位浮点数 | FP6 | Blackwell(2024,实验性) |
| 4位浮点数 | FP4 | Blackwell(2024) |
| 混合精度FP4 | MXFP4 | Blackwell(2024) |
| NVIDIA FP4 | NVFP4 | Blackwell(2024,专有) |
| 4位整数 | INT4 | Turing(2018) |
最大的数字格式FP64(即”双精度”)仅用于高精度科学计算,而非AI训练或推理。FP32有时用于训练,但几乎不用于推理。FP6在本书出版时仍处于较为实验性的阶段,但AMD GPU正在迅速采用该格式。
因此,16位、8位和4位精度成为推理的主要格式。数字格式具有以下属性:
-
精度:用于表示格式中单个值的比特数。例如,FP16使用16位。
-
类型:这些比特是被解释为整数(无小数)还是浮点数(有小数)。
-
缩放因子:用于将低精度格式的值映射回高精度格式的乘数。
这些属性共同决定了数字格式在推理中表示值的两个关键因素:
-
动态范围:该格式所能表示的最小值与最大值之间的差距。
-
粒度:沿单个缩放因子进行量化的参数或其他值的数量。
动态范围对于在不损失质量的情况下进行低精度推理至关重要。16位可以表示65,536个不同的值,而8位只能表示256个不同的值。动态范围是这些值的分布——即可用最小值与最大值之间的差距。
动态范围解释了为什么浮点格式在推理中优于整数格式。浮点格式具有三个属性:
-
符号位:表示数字正负的单个比特。
-
指数位:若干比特组合在一起,表示指数因子。
-
尾数位:若干比特组合在一起,表示以2为底、以指数为幂的基础值。
E4M3数据格式中的FP8数字表示它有4位指数和3位尾数,其余1位为符号位。整数格式只有符号位和数值位。
浮点数中的指数赋予其更高的动态范围,这意味着它能更好地表示非常大和非常小的数字。这一点非常重要,因为异常值在推理中具有重要意义,而浮点数格式在量化后能更好地表示异常值。
在浮点格式中,每种精度下都有多种选项,如FP4、MXFP4和NVFP4。这些格式在粒度上有所不同,即由单个缩放因子量化的值的数量不同。
量化可以应用于以下级别:
-
张量级别:为整个QKV张量计算单一缩放因子。
-
通道级别:为张量中的每个特征向量计算不同的缩放因子。
-
块级别:在每个特征向量内,将向量划分为N个值的块,并为每个块计算缩放因子。
粒度越细,平滑掉异常值的可能性越低,从而保留更高质量。然而,粒度越细,存储和应用缩放因子的开销也越大。
MXFP8和MXFP4是Blackwell支持的新数字格式,属于”微缩放”格式,对每32个参数计算一个块级缩放因子,从而降低这些数字格式较低动态范围带来的影响。
NVFP4是NVIDIA推出的4位格式,其块大小为16,并附加一个32位全局缩放因子,比MX格式提供更高的粒度,以进一步抑制4位格式引入的质量损失。
微缩放格式的代价是小块缩放因子也需要存储在内存中,略微降低了量化带来的性能提升。此外,张量和块缩放因子都需要被应用,引入了一定的计算开销。Blackwell GPU通过在Tensor Core中应用缩放因子来抵消这一开销。
虽然本书的重点是数据中心的推理工程,但量化对于本地和边缘推理也是一个至关重要的话题,尤其是对于大型模型。GGUF是一种用于存储模型的二进制格式,是在Hugging Face上分发高度量化模型的最流行选择,个人研究人员和公司将DeepSeek等大型模型压缩到苹果电脑等消费级硬件上运行。
这些量化策略通过动态量化来抵御质量损失——模型的某些层或其他组件保持原始精度,而其他部分则被量化为低至1位精度的整数。动态格式代表其平均精度,这就是为什么你可能会看到诸如微调实验室Unsloth推出的流行1.58位量化之类的说法。
虽然这些动态量化是令人印象深刻的工程成就,非常适合本地推理,但从事生产系统的推理工程师应坚持使用浮点数格式——由于整数格式缺乏动态范围,不适合对质量敏感的工作负载。
相反,8位浮点格式(FP8、MXFP8)通常是在不牺牲质量的前提下提升性能的最佳选择。FP4很有前景,尤其是NVFP4格式引入了更高的粒度以提升精度,但FP8和MXFP8提供了最大的灵活性,尤其是在对KV缓存进行量化时。
5.1.2 量化方法
模型的参数越多,对量化的敏感性就越低,因为每个单独参数的重要性相对较小。然而,即使对于非常大的模型,谨慎地进行量化也至关重要。
量化可以在训练期间或训练之后进行:
-
量化感知训练:将权重训练与缩放因子计算同步进行,以确保最终收敛的权重在给定精度下保持准确。
-
训练后量化:通过计算缩放因子并借助校准来保留精度,将已完成的模型权重转换为新的精度格式。
虽然一些实验室发布了经过量化感知训练创建的模型,例如 MXFP4 格式的 GPT-OSS 和 INT4 格式的 Kimi K2 Thinking,但使用开放模型的推理工程师只能对已完成的权重执行训练后量化。
NVIDIA TensorRT Model Optimizer(ModelOpt)是训练后量化的领先工具,这是一个同时支持剪枝、蒸馏和稀疏化的开源库。ModelOpt 的输出与所有推理引擎(vLLM、SGLang、TensorRT-LLM)兼容。
在选定精度之后,执行训练后量化之前需要做出两个决策:
- 模型的哪些部分(权重、激活值、KV 缓存、注意力)应该被量化?
- 哪种数字格式能提供适当的动态范围和粒度?
这些决策将量化从一个二元选择转变为围绕性能与质量之间权衡的连续谱系。
模型的各个组件对量化的敏感程度不同。降低更敏感组件的精度会带来更高的质量下降风险。从最不敏感到最敏感依次为:
- 权重:具体而言,线性层对量化最不敏感。
- 激活值:激活函数的中间输出对量化只有一定程度的敏感性。注意,激活函数本身很少被量化,因为它们在模型权重中占比极小。
- KV 缓存:注意力计算中缓存的值对量化具有中等敏感性。
- 注意力:模型的注意力层对量化高度敏感,尤其是 softmax 等运算。
在上述每个组件内部,还可以对量化进行更精细的选择。
即使在线性层和激活值中——由于其规模较大,通常对量化最不敏感——神经网络的输入层和输出层等早期和晚期层也可能保留其原始精度,因为这些层更为敏感。
量化权重和激活值直接有助于提升性能,而 KV 缓存量化则能进一步增强前缀缓存和解耦等技术的效果。KV 缓存是宝贵的资源,对其进行量化可以让推理引擎在内存中存储更多内容并更快速地读取。
然而,每个 token 的 KV 缓存都会被后续的每个 token 所使用。这意味着量化引入的精度误差可能会从一个 token 累积到下一个 token。
误差累积正是注意力层量化风险最高的原因。注意力不仅对动态范围非常敏感,而且每次注意力计算都依赖于之前每次注意力计算的结果。在数千个 token 的序列中,这些误差会迅速积累。
除了最激进的量化方案外,几乎所有方案都以原始精度运行 softmax 等函数。
低精度推理的一种适中方法是使用具有高动态范围的格式(如 FP8)——如果可能,使用微缩放格式如 MXFP8——对选定的线性层、激活值以及通常的 KV 缓存值进行谨慎量化。即便使用这些高动态范围格式,注意力层的各组件也很少被量化。
5.1.3 衡量质量影响
生产就绪量化的标准是零可感知质量损失。对模型进行量化后,必须对其输出质量与原始精度进行全面测试。
量化后检查模型质量有三种方法:
- 困惑度:计算量化模型的困惑度分数,并与原始模型进行比较。
- 智能基准测试:运行 MMLU 或 SWE-bench 等标准智能基准测试,并与原始分数进行比较。
- 自定义评估:在量化模型上运行特定产品的评估套件,并与原始权重进行比较。
在任何情况下,您都在寻找与噪声无法区分的分数差异。大语言模型具有非确定性,因此每次运行的分数会略有不同。
最简单的质量检查方法是困惑度。困惑度不要求模型生成输出,而是向模型提供预期的输出序列,并计算模型预测这些词元的可能性。
困惑度越高,意味着模型对这些序列越”感到意外”——这对于一个本应预测词元的模型来说并不理想。量化后,您希望困惑度的增加尽可能小。
更全面的质量检查依赖于公开的智能基准测试,或者更好的是,与您预期的实际使用场景相匹配的特定领域评估。在评估中,您希望质量分数的降低尽可能小。
全面了解量化影响的最佳方法是运行所有三种类型的检查,并与原始模型权重进行同等条件下的比较。
请记住,量化是一个连续的范围,而非二元决策。通过将量化精度从 FP4 改为 FP8,或对模型的较少组件进行量化(如仅对权重进行量化),您仍然可以在降低质量损失风险的同时获得一定的性能提升。
如果您在高度敏感的领域工作,无法承担模型质量风险,请不必担心:本章中的所有其他技术在质量方面都是无损的。
5.2 推测解码
LLM推理的解码阶段是一个自回归过程,每次生成一个token。解码的瓶颈在于内存带宽,在低到中等批量大小时,计算资源处于空闲状态,因为权重需要从内存中读取。
推测解码利用这些空闲的计算资源,尝试在每次目标模型的前向传播中生成多个token。如果推理引擎能够在每次权重从内存读取的往返过程中生成两个、三个甚至更多的token,它将每秒生成更多的token。推测解码只能提升TPS/ITL,而不能改善TTFT。
推测解码有多种算法,它们共享一个通用机制:
- 推测器生成一个或多个草稿token。
- 目标模型(即你试图加速的底层模型)对这些token执行验证,检查它们是否与模型本身会生成的内容相匹配。
- 目标模型接受所有有效的草稿token,并自行生成一个额外的token,完成前向传播。
每次前向传播(即解码循环的每次迭代)可生成N+1个token,其中N是被接受的草稿token数量。
生成草稿token并非没有代价,它需要消耗计算和内存资源。然而,目标模型验证一个草稿token比生成一个原始token要快得多。想象一个数独谜题,解题很难,但检验答案是否正确却非常容易。对于目标模型来说,生成一个token就像解数独,而验证一个草稿token就像检查一个已完成的数独。
任何推测解码策略的性能提升取决于三个因素:
- 草稿token成本:生成一个草稿token所需的时间。
- 草稿序列长度:每次前向传播中生成的草稿token数量。
- Token接受率:被目标模型接受的草稿token的百分比。
Token接受率在草稿序列的早期较高,但随着序列深度增加,草稿token的可靠性会降低。
应追求短而高接受率的序列,因为生成和验证token虽然相比在原始模型中生成token代价较低,但仍会带来可观的开销。此外,一旦某个草稿token被判定为错误而拒绝,序列中后续的所有token也会被一并拒绝。
使用推测解码很有意思,因为有很多因素会影响token接受率。其中最重要的是温度——较高的温度会产生更难预测的token分布,从而降低推测解码的有效性。但即使是像主题内容这样简单的因素,也可能对接受率产生影响,例如用于推测的草稿模型或附加头在数学方面比历史方面更擅长。
推测解码的另一个限制是,它在低批量大小(存在空闲计算周期)时最为有用。在较高的批量大小下,由于计算资源过于饱和,无法承担验证开销,推测解码必须被动态禁用。
每种推测算法以不同的方式权衡这些取舍,针对具体情况仔细实现合适的算法,可以带来TPS的显著提升。
5.2.1 草稿-目标推测解码
推测解码的原始方法使用两个模型:
-
草稿模型:一个额外的模型,用于生成推测性草稿词元。
-
目标模型:原始模型,在执行普通解码的同时,还负责验证草稿词元。
配置草稿-目标推测解码时,最重要的决策是选择哪个草稿模型。一个好的草稿模型在运行时资源消耗极少的同时,还能保持较高的词元接受率。
草稿模型通常是与目标模型同系列的较小成员,因为它们共享分词器和行为特征。根据经验,草稿模型的参数量应至少比目标模型小十倍。通过微调或蒸馏,可以让这些小模型的行为更接近目标模型,从而提高词元接受率。
当您希望无需任何训练或微调、开箱即用时,草稿-目标推测解码是一个不错的选择。
然而,其他推测算法通常能提供更好的性能。在所有推测解码方法中,草稿-目标方法引入的开销最大。尽管草稿模型较小,但推理引擎必须将草稿模型的权重、激活值和KV缓存存储在内存中,并为草稿模型的预填充分配计算资源。此外,草稿模型和目标模型必须协调运行,以避免资源竞争,不过像TensorRT-LLM这样的推理引擎可以处理该模型编排问题。
5.2.2 Medusa
Medusa 是草稿-目标推测(draft-target speculation)最早的替代方案之一。Medusa 通过对目标模型进行微调,使其在每次前向传播时生成额外的令牌,从而解决了运行草稿模型进行推测所带来的复杂性和开销问题。
为 Medusa 对模型进行微调,意味着在模型上附加额外的解码器头。LLM 的普通架构包含单个解码器头,而 Medusa 则额外增加了两到四个头,用于生成连续的草稿令牌。
与草稿-目标推测类似,草稿令牌在下一次前向传播时进行验证。
Medusa 在草稿令牌数量和草稿令牌接受率方面仍存在局限性,目前在生产环境中并未得到广泛应用。然而,Medusa 启发了 EAGLE 等更为流行的技术。
5.2.3 EAGLE
使用现成的预训练模型作为草稿模型的主要问题在于,像 Qwen 0.5B 这样的模型是为在廉价硬件上独立运行而设计的,而非为在 B200 上推测草稿令牌而优化。这些草稿模型运行效率低下,且接受率相对较低。
EAGLE 提供了一种替代方案:这是一种专门构建的草稿模型,从头开始训练,能够生成最多八个草稿令牌的序列(是 Medusa 的两倍),且具有极高的接受率。
在推理过程中,大型语言模型以层间隐藏状态的形式积累了大量关于预测令牌的上下文信息。传统草稿模型无法访问这些信息。
EAGLE 是一种经过训练、以隐藏状态作为输入并生成推测令牌作为输出的草稿模型。具体而言,它在三组隐藏状态上进行训练:一个来自早期层,一个来自中间层,一个来自后期层。EAGLE 的参数量通常不足十亿,并且在获得更多训练数据时具有良好的扩展性。
在实践中,当将 EAGLE 与 TensorRT-LLM 等推理引擎配合使用时,实现过程往往较为简单,支持训练后创建 EAGLE 以及单序列推测。
EAGLE 可以附加到与目标模型相同的模块(PyTorch 类)上,因此每次前向传播会同时对目标模型和 EAGLE 推测器执行推理。这种统一的流水线解决了草稿-目标解码中的另一个问题——此前需要多次往返 CPU 来协调草稿模型和目标模型。
对于具备训练 EAGLE 头部的知识和能力的推理工程师而言,EAGLE 是通用推测算法的首选,并且得到了推理引擎的良好支持。与其他推测技术一样,采用 EAGLE 来改善延迟需要减小批量大小,这会降低吞吐量并增加成本。
5.2.4 N-gram 推测解码与前瞻解码
N-gram 推测解码使用与其他类型推测解码不同的机制,它不需要草稿模型。
取而代之的是,推理引擎在生成 KV 缓存的同时,并行构建一个 N-gram 字典。该 N-gram 字典将单个起始词元映射到一个由 N 个词元组成的观测序列(即 N-gram)。
该 N-gram 字典包含输入文本中的常见序列,并在预填充阶段首先构建完成。在解码阶段,生成的词元被输入字典,所有可用的后缀将被选作草稿词元。在下一次前向传播中,目标模型会像往常一样对这些草稿词元进行验证。
N-gram 推测解码相较于 EAGLE 的优势在于序列可以更长。EAGLE 通常能以较高的接受率生成约八个草稿词元,而 N-gram 序列可以超过十个词元。
然而,只有当模型输出内容与模型输入高度相似时,N-gram 的接受率才会较高。N-gram 推测解码主要用于代码补全和代码修改等场景,因为这类场景语法具有可预测性,且输出与输入高度匹配。但在这一特定领域内,它的表现轻松超越 EAGLE。
与 N-gram 推测解码类似的方法是前瞻解码(Lookahead Decoding),它在推理过程中动态生成 N-gram 以填充字典。前瞻解码比 N-gram 推测解码更为通用,因为它不像后者那样依赖高度重复的上下文,但它需要额外的算力来生成 N-gram。
所有推测解码算法都旨在减少生成完整输出序列所需的前向传播总次数,从而改善整体延迟,尤其是提升解码阶段每用户每秒的词元生成速度。N-gram 推测解码在代码补全及类似任务中表现出色,而前瞻解码则更适合在算力富余的系统中实现更广泛的通用性。
5.3 缓存
在预填充阶段,推理引擎会针对输入序列构建KV缓存(即存储每个token的键和值)。随后在解码阶段,它会为每个新生成的token更新KV缓存。由于推理过程是自回归的,每个新token的值都依赖于序列中所有先前token的值。
每个推理引擎默认会在单次请求范围内使用KV缓存。如果没有KV缓存,大语言模型的推理速度将会慢得令人难以接受,因为对于每个后续token,都需要重新计算整个序列中所有先前的值。
然而,工程师可以通过在请求之间复用KV缓存(而不仅仅是在单次推理序列内部使用),从KV缓存中获得更大的价值。
5.3.1 前缀缓存与KV缓存复用
考虑以下两个提示词,在大多数分词器中每个提示词均包含四个Token,如图5.7所示。
默认情况下,推理引擎需要对每个提示词的全部四个Token执行预填充。但两个提示词的前两个Token——“Weather in”——构成了这对提示词之间的共享前缀。
通过前缀缓存,你可以复用第一个请求的KV缓存,跳过对前两个Token的预填充,转而直接读取已有的KV缓存,从而改善第二个请求的首Token生成时间(TTFT)。
当你看到按Token计费的API对”缓存命中”的输入Token收费低于”缓存未命中”Token时,原因正在于此——复用已缓存的Token几乎不需要计算资源或时间。作为推理工程师,你可以将同样的原理应用于自己的部署中,以降低延迟、提升吞吐量(从而节省成本)。
节省两个Token对TTFT的影响不大,但在某些场景下,前缀缓存可以跳过数千个Token的预填充:
- 复杂系统提示词:智能体、面向用户的聊天机器人、RAG框架以及工具调用,往往在每次调用时都包含冗长复杂的系统提示词。
- 代码补全:代码补全、代码生成及其他编程功能需要将相同的数千行代码作为共享上下文传入。
- 文档与检索:文档摘要、问答以及检索任务都会在用户提示词前添加重复的上下文。
- 多轮对话:普通对话会在聊天模板中重复每一条历史消息,每多一轮对话,前缀缓存带来的节省就越多。
前缀缓存从输入序列的起始位置生效,直至第一个非重复Token为止。在天气示例中,第四个Token(问号)在两个输入序列中是相同的,但由于前缀在第一个非重复Token处终止,第四个Token并不会从缓存中读取。
由于前缀在第一个唯一Token处结束,你的上下文工程策略决定了TTFT的节省幅度。来看同一提示词的另一种处理方式:
在这里,前缀缓存没有任何收益,因为两个序列的第一个Token就已不同,尽管后续每个Token都相同。
要充分利用前缀缓存,请确保新颖Token尽可能靠后出现在你的上下文中。
前缀缓存是KV缓存复用的主导形式,这是由LLM的自回归特性决定的。每个Token都会影响其后的所有Token,因此一个新颖Token就会改变模型对序列剩余部分的内部表示方式,即便这些序列在人类读者看来完全相同。
然而,目前学界正积极研究其他类型的KV缓存复用方案,以克服这一局限。缓存提示词中间任意位置的序列,需要同时修正位置编码,并有选择地重新计算KV条目,以维持输出质量。CacheBlend和LMCache等工具支持非前缀序列的缓存,进一步拓展了KV缓存复用的可能性。
5.3.2 KV 缓存存储在哪里
KV 缓存非常宝贵,但它会占用大量内存,而 GPU 的显存(VRAM)十分有限。
您可以配置推理引擎为 KV 缓存分配多少内存。例如,在 TensorRT-LLM 中,您可以进行如下设置:
如果您使用的是拥有 180 GB 显存的 B200 GPU,其中 100 GB 用于模型权重和缓冲区,则该配置将把剩余显存的 80%(即 64 GB)分配给 KV 缓存。
一旦这部分分配被填满——而且很快就会被填满——您将不得不开始删除已保存的 KV 缓存,从而增加未来请求发生缓存未命中的概率。
为了获得更多 KV 缓存空间,可以将数据从显存卸载到其他邻近存储介质。以下是可以存储 KV 缓存的四个位置,按与 GPU 带宽从高到低排列:
| 级别 | 内存类型 | 近似速度 | 近似容量 |
|---|---|---|---|
| G1 | 设备内存(GPU 显存) | 每秒数 TB | 数十至数百 GB |
| G2 | 主机内存(CPU 内存) | 每秒数十至数百 GB | 数百 GB 至数 TB |
| G3 | 本地 SSD | 每秒 5-10 GB | 数 TB |
| G4 | 网络 SSD | 每秒数 GB | 数十 TB |
某些 SKU(如 GB200)配备了 CPU 和互连模块,能够提供更快的 G2 存储速度,非常适合 KV 缓存卸载。
NVIDIA Dynamo 通过 KVBM(KV 块管理器)提供对 KV 缓存卸载的支持。KVBM 提供了用于在不同内存层级之间移动 KV 缓存块的 API。一般原则是:将最频繁使用的块保留在高带宽内存中,而较少使用的块则可以转移到较慢的存储介质中,待需要时再调用。
5.3.3 缓存感知路由
在生产环境中,推理服务器通常会有多个副本,传入流量会分配到各个副本上。通常,流量的路由依据是每个副本的繁忙程度。
如果您的推理服务器大量使用前缀缓存,则需要更新路由逻辑以将其考虑在内。与聊天机器人进行长对话或就某个代码库提出多个问题的用户,应尽可能将其请求路由到同一副本,以便命中缓存,从而获得更快、更低成本的请求响应。
另一种方案是使用G4网络存储在各副本之间构建全局KV缓存。路由在这里仍然重要——拥有热G1缓存的副本处理请求的速度会快于从G4读取数据的副本——但全局缓存可以确保所有副本最终都能访问任何预计算序列,并且在自动扩缩容期间节点循环或关闭时,已缓存的序列不会丢失。
5.3.4 长上下文处理
“长上下文”的定义在某种程度上是循环的:当一个序列生成的 KV 缓存足够大,在推理过程中引发问题时,它就被称为”长上下文”。
根据模型、硬件、引擎和流量的不同,这些问题可能在超过 32K、64K 或 128K token 等常见阈值后开始出现。在进行性能基准测试时,务必发送非常大的输入序列,以测试您的推理服务对长上下文请求的处理能力。
基础模型实验室一直在使用 RoPE 等扩展技术来解锁更长、更精确的上下文窗口。但支持这些升级后的上下文窗口在推理方面带来了新的挑战。
考虑到 KV 缓存,注意力计算的复杂度随序列长度线性增长。对于长序列,注意力机制可能成为 VRAM 的主要消耗者——而 VRAM 正是解码阶段的瓶颈资源。
尽管滑动窗口注意力、压缩注意力和稀疏注意力等方法针对各个模型提供了解决方案,但也存在优化标准注意力算法的通用方法:
-
FlashAttention:一系列经过优化的注意力算子,通过减少内存读写次数来高效计算注意力。
-
PagedAttention:一种内存管理技术,将 KV 缓存存储在固定大小的页中,从而减少内存碎片和重复占用。
-
分块预填充(Chunked Prefill):一种将大型输入序列拆分为多个块的策略,可在资源允许的情况下与解码并行运行,避免长序列对推理引擎造成过大压力。
但如果经过这些优化之后,单个 GPU 所提供的 VRAM 仍不足以存储 KV 缓存,该怎么办?您将需要跨多个 GPU 并行化推理。
5.4 模型并行
当今市场上每一个前沿大语言模型都太大,无法在单个 GPU 上进行批量推理。虽然 GPU 的显存容量在不断增大,但模型规模也在同步增长,这一趋势并没有显示出逆转的迹象。
在 FP8 精度下,加载十亿个模型参数大约需要 1 GB 的显存(VRAM)。对于像 DeepSeek-V3.1 这样拥有 6710 亿参数的模型,仅模型权重本身就会导致单块 B200 GPU 立即抛出内存不足(OOM)错误。
仅仅勉强将模型权重塞入显存是远远不够的。在 4 块 B200 GPU 上,拥有 720 GB 显存,理论上可以加载 DeepSeek 的权重。但由于没有剩余空间用于 KV 缓存——而 KV 缓存在权重加载后通常会占用 80% 甚至更多的剩余显存——四块 B200 GPU 将无法以任何合理的序列长度或批量大小来服务 DeepSeek。
因此,需要一个完整节点的八块 B200 GPU,才能在 DeepSeek 这种规模的模型上承载真实的生产流量。您可以通过将精度、参数数量和预期 KV 缓存分配相乘来估算模型所需的最少 GPU 数量。
在许多情况下,即使对于像 GPT OSS 这样的中等规模模型,您也希望使用超过最低要求数量的 GPU,以支持更大的 KV 缓存并实现更优的单用户延迟。
然而,所有这一切都要求推理能够高效地从单 GPU 扩展到多 GPU。并行推理扩展的瓶颈在于 GPU 之间的通信开销。
第 3 章详细介绍了 GPU 之间的各种互联方式:节点内的 NVLink 和 NVSwitch,以及节点间的 InfiniBand。
尽管 NVLink 和 InfiniBand 提供了较高的带宽,但它们的速度仍只是显存速度的一小部分。由于解码阶段受内存带宽限制,多 GPU 推理需要精心设计,以避免 GPU 间通信成为瓶颈。这一研究领域被称为拓扑感知并行。
推理中有三种主要的模型并行形式:
- 流水线并行(PP):将模型的各层分布到不同的 GPU 上。
- 张量并行(TP):将每一层内的张量分布到不同的 GPU 上。
- 专家并行(EP):将 MoE 模型中的完整专家分片到不同的 GPU 上。
每种并行形式都有其自身的权衡取舍:
| 方法 | 机制 | 缺点 |
|---|---|---|
| PP | 每个 GPU 负责前向和反向传播的一个阶段。 | 由于逐步流水线导致延迟高、利用率低,不推荐使用。 |
| TP | 矩阵乘法等计算密集型操作被分配到多个 GPU 上。 | 需要 GPU 间同步,不适合多节点场景。 |
| EP | 每个专家位于单个 GPU 内,使专家内部推理速度较快。 | 需要在 GPU 之间进行路由以访问多个专家,更适合提升吞吐量。 |
张量并行通常最适合单节点内的低延迟模型推理,而专家并行则可提高 MoE 大语言模型的吞吐量。流水线并行仅用于多节点推理。
此外,像上下文并行(Context Parallelism)这样的数据并行策略可以将计算分布到多个设备上。这些策略在大语言模型推理中较为少见,但对于视频生成(第 6.6 节)至关重要。
5.4.1 张量并行以降低延迟
张量并行应作为多GPU模型推理的默认策略。它同时支持密集模型(如Llama 405B)和目前主导开放模型领域的MoE模型。
张量并行的工作原理是将模型的每一层拆分开来(与流水线并行相反,流水线并行保持各层完整),并将层的碎片分布在所分配的各GPU上。对于每一层,读取权重内存和执行矩阵乘法的开销由各GPU共同承担。
5.4.2 专家并行以提升吞吐量
专家并行将专家整齐地分配到各个 GPU 上。在一个拥有 128 位专家、以 EP8 模式跨八个 GPU 提供服务的模型中,每个 GPU 将承载 16 个完整的专家。
EP 提升了系统的总体吞吐量,使推理更具可扩展性且成本更低。由于各个专家分别处理 token,每个 token 的处理时间不变,但整个系统能够同时处理更多的 token。
许多部署方案混合使用 TP 和 EP,以同时获得两者的优势。
专家并行所需的 GPU 间通信量少于张量并行。专家路由器负责决定每个 token 激活哪些专家,由于它是模型中相对较小的组件,因此会被复制到每个 GPU 上。在专家之间传递 token 时需要进行 GPU 间通信,但与 TP 不同,无需在每一层收集结果时进行通信。
得益于较低的通信开销,EP 能够良好地扩展至多节点部署以及互联带宽有限的系统。
5.4.3 多节点推理
如果你需要以高精度部署超大规模模型、支持数百万token的输入序列,或者只是想尽可能快地运行推理,那么可能需要超过八块GPU。
GPU天生适合跨节点协同工作,多节点训练多年来一直是开发前沿模型的标准做法。但多节点推理带来了新的挑战:
-
基础设施:如何可靠地部署两个或更多互联的GPU节点,并在云服务商之间构建抽象层(第7章)?
-
并行性:如何在InfiniBand上进行高效通信?InfiniBand的速度远慢于NVLink。
InfiniBand为拓扑感知并行性引入了新的复杂性。张量并行通常需要大量跨GPU通信,因此并不适合多节点推理。相反,有两种方案在InfiniBand上表现良好:
- 对于稠密模型,在每个节点内使用张量并行,节点间使用流水线并行(例如TP8PP2)。
- 对于MoE模型,也可以尝试专家并行(例如EP16),因为其通信开销低于张量并行。
对于MoE模型,TP8PP2通常可为每个用户提供更低的延迟,而EP16则能带来更高的整体系统吞吐量。
除非你的模型和KV缓存大到必须使用多节点推理,否则这可能并不是利用额外硬件的最佳方式。通常情况下,将额外节点用于副本的横向扩展或分离式服务会更加合适。
5.5 分离式推理
分离式推理结合了推理工程中三个重要理念:
- 预填充(Prefill)是一个计算密集型过程,决定了首个Token的生成时间(TTFT);而解码(Decode)是一个内存密集型过程,决定了每秒生成的Token数(TPS)。
- 专业化分工可以提升各方面的性能,从内核选择到推理引擎参数调优皆是如此。
- 如果能够避免低带宽互联所带来的瓶颈,就可以有效地将模型推理并行化到多个GPU甚至多个节点上。
当预填充和解码在高负载流量下运行于同一节点时,二者相互干扰的概率会更高。理想情况下,预填充使用更多的计算资源,而解码使用更多的内存资源,两者可以高效共存。然而,随着批处理规模的增大以及计算密集型优化的增多,预填充和解码开始相互争夺资源。
5.5.1 disaggregation的工作原理
disaggregation,即分离式服务,是指将预填充(prefill)和解码(decode)分离到不同GPU或节点上的独立引擎中的理念。
disaggregation将大语言模型推理转变为三步流程:
- 预填充引擎接收输入序列,在计算第一个token的同时生成KV缓存。
- 预填充引擎通过硬件互联将KV缓存传输至解码引擎。
- 解码引擎计算所有后续token。
在条件disaggregation中,请求首先被发送至解码引擎,由其检查输入序列是否已被缓存或长度足够短以便在本地处理:
- 若满足条件,解码引擎在本地处理预填充,跳过disaggregation流程。
- 若不满足条件,解码引擎将请求转移至预填充引擎进行分离式服务。
条件disaggregation更适合真实世界的流量场景。
disaggregation的另一个优势在于,通过独立的预填充和解码引擎,可以对每个引擎以及整个系统进行单独优化。例如,受计算瓶颈限制的预填充引擎所需的张量并行度(TP)低于受内存瓶颈限制的解码引擎。
5.5.2 何时使用分离式架构
分离式架构非常强大,但需要多块 GPU 以及额外的工程工作。只有在以下情况下,您才应考虑使用分离式架构:
- 您正在处理大量流量,根据模型大小,每天的 token 处理量从一亿到十亿不等。
- 您正在服务一个较大的模型,至少拥有千亿参数。
- 您的流量以预填充为主,且输入序列较长。
如果第一点或第二点不成立,您很可能在为微乎其微的性能提升而浪费额外的硬件成本。如果第三点不成立,您或许更适合利用额外的 GPU 横向扩展副本数量,因为对于短序列或前缀缓存命中的情况,解码引擎的效率会更高。
分离式架构的一个典型应用场景是在代码编辑器中服务前沿大语言模型,众多开发者同时将大量各异的代码片段作为上下文传入。海量 token、以预填充为主、运行在万亿参数级大语言模型上——这正是分离式架构的教科书式工作负载。
5.5.3 使用 NVIDIA Dynamo 实现动态解耦
Dynamo 为解耦提供了生产就绪的支持,能够灵活处理异构的真实流量。
Dynamo 提供开发者工具和预构建优化以实现解耦:
-
一个预填充队列,用于在所有预填充引擎饱和时保存请求。
-
对条件解耦的强大支持,基于前缀缓存后的 ISL 和预填充队列大小的可配置阈值进行预填充路由。
-
高效的基于 NIXL 的 KV 从预填充引擎到解码引擎的传输,当引擎具有不同的 TP 配置时,使用内核在布局之间转置 KV 块。
综合来看,这些功能实现了动态解耦,即预填充和解码引擎的数量可在运行时配置,并可随时间调整以匹配传入流量的变化特性。
解耦不需要在预填充引擎和解码引擎之间保持一对一的比例。虽然用单个预填充引擎和单个解码引擎来解释解耦很简单,但实际系统中两者各有多个。
预填充和解码引擎的数量记为 xPyD,例如,5P3D 表示五个预填充引擎和三个解码引擎协同工作,共同服务于单个模型部署。
随着系统变得越来越复杂,潜在的瓶颈也越来越多。在解耦中,新的瓶颈是预填充队列大小。重要的是不要让队列增长过大,既要为解码引擎上的本地预填充设置合理的阈值,也要在运行时重新配置 xPyD,以便在需要时为预填充分配更多资源。
解耦中另一个潜在瓶颈是在高负载下解码引擎上的 KV 缓存耗尽。可通过量化和 KV 缓存卸载来提高 KV 缓存的可用性。