Qwen3架构深度解析:从Base模型到Instruct模型

Qwen3架构深度解析:从Base模型到Instruct模型

全面解析Qwen3模型架构、参数配置、动态序列处理和后训练数据格式(no Coder and MoE)

Qwen3架构深度解析:从Base到Instruct的演进

📋 目录


一、模型架构核心参数

1.1 三个模型对比概览

模型版本 参数量 目标用途 关键特性
Base-4B 4B 预训练基座 纯补全任务,无对话能力
Instruct-4B 4B 通用对话助手 指令微调,ChatML格式
Thinking-4B 4B 推理增强 支持思维链 <think> 标签

1.2 参数分布深度解析(以Qwen3-4B为例)

总参数构成

总参数: 4B
├─ Embedding层: 389M (151,643 × 2,560)
  占比: 9.7%

├─ 36Transformer层: 3,538M
  每层参数: 98.3M
  
  ├─ Attention部分: 26.2M/ (26.6%)
    ├─ Q投影: 6.55M (2,560 × 2,560)
    ├─ K投影: 6.55M (2,560 × 2,560)
    ├─ V投影: 6.55M (2,560 × 2,560)
    └─ O投影: 6.55M (2,560 × 2,560)
  
  ├─ FFN部分: 72.1M/ (73.4%)
    ├─ Gate投影: 14.4M (2,560 × 5,632)
    ├─ Up投影: 14.4M (2,560 × 5,632)
    └─ Down投影: 14.4M (5,632 × 2,560)
    注:SwiGLU需要额外的Gate分支
  
  └─ LayerNorm: 5KB/ (可忽略)

└─ 最终LayerNorm: 2.5KB

关键洞察:
 FFN占每层参数的73%(计算密集)
 Attention仅占27%(但主导长序列性能)
 中间层维度: 5,632  2.2 × hidden_sizeSwiGLU标准比例

推理内存需求(FP16精度,32K上下文):

组件 内存占用 计算方式
模型权重 8 GB 4B × 2 bytes
KV Cache 9.4 GB 36层 × 2(K+V) × 32K × 2,560 × 2 bytes
激活值 ~2 GB batch_size × seq_len × hidden_size
总计 ~19.4 GB 单batch推理

结论:长上下文时,KV Cache内存超过模型权重!

1.3 架构不变量(所有版本共享)

Architecture:
  model_type: qwen2          # Qwen3使用Qwen2架构
  hidden_size: 2560          # 隐藏层维度
  num_hidden_layers: 36      # Transformer层数
  num_attention_heads: 32    # 注意力头数(Q头数)
  num_key_value_heads: 32    # KV头数(32 = MHA,小于32 = GQA)
  intermediate_size: 5632    # FFN中间层维度(2.2x hidden_size,SwiGLU标准)

Tokenizer:
  vocab_size: 151936         # config声明大小(GPU对齐优化)
  actual_tokens: 151643      # 实际训练token数
  reserved_space: 293        # 预留扩展位(151643-151935)

Model:
  architecture: Dense        # 非MoE架构
  activation: SwiGLU         # 激活函数(非ReLU/GELU)
  normalization: RMSNorm     # 归一化方式
  norm_position: Pre-Norm    # 归一化位置

1.4 后训练改变的参数

参数 Base-4B Instruct-4B Thinking-4B 说明
max_position_embeddings 32,768 262,144 262,144 最大序列长度(8x扩展)
rope_theta 1,000,000 5,000,000 5,000,000 RoPE基频率(YaRN扩展关键)
eos_token_id 151643 [151645, 151643] [151645, 151643] 结束标记ID
eos_token <\|endoftext\|> <\|im_end\|> <\|im_end\|> 结束字符串

一点五、核心架构组件深度解析

1.5.1 Grouped Query Attention (GQA)

什么是GQA?

Qwen3-4B虽然使用标准MHA(32Q头 = 32KV头),但理解GQA对于理解整个Qwen3家族很重要:

传统MHA (Multi-Head Attention):
• Q heads: 32个,每个80维 (2560/32)
• K heads: 32个,每个80维
• V heads: 32个,每个80维
• KV Cache: 32 × 80 × SeqLen × 2 (K+V)

GQA (Grouped Query Attention,小模型如0.6B):
• Q heads: 16个,每个64维
• K heads: 8个,每个64维
• V heads: 8个,每个64维
• KV Cache: 8 × 64 × SeqLen × 2 ← 减少50%!

