Download code/moth/sft.py from darioooooo0o/moth-112m: direct link, hf CLI and curl.
- Browser
- Download file 8.96 kB
-
https://huggingface.co/darioooooo0o/moth-112m/resolve/main/code/moth/sft.py
- Command line
-
hf download hf://darioooooo0o/moth-112m/code/moth/sft.py
-
curl -L -o sft.py https://huggingface.co/darioooooo0o/moth-112m/resolve/main/code/moth/sft.py
8.96 kB
| """Fine-tune tiny-agent-112m into moth. | |
| python -m moth.sft --init data/tiny-agent-112m/base --data data/sft --out runs/sft1 | |
| Packs chats into (B, T) rows with one doc id per chat (FlexAttention keeps them apart), trains only on | |
| moth's tokens, uses the pretraining optimizers (Muon + Sinkhorn momentum) with a warmup-stable-decay schedule that | |
| reaches 0 at the end of the run (default 1 epoch, linear decay over the last --decay-frac), and writes | |
| model.safetensors + config.json (the HF layout) to <out>/model (best val) and <out>/ckpt_60, ckpt_80, ckpt_100 | |
| (fixed fractions of the run, see --ckpts). --engram-lr-mult scales the LR of the Engram n-gram tables. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| import random | |
| import sys | |
| import time | |
| import torch | |
| TA = os.environ.get("TA_REPO") or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| sys.path.insert(0, TA) | |
| from tiny_agent.checkpoint import load_model # noqa: E402 | |
| from tiny_agent.model import make_block_mask # noqa: E402 | |
| from tiny_agent.optim import build_optimizers, wsd_lr # noqa: E402 | |
| def pack(rows, T, seed): | |
| """Greedy-pack (ids, mask) chats into rows of T tokens -> list of (inp, tgt, doc) tensors.""" | |
| rows = list(rows) | |
| random.Random(seed).shuffle(rows) | |
| out, cur = [], [] | |
| def flush(): | |
| inp = torch.zeros(T, dtype=torch.long) | |
| tgt = torch.full((T,), -1, dtype=torch.long) | |
| doc = torch.full((T,), 10_000, dtype=torch.long) # padding: its own doc, no targets | |
| p = 0 | |
| for k, (ids, mask) in enumerate(cur): | |
| n = len(ids) - 1 | |
| inp[p:p + n] = torch.tensor(ids[:-1]) | |
| t = torch.tensor(ids[1:]) | |
| t[torch.tensor(mask[1:]) == 0] = -1 | |
| tgt[p:p + n] = t | |
| doc[p:p + n] = k | |
| p += n | |
| out.append((inp, tgt, doc)) | |
| used = 0 | |
| for ids, mask in rows: | |
| n = len(ids) - 1 | |
| if n > T: | |
| continue | |
| if used + n > T: | |
| flush() | |
| cur, used = [], 0 | |
| cur.append((ids, mask)) | |
| used += n | |
| if cur: | |
| flush() | |
| return out | |
| def batches(packed, B): | |
| for i in range(0, len(packed) - B + 1, B): | |
| chunk = packed[i:i + B] | |
| yield tuple(torch.stack([c[j] for c in chunk]) for j in range(3)) | |
| def ckpt_steps(steps: int, fracs: str) -> dict[int, str]: | |
| """--ckpts 0.6,0.8,1.0 over 500 steps -> {300: "ckpt_60", 400: "ckpt_80", 500: "ckpt_100"}.""" | |
| out = {} | |
| for f in filter(None, fracs.split(",")): | |
| out[min(steps, max(1, round(steps * float(f))))] = f"ckpt_{round(float(f) * 100)}" | |
| return out | |
| def split_engram_lr(model, opts, mult: float) -> int: | |
| """Move the Engram tables into their own param group at mult x the LR (build_optimizers puts them in the | |
| Sinkhorn group with the embedding and head). Returns the number of table parameters moved.""" | |
| if mult == 1: | |
| return 0 | |
| ids = {id(m.table.weight) for m in model.engram_modules()} | |
| moved = 0 | |
| for o in opts: | |
| for g in list(o.param_groups): | |
| eng = [p for p in g["params"] if id(p) in ids] | |
| if not eng: | |
| continue | |
| g["params"] = [p for p in g["params"] if id(p) not in ids] | |
| ng = {k: v for k, v in g.items() if k != "params"} | |
| ng.update(params=eng, lr=g["lr"] * mult, base_lr=g["base_lr"] * mult) | |
| o.add_param_group(ng) | |
| moved += sum(p.numel() for p in eng) | |
| return moved | |
| def save(model, out_dir): | |
| from safetensors.torch import save_file | |
| os.makedirs(out_dir, exist_ok=True) | |
| sd = {k: (v.detach().to(torch.bfloat16) if v.is_floating_point() else v).contiguous().cpu() | |
| for k, v in model.state_dict().items()} | |
| save_file(sd, os.path.join(out_dir, "model.safetensors")) | |
| cfg = {k: list(v) if isinstance(v, tuple) else v for k, v in vars(model.cfg).items()} | |
| json.dump(cfg, open(os.path.join(out_dir, "config.json"), "w"), indent=1) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--init", default="data/tiny-agent-112m/base") | |
| ap.add_argument("--data", default="data/sft") | |
| ap.add_argument("--out", default="runs/sft1") | |
| ap.add_argument("--epochs", type=float, default=1) | |
| ap.add_argument("--T", type=int, default=2048) | |
| ap.add_argument("--micro_B", type=int, default=8) | |
| ap.add_argument("--accum", type=int, default=2) | |
| ap.add_argument("--lr", type=float, default=3e-4) | |
| ap.add_argument("--warmup", type=int, default=20) | |
| ap.add_argument("--decay-frac", type=float, default=0.25, help="LR decays linearly to 0 over this last part of the run") | |
| ap.add_argument("--ckpts", default="0.6,0.8,1.0", help="fractions of the run to save as <out>/ckpt_<pct>") | |
| ap.add_argument("--engram-lr-mult", type=float, default=1.0, help="LR multiplier for the Engram tables") | |
| ap.add_argument("--device", default="xpu" if hasattr(torch, "xpu") and torch.xpu.is_available() else "cuda") | |
| a = ap.parse_args() | |
| dev = a.device | |
| os.makedirs(a.out, exist_ok=True) | |
| torch.manual_seed(0) | |
| model = load_model(a.init, device=dev, dtype=torch.float32).train() | |
| cfg = model.cfg | |
| train_rows = torch.load(os.path.join(a.data, "train.pt")) | |
| val = pack(torch.load(os.path.join(a.data, "val.pt")), a.T, 0) | |
| val = val * -(-a.micro_B // len(val)) if len(val) < a.micro_B else val # at least one full batch | |
| n_rows = len(pack(train_rows, a.T, 0)) | |
| steps = max(1, int(n_rows * a.epochs) // (a.micro_B * a.accum)) | |
| opts = build_optimizers(model, lr=a.lr) | |
| moved = split_engram_lr(model, opts, a.engram_lr_mult) | |
| ckpts = ckpt_steps(steps, a.ckpts) | |
| cmodel = torch.compile(model, dynamic=False) | |
| print(f"{len(train_rows)} chats -> {n_rows} rows of {a.T}/epoch, {steps} steps, val rows {len(val)}; " | |
| f"WSD warmup {a.warmup} decay last {a.decay_frac:.0%}; checkpoints at {ckpts}; " | |
| f"engram tables x{a.engram_lr_mult} LR ({moved / 1e6:.1f}M params)", flush=True) | |
| def evaluate(): | |
| model.eval() | |
| ls = [] | |
| with torch.no_grad(): | |
| for inp, tgt, doc in batches(val, a.micro_B): | |
| inp, tgt, doc = inp.to(dev), tgt.to(dev), doc.to(dev) | |
| with torch.autocast(dev, dtype=torch.bfloat16): | |
| ls.append(cmodel(inp, doc, make_block_mask(doc, cfg.swa_window), tgt).item()) | |
| model.train() | |
| return round(sum(ls) / max(len(ls), 1), 4) | |
| log = open(os.path.join(a.out, "log.jsonl"), "a") | |
| print(json.dumps({"step": 0, "val": evaluate()}), file=log, flush=True) | |
| step, epoch, t0, best = 0, 0, time.time(), float("inf") | |
| while step < steps: | |
| it = batches(pack(train_rows, a.T, epoch), a.micro_B) | |
| epoch += 1 | |
| while step < steps: | |
| m = wsd_lr(step, steps, warmup=a.warmup, decay_frac=a.decay_frac) | |
| for o in opts: | |
| for g in o.param_groups: | |
| g["lr"] = g["base_lr"] * m | |
| loss_acc, ok = 0.0, True | |
| for _ in range(a.accum): | |
| b = next(it, None) | |
| if b is None: | |
| ok = False | |
| break | |
| inp, tgt, doc = (x.to(dev) for x in b) | |
| with torch.autocast(dev, dtype=torch.bfloat16): | |
| loss = cmodel(inp, doc, make_block_mask(doc, cfg.swa_window), tgt) | |
| (loss / a.accum).backward() | |
| loss_acc += loss.detach() | |
| if not ok: | |
| for o in opts: | |
| o.zero_grad(set_to_none=True) | |
| break | |
| for o in opts: | |
| o.step() | |
| o.zero_grad(set_to_none=True) | |
| step += 1 | |
| ck = ckpts.get(step) | |
| if step % 10 == 0 or step == steps or ck: | |
| l = (loss_acc / a.accum).item() | |
| if not math.isfinite(l): | |
| raise RuntimeError(f"non-finite loss at step {step}") | |
| rec = {"step": step, "epoch": epoch, "loss": round(l, 4), "lr_mult": round(m, 3), | |
| "min": round((time.time() - t0) / 60, 1)} | |
| if step % 50 == 0 or step == steps or ck: | |
| rec["val"] = evaluate() | |
| if rec["val"] < best: # keep the best-val weights too | |
| best = rec["val"] | |
| save(model, os.path.join(a.out, "model")) | |
| rec["saved"] = True | |
| if ck: # fixed points of the decay, to compare by eval rather than val loss | |
| save(model, os.path.join(a.out, ck)) | |
| rec["ckpt"] = ck | |
| print(json.dumps(rec), file=log, flush=True) | |
| print(json.dumps(rec), flush=True) | |
| print(f"done after {step} steps, {(time.time() - t0) / 60:.1f} min; {a.out}/model = best val {best}; " | |
| f"checkpoints {sorted(ckpts.values())}", flush=True) | |
| if __name__ == "__main__": | |
| main() | |