Spaces:
Running on Zero
Running on Zero
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from __future__ import annotations | |
| import copy | |
| from functools import partial | |
| from contextlib import contextmanager | |
| import time | |
| from typing import TYPE_CHECKING, Optional, Callable | |
| import torch | |
| from torch.distributed.fsdp import ( | |
| CPUOffloadPolicy, | |
| ) | |
| from torch.distributed.device_mesh import init_device_mesh | |
| from torch.distributed.fsdp import fully_shard, MixedPrecisionPolicy | |
| from torch.distributed.checkpoint.state_dict import ( | |
| set_model_state_dict, | |
| StateDictOptions, | |
| ) | |
| from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import ( | |
| CheckpointImpl, | |
| apply_activation_checkpointing, | |
| checkpoint_wrapper, | |
| ) | |
| from lipforcing.networks.network import FastGenNetwork | |
| from lipforcing.utils.distributed import world_size, synchronize, is_rank0 | |
| import lipforcing.utils.logging_utils as logger | |
| if TYPE_CHECKING: | |
| from lipforcing.methods import FastGenModel | |
| def apply_fsdp_checkpointing(module: torch.nn.Module, check_fn: Optional[Callable[[torch.nn.Module], bool]] = None): | |
| """ | |
| Apply FSDP checkpointing to a module. | |
| Follows overall approach outlined in https://pytorch.org/blog/maximizing-training/, without adaptive selection. | |
| Args: | |
| module: The module to wrap with activation checkpointing. | |
| check_fn: A function (Module -> bool) that takes a module and returns True if it should be wrapped. | |
| If None, wraps transformer blocks (Block in class name) only. | |
| """ | |
| non_reentrant_wrapper = partial( | |
| checkpoint_wrapper, | |
| checkpoint_impl=CheckpointImpl.NO_REENTRANT, | |
| ) | |
| if check_fn is None: | |
| # Default: only checkpoint block-level modules (e.g. WanTransformerBlock in Wan network) | |
| # This is the correct granularity for FSDP + checkpointing | |
| def check_fn(submodule): | |
| # Check for common transformer block class names | |
| class_name = submodule.__class__.__name__ | |
| return "Block" in class_name | |
| logger.info("Using default check_fn: checkpointing modules with 'Block' in class name") | |
| apply_activation_checkpointing(module, checkpoint_wrapper_fn=non_reentrant_wrapper, check_fn=check_fn) | |
| def model_to_fsdp( | |
| model: FastGenModel, | |
| min_num_params: int = 10_000_000, | |
| apply_cpu_offload: bool = False, | |
| sync_module_states: bool = False, | |
| sharding_group_size: Optional[int] = None, | |
| ): | |
| """Convert model to FSDP. | |
| Args: | |
| model: The model to wrap with FSDP. | |
| min_num_params: Minimum number of parameters for a module to be wrapped as a separate FSDP unit. | |
| Default is 10M, which wraps large models quite finely. | |
| For a 14B model, each transformer block has ~300-400M params, so a larger value may | |
| be preferable (e.g., 100M-500M). | |
| apply_cpu_offload: Whether to offload parameters to CPU. | |
| sync_module_states: If True, broadcast module states from rank 0 to all other ranks. | |
| Enable this when using memory-efficient loading where only rank 0 loads weights. | |
| Memory-Efficient Loading with Meta Device: | |
| To use memory-efficient loading for large models (14B+): | |
| 1. Set `fsdp_meta_init=True` in trainer config | |
| 2. Network classes should check `is_rank0()` and: | |
| - Rank 0: load full weights | |
| - Other ranks: use `with torch.device("meta"): load_model()` for ZERO memory allocation | |
| 4. FSDP will materialize meta tensors and broadcast weights from rank 0 | |
| This reduces initialization from N*model_size RAM to 1*model_size RAM, | |
| and avoids N parallel disk reads (major I/O contention). | |
| """ | |
| fsdp_dict = model.fsdp_dict | |
| total_world_size = world_size() | |
| if sharding_group_size is None: | |
| # Full sharding over the world | |
| logger.info(f"Fully sharding model with {total_world_size} ranks...") | |
| device_mesh = init_device_mesh("cuda", (total_world_size,)) | |
| else: | |
| if total_world_size % sharding_group_size != 0: | |
| raise ValueError( | |
| f"World size {total_world_size} must be divisible by shard group size {sharding_group_size}" | |
| ) | |
| replica_group_size = total_world_size // sharding_group_size | |
| logger.info(f"Sharding model with {replica_group_size} sharding groups of size {sharding_group_size}...") | |
| device_mesh = init_device_mesh( | |
| "cuda", (replica_group_size, sharding_group_size), mesh_dim_names=("replicate", "shard") | |
| ) | |
| # Mixed precision policy for FSDP2 | |
| mp_policy = MixedPrecisionPolicy( | |
| param_dtype=model.precision, | |
| reduce_dtype=model.precision_fsdp, | |
| # We avoid casting all inputs so we can control t precision etc. | |
| output_dtype=None, | |
| cast_forward_inputs=False, | |
| ) | |
| offload_policy = CPUOffloadPolicy() if apply_cpu_offload else None | |
| for k, v in fsdp_dict.items(): | |
| if k.startswith("ema"): | |
| logger.warning("EMA network stored in fsdp_dict will be skipped during FSDP2 wrap.") | |
| continue | |
| num_params = sum(p.numel() for p in v.parameters()) / 1e9 | |
| logger.info(f"Starting FSDP2 wrap for '{k}' ({num_params:.2f}B params)...") | |
| t0 = time.time() | |
| # Step 1: Extract full state dict from rank 0 BEFORE sharding | |
| # Rank 0 has real weights, other ranks have meta tensors | |
| if sync_module_states: | |
| if is_rank0(): | |
| # Rank 0 has the real weights - extract them | |
| # Extracting the full state dict on rank 0 can be slow for very large models. | |
| state_dict = copy.deepcopy(v.state_dict()) | |
| logger.info(f" [Rank 0] Extracted state dict with {len(state_dict)} tensors") | |
| else: | |
| state_dict = None | |
| else: | |
| state_dict = None | |
| # Step 2: Apply fully_shard to create DTensor structure | |
| if isinstance(v, FastGenNetwork): | |
| # We use the network's custom fully_shard method | |
| v.fully_shard( | |
| mesh=device_mesh, | |
| mp_policy=mp_policy, | |
| offload_policy=offload_policy, | |
| ) | |
| else: | |
| # Fall back to size-based auto-wrap policy | |
| modules_to_shard = _get_submodules_to_shard(v, min_num_params) | |
| logger.info(f" Sharding {len(modules_to_shard)} submodules") | |
| for submodule in modules_to_shard: | |
| fully_shard( | |
| submodule, | |
| mesh=device_mesh, | |
| mp_policy=mp_policy, | |
| offload_policy=offload_policy, | |
| reshard_after_forward=True, | |
| ) | |
| fully_shard( | |
| v, | |
| mesh=device_mesh, | |
| mp_policy=mp_policy, | |
| offload_policy=offload_policy, | |
| reshard_after_forward=True, | |
| ) | |
| logger.info("Completed sharding") | |
| # Step 3: Move parameters to correct device and optionally broadcast state dict | |
| # With CPU offloading, use CPU as target device so FSDP can manage GPU placement | |
| # Without CPU offloading, use CUDA directly | |
| target_device = "cpu" if apply_cpu_offload else torch.cuda.current_device() | |
| if sync_module_states: | |
| logger.info("Syncing module states from rank 0 to all ranks") | |
| options = StateDictOptions( | |
| full_state_dict=True, | |
| broadcast_from_rank0=True, | |
| cpu_offload=apply_cpu_offload, | |
| ) | |
| logger.debug(f"Moving all ranks to target device: {target_device}") | |
| v.to_empty(device=target_device) | |
| if hasattr(v, "reset_parameters"): | |
| # We need this to reinitialize non-persistent buffers like RoPE freqs_cos/freqs_sin | |
| # These aren't stored in the state dict, so we need to reinitialize them after to_empty() | |
| v.reset_parameters() | |
| else: | |
| logger.warning( | |
| f"Network {v.__class__.__name__} does not implement the reset_parameters method. " | |
| "This may cause unexpected behavior with FSDP2, like non-persistent buffers not " | |
| "being initialized correctly." | |
| ) | |
| synchronize() | |
| logger.debug("Moved all ranks to target devices") | |
| logger.debug("Broadcasting the state dict to all ranks") | |
| set_model_state_dict(v, model_state_dict=state_dict, options=options) | |
| torch.cuda.empty_cache() | |
| else: | |
| v.to(device=target_device) | |
| synchronize() | |
| logger.info(f"FSDP2 wrapped {k} in {time.time() - t0:.1f}s") | |
| return model | |
| def _get_submodules_to_shard(module: torch.nn.Module, min_num_params: int) -> list[torch.nn.Module]: | |
| """Get list of submodules that should be sharded based on parameter count.""" | |
| modules_to_shard = [] | |
| def _count_params(m: torch.nn.Module) -> int: | |
| return sum(p.numel() for p in m.parameters(recurse=False)) | |
| def _recurse(m: torch.nn.Module) -> None: | |
| for child in m.children(): | |
| _recurse(child) | |
| own_params = _count_params(m) | |
| if own_params >= min_num_params: | |
| modules_to_shard.append(m) | |
| _recurse(module) | |
| return modules_to_shard | |
| def fsdp_sync_grad(model: FastGenModel, enabled: bool): | |
| """ | |
| Context manager to enable/disable gradient synchronization for FSDP2 modules. | |
| This mirrors DDP's no_sync behavior for gradient accumulation: for non-last | |
| microbatches, set_requires_gradient_sync(False) to skip communication. | |
| """ | |
| fsdp_modules = [ | |
| m | |
| for m in model.fsdp_dict.values() | |
| if hasattr(m, "set_requires_gradient_sync") and any(p.requires_grad for p in m.parameters()) | |
| ] | |
| if fsdp_modules: | |
| for module in fsdp_modules: | |
| module.set_requires_gradient_sync(enabled, recurse=True) | |
| if hasattr(module, "set_is_last_backward"): | |
| module.set_is_last_backward(enabled) | |
| try: | |
| yield | |
| finally: | |
| if fsdp_modules and not enabled: | |
| for module in fsdp_modules: | |
| module.set_requires_gradient_sync(True, recurse=True) | |
| if hasattr(module, "set_is_last_backward"): | |
| module.set_is_last_backward(True) | |