Appearance & Root Cause
问题现象:MindSpore backend 在 TP2 + CP4、DP/FSDP shard size=1 场景下,使用相同 seed 和相同配置重复训练时,loss 和 grad_norm 会概率性分叉。原始 100-step A/A 中 loss 首次在 step 4 出现差异,最大绝对误差 2.6100e-4;grad_norm 首次在 step 3 出现差异,最大绝对误差 1.4444e-2。
触发条件:MindSpore backend,模块已经 fully_shard/HSDP 包装,但参数 shard group size=1,同时存在跨 TransformerLayer 的 backward prefetch。已通过 prefetch 边界实验定位到 layer1 backward pre-hook 对 layer0 发起的无通信 unshard prefetch;仅关闭 backward prefetch 或仅关闭该边界可恢复 A/A 一致。
根因:原实现用 prefetch_handle is not None 同时表示“存在真实异步 AllGather”和“存在待消费的 unshard”。shard size=1 时没有 collective,异步 prefetch 路径仍会进入本地 unshard,执行 reset_sharded_param()、buffer 准备/拷贝并返回 handle=None。目标层真正进入 backward 时因没有 pending handle 而再次 unshard,导致参数 reset/rebind、DTensor 本地视图和 storage 生命周期与前一层 backward 交叠。底层回调和算子的微小执行时序差异使两次运行出现梯度分叉,随优化器更新扩散到 loss 和 grad_norm。
Fix Solution
修复方案:在 MindSpore 参数层引入 _NoOpAllGatherHandle,将“待消费的 unshard”与“真实通信 handle”分离表达。对 shard size=1 等无 collective 场景,prefetch 阶段只记录逻辑 pending handle,不提前 reset、分配 buffer 或 copy;目标层消费参数时,由 wait_for_unshard() 调用 NoOp handle 的 callback,使用最新参数完成本地 materialization,然后统一执行 init_unsharded_param() 和 to_unsharded()。真实 FSDP AllGather 仍使用 CommHandle,原有异步通信重叠保持不变;同步 collective 即使底层返回 None,也用空 NoOp handle 保留待完成的状态转换。
master 分支的 PR #1326 通过 state 层跳过 size-one prefetch 规避了同一问题;r1.0.0 分支的 PR #1330 采用上述参数生命周期方案完成修复。
Fix Description & Test Suggestion
修复 PR:https://gitcode.com/mindspore/hyper-parallel/merge_requests/1330
PR 状态:已于 2026-09-16 合入 r1.0.0,最终提交为 98a601f69016c2c2c95405cb54dda1133a03fad4,门禁标签为 pr-check-pass 和 ci-pipeline-passed。master 上的对应修复 PR #1326 已于 2026-09-04 合入。
建议回归版本:r1.0.0 分支中包含 PR #1330/提交 98a601f6 的构建;具体 daily 版本号待测试里程碑确认。如回归 master,使用包含 PR #1326 的构建。
测试建议:
- 使用 issue 中的 8 卡 MLA、TP2 + CP4、DP/FSDP shard size=1、AdamW、
deterministic=true配置,在最终 #1330 代码上连续执行两次 200 或 300 step,逐 step 比较 loss 和 grad_norm,期望different_step_count=0且max_absolute_error=0。 - 确认运行时存在 size-one state 和 layer1→layer0 backward prefetch 边,避免未覆盖问题路径。
- 使用 TP2 + CP1、FSDP4 作为对照,确认真实 AllGather prefetch 仍启用且精度一致。
Self-test Report & DT Review
自验环境:MindSpore backend,Ascend 910B;最终修复分支为 r1.0.0 + PR #1330。端到端 A/A 历史实验使用 8 卡 TP2 + CP4、显式 FSDP shard size=1 拓扑。
自验项与结果:自验通过。最终 _NoOpAllGatherHandle 提交的 MindSpore fully_shard UT 结果为 232 passed, 12 skipped,compileall 和 git diff --check 通过,PR 门禁通过。定向 UT 覆盖 size-one async prefetch 不提前 materialize、wait 阶段才 reset/copy/安装参数、NoOp handle 重复 wait 不重复执行、真实 CommHandle 流程不变、同步通信返回 None 时仍保留 pending 状态。此前参数层 reset-guard 候选版本的 8 卡 200-step A/A 结果为 loss/grad_norm 全部一致;该结果不冒充为最终 NoOp handle 提交的端到端结果,回归阶段仍需按上述建议重跑。
是否需要补充 ST/UT:是。UT 已随 PR #1330 补充;由于原问题为概率性长训练轨迹分叉,建议测试阶段在最终合入代码上补做 8 卡 200/300-step A/A ST。
用例路径和用例名:tests/ut/platform/mindspore/fully_shard/test_param.py::TestMindSporeParam::test_no_comm_async_unshard_defers_materialization_until_wait;tests/ut/platform/mindspore/fully_shard/test_mixed_precision.py::TestPrefetchStateMachine::test_sync_collective_without_backend_handle_records_pending_state;tests/ut/platform/mindspore/fully_shard/test_mixed_precision.py::TestPrefetchStateMachine::test_wait_for_unshard_without_pending_work_is_noop。
Introduction Analysis
引入类型:Feature Merge
引入 PR:https://gitcode.com/mindspore/hyper-parallel/merge_requests/240
PR 合入时间:2026-03-14 09:35:34 +08:00
问题是否偶现:是
引入原因:PR #240 首次引入 MindSpore fully_shard backend 时,将底层 AllGather 返回的通信 handle 直接作为 unshard pending 状态。该设计在有真实 AllGather 时成立,但没有覆盖 shard size=1 时“无通信但仍存在待消费 unshard”的状态。原 DT 主要验证单卡 baseline 与多卡 fully_shard 的 loss 容差和 local shape,未覆盖 TP + CP 组合下 DP/FSDP=1、跨层 backward prefetch 和多次长 step A/A 可复现性,因此未在特性合入时识别该交互问题。


