大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer 大模型处理文本的全流程解析(一)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(二)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(四)-CSDN博客
11. 多头注意力:从多个角度理解上下文
实际 Transformer 不只有一组 Q、K、V,而是有多组。
这就是多头注意力。
不同的头可能关注不同信息:
bash
Head 1 可能关注语法关系
Head 2 可能关注指代关系
Head 3 可能关注位置关系
Head 4 可能关注语义相似关系
类比:
bash
看一幅画,
有人看构图,
有人看色彩,
有人看笔触,
有人看主题。
每个头就是一个观察角度。
11.1 维度怎么切
多头注意力不是把维度复制很多份,而是把总维度切成多个小维度。
假设:
| 项目 | 数值 |
|---|---|
模型总维度 d_model |
4096 |
头数 h |
32 |
每个头维度 d_head |
4096 / 32 = 128 |
每个头在 128 维的子空间里工作。
总参数量不会因为头数变多而变成 32 倍。
11.2 GQA:现代大模型的注意力优化
标准多头注意力 MHA 中:
bash
Q、K、V 各有 h 个头。
但许多现代大模型使用:
bash
GQA,Grouped Query Attention
MQA,Multi-Query Attention
目的之一是减少 KV Cache。
| 注意力类型 | Q 头数 | K/V 头数 | 特点 |
|---|---|---|---|
| MHA,标准 | 32 | 32 | 经典,KV Cache 大 |
| GQA,分组 | 32 | 4 或 8 | 减少 KV Cache |
| MQA,极端 | 32 | 1 | KV Cache 最小,精度可能略降 |
各模型实际配置差异较大。
常见示例:
| 模型 | Q heads | KV heads | 注意力类型 |
|---|---|---|---|
| LLaMA-7B,原版 | 32 | 32 | 标准 MHA |
| LLaMA-2-7B | 32 | 32 | 标准 MHA |
| LLaMA-3-8B | 32 | 8 | GQA |
| Mistral-7B | 32 | 8 | GQA |
| Qwen2-7B | 28 | 4 | GQA |
MLOps 视角:
具体请以 HuggingFace config.json 中字段为准:
bash
num_attention_heads
num_key_value_heads
其中:
bash
num_key_value_heads 直接决定 KV Cache 显存大小。
11.3 多头结果如何合并
每个头得到一个输出向量。
所有头的输出拼接起来,再经过一个线性投影矩阵 W_O:
bash
MultiHead(Q, K, V)
= Concat(head_1, ..., head_h) · W_O
其中每个头:
bash
head_i = Attention(Q_i, K_i, V_i)
例如:
bash
32 个头 × 128 维 = 4096 维
经过:
bash
W_O
仍然得到:
bash
4096 维
W_O 的作用是:
bash
把不同子空间的信息融合回统一的主维度空间。
11.4 GQA 为什么能减少 KV Cache
标准 MHA 中:
python
如果有 32 个 Q 头,
通常也有 32 个 K 头和 32 个 V 头。
也就是:
python
Q heads = 32
K heads = 32
V heads = 32
每个头都有自己的 K、V。
推理时,每一层都要缓存这些 K、V。
这就是 KV Cache。
现代模型为了降低推理显存,经常使用 GQA。
例如:
python
Q heads = 32
KV heads = 8
这意味着:
python
K 和 V 只有 8 组。
32 个 Q 头会分组共享这 8 组 KV。
例如:
python
Q head 0~3 共享 KV head 0
Q head 4~7 共享 KV head 1
Q head 8~11 共享 KV head 2
...
这样做的直接好处是:
KV Cache 显存大约降低到原来的:
python
8 / 32 = 1/4
相比标准 MHA,显存压力明显下降。
所以在长上下文、高并发推理场景中,GQA 非常常见。
MLOps 视角 :
估算 KV Cache 时,必须使用:
pythonnum_key_value_heads而不是:
pythonnum_attention_heads否则很容易严重高估或低估显存。
12. 残差连接与归一化
Transformer 不是一层就结束。
实际模型通常有几十层。
为了让深层网络能训练,每层通常会使用:
bash
残差连接
归一化
12.1 残差连接,Residual Connection
公式:
bash
X_out = X + F(X)
其中 F 可以是注意力模块,也可以是 FFN。
类比:
bash
你抄写一篇文章,这是原始信息 X。
然后在旁边写批注,这是 F(X)。
最终你手里有原文 + 批注。
即使批注写得不好,原文还在,信息不会丢。
如果没有残差连接,深层网络中信息可能:
bash
逐层衰减
逐层爆炸
难以训练
12.2 归一化:LayerNorm 与 RMSNorm
归一化的作用是把向量各维度的数值拉到一个更稳定的范围,缓解层间数值范围变化过大的问题。
类比:
bash
每次考试后把分数标准化。
避免有的科目满分 100,有的满分 1000,导致计算失衡。
标准 LayerNorm
原始 Transformer 常用 LayerNorm。
公式:
bash
LayerNorm(x)
= γ ⊙ (x - μ) / sqrt(σ² + ε) + β
其中:
bash
μ 是均值
σ² 是方差
γ、β 是可学习参数
ε 是防止除零的极小值
RMSNorm
许多现代大模型使用 RMSNorm,例如 LLaMA、Qwen、Mistral 等常见模型。
公式:
bash
RMSNorm(x)
= γ ⊙ x / sqrt((1 / d) · sum_i x_i² + ε)
它去掉了 LayerNorm 中"减去均值"的步骤,只做缩放。
可以理解为:
bash
LayerNorm:中心化 + 缩放
RMSNorm:只做缩放
效果通常接近,但 RMSNorm 计算更简单。
MLOps 视角:
RMSNorm 省去求均值的计算,在 GPU 上更容易与前面的线性层进行算子融合。
如果你看 vLLM、TensorRT-LLM、SGLang 等推理框架源码,会看到很多定制的:
bash
rms_norm CUDA kernel
这是推理加速的关键一环。
12.3 Pre-LN 与 Post-LN
不同模型归一化的位置不同。
Pre-LN,当前主流
bash
X + Attention(RMSNorm(X))
以及:
bash
X + FFN(RMSNorm(X))
Post-LN,原始 Transformer
bash
LayerNorm(X + Attention(X))
当前主流开源 LLM 多使用:
bash
Pre-LN + RMSNorm
但并非绝对,具体以模型实现为准。
12.4 主流 decoder block 的伪代码表达
以当前常见的 Pre-LN + RMSNorm 结构为例,一个 Transformer block 可以写成:
python
residual = x
x = rms_norm(x)
x = attention(x)
x = residual + x
residual = x
x = rms_norm(x)
x = ffn(x)
x = residual + x
也可以简写成:
python
x = x + Attention(RMSNorm(x))
x = x + FFN(RMSNorm(x))
这里的 Attention 内部包括:
python
Q/K/V 投影
RoPE 或其他位置信息注入
QK^T
缩放
mask
softmax
加权 V
多头合并
W_O 输出投影
FFN 内部包括:
python
升维
门控或激活函数
降维
MLOps 视角 :
RMSNorm 虽然看起来只是一个小操作,但它在每一层都会执行。
所以在高性能推理框架中,经常会把 RMSNorm 和前后算子做融合,减少 kernel launch 和显存读写。
例如:
pythonfused rms norm fused add rms norm fused attention output rms norm这些优化对 Decode 阶段尤其重要,因为 Decode 阶段经常是 memory-bound。
13. 前馈网络 FFN:每个 Token 自己的加工
Transformer 每一层除了注意力,还有一个前馈网络 FFN,也叫 MLP。
13.1 注意力和 FFN 的分工
| 模块 | 作用 | 类比 |
|---|---|---|
| Attention | 让 token 之间互相交流信息 | 查资料、问别人 |
| FFN | 对每个 token 的表示做进一步非线性加工 | 自己思考、消化、总结 |
注意力负责上下文中"找信息"。
FFN 负责把找来的信息进一步加工。
13.2 传统 FFN vs 现代 FFN,SwiGLU
传统 FFN
原始 Transformer、BERT 常用:
bash
FFN(x) = W_2 · ReLU(W_1 x + b_1) + b_2
两个大矩阵:
bash
W_1
W_2
结构是:
bash
升维 → 激活 → 降维
例如:
bash
4096 → 16384 → 4096
现代 FFN,SwiGLU
LLaMA、Qwen、Mistral 等模型常见 SwiGLU 风格 FFN。
公式可以写成:
bash
FFN(x)
= (SiLU(x · W_gate) ⊗ (x · W_up)) · W_down
三个大矩阵:
bash
W_gate
W_up
W_down
其中:
bash
⊗ 表示逐元素相乘。
门控机制可以理解为:
bash
网络自己学会哪些信息该通过,哪些信息该抑制。
W_gate 决定开关,W_up 提供内容。
两者逐元素相乘后,再由 W_down 降维。
13.3 为什么 SwiGLU 的隐藏层维度不是 4 倍?
传统 FFN 用 2 个大矩阵。
如果:
bash
d_model = 4096
d_ff = 4 × d_model = 16384
参数量是:
bash
2 × 4096 × 16384
SwiGLU 用 3 个大矩阵。
为了保持总参数量大致相当,隐藏层维度通常缩小为约:
bash
8 / 3 × d_model
例如:
| 类型 | d_model |
FFN 隐藏层维度 | 矩阵数 | 参数量 |
|---|---|---|---|---|
| 传统 FFN,4× | 4096 | 16384 | 2 | 2 × 4096 × 16384 ≈ 134M |
| SwiGLU,约 8/3× | 4096 | 11008 | 3 | 3 × 4096 × 11008 ≈ 135M |
两者参数量接近:
bash
3 × 4096 × 11008 ≈ 2 × 4096 × 16384
MLOps 视角:
当你在 HuggingFace 的 config.json 中看到:
bash
"intermediate_size": 11008
不要奇怪为什么不是:
bash
4096 × 4 = 16384
因为这可能使用的是 SwiGLU 风格 FFN,有 3 个矩阵。
13.4 FFN 参数量占比
如果以一个标准 decoder block 为例,并假设:
bash
d_model = 4096
Attention 为标准 MHA,即 KV 头数等于 Q 头数
FFN 为 SwiGLU,intermediate_size = 11008
大致参数如下:
| 模块 | 参数量 | 计算 |
|---|---|---|
| Attention | ≈ 67M | 4 × 4096² |
| FFN,SwiGLU | ≈ 135M | 3 × 4096 × 11008 |
| 合计 | ≈ 202M |
FFN 占比:
bash
135 / 202 ≈ 67%
所以在标准 MHA 的 block 中:
bash
FFN 通常占单个 Transformer block 参数量的约三分之二。
如果使用 GQA:
bash
Attention 参数会更少。
FFN 占比可能更高。
整模型的 FFN 占比还要看:
bash
词表大小
embedding 大小
是否 Weight Tying
是否使用 MoE
是否有额外视觉 / 音频模块
常见范围大致是:
bash
约一半到三分之二,甚至更高。
完整的一层包括:
bash
注意力
FFN
残差连接
归一化
不能把 Transformer 简单理解成"只有注意力"。
13.5 SwiGLU 更清晰的公式表达
现代开源大模型常见 SwiGLU 风格 FFN。
它可以理解成三个矩阵:
python
W_gate
W_up
W_down
计算过程可以写成:
python
h_gate = x · W_gate
h_up = x · W_up
h = SiLU(h_gate) ⊗ h_up
output = h · W_down
其中:
python
SiLU 是激活函数。
⊗ 表示逐元素相乘。
W_gate 控制"哪些信息应该通过"。
W_up 提供"通过的内容"。
W_down 再把高维中间表示映射回模型主维度。
也就是说:
python
FFN 不只是简单升维降维。
它还通过门控机制让网络学会选择性地保留或抑制信息。
为什么 intermediate_size 经常不是 hidden_size 的 4 倍?
传统 FFN 通常有两个大矩阵:
python
W_1
W_2
如果:
python
hidden_size = 4096
FFN 中间维度经常是:
python
4 × 4096 = 16384
参数量大约是:
python
2 × 4096 × 16384
SwiGLU 有三个大矩阵:
python
W_gate
W_up
W_down
为了让总参数量不要增加太多,中间维度通常会缩小。
理论上大约是:
python
intermediate_size ≈ 8/3 × hidden_size
例如:
python
hidden_size = 4096
那么:
python
8/3 × 4096 ≈ 10922.67
但实际工程中,维度通常要对齐,方便 GPU 计算。
所以常见取值可能是:
python
11008
这就是为什么你在 config.json 里看到:
python
"hidden_size": 4096,
"intermediate_size": 11008
不要奇怪它不是 16384。
这通常说明模型使用了 SwiGLU 风格 FFN。
14. Transformer Block 与多层堆叠
14.1 一个 Block 的结构
以当前主流的 Pre-LN + RMSNorm 为例:
bash
X_1 = X + Attention(RMSNorm(X))
X_2 = X_1 + FFN(RMSNorm(X_1))
一层可以理解为:
bash
归一化
注意力
残差
归一化
FFN
残差
14.2 多层堆叠
实际大模型会把 Block 堆叠很多层。
| 模型规模 | 层数 |
|---|---|
| 小型 | 几层到十几层 |
| 中型 | 几十层 |
| 大型 | 几十层到上百层 |
常见配置:
bash
24 层
32 层
40 层
80 层
96 层
粗略理解:
| 层级 | 可能学到的内容 |
|---|---|
| 浅层 | 局部搭配、词法、简单共现 |
| 中层 | 短语结构、句法关系 |
| 深层 | 抽象语义、指代、推理倾向 |
注意:这只是经验性理解,并不是模型内部有明确分层规则。
14.3 模型参数量的直观感受
| 模型 | 参数量 | FP16 显存,仅权重 | 典型层数 | d_model |
|---|---|---|---|---|
| 1B | 10 亿 | ~2 GB | ~24 层 | 2048 |
| 7B | 70 亿 | ~14 GB | 32 层 | 4096 |
| 13B | 130 亿 | ~26 GB | 40 层 | 5120 |
| 70B | 700 亿 | ~140 GB | 80 层 | 8192 |
快速估算:
bash
参数量 × 2 字节,FP16/BF16 ≈ 权重显存
但推理时还要加上:
bash
KV Cache
激活值
临时缓冲
14.4 config.json:MLOps 必看的模型配置字段
部署任何模型前,第一件事就是看 config.json。
| 字段 | 含义 | MLOps 关注点 |
|---|---|---|
hidden_size |
模型主维度 d_model |
决定向量宽度 |
num_hidden_layers |
Transformer 层数 | 影响深度和显存 |
num_attention_heads |
Q 头数 | 注意力并行切分 |
num_key_value_heads |
KV 头数 | 直接决定 KV Cache 大小 |
intermediate_size |
FFN 中间维度 | 判断 FFN 参数量 |
vocab_size |
词表大小 | embedding / LM Head 显存 |
tie_word_embeddings |
是否绑定输入输出 embedding | 影响显存 |
max_position_embeddings |
最大位置长度 | 长上下文参考 |
rope_theta / rope_scaling |
RoPE 相关参数 | 长文本外推 |
torch_dtype |
权重精度 | FP16/BF16/FP32 显存估算 |
hidden_act |
激活函数类型 | 辅助判断 FFN / 激活风格 |
14.5 一个 Block 的完整信息流
以一个 token 序列为例:
python
输入:
X: [batch_size, seq_len, hidden_size]
第一步:归一化
python
输入:
X: [batch_size, seq_len, hidden_size]
第二步:注意力
python
attn_output = Attention(normed_x)
Attention 内部会完成:
python
Q = normed_x · W_Q
K = normed_x · W_K
V = normed_x · W_V
如果是 RoPE:
python
对 Q、K 施加旋转位置编码。
然后:
python
scores = Q K^T / sqrt(d_k)
scores = mask(scores)
weights = softmax(scores)
attn_output = weights · V
多头结果拼接后:
python
attn_output = attn_output · W_O
第三步:第一次残差连接
python
X = X + attn_output
第四步:第二次归一化
python
normed_x = RMSNorm(X)
第五步:FFN
python
ffn_output = FFN(normed_x)
第六步:第二次残差连接
python
X = X + ffn_output
最终:
python
X: [batch_size, seq_len, hidden_size]
这个输出会传给下一层 Transformer Block。
注意:
python
每一层之后,token 的向量都会被更新。
同一个 token 在第 1 层、第 10 层、第 30 层的表示可能完全不同。
这也是为什么:
python
初始 embedding 只是起点。
深层 hidden state 才是模型真正用于预测和理解的语义表示。
15. 训练目标:预测下一个 Token
15.1 训练数据有多大
现代大模型通常用几万亿 token 的文本训练。
直观感受:
bash
LLaMA-2 用了约 2 万亿 token。
一些更大的模型使用 10 万亿+ token。
"大模型"的"大",既指:
bash
参数量大
也指:
bash
训练数据大
15.2 训练任务
给模型一句话:
bash
我 吃 鱼
训练时会同时学习多个位置的预测。
| 输入上下文 | 目标下一个 token |
|---|---|
| 我 | 吃 |
| 我 吃 | 鱼 |
| 我 吃 鱼 | [EOS] 或结束符号 |
模型输出词表上的概率分布。
如果真实下一个 token 是"鱼",模型就应该给"鱼"更高概率。
15.3 交叉熵损失
单个位置的 loss 可以理解为:
bash
loss = -log p(y)
其中:
bash
p(y) 是模型给正确 token 的概率。
示例:
| 模型给正确 token 的概率 | 损失 |
|---|---|
| 0.9 | 较低 |
| 0.5 | 中等 |
| 0.01 | 很高 |
训练就是不断调整参数,让 loss 越来越小。
15.4 哪些参数会被更新
全参数训练时,通常会更新:
bash
embedding table
可学习位置编码,如果有
W_Q
W_K
W_V
W_O
FFN 参数,例如 W_gate、W_up、W_down
归一化参数,例如 RMSNorm 的 γ
LM Head 参数
注意:
bash
RoPE 的基础旋转参数通常不是逐位置学习的。
但有些模型会有 rope_scaling 或长上下文微调配置。
15.5 训练时所有位置可以并行
虽然生成文本时,模型是一个 token 一个 token 往后生成的:
bash
我 → 今天 → 很 → 开心
但在训练时,并不需要一个 token 一个 token 串行跑。
由于因果掩码的存在,一次前向传播可以同时计算所有位置的预测。
例如句子:
bash
我 吃 鱼
训练时可以在一次前向传播中同时学习:
| 输入位置 | 可见上下文 | 目标下一个 token |
|---|---|---|
| 第 1 个位置 | 我 | 吃 |
| 第 2 个位置 | 我 吃 | 鱼 |
| 第 3 个位置 | 我 吃 鱼 | [EOS] 或下一个 token |
所以:
bash
训练并行,生成串行。
这是 Transformer 训练效率高的关键原因之一。
15.6 Teacher Forcing:训练时为什么可以并行
训练大模型时,通常使用 Teacher Forcing。
意思是:
python
训练时,模型不需要真的自己生成下一个 token。
训练数据已经提供了正确的上下文。
例如完整句子是:
python
今天天气很好
训练时会构造这样的学习任务:
python
输入:今天
目标:天气
输入:今天天气
目标:很
输入:今天天气很
目标:好
输入:今天天气很好
目标:EOS 或下一个 token
因为有因果掩码,模型在一次前向传播中,可以同时计算所有位置的预测。
这就是训练高效的重要原因。
对比推理:
python
推理时没有标准答案。
模型必须先生成第 1 个 token,
才能把它作为上下文生成第 2 个 token。
所以:
python
训练可以高度并行。
生成必须逐步串行。
MLOps 视角 :
训练瓶颈通常是:
python算力 通信 优化器显存 激活值推理瓶颈通常是:
pythonKV Cache 显存带宽 batch 调度 延迟指标
16. 生成下一个 Token
16.1 输出层,LM Head
最后一层输出的向量是:
bash
h_last
维度通常是:
bash
d_model
通过 LM Head 映射到词表大小:
bash
logits = h_last · W_LM_Head
再经过 softmax:
bash
probs = softmax(logits)
得到词表上的概率分布。
16.2 权重绑定,Weight Tying
部分模型中,输入 Embedding 表和输出 LM Head 共享同一套权重。
这叫做:
bash
Weight Tying
输入时:
bash
token id 查表得到向量
输出时:
bash
最后一层向量直接和这张表做点积,算出词表概率
是否绑定取决于具体模型。
查看:
bash
"tie_word_embeddings": true
或:
bash
"tie_word_embeddings": false
常见情况:
bash
GPT-2 系列通常绑定。
LLaMA 系列很多实现不绑定。
不同开源模型需要具体看 config。
MLOps 视角:
Weight Tying 直接影响显存估算。
假设:
bash
vocab_size = 128K
hidden_size = 4096
精度 = BF16/FP16
一张表的参数量:
bash
128000 × 4096 ≈ 524M
显存约:
bash
524M × 2 bytes ≈ 1.05GB
如果不绑定:
bash
Embedding + LM Head ≈ 2.1GB
如果绑定:
bash
约 1.05GB
大约省下 1GB 显存。
16.3 生成过程
自回归生成过程如下:
bash
输入:今天天气
模型预测:很
输入变成:今天天气很
模型预测:好
最终生成:今天天气很好
每一步只生成一个 token。
16.4 常见生成策略
| 策略 | 说明 |
|---|---|
| greedy | 每次选概率最高的 token |
| top-k | 从前 k 个候选中采样 |
| top-p,nucleus | 从累积概率达到 p 的候选中采样 |
| temperature | 调整概率分布的平滑程度 |
temperature 的具体数字感受
假设原始 logits 是:
bash
[5.0, 2.0, 1.0]
| temperature | 实际计算 | softmax 后概率 | 效果 |
|---|---|---|---|
| 0.1 | softmax([50, 20, 10]) |
[≈1.00, ≈0.00, ≈0.00] |
几乎 greedy |
| 1.0 | softmax([5, 2, 1]) |
[0.936, 0.047, 0.017] |
原始分布 |
| 2.0 | softmax([2.5, 1.0, 0.5]) |
[0.736, 0.164, 0.100] |
更平滑 |
| 3.0 | softmax([1.67, 0.67, 0.33]) |
[0.612, 0.225, 0.162] |
更随机 |
规律:
bash
temperature 越低,越确定。
temperature 越高,越随机。
16.5 停止条件和特殊 token
模型生成文本时,不是一直无限生成下去。
通常需要在满足某个条件时停止。
常见停止条件包括:
python
生成了 EOS token。
达到了 max_new_tokens。
出现了用户指定的 stop strings。
生成了特定 chat 模板中的结束标记。
MLOps 视角 :
部署 chat 模型时,必须清楚模型使用哪些特殊 token。
例如:
pythonBOS EOS PAD system token user token assistant token tool token function call token不同模型差异很大。
例如有些模型使用:
python<|im_start|> <|im_end|>有些模型使用:
python<|system|> <|user|> <|assistant|>有些模型使用:
python[INST] [/INST]如果 chat template 用错,可能导致:
python模型不遵循角色设定 回复格式混乱 无法正常停止 重复生成 工具调用失败所以在部署前,一定要查看
tokenizer_config.json里的:
pythonchat_template并且优先使用:
pythontokenizer.apply_chat_template()不要凭感觉手拼 prompt。