执行摘要
- 一句话:Megatron优化器状态与DDP梯度桶对齐模型类型
- 推荐动作:该PR值得精读,尤其是精度感知优化器与DDP梯度桶的联动设计。对于关注显存优化的团队,建议评估后启用,并在各自任务上进行收敛性验证。
功能与动机
在bf16训练中,Megatron分布式优化器保持Adam动量和梯度累积缓冲在fp32,占用了不必要的显存(3倍所需)。本PR使其可选地遵循模型dtype,类似于Megatron CLI的--use-precision-aware-optimizer等选项。最终经过讨论和实验验证,确定为可配置选项,默认保持fp32以避免收敛风险。
实现拆解
- 重构
init_megatron_optim_config(verl/utils/megatron/optimizer.py):添加bf16参数(默认True),将原先的else分支拆分为elif bf16:(bf16模式)和else:(fp32模式)。在elif bf16:内部,当检测到use_precision_aware_optimizer=True时,启用精度感知优化器,将main_grads_dtype、exp_avg_dtype、exp_avg_sq_dtype设置为对应dtype(利用PrecisionType.to_dtype转换);fp32模式则禁用精度感知优化器。fp16分支保持不变。
- 扩展
McoreOptimizerConfig(verl/workers/config/optimizer.py):新增use_precision_aware_optimizer(默认False)、main_grads_dtype(默认"fp32")、exp_avg_dtype(默认"fp32")、exp_avg_sq_dtype(默认"fp32")字段,并添加__post_init__验证dtype字符串合法性。同步更新verl/trainer/config/optim/megatron.yaml和_generated_ppo_megatron_trainer.yaml默认配置。
- 引擎集成(
verl/workers/engine/megatron/transformer_impl.py):新增_resolve_override_ddp_config方法,根据优化器配置判断是否需将grad_reduce_in_fp32设为False以同步DDP梯度桶dtype;用户显式设置时优先。在_build_megatron_module中调用该方法,并在_build_optimizer调用init_megatron_optim_config时传入bf16参数。
- 其他调用点更新:
megatron_workers.py中的调用已随文件删除而移除(冲突解决后仅保留transformer_impl.py的更新)。
- 新增测试:
tests/utils/megatron/test_optimizer.py包含9个CPU单元测试,通过monkeypatchOptimizerConfig为记录器验证各模式下组装的kwargs,覆盖bf16默认fp32、bf16启用精度感知、每字段独立控制、fp16分支、fp32分支、默认参数传递、override优化器配置等场景。
关键文件:
tests/utils/megatron/test_optimizer.py(模块 测试;类别 test;类型 test-coverage;符号 _base_optim_config, _precision_aware_optim_config, captured_args, _fake): 9个CPU单元测试验证各模式下配置组装,确保逻辑正确性
verl/utils/megatron/optimizer.py(模块 优化器;类别 source;类型 dependency-wiring): 核心函数init_megatron_optim_config的重构,增加bf16参数和精度感知分支
verl/workers/engine/megatron/transformer_impl.py(模块 引擎;类别 source;类型 core-logic;符号 _resolve_override_ddp_config): 新增_resolve_override_ddp_config确保DDP梯度桶与精度感知优化器一致,并更新_build_optimizer调用
关键符号:init_megatron_optim_config, McoreOptimizerConfig.post_init, _resolve_override_ddp_config, _base_optim_config, _precision_aware_optim_config, test_bf16_branch_defaults_to_fp32_optimizer_state, test_bf16_branch_opt_in_enables_precision_aware_with_bf16_state, test_bf16_opt_in_respects_per_field_dtypes, test_fp16_branch_uses_precision_aware_but_keeps_fp32_optimizer_state
关键源码片段
tests/utils/megatron/test_optimizer.py
9个CPU单元测试验证各模式下配置组装,确保逻辑正确性
import pytest
import torch
from omegaconf import OmegaConf
from unittest.mock import MagicMock
from verl.utils.megatron import optimizer as opt_mod
from verl.utils.megatron.optimizer import init_megatron_optim_config
def _base_optim_config(**overrides):
# 基础优化配置字典(OmegaConf)
cfg = {
"optimizer": "adam",
"lr": 1e-3,
"min_lr": 0.0,
"clip_grad": 1.0,
"weight_decay": 0.01,
}
cfg.update(overrides)
return OmegaConf.create(cfg)
def _precision_aware_optim_config(**overrides):
# 启用精度感知优化器的配置
fields = {
"use_precision_aware_optimizer": True,
"main_grads_dtype": "bf16",
"exp_avg_dtype": "bf16",
"exp_avg_sq_dtype": "bf16",
}
fields.update(overrides)
return _base_optim_config(**fields)
@pytest.fixture
def captured_args(monkeypatch):
# 替换 OptimizerConfig 为记录器,捕获实际传入的 kwargs
captured: dict = {}
def _fake(**kwargs):
captured.clear()
captured.update(kwargs)
return MagicMock(name="OptimizerConfig", **kwargs)
monkeypatch.setattr(opt_mod, "OptimizerConfig", _fake)
return captured
def test_bf16_branch_defaults_to_fp32_optimizer_state(captured_args):
# 默认 bf16 模式下优化器状态保持 fp32(精度感知未启用)
init_megatron_optim_config(_base_optim_config(), fp16=False, bf16=True)
assert captured_args["bf16"] is True
assert captured_args["params_dtype"] is torch.bfloat16
# 不应出现精度感知相关的键
assert "use_precision_aware_optimizer" not in captured_args
assert "main_grads_dtype" not in captured_args
assert "exp_avg_dtype" not in captured_args
assert "exp_avg_sq_dtype" not in captured_args
def test_bf16_branch_opt_in_enables_precision_aware_with_bf16_state(captured_args):
# 显式启用精度感知优化器后,main_grads_dtype 等应变为 bf16
init_megatron_optim_config(
_precision_aware_optim_config(), fp16=False, bf16=True
)
assert captured_args["use_precision_aware_optimizer"] is True
assert captured_args["main_grads_dtype"] is torch.bfloat16
assert captured_args["exp_avg_dtype"] is torch.bfloat16
assert captured_args["exp_avg_sq_dtype"] is torch.bfloat16
# master_params_dtype 保留 fp32(TE FusedAdam 限制)
assert "main_params_dtype" not in captured_args
verl/utils/megatron/optimizer.py
核心函数init_megatron_optim_config的重构,增加bf16参数和精度感知分支
import torch
from megatron.core.optimizer import OptimizerConfig
from verl.utils.logger import print_rank_0
from verl.utils.torch_dtypes import PrecisionType
def init_megatron_optim_config(
optim_config: dict,
use_distributed_optimizer: bool = True,
fp16: bool = False,
bf16: bool = True, # 默认 True 对应 bf16 模式,但精度感知优化器默认关闭
) -> OptimizerConfig:
# 基础配置项
optim_args = {
"optimizer": optim_config.optimizer,
"lr": optim_config.lr,
"min_lr": optim_config.min_lr,
"clip_grad": optim_config.clip_grad,
"weight_decay": optim_config.weight_decay,
"use_distributed_optimizer": use_distributed_optimizer,
}
if fp16:
# fp16 分支:启用精度感知优化器,但 Adam 动量保持 fp32(默认)
optim_args.update({
"bf16": False,
"fp16": True,
"params_dtype": torch.float16,
"initial_loss_scale": 32768,
"min_loss_scale": 1,
"use_precision_aware_optimizer": True,
"store_param_remainders": False,
})
elif bf16:
# bf16 分支:模型参数为 bf16,但优化器状态默认 fp32(向后兼容)
optim_args.update({
"bf16": True,
"params_dtype": torch.bfloat16,
})
# 精度感知优化器为可选项:仅在配置中设置 use_precision_aware_optimizer=True 后启用
if optim_config.get("use_precision_aware_optimizer", False):
optim_args.update({
"use_precision_aware_optimizer": True,
# 从配置中读取各字段类型,默认 fp32(即不降低精度)
"main_grads_dtype": PrecisionType.to_dtype(
optim_config.get("main_grads_dtype", "fp32")
),
"exp_avg_dtype": PrecisionType.to_dtype(
optim_config.get("exp_avg_dtype", "fp32")
),
"exp_avg_sq_dtype": PrecisionType.to_dtype(
optim_config.get("exp_avg_sq_dtype", "fp32")
),
})
else:
# fp32 分支:禁用精度感知优化器(Megatron 要求关闭时 dtype 必须为 fp32)
optim_args.update({
"bf16": False,
"fp16": False,
"params_dtype": torch.float32,
})
# 允许用户通过 override_optimizer_config 完全覆盖最终的任何配置项
override_config = optim_config.get("override_optimizer_config", {})
if override_config:
for k, v in override_config.items():
optim_args[k] = v
print_rank_0(f"optimizer config after override: {optim_args}")
config = OptimizerConfig(**optim_args)
return config
verl/workers/engine/megatron/transformer_impl.py
新增_resolve_override_ddp_config确保DDP梯度桶与精度感知优化器一致,并更新_build_optimizer调用
def _resolve_override_ddp_config(self):
# 保持 DDP 梯度桶 dtype 与优化器的梯度缓冲 dtype 一致
# 当精度感知优化器启用且 main_grads_dtype 低于 fp32 时,
# 自动设置 grad_reduce_in_fp32=False,除非用户已显式指定
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 getattr(opt_cfg, "use_precision_aware_optimizer", False)
and PrecisionType.to_dtype(getattr(opt_cfg, "main_grads_dtype", "fp32")) != torch.float32
and "grad_reduce_in_fp32" not in override_ddp_config
):
override_ddp_config["grad_reduce_in_fp32"] = False
return override_ddp_config
def _build_megatron_module(self):
# ... 其他代码 ...
override_ddp_config = self._resolve_override_ddp_config() # 新增调用
module, updated_tf_config = make_megatron_module(
...
override_ddp_config=override_ddp_config, # 使用解析后的配置
...
)
# ...
def _build_optimizer(self):
# ...
optim_config_megatron = init_megatron_optim_config(
self.optimizer_config,
use_distributed_optimizer=self.engine_config.use_distributed_optimizer,
fp16=self.param_dtype == torch.float16,
bf16=self.param_dtype == torch.bfloat16, # 新增:传入 bf16 标志
)
# ...
评论区精华
讨论主要围绕优化器精度是否应保持fp32展开:
风险与影响
- 风险:
- 数值收敛风险:启用bf16优化器状态可能导致某些场景下训练不收敛,虽然实验显示无影响,但默认关闭降低了风险,用户需自行验证。
- DDP梯度桶一致性风险:
_resolve_override_ddp_config确保了一致性,但依赖于main_grads_dtype配置。默认main_grads_dtype为fp32时不会修改grad_reduce_in_fp32,安全。
- TransformerEngine限制:TE FusedAdam不支持bf16 master weights,因此
main_params_dtype保留fp32,无法进一步省显存。
- 向后兼容性:默认行为未变,不影响现有用户。
- 测试覆盖:CPU单元测试覆盖配置逻辑,但缺乏端到端集成测试。
-
影响:
- 用户影响:使用Megatron后端的用户可通过配置降低优化器状态显存(约1/3),默认不启用无影响。
- 系统影响:新增配置项和内部逻辑,不改变现有训练流程。
- 团队影响:增加维护复杂性但代码结构化清晰,测试覆盖主要分支。
-
风险标记:默认关闭,需显式启用, 依赖TE FusedAdam, 需要额外验证收敛性, 仅Megatron后端, 缺乏端到端CI测试
关联脉络
参与讨论