757 lines
43 KiB
Python
757 lines
43 KiB
Python
#!/usr/bin/env python3
|
|
"""Phase 4C: freeze reference, audit visual semantics, and optimize exact runtime."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import platform
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from collections import Counter
|
|
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 phase4b as p4b
|
|
|
|
LOG = logging.getLogger("phase4c")
|
|
|
|
|
|
def sha256(path: Path, chunk: int = 8 << 20) -> str:
|
|
h = hashlib.sha256()
|
|
with path.open("rb") as f:
|
|
while data := f.read(chunk):
|
|
h.update(data)
|
|
return h.hexdigest()
|
|
|
|
|
|
def write_json(path: Path, value: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
|
|
|
|
def copy_immutable(source: Path, destination: Path) -> None:
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
if destination.exists():
|
|
if sha256(source) != sha256(destination):
|
|
raise RuntimeError(f"immutable destination differs: {destination}")
|
|
return
|
|
shutil.copy2(source, destination)
|
|
destination.chmod(0o444)
|
|
|
|
|
|
def exact_paths(cfg: dict[str, Any]) -> dict[str, Path]:
|
|
root = Path(cfg["paths"]["connectome_root"]).resolve()
|
|
allowed = Path("/data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only").resolve()
|
|
if root != allowed:
|
|
raise RuntimeError(f"only allowed input is {allowed}")
|
|
paths = {
|
|
"indptr": root / "gpu/csr-indptr.int64.npy",
|
|
"indices": root / "gpu/csr-indices.int32.npy",
|
|
"raw_weights": root / "gpu/csr-weights.int64.npy",
|
|
"metadata": root / "neuron-index/unified-metadata.parquet",
|
|
"mapping": root / "neuron-index/body-id-neuron-idx.parquet",
|
|
"csr_metadata": root / "gpu/csr-metadata.json",
|
|
}
|
|
for path in paths.values():
|
|
if not path.is_file():
|
|
raise FileNotFoundError(path)
|
|
return paths
|
|
|
|
|
|
def freeze_reference(cfg: dict[str, Any], config_path: Path, paths: dict[str, Path]) -> dict[str, Any]:
|
|
model_root = Path(cfg["paths"]["model_root"])
|
|
ref = model_root / "reference-v1"
|
|
ref.mkdir(parents=True, exist_ok=True)
|
|
source_dir = Path(__file__).resolve().parent
|
|
phase4b_root = Path(cfg["paths"]["phase4b_root"])
|
|
source_files = {
|
|
"model.py": source_dir / "phase4b.py",
|
|
"config.yaml": source_dir / "phase4b.yaml",
|
|
}
|
|
for name, source in source_files.items():
|
|
copy_immutable(source, ref / name)
|
|
phase4b_seal = phase4b_root / "manifests/checksums.sha256"
|
|
phase4b_manifest = phase4b_root / "manifests/manifest.json"
|
|
if not phase4b_seal.is_file() or not phase4b_manifest.is_file():
|
|
raise RuntimeError("Phase 4B manifest/seal missing")
|
|
seal_check = subprocess.run(
|
|
["sha256sum", "-c", "--quiet", str(phase4b_seal)], cwd=phase4b_root,
|
|
text=True, capture_output=True,
|
|
)
|
|
if seal_check.returncode:
|
|
raise RuntimeError(f"Phase 4B seal failed: {seal_check.stderr}")
|
|
frozen = {
|
|
"name": cfg["names"]["reference"],
|
|
"description": cfg["names"]["description"],
|
|
"not_claimed": "strict Shiu 2024 original-model reproduction",
|
|
"reason": "MaleCNS v1.0 transmitter inventory includes histamine and differs from the original dataset/inventory",
|
|
"parameters": cfg["model"],
|
|
"sign_policy": cfg["sign_policy"],
|
|
"graph": cfg["expected_graph"],
|
|
"source_phase": "Phase 4B PASS",
|
|
"phase4b_manifest_sha256": sha256(phase4b_manifest),
|
|
"phase4b_checksums_sha256": sha256(phase4b_seal),
|
|
"phase4b_checksum_verification": "PASS",
|
|
"local_repository_commit": cfg["reproducibility"]["local_repository_commit"],
|
|
"remote_model_directory_is_git_repository": False,
|
|
"code": {name: {"path": str(ref / name), "sha256": sha256(ref / name)} for name in source_files},
|
|
"inputs": {name: {"path": str(path), "sha256": sha256(path)} for name, path in paths.items()},
|
|
}
|
|
version_path = ref / "VERSION.json"
|
|
if version_path.exists():
|
|
previous = json.loads(version_path.read_text(encoding="utf-8"))
|
|
if previous != frozen:
|
|
raise RuntimeError("existing immutable reference VERSION differs")
|
|
else:
|
|
write_json(version_path, frozen)
|
|
version_path.chmod(0o444)
|
|
return frozen
|
|
|
|
|
|
def full_metadata(path: Path) -> pa.Table:
|
|
table = pq.read_table(path)
|
|
required = [
|
|
"neuron_idx", "body_id", "annotation_superclass", "annotation_class",
|
|
"annotation_subclass", "annotation_type", "annotation_instance", "side",
|
|
"neurotransmitter",
|
|
]
|
|
missing = [x for x in required if x not in table.column_names]
|
|
if missing:
|
|
raise RuntimeError(f"metadata columns missing: {missing}")
|
|
return table
|
|
|
|
|
|
def list_mask(values: list[Any], accepted: set[str]) -> np.ndarray:
|
|
return np.asarray([x in accepted for x in values], dtype=bool)
|
|
|
|
|
|
def populations(table: pa.Table) -> dict[str, np.ndarray]:
|
|
cls = table["annotation_class"].to_pylist()
|
|
sub = table["annotation_subclass"].to_pylist()
|
|
mech = {"mechanosensory", "mechanosensory_tactile", "mechanosensory_proprioceptive"}
|
|
return {
|
|
"visual": np.asarray([x == "visual" for x in cls]),
|
|
"mechanosensory": np.asarray([(x in mech) and y != "auditory" for x, y in zip(cls, sub)]),
|
|
"olfactory": np.asarray([x == "olfactory" for x in cls]),
|
|
"gustatory": np.asarray([x == "gustatory" for x in cls]),
|
|
"auditory": np.asarray([x == "auditory" for x in sub]),
|
|
}
|
|
|
|
|
|
def layer_edge_stats(indptr: np.ndarray, indices: np.ndarray, raw: np.ndarray,
|
|
layers: dict[str, np.ndarray]) -> dict[str, Any]:
|
|
n = len(indptr) - 1
|
|
label = np.full(n, -1, dtype=np.int16)
|
|
names = list(layers)
|
|
for j, name in enumerate(names):
|
|
label[layers[name]] = j
|
|
counts = np.zeros((len(names), len(names)), dtype=np.int64)
|
|
weights = np.zeros_like(counts)
|
|
for src in range(n):
|
|
a, b = int(indptr[src]), int(indptr[src + 1])
|
|
x = label[src]
|
|
if x < 0 or a == b:
|
|
continue
|
|
y = label[indices[a:b]]
|
|
valid = y >= 0
|
|
if np.any(valid):
|
|
counts[x] += np.bincount(y[valid], minlength=len(names))
|
|
weights[x] += np.bincount(y[valid], weights=raw[a:b][valid], minlength=len(names)).astype(np.int64)
|
|
return {
|
|
"layer_order": names,
|
|
"edge_counts": counts.tolist(),
|
|
"synapse_weight_sums": weights.tolist(),
|
|
}
|
|
|
|
|
|
def visual_audit(exp: Path, table: pa.Table, indptr: np.ndarray,
|
|
indices: np.ndarray, raw: np.ndarray) -> tuple[dict[str, Any], dict[str, np.ndarray]]:
|
|
out = exp / "visual-audit"
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
pops = populations(table)
|
|
visual = pops["visual"]
|
|
nodes = np.flatnonzero(visual).astype(np.int32)
|
|
fields = [
|
|
"neuron_idx", "body_id", "annotation_superclass", "annotation_class",
|
|
"annotation_subclass", "annotation_type", "annotation_instance", "side", "neurotransmitter",
|
|
]
|
|
visual_table = table.select(fields).filter(pa.array(visual))
|
|
pq.write_table(visual_table, out / "phase4b-visual-6091.parquet", compression="zstd")
|
|
arrays = {name: table[name].to_pylist() for name in fields}
|
|
breakdown: dict[str, Any] = {}
|
|
for field in fields[2:]:
|
|
c = Counter("<null>" if arrays[field][i] is None else str(arrays[field][i]) for i in nodes)
|
|
breakdown[field] = dict(sorted(c.items(), key=lambda x: (-x[1], x[0])))
|
|
visual_types = sorted({str(arrays["annotation_type"][i]) for i in nodes if arrays["annotation_type"][i] is not None})
|
|
exact_photoreceptor = visual & list_mask(arrays["annotation_superclass"], {"ol_sensory"})
|
|
|
|
# These anatomical memberships are derived only from explicit metadata fields.
|
|
super_values = arrays["annotation_superclass"]
|
|
type_values = ["" if x is None else str(x) for x in arrays["annotation_type"]]
|
|
ol_intrinsic = list_mask(super_values, {"ol_intrinsic"})
|
|
visual_projection = list_mask(super_values, {"visual_projection", "visual_projection_tbc"})
|
|
lamina_types = {"L1", "L2", "L3", "L4", "L5", "C2", "C3", "T1", "Lawf1", "Lawf2"}
|
|
lamina = ol_intrinsic & np.asarray([x in lamina_types for x in type_values])
|
|
lobula_plate = ol_intrinsic & np.asarray([
|
|
x.startswith(("T4", "T5", "LPi", "LPLC", "LLPC")) for x in type_values
|
|
])
|
|
lobula = ol_intrinsic & ~lobula_plate & np.asarray([
|
|
x.startswith(("LC", "LT", "Li", "Lobula")) for x in type_values
|
|
])
|
|
medulla = ol_intrinsic & ~lamina & ~lobula & ~lobula_plate & np.asarray([
|
|
x.startswith(("Mi", "Dm", "Tm", "TmY", "Pm", "Cm", "Me")) for x in type_values
|
|
])
|
|
optic_other = ol_intrinsic & ~(lamina | medulla | lobula | lobula_plate)
|
|
anatomy = {
|
|
"photoreceptor": exact_photoreceptor,
|
|
"retinal_sensory": exact_photoreceptor,
|
|
"lamina": lamina,
|
|
"medulla": medulla,
|
|
"lobula": lobula,
|
|
"lobula_plate": lobula_plate,
|
|
"optic_lobe_interneurons": ol_intrinsic,
|
|
"visual_projection_neurons": visual_projection,
|
|
"optic_lobe_interneurons_unassigned_to_named_region": optic_other,
|
|
}
|
|
membership_rows: list[dict[str, Any]] = []
|
|
for role, mask in anatomy.items():
|
|
for i in np.flatnonzero(mask):
|
|
membership_rows.append({"anatomical_role": role, "neuron_idx": int(i), "body_id": int(arrays["body_id"][i])})
|
|
pq.write_table(pa.Table.from_pylist(membership_rows), out / "anatomical-memberships.parquet", compression="zstd")
|
|
|
|
# Exact graph hierarchy uses shortest directed distance, not inferred body IDs.
|
|
distance = p4b.directed_distances(indptr, indices, nodes.astype(np.int64), len(visual))
|
|
optic_super = {"ol_intrinsic", "visual_projection", "visual_projection_tbc", "visual_centrifugal"}
|
|
optic = list_mask(super_values, optic_super)
|
|
layers = {
|
|
"visual_sensory": distance == 0,
|
|
"first_order": distance == 1,
|
|
"second_order": distance == 2,
|
|
"higher_visual": (distance >= 3) & optic,
|
|
"central_brain": (distance >= 3) & ~optic,
|
|
}
|
|
for name, mask in layers.items():
|
|
ids = np.flatnonzero(mask).astype(np.int32)
|
|
pq.write_table(pa.table({
|
|
"neuron_idx": pa.array(ids, type=pa.int32()),
|
|
"body_id": pa.array(np.asarray(arrays["body_id"], dtype=np.uint64)[ids], type=pa.uint64()),
|
|
"shortest_visual_path_depth": pa.array(distance[ids], type=pa.int16()),
|
|
}), out / f"hierarchy-{name}.parquet", compression="zstd")
|
|
edge_stats = layer_edge_stats(indptr, indices, raw, layers)
|
|
write_json(out / "hierarchy-edge-statistics.json", edge_stats)
|
|
summary = {
|
|
"phase4b_visual_count": int(visual.sum()),
|
|
"all_histaminergic": all(arrays["neurotransmitter"][i] == "histamine" for i in nodes),
|
|
"all_ol_sensory": all(arrays["annotation_superclass"][i] == "ol_sensory" for i in nodes),
|
|
"raw_visual_types": visual_types,
|
|
"conclusion": "The 6091 annotation_class=visual bodies are photoreceptor/retinal sensory input neurons, not the whole visual processing network.",
|
|
"field_breakdown": breakdown,
|
|
"anatomical_membership_counts": {k: int(v.sum()) for k, v in anatomy.items()},
|
|
"anatomical_rule_provenance": {
|
|
"photoreceptor_retinal_sensory": "annotation_class=visual AND annotation_superclass=ol_sensory; observed types recorded verbatim",
|
|
"optic_lobe_interneurons": "annotation_superclass=ol_intrinsic",
|
|
"visual_projection_neurons": "annotation_superclass in {visual_projection, visual_projection_tbc}",
|
|
"named_optic_regions": "explicit annotation_type prefix/set rules, preserved in this audit; no body_id was guessed",
|
|
},
|
|
"hierarchy_definition": "exclusive shortest directed connectome distance from the 6091 sensory neurons: depth 0 sensory, depth 1 first-order, depth 2 second-order, depth >=3 split into optic/visual-superclass higher_visual versus non-optic downstream central_brain; unreachable bodies are excluded",
|
|
"hierarchy_counts": {k: int(v.sum()) for k, v in layers.items()},
|
|
"edge_statistics_file": str(out / "hierarchy-edge-statistics.json"),
|
|
}
|
|
write_json(out / "summary.json", summary)
|
|
return summary, layers
|
|
|
|
|
|
def generate_forced(steps: int, target_count: int, rate_hz: float, dt: float,
|
|
seed: int, device: torch.device) -> torch.Tensor:
|
|
probability = rate_hz * dt / 1000.0
|
|
generator = torch.Generator(device=device)
|
|
generator.manual_seed(seed)
|
|
forced = torch.empty((steps, target_count), dtype=torch.uint8, device=device)
|
|
# Deliberately preserve Phase 4B's one rand() call per timestep.
|
|
for step in range(steps):
|
|
forced[step].copy_((torch.rand(target_count, generator=generator, device=device) < probability).to(torch.uint8))
|
|
return forced
|
|
|
|
|
|
def expand_rows(indptr: torch.Tensor, sources: torch.Tensor) -> torch.Tensor:
|
|
if sources.numel() == 0:
|
|
return torch.empty(0, dtype=torch.int64, device=indptr.device)
|
|
starts = indptr[sources]
|
|
counts = indptr[sources + 1] - starts
|
|
cumulative = torch.cumsum(counts, 0)
|
|
total = int(cumulative[-1].item()) if cumulative.numel() else 0
|
|
if total == 0:
|
|
return torch.empty(0, dtype=torch.int64, device=indptr.device)
|
|
seq = torch.arange(total, dtype=torch.int64, device=indptr.device)
|
|
row = torch.searchsorted(cumulative, seq, right=True)
|
|
return starts[row] + seq - (cumulative - counts)[row]
|
|
|
|
|
|
class Runtime:
|
|
def __init__(self, cfg: dict[str, Any], table: pa.Table, indptr_np: np.ndarray,
|
|
indices_np: np.ndarray, raw_np: np.ndarray, device: torch.device,
|
|
extension: Any | None):
|
|
self.cfg, self.table, self.device, self.extension = cfg, table, device, extension
|
|
self.n = int(cfg["expected_graph"]["neurons"])
|
|
self.indptr = torch.as_tensor(np.asarray(indptr_np), dtype=torch.int64, device=device)
|
|
self.indices = torch.as_tensor(np.asarray(indices_np), dtype=torch.int32, device=device)
|
|
nt = table["neurotransmitter"].to_pylist()
|
|
policy = cfg["sign_policy"]
|
|
signs = np.empty(self.n, dtype=np.int8)
|
|
for i, value in enumerate(nt):
|
|
key = "" if value is None else str(value).strip().lower()
|
|
if not key:
|
|
signs[i] = int(policy["unknown_sign"])
|
|
elif key == "unclear":
|
|
signs[i] = int(policy["unclear_sign"])
|
|
elif key in policy["neurotransmitter_sign"]:
|
|
signs[i] = int(policy["neurotransmitter_sign"][key])
|
|
else:
|
|
raise RuntimeError(f"unmapped neurotransmitter: {value!r}")
|
|
effective = np.empty(len(raw_np), dtype=np.float32)
|
|
for src in range(self.n):
|
|
a, b = int(indptr_np[src]), int(indptr_np[src + 1])
|
|
effective[a:b] = raw_np[a:b].astype(np.float32) * signs[src] * float(cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
|
self.weights = torch.as_tensor(effective, dtype=torch.float32, device=device)
|
|
self.signs = signs
|
|
self.superclass = np.asarray(table["annotation_superclass"].to_pylist(), dtype=object)
|
|
|
|
def compiled_state(self, mode: str):
|
|
m = self.cfg["model"]
|
|
gd = 1.0 - float(m["dt_ms"]) / float(m["synaptic_tau_ms"])
|
|
alpha = float(m["dt_ms"]) / float(m["membrane_tau_ms"])
|
|
rest, reset, threshold = (float(m[x]) for x in ("resting_potential_mv", "reset_potential_mv", "threshold_mv"))
|
|
|
|
def fn(v, g, refractory, arrival):
|
|
g2 = g * gd
|
|
g2 = g2 + arrival
|
|
r2 = torch.clamp_min(refractory, 1) - 1
|
|
available = r2 == 0
|
|
leak = v - rest
|
|
drive = g2 - leak
|
|
integrated = v + alpha * drive
|
|
v2 = torch.where(available, integrated, torch.full_like(v, reset))
|
|
natural = available & (v2 >= threshold)
|
|
return v2, g2, r2, natural, available
|
|
|
|
kwargs: dict[str, Any] = {"backend": "inductor", "fullgraph": True}
|
|
if mode == "compile-reduce-overhead":
|
|
kwargs["mode"] = "reduce-overhead"
|
|
return torch.compile(fn, **kwargs), fn
|
|
|
|
def run(self, backend: str, targets_np: np.ndarray, rate_hz: float, seed: int,
|
|
duration_ms: float, capture_events: bool, monitor: bool = False,
|
|
forced_override: torch.Tensor | None = None) -> dict[str, Any]:
|
|
m = self.cfg["model"]
|
|
dt = float(m["dt_ms"]); steps = int(round(duration_ms / dt))
|
|
delay_steps = int(round(float(m["synaptic_delay_ms"]) / dt)); qlen = delay_steps + 1
|
|
ref_steps = int(round(float(m["refractory_ms"]) / dt))
|
|
alpha = dt / float(m["membrane_tau_ms"]); gd = 1.0 - dt / float(m["synaptic_tau_ms"])
|
|
rest = float(m["resting_potential_mv"]); reset = float(m["reset_potential_mv"]); threshold = float(m["threshold_mv"])
|
|
targets = torch.as_tensor(targets_np, dtype=torch.int64, device=self.device)
|
|
forced = forced_override if forced_override is not None else generate_forced(steps, len(targets_np), rate_hz, dt, seed, self.device)
|
|
if forced.shape != (steps, len(targets_np)):
|
|
raise RuntimeError("forced input shape mismatch")
|
|
v = torch.full((self.n,), rest, dtype=torch.float32, device=self.device)
|
|
g = torch.zeros(self.n, dtype=torch.float32, device=self.device)
|
|
refractory = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
|
queue = torch.zeros((qlen, self.n), dtype=torch.float32, device=self.device)
|
|
counts = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
|
event_steps: list[np.ndarray] = []; event_nodes: list[np.ndarray] = []
|
|
compiled = eager_fn = None
|
|
if backend.startswith("compile-"):
|
|
compiled, eager_fn = self.compiled_state(backend)
|
|
# Exclude compile latency and record a graph-break diagnostic separately.
|
|
tv, tg, tr, ta = v.clone(), g.clone(), refractory.clone(), queue[0].clone()
|
|
compiled(tv, tg, tr, ta); torch.cuda.synchronize()
|
|
del tv, tg, tr, ta
|
|
if backend.startswith("custom") or backend in ("cudagraph-custom-state", "hybrid-exact"):
|
|
if self.extension is None:
|
|
raise RuntimeError("custom extension unavailable")
|
|
spike_u8 = torch.zeros(self.n, dtype=torch.uint8, device=self.device)
|
|
graph = staging = None
|
|
if backend == "cudagraph-custom-state":
|
|
staging = torch.zeros(self.n, dtype=torch.float32, device=self.device)
|
|
graph = torch.cuda.CUDAGraph()
|
|
# Warm-up on disposable state, then capture only the graph-safe fixed-shape state kernel.
|
|
sv, sg = v.clone(), g.clone(); sr = refractory.clone(); ss = spike_u8.clone(); sc = counts.clone(); sa = staging.clone()
|
|
self.extension.state_step(sv, sg, sr, sa, ss, sc, gd, alpha, rest, reset, threshold, ref_steps)
|
|
torch.cuda.synchronize()
|
|
del sv, sg, sr, ss, sc, sa
|
|
with torch.cuda.graph(graph):
|
|
self.extension.state_step(v, g, refractory, staging, spike_u8, counts, gd, alpha, rest, reset, threshold, ref_steps)
|
|
sampler = p4b.GPUSampler(self.cfg["device"]["launcher_visible_gpu_uuid"], float(self.cfg["benchmark"]["gpu_sample_interval_seconds"])) if monitor else None
|
|
if sampler: sampler.start()
|
|
torch.cuda.synchronize(); torch.cuda.reset_peak_memory_stats(); start = time.perf_counter()
|
|
cpu_start = time.process_time()
|
|
for step in range(steps):
|
|
slot = step % qlen
|
|
if backend in ("eager", "hybrid-exact"):
|
|
g.mul_(gd).add_(queue[slot]); queue[slot].zero_()
|
|
refractory.clamp_min_(1).sub_(1)
|
|
available = refractory == 0
|
|
v[available] += alpha * (g[available] - (v[available] - rest))
|
|
v[~available] = reset
|
|
spikes = available & (v >= threshold)
|
|
accepted = forced[step].bool() & available[targets]
|
|
forced_sources = targets[accepted]
|
|
if forced_sources.numel(): spikes[forced_sources] = True
|
|
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
|
counts.add_(spikes.to(torch.int32))
|
|
if sources.numel():
|
|
v[sources] = reset; refractory[sources] = ref_steps
|
|
if backend == "hybrid-exact":
|
|
starts = self.indptr[sources]
|
|
degrees = self.indptr[sources + 1] - starts
|
|
cumulative = torch.cumsum(degrees, 0)
|
|
total = int(cumulative[-1].item()) if cumulative.numel() else 0
|
|
edges = torch.empty(total, dtype=torch.int64, device=self.device)
|
|
if total:
|
|
self.extension.fill_edges(self.indptr, sources, cumulative - degrees, edges)
|
|
else:
|
|
edges = expand_rows(self.indptr, sources)
|
|
if edges.numel():
|
|
queue[(step + delay_steps) % qlen].scatter_add_(0, self.indices[edges].long(), self.weights[edges])
|
|
elif backend.startswith("compile-"):
|
|
assert compiled is not None
|
|
arrival = queue[slot].clone(); queue[slot].zero_()
|
|
v, g, refractory, spikes, available = compiled(v, g, refractory, arrival)
|
|
accepted = forced[step].bool() & available[targets]
|
|
forced_sources = targets[accepted]
|
|
if forced_sources.numel(): spikes[forced_sources] = True
|
|
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
|
counts.add_(spikes.to(torch.int32))
|
|
if sources.numel():
|
|
v[sources] = reset; refractory[sources] = ref_steps
|
|
edges = expand_rows(self.indptr, sources)
|
|
if edges.numel(): queue[(step + delay_steps) % qlen].scatter_add_(0, self.indices[edges].long(), self.weights[edges])
|
|
else:
|
|
assert self.extension is not None
|
|
if backend == "cudagraph-custom-state":
|
|
assert staging is not None and graph is not None
|
|
staging.copy_(queue[slot]); queue[slot].zero_(); graph.replay()
|
|
else:
|
|
self.extension.state_step(v, g, refractory, queue[slot], spike_u8, counts, gd, alpha, rest, reset, threshold, ref_steps)
|
|
self.extension.force_step(v, refractory, spike_u8, counts, targets, forced[step], reset, ref_steps)
|
|
self.extension.fanout(spike_u8, self.indptr, self.indices, self.weights, queue[(step + delay_steps) % qlen])
|
|
sources = torch.nonzero(spike_u8, as_tuple=False).flatten() if capture_events else None
|
|
if capture_events:
|
|
if not (backend.startswith("custom") or backend == "cudagraph-custom-state"):
|
|
pass
|
|
assert sources is not None
|
|
src = sources.detach().cpu().numpy().astype(np.int32, copy=True)
|
|
event_nodes.append(src); event_steps.append(np.full(len(src), step, dtype=np.int32))
|
|
torch.cuda.synchronize(); wall = time.perf_counter() - start; cpu = time.process_time() - cpu_start
|
|
resources = sampler.stop() if sampler else {}
|
|
v_np = v.cpu().numpy().copy(); g_np = g.cpu().numpy().copy(); counts_np = counts.cpu().numpy().copy()
|
|
es = np.concatenate(event_steps) if event_steps else np.empty(0, dtype=np.int32)
|
|
en = np.concatenate(event_nodes) if event_nodes else np.empty(0, dtype=np.int32)
|
|
active = counts_np > 0
|
|
super_values = self.superclass
|
|
desc = np.isin(super_values, ["descending_neuron", "descending_neuron_tbc"])
|
|
motor = np.isin(super_values, ["vnc_motor", "cb_motor"])
|
|
bio_s = duration_ms / 1000.0
|
|
result = {
|
|
"backend": backend, "duration_ms": duration_ms, "steps": steps,
|
|
"wall_clock_seconds": wall, "steps_per_second": steps / wall,
|
|
"biological_realtime_speed": bio_s / wall, "realtime_factor_wall_over_biological": wall / bio_s,
|
|
"total_spikes": int(counts_np.sum(dtype=np.int64)), "active_neurons": int(active.sum()),
|
|
"mean_firing_rate_hz": float(counts_np.sum(dtype=np.int64) / (self.n * bio_s)),
|
|
"descending_active_neurons": int(np.count_nonzero(active & desc)),
|
|
"descending_spikes": int(counts_np[desc].sum(dtype=np.int64)),
|
|
"motor_active_neurons": int(np.count_nonzero(active & motor)),
|
|
"motor_spikes": int(counts_np[motor].sum(dtype=np.int64)),
|
|
"peak_vram_bytes": int(torch.cuda.max_memory_allocated()),
|
|
"cpu_process_utilization_percent_of_one_core": 100.0 * cpu / wall,
|
|
"resource_sampling": resources,
|
|
"event_count": int(len(en)),
|
|
"event_sha256": hashlib.sha256(es.tobytes() + en.tobytes()).hexdigest() if capture_events else None,
|
|
"spike_counts_sha256": hashlib.sha256(counts_np.tobytes()).hexdigest(),
|
|
"final_voltage_sha256": hashlib.sha256(v_np.tobytes()).hexdigest(),
|
|
"final_g_sha256": hashlib.sha256(g_np.tobytes()).hexdigest(),
|
|
}
|
|
return {"metrics": result, "event_steps": es, "event_nodes": en, "counts": counts_np, "v": v_np, "g": g_np}
|
|
|
|
|
|
def compare(reference: dict[str, Any], candidate: dict[str, Any], tolerance: float) -> dict[str, Any]:
|
|
events = np.array_equal(reference["event_steps"], candidate["event_steps"]) and np.array_equal(reference["event_nodes"], candidate["event_nodes"])
|
|
counts = np.array_equal(reference["counts"], candidate["counts"])
|
|
vdiff = np.abs(reference["v"] - candidate["v"])
|
|
gdiff = np.abs(reference["g"] - candidate["g"])
|
|
return {
|
|
"spike_events_exact": bool(events), "per_neuron_spike_counts_exact": bool(counts),
|
|
"final_voltage_max_abs_error": float(vdiff.max(initial=0)),
|
|
"final_voltage_mean_abs_error": float(vdiff.mean()),
|
|
"final_g_max_abs_error": float(gdiff.max(initial=0)), "final_g_mean_abs_error": float(gdiff.mean()),
|
|
"state_within_tolerance": bool(vdiff.max(initial=0) <= tolerance and gdiff.max(initial=0) <= tolerance),
|
|
"eligible_for_runtime_v1": bool(events and counts),
|
|
}
|
|
|
|
|
|
def save_reference_trial(exp: Path, spec: dict[str, Any], run: dict[str, Any]) -> None:
|
|
base = exp / "regression/reference" / spec["name"]
|
|
base.mkdir(parents=True, exist_ok=True)
|
|
pq.write_table(pa.table({
|
|
"step": pa.array(run["event_steps"], type=pa.int32()),
|
|
"timestamp_ms": pa.array(run["event_steps"].astype(np.float64) * 0.1, type=pa.float64()),
|
|
"neuron_idx": pa.array(run["event_nodes"], type=pa.int32()),
|
|
}), base / "spike-events.parquet", compression="zstd")
|
|
np.save(base / "per-neuron-spike-counts.int32.npy", run["counts"], allow_pickle=False)
|
|
np.save(base / "final-voltage.float32.npy", run["v"], allow_pickle=False)
|
|
np.save(base / "final-g.float32.npy", run["g"], allow_pickle=False)
|
|
write_json(base / "statistics.json", run["metrics"] | {"trial": spec})
|
|
|
|
|
|
def propagation_stats(run: dict[str, Any], targets: np.ndarray, distance: np.ndarray,
|
|
superclass: np.ndarray) -> dict[str, Any]:
|
|
active = run["counts"] > 0
|
|
nonnegative = distance[active & (distance >= 0)]
|
|
return {
|
|
"active_neurons": int(active.sum()),
|
|
"downstream_active_neurons": int(np.count_nonzero(active & ~np.isin(np.arange(len(active)), targets))),
|
|
"propagation_depth": int(nonnegative.max(initial=0)),
|
|
"descending_active_neurons": int(np.count_nonzero(active & np.isin(superclass, ["descending_neuron", "descending_neuron_tbc"]))),
|
|
"motor_active_neurons": int(np.count_nonzero(active & np.isin(superclass, ["vnc_motor", "cb_motor"]))),
|
|
}
|
|
|
|
|
|
def visual_dynamics(exp: Path, cfg: dict[str, Any], runtime: Runtime,
|
|
layers: dict[str, np.ndarray], indptr: np.ndarray, indices: np.ndarray) -> dict[str, Any]:
|
|
spec = cfg["visual_dynamics"]; duration = float(spec["duration_ms"]); rate = float(spec["rate_hz"]); seed = int(spec["seed"])
|
|
results: dict[str, Any] = {}
|
|
tests: list[tuple[str, np.ndarray, float, int, torch.Tensor | None, str]] = []
|
|
sensory = np.flatnonzero(layers["visual_sensory"]).astype(np.int64)
|
|
tests.append(("A-forced-histaminergic-sensory", sensory, rate, seed, None, "engineering forcing of sensory output spikes"))
|
|
steps = int(round(duration / float(cfg["model"]["dt_ms"])))
|
|
tonic = generate_forced(steps, len(sensory), float(spec["tonic_histamine_baseline_hz"]), float(cfg["model"]["dt_ms"]), seed + 1, runtime.device)
|
|
mod_start = int(round(float(spec["modulation_start_ms"]) / float(cfg["model"]["dt_ms"])))
|
|
mod = generate_forced(steps - mod_start, len(sensory), float(spec["modulation_rate_hz"]), float(cfg["model"]["dt_ms"]), seed + 2, runtime.device)
|
|
tonic[mod_start:] = mod
|
|
tests.append(("B-tonic-histamine-rate-modulation", sensory, 0.0, seed + 1, tonic, "exploratory tonic output plus rate modulation; not baseline"))
|
|
first = np.flatnonzero(layers["first_order"]).astype(np.int64)
|
|
second = np.flatnonzero(layers["second_order"]).astype(np.int64)
|
|
tests.append(("C-first-order-external-input", first, rate, seed + 3, None, "engineering input interface moved to real first-order targets"))
|
|
tests.append(("C-second-order-external-input", second, rate, seed + 4, None, "engineering input interface moved to real second-order targets"))
|
|
for j, name in enumerate(("visual_sensory", "first_order", "second_order", "higher_visual", "central_brain")):
|
|
targets = np.flatnonzero(layers[name]).astype(np.int64)
|
|
# Keep the audit bounded without inventing body IDs: deterministic full layer up to 25k, otherwise fixed index stride.
|
|
if len(targets) > 25000:
|
|
targets = targets[np.linspace(0, len(targets) - 1, 25000, dtype=np.int64)]
|
|
tests.append((f"D-layer-{name}", targets, rate, seed + 10 + j, None, f"engineering external input at hierarchy layer {name}"))
|
|
for name, targets, hz, trial_seed, override, interpretation in tests:
|
|
LOG.info("visual dynamics %s targets=%d", name, len(targets))
|
|
run = runtime.run("eager", targets, hz, trial_seed, duration, False, forced_override=override)
|
|
distance = p4b.directed_distances(indptr, indices, targets, runtime.n)
|
|
record = run["metrics"] | propagation_stats(run, targets, distance, runtime.superclass) | {
|
|
"test": name, "stimulated_population_size": int(len(targets)), "interpretation": interpretation,
|
|
"biological_interpretation": "histamine remains inhibitory; forced sensory spikes are an engineering interface, not phototransduction/receptor-potential biology",
|
|
"baseline_status": "exploratory only" if name.startswith("B-") else "semantic test; no automatic selection",
|
|
}
|
|
results[name] = record
|
|
write_json(exp / "visual-audit/dynamics" / f"{name}.json", record)
|
|
write_json(exp / "visual-audit/dynamics-summary.json", results)
|
|
return results
|
|
|
|
|
|
def compiler_report(exp: Path, runtime: Runtime) -> dict[str, Any]:
|
|
report: dict[str, Any] = {"torch_version": torch.__version__, "modes": {}}
|
|
n = runtime.n
|
|
m = runtime.cfg["model"]
|
|
for mode in ("compile-default", "compile-reduce-overhead"):
|
|
try:
|
|
compiled, fn = runtime.compiled_state(mode)
|
|
v = torch.full((n,), float(m["resting_potential_mv"]), device=runtime.device)
|
|
g = torch.zeros(n, device=runtime.device); r = torch.zeros(n, dtype=torch.int32, device=runtime.device); a = torch.zeros(n, device=runtime.device)
|
|
compiled(v, g, r, a); torch.cuda.synchronize()
|
|
report["modes"][mode] = {"status": "compiled", "fullgraph": True, "graph_breaks_in_fused_static_state": 0}
|
|
except Exception as exc:
|
|
report["modes"][mode] = {"status": "failed", "error": repr(exc)}
|
|
report["fullgraph_dynamic_csr"] = {
|
|
"status": "not eligible",
|
|
"graph_break_reasons": [
|
|
"torch.nonzero produces data-dependent dynamic shape",
|
|
"Python branching on sources.numel() and edge_ids.numel()",
|
|
"expand_rows requires cumulative[-1].item() host synchronization",
|
|
"dynamic torch.arange allocation and event-dependent CSR fan-out",
|
|
],
|
|
"action": "kept eager reference; custom CUDA prototype keeps spike detection/fan-out/accumulation on GPU",
|
|
}
|
|
write_json(exp / "compiler-reports/torch-compile.json", report)
|
|
return report
|
|
|
|
|
|
def build_extension(source_dir: Path, exp: Path) -> tuple[Any | None, dict[str, Any]]:
|
|
cpp, cu = source_dir / "phase4c_ext.cpp", source_dir / "phase4c_ext.cu"
|
|
started = time.perf_counter()
|
|
try:
|
|
ext = load_extension(
|
|
name="male_cns_lif_runtime_v1_ext", sources=[str(cpp), str(cu)],
|
|
extra_cflags=["-O3"], extra_cuda_cflags=["-O3"], verbose=True,
|
|
build_directory=str(exp / "compiler-reports/extension-build"),
|
|
)
|
|
result = {"status": "PASS", "build_seconds": time.perf_counter() - started, "sources": {str(cpp): sha256(cpp), str(cu): sha256(cu)}}
|
|
return ext, result
|
|
except Exception as exc:
|
|
result = {"status": "FAILED", "build_seconds": time.perf_counter() - started, "error": repr(exc), "sources": {str(cpp): sha256(cpp), str(cu): sha256(cu)}}
|
|
return None, result
|
|
|
|
|
|
def seal_outputs(exp: Path, cfg: dict[str, Any], paths: dict[str, Path], code_paths: list[Path], summary: dict[str, Any]) -> None:
|
|
controls = {exp / "manifests/manifest.json", exp / "manifests/checksums.sha256"}
|
|
outputs = sorted(x for x in exp.rglob("*") if x.is_file() and x not in controls)
|
|
manifest = {
|
|
"phase": "4C", "created_unix_time": time.time(), "status": summary["status"],
|
|
"software": {"python": platform.python_version(), "torch": torch.__version__, "numpy": np.__version__, "pyarrow": pa.__version__},
|
|
"code": [{"path": str(x), "bytes": x.stat().st_size, "sha256": sha256(x)} for x in code_paths],
|
|
"inputs": {name: {"path": str(path), "bytes": path.stat().st_size, "sha256": sha256(path)} for name, path in paths.items()},
|
|
"outputs": [{"path": str(x), "bytes": x.stat().st_size, "sha256": sha256(x)} for x in outputs],
|
|
"prohibitions_respected": cfg["prohibitions"],
|
|
}
|
|
write_json(exp / "manifests/manifest.json", manifest)
|
|
targets = outputs + [exp / "manifests/manifest.json"]
|
|
(exp / "manifests/checksums.sha256").write_text("".join(f"{sha256(x)} {x.relative_to(exp)}\n" for x in targets), encoding="utf-8")
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(); ap.add_argument("--config", required=True, type=Path); args = ap.parse_args()
|
|
cfg = yaml.safe_load(args.config.read_text(encoding="utf-8"))
|
|
exp = Path(cfg["paths"]["experiment_root"])
|
|
for sub in ("configs", "visual-audit", "benchmarks", "compiler-reports", "profiler", "regression", "manifests", "audit"):
|
|
(exp / sub).mkdir(parents=True, exist_ok=True)
|
|
p4b.setup_logging(exp / "audit/phase4c.log")
|
|
shutil.copy2(args.config, exp / "configs/phase4c.yaml")
|
|
paths = exact_paths(cfg); hashes_before = {k: sha256(v) for k, v in paths.items()}
|
|
indptr, indices, raw = p4b.validate_graph(cfg, paths)
|
|
table = full_metadata(paths["metadata"]); pops = populations(table)
|
|
expected_pop = {"visual": 6091, "auditory": 114}
|
|
for name, count in expected_pop.items():
|
|
if int(pops[name].sum()) != count: raise RuntimeError(f"{name} count mismatch")
|
|
frozen = freeze_reference(cfg, args.config, paths)
|
|
visual_summary, layers = visual_audit(exp, table, indptr, indices, raw)
|
|
if not torch.cuda.is_available() or torch.cuda.device_count() != 1: raise RuntimeError("exactly one visible CUDA GPU required")
|
|
device = torch.device("cuda:0"); props = torch.cuda.get_device_properties(device)
|
|
if cfg["device"]["required_name_substring"] not in props.name: raise RuntimeError(f"wrong GPU: {props.name}")
|
|
source_dir = Path(__file__).resolve().parent
|
|
extension, extension_report = build_extension(source_dir, exp)
|
|
write_json(exp / "compiler-reports/custom-extension.json", extension_report)
|
|
runtime = Runtime(cfg, table, indptr, indices, raw, device, extension)
|
|
compiler = compiler_report(exp, runtime)
|
|
|
|
reference_runs: dict[str, dict[str, Any]] = {}
|
|
regression: dict[str, Any] = {}
|
|
tol = float(cfg["regression"]["float_absolute_tolerance"])
|
|
for spec in cfg["reference_trials"]:
|
|
targets = np.flatnonzero(pops[spec["population"]]).astype(np.int64)
|
|
LOG.info("reference trial %s", spec["name"])
|
|
ref = runtime.run("eager", targets, float(spec["rate_hz"]), int(spec["seed"]), float(spec["duration_ms"]), True)
|
|
dist = p4b.directed_distances(indptr, indices, targets, runtime.n)
|
|
ref["metrics"].update(propagation_stats(ref, targets, dist, runtime.superclass))
|
|
save_reference_trial(exp, spec, ref); reference_runs[spec["name"]] = ref
|
|
candidates: dict[str, Any] = {}
|
|
for backend in (["hybrid-exact", "custom-cuda"] if extension is not None else []):
|
|
cand = runtime.run(backend, targets, float(spec["rate_hz"]), int(spec["seed"]), float(spec["duration_ms"]), True)
|
|
comp = compare(ref, cand, tol)
|
|
comp.update({
|
|
"propagation_statistics_exact": propagation_stats(ref, targets, dist, runtime.superclass) == propagation_stats(cand, targets, dist, runtime.superclass),
|
|
"reference_event_sha256": ref["metrics"]["event_sha256"], "candidate_event_sha256": cand["metrics"]["event_sha256"],
|
|
})
|
|
candidates[backend] = comp
|
|
regression[spec["name"]] = candidates
|
|
write_json(exp / "regression" / f"{spec['name']}.json", candidates)
|
|
visual_results = visual_dynamics(exp, cfg, runtime, layers, indptr, indices)
|
|
|
|
bench: dict[str, Any] = {}
|
|
auditory = np.flatnonzero(pops["auditory"]).astype(np.int64)
|
|
baseline_by_duration: dict[float, dict[str, Any]] = {}
|
|
for duration in cfg["benchmark"]["durations_ms"]:
|
|
duration = float(duration); bench[str(duration)] = {}
|
|
for backend in cfg["benchmark"]["backends"]:
|
|
if (backend.startswith("custom") or backend in ("cudagraph-custom-state", "hybrid-exact")) and extension is None:
|
|
bench[str(duration)][backend] = {"status": "unavailable", "reason": extension_report.get("error")}; continue
|
|
LOG.info("benchmark backend=%s duration_ms=%s", backend, duration)
|
|
try:
|
|
run = runtime.run(backend, auditory, float(cfg["benchmark"]["rate_hz"]), int(cfg["benchmark"]["seed"]), duration, False, monitor=True)
|
|
record = run["metrics"] | {"status": "PASS"}
|
|
if backend == "eager": baseline_by_duration[duration] = run
|
|
else:
|
|
ref = baseline_by_duration.get(duration)
|
|
if ref is not None:
|
|
state_cmp = compare({**ref, "event_steps": np.empty(0,np.int32), "event_nodes": np.empty(0,np.int32)}, {**run, "event_steps": np.empty(0,np.int32), "event_nodes": np.empty(0,np.int32)}, tol)
|
|
# Performance runs deliberately disable event capture. Do not
|
|
# mislabel two empty arrays as an event-sequence regression.
|
|
state_cmp["spike_events_exact"] = None
|
|
state_cmp["event_sequence_checked"] = False
|
|
record["benchmark_state_regression"] = state_cmp
|
|
bench[str(duration)][backend] = record
|
|
except Exception as exc:
|
|
bench[str(duration)][backend] = {"status": "FAILED", "error": repr(exc)}
|
|
write_json(exp / "benchmarks/runtime-matrix.json", bench)
|
|
|
|
eligible = ["eager"]
|
|
if extension is not None and all(regression[x].get("hybrid-exact", {}).get("eligible_for_runtime_v1") for x in regression):
|
|
eligible.append("hybrid-exact")
|
|
if extension is not None and all(regression[x].get("custom-cuda", {}).get("eligible_for_runtime_v1") for x in regression):
|
|
eligible.append("custom-cuda")
|
|
# Compiled/CUDA-graph paths need an exact 1 s event regression before eligibility.
|
|
audit_spec = cfg["reference_trials"][0]; audit_ref = reference_runs[audit_spec["name"]]
|
|
targets = np.flatnonzero(pops[audit_spec["population"]]).astype(np.int64)
|
|
backend_regression: dict[str, Any] = {}
|
|
for backend in ("compile-default", "compile-reduce-overhead", "cudagraph-custom-state"):
|
|
if backend == "cudagraph-custom-state" and extension is None: continue
|
|
try:
|
|
candidate = runtime.run(backend, targets, float(audit_spec["rate_hz"]), int(audit_spec["seed"]), 1000.0, True)
|
|
backend_regression[backend] = compare(audit_ref, candidate, tol)
|
|
if backend_regression[backend]["eligible_for_runtime_v1"]: eligible.append(backend)
|
|
except Exception as exc:
|
|
backend_regression[backend] = {"eligible_for_runtime_v1": False, "error": repr(exc)}
|
|
write_json(exp / "regression/backend-eligibility.json", backend_regression)
|
|
|
|
def speed(backend: str) -> float:
|
|
x = bench.get("10000.0", {}).get(backend, {})
|
|
return float(x.get("biological_realtime_speed", 0.0)) if x.get("status") == "PASS" else 0.0
|
|
fastest = max(eligible, key=speed)
|
|
runtime_root = Path(cfg["paths"]["model_root"]) / "runtime-v1"
|
|
runtime_root.mkdir(parents=True, exist_ok=True)
|
|
runtime_sources = (Path(__file__).resolve(), source_dir / "phase4c_ext.cpp", source_dir / "phase4c_ext.cu", source_dir / "requirements.txt", args.config.resolve())
|
|
for source in runtime_sources:
|
|
copy_immutable(source, runtime_root / source.name)
|
|
runtime_version = {
|
|
"name": cfg["names"]["runtime"], "backend": fastest,
|
|
"reference": cfg["names"]["reference"], "eligibility": backend_regression | regression,
|
|
"ten_second_biological_realtime_speed": speed(fastest),
|
|
"spike_behavior_requirement": "exact on all five 1 s frozen reference trials for custom runtime; exact auditory trial for compiler variants",
|
|
"algorithm": "exact sparse event-driven CSR fan-out; no dense full-edge scan; exact 18-step GPU-resident ring buffer",
|
|
"fallback_backend": "eager",
|
|
}
|
|
write_json(runtime_root / "VERSION.json", runtime_version); (runtime_root / "VERSION.json").chmod(0o444)
|
|
hashes_after = {k: sha256(v) for k, v in paths.items()}
|
|
if hashes_before != hashes_after: raise RuntimeError("Phase 3B inputs changed")
|
|
summary = {
|
|
"status": "PASS" if fastest else "FAILED", "reference_frozen": True,
|
|
"reference": frozen, "visual_audit": visual_summary, "visual_dynamics": visual_results,
|
|
"compiler_report": compiler, "custom_extension": extension_report,
|
|
"regression": regression, "backend_regression": backend_regression,
|
|
"runtime_v1_backend": fastest, "benchmarks": bench,
|
|
"device": {"name": props.name, "total_memory_bytes": int(props.total_memory)},
|
|
"input_hashes_unchanged": True, "entered_phase4d": False, "game_rl_web_3d_training": False,
|
|
}
|
|
write_json(exp / "benchmarks/summary.json", summary)
|
|
seal_outputs(exp, cfg, paths, list(runtime_sources), summary)
|
|
LOG.info("PHASE4C_COMPLETE runtime=%s", fastest)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|