wuff-mann commited on
Commit
2fe08f9
·
verified ·
1 Parent(s): a143011

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