Prhub

#6555 [megatron] feat: add dynamic context parallel scheduling

原始 PR 作者 xiaoyao0115 合并时间 2026-08-11 13:13 文件变更 21 提交数 12 评论 20 代码增减 +1500 / -209

执行摘要

Megatron 新增动态 CP 调度,长尾序列吞吐提升约 60%

PR body 明确指出,静态 CP 在长尾序列长度分布(1K 到 16K)下会造成大量不必要的 CP 通信与 padding 工作,而 DCP 可以在每个 packed micro-batch 上按需选用更小的本地 CP group,从而减少开销。ISEEKYAN 在 review 中要求提供性能与精度曲线证明有效性("it would be better if you could provide some perf and precision curve to prove the effectiveness of DCP"),作者随后补充了 +60.6% 吞吐与 loss 几乎不变的基准。此外,ISEEKYAN 强调不能为了 Megatron 特性改动通用 loss 函数("it is not permitted to modify the loss function for megatron's specific feature... we must make our DCP free of changing losses"),这成为实现中把裁剪逻辑放在路由侧而非 loss 侧的重要约束。

值得精读。本 PR 的核心价值有三:一是 thin adapter 设计——把调度算法委托给上游 Megatron-Core,verl 只保留数据路由与输出收集,避免重复实现带来的正确性风险(早期自研 1470 行调度器因多个逻辑错误被整体移除);二是集中式 feature 校验 _check_dcp_unsupported_features,把所有不兼容组合收敛到单一入口;三是 DCP 下 local token loss 与输出顺序恢复的实现细节(leader 收集 + all_gather_object + jagged 重建),可作为分布式训练数据流设计的参考。建议重点关注 verl/utils/dynamic_cp_scheduler.pyverl/workers/engine/megatron/transformer_impl.py 两处。

讨论亮点

核心讨论集中在三处:

  1. 是否应该复用 Megatron-Core 调度器:ISEEKYAN 在 verl/utils/dynamic_cp_scheduler.py 上提问 "is it possible that we import the core algorithm from megatron-core instead of rewrite on verl?",作者回复已改为导入 DefaultDynamicCPScheduler,verl 只保留数据路由(data routing)。这一决策同时消除了 gemini-code-assist 指出的自研调度器多个问题(dcp_make_buckets_equal 阈值递减过快、align_sample_id_groups 切片赋值改变列表长度、fill_empty_gpus 低估打包 workload)。

  2. 不得修改通用 loss 函数:ISEEKYAN 在 verl/workers/utils/losses.py 上明确拒绝为 Megatron 特性改动通用且后端无关的 loss,作者回复 "got it";最终版本中 losses.py 无 DCP 改动,序列裁剪改由路由侧完成。

  3. P0 兼容清单:ISEEKYAN 问 fused kernel(线性交叉熵)对长上下文 RL 至关重要,建议按 fused_kernel、router replay、mtp 排序兼容;作者回复最新修订已支持全部三项,并说明 moe_router_fusion 仍显式不支持(绕过 replay hooks)。

实现拆解

整体实现分为 5 步:

  1. 新增调度适配层 verl/utils/dynamic_cp_scheduler.py(新增 286 行)get_megatron_dynamic_cp_scheduler_cls()megatron.core.datasets.data_schedule 导入 DefaultDynamicCPScheduler,若 Megatron-Core 不含 NVIDIA/Megatron-LM#5154 则直接报错;DynamicCPScheduler.schedule() 按 DPxCP group 与 max_seqlen_per_dp_cp_rank 计算每个 micro-batch 的本地 CP group,并通过 tu.assign_non_tensor_data 给 micro-batch 附加 local_cp_size_dcp_sample_ids_dcp_group_leader_dcp_padding_mask 等元数据;postprocess_dynamic_cp_batch() 只收集每个 CP group leader 的输出,经 all_gather_object 汇总后按 sample id 恢复原始顺序并重建 jagged NestedTensor。

  2. 引擎集成 verl/workers/engine/megatron/transformer_impl.py(+254/-95)_init_device_mesh()mpu 已初始化时校验 DPxCP 世界大小(偶数且不小于 2)并尝试获取动态 DPxCP 分组,未初始化时向 initialize_model_parallel 传入 dynamic_context_parallel=True_check_dcp_unsupported_features() 集中拒绝 FP8、VPP、value model、多模态、distillation 与 moe_router_fusion;fused forward 通过 _resolve_fused_temperature() 支持标量/均匀温度;_attach_dcp_recorded_routes() 按 sample id 把记录的 topk 路由回挂到各 micro-batch 的 model_output

  3. 配套数据契约改造verl/utils/megatron/router_replay_utils.py_context_parallel_layout() 抽取共享,merge_router_topk_indices()/set_router_replay_data() 增加 local_cp_sizecp_layout 透传,新增 align_r3_router_replay_data() 为 rollout 缺失的最后一行 route 补占位,pp_gather() 对嵌套路由先搬到 CPU 再 all_gather;verl/models/mcore/mtp_patch.py 新增 _resolve_cp_group()(优先使用 packed_seq_params.cp_group)与 _get_mtp_loss_config()(按 token 归一化因子调整 MTP scaling);verl/utils/megatron_utils.py 删除旧版 dynamic_cp_split_batch/dynamic_cp_merge_output(基于 2 的幂切分的简化实现,-82 行)。

  4. 配置与文档verl/trainer/config/engine/megatron.yamlref/megatron_ref.yaml_generated_ppo_megatron_trainer.yaml 新增 dynamic_context_parallel 相关配置;新增 docs/advance/dynamic_context_parallel.rst 说明用法与限制;PR body 附带 Qwen3-30B-A3B 的吞吐与 loss 对比基准。

  5. 测试配套(6 个文件):新增 tests/utils/test_dynamic_cp_scheduler.py(调度、padding mask、输出顺序恢复、引擎契约、fused forward 传参)、tests/utils/test_megatron_router_replay_dcp.py(R2/R3 路由与动态 CP group)、tests/special_distributed/test_megatron_dynamic_cp_features.py(真实 4 rank 下 CP2+CP1 混合的 router replay 与 MTP 前反向)、tests/utils/test_megatron_mtp_dcp.py(MTP loss 动态 group 与 token 归一化);修改 tests/utils/test_megatron_bshd_preprocess.py(多维 THD 序列维 padding)与 tests/special_distributed/run_all.sh

