Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / fully_shard / param_group.py: 82%

541 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +0800

1# Copyright 2025-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# Adapted from https://github.com/pytorch/pytorch/blob/release/2.6/torch/distributed/fsdp/_fully_shard/_fsdp_param.py 

16# enhanced with fully_shard parameter management 

17# ============================================================================ 

18"""HSDP parameter group. 

19 

20This module implements fused communication for HSDP (Hybrid Shard Data Parallel) parameters. 

21Instead of issuing one all-gather / reduce-scatter per parameter, ``HSDPParamGroup`` packs all 

22parameters within a module into a single contiguous buffer and performs one collective operation, 

23which reduces kernel launch overhead and improves bandwidth utilization. 

24 

25Key components: 

26- ``HSDPParamGroup``: Groups all HSDP parameters in a module for fused all-gather (forward) 

27 and fused reduce-scatter + all-reduce (backward). 

28- ``AllGatherMetadata`` / ``AllGatherMetadataCache``: Caches per-group metadata (dtypes, numels, 

29 split sizes) to avoid recomputation across iterations. 

30- ``CommContext``: Global context that tracks the in-flight async communication handle and the 

31 param group that owns it, enabling pipelined overlap between communication and computation. 

32""" 

33from typing import List, Optional, NamedTuple, Any 

34from dataclasses import dataclass, field 

35from contextlib import ExitStack 

36import torch 

37import torch.distributed as dist 

38from torch.distributed import Work 

39from hyper_parallel.core.fully_shard.hsdp_utils import apply_gradient_scaling_factor 

40from hyper_parallel.core.fully_shard.utils import ( 

41 MixedPrecisionPolicy, 

42 FSDPMeshInfo, 

43 DDPMeshInfo, 

44 HSDPMeshInfo, 

45) 

46from hyper_parallel.platform.torch.fully_shard.pack_utils import ( 

47 build_rs_plan, 

48 pack_for_reduce_scatter, 

49) 

50from hyper_parallel.platform.torch.fully_shard.param import TorchHSDPParamV2 

51 

52 

53def get_all_gather_metadata(hsdp_params): 

54 """Collect metadata required for fused all-gather from all HSDP parameters. 

55 

56 Iterates over each parameter's local shard inputs and records their dtypes and 

57 element counts. All parameters must share the same dtype (heterogeneous dtypes 

58 are not yet supported). 

59 

60 Args: 

61 hsdp_params: List of ``TorchHSDPParamV2`` whose ``all_gather_inputs`` will 

62 be inspected. 

63 

64 Returns: 

65 AllGatherMetadata: Aggregated metadata used by ``foreach_all_gather`` to 

66 allocate the fused output buffer and perform copy-in/copy-out. 

67 

68 Raises: 

69 ValueError: If parameters have different dtypes. 

70 """ 

71 param_input_dtypes = [] 

72 param_input_numels = [] 

73 inp_split_sizes = [] 

74 total_input_numel = 0 

75 first_dtype = None 

76 

77 for hsdp_param in hsdp_params: 

78 inputs = hsdp_param.all_gather_inputs 

79 if first_dtype is None: 

80 first_dtype = inputs[0].dtype 

81 elif first_dtype != inputs[0].dtype: 

82 raise ValueError("All parameters in the group must have a uniform dtype.") 

83 param_dtypes = [t.dtype for t in inputs] 

84 param_numels = [t.numel() for t in inputs] 

85 param_input_dtypes.append(param_dtypes) 

86 param_input_numels.append(param_numels) 

87 inp_split_sizes.extend(param_numels) 

88 total_input_numel += sum(param_numels) 

89 

90 return AllGatherMetadata( 

91 param_input_dtypes, 

92 param_input_numels, 

93 first_dtype, 

94 inp_split_sizes, 

95 total_input_numel 

96 ) 

97 

98 

99@dataclass 

100class AllGatherMetadata: 

101 """Metadata describing the fused all-gather buffer layout. 

102 

103 Attributes: 

104 param_input_dtypes: Per-parameter list of input tensor dtypes. 

105 param_input_numels: Per-parameter list of input tensor element counts. 

106 dtype: Uniform dtype of all inputs (used to allocate the fused buffer). 

107 inp_split_sizes: Flat list of element counts for each input tensor across 

108 all parameters, used by ``torch.split`` / ``split_with_sizes_copy`` to 

109 slice the fused buffer back into per-parameter chunks. 

110 total_input_numel: Total number of elements from all local shards (one rank's 

111 contribution); the full all-gather output has ``total_input_numel * world_size`` 

112 elements. 

113 hash_key: Computed in ``__post_init__`` for use as a cache key. 

114 """ 

115 param_input_dtypes: list[list[torch.dtype]] 

116 param_input_numels: list[list[int]] 

117 dtype: torch.dtype 

118 inp_split_sizes: list[int] 

119 total_input_numel: int 

120 hash_key: int = field(init=False) 

121 

122 def __post_init__(self): 

123 self.hash_key = hash(( 

124 tuple(tuple(d) for d in self.param_input_dtypes), 

125 tuple(tuple(n) for n in self.param_input_numels), 

126 self.dtype, 

127 tuple(self.inp_split_sizes), 

128 self.total_input_numel 

129 )) 

130 

131 

132class AllGatherResult(NamedTuple): 

133 """Result of a fused all-gather operation. 

134 

135 Attributes: 

136 all_gather_output: The contiguous output buffer holding gathered data from all ranks. 

137 metadata: The ``AllGatherMetadata`` used to interpret the buffer layout. 

138 handle: Async work handle from ``dist.all_gather_into_tensor``; ``None`` when 

139 the operation was synchronous or when ``shard_world_size == 1``. 

140 """ 

141 all_gather_output: torch.Tensor 

142 metadata: AllGatherMetadata 

143 handle: Optional[Work] 

144 

145 

146@dataclass 

147class CommContext: 

148 """Global communication context for pipelining fused gradient reduction. 

149 

150 For FSDP (shard-only), the reduce-scatter handle is stored in ``comm_handle`` 

151 and the next module's backward hook waits on it before issuing its own reduction. 

152 

153 For HSDP (shard + replicate), a two-phase pipeline is used: 

154 Phase 1 (``wait_reduce_scatter_and_issue_all_reduce``): wait for 

155 reduce-scatter, then issue one or more async all-reduces stored on 

156 the owning ``HSDPParamGroup``. 

157 Phase 2 (``wait_all_reduce_and_apply_grad``): wait for all-reduce and 

158 write reduced gradients back. 

159 

160 This allows three-way overlap: 

161 Layer N reduce_scatter ↔ Layer N-1 backward compute 

162 Layer N all_reduce ↔ Layer N-1 reduce_scatter 

163 """ 

164 comm_handle: Optional[Work] = None 

165 all_reduce_handle: Optional[Work] = None 

166 pre_param_group = None 

167 # Param group whose all_reduce has been issued but grad not yet applied 

168 all_reduce_param_group = None 

169 

170 

171comm_ctx = CommContext() 

172 

173 

174def get_comm_ctx(): 

