772 lines
23 KiB
Markdown
772 lines
23 KiB
Markdown
# 第 8 课:MiniMindConfig 与参数规模
|
||
|
||
这一课只解决一个问题:`MiniMindConfig` 里的配置项,如何决定模型结构、张量 shape、参数量和权重兼容性。
|
||
|
||
## 目录
|
||
|
||
- [0. 本节主线](#l08-mainline)
|
||
- [1. 本节要懂的 6 个原理](#l08-principles)
|
||
- [2. 变量流转](#l08-flow)
|
||
- [3. 原理一:Config 是模型结构合同](#l08-config-contract)
|
||
- [4. 原理二:`vocab_size * hidden_size` 是 token 矩阵大小](#l08-vocab-hidden)
|
||
- [5. 原理三:`num_hidden_layers` 是 block 的复制次数](#l08-layers)
|
||
- [6. 原理四:attention head 决定 Q/K/V 的拆分方式](#l08-attention-heads)
|
||
- [7. 原理五:`intermediate_size` 主要影响 FFN 参数量](#l08-intermediate-size)
|
||
- [8. 原理六:改 config 会影响权重加载](#l08-weight-loading)
|
||
- [9. 实验验证](#l08-experiment)
|
||
- [10. 本节检查](#l08-check)
|
||
- [11. 下一课](#l08-next)
|
||
|
||
<a id="l08-mainline"></a>
|
||
## 0. 本节主线
|
||
|
||
模型配置的本质是:
|
||
|
||
```text
|
||
MiniMindConfig
|
||
-> 决定 vocab_size / hidden_size / num_hidden_layers / heads / intermediate_size
|
||
-> 创建 embedding、N 个 block、norm、lm_head
|
||
-> 决定每个参数矩阵的 shape
|
||
-> 决定总参数量
|
||
-> 决定某个 .pth 权重能不能加载进来
|
||
```
|
||
|
||
所以这节课开始正式进入模型本体。不要把 `hidden_size=768`、`num_hidden_layers=8` 当成普通超参数,它们会直接改变模型里矩阵的形状。
|
||
|
||
<a id="l08-principles"></a>
|
||
## 1. 本节要懂的 6 个原理
|
||
|
||
| 原理 | 要理解什么 | 源码证据 |
|
||
|---|---|---|
|
||
| Config 是模型结构合同 | `MiniMindConfig` 保存的不是训练技巧,而是模型骨架参数 | `model/model_minimind.py:10-45`, `minimind-3/config.json:8-37` |
|
||
| `vocab_size * hidden_size` 是 token 矩阵大小 | embedding 和 lm_head 的 shape 都受词表和隐藏维度控制 | `model/model_minimind.py:196-204`, `model/model_minimind.py:234-243` |
|
||
| `num_hidden_layers` 是 block 的复制次数 | 每一层 block 都有自己独立的 Attention、Norm、MLP 参数 | `model/model_minimind.py:178-185`, `model/model_minimind.py:196-204` |
|
||
| attention head 决定 Q/K/V 的拆分方式 | `num_attention_heads`、`num_key_value_heads`、`head_dim` 决定投影矩阵和中间 shape | `model/model_minimind.py:22-24`, `model/model_minimind.py:91-116` |
|
||
| `intermediate_size` 主要影响 FFN 参数量 | FFN 有 3 个大矩阵,通常是每层 block 的重要参数来源 | `model/model_minimind.py:25-26`, `model/model_minimind.py:136-145` |
|
||
| 改 config 会影响权重加载 | `.pth` 里的 tensor shape 必须和当前模型 shape 对得上 | `trainer/trainer_utils.py:119-127` |
|
||
|
||
学完本节,你应该能解释:为什么 MiniMind 的 `64M` 不是一个魔法数字,而是由词表、隐藏维度、层数、Attention 和 FFN 矩阵共同堆出来的。
|
||
|
||
<a id="l08-flow"></a>
|
||
## 2. 变量流转
|
||
|
||
从 config 到模型参数:
|
||
|
||
```text
|
||
vocab_size, hidden_size
|
||
-> nn.Embedding(vocab_size, hidden_size)
|
||
-> input_ids [batch, seq]
|
||
-> hidden_states [batch, seq, hidden_size]
|
||
|
||
num_hidden_layers
|
||
-> ModuleList([MiniMindBlock(...) for _ in range(num_hidden_layers)])
|
||
-> hidden_states 依次经过 N 个 block
|
||
|
||
num_attention_heads, num_key_value_heads, head_dim
|
||
-> q_proj / k_proj / v_proj / o_proj
|
||
-> attention 内部拆成多个 head
|
||
|
||
intermediate_size
|
||
-> FeedForward 的 gate_proj / up_proj / down_proj
|
||
|
||
hidden_size, vocab_size
|
||
-> lm_head(hidden_states)
|
||
-> logits [batch, seq, vocab_size]
|
||
```
|
||
|
||
最重要的三个 shape:
|
||
|
||
```text
|
||
input_ids: [batch_size, seq_len]
|
||
hidden_states: [batch_size, seq_len, hidden_size]
|
||
logits: [batch_size, seq_len, vocab_size]
|
||
```
|
||
|
||
<a id="l08-config-contract"></a>
|
||
## 3. 原理一:Config 是模型结构合同
|
||
|
||
### 原理讲解
|
||
|
||
`MiniMindConfig` 不是“训练参数表”,而是“模型结构合同”。
|
||
|
||
训练参数像 `learning_rate`、`batch_size`,改变的是训练方式;结构参数像 `hidden_size`、`num_hidden_layers`、`vocab_size`,改变的是模型里参数矩阵的形状。
|
||
|
||
比如:
|
||
|
||
```text
|
||
hidden_size=768
|
||
```
|
||
|
||
表示每个 token 在模型内部会被表示成 768 维向量。
|
||
|
||
```text
|
||
num_hidden_layers=8
|
||
```
|
||
|
||
表示模型主体里有 8 个 `MiniMindBlock`。
|
||
|
||
```text
|
||
vocab_size=6400
|
||
```
|
||
|
||
表示模型最后每个位置要在 6400 个 token id 上预测概率。
|
||
|
||
所以 config 一旦变了,模型结构也变了。结构变了,旧权重通常就不能直接加载。
|
||
|
||
### 源码证据 A:MiniMind 自己的配置类
|
||
|
||
文件:`model/model_minimind.py:10-45`
|
||
|
||
看它是为了理解:哪些字段会成为模型结构的一部分。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class MiniMindConfig(PretrainedConfig):
|
||
model_type = "minimind"
|
||
def __init__(self, hidden_size=768, num_hidden_layers=8, use_moe=False, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.hidden_size = hidden_size
|
||
self.num_hidden_layers = num_hidden_layers
|
||
self.use_moe = use_moe
|
||
self.dropout = kwargs.get("dropout", 0.0)
|
||
self.vocab_size = kwargs.get("vocab_size", 6400)
|
||
self.num_attention_heads = kwargs.get("num_attention_heads", 8)
|
||
self.num_key_value_heads = kwargs.get("num_key_value_heads", 4)
|
||
self.head_dim = kwargs.get("head_dim", self.hidden_size // self.num_attention_heads)
|
||
self.intermediate_size = kwargs.get("intermediate_size", math.ceil(hidden_size * math.pi / 64) * 64)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `hidden_size` 默认是 768。
|
||
- `num_hidden_layers` 默认是 8。
|
||
- `vocab_size` 默认是 6400。
|
||
- `head_dim` 默认由 `hidden_size // num_attention_heads` 推出来。
|
||
- `intermediate_size` 默认由 `hidden_size` 推出来。
|
||
|
||
### 源码证据 B:下载好的 `minimind-3` 配置
|
||
|
||
文件:`minimind-3/config.json:8-37`
|
||
|
||
看它是为了理解:真实下载模型使用的是哪组结构参数。
|
||
|
||
配置片段:
|
||
|
||
```json
|
||
{
|
||
"head_dim": 96,
|
||
"hidden_size": 768,
|
||
"intermediate_size": 2432,
|
||
"max_position_embeddings": 32768,
|
||
"num_attention_heads": 8,
|
||
"num_hidden_layers": 8,
|
||
"num_key_value_heads": 4,
|
||
"tie_word_embeddings": true,
|
||
"vocab_size": 6400
|
||
}
|
||
```
|
||
|
||
这段配置说明:
|
||
|
||
- 你下载的 `minimind-3` 是 `hidden_size=768`、`num_hidden_layers=8`。
|
||
- `head_dim=96`,所以 `8 heads * 96 = 768`。
|
||
- `vocab_size=6400`,和 MiniMind tokenizer 的词表规模对齐。
|
||
- `tie_word_embeddings=true`,表示 embedding 和输出头共享同一组 token 矩阵。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能说清楚:
|
||
|
||
```text
|
||
Config 决定模型结构;
|
||
训练参数决定怎么训练;
|
||
二者不是一回事。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- YaRN/RoPE 长上下文扩展细节。
|
||
- `PretrainedConfig` 在 Transformers 里的完整机制。
|
||
|
||
<a id="l08-vocab-hidden"></a>
|
||
## 4. 原理二:`vocab_size * hidden_size` 是 token 矩阵大小
|
||
|
||
### 原理讲解
|
||
|
||
LLM 里有一个很大的矩阵负责把 token id 变成向量:
|
||
|
||
```text
|
||
embedding matrix: [vocab_size, hidden_size]
|
||
```
|
||
|
||
如果:
|
||
|
||
```text
|
||
vocab_size = 6400
|
||
hidden_size = 768
|
||
```
|
||
|
||
那么这个矩阵参数量就是:
|
||
|
||
```text
|
||
6400 * 768 = 4,915,200
|
||
```
|
||
|
||
也就是约 `4.9M` 参数。
|
||
|
||
模型输出时还需要一个矩阵把 hidden state 映射回词表:
|
||
|
||
```text
|
||
lm_head: [vocab_size, hidden_size]
|
||
```
|
||
|
||
MiniMind 默认把 embedding 和 lm_head 绑定成同一组权重,所以这块参数通常只算一份。这叫 `tie_word_embeddings`。
|
||
|
||
### 源码证据 A:输入 embedding
|
||
|
||
文件:`model/model_minimind.py:196-204`
|
||
|
||
看它是为了理解:`input_ids` 进入模型后,第一块参数矩阵由谁创建。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class MiniMindModel(nn.Module):
|
||
def __init__(self, config: MiniMindConfig):
|
||
super().__init__()
|
||
self.config = config
|
||
self.vocab_size, self.num_hidden_layers = config.vocab_size, config.num_hidden_layers
|
||
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
|
||
self.dropout = nn.Dropout(config.dropout)
|
||
self.layers = nn.ModuleList([MiniMindBlock(l, config) for l in range(self.num_hidden_layers)])
|
||
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `embed_tokens` 的 shape 由 `vocab_size` 和 `hidden_size` 决定。
|
||
- `input_ids` 经过 embedding 后会变成 `hidden_states`。
|
||
- `num_hidden_layers` 同时在这里决定 block 数量。
|
||
|
||
### 源码证据 B:输出 lm_head 和权重绑定
|
||
|
||
文件:`model/model_minimind.py:234-243`
|
||
|
||
看它是为了理解:模型最后为什么能输出 `[batch, seq, vocab_size]` 的 logits。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class MiniMindForCausalLM(PreTrainedModel, GenerationMixin):
|
||
config_class = MiniMindConfig
|
||
_tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
|
||
def __init__(self, config: MiniMindConfig = None):
|
||
self.config = config or MiniMindConfig()
|
||
super().__init__(self.config)
|
||
self.model = MiniMindModel(self.config)
|
||
self.lm_head = nn.Linear(self.config.hidden_size, self.config.vocab_size, bias=False)
|
||
if self.config.tie_word_embeddings: self.model.embed_tokens.weight = self.lm_head.weight
|
||
self.post_init()
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `lm_head` 把 `hidden_size` 映射到 `vocab_size`。
|
||
- `tie_word_embeddings=True` 时,`embed_tokens.weight` 和 `lm_head.weight` 是同一组参数。
|
||
- 如果取消绑定,token 相关参数大约会多一份 `vocab_size * hidden_size`。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能回答:
|
||
|
||
```text
|
||
为什么 tokenizer 的 vocab_size 会影响模型大小?
|
||
因为 embedding 和 lm_head 都要覆盖整个词表。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- logits softmax 的数值稳定细节。
|
||
- 不同 tokenizer 的 BPB/PPL 对比。
|
||
|
||
<a id="l08-layers"></a>
|
||
## 5. 原理三:`num_hidden_layers` 是 block 的复制次数
|
||
|
||
### 原理讲解
|
||
|
||
Transformer 主体不是一个单独的大函数,而是很多层 block 叠起来。
|
||
|
||
MiniMind 里一个 block 大致是:
|
||
|
||
```text
|
||
RMSNorm
|
||
-> Attention
|
||
-> 残差
|
||
-> RMSNorm
|
||
-> FFN 或 MoE
|
||
-> 残差
|
||
```
|
||
|
||
`num_hidden_layers=8` 的含义不是“同一个 block 循环 8 次并共享参数”,而是创建 8 个独立的 `MiniMindBlock`。每一层都有自己的 Attention、Norm、MLP 参数。
|
||
|
||
所以层数增加时,参数量通常接近线性增加:
|
||
|
||
```text
|
||
总 block 参数量 ≈ num_hidden_layers * 单个 block 参数量
|
||
```
|
||
|
||
### 源码证据 A:一个 block 里有什么
|
||
|
||
文件:`model/model_minimind.py:178-185`
|
||
|
||
看它是为了理解:每一层 block 里包含哪些子模块。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class MiniMindBlock(nn.Module):
|
||
def __init__(self, layer_id: int, config: MiniMindConfig):
|
||
super().__init__()
|
||
self.self_attn = Attention(config)
|
||
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
self.post_attention_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
|
||
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- 每个 block 都有一个 Attention。
|
||
- 每个 block 有两个 RMSNorm。
|
||
- 每个 block 的 MLP 可以是普通 FFN,也可以是 MoE。
|
||
|
||
### 源码证据 B:创建多层 block
|
||
|
||
文件:`model/model_minimind.py:196-204`
|
||
|
||
看它是为了理解:`num_hidden_layers` 如何变成多层模型。
|
||
|
||
源码:
|
||
|
||
```python
|
||
self.layers = nn.ModuleList([MiniMindBlock(l, config) for l in range(self.num_hidden_layers)])
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `range(self.num_hidden_layers)` 循环几次,就创建几个 block。
|
||
- `ModuleList` 里的每个 block 都是独立模块。
|
||
- 改 `num_hidden_layers` 会改变模型参数数量和权重 key 数量。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能说清楚:
|
||
|
||
```text
|
||
num_hidden_layers 控制模型深度;
|
||
hidden_size 控制每层内部向量宽度;
|
||
二者都会影响参数量,但方式不同。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- 每一层学到的语义差异。
|
||
- 深层模型的训练稳定性问题。
|
||
|
||
<a id="l08-attention-heads"></a>
|
||
## 6. 原理四:attention head 决定 Q/K/V 的拆分方式
|
||
|
||
### 原理讲解
|
||
|
||
`hidden_size` 是 token 向量总宽度,attention 会把这个向量拆成多个 head。
|
||
|
||
在 `minimind-3` 里:
|
||
|
||
```text
|
||
hidden_size = 768
|
||
num_attention_heads = 8
|
||
head_dim = 96
|
||
```
|
||
|
||
所以:
|
||
|
||
```text
|
||
8 * 96 = 768
|
||
```
|
||
|
||
这表示 Query 被拆成 8 个 head,每个 head 96 维。
|
||
|
||
MiniMind 还用了 `num_key_value_heads=4`。这表示 K/V head 比 Q head 少,属于 Grouped Query Attention 的思路。直观理解是:Q 有 8 组,但 K/V 只有 4 组,后面再复用到 8 组 query 上。这样可以减少 K/V 相关参数和 cache 体积。
|
||
|
||
本节只要理解配置如何决定 shape,不展开 Attention 公式。Attention 的计算细节放到后面的专门课。
|
||
|
||
### 源码证据 A:head 配置从哪里来
|
||
|
||
文件:`model/model_minimind.py:22-24`
|
||
|
||
看它是为了理解:`head_dim` 默认如何计算。
|
||
|
||
源码:
|
||
|
||
```python
|
||
self.num_attention_heads = kwargs.get("num_attention_heads", 8)
|
||
self.num_key_value_heads = kwargs.get("num_key_value_heads", 4)
|
||
self.head_dim = kwargs.get("head_dim", self.hidden_size // self.num_attention_heads)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- 默认有 8 个 attention heads。
|
||
- 默认有 4 个 key/value heads。
|
||
- 默认 `head_dim = hidden_size // num_attention_heads`。
|
||
|
||
### 源码证据 B:Q/K/V/O 投影矩阵
|
||
|
||
文件:`model/model_minimind.py:91-103`
|
||
|
||
看它是为了理解:head 配置如何变成 Linear 层的输入输出维度。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class Attention(nn.Module):
|
||
def __init__(self, config: MiniMindConfig):
|
||
super().__init__()
|
||
self.num_key_value_heads = config.num_attention_heads if config.num_key_value_heads is None else config.num_key_value_heads
|
||
self.n_local_heads = config.num_attention_heads
|
||
self.n_local_kv_heads = self.num_key_value_heads
|
||
self.n_rep = self.n_local_heads // self.n_local_kv_heads
|
||
self.head_dim = config.head_dim
|
||
self.q_proj = nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False)
|
||
self.k_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
||
self.v_proj = nn.Linear(config.hidden_size, self.num_key_value_heads * self.head_dim, bias=False)
|
||
self.o_proj = nn.Linear(config.num_attention_heads * self.head_dim, config.hidden_size, bias=False)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `q_proj` 输出维度是 `num_attention_heads * head_dim`。
|
||
- `k_proj` 和 `v_proj` 输出维度是 `num_key_value_heads * head_dim`。
|
||
- `o_proj` 再把多头结果映射回 `hidden_size`。
|
||
- K/V head 更少时,K/V 投影参数会比 Q 投影少。
|
||
|
||
### 源码证据 C:forward 里的 head shape
|
||
|
||
文件:`model/model_minimind.py:111-116`
|
||
|
||
看它是为了理解:Linear 输出后如何 reshape 成多头格式。
|
||
|
||
源码:
|
||
|
||
```python
|
||
bsz, seq_len, _ = x.shape
|
||
xq, xk, xv = self.q_proj(x), self.k_proj(x), self.v_proj(x)
|
||
xq = xq.view(bsz, seq_len, self.n_local_heads, self.head_dim)
|
||
xk = xk.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)
|
||
xv = xv.view(bsz, seq_len, self.n_local_kv_heads, self.head_dim)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- attention 输入 `x` 的 shape 是 `[batch, seq, hidden_size]`。
|
||
- Q 被 reshape 成 `[batch, seq, num_attention_heads, head_dim]`。
|
||
- K/V 被 reshape 成 `[batch, seq, num_key_value_heads, head_dim]`。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能说清楚:
|
||
|
||
```text
|
||
num_attention_heads 不是层数;
|
||
它是在每层 attention 内部把 hidden 向量拆成多少组。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- scaled dot-product attention 的完整公式。
|
||
- causal mask 和 flash attention 分支。
|
||
- KV cache 的拼接过程。
|
||
|
||
<a id="l08-intermediate-size"></a>
|
||
## 7. 原理五:`intermediate_size` 主要影响 FFN 参数量
|
||
|
||
### 原理讲解
|
||
|
||
每个 Transformer block 除了 Attention,还有 FFN。
|
||
|
||
MiniMind 的普通 FFN 有三个矩阵:
|
||
|
||
```text
|
||
gate_proj: hidden_size -> intermediate_size
|
||
up_proj: hidden_size -> intermediate_size
|
||
down_proj: intermediate_size -> hidden_size
|
||
```
|
||
|
||
所以 FFN 参数量大致是:
|
||
|
||
```text
|
||
3 * hidden_size * intermediate_size
|
||
```
|
||
|
||
在很多 Transformer 里,FFN 是每层参数量的重要来源。`hidden_size` 增大时,`intermediate_size` 也会跟着变大,所以参数量会明显上升。
|
||
|
||
MoE 也是从这里切入的:如果 `use_moe=True`,block 里的 `mlp` 不再是一个普通 FFN,而是多个 expert FFN 加一个 router。本课只先知道开关在哪里,MoE 细节后面讲到 FFN 与 MoE 时再展开。
|
||
|
||
### 源码证据 A:`intermediate_size` 的默认计算
|
||
|
||
文件:`model/model_minimind.py:25-26`
|
||
|
||
看它是为了理解:FFN 中间维度默认不是随便写死的。
|
||
|
||
源码:
|
||
|
||
```python
|
||
self.hidden_act = kwargs.get("hidden_act", 'silu')
|
||
self.intermediate_size = kwargs.get("intermediate_size", math.ceil(hidden_size * math.pi / 64) * 64)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- 激活函数默认是 `silu`。
|
||
- `intermediate_size` 默认由 `hidden_size` 推导。
|
||
- hidden 越宽,FFN 中间层通常也越宽。
|
||
|
||
### 源码证据 B:普通 FFN 的三个矩阵
|
||
|
||
文件:`model/model_minimind.py:136-145`
|
||
|
||
看它是为了理解:FFN 参数量为什么和 `hidden_size * intermediate_size` 相关。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class FeedForward(nn.Module):
|
||
def __init__(self, config: MiniMindConfig, intermediate_size: int = None):
|
||
super().__init__()
|
||
intermediate_size = intermediate_size or config.intermediate_size
|
||
self.gate_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||
self.down_proj = nn.Linear(intermediate_size, config.hidden_size, bias=False)
|
||
self.up_proj = nn.Linear(config.hidden_size, intermediate_size, bias=False)
|
||
self.act_fn = ACT2FN[config.hidden_act]
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- FFN 有 `gate_proj`、`up_proj`、`down_proj` 三个 Linear。
|
||
- 这三个矩阵都和 `hidden_size`、`intermediate_size` 直接相关。
|
||
- 改 `hidden_size` 会同时影响 Attention 和 FFN。
|
||
|
||
### 源码证据 C:MoE 只先看开关
|
||
|
||
文件:`model/model_minimind.py:148-153`
|
||
|
||
看它是为了理解:MoE 是如何接在 FFN 位置上的。
|
||
|
||
源码:
|
||
|
||
```python
|
||
class MOEFeedForward(nn.Module):
|
||
def __init__(self, config: MiniMindConfig):
|
||
super().__init__()
|
||
self.config = config
|
||
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
|
||
self.experts = nn.ModuleList([FeedForward(config, intermediate_size=config.moe_intermediate_size) for _ in range(config.num_experts)])
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- MoE 在 FFN 位置引入多个 expert。
|
||
- `gate` 负责给 token 分配 expert。
|
||
- 本节先不展开 router、top-k、aux loss,只知道它会显著改变 FFN 部分的参数结构。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能回答:
|
||
|
||
```text
|
||
为什么 hidden_size 增大时,参数量不是只小幅增加?
|
||
因为 embedding、attention、FFN 都会被 hidden_size 影响,其中 FFN 还有 intermediate_size 放大。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- SwiGLU 的数学细节。
|
||
- MoE 的 token dispatch 和负载均衡 loss。
|
||
|
||
<a id="l08-weight-loading"></a>
|
||
## 8. 原理六:改 config 会影响权重加载
|
||
|
||
### 原理讲解
|
||
|
||
`.pth` 权重文件里保存的是一堆 tensor。
|
||
|
||
每个 tensor 都有名字和 shape,例如:
|
||
|
||
```text
|
||
model.embed_tokens.weight: [6400, 768]
|
||
model.layers.0.self_attn.q_proj.weight: [768, 768]
|
||
```
|
||
|
||
如果你把当前模型改成:
|
||
|
||
```text
|
||
hidden_size=512
|
||
```
|
||
|
||
那么新模型期待的 shape 就会变成另一套。旧权重里的 `[6400, 768]` 通常就装不进新模型的 `[6400, 512]`。
|
||
|
||
这里要注意:`strict=False` 可以容忍缺 key 或多 key,但不能神奇地把 shape 不一致的 tensor 变成可加载。结构参数变了,权重兼容性就要重新检查。
|
||
|
||
### 源码证据 A:加载模型时先按当前 config 创建结构
|
||
|
||
文件:`trainer/trainer_utils.py:119-127`
|
||
|
||
看它是为了理解:加载权重之前,模型结构已经由当前 `lm_config` 决定了。
|
||
|
||
源码:
|
||
|
||
```python
|
||
def init_model(lm_config, from_weight='pretrain', tokenizer_path='../model', save_dir='../out', device='cuda'):
|
||
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
|
||
model = MiniMindForCausalLM(lm_config)
|
||
|
||
if from_weight!= 'none':
|
||
moe_suffix = '_moe' if lm_config.use_moe else ''
|
||
weight_path = f'{save_dir}/{from_weight}_{lm_config.hidden_size}{moe_suffix}.pth'
|
||
weights = torch.load(weight_path, map_location=device)
|
||
model.load_state_dict(weights, strict=False)
|
||
```
|
||
|
||
这段代码说明:
|
||
|
||
- `MiniMindForCausalLM(lm_config)` 先创建当前结构。
|
||
- `torch.load` 只是读出旧权重。
|
||
- `load_state_dict` 要把旧权重塞进当前结构。
|
||
- 如果当前结构和旧权重 shape 对不上,就会失败。
|
||
|
||
### 理解到这一步就够
|
||
|
||
你应该能说清楚:
|
||
|
||
```text
|
||
改 hidden_size / vocab_size / layers 不是普通调参;
|
||
它会改变权重 shape,因此影响已有权重能不能加载。
|
||
```
|
||
|
||
暂时不用看:
|
||
|
||
- `state_dict` 里每个 key 的完整命名规则。
|
||
- 模型结构迁移和权重插值。
|
||
|
||
<a id="l08-experiment"></a>
|
||
## 9. 实验验证
|
||
|
||
### 实验 A:看 tiny config 的参数分解
|
||
|
||
这个实验验证:
|
||
|
||
```text
|
||
config
|
||
-> embedding/lm_head shape
|
||
-> block 参数量
|
||
-> attention 参数量
|
||
-> FFN 参数量
|
||
```
|
||
|
||
运行:
|
||
|
||
```bash
|
||
cd /home/sun/minimind
|
||
PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py
|
||
```
|
||
|
||
记录:
|
||
|
||
```text
|
||
hidden_size =
|
||
num_hidden_layers =
|
||
vocab_size =
|
||
embed_tokens.weight =
|
||
lm_head.weight =
|
||
total_unique_params =
|
||
token_matrix_params_once =
|
||
block0.self_attn =
|
||
block0.mlp =
|
||
```
|
||
|
||
你应该看到:
|
||
|
||
```text
|
||
embed_tokens.weight = (6400, 64)
|
||
lm_head.weight = (6400, 64)
|
||
embedding_and_lm_head_are_same_parameter = True
|
||
hidden_states = [batch_size, seq_len, 64]
|
||
```
|
||
|
||
这说明 token 矩阵的 shape 直接来自 `vocab_size` 和 `hidden_size`。
|
||
|
||
### 实验 B:比较不同 config 对参数量的影响
|
||
|
||
运行:
|
||
|
||
```bash
|
||
PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py --compare
|
||
```
|
||
|
||
记录:
|
||
|
||
```text
|
||
base total =
|
||
wider_hidden total =
|
||
deeper_layers total =
|
||
larger_vocab total =
|
||
untied_lm_head total =
|
||
```
|
||
|
||
你应该观察:
|
||
|
||
- `wider_hidden` 会让 block、attention、FFN 都变大。
|
||
- `deeper_layers` 主要增加 block 数量。
|
||
- `larger_vocab` 主要增加 token 矩阵。
|
||
- `untied_lm_head` 会多出一份输出头参数。
|
||
|
||
### 实验 C:查看真实 `minimind-3` 规模
|
||
|
||
运行:
|
||
|
||
```bash
|
||
PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py --preset minimind3 --device meta
|
||
```
|
||
|
||
这里用 `meta` 设备是为了只看参数 shape 和数量,不真正分配完整权重内存。
|
||
|
||
记录:
|
||
|
||
```text
|
||
hidden_size =
|
||
num_hidden_layers =
|
||
intermediate_size =
|
||
total_unique_params =
|
||
token_matrix_params_once =
|
||
block0_total =
|
||
```
|
||
|
||
你应该看到总参数量接近 `64M`。这就是 `minimind-3` 这个小模型参数规模的大致来源。
|
||
|
||
<a id="l08-check"></a>
|
||
## 10. 本节检查
|
||
|
||
如果你真懂了本节,应该能不看答案说清楚:
|
||
|
||
1. `hidden_size=768` 具体表示什么。
|
||
2. `vocab_size=6400` 为什么会影响 embedding 和 lm_head。
|
||
3. `tie_word_embeddings=True` 对参数量有什么影响。
|
||
4. `num_hidden_layers=8` 是共享一个 block 循环 8 次,还是创建 8 个独立 block。
|
||
5. `num_attention_heads`、`num_key_value_heads`、`head_dim` 三者是什么关系。
|
||
6. 为什么改 `hidden_size` 后,旧的 `.pth` 权重通常不能直接加载。
|
||
|
||
<a id="l08-next"></a>
|
||
## 11. 下一课
|
||
|
||
第 9 课进入 `Embedding、RMSNorm 与残差 Block`:我们会跟踪 `input_ids` 如何变成 `hidden_states`,以及一个 block 如何保持 `[batch, seq, hidden_size]` 这个主干 shape。
|