Download scripts/train.py from OneScience-Group/SatMAE: direct link, hf CLI and curl.
- Browser
- Download file 9.22 kB
-
https://huggingface.co/OneScience-Group/SatMAE/resolve/main/scripts/train.py
- Command line
-
hf download hf://OneScience-Group/SatMAE/scripts/train.py
-
curl -L -o train.py https://huggingface.co/OneScience-Group/SatMAE/resolve/main/scripts/train.py
9.22 kB
| """Pre-train SatMAE with masked reconstruction; supports torchrun.""" | |
| import argparse | |
| import importlib.util | |
| import json | |
| import math | |
| import os | |
| import random | |
| from contextlib import nullcontext | |
| from functools import partial | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import yaml | |
| from torch import distributed as dist | |
| from torch.nn.parallel import DistributedDataParallel | |
| from torch.utils.data import DataLoader, Dataset, DistributedSampler | |
| ROOT = Path(__file__).resolve().parents[1] | |
| class NPZDataset(Dataset): | |
| def __init__(self, path, mode): | |
| archive = np.load(path) | |
| self.images = archive["images"] | |
| self.timestamps = archive["timestamps"] if "timestamps" in archive else None | |
| if mode == "temporal" and self.timestamps is None: | |
| raise ValueError("temporal datasets must contain timestamps") | |
| def __len__(self): | |
| return len(self.images) | |
| def __getitem__(self, index): | |
| images = torch.from_numpy(self.images[index]) | |
| if self.timestamps is None: | |
| return images, torch.empty(0) | |
| return images, torch.from_numpy(self.timestamps[index]) | |
| def load_model_class(): | |
| spec = importlib.util.spec_from_file_location("satmae", ROOT / "model/satmae.py") | |
| module = importlib.util.module_from_spec(spec) | |
| spec.loader.exec_module(module) | |
| return module.SatMAE | |
| def model_config(config): | |
| return { | |
| key: value for key, value in config["model"].items() | |
| if key not in {"architecture", "runtime_profile"} | |
| } | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml") | |
| parser.add_argument("--data", type=Path, default=None) | |
| parser.add_argument("--output", type=Path, default=None) | |
| parser.add_argument("--resume", type=Path, default=None) | |
| parser.add_argument("--epochs", type=int, default=None) | |
| parser.add_argument("--batch-size", type=int, default=None) | |
| parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default=None) | |
| return parser.parse_args() | |
| def cosine_learning_rate(progress, config, peak_lr): | |
| warmup = config["warmup_epochs"] | |
| if warmup > 0 and progress < warmup: | |
| return peak_lr * progress / warmup | |
| span = max(config["epochs"] - warmup, 1) | |
| phase = min(max((progress - warmup) / span, 0.0), 1.0) | |
| return config["min_learning_rate"] + 0.5 * ( | |
| peak_lr - config["min_learning_rate"] | |
| ) * (1.0 + math.cos(math.pi * phase)) | |
| def main(): | |
| args = parse_args() | |
| config = yaml.safe_load(args.config.read_text()) | |
| train_config = config["training"] | |
| if args.epochs is not None: | |
| train_config["epochs"] = args.epochs | |
| if args.batch_size is not None: | |
| train_config["batch_size"] = args.batch_size | |
| world_size = int(os.environ.get("WORLD_SIZE", "1")) | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| rank = int(os.environ.get("RANK", "0")) | |
| distributed = world_size > 1 | |
| requested_device = args.device or config["runtime"]["device"] | |
| use_cuda = torch.cuda.is_available() and requested_device != "cpu" | |
| if requested_device == "cuda" and not torch.cuda.is_available(): | |
| raise RuntimeError("CUDA was requested but is unavailable") | |
| if distributed: | |
| dist.init_process_group("nccl" if use_cuda else "gloo") | |
| device = torch.device(f"cuda:{local_rank}" if use_cuda else "cpu") | |
| if use_cuda: | |
| torch.cuda.set_device(local_rank) | |
| seed = config["seed"] + rank | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| data_path = args.data or ROOT / config["data"]["root"] / "train.npz" | |
| if not data_path.exists(): | |
| raise FileNotFoundError(f"training data not found: {data_path}") | |
| dataset = NPZDataset(data_path, config["model"]["mode"]) | |
| sampler = DistributedSampler(dataset, shuffle=True) if distributed else None | |
| loader = DataLoader( | |
| dataset, | |
| batch_size=train_config["batch_size"], | |
| shuffle=sampler is None, | |
| sampler=sampler, | |
| num_workers=train_config["num_workers"], | |
| pin_memory=use_cuda, | |
| drop_last=False, | |
| ) | |
| model = load_model_class()(**model_config(config)).to(device) | |
| model_without_ddp = model | |
| if distributed: | |
| model = DistributedDataParallel( | |
| model, device_ids=[local_rank] if use_cuda else None | |
| ) | |
| model_without_ddp = model.module | |
| effective_batch = ( | |
| train_config["batch_size"] * train_config["accum_iter"] * world_size | |
| ) | |
| peak_lr = train_config["learning_rate"] | |
| if peak_lr is None: | |
| peak_lr = train_config["base_learning_rate"] * effective_batch / 256 | |
| decay, no_decay = [], [] | |
| for name, parameter in model_without_ddp.named_parameters(): | |
| if not parameter.requires_grad: | |
| continue | |
| (no_decay if parameter.ndim == 1 or name.endswith("bias") else decay).append(parameter) | |
| optimizer = torch.optim.AdamW( | |
| [ | |
| {"params": decay, "weight_decay": train_config["weight_decay"]}, | |
| {"params": no_decay, "weight_decay": 0.0}, | |
| ], | |
| lr=peak_lr, | |
| betas=(0.9, 0.95), | |
| ) | |
| amp_enabled = bool(config["runtime"].get("amp", True) and use_cuda) | |
| scaler = torch.amp.GradScaler("cuda", enabled=amp_enabled) | |
| start_epoch = 0 | |
| history = [] | |
| resume_path = args.resume | |
| if resume_path is None and train_config.get("resume"): | |
| resume_path = ROOT / train_config["resume"] | |
| if resume_path is not None: | |
| checkpoint = torch.load(resume_path, map_location="cpu", weights_only=False) | |
| model_without_ddp.load_state_dict(checkpoint["model"]) | |
| optimizer.load_state_dict(checkpoint["optimizer"]) | |
| if checkpoint.get("scaler") is not None: | |
| scaler.load_state_dict(checkpoint["scaler"]) | |
| start_epoch = checkpoint["epoch"] + 1 | |
| history = checkpoint.get("history", []) | |
| checkpoint_path = args.output or ROOT / config["paths"]["checkpoint"] | |
| metrics_path = ROOT / config["paths"]["training_metrics"] | |
| optimizer.zero_grad(set_to_none=True) | |
| for epoch in range(start_epoch, train_config["epochs"]): | |
| if sampler is not None: | |
| sampler.set_epoch(epoch) | |
| model.train() | |
| total_loss = 0.0 | |
| steps = len(loader) | |
| for step, (images, timestamps) in enumerate(loader): | |
| progress = epoch + step / max(steps, 1) | |
| learning_rate = cosine_learning_rate(progress, train_config, peak_lr) | |
| for group in optimizer.param_groups: | |
| group["lr"] = learning_rate | |
| images = images.to(device, non_blocking=use_cuda) | |
| timestamps = timestamps.to(device, non_blocking=use_cuda) | |
| timestamps = timestamps if timestamps.numel() else None | |
| autocast = partial(torch.amp.autocast, "cuda") if amp_enabled else nullcontext | |
| with autocast(): | |
| output = model(images, timestamps=timestamps) | |
| loss = output["loss"] / train_config["accum_iter"] | |
| if not torch.isfinite(loss): | |
| raise ValueError(f"non-finite loss at epoch {epoch}, step {step}") | |
| scaler.scale(loss).backward() | |
| update = (step + 1) % train_config["accum_iter"] == 0 or step + 1 == steps | |
| if update: | |
| scaler.step(optimizer) | |
| scaler.update() | |
| optimizer.zero_grad(set_to_none=True) | |
| total_loss += output["loss"].detach().item() | |
| epoch_loss = total_loss / max(steps, 1) | |
| record = { | |
| "epoch": epoch + 1, | |
| "reconstruction_loss": epoch_loss, | |
| "learning_rate": optimizer.param_groups[0]["lr"], | |
| } | |
| history.append(record) | |
| if rank == 0: | |
| print( | |
| f"epoch={epoch + 1} reconstruction_loss={epoch_loss:.6f} " | |
| f"lr={record['learning_rate']:.3e}" | |
| ) | |
| if (epoch + 1) % train_config["save_every"] == 0 or epoch + 1 == train_config["epochs"]: | |
| checkpoint_path.parent.mkdir(parents=True, exist_ok=True) | |
| torch.save( | |
| { | |
| "model": model_without_ddp.state_dict(), | |
| "optimizer": optimizer.state_dict(), | |
| "scaler": scaler.state_dict() if amp_enabled else None, | |
| "epoch": epoch, | |
| "history": history, | |
| "config": config, | |
| }, | |
| checkpoint_path, | |
| ) | |
| if rank == 0: | |
| metrics_path.parent.mkdir(parents=True, exist_ok=True) | |
| metrics_path.write_text(json.dumps({ | |
| "history": history, | |
| "protocol": config["data"]["protocol"], | |
| "data_source": "synthetic" if "synthetic" in data_path.name or (data_path.parent / "format.json").exists() else "provided", | |
| "effective_batch_size": effective_batch, | |
| "peak_learning_rate": peak_lr, | |
| }, indent=2) + "\n") | |
| print("checkpoint=", checkpoint_path) | |
| if distributed: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |