Prhub

#6150 [fsdp] fix: honor mixed_precision.param_dtype in forward_step autocast (#5932)

原始 PR 作者 shivam2199 合并时间 2026-04-29 20:58 文件变更 2 提交数 5 评论 10 代码增减 +126 / -7

执行摘要

修复 FSDP 引擎 forward_step 硬编码 bf16 问题

Issue #5932 报告 FSDP 引擎 forward_step 中 autocast 强制使用 bf16,即使 mixed_precision 配置了 fp32 或 fp16 也被忽略,导致前向精度与配置不一致。此外,fp16 路径原本未实现且静默失败,需要集成梯度缩放器以支持。

建议 FSDP 引擎使用者和开发者阅读此 PR,重点学习 ShardedGradScaler 集成模式和子类兼容性设计(getattr 回退与默认值初始化)。fp16 用户应先在小规模任务上验证收敛稳定性。

讨论亮点
  1. reduce_dtype 是否需要检查 fp16? 代码审查工具建议当 reduce_dtype 为 fp16 时也应报错,但作者解释 reduce_dtype 仅控制梯度规约精度,不影响梯度计算,无需 scaler。结论未被质疑。
  2. fp16 路径的 GRPO 收敛验证:维护者 wuxibin89 要求通过 GRPO 实验对比 fp16 与 bf16 的收敛性。作者使用 Qwen2-7B-Instruct 在 GSM8K 上运行 20 步,结果显示步 10 时精度接近(0.864 vs 0.867),但步 20 时 fp16 略低(0.718 vs 0.880),可能因缩放误差累积。尽管存在退化,维护者仍接受了合并。

实现拆解

  1. 默认状态初始化:在 FSDPEngine.init 中设置 self._autocast_dtype = torch.bfloat16 和 self.scaler = None,确保子类(如 VeOmniEngine)即便跳过 _build_fsdp_module 也能安全访问这些属性。
  2. 解析配置并存储:在 _build_fsdp_module 中从 mixed_precision.param_dtype 提取 dtype,存入 self._autocast_dtype;若为 fp16 则创建 ShardedGradScaler,否则设 None。
  3. 修改 forward_step:使用 self._autocast_dtype 控制 autocast context,当 dtype 为 fp32 时使用 nullcontext 避免额外开销。
  4. 修改 execute_micro_batches:根据 self.scaler 决定是否使用 scaler.scale(loss).backward()。
  5. 修改 optimizer_step:在梯度裁剪前调用 scaler.unscale_,裁剪后使用 scaler.step 代替 optimizer.step,并跳过 inf/nan 梯度。
  6. 回归测试:新增 test_fsdp2_autocast_dtype_honors_mixed_precision,在 8× A100 上验证 bf16/fp32/fp16 三种配置下 _autocast_dtype 和 scaler 的正确性。
文件 模块 状态 重要度
verl/workers/engine/fsdp/transformer_impl.py FSDP 引擎 modified 7.17
tests/models/test_engine.py 测试 modified 6.4

关键符号

forward_step _build_fsdp_module forward_backward_batch optimizer_step _autocast_dtype_worker test_fsdp2_autocast_dtype_honors_mixed_precision

关键源码片段

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

核心修复文件:修改 forward_step autocast dtype 并集成 ShardedGradScaler,同时调整 optimizer_step 和 execute_micro_batches 中的缩放处理。

    def _build_fsdp_module(self, module):
        # 从 mixed_precision_config 解析出 param_dtype, reduce_dtype, buffer_dtype
        mixed_precision_config = self.engine_config.mixed_precision
        if mixed_precision_config is not None:
            param_dtype = PrecisionType.to_dtype(mixed_precision_config.get("param_dtype", "bf16"))
            reduce_dtype = PrecisionType.to_dtype(mixed_precision_config.get("reduce_dtype", "fp32"))
            buffer_dtype = PrecisionType.to_dtype(mixed_precision_config.get("buffer_dtype", "fp32"))
        else:
            param_dtype = torch.bfloat16
            reduce_dtype = torch.float32
            buffer_dtype = torch.float32
​
        mixed_precision = MixedPrecision(param_dtype=param_dtype, reduce_dtype=reduce_dtype, buffer_dtype=buffer_dtype)
​
        # 存储解析出的 param_dtype,供 forward_step 中的 autocast 使用
        self._autocast_dtype = param_dtype
​
        # fp16 需要 ShardedGradScaler 进行梯度缩放以防止下溢
        # 参考 dp_actor 模式 (#4036),bf16 / fp32 不需要
        if param_dtype == torch.float16:
            from torch.distributed.fsdp.sharded_grad_scaler import ShardedGradScaler
            self.scaler = ShardedGradScaler(growth_interval=400)
        else:
            self.scaler = None
