知识蒸馏

阶段 6 · 让大模型把毕生功力传授给小模型

模型太大跑不动?蒸馏一下,小模型也能拥有大智慧

一个现实困境

你花了几周训练出一个效果不错的 MoE 模型。正准备上线,老板说:

老板:模型效果很好,但推理太慢了,能不能快 3 倍?
你:MoE 每次推理要走门控选 expert,确实慢……要不我把 MoE 换成 Dense?
老板:可以,但效果不能掉太多。

直接用小模型从头训练,效果差得远。但如果让一个大模型(教师)来"指导"小模型(学生)学习,学生就能在保持轻量的同时,获得接近教师的能力。

这就是知识蒸馏(Knowledge Distillation)要解决的问题。

本章地图

核心概念

  • 教师教学生
  • 软标签与暗知识
  • KL 散度

关键技术

  • 温度缩放
  • 组合损失
  • T 的平方补偿

代码精读

  • distillation_loss
  • train_epoch
  • 参数配置

实战场景

  • MoE 到 Dense
  • Large 到 Small

练习与排错

动手实践,巩固知识蒸馏的关键技能

"Many insects成功优化了它们的幼虫形态,

以便从环境中汲取最大量的营养和能量。

类似地,我们可以训练一个庞大的模型,

然后将它的知识蒸馏到一个更适合部署的小模型中。"

—— Hinton et al., "Distilling the Knowledge in a Neural Network", 2015

Part 1: 核心概念

理解知识蒸馏为什么存在、解决什么问题、核心角色是谁。本节不涉及数学公式,只建立直觉。

在动手写代码之前,我们需要先把几个根本性的问题搞清楚:蒸馏到底在干什么?教师和学生各自扮演什么角色?为什么软标签比硬标签更有价值?只有把这些概念吃透了,后面遇到公式和代码才知道它们在解决什么问题。

  • 什么是蒸馏 —— 用大模型(教师)的输出概率来指导小模型(学生)训练,实现模型压缩
  • 教师与学生 —— 教师冻结参数只做推理,学生才是真正被训练的那一方
  • 蒸馏流程 —— 同一份数据同时喂给两个模型,组合两个损失来优化学生
  • 硬标签 vs 软标签 —— 硬标签只有标准答案,软标签还包含"哪些选项相近"的暗知识
  • 暗知识 —— 教师概率分布中隐藏的、硬标签无法表达的额外信息

什么是蒸馏

用大模型的知识来训练小模型,实现模型压缩

定义:知识蒸馏是一种模型压缩技术。用一个已训练好的大模型(教师)的输出概率分布来指导小模型(学生)训练,使学生获得接近教师的能力。

白话:好比你是个小学生,旁边坐着一位大学教授。你做题时,教授不只告诉你"答案是 C",还告诉你"A 和 B 也有点道理但不太对,D 完全不靠谱"。这些额外信息就是教授的"思维方式",你学到的远不止标准答案。

这个比喻只是为了帮你建立直观感受。在计算机内部,它实际上是教师模型输出 logits 作为额外监督信号,结合 KL 散度损失函数来训练小模型——没有真正的"教授"和"学生"在对话。

图:知识蒸馏的教师-学生架构

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左侧蓝色大圆标注"教师模型(参数量大、推理慢、知识丰富)",右侧橙色小圆标注"学生模型(参数量小、推理快、需要学习)" - 各元素关系:从教师到学生有一条橙色箭头,箭头上方标注"传授思维方式(软标签)" - 关键标注:教师圆内列出"参数量大、推理慢、知识丰富",学生圆内列出"参数量小、推理快、需要学习" - 配色建议:浅蓝/浅灰背景,教师深蓝色,学生橙色,箭头橙色加粗 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

教师与学生

蒸馏中的两个角色分工明确,不能搞混。

教师模型(Teacher)

  • 已经训练好的大模型
  • 参数冻结,不参与梯度更新
  • 只做前向推理,输出 logits
  • 作用:提供"软标签"作为额外监督

学生模型(Student)

  • 待训练的小模型
  • 参数可训练,正常反向传播
  • 同时学习硬标签和软标签
  • 作用:吸收教师知识,保持轻量
易混淆点:教师不是"教"学生参数,而是通过输出概率分布提供额外训练信号。学生的参数从零开始(或从预训练权重开始),通过损失函数反向传播来更新。

蒸馏流程

同一份数据同时喂给教师和学生,只训练学生

蒸馏的核心流程:一个输入,两条路径,两个损失,一个优化目标。

图:知识蒸馏的完整训练流程

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:顶部一个输入节点"输入数据 X"分为两条路径,左侧蓝色路径经过"教师模型(冻结参数)"输出"教师 Logits",右侧橙色路径经过"学生模型(训练参数)"输出"学生 Logits" - 各元素关系:两组 Logits 分别汇入两个损失计算——红色框"软标签损失 KL Divergence"和绿色框"硬标签损失 Cross Entropy",两个损失再汇入紫色框"总损失 L",最后指向"反向传播(仅更新学生)" - 关键标注:教师模型旁标注"冻结",反向传播箭头标注"仅更新学生" - 配色建议:浅蓝/浅灰背景,教师蓝色,学生橙色,KL损失红色,CE损失绿色,总损失紫色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

