mindspore-ai / mindspore-ai/hyper-parallel

CC分析fully_shard/DTensor相关编码问题

Open
#332 3 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

HyperParallel 框架深度分析报告

分析范围

fully_shard() 强相关的 DTensor 核心(Layout / Placement / DeviceMesh / Redistribution)、FSDP/HSDP 训练流水线(参数分片/聚合/梯度同步)以及分布式算子推导进行了全面审计。以下按 严重程度 分级汇报所有发现的问题。


一、致命级别 (Critical) — 会导致运行崩溃或静默产生错误结果

1. is_partial 未加括号调用,条件永远为 True
        if self._apply_eazy_redistribute(from_layout, to_layout):
            if from_layout.is_partial:
                from_layout.reset_partial()
            x = self._redistribution_without_shape(x, from_layout, to_layout, key)

Layout.is_partial 是一个 普通方法(非 @property):

    def is_partial(self):
        """Return true if any dim in mesh_shape is partial"""
        return any(self.partial)

不加 () 取到的是方法对象本身,Python 中任何方法对象都是 truthy。后果

  • 第 214 行:reset_partial() 永远被调用,即使没有 partial 状态。这会篡改 from_layout(即源 DTensor 的 layout),可能导致共享同一 layout 对象的其他 DTensor 状态被破坏。
  • 第 264 行同一文件:not from_layout.is_partial 永远为 Falsereduce_partial 的 early-return 永远不触发,即使没有 partial 也会尝试执行 reduce。
2. self.platform 未定义 — ReduceScatter 路径必定崩溃
    def _reduce_scatter_along_dev_dim_with_axis(self, x, axis, op, layout, dev_dim):
        """Do reduce_scatter at specified axis along dev_dim."""
        dev_num = layout.mesh_shape[layout.alias_name.index(dev_dim)]
        group = layout.get_comm_group_by_axis(dev_dim)
        output_tensor = self.platform.reduce_scatter(x, dev_num, axis, op, group)
        return output_tensor

TensorRedistribution 类没有 self.platform 属性;模块级变量是 platform。当重分布路径需要 ReduceScatter(Partial -> Shard 转换)时,会抛出 AttributeError这意味着 Partial -> Shard 重分布功能完全不可用。

3. DeviceMesh 缓存 key 仅用首尾 rank — 不同拓扑可能返回错误 Mesh
    rank_ids = (rank_list[0], rank_list[-1])
    mesh_dim_names = tuple(mesh_dim_names)
    map_key = hash((mesh_shape, mesh_dim_names, rank_ids))

只使用 rank_list 的第一个和最后一个 rank 作为缓存 key。两个 shape 和 dim_names 相同但内部 rank 排布不同的 mesh(如 (0,2,1,3) vs (0,1,2,3))会命中同一缓存。后果:返回错误的 DeviceMesh 对象,通信分组错误,数据放置错误。同样的缺陷存在于 to_hash() 方法中,会进一步传播到 redistribution 的缓存 key。

4. post_backwardreduced_grad 可能未定义 — HSDP 场景崩溃
                if hsdp_param.shard_world_size > 1:
                    if hsdp_param.unsharded_param.grad is None:
                        continue
                    reduced_grad, _ = hsdp_param.reduce_scatter_grad(...)
                if self.requires_all_reduce and hsdp_param.replicate_world_size > 1:
                    ...
                    reduced_grad, _ = hsdp_param.all_reduce_grad(
                        grad=reduced_grad,  # <-- 可能 NameError
                        ...
                    )

当参数 shard_world_size == 1(未分片)但 replicate_world_size > 1(有复制)时,if shard_world_size > 1 分支被跳过,reduced_grad 从未赋值。下方 all_reduce_grad(grad=reduced_grad) 将抛出 UnboundLocalError。这个场景在 HSDP(2D mesh,部分维度复制、部分维度分片)下完全有可能出现。

5. set_requires_all_reduce 设置了错误的属性名
    def set_requires_all_reduce(self, requires_all_reduce: bool):
        if self.hsdp_state is not None:
            self.hsdp_state.all_reduce_grads = requires_all_reduce

实际使用的属性名是 self.requires_all_reduce(在 TorchHSDPStateV2.__init__ 第 51 行设置),但此 setter 写的是 all_reduce_grads,完全是一个新属性。后果:用户调用 set_requires_all_reduce(False) 后静默无效,梯度的 AllReduce 仍然会执行。


