Download modular_ts/distill.py from Modularcomputing/Modular-TS-2.4M: direct link, hf CLI and curl.
- Browser
- Download file 8.53 kB
-
https://huggingface.co/Modularcomputing/Modular-TS-2.4M/resolve/main/modular_ts/distill.py
- Command line
-
hf download hf://Modularcomputing/Modular-TS-2.4M/modular_ts/distill.py
-
curl -L -o distill.py https://huggingface.co/Modularcomputing/Modular-TS-2.4M/resolve/main/modular_ts/distill.py
8.53 kB
| """Stage B: fine-tune a Stage-A checkpoint with ground-truth pinball + teacher-ensemble pinball (TimesFM-2.5 + Chronos-2). | |
| python -m modular_ts.distill --init runs/run2_8k/ckpt.pt --teach teach/synth,teach/gep --lam 0.5 --minutes 45 --out runs/run3 | |
| Batches: with prob --p_distill a teacher batch (V=1, T=C+F windows, min-context split s=C/32; | |
| synthetic rows: GT pinball + teacher pinball; GiftEvalPretrain rows: teacher pinball ONLY, their | |
| future is filled with the teacher-ensemble median just for the normalisation frame and never used | |
| as a target), else a fresh synthetic Stage-A batch (GT pinball only) to avoid forgetting. | |
| Teacher ensemble = mean of the two teachers' quantiles (Vincentisation) at 0.1..0.9. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import glob | |
| import json | |
| import math | |
| import os | |
| import time | |
| import numpy as np | |
| import torch | |
| from modular_ts.model import ModularTS, count_params, make_config | |
| from modular_ts.synth import SynthStream | |
| from modular_ts.train import export, lr_at | |
| def load_teach(dirs, dev, max_per_dir=0): | |
| ctx, fut, q, src = [], [], [], [] | |
| for si, d in enumerate(dirs): | |
| files = sorted(glob.glob(os.path.join(d, "shard_*.npz"))) | |
| n = 0 | |
| for f in files: | |
| z = np.load(f) | |
| qe = 0.5 * (z["q_tfm"].astype(np.float32) + z["q_cr2"].astype(np.float32)) | |
| qe = np.sort(qe, -1) | |
| ctx.append(z["ctx"]); fut.append(z["fut"]); q.append(qe); src.append(np.full(len(qe), si)) | |
| n += len(qe) | |
| if max_per_dir and n >= max_per_dir: | |
| break | |
| print(f"[teach] {d}: {n} windows from {len(files)} shards", flush=True) | |
| c = torch.from_numpy(np.concatenate(ctx)).to(dev) | |
| f = torch.from_numpy(np.concatenate(fut)).to(dev) | |
| qq = torch.from_numpy(np.concatenate(q)).to(dev) | |
| s = torch.from_numpy(np.concatenate(src)).to(dev) | |
| return c, f, qq, s | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--init", required=True, help="Stage-A ckpt.pt (model weights only are used)") | |
| ap.add_argument("--teach", required=True, help="comma list of teacher shard dirs; dirs containing 'gep' are teacher-only") | |
| ap.add_argument("--lam", type=float, default=0.5) | |
| ap.add_argument("--p_distill", type=float, default=0.6) | |
| ap.add_argument("--lr", type=float, default=3e-4) | |
| ap.add_argument("--warmup", type=int, default=200) | |
| ap.add_argument("--minutes", type=float, default=45) | |
| ap.add_argument("--tokens_per_batch", type=int, default=262144) | |
| ap.add_argument("--ctx", type=int, default=0, help="model context_length for export (0 = from init ckpt)") | |
| ap.add_argument("--compile", action="store_true") | |
| ap.add_argument("--log_every", type=int, default=100) | |
| ap.add_argument("--seed", type=int, default=7) | |
| ap.add_argument("--out", required=True) | |
| args = ap.parse_args() | |
| os.makedirs(args.out, exist_ok=True) | |
| dev = torch.device("cuda") | |
| torch.manual_seed(args.seed) | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| st = torch.load(args.init, map_location=dev) | |
| ctx_len = args.ctx or st["args"]["ctx"] | |
| cfg = make_config(st["args"].get("size", "2.5M"), context_length=ctx_len) | |
| model = ModularTS(cfg).to(dev) | |
| model.load_state_dict(st["model"]) | |
| print("params", count_params(model), "init", args.init, "step", st["step"], flush=True) | |
| dirs = [d for d in args.teach.split(",") if d] | |
| tc, tf, tq, tsrc = load_teach(dirs, dev) | |
| teacher_only = torch.tensor(["gep" in d for d in dirs], device=dev)[tsrc] | |
| C, F, H = tc.shape[1], tf.shape[1], tq.shape[1] | |
| T = C + F | |
| P = cfg.patch_size | |
| Bd = max(1, args.tokens_per_batch // (T // P)) | |
| nT = tc.shape[0] | |
| print(f"[teach] total {nT} windows (teacher-only {int(teacher_only.sum())}), C={C} F={F} H={H}, rows/batch {Bd}", flush=True) | |
| # hold out 2% of synthetic teacher windows for a GT/KD validation number | |
| g = torch.Generator(device=dev); g.manual_seed(args.seed) | |
| perm = torch.randperm(nT, generator=g, device=dev) | |
| syn_idx = perm[~teacher_only[perm]] | |
| val_idx = syn_idx[:min(4096, max(1, len(syn_idx) // 50))] | |
| trmask = torch.ones(nT, dtype=torch.bool, device=dev); trmask[val_idx] = False | |
| tr_idx = torch.nonzero(trmask).squeeze(1) | |
| def distill_batch(idx): | |
| c, f, q, to = tc[idx], tf[idx], tq[idx], teacher_only[idx] | |
| med = q[:, :, 4] | |
| fill = torch.full_like(f, float("nan")) | |
| fill[:, :H] = med | |
| f = torch.where(to[:, None], fill, f) | |
| vals = torch.cat([c, f], 1) | |
| obs = torch.isfinite(vals) | |
| vals = torch.where(obs, vals, torch.zeros_like(vals)) | |
| teach = torch.zeros(len(idx), T, q.shape[-1], device=dev) | |
| teach[:, C:C + H] = q | |
| tobs = torch.zeros(len(idx), T, dtype=torch.bool, device=dev) | |
| tobs[:, C:C + H] = True | |
| R = len(idx) | |
| return dict(values=vals[:, None], observed=obs[:, None], roles=torch.zeros(R, 1, dtype=torch.long, device=dev), | |
| s=torch.full((R,), C // P, dtype=torch.long, device=dev), teacher=teach, teacher_obs=tobs, | |
| gt_row=(~to).float()) | |
| synth = SynthStream(4096, args.tokens_per_batch, dev, seed=args.seed + 99) | |
| decay, no_decay = [], [] | |
| for n, p in model.named_parameters(): | |
| (decay if p.dim() >= 2 else no_decay).append(p) | |
| opt = torch.optim.AdamW([{"params": decay, "weight_decay": 0.05}, {"params": no_decay, "weight_decay": 0.0}], | |
| lr=args.lr, betas=(0.9, 0.95), eps=1e-8, fused=True) | |
| fwd = torch.compile(model.forward_train, dynamic=False) if args.compile else model.forward_train | |
| amp = torch.autocast(device_type="cuda", dtype=torch.bfloat16) | |
| lam = args.lam | |
| def evaluate(): | |
| model.eval() | |
| out = {"val_gt": 0.0, "val_kd": 0.0} | |
| with torch.no_grad(), amp: | |
| for i in range(0, len(val_idx), Bd): | |
| b = distill_batch(val_idx[i:i + Bd]) | |
| l, s_ = model.forward_train(b["values"], b["observed"], b["roles"], b["s"], teacher=b["teacher"], | |
| teacher_obs=b["teacher_obs"], gt_row=b["gt_row"], lam=lam) | |
| out["val_gt"] += float(s_["pinball"]); out["val_kd"] += float(s_["kd"]) | |
| n = math.ceil(len(val_idx) / Bd) | |
| model.train() | |
| return {k: v / n for k, v in out.items()} | |
| log = open(os.path.join(args.out, "log.jsonl"), "a") | |
| rng = np.random.default_rng(args.seed) | |
| t0 = time.time(); step = 0 | |
| rec0 = {"step": 0, **evaluate()} | |
| print(json.dumps(rec0), flush=True); log.write(json.dumps(rec0) + "\n") | |
| model.train() | |
| while (time.time() - t0) / 60 < args.minutes: | |
| frac = (time.time() - t0) / 60 / args.minutes | |
| lr = lr_at(step, max(step + 1, int(step / max(frac, 1e-6))) if step > args.warmup else 10**8, args.warmup, args.lr) | |
| for gp in opt.param_groups: | |
| gp["lr"] = lr | |
| is_d = rng.random() < args.p_distill | |
| with amp: | |
| if is_d: | |
| idx = tr_idx[torch.randint(0, len(tr_idx), (Bd,), device=dev)] | |
| b = distill_batch(idx) | |
| loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"], False, b["teacher"], b["teacher_obs"], b["gt_row"], lam) | |
| else: | |
| b = synth.next_batch() | |
| loss, s_ = fwd(b["values"], b["observed"], b["roles"], b["s"]) | |
| if not torch.isfinite(loss): | |
| opt.zero_grad(set_to_none=True); step += 1 | |
| continue | |
| loss.backward() | |
| gn = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step(); opt.zero_grad(set_to_none=True) | |
| step += 1 | |
| if step % args.log_every == 0: | |
| rec = {"step": step, "lr": lr, "loss": float(loss), "kind": "distill" if is_d else "synth", | |
| "kd": float(s_.get("kd", float("nan"))), "gnorm": float(gn), "min": (time.time() - t0) / 60} | |
| if step % (args.log_every * 10) == 0: | |
| rec.update(evaluate()) | |
| print(json.dumps(rec), flush=True); log.write(json.dumps(rec) + "\n"); log.flush() | |
| torch.save({"model": model.state_dict(), "step": step, "args": {**st["args"], "distill": vars(args)}}, os.path.join(args.out, "ckpt.pt")) | |
| export(model, os.path.join(args.out, "export")) | |
| fin = {"steps": step, **evaluate(), "minutes": (time.time() - t0) / 60, "lam": lam} | |
| print("FINAL", json.dumps(fin), flush=True) | |
| json.dump(fin, open(os.path.join(args.out, "final.json"), "w")) | |
| if __name__ == "__main__": | |
| main() | |