175 """Return the global ``CommContext`` singleton.""" 

176 return comm_ctx 

177 

178 

179@dataclass 

180class ReplicateBucket: 

181 """One fused all-reduce bucket sharing the same replicate process group.""" 

182 

183 key: int 

184 group: Any 

185 group_size: int 

186 param_indices: list[int] 

187 flat_numel: int 

188 buffer: Optional[torch.Tensor] = None 

189 

190 

191@dataclass 

192class PendingBucketAllReduce: 

193 """One in-flight async all-reduce launched for a replicate bucket.""" 

194 

195 bucket_key: int 

196 handle: Any 

197 

198 

199class AllGatherMetadataCache: 

200 """Cache for ``AllGatherMetadata`` to avoid recomputation across iterations. 

201 

202 The cache key is derived from ``(id(param), param.version)`` tuples so that 

203 it invalidates automatically when parameters are re-sharded or replaced. 

204 """ 

205 _cache: dict[int, AllGatherMetadata] = {} 

206 

207 @classmethod 

208 def get_metadata(cls, hsdp_params, fn): 

209 """Return cached metadata or compute via *fn* and cache the result.""" 

210 param_key = tuple((id(p), getattr(p, 'version', 0)) for p in hsdp_params) 

211 key = hash(param_key) 

212 

213 if key in cls._cache: 

214 return cls._cache[key] 

215 metadata = fn(hsdp_params) 

216 cls._cache[key] = metadata 

217 return metadata 

218 

219 

220def all_gather_copy_in(all_gather_inputs, all_gather_output, inp_split_sizes, all_gather_input_numel, rank): 

221 """Copy per-parameter local shards into the fused all-gather input buffer. 

222 

223 The fused output buffer has shape ``(total_input_numel * world_size,)``. Each rank 

224 writes its local shards into the slice ``[input_numel * rank : input_numel * (rank+1)]`` 

225 using ``torch._foreach_copy_`` for efficient batched copy. 

226 

227 Args: 

228 all_gather_inputs: Flat list of local shard tensors from all parameters. 

229 all_gather_output: The pre-allocated fused output buffer. 

230 inp_split_sizes: Element counts for splitting the rank-local slice. 

231 all_gather_input_numel: Total elements for one rank's local shards. 

232 rank: This rank's index within the shard process group. 

233 

234 Returns: 

235 Tuple of (rank-local input slice, full output buffer). 

236 """ 

237 all_gather_input = all_gather_output.narrow(0, all_gather_input_numel * rank, all_gather_input_numel) 

238 foreach_copy_dsts = torch.split(all_gather_input, inp_split_sizes) 

239 with torch.no_grad(): 

240 # pylint: disable=W0212 

241 torch._foreach_copy_(foreach_copy_dsts, all_gather_inputs) 

242 return all_gather_input, all_gather_output 

243 

244 

245def reduce_scatter_copy_in( 

246 hsdp_params: List[TorchHSDPParamV2], 

247 unsharded_grads: List[torch.Tensor], 

248 reduce_scatter_input: torch.Tensor, 

249 world_size: int, 

250) -> None: 

251 """Pack unsharded gradients into the fused reduce-scatter input buffer. 

252 

253 Uses ``torch._chunk_cat`` to interleave chunks from each gradient tensor so that 

254 the buffer layout matches what ``dist.reduce_scatter_tensor`` expects: the buffer 

255 is viewed as ``(world_size, total_numel // world_size)`` where row *i* contains 

256 the slice destined for rank *i* after reduction. 

257 

258 Args: 

259 hsdp_params: Parameters whose layout determines the pack plan per gradient. 

260 unsharded_grads: Full (unsharded) gradients from all parameters. 

261 reduce_scatter_input: Pre-allocated flat buffer of size ``sum(g.numel() for g in unsharded_grads)``. 

262 world_size: Number of ranks in the shard process group. 

263 """ 

264 if len(hsdp_params) != len(unsharded_grads): 

265 raise AssertionError( 

266 "reduce_scatter_copy_in expects one hsdp_param per unsharded_grad, but got " 

267 f"{len(hsdp_params)} params and {len(unsharded_grads)} grads" 

268 ) 

269 packed_rows = reduce_scatter_input.view(world_size, -1) 

270 col_offset = 0 

271 with torch.no_grad(): 

272 for hsdp_param, grad in zip(hsdp_params, unsharded_grads): 

273 grad = grad.contiguous() 

274 plan = build_rs_plan(hsdp_param, grad, world_size) 

275 packed_grad = pack_for_reduce_scatter(grad, plan) 

276 next_col_offset = col_offset + packed_grad.size(1) 

277 packed_rows[:, col_offset:next_col_offset].copy_(packed_grad) 

278 col_offset = next_col_offset 

279 if col_offset != packed_rows.size(1): 

280 raise AssertionError( 

281 "reduce_scatter_copy_in packed an unexpected number of elements: " 

282 f"{col_offset} != {packed_rows.size(1)}" 

283 ) 

284 

285 

286class HSDPParamGroup: 

287 """Groups all HSDP parameters within a module for fused collective communication. 

288 

289 Instead of issuing per-parameter all-gather (forward) and reduce-scatter (backward), 

290 this class packs all parameter shards into a single contiguous buffer and performs one 

291 fused collective, reducing NCCL/HCCL kernel launch overhead. 

292 

293 Lifecycle within one training iteration: 

294 1. **Forward** — ``unshard()`` → ``foreach_all_gather()`` packs local shards into 

295 ``ag_output`` and issues a single ``all_gather_into_tensor``. 

296 2. **Forward (wait)** — ``wait_for_unshard()`` → ``foreach_all_gather_copy_out()`` 

297 waits on the handle and scatters gathered data back to per-parameter buffers. 

298 3. **Backward** — ``foreach_reduce()`` packs unsharded gradients, issues fused 

299 ``reduce_scatter_tensor`` (+ optional ``all_reduce`` for HSDP replicate dim), 

300 and stores the handle in ``CommContext`` for pipelined overlap. 

301 4. **Backward (apply)** — ``apply_fusion_reduced_grad()`` waits on the handle and 

302 writes reduced gradient slices back to each parameter's ``.grad`` or ``.main_grad``. 

303 

304 Args: 

305 hsdp_params: List of ``TorchHSDPParamV2`` belonging to this module. 

306 mesh_info: Mesh info providing shard/replicate process groups. 

307 device: Target device for buffer allocation. 

308 mp_policy: Mixed-precision policy controlling reduce dtype and grad dtype. 

309 """ 

310 

311 def __init__( 

312 self, 

313 hsdp_params, 

314 mesh_info: FSDPMeshInfo, 

315 device: Optional[torch.device] = None, 

316 mp_policy: Optional[MixedPrecisionPolicy] = None, 

317 enable_zero_copy: bool = True, 

318 ): 

319 self.mesh_info = mesh_info 

320 self.device = device 

321 self.hsdp_params = hsdp_params 

322 if isinstance(self.mesh_info, (FSDPMeshInfo, HSDPMeshInfo)): 

