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

193 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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 

24from __future__ import annotations 

25 

26__all__ = [ 

27 "DistributedSetup", 

28 "LogicalDeviceMesh", 

29 "MeshContext", 

30 "ParallelLayout", 

31 "ParallelTopologySpec", 

32 "infer_minimum_world_size", 

33 "resolve_parallel_layout", 

34] 

35 

36import inspect 

37import math 

38from dataclasses import dataclass, field 

39from typing import Any, Optional, Tuple, Union 

40 

41import torch.distributed as dist 

42 

43from hyper_parallel import init_device_mesh 

44 

45 

46def _compatible_parallel_degree(value: Any) -> Any: 

47 """Preserve the Trainer's historical minimum parallel degree of one.""" 

48 return max(1, value) 

49 

50 

51def _positive_world_size(value: Any) -> int: 

52 """Validate the process-group or inferred world size.""" 

53 if isinstance(value, bool) or not isinstance(value, int) or value < 1: 

54 raise ValueError(f"world_size must be a positive integer, got {value!r}") 

55 return value 

56 

57 

58@dataclass(frozen=True) 

59class ParallelTopologySpec: 

60 """Normalized parallel degrees shared by runtime and offline planning.""" 

61 

62 tp_size: int 

63 cp_size: int 

64 pp_size: int 

65 ep_size: int 

66 dp_shard_size: int 

67 edp_shard_size: int 

68 sequence_parallel: bool 

69 loss_parallel: bool 

70 

71 @classmethod 

72 def from_config(cls, config: Any) -> "ParallelTopologySpec": 

73 """Read parallel degrees using the Trainer's compatibility semantics.""" 

74 accelerator = config.accelerator 

75 fsdp_config = config.fsdp_config 

76 return cls( 

77 tp_size=_compatible_parallel_degree(accelerator.tp_size), 

78 cp_size=_compatible_parallel_degree(accelerator.cp_size), 

79 pp_size=_compatible_parallel_degree(accelerator.pp_size), 

80 ep_size=_compatible_parallel_degree(accelerator.ep_size), 

81 dp_shard_size=_compatible_parallel_degree(fsdp_config.dp_shard_size), 

82 edp_shard_size=_compatible_parallel_degree(fsdp_config.edp_shard_size), 

83 sequence_parallel=bool(accelerator.sequence_parallel), 

84 loss_parallel=bool(accelerator.loss_parallel), 

85 ) 

86 

87 

88@dataclass(frozen=True) 

89class ParallelLayout: 

90 """Resolved parallel topology independent of devices and process groups.""" 

91 

92 spec: ParallelTopologySpec 

93 world_size: int 

94 dp_size: int 

95 dp_replicate_size: int 

96 

97 @property 

98 def dense_world_size(self) -> int: 

99 """Return the rank count in one pipeline stage.""" 

100 return self.dp_size * self.spec.cp_size * self.spec.tp_size 

101 

102 

103def resolve_parallel_layout( 

104 spec: ParallelTopologySpec, world_size: int 

105) -> ParallelLayout: 

106 """Validate a world size and resolve its DP/FSDP/EP layout.""" 

107 world_size = _positive_world_size(world_size) 

108 non_dp_size = spec.tp_size * spec.cp_size * spec.pp_size 

109 if world_size % non_dp_size != 0: 

110 raise ValueError( 

111 f"world_size {world_size} is not divisible by non-DP size " 

112 f"{non_dp_size}" 

113 ) 

114 dp_size = world_size // non_dp_size 

115 fsdp_data_parallel_size = dp_size * spec.cp_size 

116 if fsdp_data_parallel_size % spec.dp_shard_size != 0: 

117 raise ValueError( 

118 "DP+CP size " 

119 f"{fsdp_data_parallel_size} is not divisible by FSDP shard size " 

120 f"{spec.dp_shard_size}" 

121 ) 

122 dense_world_size = dp_size * spec.cp_size * spec.tp_size 