​
        # 继续创建 auto_wrap_policy、FSDP module ...
tests/models/test_engine.py test-coverage

新增回归测试,验证 bf16/fp32/fp16 三种混合精度配置下 autocast dtype 和 scaler 创建的正确性。

# 回归测试 #5932:FSDP 引擎必须从 mixed_precision.param_dtype 解析 autocast dtype
# 而不是硬编码 bfloat16。
def _autocast_dtype_worker(rank: int, world_size: int, rendezvous_file: str, model_path: str):
    torch.cuda.set_device(rank)
    dist.init_process_group(
        backend="nccl",
        init_method=f"file://{rendezvous_file}",
        rank=rank,
        world_size=world_size,
    )
​
    from verl.workers.engine import BaseEngine, EngineRegistry
​
    model_config = HFModelConfig(
        path=model_path,
        load_tokenizer=False,
        override_config={"attn_implementation": "sdpa"},
    )
​
    def build_engine(mixed_precision):
        engine_config = FSDPEngineConfig(
            forward_only=False,
            fsdp_size=world_size,
            strategy="fsdp2",
            ulysses_sequence_parallel_size=1,
            mixed_precision=mixed_precision,
        )
        engine: BaseEngine = EngineRegistry.new(
            model_type="language_model",
            backend=engine_config.strategy,
            model_config=model_config,
            engine_config=engine_config,
            optimizer_config=FSDPOptimizerConfig(),
            checkpoint_config=CheckpointConfig(),
        )
        engine.initialize()
        return engine
​
    from torch.distributed.fsdp.sharded_grad_scaler import ShardedGradScaler
​
    # bf16 ( 默认 ) 应解析为 torch.bfloat16,不需要 scaler
    engine = build_engine({"param_dtype": "bf16", "reduce_dtype": "fp32", "buffer_dtype": "fp32"})
    assert engine._autocast_dtype == torch.bfloat16, f"expected bf16, got {engine._autocast_dtype}"
    assert engine.scaler is None, "bf16 should not create a scaler"
​
    # fp32 应解析为 torch.float32,forward_step 使用 nullcontext,不需要 scaler
    engine = build_engine({"param_dtype": "fp32", "reduce_dtype": "fp32", "buffer_dtype": "fp32"})
    assert engine._autocast_dtype == torch.float32, f"expected fp32, got {engine._autocast_dtype}"
    assert engine.scaler is None, "fp32 should not create a scaler"
​
    # fp16 应创建 ShardedGradScaler 进行损失缩放
    engine = build_engine({"param_dtype": "fp16", "reduce_dtype": "fp32", "buffer_dtype": "fp32"})
    assert engine._autocast_dtype == torch.float16, f"expected fp16, got {engine._autocast_dtype}"
    assert isinstance(engine.scaler, ShardedGradScaler), "fp16 must create a ShardedGradScaler"
​
    dist.barrier()
    dist.destroy_process_group()

评论区精华

是否需要检查 reduce_dtype 为 fp16 正确性

自动审查建议当 reduce_dtype 为 fp16 时也应报错,因为梯度规约在半精度可能有问题。作者解释 reduce_dtype 只控制规约精度,不影响梯度计算,所以不需要 scaler。维护者未进一步坚持。

结论:作者的解释被接受,未添加 reduce_dtype 检查。 · 已解决

fp16 路径需进行 GRPO 收敛验证 测试

维护者 wuxibin89 要求作者进行 GRPO 实验来验证 fp16 与 bf16 的收敛性。作者使用 Qwen2-7B-Instruct 在 GSM8K 上运行 20 步,提供了对比表格。结果显示步 10 时 fp16 接近 bf16,但步 20 时 fp16 出现退化。

结论:尽管 fp16 在步 20 精度较低,维护者仍接受了合并,认为在可控范围内。 · 已解决

风险与影响

fp16 收敛曲线在步 20 出现明显下降,说明存在不稳定风险;该实现仅在 FSDP2 的回归测试中验证,传统 FSDP(fsdp 策略)未覆盖;VeOmniEngine 通过 getattr 回退方式确保兼容,但其他未来子类若未模仿此模式可能引入 AttributeError。

影响所有配置 mixed_precision 的 FSDP 用户:修复了 fp32 被静默覆盖为 bf16 的 bug,新增 fp16 支持需明确配置;已有 bf16 配置无行为变化;回归测试确保后续改动不会破坏三种精度路径。

fp16 收敛不稳定性风险 子类兼容性隐患 测试范围有限(仅 FSDP2)

关联 Issue

#5932 [BUG] FSDP engine forward_step hardcodes bf16 autocast and ignores configured mixed-precision dtype

完整报告

参与讨论