from __future__ import annotations import argparse import os import random from pathlib import Path import numpy as np import torch import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel from torch.utils.data import DataLoader from torch.utils.data.distributed import DistributedSampler from tqdm import tqdm from config import load_config, resolve_path from diffusion import create_diffusion from two_condition_dataset import TwoConditionDataset from models_controlnet_two_condition import TwoConditionControlNetSWaG # This script is bundled directly under /src on the server. ROOT = Path(__file__).resolve().parents[1] def distributed_context() -> tuple[int, int, int]: world_size = int(os.environ.get("WORLD_SIZE", "1")) rank = int(os.environ.get("RANK", "0")) local_rank = int(os.environ.get("LOCAL_RANK", "0")) if world_size > 1: dist.init_process_group("nccl") return rank, local_rank, world_size def save_checkpoint(path, model, optimizer, epoch, step, config, include_optimizer: bool): source = model.module if isinstance(model, DistributedDataParallel) else model payload = { "model": source.state_dict(), "epoch": epoch, "step": step, "config": config, } if include_optimizer: payload["optimizer"] = optimizer.state_dict() temporary = path.with_suffix(".tmp") torch.save(payload, temporary) temporary.replace(path) def main(): parser = argparse.ArgumentParser() parser.add_argument("--config", required=True) parser.add_argument("--smoke", action="store_true") parser.add_argument("--target-epoch", type=int, default=0) parser.add_argument("--resume", type=Path, default=None) args = parser.parse_args() if not torch.cuda.is_available(): raise RuntimeError("CUDA is required") rank, local_rank, world_size = distributed_context() device = torch.device("cuda", local_rank) torch.cuda.set_device(device) config_path = Path(args.config).resolve() config = load_config(config_path) training = config["training"] seed = int(training.get("seed", 0)) + rank random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) output = resolve_path(ROOT, config["output"]["directory"]) checkpoint_dir = output / "checkpoints" if rank == 0: checkpoint_dir.mkdir(parents=True, exist_ok=True) (output / "config.yaml").write_text(config_path.read_text(encoding="utf-8"), encoding="utf-8") if world_size > 1: dist.barrier() dataset = TwoConditionDataset( resolve_path(ROOT, config["data"]["train_h5"]), cache_in_memory=bool(training.get("cache_in_memory", False)), ) global_batch = int(training["global_batch_size"]) if global_batch % world_size: raise ValueError(f"global_batch_size must be divisible by world_size ({world_size})") local_batch = global_batch // world_size sampler = DistributedSampler(dataset, shuffle=True, seed=int(training.get("seed", 0))) if world_size > 1 else None loader = DataLoader( dataset, batch_size=local_batch, shuffle=sampler is None, sampler=sampler, num_workers=int(training.get("num_workers", 0)), pin_memory=True, drop_last=True, ) model = TwoConditionControlNetSWaG(**config["model"]) start_epoch = 0 step = 0 resume_checkpoint = args.resume.resolve() if args.resume else None if resume_checkpoint is not None: checkpoint = torch.load(resume_checkpoint, map_location="cpu", weights_only=False) model.load_state_dict(checkpoint["model"], strict=True) start_epoch = int(checkpoint.get("epoch", 0)) step = int(checkpoint.get("step", 0)) else: checkpoint_path = resolve_path(ROOT, training["base_checkpoint"]) checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=False) base_state = checkpoint["model"] result = model.load_state_dict(base_state, strict=False) allowed_missing = ( "p_embedder.", "s_embedder.", "condition_mlp.", "control_input_zero.", "control_mid_input_zero.", "control_large_input_zero.", "control_blocks.", "control_zero.", "control_mid_zero.", "control_large_zero.", ) unexpected = list(result.unexpected_keys) missing = [key for key in result.missing_keys if not key.startswith(allowed_missing)] if unexpected or missing: raise RuntimeError(f"checkpoint mismatch: unexpected={unexpected[:8]} missing={missing[:8]}") model.copy_backbone_blocks_to_control() train_prefixes = ( "p_embedder.", "s_embedder.", "condition_mlp.", "control_input_zero.", "control_mid_input_zero.", "control_large_input_zero.", "control_blocks.", "control_zero.", "control_mid_zero.", "control_large_zero.", ) for name, parameter in model.named_parameters(): parameter.requires_grad_(name.startswith(train_prefixes)) trainable = [parameter for parameter in model.parameters() if parameter.requires_grad] if rank == 0: print(f"resume_checkpoint={resume_checkpoint}") print(f"world_size={world_size} global_batch={global_batch} per_gpu_batch={local_batch}") print(f"control_depth={model.control_depth}") print(f"trainable_parameters={sum(p.numel() for p in trainable):,}") print(f"frozen_parameters={sum(p.numel() for p in model.parameters() if not p.requires_grad):,}") model.to(device) if world_size > 1: model = DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank) optimizer = torch.optim.AdamW( trainable, lr=float(training["learning_rate"]), weight_decay=float(training.get("weight_decay", 0.0)), ) diffusion = create_diffusion(timestep_respacing="", **config["diffusion"]) use_amp = bool(training.get("use_amp", True)) if hasattr(torch, "amp") and hasattr(torch.amp, "GradScaler"): scaler = torch.amp.GradScaler("cuda", enabled=use_amp) else: scaler = torch.cuda.amp.GradScaler(enabled=use_amp) if resume_checkpoint is not None and "optimizer" in checkpoint: optimizer.load_state_dict(checkpoint["optimizer"]) elif resume_checkpoint is not None: print("WARNING: resume checkpoint has no optimizer state; optimizer restarts fresh.") del checkpoint epochs = 1 if args.smoke else (int(args.target_epoch) if args.target_epoch else int(training["epochs"])) if epochs < start_epoch: raise ValueError(f"target epoch {epochs} is before checkpoint epoch {start_epoch}") optimizer.zero_grad(set_to_none=True) for epoch in range(start_epoch, epochs): if sampler is not None: sampler.set_epoch(epoch) model.train() total_loss = 0.0 for batch_index, (waveforms, labels) in enumerate( tqdm( loader, desc=f"epoch {epoch + 1}/{epochs}", disable=rank != 0 or not bool(training.get("progress_bar", True)), ) ): waveforms = waveforms.to(device, non_blocking=True) labels = labels.to(device, non_blocking=True) timesteps = torch.randint( 0, diffusion.num_timesteps, (waveforms.shape[0],), device=device ) with torch.autocast("cuda", enabled=use_amp, dtype=torch.float16): loss = diffusion.training_losses( model, waveforms, timesteps, model_kwargs={"labels": labels} )["loss"].mean() scaler.scale(loss).backward() max_grad_norm = float(training.get("max_grad_norm", 0.0)) if max_grad_norm > 0: scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(trainable, max_grad_norm) scaler.step(optimizer) scaler.update() optimizer.zero_grad(set_to_none=True) step += 1 total_loss += float(loss.detach()) if args.smoke: break batches = 1 if args.smoke else len(loader) mean_loss = torch.tensor(total_loss / batches, device=device) if world_size > 1: dist.all_reduce(mean_loss, op=dist.ReduceOp.SUM) mean_loss /= world_size if rank == 0: print(f"epoch={epoch + 1} loss={mean_loss.item():.6f}") interval = int(config["output"].get("checkpoint_every_epochs", 10)) if rank == 0 and not args.smoke and ((epoch + 1) % interval == 0 or epoch + 1 == epochs): save_checkpoint( checkpoint_dir / f"checkpoint_epoch_{epoch + 1:05d}.pt", model, optimizer, epoch + 1, step, config, include_optimizer=bool(config["output"].get("save_optimizer_state", True)), ) if world_size > 1: dist.barrier() dataset.close() if world_size > 1: dist.destroy_process_group() if __name__ == "__main__": main()