123 if dense_world_size % spec.ep_size != 0: 

124 raise ValueError( 

125 f"expert domain size ({dense_world_size}) must be divisible by " 

126 f"ep_size ({spec.ep_size})" 

127 ) 

128 expert_data_parallel_size = dense_world_size // spec.ep_size 

129 if expert_data_parallel_size % spec.edp_shard_size != 0: 

130 raise ValueError( 

131 f"expert data-parallel size ({expert_data_parallel_size}) must be " 

132 f"divisible by edp_shard_size ({spec.edp_shard_size})" 

133 ) 

134 return ParallelLayout( 

135 spec=spec, 

136 world_size=world_size, 

137 dp_size=dp_size, 

138 dp_replicate_size=fsdp_data_parallel_size // spec.dp_shard_size, 

139 ) 

140 

141 

142def infer_minimum_world_size(spec: ParallelTopologySpec) -> int: 

143 """Return the smallest world size satisfying every parallel contract.""" 

144 dense_world_size = math.lcm( 

145 spec.tp_size * spec.cp_size, 

146 spec.ep_size, 

147 spec.tp_size * spec.dp_shard_size, 

148 spec.ep_size * spec.edp_shard_size, 

149 ) 

150 return dense_world_size * spec.pp_size 

151 

152 

153@dataclass(frozen=True) 

154class LogicalDeviceMesh: 

155 """Process-group-free subset of the DeviceMesh planning interface.""" 

156 

157 mesh_dim_names: tuple[str, ...] 

158 mesh_shape: tuple[int, ...] 

159 

160 @property 

161 def shape(self) -> tuple[int, ...]: 

162 """Return the logical mesh shape.""" 

163 return self.mesh_shape 

164 

165 def __getitem__( 

166 self, mesh_dim_names: Union[str, Tuple[str, ...]] 

167 ) -> "LogicalDeviceMesh": 

168 """Return a logical submesh selected by dimension name.""" 

169 names = ( 

170 (mesh_dim_names,) 

171 if isinstance(mesh_dim_names, str) 

172 else tuple(mesh_dim_names) 

173 ) 

174 missing = tuple(name for name in names if name not in self.mesh_dim_names) 

175 if missing: 

176 raise KeyError(f"logical mesh has no dimensions {missing!r}") 

177 shape_by_name = dict(zip(self.mesh_dim_names, self.mesh_shape)) 

178 return LogicalDeviceMesh( 

179 mesh_dim_names=names, 

180 mesh_shape=tuple(shape_by_name[name] for name in names), 

181 ) 

182 

183 def size(self, mesh_dim: Optional[Union[int, str]] = None) -> int: 

184 """Return the total or selected logical dimension size.""" 

185 if mesh_dim is None: 

186 return math.prod(self.mesh_shape) 

187 index = ( 

188 self.mesh_dim_names.index(mesh_dim) 

189 if isinstance(mesh_dim, str) 

190 else mesh_dim 

191 ) 

192 return self.mesh_shape[index] 

193 

194 

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

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

197 supported_arguments = inspect.signature(init_device_mesh).parameters 

198 filtered_kwargs = { 

199 name: value 

200 for name, value in mesh_kwargs.items() 

201 if name in supported_arguments 

202 } 

203 return init_device_mesh(**filtered_kwargs) 

204 

205 

206@dataclass 

207class MeshContext: 

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

209 device_mesh: Any = None 

210 fsdp_non_moe_mesh: Any = None 

211 fsdp_moe_mesh: Any = None 

212 dp_size: int = 1 

213 dp_replicate_size: int = 1 

214 dp_shard_size: int = 1 

215 edp_shard_size: int = 1 

216 tp_size: int = 1 

217 cp_size: int = 1 

218 pp_size: int = 1 

219 ep_size: int = 1 

220 dp_rank: int = 0 

221 tp_rank: int = 0 

222 cp_rank: int = 0 

