minimind/course/08_minimind_config_and_para...

23 KiB
Raw Blame History

第 8 课:MiniMindConfig 与参数规模

这一课只解决一个问题:MiniMindConfig 里的配置项,如何决定模型结构、张量 shape、参数量和权重兼容性。

目录

0. 本节主线

模型配置的本质是:

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 当成普通超参数,它们会直接改变模型里矩阵的形状。

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 矩阵共同堆出来的。

2. 变量流转

从 config 到模型参数:

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:

input_ids:     [batch_size, seq_len]
hidden_states: [batch_size, seq_len, hidden_size]
logits:        [batch_size, seq_len, vocab_size]

3. 原理一:Config 是模型结构合同

原理讲解

MiniMindConfig 不是“训练参数表”,而是“模型结构合同”。

训练参数像 learning_rate、batch_size,改变的是训练方式;结构参数像 hidden_size、num_hidden_layers、vocab_size,改变的是模型里参数矩阵的形状。

比如:

hidden_size=768

表示每个 token 在模型内部会被表示成 768 维向量。

num_hidden_layers=8

表示模型主体里有 8 个 MiniMindBlock。

vocab_size=6400

表示模型最后每个位置要在 6400 个 token id 上预测概率。

所以 config 一旦变了,模型结构也变了。结构变了,旧权重通常就不能直接加载。

源码证据 A:MiniMind 自己的配置类

文件:model/model_minimind.py:10-45

看它是为了理解:哪些字段会成为模型结构的一部分。

源码:

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

看它是为了理解:真实下载模型使用的是哪组结构参数。

配置片段:

{
  "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 矩阵。

理解到这一步就够

你应该能说清楚:

Config 决定模型结构;
训练参数决定怎么训练;
二者不是一回事。

暂时不用看:

  • YaRN/RoPE 长上下文扩展细节。
  • PretrainedConfig 在 Transformers 里的完整机制。

4. 原理二:vocab_size * hidden_size 是 token 矩阵大小

原理讲解

LLM 里有一个很大的矩阵负责把 token id 变成向量:

embedding matrix: [vocab_size, hidden_size]

如果:

vocab_size = 6400
hidden_size = 768

那么这个矩阵参数量就是:

6400 * 768 = 4,915,200

也就是约 4.9M 参数。

模型输出时还需要一个矩阵把 hidden state 映射回词表:

lm_head: [vocab_size, hidden_size]

MiniMind 默认把 embedding 和 lm_head 绑定成同一组权重,所以这块参数通常只算一份。这叫 tie_word_embeddings。

源码证据 A:输入 embedding

文件:model/model_minimind.py:196-204

看它是为了理解:input_ids 进入模型后,第一块参数矩阵由谁创建。

源码:

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。

源码:

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。

理解到这一步就够

你应该能回答:

为什么 tokenizer 的 vocab_size 会影响模型大小?
因为 embedding 和 lm_head 都要覆盖整个词表。

暂时不用看:

  • logits softmax 的数值稳定细节。
  • 不同 tokenizer 的 BPB/PPL 对比。

5. 原理三:num_hidden_layers 是 block 的复制次数

原理讲解

Transformer 主体不是一个单独的大函数,而是很多层 block 叠起来。

MiniMind 里一个 block 大致是:

RMSNorm
-> Attention
-> 残差
-> RMSNorm
-> FFN 或 MoE
-> 残差

num_hidden_layers=8 的含义不是“同一个 block 循环 8 次并共享参数”,而是创建 8 个独立的 MiniMindBlock。每一层都有自己的 Attention、Norm、MLP 参数。

所以层数增加时,参数量通常接近线性增加:

总 block 参数量 ≈ num_hidden_layers * 单个 block 参数量

源码证据 A:一个 block 里有什么

文件:model/model_minimind.py:178-185

看它是为了理解:每一层 block 里包含哪些子模块。

源码:

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 如何变成多层模型。

源码:

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 数量。

理解到这一步就够

你应该能说清楚:

num_hidden_layers 控制模型深度;
hidden_size 控制每层内部向量宽度;
二者都会影响参数量,但方式不同。

暂时不用看:

  • 每一层学到的语义差异。
  • 深层模型的训练稳定性问题。

6. 原理四:attention head 决定 Q/K/V 的拆分方式

原理讲解

hidden_size 是 token 向量总宽度,attention 会把这个向量拆成多个 head。

在 minimind-3 里:

hidden_size = 768
num_attention_heads = 8
head_dim = 96

所以:

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 默认如何计算。

源码:

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 层的输入输出维度。

源码:

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 成多头格式。

源码:

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]。

