LLM-Code-Handwritten
本文主要包含 LLM 代码手撕的实现,包括 RoPE、Attention(MHA、MQA)、Decoder-only、Encoder-only、Transformer 等。
1. RoPE (Rotary Positional Embedding)
核心思想:通过旋转矩阵将绝对位置信息注入到 Q 和 K 中,且具有相对位置特性。
面试重点:复数乘法的实现方式(两两配对旋转)。
为了将位置信息注入模型中,我们将实现 旋转位置编码(RoPE)(Su 等,2021)。
对于一个 token 的 query 向量 $q^{(i)}$,它位于序列中的第 $i$ 个位置,维度为 $d$。
我们会对它施加一个成对旋转矩阵 $R^i$,得到:$q’^{(i)} = R^i q^{(i)} = R^i W_q x^{(i)}$
也就是说,矩阵 $R^i$ 会将 query 向量的每两个元素看作一个 二维向量,并按角度 $\theta_{i,k}$ 进行旋转,其中:$\theta_{i,k} = \frac{i}{\Theta^{(2k-2)/d}} \quad (k = 1, \ldots, d/2)$
$\Theta$ 是一个常数(一般取 10000,与 Transformer 的位置编码一致)。
因此,矩阵 $R^i$ 可以看作一个 分块对角矩阵,每个 2×2 小块为:
$$
R^i_k= \begin{bmatrix} \cos(\theta_{i,k}) & -\sin(\theta_{i,k}) \\
\sin(\theta_{i,k}) & \cos(\theta_{i,k}) \end{bmatrix}
$$
所以完整的旋转矩阵 $R^i$ 是:
$$
R^i= \begin{bmatrix}
R^i_1 & 0 & 0 & … & 0 \\
0 & R^i_2 & 0 & … & 0 \\
0 & 0 & R^i_3 & … & 0 \\
… & … & … & … & … \\
0 & 0 & 0 & … & R^i_{d/2}
\end{bmatrix}
$$
其中所有的 $0$ 都代表 2×2 的零矩阵。
虽然我们可以显式构造整个 $d \times d$ 的旋转矩阵,但更高效的做法是利用其特性直接对向量进行旋转。
而且,由于我们只关心一个序列内部 token 的相对旋转关系,所以所有层都可以共享同一套 cos 和 sin 表。 因此,这一层通常用 self.register_buffer(persistent=False) 来保存 cos 和 sin,而不会作为可训练参数。
最终,Q 和 K 都会用对应的$R^i$ 进行旋转。 注意:这个层 没有可学习参数
- 直接实现
1 | import torch |
- 复数实现
1 | import torch |
2. Attention
1 | import torch |
MHA
1 | class MultiHeadAttention(nn.Module): |
MQA
1 | import torch |
GQA
1 | import torch |
MLA
Code
1 | import torch |
Explanation
Generated By Gemini-3-Pro
MLA(Multi-Head Latent Attention)不仅仅是一个代码技巧,它是 DeepSeek-V2/V3 能够以极低的显存占用(KV Cache)实现媲美 LLaMA3 MHA(多头注意力)性能的核心秘密。
要真正理解 MLA,我们需要解决两个主要矛盾:
性能矛盾:想效果好,就得用 MHA(多头),但显存爆炸。
显存矛盾:想省显存,就得用 MQA/GQA(少头),但模型表达能力可能下降。
MLA 的核心原理就是:用“矩阵分解”的数学技巧,在“显存”上模拟 MQA,在“计算”上模拟 MHA。
下面分三个关键模块进行详细拆解:
第一模块:低秩键值联合压缩 (Low-Rank Key-Value Joint Compression)
这是 MLA 的地基。
在传统的 Standard MHA 中,通过线性层把输入 $x$ 映射为 $K$ 和 $V$:
$$\mathbf{k} = x W_K, \quad \mathbf{v} = x W_V$$
假设 hidden_dim 是 4096,batch_size 和 seq_len 很大,生成的 $K$ 和 $V$ 矩阵非常巨大,都要存进显存(KV Cache)。
MLA 的思路是:$K$ 和 $V$ 矩阵其实有很多冗余信息(低秩性)。为什么不先把输入压缩成一个很小的“潜在向量”(Latent Vector),存 KV Cache 时只存这个小向量,计算时再还原回去呢?
Down-Projection (压缩):
输入 $x$ 先经过一个压缩矩阵 $W_{DKV}$,变成一个低维向量 $c_{KV}$(Latent Vector)。
$$c_{KV} = x W_{DKV}$$
- 关键点:推理时,KV Cache 只存这个 $c_{KV}$。它的维度(比如 512)远小于标准 KV 的维度(比如 num_heads 128 * head_dim 128 = 16384)。这使得显存占用极低。
Up-Projection (还原):
在计算 Attention Score 时,通过两个上投影矩阵 $W_{UK}$ 和 $W_{UV}$,把 $c_{KV}$ 还原成多头的形式。
$$\mathbf{k} = c_{KV} W_{UK}, \quad \mathbf{v} = c_{KV} W_{UV}$$
此时的问题:如果仅仅是这样,计算量并没有减少,推理时还得实时把 $c_{KV}$ 乘回大矩阵再算 Attention,效率不高。于是有了下面的“矩阵吸收”。
第二模块:推理时的矩阵吸收 (Matrix Absorption)
这是 MLA 最骚的操作。利用矩阵乘法的结合律,我们不需要在推理时真正把 $K$ 还原出来。
回顾 Attention 的核心公式(忽略 Scale 和 Softmax):
$$Score = Q \cdot K^T$$
在 MLA 中,$K$ 是由 $c_{KV}$ 还原来的,代入公式:
$$Score = Q \cdot (c_{KV} \cdot W_{UK})^T = Q \cdot W_{UK}^T \cdot c_{KV}^T$$
结合律魔法:
我们可以把 $(Q \cdot W_{UK}^T)$ 这一项结合在一起!
$$Score = \underbrace{(Q \cdot W_{UK}^T)}{\text{Absorbed Query}} \cdot c{KV}^T$$
这一步意味着什么?
在推理时,我们计算出 Query 后,直接把 $W_{UK}^T$(Key 的上投影矩阵)吸收(融合) 进 Query 里。
这就变成了:一个变形后的 Query,直接去和 KV Cache 里存储的那个极小的 $c_{KV}$ 做点积。
结果:我们完全不需要在显存里复原那个巨大的 Key 矩阵。
这也是为什么 MLA 被称为“伪装成 MQA 的 MHA” —— 它的存储量像 MQA 一样小,但它的权重矩阵 $W_{UK}$ 依然保留了多头的信息。
第三模块:解耦旋转位置编码 (Decoupled RoPE)
既然上面的“矩阵吸收”这么完美,为什么代码里还要把 RoPE 单独拆出来(peeled off)?
因为 RoPE 破坏了矩阵结合律。
RoPE 是对位置 $m$ 和 $n$ 进行旋转变换 $\mathcal{R}$。如果直接对压缩后的向量 $c_{KV}$ 或者还原后的 $K$ 加 RoPE,公式会变成:
$$Score = Q \cdot \text{RoPE}(c_{KV} \cdot W_{UK})^T$$
这里的 $\text{RoPE}$ 操作是非线性的(相对于矩阵乘法顺序而言),它夹在中间,导致 $W_{UK}$ 无法被 $Q$ 吸收。
DeepSeek 的解决方案:切分(Decouple)
它把 Query 和 Key 的向量拆成了两部分:
Content Part (内容向量):承载语义信息,不加 RoPE。这部分可以使用上面的“矩阵吸收”技巧,极致压缩。
RoPE Part (位置向量):承载位置信息,必须加 RoPE。
具体做法:
生成的 Key ($k_{total}$) = [$k_{content}$, $k_{rope}$]
$k_{content}$ 来自压缩的 $c_{KV}$(走矩阵吸收逻辑)。
$k_{rope}$ 单独生成(通常头数很少,甚至像代码里那样共享头,即 head_dim=1,然后广播),只用来携带位置信息。
Query 同理拆分。
计算 Attention Score 时,利用数学性质:
$$Q^T K = (q_{content}^T k_{content}) + (q_{rope}^T k_{rope})$$
这意味着:我们算一部分“内容分”,算一部分“位置分”,加起来就是总分。
内容分:享受极致显存压缩。
位置分:虽然不压缩,但维度很小(如代码中的 64 维),且共享头,显存占用忽略不计。
总结:MLA 的本质图景
做一个简单的对比:
| 特性 | Standard MHA | GQA (LLaMA-2/3) | DeepSeek MLA |
|---|---|---|---|
| KV Cache 存什么 | 存完整的 num_heads $\times$ d_head | 存较少的 num_kv_heads $\times$ d_head | 只存 1 个 latent 向量 + 少量 RoPE 向量 |
| 显存占用 | 巨大 (100%) | 中等 (比如 1/8) | 极小 (比 GQA 还小) |
| 模型容量 | 强 (每个头都有独立参数) | 弱 (KV 头参数减少了) | 强 (投影矩阵 $W_{UK}$ 依然是多头的) |
| 推理技巧 | 无 | 无 | 矩阵吸收 (Absorbed Key) |
| 位置编码 | 直接作用于 K | 直接作用于 K | Decoupled (分离内容与位置) |
一句话概括 MLA 的原理:
通过低秩压缩减少 KV 存储,利用矩阵吸收在推理时跳过解压步骤,最后通过解耦 RoPE 绕过位置编码对矩阵结合律的破坏,从而实现了“用 MQA 的显存,跑出 MHA 的性能”。
3. Utils
Softmax
1 | import torch |
AdamW
1 | import math |
LR
1 | import math |
RMSNorm
1 | """ |
LLM-Code-Handwritten