"""Figures + summary numbers for the report from pulled metrics (results/{ice,dm}//metrics.jsonl). Run names: [-]--lr-[-s] """ import argparse import json import re from collections import defaultdict from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import numpy as np COLORS = {"scratch": "#2a78d6", "ivgpt": "#eb6834", "shuffled": "#1baf7a", "rescaled": "#eda100"} # validated, light mode STYLES = {"scratch": "-", "ivgpt": "-", "shuffled": "--", "rescaled": "-."} LABELS = {"scratch": "scratch", "ivgpt": "iVideoGPT init", "shuffled": "shuffled iVideoGPT", "rescaled": "rescaled iVideoGPT"} NAME = re.compile(r"(?:(?P[^-]+)-)?(?Pscratch|ivgpt|shuffled|rescaled|sp[0-9.]+(?:attn|mlp|lower|upper)?|spshuf[0-9.]+|nobody)-(?Pnone|body|lora|top4)-lr(?P[0-9.e-]+)-(?P\d+M)(?:-s(?P\d+))?$") plt.rcParams.update({ "font.size": 8, "axes.titlesize": 8.5, "axes.labelsize": 8, "legend.fontsize": 7, "axes.spines.top": False, "axes.spines.right": False, "axes.edgecolor": "#52514e", "axes.labelcolor": "#0b0b0b", "xtick.color": "#52514e", "ytick.color": "#52514e", "axes.grid": True, "grid.color": "#e4e3df", "grid.linewidth": 0.5, "lines.linewidth": 1.4, "figure.dpi": 200, "savefig.bbox": "tight", "pdf.fonttype": 42, }) def load_runs(roots): runs = [] for root in roots: for d in sorted(Path(root).glob("*/")): m = NAME.match(d.name) f = d / "metrics.jsonl" if not m or not f.exists(): continue recs = [json.loads(l) for l in f.read_text().splitlines() if l.strip()] val = np.array([(r["tokens"], r["val_loss"]) for r in recs if "val_loss" in r]) train = [r for r in recs if "tok_per_s" in r] if len(val) < 2: continue g = m.groupdict() runs.append({ "name": d.name, "src": Path(root).name, "init": g["init"], "freeze": g["freeze"], "lr": float(g["lr"]), "budget": g["budget"], "seed": int(g["seed"] or 0), "done": (d / "DONE").exists(), "tokens": val[:, 0], "val": val[:, 1], "tok_per_s": float(np.median([r["tok_per_s"] for r in train[2:]])) if len(train) > 3 else None, "hardware": (d / "hardware.txt").read_text().strip() if (d / "hardware.txt").exists() else d.parent.name, "evals": load_evals(d / "eval.json"), }) return runs EVAL_TASKS = {"piqa": "acc_norm", "hellaswag": "acc_norm", "arc_easy": "acc_norm", "sciq": "acc_norm", "winogrande": "acc", "lambada_openai": "acc"} def load_evals(path): if not path.exists(): return None res = json.loads(path.read_text())["results"] return {t: {"v": res[t][f"{m},none"], "se": res[t].get(f"{m}_stderr,none")} for t, m in EVAL_TASKS.items() if t in res} def tokens_to_reach(run, target): """Tokens at which the (log-linearly interpolated) val curve first hits target, else None.""" t, v = run["tokens"], run["val"] idx = np.where(v <= target)[0] if len(idx) == 0: return None i = idx[0] if i == 0: return float(t[0]) lt0, lt1 = np.log(max(t[i - 1], 1.0)), np.log(t[i]) frac = (v[i - 1] - target) / (v[i - 1] - v[i]) return float(np.exp(lt0 + frac * (lt1 - lt0))) def best_by_arm(runs, budget, freeze): """Best-LR finished run per init (seed 0) at a given budget/freeze.""" best = {} for r in runs: if r["budget"] != budget or r["freeze"] != freeze or r["seed"] != 0 or not r["done"]: continue if r["init"] not in best or r["val"][-1] < best[r["init"]]["val"][-1]: best[r["init"]] = r return best def direct_label(ax, runs_by_init, xmax=None): ends = sorted(((r["val"][-1], init) for init, r in runs_by_init.items()), reverse=True) lo, hi = ax.get_ylim() gap = 0.045 * (hi - lo) last_y = None for y, init in ends: # top to bottom, nudge to avoid collisions ly = y if last_y is None else min(y, last_y - gap) r = runs_by_init[init] ax.annotate(LABELS[init], (r["tokens"][-1], y), xytext=(4, (ly - y) / (hi - lo) * 100), textcoords="offset points", fontsize=6.5, color="#0b0b0b", va="center") last_y = ly def fig_curves(runs, budget, freeze, out, title, ylim=None, logx=True): best = best_by_arm(runs, budget, freeze) if not best: return None fig, ax = plt.subplots(figsize=(3.3, 2.3)) for init in ("scratch", "shuffled", "rescaled", "ivgpt"): if init in best: r = best[init] m = r["tokens"] > 0 ax.plot(r["tokens"][m] / 1e6, r["val"][m], STYLES[init], color=COLORS[init], label=f"{LABELS[init]} (lr {r['lr']:g})") if logx: ax.set_xscale("log") if ylim: ax.set_ylim(*ylim) ax.set_xlabel("text tokens (M)") ax.set_ylabel("validation loss (nats/token)") ax.set_title(title) ax.legend(frameon=False, loc="upper right") fig.savefig(out) plt.close(fig) return {k: {"lr": v["lr"], "final": float(v["val"][-1]), "name": v["name"]} for k, v in best.items()} def fig_lr(runs, budget, freeze, out): """Final val loss vs peak LR, one line per arm (seed 0).""" table = defaultdict(dict) fig, ax = plt.subplots(figsize=(3.3, 2.3)) for init in ("scratch", "shuffled", "rescaled", "ivgpt"): pts = sorted((r["lr"], float(r["val"][-1])) for r in runs if r["budget"] == budget and r["freeze"] == freeze and r["init"] == init and r["done"] and r["seed"] == 0) for lr, v in pts: table[init][lr] = v if pts: x, y = zip(*pts) ax.plot(x, y, STYLES[init], marker="o", markersize=3.5, color=COLORS[init], label=LABELS[init]) if not table: plt.close(fig) return None ax.set_xscale("log") ax.set_xlabel("peak learning rate") ax.set_ylabel("final validation loss") ax.set_title(f"{'full fine-tune' if freeze == 'none' else 'frozen body'}, {budget} tokens") ax.legend(frameon=False) fig.savefig(out) plt.close(fig) return dict(table) def fig_multiplier(runs, budget, freeze, out, seeds=False): """Data multiplier: tokens scratch needs / tokens arm needs to reach the same loss.""" groups = defaultdict(list) best = best_by_arm(runs, budget, freeze) if "scratch" not in best: return None for r in runs: if r["budget"] == budget and r["freeze"] == freeze and r["done"] and r["lr"] == best.get(r["init"], {}).get("lr"): groups[r["init"]].append(r) scratch_runs = groups["scratch"] lo = max(min(r["val"].min() for rs in groups.values() for r in rs), 0) hi = min(r["val"][1] for rs in groups.values() for r in rs) targets = np.linspace(hi, lo + 0.02, 40) fig, ax = plt.subplots(figsize=(3.3, 2.3)) summary = {} for init in ("shuffled", "rescaled", "ivgpt"): if init not in groups: continue ratios = [] for a in groups[init]: for s in scratch_runs: row = [] for tg in targets: ta, ts = tokens_to_reach(a, tg), tokens_to_reach(s, tg) row.append(ts / ta if ta and ts else np.nan) ratios.append(row) ratios = np.array(ratios) med = np.nanmedian(ratios, axis=0) ax.plot(targets, med, STYLES[init], color=COLORS[init], label=LABELS[init]) if len(ratios) > 1: ax.fill_between(targets, np.nanmin(ratios, 0), np.nanmax(ratios, 0), color=COLORS[init], alpha=0.15, lw=0) summary[init] = {f"{t:.2f}": float(m) for t, m in zip(targets[::8], med[::8])} ax.axhline(1.0, color="#52514e", lw=0.8) ax.invert_xaxis() ax.set_xlabel("target validation loss (lower = later in training)") ax.set_ylabel("data multiplier vs scratch") ax.set_title(f"tokens(scratch) / tokens(arm), {budget}") ax.legend(frameon=False) fig.savefig(out) plt.close(fig) return summary def fig_delta(runs, budget, freeze, out): """Val loss minus best-LR scratch (seed 0) at matched eval steps; seeds at each arm's best LR, mean +/- range.""" best = best_by_arm(runs, budget, freeze) if "scratch" not in best: return None ref = dict(zip(best["scratch"]["tokens"], best["scratch"]["val"])) fig, ax = plt.subplots(figsize=(3.3, 2.3)) summary = {} for init in ("scratch", "shuffled", "rescaled", "ivgpt"): if init not in best: continue reps = [r for r in runs if r["init"] == init and r["budget"] == budget and r["freeze"] == freeze and r["lr"] == best[init]["lr"] and r["done"]] toks = sorted(set.intersection(*[set(r["tokens"]) for r in reps]) & set(ref)) toks = [t for t in toks if t > 0] d = np.array([[dict(zip(r["tokens"], r["val"]))[t] - ref[t] for t in toks] for r in reps]) x = np.array(toks) / 1e6 ax.plot(x, d.mean(0), STYLES[init], color=COLORS[init], label=f"{LABELS[init]} (n={len(reps)})") if len(reps) > 1: ax.fill_between(x, d.min(0), d.max(0), color=COLORS[init], alpha=0.18, lw=0) summary[init] = {"n": len(reps), "final_mean": float(d[:, -1].mean()), "final_min": float(d[:, -1].min()), "final_max": float(d[:, -1].max()), "lr": best[init]["lr"]} ax.axhline(0.0, color="#52514e", lw=0.8) ax.set_xscale("log") ax.set_xlabel("text tokens (M)") ax.set_ylabel("val loss minus scratch (nats/token)") ax.set_title(f"relative to best-LR scratch, {budget}") ax.legend(frameon=False, fontsize=6.5) fig.savefig(out) plt.close(fig) return summary def main(): p = argparse.ArgumentParser() p.add_argument("--roots", nargs="+", default=["results/ice", "results/dm_backup"]) p.add_argument("--out", default="report/figs") args = p.parse_args() out = Path(args.out) out.mkdir(parents=True, exist_ok=True) runs = load_runs(args.roots) s = {"runs": [{k: (v if not isinstance(v, np.ndarray) else None) for k, v in r.items()} | {"final_val": float(r["val"][-1]), "final_tokens": float(r["tokens"][-1])} for r in runs]} budgets = sorted({r["budget"] for r in runs}, key=lambda b: int(b[:-1])) for b in budgets: s[f"best_full_{b}"] = fig_curves(runs, b, "none", out / f"curves_full_{b}.pdf", f"full fine-tune, best LR per arm ({b})") s[f"best_frozen_{b}"] = fig_curves(runs, b, "body", out / f"curves_frozen_{b}.pdf", f"frozen body, best LR per arm ({b})") s[f"lr_full_{b}"] = fig_lr(runs, b, "none", out / f"lr_full_{b}.pdf") s[f"lr_frozen_{b}"] = fig_lr(runs, b, "body", out / f"lr_frozen_{b}.pdf") s[f"mult_full_{b}"] = fig_multiplier(runs, b, "none", out / f"multiplier_full_{b}.pdf") s[f"mult_frozen_{b}"] = fig_multiplier(runs, b, "body", out / f"multiplier_frozen_{b}.pdf") s[f"delta_full_{b}"] = fig_delta(runs, b, "none", out / f"delta_full_{b}.pdf") s[f"delta_frozen_{b}"] = fig_delta(runs, b, "body", out / f"delta_frozen_{b}.pdf") (out.parent / "summary.json").write_text(json.dumps(s, indent=1, default=str)) for r in sorted(runs, key=lambda r: r["name"]): print(f"{r['name']:45s} {'done' if r['done'] else 'run '} {r['tokens'][-1] / 1e6:>7.0f}M " f"val={r['val'][-1]:.3f} tok/s={r['tok_per_s']}") if __name__ == "__main__": main()