Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / fully_shard / utils.py: 66%
161 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Torch helpers, policies and mesh metadata for fully_shard.
17The fully_shard core used to reach process-group management, module traversal and
18tensor plumbing through the platform abstraction layer. Only the torch backend is
19supported now, so those implementations live here and every caller calls them
20directly.
21"""
22# pylint: disable=C9006,C9007
23from __future__ import annotations
25from collections import OrderedDict
26from collections.abc import Mapping
27from dataclasses import dataclass, fields, replace
28from typing import Any, Callable, Dict, Optional
30import torch
31from torch import Tensor, nn
32from torch.nn.utils.rnn import PackedSequence
34from hyper_parallel.core.dtensor.device_mesh import DeviceMesh
35from hyper_parallel.core.dtensor.placement_types import Placement
36from hyper_parallel.core.utils.communication import get_device_handle
37from hyper_parallel.core.utils.communication import get_group_local_rank
39DType = torch.dtype
42# ---------------------------------------------------------------------------
43# Module traversal
44# ---------------------------------------------------------------------------
46def get_modules(module: nn.Module):
47 """Return every sub-module contained in ``module``.
49 Args:
50 module: The root module to traverse.
52 Returns:
53 An iterator over the module tree.
54 """
55 return module.modules()
58def parameters_dict(cell: nn.Module):
59 """Return every named parameter registered by the module tree.
61 Args:
62 cell: The root module to traverse.
64 Returns:
65 An iterator of ``(name, parameter)`` pairs.
66 """
67 return cell.named_parameters()
70def buffers_dict(cell: nn.Module):
71 """Return every named buffer registered by the module tree.
73 Args:
74 cell: The root module to traverse.
76 Returns:
77 An iterator of ``(name, buffer)`` pairs.
78 """
79 return cell.named_buffers()
82# ---------------------------------------------------------------------------
83# Tensor plumbing
84# ---------------------------------------------------------------------------
86def load_into_param(param: Tensor, data: Tensor) -> None:
87 """Write ``data`` into ``param``, materialising a meta local tensor first.
89 Args:
90 param: The destination parameter, possibly a DTensor.
91 data: The data to write into it.
92 """
93 from hyper_parallel.core.dtensor.dtensor import DTensor # pylint: disable=C0415
95 if isinstance(param, DTensor):
96 local = param._local_tensor # pylint: disable=protected-access
97 if local.is_meta:
98 orig_requires_grad = param.requires_grad
99 param._local_tensor = data # pylint: disable=protected-access
100 if data.requires_grad != orig_requires_grad:
101 param.requires_grad_(orig_requires_grad)
102 else:
103 local.copy_(data)
104 else:
105 param.copy_(data)
108def cast_fp_tensor(dtype: DType, x: Any) -> Any:
109 """Cast a floating-point tensor to ``dtype``, leaving anything else alone.
111 Args:
112 dtype: The target dtype.
113 x: The candidate tensor.
115 Returns:
116 Any: The cast tensor, or ``x`` unchanged when it is not a floating-point tensor.
117 """
118 if not isinstance(x, Tensor) or not torch.is_floating_point(x) or x.dtype == dtype:
119 return x
120 return x.to(dtype)
123def apply_to_tensors(fn: Callable[[Tensor], Any], container: Any) -> Any:
124 """Recursively apply ``fn`` to every tensor inside ``container``.
126 Handles tensors, dataclasses, ordered and plain dicts, named tuples,
127 lists, tuples, sets and packed sequences.
129 Args:
130 fn: The callable applied to each tensor.
131 container: The structure to walk.
133 Returns:
134 Any: A structure of the same shape with every tensor replaced by ``fn``'s result.
135 """
137 def apply(x):
138 if isinstance(x, Tensor):
139 return fn(x)
140 if hasattr(x, "__dataclass_fields__"):
141 dc = replace(x)
142 changes = {f.name: apply(getattr(dc, f.name)) for f in fields(dc)}
143 return replace(dc, **changes)
144 if isinstance(x, OrderedDict):
145 od = x.__class__()
146 for key, value in x.items():
147 od[key] = apply(value)
148 return od
149 if isinstance(x, PackedSequence):
150 return x._replace(data=apply(x.data))
151 if isinstance(x, dict):
152 return {key: apply(value) for key, value in x.items()}
153 if isinstance(x, tuple) and hasattr(x, "_asdict") and hasattr(x, "_fields"):
154 res = (apply(el) for el in x)
155 return type(x)(*res)
156 if isinstance(x, (list, tuple, set)):
157 return type(x)(apply(el) for el in x)
158 return x
160 return apply(container)
163def profiler_record(name: str):
164 """Return the torch profiler annotation context for ``name``.
166 Args:
167 name: Label shown in profiler traces for the enclosed region.
169 Returns:
170 A context manager that records the region when the profiler is active.
171 """
172 return torch.profiler.record_function(name)
175# ---------------------------------------------------------------------------
176# Gradient-ready stream
177# ---------------------------------------------------------------------------
178# The handle and its stream are per-process singletons: at most one gradient
179# reduction is in flight across the whole process, so the state belongs at
180# module scope rather than on any scheduler or parameter object.
181_GRAD_READY_STATE: Dict[str, Any] = {
182 "handle": None,
183 "post_process": None,
184 "stream": None,
185}
188def _process_pending_grad_handle() -> None:
189 """Wait for the in-flight gradient handle and run its post-process callback."""
190 handle = _GRAD_READY_STATE["handle"]
191 if handle is None:
192 return
193 handle.wait()
194 post_process = _GRAD_READY_STATE["post_process"]
195 if post_process is not None:
196 post_process()
199def _grad_ready_stream():
200 """Return the stream used to order gradient-handle waits, creating it lazily."""
201 if _GRAD_READY_STATE["stream"] is None:
202 _GRAD_READY_STATE["stream"] = get_device_handle().Stream()
203 return _GRAD_READY_STATE["stream"]
206def grad_ready_stream():
207 """Return the context manager that switches to the gradient-ready stream.
209 Returns:
210 The device stream context manager guarding the synchronisation stream.
211 """
212 return get_device_handle().stream(_grad_ready_stream())
215def set_grad_reduce_handle(handle: Any, post_process: Optional[Callable[[], None]] = None) -> None:
216 """Record a new in-flight gradient reduction handle.
218 Any previously recorded handle is waited on first, so at most one handle is
219 ever outstanding.
221 Args:
222 handle: The async work handle returned by the reduction collective.
223 post_process: Callback run once ``handle`` completes, or ``None``.
224 """
225 with grad_ready_stream():
226 _process_pending_grad_handle()
227 _GRAD_READY_STATE["handle"] = handle
228 _GRAD_READY_STATE["post_process"] = post_process
231def wait_grad_handle() -> None:
232 """Block until the in-flight gradient reduction handle completes and clear it."""
233 handle = _GRAD_READY_STATE["handle"]
234 if handle is None:
235 return
236 with grad_ready_stream():
237 _process_pending_grad_handle()
238 sync_event = _grad_ready_stream().record_event()
239 sync_event.wait()
240 _GRAD_READY_STATE["handle"] = None
241 _GRAD_READY_STATE["post_process"] = None
244# ---------------------------------------------------------------------------
245# Policies and mesh metadata
246# ---------------------------------------------------------------------------
248@dataclass
249class MixedPrecisionPolicy:
250 """
251 Configures mixed precision training for HSDP.
253 This policy controls data type casting during forward/backward computation
254 and gradient reduction, enabling memory savings and potential speedups.
256 Attributes:
257 param_dtype: Data type for parameter computation. If None, uses original dtype.
258 reduce_dtype: Data type for gradient reduction. If None, uses param_dtype.
259 output_dtype: Data type for module outputs. If None, no casting applied.
260 custom_params: Per-parameter overrides that take precedence over this policy's
261 ``param_dtype`` and ``reduce_dtype`` defaults.
262 """
263 param_dtype: Optional[DType] = None
264 reduce_dtype: Optional[DType] = None
265 output_dtype: Optional[DType] = None
266 cast_forward_inputs: bool = True
267 apply_grad_on_fp32_main_grad: bool = False
268 custom_params: Optional[Mapping[nn.Parameter, Mapping[str, Optional[DType]]]] = None
270 def __post_init__(self) -> None:
271 """Validate overrides and index dtype configurations by source Parameter ID."""
272 if self.custom_params is None:
273 return
274 self._check_custom_param()
275 self.custom_params = {
276 id(parameter): dict(custom_dtypes)
277 for parameter, custom_dtypes in self.custom_params.items()
278 }
280 def _check_custom_param(self) -> None:
281 """Validate configured parameter keys and dtype values without changing the mapping."""
282 if not isinstance(self.custom_params, Mapping):
283 raise ValueError("custom_params must map Parameter objects to dtype dictionaries")
284 for parameter, custom_dtypes in self.custom_params.items():
285 if not isinstance(parameter, nn.Parameter):
286 raise ValueError("custom_params keys must be Parameter objects")
287 if not isinstance(custom_dtypes, Mapping):
288 raise ValueError("custom_params values must be dtype dictionaries")
289 if custom_dtypes.keys() - {"param_dtype", "reduce_dtype"}:
290 raise ValueError("custom_params only supports param_dtype and reduce_dtype overrides")
291 for dtype_name, dtype in custom_dtypes.items():
292 if dtype is not None and not isinstance(dtype, torch.dtype):
293 raise ValueError(f"custom_params {dtype_name} must be torch.dtype or None, got {dtype!r}")
296@dataclass
297class OffloadPolicy:
298 """
299 Base class for offload policies.
301 This represents no offloading and serves as the default policy.
302 Subclass this to implement custom offload strategies.
303 """
306@dataclass
307class CPUOffloadPolicy(OffloadPolicy):
308 """
309 Offloads sharded parameters and gradients to CPU memory.
311 When enabled, sharded parameters are kept on CPU and copied to device
312 before all-gather. Gradients are copied back to CPU after backward.
313 This reduces NPU memory usage at the cost of additional data transfers.
315 Attributes:
316 pin_memory: If True, pins CPU memory for faster H2D/D2H transfers
317 and enables overlap with computation. Disable if CPU memory
318 is constrained. (Default: True)
319 """
320 pin_memory: bool = True
323@dataclass
324class CommFusionPolicy():
325 enable_comm_fusion: bool = False
326 comm_fusion_zero_copy: bool = False
329@dataclass
330class DataParallelMeshInfo:
331 mesh: DeviceMesh
332 shard_mesh_dim: Optional[int] = None
333 replicate_mesh_dim: Optional[int] = None
335 def __post_init__(self):
336 if self.shard_mesh_dim is None and self.replicate_mesh_dim is None:
337 raise AssertionError(
338 "At least one of shard_mesh_dim and replicate_mesh_dim must not be None"
339 )
342@dataclass
343class FSDPMeshInfo(DataParallelMeshInfo):
344 def __post_init__(self):
345 super().__post_init__()
346 if self.shard_mesh_dim is None:
347 raise AssertionError("Expects non-None shard_mesh_dim")
348 self.shard_mesh_size: int = self.mesh.mesh_shape[self.shard_mesh_dim]
349 self.shard_process_group = self.mesh.get_group(self.shard_mesh_dim)
350 self.shard_mesh_rank: int = get_group_local_rank(self.shard_process_group)
353@dataclass
354class DDPMeshInfo(DataParallelMeshInfo):
355 def __post_init__(self):
356 super().__post_init__()
357 if self.replicate_mesh_dim is None:
358 raise AssertionError("Expects non-None replicate_mesh_dim")
359 self.replicate_mesh_size: int = self.mesh.mesh_shape[self.replicate_mesh_dim]
360 self.replicate_process_group = self.mesh.get_group(self.replicate_mesh_dim)
361 self.replicate_mesh_rank: int = get_group_local_rank(self.replicate_process_group)
364@dataclass
365class HSDPMeshInfo(FSDPMeshInfo, DDPMeshInfo):
366 # pylint: disable=W0246
367 def __post_init__(self):
368 # Calls `FSDPMeshInfo` -> `DDPMeshInfo` -> `DataParallelMeshInfo`
369 super().__post_init__()
372@dataclass(frozen=True)
373class SourceShardMetaInfo:
374 """Describe a parameter's source TP/EP layout before fully_shard."""
376 mesh: DeviceMesh
377 placements: tuple[Placement, ...]
378 origin_is_dtensor: bool = False
381__all__ = [
382 # Module traversal and tensor plumbing.
383 "DType",
384 "get_modules",
385 "parameters_dict",
386 "buffers_dict",
387 "load_into_param",
388 "cast_fp_tensor",
389 "apply_to_tensors",
390 "profiler_record",
391 # Grad-reduce handle choreography.
392 "grad_ready_stream",
393 "set_grad_reduce_handle",
394 "wait_grad_handle",
395 # Precision/offload policies and mesh metadata.
396 "MixedPrecisionPolicy",
397 "OffloadPolicy",
398 "CPUOffloadPolicy",
399 "CommFusionPolicy",
400 "DataParallelMeshInfo",
401 "FSDPMeshInfo",
402 "DDPMeshInfo",
403 "HSDPMeshInfo",
404 "SourceShardMetaInfo",
405 # Re-exported from :mod:`hyper_parallel.core.utils.communication` because
406 # callers here and in the tests reach them through this module.
407 "get_device_handle",
408 "get_group_local_rank",
409]