Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / fully_shard / api.py: 78%

332 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"""hybrid shard data parallel interface""" 

16from collections import namedtuple 

17from typing import Any, List, Mapping, cast, Optional, Union 

18 

19import torch 

20import torch.distributed as dist 

21from torch import Tensor, nn 

22 

23from hyper_parallel.core.fully_shard.utils import ( 

24 MixedPrecisionPolicy, 

25 OffloadPolicy, 

26 SourceShardMetaInfo, 

27 load_into_param, 

28 parameters_dict, 

29 wait_grad_handle, 

30) 

31from hyper_parallel.core.utils.communication import get_device_handle 

32from hyper_parallel.core.fully_shard.hsdp_scheduler import HSDPSchedulerV2 

33from hyper_parallel.core.fully_shard.state_dict_utils import ( 

34 get_model_state_dict as get_model_state_dict_impl, 

35 set_model_state_dict as set_model_state_dict_impl, 

36) 

37from hyper_parallel import DeviceMesh, init_device_mesh 

38from hyper_parallel.core.dtensor.dtensor import DTensor, distribute_tensor 

39from hyper_parallel.core.fully_shard.hsdp_utils import ( 

40 get_managed_modules_parameters, 

41) 

42 

43origin_class_to_extend_class = {} 

44 

45 

46def _resolve_comm_fusion_zero_copy_default( 

47 comm_fusion: bool, 

48 comm_fusion_zero_copy: Optional[bool], 

49) -> bool: 

50 """Resolve the default for the comm_fusion zero-copy path.""" 

51 if comm_fusion_zero_copy is not None: 

52 return comm_fusion_zero_copy 

53 return comm_fusion 

54 

55 

56def _check_strict_keys( 

57 module: nn.Module, state_dict: Mapping[str, Any], 

58) -> None: 

59 """Raise ``RuntimeError`` if *state_dict* keys do not match *module*.""" 

60 expected_keys = set(module.state_dict().keys()) 

61 missing = expected_keys - set(state_dict.keys()) 

62 unexpected = set(state_dict.keys()) - expected_keys 

63 error_msgs: list[str] = [] 

64 if missing: 

65 error_msgs.append( 

66 "Missing key(s): " + ", ".join(repr(k) for k in sorted(missing)) 

67 ) 

68 if unexpected: 

69 error_msgs.append( 

70 "Unexpected key(s): " + ", ".join(repr(k) for k in sorted(unexpected)) 

71 ) 

72 if error_msgs: 

73 raise RuntimeError( 

74 f"Error(s) in loading state_dict for " 

75 f"{module.__class__.__name__}:\n\t" 

76 + "\n\t".join(error_msgs) 

77 ) 

78 

79 

80def _resolve_local_tensor( 

81 key: str, val: Tensor, target: DTensor, 

82) -> Tensor: 

83 """Return the local shard tensor to be loaded into *target*.""" 

84 if isinstance(val, DTensor): 

85 return val.to_local() 

86 local_shape = tuple(target.local_shape) 

87 global_shape = tuple(target.shape) 

88 val_shape = tuple(val.shape) 

89 if val_shape == local_shape: 

90 return val 

91 if val_shape == global_shape: 

92 wrapped = distribute_tensor( 

93 val, target.device_mesh, 

94 target.layout.alias_placements if target.layout else target.placements, 

95 ) 

96 return wrapped.to_local() 

97 

98 raise ValueError( 

99 f"load '{key}': plain tensor shape {val_shape} " 

100 f"matches neither local shard {local_shape} " 

101 f"nor global {global_shape}." 

102 ) 

103 

104 

105class _UnshardHandle: 

106 """Unshard handle for user call HSDPModule.unshard(async_op=True)""" 

107 def __init__(self, hsdp_state=None): 

108 """ 

109 Initialize an async unshard handle. 

110 

111 Args: 

112 hsdp_state (HSDPState, optional): The state to wait on. None means a no-op handle. 

113 """ 

114 self._hsdp_state = hsdp_state 

115 

116 def wait(self): 

117 """Block until the async unshard operation completes.""" 

118 if self._hsdp_state is not None: 

119 self._hsdp_state.wait_for_unshard() 

120 self._hsdp_state = None 

121 

122 

123class HSDPModule: 

124 """ 

125 The hsdp block of neural networks with hsdp interface. 

126 """ 

127 

128 def __init__(self): 

129 """Initialize HSDPModule.""" 

130 self.hsdp_scheduler = None # Initialized in hsdp_init() 

131 

