161 lines
11 KiB
Python
161 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Canonical deterministic MaleCNS LIF reference backend and validation tasks."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import pyarrow as pa
|
|
import pyarrow.parquet as pq
|
|
import torch
|
|
import yaml
|
|
from torch.utils.cpp_extension import load as load_extension
|
|
|
|
import phase4c
|
|
|
|
|
|
def sha_bytes(*arrays: np.ndarray) -> str:
|
|
h=hashlib.sha256()
|
|
for x in arrays:h.update(np.ascontiguousarray(x).tobytes())
|
|
return h.hexdigest()
|
|
|
|
|
|
def save(path: Path,x: object):
|
|
path.parent.mkdir(parents=True,exist_ok=True)
|
|
path.write_text(json.dumps(x,indent=2,sort_keys=True,default=str)+'\n')
|
|
|
|
|
|
def build_extension(model:Path,exp:Path):
|
|
build=exp/'gpu-reference/extension-build';build.mkdir(parents=True,exist_ok=True)
|
|
start=time.perf_counter()
|
|
ext=load_extension(name='male_cns_lif_deterministic_v2_ext',
|
|
sources=[str(model/'phase4p_ext.cpp'),str(model/'phase4p_ext.cu')],
|
|
build_directory=str(build),extra_cflags=['-O3','-ffp-contract=off'],
|
|
extra_cuda_cflags=['-O3','--fmad=false','-std=c++20'],verbose=True)
|
|
report={'status':'PASS','build_seconds':time.perf_counter()-start,'binary':str(Path(ext.__file__)),
|
|
'binary_sha256':phase4c.sha256(Path(ext.__file__)),'flags':{'cxx':['-O3','-ffp-contract=off'],'cuda':['-O3','--fmad=false','-std=c++20']}}
|
|
save(exp/'gpu-reference/extension.json',report);return ext
|
|
|
|
|
|
class CanonicalRuntime:
|
|
def __init__(self,cfg:dict[str,Any],model:Path,exp:Path):
|
|
self.cfg=cfg;self.exp=exp;self.device=torch.device('cuda:0')
|
|
if torch.cuda.device_count()!=1 or cfg['device']['required_name_substring'] not in torch.cuda.get_device_name(0):
|
|
raise RuntimeError('exactly one visible RTX 4090 required')
|
|
p4cfg=yaml.safe_load((Path(cfg['paths']['historical_runtime_root'])/'phase4c.yaml').read_text())
|
|
paths=phase4c.exact_paths(p4cfg)
|
|
self.indptr_np,self.indices_np,self.raw_np=phase4c.p4b.validate_graph(p4cfg,paths)
|
|
self.table=phase4c.full_metadata(paths['metadata']);self.pops=phase4c.populations(self.table)
|
|
base=phase4c.Runtime(p4cfg,self.table,self.indptr_np,self.indices_np,self.raw_np,self.device,None)
|
|
self.indptr,self.indices,self.weights=base.indptr,base.indices,base.weights
|
|
self.ext=build_extension(model,exp)
|
|
m=cfg['model'];self.n=int(cfg['expected_graph']['neurons']);self.dt=float(m['dt_ms'])
|
|
self.delay_steps=int(round(m['synaptic_delay_ms']/self.dt));self.qlen=self.delay_steps+1
|
|
self.ref_steps=int(round(m['refractory_ms']/self.dt));self.gd=np.float32(1-self.dt/m['synaptic_tau_ms'])
|
|
self.alpha=np.float32(self.dt/m['membrane_tau_ms']);self.rest=np.float32(m['resting_potential_mv'])
|
|
self.resetv=np.float32(m['reset_potential_mv']);self.threshold=np.float32(m['threshold_mv'])
|
|
self.superclass=np.asarray(self.table['annotation_superclass'].to_pylist(),object)
|
|
|
|
def reset(self):
|
|
d=self.device;self.v=torch.full((self.n,),float(self.rest),dtype=torch.float32,device=d)
|
|
self.g=torch.zeros(self.n,dtype=torch.float32,device=d);self.ref=torch.zeros(self.n,dtype=torch.int32,device=d)
|
|
self.queue=torch.zeros((self.qlen,self.n),dtype=torch.float32,device=d)
|
|
self.spike_u8=torch.zeros(self.n,dtype=torch.uint8,device=d);self.counts=torch.zeros(self.n,dtype=torch.int32,device=d)
|
|
self.step_index=0;self.event_steps=[];self.event_nodes=[]
|
|
|
|
def canonical_fanout(self,sources:torch.Tensor,destination:torch.Tensor)->dict[str,float|int]:
|
|
if not sources.numel():return {'contributions':0,'segments':0,'sort_seconds':0.,'reduce_seconds':0.}
|
|
starts=self.indptr[sources];degree=self.indptr[sources+1]-starts;cum=torch.cumsum(degree,0);total=int(cum[-1].item())
|
|
edges=torch.empty(total,dtype=torch.int64,device=self.device)
|
|
self.ext.fill_edges(self.indptr,sources,cum-degree,edges)
|
|
t0=time.perf_counter();targets=self.indices[edges]
|
|
order=torch.argsort(targets,stable=True);sorted_edges=edges[order];sorted_targets=targets[order]
|
|
boundary=torch.ones(total,dtype=torch.bool,device=self.device)
|
|
if total>1:boundary[1:]=sorted_targets[1:]!=sorted_targets[:-1]
|
|
seg_starts=torch.nonzero(boundary,as_tuple=False).flatten().to(torch.int64)
|
|
unique_targets=sorted_targets[seg_starts].to(torch.int32);sort_sec=time.perf_counter()-t0
|
|
t1=time.perf_counter();self.ext.segmented_sum(sorted_edges,seg_starts,unique_targets,self.weights,destination)
|
|
return {'contributions':total,'segments':int(len(unique_targets)),'sort_seconds':sort_sec,'reduce_seconds':time.perf_counter()-t1}
|
|
|
|
def run(self,targets_np:np.ndarray,rate:float,seed:int,duration_ms:float,capture=True,forced_override=None):
|
|
self.reset();steps=int(round(duration_ms/self.dt));targets=torch.as_tensor(targets_np,dtype=torch.int64,device=self.device)
|
|
forced=forced_override if forced_override is not None else phase4c.generate_forced(steps,len(targets_np),rate,self.dt,seed,self.device)
|
|
torch.cuda.synchronize();t0=time.perf_counter();sort_t=reduce_t=0.;contrib=0
|
|
for step in range(steps):
|
|
slot=step%self.qlen
|
|
self.ext.state_step(self.v,self.g,self.ref,self.queue[slot],self.spike_u8,self.counts,
|
|
float(self.gd),float(self.alpha),float(self.rest),float(self.resetv),float(self.threshold),self.ref_steps)
|
|
self.ext.force_step(self.v,self.ref,self.spike_u8,self.counts,targets,forced[step],float(self.resetv),self.ref_steps)
|
|
sources=torch.nonzero(self.spike_u8,as_tuple=False).flatten()
|
|
stats=self.canonical_fanout(sources,self.queue[(step+self.delay_steps)%self.qlen])
|
|
sort_t+=stats['sort_seconds'];reduce_t+=stats['reduce_seconds'];contrib+=stats['contributions']
|
|
if capture and sources.numel():
|
|
x=sources.cpu().numpy().astype(np.int32,copy=True);self.event_nodes.append(x);self.event_steps.append(np.full(len(x),step,np.int32))
|
|
self.step_index+=1
|
|
torch.cuda.synchronize();wall=time.perf_counter()-t0
|
|
es=np.concatenate(self.event_steps) if self.event_steps else np.empty(0,np.int32)
|
|
en=np.concatenate(self.event_nodes) if self.event_nodes else np.empty(0,np.int32)
|
|
return {'step':es,'neuron_idx':en,'counts':self.counts.cpu().numpy().copy(),'v':self.v.cpu().numpy().copy(),'g':self.g.cpu().numpy().copy(),
|
|
'metrics':{'wall_seconds':wall,'biological_ms':duration_ms,'realtime_factor':wall/(duration_ms/1000),
|
|
'peak_vram_bytes':int(torch.cuda.max_memory_allocated()),'contributions':int(contrib),
|
|
'sort_submit_seconds':sort_t,'reduce_submit_seconds':reduce_t,'events':len(en),'active':int((self.counts>0).sum().item())}}
|
|
|
|
|
|
def cpu_ordered_sum(edge_ids:np.ndarray,indices:np.ndarray,weights:np.ndarray):
|
|
edge_ids=np.asarray(edge_ids,np.int64);targets=np.asarray(indices[edge_ids],np.int32)
|
|
order=np.argsort(targets,kind='stable');ordered=edge_ids[order];ordered_target=targets[order]
|
|
unique=[];sums=[];begin=0
|
|
while begin<len(ordered):
|
|
target=int(ordered_target[begin]);end=begin+1;acc=np.float32(0)
|
|
while end<len(ordered) and ordered_target[end]==target:end+=1
|
|
for e in ordered[begin:end]:acc=np.float32(acc+np.float32(weights[e]))
|
|
unique.append(target);sums.append(acc);begin=end
|
|
return np.asarray(unique,np.int32),np.asarray(sums,np.float32),ordered
|
|
|
|
|
|
def save_trial(root:Path,run:dict):
|
|
root.mkdir(parents=True,exist_ok=True)
|
|
pq.write_table(pa.table({'step':run['step'],'timestamp_ms':run['step'].astype(np.float64)*.1,'neuron_idx':run['neuron_idx']}),root/'spike-events.parquet',compression='zstd')
|
|
for name,key,dtype in [('per-neuron-spike-counts.int32.npy','counts',np.int32),('final-voltage.float32.npy','v',np.float32),('final-g.float32.npy','g',np.float32)]:np.save(root/name,run[key].astype(dtype),allow_pickle=False)
|
|
save(root/'statistics.json',run['metrics']|{'event_sha256':sha_bytes(run['step'],run['neuron_idx']),
|
|
'counts_sha256':sha_bytes(run['counts']),'v_sha256':sha_bytes(run['v']),'g_sha256':sha_bytes(run['g'])})
|
|
|
|
|
|
def phase4c_trials(cfg,rt,repeats):
|
|
exp=Path(cfg['paths']['experiment_root']);rows=[]
|
|
for spec in cfg['reference_trials']:
|
|
target=np.flatnonzero(rt.pops[spec['population']]).astype(np.int64)
|
|
baseline=None
|
|
for repeat in range(repeats):
|
|
run=rt.run(target,float(spec['rate_hz']),int(spec['seed']),float(spec['duration_ms']),True)
|
|
if baseline is None:
|
|
baseline={k:run[k].copy() for k in ('step','neuron_idx','counts','v','g')};save_trial(exp/'phase4c-v2/reference'/spec['name'],run)
|
|
exact={k:bool(np.array_equal(run[k],baseline[k])) for k in ('step','neuron_idx','counts','v','g')}
|
|
row={'trial':spec['name'],'repeat':repeat,**exact,**run['metrics']};rows.append(row)
|
|
save(exp/'phase4c-v2/reproducibility-matrix.json',rows)
|
|
print(json.dumps({'trial':spec['name'],'repeat':repeat,'exact':all(exact.values()),'wall':run['metrics']['wall_seconds']}),flush=True)
|
|
summary={'repeats':repeats,'matrix':{s['name']:{k:sum(r[k] for r in rows if r['trial']==s['name']) for k in ('step','neuron_idx','counts','v','g')} for s in cfg['reference_trials']},
|
|
'all_exact':all(all(r[k] for k in ('step','neuron_idx','counts','v','g')) for r in rows),
|
|
'performance':{s['name']:{'mean_wall_seconds':float(np.mean([r['wall_seconds'] for r in rows if r['trial']==s['name']])),
|
|
'mean_realtime_factor':float(np.mean([r['realtime_factor'] for r in rows if r['trial']==s['name']])),
|
|
'peak_vram_bytes':max(r['peak_vram_bytes'] for r in rows if r['trial']==s['name']),
|
|
'mean_contributions':float(np.mean([r['contributions'] for r in rows if r['trial']==s['name']]))} for s in cfg['reference_trials']}}
|
|
save(exp/'phase4c-v2/summary.json',summary)
|
|
|
|
|
|
def main():
|
|
ap=argparse.ArgumentParser();ap.add_argument('--config',type=Path,required=True);ap.add_argument('task',choices=['phase4c']);ap.add_argument('--repeats',type=int)
|
|
a=ap.parse_args();cfg=yaml.safe_load(a.config.read_text());exp=Path(cfg['paths']['experiment_root']);model=Path(cfg['paths']['model_root'])
|
|
for sub in ('numerical-contract','cpu-reference','gpu-reference','threshold-3942','fanout-tests','phase4c-v2','historical-comparison','brainio','activity-stream','eligibility-replay','performance','revalidation','regression','audit','manifests'):(exp/sub).mkdir(parents=True,exist_ok=True)
|
|
rt=CanonicalRuntime(cfg,model,exp)
|
|
phase4c_trials(cfg,rt,a.repeats or int(cfg['repeats_per_trial']))
|
|
|
|
|
|
if __name__=='__main__':main()
|