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

515 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# 

15# Adapted from https://github.com/pytorch/pytorch/blob/release/2.6/torch/distributed/fsdp/_fully_shard 

16# ========================================================================== 

17"""Fused parameter communication for Torch fully_shard. 

18 

19Instead of issuing one collective per parameter, the managed parameters of a 

20fully_shard unit are packed into contiguous buckets and communicated with a 

21single collective each, which cuts kernel-launch overhead and improves 

22bandwidth utilization. 

23 

24Bucketing: parameters are not fused into one global buffer. Each collective 

25splits its parameters into buckets keyed by the attributes that must be uniform 

26within one call -- ``(process group, dtype)`` -- because a collective cannot mix 

27communication groups or element types: 

28 

29- ``AllGatherBucket``: one fused all-gather, keyed by (shard group, param dtype). 

30 Owns the optional zero-copy ``flat_param_buffer`` that parameter shards are 

31 rebased onto, so the gather reads parameter storage directly. 

32- ``ReduceScatterBucket``: one fused reduce-scatter, keyed by (shard group, 

33 reduce dtype). Packs gradients whose shard dim may differ per parameter. 

34- ``AllReduceBucket``: the optional HSDP/replicate stage paired with one 

35 reduce-scatter bucket; it takes over the completed RS output and reduces it 

36 in place without per-parameter repacking. 

37 

38``ParamGroupCommCtx`` tracks which ``HSDPParamGroup`` owns each in-flight backward 

39stage so the next module's backward can wait on it, giving the three-way 

40overlap: layer N reduce-scatter with layer N-1 backward compute, and layer N 

41all-reduce with layer N-1 reduce-scatter. 

42""" 

43from contextlib import ExitStack 

44from dataclasses import dataclass 

45from typing import List, Optional 

46 

47import torch 

48import torch.distributed as dist 

49 

50from hyper_parallel.core.fully_shard.hsdp_utils import apply_gradient_scaling_factor 

51from hyper_parallel.core.fully_shard.hsdp_scheduler import ParamGroupCommCtx 

52from hyper_parallel.core.fully_shard.utils import DDPMeshInfo, FSDPMeshInfo 

53from hyper_parallel.core.fully_shard.hsdp_param import HSDPParamV2 

54 

55 

56@dataclass 

57class AllGatherMetadata: 

58 """Describe the rank-local layout of one fused all-gather bucket.""" 

59 

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

61 param_input_numels: list[list[int]] 

62 dtype: torch.dtype 

63 inp_split_sizes: list[int] 

64 total_input_numel: int 

65 

66 

67@dataclass 

68class AllGatherResult: 

69 """Keep one all-gather input, output, and handle alive until copy-out.""" 

70 

71 all_gather_input: Optional[torch.Tensor] 

72 all_gather_output: Optional[torch.Tensor] 

73 handle: Optional[dist.Work] 

74 

75 

76@dataclass 

77class AllGatherBucket: 

78 """Own parameters and buffers sharing one all-gather group and dtype.""" 

79 

80 hsdp_params: list[HSDPParamV2] 

81 shard_group: dist.ProcessGroup 

82 shard_rank: int 

83 shard_world_size: int 

84 dtype: torch.dtype 

85 metadata: AllGatherMetadata 

86 all_gather_result: Optional[AllGatherResult] = None 

87 flat_param_buffer: Optional[torch.Tensor] = None 

88 

89 def init_flat_param_buffer(self, device: Optional[torch.device]) -> None: 

90 """Rebase this bucket's homogeneous shards into one persistent flat buffer. 

91 

92 Side effect: rebinds every parameter's ``_sharded_param_data`` and 

93 ``sharded_param`` onto a view of the new buffer, so the original shard 

94 storages are dropped. Leaves ``flat_param_buffer`` as ``None`` when the 

95 bucket mixes storage dtypes or holds offloaded/meta parameters, which is 

96 the signal that the zero-copy path does not apply. 

97 """ 

98 storage_dtype = self.hsdp_params[0]._sharded_param_data.dtype 

99 if any( 

100 hsdp_param._sharded_param_data.dtype != storage_dtype 

101 for hsdp_param in self.hsdp_params[1:] 

102 ): 

103 self.flat_param_buffer = None 

104 return 

105 if any( 

106 hsdp_param.offload_to_cpu or hsdp_param.sharded_param.device.type == "meta" 

107 for hsdp_param in self.hsdp_params 

108 ): 

109 self.flat_param_buffer = None 

110 return 

111 

112 total_numel = sum(hsdp_param._sharded_param_data.numel() for hsdp_param in self.hsdp_params) 

113 flat_param_buffer = torch.empty(total_numel, dtype=storage_dtype, device=device) 

114 flat_offset = 0 

115 for hsdp_param in self.hsdp_params: 

116 param_numel = hsdp_param._sharded_param_data.numel() 

117 flat_param_view = flat_param_buffer.narrow(0, flat_offset, param_numel) 

118 flat_param_view.copy_(hsdp_param._sharded_param_data) 

119 hsdp_param._sharded_param_data = flat_param_view 

120 padded_local_param = flat_param_view.view(hsdp_param.padded_sharded_param_size) 

121 local_param = padded_local_param.narrow( 

122 hsdp_param.hsdp_placement.dim, 

123 0, 

124 hsdp_param.sharded_size[hsdp_param.hsdp_placement.dim], 

125 ) 

126 requires_grad = hsdp_param.sharded_param.requires_grad 

127 hsdp_param.sharded_param._local_tensor = local_param 

128 hsdp_param.sharded_param.data = local_param 

129 if requires_grad: 

130 local_param.requires_grad_(True) 

131 hsdp_param.sharded_param.requires_grad_(True) 

132 flat_offset += param_numel 

133 self.flat_param_buffer = flat_param_buffer 

134 

135 def is_flat_buffer_valid(self) -> bool: 

136 """Return whether this bucket's parameter shards still use its flat storage. 

137 

138 Parameter storage can be replaced behind this bucket's back (optimizer 

139 surgery, ``load_state_dict``), which silently invalidates the zero-copy 

140 rebase; comparing storage pointers is what detects that. 

141 """ 

