Di's Notes
LLM Basics · 连载中 · 第二篇

Step by Step 读懂 LLM(二):LayerNorm、Self-Attention 与 MLP

目前为止,我们的 input string 经过了 BPE 和 embedding,得到了一个 t×d 的矩阵 X(上一篇里特征维度记作 b,从这篇开始按惯例记作 d)。其中每个横行代表一个 token 的 feature,整个矩阵代表了我们输入的这一段话。

接下来就是 Transformer 的 block,它包括 LayerNorm(近几年的模型多用 RMSNorm)、Self-Attention 和一个两层的 MLP。这几部分是这样连起来的:

一个 Transformer blockpre-norm
图 1点任意一块看它做什么。上方的弧线是残差连接:Attention 和 MLP 的输出都是加回到输入上,而不是替换输入。所以整个 block 进来是 t×d,出去还是 t×d,可以一层层堆下去。

注意这里其实有两个 Norm:一个在 Attention 之前,一个在 MLP 之前。这种“先 Norm 再进子层”的写法叫 pre-norm,是 GPT-2 之后几乎所有 LLM 的选择;原始 Transformer 是把 Norm 放在残差相加之后(post-norm),层数一深就很难训练[8]。

1LayerNorm

Normalization 是 machine learning 中常见的手段,常见的主要有 Batch Normalization[1] 和 Layer Normalization[2]。

Batch Normalization 是针对每一个 feature,把一个 batch 里所有数据点在这个 feature 上的数值放在一起,求均值和方差,再做 normalization。在 CNN 里,feature 就是 channel,“所有数据点”是 batch 里每张图片的每一个像素位置;如果搬到 LLM 里,就是 batch 里每一段 input 的每一个 token,在词向量同一个位置上的 feature value。

BN 在过去的 CNN 模型中非常常见。人们在训练中 apply BN,用当前 batch 的均值方差做归一化,同时累计计算它的 running mean 和 running var;实际使用(推理)时,直接用训练结束时累计出来的这组 mean 和 var,不再依赖推理时的 batch。BN 之所以在 CNN 里好用,有几个前提:

这几点在 LLM 中都不成立:

因此 LLM 更常用的是 Layer Norm:在每个 token 里,对它所有的 feature 求均值和方差做 norm。对一个 token 的向量 :

其中 、 是可学习的缩放和偏移。RMSNorm[3] 更简单,连均值都不减,只除以均方根:

那为什么不在同一个句子里、不同的 token 之间做 norm 呢?“跨 token”有两种做法:

下图里点任意一个格子,看在不同的做法下,哪些数会和它一起算均值和方差。

哪些数放在一起归一化
图 2一个 batch 有三句话,每句是 token(行)× feature(列)。白框是你点的格子,蓝色是和它一起算统计量的数,红色是不该被用到的数:同一句话里排在它后面的 token,以及 padding。

只有 LayerNorm 的统计量完全来自这个 token 自己。跨 token 做 norm 行不通,有这样几个原因:

第一,训练时会偷看未来的 token。上面讲 BN 时那个“做一次加法就能算出答案”的例子,对做法 (a)、(b) 一样成立:做法 (a) 本来就是一句话内部的 BN。只要统计量跨 token,Attention 用 causal mask 小心挡住的未来信息,就会从 norm 这里漏进来。

第二,KV cache 没法做。KV cache 是推理时把前面 token 算好的中间结果(每层的 K 和 V)存起来复用,生成新 token 时就不用重算。可如果统计量跨 token,对话每延续一个 token,均值方差就变了,前面所有 token 归一化之后的值都跟着改,缓存全部作废。

每生成一个 token,前面的结果变不变
图 3柱子是每个 token 第 1 个 feature 归一化之后的值。虚线框是追加之前的值:红色表示这个 token 的结果被新 token 改掉了,蓝色表示没变、缓存可以直接复用。

第三,抹掉有用的信息。做法 (a) 会把每个特征减去它在整句上的平均值。但“整句话共同的东西”,比如话题、语言、文风,往往正是模型需要的信息。减掉之后,每个 token 只剩下“和这句话平均水平的差别”。LayerNorm 去掉的则是一个 token 向量自己的整体偏移和长度,保留的是这个向量的方向,也就是各个特征之间的相对大小,而信息主要就编码在方向里。RMSNorm 更直接:它几乎就是把向量缩放到固定长度 ,即 ,方向完全不变。

第四,一个 token 会影响其他所有 token。真实的 LLM 里,不同 token 的激活值差别很大。一个典型现象是:第一个 token(或者某些标点)在少数几个 feature 上的激活值大得离谱,可以比典型的激活值大上千倍,极端的到 10 万倍(massive activations[5],和 attention sink[6] 现象有关)。在做法 (b) 下,这个 token 会把整句的方差撑得很大,其他 token 归一化后就被压得接近 0。用 LayerNorm 时,每个 token 各自缩放,这个异常 token 只影响它自己。