记住:教师模型全程冻结,不参与反向传播。只有学生的参数会被更新。蒸馏的"代价"是多了一次教师前向传播的推理时间。

硬标签与软标签

这是理解蒸馏的关键一步。两种标签决定了学生能从训练中获取多少信息。

输入: "猫坐在___"

硬标签: [0, 0, 0, 1, 0, 0]   ← 只有"垫子上"是1,其余全是0
         墙上 地上 桌上 垫子上 椅子 天上

软标签: [0.01, 0.05, 0.10, 0.70, 0.12, 0.02]
         墙上  地上  桌上  垫子上  椅子  天上

硬标签 = 标准答案

只知道"垫子上"是对的。信息量极少,只有一个 bit。

来源:训练数据中的真实标注。

软标签 = 教师思考过程

"垫子上"最可能(70%),"椅子"也合理(12%),"桌上"还行(10%),"墙上"和"天上"不靠谱。

来源:教师模型的输出概率分布。

软标签的信息量远超硬标签。它不仅告诉学生"答案是什么",还告诉学生"哪些选项相近、哪些完全不合理"。这些额外信息就是蒸馏的价值来源。

暗知识

Hinton 说的"Dark Knowledge"到底是什么

核心概念:暗知识(Dark Knowledge)

Hinton 2015 年提出:教师模型的概率分布里隐藏着硬标签无法表达的额外信息。比如"椅子和桌上也不错,但墙上和天上完全不合理"。这些信息就是暗知识,是蒸馏的核心价值。

图:硬标签与软标签的暗知识对比

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左侧绿色框"硬标签"内显示一行概率分布"垫子上=100%,其他=0%",标注"信息量极少";右侧橙色框"软标签"内显示完整概率分布"垫子上=70%、椅子=12%、桌上=10%、地上=5%",标注"暗知识:知道各选项的合理程度" - 各元素关系:从硬标签框到软标签框有一条虚线箭头,标注"蒸馏就是要挖掘这些差异" - 关键标注:软标签中"椅子12%"和"桌上10%"用深橙色高亮,表示这些微小概率差异就是暗知识 - 配色建议:浅灰背景,硬标签绿色系,软标签橙色系,暗知识部分深橙高亮 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

暗知识的本质:教师对不同错误答案赋予的概率差异。比如教师认为"椅子"比"墙上"更合理(12% vs 1%),这种细微的排序信息是硬标签无法提供的。

Part 2: 数学工具

蒸馏用到了两个关键的数学工具:KL 散度和温度缩放。理解它们,才能看懂蒸馏损失函数的设计。

上一节我们用直觉理解了蒸馏的流程,但要让代码跑起来,需要两个数学工具:KL 散度负责"衡量学生和教师的差距",温度缩放负责"让教师的暗知识暴露出来"。这两个工具的组合就是蒸馏损失的核心。

  • KL 散度 —— 衡量两个概率分布之间"距离"的数学工具,学生用它在训练中逼近教师
  • 温度缩放 —— 在 softmax 中引入温度 T,让分布变平滑、暗知识浮出水面
  • T² 补偿 —— 温度升高后梯度会缩小 T² 倍,必须乘回来才能保持梯度量级一致
  • 组合损失 —— 把硬标签损失(CE)和软标签损失(KL)加权组合,各取所长

KL 散度

衡量两个概率分布"距离"的工具

定义:KL 散度(Kullback-Leibler Divergence)衡量两个概率分布 P 和 Q 之间的"距离"。公式:

D_KL(P || Q) = Σ P(i) × log(P(i) / Q(i))

白话:教师输出概率分布 P,学生输出概率分布 Q。你想知道 Q 离 P 有多"远"——这就是 KL 散度。Q 越接近 P,KL 散度越小,说明学生学得越好。

代码:PyTorch 里用 F.kl_div 一行搞定。

# P = 教师概率分布, Q = 学生概率分布
# PyTorch 要求第一个参数传入 Q 的 log 形式
kl = F.kl_div(Q_log, P, reduction='batchmean')

KL 散度四个特点

  • 非对称:D_KL(P||Q) ≠ D_KL(Q||P)
  • 非负:始终 ≥ 0
  • 零点:当且仅当 P = Q 时为 0
  • 锚定:以教师 P 为锚点,让学生 Q 去逼近

预判式解惑

问:KL 散度不是真正的"距离",为什么用它?

因为它虽然是"散度"而非"距离"(不满足三角不等式和对称性),但它有一个好的性质:当 P 固定时,最小化 D_KL(P||Q) 等价于让 Q 尽可能接近 P。这正是蒸馏需要的。

温度缩放

让 softmax 变平滑,暴露暗知识