142 if self.flat_param_buffer is None: 

143 return False 

144 flat_storage_ptr = self.flat_param_buffer.untyped_storage().data_ptr() 

145 return all( 

146 hsdp_param._sharded_param_data.untyped_storage().data_ptr() == flat_storage_ptr 

147 for hsdp_param in self.hsdp_params 

148 ) 

149 

150 @torch.no_grad() 

151 def copy_out(self) -> None: 

152 """Wait this bucket's all-gather and copy it into stable unsharded buffers. 

153 

154 Blocks on the in-flight handle, then releases the bucket's all-gather 

155 input/output references so the fused communication buffers can be freed 

156 before the next bucket runs. No-op when the bucket has no pending result. 

157 """ 

158 all_gather_result = self.all_gather_result 

159 if all_gather_result is None or all_gather_result.all_gather_output is None: 

160 return 

161 if all_gather_result.handle is not None: 

162 all_gather_result.handle.wait() 

163 all_gather_result.handle = None 

164 all_gather_output = all_gather_result.all_gather_output 

165 metadata = self.metadata 

166 split_with_sizes_out = [] 

167 for input_numels, input_dtypes, hsdp_param in zip( 

168 metadata.param_input_numels, 

169 metadata.param_input_dtypes, 

170 self.hsdp_params, 

171 ): 

172 hsdp_param.init_unsharded_param_buffers( 

173 input_numels, 

174 input_dtypes, 

175 self.shard_world_size, 

176 all_gather_output.device, 

177 ) 

178 hsdp_param.alloc_unsharded_param_buffers() 

179 split_with_sizes_out.extend(hsdp_param.unsharded_param_buffers) 

180 

181 gathered_rows = all_gather_output.view(self.shard_world_size, -1) 

182 output_rows = [ 

183 output_buffer.view(self.shard_world_size, -1) 

184 for output_buffer in split_with_sizes_out 

185 ] 

186 non_inference_outputs = [output for output in output_rows if not output.is_inference()] 

187 # PyTorch 2.6 accepts only one tensor per context manager. no_grad does 

188 # not suppress version-counter bumps from the copy-out operations. 

189 # pylint: disable=W0212 

190 with ExitStack() as stack: 

191 for output in non_inference_outputs: 

192 stack.enter_context(torch.autograd._unsafe_preserve_version_counter(output)) 

193 if all( 

194 hsdp_param.hsdp_placement.dim == 0 

195 for hsdp_param in self.hsdp_params 

196 ): 

197 torch.split_with_sizes_copy( 

198 gathered_rows, 

199 metadata.inp_split_sizes, 

200 dim=1, 

201 out=output_rows, 

202 ) 

203 else: 

204 column_offset = 0 

205 for input_numels, hsdp_param in zip( 

206 metadata.param_input_numels, 

207 self.hsdp_params, 

208 ): 

209 if hsdp_param.hsdp_placement.dim != 0 and len(input_numels) != 1: 

210 raise NotImplementedError( 

211 "Fused non-dim-0 all-gather expects one local shard tensor per parameter." 

212 ) 

213 for input_numel, output_buffer in zip( 

214 input_numels, 

215 hsdp_param.unsharded_param_buffers, 

216 ): 

217 gathered_param = gathered_rows.narrow(1, column_offset, input_numel) 

218 if hsdp_param.hsdp_placement.dim == 0: 

219 output_buffer.view(self.shard_world_size, -1).copy_(gathered_param) 

220 else: 

221 packed_shape = list(hsdp_param.sharded_size) 

222 packed_shape[0] *= self.shard_world_size 

223 packed_param = gathered_param.contiguous().view(packed_shape) 

224 param_chunks = torch.chunk( 

225 packed_param, 

226 self.shard_world_size, 

227 dim=0, 

228 ) 

229 torch.cat( 

230 param_chunks, 

231 dim=hsdp_param.hsdp_placement.dim, 

232 out=output_buffer.view(hsdp_param._orig_size), 

233 ) 

234 column_offset += input_numel 

235 if column_offset != gathered_rows.size(1): 

236 raise AssertionError( 

237 "Fused all-gather copy-out consumed an unexpected number of elements: " 

238 f"{column_offset} != {gathered_rows.size(1)}" 

239 ) 

240 all_gather_result.all_gather_input = None 

241 all_gather_result.all_gather_output = None 

242 self.all_gather_result = None 

243 

244 

245@dataclass 

246class GradientBucketLayout: 

247 """Describe the shared per-parameter layout of an RS-to-AR bucket chain.""" 

248 

249 hsdp_params: list[HSDPParamV2] 

250 param_offsets: list[int] 

251 param_numels: list[int] 

252 total_numel: int 

253 

254 

255@dataclass 

256class ReduceScatterBucket: 

257 """Own one fused reduce-scatter operation and its temporary buffers. 

258 

259 Bucket construction records only communication metadata and the source 

260 gradients. Buffer preparation, gradient scaling, collective launch, and 

261 final result placement are separate execution phases on ``HSDPParamGroup``. 

262 

263 The shard group plus reduce dtype is this bucket's identity across 

264 micro-steps and keys its parked partial output. 

265 """ 

266 

267 layout: GradientBucketLayout 

268 unsharded_grads: list[torch.Tensor] 

269 shard_group: Optional[dist.ProcessGroup] 

270 shard_world_size: int 

271 dtype: torch.dtype 

272 reduce_op: dist.ReduceOp 

273 needs_avg_div: bool 

274 reduce_scatter_input: Optional[torch.Tensor] = None 

275 reduce_scatter_output: Optional[torch.Tensor] = None 

276 handle: Optional[dist.Work] = None 

277 

278 @property 

279 def hsdp_params(self) -> list[HSDPParamV2]: 

280 """Return parameters in their fused-buffer order.""" 

281 return self.layout.hsdp_params 

282 

283 @property 

284 def param_offsets(self) -> list[int]: 

285 """Return parameter offsets in the reduce-scatter output.""" 

286 return self.layout.param_offsets 

