| """ |
| Training utilities: FastText init, FORGETTER, checkpointing, optimizers, schedulers. |
| """ |
|
|
| import math |
| import copy |
| from pathlib import Path |
| from typing import Optional |
|
|
| import torch |
| import torch.nn as nn |
| from torch.optim import Adam, AdamW |
| from torch.optim.lr_scheduler import LambdaLR |
|
|
|
|
| ROOT = Path(__file__).resolve().parent.parent.parent |
|
|
|
|
| |
| |
| |
|
|
| def init_embeddings_fasttext(model, tokenizer, fasttext_path: str): |
| """ |
| Initialize model's word embeddings from pre-trained FastText vectors. |
| +2-3pp BLiMP in 2025 papers. |
| |
| Args: |
| model: model with .word_embedding or .embedding.word_embedding |
| tokenizer: HuggingFace tokenizer |
| fasttext_path: path to FastText .bin file |
| """ |
| try: |
| from gensim.models import FastText |
| except ImportError: |
| print("WARNING: gensim not installed, skipping FastText init. pip install gensim") |
| return |
|
|
| print(f"Loading FastText model from {fasttext_path}...") |
| ft_model = FastText.load(fasttext_path) |
|
|
| |
| if hasattr(model, 'word_embedding'): |
| embed = model.word_embedding |
| elif hasattr(model, 'embedding') and hasattr(model.embedding, 'word_embedding'): |
| embed = model.embedding.word_embedding |
| else: |
| print("WARNING: Could not find word_embedding layer, skipping FastText init") |
| return |
|
|
| vocab = tokenizer.get_vocab() |
| hidden_size = embed.weight.shape[1] |
| ft_dim = ft_model.wv.vector_size |
|
|
| |
| if ft_dim != hidden_size: |
| print(f" FastText dim={ft_dim} != hidden_size={hidden_size}, using linear projection") |
| proj = torch.randn(ft_dim, hidden_size) * (1.0 / math.sqrt(ft_dim)) |
| else: |
| proj = None |
|
|
| initialized = 0 |
| with torch.no_grad(): |
| for token, idx in vocab.items(): |
| |
| clean = token.replace('Ġ', ' ').replace('▁', ' ').strip() |
| if not clean: |
| continue |
| try: |
| vec = torch.tensor(ft_model.wv[clean], dtype=torch.float32) |
| if proj is not None: |
| vec = vec @ proj |
| embed.weight[idx] = vec |
| initialized += 1 |
| except KeyError: |
| pass |
|
|
| print(f" Initialized {initialized}/{len(vocab)} token embeddings from FastText") |
|
|
|
|
| |
| |
| |
|
|
| def reset_optimizer_state(optimizer): |
| """ |
| FORGETTER Sleep 1 (Light Sleep): Reset optimizer state (momentum, variance). |
| Model weights are kept. From Yamamoto & Miura 2025. |
| """ |
| for group in optimizer.param_groups: |
| for p in group['params']: |
| if p in optimizer.state: |
| optimizer.state[p] = {} |
| print(" FORGETTER Sleep 1: optimizer state reset") |
|
|
|
|
| def deep_sleep(model): |
| """ |
| FORGETTER Sleep 2 (Deep Sleep): Reinitialize all model state EXCEPT weights. |
| Saves weights, constructs fresh model, loads weights back. |
| From Yamamoto & Miura 2025 — used for the final epoch. |
| |
| Returns a new model instance with same weights but fresh buffers/state. |
| """ |
| state_dict = {k: v.clone() for k, v in model.state_dict().items() |
| if 'weight' in k or 'bias' in k} |
| |
| device = next(model.parameters()).device |
| model_class = type(model) |
| |
| print(" FORGETTER Sleep 2: deep sleep — reinitialize model, keep weights") |
| return state_dict |
|
|
|
|
| |
| |
| |
|
|
| def save_checkpoint(model, optimizer, epoch: int, step: int, loss: float, |
| save_dir: str, cfg=None): |
| """Save training checkpoint.""" |
| save_path = Path(save_dir) |
| save_path.mkdir(parents=True, exist_ok=True) |
|
|
| ckpt = { |
| 'epoch': epoch, |
| 'step': step, |
| 'loss': loss, |
| 'model_state_dict': model.state_dict(), |
| 'optimizer_state_dict': optimizer.state_dict(), |
| } |
| if cfg is not None: |
| from .config import config_to_dict |
| ckpt['config'] = config_to_dict(cfg) |
|
|
| path = save_path / f"checkpoint_epoch{epoch}.pt" |
| torch.save(ckpt, path) |
| print(f" Saved checkpoint: {path}") |
| return path |
|
|
|
|
| def load_checkpoint(model, optimizer, checkpoint_path: str): |
| """Load training checkpoint.""" |
| ckpt = torch.load(checkpoint_path, map_location='cpu') |
| model.load_state_dict(ckpt['model_state_dict']) |
| if optimizer is not None: |
| optimizer.load_state_dict(ckpt['optimizer_state_dict']) |
| print(f" Loaded checkpoint from epoch {ckpt['epoch']}, step {ckpt['step']}") |
| return ckpt['epoch'], ckpt['step'] |
|
|
|
|
| def average_checkpoints(checkpoint_paths: list, model) -> dict: |
| """ |
| Average multiple checkpoint model weights. |
| Used as final model for evaluation. |
| |
| Args: |
| checkpoint_paths: list of checkpoint file paths |
| model: model instance (for structure reference) |
| |
| Returns: |
| averaged state_dict |
| """ |
| print(f"Averaging {len(checkpoint_paths)} checkpoints...") |
|
|
| avg_state = None |
| for i, path in enumerate(checkpoint_paths): |
| ckpt = torch.load(path, map_location='cpu') |
| state = ckpt['model_state_dict'] |
|
|
| if avg_state is None: |
| avg_state = {k: v.clone().float() for k, v in state.items()} |
| else: |
| for k in avg_state: |
| avg_state[k] += state[k].float() |
|
|
| |
| n = len(checkpoint_paths) |
| for k in avg_state: |
| avg_state[k] /= n |
|
|
| model.load_state_dict(avg_state) |
| print(f" Checkpoint averaging complete") |
| return avg_state |
|
|
|
|
| |
| |
| |
|
|
| def get_cosine_schedule_with_warmup(optimizer, num_warmup_steps: int, |
| num_training_steps: int, |
| min_lr_ratio: float = 0.1, |
| num_cooldown_steps: int = 0): |
| """ |
| Cosine annealing with linear warmup and optional linear cooldown. |
| Matches official GPT-BERT scheduler (warmup 1.6%, cooldown 1.6%, min_lr=0.1). |
| """ |
|
|
| def lr_lambda(current_step): |
| |
| if current_step < num_warmup_steps: |
| return float(current_step) / float(max(1, num_warmup_steps)) |
| |
| if num_cooldown_steps > 0 and current_step > num_training_steps - num_cooldown_steps: |
| remaining = num_training_steps - current_step |
| return max(0.0, min_lr_ratio * float(remaining) / float(max(1, num_cooldown_steps))) |
| |
| progress = float(current_step - num_warmup_steps) / float( |
| max(1, num_training_steps - num_warmup_steps - num_cooldown_steps)) |
| return max(min_lr_ratio, 0.5 * (1.0 + math.cos(math.pi * progress))) |
|
|
| return LambdaLR(optimizer, lr_lambda) |
|
|
|
|
| |
| |
| |
|
|
| class _CombinedOptimizer(torch.optim.Optimizer): |
| """Wraps two optimizers (e.g., Muon for 2D + AdamW for 1D) into one. |
| |
| Inherits from Optimizer so LR schedulers accept it via isinstance() check. |
| We initialize the base class with a dummy param to satisfy the non-empty check, |
| then replace param_groups with the real ones from both sub-optimizers. |
| """ |
|
|
| def __init__(self, opt1, opt2): |
| |
| dummy = torch.nn.Parameter(torch.zeros(1), requires_grad=False) |
| super().__init__([dummy], defaults={"lr": 0.0}) |
| |
| self.param_groups = list(opt1.param_groups) |
| self.opt1 = opt1 |
| self.opt2 = opt2 |
|
|
| def step(self, closure=None): |
| |
| self.opt1.step() |
| self.opt2.step() |
|
|
| def zero_grad(self, set_to_none=False): |
| self.opt1.zero_grad(set_to_none=set_to_none) |
| self.opt2.zero_grad(set_to_none=set_to_none) |
|
|
| def state_dict(self): |
| return {"opt1": self.opt1.state_dict(), "opt2": self.opt2.state_dict()} |
|
|
| def load_state_dict(self, state_dict): |
| self.opt1.load_state_dict(state_dict["opt1"]) |
| self.opt2.load_state_dict(state_dict["opt2"]) |
|
|
|
|
| def build_optimizer(model, opt_cfg, training_cfg): |
| """Build optimizer from config.""" |
|
|
| |
| opt_cfg.type = opt_cfg.type.lower() |
|
|
| |
| no_decay = {"bias", "layer_norm"} |
| params = [ |
| { |
| "params": [p for n, p in model.named_parameters() |
| if not any(nd in n for nd in no_decay) and p.requires_grad], |
| "weight_decay": training_cfg.weight_decay, |
| }, |
| { |
| "params": [p for n, p in model.named_parameters() |
| if any(nd in n for nd in no_decay) and p.requires_grad], |
| "weight_decay": 0.0, |
| }, |
| ] |
|
|
| |
| |
| |
| |
| |
| if opt_cfg.type == "adam": |
| optimizer = Adam( |
| params, |
| lr=training_cfg.learning_rate, |
| betas=opt_cfg.betas, |
| eps=opt_cfg.eps, |
| ) |
|
|
| |
| |
| |
| |
| |
| elif opt_cfg.type == "adamw": |
| optimizer = AdamW( |
| params, |
| lr=training_cfg.learning_rate, |
| betas=opt_cfg.betas, |
| eps=opt_cfg.eps, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| elif opt_cfg.type == "lamb": |
| try: |
| from torch_optimizer import Lamb |
| optimizer = Lamb( |
| params, |
| lr=training_cfg.learning_rate, |
| betas=opt_cfg.betas, |
| eps=opt_cfg.eps, |
| ) |
| except ImportError: |
| print("WARNING: torch-optimizer not installed, falling back to AdamW") |
| optimizer = AdamW(params, lr=training_cfg.learning_rate, |
| betas=opt_cfg.betas, eps=opt_cfg.eps) |
|
|
| |
| |
| |
| |
| |
| |
| elif opt_cfg.type == "muon": |
| try: |
| from muon import Muon |
| import torch.distributed as dist |
| |
| if not dist.is_initialized(): |
| dist.init_process_group(backend="gloo", init_method="tcp://127.0.0.1:29500", |
| rank=0, world_size=1) |
| |
| |
| |
| |
| muon_params = [p for n, p in model.named_parameters() if p.requires_grad and p.ndim >= 2] |
| adamw_1d = [p for n, p in model.named_parameters() if p.requires_grad and p.ndim < 2] |
| muon_opt = Muon(muon_params, lr=training_cfg.learning_rate) |
| adamw_opt = AdamW([{"params": adamw_1d, "weight_decay": 0.0}], |
| lr=training_cfg.learning_rate * 0.1, |
| betas=opt_cfg.betas, eps=opt_cfg.eps) |
| optimizer = _CombinedOptimizer(muon_opt, adamw_opt) |
| print(f" Muon: {len(muon_params)} matrix params + {len(adamw_1d)} scalar params (AdamW)") |
| except ImportError: |
| print("WARNING: muon not installed, falling back to AdamW") |
| optimizer = AdamW(params, lr=training_cfg.learning_rate, |
| betas=opt_cfg.betas, eps=opt_cfg.eps) |
|
|
| else: |
| raise ValueError(f"Unknown optimizer type: {opt_cfg.type}") |
|
|
| return optimizer |
|
|
|
|
| |
| |
| |
|
|
| def export_for_eval(model, tokenizer, cfg, export_dir: str): |
| """ |
| Export trained model in HuggingFace format for the evaluation pipeline. |
| Creates a self-contained directory loadable via: |
| AutoModelForCausalLM.from_pretrained(path, trust_remote_code=True) |
| |
| Uses the hf_export module which handles all 5 architectures. |
| """ |
| from .config import config_to_dict |
| from .hf_export.export import export_state_dict |
|
|
| cfg_dict = config_to_dict(cfg) |
| model_cfg = cfg_dict.get("model", {}) |
| arch = model_cfg.get("arch", "gpt_bert") |
| tokenizer_dir = cfg_dict.get("data", {}).get("tokenizer_path", "models/tokenizer") |
|
|
| export_state_dict( |
| state_dict=model.state_dict(), |
| arch=arch, |
| model_cfg=model_cfg, |
| output_dir=export_dir, |
| tokenizer_dir=tokenizer_dir, |
| ) |
|
|