定义:在 softmax 中引入温度 T,公式变为 q_i = exp(z_i/T) / Σ exp(z_j/T)。

白话:标准 softmax 太"尖"了——最高概率接近 1,其余接近 0,软标签退化成硬标签。升温(T > 1)让分布变平滑,暗知识才能暴露出来。就好比教授讲课:T=1 时只说"答案是 C";T=2 时会解释"每个选项的优劣"。

这个比喻只是为了帮你建立直观感受。在计算机内部,它实际上是温度参数通过 logits / T 运算来改变 softmax 输出的平滑程度——没有真正的"教授"在讲课。

代码:

# 标准 softmax(T=1)
probs = F.softmax(logits, dim=-1)

# 温度缩放 softmax(T=1.5,MiniMind 默认)
probs = F.softmax(logits / temperature, dim=-1)

T 越大,分布越平滑,暗知识暴露越多。但 T 太大(比如 5 以上)会"过度稀释",有用信息和噪声混在一起。MiniMind 默认 T = 1.5。

温度数值对比

假设教师模型对某位置的 logits:[2.0, 1.0, 0.1, 4.0, 0.5],对应词汇:猫、跑了、睡了、坐下、跳了。

温度坐下猫跑了跳了睡了特点
T = 1.00.7250.0980.0360.0220.009太尖,暗知识少
T = 1.50.5360.1420.0730.0510.032推荐默认值
T = 2.00.4270.1780.1080.0890.073平滑,暗知识丰富
T = 5.00.2670.2190.1790.1640.150过度平滑,噪声多

温度就像教授讲课的"细致程度"。T 太小,教授惜字如金;T 太大,教授开始扯远。T = 1.5 是甜蜜点。

这个比喻只是为了帮你建立直观感受。在计算机内部,温度是一个数值参数,通过除法和 softmax 运算控制概率分布的熵。

图:温度参数对 softmax 概率分布的影响

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:四组并排的柱状图,每组对应一个温度值(T=1.0、T=1.5、T=2.0、T=5.0),每组有5根柱子分别代表不同词的概率 - 各元素关系:从左到右,最高柱逐渐变矮,其他柱逐渐变高,整体分布趋于平坦 - 关键标注:T=1.5处用绿色边框标注"推荐默认值";T=5.0处用红色虚线框标注"过度稀释" - 配色建议:浅灰背景,柱子蓝色渐变,T=1.5组背景淡绿色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

为什么乘 T²

蒸馏中最容易被忽略的细节

看 distillation_loss 函数的返回语句:

return (temperature ** 2) * kl

直觉理解

logits 除以 T 后再做 softmax,梯度会缩小 T² 倍。如果不补偿,高温下蒸馏损失对学生的影响力会变得微乎其微。

T² 补偿确保了不管 T 选多大,蒸馏损失的梯度量级保持一致。

数学推导(简版)

softmax(z/T) 对 z 的雅可比矩阵,每个元素约为 1/T² 量级。所以梯度缩小了 T² 倍。乘以 T² 就补回来了。

一个具体例子:T=2 时,如果不乘 T²,蒸馏损失的梯度只有 T=1 时的 1/4。这意味着温度越高,蒸馏损失对学生的影响越小——这显然不是我们想要的。乘以 T² 后,不同温度下的梯度量级一致。

组合损失

硬标签和软标签各取所长

定义:蒸馏总损失由两部分加权组合:

蒸馏总损失公式

L = α × L_CE + (1 - α) × T² × D_KL

其中 L_CE 是交叉熵硬标签损失,D_KL 是 KL 散度软标签损失

白话:学生不能只跟教师学(会偏离真实数据),也不能只看标准答案(浪费了教师的知识)。α 就是"听老师讲"和"看标准答案"的平衡。

代码(来自 train_distillation.py):

# 总损失 = alpha * CE + (1-alpha) * Distill
loss = (alpha * ce_loss + (1 - alpha) * distill_loss) / args.accumulation_steps
α 值含义场景
α = 1.0退化为纯 SFT完全不用教师
α = 0.5各取一半MiniMind 默认值
α = 0.0纯蒸馏只跟教师学

为什么双损失

只用一个损失行不行?

只用 CE(α=1.0)

  • 退化为普通 SFT 训练
  • 教师的知识完全浪费
  • 学生只看硬标签
  • 效果上限 = 学生独立训练的水平

只用 KL(α=0.0)

  • 学生只跟教师学
  • 教师若有偏差,学生会放大
  • 没有真实标签锚定
  • 可能偏离真实数据分布

组合损失的精髓:CE 损失锚定真实标签,保证基本能力;KL 损失引入教师知识,提供额外监督。两者互补,缺一不可。

Part 3:从数学到实现

前面的 KL 散度和温度缩放都是数学公式。现在把这些公式变成能跑的代码——看看蒸馏训练究竟是怎么运转的。