文件 模块 状态 重要度
verl/utils/dynamic_cp_scheduler.py 调度适配 added 9.36
verl/workers/engine/megatron/transformer_impl.py 引擎集成 modified 9.05
verl/utils/megatron/router_replay_utils.py 路由回放 modified 8.44
verl/utils/megatron_utils.py 训练工具 modified 7.55
verl/models/mcore/mtp_patch.py MTP 支持 modified 7.47
verl/models/mcore/util.py 序列预处理 modified 7.22
tests/utils/test_dynamic_cp_scheduler.py 单元测试 added 7.94
tests/special_distributed/test_megatron_dynamic_cp_features.py 分布式测试 added 7.69
docs/advance/dynamic_context_parallel.rst 文档 added 4.11
verl/trainer/config/engine/megatron.yaml 配置 modified 3.75

关键符号

DynamicCPScheduler.schedule postprocess_dynamic_cp_batch _local_padding_mask _cp_members _check_dcp_unsupported_features _attach_dcp_recorded_routes _resolve_fused_temperature _resolve_cp_group _get_mtp_loss_config align_r3_router_replay_data merge_router_topk_indices set_router_replay_data preprocess_thd_engine postprocess_thd_engine

关键源码片段

verl/workers/engine/megatron/transformer_impl.py core-logic

DCP 的引擎集成入口:设备网格初始化、不支持特性集中校验、fused forward 温度解析与 router replay 路由回挂,静态 CP 用户也受此重构影响。

# DCP 能力集中校验:所有不兼容组合都收敛到这里,避免散落在各函数中def _check_dcp_unsupported_features(engine_config, model_config, tf_config=None, batch=None) -> None:
    """随着模型、transformer 与 batch 状态逐步可得,分阶段校验 DCP 能力边界。"""
    if not engine_config.dynamic_context_parallel:
        return
    # remove padding 是 THD 打包的前提
    if not engine_config.use_remove_padding:
        raise ValueError("dynamic_context_parallel requires use_remove_padding=True")
    if model_config.model_type == "value_model":
        raise NotImplementedError("Dynamic CP currently supports language models only")
    if hasattr(model_config.hf_config, "vision_config"):
        raise NotImplementedError("Dynamic CP does not support multimodal models")
    if engine_config.virtual_pipeline_model_parallel_size not in (None, 1):
        raise NotImplementedError("Dynamic CP does not support virtual pipeline parallelism")
​
    if tf_config is not None:
        if getattr(tf_config, "fp8", None) not in (None, False):
            raise NotImplementedError("Dynamic CP does not support FP8 training")
        # moe_router_fusion 会绕过 replay hooks,所以与 router replay 互斥
        if engine_config.router_replay.mode != "disabled" and getattr(tf_config, "moe_router_fusion", False):
            raise NotImplementedError("Dynamic CP router replay requires moe_router_fusion=False")
​
    if batch is not None:
        if tu.get_non_tensor_data(batch, key="distillation_use_topk", default=False) or tu.get_non_tensor_data(
            batch, key="distillation_only", default=False
        ):
            raise NotImplementedError("Dynamic CP does not support distillation")
