| |
|
|
| """Single-step train / eval primitives.""" |
|
|
| from __future__ import annotations |
|
|
| import contextlib |
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
| import torch |
| import torch.nn as nn |
|
|
| from Model.config import IGNORE_INDEX, TrainingConfig |
| from Model.training.optim import recurrent_steps_for_step |
|
|
|
|
| @dataclass |
| class TrainState: |
| step: int = 0 |
| tokens_seen: int = 0 |
| last_loss: float = float("nan") |
| extra: dict[str, Any] = field(default_factory=dict) |
|
|
|
|
| def _autocast_ctx(precision: str, device_type: str): |
| if precision == "fp32": |
| return contextlib.nullcontext() |
| dtype = torch.bfloat16 if precision == "bf16" else torch.float16 |
| return torch.autocast(device_type=device_type, dtype=dtype) |
|
|
|
|
| def _new_cuda_grad_scaler(): |
| """Build a CUDA ``GradScaler`` using the non-deprecated API when available.""" |
|
|
| try: |
| return torch.amp.GradScaler("cuda") |
| except (AttributeError, TypeError): |
| return torch.cuda.amp.GradScaler() |
|
|
|
|
| def _grad_scaler_for(model: nn.Module, cfg: TrainingConfig, device_type: str): |
| """Return a fp16 ``GradScaler`` (sharded under FSDP) or ``None``. |
| |
| fp16 autocast without loss scaling silently underflows small gradients to |
| zero and can diverge to NaN. bf16/fp32 have enough exponent range and need |
| no scaler. CPU has no fp16 GradScaler support, so we leave it unscaled. |
| """ |
|
|
| if cfg.precision != "fp16" or device_type != "cuda": |
| return None |
| |
| |
| if hasattr(model, "clip_grad_norm_"): |
| try: |
| from torch.distributed.fsdp.sharded_grad_scaler import ( |
| ShardedGradScaler, |
| ) |
|
|
| return ShardedGradScaler() |
| except Exception: |
| return _new_cuda_grad_scaler() |
| return _new_cuda_grad_scaler() |
|
|
|
|
| def _to_device(batch: dict[str, Any], device: torch.device) -> dict[str, Any]: |
| out: dict[str, Any] = {} |
| for k, v in batch.items(): |
| if isinstance(v, torch.Tensor): |
| out[k] = v.to(device, non_blocking=True) |
| elif isinstance(v, dict): |
| |
| |
| |
| out[k] = { |
| kk: vv.to(device, non_blocking=True) if isinstance(vv, torch.Tensor) else vv |
| for kk, vv in v.items() |
| } |
| else: |
| out[k] = v |
| return out |
|
|
|
|
| def train_one_step( |
| model: nn.Module, |
| batch_iter, |
| optimizer: torch.optim.Optimizer, |
| scheduler: torch.optim.lr_scheduler._LRScheduler, |
| cfg: TrainingConfig, |
| state: TrainState, |
| *, |
| device: torch.device, |
| target_recurrent_steps: int | None = None, |
| ) -> dict[str, float]: |
| """Run one optimizer step (covers ``grad_accum_steps`` micro batches).""" |
|
|
| model.train() |
| optimizer.zero_grad(set_to_none=True) |
|
|
| device_type = device.type if device.type in ("cuda", "cpu") else "cpu" |
| loss_terms: list[torch.Tensor] = [] |
| token_count = 0 |
|
|
| |
| |
| scaler = state.extra.get("grad_scaler") |
| if scaler is None and cfg.precision == "fp16": |
| scaler = _grad_scaler_for(model, cfg, device_type) |
| if scaler is not None: |
| pending = state.extra.pop("grad_scaler_state", None) |
| if pending is not None: |
| scaler.load_state_dict(pending) |
| state.extra["grad_scaler"] = scaler |
|
|
| rec_steps = None |
| if target_recurrent_steps is not None: |
| rec_steps = recurrent_steps_for_step(state.step, cfg, target_recurrent_steps) |
|
|
| for _ in range(cfg.grad_accum_steps): |
| batch = next(batch_iter) |
| |
| |
| cpu_mask = batch.get("attention_mask") |
| if isinstance(cpu_mask, torch.Tensor): |
| token_count += int(cpu_mask.sum().item()) |
| batch = _to_device(batch, device) |
|
|
| with _autocast_ctx(cfg.precision, device_type): |
| out = model( |
| input_ids=batch["input_ids"], |
| attention_mask=batch.get("attention_mask"), |
| labels=batch["labels"], |
| word_pos=batch.get("word_pos"), |
| morph_depth=batch.get("morph_depth"), |
| pixel_values=batch.get("pixel_values"), |
| steps=rec_steps, |
| bptt_window=cfg.bptt_window, |
| return_logits=not cfg.use_loss_chunking, |
| ) |
| loss = out["loss"] / cfg.grad_accum_steps |
| if scaler is not None: |
| scaler.scale(loss).backward() |
| else: |
| loss.backward() |
|
|
| |
| |
| |
| loss_terms.append(loss.detach()) |
|
|
| if scaler is not None: |
| |
| scaler.unscale_(optimizer) |
|
|
| if cfg.grad_clip and cfg.grad_clip > 0: |
| if hasattr(model, "clip_grad_norm_"): |
| grad_norm = model.clip_grad_norm_(cfg.grad_clip) |
| else: |
| grad_norm = torch.nn.utils.clip_grad_norm_( |
| model.parameters(), cfg.grad_clip |
| ) |
| grad_norm_val = float(grad_norm) |
| else: |
| grad_norm_val = float("nan") |
|
|
| if scaler is not None: |
| |
| |
| |
| |
| |
| scale_before = scaler.get_scale() |
| scaler.step(optimizer) |
| scaler.update() |
| stepped = scaler.get_scale() >= scale_before |
| else: |
| optimizer.step() |
| stepped = True |
| if stepped: |
| scheduler.step() |
|
|
| if loss_terms: |
| |
| loss_sum = torch.stack(loss_terms).sum().item() * cfg.grad_accum_steps |
| else: |
| loss_sum = 0.0 |
|
|
| state.step += 1 |
| state.tokens_seen += token_count |
| state.last_loss = loss_sum / max(1, cfg.grad_accum_steps) |
| return { |
| "loss": state.last_loss, |
| "grad_norm": grad_norm_val, |
| "lr": float(scheduler.get_last_lr()[0]), |
| "tokens": float(token_count), |
| } |
|
|
|
|
| @torch.no_grad() |
| def evaluate( |
| model: nn.Module, |
| batches, |
| cfg: TrainingConfig, |
| *, |
| device: torch.device, |
| max_batches: int = 32, |
| ) -> dict[str, float]: |
| model.eval() |
| device_type = device.type if device.type in ("cuda", "cpu") else "cpu" |
| |
| |
| |
| |
| |
| core = model |
| while hasattr(core, "module"): |
| core = core.module |
| ignore_index = getattr(getattr(core, "cfg", None), "ignore_index", IGNORE_INDEX) |
|
|
| total_loss = 0.0 |
| total_targets = 0 |
| seen = 0 |
| for batch in batches: |
| if seen >= max_batches: |
| break |
| |
| |
| |
| seen += 1 |
| batch = _to_device(batch, device) |
| with _autocast_ctx(cfg.precision, device_type): |
| out = model( |
| input_ids=batch["input_ids"], |
| attention_mask=batch.get("attention_mask"), |
| labels=batch["labels"], |
| word_pos=batch.get("word_pos"), |
| morph_depth=batch.get("morph_depth"), |
| pixel_values=batch.get("pixel_values"), |
| return_logits=not cfg.use_loss_chunking, |
| ) |
| |
| |
| |
| |
| |
| |
| |
| labels = batch["labels"] |
| n_targets = int((labels[:, 1:] != ignore_index).sum().item()) |
| if n_targets == 0: |
| continue |
| loss_parts = out.get("loss_parts") or {} |
| batch_loss = loss_parts.get("forward") |
| if batch_loss is None: |
| batch_loss = float(out["loss"].item()) |
| total_loss += float(batch_loss) * n_targets |
| total_targets += n_targets |
| avg = total_loss / max(1, total_targets) |
| return {"eval_loss": avg, "eval_tokens": float(total_targets)} |
|
|
|
|
| __all__ = ["TrainState", "evaluate", "train_one_step"] |
|
|