323 self.shard_rank = self.mesh_info.shard_mesh_rank 

324 self.shard_world_size = self.mesh_info.shard_mesh_size 

325 else: 

326 self.shard_rank = 0 

327 self.shard_world_size = 1 

328 self.shard_group = self.mesh_info.shard_process_group 

329 self.replicate_group = None 

330 if isinstance(self.mesh_info, (HSDPMeshInfo, DDPMeshInfo)): 

331 self.replicate_group = self.mesh_info.replicate_process_group 

332 elif isinstance(self.mesh_info, FSDPMeshInfo): 

333 self.replicate_group = self._infer_layout_replicate_group() 

334 self.device = device 

335 self._all_gather_output = torch.empty(0, device=self.device) 

336 self.ag_output = None # Fused all-gather output buffer, lazily allocated 

337 self.metadata_cache = None 

338 self.mp_policy = mp_policy 

339 self.enable_zero_copy = enable_zero_copy 

340 self._result = None # Pending AllGatherResult from async all-gather 

341 self._reduce_output = None # Fused reduce-scatter output, consumed by apply_fusion_reduced_grad 

342 self._reduce_op = None # Reduce op saved from foreach_reduce for use in apply_fusion_reduced_grad 

343 self._needs_avg_div = False # Whether AVG was split into SUM + deferred div 

344 self._reduce_hsdp_params = None 

345 self._active_replicate_buckets: dict[int, ReplicateBucket] = {} 

346 self._active_param_flat_offsets: list[int] = [] 

347 self._pending_all_reduce_handles: list[PendingBucketAllReduce] = [] 

348 self._init_mp_dtypes() 

349 self._flat_param_buffer = None # Contiguous buffer holding all params' sharded data 

350 self._flat_cast_buffer = None # Cast buffer for mixed precision (param_dtype) 

351 if self.enable_zero_copy: 

352 self._init_flat_param_buffer() 

353 self.gradient_scaling_factor = None 

354 

355 def _infer_layout_replicate_group(self): 

356 """Infer a compatibility all-reduce group from params' final DTensor layout when mesh_info has none. 

357 

358 DTENSOR_UNIFIED parameters may still carry replicate axes from the original 

359 DTensor layout, for example a ``(tp, ep)`` mesh where ``ep`` is replicate-only. 

360 The non-fused path derives this group from each param's layout-driven 

361 ``unsharded_group_info``. ``comm_fusion`` now buckets by those groups, so 

362 this helper only preserves the historical ``self.replicate_group`` field 

363 for compatibility with simpler single-group paths. 

364 """ 

365 replicate_groups = [] 

366 for hsdp_param in self.hsdp_params: 

367 group_info = getattr(hsdp_param, "unsharded_group_info", None) 

368 group = getattr(group_info, "group", None) 

369 if group is None or getattr(hsdp_param, "replicate_world_size", 1) <= 1: 

370 continue 

371 replicate_groups.append((group, getattr(hsdp_param, "_param_fqn", "<unknown>"))) 

372 

373 if not replicate_groups: 

374 return None 

375 

376 ref_group, _ = replicate_groups[0] 

377 return ref_group 

378 

379 def _build_active_replicate_buckets(self, hsdp_params): 

380 """Group active params by their layout-driven replicate all-reduce group.""" 

381 buckets: dict[int, ReplicateBucket] = {} 

382 for idx, hsdp_param in enumerate(hsdp_params): 

383 group_info = getattr(hsdp_param, "unsharded_group_info", None) 

384 group = getattr(group_info, "group", None) 

385 group_size = getattr( 

386 group_info, 

387 "rank_size", 

388 getattr(hsdp_param, "replicate_world_size", 1), 

389 ) 

390 if not isinstance(group_size, int): 

391 fallback_group_size = getattr(hsdp_param, "replicate_world_size", 1) 

392 group_size = fallback_group_size if isinstance(fallback_group_size, int) else 1 

393 if group is None or group_size <= 1: 

394 continue 

395 

396 key = id(group) 

397 if key not in buckets: 

398 buckets[key] = ReplicateBucket( 

399 key=key, 

400 group=group, 

401 group_size=group_size, 

402 param_indices=[], 

403 flat_numel=0, 

404 ) 

405 buckets[key].param_indices.append(idx) 

406 buckets[key].flat_numel += hsdp_param.sharded_size.numel() 

407 return buckets 

408 

409 def _allocate_bucket_buffers_if_needed(self, device, dtype): 

410 """Allocate or resize per-bucket temporary all-reduce buffers.""" 

411 for bucket in self._active_replicate_buckets.values(): 

412 if bucket.flat_numel == 0: 

413 continue 

414 needs_new_buffer = ( 

415 bucket.buffer is None 

416 or bucket.buffer.numel() != bucket.flat_numel 

417 or bucket.buffer.device != device 

418 or bucket.buffer.dtype != dtype 

419 ) 

420 if needs_new_buffer: 

421 bucket.buffer = torch.empty(bucket.flat_numel, device=device, dtype=dtype) 

422 

423 def _pack_bucket_from_reduce_output(self, bucket: ReplicateBucket) -> torch.Tensor: 

424 """Pack one replicate bucket's scattered shards into a contiguous all-reduce buffer.""" 

425 if bucket.buffer is None: 

426 raise AssertionError("Bucket buffer must be allocated before packing from reduce output") 

427 if self._reduce_output is None or self._reduce_hsdp_params is None: 

428 raise AssertionError("Bucket packing requires an active fused reduce output") 

429 dst_offset = 0 

430 for idx in bucket.param_indices: 

431 hsdp_param = self._reduce_hsdp_params[idx] 

432 src_offset = self._active_param_flat_offsets[idx] 

433 numel = hsdp_param.sharded_size.numel() 

434 bucket.buffer.narrow(0, dst_offset, numel).copy_( 

435 self._reduce_output.narrow(0, src_offset, numel) 

436 ) 

437 dst_offset += numel 

438 return bucket.buffer 

439 

440 def _unpack_bucket_to_reduce_output(self, bucket: ReplicateBucket) -> None: 

441 """Write one bucket's post-all-reduce data back into the fused reduce output.""" 

442 if bucket.buffer is None: 

443 raise AssertionError("Bucket buffer must exist before unpacking to reduce output") 

444 if self._reduce_output is None or self._reduce_hsdp_params is None: 

445 raise AssertionError("Bucket unpack requires an active fused reduce output") 

446 src_offset = 0 

447 for idx in bucket.param_indices: 

448 hsdp_param = self._reduce_hsdp_params[idx] 

449 dst_offset = self._active_param_flat_offsets[idx] 

450 numel = hsdp_param.sharded_size.numel() 

451 self._reduce_output.narrow(0, dst_offset, numel).copy_( 

452 bucket.buffer.narrow(0, src_offset, numel) 

453 ) 

454 src_offset += numel 

455 

456 def _init_flat_param_buffer(self): 

