vLLM 中 FP8 KV 缓存与注意力机制量化的现状
简介
长上下文 LLM 服务正日益受到内存限制:对于标准的全注意力解码器,当上下文达到 128k 以上时,KV 缓存通常会占据 GPU 内存的大部分,且每个解码步骤都必须读取该缓存的很大一部分。因此,若精度能够维持,通过 FP8 将 KV 缓存存储空间减半,可以在相同的硬件成本下转化为更高的并发度或支持更长的上下文。
vLLM 的 --kv-cache-dtype fp8 标志会对 KV 缓存进行量化,并在 FP8 中运行整个注意力计算(QK 和 ScoreV 矩阵乘法,本文全程使用 e4m3 格式)。该功能在 vLLM 中已存在一段时间,但它在预填充(prefill)密集型和解码(decode)密集型工作负载的压力测试下表现如何?我们对仅解码模型和 MoE 模型,以及 Hopper 和 Blackwell 架构进行了全面验证。我们识别并修复了 Flash Attention 3 (FA3) 后端中的关键精度和性能问题(见图 1 示例)。在本文验证的路径中,该功能在保持接近基准精度的同时,降低了解码成本和 KV 缓存内存占用。主要注意事项包括:具有小滑动窗口层的混合注意力模型(通常跳过这些层会更好),以及大头维度模型(head_dim = 256,其预填充性能可能仍会倒退)。此外,对于头维度 64 和 128,FP8 格式在预填充和解码方面均提供了加速。对于受内存限制的解码,KV 缓存的每 Token 成本在最佳情况下可降至 BF16 的 54%。对于像 256 这样大的头维度,FP8 也降低了 ITL(Token 间延迟);但默认的预填充性能目前仍略逊于 BF16。

