mindspore-ai / mindspore-ai/hyper-parallel

[Bug]: MindSpore FSDP shard size=1 时无效 backward prefetch 导致训练轨迹分叉

Open
#166 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
53
Forks
63
Avg merge
23h 45m
Merged PRs (30d)
63

Description

Checklist

  • 已检索现有 Hyper-Parallel issues。
  • 已阅读 fully_shard/prefetch 相关实现。
  • 已构造可稳定比较的 A/A 复现场景,并完成逐层、逐 rank 梯度快照定位。

问题描述

在 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

环境与版本

  • Hyper-Parallel master 基线:eccce0bc227d007c948ae04076d91622002a13c8
  • MindFormers master 基线:dbbf170529fd6464bda46e314caad686182972a7
  • 8 卡单机
  • deterministic: true
  • AdamW
  • 模型包含 MoE shared expert

最小复现拓扑

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

实际并行拓扑为:

data_parallel=1 (dp_shard=1 × dp_replicate=1)
tensor_parallel=2
context_parallel=4

因此 TransformerLayer 虽为 HSDPModule,但参数 shard_size=1,不存在真实 FSDP AllGather。

完整复现配置:

/home/l00913161/lzy/tp2_cp4_determinism_20260903/configs/mla_tp2_cp4_adamw_same_model_100.yaml

使用同一配置、同一 seed 连续运行两次,各训练 100 step,然后逐 step 比较 loss 和 grad_norm。

原始实现的 A/A 结果

MLA、TP2+CP4、AdamW、无重计算,100-step A/A:

指标 首个差异 step MAE P95 最大绝对误差
loss 4 3.6890e-5 1.0100e-4 2.6100e-4
grad_norm 3 3.0893e-4 8.1200e-4 1.4444e-2

拓扑隔离结果:

并行拓扑 loss A/A grad_norm A/A
TP1+CP1 完全一致 完全一致
TP2+CP1 完全一致 完全一致
TP1+CP4 完全一致 完全一致
TP2+CP4 出现漂移 出现漂移

首次分叉证据

首步在 global grad norm 计算前采集 8 rank × 29 参数,共 232 个更新前梯度 SHA,两次运行全部相同,说明 backward 数学计算和 TP reduce 在首步没有产生不同值。

随后使用无 Host 同步快照:每 step 只在 NPU clone 4 个 shared FC 梯度,训练结束后统一计算 SHA。两次运行各得到 3200 条记录:

  • step 1–45:8 rank、4 参数全部一致;
  • step 46:仅 rank 2/3 的 decoder.layers.0.mlp.shared_experts.linear_fc1.weight 首次不同;
  • step 46 的打印 loss/grad_norm 仍一致;
  • step 47:差异扩散到所有 rank 的 shared FC 梯度和 loss/grad_norm;
  • 100 step 共 1730 条 shared 梯度快照不同,54 个训练 step 的轨迹不同。

Prefetch 边界实验

设置 100-step A/A 结果
全部 prefetch 开启 漂移
全部关闭 完全一致
仅关闭 backward prefetch 完全一致
仅关闭 forward prefetch 仍漂移,grad step 26 起
仅保留 tail→last-layer backward prefetch 完全一致
只关闭 layer1→layer0 backward prefetch 完全一致

该结果将首个异常边界定位到后一层 backward pre-hook 对前一层发起的无效 prefetch。

调用链与根因

MindFormers 为相邻 TransformerLayer 建立 backward prefetch 链:

layer1 backward_pre_hook
  -> prefetch_state(layer0).prefetch()
  -> HSDPState.prefetch()
  -> unshard(async_op=True)

基础实现无条件调用:

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

期望行为

  • 当一个 HSDP state 的全部参数均为 shard_size=1 时,跳过 prefetch,不提前执行本地 unshard/buffer copy。
  • 任一参数 shard_size>1 时,保留原有异步 prefetch,使真实 FSDP AllGather 能继续与计算重叠。
  • 不改变前向、反向、梯度归约和 optimizer 数学语义。

修复方案

在 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。

修复验证

  • TP2+CP4、DP/FSDP1:300-step A/A,loss/grad_norm 逐 step 完全一致。
  • MindSpore-backend 最终实现:TP2+CP4 100-step A/A,loss/grad_norm 完全一致。
  • TP2+CP1、DP/FSDP4:100-step A/A 完全一致,真实参数 AllGather prefetch 保持启用。
  • DSA2、TP2+CP4、full recompute,叠加 MindFormers 完整层 replay 修复:300-step loss/indexer_loss/grad_norm A/A 完全一致。
  • 新增单测 2/2 通过:size-one 不调用 async unshard,shard-size=4 仍调用。
  • git diff --checkpy_compile 通过。

完整分析报告及原始日志索引:

/home/l00913161/lzy/tp2_cp4_determinism_20260903/TP2_CP4_DETERMINISM_ANALYSIS.md

schema_version: 1
source: gitcode
gitcode_repo: mindspore/hyper-parallel
gitcode_issue: 363
source_url: https://gitcode.com/mindspore/hyper-parallel/issues/363

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review linked PR #1326 and the MindSpore backend's MindSporeHSDPStateV2.prefetch() implementation. Check how _init_group_infos() supplies shard_size and inspect the two reported unit tests for size-one and shard-size=4 behavior. Done means size-one states skip async unshard while real FSDP AllGather prefetch remains enabled.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.