从 2017 年《Attention Is All You Need》公开算起,Transformer 架构已经走过了七年多。这七年里,NLP 从 RNN 统治变成了 Transformer 一统天下,CV 从 CNN 一统天下变成了 ViT 分庭抗礼,多模态、推荐系统、甚至蛋白质结构预测都开始向 Transformer 靠拢。很多人年初还在看 GPT-3 的论文,年尾就在调 LLaMA 的权重了。但说实话,大多数人对 Transformer 的理解停在"有 QKV、有残差、有 LayerNorm、FFN 两层全连接"这个层面,一旦要自己设计模型、改结构、做长序列适配,就会被一堆变体搞得头晕:MQA、GQA、RoPE、ALiBi、SwiGLU、RMSNorm、FlashAttention、MoE……这些到底是怎么一步步演化来的?它们各自解决了什么问题?有没有适用范围和代价?
这篇文章我打算从算法演进的角度,把 Transformer 核心模块的来龙去脉捋一遍。不堆公式,尽量用大白话讲清楚"为什么当年要这么设计""后来为什么又改了""改完带来什么收益、牺牲了什么"。适合正在读论文但读不太懂动机的人,也适合想动手改模型结构、做工程优化的人。我自己在几个实际项目里把这些演进逐一验证过,文章最后也会讲一些选型上的实操体会。
1. 从 2017 年的原始 Transformer 说起:为什么骨架长这样
1.1 QKV 机制的直觉:一批人查数据库
原始 Transformer 里最核心的算法就是缩放点积注意力。很多人第一次看到 Attention 公式时被 Q、K、V 三个矩阵绕晕了。我用一个接地气的类比来解释它到底在干什么:它像是公司里的一批人在查数据库。
你要查一份文件,先要有一个查询词,就是 Q(Query)。数据库里每个文件都有两个属性:一个是索引标签,就是 K(Key),一个是文件实际内容,就是 V(Value)。系统会把你的查询词和所有文件的索引做匹配,算出相关度,然后把相关度转成权重,最后按权重把文件内容加权求和,得到最终结果。注意力机制里"输出 = Softmax(QK^T / √d_k) V"这句话,翻译成人话就是:每个 token 作为查询者,去和其他所有 token 的索引做点积,得到"我和你的关系有多近",再拿这个关系度去加权提取对方的特征。
为什么叫"缩放点积"?因为点积结果的数值会随着向量维度 d_k 的增大而变大。假设两个向量都是均值为 0、方差为 1 的随机向量,它们的点积方差就是 d_k。d_k 越大,点积分数波动越大,Softmax 之后就越容易产生梯度消失——一项独大,其他项的梯度趋近于零。除以 √d_k,本质上是把方差拉回 1 附近,让 Softmax 落在一个梯度敏感的区间里。这是一个很小但关系到模型能不能训得动的细节。
1.2 多头注意力为什么要"多头"
原始 Transformer 没有只做一个 Attention,而是把 Q、K、V 投影到 h 个子空间里,各自做一次注意力,再把结果拼接起来。为什么?你可以理解为:一个头只能关注一种关系。一个头学会关注"动词和宾语"的依赖,另一个头学会关注"代词和先行词"的指代关系,还有一个头专门盯句法位置。多个头并行,相当于模型能同时从不同关系维度观察句子。
实际观察中发现,有些头确实有明显的语义分工,但也有一些头是冗余的。后来的研究甚至发现剪掉一半的头,性能下降也不大。所以在理解了多头动机之后,再去看到处都在做"减少头数、共享 KV"的改动,就不会觉得奇怪了——多头中的冗余本来就是后续算法优化的空间之一。
1.3 残差连接和 LayerNorm 的位置不是随便定的
原始 Transformer 给每个子层外面都套了一层"残差 + LayerNorm",结构是 Post-LN,也就是 Add 之后再做 Norm。残差连接的动机非常朴素:网络越深,梯度越难传。有了恒等映射的旁路,深层模型至少不会比浅层模型差。这个思想不是 Transformer 首创,但 Transformer 把它用得极其彻底——整个 Encoder 和 Decoder 都是在一个个残差块上堆出来的。
LayerNorm 的位置却大有讲究。Post-LN 在早期 Transformer 里训练很不稳定,稍微调大学习率就炸。后来大家发现把它移到残差分支的起点,也就是 Pre-LN 结构,训练会稳得多。这个细节我在第 4 章会单独展开,因为它是后来几乎所有大规模模型架构调整的第一步。
FFN 的设计同样容易被忽略。每个 Transformer Block 里 Attention 之后,还跟着一个"先升维再降维"的两层 MLP,标准实现是可选的隐藏层,通常是 4 倍 d_model——GPT-3 是 12288 对 3072,LLaMA 更大。它的作用是给 Attention 之后的表征做非线性变换和通道间的信息混合。有研究者把这种结构称为 token-mixer 和 channel-mixer 的交替,这个视角对理解后面 ViT、Swin 的演进非常关键。
2. Attention 的军备竞赛:从 O(n²) 到硬件感知
2.1 平方复杂度的痛苦
原始 Attention 的时间复杂度和空间复杂度都是 O(n²),其中 n 是序列长度。在 512 token 这个量级,问题不大。但一旦进入长文本、长音频、长视频场景,序列长度到几万甚至几十万,平方复杂度就是灾难。
举个具体数字:序列长度 n = 65536,注意力矩阵就是 65536 × 65536 的 float16 矩阵,占 8GB 显存。这还只是一层一个头,模型还有几十层和几十个头。所以想靠堆显存硬算长序列,基本不现实。
业内解决这个问题的路线分两类:一类是"降低计算量"的算法层改进,另一类是"减少显存浪费"的工程层改进。两条线都很重要。
2.2 稀疏注意力:固定窗口加全局哨兵
第一类改进思路是把注意力矩阵从"每个 token 看所有 token"改成"每个 token 看部分 token"。2019 年之后出现了大量稀疏注意力方案,比如固定窗口注意力、步长跳线注意力、全局 token 注意力。Longformer 和 BigBird 是这类的代表。
Longformer 的做法:每个 token 只看左右两边各若干个 token(滑动窗口),同时隔一段距离放一个全局 token,这个全局 token 能看整个序列。这样复杂度从 O(n²) 降到了 O(n × w),w 是窗口大小,通常几百。BigBird 更进一步,结合了随机注意力线。
稀疏注意力在实际任务里效果相当不错,但是有一个工程痛点:注意力矩阵变成稀疏结构后,GPU 上的稠密矩阵乘法和 Softmax 优化就不太好直接用了,很多实现依赖自定义 CUDA kernel。所以后来很多项目宁愿用别的方案,也不愿意轻易上稀疏注意力。
2.3 线性注意力:把 Softmax 换成核函数
另一条路线是从数学上改变注意力形式。注意力的核心是 Softmax(QK^T/√d_k),Softmax 本身就引入了序列长度的耦合——每一项都要除以所有项的和。如果定义一个不需要全局归一化的相似度函数,比如直接用带核函数的点积,那么注意力可以写成 φ(Q)φ(K)^T V 的形式,运算顺序一变,就能先算 KV 相乘,再算 Q 相乘,把复杂度降到 O(n)。
Performer 用的随机特征映射就是这样的思路,用随机投影近似 Softmax 核。Linear Transformer 也有类似做法。理论上很漂亮,我也在文本摘要任务上试过,训练速度确实快,但效果普遍比标准 Softmax Attention 掉点,尤其在需要长距离精确依赖的任务上。原因也容易理解:Softmax 的指数归一化本身就是一种强稀疏化操作,改成核函数后每个 token 都会"均匀"地看到所有 token,注意力分布变糊了。
所以线性注意力后来没有成为长文本的主流,反而是 FlashAttention 这种不改算法、只改 IO 的方案在实践中大放异彩。
2.4 FlashAttention:让 GPU 的 SRAM 忙起来
FlashAttention 的出现改变了整个长序列训练的格局。它的核心洞察是:GPU 计算很快,但显存带宽慢;注意力的瓶颈不在 FLOPS,而在反复读写显存。
标准注意力流程是:从 HBM(高带宽显存)读取 Q、K、V → 计算 S = QK^T 写回 HBM → 从 HBM 读 S 算 Softmax → 把 P 写回 HBM → 再读 P 和 V 算输出。中间结果 S 和 P 都被反复存取,浪费大量带宽。FlashAttention 的思路是分块计算,把 S 和 P 留在 SRAM(片上高速缓存)里,利用 Softmax 的在线累加技巧,不把中间矩阵写回 HBM。这样显存占用大幅下降,速度还变快了。
FlashAttention 不是改变 Attention 的数学形式,它算出的结果和标准 Attention 几乎一样。这一点很关键:它让工程师可以在不做算法妥协的前提下,把序列长度从 2048 拉到 8192、16384。现在主流大模型训练基本都离不开它。
2.5 MQA 和 GQA:共享 KV 的工程智慧
当序列变长、推理并发变高之后,又一个矛盾出现了:KV Cache 占显存太多。Transformer 推理时,每个 token 的 K 和 V 都要缓存起来给后续 token 做注意力计算。长序列下,KV Cache 动不动就是几百 MB 甚至几个 GB。
MQA(Multi-Query Attention)的解决办法是让所有头共享同一份 K 和 V,每个头只保留自己的 Q。这样 KV Cache 直接除以头数。PaLM 和 Falcon 等模型用了这个方案,推理内存压力显著下降,代价是模型质量略微下跌。
GQA(Grouped-Query Attention)则是折中方案:把若干个头分成一组,每组共享一份 K 和 V。LLaMA 2 从 70B 开始用 GQA,LLaMA 3 全线用 GQA,实践中质量和 MQA 相比更接近标准多头,速度又比标准多头快得多。
这里有个实用的结论:如果你现在要做推理优化,优先考虑上 GQA;如果要从零训练一个大模型,GQA 几乎是标配选择。
不同注意力改版的对比,我整理了一个表格:
| 方案 | 核心思路 | 复杂度 | 质量影响 | 主要成本 |
|---|---|---|---|---|
| 标准 Attention | 全量 Softmax | O(n²) | 基准 | 显存和速度瓶颈 |
| 稀疏注意力 | 窗口+全局 | O(nw) | 略降 | 实现复杂,依赖 custom kernel |
| 线性注意力 | 核函数近似 | O(n) | 明显下降 | 注意力分布变糊 |
| FlashAttention | IO 优化,分块+在线 Softmax | O(n²)但低常数 | 无损 | 需要 GPU 支持,FMA 指令 |
| MQA | 全头共享 KV | 复杂度不变 | 略降 | 质量损失 |
| GQA | 分组共享 KV | 复杂度不变 | 接近标准 | 分组数需调参 |
3. 位置编码的进化:从三角函数到旋转位置编码
3.1 绝对位置编码的先天短板
Transformer 本身没有顺序概念,它对 token 的处理是"袋中词"。所以必须额外加位置信息。原始 Transformer 用正弦/余弦函数生成绝对位置编码,BERT 则用可学习的绝对位置编码。这两种都算绝对位置编码,特点是:每个位置映射到一个固定向量,位置向量的含义不随输入内容变化。
绝对位置编码的短板有两个:一是截断外推——训练时的最大位置是 512 或 1024,推理时超过这个长度要么截断,要么位置编码分布漂移,效果暴跌;二是位置信息和 token 信息是相加关系,模型需要额外学习如何从混合向量里解耦出位置语义。
3.2 相对位置编码:让模型学距离
相对位置编码的思路是:位置信息本身并不需要绝对坐标,只需要知道"当前 attention 的目标 token 相对我是左移了 2 格还是右移了 5 格"。T5 用了一个简化版:把相对距离分桶,映射到可学习的 embedding。Transformer-XL 也提出了相对位置编码方案。
相对位置编码的好处是泛化性明显增强:模型学的是"位置之间的距离关系",而不是"位置 100 的绝对含义",外推能力比绝对位置编码好一些。缺点是实现复杂度高,工程上要和注意力分数的计算耦合到一起,不是简单地加到 embedding 上。
3.3 RoPE:让位置信息"旋转"起来
RoPE(Rotary Position Embedding)是目前大模型中最主流的位置编码方案,GPT-NeoX、LLaMA、Qwen 系列都在用。它的思想比较巧妙:把位置信息编码成多维向量的旋转角度,让 Q 和 K 在做内积时自动携带相对位置关系。
具体做法是,把 Q 和 K 向量按维度两两分组,对每组施加一个随位置变化的旋转。旋转后的向量做点积,结果中会出现两个位置角度的差——也就是说,注意力分数天然依赖于两个 token 之间的相对距离。RoPE 最吸引人的地方是它把绝对位置编码的简单实现和相对位置编码的外推优势结合了起来,不需要改变 Attention 计算逻辑,只需要在 Q、K 上多乘一个旋转矩阵。
RoPE 还衍生出了外推技巧,比如 NTK-Aware Scaled RoPE 和 YaRN。核心做法是调整旋转频率,让模型在超过训练长度时依然能保持注意力分数的合理分布。我自己在长文本续写场景测试过,把 LLaMA 模型从 2K 长度扩展到 8K,配合 YaRN 微调,效果比直接线性插值稳定不少。
3.4 ALiBi:更朴素的位置偏见
ALiBi 做了一件比 RoPE 还简单的事:不加位置向量,而是在注意力分数上直接减去一个和相对距离成正比的惩罚项。离得越远的 token,注意力分数越低。这个惩罚项是预先定义好的斜率,不需要学习。
ALiBi 在推理时完全不需要维护位置编码表,内存友好,且外推性不错。BLOOM 等模型采用了它。但它的问题是硬编码的距离惩罚限制了模型对长距离依赖的灵活学习——如果某个任务真的需要远距离的强关联,统一的距离惩罚反而会成为阻碍。
从我这几个项目的体感来看,如果做大规模预训练,RoPE 是最优先的选择;如果做短文本小模型且不想额外引入位置编码的复杂度,ALiBi 是个很省心的备选。
4. 隐藏层的精细活:归一化位置与 FFN 结构的分岔路
4.1 Post-LN 到 Pre-LN:一次稳定性的胜利
原始 Transformer 把 LayerNorm 放在残差加法之后,这被称为 Post-LN。它的理论好处是更符合残差网络的经典设定,但实际训练起来梯度容易暴涨,需要精细的 warmup 和低学习率。
后来 GPT 系列等模型改成了 Pre-LN,也就是每个子层先做 LayerNorm,再做 Attention 或 FFN,最后和残差连接相加。Pre-LN 的优点是梯度通过残差捷径传播得更干净,训练更稳定,对大学习率更耐受,因此成为后续大模型的主流选择。
代价也不是没有:Pre-LN 在某些任务上的最终精度通常略低于 Post-LN。换句话说,Post-LN 的天花板更高,但通向天花板的路径很陡峭;Pre-LN 的天花板略低,但到达的路径非常平稳。考虑到大规模训练的种种不确定性,大家普遍选择后者。
4.2 RMSNorm:去掉均值中心化
LayerNorm 做了两件事:减去均值(中心化),除以标准差(缩放)。RMSNorm 的研究者发现,减去均值这一步骤可以省掉,只用 RMS(均方根)做缩放就够了。它的计算量比 LayerNorm 少一层偏移操作,在硬件上有更高效实现,而且实验效果和 LayerNorm 基本持平。
LLaMA 系列全线使用 RMSNorm,口号就是"不下滑效果,还能省一点算力"。在大模型里,每一层都省一点,累积起来就很可观。
4.3 FFN 的 GLU 化:从 ReLU 到 SwiGLU 的涨点之路
原始 Transformer 的 FFN 是先线性+ReLU+再线性。后来 BERT 用了 GeLU,GPT-2 沿用 GeLU,然后研究者发现 GLU(门控线性单元)家族的表现力更强,其中 SwiGLU 尤为突出——它把输入分成两个分支,一个分支过 Swish 激活函数,另一个分支保持线性,两个分支逐元素相乘。
这个结构把 FFN 变成了"门控版"FFN。SwiGLU 在论文里效果比普通 ReLU FFN 高出不少,LLaMA 系列全部采用了 SwiGLU 变体,Qwen、Mistral 等也跟进。代价是参数量变大了:SwiGLU 的中间层矩阵实际上变成三份,参数量大约是普通 FFN 的 1.5 倍,所以 LLaMA 把隐藏层维度从 4 倍 d_model 降到 8/3 倍左右,维持总参数不增。
这里有一个选型经验:预训练大模型建议直接用 SwiGLU + 降维,这在当前社区几乎成了标配;如果只是做下游微调,直接加载现成模型就行,暂时不需要自己重改 FFN。
4.4 归一化与 FFN 演进对比
我把这几项变动汇总一下:
| 组件 | 原始设计 | 演进设计 | 动机 |
|---|---|---|---|
| 归一化位置 | Post-LN | Pre-LN | 训练稳定性 |
| 归一化方式 | LayerNorm | RMSNorm | 减少计算、保持效果 |
| FFN 激活 | ReLU | GeLU / SwiGLU | 更好的梯度流动和表现力 |
| FFN 结构 | 升维再降维 | 门控线性单元 | 更强的非线性表达 |
| 隐藏层比例 | 4:1 | 8/3:1(配合 GLU) | 控制参数量 |
5. 架构形态的收敛与发散:Encoder、Decoder、ViT 与 MoE
5.1 三种基础形态的分工演化
原始 Transformer 自带 Encoder-Decoder 完整结构,机器翻译是它的主场景,Encoder 读完整句子,Decoder 自回归生成译文。BERT 砍掉 Decoder 只留 Encoder,用双向上下文做理解型任务;GPT 砍掉 Encoder 只留 Decoder,用自回归方式做生成型任务。
后来的演进很有意思:理解型任务逐步被生成型任务吸收,"把一切变成生成"成了趋势。T5 还保留着 Encoder-Decoder,但到了 GPT-3、ChatGPT 时代,Decoder-only 成了大模型的主流选择。为什么?工程上,Decoder-only 可以把预训练和下游任务统一成"下一个词预测",推理更方便;效果上,研究者发现足够大的 Decoder-only 模型在理解任务上一点也不输 Encoder 模型。虽然这种统一代价是不对称,但社区已经用规模填平了差异。
5.2 ViT:Transformer 跨界视觉的关键改动
ViT 把图像切成固定大小的 patch,每个 patch 拉平成向量,再套用标准 Transformer Encoder。这个改动看似粗暴,效果却很好——在足够大的数据集上,ViT 的表现超过了当时的 CNN。
ViT 给架构演进带来的启发是:Transformer 本身是个通用计算范式,只要能把数据变成 token 序列,它就能工作。这个思路直接启发了后面的多模态模型:文本 token、图像 patch、音频帧,统统拼成序列塞进同一个 Transformer。
5.3 Swin Transformer:层级化和窗口注意力的视觉化改造
ViT 的问题在于 patch 大小固定,没有层级,对视觉任务的高分辨率多尺度检测不利。Swin Transformer 引入了类似 CNN 的金字塔结构:浅层 token 多,深层 token 少,同时把注意力限制在窗口内,窗口之间通过 shift 操作实现跨窗口交流。
Swin 的成功表明,架构演进不只是"堆模块",有时还需要针对输入数据的特性做结构性改造。它在检测、分割这类稠密预测任务上优势明显,后来很多视觉骨干网络都以它为基底。
5.4 MoE:稀疏激活带来的参数爆炸式增长
Mixture of Experts(MoE)源自更早的门控网络思想,和 Transformer 结合后,在 Switch Transformer 等模型中大放异彩。MoE 的核心是把 FFN 替换成多个专家 FFN,每个 token 只激活其中少数几个专家(比如 Top-2),由路由门控网络选择。
这样做的好处是总参数量可以做到很大,但每个 token 的激活数量没增加太多。训练 1T 参数的 MoE 模型,单 token 的算力开销相当于一个 20B 左右的稠密模型。这打破了大模型的"参数量和算力必须线性绑定"的定律。
代价是工程复杂度剧增:路由负载不均衡、显存交换枯竭、多机通信量上升。DeepSeek 等模型在 MoE 上做了大量优化,让我觉得 MoE 在大模型时代几乎是必然的方向——毕竟算力预算有限,大家都在想用更小的激活换取更大的知识容量。
5.5 架构演进的主线
如果把以上变化放在一起,大概能提炼出三条主线:一是降低注意力的复杂度和显存开销,这是硬约束;二是改善训练稳定性和归一化策略,这是模型可扩展的底座;三是提高参数量利用效率,让模型以可控算力继续增大。理解这三条主线,架构演进的其他细节基本都能归类进去。
6. 我踩过的坑:不同场景下到底该怎么选型
6.1 长文档场景:最稳妥的组合
我之前做长文档理解项目,序列长度从 2048 拉到 4096 时直接用标准位置编码和稠密 Attention,结果训练时 OOM 和算力开销直接超预算,外推推理质量也崩。后来改成 RoPE + FlashAttention + GQA 的组合,在类 LLaMA 结构上做继续预训练,效果显著改善,训练时间还缩短了四成左右。
如果还要再长到 16K 以上,我会认真考虑加一层稀疏注意力或者窗口注意力,在模型的浅层用窗口、深层用全局,这样质量损失相对可控。
6.2 小模型部署:不是所有新改动都划算
在小模型场景(如 100M 级别)里,SwiGLU、Pre-LN、RMSNorm 这些改动依然有用,但 GQA 的收益不明显,因为小模型的 KV Cache 本来就小;RoPE 相比 ALiBi 也没有拉开太大差距。小模型最重要的是少吃显存、推理快,取舍逻辑和大模型完全不同,别为了追新结构而盲目套用。
6.3 从零开始读代码从哪里读起
经常有人问,Transformer 演进这么多,应该从哪个版本开始学?我的经验是从 LLaMA 的代码读起,因为它在开源工程化上做得极好——RMSNorm、SwiGLU、RoPE、GQA、Pre-LN 全部集中在一个模型里,一行 Sharded 数据并行就能直接跑起来。读起来比原始 Transformer 更费劲,但收获大得多。
6.4 不要盲目追新
最后一点体会:架构演进文章铺天盖地,但每个新算法都有适合自己的场景。比如 FlashAttention 在消费级显卡上提升有限,因为它的优化目标是大矩阵 HBM 带宽调度,小模型小 batch 根本跑不满 GPU。我的习惯是先确定任务约束(序列长度、显存预算、推理时延),再倒推需要哪个版本的 Attention 和位置编码,而不是看到 paper 就往上搬。
架构演进这件事,本质上是在"更大的模型、更长的序列、更低的成本"这三者之间寻找新的可行点。每一轮改动都是前一轮方案在实际项目中碰壁之后的必然修正。理解了这个动机链条,你就不需要背任何一张榜单——自己就能做出合理的判断。