☰
别被KV Cache命中骗了:LLM推理慢在显存带宽与数据搬运
2026/10/20 5:13:38 网站建设 项目流程

别被“Cache 命中”骗了——LLM 推理的慢通常不卡在计算上

先上个真实到骨子里的场景:某次我在排查一个推理服务的性能问题,模型不大,显存也宽裕,KV Cache 的命中率打到了九成往上。按“命中率高 = 快”的直觉,这个服务应该跑得飞快。但实际压测一打上去,延迟曲线直接起飞,单 token 生成速度惨到让人怀疑是不是用了 CPU 跑。

问题出在哪?我盯着 профилирование 数据看了半天,SM(GPU 计算核心)利用率低得可怜,显存读写却拉得满满的。那一刻我才彻底明白:大模型推理到 decode 阶段,很多时候根本不是“算不过来”,而是“数据搬不过来”。KV Cache 命中只代表你省掉了重复计算,不代表数据就躺在最合适的地方等你来读。这篇文章就想把这个“命中了还是慢”的悖论讲透,从显存带宽的物理限制,一路聊到内存换进换出,再聊到 Mooncake 这类把 KV Cache 调度到更大资源池的方案思路。

这篇文章不是教科书式科普,而是我在实际项目里踩坑、量数据、做优化后的经验记录。无论你是正在优化在线推理服务的工程师,还是刚接触大模型部署、准备把吞吐和延迟做上去的开发者,都应该能从这里找到几条能直接用的判断方法和排查手段。

1. 别被“Cache 命中”骗了——LLM 推理的慢通常不卡在计算上

1.1 显存里最贵的东西其实不是容量,是带宽

很多人一聊到显存,第一反应就是容量:这张卡 24G,那张卡 80G,能不能塞下模型权重。但在大模型推理场景里,尤其是在 decode 阶段,带宽往往比容量更容易成为瓶颈。

我用一个不那么严谨但特别贴切的类比:显存容量是仓库的大小,显存带宽是仓库门口传送带的宽度。仓库再大,如果传送带只有一根细细的履带,货物只能一件一件慢慢挪,出货速度照样上不去。KV Cache 命中之后,你要做的是把这些“货物”从仓库里搬出来参与计算,而不是重新生产它们。搬运的速度,就由带宽决定。

GPU 里的 HBM 显存带宽看似惊人,单卡能做到 1TB/s 到 2TB/s 这个量级,但这是“峰值理论值”。实际跑到 80% 都算不错了。更关键的是, decode 是逐 token 串行推进的:每生成一个 token,都要把当前的 KV Cache 从头到尾再读一遍。这种“每次全量读一遍”的访问模式,刚好把带宽需求的放大器开到了最大。

1.2 一次 decode 到底要读多少数据

这里我给你算一笔账,拿一个常见的 7B 规模模型举例。FP16 精度下,光模型权重就是 14GB 左右。decode 阶段每生成一个 token,虽然计算量很小,但原则上需要把注意力相关的权重和 KV Cache 都搬进计算单元。

KV Cache 每存一个 token 大约需要多大?经验公式是:

单 token KV Cache 大小 = 2 × 层数 × 隐藏维度 × 每 KV 字节数

假设一个 7B 模型有 32 层,隐藏维度 4096,FP16 精度(每路 2 字节),那么一个 token 的 K 和 V 加起来大概是 2 × 32 × 4096 × 2 = 524288 字节,也就是 512KB。这个数字听着不多对吧?但如果上下文长度是 4096,整个 KV Cache 就是 4096 × 512KB = 2GB。如果上下文拉到 8192,就变成 4GB。

好,现在看 decode 一次要搬多少东西:权重 14GB + 2GB KV Cache = 16GB。假设你的 GPU 显存带宽约 2TB/s,理想情况读完这些数据至少需要 16GB / 2TB/s = 8ms。也就是说,理论上一秒钟最多生成 125 个 token。如果 KV Cache 更大、或者显存带宽更低(比如某些卡只有 800GB/s),这个数字还会往下掉。

注意,这个计算还没算任何通信开销和调度开销。在真实的推理框架里,kernel 启动、多头注意力的数据切分、上下文切换,都会再吃掉一截性能。所以实际 token 速率往往连理论值的一半都到不了。这跟你有没有命中 KV Cache 没关系——你就算把 Prompt 前处理的重复计算省到零,该搬的 KV 数据照样得搬。

