| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import math |
| import os |
| import sys |
|
|
| import torch |
|
|
| _NC = os.environ.get("NANOCHAT_PATH") |
| if not _NC or not os.path.isdir(os.path.join(os.path.expanduser(_NC), "nanochat")): |
| sys.exit("set NANOCHAT_PATH to a nanochat checkout, e.g. NANOCHAT_PATH=~/nanochat") |
| sys.path.insert(0, os.path.expanduser(_NC)) |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
|
|
| |
| |
| os.environ["NANOCHAT_DTYPE"] = "bfloat16" |
| |
| |
| |
| os.environ["TORCHDYNAMO_DISABLE"] = "1" |
|
|
| from nanochat.gpt import GPT, GPTConfig |
| from convert_ckpt_to_nanochat import (convert, convert_optimizer, |
| ve_layers, _matrix_params) |
|
|
| NL, DM, NH, HD, V = 8, 192, 12, 16, 384 |
| KV, MLP = NH * HD, 4 * DM |
| VE, D_VE_GATE, D_SMR = ve_layers(NL), 12, 24 |
| NVE = len(VE) |
| LRS = dict(unembedding_lr=0.004, embedding_lr=0.2, matrix_lr=0.02, scalar_lr=0.5, |
| weight_decay=0.0) |
|
|
| upper = lambda x: (x.contiguous().view(torch.int32) >> 16).to(torch.int16).view(torch.bfloat16) |
| lower = lambda x: (x.contiguous().view(torch.int32)).to(torch.int16).view(torch.uint16) |
|
|
| torch.manual_seed(0) |
|
|
| |
| BANKS = { |
| "input_embeds": (V, DM), "value_embeds": (NVE, V, KV), "lm_head": (V, DM), |
| "W_Q": (NL, KV, DM), "W_K": (NL, KV, DM), "W_V": (NL, KV, DM), "W_O": (NL, DM, KV), |
| "W_in": (NL, MLP, DM), "W_out": (NL, DM, MLP), "ve_gate": (NVE, NH, D_VE_GATE), |
| } |
| fp32_src, weights, state = {}, {}, {} |
| for name, shape in BANKS.items(): |
| t = torch.randn(*shape) |
| fp32_src[name], weights[name], state[f"{name}.mantissa"] = t, upper(t), lower(t) |
| for name, shape in [("resid_lambdas", (NL,)), ("x0_lambdas", (NL,)), |
| ("smear_gate", (1, D_SMR)), ("smear_lambda", (1,)), |
| ("backout_lambda", (1,))]: |
| weights[name] = torch.randn(*shape) |
|
|
| |
| |
| |
| |
| |
| |
| for t_id, (name, shape) in enumerate([("input_embeds", (V, DM)), ("lm_head", (V, DM)), |
| ("value_embeds", (NVE * V, KV)), |
| ("resid_lambdas", (NL,)), ("x0_lambdas", (NL,)), |
| ("smear_gate", (1, D_SMR)), ("smear_lambda", (1,)), |
| ("backout_lambda", (1,))]): |
| for a_id, attr in enumerate(("exp_avg", "exp_avg_sq")): |
| n = math.prod(shape) |
| base = (t_id * 2 + a_id) * n |
| state[f"{name}.{attr}"] = ( |
| (torch.arange(n, dtype=torch.float32) + base) * 1e-7).reshape(*shape) |
| |
| |
| SND = {"W_Q": (NL, KV, 1), "W_K": (NL, KV, 1), "W_V": (NL, KV, 1), "W_O": (NL, 1, KV), |
| "W_in": (NL, MLP, 1), "W_out": (NL, 1, MLP), "ve_gate": (NVE, NH, 1)} |
| for name, shape in SND.items(): |
| b = BANKS[name] |
| state[f"{name}.frst_mntm"] = torch.stack( |
| [torch.full(b[1:], float(i + 1)) for i in range(b[0])]) |
| state[f"{name}.scnd_mntm"] = torch.stack( |
| [torch.full(shape[1:], float(i + 1) * 0.5) for i in range(shape[0])]) |
|
|
| model_data = {"step": 42, "code": "# toy\n", "weights": weights} |
| optim_data = {"step": 42, "t_step": 42, "state": state} |
|
|
| cfg = GPTConfig(sequence_len=256, vocab_size=V, n_layer=NL, n_head=NH, n_kv_head=NH, |
| n_embd=DM, window_pattern="SSSL") |
| new_opt = lambda m: m.setup_optimizer(unembedding_lr=LRS["unembedding_lr"], |
| embedding_lr=LRS["embedding_lr"], |
| matrix_lr=LRS["matrix_lr"], |
| weight_decay=LRS["weight_decay"], |
| scalar_lr=LRS["scalar_lr"]) |
|
|
| |
| with torch.device("meta"): |
| ref = GPT(cfg) |
| ref_sd = ref.state_dict() |
| for tag, mant in [("with mantissas", state), ("model only", None)]: |
| sd = convert(model_data, mant) |
| assert set(sd) == set(ref_sd), (f"key mismatch: missing " |
| f"{sorted(set(ref_sd) - set(sd))[:4]}, extra " |
| f"{sorted(set(sd) - set(ref_sd))[:4]}") |
| with torch.device("meta"): |
| model = GPT(cfg) |
| model.to_empty(device="cpu") |
| model.init_weights() |
| want_dtype = {k: v.dtype for k, v in model.state_dict().items()} |
| model.load_state_dict(sd, strict=True, assign=True) |
| bad = {k: (sd[k].dtype, want_dtype[k]) for k in sd if sd[k].dtype != want_dtype[k]} |
| assert not bad, f"dtype mismatch vs a fresh model: {bad}" |
| for key, src, idx in [("lm_head.weight", "lm_head", None), |
| ("transformer.h.2.attn.c_q.weight", "W_Q", 2), |
| ("transformer.h.2.attn.c_proj.weight", "W_O", 2), |
| ("transformer.h.3.mlp.c_fc.weight", "W_in", 3), |
| (f"transformer.h.{VE[1]}.attn.ve_gate.weight", "ve_gate", 1)]: |
| full = fp32_src[src] if idx is None else fp32_src[src][idx] |
| want = full if mant else (upper(fp32_src[src]) if idx is None |
| else upper(fp32_src[src])[idx]).float() |
| assert torch.equal(sd[key], want), f"{tag}: {key} value mismatch" |
| for key, src, idx in [("transformer.wte.weight", "input_embeds", None), |
| (f"value_embeds.{VE[0]}.weight", "value_embeds", 0)]: |
| want = weights[src] if idx is None else weights[src][idx] |
| assert torch.equal(sd[key], want), f"{tag}: {key} should be the bf16 live weight" |
| print(f"[OK] weights, {tag}: {len(sd)} tensors, strict load, values verified") |
|
|
| ve_keys = sorted(k for k in ref_sd if k.endswith("ve_gate.weight")) |
| assert ve_keys == [f"transformer.h.{i}.attn.ve_gate.weight" for i in VE], ve_keys |
| print(f"[OK] ve slot->layer map: slots 0..{NVE - 1} -> layers {VE}") |
|
|
| |
| ref_opt = new_opt(model) |
| param_key = {id(p): k for k, p in model.named_parameters()} |
| flat = [p for g in ref_opt.param_groups for p in g["params"]] |
| index_of_key = {param_key[id(p)]: i for i, p in enumerate(flat)} |
| print(f"reference optimizer: {len(ref_opt.param_groups)} groups, {len(flat)} params") |
|
|
| ROLE = {"W_Q": "attn.c_q", "W_K": "attn.c_k", "W_V": "attn.c_v", "W_O": "attn.c_proj", |
| "W_in": "mlp.c_fc", "W_out": "mlp.c_proj"} |
| def key_of(entry): |
| name, slot = entry |
| return {"lm_head": "lm_head.weight", "input_embeds": "transformer.wte.weight", |
| "smear_gate": "smear_gate.weight"}.get(name) \ |
| or (f"value_embeds.{VE[slot]}.weight" if name == "value_embeds" |
| else f"transformer.h.{VE[slot]}.attn.ve_gate.weight" if name == "ve_gate" |
| else name if slot is None |
| else f"transformer.h.{slot}.{ROLE[name]}.weight") |
|
|
| ADAMW = ([("lm_head", None), ("input_embeds", None)] |
| + [("value_embeds", j) for j in range(NVE)] |
| + [("resid_lambdas", None), ("x0_lambdas", None), ("smear_gate", None), |
| ("smear_lambda", None), ("backout_lambda", None)]) |
| matrix = _matrix_params(NL, VE) |
| shape_of = lambda e: tuple(weights[e[0]].shape[1:]) |
|
|
| |
| |
| |
| for W in (1, 2, 3, 4): |
| shards = [convert_optimizer(model_data, optim_data, W, r, LRS) for r in range(W)] |
|
|
| for sd_r in shards: |
| opt = new_opt(model) |
| opt.load_state_dict(sd_r) |
| for g in opt.param_groups: |
| if g["kind"] != "muon": |
| continue |
| assert "momentum_buffer" in opt.state[g["params"][0]], "muon state off params[0]" |
| assert not any(opt.state.get(p) for p in g["params"][1:]), "muon state leaked" |
|
|
| for entry in ADAMW: |
| pidx = index_of_key[key_of(entry)] |
| full = state[f"{entry[0]}.exp_avg"] |
| if entry[1] is not None: |
| full = full.view(NVE, -1, full.shape[-1])[entry[1]] |
| pieces = [s["state"][pidx]["exp_avg"] for s in shards] |
| got = pieces[0] if pieces[0].shape == full.shape else torch.cat(pieces, 0) |
| assert torch.equal(got, full), f"W={W} {key_of(entry)}: adamw state misassembled" |
| assert all(s["state"][pidx]["step"] == 42 for s in shards), "step not carried" |
|
|
| for shape in sorted({shape_of(e) for e in matrix}): |
| members = [e for e in matrix if shape_of(e) == shape] |
| first = index_of_key[key_of(members[0])] |
| chunk = -(-len(members) // W) |
| for n, e in enumerate(members): |
| r, k = n // chunk, n % chunk |
| assert torch.equal(shards[r]["state"][first]["momentum_buffer"][k], |
| state[f"{e[0]}.frst_mntm"][e[1]]), f"W={W} {e}: momentum" |
| got = shards[r]["state"][first]["second_momentum_buffer"][k] |
| ours = state[f"{e[0]}.scnd_mntm"][e[1]] |
| if tuple(got.shape) == tuple(ours.shape): |
| assert torch.equal(got, ours), f"W={W} {e}: second moment" |
| else: |
| assert e[0] == "W_O" and shape[-2] == shape[-1], f"unexpected fallback {e}" |
| assert torch.allclose(got, ours.mean().expand_as(got)), f"W={W} {e}: fallback" |
| for r in range(W): |
| for k in range(chunk): |
| if r * chunk + k >= len(members): |
| assert not shards[r]["state"][first]["momentum_buffer"][k].any(), "padding" |
| print(f"[OK] optimizer, world_size={W}: structure + routing + reassembly verified") |
|
|
| |
| |
| |
| |
| opt = new_opt(model) |
| opt.load_state_dict(convert_optimizer(model_data, optim_data, 1, 0, LRS)) |
| for p in model.parameters(): |
| p.grad = torch.randn_like(p) * 1e-3 |
| before = {k: v.detach().clone() for k, v in model.named_parameters()} |
| mom_before = {i: opt.state[g["params"][0]]["momentum_buffer"].clone() |
| for i, g in enumerate(opt.param_groups) if g["kind"] == "muon"} |
| opt.step() |
| stuck = [k for k, v in model.named_parameters() if torch.equal(v, before[k])] |
| assert not stuck, f"params did not move: {stuck[:5]}" |
| for i, m0 in mom_before.items(): |
| assert not torch.equal(m0, opt.state[opt.param_groups[i]["params"][0]]["momentum_buffer"]), \ |
| f"muon group {i}: momentum buffer did not advance" |
| assert all(opt.state[g["params"][0]]["step"] == 43 |
| for g in opt.param_groups if g["kind"] == "adamw"), "step did not advance from 42" |
| assert all(torch.isfinite(v).all() for _, v in model.named_parameters()), "non-finite param" |
| print(f"[OK] warm-started MuonAdamW.step(): all {len(before)} params updated, " |
| "buffers advanced, step 42 -> 43") |
|
|
| |
| |
| |
| bad_state = dict(state) |
| bad_state["W_in.scnd_mntm"] = torch.zeros(NL, 1, DM) |
| try: |
| convert_optimizer(model_data, {"step": 42, "t_step": 42, "state": bad_state}, 1, 0, LRS) |
| sys.exit("FAIL: a non-square second-moment axis mismatch was accepted") |
| except AssertionError as e: |
| assert "non-square" in str(e), e |
| print("[OK] non-square second-moment axis mismatch refused") |
| print("PASS") |
|
|