132 def _apply(self, *args: Any, **kwargs: Any) -> Any: 

133 """Apply a tensor conversion while preserving HSDP parameter storage. 

134 

135 ``nn.Module._apply`` can replace the DTensor local storage while an 

136 unsharded parameter buffer is still materialized on the device. HSDP 

137 must reshard before delegating to the conversion and rebuild its 

138 sharded-parameter bookkeeping afterwards. The bookkeeping reset is a 

139 storage operation and must not become part of an autograd graph. 

140 

141 The module's original class is added after HSDPModule in the runtime 

142 MRO, so static analysis cannot resolve the delegated ``_apply``. 

143 """ 

144 if not hasattr(self, "hsdp_scheduler") or self.hsdp_scheduler is None: 

145 return super()._apply(*args, **kwargs) # pylint: disable=no-member 

146 self.reshard() 

147 result = super()._apply(*args, **kwargs) # pylint: disable=no-member 

148 hsdp_state = self.hsdp_scheduler.hsdp_state 

149 if hsdp_state is None: 

150 return result 

151 with torch.no_grad(): 

152 for hsdp_param in hsdp_state.hsdp_params: 

153 hsdp_param.reset_sharded_param() 

154 if hsdp_state.param_group is not None: 

155 for bucket in hsdp_state.param_group.all_gather_buckets: 

156 # Conversion rebinds shards; an obsolete zero-copy buffer 

157 # must not keep the previous device allocation resident. 

158 if not bucket.is_flat_buffer_valid(): 

159 bucket.flat_param_buffer = None 

160 return result 

161 

162 def hsdp_init(self, module, mesh, reshard_after_forward, 

163 shard_placement_fn, mp_policy, offload_policy, ignored_params, replicate_params, device, 

164 comm_fusion, comm_fusion_zero_copy: Optional[bool] = None, 

165 source_shard_infos: Optional[Mapping[nn.Parameter, SourceShardMetaInfo]] = None): 

166 """init hsdp2 scheduler.""" 

167 resolved_comm_fusion_zero_copy = _resolve_comm_fusion_zero_copy_default( 

168 comm_fusion, 

169 comm_fusion_zero_copy, 

170 ) 

171 

172 self.hsdp_scheduler = HSDPSchedulerV2(module, 

173 mesh, 

174 reshard_after_forward, 

175 shard_placement_fn, 

176 mp_policy, 

177 offload_policy, 

178 ignored_params, 

179 replicate_params, 

180 device, 

181 comm_fusion, 

182 resolved_comm_fusion_zero_copy, 

183 source_shard_infos=source_shard_infos, 

184 ) 

185 

186 def set_requires_gradient_sync(self, requires_grad_sync: bool, *, recurse: bool = True) -> None: 

187 """Set gradient synchronization for this module and optionally its descendants.""" 

188 if not isinstance(requires_grad_sync, bool): 

189 raise ValueError("requires_grad_sync must be bool") 

190 if not isinstance(recurse, bool): 

191 raise ValueError("recurse must be bool") 

192 self_module = cast(nn.Module, self) 

193 for _, module in self_module.named_modules() if recurse else (("", self),): 

194 if isinstance(module, HSDPModule): 

195 module.hsdp_scheduler.set_requires_grad_sync(requires_grad_sync) 

196 

197 def zero_grad(self, set_to_none: bool = True) -> None: 

198 """Clear parameter gradients according to set_to_none.""" 

199 super().zero_grad(set_to_none=set_to_none) 

200 

201 def set_modules_to_forward_prefetch(self, modules): 

202 """set forward prefetch module list to prefetch all gather for unsharded parameters""" 

203 if not isinstance(modules, (tuple, list)): 

204 raise ValueError("modules must be HSDPModule list") 

205 for module in modules: 

206 if not isinstance(module, HSDPModule): 

207 raise ValueError(f"modules must be HSDPModule list but got {type(module)} in list.") 

208 if not hasattr(self, "hsdp_scheduler"): 

209 raise ValueError("call hsdp interface first.") 

210 self.hsdp_scheduler.set_forward_prefetch_cells(modules) 

211 

212 def set_modules_to_backward_prefetch(self, modules): 

213 """set backward prefetch module list to prefetch all gather for unsharded parameters""" 

214 if not isinstance(modules, (tuple, list)): 

215 raise ValueError("modules must be HSDPModule list") 

216 for module in modules: 

217 if not isinstance(module, HSDPModule): 

218 raise ValueError(f"modules must be HSDPModule list but got {type(module)} in list.") 

