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