笔记日期: 2026-08-17 作者: Zhongzhu Zhou 阅读来源: 0 train-infer mismatch for Open-weight MoE RL in Open-source code,作者 Ashwinee Panda(@PandaAshwinee) 相关项目: XoRL(训练引擎)、XoRL-SGLang(推理引擎 fork) 发布时间: 2026 年 8 月
先说一句题外话:这篇文章讲的是我和 Ashwinee(apanda)一起做的 XoRL 项目里的一块工作,文末的致谢部分也确实提到了我。所以这篇笔记既是一篇技术拆解,也算是我对自己参与过的工作做一次”回头看”式的复盘——我会尽量保持客观,遇到我认为写得漂亮的地方会说漂亮在哪,遇到我觉得还能继续深挖的地方也会直说。
1. 先讲清楚:这篇文章到底在解决什么问题
如果你训练过强化学习(RL)模型,尤其是用 GRPO/PPO 这类基于重要性采样(importance sampling)的算法训练大模型,你大概率会用到两套完全不同的软件:一套是训练引擎(比如 Megatron-LM、TorchTitan,或者本文的主角 XoRL),负责算梯度、更新权重;另一套是推理引擎(比如 SGLang、vLLM、TensorRT-LLM),负责快速生成 rollout(也就是采样轨迹)。
问题在于:这两套引擎在数学上应该算出”一样”的东西——给定同一个 prompt、同一份模型权重,两边都应该给出同一个 token 序列的对数概率(logprob)——但实际上它们几乎从来不会真的一模一样。差个几个 bit,看起来无伤大雅,但这个”几个 bit”的误差会通过重要性采样的比值(importance ratio)被放大,进而污染整个训练信号。这就是所谓的 train-infer mismatch(训练-推理不匹配)。
这篇文章要做的事情非常极端:不是把这个误差降到”足够小”,而是让 XoRL 和 SGLang 在浮点层面逐字节完全相同(bitwise-identical)。听起来像是过度工程,但作者证明了这件事真金白银地影响训练效果——在 Qwen3.6-35B-A3B 玩 Wordle 的任务上,把 mismatch 消灭到 0 之后,held-out solve rate 从 63.9% 直接跳到 77.4%,提升了 13.5 个百分点。这不是噪声级别的差异。
2. 前置知识:你需要先知道的几件事
2.1 RL 里的重要性采样是怎么回事
在用 GRPO/PPO 训练 LLM 时,标准流程是:推理引擎按当前策略采样出一批 rollout(记录下采样时的概率 ),然后训练引擎把这批 rollout 重新”过一遍”(prefill),算出自己认为的概率 ,用两者的比值
去修正梯度,这样即使 rollout 是在”稍微旧一点”的权重下采样出来的(off-policy),训练依然能收敛到正确的目标。这套理论的前提是 反映的是策略本身的变化(权重更新导致的概率漂移),而不是”同一个策略在两个引擎里被算出了两个不同的数”。如果后者才是主因,那么重要性采样修正的其实是一堆噪声,训练信号就被污染了。
2.2 为什么浮点数会”算出两个不同的结果”
这是全篇最基础也最容易被忽视的一点:浮点加法不满足结合律。举个例子:BF16 里比 1 大的最小可表示数是 。如果你先算 ,这个结果会被舍入回 1(因为 太小,加不上去);但如果你先把两个 加在一起再加到 1 上,,这次就真的加上去了,结果变成 。同样的三个数,只是加法顺序不同,结果就不同。
这意味着,只要两个引擎做”求和/归约”(reduction)操作时用了不同的加法顺序——不管是因为它们用了不同的 kernel、不同的并行切分方式,还是仅仅因为 batch size 不一样导致库自动换了个实现——就可能产生比特级别的差异。GPU 上大量的操作本质上都是归约:RMSNorm 里的平方和、矩阵乘法里沿着 K 维度的累加、attention 里的 softmax 归一化、专家网络输出的多卡合并……每一处都是潜在的 mismatch 来源。
2.3 prefill / decode / training:同一个 token 会被打分三次
在一次 RL 迭代里,同一个 token 实际上会被”评分”三次,而这三次很可能用的是不同的计算路径:
- 采样时:推理引擎逐 token decode,边生成边记录概率 。
- 训练时打分:训练引擎把完整的 rollout 一次性 prefill 进去,重新算出概率 。
- 推理引擎自己的 prefill 回放:如果要单独验证推理引擎内部是否自洽,还可以让推理引擎把同一段文本当作 prefill 重新算一遍,得到 ,跟它自己 decode 时的 对比。
于是完整的重要性比值可以拆成三段:
(其中 A = 训练打分 vs 推理 prefill,B = 推理 prefill vs 推理 decode,C = 陈旧状态 stale state)
这个拆解非常关键,它把”消灭 mismatch”这个笼统的目标,拆成了三个可以分别攻克的子问题:训练要跟推理 prefill 对齐,推理 prefill 要跟推理 decode 对齐,以及权重/缓存要保持新鲜(对于同步 rollout 且缓存刚刷新过的情况,第三项天然是 1)。文章后面几乎所有的工程工作,都是在啃前两项。
3. 核心方法论:四种对齐武器 + 一把验证的梯子
3.1 度量标准:K3 而不是简单的差值
作者选择用 Schulman 提出的 K3 估计量来报告 mismatch 程度:
这个量处处非负,且只在 时才等于 0(可以用 在 处取最小值 0 且是凸函数来证明)。用它而不是简单的 ,一个好处是它在 接近 1 时表现为 的二阶行为,对小偏差更敏感也更平滑。但作者非常诚实地指出:他们真正达成的目标比”K3 = 0”更强——是训练打分、推理 prefill、推理 decode 三处算出来的 FP32 logprob 逐字节完全相同。K3=0 只是这个更强保证的必然推论(因为 恒为 1),反过来则不然:K3 在浮点精度下测出来是 0,不代表底层字节真的相同(1e-8 的差异用 FP32 算 K3 可能已经四舍五入成了精确的 0)。这个区分我觉得写得很讲究,很多团队会满足于”K3 看起来很小”就宣布问题解决了,但这篇文章把标准提到了硬件事实层面。
3.2 四种对齐一个算子的方法
面对每一个可能产生 mismatch 的算子,作者总结出四种可选策略:
- 直接在训练引擎里调用推理引擎的 kernel,自己写反向传播。最直接,保真度最高,但可能在训练场景下效率不是最优(比如 LoRA 的 merged 模式)。
- 给两个引擎都加一个新的共享 kernel。当双方原有实现都不满足要求时,干脆重新写一个双方都调用的版本(比如新的 RMSNorm kernel)。
- 让归约操作变成 batch-invariant(批不变)。锁定归约操作的”形状”(比如 GEMM 沿 K 维度怎么切片),这样不管 batch size 多大,反正每次都用同样的切分和同样的加法顺序,只是并行发射更多份相同的工作。
- 两个引擎各写一份 kernel,但保证算术运算完全一致。当双方需要的数据排布或调用方式天然不同(比如推理需要完整的 logits 行来采样,训练只需要被选中 token 的 logprob),允许各自优化实现,但要求在哪里舍入、以什么顺序相加必须一致。
我很喜欢这个分类,因为它其实回答了一个很实际的工程问题:“什么时候可以偷懒复用,什么时候必须重新写”。策略 1 和 2 是”复用/共享”,策略 3 和 4 是”各自实现但收敛到同一套算术规则”。选哪种取决于两个引擎在这个算子上的天然差异有多大。
3.3 验证的梯子:从简单到难
要证明两个复杂系统在数值上完全一致,不能只跑一次训练然后看结果对不对——中间出错了根本无法定位。作者用了一套难度递增的验证阶梯:
- 同一个程序跑两次,比较输出字节是否相同(排除非确定性本身)。
- 改变 batch 形状和 launch 配置,比较同一行的结果是否相同(排除 batch-size 依赖的归约顺序问题)。
- 让 XoRL 和 SGLang 在同样的输入上跑,直接比较输出字节。
- 在真实的在线采样过程中、同一个权重版本下比较。
- 训练完整模型,要求 K3 全程保持为 0。
这个”从单元测试到集成测试再到端到端”的思路虽然朴素,但很多论文/团队在做数值对齐工作时反而容易跳过中间步骤,直接看端到端指标——出问题了只能靠猜。
4. 案例递进:从最简单的 dense 模型,一路啃到最复杂的 MoE
文章的叙事结构是层层递进的,我觉得这个顺序设计得很聪明:先在最简单的场景里把方法论立住,再逐步引入更难的挑战。
4.1 Qwen3-8B(dense):立规矩
在最简单的 dense Transformer 上,作者逐个算子过了一遍:
- LoRA:合并模式(把 提前算好)和分离模式(保留 、 分开,前向时把 adapter 的贡献单独加上去)两条路径,分别对齐权重字节和调用点。
- RoPE:一个非常具体也非常有教育意义的例子——SGLang 在 CPU 上只构造逆频率表、把外积和三角函数计算挪到 GPU 上并行做;XoRL 出于 FSDP 初始化时机的限制,直接在 CPU 上把完整的位置表建好。CPU 和 GPU 的三角函数实现本身就可能不同,于是两边的表在解码到 Qwen3-30B-A3B 的第 593 个位置时开始出现差异(前 592 个位置刚好一致,纯粹是运气)。这说明测试序列长度不够长根本发现不了这个问题——如果只测 100 个 token 的短序列,你会误以为对齐已经完成了。修复方式是统一在 CPU 上用 FP32 建完整的表再传到 GPU,并且在两种旋转实现(先转 BF16 再算 vs. 全程 FP32 最后再转 BF16)里选了更快也更准的后者。
- RMSNorm:这是我认为全文最精彩的一个技术细节。同样一行 Triton 源码
mean = tl.sum(x, axis=0),仅仅因为函数签名多了一个”要不要顺便存一份 BF16 残差”的参数,编译器给归约操作分配的内存布局就从sizePerThread=[4]变成了sizePerThread=[8]——每个线程负责的 8 个数,从”两段不连续的 4 个数(相隔 512)“变成了”一段连续的 8 个数”。这直接导致 8192 行里有 2854 行的结果发生变化。这个例子非常好地说明了 mismatch 有多”随机”:你甚至不需要写错代码,仅仅是加一个看似无关的输出,编译器的自动布局决策就能悄悄改变数值结果。修复方式是干脆抛弃tl.sum,手写一个显式的树形归约,把加法顺序焊死。 - GEMM:cuBLAS 这类库会根据矩阵形状自动挑选不同的 kernel(比如 Split-K),M 一变,切分方式跟着变,归约顺序也跟着变。解法是写一个”batch-invariant”的 Triton kernel,固定死沿 K 维度的切片方式(64 值一片),不管 M 多大都只是多发射几份相同的工作。能用现成的 DeepGEMM 就用(前提是它输出的字节和自家 Triton 参考实现一致且更快),不行就退回自己的 kernel。
- SwiGLU、Attention、LM head、采样变换:类似的思路,不再展开,但值得一提的是 LM head 部分——训练要的是”被选中 token 的 logprob”,推理要的是”完整的 logits 行去采样”,这是策略 4(各写各的但保证同算术)的典型案例:XoRL 直接从 GEMM tile 的 FP32 累加器里算出被选中 token 的 logit 和流式 softmax 摘要,完全不用把整个
[M, V]的 logits 矩阵实体化出来,省了大量显存和算力,同时保证结果字节相同。
代价:仅在 dense Qwen3-8B 上,bitwise-identical 前向让采样吞吐从 2116 tok/s 降到 1774 tok/s(降低 16.2%),其中大部分损失来自 batch-invariant 的 dense GEMM(去掉它能回到 1970 tok/s)。这是个很重要的信号:批不变性本身就有性能税,跟对齐是不是”做对了”无关,是这条路线天然的代价。
4.2 四种并行方式:分片之后还要不要对齐?
在把单卡算子对齐之后,下一步是训练/推理在分布式场景下的并行切分(专家并行 EP、上下文并行 CP、数据并行 DP、流水线并行 PP)。核心结论是:只要引擎在做归约时用了不同的通信原语(allreduce vs all-to-all-then-reduce),归约树就可能不同,字节就可能不同。作者的通用策略是:先用不做归约的通信原语把数据搬过去(比如只搬不合并),再在本地手写一个固定顺序的显式归约树(专家输出的合并用 FP64 累加、两两配对、最后再舍入回 BF16)。DeepEP 这类库自带的通信 kernel 会在传输过程中顺便把值加起来,这样就没法控制加法顺序了——所以作者只用 DeepEP 来搬运数据,归约留给自己来做。
这里有个很细节但很重要的点:数据并行和上下文并行混用时,专家输出归约不能再简单地按”卡号”排序(因为不同的切分方式下同一个 token 的行可能落在不同的卡上),于是作者引入了”数据并行为主、上下文并行为次”的逻辑标签,确保不管 token 实际怎么分布,归约树看到的输入顺序永远一致。这是一种很典型的”用一层间接寻址换取不变性”的设计。
4.3 Qwen3.5/3.6:循环层和 MoE 的双重挑战
Gated DeltaNet(一种线性注意力的循环层)第一次引入了”状态”(state)的概念——它维护一个跨 token 传递的 FP32 矩阵 ,任何一步的误差都会传染到后面所有 token。递推公式是:
问题的核心在于:XoRL 训练时一次性算 64 个 token 一组(chunk),SGLang prefill 阶段也是 64 个一组,但 decode 阶段是一个 token 一个 token 地更新 。“一次算 64 个”和”一个一个算”在浮点上分组方式不同,对应不同的加法顺序,在 chunk 边界处会产生状态偏差,而这个偏差会顺着递推关系传染到后面的每一个 token——这是全篇里”误差累积”表现得最明显的一个例子。
解法很巧妙:既然递推是因果的(后面的行不会影响前面的行),那么 decode 某个 token 时,可以直接从上一个 chunk 边界重新按训练时的 64 行分组顺序,把 0 到 重新算一遍——反正最多重算 64 行。这是用”多算一点重复计算”换取”完全相同的分组顺序”,本质上是空间换正确性的思路,也呼应了后面 CUDA Graph 场景下”每个 scheduler slot 预留 64 行”的工程实现。
MoE 层本身的对齐相对更直接:路由用 FP32 softmax 选出 top-8 专家并重新归一化权重(专家选择是离散的,最后一位的浮点误差就可能让第 8 名和第 9 名互换,这也是 MoE 比 dense 模型更容易受 mismatch 影响的根本原因——本文后面会反复回到这一点);专家计算部分,训练引擎直接调用推理引擎现成的 Triton kernel 并只补一个自定义反向。
代价:这一步的性能税明显跳升——flags off 7863 tok/s,只对齐到”except GDN”是 5778 tok/s(73.5%),完全对齐是 4767 tok/s(60.6%,即吞吐降低 39.4%,采样墙钟时间增加 57.9%)。去掉 GDN 的精确 decode 能挽回大约一半的额外墙钟开销,说明循环状态对齐是这批模型里最贵的单项。
我想在这里多停一下,把 Gated DeltaNet 这个例子的教训提炼得更通用一点:任何带有”跨 token 状态”的层(循环神经网络、线性注意力、甚至某些带 KV cache 压缩的注意力变体)都天生比无状态的 dense 层更难对齐。原因在于:无状态的层只需要当前 token 的输入就能算出输出,两个引擎只要对齐好单步的算法就行;但有状态的层的输出依赖于整个历史的”汇总方式”,而这个汇总方式在训练(批量处理)和推理(逐步生成)两种场景下天然就不一样。这也是为什么文章选择先在 dense 模型上立规矩,再引入 Qwen3.5/3.6 这个带循环层的模型——因为循环状态层确实代表了一类独立于前面所有对齐技巧之外的新难题,它需要的不是”把某一次运算对齐”,而是”把整个递推过程中的分组粒度对齐”。
4.4 GLM-5.2:稀疏注意力选择器 + FP8 专家
GLM-5.2 引入了稀疏注意力:模型在若干层里会挑选”值得关注”的历史位置,后续层复用这个选择结果。如果两个引擎的选择器(selector)选出的位置不一样,误差会沿着后续所有层级联下去——这跟前面 RoPE 的 CPU/GPU 表分叉是同一类”一步错步步错”的问题,只是这次错的是”注意力该看哪里”而不是”三角函数值是多少”。作者把 XoRL 的选择器输入构造方式改成跟 SGLang 完全一致(先合并投影 key 和 head gate 再转 BF16、之后各自转 FP8),确保平局时选低位置、按升序返回。
MoE 部分,GLM 在选专家时加了一个 correction bias,但专家权重仍然来自加 bias 之前的 sigmoid 分数——训练和推理如果对这个 bias 的精度处理不一致(BF16 vs FP32)就会选出不同的专家。修复方式是直接从 XoRL 里调用 SGLang 的路由实现。FP8 专家权重的量化则是个很实际的工程决策:与其让两边各自量化再祈祷量化点位一致,不如每次优化器更新后,把 XoRL 的 FP32 主权重量化进 SGLang 的 block 布局里一次,之后的每次前向都读这份缓存好的 FP8 编码——而且只传输”变了的那部分”(每步大约只有 1% 的 block scale 会变化),这是个很聪明的工程优化,顺带解决了正确性问题。
代价:147.6 → 107.2 tok/s(降低 27.4%)。专家路径(路由 + FP8 专家前向 + 按 rank 顺序归约)几乎占了对齐总代价的一半;关掉专家路径对齐能回到 125.9 tok/s。稀疏注意力选择器和 top-k 大约贡献 2 个百分点。dense 部分的 batch-invariant 算子代价几乎可以忽略——因为它们本来占的算力比重就小。
4.5 DeepSeek-V4:四流残差 + 压缩注意力 + MXFP4
DeepSeek-V4 每个 block 维护四条 BF16 残差流,模型自带的 mHC(我理解类似 hyper-connections 的思路)在进入 attention/expert 层之前把四条流混合成一条,计算结束后再混合回去。作者的处理方式是直接调用 SGLang 的 pre-mix/post-mix kernel,训练时把混合系数设为 stop-gradient(不训练 mixer 本身)。
压缩注意力这块工程量最大:每层都用 128-token 滑窗注意力,部分层还要额外关注”摘要”(每 4 个 token 一份的 C4 摘要,每 128 个 token 一份的 C128 摘要)。SGLang 的稀疏 prefill kernel 本身不是字节稳定的,所以作者干脆放弃了并行 prefill 的捷径,改成在 XoRL 里循环调用 decode kernel、一行一行地把 prompt “重放”一遍,代价是慢,但换来了跟真实 serving 完全一致的分页缓存状态。
MXFP4 专家权重带来一个 FP8 没有的额外约束:Marlin 这个专用 kernel 在不同行数下可能返回不同的比特,所以两边都得用同样的”每次只处理未 padding 的实际行数”的方式跑,包括训练时也是一行一行来(放弃了批量化的效率)。
代价:46.0 → 40.2 tok/s(降低 12.7%),是三个 MoE 里最便宜的一个——虽然工程复杂度看起来最高(四条残差流、压缩历史、MXFP4),但因为这次训练用的是 LoRA(基础权重不需要重新量化,专家路径的改动相对局部),实际吞吐损失反而最小。这提醒我们:对齐的工程复杂度和最终的性能代价不是线性相关的,具体取决于哪些计算路径真正在关键路径上被反复执行。
下面这张图是文章里对最终 Wordle 训练结果的汇总(0-mismatch 的 XoRL 对比无 mismatch 修正的 IS baseline,以及两个闭源托管 RL 服务 River 和 Tinker 的 IS baseline):