219 if not hasattr(self, "hsdp_scheduler"): 

220 raise ValueError("call fully_shard interface first.") 

221 self.hsdp_scheduler.set_backward_prefetch_cells(modules) 

222 

223 def reshard(self) -> None: 

224 """reshard all sharded parameters""" 

225 if not self.hsdp_scheduler: 

226 raise ValueError("hsdp_scheduler is None") 

227 hsdp_state = self.hsdp_scheduler.hsdp_state 

228 if hsdp_state: 

229 hsdp_state.shard() 

230 

231 def unshard(self, async_op: bool = False): 

232 """unshard all sharded parameters""" 

233 if not isinstance(async_op, bool): 

234 raise ValueError(f"async_op should be a bool, got {type(async_op)}") 

235 if not self.hsdp_scheduler: 

236 raise ValueError("hsdp_scheduler is None") 

237 hsdp_state = self.hsdp_scheduler.hsdp_state 

238 if hsdp_state: 

239 hsdp_state.unshard(async_op) # pylint: disable=too-many-function-args 

240 if async_op: 

241 return _UnshardHandle(hsdp_state=hsdp_state) 

242 return None 

243 

244 def load_state_dict( 

245 self, 

246 state_dict: Mapping[str, Any], 

247 strict: bool = True, 

248 assign: bool = False, 

249 ): 

250 """ 

251 Load state dict by copying directly into local shards. 

252 

253 Bypasses ``super().load_state_dict()`` because the standard PyTorch 

254 implementation triggers ``copy_`` through the DTensor dispatcher, which 

255 is not registered in the hyper-parallel layout system. 

256 

257 Each value in ``state_dict`` is dispatched by type: 

258 - hyper DTensor: extract local shard and copy directly. 

259 - plain Tensor whose shape == local shard shape: copy as-is. 

260 - plain Tensor whose shape == global shape: distribute via 

261 ``distribute_tensor``, then copy the local shard. 

262 

263 Args: 

264 state_dict (Mapping[str, Any]): Fully-qualified parameter/buffer 

265 names mapped to tensors (DTensor or plain Tensor). 

266 strict (bool): If ``True`` (default), missing or unexpected keys 

267 raise ``RuntimeError``, matching ``nn.Module.load_state_dict`` 

268 semantics. 

269 assign (bool): When ``True`` *and* every value in ``state_dict`` is 

270 already a hyper DTensor, defer to the standard 

271 ``nn.Module.load_state_dict(assign=True)``, which replaces the 

272 module's parameters/buffers with the given DTensors instead of 

273 copying into existing storage. This is required when loading 

274 sharded DTensors onto a meta-device model (e.g. 

275 ``cpu_ram_efficient_loading``). If ``state_dict`` contains any 

276 plain tensor (local-shard or global shape), ``assign`` is 

277 ignored and the copy/distribute path below is used so the 

278 target stays a properly sharded DTensor. 

279 

280 Raises: 

281 RuntimeError: When ``strict`` is ``True`` and keys do not match. 

282 ValueError: When a plain tensor shape matches neither the local 

283 shard shape nor the global shape of the target DTensor. 

284 """ 

285 if assign and state_dict and all( 

286 isinstance(val, DTensor) for val in state_dict.values() 

287 ): 

288 return super().load_state_dict(state_dict, strict=strict, assign=True) 

289 self_module = cast(nn.Module, self) 

290 

291 target_map: dict[str, Tensor] = {} 

292 for name, p in parameters_dict(self_module): 

293 target_map[name] = p 

294 for name, b in self_module.named_buffers(): 

295 target_map[name] = b 

296 

297 if strict: 

298 _check_strict_keys(self_module, state_dict) 

299 

300 with torch.no_grad(): 

301 for key, val in state_dict.items(): 

302 target = target_map.get(key) 

303 if target is None: 

304 continue 

305 

306 if isinstance(target, DTensor): 

307 val = _resolve_local_tensor(key, val, target) 

308 load_into_param(target, val) 

309 

310 # Trigger load_state_dict post-hooks so that HSDP internal 

311 # bookkeeping (e.g. _sharded_param_data) stays in sync. 

312 # Pass an IncompatibleKeys with the same attribute names as PyTorch 

313 # so external hooks can safely read .missing_keys/.unexpected_keys. 

314 _IK = namedtuple("IncompatibleKeys", ["missing_keys", "unexpected_keys"]) 

315 incompatible_keys = _IK([], []) 

316 for _, module in self_module.named_modules(): 

