Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / _builder / fsdp_adapter.py: 82%
398 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"""fsdp_adapter: FSDP2 construction-time adapter (05 §15.2.4).
17Wrap policy, mesh/policy construction, ``fully_shard`` application and
18prefetch configuration all happen at model-construction time (not in the
19Trainer step runtime); ``FSDP2Manager`` / ``_instantiate_fsdp2`` and the
20``parallelize`` signature are unchanged. The source-layout discovery logic
21(FQN/DTensor source-shard resolution) merged into
22``distributed/_builder/source_shard.py`` (05 row 406).
23"""
24# pylint: disable=forbidden-backend-import
26from __future__ import annotations
28import fnmatch
29import logging
30from collections import defaultdict
31from collections.abc import Mapping, Sequence
32from dataclasses import dataclass
33from typing import TYPE_CHECKING, Any
35import torch
36from torch.nn import Module as ModuleClass
37from torch.nn import Parameter as ParameterClass
39from hyper_parallel.components.modules.module_family import collect_family_modules
40import hyper_parallel.core.fully_shard.utils as fully_shard_utils
41from hyper_parallel import DeviceMesh, HSDPModule, fully_shard
42from hyper_parallel.core.fully_shard.hsdp_utils import get_managed_modules_parameters
43from hyper_parallel.distributed._builder.source_shard import (
44 SourceShardInfoByFQN, # pylint: disable=unused-import
45 SourceShardInfoByParam, # pylint: disable=unused-import
46 _build_managed_source_shard_info,
47 _build_source_shard_info_by_param,
48 _source_infos_for_fully_shard,
49)
50from hyper_parallel.models.build_options import FSDP2Config
51from hyper_parallel.models.registry import get_model_adapter
53if TYPE_CHECKING:
54 from hyper_parallel.distributed.mesh import (
55 MeshContext,
56 )
58logger = logging.getLogger(__name__)
61@dataclass(frozen=True)
62class _WrapModuleInfo:
63 """One FSDP child unit selected by automatic or adapter policy."""
65 fqn: str
66 module: ModuleClass
69def _is_expert_source_mesh(mesh: DeviceMesh | None) -> bool:
70 """Whether a source mesh carries the expert (EP) axis."""
71 return mesh is not None and "ep" in (getattr(mesh, "mesh_dim_names", None) or ())
74def _get_checkpoint_wrapped_module(module: ModuleClass) -> ModuleClass | None:
75 """Return the module directly wrapped by a supported checkpoint wrapper."""
76 for attr_name in ("_wrapped_module", "_checkpoint_wrapped_module"):
77 wrapped_module = getattr(module, attr_name, None)
78 if isinstance(wrapped_module, ModuleClass):
79 return wrapped_module
80 return None
83def _declared_expert_unit_fqns(model, expert_parameters):
84 """Find family/native execution units by identity rather than an experts path suffix."""
85 expert_module_fqns = set()
86 for boundary in collect_family_modules(model):
87 parameters = set(boundary.module.parameters())
88 if parameters and parameters <= expert_parameters:
89 expert_module_fqns.add(boundary.fqns[0])
90 for fqn, module in model.named_modules():
91 binding = getattr(module, "_hp_parallel_binding", None)
92 if binding is not None and binding.ep_context is not None:
93 expert_module_fqns.add(fqn)
94 expert_module_fqns.discard("")
95 return expert_module_fqns
98class FSDP2Manager:
99 """Configure and apply nested FSDP2 wrapping for a dual-mode model."""
101 def __init__(
102 self,
103 config: FSDP2Config,
104 mesh_context: MeshContext,
105 fp32_main_params: bool = False,
106 ) -> None:
107 """Initialize the manager from resolved YAML config and runtime mesh.
109 Args:
110 config: Resolved top-level ``fsdp_config``.
111 mesh_context: Runtime topology containing the shared world mesh.
112 fp32_main_params: Whether FSDP writes reduced gradients to
113 ``main_grad`` for the fp32 main-parameter optimizer wrapper.
114 """
115 self.config = config
116 self.mesh_context = mesh_context
117 self.fp32_main_params = fp32_main_params
118 self._mp_policy: fully_shard_utils.MixedPrecisionPolicy | None = None
121 def _resolve_replicate_params(
122 self,
123 model: ModuleClass,
124 ) -> set[ParameterClass] | None:
125 """Resolve configured replicate-parameter FQNs on the final model."""
126 if not self.config.replicate_params:
127 return None
129 parameters_by_fqn = dict(model.named_parameters(remove_duplicate=False))
130 replicate_params = set()
131 for parameter_fqn in self.config.replicate_params:
132 parameter = parameters_by_fqn.get(parameter_fqn)
133 if parameter is None:
134 raise ValueError(
135 "replicate_params contains unknown parameter FQN: "
136 f"{parameter_fqn}"
137 )
138 replicate_params.add(parameter)
139 return replicate_params
141 def _build_expert_fsdp_mesh(self) -> DeviceMesh:
142 """Select the expert FSDP child mesh."""
143 expert_mesh = self.mesh_context.fsdp_moe_mesh
144 if expert_mesh is None:
145 raise ValueError("Expert FSDP units require MeshContext.fsdp_moe_mesh")
146 if "edp_replicate" in (expert_mesh.mesh_dim_names or ()):
147 return expert_mesh[("edp_replicate", "edp_shard")]
148 return expert_mesh["edp_shard"]
150 def _build_dense_fsdp_mesh(self) -> DeviceMesh | None:
151 """Select the prebuilt dense FSDP child mesh when available."""
152 dense_mesh = self.mesh_context.fsdp_non_moe_mesh
153 if dense_mesh is None:
154 return None
155 if self.mesh_context.dp_replicate_size > 1:
156 return dense_mesh[("fsdp_replicate", "fsdp_shard")]
157 return dense_mesh["fsdp_shard"]
159 def _build_compatibility_fsdp_mesh(self) -> DeviceMesh:
160 """Build an FSDP mesh from the combined world mesh."""
161 world_mesh = self.mesh_context.device_mesh
162 if world_mesh is None:
163 raise ValueError("FSDP2 requires MeshContext.fsdp_non_moe_mesh")
164 mesh_dim_names = world_mesh.mesh_dim_names
165 if mesh_dim_names is None:
166 raise ValueError("FSDP2 requires named DeviceMesh dimensions")
167 shard_dim_names = tuple(
168 dim_name for dim_name in ("dp_shard", "cp") if dim_name in mesh_dim_names
169 )
170 if not shard_dim_names:
171 raise ValueError("FSDP2 requires a dp_shard or cp dimension in the world mesh")
172 shard_mesh = (
173 world_mesh[shard_dim_names[0]]
174 if len(shard_dim_names) == 1
175 else world_mesh[shard_dim_names].flatten("fsdp_shard")
176 )
177 if "dp_replicate" not in mesh_dim_names:
178 return shard_mesh
179 return DeviceMesh.concatenate([world_mesh["dp_replicate"], shard_mesh])
181 def _build_fsdp_actual_mesh(self, expert: bool = False) -> DeviceMesh:
182 """Select the prebuilt dense or expert FSDP child mesh."""
183 if expert:
184 return self._build_expert_fsdp_mesh()
185 dense_mesh = self._build_dense_fsdp_mesh()
186 if dense_mesh is not None:
187 return dense_mesh
188 return self._build_compatibility_fsdp_mesh()
190 def parameter_mesh_bindings(
191 self, model: ModuleClass, source_shard_info: SourceShardInfoByFQN | None,
192 ignored_params: set[ParameterClass] | None = None,
193 ) -> dict[ParameterClass, DeviceMesh]:
194 """Resolve unit-owned parameter meshes without installing FSDP hooks.
196 Args:
197 model: Final model before graph capture or eager wrapping.
198 source_shard_info: The planner's non-FSDP parameter placements.
199 ignored_params: Parameters managed outside FSDP.
201 The same managed-parameter selector runs inside-out in both paths.
202 Claimed identities model the initialized inner units, including tied
203 aliases; nearest-parent ownership alone would not match fully_shard.
204 """
205 metadata = _build_source_shard_info_by_param(self, model, source_shard_info)
206 units = self._find_wrap_modules(model, metadata)
207 units = [unit.module for unit in sorted(units, key=lambda unit: unit.fqn.count("."), reverse=True)]
208 claimed = set(ignored_params or ())
209 bindings = {}
210 for owner in [*units, model]:
211 parameters = get_managed_modules_parameters((owner,), claimed)
212 source = _build_managed_source_shard_info(self, owner, metadata, claimed)
213 expert = self._uses_expert_mesh(source)
214 if owner is model and expert:
215 raise ValueError("Routed expert parameters must belong to a nested experts FSDP unit")
216 mesh = self._build_fsdp_actual_mesh(expert)
217 bindings.update(dict.fromkeys(parameters, mesh))
218 claimed.update(parameters)
219 return bindings
221 def _resolve_custom_params(
222 self, parameters_by_fqn: Mapping[str, ParameterClass]
223 ) -> dict[ParameterClass, dict[str, Any]]:
224 """Resolve configured names and patterns against the supplied parameter mapping."""
225 custom_params = {}
226 for parameter_fqn, custom_dtypes in self.config.mix_precision.custom_params.items():
227 matched_fqns = [parameter_fqn] if parameter_fqn in parameters_by_fqn else [
228 fqn for fqn in parameters_by_fqn if fnmatch.fnmatchcase(fqn, parameter_fqn)
229 ]
230 if not matched_fqns:
231 raise ValueError(f"mix_precision.custom_params matches no parameter: {parameter_fqn}")
232 for matched_fqn in matched_fqns:
233 parameter = parameters_by_fqn[matched_fqn]
234 if parameter in custom_params and custom_params[parameter] != custom_dtypes:
235 raise ValueError(f"mix_precision.custom_params has conflicting overrides for {matched_fqn}")
236 custom_params[parameter] = dict(custom_dtypes)
237 return custom_params
239 def _build_mixed_precision_policy(
240 self, custom_params: Mapping[ParameterClass, Mapping[str, Any]] | None = None
241 ) -> fully_shard_utils.MixedPrecisionPolicy:
242 """Build or reuse the shared precision policy for the current model."""
243 if custom_params is None and self._mp_policy is not None:
244 return self._mp_policy
245 mix_precision = self.config.mix_precision
246 dtype_by_name = {
247 None: None,
248 "bfloat16": torch.bfloat16,
249 "float16": torch.float16,
250 "float32": torch.float32,
251 }
252 custom_params = {
253 parameter: {name: dtype_by_name[dtype] for name, dtype in custom_dtypes.items()}
254 for parameter, custom_dtypes in (custom_params or {}).items()
255 }
256 self._mp_policy = fully_shard_utils.MixedPrecisionPolicy(
257 param_dtype=dtype_by_name[mix_precision.param_dtype],
258 reduce_dtype=dtype_by_name[mix_precision.reduce_dtype],
259 output_dtype=dtype_by_name[mix_precision.output_dtype],
260 cast_forward_inputs=mix_precision.cast_forward_inputs,
261 apply_grad_on_fp32_main_grad=self.fp32_main_params,
262 custom_params=custom_params if custom_params else None,
263 )
264 return self._mp_policy
266 def _build_offload_policy(self) -> fully_shard_utils.OffloadPolicy:
267 """Return CPU offload when enabled, otherwise the default no-offload policy."""
268 if not self.config.enable_offload:
269 return fully_shard_utils.OffloadPolicy()
270 return fully_shard_utils.CPUOffloadPolicy()
272 def _build_fully_shard_kwargs(
273 self,
274 fsdp_mesh: DeviceMesh,
275 ) -> tuple[dict[str, Any], dict[str, Any]]:
276 """Build child and root kwargs with the same configured reshard policy."""
277 mp_policy = self._build_mixed_precision_policy()
278 offload_policy = self._build_offload_policy()
280 fsdp_sublayer_kwargs = {
281 "mesh": fsdp_mesh,
282 "reshard_after_forward": self.config.reshard_after_forward,
283 "mp_policy": mp_policy,
284 "offload_policy": offload_policy,
285 "comm_fusion": self.config.comm_fusion,
286 "comm_fusion_zero_copy": self.config.comm_fusion_zero_copy,
287 }
288 fsdp_root_kwargs = fsdp_sublayer_kwargs.copy()
289 return fsdp_sublayer_kwargs, fsdp_root_kwargs
291 @staticmethod
292 def _find_transformer_block_modules(
293 model: ModuleClass,
294 excluded_subtree_module_fqns: set[str] | None = None,
295 ) -> tuple[list[_WrapModuleInfo], set[int]]:
296 """Find transformer blocks under HF gradient-checkpointing containers.
298 Args:
299 model: Root model to inspect.
300 excluded_subtree_module_fqns: Roots declared by a model adapter as
301 non-decoder branches or self-contained FSDP units. Automatic
302 decoder discovery skips these roots and their descendants.
303 """
304 wrap_modules = []
305 wrapped_module_ids = set()
306 excluded_subtree_module_fqns = excluded_subtree_module_fqns or set()
308 def is_inside_declared_subtree(module_fqn: str) -> bool:
309 """Whether one module is inside an automatic-discovery exclusion.
311 Args:
312 module_fqn: Fully qualified name of the module.
313 """
314 return any(
315 module_fqn == declared_fqn
316 or module_fqn.startswith(f"{declared_fqn}.")
317 for declared_fqn in excluded_subtree_module_fqns
318 )
320 for container_fqn, container in model.named_modules():
321 if container_fqn and is_inside_declared_subtree(container_fqn):
322 continue
323 if id(container) in wrapped_module_ids:
324 continue
325 if not hasattr(container, "gradient_checkpointing"):
326 continue
327 for child_name, child in container.named_children():
328 child_fqn = f"{container_fqn}.{child_name}" if container_fqn else child_name
329 if is_inside_declared_subtree(child_fqn):
330 continue
331 blocks = list(child.named_children())
332 if not blocks:
333 continue
334 for block_name, block in blocks:
335 if id(block) in wrapped_module_ids:
336 continue
337 wrapped_module_ids.add(id(block))
338 wrapped_module = _get_checkpoint_wrapped_module(block)
339 if wrapped_module is not None:
340 # The wrapper and its direct child represent one logical
341 # transformer block during module-tree traversal.
342 wrapped_module_ids.add(id(wrapped_module))
343 wrap_modules.append(
344 _WrapModuleInfo(f"{child_fqn}.{block_name}", block)
345 )
346 return wrap_modules, wrapped_module_ids
348 def _find_expert_wrap_modules(
349 self,
350 model: ModuleClass,
351 metadata_by_parameter: SourceShardInfoByParam | None,
352 wrapped_module_ids: set[int],
353 ) -> list[_WrapModuleInfo]:
354 """Find nested expert containers whose parameters use the expert mesh."""
355 if metadata_by_parameter is None or self.mesh_context.fsdp_moe_mesh is None:
356 return []
357 expert_parameters = {
358 parameter
359 for parameter, source_shard_info in metadata_by_parameter.items()
360 if _is_expert_source_mesh(source_shard_info.mesh)
361 }
362 expert_module_fqns = {
363 parameter_fqn.split(".experts.", 1)[0] + ".experts"
364 for parameter_fqn, parameter in model.named_parameters(remove_duplicate=False)
365 if parameter in expert_parameters and ".experts." in parameter_fqn
366 }
367 expert_module_fqns.update(_declared_expert_unit_fqns(model, expert_parameters))
368 expert_module_fqns.discard("")
369 module_by_fqn = dict(model.named_modules())
370 expert_wrap_modules = []
371 for expert_module_fqn in sorted(expert_module_fqns):
372 expert_module = module_by_fqn.get(expert_module_fqn)
373 if expert_module is None or id(expert_module) in wrapped_module_ids:
374 continue
375 wrapped_module_ids.add(id(expert_module))
376 expert_wrap_modules.append(
377 _WrapModuleInfo(expert_module_fqn, expert_module)
378 )
379 return expert_wrap_modules
381 @staticmethod
382 def _get_model_adapter_spec(model: ModuleClass) -> Any | None:
383 """Resolve a model adapter from the final model configuration."""
384 if hasattr(model, "_hp_model_adapter_spec"):
385 return model._hp_model_adapter_spec # pylint: disable=protected-access
386 config = getattr(model, "config", None)
387 if config is None:
388 return None
389 identities = (
390 getattr(config, "model_type", ""),
391 *((getattr(config, "architectures", None) or [])[:1]),
392 )
393 for identity in identities:
394 if identity:
395 adapter_spec = get_model_adapter(identity)
396 if adapter_spec is not None:
397 return adapter_spec
398 return None
400 @classmethod
401 def _find_adapter_declared_wrap_modules(
402 cls,
403 model: ModuleClass,
404 ) -> list[_WrapModuleInfo]:
405 """Resolve adapter-declared FSDP units outside HF decoder containers.
407 ``ModelAdapterSpec.fsdp_wrap_modules`` keeps architecture-specific
408 execution boundaries on the family adapter side. It receives the
409 finalized model and returns exact root-relative module FQNs.
411 Args:
412 model: Root model being wrapped.
414 Returns:
415 Additional FSDP child units selected by model declarations.
417 Raises:
418 ValueError: If a declaration has an invalid type, targets no
419 module in the model tree, or aliases the same module twice.
420 """
421 adapter_spec = cls._get_model_adapter_spec(model)
422 if adapter_spec is None or adapter_spec.fsdp_wrap_modules is None:
423 return []
425 declared_fqns = adapter_spec.fsdp_wrap_modules(model)
426 if isinstance(declared_fqns, str) or not isinstance(declared_fqns, Sequence):
427 raise ValueError(
428 "ModelAdapterSpec.fsdp_wrap_modules must return a sequence of exact module FQNs"
429 )
430 module_by_fqn = dict(model.named_modules())
431 declared_wrap_modules = []
432 wrapped_module_ids = set()
433 for module_fqn in declared_fqns:
434 if not isinstance(module_fqn, str) or not module_fqn:
435 raise ValueError(
436 "ModelAdapterSpec.fsdp_wrap_modules entries must be non-empty strings"
437 )
438 wrap_module = module_by_fqn.get(module_fqn)
439 if wrap_module is None:
440 raise ValueError(
441 "ModelAdapterSpec.fsdp_wrap_modules declared an unknown module: "
442 f"{module_fqn}"
443 )
444 if id(wrap_module) in wrapped_module_ids:
445 raise ValueError(
446 "ModelAdapterSpec.fsdp_wrap_modules aliases one module more than once: "
447 f"{module_fqn}"
448 )
449 wrapped_module_ids.add(id(wrap_module))
450 declared_wrap_modules.append(_WrapModuleInfo(module_fqn, wrap_module))
451 return declared_wrap_modules
453 @classmethod
454 def _find_adapter_excluded_subtrees(cls, model: ModuleClass) -> set[str]:
455 """Resolve model branches excluded from automatic decoder discovery."""
456 adapter_spec = cls._get_model_adapter_spec(model)
457 excluded_subtrees_provider = getattr(adapter_spec, "fsdp_excluded_subtrees", None)
458 if excluded_subtrees_provider is None:
459 return set()
460 if not callable(excluded_subtrees_provider):
461 raise ValueError("ModelAdapterSpec.fsdp_excluded_subtrees must be callable")
462 excluded_fqns = excluded_subtrees_provider(model) # pylint: disable=not-callable
463 if isinstance(excluded_fqns, str) or not isinstance(excluded_fqns, Sequence):
464 raise ValueError(
465 "ModelAdapterSpec.fsdp_excluded_subtrees must return a sequence of exact module FQNs"
466 )
467 module_by_fqn = dict(model.named_modules())
468 for module_fqn in excluded_fqns:
469 if not isinstance(module_fqn, str) or not module_fqn:
470 raise ValueError(
471 "ModelAdapterSpec.fsdp_excluded_subtrees entries must be non-empty strings"
472 )
473 if module_fqn not in module_by_fqn:
474 raise ValueError(
475 "ModelAdapterSpec.fsdp_excluded_subtrees declared an unknown module: "
476 f"{module_fqn}"
477 )
478 return set(excluded_fqns)
480 def _find_class_wrap_modules(self, model: ModuleClass) -> list[_WrapModuleInfo]:
481 """Select explicit classes, retaining checkpoint wrappers as the units."""
482 requested = set(self.config.wrapper_class)
483 matched = set()
484 selected = []
485 seen = set()
486 aliases = defaultdict(list)
487 for name, parameter in model.named_parameters(remove_duplicate=False):
488 aliases[parameter].append(name)
489 for fqn, module in model.named_modules():
490 if not fqn or id(module) in seen:
491 continue
492 underlying = _get_checkpoint_wrapped_module(module)
493 candidate = underlying if underlying is not None else module
494 class_name = type(candidate).__name__
495 if class_name not in requested:
496 continue
497 matched.add(class_name)
498 parameters = list(module.parameters())
499 if not parameters:
500 raise ValueError(f"wrapper_class selected a parameterless module: {fqn}")
501 if any(not name.startswith(f"{fqn}.") for param in parameters for name in aliases[param]):
502 raise ValueError(f"wrapper_class module shares parameters outside its subtree: {fqn}")
503 selected.append(_WrapModuleInfo(fqn, module))
504 seen.add(id(module))
505 seen.add(id(candidate))
506 if requested - matched:
507 raise ValueError(f"wrapper_class matched no child module for: {sorted(requested - matched)}")
508 return selected
510 def _find_wrap_modules(
511 self,
512 model: ModuleClass,
513 metadata_by_parameter: SourceShardInfoByParam | None = None,
514 ) -> list[_WrapModuleInfo]:
515 """Find automatic decoder/expert units and adapter-declared units."""
516 if self.config.wrapper_class:
517 wrap_modules = self._find_class_wrap_modules(model)
518 wrapped_module_ids = {id(unit.module) for unit in wrap_modules}
519 else:
520 declared_wrap_modules = self._find_adapter_declared_wrap_modules(model)
521 if declared_wrap_modules:
522 logger.info(
523 "Adapter-declared FSDP child units: %s",
524 [wrap_module.fqn for wrap_module in declared_wrap_modules],
525 )
526 excluded_subtree_module_fqns = {
527 wrap_module.fqn for wrap_module in declared_wrap_modules
528 }
529 excluded_subtree_module_fqns.update(
530 self._find_adapter_excluded_subtrees(model)
531 )
532 wrap_modules, wrapped_module_ids = self._find_transformer_block_modules(
533 model,
534 excluded_subtree_module_fqns=excluded_subtree_module_fqns,
535 )
536 wrapped_module_ids.update(
537 id(wrap_module.module) for wrap_module in declared_wrap_modules
538 )
539 wrap_modules.extend(declared_wrap_modules)
540 wrap_modules.extend(
541 self._find_expert_wrap_modules(
542 model,
543 metadata_by_parameter,
544 wrapped_module_ids,
545 )
546 )
547 if not wrap_modules:
548 raise ValueError(
549 "wrap_policy='transformer_block' found no decoder, expert, or "
550 "adapter-declared FSDP child unit"
551 )
552 return wrap_modules
554 @staticmethod
555 def _resolve_parameter_owners(
556 model: ModuleClass,
557 wrap_modules: list[_WrapModuleInfo],
558 ) -> dict[ParameterClass, ModuleClass]:
559 """Assign each parameter to its deepest common FSDP wrap module."""
560 aliases_by_parameter: dict[ParameterClass, list[str]] = defaultdict(list)
561 for parameter_fqn, parameter in model.named_parameters(remove_duplicate=False):
562 aliases_by_parameter[parameter].append(parameter_fqn)
564 owner_by_parameter = {}
565 for parameter, parameter_fqns in aliases_by_parameter.items():
566 common_wrap_modules = [
567 wrap_module
568 for wrap_module in wrap_modules
569 if all(
570 parameter_fqn.startswith(f"{wrap_module.fqn}.")
571 for parameter_fqn in parameter_fqns
572 )
573 ]
574 if common_wrap_modules:
575 owner_by_parameter[parameter] = max(
576 common_wrap_modules,
577 key=lambda wrap_module: wrap_module.fqn.count("."),
578 ).module
579 else:
580 owner_by_parameter[parameter] = model
581 return owner_by_parameter
584 def _uses_expert_mesh(self, metadata_by_parameter: SourceShardInfoByParam | None) -> bool:
585 """Return whether one FSDP unit owns routed-expert source metadata."""
586 if metadata_by_parameter is None or self.mesh_context.fsdp_moe_mesh is None:
587 return False
588 return any(
589 _is_expert_source_mesh(source_shard_info.mesh)
590 for source_shard_info in metadata_by_parameter.values()
591 )
594 @staticmethod
595 def _build_managed_replicate_params(
596 owner: ModuleClass,
597 owner_by_parameter: Mapping[ParameterClass, ModuleClass],
598 replicate_params: set[ParameterClass] | None,
599 ignored_params: set[ParameterClass] | None = None,
600 ) -> set[ParameterClass] | None:
601 """Select configured replicate parameters owned by one FSDP module."""
602 if replicate_params is None:
603 return None
604 return {
605 parameter
606 for parameter, parameter_owner in owner_by_parameter.items()
607 if parameter_owner is owner and parameter in replicate_params
608 and parameter not in (ignored_params or ())
609 }
611 def _configure_source_layout_gradient_scaling(
612 self,
613 hsdp_module: HSDPModule,
614 managed_source_shard_info: SourceShardInfoByParam | None,
615 ) -> bool:
616 """Restore global-mean gradients for one source-layout FSDP unit.
618 Master defaults every ``fully_shard`` unit to AVG reduction, so units
619 with source-layout metadata explicitly select SUM. The token-weighted
620 backward loss is scaled by the FSDP data domain (DP x CP), so a
621 source-layout SUM-reduced unit must apply its inverse exactly once.
622 With loss parallelism, TP ranks hold shards or partial contributions
623 to one logical loss and TP is not another averaging axis. Without loss
624 parallelism, logits and the loss are replicated over TP, so the SUM
625 path also accumulates ``tp_size`` identical loss replicas and must
626 divide by TP. EP is excluded because expert ranks own different
627 parameters.
629 Returns:
630 Whether a gradient scaling factor was configured for the unit.
631 """
632 if not managed_source_shard_info:
633 return False
634 hsdp_module.set_reduce_op_type("sum", recurse=False)
636 tp_loss_replica_size = (
637 1 if self.mesh_context.loss_parallel else self.mesh_context.tp_size
638 )
639 source_gradient_domain_size = (
640 self.mesh_context.dp_size
641 * self.mesh_context.cp_size
642 * tp_loss_replica_size
643 )
644 if source_gradient_domain_size <= 1:
645 return False
646 hsdp_module.set_gradient_scaling_factor(1.0 / source_gradient_domain_size)
647 return True
649 def _configure_prefetch(self, hsdp_modules: list[HSDPModule]) -> None:
650 """Configure forward and backward prefetch targets by list depth."""
651 for module_index, hsdp_module in enumerate(hsdp_modules):
652 forward_end = module_index + self.config.forward_prefetch_depth + 1
653 hsdp_module.set_modules_to_forward_prefetch(
654 hsdp_modules[module_index + 1:forward_end]
655 )
657 backward_start = max(
658 0, module_index - self.config.backward_prefetch_depth
659 )
660 hsdp_module.set_modules_to_backward_prefetch(
661 list(reversed(hsdp_modules[backward_start:module_index]))
662 )
664 def _order_wrap_modules(
665 self,
666 model: ModuleClass,
667 wrap_modules: list[_WrapModuleInfo],
668 ) -> list[_WrapModuleInfo]:
669 """Order FSDP units by adapter execution order or module traversal."""
670 module_order = {
671 id(module): module_index
672 for module_index, (_, module) in enumerate(model.named_modules())
673 }
674 default_order = sorted(
675 wrap_modules,
676 key=lambda wrap_module: module_order.get(id(wrap_module.module), len(module_order)),
677 )
678 adapter_spec = self._get_model_adapter_spec(model)
679 execution_order_provider = getattr(adapter_spec, "fsdp_execution_order", None)
680 if execution_order_provider is None:
681 return default_order
682 if not callable(execution_order_provider):
683 raise ValueError("ModelAdapterSpec.fsdp_execution_order must be callable")
685 wrap_module_by_fqn = {
686 wrap_module.fqn: wrap_module for wrap_module in wrap_modules
687 }
688 execution_fqns = execution_order_provider( # pylint: disable=not-callable
689 model,
690 tuple(wrap_module.fqn for wrap_module in default_order),
691 )
692 if isinstance(execution_fqns, str) or not isinstance(execution_fqns, Sequence):
693 raise ValueError(
694 "ModelAdapterSpec.fsdp_execution_order must return a sequence of module FQNs"
695 )
696 if any(not isinstance(module_fqn, str) for module_fqn in execution_fqns):
697 raise ValueError(
698 "ModelAdapterSpec.fsdp_execution_order entries must be module FQN strings"
699 )
700 if (
701 len(execution_fqns) != len(wrap_module_by_fqn)
702 or len(set(execution_fqns)) != len(execution_fqns)
703 or set(execution_fqns) != set(wrap_module_by_fqn)
704 ):
705 raise ValueError(
706 "ModelAdapterSpec.fsdp_execution_order must return every selected FSDP "
707 "child-unit FQN exactly once"
708 )
709 logger.info("Adapter-declared FSDP execution order: %s", list(execution_fqns))
710 return [wrap_module_by_fqn[module_fqn] for module_fqn in execution_fqns]
712 def _parallelize_child_units(
713 self,
714 wrap_modules: list[_WrapModuleInfo],
715 owner_by_parameter,
716 metadata_by_parameter,
717 replicate_params,
718 dense_fsdp_kwargs: dict[str, Any],
719 ignored_params: set[ParameterClass] | None = None,
720 ) -> int:
721 """Apply FSDP to child units and return the gradient-scaled count."""
722 gradient_scaled_units = 0
723 wrapping_order = sorted(
724 wrap_modules, key=lambda wrap_module: wrap_module.fqn.count("."), reverse=True
725 )
726 for wrap_module in wrapping_order:
727 managed_source_info = _build_managed_source_shard_info(
728 self,
729 wrap_module.module,
730 metadata_by_parameter,
731 ignored_params,
732 )
733 fsdp_sublayer_kwargs = dense_fsdp_kwargs
734 if self._uses_expert_mesh(managed_source_info):
735 if self.mesh_context.fsdp_moe_mesh is None:
736 raise ValueError("Expert TP metadata requires MeshContext.fsdp_moe_mesh")
737 fsdp_sublayer_kwargs, _ = self._build_fully_shard_kwargs(
738 self._build_fsdp_actual_mesh(expert=True)
739 )
740 fully_shard( # pylint: disable=unexpected-keyword-arg
741 wrap_module.module,
742 source_shard_infos=_source_infos_for_fully_shard(managed_source_info),
743 replicate_params=self._build_managed_replicate_params(
744 wrap_module.module, owner_by_parameter, replicate_params, ignored_params
745 ),
746 ignored_params=ignored_params,
747 **fsdp_sublayer_kwargs,
748 )
749 if self._configure_source_layout_gradient_scaling(wrap_module.module, managed_source_info):
750 gradient_scaled_units += 1
751 return gradient_scaled_units
753 def _parallelize_root(
754 self,
755 model: ModuleClass,
756 owner_by_parameter,
757 metadata_by_parameter,
758 replicate_params,
759 dense_root_kwargs: dict[str, Any],
760 ignored_params: set[ParameterClass] | None = None,
761 ) -> int:
762 """Apply root FSDP and report whether gradient scaling was configured."""
763 root_source_info = _build_managed_source_shard_info(
764 self,
765 model,
766 metadata_by_parameter,
767 ignored_params,
768 )
769 if self._uses_expert_mesh(root_source_info):
770 raise ValueError("Routed expert parameters must belong to a nested experts FSDP unit")
771 fully_shard( # pylint: disable=unexpected-keyword-arg
772 model,
773 source_shard_infos=_source_infos_for_fully_shard(root_source_info),
774 replicate_params=self._build_managed_replicate_params(
775 model, owner_by_parameter, replicate_params, ignored_params
776 ),
777 ignored_params=ignored_params,
778 **dense_root_kwargs,
779 )
780 return int(self._configure_source_layout_gradient_scaling(model, root_source_info))
782 def _log_gradient_scaling(self, gradient_scaled_units: int) -> None:
783 """Log the source-layout gradient scaling applied to FSDP units."""
784 if not gradient_scaled_units:
785 return
786 domain_size = (
787 self.mesh_context.dp_size
788 * self.mesh_context.cp_size
789 * (1 if self.mesh_context.loss_parallel else self.mesh_context.tp_size)
790 )
791 logger.info(
792 "Applied source-layout SUM gradient scaling: dp_size=%d, cp_size=%d, tp_size=%d, "
793 "source_gradient_domain_size=%d, factor=%s, units=%d",
794 self.mesh_context.dp_size,
795 self.mesh_context.cp_size,
796 self.mesh_context.tp_size,
797 domain_size,
798 1.0 / domain_size,
799 gradient_scaled_units,
800 )
802 def parallelize(
803 self,
804 model: ModuleClass,
805 source_shard_info: SourceShardInfoByFQN | None = None,
806 ignored_params: set[ParameterClass] | None = None,
807 ) -> ModuleClass:
808 """Apply configured child FSDP units, root FSDP, and prefetch links.
810 Args:
811 model: Model to wrap in place.
812 source_shard_info: FQN-keyed source-layout metadata returned by
813 ``apply_sharding_plan``. The manager resolves parameter
814 identities before applying nested FSDP units.
816 Returns:
817 The model enhanced with the HyperParallel HSDPModule interface.
819 Raises:
820 ValueError: If TP is enabled without FQN metadata or configured
821 parameter FQNs cannot be resolved.
822 """
823 self._mp_policy = None
824 metadata_by_parameter = _build_source_shard_info_by_param(
825 self,
826 model,
827 source_shard_info,
828 )
829 replicate_params = self._resolve_replicate_params(model)
830 custom_params = None
831 if self.config.mix_precision.custom_params:
832 custom_params = self._resolve_custom_params(dict(model.named_parameters(remove_duplicate=False)))
833 self._build_mixed_precision_policy(custom_params)
835 wrap_modules = self._find_wrap_modules(model, metadata_by_parameter)
836 owner_by_parameter = self._resolve_parameter_owners(model, wrap_modules)
837 dense_fsdp_kwargs, dense_root_kwargs = self._build_fully_shard_kwargs(
838 self._build_fsdp_actual_mesh()
839 )
841 gradient_scaled_units = self._parallelize_child_units(
842 wrap_modules,
843 owner_by_parameter,
844 metadata_by_parameter,
845 replicate_params,
846 dense_fsdp_kwargs,
847 ignored_params,
848 )
849 gradient_scaled_units += self._parallelize_root(
850 model,
851 owner_by_parameter,
852 metadata_by_parameter,
853 replicate_params,
854 dense_root_kwargs,
855 ignored_params,
856 )
857 self._log_gradient_scaling(gradient_scaled_units)
858 ordered_wrap_modules = self._order_wrap_modules(model, wrap_modules)
859 self._configure_prefetch(
860 [wrap_module.module for wrap_module in ordered_wrap_modules]
861 )
862 logger.info(
863 "Applied FSDP2 to %d child units and the root module",
864 len(wrap_modules),
865 )
866 return model
869def _instantiate_fsdp2(
870 *,
871 config: Any,
872 mesh_context: MeshContext,
873 fp32_main_params: bool = False,
874) -> FSDP2Manager | None:
875 """Create an FSDP2 manager for the resolved strategy config."""
876 if config is None:
877 return None
878 if isinstance(config, FSDP2Config):
879 return FSDP2Manager(
880 config=config,
881 mesh_context=mesh_context,
882 fp32_main_params=fp32_main_params,
883 )
884 raise ValueError(f"FSDP2Manager requires FSDP2Config, got {type(config).__name__}")
887def _apply_fsdp2(
888 model: ModuleClass,
889 fsdp2_manager: Any,
890 source_shard_info: Any,
891 ignored_params: set[ParameterClass] | None = None,
892) -> ModuleClass:
893 """Apply FSDP2; Torch scheduler hooks remain outside Dynamo by design.
895 Moved from ``_transformers/infrastructure.py`` (05 §15.2.1) so the FSDP
896 application step lives with ``FSDP2Manager.parallelize``.
897 """
898 if fsdp2_manager is None:
899 return model
900 if not isinstance(fsdp2_manager, FSDP2Manager):
901 logger.warning("fsdp2_manager is not an FSDP2Manager instance")
902 return model
903 model = fsdp2_manager.parallelize(
904 model,
905 source_shard_info=source_shard_info,
906 ignored_params=ignored_params,
907 )
908 logger.info("FSDP2 wrap applied")
909 return model