Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / tensor_parallel / mc2.py: 43%

130 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""MC2 (matmul + communication) fused linear primitives for tensor parallelism. 

16 

17PyTorch / Ascend exposes fused kernels via ``torch_npu.npu_all_gather_base_mm`` and 

18``torch_npu.npu_mm_reduce_scatter_base``. These kernels do not ship a complete 

19autograd formula for every training path, so this module wraps each fused forward 

20with a custom backward so they can be used inside a trainable TP + SP model. 

21 

22The two kernels are mathematical duals (aligned with MindFormers MC2): 

23 

24* ``all_gather_matmul`` forward : ``Y = AllGather_m(X) @ W^T`` 

25 backward: ``dX = matmul_reduce_scatter(dY, W)`` 

26 ``dW = dY^T @ AllGather_m(X)`` 

27 

28* ``matmul_reduce_scatter`` forward : ``Y = ReduceScatter_m(X @ W^T)`` 

29 backward: ``dX, dY_full = all_gather_matmul(dY, W)`` 

30 ``dW = AllGather_m(dY)^T @ X`` 

31 

32Both forward and backward ``dX`` paths use fused Ascend MC2 kernels. Callers must 

33satisfy kernel constraints (notably contraction dim ``k ∈ [256, 65535)``); for 

34column-parallel backward that ``k`` is local ``out_features`` (``n_local``). 

35 

36All autograd functions operate on **local** (non-DTensor) 2-D tensors. 

37``W`` uses the native ``nn.Linear`` layout ``(out, in)``. 

38""" 

39from __future__ import annotations 

40 

41from typing import Any, Optional 

42 

43import torch 

44from torch import nn 

45 

46from hyper_parallel.core.dtensor.dtensor import DTensor 

47from hyper_parallel.core.dtensor.placement_types import Shard 

48 

49__all__ = [ 

50 "get_hcomm_info", 

51 "AllGatherMatmulFunction", 

52 "MatmulReduceScatterFunction", 

53 "MC2Linear", 

54] 

55 

56 

57def get_hcomm_info(group: Any) -> str: 

58 """Resolve Ascend HCCL communicator name for a torch ProcessGroup. 

59 

60 Args: 

61 group (Any): ``torch.distributed.ProcessGroup`` for the TP mesh axis. 

62 

63 Returns: 

64 HCCL communicator handle name expected by ``torch_npu`` MC2 kernels. 

65 """ 

66 rank = torch.distributed.get_rank(group) 

67 if torch.__version__ > "2.0": 

68 global_rank = torch.distributed.get_global_rank(group, rank) 

69 # torch.distributed ProcessGroup exposes HCCL via a private backend API. 

70 return group._get_backend(torch.device("npu")).get_hccl_comm_name( # pylint: disable=protected-access 

71 global_rank 

72 ) 

73 return group.get_hccl_comm_name(rank) 

74 

75 

76def _require_torch_npu(): 

77 try: 

78 import torch_npu # pylint: disable=import-outside-toplevel 

79 except ImportError as exc: 

80 raise RuntimeError( 

81 "MC2 fused kernels require torch_npu " 

82 "(npu_all_gather_base_mm / npu_mm_reduce_scatter_base)." 

83 ) from exc 

84 return torch_npu 

85 

86 

87def _normalize_sequence_dim(sequence_dim: int, ndim_leading: int) -> int: 

88 """Resolve a possibly-negative sequence dim against leading-rank count.""" 

89 seq_dim = sequence_dim 

90 if seq_dim < 0: 

91 seq_dim += ndim_leading 

92 if seq_dim < 0 or seq_dim >= ndim_leading: 

93 raise RuntimeError( 

94 f"MC2Linear sequence_dim={sequence_dim} is out of range " 

95 f"for leading rank {ndim_leading}." 

96 ) 

97 return seq_dim 

98 

99 

100def _move_dim_to_front(tensor: torch.Tensor, dim: int) -> torch.Tensor: 

101 """Permute ``dim`` to axis 0 so fused AG/RS on flattened dim-0 is SP-correct.""" 

102 if dim == 0: 

103 return tensor 

104 order = (dim,) + tuple(i for i in range(tensor.dim()) if i != dim) 

105 return tensor.permute(*order).contiguous() 

106 

107 

108def _move_front_to_dim(tensor: torch.Tensor, dim: int) -> torch.Tensor: 

109 """Inverse of :func:`_move_dim_to_front`.""" 

110 if dim == 0: 

111 return tensor 

112 # front -> dim: [1..dim] + [0] + [dim+1..] 

113 order = tuple(range(1, dim + 1)) + (0,) + tuple(range(dim + 1, tensor.dim())) 

114 return tensor.permute(*order).contiguous() 

115 

116 

117class AllGatherMatmulFunction(torch.autograd.Function): 

118 """Column-parallel fused all-gather + matmul with custom backward. 