一个异常大的 tokenmassive activation
图 4六个 token,每个 8 个 feature。第一个 token 有一个 feature 特别大(黄色,超出画面的部分截断)。拖动滑块,看做法 (b) 和 LayerNorm 下其他 5 个 token 变成什么样。

第五,结果依赖句子长度。同一个 token,在 10 个 token 的句子里和在 1000 个 token 的句子里,会被归一化成不同的值(图 3 里每追加一个 token,前面的值都在变,就是这个原因)。变长序列再加上 padding,又会多一层污染。

所以 LayerNorm / RMSNorm 的作用可以总结成一句话:每个 token 只管自己,把自己的向量缩放到稳定的数值范围,不依赖 batch,不依赖句子长度,也不会把别的 token 的信息混进来。token 之间交换信息这件事,全部交给下一步的 Self-Attention。

2Self-Attention

到目前为止,每个 token 的向量只知道它自己:embedding 是按 id 查表,LayerNorm 也是每个 token 各算各的。但理解一句话离不开上下文。想象一下这句话:

The tired cat crossed the street, then it slept.

要理解 it,就得知道它指的是前面的 cat。Self-Attention 做的就是这件事:让每个 token 回头看前面的 token,找到跟自己相关的,把它们的信息混进自己的向量。这是整个 Transformer 里 token 之间唯一交换信息的地方,其他部分(Norm、MLP)都是每个 token 单独算的。

具体怎么做呢?首先,每个 token 的向量分别乘上三个 d×d 的矩阵,得到三个新向量:

和上一篇讲的一样,矩阵乘法 的第 i 行就是 ,所以这一步也是每个 token 各算各的。可以这样理解这三个向量:

然后用 query 和 key 做点积来打分: 越大,token i 就越想看 token j。所有的分数排在一起,正好是一个 t×t 的矩阵 。接下来三步:除以 ,盖上 causal mask,每一行做 softmax 变成权重,最后用这组权重把 value 加权平均:

下图把这个过程一步步拆开。

Self-Attention 一步步
图 5五个 token 的示意数值。行是 query(谁在看),列是 key(被看的是谁)。点左边的 token 名字,换一行看。最后一步右侧是这一行的权重怎样把 v 混合成输出。

几个细节:

多头注意力

上面这个版本有一个限制:每个 token 只有一次 softmax,也就是只有一组权重。但一个 token 往往同时需要好几种信息。还是看 it:它既需要知道“我指的是谁”(cat),也许还需要知道“它当时是什么状态”(tired)。一组权重只能在这两者之间折中,结果是哪个都看不清楚。

多头(multi-head)注意力[7]的做法是:把 d 维切成 H 段,每段 维,叫一个“头”。每个头用自己那一段的 q、k 打分,各做各的 softmax,再用自己的权重去混合自己那一段的 v,最后把 H 段拼回 d 维,乘上 :

下图是一个 4 维、两个头的例子。拖动 it 的 query,比较单头和双头。

单头 vs 双头query 来自 it

单头

4 维一起点积 · 1 次 softmax · ÷√4
权重
输出 = 一组权重混合全部 4 维

双头

各自 2 维点积 · 2 次 softmax · 各 ÷√2
权重
输出 = 每段用自己那组权重
单头的分数,恰好等于两个头的分数之和:
区别只在于加法发生在 softmax 之前还是之后。
图 6数值是手工构造的示意。紫色是头 1(第 1–2 维),绿色是头 2(第 3–4 维);柱子的颜色是 token:cat、street、tired。v 的四维可以理解成[动物性,地点性,疲惫,移动]。两边最后都会乘同一个 WO,这里省略。

可以看到,单头和双头用的是同一组 q、k、v,乘法的次数也完全一样,多头并不增加计算量。唯一的区别是 softmax 做了几次:单头把 4 维的分数加在一起做一次 softmax,两个头想找的东西被摊成一个分布;双头分开做,每个头各自变尖,it 的前两维从 cat 那里拿,后两维从 tired 那里拿。

推广到 32 个头,可以这样理解:每个 value 被切成 32 段,每段用它所属那个头的权重单独加权求和,再拼回去。最后的 把这 32 段重新混合起来,所以“第几段来自第几个头”只是中间状态,不会一直保留到输出。

数学上多头带来了什么

把 按行也切成 H 段,多头注意力可以写成 H 个独立注意力的和:

单头只有一个注意力矩阵 ,所有信息都按同一组权重搬运;多头有 H 个 ,可以同时按 H 种不同的方式看上下文。代价是每个头只在 维的子空间里比较和搬运信息: 和 这两个 d×d 矩阵的秩都不超过 [9]。用表达能力上的一点限制,换来了 H 组互不干扰的注意力分布。

