203 lines
12 KiB
Python
203 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only Phase 4O-A runtime determinism audit; never writes frozen roots."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import platform
|
|
import random
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pyarrow.parquet as pq
|
|
import torch
|
|
import yaml
|
|
|
|
ROOT = Path("/data/digital-drosophila")
|
|
EXP = ROOT / "experiments/phase4o-runtime-credit"
|
|
MODEL = ROOT / "models/male-cns-lif/runtime-credit-v1"
|
|
FROZEN = ROOT / "experiments/phase4c-runtime/regression/reference"
|
|
GPU_UUID = "GPU-79f3a85d-adce-7833-7003-6488140ca596"
|
|
|
|
|
|
def save(path: Path, value: object) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True, default=str) + "\n")
|
|
|
|
|
|
def sha(path: Path) -> str:
|
|
h = hashlib.sha256()
|
|
with path.open("rb") as f:
|
|
for block in iter(lambda: f.read(8 << 20), b""):
|
|
h.update(block)
|
|
return h.hexdigest()
|
|
|
|
|
|
def digest(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def cmd(args: list[str]) -> dict:
|
|
exe = shutil.which(args[0])
|
|
if exe is None:
|
|
return {"available": False}
|
|
p = subprocess.run(args, text=True, capture_output=True)
|
|
return {"available": True, "exit_code": p.returncode, "stdout": p.stdout.strip()[:8000], "stderr": p.stderr.strip()[:2000]}
|
|
|
|
|
|
def env_audit() -> None:
|
|
if os.environ.get("CUDA_VISIBLE_DEVICES") != GPU_UUID:
|
|
raise RuntimeError("CUDA_VISIBLE_DEVICES must select frozen RTX 4090 UUID")
|
|
names = ["CUDA_VISIBLE_DEVICES", "CUBLAS_WORKSPACE_CONFIG", "PYTHONHASHSEED", "OMP_NUM_THREADS", "MKL_NUM_THREADS"]
|
|
names += sorted(x for x in os.environ if x.startswith("TORCH_"))
|
|
gpu = torch.cuda.get_device_properties(0)
|
|
e = {"OS_release": platform.platform(), "kernel": platform.release(), "uname": platform.uname()._asdict(),
|
|
"python": sys.version, "torch": torch.__version__, "cuda_runtime": torch.version.cuda,
|
|
"cudnn": torch.backends.cudnn.version(), "gpu_name": gpu.name, "gpu_uuid": GPU_UUID,
|
|
"gpu_total_memory_bytes": gpu.total_memory, "env_allowlist": {x: os.environ.get(x) for x in names},
|
|
"nvcc": cmd(["nvcc", "--version"]), "nvidia_smi": cmd(["nvidia-smi", "--query-gpu=name,uuid,driver_version,memory.total", "--format=csv,noheader"]),
|
|
"gcc": cmd(["gcc", "--version"]), "gxx": cmd(["g++", "--version"]), "ninja": cmd(["ninja", "--version"]),
|
|
"git_model": cmd(["git", "-C", str(ROOT / "models/male-cns-lif"), "status", "--short", "--branch"]),
|
|
"git_workspace": cmd(["git", "-C", str(ROOT), "rev-parse", "HEAD"]),
|
|
"torch_flags": {"deterministic_algorithms": torch.are_deterministic_algorithms_enabled(),
|
|
"cudnn_deterministic": torch.backends.cudnn.deterministic,
|
|
"cudnn_benchmark": torch.backends.cudnn.benchmark,
|
|
"cuda_matmul_allow_tf32": torch.backends.cuda.matmul.allow_tf32,
|
|
"cudnn_allow_tf32": torch.backends.cudnn.allow_tf32,
|
|
"matmul_precision": torch.get_float32_matmul_precision()}}
|
|
save(EXP / "environment/runtime-environment.json", e)
|
|
roots = [ROOT / "models/male-cns-lif/reference-v1", ROOT / "models/male-cns-lif/runtime-v1",
|
|
FROZEN, ROOT / "models/male-cns-lif/kcmbon-efficacy-v1",
|
|
ROOT / "experiments/phase4n-credit-assignment/compiler-reports/extension-build",
|
|
ROOT / "experiments/phase4c-runtime/compiler-reports/extension-build"]
|
|
files = []
|
|
for root in roots:
|
|
if root.exists():
|
|
for p in sorted(root.rglob("*")):
|
|
if p.is_file() and p.suffix not in (".pyc", ".o"):
|
|
files.append({"path": str(p), "bytes": p.stat().st_size, "mtime_ns": p.stat().st_mtime_ns, "sha256": sha(p)})
|
|
for p in [ROOT / "models/male-cns-lif/brain_runtime.py", ROOT / "models/male-cns-lif/phase4i.py",
|
|
ROOT / "models/male-cns-lif/phase4m.py", ROOT / "models/male-cns-lif/phase4l.py",
|
|
ROOT / "models/male-cns-lif/phase4n_events.py"]:
|
|
if p.is_file():
|
|
files.append({"path": str(p), "bytes": p.stat().st_size, "mtime_ns": p.stat().st_mtime_ns, "sha256": sha(p)})
|
|
save(EXP / "binary-audit/execution-file-sha256.json", files)
|
|
binary = [x for x in files if x["path"].endswith(".so")]
|
|
save(EXP / "binary-audit/runtime-binary-diff-report.json", {
|
|
"binary_files": binary, "bytewise_same": len({x["sha256"] for x in binary}) == 1 if len(binary) > 1 else None,
|
|
"warning": "A binary hash difference is not itself proof of changed semantics; build paths and toolchain can alter ELF bytes.",
|
|
"historical_phase4c_manifest": str(ROOT / "experiments/phase4c-runtime/manifests/manifest.json")})
|
|
|
|
|
|
def rng_hashes(rt=None) -> dict:
|
|
py = digest(repr(random.getstate()).encode())
|
|
npy = digest(repr(np.random.get_state()).encode())
|
|
out = {"python_random": py, "numpy_global": npy, "torch_cpu": digest(torch.get_rng_state().numpy().tobytes()),
|
|
"torch_cuda": digest(torch.cuda.get_rng_state(0).cpu().numpy().tobytes())}
|
|
if rt is not None and hasattr(rt, "generator"):
|
|
out["input_generator"] = digest(rt.generator.get_state().cpu().numpy().tobytes())
|
|
return out
|
|
|
|
|
|
def first_event_diff(a_step, a_node, b_step, b_node):
|
|
n = min(len(a_step), len(b_step))
|
|
mismatched = np.flatnonzero((a_step[:n] != b_step[:n]) | (a_node[:n] != b_node[:n]))
|
|
j = int(mismatched[0]) if len(mismatched) else (n if len(a_step) != len(b_step) else None)
|
|
if j is None:
|
|
return None
|
|
ref = None if j >= len(a_step) else {"step": int(a_step[j]), "neuron_idx": int(a_node[j])}
|
|
got = None if j >= len(b_step) else {"step": int(b_step[j]), "neuron_idx": int(b_node[j])}
|
|
step = min(x["step"] for x in (ref, got) if x is not None)
|
|
return {"event_position": j, "first_difference_step_upper_bound": step, "reference_event": ref, "observed_event": got,
|
|
"note": "Event-list first difference is not necessarily first state divergence; state-level instrumentation is required."}
|
|
|
|
|
|
def repeat_audit(repeats: int, trial_filter: str | None = None, tag: str = "baseline") -> None:
|
|
from brain_runtime import BrainRuntime
|
|
from phase4i import poisson_batch
|
|
cfgpath = ROOT / "models/male-cns-lif/mb-mechanistic-v1/phase4i.yaml"
|
|
rt = BrainRuntime(cfgpath, EXP / "compiler-reports/extension-build")
|
|
backend_file = Path(rt.extension.__file__)
|
|
identity = {"module": rt.extension.__name__, "binary_path": str(backend_file), "binary_sha256": sha(backend_file),
|
|
"runtime_v1_python_sha256": sha(ROOT / "models/male-cns-lif/runtime-v1/phase4c.py"),
|
|
"source": "BrainRuntime._hybrid_edges plus GPU scatter_add_", "config": str(cfgpath), "config_sha256": sha(cfgpath)}
|
|
save(EXP / "binary-audit/loaded-backend.json", identity)
|
|
specs = [(s["name"], s["population"], float(s["rate_hz"]), int(s["seed"])) for s in yaml.safe_load((ROOT / "models/male-cns-lif/runtime-v1/phase4c.yaml").read_text())["reference_trials"]]
|
|
if trial_filter:
|
|
specs = [x for x in specs if x[0] == trial_filter]
|
|
if not specs: raise ValueError(f"unknown frozen trial {trial_filter}")
|
|
rows = []
|
|
output_dir = EXP / "phase4c-replay" / tag
|
|
for name, pop, rate, seed in specs:
|
|
refroot = FROZEN / name
|
|
table = pq.read_table(refroot / "spike-events.parquet")
|
|
rstep = table["step"].to_numpy().astype(np.int32); rnode = table["neuron_idx"].to_numpy().astype(np.int32)
|
|
rcount = np.load(refroot / "per-neuron-spike-counts.int32.npy")
|
|
rv = np.load(refroot / "final-voltage.float32.npy"); rg = np.load(refroot / "final-g.float32.npy")
|
|
targets = np.flatnonzero(rt.population_masks[pop]).astype(np.int64)
|
|
for repeat in range(repeats):
|
|
before = rng_hashes(rt)
|
|
rt.reset(seed)
|
|
after_reset = rng_hashes(rt)
|
|
before_first = rng_hashes(rt)
|
|
t0 = time.perf_counter()
|
|
error = None
|
|
try:
|
|
rt.step(poisson_batch(targets, rate, 1000.0, name), 1000.0)
|
|
events = rt.read_spikes()
|
|
step = events["step"].astype(np.int32); node = events["neuron_idx"].astype(np.int32)
|
|
counts = rt.spike_counts.cpu().numpy().astype(np.int32)
|
|
voltage = rt.v.cpu().numpy(); g = rt.g.cpu().numpy()
|
|
match_events = bool(np.array_equal(step, rstep) and np.array_equal(node, rnode))
|
|
match_counts = bool(np.array_equal(counts, rcount))
|
|
row = {"trial": name, "repeat": repeat, "target_count": int(len(targets)),
|
|
"spike_events_exact": match_events, "per_neuron_counts_exact": match_counts,
|
|
"final_voltage_max_abs_error": float(np.max(np.abs(voltage-rv))),
|
|
"final_g_max_abs_error": float(np.max(np.abs(g-rg))),
|
|
"event_count": len(node), "event_sha256": digest(step.tobytes()+node.tobytes()),
|
|
"counts_sha256": digest(counts.tobytes()), "first_event_difference": first_event_diff(rstep,rnode,step,node),
|
|
"wall_seconds": time.perf_counter()-t0,
|
|
"rng_before_reset": before, "rng_after_reset": after_reset,
|
|
"rng_before_first_timestep": before_first, "rng_after_trial": rng_hashes(rt)}
|
|
if not match_events:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
pq.write_table(__import__('pyarrow').table({'step':step,'neuron_idx':node}),
|
|
output_dir / f"divergent-{name}-repeat-{repeat}-events.parquet",compression='zstd')
|
|
np.savez_compressed(output_dir / f"divergent-{name}-repeat-{repeat}-state.npz",
|
|
final_voltage=voltage, final_g=g, counts=counts)
|
|
except Exception as exc:
|
|
error = repr(exc)
|
|
row = {"trial": name, "repeat": repeat, "error": error, "wall_seconds": time.perf_counter()-t0,
|
|
"rng_before_reset": before, "rng_after_reset": after_reset}
|
|
rows.append(row)
|
|
save(output_dir / "reproducibility-matrix.json", rows)
|
|
print(json.dumps({"trial": name, "repeat": repeat, "event_exact": row.get("spike_events_exact"),
|
|
"count_exact": row.get("per_neuron_counts_exact"), "first_diff": row.get("first_event_difference"), "error": error}), flush=True)
|
|
save(output_dir / "summary.json", {"repeats_per_trial": repeats, "all_spike_events_exact": all(r.get("spike_events_exact",False) for r in rows),
|
|
"all_counts_exact": all(r.get("per_neuron_counts_exact",False) for r in rows),
|
|
"trial_summary": {name: {"event_exact": sum(r.get("spike_events_exact",False) for r in rows if r["trial"]==name),
|
|
"count_exact": sum(r.get("per_neuron_counts_exact",False) for r in rows if r["trial"]==name)} for name,_,_,_ in specs},
|
|
"backend": identity})
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("task", choices=["environment", "repeats"])
|
|
ap.add_argument("--repeats", type=int, default=10)
|
|
ap.add_argument("--trial")
|
|
ap.add_argument("--tag", default="baseline")
|
|
args = ap.parse_args()
|
|
EXP.mkdir(parents=True, exist_ok=True); MODEL.mkdir(parents=True, exist_ok=True)
|
|
if args.task == "environment": env_audit()
|
|
else: repeat_audit(args.repeats, args.trial, args.tag)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|