119 

120 Forward (local view): ``out = AllGather_m(x) @ w^T``. 

121 """ 

122 

123 @staticmethod 

124 def forward(ctx, x, w, group, world_size, bias): # pylint: disable=arguments-differ 

125 """Run fused all-gather + matmul and stash tensors for backward.""" 

126 torch_npu = _require_torch_npu() 

127 hcom = get_hcomm_info(group) 

128 # x2 = w.T -> physical (k, n_local); kernel computes AG(x) @ x2 

129 out, gathered = torch_npu.npu_all_gather_base_mm( 

130 x, 

131 w.t(), 

132 hcom, 

133 world_size, 

134 bias=None, 

135 gather_index=0, 

136 gather_output=True, 

137 ) 

138 if bias is not None: 

139 out = out + bias 

140 ctx.save_for_backward(gathered, w) 

141 ctx.group = group 

142 ctx.world_size = world_size 

143 ctx.hcom = hcom 

144 ctx.has_bias = bias is not None 

145 return out 

146 

147 @staticmethod 

148 def backward(ctx, grad_out): # pylint: disable=arguments-differ 

149 """Gradient: dx via fused matmul_reduce_scatter, dw via gathered-input matmul. 

150 

151 Matches MindFormers: ``dX = matmul_reduce_scatter(dY, W)``. The fused 

152 kernel contracts over ``n_local`` (``W.shape[0]``), which must be in 

153 ``[256, 65535)``. 

154 """ 

155 torch_npu = _require_torch_npu() 

156 gathered, w = ctx.saved_tensors 

157 # input (m_full, n_local) @ x2 (n_local, k) -> RS_m -> (m_local, k) 

158 grad_x = torch_npu.npu_mm_reduce_scatter_base( 

159 grad_out.contiguous(), 

160 w, 

161 ctx.hcom, 

162 ctx.world_size, 

163 reduce_op="sum", 

164 bias=None, 

165 ) 

166 grad_w = grad_out.t().matmul(gathered) 

167 grad_bias = grad_out.sum(dim=0) if ctx.has_bias else None 

168 return grad_x, grad_w, None, None, grad_bias 

169 

170 

171class MatmulReduceScatterFunction(torch.autograd.Function): 

172 """Row-parallel fused matmul + reduce-scatter with custom backward. 

173 

174 Forward (local view): ``out = ReduceScatter_m(x @ w^T)``. 

175 """ 

176 

177 @staticmethod 

178 def forward(ctx, x, w, group, world_size, bias): # pylint: disable=arguments-differ 

179 """Run fused matmul + reduce-scatter and stash tensors for backward.""" 

180 torch_npu = _require_torch_npu() 

181 hcom = get_hcomm_info(group) 

182 out = torch_npu.npu_mm_reduce_scatter_base( 

183 x, 

184 w.t(), 

185 hcom, 

186 world_size, 

187 reduce_op="sum", 

188 bias=None, 

189 ) 

190 if bias is not None: 

191 out = out + bias 

192 ctx.save_for_backward(x, w) 

193 ctx.group = group 

194 ctx.world_size = world_size 

195 ctx.hcom = hcom 

196 ctx.has_bias = bias is not None 

197 return out 

198 

199 @staticmethod 

200 def backward(ctx, grad_out): # pylint: disable=arguments-differ 

201 """Gradient: dx via fused all-gather+matmul, dw via gathered-grad matmul.""" 

202 torch_npu = _require_torch_npu() 

203 x, w = ctx.saved_tensors 

204 # AG(dY) @ W : pass W (n, k) so the kernel uses transposed-x2 semantics. 

205 grad_x, grad_out_full = torch_npu.npu_all_gather_base_mm( 

206 grad_out, 

207 w, 

208 ctx.hcom, 

209 ctx.world_size, 

210 bias=None, 

211 gather_index=0, 

212 gather_output=True, 

213 ) 

214 grad_w = grad_out_full.t().matmul(x) 

215 grad_bias = grad_out_full.sum(dim=0) if ctx.has_bias else None 

216 return grad_x, grad_w, None, None, grad_bias 

217 

218 

219class MC2Linear(nn.Linear): 

220 """``nn.Linear`` that uses fused matmul + TP communication kernels.""" 

221 

222 def configure_mc2( 

223 self, 

224 mode: str, 

225 group: Any, 

226 world_size: int, 

227 sequence_dim: int = 0, 

228 ) -> None: 

229 """Configure the fused collective used by this layer. 

230 

231 Args: 

232 mode (str): ``\"all_gather\"`` (column) or ``\"reduce_scatter\"`` (row). 

