Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/dtensor/dtensor.py 100%  
hyper_parallel/core/fully_shard/api.py 90.0% 281,283,331,644,776
hyper_parallel/core/fully_shard/hsdp_param.py 100%  
hyper_parallel/core/fully_shard/hsdp_scheduler.py 91.0% 95,99-100,106,212,260
hyper_parallel/core/fully_shard/hsdp_state.py 97.9% 180
hyper_parallel/core/fully_shard/hsdp_utils.py 100%  
hyper_parallel/core/fully_shard/utils.py 100%  
hyper_parallel/core/pipeline_parallel/stage.py 0.0% 604
hyper_parallel/core/shard/_op_dispatch.py 91.7% 615
hyper_parallel/platform/mindspore/fully_shard/pack_utils.py 100%  
hyper_parallel/platform/mindspore/fully_shard/param.py 53.9% 187-189,197,200,204-205,208,212-213,216,221-223,225,244,248,256-260,270-271,278,399-401,403-406,408-409,413,415,420-421,505,509,582,600,781,787-789,792,798,805-806,816,819,823-826,903,905-907,912,914-916,929,974,976,983-985,1018,1064,1068,1087,1095,1097,1106
hyper_parallel/platform/mindspore/fully_shard/param_group.py 78.2% 85-89,107-114,119,125-127,133,169,184,234,267-268,332,357,360-361,370,406-409,436,438,442,458,486,494,499,563,569-570,580,582,595,597,624,637-638,643,658,661,679-695,738-739,748,760,762,765-768,799
hyper_parallel/platform/mindspore/fully_shard/scheduler.py 63.8% 157-158,172,175-180,184-186,191-195
hyper_parallel/platform/mindspore/fully_shard/state.py 66.5% 66,85,98-100,106-107,114-117,127-129,131,140,156,167,182,187,197-203,221-222,251,270-274,276-277,279,337-342,347-349,353-355,360-361,377,383,387-388,412,440-441
hyper_parallel/platform/torch/dtensor.py 100%  
hyper_parallel/platform/torch/fully_shard/param.py 85.1% 247,267-271,309-312,407,409,504,601-603,609,612,617-618,713,720,728,749,805,836,854,873,933-935,937-941,955,1042-1043,1057,1061,1094,1132,1141,1144,1150
hyper_parallel/platform/torch/fully_shard/param_group.py 93.3% 109-110,148,165,215,241,306,372,501,539,544,557,559,568-569,571,618,620,624,647,701,756,782,788-789,800,815,890,1033,1036
hyper_parallel/platform/torch/fully_shard/scheduler.py 85.0% 157,161-162,164,168-169,171-172,187
hyper_parallel/platform/torch/fully_shard/state.py 86.7% 140,365,408-409,436,443,472,474,484,529-532,534
hyper_parallel/trainer/base.py 0.0% 855-856,861,863-864
hyper_parallel/core/fully_shard/api.py
277
278
279
280
281
282
283
284
285
286
287
        if assign and state_dict and all(
            isinstance(val, DTensor) for val in state_dict.values()
        ):
            return super().load_state_dict(state_dict, strict=strict, assign=True)
        self_module = cast(ModuleClass, self)

        target_map: dict[str, TensorClass] = {}
        for name, p in platform.parameters_dict(self_module):
            target_map[name] = p
        for name, b in self_module.named_buffers():
            target_map[name] = b
327
328
329
330
331
332
333
334
335
        Raises:
            ValueError: If ``recursive`` is not a bool.
        """
        if not isinstance(recursive, bool):
            raise ValueError(f"recursive should be a bool, got {type(recursive)}")
        self_module = cast(ModuleClass, self)
        modules = (
            [module for _, module in platform.get_cells_and_names(self_module)]
            if recursive
640
641
642
643
644
645
646
647
648
    """Validate the parameter-identity metadata consumed by one fully_shard unit."""
    if tp_grad_infos is None:
        return
    if not isinstance(tp_grad_infos, Mapping):
        raise ValueError("tp_grad_infos must be a mapping from Parameter to TPShardMetaInfo")

    managed_parameters = set(managed_parameters)

    if any(isinstance(parameter, DTensor) for parameter in managed_parameters):
772
773
774
775
776
777
778
779
    platform_type = platform.platform_type
    _validate_module_for_fully_shard(module, platform_type)

    if tp_grad_infos is not None and platform_type != PlatformType.PYTORCH:
        raise NotImplementedError("tp_grad_infos is currently supported only on the Torch backend")

    if platform_type == PlatformType.MINDSPORE:
        from hyper_parallel.platform.mindspore.autograd_compat import enable_mindspore_backward_compat
hyper_parallel/core/fully_shard/hsdp_scheduler.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
        self.mesh: DeviceMesh = mesh
        self.shard_placement_fn = shard_placement_fn
        self.mp_policy = mp_policy
        self.offload_policy = offload_policy
        self.comm_fusion_policy = CommFusionPolicy(comm_fusion, comm_fusion_zero_copy)
        self.ignored_params = ignored_params
        self.replicate_params = replicate_params
        self.device = device
        self.reshard_after_forward = reshard_after_forward
        self.tp_grad_infos = tp_grad_infos
        self.scheduler_state = None
        self.forward_prefetch_cells = []
        self.backward_prefetch_cells = []
        self._backup_forward_fetch = None
102
103
104
105
106
107
108
109
110
        self.forward_prefetch_cells = []
        self.backward_prefetch_cells = []
        self._backup_forward_fetch = None
        # Flag to identify root module.
        self._is_root = True
        # module and its all sub-modules share one same 'HSDPSchedulerContext'
        self.scheduler_ctx = HSDPSchedulerContext()
        # When ``fully_shard`` is given multiple root modules, forward pre/post hooks coordinate
        # so unshard / PostBackward / reshard run once per forward (aligned with PyTorch FSDP2).
208
209
210
211
212
213
214
215
216
                from hyper_parallel.core.fully_shard.api import HSDPModule  # pylint: disable=C0415
                if isinstance(module, HSDPModule):
                    submod_scheduler = module.hsdp_scheduler
                    if submod_scheduler is None or id(submod_scheduler) in registered_schedulers:
                        continue
                    registered_schedulers.add(id(submod_scheduler))
                    if submod_scheduler.scheduler_ctx is not tree_ctx:
                        if submod_scheduler.scheduler_ctx.root_module is not None:
                            raise ValueError(
256
257
258
259
260
261
262
263
264
    def _init_params_fqn(self):  # pylint: disable=W0212
        if not self._is_root or self.scheduler_ctx.root_module is None:
            return
        if self.scheduler_ctx._param_fqn_initialized:  # pylint: disable=protected-access
            return
        # Build a map from original (sharded) parameter tensor to its HSDPParam wrapper.
        param_to_hsdp_param = {}
        for submod_scheduler in self.scheduler_ctx.all_hsdp_schedulers:
            hsdp_state = submod_scheduler.hsdp_state
hyper_parallel/core/fully_shard/hsdp_state.py
176
177
178
179
180
181
182
183
184
        """
        if self.param_group is not None:
            self.param_group.gradient_scaling_factor = factor
        else:
            for hsdp_param in self.hsdp_params:
                hsdp_param.gradient_scaling_factor = factor

    def set_requires_all_reduce(self, requires_all_reduce: bool) -> None:
        """Propagate the HSDP all-reduce switch to the active communication path."""