287 

288 @property 

289 def bucket_key(self) -> tuple: 

290 """Identity of this bucket's reduce-scatter fusion class.""" 

291 return (id(self.shard_group), self.dtype) 

292 

293 @property 

294 def uses_collective(self) -> bool: 

295 """Return whether this bucket needs an actual reduce-scatter call.""" 

296 return self.shard_group is not None and self.shard_world_size > 1 

297 

298 def move_reducescatter_output(self) -> torch.Tensor: 

299 """Transfer exclusive ownership of the completed output to the next stage.""" 

300 if self.reduce_scatter_output is None: 

301 raise RuntimeError("Reduce-scatter output has already been released.") 

302 reduce_scatter_output = self.reduce_scatter_output 

303 self.reduce_scatter_output = None 

304 return reduce_scatter_output 

305 

306 

307@dataclass 

308class AllReduceBucket: 

309 """Own the final stage of one fused RS-to-AR bucket chain. 

310 

311 The bucket shares its parameter layout with exactly one source 

312 reduce-scatter bucket. Once RS completes, it takes over that bucket's whole 

313 output tensor and all-reduces it in place without per-parameter repacking. 

314 """ 

315 

316 layout: GradientBucketLayout 

317 source_reduce_scatter_bucket: ReduceScatterBucket 

318 replicate_group: dist.ProcessGroup 

319 replicate_world_size: int 

320 dtype: torch.dtype 

321 reduce_op: dist.ReduceOp 

322 needs_avg_div: bool 

323 all_reduce_output: Optional[torch.Tensor] = None 

324 handle: Optional[dist.Work] = None 

325 

326 @property 

327 def hsdp_params(self) -> list[HSDPParamV2]: 

328 """Return parameters in their fused-buffer order.""" 

329 return self.layout.hsdp_params 

330 

331 @property 

332 def param_numels(self) -> list[int]: 

333 """Return padded parameter sizes in the all-reduce output.""" 

334 return self.layout.param_numels 

335 

336 @property 

337 def param_offsets(self) -> list[int]: 

338 """Return parameter offsets in the all-reduce output.""" 

339 return self.layout.param_offsets 

340 

341 @property 

342 def uses_collective(self) -> bool: 

343 """Return whether this bucket needs an actual all-reduce call.""" 

344 return self.replicate_world_size > 1 

345 

346 

347def get_all_gather_metadata(hsdp_params: list[HSDPParamV2]) -> AllGatherMetadata: 

348 """Build metadata for parameters with one all-gather communication dtype.""" 

349 param_input_dtypes = [] 

350 param_input_numels = [] 

351 inp_split_sizes = [] 

352 total_input_numel = 0 

353 dtype = None 

354 for hsdp_param in hsdp_params: 

355 inputs = hsdp_param.all_gather_inputs 

356 if dtype is None: 

357 dtype = inputs[0].dtype 

358 if any(tensor.dtype != dtype for tensor in inputs): 

359 raise ValueError("All parameters in an all-gather bucket must have the same dtype.") 

360 input_dtypes = [tensor.dtype for tensor in inputs] 

361 input_numels = [tensor.numel() for tensor in inputs] 

362 param_input_dtypes.append(input_dtypes) 

363 param_input_numels.append(input_numels) 

364 inp_split_sizes.extend(input_numels) 

365 total_input_numel += sum(input_numels) 

366 if dtype is None: 

367 raise ValueError("Cannot build all-gather metadata for an empty parameter bucket.") 

368 return AllGatherMetadata( 

369 param_input_dtypes=param_input_dtypes, 

370 param_input_numels=param_input_numels, 

371 dtype=dtype, 

372 inp_split_sizes=inp_split_sizes, 

373 total_input_numel=total_input_numel, 

374 ) 

375 

376 

377def all_gather_copy_in( 

378 all_gather_inputs: list[torch.Tensor], 

379 all_gather_output: torch.Tensor, 

380 inp_split_sizes: list[int], 

381 all_gather_input_numel: int, 

382 rank: int, 

383) -> tuple[torch.Tensor, torch.Tensor]: 

384 """Copy local parameter shards into this rank's fused output slice.""" 

385 all_gather_input = all_gather_output.narrow( 

386 0, 

387 all_gather_input_numel * rank, 

388 all_gather_input_numel, 

389 ) 

390 copy_destinations = torch.split(all_gather_input, inp_split_sizes) 

391 with torch.no_grad(): 

392 # pylint: disable=W0212 

393 torch._foreach_copy_(copy_destinations, all_gather_inputs) 

394 return all_gather_input, all_gather_output 

395 

396 

397def reduce_scatter_copy_in( 

398 hsdp_params: list[HSDPParamV2], 

399 unsharded_grads: list[torch.Tensor], 

400 reduce_scatter_input: torch.Tensor, 

401 world_size: int, 

402) -> None: 

403 """Pack gradients with mixed shard dimensions into a fused RS input.""" 

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

405 raise AssertionError( 

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

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

408 ) 

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

410 column_offset = 0 

411 with torch.no_grad(): 

412 for hsdp_param, unsharded_grad in zip(hsdp_params, unsharded_grads): 

413 padded_shard_numel = hsdp_param.padded_sharded_param_size.numel() 

414 packed_grad_slot = packed_rows.narrow(1, column_offset, padded_shard_numel) 

415 shard_dim = hsdp_param.hsdp_placement.dim 

416 if shard_dim == 0: 

417 # pylint: disable=W0212 

418 torch._chunk_cat( 

419 [unsharded_grad], 

420 dim=0, 

421 num_chunks=world_size, 

422 out=packed_grad_slot, 

423 ) 

424 else: 

425 grad_chunks = torch.chunk(unsharded_grad, world_size, dim=shard_dim) 

426 packed_grad = torch.cat(grad_chunks, dim=0).contiguous().view(world_size, -1) 

427 packed_grad_slot.copy_(packed_grad) 

428 column_offset += padded_shard_numel 

429 if column_offset != packed_rows.size(1): 