目录
快速入门
# FP8 KV-cache for all layers
vllm serve meta-llama/Llama-3.1-8B --kv-cache-dtype fp8
# FP8 KV-cache, skipping sliding-window layers (recommended for hybrid-attention models)
vllm serve gpt-oss-20b --kv-cache-dtype fp8 --kv-cache-dtype-skip-layers sliding_window我们发现的问题
尽管 --kv-cache-dtype fp8 在 vLLM 中已发布一段时间,但我们的压力测试揭示了两类问题:
精度: 在 Hopper GPU 上,FP8 Flash Attention 3 内核在长上下文中遭受了累加精度损失。在 128k 大海捞针任务中,FP8 精度从 91%(BF16 基准)骤降至仅 13%——该回归源于张量核心(Tensor Cores)中不精确的 FP32 累加(见下文的双级累加修复)。
性能: 带有滑动窗口注意力层的模型(如 gpt-oss-20b)的 FP8 ITL 斜率几乎与 BF16 相同(BF16 斜率的 96%),这意味着尽管内存减半,用户几乎没有获得任何解码提速。盈亏平衡点超过了 700k Token,这远超大多数实际上下文长度。
以下部分描述了我们为解决这些问题而发布的改进。
内核与 vLLM 的改进
在调查期间,我们发布了多项改进,以增强量化方案的灵活性、修复精度问题并提升性能。我们在此简要介绍这些改进。
双级累加: Hopper 的 FP8 张量核心记录为累加到 FP32 寄存器中,但实际上当收缩维度很大时,中间累加会丢失精度——这是一个已知的硬件级问题,在 DeepSeek-V3 训练期间也遇到过(见 DeepSeek-V3 技术报告 中的图 7(b))。当收缩维度达到 100K 或更多时,这种不精确的累加会导致严重的数值误差。具体来说,在长上下文推理中,在 Softmax(AttnScore) * V 矩阵乘法期间,收缩维度对应于上下文长度。根据经验,我们发现这会导致长上下文大海捞针任务中精度从 91% (BF16) 回归到 13% (FP8)。为了缓解这种情况,我们添加了一种双级累加策略(见 SageAttention2),将部分累加结果写入实际的 FP32 寄存器(flash-attention#104),这使 FP8 精度恢复到了 89%。缺点是,这种双级累加增加了寄存器压力,并导致预填充期间速度减慢。我们通过优化的分块配置(flash-attention#125)部分解决了这个问题,然而对于大于 128 的头维度,预填充性能仍落后于 BF16。
跳过层: 早期,vLLM 只允许用户为所有注意力层选择单一数值格式。我们添加了 --kv-cache-dtype-skip-layers (vllm#33695) 以允许混合设置。像 GPT-OSS 这样的模型会在某些层中使用滑动窗口注意力,其中 Token 关注固定的窗口大小(例如 128 个 Token)。在这种情况下,FP8 的开销无法被摊销。因此,将这些层保留在 BF16 中实际上比量化它们更快(见我们下文的实证结果)。此外,如果某些层对量化特别敏感,此功能允许跳过这些层。
每头缩放(Per-Head Scales): Flash Attention 3 内核允许为注意力操作的 FP8 量化指定一组缩放因子,每个缩放因子对应一个 KV 头。在 vLLM 中启用此功能需要为所有组形状(group-shapes)泛化静态量化支持(vllm#30833),并扩展 reshape_and_cache_flash 内核(vllm#30141)的范围,以处理一组缩放因子而不是单个标量。
查询量化融合: 我们将查询(Query)量化从注意力后端移除,改为简单的 torch 实现,torch.compile 可以将其融合到周围的操作中,消除了固定的每 Token 开销(vllm#24914)。
优化的 FA3 FP8 分块大小: 我们调整了 head_dim = 64 和 head_dim = 128 的预填充分块配置,以减少因双级累加引入的寄存器溢出(flash-attention#125)。此外,我们针对受内存限制的解码工作负载添加了专门调整的配置,这些配置极大地减少了 ITL 中与上下文相关的增长(即斜率)(flash-attention#96, flash-attention#91)。
性能基准测试
对于长上下文 LLM 服务,注意力在解码过程中可能产生显著成本。每个生成的 Token 都必须对完整的 KV 缓存进行注意力计算,因此 Token 间延迟(ITL)随输入长度线性增长。将 KV 缓存从 BF16 量化为 FP8 可将每个缓存 Token 的内存占用减半,从而使每个注意力步骤的内存流量减半,这直接转化为更低的 ITL 斜率。由于 Hopper 和 Blackwell GPU 为 FP8 提供的 FLOPs 是 BF16 的两倍,理想情况下我们也期待预填充速度的提升。然而在实践中,正如我们在下文中展示的那样,这些增益并非总是即刻可见的。
单请求基准测试
为了清晰地理解注意力行为,我们首先展示并发度为 1 的基准测试,稍后再介绍批处理推理的情况。对于并发度 1,Token 间延迟 (ITL) 和首 Token 时间 (TTFT) 是完全分离的,我们可以将 ITL 与输入长度拟合为一个线性模型:
ITL = 斜率 × 输入长度 + 截距
并将 TTFT 拟合为一个二次模型:
TTFT = a × 输入长度² + b × 输入长度 + c
ITL 斜率直接反映了每 Token 的注意力成本——较低的斜率意味着每个额外的缓存 Token 增加的延迟更少,这对长上下文工作负载至关重要。盈亏平衡点是 FP8 ITL 低于 BF16 ITL 的上下文长度;超过此点,FP8 在解码方面绝对更快。二次 TTFT 模型捕获了受计算限制的预填充阶段,在该阶段,由于对输入进行自注意力计算,成本随序列长度呈二次方增长。
所有 Hopper 实验均在单个 H100 GPU 上使用 FlashAttention-3(通过 vLLM 分支)运行,该分支提供带有在线 Softmax 重缩放功能的原生 FP8 KV 缓存支持。我们使用单个 GPU,运行 vllm bench serve,并发度为 1,输出长度为 128 Token,输入长度从 256 到 125k Token 不等。
图 2 展示了 Llama-3.1-8B 模型的结果。

拟合的 ITL 斜率从 4.37e-05 降至 2.37e-05 ms/Token,而截距仅从 6.44 变为 6.58 ms。FP8 的斜率比率为 BF16 的 54%,接近最优值,截距差距仅为 0.14 ms。这使得盈亏平衡点降低至约 7k Token。此外,即使启用了双级累加,我们在长上下文中依然获得了轻微的 FP8 TTFT 加速。
接下来,我们转向 gpt-oss-20b,这是一款具有混合注意力架构的 20B 参数模型,具有全局层和滑动窗口层(窗口大小为 128)。滑动窗口层具有受限的 KV 缓存大小,因此在长上下文中量化它们收益递减。使用 --kv-cache-dtype-skip-layers sliding_window,我们将这些层保留在 BF16 中,仅将全局注意力层量化为 FP8。图 3 报告了 KV 缓存分别采用 BF16、FP8 以及跳过滑动窗口层的 FP8 时的模型结果。

拟合的 ITL 斜率从 BF16 中的 8.94e-06 ms/Token 降至全 FP8 中的 7.14e-06 和跳过 SW(滑动窗口)的 FP8 中的 6.34e-06,同时截距紧密分布在 4.03 到 4.07 ms 之间。FP8 斜率分别为 BF16 的 80%(全 FP8)和 71%(跳过 SW),这使得 FP8 成为一个有吸引力的选择。在我们的改进之前,BF16 和 FP8 的斜率几乎相同。
跳过滑动窗口的变体是最终赢家:通过将受限的滑动窗口层保留在 BF16 中(量化在那里会增加持续开销,但在长上下文中没有内存节省),它以极小的截距代价实现了最低斜率。因此,我们建议使用此变体。
实践总结:对于长上下文解码密集型服务,当 KV 缓存流量占主导地位时,FP8 最具吸引力;在 H100 上,对于 Llama 类模型以及跳过小滑动窗口层的混合模型,它显然是有益的。
下表总结了我们改进前后的单请求性能,显示出盈亏平衡点和 ITL 斜率的显著降低。
表 3:两个模型和 KV 缓存变体的改进总结。
| 模型 | 版本 | FP8 变体 | 盈亏平衡点 (Token) | FP8 斜率 (% of BF16) |
|---|---|---|---|---|
| Llama-3.1-8B | 之前 (v0.10.2) | FP8 | 24,889 | 63% |
| Llama-3.1-8B | 之后 (v0.19.1) | FP8 | 7,010 | 54% |
| gpt-oss-20b | 之前 (v0.10.2) | FP8 | 741,565 | 96% |
| gpt-oss-20b | 之后 (v0.19.1) | FP8 | 22,109 | 80% |
| gpt-oss-20b | 之后 (v0.19.1) | FP8 跳过 SW | 7,659 | 71% |
负载下的吞吐量
上面的扫描隔离了并发度 1 时的每 Token 注意力成本。为了在真实条件下衡量端到端服务性能,我们运行了吞吐量基准测试:在并发度 8 下运行 150 个请求,每个请求约 20k 输入 Token 和 2k 输出 Token(±15% 偏差)。我们在表 4 和表 5 中报告了结果。
表 4:Llama-3.1-8B 模型在高吞吐量负载以及 BF16 和 FP8 格式 KV 缓存下的性能结果。FP8 的输出吞吐量提高 14.9%,总运行时缩短 13.0%,中位数 ITL 降低 14.8%。
| 配置 | 中位数 TTFT (ms) | 中位数 ITL (ms) | 总持续时间 (s) | 输出 Token/s |
|---|---|---|---|---|
| BF16 | 763.6 | 15.18 | 672.6 | 450.3 |
| FP8 | 742.8 | 12.93 | 585.2 | 517.5 |
表 5:gpt-oss-20b 模型在高吞吐量负载以及 BF16、FP8 和跳过滑动窗口的 FP8 格式 KV 缓存下的性能结果。FP8 跳过 SW 的输出吞吐量提高 4.8%,总运行时缩短 4.6%,中位数 ITL 降低 4.8%。
| 配置 | 中位数 TTFT (ms) | 中位数 ITL (ms) | 总持续时间 (s) | 输出 Token/s |
|---|---|---|---|---|
| BF16 | 468.9 | 8.09 | 364.2 | 831.6 |
| FP8 | 451.7 | 7.90 | 355.1 | 853.0 |
| FP8 跳过 SW | 456.4 | 7.70 | 347.4 | 871.8 |
这些吞吐量结果证实,单请求的 ITL 改进转化为负载下的实际服务收益。对于 Llama-3.1-8B,并发度 1 下 54% 的 ITL 斜率降低转化为并发度 8 下 14.9% 的输出吞吐量提升——FP8 不仅使每个 Token 解码更快,而且 2 倍的内存节省还允许调度程序打包更多并发请求。对于 gpt-oss-20b,收益较小(4.8%),因为模型的滑动窗口层限制了内存节省;跳过 SW 的变体通过避免对无法从量化中受益的层产生开销,恢复了大部分收益。
请注意,这些基准测试使用了并发度 8 和约 20k Token 的输入,这属于中度负载。在更高的并发度或更长的上下文中,FP8 的内存节省影响更加显著,因为 BF16 会触发 OOM 或需要更激进的 KV 缓存驱逐。
大头维度模型的局限性
通过 flash-attention#104,我们默认启用了双级累加,以确保最高的模型质量并防止用户出现意外的精度下降。然而,对于大的头维度,这会导致 TTFT 比 BF16 更慢。
为了说明这一点,图 4 报告了 H100 上 gemma-4-E2B 的结果,它使用了 head_dim = 256。它还有 4 层中的 3 层具有大小为 512 的滑动窗口。

对于 gemma-4-E2B,ITL 斜率从 5.30e-05 降至 3.60e-05 ms/Token,而 TTFT 二次系数从 6.93e-07 升至 1.12e-06 ms/Token²。因此,FP8 在测量范围内提供了明确的解码优势(斜率为 BF16 的 68%)。此外,由于 gemma-4-E2B 的滑动窗口大小 (512) 是 gpt-oss-20b (128) 的 4 倍,每个窗口内有足够的数据来摊销 FP8 开销,因此对滑动窗口层进行量化也是值得的。这提供了一个相对于跳过滑动窗口层的恒定抵消。然而,FP8 的 TTFT 二次系数是 BF16 的约 1.6 倍,这意味着由于 head_dim = 256 时双级累加的寄存器压力,预填充在长上下文中变得明显变慢。
目前有两种方法可以解决此问题:1) 用户可以禁用双级累加以提高性能。然而,在这种情况下,我们建议对相关工作负载进行广泛的精度测试。2) 可以让累加仅每 N 步发生一次,而不是每一步。可以在此 PR flash-attention#122 中找到功能性实现,它恢复了预填充的加速。请注意,特别是第一种选择对于头维度 64 和 128 也将提供更大的预填充加速。
Blackwell (B200) GPU 上 FlashInfer 的性能表现
虽然我们的主要性能改进针对 H100 和 Flash-Attention 3,但为了完整起见,我们还提供了 B200 上使用 FlashInfer 后端的基准测试。请注意,在 B200 上,累加问题已修复,因此不需要显式的双级累加。图 5 和 6 分别可视化了 Llama-3.1-8B 和 gpt-oss-20b 的性能。

对于 B200 上的 Llama-3.1-8B,拟合的 ITL 斜率从 1.80e-05 降至 9.72e-06 ms/Token,而截距仅从 3.93 变为 3.96 ms。

对于 B200 上的 gpt-oss-20b,拟合的 ITL 斜率从 3.56e-06 降至 2.06e-06 ms/Token,截距从 3.15 变为 3.17 ms,拟合得出在约 13k Token 时达到盈亏平衡。与 H100 不同,这些 B200 基准测试仅比较了 BF16 和 FP8(没有跳过 SW 变体),因为在进行实验时,B200 尚未支持跳过层。
精度基准测试
我们关注以下模型:Llama-3.3-70B-Instruct、Qwen3-30B-A3B-Instruct-2507、Qwen3-30B-A3B-Thinking-2507 和 Qwen3.5-27B。
对于长上下文(预填充密集型)评估,我们使用 openai/mrcr 任务,测试长度高达 1M 的序列。我们报告每个序列长度区间在 5 次重复中的平均 pass@1 分数,以及作为跨所有测试长度聚合指标的曲线下面积 (AUC) (Context Arena)。
对于推理(解码密集型)评估,我们使用 AIME25、GPQA:Diamond、MATH500 和 LiveCodeBench-v6。我们报告平均 pass@1 分数:AIME25 和 LiveCodeBench-v6 为 10 次重复,GPQA:Diamond 和 MATH500 为 5 次重复。
所有评估均采用模型创建者建议的默认非贪婪采样参数,以模拟真实世界的部署。
重要: 所有评估均使用每张量(per-tensor)未校准的量化缩放因子(即缩放因子 = 1.0)。这是最简单的配置——没有校准数据,没有每头调整——代表了精度的最差情况。我们选择此设置有两个原因:1) 任何 vLLM 用户都可以通过 --kv-cache-dtype fp8 轻松复现;2) 它确立了下限——使用校准缩放因子的结果只会更好。然而,我们也支持在目标数据上校准量化缩放因子,以及更高粒度的缩放因子(每注意力头而不是每张量)。有关这些功能的更多详细信息,请参阅以下部分。
推理能力评估
图 7 比较了 Qwen3-30B-A3B-Thinking-2507 的两个版本——原始 BF16 模型及其 FP8 权重和激活量化变体——在推理基准测试中的表现,这些基准测试特征是简短的预填充后跟漫长的解码密集型生成,通常达到数万个 Token。这些基准测试验证了 FP8 KV 缓存和注意力量化是否会在长生成链中降低推理能力。

FP8 KV 缓存和注意力量化引入了最多 1-2 个点的精度下降,最低恢复率为 97%(GPQA:Diamond,BF16 模型)。
在图 8 中,我们报告了仅解码模型 Qwen3.5-27B 的同一组基准测试,使用了 BF16 和 FP8 权重和激活配置。

FP8 KV 缓存和注意力量化显示精度影响可忽略不计(最多 0.7 点),BF16 模型在 AIME25 上的最低恢复率为 99%。
长上下文评估
我们使用 openai/mrcr 长上下文数据集进行评估,该数据集特征是繁重的预填充(长上下文)后跟简短解码。这验证了 FP8 KV 缓存和注意力量化即使在 1M Token 的提示下也能维持模型能力。
图 9 描绘了 Llama-3.3-70B-Instruct(未量化的 BF16)及其权重和激活 FP8 量化变体在从 8k 到模型最大输入长度 128k 的序列长度区间内的精度。

FP8 KV 缓存和注意力量化恢复了 128k 基准 AUC@1 的 97-98%。
在图 10 中,我们将焦点转向 MoE 模型 Qwen3-30B-A3B-Instruct-2507。

尽管所有区间的得分方差较高(如两图所示),但整体 AUC@256k 指标仍接近基准,恢复范围从约 94% 到 98% 不等,具体取决于底层模型权重和激活是 BF16 还是 FP8。方差增加归因于基准模型稍显不稳定的行为(例如,32k 的精度 > 8k/16k 的精度;128k 的精度 > 64k 的精度)。
最后,我们重点关注非常新的 Qwen3.5-27B 模型,它支持高达 1M Token 的输入序列长度,并在所有考虑的序列长度上展示了非常有竞争力的精度。

图 11 显示,即使在强基准模型的 1M Token 极限下,FP8 KV 缓存和注意力量化也完全恢复了聚合 AUC@1M 指标。
Blackwell (B200) GPU 上 FlashInfer 的精度表现
我们还检查了 FP8 KV 缓存和注意力量化在较新的 Blackwell 架构上的表现。与需要双级累加等干预措施以实现 Flash Attention 内核精度的 Hopper 不同,Blackwell 利用默认的 FlashInfer 内核,消除了这些精度问题。我们复制了确切的 Hopper 实验:在 openai/mrcr 长上下文基准上进行 Qwen3-30B-A3B-Instruct-2507 (BF16/FP8) 实验,在推理基准上进行 Qwen3-30B-A3B-Thinking-2507 (BF16/FP8) 实验。结果见图 12 和图 13。


在配备 FlashInfer 后端的 B200 GPU 上,FP8 KV 缓存加 FP8 注意力在保持精度的同时具有竞争力,并保留了相同的核心系统优势:更小的 KV 缓存和更低的解码成本。在我们的结果中,精度匹配仍然很强,尽管并不总是像最佳的 Hopper/FA3 情况那样紧密。
总结
我们的主要结论是,FP8 KV 缓存量化现在已准备好成为许多长上下文 vLLM 部署和硬件环境的默认起点。如果您的工作负载是解码密集型且受内存限制,FP8 可以带来显著的延迟和容量增益,且仅有很小或可忽略不计的精度损失。主要例外是预填充在 head_dim = 256 上占主导地位的工作负载、应保留在 BF16 中的小型滑动窗口注意力层的混合模型,以及显示持续未校准精度损失的模型或后端,在这些情况下,校准仍然很重要。
虽然我们这里主要关注最简单(未校准缩放因子)的配置,但我们也为利基部署中的更好精度恢复实现了两个附加功能:1) 通过 vllm-project/LLM-Compressor 使用用户提供的数据集启用缩放因子校准,2) 支持更细粒度的 每注意力头量化缩放因子。 有关详细示例,请参阅 vLLM 示例。
何时应该使用校准?
并非所有模型都能很好地使用未校准的 FP8 缩放因子。为了说明这一点,我们在 H200 GPU 上测试了 Kimi-K2.5 模型——它使用 Flash MLA 注意力后端,与上述模型的内核路径不同——使用了未校准的 FP8 KV 缓存量化。
图 14 显示了跨序列长度区间的持续向下偏移。虽然聚合 AUC 下降幅度不大,且标准误差带重叠,但这种退化是系统的而非随机的。实践总结:从未校准的 FP8 开始,因为它简单且通常足够好,但如果您在实际工作负载而不是孤立的嘈杂区间中观察到这种持续的向下偏移,则进行校准。这对于使用非标准注意力后端(例如 FlashMLA)的模型尤其相关,这些模型的 FP8 内核行为可能与经过充分验证的 FA3 和 FlashInfer 路径不同。

何时应避免使用 FP8 KV 缓存
FP8 KV 缓存量化并不总是正确的选择。如果出现以下情况,请考虑继续使用 BF16:
- 上下文很短(< ~7k Token): FP8 具有较小的持续开销(截距差距),因此在短上下文中,BF16 在 ITL 方面可能稍快。
- 模型使用
head_dim = 256且预填充延迟至关重要: 在长上下文中,双级累加开销使 TTFT 增加约 1.6 倍。禁用双级累加可以恢复速度,但需要仔细的精度验证。 - 工作负载下的未校准精度降至 95% 以下: 一些模型(例如带有 FlashMLA 的 Kimi-K2.5)在未校准缩放因子下显示出持续退化,并可能从目标数据集上的校准中受益。
- 模型具有许多小型滑动窗口注意力层: FP8 开销可能无法很好地摊销;对于混合注意力模型,首选
--kv-cache-dtype-skip-layers sliding_window。