Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / mindspore / fully_shard / param_group.py: 83%
541 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-06 05:41 +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"""MindSpore HSDP parameter group with fused communication."""
17from __future__ import annotations
19import math
20from dataclasses import dataclass, field
21from typing import Any, List, NamedTuple, Optional
23import mindspore as ms
24from mindspore import ops
25from mindspore.common.api import _no_grad
26import mindspore.mint.distributed as dist
27from mindspore.ops.function.comm_func import CommHandle
29from hyper_parallel.core.fully_shard.hsdp_utils import apply_gradient_scaling_factor
30from hyper_parallel.core.fully_shard.utils import DDPMeshInfo, FSDPMeshInfo, HSDPMeshInfo, MixedPrecisionPolicy
31from hyper_parallel.platform.mindspore.fully_shard._version_utils import copy_without_bumping_version
32from hyper_parallel.platform.mindspore.fully_shard.pack_utils import build_rs_plan, pack_for_reduce_scatter
33from hyper_parallel.platform.mindspore.fully_shard.param import MindSporeHSDPParamV2
36def _normalize_device(device: Any) -> str:
37 if isinstance(device, str):
38 return device.split(":", 1)[0]
39 return str(device).split(":", 1)[0]
42def _shape_numel(shape) -> int:
43 return math.prod(int(dim) for dim in shape)
46def get_all_gather_metadata(hsdp_params):
47 """Collect metadata required for fused all-gather."""
48 param_input_dtypes = []
49 param_input_numels = []
50 inp_split_sizes = []
51 total_input_numel = 0
52 first_dtype = None
54 for hsdp_param in hsdp_params:
55 inputs = hsdp_param.all_gather_inputs
56 if first_dtype is None:
57 first_dtype = inputs[0].dtype
58 elif first_dtype != inputs[0].dtype:
59 raise ValueError("All parameters in the group must have a uniform dtype.")
60 param_dtypes = [t.dtype for t in inputs]
61 param_numels = [t.numel() for t in inputs]
62 param_input_dtypes.append(param_dtypes)
63 param_input_numels.append(param_numels)
64 inp_split_sizes.extend(param_numels)
65 total_input_numel += sum(param_numels)
67 return AllGatherMetadata(
68 param_input_dtypes,
69 param_input_numels,
70 first_dtype,
71 inp_split_sizes,
72 total_input_numel,
73 )
76@dataclass
77class AllGatherMetadata:
78 """Metadata describing the fused all-gather buffer layout."""
80 param_input_dtypes: list[list[Any]]
81 param_input_numels: list[list[int]]
82 dtype: Any
83 inp_split_sizes: list[int]
84 total_input_numel: int
85 hash_key: int = field(init=False)
87 def __post_init__(self):
88 self.hash_key = hash(
89 (
90 tuple(tuple(d) for d in self.param_input_dtypes),
91 tuple(tuple(n) for n in self.param_input_numels),
92 self.dtype,
93 tuple(self.inp_split_sizes),
94 self.total_input_numel,
95 )
96 )
99class AllGatherResult(NamedTuple):
100 """Result of a fused all-gather operation."""
102 all_gather_output: Optional[ms.Tensor]
103 metadata: Optional[AllGatherMetadata]
104 handle: Optional[CommHandle]
107@dataclass
108class CommContext:
109 """Global communication context for pipelined fused reductions."""
111 comm_handle: Optional[CommHandle] = None
112 all_reduce_handle: Optional[CommHandle] = None
113 pre_param_group = None
114 all_reduce_param_group = None
117comm_ctx = CommContext()
120def get_comm_ctx():
121 """Return the global communication context singleton."""
122 return comm_ctx
125@dataclass
126class ReplicateBucket:
127 """One fused all-reduce bucket sharing the same replicate process group."""
129 key: int
130 group: Any
131 group_size: int
132 param_indices: list[int]
133 flat_numel: int
134 buffer: Optional[ms.Tensor] = None
137@dataclass
138class PendingBucketAllReduce:
139 """One in-flight async all-reduce launched for a replicate bucket."""
141 bucket_key: int
142 handle: Any
145class AllGatherMetadataCache:
146 """Cache for all-gather metadata across iterations."""
148 _cache: dict[int, AllGatherMetadata] = {}
150 @classmethod
151 def get_metadata(cls, hsdp_params, fn):
152 """Retrieve or compute all-gather metadata, caching the result by parameter identity."""
153 param_key = tuple((id(p), getattr(p, "version", 0)) for p in hsdp_params)
154 key = hash(param_key)
155 if key in cls._cache:
156 return cls._cache[key]
157 metadata = fn(hsdp_params)
158 cls._cache[key] = metadata
159 return metadata
162@_no_grad()
163def all_gather_copy_in(all_gather_inputs, all_gather_output, inp_split_sizes, all_gather_input_numel, rank):
164 """Copy per-parameter local shards into one fused rank-local all-gather slice."""
165 all_gather_input = all_gather_output.narrow(0, all_gather_input_numel * rank, all_gather_input_numel)
166 offset = 0
167 for src, size in zip(all_gather_inputs, inp_split_sizes):
168 src_flat = src.view(-1)
169 all_gather_input.narrow(0, offset, size).copy_(src_flat)
170 offset += size
171 return all_gather_input, all_gather_output
174@_no_grad()
175def split_with_sizes_copy(all_gather_output, split_sizes, dim, out):
176 """Copy split views from a fused all-gather output into pre-allocated outputs."""
177 if dim != 1:
178 raise NotImplementedError("split_with_sizes_copy currently only supports dim=1")
179 offset = 0
180 for dst, size in zip(out, split_sizes):
181 src = all_gather_output.narrow(dim, offset, size)
182 copy_without_bumping_version(dst, src)
183 offset += size
186@_no_grad()
187def reduce_scatter_copy_in(
188 hsdp_params: List[MindSporeHSDPParamV2],
189 unsharded_grads: List[ms.Tensor],
190 reduce_scatter_input: ms.Tensor,
191 world_size: int,
192) -> None:
193 """Pack all unsharded gradients into one fused reduce-scatter input buffer."""
194 if len(hsdp_params) != len(unsharded_grads):
195 raise AssertionError(
196 "reduce_scatter_copy_in expects one hsdp_param per unsharded_grad, but got "
197 f"{len(hsdp_params)} params and {len(unsharded_grads)} grads"
198 )
199 packed_rows = reduce_scatter_input.view(world_size, -1)
200 col_offset = 0
201 for hsdp_param, grad in zip(hsdp_params, unsharded_grads):
202 grad = grad.contiguous()
203 plan = build_rs_plan(hsdp_param, grad, world_size)
204 packed_grad = pack_for_reduce_scatter(grad, plan)
205 next_col_offset = col_offset + packed_grad.shape[1]
206 for row_idx in range(world_size):
207 packed_rows[row_idx].narrow(0, col_offset, packed_grad.shape[1]).copy_(
208 packed_grad[row_idx].view(-1)
209 )
210 col_offset = next_col_offset
211 if col_offset != packed_rows.shape[1]:
212 raise AssertionError(
213 "reduce_scatter_copy_in packed an unexpected number of elements: "
214 f"{col_offset} != {packed_rows.shape[1]}"
215 )
218class HSDPParamGroup:
219 """Group HSDP parameters within a module for fused collectives."""
221 def __init__(
222 self,
223 hsdp_params,
224 mesh_info: FSDPMeshInfo,
225 device: Optional[str] = None,
226 mp_policy: Optional[MixedPrecisionPolicy] = None,
227 enable_zero_copy_param_buffer: bool = False,
228 ):
229 self.mesh_info = mesh_info
230 self.device = device
231 self.hsdp_params = hsdp_params
232 self.enable_zero_copy_param_buffer = enable_zero_copy_param_buffer
233 if isinstance(self.mesh_info, (FSDPMeshInfo, HSDPMeshInfo)):
234 self.shard_rank = self.mesh_info.shard_mesh_rank
235 self.shard_world_size = self.mesh_info.shard_mesh_size
236 else:
237 self.shard_rank = 0
238 self.shard_world_size = 1
239 self.shard_group = self.mesh_info.shard_process_group
240 self.replicate_group = None
241 if isinstance(self.mesh_info, (HSDPMeshInfo, DDPMeshInfo)):
242 self.replicate_group = self.mesh_info.replicate_process_group
243 elif isinstance(self.mesh_info, FSDPMeshInfo):
244 self.replicate_group = self._infer_layout_replicate_group()
245 self.ag_output: Optional[ms.Tensor] = None
246 self.metadata_cache = None
247 self.mp_policy = mp_policy
248 self._result = None
249 self._reduce_output = None
250 self._reduce_op = None
251 self._reduce_hsdp_params = None
252 self._active_replicate_buckets: dict[int, ReplicateBucket] = {}
253 self._active_param_flat_offsets: list[int] = []
254 self._pending_all_reduce_handles: list[PendingBucketAllReduce] = []
255 self._flat_param_buffer: Optional[ms.Tensor] = None
256 self._flat_cast_buffer: Optional[ms.Tensor] = None
257 self._init_mp_dtypes()
258 if self.enable_zero_copy_param_buffer:
259 self._init_flat_param_buffer()
260 self.gradient_scaling_factor = None
262 def _infer_layout_replicate_group(self):
263 replicate_groups = []
264 for hsdp_param in self.hsdp_params:
265 group_info = getattr(hsdp_param, "unsharded_group_info", None)
266 group = getattr(group_info, "group", None)
267 if group is None or getattr(hsdp_param, "replicate_world_size", 1) <= 1:
268 continue
269 replicate_groups.append(group)
270 if not replicate_groups:
271 return None
272 return replicate_groups[0]
274 @staticmethod
275 def _build_active_replicate_buckets(hsdp_params):
276 buckets: dict[int, ReplicateBucket] = {}
277 for idx, hsdp_param in enumerate(hsdp_params):
278 group_info = getattr(hsdp_param, "unsharded_group_info", None)
279 group = getattr(group_info, "group", None)
280 group_size = getattr(group_info, "rank_size", getattr(hsdp_param, "replicate_world_size", 1))
281 if group is None or group_size <= 1:
282 continue
283 key = id(group)
284 if key not in buckets:
285 buckets[key] = ReplicateBucket(
286 key=key,
287 group=group,
288 group_size=group_size,
289 param_indices=[],
290 flat_numel=0,
291 )
292 buckets[key].param_indices.append(idx)
293 buckets[key].flat_numel += _shape_numel(hsdp_param.sharded_size)
294 return buckets
296 def _init_flat_param_buffer(self):
297 """Rebase local shards into one flat buffer when storage semantics allow it."""
298 if not self.enable_zero_copy_param_buffer:
299 return
300 if self.shard_world_size <= 1 or len(self.hsdp_params) == 0:
301 return
302 if any(p.offload_to_cpu or str(p.sharded_param.device) == "meta" for p in self.hsdp_params):
303 return
305 total_numel = sum(hsdp_param._sharded_param_data.numel() for hsdp_param in self.hsdp_params)
306 orig_dtype = self.hsdp_params[0]._sharded_param_data.dtype
307 flat_buffer = ms.mint.empty((total_numel,), dtype=orig_dtype, device=_normalize_device(self.device))
309 offset = 0
310 original_locals = []
311 try:
312 for hsdp_param in self.hsdp_params:
313 original_locals.append((hsdp_param, hsdp_param._sharded_param_data, hsdp_param._sharded_local_tensor))
314 numel = hsdp_param._sharded_param_data.numel()
315 flat_slice = flat_buffer.narrow(0, offset, numel)
316 flat_slice.copy_(hsdp_param._sharded_param_data)
317 hsdp_param._sharded_param_data = flat_slice
318 new_local = flat_slice.view(hsdp_param.sharded_size)
319 req_grad = hsdp_param.sharded_param.requires_grad
320 hsdp_param.sharded_param.set_data(new_local)
321 hsdp_param.sharded_param._local_tensor = new_local
322 if req_grad:
323 new_local.requires_grad_(True)
324 hsdp_param.sharded_param.requires_grad_(True)
325 offset += numel
326 except Exception: # pylint: disable=W0718
327 for hsdp_param, orig_flat, orig_local in original_locals:
328 hsdp_param._sharded_param_data = orig_flat
329 hsdp_param.sharded_param.set_data(orig_local)
330 hsdp_param.sharded_param._local_tensor = orig_local
331 self._flat_param_buffer = None
332 self._flat_cast_buffer = None
333 return
335 self._flat_param_buffer = flat_buffer
336 has_param_dtype = any(p.param_dtype is not None for p in self.hsdp_params)
337 if has_param_dtype:
338 cast_dtype = next(p.param_dtype for p in self.hsdp_params if p.param_dtype is not None)
339 self._flat_cast_buffer = ms.mint.empty(
340 (total_numel,), dtype=cast_dtype, device=_normalize_device(self.device)
341 )
343 def _is_flat_buffer_valid(self):
344 """Check if flat buffer still backs the params' sharded data."""
345 if self._flat_param_buffer is None or len(self.hsdp_params) == 0:
346 return False
347 first_param = self.hsdp_params[0]
348 return (
349 first_param._sharded_param_data.untyped_storage().data_ptr()
350 == self._flat_param_buffer.untyped_storage().data_ptr()
351 )
353 def _allocate_bucket_buffers_if_needed(self, device, dtype):
354 normalized_device = _normalize_device(device)
355 for bucket in self._active_replicate_buckets.values():
356 if bucket.flat_numel == 0:
357 continue
358 needs_new_buffer = (
359 bucket.buffer is None
360 or bucket.buffer.numel() != bucket.flat_numel
361 or bucket.buffer.dtype != dtype
362 )
363 if needs_new_buffer:
364 bucket.buffer = ms.mint.empty((bucket.flat_numel,), dtype=dtype, device=normalized_device)
366 def _pack_bucket_from_reduce_output(self, bucket: ReplicateBucket) -> ms.Tensor:
367 if bucket.buffer is None:
368 raise AssertionError("Bucket buffer must be allocated before packing from reduce output")
369 if self._reduce_output is None or self._reduce_hsdp_params is None:
370 raise AssertionError("Bucket packing requires an active fused reduce output")
371 dst_offset = 0
372 for idx in bucket.param_indices:
373 hsdp_param = self._reduce_hsdp_params[idx]
374 src_offset = self._active_param_flat_offsets[idx]
375 numel = _shape_numel(hsdp_param.sharded_size)
376 bucket.buffer.narrow(0, dst_offset, numel).copy_(
377 self._reduce_output.narrow(0, src_offset, numel)
378 )
379 dst_offset += numel
380 return bucket.buffer
382 def _unpack_bucket_to_reduce_output(self, bucket: ReplicateBucket) -> None:
383 if bucket.buffer is None:
384 raise AssertionError("Bucket buffer must exist before unpacking to reduce output")
385 if self._reduce_output is None or self._reduce_hsdp_params is None:
386 raise AssertionError("Bucket unpack requires an active fused reduce output")
387 src_offset = 0
388 for idx in bucket.param_indices:
389 hsdp_param = self._reduce_hsdp_params[idx]
390 dst_offset = self._active_param_flat_offsets[idx]
391 numel = _shape_numel(hsdp_param.sharded_size)
392 self._reduce_output.narrow(0, dst_offset, numel).copy_(
393 bucket.buffer.narrow(0, src_offset, numel)
394 )
395 src_offset += numel
397 def unshard(self, async_op: bool = False):
398 """Trigger fused all-gather for all parameters in this group."""
399 if self._result is not None:
400 return
401 if self.shard_world_size == 1:
402 self._result = AllGatherResult(None, None, None)
403 return
404 self.foreach_all_gather(async_op=async_op)
406 def _init_mp_dtypes(self):
407 for hsdp_param in self.hsdp_params:
408 hsdp_param.init_dtype_attrs(self.mp_policy)
409 trainable_params: list[MindSporeHSDPParamV2] = [
410 p for p in self.hsdp_params if p.sharded_param.requires_grad
411 ]
412 orig_dtypes = {p.orig_dtype for p in trainable_params}
413 reduce_dtypes = {p.reduce_dtype for p in trainable_params}
414 if len(trainable_params) > 0 and len(orig_dtypes) != 1:
415 raise AssertionError(
416 f"hsdp expects uniform original parameter dtype but got {orig_dtypes}"
417 )
418 self._orig_dtype = next(iter(orig_dtypes)) if trainable_params else None
419 if len(trainable_params) > 0 and len(reduce_dtypes) != 1:
420 raise AssertionError(
421 f"hsdp expects uniform reduce dtype but got {reduce_dtypes}"
422 )
423 self._reduce_dtype = next(iter(reduce_dtypes)) if trainable_params else None
425 def wait_for_unshard(self):
426 """Wait for fused all-gather and materialize per-parameter unsharded views."""
427 if self._result is None:
428 return
429 if self.shard_world_size == 1:
430 for hsdp_param in self.hsdp_params:
431 all_gather_input = hsdp_param.all_gather_inputs[0]
432 hsdp_param.init_all_gather_outputs(
433 [all_gather_input.numel()],
434 [all_gather_input.dtype],
435 self.shard_world_size,
436 _normalize_device(self.device),
437 )
438 hsdp_param.alloc_all_gather_outputs()
439 copy_without_bumping_version(hsdp_param.all_gather_outputs[0], all_gather_input)
440 self._result = None
441 else:
442 self.foreach_all_gather_copy_out()
443 for hsdp_param in self.hsdp_params:
444 hsdp_param.init_unsharded_param()
445 hsdp_param.to_unsharded()
447 def alloc_all_gather_output(self, total_output_numel, dtype):
448 """Allocate or resize the fused all-gather output buffer to the specified size and dtype."""
449 normalized_device = _normalize_device(self.device)
450 if self.ag_output is None or self.ag_output.dtype != dtype:
451 self.ag_output = ms.mint.empty((total_output_numel,), dtype=dtype, device=normalized_device)
452 return
453 storage = self.ag_output.untyped_storage()
454 expected_size = total_output_numel * self.ag_output.itemsize
455 if storage.size() != expected_size:
456 storage.resize_(expected_size)
458 def free_all_gather_output(self):
459 """Release the fused all-gather output buffer by resizing its storage to zero."""
460 if self.ag_output is None:
461 return
462 storage = self.ag_output.untyped_storage()
463 if storage.size() != 0:
464 storage.resize_(0)
466 @_no_grad()
467 def foreach_all_gather(self, async_op=False):
468 """Perform one fused all-gather across all parameters in the group."""
469 if self.metadata_cache is None:
470 self.metadata_cache = AllGatherMetadataCache()
471 metadata = self.metadata_cache.get_metadata(self.hsdp_params, get_all_gather_metadata)
472 if metadata.total_input_numel == 0:
473 return
474 world_size = self.shard_world_size
475 rank = self.shard_rank
476 total_output_numel = metadata.total_input_numel * world_size
477 self.alloc_all_gather_output(total_output_numel, metadata.dtype)
478 for hsdp_param in self.hsdp_params:
479 hsdp_param.reset_sharded_param()
480 if self.enable_zero_copy_param_buffer and not self._is_flat_buffer_valid():
481 self._init_flat_param_buffer()
483 use_flat_buffer = (
484 self.enable_zero_copy_param_buffer
485 and self._flat_param_buffer is not None
486 and self._is_flat_buffer_valid()
487 )
488 if use_flat_buffer:
489 if self._flat_cast_buffer is not None:
490 self._flat_cast_buffer.copy_(self._flat_param_buffer)
491 all_gather_input = self._flat_cast_buffer
492 else:
493 all_gather_input = self._flat_param_buffer
494 else:
495 all_gather_inputs = []
496 for hsdp_param in self.hsdp_params:
497 all_gather_inputs.extend(hsdp_param.all_gather_inputs)
498 if len(all_gather_inputs) == 0:
499 return
500 all_gather_input, _ = all_gather_copy_in(
501 all_gather_inputs,
502 self.ag_output,
503 metadata.inp_split_sizes,
504 metadata.total_input_numel,
505 rank,
506 )
507 handle = dist.all_gather_into_tensor(self.ag_output, all_gather_input, self.shard_group, async_op)
508 self._result = AllGatherResult(self.ag_output, metadata, handle)
510 @_no_grad()
511 def foreach_all_gather_copy_out(self):
512 """Scatter one fused all-gather result back into per-parameter buffers."""
513 ag_output, metadata, handle = self._result
514 if handle is not None:
515 handle.wait()
516 world_size = self.shard_world_size
517 split_with_sizes_out = []
518 for input_numels, input_dtypes, hsdp_param in zip(
519 metadata.param_input_numels, metadata.param_input_dtypes, self.hsdp_params
520 ):
521 hsdp_param.init_all_gather_outputs(
522 input_numels,
523 input_dtypes,
524 world_size,
525 _normalize_device(ag_output.device),
526 )
527 hsdp_param.alloc_all_gather_outputs()
528 split_with_sizes_out.extend(hsdp_param.all_gather_outputs)
529 ag_output = ag_output.view(world_size, -1)
530 out = [t.view(world_size, -1) for t in split_with_sizes_out]
531 split_with_sizes_copy(ag_output, metadata.inp_split_sizes, dim=1, out=out)
532 self._result = None
533 self.free_all_gather_output()
535 @_no_grad()
536 def foreach_reduce(
537 self,
538 reduce_scatter_reduce_op: Optional[ops.ReduceOp] = ops.ReduceOp.SUM,
539 async_op: bool = True,
540 ) -> Optional[ms.Tensor]:
541 """Perform fused reduce-scatter and optional bucketed all-reduce."""
542 hsdp_params: List[MindSporeHSDPParamV2] = []
543 unsharded_grads: List[ms.Tensor] = []
544 for hsdp_param in self.hsdp_params:
545 if not hasattr(hsdp_param, "_unsharded_param"):
546 continue
547 if hsdp_param.unsharded_accumulated_grad is not None:
548 hsdp_params.append(hsdp_param)
549 unsharded_grads.append(hsdp_param.unsharded_accumulated_grad_data)
550 elif hsdp_param._unsharded_param.grad is not None:
551 hsdp_params.append(hsdp_param)
552 unsharded_grads.append(hsdp_param.unsharded_grad_data)
553 if not hsdp_params:
554 return None
555 grad_dtypes = {g.dtype for g in unsharded_grads}
556 if len(grad_dtypes) != 1:
557 raise ValueError(
558 f"FSDP reduce-scatter expects uniform grad dtype but got {grad_dtypes}"
559 )
560 grad_dtype = unsharded_grads[0].dtype
561 reduce_dtype = self._reduce_dtype or grad_dtype
562 world_size = self.shard_world_size
563 reduce_scatter_input_numel = sum(s.numel() for s in unsharded_grads)
564 reduce_scatter_output_numel = reduce_scatter_input_numel // world_size
565 device = _normalize_device(unsharded_grads[0].device)
566 reduce_scatter_input = ms.mint.empty((reduce_scatter_input_numel,), dtype=reduce_dtype, device=device)
567 reduce_scatter_copy_in(hsdp_params, unsharded_grads, reduce_scatter_input, world_size)
568 # Captured here, consumed once in _apply_reduced_grad after all collectives
569 # complete. Async paths cross method boundaries, so the field is unavoidable.
570 reduce_output = ms.mint.empty((reduce_scatter_output_numel,), dtype=reduce_dtype, device=device)
571 self._reduce_op = reduce_scatter_reduce_op
572 self._reduce_hsdp_params = hsdp_params
573 self._active_param_flat_offsets = []
574 flat_offset = 0
575 for hsdp_param in hsdp_params:
576 self._active_param_flat_offsets.append(flat_offset)
577 flat_offset += _shape_numel(hsdp_param.sharded_size)
578 self._active_replicate_buckets = self._build_active_replicate_buckets(hsdp_params)
579 self._allocate_bucket_buffers_if_needed(reduce_output.device, reduce_output.dtype)
580 self._pending_all_reduce_handles = []
581 if self.shard_group is None or world_size <= 1:
582 comm_ctx.comm_handle = None
583 self._reduce_output = reduce_scatter_input
584 if async_op:
585 comm_ctx.pre_param_group = self
586 else:
587 self.apply_fusion_reduced_grad()
588 return self._reduce_output
589 apply_gradient_scaling_factor(reduce_scatter_input, self.gradient_scaling_factor)
590 rs_handle = dist.reduce_scatter_tensor(
591 output=reduce_output,
592 input=reduce_scatter_input,
593 group=self.shard_group,
594 op=reduce_scatter_reduce_op,
595 async_op=async_op,
596 )
597 comm_ctx.comm_handle = rs_handle
598 self._reduce_output = reduce_output
599 if async_op:
600 comm_ctx.pre_param_group = self
601 else:
602 self.apply_fusion_reduced_grad()
603 return reduce_output
605 def wait_reduce_scatter_and_issue_all_reduce(self):
606 """Wait for reduce-scatter and issue async all-reduces for active buckets."""
607 if comm_ctx.comm_handle is not None:
608 comm_ctx.comm_handle.wait()
609 comm_ctx.comm_handle = None
610 if not self._active_replicate_buckets:
611 self._apply_reduced_grad()
612 return
613 self._pending_all_reduce_handles = []
614 for bucket in self._active_replicate_buckets.values():
615 packed = self._pack_bucket_from_reduce_output(bucket)
616 ar_handle = dist.all_reduce(
617 packed,
618 group=bucket.group,
619 op=self._reduce_op,
620 async_op=True,
621 )
622 self._pending_all_reduce_handles.append(
623 PendingBucketAllReduce(bucket_key=bucket.key, handle=ar_handle)
624 )
625 comm_ctx.all_reduce_param_group = self
627 def wait_all_reduce_and_apply_grad(self):
628 """Wait for pending bucket all-reduces and apply reduced grads."""
629 for pending in self._pending_all_reduce_handles:
630 bucket = self._active_replicate_buckets[pending.bucket_key]
631 pending.handle.wait()
632 self._unpack_bucket_to_reduce_output(bucket)
633 self._pending_all_reduce_handles = []
634 comm_ctx.all_reduce_handle = None
635 self._apply_reduced_grad()
637 def apply_fusion_reduced_grad(self):
638 """Synchronous fallback: wait, all-reduce buckets, then apply grads."""
639 if comm_ctx.comm_handle is not None:
640 comm_ctx.comm_handle.wait()
641 comm_ctx.comm_handle = None
642 for bucket in self._active_replicate_buckets.values():
643 packed = self._pack_bucket_from_reduce_output(bucket)
644 dist.all_reduce(
645 packed,
646 group=bucket.group,
647 op=self._reduce_op,
648 )
649 self._unpack_bucket_to_reduce_output(bucket)
650 self._apply_reduced_grad()
652 def _apply_reduced_grad(self):
653 """Write reduced gradients from the fused output buffer back to params."""
654 flat_grad_offset = 0
655 if self._reduce_hsdp_params is None or self._reduce_output is None:
656 return
657 # All collectives have completed; scale once on the fused buffer right
658 # before slicing it into per-parameter sharded grads.
659 for hsdp_param in self._reduce_hsdp_params:
660 shard_numel = _shape_numel(hsdp_param.sharded_size)
661 new_sharded_grad = self._reduce_output.narrow(0, flat_grad_offset, shard_numel)
662 hsdp_param.apply_reduced_grad(new_sharded_grad, self._orig_dtype)
663 flat_grad_offset += shard_numel
664 self._reduce_output = None
665 self._reduce_hsdp_params = None
666 self._active_param_flat_offsets = []
667 self._active_replicate_buckets = {}
668 self._pending_all_reduce_handles = []
671class AllReduceParamGroup:
672 """Groups HSDP parameters by replicate group for fused async all-reduce."""
674 ALIGNMENT_BYTES = 512
676 @staticmethod
677 def _resolve_reduce_dtype(
678 reduce_dtype: Any,
679 hsdp_params: List[MindSporeHSDPParamV2],
680 orig_dtypes: List[Any],
681 ) -> Any:
682 """Resolve None reduce_dtype to match ``reduce_scatter_grad``'s ``dtype or grad.dtype``."""
683 if reduce_dtype is not None:
684 return reduce_dtype
685 for hsdp_param in hsdp_params:
686 if getattr(hsdp_param, "unsharded_accumulated_grad", None) is not None:
687 return hsdp_param.unsharded_accumulated_grad_data.dtype
688 unsharded_param = getattr(hsdp_param, "unsharded_param", None)
689 if unsharded_param is not None and getattr(unsharded_param, "grad", None) is not None:
690 return hsdp_param.unsharded_grad_data.dtype
691 return orig_dtypes[0] if orig_dtypes else None
693 def __init__(
694 self,
695 replicate_group,
696 hsdp_params: List[MindSporeHSDPParamV2],
697 orig_dtypes: List[Any],
698 reduce_dtype: Any,
699 reduce_op: ops.ReduceOp,
700 mp_policy: Optional[MixedPrecisionPolicy] = None,
701 replicate_world_size: Optional[int] = None,
702 ):
703 self.replicate_group = replicate_group
704 self.hsdp_params = hsdp_params
705 self.orig_dtypes = orig_dtypes
706 self.reduce_dtype = self._resolve_reduce_dtype(reduce_dtype, hsdp_params, orig_dtypes)
707 self.reduce_op = reduce_op
708 self.mp_policy = mp_policy
709 if replicate_world_size is not None:
710 self.replicate_world_size = replicate_world_size
711 elif replicate_group is not None and hasattr(replicate_group, "rank_size"):
712 self.replicate_world_size = replicate_group.rank_size
713 elif hsdp_params:
714 self.replicate_world_size = hsdp_params[0].unsharded_group_info.rank_size
715 else:
716 self.replicate_world_size = 1
717 self.fused_buffer: Optional[ms.Tensor] = None
718 self.param_offsets: List[int] = []
719 self.param_numels: List[int] = []
720 self.all_reduce_handle: Optional[CommHandle] = None
722 def compute_aligned_layout(self) -> int:
723 """Compute fused buffer layout with trailing 512-byte alignment."""
724 self.param_offsets = []
725 self.param_numels = []
726 element_size = int(ms.Tensor([], dtype=self.reduce_dtype).itemsize)
727 current_offset = 0
728 for hsdp_param in self.hsdp_params:
729 numel = _shape_numel(hsdp_param.sharded_size)
730 self.param_numels.append(numel)
731 self.param_offsets.append(current_offset)
732 current_offset += numel
733 total_bytes = current_offset * element_size
734 aligned_total_bytes = (
735 (total_bytes + self.ALIGNMENT_BYTES - 1) // self.ALIGNMENT_BYTES
736 ) * self.ALIGNMENT_BYTES
737 return aligned_total_bytes // element_size
739 def allocate_fused_buffer(self, device: Any) -> None:
740 """Allocate the fused buffer with computed layout."""
741 total_numel = self.compute_aligned_layout()
742 normalized_device = _normalize_device(device)
743 self.fused_buffer = ms.mint.empty(
744 (total_numel,), dtype=self.reduce_dtype, device=normalized_device
745 )
746 self.fused_buffer.zero_()
748 def get_param_buffer_view(self, idx: int) -> ms.Tensor:
749 """Return a flat view for reduce_scatter output of parameter idx."""
750 if self.fused_buffer is None:
751 raise RuntimeError("Fused buffer not allocated. Call allocate_fused_buffer first.")
752 offset = self.param_offsets[idx]
753 numel = self.param_numels[idx]
754 return self.fused_buffer.narrow(0, offset, numel)
756 def get_param_grad_view(self, idx: int, target_shape) -> ms.Tensor:
757 """Return a reshaped view of the reduced gradient for parameter idx."""
758 return self.get_param_buffer_view(idx).view(target_shape)
760 def accumulate_existing_grads_to_buffer(self) -> None:
761 """Accumulate existing sharded grads into fused_buffer before all-reduce."""
762 if self.fused_buffer is None:
763 return
764 from hyper_parallel.core.dtensor.dtensor import DTensor
766 for idx, hsdp_param in enumerate(self.hsdp_params):
767 existing_grad = None
768 if self.mp_policy is not None and self.mp_policy.apply_grad_on_fp32_main_grad:
769 if hasattr(hsdp_param.sharded_param, "main_grad"):
770 existing_grad = hsdp_param.sharded_param.main_grad
771 else:
772 existing_grad = hsdp_param.sharded_param.grad
773 if existing_grad is not None and not hsdp_param.accumulated_allreduced_grad:
774 if isinstance(existing_grad, DTensor):
775 existing_grad_local = existing_grad._local_tensor
776 else:
777 existing_grad_local = existing_grad
778 buffer_view = self.get_param_buffer_view(idx)
779 if existing_grad_local.dtype != self.reduce_dtype:
780 existing_grad_local = existing_grad_local.to(self.reduce_dtype)
781 buffer_view.add_(existing_grad_local.view_as(buffer_view))
782 if self.mp_policy is not None and self.mp_policy.apply_grad_on_fp32_main_grad:
783 if hasattr(hsdp_param.sharded_param, "main_grad"):
784 hsdp_param.sharded_param.main_grad = None
785 else:
786 hsdp_param.sharded_param.grad = None
788 def issue_async_allreduce(self) -> None:
789 """Issue async all_reduce on the fused buffer (SUM for padding correctness)."""
790 if self.fused_buffer is None:
791 raise RuntimeError("Fused buffer not allocated.")
792 self.all_reduce_handle = dist.all_reduce(
793 self.fused_buffer,
794 op=ops.ReduceOp.SUM,
795 group=self.replicate_group,
796 async_op=True,
797 )
799 def wait_and_apply_grads(self) -> bool:
800 """Wait for all_reduce and apply gradients to parameters."""
801 if self.all_reduce_handle is not None:
802 self.all_reduce_handle.wait()
803 self.all_reduce_handle = None
804 need_synchronize = False
805 for idx, hsdp_param in enumerate(self.hsdp_params):
806 reduced_grad = self.get_param_grad_view(idx, hsdp_param.sharded_size)
807 # issue_async_allreduce uses SUM (so end-of-buffer padding zeros stay
808 # correct), so an AVG reduce op must divide by the replicate world size
809 # here. The reduce-scatter leg already averaged over the shard axis.
810 if self.reduce_op == ops.ReduceOp.AVG and self.replicate_world_size > 1:
811 reduced_grad = reduced_grad / self.replicate_world_size
812 need_synchronize = (
813 hsdp_param.apply_reduced_grad(reduced_grad, self.orig_dtypes[idx])
814 or need_synchronize
815 )
816 hsdp_param.accumulated_allreduced_grad = True
817 self.fused_buffer = None
818 return need_synchronize