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 Norm

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 中都不成立:

同一个句子里的 token 之间做 norm

既然跨句子做 norm 不行,那只在同一个句子里、不同的 token 之间做 norm 呢?这样不依赖 batch,也碰不到别的句子。具体有两种做法:

下图里点任意一个格子,看在不同的做法下,哪些数会和它一起算均值和方差。图里也放上了 LayerNorm 作为对照:它只在每个 token 自己内部做归一化,最后一小节细讲。

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

可以看到,(a) 和 (b) 虽然只在一句话内部,统计量里还是混进了排在后面的 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 只剩下“和这句话平均水平的差别”。

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

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

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

Layer Norm:每个 token 只管自己

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

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

和做法 (a) 减掉“整句话的平均”不同,LayerNorm 去掉的只是一个 token 向量自己的整体偏移和长度,保留的是这个向量的方向,也就是各个特征之间的相对大小,而信息主要就编码在方向里。RMSNorm 更直接:它几乎就是把向量缩放到固定长度 ,即 ,方向完全不变。

回头看前面那些问题:依赖 batch 和 padding、偷看未来、KV cache 失效、一个异常 token 拖累整句、结果依赖句子长度,全都来自“统计量跨了句子或者跨了 token”。LayerNorm 的统计量只来自这个 token 自己,这些问题就都没有了。所以 LayerNorm / RMSNorm 的作用可以总结成一句话:每个 token 只管自己,把自己的向量缩放到稳定的数值范围,不依赖 batch,不依赖句子长度,也不会把别的 token 的信息混进来。token 之间交换信息这件事,全部交给下一步的 Self-Attention。

2Self-Attention

讲 Self-Attention 之前,首先要讲清楚什么是 attention。

在 NLP 领域还在用 RNN 的时候,模型是从左到右一个词一个词地读的。每读到一个词 ,就把它和前面压缩好的 hidden state 合在一起,更新出新的 :

每个位置拿来做预测的,就是这一个固定长度的 ,它由前面所有的词压缩而成。这样哪怕前面有 1000 个词,也会被压成同一个固定长度的 vector 来做预测;而且每读一个新词 都要被更新一次,越早的词被冲淡得越厉害,越近的词影响力越大。对前文信息的压缩非常严重。

于是后来人们开始想:是不是除了压缩完的 ,也可以让模型直接回头看看前文每个位置的内容,再进行处理?这就是最开始的 attention 机制[14]。它最早用在机器翻译上:一个 RNN(encoder)先读完原句,另一个 RNN(decoder)再一个词一个词地生成译文,生成每个译文词的时候,都回头看一遍原句的每一个位置。具体分三步:

  1. 每个位置的内容。首先,每个词都有一个自己输出的 hidden state :它从这个词本身的词向量出发,结合前文的 hidden state,经过这一步的 RNN 单元处理之后得到。可以把它看作这个位置的结果向量,也是之后每一步要回头看的“前文内容”。(原论文的 encoder 从左往右、从右往左各读一遍,所以 里有这个词两边的上下文。)
  2. 打分。接下来需要决定一个权重,也就是给每个词多少 attention。当时的做法是一个小 MLP:输入是当前这一步的向量(decoder 上一步的 hidden state )和原句某个词的向量 ,各乘一个矩阵后相加,过一个 tanh,再和一个向量 点乘,变成一个一维的数值: 这个过程是当前这一步和原句每个词都做一遍,所以每个词都得到一个“分数”。
  3. softmax 加权。紧接着对所有这些分数做 softmax,得到一组加起来等于 1 的权重;然后用这组权重把每个词的向量加起来,得到一个加权向量 ,交给 decoder 去生成这一步的词:

举个例子,把 the cat on the mat 翻译成“垫子上的猫”。中文和英文的语序正好是反的:生成“垫子”的时候,权重应该集中在 mat 上;到了生成“猫”,又要跳回前面的 cat。

最早的 attentionBahdanau 2014
图 5分数是示意。上方是原句和每个位置的 hidden state h,下方是正在生成的译文。点上面的译文词换一步,或者用“播放”把四个词连起来看:权重从 mat 跳到 on,再跳回 cat。

把每一步的权重排成一张表,就是原论文里那张有名的“对齐图”:哪个译文词对应哪个原文词,模型自己学了出来,没有人教过它。而且这是“软”的对齐,用 softmax 加权而不是硬选一个位置,所以整个过程可导,可以和翻译模型一起端到端训练。

这个算法后来被延伸成了今天 QKV 版本的 self-attention,逻辑其实还是一样的:

  1. 前文词的 value:作为加权的基础值;
  2. attention score:作为加权的权重。

还是一样的 pattern,只不过计算方法做了升级。主要升级了三处。

