Prhub

#7466 [cfg, megatron, doc] fix: drop unused actor.router_replay in favor of engine config

原始 PR 作者 YeonwooSung 合并时间 2026-08-24 10:43 文件变更 10 提交数 2 评论 2 代码增减 +59 / -45

执行摘要

移除 actor.router_replay 死键,路由回放统一走引擎配置

大 MoE 训练中训练引擎与推理引擎的路由计算差异会导致专家选择不一致、引入训练噪声,路由回放(R2/R3)通过锁定专家路由路径来稳定训练(见 #3762 引用的 GSPO 论文 arXiv:2507.18071)。#7463 定位到两套独立 schema:顶层 actor.router_replay(死键,无人消费)与引擎 actor.{megatron,veomni}.router_replay(实键),worker 只读引擎副本,官方 NPU 指南却指向死键。Issue 原文:"Following the NPU guide (or actor.yaml) silently leaves routing replay disabled. For large MoE this is train/infer route mismatch with no error." 同时 experimental/separation/ray_trainer.py 用顶层死键决定 R2/R3 冲突处理,导致正确设置 actor.megatron.router_replay.mode=R2 的用户仍走 R3 分支。PR body 明确决策:"drops the unused top-level key instead of aliasing it"——删除而不是兼容。

值得快速精读。这是一个典型的“配置死键静默失效”修复案例,核心看点是 review 中 wuxibin89 的“直接删除优于兼容”决策如何改变实现方向,以及用契约测试锁定配置 schema 的写法。对后续任何“文档与代码不一致、双份配置入口”问题都有参考价值。

讨论亮点

wuxibin89 在 verl/trainer/config/actor/actor.yaml 上提出关键意见:"I think we can drop actor.router_replay since it have been move to actor.{megatron,veomni}.router_replay"——与其在 issue 建议的“复制/冲突报错”方案上做兼容层,不如直接删除死键,让配置模型只保留一个权威入口。作者回复确认删除方案落地:"Dropped actor.router_replay from actor.yaml and ActorConfig. Routing replay is now only actor.{megatron,veomni}.router_replay, matching this review. Docs and generated trainer YAMLs are updated; actor_rollout_ref.actor.router_replay.mode=... will no longer compose." 即错误配置会立即失败而不是静默忽略。

实现拆解

  1. 定位双份 schema 与消费差异:依据 #7463 复现脚本确认 verl/workers/engine_workers.py 仅读取 self.config.actor.{megatron,veomni}.router_replay,而 ActorConfig / actor.yaml 暴露的顶层 router_replay 在组装 TrainingWorkerConfig 时被丢弃,是纯 no-op。
  2. Schema 收敛verl/workers/config/actor.py 删除 ActorConfig.router_replay: RouterReplayConfig 字段及其 Args 文档;verl/trainer/config/actor/actor.yaml 删除 19 行 router_replay 配置块;同步清理 4 份生成 YAML(_generated_ppo_trainer.yaml_generated_ppo_megatron_trainer.yaml_generated_ppo_veomni_trainer.yaml_generated_ppo_torchtitan_trainer.yaml),保证 Hydra struct 模式实例化不报错。
  3. 修正分离式训练器verl/experimental/separation/ray_trainer.py_fit_compute_log_prob 中,路由冲突处理从读顶层死键改为根据 actor.strategy 定位 actor.megatron / actor.veomni 子配置再取 router_replay.mode,非引擎策略回退 disabled(与旧默认行为等价)。
  4. 文档与测试配套transfer_to_npu_guide.md 的 R2/R3 示例改为引擎键并注明“顶层 actor.router_replay 已移除”;parameter_and_metrics.md 参数表同步修正。新增 tests/workers/config/test_actor_router_replay_sync_on_cpu.py,3 个 CPU 测试分别断言“顶层无该字段”“megatron 引擎可承载 R3”“veomni 引擎可承载 R2”。
  5. 实现演进:首个 commit 按 issue 建议实现“honor or fail”(把顶层值复制到引擎键、冲突时抛错);wuxibin89 review 提出“直接删除”后,第二个 commit 改为删除方案并同步更新全部文档与生成配置。
文件 模块 状态 重要度
verl/experimental/separation/ray_trainer.py 分离训练 modified 5.48
verl/workers/config/actor.py 配置模型 modified 4.49
verl/trainer/config/actor/actor.yaml 配置文件 modified 4.13
tests/workers/config/test_actor_router_replay_sync_on_cpu.py 单元测试 added 6.49
verl/trainer/config/_generated_ppo_megatron_trainer.yaml 生成配置 modified 3.31
verl/trainer/config/_generated_ppo_torchtitan_trainer.yaml 生成配置 modified 3.31
verl/trainer/config/_generated_ppo_trainer.yaml 生成配置 modified 3.31
verl/trainer/config/_generated_ppo_veomni_trainer.yaml 生成配置 modified 3.31
docs/ascend_tutorial/dev_guide/model_dev/transfer_to_npu_guide.md NPU 文档 modified 2.4
docs/ascend_tutorial/dev_guide/model_dev/parameter_and_metrics.md NPU 文档 modified 1.53

关键符号

_fit_compute_log_prob test_actor_config_has_no_top_level_router_replay test_mcore_router_replay_lives_on_engine test_veomni_router_replay_lives_on_engine

关键源码片段

verl/experimental/separation/ray_trainer.py core-logic

核心逻辑修复点:R2/R3 冲突处理从读顶层死键改为按 strategy 读引擎子配置,消除与主 worker 的口径不一致。

# verl/experimental/separation/ray_trainer.py
# 分离式训练中 R2 / R3 的路由冲突处理,统一读取引擎侧配置。
# 修复前读取顶层死键,导致 `actor.megatron.router_replay.mode=R2`
# 时仍误走 R3 分支。
if "routed_experts" in batch.batch and "routed_experts" in old_log_prob.batch:
    actor_cfg = self.config.actor_rollout_ref.actor
    # 按 training strategy 定位对应的引擎子配置块
    if getattr(actor_cfg, "strategy", None) == "megatron":
        engine_cfg = getattr(actor_cfg, "megatron", None)
    elif getattr(actor_cfg, "strategy", None) == "veomni":
        engine_cfg = getattr(actor_cfg, "veomni", None)
    else:
        engine_cfg = None
    # 引擎配置缺失时回退 disabled,与旧默认行为等价
    router_mode = getattr(getattr(engine_cfg, "router_replay", None), "mode", "disabled")
    if router_mode == "R2":
        # R2 回放旧路由:丢弃当前前向计算出的 routed_experts
        batch.batch.pop("routed_experts")
    else:
        # R3 沿用新路由:丢弃 old_log_prob 中的 routed_experts
        old_log_prob.batch.pop("routed_experts")
tests/workers/config/test_actor_router_replay_sync_on_cpu.py test-coverage

新增契约测试:断言 ActorConfig 无顶层 router_replay,且 megatron / veomni 引擎子配置可正常承载 R2 / R3。

# tests/workers/config/test_actor_router_replay_sync_on_cpu.py
from dataclasses import fieldsfrom verl.workers.config.actor import ActorConfig, McoreActorConfig, VeOmniActorConfig
from verl.workers.config.engine import EngineRouterReplayConfig, McoreEngineConfig, VeOmniEngineConfig
from verl.workers.config.optimizer import OptimizerConfig
​
​
def test_actor_config_has_no_top_level_router_replay():
    # 契约断言:ActorConfig 不得再暴露顶层 router_replay 字段
    assert "router_replay" not in {f.name for f in fields(ActorConfig)}
​
​
def test_mcore_router_replay_lives_on_engine():
    # Megatron 路由回放只挂在 actor.megatron.router_replay 上
    cfg = McoreActorConfig(
        rollout_n=1,
        ppo_micro_batch_size_per_gpu=1,
        megatron=McoreEngineConfig(router_replay=EngineRouterReplayConfig(mode="R3")),
        optim=OptimizerConfig(lr=1e-6),
    )
    assert not hasattr(cfg, "router_replay")
    assert cfg.megatron.router_replay.mode == "R3"
​
​
def test_veomni_router_replay_lives_on_engine():
    # VeOmni 引擎同理:R2 模式挂在 actor.veomni.router_replay 上
    cfg = VeOmniActorConfig(
        rollout_n=1,
        ppo_micro_batch_size_per_gpu=1,
        use_remove_padding=True,
        veomni=VeOmniEngineConfig(router_replay=EngineRouterReplayConfig(mode="R2")),
        optim=OptimizerConfig(lr=1e-6),
    )
    assert not hasattr(cfg, "router_replay")
    assert cfg.veomni.router_replay.mode == "R2"

评论区精华

是否直接删除 actor.router_replay 而不是做兼容 设计

wuxibin89 在 `verl/trainer/config/actor/actor.yaml` 上评论:"I think we can drop `actor.router_replay` since it have been move to `actor.{megatron,veomni}.router_replay`"。这比 issue #7463 建议的“复制到引擎键 / 冲突时报错”方案更彻底。

结论:采用删除方案:保留引擎侧唯一入口,顶层键在 Hydra struct 模式下直接报错;作者第二个 commit 落实并同步文档与生成配置。 · 已解决

删除后的配置行为与文档同步确认 other

作者 YeonwooSung 回复确认:"Dropped `actor.router_replay` from `actor.yaml` and `ActorConfig`. Routing replay is now only `actor.{megatron,veomni}.router_replay`, matching this review. Docs and generated trainer YAMLs are updated; `actor_rollout_ref.actor.router_replay.mode=...` will no longer compose."

结论:双方对齐:死键不再 compose,错误配置立即失败而不是静默忽略。 · 已解决

风险与影响

兼容性(有意的破坏):使用旧键 actor_rollout_ref.actor.router_replay.mode=... 的存量脚本会在 Hydra struct 模式下启动报错。由于旧键本就是 no-op,报错比静默失效更安全,但需在 release note 中给出迁移指引。行为修正:experimental/separation/ray_trainer.py 的 R2/R3 判定改读引擎键后,若用户只设置了死键 R2 而引擎键保持 disabled,将从误走 R2 分支变为回退 disabled——这是 bugfix 的预期行为变化。残留不一致:PR body 明确 ref.yamlref.router_replay 未在本 PR 处理,属于已知遗留。测试覆盖局限:新测试只锁定 dataclass 级配置契约,未直接覆盖 _fit_compute_log_prob 的 R2/R3 分支逻辑,分离式训练器改动缺少针对性单测。生成配置漂移:4 份 _generated_*.yamlactor.yaml 的一致性依赖生成脚本,后续重新生成配置时需防止死键复活。

对用户:Ascend NPU 大 MoE 用户按文档配置 R3/R2 将真正生效;错误配置立即报错而非静默失效,避免 train/infer 路由不一致带来的训练不稳定。对系统:消除“第二套真相”,配置模型单源化;experimental/separation 与主 worker 的路由回放判定口径一致。对团队:新增契约测试降低回归风险;后续改动 engine 配置时需同步生成 YAML。影响范围主要集中在 Megatron / VeOmni + 路由回放用户,其余策略(fsdp / torchtitan 等)因 router_mode 回退 disabled 而行为不变。

配置契约变更 存量脚本兼容 separation 行为修正 生成配置漂移 测试覆盖局限

关联 Issue

#3762 Does verl have plans to support routing replay?
#7463 [config][MoE] actor.router_replay.mode is ignored; official docs point at a no-op key

完整报告

参与讨论