Archive CIDM-v2 reproducible cidm_mhc_a0_qualification.py
Browse files
cidm_v2/source/MHC_A0/cidm_mhc_a0_qualification.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CIDM-MHC A0: matched-budget, truncated-gradient, Markov hierarchical closure pilot.
|
| 2 |
+
|
| 3 |
+
IMPORTANT: Q0 numerical + immediate causal-gradient prequalification, Q1 small pilot only.
|
| 4 |
+
No architecture-wide finetuning, no external-holdout claim, no full-training permission.
|
| 5 |
+
Historical parent model is frozen and exact B0-R1 official replay must PASS.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import csv, hashlib, json, math, time, copy, traceback, zipfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from torch import nn
|
| 13 |
+
import torch.nn.functional as F
|
| 14 |
+
from benchmark import Config, Windows, UnifiedModel, make_synthetic_cache
|
| 15 |
+
from b2_r6_causal_spatial_controller import (PARENT_SHA, TASK_LIST, make_cache_checked)
|
| 16 |
+
from b2_r1_flux_control import check_zip
|
| 17 |
+
|
| 18 |
+
LEADS=(1,3,6,9,12)
|
| 19 |
+
ARMS=('stateless_closure','mhc_hierarchical','mhc_no_aux')
|
| 20 |
+
BUDGET=0.003
|
| 21 |
+
SLOW_RATE=0.25 # Persistent coarse state responds at its own timescale; not P(z_t).
|
| 22 |
+
AUX_WEIGHT=0.10
|
| 23 |
+
SEED=20311015
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def sha(path):
|
| 27 |
+
h=hashlib.sha256()
|
| 28 |
+
with open(path,'rb') as f:
|
| 29 |
+
for chunk in iter(lambda:f.read(2**20),b''):h.update(chunk)
|
| 30 |
+
return h.hexdigest()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def dump(path,obj):
|
| 34 |
+
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
|
| 35 |
+
path.write_text(json.dumps(obj,indent=2,ensure_ascii=False,allow_nan=False,default=str),encoding='utf8')
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def write_rows(path,rows):
|
| 39 |
+
if not rows:return
|
| 40 |
+
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
|
| 41 |
+
with open(path,'w',encoding='utf-8',newline='') as f:
|
| 42 |
+
w=csv.DictWriter(f,fieldnames=list(dict.fromkeys(k for row in rows for k in row)));w.writeheader();w.writerows(rows)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def lowpass(x):
|
| 46 |
+
if x.ndim !=4:raise RuntimeError('EXPECTED_BCHW')
|
| 47 |
+
kh=1 if x.shape[-2]==1 else 3
|
| 48 |
+
return F.avg_pool2d(x,kernel_size=(kh,3),stride=1,padding=(kh//2,1),count_include_pad=False)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def lowpass_field(x):
|
| 52 |
+
return lowpass(x.unsqueeze(-2)).squeeze(-2) if x.ndim==3 else lowpass(x)
|
| 53 |
+
|
| 54 |
+
def rms(x):
|
| 55 |
+
return torch.sqrt(torch.clamp(x.square().mean(dim=(1,2,3),keepdim=True),min=1e-24))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class MarkovClosureHead(nn.Module):
|
| 59 |
+
"""One small shared-architecture controller. No lead ID, no history or future truth."""
|
| 60 |
+
def __init__(self,channels,latent_dim,width=16,projection_dim=8,seed=SEED):
|
| 61 |
+
super().__init__()
|
| 62 |
+
if latent_dim>256:raise RuntimeError('LATENT_CHANNELS_GT_256')
|
| 63 |
+
generator=torch.Generator().manual_seed(seed)
|
| 64 |
+
self.register_buffer('proj',torch.randn(projection_dim,256,generator=generator)/math.sqrt(projection_dim))
|
| 65 |
+
inputs=2*projection_dim+2*channels+2
|
| 66 |
+
self.net=nn.Sequential(nn.Conv2d(inputs,width,3,padding=1),nn.SiLU(),
|
| 67 |
+
nn.Conv2d(width,width,3,padding=1),nn.SiLU(),nn.Conv2d(width,2,1))
|
| 68 |
+
nn.init.zeros_(self.net[-1].weight)
|
| 69 |
+
nn.init.zeros_(self.net[-1].bias)
|
| 70 |
+
self.channels=channels
|
| 71 |
+
self.latent_dim=latent_dim
|
| 72 |
+
def forward(self,slow,fast,prev):
|
| 73 |
+
pp=self.proj[:,:self.latent_dim]
|
| 74 |
+
aa=torch.einsum('kd,bdhw->bkhw',pp,slow.detach())
|
| 75 |
+
bb=torch.einsum('kd,bdhw->bkhw',pp,fast.detach())
|
| 76 |
+
cs=prev.detach()
|
| 77 |
+
x=torch.cat([aa,bb,cs,lowpass(cs),
|
| 78 |
+
torch.log1p(rms(slow.detach())).expand(-1,-1,*cs.shape[-2:]),
|
| 79 |
+
torch.log1p(rms(fast.detach())).expand(-1,-1,*cs.shape[-2:])],dim=1)
|
| 80 |
+
return torch.tanh(self.net(x))
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def run_rollout(core,head,h,arm='stateless_closure',steps=12,truncate=True):
|
| 84 |
+
"""12-step original core; optional learned correction on persistent slow/fast Markov states.
|
| 85 |
+
|
| 86 |
+
No ground truth or time/round label enters dynamics; only current (slow,fast,p).
|
| 87 |
+
Each update is bounded by <=.3% of latent RMS (actual action can be smaller).
|
| 88 |
+
"""
|
| 89 |
+
if arm not in ('baseline',)+ARMS:raise ValueError(arm)
|
| 90 |
+
mm=core.model
|
| 91 |
+
original_1d=(h.ndim==4)
|
| 92 |
+
hh=h.unsqueeze(-2) if original_1d else h
|
| 93 |
+
batch,history,ch,height,width=hh.shape
|
| 94 |
+
coef=mm.coefficients();skew=mm.skew-mm.skew.T
|
| 95 |
+
with torch.no_grad():
|
| 96 |
+
z=mm.encode(hh.reshape(batch,history*ch,height,width))
|
| 97 |
+
prev=hh[:,-1]
|
| 98 |
+
slow=lowpass(z)
|
| 99 |
+
fast=z-slow
|
| 100 |
+
outs=[];effects=[];scales=[]
|
| 101 |
+
for step in range(steps):
|
| 102 |
+
# BPTT(1) only; never differentiate original 12-step dynamical core across time.
|
| 103 |
+
if truncate:
|
| 104 |
+
slow=slow.detach();fast=fast.detach();prev=prev.detach()
|
| 105 |
+
with torch.no_grad():
|
| 106 |
+
z=slow+fast
|
| 107 |
+
flux=coef['flux']*mm.flux(z)
|
| 108 |
+
local=coef['local']*torch.tanh(mm.local(z))
|
| 109 |
+
skewpart=.035*torch.einsum('ij,bjhw->bihw',skew,z)
|
| 110 |
+
tendency=flux+local+skewpart
|
| 111 |
+
base=z+tendency
|
| 112 |
+
if arm=='mhc_hierarchical' or arm=='mhc_no_aux':
|
| 113 |
+
slow_increment=SLOW_RATE*lowpass(tendency)
|
| 114 |
+
slow_base=slow+slow_increment
|
| 115 |
+
fast_base=fast+(tendency-slow_increment)
|
| 116 |
+
else:
|
| 117 |
+
slow_base=lowpass(base)
|
| 118 |
+
fast_base=base-slow_base
|
| 119 |
+
assert base.shape==slow_base.shape
|
| 120 |
+
# Algebraic zero-control parity: slow_base+fast_base == original base.
|
| 121 |
+
if head is None or arm=='baseline':
|
| 122 |
+
corr=torch.zeros_like(base)
|
| 123 |
+
else:
|
| 124 |
+
gate=head(slow_base,fast_base,prev)
|
| 125 |
+
us=lowpass(tendency)
|
| 126 |
+
uf=tendency-us
|
| 127 |
+
# This is a spatially structured tendency-direction control, not a truth oracle.
|
| 128 |
+
norm_s=us/(rms(us).detach().clamp_min(1e-6))
|
| 129 |
+
norm_f=uf/(rms(uf).detach().clamp_min(1e-6))
|
| 130 |
+
raw=(lowpass(gate[:,0:1]*norm_s)+(gate[:,1:2]*norm_f-lowpass(gate[:,1:2]*norm_f)))/math.sqrt(2)
|
| 131 |
+
# No sqrt(0) backward singularity. Smooth denominator for near-zero action.
|
| 132 |
+
target=BUDGET*rms(base).detach().clamp_min(1e-4)
|
| 133 |
+
corr=target*raw/(rms(raw).detach()+0.05)
|
| 134 |
+
corr=corr*torch.clamp(target/(rms(corr).detach()+1e-12),max=1.0)
|
| 135 |
+
# The maximum RMS correction contract is strict and batchwise.
|
| 136 |
+
exposure=(rms(corr)/rms(base).detach().clamp_min(1e-4)).detach()
|
| 137 |
+
if bool((exposure>BUDGET+1e-6).any()):raise RuntimeError('LATENT_RMS_EXPOSURE_EXCEEDED')
|
| 138 |
+
zn=base+corr
|
| 139 |
+
if arm in ('mhc_hierarchical','mhc_no_aux'):
|
| 140 |
+
cs=SLOW_RATE*lowpass(corr)
|
| 141 |
+
slow=slow_base+cs
|
| 142 |
+
fast=fast_base+(corr-cs)
|
| 143 |
+
else:
|
| 144 |
+
slow=lowpass(zn)
|
| 145 |
+
fast=zn-slow
|
| 146 |
+
# Frozen decoder weights, but input gradient is essential for head learning.
|
| 147 |
+
pnew=prev+coef['out']*torch.tanh(mm.decode(zn))
|
| 148 |
+
outs.append(pnew.squeeze(-2) if original_1d else pnew)
|
| 149 |
+
effects.append(rms(corr).detach().flatten()/rms(base).detach().flatten().clamp_min(1e-4))
|
| 150 |
+
scales.append((rms(slow).detach(),rms(fast).detach()))
|
| 151 |
+
prev=pnew
|
| 152 |
+
return torch.stack(outs,dim=1),torch.stack(effects,dim=1),scales
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def physical_scale(window,field):
|
| 156 |
+
return window.sd.to(field.device).reshape(1,1,-1,*([1]*(field.ndim-3)))
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def criterion(pred,truth,window,arm):
|
| 160 |
+
# In physical units, with same fixed objective for stateless/MHC/full vs no_aux.
|
| 161 |
+
scale=physical_scale(window,pred)
|
| 162 |
+
diff=(pred-truth)*scale
|
| 163 |
+
losses=[]
|
| 164 |
+
for lead in LEADS:
|
| 165 |
+
losses.append(diff[:,lead-1].square().mean())
|
| 166 |
+
loss=sum(losses)/len(losses)
|
| 167 |
+
if arm!='mhc_no_aux' and arm!='baseline':
|
| 168 |
+
# Fixed, physically interpretable hierarchical target representations.
|
| 169 |
+
# Stop-gradient on truth (not learnable encoder) and horizons fixed before testing.
|
| 170 |
+
target=truth.detach()*scale
|
| 171 |
+
physical=pred*scale
|
| 172 |
+
slo=[3,6,9,12];shi=[1,3]
|
| 173 |
+
ll=sum((lowpass_field(physical[:,l-1])-lowpass_field(target[:,l-1])).square().mean() for l in slo)/len(slo)
|
| 174 |
+
lh=sum(((physical[:,l-1]-lowpass_field(physical[:,l-1]))-
|
| 175 |
+
(target[:,l-1]-lowpass_field(target[:,l-1]))).square().mean() for l in shi)/len(shi)
|
| 176 |
+
loss=loss+AUX_WEIGHT*(ll+lh)*0.5
|
| 177 |
+
return loss
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def get_data(cache,split):
|
| 181 |
+
return Windows(cache['splits'][split]['h'],cache['splits'][split]['y'],cache['mean'],cache['std'])
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def get_batch(ds,ids,device):
|
| 185 |
+
samples=[ds[int(i)] for i in ids]
|
| 186 |
+
return torch.stack([v[0] for v in samples]).to(device),torch.stack([v[1] for v in samples]).to(device)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
@torch.no_grad()
|
| 190 |
+
def evaluate(core,heads,cache,split,ids,device,task):
|
| 191 |
+
ds=get_data(cache,split);records=[]
|
| 192 |
+
for ix in ids:
|
| 193 |
+
x,y=get_batch(ds,[ix],device)
|
| 194 |
+
for arm in ('baseline',)+ARMS:
|
| 195 |
+
p,a,_=run_rollout(core,None if arm=='baseline' else heads[arm],x,arm=arm,truncate=True)
|
| 196 |
+
scale=physical_scale(ds,p)
|
| 197 |
+
for lead in LEADS:
|
| 198 |
+
err=((p[:,lead-1]-y[:,lead-1])*scale[:,0]).square().flatten(1).mean(1)
|
| 199 |
+
records.append({'task':task,'split':split,'window':int(ix),'arm':arm,'lead':lead,
|
| 200 |
+
'mse_physical':float(err.mean()),'rel_action_mean':float(a[:,lead-1].mean())})
|
| 201 |
+
return records
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def aggregate(rows,arm,leads=(9,12)):
|
| 205 |
+
# pooled identical number of observations on each lead and window.
|
| 206 |
+
vals=[r['mse_physical'] for r in rows if r['arm']==arm and r['lead'] in leads]
|
| 207 |
+
return sum(vals)/max(1,len(vals))
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def make_arms(ch,latent_dim,device):
|
| 211 |
+
torch.manual_seed(SEED)
|
| 212 |
+
first=MarkovClosureHead(ch,latent_dim).to(device)
|
| 213 |
+
heads={a:copy.deepcopy(first).to(device) for a in ARMS}
|
| 214 |
+
sizes={a:sum(p.numel() for p in heads[a].parameters() if p.requires_grad) for a in ARMS}
|
| 215 |
+
assert len(set(sizes.values()))==1
|
| 216 |
+
return heads,sizes
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def q0(core,heads,cache,task,device,output,smoke=False):
|
| 220 |
+
train=get_data(cache,'train');val=get_data(cache,'val')
|
| 221 |
+
x,y=get_batch(train,[0],device)
|
| 222 |
+
with torch.no_grad():
|
| 223 |
+
ref=run_rollout(core,None,x,arm='baseline')[0]
|
| 224 |
+
# Confirm original no-correction path exactly matches original causal R9 recurrence.
|
| 225 |
+
from b2_r9_activation_timing import rollout_timing
|
| 226 |
+
old=rollout_timing(core,None,x)[0]
|
| 227 |
+
baseline_parity=float((ref-old).abs().max())
|
| 228 |
+
assert baseline_parity<=2e-6,('BASELINE_R9_PARITY_FAIL',baseline_parity)
|
| 229 |
+
# zero initialized controller must preserve baseline both arms exactly.
|
| 230 |
+
zpar={arm:float((run_rollout(core,head,x,arm=arm)[0]-ref).abs().max()) for arm,head in heads.items()}
|
| 231 |
+
if max(zpar.values())>2e-6:raise RuntimeError('ZERO_GATE_BASELINE_PARITY_FAIL')
|
| 232 |
+
qrecord={'task':task,'baseline_parity_max':baseline_parity,'zero_gate_parity':zpar,'arms':{},'passed':False}
|
| 233 |
+
flags=[]
|
| 234 |
+
for arm,head in heads.items():
|
| 235 |
+
head.train()
|
| 236 |
+
opt=torch.optim.AdamW(head.parameters(),lr=3e-4,weight_decay=0.)
|
| 237 |
+
before,act,_=run_rollout(core,head,x,arm=arm)
|
| 238 |
+
l0=criterion(before,y,train,arm)
|
| 239 |
+
opt.zero_grad(set_to_none=True);l0.backward()
|
| 240 |
+
grads=[p.grad for p in head.parameters() if p.grad is not None]
|
| 241 |
+
gn=math.sqrt(sum(float(g.detach().double().square().sum()) for g in grads))
|
| 242 |
+
finite=bool(math.isfinite(gn) and gn>1e-13 and all(bool(torch.isfinite(g).all()) for g in grads))
|
| 243 |
+
if finite:opt.step()
|
| 244 |
+
with torch.no_grad():
|
| 245 |
+
pnew,newact,_=run_rollout(core,head,x,arm=arm)
|
| 246 |
+
l1=float(criterion(pnew,y,train,arm))
|
| 247 |
+
l0=float(l0.detach())
|
| 248 |
+
down=bool(math.isfinite(l1) and (l1<l0-1e-11))
|
| 249 |
+
mag=float(newact.mean())
|
| 250 |
+
qrecord['arms'][arm]={'loss_before':l0,'loss_after_one_step':l1,
|
| 251 |
+
'relative_train_objective_change':(l1/l0-1) if l0>0 else None,
|
| 252 |
+
'gradient_l2':gn,'finite_and_nonzero_gradient':finite,
|
| 253 |
+
'immediate_descent':down,'actual_action_mean':mag,
|
| 254 |
+
'parameter_count':sum(p.numel() for p in head.parameters() if p.requires_grad)}
|
| 255 |
+
flags.append(finite and down and 0.<mag<BUDGET+1e-6)
|
| 256 |
+
qrecord['passed']=all(flags)
|
| 257 |
+
dump(output/f'{task}_q0_gate.json',qrecord)
|
| 258 |
+
return qrecord
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def train_small(core,heads,cache,task,output,device,updates=24,smoke=False):
|
| 262 |
+
"""PREQUALIFIED mini-pilot only. Identical updates/cost, NO full architecture optimization."""
|
| 263 |
+
train=get_data(cache,'train');val=get_data(cache,'val')
|
| 264 |
+
opt={a:torch.optim.AdamW(heads[a].parameters(),lr=3e-4,weight_decay=0.) for a in ARMS}
|
| 265 |
+
progress=[];schedule=[0,8,16,24] if not smoke else [0,1,2,3]
|
| 266 |
+
# Selection evaluation is reporting only: all arms use last step, never choose using audit.
|
| 267 |
+
def snap(step):
|
| 268 |
+
with torch.no_grad():
|
| 269 |
+
rows=evaluate(core,heads,cache,'val',list(range(min(4,len(val)))),device,task)
|
| 270 |
+
for arm in ('baseline',)+ARMS:
|
| 271 |
+
progress.append({'task':task,'step':step,'arm':arm,'selection_late_mse':aggregate(rows,arm)})
|
| 272 |
+
write_rows(output/f'{task}_progress.csv',progress)
|
| 273 |
+
snap(0)
|
| 274 |
+
for step in range(1,updates+1):
|
| 275 |
+
for arm,head in heads.items():
|
| 276 |
+
head.train()
|
| 277 |
+
ix=((step-1)*13+SEED)%len(train)
|
| 278 |
+
h,y=get_batch(train,[ix],device)
|
| 279 |
+
p,e,_=run_rollout(core,head,h,arm=arm,truncate=True)
|
| 280 |
+
loss=criterion(p,y,train,arm)
|
| 281 |
+
if not bool(torch.isfinite(loss)):raise RuntimeError(f'NONFINITE_Q1_LOSS {task} {arm} {step}')
|
| 282 |
+
opt[arm].zero_grad(set_to_none=True);loss.backward()
|
| 283 |
+
norm=torch.sqrt(sum(p.grad.detach().double().square().sum() for p in head.parameters() if p.grad is not None))
|
| 284 |
+
# No gradient clipping: nonfinite or extreme gradients FAIL CLOSED.
|
| 285 |
+
if not bool(torch.isfinite(norm)) or float(norm)>1e5:raise RuntimeError(f'UNSAFE_Q1_GRADIENT {task} {arm} {step}: {float(norm)}')
|
| 286 |
+
opt[arm].step()
|
| 287 |
+
if step%4==0 or step==1:
|
| 288 |
+
progress.append({'task':task,'step':step,'arm':arm,'train_objective':float(loss.detach()),
|
| 289 |
+
'train_grad_l2_unclipped':float(norm.detach()),'actual_action_mean':float(e.mean())})
|
| 290 |
+
write_rows(output/f'{task}_progress.csv',progress)
|
| 291 |
+
if step in schedule:snap(step)
|
| 292 |
+
if device.startswith('cuda') and step%4==0:torch.cuda.empty_cache()
|
| 293 |
+
for arm,head in heads.items():
|
| 294 |
+
torch.save({'arm':arm,'task':task,'steps':updates,'state_dict':{k:v.detach().cpu() for k,v in head.state_dict().items()}},output/f'{task}_{arm}_mini_pilot.pt')
|
| 295 |
+
audit_ix=list(range(16,min(32,len(val))))
|
| 296 |
+
if smoke:audit_ix=list(range(2,min(4,len(val))))
|
| 297 |
+
if len(audit_ix)<2:raise RuntimeError('INSUFFICIENT_DEFERRED_VALIDATION_WINDOWS')
|
| 298 |
+
audit=evaluate(core,heads,cache,'val',audit_ix,device,task)
|
| 299 |
+
write_rows(output/f'{task}_audit_per_window.csv',audit)
|
| 300 |
+
return audit,progress
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
def gate(audits,smoke=False):
|
| 304 |
+
result={'status':'DIAGNOSTIC_ONLY_NO_FULL_TRAINING','train_allowed':False,'tasks':{},
|
| 305 |
+
'early_regression_cap':.005,'late_gain_target':.01,'max_severe_window_fraction':.20,
|
| 306 |
+
'warnings':['selection and audit both derive from original B0 validation split; NOT unseen outer holdout',
|
| 307 |
+
'seed17 only and 24 update small pilot','full long-BPTT remains unqualified','state sufficiency is operational/model-defined, not physical state sufficiency']}
|
| 308 |
+
for task,rows in audits.items():
|
| 309 |
+
baseline=aggregate(rows,'baseline');early=aggregate(rows,'baseline',(1,3))
|
| 310 |
+
n={}
|
| 311 |
+
for arm in ARMS:
|
| 312 |
+
late=aggregate(rows,arm)
|
| 313 |
+
er=aggregate(rows,arm,(1,3))
|
| 314 |
+
# A severe regression is >2% on at least one late lead for the same window.
|
| 315 |
+
sub={(r['window'],r['lead']):r['mse_physical'] for r in rows if r['arm']==arm}
|
| 316 |
+
ref={(r['window'],r['lead']):r['mse_physical'] for r in rows if r['arm']=='baseline'}
|
| 317 |
+
ids=sorted({r['window'] for r in rows})
|
| 318 |
+
severe=sum(any(sub[(i,l)]>1.02*ref[(i,l)] for l in (9,12)) for i in ids)
|
| 319 |
+
n[arm]={'late_vs_baseline_ratio':late/baseline,
|
| 320 |
+
'early_vs_baseline_ratio':er/early,
|
| 321 |
+
'late_severe_window_fraction':severe/len(ids),
|
| 322 |
+
'pass_diagnostic':bool(late<=baseline*.99 and er<=early*1.005 and severe/len(ids)<=.20)}
|
| 323 |
+
n['mhc_vs_stateless_late_ratio']=aggregate(rows,'mhc_hierarchical')/aggregate(rows,'stateless_closure')
|
| 324 |
+
n['mhc_vs_no_aux_late_ratio']=aggregate(rows,'mhc_hierarchical')/aggregate(rows,'mhc_no_aux')
|
| 325 |
+
result['tasks'][task]=n
|
| 326 |
+
success=sum(r['mhc_hierarchical']['pass_diagnostic'] and r['mhc_vs_stateless_late_ratio']<=.99 for r in result['tasks'].values())
|
| 327 |
+
result['diagnostic_hits']=success
|
| 328 |
+
result['status']=('MHC_SMALL_PILOT_SIGNAL_NEEDS_MULTI_SEED_AND_OUTER_DATA' if success>=2 else
|
| 329 |
+
'MHC_SMALL_PILOT_GATE_FAIL_NO_FORMAL_TRAINING')
|
| 330 |
+
if smoke:result['status']='PROGRAM_ONLY_SMOKE_NO_SCIENCE'
|
| 331 |
+
return result
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def smoke_case():
|
| 335 |
+
cfg=Config(profile='smoke')
|
| 336 |
+
out=[]
|
| 337 |
+
for tag in ('smoke_1d','smoke_2d'):
|
| 338 |
+
cache=make_synthetic_cache(cfg)
|
| 339 |
+
for split in ('train','val','test'):
|
| 340 |
+
if tag=='smoke_1d':
|
| 341 |
+
cache['splits'][split]['h']=cache['splits'][split]['h'].mean(dim=-2)[...,:4]
|
| 342 |
+
cache['splits'][split]['y']=cache['splits'][split]['y'].mean(dim=-2)[...,:4]
|
| 343 |
+
else:
|
| 344 |
+
cache['splits'][split]['h']=cache['splits'][split]['h'][...,:2,:4]
|
| 345 |
+
cache['splits'][split]['y']=cache['splits'][split]['y'][...,:2,:4]
|
| 346 |
+
core=UnifiedModel('V2_Ported_R5',1,8,12,4).cpu().eval().requires_grad_(False)
|
| 347 |
+
out.append((tag,core,cache))
|
| 348 |
+
return out
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def run(parent,work,output,smoke=False,device=None):
|
| 352 |
+
work=Path(work);output=Path(output);work.mkdir(parents=True,exist_ok=True);output.mkdir(parents=True,exist_ok=True)
|
| 353 |
+
torch.set_num_threads(min(4,torch.get_num_threads()))
|
| 354 |
+
device='cpu' if smoke else (device or ('cuda' if torch.cuda.is_available() else 'cpu'))
|
| 355 |
+
torch.manual_seed(SEED);np.random.seed(SEED)
|
| 356 |
+
cases=[];progress=[];audits={};groups=[]
|
| 357 |
+
try:
|
| 358 |
+
if smoke:cases=smoke_case()
|
| 359 |
+
else:
|
| 360 |
+
if parent is None or sha(parent)!=PARENT_SHA:raise RuntimeError('MISSING_OR_WRONG_PARENT')
|
| 361 |
+
with zipfile.ZipFile(parent) as z:
|
| 362 |
+
manifest,_=check_zip(z);cfg=Config(**manifest['config'])
|
| 363 |
+
for task in TASK_LIST:
|
| 364 |
+
core,cache,replay=make_cache_checked(task,z,cfg,work/'official_runtime','cpu')
|
| 365 |
+
dump(output/f'{task}_parent_replay.json',replay)
|
| 366 |
+
cases.append((task,core,cache))
|
| 367 |
+
for task,core,cache in cases:
|
| 368 |
+
core=core.to(device).eval().requires_grad_(False)
|
| 369 |
+
ch=int(cache['splits']['train']['h'].shape[2]);latent_dim=int(core.model.skew.shape[0])
|
| 370 |
+
heads,sizes=make_arms(ch,latent_dim,device)
|
| 371 |
+
precert=q0(core,heads,cache,task,device,output,smoke=smoke)
|
| 372 |
+
groups.append({'task':task,'q0':precert['passed'],'head_params':sizes,'device':device})
|
| 373 |
+
dump(output/'progress_summary.json',groups)
|
| 374 |
+
if not precert['passed']:
|
| 375 |
+
# All three tasks are separately Q0-gated. Never silently claim a trained result.
|
| 376 |
+
continue
|
| 377 |
+
audit,_=train_small(core,heads,cache,task,output,device,updates=3 if smoke else 24,smoke=smoke)
|
| 378 |
+
audits[task]=audit
|
| 379 |
+
dump(output/'progress_summary.json',groups)
|
| 380 |
+
g=gate(audits,smoke=smoke)
|
| 381 |
+
if not smoke and len(audits)!=3:
|
| 382 |
+
g['status']='MHC_Q0_OR_RESOURCE_GATE_FAIL_NO_FORMAL_TRAINING'
|
| 383 |
+
dump(output/'mhc_gate.json',g)
|
| 384 |
+
report={'status':g['status'],'mode':'smoke' if smoke else 'official_small_pilot',
|
| 385 |
+
'task_groups':groups,'completed_pilot_tasks':list(audits),
|
| 386 |
+
'updates_per_head':3 if smoke else 24,'full_model_updates':0,'train_allowed':False,
|
| 387 |
+
'data_contract':'original B0 official parent and original split; no test-set pilot tuning'}
|
| 388 |
+
dump(output/'report.json',report)
|
| 389 |
+
return report
|
| 390 |
+
except Exception as exc:
|
| 391 |
+
dump(output/'failure_details.json',{'type':type(exc).__name__,'exception':str(exc),
|
| 392 |
+
'traceback':traceback.format_exc()[-16000:],'progress':groups,'train_allowed':False})
|
| 393 |
+
raise
|