1.3 计算量和搬运量的反直觉对比

这里有个特别反直觉的点:在 decode 阶段,GPU 的计算单元其实相当一部分时间是“闲着”的。你去做 profiling 就会发现,SM 利用率可能只有 20%~30%,但显存控制器忙得不行。

因为 decode 阶段每个 token 的计算是一次矩阵向量乘(GEMV),它的计算规模比 prefill 阶段的矩阵乘(GEMM)小得多。GEMM 可以把数据复用很多次,算得越密集越高效;而 GEMV 是每个权重只被读一次、用一次,几乎完全受限于读数据的速度。

所以,判断一个推理服务到底慢在哪,不能只看“显存够不够”“Cache 命中率多少”。命中率解决的是“要不要重算”,带宽解决的是“读一遍要多久”。两者是完全不同的维度。后面我会细说,当 KV Cache 大到放不进显存、必须借内存甚至远端来放的时候,这个“搬运”的代价还会再放大一个数量级。

2. 显存容量明明够,为什么每生成一个 Token 还是像“搬山”

2.1 先算清你的 KV Cache 有多胖

很多人在优化服务时不量化,凭感觉觉得“我的显存还有 20G,肯定够”。其实 KV Cache 的膨胀速度远比你想象的快。我再给一个更完整的计算公式:

KV Cache 总大小(字节)= 2 × 层数 × KV 头数对应的低秩投影维度 × 序列长度 × 批量大小 × 每元素字节数

注意,有些模型用的是 MHA(多头注意力),KV 头数和 Q 头数一样多;有些模型用 GQA(分组查询注意力)或者 MQA(多查询注意力),KV 头数明显少于 Q 头数。GQA 就是典型用来压缩 KV Cache 结构。你用 GQA 替代 MHA,KV Cache 可能直接缩小好几倍,这就是为什么新模型普遍上 GQA。

我拿三个典型规模给你列个大概,帮助你建立量级感(下面的数字都是近似值,不代表任何特定模型):

模型规模层数隐藏维度每 token KV(FP16)4096 上下文8192 上下文
7B 级324096512KB约 2GB约 4GB
13B 级405120800KB约 3.2GB约 6.4GB
70B 级8081922MB约 8GB约 16GB

看到 70B 模型在 8192 上下文时 KV Cache 能到 16GB 了吧?这还没算批量大小。如果你同时服务多个用户(batch 为 8、16),这个数字要直接乘以 batch 数。显存再多,也经不起几路长上下文用户同时挤。

所以第一个排查点永远是:你的 KV Cache 到底占了多少显存,是不是在服务的持续运行中偷偷膨胀了。很多“突然变慢”的故障,背后就是 KV Cache 随着并发和上下文长度增长,悄悄逼近了显存天花板,然后触发换出。

2.2 带宽天花板:为什么命中缓存后还是只有这点速率

回到带宽。假设你的 KV Cache 是 4GB,显存带宽约 1.5TB/s,那么每 decode 一步光读 KV 就需要约 2.7ms。再算上加权重等其他开销,单 token 实际耗时冲到 4~5ms 很常见,对应生成速率只有 200~250 token/s。体感还是一个“快”字。

但如果你把 KV Cache 换成 12GB,同样带宽下读 KV 就要 8ms,加上权重搬运,单 token 可能到 15ms,生成速率就掉到 60~70 token/s 了。很多团队在长上下文场景下抱怨“输出越来越慢”,往往就是这个原因:不是模型变笨了,而是每一次生成都要把越来越长的历史重新读一遍。

有同学会问:能不能让 KV Cache 部分留在显存、部分留到内存,只读需要的部分?这就涉及工程上的换入换出策略。但请你注意一个残酷的现实:decode 阶段访问 KV Cache 是顺序的,几乎每一层都要用到自己那一份 K 和 V,所以它本质上是一个“全量遍历”的访问模式。你很难通过局部性优化来大幅减少搬运量,除非你有意识地把部分历史 token 的 KV 丢到低速介质,然后接受“要用时得等”的代价。

2.3 该盯哪些观测指标