223 pp_rank: int = 0 

224 ep_rank: int = 0 

225 sequence_parallel: bool = False 

226 loss_parallel: bool = False 

227 

228 @classmethod 

229 def from_layout(cls, layout: ParallelLayout) -> "MeshContext": 

230 """Create common mesh metadata from a resolved parallel layout.""" 

231 spec = layout.spec 

232 return cls( 

233 dp_size=layout.dp_size, 

234 dp_replicate_size=layout.dp_replicate_size, 

235 dp_shard_size=spec.dp_shard_size, 

236 edp_shard_size=spec.edp_shard_size, 

237 tp_size=spec.tp_size, 

238 cp_size=spec.cp_size, 

239 pp_size=spec.pp_size, 

240 ep_size=spec.ep_size, 

241 sequence_parallel=spec.sequence_parallel, 

242 loss_parallel=spec.loss_parallel, 

243 ) 

244 

245 @property 

246 def pp_enabled(self) -> bool: 

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

248 return self.pp_size > 1 

249 

250 @property 

251 def dp_cp_mesh(self) -> Any: 

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

253 

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

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

256 """ 

257 if self.device_mesh is None: 

258 return None 

259 dim_names = self.device_mesh.mesh_dim_names or () 

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

261 if not selected_dims: 

262 selected_dims = tuple( 

263 dim_name 

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

265 if dim_name in dim_names 

266 ) 

267 if not selected_dims: 

268 return None 

269 sub = ( 

270 self.device_mesh[selected_dims[0]] 

271 if len(selected_dims) == 1 

272 else self.device_mesh[selected_dims] 

273 ) 

274 if sub.ndim > 1: 

275 sub = sub.flatten("dp_cp") 

276 return sub 

277 

278 @property 

279 def cp_mesh(self) -> Any: 

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

281 

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

283 or no DeviceMesh is available. 

284 """ 

285 if self.device_mesh is None: 

286 return None 

287 dim_names = self.device_mesh.mesh_dim_names or () 

288 if "cp" in dim_names: 

289 return self.device_mesh["cp"] 

290 return None 

291 

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

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

294 

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

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

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

298 topology from the flattened device domain. 

299 

300 Args: 

301 device_type: Device type accepted by init_device_mesh. 

302 world_size: Number of ranks in the process group. 

303 

304 Raises: 

305 ValueError: If the configured topology cannot cover world_size or 

306 cannot form the requested expert mesh. 