317 hooks = module._load_state_dict_post_hooks # pylint: disable=protected-access 

318 for hook in hooks.values(): 

319 hook(module, incompatible_keys) 

320 

321 def set_is_last_backward(self, is_last_backward: bool): 

322 """set is_last_backward flag""" 

323 self.hsdp_scheduler.scheduler_ctx.is_last_backward = is_last_backward 

324 

325 def reset_iter_state(self, recursive: bool = True) -> None: 

326 """Reset fully_shard iteration bookkeeping without clearing optimizer gradients. 

327 

328 This method must be called only after communication for the iteration has 

329 completed. It does not wait in-flight collectives or clear 

330 ``sharded_param.grad``/``main_grad``. 

331 

332 Args: 

333 recursive: Whether to reset every HSDPModule in this module tree. 

334 

335 Raises: 

336 ValueError: If ``recursive`` is not a bool. 

337 """ 

338 if not isinstance(recursive, bool): 

339 raise ValueError(f"recursive should be a bool, got {type(recursive)}") 

340 self_module = cast(nn.Module, self) 

341 modules = ( 

342 [module for _, module in self_module.named_modules()] 

343 if recursive 

344 else [self_module] 

345 ) 

346 for module in modules: 

347 if isinstance(module, HSDPModule): 

348 module.hsdp_scheduler.reset_iter_state() 

349 

350 def set_requires_all_reduce(self, requires_all_reduce: bool, *, recurse: bool = True) -> None: 

351 """Set all-reduce for this module and optionally its descendants.""" 

352 if not isinstance(requires_all_reduce, bool): 

353 raise ValueError("requires_all_reduce must be bool") 

354 if not isinstance(recurse, bool): 

355 raise ValueError("recurse must be bool") 

356 self_module = cast(nn.Module, self) 

357 for _, module in self_module.named_modules() if recurse else (("", self),): 

358 if isinstance(module, HSDPModule): 

359 module.hsdp_scheduler.set_requires_all_reduce(requires_all_reduce) 

360 

361 def set_reshard_after_forward(self, reshard_after_forward: bool, recurse: bool = True) -> None: 

362 """Set forward resharding for this module and optionally its descendants.""" 

363 if not isinstance(reshard_after_forward, bool): 

364 raise ValueError("reshard_after_forward must be bool") 

365 if not isinstance(recurse, bool): 

366 raise ValueError("recurse must be bool") 

367 self_module = cast(nn.Module, self) 

368 for _, module in self_module.named_modules() if recurse else (("", self),): 

369 if isinstance(module, HSDPModule): 

370 module.hsdp_scheduler.set_reshard_after_forward(reshard_after_forward) 

371 

372 def set_reshard_after_backward(self, reshard_after_backward: bool, recurse: bool = True) -> None: 

373 """Set backward resharding for this module and optionally its descendants.""" 

374 if not isinstance(reshard_after_backward, bool): 

375 raise ValueError("reshard_after_backward must be bool") 

376 if not isinstance(recurse, bool): 

377 raise ValueError("recurse must be bool") 

378 self_module = cast(nn.Module, self) 

379 for _, module in self_module.named_modules() if recurse else (("", self),): 

380 if isinstance(module, HSDPModule): 

381 module.hsdp_scheduler.set_reshard_after_backward(reshard_after_backward) 

382 

383 def set_reduce_op_type(self, reduce_op_type, recurse: bool = True) -> None: 

384 """ 

385 set reduce_op_type for all reduce operations in HSDP 

386 support reduce_op_type "avg" and "sum", default is "avg" 

387 """ 

388 self_module = cast(nn.Module, self) 

389 if recurse: 

390 sub_modules = [m for _, m in self_module.named_modules()] 

391 else: 

392 sub_modules = [self_module] 

393 for module in sub_modules: 

394 if isinstance(module, HSDPModule): 

395 hsdp_state = module.hsdp_scheduler.hsdp_state 

396 if hsdp_state: 

397 hsdp_state.set_reduce_op_type(reduce_op_type) 

398 

399 def set_gradient_scaling_factor(self, factor=None): 

400 """ 

401 Set a multiplicative scaling factor applied to gradients after 

402 reduce-scatter / all-reduce and before they are written into 

403 ``sharded_param.grad``. 

404 

405 ``factor`` may be ``None`` (disable scaling), a Python ``float``/``int``, 

406 or a 0-dim/1-element tensor. Setting ``factor`` to ``None`` (the default 

407 on construction) skips the scaling op entirely so no extra device-side 

408 ``mul_`` is launched on the hot path. 

409 

410 Args: 

411 factor (None | float | int | torch.Tensor): Scaling coefficient. 

412 Use ``None`` to disable scaling. 

413 

414 Raises: 

415 ValueError: If ``factor`` is not one of the supported types or is a 

416 tensor with more than one element. 

417 """ 