我强烈建议你在服务端把这几类指标打点记下来,比“Cache 命中率”有用得多:

  • 显存读写带宽利用率:看是不是长期顶在高位。
  • SM 利用率:如果很低,大概率是 memory-bound。
  • KV Cache 当前占用:盯着它随并发和上下文的增长曲线。
  • 单 token 平均时延:这个直接反应体感。
  • 换页/预取次数:看有没有数据在显存和内存之间来回倒腾。

如果 SM 利用率和显存带宽利用率同时都低,那问题可能在调度层(比如锁、队列、CPU 端开销)。如果 SM 低但带宽高,那就是典型的“数据搬运瓶颈”。后面第 5 节我会讲完整排查顺序。

3. 显存溢出的真凶:换入换出与跨设备搬运的隐藏成本

3.1 显存放不下时,大多数方案先想到“借内存”

当 KV Cache 把显存塞满,事情就开始复杂了。常规做法是把一部分 KV Cache 换出到 CPU 内存(host memory),等用到的时候再换回显存。听起来很合理,但对带宽的消耗是毁灭性的。

CPU 内存(DDR)的带宽通常只有几十 GB/s,跟 HBM 的 1~2TB/s 相比差了一到两个数量级。举例:一块普通服务器的 DDR 带宽大概在 40~60GB/s,你在 CPU 内存里读 2GB 的 KV Cache,需要 2GB / 50GB/s = 40ms。对比前面在显存里读同样的 2GB 只要 1ms 左右。差了 40 倍。这还不算 PCIe 传输本身的开销。

PCIe 的带宽也必须要算进去。PCIe 4.0 x16 双向带宽理论值约 32GB/s,PCIe 5.0 x16 约 64GB/s。但实际能跑满一半就不错了,还要跟其他设备共享。你想把 2GB KV 从 CPU 内存经 PCIe 搬到显存,至少 30~60ms 起步。你觉得这个延迟在用户体感上是什么概念?就是生成一个字卡顿好几秒。

3.2 内存带宽和 PCIe:两道窄门叠在一起

这里有个关键点:跨设备搬运 CPU 内存里的 KV 到 GPU,要先过内存带宽,再过 PCIe 带宽。两道都是窄门。即使你的 PCIe 是超快的 5.0,内存带宽可能先卡住;即使内存带宽很大,PCIe 也会卡住。实际传输速度取两者中较小值。

更坑的是,换出换入不是一次性的。decode 过程可能反复需要那部分数据:这个请求用到一部分,换回显存;另一个请求又需要另一部分,再把之前的换出去。如果调度策略不好,会出现“抖动”——数据在显存和内存之间来回搬,性能瞬间归零。这有点像操作系统里的内存换页颠簸(thrashing),只不过发生在 GPU 上。

业内已经有思路去缓解,比如:

  • 把解码步长内的计算和换入换出做流水线重叠(overlap),让计算的同时提前把下一块数据搬进来。
  • 分块调度,尽量让每次换入的数据能支撑多步 decode,减少一次一换。
  • 优先级控制,优先保住当前活跃请求的 KV,冷数据先换出。

但这些都需要推理框架层面有很强的调度能力,不是说换就换。如果你用的是现成的开源推理框架,默认配置不一定开了这些优化,可能还是最简单的“满了就换出”。

3.3 Cache 命中在这里失效的根因

现在你可以理解为什么“Cache 命中了但依然慢”了。KV Cache 命中的语义是“我不用重新计算这些 K 和 V”。但是数据存储的位置才是决定读取速度的关键。缓存层的真正目标是让数据在需要的那一刻,出现在读写最快的那一层。命中缓存只是第一步,数据局部性(data locality)才是更深一层的问题。

打个比方:你要用一本工具书,图书馆系统告诉你“这本书在馆”(命中),但它不在你手边,而是在隔壁城市的分馆,要等快递送来。那跟“这本书不存在”有什么区别?都是要等。KV Cache 也一样:它存在,但被放到了内存甚至是远端存储,decode 的时候才临时去取,那就是“命中了个寂寞”。

所以真正的优化方向,是想办法把“数据在需要之前、就预置到正确的位置”。这引出了 Mooncake 这类方案的核心思想。

4. Mooncake 这类思路到底在解决什么:把 KV Cache 当成可调度资源池

4.1 不再把 KV Cache 看成某张卡的私有状态

