Initial import: grid-bot — grid trading bot for BTC-USDT on Cifra Markets
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
from ._flat_param import FlatParameter as FlatParameter
|
||||
from ._fully_shard import (
|
||||
CPUOffloadPolicy,
|
||||
DataParallelMeshDims,
|
||||
FSDPModule,
|
||||
fully_shard,
|
||||
MixedPrecisionPolicy,
|
||||
OffloadPolicy,
|
||||
register_fsdp_forward_method,
|
||||
share_comm_ctx,
|
||||
UnshardHandle,
|
||||
)
|
||||
from .fully_sharded_data_parallel import (
|
||||
BackwardPrefetch,
|
||||
CPUOffload,
|
||||
FullOptimStateDictConfig,
|
||||
FullStateDictConfig,
|
||||
FullyShardedDataParallel,
|
||||
LocalOptimStateDictConfig,
|
||||
LocalStateDictConfig,
|
||||
MixedPrecision,
|
||||
OptimStateDictConfig,
|
||||
OptimStateKeyType,
|
||||
ShardedOptimStateDictConfig,
|
||||
ShardedStateDictConfig,
|
||||
ShardingStrategy,
|
||||
StateDictConfig,
|
||||
StateDictSettings,
|
||||
StateDictType,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
# FSDP1
|
||||
"BackwardPrefetch",
|
||||
"CPUOffload",
|
||||
"FullOptimStateDictConfig",
|
||||
"FullStateDictConfig",
|
||||
"FullyShardedDataParallel",
|
||||
"LocalOptimStateDictConfig",
|
||||
"LocalStateDictConfig",
|
||||
"MixedPrecision",
|
||||
"OptimStateDictConfig",
|
||||
"OptimStateKeyType",
|
||||
"ShardedOptimStateDictConfig",
|
||||
"ShardedStateDictConfig",
|
||||
"ShardingStrategy",
|
||||
"StateDictConfig",
|
||||
"StateDictSettings",
|
||||
"StateDictType",
|
||||
# FSDP2
|
||||
"CPUOffloadPolicy",
|
||||
"DataParallelMeshDims",
|
||||
"FSDPModule",
|
||||
"fully_shard",
|
||||
"MixedPrecisionPolicy",
|
||||
"OffloadPolicy",
|
||||
"register_fsdp_forward_method",
|
||||
"UnshardHandle",
|
||||
"share_comm_ctx",
|
||||
]
|
||||
|
||||
# Set namespace for exposed private names
|
||||
CPUOffloadPolicy.__module__ = "torch.distributed.fsdp"
|
||||
DataParallelMeshDims.__module__ = "torch.distributed.fsdp"
|
||||
FSDPModule.__module__ = "torch.distributed.fsdp"
|
||||
fully_shard.__module__ = "torch.distributed.fsdp"
|
||||
MixedPrecisionPolicy.__module__ = "torch.distributed.fsdp"
|
||||
OffloadPolicy.__module__ = "torch.distributed.fsdp"
|
||||
register_fsdp_forward_method.__module__ = "torch.distributed.fsdp"
|
||||
UnshardHandle.__module__ = "torch.distributed.fsdp"
|
||||
share_comm_ctx.__module__ = "torch.distributed.fsdp"
|
||||
@@ -0,0 +1,717 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""
|
||||
This file includes private common utilities for FSDP.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import traceback
|
||||
import warnings
|
||||
import weakref
|
||||
from collections.abc import Callable, Generator, Iterable, Iterator
|
||||
from enum import auto, Enum
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from typing import Any, cast, no_type_check, Optional, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.fsdp._flat_param as flat_param_file
|
||||
import torch.nn as nn
|
||||
from torch.distributed._composable_state import _get_module_state, _State
|
||||
from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
|
||||
_CHECKPOINT_PREFIX,
|
||||
)
|
||||
from torch.distributed.utils import _apply_to_tensors
|
||||
from torch.utils._mode_utils import no_dispatch
|
||||
|
||||
from .api import (
|
||||
FullOptimStateDictConfig,
|
||||
FullStateDictConfig,
|
||||
OptimStateDictConfig,
|
||||
ShardingStrategy,
|
||||
StateDictConfig,
|
||||
StateDictType,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from torch.distributed.device_mesh import DeviceMesh
|
||||
from torch.distributed.fsdp._fsdp_extensions import FSDPExtensions
|
||||
|
||||
from ._flat_param import FlatParamHandle
|
||||
|
||||
|
||||
_MAX_TRAVERSE_DEPTH = 128
|
||||
|
||||
|
||||
def _is_namedtuple(obj: Any) -> bool:
|
||||
# Mirrors torch.nn.parallel.scatter_gather._is_namedtuple
|
||||
fields = getattr(type(obj), "_fields", None)
|
||||
return (
|
||||
isinstance(obj, tuple)
|
||||
and hasattr(obj, "_asdict")
|
||||
and isinstance(fields, tuple)
|
||||
and all(isinstance(f, str) for f in fields)
|
||||
)
|
||||
|
||||
|
||||
def collect_grad_tensors(output: Any) -> tuple[torch.Tensor, ...]:
|
||||
"""
|
||||
Recursively collect tensors that require gradients from a nested structure.
|
||||
|
||||
Traverses dict, list, tuple, NamedTuple, and dataclass containers.
|
||||
Sets and other iterables are *not* traversed (consistent with
|
||||
``tree_flatten``). Uses the same traversal order as
|
||||
:func:`replace_grad_tensors`.
|
||||
"""
|
||||
tensors_list: list[torch.Tensor] = []
|
||||
_collect_grad_tensors(output, tensors_list)
|
||||
return tuple(tensors_list)
|
||||
|
||||
|
||||
def _collect_grad_tensors(
|
||||
output: Any, out: list[torch.Tensor], _depth: int = 0
|
||||
) -> None:
|
||||
"""Collect grad-requiring tensors in the same order as _replace_grad_tensors."""
|
||||
if _depth >= _MAX_TRAVERSE_DEPTH:
|
||||
raise RuntimeError(
|
||||
f"collect_grad_tensors exceeded max depth ({_MAX_TRAVERSE_DEPTH}), "
|
||||
"likely due to a circular reference in the output structure"
|
||||
)
|
||||
# Branch order must mirror _replace_grad_tensors exactly.
|
||||
# Only dict, list, tuple, NamedTuple, and dataclass are traversed;
|
||||
# set and other iterables are intentionally skipped (matching tree_flatten).
|
||||
if torch.is_tensor(output) and output.requires_grad:
|
||||
out.append(output)
|
||||
elif _is_namedtuple(output):
|
||||
# NamedTuple before dataclass to match _replace_grad_tensors ordering.
|
||||
for item in output:
|
||||
_collect_grad_tensors(item, out, _depth + 1)
|
||||
elif dataclasses.is_dataclass(output) and not isinstance(output, type):
|
||||
for field in dataclasses.fields(output):
|
||||
_collect_grad_tensors(getattr(output, field.name), out, _depth + 1)
|
||||
elif isinstance(output, dict):
|
||||
for v in output.values():
|
||||
_collect_grad_tensors(v, out, _depth + 1)
|
||||
elif isinstance(output, (list, tuple)):
|
||||
for item in output:
|
||||
_collect_grad_tensors(item, out, _depth + 1)
|
||||
|
||||
|
||||
def replace_grad_tensors(output: Any, tensor_iter: Iterator[torch.Tensor]) -> Any:
|
||||
"""
|
||||
Replace grad-requiring tensors in a nested structure using replacements
|
||||
from tensor_iter.
|
||||
|
||||
Tensors are consumed from tensor_iter in the same traversal order as
|
||||
:func:`collect_grad_tensors`. Traverses dict, list, tuple, NamedTuple,
|
||||
and dataclass containers; sets and other iterables are *not* traversed
|
||||
(consistent with ``tree_flatten``).
|
||||
|
||||
Note: dataclass reconstruction uses ``dataclasses.replace()``, which calls
|
||||
``__init__``. Dataclasses with custom ``__init__`` validation,
|
||||
``__post_init__`` side effects, or non-standard dict subclass constructors
|
||||
may not be compatible. In practice, FSDP module outputs are expected to be
|
||||
shallowly nested, so recursion depth is not a concern.
|
||||
"""
|
||||
result = _replace_grad_tensors(output, tensor_iter)
|
||||
sentinel = object()
|
||||
leftover = next(tensor_iter, sentinel)
|
||||
if leftover is not sentinel:
|
||||
# Count remaining without holding references to all of them
|
||||
n = 1 + sum(1 for _ in tensor_iter)
|
||||
raise RuntimeError(
|
||||
f"{n} replacement tensors were not consumed while processing "
|
||||
f"{type(output).__qualname__}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _replace_grad_tensors(
|
||||
output: Any, tensor_iter: Iterator[torch.Tensor], _depth: int = 0
|
||||
) -> Any:
|
||||
# Branch order must mirror _collect_grad_tensors exactly.
|
||||
if _depth >= _MAX_TRAVERSE_DEPTH:
|
||||
raise RuntimeError(
|
||||
f"replace_grad_tensors exceeded max depth ({_MAX_TRAVERSE_DEPTH}), "
|
||||
"likely due to a circular reference in the output structure"
|
||||
)
|
||||
if torch.is_tensor(output) and output.requires_grad:
|
||||
return next(tensor_iter)
|
||||
elif _is_namedtuple(output):
|
||||
# NamedTuple before dataclass: a NamedTuple that is also a dataclass
|
||||
# should be reconstructed via positional args, not dataclasses.replace.
|
||||
new_items = []
|
||||
any_changed = False
|
||||
for item in output:
|
||||
new_item = _replace_grad_tensors(item, tensor_iter, _depth + 1)
|
||||
new_items.append(new_item)
|
||||
if new_item is not item:
|
||||
any_changed = True
|
||||
if any_changed:
|
||||
return type(output)(*new_items)
|
||||
return output
|
||||
elif dataclasses.is_dataclass(output) and not isinstance(output, type):
|
||||
changes = {}
|
||||
for field in dataclasses.fields(output):
|
||||
old_val = getattr(output, field.name)
|
||||
new_val = _replace_grad_tensors(old_val, tensor_iter, _depth + 1)
|
||||
if new_val is not old_val:
|
||||
changes[field.name] = new_val
|
||||
if changes:
|
||||
try:
|
||||
return dataclasses.replace(output, **changes)
|
||||
except TypeError as e:
|
||||
raise TypeError(
|
||||
f"Failed to reconstruct dataclass {type(output).__qualname__} "
|
||||
f"via dataclasses.replace(). Dataclasses used as FSDP module "
|
||||
f"inputs/outputs must support dataclasses.replace(): {e}"
|
||||
) from None
|
||||
return output
|
||||
elif isinstance(output, dict):
|
||||
new_dict = {}
|
||||
any_changed = False
|
||||
for k, v in output.items():
|
||||
new_v = _replace_grad_tensors(v, tensor_iter, _depth + 1)
|
||||
new_dict[k] = new_v
|
||||
if new_v is not v:
|
||||
any_changed = True
|
||||
if any_changed:
|
||||
return new_dict if type(output) is dict else type(output)(new_dict)
|
||||
return output
|
||||
elif isinstance(output, (list, tuple)):
|
||||
new_items = []
|
||||
any_changed = False
|
||||
for item in output:
|
||||
new_item = _replace_grad_tensors(item, tensor_iter, _depth + 1)
|
||||
new_items.append(new_item)
|
||||
if new_item is not item:
|
||||
any_changed = True
|
||||
if any_changed:
|
||||
typ = type(output)
|
||||
try:
|
||||
return typ(new_items)
|
||||
except TypeError:
|
||||
# Fall back to base type for subclasses with custom __init__
|
||||
return list(new_items) if isinstance(output, list) else tuple(new_items)
|
||||
return output
|
||||
else:
|
||||
return output
|
||||
|
||||
|
||||
FSDP_WRAPPED_MODULE = "_fsdp_wrapped_module"
|
||||
FSDP_PREFIX = FSDP_WRAPPED_MODULE + "."
|
||||
FSDP_FLATTENED = "_fsdp_flattened"
|
||||
|
||||
# Save a global mapping from module to its input tensor dtype to be populated
|
||||
# during the forward pre-hook and consumed in the forward post-hook when
|
||||
# overriding a module's mixed precision
|
||||
# NOTE: We currently take the last input tensor's dtype in the case of multiple
|
||||
# floating-point input tensors, which may be incorrect. However, since there is
|
||||
# not a 1:1 correspondence between input and output tensors, we must use *some*
|
||||
# heuristic like this to predict the desired output dtype.
|
||||
_MODULE_TO_INP_DTYPE: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
|
||||
|
||||
|
||||
class _FSDPDeviceHandle:
|
||||
"""
|
||||
This is a simple abstraction for FSDP computing devices,
|
||||
which enables custom backends that implement CUDA-like
|
||||
semantics to be integrated with FSDP.
|
||||
"""
|
||||
|
||||
def __init__(self, device: torch.device, backend: Any = None):
|
||||
if backend is None:
|
||||
try:
|
||||
self.__backend = getattr(torch, device.type)
|
||||
self.__device = device
|
||||
except AttributeError as exc:
|
||||
raise AttributeError(
|
||||
f"Device '{device}' does not have a corresponding backend registered as 'torch.{device.type}'."
|
||||
) from exc
|
||||
else:
|
||||
self.__backend = backend
|
||||
|
||||
@classmethod
|
||||
def from_device(cls, device: torch.device) -> "_FSDPDeviceHandle":
|
||||
"""
|
||||
Return a device handle corresponding to the device, and through this handle,
|
||||
operations with the same semantics as CUDA can be performed on the device.
|
||||
Just return torch.cuda if the device is cuda to make attribute-access faster.
|
||||
Custom backend must first register a module with the same name with {device.type} on torch.
|
||||
"""
|
||||
if device.type == "cuda":
|
||||
return cast(_FSDPDeviceHandle, torch.cuda)
|
||||
elif device.type == "mtia":
|
||||
return cast(_FSDPDeviceHandle, torch.mtia)
|
||||
return cls(device)
|
||||
|
||||
def __getattr__(self, name: str, /) -> Any:
|
||||
try:
|
||||
return getattr(self.__backend, name)
|
||||
except AttributeError as exc:
|
||||
raise AttributeError(
|
||||
f"Custom backend '{self.__device.type}' not implement 'torch.{self.__device.type}.{name}'"
|
||||
) from exc
|
||||
|
||||
|
||||
class _UninitializedDeviceHandle(_FSDPDeviceHandle):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def __getattribute__(self, name: str, /) -> Any:
|
||||
raise RuntimeError("Trying to use an uninitialized device handle.")
|
||||
|
||||
|
||||
class _FSDPState(_State):
|
||||
def __init__(self) -> None:
|
||||
# TODO: Move all the attributes to this class to enable typing for
|
||||
# FSDP/fully_shard.
|
||||
self._ignored_modules: set[nn.Module] = set()
|
||||
self._ignored_params: set[nn.Parameter] = set()
|
||||
# Buffer names are cleaned (without wrapper prefixes)
|
||||
self._ignored_buffer_names: set[str] = set()
|
||||
self.process_group: dist.ProcessGroup | None = None
|
||||
self.rank: int = -1
|
||||
self.world_size: int = -1
|
||||
self._device_mesh: DeviceMesh | None = None
|
||||
self.sharding_strategy = ShardingStrategy.FULL_SHARD
|
||||
self._use_orig_params: bool = False
|
||||
self.training_state = TrainingState.IDLE
|
||||
self._unshard_params_ctx: dict[nn.Module, Generator] = {}
|
||||
self._state_dict_type: StateDictType = StateDictType.FULL_STATE_DICT
|
||||
self._state_dict_config: StateDictConfig = FullStateDictConfig()
|
||||
self._optim_state_dict_config: OptimStateDictConfig = FullOptimStateDictConfig()
|
||||
self._is_root: bool | None = None
|
||||
self._handle: flat_param_file.FlatParamHandle | None = None
|
||||
self._fully_sharded_module_to_handle: dict[
|
||||
nn.Module, flat_param_file.FlatParamHandle | None
|
||||
] = {}
|
||||
self.compute_device: torch.device | None = None
|
||||
self._gradient_predivide_factor: int = 0
|
||||
self._gradient_postdivide_factor: int = 0
|
||||
self._comm_hook: Callable | None = None
|
||||
self._comm_hook_state: Any | None = None
|
||||
self._unshard_event: torch.Event | None = None
|
||||
# Abstract device handle for fsdp compute device. For now,
|
||||
# the compute device must implement cuda semantics used by fsdp
|
||||
self._device_handle: _FSDPDeviceHandle = _UninitializedDeviceHandle()
|
||||
# All following attributes should only be used for root states:
|
||||
# Save these static lists to avoid the repeated tree traversals
|
||||
self._all_fsdp_states: list[_FSDPState] = []
|
||||
self._all_handles: list[flat_param_file.FlatParamHandle] = []
|
||||
self._fsdp_extension: FSDPExtensions | None = None
|
||||
|
||||
|
||||
def _get_module_fsdp_state(module: nn.Module) -> _FSDPState | None:
|
||||
state = _get_module_state(module)
|
||||
if state is None or not isinstance(state, _FSDPState):
|
||||
return None
|
||||
return state
|
||||
|
||||
|
||||
def _get_module_fsdp_state_if_fully_sharded_module(
|
||||
module: nn.Module,
|
||||
) -> _FSDPState | None:
|
||||
state = _get_module_fsdp_state(module)
|
||||
if state is None:
|
||||
return None
|
||||
if state == module: # FullyShardedDataParallel module case.
|
||||
return state
|
||||
if module in state._fully_sharded_module_to_handle: # fully_shard case.
|
||||
return state
|
||||
return None
|
||||
|
||||
|
||||
class TrainingState(Enum):
|
||||
"""
|
||||
An enum that indicates the state of a ``FullyShardedDataParallel` instance.
|
||||
"""
|
||||
|
||||
IDLE = auto()
|
||||
FORWARD_BACKWARD = auto()
|
||||
SUMMON_FULL_PARAMS = auto()
|
||||
|
||||
|
||||
class HandleTrainingState(Enum):
|
||||
"""
|
||||
An enum that indicates the state of a ``FlatParamHandle`.
|
||||
"""
|
||||
|
||||
IDLE = auto()
|
||||
FORWARD = auto()
|
||||
BACKWARD_PRE = auto()
|
||||
BACKWARD_POST = auto()
|
||||
SUMMON_FULL_PARAMS = auto()
|
||||
|
||||
|
||||
def _is_composable(state: _FSDPState):
|
||||
# TODO: This is a temporary hack for differentiate between code paths.
|
||||
return not isinstance(state, nn.Module)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _module_handle(state: _FSDPState, module: nn.Module) -> Optional["FlatParamHandle"]:
|
||||
"""
|
||||
Returns the ``FlatParamHandle`` s corresponding to ``module``. This is
|
||||
the handle that contains some parameter in ``module``.
|
||||
"""
|
||||
if _is_composable(state):
|
||||
# A valid FSDP state may have no managed parameters and hence no
|
||||
# handles, meaning no entry in `_fully_sharded_module_to_handles`
|
||||
if state._handle is None:
|
||||
return None
|
||||
if module not in state._fully_sharded_module_to_handle:
|
||||
raise AssertionError(
|
||||
f"Expects a fully sharded module but got {module} on rank {state.rank}"
|
||||
)
|
||||
return state._fully_sharded_module_to_handle[module]
|
||||
else:
|
||||
# NOTE: This assumes `module` is a `FullyShardedDataParallel` instance.
|
||||
return module._handle
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _has_fsdp_params(state: _FSDPState, module: nn.Module) -> bool:
|
||||
"""Returns if ``module`` has parameters managed by FSDP."""
|
||||
return _module_handle(state, module) is not None
|
||||
|
||||
|
||||
def _get_sharding_strategy(handle):
|
||||
"""
|
||||
Returns the sharding strategy of the handle.
|
||||
"""
|
||||
return handle._sharding_strategy if handle else None
|
||||
|
||||
|
||||
def clean_tensor_name(tensor_name: str) -> str:
|
||||
"""
|
||||
Cleans the parameter or buffer name by removing any module wrapper
|
||||
prefixes.
|
||||
"""
|
||||
tensor_name = tensor_name.replace(FSDP_PREFIX, "")
|
||||
# TODO: Explicitly replacing the checkpoint wrapper prefix is not ideal as
|
||||
# it couples `CheckpointWrapper` and FSDP and also does not scale for more
|
||||
# module wrappers.
|
||||
tensor_name = tensor_name.replace(_CHECKPOINT_PREFIX, "")
|
||||
return tensor_name
|
||||
|
||||
|
||||
def _set_fsdp_flattened(tensor: torch.Tensor) -> None:
|
||||
"""
|
||||
Sets an attribute on ``tensor`` to mark it as flattened by FSDP. This is to
|
||||
avoid re-flattening it during nested construction.
|
||||
"""
|
||||
setattr(tensor, FSDP_FLATTENED, True)
|
||||
|
||||
|
||||
def _is_fsdp_flattened(tensor: torch.Tensor) -> bool:
|
||||
"""Returns if ``tensor`` has been marked as flattened by FSDP."""
|
||||
return getattr(tensor, FSDP_FLATTENED, False)
|
||||
|
||||
|
||||
def _named_parameters_with_duplicates(
|
||||
module: nn.Module, **kwargs: Any
|
||||
) -> list[tuple[str, nn.Parameter]]:
|
||||
"""
|
||||
This API is required as some modules overwrite `named_parameters()` but do not support
|
||||
`remove_duplicate`.
|
||||
"""
|
||||
if "remove_duplicate" in kwargs:
|
||||
raise AssertionError(
|
||||
"_named_parameters_with_duplicates cannot be used with `remove_duplicate` argument."
|
||||
)
|
||||
kwargs["remove_duplicate"] = False
|
||||
try:
|
||||
ret = list(module.named_parameters(**kwargs))
|
||||
except AssertionError:
|
||||
kwargs.pop("remove_duplicate")
|
||||
ret = list(module.named_parameters(**kwargs))
|
||||
return ret
|
||||
|
||||
|
||||
def _get_param_to_fqns(
|
||||
model: torch.nn.Module,
|
||||
dedup_shared_params: bool = True,
|
||||
) -> dict[nn.Parameter, list[str]]:
|
||||
"""
|
||||
Constructs a mapping from parameter to a list of its \"canonical\" FQNs. Here,
|
||||
we use canonical to mean the fully-qualified name assigned to the parameter
|
||||
based on its position in the original nn.Module hierarchy before any wrapper
|
||||
or parallelism has been applied to it. This is in contrast to FQNs that may be
|
||||
generated after parallelisms or wrappers have been applied to the model.
|
||||
|
||||
Each normal parameter maps to a singleton list containing its FQN, while each
|
||||
``FlatParameter`` maps to a list of its original parameter FQNs, which may
|
||||
have length greater than one. All FQNs are prefixed starting from ``model``.
|
||||
|
||||
In the case where FSDP was applied with ``use_orig_params=True``, there should be no
|
||||
``FlatParameter`` s registered to the model's modules and this mapping will only
|
||||
contain mappings from ``nn.Parameter`` s to singleton FQN lists.
|
||||
|
||||
It is only in the case where FSDP was applied with ``use_orig_params=False`` where
|
||||
a ``FlatParameter`` will be registered in place of the original parameters and there
|
||||
will be mappings from each ``FlatParameter`` to lists of FQNs corresponding to the
|
||||
original parameters.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Root module (which may or may not be a
|
||||
:class:`FullyShardedDataParallel` instance).
|
||||
dedup_shared_params (bool): For shared parameters, if ``True``, only
|
||||
includes the FQNs corresponding to the first encounter of the
|
||||
shared parameter in the module traversal; if ``False``, then
|
||||
includes the FQNs across all encounters. (Default: ``True``)
|
||||
"""
|
||||
|
||||
def module_fn(module, prefix, tree_level, param_to_fqns):
|
||||
for param_name, param in _named_parameters_with_duplicates(
|
||||
module, recurse=False
|
||||
):
|
||||
local_fqns = (
|
||||
param._fqns
|
||||
if isinstance(param, flat_param_file.FlatParameter)
|
||||
else [param_name]
|
||||
) # prefixed from `module`
|
||||
global_fqns = [
|
||||
clean_tensor_name(prefix + name) for name in local_fqns
|
||||
] # prefixed from the top level `model` (i.e. including `prefix`)
|
||||
is_shared_param = param in param_to_fqns
|
||||
if not is_shared_param:
|
||||
param_to_fqns[param] = global_fqns
|
||||
else:
|
||||
if isinstance(param, flat_param_file.FlatParameter):
|
||||
# DMP overwrites `named_parameters` and skip (advance to
|
||||
# the next child module) the wrapped_module (e.g.,
|
||||
# _dmp_wrapped_module and _fsdp_wrapped_module). When a user
|
||||
# calls `named_child` to traverse the module recursively and
|
||||
# calls `named_parameters` with `recurse=False`, parameters
|
||||
# will be traversed more than once.
|
||||
# This hack is specified designed for DMP + FSDP. We
|
||||
# overwrite the flat_parameters traversal result to only obtain
|
||||
# the last one, which happens to be the correct one.
|
||||
#
|
||||
# TODO: Remove this hack once DMP + FSDP is not supported.
|
||||
warnings.warn(
|
||||
"FlatParameter is being traversed more than once. "
|
||||
"This case should only happen when using "
|
||||
"DistributedModelParallel with FullyShardedDataParallel.",
|
||||
stacklevel=2,
|
||||
)
|
||||
param_to_fqns[param] = global_fqns
|
||||
elif not dedup_shared_params:
|
||||
param_to_fqns[param].extend(global_fqns)
|
||||
|
||||
def return_fn(param_to_fqns):
|
||||
return param_to_fqns
|
||||
|
||||
param_to_unflat_param_names: dict[torch.nn.Parameter, list[str]] = {}
|
||||
return _apply_to_modules(
|
||||
model,
|
||||
module_fn,
|
||||
return_fn,
|
||||
[key for key, _ in _named_parameters_with_duplicates(model)],
|
||||
param_to_unflat_param_names,
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _log_post_backward_hook(
|
||||
state: _FSDPState, handle: "FlatParamHandle", logger: logging.Logger
|
||||
) -> None:
|
||||
# Under TORCH_DISTRIBUTED_DEBUG=INFO, log the module names this hook fires for.
|
||||
# Below logging of module names this post-bwd hook fires for can help debug certain
|
||||
# cases where hooks don't fire, such as under certain activation checkpoint configs.
|
||||
if state._use_orig_params and handle._debug_level == dist.DebugLevel.INFO:
|
||||
param_fqns = _get_handle_fqns_from_root(state, handle)
|
||||
logger.warning("FSDP firing post-backward hooks for parameters %s", param_fqns)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _get_handle_fqns_from_root(
|
||||
state: _FSDPState, handle: "FlatParamHandle"
|
||||
) -> list[str] | None:
|
||||
if handle is None:
|
||||
return None
|
||||
param_to_fqn = state._exec_order_data.param_to_fqn
|
||||
handle_params = handle.flat_param._params # only populated for use_orig_params
|
||||
param_fqns = [*chain.from_iterable(param_to_fqn[p] for p in handle_params)]
|
||||
return param_fqns
|
||||
|
||||
|
||||
def _apply_to_modules(
|
||||
root_module: torch.nn.Module,
|
||||
module_fn: Callable,
|
||||
return_fn: Callable,
|
||||
filter_fqns: list[str] | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Performs a pre-order traversal of the modules in the hierarchy rooted at
|
||||
``root_module``, applying ``module_fn`` at each module and finally
|
||||
returning a value using ``return_fn``. The traversal constructs the full
|
||||
module prefix name (e.g. "module.submodule." just like in model state dict)
|
||||
and makes that available to ``module_fn``.
|
||||
|
||||
``filter_fqns`` is used because some module may have its own prefix similar
|
||||
to ``FullyShardedDataParallel`` and the ``named_parameters()`` is overwritten
|
||||
to remove the prefix.
|
||||
"""
|
||||
|
||||
# Precompute the set of all prefixes from filter_fqns so that the
|
||||
# "any FQN starts with new_prefix?" check is O(1) instead of O(N).
|
||||
filter_prefixes: set[str] | None = None
|
||||
if filter_fqns is not None:
|
||||
filter_prefixes = set()
|
||||
for fqn in filter_fqns:
|
||||
i = fqn.find(".")
|
||||
while i != -1:
|
||||
filter_prefixes.add(fqn[: i + 1])
|
||||
i = fqn.find(".", i + 1)
|
||||
|
||||
def f(module: torch.nn.Module, prefix: str, tree_level: int, *args, **kwargs):
|
||||
# Call the module function before recursing over children (pre-order)
|
||||
module_fn(module, prefix, tree_level, *args, **kwargs)
|
||||
for submodule_name, submodule in module.named_children():
|
||||
if submodule is None:
|
||||
continue
|
||||
new_prefix = prefix + submodule_name + "."
|
||||
new_tree_level = tree_level + 1
|
||||
if filter_prefixes is not None:
|
||||
if new_prefix not in filter_prefixes:
|
||||
# DMP's named_parameter() will mess up the traversal with
|
||||
# ``named_children`` + `named_parameter(recurse=False)``.
|
||||
# This hack is a must to make the traversal work.
|
||||
# TODO: Remove this hack once DMP + FSDP is not supported.
|
||||
# It turns out that recursive wrapping may trigger this as
|
||||
# well.
|
||||
if (
|
||||
submodule_name == "_fsdp_wrapped_module"
|
||||
or submodule_name == "_dmp_wrapped_module"
|
||||
):
|
||||
new_prefix = prefix
|
||||
elif submodule_name == "module":
|
||||
new_prefix = prefix
|
||||
f(submodule, new_prefix, new_tree_level, *args, **kwargs)
|
||||
|
||||
f(root_module, "", 0, *args, **kwargs)
|
||||
return return_fn(*args, **kwargs)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _assert_in_training_states(
|
||||
state: _FSDPState,
|
||||
training_states: list[TrainingState],
|
||||
) -> None:
|
||||
"""Asserts that FSDP is in the states ``_training_states``."""
|
||||
# Raise a `ValueError` instead of using `assert` to ensure that these
|
||||
# logical assertions run even if `assert`s are disabled
|
||||
if state.training_state not in training_states:
|
||||
msg = (
|
||||
f"expected to be in states {training_states} but current state is "
|
||||
f"{state.training_state}"
|
||||
)
|
||||
# Print the error on rank 0 in case this is called in the backward pass
|
||||
if state.rank == 0:
|
||||
if isinstance(state, nn.Module):
|
||||
print(f"Asserting FSDP instance is: {state}")
|
||||
print(f"ERROR: {msg}")
|
||||
traceback.print_stack()
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _get_root_modules(modules: set[nn.Module]) -> set[nn.Module]:
|
||||
"""
|
||||
Returns:
|
||||
Set[nn.Module]: The subset of ``modules`` that are root modules (i.e.
|
||||
parent-less) with respect to the modules in the set itself. In other
|
||||
words, these are the modules in ``modules`` that are not the child of
|
||||
any other module in ``modules``.
|
||||
"""
|
||||
root_modules: set[nn.Module] = set()
|
||||
module_to_submodules = {module: set(module.modules()) for module in modules}
|
||||
for candidate_module in modules:
|
||||
is_root_module = True
|
||||
for module, submodules in module_to_submodules.items():
|
||||
is_child_module = (
|
||||
candidate_module is not module and candidate_module in submodules
|
||||
)
|
||||
if is_child_module:
|
||||
is_root_module = False
|
||||
break
|
||||
if is_root_module:
|
||||
root_modules.add(candidate_module)
|
||||
return root_modules
|
||||
|
||||
|
||||
def _override_module_mixed_precision(
|
||||
root: torch.nn.Module,
|
||||
module_classes_to_override: Iterable[type[nn.Module]],
|
||||
wrap_override_dict: dict[str, Any] = {"mixed_precision": None}, # noqa: B006
|
||||
) -> set[type[nn.Module]]:
|
||||
module_classes_to_override = tuple(set(module_classes_to_override))
|
||||
# Return a set of the actually overridden module classes
|
||||
overridden_module_classes: set[type[nn.Module]] = set()
|
||||
for mod in root.modules():
|
||||
if isinstance(mod, module_classes_to_override):
|
||||
overridden_module_classes.add(type(mod))
|
||||
mod._wrap_overrides = wrap_override_dict # type: ignore[assignment]
|
||||
# TODO: We need to run this mixed precision ignored module in fp32,
|
||||
# but ensure subsequent modules, that may possibly be running with
|
||||
# mixed precision, still receive the appropriate precision inputs
|
||||
# without user having to adjust mixed precision config too much.
|
||||
# As a result, we attach pre and post forward hooks to up / down
|
||||
# cast. We should revisit this design.
|
||||
|
||||
def cast_fn(
|
||||
dtype: torch.dtype, module: nn.Module, x: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
if not torch.is_floating_point(x) or x.dtype == dtype:
|
||||
return x
|
||||
_MODULE_TO_INP_DTYPE[module] = x.dtype
|
||||
return x.to(dtype)
|
||||
|
||||
def forward_pre_hook(module, args):
|
||||
return _apply_to_tensors(partial(cast_fn, torch.float32, module), args)
|
||||
|
||||
def forward_post_hook(module, args, output):
|
||||
# NOTE: If the forward did not have any floating-point tensors,
|
||||
# then the dtype will not be set for this module, and we do not
|
||||
# upcast the dtype.
|
||||
if module in _MODULE_TO_INP_DTYPE:
|
||||
old_dtype = _MODULE_TO_INP_DTYPE[module]
|
||||
return _apply_to_tensors(
|
||||
partial(cast_fn, old_dtype, module), output
|
||||
)
|
||||
|
||||
# We intentionally append both of these hooks so that they run after
|
||||
# all other hooks.
|
||||
mod.register_forward_pre_hook(forward_pre_hook, prepend=False)
|
||||
mod.register_forward_hook(forward_post_hook, prepend=False)
|
||||
return overridden_module_classes
|
||||
|
||||
|
||||
def _no_dispatch_record_stream(tensor: torch.Tensor, stream: torch.Stream) -> None:
|
||||
# FIXME record_stream doesn't work with non-cuda/mtia/xpu tensors
|
||||
if tensor.device.type not in [
|
||||
"cuda",
|
||||
"mtia",
|
||||
"xpu",
|
||||
torch._C._get_privateuse1_backend_name(),
|
||||
]:
|
||||
return
|
||||
|
||||
if torch.distributed._functional_collectives.is_torchdynamo_compiling():
|
||||
return
|
||||
# from @ezyang:
|
||||
# The no_dispatch was added in https://github.com/pytorch/pytorch/pull/88014 cc @fegin
|
||||
# Looking over the PR, it looks like this is because we don't actually support Stream arguments
|
||||
# in torch dispatch, so it just chokes.
|
||||
# If Dynamo is able to answer "are there any torch dispatch modes" active (it should answer False),
|
||||
# a better version of this would just be to check if there are any modes before disabling dispatch.
|
||||
# TODO(voz): Extend a dynamo util to answer the above, unify the codepaths here.
|
||||
tensor.record_stream(stream)
|
||||
else:
|
||||
with no_dispatch():
|
||||
tensor.record_stream(stream)
|
||||
@@ -0,0 +1,159 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from enum import Enum
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.fsdp._flat_param as flat_param_file
|
||||
from torch.distributed.fsdp._common_utils import (
|
||||
_apply_to_modules,
|
||||
_get_module_fsdp_state,
|
||||
clean_tensor_name,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SimpleProfiler:
|
||||
class Type(str, Enum):
|
||||
ALL = "all"
|
||||
ALLGATHER = "all_gather"
|
||||
ALLGATHER_OBJ = "all_gather_object"
|
||||
RESHARDING = "resharding"
|
||||
H2D = "H2D"
|
||||
D2H = "D2H"
|
||||
|
||||
results: dict[str, float] = defaultdict(float)
|
||||
profiling: set[str] = set()
|
||||
|
||||
@classmethod
|
||||
def reset(cls) -> None:
|
||||
cls.results.clear()
|
||||
cls.profiling.clear()
|
||||
|
||||
@classmethod
|
||||
@contextmanager
|
||||
def profile(cls, profile_type: str) -> Iterator[None]:
|
||||
if profile_type in cls.profiling:
|
||||
raise AssertionError(
|
||||
f"{profile_type} is already being profiled. "
|
||||
"SimpleProfiler does not support profiling multiple instances at "
|
||||
"the same time. "
|
||||
)
|
||||
|
||||
cls.profiling.add(profile_type)
|
||||
begin = time.monotonic()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
end = time.monotonic()
|
||||
cls.results[profile_type] += end - begin
|
||||
cls.profiling.remove(profile_type)
|
||||
|
||||
@classmethod
|
||||
def dump_and_reset(cls, msg: str) -> None:
|
||||
# This cannot be combined with DETAIL distributed log
|
||||
# as the profiling will be very incorrect.
|
||||
if dist.get_rank() == 0 and dist.get_debug_level() == dist.DebugLevel.INFO:
|
||||
logger.info("%s %s", msg, cls.results)
|
||||
cls.reset()
|
||||
|
||||
|
||||
def _get_sharded_module_tree_with_module_name_to_fqns(
|
||||
model: torch.nn.Module,
|
||||
) -> tuple[str, dict[str, list[str]]]:
|
||||
"""
|
||||
It is used for composable fully_shard() code path, it returns
|
||||
1. sharded module tree info: each line represents a submodule name that contains the
|
||||
submodule's FQN and its submodule class name, if the submodule is sharded by `fully_shard`,
|
||||
the submodule name will add a postfix with ' FULLY SHARDED'. Each increased tree
|
||||
level adds 4 spaces before the printed name. A printed sharded module tree info for a toy model
|
||||
is like this:
|
||||
[CompositeModel] FULLY SHARDED
|
||||
l1[Linear]
|
||||
u1[UnitModule] FULLY SHARDED
|
||||
u1.l1[Linear]
|
||||
u1.seq[Sequential]
|
||||
u1.seq.0[ReLU]
|
||||
u1.seq.1[Linear]
|
||||
u1.seq.2[ReLU]
|
||||
u1.l2[Linear]
|
||||
u2[UnitModule] FULLY SHARDED
|
||||
u2.l1[Linear]
|
||||
u2.seq[Sequential]
|
||||
u2.seq.0[ReLU]
|
||||
u2.seq.1[Linear]
|
||||
u2.seq.2[ReLU]
|
||||
u2.l2[Linear]
|
||||
l2[Linear]
|
||||
2. a dict mapping from the concated module FQN and class name to a list of its managed
|
||||
original parameters' FQNs. An example of the dict for the above toy sharded model is like this:
|
||||
{'[CompositeModel]': ['l1.weight', 'l1.bias', 'l2.weight', 'l2.bias'],
|
||||
'u1[UnitModule]': ['u1.l1.weight', 'u1.l1.bias', 'u1.seq.1.weight', 'u1.seq.1.bias', 'u1.l2.weight', 'u1.l2.bias'],
|
||||
'u2[UnitModule]': ['u2.l1.weight', 'u2.l1.bias', 'u2.seq.1.weight', 'u2.seq.1.bias', 'u2.l2.weight', 'u2.l2.bias']
|
||||
}
|
||||
All FQNs are prefixed starting from ``model``.
|
||||
|
||||
Args:
|
||||
model (torch.nn.Module): Root module (which may or may not be passed to
|
||||
composable `fully_shard()`).
|
||||
"""
|
||||
|
||||
def module_fn(
|
||||
module, prefix, tree_level, sharded_tree_info, sharded_module_name_to_fqns
|
||||
):
|
||||
num_spaces = tree_level * 4
|
||||
trimed_prefix = (
|
||||
prefix[:-1] if (len(prefix) > 0 and prefix[-1] == ".") else prefix
|
||||
)
|
||||
prefixed_module_name = trimed_prefix + "[" + module.__class__.__name__ + "]"
|
||||
printed_prefixed_module_name = " " * num_spaces + prefixed_module_name
|
||||
|
||||
state = _get_module_fsdp_state(module)
|
||||
if state is None:
|
||||
sharded_tree_info[0] += printed_prefixed_module_name + "\n"
|
||||
return
|
||||
|
||||
handle = state._fully_sharded_module_to_handle.get(module, None)
|
||||
|
||||
if handle:
|
||||
sharded_tree_info[0] += (
|
||||
printed_prefixed_module_name + " FULLY SHARDED" + "\n"
|
||||
)
|
||||
else:
|
||||
sharded_tree_info[0] += printed_prefixed_module_name + "\n"
|
||||
|
||||
if handle:
|
||||
param = handle.flat_param
|
||||
if not isinstance(param, flat_param_file.FlatParameter):
|
||||
raise AssertionError(f"Expected FlatParameter, got {type(param)}")
|
||||
global_fqns = [
|
||||
clean_tensor_name(prefix + name) for name in param._fqns
|
||||
] # prefixed from the top level `model` (i.e. including `prefix`)
|
||||
|
||||
if prefixed_module_name in sharded_module_name_to_fqns:
|
||||
sharded_module_name_to_fqns[prefixed_module_name].extend(global_fqns)
|
||||
else:
|
||||
sharded_module_name_to_fqns[prefixed_module_name] = global_fqns
|
||||
|
||||
def return_fn(sharded_tree_info, sharded_module_name_to_fqns):
|
||||
return sharded_tree_info[0], sharded_module_name_to_fqns
|
||||
|
||||
# Use List to mutate its value in place while running the recursive functions
|
||||
sharded_tree_info: list[str] = [
|
||||
"",
|
||||
]
|
||||
sharded_module_name_to_fqns: dict[str, list[str]] = {}
|
||||
return _apply_to_modules(
|
||||
model,
|
||||
module_fn,
|
||||
return_fn,
|
||||
[key for key, _ in model.named_parameters()],
|
||||
sharded_tree_info,
|
||||
sharded_module_name_to_fqns,
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
def _annotate_modules_for_dynamo(
|
||||
module: nn.Module,
|
||||
ignored_modules: set[nn.Module],
|
||||
use_orig_params: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Annotates the submodules in ``module`` 's tree, except those in
|
||||
``ignored_modules``, indicating that the submodules are FSDP-managed and
|
||||
saving the ``use_orig_params`` setting passed to the FSDP constructor.
|
||||
"""
|
||||
for submodule in module.modules():
|
||||
if submodule not in ignored_modules:
|
||||
"""[note: Dynamo treats FSDP wrapped modules as UnspecializedNNModule]
|
||||
|
||||
Dynamo doesn't get to see this instance (FullyShardedDataParallel) during tracing, since
|
||||
it skips tracing all the torch.distributed.fsdp code.
|
||||
- Why? Running the FSDP code eagerly avoids lots of issues trying to trace complex hooks, and also
|
||||
gets us graph-breaks on FSDP module boundaries which we want anyway for comm ops.
|
||||
- However, we _also_ want dynamo to treat the wrapped module inside FSDP 'unspecially' (*),
|
||||
and we need a way to indicate to dynamo which modules are wrapped by FSDP.
|
||||
|
||||
(*) UnspecializedNNModules in dynamo are traced-through without any assumptions, and with thorough
|
||||
guards. NNModules otherwise are 'specialized', meaning there is less overhead due to assuming
|
||||
their code is well-behaved.
|
||||
|
||||
One particular issue with specialized NNModules for FSDP is that the
|
||||
views created for orig_params are captured into the compiled graph on the first iteration, and while
|
||||
they are always going to point to the correct flatparameter and give correct results, their order
|
||||
of creation influences the order of backward execution, preventing overlap of comm and computation
|
||||
during backward. We need to _use_ the new parameter views created on each forward iteration, in
|
||||
order for backward to interleave hooks with compute per layer. UnspecializedNNModule lets us achieve
|
||||
this by capturing the module code more 'functionally' and passing parameters in as inputs each time.
|
||||
"""
|
||||
submodule._is_fsdp_managed_module = True # type: ignore[assignment]
|
||||
|
||||
# Dynamo only supports FSDP with use_orig_params=True.
|
||||
# This is hacky, but I could not think of another way to add an assertion to dynamo
|
||||
# for this, since Dynamo skips all the FSDP code frames and thus can't inspect the
|
||||
# FSDP module directly
|
||||
submodule._fsdp_use_orig_params = use_orig_params # type: ignore[assignment]
|
||||
@@ -0,0 +1,365 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import itertools
|
||||
import warnings
|
||||
from enum import auto, Enum
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.fsdp._traversal_utils as traversal_utils
|
||||
import torch.nn as nn
|
||||
from torch.distributed.fsdp._common_utils import _FSDPState, _get_param_to_fqns
|
||||
from torch.distributed.fsdp._flat_param import FlatParamHandle
|
||||
|
||||
|
||||
class _ExecOrderWarnStatus(Enum):
|
||||
"""Used internally for execution order validation."""
|
||||
|
||||
NONE = auto() # no deviation yet
|
||||
WARNING = auto() # deviated this iteration; currently issuing warnings
|
||||
WARNED = auto() # deviated in a previous iteration
|
||||
|
||||
|
||||
class _ExecOrderData:
|
||||
"""
|
||||
This contains the data structures to track the execution order. We track
|
||||
the pre-forward order on the *first* iteration for forward prefetching
|
||||
(which thus assumes static graph) and the post-forward order on *every*
|
||||
iteration for backward prefetching (which thus does not assume static
|
||||
graph but may be provide an incorrect order).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
debug_level: dist.DebugLevel,
|
||||
backward_prefetch_limit: int,
|
||||
forward_prefetch_limit: int,
|
||||
) -> None:
|
||||
# Tracks the (static) pre-forward order for execution order validation
|
||||
# and forward prefetching
|
||||
self.handles_pre_forward_order: list[FlatParamHandle] = []
|
||||
# Tracks the post-forward order for pre-backward prefetching
|
||||
self.handles_post_forward_order: list[FlatParamHandle | None] = []
|
||||
self._iter = 0
|
||||
|
||||
# Gives the max number of backward/forward prefetched all-gathers by a
|
||||
# single module
|
||||
self._backward_prefetch_limit = backward_prefetch_limit
|
||||
self._forward_prefetch_limit = forward_prefetch_limit
|
||||
|
||||
# Data structures for execution order validation
|
||||
self._checking_order: bool = debug_level == dist.DebugLevel.DETAIL
|
||||
self.process_group: dist.ProcessGroup | None = None
|
||||
self.world_size: int | None = None
|
||||
self.all_handles: list[FlatParamHandle] = []
|
||||
# Names are prefixed from the root module
|
||||
self.param_to_fqn: dict[nn.Parameter, list[str]] = {}
|
||||
# Current index in the pre-forward execution order
|
||||
self.current_order_index = 0
|
||||
self.warn_status = _ExecOrderWarnStatus.NONE
|
||||
|
||||
def init(
|
||||
self,
|
||||
state: _FSDPState,
|
||||
root_module: nn.Module,
|
||||
process_group: dist.ProcessGroup,
|
||||
) -> None:
|
||||
"""
|
||||
Initializes the data structures needed for checking the forward order.
|
||||
This should be called after a root FSDP instance has been set during
|
||||
lazy initialization.
|
||||
"""
|
||||
self.process_group = process_group
|
||||
self.rank = process_group.rank()
|
||||
self.world_size = process_group.size()
|
||||
# Fix an order over the handles, which should be the same across ranks
|
||||
for handle in traversal_utils._get_fsdp_handles(root_module):
|
||||
index = len(self.all_handles)
|
||||
self.all_handles.append(handle)
|
||||
handle._handle_index = index
|
||||
self.param_to_fqn = _get_param_to_fqns(root_module)
|
||||
# TODO (awgu): We can broadcast the metadata of rank 0's `all_handles`
|
||||
# to check that all ranks have the same handles in the same order.
|
||||
# https://github.com/pytorch/pytorch/issues/79620
|
||||
|
||||
@property
|
||||
def is_first_iter(self) -> bool:
|
||||
return self._iter == 0
|
||||
|
||||
def get_handle_to_backward_prefetch(
|
||||
self,
|
||||
current_handle: FlatParamHandle,
|
||||
) -> FlatParamHandle | None:
|
||||
"""
|
||||
Returns a :class:`list` of the handles keys of the handles to backward
|
||||
prefetch given the current handles key. If there are no valid handles
|
||||
keys to prefetch, then this returns an empty :class:`list`.
|
||||
"""
|
||||
current_index = current_handle._post_forward_index
|
||||
if current_index is None:
|
||||
return None
|
||||
target_index = current_index - 1
|
||||
target_handle: FlatParamHandle | None = None
|
||||
for _ in range(self._backward_prefetch_limit):
|
||||
if target_index < 0:
|
||||
break
|
||||
target_handle = self.handles_post_forward_order[target_index]
|
||||
target_index -= 1
|
||||
return target_handle
|
||||
|
||||
def get_handle_to_forward_prefetch(
|
||||
self,
|
||||
current_handle: FlatParamHandle,
|
||||
) -> FlatParamHandle | None:
|
||||
"""
|
||||
Returns a :class:`list` of the handles keys of the handles to forward
|
||||
prefetch given the current handles key. If there are no valid handles
|
||||
keys to prefetch, then this returns an empty :class:`list`.
|
||||
"""
|
||||
current_index = current_handle._pre_forward_order_index
|
||||
if current_index is None:
|
||||
return None
|
||||
target_index = current_index + 1
|
||||
target_handle: FlatParamHandle | None = None
|
||||
for _ in range(self._forward_prefetch_limit):
|
||||
if target_index >= len(self.handles_pre_forward_order):
|
||||
break
|
||||
target_handle = self.handles_pre_forward_order[target_index]
|
||||
target_index += 1
|
||||
return target_handle
|
||||
|
||||
def record_post_forward(self, handle: FlatParamHandle | None) -> None:
|
||||
"""
|
||||
Records ``handles`` in the post-forward order, where ``handles`` should
|
||||
be a group of handles used in the same module's forward. If ``handles``
|
||||
is empty, then it is omitted.
|
||||
|
||||
Unlike :meth:`record_pre_forward`, this records the order *every*
|
||||
iteration with the expectation that the recorded order is reset in
|
||||
:meth:`next_iter`.
|
||||
"""
|
||||
if not handle:
|
||||
return
|
||||
# Only record the first usage of a handles key
|
||||
if handle._post_forward_index:
|
||||
self.handles_post_forward_order.append(handle)
|
||||
return
|
||||
index = len(self.handles_post_forward_order)
|
||||
handle._post_forward_index = index
|
||||
self.handles_post_forward_order.append(handle)
|
||||
|
||||
def record_pre_forward(
|
||||
self, handle: FlatParamHandle | None, is_training: bool
|
||||
) -> None:
|
||||
"""
|
||||
Records ``handles`` in the pre-forward order, where ``handles`` should
|
||||
be a group of handles used in the same module's forward. If ``handles``
|
||||
is empty, then it is omitted.
|
||||
|
||||
On the first iteration, this checks the execution order across ranks.
|
||||
See :meth:`_check_order` for details.
|
||||
"""
|
||||
if not handle:
|
||||
return
|
||||
self._check_order(handle, is_training)
|
||||
# Fix the order after the first iteration and only record the first
|
||||
# usage of a handles key
|
||||
if not self.is_first_iter or handle._pre_forward_order_index is not None:
|
||||
return
|
||||
index = len(self.handles_pre_forward_order)
|
||||
handle._pre_forward_order_index = index
|
||||
self.handles_pre_forward_order.append(handle)
|
||||
|
||||
def _check_order(self, handle: FlatParamHandle, is_training: bool) -> None:
|
||||
"""
|
||||
Checks the forward execution order as long as ``is_training`` is
|
||||
``True`` since checking in eval mode is not supported. This only checks
|
||||
if the distributed debug level is DETAIL.
|
||||
|
||||
- On the first iteration, this uses all-gathers to check that all ranks
|
||||
are all-gathering the same handles and hence ``FlatParameter`` s,
|
||||
raising an error if not.
|
||||
- On subsequent iterations, this checks that each rank is locally
|
||||
consistent with its own forward order from the first iteration, issuing
|
||||
a warning if not. This issues a warning on the first deviating
|
||||
iteration and stops warning thereafter.
|
||||
"""
|
||||
# Do not check order in eval mode since the post-backward callback does
|
||||
# not run so it cannot be used to mark the end of an iteration
|
||||
if not is_training or not self._checking_order:
|
||||
return
|
||||
if self.is_first_iter:
|
||||
msg_prefix = "Forward order differs across ranks:"
|
||||
optional_local_indices: tuple[int | None, ...] = self._get_handle_indices(
|
||||
handle
|
||||
)
|
||||
device = handle.device # guaranteed to be non-CPU
|
||||
num_valid_indices = sum(
|
||||
(index is not None) for index in optional_local_indices
|
||||
)
|
||||
tensor_kwargs: dict[str, torch.dtype | torch.device] = {
|
||||
"dtype": torch.int32,
|
||||
"device": device,
|
||||
}
|
||||
world_num_valid_indices = torch.zeros(self.world_size, **tensor_kwargs) # type: ignore[arg-type, call-overload]
|
||||
local_num_valid_indices = torch.tensor([num_valid_indices], **tensor_kwargs) # type: ignore[arg-type, call-overload]
|
||||
dist.all_gather_into_tensor(
|
||||
world_num_valid_indices,
|
||||
local_num_valid_indices,
|
||||
group=self.process_group,
|
||||
)
|
||||
# Copy entire tensor from D2H once to avoid per element D2H copies
|
||||
world_num_valid_indices = world_num_valid_indices.cpu()
|
||||
# Check that all ranks plan to all-gather the same number of
|
||||
# parameters
|
||||
# TODO (awgu): Since every module has at most one handle in the
|
||||
# current implementation, this should never raise the error.
|
||||
if self.world_size is None:
|
||||
raise AssertionError("Expected world_size to not be None")
|
||||
if not torch.distributed._functional_collectives.is_torchdynamo_compiling():
|
||||
# TODO(voz): Don't graph break on this - dynamo hates the n1 != n2
|
||||
# tensor comparison control flow.
|
||||
# https://github.com/pytorch/pytorch/issues/107055
|
||||
for (r1, n1), (r2, n2) in itertools.combinations(
|
||||
(
|
||||
(rank, world_num_valid_indices[rank])
|
||||
for rank in range(self.world_size)
|
||||
),
|
||||
2,
|
||||
):
|
||||
if n1 != n2:
|
||||
raise RuntimeError(
|
||||
f"{msg_prefix} rank {r1} is all-gathering {n1} parameters "
|
||||
f"while rank {r2} is all-gathering {n2} parameters"
|
||||
)
|
||||
world_indices = torch.zeros( # type: ignore[call-overload]
|
||||
self.world_size * num_valid_indices, **tensor_kwargs
|
||||
)
|
||||
local_indices = torch.tensor(optional_local_indices, **tensor_kwargs) # type: ignore[arg-type]
|
||||
dist.all_gather_into_tensor(
|
||||
world_indices, local_indices, group=self.process_group
|
||||
)
|
||||
# Copy entire tensor from D2H once to avoid per element D2H copies
|
||||
world_indices = world_indices.cpu()
|
||||
# Check that all ranks plan to all-gather the same index parameters
|
||||
if not torch.distributed._functional_collectives.is_torchdynamo_compiling():
|
||||
# TODO(voz): Don't graph break on this - dynamo hates the i1 != i2
|
||||
# tensor comparison control flow.
|
||||
# https://github.com/pytorch/pytorch/issues/107055
|
||||
for (r1, i1), (r2, i2) in itertools.combinations(
|
||||
(
|
||||
(
|
||||
rank,
|
||||
world_indices[
|
||||
rank * num_valid_indices : (rank + 1)
|
||||
* num_valid_indices
|
||||
],
|
||||
)
|
||||
for rank in range(self.world_size)
|
||||
),
|
||||
2,
|
||||
):
|
||||
if i1 != i2:
|
||||
r1_param_names = self._get_names_from_handle_indices(i1)
|
||||
r2_param_names = self._get_names_from_handle_indices(i2)
|
||||
raise RuntimeError(
|
||||
f"{msg_prefix} rank {r1} is all-gathering parameters "
|
||||
f"for {r1_param_names} while rank {r2} is all-gathering "
|
||||
f"parameters for {r2_param_names}"
|
||||
)
|
||||
else:
|
||||
# Only issue warnings on the first deviating iteration and stop
|
||||
# checking thereafter to avoid flooding the console
|
||||
if self.warn_status == _ExecOrderWarnStatus.WARNED:
|
||||
return
|
||||
msg_prefix = None # non-`None` means we should warn
|
||||
if self.current_order_index >= len(self.handles_pre_forward_order):
|
||||
# This iteration sees extra all-gather(s) compared to the first
|
||||
msg_prefix = (
|
||||
"Expected to not all-gather any more parameters in the "
|
||||
"forward but trying to all-gather parameters for "
|
||||
)
|
||||
else:
|
||||
expected_handle = self.handles_pre_forward_order[
|
||||
self.current_order_index
|
||||
]
|
||||
if expected_handle != handle:
|
||||
expected_param_names = self._get_names_from_handles(expected_handle)
|
||||
msg_prefix = (
|
||||
f"Expected to all-gather for {expected_param_names} "
|
||||
"but trying to all-gather parameters for "
|
||||
)
|
||||
if msg_prefix is not None:
|
||||
param_names = self._get_names_from_handles(handle)
|
||||
msg_suffix = (
|
||||
f"{param_names}"
|
||||
if param_names
|
||||
else "a newly-added parameter since construction time"
|
||||
)
|
||||
warnings.warn(
|
||||
"Forward order differs from that of the first iteration "
|
||||
f"on rank {self.rank}. Collectives are unchecked and may "
|
||||
f"give incorrect results or hang.\n{msg_prefix}{msg_suffix}",
|
||||
stacklevel=2,
|
||||
)
|
||||
self.warn_status = _ExecOrderWarnStatus.WARNING
|
||||
self.current_order_index += 1
|
||||
|
||||
def _get_handle_indices(
|
||||
self,
|
||||
handle: FlatParamHandle,
|
||||
) -> tuple[int | None, ...]:
|
||||
"""
|
||||
Returns the handle indices (i.e. indices into ``self.all_handles``)
|
||||
corresponding to the handles in ``handle``. An entry in the
|
||||
returned tuple is ``None`` if the handle is invalid.
|
||||
"""
|
||||
indices: list[int | None] = []
|
||||
if handle:
|
||||
indices.append(handle._handle_index)
|
||||
return tuple(indices)
|
||||
|
||||
def _get_names_from_handle_indices(
|
||||
self,
|
||||
handle_indices: tuple[int, ...],
|
||||
) -> list[list[str]]:
|
||||
"""
|
||||
Returns a list of FQNs for each handle in ``handle_indices``. If a
|
||||
handle index is invalid, then its FQNs are omitted from the returned
|
||||
list.
|
||||
"""
|
||||
fqns: list[list[str]] = []
|
||||
for index in handle_indices:
|
||||
if index is None or index < 0 or index >= len(self.all_handles):
|
||||
continue
|
||||
handle = self.all_handles[index]
|
||||
flat_param = handle.flat_param
|
||||
fqns.append(self.param_to_fqn[flat_param])
|
||||
return fqns
|
||||
|
||||
def _get_names_from_handles(
|
||||
self,
|
||||
handle: FlatParamHandle,
|
||||
) -> list[list[str]]:
|
||||
"""
|
||||
Returns a list of FQNs for each handle in ``handles_key``. If a handle
|
||||
is invalid, then its FQNs are omitted from the returned list.
|
||||
"""
|
||||
fqns: list[list[str]] = []
|
||||
if handle:
|
||||
flat_param = handle.flat_param
|
||||
if flat_param in self.param_to_fqn:
|
||||
fqns.append(self.param_to_fqn[flat_param])
|
||||
return fqns
|
||||
|
||||
def next_iter(self):
|
||||
"""
|
||||
Advances the internal data structures per iteration. This should be
|
||||
called in the post-backward callback since that marks the true end of
|
||||
an iteration.
|
||||
"""
|
||||
self._iter += 1
|
||||
self.handles_post_forward_order.clear()
|
||||
if self._checking_order:
|
||||
self.current_order_index = 0
|
||||
if self.warn_status == _ExecOrderWarnStatus.WARNING:
|
||||
self.warn_status = _ExecOrderWarnStatus.WARNED
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch.distributed._shard.sharded_tensor.api import ShardedTensor
|
||||
from torch.distributed._shard.sharded_tensor.shard import Shard
|
||||
from torch.distributed.fsdp._shard_utils import (
|
||||
_all_gather_dtensor,
|
||||
_create_chunk_dtensor,
|
||||
_create_chunk_sharded_tensor,
|
||||
)
|
||||
from torch.distributed.tensor import DeviceMesh, DTensor
|
||||
|
||||
|
||||
class FSDPExtensions(ABC):
|
||||
"""
|
||||
This enables some customizable hooks to enable composability with tensor
|
||||
parallelism. To activate these hooks, use :func:`_set_fsdp_extensions` to
|
||||
set a custom :class:`FSDPExtensions` that implements the hooks.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def pre_flatten_transform(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, Any | None]:
|
||||
"""E.g. converting ``DistributedTensor`` to local tensor."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def post_unflatten_transform(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
param_extension: Any,
|
||||
) -> torch.Tensor:
|
||||
"""E.g. converting local tensor to ``DistributedTensor``."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def chunk_tensor(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
world_size: int,
|
||||
num_devices_per_node: int,
|
||||
pg: dist.ProcessGroup,
|
||||
device: torch.device | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Shards a tensor to chunks and returns the local chunk."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def chunk_dtensor(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
device_mesh: DeviceMesh,
|
||||
) -> torch.Tensor:
|
||||
"""Shards a tensor/DTensor to DTensor and returns the local DTensor."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def pre_load_state_dict_transform(
|
||||
self,
|
||||
tensor: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, list[Shard]]:
|
||||
"""
|
||||
This is to be called before loading a *sharded* model state dict and
|
||||
should return the tensor and list of shards from which to load data.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def all_gather_dtensor(
|
||||
self,
|
||||
tensor: DTensor,
|
||||
parent_mesh: DeviceMesh | None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
This is to be called before loading a *sharded* DTensor state dict.
|
||||
This gathers tensor in FSDP dimension and returns local tensor of
|
||||
TP DTensor.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
_extensions: FSDPExtensions | None = None
|
||||
|
||||
|
||||
def _set_fsdp_extensions(flattener: FSDPExtensions) -> None:
|
||||
global _extensions
|
||||
_extensions = flattener
|
||||
|
||||
|
||||
def _ext_pre_flatten_transform(
|
||||
tensor: torch.Tensor,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> tuple[torch.Tensor, Any | None]:
|
||||
if fsdp_extension is not None:
|
||||
new_tensor, param_extension = fsdp_extension.pre_flatten_transform(tensor)
|
||||
if param_extension is not None:
|
||||
return new_tensor, param_extension
|
||||
return tensor, None
|
||||
|
||||
|
||||
def _ext_post_unflatten_transform(
|
||||
tensor: torch.Tensor,
|
||||
param_extension: Any,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> torch.Tensor:
|
||||
if fsdp_extension is not None and param_extension is not None:
|
||||
return fsdp_extension.post_unflatten_transform(tensor, param_extension)
|
||||
return tensor
|
||||
|
||||
|
||||
def _ext_chunk_tensor(
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
world_size: int,
|
||||
num_devices_per_node: int,
|
||||
pg: dist.ProcessGroup,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> torch.Tensor:
|
||||
chunk_tensor_fn = (
|
||||
fsdp_extension.chunk_tensor
|
||||
if fsdp_extension is not None
|
||||
else _create_chunk_sharded_tensor
|
||||
)
|
||||
return chunk_tensor_fn(
|
||||
tensor,
|
||||
rank,
|
||||
world_size,
|
||||
num_devices_per_node,
|
||||
pg,
|
||||
)
|
||||
|
||||
|
||||
def _ext_chunk_dtensor(
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
device_mesh: DeviceMesh,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> torch.Tensor:
|
||||
chunk_dtensor_fn = (
|
||||
fsdp_extension.chunk_dtensor
|
||||
if fsdp_extension is not None
|
||||
else _create_chunk_dtensor
|
||||
)
|
||||
return chunk_dtensor_fn(
|
||||
tensor,
|
||||
rank,
|
||||
device_mesh,
|
||||
)
|
||||
|
||||
|
||||
def _ext_pre_load_state_dict_transform(
|
||||
tensor: torch.Tensor,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> tuple[torch.Tensor, list[Shard]]:
|
||||
if fsdp_extension is not None:
|
||||
return fsdp_extension.pre_load_state_dict_transform(tensor)
|
||||
|
||||
if type(tensor) is not ShardedTensor:
|
||||
raise AssertionError(f"Expected ShardedTensor, got {type(tensor)}")
|
||||
shards = tensor.local_shards()
|
||||
return (tensor, shards)
|
||||
|
||||
|
||||
def _ext_all_gather_dtensor(
|
||||
tensor: DTensor,
|
||||
parent_mesh: DeviceMesh | None,
|
||||
fsdp_extension: FSDPExtensions | None = None,
|
||||
) -> torch.Tensor:
|
||||
all_gather_dtensor_fn = (
|
||||
fsdp_extension.all_gather_dtensor
|
||||
if fsdp_extension is not None
|
||||
else _all_gather_dtensor
|
||||
)
|
||||
return all_gather_dtensor_fn(tensor, parent_mesh)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
from ._fsdp_api import (
|
||||
CPUOffloadPolicy,
|
||||
DataParallelMeshDims,
|
||||
MixedPrecisionPolicy,
|
||||
OffloadPolicy,
|
||||
)
|
||||
from ._fully_shard import (
|
||||
FSDPModule,
|
||||
fully_shard,
|
||||
register_fsdp_forward_method,
|
||||
share_comm_ctx,
|
||||
UnshardHandle,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CPUOffloadPolicy",
|
||||
"DataParallelMeshDims",
|
||||
"FSDPModule",
|
||||
"fully_shard",
|
||||
"MixedPrecisionPolicy",
|
||||
"OffloadPolicy",
|
||||
"register_fsdp_forward_method",
|
||||
"UnshardHandle",
|
||||
"share_comm_ctx",
|
||||
]
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
|
||||
|
||||
_ReduceOp = dist.ReduceOp | dist.ReduceOp.RedOpType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MixedPrecisionPolicy:
|
||||
"""
|
||||
This configures FSDP's mixed precision. Unlike autocast, this applies mixed
|
||||
precision at the module level, not op level, which means low-precision
|
||||
activations are saved for backward and high-to-low-precision casts are
|
||||
incurred only at module boundaries.
|
||||
|
||||
FSDP works well with module-level mixed precision since it keeps the
|
||||
high-precision sharded parameters in memory anyway. In other words, FSDP
|
||||
does not require any extra memory to keep a high-precision copy of the
|
||||
parameters for the optimizer step.
|
||||
|
||||
Attributes:
|
||||
param_dtype (Optional[torch.dtype]): This specifies the dtype for
|
||||
the unsharded parameter and hence the dtype for forward/backward
|
||||
computation and the parameter all-gather. If this is ``None``, then
|
||||
the unsharded parameter uses the original dtype. The optimizer step
|
||||
uses the sharded parameter in the original dtype. (Default:
|
||||
``None``)
|
||||
reduce_dtype (Optional[torch.dtype]): This specifies the dtype for
|
||||
gradient reduction (i.e. reduce-scatter or all-reduce). If this is
|
||||
``None`` but ``param_dtype`` is not ``None``, then the reduction
|
||||
uses the compute dtype. This can be used to run gradient reduction
|
||||
in full precision while using low precision for compute. If also
|
||||
gradient reduction is disabled via :meth:`set_requires_gradient_sync`,
|
||||
then FSDP will accumulate gradients using ``reduce_dtype``.
|
||||
(Default: ``None``)
|
||||
output_dtype (Optional[torch.dtype]): This specifies the dtype for
|
||||
casting floating-point forward outputs. This can be used to
|
||||
help implement cases where different modules have different mixed
|
||||
precision policies. (Default: ``None``)
|
||||
cast_forward_inputs (bool): This specifies whether FSDP should cast the
|
||||
forward's floating-point input tensors to ``param_dtype`` or not.
|
||||
"""
|
||||
|
||||
param_dtype: torch.dtype | None = None
|
||||
reduce_dtype: torch.dtype | None = None
|
||||
output_dtype: torch.dtype | None = None
|
||||
cast_forward_inputs: bool = True
|
||||
|
||||
|
||||
class Comm(ABC):
|
||||
"""
|
||||
Interface for communication primitives.
|
||||
A primitive primarily needs to handle 3 tasks, namely:
|
||||
|
||||
1. How to allocate memory for communication
|
||||
Depending on the goal, an implementation can choose to:
|
||||
a. associate each call to a temporary buffer
|
||||
(best for flexibility and simplicity)
|
||||
b. reuse an persistent buffer for efficiency reasons
|
||||
|
||||
2. Where to allocate memory
|
||||
(e.g. NCCL mem pool or regular cuda caching allocator)
|
||||
|
||||
3. What to do/call upon the comm is called
|
||||
(see `AllGather` interface as an example)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def allocate(
|
||||
self,
|
||||
size: Sequence[int | torch.SymInt],
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
This handles the "how to allocate memory" part.
|
||||
|
||||
A default implementation could be simply:
|
||||
|
||||
.. code-block:: python
|
||||
with self.mem_pool:
|
||||
torch.empty(...)
|
||||
|
||||
Args:
|
||||
size (Sequence[Union[int, torch.SymInt]]): size of the tensor buffer
|
||||
dtype (torch.dtype): dtype of the tensor buffer
|
||||
device (torch.device): which device to allocate the tensor onto
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AllGather(Comm):
|
||||
"""
|
||||
Interface for all_gather comm primitive
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None: ...
|
||||
|
||||
|
||||
class ReduceScatter(Comm):
|
||||
"""
|
||||
Interface for reduce_scatter comm primitive
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
op: _ReduceOp,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataParallelMeshDims:
|
||||
"""
|
||||
Specifies which dimensions of a full SPMD :class:`DeviceMesh` correspond to
|
||||
data parallelism when using :func:`fully_shard` whose parameters are already
|
||||
DTensors on that mesh.
|
||||
|
||||
Attributes:
|
||||
shard (Optional[Union[str, tuple[str, ...]]]): Mesh dimension name(s)
|
||||
that FSDP shards parameters on. If a tuple of names, those dims
|
||||
are flattened into a single shard dimension. At least one of
|
||||
``shard`` and ``replicate`` must be set.
|
||||
replicate (Optional[Union[str, tuple[str, ...]]]): Mesh dimension
|
||||
name(s) for HSDP or DDP replication. If a tuple of names, those
|
||||
dims are flattened into a single replicate dimension.
|
||||
"""
|
||||
|
||||
shard: str | tuple[str, ...] | None = None
|
||||
replicate: str | tuple[str, ...] | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.shard is None and self.replicate is None:
|
||||
raise ValueError(
|
||||
"At least one of shard or replicate must be set in DataParallelMeshDims"
|
||||
)
|
||||
|
||||
@property
|
||||
def shard_names(self) -> tuple[str, ...]:
|
||||
if self.shard is None:
|
||||
return ()
|
||||
if isinstance(self.shard, str):
|
||||
return (self.shard,)
|
||||
return tuple(self.shard)
|
||||
|
||||
@property
|
||||
def replicate_names(self) -> tuple[str, ...]:
|
||||
if self.replicate is None:
|
||||
return ()
|
||||
if isinstance(self.replicate, str):
|
||||
return (self.replicate,)
|
||||
return tuple(self.replicate)
|
||||
|
||||
|
||||
@dataclass
|
||||
class OffloadPolicy:
|
||||
"""
|
||||
This base class represents the policy of no offloading and is only used as
|
||||
the default value for the ``offload_policy`` arg.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CPUOffloadPolicy(OffloadPolicy):
|
||||
"""
|
||||
This offload policy offloads parameters, gradients, and optimizer states to
|
||||
CPU. Sharded parameters are copied host-to-device before all-gather. The
|
||||
all-gathered parameters are freed according to ``reshard_after_forward``.
|
||||
Sharded gradients are copied device-to-host in backward, and the optimizer
|
||||
step runs on CPU with CPU optimizer states.
|
||||
|
||||
Attributes:
|
||||
pin_memory (bool): Whether to pin sharded parameter and gradient
|
||||
memory. Pinning memory allows both more efficient H2D/D2H copies
|
||||
and for the copies to overlap with compute. However, the pinned
|
||||
memory cannot be used by other processes. Set this to ``False`` if
|
||||
you have insufficient CPU memory. (Default: ``True``)
|
||||
"""
|
||||
|
||||
pin_memory: bool = True
|
||||
+846
@@ -0,0 +1,846 @@
|
||||
import math
|
||||
from collections.abc import Callable, Sequence
|
||||
from itertools import chain
|
||||
from typing import Any, cast, Literal, NamedTuple
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed._symmetric_memory as symm_mem
|
||||
from torch.distributed.device_mesh import _get_device_handle
|
||||
from torch.distributed.distributed_c10d import ReduceOp
|
||||
from torch.distributed.fsdp._fully_shard._fsdp_api import AllGather, ReduceScatter
|
||||
from torch.distributed.tensor import DTensor
|
||||
|
||||
from ._fsdp_api import _ReduceOp
|
||||
from ._fsdp_common import (
|
||||
_get_dim0_padded_size,
|
||||
_raise_assert_with_print,
|
||||
_to_dtype_if_needed,
|
||||
)
|
||||
from ._fsdp_param import FSDPParam, ShardedState
|
||||
|
||||
|
||||
class AllGatherResult(NamedTuple):
|
||||
all_gather_output: torch.Tensor
|
||||
all_gather_event: torch.Event | None
|
||||
all_gather_work: dist.distributed_c10d.Work | None
|
||||
# For each parameter, the all-gather input dtype for each input
|
||||
param_all_gather_input_dtypes: list[list[torch.dtype]]
|
||||
# For each parameter, the all-gather input numel for each input
|
||||
param_all_gather_input_numels: list[list[int]]
|
||||
# 1D flattened version of `param_all_gather_input_numels` saved to avoid
|
||||
# CPU overhead from recomputing
|
||||
all_gather_input_split_sizes: list[int]
|
||||
|
||||
|
||||
lib = torch.library.Library("fsdp", "FRAGMENT") # noqa: TOR901
|
||||
|
||||
lib.define(
|
||||
"""
|
||||
all_gather_copy_in(
|
||||
Tensor[] all_gather_inputs,
|
||||
Tensor all_gather_output,
|
||||
SymInt[] inp_split_sizes,
|
||||
SymInt all_gather_input_numel,
|
||||
SymInt rank
|
||||
) -> (Tensor, Tensor)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class DefaultAllocMixin:
|
||||
def allocate(
|
||||
self,
|
||||
size: Sequence[int | torch.SymInt],
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
return torch.empty(*size, dtype=dtype, device=device)
|
||||
|
||||
|
||||
class ProcessGroupAllocMixin:
|
||||
def __init__(self, group: dist.ProcessGroup, *args: Any, **kwargs: Any):
|
||||
self._group = group
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def allocate(
|
||||
self,
|
||||
size: Sequence[int | torch.SymInt],
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
backend = self._group._get_backend(device)
|
||||
if backend.supports_tensor_alloc(device):
|
||||
size_1d = math.prod(int(s) for s in size)
|
||||
return backend.allocate_tensor(size_1d, dtype=dtype, device=device)
|
||||
return torch.empty(*size, dtype=dtype, device=device)
|
||||
|
||||
|
||||
class SymmMemAllocMixin:
|
||||
def __init__(
|
||||
self,
|
||||
group: dist.ProcessGroup,
|
||||
backend: Literal["NCCL"] = "NCCL",
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
):
|
||||
self._group = group
|
||||
symm_mem.set_backend(backend)
|
||||
# Force initialization of communicator; otherwise, the rendezvous may
|
||||
# see empty communicator.
|
||||
# TODO: Remove this, maybe by warning user to perform eager dist init.
|
||||
# For now, it is okay since it isjust a one-time cost at init.
|
||||
dist.barrier(group=group)
|
||||
|
||||
def allocate(
|
||||
self,
|
||||
size: Sequence[int | torch.SymInt],
|
||||
*,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
# Leverage MemPool to reuse the symmetric buffer, avoiding allocation
|
||||
# and rendezvous overhead
|
||||
mempool = symm_mem.get_mem_pool(device)
|
||||
with torch.cuda.use_mem_pool(mempool):
|
||||
return torch.empty(size, dtype=dtype, device=device)
|
||||
|
||||
|
||||
class DefaultAllGather(DefaultAllocMixin, AllGather):
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None:
|
||||
return dist.all_gather_into_tensor(
|
||||
output_tensor,
|
||||
input_tensor,
|
||||
group=group,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
class ProcessGroupAllocAllGather(ProcessGroupAllocMixin, AllGather):
|
||||
def __init__(self, group: dist.ProcessGroup) -> None:
|
||||
super().__init__(group)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None:
|
||||
return dist.all_gather_into_tensor(
|
||||
output_tensor,
|
||||
input_tensor,
|
||||
group=group,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
class SymmMemAllGather(SymmMemAllocMixin, AllGather):
|
||||
def __init__(
|
||||
self,
|
||||
group: dist.ProcessGroup,
|
||||
backend: Literal["NCCL"] = "NCCL",
|
||||
) -> None:
|
||||
super().__init__(group, backend)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None:
|
||||
# We are doing inplace all-gather, so we need to rendezvous the output tensor only
|
||||
symm_mem.rendezvous(output_tensor, group=group.group_name)
|
||||
# Calling regular all-gather would already cause libraries like NCCL to
|
||||
# use its optimized all-gather implementation for symmetric memory:
|
||||
# - Copy Engine All-Gather (when zero-CTA policy is enabled)
|
||||
# - Symmetric Kernel All-Gather (when zero-CTA policy is not enabled)
|
||||
return dist.all_gather_into_tensor(
|
||||
output_tensor,
|
||||
input_tensor,
|
||||
group=group,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
class DefaultReduceScatter(DefaultAllocMixin, ReduceScatter):
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
op: _ReduceOp,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work:
|
||||
return dist.reduce_scatter_tensor(
|
||||
output=output_tensor,
|
||||
input=input_tensor,
|
||||
group=group,
|
||||
op=op,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
class ProcessGroupAllocReduceScatter(ProcessGroupAllocMixin, ReduceScatter):
|
||||
def __init__(self, group: dist.ProcessGroup) -> None:
|
||||
super().__init__(group)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
op: _ReduceOp,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work:
|
||||
return dist.reduce_scatter_tensor(
|
||||
output=output_tensor,
|
||||
input=input_tensor,
|
||||
group=group,
|
||||
op=op,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
class SymmMemReduceScatter(SymmMemAllocMixin, ReduceScatter):
|
||||
def __init__(
|
||||
self,
|
||||
group: dist.ProcessGroup,
|
||||
backend: Literal["NCCL"] = "NCCL",
|
||||
) -> None:
|
||||
super().__init__(group, backend)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
output_tensor: torch.Tensor,
|
||||
input_tensor: torch.Tensor,
|
||||
group: dist.ProcessGroup,
|
||||
op: _ReduceOp,
|
||||
async_op: bool = False,
|
||||
) -> dist.Work | None:
|
||||
symm_mem.rendezvous(input_tensor, group=group.group_name)
|
||||
symm_mem.rendezvous(output_tensor, group=group.group_name)
|
||||
# Calling regular reduce-scatter would already cause libraries like NCCL to
|
||||
# use its optimized reduce-scatter implementation for symmetric memory
|
||||
return dist.reduce_scatter_tensor(
|
||||
output=output_tensor,
|
||||
input=input_tensor,
|
||||
group=group,
|
||||
op=op,
|
||||
async_op=async_op,
|
||||
)
|
||||
|
||||
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "Meta")
|
||||
def all_gather_copy_in_meta(
|
||||
all_gather_inputs: list[torch.Tensor],
|
||||
all_gather_output: torch.Tensor,
|
||||
inp_split_sizes: list[int],
|
||||
all_gather_input_numel: int,
|
||||
rank: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
all_gather_input = all_gather_output.narrow(
|
||||
0, all_gather_input_numel * rank, all_gather_input_numel
|
||||
)
|
||||
return all_gather_input, all_gather_output
|
||||
|
||||
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "CUDA")
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "XPU")
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "HPU")
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "CPU")
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "MTIA")
|
||||
@torch.library.impl(lib, "all_gather_copy_in", "PrivateUse1")
|
||||
def all_gather_copy_in_cuda(
|
||||
all_gather_inputs: list[torch.Tensor],
|
||||
all_gather_output: torch.Tensor,
|
||||
inp_split_sizes: list[int],
|
||||
all_gather_input_numel: int,
|
||||
rank: int,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
all_gather_input = all_gather_output.narrow(
|
||||
0, all_gather_input_numel * rank, all_gather_input_numel
|
||||
)
|
||||
foreach_copy_dsts = torch.split(all_gather_input, inp_split_sizes)
|
||||
with torch.no_grad():
|
||||
torch._foreach_copy_(foreach_copy_dsts, all_gather_inputs)
|
||||
return all_gather_input, all_gather_output
|
||||
|
||||
|
||||
lib.define(
|
||||
"split_with_sizes_copy(Tensor all_gather_output, SymInt[] all_gather_input_split_sizes, int dim=0, *, Tensor(a!)[] out) -> ()"
|
||||
)
|
||||
|
||||
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "Meta")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "CUDA")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "XPU")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "HPU")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "CPU")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "MTIA")
|
||||
@torch.library.impl(lib, "split_with_sizes_copy", "PrivateUse1")
|
||||
def split_with_sizes_copy(
|
||||
all_gather_output: torch.Tensor,
|
||||
all_gather_input_split_sizes: list[int],
|
||||
dim: int = 0,
|
||||
*,
|
||||
out: list[torch.Tensor],
|
||||
) -> None:
|
||||
torch.split_with_sizes_copy(
|
||||
all_gather_output, all_gather_input_split_sizes, dim=dim, out=out
|
||||
)
|
||||
|
||||
|
||||
lib.define(
|
||||
"chunk_cat(Tensor[] tensors, int dim, int num_chunks, *, Tensor(a!) out) -> ()"
|
||||
)
|
||||
|
||||
|
||||
@torch.library.impl(lib, "chunk_cat", "Meta")
|
||||
@torch.library.impl(lib, "chunk_cat", "CUDA")
|
||||
@torch.library.impl(lib, "chunk_cat", "XPU")
|
||||
@torch.library.impl(lib, "chunk_cat", "HPU")
|
||||
@torch.library.impl(lib, "chunk_cat", "CPU")
|
||||
@torch.library.impl(lib, "chunk_cat", "MTIA")
|
||||
@torch.library.impl(lib, "chunk_cat", "PrivateUse1")
|
||||
def chunk_cat(
|
||||
tensors: list[torch.Tensor],
|
||||
dim: int,
|
||||
num_chunks: int,
|
||||
out: torch.Tensor,
|
||||
) -> None:
|
||||
torch._chunk_cat(tensors, dim, num_chunks, out=out)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def foreach_all_gather(
|
||||
fsdp_params: list[FSDPParam],
|
||||
group: dist.ProcessGroup,
|
||||
async_op: bool,
|
||||
all_gather_copy_in_stream: torch.Stream,
|
||||
all_gather_stream: torch.Stream,
|
||||
device: torch.device,
|
||||
all_gather_comm: AllGather,
|
||||
) -> AllGatherResult | None:
|
||||
world_size, rank = group.size(), group.rank()
|
||||
device_handle = _get_device_handle(device.type)
|
||||
with device_handle.stream(all_gather_copy_in_stream):
|
||||
param_all_gather_inputs = _get_param_all_gather_inputs(fsdp_params)
|
||||
(
|
||||
param_all_gather_input_dtypes,
|
||||
param_all_gather_input_numels,
|
||||
dtype,
|
||||
) = _get_all_gather_input_metadatas(param_all_gather_inputs)
|
||||
if dtype == torch.uint8:
|
||||
all_gather_inputs = [
|
||||
t.view(torch.uint8) for ts in param_all_gather_inputs for t in ts
|
||||
]
|
||||
else:
|
||||
all_gather_inputs = [*chain.from_iterable(param_all_gather_inputs)]
|
||||
inp_split_sizes = [t.numel() for t in all_gather_inputs]
|
||||
all_gather_input_numel = sum(inp_split_sizes)
|
||||
all_gather_output = all_gather_comm.allocate(
|
||||
(all_gather_input_numel * world_size,), dtype=dtype, device=device
|
||||
)
|
||||
all_gather_input, all_gather_output = torch.ops.fsdp.all_gather_copy_in(
|
||||
all_gather_inputs,
|
||||
all_gather_output,
|
||||
inp_split_sizes,
|
||||
all_gather_input_numel,
|
||||
rank,
|
||||
)
|
||||
del param_all_gather_inputs
|
||||
all_gather_stream.wait_stream(all_gather_copy_in_stream)
|
||||
with device_handle.stream(all_gather_stream):
|
||||
all_gather_work = all_gather_comm(
|
||||
output_tensor=all_gather_output,
|
||||
input_tensor=all_gather_input,
|
||||
group=group,
|
||||
async_op=async_op,
|
||||
)
|
||||
all_gather_event = all_gather_stream.record_event()
|
||||
return AllGatherResult(
|
||||
all_gather_output,
|
||||
all_gather_event,
|
||||
all_gather_work,
|
||||
param_all_gather_input_dtypes,
|
||||
param_all_gather_input_numels,
|
||||
inp_split_sizes,
|
||||
)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def _get_param_all_gather_inputs(
|
||||
fsdp_params: list[FSDPParam],
|
||||
) -> list[list[torch.Tensor]]:
|
||||
# Intentionally try to run a fast-path that bypasses abstractions for the
|
||||
# common FSDP case of bf16/fp32 mixed precision in order to use foreach
|
||||
# copy for lower CPU overhead and more efficient copying in eager
|
||||
def use_foreach_copy(fsdp_param: FSDPParam) -> bool:
|
||||
return (
|
||||
fsdp_param.param_dtype is not None
|
||||
and not fsdp_param.offload_to_cpu
|
||||
and not hasattr(fsdp_param._sharded_local_tensor, "fsdp_pre_all_gather")
|
||||
)
|
||||
|
||||
param_all_gather_inputs: list[list[torch.Tensor]] = [[] for _ in fsdp_params]
|
||||
foreach_copy_indices: list[int] = []
|
||||
foreach_copy_inputs: list[torch.Tensor] = []
|
||||
foreach_copy_input_numels: list[int] = []
|
||||
|
||||
# 1st pass: for foreach-copy parameters, get inputs and metadata for the
|
||||
# foreach copy, and for the others, actually get their all-gather inputs
|
||||
for i, fsdp_param in enumerate(fsdp_params):
|
||||
if use_foreach_copy(fsdp_param):
|
||||
foreach_copy_indices.append(i)
|
||||
all_gather_input = (
|
||||
fsdp_param._sharded_param_data
|
||||
if fsdp_param.sharded_state == ShardedState.SHARDED
|
||||
else cast(torch.Tensor, fsdp_param._sharded_post_forward_param_data)
|
||||
)
|
||||
foreach_copy_inputs.append(all_gather_input)
|
||||
foreach_copy_input_numels.append(all_gather_input.numel())
|
||||
else:
|
||||
param_all_gather_inputs[i] = fsdp_param.all_gather_inputs
|
||||
|
||||
# 2nd pass: use foreach copy to compute the remaining all-gather inputs
|
||||
if foreach_copy_inputs:
|
||||
fsdp_param_0 = fsdp_params[foreach_copy_indices[0]]
|
||||
param_dtype, device = fsdp_param_0.param_dtype, fsdp_param_0.device
|
||||
flat_foreach_copy_input = torch.empty(
|
||||
(sum(foreach_copy_input_numels),), device=device, dtype=param_dtype
|
||||
)
|
||||
splits = torch.split(flat_foreach_copy_input, foreach_copy_input_numels)
|
||||
torch._foreach_copy_(splits, foreach_copy_inputs)
|
||||
for i, split in zip(foreach_copy_indices, splits):
|
||||
param_all_gather_inputs[i] = [split]
|
||||
|
||||
return param_all_gather_inputs
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def foreach_all_gather_copy_out(
|
||||
all_gather_result: AllGatherResult,
|
||||
fsdp_params: list[FSDPParam],
|
||||
group: dist.ProcessGroup,
|
||||
) -> None:
|
||||
(
|
||||
all_gather_output,
|
||||
all_gather_event,
|
||||
all_gather_work,
|
||||
param_all_gather_input_dtypes,
|
||||
param_all_gather_input_numels,
|
||||
all_gather_input_split_sizes,
|
||||
) = all_gather_result
|
||||
_dtype, device = all_gather_output.dtype, all_gather_output.device
|
||||
device_handle = _get_device_handle(device.type)
|
||||
if all_gather_event is not None: # sync op
|
||||
device_handle.current_stream().wait_event(all_gather_event)
|
||||
if isinstance(all_gather_work, dist.distributed_c10d.Work): # async op
|
||||
all_gather_work.wait()
|
||||
world_size, device = group.size(), all_gather_output.device
|
||||
|
||||
split_with_sizes_out: list[torch.Tensor] = []
|
||||
shard_i_copy_infos: list[tuple[FSDPParam, list[torch.Tensor]]] = []
|
||||
for all_gather_input_numels, all_gather_input_dtypes, fsdp_param in zip(
|
||||
param_all_gather_input_numels, param_all_gather_input_dtypes, fsdp_params
|
||||
):
|
||||
# NOTE: Under compile, make sure we always recreate all_gather_outputs
|
||||
# per AllGather. See [Note: Invariants for torch.compile Traceable FSDP2].
|
||||
fsdp_param.init_all_gather_outputs(
|
||||
all_gather_input_numels,
|
||||
all_gather_input_dtypes,
|
||||
world_size,
|
||||
device,
|
||||
)
|
||||
fsdp_param.alloc_all_gather_outputs()
|
||||
param_all_gather_outputs = fsdp_param.all_gather_outputs
|
||||
if fsdp_param.fsdp_placement.dim != 0:
|
||||
# Copy to a temporary and then chunk-cat into the final all-gather
|
||||
# output tensors
|
||||
param_all_gather_outputs = [
|
||||
torch.empty_like(t) for t in param_all_gather_outputs
|
||||
]
|
||||
shard_i_copy_infos.append((fsdp_param, param_all_gather_outputs))
|
||||
split_with_sizes_out.extend(param_all_gather_outputs)
|
||||
|
||||
all_gather_output = all_gather_output.view(world_size, -1)
|
||||
if all_gather_output.dtype == torch.uint8:
|
||||
out = [t.view(world_size, -1).view(torch.uint8) for t in split_with_sizes_out]
|
||||
else:
|
||||
out = [t.view(world_size, -1) for t in split_with_sizes_out]
|
||||
|
||||
# only avoid VC bump if we are not in inference mode
|
||||
non_inference_outs = [o for o in out if not o.is_inference()]
|
||||
|
||||
if len(non_inference_outs) > 0:
|
||||
with torch.autograd._unsafe_preserve_version_counter(tuple(non_inference_outs)):
|
||||
torch.ops.fsdp.split_with_sizes_copy(
|
||||
all_gather_output, all_gather_input_split_sizes, dim=1, out=out
|
||||
)
|
||||
else:
|
||||
torch.ops.fsdp.split_with_sizes_copy(
|
||||
all_gather_output, all_gather_input_split_sizes, dim=1, out=out
|
||||
)
|
||||
|
||||
for fsdp_param, param_all_gather_outputs in shard_i_copy_infos:
|
||||
# Chunk-cat from the temporary to the final all-gather output tensors
|
||||
shard_dim = fsdp_param.fsdp_placement.dim
|
||||
|
||||
with torch.autograd._unsafe_preserve_version_counter(
|
||||
tuple(fsdp_param.all_gather_outputs)
|
||||
):
|
||||
for param_all_gather_output, target_all_gather_output in zip(
|
||||
param_all_gather_outputs, fsdp_param.all_gather_outputs
|
||||
):
|
||||
padded_sharded_size = (
|
||||
fsdp_param.padded_sharded_param_size
|
||||
if fsdp_param.sharded_state == ShardedState.SHARDED
|
||||
else cast(
|
||||
torch.Tensor, fsdp_param._sharded_post_forward_param_data
|
||||
).size()
|
||||
)
|
||||
pre_param_size = list(padded_sharded_size)
|
||||
pre_param_size[0] *= world_size
|
||||
chunks = torch.chunk(
|
||||
param_all_gather_output.view(pre_param_size), world_size, dim=0
|
||||
)
|
||||
post_param_size = list(padded_sharded_size)
|
||||
post_param_size[shard_dim] *= world_size
|
||||
cat_out = target_all_gather_output.view(post_param_size)
|
||||
torch.cat(chunks, dim=shard_dim, out=cat_out)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def foreach_reduce(
|
||||
fsdp_params: list[FSDPParam],
|
||||
unsharded_grads: list[torch.Tensor],
|
||||
reduce_scatter_group: dist.ProcessGroup,
|
||||
reduce_scatter_stream: torch.Stream,
|
||||
reduce_scatter_comm: ReduceScatter,
|
||||
orig_dtype: torch.dtype | None,
|
||||
reduce_dtype: torch.dtype | None,
|
||||
device: torch.device,
|
||||
gradient_divide_factor: float | None,
|
||||
all_reduce_group: dist.ProcessGroup | None, # not `None` iff HSDP
|
||||
all_reduce_stream: torch.Stream,
|
||||
all_reduce_grads: bool,
|
||||
partial_reduce_output: torch.Tensor | None, # only used for HSDP
|
||||
all_reduce_hook: Callable[[torch.Tensor], None] | None,
|
||||
force_sum_reduction_for_comms: bool = False,
|
||||
) -> tuple[
|
||||
torch.Tensor,
|
||||
torch.Event,
|
||||
torch.Event,
|
||||
torch.Tensor | None,
|
||||
torch.Event | None,
|
||||
torch.Tensor | None,
|
||||
]:
|
||||
"""
|
||||
``unsharded_grads`` owns the references to the gradients computed by
|
||||
autograd, so clearing the list frees the gradients.
|
||||
"""
|
||||
|
||||
grad_dtypes = {grad.dtype for grad in unsharded_grads}
|
||||
if len(grad_dtypes) != 1:
|
||||
# Check this at runtime since it could be a real runtime error if e.g.
|
||||
# fp8 weights do not produce the correct higher precision gradients
|
||||
_raise_assert_with_print(
|
||||
f"FSDP reduce-scatter expects uniform gradient dtype but got {grad_dtypes}"
|
||||
)
|
||||
grad_dtype = unsharded_grads[0].dtype
|
||||
reduce_dtype = reduce_dtype or grad_dtype
|
||||
(predivide_factor, postdivide_factor, reduce_scatter_op, all_reduce_op) = (
|
||||
_get_gradient_divide_factors(
|
||||
reduce_scatter_group,
|
||||
all_reduce_group,
|
||||
reduce_dtype,
|
||||
device.type,
|
||||
gradient_divide_factor,
|
||||
force_sum_reduction_for_comms,
|
||||
)
|
||||
)
|
||||
|
||||
if reduce_scatter_group is None:
|
||||
world_size = 1
|
||||
else:
|
||||
world_size = reduce_scatter_group.size()
|
||||
device_handle = _get_device_handle(device.type)
|
||||
current_stream = device_handle.current_stream()
|
||||
|
||||
if world_size > 1:
|
||||
for i, (fsdp_param, unsharded_grad) in enumerate(
|
||||
zip(fsdp_params, unsharded_grads)
|
||||
):
|
||||
if (shard_dim := fsdp_param.fsdp_placement.dim) == 0:
|
||||
continue
|
||||
if unsharded_grad.size(shard_dim) % world_size != 0:
|
||||
raise AssertionError(
|
||||
f"Shard({shard_dim}) requires even sharding: {unsharded_grad.size()=} {world_size=}"
|
||||
)
|
||||
chunks = torch.chunk(unsharded_grad, world_size, dim=shard_dim)
|
||||
unsharded_grads[i] = torch.cat(chunks, dim=0)
|
||||
|
||||
padded_unsharded_sizes = tuple(
|
||||
_get_dim0_padded_size(grad.size(), world_size) for grad in unsharded_grads
|
||||
)
|
||||
reduce_scatter_input_numel = sum(s.numel() for s in padded_unsharded_sizes)
|
||||
reduce_scatter_output_numel = reduce_scatter_input_numel // world_size
|
||||
reduce_scatter_input = reduce_scatter_comm.allocate(
|
||||
(reduce_scatter_input_numel,),
|
||||
dtype=reduce_dtype,
|
||||
device=device,
|
||||
)
|
||||
|
||||
foreach_reduce_scatter_copy_in(unsharded_grads, reduce_scatter_input, world_size)
|
||||
|
||||
# Only after the copy-in finishes can we free the gradients
|
||||
unsharded_grads.clear()
|
||||
reduce_scatter_stream.wait_stream(current_stream)
|
||||
all_reduce_input = None
|
||||
all_reduce_event = None
|
||||
|
||||
with device_handle.stream(reduce_scatter_stream):
|
||||
reduce_output = reduce_scatter_comm.allocate(
|
||||
(reduce_scatter_output_numel,),
|
||||
dtype=reduce_dtype,
|
||||
device=device,
|
||||
)
|
||||
_div_if_needed(reduce_scatter_input, predivide_factor)
|
||||
if world_size > 1:
|
||||
reduce_scatter_comm(
|
||||
output_tensor=reduce_output,
|
||||
input_tensor=reduce_scatter_input,
|
||||
group=reduce_scatter_group,
|
||||
op=reduce_scatter_op,
|
||||
)
|
||||
else:
|
||||
# For single GPU, just copy the input to output (no actual reduce-scatter needed), and
|
||||
# account for a possible gradient_divide_factor.
|
||||
if gradient_divide_factor is not None:
|
||||
reduce_output.copy_(reduce_scatter_input / gradient_divide_factor)
|
||||
else:
|
||||
reduce_output.copy_(reduce_scatter_input)
|
||||
reduce_scatter_event = reduce_scatter_stream.record_event()
|
||||
post_reduce_stream = reduce_scatter_stream
|
||||
if all_reduce_group is not None: # HSDP or DDP/replicate
|
||||
# Accumulations must run in the reduce-scatter stream
|
||||
if not all_reduce_grads:
|
||||
if partial_reduce_output is not None:
|
||||
partial_reduce_output += reduce_output
|
||||
else:
|
||||
partial_reduce_output = reduce_output
|
||||
return (
|
||||
reduce_scatter_input,
|
||||
reduce_scatter_event,
|
||||
post_reduce_stream.record_event(),
|
||||
all_reduce_input,
|
||||
all_reduce_event,
|
||||
partial_reduce_output,
|
||||
)
|
||||
if partial_reduce_output is not None:
|
||||
reduce_output += partial_reduce_output
|
||||
post_reduce_stream = all_reduce_stream
|
||||
if world_size >= 1:
|
||||
all_reduce_stream.wait_stream(reduce_scatter_stream)
|
||||
else:
|
||||
all_reduce_stream.wait_stream(current_stream)
|
||||
with device_handle.stream(all_reduce_stream):
|
||||
dist.all_reduce(
|
||||
reduce_output,
|
||||
group=all_reduce_group,
|
||||
op=all_reduce_op,
|
||||
)
|
||||
# Keep refs to the reduce-dtype AR buffer + completion
|
||||
# event so FSDPParamGroup._all_reduce_state can hold them
|
||||
# across layers. This keeps the buffer off the caching
|
||||
# allocator's free list; otherwise the next layer's
|
||||
# reduce-scatter can reuse the same physical block while
|
||||
# this layer's AR is still in flight, causing cross-layer
|
||||
# gradient aliasing under slow AR. See PR #140044,
|
||||
# regression test PR #180900.
|
||||
all_reduce_input = reduce_output
|
||||
all_reduce_event = all_reduce_stream.record_event()
|
||||
# -- END: ops in reduce_scatter stream
|
||||
|
||||
if all_reduce_hook is not None:
|
||||
# Execute user-specified all reduce hook.
|
||||
# If native HSDP is used, this is executed after the HSDP all reduce.
|
||||
# If 1-d FSDP is used, this is executed post reduce-scatter.
|
||||
post_reduce_stream = all_reduce_stream
|
||||
all_reduce_stream.wait_stream(reduce_scatter_stream)
|
||||
with device_handle.stream(all_reduce_stream):
|
||||
all_reduce_hook(reduce_output)
|
||||
# -- END: ops post reduce_scatter
|
||||
|
||||
with device_handle.stream(post_reduce_stream):
|
||||
_div_if_needed(reduce_output, postdivide_factor)
|
||||
reduce_output = _to_dtype_if_needed(reduce_output, orig_dtype)
|
||||
# View out and accumulate sharded gradients
|
||||
flat_grad_offset = 0 # [0, reduce_scatter_output_numel - 1]
|
||||
for padded_unsharded_size, fsdp_param in zip(
|
||||
padded_unsharded_sizes, fsdp_params
|
||||
):
|
||||
# Assume even sharding for Shard(i), i > 0; otherwise would require
|
||||
# copy-out for contiguous strides
|
||||
new_sharded_grad = torch.as_strided(
|
||||
reduce_output,
|
||||
size=fsdp_param.sharded_size,
|
||||
stride=fsdp_param.contiguous_sharded_stride,
|
||||
storage_offset=flat_grad_offset,
|
||||
)
|
||||
to_accumulate_grad = fsdp_param.sharded_param.grad is not None
|
||||
if fsdp_param.offload_to_cpu:
|
||||
# Only overlap the D2H copy (copying to pinned memory) if not
|
||||
# accumulating gradients since the CPU add kernel depends on
|
||||
# the copy result and we cannot run the add as a callback
|
||||
non_blocking = fsdp_param.pin_memory and not to_accumulate_grad
|
||||
# Since the GPU sharded gradient is allocated in the RS stream,
|
||||
# we can free it here by not keeping a ref without waiting for
|
||||
# the D2H copy since future RS-stream ops run after the copy
|
||||
new_sharded_grad = new_sharded_grad.to(
|
||||
torch.device("cpu"), non_blocking=non_blocking
|
||||
)
|
||||
if non_blocking:
|
||||
# Record an event on which to block the CPU thread to
|
||||
# ensure that the D2H copy finishes before the optimizer
|
||||
fsdp_param.grad_offload_event = post_reduce_stream.record_event()
|
||||
if to_accumulate_grad:
|
||||
if not isinstance(fsdp_param.sharded_param.grad, DTensor):
|
||||
raise AssertionError(
|
||||
f"Expected fsdp_param.sharded_param.grad to be DTensor, got {type(fsdp_param.sharded_param.grad)}"
|
||||
)
|
||||
fsdp_param.sharded_param.grad._local_tensor += new_sharded_grad
|
||||
else:
|
||||
new_sharded_dtensor_grad = fsdp_param.to_sharded_dtensor(
|
||||
new_sharded_grad
|
||||
)
|
||||
fsdp_param.sharded_param.grad = new_sharded_dtensor_grad
|
||||
for hook in (
|
||||
getattr(fsdp_param.sharded_param, "_post_accumulate_grad_hooks", {})
|
||||
or {}
|
||||
).values():
|
||||
hook(fsdp_param.sharded_param)
|
||||
padded_sharded_numel = padded_unsharded_size.numel() // world_size
|
||||
flat_grad_offset += padded_sharded_numel
|
||||
post_reduce_event = post_reduce_stream.record_event()
|
||||
# The RS output is allocated in the RS stream and used in the default
|
||||
# stream (for optimizer). To ensure its memory is not reused for later
|
||||
# RSs, we do not need extra synchronization since the sharded parameters
|
||||
# hold refs through the end of backward.
|
||||
return (
|
||||
reduce_scatter_input,
|
||||
reduce_scatter_event,
|
||||
post_reduce_event,
|
||||
all_reduce_input,
|
||||
all_reduce_event,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def foreach_reduce_scatter_copy_in(
|
||||
unsharded_grads: list[torch.Tensor],
|
||||
reduce_scatter_input: torch.Tensor,
|
||||
world_size: int,
|
||||
) -> None:
|
||||
reduce_scatter_input = reduce_scatter_input.view(world_size, -1)
|
||||
torch.ops.fsdp.chunk_cat(
|
||||
unsharded_grads, dim=0, num_chunks=world_size, out=reduce_scatter_input
|
||||
)
|
||||
|
||||
|
||||
def _get_all_gather_input_metadatas(
|
||||
param_all_gather_inputs: list[list[torch.Tensor]],
|
||||
) -> tuple[list[list[torch.dtype]], list[list[int]], torch.dtype]:
|
||||
param_all_gather_input_dtypes: list[list[torch.dtype]] = []
|
||||
param_all_gather_input_numels: list[list[int]] = []
|
||||
all_gather_dtype = param_all_gather_inputs[0][0].dtype
|
||||
for all_gather_inputs in param_all_gather_inputs:
|
||||
input_dtypes: list[torch.dtype] = []
|
||||
input_numels: list[int] = []
|
||||
for all_gather_input in all_gather_inputs:
|
||||
if all_gather_input.dtype != all_gather_dtype:
|
||||
all_gather_dtype = torch.uint8
|
||||
input_dtypes.append(all_gather_input.dtype)
|
||||
input_numels.append(all_gather_input.numel())
|
||||
param_all_gather_input_dtypes.append(input_dtypes)
|
||||
param_all_gather_input_numels.append(input_numels)
|
||||
return (
|
||||
param_all_gather_input_dtypes,
|
||||
param_all_gather_input_numels,
|
||||
all_gather_dtype,
|
||||
)
|
||||
|
||||
|
||||
def _get_gradient_divide_factors(
|
||||
reduce_scatter_group: dist.ProcessGroup | None,
|
||||
all_reduce_group: dist.ProcessGroup | None,
|
||||
reduce_dtype: torch.dtype,
|
||||
device_type: str = "",
|
||||
factor: float | None = None,
|
||||
force_sum_reduction_for_comms: bool = False,
|
||||
) -> tuple[
|
||||
float | None,
|
||||
float | None,
|
||||
dist.ReduceOp | dist.ReduceOp.RedOpType,
|
||||
dist.ReduceOp | dist.ReduceOp.RedOpType,
|
||||
]:
|
||||
# MTIA appears to only support SUM reduction, hence we force it implicitly
|
||||
if device_type == "mtia":
|
||||
force_sum_reduction_for_comms = True
|
||||
|
||||
# For fp32/bf16, we do not need to worry about overflow/underflow, so we
|
||||
# use NCCL's built-in division to avoid separate div kernels
|
||||
overflow_risk = reduce_dtype not in (torch.float32, torch.bfloat16)
|
||||
if reduce_scatter_group is not None:
|
||||
data_parallel_size = reduce_scatter_group.size()
|
||||
else:
|
||||
data_parallel_size = 1
|
||||
|
||||
if all_reduce_group is not None:
|
||||
data_parallel_size *= all_reduce_group.size()
|
||||
|
||||
if not overflow_risk and not force_sum_reduction_for_comms:
|
||||
if factor is None:
|
||||
# Warning: NCCL ReduceOp.AVG may produce incorrect results with
|
||||
# world size 1.
|
||||
if data_parallel_size == 1:
|
||||
return None, None, ReduceOp.SUM, ReduceOp.SUM
|
||||
return None, None, ReduceOp.AVG, ReduceOp.AVG
|
||||
if reduce_scatter_group is not None and factor == reduce_scatter_group.size():
|
||||
reduce_scatter_op = ReduceOp.AVG
|
||||
else:
|
||||
reduce_scatter_op = torch.distributed._make_nccl_premul_sum(1 / factor)
|
||||
return None, None, reduce_scatter_op, ReduceOp.SUM
|
||||
|
||||
if factor is None:
|
||||
factor = float(data_parallel_size)
|
||||
pre_factor: float | None
|
||||
if overflow_risk:
|
||||
# Since fp16 has smaller dynamic range than fp32/bf16, we want to avoid
|
||||
# overflow/underflow. For N data parallel workers, each worker computes
|
||||
# g_i, and they collectively reduce (g_1 + ... + g_N) / N. To avoid
|
||||
# overflow/underflow, we divide by ~sqrt(N) before/after the reduction.
|
||||
pre_factor = 1
|
||||
while factor % pre_factor == 0 and factor / pre_factor > pre_factor:
|
||||
pre_factor *= 2
|
||||
post_factor = factor / pre_factor
|
||||
else:
|
||||
# Prefer post-multiplying as it operates on less data and is thus faster
|
||||
pre_factor, post_factor = None, factor
|
||||
|
||||
return pre_factor, post_factor, ReduceOp.SUM, ReduceOp.SUM
|
||||
|
||||
|
||||
def _div_if_needed(tensor: torch.Tensor, div_factor: float | None) -> None:
|
||||
if div_factor is not None and div_factor != 1:
|
||||
tensor.div_(div_factor)
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
import math
|
||||
import traceback
|
||||
from dataclasses import dataclass, field
|
||||
from enum import auto, Enum
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
from torch.distributed._composable.contract import _get_registry
|
||||
from torch.distributed.tensor import DeviceMesh, DTensor, Shard
|
||||
from torch.distributed.tensor._dtensor_spec import DTensorSpec
|
||||
|
||||
from ._fsdp_api import DataParallelMeshDims
|
||||
|
||||
|
||||
def _dynamo_disable(func):
|
||||
"""Disable dynamo tracing for FSDP hooks."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return torch._dynamo.disable(
|
||||
func, recursive=True, reason="skipping FSDP hooks"
|
||||
)(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataParallelMeshInfo:
|
||||
mesh: DeviceMesh
|
||||
shard_mesh_dim: int | None = None
|
||||
replicate_mesh_dim: int | None = None
|
||||
dp_mesh_dims: DataParallelMeshDims | None = None
|
||||
# The full SPMD mesh (excluding PP dims) that params are distributed on.
|
||||
# Must include all non-PP SPMD dims (e.g. DP + TP); passing a submesh
|
||||
# that omits dims like TP will lead to incorrect behavior.
|
||||
spmd_mesh: DeviceMesh | None = field(default=None, repr=False)
|
||||
is_spmd_mesh: bool = field(default=False, init=False, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.shard_mesh_dim is None and self.replicate_mesh_dim is None:
|
||||
raise AssertionError(
|
||||
"At least one of shard_mesh_dim and replicate_mesh_dim must not be None"
|
||||
)
|
||||
self.is_spmd_mesh = self.dp_mesh_dims is not None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FSDPMeshInfo(DataParallelMeshInfo):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.shard_mesh_dim is None:
|
||||
raise AssertionError("Expects non-None shard_mesh_dim")
|
||||
self.shard_mesh_size: int = self.mesh.size(self.shard_mesh_dim)
|
||||
self.shard_process_group = self.mesh.get_group(self.shard_mesh_dim)
|
||||
self.shard_mesh_rank: int = self.shard_process_group.rank()
|
||||
|
||||
|
||||
@dataclass
|
||||
class DDPMeshInfo(DataParallelMeshInfo):
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.replicate_mesh_dim is None:
|
||||
raise AssertionError("Expects non-None replicate_mesh_dim")
|
||||
self.replicate_mesh_size: int = self.mesh.size(self.replicate_mesh_dim)
|
||||
self.replicate_process_group = self.mesh.get_group(self.replicate_mesh_dim)
|
||||
self.replicate_mesh_rank: int = self.replicate_process_group.rank()
|
||||
|
||||
|
||||
@dataclass
|
||||
class HSDPMeshInfo(FSDPMeshInfo, DDPMeshInfo):
|
||||
def __post_init__(self): # pylint:disable=useless-parent-delegation
|
||||
# Calls `FSDPMeshInfo` -> `DDPMeshInfo` -> `DataParallelMeshInfo`
|
||||
super().__post_init__()
|
||||
|
||||
|
||||
class TrainingState(Enum):
|
||||
"""Describes the training state of one FSDP state / parameter group."""
|
||||
|
||||
# Transition to forward starting pre-forward until post-forward
|
||||
FORWARD = auto()
|
||||
# Transition to pre-backward when unsharding in backward
|
||||
PRE_BACKWARD = auto()
|
||||
# Transition to post-backward when resharding and reducing gradients
|
||||
POST_BACKWARD = auto()
|
||||
# Idle before/after forward or before pre-backward/after post-backward
|
||||
IDLE = auto()
|
||||
|
||||
|
||||
def _raise_assert_with_print(*args: Any, **kwargs: Any):
|
||||
print(f"[Rank {dist.get_rank()}] ", end="")
|
||||
print(*args, **kwargs)
|
||||
traceback.print_stack()
|
||||
raise AssertionError(*args, **kwargs)
|
||||
|
||||
|
||||
def _is_composable_with_fsdp(module: nn.Module) -> bool:
|
||||
registry = _get_registry(module)
|
||||
if registry is None:
|
||||
return True
|
||||
# Registry keys by function name
|
||||
return "replicate" not in registry
|
||||
|
||||
|
||||
def _get_dim0_padded_size(tensor_size: torch.Size, dim0_factor: int) -> torch.Size:
|
||||
padded_dim0 = math.ceil(tensor_size[0] / dim0_factor) * dim0_factor
|
||||
return torch.Size([padded_dim0]) + tensor_size[1:]
|
||||
|
||||
|
||||
def _chunk_with_empty(
|
||||
tensor: torch.Tensor, num_chunks: int, dim: int
|
||||
) -> list[torch.Tensor]:
|
||||
chunks = list(torch.chunk(tensor, num_chunks, dim=dim))
|
||||
while len(chunks) < num_chunks:
|
||||
chunks.append(chunks[0].new_empty(0))
|
||||
return chunks
|
||||
|
||||
|
||||
def _get_dim_chunked_size(
|
||||
chunk: torch.Tensor, unchunked_size: torch.Size, dim: int
|
||||
) -> torch.Size:
|
||||
if chunk.numel() > 0:
|
||||
return chunk.size()
|
||||
# For 0 numel, we need to preserve nonzero-sized dims for DTensor APIs
|
||||
# pyrefly: ignore [bad-return]
|
||||
return unchunked_size[:dim] + torch.Size([0]) + unchunked_size[dim + 1 :]
|
||||
|
||||
|
||||
def _from_local_no_grad(
|
||||
local_tensor: torch.Tensor,
|
||||
sharding_spec: DTensorSpec,
|
||||
) -> DTensor:
|
||||
"""
|
||||
This method is similar to ``DTensor.from_local()`` except that in eager mode
|
||||
it avoids some CPU overhead by avoiding default args and not being differentiable.
|
||||
"""
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
return DTensor(
|
||||
# Use the local tensor directly instead of constructing a new tensor
|
||||
# variable, e.g. with `view_as()`, since this is not differentiable
|
||||
# pyrefly: ignore [bad-argument-count]
|
||||
local_tensor,
|
||||
sharding_spec,
|
||||
# pyrefly: ignore [unexpected-keyword]
|
||||
requires_grad=local_tensor.requires_grad,
|
||||
)
|
||||
|
||||
|
||||
def _to_dtype_if_needed(
|
||||
tensor: torch.Tensor, dtype: torch.dtype | None
|
||||
) -> torch.Tensor:
|
||||
if dtype is not None and tensor.dtype != dtype:
|
||||
return tensor.to(dtype)
|
||||
return tensor
|
||||
|
||||
|
||||
def _cast_fp_tensor(dtype: torch.dtype, x: torch.Tensor) -> torch.Tensor:
|
||||
if (
|
||||
not isinstance(x, torch.Tensor)
|
||||
or not torch.is_floating_point(x)
|
||||
or x.dtype == dtype
|
||||
):
|
||||
return x
|
||||
return x.to(dtype)
|
||||
|
||||
|
||||
def is_bw() -> bool:
|
||||
return torch._C._current_graph_task_id() != -1
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShardPlacementResult:
|
||||
placement: Shard | None
|
||||
mesh_info: FSDPMeshInfo
|
||||
|
||||
|
||||
ShardPlacementFnResult = Shard | ShardPlacementResult | None
|
||||
|
||||
|
||||
def resolve_shard_placement(
|
||||
result: ShardPlacementFnResult,
|
||||
default_mesh_info: FSDPMeshInfo,
|
||||
) -> ShardPlacementResult:
|
||||
"""Resolve the shard_placement_fn result to a ShardPlacementResult.
|
||||
|
||||
Handles different input types and applies defaults:
|
||||
- None: Use default sharding (Shard(0)) on default mesh
|
||||
- Shard: Use specified shard dimension on default mesh
|
||||
- ShardPlacementResult: Use as-is
|
||||
|
||||
Args:
|
||||
result: The return value from shard_placement_fn, or None if no fn provided.
|
||||
default_mesh_info: The default FSDPMeshInfo to use if not specified.
|
||||
|
||||
Returns:
|
||||
A ShardPlacementResult with placement and mesh_info.
|
||||
"""
|
||||
if result is None:
|
||||
return ShardPlacementResult(placement=None, mesh_info=default_mesh_info)
|
||||
if isinstance(result, Shard):
|
||||
return ShardPlacementResult(placement=result, mesh_info=default_mesh_info)
|
||||
if isinstance(result, ShardPlacementResult):
|
||||
return result
|
||||
raise ValueError(f"Invalid shard_placement_fn result: {result}")
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
import itertools
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
from torch._logging import warning_once
|
||||
from torch.distributed.device_mesh import _get_device_handle
|
||||
from torch.distributed.tensor import DeviceMesh, DTensor, init_device_mesh
|
||||
from torch.utils._python_dispatch import is_traceable_wrapper_subclass
|
||||
|
||||
from ._fsdp_common import (
|
||||
_is_composable_with_fsdp,
|
||||
DataParallelMeshInfo,
|
||||
DDPMeshInfo,
|
||||
FSDPMeshInfo,
|
||||
HSDPMeshInfo,
|
||||
)
|
||||
from ._fsdp_state import _get_module_fsdp_state
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from ._fsdp_api import DataParallelMeshDims, MixedPrecisionPolicy, OffloadPolicy
|
||||
from ._fsdp_common import ShardPlacementFnResult
|
||||
from ._fsdp_state import FSDPState
|
||||
|
||||
|
||||
logger = logging.getLogger("torch.distributed.fsdp.fully_shard")
|
||||
|
||||
|
||||
def _validate_module(module: nn.Module, func_name: str) -> None:
|
||||
"""
|
||||
Validate that the module can be used with fully_shard or replicate.
|
||||
|
||||
Raises ValueError if the module is a container that doesn't implement forward.
|
||||
"""
|
||||
if (
|
||||
isinstance(module, (nn.ModuleList, nn.ModuleDict))
|
||||
and module.__class__.forward is nn.Module.forward
|
||||
):
|
||||
raise ValueError(
|
||||
f"{func_name} does not support containers that do not implement forward: {module}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_mesh(
|
||||
mesh: "DeviceMesh",
|
||||
dp_mesh_dims: "DataParallelMeshDims | None" = None,
|
||||
) -> None:
|
||||
"""
|
||||
Validate that the mesh can be used with fully_shard.
|
||||
|
||||
When ``dp_mesh_dims`` is provided, validates that the named dims
|
||||
exist in the mesh and at least one of shard/replicate is set.
|
||||
Otherwise raises ValueError if the mesh is not 1D or 2D.
|
||||
"""
|
||||
if dp_mesh_dims is not None:
|
||||
if dp_mesh_dims.shard is None and dp_mesh_dims.replicate is None:
|
||||
raise ValueError(
|
||||
"At least one of shard or replicate must be set in dp_mesh_dims"
|
||||
)
|
||||
if mesh.mesh_dim_names is None:
|
||||
raise ValueError(
|
||||
"mesh must have mesh_dim_names when dp_mesh_dims is provided"
|
||||
)
|
||||
names_to_check: list[str] = list(dp_mesh_dims.shard_names)
|
||||
names_to_check.extend(dp_mesh_dims.replicate_names)
|
||||
for name in names_to_check:
|
||||
if name not in mesh.mesh_dim_names:
|
||||
raise ValueError(
|
||||
f"Mesh dim name '{name}' not found in mesh.mesh_dim_names "
|
||||
f"{mesh.mesh_dim_names}"
|
||||
)
|
||||
return
|
||||
if mesh.ndim not in (1, 2):
|
||||
raise ValueError(f"fully_shard expects a 1D or 2D DeviceMesh but got {mesh}")
|
||||
if mesh.ndim == 2 and mesh.mesh_dim_names is None:
|
||||
raise AssertionError(
|
||||
"Please init the 2D mesh for HSDP with mesh_dim_names specified"
|
||||
)
|
||||
|
||||
|
||||
def _get_mesh_info(
|
||||
mesh: "DeviceMesh",
|
||||
dp_mesh_dims: "DataParallelMeshDims | None" = None,
|
||||
) -> "DataParallelMeshInfo":
|
||||
"""
|
||||
Get the appropriate mesh info for the given mesh.
|
||||
|
||||
When ``dp_mesh_dims`` is provided, extracts the DP submesh from the
|
||||
full SPMD mesh and returns FSDPMeshInfo, HSDPMeshInfo, or DDPMeshInfo
|
||||
with ``dp_mesh_dims`` set and ``is_spmd_mesh`` as True.
|
||||
|
||||
Returns FSDPMeshInfo for 1D mesh, HSDPMeshInfo for 2D mesh.
|
||||
"""
|
||||
if dp_mesh_dims is not None:
|
||||
return _get_mesh_info_from_named_dims(mesh, dp_mesh_dims)
|
||||
if mesh.ndim == 1:
|
||||
return FSDPMeshInfo(mesh, shard_mesh_dim=0)
|
||||
else:
|
||||
return HSDPMeshInfo(mesh, shard_mesh_dim=1, replicate_mesh_dim=0)
|
||||
|
||||
|
||||
def _get_mesh_info_from_named_dims(
|
||||
mesh: "DeviceMesh",
|
||||
dp_mesh_dims: "DataParallelMeshDims",
|
||||
) -> "DataParallelMeshInfo":
|
||||
shard_names = dp_mesh_dims.shard_names
|
||||
replicate_names = dp_mesh_dims.replicate_names
|
||||
|
||||
def _get_submesh(names: tuple[str, ...]) -> "DeviceMesh":
|
||||
if len(names) == 1:
|
||||
return mesh[names[0]]
|
||||
# Flatten multi-dim submesh into a single dim so FSDP's internal
|
||||
# logic (which expects one shard and/or one replicate dim) works
|
||||
# unchanged. This creates a new 1D DeviceMesh and ProcessGroup.
|
||||
return mesh[names]._flatten("_".join(names))
|
||||
|
||||
if len(shard_names) == 0: # DDP
|
||||
dp_mesh = _get_submesh(replicate_names)
|
||||
return DDPMeshInfo(
|
||||
dp_mesh,
|
||||
replicate_mesh_dim=0,
|
||||
dp_mesh_dims=dp_mesh_dims,
|
||||
spmd_mesh=mesh,
|
||||
)
|
||||
if len(replicate_names) == 0: # FSDP
|
||||
dp_mesh = _get_submesh(shard_names)
|
||||
return FSDPMeshInfo(
|
||||
dp_mesh,
|
||||
shard_mesh_dim=0,
|
||||
dp_mesh_dims=dp_mesh_dims,
|
||||
spmd_mesh=mesh,
|
||||
)
|
||||
# HSDP
|
||||
shard_mesh = _get_submesh(shard_names)
|
||||
replicate_mesh = _get_submesh(replicate_names)
|
||||
dp_mesh = DeviceMesh._concatenate([replicate_mesh, shard_mesh])
|
||||
return HSDPMeshInfo(
|
||||
dp_mesh,
|
||||
shard_mesh_dim=1,
|
||||
replicate_mesh_dim=0,
|
||||
dp_mesh_dims=dp_mesh_dims,
|
||||
spmd_mesh=mesh,
|
||||
)
|
||||
|
||||
|
||||
def _get_post_forward_mesh_info(
|
||||
reshard_after_forward: bool | int, mesh_info: FSDPMeshInfo
|
||||
) -> FSDPMeshInfo | None:
|
||||
shard_mesh_size = mesh_info.shard_mesh_size
|
||||
if not isinstance(reshard_after_forward, (bool, int)):
|
||||
raise ValueError(
|
||||
"reshard_after_forward should be a bool or an int representing the "
|
||||
f"group size to reshard to, not {reshard_after_forward}"
|
||||
)
|
||||
# NOTE: `isinstance(False, int)` returns `True`.
|
||||
if not isinstance(reshard_after_forward, bool) and isinstance(
|
||||
reshard_after_forward, int
|
||||
):
|
||||
if (
|
||||
reshard_after_forward < 1
|
||||
or reshard_after_forward > shard_mesh_size
|
||||
or shard_mesh_size % reshard_after_forward != 0
|
||||
):
|
||||
raise ValueError(
|
||||
"If passing reshard_after_forward as an int, it should be a "
|
||||
f"factor of {shard_mesh_size}, not {reshard_after_forward}"
|
||||
)
|
||||
elif reshard_after_forward == 1:
|
||||
msg = (
|
||||
"reshard_after_forward=1 (int) means resharding parameters to world size 1, "
|
||||
"instead of reshard_after_forward=True (bool)"
|
||||
)
|
||||
warning_once(logger, msg, stacklevel=2)
|
||||
reshard_after_forward = False
|
||||
elif reshard_after_forward == shard_mesh_size:
|
||||
reshard_after_forward = True
|
||||
post_forward_mesh_info = None
|
||||
if reshard_after_forward is True:
|
||||
post_forward_mesh_info = mesh_info
|
||||
elif reshard_after_forward is not False: # int case
|
||||
# For HSDP, we can flatten the two replicate dims into the 0th dim
|
||||
post_forward_mesh_tensor = mesh_info.mesh.mesh.view(-1, reshard_after_forward)
|
||||
post_forward_mesh = DeviceMesh(
|
||||
mesh_info.mesh.device_type, post_forward_mesh_tensor
|
||||
)
|
||||
post_forward_mesh_info = HSDPMeshInfo(
|
||||
post_forward_mesh, shard_mesh_dim=1, replicate_mesh_dim=0
|
||||
)
|
||||
return post_forward_mesh_info
|
||||
|
||||
|
||||
def _init_default_mesh(
|
||||
mesh_dim_names: tuple[str, ...] | None = None,
|
||||
) -> DeviceMesh:
|
||||
"""Default to global CUDA mesh if possible else global CPU mesh."""
|
||||
if not dist.distributed_c10d.is_initialized():
|
||||
dist.distributed_c10d.init_process_group()
|
||||
default_pg = dist.distributed_c10d._get_default_group()
|
||||
device = torch._C._get_accelerator()
|
||||
mesh = init_device_mesh(
|
||||
device.type,
|
||||
mesh_shape=(default_pg.size(),),
|
||||
mesh_dim_names=mesh_dim_names,
|
||||
)
|
||||
return mesh
|
||||
|
||||
|
||||
def _init_default_fully_shard_mesh() -> DeviceMesh:
|
||||
"""Default to global CUDA mesh if possible else global CPU mesh."""
|
||||
return _init_default_mesh()
|
||||
|
||||
|
||||
def _get_device_from_mesh(mesh: DeviceMesh) -> torch.device:
|
||||
if mesh.device_type == "cpu":
|
||||
return torch.device("cpu")
|
||||
device_handle = _get_device_handle(mesh.device_type)
|
||||
return torch.device(mesh.device_type, device_handle.current_device())
|
||||
|
||||
|
||||
def _ignore_module(
|
||||
module: nn.Module,
|
||||
ignored_params: set[nn.Parameter],
|
||||
ignore_decision: dict[nn.Module, bool],
|
||||
) -> bool:
|
||||
"""
|
||||
Decide if it is safe to ignore a module for applying fully_shard.
|
||||
"""
|
||||
if module in ignore_decision:
|
||||
return ignore_decision[module]
|
||||
|
||||
if len(list(module.buffers(recurse=False))) > 0:
|
||||
# Cannot ignore a module with any buffer
|
||||
ignore_decision[module] = False
|
||||
return False
|
||||
|
||||
for _, param in module.named_parameters(recurse=False):
|
||||
if param not in ignored_params:
|
||||
# at least one param is not ignored. So this module shouldn't be.
|
||||
ignore_decision[module] = False
|
||||
return False
|
||||
|
||||
# Need to consider descendants of module
|
||||
for child in list(module.children()):
|
||||
ignore_child = _ignore_module(child, ignored_params, ignore_decision)
|
||||
if not ignore_child:
|
||||
# Cannot ignore module if one of its children is not ignored
|
||||
ignore_decision[module] = False
|
||||
return False
|
||||
|
||||
# Safe to ignore module
|
||||
ignore_decision[module] = True
|
||||
return True
|
||||
|
||||
|
||||
def _adjust_managed_modules(
|
||||
modules: list[nn.Module], ignored_params: set[nn.Parameter]
|
||||
) -> list[nn.Module]:
|
||||
"""
|
||||
Adjust the given list of managed modules by removing those with all parameters ignored.
|
||||
"""
|
||||
ignore_decision: dict[nn.Module, bool] = {}
|
||||
new_modules = []
|
||||
for module in modules:
|
||||
ignored = _ignore_module(module, ignored_params, ignore_decision)
|
||||
if not ignored:
|
||||
new_modules.append(module)
|
||||
return new_modules
|
||||
|
||||
|
||||
def _get_managed_modules(
|
||||
root_modules: tuple[nn.Module, ...],
|
||||
ignored_params: set[nn.Parameter] | None = None,
|
||||
is_composable_fn: "Callable[[nn.Module], bool] | None" = None,
|
||||
get_state_fn: "Callable[[nn.Module], Any] | None" = None,
|
||||
) -> list[nn.Module]:
|
||||
"""
|
||||
Get the list of managed modules for FSDP/replicate.
|
||||
|
||||
Args:
|
||||
root_modules: The root modules to start the search from.
|
||||
ignored_params: Parameters to ignore.
|
||||
is_composable_fn: Callable to check if a module is composable.
|
||||
Defaults to ``_is_composable_with_fsdp``.
|
||||
get_state_fn: Callable to get the state of a module.
|
||||
Defaults to ``_get_module_fsdp_state``.
|
||||
"""
|
||||
if is_composable_fn is None:
|
||||
is_composable_fn = _is_composable_with_fsdp
|
||||
if get_state_fn is None:
|
||||
get_state_fn = _get_module_fsdp_state
|
||||
|
||||
modules: list[nn.Module] = []
|
||||
root_modules_set = set(root_modules)
|
||||
# Track visisted modules to avoid visiting shared modules multiple times
|
||||
visited_modules: set[nn.Module] = set()
|
||||
|
||||
def dfs(module: nn.Module) -> None:
|
||||
"""
|
||||
Runs a DFS to collect managed modules, not recursing into modules with
|
||||
a non-composable API or ``fully_shard`` already applied.
|
||||
"""
|
||||
if not is_composable_fn(module):
|
||||
return
|
||||
elif module not in root_modules_set and get_state_fn(module) is not None:
|
||||
return # nested `fully_shard` module
|
||||
visited_modules.add(module)
|
||||
for submodule in module.children():
|
||||
if submodule not in visited_modules:
|
||||
dfs(submodule)
|
||||
modules.append(module)
|
||||
|
||||
for root_module in root_modules:
|
||||
dfs(root_module)
|
||||
|
||||
if ignored_params is None:
|
||||
return modules
|
||||
|
||||
adjusted_modules = _adjust_managed_modules(modules, ignored_params)
|
||||
return adjusted_modules
|
||||
|
||||
|
||||
def _verify_managed_param(name: str, param: nn.Parameter) -> None:
|
||||
"""
|
||||
Verify if the parameter is accepted by fully_shard. The only restriction now
|
||||
is that the parameter cannot be a scalar tensor (param.numel == 0) since we
|
||||
need at least one dim to shard.
|
||||
"""
|
||||
if len(param.shape) == 0:
|
||||
raise ValueError(
|
||||
"fully_shard doesn't support scalar parameters. "
|
||||
f"Change {name} to a 1D tensor with numel equal to 1."
|
||||
)
|
||||
|
||||
|
||||
def _get_managed_states(
|
||||
modules: list[nn.Module], ignored_params: set[nn.Parameter] | None = None
|
||||
) -> tuple[list[nn.Parameter], list[torch.Tensor]]:
|
||||
params: list[nn.Parameter] = []
|
||||
buffers: list[torch.Tensor] = []
|
||||
# Track visited parameters/buffers to avoid visiting shared parameters and
|
||||
# buffers multiple times
|
||||
visited_params: set[nn.Parameter] = set()
|
||||
visited_buffers: set[torch.Tensor] = set()
|
||||
if ignored_params is None:
|
||||
ignored_params = set()
|
||||
|
||||
for module in modules:
|
||||
for name, param in module.named_parameters(recurse=False):
|
||||
if param in ignored_params:
|
||||
# do not include an ignored parameters
|
||||
continue
|
||||
if param not in visited_params:
|
||||
_verify_managed_param(name, param)
|
||||
params.append(param)
|
||||
visited_params.add(param)
|
||||
for buffer in module.buffers(recurse=False):
|
||||
if buffer not in visited_buffers:
|
||||
buffers.append(buffer)
|
||||
visited_buffers.add(buffer)
|
||||
return params, buffers
|
||||
|
||||
|
||||
def _move_states_to_device(
|
||||
params: list[nn.Parameter],
|
||||
buffers: list[torch.Tensor],
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
"""
|
||||
We have FSDP move states to device for simpler and faster initialization
|
||||
since FSDP almost always uses CUDA for training. We move parameters/buffers
|
||||
rather than modules since modules to support ignoring parameters/buffers in
|
||||
the future.
|
||||
"""
|
||||
# Follow the logic in `nn.Module._apply`
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
for tensor in itertools.chain(params, buffers):
|
||||
if tensor.device == device or tensor.device.type == "meta":
|
||||
# Keep meta-device tensors on meta device for deferred init
|
||||
continue
|
||||
if isinstance(tensor, DTensor):
|
||||
if (dtensor_mesh_type := tensor.device_mesh.device_type) != device.type:
|
||||
raise ValueError(
|
||||
"Requires DTensor to have mesh of the same type as the FSDP mesh "
|
||||
f"but got {dtensor_mesh_type} for DTensor and {device.type} for FSDP"
|
||||
)
|
||||
raise AssertionError(
|
||||
f"Expects DTensor to be moved to {dtensor_mesh_type} but got {tensor.device}"
|
||||
)
|
||||
tensor_ = tensor
|
||||
if is_traceable_wrapper_subclass(tensor_):
|
||||
with torch.no_grad(): # avoid autograd increasing C++ refcount by 1
|
||||
tensor_on_device = nn.Parameter(tensor.to(device))
|
||||
torch.utils.swap_tensors(tensor, tensor_on_device)
|
||||
else:
|
||||
tensor.data = tensor.to(device)
|
||||
|
||||
|
||||
def _apply_to_module(
|
||||
modules: tuple[nn.Module, ...],
|
||||
cls_to_wrapper_cls: dict[type, type],
|
||||
wrapper_module_cls: type,
|
||||
wrapper_cls_prefix: str,
|
||||
unimplemented_deepcopy: "Callable",
|
||||
) -> None:
|
||||
"""
|
||||
Modify module classes to include the wrapper class in their MRO.
|
||||
|
||||
Args:
|
||||
modules: The modules to apply the wrapper to.
|
||||
cls_to_wrapper_cls: Cache dict mapping original class to wrapper class.
|
||||
wrapper_module_cls: The wrapper module class (e.g., FSDPModule, ReplicateModule).
|
||||
wrapper_cls_prefix: Prefix for the dynamically created class name (e.g., "FSDP", "Replicate").
|
||||
unimplemented_deepcopy: The deepcopy function to use for the wrapper class.
|
||||
"""
|
||||
for module in modules:
|
||||
cls = module.__class__
|
||||
new_cls = cls_to_wrapper_cls.get(cls)
|
||||
if not new_cls:
|
||||
dct = {"__deepcopy__": unimplemented_deepcopy}
|
||||
new_cls = type(
|
||||
f"{wrapper_cls_prefix}{cls.__name__}", (wrapper_module_cls, cls), dct
|
||||
)
|
||||
cls_to_wrapper_cls[cls] = new_cls
|
||||
module.__class__ = new_cls
|
||||
|
||||
|
||||
def _init_param_group(
|
||||
state: "FSDPState",
|
||||
params: list[nn.Parameter],
|
||||
modules: tuple[nn.Module, ...],
|
||||
mesh_info: DataParallelMeshInfo,
|
||||
post_forward_mesh_info: FSDPMeshInfo | None,
|
||||
device: torch.device,
|
||||
shard_placement_fn: "Callable[[nn.Parameter], ShardPlacementFnResult] | None",
|
||||
mp_policy: "MixedPrecisionPolicy",
|
||||
offload_policy: "OffloadPolicy",
|
||||
reshard_after_forward: bool | int = True,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize FSDP param groups for the given state.
|
||||
|
||||
Params are grouped by their process group (derived from ``mesh_info`` via
|
||||
``shard_placement_fn``). Each group becomes a separate ``FSDPParamGroup``.
|
||||
When ``shard_placement_fn`` is ``None`` or returns the same mesh for all
|
||||
params, this creates a single group.
|
||||
"""
|
||||
# Import here to avoid circular imports
|
||||
from ._fsdp_common import FSDPMeshInfo, resolve_shard_placement
|
||||
from ._fsdp_param_group import FSDPParamGroup
|
||||
|
||||
if not params:
|
||||
return
|
||||
|
||||
if shard_placement_fn is None:
|
||||
# No shard_placement_fn means all params use the same mesh_info,
|
||||
# so no grouping is needed. This also handles DDPMeshInfo from
|
||||
# replicate_with_fsdp, which doesn't have shard_process_group.
|
||||
state._fsdp_param_groups.append(
|
||||
FSDPParamGroup(
|
||||
params,
|
||||
modules,
|
||||
mesh_info,
|
||||
post_forward_mesh_info,
|
||||
device,
|
||||
shard_placement_fn,
|
||||
mp_policy,
|
||||
offload_policy,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Group params by their process group to support per-param mesh,
|
||||
# e.g., expert params using ep_mesh vs regular params using dp_mesh.
|
||||
# For HSDP, also key by replicate_process_group to avoid grouping
|
||||
# FSDPMeshInfo params with HSDPMeshInfo params that share the same
|
||||
# shard_process_group but require different gradient reduction behavior.
|
||||
if not isinstance(mesh_info, FSDPMeshInfo):
|
||||
raise ValueError(
|
||||
"Per-param mesh via shard_placement_fn is not supported with "
|
||||
f"{type(mesh_info).__name__}; it requires FSDPMeshInfo (or subclass)"
|
||||
)
|
||||
pg_to_group: dict[
|
||||
tuple[dist.ProcessGroup, dist.ProcessGroup | None],
|
||||
tuple[FSDPMeshInfo, list[nn.Parameter]],
|
||||
] = {}
|
||||
for param in params:
|
||||
param_mesh_info = resolve_shard_placement(
|
||||
shard_placement_fn(param),
|
||||
mesh_info,
|
||||
).mesh_info
|
||||
shard_pg = param_mesh_info.shard_process_group
|
||||
replicate_pg: dist.ProcessGroup | None = None
|
||||
if isinstance(param_mesh_info, HSDPMeshInfo):
|
||||
replicate_pg = param_mesh_info.replicate_process_group
|
||||
key = (shard_pg, replicate_pg)
|
||||
if key not in pg_to_group:
|
||||
pg_to_group[key] = (param_mesh_info, [param])
|
||||
else:
|
||||
existing_mesh_info = pg_to_group[key][0]
|
||||
if existing_mesh_info is not param_mesh_info:
|
||||
raise ValueError(
|
||||
f"Params sharing the same process group must use the same "
|
||||
f"FSDPMeshInfo object, but got different objects: "
|
||||
f"{existing_mesh_info} vs {param_mesh_info}"
|
||||
)
|
||||
pg_to_group[key][1].append(param)
|
||||
|
||||
# Create a FSDPParamGroup per process group
|
||||
for group_mesh_info, group_params in pg_to_group.values():
|
||||
if group_mesh_info is not mesh_info:
|
||||
group_post_forward = _get_post_forward_mesh_info(
|
||||
reshard_after_forward, group_mesh_info
|
||||
)
|
||||
else:
|
||||
group_post_forward = post_forward_mesh_info
|
||||
state._fsdp_param_groups.append(
|
||||
FSDPParamGroup(
|
||||
group_params,
|
||||
modules,
|
||||
group_mesh_info,
|
||||
group_post_forward,
|
||||
device,
|
||||
shard_placement_fn,
|
||||
mp_policy,
|
||||
offload_policy,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _get_modules_and_states(
|
||||
module: nn.Module,
|
||||
device: torch.device,
|
||||
ignored_params: set[nn.Parameter] | None,
|
||||
is_composable_fn: "Callable[[nn.Module], bool] | None" = None,
|
||||
get_state_fn: "Callable[[nn.Module], Any] | None" = None,
|
||||
) -> tuple[
|
||||
nn.Module,
|
||||
tuple[nn.Module, ...],
|
||||
list[nn.Module],
|
||||
list[nn.Parameter],
|
||||
list[torch.Tensor],
|
||||
]:
|
||||
"""
|
||||
Get modules tuple, managed modules, params, and buffers for FSDP/replicate initialization.
|
||||
|
||||
Returns:
|
||||
Tuple of (arg_module, modules, managed_modules, params, buffers)
|
||||
"""
|
||||
from torch.distributed.utils import _get_root_modules
|
||||
|
||||
arg_module = module
|
||||
modules = (
|
||||
(module,) if isinstance(module, nn.Module) else tuple(_get_root_modules(module))
|
||||
)
|
||||
|
||||
managed_modules = _get_managed_modules(
|
||||
modules, ignored_params, is_composable_fn, get_state_fn
|
||||
)
|
||||
params, buffers = _get_managed_states(managed_modules, ignored_params)
|
||||
|
||||
_move_states_to_device(params, buffers, device)
|
||||
|
||||
return arg_module, modules, managed_modules, params, buffers
|
||||
+1017
File diff suppressed because it is too large
Load Diff
+923
@@ -0,0 +1,923 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any, cast, Literal, NamedTuple, TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.nn as nn
|
||||
from torch.distributed.device_mesh import _get_device_handle
|
||||
from torch.distributed.fsdp._common_utils import (
|
||||
_named_parameters_with_duplicates,
|
||||
collect_grad_tensors,
|
||||
replace_grad_tensors,
|
||||
)
|
||||
from torch.profiler import record_function
|
||||
from torch.utils.hooks import RemovableHandle
|
||||
|
||||
from ._fsdp_api import CPUOffloadPolicy, MixedPrecisionPolicy, OffloadPolicy
|
||||
from ._fsdp_collectives import (
|
||||
AllGather,
|
||||
AllGatherResult,
|
||||
DefaultAllGather,
|
||||
DefaultReduceScatter,
|
||||
foreach_all_gather,
|
||||
foreach_all_gather_copy_out,
|
||||
foreach_reduce,
|
||||
ProcessGroupAllocAllGather,
|
||||
ProcessGroupAllocReduceScatter,
|
||||
ReduceScatter,
|
||||
SymmMemAllGather,
|
||||
SymmMemReduceScatter,
|
||||
)
|
||||
from ._fsdp_common import (
|
||||
_dynamo_disable,
|
||||
DataParallelMeshInfo,
|
||||
DDPMeshInfo,
|
||||
FSDPMeshInfo,
|
||||
HSDPMeshInfo,
|
||||
is_bw,
|
||||
ShardPlacementFnResult,
|
||||
TrainingState,
|
||||
)
|
||||
from ._fsdp_param import alloc_storage, FSDPParam, ParamModuleInfo, ShardedState
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
logger = logging.getLogger("torch.distributed.fsdp.fully_shard")
|
||||
|
||||
_ModuleToHandleDict = dict[nn.Module, RemovableHandle] # for state dict
|
||||
|
||||
|
||||
"""
|
||||
[Note: Overlapping all-gather copy-in and all-gather]
|
||||
For implicit forward prefetching, we want to overlap the next copy-in with the
|
||||
current all-gather. We do so using a separate copy-in stream. However, since
|
||||
we have the all-gather input as a view into the output, we must make sure to
|
||||
copy into different memory from the current all-gather's output. Thus, we keep
|
||||
a reference to the current all-gather's output and have the next FSDP parameter
|
||||
group free it after its copy-in. Finally, we have the last FSDP state flush the
|
||||
reference to avoid holding onto memory after forward.
|
||||
"""
|
||||
|
||||
|
||||
class FSDPCommContext:
|
||||
"""This has the communication state shared across FSDP states/parameter groups."""
|
||||
|
||||
def lazy_init(self, device: torch.device):
|
||||
self.device_handle = _get_device_handle(device.type)
|
||||
# Setting the all-gather/reduce-scatter streams to be higher priority
|
||||
# can help avoid some issues where their copies in/out are delayed and
|
||||
# block computation (this is different from high-pri NCCL streams)
|
||||
high_priority = -1
|
||||
# All-gather state and copy-in stream allow overlapping the next
|
||||
# copy-in with the current all-gather in forward; copy-in overlaps with
|
||||
# reduce-scatter in backward without the separate copy-in stream
|
||||
self.all_gather_copy_in_stream = self.device_handle.Stream(
|
||||
priority=high_priority
|
||||
)
|
||||
# All-gather stream allows overlapping next all-gather with current
|
||||
# forward compute
|
||||
self.all_gather_stream = self.device_handle.Stream(priority=high_priority)
|
||||
# Reduce-scatter stream gives separate execution "thread" for post-
|
||||
# backward logic like pre/post-gradient division and reduce-scatter
|
||||
self.reduce_scatter_stream = self.device_handle.Stream(priority=high_priority)
|
||||
# Run the HSDP all-reduces concurrently with all-gather/reduce-scatter
|
||||
# since collectives use different network resources and can overlap
|
||||
# in the typical intra-node sharding / inter-node replication case
|
||||
self.all_reduce_stream = self.device_handle.Stream()
|
||||
# All-gather/reduce-scatter states keep references to collective
|
||||
# tensors produced in one stream and used in another and accompanying
|
||||
# CUDA events for synchronization
|
||||
self.all_gather_state: AllGatherState | None = None
|
||||
self.reduce_scatter_states: list[ReduceScatterState] = []
|
||||
# Post-forward order for explicit backward prefetching
|
||||
self.post_forward_order: list[FSDPParamGroup] = [] # will cause ref cycles
|
||||
|
||||
def get_all_gather_streams(
|
||||
self, async_op: bool, training_state: TrainingState
|
||||
) -> tuple[torch.Stream, torch.Stream]:
|
||||
if not async_op and training_state in (
|
||||
TrainingState.FORWARD,
|
||||
TrainingState.PRE_BACKWARD,
|
||||
):
|
||||
# Use separate streams for implicit prefetching
|
||||
return self.all_gather_copy_in_stream, self.all_gather_stream
|
||||
current_stream = self.device_handle.current_stream()
|
||||
return current_stream, current_stream
|
||||
|
||||
|
||||
# See [Note: Overlapping all-gather copy-in and all-gather]
|
||||
class AllGatherState(NamedTuple):
|
||||
all_gather_result: AllGatherResult
|
||||
event: torch.Event | None # all-gather copy-out
|
||||
|
||||
|
||||
class ReduceScatterState(NamedTuple):
|
||||
reduce_scatter_input: torch.Tensor
|
||||
event: torch.Event | None # reduce-scatter event
|
||||
|
||||
|
||||
class AllReduceState(NamedTuple):
|
||||
all_reduce_input: torch.Tensor
|
||||
event: torch.Event | None # all-reduce event
|
||||
|
||||
|
||||
class FSDPParamGroup:
|
||||
"""This class represents a parameter group to communicate together."""
|
||||
|
||||
_orig_dtype: torch.dtype | None
|
||||
_reduce_dtype: torch.dtype | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: list[nn.Parameter],
|
||||
modules: tuple[nn.Module, ...],
|
||||
mesh_info: DataParallelMeshInfo,
|
||||
post_forward_mesh_info: FSDPMeshInfo | None,
|
||||
device: torch.device,
|
||||
shard_placement_fn: Callable[[nn.Parameter], ShardPlacementFnResult] | None,
|
||||
mp_policy: MixedPrecisionPolicy,
|
||||
offload_policy: OffloadPolicy,
|
||||
):
|
||||
self.modules = modules # permit ref cycle because 1:1 lifetime
|
||||
param_module_infos = _get_param_module_infos(params, modules)
|
||||
|
||||
self.fsdp_params = [
|
||||
FSDPParam(
|
||||
param,
|
||||
module_info,
|
||||
mesh_info,
|
||||
post_forward_mesh_info,
|
||||
device,
|
||||
shard_placement_fn,
|
||||
mp_policy,
|
||||
offload_policy,
|
||||
)
|
||||
for param, module_info in zip(params, param_module_infos)
|
||||
]
|
||||
self.mesh_info = mesh_info
|
||||
self.post_forward_mesh_info = post_forward_mesh_info
|
||||
self.device = device
|
||||
self.device_handle = _get_device_handle(device.type)
|
||||
self.mp_policy = mp_policy
|
||||
self.offload_policy = offload_policy
|
||||
self._training_state = TrainingState.IDLE
|
||||
# Group's sharded state always matches its parameters' sharded states
|
||||
self._sharded_state = ShardedState.SHARDED
|
||||
self._module_fqn: str | None = None # prefixed from root module
|
||||
# Only consider resetting sharded parameters once in lazy init since it
|
||||
# can incur nontrivial overhead to reset them
|
||||
self._reset_sharded_params: bool = False
|
||||
|
||||
# - Hook state
|
||||
self._module_to_pre_save_state_dict_hook_handle: _ModuleToHandleDict = {}
|
||||
self._module_to_pre_load_state_dict_hook_handle: _ModuleToHandleDict = {}
|
||||
self._all_reduce_hook: Callable[[torch.Tensor], None] | None = None
|
||||
self._all_gather_comm: AllGather = DefaultAllGather()
|
||||
self._all_gather_output = torch.empty(0, device=self.device)
|
||||
self._reduce_scatter_comm: ReduceScatter = DefaultReduceScatter()
|
||||
# Optional stream to run the user-defined all-reduce hook in
|
||||
# Saved here and not in the comm. context because we allow the user to
|
||||
# specify it, possibly at construction time before lazy init
|
||||
self._all_reduce_hook_stream: torch.cuda.Stream | None = None
|
||||
|
||||
# - Communication and communication/computation overlap
|
||||
self.comm_ctx = FSDPCommContext()
|
||||
self._param_group_index: int = 0
|
||||
self._num_param_groups: int = 1
|
||||
# Group's indices in the shared post-forward order
|
||||
self._post_forward_indices: list[int] = []
|
||||
# Whether to reduce gradients at all (whether for FSDP or HSDP)
|
||||
self.reduce_grads: bool = True
|
||||
# Whether to all-reduce gradients for HSDP; only used if
|
||||
# `self.reduce_grads` is true, in which case setting this to false
|
||||
# means reduce-scatter but no all-reduce
|
||||
self.all_reduce_grads: bool = True
|
||||
# Whether to reshard parameters after backward (only useful for
|
||||
# gradient accumulation)
|
||||
self.reshard_after_backward: bool = True
|
||||
# Optional custom factor for the gradient reduction op (e.g. to divide
|
||||
# by a factor other than the world size)
|
||||
self.gradient_divide_factor: float | None = None
|
||||
# Whether reduce-scatter and all-reduce should be issued using only
|
||||
# summations, potentially with separate pre-/post-scaling.
|
||||
self.force_sum_reduction_for_comms: bool = False
|
||||
# `async_op` arg used for pre-forward/pre-backward unshard; can be
|
||||
# overridden to only do explicit prefetching and avoid inter-stream
|
||||
# fragmentation from using separate unshard streams
|
||||
self.unshard_async_op: bool = False
|
||||
# Whether to unshard in backward: can be overridden by the user if the
|
||||
# parameters in this group are not needed for backward (e.g. embedding)
|
||||
self.unshard_in_backward: bool = True
|
||||
|
||||
# - CUDA events for stream synchronization
|
||||
# Holds the all-gather output buffer, sync objects, and metadata
|
||||
self._all_gather_result: AllGatherResult | None = None
|
||||
# Holds the reduce-scatter/all-reduce view-out CUDA event that marks the end of
|
||||
# the group's post-backward (e.g. reduce-scatter, all-reduce and div), which
|
||||
# should be waited on at the end of backward
|
||||
self._post_reduce_event: torch.Event | None = None
|
||||
# Holds the reshard-after-forward CUDA event when resharding to a
|
||||
# different world size, which should be waited on in the next unshard
|
||||
self._reshard_after_forward_event: torch.Event | None = None
|
||||
|
||||
# Only for HSDP, if accumulating gradients without all-reduce, save the
|
||||
# partial reduce output (only reduce-scattered but not all-reduced)
|
||||
self._partial_reduce_output: torch.Tensor | None = None
|
||||
# Holds the all-reduce input and all-reduce event to keep it alive
|
||||
# until the end of backward (critical when doing bf16 reduction with
|
||||
# fp32 parameters since the all-reduce input is allocated in the RS
|
||||
# stream and will have no refs to it after being upcast to fp32)
|
||||
self._all_reduce_state: AllReduceState | None = None
|
||||
|
||||
# Initialization #
|
||||
def _init_mp_dtypes(self) -> None:
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.init_dtype_attrs(self.mp_policy)
|
||||
trainable_params: list[FSDPParam] = [
|
||||
p for p in self.fsdp_params if p.sharded_param.requires_grad
|
||||
]
|
||||
orig_dtypes = {p.orig_dtype for p in trainable_params}
|
||||
reduce_dtypes = {p.reduce_dtype for p in trainable_params}
|
||||
if len(trainable_params) > 0 and len(orig_dtypes) != 1:
|
||||
# Models may have no grad params
|
||||
raise AssertionError(
|
||||
f"FSDP expects uniform original parameter dtype but got {orig_dtypes}"
|
||||
)
|
||||
self._orig_dtype = next(iter(orig_dtypes)) if trainable_params else None
|
||||
if len(trainable_params) > 0 and len(reduce_dtypes) != 1:
|
||||
# This can be relaxed if we issue one reduce-scatter per reduce
|
||||
# dtype (but we would need a way for users to specify multiple
|
||||
# reduce dtypes)
|
||||
raise AssertionError(
|
||||
f"FSDP expects uniform reduce dtype but got {reduce_dtypes}"
|
||||
)
|
||||
self._reduce_dtype = next(iter(reduce_dtypes)) if trainable_params else None
|
||||
|
||||
def lazy_init(self):
|
||||
# Lazy init should be idempotent
|
||||
# Users may change or register parameters after construction time.
|
||||
# For example, DoRA (https://arxiv.org/abs/2402.09353) initializes linear magnitudes based on
|
||||
# other parameters (e.g. loaded from the state dict).
|
||||
if not hasattr(self.comm_ctx, "device_handle"):
|
||||
self.comm_ctx.device_handle = _get_device_handle(self.device.type)
|
||||
if self.is_sharded and not self._reset_sharded_params:
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.reset_sharded_param()
|
||||
fsdp_param._init_extensions() # allow monkey patch after init
|
||||
self._reset_sharded_params = True
|
||||
self._validate_no_meta_params()
|
||||
self._validate_cpu_offload_params()
|
||||
# Initialize mixed precision attributes lazily in case the user changes
|
||||
# the parameter dtypes after construction time but before forward
|
||||
self._init_mp_dtypes()
|
||||
self._register_state_dict_hooks()
|
||||
|
||||
def set_symm_mem(self, backend: Literal["NCCL"] = "NCCL") -> None:
|
||||
if not isinstance(self._all_gather_comm, (DefaultAllGather | SymmMemAllGather)):
|
||||
raise AssertionError(
|
||||
"cannot call set_symm_mem() "
|
||||
f"when all gather comm is custom: {self._all_gather_comm.__class__.__name__}"
|
||||
)
|
||||
self._all_gather_comm = SymmMemAllGather(
|
||||
self._all_gather_process_group, backend
|
||||
)
|
||||
if not isinstance(
|
||||
self._reduce_scatter_comm, (DefaultReduceScatter | SymmMemReduceScatter)
|
||||
):
|
||||
raise AssertionError(
|
||||
"cannot call set_symm_mem() "
|
||||
f"when reduce scatter comm is custom: {self._reduce_scatter_comm.__class__.__name__}"
|
||||
)
|
||||
if self.force_sum_reduction_for_comms:
|
||||
# As of NCCL 2.29.3, NCCL symmetric reduce-scatter only supports SUM reduction
|
||||
self._reduce_scatter_comm = SymmMemReduceScatter(
|
||||
self._reduce_scatter_process_group, backend
|
||||
)
|
||||
|
||||
def set_allocate_memory_from_process_group(self, enable: bool) -> None:
|
||||
"""
|
||||
Whether to (try to) use the ProcessGroup's allocate_tensor method for
|
||||
the staging buffers for collective comms.
|
||||
"""
|
||||
if not isinstance(
|
||||
self._all_gather_comm, (DefaultAllGather | ProcessGroupAllocAllGather)
|
||||
):
|
||||
raise AssertionError(
|
||||
"cannot call set_allocate_memory_from_process_group() "
|
||||
f"when all gather comm is custom: {self._all_gather_comm.__class__.__name__}"
|
||||
)
|
||||
self._all_gather_comm = (
|
||||
ProcessGroupAllocAllGather(self._all_gather_process_group)
|
||||
if enable
|
||||
else DefaultAllGather()
|
||||
)
|
||||
|
||||
if not isinstance(
|
||||
self._reduce_scatter_comm,
|
||||
(DefaultReduceScatter | ProcessGroupAllocReduceScatter),
|
||||
):
|
||||
raise AssertionError(
|
||||
"cannot call set_allocate_memory_from_process_group() "
|
||||
f"when reduce scatter comm is custom: {self._reduce_scatter_comm.__class__.__name__}"
|
||||
)
|
||||
self._reduce_scatter_comm = (
|
||||
ProcessGroupAllocReduceScatter(self._reduce_scatter_process_group)
|
||||
if enable
|
||||
else DefaultReduceScatter()
|
||||
)
|
||||
|
||||
# Runtime #
|
||||
def unshard(self, async_op: bool = False):
|
||||
if self._all_gather_result is not None: # already called, pending wait
|
||||
return
|
||||
if self.is_unsharded:
|
||||
return # no-op
|
||||
if (
|
||||
not self.unshard_in_backward
|
||||
and self._training_state == TrainingState.PRE_BACKWARD
|
||||
):
|
||||
return
|
||||
if self._reshard_after_forward_event is not None:
|
||||
# Resharded parameter data is allocated in the default stream and
|
||||
# used in the all-gather streams
|
||||
self._wait_all_gather_streams_on_event(self._reshard_after_forward_event)
|
||||
self._reshard_after_forward_event = None
|
||||
|
||||
if isinstance(self.mesh_info, FSDPMeshInfo):
|
||||
world_size = self._all_gather_process_group.size()
|
||||
else:
|
||||
world_size = 1
|
||||
if world_size == 1:
|
||||
# can't skip due to early return in wait_for_unshard if
|
||||
# no self._all_gather_result
|
||||
self._all_gather_result = AllGatherResult(
|
||||
all_gather_output=self._all_gather_output,
|
||||
all_gather_event=self.device_handle.Event().record(),
|
||||
all_gather_work=None,
|
||||
param_all_gather_input_dtypes=[],
|
||||
param_all_gather_input_numels=[],
|
||||
all_gather_input_split_sizes=[],
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
with record_function(self._with_fqn("FSDP::all_gather")):
|
||||
self._all_gather_result = foreach_all_gather(
|
||||
self.fsdp_params,
|
||||
self._all_gather_process_group,
|
||||
async_op,
|
||||
*self.comm_ctx.get_all_gather_streams(async_op, self._training_state),
|
||||
self.device,
|
||||
self._all_gather_comm,
|
||||
)
|
||||
|
||||
def wait_for_unshard(self):
|
||||
"""
|
||||
1. In forward with implicit prefetching, to overlap the current copy-out
|
||||
with the next all-gather, we save a reference to the current all-gather
|
||||
result to free after the next copy-out.
|
||||
2. Otherwise (explicit prefetching or in backward), we free the
|
||||
all-gather result immediately after the current copy-out since we can
|
||||
already overlap the current copy-out with the previous reduce-scatter.
|
||||
"""
|
||||
if not self._all_gather_result:
|
||||
return # no preceding unshard
|
||||
async_op = self._all_gather_result.all_gather_work is not None
|
||||
if self._training_state == TrainingState.FORWARD: # implicit prefetch
|
||||
if prev_all_gather_state := self.comm_ctx.all_gather_state:
|
||||
self._wait_all_gather_streams_on_event(prev_all_gather_state.event)
|
||||
self.comm_ctx.all_gather_state = None # free the all-gather result
|
||||
if isinstance(self.mesh_info, FSDPMeshInfo):
|
||||
world_size = self._all_gather_process_group.size()
|
||||
else:
|
||||
world_size = 1
|
||||
if world_size == 1:
|
||||
# directly initialize unsharded parameters from sharded parameters
|
||||
|
||||
for fsdp_param in self.fsdp_params:
|
||||
# Use all_gather_inputs which already handles conversion to param_dtype
|
||||
# This is consistent with the world_size > 1 path
|
||||
all_gather_input = fsdp_param.all_gather_inputs[0]
|
||||
|
||||
# Make sure the all_gather_outputs has proper storage size before using it
|
||||
# First ensure we have at least one tensor in all_gather_outputs
|
||||
fsdp_param.init_all_gather_outputs(
|
||||
[all_gather_input.numel()],
|
||||
[all_gather_input.dtype],
|
||||
world_size,
|
||||
self.device,
|
||||
force_recreate=False,
|
||||
)
|
||||
|
||||
tensor = fsdp_param.all_gather_outputs[0]
|
||||
alloc_storage(tensor)
|
||||
|
||||
# find alternative way to check if tensor.is_inference
|
||||
with torch.autograd._unsafe_preserve_version_counter(tensor):
|
||||
tensor.copy_(all_gather_input)
|
||||
|
||||
else:
|
||||
with record_function(self._with_fqn("FSDP::all_gather_copy_out")):
|
||||
foreach_all_gather_copy_out(
|
||||
self._all_gather_result,
|
||||
self.fsdp_params,
|
||||
self._all_gather_process_group,
|
||||
)
|
||||
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.init_unsharded_param()
|
||||
|
||||
self._to_unsharded()
|
||||
all_gather_copy_out_event = self.device_handle.Event()
|
||||
all_gather_copy_out_event.record()
|
||||
|
||||
if (
|
||||
not async_op
|
||||
and self._training_state == TrainingState.FORWARD
|
||||
and world_size > 1
|
||||
):
|
||||
# Defer free to allow for overlap of this copy-out with next
|
||||
# all-gather collective
|
||||
self.comm_ctx.all_gather_state = AllGatherState(
|
||||
self._all_gather_result, all_gather_copy_out_event
|
||||
)
|
||||
else:
|
||||
self._wait_all_gather_streams_on_event(all_gather_copy_out_event)
|
||||
|
||||
self._all_gather_result = None # free unless saved in `all_gather_state`
|
||||
|
||||
def _wait_all_gather_streams_on_event(self, event: torch.Event | None):
|
||||
# Calling `unshard` before lazy init means streams are not initialized
|
||||
if hasattr(self.comm_ctx, "all_gather_copy_in_stream") and event is not None:
|
||||
self.comm_ctx.all_gather_copy_in_stream.wait_event(event)
|
||||
if hasattr(self.comm_ctx, "all_gather_stream") and event is not None:
|
||||
self.comm_ctx.all_gather_stream.wait_event(event)
|
||||
|
||||
def reshard(self):
|
||||
if self._training_state == TrainingState.FORWARD:
|
||||
if not self._reshard_after_forward:
|
||||
return
|
||||
if self._use_post_forward_mesh:
|
||||
self._to_sharded_post_forward()
|
||||
self._reshard_after_forward_event = self.device_handle.Event()
|
||||
if self._reshard_after_forward_event is not None:
|
||||
self._reshard_after_forward_event.record()
|
||||
return
|
||||
self._to_sharded()
|
||||
|
||||
def pre_forward(
|
||||
self, module: nn.Module, args: tuple[Any, ...], kwargs: dict[str, Any]
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
logger.debug("%s", self._with_fqn("FSDP::pre_forward"))
|
||||
with record_function(self._with_fqn("FSDP::pre_forward")):
|
||||
self._training_state = TrainingState.FORWARD
|
||||
self.unshard(self.unshard_async_op)
|
||||
self.wait_for_unshard()
|
||||
args, kwargs = self._register_post_backward_hook(args, kwargs)
|
||||
return args, kwargs
|
||||
|
||||
def post_forward(self, module: nn.Module, input: Any, output: Any):
|
||||
logger.debug("%s", self._with_fqn("FSDP::post_forward"))
|
||||
with record_function(self._with_fqn("FSDP::post_forward")):
|
||||
# for AC(fully_shard(model)), AC runs fsdp's _pre_forward
|
||||
# it shouldn't change post_forward_order
|
||||
if not is_bw():
|
||||
self.reshard()
|
||||
self._record_post_forward()
|
||||
self._training_state = TrainingState.IDLE
|
||||
return output
|
||||
|
||||
def _record_post_forward(self) -> None:
|
||||
# Since a group has one pre-backward unshard for each forward call
|
||||
# before the backward, we record each usage (with multiplicity)
|
||||
post_forward_index = len(self.comm_ctx.post_forward_order)
|
||||
self.comm_ctx.post_forward_order.append(self)
|
||||
self._post_forward_indices.append(post_forward_index)
|
||||
|
||||
@_dynamo_disable
|
||||
def pre_backward(self, default_prefetch: bool, *unused: Any):
|
||||
if self._training_state == TrainingState.PRE_BACKWARD:
|
||||
return
|
||||
logger.debug("%s", self._with_fqn("FSDP::pre_backward"))
|
||||
with record_function(self._with_fqn("FSDP::pre_backward")):
|
||||
self._training_state = TrainingState.PRE_BACKWARD
|
||||
self.unshard(self.unshard_async_op) # no-op if prefetched
|
||||
self.wait_for_unshard()
|
||||
if default_prefetch:
|
||||
self._backward_prefetch()
|
||||
|
||||
@_dynamo_disable
|
||||
def post_backward(self, *unused: Any):
|
||||
# This method should be idempotent and safe to call even when this
|
||||
# FSDP parameter group was not used in backward (should be a no-op)
|
||||
logger.debug("%s", self._with_fqn("FSDP::post_backward"))
|
||||
self._training_state = TrainingState.POST_BACKWARD
|
||||
with record_function(self._with_fqn("FSDP::post_backward_accumulate")):
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.accumulate_unsharded_grad_if_needed()
|
||||
with record_function(self._with_fqn("FSDP::post_backward_reshard")):
|
||||
if not self.reduce_grads:
|
||||
if self.reshard_after_backward:
|
||||
self.reshard()
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.to_accumulated_grad_if_needed()
|
||||
return
|
||||
# Save the autograd-computed gradients before resharding to only
|
||||
# access the unsharded parameters when their data is present
|
||||
fsdp_params_with_grad: list[FSDPParam] = []
|
||||
unsharded_grads: list[torch.Tensor] = []
|
||||
for fsdp_param in self.fsdp_params:
|
||||
if not hasattr(fsdp_param, "_unsharded_param"):
|
||||
continue
|
||||
# May have an accumulated gradient of the reduce dtype if the
|
||||
# previous backward did not reduce-scatter
|
||||
if fsdp_param.unsharded_accumulated_grad is not None:
|
||||
fsdp_params_with_grad.append(fsdp_param)
|
||||
unsharded_grads.append(fsdp_param.unsharded_accumulated_grad_data)
|
||||
fsdp_param.unsharded_accumulated_grad = None
|
||||
elif fsdp_param.unsharded_param.grad is not None:
|
||||
fsdp_params_with_grad.append(fsdp_param)
|
||||
unsharded_grads.append(fsdp_param.unsharded_grad_data)
|
||||
fsdp_param.unsharded_param.grad = None
|
||||
if self.reshard_after_backward:
|
||||
self.reshard()
|
||||
# Wait on prior module's RS states (assumes backward fires groups
|
||||
# N-1 first; if not, overlap degrades but correctness is preserved).
|
||||
if (
|
||||
self._param_group_index == self._num_param_groups - 1
|
||||
and self.comm_ctx.reduce_scatter_states
|
||||
):
|
||||
with record_function(f"FSDP::post_backward_rs_wait ({self._module_fqn})"):
|
||||
for rs_state in self.comm_ctx.reduce_scatter_states:
|
||||
if rs_state.event is not None:
|
||||
self.device_handle.current_stream().wait_event(rs_state.event)
|
||||
self.comm_ctx.reduce_scatter_states.clear()
|
||||
if len(fsdp_params_with_grad) == 0:
|
||||
return
|
||||
with record_function(self._with_fqn("FSDP::post_backward_reduce")):
|
||||
all_reduce_pg = (
|
||||
self._all_reduce_process_group
|
||||
if isinstance(self.mesh_info, DDPMeshInfo)
|
||||
else None
|
||||
)
|
||||
all_reduce_stream: torch.cuda.Stream
|
||||
if all_reduce_pg is None and self._all_reduce_hook_stream is not None:
|
||||
# this means the native HSDP is not enabled,
|
||||
# but user may want to have a custom HSDP setup
|
||||
if self._all_reduce_hook is None:
|
||||
raise AssertionError(
|
||||
"all reduce hook stream is specified but hook itself is missing."
|
||||
)
|
||||
all_reduce_stream = self._all_reduce_hook_stream
|
||||
else:
|
||||
all_reduce_stream = self.comm_ctx.all_reduce_stream
|
||||
|
||||
self._wait_for_post_backward()
|
||||
(
|
||||
reduce_scatter_input,
|
||||
reduce_scatter_event,
|
||||
self._post_reduce_event,
|
||||
all_reduce_input,
|
||||
all_reduce_event,
|
||||
self._partial_reduce_output,
|
||||
) = foreach_reduce(
|
||||
fsdp_params_with_grad,
|
||||
unsharded_grads,
|
||||
(
|
||||
# pyrefly: ignore [bad-argument-type]
|
||||
self._reduce_scatter_process_group
|
||||
if isinstance(self.mesh_info, FSDPMeshInfo)
|
||||
else None # pyre-fixme[6]
|
||||
),
|
||||
self.comm_ctx.reduce_scatter_stream,
|
||||
self._reduce_scatter_comm,
|
||||
self._orig_dtype,
|
||||
self._reduce_dtype,
|
||||
self.device,
|
||||
self.gradient_divide_factor,
|
||||
(
|
||||
self._all_reduce_process_group
|
||||
if isinstance(self.mesh_info, DDPMeshInfo)
|
||||
else None
|
||||
),
|
||||
all_reduce_stream,
|
||||
self.all_reduce_grads,
|
||||
self._partial_reduce_output,
|
||||
self._all_reduce_hook,
|
||||
self.force_sum_reduction_for_comms,
|
||||
)
|
||||
self.comm_ctx.reduce_scatter_states.append(
|
||||
ReduceScatterState(reduce_scatter_input, reduce_scatter_event)
|
||||
)
|
||||
if all_reduce_input is not None:
|
||||
if self.device.type != "cpu":
|
||||
if all_reduce_event is None:
|
||||
raise AssertionError(
|
||||
"Expected all_reduce_event to be set for non-CPU device"
|
||||
)
|
||||
self._all_reduce_state = AllReduceState(
|
||||
all_reduce_input, all_reduce_event
|
||||
)
|
||||
|
||||
def finalize_backward(self):
|
||||
self._wait_for_post_backward()
|
||||
for fsdp_param in self.fsdp_params:
|
||||
if fsdp_param.grad_offload_event is not None:
|
||||
fsdp_param.grad_offload_event.synchronize()
|
||||
fsdp_param.grad_offload_event = None
|
||||
if self._all_gather_result is not None:
|
||||
# If there was a mistargeted unshard without a corresponding wait,
|
||||
# then we wait here and clear the unshard
|
||||
if (event := self._all_gather_result.all_gather_event) is not None:
|
||||
torch.accelerator.current_stream().wait_event(event)
|
||||
work = self._all_gather_result.all_gather_work
|
||||
if isinstance(work, dist.distributed_c10d.Work):
|
||||
work.wait()
|
||||
self._all_gather_result = None
|
||||
self._post_forward_indices.clear()
|
||||
|
||||
def _wait_for_post_backward(self):
|
||||
if self._post_reduce_event is not None:
|
||||
self.device_handle.current_stream().wait_event(self._post_reduce_event)
|
||||
self._post_reduce_event = None
|
||||
if (
|
||||
self._all_reduce_state is not None
|
||||
and self._all_reduce_state.event is not None
|
||||
):
|
||||
self.device_handle.current_stream().wait_event(self._all_reduce_state.event)
|
||||
self._all_reduce_state = None
|
||||
|
||||
def _backward_prefetch(self) -> None:
|
||||
if self._training_state == TrainingState.PRE_BACKWARD:
|
||||
if not self._post_forward_indices:
|
||||
# Can be cleared if running multiple `backward`s
|
||||
return
|
||||
curr_index = self._post_forward_indices.pop()
|
||||
if self._num_param_groups > 1:
|
||||
# Backward fires groups in reverse forward order:
|
||||
# N-1, N-2, ..., 1, 0. Index 1 is always the
|
||||
# penultimate group regardless of N. Prefetching here
|
||||
# lets the next module's AG overlap with group 0's RS
|
||||
# without holding unsharded params too long (as would
|
||||
# happen if we prefetched from N-1).
|
||||
if self._param_group_index != 1:
|
||||
return
|
||||
# E.g. fully_shard(block, shard_placement_fn=...) creates two
|
||||
# param groups per block (dense + moe), giving
|
||||
# post_forward_order = [block0, block0.moe, block1, block1.moe].
|
||||
# block1.moe walks back past block1 to prefetch block0.moe then block0.
|
||||
curr_modules = self.modules
|
||||
target_modules: tuple[nn.Module, ...] | None = None
|
||||
for step in range(1, curr_index + 1):
|
||||
target = self.comm_ctx.post_forward_order[curr_index - step]
|
||||
if target.modules is curr_modules:
|
||||
continue
|
||||
if target_modules is None:
|
||||
target_modules = target.modules
|
||||
elif target.modules is not target_modules:
|
||||
break
|
||||
# Prefetch all groups of the target module in
|
||||
# reverse forward order (highest index first),
|
||||
# matching the explicit path in _pre_backward.
|
||||
self._prefetch_unshard(target, "backward")
|
||||
elif curr_index > 0:
|
||||
target = self.comm_ctx.post_forward_order[curr_index - 1]
|
||||
self._prefetch_unshard(target, "backward")
|
||||
|
||||
@staticmethod
|
||||
def _prefetch_unshard(
|
||||
target_fsdp_param_group: FSDPParamGroup, pass_type: str
|
||||
) -> None:
|
||||
if pass_type == "backward":
|
||||
training_state = TrainingState.PRE_BACKWARD
|
||||
elif pass_type == "forward":
|
||||
training_state = TrainingState.FORWARD
|
||||
else:
|
||||
raise ValueError(f"Unknown pass type: {pass_type}")
|
||||
target_fqn = target_fsdp_param_group._module_fqn
|
||||
with (
|
||||
record_function(f"FSDP::{pass_type}_prefetch for {target_fqn}"),
|
||||
target_fsdp_param_group.use_training_state(training_state),
|
||||
):
|
||||
async_op = target_fsdp_param_group.unshard_async_op
|
||||
target_fsdp_param_group.unshard(async_op)
|
||||
|
||||
# Utilities #
|
||||
def _to_sharded(self):
|
||||
if not self.is_sharded:
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.to_sharded()
|
||||
self._sharded_state = ShardedState.SHARDED
|
||||
|
||||
def _to_sharded_post_forward(self):
|
||||
if not self.is_sharded_post_forward:
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.to_sharded_post_forward()
|
||||
self._sharded_state = ShardedState.SHARDED_POST_FORWARD
|
||||
|
||||
def _to_unsharded(self):
|
||||
if not self.is_unsharded:
|
||||
for fsdp_param in self.fsdp_params:
|
||||
fsdp_param.to_unsharded()
|
||||
self._sharded_state = ShardedState.UNSHARDED
|
||||
|
||||
@property
|
||||
def is_sharded(self) -> bool:
|
||||
return self._sharded_state == ShardedState.SHARDED
|
||||
|
||||
@property
|
||||
def is_sharded_post_forward(self) -> bool:
|
||||
return self._sharded_state == ShardedState.SHARDED_POST_FORWARD
|
||||
|
||||
@property
|
||||
def is_unsharded(self) -> bool:
|
||||
return self._sharded_state == ShardedState.UNSHARDED
|
||||
|
||||
@contextlib.contextmanager
|
||||
def use_training_state(self, training_state: TrainingState):
|
||||
old_training_state = self._training_state
|
||||
self._training_state = training_state
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._training_state = old_training_state
|
||||
|
||||
# Hook Registration #
|
||||
def _register_post_backward_hook(
|
||||
self, args: tuple[Any, ...], kwargs: dict[str, Any]
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
if not torch.is_grad_enabled():
|
||||
return args, kwargs
|
||||
|
||||
# Collect all tensors that require gradients (including from dataclasses)
|
||||
inp_tensors = collect_grad_tensors((args, kwargs))
|
||||
if not inp_tensors:
|
||||
return args, kwargs
|
||||
|
||||
# Apply RegisterPostBackwardFunction to all tensors at once
|
||||
out_tensors = RegisterPostBackwardFunction.apply(self, *inp_tensors)
|
||||
|
||||
# Replace tensors in the structure (iterator order matches collect order)
|
||||
new_args, new_kwargs = replace_grad_tensors((args, kwargs), iter(out_tensors))
|
||||
return new_args, new_kwargs
|
||||
|
||||
def _register_state_dict_hooks(self) -> None:
|
||||
num_pre_save_hooks = len(self._module_to_pre_save_state_dict_hook_handle)
|
||||
num_pre_load_hooks = len(self._module_to_pre_load_state_dict_hook_handle)
|
||||
if num_pre_save_hooks != num_pre_load_hooks:
|
||||
raise AssertionError(
|
||||
f"Pre-save: {num_pre_save_hooks} pre-load: {num_pre_load_hooks}"
|
||||
)
|
||||
if num_pre_save_hooks > 0:
|
||||
return # already registered
|
||||
modules_with_fsdp_params: set[nn.Module] = {
|
||||
fsdp_param._module_info.module for fsdp_param in self.fsdp_params
|
||||
}
|
||||
|
||||
def to_sharded_hook(*args: Any, **kwargs: Any) -> None:
|
||||
self._to_sharded()
|
||||
|
||||
for module in modules_with_fsdp_params:
|
||||
self._module_to_pre_save_state_dict_hook_handle[module] = (
|
||||
module.register_state_dict_pre_hook(to_sharded_hook)
|
||||
)
|
||||
self._module_to_pre_load_state_dict_hook_handle[module] = (
|
||||
module._register_load_state_dict_pre_hook(to_sharded_hook)
|
||||
)
|
||||
|
||||
# Properties #
|
||||
@property
|
||||
def _reshard_after_forward(self) -> bool:
|
||||
return self.post_forward_mesh_info is not None
|
||||
|
||||
@property
|
||||
def _use_post_forward_mesh(self) -> bool:
|
||||
return (
|
||||
self._reshard_after_forward
|
||||
and self.mesh_info != self.post_forward_mesh_info
|
||||
)
|
||||
|
||||
@property
|
||||
def _is_hsdp(self) -> bool:
|
||||
return isinstance(self.mesh_info, HSDPMeshInfo)
|
||||
|
||||
@property
|
||||
def _all_gather_process_group(self) -> dist.ProcessGroup:
|
||||
mesh_info = (
|
||||
cast(FSDPMeshInfo, self.post_forward_mesh_info)
|
||||
if self.is_sharded_post_forward
|
||||
else self.mesh_info
|
||||
)
|
||||
if not isinstance(mesh_info, FSDPMeshInfo):
|
||||
raise AssertionError(
|
||||
f"Expected mesh_info to be FSDPMeshInfo, got {type(mesh_info)}"
|
||||
)
|
||||
return mesh_info.shard_process_group
|
||||
|
||||
@property
|
||||
def _reduce_scatter_process_group(self) -> dist.ProcessGroup:
|
||||
if not isinstance(self.mesh_info, FSDPMeshInfo):
|
||||
raise AssertionError(
|
||||
f"Expected mesh_info to be FSDPMeshInfo, got {type(self.mesh_info)}"
|
||||
)
|
||||
return self.mesh_info.shard_process_group
|
||||
|
||||
@property
|
||||
def _all_reduce_process_group(self) -> dist.ProcessGroup:
|
||||
if not isinstance(self.mesh_info, DDPMeshInfo):
|
||||
raise AssertionError(
|
||||
f"Expected mesh_info to be DDPMeshInfo or HSDPMeshInfo, got {type(self.mesh_info)}"
|
||||
)
|
||||
return self.mesh_info.replicate_process_group
|
||||
|
||||
def _with_fqn(self, label: str) -> str:
|
||||
if self._module_fqn:
|
||||
label = f"{label} ({self._module_fqn})"
|
||||
if self._num_param_groups > 1 and isinstance(self.mesh_info, FSDPMeshInfo):
|
||||
label = f"{label} [pg={self.mesh_info.shard_mesh_size}]"
|
||||
return label
|
||||
|
||||
def __repr__(self):
|
||||
return f"FSDPParamGroup(fqn={self._module_fqn})"
|
||||
|
||||
def _validate_no_meta_params(self):
|
||||
param_names_on_meta = [
|
||||
fsdp_param._param_fqn
|
||||
for fsdp_param in self.fsdp_params
|
||||
if fsdp_param.sharded_param.device.type == "meta"
|
||||
]
|
||||
if param_names_on_meta:
|
||||
raise RuntimeError(
|
||||
"FSDP parameters should be materialized from meta device before training, "
|
||||
f"but the following were still on meta device: {param_names_on_meta}\n"
|
||||
"For example, call module.to_empty(device) to materialize to device and "
|
||||
"call module.reset_parameters() on each module to initialize values."
|
||||
)
|
||||
|
||||
def _validate_cpu_offload_params(self):
|
||||
if not isinstance(self.offload_policy, CPUOffloadPolicy):
|
||||
return
|
||||
fsdp_params_not_on_cpu = [
|
||||
fsdp_param
|
||||
for fsdp_param in self.fsdp_params
|
||||
if fsdp_param.sharded_param.device.type != "cpu"
|
||||
]
|
||||
if fsdp_params_not_on_cpu:
|
||||
raise RuntimeError(
|
||||
"FSDP parameters should be materialized on CPU when enabling CPU offloading. "
|
||||
'For example, load a CPU state dict or call module.to_empty(device="cpu"). '
|
||||
"Found following parameters on non-CPU device: "
|
||||
f"{[(fsdp_param._param_fqn, fsdp_param.sharded_param.device) for fsdp_param in fsdp_params_not_on_cpu]}\n"
|
||||
)
|
||||
|
||||
|
||||
def _get_param_module_infos(
|
||||
params: list[nn.Parameter], modules: tuple[nn.Module, ...]
|
||||
) -> list[ParamModuleInfo]:
|
||||
"""
|
||||
Shared parameter: lin1.weight = lin2.weight
|
||||
Shared module: mlp.lin1 = mlp.lin2
|
||||
We do not remove duplicates when traversing both modules and parameters to
|
||||
find shared modules' parameters and shared parameters within a module.
|
||||
"""
|
||||
params_set = set(params)
|
||||
param_to_module_info: dict[nn.Parameter, ParamModuleInfo] = {}
|
||||
for module in modules:
|
||||
for _, submodule in module.named_modules(remove_duplicate=False):
|
||||
for param_name, param in _named_parameters_with_duplicates(
|
||||
submodule, recurse=False
|
||||
):
|
||||
if param in params_set:
|
||||
if param not in param_to_module_info:
|
||||
param_to_module_info[param] = ParamModuleInfo(
|
||||
submodule, param_name
|
||||
)
|
||||
else:
|
||||
param_to_module_info[param].shared_modules.append(submodule)
|
||||
param_to_module_info[param].shared_param_names.append(
|
||||
param_name
|
||||
)
|
||||
if len(param_to_module_info) != len(params):
|
||||
raise AssertionError(f"Some parameters are not in the module tree of {modules}")
|
||||
return [param_to_module_info[param] for param in params]
|
||||
|
||||
|
||||
class RegisterPostBackwardFunction(torch.autograd.Function):
|
||||
@staticmethod
|
||||
# pyrefly: ignore [bad-override]
|
||||
def forward(ctx, param_group: FSDPParamGroup, *inputs: torch.Tensor):
|
||||
# All tensors in `inputs` should require gradient
|
||||
ctx.param_group = param_group
|
||||
return inputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *grads: torch.Tensor):
|
||||
ctx.param_group.post_backward()
|
||||
return (None,) + grads
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
# mypy: allow-untyped-decorators
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeVar
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch._logging import warning_once
|
||||
from torch.autograd import Variable
|
||||
from torch.autograd.graph import _MultiHandle
|
||||
from torch.distributed._composable_state import (
|
||||
_get_module_state,
|
||||
_insert_module_state,
|
||||
_State,
|
||||
)
|
||||
from torch.distributed.device_mesh import _get_device_handle
|
||||
from torch.distributed.fsdp._common_utils import collect_grad_tensors
|
||||
from torch.distributed.utils import _apply_to_tensors, _to_kwargs
|
||||
|
||||
from ._fsdp_api import MixedPrecisionPolicy
|
||||
from ._fsdp_common import _cast_fp_tensor, _dynamo_disable, TrainingState
|
||||
from ._fsdp_param_group import FSDPCommContext, FSDPParamGroup
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._fsdp_param import FSDPParam
|
||||
|
||||
|
||||
logger = logging.getLogger("torch.distributed.fsdp.fully_shard")
|
||||
|
||||
_StateType = TypeVar("_StateType", bound="FSDPState")
|
||||
|
||||
|
||||
class FSDPStateContext(Generic[_StateType]):
|
||||
"""This has state shared across FSDP states."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# All FSDP states in the root state's module tree
|
||||
self.all_states: list[_StateType] = []
|
||||
# Iteration's forward root runs the once-per-forward logic; this root
|
||||
# may not be the overall root set by lazy initialization in cases where
|
||||
# only a submodule runs forward (e.g. encoder-only for eval)
|
||||
self.iter_forward_root: _StateType | None = None
|
||||
# Final callback should only be queued once per backward
|
||||
self.post_backward_final_callback_queued: bool = False
|
||||
# Whether to finalize backward in this backward's final callback
|
||||
self.is_last_backward: bool = True
|
||||
# Optional user-provided event recorded after optimizer for the
|
||||
# all-gather streams to wait on in the root pre-forward
|
||||
self.post_optim_event: torch.Event | None = None
|
||||
|
||||
|
||||
class FSDPState(_State):
|
||||
# Name used in error messages; subclasses can override
|
||||
_state_name: str = "FSDP"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# Support multiple param groups for per-param mesh support.
|
||||
# Each group has params with the same mesh_info.
|
||||
self._fsdp_param_groups: list[FSDPParamGroup] = []
|
||||
self._is_root: bool | None = None # root set during lazy init
|
||||
self._state_ctx = FSDPStateContext()
|
||||
self._comm_ctx = FSDPCommContext()
|
||||
self._training_state: TrainingState = TrainingState.IDLE
|
||||
self._states_to_forward_prefetch: list[FSDPState] = []
|
||||
self._states_to_backward_prefetch: list[FSDPState] = []
|
||||
self._modules_to_run_forward: set[nn.Module] = set()
|
||||
# ``False`` when user set reshard_after_forward
|
||||
# through ``fully_shard`` or ``set_reshard_after_forward``
|
||||
self._auto_reshard_after_forward: bool | None = True
|
||||
|
||||
def _get_state_for_module(self, module: nn.Module) -> "FSDPState | None":
|
||||
"""Get the state for a module. Subclasses can override to use different state getters."""
|
||||
return _get_module_fsdp_state(module)
|
||||
|
||||
@property
|
||||
def _fsdp_param_group(self) -> FSDPParamGroup | None:
|
||||
"""
|
||||
Returns the param group for backward compatibility.
|
||||
This property is only valid when there is at most one param group.
|
||||
For per-param mesh support with multiple param groups, use
|
||||
``_fsdp_param_groups`` instead.
|
||||
"""
|
||||
if len(self._fsdp_param_groups) > 1:
|
||||
group_fqns = [g._module_fqn for g in self._fsdp_param_groups]
|
||||
raise AssertionError(
|
||||
f"Expected at most 1 param group for backward compatibility, "
|
||||
f"but got {len(self._fsdp_param_groups)} (fqns: {group_fqns}). "
|
||||
f"Use `_fsdp_param_groups` (plural) to access all param groups "
|
||||
f"when using per-param mesh via shard_placement_fn returning "
|
||||
f"ShardPlacementResult."
|
||||
)
|
||||
if self._fsdp_param_groups:
|
||||
return self._fsdp_param_groups[0]
|
||||
return None
|
||||
|
||||
# Define a separate init since `__init__` is called in the contract
|
||||
def init(
|
||||
self,
|
||||
modules: tuple[nn.Module, ...],
|
||||
device: torch.device,
|
||||
mp_policy: MixedPrecisionPolicy,
|
||||
auto_reshard_after_forward: bool,
|
||||
) -> None:
|
||||
for module in modules:
|
||||
_insert_module_state(module, self)
|
||||
self._modules = modules
|
||||
self._device = device
|
||||
self._device_handle = _get_device_handle(device.type)
|
||||
self._mp_policy = mp_policy
|
||||
self._auto_reshard_after_forward = auto_reshard_after_forward
|
||||
if len(modules) == 1:
|
||||
self._pre_forward_hook_handle = modules[0].register_forward_pre_hook(
|
||||
self._pre_forward, prepend=True, with_kwargs=True
|
||||
)
|
||||
self._post_forward_hook_handle = modules[0].register_forward_hook(
|
||||
self._post_forward, prepend=False
|
||||
)
|
||||
else:
|
||||
hook_handle = _register_group_forward_hooks(
|
||||
modules,
|
||||
self._pre_forward,
|
||||
self._post_forward,
|
||||
self._modules_to_run_forward,
|
||||
)
|
||||
self._pre_forward_hook_handle = hook_handle
|
||||
self._post_forward_hook_handle = hook_handle
|
||||
|
||||
def _root_pre_forward(
|
||||
self, module: nn.Module, args: tuple[Any, ...], kwargs: dict[str, Any]
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
self._lazy_init()
|
||||
if self._state_ctx.iter_forward_root is not None:
|
||||
return args, kwargs
|
||||
logger.debug("FSDP::root_pre_forward")
|
||||
self._state_ctx.iter_forward_root = self
|
||||
with torch.profiler.record_function("FSDP::root_pre_forward"):
|
||||
# Wait for optimizer before implicitly prefetched all-gathers
|
||||
if (event := self._state_ctx.post_optim_event) is not None:
|
||||
self._comm_ctx.all_gather_copy_in_stream.wait_event(event)
|
||||
self._comm_ctx.all_gather_stream.wait_event(event)
|
||||
self._state_ctx.post_optim_event = None
|
||||
else:
|
||||
current_stream = self._device_handle.current_stream()
|
||||
self._comm_ctx.all_gather_copy_in_stream.wait_stream(current_stream)
|
||||
self._comm_ctx.all_gather_stream.wait_stream(current_stream)
|
||||
if self._device.type in [
|
||||
"cuda",
|
||||
"hpu",
|
||||
"xpu",
|
||||
"mtia",
|
||||
torch._C._get_privateuse1_backend_name(),
|
||||
]:
|
||||
with torch.profiler.record_function("FSDP::inputs_to_device"):
|
||||
args_tuple, kwargs_tuple = _to_kwargs(
|
||||
args, kwargs, self._device, False
|
||||
) # same as DDP
|
||||
args, kwargs = args_tuple[0], kwargs_tuple[0]
|
||||
return args, kwargs
|
||||
|
||||
def _lazy_init(self) -> None:
|
||||
"""
|
||||
Lazy initialization represents when all modules' parallelisms have
|
||||
finalized (e.g. FSDP has been applied to all desired modules). This
|
||||
means that we can determine which state is the root, and we do so by
|
||||
the 1st state to run forward.
|
||||
"""
|
||||
if self._is_root is not None:
|
||||
return # no-op: already initialized
|
||||
self._is_root = True
|
||||
if len(self._modules) > 1:
|
||||
raise RuntimeError(
|
||||
f"{self._state_name} requires a single root module but got {self._modules}"
|
||||
)
|
||||
root_module = self._modules[0]
|
||||
visited_states: set[FSDPState] = set()
|
||||
for module_name, module in root_module.named_modules():
|
||||
if (state := self._get_state_for_module(module)) is None:
|
||||
continue
|
||||
if module is not root_module:
|
||||
if state not in visited_states and state._is_root is not None:
|
||||
raise RuntimeError(
|
||||
f"{self._state_name} state has already been lazily initialized for "
|
||||
f"{module_name}\n{self._state_name} requires running forward through "
|
||||
"the root module first"
|
||||
)
|
||||
state._is_root = False
|
||||
# A single state can map to multiple modules (e.g.
|
||||
# fully_shard([mod_a, mod_b, mod_c])), so dedup here.
|
||||
if state not in visited_states:
|
||||
self._state_ctx.all_states.append(state)
|
||||
visited_states.add(state)
|
||||
# For the root, do not reshard after forward since for training,
|
||||
# the parameters would be freed and all-gathered immediately
|
||||
if self._auto_reshard_after_forward:
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
fsdp_param_group.post_forward_mesh_info = None
|
||||
self._init_fqns()
|
||||
self._init_shared_state()
|
||||
self._validate_no_duplicate_params()
|
||||
# Run parameter group lazy inits after initializing FQNs for improved
|
||||
# error messages
|
||||
for state in self._state_ctx.all_states:
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.lazy_init()
|
||||
|
||||
def _validate_no_duplicate_params(self) -> None:
|
||||
seen: set[int] = set()
|
||||
for state in self._state_ctx.all_states:
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
for fsdp_param in fsdp_param_group.fsdp_params:
|
||||
if fsdp_param._orig_param_uid in seen:
|
||||
raise ValueError(
|
||||
f"Parameter '{fsdp_param._param_fqn}' is shared with a "
|
||||
f"parameter already managed by another FSDP group. "
|
||||
f"For shared/tied parameters, use "
|
||||
f"fully_shard([module_a, module_b]) to place them in "
|
||||
f"the same FSDP group."
|
||||
)
|
||||
seen.add(fsdp_param._orig_param_uid)
|
||||
|
||||
def _init_shared_state(self) -> None:
|
||||
self._comm_ctx.lazy_init(self._device)
|
||||
for state in self._state_ctx.all_states:
|
||||
state._state_ctx = self._state_ctx
|
||||
state._comm_ctx = self._comm_ctx
|
||||
num_groups = len(state._fsdp_param_groups)
|
||||
for i, fsdp_param_group in enumerate(state._fsdp_param_groups):
|
||||
fsdp_param_group.comm_ctx = self._comm_ctx
|
||||
fsdp_param_group._param_group_index = i
|
||||
fsdp_param_group._num_param_groups = num_groups
|
||||
|
||||
def _init_fqns(self) -> None:
|
||||
"""Sets module and parameter FQN attributes for debugging."""
|
||||
if not self._is_root:
|
||||
raise AssertionError("Expected _is_root to be True")
|
||||
root_module = self._modules[0]
|
||||
param_to_fsdp_param: dict[nn.Parameter, FSDPParam] = {}
|
||||
# Build a mapping from module to all its FSDPParamGroups (not just one)
|
||||
module_to_fsdp_param_groups: dict[nn.Module, list[FSDPParamGroup]] = {}
|
||||
for state in self._state_ctx.all_states:
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
for fsdp_param in fsdp_param_group.fsdp_params:
|
||||
param_to_fsdp_param[fsdp_param.sharded_param] = fsdp_param
|
||||
for module in fsdp_param_group.modules:
|
||||
if module not in module_to_fsdp_param_groups:
|
||||
module_to_fsdp_param_groups[module] = []
|
||||
module_to_fsdp_param_groups[module].append(fsdp_param_group)
|
||||
for param_name, param in root_module.named_parameters():
|
||||
if param in param_to_fsdp_param:
|
||||
param_to_fsdp_param[param]._param_fqn = param_name
|
||||
for module_name, module in root_module.named_modules():
|
||||
if module in module_to_fsdp_param_groups:
|
||||
# Set FQN for all param groups associated with this module
|
||||
for fsdp_param_group in module_to_fsdp_param_groups[module]:
|
||||
module_fqn = fsdp_param_group._module_fqn
|
||||
if module_fqn is None:
|
||||
fsdp_param_group._module_fqn = module_name
|
||||
else:
|
||||
if not isinstance(module_fqn, str):
|
||||
raise AssertionError(
|
||||
f"Expected module_fqn to be str, got {type(module_fqn)}: {module_fqn}"
|
||||
)
|
||||
module_fqn += f", {module_name}"
|
||||
fsdp_param_group._module_fqn = module_fqn
|
||||
|
||||
@_dynamo_disable
|
||||
def _pre_forward(
|
||||
self, module: nn.Module, args: tuple[Any, ...], kwargs: dict[str, Any]
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
# When composing with module-hook-based activation checkpointing, the
|
||||
# pre-backward hook is responsible for the unshard
|
||||
if self._training_state == TrainingState.PRE_BACKWARD:
|
||||
# With nested FSDP and multiple forward passes before backward,
|
||||
# the params might have been resharded by a previous post_backward.
|
||||
# We need to ensure params are unsharded for AC recomputation.
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
if not fsdp_param_group.is_unsharded:
|
||||
fsdp_param_group.unshard()
|
||||
fsdp_param_group.wait_for_unshard()
|
||||
return args, kwargs
|
||||
self._training_state = TrainingState.FORWARD
|
||||
args, kwargs = self._root_pre_forward(module, args, kwargs)
|
||||
if self._mp_policy.cast_forward_inputs and self._mp_policy.param_dtype:
|
||||
with torch.profiler.record_function("FSDP::cast_forward_inputs"):
|
||||
cast_fn = functools.partial(
|
||||
_cast_fp_tensor, self._mp_policy.param_dtype
|
||||
)
|
||||
args, kwargs = (
|
||||
_apply_to_tensors(cast_fn, args),
|
||||
_apply_to_tensors(cast_fn, kwargs),
|
||||
)
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
args, kwargs = fsdp_param_group.pre_forward(module, args, kwargs)
|
||||
for fsdp_state in self._states_to_forward_prefetch:
|
||||
# Forward order (not reversed) to match forward execution order;
|
||||
# contrast with reversed() in _pre_backward for backward order.
|
||||
for target_param_group in fsdp_state._fsdp_param_groups:
|
||||
FSDPParamGroup._prefetch_unshard(target_param_group, "forward")
|
||||
return args, kwargs
|
||||
|
||||
@_dynamo_disable
|
||||
def _post_forward(self, module: nn.Module, input: Any, output: Any) -> Any:
|
||||
# When composing with module-hook-based activation checkpointing, the
|
||||
# post-backward hook is responsible for the reshard
|
||||
if self._training_state == TrainingState.PRE_BACKWARD:
|
||||
return output
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
output = fsdp_param_group.post_forward(module, input, output)
|
||||
output = self._register_pre_backward_hook(output)
|
||||
self._training_state = TrainingState.IDLE
|
||||
if self._state_ctx.iter_forward_root is self:
|
||||
if all_gather_state := self._comm_ctx.all_gather_state:
|
||||
# Free the last all-gather result if needed; refer to
|
||||
# [Note: Overlapping all-gather copy-in and all-gather]
|
||||
self._comm_ctx.all_gather_copy_in_stream.wait_event(
|
||||
all_gather_state.event
|
||||
)
|
||||
self._comm_ctx.all_gather_stream.wait_event(all_gather_state.event)
|
||||
self._comm_ctx.all_gather_state = None # free the all-gather result
|
||||
self._state_ctx.iter_forward_root = None
|
||||
if self._mp_policy.output_dtype is not None:
|
||||
with torch.profiler.record_function("FSDP::cast_forward_outputs"):
|
||||
output = _apply_to_tensors(
|
||||
functools.partial(_cast_fp_tensor, self._mp_policy.output_dtype),
|
||||
output,
|
||||
)
|
||||
return output
|
||||
|
||||
@_dynamo_disable
|
||||
def _pre_backward(self, grad: torch.Tensor) -> torch.Tensor:
|
||||
self._training_state = TrainingState.PRE_BACKWARD
|
||||
self._register_root_post_backward_final_callback()
|
||||
default_prefetch = len(self._states_to_backward_prefetch) == 0
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
fsdp_param_group.pre_backward(default_prefetch)
|
||||
for fsdp_state in self._states_to_backward_prefetch:
|
||||
# Reverse so higher-indexed groups are prefetched first,
|
||||
# matching backward execution order (reverse of forward).
|
||||
for target_param_group in reversed(fsdp_state._fsdp_param_groups):
|
||||
FSDPParamGroup._prefetch_unshard(target_param_group, "backward")
|
||||
return grad
|
||||
|
||||
@_dynamo_disable
|
||||
def _root_post_backward_final_callback(self) -> None:
|
||||
logger.debug("FSDP::root_post_backward")
|
||||
with torch.profiler.record_function("FSDP::root_post_backward_callback"):
|
||||
for state in self._state_ctx.all_states:
|
||||
# Reverse so that the last param group (which gates the
|
||||
# reduce-scatter wait/clear) fires first, matching the
|
||||
# autograd backward order and preserving RS overlap for
|
||||
# per-param-mesh modules whose inputs lack gradients.
|
||||
for fsdp_param_group in reversed(state._fsdp_param_groups):
|
||||
if fsdp_param_group._training_state != TrainingState.POST_BACKWARD:
|
||||
# Run post-backward in case forward inputs did not require
|
||||
# gradient so the autograd backward did not run
|
||||
fsdp_param_group.post_backward()
|
||||
fsdp_param_group._training_state = TrainingState.IDLE
|
||||
state._training_state = TrainingState.IDLE
|
||||
if self._state_ctx.is_last_backward:
|
||||
state._finalize_backward()
|
||||
if self._state_ctx.is_last_backward:
|
||||
self._comm_ctx.post_forward_order.clear()
|
||||
# Catch the last module's RS states that no subsequent
|
||||
# module's group N-1 wait will clear.
|
||||
for rs_state in self._comm_ctx.reduce_scatter_states:
|
||||
if rs_state.event is not None:
|
||||
self._device_handle.current_stream().wait_event(rs_state.event)
|
||||
self._comm_ctx.reduce_scatter_states.clear()
|
||||
self._state_ctx.post_backward_final_callback_queued = False
|
||||
|
||||
def _finalize_backward(self) -> None:
|
||||
if self._modules_to_run_forward:
|
||||
msg = (
|
||||
f"{len(self._modules_to_run_forward)} of the {len(self._modules)} "
|
||||
f"modules passed to fully_shard did not run forward before backward, "
|
||||
"which is error-prone since FSDP post-forward/pre-backward logic "
|
||||
"will not run for these modules. We recommend passing only modules "
|
||||
"that run forward together. Modules that did not run forward: "
|
||||
f"{list(self._modules_to_run_forward)}"
|
||||
)
|
||||
warning_once(logger, msg, stacklevel=2)
|
||||
# Clear since we want the next forward to run
|
||||
self._modules_to_run_forward.clear()
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
fsdp_param_group.finalize_backward()
|
||||
|
||||
def _register_pre_backward_hook(self, output: Any) -> Any:
|
||||
if not torch.is_grad_enabled():
|
||||
return output
|
||||
# output is the forward return value — pass directly without wrapping
|
||||
# (unlike _register_post_backward_hook which wraps (args, kwargs))
|
||||
tensors = collect_grad_tensors(output)
|
||||
for t in tensors:
|
||||
t.register_hook(self._pre_backward)
|
||||
return output
|
||||
|
||||
def _register_root_post_backward_final_callback(self):
|
||||
if self._state_ctx.post_backward_final_callback_queued:
|
||||
return
|
||||
self._state_ctx.post_backward_final_callback_queued = True
|
||||
Variable._execution_engine.queue_callback(
|
||||
self._root_post_backward_final_callback
|
||||
)
|
||||
|
||||
|
||||
def _get_module_fsdp_state(module: nn.Module) -> FSDPState | None:
|
||||
state = _get_module_state(module)
|
||||
if isinstance(state, FSDPState):
|
||||
return state
|
||||
return None
|
||||
|
||||
|
||||
def _register_group_forward_hooks(
|
||||
modules: Sequence[nn.Module],
|
||||
pre_hook: Callable,
|
||||
post_hook: Callable,
|
||||
modules_to_run: set[nn.Module],
|
||||
):
|
||||
"""
|
||||
Registers group forward pre and post-hooks. The pre-hook runs upon the
|
||||
first module pre-forward, and the post-hook runs upon the last. If at least
|
||||
one module does not run forward, then the post-hook does not run.
|
||||
"""
|
||||
modules_set = set(modules)
|
||||
|
||||
@_dynamo_disable
|
||||
@functools.wraps(pre_hook)
|
||||
def wrapped_pre_hook(*args: Any, **kwargs: Any):
|
||||
if len(modules_to_run) == 0: # first to run
|
||||
modules_to_run.update(modules_set)
|
||||
return pre_hook(*args, **kwargs)
|
||||
|
||||
@_dynamo_disable
|
||||
def get_wrapped_post_hook(module: nn.Module):
|
||||
@functools.wraps(post_hook)
|
||||
def wrapped_post_hook(*args: Any, **kwargs: Any):
|
||||
modules_to_run.discard(module)
|
||||
if len(modules_to_run) == 0:
|
||||
return post_hook(*args, **kwargs)
|
||||
|
||||
return wrapped_post_hook
|
||||
|
||||
pre_handles = [
|
||||
module.register_forward_pre_hook(
|
||||
wrapped_pre_hook, prepend=True, with_kwargs=True
|
||||
)
|
||||
for module in modules
|
||||
]
|
||||
post_handles = [
|
||||
module.register_forward_hook(
|
||||
get_wrapped_post_hook(module), prepend=False, always_call=True
|
||||
)
|
||||
for module in modules
|
||||
]
|
||||
return _MultiHandle(tuple(pre_handles + post_handles))
|
||||
+828
@@ -0,0 +1,828 @@
|
||||
# mypy: allow-untyped-decorators
|
||||
# mypy: allow-untyped-defs
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, cast, Literal, NoReturn, overload, TYPE_CHECKING
|
||||
from typing_extensions import deprecated
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.distributed._composable import contract
|
||||
|
||||
from ._fsdp_api import (
|
||||
AllGather,
|
||||
DataParallelMeshDims,
|
||||
MixedPrecisionPolicy,
|
||||
OffloadPolicy,
|
||||
ReduceScatter,
|
||||
)
|
||||
from ._fsdp_common import FSDPMeshInfo, ShardPlacementFnResult
|
||||
from ._fsdp_init import (
|
||||
_apply_to_module,
|
||||
_get_device_from_mesh,
|
||||
_get_mesh_info,
|
||||
_get_modules_and_states,
|
||||
_get_post_forward_mesh_info,
|
||||
_init_default_mesh,
|
||||
_init_param_group,
|
||||
_validate_mesh,
|
||||
_validate_module,
|
||||
)
|
||||
from ._fsdp_state import _get_module_fsdp_state, FSDPState
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable, Iterator
|
||||
|
||||
from torch.distributed.tensor import DeviceMesh
|
||||
|
||||
from ._fsdp_param_group import FSDPParamGroup
|
||||
|
||||
__all__ = [
|
||||
"fully_shard",
|
||||
"FSDPModule",
|
||||
"UnshardHandle",
|
||||
"register_fsdp_forward_method",
|
||||
"get_cls_to_fsdp_cls",
|
||||
"disable_fsdp_module_new_init",
|
||||
"share_comm_ctx",
|
||||
]
|
||||
|
||||
|
||||
cls_to_fsdp_cls: dict[type, type] = {}
|
||||
|
||||
|
||||
def get_cls_to_fsdp_cls() -> dict[type, type]:
|
||||
return cls_to_fsdp_cls
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def fully_shard(
|
||||
module: nn.Module,
|
||||
*,
|
||||
mesh: DeviceMesh | None = ...,
|
||||
reshard_after_forward: bool | int | None = ...,
|
||||
shard_placement_fn: Callable[[nn.Parameter], ShardPlacementFnResult] | None = ...,
|
||||
mp_policy: MixedPrecisionPolicy = ...,
|
||||
offload_policy: OffloadPolicy = ...,
|
||||
ignored_params: set[nn.Parameter] | None = ...,
|
||||
dp_mesh_dims: DataParallelMeshDims | None = ...,
|
||||
) -> FSDPModule: ...
|
||||
|
||||
|
||||
@overload
|
||||
# pyrefly: ignore [inconsistent-overload]
|
||||
def fully_shard(
|
||||
module: list[nn.Module],
|
||||
*,
|
||||
mesh: DeviceMesh | None = ...,
|
||||
reshard_after_forward: bool | int | None = ...,
|
||||
shard_placement_fn: Callable[[nn.Parameter], ShardPlacementFnResult] | None = ...,
|
||||
mp_policy: MixedPrecisionPolicy = ...,
|
||||
offload_policy: OffloadPolicy = ...,
|
||||
ignored_params: set[nn.Parameter] | None = ...,
|
||||
dp_mesh_dims: DataParallelMeshDims | None = ...,
|
||||
) -> list[FSDPModule]: ...
|
||||
|
||||
|
||||
# The decorator adds a state object to `module` that can be accessed via
|
||||
# `fully_shard.state(module)`. The state object and module are 1:1.
|
||||
# [1] Python runtime decorator does not play well with static type checking
|
||||
# so suppressing some type checks to support type overloads
|
||||
# such that caller can still get correct return types based on input type
|
||||
@contract(state_cls=FSDPState) # type: ignore[misc] # see [1]
|
||||
def fully_shard(
|
||||
module,
|
||||
*,
|
||||
mesh: DeviceMesh | None = None,
|
||||
reshard_after_forward: bool | int | None = None,
|
||||
shard_placement_fn: Callable[[nn.Parameter], ShardPlacementFnResult] | None = None,
|
||||
mp_policy: MixedPrecisionPolicy = MixedPrecisionPolicy(),
|
||||
offload_policy: OffloadPolicy = OffloadPolicy(),
|
||||
ignored_params: set[nn.Parameter] | None = None,
|
||||
dp_mesh_dims: DataParallelMeshDims | None = None,
|
||||
):
|
||||
"""
|
||||
Apply fully sharded data parallelism (FSDP) to ``module``, where FSDP
|
||||
shards module parameters, gradients, and optimizer states across data
|
||||
parallel workers to save memory at the cost of communication.
|
||||
|
||||
At initialization, FSDP shards the module's parameters across the data
|
||||
parallel workers given by ``mesh``. Before forward, FSDP all-gathers the
|
||||
sharded parameters across the data-parallel workers to get the unsharded
|
||||
parameters for forward computation. If ``reshard_after_forward`` is
|
||||
``True``, then FSDP frees the unsharded parameters after forward and
|
||||
re-all-gathers them in backward before gradient computation. After gradient
|
||||
computation, FSDP frees the unsharded parameters and reduce-scatters the
|
||||
unsharded gradients across data-parallel workers.
|
||||
|
||||
This implementation represents the sharded parameters as :class:`DTensor` s
|
||||
sharded on dim-0, while the unsharded parameters will be like the original
|
||||
parameters on ``module`` (e.g. :class:`torch.Tensor` if originally
|
||||
:class:`torch.Tensor`). A module
|
||||
`forward pre-hook <https://pytorch.org/docs/main/generated/torch.nn.Module.html#torch.nn.Module.register_forward_pre_hook>`_
|
||||
on ``module`` all-gathers the parameters, and a module
|
||||
`forward hook <https://pytorch.org/docs/main/generated/torch.nn.Module.html#torch.nn.Module.register_forward_hook>`_
|
||||
on ``module`` frees them (if needed). Similar backward hooks all-gather
|
||||
parameters and later free parameters and reduce-scatter gradients.
|
||||
|
||||
Since grouping multiple tensors together for one collective is critical for
|
||||
communication efficiency, this implementation makes this grouping first
|
||||
class. Calling :meth:`fully_shard` on ``module`` constructs one group that
|
||||
includes the parameters in ``module.parameters()`` except those already
|
||||
assigned to a group from an earlier call on a submodule. This means that
|
||||
:meth:`fully_shard` should be called bottom-up on your model. Each group's
|
||||
parameters are all-gathered in one collective, and its gradients are
|
||||
reduce-scattered in one collective. Partitioning the model into multiple
|
||||
groups ("layer by layer") allows for peak memory savings and communication/computation
|
||||
overlap. Users generally should *not* call :meth:`fully_shard` only on the
|
||||
topmost root module.
|
||||
|
||||
Args:
|
||||
module (Union[nn.Module, List[nn.Module]): The module or modules to
|
||||
shard with FSDP and group together for communication.
|
||||
mesh (Optional[DeviceMesh]): This data parallel mesh defines the
|
||||
sharding and device. If 1D, then parameters are fully sharded
|
||||
across the 1D mesh (FSDP) with ``(Shard(0),)`` placement. If 2D,
|
||||
then parameters are sharded across the 1st dim and replicated
|
||||
across the 0th dim (HSDP) with ``(Replicate(), Shard(0))``
|
||||
placement. The mesh's device type gives the device type used for
|
||||
communication; if a CUDA or CUDA-like device type, then we use the
|
||||
current device.
|
||||
reshard_after_forward (Optional[Union[bool, int]]): This controls the parameter
|
||||
behavior after forward and can trade off memory and communication:
|
||||
|
||||
- If ``True``, then this reshards parameters after forward and
|
||||
re-all-gathers in backward.
|
||||
- If ``False``, then this keeps the unsharded parameters in memory
|
||||
after forward and avoids the all-gather in backward. For best performance,
|
||||
we usually set ``False`` for the root module, because the root module
|
||||
is typically required immediately when the backward pass begins.
|
||||
- If ``None``, it is set to ``True`` for non-root modules and ``False``
|
||||
for root modules.
|
||||
- If an ``int``, then this represents the world size to reshard to
|
||||
after forward. It should be a non-trivial divisor of the ``mesh``
|
||||
shard dim size (i.e. excluding 1 and the dim size itself). A
|
||||
choice may be the intra-node size (e.g. ``torch.cuda.device_count()``).
|
||||
This allows the all-gather in backward to be over a smaller world
|
||||
size at the cost of higher memory usage than setting to ``True``.
|
||||
- After forward, the parameters registered to the module depend on
|
||||
to this: The registered parameters are the sharded parameters if
|
||||
``True``; unsharded parameters if ``False``; and the parameters
|
||||
resharded to the smaller mesh otherwise. To modify the parameters
|
||||
between forward and backward, the registered parameters must be
|
||||
the sharded parameters. For ``False`` or an ``int``, this can be
|
||||
done by manually resharding via :meth:`reshard`.
|
||||
shard_placement_fn (Optional[Callable[[nn.Parameter], Optional[Shard | ShardPlacementResult]]]):
|
||||
This callable can be used to override the sharding placement and/or
|
||||
mesh for a parameter. It can return:
|
||||
|
||||
- ``None``: Use default sharding (Shard(0)) on the mesh passed to
|
||||
``fully_shard``.
|
||||
- :class:`Shard`: Shard the parameter on the specified dimension
|
||||
using the mesh passed to ``fully_shard``.
|
||||
- :class:`ShardPlacementResult`: Specify both the shard placement
|
||||
and a custom :class:`FSDPMeshInfo`. This allows different
|
||||
parameters to be sharded across different process groups, enabling
|
||||
use cases like Mixture of Experts where expert params use a
|
||||
different mesh than regular params.
|
||||
|
||||
If sharding on a nonzero dim, we currently require even sharding,
|
||||
i.e. the tensor dim size on that dim must be divisible by the FSDP
|
||||
shard mesh size.
|
||||
mp_policy (MixedPrecisionPolicy): This controls the mixed precision
|
||||
policy, which offers parameter/reduction mixed precision for this
|
||||
module. See :class:`MixedPrecisionPolicy` for details.
|
||||
offload_policy (OffloadPolicy): This controls the offloading policy,
|
||||
which offers parameter/gradient/optimizer state offloading. See
|
||||
:class:`OffloadPolicy` and its subclasses for details.
|
||||
ignored_params: Optional(Set[nn.Parameter]): The set of parameters to be
|
||||
ignored by FSDP. They will not be sharded, nor moved to the device
|
||||
during init, nor have their gradients reduced in backward.
|
||||
dp_mesh_dims (Optional[DataParallelMeshDims]): When provided,
|
||||
``mesh`` is treated as the full SPMD mesh, and parameters should be
|
||||
DTensors on this mesh with ``Replicate()`` on all DP dimensions.
|
||||
The ``shard`` field names which dim(s) FSDP shards on (multiple
|
||||
dims are flattened). The ``replicate`` field names the HSDP
|
||||
replication dim(s) (multiple dims are flattened).
|
||||
|
||||
Returns:
|
||||
FSDPModule: The module with FSDP applied (in-place).
|
||||
"""
|
||||
torch._C._log_api_usage_once("torch.distributed.fsdp.fully_shard")
|
||||
_validate_module(module, "fully_shard")
|
||||
mesh = mesh or _init_default_mesh()
|
||||
_validate_mesh(mesh, dp_mesh_dims)
|
||||
mesh_info = _get_mesh_info(mesh, dp_mesh_dims)
|
||||
device = _get_device_from_mesh(mesh)
|
||||
auto_reshard_after_forward = reshard_after_forward is None
|
||||
# If the user does not provide ``reshard_after_forward``, we set it to True.
|
||||
# During lazy_init, we identify which module is the root and override its value to False
|
||||
if isinstance(mesh_info, FSDPMeshInfo):
|
||||
if (
|
||||
mesh_info.is_spmd_mesh
|
||||
and not isinstance(reshard_after_forward, bool)
|
||||
and isinstance(reshard_after_forward, int)
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"reshard_after_forward as int is not yet supported with "
|
||||
"SPMD mesh (dp_mesh_dims)"
|
||||
)
|
||||
post_forward_mesh_info = _get_post_forward_mesh_info(
|
||||
reshard_after_forward if not auto_reshard_after_forward else True, # type: ignore[arg-type]
|
||||
mesh_info,
|
||||
)
|
||||
else:
|
||||
# DDPMeshInfo: no sharding, so no post-forward resharding needed
|
||||
post_forward_mesh_info = None
|
||||
arg_module, modules, managed_modules, params, buffers = _get_modules_and_states(
|
||||
module, device, ignored_params
|
||||
)
|
||||
state = fully_shard.state(modules[0]) # type: ignore[attr-defined]
|
||||
state.init(modules, device, mp_policy, auto_reshard_after_forward)
|
||||
|
||||
_init_param_group(
|
||||
state,
|
||||
params,
|
||||
modules,
|
||||
mesh_info,
|
||||
post_forward_mesh_info,
|
||||
device,
|
||||
shard_placement_fn,
|
||||
mp_policy,
|
||||
offload_policy,
|
||||
reshard_after_forward=reshard_after_forward
|
||||
if not auto_reshard_after_forward
|
||||
else True,
|
||||
)
|
||||
|
||||
# For Dynamo
|
||||
for managed_module in managed_modules:
|
||||
managed_module._is_fsdp_managed_module = True # type: ignore[assignment]
|
||||
managed_module._fsdp_use_orig_params = True # type: ignore[assignment]
|
||||
|
||||
# Place FSDP leftmost for highest priority in the method resolution order
|
||||
_apply_to_module(
|
||||
modules, cls_to_fsdp_cls, FSDPModule, "FSDP", _unimplemented_deepcopy
|
||||
)
|
||||
return arg_module
|
||||
|
||||
|
||||
def _unimplemented_deepcopy(*args: Any, **kwargs: Any) -> NoReturn:
|
||||
raise AssertionError(
|
||||
"FSDP does not support deepcopy. Please use state dict for serialization."
|
||||
)
|
||||
|
||||
|
||||
_enable_fsdp_module_new_init: bool = True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def disable_fsdp_module_new_init() -> Iterator[None]:
|
||||
global _enable_fsdp_module_new_init
|
||||
prev, _enable_fsdp_module_new_init = _enable_fsdp_module_new_init, False
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_enable_fsdp_module_new_init = prev
|
||||
|
||||
|
||||
class FSDPModule:
|
||||
# Index in MRO where the original class is found.
|
||||
# For FSDP: [FSDP<Orig>, FSDPModule, Orig, ...] -> index 2
|
||||
# Subclasses like ReplicateModule override this.
|
||||
_orig_cls_mro_index: int = 2
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
Override ``__new__`` to remove the FSDP class and directly construct
|
||||
the original class for cases like indexing into a container module.
|
||||
"""
|
||||
orig_cls = cls.__mro__[cls._orig_cls_mro_index]
|
||||
self = orig_cls.__new__(orig_cls, *args, **kwargs)
|
||||
if _enable_fsdp_module_new_init:
|
||||
self.__init__(*args, **kwargs)
|
||||
return self
|
||||
|
||||
def reshard(self) -> None:
|
||||
"""
|
||||
Reshards the module's parameters, freeing the unsharded parameters if
|
||||
they are allocated and registering the sharded parameters to the
|
||||
module. This method is *not* recursive.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.reshard()
|
||||
|
||||
def unshard(self, async_op: bool = False) -> UnshardHandle | None:
|
||||
"""
|
||||
Unshards the module's parameters by allocating memory and all-gathering
|
||||
the parameters. This method is *not* recursive. The unshard follows the
|
||||
:class:`MixedPrecisionPolicy`, so it will all-gather following
|
||||
``param_dtype`` if set.
|
||||
|
||||
Args:
|
||||
async_op (bool): If ``True``, then returns a :class:`UnshardHandle`
|
||||
that has a :meth:`wait` method to wait on the unshard op. If
|
||||
``False``, then returns ``None`` and waits on the handle inside
|
||||
this function.
|
||||
|
||||
.. note:: If ``async_op=True``, then FSDP will wait on the pending
|
||||
unshard in the module's pre-forward for the user. The user only
|
||||
needs to call :meth:`wait` explicitly if the wait should happen
|
||||
before pre-forward.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.lazy_init()
|
||||
fsdp_param_group.unshard(async_op=async_op)
|
||||
handle = _UnshardHandleImpl(
|
||||
list(state._fsdp_param_groups) if state._fsdp_param_groups else None
|
||||
)
|
||||
if async_op:
|
||||
return handle
|
||||
handle.wait()
|
||||
return None
|
||||
|
||||
def set_is_last_backward(self, is_last_backward: bool) -> None:
|
||||
"""
|
||||
Sets whether the next backward is the last one. On the last backward,
|
||||
FSDP waits on pending gradient reduction and clears internal data
|
||||
data structures for backward prefetching. This can be useful for
|
||||
microbatching.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
state._state_ctx.is_last_backward = is_last_backward
|
||||
|
||||
def set_requires_gradient_sync(
|
||||
self, requires_gradient_sync: bool, *, recurse: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Sets if the module should sync gradients. This can be used to implement
|
||||
gradient accumulation *without communication*. For HSDP, this controls
|
||||
both reduce-scatter and all-reduce together. This is the equivalence of
|
||||
`no_sync` in FSDP1.
|
||||
|
||||
Args:
|
||||
requires_gradient_sync (bool): Whether to reduce gradients for the
|
||||
module's parameters.
|
||||
recurse (bool): Whether to set for all FSDP submodules or just the
|
||||
passed-in module.
|
||||
"""
|
||||
self_module = cast(nn.Module, self)
|
||||
modules = list(self_module.modules()) if recurse else [self_module]
|
||||
for module in modules:
|
||||
if isinstance(module, FSDPModule):
|
||||
state = module._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.reduce_grads = requires_gradient_sync
|
||||
fsdp_param_group.all_reduce_grads = requires_gradient_sync
|
||||
|
||||
def set_requires_all_reduce(
|
||||
self, requires_all_reduce: bool, *, recurse: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Sets if the module should all-reduce gradients. This can be used to
|
||||
implement gradient accumulation with only reduce-scatter but not
|
||||
all-reduce for HSDP.
|
||||
"""
|
||||
self_module = cast(nn.Module, self)
|
||||
modules = list(self_module.modules()) if recurse else [self_module]
|
||||
for module in modules:
|
||||
if isinstance(module, FSDPModule):
|
||||
state = module._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.all_reduce_grads = requires_all_reduce
|
||||
|
||||
def set_reshard_after_forward(
|
||||
self, reshard_after_forward: bool, recurse: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Sets if the module should reshard parameters after forward. This can be
|
||||
used to change the ``reshard_after_forward`` FSDP arg at runtime. For
|
||||
example, this can be used to set the FSDP root module's value to
|
||||
``True`` (since it is otherwise specially set to ``False``), or it can
|
||||
set an FSDP module's value to ``False`` for running evals and set back
|
||||
to ``True`` for training.
|
||||
|
||||
Args:
|
||||
reshard_after_forward (bool): Whether to reshard parameters after
|
||||
forward.
|
||||
recurse (bool): Whether to set for all FSDP submodules or just the
|
||||
passed-in module.
|
||||
"""
|
||||
if not isinstance(reshard_after_forward, bool):
|
||||
raise ValueError(
|
||||
f"reshard_after_forward should be a bool, got {type(reshard_after_forward)}"
|
||||
)
|
||||
self_module = cast(nn.Module, self)
|
||||
modules = list(self_module.modules()) if recurse else [self_module]
|
||||
for module in modules:
|
||||
if isinstance(module, FSDPModule):
|
||||
state = module._get_fsdp_state()
|
||||
state._auto_reshard_after_forward = False
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
if not isinstance(fsdp_param_group.mesh_info, FSDPMeshInfo):
|
||||
raise AssertionError
|
||||
fsdp_param_group.post_forward_mesh_info = (
|
||||
_get_post_forward_mesh_info(
|
||||
reshard_after_forward,
|
||||
fsdp_param_group.mesh_info,
|
||||
)
|
||||
)
|
||||
|
||||
def set_reshard_after_backward(
|
||||
self, reshard_after_backward: bool, *, recurse: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Sets if the module should reshard parameters after backward. This can
|
||||
be used during gradient accumulation to trade off higher memory for
|
||||
reduced communication since the unsharded parameters do not need to be
|
||||
re-all-gathered before the next forward.
|
||||
|
||||
Args:
|
||||
reshard_after_backward (bool): Whether to reshard parameters after
|
||||
backward.
|
||||
recurse (bool): Whether to set for all FSDP submodules or just the
|
||||
passed-in module.
|
||||
"""
|
||||
self_module = cast(nn.Module, self)
|
||||
modules = list(self_module.modules()) if recurse else [self_module]
|
||||
for module in modules:
|
||||
if isinstance(module, FSDPModule):
|
||||
state = module._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.reshard_after_backward = reshard_after_backward
|
||||
|
||||
def set_modules_to_forward_prefetch(self, modules: list[FSDPModule]) -> None:
|
||||
"""
|
||||
Sets the FSDP modules for which this FSDP module should explicitly
|
||||
prefetch all-gathers in forward. The prefetching runs after this
|
||||
module's all-gather copy-out.
|
||||
|
||||
Passing a singleton list containing the next FSDP module gives the same
|
||||
all-gather overlap behavior as the default overlap behavior, except the
|
||||
prefetched all-gather is issued earlier from the CPU. Passing a list
|
||||
with at least length two is required for more aggressive overlap and
|
||||
will use more reserved memory.
|
||||
|
||||
Args:
|
||||
modules (List[FSDPModule]): FSDP modules to prefetch.
|
||||
"""
|
||||
_assert_all_fsdp_modules(modules)
|
||||
self._get_fsdp_state()._states_to_forward_prefetch = [
|
||||
module._get_fsdp_state() for module in modules
|
||||
]
|
||||
|
||||
def set_modules_to_backward_prefetch(self, modules: list[FSDPModule]) -> None:
|
||||
"""
|
||||
Sets the FSDP modules for which this FSDP module should explicitly
|
||||
prefetch all-gathers in backward. This overrides the default backward
|
||||
pretching implementation that prefetches the next FSDP module based on
|
||||
the reverse post-forward order.
|
||||
|
||||
Passing a singleton list containing the previous FSDP module gives the
|
||||
same all-gather overlap behavior as the default overlap behavior.
|
||||
Passing a list with at least length two is required for more aggressive
|
||||
overlap and will use more reserved memory.
|
||||
|
||||
Args:
|
||||
modules (List[FSDPModule]): FSDP modules to prefetch.
|
||||
"""
|
||||
_assert_all_fsdp_modules(modules)
|
||||
self._get_fsdp_state()._states_to_backward_prefetch = [
|
||||
module._get_fsdp_state() for module in modules
|
||||
]
|
||||
|
||||
def set_custom_all_gather(self, comm: AllGather) -> None:
|
||||
"""
|
||||
Overrides the default ``all_gather`` communication behavior,
|
||||
to have better control over the communication and memory usage.
|
||||
See `Comm` and `ReduceScatter` for details.
|
||||
|
||||
Args:
|
||||
comm (AllGather): Custom all-gather communication.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
if len(state._fsdp_param_groups) > 1:
|
||||
raise ValueError(
|
||||
"set_custom_all_gather is not supported with multiple param "
|
||||
"groups (from per-param mesh via shard_placement_fn). "
|
||||
"The custom comm would be ambiguous across groups with different meshes."
|
||||
)
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group._all_gather_comm = comm
|
||||
|
||||
def set_custom_reduce_scatter(self, comm: ReduceScatter) -> None:
|
||||
"""
|
||||
Overrides the default ``reduce_scatter`` communication behavior,
|
||||
to have better control over the communication and memory usage.
|
||||
See `Comm` and `ReduceScatter` for details.
|
||||
|
||||
Args:
|
||||
comm (ReduceScatter): Custom reduce_scatter communication.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
if len(state._fsdp_param_groups) > 1:
|
||||
raise ValueError(
|
||||
"set_custom_reduce_scatter is not supported with multiple param "
|
||||
"groups (from per-param mesh via shard_placement_fn). "
|
||||
"The custom comm would be ambiguous across groups with different meshes."
|
||||
)
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group._reduce_scatter_comm = comm
|
||||
|
||||
def set_all_reduce_hook(
|
||||
self,
|
||||
hook: Callable[[torch.Tensor], None],
|
||||
*,
|
||||
stream: torch.cuda.Stream | None = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
hook (Callable[[torch.Tensor], None]): User-defined all-reduce hook
|
||||
with expected signature ``hook(reduce_output: torch.Tensor) -> None``
|
||||
where ``reduce_output`` is the reduce-scatter output if only
|
||||
using FSDP or the all-reduce output if using native HSDP.
|
||||
stream (Optional[torch.cuda.Stream]): Stream to run the all-reduce
|
||||
hook in. This should only be set if not using native HSDP. If
|
||||
using native HSDP, the hook will run in the internally defined
|
||||
all-reduce stream used by the native HSDP all-reduce.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
if len(state._fsdp_param_groups) > 1:
|
||||
raise ValueError(
|
||||
"set_all_reduce_hook is not supported with multiple param "
|
||||
"groups (from per-param mesh via shard_placement_fn). "
|
||||
"The hook would be ambiguous across groups with different meshes."
|
||||
)
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group._all_reduce_hook = hook
|
||||
if stream is not None:
|
||||
if fsdp_param_group._is_hsdp:
|
||||
raise ValueError("stream cannot be set when using native HSDP")
|
||||
fsdp_param_group._all_reduce_hook_stream = stream
|
||||
|
||||
def set_post_optim_event(self, event: torch.Event) -> None:
|
||||
"""
|
||||
Sets a post-optimizer-step event for the root FSDP module to wait the
|
||||
all-gather streams on.
|
||||
|
||||
By default, the root FSDP module waits the all-gather streams on the
|
||||
current stream to ensure that the optimizer step has finished before
|
||||
all-gathering. However, this may introduce false dependencies if
|
||||
there is unrelated computation after the optimizer step. This API
|
||||
allows the user to provide their own event to wait on. After the root
|
||||
waits on the event, the event is discarded, so this API should be
|
||||
called with a new event each iteration.
|
||||
|
||||
Args:
|
||||
event (torch.Event): Event recorded after the optimizer step
|
||||
to wait all-gather streams on.
|
||||
"""
|
||||
self._get_fsdp_state()._state_ctx.post_optim_event = event
|
||||
|
||||
@deprecated("Use `set_gradient_divide_factor` instead")
|
||||
def set_reduce_scatter_divide_factor(self, factor: float) -> None:
|
||||
"""Use :py:meth:`set_gradient_divide_factor` instead"""
|
||||
self.set_gradient_divide_factor(factor)
|
||||
|
||||
def set_gradient_divide_factor(self, factor: float) -> None:
|
||||
"""
|
||||
Sets a custom divide factor for the gradient reduction. This might use
|
||||
a custom reduce op using NCCL's PreMulSum, which allows multiplying by
|
||||
the factor before reduction.
|
||||
|
||||
Args:
|
||||
factor (float): Custom divide factor.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.gradient_divide_factor = factor
|
||||
|
||||
def set_force_sum_reduction_for_comms(self, enable: bool) -> None:
|
||||
"""
|
||||
Sets whether to require the low-level collective communication
|
||||
primitives to exclusively use "sum"-type reductions, even if it comes
|
||||
at the cost of separate additional pre- or post-scaling operations.
|
||||
This is needed for example because NCCL currently supports zero-copy
|
||||
transfers only for this kind of collectives.
|
||||
|
||||
NB: for MTIA devices, this is always implicitly enabled.
|
||||
|
||||
NB: if `set_all_reduce_hook` is used under FSDP setup, the caller needs
|
||||
to ensure the custom all-reduce across FSDP units follow this strategy
|
||||
as well, as FSDP can no longer automatically handle that.
|
||||
|
||||
Args:
|
||||
enable (bool): Whether to only ever use ReduceOp.SUM for comms.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.force_sum_reduction_for_comms = enable
|
||||
|
||||
def set_unshard_in_backward(self, unshard_in_backward: bool) -> None:
|
||||
"""
|
||||
Sets whether the FSDP module's parameters need to be unsharded in
|
||||
backward. This can be used in expert cases when the user knows that all
|
||||
parameters in this FSDP module's parameter group are not needed for
|
||||
backward computation (e.g. embedding).
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.unshard_in_backward = unshard_in_backward
|
||||
|
||||
def set_allocate_memory_from_process_group_for_comm(self, enable: bool) -> None:
|
||||
"""
|
||||
Sets whether the temporary staging buffers used to send and receive data
|
||||
over collective communications should be allocated using the custom
|
||||
optimized allocator provided by the ProcessGroup itself (if any). This
|
||||
might allow the ProcessGroup to be more efficient. For example, when
|
||||
using NCCL, this enables it to leverage zero-copy transfers over SHARP
|
||||
(for NVLink and/or InfiniBand).
|
||||
|
||||
This cannot be used together with :meth:`set_custom_all_gather` or
|
||||
:meth:`set_custom_reduce_scatter` as those APIs allow for
|
||||
finer-grained control over each communication, and this method cannot
|
||||
determine their staging buffer allocation strategy.
|
||||
|
||||
Args:
|
||||
enable (bool): Whether to turn on ProcessGroup allocation.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.set_allocate_memory_from_process_group(enable)
|
||||
|
||||
def set_symm_mem_for_comm(self, backend: Literal["NCCL"] = "NCCL") -> None:
|
||||
"""
|
||||
Sets the symmetric memory (``symm_mem``) backend for allocating the
|
||||
staging buffers used in all-gather collectives. This allows NCCL to use
|
||||
optimized all-gather implementations via symmetric memory. Such
|
||||
optimization may depend on the topology of the system. For single node,
|
||||
Copy Engine All-Gather may be used. For multi-node, Symmetric Kernel
|
||||
All-Gather may be used.
|
||||
|
||||
To enable Copy Engine All-Gather, you need to set the NCCL process group
|
||||
with the zero-CTA policy.
|
||||
```python
|
||||
opts = dist.ProcessGroupNCCL.Options()
|
||||
opts.config.cta_policy = dist.ProcessGroupNCCL.NCCL_CTA_POLICY_ZERO
|
||||
dist.init_process_group(backend="nccl", pg_options=opts, device_id=device)
|
||||
```
|
||||
Alternatively, you can set the environment variable `NCCL_CTA_POLICY` to 2.
|
||||
```bash
|
||||
export NCCL_CTA_POLICY=2
|
||||
```
|
||||
For more details, see [Copy Engine
|
||||
Collectives](https://docs.pytorch.org/docs/2.11/symmetric_memory.html#copy-engine-collectives).
|
||||
|
||||
This cannot be used together with :meth:`set_custom_all_gather` or
|
||||
:meth:`set_custom_reduce_scatter`.
|
||||
|
||||
Args:
|
||||
backend (str): The symmetric memory backend to use. Defaults to
|
||||
``"NCCL"``. Currently, only ``"NCCL"`` is supported.
|
||||
"""
|
||||
state = self._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.set_symm_mem(backend)
|
||||
|
||||
def _set_unshard_async_op(self, async_op: bool):
|
||||
"""
|
||||
Sets whether to use ``async_op=True`` or ``False`` for the pre-forward
|
||||
and pre-backward unshard op. This defaults to ``False`` but can be set
|
||||
to ``True`` with this method.
|
||||
|
||||
Setting this to ``True`` allows the all-gather allocations to happen in
|
||||
the default stream, avoiding inter-stream memory fragmentation.
|
||||
However, you must use explicit prefetching (e.g. via :meth:`unshard`)
|
||||
in forward to still get overlap, and the pre-all-gather ops like dtype
|
||||
casting and copy-in will not overlap with compute.
|
||||
"""
|
||||
self_module = cast(nn.Module, self)
|
||||
for module in self_module.modules():
|
||||
if isinstance(module, FSDPModule):
|
||||
state = module._get_fsdp_state()
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
fsdp_param_group.unshard_async_op = async_op
|
||||
|
||||
def _get_fsdp_state(self) -> FSDPState:
|
||||
if (state := _get_module_fsdp_state(cast(nn.Module, self))) is None:
|
||||
raise AssertionError(f"No FSDP state found on {self}")
|
||||
return state
|
||||
|
||||
def _apply(self, *args: Any, **kwargs: Any) -> Any:
|
||||
# Reshard to ensure that sharded parameters are registered
|
||||
self.reshard()
|
||||
ret = super()._apply(*args, **kwargs) # type: ignore[misc]
|
||||
state = self._get_fsdp_state()
|
||||
if not state._fsdp_param_groups:
|
||||
return ret
|
||||
# TODO: Remove this padding logic once DTensor pads the local tensor:
|
||||
# https://github.com/pytorch/pytorch/issues/113045
|
||||
with torch.no_grad():
|
||||
for fsdp_param_group in state._fsdp_param_groups:
|
||||
for fsdp_param in fsdp_param_group.fsdp_params:
|
||||
fsdp_param.reset_sharded_param()
|
||||
return ret
|
||||
|
||||
|
||||
class UnshardHandle:
|
||||
"""
|
||||
A handle to wait on a :meth:`FSDPModule.unshard` op.
|
||||
"""
|
||||
|
||||
def wait(self) -> None:
|
||||
"""
|
||||
Waits on the unshard op. This ensures that the current stream can use
|
||||
the unsharded parameters, which are now registered to the module.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
class _UnshardHandleImpl(UnshardHandle):
|
||||
def __init__(self, fsdp_param_groups: list[FSDPParamGroup] | None):
|
||||
self._fsdp_param_groups = fsdp_param_groups
|
||||
|
||||
def wait(self):
|
||||
if self._fsdp_param_groups is not None:
|
||||
for fsdp_param_group in self._fsdp_param_groups:
|
||||
fsdp_param_group.wait_for_unshard()
|
||||
# Avoid keeping a reference
|
||||
self._fsdp_param_groups = None
|
||||
|
||||
|
||||
def register_fsdp_forward_method(module: nn.Module, method_name: str) -> None:
|
||||
"""
|
||||
Registers a method on ``module`` to be considered a forward method for
|
||||
FSDP.
|
||||
|
||||
FSDP all-gathers parameters pre-forward and optionally frees parameters
|
||||
post-forward (depending on ``reshard_after_forward``). FSDP only knows to
|
||||
do this for :meth:`nn.Module.forward` by default. This function patches a
|
||||
user-specified method to run the pre/post-forward hooks before/after the
|
||||
method, respectively. If ``module`` is not an :class:`FSDPModule`, then
|
||||
this is a no-op.
|
||||
|
||||
Args:
|
||||
module (nn.Module): Module to register the forward method on.
|
||||
method_name (str): Name of the forward method.
|
||||
"""
|
||||
if not isinstance(module, FSDPModule):
|
||||
# Make no-op to allow including both when using/not using FSDP
|
||||
return
|
||||
if not hasattr(module, method_name):
|
||||
raise ValueError(f"{type(module)} does not have a method {method_name}")
|
||||
orig_method = getattr(module, method_name)
|
||||
|
||||
@functools.wraps(orig_method)
|
||||
def wrapped_method(self, *args, **kwargs):
|
||||
fsdp_state = self._get_fsdp_state()
|
||||
args, kwargs = fsdp_state._pre_forward(self, args, kwargs)
|
||||
out = orig_method(*args, **kwargs)
|
||||
return fsdp_state._post_forward(self, args, out)
|
||||
|
||||
# Use `__get__` to make `wrapped_method` an instance method
|
||||
setattr(
|
||||
module,
|
||||
method_name,
|
||||
wrapped_method.__get__(module, type(module)), # type:ignore[attr-defined]
|
||||
)
|
||||
|
||||
|
||||
def share_comm_ctx(modules: list[FSDPModule]) -> None:
|
||||
"""
|
||||
Share cuda streams for multiple FSDPModules
|
||||
|
||||
Example usage:
|
||||
from torch.distributed.fsdp import share_comm_ctx
|
||||
share_comm_ctx([fsdp_model_1, fsdp_model_2, ...])
|
||||
|
||||
For Pipeline Parallelism (PP), each model chunk is a FSDP root. We want
|
||||
to share cuda streams for all-gather, reduce-scatter, and all-reduce.
|
||||
This avoids allocating inter-stream memory framgmentation
|
||||
|
||||
Args:
|
||||
modules (List[FSDPModule]): modules to share cuda streams
|
||||
"""
|
||||
if len(modules) == 0:
|
||||
return
|
||||
for module in modules:
|
||||
if not isinstance(module, FSDPModule):
|
||||
raise ValueError(f"Expects list of FSDPModules but got {module}")
|
||||
fsdp_states = [module._get_fsdp_state() for module in modules]
|
||||
comm_ctx = fsdp_states[0]._comm_ctx
|
||||
for fsdp_state in fsdp_states[1:]:
|
||||
fsdp_state._comm_ctx = comm_ctx
|
||||
for fsdp_param_group in fsdp_state._fsdp_param_groups:
|
||||
fsdp_param_group.comm_ctx = comm_ctx
|
||||
|
||||
|
||||
def _assert_all_fsdp_modules(modules: Iterable[Any]) -> None:
|
||||
for module in modules:
|
||||
if not isinstance(module, FSDPModule):
|
||||
raise ValueError(f"Expects FSDPModule but got {type(module)}: {module}")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
import collections
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class _FreeEventQueue:
|
||||
"""
|
||||
This tracks all pending frees corresponding to inflight all-gathers. The
|
||||
queueing pattern is iterative enqueues with a single dequeue per iteration
|
||||
once the limit ``_max_num_inflight_all_gathers`` is reached.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._queue: collections.deque[torch.Event] = collections.deque()
|
||||
self._max_num_inflight_all_gathers = 2 # empirically chosen
|
||||
|
||||
def enqueue(self, free_event: torch.Event) -> None:
|
||||
"""Enqueues a free event."""
|
||||
self._queue.append(free_event)
|
||||
|
||||
def dequeue_if_needed(self) -> torch.Event | None:
|
||||
"""Dequeues a single event if the limit is reached."""
|
||||
if len(self._queue) >= self._max_num_inflight_all_gathers:
|
||||
return self._dequeue()
|
||||
return None
|
||||
|
||||
def _dequeue(self) -> torch.Event | None:
|
||||
"""Dequeues a free event if possible."""
|
||||
if self._queue:
|
||||
event = self._queue.popleft()
|
||||
return event
|
||||
return None
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import copy
|
||||
import itertools
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch._utils import _get_device_module
|
||||
from torch.distributed import distributed_c10d
|
||||
from torch.distributed._shard.sharded_tensor import (
|
||||
Shard,
|
||||
ShardedTensor,
|
||||
ShardedTensorMetadata,
|
||||
TensorProperties,
|
||||
)
|
||||
from torch.distributed._shard.sharding_spec import ShardMetadata
|
||||
from torch.distributed.tensor import DeviceMesh, DTensor, Replicate, Shard as DShard
|
||||
|
||||
|
||||
def _get_remote_device_str(rank, device_type, num_devices_per_node):
|
||||
if device_type.lower() == "cpu":
|
||||
return f"rank:{rank}/{device_type}"
|
||||
elif device_type.lower() == "hpu":
|
||||
return f"rank:{rank}/{device_type}:{_get_device_module(device_type).current_device()}"
|
||||
else:
|
||||
return f"rank:{rank}/{device_type}:{rank % num_devices_per_node}"
|
||||
|
||||
|
||||
def _create_chunk_sharded_tensor(
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
world_size: int,
|
||||
num_devices_per_node: int,
|
||||
pg: dist.ProcessGroup,
|
||||
device: torch.device | None = None,
|
||||
) -> ShardedTensor:
|
||||
"""
|
||||
Shard a tensor to chunks along the first dimension. The local rank will gets its
|
||||
corresponding chunk as the local shard to create a ShardedTensor.
|
||||
"""
|
||||
chunks = tensor.chunk(world_size, dim=0)
|
||||
if len(chunks) > rank:
|
||||
local_shard = chunks[rank].clone()
|
||||
offsets = [0 for _ in tensor.size()]
|
||||
offsets[0] = math.ceil(tensor.size()[0] / world_size) * rank
|
||||
local_shards = [Shard.from_tensor_and_offsets(local_shard, offsets, rank)]
|
||||
else:
|
||||
local_shards = []
|
||||
|
||||
# Create a ShardedTensor without invoking communication.
|
||||
chunk_sizes = [list(chunk.size()) for chunk in chunks]
|
||||
dim0_offsets = [0] + list(
|
||||
itertools.accumulate([chunk_size[0] for chunk_size in chunk_sizes])
|
||||
)[:-1]
|
||||
offsets = [0] * (len(chunk_sizes[0]) - 1)
|
||||
chunk_offsets = [[d0] + offsets for d0 in dim0_offsets]
|
||||
device_type = (
|
||||
distributed_c10d._get_pg_default_device(pg).type
|
||||
if device is None
|
||||
else device.type
|
||||
)
|
||||
placements = [
|
||||
_get_remote_device_str(
|
||||
dist.get_global_rank(pg, r),
|
||||
device_type,
|
||||
num_devices_per_node,
|
||||
)
|
||||
for r in range(len(chunk_sizes))
|
||||
]
|
||||
if len(chunk_sizes) != len(chunk_offsets) or len(chunk_sizes) != len(placements):
|
||||
raise AssertionError(
|
||||
f"Expected chunk_sizes, chunk_offsets, and placements to have the same length, "
|
||||
f"got {len(chunk_sizes)}, {len(chunk_offsets)}, {len(placements)}"
|
||||
)
|
||||
shard_metadata = [
|
||||
ShardMetadata(offset, size, placement)
|
||||
for offset, size, placement in zip(chunk_offsets, chunk_sizes, placements)
|
||||
]
|
||||
sharded_tensor_metadata = ShardedTensorMetadata(
|
||||
shards_metadata=shard_metadata,
|
||||
size=tensor.size(),
|
||||
tensor_properties=TensorProperties(
|
||||
dtype=tensor.dtype,
|
||||
layout=tensor.layout,
|
||||
requires_grad=False,
|
||||
memory_format=torch.contiguous_format,
|
||||
pin_memory=tensor.is_pinned(),
|
||||
),
|
||||
)
|
||||
return ShardedTensor._init_from_local_shards_and_global_metadata(
|
||||
local_shards, sharded_tensor_metadata=sharded_tensor_metadata, process_group=pg
|
||||
)
|
||||
|
||||
|
||||
def _create_chunk_dtensor(
|
||||
tensor: torch.Tensor,
|
||||
rank: int,
|
||||
device_mesh: DeviceMesh,
|
||||
) -> DTensor:
|
||||
"""
|
||||
Shard a tensor to chunks along the first dimension. The local rank will gets its
|
||||
corresponding chunk as the local tensor to create a DTensor.
|
||||
"""
|
||||
# We need to explicitly call .detach() to return a new tensor detached from the current graph.
|
||||
tensor = tensor.detach().clone()
|
||||
|
||||
# FSDP placements: [Shard(0)]
|
||||
# HSDP placements: [Replicate(), Shard(0)]
|
||||
replicate_placements = [Replicate() for _ in range(device_mesh.ndim)]
|
||||
shard_placements = [Replicate() for _ in range(device_mesh.ndim)]
|
||||
shard_placements[-1] = DShard(0) # type: ignore[call-overload]
|
||||
|
||||
return DTensor.from_local(
|
||||
tensor, device_mesh, replicate_placements, run_check=False
|
||||
).redistribute(
|
||||
placements=shard_placements,
|
||||
)
|
||||
|
||||
|
||||
def _all_gather_dtensor(
|
||||
tensor: DTensor,
|
||||
root_mesh: DeviceMesh | None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
All gather a DTensor in its sharded dimension and return the local tensor.
|
||||
"""
|
||||
if root_mesh != tensor.device_mesh:
|
||||
raise AssertionError("The device mesh of a tensor should be a root mesh.")
|
||||
|
||||
placements = list(copy.deepcopy(tensor.placements))
|
||||
# FSDP placements: [Shard(0)] -> [Replicate()]
|
||||
# HSDP placements: [Replicate(), Shard(0)] -> [Replicate(), Replicate()]
|
||||
placements[-1] = Replicate()
|
||||
tensor = tensor.redistribute(
|
||||
device_mesh=tensor.device_mesh,
|
||||
placements=placements,
|
||||
)
|
||||
|
||||
return tensor.to_local()
|
||||
@@ -0,0 +1,932 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import contextlib
|
||||
import logging
|
||||
import math
|
||||
import warnings
|
||||
from collections.abc import Callable, Generator, Iterator
|
||||
from typing import Any, cast, no_type_check
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
import torch.distributed.algorithms._checkpoint.checkpoint_wrapper as checkpoint_wrapper
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.distributed._shard.sharded_tensor import (
|
||||
init_from_local_shards,
|
||||
Shard,
|
||||
ShardedTensor,
|
||||
)
|
||||
from torch.distributed.fsdp._common_utils import (
|
||||
_FSDPState,
|
||||
_get_module_fsdp_state_if_fully_sharded_module,
|
||||
_has_fsdp_params,
|
||||
_is_composable,
|
||||
_module_handle,
|
||||
clean_tensor_name,
|
||||
FSDP_PREFIX,
|
||||
FSDP_WRAPPED_MODULE,
|
||||
)
|
||||
from torch.distributed.fsdp._debug_utils import SimpleProfiler
|
||||
from torch.distributed.fsdp._runtime_utils import (
|
||||
_cast_buffers_to_dtype_and_device,
|
||||
_get_orig_buffer_dtypes,
|
||||
_lazy_init,
|
||||
_reset_flat_param_grad_info_if_needed,
|
||||
)
|
||||
from torch.distributed.fsdp.api import (
|
||||
FullStateDictConfig,
|
||||
ShardingStrategy,
|
||||
StateDictType,
|
||||
)
|
||||
from torch.distributed.tensor import DTensor
|
||||
from torch.distributed.utils import _replace_by_prefix
|
||||
|
||||
from ._fsdp_extensions import (
|
||||
_ext_all_gather_dtensor,
|
||||
_ext_chunk_dtensor,
|
||||
_ext_chunk_tensor,
|
||||
_ext_post_unflatten_transform,
|
||||
_ext_pre_load_state_dict_transform,
|
||||
)
|
||||
from ._unshard_param_utils import _unshard_fsdp_state_params, FLAT_PARAM
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _should_unshard_params(fsdp_state: _FSDPState) -> bool:
|
||||
return not (
|
||||
fsdp_state.sharding_strategy == ShardingStrategy.NO_SHARD
|
||||
and (_is_composable(fsdp_state) or fsdp_state._use_orig_params)
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_wrapped_module_name(module_name: str) -> str:
|
||||
module_name = module_name.replace(f"{FSDP_PREFIX}", "")
|
||||
module_name = module_name.replace(f"{FSDP_WRAPPED_MODULE}", "")
|
||||
if module_name:
|
||||
module_name = f"{module_name}."
|
||||
# `CheckpointWrapper` adds a prefix that has to be removed as well.
|
||||
module_name = module_name.replace(checkpoint_wrapper._CHECKPOINT_PREFIX, "")
|
||||
return module_name
|
||||
|
||||
|
||||
def _param_name_infos(
|
||||
module: nn.Module, fsdp_state: _FSDPState
|
||||
) -> Iterator[tuple[str, str, str]]:
|
||||
if not _has_fsdp_params(fsdp_state, module):
|
||||
return
|
||||
for param_name, module_name in _module_handle(
|
||||
fsdp_state, module
|
||||
).param_module_names():
|
||||
module_name = _convert_to_wrapped_module_name(module_name)
|
||||
fqn = f"{module_name}{param_name}"
|
||||
yield fqn, param_name, module_name
|
||||
|
||||
|
||||
def _shared_param_name_infos(
|
||||
module: nn.Module, fsdp_state
|
||||
) -> Iterator[tuple[str, str, str]]:
|
||||
for param_name, module_name in _module_handle(
|
||||
fsdp_state, module
|
||||
).shared_param_module_names():
|
||||
module_name = _convert_to_wrapped_module_name(module_name)
|
||||
fqn = f"{module_name}{param_name}"
|
||||
yield fqn, param_name, module_name
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _enter_unshard_params_ctx(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
writeback: bool = False,
|
||||
rank0_only: bool = False,
|
||||
offload_to_cpu: bool = False,
|
||||
with_grads: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
state_dict hooks cannot use the pure context call as the checkpoint flow
|
||||
requires to enter the context in the pre-hook but leave the context in the
|
||||
post-hook. This API enters the context of ``_unshard_fsdp_state_params``.
|
||||
"""
|
||||
if module in fsdp_state._unshard_params_ctx:
|
||||
raise AssertionError(
|
||||
"Entering the ``_unshard_fsdp_state_params`` context but _unshard_params_ctx[module] "
|
||||
"is not None."
|
||||
)
|
||||
fsdp_state._unshard_params_ctx[module] = _unshard_fsdp_state_params(
|
||||
module,
|
||||
fsdp_state,
|
||||
writeback=writeback,
|
||||
rank0_only=rank0_only,
|
||||
offload_to_cpu=offload_to_cpu,
|
||||
with_grads=with_grads,
|
||||
)
|
||||
fsdp_state._unshard_params_ctx[module].__enter__()
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _exit_unshard_params_ctx(module: nn.Module, fsdp_state: _FSDPState) -> None:
|
||||
"""A helper function to exit ``_unshard_fsdp_state_params`` context."""
|
||||
fsdp_state._unshard_params_ctx[module].__exit__(None, None, None)
|
||||
fsdp_state._unshard_params_ctx.pop(module)
|
||||
|
||||
|
||||
def _common_pre_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
) -> None:
|
||||
"""Performs the pre-state_dict tasks shared by all state_dict types."""
|
||||
if fsdp_state._device_handle.is_available():
|
||||
fsdp_state._device_handle.synchronize()
|
||||
# TODO: need to check if this is always correct for composable FSDP.
|
||||
_lazy_init(fsdp_state, module)
|
||||
if fsdp_state._is_root:
|
||||
_reset_flat_param_grad_info_if_needed(fsdp_state._all_handles)
|
||||
|
||||
|
||||
def _common_unshard_pre_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
offload_to_cpu: bool,
|
||||
rank0_only: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Performs the pre-state_dict tasks shared by all state_dict types that require
|
||||
``_unshard_fsdp_state_params()``. FULL_STATE_DICT and SHARDED_STATE_DICT use this hook.
|
||||
"""
|
||||
# For composable `fully_shard`, it does not need to unshard parameters for `NO_SHARD` cases.
|
||||
if not _should_unshard_params(fsdp_state):
|
||||
return
|
||||
_enter_unshard_params_ctx(
|
||||
module,
|
||||
fsdp_state,
|
||||
writeback=False,
|
||||
offload_to_cpu=offload_to_cpu,
|
||||
rank0_only=rank0_only,
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _common_unshard_post_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
param_hook: Callable,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
The post-state_dict flow that shared by all state_dict types that require
|
||||
``_unshard_fsdp_state_params()``. FULL_STATE_DICT and SHARDED_STATE_DICT use this
|
||||
hook.
|
||||
"""
|
||||
_replace_by_prefix(state_dict, prefix + f"{FSDP_PREFIX}", prefix)
|
||||
# Return early for trivial cases
|
||||
if not state_dict or not _has_fsdp_params(fsdp_state, module):
|
||||
if _should_unshard_params(fsdp_state):
|
||||
_exit_unshard_params_ctx(module, fsdp_state)
|
||||
return state_dict
|
||||
|
||||
# If a rank does not have unsharded parameters(when `rank0_only=True`
|
||||
# and `rank != 0`), then the rank only needed to participate in the
|
||||
# all-gather and does not need to save the # state dict. We simply check
|
||||
# rank0_only to ensure this issue.
|
||||
rank0_only = (
|
||||
fsdp_state._state_dict_type == StateDictType.FULL_STATE_DICT
|
||||
and cast(FullStateDictConfig, fsdp_state._state_dict_config).rank0_only
|
||||
)
|
||||
# no_fsdp_return means the state_dict returned by this rank should contain
|
||||
# only non-FSDP controlled parameters and buffers.
|
||||
no_fsdp_return = rank0_only and fsdp_state.rank != 0
|
||||
if no_fsdp_return and not fsdp_state._use_orig_params:
|
||||
for clean_key in fsdp_state._buffer_names:
|
||||
# This is a hack to support activation checkpoint.
|
||||
clean_key = clean_key.replace(
|
||||
f"{checkpoint_wrapper._CHECKPOINT_PREFIX}.", ""
|
||||
)
|
||||
state_dict.pop(f"{prefix}{clean_key}", None)
|
||||
# Non-zero ranks have flat_param key when rank0_only=True, because rank0_only=True is
|
||||
# passed in to unshard context, but nonzero ranks reshard early, causing this flat_param
|
||||
# to appear in state_dict.
|
||||
state_dict.pop(f"{prefix}{FLAT_PARAM}")
|
||||
_exit_unshard_params_ctx(module, fsdp_state)
|
||||
return state_dict
|
||||
|
||||
# Loop only the parameters saved in this instance's wrapped module to
|
||||
# avoid processing buffers.
|
||||
for fqn, param_name, module_name in _param_name_infos(module, fsdp_state):
|
||||
fqn = f"{prefix}{fqn}"
|
||||
if no_fsdp_return:
|
||||
state_dict.pop(fqn)
|
||||
continue
|
||||
if fqn not in state_dict:
|
||||
raise AssertionError(
|
||||
f"FSDP assumes {fqn} is in the state_dict but the state_dict only "
|
||||
f"has {state_dict.keys()}. "
|
||||
f"prefix={prefix}, module_name={module_name}, "
|
||||
f"param_name={param_name} rank={fsdp_state.rank}."
|
||||
)
|
||||
|
||||
param_hook(state_dict, prefix, fqn)
|
||||
|
||||
if _should_unshard_params(fsdp_state):
|
||||
_exit_unshard_params_ctx(module, fsdp_state)
|
||||
|
||||
cpu_device = torch.device("cpu")
|
||||
buffer_clean_fqns = []
|
||||
buffers = []
|
||||
for clean_key in fsdp_state._buffer_names:
|
||||
# This is a hack to support activation checkpoint.
|
||||
clean_key = clean_tensor_name(clean_key)
|
||||
fqn = f"{prefix}{clean_key}"
|
||||
if fqn not in state_dict:
|
||||
# A buffer can be registered as non-persistent.
|
||||
continue
|
||||
if no_fsdp_return:
|
||||
state_dict.pop(fqn)
|
||||
else:
|
||||
buffer = state_dict[fqn]
|
||||
if (
|
||||
fsdp_state._state_dict_config.offload_to_cpu
|
||||
and buffer.device != cpu_device
|
||||
):
|
||||
state_dict[fqn] = buffer.to(cpu_device)
|
||||
# skip upcasting for ignored buffers
|
||||
if clean_key not in fsdp_state._ignored_buffer_names:
|
||||
buffer_clean_fqns.append(clean_key)
|
||||
buffers.append(state_dict[fqn])
|
||||
|
||||
if buffers:
|
||||
mixed_precision_enabled_for_buffers = (
|
||||
fsdp_state._mixed_precision_enabled_for_buffers()
|
||||
if not _is_composable(fsdp_state)
|
||||
else (fsdp_state.mixed_precision.buffer_dtype is not None)
|
||||
)
|
||||
if mixed_precision_enabled_for_buffers:
|
||||
buffer_dtypes = _get_orig_buffer_dtypes(fsdp_state, buffer_clean_fqns)
|
||||
_cast_buffers_to_dtype_and_device(
|
||||
buffers, buffer_dtypes, fsdp_state.compute_device
|
||||
)
|
||||
for buffer, clean_fqn in zip(buffers, buffer_clean_fqns):
|
||||
fqn = f"{prefix}{clean_fqn}"
|
||||
logger.info("FSDP is casting the dtype of %s to %s", fqn, buffer.dtype)
|
||||
state_dict[fqn] = buffer.clone()
|
||||
return state_dict
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _full_pre_state_dict_hook(
|
||||
fsdp_state: _FSDPState,
|
||||
module: nn.Module,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Hook that runs before model.state_dict() is called. pre-state_dict hook is
|
||||
not actually supported by ``nn.Module``. As a result, this API is called
|
||||
from ``_full_post_state_dict_hook()`` to simulate the case. Once pre-state_dict
|
||||
is supported in ``nn.Module``, this hook will be registered as a hook in
|
||||
``nn.Module``.
|
||||
"""
|
||||
if getattr(fsdp_state, "_device_mesh", False):
|
||||
fsdp_state._device_mesh._get_root_mesh()
|
||||
|
||||
_common_pre_state_dict_hook(module, fsdp_state)
|
||||
_common_unshard_pre_state_dict_hook(
|
||||
module,
|
||||
fsdp_state,
|
||||
offload_to_cpu=fsdp_state._state_dict_config.offload_to_cpu,
|
||||
rank0_only=cast(FullStateDictConfig, fsdp_state._state_dict_config).rank0_only,
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _full_post_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Hook that runs after model.state_dict() is called before returning result to
|
||||
user. For FSDP, we may have to clone the tensors in state_dict as params go
|
||||
back to sharded version after _unshard_fsdp_state_params ends, and also remove
|
||||
the ``FSDP_WRAPPED_MODULE`` prefix.
|
||||
"""
|
||||
|
||||
def param_hook(
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
fqn: str,
|
||||
) -> None:
|
||||
clean_key = fqn
|
||||
clean_prefix = clean_tensor_name(prefix)
|
||||
# Strip prefix out of key if needed as buffer names and param names
|
||||
# do not have prefix considered as they are not computed in `state_dict`
|
||||
# call.
|
||||
clean_key = clean_key.removeprefix(clean_prefix)
|
||||
|
||||
# Clone parameters before exiting the `_unshard_fsdp_state_params()` context.
|
||||
if not getattr(state_dict[fqn], "_has_been_cloned", False):
|
||||
try:
|
||||
state_dict[fqn] = state_dict[fqn].detach().clone()
|
||||
state_dict[fqn]._has_been_cloned = True # type: ignore[attr-defined]
|
||||
except BaseException as e: # noqa: B036
|
||||
warnings.warn(
|
||||
f"Failed to clone() tensor with name {fqn} on rank {fsdp_state.rank}. "
|
||||
"This may mean that this state_dict entry could point to invalid "
|
||||
"memory regions after returning from state_dict() call if this "
|
||||
"parameter is managed by FSDP. Please check clone "
|
||||
f"implementation of {fqn}. Error: {str(e)}",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
return _common_unshard_post_state_dict_hook(
|
||||
module, fsdp_state, state_dict, prefix, param_hook
|
||||
)
|
||||
|
||||
|
||||
def _full_pre_load_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> None:
|
||||
_lazy_init(fsdp_state, module)
|
||||
if _should_unshard_params(fsdp_state):
|
||||
with SimpleProfiler.profile("_enter_unshard_params_ctx"):
|
||||
_enter_unshard_params_ctx(module, fsdp_state, writeback=True)
|
||||
# Add FSDP_PREFIX only for wrapper-based FSDP.
|
||||
if not _is_composable(fsdp_state):
|
||||
_replace_by_prefix(state_dict, prefix, prefix + f"{FSDP_PREFIX}")
|
||||
|
||||
|
||||
def _full_post_load_state_dict_hook(
|
||||
module: nn.Module, fsdp_state: _FSDPState, *args, **kwargs
|
||||
) -> None:
|
||||
if _should_unshard_params(fsdp_state):
|
||||
with SimpleProfiler.profile("_exit_unshard_params_ctx"):
|
||||
_exit_unshard_params_ctx(module, fsdp_state)
|
||||
|
||||
|
||||
def _local_pre_state_dict_hook(
|
||||
fsdp_state: _FSDPState,
|
||||
module: nn.Module,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Hook that runs before model.state_dict() is called. Right now, pre-state_dict
|
||||
hook is not supported by the PyTorch core. So this API is called from
|
||||
`_local_post_state_dict_hook()` to simulate the case.
|
||||
"""
|
||||
if (
|
||||
_has_fsdp_params(fsdp_state, module)
|
||||
and not _module_handle(fsdp_state, module).uses_sharded_strategy
|
||||
):
|
||||
raise RuntimeError(
|
||||
"``local_state_dict`` can only be used when parameters are flatten "
|
||||
"and sharded."
|
||||
)
|
||||
_common_pre_state_dict_hook(module, fsdp_state)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _local_post_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
This hook create a ShardedTensor from the local flat_param and replace
|
||||
the state_dict[f"{prefix}{FLAT_PARAM}] with the ShardedTensor. No copy
|
||||
will happen. The underlying storage is the same.
|
||||
"""
|
||||
|
||||
_replace_by_prefix(state_dict, f"{prefix}{FSDP_PREFIX}", prefix)
|
||||
if not _has_fsdp_params(fsdp_state, module):
|
||||
return state_dict
|
||||
|
||||
# state_dict[f"{prefix}{FLAT_PARAM}"] exists and has the same tensor
|
||||
# value as the flat_param but it is a pure Tensor because
|
||||
# nn.Module.state_dict() will detach the parameter. Therefore, we need
|
||||
# to get flat_param to get the metadata.
|
||||
if not _module_handle(fsdp_state, module):
|
||||
raise AssertionError("Should have returned early")
|
||||
flat_param = _module_handle(fsdp_state, module).flat_param
|
||||
# Constructs a ShardedTensor from the flat_param "without" padding.
|
||||
# Removing the padding allows users to change the number of ranks
|
||||
# when loading the local_state_dict.
|
||||
full_numel = flat_param._unpadded_unsharded_size.numel() # type: ignore[attr-defined]
|
||||
shard_offset = flat_param.numel() * fsdp_state.rank
|
||||
valid_data_size = flat_param.numel() - flat_param._shard_numel_padded
|
||||
if valid_data_size > 0:
|
||||
# If FlatParameter is returned, FlatParameter._local_shard cause a
|
||||
# pickling issue (can be torch.save but not torch.load). Since there
|
||||
# is no benefit for state_dict to return the actual FlatParameter class,
|
||||
# a view (which is a tensor) of the FlatParameter will be returned.
|
||||
flat_param = flat_param[:valid_data_size].view(valid_data_size)
|
||||
local_shards = [
|
||||
Shard.from_tensor_and_offsets(flat_param, [shard_offset], fsdp_state.rank)
|
||||
]
|
||||
else:
|
||||
local_shards = []
|
||||
sharded_tensor = init_from_local_shards(
|
||||
local_shards, full_numel, process_group=fsdp_state.process_group
|
||||
) # type: ignore[assignment]
|
||||
# TODO: Add DTensor state_dict support for LOCAL_STATE_DICT.
|
||||
if fsdp_state._state_dict_config.offload_to_cpu:
|
||||
sharded_tensor = sharded_tensor.cpu()
|
||||
state_dict[f"{prefix}{FLAT_PARAM}"] = sharded_tensor
|
||||
return state_dict
|
||||
|
||||
|
||||
def _local_post_load_state_dict_hook(
|
||||
module: nn.Module, fsdp_state: _FSDPState, *args, **kwargs
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _local_pre_load_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""
|
||||
This hook finds the local flat_param for this FSDP module from the
|
||||
state_dict. The flat_param should be a ShardedTensor. This hook converts
|
||||
the ShardedTensor to a tensor. No copy happen unless padding is required.
|
||||
"""
|
||||
_lazy_init(fsdp_state, module)
|
||||
_replace_by_prefix(state_dict, prefix, f"{prefix}{FSDP_PREFIX}")
|
||||
fqn = f"{prefix}{FSDP_PREFIX}{FLAT_PARAM}"
|
||||
if fqn not in state_dict:
|
||||
if _has_fsdp_params(fsdp_state, module):
|
||||
raise AssertionError(
|
||||
"No `FlatParameter` in `state_dict` for this FSDP instance "
|
||||
"but it has parameters"
|
||||
)
|
||||
return
|
||||
load_tensor = state_dict[fqn]
|
||||
if not isinstance(load_tensor, ShardedTensor):
|
||||
raise AssertionError("Tensors in local_state_dict should be ShardedTensor.")
|
||||
|
||||
# Convert the ShardedTensor to a Tensor.
|
||||
flat_param = _module_handle(fsdp_state, module).flat_param
|
||||
if flat_param is None:
|
||||
raise AssertionError("Expected flat_param to be set")
|
||||
valid_data_size = flat_param.numel() - flat_param._shard_numel_padded
|
||||
shards = load_tensor.local_shards()
|
||||
if valid_data_size > 0:
|
||||
if not len(shards):
|
||||
raise AssertionError(
|
||||
"load_local_state_dict assume one shard per ShardedTensor."
|
||||
)
|
||||
load_tensor = shards[0].tensor
|
||||
|
||||
# Get the metadata of the flat_param to decide whether to pad the loaded
|
||||
# tensor.
|
||||
if flat_param._shard_numel_padded > 0:
|
||||
if load_tensor.numel() >= flat_param.numel():
|
||||
raise AssertionError(
|
||||
f"Local shard size = {flat_param.numel()} and the tensor in "
|
||||
f"the state_dict is {load_tensor.numel()}."
|
||||
)
|
||||
load_tensor = F.pad(load_tensor, [0, flat_param._shard_numel_padded])
|
||||
else:
|
||||
load_tensor = flat_param
|
||||
# TODO: Add DTensor state_dict support for LOCAL_STATE_DICT.
|
||||
state_dict[fqn] = load_tensor
|
||||
|
||||
|
||||
def _sharded_pre_state_dict_hook(
|
||||
fsdp_state: _FSDPState,
|
||||
module: nn.Module,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
Hook that runs before model.state_dict() is called. Check
|
||||
``_full_pre_load_state_dict_hook`` for the detail.
|
||||
"""
|
||||
if (
|
||||
_has_fsdp_params(fsdp_state, module)
|
||||
and not _module_handle(fsdp_state, module).uses_sharded_strategy
|
||||
):
|
||||
raise RuntimeError(
|
||||
"``sharded_state_dict`` can only be used when parameters are flatten "
|
||||
"and sharded."
|
||||
)
|
||||
_common_pre_state_dict_hook(module, fsdp_state)
|
||||
# Setting offload_to_cpu here does not work even if offload_to_cpu is True.
|
||||
# We have to create ShardedTensor first then move it to CPU.
|
||||
_common_unshard_pre_state_dict_hook(
|
||||
module,
|
||||
fsdp_state,
|
||||
offload_to_cpu=False,
|
||||
rank0_only=False,
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _sharded_post_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
The hook replaces the unflattened, unsharded parameter in the state_dict
|
||||
with a unflattened, sharded parameter (a ShardedTensor).
|
||||
"""
|
||||
|
||||
def param_hook(state_dict: dict[str, Any], prefix: str, fqn: str):
|
||||
param = state_dict[fqn]
|
||||
if not fsdp_state._state_dict_config._use_dtensor:
|
||||
sharded_tensor = _ext_chunk_tensor(
|
||||
tensor=param,
|
||||
rank=fsdp_state.rank,
|
||||
world_size=fsdp_state.world_size,
|
||||
num_devices_per_node=fsdp_state._device_handle.device_count(),
|
||||
pg=fsdp_state.process_group,
|
||||
fsdp_extension=fsdp_state._fsdp_extension,
|
||||
)
|
||||
else:
|
||||
sharded_tensor = _ext_chunk_dtensor(
|
||||
tensor=param,
|
||||
rank=fsdp_state.rank,
|
||||
device_mesh=fsdp_state._device_mesh,
|
||||
fsdp_extension=fsdp_state._fsdp_extension,
|
||||
)
|
||||
if fsdp_state._state_dict_config.offload_to_cpu:
|
||||
sharded_tensor = sharded_tensor.cpu()
|
||||
state_dict[fqn] = sharded_tensor
|
||||
|
||||
return _common_unshard_post_state_dict_hook(
|
||||
module, fsdp_state, state_dict, prefix, param_hook
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _sharded_post_load_state_dict_hook(
|
||||
module: nn.Module, fsdp_state: _FSDPState, *args, **kwargs
|
||||
) -> None:
|
||||
if _has_fsdp_params(fsdp_state, module):
|
||||
with SimpleProfiler.profile("_exit_unshard_params_ctx"):
|
||||
_exit_unshard_params_ctx(module, fsdp_state)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _sharded_pre_load_state_dict_hook(
|
||||
module: nn.Module,
|
||||
fsdp_state: _FSDPState,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
) -> None:
|
||||
"""
|
||||
The hook combines the unflattened, sharded parameters (ShardedTensor) to
|
||||
a new FlatParameter and shards the new FlatParameter to the local chunk.
|
||||
"""
|
||||
_lazy_init(fsdp_state, module)
|
||||
if not _is_composable(fsdp_state):
|
||||
_replace_by_prefix(state_dict, prefix, prefix + f"{FSDP_PREFIX}")
|
||||
if not _has_fsdp_params(fsdp_state, module):
|
||||
return
|
||||
|
||||
handle = _module_handle(fsdp_state, module)
|
||||
if not handle.uses_sharded_strategy:
|
||||
raise RuntimeError(
|
||||
"load_sharded_state_dict can only be called when parameters "
|
||||
"are flattened and sharded."
|
||||
)
|
||||
fqn_to_param_ext = dict(
|
||||
zip(handle.flat_param._fqns, handle.flat_param._param_extensions)
|
||||
)
|
||||
|
||||
for fqn, _, _ in _param_name_infos(module, fsdp_state):
|
||||
if not _is_composable(fsdp_state):
|
||||
fqn_from_global_root = f"{prefix}{FSDP_PREFIX}{fqn}"
|
||||
else:
|
||||
fqn_from_global_root = f"{prefix}{fqn}"
|
||||
try:
|
||||
param = state_dict.pop(fqn_from_global_root)
|
||||
except KeyError:
|
||||
logger.warning(
|
||||
f"Did not find param with FQN {fqn_from_global_root}, skipping it. " # noqa: G004
|
||||
"The weight will not be filled if you expect it to be."
|
||||
)
|
||||
continue # TODO: Improve unittesting for state_dict finetuning
|
||||
# cases: https://github.com/pytorch/pytorch/issues/109134
|
||||
|
||||
if not fsdp_state._state_dict_config._use_dtensor:
|
||||
# All-gather the param (ShardedTensor)
|
||||
param, shards = _ext_pre_load_state_dict_transform(
|
||||
param, fsdp_state._fsdp_extension
|
||||
)
|
||||
|
||||
if len(shards) >= 2:
|
||||
raise AssertionError(
|
||||
"Expects 0 or 1 shard per rank "
|
||||
f"but got {len(shards)} shards on rank {fsdp_state.rank}."
|
||||
)
|
||||
param_numel = param.size().numel()
|
||||
dim_0_size = param.size()[0]
|
||||
chunk_size = (
|
||||
math.ceil(dim_0_size / fsdp_state.world_size)
|
||||
* param_numel
|
||||
// dim_0_size
|
||||
)
|
||||
if len(shards) == 1:
|
||||
local_tensor = shards[0].tensor.flatten()
|
||||
with SimpleProfiler.profile(SimpleProfiler.Type.H2D):
|
||||
local_tensor = local_tensor.to(fsdp_state.compute_device)
|
||||
num_padding = chunk_size - local_tensor.numel()
|
||||
if num_padding > 0:
|
||||
local_tensor = F.pad(local_tensor, [0, num_padding])
|
||||
else:
|
||||
local_tensor = torch.zeros(
|
||||
chunk_size, dtype=param.dtype, device=fsdp_state.compute_device
|
||||
)
|
||||
tensor = torch.empty(
|
||||
chunk_size * fsdp_state.world_size,
|
||||
dtype=local_tensor.dtype,
|
||||
device=fsdp_state.compute_device,
|
||||
)
|
||||
with SimpleProfiler.profile(SimpleProfiler.Type.ALLGATHER):
|
||||
dist.all_gather_into_tensor(
|
||||
tensor, local_tensor, group=fsdp_state.process_group
|
||||
)
|
||||
tensor = tensor.narrow(0, 0, param_numel).reshape(param.size())
|
||||
state_dict[fqn_from_global_root] = tensor
|
||||
else:
|
||||
if param.device != fsdp_state._device_mesh.device_type:
|
||||
param = param.to(fsdp_state._device_mesh.device_type)
|
||||
|
||||
root_mesh = fsdp_state._device_mesh._get_root_mesh()
|
||||
local_tensor = _ext_all_gather_dtensor(
|
||||
param, root_mesh, fsdp_state._fsdp_extension
|
||||
)
|
||||
|
||||
if fqn_to_param_ext.get(fqn) is not None:
|
||||
ext = fqn_to_param_ext[fqn]
|
||||
local_tensor = _ext_post_unflatten_transform(
|
||||
local_tensor, ext, fsdp_state._fsdp_extension
|
||||
)
|
||||
state_dict[fqn_from_global_root] = local_tensor
|
||||
|
||||
with SimpleProfiler.profile("_enter_unshard_params_ctx"):
|
||||
_enter_unshard_params_ctx(module, fsdp_state, writeback=True)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _replace_with_full_state_dict_type(fsdp_state: _FSDPState) -> Generator:
|
||||
old_state_dict_config = fsdp_state._state_dict_config
|
||||
old_state_dict_type = fsdp_state._state_dict_type
|
||||
fsdp_state._state_dict_config = FullStateDictConfig()
|
||||
fsdp_state._state_dict_type = StateDictType.FULL_STATE_DICT
|
||||
yield
|
||||
fsdp_state._state_dict_config = old_state_dict_config
|
||||
fsdp_state._state_dict_type = old_state_dict_type
|
||||
|
||||
|
||||
@no_type_check
|
||||
@torch.no_grad()
|
||||
def _post_state_dict_hook(
|
||||
module: nn.Module,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
*args: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
_post_state_dict_hook() is called after the state_dict() of this
|
||||
FSDP module is executed. ``fsdp_state._state_dict_type`` is used to decide
|
||||
what postprocessing will be done.
|
||||
"""
|
||||
fsdp_state = _get_module_fsdp_state_if_fully_sharded_module(module)
|
||||
if fsdp_state.sharding_strategy == ShardingStrategy.NO_SHARD:
|
||||
context = _replace_with_full_state_dict_type(fsdp_state)
|
||||
warnings.warn(
|
||||
"When using ``NO_SHARD`` for ``ShardingStrategy``, full_state_dict will "
|
||||
"be returned.",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
context = contextlib.nullcontext()
|
||||
|
||||
with context:
|
||||
_post_state_dict_hook_fn = {
|
||||
StateDictType.FULL_STATE_DICT: _full_post_state_dict_hook,
|
||||
StateDictType.LOCAL_STATE_DICT: _local_post_state_dict_hook,
|
||||
StateDictType.SHARDED_STATE_DICT: _sharded_post_state_dict_hook,
|
||||
}
|
||||
processed_state_dict = _post_state_dict_hook_fn[fsdp_state._state_dict_type](
|
||||
module, fsdp_state, state_dict, prefix
|
||||
)
|
||||
|
||||
if fsdp_state._is_root:
|
||||
logger.info("FSDP finished processing state_dict(), prefix=%s", prefix)
|
||||
for key, tensor in sorted(processed_state_dict.items()):
|
||||
if key.startswith(prefix) and isinstance(tensor, torch.Tensor):
|
||||
local_shape = tensor.shape
|
||||
device = None
|
||||
if isinstance(tensor, ShardedTensor):
|
||||
local_shape = None
|
||||
shards = tensor.local_shards()
|
||||
if shards:
|
||||
local_shape = shards[0].tensor.shape
|
||||
device = shards[0].tensor.device
|
||||
elif isinstance(tensor, DTensor):
|
||||
local_shape = tensor.to_local().shape
|
||||
device = tensor.device
|
||||
else:
|
||||
device = tensor.device
|
||||
logger.info(
|
||||
"FQN=%s: type=%s, shape=%s, local_shape=%s, dtype=%s, device=%s",
|
||||
key,
|
||||
type(tensor),
|
||||
tensor.shape,
|
||||
local_shape,
|
||||
tensor.dtype,
|
||||
device,
|
||||
)
|
||||
|
||||
return processed_state_dict
|
||||
|
||||
|
||||
@no_type_check
|
||||
@torch.no_grad()
|
||||
def _pre_state_dict_hook(
|
||||
module: nn.Module,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
This is called before the core state dict saving logic of ``module``.
|
||||
``fsdp_state._state_dict_type`` is used to decide what postprocessing will
|
||||
be done.
|
||||
"""
|
||||
fsdp_state = _get_module_fsdp_state_if_fully_sharded_module(module)
|
||||
if fsdp_state.sharding_strategy == ShardingStrategy.NO_SHARD:
|
||||
context = _replace_with_full_state_dict_type(fsdp_state)
|
||||
warnings.warn(
|
||||
"When using ``NO_SHARD`` for ``ShardingStrategy``, full_state_dict will "
|
||||
"be returned.",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
_set_use_dtensor(fsdp_state)
|
||||
context = contextlib.nullcontext()
|
||||
|
||||
with context:
|
||||
_pre_state_dict_hook_fn = {
|
||||
StateDictType.FULL_STATE_DICT: _full_pre_state_dict_hook,
|
||||
StateDictType.LOCAL_STATE_DICT: _local_pre_state_dict_hook,
|
||||
StateDictType.SHARDED_STATE_DICT: _sharded_pre_state_dict_hook,
|
||||
}
|
||||
_pre_state_dict_hook_fn[fsdp_state._state_dict_type](
|
||||
fsdp_state,
|
||||
module,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _set_use_dtensor(fsdp_state: _FSDPState) -> None:
|
||||
# If device_mesh is passed in when initializing FSDP, we automatically turn the
|
||||
# _use_dtensor flag to be true for ShardedStateDictConfig().
|
||||
if getattr(fsdp_state, "_device_mesh", None):
|
||||
state_dict_type = fsdp_state._state_dict_type
|
||||
if state_dict_type == StateDictType.LOCAL_STATE_DICT:
|
||||
raise RuntimeError(
|
||||
"Found state_dict_type LOCAL_STATE_DICT. "
|
||||
"DeviceMesh is not compatible with LOCAL_STATE_DICT. "
|
||||
"Please set state_dict_type to SHARDED_STATE_DICT to get DTensor state_dict."
|
||||
)
|
||||
else:
|
||||
fsdp_state._state_dict_config._use_dtensor = True
|
||||
|
||||
|
||||
@no_type_check
|
||||
@torch.no_grad()
|
||||
def _pre_load_state_dict_hook(
|
||||
module: nn.Module,
|
||||
state_dict: dict[str, Any],
|
||||
prefix: str,
|
||||
*args: Any,
|
||||
) -> None:
|
||||
"""
|
||||
This is called before ``module._load_from_state_dict()``.
|
||||
``fsdp_state._state_dict_type`` is used to decide what preprocessing will
|
||||
be done.
|
||||
"""
|
||||
fsdp_state = _get_module_fsdp_state_if_fully_sharded_module(module)
|
||||
if fsdp_state.sharding_strategy == ShardingStrategy.NO_SHARD:
|
||||
context = _replace_with_full_state_dict_type(fsdp_state)
|
||||
warnings.warn(
|
||||
"When using ``NO_SHARD`` for ``ShardingStrategy``, full_state_dict will"
|
||||
"be returned.",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
_set_use_dtensor(fsdp_state)
|
||||
context = contextlib.nullcontext()
|
||||
|
||||
_lazy_init(fsdp_state, module)
|
||||
if fsdp_state._is_root:
|
||||
SimpleProfiler.reset()
|
||||
|
||||
with context:
|
||||
_pre_load_state_dict_hook_fn = {
|
||||
StateDictType.FULL_STATE_DICT: _full_pre_load_state_dict_hook,
|
||||
StateDictType.LOCAL_STATE_DICT: _local_pre_load_state_dict_hook,
|
||||
StateDictType.SHARDED_STATE_DICT: _sharded_pre_load_state_dict_hook,
|
||||
}
|
||||
# Code that is common for all state_dict impls
|
||||
if fsdp_state._device_handle.is_available():
|
||||
fsdp_state._device_handle.synchronize()
|
||||
# Dispatch into state_dict specific implementation of pre-hook.
|
||||
_pre_load_state_dict_hook_fn[fsdp_state._state_dict_type](
|
||||
module, fsdp_state, state_dict, prefix
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
@torch.no_grad()
|
||||
def _post_load_state_dict_hook(
|
||||
module: nn.Module,
|
||||
incompatible_keys: tuple[list[str], list[str]],
|
||||
*args: Any,
|
||||
) -> None:
|
||||
fsdp_state = _get_module_fsdp_state_if_fully_sharded_module(module)
|
||||
if fsdp_state.sharding_strategy == ShardingStrategy.NO_SHARD:
|
||||
context = _replace_with_full_state_dict_type(fsdp_state)
|
||||
warnings.warn(
|
||||
"When using ``NO_SHARD`` for ``ShardingStrategy``, full_state_dict will"
|
||||
"be returned.",
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
context = contextlib.nullcontext()
|
||||
|
||||
with context:
|
||||
_post_load_state_dict_hook_fn = {
|
||||
StateDictType.FULL_STATE_DICT: _full_post_load_state_dict_hook,
|
||||
StateDictType.LOCAL_STATE_DICT: _local_post_load_state_dict_hook,
|
||||
StateDictType.SHARDED_STATE_DICT: _sharded_post_load_state_dict_hook,
|
||||
}
|
||||
# Code that is common for all state_dict impls
|
||||
# Dispatch into state_dict type specific implementation of post-hook for
|
||||
# loading state_dict.
|
||||
_post_load_state_dict_hook_fn[fsdp_state._state_dict_type](module, fsdp_state)
|
||||
|
||||
# When reporting incompatible keys, trim FSDP prefixes.
|
||||
missing_keys = incompatible_keys[0]
|
||||
unexpected_keys = incompatible_keys[1]
|
||||
for i in range(len(missing_keys)):
|
||||
missing_keys[i] = clean_tensor_name(missing_keys[i])
|
||||
|
||||
for i in range(len(unexpected_keys)):
|
||||
unexpected_keys[i] = clean_tensor_name(unexpected_keys[i])
|
||||
|
||||
if fsdp_state._is_root:
|
||||
SimpleProfiler.dump_and_reset("FSDP model load_state_dict profiling: ")
|
||||
|
||||
|
||||
def _register_all_state_dict_hooks(state: _FSDPState):
|
||||
"""
|
||||
Registers pre-save, post-save, pre-load, and post-load state dict hooks.
|
||||
"""
|
||||
for hook_registration_fn_str, hook, hook_registration_fn_kwargs in (
|
||||
("register_state_dict_pre_hook", _pre_state_dict_hook, {}),
|
||||
("_register_state_dict_hook", _post_state_dict_hook, {}),
|
||||
(
|
||||
"_register_load_state_dict_pre_hook",
|
||||
_pre_load_state_dict_hook,
|
||||
{"with_module": True},
|
||||
),
|
||||
("register_load_state_dict_post_hook", _post_load_state_dict_hook, {}),
|
||||
):
|
||||
_register_state_dict_hooks_base(
|
||||
state, hook_registration_fn_str, hook, hook_registration_fn_kwargs
|
||||
)
|
||||
|
||||
|
||||
@no_type_check
|
||||
def _register_state_dict_hooks_base(
|
||||
state: _FSDPState,
|
||||
hook_registration_fn_name: str,
|
||||
hook: Callable,
|
||||
hook_registration_fn_kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""Registers ``hook`` using ``hook_registration_fn``."""
|
||||
if not _is_composable(state):
|
||||
getattr(state, hook_registration_fn_name)(hook, **hook_registration_fn_kwargs)
|
||||
else:
|
||||
handle = state._handle
|
||||
if handle:
|
||||
getattr(handle._fully_sharded_module, hook_registration_fn_name)(
|
||||
hook, **hook_registration_fn_kwargs
|
||||
)
|
||||
@@ -0,0 +1,240 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import functools
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
@dataclass
|
||||
class TracingConfig:
|
||||
"""
|
||||
This represents a symbolic tracing configuration.
|
||||
|
||||
Args:
|
||||
tracer (torch.fx.Tracer): An instance of :class:`torch.fx.Tracer` to
|
||||
use for symbolic tracing. The default value is the native
|
||||
:class:`torch.fx.Tracer` constructed with default arguments.
|
||||
However, the user may want to pass a different value such as the
|
||||
``HFTracer`` for models in the HuggingFace Transformers_ library.
|
||||
.. _Transformers: https://huggingface.co/docs/transformers/index
|
||||
concrete_args (Optional[Dict[str, Any]]): Concrete arguments that
|
||||
should not be treated as ``torch.fx.Proxy`` when tracing the
|
||||
module ``forward()``. Passing ``concrete_args`` allows partially
|
||||
specializing the forward, e.g. to remove control flow or data
|
||||
structures. This ``concrete_args`` here is the same argument used
|
||||
in :meth:`~torch.fx.Tracer.trace`.
|
||||
"""
|
||||
|
||||
tracer: torch.fx.Tracer = field(default_factory=torch.fx.Tracer)
|
||||
concrete_args: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class _ParamUsageInfo(NamedTuple):
|
||||
"""
|
||||
This is used for ``_ExecutionInfo.module_to_param_usage_infos`` to record
|
||||
execution information. The ``dict`` maps modules to a list of these
|
||||
``_ParamUsageInfo`` instances, where each instance represents a group of
|
||||
parameters used together.
|
||||
|
||||
Specifically, for each module key in the ``dict``, each instance of this
|
||||
class represents either:
|
||||
(1) the module and some sublist of its ``named_parameters()`` used
|
||||
together in execution (see ``_patched_create_proxy()``), or
|
||||
(2) a submodule and all of ``submodule.named_parameters()`` (see
|
||||
``_patched_call_module()``).
|
||||
|
||||
Type (1) corresponds to directly using parameters in ops without calling
|
||||
``forward()``, and type (2) corresponds to calling ``forward()``. The
|
||||
mapped-to lists in the ``dict`` follow the execution order.
|
||||
"""
|
||||
|
||||
module: nn.Module
|
||||
named_params: list[tuple[str, nn.Parameter]]
|
||||
|
||||
|
||||
class _ExecutionInfo:
|
||||
"""
|
||||
This represents the execution order information from the forward pass.
|
||||
|
||||
Attributes:
|
||||
curr_module (nn.Module): Current module being traced.
|
||||
module_forward_order (List[nn.Module]): The modules in (pre-)forward
|
||||
order, i.e. the order in which their ``forward()`` methods are
|
||||
called. Each call to a module's ``forward()`` corresponds to one
|
||||
element in the list.
|
||||
module_to_param_usage_infos (Dict[nn.Module, List[_ParamUsageInfo]]):
|
||||
Maps a module to a list of module execution infos. See
|
||||
:class:`_ParamUsageInfo` for details.
|
||||
param_forward_order (List[nn.Parameter]): The parameters in forward
|
||||
execution order, where only a parameter's first participation is
|
||||
included.
|
||||
visited_params (Set[nn.Parameter]): The parameters visited so far
|
||||
during the trace. This is only used during tracing for fast
|
||||
membership check. Invariant: The parameters in
|
||||
``param_forward_order`` are exactly those in ``visited_params``.
|
||||
"""
|
||||
|
||||
def __init__(self, root_module: nn.Module) -> None:
|
||||
self.curr_module: nn.Module = root_module
|
||||
self.module_forward_order: list[nn.Module] = [root_module]
|
||||
self.module_to_param_usage_infos: dict[nn.Module, list[_ParamUsageInfo]] = {
|
||||
root_module: []
|
||||
}
|
||||
self.param_forward_order: list[nn.Parameter] = []
|
||||
self.visited_params: set[nn.Parameter] = set()
|
||||
|
||||
|
||||
class _ExecOrderTracer:
|
||||
def __init__(self) -> None:
|
||||
self.exec_info: _ExecutionInfo | None = None
|
||||
|
||||
@contextmanager
|
||||
def patch_tracer(self, tracer: torch.fx.Tracer, root_module: nn.Module):
|
||||
self.exec_info = _ExecutionInfo(root_module)
|
||||
orig_call_module = tracer.call_module
|
||||
orig_create_proxy = tracer.create_proxy
|
||||
tracer.call_module = functools.partial( # type: ignore[method-assign]
|
||||
self._patched_call_module, orig_call_module, self.exec_info
|
||||
)
|
||||
fqn_to_param = dict(root_module.named_parameters())
|
||||
tracer.create_proxy = functools.partial( # type: ignore[method-assign]
|
||||
self._patched_create_proxy,
|
||||
orig_create_proxy,
|
||||
self.exec_info,
|
||||
fqn_to_param,
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
tracer.call_module = orig_call_module # type: ignore[method-assign]
|
||||
tracer.create_proxy = orig_create_proxy # type: ignore[method-assign]
|
||||
|
||||
def _patched_call_module(
|
||||
self,
|
||||
call_module: Callable,
|
||||
exec_info: _ExecutionInfo,
|
||||
# Below are the expected arguments to `call_module()`
|
||||
module: nn.Module,
|
||||
forward: Callable,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Any:
|
||||
"""
|
||||
Overrides ``call_module`` to save execution information to
|
||||
``exec_info``. Note that ``call_module`` is called during symbolic
|
||||
tracing for each non-root module.
|
||||
|
||||
Args:
|
||||
call_module (Callable): Original ``call_module`` to override.
|
||||
exec_info (_ExecutionInfo): Used to record execution information.
|
||||
module (nn.Module): Module corresponding to this ``call_module``.
|
||||
forward (Callable): ``forward()`` method of ``module`` to be called
|
||||
for this ``call_module``.
|
||||
args (Tuple[Any, ...]): Positional arguments for ``forward``.
|
||||
kwargs (Dict[str, Any]): Keyword arguments for ``forward``.
|
||||
|
||||
Returns:
|
||||
Same return value as ``call_module``.
|
||||
"""
|
||||
exec_info.module_forward_order.append(module)
|
||||
named_params = list(module.named_parameters())
|
||||
curr_module = exec_info.curr_module
|
||||
if named_params:
|
||||
if curr_module not in exec_info.module_to_param_usage_infos:
|
||||
raise AssertionError(
|
||||
"The current module should have already been processed by a patched `call_module`"
|
||||
)
|
||||
exec_info.module_to_param_usage_infos[exec_info.curr_module].append(
|
||||
_ParamUsageInfo(module, named_params)
|
||||
)
|
||||
prev_curr_module = curr_module
|
||||
exec_info.curr_module = module
|
||||
exec_info.module_to_param_usage_infos[module] = []
|
||||
output = call_module(module, forward, args, kwargs)
|
||||
exec_info.curr_module = prev_curr_module
|
||||
return output
|
||||
|
||||
def _patched_create_proxy(
|
||||
self,
|
||||
create_proxy: Callable,
|
||||
exec_info: _ExecutionInfo,
|
||||
fqn_to_param: dict[str, nn.Parameter],
|
||||
# Below are the expected arguments to `create_proxy()`
|
||||
kind: str,
|
||||
target: torch.fx.node.Target,
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any],
|
||||
name: str | None = None,
|
||||
type_expr: Any | None = None,
|
||||
proxy_factory_fn: Callable[[torch.fx.Node], torch.fx.Proxy] | None = None,
|
||||
) -> torch.fx.Proxy:
|
||||
"""
|
||||
Overrides ``create_proxy`` to save execution information to
|
||||
``exec_info``. Note that ``create_proxy`` is called during symbolic
|
||||
tracing for each leaf function/method/module.
|
||||
|
||||
Args:
|
||||
create_proxy (Callable): Original ``create_proxy`` to override.
|
||||
exec_info (_ExecutionInfo): Used to record execution information.
|
||||
fqn_to_param (Dict[str, nn.Parameter]): ``dict`` version of the
|
||||
root module's ``named_parameters()`` with FQN as key and
|
||||
parameter as value.
|
||||
kind (str): Kind of the target method ('call_function',
|
||||
'call_method', 'get_attr', 'call_module', 'placeholder', or
|
||||
'output'). See :class:`torch.fx.Graph` for details. This is
|
||||
passed to ``create_proxy``.
|
||||
target (torch.fx.node.Target): Contains the string name of the
|
||||
function/method/module. This is passed to ``create_proxy``.
|
||||
args (Tuple[Any, ...]): Positional arguments for the function/
|
||||
method/module. This is passed to ``create_proxy``.
|
||||
kwargs (Dict[str, Any]): Keyword arguments for the function/method/
|
||||
module. This is passed to ``create_proxy``
|
||||
name (Optional[str]): An optional string name for the ``Node``
|
||||
created in ``create_proxy``. This is passed to
|
||||
``create_proxy``.
|
||||
type_expr (Optional[Any]): An optional type annotation representing
|
||||
the Python type that the output of the node has. This is passed
|
||||
to ``create_proxy``.
|
||||
proxy_factory_fn (Callable[[torch.fx.Node], torch.fx.Proxy]):
|
||||
An alternative proxy constructor used in ``create_proxy``. This
|
||||
is passed to ``create_proxy``.
|
||||
|
||||
Returns:
|
||||
torch.fx.Proxy: Created ``Node`` wrapped in a ``Proxy`` object.
|
||||
"""
|
||||
proxy = create_proxy(
|
||||
kind, target, args, kwargs, name, type_expr, proxy_factory_fn
|
||||
)
|
||||
curr_module = exec_info.curr_module
|
||||
if kind in ("call_function", "call_method"):
|
||||
if args is not None:
|
||||
named_params: list[tuple[str, nn.Parameter]] = []
|
||||
for arg in args:
|
||||
if (
|
||||
isinstance(arg, torch.fx.Proxy)
|
||||
and arg.node.target in fqn_to_param
|
||||
):
|
||||
param = fqn_to_param[arg.node.target] # type: ignore[index]
|
||||
named_params.append((arg.node.target, param)) # type: ignore[arg-type]
|
||||
if param not in exec_info.visited_params:
|
||||
exec_info.visited_params.add(param)
|
||||
exec_info.param_forward_order.append(param)
|
||||
if named_params:
|
||||
exec_info.module_to_param_usage_infos[curr_module].append(
|
||||
_ParamUsageInfo(curr_module, named_params)
|
||||
)
|
||||
elif kind == "call_module":
|
||||
named_params = list(curr_module.named_parameters())
|
||||
if named_params:
|
||||
exec_info.module_to_param_usage_infos[curr_module].append(
|
||||
_ParamUsageInfo(curr_module, named_params)
|
||||
)
|
||||
for _, param in named_params:
|
||||
if param not in exec_info.visited_params:
|
||||
exec_info.visited_params.add(param)
|
||||
exec_info.param_forward_order.append(param)
|
||||
return proxy
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
NOTE: This file must be imported like
|
||||
``import torch.distributed.fsdp._traversal_utils`` and not like
|
||||
``from torch.distributed.fsdp._traversal_utils import ...`` to avoid circular
|
||||
imports. For brevity, we may import the file as ``traversal_utils``.
|
||||
"""
|
||||
|
||||
import collections
|
||||
|
||||
import torch.nn as nn
|
||||
from torch.distributed._composable.contract import _get_registry
|
||||
from torch.distributed.fsdp._common_utils import _FSDPState, _get_module_fsdp_state
|
||||
|
||||
|
||||
"""
|
||||
[Note: FSDP State Traversal]
|
||||
For the wrapper code path, ``_FSDPState`` is the ``FullyShardedDataParallel``
|
||||
module wrapping a fully sharded module, and for the non-wrapper code path,
|
||||
``_FSDPState`` is an object that gets embedded on a fully sharded module.
|
||||
See [Note: Fully Sharded Module] for the definition.
|
||||
|
||||
There are three common traversal idioms: Given a root module,
|
||||
- ``_get_fsdp_states()`` returns all ``_FSDPState`` s in the tree.
|
||||
- ``get_fsdp_root_states()`` returns all local root ``_FSDPState`` s in the
|
||||
tree (i.e. those with ``_is_root == True``).
|
||||
- ``_get_fsdp_handles()``returns all ``FlatParamHandle`` s in the tree.
|
||||
|
||||
All of these methods must take in the root module (i.e. an ``nn.Module``) and
|
||||
not a general ``_FSDPState`` because ``_FSDPState`` does not support a graph
|
||||
traversal, whereas ``nn.Module`` has ``nn.Module.modules()`` for traversal.
|
||||
"""
|
||||
|
||||
|
||||
def _composable(module: nn.Module) -> bool:
|
||||
"""
|
||||
Returns if ``module`` can compose with ``fully_shard``.
|
||||
"""
|
||||
# TODO: Add any other composable APIs that are mutually exclusive.
|
||||
registry = _get_registry(module)
|
||||
if registry is None:
|
||||
return True
|
||||
return "replicate" not in registry
|
||||
|
||||
|
||||
# TODO (awgu): We may be able to remove this function if we retired the
|
||||
# `use_orig_params=False` code path since so far we only need the module for
|
||||
# `FlatParameter` registration, which is not needed for `use_orig_params=True`.
|
||||
def _get_fsdp_states_with_modules(
|
||||
module: nn.Module,
|
||||
) -> tuple[list[_FSDPState], list[nn.Module]]:
|
||||
"""
|
||||
Returns a tuple containing:
|
||||
1. A list of the ``_FSDPState`` instances in the module tree rooted at
|
||||
``module`` without any duplicates and following the ``module.modules()``
|
||||
traversal order (which is assumed to be depth-first).
|
||||
2. A corresponding list of the modules owning the states in the first list.
|
||||
|
||||
For the wrapper code path, both returned lists are the same, each
|
||||
containing all ``FullyShardedDataParallel`` instances. For the composable
|
||||
code path, this returns a list of all composable state instances and a list
|
||||
of the corresponding fully sharded modules. See [Note: Fully Sharded
|
||||
Module].
|
||||
|
||||
NOTE: The traversal does not proceed into any module annotated by an
|
||||
incompatible API (e.g. ``replicate``).
|
||||
"""
|
||||
fsdp_states: list[_FSDPState] = []
|
||||
fsdp_modules: list[nn.Module] = []
|
||||
# Track the visited FSDP states since multiple modules may share the same
|
||||
# one and we want to return a de-duplicated list
|
||||
visited_fsdp_states: set[_FSDPState] = set()
|
||||
# Track the visited modules in case of shared modules, which implies the
|
||||
# module graph is no longer a tree
|
||||
visited_modules: set[nn.Module] = set()
|
||||
|
||||
# Perform depth-first search from `module` to ensure that we do not
|
||||
# traverse into an incompatible API's subtree (use DFS instead of BFS to
|
||||
# match `.modules()` order)
|
||||
deque: collections.deque[nn.Module] = collections.deque([module])
|
||||
while deque:
|
||||
submodule = deque.popleft()
|
||||
visited_modules.add(submodule)
|
||||
if not _composable(submodule):
|
||||
continue
|
||||
for child_module in reversed(list(submodule.children())):
|
||||
if child_module not in visited_modules:
|
||||
deque.appendleft(child_module)
|
||||
optional_state = _get_module_fsdp_state(submodule)
|
||||
if optional_state is not None and optional_state not in visited_fsdp_states:
|
||||
visited_fsdp_states.add(optional_state)
|
||||
fsdp_states.append(optional_state)
|
||||
fsdp_modules.append(submodule)
|
||||
return fsdp_states, fsdp_modules
|
||||
|
||||
|
||||
def _get_fsdp_states(module: nn.Module) -> list[_FSDPState]:
|
||||
"""See :func:`_get_fsdp_states_with_modules`."""
|
||||
fsdp_states, _ = _get_fsdp_states_with_modules(module)
|
||||
return fsdp_states
|
||||
|
||||
|
||||
def _get_fsdp_handles(module: nn.Module) -> list:
|
||||
"""
|
||||
Returns all ``FlatParamHandle`` s in the module tree rooted at ``module``
|
||||
following the rules in :func:`_get_fsdp_state`.
|
||||
"""
|
||||
handles = [
|
||||
fsdp_state._handle
|
||||
for fsdp_state in _get_fsdp_states(module)
|
||||
if fsdp_state._handle is not None
|
||||
]
|
||||
return handles
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import contextlib
|
||||
import warnings
|
||||
from collections.abc import Generator
|
||||
from typing import cast
|
||||
|
||||
import torch
|
||||
import torch.distributed.fsdp._traversal_utils as traversal_utils
|
||||
import torch.nn as nn
|
||||
from torch.distributed.fsdp._common_utils import (
|
||||
_FSDPState,
|
||||
_get_module_fsdp_state,
|
||||
_has_fsdp_params,
|
||||
_module_handle,
|
||||
HandleTrainingState,
|
||||
TrainingState,
|
||||
)
|
||||
from torch.distributed.fsdp._runtime_utils import (
|
||||
_lazy_init,
|
||||
_reset_flat_param_grad_info_if_needed,
|
||||
_reshard,
|
||||
_reshard_grads,
|
||||
_unshard,
|
||||
_unshard_grads,
|
||||
)
|
||||
from torch.distributed.utils import _p_assert
|
||||
|
||||
from ._flat_param import FlatParamHandle
|
||||
|
||||
|
||||
FLAT_PARAM = "_flat_param"
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def _writeback_to_local_shard(
|
||||
handle: FlatParamHandle,
|
||||
writeback_grad: bool,
|
||||
):
|
||||
"""
|
||||
For the handle, writes back the this rank's shard of the unsharded
|
||||
flattened parameter to the sharded flattened parameter. If
|
||||
``writeback_grad=True``, then writes back to the sharded gradient as
|
||||
well.
|
||||
|
||||
Precondition: The handle's ``FlatParameter`` 's data points to the
|
||||
padded unsharded flattened parameter.
|
||||
"""
|
||||
|
||||
def _get_shard(flat_param_or_grad: torch.Tensor) -> torch.Tensor:
|
||||
if handle.uses_sharded_strategy:
|
||||
# For sharded strategies, get the *unpadded* shard instead of
|
||||
# the *padded* shard to persist user changes to the padding
|
||||
# (though FSDP does not explicitly support this)
|
||||
shard, _ = FlatParamHandle._get_unpadded_shard(
|
||||
flat_param_or_grad,
|
||||
handle.rank,
|
||||
handle.world_size,
|
||||
)
|
||||
return shard
|
||||
# For `NO_SHARD`, the `flat_param` or its gradient may be modified,
|
||||
# so we write it back directly
|
||||
return flat_param_or_grad
|
||||
|
||||
param_shard = _get_shard(handle.flat_param)
|
||||
handle.flat_param._local_shard[: param_shard.numel()].copy_(param_shard) # type: ignore[attr-defined]
|
||||
if writeback_grad:
|
||||
existing_grad = handle.sharded_grad
|
||||
if existing_grad is not None:
|
||||
if handle.flat_param.grad is None:
|
||||
raise AssertionError("Expected handle.flat_param.grad to not be None")
|
||||
grad_shard = _get_shard(handle.flat_param.grad)
|
||||
existing_grad[: grad_shard.numel()].copy_(grad_shard)
|
||||
|
||||
|
||||
def _deregister_flat_param(state: _FSDPState, module: nn.Module) -> None:
|
||||
"""
|
||||
De-registers the flattened parameter from the wrapped module, hiding it
|
||||
from ``nn.Module`` methods.
|
||||
|
||||
We do not use ``del`` because we want ``FLAT_PARAM`` to always be an
|
||||
attribute but dynamically change whether it is visible to ``nn.Module``
|
||||
methods.
|
||||
"""
|
||||
if _has_fsdp_params(state, module):
|
||||
# TODO: figure out the case for the composable APIs.
|
||||
cast(nn.Module, module.module)._parameters.pop(FLAT_PARAM, None)
|
||||
|
||||
|
||||
def _register_flat_param(state: _FSDPState, module: nn.Module) -> None:
|
||||
"""
|
||||
Registers the flattened parameter to the wrapped module, making it
|
||||
visible to ``nn.Module`` methods.
|
||||
|
||||
We do not use :meth:`nn.Module.register_parameter` because we want
|
||||
``FLAT_PARAM`` to always be an attribute but dynamically change whether
|
||||
it is visible to ``nn.Module`` methods.
|
||||
"""
|
||||
handle = _module_handle(state, module)
|
||||
if _has_fsdp_params(state, module):
|
||||
# TODO: figure out the case for the composable APIs.
|
||||
cast(nn.Module, module.module)._parameters[FLAT_PARAM] = handle.flat_param
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unflatten_as_params(state: _FSDPState, module: nn.Module) -> Generator:
|
||||
"""
|
||||
Assumes that the flattened parameter is unsharded. When in the context,
|
||||
de-registers the flattened parameter and unflattens the original
|
||||
parameters as ``nn.Parameter`` views into the flattened parameter.
|
||||
After the context, re-registers the flattened parameter and restores
|
||||
the original parameters as ``Tensor`` views into the flattened
|
||||
parameter.
|
||||
"""
|
||||
handle = _module_handle(state, module)
|
||||
if not handle:
|
||||
yield
|
||||
else:
|
||||
_deregister_flat_param(state, module)
|
||||
try:
|
||||
with handle.unflatten_as_params():
|
||||
yield
|
||||
finally:
|
||||
if not handle._use_orig_params:
|
||||
_register_flat_param(state, module)
|
||||
|
||||
|
||||
def _validate_unshard_params_args(
|
||||
state: _FSDPState,
|
||||
writeback: bool,
|
||||
rank0_only: bool,
|
||||
offload_to_cpu: bool,
|
||||
with_grads: bool,
|
||||
) -> None:
|
||||
if with_grads and (offload_to_cpu or not state._use_orig_params):
|
||||
raise NotImplementedError(
|
||||
f"with_grads={with_grads}, "
|
||||
f"use_orig_params={state._use_orig_params}, "
|
||||
f"offload_to_cpu={offload_to_cpu} "
|
||||
f"is not supported yet"
|
||||
)
|
||||
if offload_to_cpu and state._handle and (not state._handle.uses_sharded_strategy):
|
||||
raise NotImplementedError(
|
||||
"offload_to_cpu=True and NO_SHARD is not supported yet"
|
||||
)
|
||||
if writeback and rank0_only:
|
||||
# TODO: Rank 0 can broadcast the `FlatParameter` to allow all ranks to
|
||||
# persist the changes.
|
||||
raise NotImplementedError(
|
||||
"writeback=True and rank0_only=True is not supported yet"
|
||||
)
|
||||
if offload_to_cpu and not rank0_only:
|
||||
warnings.warn(
|
||||
"offload_to_cpu=True and rank0_only=False may result in the"
|
||||
"unsharded parameters being redundantly copied to CPU memory for "
|
||||
"GPUs sharing the same CPU memory, which risks CPU OOM. We "
|
||||
"recommend using offload_to_cpu=True with rank0_only=True.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unshard_fsdp_state_params(
|
||||
module: nn.Module,
|
||||
state: _FSDPState,
|
||||
writeback: bool,
|
||||
rank0_only: bool,
|
||||
offload_to_cpu: bool,
|
||||
with_grads: bool,
|
||||
):
|
||||
"""
|
||||
This unshards the parameters for a single FSDP state ``state`` that
|
||||
corresponds to ``module``.
|
||||
"""
|
||||
_validate_unshard_params_args(
|
||||
state, writeback, rank0_only, offload_to_cpu, with_grads
|
||||
)
|
||||
state._device_handle.synchronize()
|
||||
# If handles are shared by other module(s), the handle may be already unsharded.
|
||||
maybe_handle = _module_handle(state, module)
|
||||
handle = None
|
||||
if (
|
||||
maybe_handle
|
||||
and maybe_handle._training_state != HandleTrainingState.SUMMON_FULL_PARAMS
|
||||
):
|
||||
handle = maybe_handle
|
||||
if not handle:
|
||||
yield
|
||||
return
|
||||
|
||||
if handle._training_state != HandleTrainingState.IDLE:
|
||||
raise AssertionError(
|
||||
f"Expects the handle training to be IDLE but got {handle._training_state}"
|
||||
)
|
||||
|
||||
handle._training_state = HandleTrainingState.SUMMON_FULL_PARAMS
|
||||
|
||||
_reset_flat_param_grad_info_if_needed(handle)
|
||||
free_unsharded_flat_param = handle.needs_unshard()
|
||||
# No need to call `wait_stream()` since we unshard in the computation
|
||||
# stream directly
|
||||
computation_stream = state._device_handle.current_stream()
|
||||
_unshard(state, handle, computation_stream, computation_stream)
|
||||
if with_grads:
|
||||
_unshard_grads(handle)
|
||||
|
||||
if rank0_only and state.rank != 0:
|
||||
# Free the unsharded flattened parameter early
|
||||
_reshard(state, handle, free_unsharded_flat_param)
|
||||
if with_grads:
|
||||
_reshard_grads(handle)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
handle._training_state = HandleTrainingState.IDLE
|
||||
else:
|
||||
# Unflatten the unsharded flattened parameters
|
||||
with contextlib.ExitStack() as stack:
|
||||
# Invariant: rank == 0 or !rank0_only
|
||||
if offload_to_cpu and handle.uses_sharded_strategy:
|
||||
stack.enter_context(handle.to_cpu())
|
||||
# NOTE: Since PyTorch enforces that a parameter and its
|
||||
# gradients need to match metadata (e.g. device), we must
|
||||
# move gradients to CPU *after* we move parameters.
|
||||
# NOTE: This assumes 1 `FlatParameter`
|
||||
if not state._use_orig_params:
|
||||
stack.enter_context(_unflatten_as_params(state, module))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stack.close()
|
||||
if writeback:
|
||||
_writeback_to_local_shard(handle, with_grads)
|
||||
_reshard(state, handle, free_unsharded_flat_param)
|
||||
if with_grads:
|
||||
_reshard_grads(handle)
|
||||
handle._training_state = HandleTrainingState.IDLE
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unshard_params_for_summon(
|
||||
module: nn.Module,
|
||||
state: _FSDPState,
|
||||
writeback: bool,
|
||||
rank0_only: bool,
|
||||
offload_to_cpu: bool,
|
||||
with_grads: bool,
|
||||
):
|
||||
_validate_unshard_params_args(
|
||||
state, writeback, rank0_only, offload_to_cpu, with_grads
|
||||
)
|
||||
_lazy_init(state, module)
|
||||
if state.training_state == TrainingState.FORWARD_BACKWARD:
|
||||
raise AssertionError(
|
||||
"Cannot manually unshard parameters during forward/backward"
|
||||
)
|
||||
elif state.training_state == TrainingState.SUMMON_FULL_PARAMS:
|
||||
raise AssertionError(
|
||||
"Cannot manually unshard parameters when already unsharding parameters"
|
||||
)
|
||||
with _unshard_fsdp_state_params(
|
||||
module=module,
|
||||
state=state,
|
||||
writeback=writeback,
|
||||
rank0_only=rank0_only,
|
||||
offload_to_cpu=offload_to_cpu,
|
||||
with_grads=with_grads,
|
||||
):
|
||||
try:
|
||||
state.training_state = TrainingState.SUMMON_FULL_PARAMS
|
||||
yield
|
||||
finally:
|
||||
state.training_state = TrainingState.IDLE
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unshard_params(
|
||||
module: nn.Module,
|
||||
recurse: bool,
|
||||
writeback: bool,
|
||||
rank0_only: bool,
|
||||
offload_to_cpu: bool,
|
||||
with_grads: bool,
|
||||
):
|
||||
"""
|
||||
This unshards FSDP-managed parameters for all modules with FSDP applied in
|
||||
the module tree rooted at ``module``.
|
||||
"""
|
||||
if not recurse:
|
||||
optional_state = _get_module_fsdp_state(module)
|
||||
if optional_state is None:
|
||||
with contextlib.nullcontext():
|
||||
yield
|
||||
return
|
||||
states_and_modules = ([optional_state], [module])
|
||||
else:
|
||||
states_and_modules = traversal_utils._get_fsdp_states_with_modules(module)
|
||||
with contextlib.ExitStack() as stack:
|
||||
for state, module in zip(*states_and_modules):
|
||||
stack.enter_context(
|
||||
_unshard_params_for_summon(
|
||||
module=module,
|
||||
state=state,
|
||||
writeback=writeback,
|
||||
rank0_only=rank0_only,
|
||||
offload_to_cpu=offload_to_cpu,
|
||||
with_grads=with_grads,
|
||||
)
|
||||
)
|
||||
yield
|
||||
|
||||
|
||||
def _deregister_orig_params(state: _FSDPState, module: nn.Module) -> None:
|
||||
"""
|
||||
Deregisters the original parameters; registers the ``FlatParameter``.
|
||||
"""
|
||||
handle = _module_handle(state, module)
|
||||
if not handle:
|
||||
return
|
||||
_p_assert(
|
||||
handle._use_orig_params,
|
||||
f"Inconsistent `_use_orig_params` -- FSDP: {state._use_orig_params} "
|
||||
f"handle: {handle._use_orig_params}",
|
||||
)
|
||||
handle._deregister_orig_params()
|
||||
_register_flat_param(state, module)
|
||||
|
||||
|
||||
def _register_orig_params(state: _FSDPState, module: nn.Module) -> None:
|
||||
"""
|
||||
Deregisters the ``FlatParameter``; registers the original parameters.
|
||||
"""
|
||||
handle = _module_handle(state, module)
|
||||
if not handle:
|
||||
return
|
||||
_deregister_flat_param(state, module)
|
||||
if handle.is_sharded(handle.flat_param):
|
||||
handle._use_sharded_views()
|
||||
handle._use_sharded_grad_views()
|
||||
else:
|
||||
handle._use_unsharded_views(as_params=True)
|
||||
@@ -0,0 +1,264 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import collections
|
||||
import functools
|
||||
import inspect
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import Any
|
||||
|
||||
import torch.nn as nn
|
||||
from torch.distributed.fsdp._common_utils import (
|
||||
_get_module_fsdp_state,
|
||||
_override_module_mixed_precision,
|
||||
)
|
||||
from torch.distributed.fsdp.wrap import (
|
||||
_construct_wrap_fn,
|
||||
_or_policy,
|
||||
_Policy,
|
||||
_post_order_apply,
|
||||
_recursive_wrap,
|
||||
_run_mixed_precision_override_policy,
|
||||
_wrap_module_cls_individually,
|
||||
)
|
||||
|
||||
|
||||
def _auto_wrap(
|
||||
root_module: nn.Module,
|
||||
policy: Callable | _Policy,
|
||||
ignored_modules: set[nn.Module],
|
||||
ignored_params: set[nn.Parameter],
|
||||
root_kwargs: dict[str, Any],
|
||||
fsdp_fn: Callable, # e.g. `FullyShardedDataParallel` or `fully_shard`
|
||||
):
|
||||
"""
|
||||
Auto wraps modules in ``root_module`` 's tree according to ``policy``
|
||||
following a post-order traversal.
|
||||
|
||||
Precondition: ``root_kwargs`` should contain all arguments except
|
||||
``module``. This function accepts the kwargs dict directly since it gets
|
||||
forwarded into the post-order traversal function.
|
||||
"""
|
||||
mixed_precision = root_kwargs["mixed_precision"]
|
||||
is_wrapper = inspect.isclass(fsdp_fn)
|
||||
# TODO: We may relax this no-nested-wrapping constraint to support manual
|
||||
# wrapping followed by auto wrapping.
|
||||
_check_nested_wrapping(root_module)
|
||||
|
||||
if isinstance(policy, _Policy):
|
||||
root_kwargs["auto_wrap_policy" if is_wrapper else "policy"] = None
|
||||
target_module_to_kwargs = policy._run_policy(
|
||||
root_module, ignored_modules, root_kwargs
|
||||
)
|
||||
if mixed_precision is not None:
|
||||
target_module_to_kwargs = _run_mixed_precision_override_policy(
|
||||
root_module,
|
||||
mixed_precision._module_classes_to_ignore,
|
||||
ignored_modules,
|
||||
root_kwargs,
|
||||
target_module_to_kwargs,
|
||||
)
|
||||
overridden_module_classes = _override_module_mixed_precision(
|
||||
root_module, mixed_precision._module_classes_to_ignore
|
||||
)
|
||||
_warn_on_overridden_mixed_precision(overridden_module_classes)
|
||||
use_orig_params = root_kwargs.get("use_orig_params", False)
|
||||
_validate_frozen_params(
|
||||
root_module,
|
||||
set(target_module_to_kwargs.keys()),
|
||||
ignored_params,
|
||||
use_orig_params,
|
||||
)
|
||||
wrap_fn = _construct_wrap_fn(root_module, target_module_to_kwargs, fsdp_fn)
|
||||
_post_order_apply(root_module, wrap_fn)
|
||||
return
|
||||
|
||||
recursive_wrap_kwargs = {
|
||||
"module": root_module,
|
||||
"auto_wrap_policy": policy,
|
||||
"wrapper_cls": fsdp_fn,
|
||||
"ignored_modules": ignored_modules,
|
||||
"ignored_params": ignored_params,
|
||||
"only_wrap_children": True,
|
||||
}
|
||||
if mixed_precision is not None:
|
||||
# Wrap modules of the ignored types separately and register forward
|
||||
# hooks to cast to fp32 and back to the original dtype, respectively
|
||||
overridden_module_classes = _override_module_mixed_precision(
|
||||
root_module, mixed_precision._module_classes_to_ignore
|
||||
)
|
||||
policy = functools.partial(
|
||||
_or_policy,
|
||||
policies=[
|
||||
policy,
|
||||
partial(
|
||||
_wrap_module_cls_individually,
|
||||
module_classes=mixed_precision._module_classes_to_ignore,
|
||||
),
|
||||
],
|
||||
)
|
||||
recursive_wrap_kwargs["auto_wrap_policy"] = policy
|
||||
_warn_on_overridden_mixed_precision(overridden_module_classes)
|
||||
_recursive_wrap(**recursive_wrap_kwargs, **root_kwargs) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _check_nested_wrapping(root_module: nn.Module):
|
||||
for module_name, module in root_module.named_modules():
|
||||
if _get_module_fsdp_state(module) is not None:
|
||||
raise ValueError(
|
||||
"FSDP auto wrapping requires modules to not already have "
|
||||
f"FSDP applied but found {module_name} in\n{root_module}"
|
||||
)
|
||||
|
||||
|
||||
def _warn_on_overridden_mixed_precision(
|
||||
overridden_module_classes: set[type[nn.Module]],
|
||||
):
|
||||
if len(overridden_module_classes) == 0:
|
||||
return
|
||||
warnings.warn(
|
||||
"Both mixed precision and an auto_wrap_policy were specified to FSDP, "
|
||||
f"where the wrapped module has submodules of type:\n{overridden_module_classes}\n"
|
||||
"These modules will be wrapped as separate FSDP instacnes with mixed "
|
||||
"precision disabled.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
def _validate_frozen_params(
|
||||
root_module: nn.Module,
|
||||
modules_to_wrap: set[nn.Module],
|
||||
ignored_params: set[nn.Parameter],
|
||||
use_orig_params: bool,
|
||||
):
|
||||
"""
|
||||
This checks that, given ``modules_to_wrap``, each module would manage
|
||||
parameters that are uniformly frozen or non-frozen. This uniformity
|
||||
requirement is strict for ``use_orig_params=False`` (hard error) and highly
|
||||
recommended for ``use_orig_params=True`` (user warning).
|
||||
"""
|
||||
post_order_named_modules = _get_post_order_named_modules(root_module)
|
||||
visited_modules: set[nn.Module] = set()
|
||||
for module_name, module in post_order_named_modules:
|
||||
if module in modules_to_wrap:
|
||||
param_to_fqn = _get_managed_param_to_fqn(
|
||||
module, ignored_params, visited_modules, module_name
|
||||
)
|
||||
frozen_param_fqns: list[str] = []
|
||||
frozen_param_numel = 0
|
||||
nonfrozen_param_fqns: list[str] = []
|
||||
nonfrozen_param_numel = 0
|
||||
for param, fqn in param_to_fqn.items():
|
||||
if param.requires_grad:
|
||||
nonfrozen_param_fqns.append(fqn)
|
||||
nonfrozen_param_numel += param.numel()
|
||||
else:
|
||||
frozen_param_fqns.append(fqn)
|
||||
frozen_param_numel += param.numel()
|
||||
if len(frozen_param_fqns) > 0 and len(nonfrozen_param_fqns) > 0:
|
||||
msg = f"{module_name} has both parameters with requires_grad=True and False."
|
||||
if use_orig_params:
|
||||
total_param_numel = frozen_param_numel + nonfrozen_param_numel
|
||||
msg += (
|
||||
" We do not recommend wrapping such modules since "
|
||||
"the gradient memory usage will be higher than expected "
|
||||
f"({total_param_numel} numel instead of {nonfrozen_param_numel} numel "
|
||||
"before sharding via reduce-scatter). "
|
||||
)
|
||||
else:
|
||||
msg += " FSDP does not support wrapping such modules when use_orig_params=False. "
|
||||
msg += "If possible, wrap the frozen parameters with FSDP separately.\n"
|
||||
msg += (
|
||||
f"The following parameters have requires_grad=True:\n{nonfrozen_param_fqns}\n"
|
||||
f"The following parameters have requires_grad=False:\n{frozen_param_fqns}"
|
||||
)
|
||||
if use_orig_params:
|
||||
warnings.warn(msg, stacklevel=2)
|
||||
else:
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _get_post_order_named_modules(
|
||||
root_module: nn.Module,
|
||||
) -> list[tuple[str, nn.Module]]:
|
||||
"""
|
||||
This returns the named modules following a post-order traversal, which is a
|
||||
valid reverse topological sort. We achieve this using the reverse of a
|
||||
stack-based DFS order instead of reversing ``root_module.named_modules()``
|
||||
since the former gives the modules in registration order at each level in
|
||||
the module tree (as opposed to the reverse), which allows us to error/warn
|
||||
on the first registered module that violates the condition.
|
||||
|
||||
For example, consider the following module structure:
|
||||
M(
|
||||
S1(),
|
||||
S2(
|
||||
SS1(),
|
||||
SS2(),
|
||||
),
|
||||
S3(),
|
||||
)
|
||||
The reverse DFS order is [S1, SS1, SS2, S2, S3, M], while the reverse
|
||||
``named_modules()`` order is [S3, SS2, SS1, S2, S1, M].
|
||||
"""
|
||||
visited_modules = {root_module}
|
||||
stack = [("", root_module)]
|
||||
# Append and reverse at the end for linear-time algorithm
|
||||
reverse_post_order_named_modules: list[tuple[str, nn.Module]] = []
|
||||
while stack:
|
||||
module_name, module = stack.pop()
|
||||
reverse_post_order_named_modules.append((module_name, module))
|
||||
for child_module_name, child_module in module.named_children():
|
||||
if child_module is None: # only for overrides of `named_children()`
|
||||
continue
|
||||
if child_module not in visited_modules:
|
||||
visited_modules.add(child_module)
|
||||
if module_name != "":
|
||||
child_module_name = module_name + "." + child_module_name
|
||||
stack.append((child_module_name, child_module))
|
||||
post_order_named_modules = list(reversed(reverse_post_order_named_modules))
|
||||
return post_order_named_modules
|
||||
|
||||
|
||||
def _get_managed_param_to_fqn(
|
||||
module_to_wrap: nn.Module,
|
||||
ignored_params: set[nn.Parameter],
|
||||
visited_modules: set[nn.Module],
|
||||
root_prefix: str,
|
||||
) -> dict[nn.Parameter, str]:
|
||||
"""
|
||||
This returns a dict that maps managed parameter to its FQN for the given
|
||||
``module_to_wrap``. The dict's keys are exactly the parameters that would
|
||||
be managed by the module, where this is achieved by calling this function
|
||||
on the modules to wrap in reverse topological order, destructively updating
|
||||
``visited_modules``, and not traversing into those modules. The FQNs are
|
||||
prefixed from the root (via ``root_prefix``) to be more informative.
|
||||
|
||||
NOTE: This function is meant to be called pre-wrapping and iteratively in
|
||||
reverse topological order to cover the full module tree. This differs from
|
||||
the ``_get_param_to_fqn()`` function meant to be called post-wrapping and
|
||||
on the full module tree in one shot. Given those differences, we do not try
|
||||
to unify the two.
|
||||
"""
|
||||
param_to_fqn: dict[nn.Parameter, str] = {}
|
||||
# Run BFS (or any tree traversal works)
|
||||
queue = collections.deque([(module_to_wrap, root_prefix)])
|
||||
visited_modules.add(module_to_wrap)
|
||||
while queue:
|
||||
module, prefix = queue.popleft()
|
||||
for param_name, param in module.named_parameters(recurse=False):
|
||||
if param not in ignored_params:
|
||||
fqn = param_name if prefix == "" else prefix + "." + param_name
|
||||
param_to_fqn[param] = fqn
|
||||
for child_module_name, child_module in module.named_children():
|
||||
if child_module is None: # only for overrides of `named_children()`
|
||||
continue
|
||||
if child_module not in visited_modules:
|
||||
visited_modules.add(child_module)
|
||||
child_prefix = (
|
||||
child_module_name
|
||||
if prefix == ""
|
||||
else prefix + "." + child_module_name
|
||||
)
|
||||
queue.append((child_module, child_prefix))
|
||||
return param_to_fqn
|
||||
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
This file includes public APIs for FSDP such as the classes used for the
|
||||
constructor arguments.
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import auto, Enum
|
||||
|
||||
import torch
|
||||
from torch.nn.modules.batchnorm import _BatchNorm
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ShardingStrategy",
|
||||
"BackwardPrefetch",
|
||||
"MixedPrecision",
|
||||
"CPUOffload",
|
||||
"StateDictType",
|
||||
"StateDictConfig",
|
||||
"FullStateDictConfig",
|
||||
"LocalStateDictConfig",
|
||||
"ShardedStateDictConfig",
|
||||
"OptimStateDictConfig",
|
||||
"FullOptimStateDictConfig",
|
||||
"LocalOptimStateDictConfig",
|
||||
"ShardedOptimStateDictConfig",
|
||||
"StateDictSettings",
|
||||
]
|
||||
|
||||
|
||||
class ShardingStrategy(Enum):
|
||||
"""
|
||||
This specifies the sharding strategy to be used for distributed training by
|
||||
:class:`FullyShardedDataParallel`.
|
||||
|
||||
- ``FULL_SHARD``: Parameters, gradients, and optimizer states are sharded.
|
||||
For the parameters, this strategy unshards (via all-gather) before the
|
||||
forward, reshards after the forward, unshards before the backward
|
||||
computation, and reshards after the backward computation. For gradients,
|
||||
it synchronizes and shards them (via reduce-scatter) after the backward
|
||||
computation. The sharded optimizer states are updated locally per rank.
|
||||
- ``SHARD_GRAD_OP``: Gradients and optimizer states are sharded during
|
||||
computation, and additionally, parameters are sharded outside
|
||||
computation. For the parameters, this strategy unshards before the
|
||||
forward, does not reshard them after the forward, and only reshards them
|
||||
after the backward computation. The sharded optimizer states are updated
|
||||
locally per rank. Inside ``no_sync()``, the parameters are not resharded
|
||||
after the backward computation.
|
||||
- ``NO_SHARD``: Parameters, gradients, and optimizer states are not sharded
|
||||
but instead replicated across ranks similar to PyTorch's
|
||||
:class:`DistributedDataParallel` API. For gradients, this strategy
|
||||
synchronizes them (via all-reduce) after the backward computation. The
|
||||
unsharded optimizer states are updated locally per rank.
|
||||
- ``HYBRID_SHARD``: Apply ``FULL_SHARD`` within a node, and replicate parameters across
|
||||
nodes. This results in reduced communication volume as expensive all-gathers and
|
||||
reduce-scatters are only done within a node, which can be more performant for medium
|
||||
-sized models.
|
||||
- ``_HYBRID_SHARD_ZERO2``: Apply ``SHARD_GRAD_OP`` within a node, and replicate parameters across
|
||||
nodes. This is like ``HYBRID_SHARD``, except this may provide even higher throughput
|
||||
since the unsharded parameters are not freed after the forward pass, saving the
|
||||
all-gathers in the pre-backward.
|
||||
"""
|
||||
|
||||
FULL_SHARD = auto()
|
||||
SHARD_GRAD_OP = auto()
|
||||
NO_SHARD = auto()
|
||||
HYBRID_SHARD = auto()
|
||||
_HYBRID_SHARD_ZERO2 = auto()
|
||||
|
||||
|
||||
class BackwardPrefetch(Enum):
|
||||
"""
|
||||
This configures explicit backward prefetching, which improves throughput by
|
||||
enabling communication and computation overlap in the backward pass at the
|
||||
cost of slightly increased memory usage.
|
||||
|
||||
- ``BACKWARD_PRE``: This enables the most overlap but increases memory
|
||||
usage the most. This prefetches the next set of parameters *before* the
|
||||
current set of parameters' gradient computation. This overlaps the *next
|
||||
all-gather* and the *current gradient computation*, and at the peak, it
|
||||
holds the current set of parameters, next set of parameters, and current
|
||||
set of gradients in memory.
|
||||
- ``BACKWARD_POST``: This enables less overlap but requires less memory
|
||||
usage. This prefetches the next set of parameters *after* the current
|
||||
set of parameters' gradient computation. This overlaps the *current
|
||||
reduce-scatter* and the *next gradient computation*, and it frees the
|
||||
current set of parameters before allocating memory for the next set of
|
||||
parameters, only holding the next set of parameters and current set of
|
||||
gradients in memory at the peak.
|
||||
- FSDP's ``backward_prefetch`` argument accepts ``None``, which disables
|
||||
the backward prefetching altogether. This has no overlap and does not
|
||||
increase memory usage. In general, we do not recommend this setting since
|
||||
it may degrade throughput significantly.
|
||||
|
||||
For more technical context: For a single process group using NCCL backend,
|
||||
any collectives, even if issued from different streams, contend for the
|
||||
same per-device NCCL stream, which implies that the relative order in which
|
||||
the collectives are issued matters for overlapping. The two backward
|
||||
prefetching values correspond to different issue orders.
|
||||
"""
|
||||
|
||||
# NOTE: For both modes, the ordering that defines "current" and "next" is
|
||||
# not always exact in the current implementation. A mistargeted prefetch
|
||||
# simply means that the parameter memory is allocated earlier than needed,
|
||||
# possibly increasing peak memory usage, but does not affect correctness.
|
||||
BACKWARD_PRE = auto()
|
||||
BACKWARD_POST = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class MixedPrecision:
|
||||
"""
|
||||
This configures FSDP-native mixed precision training.
|
||||
|
||||
Attributes:
|
||||
param_dtype (Optional[torch.dtype]): This specifies the dtype for model
|
||||
parameters during forward and backward and thus the dtype for
|
||||
forward and backward computation. Outside forward and backward, the
|
||||
*sharded* parameters are kept in full precision (e.g. for the
|
||||
optimizer step), and for model checkpointing, the parameters are
|
||||
always saved in full precision. (Default: ``None``)
|
||||
reduce_dtype (Optional[torch.dtype]): This specifies the dtype for
|
||||
gradient reduction (i.e. reduce-scatter or all-reduce). If this is
|
||||
``None`` but ``param_dtype`` is not ``None``, then this takes on
|
||||
the ``param_dtype`` value, still running gradient reduction in low
|
||||
precision. This is permitted to differ from ``param_dtype``, e.g.
|
||||
to force gradient reduction to run in full precision. (Default:
|
||||
``None``)
|
||||
buffer_dtype (Optional[torch.dtype]): This specifies the dtype for
|
||||
buffers. FSDP does not shard buffers. Rather, FSDP casts them to
|
||||
``buffer_dtype`` in the first forward pass and keeps them in that
|
||||
dtype thereafter. For model checkpointing, the buffers are saved
|
||||
in full precision except for ``LOCAL_STATE_DICT``. (Default:
|
||||
``None``)
|
||||
keep_low_precision_grads (bool): If ``False``, then FSDP upcasts
|
||||
gradients to full precision after the backward pass in preparation
|
||||
for the optimizer step. If ``True``, then FSDP keeps the gradients
|
||||
in the dtype used for gradient reduction, which can save memory if
|
||||
using a custom optimizer that supports running in low precision.
|
||||
(Default: ``False``)
|
||||
cast_forward_inputs (bool): If ``True``, then this FSDP module casts
|
||||
its forward args and kwargs to ``param_dtype``. This is to ensure
|
||||
that parameter and input dtypes match for forward computation, as
|
||||
required by many ops. This may need to be set to ``True`` when only
|
||||
applying mixed precision to some but not all FSDP modules, in which
|
||||
case a mixed-precision FSDP submodule needs to recast its inputs.
|
||||
(Default: ``False``)
|
||||
cast_root_forward_inputs (bool): If ``True``, then the root FSDP module
|
||||
casts its forward args and kwargs to ``param_dtype``, overriding
|
||||
the value of ``cast_forward_inputs``. For non-root FSDP modules,
|
||||
this does not do anything. (Default: ``True``)
|
||||
_module_classes_to_ignore: (Sequence[Type[nn.Module]]): This specifies
|
||||
module classes to ignore for mixed precision when using an
|
||||
``auto_wrap_policy``: Modules of these classes will have FSDP
|
||||
applied to them separately with mixed precision disabled (meaning
|
||||
that the final FSDP construction would deviate from the specified
|
||||
policy). If ``auto_wrap_policy`` is not specified, then this does
|
||||
not do anything. This API is experimental and subject to change.
|
||||
(Default: ``(_BatchNorm,)``)
|
||||
|
||||
.. note:: This API is experimental and subject to change.
|
||||
|
||||
.. note:: Only floating point tensors are cast to their specified dtypes.
|
||||
|
||||
.. note:: In ``summon_full_params``, parameters are forced to full
|
||||
precision, but buffers are not.
|
||||
|
||||
.. note:: Layer norm and batch norm accumulate in ``float32`` even when
|
||||
their inputs are in a low precision like ``float16`` or ``bfloat16``.
|
||||
Disabling FSDP's mixed precision for those norm modules only means that
|
||||
the affine parameters are kept in ``float32``. However, this incurs
|
||||
separate all-gathers and reduce-scatters for those norm modules, which
|
||||
may be inefficient, so if the workload permits, the user should prefer
|
||||
to still apply mixed precision to those modules.
|
||||
|
||||
.. note:: By default, if the user passes a model with any ``_BatchNorm``
|
||||
modules and specifies an ``auto_wrap_policy``, then the batch norm
|
||||
modules will have FSDP applied to them separately with mixed precision
|
||||
disabled. See the ``_module_classes_to_ignore`` argument.
|
||||
|
||||
.. note:: ``MixedPrecision`` has ``cast_root_forward_inputs=True`` and
|
||||
``cast_forward_inputs=False`` by default. For the root FSDP instance,
|
||||
its ``cast_root_forward_inputs`` takes precedence over its
|
||||
``cast_forward_inputs``. For non-root FSDP instances, their
|
||||
``cast_root_forward_inputs`` values are ignored. The default setting is
|
||||
sufficient for the typical case where each FSDP instance has the same
|
||||
``MixedPrecision`` configuration and only needs to cast inputs to the
|
||||
``param_dtype`` at the beginning of the model's forward pass.
|
||||
|
||||
.. note:: For nested FSDP instances with different ``MixedPrecision``
|
||||
configurations, we recommend setting individual ``cast_forward_inputs``
|
||||
values to configure casting inputs or not before each instance's
|
||||
forward. In such a case, since the casts happen before each FSDP
|
||||
instance's forward, a parent FSDP instance should have its non-FSDP
|
||||
submodules run before its FSDP submodules to avoid the activation dtype
|
||||
being changed due to a different ``MixedPrecision`` configuration.
|
||||
|
||||
Example::
|
||||
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> model = nn.Sequential(nn.Linear(3, 3), nn.Linear(3, 3))
|
||||
>>> model[1] = FSDP(
|
||||
>>> model[1],
|
||||
>>> mixed_precision=MixedPrecision(param_dtype=torch.float16, cast_forward_inputs=True),
|
||||
>>> )
|
||||
>>> model = FSDP(
|
||||
>>> model,
|
||||
>>> mixed_precision=MixedPrecision(param_dtype=torch.bfloat16, cast_forward_inputs=True),
|
||||
>>> )
|
||||
|
||||
The above shows a working example. On the other hand, if ``model[1]``
|
||||
were replaced with ``model[0]``, meaning that the submodule using
|
||||
different ``MixedPrecision`` ran its forward first, then ``model[1]``
|
||||
would incorrectly see ``float16`` activations instead of ``bfloat16``
|
||||
ones.
|
||||
|
||||
"""
|
||||
|
||||
param_dtype: torch.dtype | None = None
|
||||
reduce_dtype: torch.dtype | None = None
|
||||
buffer_dtype: torch.dtype | None = None
|
||||
keep_low_precision_grads: bool = False
|
||||
cast_forward_inputs: bool = False
|
||||
cast_root_forward_inputs: bool = True
|
||||
_module_classes_to_ignore: Sequence[type[torch.nn.Module]] = (_BatchNorm,)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CPUOffload:
|
||||
"""
|
||||
This configures CPU offloading.
|
||||
|
||||
Attributes:
|
||||
offload_params (bool): This specifies whether to offload parameters to
|
||||
CPU when not involved in computation. If ``True``, then this
|
||||
offloads gradients to CPU as well, meaning that the optimizer step
|
||||
runs on CPU.
|
||||
"""
|
||||
|
||||
offload_params: bool = False
|
||||
|
||||
|
||||
class StateDictType(Enum):
|
||||
"""
|
||||
This enum indicates that which type of ``state_dict`` the FSDP module is
|
||||
currently processing (returning or loading).
|
||||
The default value is FULL_STATE_DICT to comply the PyTorch convention.
|
||||
|
||||
.. note::
|
||||
FSDP currently supports three types of ``state_dict``:
|
||||
1. ``state_dict/load_state_dict`: this pair of APIs return and load
|
||||
the non-sharded, unflattened parameters. The semantics is the
|
||||
same as using DDP.
|
||||
2. ``_local_state_dict/_load_local_state_dict``: this pair of APIs return
|
||||
and load local sharded, flattened parameters. The values returned
|
||||
by ``_local_state_dict`` can be directly used by FSDP and is only
|
||||
meaningful to FSDP (because parameters are flattened). Note that
|
||||
these APIs are meant for use via the :func:`state_dict_type`
|
||||
context manager as follows:
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> with fsdp.state_dict_type(StateDictType.LOCAL_STATE_DICT):
|
||||
... state = fsdp.state_dict() # loads local state dict
|
||||
3. ``_sharded_state_dict/_load_sharded_state_dict``: this pair of APIs
|
||||
return and load sharded, unflattened parameters. The ``state_dict``
|
||||
return by ``sharded_state_dict`` can be used by all other parallel
|
||||
schemes (resharding may be required).
|
||||
"""
|
||||
|
||||
FULL_STATE_DICT = auto()
|
||||
LOCAL_STATE_DICT = auto()
|
||||
SHARDED_STATE_DICT = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateDictConfig:
|
||||
"""
|
||||
``StateDictConfig`` is the base class for all ``state_dict`` configuration
|
||||
classes. Users should instantiate a child class (e.g.
|
||||
``FullStateDictConfig``) in order to configure settings for the
|
||||
corresponding ``state_dict`` type supported by FSDP.
|
||||
|
||||
Attributes:
|
||||
offload_to_cpu (bool): If ``True``, then FSDP offloads the state dict
|
||||
values to CPU, and if ``False``, then FSDP keeps them on GPU.
|
||||
(Default: ``False``)
|
||||
"""
|
||||
|
||||
offload_to_cpu: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class FullStateDictConfig(StateDictConfig):
|
||||
"""
|
||||
``FullStateDictConfig`` is a config class meant to be used with
|
||||
``StateDictType.FULL_STATE_DICT``. We recommend enabling both
|
||||
``offload_to_cpu=True`` and ``rank0_only=True`` when saving full state
|
||||
dicts to save GPU memory and CPU memory, respectively. This config class
|
||||
is meant to be used via the :func:`state_dict_type` context manager as
|
||||
follows:
|
||||
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
|
||||
>>> fsdp = FSDP(model, auto_wrap_policy=...)
|
||||
>>> cfg = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
|
||||
>>> with FSDP.state_dict_type(fsdp, StateDictType.FULL_STATE_DICT, cfg):
|
||||
>>> state = fsdp.state_dict()
|
||||
>>> # `state` will be empty on non rank 0 and contain CPU tensors on rank 0.
|
||||
>>> # To reload checkpoint for inference, finetuning, transfer learning, etc:
|
||||
>>> model = model_fn() # Initialize model in preparation for wrapping with FSDP
|
||||
>>> if dist.get_rank() == 0:
|
||||
>>> # Load checkpoint only on rank 0 to avoid memory redundancy
|
||||
>>> state_dict = torch.load("my_checkpoint.pt")
|
||||
>>> model.load_state_dict(state_dict)
|
||||
>>> # All ranks initialize FSDP module as usual. `sync_module_states` argument
|
||||
>>> # communicates loaded checkpoint states from rank 0 to rest of the world.
|
||||
>>> fsdp = FSDP(
|
||||
... model,
|
||||
... device_id=torch.cuda.current_device(),
|
||||
... auto_wrap_policy=...,
|
||||
... sync_module_states=True,
|
||||
... )
|
||||
>>> # After this point, all ranks have FSDP model with loaded checkpoint.
|
||||
|
||||
Attributes:
|
||||
rank0_only (bool): If ``True``, then only rank 0 saves the full state
|
||||
dict, and nonzero ranks save an empty dict. If ``False``, then all
|
||||
ranks save the full state dict. (Default: ``False``)
|
||||
"""
|
||||
|
||||
rank0_only: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalStateDictConfig(StateDictConfig):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShardedStateDictConfig(StateDictConfig):
|
||||
"""
|
||||
``ShardedStateDictConfig`` is a config class meant to be used with
|
||||
``StateDictType.SHARDED_STATE_DICT``.
|
||||
|
||||
Attributes:
|
||||
_use_dtensor (bool): If ``True``, then FSDP saves the state dict values
|
||||
as ``DTensor``, and if ``False``, then FSDP saves them as
|
||||
``ShardedTensor``. (Default: ``False``)
|
||||
|
||||
.. warning:: ``_use_dtensor`` is a private field of :class:`ShardedStateDictConfig`
|
||||
and it is used by FSDP to determine the type of state dict values. Users should not
|
||||
manually modify ``_use_dtensor``.
|
||||
"""
|
||||
|
||||
_use_dtensor: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class OptimStateDictConfig:
|
||||
"""
|
||||
``OptimStateDictConfig`` is the base class for all ``optim_state_dict``
|
||||
configuration classes. Users should instantiate a child class (e.g.
|
||||
``FullOptimStateDictConfig``) in order to configure settings for the
|
||||
corresponding ``optim_state_dict`` type supported by FSDP.
|
||||
|
||||
Attributes:
|
||||
offload_to_cpu (bool): If ``True``, then FSDP offloads the state dict's
|
||||
tensor values to CPU, and if ``False``, then FSDP keeps them on the
|
||||
original device (which is GPU unless parameter CPU offloading is
|
||||
enabled). (Default: ``True``)
|
||||
"""
|
||||
|
||||
offload_to_cpu: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class FullOptimStateDictConfig(OptimStateDictConfig):
|
||||
"""
|
||||
Attributes:
|
||||
rank0_only (bool): If ``True``, then only rank 0 saves the full state
|
||||
dict, and nonzero ranks save an empty dict. If ``False``, then all
|
||||
ranks save the full state dict. (Default: ``False``)
|
||||
"""
|
||||
|
||||
rank0_only: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalOptimStateDictConfig(OptimStateDictConfig):
|
||||
offload_to_cpu: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShardedOptimStateDictConfig(OptimStateDictConfig):
|
||||
"""
|
||||
``ShardedOptimStateDictConfig`` is a config class meant to be used with
|
||||
``StateDictType.SHARDED_STATE_DICT``.
|
||||
|
||||
Attributes:
|
||||
_use_dtensor (bool): If ``True``, then FSDP saves the state dict values
|
||||
as ``DTensor``, and if ``False``, then FSDP saves them as
|
||||
``ShardedTensor``. (Default: ``False``)
|
||||
|
||||
.. warning:: ``_use_dtensor`` is a private field of :class:`ShardedOptimStateDictConfig`
|
||||
and it is used by FSDP to determine the type of state dict values. Users should not
|
||||
manually modify ``_use_dtensor``.
|
||||
"""
|
||||
|
||||
_use_dtensor: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class StateDictSettings:
|
||||
state_dict_type: StateDictType
|
||||
state_dict_config: StateDictConfig
|
||||
optim_state_dict_config: OptimStateDictConfig
|
||||
+2167
File diff suppressed because it is too large
Load Diff
+377
@@ -0,0 +1,377 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import logging
|
||||
from collections import abc, defaultdict
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, overload
|
||||
|
||||
import torch
|
||||
import torch.distributed as dist
|
||||
from torch.amp.grad_scaler import _MultiDeviceReplicator, GradScaler, OptState
|
||||
from torch.distributed.distributed_c10d import ProcessGroup
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _refresh_per_optimizer_state() -> dict[str, Any]:
|
||||
return {"stage": OptState.READY, "found_inf_per_device": {}}
|
||||
|
||||
|
||||
def _is_supported_device(tensor: torch.Tensor) -> bool:
|
||||
return tensor.is_cuda or tensor.device.type in (
|
||||
"xla",
|
||||
"cpu",
|
||||
"hpu",
|
||||
"mtia",
|
||||
"xpu",
|
||||
torch._C._get_privateuse1_backend_name(),
|
||||
)
|
||||
|
||||
|
||||
class _GeneralMultiDeviceReplicator(_MultiDeviceReplicator):
|
||||
"""
|
||||
Lazily serves tensor to request device. This class extends
|
||||
_MultiDeviceReplicator to allow support for "cpu" as a device.
|
||||
"""
|
||||
|
||||
def __init__(self, master_tensor: torch.Tensor) -> None:
|
||||
if not _is_supported_device(master_tensor):
|
||||
raise AssertionError(
|
||||
f"Expected supported device, got {master_tensor.device}"
|
||||
)
|
||||
self.master = master_tensor
|
||||
self._per_device_tensors: dict[torch.device, torch.Tensor] = {}
|
||||
|
||||
|
||||
class ShardedGradScaler(GradScaler):
|
||||
"""
|
||||
ShardedGradScaler helps perform gradient scaling in a shard aware manner. It extends
|
||||
functionality from GradScaler:
|
||||
* Supports Pytorch DDP and FSDP implementations
|
||||
* Support CPU offloaded tensors (as used in fully sharded data parallel[FSDP])
|
||||
* Supports the custom Mixed Precision loss dtype (fp16, bf16) that FSDP returns
|
||||
* Sync inf/nan for scaled gradient tensors on any torch.device (where tensors are placed) across
|
||||
nodes
|
||||
|
||||
Example::
|
||||
|
||||
# Creates a ShardedGradScaler once at the beginning of training.
|
||||
scaler = ShardedGradScaler()
|
||||
|
||||
for epoch in epochs:
|
||||
for input, target in data:
|
||||
optimizer.zero_grad()
|
||||
output = model(input)
|
||||
loss = loss_fn(output, target)
|
||||
|
||||
# Scales loss. Calls backward() on scaled loss to create scaled gradients.
|
||||
scaler.scale(loss).backward()
|
||||
|
||||
# scaler.step() first unscales gradients of the optimizer's params.
|
||||
# If gradients don't contain infs/NaNs, optimizer.step() is then called,
|
||||
# otherwise, optimizer.step() is skipped.
|
||||
scaler.step(optimizer)
|
||||
|
||||
# Updates the scale for next iteration.
|
||||
scaler.update()
|
||||
|
||||
See :class:`GradScaler` for explanation of scaling/unscaling and more use cases.
|
||||
|
||||
Args:
|
||||
init_scale (float, optional, default=2.**16): Initial scale factor.
|
||||
growth_factor (float, optional, default=2.0): Factor by which the scale is multiplied during
|
||||
:meth:`update` if no inf/NaN gradients occur for ``growth_interval`` consecutive iterations.
|
||||
backoff_factor (float, optional, default=0.5): Factor by which the scale is multiplied during
|
||||
:meth:`update` if inf/NaN gradients occur in an iteration.
|
||||
growth_interval (int, optional, default=2000): Number of consecutive iterations without inf/NaN gradients
|
||||
that must occur for the scale to be multiplied by ``growth_factor``.
|
||||
enabled (bool, optional): If ``False``, disables gradient scaling. :meth:`step` simply
|
||||
invokes the underlying ``optimizer.step()``, and other methods become no-ops.
|
||||
Default: ``True``
|
||||
process_group (ProcessGroup, optional, default=torch.distributed.group.WORLD):
|
||||
process group for sharding
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: str = "cuda",
|
||||
init_scale: float = 2.0**16,
|
||||
backoff_factor: float = 0.5,
|
||||
growth_factor: float = 2.0,
|
||||
growth_interval: int = 2000,
|
||||
enabled: bool = True,
|
||||
process_group: ProcessGroup | None = dist.group.WORLD,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
device,
|
||||
init_scale=init_scale,
|
||||
backoff_factor=backoff_factor,
|
||||
growth_factor=growth_factor,
|
||||
growth_interval=growth_interval,
|
||||
enabled=enabled,
|
||||
)
|
||||
if self._enabled:
|
||||
self.process_group = process_group
|
||||
self._per_optimizer_states = defaultdict(_refresh_per_optimizer_state)
|
||||
|
||||
@overload
|
||||
def scale(self, outputs: torch.Tensor) -> torch.Tensor: ...
|
||||
|
||||
@overload
|
||||
def scale(self, outputs: list[torch.Tensor]) -> list[torch.Tensor]: ...
|
||||
|
||||
@overload
|
||||
def scale(self, outputs: tuple[torch.Tensor, ...]) -> tuple[torch.Tensor, ...]: ...
|
||||
|
||||
@overload
|
||||
def scale(self, outputs: Iterable[torch.Tensor]) -> Iterable[torch.Tensor]: ...
|
||||
|
||||
def scale(
|
||||
self, outputs: torch.Tensor | Iterable[torch.Tensor]
|
||||
) -> torch.Tensor | Iterable[torch.Tensor]:
|
||||
if not self._enabled:
|
||||
return outputs
|
||||
|
||||
if isinstance(outputs, torch.Tensor):
|
||||
if not _is_supported_device(outputs):
|
||||
raise AssertionError(f"Expected supported device, got {outputs.device}")
|
||||
if self._scale is None:
|
||||
self._lazy_init_scale_growth_tracker(outputs.device)
|
||||
if self._scale is None:
|
||||
raise AssertionError("Expected _scale to be initialized, got None")
|
||||
scaled_output = outputs * self._scale.to(
|
||||
device=outputs.device, non_blocking=True
|
||||
)
|
||||
# Here we ensure the return dtype is the same as the outputs dtype.
|
||||
# For the FSDP + Mixed Precision use case, the loss output is in the Mixed Precision
|
||||
# format (fp16, bf16) and so the scaled loss should be of the same dtype.
|
||||
return scaled_output.type(outputs.dtype)
|
||||
|
||||
stash: list[_GeneralMultiDeviceReplicator] = []
|
||||
|
||||
def apply_scale(val: torch.Tensor | Iterable[torch.Tensor]):
|
||||
if isinstance(val, torch.Tensor):
|
||||
if not _is_supported_device(val):
|
||||
raise AssertionError(f"Expected supported device, got {val.device}")
|
||||
if len(stash) == 0:
|
||||
if self._scale is None:
|
||||
self._lazy_init_scale_growth_tracker(val.device)
|
||||
if self._scale is None:
|
||||
raise AssertionError(
|
||||
"Expected _scale to be initialized, got None"
|
||||
)
|
||||
stash.append(_GeneralMultiDeviceReplicator(self._scale))
|
||||
scaled_val = val * stash[0].get(val.device)
|
||||
# Here we ensure the return dtype is the same as the outputs dtype.
|
||||
# For the FSDP + Mixed Precision use case, the loss output is in the Mixed Precision
|
||||
# format (fp16, bf16) and so the scaled loss should be of the same dtype.
|
||||
return scaled_val.type(val.dtype)
|
||||
if isinstance(val, abc.Iterable):
|
||||
iterator = map(apply_scale, val)
|
||||
if isinstance(val, (list, tuple)):
|
||||
return type(val)(iterator)
|
||||
return iterator
|
||||
raise ValueError("outputs must be a Tensor or an iterable of Tensors")
|
||||
|
||||
return apply_scale(outputs)
|
||||
|
||||
def _unscale_grads_(
|
||||
self,
|
||||
optimizer: torch.optim.Optimizer,
|
||||
inv_scale: torch.Tensor,
|
||||
found_inf: torch.Tensor,
|
||||
allow_fp16: bool = True,
|
||||
) -> dict[torch.device, torch.Tensor]:
|
||||
per_device_inv_scale = _GeneralMultiDeviceReplicator(inv_scale)
|
||||
per_device_found_inf = _GeneralMultiDeviceReplicator(found_inf)
|
||||
|
||||
# To set up _amp_foreach_non_finite_check_and_unscale_, split grads by device and dtype.
|
||||
# There could be thousands of grads, so we'd like to iterate through them just once.
|
||||
# However, we don't know their devices or dtypes in advance.
|
||||
|
||||
# https://stackoverflow.com/questions/5029934/defaultdict-of-defaultdict
|
||||
# Google says mypy struggles with defaultdicts type annotations.
|
||||
per_device_and_dtype_grads = defaultdict(lambda: defaultdict(list)) # type: ignore[var-annotated]
|
||||
with torch.no_grad():
|
||||
for group in optimizer.param_groups:
|
||||
for param in group["params"]:
|
||||
if param.grad is None:
|
||||
continue
|
||||
if (not allow_fp16) and param.grad.dtype == torch.float16:
|
||||
raise ValueError("Attempting to unscale FP16 gradients.")
|
||||
if param.grad.is_sparse:
|
||||
# is_coalesced() == False means the sparse grad has values with duplicate indices.
|
||||
# coalesce() deduplicates indices and adds all values that have the same index.
|
||||
# For scaled fp16 values, there's a good chance coalescing will cause overflow,
|
||||
# so we should check the coalesced _values().
|
||||
if param.grad.dtype is torch.float16:
|
||||
# coalesce is not supported in torch.float16
|
||||
param_grad_fp32 = param.grad.type(torch.float32).coalesce()
|
||||
param.grad = param_grad_fp32.type(torch.float16)
|
||||
to_unscale = param.grad._values()
|
||||
else:
|
||||
to_unscale = param.grad
|
||||
|
||||
per_device_and_dtype_grads[to_unscale.device][
|
||||
to_unscale.dtype
|
||||
].append(to_unscale)
|
||||
|
||||
for device, per_dtype_grads in per_device_and_dtype_grads.items():
|
||||
for grads in per_dtype_grads.values():
|
||||
torch._amp_foreach_non_finite_check_and_unscale_(
|
||||
grads,
|
||||
per_device_found_inf.get(device),
|
||||
per_device_inv_scale.get(device),
|
||||
)
|
||||
# There exist contexts (e.g. w/ `use_orig_params=True`) wherein some
|
||||
# ranks may have no (non-zero sized) parameter shards, necessitating the
|
||||
# initialization of `per_device_found_inf._per_device_tensors` here
|
||||
if not per_device_found_inf._per_device_tensors:
|
||||
if self._scale is None:
|
||||
raise AssertionError("Expected _scale to be initialized, got None")
|
||||
per_device_found_inf.get(self._scale.device)
|
||||
return per_device_found_inf._per_device_tensors
|
||||
|
||||
def unscale_(self, optimizer: torch.optim.Optimizer) -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
self._check_scale_growth_tracker("unscale_")
|
||||
|
||||
optimizer_state = self._per_optimizer_states[id(optimizer)]
|
||||
|
||||
if optimizer_state["stage"] is OptState.UNSCALED:
|
||||
raise RuntimeError(
|
||||
"unscale_() has already been called on this optimizer since the last update()."
|
||||
)
|
||||
elif optimizer_state["stage"] is OptState.STEPPED:
|
||||
raise RuntimeError("unscale_() is being called after step().")
|
||||
|
||||
# FP32 division can be imprecise for certain compile options, so we carry out the reciprocal in FP64.
|
||||
if self._scale is None:
|
||||
raise AssertionError("Expected _scale to be initialized, got None")
|
||||
inv_scale = self._scale.double().reciprocal().float()
|
||||
found_inf = torch.full(
|
||||
(1,), 0.0, dtype=torch.float32, device=self._scale.device
|
||||
)
|
||||
|
||||
optimizer_state["found_inf_per_device"] = self._unscale_grads_(
|
||||
optimizer, inv_scale, found_inf, True
|
||||
)
|
||||
optimizer_state["stage"] = OptState.UNSCALED
|
||||
|
||||
# Synchronize the detected inf across the ranks
|
||||
optimizer_state = self._per_optimizer_states[id(optimizer)]
|
||||
works = []
|
||||
found_inf_on_cpus = []
|
||||
found_inf_on_devices = []
|
||||
|
||||
for found_inf in optimizer_state["found_inf_per_device"].values():
|
||||
if self._device != "cpu" and found_inf.device.type == "cpu":
|
||||
found_inf_on_cpus.append(found_inf)
|
||||
found_inf_on_device = found_inf.to(self._device)
|
||||
found_inf_on_devices.append(found_inf_on_device)
|
||||
works.append(
|
||||
dist.all_reduce(
|
||||
found_inf_on_device, async_op=True, group=self.process_group
|
||||
)
|
||||
)
|
||||
else:
|
||||
works.append(
|
||||
dist.all_reduce(found_inf, async_op=True, group=self.process_group)
|
||||
)
|
||||
for work in works:
|
||||
work.wait()
|
||||
if found_inf_on_cpus:
|
||||
torch._foreach_copy_(found_inf_on_cpus, found_inf_on_devices)
|
||||
|
||||
def _amp_update_scale_cpu_(self, found_inf: torch.Tensor) -> None:
|
||||
"""
|
||||
If found_inf is 1.0 (True), then scale is multiplied by backoff_factor and growth_tracker is set to zero.
|
||||
Otherwise, scale is multiplied by the growth factor when the growth interval is reached.
|
||||
"""
|
||||
if self._scale is None or self._growth_tracker is None:
|
||||
raise AssertionError(
|
||||
"Expected _scale and _growth_tracker to be initialized, got None"
|
||||
)
|
||||
|
||||
if found_inf.item() >= 1.0:
|
||||
self._scale *= self._backoff_factor
|
||||
self._growth_tracker.fill_(0)
|
||||
else:
|
||||
successful = self._growth_tracker + 1
|
||||
if successful == self._growth_interval:
|
||||
self._scale *= self._growth_factor
|
||||
self._growth_tracker.fill_(0)
|
||||
else:
|
||||
self._growth_tracker = successful
|
||||
|
||||
def update(self, new_scale: float | torch.Tensor | None = None) -> None:
|
||||
"""
|
||||
Updates the scale factor.
|
||||
If any optimizer steps were skipped the scale is multiplied by ``backoff_factor``
|
||||
to reduce it. If ``growth_interval`` unskipped iterations occurred consecutively,
|
||||
the scale is multiplied by ``growth_factor`` to increase it.
|
||||
Passing ``new_scale`` sets the new scale value manually. (``new_scale`` is not
|
||||
used directly, it's used to fill GradScaler's internal scale tensor. So if
|
||||
``new_scale`` was a tensor, later in-place changes to that tensor will not further
|
||||
affect the scale GradScaler uses internally.)
|
||||
Args:
|
||||
new_scale (float or :class:`torch.Tensor`, optional, default=None): New scale factor.
|
||||
.. warning::
|
||||
:meth:`update` should only be called at the end of the iteration, after ``scaler.step(optimizer)`` has
|
||||
been invoked for all optimizers used this iteration.
|
||||
"""
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
_scale, _growth_tracker = self._check_scale_growth_tracker("update") # type: ignore[var-annotated]
|
||||
|
||||
if new_scale is not None:
|
||||
# Accept a new user-defined scale.
|
||||
if isinstance(new_scale, float):
|
||||
self._scale.fill_(new_scale) # type: ignore[union-attr]
|
||||
else:
|
||||
reason = (
|
||||
"new_scale should be a float or a 1-element torch.cuda.FloatTensor or "
|
||||
"torch.FloatTensor with requires_grad=False."
|
||||
)
|
||||
if new_scale.device.type != self._device:
|
||||
raise AssertionError(reason)
|
||||
if new_scale.numel() != 1:
|
||||
raise AssertionError(reason)
|
||||
if new_scale.requires_grad is not False:
|
||||
raise AssertionError(reason)
|
||||
self._scale.copy_(new_scale) # type: ignore[union-attr]
|
||||
else:
|
||||
# Consume shared inf/nan data collected from optimizers to update the scale.
|
||||
# If all found_inf tensors are on the same device as self._scale, this operation is asynchronous.
|
||||
found_infs = [
|
||||
found_inf.to(device=_scale.device, non_blocking=True)
|
||||
for state in self._per_optimizer_states.values()
|
||||
for found_inf in state["found_inf_per_device"].values()
|
||||
]
|
||||
|
||||
if len(found_infs) == 0:
|
||||
raise AssertionError("No inf checks were recorded prior to update.")
|
||||
|
||||
found_inf_combined = found_infs[0]
|
||||
if len(found_infs) > 1:
|
||||
for i in range(1, len(found_infs)):
|
||||
found_inf_combined += found_infs[i]
|
||||
|
||||
if _scale.device.type == "cpu":
|
||||
self._amp_update_scale_cpu_(found_inf_combined)
|
||||
else:
|
||||
torch._amp_update_scale_(
|
||||
self._scale, # type: ignore[arg-type]
|
||||
self._growth_tracker, # type: ignore[arg-type]
|
||||
found_inf_combined,
|
||||
self._growth_factor, # type: ignore[arg-type]
|
||||
self._backoff_factor, # type: ignore[arg-type]
|
||||
self._growth_interval, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# To prepare for next iteration, clear the data collected from optimizers this iteration.
|
||||
self._per_optimizer_states = defaultdict(_refresh_per_optimizer_state)
|
||||
@@ -0,0 +1,608 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# Copyright (c) Facebook, Inc. and its affiliates.
|
||||
#
|
||||
# This source code is licensed under the BSD license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Generator, Iterable, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
__all__ = [
|
||||
"always_wrap_policy",
|
||||
"lambda_auto_wrap_policy",
|
||||
"transformer_auto_wrap_policy",
|
||||
"size_based_auto_wrap_policy",
|
||||
"enable_wrap",
|
||||
"wrap",
|
||||
"CustomPolicy",
|
||||
"ModuleWrapPolicy",
|
||||
]
|
||||
|
||||
|
||||
# NOTE: We intentionally keep this function simple and isolate the complexity
|
||||
# to `fn` to enable using this function generically. We may move this to a
|
||||
# non-FSDP-specific folder and/or make it public in the future.
|
||||
def _post_order_apply(
|
||||
root_module: nn.Module,
|
||||
fn: Callable[[nn.Module], nn.Module | None],
|
||||
):
|
||||
"""
|
||||
This applies ``fn`` to every module in the module tree of ``root_module``
|
||||
following a post-order traversal. If ``fn`` returns an :class:`nn.Module`,
|
||||
then this replaces the original module with the newly returned one in the
|
||||
tree. Otherwise, ``fn`` should return ``None``, in which case the module is
|
||||
not changed.
|
||||
"""
|
||||
# Track visited modules to avoid visiting shared modules multiple times
|
||||
visited_modules: set[nn.Module] = {root_module}
|
||||
|
||||
def _post_order_apply_inner(
|
||||
module: nn.Module,
|
||||
module_name: str,
|
||||
parent_module: nn.Module | None,
|
||||
):
|
||||
for child_module_name, child_module in module.named_children():
|
||||
if child_module not in visited_modules:
|
||||
visited_modules.add(child_module)
|
||||
_post_order_apply_inner(child_module, child_module_name, module)
|
||||
optional_module = fn(module)
|
||||
if optional_module is not None:
|
||||
if not isinstance(parent_module, nn.Module):
|
||||
raise AssertionError(
|
||||
"Non-root modules should have their parent module set but got "
|
||||
f"{parent_module} for {module}"
|
||||
)
|
||||
if not module_name:
|
||||
raise AssertionError(
|
||||
"Non-root modules should have their module name set but got "
|
||||
f"an empty module name for {module}"
|
||||
)
|
||||
if not isinstance(optional_module, nn.Module):
|
||||
raise AssertionError(
|
||||
f"fn should return None or an nn.Module but got {optional_module}"
|
||||
)
|
||||
setattr(parent_module, module_name, optional_module)
|
||||
|
||||
_post_order_apply_inner(root_module, "", None)
|
||||
|
||||
|
||||
def _construct_wrap_fn(
|
||||
root_module: nn.Module,
|
||||
target_module_to_kwargs: dict[nn.Module, dict[str, Any]],
|
||||
fsdp_fn: Callable,
|
||||
) -> Callable[[nn.Module], nn.Module | None]:
|
||||
"""
|
||||
This constructs the "wrap" function to pass to :func:`_post_order_apply`
|
||||
based on ``target_module_to_kwargs``, which should be constructed from the
|
||||
wrapping policy.
|
||||
"""
|
||||
|
||||
def fn(module: nn.Module) -> nn.Module | None:
|
||||
# Explicitly avoid wrapping the root module since for FSDP, it is
|
||||
# handled by the caller
|
||||
if module in target_module_to_kwargs and module is not root_module:
|
||||
kwargs = target_module_to_kwargs[module]
|
||||
return fsdp_fn(module, **kwargs)
|
||||
return None
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
def _run_mixed_precision_override_policy(
|
||||
root_module: nn.Module,
|
||||
module_classes: Iterable[type[nn.Module]],
|
||||
ignored_modules: set[nn.Module],
|
||||
root_kwargs: dict[str, Any],
|
||||
target_module_to_kwargs: dict[nn.Module, dict[str, Any]],
|
||||
):
|
||||
module_classes_tuple = tuple(set(module_classes))
|
||||
for module in root_module.modules():
|
||||
if module in ignored_modules:
|
||||
continue
|
||||
elif isinstance(module, module_classes_tuple):
|
||||
# This policy overrides any existing policy
|
||||
if module not in target_module_to_kwargs:
|
||||
# Only inherit from the root kwargs if not already specified
|
||||
target_module_to_kwargs[module] = root_kwargs
|
||||
target_module_to_kwargs[module]["mixed_precision"] = None
|
||||
return target_module_to_kwargs
|
||||
|
||||
|
||||
def always_wrap_policy(*args, **kwargs) -> bool:
|
||||
"""
|
||||
A simple recursive wrap policy that always returns ``True``. This means
|
||||
that every submodule is wrapped by the wrapper class in
|
||||
:func:`_recursive_wrap`.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class _Policy(ABC):
|
||||
"""
|
||||
This defines an abstract base class that represents a policy for applying
|
||||
a module-level API.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _run_policy(
|
||||
self,
|
||||
root_module: nn.Module,
|
||||
ignored_modules: set[nn.Module],
|
||||
root_kwargs: dict[str, Any],
|
||||
) -> dict[nn.Module, dict[str, Any]]:
|
||||
"""
|
||||
This should return a dict ``target_module_to_kwargs`` that maps from
|
||||
each target module to wrap to its kwargs.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
def _module_wrap_policy(
|
||||
module: nn.Module,
|
||||
recurse: bool,
|
||||
nonwrapped_numel: int,
|
||||
module_classes: set[type[nn.Module]],
|
||||
) -> bool:
|
||||
"""
|
||||
This auto wrap policy wraps every module that is an instance of any type in
|
||||
``module_classes`` as its own FSDP instance. The root module given by
|
||||
``module`` is always wrapped as an FSDP instance regardless. Since the
|
||||
wrapping proceeds bottom up, each FSDP instance manages the parameters in
|
||||
its subtree excluding any already managed by a child FSDP instance.
|
||||
|
||||
Args:
|
||||
module (nn.Module): Current module being considered.
|
||||
recurse (bool): If ``False``, then this function must decide whether
|
||||
``module`` should be wrapped as an FSDP instance or not. If
|
||||
``True``, then the function is still recursing down the module
|
||||
tree as a part of the DFS.
|
||||
nonwrapped_numel (int): Parameter numel not yet wrapped.
|
||||
module_classes (Set[Type[nn.Module]]): Set of module classes that are
|
||||
wrapped as FSDP instances.
|
||||
|
||||
Returns:
|
||||
``True`` if ``recurse=True``, and whether ``module`` should be wrapped
|
||||
if ``recurse=False``.
|
||||
"""
|
||||
if recurse:
|
||||
return True # always recurse
|
||||
return isinstance(module, tuple(module_classes))
|
||||
|
||||
|
||||
class ModuleWrapPolicy(_Policy):
|
||||
"""
|
||||
This policy applies to every module of the specified module classes,
|
||||
passing in the kwargs given to the root.
|
||||
"""
|
||||
|
||||
def __init__(self, module_classes: Iterable[type[nn.Module]]):
|
||||
module_classes_set = set(module_classes)
|
||||
self._module_classes = module_classes_set
|
||||
self._module_classes_str = str(module_classes_set)
|
||||
|
||||
def _run_policy(
|
||||
self,
|
||||
root_module: nn.Module,
|
||||
ignored_modules: set[nn.Module],
|
||||
root_kwargs: dict[str, Any],
|
||||
) -> dict[nn.Module, dict[str, Any]]:
|
||||
module_classes = tuple(self._module_classes)
|
||||
target_module_to_kwargs: dict[nn.Module, dict[str, Any]] = {}
|
||||
for module in root_module.modules():
|
||||
if module in ignored_modules:
|
||||
continue
|
||||
elif isinstance(module, module_classes):
|
||||
# Shallow copy to avoid coupling changes across modules
|
||||
target_module_to_kwargs[module] = copy.copy(root_kwargs)
|
||||
return target_module_to_kwargs
|
||||
|
||||
def __call__(self, module, recurse, *args, **kwargs):
|
||||
# nonwrapped_numel is not used.
|
||||
return _module_wrap_policy(
|
||||
module, recurse, nonwrapped_numel=-1, module_classes=self._module_classes
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return super().__repr__() + f"({self._module_classes_str})"
|
||||
|
||||
|
||||
class CustomPolicy(_Policy):
|
||||
"""
|
||||
This policy takes in a lambda function that maps a given ``nn.Module`` to
|
||||
either ``False``, ``True``, or a kwarg dictionary.
|
||||
- If the function returns ``False`` or an empty dictionary, then the module
|
||||
does not have the API applied.
|
||||
- If the function returns ``True``, then the module has the API applied
|
||||
with the root's kwargs.
|
||||
- If the function returns a non-empty dictionary, then the module has the
|
||||
API applied, and the dictionary overrides the root's kwargs.
|
||||
|
||||
Example::
|
||||
|
||||
>>> # xdoctest: +SKIP("undefined variables")
|
||||
>>> model = init_transformer_model(...)
|
||||
>>> def lambda_fn(module: nn.Module):
|
||||
>>> if module is model.lm_head:
|
||||
>>> return {"sharding_strategy": ShardingStrategy.SHARD_GRAD_OP}
|
||||
>>> elif isinstance(module, TransformerBlock):
|
||||
>>> return True
|
||||
>>> return False
|
||||
>>> policy = CustomPolicy(lambda_fn)
|
||||
>>> fsdp_model = FSDP(model, auto_wrap_policy=policy)
|
||||
"""
|
||||
|
||||
def __init__(self, lambda_fn: Callable[[nn.Module], bool | dict[str, Any]]):
|
||||
self._lambda_fn = lambda_fn
|
||||
|
||||
def _run_policy(
|
||||
self,
|
||||
root_module: nn.Module,
|
||||
ignored_modules: set[nn.Module],
|
||||
root_kwargs: dict[str, Any],
|
||||
) -> dict[nn.Module, dict[str, Any]]:
|
||||
target_module_to_kwargs: dict[nn.Module, dict[str, Any]] = {}
|
||||
for module in root_module.modules():
|
||||
if module in ignored_modules:
|
||||
continue
|
||||
res = self._lambda_fn(module)
|
||||
if not isinstance(res, (dict, bool)):
|
||||
raise ValueError(
|
||||
"The lambda_fn passed to CustomPolicy should return "
|
||||
f"False/True or a kwarg dict, but it returned {res}"
|
||||
)
|
||||
if not res:
|
||||
continue
|
||||
kwargs = copy.copy(root_kwargs)
|
||||
if isinstance(res, dict):
|
||||
# Override the root kwargs with the ones specified by the
|
||||
# lambda function
|
||||
kwargs.update(res)
|
||||
target_module_to_kwargs[module] = kwargs
|
||||
return target_module_to_kwargs
|
||||
|
||||
|
||||
def lambda_auto_wrap_policy(
|
||||
module: nn.Module, recurse: bool, nonwrapped_numel: int, lambda_fn: Callable
|
||||
) -> bool:
|
||||
"""
|
||||
A convenient auto wrap policy to wrap submodules based on an arbitrary user
|
||||
function. If `lambda_fn(submodule) == True``, the submodule will be wrapped as
|
||||
a `wrapper_cls` unit.
|
||||
|
||||
Return if a module should be wrapped during auto wrapping.
|
||||
|
||||
The first three parameters are required by :func:`_recursive_wrap`.
|
||||
|
||||
Args:
|
||||
module (nn.Module): Current module being considered.
|
||||
recurse (bool): If ``False``, then this function must decide whether
|
||||
``module`` should be wrapped as an FSDP instance or not. If
|
||||
``True``, then the function is still recursing down the module
|
||||
tree as a part of the DFS.
|
||||
nonwrapped_numel (int): Parameter numel not yet wrapped.
|
||||
|
||||
lambda_fn (Callable[[nn.Module], bool]): If this returns ``True``, then
|
||||
this module will be wrapped.
|
||||
"""
|
||||
if recurse:
|
||||
return True # always recurse
|
||||
return lambda_fn(module)
|
||||
|
||||
|
||||
def transformer_auto_wrap_policy(
|
||||
module: nn.Module,
|
||||
recurse: bool,
|
||||
nonwrapped_numel: int,
|
||||
transformer_layer_cls: set[type[nn.Module]],
|
||||
) -> bool:
|
||||
"""
|
||||
See :func:`_module_wrap_policy`, where ``transformer_layer_cls`` is the
|
||||
same as ``module_classes``. Note that shared parameters must be wrapped in
|
||||
the same FSDP instance, so this auto wrap policy can help wrap shared
|
||||
embeddings into the same FSDP instance for transformer models.
|
||||
"""
|
||||
return _module_wrap_policy(module, recurse, nonwrapped_numel, transformer_layer_cls)
|
||||
|
||||
|
||||
def _wrap_module_cls_individually(
|
||||
module: nn.Module, module_classes: Sequence[type], recurse: bool, *args, **kwargs
|
||||
):
|
||||
if recurse:
|
||||
# always recurse
|
||||
return True
|
||||
else:
|
||||
# if not recursing, decide whether we should wrap based on whether the type of module
|
||||
# is in `module_classes`.
|
||||
return isinstance(module, tuple(module_classes))
|
||||
|
||||
|
||||
def _or_policy(
|
||||
module: nn.Module,
|
||||
recurse: bool,
|
||||
nonwrapped_numel: int,
|
||||
policies,
|
||||
) -> bool:
|
||||
"""
|
||||
A policy that wraps ``module`` if any policy in the passed in iterable of
|
||||
``policies`` returns ``True``.
|
||||
"""
|
||||
return any(
|
||||
policy(module=module, recurse=recurse, nonwrapped_numel=nonwrapped_numel)
|
||||
for policy in policies
|
||||
)
|
||||
|
||||
|
||||
def size_based_auto_wrap_policy(
|
||||
module: nn.Module,
|
||||
recurse: bool,
|
||||
nonwrapped_numel: int,
|
||||
# Additional custom arguments
|
||||
min_num_params: int = int(1e8),
|
||||
force_leaf_modules: set[type[nn.Module]] | None = None,
|
||||
exclude_wrap_modules: set[type[nn.Module]] | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
A size-based auto wrap policy.
|
||||
|
||||
Args:
|
||||
module (nn.Module): Current module being considered.
|
||||
recurse (bool): If ``False``, then this function must decide whether
|
||||
``module`` should be wrapped as an FSDP instance or not. If
|
||||
``True``, then the function is still recursing down the module
|
||||
tree as a part of the DFS.
|
||||
nonwrapped_numel (int): Parameter numel not yet wrapped.
|
||||
|
||||
min_num_params (int): Customizable policy input that controls the size
|
||||
threshold over which a module is ready to be wrapped. This is in
|
||||
units of numel.
|
||||
force_leaf_modules (Optional[set[type[nn.Module]]]): Set of module types to keep
|
||||
as leaves, i.e. their children will never be wrapped.
|
||||
exclude_wrap_modules (Optional[set[type[nn.Module]]]): Set of module types to be
|
||||
excluded in wrapping.
|
||||
|
||||
Returns:
|
||||
Whether ``module`` should be wrapped.
|
||||
"""
|
||||
force_leaf_modules = (
|
||||
size_based_auto_wrap_policy.FORCE_LEAF_MODULES # type: ignore[attr-defined]
|
||||
if force_leaf_modules is None
|
||||
else force_leaf_modules
|
||||
)
|
||||
exclude_wrap_modules = (
|
||||
size_based_auto_wrap_policy.EXCLUDE_WRAP_MODULES # type: ignore[attr-defined]
|
||||
if exclude_wrap_modules is None
|
||||
else exclude_wrap_modules
|
||||
)
|
||||
|
||||
# Keep the argument `min_num_params` for BC for now, but it represents the
|
||||
# minimum non-wrapped *numel* before triggering a wrapping
|
||||
min_nonwrapped_numel = min_num_params
|
||||
is_large = nonwrapped_numel >= min_nonwrapped_numel
|
||||
if recurse:
|
||||
# We should recurse if the module is big enough but not in force_leaf_modules list.
|
||||
return is_large and not isinstance(module, tuple(force_leaf_modules))
|
||||
else:
|
||||
# If we are not recursing, determine if we should wrap.
|
||||
return is_large and not isinstance(module, tuple(exclude_wrap_modules))
|
||||
|
||||
|
||||
# Set those defaults to the size_based_auto_wrap_policy function. Make them easy to be imported.
|
||||
size_based_auto_wrap_policy.EXCLUDE_WRAP_MODULES = {nn.ModuleList, nn.ModuleDict} # type: ignore[attr-defined]
|
||||
size_based_auto_wrap_policy.FORCE_LEAF_MODULES = {nn.MultiheadAttention} # type: ignore[attr-defined]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def enable_wrap(
|
||||
*, wrapper_cls: Any, **wrapper_kwargs: Any
|
||||
) -> Generator[None, None, None]:
|
||||
"""
|
||||
Context manager to wrap modules using a wrapper.
|
||||
|
||||
Useful for when you'd like to apply the same configuration arguments to all
|
||||
child modules that you wrap. A particularly important use case is wrapping
|
||||
large layers so that they get sharded (in-place) during initialization, to
|
||||
avoid running out of system memory. Large layers can indicate that they
|
||||
should be sharded via the ``wrap`` annotation and this context manager can
|
||||
provide the exact configuration for these nested instances.
|
||||
|
||||
Usage::
|
||||
|
||||
with enable_wrap(wrapper_cls, **params):
|
||||
# Wraps layer in FSDP by default if within context
|
||||
self.l1 = wrap(torch.nn.Linear(5, 5))
|
||||
|
||||
Args:
|
||||
wrapper_cls:
|
||||
Class that `wrap` annotation will `wrap` modules with, such as
|
||||
`FullyShardedDataParallel`.
|
||||
**wrapper_kwargs:
|
||||
Configuration settings that will be passed to all ``wrap``
|
||||
instances inside the context
|
||||
"""
|
||||
kwargs = {
|
||||
"wrapper_cls": wrapper_cls,
|
||||
**wrapper_kwargs,
|
||||
}
|
||||
with _ConfigAutoWrap(**kwargs):
|
||||
yield
|
||||
|
||||
|
||||
def wrap(module: nn.Module, **wrap_overrides: Any) -> nn.Module:
|
||||
"""
|
||||
Annotate that a module should be wrapped. Annotated modules will only be
|
||||
wrapped if inside of an :func:`enable_wrap` context manager. This allows
|
||||
a module to be initialized both with and without a wrapper without code
|
||||
change.
|
||||
|
||||
The class that this function wraps the passed in ``nn.Module`` with is the
|
||||
passed in ``wrapper_cls`` argument into ``enable_wrap``. Both
|
||||
``enable_wrap`` and ``wrap`` can take in kwargs specifying how to construct
|
||||
the ``wrapper_cls`` instance. In the case of duplicate kwargs in
|
||||
``enable_wrap`` and ``wrap``, the argument passed into ``wrap`` will be
|
||||
respected.
|
||||
|
||||
Usage::
|
||||
|
||||
with enable_wrap(wrapper_cls=FSDP, **fsdp_config):
|
||||
# Wraps layer in FSDP by default if within context
|
||||
self.l1 = wrap(torch.nn.Linear(5, 5))
|
||||
|
||||
Args:
|
||||
module (nn.Module): module to wrap (if in :func:`enable_wrap` context)
|
||||
**wrap_overrides: configuration overrides that will take priority over
|
||||
the values provided by the :func:`enable_wrap` context
|
||||
"""
|
||||
if _ConfigAutoWrap.in_autowrap_context:
|
||||
if _ConfigAutoWrap.wrapper_cls is None:
|
||||
raise AssertionError("Expected _ConfigAutoWrap.wrapper_cls to be set")
|
||||
|
||||
wrap_overrides = {**_ConfigAutoWrap.kwargs, **wrap_overrides}
|
||||
return _wrap(
|
||||
module,
|
||||
_ConfigAutoWrap.wrapper_cls,
|
||||
**wrap_overrides,
|
||||
)
|
||||
return module
|
||||
|
||||
|
||||
def _wrap(module: nn.Module, wrapper_cls: Callable, **kwargs) -> nn.Module:
|
||||
if wrapper_cls is None:
|
||||
raise AssertionError("Expected wrapper_cls to be set")
|
||||
if hasattr(module, "_wrap_overrides"):
|
||||
# If module has a _wrap_overrides attribute, we force overriding the
|
||||
# FSDP config with these attributes for this module. Currently this
|
||||
# is only used to disable mixed precision for BatchNorm when
|
||||
# auto_wrapping.
|
||||
overrides = {**kwargs, **module._wrap_overrides} # type: ignore[arg-type, dict-item]
|
||||
return wrapper_cls(module, **overrides)
|
||||
|
||||
return wrapper_cls(module, **kwargs)
|
||||
|
||||
|
||||
def _recursive_wrap(
|
||||
module: nn.Module,
|
||||
auto_wrap_policy: Callable,
|
||||
wrapper_cls: Callable,
|
||||
ignored_modules: set[nn.Module],
|
||||
ignored_params: set[nn.Parameter],
|
||||
only_wrap_children: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> tuple[nn.Module, int]:
|
||||
"""
|
||||
Wraps submodules of ``module`` for which ``auto_wrap_policy`` returns
|
||||
``True`` with ``wrapper_cls``.
|
||||
|
||||
Args:
|
||||
module (nn.Module): Module to recursively wrap.
|
||||
auto_wrap_policy (Callable): A callable representing a policy that
|
||||
determines which modules to recursively wrap with ``wrapper_cls``.
|
||||
ignored_modules (set[torch.nn.Module]): Modules to ignore when
|
||||
wrapping.
|
||||
ignored_params (set[torch.nn.Parameter]): Parameters to ignore when
|
||||
wrapping; these should be the parameters contained in the modules
|
||||
in ``ignored_modules``.
|
||||
Returns:
|
||||
(nn.Module, int):
|
||||
``module`` after wrapping and the numel recursively wrapped.
|
||||
"""
|
||||
if auto_wrap_policy is None:
|
||||
raise AssertionError("Must specify auto_wrap_policy.")
|
||||
if wrapper_cls is None:
|
||||
raise AssertionError("Must specify wrapper_cls")
|
||||
# Make sure no child is already wrapped.
|
||||
for _, child in module.named_modules():
|
||||
if child in ignored_modules:
|
||||
continue
|
||||
try:
|
||||
if isinstance(child, cast(type, wrapper_cls)):
|
||||
raise AssertionError(
|
||||
f"Child module {child} is already wrapped by {wrapper_cls}"
|
||||
)
|
||||
except TypeError:
|
||||
# wrapper_cls is a function as opposed to a class type, just bypass above check.
|
||||
pass
|
||||
|
||||
# We count all params, assuming none of them are already wrapped.
|
||||
nonwrapped_numel = sum(
|
||||
p.numel() for p in module.parameters() if p not in ignored_params
|
||||
)
|
||||
|
||||
if auto_wrap_policy is None:
|
||||
raise AssertionError("Expected auto_wrap_policy to be set")
|
||||
if auto_wrap_policy(module=module, recurse=True, nonwrapped_numel=nonwrapped_numel):
|
||||
total_wrapped_numel = 0
|
||||
# Iterate through the children, recursively wrap if necessary
|
||||
for name, child in module.named_children():
|
||||
if child in ignored_modules:
|
||||
continue
|
||||
wrapped_child, num_wrapped_params = _recursive_wrap(
|
||||
module=child,
|
||||
auto_wrap_policy=auto_wrap_policy,
|
||||
wrapper_cls=wrapper_cls,
|
||||
ignored_modules=ignored_modules,
|
||||
ignored_params=ignored_params,
|
||||
**kwargs,
|
||||
)
|
||||
setattr(module, name, wrapped_child)
|
||||
# Keep track of how many parameters have been wrapped
|
||||
total_wrapped_numel += num_wrapped_params
|
||||
# decide if we need to wrap the current module,
|
||||
# since the left over parameters exceed the number of params to wrap
|
||||
remainder = nonwrapped_numel - total_wrapped_numel
|
||||
if not only_wrap_children and auto_wrap_policy(
|
||||
module=module, recurse=False, nonwrapped_numel=remainder
|
||||
):
|
||||
# Leaf node or final wrapping of the remainder both happen here.
|
||||
return _wrap(module, wrapper_cls, **kwargs), nonwrapped_numel
|
||||
else:
|
||||
return module, total_wrapped_numel
|
||||
return module, 0
|
||||
|
||||
|
||||
class _ConfigAutoWrap:
|
||||
"""
|
||||
Helper class to wrap modules based on default config args via a context manager.
|
||||
See :func:`enable_wrap` for more information.
|
||||
"""
|
||||
|
||||
in_autowrap_context: bool = False # Context flag
|
||||
wrapper_cls: Callable | None = None # The wrapper class
|
||||
kwargs: dict[str, Any] = {} # Wrapper's args
|
||||
|
||||
def __init__(self, **kwargs: dict[str, Any]):
|
||||
self.kwargs = kwargs
|
||||
|
||||
@staticmethod
|
||||
def enable_autowrap_context(kwargs: Any) -> None:
|
||||
if _ConfigAutoWrap.in_autowrap_context:
|
||||
raise NotImplementedError(
|
||||
"You are already within an autowrap context and we currently do not supported nested autowrap."
|
||||
)
|
||||
_ConfigAutoWrap.in_autowrap_context = True
|
||||
# Get and save the wrapper cls for the context.
|
||||
if "wrapper_cls" not in kwargs:
|
||||
raise AssertionError(
|
||||
"Expected to pass in wrapper_cls arg into _ConfigAutoWrap."
|
||||
)
|
||||
_ConfigAutoWrap.wrapper_cls = cast(Callable, kwargs["wrapper_cls"])
|
||||
del kwargs["wrapper_cls"]
|
||||
# Save the rest.
|
||||
_ConfigAutoWrap.kwargs = kwargs
|
||||
|
||||
@staticmethod
|
||||
def disable_autowrap_context() -> None:
|
||||
_ConfigAutoWrap.in_autowrap_context = False
|
||||
_ConfigAutoWrap.wrapper_cls = None
|
||||
_ConfigAutoWrap.kwargs = {}
|
||||
|
||||
def __enter__(self) -> None:
|
||||
self.enable_autowrap_context(self.kwargs)
|
||||
|
||||
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
self.disable_autowrap_context()
|
||||
Reference in New Issue
Block a user