公式看懂了,但跑起来是什么样的?本节带你把数学翻译成 PyTorch 代码,逐行拆解蒸馏训练的完整实现。你会看到教师怎么冻结、两个模型怎么前向传播、损失怎么组合、梯度怎么更新——所有细节都在这里。

  • 训练脚本结构 —— 整个文件只有两个核心函数,约 245 行搞定蒸馏训练
  • distillation_loss 函数 —— 12 行代码实现教师端 no_grad + 学生端 log_softmax + KL 散度 + T² 补偿
  • 双模型前向传播 —— 同一输入走两条路径,教师冻结、学生训练
  • 词表对齐与三重损失 —— 处理词表大小不匹配,组合 CE + KL + 辅助损失
  • loss_mask 与梯度更新 —— 用掩码过滤 padding,混合精度下的梯度累积与裁剪

训练脚本三件事

蒸馏训练只需做三件事:定义损失 → 同时跑两个模型 → 更新学生

# train_distillation.py 全局结构

# 1. 核心损失函数
def distillation_loss(student_logits, teacher_logits, temperature, reduction):
    ...

# 2. 训练主循环
def train_epoch(epoch, loader, iters, teacher_model, ...):
    ...

# 3. 主程序入口
if __name__ == "__main__":
    # 3.1 解析参数(alpha, temperature, 学生/教师配置)
    # 3.2 初始化模型(学生可训练,教师冻结)
    # 3.3 训练循环
    # 3.4 清理

整个文件约 245 行。核心只有两个函数:distillation_loss(12 行)和 train_epoch(~100 行)。其余是参数解析和训练循环的常规逻辑。

蒸馏损失函数

整个文件最核心的 12 行代码

def distillation_loss(student_logits, teacher_logits,
                      temperature=1.0, reduction='batchmean'):
    # 教师端:关闭梯度 + 温度缩放 + softmax
    with torch.no_grad():
        teacher_probs = F.softmax(
            teacher_logits / temperature, dim=-1
        ).detach()

    # 学生端:温度缩放 + log_softmax
    student_log_probs = F.log_softmax(
        student_logits / temperature, dim=-1
    )

    # KL 散度 + T² 补偿
    kl = F.kl_div(
        student_log_probs, teacher_probs,
        reduction=reduction
    )
    return (temperature ** 2) * kl

数据怎么流

图:蒸馏损失函数的数据流

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:从左到右的流程图。左侧蓝色区域"教师端(no_grad)"内:teacher_logits → 除以T → softmax → teacher_probs detached;右侧橙色区域"学生端"内:student_logits → 除以T → log_softmax → student_log_probs - 各元素关系:teacher_probs 和 student_log_probs 同时汇入中间的"F.kl_div"节点,再经过"乘以T²",最终输出红色框"distillation_loss" - 关键标注:教师端旁标注"no_grad + detach",学生端旁标注"log_softmax(非softmax)" - 配色建议:浅灰背景,教师端蓝色系,学生端橙色系,输出红色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

教师端:为什么用 no_grad + detach?

教师不参与训练,计算它的梯度纯粹浪费显存。torch.no_grad() 关闭自动求导,.detach() 把张量从计算图摘出来。双保险,一个都不该省。

学生端:为什么用 log_softmax?

因为 F.kl_div 的第一个参数要求输入log 概率,不是原始概率。这是 PyTorch 的接口约定。如果传 softmax 的结果,计算结果会是错的。

reduction='batchmean' 的含义

对 batch 维度取平均。这样 loss 值不会因为 batch size 变化而剧烈波动。

冻结教师模型

train_epoch 的第一步操作

train_epoch 函数开头,先冻结教师:

if teacher_model is not None:
    teacher_model.eval()              # 切到评估模式
    teacher_model.requires_grad_(False)  # 禁用所有参数梯度

eval() 做了什么?

  • 关闭 Dropout(推理时不应随机丢弃)
  • BatchNorm 使用全局统计量(不更新均值/方差)

requires_grad_(False) 做了什么?

  • 禁止为教师参数计算梯度
  • 节省大量显存(不存梯度、不存优化器状态)
为什么必须冻结? 两个原因:1) 教师已经训练好了,蒸馏阶段不能动它的参数;2) 冻结能节省大量显存。一个 768 维的教师,冻结后大约能省 50% 显存。

双模型前向

同一个输入,两条路径

# 学生前向传播(在混合精度上下文中)
with autocast_ctx:
    res = model(input_ids)
    student_logits = res.logits[..., :-1, :].contiguous()

# 教师前向传播(纯推理,不计算梯度)
if teacher_model is not None:
    with torch.no_grad():
        teacher_logits = teacher_model(input_ids)
        teacher_logits = teacher_logits.logits[..., :-1, :].contiguous()

两个要点:
1) [..., :-1, :] 切掉最后一个位置的 logits(自回归语言模型的标准做法,预测第 t 个 token 只用前 t-1 个位置)。
2) 教师前向传播包裹在 torch.no_grad() 中,确保不产生梯度。

词表对齐

教师和学生词表大小不同时的处理

