Download train.py from HCho/bitstream-modmul-model: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/HCho/bitstream-modmul-model/resolve/main/train.py
- Command line
-
hf download hf://HCho/bitstream-modmul-model/train.py
-
curl -L -o train.py https://huggingface.co/HCho/bitstream-modmul-model/resolve/main/train.py
12.6 kB
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import queue | |
| import random | |
| import sys | |
| import threading | |
| import time | |
| from pathlib import Path | |
| import torch | |
| import torch.nn.functional as F | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| from model import ( | |
| make_reduce_cell, make_add_cell, reduce_features, add_features, | |
| ) | |
| from data import make_reduce_batch, make_add_batch | |
| STAGES = [8, 12, 20, 36, 68, 132, 260, 516, 1027] | |
| TOKEN_BUDGET = 40_000 | |
| EVAL_EVERY = 500 | |
| ADVANCE_THRESHOLD = 0.9995 | |
| SOFT_STEPS = 1_500 | |
| NOISE = 0.01 | |
| MARGIN = 3.0 | |
| MARGIN_W = 0.1 | |
| AUX_W = 0.3 | |
| PLATEAU_STEPS = 12_000 | |
| KINDS = ("reduce", "add") | |
| def features_for(kind, b): | |
| if kind == "reduce": | |
| return reduce_features(b["x"], b["p"], b["p3"]) | |
| return add_features(b["x"], b["y"], b["g"]) | |
| def forward_loss(kind, cell, batch): | |
| outs = cell.forward_train(features_for(kind, batch)) | |
| tgt = batch["z"] | |
| loss_bits = F.binary_cross_entropy_with_logits(outs["bits"], tgt) | |
| sign = 2 * tgt - 1 | |
| loss_margin = F.relu(MARGIN - outs["bits"] * sign).mean() | |
| if kind == "reduce": | |
| loss_aux = ( | |
| F.binary_cross_entropy_with_logits(outs["borrow"], batch["borrow"]) | |
| + F.cross_entropy(outs["q"], batch["q"]) | |
| ) | |
| else: | |
| loss_aux = F.binary_cross_entropy_with_logits( | |
| outs["carry"], batch["carry"]) | |
| loss = loss_bits + MARGIN_W * loss_margin + AUX_W * loss_aux | |
| loss = loss + 1e-4 * sum(blk.last_h_l1 for blk in cell.blocks) | |
| ok_rows = ((outs["bits"] > 0) == (tgt > 0.5)).all(dim=1) | |
| return loss, { | |
| "loss": loss.item(), "bits": loss_bits.item(), | |
| "exact": ok_rows.float().mean().item(), | |
| }, ok_rows | |
| def bsz_for(n): | |
| return min(1024, max(8, TOKEN_BUDGET // n)) | |
| def fresh_batch(kind, rng, n, bsz, instances=None): | |
| if kind == "reduce": | |
| return make_reduce_batch(rng, n, bsz, instances) | |
| return make_add_batch(rng, n, bsz, instances) | |
| def evaluate(kind, cell, rng, n, device, n_batches=4): | |
| cell.eval() | |
| total, good = 0, 0 | |
| for _ in range(n_batches): | |
| b = fresh_batch(kind, rng, n, bsz_for(n)) | |
| b.pop("raw") | |
| b = {k: v.to(device) for k, v in b.items()} | |
| logits = cell(features_for(kind, b)) | |
| ok = ((logits > 0) == (b["z"] > 0.5)).all(dim=1) | |
| good += int(ok.sum()) | |
| total += ok.numel() | |
| cell.train() | |
| return good / total | |
| def set_mode(cell, mode, noise): | |
| cell.gate.mode = mode | |
| for blk in cell.blocks: | |
| blk.scan_noise = noise | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--resume", default=None) | |
| ap.add_argument("--steps", type=int, default=600_000) | |
| ap.add_argument("--lr", type=float, default=1e-3) | |
| ap.add_argument("--out", default="ckpt2") | |
| ap.add_argument("--seed", type=int, default=0) | |
| ap.add_argument("--max-stage", type=int, default=None) | |
| ap.add_argument("--stages", type=int, nargs="*", default=None) | |
| ap.add_argument("--force-stage", type=int, default=None) | |
| ap.add_argument("--eval-window", type=int, default=0, | |
| help="0 = min over all widths <= stage; K>0 = min over last K stages only") | |
| args = ap.parse_args() | |
| global STAGES | |
| if args.stages: | |
| STAGES = args.stages | |
| if args.max_stage is None: | |
| args.max_stage = len(STAGES) - 1 | |
| device = torch.device("mps" if torch.backends.mps.is_available() else "cpu") | |
| rng = random.Random(args.seed) | |
| torch.manual_seed(args.seed) | |
| cells = {"reduce": make_reduce_cell("soft").to(device), | |
| "add": make_add_cell("soft").to(device)} | |
| opts = {k: torch.optim.AdamW(c.parameters(), lr=args.lr, weight_decay=0.0) | |
| for k, c in cells.items()} | |
| import copy as _copy | |
| ema_cells = {k: _copy.deepcopy(c) for k, c in cells.items()} | |
| EMA_DECAY = 0.999 | |
| def ema_update(kind): | |
| for pe, pr in zip(ema_cells[kind].parameters(), | |
| cells[kind].parameters()): | |
| pe.lerp_(pr, 1.0 - EMA_DECAY) | |
| stage, step, last_advance_step = 0, 0, 0 | |
| streak = 0 | |
| best_box: dict[int, float] = {} | |
| out_dir = Path(args.out) | |
| out_dir.mkdir(exist_ok=True) | |
| metrics_f = open(out_dir / "metrics.jsonl", "a") | |
| if args.resume: | |
| ck = torch.load(args.resume, map_location=device, weights_only=True) | |
| cells["reduce"].load_state_dict(ck["reduce_state_dict"], strict=False) | |
| if "add_state_dict" in ck: | |
| cells["add"].load_state_dict(ck["add_state_dict"], strict=False) | |
| for k in KINDS: | |
| if f"{k}_state_dict" in ck: | |
| sdk = ck.get(f"{k}_ema_state_dict", ck[f"{k}_state_dict"]) | |
| ema_cells[k].load_state_dict(sdk, strict=False) | |
| for k in KINDS: | |
| try: | |
| opts[k].load_state_dict(ck[f"{k}_opt"]) | |
| except Exception: | |
| print(f"{k}: fresh optimizer", flush=True) | |
| stage, step = ck["stage"], ck["step"] | |
| if args.force_stage is not None: | |
| stage = args.force_stage | |
| last_advance_step = step | |
| print(f"resumed at step {step}, stage {stage}", flush=True) | |
| def set_lr(v): | |
| for o in opts.values(): | |
| for g in o.param_groups: | |
| g["lr"] = v | |
| def get_lr(): | |
| return opts["reduce"].param_groups[0]["lr"] | |
| set_lr(args.lr) | |
| def pick_width(stage_now): | |
| if stage_now == 0 or rng.random() < 0.5: | |
| n = STAGES[stage_now] | |
| else: | |
| n = STAGES[rng.randint(0, stage_now - 1)] | |
| if rng.random() < 0.3: | |
| n = max(5, n - rng.randint(0, max(1, n // 8))) | |
| return n | |
| q: queue.Queue = queue.Queue(maxsize=4) | |
| stage_box = {"stage": stage} | |
| hard_buf: dict[tuple, list] = {} | |
| buf_lock = threading.Lock() | |
| def load_mined(kind): | |
| import json as _json | |
| path = out_dir / f"mined_{kind}.jsonl" | |
| if not path.exists(): | |
| return | |
| try: | |
| lines = path.read_text().splitlines()[-40000:] | |
| except Exception: | |
| return | |
| per_width: dict[int, list] = {} | |
| for ln in lines: | |
| try: | |
| row = _json.loads(ln) | |
| except Exception: | |
| continue | |
| if kind == "reduce": | |
| inst = (row["m"], row["x"]) | |
| else: | |
| inst = (row["x"], row["y"], row["g"]) | |
| per_width.setdefault(row["n"], []).append(inst) | |
| with buf_lock: | |
| for n, insts in per_width.items(): | |
| buf = hard_buf.setdefault((kind, n), []) | |
| buf.extend(insts) | |
| if len(buf) > 20_000: | |
| del buf[: len(buf) - 20_000] | |
| def producer(): | |
| prng = random.Random(args.seed + 1) | |
| last_mined = 0.0 | |
| while True: | |
| if time.time() - last_mined > 120: | |
| for k in KINDS: | |
| load_mined(k) | |
| last_mined = time.time() | |
| kind = "reduce" if prng.random() < 0.5 else "add" | |
| cur_w = STAGES[min(stage_box["stage"], len(STAGES) - 1)] + 8 | |
| with buf_lock: | |
| widths = [w for (k, w), v in hard_buf.items() | |
| if k == kind and len(v) >= 64 and w <= cur_w] | |
| if widths and prng.random() < 0.30: | |
| n = prng.choice(widths) | |
| bsz = bsz_for(n) | |
| with buf_lock: | |
| pool = hard_buf[(kind, n)] | |
| replay = [pool[prng.randrange(len(pool))] | |
| for _ in range(bsz // 2)] | |
| b = fresh_batch(kind, prng, n, bsz - len(replay)) | |
| br = fresh_batch(kind, prng, n, len(replay), instances=replay) | |
| merged = {k: torch.cat([b[k], br[k]]) | |
| for k in b if k != "raw"} | |
| merged["raw"] = b["raw"] + br["raw"] | |
| q.put((kind, n, merged)) | |
| else: | |
| n = pick_width(stage_box["stage"]) | |
| q.put((kind, n, fresh_batch(kind, prng, n, bsz_for(n)))) | |
| threading.Thread(target=producer, daemon=True).start() | |
| for c in cells.values(): | |
| c.train() | |
| t0 = time.time() | |
| while step < args.steps: | |
| step += 1 | |
| mode = "soft" if step <= SOFT_STEPS else "ste" | |
| noise = 0.0 if step <= SOFT_STEPS else NOISE | |
| for c in cells.values(): | |
| set_mode(c, mode, noise) | |
| stage_box["stage"] = stage | |
| kind, n, batch = q.get() | |
| raw = batch.pop("raw") | |
| batch = {k: v.to(device) for k, v in batch.items()} | |
| cell, opt = cells[kind], opts[kind] | |
| loss, stats, ok_rows = forward_loss(kind, cell, batch) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(cell.parameters(), 1.0) | |
| opt.step() | |
| ema_update(kind) | |
| if step > SOFT_STEPS: | |
| bad = (~ok_rows).nonzero().flatten().tolist() | |
| if bad: | |
| with buf_lock: | |
| buf = hard_buf.setdefault((kind, n), []) | |
| buf.extend(raw[i] for i in bad) | |
| if len(buf) > 20_000: | |
| del buf[: len(buf) - 20_000] | |
| if step % 100 == 0: | |
| stats.update(step=step, stage=stage, n=n, kind=kind, | |
| lr=get_lr(), | |
| sps=round(step / (time.time() - t0), 2)) | |
| print(json.dumps(stats), flush=True) | |
| metrics_f.write(json.dumps(stats) + "\n") | |
| metrics_f.flush() | |
| if step % EVAL_EVERY == 0 and step > SOFT_STEPS: | |
| accs = {} | |
| for k in KINDS: | |
| set_mode(ema_cells[k], "hard", 0.0) | |
| ema_cells[k].eval() | |
| lo = 0 if args.eval_window == 0 else max(0, stage + 1 - args.eval_window) | |
| accs[k] = min( | |
| evaluate(k, ema_cells[k], rng, STAGES[s], device, | |
| n_batches=2 if s < stage else 4) | |
| for s in range(lo, stage + 1)) | |
| print(json.dumps({"eval": accs, "stage": stage, | |
| "n": STAGES[stage], "step": step}), flush=True) | |
| metrics_f.write(json.dumps( | |
| {"eval": accs, "stage": stage, "step": step}) + "\n") | |
| metrics_f.flush() | |
| score = min(accs.values()) | |
| if score > best_box.get(stage, 0.0): | |
| best_box[stage] = score | |
| torch.save( | |
| {"reduce_state_dict": cells["reduce"].state_dict(), | |
| "add_state_dict": cells["add"].state_dict(), | |
| "reduce_ema_state_dict": | |
| ema_cells["reduce"].state_dict(), | |
| "add_ema_state_dict": ema_cells["add"].state_dict(), | |
| "stage": stage, "step": step, "score": score}, | |
| out_dir / f"best_stage{stage}.pt", | |
| ) | |
| ok = score >= ADVANCE_THRESHOLD | |
| streak = streak + 1 if ok else 0 | |
| if streak >= 2 and stage < min(args.max_stage, len(STAGES) - 1): | |
| stage += 1 | |
| streak = 0 | |
| last_advance_step = step | |
| set_lr(args.lr) | |
| print(f"=== ADVANCE to stage {stage} (N={STAGES[stage]}) ===", | |
| flush=True) | |
| elif step - last_advance_step >= PLATEAU_STEPS and get_lr() > 5.1e-5: | |
| set_lr(max(5e-5, get_lr() * 0.5)) | |
| last_advance_step = step | |
| print(f"=== LR DECAY to {get_lr():.2e} (plateau) ===", | |
| flush=True) | |
| torch.save( | |
| {"reduce_state_dict": cells["reduce"].state_dict(), | |
| "add_state_dict": cells["add"].state_dict(), | |
| "reduce_ema_state_dict": ema_cells["reduce"].state_dict(), | |
| "add_ema_state_dict": ema_cells["add"].state_dict(), | |
| "reduce_opt": opts["reduce"].state_dict(), | |
| "add_opt": opts["add"].state_dict(), | |
| "stage": stage, "step": step}, | |
| out_dir / "latest.pt", | |
| ) | |
| if step % 5000 == 0: | |
| import shutil | |
| shutil.copy(out_dir / "latest.pt", | |
| out_dir / f"snap_{step}.pt") | |
| snaps = sorted(out_dir.glob("snap_*.pt"), | |
| key=lambda p: int(p.stem.split("_")[1])) | |
| for old in snaps[:-6]: | |
| old.unlink() | |
| metrics_f.close() | |
| if __name__ == "__main__": | |
| main() | |