HCho's picture
BitStream Modular Machine - submission 1
4fc906a verified
Raw
History Blame Contribute Delete
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)
@torch.no_grad()
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
@torch.no_grad()
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()