Skip to content

8. 权重适配:加载官方 Qwen3.5 权重 ​

第 6 章讲了 MiniQwen 的架构。本章讲怎么把官方 Qwen3.5-0.8B-Base 的预训练权重直接加载到 MiniQwen——跳过我们 0.3B 数据的小规模预训练(效果有限),直接获得官方模型的全部能力。

两条适配线:本章讲 Base 权重 → MiniQwen(miniqwen/adapter.py,模型 ID Qwen/Qwen3.5-0.8B-Base)。后训练成品 Qwen/Qwen3.5-0.8B(SFT→DPO→GRPO 之后的官方成品)有独立适配脚本 posttrain/adapter.py,映射逻辑完全相同,只是模型 ID 与输出目录不同(checkpoints/qwen35_posttrain_adapter)——它是第 9/10 章真实后训练链路的默认起点。

8.1 为什么需要适配 ​

个人预训练是不现实的,没有那么多资源。

官方 Qwen3.5 vs MiniQwen 的结构差异 ​

官方 Qwen3.5MiniQwen
模型类Qwen3_5ForConditionalGenerationMiniQwenForCausalLM
文本模型路径model.language_model.*model.*
视觉模型model.visual.*❌ 不实现
MTPmtp.*❌ 不实现
DeltaNet 层名linear_attnlinear_attn ✅ 相同
Attention 层名self_attnself_attn ✅ 相同
MLP 层名mlpmlp ✅ 相同

关键发现:层内部的权重名称完全一致,只需要修改路径前缀。

权重映射规则 ​

官方                              MiniQwen
─────────────────────────────────────────────────
model.language_model.embed_tokens  →  model.embed_tokens
model.language_model.layers.N.*    →  model.layers.N.*
model.language_model.norm          →  model.norm
lm_head                            →  lm_head
model.visual.*                     →  跳过
mtp.*                              →  跳过

8.2 适配流程 ​

代码文件

adapter.py
│
├── 1. download_official_model()
│   └── 从 ModelScope 下载 Qwen3.5-0.8B-Base → checkpoints/qwen35_official_raw
│
├── 2. adapt_weights()
│   ├── 加载 safetensors 文件
│   ├── 名称映射 (去掉 language_model. 前缀)
│   ├── 跳过 visual.* 和 mtp.*
│   └── 处理 lm_head 权重共享(官方 tie,从 embedding 克隆)
│
├── 3. verify_weights()
│   ├── 对比 MiniQwen 期望的权重名
│   └── 测试加载到模型
│
└── 4. save_checkpoint()
    └── 保存 model.safetensors + config.json + miniqwen_config.pt + tokenizer

4 个函数的实际代码 ​

download_official_model(output_dir):

python
from modelscope import snapshot_download
print(f"Downloading Qwen3.5-0.8B-Base to: {output_dir}")
path = snapshot_download(MODELSCOPE_OFFICIAL_MODEL_ID, local_dir=output_dir)

MODELSCOPE_OFFICIAL_MODEL_ID = "Qwen/Qwen3.5-0.8B-Base",下载到 qwen35_official_raw。

adapt_weights(official_dir) -> dict:

python
def adapt_weights(official_dir: str) -> dict:
    safetensor_files = sorted([f for f in os.listdir(official_dir) if f.endswith(".safetensors")])
    official_state = {}
    for fname in safetensor_files:
        tensors = load_file(os.path.join(official_dir, fname))
        official_state.update(tensors)
    print(f"  Official: {len(official_state)} tensors")

    miniqwen_state = {}
    skipped_vision = skipped_mtp = 0
    for key, value in official_state.items():
        if key.startswith("model.visual."):  # 跳视觉
            skipped_vision += 1; continue
        if key.startswith("mtp."):            # 跳 MTP
            skipped_mtp += 1; continue
        new_key = key.replace("model.language_model.", "model.", 1)  # 去前缀
        miniqwen_state[new_key] = value

    # 官方 tie_word_embeddings=True,lm_head 不在权重文件,从 embedding 克隆
    if "lm_head.weight" not in miniqwen_state and "model.embed_tokens.weight" in miniqwen_state:
        miniqwen_state["lm_head.weight"] = miniqwen_state["model.embed_tokens.weight"].clone()

    # safetensors 不支持同一 tensor 出现多次,clone 共享权重
    seen_ptrs = set()
    for key in list(miniqwen_state.keys()):
        ptr = miniqwen_state[key].data_ptr()
        if ptr in seen_ptrs:
            miniqwen_state[key] = miniqwen_state[key].detach().clone()
        seen_ptrs.add(ptr)

    print(f"  Adapted: {len(miniqwen_state)} tensors")
    print(f"  Skipped: vision={skipped_vision}, mtp={skipped_mtp}")
    return miniqwen_state