418 if factor is not None: 

419 if isinstance(factor, bool): 

420 raise ValueError( 

421 f"gradient_scaling_factor must be None, float, int or a 1-element Tensor, " 

422 f"but got bool {factor}." 

423 ) 

424 if isinstance(factor, Tensor): 

425 if factor.numel() != 1: 

426 raise ValueError( 

427 f"gradient_scaling_factor tensor must have exactly 1 element, " 

428 f"but got shape {tuple(factor.shape)}." 

429 ) 

430 elif not isinstance(factor, (float, int)): 

431 raise ValueError( 

432 f"gradient_scaling_factor must be None, float, int or a 1-element Tensor, " 

433 f"but got {type(factor).__name__}." 

434 ) 

435 hsdp_state = self.hsdp_scheduler.hsdp_state 

436 if hsdp_state: 

437 hsdp_state.set_gradient_scaling_factor(factor) 

438 

439 

440def _extend_module_with_hsdp_interface(module): 

441 """Dynamically extend module's class to inherit from HSDPModule, adding HSDP capabilities.""" 

442 origin_class = module.__class__ 

443 extend_class = origin_class_to_extend_class.get(origin_class, None) 

444 if extend_class is None: 

445 extend_class = type(f"HSDP{origin_class.__name__}", (HSDPModule, origin_class), {}) 

446 origin_class_to_extend_class[origin_class] = extend_class 

447 module.__class__ = extend_class 

448 

449 

450def _get_root_modules(modules: List[nn.Module]) -> List[nn.Module]: 

451 """ 

452 Returns the modules in ``modules`` that are root modules (i.e. parent-less) 

453 with respect to the set ``modules``. In other words, these are the modules 

454 in ``modules`` that are not the child of any other module in ``modules``. 

455 

456 Aligned with PyTorch torch.distributed.utils._get_root_modules. 

457 """ 

458 root_modules: List[nn.Module] = [] 

459 

460 def _get_submodules(mod): 

461 return set(mod.modules()) 

462 

463 module_to_modules: dict[nn.Module, set] = { 

464 m: _get_submodules(m) for m in modules 

465 } 

466 for candidate in modules: 

467 is_root = True 

468 for mod, submodules in module_to_modules.items(): 

469 if candidate is not mod and candidate in submodules: 

470 is_root = False 

471 break 

472 if is_root: 

473 root_modules.append(candidate) 

474 return root_modules 

475 

476 

477def _check_module_valid(module): 

478 """check module valid""" 

479 if not isinstance(module, nn.Module): 

480 raise ValueError(f"module's type must be nn.Module but got {type(module)}.") 

481 

482 

483def _validate_module_for_fully_shard( 

484 module: Union[nn.Module, List[nn.Module]] 

485) -> None: 

486 """Validate module(s) for fully_shard.""" 

487 if isinstance(module, list): 

488 if len(module) == 0: 

489 raise ValueError("fully_shard does not support empty list of modules.") 

490 for i, m in enumerate(module): 

491 try: 

492 _check_module_valid(m) 

493 except ValueError: 

494 raise ValueError( 

495 f"fully_shard expects nn.Module or list[nn.Module], " 

496 f"but got list with {type(m).__name__} at index {i}." 

497 ) from None 

498 else: 

499 _check_module_valid(module) 

500 

501 

502HsdpValidationOptions = namedtuple( 

503 "HsdpValidationOptions", 

504 [ 

505 "shard_size", 

506 "threshold", 

507 "optimizer_level", 

508 "enable_grad_accumulation", 

509 "grad_scale", 

510 "reduce_dtype", 

511 "comm_async", 

512 "comm_fusion", 

513 "bucket_size", 

514 ], 

515) 

516 

517 

518def _validate_hsdp_shard_size(shard_size: int) -> None: 

519 if not isinstance(shard_size, int) or (shard_size <= 0 and shard_size != -1): 

520 raise ValueError(f"shard_size must be a positive integer, but got {shard_size}.") 

521 

522 

523def _validate_hsdp_threshold(threshold: int) -> None: 

524 if not isinstance(threshold, int) or threshold < 0: 

525 raise ValueError(f"threshold must be a positive integer or 0, but got {threshold}.") 

526 

527 

