from __future__ import annotations import os from pathlib import Path from typing import Any import torch import yaml ROOT = Path(__file__).resolve().parents[1] DEFAULT_CONFIG = ROOT / "conf/config.yaml" def load_config(path: str | Path = DEFAULT_CONFIG) -> dict[str, Any]: config_path = Path(path).expanduser().resolve() with config_path.open("r", encoding="utf-8") as handle: config = yaml.safe_load(handle) if not isinstance(config, dict): raise ValueError("configuration must be a YAML mapping") for section in ("data", "model", "train", "dataloader", "distributed"): if section not in config: raise ValueError(f"missing configuration section: {section}") data, model = config["data"], config["model"] for key in ("input_length", "output_length", "height", "width", "channels"): if int(data.get(key, 0)) <= 0: raise ValueError(f"data.{key} must be positive") if int(data["height"]) % 4 or int(data["width"]) % 4: raise ValueError("data height and width must be divisible by four") dims, depths = model.get("dims"), model.get("depths") if not isinstance(dims, list) or len(dims) != 2 or int(dims[1]) != 2 * int(dims[0]): raise ValueError("model.dims must be [D, 2*D]") if not isinstance(depths, list) or len(depths) != 2 or min(int(x) for x in depths) < 1: raise ValueError("model.depths must contain two positive integers") heads = int(model.get("heads", 0)) if heads < 1 or any(int(dim) % heads for dim in dims): raise ValueError("model.heads must divide both hidden dimensions") normalization = data.get("normalization", "unit") if normalization not in ("unit", "uint8_255"): raise ValueError("data.normalization must be 'unit' or 'uint8_255'") for key in ("data_dir", "train_npz", "val_npz", "test_npz"): if data.get(key): value = Path(data[key]).expanduser() data[key] = str(value if value.is_absolute() else ROOT / value) output_dir = Path(config["train"]["output_dir"]).expanduser() config["train"]["output_dir"] = str(output_dir if output_dir.is_absolute() else ROOT / output_dir) return config def resolve_device(requested: str, local_rank: int = 0) -> torch.device: if requested not in ("auto", "cpu", "cuda"): raise ValueError("device must be auto, cpu, or cuda") use_accelerator = requested == "cuda" or (requested == "auto" and torch.cuda.is_available()) if use_accelerator: if not torch.cuda.is_available(): raise RuntimeError("CUDA/ROCm device requested but torch.cuda.is_available() is false") torch.cuda.set_device(local_rank) return torch.device("cuda", local_rank) return torch.device("cpu") def resolve_cli_path(value: str | None) -> str | None: if not value: return value path = Path(value).expanduser() return str(path if path.is_absolute() else ROOT / path) def load_checkpoint_payload(path: str | Path, device: torch.device) -> dict[str, Any]: payload = torch.load(path, map_location=device, weights_only=False) if not isinstance(payload, dict) or "model" not in payload or "config" not in payload: raise ValueError("checkpoint must contain model and config") return payload def clean_state_dict(state_dict: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: return {key.removeprefix("module."): value for key, value in state_dict.items()} def atomic_torch_save(payload: dict[str, Any], path: str | Path) -> Path: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp") torch.save(payload, temporary) os.replace(temporary, path) return path