verify_weights(miniqwen_state):对比 model.state_dict().keys() 和 miniqwen_state.keys() 的 missing/extra,理想情况都为空;再 model.load_state_dict(..., strict=False) 测试加载。

save_checkpoint(miniqwen_state, output_dir):

  • 写 model.safetensors(主权重)
  • 复制 tokenizer.json/tokenizer_config.json(从仓库 tokenizer/,与官方逐字节一致)
  • 写 config.json(MiniQwenConfig().to_dict(),权威构造参数,含 rope_theta=1e7——minivllm 直接读这个)
  • 写 miniqwen_config.pt(pickle,向后兼容旧加载器)

权重统计 ​

类别数量说明
官方总权重488包含文本 + 视觉 + MTP
视觉权重153跳过(ViT 编码器)
MTP 权重15跳过(Multi-Token Prediction)
适配后321纯文本模型权重
MiniQwen 期望321✅ 完美匹配

8.3 实际操作 ​

使用方法 ​

bash
cd /qwen35-reproduce
python miniqwen/adapter.py --output ./checkpoints/qwen35_adapter

不传 --model_path 时自动从 ModelScope 下载官方权重到 checkpoints/qwen35_official_raw。

输出文件 ​

checkpoints/qwen35_adapter/
├── model.safetensors       # 适配后的权重 (约 1.9 GB)
├── config.json             # MiniQwenConfig 全部构造参数(含 rope_theta=1e7)
├── miniqwen_config.pt      # MiniQwen 配置对象(pickle,向后兼容旧加载器)
├── tokenizer.json          # Tokenizer
└── tokenizer_config.json   # Tokenizer 配置

格式注意:适配产物是 model.safetensors + config.json,不是 model.pt + config.pt。inference.py 默认读 model.pt/config.pt(那是预训练产物 checkpoints/miniqwen_pretrain/miniqwen_final/ 的格式),想用 inference.py 直接加载适配权重会走到"随机权重"分支。正确方式是 miniqwen/eval/pretrain_adapter/eff_official_vs_native.py 那样 load_file("checkpoints/qwen35_adapter/model.safetensors") 加载。

检验标准 ​

若终端输出如下则表明加载完毕:

Downloading Qwen3.5-0.8B-Base...
Loading official weights...
  Official: 488 tensors
  Adapted: 321 tensors
  Skipped: vision=153, mtp=15

Verifying weights...
  ✅ All 321 tensors match perfectly!
  ✅ Weights loaded successfully
  Total params: 752,393,024

Saved: checkpoints/qwen35_adapter/model.safetensors (1920.1 MB)

8.4 加载适配后的权重 ​

python
from safetensors.torch import load_file
from miniqwen.model.config import MiniQwenConfig
from miniqwen.model.modeling_miniqwen import MiniQwenForCausalLM

# 创建模型
model = MiniQwenForCausalLM(MiniQwenConfig())

# 加载适配后的权重
state = load_file("checkpoints/qwen35_adapter/model.safetensors")
model.load_state_dict(state)

# 推理
model = model.cuda().bfloat16().eval()

8.5 eval 验证:行为等价 + 能力基线 ​

完整套件在 miniqwen/eval/,6 个脚本覆盖了推理引擎评测(吞吐/C-Eval/数学/native 对齐/DeltaNet 微基准)。本节只展示"权重适配"相关的两个核心结果——这套适配产物的权重真的能复现官方行为吗?能力基线是什么水平?

