MindSpore 后端的 PipelineStageBase._grad_position_from_requires_grad() 在“所有 positional Tensor 都需要梯度”时返回 -1。但 forward_and_gradfn() 对 grad_position=-1 的公开语义是:将所有 positional 和 keyword Tensor 都作为输入梯度目标,并原地设置 _requires_grad=True。
PipelineStageBase._grad_position_from_requires_grad()
-1
forward_and_gradfn()
grad_position=-1
_requires_grad=True
PipelineStage 实际只会通过 PP backward-send 路由回传 positional stage input(接收到的 activation)梯度,并没有 kwargs 的 backward-send 路由。因此,这里把 -1 当作“所有 positional Tensor”的快捷表示,会意外修改调用方持有的共享 kwargs Tensor。
PipelineStage
在 interleaved pipeline + full recompute 场景中,这个副作用会改变重计算前后输入 Tensor 的梯度状态,进一步造成重计算 placeholder 顺序错位,最终在与根因无关的下游算子中表现为 shape mismatch。
已复现的场景为 MindFormers PyNative DeepSeek-V4:
input_ids
schedule.run(**inputs)
具体过程如下:
requires_grad_indices
[]
forward_and_gradfn(..., grad_position=-1, **kwargs)
input_ids._requires_grad
False
True
Mul
关闭 interleave 后能够运行,是因为 rank 0 不再同时持有后续 non-first virtual stage;没有 stage 4 在 stage 0 重计算前原地修改同一份 kwargs,触发链被切断。
旧的 stage 侧选择逻辑:
if not requires_grad_indices: return [] if len(requires_grad_indices) == len(tensor_indices): return -1 return tuple(requires_grad_indices)
而 forward_and_gradfn() 的 -1 分支会处理 args 和 kwargs:
if grad_position == -1: flatten_inputs, _ = tree_flatten(inputs, tensors_only_leaf=True) for inp in flatten_inputs: inp._requires_grad = True flatten_kwargs, _ = tree_flatten(kwargs, tensors_only_leaf=True) for kwarg in flatten_kwargs: kwarg._requires_grad = True
因此根因不是 forward_and_gradfn(-1) 的实现错误,而是 PipelineStage 使用了语义范围更大的 -1 哨兵。
forward_and_gradfn(-1)
下面的代码展示 stage 选择逻辑与 forward_and_gradfn 组合后的副作用:
forward_and_gradfn
import os os.environ["HYPER_PARALLEL_PLATFORM"] = "mindspore" import numpy as np import mindspore as ms from mindspore import Tensor, nn, ops from hyper_parallel.platform.mindspore.pipeline_parallel.backward import forward_and_gradfn from hyper_parallel.platform.mindspore.pipeline_parallel.stage import PipelineStageBase class Net(nn.Cell): def construct(self, activation, input_ids=None): return activation * ops.cast(input_ids, activation.dtype) ms.set_context(mode=ms.PYNATIVE_MODE) activation = Tensor(np.array([2.0], np.float32)) activation._requires_grad = True input_ids = Tensor(np.array([3], np.int32)) grad_position = PipelineStageBase._grad_position_from_requires_grad([activation]) assert grad_position == -1 # 修复前 forward_and_gradfn(Net(), activation, grad_position=grad_position, input_ids=input_ids) assert not input_ids._requires_grad # 修复前失败:实际为 True
这里最后一个断言表达的是 PipelineStage 所需的隔离性,不是要改变 forward_and_gradfn(-1) 的公开语义。
在重计算输入和 placeholder pack/unpack 处增加 Python 打点后,能够稳定观察到:
input_ids expected: Tensor(shape=[1, 1024], dtype=Int32, requires_grad=False) input_ids actual: Tensor(shape=[1, 1024], dtype=Int32, requires_grad=True) placeholder index 67 expected: Tensor(shape=[1024, 32], dtype=Float32) placeholder index 67 actual: Tensor(shape=[1024], dtype=Int32)
随后报错:
ValueError: For 'Mul', input1.shape and input2.shape need to broadcast, but got input1.shape = [2048, 512] and input2.shape = [1024, 1, 4, 512]
将 stage 的返回值改为显式 positional tuple 后,input_ids 不再发生状态变化,placeholder mismatch 和后续 Mul shape mismatch 同时消失。该因果关系已通过最小 UT、4 卡 ST 和原始 8 卡训练场景交叉验证,并非仅根据最终异常推测。
修改 MindSpore 或 forward_and_gradfn(-1) 的公共语义
_requires_grad
在 MindFormers 侧规避
为 forward_and_gradfn 增加独立的 kwargs 梯度选择 API
grad_keyword_names
Hyper PipelineStage 始终传入显式 positional indices(当前采用)
_grad_position_from_requires_grad()
tuple(requires_grad_indices)
()
(0, 1, ...)
当前核心修改为:
requires_grad_indices = [ i for i in tensor_indices if composite_args[i]._requires_grad ] return tuple(requires_grad_indices)
并增加以下回归覆盖:
dx
_requires_grad=False
loss: 11.783281 load_balancing_loss: 1.128077 mtp_1_loss: 3.541678 grad_norm: 7.811133
a7a9b2b4fad6dfea6d73a255795d7ced5b261c00
2.10.0
ms/master
650abf288
aarch64
Checklist
🐛 Describe the bug
问题概述
MindSpore 后端的
PipelineStageBase._grad_position_from_requires_grad()在“所有 positional Tensor 都需要梯度”时返回-1。但forward_and_gradfn()对grad_position=-1的公开语义是:将所有 positional 和 keyword Tensor 都作为输入梯度目标,并原地设置_requires_grad=True。PipelineStage实际只会通过 PP backward-send 路由回传 positional stage input(接收到的 activation)梯度,并没有 kwargs 的 backward-send 路由。因此,这里把-1当作“所有 positional Tensor”的快捷表示,会意外修改调用方持有的共享 kwargs Tensor。在 interleaved pipeline + full recompute 场景中,这个副作用会改变重计算前后输入 Tensor 的梯度状态,进一步造成重计算 placeholder 顺序错位,最终在与根因无关的下游算子中表现为 shape mismatch。
触发条件与前因后果
已复现的场景为 MindFormers PyNative DeepSeek-V4:
input_ids、mask 等模型输入通过schedule.run(**inputs)作为 kwargs 传入并在 virtual stage 间复用。具体过程如下:
requires_grad_indices,返回[],因此 stage 0 不会修改 kwargs。_requires_grad=True。旧逻辑因此返回-1。forward_and_gradfn(..., grad_position=-1, **kwargs)会递归遍历 kwargs,并原地把共享的input_ids._requires_grad从False改为True。Mul中暴露为不相关的 shape mismatch,容易被误判为 Hyper-Connections 或 MindSpore 算子问题。关闭 interleave 后能够运行,是因为 rank 0 不再同时持有后续 non-first virtual stage;没有 stage 4 在 stage 0 重计算前原地修改同一份 kwargs,触发链被切断。
根因代码
旧的 stage 侧选择逻辑:
if not requires_grad_indices: return [] if len(requires_grad_indices) == len(tensor_indices): return -1 return tuple(requires_grad_indices)而
forward_and_gradfn()的-1分支会处理 args 和 kwargs:if grad_position == -1: flatten_inputs, _ = tree_flatten(inputs, tensors_only_leaf=True) for inp in flatten_inputs: inp._requires_grad = True flatten_kwargs, _ = tree_flatten(kwargs, tensors_only_leaf=True) for kwarg in flatten_kwargs: kwarg._requires_grad = True因此根因不是
forward_and_gradfn(-1)的实现错误,而是PipelineStage使用了语义范围更大的-1哨兵。最小复现
下面的代码展示 stage 选择逻辑与
forward_and_gradfn组合后的副作用:import os os.environ["HYPER_PARALLEL_PLATFORM"] = "mindspore" import numpy as np import mindspore as ms from mindspore import Tensor, nn, ops from hyper_parallel.platform.mindspore.pipeline_parallel.backward import forward_and_gradfn from hyper_parallel.platform.mindspore.pipeline_parallel.stage import PipelineStageBase class Net(nn.Cell): def construct(self, activation, input_ids=None): return activation * ops.cast(input_ids, activation.dtype) ms.set_context(mode=ms.PYNATIVE_MODE) activation = Tensor(np.array([2.0], np.float32)) activation._requires_grad = True input_ids = Tensor(np.array([3], np.int32)) grad_position = PipelineStageBase._grad_position_from_requires_grad([activation]) assert grad_position == -1 # 修复前 forward_and_gradfn(Net(), activation, grad_position=grad_position, input_ids=input_ids) assert not input_ids._requires_grad # 修复前失败:实际为 True这里最后一个断言表达的是
PipelineStage所需的隔离性,不是要改变forward_and_gradfn(-1)的公开语义。定位证据
在重计算输入和 placeholder pack/unpack 处增加 Python 打点后,能够稳定观察到:
随后报错:
将 stage 的返回值改为显式 positional tuple 后,
input_ids不再发生状态变化,placeholder mismatch 和后续Mulshape mismatch 同时消失。该因果关系已通过最小 UT、4 卡 ST 和原始 8 卡训练场景交叉验证,并非仅根据最终异常推测。Expected behavior
PipelineStage只把需要 PP backward-send 的 positional stage inputs 作为输入梯度目标;forward_and_gradfn(-1)的公开 args+kwargs 求梯度语义保持不变;Additional context
可选修复方案
修改 MindSpore 或
forward_and_gradfn(-1)的公共语义_requires_grad。forward_and_gradfn(-1)已有的 args+kwargs 求梯度语义和对应测试;同时把 PipelineStage 的策略泄漏到通用 autograd API,影响范围过大。在 MindFormers 侧规避
input_ids从 kwargs 改为其他传递方式。为
forward_and_gradfn增加独立的 kwargs 梯度选择 APIgrad_keyword_names,允许 positional 和 keyword 梯度目标分别指定。Hyper PipelineStage 始终传入显式 positional indices(当前采用)
_grad_position_from_requires_grad()统一返回tuple(requires_grad_indices);没有目标时为(),全部 positional Tensor 都需要梯度时为(0, 1, ...),不再返回-1。forward_and_gradfn(-1)公共语义;权重梯度目标不变。当前核心修改为:
requires_grad_indices = [ i for i in tensor_indices if composite_args[i]._requires_grad ] return tuple(requires_grad_indices)并增加以下回归覆盖:
dx正确,Int32input_ids保持_requires_grad=False;forward_and_gradfn(-1)kwargs 梯度测试,确认公共 API 未回归;Environment info
a7a9b2b4fad6dfea6d73a255795d7ced5b261c002.10.0ms/master:650abf288aarch64