"""Optimizer: Muon for 2D weight matrices, AdamW for everything else. Routing is explicit via ``optax.multi_transform``: any 2D parameter that is *not* the token-embedding table goes to Muon (all the Linear ``kernel`` weights); embeddings, RMSNorm scales, LayerScale vectors, biases -> AdamW. Both branches follow a Warmup-Stable-Decay (WSD) schedule, which fits Kaggle's preemptible 9h sessions: hold the stable phase across many sessions, decay only at the very end. """ from __future__ import annotations from collections import defaultdict import jax import jax.numpy as jnp import optax from optax import contrib from optax.contrib import MuonDimensionNumbers from .config import TrainConfig # ---------------------------------------------------------------------------- # WSD schedule # ---------------------------------------------------------------------------- def wsd_schedule(peak_lr: float, warmup_steps: int, stable_steps: int, decay_steps: int, end_lr_frac: float = 0.0, decay_type: str = "1-sqrt") -> optax.Schedule: end_lr = peak_lr * end_lr_frac warmup = optax.linear_schedule(0.0, peak_lr, max(warmup_steps, 1)) stable = optax.constant_schedule(peak_lr) if decay_type == "linear": decay = optax.linear_schedule(peak_lr, end_lr, max(decay_steps, 1)) elif decay_type == "cosine": alpha = end_lr / peak_lr if peak_lr > 0 else 0.0 decay = optax.cosine_decay_schedule(peak_lr, max(decay_steps, 1), alpha=alpha) elif decay_type == "1-sqrt": # MiniCPM-style; sharp-then-flat d = max(decay_steps, 1) def decay(step): frac = jnp.clip(step / d, 0.0, 1.0) return end_lr + (peak_lr - end_lr) * (1.0 - jnp.sqrt(frac)) else: raise ValueError(f"unknown decay_type: {decay_type}") return optax.join_schedules([warmup, stable, decay], [warmup_steps, warmup_steps + stable_steps]) # ---------------------------------------------------------------------------- # param routing # ---------------------------------------------------------------------------- def _is_muon_leaf(path, x) -> bool: """A param goes to Muon iff it is a 2D matrix that is not the embedding table.""" key = jax.tree_util.keystr(path) return getattr(x, "ndim", 0) == 2 and "embedding" not in key def muon_dimension_numbers(params): """Routing for ``optax.contrib.muon``. Returns a prefix tree: ``MuonDimensionNumbers()`` (reduction_axis=0, output_axis=1 -> correct for ``[in, out]`` Linear kernels) for params Muon should orthogonalize, and ``None`` for params that should fall through to the AdamW branch (embeddings, RMSNorm scales, LayerScale vectors, biases). """ return jax.tree_util.tree_map_with_path( lambda p, x: MuonDimensionNumbers() if _is_muon_leaf(p, x) else None, params) def param_stats(params) -> dict: counts = defaultdict(int) def f(path, x): lbl = "muon" if _is_muon_leaf(path, x) else "adamw" counts[lbl] += int(x.size) jax.tree_util.tree_map_with_path(f, params) counts["total"] = counts["muon"] + counts["adamw"] return dict(counts) # ---------------------------------------------------------------------------- # build optimizer # ---------------------------------------------------------------------------- def build_optimizer(tcfg: TrainConfig) -> optax.GradientTransformation: """Muon (2D matmuls) + AdamW (rest), both on a WSD schedule, with grad clipping. Uses the full ``optax.contrib.muon`` so we get the Moonlight update-scale matching (``scale_by_shape``) that lets a single sane LR work; routing of the embedding table to AdamW is handled by ``muon_dimension_numbers``. """ muon_sched = wsd_schedule(tcfg.muon_lr, tcfg.warmup_steps, tcfg.stable_steps, tcfg.decay_steps, tcfg.end_lr_frac, tcfg.decay_type) adam_sched = wsd_schedule(tcfg.adam_lr, tcfg.warmup_steps, tcfg.stable_steps, tcfg.decay_steps, tcfg.end_lr_frac, tcfg.decay_type) tx_core = contrib.muon( learning_rate=muon_sched, beta=tcfg.muon_beta, weight_decay=tcfg.weight_decay, nesterov=True, adam_b1=tcfg.adam_b1, adam_b2=tcfg.adam_b2, adam_weight_decay=tcfg.weight_decay, adam_learning_rate=adam_sched, mu_dtype=jnp.dtype(tcfg.opt_mu_dtype), # bf16 first moments (Muon mu + AdamW m); v stays fp32 muon_weight_dimension_numbers=muon_dimension_numbers, ) return optax.chain(optax.clip_by_global_norm(tcfg.grad_clip), tx_core)