​
​
def _attach_dcp_recorded_routes(losses_reduced: list[dict], layers_topk_idx: torch.Tensor) -> None:
    """把 router replay 记录的 topk 路由按 DCP micro-batch 的 sample id 回挂。    不能丢样本也不能虚构样本,因此前后都要做数量核对。
    """
    recorded_routes = list(layers_topk_idx.unbind())
    cursor = 0
    for micro_batch_index, micro_output in enumerate(losses_reduced):
        micro_sample_ids = micro_output.get(DCP_SAMPLE_IDS)
        if not micro_sample_ids:
            raise RuntimeError(f"DCP router replay micro-batch {micro_batch_index} has no sample ids")
        end = cursor + len(micro_sample_ids)
        if end > len(recorded_routes):
            raise RuntimeError(
                "DCP router replay produced fewer route tensors than scheduled samples: "
                f"need {end}, got {len(recorded_routes)}"
            )
        # 组装成 jagged NestedTensor,保持与 model_output 其他字段一致的布局
        micro_output.setdefault("model_output", {})["routed_experts"] = torch.nested.as_nested_tensor(
            recorded_routes[cursor:end], layout=torch.jagged
        )
        cursor = end
​
    if cursor != len(recorded_routes):
        raise RuntimeError(
            f"DCP router replay produced unconsumed route tensors: consumed {cursor}, recorded {len(recorded_routes)}"
        )
verl/utils/megatron/router_replay_utils.py core-logic

router replay(R2/R3)在 DCP 下的数据契约改造:local_cp_size 透传、R3 路由对齐与 PP 跨 rank 收集的 device 归一化。

def align_r3_router_replay_data(layers_topk_idx: torch.Tensor, input_ids: torch.Tensor) -> torch.Tensor:
    """把 rollout 阶段捕获的 R3 路由与完整训练输入逐序列对齐。    自回归 rollout 只为每个生成 token 记录路由,因此最后一个生成 token
    没有对应 route;R3 replay mask 会把最后的模型行保留为 native,
    这里为每个受影响序列补一个被忽略的占位行,保证路由与模型张量形状一致。
    """
    if not layers_topk_idx.is_nested or not input_ids.is_nested:
        raise TypeError("R3 router replay requires jagged route targets and input_ids")
​
    route_parts = list(layers_topk_idx.unbind())
    input_lens = [int(x) for x in input_ids.offsets().diff().tolist()]
    if len(route_parts) != len(input_lens):
        raise RuntimeError(
            f"R3 router replay has {len(route_parts)} route sequences for {len(input_lens)} input sequences"
        )
​
    aligned_parts = []
    for sample_id, (routes, input_len) in enumerate(zip(route_parts, input_lens, strict=True)):
        route_len = routes.shape[0]
        if route_len == input_len:
            # 已经对齐,直接使用
            aligned_parts.append(routes)
        elif route_len == input_len - 1:
            # 缺最后一行 route,补一个零占位,保持形状完全一致
            placeholder = torch.zeros((1, *routes.shape[1:]), dtype=routes.dtype, device=routes.device)
            aligned_parts.append(torch.cat((routes, placeholder), dim=0))
        else:
            raise RuntimeError(
                f"R3 router replay sample {sample_id} has {route_len} route rows for {input_len} input tokens; "
                "expected equal lengths or exactly one missing final route"
            )
​
    return torch.nested.as_nested_tensor(aligned_parts, layout=torch.jagged)

评论区精华

是否复用 Megatron-Core 调度算法而非重写 设计

ISEEKYAN 在动态 _cp_scheduler.py 指出 verl 不应重写核心调度算法:"is it possible that we import the core algorithm from megatron-core instead of rewrite on verl?"。作者回应已改为导入 DefaultDynamicCPScheduler,verl 只保留数据路由。

结论:核心排程委托给 Megatron-Core 的 DefaultDynamicCPScheduler,verl 侧仅做 TensorDict 适配、padding mask 构造与输出收集;同时消除了自研实现中多个逻辑缺陷。 · 已解决

不得为 Megatron 特性修改通用 loss 函数 设计

ISEEKYAN 在 verl/workers/utils/losses.py 上强调:"it is not permitted to modify the loss function for megatron's specific feature, because it is a universal and backend agnostic function, we must make our DCP free of changing losses"。

结论:最终版本中 losses.py 无 DCP 相关改动,序列长度差异改由路由侧在 micro-batch 准备阶段处理。 · 已解决

fused kernel / router replay / MTP 兼容优先级 性能

ISEEKYAN 提出 fused linear CE 对长上下文 RL 至关重要,建议以 P0 顺序兼容 fused_kernel、router replay、mtp。作者回复最新修订已支持全部三项,moe_router_fusion 仍显式不支持。

结论:三项均兼容:fused CE 接收 local_cp_size 与 DCP padding mask;R2/R3 使用动态 CP group;MTP 使用 packed_seq_params 的 cp_group。moe_router_fusion 被 _check_dcp_unsupported_features 拒绝。 · 已解决

