记两个在 on-policy 蒸馏(TCoD)里真实踩到的坑:一个是同样权重却对不上的「训推不一致」,一个是训练后期 teacher 给学生的信号塌成 0/1 的现象。前者是算子层面的数值问题,后者是蒸馏信号的结构性问题。
同一份权重,推理侧 decode 出的 μ 和训练侧 prefill 重算的 π_θ 并不相等。
同一份权重,不同算子路径算出来的概率不相等;而当学生学到后期,老师能给的「软监督」会塌成非 0 即 1。
这两件事都发生在 on-policy distillation 里:学生在自己的分布上采样,老师在这些采样上给信号。第一个坑藏在基础设施的数值层面,第二个坑藏在蒸馏信号的结构里。下面分别拆开。
先看 on-policy 蒸馏一轮里到底发生了几次前向计算:
蒸馏损失大致是在学生采样的轨迹上算逐 token 的 KL(on-policy 蒸馏通常用 reverse KL,$\mathrm{KL}(\pi_{\theta}\,\|\,\pi_{T})$)。这里藏着三层不一致:前两层是「同一个 token,logprob 却不同」(数值问题);第三层更狠,是「连 token 本身都变了」(正确性问题)。
vLLM 和 FSDP 即使加载完全相同的 checkpoint,因为 kernel / 算子实现不同,对同一输入给出的下一 token 分布也不同。后果:采样策略 $\mu$ ≠ 训练策略 $\pi_\theta$,本该 on-policy 的训练悄悄变成 off-policy。而且这种偏差是「基础设施层面的噪声」,不是 PPO mini-step 那种 off-policy,光靠 clip 治不了。
前两层是「同样的 token,logprob 不同」;这一层是「token 本身就不是同一串」,业内叫 retokenization drift(再分词漂移)。数据从推理侧传到训练侧有两种做法:
chat.completions 就是这样),训练时再把文本 重新 tokenize 回 id。问题在于 $\text{tokenize}(\text{detokenize}(\text{ids})) \neq \text{ids}$:哪怕字符串一模一样,重新编码出来的 token 边界也可能不同——
后果比数值不一致更硬:训练时算 logprob/梯度的 token 和实际采样的不是同一串 → 挂错了标的、本质变成 off-policy;而且 loss mask 也会错(只有模型自己生成的 behavior-policy token 该训,工具/模板等插入 token 该 mask 掉)。这不是小事——有工作(Strands-SGLang)报告非 TITO 的 agent RL 在 step 50 前就训崩了。
解法:全程带着 token id 走,绝不 round-trip 文本。vLLM 的 OpenAI 端点用
"return_token_ids": true会连prompt_token_ids和token_ids一起返回;SGLang 原生/generate直接给 token + logprob + mask。只有外部插入的文本(工具/用户)才做一次规范 tokenize 并 mask 掉——这样「生成的 token = 训练的 token = 算 logprob 的 token」。
根因不在算法,而在 GPU 上浮点算子的实现细节:
train_micro_batch_size 与 logprob_batch_size 不一致,同一序列就走了不同计算路径。一句话:数学上「应该相等」,但因为算子实现、并行策略、精度不同,数值上不相等。
常见误区是「只有大模型才需要管」。其实它和参数量没有直接关系——大部分诊断 TIM 的工作就是在 7B 级别的小模型(如 DeepSeek-R1-Distill-Qwen-7B、Qwen 1.5B–7B)上复现并看到训练崩的。真正决定严重程度的是另一组变量:
| 放大因素 | 为什么放大 | 和「大模型」的关系 |
|---|---|---|
| 生成长度 | 自回归误差逐 token 累积,序列越长分叉越大 | 长 CoT / 长程 agent 最致命,与尺寸无关 |
| 精度 bf16/fp16 | bf16 尾数少,舍入误差大 | 与尺寸无关,最主因 |
| TP 并行度 | all-reduce 求和顺序变,浮点不结合 | 大模型 TP 切得多 → 偏差更大 |
| MoE 路由 | 推理/训练选的专家可能不同 | 大模型多是 MoE → 多一层不一致 |
| 数据复用 / off-policy 程度 | 越 off-policy,$\mu$ 与 $\pi_\theta$ 差越远 | 与尺寸无关 |
直接放大它的是「长序列 + bf16 + 复用数据」,这些在 7B 上完全可以很严重;而 TP、MoE 随规模加重——这才是「大模型更明显」这一印象的真正来源。不是参数多本身让它变严重,而是大模型往往同时叠了 MoE + 大 TP + 长上下文。
什么时候可基本忽略:输出短、严格 on-policy 不复用数据、dense + 小 TP、用 fp16,或做的是纯离线蒸馏 / SFT(不采样)。但只要任务是长链推理 / 多轮 agent,哪怕 7B 也得认真对待。
train_micro_batch_size == logprob_batch_size。$$ w_t=\min\!\Big(\frac{\pi_\theta(o_t)}{\mu(o_t)},\,C\Big) $$
序列级版本叫 MIS,长序列上通常比 token 级更稳。在 TCoD 的训练后期观察到:老师给学生的 reward(这里指对学生采样 token 的「认可度」,比如 teacher 概率 $\pi_{T}(o_t)\in[0,1]$)几乎都集中在 0 或 1 两端——要么完全认可、要么完全否定,中间 0.3~0.7 的「软」信号基本消失。这通常是预期内的,有几层成因:
三者叠加 → 老师尖 + 学生也尖 + 中间软样本被消化 → reward 自然双峰到 {0,1}。这恰恰说明 temporal curriculum 把容易的、信息量大的部分吃完了。
如果 reward 直接取 teacher prob,而在 bf16/fp16 下算:匹配 token 的概率太接近 1 会被舍入成 1.0,不匹配的太小会下溢成 0.0。也就是本来还有梯度的软信号,被低精度量化成了硬 0/1。排查方法:把 reward / logprob 那段计算单独提到 fp32,看双峰是否缓解;缓解了就说明至少有一部分是数值假象。
0/1 化本身不能直接判断好坏,要配合其它指标一起看:
| 同时观察 | 健康收敛 | 病态塌缩 / 课程耗尽 |
|---|---|---|
| reward≈1 的占比 | 持续上升 | 上不去,或 0/1 各半卡住 |
| 下游 eval 准确率 | 上升或高位平台 | 早就不动了 |
| 学生 token 熵 / 多样性 | 适中、稳定 | 骤降、开始重复复读 |
| reward=0 的 token | 越来越少,集中在真难点 | 一直一大片,且学生很自信地错 |
两个坑其实指向同一句话:on-policy 蒸馏里,「学生采样的分布」和「用来算梯度/打分的分布」必须尽量是同一个东西——数值上要一致(坑一),信号上要还有区分度(坑二)。