☰
Transformer核心算法演进:从自注意力到FlashAttention与推理优化
2026/10/26 6:25:33 网站建设 项目流程

一开始接触Transformer的人,大概率都从那个著名的公式看起:Attention(Q,K,V)=softmax(QKᵀ/√d)V。2017年《Attention Is All You Need》出来之后,这套架构从NLP一路碾压到CV、语音、多模态,连AlphaFold都用它搞定了蛋白质结构预测。但真正让人头疼的问题是:Transformer的论文原文读起来像一份数学说明书,读过一遍好像懂了,回头自己手写一个却处处踩坑,看到Flan-T5、Llama、Swin Transformer这些衍生模型更是一头雾水。

这篇文章不打算复述论文,而是沿着“核心算法演进”这条主线,把Transformer从原版到当下主流变体的关键改动拆开讲:每个算法改动的动机是什么,数学上怎么算,工程上踩过哪些坑。适合已经跑过一些深度学习模型、准备深入源码或做训练/推理优化的工程师。我会尽量把每个改动背后的“为什么”摊开,配合可以直接抄的参数和代码片段。

1. 原版Transformer:三个算法支点撑起整个架构

1.1 自注意力:QKV到底在算什么

很多入门教程把自注意力讲得很玄,其实它做的事情就是“让序列里的每个元素,根据其他元素决定自己要输出什么”。具体分三步:

输入一个长度为n的序列,每个token的向量表示经过三个不同的线性变换,得到Query、Key、Value三个向量。Query是“我在找什么”,Key是“我有什么可被找到的”,Value是“如果被找到了,我能提供什么信息”。注意力分数就是query和key的点积,点积越大说明“这个key对应的token对我越重要”。

[ score_{i,j} = \frac{q_i \cdot k_j}{\sqrt{d_k}} ]

对每个query,拿它和所有key算完分数后,做一次softmax,再用得到的权重对value加权求和,得到这个位置的输出。

这里有三个细节值得单独拎出来说。

第一个是缩放因子√d。为什么非要除这个?假设q和k都是d维向量,每个分量独立且方差为1,那么两个向量的点积方差就是d。d越大(比如64、128),点积的数值范围就越大,softmax在输入很大时会进入饱和区,梯度趋近于零,训练根本推不动。除以√d把方差拉回1附近,softmax落在梯度良好的区间。这不是数值洁癖,是让自注意力能够稳定训练的关键。

第二个是softmax的温度特性。softmax天然会把最大的分数“放大”,最小的分数压到接近零。这就意味着注意力权重通常很“尖”,模型很快学会只关注少数几个关键位置。所以很多人说注意力是“稀疏”的,其实不是结构上的稀疏,而是softmax造成的分布稀疏。

第三个是复杂度。自注意力的时间复杂度和空间复杂度都是O(n²),n是序列长度。n=512时还扛得住,n=8192时注意力矩阵就是8192×8192,光这一个矩阵浮点存下来就是268MB(按FP32算)。这个平方复杂度,直接决定了后续所有效率优化的方向。

1.2 多头注意力:为什么“分头”有效

原版Transformer没有只做一个注意力,而是把d维的QKV拆成h个头,每个头在d/h维的子空间里独立做注意力,最后拼接起来再过一次线性变换。

[ MultiHead(Q,K,V) = Concat(head_1,...,head_h)W^O ]

多头为什么比单头强?我个人的理解是:单头注意力只能学习一种“相关性模式”,而不同的头可以关注不同类型的关系。举个例子,在翻译任务里,一个头可能学会了语法依赖(主语-动词),另一个头学会了指代关系(代词-先行词),还有一个头可能只是在做位置计数。多个子空间并存,相当于团队里有人管语法、有人管语义、有人管指代,各干各的再汇总。

实际使用中h一般取8或12,d_model=512时每个头的维度是64。这里有个容易被忽略的点:分头不会增加参数量,反而略微减少了中间计算量,因为每个头只在自己的低维空间里做矩阵运算。

踩坑提醒:早期很多人实现多头注意力时,喜欢把h放在最后一维,即(Batch, Head, SeqLen, HeadDim),但PyTorch的attention_bias、flash-attention等库默认的布局是(Batch, SeqLen, Head, HeadDim)。换库或者手写时维度顺序不一致,轻则结果不对,重则直接广播成错误形状还不报错,这种bug特别难查。