528def _validate_hsdp_optimizer_level(optimizer_level: str) -> None: 

529 if optimizer_level not in ["level1", "level2", "level3"]: 

530 raise ValueError( 

531 f"Optimizer level should in ['level1', 'level2', 'level3'], but got {optimizer_level}." 

532 ) 

533 

534 

535def _validate_hsdp_reduce_dtype(reduce_dtype) -> None: 

536 """Check ``reduce_dtype`` is a torch dtype or ``None``.""" 

537 if reduce_dtype is not None and not isinstance(reduce_dtype, torch.dtype): 

538 raise ValueError(f"reduce_dtype must be torch.dtype but got {reduce_dtype}.") 

539 

540 

541def _check_hsdp_input_valid(module, options: HsdpValidationOptions): 

542 """check hsdp input valid""" 

543 _check_module_valid(module) 

544 _validate_hsdp_shard_size(options.shard_size) 

545 _validate_hsdp_threshold(options.threshold) 

546 _validate_hsdp_optimizer_level(options.optimizer_level) 

547 if not isinstance(options.enable_grad_accumulation, bool): 

548 raise ValueError( 

549 f"enable_grad_accumulation must be bool but got {options.enable_grad_accumulation}." 

550 ) 

551 if not isinstance(options.grad_scale, float): 

552 raise ValueError(f"grad_scale must be float but got {options.grad_scale}.") 

553 _validate_hsdp_reduce_dtype(options.reduce_dtype) 

554 if not isinstance(options.comm_async, bool): 

555 raise ValueError(f"comm_async must be bool but got {options.comm_async}.") 

556 if not isinstance(options.comm_fusion, bool): 

557 raise ValueError(f"comm_fusion must be bool but got {options.comm_fusion}.") 

558 if not isinstance(options.bucket_size, int) or ( 

559 options.bucket_size < 0 and options.bucket_size != -1 

560 ): 

561 raise ValueError( 

562 f"bucket_size must be a positive integer or 0, but got {options.bucket_size}." 

563 ) 

564 

565 

566def _get_device_from_mesh(mesh: DeviceMesh): 

567 """Extract and validate the torch device from the device mesh.""" 

568 device = None 

569 device_type = mesh.device_type 

570 if device_type not in ("npu", "cuda"): 

571 raise AssertionError( 

572 f"hyper_parallel.fully_shard support device in [torch.npu, torch.cuda], " 

573 f"but got '{device_type}'" 

574 ) 

575 device_handle = get_device_handle(device_type) 

576 if device_handle is None: 

577 raise ValueError( 

578 f"hyper_parallel.fully_shard can't find device_handle of " 

579 f"'torch.{device_type}', check the environment." 

580 ) 

581 if device_handle.is_available(): 

582 device = torch.device(device_handle.current_device()) 

583 return device 

584 

585 

586def _normalize_replicate_params( 

587 replicate_params: Optional[set[nn.Parameter]], 

588) -> set[nn.Parameter]: 

589 """ 

590 Normalize replicate_params for fully_shard 

591 Args: 

592 replicate_params (Optional[set[nn.Parameter]]): Set of parameters to exclude from sharding. 

593 Returns: 

594 set[nn.Parameter]: Set of parameters to exclude from sharding. 

595 """ 

596 if replicate_params is None: 

597 return set() 

598 out = set(replicate_params) 

599 for p in out: 

600 if not isinstance(p, (nn.Parameter, DTensor)): 

601 raise TypeError( 

602 "replicate_params must contain only nn.Parameter or DTensor, " 

603 f"got {type(p).__name__}." 

604 ) 

605 return out 

606 

607 

608def _get_modules_parameters(modules, ignored_params=None): 

609 """Collect deduplicated parameters from module roots.""" 

610 return get_managed_modules_parameters(modules, ignored_params) 

611 

612 

613def _validate_managed_params_source_shard_infos( 

614 managed_parameters: set[nn.Parameter], 

615 source_shard_infos: Optional[Mapping[nn.Parameter, SourceShardMetaInfo]], 

616) -> None: 

617 """Validate the parameter-identity metadata consumed by one fully_shard unit.""" 

618 if source_shard_infos is None: 

619 return 

620 if not isinstance(source_shard_infos, Mapping): 

621 raise ValueError("source_shard_infos must be a mapping from Parameter to SourceShardMetaInfo") 

622 

623 managed_parameters = set(managed_parameters) 

624 

625 if any(isinstance(parameter, DTensor) for parameter in managed_parameters): 