可以看到 0-mismatch(青色)曲线全程压过 XoRL 自己的 IS baseline,最终以 77.4% 对 63.9% 收尾;River 的 IS baseline 落在 76.4%(跟 XoRL IS baseline 很接近,符合”三个基线的 mismatch 水平相近”这个说法);Tinker 的 IS baseline 只有 49.2%,明显掉队——这就引出了下一节的核心问题。
5. 为什么要付出这么大代价:Tinker 的灾难性发散
如果只看上面这张汇总图,你可能觉得”从 64% 到 77%,提升 13 个点,值不值得为此多付出 20% 的训练开销”是个见仁见智的判断题。但作者接下来展示的这张图,把这个问题变成了一个安全性问题:

同样从 step 56 的 checkpoint 出发,三次 Tinker 训练里有两次在 step 62 突然崩溃。作者排查后发现,Tinker 的基础设施本身没有报错,采样端记录的平均选中 token logprob 全程稳定在 -0.20 附近,rollout 看起来也很正常——问题出在训练端算出来的 logprob 在 step 62 突然大幅偏离采样端的记录,导致重要性比值 出现极端值。
具体数字很触目惊心:Run 1 里有一个单独的 </ token,重要性比值高达 19276.5,这一个 token 就贡献了整批数据平方梯度质量(squared coefficient mass,也就是 在全 batch 里的占比)的 65.7%。Run 3 稍好一点,但排名前四的 token 也贡献了 72% 的梯度质量。相比之下,扛住了这次冲击的 Run 2,最大单 token 只占 0.39% 的梯度质量。
这个对比非常有说服力:mismatch 不是”平均意义上让训练慢一点”,而是可能在某个偶然的瞬间,让个别 token 的重要性比值炸到接近两万倍,进而让整个梯度更新被这一个 token 主导——Run 1 直接学坏了格式,开始在大多数回合里输出非法动作;Run 3 保持了合法的 Wordle 走法,但决策质量明显变差;Run 2 侥幸survive但表现平庸。这不是”训练变慢”的问题,是”训练随时可能无预警地整体报废”的问题。
5.0 一个数值思想实验:为什么 MoE 比 dense 模型更容易被 mismatch 击穿
在继续往下看目标函数的对比之前,我想先补一个思想实验,帮助建立直觉:为什么同样量级的浮点误差,在 dense 模型和 MoE 模型里造成的后果天差地别。
设想一个 dense 模型的某个隐藏维度上,两个引擎算出的激活值分别是 和 ,其中 是几个 ULP(unit in the last place)级别的舍入误差,比如 。这个误差会经过后续的矩阵乘法、归一化、激活函数,逐层传播,但传播方式基本是连续的——下一层的输出相对于这一点误差的变化,可以用泰勒展开的一阶项 来近似,误差的影响幅度和 本身成正比。也就是说,只要 足够小,最终对 logit、对 logprob 的影响也会足够小——这是一个平滑的误差传播通道。
但 MoE 模型在路由层引入了一个不连续的决策点:路由器对每个专家给一个分数,选出分数最高的 top-8。假设第 8 名专家的分数是 ,第 9 名是 ,且 和 非常接近(差距小于误差 )。这时只要两个引擎算出的 排序发生翻转,第 8 名和第 9 名的专家就会对调——而这不是一个连续量的小变化,而是一个离散的二选一开关:训练引擎把计算资源分配给了专家 A,推理引擎却用了专家 B——两者的输出可能完全不相关(因为不同专家的权重本来就没有任何连续性关系)。这就是为什么文中反复强调”一旦路由 top-k 翻转就选中了不同专家,mismatch 的幅度会比网络其他部分大得多”的根本原因:误差传播从连续的“微扰动”变成了离散的“开关翻转”,后果就从“输出略有偏差”变成了“输出来自一个完全不同的子网络”。这也解释了为什么作者在 4.3 节提到的“最后一位的浮点误差就能交换第八名和第九名专家”不是个小问题,而是 Tinker 崩溃案例背后最可能的根本机制——一个 token 的路由翻转,可以让该 token 后续所有计算都走在一条训练引擎从未真正训练过的路径上,而重要性采样比值完全无法修正这种“选错了专家”的错误——它只能修正“同一个专家下概率算得不一样”这种连续型误差,对于“根本就选了不同专家”这种结构性错误,重要性采样基本无能为力——因为两个引擎对同一个 token 的“推理路径”本身就不一样了,不是概率数值差一点点的问题。
5.1 三种目标函数,谁能顶住这种冲击
既然重要性比值可能失控,一个自然的想法是:能不能靠目标函数本身的设计去兜底,而不用花力气做字节级对齐?作者对比了三种处理 的方式:
不裁剪的重要性采样:直接乘上 ,其梯度二阶矩正比于 ,理论上就已经暴露了”少数极端 token 主导梯度”的风险:
PPO 式裁剪:一旦 超出 就直接把这个 token 的梯度整体丢弃:
CISPO:先把 本身裁剪到绝对区间 再乘进梯度里,注意这里裁剪的是”比值”而不是丢弃整个梯度贡献:
实验结果很直白:不裁剪的 IS 在 XoRL 里能训到 63.9%,但仍然明显落后于 0-mismatch 的 77.4%;纯 PPO 裁剪反而是最差的——它把太多”重要”的 token 梯度直接丢掉了,几乎学不到东西(测出来的 mismatch 数值反而很低,但那只是因为策略已经退化到只会生成简单轨迹);CISPO 表现最好,在 XoRL 里能到 72.5%,在 Tinker 上更是直接把表现提升了 25%,几乎追平 0-mismatch 的水平。
这里我的评论是:CISPO 这类”裁剪比值而不是丢弃梯度”的设计确实是目前性价比最高的”止损”方案,如果你没有精力做字节级对齐工程,CISPO 应该是默认的安全网。但作者也诚实地指出:没有一个目标函数能完全打平 0-mismatch 的效果,这说明目标函数层面的修正终究是”事后补救”,treat 的是症状而不是病因。
5.2 Total Router Recall:一个轻量但有代价的折中方案
考虑到 MoE 模型的 mismatch 更容易在路由决策上”离散地”爆发(专家选择要么选中要么没选中,没有中间地带),一个已有的思路(GSPO 的 Routing Replay、R3 的 Rollout Routing Replay)是只把推理引擎选中的专家 id 回放给训练引擎。作者发现单纯回放 id 收益有限——如果推理选了专家 0 但训练给专家 0 打的分很低,你回放了 id 但权重还是训练引擎自己重新算的,等于”专家对了,权重错了”,反而可能制造新的不一致。
于是他们把这个思路往前推了一步——Total Router Recall:连专家的路由权重也一起回放,而不只是 id。效果很好,几乎追平 bitwise-identical 的表现,K3 全程稳定在 3e-4。但代价也很实际:路由器本身没法再正常训练(只能用 stop-gradient 技巧走一个一阶近似的梯度,且这个近似的误差项本身就来自两个路由器的差异——某种意义上是”用一种已知的偏差换取另一种已知的偏差”);而且需要把每个 token、每一层 MoE、每个被选中专家的权重都以推理侧的布局传给训练侧,这是一笔不小的数据量,作者甚至提到得靠 Mooncake 或者直接过文件系统来传,不能塞进普通的 rollout record 里。
我觉得这部分作者写得很克制——没有把 Total Router Recall 包装成”完美的替代方案”,而是明确说”这是我们目前能想到的、在不追求 bitwise-identical 的前提下最好的折中”,并且指出如果你的 client 端还想对 token/logprob 做任何后处理(比如截断),路由 metadata 也得同步镜像处理,工程上相当”kludgey”(他原话)。这种坦诚在系统类论文里其实不算常见。
5.3 异步 RL 的代价,以及 Streaming RL 这个折中
即便做到了 bitwise-identical 的同步 RL,异步 RL(Async RL,训练和采样并行跑,容忍”稍微旧一点”的权重)依然很诱人——它能让 GPU 利用率更高。但异步本身就会引入”陈旧权重”和”陈旧缓存”两项新的 mismatch 来源,实测下来:即便底座是 0-mismatch 的,纯粹异步化也会让性能明显下滑;但如果把 Total Router Recall 和 CISPO 叠加上去,能挽回相当一部分性能,代价是 K3 在训练早期还是会有一个尖峰,之后逐渐下降。
作者还提出了一个我觉得很优雅的折中方案——Streaming RL:让推理引擎一算完一条完整轨迹就立刻交出去,client 端负责把同一组(group)的轨迹攒齐,只有攒齐了才做一次 forward-backward,全程只在最后做一次 optimizer step——这样梯度更新在数学上跟”整批一起送进去”完全等价,因为优势函数的归一化本来就是在组内做的。由于轨迹长度服从几何分布,训练和采样之间天然能有不错的重叠,不需要引入任何异步带来的 mismatch。实测能省 46% 的串行墙钟时间(跟 PipelineRL 组合后从 338.5s 降到 244.0s,rollouts/s 提升 1.39 倍)。这算是”既要又要”里少有的真正没有代价的免费午餐——前提是你的任务允许按组攒批(一些长思维链的任务里,组内轨迹长度差异可能很大,重叠效果会打折扣)。
6. 我的评论:这篇工作真正的价值在哪
写到这里,说一下我自己的几点看法(既然 Charles 特别要求要有 comment,那我就多说几句真话):
1. 这篇文章最大的贡献不是”消灭了 mismatch”,而是把 mismatch 这个模糊概念拆成了可操作的工程清单。 附录 A 那张操作映射表(列出了每个算子用什么方法对齐、哪些参数决定了输出字节)本质上是一份”如果你也想复现这件事,照着抄就行”的 checklist。这比很多论文里”我们做了大量工程优化”这种一笔带过的说法要有价值得多。
2. Tinker 的两次崩溃案例,是我认为全文说服力最强的部分。 它把一个抽象的数值稳定性问题,变成了一个具体的、可以量化的风险事件——单个 token 19276.5 倍的重要性比值、65.7% 的梯度质量集中在一个 token 上。这种”黑天鹅”式的风险描述,比任何”平均 K3 降低了 X%“的表述都更能说明为什么大规模生产训练值得为此投入工程资源。
3. 但我也想诚实地指出这项工作的边界。 首先,20% 左右的 step 级别开销(排除 weight sync 后从 334.8s 涨到 409.9s)对于长期跑的大规模训练来说不是小数目,尤其是对 Qwen 系列这种循环状态层占大头的模型,代价会更高;其次,这套方法目前深度绑定在”XoRL + SGLang fork”这一对特定的训练/推理引擎组合上,虽然作者说方法论可以移植到别的引擎,但实际迁移成本(把每一个算子重新过一遍前面那套四选一策略)显然不小;第三,全文的实验场景相对单一——都是 Qwen3.6-35B-A3B 玩 Wordle 这一个任务,虽然选择 Wordle 大概率是为了控制变量、快速迭代(游戏规则清晰、可以快速判定对错),但这也意味着”0-mismatch 带来 13.5 个百分点提升”这个具体数字能不能泛化到更复杂的推理任务或者更大规模的模型上,还需要更多验证。
4. 关于 Total Router Recall 我有一点个人偏好上的保留。 我理解这是一个务实的折中,但”路由器没法正常训练”这个代价其实相当大——对于 MoE 模型来说,路由本身往往是最值得训练、最能决定专家分工效果的部分。如果一个任务里路由的自适应能力很重要(比如专家需要随着训练动态特化),Total Router Recall 的这个牺牲可能比想象中更贵。这也是为什么我觉得 bitwise-identical 这条”贵但彻底”的路线,长期看反而可能是更稳的选择——它不需要在”正确性”和”可训练性”之间做取舍。
5. 最后一个观察,是关于这类工作的”可信度设计”。 全文反复强调”我们用了验证阶梯,从单元测试一路做到端到端”,并且诚实地报告了每一步的性能代价(甚至专门做了”去掉某个对齐步骤后代价降低多少”的消融)。这种写法本身就是在示范一种做数值正确性工作应有的严谨度——不是简单地说”我们做对了”,而是把每一步”为什么相信自己做对了”的证据链摆出来。这对我自己做系统工作也是个提醒:结果好看不代表过程可信,过程可信才是结果好看的前提。
7. 实践清单:如果你也想在自己的栈上做这件事
假设你看完这篇文章,确实认为自己的 RL 训练栏目值得投入这个工程量,根据我自己参与这类工作的经验,整理一个实践上的优先级清单,供参考:
第一步:先修取样本本身的奇异值,而不是上来就写归约层。 在把精力投入到 batch-invariant kernel 之前,先把 K3 监控接上去,看看是否存在像 Tinker 那样的少数 token 极端值。如果大部分时间 K3 都很小但偶尔出现尖峰,那么你面对的很可能就是本文描述的这种问题,而不是单纯的训练不稳定。
第二步:先上 CISPO,再考虑字节级对齐。 文中的消融实验说明,CISPO 均能在 XoRL 和 Tinker 两套不同基础设施上带来显著改善,而且实施成本远低于重写 kernel——只需要改一行目标函数代码。把这一步当作“先止血”的必选项,而不是可选项。
第三步:先针对 MoE 路由层做局部对齐,而不是一来就想做到 bitwise-identical。 因为本文 5.0 节的思想实验表明,路由层的离散选择是最容易出大事的地方,而 dense 层的连续型误差相对容易容忍。优先确保路由投影的 gate 投影两边一致、top-k 选择逻辑一致(包括平局时的排序规则)。
第四步:建立你自己的验证阶梯,不要跳过中间步骤。 直接看端到端指标很难定位问题在哪一层。参照文中 3.3 节的五阶验证梯子,至少先确保第一步(同一程序跑两次输出完全一致)和第二步(改 batch size 结果不变)——这两步完全可以在单引擎内部完成,不需要跨引擎对比,性价比最高。
第五步:如果模型里有循环层(类似 Gated DeltaNet / Mamba 这类线性注意力),要特别小心。 本文 4.3 节的教训是:训练时的分组方式(多少个 token 一批)必须与推理 decode 时的实际重算策略完全匹配,否则误差会在 chunk 边界处积累并沿递推关系传播到后面所有 token。如果你的模型里有这类层,要专门设计测试用例验证长序列下的状态一致性,不能只测短序列。
第六步:接受一个现实:完全对齐不是免费的,要提前预算吧带来的吸进度下降。 本文的数据说明,越复杂的模型(尤其是带循环层的 MoE)代价越高(GDN 对齐均均耗去了接近一半的额外开销)。提前把这笔账算进集群预算,避免在上线前才发现吸进度降幅超出预期。
8. 术语速查
| 术语 | 含义 |
|---|---|
| Train-infer mismatch | 训练引擎和推理引擎对同一份权重、同一段文本算出的 logprob 不一致 |
| K3 估计量 | ,衡量 mismatch 程度的非负指标,仅在 时为 0 |
| Bitwise-identical | 逐字节完全相同,比”K3=0”更强的对齐保证 |
| Batch-invariant kernel | 归约操作的加法顺序不随 batch size 变化而改变的 kernel 实现 |
| Router Replay / Total Router Recall | 把推理引擎的专家路由决策(id,或 id+权重)回放给训练引擎以减少 MoE 路由处的 mismatch |
| CISPO | 裁剪重要性比值本身(而非丢弃整个梯度)的 RL 目标函数 |
| Streaming RL | 轨迹算完即交、按组攒批、无异步 mismatch 的训练-采样重叠方案 |
9. 写在最后:这件事情跟我自己的关系
最后再多说两句,算是对开头那句题外话的呼应。我在 XoRL 这个项目里具体参与的部分,主要集中在分布式训练的并行化切分和部分 MoE 路由相关的讨论上,这也是为什么我对第 4.2 节那个“数据并行为主、上下文并行为次”的逻辑标签设计印象特别深——它确实是我们当时为了让专家输出归约在任意并行组合下都保持一致而反复迭代出来的方案,中间走过不少弯路——最初的版本直接按 rank id 排序,在纯数据并行下没问题,但一旦引入上下文并行,同一个 token 的行会落在不同的 rank 上,直接按 rank id 排序就会让同一个 token 在两种并行方式下得到不同的归约树形状——这个坐坑我们自己踩过一次后才意识到需要引入逻辑标签这层间接寻址。文章里说得很轻巧的一句话(“给每个贡献者一个标签,标签以数据并行为主、上下文并行为次”)背后,其实是一段不短的调试历史。
我也想诚实地说,自己在这篇笔记里对自己参与过的工作提出了不少批评(比如对 Total Router Recall 的保留、对实验场景单一性的指出),这不是自我贬低,而是我真心觉得一项工作能经得起自己人的批评,才是它真正经得起时间考验的前提。如果你正好也在做训练/推理数值对齐相关的工作,欢迎直接去看 XoRL 和 XoRL-SGLang 的源码——文章里链接的每一个 PR 都是真实可运行的代码,而不是伪代码示意。
参考链接
- Ashwinee Panda, 0 train-infer mismatch for Open-weight MoE RL in Open-source code
- XoRL / XoRL-SGLang(开源代码)
- Shao et al., “DeepSeekMath” (2024) — arXiv:2402.03300,GRPO 的出处
- Schulman, “Approximating KL Divergence” (2020) — K3 估计量的出处
- MiniMax-M1 (2025) — arXiv:2506.13585,CISPO 的出处
- Yang, Kautz, Hatamizadeh, “Gated Delta Networks” (2024) — arXiv:2412.06464
- Zheng et al., “Group Sequence Policy Optimization” (GSPO) — arXiv:2507.18071,Routing Replay 的相关工作
笔记写于 2026-08-17,原文来自 Ashwinee Panda 的个人博客,代表 XoRL 项目团队(含笔者本人)在训练-推理数值一致性方向的工程实践总结。