二、严重级别 (High) — 可能导致精度问题或特定场景下的错误

6. RNG offset 计算使用了 mesh shape 而非 tensor shape
    def _set_post_op_offset(self, state, device_mesh, old_offset):
        dtensor_shape = device_mesh.mesh_shape  # BUG
        numel = functools.reduce(operator.mul, dtensor_shape, 1)

应该使用分布式张量的全局 shape 而非设备网格 shape。这会导致各 rank 的 RNG offset 推进量错误,破坏随机数的确定性重现保证,进而导致 dropout 等操作在不同 rank 上不一致。

7. need_synchronize 变量作用域 Bug — 仅反映最后一个参数
                need_synchronize = self._apply_reduced_grad(hsdp_param, reduced_grad)
            if need_synchronize:

need_synchronize 在 for 循环内赋值,但检查在循环外。只有最后一个参数的返回值生效。如果前面的参数需要同步(如 CPU offload)但最后一个不需要,同步会被跳过,导致 CPU offload 场景下的数据竞争。

8. Layout _infer_slice_area_by_rank 不处理不均匀分片
        slice_size = full_shape[axis] // split_num
        start = slice_id * slice_size

整数除法直接截断,不均匀分片时(如 shape=7 在 2 设备上分片),最后的元素被静默丢弃。PyTorch DTensor 会处理 remainder shard,这里缺失。

9. Norm 算子的输出 layout 可能错误
        output_map = x_layout.alias_tensor_map[:begin_norm_axis] + ("None",) * len(gamma_tensor_map)

NormDistributedOp 返回的 out_layout 将归一化维度设为 "None"(不分片),这是 mean/rstd 辅助输出的 layout,而非主输出的 layout。主输出应保持与输入相同的 layout。


三、性能级别 (Performance) — 严重影响训练效率

10. 所有通信操作均为同步执行 — 无计算与通信重叠

这是 最大的性能问题

unshard 阶段(forward/backward 前的 all-gather):

            for param in self.sharded_hsdp_params:
                param.unshard()
                param.wait_for_unshard()

每个参数的 all-gather 逐个同步执行,完成后才开始下一个。

post_backward 阶段(reduce-scatter + all-reduce):

                    reduced_grad, _ = hsdp_param.reduce_scatter_grad(
                        dtype=self._reduce_dtype,
                        reduce_op=self.reduce_op_type
                    )
                    ...
                    reduced_grad, _ = hsdp_param.all_reduce_grad(
                        grad=reduced_grad,
                        reduce_op=self.reduce_op_type,
                    )

两者均为同步操作(async_op 默认 False),没有计算与通信重叠的可能。PyTorch 原生 FSDP2 通过独立的 CUDA stream 实现 overlap,此处完全缺失。

11. V2 路径无梯度 Bucketing 支持
        self.comm_async = False
        self.comm_fusion = False
        self.bucket_size = 9999
        self.grad_fusion = False

配置硬编码关闭了通信融合和梯度融合。每个参数独立发起一次 reduce-scatter/all-reduce 小集合通信,大量小消息无法利用网络带宽。

12. Layout 构造存在双重 deepcopy
    def __call__(self, *alias_tensor_map):
        obj = copy.deepcopy(self)        # 第一次
        ...
    def _process_placement_layout(self, obj, placements):
        obj.set_placements(placements)
        return copy.deepcopy(obj)         # 第二次

每次从 placement 构造 Layout 都执行两次 deepcopy,包括其中的 DeviceMesh(numpy 数组、缓存数据结构等)。这个路径是 DTensor 创建和重分布的热路径。

13. reduce-scatter 每次反向传播都重新分配输出 buffer
        output = torch.empty(output_numel, dtype=reduce_dtype, device=grad.device)

不像 all-gather 有持久化 buffer 的机制,reduce-scatter 每次调用都 torch.empty,增加显存分配压力。

14. SDPA 序列并行构造显式 causal mask 退化了 FlashAttention 的优化
    def _build_causal_mask_for_chunk(self, local_q_len, global_kv_len, split_id, device):
        ...
        return kv_positions <= q_positions