现在的模型还会让几个 query 头共用同一组 K、V(GQA[12],例如 Llama 3 是 32 个 query 头共用 8 组 K、V),这样 KV cache 能小好几倍。这个等讲 decode 的时候再细说。

3MLP

Attention 之后,每个 token 的向量已经混进了上下文的信息。接下来的 MLP 又回到了每个 token 单独计算:同一个两层的全连接网络,作用在每一行上。

第一层把 d 维升到 4d 维,经过激活函数 ,第二层再降回 d 维,加回残差。如果说 Attention 是 token 之间“交流”,MLP 就是每个 token 拿着交流来的信息,自己“想一想”。

一个 token 穿过 MLP
图 7示意大小:d = 4,中间层 4d = 16,权重是随机的。蓝色是正值,红色是负值;右下角是当前的激活函数。换个 token 看中间层哪些神经元被“点亮”。

这里的激活函数,原始 Transformer 用的是 ReLU,GPT-2 换成了更平滑的 GELU。现在的 Llama 等模型用的是 SwiGLU[10]:第一层拆成两个矩阵,一个过 SiLU 激活,一个不过,两者逐元素相乘,起到“门控”的作用:

多了一个矩阵,所以中间层通常从 4d 缩到大约 ,让参数量和原来差不多(Llama 2 7B 里 d = 4096,中间层是 11008)。结构上它仍然是“升维、激活、降维”的两层网络。

MLP 看起来比 Attention 简单得多,但模型的大部分参数都在这里。一层 Attention 有 四个 d×d 矩阵,共 ;一层标准 MLP 是 加 ,共 ,是 Attention 的两倍。

参数都在哪Llama 2 7B
图 8按 Llama 2 7B 的结构计算[11]:d = 4096,32 层,SwiGLU 中间层 11008,词表 32000(输入 embedding 和输出分类器各一个矩阵)。Norm 的参数只有几十万,图上看不见。

MLP 占了将近三分之二的参数,所以有一种很流行的理解:MLP 是模型存放“知识”的地方[13]。第一层 的每一列像一个“模式探测器”,当 token 的向量和它对得上时,对应的神经元被点亮(图 7 里中间层亮起来的那几个);第二层 的对应那一行,就是这个神经元被点亮时要写回 token 向量的内容。这和 Attention 的 key/value 很像,只不过 key 和 value 不是来自上下文,而是训练时学到、固定在权重里的。

也正因为 MLP 参数最多、又是每个 token 单独计算,MoE(Mixture of Experts)模型就是从这里下手:把一个 MLP 换成很多个“专家” MLP,每个 token 只选其中几个来算,参数量大大增加,但每个 token 的计算量不变。

到这里,一个 Transformer block 就走完了:Norm → Attention → 加回残差 → Norm → MLP → 加回残差。输入是 t×d,输出还是 t×d,每一行仍然对应一个 token,只是已经混进了上下文,又经过了 MLP 的加工。真实的模型把这个 block 堆叠几十层(Llama 2 7B 是 32 层)。

未完待续

下一篇走完图 1 之后的部分:

  1. Cross Entropy 分类器:从最后一层的向量到下一个 token 的概率
  2. Prefill 与 Decode:推理时一整句和一个一个 token 的两种算法
  3. KV cache:为什么本篇的 LayerNorm 和 causal mask 让它成为可能

参考

  1. Ioffe, Szegedy. Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift. ICML 2015.
  2. Ba, Kiros, Hinton. Layer Normalization. 2016.
  3. Zhang, Sennrich. Root Mean Square Layer Normalization. NeurIPS 2019.
  4. Shen, Yao, Gholami, Mahoney, Keutzer. PowerNorm: Rethinking Batch Normalization in Transformers. ICML 2020.
  5. Sun, Chen, Kolter, Liu. Massive Activations in Large Language Models. COLM 2024.
  6. Xiao, Tian, Chen, Han, Lewis. Efficient Streaming Language Models with Attention Sinks. ICLR 2024.
  7. Vaswani et al. Attention Is All You Need. NeurIPS 2017.
  8. Xiong et al. On Layer Normalization in the Transformer Architecture. ICML 2020.
  9. Elhage et al. A Mathematical Framework for Transformer Circuits. 2021.
  10. Shazeer. GLU Variants Improve Transformer. 2020.
  11. Touvron et al. Llama 2: Open Foundation and Fine-Tuned Chat Models. 2023.
  12. Ainslie et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. EMNLP 2023.
  13. Geva, Schuster, Berant, Levy. Transformer Feed-Forward Layers Are Key-Value Memories. EMNLP 2021.