Files
digital-drosophila/models/male-cns-lif/normal-conditioning-v1/normal_runner.py
T
2026-09-30 21:00:33 +08:00

342 lines
24 KiB
Python

"""Serial normal conditioning. Original R/E/U executables stay unchanged.
Every fresh brain trial is followed by original E and independent original-U
recomputation in separate exec processes. Probes never run E or feed back.
Only fully committed steps may be skipped on explicit resume. An uncommitted
start, failed child or partial task always stops; no inferred OS exit or retry.
"""
from __future__ import annotations
import argparse
import copy
from datetime import datetime, timezone
import os
import shutil
import subprocess
import sys
import time
import traceback
import numpy as np
from normal_io import *
from append_ledger import Ledger
def registration(expected):
require(digest(EXP/'protocol/preregistered.json')==expected,'Registration hash differs')
reg=read(EXP/'protocol/preregistered.json')
require(reg['schema']=='normal-conditioning-registration-v1' and reg['scientific_scope']=='normal-acquisition-only'
and reg['subsequent_assays_authorized'] is False,'Wrong authority')
verify(reg['source_sha256']);verify(reg['input_sha256']);verify(reg['prepared_sha256'])
return reg
def derive_spec(original,task,*,budget_root,output_cap):
require(original['kind']=='fullgraph' and original['dt_ms']==.1 and original['apl_delay_steps']==18 and
original['precision']=='float64' and original['apl_precision']=='float32' and original['record_history'] is False,
'Original fixed D18 spec required')
require(task['steps']==1200 and task['kind'] in ('train','probe'),'Wrong task')
allowed=('validation_cohort','odor_index','expected_external_events','training','cs_input_seed','cs_input_seed_bits',
'seed_scope','odor_id','odor_name','source_panel_row','source_panel_split','rate_sum_hz','target_count',
'expected_CS_requests','CS_window','outer_epoch_id','outer_arm','outer_protocol')
result={k:copy.deepcopy(v) for k,v in original.items() if k not in allowed}
result.update(name=task_key(task),steps=1200,duration_ms=120.,max_steps=1200,budget_root=str(budget_root),
output_budget_bytes=output_cap,mode=task['mode'],within_trial_weight_updates=False,
outer_protocol='normal-acquisition-v1',outer_task_id=task['task_id'],outer_arm=task['arm'])
# The frozen independent reset-control seed is retained, not derived from CS.
require(result['seed']==original['seed'],'Reset seed changed')
return result
def compose_requests(cs,dan,task,inputs):
require(task['steps']==1200,'Only normal120ms tasks')
if task['kind']=='probe':
recipe=task['recipe'];require(recipe['US_requested'] is False and recipe['D_command']==0 and
recipe['weight_updates_enabled'] is False and task['feedback_to_training_or_idle'] is False,'Probe command differs')
nodes=np.empty(0,np.int32)
else:
recipe=task['recipe'];us=recipe['us']
require(us['nominal_slot_tick']==900 and us['rng_seed'] is None and us['rng_draws']==0 and
((us['requested'] is True and us['tick']==900) or (us['requested'] is False and us['tick'] is None)),
'Scheduled artificial US differs')
require(recipe['D_command']==(1. if us['requested'] and task['arm']!='d_zero' else 0.) and
recipe['update_enabled'] is (task['arm']!='frozen'),'D/update command differs')
require(dan.dtype==np.int32 and dan.ndim==1 and len(dan) in (2,4) and np.all(dan[1:]>dan[:-1]),'Bound DAN IDs required')
nodes=dan.copy() if us['requested'] else np.empty(0,np.int32)
us=dict(tick=np.full(len(nodes),900,np.int32),neuron_idx=nodes)
composed=inputs.merge_requests(cs,us,steps=1200,conflict_policy='error')
return composed,us
class Workflow:
def __init__(self,sha):
self.sha=sha;self.reg=registration(sha);self.start=time.perf_counter()
require(not (EXP/'audit/stopped.json').exists() and not (EXP/'manifest.json').exists(), 'Stopped or sealed phase cannot run')
self.plan=read(EXP/'protocol/normal-plan.json');self.input_plan=read(EXP/'protocol/input-inventory.json')
self.tasks={x['task_id']:x for x in self.plan['tasks']}
self.h=load_source('_normal_AP_helpers',AP/'isolated_workers.py',AP_SHA)
self.inputs=load_source('_normal_request_composer',LAB/'conditioning_inputs.py',self.reg['static_input_sha256'][str(LAB/'conditioning_inputs.py')])
self.metric=load_source('_normal_CPU_state_validation',LAB/'metrics.py',self.reg['input_sha256'][str(LAB/'metrics.py')])
self.nodes=np.load(Path(read(EXP/'protocol/case-template.json')['graph_dir'])/'nodes.npy',allow_pickle=False)
self.populations=npz(self.reg['population_file'])
initial={Path(p).relative_to(EXP).as_posix():identity(p) for p in self.reg['prepared_sha256']}
initial['protocol/preregistered.json']=identity(EXP/'protocol/preregistered.json')
self.ledger=Ledger(EXP,sha,initial,self.reg['budget']['output_cap_bytes'],self.reg['budget']['minimum_free_reserve_bytes'])
status=self.ledger.inspect();require(not status['uncommitted_entry_ids'],'Uncommitted OS state unknown; no automatic retry')
self.completed=set(status['entry_ids']);self.expected=[]
def basepins(self):
return {**self.reg['runtime_input_sha256'],str(EXP/'protocol/preregistered.json'):self.sha}
def immutable_pin(self,path):
path=unlinked(path);rel=path.relative_to(EXP).as_posix()
require(rel in self.ledger._tracked and identity(path)==self.ledger._tracked[rel],
'Consumed immutable file differs from committed bytes: '+rel)
return dict(path=str(path),sha256=self.ledger._tracked[rel]['sha256'])
def budget(self):
require(time.perf_counter()-self.start < self.reg['budget']['workflow_timeout_seconds'],'Invocation wall cap exceeded')
require(datetime.now(timezone.utc)<datetime.fromisoformat(self.reg['budget']['not_after_utc']),
'Registered absolute deadline exceeded; resume cannot reset wall allowance')
require(shutil.disk_usage(EXP).free>=self.reg['budget']['minimum_free_reserve_bytes'],'Free space reserve exceeded')
# Running totals account only committed payload, ledger and shared build.
# Each child also gets a fresh per-child output allowance; full tree at final.
accounting=self.ledger._budget()
require(accounting['total_accounted_bytes']+self.reg['budget']['next_child_headroom_bytes'] <=
self.reg['budget']['output_cap_bytes'],'No registered headroom for another complete child')
def launch(self,name,job,argv,outputs,*,pre_outputs=(),post=None,ap=False):
self.expected.append(name)
require(name not in self.completed,'Internal attempt to rerun committed job')
self.budget()
unlinked(EXP/'receipts'/name).mkdir()
jobfile=EXP/f'jobs/{name}.json';save(jobfile,job);jobsha=digest(jobfile)
argv=[*argv]
if ap:argv+=['--job',str(jobfile),'--job-sha256',jobsha]
start=self.ledger.begin(name,jobfile,dict(kind=job['kind']))
stdout=EXP/f'logs/{name}.stdout.log';stderr=EXP/f'logs/{name}.stderr.log'
process=EXP/f'receipts/{name}/process.json'
started=time.perf_counter();rc=None;timed=False;launch_error=None
env=dict(os.environ);env['PYTHONDONTWRITEBYTECODE']='1'
if job['kind'] not in ('brain','sample'):env['CUDA_VISIBLE_DEVICES']=''
with unlinked(stdout).open('xb') as out,unlinked(stderr).open('xb') as err:
try:rc=subprocess.run(argv,cwd=MODEL,env=env,stdout=out,stderr=err,
timeout=self.reg['budget']['child_timeout_seconds']).returncode
except subprocess.TimeoutExpired:timed=True
except OSError as error:launch_error=type(error).__name__+': '+str(error)
base=dict(entry_id=name,registration_sha256=self.sha,job_sha256=jobsha,argv=argv,
exit_code=rc,timed_out=timed,exit_observed_by_parent=rc is not None or timed,launch_error=launch_error,
child_wall_seconds=time.perf_counter()-started,observed_utc=datetime.now(timezone.utc).isoformat(),
stdout_sha256=digest(stdout),stderr_sha256=digest(stderr))
try:
require(type(rc)is int and rc==0 and not timed,'Child failed; retain outputs and stop')
verify(job['input_sha256'])
if ap:
receipt=read(job['receipt_file'])
require(receipt['schema']=='phase4ap-isolated-worker-receipt-v1' and receipt['complete'] is True and
receipt['job_sha256']==jobsha and receipt['registration_sha256']==self.sha and
receipt['kind']==job['kind'] and receipt['job_id']==name,'Actual child receipt differs')
expected={**job['input_sha256'],str(jobfile):jobsha,str(AP/'isolated_workers.py'):AP_SHA,
**{v['path']:v['sha256'] for v in job['sources'].values()}}
require(receipt['verified_input_sha256']==expected,'Worker input closure differs')
actual={str(Path(p).relative_to(job['output_dir'])):v['sha256'] for p,v in inventory(Path(job['output_dir'])).items()}
require(actual==receipt['output_files_sha256'],'Worker output inventory differs')
require(digest(job['receipt_file']+'.attempt.json')==receipt['attempt_sha256'],'Attempt changed')
if post is not None:post()
base.update(complete=True,end_to_end_seconds=time.perf_counter()-started)
save(process,base)
files=[*pre_outputs,jobfile,stdout,stderr,process]
for p in outputs:
p=Path(p);files.extend(Path(x) for x in inventory(p)) if p.is_dir() else files.append(p)
outgoing={str(p):identity(p) for p in files}
incoming={p:identity(p) for p in job['input_sha256']}
incoming[str(jobfile)]=identity(jobfile)
self.ledger.append(name,incoming,outgoing,dict(job_file=str(jobfile),process_receipt_file=str(process),
observed_returncode=rc,scope='actual serial child exit and immutable artifact verification'))
self.completed.add(name)
print(json.dumps(dict(complete=True,entry_id=name,wall_seconds=base['end_to_end_seconds']),sort_keys=True),flush=True)
except BaseException as error:
if not process.exists():save(process,dict(base,complete=False,error=str(error)))
raise
def input_generation(self):
name='input-generation'
if name in self.completed:self.expected.append(name);return
pins={**self.reg['source_sha256'],**self.reg['static_input_sha256'],
**{p:s for p,s in self.reg['prepared_sha256'].items() if '/inputs/static/' in p or p.endswith('/input-inventory.json')},
str(EXP/'protocol/preregistered.json'):self.sha}
job=dict(kind='sample',registration_sha256=self.sha,input_sha256=pins)
def validate_sample():
report=read(EXP/'receipts/input-generation.json')
require(report['complete'] is True and report['registration_sha256']==self.sha and
set(report['inputs'])==set(self.input_plan['descriptors']) and report['descriptors']==4080,
'Incomplete original sampling inventory')
for key,d in self.input_plan['descriptors'].items():
f=EXP/f'inputs/generated/{key}.npz';a=EXP/f'inputs/generated/{key}.json'
audit=read(a)
require(audit['descriptor']==d and audit['requests_sha256']==digest(f) and
report['inputs'][key]==dict(requests=pin(f),audit=pin(a)), 'Saved sampler binding differs')
self.inputs.validate_requests(npz(f),1200)
self.launch(name,job,[sys.executable,'-B',str(MODEL/'normal_inputs.py'),'--registration-sha256',self.sha],
[EXP/'inputs/generated',EXP/'receipts/input-generation.json'],post=validate_sample)
def overlay(self,task):
self.overlay_dependency_pins={}
dependency=task['delta_dependency']
if dependency==dict(kind='zero',source=None):return copy.deepcopy(self.reg['initial_overlay'])
require(dependency['kind']=='epoch_delta_after' and dependency['source'].endswith('/delta-after'),'Unknown delta dependency')
previous=self.tasks[dependency['source'][:-len('/delta-after')]]
key=task_key(previous);require(key+'--verify' in self.completed,'Previous real E+CPU validation missing')
result_path=EXP/f'results/{key}.json'
def committed(path):
checked=self.immutable_pin(path)
self.overlay_dependency_pins[checked['path']]=checked['sha256']
committed(result_path);record=read(result_path)
require(record['complete'] is True and record['task']==previous and previous['kind']=='train' and
all(previous[k]==task[k] for k in ('block_id','arm')),'Delta cannot cross unrelated branch')
validation=Path(record['cpu_validation']['path']);committed(validation)
require(digest(validation)==record['cpu_validation']['sha256'],'CPU receipt changed')
valid=read(validation)
committed(Path(record['cpu_process_receipt_path']))
process=read(record['cpu_process_receipt_path'])
require(process['complete'] is True and type(process['exit_code']) is int and process['exit_code']==0 and
process['entry_id']==key+'--verify' and process['registration_sha256']==self.sha,
'Previous CPU exit missing')
for role,filename,field in (('overlay_file','delta-after.npz','after_overlay_sha256'),
('overlay_manifest','overlay-manifest.json','after_manifest_sha256')):
path=Path(record['after_overlay'][role]);committed(path)
require(path==Path(record['update_dir'])/filename and digest(path)==valid[field],
'Next installed delta is not the actual independently verified output')
return copy.deepcopy(record['after_overlay'])
def prepare_case(self,task):
key=task_key(task);case=unlinked(EXP/'cases'/key);case.mkdir()
template_file=EXP/'protocol/case-template.json'
checked=self.immutable_pin(template_file)
self.case_dependency_pins={checked['path']:checked['sha256'],
self.reg['population_file']:self.reg['input_sha256'][self.reg['population_file']]}
template=read(template_file)
spec=derive_spec(template,task,budget_root=EXP,output_cap=self.reg['budget']['output_cap_bytes'])
csid=self.input_plan['task_inputs'][task['task_id']]
if csid is None:cs=dict(tick=np.empty(0,np.int32),neuron_idx=np.empty(0,np.int32));cs_source=None
else:
cs_source=EXP/f'inputs/generated/{csid}.npz'
audit_file=EXP/'inputs/generated'/(csid+'.json')
for path in (cs_source,audit_file):
checked=self.immutable_pin(path);self.case_dependency_pins[checked['path']]=checked['sha256']
generated=read(audit_file)
require(generated['descriptor']==self.input_plan['descriptors'][csid] and
generated['requests_sha256']==digest(cs_source),'Saved shared CS changed')
cs=npz(cs_source)
combined,us=compose_requests(cs,self.populations[task['dan_type']],task,self.inputs)
save(case/'spec.json',spec);save_npz(case/'requests.npz',combined['requests'])
save_npz(case/'cs_requests.npz',cs);save_npz(case/'us_requests.npz',us)
save_npz(case/'source-mask.npz',dict(source_mask=combined['source_mask']))
save(case/'input-binding.json',dict(task_id=task['task_id'],CS_source=None if cs_source is None else pin(cs_source),
CS_descriptor_id=csid,composition=combined['audit'],DAN_type=task['dan_type'],
DAN_membership_sha256=hashlib.sha256(self.populations[task['dan_type']].tobytes()).hexdigest(),
population_file=pin(self.reg['population_file']),artificial_US_not_dopamine_concentration=True))
return case
def ap_job(self,name,kind,output,*,case=None,overlay=None,authority=None):
pins=self.basepins()
job=dict(schema='phase4ap-isolated-worker-job-v1',job_id=name,kind=kind,registration_sha256=self.sha,
worker_sha256=AP_SHA,output_dir=str(output),receipt_file=str(EXP/f'receipts/{name}/worker.json'))
if kind=='brain':
pins.update(self.overlay_dependency_pins)
pins.update(self.case_dependency_pins)
for p in (case/'spec.json',case/'requests.npz',overlay['overlay_file'],overlay['overlay_manifest']):pins[str(p)]=digest(p)
job.update(case_dir=str(case),build_root=str(EXP/'extension-build'),**overlay,diagnostic_mode='light',
admission=dict(gpu_uuid=GPU,comfy_queue_urls=list(self.h.COMFY_URLS)),
sources={n:dict(path=str(LAB/(n+'.py')),sha256=self.h.SOURCE_PINS[n]) for n in ('runtime_light','runtime_continuation')})
else:
auth=read(authority);pins[str(authority)]=digest(authority)
pins.update({v['path']:v['sha256'] for v in auth['files'].values()})
pins.update(auth['frozen_inputs']);pins[str(Path(auth['checkpoint_dir'])/'checkpoint-manifest.json')]=auth['checkpoint_manifest_sha256']
job.update(authority_file=str(authority),authority_sha256=digest(authority),
sources={'conditioning_epoch':dict(path=str(LAB/'conditioning_epoch.py'),sha256=self.h.SOURCE_PINS['conditioning_epoch'])})
job['input_sha256']=pins;return job
def brain_verify(self,task,case,output):
summary=read(output/'summary.json');state=npz(output/'state.npz')
checked=self.metric.validate_state(state,nodes=self.nodes,steps=1200)
require(summary['all_finite'] is True and summary['steps']==1200 and summary['diagnostic_mode']=='light','Nonfinite/incomplete brain')
require(int(state['counts'].sum())==summary['total_spikes'],'Spike event/count mismatch')
manifest=read(output/'checkpoint-manifest.json')
for rel,sha in manifest['files_sha256'].items():require(digest(output/rel)==sha,'Checkpoint payload changed')
return dict(complete=True,state_raw_sha256=checked['hashes'],finite=True,
checkpoint_manifest_sha256=digest(output/'checkpoint-manifest.json'),task_id=task['task_id'])
def task(self,task):
key=task_key(task);brain=key+'--brain';up=key+'--update';cpu=key+'--verify'
planned=[brain,up,cpu] if task['kind']=='train' else [brain]
if all(x in self.completed for x in planned):self.expected.extend(planned);return
require(not any(x in self.completed for x in planned),'Partially completed task: preserve and inspect; no implicit replay')
overlay=self.overlay(task);case=self.prepare_case(task);output=EXP/'runs'/key
record=dict(schema='normal-task-result-v1',complete=True,registration_sha256=self.sha,task=task,
checkpoint_dir=str(output),overlay_before=overlay,brain_process_receipt_path=str(EXP/f'receipts/{brain}/process.json'))
def after_brain():
record['brain_audit']=self.brain_verify(task,case,output)
record['checkpoint_manifest_sha256']=digest(output/'checkpoint-manifest.json')
# Parent process receipt is produced later and is bound by the ledger,
# avoiding a self-reference in a result written before process exit audit.
if task['kind']=='probe':save(EXP/f'results/{key}.json',record)
job=self.ap_job(brain,'brain',output,case=case,overlay=overlay)
outs=[output,Path(job['receipt_file']),Path(job['receipt_file']+'.attempt.json')]
if task['kind']=='probe':outs.append(EXP/f'results/{key}.json')
self.launch(brain,job,[sys.executable,'-B',str(AP/'isolated_workers.py'),'brain'],outs,
pre_outputs=[Path(p) for p in inventory(case)],post=after_brain,ap=True)
if task['kind']=='probe':return
template_pin=self.immutable_pin(EXP/'protocol/epoch-template.json')
template=read(template_pin['path']);auth=copy.deepcopy(template)
r=task['recipe'];auth.update(epoch_id=task['task_id'],diagnostic_mode='light',checkpoint_dir=str(output),
checkpoint_manifest_sha256=digest(output/'checkpoint-manifest.json'),
command=dict(steps=1200,compartment=task['compartment_verbatim'],us_tick=r['us']['tick'],d_command=r['D_command'],
frozen=not r['update_enabled'],trace_backend='torch'))
for role,path in dict(spec=case/'spec.json',requests=case/'requests.npz',state=output/'state.npz',
overlay_before=overlay['overlay_file'],overlay_manifest_before=overlay['overlay_manifest']).items():auth['files'][role]=pin(path)
authority=EXP/f'jobs/{key}.authority.json';save(authority,auth);update=EXP/'updates'/key
job=self.ap_job(up,'update',update,authority=authority)
job['input_sha256'][template_pin['path']]=template_pin['sha256']
for path in (EXP/f'receipts/{brain}/process.json',EXP/f'receipts/{brain}/worker.json'):
checked=self.immutable_pin(path);job['input_sha256'][checked['path']]=checked['sha256']
self.launch(up,job,[sys.executable,'-B',str(AP/'isolated_workers.py'),'update'],
[update,Path(job['receipt_file']),Path(job['receipt_file']+'.attempt.json')],pre_outputs=[authority],ap=True)
out=unlinked(EXP/'validation'/key);out.mkdir();validation=out/'validation.json'
script=AQ/'validate_update_cpu.py';verify({str(script):self.reg['input_sha256'][str(script)]})
pins={**job['input_sha256'],str(script):self.reg['input_sha256'][str(script)]}
for name in ('conditioning_epoch.py','conditioning_preflight_compare.py','engineering_compare.py'):
path=str(LAB/name);pins[path]=self.reg['input_sha256'][path]
pins.update({p:v['sha256'] for p,v in inventory(update).items()})
job=dict(kind='cpu_verify',registration_sha256=self.sha,input_sha256=pins,authority_file=str(authority),
authority_sha256=digest(authority),update_dir=str(update),chain_sha256=digest(update/'chain-receipt.json'),
receipt_file=str(validation),validator_sha256=self.reg['input_sha256'][str(script)])
def after_cpu():
valid=read(validation)
require(valid['complete'] is True and valid['frozen_U_verify_receipt_recomputed'] is True and
valid['actual_sealed_events_used'] is True and valid['after_overlay_sha256']==digest(update/'delta-after.npz') and
valid['epoch_authority_sha256']==digest(authority),'Independent update verification differs')
if task['arm'] in ('d_zero','frozen','cs_only'):
require(not npz(update/'delta-after.npz')['plastic_delta_mv'].view(np.uint32).any(),'Zero-update control became nonzero')
record.update(epoch_authority=pin(authority),update_dir=str(update),cpu_validation=pin(validation),
after_overlay=dict(overlay_file=str(update/'delta-after.npz'),overlay_manifest=str(update/'overlay-manifest.json')),
update_process_receipt_path=str(EXP/f'receipts/{up}/process.json'),
cpu_process_receipt_path=str(EXP/f'receipts/{cpu}/process.json'))
save(EXP/f'results/{key}.json',record)
self.launch(cpu,job,[sys.executable,'-B',str(script),'--authority',str(authority),'--authority-sha256',digest(authority),
'--update-dir',str(update),'--chain-sha256',digest(update/'chain-receipt.json'),'--receipt',str(validation)],
[out,EXP/f'results/{key}.json'],post=after_cpu)
def run(self):
try:
self.input_generation()
for task in self.plan['tasks']:self.task(task)
report=self.ledger.final_verify(self.expected)
require(report['pass_all'],'Final ledger coverage/integrity failed')
save(EXP/'audit/execution-complete.json',dict(complete=True,registration_sha256=self.sha,
inventory=self.plan['inventory'],ledger=report,learning_success_claimed=False,
next='independent scientific analysis and frozen integrity, no subsequent assay'))
except BaseException as error:
if not (EXP/'audit/stopped.json').exists():save(EXP/'audit/stopped.json',dict(complete=False,
registration_sha256=self.sha,error_type=type(error).__name__,error=str(error),
traceback=traceback.format_exc(),partial_files_preserved=True,automatic_retry=False))
raise
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--registration-sha256',required=True)
Workflow(p.parse_args().registration_sha256).run()