hyper_parallel/core/pipeline_parallel/stage.py
600
601
602
603
604
605
606
607
            root_schedulers = fsdp_schedulers[:1]
        for scheduler in root_schedulers:
            # No public API exposes root backward finalization. The hook drains
            # unconditionally, so a differentiable PP input cannot defer it.
            scheduler._root_backward_hook()  # pylint: disable=protected-access

    def _build_padded_sens(self, micro_index):
        """Build an N-length sens list aligned with the forward output structure.
hyper_parallel/core/shard/_op_dispatch.py
611
612
613
614
615
616
617
618
619
        py_output = op_call(*local_args, **local_kwargs)
        op_name = platform.get_op_name(op_call)
        if op_name in _RAGGED_INPLACE_ELEMENTWISE_OPS:
            if not args or not isinstance(args[0], DTensor):
                raise ValueError(
                    f"Ragged in-place operator {op_name!r} requires a DTensor first argument"
                )
            return args[0]
        return DTensor.from_local_with_layout(
hyper_parallel/platform/mindspore/fully_shard/param.py
183
184
185
186
187
188
189
190
191
192
193
        self._module_info: ParamModuleInfo = module_info
        self.mesh_info = mesh_info
        self.mp_policy = mp_policy
        self.device = device
        self.orig_dtype = None
        self.param_dtype = None
        self.reduce_dtype = None
        self.offload_to_cpu: bool = isinstance(offload_policy, CPUOffloadPolicy)
        self.pin_memory = (
            self.offload_to_cpu and cast(CPUOffloadPolicy, offload_policy).pin_memory
        )
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
        )
        self._orig_param_hooks: List[Callable] = []
        self.grad_offload_event: Optional[ms.runtime.Event] = None
        dtensor_payload = unwrap_dtensor_param(param)
        if (dtensor_payload is not None) != (
            tp_grad_info is not None and tp_grad_info.origin_is_dtensor
        ):
            raise ValueError(
                "tp_grad_info.origin_is_dtensor must be True exactly for native DTensor parameters, "
                f"got parameter type {type(param).__name__} and tp_grad_info={tp_grad_info}"
            )
        self.tp_grad_info = tp_grad_info
        self._orig_param_is_dtensor = (
            tp_grad_info is not None and tp_grad_info.origin_is_dtensor
        )
        self._orig_dtensor_mesh = tp_grad_info.mesh if self._orig_param_is_dtensor else None
        self._orig_dtensor_placements = (
            tuple(tp_grad_info.placements) if self._orig_param_is_dtensor else None
        )
        self._spmd_shard_mesh_dim = self.mesh_info.shard_mesh_dim
        self._spmd_replicate_mesh_dim = self.mesh_info.replicate_mesh_dim
        self._init_sharded_param(param, shard_placement_fn)
        self._save_backward_hooks(param)
        self.unsharded_param_buffers: List[ms.Tensor] = []
        self.unsharded_accumulated_grad = None
        self._unsharded_param: Optional[Parameter] = None
        self._param_fqn: Optional[str] = None
        # Communication attributes for prefetch pattern
        self.allgather_comm_ctx = AllGatherCommCtx()
        self.reduce_scatter_comm_ctx = ReduceScatterCommCtx()
        self.all_reduce_comm_ctx = AllReduceCommCtx()
        self._accumulated_allreduced_grad = True
        self._reduce_partial_output = None
        self._post_load_hook_handle = (
            module_info.module.register_load_state_dict_post_hook(
                lambda *args, **kwargs: self.reset_sharded_param()
            )
240
241
242
243
244
245
246
247
248
249
250
251
252

    @property
    def reduce_partial_output(self) -> Optional[ms.Tensor]:
        """Return reduce-scatter results accumulated before the final micro-step."""
        return self._reduce_partial_output

    @reduce_partial_output.setter
    def reduce_partial_output(self, value: Optional[ms.Tensor]) -> None:
        self._reduce_partial_output = value

    def reduce_comm_dtype(self, grad: Optional[ms.Tensor] = None):
        """Resolve the communication dtype owned by this parameter."""
        if self.reduce_dtype is not None:
252
253
254
255
256
257
258
259
260
261
262
263
264
        if self.reduce_dtype is not None:
            return self.reduce_dtype
        if grad is not None:
            return grad.dtype
        if self.unsharded_accumulated_grad is not None:
            return self.unsharded_accumulated_grad_data.dtype
        if self.unsharded_param is not None and self.unsharded_param.grad is not None:
            return self.unsharded_grad_data.dtype
        return self.orig_dtype

    def _get_base_spmd_placements(self) -> tuple:
        """Return source-layout placements prefixed by explicit data-parallel axes."""
        if self.tp_grad_info is not None:
266
267
268
269
270
271
272
273
274
275
                [self.mesh_info.mesh, self.tp_grad_info.mesh]
            )
            dp_prefix = tuple(Replicate() for _ in range(self.mesh_info.mesh.ndim))
            return dp_prefix + tuple(self.tp_grad_info.placements)
        self._spmd_mesh = self.mesh_info.mesh
        return tuple(Replicate() for _ in range(self._spmd_mesh.ndim))

    def _apply_data_parallel_placements(
        self, placements: list, shard_placement: Shard
    ) -> tuple:
274
275
276
277
278
279
280
281
282
        self, placements: list, shard_placement: Shard
    ) -> tuple:
        """Apply the parameter-specific DDP/FSDP layout to source placements."""
        if len(placements) != self._spmd_mesh.ndim:
            raise AssertionError(
                f"Expected {self._spmd_mesh.ndim} unified placements, got "
                f"{len(placements)}: {placements}"
            )
        if (
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
        shard_dim = hsdp_placement.dim
        self._orig_size = param_data.shape
        self._contiguous_orig_stride = make_contiguous_strides_for(self._orig_size)

        if isinstance(self.mesh_info, FSDPMeshInfo):
            self.shard_rank = self.mesh_info.shard_mesh_rank
            self.shard_world_size = self.mesh_info.shard_mesh_size
        else:
            self.shard_rank = 0
            self.shard_world_size = 1
        if isinstance(self.mesh_info, DDPMeshInfo):
            self.replicate_world_size = self.mesh_info.replicate_mesh_size
        else:
            self.replicate_world_size = 1
        self.is_replicate_param = (
            isinstance(self.mesh_info, DDPMeshInfo)
            and not isinstance(self.mesh_info, HSDPMeshInfo)
        )
        self.is_sharded = self.shard_world_size > 1

        if param_data.shape[shard_dim] % self.shard_world_size != 0:
            raise NotImplementedError(
                f"Uneven sharding on dim {shard_dim} not supported: "
                f"shape={param_data.shape}, world_size={self.shard_world_size}"
            )
        chunks = ms.mint.chunk(param_data, self.shard_world_size, dim=shard_dim)
        sharded_param = chunks[self.shard_rank].clone().contiguous()
        self.sharded_size = sharded_param.shape
        self.contiguous_sharded_stride = make_contiguous_strides_for(self.sharded_size)
        self._sharded_param_data = sharded_param.view(-1)
501
502
503
504
505
506
507
508
509
510
511
512
513
            self._unsharded_param.requires_grad = True

    def _get_unsharded_param_from_all_gather_output(self):
        """Reconstruct the full local parameter view from the packed all-gather output."""
        if len(self.unsharded_param_buffers) != 1:
            raise AssertionError(
                f"Expected 1 unsharded_param_buffer, got {len(self.unsharded_param_buffers)}"
            )
        unsharded_tensor = self.unsharded_param_buffers[0]
        plan = build_rs_plan(
            self,
            self._sharded_local_tensor,
            self.shard_world_size if self.is_sharded else 1,
578
579
580
581
582
583
584
585
            unsharded_grad = unsharded_grad.to(self.reduce_dtype)
        if self.unsharded_accumulated_grad is None:
            self.unsharded_accumulated_grad = unsharded_grad
        else:
            self.unsharded_accumulated_grad = ms.mint.add(
                self.unsharded_accumulated_grad,
                unsharded_grad,
            )
596
597
598
599
600
601
602
603
604
            )
            self.unsharded_param.grad = None

    def alloc_unsharded_param_buffers(self) -> None:
        for tensor in self.unsharded_param_buffers:
            expected_size = tensor.numel() * tensor.itemsize

            storage = tensor.untyped_storage()
            if storage.size() != expected_size:
777
778
779
780
781
782
783
784
785
        all_gather_input = self.all_gather_inputs[0]

        # If parameter is not sharded (below threshold), no communication needed
        if not self.is_sharded:
            self.init_unsharded_param_buffers(
                all_gather_input_numels=[all_gather_input.numel()],
                all_gather_input_dtypes=[all_gather_input.dtype],
                world_size=1,
                device=all_gather_input.device.split(':')[0],
783
784
785
786
787
788
789
790
791
792
793
794
795
796
                all_gather_input_dtypes=[all_gather_input.dtype],
                world_size=1,
                device=all_gather_input.device.split(':')[0],
            )
            self.alloc_unsharded_param_buffers()
            copy_without_bumping_version(self.unsharded_param_buffers[0], all_gather_input)
            return all_gather_input, self.unsharded_param_buffers[0], None

        # Initialize output buffer
        self.init_unsharded_param_buffers(
            all_gather_input_numels=[all_gather_input.numel()],
            all_gather_input_dtypes=[all_gather_input.dtype],
            world_size=self.shard_world_size,
            device=self._sharded_param_data.device.split(':')[0],
794
795
796
797
798
799
800
801
            all_gather_input_dtypes=[all_gather_input.dtype],
            world_size=self.shard_world_size,
            device=self._sharded_param_data.device.split(':')[0],
        )
        self.alloc_unsharded_param_buffers()

        # Get communication group
        shard_group = self.mesh_info.shard_process_group if isinstance(self.mesh_info, FSDPMeshInfo) else None
801
802
803
804
805
806
807
808
809
810
        shard_group = self.mesh_info.shard_process_group if isinstance(self.mesh_info, FSDPMeshInfo) else None

        if shard_group is None or self.shard_world_size <= 1:
            # No communication needed, just copy
            copy_without_bumping_version(self.unsharded_param_buffers[0], all_gather_input)
            return all_gather_input, self.unsharded_param_buffers[0], None

        # Execute all_gather_into_tensor
        handle = dist.all_gather_into_tensor(
            self.unsharded_param_buffers[0],
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
            group=shard_group,
            async_op=async_op,
        )

        return all_gather_input, self.unsharded_param_buffers[0], handle

    def unshard(self, async_op: bool = False) -> None:
        if self.allgather_comm_ctx.allgather_output is not None:
            # Already triggered by HSDPState.prefetch(), so return directly.
            return  # no-op

        all_gather_input, output, handle = self._get_unsharded_param_data(async_op=async_op)
        self.allgather_comm_ctx.allgather_input = all_gather_input
        self.allgather_comm_ctx.allgather_output = output
        self.allgather_comm_ctx.allgather_handle = handle

    def wait_for_unshard(self) -> None:
        self._assert_in_states(ShardedState.SHARDED)
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
        # If parameter is not sharded (below threshold), no reduce-scatter needed
        if not self.is_sharded:
            if output_buffer is not None:
                copy_without_bumping_version(output_buffer, grad_flat)
                self.reduce_scatter_comm_ctx.reduce_scatter_output = output_buffer
            else:
                self.reduce_scatter_comm_ctx.reduce_scatter_output = grad_flat
            self.reduce_scatter_comm_ctx.reduce_scatter_handle = None
            return

        if shard_group is None or shard_group_size <= 1:
            if output_buffer is not None:
                copy_without_bumping_version(output_buffer, grad_flat)
                self.reduce_scatter_comm_ctx.reduce_scatter_output = output_buffer
            else:
                self.reduce_scatter_comm_ctx.reduce_scatter_output = grad_flat
            self.reduce_scatter_comm_ctx.reduce_scatter_handle = None
            return

        # Calculate output size
        output_numel = grad_flat.numel() // shard_group_size
        if output_buffer is not None:
925
926
927
928
929
930
931
932
933
            if output_buffer.dtype != reduce_dtype:
                raise ValueError(
                    f"output_buffer dtype mismatch: expected {reduce_dtype}, got {output_buffer.dtype}"
                )
            self.reduce_scatter_comm_ctx.reduce_scatter_output = output_buffer
        else:
            self.reduce_scatter_comm_ctx.reduce_scatter_output = ms.mint.empty(
                output_numel, dtype=reduce_dtype, device=grad.device.split(":")[0]
            )
970
971
972
973
974
975
976
977
978
979
980
        ``all_reduce_comm_ctx``.
        """
        grad = self.reduce_scatter_comm_ctx.reduce_scatter_output
        if grad is None:
            raise RuntimeError("all_reduce_grad requires a completed reduce-scatter output.")
        if self.reduce_dtype is not None and self.reduce_dtype != grad.dtype:
            grad = grad.to(self.reduce_dtype)
        reduce_group = (
            self.mesh_info.replicate_process_group
            if isinstance(self.mesh_info, DDPMeshInfo)
            else None
979
980
981
982
983
984
985
986
987
988
989
            if isinstance(self.mesh_info, DDPMeshInfo)
            else None
        )
        if reduce_group is None or self.replicate_world_size <= 1:
            self.all_reduce_comm_ctx.all_reduce_output = grad
            self.all_reduce_comm_ctx.all_reduce_handle = None
            return

        # Ascend HCCL DistCommAllReduce rejects non-contiguous tensors.
        # ``grad`` here may be a view returned by ``_to_local_unsharded_grad``
        # (DTensor.to_local() / redistribute().to_local()) or by autograd.
