from datetime import timedelta from functools import partial import os import torch import torch.distributed as dist from torch.distributed.fsdp import FullOptimStateDictConfig, FullStateDictConfig, FullyShardedDataParallel as FSDP, MixedPrecision, ShardingStrategy, StateDictType from torch.distributed.fsdp.api import CPUOffload from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy, transformer_auto_wrap_policy def fsdp_state_dict(model): fsdp_fullstate_save_policy = FullStateDictConfig( offload_to_cpu=True, rank0_only=True ) with FSDP.state_dict_type( model, StateDictType.FULL_STATE_DICT, fsdp_fullstate_save_policy ): checkpoint = model.state_dict() return checkpoint def fsdp_optim_state_dict(model, optimizer): """Gather the full optimizer state on rank 0 (CPU). Other ranks return an empty dict.""" state_dict_config = FullStateDictConfig(offload_to_cpu=True, rank0_only=True) optim_state_dict_config = FullOptimStateDictConfig(offload_to_cpu=True, rank0_only=True) with FSDP.state_dict_type( model, StateDictType.FULL_STATE_DICT, state_dict_config=state_dict_config, optim_state_dict_config=optim_state_dict_config, ): return FSDP.optim_state_dict(model, optimizer) def load_fsdp_optim_state_dict(model, optimizer, full_optim_state_dict): """Scatter a full optimizer state (valid on rank 0) into the local FSDP shards.""" with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT): sharded = FSDP.optim_state_dict_to_load( model=model, optim=optimizer, optim_state_dict=full_optim_state_dict, ) optimizer.load_state_dict(sharded) def fsdp_wrap(module, sharding_strategy="full", mixed_precision=False, wrap_strategy="size", min_num_params=int(5e7), transformer_module=None, cpu_offload=False): if mixed_precision: mixed_precision_policy = MixedPrecision( param_dtype=torch.bfloat16, reduce_dtype=torch.float32, buffer_dtype=torch.float32, cast_forward_inputs=False ) else: mixed_precision_policy = None if wrap_strategy == "transformer": auto_wrap_policy = partial( transformer_auto_wrap_policy, transformer_layer_cls=transformer_module ) elif wrap_strategy == "size": auto_wrap_policy = partial( size_based_auto_wrap_policy, min_num_params=min_num_params ) else: raise ValueError(f"Invalid wrap strategy: {wrap_strategy}") os.environ["NCCL_CROSS_NIC"] = "1" sharding_strategy = { "full": ShardingStrategy.FULL_SHARD, "hybrid_full": ShardingStrategy.HYBRID_SHARD, "hybrid_zero2": ShardingStrategy._HYBRID_SHARD_ZERO2, "no_shard": ShardingStrategy.NO_SHARD, }[sharding_strategy] module = FSDP( module, auto_wrap_policy=auto_wrap_policy, sharding_strategy=sharding_strategy, mixed_precision=mixed_precision_policy, device_id=torch.cuda.current_device(), limit_all_gathers=True, use_orig_params=True, cpu_offload=CPUOffload(offload_params=cpu_offload), sync_module_states=False # Load ckpt on rank 0 and sync to other ranks ) return module def barrier(): if dist.is_initialized(): dist.barrier() def launch_distributed_job(backend: str = "nccl"): rank = int(os.environ["RANK"]) local_rank = int(os.environ["LOCAL_RANK"]) world_size = int(os.environ["WORLD_SIZE"]) host = os.environ["MASTER_ADDR"] port = int(os.environ["MASTER_PORT"]) if ":" in host: # IPv6 init_method = f"tcp://[{host}]:{port}" else: # IPv4 init_method = f"tcp://{host}:{port}" dist.init_process_group(rank=rank, world_size=world_size, backend=backend, init_method=init_method, timeout=timedelta(minutes=30)) torch.cuda.set_device(local_rank) class EMA_FSDP: def __init__(self, fsdp_module: torch.nn.Module, decay: float = 0.999): self.decay = decay self.shadow = {} self._init_shadow(fsdp_module) @torch.no_grad() def _init_shadow(self, fsdp_module): from torch.distributed.fsdp import FullyShardedDataParallel as FSDP # Anchor shadow to the local CUDA device. summon_full_params can return # CPU views of params under some FSDP/resume code paths, so don't # implicitly inherit p's device. target_device = torch.device("cuda", torch.cuda.current_device()) with FSDP.summon_full_params(fsdp_module, writeback=False): for n, p in fsdp_module.module.named_parameters(): self.shadow[n] = p.detach().to(target_device).float().clone() @torch.no_grad() def update(self, fsdp_module): d = self.decay from torch.distributed.fsdp import FullyShardedDataParallel as FSDP with FSDP.summon_full_params(fsdp_module, writeback=False): for n, p in fsdp_module.module.named_parameters(): shadow = self.shadow[n] # Always align the source tensor to shadow's device to avoid # cuda/cpu mismatches when FSDP exposes params on CPU. shadow.mul_(d).add_( p.detach().to(shadow.device).float(), alpha=1. - d ) # Optional helpers --------------------------------------------------- def state_dict(self): return self.shadow # picklable def load_state_dict(self, sd): self.shadow = {k: v.clone() for k, v in sd.items()} def copy_to(self, fsdp_module): # load EMA weights into an (unwrapped) copy of the generator from torch.distributed.fsdp import FullyShardedDataParallel as FSDP with FSDP.summon_full_params(fsdp_module, writeback=True): for n, p in fsdp_module.module.named_parameters(): if n in self.shadow: p.data.copy_(self.shadow[n].to(p.dtype, device=p.device))