YCode / Code-agent-main /tests /test_lm_v5.py
Yazhiniyan's picture
Upload 190 files
5ef3875 verified
Raw History Blame Contribute Delete
8.03 kB
"""v5 training at 1.5B scale: low-memory Lion (updates inside backward), sharded (FSDP) training
with Muon, fp16 Newton-Schulz on GPUs without bf16, and autotrain's out-of-memory fallback."""
import copy
import subprocess
import sys
import pytest
torch = pytest.importorskip("torch")
pytest.importorskip("numpy")
from ycode.lm.model import GPT, PRESETS, GPTConfig # noqa: E402
from ycode.lm.optim import Lion, build_optimizer # noqa: E402
from ycode.lm.train import TrainConfig, load_checkpoint, train # noqa: E402
def _tiny(seed=0):
torch.manual_seed(seed)
return GPT(GPTConfig(vocab_size=100, **{**PRESETS["v5-tiny"], "block_size": 32}))
def test_v5_presets_sizes():
from ycode.lm.autotrain import preset_params
assert 1.45e9 < preset_params("v5-1.5b") < 1.6e9
assert 3.5e8 < preset_params("v5-385m") < 4.0e8
def test_lion_in_backward_matches_regular_step():
m1 = _tiny()
m2 = copy.deepcopy(m1)
x, y = torch.randint(0, 100, (2, 32)), torch.randint(0, 100, (2, 32))
o1 = build_optimizer(m1, kind="lion", lr=1e-3, muon_lr=0, weight_decay=0.1)
o2 = build_optimizer(m2, kind="lion", lr=1e-3, muon_lr=0, weight_decay=0.1)
o2.optimizers[0].attach()
for _ in range(3):
m1(x, y)[1].backward()
o1.step()
o1.zero_grad()
m2(x, y)[1].backward() # updates happen here
assert all(p.grad is None for p in m2.parameters()) # gradients freed as they are used
for a, b in zip(m1.parameters(), m2.parameters()):
assert torch.equal(a, b)
def test_lion_moves_each_weight_by_lr_and_keeps_bf16_state():
p = torch.nn.Parameter(torch.zeros(4))
opt = Lion([p], lr=0.1)
p.grad = torch.tensor([1.0, -2.0, 0.5, -0.1])
opt.step()
assert torch.allclose(p.detach(), torch.tensor([-0.1, 0.1, -0.1, 0.1]))
assert opt.state[p]["exp_avg"].dtype == torch.bfloat16
fresh = Lion([p], lr=0.5)
fresh.load_state_dict(opt.state_dict()) # torch's default would upcast the state to fp32
assert fresh.state[p]["exp_avg"].dtype == torch.bfloat16
assert fresh.param_groups[0]["lr"] == 0.1
def test_training_with_lion_low_memory_and_resume(data_dir, tmp_path):
cfg = dict(preset="v3-tiny", model_overrides={"block_size": 32}, batch_size=4, warmup_steps=2,
eval_interval=6, eval_iters=2, log_interval=1000, device="cpu", precision="fp32",
optimizer="lion", lr=3e-3, grad_checkpoint=True)
logs = []
s1 = train(data_dir, tmp_path / "m", TrainConfig(max_steps=12, until_step=6, **cfg), log=logs.append)
assert s1["steps"] == 6
assert not any("overflow" in line for line in logs)
state = torch.load(tmp_path / "m" / "optim.pt", weights_only=True)["optimizer"]["optimizers"][0]
assert next(iter(state["state"].values()))["exp_avg"].dtype == torch.bfloat16
s2 = train(data_dir, tmp_path / "m", TrainConfig(max_steps=12, resume=True, **cfg), log=lambda *_: None)
assert s2["steps"] == 12 and s2["final"]["train"] < 5.5 # untrained ~ ln(400) = 6.0
with pytest.raises(ValueError, match="grad-accum 1"):
train(data_dir, tmp_path / "x", TrainConfig(max_steps=2, grad_accum=2, **cfg), log=lambda *_: None)
def test_newton_schulz_dtype():
from ycode.lm.optim import _ns_dtype
assert _ns_dtype(torch.device("cpu")) == torch.bfloat16
def test_sharded_training_two_processes_with_muon_and_resume(data_dir, tmp_path):
"""FSDP across 2 processes (gloo on CPU), as used for 1.5B on 2 GPUs: Muon gathers each sharded
matrix to orthogonalize it, checkpoints gather the full model, optimizer state is per rank."""
out = tmp_path / "fsdp"
def run(*extra):
cmd = [sys.executable, "-m", "torch.distributed.run", "--nproc_per_node=2", "--master_port=29519",
"-m", "ycode.lm.cli", "train", "--data", str(data_dir), "--out", str(out), "--preset", "v3-tiny",
"--context", "32", "--steps", "8", "--batch-size", "2", "--device", "cpu", "--precision", "fp32",
"--eval-interval", "4", "--optimizer", "muon", "--shard", *extra]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
assert proc.returncode == 0, proc.stdout[-2000:] + proc.stderr[-3000:]
return proc.stdout
first = run("--until-step", "4")
assert "sharded (FSDP)" in first
assert (out / "optim-rank0-of-2.pt").is_file() and (out / "optim-rank1-of-2.pt").is_file()
second = run("--resume")
assert "starting it fresh" not in second
model, _, payload = load_checkpoint(out)
assert payload["step"] == 8
# The gathered checkpoint is a normal, complete model.
assert model.num_params() == GPT(model.cfg).num_params()
assert payload["val_loss"] < 6.0
def test_autotrain_halves_batch_after_out_of_memory(tmp_path, monkeypatch):
from ycode.lm import autotrain
from ycode.lm.autotrain import AutoTrainer, Plan, State
from ycode.lm.hub import LocalHub
plan = Plan(base_model="unused", batch_size=8, grad_accum=1, first_segment_steps=2, device="cpu")
trainer = AutoTrainer(plan, tmp_path / "w", LocalHub(tmp_path / "hub", "you/ckpt"), None,
log=lambda *_: None)
out = tmp_path / "w" / "base"
calls = []
def fake_run(cmd):
calls.append(cmd)
if len(calls) == 1:
return 1, "torch.OutOfMemoryError: CUDA out of memory. Tried to allocate 2.00 GiB"
out.mkdir(parents=True, exist_ok=True)
(out / "info.json").write_text('{"step": 2}')
return 0, ""
monkeypatch.setattr(autotrain, "_run_streaming", fake_run)
state = State()
assert trainer.run_phase(state, "pretrain", tmp_path, tmp_path, out, total=2)
assert (state.batch_size, state.grad_accum) == (4, 2)
retry = calls[1]
assert retry[retry.index("--batch-size") + 1] == "4" and retry[retry.index("--grad-accum") + 1] == "2"
def test_autotrain_shards_large_models_on_multiple_gpus(tmp_path):
from ycode.lm.autotrain import AutoTrainer, Plan
from ycode.lm.hub import LocalHub
plan = Plan(base_model="unused", preset="v5-1.5b", nproc=2, shard=True)
trainer = AutoTrainer(plan, tmp_path / "w", LocalHub(tmp_path / "hub", "x/y"), None, log=lambda *_: None)
pre = trainer._train_cmd("pretrain", tmp_path, tmp_path, tmp_path, 10, 5)
sft = trainer._train_cmd("sft", tmp_path, tmp_path, tmp_path, 10, 5)
assert "--shard" in pre and "torch.distributed.run" in pre
assert sft[sft.index("--optimizer") + 1] == "muon" # AdamW's two states would not fit
def test_half_precision_loading_for_inference(data_dir, tmp_path):
"""Big models load straight into bf16 (half the RAM, no fp32 copy) and still generate."""
from ycode.lm.generate import LocalLM
from ycode.lm.tokenizer import BPETokenizer
from ycode.lm.train import export_checkpoint, save_checkpoint
tok = BPETokenizer.load(data_dir / "tokenizer.json")
model = GPT(GPTConfig(vocab_size=tok.vocab_size, **{**PRESETS["v5-tiny"], "block_size": 64}))
save_checkpoint(tmp_path / "full", model, tok, step=1, stage="sft", val_loss=None)
export_checkpoint(tmp_path / "full", tmp_path / "slim", dtype="bf16", max_shard_mb=0.05)
small = LocalLM(tmp_path / "slim", device="cpu") # tiny model: auto keeps full precision
assert small.dtype is None and next(small.model.parameters()).dtype == torch.float32
half = LocalLM(tmp_path / "slim", device="cpu", dtype="bf16")
assert half.dtype == torch.bfloat16
assert all(p.dtype == torch.bfloat16 for p in half.model.parameters())
assert half.model.head.weight is half.model.embed.weight # still tied
assert half.model.rope_cos.dtype == torch.float32 # positions keep full precision
torch.manual_seed(0)
assert isinstance(half.complete("def f(", max_new_tokens=5), str)
with half.precision():
x = torch.randint(0, tok.vocab_size, (1, 16))
_, loss_half = half.model(x, x)
_, loss_full = small.model(x, x)
assert abs(loss_half.item() - loss_full.item()) < 0.1