626 raise ValueError( 

627 "source_shard_infos cannot be provided when fully_shard manages a native DTensor parameter" 

628 ) 

629 

630 invalid_values = [ 

631 parameter 

632 for parameter, metadata in source_shard_infos.items() 

633 if not isinstance(metadata, SourceShardMetaInfo) 

634 ] 

635 if invalid_values: 

636 raise ValueError("source_shard_infos values must be SourceShardMetaInfo instances") 

637 

638 invalid_origins = [ 

639 parameter 

640 for parameter, metadata in source_shard_infos.items() 

641 if metadata.origin_is_dtensor and not isinstance(parameter, DTensor) 

642 ] 

643 if invalid_origins: 

644 raise ValueError( 

645 "source_shard_infos origin_is_dtensor=True requires a native DTensor parameter" 

646 ) 

647 

648 missing_parameters = managed_parameters.difference(source_shard_infos) 

649 if missing_parameters: 

650 raise ValueError( 

651 "source_shard_infos must cover every parameter managed by this fully_shard call" 

652 ) 

653 unexpected_parameters = set(source_shard_infos).difference(managed_parameters) 

654 if unexpected_parameters: 

655 raise ValueError( 

656 "source_shard_infos contains a parameter not managed by this fully_shard call" 

657 ) 

658 

659 

660def fully_shard( 

661 module: Union[nn.Module, List[nn.Module]], 

662 *, 

663 mesh: Optional[DeviceMesh] = None, 

664 reshard_after_forward: bool = True, 

665 shard_placement_fn: None = None, 

666 mp_policy: MixedPrecisionPolicy = MixedPrecisionPolicy(), 

667 offload_policy: OffloadPolicy = OffloadPolicy(), 

668 ignored_params: Optional[set[nn.Parameter]] = None, 

669 replicate_params: Optional[set[nn.Parameter]] = None, 

670 comm_fusion: bool = False, 

671 comm_fusion_zero_copy: Optional[bool] = None, 

672 source_shard_infos: Optional[Mapping[nn.Parameter, SourceShardMetaInfo]] = None, 

673) -> Union[nn.Module, List[nn.Module]]: 

674 

675 """ 

676 Apply fully_shard to a module (or list of modules) for distributed training with parameter sharding. 

677 

678 This interface provides PyTorch-compatible HSDP (Hybrid Sharded Data Parallelism) 

679 functionality, enabling efficient training of large models by sharding parameters 

680 across multiple devices. The module is automatically enhanced with distributed 

681 capabilities including parameter sharding, gradient synchronization, and memory 

682 management. 

683 

684 When a list of modules is passed, they are treated as one FSDP unit (parameters 

685 grouped together). 

686 

687 Parameters: 

688 module (nn.Module or List[nn.Module]): 

689 The module(s) to apply fully_shard to. Modified in-place. When a list 

690 is passed, parameters from all modules are grouped as one FSDP unit. 

691 

692 mesh (Optional[DeviceMesh], default=None): 

693 The device mesh defining the process topology for distributed training. 

694 If None, fully_shard creates a default 1D mesh for local parameters. 

695 Native DTensor parameters require an explicit DP submesh. 

696 

697 reshard_after_forward (bool, default=True): 

698 Whether to automatically reshard parameters after forward. When True, 

699 parameters are resharded immediately after they are no longer needed, 

700 freeing memory for subsequent operations. Set to False if you want to 

701 keep parameters unsharded for backward pass or manual control. 

702 

703 shard_placement_fn (Callable, default=None): 

704 A callable that determines how to shard each parameter. The function 

705 should accept a parameter and return a Shard object specifying the 

706 sharding dimension, or None to use default sharding (dimension 0) 

707 

708 mp_policy (MixedPrecisionPolicy, default=MixedPrecisionPolicy()): 

709 Mixed precision training policy controlling data type conversions. 

710 

711 offload_policy (OffloadPolicy, default=OffloadPolicy()): 

712 Memory offload policy for reducing device memory usage. 

713 

714 ignored_params (Optional[set[nn.Parameter]], default=None): 

715 Set of parameters to exclude from fully_shard management entirely. 

716 These parameters are left on the original module as regular parameters, 

717 are not sharded, and do not participate in fully_shard gradient 

718 synchronization. Use this for parameters that should remain outside 

719 the fully_shard lifecycle. 

720 

721 replicate_params (Optional[set[nn.Parameter]], default=None): 

722 Set of parameters to keep replicated while still managing them under 

723 fully_shard. These parameters are not sharded, but their gradients 

724 are still synchronized with DDP-style all-reduce over the current 

725 fully_shard communication domain. This differs from ``ignored_params``, 

726 which skips fully_shard management and gradient synchronization 

727 entirely for the selected parameters. 

728 

729 comm_fusion (bool, default=False): 

730 Whether enable all_gather fusion and reduce_scatter fusion. 

731 

732 comm_fusion_zero_copy (Optional[bool], default=None): 

733 Whether allow the experimental zero-copy path for 

734 ``comm_fusion``. When set to ``None``, fully_shard enables it 

735 automatically whenever ``comm_fusion=True``. 

736 When enabled, fully_shard may rebase sharded local parameter storage 

737 into one shared flat buffer so fused all-gather can read directly from 

738 contiguous memory. This path depends on optimizer compatibility with 

739 view-backed parameters. 

740 source_shard_infos (Optional[Mapping[nn.Parameter, SourceShardMetaInfo]]): 

741 Source TP/EP mesh and placements for the plain-parameter dual mode. 

742 

743 Returns: 

744 nn.Module or List[nn.Module]: The input module(s) with HSDP capabilities added. 

745 """ 

