一、前言
在上一篇《大模型时代全景图》里,我们梳理了从 2017 年 Transformer 论文到 2026 年大模型生态的完整演进。如果你把那篇文章读完,会发现一个事实:过去八年所有的大模型——GPT、Claude、Llama、Qwen、DeepSeek——本质上都是 Transformer 的变体。
这一篇我们就来彻底搞懂 Transformer。
但这不是一篇"复现论文"的文章。论文级的推导网上已经太多,多数读者读完仍然不知道这玩意儿在生产环境里到底怎么用。本文从工程师视角出发,目标是让你读完之后能回答这些问题:
为什么所有大模型推理框架都要专门优化 attention?
KV Cache 到底缓存了什么?为什么能加速?
上下文从 4K 扩展到 1M 的工程难点在哪里?
为什么 Flash Attention、PagedAttention 都围着 attention 转?
为什么 Decoder-only 架构吃掉了一切?
如果你能回答以上每一个问题,那么后续推理优化、显存计算、长上下文等等一系列工程话题,你都会有一个稳固的"心智模型"作为参照——这也是为什么把 Transformer 放在系列第 2 篇。
我们开始。
二、为什么 Transformer 是大模型的「地基」
2.1 前 Transformer 时代:RNN 的三个死结
在 2017 年之前,处理"序列"的主流方案是 RNN/LSTM/GRU。这类网络有一个核心结构:当前时刻的输出,依赖上一时刻的隐状态。
h_t = f(h_{t-1}, x_t)这个看似优雅的递推公式,给工程师埋下了三个炸弹:
炸弹 1:无法并行
序列必须按时间步顺序计算——要算h_10,必须先算完h_1, h_2, ..., h_9。这意味着:
训练时,单条样本内部完全串行,GPU 闲置严重
序列越长,训练越慢,且无法靠堆 GPU 解决
炸弹 2:长程依赖丢失
虽然 LSTM 引入门控机制缓解了梯度消失,但实际效果在序列长度超过 200 后就明显退化。让 RNN 理解一篇 8K Token 的文章?基本不可能。
炸弹 3:状态压缩瓶颈
整个序列的信息被压缩到一个固定维度的隐状态向量里——就像让你用 256 个数字记住一整本《三国演义》。
2.2 2017 年的转折点
2017 年 6 月,Google Brain 团队发表了《Attention is All You Need》。这篇论文做了一件极其大胆的事:把 RNN 整个扔掉。
新架构的核心思想可以用一句话概括:
让序列中任意两个位置直接"对话",不需要经过中间状态。
这个改变看似激进,但带来了三个工程红利:
(1)完全并行:序列内所有位置同时计算,训练效率呈数量级提升
(2)长程依赖直连:第 1 个 token 可以直接"看到"第 8000 个 token
(3)架构高度统一:CV、NLP、语音、多模态最终都收敛到 Transformer
也正因为如此,Transformer 才有可能"做大"——175B 的 GPT-3、671B 的 DeepSeek、千亿级 MoE,本质上都建立在 attention 可大规模并行计算这个基础之上。
2.3 工程师为什么必须懂 Transformer
你可能会问:我又不训模型,懂这个干嘛?
答案是:Transformer 不只是训练框架的事,更是推理框架、显存管理、性能优化的根本依据。
下面这张表列出了"工程问题"和"Transformer 概念"的对应关系:
工程问题 | 背后的 Transformer 概念 |
为什么单卡跑不动 70B 模型? | 参数量 + KV Cache 显存占用 |
为什么上下文一长就 OOM? | Attention 是 O(n²) 复杂度 |
为什么 vLLM 比朴素推理快几十倍? | PagedAttention 优化 KV Cache 管理 |
为什么 Flash Attention 能省显存? | Attention 计算的 IO 瓶颈 |
为什么 RoPE 比绝对位置编码好? | 位置编码的外推能力 |
为什么 MoE 能省推理算力? | FFN 层占了模型大半参数 |
投机解码为什么有效? | Attention 的自回归特性 |
读完本文,你会理解上面每一行的逻辑。
三、核心原理:从 Self-Attention 到完整 Transformer
我们从最基本的 Self-Attention 开始,逐层往外推。
3.1 Self-Attention:让 Token 互相"对话"
问题设定:给定一个序列[token_1, token_2, ..., token_n],怎么让每个 token 都能"考虑到"其他所有 token?
核心思路:对每个位置i,计算它和所有其他位置j的"相关度"权重,然后用这些权重对所有位置的信息做加权求和。
具体来说,每个 token 会被投影成三个向量:
Query (Q):当前位置在"问什么"
Key (K):其他位置能"回答什么"
Value (V):其他位置实际"携带的信息"
公式如下:
其中,
| 符号 | 含义 | 常见维度(NLP 场景) |
|---|---|---|
| Q Query 查询矩阵 | 代表当前要编码的 token,用来 “提问”,找和自己相关的内容 | |
| K Key 键矩阵 | 待匹配的候选 token,和 Q 做相似度匹配 | |
| V Value 值矩阵 | 最终要提取的信息载体,匹配成功后加权取值 | |
| 矩阵乘法,计算Q 中每个向量和所有 K 向量的点积相似度 | ||
| 缩放因子,Key 向量维度的平方根 | 归一化点积,解决高维下点积方差爆炸问题。防止点积过大导致 softmax 进入梯度消失区。 | |
| softmax | 按行归一化,把相似度转为权重分布(总和 = 1) | |
| 末尾乘V | 用注意力权重对 Value 加权求和,输出融合后的特征 |
几何直观:把每个 token 想象成高维空间中的一个向量。Self-Attention 让每个 token 都能"看一眼"所有其他 token,然后把自己变成"和我相关的那些 token 的加权混合体"。
工程视角:整个过程是纯矩阵乘法 + softmax。这意味着:
可以完全并行:所有位置同时计算
可以高度优化:矩阵乘法是 GPU 的看家本领(CUDA Tensor Core)
可以精确缓存:K 和 V 一旦算出可以反复用(这就是 KV Cache 的根基)
3.2 Multi-Head Attention:多个"视角"并行看
单个 attention 头有一个局限:它只能学到一种"相关性模式"。但语言中的关系是多元的——语法关系、语义关系、指代关系、共现关系……一种 attention 学不完。
解决方案:把 Q、K、V 拆成多组(比如 32 头),每组独立做 attention,最后拼起来。
# 伪代码 head_outputs = [] for i in range(num_heads): Q_i, K_i, V_i = split_per_head(Q, K, V, i) head_outputs.append(attention(Q_i, K_i, V_i)) output = concat(head_outputs) @ W_o工程角度的几个关键数字(以 Llama-3-70B 为例):
hidden_dim= 8192num_heads= 64head_dim= 128(注意:8192 / 64 = 128)
这种"切分维度"的设计让多头几乎不增加计算量——总的矩阵规模没变,只是 reshape 了一下。
💡2024 年后的演进:GQA / MQA
Llama 2 之后大量模型用Grouped Query Attention:让多个 query head 共享同一组 K/V head。
例如 Llama-3-70B 实际有 64 个 Q head 但只有 8 个 KV head。
这个改动直接减少了 KV Cache 显存占用 8 倍——是长上下文部署的关键优化。
3.3 Positional Encoding:注入"顺序"信息
Self-Attention 有一个隐藏问题:它对位置完全不敏感。
把"我打你"和"你打我"输入进去,由于是对所有位置做加权求和,结果几乎一样——这显然不对。
解决方案:在输入向量上加一个表示位置的编码。
演进路线:
方法 | 提出时间 | 思路 | 当下使用 |
Sinusoidal | 2017 | 用 sin/cos 表示绝对位置 | 已淘汰 |
Learned PE | 2018 | 给每个位置学习一个向量 | BERT、早期 GPT |
ALiBi | 2021 | 在 attention 分数上加位置偏置 | 部分模型 |
| RoPE | 2021 | 用旋转矩阵编码相对位置 | 当下主流 (Llama / Qwen / DeepSeek) |
RoPE(Rotary Position Embedding)为什么赢了:
相对位置:天然反映 token 之间的距离关系
可外推:训练 4K,推理时通过 YaRN 等技术可以扩展到 32K、128K、1M
计算高效:只在 Q 和 K 上做旋转,不增加参数
👉 关于长上下文外推,详见系列第 15 篇:YaRN、Ring Attention 详解
3.4 完整的 Transformer Block
把 attention 包装成可堆叠的 block,再加几个组件,就得到了完整的 Transformer Block:
输入 x ├─→ LayerNorm │ ↓ │ Self-Attention │ ↓ └─── ⊕ (残差连接) ↓ ├─→ LayerNorm │ ↓ │ FFN (前馈网络) │ ↓ └─── ⊕ (残差连接) ↓ 输出几个工程上的关键设计:
① 残差连接(Residual Connection)
输入直接"短路"加到输出上:x + f(x)。这让深层网络可以训练——没有残差,超过几十层就训不动了。Llama-3-70B 有 80 层 Transformer Block,全靠残差扛着。
② LayerNorm vs RMSNorm
LayerNorm 把每层激活做归一化。2023 年后主流大模型基本都换成了 RMSNorm(Llama 系列首推),相比 LayerNorm 减少了一次均值计算,推理速度更快、精度几乎无损。
③ FFN(Feed-Forward Network)
每个 Transformer Block 里都有一个 FFN,结构是:
FFN(x) = W_2 · activation(W_1 · x)W_1把维度放大 4 倍,激活函数过滤一下,W_2再缩回去。这是模型大部分参数的"住所"——以 Llama-3-70B 为例,FFN 占了总参数的约70%。
💡这就是 MoE 优化的入口
既然 FFN 参数最多但每次推理只用一部分,那不如把它做成"专家库"——每个 token 只激活几个专家。
这就是 DeepSeek V3 (671B 总参数,37B 激活) 背后的核心机制。
详见系列第 31 篇:MoE 架构深度解析。
④ Pre-LN vs Post-LN
原始论文用 Post-LN(先做完 attention 再 LayerNorm),但训练大模型时不稳定。2020 年后所有大模型都改成了 Pre-LN(先 LayerNorm 再 attention)。这是一个不起眼但影响巨大的工程改进。
3.5 Decoder-only:为什么这个架构吃掉了一切
Transformer 原论文是为机器翻译设计的,包含 Encoder 和 Decoder 两部分。但实际演化中,三种架构同台竞技:
架构 | 代表模型 | 特点 | 现状 |
Encoder-only | BERT | 双向注意力,擅长理解任务 | 仍在用,但非主流 |
Encoder-Decoder | T5 / BART | 编码 + 解码,适合翻译 | 渐渐淡出 |
| Decoder-only | GPT / Llama / Claude | 自回归生成 | 绝对主流 |
为什么 Decoder-only 赢了?
从工程视角看,原因有三:
统一性:所有 NLP 任务都可以转化为"给定前缀,预测下一个 token"——分类、抽取、对话、翻译……一个范式打遍天下。
Scaling 友好:训练目标极其简单(下一个 token 预测),数据极易获取(任何文本都能用),适合 Scaling Law。
In-Context Learning 涌现:达到足够规模后,模型仅凭 prompt 中的例子就能学会新任务,无需微调。
Decoder-only 的核心机制:Causal Mask
为了保证"自回归生成"(生成第 t 个 token 时只能看到前 t-1 个),attention 需要加一个三角掩码:
Mask matrix (n=4): [0 -inf -inf -inf] [0 0 -inf -inf] [0 0 0 -inf] [0 0 0 0 ]加在QK^T后、softmax 前。-inf经过 softmax 变成 0,等于"看不到未来"。
这个看似简单的设计,直接催生了 KV Cache 这一推理优化的基石——我们下一节会看到。
四、实战拆解:用 100 行 PyTorch 写一个 Mini-GPT
光看公式记不住,写一遍代码就懂了。以下是一个可运行的极简 Transformer Decoder:
import torch import torch.nn as nn import torch.nn.functional as F from dataclasses import dataclass @dataclass classConfig: vocab_size: int = 32000 hidden_dim: int = 256 num_heads: int = 8 num_layers: int = 4 max_seq_len: int = 512 classCausalSelfAttention(nn.Module): def__init__(self, cfg): super().__init__() assert cfg.hidden_dim % cfg.num_heads == 0 self.num_heads = cfg.num_heads self.head_dim = cfg.hidden_dim // cfg.num_heads self.qkv = nn.Linear(cfg.hidden_dim, 3 * cfg.hidden_dim, bias=False) self.proj = nn.Linear(cfg.hidden_dim, cfg.hidden_dim, bias=False) # 因果掩码 mask = torch.tril(torch.ones(cfg.max_seq_len, cfg.max_seq_len)) self.register_buffer("mask", mask.view(1, 1, cfg.max_seq_len, cfg.max_seq_len)) defforward(self, x): B, T, C = x.shape # 1. 投影成 Q, K, V qkv = self.qkv(x) # [B, T, 3C] q, k, v = qkv.split(C, dim=-1) # 2. reshape 成多头形式 q = q.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) # [B, H, T, D] k = k.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) v = v.view(B, T, self.num_heads, self.head_dim).transpose(1, 2) # 3. 计算 attention 分数 scores = (q @ k.transpose(-2, -1)) / (self.head_dim ** 0.5) # 4. 因果掩码 scores = scores.masked_fill(self.mask[:, :, :T, :T] == 0, float('-inf')) # 5. softmax + 加权求和 attn = F.softmax(scores, dim=-1) out = attn @ v # [B, H, T, D] # 6. 合并多头 out = out.transpose(1, 2).contiguous().view(B, T, C) returnself.proj(out) classFFN(nn.Module): def__init__(self, cfg): super().__init__() self.w1 = nn.Linear(cfg.hidden_dim, 4 * cfg.hidden_dim, bias=False) self.w2 = nn.Linear(4 * cfg.hidden_dim, cfg.hidden_dim, bias=False) defforward(self, x): returnself.w2(F.gelu(self.w1(x))) classBlock(nn.Module): def__init__(self, cfg): super().__init__() self.ln1 = nn.LayerNorm(cfg.hidden_dim) self.attn = CausalSelfAttention(cfg) self.ln2 = nn.LayerNorm(cfg.hidden_dim) self.ffn = FFN(cfg) defforward(self, x): # Pre-LN + 残差 x = x + self.attn(self.ln1(x)) x = x + self.ffn(self.ln2(x)) return x classMiniGPT(nn.Module): def__init__(self, cfg): super().__init__() self.cfg = cfg self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.hidden_dim) self.pos_emb = nn.Embedding(cfg.max_seq_len, cfg.hidden_dim) self.blocks = nn.ModuleList([Block(cfg) for _ inrange(cfg.num_layers)]) self.ln_f = nn.LayerNorm(cfg.hidden_dim) self.head = nn.Linear(cfg.hidden_dim, cfg.vocab_size, bias=False) defforward(self, idx): B, T = idx.shape pos = torch.arange(T, device=idx.device).unsqueeze(0) x = self.tok_emb(idx) + self.pos_emb(pos) for block inself.blocks: x = block(x) x = self.ln_f(x) returnself.head(x) # 跑一个 forward 测试 if __name__ == "__main__": cfg = Config() model = MiniGPT(cfg) x = torch.randint(0, cfg.vocab_size, (2, 64)) # batch=2, seq=64 logits = model(x) print(logits.shape) # torch.Size([2, 64, 32000]) print(f"参数量: {sum(p.numel() for p in model.parameters()) / 1e6:.2f}M")跑一下输出:
torch.Size([2, 64, 32000]) 参数量: 11.95M12M 参数的 mini-GPT,结构上和 Llama-3-70B 是完全一样的——只是层数、宽度、词表大小不同。
💡真实工业级实现的差异
把
LayerNorm换成RMSNorm把绝对位置编码换成
RoPE把 FFN 的 GELU 换成 SwiGLU
Attention 改成 GQA(多个 Q 共享 K/V)
实际用
torch.nn.functional.scaled_dot_product_attention直接调底层 Flash Attention但核心骨架不变。后续大模型的所有"创新",都是这个骨架上的"换部件"。
五、工程意义:从架构到推理优化
这部分是本文最重要的一节。我们要把"架构理解"转化为"工程认知"。
5.1 KV Cache:自回归生成的最大优化
回看 Decoder-only 的生成过程:模型每次只生成一个新 token,然后把它拼回输入再生成下一个。
朴素做法(无 KV Cache):
生成第 1 个 token:处理 [prompt] → 算所有 K, V → 输出 生成第 2 个 token:处理 [prompt, t1] → 重新算所有 K, V → 输出 生成第 3 个 token:处理 [prompt, t1, t2] → 重新算所有 K, V → 输出 ...注意到了吗?前面 token 的 K 和 V 每次都在被重复计算。
KV Cache 做法:
生成第 1 个 token:处理 [prompt] → 算 K, V → 缓存 → 输出 生成第 2 个 token:只处理 [t1] → 算 t1 的 K, V → 拼到缓存 → 输出 生成第 3 个 token:只处理 [t2] → 算 t2 的 K, V → 拼到缓存 → 输出 ...每生成一个新 token,只需计算这一个新 token 的 K/V,然后拼到历史缓存上做 attention。
性能差异有多大?对于一个 prompt 1000 token、生成 100 token 的请求:
无 KV Cache:总计算量 ≈ Σ(1000+i)² ≈ 1.1 × 10⁸
有 KV Cache:总计算量 ≈ 1000² + 100 × 1000 ≈ 1.1 × 10⁶
差 100 倍。这就是为什么任何工业级推理框架都必须支持 KV Cache。
5.2 KV Cache 的代价:显存
KV Cache 不是免费午餐——它会吃掉大量显存。
KV Cache 显存公式:
KV_cache_bytes = 2 × batch × seq_len × num_layers × num_kv_heads × head_dim × dtype_size以Llama-3-70B为例:
num_layers= 80num_kv_heads= 8(GQA 后)head_dim= 128dtype_size= 2(FP16)
一个请求处理 32K 上下文:
2 × 1 × 32768 × 80 × 8 × 128 × 2 = ~10.7 GB注意:这只是单个请求的 KV Cache。如果你想并发 10 个请求,KV Cache 总占用是 107 GB——比模型权重本身(140 GB)的占用还要可观。
这就是为什么vLLM 的 PagedAttention横空出世:把 KV Cache 当作"虚拟内存"管理,避免碎片化和过度预分配。详见系列第 11 篇:推理加速三板斧。
5.3 上下文长度的代价:O(n²) 的诅咒
Attention 的核心计算QK^T是一个n × n的矩阵。这意味着:
上下文翻倍,attention 计算量变成4 倍
上下文 10 倍,attention 计算量变成100 倍
这就是为什么大家拼命要"扩上下文",但每多一个数量级都极其困难。
几个数字感受一下(同样是 Llama-3-70B,FP16):
上下文长度 | KV Cache 显存 | Attention 计算量(相对) |
4K | 1.3 GB | 1× |
32K | 10.7 GB | 64× |
128K | 43 GB | 1024× |
1M | 335 GB | 65536× |
1M 上下文不是"扩个参数"就能做到,它需要:
Flash Attention 优化 IO(系列第 13 篇)
Ring Attention 分布式注意力(系列第 15 篇)
YaRN 等位置编码外推(系列第 15 篇)
极其精细的显存管理
5.4 显存的三巨头:模型权重 + KV Cache + 激活值
实际部署一个大模型,显存被三部分吃掉:
显存 = 模型权重 + KV Cache + 激活值 + 临时 Buffer以A100 80G 部署 Llama-3-70B FP16为例:
项 | 占用 | 说明 |
模型权重 | ~140 GB | 不可省,需 2 卡 |
KV Cache (32K, batch=4) | ~43 GB | 可压缩(量化、PagedAttention) |
激活值 | ~10 GB | 推理时可优化 |
Buffer 等 | ~5 GB | 框架开销 |
一张 A100 80G 跑不动——所以工业部署必须考虑:
量化压缩模型权重(INT8 / INT4,系列第 12 篇)
KV Cache 量化(INT8)
张量并行(TP)跨卡分割模型(系列第 20 篇)
六、变体与演进:Transformer 还能怎么变
理解了原版 Transformer,再来看主流变体就轻松了。
6.1 三大架构的胜负已分
Encoder-only ──── 适合理解(BERT),渐弱 Encoder-Decoder ── 适合翻译(T5),淡出 Decoder-only ──── 适合生成(GPT/Llama/Claude),主流未来 5 年,主流大模型几乎全是 Decoder-only。
6.2 部件级的演进
主流大模型相对原版 Transformer 的"换件清单":
部件 | 原版 (2017) | 当下主流 (2026) |
归一化 | LayerNorm | RMSNorm |
位置编码 | Sinusoidal | RoPE |
激活函数 | ReLU/GELU | SwiGLU |
Attention | MHA | GQA / MQA |
LN 位置 | Post-LN | Pre-LN |
FFN 结构 | 单分支 | MoE (部分) |
每个改动看似微小,但叠加起来让模型效率提升数倍。
6.3 替代架构:Mamba、Linear Attention 等
近两年也有一些"挑战 Transformer"的架构:
Mamba / S4:状态空间模型,O(n) 复杂度
Linear Attention:把 attention 改造成线性复杂度
RWKV:RNN 风格但能并行训练
现状判断:这些架构在特定场景(长序列、低算力)有优势,但目前仍未撼动 Transformer 的主导地位。原因是:
工程生态(CUDA kernel、推理框架)全部围绕 Transformer 建立
Scaling Law 在 Transformer 上经过充分验证
模型能力上限尚未看到瓶颈
对工程师的建议:当下深入 Transformer,关注但不押注替代架构。
七、结语:地基打稳了,后面就好办了
Transformer 是大模型时代最重要的"地基"。它本身不复杂——核心就是几个矩阵乘法 + 残差 + 归一化——但它支撑的整个工程栈极其丰富。
读完本文,你应该已经能回答开头那几个问题了:
KV Cache 缓存了什么?历史 token 的 K 和 V 矩阵,避免重复计算。
上下文为什么贵?Attention 是 O(n²),从 4K 到 1M 计算量增长 65536 倍。
为什么所有推理框架都围着 attention 转?因为 attention 是计算 + 显存的双重瓶颈。
为什么 Decoder-only 赢?任务统一、Scaling 友好、In-Context Learning 涌现。
如果你对某个部分还有疑问,强烈建议把第三节的 100 行代码亲手敲一遍跑一下。架构这种东西,看十遍不如手写一遍。
-------------------------------------------------
参考文献:一文读懂 Transformer:从 Attention 到 LLM 的核心架构