Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / mesh.py: 39%

134 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"""Mesh domains and the distributed-setup container for AutoModels. 

16 

17Split out of the former ``components/distributed/infrastructure.py`` in 

18stage 7 (05 §15.11 step 3): the mesh topology objects are AutoModels-side 

19model-construction inputs, while process-group lifecycle helpers moved to 

20``hyper_parallel.trainer.runtime.distributed`` / 

21``hyper_parallel.trainer.runtime.logging``. 

22""" 

23 

24__all__ = [ 

25 "DistributedSetup", 

26 "MeshContext", 

27] 

28 

29import inspect 

30import logging 

31import math 

32from dataclasses import dataclass, field 

33from typing import Any 

34 

35import torch.distributed as dist 

36 

37from hyper_parallel import init_device_mesh 

38from hyper_parallel.models.build_options import ( # pylint: disable=syntax-error 

39 get_device_type, 

40) 

41 

42logger = logging.getLogger(__name__) 

43 

44 

45def _init_topology_mesh(mesh_kwargs: dict[str, Any]) -> Any: 

46 """Create a mesh while preserving compatibility with lightweight test fakes.""" 

47 supported_arguments = inspect.signature(init_device_mesh).parameters 

48 filtered_kwargs = { 

49 name: value 

50 for name, value in mesh_kwargs.items() 

51 if name in supported_arguments 

52 } 

53 return init_device_mesh(**filtered_kwargs) 

54 

55 

56@dataclass 

57class MeshContext: 

58 """Runtime topology and the mesh domains consumed by dual-mode Trainer.""" 

59 device_mesh: Any = None 

60 fsdp_non_moe_mesh: Any = None 

61 fsdp_moe_mesh: Any = None 

62 dp_size: int = 1 

63 dp_replicate_size: int = 1 

64 dp_shard_size: int = 1 

65 edp_shard_size: int = 1 

66 tp_size: int = 1 

67 cp_size: int = 1 

68 pp_size: int = 1 

69 ep_size: int = 1 

70 dp_rank: int = 0 

71 tp_rank: int = 0 

72 cp_rank: int = 0 

73 pp_rank: int = 0 

74 ep_rank: int = 0 

75 sequence_parallel: bool = False 

76 loss_parallel: bool = False 

77 

78 @property 

79 def pp_enabled(self) -> bool: 

80 """Whether pipeline parallelism is enabled (pp_size > 1).""" 

81 return self.pp_size > 1 

82 

83 @property 

84 def dp_cp_mesh(self) -> Any: 

85 """DP+CP joint mesh for all-reduce. 

86 

87 Mirrors the derivation in ``FinetuneRecipe.setup()``: take the DP 

88 dimension together with CP (if present) and flatten to 1-D. 

89 """ 

90 if self.device_mesh is None: 

91 return None 

92 dim_names = self.device_mesh.mesh_dim_names or () 

93 selected_dims = tuple(dim_name for dim_name in ("dp", "cp") if dim_name in dim_names) 

94 if not selected_dims: 

95 selected_dims = tuple( 

96 dim_name 

97 for dim_name in ("dp_replicate", "dp_shard", "cp") 

98 if dim_name in dim_names 

99 ) 

100 if not selected_dims: 

101 return None 

102 sub = ( 

103 self.device_mesh[selected_dims[0]] 

104 if len(selected_dims) == 1 

105 else self.device_mesh[selected_dims] 

106 ) 

107 if sub.ndim > 1: 

108 sub = sub.flatten("dp_cp") 

109 return sub 

110 

111 @property 

112 def cp_mesh(self) -> Any: 

113 """CP submesh for CP utilities (e.g. shard_batch_for_cp). 

114 

115 Returns ``None`` when context parallelism is disabled (cp_size == 1) 

116 or no DeviceMesh is available. 

117 """ 

118 if self.device_mesh is None: 

119 return None 

120 dim_names = self.device_mesh.mesh_dim_names or () 

121 if "cp" in dim_names: 

122 return self.device_mesh["cp"] 

123 return None 

124 

125 def build_meshs(self, device_type: str, world_size: int) -> None: 

126 """Build the device, dense-FSDP and expert-FSDP mesh domains. 

127 

128 The device mesh preserves the existing (dp, cp, tp) rank order. Dense 

129 FSDP flattens the DP+CP domain and reshapes it according to the 

130 configured shard size, while expert FSDP derives an (edp, ep) 

131 topology from the flattened device domain. 

132 

133 Args: 

134 device_type: Device type accepted by init_device_mesh. 

135 world_size: Number of ranks in the process group. 

136 

137 Raises: 

138 ValueError: If the configured topology cannot cover world_size or 

139 cannot form the requested expert mesh. 

140 """ 

