专栏算法工具链【大模型】构建Qwen2.5-VL 3B/7B 模型为什么无需修改代码

【大模型】构建Qwen2.5-VL 3B/7B 模型为什么无需修改代码

no_name2026-08-24
20
0

引言

一个有意思的现象是:Qwen2.5-VL 3B 和 7B 两个规模,用的是同一份代码、同一张结构图、同一套模块名,只是跑出来的网络一大一小。
按直觉,规模不同应该要写 if size == "3B": ... else: ... 这样的分支才对。但实际上没有。本文要回答的核心问题就是:
为什么仅靠读取 config.json,就能用同一份代码完成 3B 和 7B 两种规模的网络构建?
答案可以浓缩成一句:代码里没有写死任何"3B/7B"的规模参数,所有结构维度都从 config.json 动态读取并在运行时按 config 数值构建网络。 代码是通用的"config → 骨架 → 填权重"机器,3B/7B 的差异只是喂进去的 config 数值不同。

1. 零硬编码规模参数

遍历所有模块的 __init__,没有任何一处出现写死的层数、维度、头数。每一处都形如 config.hidden_size、config.num_hidden_layers、config.num_attention_heads:
3B 跑 36 次循环建 36 层、7B 跑 28 次建 28 层——循环次数由 config 决定,循环体代码完全相同。"规模"这个变量被彻底外化到了 config 层。

2. 所有算子形状由 config 推导

Attention 的 Linear 输入输出维度不是写死的,而是由 hidden_size / num_heads / num_key_value_heads 算出:
3B 是 2048 → 16头×128,7B 是 3584 → 28头×128,算子类型(Linear)和连接关系完全一致,只是形状数值不同。这里有个巧妙的点:虽然 hidden_size 和 head 数都变了,但 head_dim 在 3B/7B 里恰好都是 128(2048/16 = 3584/28 = 128),所以 attention 内部每个 head 的维度是一致的,差异只体现在"有多少个头"上。

3. config 来自权重目录,模型骨架按 config 搭建

模型构建的关键枢纽是 build_model 方法,流程是:读 config → 按搭骨架 → 取权重 → 改 key → 灌权重

这里有两点要展开,它们正是"同一份代码能通吃 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)
这一步与规模无关——它改的是"命名约定",不改形状。3B 和 7B 的权重 key 结构是一样的,所以同一套重映射规则对两个规模都成立。正因为命名映射是规模无关的,代码才不需要为 3B/7B 各写一套加载逻辑。

3.2 第 5 步:load_state_dict_with_metadata 灌权重

重映射后的 state_dict 通过 load_state_dict_with_metadata 灌进骨架。骨架在第 2 步已经按 config 尺寸建好(3B 是 36 层 × 2048 维、7B 是 28 层 × 3584 维),权重的形状天然与骨架匹配——因为两者读的是同一份 config。这就是"config → 骨架 → 填权重"这条链能自洽的根本原因:骨架和权重都源自同一个 config 数值,形状必然对得上。

4. 导出/trace 接口也按 config 动态展开

不只是建模型,连导出计算图、trace 用的 dummy input 形状、输入输出张量名都按 config 动态生成。这意味着整条"从模型到导出产物"的链路都是 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 在子对象

config.json 里这两类参数的存放位置不同,代码读取时也分别处理:
  • 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_sectionrope_scaling 子对象下(rope_scaling.type = "mrope"、rope_scaling.mrope_section = [16,24,24])。
所以"读 config 构建"不是简单地平铺读一堆字段,而是 text 走顶层(经 .text_config 归拢)、vision 走 vision_config 子对象、RoPE 配置走 rope_scaling 子对象,三路分别取数。

5.2 数值的来源

上表中 3B 与 7B 两列数值,分别取自各自权重目录 Qwen2.5-VL-3B-Instruct / Qwen2.5-VL-7B-Instruct 下的 config.json,已逐项核实,两列均准确(包括 vision 部分的 depth=32、hidden_size=1280、out_hidden_size、fullatt_block_indexes=[7,15,23,31]、window_size=112 等)。
值得注意的是两份 config 的一个共同点:视觉编码器的配置在 3B 与 7B 之间几乎完全相同——depth=32、hidden_size=1280、num_heads=16、patch_size=14、spatial_merge_size=2、window_size=112、fullatt_block_indexes=[7,15,23,31] 全都一致,唯一随规模变化的是 out_hidden_size(3B=2048 / 7B=3584),因为它要对齐到 LM 的 hidden_size。规模差异几乎全部集中在 LM 的 text 参数上,视觉部分是共享的。

6. 总结

代码遵循 HuggingFace 生态的标准范式——"网络结构 = config 的函数"。规模(层数、维度、头数)被彻底外化到 config.json,代码只描述"怎么用这些数值搭网络",不描述"具体数值是多少"。
因此 3B 和 7B 共用同一份代码是完全自然的:它们对代码而言只是 config 字典里不同的取值。要区分 3B/7B 反而需要写 if size == "3B" 的分支——这份代码没有这么做,因为它把"规模"这个变量外化到了 config 层。而能这么干净地外化,又依赖两个规模无关的机制兜底:key 重映射让 HF 权重名对得上骨架名骨架与权重同源 config 让形状天然匹配

本文解释了"为什么仅读 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 子对象。
算法工具链
社区征文杂谈
评论0
0/600