307 """ 

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

309 expected_world_size = dense_world_size * self.pp_size 

310 if expected_world_size != world_size: 

311 raise ValueError( 

312 "dp_size * cp_size * tp_size * pp_size " 

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

314 ) 

315 fsdp_data_parallel_size = self.dp_size * self.cp_size 

316 configured_fsdp_size = self.dp_replicate_size * self.dp_shard_size 

317 if configured_fsdp_size != fsdp_data_parallel_size: 

318 raise ValueError( 

319 "dp_replicate_size * dp_shard_size must equal dp_size * cp_size " 

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

321 ) 

322 

323 init_backend = dist.is_initialized() 

324 stage_rank_start = 0 

325 if init_backend and self.pp_size > 1: 

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

327 stage_rank_list = tuple( 

328 range(stage_rank_start, stage_rank_start + dense_world_size) 

329 ) 

330 device_mesh_kwargs = { 

331 "device_type": device_type, 

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

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

334 "init_backend": init_backend, 

335 } 

336 if init_backend: 

337 device_mesh_kwargs["rank_list"] = stage_rank_list 

338 self.device_mesh = _init_topology_mesh(device_mesh_kwargs) 

339 

340 dense_rank_list = ( 

341 tuple(self.device_mesh.rank_list) 

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

343 else None 

344 ) 

345 dense_mesh_kwargs = { 

346 "device_type": device_type, 

347 "mesh_shape": ( 

348 self.dp_replicate_size, 

349 self.dp_shard_size, 

350 self.tp_size, 

351 ), 

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

353 "init_backend": init_backend, 

354 } 

355 if init_backend and dense_rank_list is not None: 

356 dense_mesh_kwargs["rank_list"] = dense_rank_list 

357 self.fsdp_non_moe_mesh = _init_topology_mesh(dense_mesh_kwargs) 

358 _validate_dense_tp_rank_layout(self.device_mesh, self.fsdp_non_moe_mesh) 

359 

360 self._build_expert_parallel_mesh(device_type, init_backend, dense_rank_list) 

361 

362 def _build_expert_parallel_mesh( 

363 self, 

364 device_type: str, 

365 init_backend: bool, 

366 dense_rank_list: Optional[Tuple[int, ...]], 

367 ) -> None: 

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

369 self.fsdp_moe_mesh = None 

370 if self.ep_size <= 1: 

371 return 

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

373 if expert_domain_size % self.ep_size != 0: 

374 raise ValueError( 

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

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

377 ) 

378 edp_size = expert_domain_size // self.ep_size 

379 if edp_size % self.edp_shard_size != 0: 

380 raise ValueError( 

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

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

383 ) 

384 edp_replicate_size = edp_size // self.edp_shard_size 

385 expert_mesh_shape = ( 

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

387 if edp_replicate_size > 1 

388 else (self.edp_shard_size, self.ep_size) 

389 ) 

390 expert_mesh_names = ( 

391 ("edp_replicate", "edp_shard", "ep") 

392 if edp_replicate_size > 1 

393 else ("edp_shard", "ep") 

394 ) 

395 expert_mesh_kwargs = { 

396 "device_type": device_type, 

397 "mesh_shape": expert_mesh_shape, 

398 "mesh_dim_names": expert_mesh_names, 

399 "init_backend": init_backend, 

400 } 

401 if init_backend and dense_rank_list is not None: 

402 expert_mesh_kwargs["rank_list"] = dense_rank_list 

403 self.fsdp_moe_mesh = _init_topology_mesh(expert_mesh_kwargs) 

404 

405 

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

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

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

409 return 

410 device_tp_mesh = device_mesh["tp"] 

411 fsdp_tp_mesh = fsdp_non_moe_mesh["tp"] 

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

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

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

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

416 

417 

418@dataclass 

419class DistributedSetup: 

420 """Unified distributed configuration container. 

421 

422 Following design doc 06 §3. 

423 """ 

424 mesh_context: MeshContext = field(default_factory=MeshContext) 

425 strategy_config: Any = None 

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

427 # Trainer desugars YAML PlanOverride entries via 

428 # entries_to_plan_overrides (when-filtering against the built mesh 

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

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

431 plan_overrides: Any = None 

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

433 # desugared Trainer-side via entries_to_module_replacements. 

434 module_replacements: Any = None 

435 module_replacement_requests: Any = None 

436 # Non-forward method patches installed after module replacement and before planning. 

437 method_replacements: Any = None 

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

439 # construction and sharding consume one resolved policy object. 

440 low_precision_config: Any = None 

441 fp32_main_params: bool = False 

442 

443 @classmethod 

444 def from_config( 

445 cls, config: Any, mesh_context: MeshContext, *, strategy_enabled: bool 

446 ) -> "DistributedSetup": 

447 """Assemble model-build configuration independently of mesh execution. 

448 

449 Args: 

450 config: Resolved training configuration. 

451 mesh_context: Topology prepared by the caller. 

452 strategy_enabled: Whether to retain the configured FSDP strategy. 

453 

454 Returns: 

455 Setup with common model-build policies populated. 

456 """ 

457 return cls( 

458 mesh_context=mesh_context, 

459 strategy_config=config.fsdp_config if strategy_enabled else None, 

460 low_precision_config=getattr( 

461 getattr(config, "training", None), "low_precision", None 

462 ), 

463 fp32_main_params=config.optimizer.fp32_main_params, 

464 )