大模型推理时,显存里不只有权重。还有一块会随着「正在处理的文本有多长、同时有几路请求」膨胀的缓存,叫 KV cache。很多「权重明明放得下,一并发就 OOM」的情况,先查的就是它。
它在记什么
Transformer 的自注意力要拿当前 token,去和前面所有 token 的 Key、Value 做运算。如果每生成一个新 token 都把历史重新算一遍,解码会慢到不能用。
所以实现上把已经算过的 Key 和 Value 留下来,下一步只算新 token,再和缓存拼起来。这块留下的张量就是 KV cache。
粗略体积可以记成:
KV cache ≈ 2 × 层数 × 批大小 × 序列长度 × 每层 KV 的隐藏维度 × 每元素字节数
前面的 2 是 Key 和 Value 各一份。序列长度和批大小是乘法关系:上下文从 2K 拉到 32K,或者并发从 1 提到 16,缓存都会按比例变大。权重则约等于「参数量 × 每参数字节数」,FP16 大约是 2 字节,不随这次请求的长度变化。
Prefill 和 Decode
生成通常分成两段:
| 阶段 | 在做什么 | 显存和延迟上的特点 |
|---|---|---|
| Prefill | 把整段提示词一次吃进去,算出第一个输出 token | 算力重,决定首 token 时间(TTFT) |
| Decode | 每次只生成一个新 token,并把它的 KV 追加进缓存 | 更吃缓存带宽;上下文越长,每步读的缓存越大 |
所以两种「慢」不是同一件事。首字很久才出来,优先看提示词是不是太长、系统提示有没有被重复计算。字已经在往外蹦但很慢,优先看并发、序列长度和 KV 有没有把显存或带宽打满。
为什么短句压测会骗人
用 "hello" 测出来的剩余显存,不能代表线上。线上提示词常常是系统提示加几轮对话再加检索来的材料。KV cache 按真实长度分配。服务要为每个模型准备一张表:在某个 max_len 下,最多允许多少并发。超过就排队或拒绝,而不是让分配失败把进程打死。
另外留一块给激活和显存碎片。理论值刚好卡满时,加载、卸载几次之后,空闲显存加起来够,却划不出连续的一块,分配仍然会失败。
和别的优化怎么配合
- 前缀缓存(prefix cache):多条请求共享同一段系统提示的 KV,避免每条都重算这段。
- 分页 KV(PagedAttention 一类):把缓存切成块来分配,减轻碎片,让不同长度的请求更好共卡。
- 限制
max_tokens和上下文:硬顶比事后 OOM 便宜。 - 量化:主要缩小权重,有的实现也会压缩 KV。上下文特别长时,缓存仍然要单独限额,不能假设量化之后并发可以随意加。
小结
权重是固定账单,KV cache 是随请求变的账单。讲「这张卡能跑某个模型」时,必须同时说最大上下文和最大并发。压测用真实长度,并给并发一个硬上限。调度层面怎么分池,见 GPU 调度实战;服务本身的超时和排队,见 推理服务入门。