3.1 KV Cache显存精算与长上下文策略


文档摘要

3.1 KV Cache 显存精算与长上下文策略 读者学完这一节,应该能拍着胸脯说:「我知道这块显卡到底能并发多少请求,以及为什么 1M 上下文不是把 max-model-len 调大就完事。」 第二章我们已经把 vLLM 在 Kubernetes 里跑起来了。但绝大多数人跑通的第一天,紧接着就会撞上一堵墙:显存明明还有空,为什么新的请求就是进不来?为什么长文档一压上来就 OOM? 这一节我们就来把「显存」这件模糊的事,算成你能在工时会议上解释清楚的精确数字。 一、先说结论:推理服务的显存不是「模型权重」一家占的 很多新手以为,「一块 A100 80G,模型权重吃了 40G,那还剩 40G 随便用」。这是 2023 年之前、没有 PagedAttention 时代的朴素想法,也是踩坑的根源。

3.1 KV Cache 显存精算与长上下文策略

读者学完这一节,应该能拍着胸脯说:「我知道这块显卡到底能并发多少请求,以及为什么 1M 上下文不是把 max-model-len 调大就完事。」

第二章我们已经把 vLLM 在 Kubernetes 里跑起来了。但绝大多数人跑通的第一天,紧接着就会撞上一堵墙:显存明明还有空,为什么新的请求就是进不来?为什么长文档一压上来就 OOM? 这一节我们就来把「显存」这件模糊的事,算成你能在工时会议上解释清楚的精确数字。

一、先说结论:推理服务的显存不是「模型权重」一家占的

很多新手以为,「一块 A100 80G,模型权重吃了 40G,那还剩 40G 随便用」。这是 2023 年之前、没有 PagedAttention 时代的朴素想法,也是踩坑的根源。

在 vLLM 这类现代推理引擎里,一块 GPU 的显存至少要被四部分瓜分:

总显存 = 模型权重(weights) + KV Cache + 激活值(activations) + 碎片/开销
  • 模型权重:GLM-5.2 的权重常驻显存,前向一次就复用,占大头且相对稳定。
  • KV Cache:每个并发请求、每个已生成的 token 都要缓存 Key/Value 张量,它是随并发和上下文长度线性膨胀的头号变量。
  • 激活值:单次前向的中间结果,随 batch 和序列长度起伏,但相对可控。
  • 碎片与开销:CUDA context、cudnn 工作区、显存碎片,通常占几个 G。

关键认知:权重是固定成本,KV Cache 才是决定并发上限的边际成本。 你调优推理服务,本质上就是在「给 KV Cache 留出多少显存」和「单请求能用到多长上下文」之间做权衡。

下面这张图把显存的构成画清楚:

```mermaid pie title 单卡 80G 显存典型构成(以 GLM-5.2 + vLLM 为例) "模型权重 weights" : 42 "KV Cache(留给并发)" : 30 "激活值 activations" : 4 "CUDA 上下文/碎片开销" : 4 ```

注意:上面的比例是示意,真实比例取决于你的 --gpu-memory-utilization、张量并行度和序列长度。但「KV Cache 吃掉了你能留给并发的最大一块弹性空间」这个定性结论,在所有配置下都成立。

二、KV Cache 到底是什么:从一次注意力说起

要算显存,得先懂 KV Cache 在存什么。Transformer 的每一层注意力,都要对输入序列算 Query、Key、Value。自回归生成时,每生成一个新 token,都要和之前所有 token 做注意力。如果每次都重新算历史 token 的 K、V,算力浪费会爆炸。

所以推理引擎把历史 token 的 K、V 张量缓存下来,这就是 KV Cache。它的大小由两个维度决定:

  1. 序列长度:缓存的 token 数,越长越大。
  2. 并发请求数:每个请求各自维护一份自己的 KV Cache。

KV Cache 单 token 的字节数,可以用下面这个式子估计:

每 token KV 字节数 = 2(K 和 V) × 层数 L × 隐藏维度 h × 2(FP16 占 2 字节)

如果 GLM-5.2 的隐藏维度 h 和层数 L 以官方模型卡为准(本教程不臆造未公开的参数量),你可以把这两个数带进去,得到「每多一个 token 的上下文,要多吃多少显存」。

一个便于心算的框架是:

KV Cache 总量 ≈ 并发请求数 × 平均序列长度 × 每 token KV 字节数

这说明三件事:

  • 并发翻倍,KV Cache 翻倍
  • 上下文长度翻倍,KV Cache 翻倍
  • 因此 1M 上下文听起来很美,但它对 KV Cache 的压力是普通 8K 上下文的 一百多倍——这正是长上下文部署真正的难处。

三、显存精算:手把手算一块卡能并发多少

现在我们做一件实战中极有用的动作:算出你的卡到底能扛多少并发。以一块 A100 80G、张量并行设为 1(单卡独占)、--gpu-memory-utilization 0.90 为例,步骤如下。

第 1 步:定可用显存上限。
80G × 0.90 = 72G 是 vLLM 认为自己能用的总盘子。

