用空间换取时间:理解 LLM 的记忆开销

为什么不加 Cache 会让模型越跑越慢

在自回归生成中,第 $N$ 步生成的注意力计算需要第 $1$ 到 $N-1$ 步的所有 Key ($K$) 和 Value ($V$) 矩阵。如果没有 Cache,每生成一个新 Token,模型都要重新计算前面所有 Token 的特征。

  • 计算浪费:原本已经计算过的 $K$ 和 $V$ 在每一轮都被重复计算,计算复杂度呈 $O(N^2)$ 增长。
  • KV Cache 的作用:我们将已经计算好的历史 Token 的 $K$ 和 $V$ 缓存下来,每一轮只需要计算当前 Token 自己的 $K/V$,然后与缓存拼接。计算复杂度降为 $O(N)$。

第一阶段:KV Cache 的容量计算模型

KV Cache 极其消耗显存,是限制 LLM 并发(Batch Size)和上下文长度(Context Length)的第一杀手。你需要掌握如何精确计算其开销:

对于每一层 Transformer,每个 Token 的 KV Cache 占用字节数为: $Size = 2 \times (\text{Layers}) \times (\text{Hidden_Dim}) \times (\text{Precision_Bytes})$

案例分析: 假设使用 FP16(2 字节)推理 Llama-7B(32 层,4096 隐藏维度):

  • 每个 Token 消耗:$2 \times 32 \times 4096 \times 2 = 524,288$ 字节 $\approx 0.5$ MB。
  • 如果对话长度为 2048 Token:单用户消耗 $0.5 \times 2048 = 1$ GB 显存。
  • 如果要支持 128 个并发用户:仅 KV Cache 就要消耗 $128$ GB 显存。

这解释了为什么 80GB 的 A100 显卡也无法在 FP16 下承载长上下文的高并发任务。

第二阶段:Prefill 与 Decode 的二段论

在工程实现中,KV Cache 的生命周期分为两个截然不同的阶段:

1. Prefill(预填充)

模型接收到 Prompt 文本,一次性计算所有输入 Token 的 $K/V$ 并存入 Cache。

  • 特性:计算密集型。此时 GPU 满载,利用率高,属于 Compute-bound

2. Decode(解码)

模型逐个生成新 Token。

  • 特性:访存密集型。每生成一个词都要读一次全量 Cache 和模型权重,但计算量极小。此时 GPU 核心大部分时间在等待数据从显存搬运过来,属于 Memory-bound

第三阶段:内存碎片与 PagedAttention 的诞生

传统的 KV Cache 分配是预先分配一段连续的显存空间。

  • 痛点 1:内部碎片。如果预留了 4K 上下文但用户只聊了 10 个词,剩下的空间完全浪费。
  • 痛点 2:外部碎片。不同请求的长度不同,显存被切得支离破碎。
  • PagedAttention(vLLM 的核心):借鉴了操作系统的虚拟内存理念,将 KV Cache 存储在不连续的“块”(Blocks)中。这消除了 90% 以上的浪费,使系统的并发吞吐量提升了 2-3 倍。

实验与实战指南

在本项目中,你需要完成以下任务:

  1. 显存监控脚本:编写一段代码,记录生成过程中显存占用的线性增长趋势。
  2. 延迟对比实验:实现“不带 Cache”和“带 Cache”的生成器。你会发现随着序列变长,不带 Cache 的版本延迟呈指数级飙升,而带 Cache 的版本单字延迟几乎恒定。
  3. KV Cache 量化测试:尝试将 KV Cache 从 FP16 量化到 INT8。观察显存减半后,模型的长文召回率(如“大海捞针”测试)是否受损。

总结

KV Cache 是 LLM 推理系统的阿喀琉斯之踵。它让推理变得飞快,但也让显存变得极其紧缺。作为系统工程师,理解 KV Cache 的数学模型是进行架构优化、选择量化策略、以及估算服务器承载能力的基石。

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:项目 11:显存的吞噬者:KV Cache 机制与显存预算

本文链接:https://www.sshipanoo.com/blog/ai/llm-roadmap/lab-11-kv-cache/

本文最后一次更新为 天前,文章中的某些内容可能已过时!