Archive CIDM-v2 reproducible b2_r1_flux_control.py
Browse files
cidm_v2/source/B2_R1_FIX2/b2_r1_flux_control.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CIDM-v2 B2/R1: model-free flux stability qualification, then matched tiny training.
|
| 2 |
+
No new parameters; no test-driven threshold tuning. Source remains B0-R1 archived v1 + PDE-ported v2.
|
| 3 |
+
"""
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
import os, io, json, math, zipfile, hashlib, random, time, copy, csv
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from dataclasses import asdict
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
from benchmark import Config, TASKS, TASKS as ALL_TASKS, fetch_official, make_cache, Windows, UnifiedModel, ARCHIVE_SHA, sha, seed_all, load_data, assess as canonical_assess
|
| 13 |
+
|
| 14 |
+
TASKLIST=('burgers_1d','reacdiff_2d','swe_2d')
|
| 15 |
+
SEEDS=(17,41,73)
|
| 16 |
+
POLICIES=('baseline','flux_cap2','flux_cap4','flux_cap8','flux_scale075','flux_scale050')
|
| 17 |
+
|
| 18 |
+
def flux_rms(t):
|
| 19 |
+
return t.square().mean(dim=(1,2,3),keepdim=True).clamp_min(1e-20).sqrt()
|
| 20 |
+
|
| 21 |
+
class ControlledV2(nn.Module):
|
| 22 |
+
"""Same pretrained/newly trained v2 parameters, only altered latent Flux time stepping."""
|
| 23 |
+
def __init__(self, core, policy='baseline'):
|
| 24 |
+
super().__init__()
|
| 25 |
+
if policy not in POLICIES: raise ValueError(policy)
|
| 26 |
+
self.core=core
|
| 27 |
+
self.policy=policy
|
| 28 |
+
def forward(self, h, steps=12, return_trace=False):
|
| 29 |
+
is1d=h.ndim==4
|
| 30 |
+
h5=h.unsqueeze(-2) if is1d else h
|
| 31 |
+
mm=self.core.model
|
| 32 |
+
B,L,C,H,W=h5.shape
|
| 33 |
+
buf=list(h5.unbind(1))
|
| 34 |
+
z=mm.encode(h5.reshape(B,L*C,H,W))
|
| 35 |
+
coeff=mm.coefficients()
|
| 36 |
+
skew=mm.skew-mm.skew.T
|
| 37 |
+
f0=coeff['flux']*mm.flux(z)
|
| 38 |
+
initnorm=flux_rms(f0).detach().clamp_min(1e-10)
|
| 39 |
+
ys=[];tr=[]
|
| 40 |
+
for k in range(steps):
|
| 41 |
+
raw=coeff['flux']*mm.flux(z)
|
| 42 |
+
if self.policy.startswith('flux_cap'):
|
| 43 |
+
cap=float(self.policy.replace('flux_cap',''))
|
| 44 |
+
factor=(cap*initnorm/(flux_rms(raw)+1e-12)).clamp(max=1.)
|
| 45 |
+
f=raw*factor
|
| 46 |
+
elif self.policy.startswith('flux_scale'):
|
| 47 |
+
factor=float(self.policy.replace('flux_scale',''))/100.
|
| 48 |
+
f=raw*factor
|
| 49 |
+
else:
|
| 50 |
+
f=raw
|
| 51 |
+
local=coeff['local']*torch.tanh(mm.local(z))
|
| 52 |
+
skew_update=.035*torch.einsum('ij,bjhw->bihw',skew,z)
|
| 53 |
+
z=z+f+local+skew_update
|
| 54 |
+
logits=mm.decode(z)
|
| 55 |
+
inc=coeff['out']*torch.tanh(logits)
|
| 56 |
+
x=buf[-1]+inc
|
| 57 |
+
ys.append(x);buf.append(x)
|
| 58 |
+
if return_trace:
|
| 59 |
+
with torch.no_grad():
|
| 60 |
+
tr.append(dict(lead=k+1,raw_flux_rms=float(flux_rms(raw).mean()),
|
| 61 |
+
used_flux_rms=float(flux_rms(f).mean()),
|
| 62 |
+
local_rms=float(flux_rms(local).mean()),
|
| 63 |
+
decoder_logit_abs_gt2=float((logits.abs()>2).float().mean()),
|
| 64 |
+
output_increment_rms=float(inc.square().mean().sqrt())))
|
| 65 |
+
y=torch.stack(ys,dim=1)
|
| 66 |
+
return ((y.squeeze(-2) if is1d else y),tr) if return_trace else (y.squeeze(-2) if is1d else y)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def rms_rmse(yhat,y,sd):
|
| 70 |
+
return (((yhat-y)*sd).square().sum().item(),yhat.numel())
|
| 71 |
+
|
| 72 |
+
@torch.no_grad()
|
| 73 |
+
def assess(core,policy,cache,split,device,steps=12,batch=None,max_windows=None,details=False):
|
| 74 |
+
if batch is None: batch = 8 if cache['splits'][split]['h'].ndim==4 else 2 # MUST match archived B0 batch for 1D vs 2D
|
| 75 |
+
w=Windows(cache['splits'][split]['h'],cache['splits'][split]['y'],cache['mean'],cache['std'])
|
| 76 |
+
net=ControlledV2(core,policy).to(device).eval()
|
| 77 |
+
sq=0.;n=0; trace=[]; seq_err=None; seq_n=0; dirs={1:[],6:[],12:[]};powers={1:[],6:[],12:[]}
|
| 78 |
+
max_windows=min(len(w),max_windows) if max_windows else len(w)
|
| 79 |
+
for i in range(0,max_windows,batch):
|
| 80 |
+
xx=[w[j] for j in range(i,min(max_windows,i+batch))]
|
| 81 |
+
h=torch.stack([v[0] for v in xx]).to(device);y=torch.stack([v[1] for v in xx]).to(device)
|
| 82 |
+
result=net(h,steps,return_trace=details)
|
| 83 |
+
p,tt=result if details else (result,[])
|
| 84 |
+
s= w.sd.to(device)
|
| 85 |
+
a,b=rms_rmse(p,y[:,:steps],s);sq+=a;n+=b
|
| 86 |
+
if details:
|
| 87 |
+
for t in tt:trace.append(t)
|
| 88 |
+
hh=h.unsqueeze(-2) if h.ndim==4 else h
|
| 89 |
+
yy=y.unsqueeze(-2) if y.ndim==4 else y
|
| 90 |
+
pp=p.unsqueeze(-2) if p.ndim==4 else p
|
| 91 |
+
for k in dirs:
|
| 92 |
+
trueinc=yy[:,k-1]-(hh[:,-1] if k==1 else yy[:,k-2]);predinc=pp[:,k-1]-(hh[:,-1] if k==1 else pp[:,k-2]);
|
| 93 |
+
cos=F.cosine_similarity(predinc.flatten(1),trueinc.flatten(1),dim=1,eps=1e-12)
|
| 94 |
+
dirs[k].extend([float(x) for x in cos])
|
| 95 |
+
powers[k].append(float(predinc.square().mean().clamp_min(1e-20)/trueinc.square().mean().clamp_min(1e-20)))
|
| 96 |
+
out={'rmse12':math.sqrt(sq/max(n,1)),'mse12':sq/max(n,1),'num_windows':max_windows}
|
| 97 |
+
if details:
|
| 98 |
+
out.update({f'cos_lead{k}':float(np.mean(v)) for k,v in dirs.items()})
|
| 99 |
+
out.update({f'power_lead{k}':float(np.mean(v)) for k,v in powers.items()})
|
| 100 |
+
out['trace_lead12']={j:float(np.mean([t[j] for t in trace if t['lead']==12])) for j in ('raw_flux_rms','used_flux_rms','local_rms','decoder_logit_abs_gt2','output_increment_rms')}
|
| 101 |
+
return out
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def get_core(z,cache,task,seed,device):
|
| 105 |
+
name=f'best_models/{task}__V2_Ported_R5__seed{seed}.pt'
|
| 106 |
+
payload=z.read(name)
|
| 107 |
+
ck=torch.load(io.BytesIO(payload),map_location='cpu',weights_only=True)
|
| 108 |
+
assert ck['source_sha']==ARCHIVE_SHA and ck['seed']==seed and ck['model_kind']=='V2_Ported_R5'
|
| 109 |
+
cfg=ck['config'];channel=cache['splits']['train']['h'].shape[2]
|
| 110 |
+
core=UnifiedModel('V2_Ported_R5',channel,cfg['d'],cfg['hidden'],4).to(device)
|
| 111 |
+
core.load_state_dict(ck['model'],strict=True)
|
| 112 |
+
core.eval()
|
| 113 |
+
return core,hashlib.sha256(payload).hexdigest()
|
| 114 |
+
|
| 115 |
+
def check_zip(z):
|
| 116 |
+
assert z.testzip() is None
|
| 117 |
+
r=json.loads(z.read('B0_REPORT.json'));gate=r['gate'];c=r['config']
|
| 118 |
+
assert c['profile']=='pilot_r1' and c['eval_horizon']==12 and c['train_horizon']==6
|
| 119 |
+
assert gate['completed_runs']==18 and gate['official_data'] and gate['all_complete']
|
| 120 |
+
for task in TASKLIST:
|
| 121 |
+
for seed in SEEDS:
|
| 122 |
+
assert f'best_models/{task}__V2_Ported_R5__seed{seed}.pt' in z.namelist()
|
| 123 |
+
return r,json.loads(z.read('data_and_architecture_contract.json'))
|
| 124 |
+
|
| 125 |
+
def output_csv(path,rows):
|
| 126 |
+
if not rows:return
|
| 127 |
+
with open(path,'w',newline='',encoding='utf-8') as f:
|
| 128 |
+
wr=csv.DictWriter(f,fieldnames=list(rows[0]));wr.writeheader();wr.writerows(rows)
|
| 129 |
+
|
| 130 |
+
def gate_validate(rows):
|
| 131 |
+
"""One policy globally selected across all three PDEs; no family-dependent runtime routing."""
|
| 132 |
+
cand=[]
|
| 133 |
+
baseline={(r['task'],r['seed']):r['mse12'] for r in rows if r['policy']=='baseline'}
|
| 134 |
+
for policy in POLICIES[1:]:
|
| 135 |
+
x=[r for r in rows if r['policy']==policy]
|
| 136 |
+
ratios={(r['task'],r['seed']):r['mse12']/baseline[(r['task'],r['seed'])] for r in x}
|
| 137 |
+
family={task:float(np.mean([ratios[(task,s)] for s in SEEDS])) for task in TASKLIST}
|
| 138 |
+
seed_wins={task:sum(ratios[(task,s)]<1 for s in SEEDS) for task in TASKLIST}
|
| 139 |
+
gm=float(math.exp(np.mean([math.log(v) for v in family.values()])))
|
| 140 |
+
eligible=(gm<=.97 and all(v<=1.02 for v in family.values()) and all(v>=2 for v in seed_wins.values()))
|
| 141 |
+
cand.append(dict(policy=policy,geometric_mean_mse_ratio=gm,worst_family_ratio=max(family.values()),
|
| 142 |
+
family_ratios=family,seed_wins=seed_wins,validation_qualified=bool(eligible)))
|
| 143 |
+
eligible=sorted((t for t in cand if t['validation_qualified']),key=lambda r:r['geometric_mean_mse_ratio'])
|
| 144 |
+
return {'chosen_policy':eligible[0]['policy'] if eligible else None,
|
| 145 |
+
'state':'FROZEN_VAL_QUALIFIED_FOR_TRAINING_PREFLIGHT' if eligible else 'FROZEN_VAL_GATE_FAIL_NO_TRAINING',
|
| 146 |
+
'requirements':{'global_geometric_mse_gain_min':.03,'max_each_family_mse_regression':.02,
|
| 147 |
+
'min_paired_seed_wins_per_family':2},'candidates':cand}
|
| 148 |
+
|
| 149 |
+
def _seed(seed):
|
| 150 |
+
random.seed(seed);np.random.seed(seed);torch.manual_seed(seed)
|
| 151 |
+
if torch.cuda.is_available():torch.cuda.manual_seed_all(seed)
|
| 152 |
+
|
| 153 |
+
def train_pair(task,cache,seed,policy,outdir,device,updates=240,smoke=False):
|
| 154 |
+
"""Same initial parameters, same window indices, same training budget. Qualification only."""
|
| 155 |
+
from benchmark import choose_match
|
| 156 |
+
cfg=Config(profile='pilot_r1')
|
| 157 |
+
channel=cache['splits']['train']['h'].shape[2]
|
| 158 |
+
# Exact prior B0 parameter specs from checkpoint are ideal but same parameter count within this pair suffices.
|
| 159 |
+
conf=choose_match('V2_Ported_R5',channel,cfg) if not smoke else {'d':8,'hidden':12}
|
| 160 |
+
_seed(seed)
|
| 161 |
+
init=UnifiedModel('V2_Ported_R5',channel,conf['d'],conf['hidden'],4)
|
| 162 |
+
sd=copy.deepcopy(init.state_dict()); del init
|
| 163 |
+
dataset=Windows(cache['splits']['train']['h'],cache['splits']['train']['y'],cache['mean'],cache['std'])
|
| 164 |
+
logs=[];results=[]
|
| 165 |
+
for route in ('baseline',policy):
|
| 166 |
+
core=UnifiedModel('V2_Ported_R5',channel,conf['d'],conf['hidden'],4).to(device)
|
| 167 |
+
core.load_state_dict(sd,strict=True)
|
| 168 |
+
net=ControlledV2(core,route).to(device)
|
| 169 |
+
optimizer=torch.optim.AdamW(net.parameters(),lr=1e-3,weight_decay=1e-4)
|
| 170 |
+
best=float('inf');best_state=None;hist=[]
|
| 171 |
+
snapshot=Path(outdir)/f'Q1_LATEST_{task}__{route}__seed{seed}.pt'
|
| 172 |
+
start_step=0
|
| 173 |
+
if snapshot.exists():
|
| 174 |
+
saved=torch.load(snapshot,map_location=device,weights_only=True)
|
| 175 |
+
if (saved.get('seed')==seed and saved.get('route')==route and
|
| 176 |
+
saved.get('candidate_policy')==policy and saved.get('updates')==updates and
|
| 177 |
+
saved.get('match_spec')==conf):
|
| 178 |
+
net.load_state_dict(saved['model'],strict=True)
|
| 179 |
+
optimizer.load_state_dict(saved['optimizer'])
|
| 180 |
+
start_step=int(saved['step']);best_state=saved['best_model']
|
| 181 |
+
hist=saved['history'];best=float(saved['best_rmse12'])
|
| 182 |
+
print('[Q1 RESUME]',task,route,seed,'step',start_step,flush=True)
|
| 183 |
+
for step in range(start_step,updates):
|
| 184 |
+
net.train()
|
| 185 |
+
inds=torch.randint(0,len(dataset),(2 if len(cache['splits']['train']['h'].shape)>4 else 8,),
|
| 186 |
+
generator=torch.Generator().manual_seed(seed*1000003+step)).tolist()
|
| 187 |
+
samples=[dataset[i] for i in inds]
|
| 188 |
+
h=torch.stack([x[0] for x in samples]).to(device)
|
| 189 |
+
y=torch.stack([x[1] for x in samples]).to(device)
|
| 190 |
+
optimizer.zero_grad(set_to_none=True)
|
| 191 |
+
p=net(h,6);loss=F.mse_loss(p,y[:,:6]);assert torch.isfinite(loss)
|
| 192 |
+
loss.backward();nn.utils.clip_grad_norm_(net.parameters(),1.);optimizer.step()
|
| 193 |
+
if (step+1)%max(2,updates//4)==0 or step+1==updates:
|
| 194 |
+
ev=assess(net.core,route,cache,'val',device,batch=2,max_windows=2 if smoke else None)
|
| 195 |
+
if ev['rmse12']<best:
|
| 196 |
+
best=ev['rmse12'];best_state={k:v.detach().cpu().clone() for k,v in net.state_dict().items()}
|
| 197 |
+
rr=dict(task=task,seed=seed,route=route,step=step+1,train_mse=float(loss.detach()),
|
| 198 |
+
val_rmse12=ev['rmse12'],best_val_rmse12=best)
|
| 199 |
+
hist.append(rr);print('[Q1 train]',rr,flush=True)
|
| 200 |
+
# Persist last and best weights/optimizer for diagnosis even on interrupted runs.
|
| 201 |
+
snapshot_tmp=snapshot.with_suffix('.pt.tmp')
|
| 202 |
+
torch.save({'model':net.state_dict(),'best_model':best_state,'optimizer':optimizer.state_dict(),
|
| 203 |
+
'step':step+1,'history':hist,'seed':seed,'route':route,
|
| 204 |
+
'candidate_policy':policy,'qualified_only':True,
|
| 205 |
+
'best_rmse12':best,'updates':updates,'match_spec':conf},snapshot_tmp)
|
| 206 |
+
os.replace(snapshot_tmp,snapshot)
|
| 207 |
+
# Optional HF checkpoint + GitHub small progress report; no failure may stop scientific training.
|
| 208 |
+
if os.getenv('CIDM_LIVE_SYNC','1')=='1':
|
| 209 |
+
try:
|
| 210 |
+
from artifact_store import sync_training_milestone
|
| 211 |
+
ret=sync_training_milestone(snapshot,rr,
|
| 212 |
+
repo_id=os.getenv('CIDM_HF_RESULTS_REPO',''),
|
| 213 |
+
github_repo=os.getenv('CIDM_GITHUB_REPO','Mr-wuff/AI-Weather-Quest'))
|
| 214 |
+
print('[Q1 ARCHIVE]',ret,flush=True)
|
| 215 |
+
except Exception as archive_exc:
|
| 216 |
+
print('[Q1 ARCHIVE NONBLOCKING ERROR]',str(archive_exc)[:140],flush=True)
|
| 217 |
+
|
| 218 |
+
net.load_state_dict(best_state)
|
| 219 |
+
outfile=Path(outdir)/f'Q1_{task}__{route}__seed{seed}.pt'
|
| 220 |
+
torch.save({'model_state':best_state,'route':route,'task':task,'seed':seed,'updates':updates,
|
| 221 |
+
'match_spec':conf,'qualification_only':True},outfile)
|
| 222 |
+
results.append(dict(task=task,seed=seed,route=route,updates=updates,best_val_rmse12=best,
|
| 223 |
+
train_history=hist,checkpoint=outfile.name))
|
| 224 |
+
logs+=hist
|
| 225 |
+
return results,logs
|
| 226 |
+
|
| 227 |
+
def run(b0zip,outdir,root='/content/CIDM_v2_B0_FairPDE',smoke=False):
|
| 228 |
+
torch.set_num_threads(min(4,torch.get_num_threads()))
|
| 229 |
+
out=Path(outdir);out.mkdir(parents=True,exist_ok=True)
|
| 230 |
+
assert sha(Path(__file__).with_name('cidm_complete_frozen.py'))==ARCHIVE_SHA
|
| 231 |
+
z=zipfile.ZipFile(b0zip);r0,manifest=check_zip(z)
|
| 232 |
+
device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 233 |
+
rows=[];replay=[];caches={};diagnostic_rows=[]
|
| 234 |
+
tasks=('swe_2d',) if smoke else TASKLIST
|
| 235 |
+
seeds=(17,) if smoke else SEEDS
|
| 236 |
+
for task in tasks:
|
| 237 |
+
if smoke:
|
| 238 |
+
g=torch.Generator().manual_seed(2033)
|
| 239 |
+
a=torch.randn(4,16,1,8,16,generator=g)*.03
|
| 240 |
+
for t in range(1,16):a[:,t]=.98*a[:,t-1]+.02*a[:,t]
|
| 241 |
+
caches[task]={'splits':{k:{'h':a[:,:4], 'y':a[:,4:]} for k in ('train','val','test')},
|
| 242 |
+
'mean':torch.zeros(1),'std':torch.ones(1)}
|
| 243 |
+
else:
|
| 244 |
+
cfg=Config(**r0['config']);data_dir=Path(os.getenv('CIDM_B2_DATA_ROOT',str(Path(root)/'official_data')))
|
| 245 |
+
official=fetch_official(data_dir,task)
|
| 246 |
+
caches[task]=make_cache(task,official,cfg,Path(root)/'window_cache')
|
| 247 |
+
ref=next(t for t in manifest['tasks'] if t['task']==task)
|
| 248 |
+
assert caches[task]['signature']['source_md5']==ref['source_md5']
|
| 249 |
+
# Check trajectory IDs (not just window counts / source MD5). A stale cache can otherwise silently pass.
|
| 250 |
+
assert caches[task]['ids']==ref['ids'], f'{task}: OFFICIAL_TRAJECTORY_ID_MISMATCH'
|
| 251 |
+
for split in ('train','val','test'):
|
| 252 |
+
assert len(caches[task]['splits'][split]['h'])==ref['windows'][split]
|
| 253 |
+
for seed in seeds:
|
| 254 |
+
core,hsh=get_core(z,caches[task],task,seed,device)
|
| 255 |
+
base=assess(core,'baseline',caches[task],'val',device,batch=2,max_windows=2 if smoke else None)
|
| 256 |
+
if not smoke:
|
| 257 |
+
# REPLAY FIX2: archival benchmark loader, exact batch size, original forward FIRST.
|
| 258 |
+
# A different batch size may cause divergent GPU convolution rounding during unstable rollout.
|
| 259 |
+
cfg=Config(**r0['config'])
|
| 260 |
+
original_batch=cfg.batch_1d if caches[task]['splits']['test']['h'].ndim==4 else cfg.batch_2d
|
| 261 |
+
orig=float(json.loads(z.read(f'{task}__V2_Ported_R5__seed{seed}.json'))['test']['model_12'])
|
| 262 |
+
ref_loader=load_data(caches[task],'test',original_batch,seed,False)
|
| 263 |
+
canonical=canonical_assess(core,ref_loader,device,cfg,mode='off')['model_12']
|
| 264 |
+
# Run the wrapper with the SAME batch schedule and normalization.
|
| 265 |
+
actual=assess(core,'baseline',caches[task],'test',device,batch=original_batch)
|
| 266 |
+
# Check element-wise equality, in the original B0 test batch shape.
|
| 267 |
+
hw,yy=next(iter(ref_loader));hw=hw.to(device)
|
| 268 |
+
with torch.no_grad():
|
| 269 |
+
raw_out=core(hw,12,'off')
|
| 270 |
+
wrapped_out=ControlledV2(core,'baseline')(hw,12)
|
| 271 |
+
parity=float((raw_out-wrapped_out).abs().max())
|
| 272 |
+
tol=max(1e-5,orig*.001)
|
| 273 |
+
row=dict(task=task,seed=seed,checkpoint_sha256=hsh,archival_batch=original_batch,
|
| 274 |
+
original_rmse12=orig,canonical_rmse12=canonical,wrapped_rmse12=actual['rmse12'],
|
| 275 |
+
max_forward_parity_diff=parity,
|
| 276 |
+
original_difference=canonical-orig,wrapper_difference=actual['rmse12']-canonical,
|
| 277 |
+
pass_archive=bool(abs(canonical-orig)<=tol),
|
| 278 |
+
pass_wrapper=bool(abs(actual['rmse12']-canonical)<=tol and parity<2e-6))
|
| 279 |
+
replay.append(row)
|
| 280 |
+
if not row['pass_archive'] or not row['pass_wrapper']:
|
| 281 |
+
# Preserve exact evidence before fail-closed. Never enter Q0 on a mismatched baseline.
|
| 282 |
+
(out/'REPLAY_FAILURE_DIAGNOSTIC.json').write_text(json.dumps({'reason':'CANONICAL_SOURCE_OR_WRAPPER_REPLAY_MISMATCH',
|
| 283 |
+
'failed_run':row,'prior_runs':replay,'data_signature':caches[task]['signature'],
|
| 284 |
+
'historical_report_config':r0['config']},indent=2,ensure_ascii=False))
|
| 285 |
+
raise RuntimeError('B0 replay FAIL-CLOSED: '+json.dumps(row,ensure_ascii=False)+
|
| 286 |
+
'. See REPLAY_FAILURE_DIAGNOSTIC.json. Confirm checkpoint/data/cache/batch. DO NOT relax tolerance.')
|
| 287 |
+
print('[B0 archived exact replay PASS]',task,seed,'batch',original_batch,
|
| 288 |
+
'archived',round(orig,8),'reference',round(canonical,8),
|
| 289 |
+
'wrapper',round(actual['rmse12'],8),'max_diff',parity,flush=True)
|
| 290 |
+
# Verify unchanged baseline replay exactly against original architecture.
|
| 291 |
+
w=Windows(caches[task]['splits']['val']['h'],caches[task]['splits']['val']['y'],
|
| 292 |
+
caches[task]['mean'],caches[task]['std'])
|
| 293 |
+
h=torch.stack([w[0][0]]).to(device)
|
| 294 |
+
with torch.no_grad():
|
| 295 |
+
diff=float((ControlledV2(core,'baseline')(h,12)-core(h,12,'off')).abs().max())
|
| 296 |
+
assert diff<2e-6,(task,seed,diff)
|
| 297 |
+
print('[B2 R1 Q0]',task,'seed',seed,'baseline val12',base['rmse12'])
|
| 298 |
+
# Causally available validation histories only; measure saturation and direction
|
| 299 |
+
# on a small subset. These observations do not affect policy selection.
|
| 300 |
+
dp=assess(core,'baseline',caches[task],'val',device,batch=2,max_windows=2 if smoke else 8,details=True)
|
| 301 |
+
diagnostic_rows.append(dict(task=task,seed=seed,policy='baseline',
|
| 302 |
+
**{k:v for k,v in dp.items() if k.startswith(('cos_lead','power_lead'))},
|
| 303 |
+
**dp['trace_lead12']))
|
| 304 |
+
for policy in POLICIES:
|
| 305 |
+
x=base if policy=='baseline' else assess(core,policy,caches[task],'val',device,batch=2,max_windows=2 if smoke else None)
|
| 306 |
+
rows.append(dict(task=task,seed=seed,policy=policy,mse12=x['mse12'],rmse12=x['rmse12'],
|
| 307 |
+
gain_vs_baseline_pct=100*(1-x['mse12']/base['mse12']),val_windows=x['num_windows']))
|
| 308 |
+
del core
|
| 309 |
+
if device.type=='cuda':torch.cuda.empty_cache()
|
| 310 |
+
output_csv(out/'frozen_validation_selection.csv',rows)
|
| 311 |
+
output_csv(out/'baseline_validation_operator_diagnostics.csv',diagnostic_rows)
|
| 312 |
+
stage={'state':'PROGRAM_ONLY_SMOKE' if smoke else 'UNKNOWN','chosen_policy':None,'candidates':[]}
|
| 313 |
+
if not smoke:stage=gate_validate(rows)
|
| 314 |
+
q1results=[];history=[]
|
| 315 |
+
if stage['chosen_policy'] is not None and not smoke:
|
| 316 |
+
print('[B2 R1] Frozen validated candidate:',stage['chosen_policy'], 'entering TRAINED Q1 seed17 only')
|
| 317 |
+
for task in TASKLIST:
|
| 318 |
+
ans,logs=train_pair(task,caches[task],17,stage['chosen_policy'],out,device,updates=240)
|
| 319 |
+
q1results+=ans;history+=logs
|
| 320 |
+
ref={r['task']:r for r in q1results if r['route']=='baseline'}
|
| 321 |
+
gains={r['task']:1-r['best_val_rmse12']/ref[r['task']]['best_val_rmse12']
|
| 322 |
+
for r in q1results if r['route']!= 'baseline'}
|
| 323 |
+
# First-stage tests only, not automatic multi-seed formal training.
|
| 324 |
+
q1pass=all(gains[t]>=.02 for t in ('burgers_1d','swe_2d')) and gains['reacdiff_2d']>=-.02
|
| 325 |
+
stage['q1_trainable_prequalification']={'improvement_by_task':gains,'pass':q1pass,
|
| 326 |
+
'state':'Q1_SINGLE_SEED_PREQUALIFIED_FURTHER_SEEDS_NEEDED' if q1pass else 'Q1_STOP_BEFORE_FORMAL_TRAINING'}
|
| 327 |
+
stage['state']='Q1_SINGLE_SEED_PREQUALIFIED_FURTHER_SEEDS_NEEDED' if q1pass else 'Q1_STOP_BEFORE_FORMAL_TRAINING'
|
| 328 |
+
report={'status':stage['state'],'data':'SYNTHETIC_SMOKE' if smoke else 'OFFICIAL_MD5_CHECKED',
|
| 329 |
+
'original_B0_completed_runs':r0['gate']['completed_runs'],
|
| 330 |
+
'original_B0_source':ARCHIVE_SHA,'frozen_gate':stage,
|
| 331 |
+
'test_replay_only_not_used_for_selection':replay,'trainable_q1_results':q1results,
|
| 332 |
+
'limitations':['Q0 hyperparameter selection uses validation only; official test used for original replay check only.',
|
| 333 |
+
'Q1 is 240-update seed17 trained qualification, NOT cross-seed scientific superiority.',
|
| 334 |
+
'Policy changes learned R5 transition and may distort a frozen checkpoint; no inference policy is adopted without retrained comparison.',
|
| 335 |
+
'Flux latent telescoping is not physical energy conservation; perturbation growth is not a Lyapunov exponent.',
|
| 336 |
+
'Original three PDE families have repeatedly informed development; not truly blind transfer.']}
|
| 337 |
+
(out/'report.json').write_text(json.dumps(report,indent=2,ensure_ascii=False),encoding='utf-8')
|
| 338 |
+
output_csv(out/'q1_training_history.csv',history)
|
| 339 |
+
resultzip=out/'CIDM_v2_B2_R1_FLUX_CONTROL_QUALIFICATION_REPORTS.zip'
|
| 340 |
+
with zipfile.ZipFile(resultzip,'w',compression=zipfile.ZIP_DEFLATED) as zout:
|
| 341 |
+
for file in sorted(out.glob('*')):
|
| 342 |
+
if file!=resultzip and file.is_file():zout.write(file,arcname=file.name)
|
| 343 |
+
assert zipfile.ZipFile(resultzip).testzip() is None
|
| 344 |
+
z.close();return report,str(resultzip)
|