从 config.json 开始看懂模型结构
前面几节,我们一直在从零实现:写 Tokenizer、搭 Embedding、堆 Transformer Block——所有结构参数都硬编码在 Python 代码里,改一个隐藏维度要重新改代码。但真实世界的大模型不是这样组织和分发的。
这一节打开 SmolLM2-135M 的仓库,看一个现代 LLM 到底由哪些文件构成,每个文件管什么,以及怎么用这些文件把模型加载起来、跑起来。从「写代码定义模型」切换到「读配置描述模型」。
一个典型的 HuggingFace 模型仓库(如 SmolLM2-135M)里,权重文件(.safetensors)只占了一半。另一半是几张 JSON 配置表,各自管一摊:
- config.json 管模型长什么样——多少层、多宽、几个头
- tokenizer_config.json 管文本怎么变成数字——加不加 BOS/EOS、最多切多长
- tokenizer.json 存 BPE 词表和合并规则,是 tokenizer_config.json 的「数据文件」
- generation_config.json 管模型怎么输出——temperature、top_p、top_k
这些文件合在一起,就是一份完整的模型说明书。
config.json 里写的每一个参数,都会对应到一个具体的 PyTorch 模块。
1. 仓库文件地图
一个 HuggingFace 模型仓库通常包含这些文件。config.json 是必选项,其余取决于模型类型和配置方式。
files = [
("config.json", "必选", "模型结构参数:层数、维度、头数、激活函数等"),
("tokenizer_config.json", "标配", "分词器行为:特殊 token、最大长度、截断/填充策略"),
("generation_config.json","标配", "生成策略:temperature、top_p、top_k、repetition_penalty"),
("tokenizer.json", "标配", "分词器模型文件(BPE 词表 + 合并规则),通常几 MB"),
("special_tokens_map.json","可选", "特殊 token 的名称到 ID 映射"),
("vocab.json", "部分", "BPE 词表(如 GPT-2),从 token 字符串到 ID"),
("merges.txt", "部分", "BPE 合并规则(如 GPT-2),按优先级排列"),
]
print(f"{'文件名':<28} {'必要性':<8} {'用途'}")
print("-" * 80)
for name, required, purpose in files:
print(f"{name:<28} {required:<8} {purpose}")
2. config.json —— 把结构参数变成 PyTorch 模块
下面是 SmolLM2-135M 的真实 config.json。接下来的每个小节,不是打印这些值的含义,而是用它们建出对应的 PyTorch 模块,验证形状、核算参数量。
config = {
"architectures": ["LlamaForCausalLM"],
"hidden_size": 576,
"intermediate_size": 1536,
"num_attention_heads": 9,
"num_key_value_heads": 3,
"num_hidden_layers": 30,
"vocab_size": 49152,
"max_position_embeddings": 8192,
"hidden_act": "silu",
"rms_norm_eps": 1e-05,
"rope_theta": 100000,
"tie_word_embeddings": True,
"attention_bias": False,
}
V, D, L, H, KV, FF = (config[k] for k in (
"vocab_size", "hidden_size", "num_hidden_layers",
"num_attention_heads", "num_key_value_heads", "intermediate_size"))
head_dim = D // H
2.1 TransformerBlock —— 用 config 组装一个完整的 Block
config.json 里的参数最终要装进一个 TransformerBlock:Attention(GQA)+ FFN(SwiGLU)+ RMSNorm。
下面直接用 PyTorch 自带的模块(nn.Linear、nn.RMSNorm),用 config 数字把它们拼起来,然后打印它的结构。
import torch.nn as nn
import torch.nn.functional as F
class TransformerBlock(nn.Module):
"""TransformerBlock:RMSNorm → GQA Attention → RMSNorm → SwiGLU FFN"""
def __init__(self, config):
super().__init__()
d = config['hidden_size']
ff = config['intermediate_size']
h = config['num_attention_heads']
kv = config['num_key_value_heads']
hd = d // h
bias = config['attention_bias']
self.attn_norm = nn.RMSNorm(d, eps=config['rms_norm_eps'])
self.ffn_norm = nn.RMSNorm(d, eps=config['rms_norm_eps'])
# Attention 四个投影
self.q_proj = nn.Linear(d, h * hd, bias=bias)
self.k_proj = nn.Linear(d, kv * hd, bias=bias)
self.v_proj = nn.Linear(d, kv * hd, bias=bias)
self.o_proj = nn.Linear(h * hd, d, bias=bias)
# FFN 三个投影
self.gate = nn.Linear(d, ff, bias=False)
self.up = nn.Linear(d, ff, bias=False)
self.down = nn.Linear(ff, d, bias=False)
self.n_heads = h
self.n_kv_heads = kv
self.head_dim = hd
def forward(self, x):
# Attention (简化版,不包含 causal mask 和 RoPE)
residual = x
x = self.attn_norm(x)
B, T, D = x.shape
q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2)
k = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
v = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2)
# GQA 广播
k = k.repeat_interleave(self.n_heads // self.n_kv_heads, dim=1)
v = v.repeat_interleave(self.n_heads // self.n_kv_heads, dim=1)
scale = self.head_dim ** -0.5
attn = (q @ k.transpose(-2, -1)) * scale
attn = F.softmax(attn, dim=-1)
out = (attn @ v).transpose(1, 2).contiguous().view(B, T, D)
x = residual + self.o_proj(out)
# FFN
residual = x
x = self.ffn_norm(x)
x = residual + self.down(F.silu(self.gate(x)) * self.up(x))
return x
# 用 SmolLM2 的 config 建一个 Block
block = TransformerBlock(config)
print("=== 单个 TransformerBlock 的完整结构 ===")
print(block)
block_params = sum(p.numel() for p in block.parameters())
print(f"\n这个 Block 的参数量: {block_params:,} ({block_params/1e6:.2f}M)")
2.2 Attention —— GQA 让 K/V 投影比 Q 小
num_attention_heads=9、num_key_value_heads=3、attention_bias=false。 Q 投影是 [576, 9×64],K/V 投影是 [576, 3×64]——K 和 V 的参数只有 Q 的 1/3。 建出四个投影矩阵,看形状直接验证。
import torch
import torch.nn as nn
torch.manual_seed(42)
W_q = nn.Linear(D, H * head_dim, bias=False)
W_k = nn.Linear(D, KV * head_dim, bias=False)
W_v = nn.Linear(D, KV * head_dim, bias=False)
W_o = nn.Linear(H * head_dim, D, bias=False)
x = torch.randn(2, 16, D) # 模拟 hidden states
q = W_q(x).view(2, 16, H, head_dim).transpose(1, 2) # [2, 9, 16, 64]
k = W_k(x).view(2, 16, KV, head_dim).transpose(1, 2) # [2, 3, 16, 64]
v = W_v(x).view(2, 16, KV, head_dim).transpose(1, 2) # [2, 3, 16, 64]
print(f"输入: [2, 16, {D}]")
print(f"Q 投影: {list(W_q.weight.shape)} → Q shape: {list(q.shape)}")
print(f"K 投影: {list(W_k.weight.shape)} → K shape: {list(k.shape)}")
print(f"V 投影: {list(W_v.weight.shape)} → V shape: {list(v.shape)}")
print(f"O 投影: {list(W_o.weight.shape)}")
print()
q_p = sum(p.numel() for p in W_q.parameters())
k_p = sum(p.numel() for p in W_k.parameters())
print(f"Q 参数: {q_p:,} K 参数: {k_p:,} K/Q = {k_p/q_p:.2f}")
print(f"如果是 MHA (Q=K=V=9): K 参数也会是 {q_p:,}")
print(f"GQA 节省了 {(q_p - k_p) * 30:,.0f} 个 K+V 参数 (30 层合计)")
GQA 的核心是在 Attention 计算时把 KV 头「广播」给 Q 头。下面用一个小例子模拟这个过程:
groups = H // KV # 每组 3 个 Q 头
k_repeated = k.repeat_interleave(groups, dim=1) # [2, 3, 16, 64] → [2, 9, 16, 64]
v_repeated = v.repeat_interleave(groups, dim=1)
# 现在 Q 和 K 头数一致了,可以正常做 Attention
scale = head_dim ** -0.5
attn_weights = (q @ k_repeated.transpose(-2, -1)) * scale # [2, 9, 16, 16]
print(f"K 原始形状: {list(k.shape)} → repeat_interleave({groups}) → {list(k_repeated.shape)}")
print(f"V 原始形状: {list(v.shape)} → repeat_interleave({groups}) → {list(v_repeated.shape)}")
print(f"QK^T 结果: {list(attn_weights.shape)}")
print(f"\n分组关系:")
for kv_idx in range(KV):
q_idx = list(range(kv_idx * groups, (kv_idx + 1) * groups))
print(f" KV[{kv_idx}] → Q{q_idx}")
2.3 FFN —— SwiGLU 的三权重结构
读 config 时,FFN 重点看两个数字:
hidden_size:Block 内部统一宽度,也就是 05 节里的d_modelintermediate_size:FFN 中间层宽度,也就是 05 节里的d_ff
SmolLM2-135M 里 hidden_size=576