457 """Initialize a contiguous flat buffer and rebase all params' sharded data into it. 

458 

459 This enables zero-copy all-gather by making all local shards contiguous in memory, 

460 so they can be passed directly to ``all_gather_into_tensor`` without ``foreach_copy_``. 

461 When mixed-precision casting is needed, a separate cast buffer is also allocated. 

462 """ 

463 if self.shard_world_size <= 1: 

464 return 

465 if len(self.hsdp_params) == 0: 

466 return 

467 if any(p.offload_to_cpu or p.sharded_param.device.type == "meta" for p in self.hsdp_params): 

468 return 

469 

470 total_numel = sum(p._sharded_param_data.numel() for p in self.hsdp_params) 

471 orig_dtype = self.hsdp_params[0]._sharded_param_data.dtype 

472 flat_buffer = torch.empty(total_numel, dtype=orig_dtype, device=self.device) 

473 

474 offset = 0 

475 for hsdp_param in self.hsdp_params: 

476 numel = hsdp_param._sharded_param_data.numel() 

477 flat_slice = flat_buffer.narrow(0, offset, numel) 

478 flat_slice.copy_(hsdp_param._sharded_param_data) 

479 # Rebase _sharded_param_data to be a view into the flat buffer 

480 hsdp_param._sharded_param_data = flat_slice 

481 # Rebase DTensor's local tensor so optimizer in-place updates write to flat buffer 

482 new_local = flat_slice.view(hsdp_param.sharded_size) 

483 req_grad = hsdp_param.sharded_param.requires_grad 

484 hsdp_param.sharded_param._local_tensor = new_local 

485 hsdp_param.sharded_param.data = new_local 

486 if req_grad: 

487 new_local.requires_grad_(True) 

488 hsdp_param.sharded_param.requires_grad_(True) 

489 offset += numel 

490 

491 self._flat_param_buffer = flat_buffer 

492 

493 # Allocate cast buffer for mixed precision if needed 

494 has_param_dtype = any(p.param_dtype is not None for p in self.hsdp_params) 

495 if has_param_dtype: 

496 cast_dtype = next(p.param_dtype for p in self.hsdp_params if p.param_dtype is not None) 

497 self._flat_cast_buffer = torch.empty(total_numel, dtype=cast_dtype, device=self.device) 

498 

499 def _is_flat_buffer_valid(self): 

500 """Check if the flat buffer is still backing the params' sharded data. 

501 

502 The flat buffer becomes invalid after ``load_state_dict`` triggers 

503 ``reset_sharded_param``, which re-assigns ``_sharded_param_data``. 

504 """ 

505 if self._flat_param_buffer is None or len(self.hsdp_params) == 0: 

506 return False 

507 return self.hsdp_params[0]._sharded_param_data.data_ptr() == self._flat_param_buffer.data_ptr() 

508 

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

510 """Trigger fused all-gather to reconstruct full parameters from shards. 

511 

512 If a prefetch has already been issued (``_result is not None``), this is a no-op. 

513 For ``shard_world_size == 1`` (no sharding), skips the collective entirely. 

514 

515 Args: 

516 async_op: If True, the all-gather runs asynchronously and must be 

517 completed later via ``wait_for_unshard()``. 

518 """ 

519 # Already prefetched — skip 

520 if self._result is not None: 

521 return 

522 if self.shard_world_size == 1: 

523 self._result = AllGatherResult(self._all_gather_output, None, None) 

524 return 

525 self.foreach_all_gather(async_op=async_op) 

526 

527 def _init_mp_dtypes(self): 

528 """Initialize and validate mixed-precision dtypes across all trainable parameters. 

529 

530 All trainable parameters in the group must have a uniform ``orig_dtype`` and 

531 ``reduce_dtype``; heterogeneous dtypes would cause incorrect buffer slicing. 

532 """ 

533 for hsdp_param in self.hsdp_params: 

534 hsdp_param.init_dtype_attrs(self.mp_policy) 

535 trainable_params: list[TorchHSDPParamV2] = [ 

536 p for p in self.hsdp_params if p.sharded_param.requires_grad 

537 ] 

538 orig_dtypes = {p.orig_dtype for p in trainable_params} 

539 reduce_dtypes = {p.reduce_dtype for p in trainable_params} 

540 if len(trainable_params) > 0 and len(orig_dtypes) != 1: 

541 raise AssertionError( 

542 f"hsdp expects uniform original parameter dtype but got {orig_dtypes}" 

543 ) 

544 self._orig_dtype = next(iter(orig_dtypes)) if trainable_params else None 

545 if len(trainable_params) > 0 and len(reduce_dtypes) != 1: 

546 raise AssertionError( 

547 f"hsdp expects uniform reduce dtype but got {reduce_dtypes}" 

548 ) 

549 self._reduce_dtype = next(iter(reduce_dtypes)) if trainable_params else None 

550 

551 def wait_for_unshard(self): 

552 """Wait for the async all-gather to complete and scatter data to per-parameter buffers. 

553 

554 For ``shard_world_size == 1``, simply copies the local shard as the full parameter. 

555 Otherwise, calls ``foreach_all_gather_copy_out`` to split the fused buffer and 

556 write each parameter's all-gather output. Finally, initializes unsharded parameters. 

557 """ 

558 if self._result is None: 

559 return 

560 if self.shard_world_size == 1: 

561 for hsdp_param in self.hsdp_params: 

562 all_gather_input = hsdp_param.all_gather_inputs[0] 

563 hsdp_param.init_all_gather_outputs( 

564 [all_gather_input.numel()], 

565 [all_gather_input.dtype], 

566 self.shard_world_size, 

567 self.device 

568 ) 

569 hsdp_param.alloc_all_gather_outputs() 

570 # pylint: disable=W0212 

571 with torch.autograd._unsafe_preserve_version_counter(hsdp_param.all_gather_outputs[0]): 

572 # pylint: disable=W0212 

573 hsdp_param.all_gather_outputs[0].copy_(all_gather_input) 

574 else: 

575 self.foreach_all_gather_copy_out() 

576 for hsdp_param in self.hsdp_params: 

577 hsdp_param.init_unsharded_param() 

578 hsdp_param.to_unsharded() 

579 

580 def alloc_all_gather_output(self, total_output_numel): 

581 """Resize the fused all-gather buffer storage to fit ``total_output_numel`` elements. 

582 

583 Uses ``untyped_storage().resize_()`` to avoid reallocating the tensor object, 

584 enabling storage reuse across iterations. 

585 """ 

586 storage = self.ag_output.untyped_storage() 

587 expected_size = total_output_numel * self.ag_output.itemsize 

588 if storage.size() != expected_size: 

589 storage.resize_(expected_size) 

590 

591 def free_all_gather_output(self): 

592 """Release device memory of the fused all-gather buffer by resizing storage to 0.""" 

593 storage = self.ag_output.untyped_storage() 

594 if storage.size() != 0: 

595 storage.resize_(0) 

596 

597 @torch.no_grad() 

598 def foreach_all_gather(self, async_op=False): 