1.3 位置编码:给序列注入顺序感

自注意力本身是“置换不变”的——把输入token的顺序打乱,输出的注意力结果不变,因为点积不感知位置。但语言和图像都是强顺序相关的,“我打你”和“你打我”不是一回事。所以Transformer必须在输入里注入位置信息。

原版用的是正弦位置编码:

[ PE_{(pos, 2i)} = sin(pos / 10000^{2i/d}), \quad PE_{(pos, 2i+1)} = cos(pos / 10000^{2i/d}) ]

为什么用正弦余弦而不是直接学一个位置向量?论文给出的理由是:正弦函数的周期性让模型可以“外推”到比训练时更长的序列——模型学到的位置规律是相对的,不是绝对的。不过后来实践证明,直接学位置向量在很多任务上效果反而更好,尤其是序列长度固定(比如BERT的512)时,可学习位置编码更灵活。这也是演进早期的一个方向分化:正弦编码适合无限制长度,可学习编码适合固定长度。

后来还有相对位置编码(Transformer-XL、T5、DeBERTa),以及旋转位置编码RoPE(GPT系列、Llama都在用)。RoPE的核心思想是把位置信息“旋转”进q和k的向量空间,而不是简单地相加。它有两个先天优势:一是显式建模了相对位置——两个token的夹角差只和它们的距离有关;二是对长序列的外推能力比绝对位置编码好得多。现在新模型基本都默认RoPE,原版的正弦编码更多是教学意义了。

2. 效率演进:从平方复杂度到线性注意力

2.1 复杂度瓶颈的真实场景

自注意力的O(n²)复杂度,在真实场景里是怎么暴露的?

NLP这边,处理一篇长文档,序列长度动辄上万,直接跑原版Transformer,显存直接爆炸。CV那边更夸张,一张512×512的图片切patch,每个patch是16×16像素,得到1024个patch已经算少了,如果是高分辨率遥感图像或者医学影像,patch数量能到几万,平方复杂度完全不可接受。我当年第一次尝试把Transformer用在长文本分类上,序列长度5000,batch size才4,一张24G的A5000就直接OOM了。所以效率优化不是“锦上添花”,是决定能不能落地的生死线。

2.2 稀疏注意力与局部窗口

效率演进的第一条路线是做“稀疏化”——既然不是所有token之间都有必要交互,那就只在部分token对之间算注意力。

Reformer提出用局部敏感哈希(LSH)把token分桶,只让同一桶内的token互相做注意力。Longformer和BigBird则设计了更工程化的稀疏模式:局部窗口内的token互相注意 + 少量全局token和所有token注意。窗口注意力假设“距离远的token相关性弱”,这在很多NLP任务里成立,但在需要全局信息的长文档任务里会漏信息。

Swin Transformer把窗口注意力的思想带到了视觉领域,思路非常直接:把图像切成分层窗口,每个窗口内部做注意力,窗口之间通过shift操作交换信息。这样复杂度从O(n²)降到O(n)(窗口大小固定),同时保留了跨窗口的信息流动。Swin在ImageNet上把ViT挤下神坛,靠的不是更花哨的attention,而是这种“局部+移动窗口”的结构先验。

2.3 低秩与核方法:把注意力矩阵本身换掉

第二条路线是直接改变注意力矩阵的计算方式,把“n×n”的密集矩阵变成低秩近似或核映射。

Linformer假设注意力矩阵是低秩的,于是把K和V先投影到低维空间(比如降到k维,k远小于n),再做注意力。这样复杂度从O(n²)降到O(n·k)。这个假设在长序列上是否成立有争议,但实验证明在中等长度(512~2048)上效果不错。

