wuff-mann commited on
Commit
bfa83d6
·
verified ·
1 Parent(s): b3a961e

Archive CIDM-v2 reproducible b2_r3_readout_probe.py

Browse files
cidm_v2/source/B2_R3_P1/b2_r3_readout_probe.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CIDM-v2 B2-R3: frozen-backbone information-sufficiency pilot.
2
+ Only frozen original models and SMALL matched trainable probes. This is NOT full model retraining.
3
+ Three science outcomes: latent-specific readout gain, observable-only calibration gain, no reliable gain.
4
+ """
5
+ from __future__ import annotations
6
+ import os,io,math,json,hashlib,zipfile,random,copy,csv,time
7
+ from pathlib import Path
8
+ import numpy as np
9
+ import torch
10
+ from torch import nn
11
+ from torch.nn import functional as F
12
+ from benchmark import Config,Windows,fetch_official,make_cache,load_data,assess as canonical_assess,UnifiedModel,make_synthetic_cache
13
+ from b2_r1_flux_control import get_core,check_zip,assess as fixed_assess
14
+
15
+ PARENT_SHA='69552b29e169ec22ee85d9c1b1a678fdb188f6f25dc18f33b927b88f4dddffd4'
16
+ TASKS=('burgers_1d','reacdiff_2d','swe_2d')
17
+ LEADS=(1,3,6,9,12)
18
+ SEED=17
19
+
20
+ def sha(p):
21
+ h=hashlib.sha256()
22
+ with open(p,'rb') as f:
23
+ for t in iter(lambda:f.read(1<<20),b''):h.update(t)
24
+ return h.hexdigest()
25
+
26
+ def js(p,x):
27
+ Path(p).write_text(json.dumps(x,ensure_ascii=False,indent=2,allow_nan=False,default=str),encoding='utf-8')
28
+
29
+ def rows_csv(p,rows):
30
+ if not rows:return
31
+ with open(p,'w',newline='',encoding='utf-8') as f:
32
+ w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
33
+
34
+ def normalize_sample(x):
35
+ scale=x.detach().square().mean(dim=(1,2,3),keepdim=True).sqrt().clamp_min(.1)
36
+ return torch.tanh(x/scale)
37
+
38
+ class ReadoutProbe(nn.Module):
39
+ """Identical trainable parameters and dimensions for OBS and LATENT; source varies only in frozen features."""
40
+ def __init__(self,channels,width=16,hidden=24):
41
+ super().__init__()
42
+ self.head=nn.Sequential(nn.Conv2d(width+3*channels+1,hidden,3,padding=1),nn.SiLU(),
43
+ nn.Conv2d(hidden,channels,1))
44
+ nn.init.zeros_(self.head[-1].weight);nn.init.zeros_(self.head[-1].bias)
45
+ def forward(self,common,projection):
46
+ correction=.25*torch.tanh(self.head(torch.cat([common,projection],dim=1)))
47
+ return correction
48
+
49
+ class FixedProjection:
50
+ def __init__(self,d,ch,width=16,seed=20261014,device='cpu'):
51
+ g=torch.Generator(device='cpu').manual_seed(seed)
52
+ self.p_lat=(torch.randn(width,d,1,1,generator=g)/math.sqrt(d)).to(device)
53
+ self.p_obs=(torch.randn(width,3*ch,1,1,generator=g)/math.sqrt(3*ch)).to(device)
54
+ def feats(self,z,common,channels):
55
+ raw=common[:,:3*channels]
56
+ lat=F.conv2d(normalize_sample(z),self.p_lat)
57
+ obs=F.conv2d(normalize_sample(raw),self.p_obs)
58
+ return lat.detach(),obs.detach()
59
+
60
+ @torch.no_grad()
61
+ def rollout(core,h,leads=LEADS):
62
+ """Exact original R5 dynamics, no correction feedback or future truth in prediction path."""
63
+ mm=core.model
64
+ raised=h.ndim==4
65
+ hh=h.unsqueeze(-2) if raised else h
66
+ B,L,C,H,W=hh.shape
67
+ assert L==mm.hist==4
68
+ coef=mm.coefficients();z=mm.encode(hh.reshape(B,L*C,H,W))
69
+ prev=hh[:,-1];initial=prev; skew=mm.skew-mm.skew.T
70
+ states=[]
71
+ for t in range(1,max(leads)+1):
72
+ z=z+coef['flux']*mm.flux(z)+coef['local']*torch.tanh(mm.local(z))+.035*torch.einsum('ij,bjhw->bihw',skew,z)
73
+ inc=coef['out']*torch.tanh(mm.decode(z));x=prev+inc
74
+ if t in leads:states.append((t,z.detach(),x.detach(),inc.detach(),initial.detach()))
75
+ prev=x
76
+ return states
77
+
78
+ def common_features(x,inc,initial,lead):
79
+ # Values are observational, causally available under frozen baseline rollout.
80
+ B,C,H,W=x.shape
81
+ k=x.new_full((B,1,H,W),float(lead)/12.0)
82
+ return torch.cat([initial.clamp(-5,5),x.clamp(-5,5),inc.clamp(-5,5),k],dim=1).detach()
83
+
84
+ def make_features(states,channels,projection):
85
+ for lead,z,x,inc,initial in states:
86
+ common=common_features(x,inc,initial,lead)
87
+ latent,observable=projection.feats(z,common,channels)
88
+ yield lead,x,common,latent,observable
89
+
90
+ @torch.no_grad()
91
+ def original_parity(core,h):
92
+ states=rollout(core,h,tuple(range(1,13)))
93
+ manual=torch.stack([s[2] for s in states],dim=1)
94
+ model=core(h,12,mode='off')
95
+ if h.ndim==4:manual=manual.squeeze(-2)
96
+ delta=(manual-model).abs().max().item()
97
+ if delta>2e-6:raise RuntimeError('FROZEN_FORWARD_PARITY_MISMATCH:'+str(delta))
98
+ return delta
99
+
100
+ @torch.no_grad()
101
+ def eval_heads(core,heads,projection,ds,indices,device,batch,leads=LEADS):
102
+ core.eval();[v.eval() for v in heads.values()]
103
+ C=ds.h.shape[2];ssd=ds.sd.to(device).reshape(1,-1,1,1)**2
104
+ scores={s:{t:[0.0,0] for t in leads} for s in ['baseline',*heads]}
105
+ for i in range(0,len(indices),batch):
106
+ samples=[ds[int(j)] for j in indices[i:i+batch]]
107
+ h=torch.stack([v[0] for v in samples]).to(device);y=torch.stack([v[1] for v in samples]).to(device)
108
+ if h.ndim==4:y=y.unsqueeze(-2)
109
+ states=rollout(core,h,leads)
110
+ for lead,x,common,lat,obs in make_features(states,C,projection):
111
+ target=y[:,lead-1]
112
+ preds={'baseline':x,'observable':x+heads['observable'](common,obs),
113
+ 'latent':x+heads['latent'](common,lat)}
114
+ for name,pred in preds.items():
115
+ diff=(pred-target).square()*ssd
116
+ scores[name][lead][0]+=diff.sum().item();scores[name][lead][1]+=diff.numel()
117
+ result={s:{str(t):scores[s][t][0]/max(scores[s][t][1],1) for t in leads} for s in scores}
118
+ for name in result:
119
+ result[name]['late_mse']=(sum(scores[name][t][0] for t in [9,12])/max(sum(scores[name][t][1] for t in [9,12]),1))
120
+ result[name]['all_mse']=(sum(scores[name][t][0] for t in leads)/max(sum(scores[name][t][1] for t in leads),1))
121
+ return result
122
+
123
+ def index_plan(n):
124
+ # fixed disjoint halves: val-selection and val-audit. Never test tuning or train on validation.
125
+ if n<12:raise RuntimeError('INSUFFICIENT_VAL_WINDOWS: require >=12 for independent audit split')
126
+ ids=np.arange(n);cut=n//2
127
+ return ids[:cut].tolist(),ids[cut:].tolist()
128
+
129
+ def q0_check(archive,external_diag):
130
+ """Read-only integrity and predeclared indication from actual FIX6 results. Q0 does NOT select a fitted model."""
131
+ if sha(archive)!=PARENT_SHA:raise RuntimeError('B0_PARENT_SHA256_MISMATCH')
132
+ with zipfile.ZipFile(archive) as z:
133
+ m,c=check_zip(z)
134
+ if m['gate']['completed_runs']!=18:raise RuntimeError('INCOMPLETE_PARENT_RUNS')
135
+ import pandas as pd
136
+ data=pd.read_csv(external_diag)
137
+ assert len(data)==45 and len(data[['task','seed','lead']].drop_duplicates())==45
138
+ assert set(data.task)==set(TASKS) and set(data.seed)=={17,41,73} and set(data.lead)==set(LEADS)
139
+ if data[['pred_increment_cos_truth','pred_increment_power_ratio','self_reencode_increment_gap_rel']].isna().any().any():
140
+ raise RuntimeError('FIX6_DIAGNOSTIC_MISSING_OR_NONFINITE')
141
+ l=data[data.lead==12].groupby('task').mean(numeric_only=True)
142
+ evidence={'burgers_bad_direction':bool(l.loc['burgers_1d','pred_increment_cos_truth']<.2),
143
+ 'swe_underresponse':bool(l.loc['swe_2d','pred_increment_power_ratio']<.6),
144
+ 'rd_direction_better_than_burgers':bool(l.loc['reacdiff_2d','pred_increment_cos_truth']>l.loc['burgers_1d','pred_increment_cos_truth']+.2)}
145
+ return {'gate':'PILOT_ALLOWED' if all(evidence.values()) else 'BLOCKED_Q0_MECHANISM_NOT_REPRODUCED',
146
+ 'evidence':evidence,'three_task_lead12':l[['pred_increment_cos_truth','pred_increment_power_ratio','self_reencode_increment_gap_rel']].round(6).to_dict(orient='index'),
147
+ 'source_sha256':sha(external_diag),'parent_sha256':PARENT_SHA}
148
+
149
+ def pilot_one(task,archive,cfg,root,output,updates=100,device='cpu',smoke=False):
150
+ """Small matched-probe TRAINING ONLY, frozen backbone. Each has identical initialization and optimizer budget."""
151
+ out=Path(output)/task;out.mkdir(parents=True,exist_ok=True)
152
+ if smoke:
153
+ cache=make_synthetic_cache(cfg)
154
+ # Synthetic 2D or lifted 1D supported by same logic, NO official result.
155
+ core=UnifiedModel('V2_Ported_R5',1,8,12,4).to(device).eval()
156
+ batch=2
157
+ else:
158
+ with zipfile.ZipFile(archive) as zp:
159
+ m,contract=check_zip(zp)
160
+ path=fetch_official(Path(root)/'official_data',task)
161
+ cache=make_cache(task,path,cfg,Path(root)/'window_cache')
162
+ ref=next(v for v in contract['tasks'] if v['task']==task)
163
+ assert cache['signature']['source_md5']==ref['source_md5']
164
+ assert cache['ids']==ref['ids']
165
+ assert all(len(cache['splits'][s]['h'])==ref['windows'][s] for s in ('train','val','test'))
166
+ core,weight_sha=get_core(zp,cache,task,SEED,device)
167
+ bs=cfg.batch_1d if cache['splits']['test']['h'].ndim==4 else cfg.batch_2d
168
+ ref_score=float(json.loads(zp.read(f'{task}__V2_Ported_R5__seed{SEED}.json'))['test']['model_12'])
169
+ replay=fixed_assess(core,'baseline',cache,'test',device,batch=bs)['rmse12']
170
+ if abs(replay-ref_score)>max(1e-5,ref_score*.001):raise RuntimeError(f'BASELINE_REPLAY_FAIL_CLOSED {task} {replay} != {ref_score}')
171
+ js(out/'parent_replay.json',{'task':task,'seed':SEED,'checkpoint_sha256':weight_sha,'archive_test_rmse12':ref_score,'replay_test_rmse12':replay,'pass':True})
172
+ print('[BASELINE REPLAY PASS]',task,SEED,ref_score,flush=True)
173
+ batch=cfg.batch_1d if cache['splits']['train']['h'].ndim==4 else cfg.batch_2d
174
+ core.requires_grad_(False);core.eval()
175
+ train=Windows(cache['splits']['train']['h'],cache['splits']['train']['y'],cache['mean'],cache['std'])
176
+ val=Windows(cache['splits']['val']['h'],cache['splits']['val']['y'],cache['mean'],cache['std'])
177
+ if smoke:sel_ids,audit_ids=[0,1],[2,3]
178
+ else:sel_ids,audit_ids=index_plan(len(val))
179
+ C=int(cache['splits']['train']['h'].shape[2]);d=core.model.flux.net[-1].out_channels
180
+ proj=FixedProjection(d,C,seed=20261014,device=device)
181
+ torch.manual_seed(20261014);head_obs=ReadoutProbe(C).to(device)
182
+ torch.manual_seed(20261014);head_lat=ReadoutProbe(C).to(device)
183
+ heads={'observable':head_obs,'latent':head_lat}
184
+ assert sum(p.numel() for p in head_obs.parameters())==sum(p.numel() for p in head_lat.parameters())
185
+ opts={s:torch.optim.AdamW(heads[s].parameters(),lr=8e-4,weight_decay=1e-4) for s in heads}
186
+ g=torch.Generator(device='cpu').manual_seed(20261014+SEED)
187
+ rows=[];best={'observable':(float('inf'),None,0),'latent':(float('inf'),None,0)}
188
+ with torch.no_grad():
189
+ smoke_x=torch.stack([train[0][0],train[1][0]]).to(device)
190
+ parity=original_parity(core,smoke_x)
191
+ print('[TRAIN PRECHECK]',task,'orig parity',parity,'readout params',sum(p.numel() for p in head_obs.parameters()),flush=True)
192
+ checkpoints=[0,updates] if smoke else sorted(set([0,*range(20,updates+1,20),updates]))
193
+ def evaluate_at(step):
194
+ v=eval_heads(core,heads,proj,val,sel_ids,device,batch)
195
+ for s in heads:
196
+ mse=v[s]['late_mse']
197
+ if mse<best[s][0]:best[s]=(mse,copy.deepcopy(heads[s].state_dict()),step)
198
+ r={'task':task,'step':step,'baseline_late':v['baseline']['late_mse'],
199
+ 'observable_late':v['observable']['late_mse'],'latent_late':v['latent']['late_mse'],
200
+ 'latent_vs_observable_ratio':v['latent']['late_mse']/max(v['observable']['late_mse'],1e-20)}
201
+ rows.append(r);rows_csv(out/'pilot_curve.csv',rows)
202
+ print('[PILOT]',task,'step',step,'baseline',round(r['baseline_late'],8),'obs',round(r['observable_late'],8),'latent',round(r['latent_late'],8),flush=True)
203
+ evaluate_at(0)
204
+ for step in range(1,updates+1):
205
+ ids=torch.randint(0,len(train),(batch,),generator=g).tolist()
206
+ samples=[train[i] for i in ids]
207
+ h=torch.stack([v[0] for v in samples]).to(device);y=torch.stack([v[1] for v in samples]).to(device)
208
+ if h.ndim==4:y=y.unsqueeze(-2)
209
+ with torch.no_grad():states=rollout(core,h)
210
+ for o in opts.values():o.zero_grad(set_to_none=True)
211
+ loss={s:0.0 for s in heads}
212
+ for lead,x,common,lat,obs in make_features(states,C,proj):
213
+ target=y[:,lead-1]
214
+ for s,feature in (('observable',obs),('latent',lat)):
215
+ estimate=x+heads[s](common,feature)
216
+ loss[s]=loss[s]+(estimate-target).square().mean()/len(LEADS)
217
+ for s in heads:
218
+ if not torch.isfinite(loss[s]):raise RuntimeError('NONFINITE_TRAINING_LOSS:'+task+':'+s)
219
+ loss[s].backward();torch.nn.utils.clip_grad_norm_(heads[s].parameters(),1.)
220
+ opts[s].step()
221
+ if step in checkpoints:
222
+ evaluate_at(step)
223
+ js(out/'progress.json',{'task':task,'updates_completed':step,'best':{k:{'selection_val_mse':v[0],'best_step':v[2]} for k,v in best.items()},'time':time.time()})
224
+ # Independent audit split: never used for step selection or learning.
225
+ for s in heads:
226
+ assert best[s][1] is not None
227
+ heads[s].load_state_dict(best[s][1]);heads[s].eval()
228
+ audit=eval_heads(core,heads,proj,val,audit_ids,device,batch)
229
+ # Readout trained from train only, step selected from first validation half; second half untouched.
230
+ summary={'task':task,'seed':SEED,'updates':updates,'core_frozen':True,'matched_probe_params':sum(p.numel() for p in heads['latent'].parameters()),
231
+ 'selection_windows':len(sel_ids),'audit_windows':len(audit_ids),'best_step':{s:best[s][2] for s in best},
232
+ 'audit':audit,'late_ratios':{s:audit[s]['late_mse']/max(audit['baseline']['late_mse'],1e-20) for s in heads},
233
+ 'latent_vs_observable_late_ratio':audit['latent']['late_mse']/max(audit['observable']['late_mse'],1e-20),
234
+ 'qualifier':'SMOKE_NO_SCIENCE' if smoke else 'SINGLE_SEED_READOUT_PILOT_ONLY'}
235
+ js(out/'pilot_summary.json',summary)
236
+ for s in heads:
237
+ torch.save({'head':heads[s].state_dict(),'task':task,'seed':SEED,'probe_type':s,'best_step':best[s][2],
238
+ 'original_backbone_frozen':True,'selection_mse':best[s][0],
239
+ 'fixed_projection_seed':20261014},out/f'{s}_best_probe.pt')
240
+ return summary
241
+
242
+ def run(parent,diagnostic_csv,root,output,updates=100,smoke=False):
243
+ out=Path(output);out.mkdir(parents=True,exist_ok=True)
244
+ if smoke:
245
+ cfg=Config(profile='smoke',history=4,train_horizon=6,eval_horizon=12)
246
+ results=[pilot_one('smoke_2d',None,cfg,root,out,updates=2,smoke=True)]
247
+ status='PROGRAM_ONLY_SMOKE_NO_SCIENTIFIC_CLAIM'
248
+ q0={'gate':'SMOKE'}
249
+ else:
250
+ q0=q0_check(parent,diagnostic_csv)
251
+ js(out/'Q0_frozen_gate.json',q0)
252
+ if q0['gate']!='PILOT_ALLOWED':
253
+ status='Q0_FAIL_NO_PROBE_TRAINING'; results=[]
254
+ else:
255
+ with zipfile.ZipFile(parent) as zp:
256
+ manifest,_=check_zip(zp)
257
+ cfg=Config(**manifest['config'])
258
+ results=[]
259
+ for task in TASKS:
260
+ results.append(pilot_one(task,parent,cfg,root,out,updates=updates,device='cuda' if torch.cuda.is_available() else 'cpu'))
261
+ status='SINGLE_SEED_PILOT_COMPLETED_NOT_FULL_TRAINING'
262
+ science={'experiment':'CIDM-v2 B2-R3/P1 FROZEN BACKBONE READOUT SUFFICIENCY PILOT',
263
+ 'status':status,'parent_hash':PARENT_SHA,'trained_backbone_steps':0,
264
+ 'pilot_probe_updates_per_head_per_task':updates if not smoke and results else 0,
265
+ 'task_seed_count':len(results),'results':results,'q0':q0,
266
+ 'qualification':'NO_CIDM_V2_FULL_MODEL_QUALIFICATION',
267
+ 'evidence_limits':['Readout probes are trainable small models; results are not frozen counterfactuals.',
268
+ 'Seed17 only; no multi-seed confirmation or formal v1 comparison.',
269
+ 'Validation halves used separately for checkpoint selection and independent pilot audit.',
270
+ 'Decoder-correction probes do not feed corrections back to backbone, so no closed-loop architecture inference.',
271
+ 'Test labels are not used for training/selection; test split only strict baseline model replay.']}
272
+ if not smoke and len(results)==3:
273
+ rr={x['task']:x for x in results}
274
+ def improve(task):
275
+ x=rr[task]
276
+ return x['late_ratios']['latent']<=.97 and x['latent_vs_observable_late_ratio']<=.98
277
+ gate=improve('burgers_1d') and improve('swe_2d') and rr['reacdiff_2d']['late_ratios']['latent']<=1.02
278
+ science['pilot_gate']='CANDIDATE_FOR_THREE_SEED_REPLICATION' if gate else 'PILOT_NO_UNIFIED_LATENT_SPECIFIC_GAIN'
279
+ science['pilot_gate_policy']={'burgers_and_swe_latent_gain_vs_base_min':.03,
280
+ 'burgers_and_swe_latent_gain_vs_matched_obs_min':.02,
281
+ 'rd_max_regression_vs_base':.02,'pilot_is_not_architecture_gate':True}
282
+ js(out/'report.json',science)
283
+ return science