599 """Perform a fused all-gather for all parameters in the group. 

600 

601 When a flat parameter buffer is available (see ``_init_flat_param_buffer``), 

602 the local shards are already contiguous and can be passed directly to 

603 ``all_gather_into_tensor`` without any copy-in. Otherwise falls back to 

604 the ``all_gather_copy_in`` path. 

605 

606 Args: 

607 async_op: If True, the collective runs asynchronously. 

608 """ 

609 if self.metadata_cache is None: 

610 self.metadata_cache = AllGatherMetadataCache() 

611 # pylint: disable=W0108 

612 metadata = self.metadata_cache.get_metadata(self.hsdp_params, lambda p: get_all_gather_metadata(p)) 

613 if metadata.total_input_numel == 0: 

614 return 

615 world_size, rank = self.shard_group.size(), self.shard_group.rank() 

616 total_output_numel = metadata.total_input_numel * world_size 

617 if self.ag_output is None: 

618 self.ag_output = torch.empty(size=(total_output_numel,), 

619 dtype=metadata.dtype, device=self.device) 

620 else: 

621 self.alloc_all_gather_output(total_output_numel) 

622 

623 if self.enable_zero_copy and not self._is_flat_buffer_valid(): 

624 self._init_flat_param_buffer() 

625 use_flat_buffer = self.enable_zero_copy and self._flat_param_buffer is not None 

626 if use_flat_buffer: 

627 # Zero-copy path: flat buffer already holds contiguous shard data 

628 if self._flat_cast_buffer is not None: 

629 # Mixed precision: single contiguous cast instead of N small copies 

630 self._flat_cast_buffer.copy_(self._flat_param_buffer) 

631 all_gather_input = self._flat_cast_buffer 

632 else: 

633 all_gather_input = self._flat_param_buffer 

634 else: 

635 # Fallback: collect inputs and copy into the rank-local slice of ag_output 

636 all_gather_inputs = [] 

637 for hsdp_param in self.hsdp_params: 

638 all_gather_inputs.extend(hsdp_param.all_gather_inputs) 

639 if len(all_gather_inputs) == 0: 

640 return 

641 all_gather_input, _ = all_gather_copy_in( 

642 all_gather_inputs, 

643 self.ag_output, 

644 metadata.inp_split_sizes, 

645 metadata.total_input_numel, 

646 rank 

647 ) 

648 del all_gather_inputs # Free references to individual shard tensors 

649 

650 handle = dist.all_gather_into_tensor(self.ag_output, all_gather_input, self.shard_group, async_op) 

651 self._result = AllGatherResult(self.ag_output, metadata, handle) 

652 

653 @torch.no_grad() 

654 def foreach_all_gather_copy_out(self): 

655 """Wait for the fused all-gather and scatter results back to per-parameter buffers. 

656 

657 After the collective completes, the fused output is viewed as ``(world_size, -1)`` 

658 and split along dim=1 according to ``inp_split_sizes``. Each slice is copied into 

659 the corresponding parameter's ``all_gather_outputs`` buffer using 

660 ``split_with_sizes_copy`` for zero-extra-allocation copy-out. 

661 

662 Version counters are preserved via ``_unsafe_preserve_version_counter`` to avoid 

663 triggering autograd version checks on parameter tensors that alias these buffers. 

664 """ 

665 (ag_output, metadata, _) = self._result 

666 if self._result.handle is not None: 

667 self._result.handle.wait() 

668 device = ag_output.device 

669 world_size = self.shard_group.size() 

670 split_with_sizes_out = [] 

671 for input_numels, input_dtypes, hsdp_param in zip( 

672 metadata.param_input_numels, metadata.param_input_dtypes, self.hsdp_params 

673 ): 

674 hsdp_param.init_all_gather_outputs(input_numels, input_dtypes, world_size, device) 

675 hsdp_param.alloc_all_gather_outputs() 

676 split_with_sizes_out.extend(hsdp_param.all_gather_outputs) 

677 ag_output = ag_output.view(world_size, -1) 

678 out = [t.view(world_size, -1) for t in split_with_sizes_out] 

679 non_inference_outs = [o for o in out if not o.is_inference()] 

680 if len(non_inference_outs) > 0: 

681 # Older torch variants only accept one tensor per context manager. 

682 # Preserve all version counters explicitly for cross-version compatibility. 

683 # pylint: disable=W0212 

684 with ExitStack() as stack: 

685 for tensor in non_inference_outs: 

686 stack.enter_context(torch.autograd._unsafe_preserve_version_counter(tensor)) 

687 torch.split_with_sizes_copy(ag_output, metadata.inp_split_sizes, dim=1, out=out) 

688 else: 

689 torch.split_with_sizes_copy(ag_output, metadata.inp_split_sizes, dim=1, out=out) 

690 self._result = None 

691 self.free_all_gather_output() # Immediately release fused buffer memory 

692 

693 @torch.no_grad() 

694 def foreach_reduce( 

695 self, 

696 reduce_scatter_reduce_op: Optional[dist.ReduceOp] = dist.ReduceOp.AVG, 

697 async_op: bool = True, 

698 ) -> Optional[torch.Tensor]: 

699 """Perform fused gradient reduction (reduce-scatter + optional all-reduce). 

700 

701 Collects unsharded gradients from all parameters, packs them into a single 

702 contiguous buffer, and issues one ``reduce_scatter_tensor``. For HSDP (2D mesh), 

703 a follow-up ``all_reduce`` across the replicate dimension is also performed. 

704 

705 When ``async_op=True``, the communication handle is stored in the global 

706 ``CommContext`` so that the next module's backward hook can overlap computation 

707 with this reduction. The actual gradient write-back is deferred to 

708 ``apply_fusion_reduced_grad()``. 

709 

710 Args: 

711 reduce_scatter_reduce_op: Reduction operator (default: AVG). 

712 async_op: If True, run collectives asynchronously for compute-comm overlap. 

713 """ 

714 # Collect unsharded gradients (from accumulated grad or .grad) 

715 hsdp_params: List[TorchHSDPParamV2] = [] 

716 unsharded_grads: List[torch.Tensor] = [] 

717 for hsdp_param in self.hsdp_params: 

718 if not hasattr(hsdp_param, '_unsharded_param'): 

719 continue 

720 if hsdp_param.unsharded_accumulated_grad is not None: 

721 hsdp_params.append(hsdp_param) 

722 unsharded_grads.append(hsdp_param.unsharded_accumulated_grad_data) 

723 elif hsdp_param._unsharded_param.grad is not None: # pylint: disable=W0212 

724 hsdp_params.append(hsdp_param) 

725 unsharded_grads.append(hsdp_param.unsharded_grad_data) 

726 if not hsdp_params: 

727 return 

728 grad_dtypes = {g.dtype for g in unsharded_grads} 

729 if len(grad_dtypes) != 1: 

730 raise ValueError( 

731 f"FSDP reduce-scatter expects uniform grad dtype but got {grad_dtypes}" 

732 ) 

733 grad_dtype = unsharded_grads[0].dtype 

734 reduce_dtype = self._reduce_dtype or grad_dtype 

735 world_size = self.shard_group.size() 

736 reduce_scatter_input_numel = sum(s.numel() for s in unsharded_grads) 