1014
1015
1016
1017
1018
1019
1020
1021
1022

    def clear_unsharded_source_grad(self) -> None:
        """Release the unsharded gradient after its communication input is safe."""
        if self.unsharded_accumulated_grad is not None:
            self.unsharded_accumulated_grad = None
        if self.unsharded_param is not None and self.unsharded_param.grad is not None:
            self.unsharded_param.grad = None

    def apply_reduced_grad(self, reduced_grad):
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
            else:
                self.sharded_param.grad = self.to_sharded_dtensor(reduced_grad)
        else:
            if self.mp_policy.apply_grad_on_fp32_main_grad:
                accumulated_grad = ms.mint.add(
                    self.sharded_param.main_grad._local_tensor,
                    reduced_grad,
                )
                self.sharded_param.main_grad = self.to_sharded_dtensor(accumulated_grad)
                self.sharded_param.grad = None
            else:
                accumulated_grad = ms.mint.add(
                    self.sharded_param.grad._local_tensor,
1083
1084
1085
1086
1087
1088
1089
1090
1091
        reduce_op: str,
    ) -> None:
        """All-reduce a final gradient over replicated source-layout axes."""
        if self.tp_grad_info is None or not self.tp_grad_info.placements:
            return
        source_mesh = self.tp_grad_info.mesh
        replicate_mesh_dims = tuple(
            mesh_dim
            for mesh_dim, placement in enumerate(self.tp_grad_info.placements)
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
            for mesh_dim, placement in enumerate(self.tp_grad_info.placements)
            if placement.is_replicate()
        )
        if not replicate_mesh_dims:
            return
        if source_mesh.mesh_dim_names is None:
            raise ValueError(
                "TP shard mesh must define mesh_dim_names to all-reduce replicated gradients."
            )
        replicate_dim_names = tuple(
            source_mesh.mesh_dim_names[mesh_dim]
1102
1103
1104
1105
1106
1107
1108
1109
1110
            for mesh_dim in replicate_mesh_dims
        )
        replicate_mesh = source_mesh[replicate_dim_names].flatten()
        if replicate_mesh.size() <= 1:
            return
        dist.all_reduce(
            reduced_grad,
            op=reduce_op,
            group=replicate_mesh.get_group(),
hyper_parallel/platform/mindspore/fully_shard/param_group.py
81
82
83
84
85
86
87
88
89
90
91
92
93

    @classmethod
    def get_metadata(cls, hsdp_params, fn):
        """Retrieve or compute metadata keyed by parameter identity and version."""
        param_key = tuple((id(param), getattr(param, "version", 0)) for param in hsdp_params)
        key = hash(param_key)
        if key not in cls._cache:
            cls._cache[key] = fn(hsdp_params)
        return cls._cache[key]


@dataclass
class AllGatherBucket:
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123

    @_no_grad()
    def copy_out(self) -> None:
        """Wait the all-gather and copy each result into stable parameter buffers."""
        result = self.all_gather_result
        if result is None or result.all_gather_output is None:
            return
        if result.handle is not None:
            result.handle.wait()
        all_gather_output = result.all_gather_output
        output_buffers = []
        for input_numels, input_dtypes, hsdp_param in zip(
            self.metadata.param_input_numels,
            self.metadata.param_input_dtypes,
            self.hsdp_params,
        ):
            hsdp_param.init_unsharded_param_buffers(
                input_numels,
                input_dtypes,
                self.shard_world_size,
                _normalize_device(all_gather_output.device),
121
122
123
124
125
126
127
128
129
130
131
                input_dtypes,
                self.shard_world_size,
                _normalize_device(all_gather_output.device),
            )
            hsdp_param.alloc_unsharded_param_buffers()
            output_buffers.extend(hsdp_param.unsharded_param_buffers)
        split_with_sizes_copy(
            all_gather_output.view(self.shard_world_size, -1),
            self.metadata.inp_split_sizes,
            dim=1,
            out=[tensor.view(self.shard_world_size, -1) for tensor in output_buffers],
129
130
131
132
133
134
135
136
137
            self.metadata.inp_split_sizes,
            dim=1,
            out=[tensor.view(self.shard_world_size, -1) for tensor in output_buffers],
        )
        self.all_gather_result = None


@dataclass
class GradientBucketLayout:
165
166
167
168
169
170
171
172
173

    @property
    def param_offsets(self) -> list[int]:
        """Return parameter offsets in the reduce-scatter output."""
        return self.layout.param_offsets

    @property
    def bucket_key(self) -> tuple:
        """Return the identity of this fusion class across micro-steps."""
180
181
182
183
184
185
186
187

    def move_reduce_scatter_output(self) -> ms.Tensor:
        """Transfer exclusive ownership of the completed output."""
        if self.reduce_scatter_output is None:
            raise RuntimeError("Reduce-scatter output has already been released.")
        output = self.reduce_scatter_output
        self.reduce_scatter_output = None
        return output
230
231
232
233
234
235
236
237
238
        param_input_numels.append(input_numels)
        inp_split_sizes.extend(input_numels)
        total_input_numel += sum(input_numels)
    if dtype is None:
        raise ValueError("Cannot build all-gather metadata for an empty parameter bucket.")
    return AllGatherMetadata(
        param_input_dtypes,
        param_input_numels,
        dtype,
263
264
265
266
267
268
269
270
271
272
    """Copy dim-1 slices from a fused all-gather into stable buffers."""
    if dim != 1:
        raise NotImplementedError("split_with_sizes_copy currently only supports dim=1")
    offset = 0
    for destination, size in zip(out, split_sizes):
        copy_without_bumping_version(
            destination,
            all_gather_output.narrow(dim, offset, size),
        )
        offset += size
328
329
330
331
332
333
334
335
336
        for hsdp_param in self.hsdp_params:
            if hsdp_param.shard_world_size <= 1:
                continue
            if not isinstance(hsdp_param.mesh_info, FSDPMeshInfo):
                raise ValueError(
                    f"Fused all-gather expects FSDPMeshInfo, got {type(hsdp_param.mesh_info)}"
                )
            shard_group = hsdp_param.mesh_info.shard_process_group
            communication_dtype = hsdp_param.param_dtype or hsdp_param.orig_dtype
353
354
355
356
357
358
359
360
361
362
363
364
365
            )

    def unshard(self, async_op: bool = False) -> None:
        """Launch fused all-gathers for every compatible bucket."""
        if self.all_gather_buckets and any(
            bucket.all_gather_result is not None for bucket in self.all_gather_buckets
        ):
            return
        self.foreach_all_gather(async_op)

    @_no_grad()
    def foreach_all_gather(self, async_op: bool = False) -> None:
        """Initialize ordered buckets and launch their all-gathers."""
366
367
368
369
370
371
372
373
374
        if not self.all_gather_buckets:
            self._init_all_gather_buckets()
        for hsdp_param in self.hsdp_params:
            if hsdp_param.shard_world_size <= 1:
                hsdp_param.unshard(async_op)
        for bucket in self.all_gather_buckets:
            if bucket.all_gather_result is not None:
                continue
            metadata = bucket.metadata
402
403
404
405
406
407
408
409
410
411
412
413
            )

    def wait_for_unshard(self) -> None:
        """Wait all all-gathers and install stable unsharded parameters."""
        for bucket in self.all_gather_buckets:
            bucket.copy_out()
        for hsdp_param in self.hsdp_params:
            hsdp_param.wait_for_unshard()

    @staticmethod
    def _build_gradient_bucket_layout(hsdp_params) -> GradientBucketLayout:
        """Build one compact output layout for the supplied parameter order."""
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
        grads_by_bucket = {}
        groups_by_bucket = {}
        for hsdp_param in self.hsdp_params:
            if not hsdp_param.sharded_param.requires_grad:
                continue
            if hsdp_param.unsharded_accumulated_grad is not None:
                unsharded_grad = hsdp_param.unsharded_accumulated_grad_data
            elif hsdp_param.unsharded_param.grad is not None:
                unsharded_grad = hsdp_param.unsharded_grad_data
            else:
                continue
            shard_group = (
                hsdp_param.mesh_info.shard_process_group
                if isinstance(hsdp_param.mesh_info, FSDPMeshInfo)
                else None
454
455
456
457
458
459
460
461
462
        buckets = []
        for bucket_key, hsdp_params in params_by_bucket.items():
            shard_world_size = hsdp_params[0].shard_world_size
            if any(param.shard_world_size != shard_world_size for param in hsdp_params):
                raise ValueError("A reduce-scatter bucket must use one shard world size.")
            needs_avg_div = reduce_op == "avg"
            buckets.append(
                ReduceScatterBucket(
                    layout=self._build_gradient_bucket_layout(hsdp_params),
482
483
484
485
486
487
488
489
490
                if any(
                    isinstance(hsdp_param.mesh_info, DDPMeshInfo)
                    for hsdp_param in rs_bucket.hsdp_params[1:]
                ):
                    raise ValueError(
                        "A reduce-scatter bucket cannot mix parameters with and without "
                        "a subsequent all-reduce."
                    )
                continue
490
491
492
493
494
495
496
497
498
499
500
501
502
503
                continue
            replicate_group = representative.mesh_info.replicate_process_group
            replicate_world_size = representative.replicate_world_size
            for hsdp_param in rs_bucket.hsdp_params[1:]:
                if (
                    not isinstance(hsdp_param.mesh_info, DDPMeshInfo)
                    or hsdp_param.mesh_info.replicate_process_group != replicate_group
                    or hsdp_param.replicate_world_size != replicate_world_size
                ):
                    raise ValueError(
                        "All parameters in a reduce-scatter bucket must share one "
                        "subsequent all-reduce group."
                    )
            buckets.append(
559
560
561
562
563
564
565
566
567
        self.reduce_scatter_buckets = self._build_reduce_scatter_buckets(
            reduce_scatter_reduce_op,
        )
        if not self.reduce_scatter_buckets:
            return
        self.all_reduce_buckets = self._build_all_reduce_buckets(self.reduce_scatter_buckets)
        self._issue_reduce_scatter_buckets(async_op)
        if async_op:
            self.comm_ctx.pre_param_group = self
565
566
567
568
569
570
571
572
573
574
        self._issue_reduce_scatter_buckets(async_op)
        if async_op:
            self.comm_ctx.pre_param_group = self
        else:
            self.wait_reduce_scatter_and_issue_all_reduce(async_op=False)
            self.wait_all_reduce_and_save_grad()

    def _wait_reduce_scatter_buckets(self) -> None:
        """Wait RS buckets and finish shard-dimension averaging."""
        for bucket in self.reduce_scatter_buckets:
576
577
578
579
580
581
582
583
584
585
                bucket.handle.wait()
                bucket.handle = None
            bucket.reduce_scatter_input = None
            if bucket.reduce_scatter_output is None:
                raise RuntimeError("Reduce-scatter bucket has not been prepared.")
            if bucket.needs_avg_div and bucket.shard_world_size > 1:
                bucket.reduce_scatter_output = ms.mint.div(
                    bucket.reduce_scatter_output,
                    bucket.shard_world_size,
                )
591
592
593
594
595
596
597
598
599
600
601
    ) -> None:
        """Launch SUM all-reduces for completed RS outputs."""
        for bucket in all_reduce_buckets:
            if bucket.all_reduce_output is None:
                raise RuntimeError("All-reduce bucket has not received its reduce-scatter output.")
            if not bucket.uses_collective:
                continue
            bucket.handle = dist.all_reduce(
                bucket.all_reduce_output,
                group=bucket.replicate_group,
                op="sum",
620
621
622
623
624
625
626
627
628

        for bucket in self.reduce_scatter_buckets:
            partial_output = self.reduce_partial_outputs.pop(bucket.bucket_key, None)
            if partial_output is not None:
                bucket.reduce_scatter_output = ms.mint.add(
                    bucket.reduce_scatter_output,
                    partial_output,
                )
        all_reduce_by_source = {
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
            all_reduce_bucket = all_reduce_by_source.get(id(rs_bucket))
            if all_reduce_bucket is not None:
                all_reduce_bucket.all_reduce_output = rs_bucket.move_reduce_scatter_output()
                continue
            reduce_scatter_output = rs_bucket.move_reduce_scatter_output()
            for hsdp_param, param_numel, param_offset in zip(
                rs_bucket.hsdp_params,
                rs_bucket.layout.param_numels,
                rs_bucket.param_offsets,
            ):
                hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output = (
                    reduce_scatter_output.narrow(0, param_offset, param_numel)
                )
        self.reduce_scatter_buckets = []
        self._issue_all_reduce_buckets(self.all_reduce_buckets, async_op)
654
655
656
657
658
659
660
661
662
663
664
665
            if bucket.handle is not None:
                bucket.handle.wait()
                bucket.handle = None
            if bucket.all_reduce_output is None:
                raise RuntimeError("All-reduce output has already been released.")
            output = bucket.all_reduce_output
            if bucket.needs_avg_div and bucket.replicate_world_size > 1:
                output = ms.mint.div(output, bucket.replicate_world_size)
            for hsdp_param, param_numel, param_offset in zip(
                bucket.hsdp_params,
                bucket.layout.param_numels,
                bucket.layout.param_offsets,
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
            self.comm_ctx.all_reduce_param_group = None

    def reset_iter_state(self) -> None:
        """Drop communication references after a completed iteration."""
        for bucket in self.all_gather_buckets:
            bucket.all_gather_result = None
        for bucket in self.reduce_scatter_buckets:
            bucket.unsharded_grads = []
            bucket.reduce_scatter_input = None
            bucket.reduce_scatter_output = None
            bucket.handle = None
        for bucket in self.all_reduce_buckets:
            bucket.all_reduce_output = None
            bucket.handle = None
        self.reduce_scatter_buckets = []
        self.all_reduce_buckets = []
        self.reduce_partial_outputs.clear()
        if self.comm_ctx.pre_param_group is self:
            self.comm_ctx.pre_param_group = None
        if self.comm_ctx.all_reduce_param_group is self:
            self.comm_ctx.all_reduce_param_group = None


class AllReduceParamGroup:
    """Fuse per-parameter RS outputs for one HSDP replicate all-reduce."""
734
735
736
737
738
739
740
741
742
        return aligned_total_bytes // element_size

    def allocate_fused_buffer(self, device: Any) -> None:
        """Allocate and zero the fused all-reduce buffer."""
        del device
        self.fused_buffer = ms.mint.zeros(
            (self.compute_aligned_layout(),),
            dtype=self.reduce_dtype,
        )
744
745
746
747
748
749
750
751
752
    def get_param_buffer_view(self, index: int) -> ms.Tensor:
        """Return one parameter's communication view."""
        if self.fused_buffer is None:
            raise RuntimeError("Fused buffer not allocated. Call allocate_fused_buffer first.")
        return self.fused_buffer.narrow(
            0,
            self.param_offsets[index],
            self.param_numels[index],
        )
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
        param_outputs = []
        for hsdp_param in self.hsdp_params:
            reduced_output = hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output
            if reduced_output is None:
                raise RuntimeError("All-reduce group requires one completed reduce-scatter output per parameter.")
            if reduced_output.dtype != self.reduce_dtype:
                reduced_output = reduced_output.to(self.reduce_dtype)
            partial_output = hsdp_param.reduce_partial_output
            if partial_output is not None:
                if partial_output.dtype != self.reduce_dtype:
                    partial_output = partial_output.to(self.reduce_dtype)
                reduced_output = ms.mint.add(reduced_output, partial_output)
                hsdp_param.reduce_partial_output = None
            param_outputs.append(reduced_output.reshape(-1))
            hsdp_param.clear_reduce_scatter_output()
            hsdp_param.clear_unsharded_source_grad()
        packed_output = ms.mint.cat(param_outputs, dim=0)
795
796
797
798
799
800
801
802
803
        if self.all_reduce_handle is not None:
            self.all_reduce_handle.wait()
            self.all_reduce_handle = None
        if self.fused_buffer is None:
            raise RuntimeError("Fused buffer has already been released.")
        output = self.fused_buffer
        if self.reduce_op == "avg" and self.replicate_world_size > 1:
            output = ms.mint.div(output, self.replicate_world_size)
        for index, hsdp_param in enumerate(self.hsdp_params):
hyper_parallel/platform/mindspore/fully_shard/scheduler.py
153
154
155
156
157
158
159
160
161
162
            )
            comm_ctx.pre_param_group.wait_reduce_scatter_and_issue_all_reduce()
            comm_ctx.pre_param_group = None
        if comm_ctx.all_reduce_param_group is not None:
            comm_ctx.all_reduce_param_group.wait_all_reduce_and_save_grad()
            comm_ctx.all_reduce_param_group = None

    def _finalize_per_param_reductions(self) -> None:
        """Drain the module-tree-local comm_fusion=False communication queues."""
        previous_groups = self.hsdp_state._wait_prev_reduce_scatter()
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
        """Run final source-layout reductions and apply gradients for all states."""
        for hsdp_scheduler in self.scheduler_ctx.all_hsdp_schedulers:
            hsdp_state = hsdp_scheduler.hsdp_state
            if hsdp_state is None:
                continue
            need_synchronize = False
            for hsdp_param in hsdp_state.hsdp_params:
                reduced_grad = hsdp_param.all_reduce_comm_ctx.all_reduce_output
                if reduced_grad is None:
                    reduced_grad = hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output
                if reduced_grad is None:
                    continue
                hsdp_param.all_reduce_tp_replicate_grad_inplace(
                    reduced_grad,
                    hsdp_state.reduce_op_type,
                )
                need_synchronize = hsdp_param.apply_reduced_grad(reduced_grad) or need_synchronize
                hsdp_param.clear_all_reduce_output()
                hsdp_param.clear_reduce_scatter_output()
            hsdp_state._sync_current_stream_if_needed(need_synchronize)

    def reset_iter_state(self) -> None:
        """Reset MindSpore fully_shard iteration state after communication completes."""
        super().reset_iter_state()
        self.hsdp_state.reset_iter_state()
        comm_ctx = self.scheduler_ctx.param_group_comm_ctx
        comm_ctx.pre_param_group = None
        comm_ctx.all_reduce_param_group = None

    def _backward_hook(self):
        """Execute backward hook."""
        if self.scheduler_state == FSDPSchedulerState.BACKWARD:
hyper_parallel/platform/mindspore/fully_shard/state.py
62
63
64
65
66
67
68
69
70
        platform,
        scheduler_ctx,
        device=None,
    ):
        super().__init__(
            cell,
            mesh,
            shard_placement_fn,
            comm_fusion_policy,
81
82
83
84
85
86
87
88
89
    def _init_param_group(self) -> None:
        """Initialize fused communication for the single managed parameter list."""
        self.param_group = None
        if not self.comm_fusion_policy.enable_comm_fusion or not self.hsdp_params:
            return
        self.param_group = HSDPParamGroup(
            self.hsdp_params,
            self.device,
            self.mp_policy,
 94
 95
 96
 97
 98
 99
100
101
102
103
104
        )

    def _move_states_to_device(self) -> None:
        """Move parameters and buffers to the configured runtime device."""
        for module in self.modules:
            for param in module.get_parameters():
                if getattr(param, "_hsdp_param_initialized", False):
                    continue
                param_device = normalize_runtime_device(param.device)
                if param_device in (self.device, "meta"):
                    continue
102
103
104
105
106
107
108
109
110
111
                param_device = normalize_runtime_device(param.device)
                if param_device in (self.device, "meta"):
                    continue
                param.data = param.to(self.device)
            for buffer in module.buffers():
                if normalize_runtime_device(buffer.device) in (self.device, "meta"):
                    continue
                buffer.data = buffer.to(self.device)

    @staticmethod
110
111
112
113
114
115
116
117
118
119
120
121

    @staticmethod
    def _build_param_tp_grad_info(param):
        """Build normalized source-layout metadata for a native DTensor parameter."""
        dtensor_payload = unwrap_dtensor_param(param)
        if dtensor_payload is None:
            return None
        return TPShardMetaInfo(
            mesh=dtensor_payload.device_mesh,
            placements=tuple(dtensor_payload.placements),
            origin_is_dtensor=True,
        )
123
124
125
126
127
128
129
130
131
132
133
134
135
    def _init_hsdp_params(self) -> None:
        """Initialize all fully_shard-managed parameters for this module unit."""
        visited_params = set()
        filtered_params = []
        for module in self.modules:
            for _, param in module.parameters_and_names():
                if param in self.raw_ignored_params:
                    continue
                if getattr(param, "_hsdp_param_initialized", False):
                    continue
                if param in visited_params:
                    continue
                visited_params.add(param)
136
137
138
139
140
141
142
143
144
                filtered_params.append(param)

        module_infos = _get_param_module_infos(filtered_params, tuple(self.modules))
        for param, module_info in zip(filtered_params, module_infos):
            self.hsdp_params.append(
                MindSporeHSDPParamV2(
                    param,
                    module_info,
                    self._build_param_mesh_info(param),
152
153
154
155
156
157
158
159
160

    def _build_param_mesh_info(self, parameter):
        """Return the parameter-specific data-parallel route."""
        if self.mesh.ndim not in (1, 2):
            raise ValueError(
                "fully_shard only supports explicit 1D DP/FSDP meshes or 2D HSDP meshes. "
                f"Got mesh.ndim={self.mesh.ndim}."
            )
        if parameter in self.raw_replicate_params:
163
164
165
166
167
168
169
170
171
                replicate_mesh_dim=0,
            )
        if self.mesh.ndim == 1:
            return FSDPMeshInfo(mesh=self.mesh, shard_mesh_dim=0)
        return HSDPMeshInfo(
            mesh=self.mesh,
            shard_mesh_dim=1,
            replicate_mesh_dim=0,
        )
178
179
180
181
182
183
184
185
186
187
188
189
190
191
    def _validate_cpu_offload_params(self) -> None:
        """Validate CPU placement when CPU offload is configured."""
        if not isinstance(self.offload_policy, CPUOffloadPolicy):
            return
        params_not_on_cpu = [
            hsdp_param
            for hsdp_param in self.hsdp_params
            if not str(hsdp_param.sharded_param.device).lower().startswith("cpu")
        ]
        if params_not_on_cpu:
            raise RuntimeError(
                "HSDP parameters should be materialized on CPU when enabling CPU offloading. "
                "Found following parameters on non-CPU device: "
                f"{[(p._param_fqn, p.sharded_param.device) for p in params_not_on_cpu]}\n"
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
            )

    def lazy_init(self) -> None:
        """Refresh parameter views and validate runtime state before execution."""
        if self.is_shard and not self._reset_sharded_params:
            for hsdp_param in self.hsdp_params:
                hsdp_param.reset_sharded_param()
            self._reset_sharded_params = True
        self._validate_no_meta_params()
        self._validate_cpu_offload_params()
        self._init_mp_dtypes()

    def _validate_no_meta_params(self) -> None:
        """Validate that managed parameters have been materialized."""
        param_names_on_meta = [
217
218
219
220
221
222
223
224
225
226
            )

    def zero_grad(self) -> None:
        """Clear gradients for all managed parameters."""
        for hsdp_param in self.hsdp_params:
            hsdp_param.zero_grad()

    def post_backward_for_comm_fusion(self) -> None:
        """Pipeline fused reduce-scatter and all-reduce communication."""
        logger.debug("post_backward module=%s mode=comm_fusion enter", self)
247
248
249
250
251
252
253
254
255
        return "sum" if all_params_have_tp_grad_info else "avg"

    def _resolve_reduce_op(self) -> str:
        """Return the active mint reduction operation."""
        return self.reduce_op_type

    def post_backward(self, *unused) -> None:  # pylint: disable=unused-argument
        """Accumulate gradients, reshard parameters, and launch reductions."""
        logger.debug(
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
                self.shard()
            for hsdp_param in self.hsdp_params:
                hsdp_param.to_accumulated_grad_if_needed()
            return
        if self.reshard_after_backward:
            self.shard()
        if self.comm_fusion_policy.enable_comm_fusion:
            self.post_backward_for_comm_fusion()
            return

        previous_groups = self._wait_prev_reduce_scatter()
        self._wait_prev_reduce_scatter_without_all_reduce()
        self._issue_reduce_scatter_for_current_module()
        self._issue_prev_fused_all_reduce(previous_groups)

    def _issue_reduce_scatter_for_current_module(self) -> None:
        """Issue per-parameter reduce-scatter and fuse compatible HSDP all-reduces."""
        params_to_reduce = []
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
            self.scheduler_ctx.pre_all_reduce_groups.append(group)

    def _wait_prev_reduce_scatter(self) -> List[AllReduceParamGroup]:
        """Wait previous fused reduce-scatter groups before all-reduce."""
        if not self.scheduler_ctx.pre_all_reduce_groups:
            return []
        previous_groups = list(self.scheduler_ctx.pre_all_reduce_groups)
        self.scheduler_ctx.pre_all_reduce_groups.clear()
        for previous_group in previous_groups:
            logger.debug(
                "post_backward module=%s wait=fused_reduce_scatter group_params=%s",
                self,
                previous_group.hsdp_params,
            )
            for hsdp_param in previous_group.hsdp_params:
                hsdp_param.reduce_scatter_output()
        return previous_groups

    def _issue_prev_fused_all_reduce(self, previous_groups: List[AllReduceParamGroup]) -> None:
        """Launch the previous module's fused all-reduce asynchronously."""
        for previous_group in previous_groups:
            previous_group.accumulate_reduce_partial_outputs()
            logger.debug(
                "post_backward module=%s launch=fused_all_reduce group_params=%s",
                self,
                previous_group.hsdp_params,
            )
            previous_group.issue_async_allreduce()
            self.scheduler_ctx.pending_all_reduce_groups.append(previous_group)

    def _wait_prev_reduce_scatter_without_all_reduce(self) -> None:
        """Wait reduce-scatter outputs that do not enter a DP all-reduce."""
        while self.scheduler_ctx.pre_reduce_scatter_params:
373
374
375
376
377
378
379
380
381
            if not self.requires_all_reduce:
                if hsdp_param.reduce_partial_output is None:
                    hsdp_param.reduce_partial_output = reduced_grad
                else:
                    hsdp_param.reduce_partial_output = ms.mint.add(
                        hsdp_param.reduce_partial_output,
                        reduced_grad,
                    )
                hsdp_param.clear_reduce_scatter_output()
379
380
381
382
383
384
385
386
387
388
389
390
391
                        reduced_grad,
                    )
                hsdp_param.clear_reduce_scatter_output()
            elif hsdp_param.reduce_partial_output is not None:
                reduced_grad = ms.mint.add(
                    reduced_grad,
                    hsdp_param.reduce_partial_output,
                )
                hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output = reduced_grad
                hsdp_param.reduce_partial_output = None
            else:
                hsdp_param.reduce_scatter_comm_ctx.reduce_scatter_output = reduced_grad
            hsdp_param.clear_unsharded_source_grad()
408
409
410
411
412
413
414
415
416
        self.scheduler_ctx.pre_direct_all_reduce_grads.clear()
        self.scheduler_ctx.pre_all_reduce_groups.clear()
        self.scheduler_ctx.pending_all_reduce_groups.clear()
        if self.param_group is not None:
            self.param_group.reset_iter_state()
        for hsdp_param in self.hsdp_params:
            hsdp_param.allgather_comm_ctx.allgather_input = None
            hsdp_param.allgather_comm_ctx.allgather_output = None
            hsdp_param.allgather_comm_ctx.allgather_handle = None
436
437
438
439
440
441

    @staticmethod
    def _sync_current_stream_if_needed(need_synchronize: bool) -> None:
        """Synchronize after a non-blocking CPU-offload copy when required."""
        if need_synchronize:
            ms.runtime.current_stream().synchronize()
hyper_parallel/platform/torch/fully_shard/param.py
243
244
245
246
247
248
249
250
251

    @property
    def reduce_partial_output(self) -> Optional[torch.Tensor]:
        """Return reduce-scatter results accumulated before the final micro-step."""
        return self._reduce_partial_output

    @reduce_partial_output.setter
    def reduce_partial_output(self, value: Optional[torch.Tensor]) -> None:
        self._reduce_partial_output = value
263
264
265
266
267
268
269
270
271
272
273
274
275
        if self.reduce_dtype is not None:
            return self.reduce_dtype
        if grad is not None:
            return grad.dtype
        if self.unsharded_accumulated_grad is not None:
            return self.unsharded_accumulated_grad_data.dtype
        if self.unsharded_param.grad is not None:
            return self.unsharded_grad_data.dtype
        return self.orig_dtype

    def reduce_scatter_output(self) -> Optional[torch.Tensor]:
        """
        Get the reduce-scatter output tensor and wait for asynchronous operation to complete.
305
306
307
308
309
310
311
312
313
314
315
316
        """Clear the all-reduce output tensor to free memory."""
        self.all_reduce_comm_ctx.all_reduce_output = None

    def clear_unsharded_source_grad(self):
        if self.unsharded_accumulated_grad_data is not None:
            self.unsharded_accumulated_grad = None
        elif self.unsharded_param.grad is not None:
            self.unsharded_param.grad = None

    def apply_reduced_grad(self, reduced_grad: torch.Tensor) -> bool:
        """
        Apply reduced gradient to the sharded parameter.
403
404
405
406
407
408
409
410
411
412
413
        base_placements = list(self._get_base_spmd_placements())
        param_data = param.to_local() if self._orig_param_is_dtensor else param
        shard_dim = hsdp_placement.dim
        if param_data.ndim == 0:
            raise ValueError("fully_shard does not support scalar parameters")
        if shard_dim < 0 or shard_dim >= param_data.ndim:
            raise ValueError(
                f"Invalid fully_shard dim {shard_dim} for parameter "
                f"{self._module_info.param_name} with shape {tuple(param_data.shape)}"
            )
        self._orig_size = param_data.size()
500
501
502
503
504
505
506
507
508
            self._sharded_param_data = sharded_param.view(-1)
        else:
            padded_sharded_param = sharded_param.new_zeros(self.padded_sharded_param_size)
            if self.pin_memory and not padded_sharded_param.is_meta:
                padded_sharded_param = padded_sharded_param.pin_memory()
            if sharded_param.numel() > 0:
                padded_sharded_param.narrow(
                    shard_dim,
                    0,
597
598
599
600
601
602
603
604
605
606
607
                f"Expected 1 unsharded_param_buffer, got {len(self.unsharded_param_buffers)}"
            )

        if self.allgather_comm_ctx.allgather_output is not None:
            packed_shape = list(self.sharded_size)
            packed_shape[0] *= self.shard_world_size
            chunks = torch.chunk(
                self.allgather_comm_ctx.allgather_output.view(packed_shape),
                self.shard_world_size,
                dim=0,
            )
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
                self.shard_world_size,
                dim=0,
            )
            # pylint: disable=W0212
            with torch.autograd._unsafe_preserve_version_counter(
                self.unsharded_param_buffers[0]
            ):
                torch.cat(
                    chunks,
                    dim=self.hsdp_placement.dim,
                    out=self.unsharded_param_buffers[0].view(self._orig_size),
                )
            self.allgather_comm_ctx.allgather_output.untyped_storage().resize_(0)
            self.allgather_comm_ctx.allgather_output = None

        if hasattr(self, "_unsharded_param"):
            # Keep one stable ``_unsharded_param`` object across unshard cycles:
            # autograd-facing module state captured during forward must still be
709
710
711
712
713
714
715
716
717
            grad = self.unsharded_param.grad
            if self.reduce_dtype is not None and grad.dtype != self.reduce_dtype:
                grad = grad.to(self.reduce_dtype)
            self.unsharded_param.grad = None
            self.unsharded_accumulated_grad += grad

    def alloc_unsharded_param_buffers(self) -> None:
        """
        Restore unsharded parameter buffers to their full capacity.
716
717
718
719
720
721
722
723
724
        """
        Restore unsharded parameter buffers to their full capacity.
        unsharded_param_buffer is the final storage which should be reffereced by self._unsharded_param
        """
        for tensor in self.unsharded_param_buffers:
            expected_size = tensor.numel() * tensor.itemsize
            storage = tensor.untyped_storage()
            if storage.size() != expected_size:
                storage.resize_(expected_size)
724
725
726
727
728
729
730
731
                storage.resize_(expected_size)

    def free_unsharded_param(self) -> None:
        """Release storage of the unsharded parameter buffers."""
        for tensor in self.unsharded_param_buffers:
            storage = tensor.untyped_storage()
            if storage.size() != 0:
                storage.resize_(0)
745
746
747
748
749
750
751
752
753

    @property
    def _sharded_local_tensor(self) -> torch.Tensor:
        """Return the local tensor backing the persistent sharded DTensor."""
        return self.sharded_param._local_tensor

    @property
    def unsharded_param(self) -> nn.Parameter:
        """Return the full unsharded parameter after all-gather."""
801
802
803
804
805
806
807
808
809

    def _is_same_sharded_local_tensor(self, local_tensor: torch.Tensor) -> bool:
        """Return whether communication storage already aliases the local tensor."""
        if not isinstance(self._sharded_param_data, torch.Tensor):
            return False
        sharded_data_ptr = self._sharded_param_data.untyped_storage().data_ptr()
        return (
            # Empty shards may have a zero data pointer and must still be rebuilt.
            sharded_data_ptr > 0
832
833
834
835
836
837
838
839
840
        local_tensor: torch.Tensor,
    ) -> tuple[torch.Tensor, bool]:
        """Move reset storage to pinned CPU memory when the policy requires it."""
        if self.pin_memory and not local_tensor.is_pinned():
            return local_tensor.cpu().pin_memory(), True
        return local_tensor, False

    def _refresh_sharded_local_tensor(
        self,
850
851
852
853
854
855
856
857
858
            local_view = local_tensor.detach()
        else:
            padded_local_tensor = local_tensor.new_zeros(self.padded_sharded_param_size)
            if self.pin_memory:
                padded_local_tensor = padded_local_tensor.pin_memory()
            if local_tensor.numel() > 0:
                padded_local_tensor.narrow(
                    shard_dim,
                    0,
869
870
871
872
873
874
875
876
877
        set_requires_grad_if_needed(self.sharded_param, local_view)
        self.sharded_param._local_tensor = local_view
        self._update_shardedparam_storage_forcely()
        if not self.sharded_param._local_tensor.is_contiguous():
            raise AssertionError(
                "Expected sharded_param._local_tensor to be contiguous"
            )

    def reset_sharded_param(self) -> None:
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
        """
        all_gather_input = self.all_gather_inputs[0]

        if self.shard_world_size <= 1 or self.mesh_info.shard_process_group is None:
            if len(self.unsharded_param_buffers) == 0:
                self.unsharded_param_buffers = [all_gather_input]
            elif self.unsharded_param_buffers[0] is not all_gather_input:
                # if param_dtype cast or tensor.to caused by cpu_offload
                self.alloc_unsharded_param_buffers()
                _copy_without_bumping_version(self.unsharded_param_buffers[0], all_gather_input)
            self.allgather_comm_ctx.allgather_output = None
            self.allgather_comm_ctx.allgather_handle = None
            return

        self.init_unsharded_param_buffers(
            all_gather_input_numels=[all_gather_input.numel()],
            all_gather_input_dtypes=[all_gather_input.dtype],
951
952
953
954
955
956
957
958
959
        self.allgather_comm_ctx.allgather_output = self.unsharded_param_buffers[0]
        if self.hsdp_placement.dim != 0:
            # Non-dim-0 sharding uses an extra all-gather buffer before
            # restoring the original dimension with chunk + cat.
            self.allgather_comm_ctx.allgather_output = torch.empty_like(
                self.unsharded_param_buffers[0]
            )
        # pylint: disable=W0212
        with torch.autograd._unsafe_preserve_version_counter(
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047

        shard_process_group = self.mesh_info.shard_process_group if isinstance(self.mesh_info, FSDPMeshInfo) else None
        if shard_process_group is None or self.shard_world_size <= 1:
            if output_buffer is not None:
                output_buffer.copy_(self._grad)
                self.reduce_scatter_comm_ctx.reduce_scatter_output = output_buffer
            else:
                self.reduce_scatter_comm_ctx.reduce_scatter_output = self._grad
            self.reduce_scatter_comm_ctx.reduce_scatter_handle = None
            return
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
            if output_buffer.numel() != output_numel:
                raise ValueError(
                    f"output_buffer size mismatch: expected {output_numel}, got {output_buffer.numel()}"
                )
            if output_buffer.dtype != self._grad.dtype:
                raise ValueError(
                    f"output_buffer dtype mismatch: expected {self._grad.dtype}, got {output_buffer.dtype}"
                )
            self.reduce_scatter_comm_ctx.reduce_scatter_output = output_buffer
        else:
            self.reduce_scatter_comm_ctx.reduce_scatter_output = torch.empty(
                output_numel,
                dtype=self._grad.dtype,
1090
1091
1092
1093
1094
1095
1096
1097
        ``all_reduce_comm_ctx``.
        """
        grad = self.reduce_scatter_comm_ctx.reduce_scatter_output
        if grad is None:
            raise RuntimeError("all_reduce_grad requires a completed reduce-scatter output.")
        reduce_dtype = self.reduce_comm_dtype(grad)
        if grad.dtype != reduce_dtype:
            grad = grad.to(reduce_dtype)
1128
1129
1130
1131
1132
1133
1134
1135
1136
            reduced_grad: Final local gradient after FSDP/HSDP reduction.
            reduce_op: Reduction operation shared with the DP communication path.
        """
        if self.tp_grad_info is None or not self.tp_grad_info.placements:
            return
        source_mesh = self.tp_grad_info.mesh
        source_placements = self.tp_grad_info.placements
        replicate_mesh_dims = tuple(
            mesh_dim
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
            for mesh_dim, placement in enumerate(source_placements)
            if placement.is_replicate()
        )
        if not replicate_mesh_dims:
            return
        mesh_dim_names = source_mesh.mesh_dim_names
        if mesh_dim_names is None:
            raise ValueError(
                "TP shard mesh must define mesh_dim_names to all-reduce replicated gradients."
            )
        replicate_mesh_dim_names = tuple(mesh_dim_names[mesh_dim] for mesh_dim in replicate_mesh_dims)
        replicate_mesh = source_mesh[replicate_mesh_dim_names].flatten()
1146
1147
1148
1149
1150
1151
1152
1153
1154
            )
        replicate_mesh_dim_names = tuple(mesh_dim_names[mesh_dim] for mesh_dim in replicate_mesh_dims)
        replicate_mesh = source_mesh[replicate_mesh_dim_names].flatten()
        if replicate_mesh.size() <= 1:
            return
        dist.all_reduce(
            reduced_grad,
            op=reduce_op,
            group=replicate_mesh.get_group(),
hyper_parallel/platform/torch/fully_shard/param_group.py
105
106
107
108
109
110
111
112
113
114
        if any(
            hsdp_param.offload_to_cpu or hsdp_param.sharded_param.device.type == "meta"
            for hsdp_param in self.hsdp_params
        ):
            self.flat_param_buffer = None
            return

        total_numel = sum(hsdp_param._sharded_param_data.numel() for hsdp_param in self.hsdp_params)
        flat_param_buffer = torch.empty(total_numel, dtype=storage_dtype, device=device)
        flat_offset = 0
144
145
146
147
148
149
150
151
152
        surgery, ``load_state_dict``), which silently invalidates the zero-copy
        rebase; comparing storage pointers is what detects that.
        """
        if self.flat_param_buffer is None:
            return False
        flat_storage_ptr = self.flat_param_buffer.untyped_storage().data_ptr()
        return all(
            hsdp_param._sharded_param_data.untyped_storage().data_ptr() == flat_storage_ptr
            for hsdp_param in self.hsdp_params
161
162
163
164
165
166
167
168
169
        before the next bucket runs. No-op when the bucket has no pending result.
        """
        all_gather_result = self.all_gather_result
        if all_gather_result is None or all_gather_result.all_gather_output is None:
            return
        if all_gather_result.handle is not None:
            all_gather_result.handle.wait()
            all_gather_result.handle = None
        all_gather_output = all_gather_result.all_gather_output
211
212
213
214
215
216
217
218
219
                    metadata.param_input_numels,
                    self.hsdp_params,
                ):
                    if hsdp_param.hsdp_placement.dim != 0 and len(input_numels) != 1:
                        raise NotImplementedError(
                            "Fused non-dim-0 all-gather expects one local shard tensor per parameter."
                        )
                    for input_numel, output_buffer in zip(
                        input_numels,
237
238
239
240
241
242
243
244
245
                                out=output_buffer.view(hsdp_param._orig_size),
                            )
                        column_offset += input_numel
                if column_offset != gathered_rows.size(1):
                    raise AssertionError(
                        "Fused all-gather copy-out consumed an unexpected number of elements: "
                        f"{column_offset} != {gathered_rows.size(1)}"
                    )
        all_gather_result.all_gather_input = None
302
303
304
305
306
307
308
309

    def move_reducescatter_output(self) -> torch.Tensor:
        """Transfer exclusive ownership of the completed output to the next stage."""
        if self.reduce_scatter_output is None:
            raise RuntimeError("Reduce-scatter output has already been released.")
        reduce_scatter_output = self.reduce_scatter_output
        self.reduce_scatter_output = None
        return reduce_scatter_output
368
369
370
371
372
373
374
375
376
        param_input_numels.append(input_numels)
        inp_split_sizes.extend(input_numels)
        total_input_numel += sum(input_numels)
    if dtype is None:
        raise ValueError("Cannot build all-gather metadata for an empty parameter bucket.")
    return AllGatherMetadata(
        param_input_dtypes=param_input_dtypes,
        param_input_numels=param_input_numels,
        dtype=dtype,
497
498
499
500
501
502
503
504
505
        for hsdp_param in self.hsdp_params:
            if hsdp_param.shard_world_size <= 1:
                continue
            if not isinstance(hsdp_param.mesh_info, FSDPMeshInfo):
                raise ValueError(
                    f"Fused all-gather expects FSDPMeshInfo, got {type(hsdp_param.mesh_info)}"
                )
            shard_group = hsdp_param.mesh_info.shard_process_group
            communication_dtype = hsdp_param.param_dtype or hsdp_param.orig_dtype
535
536
537
538
539
540
541
542
543
544
545
546
547
548
            )

    def unshard(self, async_op: bool = False) -> None:
        """Launch fused all-gathers for every communication bucket."""
        if self.all_gather_buckets and any(
            bucket.all_gather_result is not None for bucket in self.all_gather_buckets
        ):
            # already triggered by pre_module.prefetch(), return directly.
            return
        self.foreach_all_gather(async_op)

    @torch.no_grad()
    def foreach_all_gather(self, async_op: bool = False) -> None:
        """Initialize ordered buckets and launch their all-gathers."""
553
554
555
556
557
558
559
560
561
562
563
                # fast path, skip copy_in process.
                hsdp_param.unshard(async_op)
        for all_gather_bucket in self.all_gather_buckets:
            if all_gather_bucket.all_gather_result is not None:
                continue
            if self.enable_zero_copy and not all_gather_bucket.is_flat_buffer_valid():
                all_gather_bucket.init_flat_param_buffer(self.device)

            metadata = all_gather_bucket.metadata
            all_gather_output = torch.empty(
                metadata.total_input_numel * all_gather_bucket.shard_world_size,
564
565
566
567
568
569
570
571
572
573
574
575
                dtype=all_gather_bucket.dtype,
                device=self.device,
            )
            if self.enable_zero_copy and all_gather_bucket.is_flat_buffer_valid():
                if all_gather_bucket.flat_param_buffer.dtype == all_gather_bucket.dtype:
                    all_gather_input = all_gather_bucket.flat_param_buffer
                else:
                    all_gather_input = all_gather_bucket.flat_param_buffer.to(all_gather_bucket.dtype)
            else:
                all_gather_inputs = []
                for hsdp_param in all_gather_bucket.hsdp_params:
                    all_gather_inputs.extend(hsdp_param.all_gather_inputs)
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
        grads_by_bucket = {}
        bucket_groups = {}
        for hsdp_param in self.hsdp_params:
            if not hsdp_param.sharded_param.requires_grad:
                continue
            if hsdp_param.unsharded_accumulated_grad is not None:
                unsharded_grad = hsdp_param.unsharded_accumulated_grad_data
            elif hsdp_param.unsharded_param.grad is not None:
                unsharded_grad = hsdp_param.unsharded_grad_data
            else:
                continue
            shard_group = (
                hsdp_param.mesh_info.shard_process_group
                if isinstance(hsdp_param.mesh_info, FSDPMeshInfo)
                else None
643
644
645
646
647
648
649
650
651
            if any(
                hsdp_param.shard_world_size != shard_world_size
                for hsdp_param in hsdp_params
            ):
                raise ValueError("A reduce-scatter bucket must use one shard world size.")
            needs_avg_div = reduce_scatter_reduce_op == dist.ReduceOp.AVG
            communication_op = dist.ReduceOp.SUM if needs_avg_div else reduce_scatter_reduce_op
            param_offsets = []
            param_numels = []
697
698
699
700
701
702
703
704
705
                if any(
                    isinstance(hsdp_param.mesh_info, DDPMeshInfo)
                    for hsdp_param in reduce_scatter_bucket.hsdp_params[1:]
                ):
                    raise ValueError(
                        "A reduce-scatter bucket cannot mix parameters with and without "
                        "a subsequent all-reduce."
                    )
                continue
752
753
754
755
756
757
758
759
760
                )

            for hsdp_param in reduce_scatter_bucket.hsdp_params:
                if hsdp_param.unsharded_accumulated_grad is not None:
                    hsdp_param.unsharded_accumulated_grad = None
                else:
                    hsdp_param.unsharded_param.grad = None
            reduce_scatter_bucket.unsharded_grads = []
            if not reduce_scatter_bucket.uses_collective:
778
779
780
781
782
783
784
785
786
        self.reduce_scatter_buckets = self._build_reduce_scatter_buckets(
            reduce_scatter_reduce_op,
        )
        if not self.reduce_scatter_buckets:
            return
        self.all_reduce_buckets = self._build_all_reduce_buckets(self.reduce_scatter_buckets)
        self._issue_reduce_scatter_buckets(async_op)
        if async_op:
            self.comm_ctx.pre_param_group = self
784
785
786
787
788
789
790
791
792
793
        self._issue_reduce_scatter_buckets(async_op)
        if async_op:
            self.comm_ctx.pre_param_group = self
        else:
            self.wait_reduce_scatter_and_issue_all_reduce(async_op=False)
            self.wait_all_reduce_and_save_grad()
        return

    def _wait_reduce_scatter_buckets(self) -> None:
        """Wait prepared RS buckets and finish shard-dimension averaging."""
796
797
798
799
800
801
802
803
804
                reduce_scatter_bucket.handle.wait()
                reduce_scatter_bucket.handle = None
            reduce_scatter_bucket.reduce_scatter_input = None
            if reduce_scatter_bucket.reduce_scatter_output is None:
                raise RuntimeError("Reduce-scatter bucket has not been prepared.")
            if reduce_scatter_bucket.needs_avg_div and reduce_scatter_bucket.shard_world_size > 1:
                reduce_scatter_bucket.reduce_scatter_output.div_(
                    reduce_scatter_bucket.shard_world_size
                )
811
812
813
814
815
816
817
818
819
    ) -> None:
        """Launch in-place AR for bucket outputs transferred from RS."""
        for all_reduce_bucket in all_reduce_buckets:
            if all_reduce_bucket.all_reduce_output is None:
                raise RuntimeError("All-reduce bucket has not received its reduce-scatter output.")
            if not all_reduce_bucket.uses_collective:
                all_reduce_bucket.handle = None
                continue
            all_reduce_bucket.handle = dist.all_reduce(
886
887
888
889
890
891
892
893
894
            if all_reduce_bucket.handle is not None:
                all_reduce_bucket.handle.wait()
                all_reduce_bucket.handle = None
            if all_reduce_bucket.all_reduce_output is None:
                raise RuntimeError("All-reduce output has already been released.")
            if all_reduce_bucket.needs_avg_div and all_reduce_bucket.replicate_world_size > 1:
                all_reduce_bucket.all_reduce_output.div_(all_reduce_bucket.replicate_world_size)
            for hsdp_param, param_numel, param_offset in zip(
                all_reduce_bucket.hsdp_params,
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
        if self.fused_buffer is None:
            return
        for index, hsdp_param in enumerate(self.hsdp_params):
            if hsdp_param.reduce_partial_output is None:
                continue
            partial_output = hsdp_param.reduce_partial_output
            if partial_output.dtype != self.reduce_dtype:
                partial_output = partial_output.to(self.reduce_dtype)
            self.get_param_buffer_view(index).add_(partial_output.view(-1))
            hsdp_param.reduce_partial_output = None

    def issue_async_allreduce(self) -> None:
hyper_parallel/platform/torch/fully_shard/scheduler.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
    def _finalize_comm_fusion_reductions(self) -> None:
        """Drain the comm_fusion=True RS/AR pipeline."""
        comm_ctx = self.scheduler_ctx.param_group_comm_ctx
        if comm_ctx.all_reduce_param_group is not None:
            logger.debug(
                "hook=root_backward_hook wait=comm_fusion_all_reduce module=%s",
                self.hsdp_state,
            )
            comm_ctx.all_reduce_param_group.wait_all_reduce_and_save_grad()
            comm_ctx.all_reduce_param_group = None
        if comm_ctx.pre_param_group is not None:
            logger.debug(
                "hook=root_backward_hook wait=comm_fusion_reduce_scatter module=%s",
                self.hsdp_state,
            )
            comm_ctx.pre_param_group.wait_reduce_scatter_and_issue_all_reduce()
            comm_ctx.pre_param_group = None
        if comm_ctx.all_reduce_param_group is not None:
            comm_ctx.all_reduce_param_group.wait_all_reduce_and_save_grad()
            comm_ctx.all_reduce_param_group = None

    def _finalize_per_param_reductions(self) -> None:
        """Drain the module-tree-local comm_fusion=False RS/AR queues."""
        # A fused root may own non-fused children, so always drain the tree queues.
183
184
185
186
187
188
189
190
191
        """Run final TP replicate reductions and apply gradients for all states."""
        for hsdp_scheduler in self.scheduler_ctx.all_hsdp_schedulers:
            hsdp_state = hsdp_scheduler.hsdp_state
            if hsdp_state is None:
                continue
            need_synchronize = False
            for hsdp_param in hsdp_state.hsdp_params:
                reduced_grad = hsdp_param.all_reduce_comm_ctx.all_reduce_output
                if reduced_grad is None:
hyper_parallel/platform/torch/fully_shard/state.py
136
137
138
139
140
141
142
143
144
    ) -> Optional[TPShardMetaInfo]:
        """Build normalized source-layout metadata for one managed parameter."""
        if isinstance(param, DTensor):
            if self.tp_grad_infos is not None:
                raise ValueError(
                    "tp_grad_infos cannot be provided when fully_shard manages a native DTensor parameter"
                )
            return TPShardMetaInfo(
                mesh=param.device_mesh,
361
362
363
364
365
366
367
368
369
                )
                hsdp_param.reduce_scatter_grad(
                    reduce_op=self.reduce_op_type,
                )
                self.scheduler_ctx.pre_reduce_scatter_params.append(hsdp_param)

        # Handle params that need all_reduce (HSDP with multiple replicas)
        for group_key, hsdp_params in groups_by_comm.items():
            if group_key is None:
404
405
406
407
408
409
410
411
412
413
        Returns:
            List of previous AllReduceParamGroups (one per communication group).
        """
        if self.scheduler_ctx.pre_all_reduce_groups:
            prev_groups = list(self.scheduler_ctx.pre_all_reduce_groups)
            self.scheduler_ctx.pre_all_reduce_groups.clear()
            for prev_group in prev_groups:
                logger.debug(
                    "post_backward module=%s wait=fused_reduce_scatter group_params=%s",
                    self,
432
433
434
435
436
437
438
439
440
        Args:
            prev_groups: Previous parameter groups whose all-reduce should be issued.
        """
        for prev_group in prev_groups:
            prev_group.accumulate_reduce_partial_outputs()
            logger.debug(
                "post_backward module=%s launch=fused_all_reduce group_params=%s",
                self,
                prev_group.hsdp_params,
439
440
441
442
443
444
445
446
                self,
                prev_group.hsdp_params,
            )
            prev_group.issue_async_allreduce()
            self.scheduler_ctx.pending_all_reduce_groups.append(prev_group)

    def _wait_prev_reduce_scatter_without_all_reduce(self) -> None:
        """Wait previous RS outputs that do not enter a replicate all-reduce.
468
469
470
471
472
473
474
475
476
477
478
                reduced_grad.add_(pre_hsdp_param.reduce_partial_output)
                pre_hsdp_param.reduce_partial_output = None

            if pre_hsdp_param.unsharded_accumulated_grad_data is not None:
                pre_hsdp_param.unsharded_accumulated_grad = None
            elif pre_hsdp_param.unsharded_param.grad is not None:
                pre_hsdp_param.unsharded_param.grad = None

    def wait_and_split_all_reduce_work_groups(self) -> None:
        """Wait fused all-reduce work and expose each parameter result."""
        for group in self.scheduler_ctx.pending_all_reduce_groups:
480
481
482
483
484
485
486
487
488
                "post_backward module=%s wait=fused_all_reduce group_params=%s",
                self,
                group.hsdp_params,
            )
            group.wait_and_split_grads()
        self.scheduler_ctx.pending_all_reduce_groups.clear()

    def reset_iter_state(self) -> None:
        """Clear Torch communication bookkeeping without clearing optimizer gradients."""
525
526
527
528
529
530
531
532
533
534
535
536
        self.reduce_op_type = reduce_op_value

    def _sync_current_stream_if_needed(self, need_synchronize):
        if need_synchronize:
            if self.device.type == "npu":
                torch.npu.current_stream().synchronize()
            elif self.device.type == "cuda":
                torch.cuda.current_stream().synchronize()
            else:
                raise NotImplementedError(
                    f"Unsupported device type {self.device.type} for synchronization after CPU offload."
                )
hyper_parallel/trainer/base.py
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
            hsdp_param.param_dtype = None
            hsdp_param.reduce_dtype = (
                None if target_reduce_dtype == target_dtype else target_reduce_dtype
            )
            hsdp_param.unsharded_param_buffers = []
            hsdp_param.reset_sharded_param()
            if hasattr(hsdp_param, "_unsharded_param"):
                delattr(hsdp_param, "_unsharded_param")

        def _refresh_hsdp_state_dtype(state) -> None:
            if state.param_group is None:
                return
            state.param_group.reset_iter_state()
            state.param_group.all_gather_buckets = []

        for state in self._iter_hsdp_states():
            buckets = (
                getattr(state, 'replicate_params', []) or [],