Update 大模型应用 - 进阶.md

补充旋转位置编码与长度外推小节、附录验证及相关配图;补充 FlashAttention 显存读写图与 GAE 计算示例,并微调部分文字表述与参考资料。
This commit is contained in:
CyC2018
2026-10-30 20:19:00 +08:00
parent fb64a7a073
commit 5e79c2b174
3 changed files with 77 additions and 9 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

+77 -9
View File
@@ -11,6 +11,7 @@
* [A1 多头注意力、GQA 与 KV Cache 的实现](#a1-多头注意力gqa-与-kv-cache-的实现)
* [A2 在线 softmax 与注意力的计算量](#a2-在线-softmax-与注意力的计算量)
* [A3 ZeRO 的显存与 MLA 的 KV Cache](#a3-zero-的显存与-mla-的-kv-cache)
* [A4 RoPE 的相对位置与频率](#a4-rope-的相对位置与频率)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -50,11 +51,13 @@ class MHA(nn.Module):
#### 2. 计算量与 FlashAttention
主线见[大模型应用 · 计算量与 FlashAttention](大模型应用.md#2-计算量与-flashattention),这里先比较线性层与注意力的计算量,再看 FlashAttention 怎样在不改变结果的前提下减少显存的读写。
主线见[大模型应用 · 计算量与 FlashAttention](大模型应用.md#2-计算量与-flashattention)。
序列长度为 n、向量维度为 d 时,QK<sup>T</sup> 和乘以 V 各需要约 n<sup>2</sup>d 次乘加,时间复杂度为 O(n<sup>2</sup>d),分数矩阵占用 O(n<sup>2</sup>) 的显存。按 Qwen2.5-7B 的配置计算,每个 Token 经过一层的线性层约需 4.7 亿次浮点运算,与上下文长度无关;生成第 T 个 Token 时,一层注意力的分数和加权求和约需 14336 × T 次,T 约为 3.2 万时两者相当(见[附录 A2](#a2-在线-softmax-与注意力的计算量))。所以上下文较短时,计算量主要在线性层,上下文越长,注意力所占的比例越大。
按 Qwen2.5-7B 的配置计算,每个 Token 经过一层的线性层约需 4.7 亿次浮点运算,与上下文长度无关;生成第 T 个 Token 时,一层注意力的分数和加权求和约需 14336 × T 次,T 约为 3.2 万时两者相当(见[附录 A2](#a2-在线-softmax-与注意力的计算量))。
标准实现分几步完成:算出 n × n 的分数矩阵写入显存,读出来做 softmax 再写回,再读出来乘以 V。GPU 的显存(HBM)容量大但带宽有限,A100 为 1.5~2.0 TB/s;每个计算单元旁边的片上缓存(SRAM)带宽约 19 TB/s,但总共只有约 20 MB。这个矩阵又很大,n = 32K 时,Qwen2.5-7B 一个头的分数矩阵在 BF16 下就占 2 GB,大部分时间花在了读写显存上。
序列长度为 n 时,标准实现分几步完成:算出 n × n 的分数矩阵写入显存,读出来做 softmax 再写回,再读出来乘以 V。GPU 的显存(HBM)容量大但带宽有限,A100 为 1.5~2.0 TB/s;每个计算单元旁边的片上缓存(SRAM)带宽约 19 TB/s,但总共只有约 20 MB。这个矩阵又很大,n = 32K 时,Qwen2.5-7B 一个头的分数矩阵在 BF16 下就占 2 GB,大部分时间花在了读写显存上。
<div align="center"> <img src="pics/llmadv-flash-io.png" width="780px"/> </div><br>
FlashAttention 把计算分块:每次把一块 Q 和一块 K、V 读入 SRAM,算出这一块的分数后直接累积到输出中,n × n 的矩阵从不写回显存。难点在于 softmax 要除以全部分数的指数和,只看到一部分 K 时还不知道这个和。解决方法是在线 softmax:对每个查询维护目前为止的最大分数 m、指数和 l 以及未归一化的输出 o,读入新的一块后,如果最大值变大了,就把之前的 l 和 o 乘以 e<sup>m<sub>旧</sub> − m<sub>新</sub></sup> 再累加。例如一个查询与 4 个 Token 的分数为 2、1、3、0,对应的 V 为 10、20、30、40,每块 2 个 Token:
@@ -69,11 +72,27 @@ FlashAttention 把计算分块:每次把一块 Q 和一块 K、V 读入 SRAM
所以 FlashAttention 是精确计算,而不是近似(numpy 实测,见[附录 A2](#a2-在线-softmax-与注意力的计算量))。反向传播时也不保存分数矩阵,只保存 m 和 l,需要时重新计算分数,多做了一些计算,但显存的读写更少,整体仍然更快。论文在 A100 上计算 GPT-2 medium 的注意力(序列长度 1024),浮点运算量从 66.6 G 增加到 75.2 G,显存读写却从 40.3 GB 降到 4.4 GB,运行时间从 41.7 毫秒降到 7.3 毫秒。FlashAttention-2 进一步减少了矩阵乘法以外的运算,并沿序列长度的方向并行,速度又提高了约 1 倍。
#### 3. 旋转位置编码与长度外推
主线见[大模型应用 · 注意力机制](大模型应用.md#注意力机制)。以 Qwen2.5-7B 为例,每个头 128 维分成 64 组,base = 10<sup>6</sup>,各组的波长(转一圈需要的 Token 数)从 6.28 到约 506 万。按训练长度 32768,64 组分成三类(见[附录 A4](#a4-rope-的相对位置与频率)):
- 24 组转了 32 圈以上,各种角度在训练中都出现过;
- 24 组转不满一圈;
- 其余 16 组介于两者之间。
<div align="center"> <img src="pics/llmadv-rope-rotations.png" width="680px"/> </div><br>
直接输入更长的文本时,问题出在转不满一圈的组:例如最慢的一组在训练中最多转到 0.041 弧度,位置 131071 要转到 0.163 弧度,模型从没见过这样的角度,困惑度迅速上升。
位置插值(Position Interpolation, PI)把所有位置按比例压缩,例如扩展 4 倍时把位置 131071 当作 32767.8 计算,角度都落回训练过的范围,论文用它把 LLaMA 扩展到 32768,微调不到 1000 步。缺点是转得快的组也被压缩了,相邻 Token 在第 0 组的角度差从 1 弧度变成 0.25 弧度,近处的 Token 更难区分。
YaRN 按组区别处理:转 32 圈以上的组不压缩,转不满一圈的组按 PI 的方式压缩,中间的组线性过渡;另外把注意力分数乘以一个随扩展倍数增大的系数,扩展 4 倍时约为 1.30。Qwen2.5-7B-Instruct 在 config.json 中加入 rope_scaling(type 为 yarn,factor 为 4.0,original_max_position_embeddings 为 32768)后,可以从 32K 扩展到 128K;vLLM 只支持静态 YaRN,倍数不随输入长度变化,可能影响短文本的效果,所以只在需要处理长文本时开启。
### 自回归生成与 KV Cache
#### 1. 带 KV Cache 的 GQA
主线见[大模型应用 · 自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache),这里接着上文多头注意力的实现,给出解码时带 KV Cache 的 GQA。
主线见[大模型应用 · 自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache)。
带 KV Cache 的 GQA 在解码时每一步只输入新的 Token,把它的 K、V 追加到缓存末尾,再与缓存中的全部 K、V 计算注意力:
@@ -92,7 +111,7 @@ o = (q_new @ k.transpose(-2, -1) / dk ** 0.5).softmax(-1) @ v # 新 Token 可
主线见[大模型应用 · 自回归生成与 KV Cache](大模型应用.md#自回归生成与-kv-cache)中减少 KV Cache 的方法。
多头潜在注意力(Multi-head Latent Attention, MLA)由 DeepSeek-V2 提出。GQA 减少的是 K、V 的组数,MLA 则把 K、V 联合压缩成一个低维的潜在向量,只缓存这个向量,计算时再用矩阵还原出各个头的 K、V;推理时还原矩阵可以合并到 Q 和输出的矩阵中,不必真的算出每个头的 K、V。RoPE 会妨碍这种合并,所以另用一个所有头共享的小向量携带位置信息。DeepSeek-V3 每层每个 Token 只缓存 512 + 64 = 576 个数,61 层共 68.6 KB,与 Qwen2.5-7B 的 56 KB 相近;同样的 128 个头如果用 MHA,每个 Token 要 4.77 MB(按 config.json 计算,见[附录 A3](#a3-zero-的显存与-mla-的-kv-cache))。论文中 MLA 的缓存量只相当于 2.25 组的 GQA,效果却好于 MHA。
MLA 只缓存每个 Token 压缩后的低维潜在向量,计算时用矩阵还原出各个头的 K、V。推理时这些还原矩阵可以合并到 Q 和输出的矩阵中,不必真的算出每个头的 K、V。RoPE 会妨碍这种合并,所以另用一个所有头共享的小向量携带位置信息。DeepSeek-V3 每层每个 Token 只缓存 512 维的潜在向量和 64 维的位置向量,共 576 个数,61 层共 68.6 KB;同样的 128 个头如果用 MHA,每个 Token 要 4.77 MB(按 config.json 计算,见[附录 A3](#a3-zero-的显存与-mla-的-kv-cache))。DeepSeek-V2 论文中 MLA 的缓存量只相当于 2.25 组的 GQA,效果却好于 MHA。
### 训练过程
@@ -100,7 +119,17 @@ o = (q_new @ k.transpose(-2, -1) / dk ** 0.5).softmax(-1) @ v # 新 Token 可
主线见[大模型应用 · RLHF 与 PPO](大模型应用.md#2-rlhf-与-ppo),这里展开 PPO 中每个 Token 的优势怎样计算。
把生成过程看作强化学习:状态是提示词和已经生成的 Token,动作是下一个 Token,奖励模型的分数在回答结束时才给出。每个 Token 的优势用广义优势估计(Generalized Advantage Estimation, GAE)计算:先算每一步的时序差分误差 δ<sub>t</sub> = r<sub>t</sub> + γV(s<sub>t+1</sub>) − V(s<sub>t</sub>),即走完这一步后价值模型的估计比之前高了多少,优势是之后各步 δ 的加权和 Σ (γλ)<sup>k</sup> δ<sub>t+k</sub>。λ = 0 时只看一步,依赖价值模型的估计,方差小但有偏差;λ = 1 时相当于最终得分减去 V(s<sub>t</sub>),没有偏差但方差大;λ 取两者之间的值来折中。
把生成过程看作强化学习:状态是提示词和已经生成的 Token,动作是下一个 Token,奖励模型的分数在回答结束时才给出。每个 Token 的优势用广义优势估计(Generalized Advantage Estimation, GAE)计算:先算每一步的时序差分误差 δ<sub>t</sub> = r<sub>t</sub> + γV(s<sub>t+1</sub>) − V(s<sub>t</sub>),即走完这一步后价值模型的估计比之前高了多少,优势是之后各步 δ 的加权和 Σ (γλ)<sup>k</sup> δ<sub>t+k</sub>。
λ = 0 时只看一步,依赖价值模型的估计,方差小但有偏差;λ = 1 时等于之后各步奖励的折扣和减去 V(s<sub>t</sub>),没有偏差但方差大;λ 取两者之间的值来折中。例如一个 3 个 Token 的回答在结尾得到奖励 1,取 RLHF 中常用的 γ = 1,暂不考虑 KL 惩罚,价值模型对三个状态的估计依次为 0.5、0.6、0.8:
```text
三步的 δ 0 + 0.6 − 0.5 = 0.1 0 + 0.8 − 0.6 = 0.2 1 + 0 − 0.8 = 0.2
第 1 个 Token 的优势:
λ = 0 0.1
λ = 1 0.1 + 0.2 + 0.2 = 0.5,等于最终得分 1 减去 V(s0) = 0.5
λ = 0.95 0.1 + 0.95 × 0.2 + 0.95² × 0.2 = 0.471
```
#### 2. GRPO 的改进
@@ -111,7 +140,7 @@ o = (q_new @ k.transpose(-2, -1) / dk ** 0.5).softmax(-1) @ v # 新 Token 可
- 全对或全错的题没有梯度:模型越学越好,全对的题越来越多,一批中有效的题目越来越少。DAPO 的动态采样多采一些题,丢掉全对和全错的,直到填满一批;
- 熵坍塌(Entropy Collapse):模型输出的熵在训练中很快下降,同一道题的多个回答几乎一样,失去了探索新解法的能力。原因之一是概率之比的上限:ε = 0.2 时,概率为 0.9 的 Token 一次可以升到 1,而概率为 0.01 的 Token 最多只能升到 0.012。DAPO 把上下限分开,下限仍为 0.2,上限放宽到 0.28;
- 长度偏差:GRPO 先在每个回答内部按 Token 数求平均,长回答中每个 Token 的权重变小,错误的长回答受到的惩罚被稀释,回答越来越长。DAPO 改为把一批中所有回答的 Token 放在一起平均;
- MoE 模型训练不稳定:每次更新后,同一个回答激活的专家会发生变化,Qwen3-30B-A3B 中约 10% 不同,按 Token 计算的概率之比剧烈波动。GSPO 改为按整个回答计算概率之比(取每个 Token 概率之比的几何平均),并对整个回答截断,Qwen3 的强化学习使用了这种方法。
- [MoE](大模型应用.md#3-混合专家) 模型训练不稳定:每次更新后,同一个回答激活的专家会发生变化,Qwen3-30B-A3B 中约 10% 不同,按 Token 计算的概率之比剧烈波动。GSPO 改为按整个回答计算概率之比(取每个 Token 概率之比的几何平均),并对整个回答截断,Qwen3 的后续版本使用了这种方法。
DAPO 还去掉了 KL 项,理由是训练长推理时模型本来就要远离初始模型。它用 Qwen2.5-32B 基座模型在 AIME 2024 上达到 50 分,最初直接使用 GRPO 时只有 30 分。
@@ -121,7 +150,7 @@ DAPO 还去掉了 KL 项,理由是训练长推理时模型本来就要远离
主线见[大模型应用 · QLoRA 与全参数微调](大模型应用.md#4-qlora-与全参数微调),这里展开全参数微调时怎样把模型状态分到多张卡上。
全参数微调一张卡放不下时要用多张卡。普通的数据并行在每张卡上保存完整的模型状态,增加卡数并不能减少每张卡的显存。DeepSpeed 的 ZeRO 把模型状态切分到各张卡上,按切分的内容分为三个阶段。以 Qwen2.5-7B 在 8 张卡上训练为例,只算模型状态,不含激活(见[附录 A3](#a3-zero-的显存与-mla-的-kv-cache)):
普通的数据并行在每张卡上保存完整的模型状态,增加卡数并不能减少每张卡的显存。DeepSpeed 的 ZeRO 把模型状态切分到各张卡上,按切分的内容分为三个阶段。以 Qwen2.5-7B 在 8 张卡上训练为例,只算模型状态,不含激活(见[附录 A3](#a3-zero-的显存与-mla-的-kv-cache)):
| 阶段 | 切分的内容 | 每张卡的显存 |
| --- | --- | --- |
@@ -146,7 +175,7 @@ AWQ(Activation-aware Weight Quantization)基于这样的观察:权重的
2. 量化之前把权重的第 j 个输入通道乘以 s<sub>j</sub> = s<sub>X,j</sub><sup>α</sup>,推理时把输入的第 j 个通道除以 s<sub>j</sub>,乘积不变;
3. 重要的通道被放大之后,相对的量化误差变小;在 [0, 1] 中搜索使输出误差最小的 α。
对 Qwen2.5-7B 第 14 层 q_proj 做每 128 个一组的 INT4 量化,这样缩放之后输出的相对误差从 8.98% 降到 7.86%(α = 0.3,见[大模型应用 · 附录 A18](大模型应用.md#a18-权重量化的误差与困惑度))。两者的区别:GPTQ 通过修改未量化的权重来补偿误差;AWQ 只做通道的缩放,不需要反向传播和重建,对校准数据的依赖更小。
对 Qwen2.5-7B 第 14 层 q_proj 做每 128 个一组的 INT4 非对称量化,这样缩放之后输出的相对误差从 8.98% 降到 7.86%(α = 0.3,见[大模型应用 · 附录 A18](大模型应用.md#a18-权重量化的误差与困惑度))。
## 附录:实测
@@ -261,12 +290,51 @@ DeepSeek-V3:61 层,128 个头,c_KV 512 维 + RoPE 键 64 维
论文中的数值为 120 GB、31.4 GB、16.6 GB、1.9 GB,与上面的核对结果一致。
### A4 RoPE 的相对位置与频率
环境:Apple M1 Pro,macOS 15.8.1,Python 3.13.7,NumPy 2.3.3。按 Qwen2.5-7B 的 config.json(每个头 128 维,rope_theta 1000000,max_position_embeddings 32768)实现 RoPE,128 维的 q、k 用随机数生成;三类的划分按 YaRN 论文的 α = 1、β = 32,扩展倍数取模型卡中的 4.0。下面只列出关键部分:
```python
import numpy as np
d, base, L, s = 128, 1e6, 32768, 4.0
theta = base ** (-np.arange(0, d, 2) / d) # 64 组的角速度
def rope(x, pos):
x2 = x.reshape(-1, 2); c, sn = np.cos(pos * theta), np.sin(pos * theta)
return np.stack([x2[:, 0] * c - x2[:, 1] * sn, x2[:, 0] * sn + x2[:, 1] * c], 1).reshape(-1)
wl = 2 * np.pi / theta; r = L / wl # 波长与训练长度内转的圈数
print((r < 1).sum(), (r > 32).sum())
print(0.1 * np.log(s) + 1) # YaRN 对 q、k 的缩放,分数为它的平方
```
输出:
```text
不旋转 q·k = 0.346338
m= 5 n= 2 m-n=3 q_m·k_n = 3.115405
m= 103 n= 100 m-n=3 q_m·k_n = 3.115405
m=10003 n=10000 m-n=3 q_m·k_n = 3.115405
m= 5 n= 4 m-n=1 q_m·k_n = 1.079816
2D 30°: q@1=[0.866 0.5 ] k@0=[1. 0.] 点积=0.866
2D 30°: q@3=[0. 1.] k@2=[0.5 0.866] 点积=0.866
2D 30°: q@3=[0. 1.] k@0=[1. 0.] 点积=0.000
第 0 组 θ=1.0000 波长 6.28;第 63 组 θ=1.241e-06 波长 5063256
训练长度内转不满一圈 (r<1): 24 组;转 32 圈以上 (r>32): 24 组;介于之间: 16 组
最低频一组:位置 32767 转 0.041 rad,位置 131071 转 0.163 rad
PI 把位置除以 4:131071 → 32767.8
YaRN sqrt(1/t)=0.1 ln s + 1 = 1.1386,注意力分数放大 1.2965 倍
```
## 参考资料
- [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
- [FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness](https://arxiv.org/abs/2205.14135)
- [Dao T. FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning. arXiv:2307.08691](https://arxiv.org/abs/2307.08691)
- [NVIDIA A100 Tensor Core GPU](https://www.nvidia.com/en-us/data-center/a100/)
- [Su J, et al. RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864](https://arxiv.org/abs/2104.09864)
- [Chen S, et al. Extending Context Window of Large Language Models via Positional Interpolation. arXiv:2306.15595](https://arxiv.org/abs/2306.15595)
- [Peng B, et al. YaRN: Efficient Context Window Extension of Large Language Models. arXiv:2309.00071](https://arxiv.org/abs/2309.00071)
- [Qwen2.5-7B-Instruct 模型卡](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct)
- [GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints](https://arxiv.org/abs/2305.13245)
- [Qwen2.5-7B-Instruct 模型配置](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct/blob/main/config.json)
- [DeepSeek-AI. DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model. arXiv:2405.04434](https://arxiv.org/abs/2405.04434)