Files

244 lines
11 KiB
Python
Executable File

# Copied from https://github.com/Wan-Video/Wan2.1/blob/main/wan/distributed/fsdp.py
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import gc
from functools import partial
import torch
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import CPUOffload, MixedPrecision, ShardingStrategy
from torch.distributed.fsdp.wrap import (lambda_auto_wrap_policy,
transformer_auto_wrap_policy)
from torch.distributed.utils import _free_storage
def find_classes_in_model(model, class_names):
"""
Recursively find unique module classes in the model that match the given class names.
Args:
model: The PyTorch model to traverse.
class_names: A list of class name strings to look for.
Returns:
A set of matched class types.
"""
found_classes = set()
class_names_set = set(class_names)
def traverse(module):
if module.__class__.__name__ in class_names_set:
found_classes.add(module.__class__)
for child in module.children():
traverse(child)
traverse(model)
print(f"Found transformer classes: {found_classes}")
return found_classes
def create_transformer_auto_wrap_policy(
model,
transformer_layer_cls_to_wrap,
):
"""
Creates an auto wrap policy that only wraps modules belonging to the specified
transformer layer classes.
Args:
model: The PyTorch model to analyze for class types.
transformer_layer_cls_to_wrap: A list of class name strings to wrap.
Returns:
A callable auto wrap policy function.
"""
# Dynamically find the actual class types corresponding to the provided names
transformer_classes = find_classes_in_model(model, transformer_layer_cls_to_wrap)
if not transformer_classes:
raise ValueError(
f"No modules found with class names {transformer_layer_cls_to_wrap}. "
"Please check the class names or the model structure."
)
def transformer_policy(module, recurse, nonwrapped_numel=None, **kwargs):
# Use the standard transformer auto wrap policy with the discovered classes
policy_kwargs = dict(module=module, recurse=recurse, transformer_layer_cls=transformer_classes)
if nonwrapped_numel is not None:
policy_kwargs["nonwrapped_numel"] = nonwrapped_numel
return transformer_auto_wrap_policy(**policy_kwargs)
return transformer_policy
def shard_model(
model,
device_id,
param_dtype=torch.bfloat16,
reduce_dtype=torch.float32,
buffer_dtype=torch.float32,
process_group=None,
sharding_strategy=ShardingStrategy.FULL_SHARD,
sync_module_states=True,
module_to_wrapper=None,
transformer_layer_cls_to_wrap=None,
cast_dtype=True,
ignored_modules=None,
offload_to_cpu=False,
):
"""
Wraps the model with FSDP using the specified configuration.
Args:
model: The PyTorch model to shard.
device_id: The CUDA device ID.
param_dtype: Data type for parameters.
reduce_dtype: Data type for gradient reduction.
buffer_dtype: Data type for buffers.
process_group: The process group for distributed training.
sharding_strategy: The FSDP sharding strategy.
sync_module_states: Whether to sync module states across ranks.
module_to_wrapper: Specific modules to wrap if using lambda policy.
transformer_layer_cls_to_wrap: List of class names to wrap using transformer policy.
cast_dtype: Whether to cast the managed parameters to `param_dtype` before wrapping. Set False to
keep a pre-quantized storage dtype (e.g. float8) — MixedPrecision still computes in `param_dtype`.
ignored_modules: Modules excluded from FSDP (kept replicated in their own dtype via ignored_states).
Use for modules a mixed-precision checkpoint pins to float32: casting them into the shard dtype
makes AdaLN/output-head rounding accumulate coherently over the denoising trajectory (flicker).
offload_to_cpu: When True, wrap with FSDP's native `CPUOffload(offload_params=True)` so each rank keeps
its parameter shard in CPU RAM and streams it back to the GPU only around the wrapped module's
forward. This is the offload path that is compatible with sequence-parallel multi-GPU inference
(accelerate / diffusers module hooks cannot manage the flat-param shards, only FSDP itself can).
Only shards the parameter memory, not activations. Default False keeps the previous fully-resident
behaviour, so existing single-GPU / model_full_load callers are unaffected.
Returns:
The FSDP-wrapped model.
"""
if transformer_layer_cls_to_wrap is not None:
# Create policy based strictly on transformer layer classes
auto_wrap_policy = create_transformer_auto_wrap_policy(
model=model,
transformer_layer_cls_to_wrap=transformer_layer_cls_to_wrap,
)
else:
# Fallback to lambda policy if no transformer classes are specified
auto_wrap_policy = partial(
lambda_auto_wrap_policy,
lambda_fn=lambda m: m in (model.blocks if module_to_wrapper is None else module_to_wrapper)
)
# FSDP flattens each wrap unit's parameters into one flat buffer and requires a uniform dtype inside it.
# Models that pin a few modules to fp32 for numerical precision (e.g. MiniMax-H3's embedders/output heads)
# would otherwise fail with "Must flatten tensors with uniform dtype"; MixedPrecision computes in
# `param_dtype` anyway, so cast the managed params up front. With `cast_dtype=False` the caller
# intentionally keeps a different storage dtype (e.g. a pre-applied float8 quantization); FSDP keeps one
# flat buffer per dtype and MixedPrecision casts to `param_dtype` for the compute. The ignored modules
# keep their own dtype (typically float32) and stay replicated — a blanket model.to() on them would round
# the AdaLN modulation and accumulate coherently over the sampling trajectory into temporal flicker.
ignored_modules = list(ignored_modules) if ignored_modules else []
ignored_param_ids = {id(p) for m in ignored_modules for p in m.parameters()}
if cast_dtype and param_dtype is not None:
for p in model.parameters():
# A pre-applied float8 quantization stays as the storage dtype (MixedPrecision casts it to
# `param_dtype` for the compute, matching the non-FSDP qfloat8 dequant wrapper numerics); only
# the remaining dtypes (e.g. fp32 heads) are homogenized into `param_dtype`.
if (p.dtype != param_dtype and id(p) not in ignored_param_ids
and p.dtype not in (torch.float8_e4m3fn, torch.float8_e5m2)):
p.data = p.data.to(param_dtype)
# With CPU offload, bound the prefetch depth so at most one extra all-gather is in flight; otherwise FSDP
# would keep the gathered (unsharded) params of upcoming blocks resident and eat the savings.
cpu_offload = CPUOffload(offload_params=True) if offload_to_cpu else None
model = FSDP(
module=model,
process_group=process_group,
sharding_strategy=sharding_strategy,
auto_wrap_policy=auto_wrap_policy,
mixed_precision=MixedPrecision(
param_dtype=param_dtype,
reduce_dtype=reduce_dtype,
buffer_dtype=buffer_dtype),
device_id=device_id,
sync_module_states=sync_module_states,
cpu_offload=cpu_offload,
limit_all_gathers=offload_to_cpu,
ignored_states=ignored_modules if ignored_modules else None)
# `device_id`/`sync_module_states` only manage the sharded params; the ignored modules must be placed on
# the device explicitly. They are small fp32 heads kept replicated (not sharded), so FSDP's CPUOffload does
# not cover them: they stay GPU-resident even in offload mode, which is the intended numerical-precision pin.
for m in ignored_modules:
m.to(device_id)
return model
def free_model(model):
"""
Frees memory associated with the FSDP model.
Args:
model: The FSDP-wrapped model to free.
"""
for m in model.modules():
if isinstance(m, FSDP):
_free_storage(m._handle.flat_param.data)
del model
gc.collect()
torch.cuda.empty_cache()
def offload_components_cpu(obj, device, component_names):
r"""
Attach top-level accelerate CPU-offload hooks to a chosen subset of a pipeline's whole-model components
(by default the VAE / audio VAE), leaving every other component untouched.
This is the offload companion to `shard_model(..., offload_to_cpu=True)` for sequence-parallel multi-GPU
inference. There the transformer and the text encoder are wrapped by FSDP, whose flat-param shards are
managed only by FSDP itself (enabled to live in CPU RAM at wrap time); accelerate's module hooks cannot
manage those shards, and `pipeline.enable_model_cpu_offload()` would both `obj.to("cpu")` the FSDP-wrapped
modules and register a second, conflicting hook on them. So under sequence parallelism the big sharded
modules offload through FSDP and only the remaining non-FSDP components (VAE / audio VAE) are offloaded here.
The hooks chain like `enable_model_cpu_offload` does: onloading a component offloads the previous one. The
pipeline drives them through its `_offload_scope` helper, which fires `pre_forward` / `post_forward` around
method calls (`encode` / `decode`) that bypass a module's `forward`.
Also patches the pipeline's `_execution_device` to keep reporting the onload (GPU) device: the FSDP modules
carry no `_hf_hook`, so the stock property would otherwise fall back to `self.device` (CPU) as soon as one
of them leads the component order.
"""
from accelerate import cpu_offload_with_hook
onload_device = torch.device(device) if isinstance(device, str) else device
hooks = []
prev_hook = None
for name in component_names:
model = getattr(obj, name, None)
if not isinstance(model, torch.nn.Module):
continue
_, hook = cpu_offload_with_hook(model, onload_device, prev_module_hook=prev_hook)
hooks.append(hook)
prev_hook = hook
obj._cpu_offload_component_hooks = hooks
# `_execution_device` must return the onload device while these component hooks are attached.
if not hasattr(obj.__class__, "_execution_device_original"):
obj.__class__._execution_device_original = obj.__class__._execution_device
@property
def _execution_device(self):
# Return the onload device of any component that carries an accelerate CPU-offload hook; skip the
# FSDP-wrapped modules (no `_hf_hook`) instead of bailing to `self.device` on them.
for _, component in self.components.items():
if isinstance(component, torch.nn.Module):
hf_hook = getattr(component, "_hf_hook", None)
if hf_hook is not None and getattr(hf_hook, "execution_device", None) is not None:
return torch.device(hf_hook.execution_device)
# No component hook left: delegate to the saved original.
return self.__class__._execution_device_original.fget(self)
obj.__class__._execution_device = _execution_device
return hooks