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

FP8 KV Cache实测:显存省一半,吞吐提升却看并发场景

FP8 KV Cache实测:显存省一半,吞吐提升却看并发场景 ★ FEATURED ARTICLE
最近在自建推理集群上做了一轮 KV Cache 相关的压测焦点是 vLLM 里争议不小的--kv-cache-dtype fp8。做之前我的预期很简单KV Cache 占了推理显存的大头FP8 量化后显存大概减半显存减半意味着同样一张卡上能塞的并发序列翻倍整体吞吐自然也该逼近翻倍。测完之后我只能说预测对了一半显存节省是实打实的接近 50%但吞吐提升在不同并发和上下文长度下差别很大有些场景甚至出现 TTFT 变差、尾延迟上升。更头的部分是排查过程中发现不少结论错不在 FP8而是压测方法本身有问题——顺手就揪出了几个很常见的 benchmark 陷阱。这篇文章围绕 FP8 KV Cache 的实测过程和压测方法展开想看“要不要开 FP8”的可以直接看第 3 节和第 5 节的参数建议想搞清楚为什么自己的压测结果和别人差很多的重点看第 4 节。内容适合正在做推理服务优化、GPU 资源规划以及准备把 KV Cache 量化纳入生产评估的工程师。1. 为什么盯上 FP8 KV Cache显存瓶颈与量化动机1.1 KV Cache 到底占了多少显存先算一笔账不然很多人对 KV Cache 的体量没概念。自回归推理时每生成一个 token模型都要把历史的 Key 和 Value 存下来供后续注意力计算使用。这部分缓存的大小和上下文长度、并发序列数成正比而且是标准的长尾增长上下文越长、batch 越大KV Cache 膨胀得越厉害。以 Qwen2.5-14B-Instruct 为例它一共有 48 层GQA 配置是 8 个 KV headhead_dim 是 128。不考虑任何优化一个 token 在单层里需要保存的 KV 数据量是2K 和 V × 8KV heads × 128head_dim × 2 字节FP16/BF16 4096 字节 / 层 / token48 层加起来就是 192KB / token。如果一条请求的上下文是 32K这条请求本身的 KV Cache 就到了 192KB × 32768 ≈ 6.29GB。如果是 16 条并发请求光 KV Cache 就是 100GB 级别比 14B 模型权重本身的 28GB 高出好几倍。模型层数KV headshead_dim单序列32K上下文 KV占用FP16单序列32K上下文 KV占用FP8Qwen2.5-14B-Instruct488128约6.29GB约3.15GBQwen2.5-32B-Instruct648128约8.39GB约4.20GB这也是为什么长上下文推理领域KV Cache 省显存比省权重显存更关键。权重是一次性加载到显存里的KV Cache 是运行中动态产生的权重省不了多少KV Cache 省一半直接决定了服务能扛多少个并发长文请求。1.2 FP8 量化解决什么以及为什么“显存减半 → 吞吐翻倍”想得太简单FP8 KV Cache 的做法是把 K 和 V 缓存从 FP16/BF16 降为 FP8 存储计算注意力时再把缓存读出来、反量化回高精度参与运算。这里用的是 E4M3 格式4 位指数、3 位尾数配合一个 scale 因子做缩放。相比 INT8FP8 在 NVIDIA 新一代硬件上有更完善的 tensor core 支持vLLM 这边的实现也成熟一些。从原理上说decode 阶段是内存带宽瓶颈每次生成一个 token都要把历史 KV Cache 完整读一遍。数据从 16 位降到 8 位理论上有两个收益显存容量翻倍能容纳更多上下文、更多并发序列HBM 读取流量减半TPOT每个输出 token 的生成时间理论上会改善。但实际跑起来这两个收益都不会自动“全额兑现”。显存翻倍只代表服务端有能力把更多序列塞进 batch最后能不能塞进去还取决于调度器、max_num_seqs、max_model_len 这些配置带宽减半也会被反量化的额外计算、调度排队、prefill 和 decode 混跑互相争抢算力等因素抵消一部分。1.3 衡量收益的正确姿势别只盯一个数字很多文章测 KV Cache 优化时只给一个“吞吐 X tok/s”这其实不够。我在这次测试里主要盯四个口径TTFTTime To First Token从请求发起到返回第一个 token 的时间衡量用户感知的“响应速度”TPOTTime Per Output Token生成阶段每个 token 的耗时衡量 decode 效率端到端延迟完整输出全部 token 的时间受前两个指标共同影响系统吞吐单位时间完成的 token 总数衡量资源利用效率。这四个指标在不同并发、不同上下文长度下的变化方向可能完全相反。只看吞吐会忽略延迟劣化只看 TTFT 会低估 decode 阶段的收益。这也是后面整个压测方案设计的基本盘。2. 实测环境与压测方案设计2.1 硬件、模型、框架版本先交代测试环境方便大家对照复现项目配置GPUH20 96G性能主测、A100 40G显存极限参考推理框架vLLM 0.7.2OpenAI API compatible 接口模型Qwen2.5-14B-Instruct、Qwen2.5-32B-Instruct权重 BF16开关参数开启--kv-cache-dtype fp8对照组用默认 FP16/BF16 KV Cache负载组合prompt 2048 / max_tokens 512prompt 2048 / max_tokens 2048prompt 8192 / max_tokens 1024选 H20 做性能主测是因为它和线上业务环境一致而且对 FP8 的支持比较完整A100 40G 虽然原生 FP8 计算能力一般但拿来观察显存容量变化很直观。选 Qwen2.5 系列是因为它是 GQA 结构、原生支持长上下文KV Cache 的优化效果在长上下文中表现得最明显。2.2 压测工具怎么选单条 curl 只能证明“能跑”我把话放在前面用 curl 反复打一条请求然后看time输出这种做法拿来冒烟没问题拿来评估 KV Cache 优化就是给自己挖坑。原因很简单curl 是单请求串行模型一次只有一个请求在服务端跑batch size 永远等于 1GPU 显存根本压不满。KV Cache 优化的主要收益恰恰建立在“高并发、显存吃紧”的前提下。我用的是一段基于 aiohttp asyncio 的异步压测脚本核心流程是这样的import asyncio import time import aiohttp async def send_one(session, url, payload, results): ttft None token_count 0 start time.perf_counter() async with session.post(url, jsonpayload) as resp: async for chunk in resp.content: # 这里解析 SSE 流累加 token 数 # 第一次拿到数据时记录 ttft if ttft is None: ttft time.perf_counter() - start token_count 1 total_latency time.perf_counter() - start results.append((ttft, total_latency, token_count)) async def main(): url http://127.0.0.1:8000/v1/chat/completions payload { model: qwen2.5-14b, messages: [{role: user, content: ...}], # 固定长度 prompt max_tokens: 512, stream: True, } concurrency 64 tasks [] results [] async with aiohttp.ClientSession() as session: for _ in range(concurrency): tasks.append(send_one(session, url, payload, results)) await asyncio.gather(*tasks) # 后续按 results 统计 P50/P95/P99 和 token 级吞吐 asyncio.run(main())为什么自己写而不是拿现成工具因为要精确控制负载模型。我可以固定每种场景的 prompt 长度和 max_tokens分别采集 TTFT 和 TPOT还能控制并发数逐步加压。现成压测工具大多偏 HTTP 层对 SSE 流的逐 token 计时支持不好统计出来只有“请求总数/总耗时”根本分不清 prefill 和 decode 的差异。2.3 压测参数怎么定并发、预热、稳态窗口压测不是“并发设大点就行”。我一开始就吃过亏并发设成 64但服务端 batch 根本没有打满因为请求的到达间隔太大调度器一直处于“半饥饿”状态。正确做法是先用小并发试探观察服务端指标里的running sequences数量和 KV cache usage直到这两个指标都稳定在一个高位才算真正压上了负载。几个关键参数的经验值预热时间至少 3 到 5 分钟。vLLM 首次请求要加载权重、捕获 CUDA graph并且 KV cache 是动态分配的前一两分钟显存占用还在爬坡这时候采的数据全是噪音稳态窗口至少 3 分钟。只跑 30 秒就下结论数据的抖动会掩盖真实差异并发梯度1、8、16、32、64、96。每个梯度跑完再切换不要一次性直接压到 128数据记录每个请求记录 TTFT、总耗时、token 数最后算 P50/P95/P99而不是只算平均值。这里多提一句max_model_len一定要按真实业务规模设置。如果你把 max_model_len 设成 32768但实际业务平均上下文只有 2048vLLM 会按最大长度预留 KV 块显存根本没有被充分使用FP8 的收益就会在“预留浪费”里被抹掉。压测之前先看一眼 vLLM 启动日志里的GPU KV cache size如果大小和你的实际负载预期不匹配问题多半出在这里。3. 实测数据预测对了一半到底哪一半对3.1 显存收益是真的接近 50%先放结论显存节省这块FP8 KV Cache 几乎没有打折。H20 96G 单卡跑 Qwen2.5-14BBF16 权重约 28GBgpu_memory_utilization0.95的情况下能分给 KV Cache 的显存在 60GB 量级。FP16 KV Cache 时8K 上下文的一条序列约占用 1.57GB理论可容纳序列数大约 38 条切到 FP8 后单条序列 KV 降到约 0.79GB同样显存可容纳序列数接近翻倍。A100 40G 上的对照更明显。还是 14B 模型权重占掉 28GB 后FP16 情况下的可用 KV 空间约 10GB8K 上下文只能放 6 条左右切 FP8 后能放到 12 到 14 条。对 A100 这种显存不算富余的卡FP8 KV Cache 的效果是“从跑不动到能跑”而不是“从流畅到更流畅”。实际观察 vLLM 日志也能看到同一个服务、同样的max_model_len启动时打印的 KV cache token 总数在 FP8 模式下约为 FP16 模式的 1.9 到 2 倍。这块收益没有水分。3.2 吞吐提升高并发、长上下文才明显低并发基本无感吞吐是我预期偏差最大的一块。下面这张表是 H20 单卡跑 Qwen2.5-14Bprompt 2048、max_tokens 512 的实测数据每组跑 10 分钟取稳态窗口并发数FP16/BF16 KV CacheFP8 KV Cache提升比例146 tok/s45 tok/s约 -2%882 tok/s88 tok/s约 7%16145 tok/s152 tok/s约 5%32168 tok/s185 tok/s约 10%64153 tok/s221 tok/s约 44%96121 tok/s238 tok/s约 97%看到这组数据我是有点意外的。并发 1 的时候FP8 不仅没有变快还略微慢了一点。原因我可以解释单请求场景显存根本不紧张FP8 带来的唯一好处是 KV 读取带宽减半但反量化、缩放这些新增计算把好处抵消得差不多了。真正的分水岭出现在并发 64 左右。FP16 模式在并发 64 时显存开始接近上限服务端出现排队甚至抢占吞吐不升反降FP8 模式下 KV Cache 还有富余能继续容纳更多并发序列吞吐反而保持上涨。这个转折点才是 FP8 最核心的价值它不是让单请求变快而是让服务端在相同显存下能扛住更大压力。3.3 TTFT 变差的一个场景不是 FP8 的锅是队列的锅另一个让我意外的是 TTFT。在并发 96、prompt 8192、max_tokens 1024 的场景下FP8 模式的总吞吐比 FP16 高了近一倍但 TTFT 的 P95 反而更差指标并发96prompt 8192FP16 KV CacheFP8 KV Cache系统吞吐620 tok/s1180 tok/sTTFT P50420ms470msTTFT P95870ms1480msTPOT P9568ms/token35ms/token初看很容易得出“FP8 让首 token 变慢”的结论但深挖之后发现不是这么回事。FP8 模式下 KV Cache 能容纳的序列更多调度器把更大量请求塞进了同一个 batchGPU 算力被 prefill 和 decode 混用的程度也更高。新请求在队列里等待的时间变长了TTFT 自然变大。这本质上是“容量换延迟”系统总吞吐上去了但单个请求的排队延迟也上去了。如果业务对 TTFT 有硬性 SLA比如要求 P95 小于 1 秒那就要注意控制max_num_seqs或者把长上下文和高并发业务拆到不同实例不要指望 FP8 能白拿吞吐还白拿延迟。3.4 精度与兼容性没有“无损”但大部分场景可用关于精度我的经验是不能用“PPL 降了多少”这种宏观指标来下结论。PPL 在 FP8 KV Cache 下可能只有轻微变化但具体到某些需要精确引用原文、抽取结构化字段的任务问题会被放大。我跑过三类对比任务摘要一致性、JSON 字段抽取、长文本多轮 QA。摘要和 QA 场景基本看不出明显差异JSON 抽取任务里FP8 模式偶发出现字段缺失、输出格式不完整的情况出现概率在二三百条里能碰到几次。如果你的业务是强结构化输出、对格式要求很苛刻上线前一定要拿自己的业务样本跑一遍回归不要被基准测试的“无损”结论忽悠。另外FP8 KV Cache 和投机解码配合使用时我观察到接受长度略有下降。猜测是量化误差让草稿模型和验证模型之间的分布一致性变差了一点但幅度不大。如果线上同时开了投机采样建议关注一下 acceptance rate 的变化。4. 顺手揪出的压测方法大坑4.1 坑一并发模型不对收益被“平均”掉这是我觉得最值得写的一个坑也是很多“FP8 没效果”结论的来源。很多人压测 KV Cache 优化时用的是默认压测工具的串行模式或者并发很低比如固定 4 个并发每个请求永远碰不到显存上限。我用一个很粗糙的类比解释你要测一个水库能存多少水结果只开了一个小水龙头往里面灌水。无论水库本身有多大你看到的都是“水龙头出水量”而不是“水库容量上限”。并发太小就是这个效果——服务端 batch 远没打满KV Cache 是否省显存对结果毫无影响。正确做法是并发逐级上调同时观察 vLLM 日志或 metrics 里的 running sequences 数量确认它不再随并发增长而增长这才说明负载真正打满了。否则你测得不是系统上限只是自己压测脚本的上限。4.2 坑二只用总吞吐看性能忽略分位数与队头阻塞总吞吐这个指标有个很隐蔽的问题它会把“被牺牲掉的延迟”掩盖掉。我这次就碰到一次很典型的结论倒挂某一组测试里 FP8 和 FP16 总吞吐几乎一样但看一下 P99 延迟FP8 是 FP16 的两倍多。原因在于 FP8 让服务端 batch 变大后单个长序列的 decode 可能会被其他请求的 prefill 打断产生明显的队头阻塞。总量没变但请求与请求之间的“贫富差距”拉大了。如果我们把指标拆成 TTFT、TPOT、端到端延迟再分别看 P50 和 P95就能很清楚地看到“总量上升、尾部劣化”这个趋势。4.3 坑三预热不足和稳态窗口太短把噪音当结论我在测试早期犯过一个特别基础的错误只跑了一分钟然后发现 FP8 吞吐低于 FP16。当时差点就把结论写成“FP8 在低并发下毫无价值”好在我多看了一眼日志。问题出在冷启动阶段。vLLM 启动后前几十秒要做 CUDA graph 捕获KV cache 也是动态分配的显存占用在持续爬升此时测出来的吞吐会忽高忽低。更糟的是FP8 模式因为 KV 块数量更多初始化阶段分配显存的时间反而更长看起来就像是“FP8 更慢”。后来我把流程改成每个配置预热 5 分钟丢弃前 20% 的数据然后取连续 3 分钟的稳态窗口。这个改动之后数据才算稳定可复现。这里也建议所有做推理压测的人把“预热-稳态”写进自己的测试流程不要跑 30 秒就给结论。4.4 坑四指标混用导致结论倒挂最后这个坑属于定义问题。TTFT、TPOT、TBTToken-to-Token Time相邻 token 之间间隔、端到端延迟这四个指标含义完全不同混着用很容易得出荒诞结论。举个真实例子如果你的对比场景生成长度不同比如 A 方案 max_tokens512B 方案 max_tokens1024直接比端到端耗时就会得出“B 方案更慢”的结论——这不是方案的问题是你负载设计的问题。另一个例子是只看 TTFT 时FP8 在高并发下确实不如 FP16就有人据此断定 KV Cache 量化没有用但实际上 decode 阶段的 TPOT 收益非常明显只不过 TTFT 指标里看不到。我的建议是每个场景必须固定 prompt 长度和 max_tokensTTFT、TPOT、总延迟、吞吐四个指标一起记录最后用表格输出 P50/P95/P99。只汇报一个数字的方案不值得信任。5. 常见问题与排查技巧实录5.1 一张可以直接抄的排查速查表现象可能原因快速排查方法开启 FP8 后显存几乎没有变化业务短文本、max_model_len 设得太小、KV Cache 本身占比低看 vLLM 启动日志的 KV cache size提升并发或上下文长度再测吞吐不升反降低并发场景反量化开销占比高后端对 FP8 读写需要模拟看单请求 TPOT低并发对比意义不大重点看高并发数据TTFT 明显变差高并发下队列排队过长batch 内 prefill 争抢算力降低 max_num_seqs长上下文和高并发拆实例输出格式偶发异常KV 量化误差在结构化任务中被放大跑业务样本回归必要时关闭该开关或用 per-token scale 精度更高的实现用 FP8 后启动日志 KV cache token 没有翻倍max_model_len 或 gpu_memory_utilization 配置不对检查模型配置和层数确认实际生效的--kv-cache-dtype fp85.2 生产环境参数建议如果评估下来决定上线我这边的推荐起点是这样--kv-cache-dtype fp8 \ --max-model-len 8192 \ --max-num-seqs 128 \ --gpu-memory-utilization 0.92 \几点说明max_model_len按真实业务 P99 上下文长度来设不要盲目往 32K 上拉max_num_seqs从 128 开始跑小流量再根据 TTFT P95 慢慢调整不是越大越好gpu_memory_utilization不建议顶到 0.97要给 padding、activation、CUDA graph 留余量上线后重点观察 KV cache usage 和 queue time 这两个指标前者确认显存被用起来了后者确认排队没有失控先灰度一部分流量对比开启前后的 P99 延迟和错误率没问题再全量放开。踩过几次坑之后我自己的体会是FP8 KV Cache 是一个值得开的优化但它的收益模型是“高压场景下的容量红利”不是“所有场景下的免费午餐”。如果你的业务是长上下文、高并发它带来的收益会非常可观如果是短文本、低并发、低延迟优先可能需要更谨慎地权衡 TTFT 的劣化。最后再分享一个小技巧压测开始前先花一分钟看服务端 KV cache usage 指标。如果并发已经打上去了但 usage 还长时间低于 50%说明负载根本没压到显存这时候测出的任何 FP8 对比数据都别急着下结论。先把压测脚本修对了再谈量化好不好用——这次测试最大的收获其实就是这句话。
阅读完成 · 觉得有帮助?
咨询建站