233 group (Any): TP process group. 

234 world_size (int): Size of ``group``. 

235 sequence_dim (int, optional): Tensor dim that carries sequence sharding 

236 under SP. Default: ``0``. 

237 """ 

238 if mode not in ("all_gather", "reduce_scatter"): 

239 raise ValueError( 

240 "For MC2Linear.configure_mc2, mode should be 'all_gather' or " 

241 f"'reduce_scatter', but got {mode}." 

242 ) 

243 self.mc2_mode = mode 

244 self.mc2_group = group 

245 self.mc2_world_size = world_size 

246 self.mc2_sequence_dim = sequence_dim 

247 

248 @classmethod 

249 def from_linear(cls, linear: nn.Linear) -> "MC2Linear": 

250 """Convert a Linear in place while preserving parameters and module state.""" 

251 if not isinstance(linear, nn.Linear): 

252 raise TypeError( 

253 f"MC2Linear can only replace nn.Linear, but got {type(linear).__name__}." 

254 ) 

255 linear.__class__ = cls 

256 return linear 

257 

258 def _mc2_forward(self, input_: DTensor, weight: DTensor) -> DTensor: 

259 """Run the configured fused kernel on local tensors. 

260 

261 Ascend MC2 kernels all-gather / reduce-scatter the **flattened dim-0** 

262 of a 2-D activation. That is only layout-correct when the SP sequence 

263 axis is the outermost leading dim. For ``sequence_dim != 0`` (e.g. 

264 batch-first ``[B, S, H]`` with ``Shard(1)``), move the sequence dim to 

265 front before flatten, then restore after the fused op. 

266 """ 

267 input_local = input_.to_local() 

268 weight_local = weight.to_local() 

269 leading_global = tuple(int(s) for s in input_.shape[:-1]) 

270 seq_dim = _normalize_sequence_dim(self.mc2_sequence_dim, len(leading_global)) 

271 

272 # [..., S_local_or_full, ..., H] -> [S_*, *other_leading, H] -> 2-D 

273 x_seq_first = _move_dim_to_front(input_local, seq_dim) 

274 other_leading = tuple(x_seq_first.shape[1:-1]) 

275 input_2d = x_seq_first.reshape(-1, x_seq_first.shape[-1]) 

276 

277 bias_local = None 

278 if self.bias is not None: 

279 bias = self.bias 

280 bias_local = bias.to_local() if isinstance(bias, DTensor) else bias 

281 

282 if self.mc2_mode == "all_gather": 

283 output_2d = AllGatherMatmulFunction.apply( 

284 input_2d, 

285 weight_local, 

286 self.mc2_group, 

287 self.mc2_world_size, 

288 bias_local, 

289 ) 

290 # AG expands the sequence dim; other leading dims stay local sizes. 

291 seq_global = leading_global[seq_dim] 

292 output = output_2d.reshape(seq_global, *other_leading, output_2d.shape[-1]) 

293 output = _move_front_to_dim(output, seq_dim) 

294 return DTensor.from_local(output, input_.device_mesh, (Shard(-1),)) 

295 

296 seq_global = leading_global[seq_dim] 

297 if seq_global % self.mc2_world_size != 0: 

298 raise RuntimeError( 

299 f"MC2Linear reduce_scatter requires sequence dim {seq_dim} " 

300 f"(size {seq_global}) divisible by world_size " 

301 f"{self.mc2_world_size}." 

302 ) 

303 output_2d = MatmulReduceScatterFunction.apply( 

304 input_2d, 

305 weight_local, 

306 self.mc2_group, 

307 self.mc2_world_size, 

308 bias_local, 

309 ) 

310 seq_local = seq_global // self.mc2_world_size 

311 output = output_2d.reshape(seq_local, *other_leading, output_2d.shape[-1]) 

312 output = _move_front_to_dim(output, seq_dim) 

313 return DTensor.from_local(output, input_.device_mesh, (Shard(seq_dim),)) 

314 

315 def forward(self, input_: torch.Tensor, weight: Optional[torch.Tensor] = None) -> torch.Tensor: 

316 """Forward using MC2 for DTensor inputs and ``nn.Linear`` otherwise.""" 

317 if not isinstance(input_, DTensor): 

318 return super().forward(input_) 

319 if not hasattr(self, "mc2_mode"): 

320 raise RuntimeError( 

321 "MC2Linear must be configured by an MC2 parallel style before use." 

322 ) 

323 

324 if weight is None: 

325 weight = self.weight 

326 if not isinstance(weight, DTensor): 

327 raise TypeError( 

328 "MC2Linear expects a DTensor weight after tensor-parallel sharding, " 

329 f"but got {type(weight).__name__}." 

330 ) 

331 return self._mc2_forward(input_, weight)