737 reduce_scatter_output_numel = reduce_scatter_input_numel // world_size 

738 device = unsharded_grads[0].device 

739 # Pack all gradients into a contiguous buffer for fused reduce-scatter 

740 reduce_scatter_input = torch.empty((reduce_scatter_input_numel,), dtype=reduce_dtype, device=device) 

741 reduce_scatter_copy_in(hsdp_params, unsharded_grads, reduce_scatter_input, world_size) 

742 unsharded_grads.clear() # Release references to full gradients 

743 # Captured here, consumed once in _apply_reduced_grad after all collectives 

744 # complete. Async paths cross method boundaries, so the field is unavoidable. 

745 reduce_output = reduce_scatter_input.new_empty((reduce_scatter_output_numel,)) 

746 self._needs_avg_div = reduce_scatter_reduce_op == dist.ReduceOp.AVG 

747 comm_op = dist.ReduceOp.SUM if self._needs_avg_div else reduce_scatter_reduce_op 

748 self._reduce_op = comm_op 

749 self._reduce_hsdp_params = hsdp_params 

750 self._active_param_flat_offsets = [] 

751 flat_offset = 0 

752 for hsdp_param in hsdp_params: 

753 self._active_param_flat_offsets.append(flat_offset) 

754 flat_offset += hsdp_param.sharded_size.numel() 

755 self._active_replicate_buckets = self._build_active_replicate_buckets(hsdp_params) 

756 self._allocate_bucket_buffers_if_needed(reduce_output.device, reduce_output.dtype) 

757 self._pending_all_reduce_handles = [] 

758 apply_gradient_scaling_factor(reduce_scatter_input, self.gradient_scaling_factor) 

759 rs_handle = dist.reduce_scatter_tensor( 

760 output=reduce_output, 

761 input=reduce_scatter_input, 

762 group=self.shard_group, 

763 op=comm_op, 

764 async_op=async_op 

765 ) 

766 comm_ctx.comm_handle = rs_handle 

767 # Step 2 (HSDP only): All-reduce is deferred to apply_fusion_reduced_grad() 

768 self._reduce_output = reduce_output 

769 if async_op: 

770 # Register this group for deferred grad application by the next backward hook 

771 comm_ctx.pre_param_group = self 

772 else: 

773 self.apply_fusion_reduced_grad() 

774 

775 def wait_reduce_scatter_and_issue_all_reduce(self): 

776 """Phase 1 of pipelined HSDP gradient reduction. 

777 

778 Waits for the async reduce-scatter to complete, then issues an async 

779 all-reduce for each active replicate bucket. The bucket handles are 

780 stored on this ``HSDPParamGroup`` so they can overlap with the next 

781 layer's reduce-scatter (Phase 2 is deferred). 

782 

783 For FSDP (no replicate group), skips the all-reduce and directly 

784 applies gradients since there is nothing further to pipeline. 

785 """ 

786 if comm_ctx.comm_handle is not None: 

787 comm_ctx.comm_handle.wait() 

788 comm_ctx.comm_handle = None 

789 # Deferred div for AVG: apply after RS completes, before AR 

790 if self._needs_avg_div: 

791 self._reduce_output.div_(self.shard_world_size) 

792 if not self._active_replicate_buckets: 

793 # No replicate group — no all-reduce needed, apply grads immediately 

794 self._apply_reduced_grad() 

795 return 

796 

797 self._pending_all_reduce_handles = [] 

798 for bucket in self._active_replicate_buckets.values(): 

799 packed = self._pack_bucket_from_reduce_output(bucket) 

800 ar_handle = dist.all_reduce( 

801 packed, 

802 group=bucket.group, 

803 op=self._reduce_op, 

804 async_op=True, 

805 ) 

806 self._pending_all_reduce_handles.append( 

807 PendingBucketAllReduce(bucket_key=bucket.key, handle=ar_handle) 

808 ) 

809 comm_ctx.all_reduce_param_group = self 

810 

811 def wait_all_reduce_and_apply_grad(self): 

812 """Phase 2 of pipelined HSDP gradient reduction. 

813 

814 Waits for the async all-reduce issued in Phase 1 and writes reduced 

815 gradients back to sharded parameters. 

816 """ 

817 for pending in self._pending_all_reduce_handles: 

818 bucket = self._active_replicate_buckets[pending.bucket_key] 

819 pending.handle.wait() 

820 if self._needs_avg_div: 

821 bucket.buffer.div_(bucket.group_size) 

822 self._unpack_bucket_to_reduce_output(bucket) 

823 self._pending_all_reduce_handles = [] 

824 comm_ctx.all_reduce_handle = None 

825 self._apply_reduced_grad() 

826 

827 def apply_fusion_reduced_grad(self): 

828 """Full synchronous reduction path (used for final drain and sync mode). 

829 

830 Waits for reduce-scatter, performs synchronous all-reduce if needed, 

831 and applies gradients — all in one call without pipelining. 

832 """ 

833 if comm_ctx.comm_handle is not None: 

834 comm_ctx.comm_handle.wait() 

835 comm_ctx.comm_handle = None 

836 # Deferred div for AVG after RS 

837 if self._needs_avg_div: 

838 self._reduce_output.div_(self.shard_world_size) 

839 for bucket in self._active_replicate_buckets.values(): 

840 packed = self._pack_bucket_from_reduce_output(bucket) 

841 dist.all_reduce( 

842 packed, 

843 group=bucket.group, 

844 op=self._reduce_op, 

845 ) 

846 # Deferred div for AVG after AR 

847 if self._needs_avg_div: 

848 packed.div_(bucket.group_size) 

849 self._unpack_bucket_to_reduce_output(bucket) 

850 self._apply_reduced_grad() 

851 

852 def _apply_reduced_grad(self): 

853 """Write reduced gradients from ``_reduce_output`` back to sharded parameters. 

854 

855 Slices the fused ``_reduce_output`` buffer into per-parameter sharded gradients 

856 using ``torch.as_strided`` (zero-copy view), then either accumulates into the 

857 existing ``.grad`` / ``.main_grad`` or assigns a new DTensor gradient. 

858 

859 Handles: 

860 - Mixed-precision: casts reduced gradient to ``_orig_dtype`` if needed. 

861 - CPU offload: transfers gradient to CPU (``non_blocking`` when possible). 

862 - Gradient accumulation: adds to existing grad when present. 

863 - Memory cleanup: nulls out unsharded grad references to free memory. 

864 """ 

865 flat_grad_offset = 0 

866 if self._reduce_hsdp_params is None: 

867 return 

868 # All collectives have completed; scale once on the fused buffer right 

869 # before slicing it into per-parameter sharded grads. 

870 for hsdp_param in self._reduce_hsdp_params: 

871 # Determine target gradient tensor (regular .grad or fp32 main_grad) 

872 sharded_grad = None 

873 if not self.mp_policy.apply_grad_on_fp32_main_grad: 

874 sharded_grad = hsdp_param.sharded_param.grad 

875 else: 

876 if not hasattr(hsdp_param.sharded_param, "main_grad"): 

