matilda-mini-v2 / tests /test_optim.py
prometheus04's picture
v2: 363M hero run (Muon hybrid, WSD, Liger, SmolLM 75/15/10 mix)
a3bc5bb verified
Raw
History Blame Contribute Delete
4.69 kB
"""Muon optimizer + Muon/AdamW hybrid."""
import pytest
import torch
from matilda import Transformer, ModelConfig
from matilda.optim import (
build_optimizer, cosine_warmup_scheduler, build_scheduler,
wsd_scheduler, WarmupStableDecay, Muon, HybridOptimizer,
zeropower_via_newtonschulz5,
)
def test_newtonschulz_orthogonalizes():
torch.manual_seed(0)
G = torch.randn(32, 16)
X = zeropower_via_newtonschulz5(G, steps=5).float()
# singular values should be pushed toward 1
s = torch.linalg.svdvals(X)
assert (s > 0.5).all() and (s < 1.5).all()
def test_hybrid_splits_params():
cfg = ModelConfig(vocab_size=128, max_seq_len=32, d_model=64,
n_layers=2, n_heads=4, n_kv_heads=2)
opt = build_optimizer(Transformer(cfg), name="muon")
assert isinstance(opt, HybridOptimizer)
muon, adamw = opt.optimizers
assert isinstance(muon, Muon)
# every Muon param is a 2-D matrix
assert all(p.ndim == 2 for g in muon.param_groups for p in g["params"])
@pytest.mark.slow
def test_muon_overfits_single_batch():
cfg = ModelConfig(vocab_size=256, max_seq_len=64, d_model=128,
n_layers=2, n_heads=4, n_kv_heads=2)
model = Transformer(cfg).train()
torch.manual_seed(0)
idx = torch.randint(0, cfg.vocab_size, (4, 32))
tgt = torch.randint(0, cfg.vocab_size, (4, 32))
opt = build_optimizer(model, name="muon", lr=3e-3, muon_lr=0.02)
sched = cosine_warmup_scheduler(opt, warmup_steps=10, total_steps=300)
last = None
for _ in range(300):
_, loss = model(idx, tgt)
opt.zero_grad(set_to_none=True)
loss.backward()
opt.step()
sched.step()
last = loss.item()
assert last < 0.5, f"Muon failed to overfit; final loss={last:.3f}"
def _dummy_opt(base_lr=1.0):
"""Minimal optimizer just to exercise scheduler interface."""
p = torch.nn.Parameter(torch.zeros(2, 2))
return torch.optim.SGD([p], lr=base_lr)
def test_wsd_schedule_three_phases():
"""Warmup ramps linearly to peak, stable holds peak, decay drops to min."""
base = 1.0
warmup, total, stable = 10, 100, 0.8
sched = wsd_scheduler(_dummy_opt(base), warmup_steps=warmup,
total_steps=total, stable_share=stable,
min_lr_ratio=0.1)
lrs = []
for _ in range(total):
lrs.append(sched.get_last_lr()[0])
sched.step()
# warmup: lr at step warmup-1 should equal base (peak)
assert lrs[0] == pytest.approx(base / warmup, rel=1e-6)
assert lrs[warmup - 1] == pytest.approx(base, rel=1e-6)
# stable phase: held at peak for stable_share of post-warmup
decay_start = warmup + int(stable * (total - warmup))
assert lrs[warmup] == pytest.approx(base, rel=1e-6)
assert lrs[decay_start - 1] == pytest.approx(base, rel=1e-6)
# decay: monotone non-increasing
decay_lrs = lrs[decay_start:]
assert all(decay_lrs[i] >= decay_lrs[i + 1] for i in range(len(decay_lrs) - 1))
# ends at min_lr_ratio * base (within one-step linear quantum)
assert lrs[-1] < base # has decayed
assert lrs[-1] >= 0.1 * base * 0.5 # not below floor
def test_wsd_state_dict_roundtrip():
"""Resume must restore step + base_lrs and re-apply the LR exactly."""
opt = _dummy_opt(1.0)
s1 = wsd_scheduler(opt, warmup_steps=10, total_steps=100, stable_share=0.8)
for _ in range(50):
s1.step()
sd = s1.state_dict()
s2 = wsd_scheduler(_dummy_opt(1.0), warmup_steps=10, total_steps=100,
stable_share=0.8)
s2.load_state_dict(sd)
assert s2.last_step == s1.last_step
assert s2.get_last_lr()[0] == pytest.approx(s1.get_last_lr()[0], rel=1e-9)
def test_build_scheduler_dispatch():
assert isinstance(
build_scheduler("wsd", _dummy_opt(), 10, 100), WarmupStableDecay)
cos = build_scheduler("cosine", _dummy_opt(), 10, 100)
assert hasattr(cos, "step") and hasattr(cos, "get_last_lr")
with pytest.raises(ValueError):
build_scheduler("bogus", _dummy_opt(), 10, 100)
def test_hybrid_state_dict_roundtrip():
cfg = ModelConfig(vocab_size=128, max_seq_len=32, d_model=64,
n_layers=2, n_heads=4, n_kv_heads=2)
model = Transformer(cfg)
opt = build_optimizer(model, name="muon")
# take a step so state is populated
_, loss = model(torch.randint(0, 128, (2, 16)), torch.randint(0, 128, (2, 16)))
loss.backward()
opt.step()
sd = opt.state_dict()
opt2 = build_optimizer(Transformer(cfg), name="muon")
opt2.load_state_dict(sd) # must not raise
assert len(sd["opts"]) == 2