2017年我读到《Attention Is All You Need》时,印象其实是矛盾的。一方面,self-attention的全局建模能力确实漂亮;另一方面,O(n²)复杂度让我下意识觉得这种架构只会停留在中等长度序列上,撑不起真正的长文本场景。后来的发展大家都看到了,transformer不仅成了NLP的默认底座,还一路蔓延到图像、语音、多模态,甚至连近年大模型里最核心的算法选型,也都围绕transformer架构在不断演进。
我想把这几年观察到的“核心算法是如何一步步被改出来的”讲清楚:从Q/K/V注意力本身,到复杂度优化、位置编码、归一化策略、注意力头配置,再到Decoder-only范式。不管你是要做模型训练、推理优化,还是单纯想理解大模型底层,这条演进线都值得过一次。这里不会只堆结论,我会把每一步背后的动机、代价和实际取舍都拆开说。理解这些,比记住一堆模型名字更能帮你应对新出的模型。
1. 起点问题:为什么循环网络会被自注意力取代
1.1 RNN序列建模的硬伤,远不止“不能并行”
先从RNN说起。RNN的致命问题不是简单的不支持并行,而是序列建模的方式决定了信息必须沿着时间步逐步传递。每一步隐状态既要表达当前输入,又要保留过去的记忆,这个隐状态本质上是个“有损压缩瓶口”。LSTM注入门控以后缓解了梯度消失,但距离很远的位置之间要经过多层门控复合,信号早就衰减或扭曲了。
机器翻译里,把整句编码成一个向量的做法尤其明显:句子越长,语义细节越难塞进一个固定向量。比如一篇新闻稿,把开头的主语和结尾的时间地点强行压缩到同一个向量里,中间的转折关系很容易被平均掉。这个问题不是靠加深循环层就能解决的,因为无论堆多少层,路径仍然是线性的,信息依然要一站一站传下去。
真正让序列建模发生质变的,是把“路径距离”这个概念直接抹平。Transformer让任意两个token之间都有一条直连路径,不需要经过中间token的中转。从信息论视角看,这相当于把一条容易衰减的串行信道,换成了全连接网络。代价是每条路径都要显式建模相关性,于是就有了自注意力那个n×n的相似度矩阵。
1.2 Attention机制从辅助走向主角的临界点
Attention也不是Transformer原创的。2014年前后,Bahdanau等人在神经机器翻译里引入的attention,本质上是动态加权源端隐状态,让decoder每一步能“按需查阅”源端信息,而不是只靠最后一个向量。这个思路解决了一个实际问题,但还没有脱离RNN的主干。
Transformer的突破在于,把attention完全提升到主架构层面:每一个位置的表示,都通过它与其他所有位置的注意力来生成。于是任意两个token之间的距离被拉到了1,长距离依赖不再是经过若干步传播,而是直接计算相关性。这在算法设计上是个根本转变:用全局检索代替顺序传递,同时让所有位置的计算天然可以并行。
所以讨论Transformer的核心算法演进,起点其实是这个选择:为什么要放弃顺序结构?答案不是“RNN不好”这么简单,而是RNN的串行瓶颈和长程衰减在规模化训练里太吃亏。Transformer把这条路跳过去之后,后面所有演进都围绕两个核心问题展开:一是如何让注意力更快、更省、能处理更长序列;二是如何在保持表达力的同时让训练更稳定。理解了这两个靶子,后面所有改进就都能对号入座。
2. 自注意力精算:Q/K/V设计、缩放点积与多头的数学逻辑
2.1 Attention运算中的“查询-键-值”到底在做什么
要理解自注意力,可以先把它想成一个数据库检索系统。每个输入token经过三套不同的线性变换,分别得到查询(Query)、键(Key)和值(Value)。查询是“我想找什么”,键是“我身上有什么索引”,值是“我实际提供的内容”。某个请求与前一个位置的键越匹配,注意力权重越大,输出就越偏向前一个位置的值。
在自注意力场景中,这个机制是模型自己对自己的序列做检索:每个token都能根据整个序列的内容决定自己的表示。从实现角度看,所有token的Q、K、V可以被组织成矩阵,因此可以用矩阵乘法一次算完所有token之间的相似度。这也是并行性的来源:不再是逐步计算,而是一次性算出一张完整的关系表。
具体公式可以写成:
Attention(Q, K, V) = softmax(Q * K^T / sqrt(d_k)) * V矩阵Q和K的每一行对应一个token的查询和键。Q和K的转置相乘,得到n×n的相似度分数矩阵;softmax把每行归一化成概率分布;最后用这些概率对V做加权求和,得到每个token的新表示。
2.2 缩放因子sqrt(d_k):一个经常被忽略的稳定性设计
公式里除以sqrt(d_k),很多入门文章把它当作“经验性技巧”,实际上这里藏着训练稳定性的关键。假设Q、K中的元素是均值0、方差1的随机变量,那么单个维度乘积的均值是0、方差是1,对d_k个维度的点积结果来说,方差会累积到d_k。
如果不对点积做缩放,当d_k变大时,点积数值的方差也会变大。Softmax的输入一旦落到两端饱和区,梯度会变得非常小,模型几乎无法学习。除以sqrt(d_k)可以让点积方差回到1左右,让softmax保持在梯度敏感的区间。这是基于随机变量方差性质的推导,不是拍脑袋的经验。实际训练中,不缩放也能跑通小模型,但深层模型和长序列场景下会明显难训。很多复现Transformer失败的项目,最后排查下来就是漏了这个因子或者实现时数据类型精度不对。
2.3 多头注意力如何让模型同时看到多种关系
一个注意力头只能有一组线性映射,也就是说它只能用一种相似度度量来衡量任意两个token的关系。但一段文本里的关系是多种多样的:有的头适合关注相邻词,有的头适合追踪句法依存,有的头适合捕捉跨句话题。如果只用单头,模式会被迫压缩到同一个空间里。
多头注意力把输入投影到h个平行子空间,每个头独立计算注意力,最后拼接再经过一次输出投影。由于每个头的初始线性映射不同,它们会被逼着分化成不同的语义角色。实验和后来的分析都表明,这种分工是自动出现的,而不是人为指定的。Transformer原始论文里d_model=512、h=8、每个头维度64,这个配置到今天仍是很多模型的起点。多头付出的额外参数不算大,但让模型表达空间立刻丰富起来,这是它能以相对较低成本碾压单头注意力的主要原因。
3. 复杂度突围:稀疏、线性与IO感知的注意力演进
3.1 平方复杂度在长序列场景中先卡住显存
自注意力的直接计算复杂度是O(n²·d),n是序列长度,d是特征维度。表面上看这是个“计算量”问题,实际第一卡点是显存:注意力权重矩阵本身是一个n×n的矩阵,需要在显存里显式存下来。
我算过一个直观数据:n=8192、float32时,单层注意力矩阵就是8192×8192×4字节,约268MB。如果模型有三四十层,又不用任何优化,显存很快会被中间矩阵占满。所以当大家想把Transformer用到长文本、长视频序列时,“平方”根本不是细节,而是第一堵墙。后面所有高效注意力算法,目标都是拆掉这堵墙。
这里容易有个误区:以为O(n²)只是慢,实际上它同时限制了批量大小和序列长度。很多时候不是算不动,而是显存放不下,连batch size 1都跑不了。理解这一点,再看稀疏注意力和FlashAttention的动机就会清楚很多。
3.2 稀疏注意力:用局部+全局连接近似完整关系
最早被大量验证的做法是稀疏注意力。Longformer的思路很直接:保留一个滑动窗口,让每个token只看附近若干token,同时额外设置少数全局token,比如CLS,让信息可以通过它们跨区域传播。BigBird在此基础上又加了随机连接,证明窗口、随机、全局三种模式的组合能够在理论上逼近完整注意力的表达能力。
视觉里的Swin Transformer也采用了类似哲学。它把注意力限制在局部窗口内,再用窗口移位获得跨窗口连接,既把复杂度降下来了,又保留了层次化的视觉感知。稀疏注意力的核心代价是,必须人工决定哪些位置需要被关注。任务不同,最佳模式也不同:长文本问答里局部窗口可能不够,全局token的选点需要试验;图像任务里窗口大小本身就是超参数。 它不像标准自注意力那样“无脑”,调起来更麻烦。
3.3 线性注意力:把softmax换成特征映射后的计算重排
另一个方向是从数学上消掉n×n矩阵。完整softmax注意力要先算QK^T,再softmax,再乘以V。线性注意力把softmax中的指数相似度,拆成两个独立特征映射的内积形式,然后利用矩阵结合律把运算顺序改成:
φ(Q) * (φ(K)^T * V)这样先算φ(K)^T * V,得到一个d×d的小矩阵,再与φ(Q)相乘,复杂度从O(n²)降到了O(n)。这类方法在超长序列检索、DNA序列建模等场景有应用,但效果普遍不如完整注意力,因为它把softmax的归一化竞争性抹掉了。没有了归一化,模型很难通过注意力权重做“选哪个、忽略哪个”的离散判断。我自己的经验是,线性注意力更适合序列非常长、但对质量要求不极端严苛的任务;如果资源允许,优先尝试FlashAttention往往更划算。
3.4 FlashAttention的真实贡献:不省计算,省内存搬运
FlashAttention经常被误归为近似注意力,这其实是最大的误解。它计算的注意力结果和标准注意力完全一致,只是改变了计算顺序和存储策略。
GPU有两块存储:片上SRAM很快但很小,显存HBM很大但慢。标准做法是频繁把QK^T等中间矩阵写到HBM,再从HBM读出来,一遍遍搬运。FlashAttention把Q、K、V分块放到SRAM里,每个block内做完整softmax,再用online softmax的思想逐步累积结果,全程不落盘完整注意力矩阵。效果是显存占用从O(n²)降到O(n),速度提升明显。它优化的是IO搬运,而不是计算量。
现在主流训练框架里,FlashAttention基本可以无脑开启,这也让长上下文在工程上变得现实了很多。如果项目里发现训练长序列时GPU利用率低,先确认是不是没开FlashAttention;开了以后瓶颈往往会从前向计算转移到通信或数据加载,这是我自己踩过的坑。
4. 位置编码演进出:sin/cos、相对编码与RoPE
4.1 为什么自注意力必须额外补充位置信息
自注意力有个性质:如果把输入序列打乱顺序,输出也只会随之打乱,而不会改变任意两个位置之间的关系结构。因为它只看内容相似度,不看谁在谁前面。对语言来说,顺序就是信息。“张三打了李四”和“李四打了张三”的词完全一样,语义完全相反。
所以Transformer在最初设计中就必须显式加入位置信息。这个模块看似配角,实际撑起了今天长文本外推能力的大半。很多项目训练时一切正常,一换序列长度就崩,往往就是位置编码选型的问题。位置编码本身也从最初的固定三角函数,一路演进到可学习绝对位置编码、相对位置编码、旋转位置编码RoPE,再到零参数的ALiBi。
4.2 绝对位置编码的两种形态:固定式与可学习式
原始Transformer使用固定三角函数编码,不同维度用不同频率的sin/cos生成。好处是参数少、不学习、可以尝试外推;更关键的是,由于三角函数的和差化积公式,理论上编码后两个位置向量的点积能包含相对位置信息。不过在深层网络里,这种相对信息能不能被有效利用并不一定。到了BERT时代,大家发现直接用可学习的位置embedding更简单,效果当时也更好,所以大量预训练模型都采用了它。
可学习绝对位置编码的问题是训练长度固定,模型见过多长就只能处理多长。如果训练时最长512,推理时来了1024,超出部分的位置embedding是从未训练过的随机值,效果自然崩。后来为了让模型能处理长文档,大家才重新转向相对位置类编码,因为相对距离比绝对序号更容易泛化:你说“隔了两句话的关系”比说“第5个词和第120个词的关系”更本质。
4.3 相对位置编码与Transformer-XL的外推思路
绝对位置编码把“第i个位置”当作独立标签,相对位置编码则直接建模“第i和j个位置相距多远”。Transformer-XL使用相对位置编码后,模型在训练长度之外也能延续一定距离,因为距离关系比绝对序号更容易迁移。T5的处理更直接:给注意力分数加一个由相对距离决定的可学习bias,让模型在打分时知道两个token离得远还是近。
这类设计的直觉是:语言中“相邻”“相隔两个词”“跨句”这些距离模式是稳定的,而具体出现在第几段并不重要。不过相对位置编码的实现细节通常更绕,比如需要调整K和V的拆分方式,网络层数一变就容易搞混。我在复现Transformer-XL时曾花了大半天排查维度不对齐的问题,原因就是相对位置索引的偏移量没写对。位置编码虽然看着只有几行代码,但确实是实现时最容易出错的地方之一。
4.4 RoPE为什么成了LLM的事实标准
旋转位置编码RoPE是近几年大模型里用得最多的位置编码。它的核心想法很漂亮:把每个位置对应的Q或K向量在高维空间里旋转一个与该位置相关的角度。两个旋转后的向量做内积时,旋转不改变向量长度,只改变相位,所以内积结果恰好只依赖两个位置的旋转角之差,也就是相对位置。
这种设计把位置信息与注意力计算融合得不露痕迹,同时具备相对位置编码的外推优势。更重要的是,后续对RoPE做长度外推很方便:直接对旋转角做线性插值或NTK插值,就可以把模型推到训练长度之外,不需要改动模型结构。Llama、Qwen这些开源模型都用它,生态工具也都对齐了,所以它成为事实标准不是偶然。如果需要在超长上下文场景部署模型,RoPE加插值方案是目前最成熟的方向之一。
4.5 外推替代方案ALiBi
如果你不想引入复杂的旋转公式,ALiBi是另一个有趣的思路。它完全不做位置embedding,而是在注意力分数上直接减去一个与两个位置距离成正比的惩罚项,让模型天然对“远距离”降低权重。这种设计几乎零参数,在短训练、中长推理的外推场景上表现不错。
我实际测评中发现,ALiBi在需要较长外推的任务里比较省心,但训练充分的大模型里RoPE配合插值通常更稳。位置编码没有银弹,关键是定义清楚你手里的序列长度需求和外推目标。如果训练和推理长度一致,可学习绝对位置编码足够;如果要外推,RoPE或ALiBi是更稳的选择。这个判断越早做,后面返工越少。
5. 同是Transformer外壳,内部件一直在换:归一化、激活、共享头
5.1 Post-LN到Pre-LN的稳定化改动
原始Transformer把LayerNorm放在残差相加之后,也就是Post-LN。理论上Post-LN表达能力更强,因为它不对残差路径做额外限制,但实际训练大模型时容易不稳定,需要warmup和精细调参。后来GPT-2和很多开源模型改用Pre-LN:先对输入做LayerNorm,再进行注意力或前馈网络,最后残差直接相加。
Pre-LN的残差路径更干净,梯度更容易跨层流动,训练更稳定。代价是模型最终效果可能略弱于Post-LN,但和“能不能训起来”相比,这个代价非常好接受。工程上我见过不少照着原始Transformer搭模型的朋友,第一版堆不出深度,改成Pre-LN以后几十层就能稳定跑了。现在看主流LLM的开源代码,几乎都是Pre-LN或者它的变体。如果你自己从零训练Transformer,先把这一步改对,比调一堆学习率更有效。
5.2 从LayerNorm到RMSNorm的一步简化
LayerNorm要计算均值和方差,然后做归一化。RMSNorm的研究者发现,均值那一步其实可以省掉,只要用RMS(均方根)做缩放,就足够稳定,而且省了不少计算。听起来像小改动,但对几十亿参数模型来说,省一个reduce对训练吞吐是有实际影响的。LLaMA系列用的就是RMSNorm。
从算法演进角度看,RMSNorm少了一次中心化偏移的可学习参数,也少了一层统计耦合,训练时更干脆。如果你的模型在训练时发现LayerNorm统计均值长期偏离0,仿照RMSNorm砍掉中心化,通常会更快收敛。当然这不是万能技巧,但在很多场景下确实能让归一化这块更简洁。现在新开源模型普遍用RMSNorm,也说明这种简化在工程上是划算的。
5.3 激活函数ReLU到GELU、SwiGLU的演进
Transformer前馈网络里的激活函数也在演进。原始论文用ReLU,简单高效,但负数区域梯度完全为0,容易造成神经元死亡。BERT和GPT-1开始用GELU,它用标准正态累积分布做平滑门控,0附近不是尖点,梯度更连续,训练更稳。后来PaLM、LLaMA、很多LLM用SwiGLU,把门控线性单元和Swish组合起来,等于一个分支负责“内容”,另一个分支负责“门控”。
SwiGLU在同等计算量下通常能带来稳定的效果提升,但会额外增加参数。实际使用时,常常要把中间层维度缩小,比如从4倍降到约2.67倍,来抵消参数增量。这里给个参数建议表:
| 激活函数 | 典型中间层倍数 | 特点 |
|---|---|---|
| ReLU | 4倍 | 简单快速,容易神经元死亡 |
| GELU | 4倍 | 平滑梯度,预训练更稳 |
| SwiGLU | 约2.67倍 | 门控机制,效果好但参数多 |
这个维度调整看起来小,但对训练吞吐影响很大。我见过有人把SwiGLU的中间层继续设成4倍,结果参数量爆炸,训练速度直线下降,效果却没提升。激活函数选型时,一定要连同维度一起看。
5.4 从MHA到MQA/GQA:推理成本驱动的头结构变化
注意力头结构的最大演进动力来自推理阶段的KV cache。标准多头注意力MHA每个头都独立存K和V,序列越长显存越大。推理时这些KV要一直存在,所以有人做了MQA:让所有头共享同一个K和V。共享以后KV cache大幅缩小,但表达能力下降,模型质量可能有折扣。
折中方案是GQA:把注意力头分成若干组,组内共享一组K/V。GQA在质量和KV cache之间取得了平衡,比如Llama 2 70B就用了GQA。如果你在做长上下文服务的部署,KV cache是绕不开的工程问题。MQA/GQA这类结构改动,远比单纯加显存更实际。下面这张表可以帮你快速决策:
| 类型 | K/V共享方式 | KV cache开销 | 适用场景 |
|---|---|---|---|
| MHA | 每头独立 | 最高 | 训练阶段,追求表达上限 |
| MQA | 所有头共享一组 | 最低 | 轻量推理,接受质量回落 |
| GQA | 组内共享 | 折中 | 生成式LLM,质量与显存平衡 |
实际开发时,训练阶段可以用MHA充分训练,推理阶段再做GQA转换或量化,这种思路在一些部署实践中已经很常见。不过要注意,训练和推理的注意力头配置不一致时,需要做参数重构,不能简单改名了事。
6. 范式位移:Decoder-only与条件计算带来的新问题
6.1 BERT与GPT左右两条路的结果
从Transformer衍生出来的早期路线分两支。BERT走Encoder-only架构,用双向上下文预训练,在GLUE、SQuAD等理解类任务上统治了很久。GPT走Decoder-only架构,用因果语言模型做生成,一开始更擅长续写。当时很多研究者的判断是两种架构各司其职:理解归BERT,生成归GPT。
后来的发展大家都清楚,生成式AI成为大模型主流后,两边在规模、生态、社区投入上开始明显倾斜。这种偏差背后有算法理念的原因:BERT的预训练目标需要额外任务头才能迁移;而GPT的自回归目标天然把所有任务统一成“预测下一个token”,下游使用时几乎不需要架构改动。看起来只是训练目标的差别,实际变成了整个产品形态的差别。
6.2 为什么生成式大模型把Decoder-only当成默认架构
除了统一任务很方便,Decoder-only在工程上也更好扩展。因果注意力意味着所有位置只看左侧,显存中KV的形态整齐,推理时逐token生成天然符合KV cache的更新方式。Encoder-Decoder架构虽然双向理解更强,但生成时仍然要从Decoder走一遍,编码端和解码端的调度更复杂,推理开销也更大。
再加上近年一些对照实验显示,在同等参数量和训练数据下,Decoder-only在下游任务上比Encoder-Decoder不弱甚至更强。这直接推动元模型、Llama、Qwen等几乎所有主流大模型都选了Decoder-only。倒不是说Encoder-Decoder没有价值,而是在当前规模化生成的范式中,Decoder-only的工程优势太明显。如果只是做纯理解任务,Encoder-only依旧有其价值;但如果你要做一个通用助手,Decoder-only几乎是最省事的起点。
6.3 MoE、多模态等架构层面的新变量
说Transformer架构演进还不能不提MoE。MoE不是改注意力算法,而是把FFN层换成多个专家网络,并通过门控网络对每个token动态选择专家。它以条件计算的方式让模型参数量大幅增加,但单次推理只激活一部分参数。Mixtral、DeepSeek等模型把MoE和Transformer结合后,长上下文的性价比进一步提升。
多模态方向则出现把图像、音频都当作token序列输入到Transformer的做法。核心算法时代的很多技巧都能原样保留,只是模态编码器不同。这些都说明,Transformer的核心算法演进不会停在“注意力本身怎么算”,而会在“结构算法如何匹配实际算力”上继续延伸。
我入行这几年,最大的感受是:很多看似理论优雅的演进,最初都来自工程上的“卡脖子”问题。FlashAttention来自显存瓶颈,Pre-LN来自深度训练不稳定,GQA来自推理KV cache膨胀。如果你自己做项目,别急着追新模块,先看看当前流程卡在哪一层:序列一长就崩,优先看注意力复杂度;模型越深越难收敛,优先看归一化和残差;服务端KV cache太吃显存,优先看GQA。按这个思路去选架构算法,比复制别人的配置要有效得多。这也是我把前面那些演进串成一条逻辑线的意义:把每个新模块的名字都还原成它原本要解决的问题,用的时候自然知道怎么选。