工作原理:
Query1 ──┐
Query2 ──┼─→ 共享 KV1
Query3 ──┐
Query4 ──┼─→ 共享 KV2
...

优势:
✓ 内存占用减半(KV Cache是长序列瓶颈)
✓ 性能损失极小(<1% perplexity增加)
✓ 特别适合边缘设备和长上下文场景

Qwen3家族中的GQA配置

模型 Q头数 KV头数 GQA比例 KV Cache节省
0.6B 16 8 2:1 50%
1.7B 16 8 2:1 50%
4B 32 32 1:1 (MHA) 0%
8B 32 32 1:1 (MHA) 0%
14B 40 8 5:1 80%
32B 40 8 5:1 80%

为什么4B/8B不用GQA? - 4B模型处于"甜蜜点":既不太小(不需要极致优化),也不太大(KV Cache还可控) - 完整MHA提供最佳性能,这个规模下内存还不是瓶颈

1.5.2 RoPE位置编码原理

为什么不用绝对位置编码?

传统位置编码问题:
Position Embedding: [0, 1, 2, 3, ..., 32767]
                      直接加到token embedding上
问题:
 外推性差(训练32K,推理64K时性能崩溃
 位置信息与内容信息混在一起
 相对位置关系不明确

RoPE的核心思想

# 概念伪代码
def apply_rope(q, k, position):
    """
    通过旋转矩阵注入相对位置信息
    """
    # 每个维度对应一个旋转频率
    freq = 1.0 / (rope_theta ** (2 * dim / hidden_size))

    # 计算旋转角度
    angle = position * freq

    # 应用旋转(复数空间)
    q_rotated = rotate(q, angle)
    k_rotated = rotate(k, angle)

    # 注意力计算自然包含相对位置
    score = q_rotated @ k_rotated.T

    return score

关键特性:
 Attention(Q_pos_i, K_pos_j) 自动包含相对位置 (i-j)
 不同维度使用不同频率多尺度位置信息
 支持外推通过调整rope_theta

Qwen3的RoPE配置演变

Base模型 (32K原生):
  rope_theta: 1,000,000
  max_position: 32,768
  effective_range: 0-32K ✓

Instruct模型 (262K扩展):
  rope_theta: 5,000,000  # 5x增大 → 降低旋转频率
  max_position: 262,144   # 8x扩展
  effective_range: 0-262K ✓

核心机制:
• 增大rope_theta → 旋转更慢 → 远距离位置不会"重叠"
• 结合YaRN温度缩放,实现平滑扩展

1.5.3 SwiGLU激活函数详解

为什么不用ReLU?

激活函数演进:
ReLU(x) = max(0, x)
  └─ 简单但表达力有限

GELU(x) = x · Φ(x)  [Φ是高斯累积分布]
  └─ 平滑,但计算稍慢

GLU(x) = (x·W_1)  σ(x·W_2)  [⊗是元素乘法]
  └─ 门控机制,但需要两倍参数

SwiGLU(x) = (x·W_gate)  Swish(x·W_up)
  └─ GLU + SwishQwen3选择

SwiGLU在Qwen3 FFN中的实现

# Qwen3的Feed-Forward Network
class SwiGLU_FFN:
    def __init__(self, hidden_size, intermediate_size):
        self.gate_proj = Linear(hidden_size, intermediate_size)  # 2560 → 5632
        self.up_proj = Linear(hidden_size, intermediate_size)    # 2560 → 5632
        self.down_proj = Linear(intermediate_size, hidden_size)  # 5632 → 2560

    def forward(self, x):
        # 两路并行计算
        gate = self.gate_proj(x)      # [batch, seq, 5632]
        up = self.up_proj(x)          # [batch, seq, 5632]

        # SwiGLU激活
        activated = gate * swish(up)  # 门控 × Swish(上行)

        # 投影回原维度
        output = self.down_proj(activated)
        return output

def swish(x):
    return x * sigmoid(x)  # 平滑非线性

参数开销:
传统ReLU FFN: 2 × hidden × inter = 2 × 2560 × 5632 = 28.8M
SwiGLU FFN:   3 × hidden × inter = 3 × 2560 × 5632 = 43.2M
增加50%参数但性能提升2-3%

为什么SwiGLU更好?

特性 ReLU GELU SwiGLU
非线性平滑度 ❌ 硬阈值 ✓ 平滑 ✓✓ 更平滑
门控机制 ❌ 无 ❌ 无 ✓ 有
表达能力 基础 较强 最强
训练稳定性 一般 很好
计算开销 最低 较高

1.5.4 RMSNorm + Pre-Normalization

LayerNorm的问题

# 传统LayerNorm
def layer_norm(x):
    mean = x.mean(dim=-1, keepdim=True)        # 需要计算均值
    var = x.var(dim=-1, keepdim=True)          # 需要计算方差
    return (x - mean) / sqrt(var + eps) * gamma + beta

计算步骤:
1. 计算均值 (O(n))
2. 计算方差 (O(n)需要再遍历一次)
3. 减去均值 (O(n))
4. 除以标准差 (O(n))
5. 缩放平移 (O(n))
总共5次操作

RMSNorm的优化

# Qwen3使用的RMSNorm
def rms_norm(x):
    rms = sqrt(mean(x**2) + eps)               # 只计算均方根
    return x / rms * gamma                      # 无需减均值

计算步骤:
1. 计算平方 (O(n))
2. 计算均值 (O(n))
3. 开方 (O(1))
4. 除以RMS (O(n))
5. 缩放 (O(n))
更快且数值更稳定

性能对比:
 速度: RMSNorm比LayerNorm快10-15%
 精度: 性能几乎相同 (<0.1% perplexity差异)
 稳定性: 大batch size时更稳定

Pre-Norm vs Post-Norm

传统Post-Norm结构:
Input
  ↓
SubLayer (Attention/FFN)
  ↓
Add (Residual)
  ↓
Norm
  ↓
Output

问题:
❌ 深层网络训练不稳定(梯度爆炸/消失)
❌ 需要careful的学习率调整
❌ warmup步数要长

Qwen3的Pre-Norm结构:
Input
  ↓
Norm ← 先归一化!
  ↓
SubLayer (Attention/FFN)
  ↓
Add (Residual)
  ↓
Output

优势:
✓ 训练更稳定(每个子层输入都是归一化的)
✓ 可以训练更深的网络(Qwen3-32B有64层)
✓ 学习率鲁棒性更好
✓ 已成为现代LLM标准

二、后训练关键变化

2.1 EOS Token演变(核心差异)

# Base模型:纯文本补全
Token ID: 151643
Token String: "<|endoftext|>"
Usage: 文档结束标记GPT风格

# Instruct/Thinking模型:多轮对话
Primary EOS: 151645 - "<|im_end|>"      # ChatML对话轮次结束
Fallback EOS: 151643 - "<|endoftext|>"  # 完整对话结束

为什么有两个EOS? - <|im_end|>: 单轮对话结束,允许继续多轮交互 - <|endoftext|>: 完整会话结束,停止生成

2.2 Chat Template对比

特性 Base-4B Instruct-4B Thinking-4B
模板长度 4116字符 2630字符 4049字符
Tools支持 ✅ 是 ✅ 是 ✅ 是
Thinking支持 ✅ 是 ❌ 否 ✅ 是
控制Token <\|im_start\|> <\|im_end\|> 同左 同左
特殊XML标签 <think> <tools> <tool_call> <tools> <tool_call> <think> <tools> <tool_call>

2.3 Generation Config差异

Base-4B (贪婪解码):
  do_sample: false
  max_new_tokens: 2048
  temperature: null
  top_p: null
  top_k: null

Instruct-4B (采样生成):
  do_sample: true
  temperature: 0.7          # 较高随机性
  top_p: 0.8                # Nucleus sampling
  top_k: 20

Thinking-4B (精确推理):
  do_sample: true
  temperature: 0.6          # 略低于Instruct,更专注
  top_p: 0.95               # 更大概率空间
  top_k: 20

二点五、动态长度序列处理

2.5.1 为什么需要动态长度?

这是Qwen3高效推理的关键优化之一。

传统Padding方式的问题

场景: 4个样本,真实长度分别为2, 4, 3, 2tokens

Padding到统一长度128:
样本1: [tok1, tok2, <pad>, <pad>, ..., <pad>]  (126padding)
样本2: [tok1, ..., tok4, <pad>, ..., <pad>]    (124padding)
样本3: [tok1, tok2, tok3, <pad>, ..., <pad>]   (125padding)
样本4: [tok1, tok2, <pad>, <pad>, ..., <pad>]  (126padding)

统计:
 tokens: 4 × 128 = 512
 有效tokens: 2+4+3+2 = 11
 浪费率: 97.9%
 Attention计算: 512² = 262,144次操作
 其中有效: 11² = 121次操作
 计算浪费: 99.95%

核心问题: - ❌ 大量计算浪费在padding tokens上 - ❌ 内存占用远超实际需求 - ❌ 短查询(占大多数)效率极低

2.5.2 Qwen3的动态长度方案

方案1: 动态Batching

# 每个样本保持真实长度
样本1: [tok1, tok2]                      Shape: (1, 2)
样本2: [tok1, tok2, tok3, tok4]          Shape: (1, 4)
样本3: [tok1, tok2, tok3]                Shape: (1, 3)
样本4: [tok1, tok2]                      Shape: (1, 2)

特点:
 不同batch可以有不同长度
 每个token都参与有效计算
 零浪费

方案2: Sequence Packing(更高效)

# 将多个样本打包成一个序列
packed = [s1_tok1, s1_tok2, <eos>,
          s2_tok1, s2_tok2, s2_tok3, s2_tok4, <eos>,
          s3_tok1, s3_tok2, s3_tok3, <eos>,
          s4_tok1, s4_tok2, <eos>]

Shape: (1, 15)  仅15个tokens

通过Attention Mask确保样本间不交互:
[1,1,0,0,0,0,0,0,0,0,0,0,0,0,0]  样本1只看自己
[0,0,0,1,1,1,1,0,0,0,0,0,0,0,0]  样本2只看自己
[0,0,0,0,0,0,0,0,1,1,1,0,0,0,0]  样本3只看自己
[0,0,0,0,0,0,0,0,0,0,0,0,1,1,0]  样本4只看自己

优势:
 GPU利用率最大化
 吞吐量大幅提升
 训练/推理成本降低

2.5.3 实际效率对比

短序列查询(最常见场景)

查询: "你好" (4 tokens)

Padding方式 (固定32,768长度):
• 计算量: 32,768² × 2,560 × 36 ≈ 98 TFLOPs
• KV Cache内存: 9.4 GB
• 延迟: ~500ms
• 利用率: 0.015%

动态长度方式:
• 计算量: 4² × 2,560 × 36 ≈ 1.5 MFLOPs
• KV Cache内存: 1.15 MB
• 延迟: ~2ms
• 利用率: 100%
• 加速比: 250x!
• 内存节省: 99.988%

不同长度的性能表现

序列长度 Padding计算 Dynamic计算 加速比 内存节省
10 tokens 98 TFLOPs 9 MFLOPs 10,889x 99.97%
128 tokens 98 TFLOPs 150 MFLOPs 653x 99.6%
2,048 tokens 98 TFLOPs 38 GFLOPs 2,579x 93.8%
16,384 tokens 98 TFLOPs 2.4 TFLOPs 41x 50%
32,768 tokens 98 TFLOPs 9.8 TFLOPs 10x 0%

关键洞察: - 短序列占日常查询的80%以上 - 动态长度对短序列提升巨大 - 这就是为什么Qwen3能实现快速响应

2.5.4 参数利用率的真相

重要澄清:动态长度不影响参数利用率!

参数 vs 计算量的区别:

模型参数 (4B):
├─ 存储在GPU显存中
├─ 每次推理都使用全部参数
├─ 与序列长度无关
└─ 利用率: 永远100%

计算量 (FLOPs):
├─ 取决于输入长度
├─ 短序列 = 少计算
├─ 长序列 = 多计算
└─ 这是效率差异的来源

举例说明:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入: "你好" (4 tokens)
• 参数使用: 4B 全部 (100%)
• 计算量: 1.5 MFLOPs
• 时间: 2ms

输入: 长文档 (32K tokens)
• 参数使用: 4B 全部 (100%)
• 计算量: 9.8 TFLOPs
• 时间: 500ms

结论:
参数利用率都是100%!
差异在于Attention的O(n²)复杂度

3.1 从32K到262K的演进

Qwen3使用 YaRN (Yet another RoPE extensioN) 方法实现8倍上下文窗口扩展:

# Base模型(32K context)
rope_theta = 1,000,000
max_position_embeddings = 32,768

# Instruct/Thinking模型(262K context)
rope_theta = 5,000,000      # 5x增大
max_position_embeddings = 262,144  # 8x扩展

3.2 YaRN核心原理

传统RoPE问题: - RoPE通过旋转角度编码位置信息:θ = 10000^(-2i/d) - 直接外推到长序列会导致高频信息失真(远距离位置编码重叠)

YaRN解决方案

  1. 温度缩放(Temperature Scaling) ```python # 将原始位置按比例缩小 scale = max_position_embeddings_new / max_position_embeddings_base adjusted_position = position / scale

# Qwen3: 262144 / 32768 = 8.0 ```

  1. 基频率调整(Base Frequency Adjustment) ```python # 增大rope_theta,降低旋转频率 rope_theta_new = rope_theta_base * adjustment_factor

# Qwen3: 1M → 5M (5x) # 作用:让远距离位置的旋转角度不会过于密集 ```

  1. 不同频率分段处理
  2. 低频部分(远距离依赖):使用NTK插值
  3. 高频部分(局部依赖):保持原始频率或轻微缩放
  4. 中频部分:平滑过渡

3.3 为什么YaRN有效?

传统方法 YaRN优势
线性插值:压缩所有位置 保留短距离的精确性
NTK:只改频率,不缩放位置 动态平衡短距和长距
ALiBi:完全抛弃位置编码 保留相对位置关系

Qwen3的具体效果: - ✅ 32K以内性能几乎无损 - ✅ 32K-262K平滑衰减,而非突然失效 - ✅ 无需重新训练整个模型(只微调部分层)


四、Tokenizer与词表设计

4.1 词表空间布局

┌─────────────────────────────────────────┐
  0 - 151642: 普通BPE tokens (151,643)    vocab.json
├─────────────────────────────────────────┤
  151643: <|endoftext|>                     Base EOS
  151644: (未定义)                        
  151645: <|im_end|>                        Chat EOS
  151646 - 151935: 预留空间 (290)         未来扩展
└─────────────────────────────────────────┘
   总计: 151,936 (config声明)

4.2 特殊Token分类

已激活的Special Tokens

Token ID 类型 用途
<\|endoftext\|> 151643 文档结束 Base模型主EOS
<\|im_start\|> (动态) 对话开始 ChatML格式
<\|im_end\|> 151645 对话结束 Instruct模型主EOS
<\|im_sep\|> (动态) 角色分隔 区分system/user/assistant

未激活但保留的Token

# 特殊token探测器发现的未使用token
Unused_Tokens = {
    128244: '',      # 可能用于<unk>
    128245: '~~',    # 保留标记1
    128247: '~~',    # 保留标记2
}

# 词表中的特殊标记
Special_Markers = {
    19542: '',           # 空标记
    21122: '<>',         # XML占位符
    71698: '<()>',       # 函数调用标记?
    74094: '>',          # 右括号
    82639: 'Ġ<|',        # 特殊token前缀
}

4.3 BPE Merge规则差异

Base-4B:      151,388 merge rules
Instruct-4B:  151,387 merge rules  # -1
Thinking-4B:  151,387 merge rules  # -1

# 原因:后训练阶段略微调整了tokenization策略
# 但词表本身(vocab.json)保持一致

五、微调实践指南

5.1 后训练数据格式深度解析

5.1.1 后训练概述

从Base模型到Instruct模型需要经过后训练(Post-training)阶段:

预训练阶段:
• 数据量: 36 trillion tokens
• 训练目标: 下一词预测(语言建模)
• 输出: Base模型(纯文本补全)
• 示例: 输入"人工智能" → 输出"的发展历程..."

后训练阶段:
• SFT数据量: ~100K-1M条指令对
• 训练目标: 遵循指令,对话交互
• 输出: Instruct模型(助手)
• 示例: 输入"解释人工智能" → 输出"人工智能是..."

关键差异:
预训练 ≈ 学习语言
后训练 ≈ 学习交流

数据量对比:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
阶段          数据量           占比
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
预训练        36T tokens      100%
SFT          100K-1M 条       0.000003%
RLHF         10K-100K 偏好对   0.0000003%

震撼事实:
仅用预训练0.000003%的数据量,
就能彻底改变模型的行为模式!

5.1.2 主流后训练数据格式对比

格式1: Alpaca(最流行)

{
  "instruction": "将这段文字翻译成英文",
  "input": "你好,世界!",
  "output": "Hello, World!"
}

特点: - ✅ 简单直观,易于理解 - ✅ 适合单轮指令任务 - ✅ 易于人工标注 - ✅ 代表数据集: - Stanford Alpaca (52K) - Alpaca-GPT4 (52K) - BELLE (1.5M中文)

格式2: ShareGPT(真实对话)

{
  "conversations": [
    {
      "from": "human",
      "value": "什么是机器学习?"
    },
    {
      "from": "gpt",
      "value": "机器学习是人工智能的一个分支..."
    },
    {
      "from": "human",
      "value": "能举个例子吗?"
    },
    {
      "from": "gpt",
      "value": "当然!比如邮件垃圾过滤器..."
    }
  ]
}

特点: - ✅ 支持多轮对话 - ✅ 真实用户交互数据 - ✅ 上下文连贯性强 - ✅ 代表数据集: - ShareGPT (~90K) - UltraChat (1.5M)

格式3: OpenAssistant(标准化)

{
  "messages": [
    {
      "role": "user",
      "content": "解释量子计算"
    },
    {
      "role": "assistant",
      "content": "量子计算利用量子力学原理..."
    }
  ]
}

特点: - ✅ 标准化role字段 - ✅ 支持system角色 - ✅ 多语言覆盖 - ✅ 代表数据集: - OpenAssistant (161K, 35语言)

格式对比总结

格式 角色字段 多轮支持 易用性 主要用途
Alpaca instruction/input ⭐⭐⭐ 单轮指令
ShareGPT from ⭐⭐ 多轮对话
OpenAssistant role ⭐⭐⭐ 标准化训练

5.1.3 统一格式转换流程

核心问题:不同格式的数据混在一起会让模型困惑吗?

答案:不会!因为训练前会统一转换。

原始数据 (多种格式)
     
步骤1: 格式统一
     
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Alpaca格式:
{
  "instruction": "翻译",
  "input": "Hello",
  "output": "你好"
}
      转换为统一messages格式
{
  "messages": [
    {"role": "user", "content": "翻译\nHello"},
    {"role": "assistant", "content": "你好"}
  ]
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ShareGPT格式:
{
  "conversations": [
    {"from": "human", "value": "Hello"},
    {"from": "gpt", "value": "你好"}
  ]
}
      转换为统一messages格式
{
  "messages": [
    {"role": "user", "content": "Hello"},
    {"role": "assistant", "content": "你好"}
  ]
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
     
步骤2: 应用Chat Template (Qwen3的ChatML)
     
<|im_start|>user
Hello<|im_end|>
<|im_start|>assistant
你好<|im_end|>
     
步骤3: Tokenization
     
[151644, 872, 198, 9906, 151645, 
 151644, 77091, 198, 108386, 151645]
     
步骤4: 模型训练
     
模型只看到: [151644, 872, 198, ...]
完全不知道原始格式是什么

为什么不会混乱?

关键理解:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1. 模型不理解"格式"
    模型输入: token IDs (数字序列)
    instruction/from/role: 这些是人类组织数据的方式
    模型从未见过这些字段名

2. 统一转换消除差异
    所有数据  ChatML  Tokens
    模型学习的是ChatML模式
    永远是: <|im_start|>...content...<|im_end|>

3. 类比理解
   就像多个人用不同方言格式说话
   但都被翻译成普通话ChatML后再教给学生模型),
   学生只学会了普通话不知道原本有不同方言

4. 实际效果
    可以混合任意格式的数据集
    训练前统一转换即可
    模型行为完全一致

5.1.4 ChatML的含义

ChatML = Chat Markup Language (聊天标记语言)

类比其他标记语言:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HTML  = HyperText Markup Language      (网页结构)
XML   = eXtensible Markup Language     (数据交换)
YAML  = YAML Ain't Markup Language     (配置文件)
ChatML= Chat Markup Language           (对话结构)

共同特点:
✓ 使用特殊标记(tags)区分内容和结构
✓ 人类可读
✓ 机器可解析
✓ 语义明确

ChatML示例:
<|im_start|>user           标记:用户开始
你好<|im_end|>              标记:结束
<|im_start|>assistant      标记:助手开始  
你好!<|im_end|>            标记:结束

与HTML对比:
<p>这是段落</p>            HTML标记
<|im_start|>...<|im_end|>  ChatML标记

5.2 添加自定义Special Token

步骤1:理解预留空间

Qwen3预留了293个token位置(151643-151935),可安全添加自定义token:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-4B-instruct")

# 查看当前vocab大小
print(f"Current vocab size: {len(tokenizer)}")  # 151643

# 添加自定义token
custom_tokens = [
    "<|custom_start|>",    # 自定义任务开始
    "<|custom_end|>",      # 自定义任务结束
    "<|思考|>",             # 中文思考标记
    "<|代码|>",             # 中文代码标记
]

num_added = tokenizer.add_special_tokens({
    "additional_special_tokens": custom_tokens
})

print(f"Added {num_added} tokens")
print(f"New vocab size: {len(tokenizer)}")  # 151647

步骤2:调整Embedding层

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-4B-instruct")

# 方法1:扩展embedding(推荐用于少量token)
model.resize_token_embeddings(len(tokenizer))

# 方法2:手动初始化(更精细控制)
old_embeddings = model.get_input_embeddings()
new_embeddings = torch.nn.Embedding(
    len(tokenizer), 
    old_embeddings.embedding_dim
)

# 复制旧权重
new_embeddings.weight.data[:old_embeddings.num_embeddings] = \
    old_embeddings.weight.data

# 新token用平均值初始化
new_embeddings.weight.data[old_embeddings.num_embeddings:] = \
    old_embeddings.weight.data.mean(dim=0)

model.set_input_embeddings(new_embeddings)

步骤3:更新LM Head

# LM Head通常与input embeddings共享权重
# 如果不共享,需要单独扩展
if not model.config.tie_word_embeddings:
    old_lm_head = model.get_output_embeddings()
    new_lm_head = torch.nn.Linear(
        old_lm_head.in_features,
        len(tokenizer),
        bias=False
    )
    new_lm_head.weight.data[:old_lm_head.out_features] = \
        old_lm_head.weight.data
    model.set_output_embeddings(new_lm_head)

5.2 微调EOS Token行为

场景1:保持Instruct行为(推荐)

# 使用原有的双EOS策略
generation_config = {
    "eos_token_id": [151645, 151643],  # <|im_end|>, <|endoftext|>
    "pad_token_id": 151643,
}

场景2:自定义结束行为

# 添加任务特定的EOS
custom_eos_id = tokenizer.convert_tokens_to_ids("<|custom_end|>")

generation_config = {
    "eos_token_id": [
        151645,        # 保留对话结束
        custom_eos_id, # 自定义任务结束
    ],
}

5.3 Chat Template定制

示例:添加思考能力到Instruct模型

# Instruct模型默认不支持<think>,需要修改template
custom_template = """
{%- if tools %}
    {{- '<|im_start|>system\n' }}
    {%- if messages[0].role == 'system' %}
        {{- messages[0].content + '\n\n' }}
    {%- endif %}
    {{- "You can use <think></think> tags for reasoning.\n" }}
    {{- "# Tools\n\n..." }}
    {{- '<|im_end|>\n' }}
{%- endif %}

{%- for message in messages %}
    {{- '<|im_start|>' + message.role + '\n' }}
    {{- message.content }}
    {{- '<|im_end|>\n' }}
{%- endfor %}

{%- if add_generation_prompt %}
    {{- '<|im_start|>assistant\n' }}
{%- endif %}
"""

tokenizer.chat_template = custom_template

5.4 完整微调Pipeline

from transformers import TrainingArguments, Trainer

# 1. 准备数据
train_dataset = [
    {
        "messages": [
            {"role": "user", "content": "解释RoPE"},
            {"role": "assistant", "content": "<think>需要解释位置编码...</think>RoPE是..."}
        ]
    }
]

# 2. 格式化为训练格式
def format_chat(example):
    text = tokenizer.apply_chat_template(
        example["messages"],
        tokenize=False,
        add_generation_prompt=False
    )
    return {"text": text}

formatted_dataset = [format_chat(x) for x in train_dataset]

# 3. Tokenize
def tokenize_function(examples):
    return tokenizer(
        examples["text"],
        truncation=True,
        max_length=4096,  # 根据需求调整
    )

# 4. 训练参数
training_args = TrainingArguments(
    output_dir="./qwen3-custom",
    num_train_epochs=3,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    learning_rate=2e-5,
    warmup_steps=100,
    logging_steps=10,
    save_strategy="steps",
    save_steps=500,
    bf16=True,  # Qwen3支持bf16
)

# 5. 开始训练
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
)

trainer.train()

5.5 微调最佳实践

操作 建议 原因
添加token数量 ≤50个 避免稀释原有词表
初始化新embedding 用相近token平均值 加速收敛
学习率 1e-5 ~ 5e-5 避免灾难性遗忘
冻结策略 冻结前32层,只训练后4层+embedding 保留基础能力
LoRA参数 r=16, alpha=32, 目标:q_proj, v_proj 高效微调
数据混合 10%原始数据 + 90%自定义数据 防止能力退化

六、总结与关键要点

6.1 三个模型的本质差异

graph LR
    A[Base-4B<br/>32K context] -->|SFT| B[Instruct-4B<br/>262K context]
    A -->|SFT + Reasoning| C[Thinking-4B<br/>262K context]

    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style C fill:#bfb,stroke:#333

核心改变矩阵

词表 架构 Context RoPE Theta EOS Chat Template
Base → Instruct ✅不变 ✅不变 ❌8x ❌5x ❌改变 ❌改变
Instruct → Thinking ✅不变 ✅不变 ✅不变 ✅不变 ✅不变 ❌添加<think>

6.2 核心架构特点

  1. 统一词汇表 (151,646 tokens)
  2. 所有Qwen3模型完全共享
  3. 3个核心control tokens + 293个预留位置
  4. 支持119种语言

  5. 参数分布特点(4B模型)

  6. Embedding: 9.7%
  7. Transformer层: 88.5%(每层98.3M)
    • FFN占73%(计算密集型)
    • Attention占27%(长序列主导)
  8. LayerNorm: 可忽略

  9. 现代Transformer优化

  10. GQA: 小模型节省50% KV Cache(4B用MHA)
  11. RoPE: 相对位置编码,支持外推(rope_theta扩展)
  12. SwiGLU: 比ReLU强2-3%,代价是50%额外参数
  13. RMSNorm: 比LayerNorm快10-15%,数值更稳定
  14. Pre-Norm: 深层网络训练稳定性关键

6.3 效率优化核心

  1. 动态长度处理(重要!)
  2. 传统padding浪费99%+计算和内存
  3. 动态长度对短序列加速250-10000倍
  4. 短查询占80%场景,这是快速响应的关键
  5. 参数利用率永远100%(与序列长度无关)

  6. YaRN长上下文扩展

  7. rope_theta: 1M → 5M(5x增大)
  8. max_position: 32K → 262K(8x扩展)
  9. 核心:温度缩放 + 基频率调整 + 分频段处理
  10. 效果:32K内无损,32K-262K平滑衰减

  11. 实际内存需求(32K上下文,FP16)

  12. 模型权重: 8 GB
  13. KV Cache: 9.4 GB(超过模型权重!)
  14. 激活值: ~2 GB
  15. 总计: ~19.4 GB

6.4 后训练关键认知

  1. 数据量对比
  2. 预训练: 36T tokens(100%)
  3. SFT: 100K-1M条(0.000003%)
  4. 质量 >> 数量:极少数据彻底改变行为

  5. 数据格式统一

  6. 不同数据集(Alpaca/ShareGPT/OpenAssistant) → 统一Messages格式
  7. 应用Chat Template → ChatML格式
  8. Tokenization → Token IDs
  9. 模型看不到原始格式,不会混淆

  10. 主流数据集

  11. Alpaca: 单轮指令,易标注(52K)
  12. ShareGPT: 多轮对话,真实数据(90K)
  13. OpenAssistant: 标准化,多语言(161K)

6.5 Base vs Instruct本质

关键差异:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特性           Base模型          Instruct模型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
架构           完全相同           完全相同
参数量         完全相同           完全相同
词汇表         完全相同           完全相同
训练阶段       仅预训练           预训练+SFT+RLHF
输入形式       纯文本            ChatML格式对话
输出行为       文本续写          指令遵循/对话
适用场景       Fine-tuning基座   直接使用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

结论: 架构无差异,行为由后训练塑造!

6.6 技术债务与未来方向

当前限制: 1. 预留token未充分利用:293个空位但只用了2个 2. Base模型Chat Template显示支持Thinking但未启用 3. Merge规则不一致:Base比Instruct多1条规则(原因未明)

未来扩展方向: - [ ] 更长上下文(1M tokens):调整rope_theta到更大值 - [ ] 动态专家路由:Dense → MoE稀疏化 - [ ] 多语言特殊token:当前预留空间足够 - [ ] 更高效的KV Cache压缩:减少长序列内存瓶颈

6.7 给新手的建议

{
  "Base-4B": {
    "max_position": 32768,
    "rope_theta": 1000000,
    "eos_id": 151643,
    "eos_str": "<|endoftext|>"
  },
  "Instruct-4B": {
    "max_position": 262144,
    "rope_theta": 5000000,
    "eos_id": [151645, 151643],
    "eos_str": "<|im_end|>"
  }
}

C. 相关资源


本文档基于Qwen3-4B系列实际模型分析整理,持续更新中。
内容和排版由Claude整理。

Thanks for Reading

If this article was helpful to you, feel free to connect with me!