430 raise AssertionError( 

431 "reduce_scatter_copy_in packed an unexpected number of elements: " 

432 f"{column_offset} != {packed_rows.size(1)}" 

433 ) 

434 

435 

436class HSDPParamGroup: 

437 """Fuse communication for the managed parameters in one fully_shard unit. 

438 

439 Packs parameter shards into contiguous buckets so communication is issued 

440 once per compatible route and dtype instead of once per parameter. 

441 

442 Lifecycle within one training iteration: 

443 1. Forward -- ``unshard()`` builds the all-gather buckets on first use 

444 and issues one ``all_gather_into_tensor`` per bucket. 

445 2. Forward (wait) -- ``wait_for_unshard()`` waits each bucket and copies 

446 the gathered data out into the per-parameter unsharded buffers. 

447 3. Backward -- ``foreach_reduce()`` packs the unsharded gradients and 

448 issues one fused ``reduce_scatter_tensor`` per bucket, then parks 

449 itself on ``ParamGroupCommCtx`` so a later module's backward waits for it. 

450 4. Backward (finalize) -- ``wait_reduce_scatter_and_issue_all_reduce()`` 

451 waits the RS buckets and launches the HSDP all-reduces; 

452 ``wait_all_reduce_and_save_grad()`` waits those and exposes each 

453 parameter's reduced-gradient view. 

454 

455 Steps 3 and 4 are deliberately split across modules: that is what allows one 

456 module's communication to overlap the next module's backward compute. The 

457 root backward hook drains whatever is still parked when backward ends. 

458 

459 Gradient accumulation is held at bucket granularity in 

460 ``reduce_partial_outputs``, keyed by the reduce-scatter bucket key. A 

461 micro-step with ``requires_all_reduce=False`` keeps its whole reduce-scatter 

462 output there and skips the all-reduce entirely; the next synchronizing 

463 micro-step adds it back with a single whole-buffer ``add_`` and lets the 

464 fused all-reduce see the total. The dict lives on the group rather than on 

465 the bucket because ``foreach_reduce`` rebuilds the buckets every micro-step, 

466 and holds whole buffers rather than per-parameter views so accumulation 

467 costs one kernel per bucket instead of one per parameter. 

468 """ 

469 

470 def __init__( 

471 self, 

472 hsdp_params: list[HSDPParamV2], 

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

474 enable_zero_copy: bool = True, 

475 comm_ctx: Optional[ParamGroupCommCtx] = None, 

476 ) -> None: 

477 self.device = device 

478 self.hsdp_params = hsdp_params 

479 self.comm_ctx = comm_ctx or ParamGroupCommCtx() 

480 self.enable_zero_copy = enable_zero_copy 

481 self.gradient_scaling_factor = None 

482 self.requires_all_reduce = True 

483 self.all_gather_buckets: list[AllGatherBucket] = [] 

484 self.reduce_scatter_buckets: list[ReduceScatterBucket] = [] 

485 self.all_reduce_buckets: list[AllReduceBucket] = [] 

486 self.reduce_partial_outputs: dict[tuple, torch.Tensor] = {} 

487 

488 def _init_all_gather_buckets(self) -> None: 

489 """Build ordered all-gather buckets from each parameter's communication facts.""" 

490 params_by_bucket = {} 

491 bucket_groups = {} 

492 for hsdp_param in self.hsdp_params: 

493 if hsdp_param.shard_world_size <= 1: 

494 continue 

495 if not isinstance(hsdp_param.mesh_info, FSDPMeshInfo): 

496 raise ValueError( 

497 f"Fused all-gather expects FSDPMeshInfo, got {type(hsdp_param.mesh_info)}" 

498 ) 

499 shard_group = hsdp_param.mesh_info.shard_process_group 

500 communication_dtype = hsdp_param.param_dtype or hsdp_param.orig_dtype 

501 bucket_key = (id(shard_group), communication_dtype) 

502 if bucket_key not in params_by_bucket: 

503 params_by_bucket[bucket_key] = [] 

504 bucket_groups[bucket_key] = shard_group 

505 params_by_bucket[bucket_key].append(hsdp_param) 

506 

507 self.all_gather_buckets = [] 

508 for bucket_key, hsdp_params in params_by_bucket.items(): 

509 shard_group = bucket_groups[bucket_key] 

510 param_input_numels = [ 

511 [hsdp_param._sharded_param_data.numel()] for hsdp_param in hsdp_params 

512 ] 

513 inp_split_sizes = [input_numels[0] for input_numels in param_input_numels] 

514 allgather_metadata = AllGatherMetadata( 

515 param_input_dtypes=[[bucket_key[1]] for _ in hsdp_params], 

516 param_input_numels=param_input_numels, 

517 dtype=bucket_key[1], 

518 inp_split_sizes=inp_split_sizes, 

519 total_input_numel=sum(inp_split_sizes), 

520 ) 

521 self.all_gather_buckets.append( 

522 AllGatherBucket( 

523 hsdp_params=hsdp_params, 

524 shard_group=shard_group, 

525 shard_rank=shard_group.rank(), 

526 shard_world_size=shard_group.size(), 

527 dtype=bucket_key[1], 

528 metadata=allgather_metadata, 

529 ) 

530 ) 

531 

532 def unshard(self, async_op: bool = False) -> None: 

533 """Launch fused all-gathers for every communication bucket.""" 

534 if self.all_gather_buckets and any( 

535 bucket.all_gather_result is not None for bucket in self.all_gather_buckets 

536 ): 

537 # already triggered by pre_module.prefetch(), return directly. 

538 return 

539 self.foreach_all_gather(async_op) 

540 

541 @torch.no_grad() 

542 def foreach_all_gather(self, async_op: bool = False) -> None: 

543 """Initialize ordered buckets and launch their all-gathers.""" 

544 if not self.all_gather_buckets: 

545 self._init_all_gather_buckets() 

546 for hsdp_param in self.hsdp_params: 

547 if hsdp_param.shard_world_size <= 1: 

548 # fast path, skip copy_in process. 

549 hsdp_param.unshard(async_op) 

550 for all_gather_bucket in self.all_gather_buckets: 