理解到这一步就够

你应该能说清楚:

num_attention_heads 不是层数;
它是在每层 attention 内部把 hidden 向量拆成多少组。

暂时不用看:

  • scaled dot-product attention 的完整公式。
  • causal mask 和 flash attention 分支。
  • KV cache 的拼接过程。

7. 原理五:intermediate_size 主要影响 FFN 参数量

原理讲解

每个 Transformer block 除了 Attention,还有 FFN。

MiniMind 的普通 FFN 有三个矩阵:

gate_proj: hidden_size -> intermediate_size
up_proj:   hidden_size -> intermediate_size
down_proj: intermediate_size -> hidden_size

所以 FFN 参数量大致是:

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 中间维度默认不是随便写死的。

源码:

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 相关。

源码:

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 位置上的。

源码:

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 部分的参数结构。

理解到这一步就够

你应该能回答:

为什么 hidden_size 增大时,参数量不是只小幅增加?
因为 embedding、attention、FFN 都会被 hidden_size 影响,其中 FFN 还有 intermediate_size 放大。

暂时不用看:

  • SwiGLU 的数学细节。
  • MoE 的 token dispatch 和负载均衡 loss。

8. 原理六:改 config 会影响权重加载

原理讲解

.pth 权重文件里保存的是一堆 tensor。

每个 tensor 都有名字和 shape,例如:

model.embed_tokens.weight: [6400, 768]
model.layers.0.self_attn.q_proj.weight: [768, 768]

如果你把当前模型改成:

hidden_size=512

那么新模型期待的 shape 就会变成另一套。旧权重里的 [6400, 768] 通常就装不进新模型的 [6400, 512]。

这里要注意:strict=False 可以容忍缺 key 或多 key,但不能神奇地把 shape 不一致的 tensor 变成可加载。结构参数变了,权重兼容性就要重新检查。

源码证据 A:加载模型时先按当前 config 创建结构

文件:trainer/trainer_utils.py:119-127

看它是为了理解:加载权重之前,模型结构已经由当前 lm_config 决定了。

源码:

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 对不上,就会失败。

理解到这一步就够

你应该能说清楚:

改 hidden_size / vocab_size / layers 不是普通调参;
它会改变权重 shape,因此影响已有权重能不能加载。

暂时不用看:

  • state_dict 里每个 key 的完整命名规则。
  • 模型结构迁移和权重插值。

9. 实验验证

实验 A:看 tiny config 的参数分解

这个实验验证:

config
-> embedding/lm_head shape
-> block 参数量
-> attention 参数量
-> FFN 参数量

运行:

cd /home/sun/minimind
PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py

记录:

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 =

你应该看到:

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 对参数量的影响

运行:

PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py --compare

记录:

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 规模

运行:

PYTHONDONTWRITEBYTECODE=1 python course/labs/inspect_model_config_params.py --preset minimind3 --device meta

这里用 meta 设备是为了只看参数 shape 和数量,不真正分配完整权重内存。

记录:

hidden_size =
num_hidden_layers =
intermediate_size =
total_unique_params =
token_matrix_params_once =
block0_total =

你应该看到总参数量接近 64M。这就是 minimind-3 这个小模型参数规模的大致来源。

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 权重通常不能直接加载。

11. 下一课

第 9 课进入 Embedding、RMSNorm 与残差 Block:我们会跟踪 input_ids 如何变成 hidden_states,以及一个 block 如何保持 [batch, seq, hidden_size] 这个主干 shape。