当启用序列并行 + causal masking 时,代码物化了一个 [local_q_len, global_kv_len] 的 bool mask。这使得 FlashAttention 内核无法使用其高效的内建 causal mask 优化,长序列下会产生巨大的额外显存开销。


四、可扩展性问题 (Scalability)

15. DeviceMesh 中 rank 查找为 O(n) 线性搜索
        idx = rank_list.index(rank)

出现在多个高频调用方法中(get_rank_list_along_axis, get_devices_for_axis, get_local_rank)。在大规模集群(数百/数千设备)下会成为瓶颈。应使用预构建的 rank -> index 字典。

16. Layout 缓存无上限 — 动态 shape 场景内存泄漏
class LayoutCacheManager:
    def __init__(self):
        self.layout_cache: Dict[str, Dict[LayoutCacheKey, Any]] = {}

Layout 缓存无淘汰策略,无容量上限。在变长序列或动态 shape 场景中(如 NLP 训练),缓存会无限增长。

17. 无词表并行 Embedding 支持

EmbeddingDistributedOp 仅处理 embedding 维度(weight 最后一维)的分片。如果用户在词表维度(weight 第 0 维)做分片,会静默产生错误结果。大模型训练中词表并行是基本需求。

18. Prefetch 需手动配置 — 无自动执行序分析

set_modules_to_forward_prefetch / set_modules_to_backward_prefetch

预取需要用户手动指定模块依赖。PyTorch FSDP2 能自动从执行图推导预取顺序。当前实现对用户要求过高,且无法做到参数粒度的细粒度预取。


五、其他代码质量问题

问题 位置 说明
Shard(0)Replicate() 哈希冲突 placement_types.py:76,97 两者 __hash__ 都返回 0,降低 dict/set 性能
unshard() 中 raise 之后有死代码 hsdp_state.py:129-131 comm_fusion 分支因 raise 永远不执行
ignored_params 参数从未被使用 api.py:356 + 整个 init 链 用户传入的忽略参数列表完全无效
hsdp_params_with_grad / unsharded_grads 声明未使用 state.py:199-200 死代码,疑似重构遗留
Partial.__str__ 丢失 reduce_op 信息 placement_types.py:134 Partial("sum")Partial("max") 都显示为 "P"
avg reduction 的溢出风险 tensor_redistribution.py:239-242 fp16/bf16 下先 AllReduce-sum 再除 N,sum 步骤可能溢出

六、总结与建议

优先级 P0 — 必须立即修复
  1. is_partial 加括号 — 影响所有重分布路径的正确性
  2. self.platformplatform — 否则 ReduceScatter 完全不可用
  3. DeviceMesh 缓存 key 使用完整 rank_list — 否则多 mesh 场景必出错
  4. post_backwardreduced_grad 的 NameError — HSDP 配置下必崩
  5. set_requires_all_reduce 属性名修正 — 否则 API 静默无效
优先级 P1 — 影响精度和效率
  1. 引入异步通信 + 独立 communication stream,实现 compute-communication overlap
  2. 实现梯度 bucketing / fusion
  3. 修复 RNG offset 计算
  4. 修复 need_synchronize 作用域
  5. Layout 构造消除多余 deepcopy
优先级 P2 — 提升可扩展性
  1. 支持不均匀分片
  2. 支持词表并行 Embedding
  3. Layout 缓存加 LRU 淘汰
  4. DeviceMesh rank 查找用哈希表
  5. 自动 prefetch 机制

整体来看,框架的架构设计(DTensor + Layout + Placement 抽象、HSDP scheduler/state/param 分层)是合理的,与 PyTorch FSDP2 的设计理念一致。但在 实现细节 上存在多个致命 bug(特别是 P0 的 5 个问题),以及在 通信性能 上与成熟框架有显著差距(同步通信、无 bucketing、无 overlap)。建议按优先级逐步修复,P0 问题应阻断合入。

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

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

This report spans tensor_redistribution.py, device_mesh.py, layout.py, fully_shard/state.py, hsdp_scheduler.py, random.py, and several shard and parameter modules. Start by triaging and reproducing the five P0 findings, then inspect nearby tests or add focused regression coverage for each confirmed problem. Done means the selected issues are fixed without introducing incorrect layouts, communication failures, or gradient errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, distributed-systems, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.