551 if all_gather_bucket.all_gather_result is not None: 

552 continue 

553 if self.enable_zero_copy and not all_gather_bucket.is_flat_buffer_valid(): 

554 all_gather_bucket.init_flat_param_buffer(self.device) 

555 

556 metadata = all_gather_bucket.metadata 

557 all_gather_output = torch.empty( 

558 metadata.total_input_numel * all_gather_bucket.shard_world_size, 

559 dtype=all_gather_bucket.dtype, 

560 device=self.device, 

561 ) 

562 if self.enable_zero_copy and all_gather_bucket.is_flat_buffer_valid(): 

563 if all_gather_bucket.flat_param_buffer.dtype == all_gather_bucket.dtype: 

564 all_gather_input = all_gather_bucket.flat_param_buffer 

565 else: 

566 all_gather_input = all_gather_bucket.flat_param_buffer.to(all_gather_bucket.dtype) 

567 else: 

568 all_gather_inputs = [] 

569 for hsdp_param in all_gather_bucket.hsdp_params: 

570 all_gather_inputs.extend(hsdp_param.all_gather_inputs) 

571 all_gather_input, all_gather_output = all_gather_copy_in( 

572 all_gather_inputs, 

573 all_gather_output, 

574 metadata.inp_split_sizes, 

575 metadata.total_input_numel, 

576 all_gather_bucket.shard_rank, 

577 ) 

578 

579 handle = dist.all_gather_into_tensor( 

580 all_gather_output, 

581 all_gather_input, 

582 group=all_gather_bucket.shard_group, 

583 async_op=async_op, 

584 ) 

585 all_gather_bucket.all_gather_result = AllGatherResult( 

586 all_gather_input=all_gather_input, 

587 all_gather_output=all_gather_output, 

588 handle=handle, 

589 ) 

590 

591 def wait_for_unshard(self) -> None: 

592 """Wait all fused all-gathers and install stable unsharded parameters.""" 

593 for all_gather_bucket in self.all_gather_buckets: 

594 all_gather_bucket.copy_out() 

595 for hsdp_param in self.hsdp_params: 

596 hsdp_param.wait_for_unshard() 

597 

598 def _build_reduce_scatter_buckets( 

599 self, 

600 reduce_scatter_reduce_op: dist.ReduceOp, 

601 ) -> list[ReduceScatterBucket]: 

602 """Build ordered reduce-scatter metadata for active gradients. 

603 

604 This method deliberately has no tensor or communication side effects: 

605 it does not allocate buffers, pack gradients, apply scaling, clear 

606 source gradients, or launch collectives. 

607 """ 

608 params_by_bucket = {} 

609 grads_by_bucket = {} 

610 bucket_groups = {} 

611 for hsdp_param in self.hsdp_params: 

612 if not hsdp_param.sharded_param.requires_grad: 

613 continue 

614 if hsdp_param.unsharded_accumulated_grad is not None: 

615 unsharded_grad = hsdp_param.unsharded_accumulated_grad_data 

616 elif hsdp_param.unsharded_param.grad is not None: 

617 unsharded_grad = hsdp_param.unsharded_grad_data 

618 else: 

619 continue 

620 shard_group = ( 

621 hsdp_param.mesh_info.shard_process_group 

622 if isinstance(hsdp_param.mesh_info, FSDPMeshInfo) 

623 else None 

624 ) 

625 reduce_dtype = hsdp_param.reduce_comm_dtype(unsharded_grad) 

626 bucket_key = (id(shard_group), reduce_dtype) 

627 if bucket_key not in params_by_bucket: 

628 params_by_bucket[bucket_key] = [] 

629 grads_by_bucket[bucket_key] = [] 

630 bucket_groups[bucket_key] = shard_group 

631 params_by_bucket[bucket_key].append(hsdp_param) 

632 grads_by_bucket[bucket_key].append(unsharded_grad) 

633 

634 reduce_scatter_buckets = [] 

635 for bucket_key, hsdp_params in params_by_bucket.items(): 

636 shard_group = bucket_groups[bucket_key] 

637 shard_world_size = hsdp_params[0].shard_world_size 

638 if any( 

639 hsdp_param.shard_world_size != shard_world_size 

640 for hsdp_param in hsdp_params 

641 ): 

642 raise ValueError("A reduce-scatter bucket must use one shard world size.") 

643 needs_avg_div = reduce_scatter_reduce_op == dist.ReduceOp.AVG 

644 communication_op = dist.ReduceOp.SUM if needs_avg_div else reduce_scatter_reduce_op 

645 param_offsets = [] 

646 param_numels = [] 

647 flat_offset = 0 

648 for hsdp_param in hsdp_params: 

649 param_numel = hsdp_param.padded_sharded_param_size.numel() 

650 param_offsets.append(flat_offset) 

651 param_numels.append(param_numel) 

652 flat_offset += param_numel 

653 reduce_scatter_buckets.append( 

654 ReduceScatterBucket( 

655 layout=self._build_gradient_bucket_layout_from(hsdp_params), 

656 unsharded_grads=grads_by_bucket[bucket_key], 

657 shard_group=shard_group, 

658 shard_world_size=shard_world_size, 

659 dtype=bucket_key[1], 

660 reduce_op=communication_op, 

661 needs_avg_div=needs_avg_div, 

662 ) 

663 ) 

664 return reduce_scatter_buckets 

665 

666 @staticmethod 

667 def _build_gradient_bucket_layout_from(hsdp_params): 

668 param_offsets = [] 

669 param_numels = [] 

670 flat_offset = 0 

671 for hsdp_param in hsdp_params: 

672 param_numel = hsdp_param.padded_sharded_param_size.numel() 

673 param_offsets.append(flat_offset) 

674 param_numels.append(param_numel) 

675 flat_offset += param_numel 

676 return GradientBucketLayout( 

677 hsdp_params, 

678 param_offsets, 

679 param_numels, 

680 flat_offset 

681 ) 

682 

683 @staticmethod 

684 def _build_all_reduce_buckets( 

685 reduce_scatter_buckets: list[ReduceScatterBucket], 

686 ) -> list[AllReduceBucket]: 

