已开启
[Bug] hp_fsdp2 + SkipDTensorDispatch 下 AdamW bf16 更新与 torch fsdp2 数值不一致 #201
changzherui创建于 6月9日
6月9日 修改了issue 的描述
6月10日 关联了pull request:fix(fully_shard): align AdamW optimizer step with torch fsdp2 numerics
6月10日 关联了pull request:fix(fully_shard): align AdamW optimizer step with torch fsdp2 numerics
背景
在
veomni上使用 4× Ascend 910B3、bf16、AdamW、seed=42、开启确定性(HCCL_DETERMINISTIC等)的配置,对比:DTensorDTensor发现从 step2 起 loss 逐步偏离(step1 一致),50 step 后 max loss diff 约 1e-3 量级。
现象
optimizer.step()后 param典型 case(
lm_head.weight,step1):grad ≈ -1.56e-08(极小,低于 bf16 ULP)根因分析
1. 分叉点:
optimizer.step(),不是 backward / 梯度归约numeric debug 打点结论:
after_bwd/after_clip/before_optim:grad 完全一致after_optim:param 开始不一致comm_fusion、fused=False等已排除为主要原因。2. 核心机制:
SkipDTensorDispatch+ 原生torch.optim.AdamW训练框架在 hp_fsdp2 下通常这样调用 optimizer:
with SkipDTensorDispatch(): optimizer.step()SkipDTensorDispatch会 关闭 HP DTensor 的 op dispatch,将参数 unwrap 为 local bf16 tensor,直接跑 PyTorch 原生 AdamW。相关代码路径:
hyper_parallel/core/dtensor/dtensor.py:SkipDTensorDispatch→disable_dtensor_dispatch()hyper_parallel/core/shard/_op_dispatch.py:bypass 分支对 in-place op(含addcdiv_)在 local tensor 上直接执行if self._should_bypass_dispatch(op_name): result = op_call(*self._unwrap_args(args), **self._unwrap_kwargs(kwargs)) ...3. 与 torch fsdp2 的差异
DTensorDTensorforeach=True(fused=False时)foreach=False则 further 偏离torch._foreach_addcdiv_on bf16AdamW 最后一步:
在 bf16 上,当数学更新量 小于 bf16 ULP 时:
一步里这样的元素很少,但下一步 forward 使用不同权重,loss 从 step2 开始累积偏离。
4. 中间修复尝试与结论
fused=Falseforeach_addcdiv关键结论:要对齐 fsdp2,不是「算得更准(fp32)」,而是「走同一条 bf16 kernel 路径」。在 state 已与 fsdp2 对齐的前提下,param 写回应使用与 PyTorch AdamW 相同的
torch._foreach_addcdiv_,而不是 fp32 算完再.to(bf16)。建议 hyper-parallel 侧改进
短期(应用层 workaround,已在 veomni 验证)
在
SkipDTensorDispatch内 不要直接调用optimizer.step(),改为:torch/optim/adam.py的_multi_tensor_adam(bf16 state 更新)foreach/fused与 fsdp2 保持一致(fused=False, foreach=True)torch._foreach_addcdiv_on bf16 local shards(与 fsdp2 相同)长期(hyper-parallel 框架层)
optimizer_stephelper(或 document best practice),避免应用层各自 hackaddcdiv_/lerp_/mul_)在 Skip 模式下仍走 与 PyTorch DTensor 等价的 numerics,而非简单 unwrap 到 local复现环境
torch==2.8.0+torch_npu 2.8.0.post2veomni_dev/scripts/zwd/run_fsdp2_vs_hp_fp32_master.sh(及 numeric debug 工具)验证结果(修复后)
bf16
foreach_addcdiv+ state bf16 foreach 对齐后:param/grad/exp_avg/exp_avg_sqmax_abs=0关联
该问题在下游
veomnihp_fsdp2 集成中定位并 workaround;根因位于 hyper-parallel 的 SkipDTensorDispatch + optimizer numerics 路径,建议在框架层正式修复或提供官方方案。补充:若要在 hyper-parallel 框架内对齐,建议修改的代码
方案 A(推荐):新增官方 optimizer step API
目的:把已在 veomni 验证过的 custom AdamW 下沉到 hyper-parallel,应用层不再各自实现。
hyper_parallel/platform/torch/fully_shard/optimizer_step.pyoptimizer_step_adamw(optimizer, *, zero_grad=True):在SkipDTensorDispatch+no_grad内,对 HP DTensor 的 local bf16 shard 执行与torch/optim/adam.py::_multi_tensor_adam等价的 foreach 路径;state 更新(lerp_/addcmul_/mul_)保持 bf16;param 写回使用torch._foreach_addcdiv_hyper_parallel/platform/torch/fully_shard/__init__.pyhyper_parallel/__init__.pyoptimizer_step_adamw(或挂到HSDPModule.optimizer_step())tests/torch/fully_shard/_test_optimizer_numerics.py不必改
_op_dispatch.py的 bypass 逻辑;仍在 Skip 下对 local tensor 执行,但由框架保证 op 序列与 PyTorch AdamW 一致。方案 B:增强
SkipDTensorDispatch/ bypass 数值语义目的:继续允许
optimizer.step(),但 bypass 路径与 PyTorch DTensor numerics 一致。hyper_parallel/core/dtensor/dtensor.pySkipDTensorDispatch:如SkipDTensorDispatch(optimizer_safe=True)或在文档中明确 optimizer 专用模式hyper_parallel/core/shard/_op_dispatch.py_should_bypass_dispatch/ bypass 分支(约 L1028–L1251):对 AdamW 涉及的 in-place op(addcdiv_/lerp_/mul_/addcmul_及 NPU 映射名如InplaceAddExt等)不要简单unwrap → local 裸调;改为调用与 PyTorch ATen 等价的 numerics wrapper(bf16 上 foreach 同 kernel)hyper_parallel/core/shard/_op_dispatch.py_INPLACE_BYPASS_OPS(L244–245):若新增 optimizer numerics wrapper,确保 in-place 后仍返回原 DTensor selfhyper_parallel/platform/torch/dtensor.py__torch_function__(L52–78):可选提供不 Skip 的 AdamW op dispatch(长期目标:应用层无需 Skip)方案 C:让
optimizer.step()无需 Skip(长期)目的:HP DTensor 完整走 dispatch,与 PyTorch DTensor 行为一致。
hyper_parallel/core/shard/_op_dispatch.py+core/shard/ops/addcdiv_/lerp_/_foreach_addcdiv_等注册 layout-infer + local compute 的 distributed op(参考torch/distributed/tensor/_ops/_pointwise_ops.py对aten.addcdiv_.default的注册)hyper_parallel/platform/torch/fully_shard/param_group.py_local_tensor/.data同步逻辑与 numerics 路径一致hyper_parallel/platform/torch/fully_shard/param.pyexamples/torch/llama3/README.mdoptimizer.step()测试 / 文档(任意方案均需)
tests/torch/fully_shard/_test_fsdp2_optimizer_vs_torch.py.agent/skills/code-review/distributed-guidelines.md或用户文档optimizer.step()而不保证 AdamW numerics;推荐调用官方 helper不建议的改法
.to(bf16):舍入点与 PyTorchforeach_addcdiv不同,无法 bit-exact。foreach=False:与 torch fsdp2(foreach=True)kernel 路径不一致。