引言
为什么仅靠读取 config.json,就能用同一份代码完成 3B 和 7B 两种规模的网络构建?
1. 零硬编码规模参数
2. 所有算子形状由 config 推导
3. config 来自权重目录,模型骨架按 config 搭建
这里有两点要展开,它们正是"同一份代码能通吃 3B/7B"的关键支撑:
3.1 第 4 步:key 重映射,让 HF 权重名对得上自建骨架名
HuggingFace 原版权重的 key 命名和自建模型骨架的命名不完全一致,灌权重前要先做一次重映射:
- merger.mlp.0/2 → merger.mlp.proj0/proj1(Patch Merger 的 MLP 层命名对齐)
- lm_head.weight → model.lm.lm_head.weight(加 model.lm. 前缀)
- language_model.xxx → lm.xxx(language_model 前缀收敛成 lm)
3.2 第 5 步:load_state_dict_with_metadata 灌权重
4. 导出/trace 接口也按 config 动态展开
- KV cache 的输入输出名按 range(n_layers) 循环——3B 生成 36 对、7B 生成 28 对:
- trace 的 dummy input 形状由 hidden_size / num_kv_heads / head_dim / n_layers / max_kvcache_len 动态构造,KV cache dummy 张量列表长度为 2 * n_layers(每层一对 K/V)。
都没有写死 "36" 或 "28",全部由 config 推导。所以同一份导出代码,喂 3B 的 config 就导出 3B 的计算图,喂 7B 的 config 就导出 7B 的计算图。
5. config 字段与代码使用的映射
下表把 config 字段、3B/7B 取值、以及代码里的使用位置对应起来。这是"规模外化到 config"的最直观体现:
config 字段 | 3B | 7B | 代码中的使用位置 |
|---|---|---|---|
num_hidden_layers | 36 | 28 | range() 建 decoder 层 / KV cache 接口数量 |
hidden_size | 2048 | 3584 | Embedding、RMSNorm、所有 Linear 维度 |
num_attention_heads | 16 | 28 | q_proj/o_proj 维度、head 分组 |
num_key_value_heads | 2 | 4 | k/v_proj 维度、GQA 分组 |
intermediate_size | 11008 | 18944 | MLP gate/up/down 维度 |
vocab_size | 151936 | 152064 | embed_tokens、lm_head 行数 |
vision_config.depth | 32 | 32 | ViT 层数 |
vision_config.hidden_size | 1280 | 1280 | ViT patch embed、attention 维度 |
vision_config.out_hidden_size | 2048 | 3584 | Patch Merger 输出维度 |
rope_scaling.mrope_section | [16,24,24] | [16,24,24] | M-RoPE 三轴切分 |
tie_word_embeddings | true | false | embed/lm_head 是否共享权重 |
5.1 config 的结构:text 在顶层,vision 在子对象
- text / LLM 参数写在顶层(没有 text_config 这个嵌套键)。hidden_size、num_hidden_layers、num_attention_heads、vocab_size、rope_theta 等直接作为顶层字段。代码里 Qwen2_5_VLModelForGeneration 用 config.text_config 取它——这是 HF Qwen2_5_VLConfig 在加载时把顶层 text 参数归拢进 .text_config 属性提供的,磁盘上的 config.json 里并没有 text_config 键。
- vision 参数在 vision_config 子对象里。depth、hidden_size、num_heads、out_hidden_size、patch_size、window_size、fullatt_block_indexes 等都在这个子对象下。代码用 config.vision_config 取。
- mrope_section 在 rope_scaling 子对象下(rope_scaling.type = "mrope"、rope_scaling.mrope_section = [16,24,24])。
5.2 数值的来源
6. 总结

本文解释了"为什么仅读 config 就能构建 3B/7B":
- 零硬编码:所有层数、维度、头数都从 config.xxx 读取,没有任何写死的 3B/7B 数值。
- 形状由 config 推导:head_dim = hidden // heads 等,算子类型和连接一致,只是数值不同(且 head_dim 恰好都是 128)。
- 构建链路:读 config → 按尺寸搭骨架 → 取 HF 权重 → key 重映射 → 灌入骨架,骨架与权重同源 config 故形状匹配。
- 导出也 config 驱动:KV cache 名、trace dummy 形状、量化 stub 名全部 range(n_layers) 动态展开。
- config 结构:text 在顶层(经 .text_config 归拢)、vision 在 vision_config 子对象、mrope_section 在 rope_scaling 子对象。