Performer走的是另一条路,用随机特征映射把softmax注意力改写成点积形式。核心是找到一个随机特征映射φ(x),使得φ(x)·φ(y)近似等于exp(x·y)(即softmax的内核),这样注意力计算就变成了(Q'·K'ᵀ)V,只需要一次Q'和K'的低维乘法,复杂度O(n·d')。Performer的理论很漂亮,但实际工程里随机特征的数量控制不好会显著掉点,所以后来用得不多。

2.4 FlashAttention:不换算法,换执行方式

在大家忙着改注意力公式的时候,FlashAttention给了另一个思路:公式不用改,精度不降,只通过“IO感知”的分块计算和核融合,把同样的算法跑出5-10倍的加速。

它的核心洞察是:GPU的算力远大于显存带宽,而原版注意力把中间结果(n×n的注意力矩阵)写回显存再读出来,这个读写比计算还慢。FlashAttention把QK^T计算、softmax、加权求和融合成一个大kernel,通过分块(tiling)让中间结果待在SRAM里而不是HBM,同时用“在线softmax”技巧在不存完整注意力矩阵的情况下算出精确结果,再用重计算省掉反向传播时的中间矩阵存储。

我第一次把FlashAttention接进自己的Transformer训练脚本时,显存直接降了40%,训练速度还快了。当时心里只有一个想法:原来“算法演进”不一定是改数学公式,把内存访问模式优化到极致,同样是一场革命。现在FlashAttention-2、FlashAttention-3已经把这一套打磨得近乎完美,主流训练框架里默认都在用它。

3. 结构演进:三种主流派系的分化

3.1 从Encoder-Decoder到Decoder-only

原版Transformer是标准的Encoder-Decoder结构:Encoder双向编码整个输入,Decoder通过掩码自回归地生成输出。这个结构天然适合机器翻译这类“序列到序列”任务。

但到了2018-2020年,两个更极端的结构逐渐统治了局面。

一个是Decoder-only路线,GPT系列为代表。它只保留Decoder部分,用因果掩码(每个token只能看到自己左侧的token)做自回归训练——给定前文,预测下一个词。这个结构最初被认为不适合需要双向理解的任务,但GPT-3用实验证明:只要模型够大、数据够多,单纯的自回归预训练就能学会大量的任务。而且Decoder-only在推理时有一个极大优势:可以缓存KV,连续生成时只需要算最后一个token的注意力,不用重算整段历史。这也是现在几乎所有主流大模型(Llama、Qwen、DeepSeek)都走Decoder-only路线的工程原因。

另一个是Encoder-only路线,BERT为代表。它只保留Encoder,用双向注意力(没有因果掩码)做掩码语言模型训练。BERT在当时的下游任务上全面碾压GPT-1,确立了“预训练+微调”范式。不过随着scale law被验证,Encoder-only在生成任务上天生吃亏,现在的主流地位已经被Decoder-only取代。

3.2 视觉Transformer:ViT和Swin的两次尝试

把Transformer搬到视觉,最早是被质疑的——图像是二维结构,自注意力没有“局部感受野”这个概念,怎么跟CNN比?

ViT的回答很粗暴:把图像切成固定大小的patch,每个patch拉平成向量,加位置编码,然后直接丢进标准Transformer encoder。这个做法在ImageNet上赛过了当时的CNN,但需要海量数据(JFT-300M)才能训出来,在中小数据集上打不过ResNet。问题出在:ViT的全局部注意力没有给模型任何“图像相邻区域应该相似”的先验。

Swin Transformer的回应是分层的局部窗口注意力:先在小窗口内做注意力,再通过shift窗口让相邻窗口“对话”,层层递进获得全局感受野。这个设计同时解决了两个问题:复杂度降下来了,跨窗口的信息也没丢。Swin在检测、分割等密集预测任务上表现尤其好。我个人的体会是:视觉Transformer的成功,很大程度是“架构个性和任务先验”的胜利——盲目套NLP的全局注意力,在图像上未必是最优解。

3.3 后Transformer时代:状态空间模型的挑战

2023年后,Mamba(基于状态空间模型SSM)和RWKV(线性注意力变体)开始挑战Transformer的统治地位。它们的核心思路一样:把注意力换成线性递推,让每个token只和前一个token的“隐藏状态”交互,而不是和所有token交互。复杂度从O(n²)降到O(n),可以处理超长序列。

但注意,Mamba并没有完全取代Transformer,它有自己明显的短板:一是对“精确召回”要求高的任务(比如从10万token里找一个具体数字)表现不稳定;二是工程生态远不如Transformer成熟,推理优化、量化部署的工具链都不完善。我试过在长文本摘要任务上对比Mamba和Transformer,Mamba确实快,但摘要的忠实度略差——注意力机制天然适合“从长文档里精准定位关键信息”的场景,这是线性递推难以替代的。

所以核心算法演进不是简单的“谁取代谁”,更像是一种分工:全局精准交互的活交给注意力,超长序列的活试试线性模型,两者也在融合(Jamba、Zamba就是Transformer+SSM混合结构)。

4. 训练与推理算法的关键跃迁

4.1 归一化位置:从Post-LN到Pre-LN

原版Transformer把LayerNorm放在残差块之后(Post-LN结构)。这个设计在深层模型里有个著名的麻烦:训练不稳定,经常出现loss spike,需要小心翼翼调学习率。

后来大家发现,把LayerNorm挪到残差块之前(Pre-LN结构),训练稳定性大幅提升,深层模型不再那么容易炸。GPT-2之后几乎所有主流模型都改用Pre-LN。原理虽然还没有完全定论,但一个直观解释是:Pre-LN让每层的输入被归一化后再进子层,残差通路上的信号更干净,梯度回传更顺畅。代价是理论上“表达能力”略弱,但实践里稳定性的收益远大于表达力的损失。

一个容易被忽略的细节:Post-LN在残差连接之后归一化,意味着“残差的加法结果”被归一化,而Pre-LN是先归一化输入再加残差,实际上每一层学到的都是“归一化输入的函数+原输入”。两者对学习率、权重初始化、梯度范数的敏感度完全不同。你用DeepNorm、Sandwich-LN这些变体的时候,需要连同模型深度、学习率一起调,不能照搬。

4.2 优化器与学习率调度:warmup为什么是必须的

Transformer训练的一个经典经验是:必须用warmup。Adam优化器在训练初期会积累一阶矩和二阶矩,如果一开始学习率就很大,模型的梯度会被放得很夸张,导致loss暴涨。

我的习惯是:前10%的步数(比如1000步里前100步)让学习率从0线性上升到峰值,之后按余弦衰减到峰值的10%或更低。峰值学习率在中等规模模型上通常取1e-4到3e-4,batch size 256以上可以适当提高。这个配置在GPT系列的训练中基本是标配套餐。

AdamW是另一个关键改进。原版Adam在做权重衰减时,衰减项混在梯度里被一阶矩“平均”掉了,导致正则化效果打折。AdamW把权重衰减单独拎出来,直接在参数更新时减掉λw,实验证明泛化更好,且对大模型尤其明显。现在基本没有人再用没修正的Adam了。

4.3 推理侧的算法优化:KV Cache与GQA

训练算法演进了,推理侧的算法同样发生了巨大变化。最核心的是KV Cache:Decoder每生成一个token,都要重新计算所有历史token的Key和Value,但历史token的K/V其实已经算过了,不需要重算。于是把之前算过的K/V缓存下来,每次只需要算新token的K/V,推理速度能提升好几倍。

KV Cache带来的问题是:缓存占显存。每个token的K/V都要存下来,序列越长,显存越紧。因此出现了MQA(Multi-Query Attention,所有头共享一份K/V)和GQA(Grouped Query Attention,分组共享K/V)。GQA在Llama 2之后成为主流,它把推理时的KV缓存大小降为原来的1/h(h是query head数),而效果loss很小。

推理侧还有一个重要算法是PagedAttention(vLLM提出)。它的思路类似于操作系统的虚拟内存分页:KV cache被分成固定大小的“页”,按需分配,碎片化被消除,批量推理的吞吐量大幅提升。我第一次用vLLM部署模型时,吞吐量比naive的实现高了不止一个量级。这提醒我们:架构演进的终点不只是训练指标,还有工程里的显存、吞吐、延迟这些“硬指标”。

5. 手写实现与踩坑实录

5.1 手写Transformer最常见的五个坑

很多人喜欢手写Transformer加深理解,我建议一定要写,因为写一遍比读十篇博客都管用。但有几个坑是几乎每个人都会踩的。

第一,mask的形状和位置。因果掩码的形状是(n, n)的上三角矩阵,但实现时容易在维度上搞错。PyTorch里mask要广播到(Batch, Head, SeqLen, SeqLen)或者(Batch, 1, SeqLen, SeqLen),多一个维度少一个维度都可能导致广播错误或者静默错结果。

第二,softmax的数值稳定性。计算QKᵀ之后,数值范围可能很大,直接softmax可能出现NaN。经典做法是先减去行最大值:

logits = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) logits = logits.masked_fill(mask == 0, float('-inf')) probs = torch.softmax(logits, dim=-1)