如果教师模型的词表比学生大,logits 的最后一维不匹配。MiniMind 用一行代码解决:

# 对齐词表:如果教师词表更大,截断到学生词表大小
vocab_size_student = student_logits.size(-1)
teacher_logits = teacher_logits[..., :vocab_size_student]

为什么直接截断是安全的?

在 MiniMind 中,教师和学生的 tokenizer 完全相同(同一个词表)。词表大小差异可能来自配置不同。截断只是去掉教师多出的无效 token 位置,不影响语义对齐。

如果教师词表比学生小怎么办?

这种情况不会触发截断。teacher_logits[..., :vocab_size_student] 当教师词表更小时,切片会自动取全部元素,等价于不做操作。但后续 KL 散度计算会因为维度不匹配报错。所以设计时要确保教师词表 ≥ 学生词表。

图:教师 logits 到学生 logits 的词表维度截断对齐

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:两个水平长条形。上方"教师 logits(32005)",下方"学生 logits(32000)" - 各元素关系:上方前32000格浅蓝,后5格红色"超出部分被截断"。裁剪箭头指向上方前32000格到下方 - 关键标注:截断代码放在箭头旁 - 配色建议:浅灰背景,有效区域浅蓝色,截断区域浅红色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

三重损失计算

CE + KL + 辅助损失,组合出击

# 1) 硬标签 CE Loss
shift_labels = labels[..., 1:].contiguous()
loss_mask_flat = loss_mask.view(-1)
ce_loss = F.cross_entropy(
    student_logits.view(-1, student_logits.size(-1)),
    shift_labels.view(-1),
    ignore_index=-100,       # 忽略 padding 位置
    reduction='none'
)
ce_loss_raw = torch.sum(ce_loss * loss_mask_flat) / (loss_mask_flat.sum() + 1e-8)
if lm_config_student.use_moe: ce_loss = ce_loss_raw + res.aux_loss

# 2) 软标签蒸馏 Loss(仅在有效 token 位置计算)
if teacher_model is not None:
    distill_loss = distillation_loss(
        student_logits.view(-1, student_logits.size(-1))[loss_mask_flat == 1],
        teacher_logits.view(-1, teacher_logits.size(-1))[loss_mask_flat == 1],
        temperature=temperature
    )

# 3) 总损失 = alpha * CE + (1-alpha) * Distill
loss = (alpha * ce_loss + (1 - alpha) * distill_loss) / args.accumulation_steps
注意:蒸馏损失只在 loss_mask_flat == 1 的位置计算,排除了 padding token。否则 padding 位置的噪声会干扰蒸馏效果。

损失计算流程

图:蒸馏训练的损失计算完整流程

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:顶部"input_ids, labels"分为两路——左侧经"学生模型前向"得到 student_logits,右侧经"教师模型前向(no_grad)"得到 teacher_logits,teacher_logits 再经过"词表对齐截断" - 各元素关系:student_logits 分两路,一路与 labels 计算"CE Loss"(绿色),另一路与已对齐的 teacher_logits 计算"KL Loss"(红色)。CE Loss 经过"+aux_loss if MoE"后,与 KL Loss 一起汇入紫色框"loss = α×ce + (1-α)×distill",最终指向"backward → clip → step" - 关键标注:教师前向旁标注"no_grad",词表对齐旁标注"截断到学生词表大小" - 配色建议:浅灰背景,学生模型橙色,教师模型蓝色,CE绿色,KL红色,总损失紫色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

loss_mask 解析

为什么需要遮罩?遮住了什么?

# loss_mask 来自 labels 中非 -100 的位置
loss_mask = (labels[..., 1:] != -100).float()
loss_mask_flat = loss_mask.view(-1)

# CE Loss:用 ignore_index=-100 忽略 padding
ce_loss = F.cross_entropy(..., ignore_index=-100, reduction='none')
ce_loss_raw = torch.sum(ce_loss * loss_mask_flat) / (loss_mask_flat.sum() + 1e-8)

# Distill Loss:用布尔索引只选有效位置
distill_loss = distillation_loss(
    student_logits.view(-1, ...)[loss_mask_flat == 1],
    teacher_logits.view(-1, ...)[loss_mask_flat == 1],
    temperature=temperature
)

两种处理方式的区别:
1) CE 用 ignore_index 参数,这是 F.cross_entropy 内置的忽略机制。
2) KL 用布尔索引 [loss_mask_flat == 1],在调用蒸馏函数之前就筛掉了无效位置。

为什么 KL 不用 ignore_index?因为 F.kl_div 没有这个参数。所以必须手动过滤。

梯度与更新

梯度累积、裁剪、更新的完整流程

# 反向传播(混合精度)
scaler.scale(loss).backward()

# 每 accumulation_steps 步执行一次更新
if step % args.accumulation_steps == 0:
    scaler.unscale_(optimizer)     # 缩放回来
    torch.nn.utils.clip_grad_norm_(
        model.parameters(), args.grad_clip  # 梯度裁剪,防止爆炸
    )
    scaler.step(optimizer)         # 更新参数
    scaler.update()                # 更新缩放因子
    optimizer.zero_grad(set_to_none=True)  # 清空梯度