687 """Build one optional all-reduce stage for each RS bucket.""" 

688 all_reduce_buckets = [] 

689 for reduce_scatter_bucket in reduce_scatter_buckets: 

690 representative_param = reduce_scatter_bucket.hsdp_params[0] 

691 if not isinstance(representative_param.mesh_info, DDPMeshInfo): 

692 # FSDP, no need allreduce on replicate mesh dim. skip build relative allreduce-bucket. 

693 if any( 

694 isinstance(hsdp_param.mesh_info, DDPMeshInfo) 

695 for hsdp_param in reduce_scatter_bucket.hsdp_params[1:] 

696 ): 

697 raise ValueError( 

698 "A reduce-scatter bucket cannot mix parameters with and without " 

699 "a subsequent all-reduce." 

700 ) 

701 continue 

702 replicate_group = representative_param.mesh_info.replicate_process_group 

703 replicate_world_size = representative_param.replicate_world_size 

704 for hsdp_param in reduce_scatter_bucket.hsdp_params[1:]: 

705 if ( 

706 not isinstance(hsdp_param.mesh_info, DDPMeshInfo) 

707 or hsdp_param.mesh_info.replicate_process_group is not replicate_group 

708 or hsdp_param.replicate_world_size != replicate_world_size 

709 ): 

710 raise ValueError( 

711 "All parameters in a reduce-scatter bucket must share one " 

712 "subsequent all-reduce group." 

713 ) 

714 all_reduce_buckets.append( 

715 AllReduceBucket( 

716 layout=reduce_scatter_bucket.layout, 

717 source_reduce_scatter_bucket=reduce_scatter_bucket, 

718 replicate_group=replicate_group, 

719 replicate_world_size=replicate_world_size, 

720 dtype=reduce_scatter_bucket.dtype, 

721 reduce_op=reduce_scatter_bucket.reduce_op, 

722 needs_avg_div=reduce_scatter_bucket.needs_avg_div, 

723 ) 

724 ) 

725 return all_reduce_buckets 

726 

727 def _issue_reduce_scatter_buckets(self, async_op: bool) -> None: 

728 """Prepare RS buffers, release source gradients, and launch communication.""" 

729 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

730 reduce_scatter_input = torch.empty( 

731 reduce_scatter_bucket.layout.total_numel * reduce_scatter_bucket.shard_world_size, 

732 dtype=reduce_scatter_bucket.dtype, 

733 device=reduce_scatter_bucket.unsharded_grads[0].device, 

734 ) 

735 reduce_scatter_copy_in( 

736 reduce_scatter_bucket.hsdp_params, 

737 reduce_scatter_bucket.unsharded_grads, 

738 reduce_scatter_input, 

739 reduce_scatter_bucket.shard_world_size, 

740 ) 

741 apply_gradient_scaling_factor(reduce_scatter_input, self.gradient_scaling_factor) 

742 reduce_scatter_bucket.reduce_scatter_input = reduce_scatter_input 

743 if not reduce_scatter_bucket.uses_collective: 

744 reduce_scatter_bucket.reduce_scatter_output = reduce_scatter_input 

745 else: 

746 reduce_scatter_bucket.reduce_scatter_output = reduce_scatter_input.new_empty( 

747 reduce_scatter_bucket.layout.total_numel 

748 ) 

749 

750 for hsdp_param in reduce_scatter_bucket.hsdp_params: 

751 if hsdp_param.unsharded_accumulated_grad is not None: 

752 hsdp_param.unsharded_accumulated_grad = None 

753 else: 

754 hsdp_param.unsharded_param.grad = None 

755 reduce_scatter_bucket.unsharded_grads = [] 

756 if not reduce_scatter_bucket.uses_collective: 

757 reduce_scatter_bucket.handle = None 

758 continue 

759 reduce_scatter_bucket.handle = dist.reduce_scatter_tensor( 

760 output=reduce_scatter_bucket.reduce_scatter_output, 

761 input=reduce_scatter_bucket.reduce_scatter_input, 

762 group=reduce_scatter_bucket.shard_group, 

763 op=reduce_scatter_bucket.reduce_op, 

764 async_op=async_op, 

765 ) 

766 

767 @torch.no_grad() 

768 def foreach_reducescatter( 

769 self, 

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

771 async_op: bool = True, 

772 ) -> Optional[torch.Tensor]: 

773 """Launch fused reduce-scatter buckets for this module's gradients.""" 

774 self.reduce_scatter_buckets = self._build_reduce_scatter_buckets( 

775 reduce_scatter_reduce_op, 

776 ) 

777 if not self.reduce_scatter_buckets: 

778 return 

779 self.all_reduce_buckets = self._build_all_reduce_buckets(self.reduce_scatter_buckets) 

780 self._issue_reduce_scatter_buckets(async_op) 

781 if async_op: 

782 self.comm_ctx.pre_param_group = self 

783 else: 

784 self.wait_reduce_scatter_and_issue_all_reduce(async_op=False) 

785 self.wait_all_reduce_and_save_grad() 

786 return 

787 

788 def _wait_reduce_scatter_buckets(self) -> None: 

789 """Wait prepared RS buckets and finish shard-dimension averaging.""" 

790 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

791 if reduce_scatter_bucket.handle is not None: 

792 reduce_scatter_bucket.handle.wait() 

793 reduce_scatter_bucket.handle = None 

794 reduce_scatter_bucket.reduce_scatter_input = None 

795 if reduce_scatter_bucket.reduce_scatter_output is None: 

796 raise RuntimeError("Reduce-scatter bucket has not been prepared.") 

797 if reduce_scatter_bucket.needs_avg_div and reduce_scatter_bucket.shard_world_size > 1: 

798 reduce_scatter_bucket.reduce_scatter_output.div_( 

799 reduce_scatter_bucket.shard_world_size 

800 ) 

801 

802 

803 @staticmethod 

804 def _issue_all_reduce_buckets( 

805 all_reduce_buckets: list[AllReduceBucket], 

806 async_op: bool, 

807 ) -> None: 

