mindspore-ai / mindspore-ai/hyper-parallel
[Bug] 分布式 MatMul 的 layout 推断不传播输入的 Partial 状态,导致链式 matmul(如 LoRA `(x@Aᵀ)@Bᵀ`)的反向梯度被破坏(数值减半 / 1-over-N)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 53
- Forks
- 63
- Avg merge
- 23h 45m
- Merged PRs (30d)
- 63
Description
环境
- hyper_parallel commit
d16b218(MindSpore 后端) - MindSpore 2.10.0;CANN 9.1.0-beta.1;Ascend 910B2
- 复现于 TP(tensor parallel)下 row-wise 切分的链式低秩 matmul
结论(先说定界)
这是 hyper_parallel 的 DTensor 分布式 MatMul bug,不是上层(MindFormers / LoRA 切分计划)的问题。 纯 DTensor 最小用例(不含任何 MindFormers / LoRA 代码、不含自定义 ParallelStyle)即可复现:对一个本应得到 Partial 输出的 matmul,框架按「自己的 contracting 维」推断 layout,不继承输入张量已有的 Partial 状态,于是把结果错误标成非 Partial,前向数值错误、反向梯度被破坏(对 Replicate 参数其梯度数值变为正确值的 1/N,N=TP 度)。
根因
链式 (x @ Aᵀ) @ Bᵀ,在 row-wise TP 下:
x=Shard(-1)(in-dim 切分),A=Shard(1),B=Replicate;- 第一个 matmul 沿被切分的 in-dim 规约 →
tmp是Partial(正确); - 第二个 matmul 沿未切分的 rank 维规约。MatMul layout 推断只看自己的 contracting 维(未切分→判定非 Partial),忽略了
tmp已是Partial,于是lora_out被错标为非 Partial; - 前向把未规约的
Partial当成已规约值使用 → 数值错误;反向时B(Replicate)的梯度被错误规约(等价于做了平均而非求和)→ 梯度 = 正确值的 1/N。
注:Partial → Replicate 的 redistribute 本身是正确的(做 SUM)(见复现脚本测试1),问题仅在 matmul 不传播输入 Partial。
最小复现脚本(纯 hyper_parallel DTensor,无 MindFormers)
repro_dtensor.py,2 卡:msrun --worker_num=2 --local_worker_num=2 --master_port=59799 --join=True repro_dtensor.py
import os
os.environ.setdefault("HYPER_PARALLEL_PLATFORM", "mindspore")
import numpy as np, mindspore as ms
from mindspore import mint
from mindspore.mint.distributed import init_process_group
from mindspore.communication import get_rank, get_group_size
from hyper_parallel import init_device_mesh, DTensor
from hyper_parallel.core.dtensor.placement_types import Shard, Replicate, Partial
ms.set_context(mode=ms.PYNATIVE_MODE)
init_process_group()
RANK, WORLD = get_rank(), get_group_size()
mesh = init_device_mesh("npu", (WORLD,), mesh_dim_names=("tp",))
def log(*a):
if RANK == 0: print("[REPRO]", *a, flush=True)
# 测试1:Partial->Replicate redistribute 应做 SUM
local = ms.Tensor(np.full((4,), float(RANK + 1), dtype=np.float32)) # rank r 持 (r+1)
rep = DTensor.from_local(local, mesh, (Partial(),)).redistribute(device_mesh=mesh, placements=(Replicate(),))
log("测试1 Partial->Replicate got=", float(rep.to_local().asnumpy()[0]), " (WORLD=2 时 SUM应=3, MEAN=1.5)")
# 测试2:链式 (x@Aᵀ)@Bᵀ,对 Replicate 的 B 求梯度,对照单卡 ground-truth
np.random.seed(0)
R, DIN, DOUT, BS = 8, 16, 32, 4
Xf = np.random.randn(BS, DIN).astype(np.float32)
Af = (np.random.randn(R, DIN) * 0.1).astype(np.float32)
Bf = (np.random.randn(DOUT, R) * 0.1).astype(np.float32)
tmp_full = Xf @ Af.T
out_truth = float((tmp_full @ Bf.T).sum())
gB_truth_norm = float(np.linalg.norm(np.tile(tmp_full.sum(0, keepdims=True), (DOUT, 1))))
sh = DIN // WORLD
x_loc = ms.Tensor(Xf[:, RANK*sh:(RANK+1)*sh]); a_loc = ms.Tensor(Af[:, RANK*sh:(RANK+1)*sh]); b_full = ms.Tensor(Bf)
def fwd(b_full_, a_loc_, x_loc_, reduce_tmp):
x_dt = DTensor.from_local(x_loc_, mesh, (Shard(1),))
a_dt = DTensor.from_local(a_loc_, mesh, (Shard(1),))
b_dt = DTensor.from_local(b_full_, mesh, (Replicate(),))
tmp = mint.matmul(x_dt, mint.transpose(a_dt, 1, 0)) # -> 应为 Partial
if reduce_tmp and isinstance(tmp, DTensor) and any(p.is_partial() for p in tmp.placements):
tmp = tmp.reduce_partial() # 规避:先 all-reduce 成 Replicate
lora = mint.matmul(tmp, mint.transpose(b_dt, 1, 0)) # 沿未切分 rank 维规约
return lora.redistribute(device_mesh=mesh, placements=(Replicate(),)).to_local().sum()
for reduce_tmp in (False, True):
fv = float(fwd(b_full, a_loc, x_loc, reduce_tmp).asnumpy())
_, gB = ms.value_and_grad(fwd, grad_position=0)(b_full, a_loc, x_loc, reduce_tmp)
ratio = float(np.linalg.norm(gB.asnumpy()) / gB_truth_norm)
log(f"测试2 reduce_partial={reduce_tmp}: forward={fv:.5f}(truth {out_truth:.5f}) grad_ratio={ratio:.4f}")
期望 vs 实测
| 期望 | 实测 | |
|---|---|---|
| 测试1 Partial→Replicate | SUM=3.0 | 3.0 ✓(redistribute 正确) |
测试2 reduce_partial=False forward |
0.5535 | 0.6640(错) |
测试2 reduce_partial=False grad_B 范数比 |
1.0 | 0.5104 ≈ 1/tp(梯度被破坏) |
测试2 reduce_partial=True(先规约)forward |
0.5535 | 0.5535 ✓ |
测试2 reduce_partial=True grad_B 范数比 |
1.0 | 1.0000 ✓ |
即:只要在第二个 matmul 前把 Partial 显式规约成 Replicate,前向与梯度都恢复正确;不规约则前向错、梯度变 1/N。
MindFormers 端到端复现(动态图 LoRA + TP)
版本:mindformers feat/pynative-lora-v1 @ 047ae22d9,DeepSeek-V3 12 层 MoE,seq 4096,2 卡。
- 触发点:
mindformers/pynative/pet/lora_layer.py: LinearWithLoRA.construct,链式tmp=x@lora_aᵀ; lora_out=tmp@lora_bᵀ。 - 复现方式(等价性判据):所有并行场景加载同一份 base 权重 W0(单卡 lr=0 存 ckpt),统一
global_batch_size=4 / lr=1e-4 / 20 步 / 仅加载权重,以单卡为 baseline 比较 loss 曲线逐步 diff。 - yaml 关键项:
parallelism.tensor_parallel: 2、sequence_parallel: true、model.compute_dtype: bfloat16、lora_config.target_modules含 row-wise 的self_attention.linear_proj、mlp.linear_fc2。 - 现象(未规避前 / lora_layer.py 不调用
reduce_partial):- 单卡 baseline
lora_b.grad‖·‖²=1.66e-11;TP=2 同一 W0/同 step1 = 4.18e-12 → 范数比 0.502 ≈ 1/tp(减半); - loss 曲线单边漂移:step20 TP loss 比单卡高 +0.045,grad_norm 仅为单卡 ~57%;
- 控制实验排除其它解释:fp32 下仍发散(非 bf16 噪声)、前向逐位对齐(非前向)、full-ft(无 LoRA)不漂移(非基座)。
- 单卡 baseline
- 规避后(lora_layer.py 调用
tmp.reduce_partial()):TP/TP×CP/DP×TP/DP×TP×CP loss diff 均回到零轴波动(mean≈+0.0001、max|Δ|≈0.003、正负步数 10/9),与单卡一致。
期望行为
分布式 MatMul 的 output layout 推断应继承输入张量的 Partial 状态(输入 Partial → 输出 Partial),使链式 matmul 的前向/反向自动正确,无需上层手动 reduce_partial 规避。
schema_version: 1
source: gitcode
gitcode_repo: mindspore/hyper-parallel
gitcode_issue: 228
source_url: https://gitcode.com/mindspore/hyper-parallel/issues/228
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with repro_dtensor.py and trace the mint.matmul calls into distributed layout inference, focusing on how an input Partial placement is handled by the second chained matmul. Compare the behavior with the explicit reduce_partial workaround and the Partial-to-Replicate test. Done means the chained case preserves the required Partial state and matches the reported forward and gradient results without that workaround.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100