746 _validate_module_for_fully_shard(module) 

747 

748 arg_module = module 

749 if isinstance(module, list): 

750 modules = tuple(_get_root_modules(module)) 

751 else: 

752 modules = (module,) 

753 

754 for mod in modules: 

755 _extend_module_with_hsdp_interface(mod) 

756 

757 params = _get_modules_parameters(modules, ignored_params) 

758 has_dtensor_param = any(isinstance(param, DTensor) for param in params) 

759 _validate_managed_params_source_shard_infos(set(params), source_shard_infos) 

760 

761 replicate_params = _normalize_replicate_params(replicate_params) 

762 

763 if mesh is None: 

764 mesh = init_device_mesh(device_type="npu", mesh_shape=(dist.get_world_size(),)) 

765 if has_dtensor_param: 

766 raise ValueError( 

767 "fully_shard does not support mesh=None with a native DTensor parameter; " 

768 "pass an explicit DP submesh instead." 

769 ) 

770 device = _get_device_from_mesh(mesh) 

771 

772 init_modules = modules 

773 modules[0].hsdp_init( 

774 init_modules, 

775 mesh, 

776 reshard_after_forward, 

777 shard_placement_fn, 

778 mp_policy, 

779 offload_policy, 

780 ignored_params, 

781 replicate_params, 

782 device, 

783 comm_fusion, 

784 comm_fusion_zero_copy, 

785 source_shard_infos=source_shard_infos, 

786 ) 

787 # Share the same scheduler handle with other roots so mods[i].unshard()/prefetch work 

788 if len(modules) > 1: 

789 for mod in modules[1:]: 

790 mod.hsdp_scheduler = modules[0].hsdp_scheduler 

791 return arg_module 

792 

793 

794def get_model_state_dict(model: Any, *, options: Any = None) -> dict[str, Any]: 

795 """Get the model state dict of a fully_shard-managed model. 

796 

797 Args: 

798 model: The model whose state dict to retrieve. 

799 options: Optional :class:`StateDictOptions` controlling gathering 

800 (``full_state_dict``), offloading (``cpu_offload``) and frozen 

801 parameter filtering (``ignore_frozen_params``). 

802 

803 Returns: 

804 The model state dict. By default values are sharded DTensors; when 

805 ``full_state_dict=True`` values are full Tensors (CPU on rank 0 only 

806 when ``cpu_offload=True``). 

807 """ 

808 return get_model_state_dict_impl(model, options=options) 

809 

810 

811def set_model_state_dict(model: Any, model_state_dict: dict[str, Any], *, options: Any = None) -> None: 

812 """Load ``model_state_dict`` into a fully_shard-managed model. 

813 

814 Full tensors are scattered into DTensor shards matching the model's layout 

815 before the in-place load. 

816 

817 Args: 

818 model: The model to load state into. 

819 model_state_dict: State dict to load. Values may be plain (global) 

820 tensors when ``full_state_dict=True`` or sharded DTensors otherwise. 

821 options: Optional :class:`StateDictOptions` controlling scattering 

822 (``full_state_dict``), device placement (``cpu_offload``) and 

823 strictness (``strict``). 

824 

825 Returns: 

826 None. 

827 """ 

828 return set_model_state_dict_impl(model, model_state_dict, options=options) 

829 

830 

831def hsdp_sync_stream(): 

832 """Wait for hsdp gradient handle to be completed.""" 

833 wait_grad_handle()