877 hsdp_param.sharded_param.main_grad = None 

878 sharded_grad = hsdp_param.sharded_param.main_grad 

879 shard_size = hsdp_param.sharded_size 

880 # Zero-copy view into the fused reduce output for this parameter's shard 

881 new_sharded_grad = torch.as_strided( 

882 self._reduce_output, 

883 size=shard_size, 

884 stride=hsdp_param.contiguous_sharded_stride, 

885 storage_offset=flat_grad_offset, 

886 ) 

887 # Cast to original dtype if reduce was done in a different precision 

888 if not self.mp_policy.apply_grad_on_fp32_main_grad and new_sharded_grad.dtype != self._orig_dtype: 

889 new_sharded_grad = new_sharded_grad.to(self._orig_dtype) 

890 need_synchronize = False 

891 if hsdp_param.offload_to_cpu: 

892 non_blocking = hsdp_param.pin_memory and sharded_grad is None 

893 new_sharded_grad = new_sharded_grad.to( 

894 torch.device("cpu"), non_blocking=non_blocking 

895 ) 

896 need_synchronize = True 

897 # Accumulate or assign gradient 

898 if sharded_grad is not None: 

899 if not self.mp_policy.apply_grad_on_fp32_main_grad: 

900 hsdp_param.sharded_param.grad._local_tensor += new_sharded_grad 

901 else: 

902 hsdp_param.sharded_param.main_grad._local_tensor += new_sharded_grad 

903 hsdp_param.sharded_param.grad = None 

904 else: 

905 if not self.mp_policy.apply_grad_on_fp32_main_grad: 

906 hsdp_param.sharded_param.grad = hsdp_param.to_sharded_dtensor(new_sharded_grad) 

907 else: 

908 hsdp_param.sharded_param.main_grad = hsdp_param.to_sharded_dtensor(new_sharded_grad) 

909 hsdp_param.sharded_param.grad = None 

910 flat_grad_offset += shard_size.numel() 

911 # Release unsharded gradient references to free memory 

912 if hsdp_param.unsharded_accumulated_grad is not None: 

913 hsdp_param.unsharded_accumulated_grad = None 

914 elif hsdp_param.unsharded_param.grad is not None: 

915 hsdp_param.unsharded_param.grad = None 

916 

917 if need_synchronize: 

918 if self.device.type == "npu": 

919 torch.npu.current_stream().synchronize() 

920 elif self.device.type == "cuda": 

921 torch.cuda.current_stream().synchronize() 

922 else: 

923 raise NotImplementedError(f"Unsupported device type {self.device} for " 

924 f"synchronization after CPU offload.") 

925 self._reduce_output = None # Release fused reduce buffer 

926 self._reduce_hsdp_params = None 

927 self._active_param_flat_offsets = [] 

928 self._active_replicate_buckets = {} 

929 self._pending_all_reduce_handles = [] 

930 

931 

932class AllReduceParamGroup: 

933 """Groups HSDP parameters by replicate group for fused async all-reduce. 

934 

935 This class enables zero-copy fused all-reduce by: 

936 1. Pre-allocating a contiguous buffer with 512-byte alignment 

937 2. Having reduce_scatter write directly into aligned views of this buffer 

938 3. Performing a single all_reduce on the entire buffer 

939 4. Applying gradients directly from buffer views (with manual averaging) 

940 

941 Key design decisions for numerical correctness: 

942 - Uses SUM instead of AVG for all_reduce to avoid padding zeros affecting the average 

943 - Manually divides by replicate_world_size when applying gradients 

944 - Padding regions are initialized to zero and don't affect SUM results 

945 

946 Attributes: 

947 replicate_group: Process group for the replicate dimension. 

948 hsdp_params: List of HSDP parameters in this group. 

949 orig_dtypes: Original dtype for each parameter (for grad casting). 

950 reduce_dtype: Uniform dtype for the fused buffer. 

951 reduce_op: Original reduce op (AVG or SUM), used to determine final scaling. 

952 replicate_world_size: Size of the replicate group. 

953 fused_buffer: Pre-allocated contiguous buffer for all params. 

954 param_offsets: Element offset for each param in the fused buffer. 

955 param_numels: Number of elements for each param (excluding padding). 

956 all_reduce_handle: Async work handle for the in-flight all_reduce. 

957 """ 

958 

959 ALIGNMENT_BYTES = 512 # 512-byte alignment requirement 

960 

961 @staticmethod 

962 def _resolve_reduce_dtype( 

963 reduce_dtype: Optional[torch.dtype], 

964 hsdp_params: List["TorchHSDPParamV2"], 

965 orig_dtypes: List[torch.dtype], 

966 ) -> Optional[torch.dtype]: 

967 """Resolve None reduce_dtype to match ``reduce_scatter_grad``'s ``dtype or grad.dtype``.""" 

968 if reduce_dtype is not None: 

969 return reduce_dtype 

970 for hsdp_param in hsdp_params: 

971 if getattr(hsdp_param, "unsharded_accumulated_grad", None) is not None: 

972 return hsdp_param.unsharded_accumulated_grad_data.dtype 

973 unsharded_param = getattr(hsdp_param, "unsharded_param", None) 

974 if unsharded_param is not None and getattr(unsharded_param, "grad", None) is not None: 

975 return hsdp_param.unsharded_grad_data.dtype 

976 return orig_dtypes[0] if orig_dtypes else None 

977 

978 def __init__( 

979 self, 

980 replicate_group: dist.ProcessGroup, 

981 hsdp_params: List["TorchHSDPParamV2"], 

982 orig_dtypes: List[torch.dtype], 

983 reduce_dtype: torch.dtype, 

984 reduce_op: dist.ReduceOp, 

985 mp_policy: Optional["MixedPrecisionPolicy"] = None, 

986 ): 

987 self.replicate_group = replicate_group 

988 self.hsdp_params = hsdp_params 

989 self.orig_dtypes = orig_dtypes 

990 self.reduce_dtype = self._resolve_reduce_dtype(reduce_dtype, hsdp_params, orig_dtypes) 

991 self.reduce_op = reduce_op 

992 self.mp_policy = mp_policy 

993 self.replicate_world_size = replicate_group.size() if replicate_group else 1 

994 

995 # Fused buffer (lazily allocated) 

996 self.fused_buffer: Optional[torch.Tensor] = None 

997 # Element offsets in fused_buffer (accounting for padding) 

998 self.param_offsets: List[int] = [] 

999 # Number of elements per param (without padding) 

1000 self.param_numels: List[int] = [] 

1001 

1002 # Async communication handle 

1003 self.all_reduce_handle: Optional[dist.Work] = None 

1004 

1005 def compute_aligned_layout(self) -> int: 

1006 """Compute buffer layout with 512-byte alignment for total buffer size only. 

1007 

1008 Parameters are packed contiguously without per-param alignment. 

1009 Padding is added only at the end of the buffer to make total size 

1010 512-byte aligned. 

1011 

1012 Returns: 

1013 Total number of elements needed for the fused buffer. 

1014 """ 

1015 self.param_offsets = [] 