141 dense_world_size = self.dp_size * self.cp_size * self.tp_size 

142 expected_world_size = dense_world_size * self.pp_size 

143 if expected_world_size != world_size: 

144 raise ValueError( 

145 "dp_size * cp_size * tp_size * pp_size " 

146 f"must equal world_size ({expected_world_size} != {world_size})" 

147 ) 

148 fsdp_data_parallel_size = self.dp_size * self.cp_size 

149 configured_fsdp_size = self.dp_replicate_size * self.dp_shard_size 

150 if configured_fsdp_size != fsdp_data_parallel_size: 

151 raise ValueError( 

152 "dp_replicate_size * dp_shard_size must equal dp_size * cp_size " 

153 f"({configured_fsdp_size} != {fsdp_data_parallel_size})" 

154 ) 

155 

156 init_backend = dist.is_initialized() 

157 stage_rank_start = 0 

158 if init_backend and self.pp_size > 1: 

159 stage_rank_start = (dist.get_rank() // dense_world_size) * dense_world_size 

160 stage_rank_list = tuple( 

161 range(stage_rank_start, stage_rank_start + dense_world_size) 

162 ) 

163 device_mesh_kwargs = { 

164 "device_type": device_type, 

165 "mesh_shape": (self.dp_size, self.cp_size, self.tp_size), 

166 "mesh_dim_names": ("dp", "cp", "tp"), 

167 "init_backend": init_backend, 

168 } 

169 if init_backend: 

170 device_mesh_kwargs["rank_list"] = stage_rank_list 

171 self.device_mesh = _init_topology_mesh(device_mesh_kwargs) 

172 

173 dense_rank_list = ( 

174 tuple(self.device_mesh.rank_list) 

175 if hasattr(self.device_mesh, "rank_list") 

176 else None 

177 ) 

178 dense_mesh_kwargs = { 

179 "device_type": device_type, 

180 "mesh_shape": ( 

181 self.dp_replicate_size, 

182 self.dp_shard_size, 

183 self.tp_size, 

184 ), 

185 "mesh_dim_names": ("fsdp_replicate", "fsdp_shard", "tp"), 

186 "init_backend": init_backend, 

187 } 

188 if init_backend and dense_rank_list is not None: 

189 dense_mesh_kwargs["rank_list"] = dense_rank_list 

190 self.fsdp_non_moe_mesh = _init_topology_mesh(dense_mesh_kwargs) 

191 _validate_dense_tp_rank_layout(self.device_mesh, self.fsdp_non_moe_mesh) 

192 

193 self._build_expert_parallel_mesh(device_type, init_backend, dense_rank_list) 

194 

195 def _build_expert_parallel_mesh( 

196 self, 

197 device_type: str, 

198 init_backend: bool, 

199 dense_rank_list: tuple[int, ...] | None, 

200 ) -> None: 

201 """Build the expert-parallel FSDP mesh when expert parallelism is enabled.""" 

202 self.fsdp_moe_mesh = None 

203 if self.ep_size <= 1: 

204 return 

205 expert_domain_size = math.prod(self.device_mesh.mesh_shape) 

206 if expert_domain_size % self.ep_size != 0: 

207 raise ValueError( 

208 f"expert domain size ({expert_domain_size}) must be divisible by " 

209 f"ep_size ({self.ep_size})" 

210 ) 

211 edp_size = expert_domain_size // self.ep_size 

212 if edp_size % self.edp_shard_size != 0: 

213 raise ValueError( 

214 f"expert data-parallel size ({edp_size}) must be divisible by " 

215 f"edp_shard_size ({self.edp_shard_size})" 

216 ) 

217 edp_replicate_size = edp_size // self.edp_shard_size 

218 expert_mesh_shape = ( 

219 (edp_replicate_size, self.edp_shard_size, self.ep_size) 

220 if edp_replicate_size > 1 

221 else (self.edp_shard_size, self.ep_size) 

222 ) 

223 expert_mesh_names = ( 

224 ("edp_replicate", "edp_shard", "ep") 

225 if edp_replicate_size > 1 

226 else ("edp_shard", "ep") 

227 ) 

228 expert_mesh_kwargs = { 

229 "device_type": device_type, 

230 "mesh_shape": expert_mesh_shape, 

231 "mesh_dim_names": expert_mesh_names, 

232 "init_backend": init_backend, 

233 } 

234 if init_backend and dense_rank_list is not None: 

235 expert_mesh_kwargs["rank_list"] = dense_rank_list 

236 self.fsdp_moe_mesh = _init_topology_mesh(expert_mesh_kwargs) 

237 

238 

239def _validate_dense_tp_rank_layout(device_mesh: Any, fsdp_non_moe_mesh: Any) -> None: 

240 """Ensure both TP child meshes describe the same groups and local rank.""" 

241 if not hasattr(device_mesh, "rank_list") or not hasattr(fsdp_non_moe_mesh, "rank_list"): 

242 return 

243 device_tp_mesh = device_mesh["tp"] 

244 fsdp_tp_mesh = fsdp_non_moe_mesh["tp"] 

245 if tuple(device_tp_mesh.rank_list) != tuple(fsdp_tp_mesh.rank_list): 

246 raise ValueError("device_mesh['tp'] and fsdp_non_moe_mesh['tp'] rank groups differ") 

247 if device_tp_mesh.get_local_rank() != fsdp_tp_mesh.get_local_rank(): 

248 raise ValueError("device_mesh['tp'] and fsdp_non_moe_mesh['tp'] local ranks differ") 

249 

250 

251@dataclass 

252class DistributedSetup: 

253 """Unified distributed configuration container. 

254 

255 Following design doc 06 §3. 

256 """ 

257 mesh_context: MeshContext = field(default_factory=MeshContext) 

258 strategy_config: Any = None 

259 pipeline_config: Any = None 

260 moe_parallel_config: Any = None 

261 activation_checkpointing: Any = None 

262 # Normalized ``{match: ModuleShardingSpec}`` mapping (or None) — the 

263 # Trainer desugars YAML PlanOverride entries via 

264 # entries_to_plan_overrides (when-filtering against the built mesh 

265 # included) BEFORE the AutoModels build pipeline runs; AutoModels never 

266 # sees the raw Trainer YAML DTO list (05 §15.2.6). 

267 plan_overrides: Any = None 

268 # Normalized module replacement rules (tuple[ModuleReplacementSpec]) 

269 # desugared Trainer-side via entries_to_module_replacements. 

270 module_replacements: Any = None 

271 # TrainingConfig.training.low_precision. Kept on the setup so model 

272 # construction and sharding consume one resolved policy object. 

273 low_precision_config: Any = None 

274 fp32_main_params: bool = False 

275 

276 

277def _build_device_mesh_from_accelerator( 

278 accel: Any, 

279 dp_shard_size: int, 

280 dp_replicate_size: int, 

281 world_size: int, 

282 edp_shard_size: int = 1, 

283) -> tuple[MeshContext, tuple[str, ...]]: 

284 """Build the explicit MeshContext domains from accelerator topology.""" 

285 tp_size = max(1, accel.tp_size) 

286 cp_size = max(1, accel.cp_size) 

287 pp_size = max(1, accel.pp_size) 

288 ep_size = max(1, accel.ep_size) 

289 

290 dp_size = world_size // (tp_size * cp_size * pp_size) 

291 mesh_context = MeshContext( 

292 dp_size=dp_size, 

293 dp_replicate_size=dp_replicate_size, 

294 dp_shard_size=dp_shard_size, 

295 edp_shard_size=edp_shard_size, 

296 tp_size=tp_size, 

297 cp_size=cp_size, 

298 pp_size=pp_size, 

299 ep_size=ep_size, 

300 sequence_parallel=bool(accel.sequence_parallel), 

301 loss_parallel=bool(accel.loss_parallel), 

302 ) 

303 mesh_context.build_meshs(get_device_type(), world_size) 

304 device_mesh_shape = ( 

305 mesh_context.device_mesh.mesh_shape 

306 if hasattr(mesh_context.device_mesh, "mesh_shape") 

307 else None 

308 ) 

309 dense_shape = ( 

310 mesh_context.fsdp_non_moe_mesh.mesh_shape 

311 if hasattr(mesh_context.fsdp_non_moe_mesh, "mesh_shape") 

312 else None 

313 ) 

314 expert_shape = ( 

315 mesh_context.fsdp_moe_mesh.mesh_shape 

316 if mesh_context.fsdp_moe_mesh is not None and hasattr(mesh_context.fsdp_moe_mesh, "mesh_shape") 

317 else None 

318 ) 

319 logger.info( 

320 "Built device mesh %s, dense FSDP mesh %s and expert FSDP mesh %s", 

321 device_mesh_shape, 

322 dense_shape, 

323 expert_shape, 

324 ) 

325 return mesh_context, mesh_context.device_mesh.mesh_dim_names