大模型推理优化:从自回归生成到 KV Cache 与量化
从 GPT 自回归生成开始,逐步讲清楚 Prefill、Decode、KV Cache、GQA、分页式缓存管理、INT8 量化、校准集和离群值。
- AI
- 大模型
大模型推理优化:从自回归生成到 KV Cache 与量化
前面的文章已经讲过 GPT 的基本路径:
文本 -> token -> embedding -> attention -> logits -> 下一个 token
如果还没有读,可以先看:
这篇文章继续往工程方向走:模型结构理解了以后,它真正推理时为什么慢,为什么占显存,为什么要做 KV Cache、Prefill、Decode、量化和缓存管理。
先从一句话生成开始
假设 prompt 是:
我喜欢
GPT 会预测下一个 token。假设预测出:
学习
下一步输入就变成:
我喜欢学习
模型再预测下一个 token。
这个过程叫自回归生成:
已有 token -> 预测下一个 token -> 拼回序列 -> 继续预测
它有一个天然限制:第 10 个 token 必须等第 9 个 token 生成后才能预测。
所以生成阶段不像训练那样可以完全并行。
朴素推理会重复计算
在 Attention 里,每个 token 会产生 Query、Key、Value。
公式是:
生成新 token 时,历史 token 已经固定了。比如已经生成:
我 喜欢 学习
下一步要预测第 4 个 token。历史三个 token 的 Key 和 Value 不会改变。
如果每一步都从头算一遍历史 token 的 K/V,就会浪费大量计算。
这时自然引出 KV Cache。
KV Cache 的直觉
KV Cache 的思想很简单:
历史 token 的 Key 和 Value 算过一次后缓存起来,后面直接复用。
生成第 个 token 时,只需要:
- 为当前新 token 计算新的 。
- 把新的 追加到缓存。
- 用当前 去读取缓存里的所有历史 。
流程可以画成:
flowchart LR
P[Prompt tokens] --> A[Prefill: 一次性计算 prompt 的 K/V]
A --> C[(KV Cache)]
C --> D[Decode step]
D --> E[计算新 token 的 Q/K/V]
E --> F[追加新 K/V 到缓存]
F --> C
E --> G[当前 Q 读取历史 K/V]
G --> H[输出下一个 token]
KV Cache 没有改变模型数学结构,它只是避免重复计算历史 K/V。
Prefill:先处理完整 prompt
推理通常分成两个阶段:Prefill 和 Decode。
Prefill 阶段负责处理用户输入的完整 prompt。
特点:
- 输入 token 多。
- 可以并行处理 prompt 中所有 token。
- 会生成每一层的初始 KV Cache。
- 主要影响首 token 延迟,也就是 Time To First Token。
例如用户输入 2000 个 token,模型要先读完这 2000 个 token,建立上下文表示和 KV Cache,然后才能开始输出第一个 token。
Decode:逐 token 生成
Decode 阶段负责一个 token 一个 token 往外生成。
特点:
- 每一步通常只处理一个新 token。
- 必须依赖上一步生成结果。
- 每一步都会读取历史 KV Cache。
- 很容易受显存带宽影响。
如果用户要求生成 500 个 token,decode 就要循环很多次。
所以优化推理时要分清:
| 指标 | 主要受什么影响 |
|---|---|
| TTFT,首 token 延迟 | Prefill |
| TPOT,每输出 token 延迟 | Decode |
| 吞吐 | batch 调度、显存带宽、缓存管理 |
KV Cache 为什么占显存
KV Cache 是用显存换计算。
它的大小大致和下面这些量成正比:
batch_size × num_layers × seq_len × num_kv_heads × head_dim × 2 × bytes_per_element
其中乘以 2 是因为同时存 Key 和 Value。
如果使用 FP16,每个元素 2 字节:
含义是:
- :batch size。
- :层数。
- :序列长度。
- :KV head 数。
- :每个 head 的维度。
这解释了为什么长上下文推理很吃显存:序列越长,KV Cache 线性增长。
GQA 为什么能降低缓存压力
在 ROPE与GQA 里已经讲过 GQA。这里从推理角度再看一次。
传统 MHA 中,每个 attention head 都有自己的 K/V。这样效果强,但 KV Cache 大。
MQA 让所有 query head 共享一组 K/V,缓存最小,但表达能力可能下降。
GQA 介于两者之间,让一组 query head 共享一组 K/V。
| 结构 | KV head 数 | KV Cache 压力 | 取舍 |
|---|---|---|---|
| MHA | 多 | 高 | 表达能力强 |
| MQA | 1 | 低 | 压缩最激进 |
| GQA | 中间值 | 中 | 常用折中 |
所以 GQA 不只是模型结构技巧,也是推理显存优化手段。
分页式 KV 管理
真实服务里,请求长度不一样,生成长度也不一样。
如果每个请求都提前分配一整块最大长度缓存,会浪费显存;如果频繁申请和释放不同大小的缓存,又容易产生碎片。
分页式或 block 式 KV 管理的思路是:
- 把 KV Cache 切成固定大小的块。
- 请求需要更多上下文时,再追加块。
- 请求结束后,把块还给缓存池。
- 调度 batch 时按块管理不同请求的历史上下文。
它解决的是服务层面的内存管理问题,不改变 Attention 公式。
量化:让模型更小、更省带宽
除了 KV Cache,模型权重本身也占大量显存。量化就是把高精度数值转换成低精度表示。
例如从 FP16 变成 INT8。
一个简单线性量化可以写成:
反量化时:
这里 是 scale。
量化的收益:
- 模型占用更小。
- 权重读取带宽更低。
- 某些硬件上 INT8 计算更快。
- 可以容纳更大 batch 或更长上下文。
为什么量化会损失精度
量化是用有限整数刻度近似连续浮点数,所以会有误差。
影响精度的关键因素有:
- 激活范围是否稳定。
- 校准集是否接近真实输入。
- 是否存在离群值。
- 哪些层保留浮点计算。
- 只量化权重,还是权重和激活都量化。
离群值很麻烦。假设大多数激活在 ,但少数值到了 20。为了覆盖 20,scale 会变大,导致 内的大量普通值可用刻度变少。
这就是为什么大模型量化经常要特殊处理 outlier,或者保留部分敏感层为 FP16 / BF16。
校准集为什么重要
后训练量化通常需要校准集。
校准集的作用是估计真实推理时的激活范围。
如果校准集和真实输入差异很大,scale 就会估错:
- 激活可能被截断。
- 刻度可能浪费。
- 下游任务精度下降。
所以校准集必须尽量贴近真实业务数据。
这个思想和 机器学习建模 里讲的数据分布问题是一致的:训练、校准、测试和真实环境的分布差异,都会影响模型表现。
推理优化是在做取舍
推理优化不是越激进越好。它总是在延迟、吞吐、显存和精度之间取舍。
| 方法 | 收益 | 代价 |
|---|---|---|
| KV Cache | 减少重复计算 | 增加显存 |
| GQA / MQA | 减少 KV 显存 | 可能影响表达能力 |
| INT8 量化 | 降低模型大小和带宽 | 可能损失精度 |
| 分页式 KV | 减少碎片,提升调度 | 系统实现更复杂 |
| 动态 batch | 提升吞吐 | 单请求延迟可能波动 |
交互式聊天更关心首 token 延迟和每 token 延迟;批量离线生成更关心吞吐;长文档问答更关心上下文长度和 KV Cache 显存。
总结
大模型推理优化可以按这条线理解:
自回归生成很慢
-> 历史 K/V 会重复计算
-> KV Cache 用显存换计算
-> Prefill 和 Decode 拆分不同阶段
-> GQA/MQA 降低 KV 压力
-> 分页式管理减少显存碎片
-> INT8 量化降低模型和带宽成本
这篇文章不是单独讲部署技巧,而是接在 GPT 和 Attention 之后:只有先理解 token 如何通过 Attention 读取上下文,才能真正理解为什么 KV Cache 是推理系统里的核心结构。
尺素
0 封尺素
正在取尺素...