Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / fully_shard / hsdp_utils.py: 98%
154 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-25 04:27 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-08-25 04:27 +0800
1# Copyright 2025-2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""HSDP optimizer shared level"""
16from dataclasses import dataclass, field
17from enum import auto, Enum
18from typing import Any, List, Optional, Sequence
20import numpy as np
22from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
23from hyper_parallel.core.dtensor.dtensor import DTensor
24from hyper_parallel.platform import get_platform
25from hyper_parallel.platform.platform import PlatformType
27platform = get_platform()
30class ShardedState(Enum):
31 """
32 Parameter shard state
33 """
34 SHARDED = auto()
35 UNSHARDED = auto()
38class FullyShardParamMode(Enum):
39 """Internal fully_shard execution modes derived from parameter layout."""
41 LOCAL_PARAM = auto()
42 DTENSOR_COMPAT = auto()
43 DTENSOR_UNIFIED = auto()
46@dataclass
47class GroupInfo:
48 """Communication group metadata used by fully_shard."""
50 group_name: str
51 group: Any
52 rank_size: int
55class FSDPSchedulerState(Enum):
56 """
57 Scheduler state:
58 - PRE_FORWARD:
59 already run hook before forward.
60 - FORWARD:
61 already run hook after forward.
62 - PRE_BACKWARD:
63 already run hook before backward.
64 - BACKWARD:
65 already run hook after backward.
66 """
67 PRE_FORWARD = auto()
68 FORWARD = auto()
69 PRE_BACKWARD = auto()
70 BACKWARD = auto()
73@dataclass
74class ParamModuleInfo:
75 """
76 Tracks parameter ownership and supports shared weights in HSDP.
78 This dataclass maintains the mapping between a parameter and its module(s),
79 enabling parameter swapping during sharding/unsharding transitions. Shared
80 weights are parameters referenced by multiple modules (e.g., tied embeddings).
82 This class tracks all references to ensure proper parameter replacement during
83 sharding/unsharding operations.
85 Attributes:
86 module: The module that owns this parameter.
87 param_name: Attribute name of the parameter in the module (e.g., "weight").
88 shared_modules: List of other modules sharing this same parameter object.
89 shared_param_names: Corresponding parameter names in shared_modules (aligned by index).
90 """
91 module: platform.Module
92 param_name: str
93 shared_modules: List[platform.Module] = field(default_factory=list)
94 shared_param_names: List[str] = field(default_factory=list)
97def _named_parameters_with_duplicates(
98 module: platform.Module, **kwargs: Any
99) -> list[tuple[str, platform.Parameter]]:
100 """
101 This API is required as some modules overwrite `named_parameters()` but do not support
102 `remove_duplicate`.
103 """
104 if "remove_duplicate" in kwargs:
105 raise AssertionError(
106 "_named_parameters_with_duplicates cannot be used with `remove_duplicate` argument."
107 )
109 def get_named_parameters(module, **kwargs):
110 if platform.platform_type == PlatformType.PYTORCH:
111 return module.named_parameters(**kwargs)
112 return module.parameters_and_names(expand=False)
113 kwargs["remove_duplicate"] = False
114 try:
115 ret = list(get_named_parameters(module, **kwargs))
116 except AssertionError:
117 kwargs.pop("remove_duplicate")
118 ret = list(get_named_parameters(module, **kwargs))
119 return ret
122def _get_param_module_infos(
123 params: list[platform.Parameter], modules: tuple[platform.Module, ...]
124) -> list['ParamModuleInfo']:
125 """
126 Shared parameter: lin1.weight = lin2.weight
127 Shared module: mlp.lin1 = mlp.lin2
128 We do not remove duplicates when traversing both modules and parameters to
129 find shared modules' parameters and shared parameters within a module.
130 """
131 params_set = set(params)
132 param_to_module_info: dict[platform.Parameter, ParamModuleInfo] = {}
134 def get_named_modules(module):
135 if platform.platform_type == PlatformType.PYTORCH:
136 return module.named_modules(remove_duplicate=False)
137 return module.cells_and_names()
139 for module in modules:
140 for _, submodule in get_named_modules(module):
141 for param_name, param in _named_parameters_with_duplicates(
142 submodule, recurse=False
143 ):
144 if param in params_set:
145 if param not in param_to_module_info:
146 param_to_module_info[param] = ParamModuleInfo(
147 submodule, param_name
148 )
149 else:
150 param_to_module_info[param].shared_modules.append(submodule)
151 param_to_module_info[param].shared_param_names.append(
152 param_name
153 )
154 if len(param_to_module_info) != len(params):
155 raise AssertionError(f"Some parameters are not in the module tree of {modules}")
156 return [param_to_module_info[param] for param in params]
159def get_managed_modules_parameters(
160 modules: Sequence[platform.Module],
161 ignored_params: Optional[Sequence[platform.Parameter]] = None,
162) -> list[platform.Parameter]:
163 """Collect deduplicated parameters from ``modules`` while skipping ignored params.
165 Parameters that were already initialized by an inner ``fully_shard`` instance
166 are intentionally excluded so nested ``fully_shard(mesh=None)`` resolves mesh
167 mode from the parameters that the current wrapper will actually manage.
168 """
169 params: list[platform.Parameter] = []
170 ignored_params_set = set(ignored_params or ())
171 visited_params: set[platform.Parameter] = set()
172 for mod in modules:
173 for _, param in platform.parameters_dict(mod):
174 if param in ignored_params_set or param in visited_params:
175 continue
176 if getattr(param, "_hsdp_param_initialized", False):
177 continue
178 visited_params.add(param)
179 params.append(param)
180 return params
183def infer_fully_shard_param_mode(
184 mesh: Optional[DeviceMesh],
185 params: Optional[Sequence[Any]] = None,
186) -> FullyShardParamMode:
187 """Infer the compatibility parameter mode from parameter layout and mesh."""
188 has_dtensor_param = any(is_dtensor_managed_param(param) for param in params or ())
189 if not has_dtensor_param:
190 return FullyShardParamMode.LOCAL_PARAM
191 if mesh is None:
192 return FullyShardParamMode.DTENSOR_COMPAT
193 return FullyShardParamMode.DTENSOR_UNIFIED
196def unwrap_dtensor_param(param: Any) -> Optional[DTensor]:
197 """Return the DTensor payload carried by ``param`` if one exists."""
198 if isinstance(param, DTensor):
199 return param
200 param_data = getattr(param, "data", None)
201 if isinstance(param_data, DTensor):
202 return param_data
203 if all(hasattr(param, attr) for attr in ("_device_mesh", "_placements", "_local_tensor")):
204 return param
205 return None
208def is_dtensor_managed_param(param: Any) -> bool:
209 """Return whether a parameter already carries DTensor layout metadata."""
210 return unwrap_dtensor_param(param) is not None
213def get_dtensor_managed_mesh(param: Any) -> Optional[DeviceMesh]:
214 """Return the DTensor mesh carried by ``param`` if one exists."""
215 payload = unwrap_dtensor_param(param)
216 if payload is None:
217 return None
218 return getattr(payload, "device_mesh", getattr(payload, "_device_mesh", None))
221def get_rank_list_for_axes(
222 mesh: DeviceMesh,
223 axes: Sequence[int],
224 rank: Optional[int] = None,
225) -> list[int]:
226 """Return ranks that vary along ``axes`` and keep all other coordinates fixed."""
227 if rank is None:
228 rank = mesh.rank
229 if rank not in mesh.rank_list:
230 raise ValueError(f"Rank {rank} not found in mesh rank list {mesh.rank_list}.")
232 normalized_axes = tuple(sorted(set(axes)))
233 if len(normalized_axes) == 0:
234 return [rank]
236 mesh_tensor = np.array(mesh.rank_list).reshape(mesh.mesh_shape)
237 rank_index = mesh.rank_list.index(rank)
238 coord = [0] * len(mesh.mesh_shape)
239 temp = rank_index
240 for i in range(len(mesh.mesh_shape) - 1, -1, -1):
241 coord[i] = temp % mesh.mesh_shape[i]
242 temp //= mesh.mesh_shape[i]
243 mesh_slice = []
244 for axis, axis_coord in enumerate(coord):
245 mesh_slice.append(slice(None) if axis in normalized_axes else axis_coord)
246 selected = mesh_tensor[tuple(mesh_slice)]
247 return [int(item) for item in np.array(selected).reshape(-1).tolist()]
250def get_split_rank_lists_for_axes(
251 mesh: DeviceMesh,
252 axes: Sequence[int],
253) -> list[list[int]]:
254 """Return all rank lists induced by varying ``axes`` and fixing the complementary axes."""
255 normalized_axes = tuple(sorted(set(axes)))
256 if len(normalized_axes) == 0:
257 return [[int(rank) for rank in mesh.rank_list]]
259 mesh_tensor = np.array(mesh.rank_list).reshape(mesh.mesh_shape)
260 complementary_axes = tuple(
261 axis for axis in range(len(mesh.mesh_shape)) if axis not in normalized_axes
262 )
263 if len(complementary_axes) == 0:
264 return [[int(item) for item in np.array(mesh_tensor).reshape(-1).tolist()]]
266 complementary_shape = tuple(mesh.mesh_shape[axis] for axis in complementary_axes)
267 split_rank_lists: list[list[int]] = []
268 for complementary_coord in np.ndindex(*complementary_shape):
269 mesh_slice = []
270 coord_idx = 0
271 for axis in range(len(mesh.mesh_shape)):
272 if axis in normalized_axes:
273 mesh_slice.append(slice(None))
274 else:
275 mesh_slice.append(complementary_coord[coord_idx])
276 coord_idx += 1
277 selected = mesh_tensor[tuple(mesh_slice)]
278 split_rank_lists.append([int(item) for item in np.array(selected).reshape(-1).tolist()])
279 return split_rank_lists
282def get_hsdp_state(module):
283 """Return the HSDPState for a fully_shard-managed module, or None."""
284 from hyper_parallel.core.fully_shard.api import HSDPModule # pylint: disable=C0415
285 if isinstance(module, HSDPModule):
286 if module.hsdp_scheduler is None:
287 raise AssertionError("Expect HSDPModule contains 'hsdp_scheduler'.")
288 return module.hsdp_scheduler.hsdp_state
291def apply_gradient_scaling_factor(reduced_grad: Any, factor: Any) -> None:
292 """In-place scale ``reduced_grad`` by ``factor`` (no-op when ``factor`` is ``None``).
294 Tensor factors are cast to ``reduced_grad``'s dtype so the multiply stays
295 in the reduction precision (e.g. fp32 accumulation, bf16 reduce).
296 """
297 if factor is None:
298 return
299 if isinstance(factor, platform.Tensor) and factor.dtype != reduced_grad.dtype:
300 factor = factor.to(reduced_grad.dtype)
301 reduced_grad.mul_(factor)
302 return None