808 """Launch in-place AR for bucket outputs transferred from RS.""" 

809 for all_reduce_bucket in all_reduce_buckets: 

810 if all_reduce_bucket.all_reduce_output is None: 

811 raise RuntimeError("All-reduce bucket has not received its reduce-scatter output.") 

812 if not all_reduce_bucket.uses_collective: 

813 all_reduce_bucket.handle = None 

814 continue 

815 all_reduce_bucket.handle = dist.all_reduce( 

816 all_reduce_bucket.all_reduce_output, 

817 group=all_reduce_bucket.replicate_group, 

818 op=all_reduce_bucket.reduce_op, 

819 async_op=async_op, 

820 ) 

821 

822 def wait_reduce_scatter_and_issue_all_reduce(self, async_op: bool = True) -> None: 

823 """Wait all RS buckets and launch the resulting HSDP all-reduces.""" 

824 self._wait_reduce_scatter_buckets() 

825 # Step1: get fianal reduce_scatter output 

826 if not self.requires_all_reduce: 

827 # if set_requires_all_reduce is False, accmu grad into reduce_partial_ouptut. 

828 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

829 bucket_key = reduce_scatter_bucket.bucket_key 

830 current_output = reduce_scatter_bucket.move_reducescatter_output() 

831 reduce_partial_output = self.reduce_partial_outputs.get(bucket_key) 

832 if reduce_partial_output is None: 

833 self.reduce_partial_outputs[bucket_key] = current_output 

834 else: 

835 reduce_partial_output.add_(current_output) 

836 self.reduce_scatter_buckets = [] 

837 self.all_reduce_buckets = [] 

838 return 

839 

840 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

841 reduce_partial_output = self.reduce_partial_outputs.pop( 

842 reduce_scatter_bucket.bucket_key, 

843 None, 

844 ) 

845 if reduce_partial_output is not None: 

846 # current reduce-scattered grad add accmulated grad 

847 reduce_scatter_bucket.reduce_scatter_output.add_(reduce_partial_output) 

848 # Step2: moving reducescatter bucket lifecycle 

849 # Step2.1 build mapping from rs_bucket -> ar_bucket 

850 all_reduce_by_source = { 

851 id(all_reduce_bucket.source_reduce_scatter_bucket): all_reduce_bucket 

852 for all_reduce_bucket in self.all_reduce_buckets 

853 } 

854 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

855 all_reduce_bucket = all_reduce_by_source.get(id(reduce_scatter_bucket)) 

856 if all_reduce_bucket is not None: 

857 # Step2.2-A: need allreduce when HSDP 

858 all_reduce_bucket.all_reduce_output = reduce_scatter_bucket.move_reducescatter_output() 

859 continue 

860 # Step2.2-B: FSDP only, scatter fused result to each hsdp_param manage. 

861 reduce_scatter_output = reduce_scatter_bucket.move_reducescatter_output() 

862 # reduce_scatter_bucket.reducescatter_output ref decrease in move method. 

863 for hsdp_param, param_numel, param_offset in zip( 

864 reduce_scatter_bucket.hsdp_params, 

865 reduce_scatter_bucket.layout.param_numels, 

866 reduce_scatter_bucket.param_offsets, 

867 ): 

868 hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output = reduce_scatter_output.narrow( 

869 0, 

870 param_offset, 

871 param_numel, 

872 ) 

873 self.reduce_scatter_buckets = [] 

874 # Step3: luanch async allreduce buckets 

875 self._issue_all_reduce_buckets(self.all_reduce_buckets, async_op) 

876 if self.all_reduce_buckets: 

877 self.comm_ctx.all_reduce_param_group = self 

878 

879 def wait_all_reduce_and_save_grad(self) -> None: 

880 """Wait HSDP all-reduces and expose their per-parameter output views.""" 

881 for all_reduce_bucket in self.all_reduce_buckets: 

882 if all_reduce_bucket.handle is not None: 

883 all_reduce_bucket.handle.wait() 

884 all_reduce_bucket.handle = None 

885 if all_reduce_bucket.all_reduce_output is None: 

886 raise RuntimeError("All-reduce output has already been released.") 

887 if all_reduce_bucket.needs_avg_div and all_reduce_bucket.replicate_world_size > 1: 

888 all_reduce_bucket.all_reduce_output.div_(all_reduce_bucket.replicate_world_size) 

889 for hsdp_param, param_numel, param_offset in zip( 

890 all_reduce_bucket.hsdp_params, 

891 all_reduce_bucket.param_numels, 

892 all_reduce_bucket.param_offsets, 

893 ): 

894 hsdp_param.all_reduce_comm_ctx.all_reduce_output = all_reduce_bucket.all_reduce_output.narrow( 

895 0, 

896 param_offset, 

897 param_numel, 

898 ) 

899 all_reduce_bucket.all_reduce_output = None 

900 self.all_reduce_buckets = [] 

901 if self.comm_ctx.all_reduce_param_group is self: 

902 self.comm_ctx.all_reduce_param_group = None 

903 

904 def reset_iter_state(self) -> None: 

905 """Drop communication references after a completed iteration.""" 

906 for all_gather_bucket in self.all_gather_buckets: 

907 if all_gather_bucket.all_gather_result is not None: 

908 all_gather_bucket.all_gather_result.all_gather_input = None 

909 all_gather_bucket.all_gather_result.all_gather_output = None 

910 all_gather_bucket.all_gather_result.handle = None 

911 all_gather_bucket.all_gather_result = None 

912 for reduce_scatter_bucket in self.reduce_scatter_buckets: 

913 reduce_scatter_bucket.unsharded_grads = [] 

914 reduce_scatter_bucket.reduce_scatter_input = None 

915 reduce_scatter_bucket.reduce_scatter_output = None 

916 reduce_scatter_bucket.handle = None 

917 for all_reduce_bucket in self.all_reduce_buckets: 

918 all_reduce_bucket.all_reduce_output = None 

919 all_reduce_bucket.handle = None 

920 self.reduce_scatter_buckets = [] 

921 self.all_reduce_buckets = [] 

