Module 7:模型选择 — Qwen2 / Qwen2.5
把抽象 transformer 落到一个真实 checkpoint:读懂 Qwen2 的 config、张量命名、权重格式与显存规模。
学习目标
- 读懂 Qwen2 / Qwen2.5 的
config.json,并把字段转成张量 shape。 - 理解 Qwen2 的关键结构:RMSNorm、RoPE、GQA、QKV bias、SwiGLU。
- 解释 tied / untied LM head 与词表大小对内存和计算的影响。
- 理解 safetensors、f32/f16/bf16、memory mapping 在推理加载中的作用。
- 能估算一个 checkpoint 是否适合教学机器的 VRAM,并知道哪些量随模型大小增长。
7.1Qwen2 decoder 架构与 config.json
前几章使用的是 decoder-only transformer 的通用描述。实现推理引擎时,还必须选择一个具体模型族,因为权重名称、config 字段、attention head 划分、normalization 细节都会进入代码。本项目以 Qwen2 / Qwen2.5 为目标:公开 checkpoint 容易取得,0.5B 规模适合调试,同时保留了现代 LLM 中常见的 GQA、RoPE、RMSNorm、SwiGLU、QKV bias 与 tied embeddings。
xinfer-core 中的 QwenConfig 只保留推理需要的字段;xinfer-loader 从模型目录读取 config.json 并反序列化为该结构。以仓库中的 Qwen2.5-0.5B-Instruct 为例,关键配置如下:
| 字段 | 值 | 含义 |
|---|---|---|
hidden_size | 896 | residual stream 宽度 |
intermediate_size | 4864 | MLP 的扩展维度 |
num_hidden_layers | 24 | decoder layer 数 |
num_attention_heads | 14 | query head 数 |
num_key_value_heads | 2 | K/V head 数(GQA) |
vocab_size | 151936 | 词表大小,也决定 LM head 输出维度 |
rope_theta | 1000000 | RoPE 的频率基底 |
rms_norm_eps | 1e-6 | RMSNorm 稳定项 |
tie_word_embeddings | true | LM head 与 embedding table 共享权重 |
这些字段直接决定运行时 buffer 的大小。单个 attention head 的维度为:
因此 Q 的 shape 是 。GQA 使 K/V 只保留 2 个 KV head,shape 为 。代码中的 head_dim() 与 kv_group_size() 正是从这些字段推导得到,而不是写死在 kernel 或模型逻辑中。
config.json 字段会直接决定推理时的张量 shape 和 buffer 大小。7.2Qwen2 的关键结构
Qwen2 的 decoder layer 仍由 attention、MLP、residual 与 normalization 组成。与抽象公式相比,工程实现需要特别处理以下结构,否则张量 shape 或权重名称会与 checkpoint 不一致。
RMSNorm
Qwen2 使用 RMSNorm。配置中的 rms_norm_eps 进入每一层的 input layernorm、post-attention layernorm,以及最终的 model.norm.weight:
RoPE 与 rope_theta
Qwen2 对 Q/K 使用 RoPE。rope_theta 给出频率基底,0.5B Instruct checkpoint 中该值为 1,000,000:
GQA head counts
Qwen2.5-0.5B 有 14 个 query heads,但只有 2 个 KV heads,因此每个 KV head 被 7 个 query head 共享:
这样可以减少 K/V projection 输出维度和 KV cache 规模,同时保留较多 query heads。
QKV bias
许多 Llama 系模型的 Q/K/V projection 不带 bias;Qwen2 的 Q、K、V projection 带 bias:
因此模型装配必须读取 q_proj.bias、k_proj.bias、v_proj.bias。xinfer-model 的 CPU reference 与 GPU forward 都按这些名称加载 bias;忽略 bias 会使 logits parity 失效。
SwiGLU
MLP 使用 gate/up/down 三个矩阵,激活函数由 config 中的 hidden_act: "silu" 对应到 SiLU:
7.3Tied vs untied LM head 与词表规模
tie_word_embeddings=true 表示输出 LM head 与输入 embedding table 共享同一份权重:
对 Qwen2.5-0.5B,这一张表的元素数为:
若以 f16 存储,大小为:
按 MiB 计约为 260 MiB。tied embeddings 使这份权重同时用于 token embedding 与 logits projection;xinfer-model 在 tie_word_embeddings 为 true 或 checkpoint 中没有 lm_head.weight 时,使用 model.embed_tokens.weight 作为 LM head。
大词表不仅占内存,也影响每个 token 的 LM head 计算和 logits 读回。xinfer 后来的 GPU argmax 优化正是为了避免每步读回 个 logits。
7.4权重格式:safetensors、dtype 与 memory mapping
HuggingFace checkpoint 通常由 config.json、tokenizer.json 和一个或多个 *.safetensors 文件组成。safetensors 的文件头记录 tensor 名称、dtype、shape 与 byte offset,数据区连续存放原始 bytes。xinfer-loader 遍历模型目录中的全部 *.safetensors 文件,用 memmap2 建立 memory mapping,再交给 safetensors 解析。
加载阶段支持 f32、f16、bf16 三种权重 dtype,并统一转换为 f32 host tensor。GPU 侧再根据用途选择存储格式:decoder layer 中 q/k/v/o 与 gate/up/down 七类线性层权重以 f16 上传,bias 与 norm weight 保持 f32;shared LM head 也以 f16 上传。这样可以保留 CPU reference 的简单性,同时降低 GPU 权重显存和带宽。
| dtype | 字节/元素 | 特点 | 本项目中如何用 |
|---|---|---|---|
| f32 | 4 | 精度高,内存/带宽大 | CPU reference、activation |
| f16 | 2 | 半精度,显存减半 | GPU matmul weights / LM head |
| bf16 | 2 | 指数范围接近 f32,尾数更少 | 加载时可转成 f32/f16 |
7.5选择一个合适大小:为什么从 0.5B 开始?
教学模型需要同时满足两个约束:结构必须接近实际 LLM,规模又要允许频繁运行 parity test、GPU kernel test 与端到端生成。Qwen2.5-0.5B 有 24 层、真实 tokenizer、真实 Qwen2 结构,适合作为从 CPU reference 迁移到 D3D12 backend 的基线。README 中的运行示例显示,f16 权重常驻 GPU 后,0.5B checkpoint 的 GPU resident weights 约为 942 MiB。
随模型规模增长,主要增长项包括:
- 层数 :所有 per-layer 权重、KV cache 都随 线性增长;
- hidden size :线性层权重通常随 或 增长;
- intermediate size :MLP gate/up/down 权重随 增长;
- 词表大小 :embedding / LM head 随 增长;
- 上下文长度 :KV cache 随 线性增长。
这些量的增长方式决定了课程中的实验顺序。先在 0.5B 上建立正确性与性能测量,再讨论 layer streaming、copy queue 与更大模型,比直接从大模型开始更容易定位错误。
Lab 7解析真实 config 并检查 safetensors
本实验要求你写一个小工具,读取模型目录中的 config.json 和 model.safetensors,输出关键字段和若干 tensor 的 shape。若 checkpoint 被拆成多个 *.safetensors 文件,也应逐个读取并合并名称列表。
# 示例输出(格式可自定义)
model_type: qwen2
hidden_size: 896
layers: 24
heads: 14
kv_heads: 2
head_dim: 64
vocab_size: 151936
model.embed_tokens.weight: [151936, 896]
model.layers.0.self_attn.q_proj.weight: [896, 896]
model.layers.0.self_attn.k_proj.weight: [128, 896]
model.layers.0.mlp.gate_proj.weight: [4864, 896]
你也应该验证这些 shape 能从 config 推导出来。例如:
小结
本章把通用 decoder-only transformer 约束到 Qwen2 / Qwen2.5 的具体实现。config.json 决定 hidden size、层数、head 数、GQA 比例、词表大小、RoPE 基底和 tied embeddings;safetensors 提供可 memory-map 的权重布局,loader 将 f16/bf16/f32 权重转换为 f32 host tensor;GPU 路径再将主要线性层权重和 LM head 压到 f16。选择 0.5B 模型的目的在于:保留真实结构,同时控制显存、加载时间和调试成本。
思考与练习
基础从 Qwen2.5-0.5B 的 config 推导 head dimension。
。K/V head 的单头维度相同,但 K/V head 数只有 2,因此 K/V projection 的输出维度是 。
基础解释 tie_word_embeddings=true 的含义。
它表示输入 embedding 矩阵与输出 LM head 共享同一份参数。对 Qwen2.5-0.5B,这避免了额外保存一张 的大矩阵;f16 下约节省 260 MiB。xinfer 在该字段为 true 时使用 model.embed_tokens.weight 作为 LM head。
进阶为什么 Qwen2 的 K/V projection 输出维度是 128,而 Q projection 是 896?
因为该模型采用 GQA:query 有 14 个 head,K/V 只有 2 个 head。每个 KV head 服务 7 个 query heads,从而降低 K/V projection 计算量和 KV cache 大小。attention 计算时按 kv_group_size=14/2 将 query head 映射到对应的 KV head。
进阶估算 embedding table 的 f16 显存大小。
元素数为 ,f16 每个元素 2 bytes,因此大小为 bytes,约 260 MiB。由于 tied embeddings,这份权重也用于 LM head,不需要再为输出投影保存第二份同形状矩阵。
挑战写一个脚本列出 safetensors 中所有 tensor,并按总字节数排序前 10。
脚本可以用 safetensors 库打开文件,遍历 f.keys(),读取每个 tensor 的 shape 与 dtype,并按 计算字节数。若模型目录中有多个 *.safetensors 文件,应合并全部 文件的结果后排序。预期前列包括 embedding、各层 MLP 的 gate/up/down 权重,以及 attention 的 q/o projection 权重。