Add isolated exact eligibility GPU replay prototype and fixed benchmark protocol

This commit is contained in:
Digital Drosophila Archive
2026-10-01 14:30:01 +08:00
parent 09187e4d4d
commit 8abfa3c66c
10 changed files with 1884 additions and 0 deletions
@@ -0,0 +1,233 @@
"""Host-only tests; CUDA equality requires separate real-device evidence."""
from __future__ import annotations
from dataclasses import replace
import importlib.util
import math
import os
from pathlib import Path
import sys
import unittest
import numpy as np
from gpu_backend import (CUDA_OPTIONS, CUDA_SOURCE, COMPARTMENTS, CudaEligibility, cpu_reference,
prepare_events)
ROOT = Path(__file__).resolve().parents[1]
PHASE4J = Path(os.environ.get("PHASE4J_SOURCE", str(ROOT / "models" / "male-cns-lif" / "phase4j.py")))
BRIDGE_PATH = Path(os.environ.get("ELIGIBILITY_BRIDGE_SOURCE", str(ROOT / "wholebrain-plastic-interface-v1" / "eligibility_bridge.py")))
spec = importlib.util.spec_from_file_location("_gpu_math_frozen_bridge", BRIDGE_PATH)
bridge = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = bridge
spec.loader.exec_module(bridge)
def assert_bits(test, left, right):
test.assertEqual(left.dtype, right.dtype)
test.assertEqual(left.shape, right.shape)
test.assertEqual(left.tobytes(), right.tobytes())
class HostMathTests(unittest.TestCase):
def setUp(self):
self.kc = np.array([2, 5, 9], np.int32)
self.step = np.array([0, 0, 1, 2, 2, 5, 7], np.int32)
self.neurons = np.array([2, 4, 2, 5, 9, 9, 2], np.int32)
self.sources = np.array([2, 2, 5, 9], np.int32)
self.compartments = np.array([COMPARTMENTS[0], COMPARTMENTS[1],
COMPARTMENTS[0], COMPARTMENTS[0]])
self.base = np.array([0.001, 0.01, 0.004, 0.08], np.float32)
self.delta = np.zeros(4, np.float32)
self.kw = dict(d=1.0, frozen=False, eta_mv=bridge.ETA_MV,
floor_fraction=bridge.FLOOR_FRACTION,
decay=math.exp(-bridge.DT_MS / bridge.TAU_MS))
def prepared(self, commands=(0, 2, 6), steps=8):
return prepare_events(self.step, self.neurons, self.kc, steps, commands, n_neurons=12)
def run_host(self, prepared=None, **overrides):
args = self.kw | overrides
return cpu_reference(self.prepared() if prepared is None else prepared,
self.sources, self.compartments, COMPARTMENTS[0],
self.delta, self.base, **args)
def test_matches_frozen_trace_and_update_and_no_input_mutation(self):
arrays = (self.step, self.neurons, self.kc, self.sources,
self.compartments, self.base, self.delta)
before = [(a.tobytes(), a.flags.writeable) for a in arrays]
prepared = self.prepared()
result = self.run_host(prepared)
trace = bridge.trace_from_events(self.step, self.neurons, self.kc, 8, [0, 2, 6],
phase4j_source=PHASE4J, n_neurons=12)
expected, _ = bridge.apply_eligibility(trace.last_command_trace, self.kc,
self.sources, self.compartments, COMPARTMENTS[0], self.delta, self.base,
d=1.0, frozen=False, phase4j_source=PHASE4J, n_neurons=12)
for left, right in ((result.snapshots, trace.snapshots),
(result.final_trace, trace.final),
(result.sampled_trace, trace.last_command_trace),
(result.delta_after, expected)):
assert_bits(self, left, right)
self.assertFalse(left.flags.writeable)
self.assertEqual(before, [(a.tobytes(), a.flags.writeable) for a in arrays])
self.assertEqual(prepared.kc_event_count, 6)
self.assertEqual(prepared.indptr.tolist(), [0, 3, 4, 6])
self.assertEqual(prepared.ticks.tolist(), [0, 1, 7, 2, 2, 5])
def test_no_command_returns_positive_zero_but_nonzero_final(self):
result = self.run_host(self.prepared(()), d=0)
self.assertEqual(result.snapshots.shape, (0, 3))
assert_bits(self, result.sampled_trace, np.zeros(3, np.float32))
self.assertTrue(np.any(result.final_trace > 0))
assert_bits(self, result.delta_after, self.delta)
def test_sampling_after_spike_and_last_command_only(self):
result = self.run_host(self.prepared((0, 2, 7)))
self.assertEqual(result.snapshots[0, 0], np.float32(1))
self.assertEqual(result.snapshots[1, 1], np.float32(1))
assert_bits(self, result.sampled_trace, result.snapshots[-1])
assert_bits(self, result.final_trace, result.snapshots[-1])
def test_frozen_d_zero_off_compartment_and_floor(self):
self.delta = np.array([-0.0001, -0.0005, -0.001, 0], np.float32)
for kwargs in (dict(d=0), dict(frozen=True)):
assert_bits(self, self.run_host(**kwargs).delta_after, self.delta)
result = self.run_host()
assert_bits(self, result.delta_after[1:2], self.delta[1:2])
self.assertEqual(result.delta_after[0], np.float32(-0.25) * self.base[0])
self.assertEqual(result.delta_after[2], self.delta[2])
def test_empty_events_and_one_tick(self):
prepared = prepare_events(np.empty(0, np.int32), np.empty(0, np.int32),
self.kc, 1, [0], n_neurons=12)
result = self.run_host(prepared)
assert_bits(self, result.final_trace, np.zeros(3, np.float32))
assert_bits(self, result.delta_after, self.delta)
def test_long_empty_gap_does_not_use_power(self):
self.step = np.array([0], np.int32)
self.neurons = np.array([2], np.int32)
prepared = self.prepared((9999,), steps=10000)
result = self.run_host(prepared)
expected = np.float32(1)
for _ in range(9999):
expected = np.float32(expected * np.float32(self.kw["decay"]))
assert_bits(self, result.sampled_trace[:1], np.array([expected], np.float32))
jumped = np.float32(float(np.float32(self.kw["decay"])) ** 9999)
self.assertNotEqual(expected.view(np.uint32), jumped.view(np.uint32))
def test_random_sparse_case_matches_frozen(self):
rng = np.random.default_rng(717)
self.kc = np.arange(0, 80, 2, dtype=np.int32)
encoded = np.unique(rng.integers(0, 80 * 257, size=1400))
self.step = (encoded // 80).astype(np.int32)
self.neurons = (encoded % 80).astype(np.int32)
self.sources = rng.choice(self.kc, 159).astype(np.int32)
self.compartments = rng.choice(np.asarray(COMPARTMENTS), 159)
self.base = rng.uniform(0.0001, 2, 159).astype(np.float32)
self.delta = -(rng.uniform(0, 0.24, 159).astype(np.float32) * self.base)
prepared = prepare_events(self.step, self.neurons, self.kc, 257, [0, 1, 20, 99, 256], n_neurons=80)
result = self.run_host(prepared)
trace = bridge.trace_from_events(self.step, self.neurons, self.kc, 257,
[0, 1, 20, 99, 256], phase4j_source=PHASE4J, n_neurons=80)
expected, _ = bridge.apply_eligibility(trace.last_command_trace, self.kc,
self.sources, self.compartments, COMPARTMENTS[0], self.delta, self.base,
d=1, phase4j_source=PHASE4J, n_neurons=80)
assert_bits(self, result.snapshots, trace.snapshots)
assert_bits(self, result.delta_after, expected)
def test_signed_zero_source_floor_tie_is_preserved(self):
# The bridge's identity guard rejects this pathological subnormal base.
# Compare the original apply_v2 directly: np.maximum(+0,-0) gives -0.
self.base[:] = np.nextafter(np.float32(0), np.float32(1))
result = self.run_host(d=0)
original, _, _ = bridge._load_original(PHASE4J)
from types import SimpleNamespace
validator = SimpleNamespace(n=12, kc=self.kc, base_candidate=self.base,
cand={"source_neuron_idx": bridge._Column(self.sources),
"target_compartment_verbatim": bridge._Column(self.compartments)})
expected, _ = original(validator, result.sampled_trace, COMPARTMENTS[0], 0,
self.delta, {"eligibility_v2": {"eta_effective_mv": bridge.ETA_MV,
"max_fractional_depression": bridge.FLOOR_FRACTION}})
assert_bits(self, result.delta_after, expected)
self.assertTrue(np.all(np.signbit(result.delta_after)))
assert_bits(self, self.run_host(frozen=True).delta_after, self.delta)
def test_input_validation(self):
bad_events = [
(self.step.astype(np.int64), self.neurons, self.kc, 8, [0]),
(self.step, self.neurons.astype(np.float32), self.kc, 8, [0]),
(self.step, self.neurons, self.kc[::-1], 8, [0]),
(self.step, self.neurons, self.kc, 8, [0, 0]),
(self.step, self.neurons, self.kc, 8, [0.0]),
(self.step, self.neurons, self.kc, 8, [8]),
(self.step, self.neurons, self.kc, True, [0]),
(self.step, self.neurons, self.kc, 0, []),
(np.array([0, 0], np.int32), np.array([2, 2], np.int32), self.kc, 8, []),
]
for args in bad_events:
with self.subTest(args=args), self.assertRaises(ValueError):
prepare_events(*args, n_neurons=12)
for kwargs in (dict(d=True), dict(d=0.5), dict(d=float("nan")),
dict(frozen=1), dict(eta_mv=float("inf")),
dict(floor_fraction=0), dict(decay=float("nan"))):
with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
self.run_host(**kwargs)
self.delta[0] = -0.0
with self.assertRaises(ValueError):
self.run_host()
self.delta[0] = 0
self.base[0] = np.nan
with self.assertRaises(ValueError):
self.run_host()
def test_forged_prepared_rejected(self):
prepared = self.prepared()
bad = replace(prepared, ticks=np.array([0, 0, 7, 2, 2, 5], np.int32))
with self.assertRaises(ValueError):
self.run_host(bad)
def test_cuda_entry_rejects_bad_inputs_before_accessing_device(self):
# An uninitialized shell has no cp/device fields. Rejection therefore
# proves the CUDA entry's validation happens before any CUDA operation.
backend = object.__new__(CudaEligibility)
prepared = self.prepared()
base_args = dict(prepared=prepared, candidate_sources=self.sources,
candidate_compartments=self.compartments, compartment=COMPARTMENTS[0],
delta_before=self.delta, base_candidate=self.base, **self.kw)
bad = [dict(candidate_sources=self.sources.astype(np.int64)),
dict(candidate_compartments=self.compartments.astype("S")),
dict(delta_before=self.delta.astype(np.float64)),
dict(base_candidate=np.array([np.inf, 1, 1, 1], np.float32)),
dict(delta_before=np.array([np.nan, 0, 0, 0], np.float32)),
dict(candidate_sources=np.array([1, 2, 5, 9], np.int32)),
dict(candidate_sources=np.array([2, 2, 5, 11], np.int32)),
dict(delta_before=np.array([-1, 0, 0, 0], np.float32)),
dict(compartment="unknown"), dict(kernel_warmup=-1),
dict(kernel_repeats=0), dict(kernel_repeats=True)]
for overrides in bad:
with self.subTest(overrides=overrides), self.assertRaises(ValueError):
backend.run(**(base_args | overrides))
def test_no_dense_tick_raster_and_input_storage_owned(self):
prepared = self.prepared()
for arr in (prepared.kc, prepared.indptr, prepared.ticks, prepared.command_steps):
self.assertEqual(arr.ndim, 1)
self.assertFalse(arr.flags.writeable)
self.kc[:] = 0
self.step[:] = 0
self.assertEqual(prepared.kc.tolist(), [2, 5, 9])
self.assertEqual(prepared.ticks.tolist(), [0, 1, 7, 2, 2, 5])
def test_kernel_explicit_rounding_controls(self):
self.assertIn("--fmad=false", CUDA_OPTIONS)
self.assertIn("--ftz=false", CUDA_OPTIONS)
self.assertIn("__fmul_rn", CUDA_SOURCE)
self.assertIn("__fadd_rn", CUDA_SOURCE)
self.assertNotIn("pow(", CUDA_SOURCE)
self.assertNotIn("__expf", CUDA_SOURCE)
if __name__ == "__main__":
unittest.main()