unparallelled commited on
Commit
aa3a64c
·
verified ·
1 Parent(s): b3b21cc

Upload scripts/mk_mix64k_plots.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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())