LLM推理基础:Transformer解码器架构、自回归生成与KV Cache
核心原理:从Transformer解码器到Token预测
现代大语言模型(LLM)的推理过程,本质上是在一个Transformer解码器上执行逐Token的自回归生成。与编码器-解码器架构不同,当前主流LLM(如GPT系列、Llama、Qwen)仅使用解码器(Decoder-only),其核心组件包括:
- 嵌入层(Embedding):将输入Token映射为高维向量,并叠加位置编码(RoPE或ALiBi)以保留序列顺序信息。
- 多头自注意力(Multi-Head Self-Attention):每个Token的Query向量与所有Token的Key向量计算注意力分数,再对Value向量加权求和,捕捉序列内任意位置间的依赖关系。
- 前馈网络(FFN):对每个Token独立进行两次线性变换与激活(如SwiGLU),增强非线性表达能力。
- 层归一化与残差连接:稳定深层网络训练,加速收敛。
解码器通过堆叠上述模块(通常为32~80层),将输入序列逐步编码为上下文表示。在生成阶段,模型每次只预测下一个Token的概率分布,然后从分布中采样或取最大概率项,作为新Token追加到输入序列末尾,再进入下一轮迭代。
自回归生成:逐Token的迭代过程
自回归(Autoregressive)生成的核心约束是:每个新Token的生成都依赖于此前所有Token。以生成“我爱学习”为例,完整过程如下:
- 初始化:输入起始符
<bos>,模型计算其隐藏状态。 - 第1次迭代:基于
<bos>的表示,预测“我”为最高概率Token。 - 第2次迭代:将“我”拼接到输入末尾,模型基于
<bos> 我预测“爱”。 - 第3次迭代:输入变为
<bos> 我爱,预测“学”。 - 第4次迭代:输入变为
<bos> 我爱学,预测“习”。 - 终止:遇到结束符
<eos>或达到最大长度,停止生成。
每次迭代中,模型需要为所有历史Token重新计算注意力分数。如果不做任何缓存,第n次迭代的计算量将包含前n-1次的所有重复计算,导致总计算量呈平方级增长。这正是KV Cache存在的根本原因。
KV Cache:消除重复计算的关键机制
KV Cache的核心思想非常直观:在自注意力计算中,历史Token的Key和Value向量只取决于其自身,与后续新Token无关。因此,可以将已计算过的K、V矩阵缓存下来,新Token到来时只需:
- 计算新Token自己的Q、K、V向量;
- 用新Token的Q与缓存中所有K做点积得到注意力分数;
- 用注意力分数对缓存中所有V加权求和,得到新Token的输出。
KV Cache的存储结构通常为两个张量:K_cache和V_cache,形状均为 [batch_size, num_heads, seq_len, head_dim]。每生成一个新Token,就将该Token在所有层、所有注意力头上的K、V向量追加到缓存末尾。
案例:KV Cache如何加速长文本生成
假设使用Llama-2-7B生成一段2000字的文章,对比有无KV Cache的差异:
| 对比维度 | 无KV Cache | 有KV Cache |
|---|---|---|
| 单Token生成延迟 | 随序列长度线性增长 | 基本恒定(仅计算新Token) |
| 总计算量 | O(n²) 次K/V计算 | O(n) 次K/V计算 |
| 2000 Token总耗时 | 约 2000²/2 = 200万次K/V计算 | 约 2000次K/V计算 |
| 显存占用 | 低(不缓存) | 高(需缓存全部历史) |
| 适用场景 | 几乎不适用 | 所有生产环境 |
实际测试中,在A100上使用KV Cache后,Llama-2-7B的生成速度可从约5 tokens/s提升至40 tokens/s以上,提升幅度达8倍。但代价是显存中需要预留大量空间存放KV Cache,这直接催生了PagedAttention等显存管理技术的出现。
误区与复盘:KV Cache的常见误解
正确理解:KV Cache只是缓存了中间计算结果,数学上与不缓存完全等价。它不会影响概率分布,只影响计算效率。
正确理解:Transformer每一层都有独立的注意力模块,因此每一层都需要缓存。Llama-2-7B的32层每层都要维护自己的K/V缓存。
正确理解:KV Cache占用显存后,会挤压模型权重和激活值的空间。当显存不足时,系统可能被迫降低batch size,反而降低整体吞吐。KV Cache的管理需要在缓存容量与批处理大小之间寻找平衡。
复盘一个典型的生产事故:某团队部署LLM服务时,将最大序列长度设为8192,结果发现并发请求稍多就触发显存溢出(OOM)。排查后发现,每个请求的KV Cache占用高达4GB(8192 Token × 512KB),8个并发请求就吃满了40GB显存。最终通过限制最大生成长度、使用PagedAttention按页分配缓存,才解决了问题。