为什么除以 accumulation_steps?

损失计算时 loss / accumulation_steps,确保梯度累积后的总梯度等于单步梯度的倍数。这样可以用小 batch size 模拟大 batch size 的效果。

set_to_none=True 的好处

比 zero_grad() 更高效。它直接把梯度设为 None 而不是填零,节省内存和计算。PyTorch 官方推荐用法。

Part 4: 实战与调优

知道原理后,怎么实际运行蒸馏?参数怎么选?不同场景的策略有何不同?

原理和代码都搞定了,接下来是真正跑起来。本节聚焦两个实战问题:怎么根据你的场景选择参数,以及训练过程中怎么判断"蒸对了还是蒸歪了"。MiniMind 提供了两种典型蒸馏场景(MoE 到 Dense、大模型到小模型),我们会逐一拆解。

  • 参数配置 —— alpha、temperature、学习率等所有可调参数的默认值和含义
  • MoE 到 Dense —— MiniMind 的默认场景,把 MoE 的知识迁移到更快的 Dense 模型
  • 大模型到小模型 —— 部署到资源受限环境时,用大模型蒸馏出小模型
  • 参数选择决策树 —— "教师越强多听教师的,学生越弱多看标准答案"的调参口诀
  • 日志解读 —— 看懂训练输出中的 ce、distill、loss,判断训练是否正常

参数配置

蒸馏相关的所有可配置参数

# 学生模型配置(小模型 / Dense)
lm_config_student = MiniMindConfig(
    hidden_size=args.student_hidden_size,      # 默认 768
    num_hidden_layers=args.student_num_layers, # 默认 8
    use_moe=bool(args.student_use_moe)         # 默认 False(Dense)
)

# 教师模型配置(大模型 / MoE)
lm_config_teacher = MiniMindConfig(
    hidden_size=args.teacher_hidden_size,      # 默认 768
    num_hidden_layers=args.teacher_num_layers, # 默认 8
    use_moe=bool(args.teacher_use_moe)         # 默认 True(MoE)
)
参数默认值说明
--alpha0.5CE 损失权重
--temperature1.5蒸馏温度
--student_hidden_size768学生隐藏层维度
--teacher_hidden_size768教师隐藏层维度
--student_use_moe0学生是否用 MoE
--teacher_use_moe1教师是否用 MoE
--learning_rate5e-6学习率(比 SFT 更小)
--epochs6训练轮数

MoE 到 Dense

MiniMind 的默认蒸馏场景

MoE 效果好但推理慢,蒸馏成 Dense 模型后部署更轻便:

python trainer/train_distillation.py \
    --student_hidden_size 768 \
    --student_num_layers 8 \
    --student_use_moe 0 \
    --teacher_hidden_size 768 \
    --teacher_num_layers 8 \
    --teacher_use_moe 1 \
    --alpha 0.5 \
    --temperature 1.5 \
    --from_student_weight full_sft \
    --from_teacher_weight full_sft

为什么这样做?

  • MoE 推理时每个 token 要经过门控网络选 expert,增加了计算复杂度
  • Dense 结构简单,推理更快,部署更容易
  • 蒸馏后的 Dense 通常能达到教师 80%-95% 的性能

为什么要加载 SFT 权重?

学生和教师都加载 SFT 后的权重作为起点。--from_student_weight full_sft 意味着学生从一个已经经过 SFT 训练的小模型开始,而不是从零开始。这样收敛更快,效果更好。

大模型到小模型

部署到资源受限环境的场景

需要部署到手机、边缘设备时,用大模型蒸馏出小模型:

python trainer/train_distillation.py \
    --student_hidden_size 512 \
    --student_num_layers 8 \
    --student_use_moe 0 \
    --teacher_hidden_size 1024 \
    --teacher_num_layers 16 \
    --teacher_use_moe 0 \
    --alpha 0.5 \
    --temperature 2.0 \
    --from_student_weight full_sft \
    --from_teacher_weight full_sft

教师和学生差距大时,建议提高温度到 T=2.0,让教师输出更平滑的分布,帮学生更好地"理解"教师。同时可以考虑将 α 调到 0.3 左右,让学生更多依赖教师的软标签。

参数选择指南

图:蒸馏参数选择决策树

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:从顶部深色节点"我要蒸馏模型"开始的决策树,第一个分支问"教师和学生差距大吗?",差距不大的分支直接给出推荐参数"α: 0.3~0.5, T: 1.0~1.5";差距大的分支继续问"差异在结构还是大小?" - 各元素关系:"MoE到Dense"分支推荐"α: 0.5, T: 1.5, 平衡结构与知识";"大模型到小模型"分支推荐"α: 0.3, T: 2.0~3.0, 更多依赖教师" - 关键标注:每个叶子节点用不同颜色标注推荐参数值 - 配色建议:浅灰背景,决策节点深灰,MoE→Dense 蓝绿色,Large→Small 橙色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