Checklist
问题描述
在 MindSpore backend 下,模型模块虽然经过
fully_shard包装,但实际 FSDP shard group size 为 1 时,跨 TransformerLayer 的 backward prefetch 仍调用unshard(async_op=True)。该场景不存在参数 AllGather,也没有可重叠的异步通信。MindSpore 参数本地 unshard 分支只进行本地 buffer 建立/复制并返回
allgather_handle=None;目标层真正进入 backward 时又执行一次 unshard,造成参数 buffer 生命周期及 replay/backward 调度交叠。相同 seed、相同配置重复运行会出现训练轨迹概率性分叉。问题不是 DSA/Indexer 独有:关闭 DSA、只运行 MLA 时同样可以复现。
关联修复 PR:#1326
环境与版本
eccce0bc227d007c948ae04076d91622002a13c8dbbf170529fd6464bda46e314caad686182972a7deterministic: true最小复现拓扑
parallelism: data_parallel_shard: -1 tensor_parallel: 2 context_parallel: 4 pipeline_parallel: 1 training: deterministic: true model: experimental_attention_variant: mla n_shared_experts: 1实际并行拓扑为:
因此 TransformerLayer 虽为 HSDPModule,但参数
shard_size=1,不存在真实 FSDP AllGather。完整复现配置:
使用同一配置、同一 seed 连续运行两次,各训练 100 step,然后逐 step 比较 loss 和 grad_norm。
原始实现的 A/A 结果
MLA、TP2+CP4、AdamW、无重计算,100-step A/A:
拓扑隔离结果:
首次分叉证据
首步在 global grad norm 计算前采集 8 rank × 29 参数,共 232 个更新前梯度 SHA,两次运行全部相同,说明 backward 数学计算和 TP reduce 在首步没有产生不同值。
随后使用无 Host 同步快照:每 step 只在 NPU clone 4 个 shared FC 梯度,训练结束后统一计算 SHA。两次运行各得到 3200 条记录:
decoder.layers.0.mlp.shared_experts.linear_fc1.weight首次不同;Prefetch 边界实验
该结果将首个异常边界定位到后一层 backward pre-hook 对前一层发起的无效 prefetch。
调用链与根因
MindFormers 为相邻 TransformerLayer 建立 backward prefetch 链:
基础实现无条件调用:
def prefetch(self) -> None: self.unshard(async_op=True)但 MindSpore 参数 unshard 在
shard_world_size <= 1时不会发起 collective,只准备或复制本地 buffer,并将 async handle 保持为None。因此这次 prefetch 没有通信收益,目标层实际进入 backward 时还会再次走同步 unshard。这里必须以
_init_group_infos()解析出的shard_size判断真实 FSDP communication group 大小,不能使用可能包含原 DTensor/TP 分片语义的shard_world_size。期望行为
shard_size=1时,跳过 prefetch,不提前执行本地 unshard/buffer copy。shard_size>1时,保留原有异步 prefetch,使真实 FSDP AllGather 能继续与计算重叠。修复方案
在 MindSpore backend 的
MindSporeHSDPStateV2.prefetch()中限定:def prefetch(self) -> None: if not any(getattr(param, "shard_size", 1) > 1 for param in self.hsdp_params): return super().prefetch()修改放在 MindSpore backend,不改变共享 HSDP 核心逻辑,也不影响 PyTorch backend。
修复验证
git diff --check、py_compile通过。完整分析报告及原始日志索引: