首页 / 资讯中心 / 文章详情

FP8 KV Cache实测:显存减半但吞吐提升有限,长上下文精度有损

FP8 KV Cache实测:显存减半但吞吐提升有限,长上下文精度有损 ★ FEATURED ARTICLE
我先把这次实测的结论放在前面FP8 KV Cache确实能砍掉一半的KV缓存占用但对吞吐量的提升远没有宣传中那么性感而且长上下文场景下精度损耗会毫不客气地浮现出来。至于压测环节这次顺手找到了好几个常见的基准测试陷阱任何一个都会让你得出完全错误的结果。先说下背景。我在做长文场景的推理优化手头有8卡A100 80G的集群主要跑Llama-3-8B-Instruct这类模型。长上下文推理最头疼的问题就是KV Cache把显存吃掉大半导致并发上不去、batch拉不高。看到vLLM支持--kv-cache-dtype fp8之后我本能地觉得这是个白捡的优化于是写了一套对比方案准备验证。在真正跑数据之前我做了四个预测KV Cache显存占用量减半同样的显存预算下能承载更长的上下文或更大的并发量化精度损失对回答质量几乎无影响解码吞吐量会有可观提升。实测下来预测一和预测二基本命中预测四只在长上下文场景下成立预测三则在某些任务上彻底打脸。更意外的是做对比时我发现之前的压测脚本本身有几个隐蔽问题差点让两组数据没法解释。这篇文章把整个测试过程、数据结论和压测方法上的坑都整理出来给正准备做这件事的你一个参考。1. 为什么要碰FP8 KV Cache1.1 KV Cache是个什么角色为什么它这么吃显存KV Cache是自回归生成过程中为了省掉重复计算而缓存下来的每层Key矩阵和Value矩阵。每生成一个新token模型都要把当前token的K、V追加到缓存里同时所有之前的token对应的K、V也要重新参与注意力计算。所以KV Cache的大小跟序列长度严格成正比而且是线性增长。以Llama-3-8B为例32层、8个KV头、每个头128维FP16精度下每新增一个token的KV占用是2K和V各一份× 32层 × 8个KV头 × 128维 × 2字节 131072字节 128KB单看128KB好像不多但上下文长度一上来就不一样了。8K上下文时单个序列的KV Cache是1GB32K时是4GB。8B模型的FP16权重本身也就16GB左右跑32K上下文时KV Cache直接占掉25%的显存预算。再乘上并发数这个数字很容易把整张卡吃穿。对比之下如果你把KV Cache从FP16压到FP8单token占用降到64KB32K上下文只需要2GB同样的显存预算就能多跑一倍的并发序列或翻倍的上下文长度。1.2 FP8量化到底在压什么压完损失在哪这里要澄清一个点FP8 KV Cache没有对模型权重做任何改动它只动缓存。vLLM在生成过程中把K、V矩阵以FP8 E4M3格式写入KV缓存池读取时再反量化回注意力计算所需的高精度。E4M3这种格式用一个符号位、四个指数位、三个尾数位表示数值动态范围大致能覆盖正常训练和推理中的数值分布代价是尾数精度很粗。粗到什么程度3个尾数位意味着尾数方向上只有8个步进档位落在某个档位区间内的数值最多可能引入约6%到12%的相对误差。模型权重如果被量化到这个精度很多模型直接崩但KV Cache里的K、V本质上只是中间激活数值分布相对集中而且误差不一定会线性传导到最终输出上所以业界才敢尝试。不过“敢尝试”和“能放心用”是两回事这也是我坚持做精度对比的原因。1.3 这次实测要回答的几个问题我给自己定的验证目标很简单就三个问题FP8 KV Cache在真实服务负载下能让KV显存实际占用量下降多少在显存不再吃紧之后吞吐量和延迟是否有可测提升量化的数值误差在短上下文和长上下文任务里到底会不会影响回答质量。前两个问题靠压测回答第三个问题得用一组内容质量对比来打分。这里多说一句如果你只是跑一下官方脚本看吞吐量数字很容易忽略精度问题因为很多基准负载根本不检查生成内容的正确性。2. 测试环境与压测方法设计2.1 软硬件配置和模型选择这次测试我没有动用8卡单张A100 80G就够说明问题主要是为了排除张量并行的干扰。详细环境如下项目配置GPUNVIDIA A100 80GPCIe版显存带宽约2TB/sCPU64核内存256GB推理引擎vLLM 0.7.x模型Llama-3-8B-InstructFP16权重基准脚本vLLM自带的benchmark_serving.py监控工具nvidia-smi、vLLM的/metrics接口选Llama-3-8B的原因很简单它是我实际生产中最常用的模型档位权重体积适中FP16约16GBKV Cache占比刚好处于“有感知但不完全爆掉”的区间特别适合观察量化效果。如果你用的模型是7B/13B甚至更大的MoE结论的数值会变但趋势是类似的。2.2 对照组设计与负载场景划分为了做公平对比我固定了所有能固定的参数启动参数里gpu-memory-utilization统一设0.9max-model-len统一设32768压测时请求速率、并发数、输入长度、输出长度两组完全一致只切换--kv-cache-dtype即auto和fp8对照每组测试跑三轮取中位数作为最终结果避免单轮抖动。负载场景我拆成四档分别覆盖短上下文和高前缀命中场景场景输入长度输出长度说明S1512512短对话S240961024中长文档摘要S381922048长文档抽取S4163842048超长上下文问答每个场景用benchmark_serving.py压500个请求请求速率从1逐步升到16观察从从容到过载的变化曲线。2.3 指标约定别被平均数和瞬时值忽悠这次压测我不只看整轮的平均吞吐量和平均延迟而是拆成了四个维度TTFTTime To First Token从请求发出到第一个token返回的时间反映预填充阶段的性能TPOTTime Per Output Token每生成一个token的耗时反映解码阶段单步性能端到端时延整个请求从发起到流式结束的总时间有效并发下的总吞吐量按token数统计的每秒生成速率。同时记录错误请求数。基准脚本默认输出这些指标但默认的汇总会给你一个“平均”后面会说为什么平均数会骗人。3. 四句预测到底对了几句3.1 预测一KV Cache显存占用量减半——完全命中KV Cache的显存占用确实是按量化精度成比例缩水的。FP16单token是128KBFP8是64KB。对单序列32K上下文来说KV池里的实际数据从4GB降到2GB。有个细节容易被忽略vLLM启动时会预先划走一整块KV Cache池不是按需一个个分配。所以你在nvidia-smi里看进程显存占用FP16和FP8可能差不多因为池的总大小由gpu-memory-utilization决定两块配置都划了差不多大小的池。真正变的是池里能放的条目数FP16的池可以容纳327680个tokenFP8后变成655360个。想看真实占用不要只看nvidia-smi看vLLM的KV Cache使用率指标更准。在/metrics接口里能查到实际使用的KV块数和整池容量的比值。我们这里测试是FP16配置下8K并发负载的KV使用率大约在65%到70%换成FP8后同样负载直接降到35%上下这就是实打实的节省。3.2 预测二同样显存能承载更长的上下文或更大的并发——命中但有前提这个预测的核心逻辑是KV Cache占用变少单个序列能跑更长的上下文或者同一上下文长度下能容纳更多并发序列。实测确实如此。FP16配置下单卡max-model-len设为32768并发稍微拉高就可能因为KV不足报错FP8配置下同样的并发在64K上下文范围内依然稳得住。但有个前提条件很容易被忽略KV Cache不再是瓶颈之后并发上限会被max-num-seqs这个参数卡住。vLLM最多同时处理max-num-seqs个序列默认值通常是256。如果你原本的并发压力在128以内切FP8后会感觉“显存突然多了”但不会带来成倍的并发提升因为调度器根本没放那么多序列进来。换句话说FP8是在搬走你眼前的石头但门框本身还有限宽。如果想享受并发红利别忘了同步调大max-num-seqs。3.3 预测三输出质量几乎无损——打脸现场这部分是我最想详细说的因为只看跑分的话完全看不出来。普通代码生成、翻译、摘要类任务FP16和FP8的输出没太大差异Token级别的差异率约在1%到3%之间属于人类难以感知的范围。但换成长上下文多跳推理和需要精确引用原文的任务后问题就冒出来了。我设计了一组“证据定位”测试在8K到16K的文档中安插具体数字和专有名词然后让模型回答“文档里出现的第三组实验数据对应的值是多少”。FP16能准确引用原文内容FP8则出现了两种典型错误把相近的数值混淆例如把0.73答成0.83省略细节直接给出一段似是而非的概括性回答。我初步归因于两个因素一是长上下文中注意力分布趋于稀疏大量低概率位置在量化后就丢失了细微差别二是序列越长量化误差在多层传递中不断累积最终在关键token上体现出来。更明显的差异出现在长文本复制生成上。让模型连续复述一段原文FP8在复述超过一定长度后开始出现个别词替换这在你做检索增强生成RAG时是很危险的。我做RAG场景验证时FP8版本的引用文本出现了轻微的拼接错误。所以预测三在短上下文基本成立长上下文则部分失效。3.4 预测四吞吐量能明显提升——一半对一半错这个预测的偏差主要来自我对性能瓶颈的误判。解码阶段生成一个token需要把全部模型权重从显存读一遍同时还要读取当前上下文对应的KV Cache。Llama-3-8B的FP16权重有16GBA100显存带宽约2TB/s理论上每生成一个token至少要花16GB / 2TB/s 8ms。KV Cache读取量要看上下文长度FP16下32K上下文是4GB额外增加2msFP8下变成2GB只增加1ms。所以两者的理论差异不是“吞吐量翻倍”而是从(164)GB / 2TB/s变成(162)GB / 2TB/s只缩减了大约11%。上下文越短权重读取占大头FP8的优势越不明显。下面这张表是我按纯显存带宽估算的理论解码耗时对比上下文长度FP16 KV Cache读取量FP8 KV Cache读取量理论单token耗时FP16理论单token耗时FP8理论提升8K1GB0.5GB8.5ms8.25ms3%左右32K4GB2GB10ms9ms10%左右64K8GB4GB12ms10ms17%左右实测结果和这个估算大体吻合。短上下文的吞吐量提升只有2%到5%几乎可以忽略32K上下文时提升约10%到12%在64K以上、同时KV Cache读取占比足够高的场景提升才接近理论水平。预填充阶段基本没有收益因为预填充是计算密集型的GEMM瓶颈不在显存带宽。如果你的线上负载平均输入长度不超过2K那么FP8 KV Cache对你最大的价值只是省显存不会带来可感知的加速。4. 压测方法上的五个坑这次全踩了一遍4.1 没预热就跑第一批数据是脏数据vLLM在启动后第一次收到请求时要做CUDA图捕获、kernel编译、内存布局初始化这部分耗时远高于正常工作负载。如果你启动服务后立刻开始压测前几个请求的TTFT会异常高直接把均值拉出一个假尾巴。我这次第一轮数据就是这样FP16组前10个请求的TTFT普遍在2秒以上后面才稳定到300毫秒左右。如果不截断预热数据所有延迟指标都会被高估。正确做法是先发几十个请求作为预热并且明确丢弃这些数据。更稳的做法是单独跑一个“空载延迟”测试确认服务稳定后再进入正式压测。4.2 用平均值做总结长尾延迟全被吞了基准脚本默认输出平均TTFT和平均TPOT。但LLM推理的延迟分布并不对称尤其是高并发下一部分请求会排队等调度尾部延迟会比中位数高一倍以上。平均值最大的问题是被少数慢请求拉高同时掩盖了绝大多数请求的真实体验。这次的对比里FP16和FP8的平均TTFT差距只有几十毫秒看起来不显著但看P95和P99时差距明显拉大。FP8因为KV Cache更充裕并发搁浅的情况更少尾部延迟明显更平滑。如果只看平均值我可能会得出“FP8对延迟没帮助”的错误结论。建议所有压测脚本都明确统计P50、P90、P95、P99再结合TPOT分位值一块分析。4.3 请求长度分布不匹配TTFT被带偏TTFT本质上是预填充阶段对输入序列做一遍完整前向计算所需的时间它和输入长度几乎线性相关。如果你不清洗长度分布让压测混入一批超长输入TTFT的均值会被超长输入拉高而这个“变慢”可能和KV Cache完全无关。另外vLLM的benchmark_serving.py默认按负指数分布随机生成输入输出长度这模拟一般Chat负载是可以的但你要对比“固定场景下的容量测试”时就不能用随机分布。我后来固定了--input-len和--output-len每组跑两轮一轮固定长度一轮混合长度分开统计。千万不要让两组测试的长度分布不一致否则你的对比没有任何意义。4.4 客户端成了服务器瓶颈一开始我用并发数16压测怎么调服务端参数吞吐量都上不去。后来发现瓶颈根本不在GPU在客户端。benchmark_serving.py用的是异步HTTP客户端单进程单脚本在请求输出很长、并发较高时客户端的解析和调度本身就会消耗大量CPU反而限制了能发出的请求速率。检查方法很简单压测时另开一个终端看客户端进程的CPU占用。如果已经稳定打满说明你在测客户端的上限不是服务器的。对策是换多机压测客户端或者至少把压测进程绑定到独立物理核上并且用--max-concurrency控制客户端侧的并发不超过能力上限。4.5 只数成功请求没数错误率还有比延迟更隐蔽的坑错误率。高并发下部分请求可能因为超时、服务端过载、连接中断而失败。基准脚本会打印失败数但如果你只看成功的平均值整个结果都是自欺欺人。FP16组在高请求速率下出现过少量HTTP 503直接拉低了有效吞吐量。FP8组因为KV缓存更充裕错误率明显低。说明FP8对稳定性的收益是真实存在的但如果你没有统计错误率只能看到“FP8吞吐更高”解释不了原因。所有压测都必须把错误率单列出来作为核心指标之一。5. 实操建议FP8 KV Cache到底该怎么用5.1 什么场景值得开FP8 KV Cache结合这次数据和实践经验我给你的直接建议是如果是短对话、短上下文为主的Chat服务输入长度普遍在2K以下FP8 KV Cache的加速效果很微弱但显存确实减半可以用来提升并发或延长max-model-len如果是RAG、长文档、代码仓库分析这类长上下文场景无论从显存还是从吞吐角度都建议开但必须做好精度验证如果模型输出需要进行精确数字引用、多跳推理或长文本复述建议保留FP16作为兜底方案或者对敏感任务做二次校验如果硬件是Hopper架构H100等FP8的硬件支持更完整收益会更明显Ampere架构也能跑但部分操作要靠软件模拟收益略打折扣。5.2 具体启动参数和压测命令参考这是我实际用过的可复现配置。对照组FP16python -m vllm.entrypoints.openai.api_server \ --model /data/models/Llama-3-8B-Instruct \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 128 \ --port 8000FP8实验组python -m vllm.entrypoints.openai.api_server \ --model /data/models/Llama-3-8B-Instruct \ --kv-cache-dtype fp8 \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 128 \ --port 8001压测命令python benchmarks/benchmark_serving.py \ --backend vllm \ --model /data/models/Llama-3-8B-Instruct \ --tokenizer /data/models/Llama-3-8B-Instruct \ --base-url http://127.0.0.1:8001/v1 \ --request-rate 8 \ --num-prompts 600 \ --max-concurrency 32 \ --input-len 4096 \ --output-len 1024 \ --seed 42 \ --save-result \ --output-dir ./results注意--seed一定要固定。如果不固定两组测试的请求内容完全不同长度分布也会有偏差对比就不公平了。另外对比时尽量让两组请求从同一份数据文件读取或者干脆用同一个seed生成。5.3 精度验证怎么做才靠谱精度验证不要只靠肉眼。我推荐至少三层检查第一层固定相同模型、相同prompt、相同采样参数seed固定、temperature固定分别跑FP16和FP8对比生成文本的Token级差异率。最容易出问题的采样参数是temperature调高后量化误差会跟采样随机性混在一起。建议先试temperature0的确定性解码第二层用带答案的评测集跑分数对比。抽取100到200条真实业务数据分别用两种配置跑完再按你自己的评测标准打分。这一步能暴露长上下文推理问题第三层针对具体任务构造压力样本比如“从长文档里找出第三组实验数据”“复述第几段开头的原文”“多跳推理链条”。我这次的问题正是在这种样本上暴露的。如果嫌麻烦至少做第一层和第三层。只跑跑分不看内容等于确认量化后模型“没有当场崩溃”而不是“结果仍然正确”。6. 实测中的问题排查速查表顺手整理一张表把这次遇到的典型问题、判断方法和对应解法放进来你真正操作时可以直接对照。现象可能原因判断方法解法FP8配置下吞吐量没有提升上下文中位数太短权重读取占主导看场景平均输入长度小于4K基本正常无需处理这是正常现象压测客户端CPU跑满客户端成为瓶颈另开终端看压测进程CPU占用换更强压测机或降低客户端并发KV Cache显存看起来没减少vLLM预分配了整块缓存池检查vLLM的KV使用率指标而非nvidia-smi关注KV池利用率进程显存不会立刻变化前几个请求TTFT异常高缺少预热观察前10个请求延迟明显偏高正式压测前先发几十个请求并丢弃数据FP8在长上下文输出错漏数字量化误差累积对比FP16和FP8在长文档问答中的输出敏感任务保留FP16或加校验环节高并发下错误率上升KV不足或过载检查HTTP错误数和vLLM日志调大max-num-seqs或降低请求速率平均延迟相近但P99差异大平均值掩盖长尾统计分位值以后统一看P50/P90/P95/P99另外补充几个实际操作中的小提示用nvidia-smi dmon边压测边看GPU利用率如果GPU利用率长期不到80%且显存没满通常说明瓶颈在调度或客户端在压测机上固定CPU频率否则CPU频率抖动会直接影响客户端吞吐造成误差换精度配置后一定要重启服务再测不要在同一个服务进程里热切换即使vLLM支持你也不能保证状态完全干净把每组测试的结果文件保存好用脚本统一汇总别靠人眼从终端日志里抄数字。这次实测给我的最大感受是FP8 KV Cache是个值得开的功能但你要清楚它解决的是显存问题其次才是吞吐问题。想要真正吃到长上下文红利它确实有用如果只是跑短对话它的价值非常有限。压测方面我建议每人至少犯错一次才知道平均数到底骗了你多少。上面这些坑你提前规避掉能省下大量的无效排查时间。
阅读完成 · 觉得有帮助?
咨询建站