| """Train compact SatlasNet on seven task families; supports AMP and torchrun.""" |
|
|
| import argparse |
| import importlib.util |
| import json |
| import os |
| 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] |
|
|
|
|
| def load_module(): |
| spec = importlib.util.spec_from_file_location("satlaspretrain", ROOT / "model/satlaspretrain.py") |
| module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module); return module |
|
|
|
|
| class NpzDataset(Dataset): |
| keys = ("highres_images", "lowres_images", "valid_highres_times", "valid_lowres_times", |
| "segmentation", "regression", "point", "polygon", "polyline", "property", "classification") |
| def __init__(self, path, config, module): |
| archive = np.load(path) |
| module.validate_npz(archive, config) |
| self.data = {key: archive[key] for key in self.keys} |
| self.source = str(archive["source"]) |
| self.protocol = str(archive["protocol"]) if "protocol" in archive else "provided_npz" |
| def __len__(self): return len(self.data["highres_images"]) |
| def __getitem__(self, index): return {key: torch.as_tensor(value[index]) for key, value in self.data.items()} |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--config", type=Path, default=ROOT / "conf/config.yaml"); parser.add_argument("--data", type=Path) |
| parser.add_argument("--checkpoint", type=Path); parser.add_argument("--device", choices=("auto", "cpu", "cuda")) |
| args = parser.parse_args(); config = yaml.safe_load(args.config.read_text()); train_cfg = config["training"] |
| world_size, rank, local_rank = int(os.environ.get("WORLD_SIZE", 1)), int(os.environ.get("RANK", 0)), int(os.environ.get("LOCAL_RANK", 0)) |
| requested = args.device or config["runtime"]["device"]; use_cuda = torch.cuda.is_available() and requested != "cpu" |
| if requested == "cuda" and not use_cuda: raise RuntimeError("CUDA requested but unavailable") |
| if world_size > 1: 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) |
| torch.manual_seed(config["seed"] + rank) |
| module = load_module() |
| data_path = args.data or ROOT / config["data"]["root"] / "train.npz"; dataset = NpzDataset(data_path, config, module) |
| sampler = DistributedSampler(dataset, shuffle=True) if world_size > 1 else None |
| loader = DataLoader(dataset, batch_size=train_cfg["batch_size"], sampler=sampler, shuffle=sampler is None, |
| num_workers=train_cfg["num_workers"], pin_memory=use_cuda) |
| model = module.SatlasPretrain(**config["model"]).to(device); raw_model = model |
| if world_size > 1: |
| model = DistributedDataParallel(model, device_ids=[local_rank] if use_cuda else None); raw_model = model.module |
| optimizer = torch.optim.AdamW(model.parameters(), lr=train_cfg["learning_rate"], weight_decay=train_cfg["weight_decay"]) |
| amp = bool(config["runtime"]["amp"] and use_cuda); scaler = torch.amp.GradScaler("cuda", enabled=amp); history = [] |
| for epoch in range(train_cfg["epochs"]): |
| if sampler is not None: sampler.set_epoch(epoch) |
| model.train(); totals = {"total": 0.0}; steps = 0 |
| for batch in loader: |
| batch = {key: value.to(device, non_blocking=use_cuda) for key, value in batch.items()} |
| autocast = partial(torch.amp.autocast, "cuda") if amp else nullcontext |
| with autocast(): |
| outputs = model(batch["highres_images"], batch["lowres_images"], |
| batch["valid_highres_times"], batch["valid_lowres_times"]) |
| loss, parts = module.multitask_loss(outputs, batch) |
| if not torch.isfinite(loss): raise ValueError("non-finite multitask loss") |
| optimizer.zero_grad(set_to_none=True); scaler.scale(loss).backward(); scaler.step(optimizer); scaler.update() |
| totals["total"] += loss.detach().item(); steps += 1 |
| for name, value in parts.items(): totals[name] = totals.get(name, 0.0) + value.detach().item() |
| names = list(totals); statistics = torch.tensor([totals[name] for name in names] + [steps], dtype=torch.float64, device=device) |
| if world_size > 1: dist.all_reduce(statistics, op=dist.ReduceOp.SUM) |
| global_steps = max(statistics[-1].item(), 1) |
| record = {"epoch": epoch + 1, **{f"{name}_loss": statistics[index].item() / global_steps for index, name in enumerate(names)}}; history.append(record) |
| if rank == 0: print(json.dumps(record)) |
| if rank == 0: |
| checkpoint_path = args.checkpoint or ROOT / config["paths"]["checkpoint"]; checkpoint_path.parent.mkdir(parents=True, exist_ok=True) |
| torch.save({"model": raw_model.state_dict(), "optimizer": optimizer.state_dict(), "scaler": scaler.state_dict() if amp else None, |
| "config": config, "epoch": train_cfg["epochs"], "history": history}, checkpoint_path) |
| metrics = ROOT / config["paths"]["training_metrics"]; metrics.parent.mkdir(parents=True, exist_ok=True) |
| metrics.write_text(json.dumps({"history": history, "protocol": dataset.protocol, |
| "source": dataset.source, |
| "world_size": world_size, "amp": amp}, indent=2) + "\n") |
| print(f"checkpoint={checkpoint_path}") |
| if world_size > 1: dist.destroy_process_group() |
|
|
|
|
| if __name__ == "__main__": main() |
|
|