Part 1:行为等价(同一份权重的官方 vs MiniQwen 行为一致吗?) ​

eff_official_vs_native.py 用同一份适配后的 model.safetensors 分别在官方 transformers (Qwen3_5ForConditionalGeneration) 和手写 MiniQwen 上跑 full forward,对比 logits。

4 段测试输入的逐位一致性:

输入max-abs-difftop1 一致率等价
深度学习是机器学习的一个分支... (len=11)0.00e+00100%✅
请用三句话介绍你自己。 (len=6)0.00e+00100%✅
Python 和 JavaScript 有什么区别? (len=8)0.00e+00100%✅
什么是注意力机制?... (len=13)0.00e+00100%✅

Part 2:效率差距(行为对齐了,但实现效率差多少?) ​

16 case 矩阵(prompt_len ∈ {64,256,1024,4096} × max_new ∈ {32,128} × batch ∈ {1,4})的代表性数据(完整表见 results/):

caseengineprefill(s)decode tok/stotal(s)峰显存(MB)
p1024/n32/b1官方(KV cache)0.2037.551.033219
p1024/n32/b1native(naive)0.184.996.403182
p4096/n128/b1官方0.4337.453.823599
p4096/n128/b1native0.422.2556.803491
p4096/n128/b4官方0.6835.844.225002
p4096/n128/b4native0.701.4290.384753

结论:

  • 行为对齐(Part 1:logits 位一致)
  • 效率差距(Part 2:官方 ~37 tok/s 稳定 vs native 1.4~5 tok/s 随上下文劣化)

native 慢的原因是没有 KV cache、每步全量重算(O(seq)/token)。这正是后续章节(第 11-13 章 minivllm)要补的差距——minivllm 推理引擎的核心收益就是 KV cache + PagedAttention + 连续批处理。

Part 3:C-Eval 能力基线(这套适配产物能答对多少?) ​

eval_ceval.py 用 5-shot greedy 在 C-Eval val 集 10 学科 255 题上测试(完整分学科数据见 results/eval_ceval/summary.md):

维度官方实现我们的 MiniQwen
总分(10 学科 255 题)108/255 = 42.35%108/255 = 42.35%
行为忠实度(answer match rate)—100% (255/255)
行为忠实度(score match rate)—100% (255/255)

分学科准确率(官方 = native = 一致):

学科准确率学科准确率
college_programming54.05% (20/37)education_science48.28% (14/29)
logic50.00% (11/22)operating_system47.37% (9/19)
high_school_physics47.37% (9/19)computer_network42.11% (8/19)
accountant40.82% (20/49)law33.33% (8/24)
middle_school_mathematics26.32% (5/19)high_school_mathematics22.22% (4/18)

解读:

  • 42.35% vs 25% 随机基线 → +17.35%,0.8B Base 模型具备真实推理能力(非刷分、非无脑随机)
  • 行为忠实度 100% 证明官方和我们的 MiniQwen 输出逐位一致——再一次佐证 Part 1 的"权重适配正确"
  • 42.35% 是未经指令微调的 Base 能力基线——第 9-10 章 SFT/DPO/GRPO 等后训练会进一步提升

8.6 总结 ​

权重适配的核心要点 ​

要点说明
名称映射model.language_model.xxx → model.xxx
跳过视觉model.visual.* 共 153 个权重
跳过 MTPmtp.* 共 15 个权重
lm_head 共享官方 tie_word_embeddings=True,需要手动 clone
验证对比权重名和形状,确保 321/321 匹配

下一步可操作 ​

  • 后训练起点:用适配后的 Base 权重(或 qwen35_posttrain_adapter 的后训练成品)做 SFT/DPO/GRPO 等(第 9-10 章)
  • 推理加速:用 minivllm 框架补 KV cache,把 decode 吞吐从 native 的 1.4~5 tok/s 提到 ~37 tok/s(第 11-13 章)

上一章:7. 预训练 | 下一章:9. 后训练理论