Download scripts/train.py from OneScience-Group/SEEDS: direct link, hf CLI and curl.
- Browser
- Download file 8.76 kB
-
https://huggingface.co/OneScience-Group/SEEDS/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/SEEDS/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/SEEDS/resolve/main/scripts/train.py
8.76 kB
| """Train or fine-tune the SEEDS conditional diffusion model.""" | |
| from __future__ import annotations | |
| import argparse | |
| import math | |
| import os | |
| import numpy as np | |
| import torch | |
| import torch.distributed as dist | |
| from torch import nn | |
| from torch.nn.parallel import DistributedDataParallel as DDP | |
| from torch.utils.data import DistributedSampler | |
| from common import SEEDS, build_model, choose_device, load_config, resolve_path, set_seed | |
| from data_loader import SEEDSDataset, build_dataloader | |
| def _is_distributed() -> bool: | |
| return int(os.environ.get("WORLD_SIZE", "1")) > 1 | |
| def _setup_distributed(requested_device: str) -> tuple[torch.device, int, int, bool]: | |
| distributed = _is_distributed() | |
| if not distributed: | |
| return choose_device(requested_device), 0, 1, False | |
| if not dist.is_initialized(): | |
| backend = "nccl" if torch.cuda.is_available() else "gloo" | |
| dist.init_process_group(backend=backend, init_method="env://") | |
| rank = dist.get_rank() | |
| world_size = dist.get_world_size() | |
| local_rank = int(os.environ.get("LOCAL_RANK", rank)) | |
| if torch.cuda.is_available(): | |
| torch.cuda.set_device(local_rank) | |
| device = torch.device("cuda", local_rank) | |
| else: | |
| device = torch.device("cpu") | |
| return device, rank, world_size, True | |
| def _denoising_loss( | |
| model: nn.Module, | |
| clean: torch.Tensor, | |
| seeds: torch.Tensor, | |
| climate: torch.Tensor, | |
| ) -> torch.Tensor: | |
| core_model = model.module if isinstance(model, DDP) else model | |
| diffusion_time = torch.rand(clean.shape[0], device=clean.device, dtype=clean.dtype) | |
| noise = torch.randn_like(clean) | |
| sigma = core_model.sigma(diffusion_time).view(-1, 1, 1, 1, 1) | |
| noisy = clean + sigma * noise | |
| model_input = noisy / torch.sqrt(1.0 + sigma.square()) | |
| prediction = model(model_input, seeds, climate, diffusion_time) | |
| return ((prediction - noise) ** 2).flatten(1).mean() | |
| def _run_validation(model: nn.Module, loader, device: torch.device, distributed: bool) -> float: | |
| model.eval() | |
| losses = [] | |
| cuda_devices = [device.index] if device.type == "cuda" and device.index is not None else [] | |
| with torch.random.fork_rng(devices=cuda_devices): | |
| torch.manual_seed(12345) | |
| with torch.no_grad(): | |
| for batch in loader: | |
| loss = _denoising_loss( | |
| model, | |
| batch["targets"].to(device), | |
| batch["seeds"].to(device), | |
| batch["climate"].to(device), | |
| ) | |
| losses.append(float(loss)) | |
| if not losses: | |
| raise RuntimeError("validation loader produced no batches") | |
| value = torch.tensor(float(np.mean(losses)), device=device) | |
| if distributed: | |
| dist.all_reduce(value, op=dist.ReduceOp.SUM) | |
| value /= dist.get_world_size() | |
| return float(value.item()) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", default="conf/config.yaml") | |
| parser.add_argument("--max-steps", type=int, default=None) | |
| parser.add_argument("--max-batches", type=int, default=None) | |
| parser.add_argument("--device", default=None) | |
| parser.add_argument("--finetune", action="store_true") | |
| args = parser.parse_args() | |
| config = load_config(args.config) | |
| device, rank, world_size, distributed = _setup_distributed(args.device or config["training"]["device"]) | |
| set_seed(config["project"]["seed"] + rank) | |
| data, paths, training = config["data"], config["paths"], config["training"] | |
| root = args.config | |
| train_path, val_path = resolve_path(paths["train_data"], root), resolve_path(paths["val_data"], root) | |
| train_dataset = SEEDSDataset(train_path, len(data["variables"]), data["faces"], data["height"], data["width"], data["seed_count"]) | |
| val_dataset = SEEDSDataset(val_path, len(data["variables"]), data["faces"], data["height"], data["width"], data["seed_count"]) | |
| train_sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank, shuffle=True) if distributed else None | |
| val_sampler = DistributedSampler(val_dataset, num_replicas=world_size, rank=rank, shuffle=False) if distributed else None | |
| train_loader = build_dataloader(train_path, len(data["variables"]), data["faces"], data["height"], data["width"], data["seed_count"], training["batch_size"], True, training["num_workers"], args.max_batches, train_sampler, train_dataset) | |
| val_loader = build_dataloader(val_path, len(data["variables"]), data["faces"], data["height"], data["width"], data["seed_count"], training["batch_size"], False, training["num_workers"], args.max_batches, val_sampler, val_dataset) | |
| model = build_model(config).to(device) | |
| checkpoint_path = resolve_path(paths["checkpoint"], root) | |
| if args.finetune or training.get("resume"): | |
| source = resolve_path(training.get("resume") or paths["checkpoint"], root) | |
| if not source.exists(): | |
| raise FileNotFoundError(f"checkpoint does not exist: {source}") | |
| state = torch.load(source, map_location=device, weights_only=False) | |
| model.load_state_dict(state["model"] if "model" in state else state) | |
| if distributed: | |
| model = DDP(model, device_ids=[device.index] if device.type == "cuda" else None) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=training["learning_rate"], weight_decay=training["weight_decay"]) | |
| train_losses, val_losses = [], [] | |
| completed_steps = 0 | |
| accumulation_steps = training.get("gradient_accumulation_steps", 1) | |
| if accumulation_steps < 1: | |
| raise ValueError("gradient_accumulation_steps must be positive") | |
| loss_ema = None | |
| for epoch in range(training["epochs"]): | |
| if train_sampler is not None: | |
| train_sampler.set_epoch(epoch) | |
| model.train() | |
| epoch_losses = [] | |
| optimizer.zero_grad(set_to_none=True) | |
| accumulated_batches = 0 | |
| for batch in train_loader: | |
| loss = _denoising_loss( | |
| model, | |
| batch["targets"].to(device), | |
| batch["seeds"].to(device), | |
| batch["climate"].to(device), | |
| ) | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError(f"non-finite training loss at epoch {epoch + 1}: {loss.item()}") | |
| (loss / accumulation_steps).backward() | |
| epoch_losses.append(float(loss.detach())) | |
| completed_steps += 1 | |
| accumulated_batches += 1 | |
| if accumulated_batches == accumulation_steps: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), training["grad_clip_norm"]) | |
| optimizer.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| accumulated_batches = 0 | |
| if args.max_steps is not None and completed_steps >= args.max_steps: | |
| break | |
| if accumulated_batches: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), training["grad_clip_norm"]) | |
| optimizer.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| if not epoch_losses: | |
| raise RuntimeError("training loader produced no batches") | |
| epoch_loss_tensor = torch.tensor([sum(epoch_losses), len(epoch_losses)], device=device, dtype=torch.float64) | |
| if distributed: | |
| dist.all_reduce(epoch_loss_tensor, op=dist.ReduceOp.SUM) | |
| epoch_loss = float((epoch_loss_tensor[0] / epoch_loss_tensor[1]).item()) | |
| train_losses.append(epoch_loss) | |
| loss_ema = epoch_loss if loss_ema is None else 0.9 * loss_ema + 0.1 * epoch_loss | |
| validation = _run_validation(model, val_loader, device, distributed) | |
| val_losses.append(validation) | |
| if rank == 0: | |
| print( | |
| f"epoch={epoch + 1}/{training['epochs']} " | |
| f"train_loss={epoch_loss:.6f} train_ema={loss_ema:.6f} val_loss={validation:.6f}" | |
| ) | |
| if args.max_steps is not None and completed_steps >= args.max_steps: | |
| break | |
| checkpoint_path.parent.mkdir(parents=True, exist_ok=True) | |
| if rank == 0: | |
| state_dict = model.module.state_dict() if distributed else model.state_dict() | |
| torch.save({"model": state_dict, "config": config, "epoch": len(train_losses), "step": completed_steps}, checkpoint_path) | |
| np.save(checkpoint_path.parent / "train_loss.npy", np.asarray(train_losses, dtype=np.float32)) | |
| np.save(checkpoint_path.parent / "val_loss.npy", np.asarray(val_losses, dtype=np.float32)) | |
| if not math.isfinite(train_losses[-1]): | |
| raise FloatingPointError("final training loss is not finite") | |
| if rank == 0: | |
| print(f"saved checkpoint: {checkpoint_path}") | |
| if distributed: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |