Download src/nida/train/optim.py from Bilal7Dev/nida-tts: direct link, hf CLI and curl.
- Browser
- Download file 6.91 kB
-
https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/train/optim.py
- Command line
-
hf download hf://Bilal7Dev/nida-tts/src/nida/train/optim.py
-
curl -L -o optim.py https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/train/optim.py
6.91 kB
| """Optimizer, learning-rate schedule and EMA of weights.""" | |
| from __future__ import annotations | |
| import math | |
| from collections.abc import Collection, Iterator | |
| from contextlib import contextmanager | |
| import torch | |
| from torch import Tensor, nn | |
| from torch.optim.lr_scheduler import LambdaLR | |
| from nida.train.config import TrainConfig | |
| __all__ = ["EMA", "build_optimizer", "build_scheduler", "lr_factor"] | |
| def lr_factor(step: int, *, warmup_steps: int, total_steps: int, min_lr_ratio: float) -> float: | |
| """Multiplier of the peak learning rate for the optimizer step number ``step`` (0-based). | |
| Linear warmup ``(step + 1) / warmup_steps`` for ``step < warmup_steps`` (so the very | |
| first step already has a non-zero rate and step ``warmup_steps - 1`` reaches 1.0), then | |
| cosine decay from 1.0 at ``step == warmup_steps`` to ``min_lr_ratio`` at | |
| ``step == total_steps``; constant ``min_lr_ratio`` afterwards. | |
| """ | |
| if step < 0: | |
| raise ValueError("step must be >= 0") | |
| if step < warmup_steps: | |
| return (step + 1) / warmup_steps | |
| decay_steps = total_steps - warmup_steps | |
| progress = 1.0 if decay_steps <= 0 else min(1.0, (step - warmup_steps) / decay_steps) | |
| cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) | |
| return min_lr_ratio + (1.0 - min_lr_ratio) * cosine | |
| def build_optimizer(model: nn.Module, cfg: TrainConfig) -> torch.optim.AdamW: | |
| """AdamW with weight decay on weight matrices (``ndim >= 2``) only. | |
| Biases, norm scales and embedding tables are exempt (decaying a lookup table shrinks | |
| the rows of rare tokens/classes towards zero between their rare updates). | |
| """ | |
| embedding_ids = { | |
| id(module.weight) for module in model.modules() if isinstance(module, nn.Embedding) | |
| } | |
| decay: list[nn.Parameter] = [] | |
| no_decay: list[nn.Parameter] = [] | |
| for _, param in model.named_parameters(): | |
| if param.requires_grad: | |
| exempt = param.ndim < 2 or id(param) in embedding_ids | |
| (no_decay if exempt else decay).append(param) | |
| o = cfg.optim | |
| groups = [ | |
| {"params": decay, "weight_decay": o.weight_decay}, | |
| {"params": no_decay, "weight_decay": 0.0}, | |
| ] | |
| return torch.optim.AdamW(groups, lr=o.lr, betas=o.betas, eps=o.eps) | |
| def build_scheduler(optimizer: torch.optim.Optimizer, cfg: TrainConfig) -> LambdaLR: | |
| """``LambdaLR`` over :func:`lr_factor`; call ``scheduler.step()`` once per optimizer step.""" | |
| warmup, total, ratio = cfg.schedule.warmup_steps, cfg.steps, cfg.schedule.min_lr_ratio | |
| def factor(step: int) -> float: | |
| return lr_factor(step, warmup_steps=warmup, total_steps=total, min_lr_ratio=ratio) | |
| return LambdaLR(optimizer, factor) | |
| class EMA: | |
| """Exponential moving average of a model's weights. | |
| ``shadow = decay_n * shadow + (1 - decay_n) * param`` after every optimizer step, where | |
| ``decay_n = decay`` or, with ``warmup``, ``min(decay, (1 + n) / (10 + n))`` for the | |
| ``n``-th update (0-based) -- a short memory early on so the average is useful before | |
| ``1 / (1 - decay)`` steps have passed. Buffers are copied, not averaged. The shadow is | |
| stored in float32 on the model's device. | |
| """ | |
| def __init__(self, model: nn.Module, *, decay: float, warmup: bool) -> None: | |
| if not 0.0 <= decay < 1.0: | |
| raise ValueError("decay must be in [0, 1)") | |
| self.decay = decay | |
| self.warmup = warmup | |
| self.num_updates = 0 | |
| self.shadow: dict[str, Tensor] = { | |
| name: tensor.detach().clone().float() for name, tensor in _state(model).items() | |
| } | |
| def current_decay(self) -> float: | |
| """Decay that the *next* :meth:`update` will use.""" | |
| if not self.warmup: | |
| return self.decay | |
| n = self.num_updates | |
| return min(self.decay, (1.0 + n) / (10.0 + n)) | |
| def update(self, model: nn.Module) -> None: | |
| decay = self.current_decay() | |
| params = dict(model.named_parameters()) | |
| for name, tensor in _state(model).items(): | |
| shadow = self.shadow[name] | |
| if name in params: | |
| shadow.mul_(decay).add_(tensor.detach().float(), alpha=1.0 - decay) | |
| else: | |
| shadow.copy_(tensor.detach().float()) | |
| self.num_updates += 1 | |
| def copy_to(self, model: nn.Module) -> None: | |
| """Overwrite ``model``'s weights with the averaged ones.""" | |
| for name, tensor in _state(model).items(): | |
| tensor.copy_(self.shadow[name].to(tensor.dtype)) | |
| def swapped_in(self, model: nn.Module) -> Iterator[None]: | |
| """Temporarily load the EMA weights into ``model`` (restored on exit, also on error).""" | |
| backup = {name: t.detach().clone() for name, t in _state(model).items()} | |
| self.copy_to(model) | |
| try: | |
| yield | |
| finally: | |
| with torch.no_grad(): | |
| for name, tensor in _state(model).items(): | |
| tensor.copy_(backup[name]) | |
| def state_dict(self) -> dict[str, object]: | |
| return { | |
| "decay": self.decay, | |
| "warmup": self.warmup, | |
| "num_updates": self.num_updates, | |
| "shadow": {k: v.clone() for k, v in self.shadow.items()}, | |
| } | |
| def load_state_dict( | |
| self, state: dict[str, object], *, allow_missing: Collection[str] = () | |
| ) -> None: | |
| """Load ``state``; its names must be exactly this EMA's names. | |
| ``allow_missing`` names tensors that ``state`` must NOT contain (a fine-tune that | |
| adds parameters, :meth:`nida.train.loop.Trainer.init_from`): they keep their current | |
| shadow value. The names missing from ``state`` have to be exactly this set. | |
| Raises: | |
| ValueError: names, shapes or ``num_updates`` do not match. | |
| """ | |
| shadow = state["shadow"] | |
| allowed = set(allow_missing) | |
| if not allowed <= set(self.shadow): | |
| raise ValueError( | |
| f"allow_missing names unknown to the EMA: {sorted(allowed - set(self.shadow))}" | |
| ) | |
| if not isinstance(shadow, dict) or set(shadow) != set(self.shadow) - allowed: | |
| raise ValueError("EMA state does not match the model's parameter names") | |
| for name, tensor in shadow.items(): | |
| if tensor.shape != self.shadow[name].shape: | |
| raise ValueError(f"EMA tensor {name!r} has shape {tuple(tensor.shape)}") | |
| self.shadow[name].copy_(tensor) | |
| num_updates = state["num_updates"] | |
| if not isinstance(num_updates, int): | |
| raise ValueError("EMA num_updates must be an int") | |
| self.num_updates = num_updates | |
| def _state(model: nn.Module) -> dict[str, Tensor]: | |
| """Parameters and buffers by name, in a deterministic order.""" | |
| out: dict[str, Tensor] = dict(model.named_parameters()) | |
| out.update(model.named_buffers()) | |
| return out | |