Upload scripts/mk_mix64k_plots.py with huggingface_hub
Browse files- scripts/mk_mix64k_plots.py +209 -0
scripts/mk_mix64k_plots.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
mk_mix64k_plots.py -- plots for the mixed-corpus 64k run, built from whatever
|
| 4 |
+
eval cells exist so far. Safe to run repeatedly: it regenerates as more land.
|
| 5 |
+
|
| 6 |
+
python3 mk_mix64k_plots.py [--tag 1200] [--out mix64k_results]
|
| 7 |
+
|
| 8 |
+
THE COMPARISON THIS RUN EXISTS TO MAKE
|
| 9 |
+
parent ext64k step 695 67/65/52/29 <- the starting point of BOTH runs
|
| 10 |
+
pure-b5 cont64k 1651 45/17/ 2/ 1 <- 100% b5: collapsed, cliff at step 1199
|
| 11 |
+
mix this run ? <- 50% b5 / 30% b3 / 20% b1, same parent,
|
| 12 |
+
same schedule, same window
|
| 13 |
+
Only the data differs. The loss panel overlays both runs on the same axis, so a
|
| 14 |
+
cliff in the mix run would sit exactly where the pure-b5 one fell.
|
| 15 |
+
|
| 16 |
+
Numbers come from summary.csv / depth_summary.csv, never from prose.
|
| 17 |
+
"""
|
| 18 |
+
import argparse, csv, glob, json, math, os, re, sys
|
| 19 |
+
import matplotlib
|
| 20 |
+
matplotlib.use("Agg")
|
| 21 |
+
import matplotlib.pyplot as plt
|
| 22 |
+
from matplotlib.ticker import FuncFormatter
|
| 23 |
+
|
| 24 |
+
S = {"surface":"#fcfcfb","ink":"#0b0b0b","ink2":"#52514e","grid":"#e4e4e1"}
|
| 25 |
+
C = {"parent":"#eb6834","pure":"#b3243b","mix":"#12508f","warn":"#eda100","ok":"#7a9a3a"}
|
| 26 |
+
LENS = [8192, 16360, 32744, 65512]
|
| 27 |
+
|
| 28 |
+
def wilson(p, n=100):
|
| 29 |
+
if not n: return float("nan")
|
| 30 |
+
z=1.96; ph=p/100.0
|
| 31 |
+
return 100.0*z*math.sqrt(ph*(1-ph)/n + z*z/(4*n*n))/(1+z*z/n)
|
| 32 |
+
|
| 33 |
+
def read_summary(path):
|
| 34 |
+
try:
|
| 35 |
+
for r in csv.reader(open(path)):
|
| 36 |
+
if r and r[0].strip()=="niah_single": return float(r[1])
|
| 37 |
+
except Exception: pass
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
def read_depth(path):
|
| 41 |
+
out=[]
|
| 42 |
+
try:
|
| 43 |
+
for r in csv.DictReader(open(path)):
|
| 44 |
+
out.append({"depth":float(r["depth"]), "hits":int(r["hits"]), "n":int(r["n"])})
|
| 45 |
+
except Exception: return []
|
| 46 |
+
return sorted(out, key=lambda d:d["depth"])
|
| 47 |
+
|
| 48 |
+
def loss_rows(path):
|
| 49 |
+
try: t=open(path,errors="ignore").read()
|
| 50 |
+
except Exception: return []
|
| 51 |
+
return [(int(a),float(b)) for a,b in re.findall(r"^step +(\d+) \| loss: ([0-9.]+)", t, re.M)]
|
| 52 |
+
|
| 53 |
+
def roll(rows, k=50):
|
| 54 |
+
xs=[];ys=[]
|
| 55 |
+
d=dict(rows)
|
| 56 |
+
for s,_ in rows:
|
| 57 |
+
w=[d[j] for j in range(s-k+1, s+1) if j in d]
|
| 58 |
+
if len(w)==k: xs.append(s); ys.append(sum(w)/k)
|
| 59 |
+
return xs, ys
|
| 60 |
+
|
| 61 |
+
def style(ax, xl, yl, title=None):
|
| 62 |
+
ax.set_facecolor(S["surface"]); ax.grid(True,color=S["grid"],lw=.8,zorder=0); ax.set_axisbelow(True)
|
| 63 |
+
for k,sp in ax.spines.items(): sp.set_visible(k in ("left","bottom")); sp.set_color(S["grid"])
|
| 64 |
+
ax.tick_params(colors=S["ink2"],labelsize=9)
|
| 65 |
+
ax.set_xlabel(xl,color=S["ink2"],fontsize=10); ax.set_ylabel(yl,color=S["ink2"],fontsize=10)
|
| 66 |
+
if title: ax.set_title(title,color=S["ink"],fontsize=12,loc="left",pad=12)
|
| 67 |
+
|
| 68 |
+
klab = lambda v,_=None: (f"{round(v/1024)}k" if v>=1024 else str(int(v)))
|
| 69 |
+
|
| 70 |
+
def main():
|
| 71 |
+
ap=argparse.ArgumentParser()
|
| 72 |
+
ap.add_argument("--tag", default="1200")
|
| 73 |
+
ap.add_argument("--out", default="mix64k_results")
|
| 74 |
+
a=ap.parse_args()
|
| 75 |
+
R="mix64k_ref"
|
| 76 |
+
|
| 77 |
+
parent={L:read_summary(f"{R}/parent_{L}_summary.csv") for L in LENS}
|
| 78 |
+
pure ={L:read_summary(f"{R}/purec5_{L}_summary.csv") for L in LENS}
|
| 79 |
+
mix ={L:read_summary(f"mix64k_{a.tag}_len_{L}/summary.csv") for L in LENS}
|
| 80 |
+
parent={k:v for k,v in parent.items() if v is not None}
|
| 81 |
+
pure ={k:v for k,v in pure.items() if v is not None}
|
| 82 |
+
mix ={k:v for k,v in mix.items() if v is not None}
|
| 83 |
+
have=sorted(mix)
|
| 84 |
+
print(f" parent {parent}\n pure-b5 {pure}\n mix@{a.tag} {mix}")
|
| 85 |
+
if not mix:
|
| 86 |
+
print(" no mix cells yet -- nothing to plot"); return 1
|
| 87 |
+
|
| 88 |
+
fig = plt.figure(figsize=(11, 17), facecolor=S["surface"])
|
| 89 |
+
gs = fig.add_gridspec(4, 1, hspace=0.34, height_ratios=[1,1,1.25,1])
|
| 90 |
+
|
| 91 |
+
# ---- 1. the three-way comparison -------------------------------------
|
| 92 |
+
ax=fig.add_subplot(gs[0])
|
| 93 |
+
for lab,d,col,mk in (("parent: ext64k step 695 (both runs start here)",parent,C["parent"],"D"),
|
| 94 |
+
("100% b5, step 1651 -- COLLAPSED",pure,C["pure"],"o"),
|
| 95 |
+
(f"MIX 50/30/20, step {a.tag}",mix,C["mix"],"s")):
|
| 96 |
+
if not d: continue
|
| 97 |
+
xs=sorted(d); ys=[d[x] for x in xs]
|
| 98 |
+
ax.errorbar(xs,ys,yerr=[wilson(y) for y in ys],color=col,lw=2.4,capsize=3,
|
| 99 |
+
marker=mk,ms=7,zorder=4,label=lab)
|
| 100 |
+
ax.set_xscale("log",base=2); ax.set_xticks(LENS)
|
| 101 |
+
ax.xaxis.set_major_formatter(FuncFormatter(klab))
|
| 102 |
+
ax.set_ylim(-4,100); ax.axhline(0,color=S["grid"],lw=1)
|
| 103 |
+
style(ax,"evaluation context length (tokens)","niah_single accuracy (%)",
|
| 104 |
+
"Same parent, same schedule, same window -- only the DATA differs")
|
| 105 |
+
ax.legend(frameon=False,fontsize=8.5,labelcolor=S["ink2"],loc="upper right")
|
| 106 |
+
|
| 107 |
+
# ---- 2. loss overlay: does the cliff reappear? ------------------------
|
| 108 |
+
ax=fig.add_subplot(gs[1])
|
| 109 |
+
pr=loss_rows(f"{R}/cont64k_purec5.log"); mr=loss_rows("mix64k_logs/train.log")
|
| 110 |
+
for rows,col,lab in ((pr,C["pure"],"100% b5 (cliffed at step 1199)"),
|
| 111 |
+
(mr,C["mix"],"MIX 50/30/20 (this run)")):
|
| 112 |
+
if not rows: continue
|
| 113 |
+
ax.plot([s for s,_ in rows],[l for _,l in rows],color=col,lw=.5,alpha=.28,zorder=2)
|
| 114 |
+
xs,ys=roll(rows)
|
| 115 |
+
if xs: ax.plot(xs,ys,color=col,lw=2.1,zorder=4,label=lab+" (50-step mean)")
|
| 116 |
+
if pr:
|
| 117 |
+
base=[l for s,l in pr if 200<=s<=700]
|
| 118 |
+
if len(base)>=100:
|
| 119 |
+
b=sum(base)/len(base)
|
| 120 |
+
ax.axhline(b-0.20,color=C["warn"],lw=1.2,ls="--",zorder=3)
|
| 121 |
+
ax.text(20,b-0.17,f"cliff threshold (baseline {b:.2f} - 0.20)",fontsize=8,color="#8a6400")
|
| 122 |
+
ax.axvline(1199,color=C["pure"],lw=1,ls=":",zorder=1)
|
| 123 |
+
ax.text(1205,1.35,"pure-b5 cliff\nonset (1199)",fontsize=8,color=C["pure"])
|
| 124 |
+
ax.set_ylim(1.0,3.4)
|
| 125 |
+
style(ax,"step (from the ext64k-695 parent)","training loss (nats)",
|
| 126 |
+
"A FALLING loss here is the failure signature, not success")
|
| 127 |
+
ax.legend(frameon=False,fontsize=8.5,labelcolor=S["ink2"],loc="lower left")
|
| 128 |
+
|
| 129 |
+
# ---- 3. the whole lineage, forking at ext64k-695 -----------------------
|
| 130 |
+
ax=fig.add_subplot(gs[2])
|
| 131 |
+
import json as _j
|
| 132 |
+
try: stages=_j.load(open(f"{R}/lineage/stages.json"))
|
| 133 |
+
except Exception: stages={}
|
| 134 |
+
ORDER=[("base pretrain",4096,"#8e8e88"),("l2 16k bucket",16384,"#9dc3ea"),
|
| 135 |
+
("ext32k",32768,"#4a8fd4"),("b4 full epoch",32768,"#12508f"),
|
| 136 |
+
("ext64k",65536,C["parent"])]
|
| 137 |
+
off=0; marks=[]
|
| 138 |
+
for name,ctx,col in ORDER:
|
| 139 |
+
st=stages.get(name)
|
| 140 |
+
if not st: continue
|
| 141 |
+
rows=st["rows"]; base=rows[0][0]
|
| 142 |
+
xs=[off+(r[0]-base) for r in rows]; ys=[r[1] for r in rows]
|
| 143 |
+
ax.plot(xs,ys,color=col,lw=.9,alpha=.85,zorder=3,label=f"{name} ({klab(ctx)})")
|
| 144 |
+
marks.append((off,xs[-1],name,col)); off=xs[-1]
|
| 145 |
+
fork=off # both 64k runs start here, from ext64k step 695
|
| 146 |
+
for rows,col,lab,lw in ((pr,C["pure"],"100% b5 -> COLLAPSED",1.3),
|
| 147 |
+
(mr,C["mix"],"MIX 50/30/20 -> this run",1.6)):
|
| 148 |
+
if not rows: continue
|
| 149 |
+
xs=[fork+s_ for s_,_ in rows]; ys=[l for _,l in rows]
|
| 150 |
+
ax.plot(xs,ys,color=col,lw=.5,alpha=.25,zorder=2)
|
| 151 |
+
rx,ry=roll(rows)
|
| 152 |
+
if rx: ax.plot([fork+x for x in rx],ry,color=col,lw=lw,zorder=5,label=lab)
|
| 153 |
+
ax.axvline(fork,color=S["ink2"],lw=1.1,ls="--",alpha=.7,zorder=4)
|
| 154 |
+
ax.annotate("both 64k runs fork here\n(ext64k step 695)",xy=(fork,9),xytext=(fork*0.52,9.2),
|
| 155 |
+
fontsize=8.5,color=S["ink2"],arrowprops=dict(arrowstyle="->",color=S["ink2"],lw=.9))
|
| 156 |
+
ax.set_yscale("log"); ax.set_ylim(0.9,13)
|
| 157 |
+
ax.set_yticks([1,2,3,5,10]); ax.set_yticklabels(["1","2","3","5","10"])
|
| 158 |
+
style(ax,"cumulative training step across the lineage","loss (nats, log)",
|
| 159 |
+
"Every stage this model has been through -- and where the two 64k runs diverge")
|
| 160 |
+
ax.legend(frameon=False,fontsize=7.8,labelcolor=S["ink2"],loc="lower left",ncol=2)
|
| 161 |
+
|
| 162 |
+
# ---- 4. depth profiles ------------------------------------------------
|
| 163 |
+
ax=fig.add_subplot(gs[3])
|
| 164 |
+
shades={8192:"#9dc3ea",16360:"#4a8fd4",32744:"#12508f",65512:"#08306b"}
|
| 165 |
+
for L in have:
|
| 166 |
+
rows=read_depth(f"mix64k_{a.tag}_len_{L}/depth_summary.csv")
|
| 167 |
+
if not rows: continue
|
| 168 |
+
xs=[r["depth"] for r in rows]; ys=[100.0*r["hits"]/r["n"] for r in rows]
|
| 169 |
+
ax.errorbar(xs,ys,yerr=[wilson(y,r["n"]) for y,r in zip(ys,rows)],color=shades.get(L,"#12508f"),
|
| 170 |
+
lw=2.1,capsize=3,marker="s",ms=6,zorder=4,label=f"MIX @ {L:,}")
|
| 171 |
+
pd_=read_depth(f"{R}/purec5_32744_depth_summary.csv")
|
| 172 |
+
if pd_:
|
| 173 |
+
ax.errorbar([r["depth"] for r in pd_],[100.0*r["hits"]/r["n"] for r in pd_],
|
| 174 |
+
color=C["pure"],lw=2.0,ls="--",marker="o",ms=5,zorder=3,
|
| 175 |
+
label="100% b5 @ 32,744 (collapsed: 0/0/0/0/10)")
|
| 176 |
+
ax.set_ylim(-4,104)
|
| 177 |
+
style(ax,"needle depth (0 = window start, 1 = next to the question)","accuracy (%)",
|
| 178 |
+
"Depth profile: a collapsed model retrieves only from the very end")
|
| 179 |
+
ax.legend(frameon=False,fontsize=8.5,labelcolor=S["ink2"],loc="upper left")
|
| 180 |
+
|
| 181 |
+
png=f"{a.out}_{a.tag}.png"
|
| 182 |
+
fig.savefig(png,dpi=130,facecolor=S["surface"],bbox_inches="tight")
|
| 183 |
+
print(f" wrote {png} ({os.path.getsize(png)//1024} KB)")
|
| 184 |
+
|
| 185 |
+
# ---- a self-contained HTML with the numbers ---------------------------
|
| 186 |
+
import base64
|
| 187 |
+
b64=base64.b64encode(open(png,"rb").read()).decode()
|
| 188 |
+
rows_html=""
|
| 189 |
+
for L in LENS:
|
| 190 |
+
p=parent.get(L); u=pure.get(L); m=mix.get(L)
|
| 191 |
+
f=lambda v: "-" if v is None else f"{v:.1f}"
|
| 192 |
+
d="" if (m is None or p is None) else f"{m-p:+.1f}"
|
| 193 |
+
rows_html+=f"<tr><td>{L:,}</td><td>{f(p)}</td><td>{f(u)}</td><td><b>{f(m)}</b></td><td>{d}</td></tr>"
|
| 194 |
+
html=f"""<!doctype html><meta charset=utf-8><title>Mixed-corpus 64k run, step {a.tag}</title>
|
| 195 |
+
<style>body{{max-width:1000px;margin:2rem auto;padding:0 1rem;font:15px/1.6 -apple-system,Segoe UI,Roboto,sans-serif;color:#111;background:#fcfcfb}}
|
| 196 |
+
table{{border-collapse:collapse;margin:1rem 0;font-size:14px}}th,td{{border:1px solid #e4e4e1;padding:.4rem .7rem;text-align:left}}th{{background:#f3f3f1}}
|
| 197 |
+
img{{max-width:100%;border:1px solid #e4e4e1}}code{{background:#f3f3f1;padding:.1rem .3rem;border-radius:3px}}</style>
|
| 198 |
+
<h1>Mixed-corpus 64k extension, step {a.tag}</h1>
|
| 199 |
+
<p>Parent <code>ext64k step 695</code>; 50% b5 / 30% b3 / 20% b1; plateau LR 1.8e-4; cp=4 dp=2 on 8x H200.
|
| 200 |
+
The 100% b5 run from the <b>same parent and schedule</b> cliffed at its step 1199 and ended at 45/17/2/1.</p>
|
| 201 |
+
<table><tr><th>context</th><th>parent (695)</th><th>100% b5 (1651)</th><th>MIX ({a.tag})</th><th>mix - parent</th></tr>{rows_html}</table>
|
| 202 |
+
<img src="data:image/png;base64,{b64}">
|
| 203 |
+
<p style="color:#666;font-size:13px">Generated from <code>mix64k_{a.tag}_len_*/summary.csv</code> and
|
| 204 |
+
<code>mix64k_logs/train.log</code>. Cells showing "-" had not finished when this was built.</p>"""
|
| 205 |
+
hp=f"{a.out}_{a.tag}.html"; open(hp,"w").write(html)
|
| 206 |
+
print(f" wrote {hp} ({os.path.getsize(hp)//1024} KB)")
|
| 207 |
+
return 0
|
| 208 |
+
|
| 209 |
+
if __name__=="__main__": sys.exit(main())
|