口诀:教师越强,多听教师的话(α 小、T 大)。学生越弱,多看标准答案(α 大、T 小)。学习率一定要小,蒸馏是在微调而非重训。

日志解读

训练时你会看到这样的输出

Epoch:[1/6](100/500), loss: 2.3456, ce: 1.8923,
aux_loss: 0.0000, distill: 2.7989,
learning_rate: 0.00000450, epoch_time: 3.200min

怎么看训练是否正常?

  • ce(硬标签损失):应该稳步下降。如果不变或上升,检查学习率和数据。
  • distill(蒸馏损失):应该逐渐减小,说明学生在逼近教师。
  • loss(总损失):ce 和 distill 的加权和,整体应下降。
  • aux_loss:仅 MoE 学生有,如果为 0 说明学生是 Dense。

什么时候停?

distill 不再下降时,说明学生已经学到了教师的大部分知识。继续训练可能过拟合。通常 4-6 个 epoch 就够了。

如果 ce 在下降但 distill 不动,检查温度设置是否合理。

初始化流程

从参数解析到模型加载的完整过程

图:蒸馏训练的初始化流程

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:从上到下的线性流程图,节点依次为:"解析命令行参数" → "设置随机种子" → "创建学生/教师配置" → "检查检查点(可选续训)" → "设置混合精度" → 橙色高亮"初始化学生模型+教师模型" → 蓝色高亮"冻结教师参数" → "加载数据集" → "创建优化器和GradScaler" → "从检查点恢复(如有)" → "torch.compile(可选)" → 绿色高亮"开始训练循环" - 各元素关系:节点之间用向下的箭头依次连接,形成线性流程 - 关键标注:"初始化学生+教师模型"旁标注"两模型可独立配置","冻结教师参数"旁标注"eval()+requires_grad_(False)" - 配色建议:浅灰背景,模型初始化橙色,冻结教师蓝色,开始训练绿色,其余浅灰 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

关键步骤:学生模型用 init_model 加载并保持可训练状态;教师模型加载后立即调用 eval() 和 requires_grad_(False) 冻结。两个模型都需要指定权重来源(from_student_weight / from_teacher_weight)。

Part 5: 练习与排错

动手实践才能把知识变成技能。以下练习从简单到困难,建议按顺序完成。

前面的知识需要动手才能内化。本节设计了从简单到进阶的练习,帮你验证温度的效果、理解教师冻结的必要性、探索 alpha 极端值的行为。同时整理了最常见的五个错误和排查清单,遇到问题可以直接对照解决。

  • 基础练习 —— 观察不同温度对 distill loss 的影响,验证冻结教师的显存差异,对比纯蒸馏和纯 SFT
  • 进阶练习 —— 用 KLDivLoss 模块重写蒸馏函数,尝试添加中间层蒸馏(TinyBERT 思路)
  • 常见错误 —— OOM、distill loss 不降、效果不如不蒸馏、KL 输出负数、维度不匹配的诊断清单
  • 调优速查表 —— 所有超参数的推荐范围和调优策略,一张表搞定

动手练习

练习 1:观察温度的影响

修改 train_distillation.py 的 --temperature 参数,分别用 T=0.5、1.0、1.5、3.0 跑一轮训练。观察 distill loss 和最终效果的变化。

预期结果
  • T=0.5:分布太尖,蒸馏几乎没有效果,distill loss 下降很慢
  • T=1.0:标准 softmax,有一定蒸馏效果
  • T=1.5:甜蜜点,distill loss 平稳下降
  • T=3.0:分布太平滑,噪声增加,loss 可能震荡

练习 2:验证教师冻结的必要性

把 teacher_model.requires_grad_(False) 注释掉,运行训练。用 nvidia-smi 观察显存变化。

预期结果
  • 显存占用会显著增加(因为要存教师的梯度和优化器状态)
  • 训练速度变慢(反向传播要过两个模型)
  • 教师的权重会被破坏,蒸馏失败

练习 3:α 极端值实验

分别设 --alpha 0.0(纯蒸馏)和 --alpha 1.0(纯 SFT),对比两者的效果。思考:为什么纯蒸馏(不看真实标签)通常效果不如组合损失?

参考答案

纯蒸馏只跟教师学,如果教师本身在某些样本上有偏差,学生会放大这些偏差。加入 CE 损失可以锚定真实标签,保证基本能力不丢失。

进阶练习

练习 4:自定义 distillation_loss

当前函数使用 F.kl_div。尝试用 torch.nn.KLDivLoss 模块重写,实现相同功能。

