Files
digital-drosophila/models/male-cns-lif/author-fullgraph-lab-v1/gpu_full.py
T

370 lines
23 KiB
Python

"""Float64 full-graph numerical audit using sealed Phase 4AE CUDA operators.
This synchronous diagnostic compares every neuron at every tick with the CPU
memmaps. It does not introduce a new integration rule, APL, training or tuning.
"""
from functools import lru_cache
import csv
import hashlib
import json
import math
from pathlib import Path
import sys
import time
import traceback
import types
import numpy as np
import torch
from context import ROOT
from oracle_full import queue_summary
NEURONS = 166700
EDGES = 25582938
COEFFICIENT_NAMES = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
REQUEST_FIELDS = ("tick", "neuron_idx", "attempted", "accepted", "rejected")
AE_MODEL = ROOT / "models/male-cns-lif/author-runtime-lab-v1"
ALLOWED_CASES = {(100, ()), (500, (1381,)), (500, (1197, 1381, 1511, 82312, 133666))}
def require(condition, message):
if not condition:
raise ValueError(message)
def write_json(path, value):
with Path(path).open("x", encoding="utf-8") as output:
json.dump(value, output, indent=2, allow_nan=False)
output.write("\n")
def digest(path):
value = hashlib.sha256()
with Path(path).open("rb") as source:
for block in iter(lambda: source.read(8 << 20), b""):
value.update(block)
return value.hexdigest()
def array_digest(array, dtype):
return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest()
def bitwise_comparison(actual, expected, floating=False):
require(actual.shape == expected.shape and actual.dtype == expected.dtype, "Comparison shape/dtype mismatch")
aa = actual.view(np.uint64) if floating else actual
bb = expected.view(np.uint64) if floating else expected
mismatch = aa != bb
count = int(np.count_nonzero(mismatch))
first = int(np.flatnonzero(mismatch)[0]) if count else None
result = {"bitwise_exact": count == 0, "mismatching_neurons": count,
"first_neuron_idx": first}
if floating:
finite = bool(np.isfinite(actual).all() and np.isfinite(expected).all())
result.update(finite=finite, max_abs_volt=float(np.max(np.abs(actual - expected))) if finite else None)
if first is not None:
result.update(actual_bits_hex=f"{int(aa[first]):016x}", expected_bits_hex=f"{int(bb[first]):016x}")
return result
@lru_cache(maxsize=1)
def sealed_backend():
"""Load reviewed source without writing bytecode into the sealed model."""
source = AE_MODEL / "gpu_backend.py"
module = types.ModuleType("phase4af_sealed_4ae_gpu_backend")
module.__file__ = str(source)
exec(compile(source.read_bytes(), str(source), "exec"), module.__dict__)
return module
@lru_cache(maxsize=2)
def load_graph(graph_directory):
graph_dir = Path(graph_directory)
require(graph_dir.is_absolute(), "graph_dir must be absolute")
dtypes = {"nodes": np.int32, "edge_id": np.int64, "src": np.int32, "dst": np.int32,
"signed_raw": np.int64, "ptr": np.int64}
graph = {}
for name, dtype in dtypes.items():
value = np.load(graph_dir / (name + ".npy"), mmap_mode="r", allow_pickle=False)
require(value.dtype == dtype and value.ndim == 1, "Invalid graph array: " + name)
graph[name] = value
require(graph["nodes"].shape == (NEURONS,) and
np.array_equal(graph["nodes"], np.arange(NEURONS, dtype=np.int32)), "Graph neuron identity changed")
require(graph["ptr"].shape == (NEURONS + 1,) and graph["ptr"][0] == 0 and
graph["ptr"][-1] == EDGES and bool(np.all(np.diff(graph["ptr"]) >= 0)), "Invalid canonical CSR")
for name in ("edge_id", "src", "dst", "signed_raw"):
require(graph[name].shape == (EDGES,), "Graph edge count differs: " + name)
require(np.array_equal(graph["src"], np.repeat(graph["nodes"], np.diff(graph["ptr"]))),
"Canonical source rows disagree with CSR")
for start in range(0, EDGES, 1 << 20):
stop = min(start + (1 << 20), EDGES)
require(np.array_equal(graph["edge_id"][start:stop], np.arange(start, stop, dtype=np.int64)),
"Graph edge IDs are not canonical CSR row positions")
require(bool(np.all((graph["dst"][start:stop] >= 0) & (graph["dst"][start:stop] < NEURONS))),
"Target index outside complete graph")
return graph
def load_inputs(case_dir, oracle_dir):
with (case_dir / "spec.json").open(encoding="utf-8") as source:
spec = json.load(source)
for name in ("name", "steps", "dt_ms", "duration_ms", "seed_nodes", "model", "graph_dir"):
require(name in spec, "Missing case field: " + name)
steps = spec["steps"]
seeds = spec["seed_nodes"]
require(type(steps) is int and isinstance(seeds, list) and
all(type(node) is int for node in seeds) and len(seeds) == len(set(seeds)), "Invalid case steps/seeds")
seeds = tuple(sorted(seeds))
require((steps, seeds) in ALLOWED_CASES, "Case not one of the three fixed full-graph trials")
with (oracle_dir / "summary.json").open(encoding="utf-8") as source:
oracle_summary = json.load(source)
require(oracle_summary.get("complete") is True and oracle_summary.get("steps") == steps and
oracle_summary.get("completed_ticks") == steps and oracle_summary.get("precision") == "float64" and
oracle_summary.get("nodes") == NEURONS and oracle_summary.get("edges") == EDGES,
"CPU oracle did not seal a complete trial with the declared steps/nodes/edges")
require(float(spec["dt_ms"]) == .1 and float(spec["duration_ms"]) == steps * .1,
"Unexpected duration or timestep")
model = spec["model"]
for name in ("resting_potential_mv", "threshold_mv", "reset_potential_mv", "synaptic_delay_ms",
"refractory_ms", "w_syn_mv_per_anatomical_synapse"):
require(name in model and math.isfinite(float(model[name])), "Missing/nonfinite model field: " + name)
require(float(model["synaptic_delay_ms"]) == 1.8 and float(model["refractory_ms"]) == 2.2 and
float(model["w_syn_mv_per_anatomical_synapse"]) == .275, "Declared delay/refractory/W_syn changed")
require(Path(spec["graph_dir"]).is_absolute(), "Case graph_dir must be absolute")
graph = load_graph(str(Path(spec["graph_dir"]).resolve()))
with (oracle_dir / "coefficients.json").open(encoding="utf-8") as source:
coefficients = json.load(source)
for name in COEFFICIENT_NAMES:
require(name in coefficients and math.isfinite(float(coefficients[name])), "Missing/nonfinite oracle coefficient: " + name)
require(coefficients["threshold"] == float(model["threshold_mv"]) * .001 and
coefficients["reset"] == float(model["reset_potential_mv"]) * .001,
"Oracle threshold/reset disagree with model declaration")
histories = {}
for name, dtype in (("v", np.float64), ("g", np.float64), ("ref", np.int32)):
filename = name + "-history." + np.dtype(dtype).name + ".npy"
value = np.load(oracle_dir / filename, mmap_mode="r", allow_pickle=False)
require(value.shape == (steps, NEURONS) and value.dtype == dtype, "Invalid CPU state history: " + name)
histories[name] = value
weights = np.load(oracle_dir / "weights.npy", mmap_mode="r", allow_pickle=False)
require(weights.dtype == np.float64 and weights.shape == (EDGES,) and bool(np.isfinite(weights).all()),
"Invalid actual Brian2 SI weights")
# A sign or row mismatch must not be hidden by copying the oracle weight file.
for start in range(0, EDGES, 1 << 20):
stop = min(start + (1 << 20), EDGES)
require(np.array_equal(np.sign(weights[start:stop]), np.sign(graph["signed_raw"][start:stop])),
"Oracle weight sign/zero pattern differs from graph")
with np.load(oracle_dir / "state.npz", allow_pickle=False) as source:
final = {name: source[name].copy() for name in source.files}
for name, dtype in (("v", np.float64), ("g", np.float64), ("ref", np.int32), ("counts", np.int32)):
require(final[name].dtype == dtype and final[name].shape == (NEURONS,), "Invalid CPU final state: " + name)
require(final["step"].dtype == np.int32 and final["neuron_idx"].dtype == np.int32 and
final["step"].ndim == 1 and final["step"].shape == final["neuron_idx"].shape, "Invalid CPU sparse events")
require(bool(np.all((final["step"] >= 0) & (final["step"] < steps))) and
bool(np.all(np.diff(final["step"]) >= 0)) and
bool(np.all((final["neuron_idx"] >= 0) & (final["neuron_idx"] < NEURONS))), "CPU event index out of range")
with (oracle_dir / "queue.jsonl").open(encoding="utf-8") as source:
queues = [json.loads(line) for line in source if line.strip()]
require(len(queues) == steps and [row["tick"] for row in queues] == list(range(steps)),
"Missing, duplicate or reordered CPU queue records")
with (oracle_dir / "requests.csv").open(encoding="utf-8", newline="") as source:
reader = csv.DictReader(source)
require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, "CPU request CSV schema mismatch")
request_rows = [{name: int(row[name]) for name in REQUEST_FIELDS} for row in reader]
require(len(request_rows) == len(seeds) and
[row["neuron_idx"] for row in request_rows] == list(seeds) and
all(row["tick"] == 0 and row["attempted"] == 1 and row["accepted"] in (0, 1) and
row["rejected"] == 1 - row["accepted"] for row in request_rows), "CPU request coverage mismatch")
return spec, graph, histories, weights, final, queues, coefficients, seeds, request_rows
def run_gpu(case_dir, out_dir, oracle_dir, build_root):
"""Execute one fixed full graph and compare every state bit, event and queue.
Full CPU histories remain read-only. The first failing tick saves complete
CPU/GPU V/g/ref arrays; every tick saves exact comparison fields and hashes.
"""
case_dir, out_dir, oracle_dir = Path(case_dir), Path(out_dir), Path(oracle_dir)
require(not out_dir.exists(), "Refusing existing output directory: " + str(out_dir))
spec, graph, histories, weights, oracle_final, oracle_queues, coeff, seeds, oracle_requests = load_inputs(case_dir, oracle_dir)
require(torch.cuda.is_available(), "CUDA unavailable")
require(torch.cuda.device_count() == 1 and "4090" in torch.cuda.get_device_name(),
"Only the declared single visible RTX4090 is permitted")
out_dir.mkdir(parents=True, exist_ok=False)
start = time.perf_counter()
tick = -1
rows = []
first_failure = None
try:
backend = sealed_backend()
ext, build_provenance = backend.build_extension(build_root)
build_end = time.perf_counter()
device = torch.device("cuda", torch.cuda.current_device())
torch.cuda.reset_peak_memory_stats(device)
def upload(value, dtype):
# Writable copies prevent any accidental write to read-only source maps.
return torch.from_numpy(np.array(value, copy=True, order="C")).to(device=device, dtype=dtype).contiguous()
indptr = upload(graph["ptr"], torch.int64)
dst = upload(graph["dst"], torch.int32)
gpu_weights = upload(weights, torch.float64)
rest = float(spec["model"]["resting_potential_mv"]) * .001
v = torch.full((NEURONS,), rest, device=device, dtype=torch.float64)
g = torch.zeros(NEURONS, device=device, dtype=torch.float64)
ref = torch.zeros(NEURONS, device=device, dtype=torch.int32)
counts = torch.zeros(NEURONS, device=device, dtype=torch.int32)
requests = torch.zeros(NEURONS, device=device, dtype=torch.uint8)
if seeds:
requests[torch.tensor(seeds, device=device, dtype=torch.int64)] = 1
ring = torch.zeros((19, NEURONS), device=device, dtype=torch.uint8)
events_step, events_node = [], []
requested_total = accepted_request_total = 0
total_due = total_accepted = 0
torch.cuda.synchronize(device)
simulation_start = time.perf_counter()
with (out_dir / "step-comparisons.jsonl").open("x", encoding="utf-8", buffering=1) as comparisons, \
(out_dir / "queue.jsonl").open("x", encoding="utf-8", buffering=1) as queue_output, \
(out_dir / "tick-state-hashes.jsonl").open("x", encoding="utf-8", buffering=1) as hash_output, \
(out_dir / "requests.csv").open("x", encoding="utf-8", newline="", buffering=1) as request_output:
request_writer = csv.DictWriter(request_output, fieldnames=REQUEST_FIELDS)
request_writer.writeheader()
for tick in range(spec["steps"]):
if tick == 1:
requests.zero_()
requested = int(torch.count_nonzero(requests).item())
accepted_requests = int(torch.count_nonzero((requests != 0) & (ref <= 1)).item())
requested_total += requested
accepted_request_total += accepted_requests
request_rows = []
if requested:
requested_nodes = torch.nonzero(requests, as_tuple=False).flatten()
accepted_mask = (ref[requested_nodes] <= 1).to(torch.uint8).cpu().numpy()
for node, accepted in zip(requested_nodes.cpu().numpy(), accepted_mask):
request_rows.append({"tick": tick, "neuron_idx": int(node), "attempted": 1,
"accepted": int(accepted), "rejected": 1 - int(accepted)})
request_writer.writerows(request_rows)
requests_exact = request_rows == [row for row in oracle_requests if row["tick"] == tick]
spikes = ring[tick % 19]
ext.state_step(v, g, ref, requests, spikes, counts,
*[float(coeff[name]) for name in COEFFICIENT_NAMES], 22)
expanded_cpu = np.empty(0, dtype=np.int64)
accepted_cpu = np.empty(0, dtype=np.uint8)
if tick >= 18:
due = ring[(tick - 18) % 19]
sources = torch.nonzero(due, as_tuple=False).flatten().contiguous()
if sources.numel():
degrees = indptr[sources + 1] - indptr[sources]
offsets = (torch.cumsum(degrees, dim=0) - degrees).contiguous()
edge_count = int(degrees.sum().item())
expanded = torch.empty(edge_count, device=device, dtype=torch.int64)
ext.fill_edges(indptr, sources, offsets, expanded)
if edge_count:
destination = dst[expanded]
# Hash the actual GPU-expanded queue BEFORE target grouping.
expanded_cpu = expanded.cpu().numpy().copy()
accepted_cpu = (ref[destination.long()] == 0).to(torch.uint8).cpu().numpy().copy()
order = torch.argsort(destination, stable=True)
ordered_edges = expanded[order].contiguous()
ordered_targets = destination[order]
boundary = torch.ones(edge_count, device=device, dtype=torch.bool)
boundary[1:] = ordered_targets[1:] != ordered_targets[:-1]
starts = torch.nonzero(boundary, as_tuple=False).flatten().contiguous()
targets = ordered_targets[starts].contiguous()
ext.apply_arrivals(ordered_edges, starts, targets, gpu_weights, g, ref)
queue = queue_summary(tick, expanded_cpu, accepted_cpu)
queue_output.write(json.dumps(queue, allow_nan=False) + "\n")
total_due += queue["count"]
total_accepted += queue["accepted_count"]
queue_exact = queue == oracle_queues[tick]
# No external input exists in these fixed cases after the tick-zero requests.
actual = {"v": v.cpu().numpy().copy(), "g": g.cpu().numpy().copy(),
"ref": ref.cpu().numpy().copy()}
state_checks = {name: bitwise_comparison(actual[name], histories[name][tick], name != "ref")
for name in ("v", "g", "ref")}
fired = torch.nonzero(spikes, as_tuple=False).flatten().cpu().numpy().astype(np.int32)
if len(fired):
events_step.append(np.full(len(fired), tick, dtype=np.int32))
events_node.append(fired.copy())
oracle_nodes = oracle_final["neuron_idx"][oracle_final["step"] == tick]
events_exact = bool(np.array_equal(fired, oracle_nodes))
finite = state_checks["v"]["finite"] and state_checks["g"]["finite"]
passed = bool(finite and queue_exact and events_exact and requests_exact and
all(value["bitwise_exact"] for value in state_checks.values()))
row = {"tick": tick, "pass_all": passed, "states": state_checks,
"events_exact": events_exact, "gpu_event_count": len(fired), "cpu_event_count": len(oracle_nodes),
"queue_exact": queue_exact, "gpu_queue": queue, "cpu_queue": oracle_queues[tick],
"requests": requested, "accepted_requests": accepted_requests,
"rejected_requests": requested - accepted_requests,
"request_rows_exact": requests_exact,
"state_sha256": {name: array_digest(actual[name], "<i4" if name == "ref" else "<f8")
for name in ("v", "g", "ref")}}
comparisons.write(json.dumps(row, allow_nan=False) + "\n")
hash_output.write(json.dumps({"tick": tick,
**{name + "_sha256": value for name, value in row["state_sha256"].items()}},
allow_nan=False) + "\n")
rows.append(row)
if not passed and first_failure is None:
first_failure = row
with (out_dir / "first-failure-state.npz").open("xb") as target:
np.savez_compressed(target, tick=np.array(tick, dtype=np.int32),
**{"gpu_" + name: value for name, value in actual.items()},
**{"cpu_" + name: np.asarray(histories[name][tick]).copy() for name in actual},
gpu_event_nodes=fired, cpu_event_nodes=oracle_nodes,
arrival_edge_id=expanded_cpu, arrival_accepted=accepted_cpu)
write_json(out_dir / "first-failure.json", row)
torch.cuda.synchronize(device)
simulation_end = time.perf_counter()
def join(values):
return np.concatenate(values).astype(np.int32, copy=False) if values else np.empty(0, dtype=np.int32)
state = {"step": join(events_step), "neuron_idx": join(events_node), "counts": counts.cpu().numpy(),
"v": v.cpu().numpy(), "g": g.cpu().numpy(), "ref": ref.cpu().numpy()}
with (out_dir / "state.npz").open("xb") as target:
np.savez_compressed(target, **state)
final_checks = {name: bool(np.array_equal(value.view(np.uint64), oracle_final[name].view(np.uint64)))
if name in ("v", "g") else bool(np.array_equal(value, oracle_final[name]))
for name, value in state.items()}
history_final_checks = {name: bool(np.array_equal(oracle_final[name].view(np.uint64), histories[name][-1].view(np.uint64)))
if name in ("v", "g") else bool(np.array_equal(oracle_final[name], histories[name][-1]))
for name in ("v", "g", "ref")}
pass_all = bool(len(rows) == spec["steps"] and all(row["pass_all"] for row in rows) and
all(final_checks.values()) and all(history_final_checks.values()) and
requested_total == len(seeds) and accepted_request_total == len(seeds))
summary = {"case": spec["name"], "pass_all": pass_all, "precision": "float64", "state_units": "SI volts",
"neurons": NEURONS, "edges": EDGES, "steps_expected": spec["steps"], "steps_compared": len(rows),
"all_neurons_compared_every_tick": True, "bitwise_includes_signed_zero": True,
"first_failed_tick": None if first_failure is None else first_failure["tick"],
"failed_tick_count": sum(not row["pass_all"] for row in rows),
"final_state_exact": final_checks, "oracle_final_matches_history": history_final_checks,
"total_spikes": len(state["step"]), "arriving_edges": total_due, "accepted_arriving_edges": total_accepted,
"requested": requested_total, "accepted_requests": accepted_request_total,
"rejected_requests": requested_total - accepted_request_total,
"whole_graph_numerical_comparison": True, "olfactory_representation_validated": False,
"apl_overlay": False, "learning": False, "candidate_generated": False,
"performance_runtime_claim": False, "cpu_sync_each_tick": True,
"first_failure_full_state_saved": first_failure is not None,
"full_gpu_history_saved": False, "sealed_4AE_model": str(AE_MODEL),
"build": build_provenance, "build_or_cache_seconds": build_end - start,
"diagnostic_simulation_seconds": simulation_end - simulation_start,
"wall_seconds": time.perf_counter() - start, "gpu": torch.cuda.get_device_name(device),
"peak_allocated_vram_bytes": int(torch.cuda.max_memory_allocated(device)),
"peak_reserved_vram_bytes": int(torch.cuda.max_memory_reserved(device)),
"input_sha256": {"spec.json": digest(case_dir / "spec.json"),
"oracle/weights.npy": digest(oracle_dir / "weights.npy"),
"oracle/coefficients.json": digest(oracle_dir / "coefficients.json")}}
write_json(out_dir / "summary.json", summary)
write_json(out_dir / "metrics.json", {name: summary[name] for name in
("wall_seconds", "build_or_cache_seconds", "diagnostic_simulation_seconds", "gpu",
"peak_allocated_vram_bytes", "peak_reserved_vram_bytes", "performance_runtime_claim", "cpu_sync_each_tick")})
print(json.dumps({"case": spec["name"], "pass_all": pass_all, "steps": len(rows),
"first_failed_tick": summary["first_failed_tick"], "total_spikes": len(state["step"])}), flush=True)
return summary
except Exception as error:
write_json(out_dir / "error.json", {"type": type(error).__name__, "message": str(error),
"traceback": traceback.format_exc(), "tick_in_progress": tick,
"completed_tick_comparisons": len(rows), "pass_all": False})
raise