Prhub

#7485 [megatron] fix: align DDP gradient dtype with optimizer precision

原始 PR 作者 Mecoli1219 合并时间 2026-08-21 10:38 文件变更 2 提交数 1 评论 0 代码增减 +88 / -12

执行摘要

DDP 梯度精度对齐优化器,消除 BF16 训练内存浪费

PR body 明确指出:The conventional optimizer expects FP32 gradients, but the Bridge path omitted grad_reduce_in_fp32 and inherited Megatron-Core's False default. For BF16 models, this kept a full BF16 gradient buffer (large blue block) and allocated FP32 gradient shards (the right peak in the profiling result) during the optimizer step。即 Megatron-Bridge 的 DDP 梯度桶 dtype 与优化器的有效主梯度精度不匹配,BF16 模型训练时显存出现双重占用峰值。本 PR 遵循 #6526 中达成的 FP32-by-default 约定修复该问题。

值得精读。这是「默认值继承陷阱」的典型案例:一个被省略的配置项让框架默认值悄悄改变了训练内存布局。最小 diff(+10/-12)修复真实缺陷,设计上采用 FP32-by-default + 显式 override 优先的合成策略,可复用到其他后端配置推导(如 FSDP 的 reduce_dtype)。测试中用 object.__new__ 绕过重型初始化的技巧,适合在依赖复杂的引擎类上做轻量单测时借鉴。

讨论亮点

本 PR 没有公开的 review 评论,合并者 wuxibin89 直接 APPROVED。核心设计决策沉淀在 PR body 中:

This follows the FP32-by-default behavior agreed on in #6526.

最大的权衡点是「默认 FP32 归约」与「precision-aware 优化器低精度归约」之间的选择:常规优化器内部期望 FP32 主梯度,因此 DDP 直接以 FP32 归约可以避免梯度分片重复分配;而 precision-aware 优化器(如 BF16 主梯度)则主动选择低精度归约以省内存。显式 override_ddp_config 永远优先,保证用户可覆盖自动推导结果。

实现拆解

  1. 变更入口:仅改动 verl/workers/engine/megatron/transformer_impl.pyMegatronEngine._resolve_override_ddp_config() 一个方法。该方法在 _build_megatron_module() 中调用,返回值用于构造 McoreModuleWrapperConfig 并传给 make_megatron_module(),是 Megatron-Bridge 路径 DDP 封装的唯一配置出口。
  2. 默认策略反转:旧实现只在「precision-aware 优化器 + sub-FP32 main_grads_dtype」两个条件同时成立时注入 grad_reduce_in_fp32=False,其他场景(尤其是 Bridge + 常规优化器)不注入任何值,从而继承 Megatron-Core 的 False 默认,造成 BF16 模型同时保留 BF16 梯度桶并在优化器 step 中额外分配 FP32 梯度分片。新实现改为:只要 optimizer_config 存在且用户未显式设置 grad_reduce_in_fp32,就无条件写入该键——常规优化器默认 True,仅 precision-aware + sub-FP32 主梯度时为 False。
  3. 显式配置优先级:分支条件 "grad_reduce_in_fp32" not in override_ddp_config 保证用户显式覆盖永远生效;Muon + LayerWise 的 use_layer_wise_param_layout 分支继续用 setdefault,不覆盖已有配置。
  4. 测试配套:新增 tests/workers/test_megatron_ddp_config_on_cpu.py(+78 行),通过 object.__new__(MegatronEngine) 绕过 __init__,用 _Config(SimpleNamespace) 模拟 engine_config / optimizer_config,直接对私有方法做纯 CPU 单测,覆盖 4 组参数化用例(常规/精度感知 × FP32/BF16)、显式 override 双向取值、无优化器引擎返回空 dict 三类场景。
文件 模块 状态 重要度
verl/workers/engine/megatron/transformer_impl.py 引擎实现 modified 6.38
tests/workers/test_megatron_ddp_config_on_cpu.py 配置测试 added 7.01

关键符号

_resolve_override_ddp_config test_ddp_grad_dtype_follows_effective_main_grad_dtype test_explicit_ddp_grad_dtype_override_wins test_optimizerless_engine_does_not_inject_grad_dtype

关键源码片段

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

唯一的生产代码改动。`_resolve_override_ddp_config()` 从「仅 precision-aware + sub-FP32 时注入 False」改为「默认注入 FP32、仅在 precision-aware + sub-FP32 时注入 False」,是修复内存浪费的核心。

def _resolve_override_ddp_config(self):
    """让 DDP 梯度桶 dtype 与优化器梯度 buffer 保持一致。    常规优化器默认使用 FP32 梯度归约;只有 precision-aware 优化器
    显式选择 sub-FP32 的 ``main_grads_dtype`` 时才改用低精度归约。
    用户通过 ``override_ddp_config`` 显式指定的值优先级最高。    对 Muon + LayerWise 场景,还会开启 ``use_layer_wise_param_layout``,
    让 master weight 直接驻留在 param buffer 中,避免额外的 FP32 拷贝。
    """
    from verl.utils.megatron.optimizer import is_muon_layer_wise_config
    from verl.utils.torch_dtypes import PrecisionType