1016 self.param_numels = [] 

1017 

1018 element_size = torch.tensor([], dtype=self.reduce_dtype).element_size() 

1019 current_offset = 0 

1020 

1021 for hsdp_param in self.hsdp_params: 

1022 # Number of elements for this param's sharded gradient 

1023 numel = hsdp_param.sharded_size.numel() 

1024 self.param_numels.append(numel) 

1025 self.param_offsets.append(current_offset) 

1026 current_offset += numel 

1027 

1028 # Total buffer size in bytes (packed, no per-param padding) 

1029 total_bytes = current_offset * element_size 

1030 

1031 # Align total buffer size to 512 bytes (padding at end only) 

1032 aligned_total_bytes = ( 

1033 (total_bytes + self.ALIGNMENT_BYTES - 1) // self.ALIGNMENT_BYTES 

1034 ) * self.ALIGNMENT_BYTES 

1035 total_numel = aligned_total_bytes // element_size 

1036 

1037 return total_numel 

1038 

1039 def allocate_fused_buffer(self, device: torch.device) -> None: 

1040 """Allocate the fused buffer with computed layout.""" 

1041 total_numel = self.compute_aligned_layout() 

1042 self.fused_buffer = torch.empty(total_numel, dtype=self.reduce_dtype, device=device) 

1043 # Initialize to zero (important for SUM correctness with padding) 

1044 self.fused_buffer.zero_() 

1045 

1046 def get_param_buffer_view(self, idx: int) -> torch.Tensor: 

1047 """Get a view into the fused buffer for parameter at index idx. 

1048 

1049 This view can be used as the output buffer for reduce_scatter, 

1050 enabling zero-copy fusion. 

1051 

1052 Args: 

1053 idx: Index of the parameter in hsdp_params. 

1054 

1055 Returns: 

1056 A 1D tensor view of size param_numels[idx]. 

1057 """ 

1058 if self.fused_buffer is None: 

1059 raise RuntimeError("Fused buffer not allocated. Call allocate_fused_buffer first.") 

1060 

1061 offset = self.param_offsets[idx] 

1062 numel = self.param_numels[idx] 

1063 return self.fused_buffer.narrow(0, offset, numel) 

1064 

1065 def get_param_grad_view(self, idx: int, target_shape: torch.Size) -> torch.Tensor: 

1066 """Get a reshaped view of the reduced gradient for applying to parameter. 

1067 

1068 Args: 

1069 idx: Index of the parameter. 

1070 target_shape: Target shape (sharded_size). 

1071 

1072 Returns: 

1073 A view of the reduced gradient with target_shape. 

1074 """ 

1075 flat_view = self.get_param_buffer_view(idx) 

1076 return flat_view.view(target_shape) 

1077 

1078 def accumulate_existing_grads_to_buffer(self) -> None: 

1079 """Accumulate existing sharded_param.grad/main_grad to fused_buffer. 

1080 

1081 This is called before allreduce in gradient accumulation scenario, 

1082 to ensure the previously accumulated gradients (from n-1 mini steps) 

1083 are included in the allreduce operation. 

1084 

1085 Handles: 

1086 - Mixed-precision: uses reduce_dtype for consistency 

1087 - main_grad vs grad: respects mp_policy.apply_grad_on_fp32_main_grad 

1088 """ 

1089 if self.fused_buffer is None: 

1090 return 

1091 

1092 for idx, hsdp_param in enumerate(self.hsdp_params): 

1093 # Get existing sharded grad 

1094 existing_grad = None 

1095 if self.mp_policy is not None and self.mp_policy.apply_grad_on_fp32_main_grad: 

1096 if hasattr(hsdp_param.sharded_param, "main_grad"): 

1097 existing_grad = hsdp_param.sharded_param.main_grad 

1098 else: 

1099 existing_grad = hsdp_param.sharded_param.grad 

1100 

1101 if existing_grad is not None and not hsdp_param.accumulated_allreduced_grad: 

1102 # Get DTensor's local_tensor 

1103 from hyper_parallel.core.dtensor.dtensor import DTensor 

1104 if isinstance(existing_grad, DTensor): 

1105 existing_grad_local = existing_grad._local_tensor 

1106 else: 

1107 existing_grad_local = existing_grad 

1108 

1109 # Get the corresponding view in fused_buffer 

1110 buffer_view = self.get_param_buffer_view(idx) 

1111 

1112 # Ensure dtype consistency (convert to reduce_dtype) 

1113 if existing_grad_local.dtype != self.reduce_dtype: 

1114 existing_grad_local = existing_grad_local.to(self.reduce_dtype) 

1115 

1116 # Accumulate to fused_buffer 

1117 buffer_view.add_(existing_grad_local.view_as(buffer_view)) 

1118 if self.mp_policy is not None and self.mp_policy.apply_grad_on_fp32_main_grad: 

1119 if hasattr(hsdp_param.sharded_param, "main_grad"): 

1120 hsdp_param.sharded_param.main_grad = None 

1121 else: 

1122 hsdp_param.sharded_param.grad = None 

1123 

1124 def issue_async_allreduce(self) -> None: 

1125 """Issue async all_reduce on the fused buffer. 

1126 

1127 Uses SUM operation for numerical correctness with padding. 

1128 If original op was AVG, scaling is done when applying gradients. 

1129 """ 

1130 if self.fused_buffer is None: 

1131 raise RuntimeError("Fused buffer not allocated.") 

1132 

1133 # Always use SUM for correctness with padding regions 

1134 # If original op was AVG, we divide by world_size when applying 

1135 self.all_reduce_handle = dist.all_reduce( 

1136 self.fused_buffer, 

1137 op=dist.ReduceOp.SUM, 

1138 group=self.replicate_group, 

1139 async_op=True, 

1140 ) 

1141 

1142 def wait_and_apply_grads(self) -> bool: 

1143 """Wait for all_reduce to complete and apply gradients to parameters. 

1144 

1145 Returns: 

1146 True if CPU synchronization is needed (for offload params). 

1147 """ 

1148 if self.all_reduce_handle is not None: 

1149 self.all_reduce_handle.wait() 

1150 self.all_reduce_handle = None 

1151 

1152 need_synchronize = False 

1153 

1154 for idx, hsdp_param in enumerate(self.hsdp_params): 

1155 # Get the reduced gradient from fused buffer 

1156 reduced_grad = self.get_param_grad_view(idx, hsdp_param.sharded_size) 

1157 

1158 # Apply manual averaging if original op was AVG 

1159 if self.reduce_op == dist.ReduceOp.AVG and self.replicate_world_size > 1: 

1160 reduced_grad = reduced_grad / self.replicate_world_size 

1161 # Apply to parameter (handles dtype cast, CPU offload, accumulation) 

1162 need_synchronize = hsdp_param.apply_reduced_grad( 

1163 reduced_grad, self.orig_dtypes[idx] 

1164 ) or need_synchronize 

1165 hsdp_param.accumulated_allreduced_grad = True 

1166 

1167 # Release fused buffer 

1168 self.fused_buffer = None 

1169 

1170 return need_synchronize