# 提示:KLDivLoss 的用法
criterion = torch.nn.KLDivLoss(
    reduction='batchmean'
)
# 注意第一个参数需要 log 概率
参考答案
def distillation_loss_v2(student_logits, teacher_logits,
                         temperature=1.0):
    with torch.no_grad():
        teacher_probs = F.softmax(
            teacher_logits / temperature, dim=-1
        ).detach()
    student_log_probs = F.log_softmax(
        student_logits / temperature, dim=-1
    )
    criterion = torch.nn.KLDivLoss(reduction='batchmean')
    kl = criterion(student_log_probs, teacher_probs)
    return (temperature ** 2) * kl

练习 5:添加中间层蒸馏

当前只蒸馏最终输出层。尝试添加教师和学生中间层的均方误差作为额外损失。这是 TinyBERT 的核心思想。

提示:需要修改模型的前向传播,返回中间层的 hidden states。然后用 MSE Loss 计算对应层的差异。

思路提示
  1. 在模型 forward 中收集每层的输出
  2. 如果教师层数 > 学生层数,需要做层映射(如每 2 层取 1 层)
  3. 对每对对应层计算 MSE Loss,加到总损失中
  4. 注意 hidden_size 可能不同,需要加一个线性投影层对齐

常见错误诊断

错误 1:显存爆炸 OOM

RuntimeError: CUDA out of memory

排查清单:

  • 是否忘记冻结教师?检查 teacher_model.requires_grad_(False)
  • 教师模型是否比学生大很多?减小 --batch_size 或增大 --accumulation_steps
  • 混合精度是否开启?确保 --dtype bfloat16

错误 2:distill loss 不下降

Epoch:[1/6](100/500), distill: 3.2456
Epoch:[2/6](100/500), distill: 3.2431

排查清单:

  • 学习率是否太小?蒸馏推荐 5e-6,可以试 1e-5
  • 温度是否太高?T > 3 时信息被过度稀释,降到 1.5-2.0
  • logits 形状是否对齐?打印 shape 检查

更多错误诊断

错误 3:效果不如不蒸馏

排查清单:

  • α 是否设为 0?纯蒸馏可能不稳定,调到 0.3-0.5
  • 教师模型本身效果是否足够好?先单独评估教师
  • 训练是否过拟合?减少 epochs,加 early stopping
  • 学生是否加载了预训练权重?--from_student_weight 要指向 SFT 后的权重

错误 4:KL 散度输出负数

distill: -0.5234

原因:学生端传了 softmax 概率而不是 log_softmax。F.kl_div 要求第一个参数是 log 概率。

修复:确保用 F.log_softmax(student_logits / T, dim=-1),不是 F.softmax。

错误 5:维度不匹配

RuntimeError: The size of tensor a (32000)
must match the size of tensor b (32005)

原因:教师和学生的词表大小不同,但代码中的词表对齐没有正确执行。

修复:确保教师模型配置中的词表大小 ≥ 学生。检查 teacher_logits[..., :vocab_size_student] 是否在正确的位置执行。

调优速查表

超参数推荐范围调优策略
α(CE 权重)0.3 ~ 0.7教师越强,α 越小
T(温度)1.0 ~ 3.0差距越大,T 越高
学习率1e-6 ~ 1e-4蒸馏比从头训练更小
epochs4 ~ 10不宜过多,防止过拟合
grad_clip1.0通常不需要调整
accumulation_steps1 ~ 4显存不足时增大

调参顺序建议:先固定 T=1.5,调 α 到最佳;再固定最佳 α,微调 T。学习率一般用默认值 5e-6 即可,只有 loss 震荡时才考虑降低。

核心回顾

核心原理

  • 教师传授知识给学生
  • 软标签蕴含暗知识
  • 教师冻结仅训练学生

关键技术

  • 温度缩放 T:平滑概率分布
  • T 的平方梯度补偿
  • 组合损失:alpha*CE + (1-alpha)*KL*T^2
  • 平衡硬标签与软标签

代码要点

  • distillation_loss 函数
  • F.kl_div + log_softmax
  • loss_mask 忽略 padding
  • MoE aux_loss 叠加

使用场景

  • MoE 到 Dense
  • Large 到 Small

延伸阅读

经典论文

  • Hinton 2015:"Distilling the Knowledge in a Neural Network" —— 开山之作,首次提出暗知识概念
  • DistilBERT 2019:BERT 蒸馏经典实践,压缩 40% 保留 97% 能力
  • TinyBERT 2020:多层蒸馏,不仅蒸馏输出还蒸馏中间层(即练习 5 的思路)

进阶方向

  • Feature-level 蒸馏:不仅蒸馏 logits,还蒸馏注意力权重、隐藏层特征
  • Relation-based 蒸馏:蒸馏样本之间的关系(如相似度矩阵)
  • 在线蒸馏:教师和学生同时训练,不需要预训练教师
  • 自蒸馏:模型自己既是教师又是学生,不同层之间互相学习

阶段 6 完成

知识蒸馏是让大模型知识迁移到小模型的核心技术。

你学会了:教师-学生架构、KL 散度与温度缩放、组合损失公式、参数调优。

下一章:阶段 7 · DPO 直接偏好优化——让模型学会什么是"好回答"

1 / X