multimodalart's picture
multimodalart HF Staff
Initial Lip Forcing 14B streaming demo
9368ee7 verified
Raw
History Blame Contribute Delete
10.5 kB
# 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
@contextmanager
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)