我理解的 Mooncake 类方案,一句话概括就是:把 KV Cache 从“单卡私有状态”升级成“全局可调度的资源池”。它不满足于让每个请求在自己的显存里缓存,而是把 KV 的存放位置扩展成一套分层体系:

  • 第一层:GPU 显存,读写最快,适合放当前正在生成的最活跃 KV。
  • 第二层:CPU 内存,容量大、速度慢,适合放冷一点的 KV 或者等待预取的数据。
  • 第三层:远端缓存/存储池,容量极大、延迟更高,适合跨机共享的公共前缀 KV。

这种分层思想其实很朴素,但工程难度在于:谁来负责数据的迁移、什么时候迁、迁多少、怎么避免抖动。Mooncake 这类方案的核心价值就在这个调度层。它不像传统方案那样被动等 cache 满了再换出,而是主动做预取(prefetch)和复用(reuse)。

4.2 核心机制拆开看:预取、复用、重叠

我把这类方案最常见的三个机制拆开讲。

预取(Prefetch):decode 是有节奏的、可预测的。你知道当前 token 生成完,下一步会访问哪些层、需要哪些 KV。那就没必要等到 decode 开始才去读数据,可以在上一步计算的同时,提前把下一步需要的 KV 从内存/远端搬到显存。如果预取做得好,传输时间可以被计算时间完全遮住,用户感知不到延迟增加。

复用(Reuse):一个很有意思的点是,不同请求之间可能存在公共前缀。比如很多人用同一个系统提示词(system prompt),或者同一个文档在多轮问答里反复被引用。如果这些公共部分的 KV Cache 能被多个请求共享,那么每个新请求只需要计算前缀之后不同的部分,省下的工作量是巨大的。这在长文档问答、客服机器人这类场景特别明显。Mooncake 这类方案意识到,KV 复用不该只局限于单请求内部,而应该变成跨请求、跨设备的基础能力。

重叠(Overlap):把数据传输和计算指令流水线化。GPU 的传输引擎(DMA)和计算单元是独立的,只要时机把握好,可以在计算的同时做数据搬移。实现这种重叠需要在 CUDA 层面搞多流(stream)并发或者专门的传输队列,工程复杂度高,但收益非常可观。

4.3 什么场景适合、什么场景反而吃亏

任何一种架构层面的方案都不是银弹。以 Mooncake 这类全局 KV 池化思想为例,我个人的判断标准是这样的:

适合的场景:

  • 高并发在线服务,很多请求共享系统提示词或文档前缀 → 复用收益大。
  • 超长上下文场景(数十 K 以上的 token),单卡显存根本放不下完整 KV → 必须外扩,分层调度几乎是必选项。
  • 多机部署的推理集群,想要让 KV Cache 打破单机物理边界,提升整体利用率。

不太适合的场景:

  • 单用户、一次性短对话,前后请求没有共享内容 → 预取和复用的收益很小,反而增加调度复杂度。
  • 显存本来就十分宽裕,KV Cache 离上限远得很 → 你费劲搞远端调度,不如老老实实把 KV 都放显存,拿本地带宽碾压。

所以,不是所有推理服务都需要上 Mooncake 这种重调度方案。我做技术选型时的原则是:先搞清楚瓶颈是容量问题还是带宽问题,再用最简单的办法去解决。如果只是容量紧张,先用 GQA、量化 KV Cache、限制最大上下文长度这些手段;如果这些手段都用尽了,还是被容量憋住,再考虑上分层调度。

5. 实操排查流程与避坑经验:一步步找出“命中但慢”的元凶

5.1 第一步:量出你的实际带宽利用率

我强调过很多遍,感觉是不靠谱的,性能问题必须用数据说话。先把 profiling 工具跑起来,看这几个数:

  • 显存带宽利用率:如果长期超过 80%,不用怀疑,就是带宽瓶颈。
  • SM 利用率:如果明显低于带宽利用率,说明计算没吃满,数据搬运是主因。
  • 单 kernel 时间分布:把 prefill 和 decode 的 kernel 分开统计,重点看 decode 里注意力相关 kernel 的耗时占比。

做完这一步,基本上就能判断“慢是因为算不动还是搬不动”。我见过太多团队在优化矩阵乘,结果发现 decode 阶段整个时间几乎都烧在数据力学上,白忙活一场。

5.2 第二步:核算 KV Cache 实际占用,并判断是否触发换页

