coarse-grained-forecasting-benchmark's picture
Add anonymous reproduction code
41dd7e0 verified
Raw History Blame Contribute Delete
20.5 kB
"""Training loop: exactly ``training.max_steps`` optimizer updates, with EMA weights, gradient clipping and
validation events.
A validation event fires after global update ``step`` when ``step % val_every_updates == 0``, at ``max_steps`` and
at every step listed in ``val_extra_steps`` (0 = the untrained model). An event evaluates the training objective on
the validation pairs (``val_loss``, and ``val_loss_ema`` for the EMA weights), writes one row of ``train_log.csv``
(``event`` = 1), saves ``checkpoints/checkpoint_step<NNNNNNNN>.pt`` and the full-state ``restart_latest.pt``. With
``log_loss_every_epochs: k`` a loss-only row (``event`` = 0, no checkpoint) is written at the end of every k-th epoch.
``train_loss`` of a row is the mean over the updates since the previous row. The validation passes run under a
forked RNG with a fixed seed and restore the loader's generator state, so they never perturb the training
trajectory. ``checkpoints/checkpoint.pt`` holds the final weights; every checkpoint stores the raw and the EMA
weights and the run configuration.
EMA and LR ramp-up follow the convention of NVlabs/edm (``training/training_loop.py``): ``ema_halflife_kimg``,
``ema_rampup_ratio`` and ``lr_rampup_kimg`` come from the training section (overrides) or the method section.
"""
from __future__ import annotations
import copy
import os
import random
import time
from pathlib import Path
from typing import Any, Dict
import numpy as np
import torch
RESTART_SCHEMA = "restart_v1"
RESTART_NAME = "restart_latest.pt"
LOG_COLUMNS = "step,epoch,batch_index,event,train_loss,val_loss,val_loss_ema,train_eval_loss,train_eval_loss_ema,lr,elapsed_s"
def _ema_beta(cur_nimg: int, batch_size: int, ema_halflife_kimg: float, ema_rampup_ratio: float | None) -> float:
ema_halflife_nimg = ema_halflife_kimg * 1000
if ema_rampup_ratio is not None:
ema_halflife_nimg = min(ema_halflife_nimg, cur_nimg * ema_rampup_ratio)
return 0.5 ** (batch_size / max(ema_halflife_nimg, 1e-8))
def _set_rampup_lr(optimizer: torch.optim.Optimizer, base_lr: float, cur_nimg: int, lr_rampup_kimg: float) -> None:
factor = min(cur_nimg / max(lr_rampup_kimg * 1000, 1e-8), 1.0)
for g in optimizer.param_groups:
g["lr"] = base_lr * factor
def _eval_loss(model, loader, method, device, max_batches=None) -> float:
losses = []
with torch.no_grad():
for i, batch in enumerate(loader):
if max_batches is not None and i >= max_batches:
break
batch = tuple(t.to(device) for t in batch)
losses.append(method.loss(model, batch).item())
return float(np.mean(losses)) if losses else float("nan")
def _save_checkpoint(path: Path, model, ema_model, cfg: Dict[str, Any], epoch: int, step: int) -> None:
payload = {"model_state_dict": model.state_dict(), "epoch": epoch, "config": cfg, "step": int(step)}
if ema_model is not None:
payload["ema_state_dict"] = ema_model.state_dict()
torch.save(payload, path)
def _save_restart(path: Path, payload: Dict[str, Any]) -> None:
tmp = path.with_name(path.name + ".tmp") # atomic: a job killed mid-save leaves no truncated file
torch.save(payload, tmp)
os.replace(tmp, path)
def _truncate_log(log_path: Path, keep_step: int) -> int:
"""Drop the rows after ``keep_step`` (and a partial final line)."""
lines = log_path.read_text().splitlines()
header, rows = lines[0], lines[1:]
kept = []
for line in rows:
try:
if int(line.split(",", 1)[0]) <= keep_step:
kept.append(line)
except ValueError:
continue
log_path.write_text("\n".join([header] + kept) + "\n")
return len(rows) - len(kept)
class Trainer:
def __init__(self, model, method, train_loader, val_loader, cfg: Dict[str, Any], run_dir: Path, device: str):
self.model = model
self.method = method
self.train_loader = train_loader
self.val_loader = val_loader
self.cfg = cfg
self.run_dir = Path(run_dir)
self.device = device
tr, m = cfg["training"], cfg["method"]
allow_tf32 = tr.get("allow_tf32")
if allow_tf32 is not None: # one arithmetic class on every GPU (False = strict FP32)
torch.backends.cuda.matmul.allow_tf32 = bool(allow_tf32)
torch.backends.cudnn.allow_tf32 = bool(allow_tf32)
print(f"precision: allow_tf32={bool(allow_tf32)} (matmul + cudnn)")
if bool(tr.get("deterministic", False)): # a resumed run is bit-identical to an uninterrupted one
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.use_deterministic_algorithms(True)
print("determinism: use_deterministic_algorithms(True), cudnn.deterministic, benchmark off, "
"CUBLAS_WORKSPACE_CONFIG=" + os.environ["CUBLAS_WORKSPACE_CONFIG"])
self.batch_size = int(tr["batch_size"])
if "lr" in tr:
self.base_lr = float(tr["lr"])
elif "lr" in m:
self.base_lr = float(m["lr"])
else:
raise KeyError("'lr' must be set in the training or the method section")
self.val_seed = int(tr.get("val_seed", 777))
self.restart_checkpointing = bool(tr.get("restart_checkpointing", True))
if tr.get("max_steps") is None or tr.get("val_every_updates") is None:
raise KeyError("training.max_steps and training.val_every_updates are required")
self.max_steps = int(tr["max_steps"])
self.val_every_updates = int(tr["val_every_updates"])
if self.max_steps <= 0 or self.val_every_updates <= 0:
raise ValueError("max_steps and val_every_updates must be positive")
self.val_extra_steps = tr.get("val_extra_steps")
if self.val_extra_steps is not None:
if not isinstance(self.val_extra_steps, (list, tuple)) or \
any(isinstance(s, bool) or int(s) != s for s in self.val_extra_steps):
raise ValueError("training.val_extra_steps must be a list of integers")
xs = sorted({int(s) for s in self.val_extra_steps})
bad = [s for s in xs if s < 0 or s >= self.max_steps]
if bad:
raise ValueError(f"training.val_extra_steps must lie in [0, max_steps): {bad}")
self.val_extra_steps = xs
self.log_loss_every_epochs = tr.get("log_loss_every_epochs")
if self.log_loss_every_epochs is not None:
k = self.log_loss_every_epochs
if isinstance(k, bool) or int(k) != k or int(k) <= 0:
raise ValueError("training.log_loss_every_epochs must be a positive integer")
self.log_loss_every_epochs = int(k)
def _opt(key, default=None): # training section overrides the method section
return tr[key] if key in tr else m.get(key, default)
self.ema_halflife_kimg = _opt("ema_halflife_kimg") # None: no EMA
self.ema_rampup_ratio = _opt("ema_rampup_ratio", 0.05)
self.lr_rampup_kimg = _opt("lr_rampup_kimg") # None: no LR ramp
self.max_grad_norm = _opt("max_grad_norm") # None: no clipping
self.weight_decay = _opt("weight_decay")
self.use_ema = self.ema_halflife_kimg is not None and float(self.ema_halflife_kimg) > 0
self.use_lr_rampup = self.lr_rampup_kimg is not None and float(self.lr_rampup_kimg) > 0
def fit(self, resume: Dict[str, Any] | None = None) -> None:
self.run_dir.mkdir(parents=True, exist_ok=True)
ckpt_dir = self.run_dir / "checkpoints"
ckpt_dir.mkdir(parents=True, exist_ok=True)
model = self.model.to(self.device)
if self.use_ema:
ema_model = copy.deepcopy(model).eval().to(self.device)
for p in ema_model.parameters():
p.requires_grad_(False)
else:
ema_model = None
optimizer = self.method.configure_optimizer(model, {"lr": self.base_lr, "weight_decay": self.weight_decay})
start_epoch = 1
if resume is not None:
if resume.get("schema") != RESTART_SCHEMA:
raise AssertionError(f"restart checkpoint schema {resume.get('schema')!r} != {RESTART_SCHEMA}")
model.load_state_dict(resume["model_state_dict"])
if ema_model is not None:
if resume.get("ema_state_dict") is None:
raise AssertionError("EMA is on but the restart checkpoint has no EMA state")
ema_model.load_state_dict(resume["ema_state_dict"])
optimizer.load_state_dict(resume["optimizer_state_dict"])
start_epoch = int(resume["epoch"]) # the interrupted epoch is replayed
n_train_pairs = len(self.train_loader.dataset)
steps_per_epoch = len(self.train_loader)
n_epochs = max(1, -(-self.max_steps // steps_per_epoch))
print(f"schedule: max_steps = {self.max_steps}, validation + checkpoint every {self.val_every_updates} updates "
f"(+ at max_steps); {n_train_pairs} training pairs, batch {self.batch_size}, {steps_per_epoch} steps/epoch -> "
f"{n_epochs} epochs (last one partial: {self.max_steps - (n_epochs - 1) * steps_per_epoch} steps)")
if self.use_ema:
seen_kimg = (self.max_steps - 1) * self.batch_size / 1000.0
eff_kimg = min(float(self.ema_halflife_kimg), float(self.ema_rampup_ratio) * seen_kimg)
print(f"EMA: half-life {self.ema_halflife_kimg} kimg, ramp-up ratio {self.ema_rampup_ratio} "
f"(effective half-life at the last update ~= {eff_kimg:.1f} kimg)")
else:
print("EMA: disabled")
print(f"LR ramp-up: {self.lr_rampup_kimg if self.use_lr_rampup else 'disabled'}; "
f"gradient clipping: {self.max_grad_norm if self.max_grad_norm is not None else 'disabled'}")
log_path = self.run_dir / "train_log.csv"
if resume is not None:
if int(resume["step"]) >= self.max_steps:
raise AssertionError(f"resume step {int(resume['step'])} >= max_steps {self.max_steps} (nothing to continue)")
if not log_path.exists():
raise FileNotFoundError(f"--resume requires the existing {log_path}")
dropped = _truncate_log(log_path, int(resume["step"]))
print(f"resume: continuing at step {int(resume['step'])} (epoch {start_epoch}, batch "
f"{int(resume['batch_index'])}; {dropped} log row(s) dropped)")
else:
log_path.write_text(LOG_COLUMNS + "\n")
cur_nimg = 0
if resume is not None: # RNG streams restored last, after every setup step
cur_nimg = int(resume["cur_nimg"])
torch.set_rng_state(resume["torch_rng_state"])
if str(self.device).startswith("cuda"):
torch.cuda.set_rng_state_all(resume["cuda_rng_state_all"])
np.random.set_state(resume["numpy_rng_state"])
random.setstate(resume["python_rng_state"])
gen = self.train_loader.generator
if resume.get("loader_generator_state_epoch_start") is not None:
if gen is None:
raise AssertionError("restart checkpoint has a loader generator state but the train loader has no generator")
gen.set_state(resume["loader_generator_state_epoch_start"])
print(f"resume: restored optimizer, EMA, counters (cur_nimg={cur_nimg}), RNG streams and loader generator", flush=True)
self._fit_updates(model=model, ema_model=ema_model, optimizer=optimizer, ckpt_dir=ckpt_dir, log_path=log_path,
resume=resume, start_epoch=start_epoch, cur_nimg=cur_nimg, n_val_batches=len(self.val_loader))
def _fit_updates(self, *, model, ema_model, optimizer, ckpt_dir, log_path, resume, start_epoch, cur_nimg, n_val_batches):
max_steps, every = self.max_steps, self.val_every_updates
extra_steps = set(self.val_extra_steps or [])
loss_every = int(self.log_loss_every_epochs) if self.log_loss_every_epochs else 0
devices_all = [self.device] if str(self.device).startswith("cuda") else []
losses_since_epoch_start: list[float] = []
last_event_step = -1
t_row = time.time()
if resume is not None:
losses_since_epoch_start = [float(x) for x in resume.get("losses_since_epoch_start", [])]
last_event_step = int(resume["step"])
step, start_batch = 0, 0
if resume is not None:
step, start_batch = int(resume["step"]), int(resume["batch_index"])
epoch = start_epoch
losses_since_event: list[float] = []
t_event = time.time()
done = False
gen = self.train_loader.generator
def _event(epoch_i: int, batch_i: int, gen_state_epoch_start):
nonlocal losses_since_event, t_event, last_event_step, t_row
train_loss = float(np.mean(losses_since_event)) if losses_since_event else float("nan")
cur_lr = optimizer.param_groups[0]["lr"]
model.eval()
g_state = gen.get_state() if gen is not None else None
with torch.random.fork_rng(devices=devices_all):
torch.manual_seed(self.val_seed)
val_loss = _eval_loss(model, self.val_loader, self.method, self.device)
train_eval = _eval_loss(model, self.train_loader, self.method, self.device, max_batches=n_val_batches)
if self.use_ema:
val_loss_ema = _eval_loss(ema_model, self.val_loader, self.method, self.device)
train_eval_ema = _eval_loss(ema_model, self.train_loader, self.method, self.device, max_batches=n_val_batches)
else:
val_loss_ema = train_eval_ema = float("nan")
if g_state is not None:
gen.set_state(g_state)
elapsed = time.time() - t_event
t_event = t_row = time.time()
last_event_step = step
print(f"step {step} (epoch {epoch_i}, batch {batch_i}): train_loss={train_loss:.6f} val={val_loss:.6f} "
f"val_ema={val_loss_ema:.6f} lr={cur_lr:.2e} elapsed={elapsed:.2f}s", flush=True)
with open(log_path, "a") as f:
f.write(f"{step},{epoch_i},{batch_i},1,{train_loss},{val_loss},{val_loss_ema},"
f"{train_eval},{train_eval_ema},{cur_lr},{elapsed}\n")
losses_since_event = []
ckpt = ckpt_dir / f"checkpoint_step{step:08d}.pt"
_save_checkpoint(ckpt, model, ema_model, self.cfg, epoch_i, step=step)
print(f" saved: {ckpt.name}", flush=True)
if self.restart_checkpointing:
_save_restart(ckpt_dir / RESTART_NAME, {
"schema": RESTART_SCHEMA, "epoch": epoch_i, "step": step, "batch_index": batch_i, "cur_nimg": cur_nimg,
"model_state_dict": model.state_dict(),
"ema_state_dict": (ema_model.state_dict() if ema_model is not None else None),
"optimizer_state_dict": optimizer.state_dict(),
"torch_rng_state": torch.get_rng_state(),
"cuda_rng_state_all": (torch.cuda.get_rng_state_all() if str(self.device).startswith("cuda") else []),
"numpy_rng_state": np.random.get_state(), "python_rng_state": random.getstate(),
"loader_generator_state_epoch_start": gen_state_epoch_start,
"losses_since_epoch_start": list(losses_since_epoch_start),
"config": self.cfg})
model.train()
def _loss_row(epoch_i: int, batch_i: int):
nonlocal losses_since_epoch_start, t_row
if not losses_since_epoch_start or step == last_event_step:
losses_since_epoch_start = []
return # nothing to log, or an event just logged this step
train_loss = float(np.mean(losses_since_epoch_start))
cur_lr = optimizer.param_groups[0]["lr"]
model.eval()
g_state = gen.get_state() if gen is not None else None
with torch.random.fork_rng(devices=devices_all):
torch.manual_seed(self.val_seed)
val_loss = _eval_loss(model, self.val_loader, self.method, self.device)
val_loss_ema = (_eval_loss(ema_model, self.val_loader, self.method, self.device) if self.use_ema else float("nan"))
if g_state is not None:
gen.set_state(g_state)
model.train()
nan = float("nan")
elapsed = time.time() - t_row
t_row = time.time()
with open(log_path, "a") as f:
f.write(f"{step},{epoch_i},{batch_i},0,{train_loss},{val_loss},{val_loss_ema},{nan},{nan},{cur_lr},{elapsed}\n")
losses_since_epoch_start = []
if 0 in extra_steps and resume is None and step == 0:
# the untrained model: its train_loss is a forward-only loss on the first batch; RNG and loader untouched
g_state = gen.get_state() if gen is not None else None
model.eval()
with torch.random.fork_rng(devices=devices_all):
torch.manual_seed(self.val_seed)
with torch.no_grad():
b0 = tuple(t.to(self.device) for t in next(iter(self.train_loader)))
loss0 = float(self.method.loss(model, b0).item())
model.train()
if g_state is not None:
gen.set_state(g_state)
losses_since_event = [loss0]
_event(epoch, 0, gen.get_state() if gen is not None else None)
while not done:
gen_state_epoch_start = gen.get_state() if gen is not None else None
replay = resume is not None and epoch == start_epoch and start_batch > 0
if replay: # the interrupted epoch: same permutation, consumed batches skipped
gen_state_epoch_start = resume["loader_generator_state_epoch_start"]
model.train()
if not replay:
losses_since_epoch_start = []
it = iter(self.train_loader)
skip = start_batch if (resume is not None and epoch == start_epoch) else 0
for _ in range(skip):
next(it)
batch_i = skip
for batch in it:
batch = tuple(t.to(self.device) for t in batch)
if self.use_lr_rampup:
_set_rampup_lr(optimizer, self.base_lr, cur_nimg, float(self.lr_rampup_kimg))
optimizer.zero_grad()
loss = self.method.loss(model, batch)
loss.backward()
if self.max_grad_norm is not None:
torch.nn.utils.clip_grad_norm_(model.parameters(), float(self.max_grad_norm))
optimizer.step()
if self.use_ema:
beta = _ema_beta(cur_nimg, self.batch_size, float(self.ema_halflife_kimg), self.ema_rampup_ratio)
for p_ema, p_net in zip(ema_model.parameters(), model.parameters()):
p_ema.copy_(p_net.detach().lerp(p_ema, beta))
for b_ema, b_net in zip(ema_model.buffers(), model.buffers()):
b_ema.copy_(b_net)
cur_nimg += batch[0].shape[0]
_li = loss.item()
losses_since_event.append(_li)
losses_since_epoch_start.append(_li)
step += 1
batch_i += 1
if step % every == 0 or step == max_steps or step in extra_steps:
_event(epoch, batch_i, gen_state_epoch_start)
if step >= max_steps:
done = True
break
if not done and loss_every and epoch % loss_every == 0:
_loss_row(epoch, batch_i)
epoch += 1
print(f"trained exactly {max_steps} updates", flush=True)
_save_checkpoint(ckpt_dir / "checkpoint.pt", model, ema_model, self.cfg, epoch - 1, step=max_steps)