mtr-dscwg
Flask app with models
6d86da0
Raw History Blame Contribute Delete
5.9 kB
"""Generic fine-tuning loop: AdamW with split encoder/head LRs, linear warmup,
bf16/fp16 autocast on CUDA, grad accumulation, early stopping on a dev metric."""
from __future__ import annotations
import json
import logging
import math
import time
from pathlib import Path
from typing import Callable
import torch
from torch import nn
from torch.utils.data import DataLoader
from transformers import get_linear_schedule_with_warmup
from ..utils import autocast
log = logging.getLogger(__name__)
NO_DECAY = ("bias", "LayerNorm.weight", "layernorm.weight", "layer_norm.weight", "norm.weight")
def param_groups(model: nn.Module, lr: float, head_lr: float, weight_decay: float) -> list[dict]:
groups = {(e, d): [] for e in (True, False) for d in (True, False)}
for name, p in model.named_parameters():
if not p.requires_grad:
continue
is_encoder = name.startswith("encoder.")
decay = not any(k in name for k in NO_DECAY)
groups[(is_encoder, decay)].append(p)
out = []
for (is_encoder, decay), params in groups.items():
if params:
out.append({"params": params, "lr": lr if is_encoder else head_lr,
"weight_decay": weight_decay if decay else 0.0})
return out
def _nonfinite_report(model: nn.Module) -> str:
bad = [n for n, p in model.named_parameters() if not torch.isfinite(p).all()]
return f"{len(bad)} weight tensors non-finite, e.g. {bad[:3]}" if bad else "all weights finite"
def fit(model: nn.Module, dataset, collate: Callable, hp, device: torch.device,
amp_dtype: torch.dtype | None, evaluate: Callable[[], dict], metric: str,
save: Callable[[dict], None], log_path: Path, seed: int, log_every: int = 50) -> dict:
"""Train; after each epoch call `evaluate()`, keep the best by `metric` via `save()`."""
generator = torch.Generator().manual_seed(seed)
loader = DataLoader(dataset, batch_size=hp.batch_size, shuffle=True, collate_fn=collate,
generator=generator, num_workers=0) # 0 workers: safest on Windows
optimizer = torch.optim.AdamW(param_groups(model, hp.lr, hp.head_lr, hp.weight_decay))
steps_per_epoch = math.ceil(len(loader) / hp.grad_accum)
total = steps_per_epoch * hp.epochs
scheduler = get_linear_schedule_with_warmup(optimizer, int(hp.warmup_ratio * total), total)
scaler = torch.amp.GradScaler("cuda", enabled=amp_dtype == torch.float16)
best, best_metrics, bad_epochs, skipped = -math.inf, {}, 0, 0
log_path.parent.mkdir(parents=True, exist_ok=True)
with open(log_path, "a", encoding="utf-8", newline="\n") as logf:
for epoch in range(1, hp.epochs + 1):
model.train()
t0, running, n = time.time(), 0.0, 0
optimizer.zero_grad(set_to_none=True)
for step, batch in enumerate(loader, 1):
batch = {k: v.to(device, non_blocking=True) for k, v in batch.items()}
with autocast(device, amp_dtype):
loss = model(**batch)["loss"]
if not torch.isfinite(loss):
raise FloatingPointError(
f"Non-finite loss at epoch {epoch} step {step} "
f"({_nonfinite_report(model)}). If weights are fine, the forward pass "
f"overflowed: try --set runtime.precision=fp32.")
scaler.scale(loss / hp.grad_accum).backward()
running += loss.item()
n += 1
if step % hp.grad_accum == 0 or step == len(loader):
scaler.unscale_(optimizer)
norm = nn.utils.clip_grad_norm_(model.parameters(), hp.max_grad_norm)
if not torch.isfinite(norm) and amp_dtype != torch.float16:
# fp16's GradScaler skips these itself; for bf16/fp32 we must, or the
# NaN gets written into the weights and surfaces a step later.
skipped += 1
bad = [name for name, p in model.named_parameters()
if p.grad is not None and not torch.isfinite(p.grad).all()]
log.warning("non-finite gradients at epoch %d step %d, update skipped "
"(%d params, e.g. %s)", epoch, step, len(bad), bad[:3])
optimizer.zero_grad(set_to_none=True)
if skipped > hp.max_skipped_steps:
raise FloatingPointError(
f"{skipped} updates skipped for non-finite gradients; first bad "
f"params: {bad[:5]}. Try --set runtime.precision=fp32.")
continue
scaler.step(optimizer)
scaler.update()
scheduler.step()
optimizer.zero_grad(set_to_none=True)
if step % log_every == 0:
log.info("epoch %d step %d/%d loss %.4f", epoch, step, len(loader), running / n)
metrics = evaluate()
metrics.update(epoch=epoch, train_loss=running / max(n, 1), epoch_seconds=time.time() - t0)
logf.write(json.dumps(metrics) + "\n")
logf.flush()
improved = metrics[metric] > best
log.info("epoch %d %s=%.4f%s (%.0fs)", epoch, metric, metrics[metric],
" *best*" if improved else "", metrics["epoch_seconds"])
if improved:
best, best_metrics, bad_epochs = metrics[metric], dict(metrics), 0
save(best_metrics)
else:
bad_epochs += 1
if bad_epochs >= hp.patience:
log.info("early stop: no %s improvement for %d epochs", metric, hp.patience)
break
return best_metrics