ncm-modular / src /ncm_modular /train_ar.py
Red-fuchs's picture
ncm-modular: initial release (code + results, no weights)
b5731a6 verified
Raw History Blame Contribute Delete
5.85 kB
"""Train AR baseline (--mode ar) or intent classifier (--mode cls) on MASSIVE."""
import os, sys, json, time, argparse
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from paths import WORK
import numpy as np, mlx.core as mx, mlx.optimizers as optim
from mlx import nn
from transformers import AutoTokenizer
from gen_mlx import FrozenCodec
from ar_mlx import *
ap = argparse.ArgumentParser()
ap.add_argument("--mode", default="ar"); ap.add_argument("--data", default=WORK+"/massive_de"); ap.add_argument("--out", default=WORK+"/ar_run")
ap.add_argument("--steps", type=int, default=6000); ap.add_argument("--bs", type=int, default=64); ap.add_argument("--lr", type=float, default=4e-4)
ap.add_argument("--P", type=int, default=48); ap.add_argument("--T", type=int, default=48); ap.add_argument("--n", type=int, default=0)
ap.add_argument("--minutes", type=float, default=1e9); ap.add_argument("--drop", type=float, default=0.1); ap.add_argument("--eval_every", type=int, default=500)
a = ap.parse_args()
EOS, PAD = 151645, 151643
os.makedirs(a.out, exist_ok=True)
tok = AutoTokenizer.from_pretrained(WORK+"/tok/Qwen3-0.6B"); fc = FrozenCodec()
load = lambda sp: [json.loads(l) for l in open(f"{a.data}/{sp}.jsonl")]
def pad(ids, n): ids = ids[:n]; return ids + [PAD] * (n - len(ids))
def prefix_arr(D): return np.array([pad(tok.encode(d["prefix"])[:a.P - 1] + [EOS], a.P) for d in D], np.int32)
Dtr, Ddev = load("train")[: a.n or None], load("dev")
INTS = sorted({d["intent"] for d in load("train")})
ttr = [tok.encode(d["target"]) for d in Dtr]
vocab = sorted({t for x in ttr for t in x}); c2 = {t: i + 3 for i, t in enumerate(vocab)} # 0 BOS, 1 EOS, 2 UNK
V = len(vocab) + 3; inv_v = {v: k for k, v in c2.items()}
json.dump({"vocab": vocab, "ints": INTS}, open(f"{a.out}/vocab.json", "w"))
ptr = prefix_arr(Dtr); pdev = prefix_arr(Ddev)
def tgt_arr(ids):
return np.array([[0] + [c2.get(t, 2) for t in x][: a.T - 1] + [1] + [1] * (a.T - 1 - min(len(x), a.T - 1)) for x in ids], np.int32) # (N, T+1): BOS..EOS, padded w/ EOS
def tmask(ids): return np.array([[1] * (min(len(x), a.T - 1) + 1) + [0] * (a.T - 1 - min(len(x), a.T - 1)) for x in ids], np.float32)
if a.mode == "ar":
Ttr = tgt_arr(ttr); Mtr = tmask(ttr)
model = ARGen(fc.tok, V, a.P, a.T, pad_id=PAD, drop=a.drop)
def loss_fn(m, p, t, w):
lg = m(p, t[:, :-1]); ce = nn.losses.cross_entropy(lg, t[:, 1:], reduction="none")
return (ce * w).sum() / w.sum()
else:
Ytr = np.array([INTS.index(d["intent"]) for d in Dtr], np.int32); Ydev = np.array([INTS.index(d["intent"]) if d["intent"] in INTS else -1 for d in Ddev])
model = IntentCls(fc.tok, len(INTS), a.P, pad_id=PAD, drop=a.drop)
def loss_fn(m, p, y, w): return nn.losses.cross_entropy(m(p), y, reduction="mean")
from gen_mlx import count_params
print("params", count_params(model) / 1e6, "M V", V, "N", len(Dtr), flush=True)
mx.random.seed(0); np.random.seed(0)
sched = optim.join_schedules([optim.linear_schedule(0, a.lr, 200), optim.cosine_decay(a.lr, max(a.steps - 200, 1), a.lr * 0.05)], [200])
opt = optim.AdamW(sched, weight_decay=0.05, betas=(0.9, 0.98))
vg = nn.value_and_grad(model, loss_fn); state = [model.state, opt.state, mx.random.state]
@mx.compile
def _s(p, t, w): pass
def step(p, t, w):
loss, g = vg(model, p, t, w); g, _ = optim.clip_grad_norm(g, 1.0); opt.update(model, g); return loss
step = mx.compile(step, inputs=state, outputs=state)
Pdv = mx.array(pdev[:300])
def dev_loss():
if a.mode == "cls": return nn.losses.cross_entropy(model(mx.array(pdev)), mx.array(np.maximum(Ydev, 0)), reduction="mean").item()
Td, Md = tgt_arr([tok.encode(d["target"]) for d in Ddev[:300]]), tmask([tok.encode(d["target"]) for d in Ddev[:300]])
return loss_fn(model, Pdv, mx.array(Td), mx.array(Md)).item()
best = [-1.0]; curve = []
def evaluate():
model.eval()
if a.mode == "cls":
hit = 0
for i in range(0, len(Ddev), 256): hit += (mx.argmax(model(mx.array(pdev[i:i+256])), -1) == mx.array(Ydev[i:i+256])).sum().item()
r = {"dev_intent_acc": hit / len(Ddev), "dev_loss": dev_loss()}; model.train(); return r
n = min(512, len(Ddev)); ex = 0
for i in range(0, n, 128):
out, _ = model.generate(mx.array(pdev[i:i+128][: n - i]), 1); mx.eval(out)
for r, d in zip(out.tolist(), Ddev[i:i+128]):
r = r[:r.index(1)] if 1 in r else r
ex += tok.decode([inv_v.get(c, 0) for c in r if c > 2]) == d["target"]
n2 = n; r = {"dev_target_exact_512": ex / n, "dev_loss": dev_loss()}; model.train(); return r
t0 = time.time(); ema = None; model.train()
for s in range(a.steps):
idx = np.random.randint(0, len(Dtr), a.bs)
if a.mode == "ar": loss = step(mx.array(ptr[idx]), mx.array(Ttr[idx]), mx.array(Mtr[idx]))
else: loss = step(mx.array(ptr[idx]), mx.array(Ytr[idx]), mx.zeros(1))
mx.eval(state, loss); l = loss.item(); ema = l if ema is None else .98 * ema + .02 * l
if (s + 1) % 100 == 0: print(f"step {s+1} loss {ema:.4f} {(time.time()-t0)/60:.1f}m", flush=True)
if (s + 1) % a.eval_every == 0 or s + 1 == a.steps:
r = evaluate(); r["step"] = s + 1; curve.append(r); print(" ", r, flush=True)
sc = list(r.values())[0] - 1e-3 * r["dev_loss"]
if sc > best[0]: best[0] = sc; model.save_weights(f"{a.out}/best.safetensors"); json.dump(r, open(f"{a.out}/best.json", "w"))
json.dump(curve, open(f"{a.out}/dev_curve.json", "w"))
model.save_weights(f"{a.out}/model.safetensors")
json.dump({"step": s + 1, "args": vars(a), "V": V}, open(f"{a.out}/ckpt.json", "w"))
if time.time() - t0 > a.minutes * 60: print("time budget"); break
model.save_weights(f"{a.out}/model.safetensors"); json.dump({"step": s + 1, "args": vars(a), "V": V}, open(f"{a.out}/ckpt.json", "w"))
print("train time min", (time.time() - t0) / 60)