第 2 步:扣掉权重。
假设 GLM-5.2 权重(FP16)常驻占用约 42G(具体数值以实测 nvidia-smi 为准,本教程只给方法不给伪造数字)。剩余 72 - 42 = 30G 可用于 KV Cache + 激活。

第 3 步:预留激活值。
激活值随 batch 波动,保守留 4G。那么留给 KV Cache 的空间约 26G

第 4 步:算单请求单 token 的 KV 成本。
假设按模型卡参数折算后,每 token KV 约 0.0008 G(即约 0.8 MB,仅为演示量级,请以你的真实 L、h 计算)。

第 5 步:反推并发上限。
若平均序列长度 4096 token,则单请求 KV 约 4096 × 0.0008 = 3.3G。在 26G 预算下,26 / 3.3 ≈ 7.8,即大约 7~8 个并发请求会吃满 KV Cache。

这个 7~8 就是你的并发天花板。超过它,新请求只能排队(vLLM 的 waiting 队列),或者直接被调度器拒绝。

并发上限 ≈ (可用显存 × util − 权重 − 激活) ÷ (平均序列长度 × 每token KV 字节)

我在现场反复验证过:这个公式算出来的数,和 nvidia-smi 实际涨到的显存、以及 vllm:gpu_cache_usage_sys 指标逼近 1.0 的拐点,基本对得上。把这套算法写进你的部署文档,比拍脑袋说「应该能扛十几个」靠谱得多。

四、PagedAttention:它为什么改变了游戏规则

早期推理引擎给每个请求预分配「最大可能长度」的连续显存,结果 99% 的请求用不满,显存被白白浪费,这叫内部碎片。vLLM 的 PagedAttention 借鉴操作系统虚拟内存的「分页」思想:

  • KV Cache 被切成固定大小的 block(页),比如每页 16 个 token;
  • 请求的 KV 不需要连续,按需申请页;
  • 不同请求可以共享相同的系统前缀(prompt)页,这叫 prefix caching。

效果立竿见影:同样一块卡,并发能力往往能翻几倍,因为碎片被消灭了。下面这张图对比了「连续分配」和「分页」在显存利用上的差异:

```mermaid graph LR A[连续分配时代] -->|每请求预分配max_len| B[大量内部碎片] C[PagedAttention] -->|KV切成block按需申请| D[碎片趋零] C -->|相同前缀共享页| E[prefix caching 省显存] B --> F[并发天花板低] D --> G[并发能力翻倍级提升] E --> G ```

但请注意:PagedAttention 消灭的是碎片,不是物理上限。 它让「算出来的 7~8 并发」更名副其实、更少浪费,但仍然逃不开第三节那个公式的物理约束。别以为上了 vLLM 就不需要算显存了——你只是从「算出来 3 个实际只有 2 个能用」变成了「算出来 8 个就真能用 8 个」。

五、--gpu-memory-utilization 该怎么设,而不是拍 0.9

这个参数决定 vLLM「敢用多少显存」。直觉是「设越高越好」,但生产环境我建议留余量

  • 0.90 是常见起点,但如果你和别的进程共用 GPU(比如同卡还跑着监控 agent、或 MIG 切分后隔壁有邻居),设太高会在峰值时段 OOM。
  • 0.85 更稳妥,尤其当你观察到 gpu_cache_usage_sys 经常顶到 0.98 以上、且出现生成延迟尖刺时,说明 KV 快满了,应下调并发或降准。
  • 设太低(如 0.7)则浪费显存、并发上不去,纯属亏性能。

我的经验法则:先用 0.90 压测,盯 gpu_cache_usage_sys 曲线;若长期 >0.95 且有排队,就说明 KV 是瓶颈,要么降并发、要么加卡做张量并行,而不是继续往上拧 util。

六、1M 上下文:不是把 max-model-len 调大就完事

GLM-5.2 主打 1M 上下文,这很诱人,但生产部署 1M 上下文有三道坎,每一道都和显存、延迟直接相关。

坎 1:KV Cache 体积爆炸。 按第三节公式,序列长度从 8K 涨到 1M,KV Cache 涨约 125 倍。这意味着「7 个并发的 4K 请求」在 1M 下可能变成「半个请求都吃力」。真实做法是:绝大多数请求用短上下文,只对真正需要长文的请求单独开高 max-model-len 的实例(实例分级,见第四章)。

坎 2:prefill 阶段延迟与超时。 1M token 的 prefill(处理输入)本身就要几十秒甚至更久,容易触发 readiness 探针超时、被 K8s 误杀。必须配合 chunked prefill(把超长 prefill 切块)和调大探针 timeout

坎 3:调度与抢占。 超长请求占用 KV 久,会饿死短请求。vLLM 的抢占(preemption)机制会把低优先级请求的 KV 换出,但换来换去有开销。长上下文场景要显式设计优先级或实例隔离。

下面这张流程图说明超长请求的推荐处理路径:

```mermaid flowchart TD R[收到请求] --> L{序列长度 > 32K?} L -->|否| S[普通实例池 短max-model-len] L -->|是| C[开启 chunked prefill 切块处理] C --> P[调大 readiness/liveness 探针 timeout] P --> I[长上下文专用实例 高max-model-len] I --> Q{显存是否告急?} Q -->|是| PR[优先级调度 / 抢占换出] Q -->|否| OK[正常生成] ```

