wuff-mann commited on
Commit
28532d1
·
verified ·
1 Parent(s): 2979836

Archive CIDM-v2 reproducible b2_r12_dual_precision_chain.py

Browse files
cidm_v2/source/B2_R12_P0_FIX1/b2_r12_dual_precision_chain.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CIDM-v2 B2-R12/P0: frozen FP32-vs-FP64 full-chain autograd integrity audit.
2
+
3
+ Uses the ORIGINAL CIDM-v2 and B2-R6 weights, 2 TRAIN windows, and B2-R10
4
+ credit objective. No optimizer or future-label predictor input. No attempt
5
+ is made to interpret FP64 as improved deployed forecast accuracy.
6
+ """
7
+ from __future__ import annotations
8
+ import copy, csv, json, math, time, zipfile
9
+ from pathlib import Path
10
+ import torch
11
+ import b2_r11_backward_attribution as r11
12
+ from b2_r1_flux_control import check_zip
13
+
14
+ ARCHIVE_FIX1_SHA = 'b37e0539102afb4047028db72b8c93f32305f81adbd42551e0e3fcedb0eb9a6d'
15
+
16
+ def norm(t):
17
+ return float(torch.linalg.vector_norm(t.detach().reshape(-1).double()))
18
+
19
+ def catparam(params):
20
+ return torch.cat([(p.grad.detach().reshape(-1) if p.grad is not None
21
+ else torch.zeros_like(p).reshape(-1)) for p in params]).double()
22
+
23
+ def finite(t,name):
24
+ if not bool(torch.isfinite(t).all()): raise RuntimeError('NONFINITE_'+name)
25
+
26
+ def write_csv(dest,rows):
27
+ if not rows: return
28
+ dest=Path(dest)
29
+ with dest.open('w',newline='',encoding='utf-8') as f:
30
+ w=csv.DictWriter(f,fieldnames=list(rows[0])); w.writeheader();w.writerows(rows)
31
+
32
+ def write_json(dest,data):
33
+ Path(dest).write_text(json.dumps(data,indent=2,ensure_ascii=False,allow_nan=False,default=str),encoding='utf-8')
34
+
35
+ # B2-R12/P0 FIX1 - memory-safe exact per-example streaming and independent local VJP.
36
+ # Intentionally preserves original FP32/FP64 dynamics and two-TRAIN-window objective.
37
+ import os, gc
38
+
39
+ def _sample_precision_pass(core, head, h, y, sd, Q, dtype, task, kind, precision, sample_index):
40
+ """Full BPTT for *one* original TRAIN window; free whole DAG before local VJPs.
41
+
42
+ Loss is the same per-example mean reduction used by the original batch=2 run.
43
+ Caller averages full head gradients and time components over the same 2 windows.
44
+ """
45
+ params=tuple(head.parameters())
46
+ h=h.to(dtype=dtype,device=next(core.parameters()).device)
47
+ y=y.to(dtype=dtype,device=h.device)
48
+ sd=sd.to(dtype=dtype,device=h.device)
49
+ q=Q.to(dtype=dtype,device=h.device) if Q is not None else None
50
+ pred,loss,recs=r11.full_forward(core,head,h,y,sd,q)
51
+ finite(pred,f'{task}_{kind}_{precision}_sample{sample_index}_prediction')
52
+ finite(loss,f'{task}_{kind}_{precision}_sample{sample_index}_loss')
53
+ extra=[]
54
+ for rec in recs:
55
+ extra.extend((rec['gates'],rec['base'],rec['f'],rec['l'],rec['s'],rec['z_old']))
56
+ # ONE backward through entire trajectory, no retain_graph=True. Extract all
57
+ # necessary adjoints in the SAME autograd invocation before graph release.
58
+ req=params+tuple(extra)
59
+ vals=torch.autograd.grad(loss,req,allow_unused=True,retain_graph=False)
60
+ pvals=vals[:len(params)]; adjvals=vals[len(params):]
61
+ full=torch.cat([torch.zeros_like(p).reshape(-1).double() if v is None else v.detach().reshape(-1).double()
62
+ for p,v in zip(params,pvals)]).cpu()
63
+ finite(full,f'{task}_{kind}_{precision}_sample{sample_index}_head_grad')
64
+ per_step=[]
65
+ for j,rec in enumerate(recs):
66
+ gate_adj,base_adj,f_adj,l_adj,s_adj,z_adj=adjvals[j*6:(j+1)*6]
67
+ if any(v is None for v in (gate_adj,base_adj,f_adj,l_adj,s_adj)):
68
+ raise RuntimeError(f'MISSING_COTANGENT {task} {kind} {precision} sample={sample_index} lead={j+1}')
69
+ for label,v in [('gate',gate_adj),('base',base_adj),('flux',f_adj),('local',l_adj),('skew',s_adj)]:
70
+ finite(v,f'{task}_{kind}_{precision}_{label}_sample{sample_index}_lead{j+1}')
71
+ # local frozen HEAD replay with detached causal features. No full-trajectory graph.
72
+ g2=head(rec['base'].detach(),rec['prev_old'].detach(),rec['initial'].detach(),rec['amps'].detach(),j+1)
73
+ replay=float((g2.detach()-rec['gates'].detach()).abs().max())
74
+ if replay>(2e-6 if dtype==torch.float32 else 2e-11):
75
+ raise RuntimeError(f'HEAD_REPLAY_MISMATCH {task} {kind} {precision} sample={sample_index} lead={j+1} replay={replay}')
76
+ pp=torch.autograd.grad(g2,params,grad_outputs=gate_adj.detach(),allow_unused=True)
77
+ p=torch.cat([(torch.zeros_like(a).flatten() if v is None else v.detach().flatten()).double()
78
+ for a,v in zip(params,pp)]).cpu()
79
+ finite(p,f'{task}_{kind}_{precision}_sample{sample_index}_part_{j+1}')
80
+ # Independently reconstruct LOCAL VJPs with exact frozen operators and
81
+ # original cotangents. In particular, don't hold 12-step graph for VJPs.
82
+ z=rec['z_old'].detach().requires_grad_(True)
83
+ mm=core.model;coeff=mm.coefficients();skew=mm.skew-mm.skew.T
84
+ f2=coeff['flux']*mm.flux(z)
85
+ vf=torch.autograd.grad(f2,z,grad_outputs=f_adj.detach())[0]
86
+ l2=coeff['local']*torch.tanh(mm.local(z))
87
+ vl=torch.autograd.grad(l2,z,grad_outputs=l_adj.detach())[0]
88
+ s2=.035*torch.einsum('ij,bjhw->bihw',skew,z)
89
+ vs=torch.autograd.grad(s2,z,grad_outputs=s_adj.detach())[0]
90
+ sq=lambda v:norm(v)**2
91
+ per_step.append({'parameter_part':p,'gate_adj_sq':sq(gate_adj),'identity_sq':sq(base_adj),
92
+ 'flux_sq':sq(vf),'local_sq':sq(vl),'skew_sq':sq(vs),
93
+ 'latent_sq':(sq(z_adj) if z_adj is not None else None),
94
+ 'replay':replay})
95
+ del g2,pp,p,z,f2,l2,s2,vf,vl,vs
96
+ out={'loss':float(loss.detach()),'full':full,'pred':pred.detach().cpu().double(), 'steps':per_step}
97
+ del recs, vals,adjvals,extra,loss,pred
98
+ gc.collect()
99
+ if h.device.type=='cuda':torch.cuda.empty_cache()
100
+ return out
101
+
102
+
103
+ def numerical_pass(core,head,h,y,sd,Q,precision,task,kind):
104
+ """Identical 2-window objective, streamed over original TRAIN windows 0/1.
105
+
106
+ Shared **CPU** backend by default makes FP32-vs-FP64 comparisons unconfounded
107
+ by different CUDA vs CPU convolution implementations. A user may opt into
108
+ CUDA for BOTH dtypes via CIDM_B2R12_FIX1_COMMON_DEVICE=cuda, but the script
109
+ refuses it for GPUs with <32 GiB of total memory, to avoid old 14.6GiB OOM.
110
+ """
111
+ common=os.environ.get('CIDM_B2R12_FIX1_COMMON_DEVICE','cpu').strip().lower()
112
+ if common not in ('cpu','cuda'):raise RuntimeError('COMMON_BACKEND_MUST_BE_CPU_OR_CUDA')
113
+ if common=='cuda':
114
+ if not torch.cuda.is_available() or torch.cuda.get_device_properties(0).total_memory < 32*1024**3:
115
+ raise RuntimeError('COMMON_CUDA_BACKEND_REQUIRES_AT_LEAST_32G_GPU_USE_CPU_INSTEAD')
116
+ dev=torch.device(common)
117
+ dtype=torch.float32 if precision=='fp32' else torch.float64
118
+ # Copy from CPU first: never clone an entire CUDA model before transferring.
119
+ core=copy.deepcopy(core.to('cpu')).to(device=dev,dtype=dtype).eval().requires_grad_(False)
120
+ head=copy.deepcopy(head.to('cpu')).to(device=dev,dtype=dtype).eval().requires_grad_(True)
121
+ Q=Q.cpu() if Q is not None else None
122
+ h=h.cpu();y=y.cpu();sd=sd.cpu()
123
+ n=int(h.shape[0]);
124
+ if n<1:raise RuntimeError('NO_TRAIN_WINDOWS')
125
+ accumulator=None;parts=None;sum_perstep=None;loss_acc=0.;preds=[]
126
+ for i in range(n):
127
+ case=_sample_precision_pass(core,head,h[i:i+1],y[i:i+1],sd,Q,dtype,task,kind,precision,i)
128
+ w=1./n
129
+ loss_acc+=w*case['loss'];preds.append(case['pred'])
130
+ accumulator=case['full']*w if accumulator is None else accumulator+case['full']*w
131
+ if parts is None:
132
+ parts=[w*x['parameter_part'] for x in case['steps']]
133
+ sum_perstep=[{k:(x[k]*w*w if k!='replay' else x[k]) for k in ('gate_adj_sq','identity_sq','flux_sq','local_sq','skew_sq','latent_sq','replay')}
134
+ for x in case['steps']]
135
+ else:
136
+ for j,x in enumerate(case['steps']):
137
+ parts[j]+=w*x['parameter_part']
138
+ for k in ('gate_adj_sq','identity_sq','flux_sq','local_sq','skew_sq','latent_sq'):
139
+ if x[k] is not None:sum_perstep[j][k]=(sum_perstep[j][k] or 0)+x[k]*w*w
140
+ sum_perstep[j]['replay']=max(sum_perstep[j]['replay'],x['replay'])
141
+ del case
142
+ gc.collect()
143
+ if dev.type=='cuda':torch.cuda.empty_cache()
144
+ pred=torch.cat(preds,dim=0)
145
+ full=accumulator
146
+ gnorm=norm(full)
147
+ recon=torch.stack(parts).sum(dim=0)
148
+ rel=norm(recon-full)/max(gnorm,1e-30)
149
+ steps=[]
150
+ for t,(p,v) in enumerate(zip(parts,sum_perstep),start=1):
151
+ get=lambda key:math.sqrt(v[key]) if v[key] is not None else None
152
+ a=get('identity_sq');b=get('flux_sq')
153
+ steps.append({'task':task,'controller':kind,'precision':precision,'lead':t,
154
+ 'parameter_part_l2':norm(p),'gate_adjoint_l2':get('gate_adj_sq'),
155
+ 'identity_vjp_l2':a,'flux_vjp_l2':b,'local_vjp_l2':get('local_sq'),
156
+ 'skew_vjp_l2':get('skew_sq'),
157
+ 'flux_to_identity_vjp_ratio':b/max(a,1e-30),
158
+ 'latent_adjoint_l2':get('latent_sq'),
159
+ 'local_gate_replay_max_abs':v['replay']})
160
+ summary={'task':task,'controller':kind,'precision':precision,
161
+ 'loss':loss_acc,'full_gradient_l2_fp64_reduce':gnorm,
162
+ 'full_gradient_max_abs':float(full.abs().max()),
163
+ 'partition_relative_error':rel,'partition_abs_residual_l2':norm(recon-full),
164
+ 'cancellation_condition':sum(norm(x) for x in parts)/max(norm(recon),1e-30),
165
+ 'first_step_flux_vjp_l2':steps[0]['flux_vjp_l2'],
166
+ 'step1_parameter_part_l2':steps[0]['parameter_part_l2'],
167
+ 'full_gradient_is_elementwise_finite':True,
168
+ 'head_partition_strict_pass':rel<=1e-4,
169
+ 'common_backend':common, 'streamed_train_windows':n,
170
+ 'graph_retention':False,
171
+ 'scoring_contract':'mean of original TRAIN window 0 and 1 per-window identical 12-step physical loss'}
172
+ return summary,steps,pred,full
173
+
174
+
175
+ def paired_case(task,core,cache,r6zip,device,heads_used=('operator','rotated'),limit=2):
176
+ """All cross-precision cases use SAME engine, weights, TRAIN sample ids and loss.
177
+
178
+ The original archived seed17 checkpoint is validated before this method runs.
179
+ """
180
+ ch=int(cache['splits']['train']['h'].shape[2]);d=int(core.model.skew.shape[0])
181
+ matched='swe_2d' if task=='smoke_2d' else ('burgers_1d' if task=='smoke_1d' else task)
182
+ heads=r11.load_heads(r6zip,matched,ch,'cpu')
183
+ Q=r11.orthogonal_matrix(d,32207,'cpu',torch.float32)
184
+ w=r11.Windows(cache['splits']['train']['h'],cache['splits']['train']['y'],cache['mean'],cache['std'])
185
+ ids=list(range(min(limit,len(w))));examples=[w[i] for i in ids]
186
+ h=torch.stack([a for a,b in examples]).cpu()
187
+ y=torch.stack([b for a,b in examples]).cpu()
188
+ sd=w.sd.cpu()
189
+ group=[];detail=[]
190
+ core=core.cpu()
191
+ for kind in heads_used:
192
+ head=heads[kind].cpu();q=Q if kind=='rotated' else None
193
+ a,rows_a,pa,ga=numerical_pass(core,head,h,y,sd,q,'fp32',task,kind)
194
+ b,rows_b,pb,gb=numerical_pass(core,head,h,y,sd,q,'fp64',task,kind)
195
+ div=float((pa-pb).abs().max())
196
+ reldiv=norm(pa-pb)/max(norm(pb),1e-30)
197
+ gcval=float(torch.dot(ga,gb)/(torch.linalg.vector_norm(ga)*torch.linalg.vector_norm(gb)).clamp_min(1e-300)) if norm(ga)>0 and norm(gb)>0 else 0.
198
+ grel=norm(ga-gb)/max(norm(gb),1e-30)
199
+ ratio=norm(ga)/max(norm(gb),1e-30)
200
+ for row in (a,b):
201
+ row.update({'forward_max_abs_fp32_vs_fp64':div,'forward_relative_l2_fp32_vs_fp64':reldiv,
202
+ 'gradient_cosine_fp32_vs_fp64':gcval,
203
+ 'gradient_relative_l2_fp32_vs_fp64':grel,
204
+ 'gradient_norm_ratio_fp32_over_fp64':ratio,
205
+ 'strict_partition_gate_1e_4':1e-4})
206
+ group.extend((a,b));detail.extend(rows_a+rows_b)
207
+ del pa,pb,ga,gb
208
+ gc.collect()
209
+ return group,detail
210
+
211
+
212
+ def run(parent,r6zip,r9zip,fix2zip,r11fix1zip,work,output,smoke=False,device='cpu'):
213
+ output=Path(output);work=Path(work)
214
+ output.mkdir(parents=True,exist_ok=True);work.mkdir(parents=True,exist_ok=True)
215
+ if r11.sha(r11fix1zip)!=ARCHIVE_FIX1_SHA:raise RuntimeError('R11_FIX1_UPLOADED_EVIDENCE_SHA_MISMATCH')
216
+ with zipfile.ZipFile(r11fix1zip) as z:
217
+ if z.testzip(): raise RuntimeError('R11_FIX1_EVIDENCE_CRC_ERROR')
218
+ rep=json.loads(z.read('report.json'));gate=json.loads(z.read('gradient_path_gate.json'))
219
+ if rep['status']!='FROZEN_ATTRIBUTION_DATA_COLLECTED_NUMERIC_GATE_FAIL_NO_TRAINING' or rep['temporal_rows']!=72 or gate['strict_numerical_gate_pass']:
220
+ raise RuntimeError('R11_FIX1_EVIDENCE_GATE_CONTRACT_MISMATCH')
221
+ if r11.sha(fix2zip)!=r11.PREV_FIX2_SHA:raise RuntimeError('R10_FIX2_EVIDENCE_SHA_MISMATCH')
222
+ r11.credit.validate_r6(r6zip);r11.credit.validate_r9(r9zip)
223
+ sums=[];steps=[];cases=[]
224
+ try:
225
+ if smoke:
226
+ cfg=r11.Config(profile='smoke')
227
+ for task,kind in [('smoke_1d','operator'),('smoke_2d','rotated')]:
228
+ cache=r11.make_synthetic_cache(cfg)
229
+ for split in ('train','val','test'):
230
+ if task=='smoke_1d':
231
+ cache['splits'][split]['h']=cache['splits'][split]['h'].mean(dim=-2)[...,:4]
232
+ cache['splits'][split]['y']=cache['splits'][split]['y'].mean(dim=-2)[...,:4]
233
+ else:
234
+ cache['splits'][split]['h']=cache['splits'][split]['h'][...,:2,:4]
235
+ cache['splits'][split]['y']=cache['splits'][split]['y'][...,:2,:4]
236
+ core=r11.UnifiedModel('V2_Ported_R5',1,8,12,4).to(device)
237
+ s,d=paired_case(task,core,cache,r6zip,device,(kind,),1)
238
+ sums.extend(s);steps.extend(d)
239
+ status='PROGRAM_ONLY_SMOKE_NO_SCIENCE'
240
+ else:
241
+ if parent is None or r11.sha(parent)!=r11.PARENT_SHA:raise RuntimeError('B0_PARENT_ZIP_SHA_MISMATCH')
242
+ with zipfile.ZipFile(parent) as z:
243
+ manifest,_=check_zip(z);cfg=r11.Config(**manifest['config'])
244
+ for task in r11.TASK_LIST:
245
+ core,cache,replay=r11.make_cache_checked(task,z,cfg,work,device)
246
+ s,d=paired_case(task,core,cache,r6zip,device)
247
+ sums.extend(s);steps.extend(d)
248
+ cases.append({'task':task,'archived_B0_checkpoint_replay':replay,'heads':2})
249
+ write_csv(output/'precision_case_summary.csv',sums)
250
+ write_csv(output/'temporal_vjp_gain.csv',steps)
251
+ write_json(output/'progress.json',{'tasks_completed':[v['task'] for v in cases]})
252
+ if len(sums)!=12 or len(steps)!=144 or len(cases)!=3:raise RuntimeError('INCOMPLETE_12_CASES_OR_144_STEPS')
253
+ status='FROZEN_DUAL_PRECISION_FIX1_COMPLETED_NO_TRAINING'
254
+ except Exception as e:
255
+ write_csv(output/'precision_case_summary.csv',sums);write_csv(output/'temporal_vjp_gain.csv',steps)
256
+ write_json(output/'failure_details.json',{'exception':repr(e),'cases_completed':cases})
257
+ raise
258
+ write_csv(output/'precision_case_summary.csv',sums);write_csv(output/'temporal_vjp_gain.csv',steps)
259
+ unresolved=[d for d in sums if not d['head_partition_strict_pass']]
260
+ swe=[d for d in sums if d['task']=='swe_2d']
261
+ gate={'experiment':'B2_R12_P0_FIX1','status':'DIAGNOSTIC_ONLY_NO_TRAINING',
262
+ 'strict_partition_pass_all':len(unresolved)==0,
263
+ 'unresolved':unresolved,'swe_precision_cases':swe,
264
+ 'train_allowed':False,
265
+ 'note':'FP64 full trajectory replay changes numerical precision; divergent FP32/FP64 forecasts cannot be compared as identical-path gradients. Branch VJP ratios are local cotangent amplification, NOT Jacobian spectral norms.'}
266
+ write_json(output/'precision_gate.json',gate)
267
+ rep={'experiment':'CIDM_v2_B2_R12_P0_FIX1_MemoryBoundedDualPrecision',
268
+ 'status':status,'case_rows':len(sums),'time_rows':len(steps),'cases':cases,
269
+ 'prior_R11_FIX1_SHA256':ARCHIVE_FIX1_SHA,'original_parent_frozen':True,
270
+ 'updates':0,'train_allowed':False,'numeric_gate':gate['strict_partition_pass_all'],
271
+ 'not_independent_external_holdout':True}
272
+ write_json(output/'report.json',rep)
273
+ return rep