集中管理不支持的 feature 设计

ISEEKYAN 建议:"it is better to centralize all the compromised features to a single place",作者采纳。

结论:新增 _check_dcp_unsupported_features 函数,集中校验 FP8、VPP、value model、多模态、distillation、moe_router_fusion。 · 已解决

自研调度器 dcp_make_buckets_equal 逻辑错误 正确性

gemini-code-assist 指出 remaining_k 已随 bucket 追加递减,再减 len(buckets) 会使阈值下降快一倍,导致 bucket 高度不均衡或为空。

结论:该自研调度器在后续重构中被整体移除,核心排程改由 Megatron-Core 承担,问题随代码删除而消失。 · 已解决

NestedTensor 上直接调用 sum() 的兼容性 正确性

gemini-code-assist 指出 forward_backward_batch 中对 NestedTensor loss_mask 直接 .sum() 在部分 PyTorch 版本不可靠,建议用 .values().sum()。

结论:作者回复 fixed,改用 data.get('loss_mask') 并在缺失时回退到 1.0,保证 loss 归一化在 DCP 路径下可用。 · 已解决

要求提供性能与精度曲线 question

ISEEKYAN 在 review 中要求:"it would be better if you could provide some perf and precision curve to prove the effectiveness of DCP"。

结论:作者在 PR body 中补充 Qwen3-30B-A3B SFT 基准:吞吐 +60.6%,loss 相对差异 0.0229%,并附曲线图。 · 已解决

DCP 处理是否应移入 prepare_micro_batches 设计

ISEEKYAN 建议把 DCP 流程放到 prepare_micro_batches 里让实现更干净,作者回复 okay。

结论:DCP 排程逻辑被组织在 forward_backward_batch 的 dcp 分支中,并通过 DynamicCPScheduler 与 postprocess_dynamic_cp_batch 封装,保持主体流程简洁。 · 已解决

风险与影响

具体风险如下:

  • 上游依赖硬性门槛get_megatron_dynamic_cp_scheduler_cls() 要求 Megatron-Core 包含 NVIDIA/Megatron-LM#5154(commit d2e7ec5b),版本不满足时在引擎初始化阶段直接 RuntimeError;若 actor/ref 等共置引擎的 Megatron 初始化不一致,会因动态 DPxCP group 缺失而报错(代码已提供显式诊断)。
  • 核心路径回归transformer_impl.py 改动幅度大(+254/-95),覆盖 _init_device_mesh_build_tf_configforward_backward_batch 等关键路径;即使 dynamic_context_parallel=False,重构后的初始化逻辑与 context_parallel_size 设置仍可能影响静态 CP 用户。
  • NestedTensor 兼容性:review 曾指出对 jagged 布局直接调用 .sum() 在部分 PyTorch 版本不可靠;当前实现改为先取 loss_mask 再聚合,但 postprocess_dynamic_cp_batchtorch.nested.as_nested_tensor 的可用性仍依赖 PyTorch 版本。
  • 输出收集开销postprocess_dynamic_cp_batch 通过 all_gather_object 把每个 leader 的 model_output 先 detach 到 CPU 再收集,长序列大 log_prob 场景下存在额外同步与拷贝开销。
  • R3 对齐假设align_r3_router_replay_data 假设 route 行数等于 input_len 或 input_len-1,否则抛错;rollout 侧若改变路由记录粒度,该假设会静默失效前的报错会中断训练。
  • 功能限制面:FP8、VPP、value model、多模态、distillation、moe_router_fusion 均被显式拒绝,用户误用时错误信息清晰,但覆盖范围有限。

对用户:开启 actor_rollout_ref.actor.megatron.dynamic_context_parallel=True 后,长尾序列分布下的 SFT/RL 吞吐显著提升(Qwen3-30B-A3B 验证 +60.6%)、loss 几乎不变(相对差异 0.0229%);但需要同步升级 Megatron-Core 到含 #5154 的版本,且 DCP 暂不支持 FP8/VPP/多模态等组合。对系统:micro-batch 数据契约扩展(local_cp_size_dcp_sample_ids_dcp_padding_mask 等),router replay、MTP、fused forward 三个子系统均需感知动态 CP group,后续维护面扩大;megatron_utils.py 中旧的分批/合并逻辑被删除,依赖旧 API 的代码需迁移。对团队:verl 与 Megatron-Core 的耦合收紧,mcore 上游 API(PackedSeqParams 字段、DefaultDynamicCPScheduler 接口)变化会直接影响 DCP 路径,CI 中新增分布式测试也提升了回归保障门槛。

依赖 Megatron-Core 上游版本 核心路径变更 限制 FP8/VPP/ 多模态 NestedTensor 兼容性 R3 对齐假设 输出收集同步开销

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论