注意mask_fill的-1e9要换成float('-inf'),否则在fp16下可能溢出为NaN。我见过太多人在这里写-1e9,然后fp16训练直接炸。

第三,多头注意力的维度交换。做完每个头的注意力后,要把维度从(Batch, Head, SeqLen, HeadDim)转回(Batch, SeqLen, Head, HeadDim),再合并。transpose和contiguous的顺序不对,结果就是错的,而且几乎不报错。

第四,残差连接的顺序。Pre-LN和Post-LN的实现顺序要选一个,并且全模型保持一致。很多人混合了两者,结果模型要么不收敛,要么收敛慢得离谱。

第五,初始化的尺度。Transformer对初始化比较敏感,一般建议按Xavier/Glorot初始化线性层,权重标准差按1/d_model缩放。不讲究初始化的话,训练初期loss波动会很剧烈。

5.2 训练不收敛排查清单

我把自己踩过的坑整理成了一张排查清单,遇到不收敛时逐条过一遍:

现象可能原因排查建议
loss完全不下降学习率过大/过小检查warmup和峰值学习率,先试1e-4
loss散点爆炸(NaN)fp16溢出或mask用了-1e9打开AMP,检查mask数值
训练很慢注意力实现无优化确认是否用了flash-attention,序列长度是否过大
验证集loss上涨过拟合增加dropout、weight decay,或减小模型
梯度范数持续增大初始化问题检查线性层初始化,尝试降低学习率