第一,从“译文看原文”变成“一句话看自己”。Self-attention 里没有两句话,每个 token 回头看的是同一句话里它前面的 token(包括它自己)。为什么需要这一步?到目前为止,每个 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 拆成 Q、K、V 三个向量。还是按上面的逻辑来想:value 是一定需要的,它是每个位置交出去的内容;然后我们需要一组给所有 value 的权重,这个权重取决于当前词本身,以及其他每个词的特征。所以每个 token 的向量分别乘上三个 d×d 的矩阵,得到三个新向量:

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

那为什么要三个?K 和 V 都长在被看的那个词身上,能不能用同一个?或者干脆 QKV 都用同一个?

第三,打分从 MLP 换成点积。 越大,token i 就越想看 token j。点积打分在 2015 年就有人用了[15],它最大的好处是所有 (i, j) 的分数可以一次写成一个矩阵乘法 ,正好是一个 t×t 的矩阵,GPU 算起来极快;而 MLP 打分每一对都要单独过一次 tanh。点积的数值会随维度变大,所以要先除以 。接下来和最早的 attention 一样:盖上 causal mask,每一行做 softmax 变成权重,再用这组权重把 value 加权平均:

和最早的 attention 对照一下:

最早的 attention(2014)Self-attention(2017)
谁看谁译文看原文一句话看自己(只看前面)
当前位置decoder 的 query
被找的依据(打分时乘 )key
交出的内容value
打分
加权softmax,softmax,

先只看一个词。下图里,it 向前文的每个词(包括它自己)连一条线,线上标出每一步的计算:先用 q·k 打分,再除以 、做 softmax 变成权重,最后用权重把每个词的 v 加起来。

一个词怎样和前文交互
图 6示意数值,d = 4。点任意一个词,换成它来看前文(试试 cat 和 crossed);它后面的词是未来,看不到。小格子是每个词的 q、k、v,蓝色为正,越亮越大。线越粗,权重越大。

每个词都在做同样的事。把所有词的这一行分数叠在一起,就是一个 t×t 的矩阵 :第 i 行就是第 i 个词对前文的打分,右上角是“未来”,被 causal mask 挡住。下图用矩阵的视角把整个过程再走一遍。

所有词一起算:矩阵形式
图 7五个 token 的示意数值。行是 query(谁在看),列是 key(被看的是谁)。点左边的 token 名字,换一行看。最后一步右侧是这一行的权重怎样把 v 混合成输出。

几个细节:

为什么要多头

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

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

多头到底改变了什么?下图跳过打分,直接看最后一步“用权重把 v 加起来”。把 cat 和 tired 交出的 v 写成一个矩阵:每一行是一个词的 v,每一列是一个维度。为了好看清,假设前两维存的是“是谁”,后两维存的是“什么状态”。格子越亮,说明它最后进入输出的越多。

单头 vs 双头:输出向量从哪来
图 8示意。左:每个格子 = 权重 × v,每一列加起来就是输出向量。右:横轴是输出里有多少 cat 的信息,纵轴是有多少 tired 的信息;蓝色是这种结构能到达的范围,★ 是 it 想要的“两样都要”。真实模型里每段维度不会这么整齐地对应一种信息,最后还要乘 WO 混合,但“一组权重只能折中”这一点不变。

单头的输出是 v 的整行加权平均:权重是一行一个数,cat 那一行和 tired 那一行都只能整行按同一个比例进来。所以输出只能落在右图那条斜线上:多拿一点 cat,就必然少拿一点 tired。it 两样都想要,最好也只能各拿一半。

多头把列切开,每一段维度有自己的一组权重。头 1 管前两维,可以把权重全给 cat;头 2 管后两维,可以全给 tired。输出就变成“前一段完整地来自 cat,后一段完整地来自 tired”,右上角那颗星终于能到达了。

所以多头并不增加计算量(乘法次数完全一样),它改变的是输出能长成什么样:从“几个 v 的整行平均”,变成“每一段维度可以各自从不同的词那里拿”。反过来,如果两个头想找的是同一个词,或者 it 本来只需要一种信息,切不切开就没什么区别。

推广到 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
图 9示意大小: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
图 10按 Llama 2 7B 的结构计算[11]:d = 4096,32 层,SwiGLU 中间层 11008,词表 32000(输入 embedding 和输出分类器各一个矩阵)。Norm 的参数只有几十万,图上看不见。

MLP 占了将近三分之二的参数,所以有一种很流行的理解:MLP 是模型存放“知识”的地方[13]。第一层 的每一列像一个“模式探测器”,当 token 的向量和它对得上时,对应的神经元被点亮(图 9 里中间层亮起来的那几个);第二层 的对应那一行,就是这个神经元被点亮时要写回 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.
  14. Bahdanau, Cho, Bengio. Neural Machine Translation by Jointly Learning to Align and Translate. ICLR 2015.
  15. Luong, Pham, Manning. Effective Approaches to Attention-based Neural Machine Translation. EMNLP 2015.
  16. Kitaev, Kaiser, Levskaya. Reformer: The Efficient Transformer. ICLR 2020.