训练不稳,可能是推理引擎的锅:Miles 的三条对齐路线
2026-09 基于 radixark/miles 源码(HEAD 7daba3a,2026-09-14)与其 arXiv 技术报告(2609.08368)逐条核对。文中源码引用以file:line标注;性能数字全部为厂商口径,截至写作时没有任何第三方独立复现。
一、同一份权重,两个概率
把一个训练好的模型权重同时交给两个系统(SGLang 做推理、Megatron 做训练),让它们对同一个 token 序列各算一遍 log-probability,得到的数字不会一样。
这不是 bug。两边用不同的 kernel、不同的数值精度、不同的归约顺序做同一件事,而浮点加法不满足结合律,结果自然不同。
问题在于,RL 后训练要求它们相同。
RL 的每一步都要算这样一个比值:当前策略生成某个 token 的概率,除以生成它时所用策略的概率。这正是 PPO 一类的算法的立足点:只有分子分母出自同一个策略,这个比值才是无偏的。而 rollout 是 SGLang 产的、概率是 Megatron 重算的,两者只要对不上,策略梯度的方向就不再是你以为的那个方向。
Miles 的文档把这件事写成了一句话:
A common failure mode in MoE RL is precision drift between training and inference. Pipelines that train in BF16 and serve in FP8 accumulate per-layer numerical disagreement, which compounds into divergent log-probabilities and gradients pointing in unintended directions.
(docs/advanced/low-precision.md)
先看这个差异的真实量级。Miles 技术报告里的 GLM-5.2 参考运行,训练与推理对同一批采样 token 的 log-prob 分歧,100 步均值是 0.0369;LMSYS 博客里 DeepSeek-V4.1 那次运行,平均绝对差是 0.017–0.025 nats。
两个数字都不大,也都没有到 0。后面会看到,「到 0」是有专门做法的,而那套做法目前还只能用在很小的模型上。
二、裂缝从四个地方来
要理解「对齐」为什么会成为一个工程问题,先得知道不一致是怎么攒出来的。
精度路径不同。 训练侧常跑 BF16,推理侧为了吞吐跑 FP8 甚至 FP4。同一个权重,一边按 BF16 舍入、一边按 FP8 舍入,每一层就多出一点偏差;几十层叠上去,log-prob 的差就出来了。Miles 文档用的词是「compounds」。
kernel 实现不同。 矩阵乘法在 GPU 上怎么切块、怎么累加、用哪条指令路径,不同实现给出的有效数字不同。Miles 在实现逐位对齐时踩到的正是这一层:它必须让两侧都走 FlashAttention 3,因为只有这个后端在 prefill 与 decode 之间是逐位相等的。
batch 组成会影响结果。 大多数 kernel 的输出依赖 batch 里的其他样本,归约顺序随 batch 形状变。这类 kernel 叫 batch-variant,是数值对齐的前提性障碍,Miles 用的是 Thinking Machines Lab 开源的那组 batch-invariant 算子。
MoE 的路由可能不同。 专家路由靠 Top-k 打分,一旦出现分数并列,两个系统可能选中不同的专家。走错专家意味着换了一条计算路径。
前三条是数值问题,第四条是结构问题,处理方式也不一样。
三、路线一:把差异消掉
Miles 最常见的那条路,目标定得很绝对:
True on-policy ensures that the log probs generated by inference engine (SGLang) is strictly equal to the one generated by the training Engine.
(examples/infra_features/true_on_policy/README.md:3)
实现思路是让训练与推理的每一个操作都逐位相等(bitwise equal):
- 注意力:两侧都用 FlashAttention 3,因为它在 prefill 与 decode 之间逐位相等;
- GEMM:用 DeepGEMM 保吞吐,同时靠它对 tensor core 指令的选择保持一致性;
- batch-invariant 算子:逐位对齐的前提,采用 Thinking Machines Lab 的开源实现;
torch.compile:把大量小 kernel 合并,减少逐位对齐需要覆盖的面。
再往下是磨细节:对齐两侧的 op dtype、替换 Megatron 侧 TE 的 fused 实现、关掉 fused rotary embedding、SGLang 开 deterministic inference、把 cuBLAS/TE/NCCL 都配成确定性、rollout 侧改用一次 prefill pass 重新打分而不是回报 decode kernel 的 log-prob(miles/true_on_policy/config.py:204-207)。
验收标准是一个数:wandb 里的 train/train_rollout_logprob_abs_diff 必须严格等于 0。这个指标在训练循环里实时算并上报(miles/backends/training_utils/loss_hub/losses.py:340-374),CI 里的断言用的是无容差的 Python ==:
# miles/backends/training_utils/log_utils.py:267-302
if args.ci_test and args.true_on_policy_mode and not args.ci_disable_logprobs_checker:
assert log_dict["log_probs"] == log_dict["rollout_log_probs"], (
f"CI check failed: true_on_policy_mode is enabled, but log_probs ...")
同一个文件里其他检查都留了余地:训练侧与参考模型之间 1e-8,训练侧与推理引擎之间 0.03。唯独这一条不留。「逐位对齐」在这里是会挂 CI 的断言。
代价,以及它现在能覆盖多远
代价很直接:确定性 kernel 放弃一部分优化空间,rollout 会变慢。Miles 官方示例的结论是「rollout time shows an acceptable slowdown」(同 README),但没给具体百分比。
更硬的边界在代码里:
第一,只有 FSDP 后端支持。 Megatron 是 Miles 的默认后端、也是万亿参数级唯一现实的选择,而它直接抛异常:
# miles/backends/megatron_utils/arguments.py:14-20
def set_default_megatron_args(args):
if getattr(args, "true_on_policy_mode", False):
raise NotImplementedError(
"--true-on-policy-mode is not supported on the megatron backend with this Megatron "
"version; support lands in a follow-up PR. Use --train-backend fsdp for true-on-policy."
)
第二,只有 Qwen3 dense 一族注册了对齐契约。 启用它的前提是存在对应模型族的 parity contract:
# miles/true_on_policy/schema.py:26-34
QWEN3_DENSE_TRUE_ON_POLICY_V1_SCHEMA = TrueOnPolicyContractSchema(
name="qwen3_dense_true_on_policy_v1",
model_family="qwen3_dense",
logprob_contract="sglang_prefill",
sglang_attention_backend="fa3",
fsdp_attention_implementation="flash_attention_3",
disable_megatron_sequence_parallel=True,
)
超出已注册的 profile 直接拒绝启动。也就是说,逐位对齐目前是「小模型 + FSDP + Qwen3 dense」的可用路径,不是通用能力。
四、路线二:让两侧走同一条量化路径
逐位对齐很难推到大规模,Miles 于是做了第二条路:不追求「消除差异」,而是让 rollout 和 trainer 的前向用同一套量化逻辑。既然两侧都在做同样精度的舍入,偏差就不会单向累积。
这条路的成品是三种端到端低精度配方:
| 格式 | 块布局 | 硬件 | 成熟度 |
|---|---|---|---|
| FP8 block-wise(DeepSeek 风格) | 128×128,FP32 scale | Hopper + Blackwell | 已可用 |
| MXFP8 | 1×32,UE8M0 scale | 仅 Blackwell | Beta |
| NVFP4(E2M1) | 1×16,两级 scale(FP8 + FP32) | 仅 Blackwell | Beta |
关键不在于选了哪种格式,而在于契约覆盖了整条链路:checkpoint 转换、trainer 前向、SGLang rollout、在线权重导出,四个阶段必须共用同一套量化规则;声明为高精度的张量在这四段里必须一致地保持高精度。
Miles 把这件事做到了量化器一级:FlashInfer(推理侧)与 TransformerEngine(训练侧)的量化器对齐到同一个 bit-level contract,单元测试对参考实现做逐字节校验。这背后是一个很容易踩的坑:
serving 环境默认会在 FP4 量化路径上开 fast math。This is a reasonable serving default, but RL weight updates benefit from exact agreement with the training-side quantizer.
所以 RL 配方反过来把 fast math 关掉(FLASHINFER_DISABLE_FP4_QUANT_FAST_MATH=1)。推理侧为了吞吐开的优化,在这里必须为训练侧的一致性让路。
同一条逻辑还体现在 NVFP4 配方的取舍上。NVFP4 预训练会用随机舍入(SR)和随机 Hadamard 变换(RHT)当稳定器,RL 配方把两者都去掉,理由是量化噪声必须压得比真实更新信号更低,否则会把脆弱能力直接覆盖掉,改为按 token 在线算 FP32 scale。
一条可以直接用的经验结论
Miles 试了按层把一部分权重留在 BF16,结论很具体:保留最后 15% 的层用 BF16,能明显降低训练-推理偏差、稳定梯度;而保留最前面的层没有帮助。
另一条同样实用:MLA 结构里有几个投影张量(kv_b_proj 以及 absorbed 的 k_up_proj / v_up_proj)要留在 BF16,因为 absorbed 与非 absorbed 模式的收缩轴不同,而 MXFP8 的块是一维的,套不上。
五、路线三:消不掉就修
前面两条都要付代价:一条要让 rollout 变慢、还只支持小模型,一条要上 Blackwell。所以在更多场景下,Miles 走的是第三条:接受差异存在,然后修正它。
修正分两个层次。
5.1 先修结构性的:R3 重放专家路由
MoE 的路由不一致不是数值噪声,是走错路径,重要性采样修正不了。Miles 的做法(--use-rollout-routing-replay)是:让 SGLang 在生成时把选中的专家一并返回,训练前向时直接回放,跳过自己的 Top-k gating。
rollout 侧打开 enable_return_routed_experts(miles/backends/sglang_utils/sglang_engine.py:133-136),返回的张量形状是 (num_tokens-1, num_layers, moe_router_topk)、int32(miles/utils/types.py:99-104)。训练侧用一个双游标的回放缓冲消费它:
# miles/utils/replay_base.py:28-41
def pop_forward(self) -> torch.Tensor:
top_indices = self.top_indices_list[self.forward_index]
self.forward_index += 1
return top_indices
def pop_backward(self) -> torch.Tensor:
top_indices = self.top_indices_list[self.backward_index]
self.backward_index += 1
return top_indices
两个独立游标是为了应对激活重算:同一批路由要被前向和反向各消费一次。
开销是明码标价的:每个 routing 张量占 (num_tokens − 1) × layers × top_k × 4 字节。32K token、60 层、top_k = 8 的序列,每条轨迹约 60 MB。所以它不是无脑开的。文档明确列出什么时候不需要 R3:模型是 dense 的,或者已经用 TIS 把 off-policy 项屏蔽掉了。
5.2 再修数值残余:TIS
数值层面消不掉的残余偏差,用截断重要性采样兜底。Miles 内置的实现很短:
# miles/backends/training_utils/loss_hub/corrections.py:7-32
def vanilla_tis_function(...):
"""Truncated importance sampling: clamp `exp(train - rollout)` to
`[tis_clip_low, tis_clip]` and multiply into `pg_loss`."""
tis = torch.exp(old_log_probs - rollout_log_probs)
tis_weights = torch.clamp(tis, min=args.tis_clip_low, max=args.tis_clip)
pg_loss = pg_loss * tis_weights
思路是给每个 token 的比值算一个权重,再把它夹在区间里:偏差大的样本不被完全信任,也不被完全丢弃。默认上界是 2.0。需要更细的策略时,可以挂自定义函数(--custom-tis-function-path),示例目录里给了一套带 12 个监控指标的 MIS 实现。
异步 RL 里这个修正不是可选项。 Miles 在启动时强制要求显式选择行为策略:
# miles/utils/arguments.py:3696-3714
def validate_async_off_policy_correction(args) -> None:
"""Require an explicit behavior-policy choice for async PPO training."""
assert args.use_rollout_logprobs or args.use_tis or args.keep_old_actor, (...)
因为异步 rollout 天然跨多个权重版本,样本本来就不是严格 on-policy 的,必须说清楚用哪种方式处理。
六、这三条路各自走到哪了
把边界收拢一下。
逐位对齐(路线一)的适用范围最窄。 FSDP 后端 + Qwen3 dense + 0.6B/4B 两个 profile,超出就拒绝启动。官方示例的 README 里那句「Potentially scale it to larger scale models if it is proven to be effective」,写在 Future Works 一节。
大模型上跑的都不是路线一。 GLM-5.2 那次运行的 log-prob 分歧是 0.0369,V4.1 那次是 0.017–0.025 nats,都不是 0。这两个数字来自 Miles 自己的参考运行,说明在万亿参数规模上,走的是「对齐 + 修正」而不是「消除」。
DeepSeek-V4.1 在 Miles 的公开代码里不存在。 仓库里有一份 225 行的 V4.1 训练侧文档,写明了 Megatron 插件位置、模型定义、启动脚本,甚至给了 checkpoint 转换命令,但它引用的 miles_plugins/models/deepseek_v41/、scripts/models/deepseek-v4.1.py、scripts/run_deepseek_v41.py、tools/dsv41_cut_cast.py 全部不存在,全仓 engram 相关文件为 0,v41 在代码目录下零命中。文档自己还写着那个 cast 工具「is kept with the bring-up scripts」。
也就是说,LMSYS 博客里描述的 V4.1 集成,从公开仓库既无法复现也无法验证。这在开源系统软件里并不罕见,文档先行、代码滞后,但它决定了我们能把哪些说法当一手证据。
没有独立验证。 截至 2026-09-14,没有找到任何第三方复现或独立基准。所有性能数字(步时、权重同步加速比、显存降幅、reward 曲线)无一例外来自 RadixArk 自己的运行,论文也自陈「some measurements come from one configuration rather than many」。
顺带一提,Miles 自己的文档有四处引用了不存在的文件。其中包括 miles/ray/train/actor_factory.py,本文第四节引用的那份低精度文档,正是引用它的两处之一。这份文档的机制描述可信(能在源码里对上),但具体路径引用要自己核。
七、为什么这对做推理的人重要
过去几年,「推理引擎怎么选 kernel」被当成一个纯吞吐问题:哪个后端快、哪条路径省显存。这篇材料给出的是另一面。
推理引擎的每一次数值选择,都是上游训练的输入。
具体地:
- 推理侧为吞吐默认打开的 fast math,会让 RL 的权重更新失去与训练侧量化器的精确一致,所以 RL 配方得把它关掉;
- 推理侧选哪个注意力后端,决定了训练侧能不能做逐位对齐:FA3 正是因为它在 prefill 与 decode 之间逐位相等才被选中;
- 推理侧的精度格式(FP8 / MXFP8 / NVFP4)不再是推理单方面的决定,而要和 trainer 的量化规则签一份覆盖四个阶段的契约。
Miles 之所以把 SGLang 和 Megatron 缝在一起,一部分原因就在这:这两个系统的一致性,是它自己的正确性问题,不是别人的。
源文件索引
本文的「源码」是 Miles 仓库(radixark/miles,HEAD 7daba3a)。
| 文件 | 关键内容 |
|---|---|
docs/advanced/low-precision.md |
训练-推理精度漂移的问题陈述、三种低精度格式、兼容性矩阵与例外清单 |
examples/infra_features/true_on_policy/README.md |
True On-Policy 的定义、bitwise 声明、验收指标、实现要点 |
miles/true_on_policy/schema.py |
QWEN3_DENSE_TRUE_ON_POLICY_V1_SCHEMA 等 parity contract |
miles/true_on_policy/config.py |
--true-on-policy 展开出的参数与环境变量 |
miles/backends/megatron_utils/arguments.py |
Megatron 后端对 true-on-policy 的 NotImplementedError |
miles/backends/training_utils/loss_hub/losses.py |
train_rollout_logprob_abs_diff / train_rollout_kl 的计算与上报 |
miles/backends/training_utils/log_utils.py |
CI 断言(true-on-policy 用无容差的 ==) |
miles/backends/training_utils/loss_hub/corrections.py |
vanilla_tis_function 与 icepop_function |
miles/utils/replay_base.py |
R3 的 Replay 双游标缓冲与 Top-k 拦截 |
miles/backends/sglang_utils/sglang_engine.py |
rollout 侧打开路由返回 |
miles/utils/types.py |
routing 张量的形状与长度不变式 |
miles/utils/arguments.py |
TIS / R3 的 flag 定义与跨 flag 校验(含异步 PPO 的行为策略断言) |
docs/models/deepseek/deepseek-v4-1-flash.md |
V4.1 训练侧文档(引用的实现文件均在公开树中缺失) |
miles/backends/megatron_utils/megatron_to_hf/processors/quantizer_{fp8,mxfp8,nvfp4}.py |
三种精度的导出量化器 |
参考资料
- RadixArk, Miles——开源仓库,Apache-2.0,fork 自 slime
- RadixArk, Miles v0.1: Production-Level Post-Training, arXiv:2609.08368, 2026——技术报告(34 页)
- SGLang & Miles Teams, SGLang and Miles Add Day-0 Support for DeepSeek-V4.1, LMSYS, 2026-09-10——§6 为 RL 训练侧
- Ziang Li & Miles Team, Towards Blackwell-Native 8-bit and 4-bit RL, LMSYS, 2026-07-29——MXFP8 / NVFP4 的 RL 配方与实测
- Miles v0.1: Production-level Post-training, LMSYS, 2026-08-18
- PyTorch, Miles: A PyTorch-Native Stack for Large-Scale LLM RL Post-Training, 2026-06-30
- batch_invariant_ops——batch-invariant 算子的开源实现
相关阅读
- 把 KV Cache 压缩推到极限:DeepSeek-V4.1-Flash 技术报告精读——同一个模型的推理侧,其 §十一 记录了引擎落地形态
- 当 Agent 流量成为推理系统的主要负载——长程 Agent 负载如何改变推理系统的设计前提