5.3 从单卡到多机:分布式训练的演进

模型规模涨到10B以上,单卡甚至单机都放不下了,分布式策略就成了算法演进的一部分。

数据并行(Data Parallelism)是最简单的:每张卡存一份完整模型,喂不同batch的数据,每次迭代做梯度同步。ZeRO(DeepSpeed)把参数、梯度、优化器状态分片到各个GPU,显存利用率大幅提高,是目前最主流的方案。

张量并行(Tensor Parallelism)把单层的大矩阵拆到多张卡上,适合单机多卡并且卡间通信带宽高的场景(比如NVLink)。流水线并行则把Transformer层按层切分到不同设备,每张卡只负责几层,代价是流水线气泡。Megatron-LM把张量并行+流水线并行组合起来,配合序列并行把LayerNorm/Dropout的计算也切分,是训练数百B模型的标准方案。

但我的建议是:不要一开始就上复杂的并行策略。先在单卡把模型跑通、把数据管线调好,再用DeepSpeed的ZeRO-2/ZeRO-3起步。分布式训练本身就是一个大坑,调试成本极高,能简单绝不要复杂。

6. 算法选型速查表与演进主线总结

场景推荐算法/结构理由
中短文本分类/理解Encoder-only(BERT系)+ Pre-LN双向注意力,理解能力强
长文本生成/对话Decoder-only + RoPE + GQA生成天然适配,推理KV开销可控
高分辨率图像分割/检测Swin或局部窗口注意力复杂度可控,保留局部先验
超长序列(10万+)Mamba/RWKV或混合结构线性复杂度,内存可控
训练大模型FlashAttention + ZeRO-3 + AdamW显存和速度双重优化
高吞吐推理部署vLLM或SGLang + GQAPagedAttention提升吞吐

回到开头那句话:Transformer的核心算法演进,表面上是模型的迭代,本质上是在“表达能力、计算复杂度、工程可行性”三角之间反复走钢丝。原版用缩放点积注意力解决训练稳定性,后续用稀疏化、核方法、IO感知分块解决复杂度,结构上分化出Encoder-only/Decoder-only/视觉Transformer,训练侧用Pre-LN、AdamW、warmup解决稳定性和泛化,推理侧用KV Cache、GQA、PagedAttention解决延迟和吞吐。每条线都是环环相扣的。

我个人的体会是,学这些演进不要只盯着论文里的公式,要带着“如果是我会怎么实现”的视角去读。每次看到一个变体(比如GQA、RoPE、FlashAttention),先自己想想它解决了什么问题、代价是什么,再去看源码验证。这样学一遍,比盲刷十篇paper有用得多。

最后再说一个实操上的小提醒:如果你想快速搭一个Transformer,与其自己手写,不如直接用HuggingFace的transformers库,但要选对版本——新版本的modeling_llama.py里已经默认集成了RoPE、GQA、flash-attention的调用入口。把它当源码教材读一遍,会发现所有我上面讲的演进,全都活生生地躺在那一个文件里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询