【学习笔记】一文读懂 Transformer:从 Attention到LLM 的核心架构(2/35)
一、前言
在上一篇《大模型时代全景图》里,我们梳理了从 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= 8192 -
num_heads= 64 -
head_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.95M
12M 参数的 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= 80 -
num_kv_heads= 8(GQA 后) -
head_dim= 128 -
dtype_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 行代码亲手敲一遍跑一下。架构这种东西,看十遍不如手写一遍。
-------------------------------------------------
更多推荐









所有评论(0)