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

132 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-21 04:29 +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 

48from hyper_parallel.platform import get_platform 

49 

50platform = get_platform() 

51 

52__all__ = [ 

53 "get_hcomm_info", 

54 "AllGatherMatmulFunction", 

55 "MatmulReduceScatterFunction", 

56 "MC2Linear", 

57] 

58 

59 

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

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

62 

63 Args: 

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

65 

66 Returns: 

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

68 """ 

69 rank = torch.distributed.get_rank(group) 

70 if torch.__version__ > "2.0": 

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

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

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

74 global_rank 

75 ) 

76 return group.get_hccl_comm_name(rank) 

77 

78 

79def _require_torch_npu(): 

80 try: 

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

82 except ImportError as exc: 

83 raise RuntimeError( 

84 "MC2 fused kernels require torch_npu " 

85 "(npu_all_gather_base_mm / npu_mm_reduce_scatter_base)." 

86 ) from exc 

87 return torch_npu 

88 

89 

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

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

92 seq_dim = sequence_dim 

93 if seq_dim < 0: 

94 seq_dim += ndim_leading 

95 if seq_dim < 0 or seq_dim >= ndim_leading: 

96 raise RuntimeError( 

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

98 f"for leading rank {ndim_leading}." 

99 ) 

100 return seq_dim 

101 

102 

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

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

105 if dim == 0: 

106 return tensor 

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

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

109 

110 

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

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

113 if dim == 0: 

114 return tensor 

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

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

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

118 

119 

120class AllGatherMatmulFunction(platform.Function): 

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

122 

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

124 """ 

125 

126 @staticmethod 

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

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

129 torch_npu = _require_torch_npu() 

130 hcom = get_hcomm_info(group) 

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

132 out, gathered = torch_npu.npu_all_gather_base_mm( 

133 x, 

134 w.t(), 

135 hcom, 

136 world_size, 

137 bias=None, 

138 gather_index=0, 

139 gather_output=True, 

140 ) 

141 if bias is not None: 

142 out = out + bias 

143 ctx.save_for_backward(gathered, w) 

144 ctx.group = group 

145 ctx.world_size = world_size 

146 ctx.hcom = hcom 

147 ctx.has_bias = bias is not None 

148 return out 

149 

150 @staticmethod 

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

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

153 

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

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

156 ``[256, 65535)``. 

157 """ 

158 torch_npu = _require_torch_npu() 

159 gathered, w = ctx.saved_tensors 

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

161 grad_x = torch_npu.npu_mm_reduce_scatter_base( 

162 grad_out.contiguous(), 

163 w, 

164 ctx.hcom, 

165 ctx.world_size, 

166 reduce_op="sum", 

167 bias=None, 

168 ) 

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

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

171 return grad_x, grad_w, None, None, grad_bias 

172 

173 

174class MatmulReduceScatterFunction(platform.Function): 

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

176 

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

178 """ 

179 

180 @staticmethod 

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

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

183 torch_npu = _require_torch_npu() 

184 hcom = get_hcomm_info(group) 

185 out = torch_npu.npu_mm_reduce_scatter_base( 

186 x, 

187 w.t(), 

188 hcom, 

189 world_size, 

190 reduce_op="sum", 

191 bias=None, 

192 ) 

193 if bias is not None: 

194 out = out + bias 

195 ctx.save_for_backward(x, w) 

196 ctx.group = group 

197 ctx.world_size = world_size 

198 ctx.hcom = hcom 

199 ctx.has_bias = bias is not None 

200 return out 

201 

202 @staticmethod 

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

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

205 torch_npu = _require_torch_npu() 

206 x, w = ctx.saved_tensors 

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

208 grad_x, grad_out_full = torch_npu.npu_all_gather_base_mm( 

209 grad_out, 

210 w, 

211 ctx.hcom, 

212 ctx.world_size, 

213 bias=None, 

214 gather_index=0, 

215 gather_output=True, 

216 ) 

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

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

219 return grad_x, grad_w, None, None, grad_bias 

220 

221 

222class MC2Linear(nn.Linear): 

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

224 

225 def configure_mc2( 

226 self, 

227 mode: str, 

228 group: Any, 

229 world_size: int, 

230 sequence_dim: int = 0, 

231 ) -> None: 

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

233 

234 Args: 

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

236 group (Any): TP process group. 

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

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

239 under SP. Default: ``0``. 

240 """ 

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

242 raise ValueError( 

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

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

245 ) 

246 self.mc2_mode = mode 

247 self.mc2_group = group 

248 self.mc2_world_size = world_size 

249 self.mc2_sequence_dim = sequence_dim 

250 

251 @classmethod 

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

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

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

255 raise TypeError( 

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

257 ) 

258 linear.__class__ = cls 

259 return linear 

260 

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

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

263 

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

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

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

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

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

269 """ 

270 input_local = input_.to_local() 

271 weight_local = weight.to_local() 

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

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

274 

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

276 x_seq_first = _move_dim_to_front(input_local, seq_dim) 

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

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

279 

280 bias_local = None 

281 if self.bias is not None: 

282 bias = self.bias 

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

284 

285 if self.mc2_mode == "all_gather": 

286 output_2d = AllGatherMatmulFunction.apply( 

287 input_2d, 

288 weight_local, 

289 self.mc2_group, 

290 self.mc2_world_size, 

291 bias_local, 

292 ) 

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

294 seq_global = leading_global[seq_dim] 

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

296 output = _move_front_to_dim(output, seq_dim) 

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

298 

299 seq_global = leading_global[seq_dim] 

300 if seq_global % self.mc2_world_size != 0: 

301 raise RuntimeError( 

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

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

304 f"{self.mc2_world_size}." 

305 ) 

306 output_2d = MatmulReduceScatterFunction.apply( 

307 input_2d, 

308 weight_local, 

309 self.mc2_group, 

310 self.mc2_world_size, 

311 bias_local, 

312 ) 

313 seq_local = seq_global // self.mc2_world_size 

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

315 output = _move_front_to_dim(output, seq_dim) 

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

317 

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

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

320 if not isinstance(input_, DTensor): 

321 return super().forward(input_) 

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

323 raise RuntimeError( 

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

325 ) 

326 

327 if weight is None: 

328 weight = self.weight 

329 if not isinstance(weight, DTensor): 

330 raise TypeError( 

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

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

333 ) 

334 return self._mc2_forward(input_, weight)