922 self.reduce_partial_outputs.clear() 

923 if self.comm_ctx.pre_param_group is self: 

924 self.comm_ctx.pre_param_group = None 

925 if self.comm_ctx.all_reduce_param_group is self: 

926 self.comm_ctx.all_reduce_param_group = None 

927 

928 

929class AllReduceParamGroup: 

930 """Group HSDP parameters by replicate group for one fused async all-reduce. 

931 

932 Used by the ``comm_fusion=False`` path only: there each parameter issues its 

933 own reduce-scatter, and this class re-fuses the resulting shards so the 

934 replicate-dimension all-reduce is still a single collective. The 

935 ``comm_fusion=True`` path never builds this group -- it fuses at the bucket 

936 level via ``AllReduceBucket``. 

937 

938 Zero-copy is achieved by pre-allocating one contiguous buffer with 512-byte 

939 alignment, having each ``reduce_scatter_grad`` write straight into an aligned 

940 view of it, all-reducing the whole buffer once, and slicing gradients back 

941 out of those views. 

942 

943 Numerical-correctness constraints (each is load-bearing, do not "simplify"): 

944 

945 - The all-reduce always uses SUM, never AVG. The buffer is padded for 

946 alignment, and averaging would divide by a world size that the zero 

947 padding does not participate in. 

948 - AVG is therefore reconstructed manually in ``wait_and_split_grads`` by 

949 dividing by ``replicate_world_size`` after the collective. 

950 - The buffer is zero-initialized so the padding regions contribute nothing 

951 to the SUM. 

952 

953 Attributes: 

954 replicate_group: Process group for the replicate dimension. 

955 hsdp_params: Parameters sharing that replicate group. 

956 reduce_dtype: Uniform element type of the fused buffer. 

957 reduce_op: Caller-requested op (AVG or SUM); decides the final scaling. 

958 replicate_world_size: Size of the replicate group. 

959 fused_buffer: Contiguous aligned buffer for all parameters; released 

960 once ``wait_and_split_grads`` has handed out its views. 

961 param_offsets: Element offset of each parameter inside the buffer. 

962 param_numels: Padded element count of each parameter. 

963 all_reduce_handle: In-flight async all-reduce work, or None. 

964 """ 

965 

966 ALIGNMENT_BYTES = 512 

967 

968 def __init__( 

969 self, 

970 replicate_group: dist.ProcessGroup, 

971 hsdp_params: List[HSDPParamV2], 

972 reduce_op: dist.ReduceOp, 

973 ) -> None: 

974 self.replicate_group = replicate_group 

975 self.hsdp_params = hsdp_params 

976 self.reduce_dtype = hsdp_params[0].reduce_comm_dtype() 

977 self.reduce_op = reduce_op 

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

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

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

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

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

983 

984 def compute_aligned_layout(self) -> int: 

985 """Compute packed parameter offsets and align the total buffer size.""" 

986 self.param_offsets = [] 

987 self.param_numels = [] 

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

989 current_offset = 0 

990 for hsdp_param in self.hsdp_params: 

991 numel = hsdp_param.padded_sharded_param_size.numel() 

992 self.param_numels.append(numel) 

993 self.param_offsets.append(current_offset) 

994 current_offset += numel 

995 total_bytes = current_offset * element_size 

996 aligned_total_bytes = ( 

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

998 ) * self.ALIGNMENT_BYTES 

999 return aligned_total_bytes // element_size 

1000 

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

1002 """Allocate and zero the fused all-reduce buffer.""" 

1003 self.fused_buffer = torch.zeros( 

1004 self.compute_aligned_layout(), 

1005 dtype=self.reduce_dtype, 

1006 device=device, 

1007 ) 

1008 

1009 def get_param_buffer_view(self, index: int) -> torch.Tensor: 

1010 """Return one parameter's padded communication view.""" 

1011 if self.fused_buffer is None: 

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

1013 return self.fused_buffer.narrow( 

1014 0, 

1015 self.param_offsets[index], 

1016 self.param_numels[index], 

1017 ) 

1018 

1019 def get_param_grad_view(self, index: int, target_shape: torch.Size) -> torch.Tensor: 

1020 """Return one parameter's actual-shard gradient view.""" 

1021 return self.get_param_buffer_view(index).narrow(0, 0, target_shape.numel()).view(target_shape) 

1022 

1023 def accumulate_reduce_partial_outputs(self) -> None: 

1024 """Merge no-all-reduce micro-step outputs into the current buffer.""" 

1025 if self.fused_buffer is None: 

1026 return 

1027 for index, hsdp_param in enumerate(self.hsdp_params): 

1028 if hsdp_param.reduce_partial_output is None: 

1029 continue 

1030 partial_output = hsdp_param.reduce_partial_output 

1031 if partial_output.dtype != self.reduce_dtype: 

1032 partial_output = partial_output.to(self.reduce_dtype) 

1033 self.get_param_buffer_view(index).add_(partial_output.view(-1)) 

1034 hsdp_param.reduce_partial_output = None 

1035 

1036 def issue_async_allreduce(self) -> None: 

1037 """Launch SUM all-reduce; AVG division is applied when splitting.""" 

1038 if self.fused_buffer is None: 

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

1040 self.all_reduce_handle = dist.all_reduce( 

1041 self.fused_buffer, 

1042 op=dist.ReduceOp.SUM, 

1043 group=self.replicate_group, 

1044 async_op=True, 

1045 ) 

1046 

1047 def wait_and_split_grads(self) -> None: 

1048 """Wait all-reduce and expose per-parameter context views.""" 

1049 if self.all_reduce_handle is not None: 

1050 self.all_reduce_handle.wait() 

1051 self.all_reduce_handle = None 

1052 for index, hsdp_param in enumerate(self.hsdp_params): 

1053 reduced_grad = self.get_param_grad_view(index, hsdp_param.sharded_size) 

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

1055 reduced_grad.div_(self.replicate_world_size) 

1056 hsdp_param.all_reduce_comm_ctx.all_reduce_output = reduced_grad 

1057 self.fused_buffer = None