​
    override_ddp_config = dict(self.engine_config.override_ddp_config or {})
    opt_cfg = self.optimizer_config
    if opt_cfg is not None and 'grad_reduce_in_fp32' not in override_ddp_config:
        # 仅当 precision-aware 优化器要 sub-FP32 主梯度时,梯度归约才跟随
        # 低精度;其余情况一律默认 FP32,避免 BF16 梯度桶与 FP32 梯度分片
        # 同时存在导致的内存翻倍。显式 override 永远优先。
        use_low_precision_main_grads = (
            getattr(opt_cfg, 'use_precision_aware_optimizer', False)
            and PrecisionType.to_dtype(getattr(opt_cfg, 'main_grads_dtype', 'fp32')) != torch.float32
        )
        override_ddp_config['grad_reduce_in_fp32'] = not use_low_precision_main_grads
    if opt_cfg is not None and is_muon_layer_wise_config(opt_cfg):
        override_ddp_config.setdefault('use_layer_wise_param_layout', True)
    return override_ddp_config
tests/workers/test_megatron_ddp_config_on_cpu.py test-coverage

新增 78 行 CPU 单测,覆盖 FP32 默认、BF16 opt-in、显式 override 优先、无优化器四类场景,用 `object.__new__` 绕过 `MegatronEngine.__init__`,是保证回归安全的配套测试。

class _Config(SimpleNamespace):
    """用 SimpleNamespace 模拟引擎配置,get() 兼容 dataclass 的默认值查询。"""
​
    def get(self, key, default=None):
        return getattr(self, key, default)
​
​
def _resolve_ddp_config(*, use_precision_aware_optimizer=False, main_grads_dtype='fp32',
                        override_ddp_config=None, with_optimizer=True):
    # 通过 object.__new__ 绕过 MegatronEngine.__init__,只构造待测方法依赖的
    # 两个配置属性,让单测可以在纯 CPU 环境直接调用私有方法。
    engine = object.__new__(MegatronEngine)
    engine.engine_config = _Config(override_ddp_config=override_ddp_config or {})
    engine.optimizer_config = (
        _Config(optimizer='adam', use_precision_aware_optimizer=use_precision_aware_optimizer,
                main_grads_dtype=main_grads_dtype)
        if with_optimizer
        else None
    )
    return engine._resolve_override_ddp_config()
​
​
@pytest.mark.parametrize(
    ('use_precision_aware_optimizer', 'main_grads_dtype', 'expected'),
    [
        (False, 'fp32', True), # 常规优化器 + FP32 主梯度:FP32 归约
        (False, 'bf16', True), # 常规优化器 + BF16 主梯度:仍走 FP32 归约(优化器内部期望 FP32)
        (True, 'fp32', True), # precision-aware 但主梯度仍为 FP32:FP32 归约
        (True, 'bf16', False), # precision-aware + BF16 主梯度:低精度归约,省内存
    ],
)
def test_ddp_grad_dtype_follows_effective_main_grad_dtype(use_precision_aware_optimizer, main_grads_dtype, expected):
    resolved = _resolve_ddp_config(
        use_precision_aware_optimizer=use_precision_aware_optimizer,
        main_grads_dtype=main_grads_dtype,
    )
    assert resolved['grad_reduce_in_fp32'] is expected

评论区精华

FP32-by-default 设计决策与 override 优先级 设计

本 PR 无公开 review 评论,仅 wuxibin89 APPROVED。设计依据在 PR body 中:'This follows the FP32-by-default behavior agreed on in #6526.' 作者对比了修复前后的 profiler 截图:旧行为在 BF16 模型下同时保留 BF16 梯度桶(大蓝块)并在优化器 step 中分配 FP32 梯度分片(右峰值)。

结论:采用 FP32-by-default:常规优化器默认 `grad_reduce_in_fp32=True`,仅 precision-aware 优化器请求 sub-FP32 主梯度时降为低精度;显式 `override_ddp_config` 始终优先。 · 已解决

风险与影响

  • 梯度通信量翻倍:常规优化器 + BF16 模型下,grad_reduce_in_fp32 从默认 False 变为 True,梯度 all-reduce 的通信字节数约为 BF16 的两倍。对网络带宽受限的大规模训练可能带来吞吐下降,PR 未提供通信开销的 benchmark 数据。
  • 静默行为变更:所有使用 Megatron-Bridge + 常规优化器且未显式设置 grad_reduce_in_fp32 的用户都会受到影响,升级后建议关注训练吞吐与显存 profile。
  • 测试覆盖局限:只有 CPU 单测,无 GPU 端到端验证;内存收益证据来自作者提供的 profiler 截图,未沉淀为自动化基准。
  • 精度语义变化:precision-aware + BF16 场景行为保持不变;但曾依赖「BF16 梯度桶 + 优化器内 FP32 分片」隐式布局的用户,升级后该布局会消失。
  • 用户视角:对 Megatron-Bridge + 常规优化器 + BF16 训练的用户是直接受益者——消除优化器 step 中的 FP32 梯度分片峰值显存,代价是梯度通信量增加。
  • 系统视角_resolve_override_ddp_config() 的输出契约由「条件注入」变为「默认注入 + 显式优先」,调用链 _build_megatron_modulemake_megatron_module 的 DDP 配置从此完全确定,不再依赖 Megatron-Core 内部默认值,降低该路径默认行为的不确定性。
  • 团队视角:与 #6526 的 FP32-by-default 约定对齐,为 precision-aware optimizer 后续推广(Muon、低精度主梯度等)提供一致的精度推导入口,并沉淀了可复用的 CPU 单测基线。
  • 影响程度:中等偏低——只影响 Megatron 后端,FSDP / vLLM / SGLang 等其他路径不受影响,逻辑集中在单一方法内。
梯度通信量翻倍 配置默认值反转 缺少 GPU 端到端测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论