去框架日志或监控面板里找到 KV Cache 的分配情况。重点看这几项:

  • 当前 KV Cache 占了多少显存;
  • 有没有发生换出换入(很多框架会有日志或指标);
  • 上下文长度和并发数是不是导致 KV Cache 激增的最大变量。

如果你发现 KV Cache 已经占了显存大头,或者换页计数很高,那“命中但慢”的答案基本就找到了——cache 虽然在,位置不对。这时候优先做的不是上 Mooncake,而是先看能不能把 KV Cache 本身变小。

5.3 第三步:用“减法”优化 KV Cache 本身的体积

这一步是收益最高、见效最快的,我按推荐顺序列一下:

  1. 启用 GQA / MQA 的模型结构。如果模型本身支持而你没开,那是白捡的倍数。2.KV Cache 量化。FP16 降到 FP8,内存减半,带宽需求减半;降到 INT4 还能再缩,但精度损失需要评估。3.限制 max_seq_len 和 max_batch_size。这两个参数直接决定 KV Cache 的上限,别让它在运行时无界增长。4.前缀共享/前缀缓存。在应用层做多轮会话共享前缀,减少重复 KV 计算和存储。

我个人见过一个真实项目,光是把 KV Cache 从 FP16 压成 FP8,加上限制最大上下文长度,服务吞吐直接翻了一倍,延迟也明显下降。而且代码改动量很小,没有引入任何复杂调度系统。先把简单高效的手段用尽,再考虑复杂架构。

5.4 第四步:如果还不行,再评估分层调度方案

如果容量确实大到单卡无解,例如要跑超长上下文、或者需要跨多机共享 KV,那就可以认真评估 Mooncake 类方案。我建议先做一次简单的收益估算:

  • 估算你的请求里有多少比例能命中公共前缀的 KV 复用;
  • 估算如果把部分 KV 放到内存或远端,预取成功率达到多少才能覆盖传输延迟;
  • 估算工程改动量(涉及调度模块、通信、缓存替换策略等)和团队精力。

这三个估算能帮你筛掉大半不必要的复杂化。别因为“听起来高级”就选它。

5.5 常见问题速查表

我把经常踩的坑整理成了一张速查表,你排查的时候可以照着看:

现象可能原因优先排查项
SM 利用率低,带宽拉满decode 阶段 memory-boundKV Cache 是否过大;带宽利用率曲线
服务刚开始快,越跑越慢KV Cache 随上下文/并发膨胀观察 KV Cache 占用曲线,限制长度
延迟抖动严重,偶发大停顿换出换入或预取失效看换页指标,检查调度策略
单 token 延迟高,但显存还有空闲权重+KV 全量读取本身有带宽下限核算读取总量和理论下限
短请求快,长请求特别慢decode 每步要遍历全部 KV核对 KV 随序列长度的增长公式
开了“前缀共享”但没效果实际前缀差异大,复用率低检查请求前缀重合度

5.6 我踩过的坑和一个小技巧

最后分享一个我在实际运维中总结的技巧:上线前一定要跑一次“KV Cache 压力测试”。用一个固定的长 Prompt + 固定长度的生成任务,把 KV Cache 从空到满的过程全部记录下来,画出“KV 占用曲线”和“单 token 延迟曲线”放在同一张图里。两条曲线几乎同步上升,说明你被带宽绑死了;如果延迟曲线在某个点突然跳变,那大概率是触发了换页。

我第一轮优化某推理服务时,就是靠这张图发现延迟拐点正好对应 KV Cache 触顶的时刻。后来把 max_seq_len 从 8192 砍到 4096,并开启 FP8 量化,服务延迟立刻稳定住,吞吐涨了接近一倍。这件事给我最大的教训是:不要迷信“命中率”这个指标,它只是一个必要不充分条件。真正决定推理快慢的,是数据在多层存储之间搬运的效率和调度能力。优化 KV Cache,本质上是在优化一条“数据搬运链”,而不是单纯做缓存。

这篇文章写到这里,核心概念和排查方法都过了一遍。如果你正在处理线上推理的延迟问题,我建议从第 5 节的排查顺序入手,先把 KV Cache 的体积压下来,再谈要不要上分层调度。绝大多数时候,你缺的不是前沿方案,而是一份对带宽和容量的清晰账本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询