Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / fully_shard / hsdp_param.py: 96%
160 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 2025 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"""HSDP parameter"""
17from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
18from hyper_parallel.core.dtensor.dtensor import DTensor
19from hyper_parallel.core.dtensor.placement_types import Replicate
20from hyper_parallel.core.fully_shard.hsdp_utils import (
21 FullyShardParamMode,
22 GroupInfo,
23 get_rank_list_for_axes,
24 get_split_rank_lists_for_axes,
25)
26from hyper_parallel.core.fully_shard.utils import DDPMeshInfo, FSDPMeshInfo
27from hyper_parallel.platform import get_platform
29platform = get_platform()
30_GROUP_INFO_CACHE = {}
33def _build_group_info_from_rank_list(group_name: str, rank_list) -> GroupInfo:
34 """Create group metadata from an explicit rank list."""
35 normalized_rank_list = tuple(sorted(int(rank) for rank in rank_list))
36 if len(normalized_rank_list) <= 1:
37 return GroupInfo(f"{group_name}_invalid", None, 1)
38 if normalized_rank_list in _GROUP_INFO_CACHE:
39 cached_group = _GROUP_INFO_CACHE[normalized_rank_list]
40 return GroupInfo(str(normalized_rank_list), cached_group, len(normalized_rank_list))
41 try:
42 group = platform.create_group(list(normalized_rank_list))
43 except (RuntimeError, ValueError): # pragma: no cover - UT may run without dist init
44 group = None
45 _GROUP_INFO_CACHE[normalized_rank_list] = group
46 return GroupInfo(str(normalized_rank_list), group, len(normalized_rank_list))
49def _build_group_info_from_process_group(
50 group_name: str,
51 process_group,
52 rank_size: int,
53 *,
54 resolved_group_name: str | None = None,
55) -> GroupInfo:
56 """Create group metadata from an existing process group."""
57 if process_group is None or rank_size <= 1:
58 return GroupInfo(f"{group_name}_invalid", None, 1)
59 return GroupInfo(resolved_group_name or group_name, process_group, rank_size)
62class HSDPParamV2:
63 """
64 HSDP parameter.
65 """
67 def __repr__(self) -> str:
68 """Stable debug name used in log lines.
70 Prefers the parameter FQN (assigned by the root forward pre-hook); before
71 that, falls back to the owning module class plus param name and object id.
72 Logging's ``%s`` calls this lazily -- only when a record is emitted.
73 """
74 fqn = getattr(self, "_param_fqn", None)
75 if fqn:
76 return str(fqn)
77 module_info = getattr(self, "_module_info", None)
78 if module_info is not None:
79 return f"{module_info.module.__class__.__name__}.{module_info.param_name}@{id(self):x}"
80 return f"{self.__class__.__name__}@{id(self):x}"
82 def __init__(
83 self,
84 param,
85 module_info,
86 mesh_info,
87 post_forward_mesh_info,
88 shard_placement_fn,
89 mp_policy,
90 offload_policy,
91 threshold,
92 ):
93 """
94 Initialize HSDPParamV2.
96 Args:
97 param (nn.Parameter): The original parameter to shard.
98 module_info (ParamModuleInfo): Ownership and shared-weight metadata for the parameter.
99 mesh_info (FSDPMeshInfo): Mesh topology describing shard/replicate dimensions.
100 post_forward_mesh_info: Mesh info used after forward (reserved for subclass use).
101 shard_placement_fn (Callable, optional): Returns a Shard placement for the parameter,
102 or None to use default (Shard(0)).
103 mp_policy (MixedPrecisionPolicy, optional): Mixed precision dtype policy.
104 offload_policy (OffloadPolicy, optional): CPU offload policy.
105 threshold: Minimum parameter size to enable sharding (reserved for subclass use).
106 """
107 raise NotImplementedError("HSDP param subclasses must implement __init__")
109 def _init_sharded_param(self, param, shard_placement_fn):
110 """add and init sharded param"""
111 raise NotImplementedError("HSDP param subclasses must implement _init_sharded_param")
113 def init_dtype_attrs(self, mp_policy):
114 """Initialize dtype attributes from mixed precision policy."""
115 raise NotImplementedError("HSDP param subclasses must implement init_dtype_attrs")
117 def init_all_gather_outputs(
118 self, all_gather_input_numels, all_gather_input_dtypes, world_size, device, force_recreate=False
119 ):
120 """Allocate or reuse output buffers for all-gather communication."""
121 raise NotImplementedError("HSDP param subclasses must implement init_all_gather_outputs")
123 def init_unsharded_param(self):
124 """Reconstruct the full unsharded parameter from all-gather outputs."""
125 raise NotImplementedError("HSDP param subclasses must implement init_unsharded_param")
127 def to_sharded(self):
128 """Transition parameter from unsharded back to sharded state and free unsharded storage."""
129 raise NotImplementedError("HSDP param subclasses must implement to_sharded")
131 def to_unsharded(self):
132 """Transition parameter to unsharded state after all-gather completes."""
133 raise NotImplementedError("HSDP param subclasses must implement to_unsharded")
135 def to_sharded_dtensor(self, tensor):
136 """Wrap a local sharded tensor as a DTensor with the correct mesh and placements."""
137 raise NotImplementedError("HSDP param subclasses must implement to_sharded_dtensor")
139 def to_accumulated_grad_if_needed(self):
140 """Move unsharded grad to accumulated grad buffer if dtype conversion is required."""
141 raise NotImplementedError("HSDP param subclasses must implement to_accumulated_grad_if_needed")
143 def accumulate_unsharded_grad_if_needed(self):
144 """Accumulate unsharded param grad into accumulated grad buffer if both exist."""
145 raise NotImplementedError("HSDP param subclasses must implement accumulate_unsharded_grad_if_needed")
147 def alloc_all_gather_outputs(self):
148 """Resize all-gather output buffers to their full capacity for communication."""
149 raise NotImplementedError("HSDP param subclasses must implement alloc_all_gather_outputs")
151 def free_unsharded_param(self):
152 """Release storage of all-gather outputs and inner tensors to free device memory."""
153 raise NotImplementedError("HSDP param subclasses must implement free_unsharded_param")
155 @property
156 def all_gather_inputs(self):
157 """Return the local sharded tensor(s) to use as input for all-gather communication."""
158 raise NotImplementedError("HSDP param subclasses must implement all_gather_inputs")
160 @property
161 def unsharded_param(self):
162 """Return the full unsharded parameter after all-gather."""
163 raise NotImplementedError("HSDP param subclasses must implement unsharded_param")
165 @property
166 def unsharded_grad_data(self):
167 """Return the unsharded_param.grad."""
168 raise NotImplementedError("HSDP param subclasses must implement unsharded_grad_data")
170 @property
171 def unsharded_accumulated_grad_data(self):
172 """Return the unsharded accumulated gradient buffer."""
173 raise NotImplementedError("HSDP param subclasses must implement unsharded_accumulated_grad_data")
175 @property
176 def _sharded_local_tensor(self):
177 """Return the underlying local tensor of the sharded DTensor parameter."""
178 raise NotImplementedError("HSDP param subclasses must implement _sharded_local_tensor")
180 def _get_unsharded_param_data(self, async_op=False):
181 """Perform all-gather to obtain unsharded parameter data, returning (tensor, handle)."""
182 raise NotImplementedError("HSDP param subclasses must implement _get_unsharded_param_data")
184 def unshard(self, async_op=False):
185 """Trigger all-gather to unshard the parameter, optionally asynchronously."""
186 raise NotImplementedError("HSDP param subclasses must implement unshard")
188 def wait_for_unshard(self):
189 """Wait for all-gather to complete and transition parameter to unsharded state."""
190 raise NotImplementedError("HSDP param subclasses must implement wait_for_unshard")
192 def shard(self):
193 """Transition parameter from unsharded back to sharded state."""
194 raise NotImplementedError("HSDP param subclasses must implement shard")
196 def reduce_scatter_grad(self):
197 """Perform reduce-scatter on the unsharded gradient to produce a sharded gradient."""
198 raise NotImplementedError("HSDP param subclasses must implement reduce_scatter_grad")
200 def all_reduce_grad(self):
201 """Perform all-reduce on gradient across the replicate dimension (HSDP mode only)."""
202 raise NotImplementedError("HSDP param subclasses must implement all_reduce_grad")
204 def _resolve_process_group_name(self, group_name: str, process_group) -> str:
205 """Resolve the name recorded in GroupInfo for an existing process group."""
206 del process_group
207 return group_name
209 def _get_base_spmd_placements(self) -> tuple:
210 """Return placements before explicit data-parallel semantics are applied."""
211 if (
212 getattr(self, "param_mode", None) == FullyShardParamMode.DTENSOR_UNIFIED
213 and getattr(self, "_orig_param_is_dtensor", False)
214 ):
215 self._spmd_mesh = DeviceMesh.concatenate([self.mesh_info.mesh, self._orig_dtensor_mesh])
216 dp_prefix_placements = tuple(Replicate() for _ in range(self.mesh_info.mesh.ndim))
217 return dp_prefix_placements + tuple(self._orig_dtensor_placements)
219 if (
220 getattr(self, "param_mode", None) == FullyShardParamMode.DTENSOR_COMPAT
221 and getattr(self, "_orig_param_is_dtensor", False)
222 ):
223 self._spmd_mesh = self._orig_dtensor_mesh
224 return tuple(self._orig_dtensor_placements)
226 self._spmd_mesh = self.mesh_info.mesh
227 return tuple(Replicate() for _ in range(self._spmd_mesh.ndim))
229 def _get_data_parallel_shard_placement(self, placements: list, shard_placement):
230 """Return the placement to apply on the explicit fully_shard dimension."""
231 del placements
232 return shard_placement
234 def _apply_data_parallel_placements(self, placements: list, shard_placement) -> tuple:
235 """Apply explicit DDP/FSDP placements on top of the base SPMD layout."""
236 if len(placements) != self._spmd_mesh.ndim:
237 raise AssertionError(
238 f"Expected {self._spmd_mesh.ndim} unified placements, got {len(placements)}: {placements}"
239 )
241 spmd_replicate_mesh_dim = getattr(self, "_spmd_replicate_mesh_dim", None)
242 if (
243 isinstance(self.mesh_info, DDPMeshInfo)
244 and spmd_replicate_mesh_dim is not None
245 and not getattr(self, "_orig_param_is_dtensor", False)
246 ):
247 placements[spmd_replicate_mesh_dim] = Replicate()
249 spmd_shard_mesh_dim = getattr(self, "_spmd_shard_mesh_dim", None)
250 if (
251 getattr(self, "uses_param_shard", False)
252 and isinstance(self.mesh_info, FSDPMeshInfo)
253 and spmd_shard_mesh_dim is not None
254 ):
255 placements[spmd_shard_mesh_dim] = self._get_data_parallel_shard_placement(
256 placements, shard_placement
257 )
258 return tuple(placements)
260 def _init_group_infos(self) -> None:
261 """Initialize sharded/unsharded communication groups from the current layout."""
262 if (
263 getattr(self, "uses_param_shard", False)
264 and getattr(self, "is_sharded", False)
265 and isinstance(self.mesh_info, FSDPMeshInfo)
266 ):
267 resolved_group_name = self._resolve_process_group_name(
268 "fully_shard_sharded_group",
269 self.mesh_info.shard_process_group,
270 )
271 self.sharded_group_info = _build_group_info_from_process_group(
272 "fully_shard_sharded_group",
273 self.mesh_info.shard_process_group,
274 self.mesh_info.shard_mesh_size,
275 resolved_group_name=resolved_group_name,
276 )
277 else:
278 self.sharded_group_info = GroupInfo("fully_shard_sharded_group_invalid", None, 1)
280 self.unsharded_group_info = self._build_layout_driven_group_info()
281 self.shard_size = self.sharded_group_info.rank_size
282 self.dp_size = self.unsharded_group_info.rank_size
283 self.rank_size = max(1, self.shard_size * self.dp_size)
285 def _build_layout_driven_group_info(self) -> GroupInfo:
286 """Build the group that should all-reduce an unsharded gradient from the final layout."""
287 group_axes = [
288 axis
289 for axis, placement in enumerate(self._spmd_placements)
290 if placement.is_replicate()
291 ]
292 spmd_shard_mesh_dim = getattr(self, "_spmd_shard_mesh_dim", None)
293 if getattr(self, "uses_param_shard", False) and spmd_shard_mesh_dim is not None:
294 group_axes = [axis for axis in group_axes if axis != spmd_shard_mesh_dim]
295 if not group_axes:
296 return GroupInfo("fully_shard_unsharded_group_invalid", None, 1)
298 group_dim_names = getattr(self._spmd_mesh, "mesh_dim_names", None)
299 if group_dim_names:
300 try:
301 mesh_axis_names = tuple(group_dim_names[axis] for axis in group_axes)
302 if len(mesh_axis_names) == 1:
303 axis_name = mesh_axis_names[0]
304 process_group = self._spmd_mesh.get_group(axis_name)
305 if process_group is not None:
306 rank_size = self._spmd_mesh.mesh_shape[group_dim_names.index(axis_name)]
307 resolved_group_name = self._resolve_process_group_name(
308 "fully_shard_unsharded_group",
309 process_group,
310 )
311 return _build_group_info_from_process_group(
312 "fully_shard_unsharded_group",
313 process_group,
314 rank_size,
315 resolved_group_name=resolved_group_name,
316 )
318 split_rank_lists = get_split_rank_lists_for_axes(self._spmd_mesh, group_axes)
319 process_group = platform.split_group(split_ranks=split_rank_lists)
320 if process_group is not None:
321 rank_size = 1
322 for axis in group_axes:
323 rank_size *= self._spmd_mesh.mesh_shape[axis]
324 resolved_group_name = self._resolve_process_group_name(
325 "fully_shard_unsharded_group",
326 process_group,
327 )
328 return _build_group_info_from_process_group(
329 "fully_shard_unsharded_group",
330 process_group,
331 rank_size,
332 resolved_group_name=resolved_group_name,
333 )
334 except (
335 AssertionError,
336 AttributeError,
337 KeyError,
338 RuntimeError,
339 TypeError,
340 ValueError,
341 ):
342 pass
344 rank_list = get_rank_list_for_axes(self._spmd_mesh, group_axes)
345 return _build_group_info_from_rank_list("fully_shard_unsharded_group", rank_list)
347 def _normalize_unsharded_grad_to_local(self, grad, *, reduce_partial_dtensor: bool = True):
348 """Normalize a pending gradient to the local tensor expected by fully_shard collectives."""
349 if not isinstance(grad, DTensor):
350 return grad
352 if reduce_partial_dtensor and any(placement.is_partial() for placement in grad.placements):
353 grad = grad.reduce_partial()
355 orig_dtensor_mesh = getattr(self, "_orig_dtensor_mesh", None)
356 orig_dtensor_placements = getattr(self, "_orig_dtensor_placements", None)
357 mesh_mismatch = (
358 orig_dtensor_mesh is not None
359 and grad.device_mesh.to_hash() != orig_dtensor_mesh.to_hash()
360 )
361 placement_mismatch = (
362 orig_dtensor_placements is not None
363 and tuple(grad.placements) != tuple(orig_dtensor_placements)
364 )
365 if mesh_mismatch or placement_mismatch:
366 grad = grad.redistribute(orig_dtensor_mesh, orig_dtensor_placements)
367 return grad.to_local()