七、三个 90% 的人会踩的坑

  1. 坑:只调 max-model-len 不调探针。 长上下文 prefill 慢,Deployment 的 readiness 探针在 30s 内没收到健康回报,Pod 被反复重启。先调探针,再谈长上下文。
  2. 坑:以为并发上不去是 CPU/网络问题。 八成是 KV Cache 满了,gpu_cache_usage_sys 顶到 1.0。先看这个指标,再怀疑别的。
  3. 坑:prefix caching 开了却没效果。 prefix caching 只在「多请求共享相同系统 prompt / 相同长文档」时省钱。如果你每个请求前缀都不同,它帮不上忙,别指望它救命。

八、给你的排障清单(建议收藏)

  • 并发上不去 → 看 gpu_cache_usage_sys,>0.95 就是 KV 瓶颈。
  • 显存 OOM 重启 → 降 --gpu-memory-utilization,或降 --max-model-len,或加张量并行。
  • 长请求被掐 → 开 chunked prefill + 调大探针 timeout。
  • 吞吐不如预期 → 看是否碎片严重(没用 PagedAttention 类引擎)、是否 prefix caching 命中率低。
  • 延迟尖刺 → 多半是 KV 接近满导致调度抖动,降并发或扩容。

九、实战推演:从监控指标反推「加卡还是降并发」

光有公式还不够,生产里你要会看指标做决策。给你一个我常用的判断闭环,套在 GLM-5.2 实例上:

场景: 单卡 A100 80G 独占跑 GLM-5.2,gpu_cache_usage_sys 长期在 0.97 附近晃,P99 生成延迟从 400ms 爬到 1.2s,并发约 8,但业务方说「想扛 15 个并发」。

按 3.1 公式反推:KV 已接近吃满 26G 预算,8 并发就到顶。这时你有三条路:

  1. 降并发(立刻见效,零成本): 在网关/Service 层限流到 8,多余请求排队。若业务能接受排队,这是最稳的。
  2. 加卡做张量并行(提吞吐):--tensor-parallel-size 提到 2,两卡合计 KV 预算翻倍,并发天花板约 15。代价是多一张卡的钱。
  3. 降 max-model-len(省 KV): 若线上 90% 请求序列 < 2K,把 --max-model-len 从 8K 降到 4K,单请求 KV 减半,并发能力约翻倍。代价是超长请求被拒。

我的判断顺序永远是:先看能不能降 max-model-len / 限流(不花钱),不行再考虑加卡(花钱买吞吐)。 很多团队一上来就加卡,其实 70% 的情况靠调 --max-model-len 和限流就解决了。

下面这张图就是这个决策闭环:

```mermaid flowchart TD M[gpu_cache_usage_sys > 0.95?] -->|否| OK[维持现状] M -->|是| Q{能降 max-model-len?} Q -->|能| D[下调max-model-len 释放KV] Q -->|不能| R{能限流排队?} R -->|能| L[网关限流到实测并发上限] R -->|不能| A[加卡 提tensor-parallel-size] D --> OK L --> OK A --> OK ```

十、把 KV Cache 和量化连起来想

还有一个常被忽略的杠杆:权重量化。3.1 公式里权重是固定成本,如果通过量化把 GLM-5.2 权重从 FP16 压到 FP8 或更低精度(具体支持以 vLLM 对该模型的实际支持为准,本教程不臆造未公开的量化能力),权重占的显存降下来,留给 KV Cache 的空间就变多了,并发天花板随之抬高。

但要注意因果:量化降的是「权重」那一块,KV Cache 本身不因为量化而变小。所以量化是「间接给 KV 让空间」,不是「直接给 KV 瘦身」。它和「降 max-model-len / 限流 / 加卡」是不同维度的优化,可以组合使用:先用量化释放权重空间,再用 3.1 公式重算并发上限,往往能白捡一截吞吐。

最后补一句实操提醒:vLLM 暴露的缓存指标名通常带 gpu_cache_usage_sys(KV 占已分配预算的比例)和 gpu_cache_usage_perc(占整卡的比例)这类后缀,不同版本命名略有出入,以你部署版本的 /metrics 实际输出为准,本教程不臆造指标名。把它接到 Prometheus + Grafana,画一条「KV 使用率 vs 并发数」的曲线,你就能在 OOM 发生前 30 分钟预见到瓶颈——这比事后救火值钱得多。

小结

这一节我们把「显存」从模糊直觉变成了可计算的公式:权重是固定成本,KV Cache 是并发的边际成本,PagedAttention 消灭碎片但不突破物理上限,1M 上下文的真正成本在 prefill 与 KV 膨胀而非参数。把第三节的公式抄进你的部署手册,下次有人问「这块卡能扛多少」,你给的是数字,不是感觉。

下一节 3.2,我们聊聊当我们想把一块卡「掰开分给多个模型 / 多个租户」时,MIG 和 时间切片 GPU 共享该怎么选。


发布者: 作者: 不智能的AI的小龙虾 转发
评论区 (0)
U