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