"""Fail-closed byte-exact audit of the shared-APL closed-loop fixtures. Comparison is independent of either backend's stored PASS. No tolerance is used, timing fields are never compared, and reports contain no wall timestamp. """ from __future__ import annotations import csv import hashlib import json import math from pathlib import Path import re import struct import numpy as np N_GLOBAL = 166700 STATE_KEYS = ('step', 'neuron_idx', 'counts', 'v', 'g', 'ref') REQUEST_FIELDS = ('tick', 'neuron_idx', 'attempted', 'accepted', 'rejected') HASH_FIELDS = {'tick', 'v_sha256', 'g_sha256', 'ref_sha256'} QUEUE_FIELDS = {'tick', 'count', 'accepted_count', 'ordered_edge_sha256', 'accepted_mask_sha256', 'joint_sha256'} BUNDLE_FILES = ('input_matrix.npy', 'output_matrix.npy', 'kc_globals.npy', 'apl_globals.npy') COEFFICIENTS = ('gd', 'c0', 'cg', 'cv', 'threshold', 'reset', 'seed_volt') def require(condition, message): if not condition: raise ValueError(message) def file_hash(path): h = hashlib.sha256() with Path(path).open('rb') as source: for block in iter(lambda: source.read(8 << 20), b''): h.update(block) return h.hexdigest() def raw_hash(array, dtype=None): return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest() def save(path, report): if path is None: return path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with path.open('x', encoding='utf-8') as target: json.dump(report, target, indent=2, sort_keys=True, allow_nan=False) target.write('\n') def _read_json(path, used): used.add(Path(path)) return json.loads(Path(path).read_text(encoding='utf-8')) def _read_jsonl(path, used): used.add(Path(path)) with Path(path).open(encoding='utf-8') as source: return [json.loads(line) for line in source if line.strip()] def _read_array(path, used): used.add(Path(path)) return np.load(path, mmap_mode='r', allow_pickle=False) def _check(checks, name, condition): checks[name] = bool(condition) def _bit_mask(a, b): if a.dtype.kind == 'f': unsigned = np.dtype('uint'+str(a.dtype.itemsize*8)) return np.ascontiguousarray(a).view(unsigned) != np.ascontiguousarray(b).view(unsigned) return a != b def _compare_array(name, a, b, checks, differences, dtype=None, shape=None): a, b = np.asarray(a), np.asarray(b) schema = a.shape == b.shape and a.dtype == b.dtype and \ (dtype is None or a.dtype == np.dtype(dtype)) and (shape is None or a.shape == shape) _check(checks, name+'.schema', schema) if not schema: differences[name] = {'left_shape': list(a.shape), 'right_shape': list(b.shape), 'left_dtype': str(a.dtype), 'right_dtype': str(b.dtype), 'expected_shape': None if shape is None else list(shape), 'expected_dtype': None if dtype is None else str(np.dtype(dtype))} _check(checks, name+'.bitwise_exact', False) return False finite = bool(np.isfinite(a).all() and np.isfinite(b).all()) _check(checks, name+'.finite', finite) mask = _bit_mask(a, b) exact = not bool(np.any(mask)) _check(checks, name+'.bitwise_exact', exact) if not exact or not finite: flat = np.flatnonzero(mask.ravel()) detail = {'mismatching_elements': int(len(flat)), 'first_positions': [list(map(int, np.unravel_index(int(index), a.shape))) for index in flat[:5]], 'left_nonfinite': int(np.count_nonzero(~np.isfinite(a))), 'right_nonfinite': int(np.count_nonzero(~np.isfinite(b)))} if finite and a.size: error = np.abs(a.astype(np.float64)-b.astype(np.float64)) detail.update(max_abs_error=float(error.max()), mean_abs_error=float(error.mean())) if a.dtype.kind == 'f' and len(flat): unsigned = np.dtype('uint'+str(a.dtype.itemsize*8)) av = np.ascontiguousarray(a).view(unsigned).ravel() bv = np.ascontiguousarray(b).view(unsigned).ravel() detail['first_bit_patterns'] = [{'left': hex(int(av[index])), 'right': hex(int(bv[index]))} for index in flat[:5]] differences[name] = detail return exact and finite def _sha(value): return isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value) is not None def _read_requests(path, steps, used): used.add(Path(path)) with Path(path).open(encoding='utf-8', newline='') as source: reader = csv.DictReader(source) require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, 'Request CSV schema mismatch: '+str(path)) rows = [{key: int(row[key]) for key in REQUEST_FIELDS} for row in reader] previous = (-1, -1) for row in rows: key = row['tick'], row['neuron_idx'] require(previous < key and 0 <= key[0] < steps and 0 <= key[1] < N_GLOBAL, 'Request rows must be sorted, unique, and in range: '+str(path)) require(row['attempted'] == 1 and row['accepted'] in (0, 1) and row['rejected'] == 1-row['accepted'], 'Request accounting is incomplete: '+str(path)) previous = key return rows def _load_run(directory, role, n, edges, steps, record_history, name, used, checks): directory = Path(directory) summary = _read_json(directory/'summary.json', used) _check(checks, role+'.summary_complete', summary.get('complete') is True and summary.get('pass_all') is True and summary.get('steps') == steps and summary.get('completed_ticks') == steps and summary.get('nodes') == n and summary.get('edges') == edges and summary.get('precision') == 'float64') is_cpu = role == 'cpu' _check(checks, role+'.case_name', summary.get('name' if is_cpu else 'case') == name) _check(checks, role+'.history_mode', summary.get('history_recorded' if is_cpu else 'record_history') is record_history) _check(checks, role+'.apl_precision', summary.get('APL_precision' if is_cpu else 'apl_precision') == 'float32') _check(checks, role+'.no_error_marker', not (directory/'error.json').exists()) used.add(directory/'state.npz') with np.load(directory/'state.npz', allow_pickle=False) as source: require(set(source.files) == set(STATE_KEYS), 'State NPZ keys changed: '+str(directory)) state = {key: source[key].copy() for key in STATE_KEYS} for key, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32'), ('counts', 'int32')): require(state[key].shape == (n,) and state[key].dtype == np.dtype(dtype), 'Invalid final '+key+': '+str(directory)) require(state['step'].dtype == state['neuron_idx'].dtype == np.int32 and state['step'].ndim == 1 and state['step'].shape == state['neuron_idx'].shape, 'Invalid sparse event schema: '+str(directory)) require(bool(np.all((state['step'] >= 0) & (state['step'] < steps))) and bool(np.all((state['neuron_idx'] >= 0) & (state['neuron_idx'] < N_GLOBAL))), 'Sparse event range error') event_keys = state['step'].astype(np.int64)*N_GLOBAL+state['neuron_idx'] _check(checks, role+'.events_sorted_unique', len(event_keys) < 2 or np.all(np.diff(event_keys) > 0)) _check(checks, role+'.count_nonnegative_total', np.all(state['counts'] >= 0) and int(state['counts'].sum()) == len(event_keys)) _check(checks, role+'.ref_range', np.all((state['ref'] >= 0) & (state['ref'] <= 22))) _check(checks, role+'.reported_events', summary.get('spikes' if is_cpu else 'total_spikes') == len(event_keys)) hashes = _read_jsonl(directory/'tick-state-hashes.jsonl', used) require(len(hashes) == steps and [row.get('tick') for row in hashes] == list(range(steps)), 'Incomplete tick hash coverage') require(all(set(row) == HASH_FIELDS and all(_sha(row[key]) for key in HASH_FIELDS-{'tick'}) for row in hashes), 'Invalid tick state hash schema') queues = _read_jsonl(directory/'queue.jsonl', used) require(len(queues) == steps and [row.get('tick') for row in queues] == list(range(steps)), 'Incomplete queue coverage') require(all(set(row) == QUEUE_FIELDS and type(row['count']) is int and type(row['accepted_count']) is int and 0 <= row['accepted_count'] <= row['count'] <= edges and all(_sha(row[key]) for key in QUEUE_FIELDS-{'tick', 'count', 'accepted_count'}) for row in queues), 'Invalid queue row schema/count/hash') for key in ('v', 'g', 'ref'): _check(checks, role+'.last_hash_'+key, hashes[-1][key+'_sha256'] == raw_hash(state[key], '= 0)) histories, pre = {}, {} for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')): files = (directory/(field+'-history.'+dtype+'.npy'), directory/('preAPL-'+field+'.'+dtype+'.npy')) if record_history: histories[field], pre[field] = (_read_array(path, used) for path in files) require(all(array.dtype == np.dtype(dtype) and array.shape == (steps, n) for array in (histories[field], pre[field])), 'Small pre/post history schema mismatch') _check(checks, role+'.history_hashes_'+field, all(row[field+'_sha256'] == raw_hash(histories[field][tick]) for tick, row in enumerate(hashes))) _check(checks, role+'.history_final_'+field, not np.any(_bit_mask(histories[field][-1], state[field]))) else: _check(checks, role+'.no_dense_'+field, not any(path.exists() for path in files)) if is_cpu: component = _read_json(directory/'apl-worker-handshake.json', used)['handshake'] else: component = _read_json(directory/'apl-component.json', used) _check(checks, role+'.component_summary_consistent', summary.get('apl_component') == component) return dict(directory=directory, summary=summary, state=state, hashes=hashes, queues=queues, requests=requests, apl=apl, histories=histories, pre=pre, component=component) def _pair_outputs(left, right, checks, differences, n, steps, record_history): for field in STATE_KEYS: shape = None if field in ('step', 'neuron_idx') else (n,) dtype = 'float64' if field in ('v', 'g') else 'int32' _compare_array('state.'+field, left['state'][field], right['state'][field], checks, differences, dtype, shape) for field, width in (('drive', 2), ('state', 2), ('inhibition', 4064)): _compare_array('apl.'+field, left['apl'][field], right['apl'][field], checks, differences, 'float32', (steps, width)) for field in ('hashes', 'queues', 'requests', 'component'): same = left[field] == right[field] _check(checks, field+'.parsed_exact', same) if not same: if isinstance(left[field], list): first = next((i for i, (a, b) in enumerate(zip(left[field], right[field])) if a != b), None) differences[field] = {'left_count': len(left[field]), 'right_count': len(right[field]), 'first_differing_row': first, 'left_row': None if first is None else left[field][first], 'right_row': None if first is None else right[field][first]} else: differences[field] = {'left': left[field], 'right': right[field]} if record_history: for stage in ('histories', 'pre'): for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')): _compare_array(stage+'.'+field, left[stage][field], right[stage][field], checks, differences, dtype, (steps, n)) def _queue_digest(tick, ids, accepted): ids = np.ascontiguousarray(ids, dtype='= 22 for index, ok in zip(range(lo, hi), accepted): row = run['requests'][index] if row['accepted'] != int(ok) or row['rejected'] != int(not ok): request_ok = False first.setdefault('request_ref_mismatch', {'tick': tick, 'neuron_idx': row['neuron_idx']}) if not np.isin(targets[accepted], fired).all() or np.isin(targets[~accepted], fired).any(): delivered_ok = False first.setdefault('request_delivery_mismatch', tick) last[fired] = tick ref = np.maximum(22-(tick-last), 0).astype(np.int32) if raw_hash(ref, '= 18 else np.empty(0, np.int64) chunks = [np.arange(ptr[source], ptr[source+1], dtype=np.int64) for source in due] indices = np.concatenate(chunks) if chunks else np.empty(0, np.int64) expected_queue = _queue_digest(tick, graph['edge_id'][indices], (ref[graph['dst'][indices]] == 0).astype(np.uint8)) if expected_queue != run['queues'][tick]: queue_ok = False first.setdefault('queue_reconstruction_mismatch', tick) _check(checks, role+'.request_refractory_gate', request_ok) _check(checks, role+'.accepted_spikes_rejected_absent', delivered_ok) _check(checks, role+'.all_ref_hashes_from_actual_events', ref_ok) if run['pre']: _check(checks, role+'.small_queue_independent_reconstruction', queue_ok) _check(checks, role+'.spike_g_reset_before_apl', reset_ok) if first: differences[role+'.independent_accounting'] = first def _check_apl_application(run, role, nodes, kc, checks, differences): if not run['pre']: return common, local_kc, bundle_kc = np.intersect1d(nodes, kc, assume_unique=True, return_indices=True) _compare_array(role+'.apl_keeps_voltage', run['pre']['v'], run['histories']['v'], checks, differences, 'float64') _compare_array(role+'.apl_keeps_ref', run['pre']['ref'], run['histories']['ref'], checks, differences, 'int32') expected = np.array(run['pre']['g'], copy=True) delta = run['apl']['inhibition'][:, bundle_kc].astype(np.float64)*np.float64(.001) expected[:, local_kc] = expected[:, local_kc]-delta _compare_array(role+'.apl_unconditional_g_write', expected, run['histories']['g'], checks, differences, 'float64') def _finish(report, checks, differences, used, out_json): report['checks'] = {name: bool(value) for name, value in checks.items()} report['differences'] = differences report['pass_all'] = bool(checks) and all(checks.values()) and 'error' not in report report['failed_checks'] = sorted(name for name, value in checks.items() if not value) report['evidence_sha256'] = {str(path): file_hash(path) for path in sorted(used) if path.is_file()} save(out_json, report) return report def compare(case_dir, gpu_dir, cpu_dir, out_json=None): """Strict independent closed-loop comparison; out_json=None is read-only.""" case_dir, gpu_dir, cpu_dir = Path(case_dir), Path(gpu_dir), Path(cpu_dir) if out_json is not None: require(not Path(out_json).exists(), 'Refusing existing comparison report') checks, differences, used = {}, {}, set() report = {'kind': 'shared-APL-component closed-loop byte-exact comparison', 'pass_all': False, 'case_dir': str(case_dir), 'gpu_dir': str(gpu_dir), 'cpu_dir': str(cpu_dir), 'tolerance': None, 'signed_zero_included': True, 'timing_fields_compared': False, 'independent_CPU_all_components_claim': False} try: spec = _read_json(case_dir/'spec.json', used) steps, history = spec['steps'], spec['record_history'] require(type(steps) is int and steps > 0 and type(history) is bool and spec['dt_ms'] == .1, 'Invalid fixed case clock/history schema') graph_dir, bundle_dir = Path(spec['graph_dir']), Path(spec['apl_bundle_dir']) graph = {field: _read_array(graph_dir/(field+'.npy'), used) for field in ('nodes', 'edge_id', 'src', 'dst', 'ptr', 'signed_raw', 'weights')} nodes = graph['nodes'] n, edges = len(nodes), len(graph['edge_id']) require(n > 0 and edges > 0 and nodes.dtype == np.int32 and nodes.ndim == 1 and np.all((nodes >= 0) & (nodes < N_GLOBAL)) and np.all(np.diff(nodes) > 0), 'Invalid canonical node identities') require(graph['ptr'].dtype == np.int64 and graph['ptr'].shape == (n+1,) and graph['ptr'][0] == 0 and graph['ptr'][-1] == edges and np.all(np.diff(graph['ptr']) >= 0), 'Invalid CSR') for key, dtype in (('edge_id', 'int64'), ('src', 'int32'), ('dst', 'int32'), ('signed_raw', 'int64'), ('weights', 'float64')): require(graph[key].dtype == np.dtype(dtype) and graph[key].shape == (edges,), 'Invalid graph '+key) require(np.array_equal(graph['src'], np.repeat(np.arange(n, dtype=np.int32), np.diff(graph['ptr']))) and np.all((graph['dst'] >= 0) & (graph['dst'] < n)), 'Invalid source/destination row mapping') require(history == (n < 512) and (n < 512 or n == N_GLOBAL), 'Invalid small/full history declaration') require(np.all(graph['edge_id'] >= 0) and np.all(np.diff(graph['edge_id']) > 0), 'Original canonical edge IDs must be unique and increasing') if n == N_GLOBAL: require(edges == 25582938 and np.array_equal(nodes, np.arange(n, dtype=np.int32)) and np.array_equal(graph['edge_id'], np.arange(edges, dtype=np.int64)), 'Full canonical graph identity changed') bundle = {name: _read_array(bundle_dir/name, used) for name in BUNDLE_FILES} kc, apl_ids = bundle['kc_globals.npy'], bundle['apl_globals.npy'] require(kc.dtype == apl_ids.dtype == np.int32 and kc.shape == (4064,) and apl_ids.shape == (2,), 'Invalid APL/KC identities') require(np.all((kc >= 0) & (kc < N_GLOBAL)) and np.all(np.diff(kc) > 0) and np.all((apl_ids >= 0) & (apl_ids < N_GLOBAL)) and np.all(np.diff(apl_ids) > 0) and not np.intersect1d(kc, apl_ids).size, 'APL/KC identities overlap or are invalid') for key, shape in (('input_matrix.npy', (2, N_GLOBAL)), ('output_matrix.npy', (2, 4064))): require(bundle[key].dtype == np.float32 and bundle[key].shape == shape and np.isfinite(bundle[key]).all() and np.all(bundle[key] >= 0), 'Invalid normalized APL matrix') gpu = _load_run(gpu_dir, 'gpu', n, edges, steps, history, spec['name'], used, checks) cpu = _load_run(cpu_dir, 'cpu', n, edges, steps, history, spec['name'], used, checks) _pair_outputs(gpu, cpu, checks, differences, n, steps, history) used.add(case_dir/'requests.npz') with np.load(case_dir/'requests.npz', allow_pickle=False) as source: require(set(source.files) == {'tick', 'neuron_idx'} and source['tick'].dtype == source['neuron_idx'].dtype == np.int32, 'Fixed request NPZ schema changed') require(source['tick'].shape == source['neuron_idx'].shape and source['tick'].ndim == 1, 'Invalid sparse request shape') requested = list(zip(map(int, source['tick']), map(int, source['neuron_idx']))) for role, run in (('gpu', gpu), ('cpu', cpu)): _independent_event_accounting(run, role, graph, requested, steps, checks, differences) _check_apl_application(run, role, nodes, kc, checks, differences) # Independently derive the effective clone. Original graph weights stay read-only. cpu_weights = _read_array(cpu_dir/'weights.npy', used) require(cpu_weights.dtype == np.float64 and cpu_weights.shape == (edges,), 'CPU effective weight schema mismatch') expected_hash, base_hash, mask_hash = hashlib.sha256(), hashlib.sha256(), hashlib.sha256() weight_ok, mask_count = True, 0 for start in range(0, edges, 1 << 20): stop = min(edges, start+(1 << 20)) original = np.asarray(graph['weights'][start:stop]) require(np.isfinite(original).all(), 'Nonfinite original weights') raw_expected = graph['signed_raw'][start:stop]*(.275*.001) require(not np.any(_bit_mask(original, raw_expected)), 'Original SI weights differ from unaltered signed contacts') mask = np.isin(nodes[graph['src'][start:stop]], apl_ids) expected = original.copy() expected[mask] = np.float64(0) mask_count += int(mask.sum()) expected_hash.update(np.ascontiguousarray(expected, dtype=' 0 and edges > 0 and steps > 0 and type(history) is bool and history == (n < 512), 'Invalid repeat dimensions') left = _load_run(first, 'first_gpu', n, edges, steps, history, name, used, checks) right = _load_run(second, 'second_gpu', n, edges, steps, history, name, used, checks) _pair_outputs(left, right, checks, differences, n, steps, history) stable = ('case', 'complete', 'pass_all', 'steps', 'completed_ticks', 'nodes', 'edges', 'precision', 'apl_precision', 'state_units', 'total_spikes', 'requested', 'accepted_requests', 'rejected_requests', 'arriving_edges', 'accepted_arriving_edges', 'counts_match_sparse_events', 'all_finite', 'record_history', 'apl_fast_edges_zeroed', 'kc_targets_present', 'effective_weights_raw_sha256', 'apl_schedule', 'apl_si_conversion', 'build', 'sealed_ae_backend_sha256', 'learning', 'input_sha256') for key in stable: _check(checks, 'summary.'+key, key in left['summary'] and key in right['summary'] and left['summary'][key] == right['summary'][key]) if not checks['summary.'+key]: differences['summary.'+key] = {'first': left['summary'].get(key), 'second': right['summary'].get(key)} report.update(case=name, steps=steps, nodes=n, edges=edges, record_history=history, total_spikes=len(left['state']['step']), explicit_exclusions=['wall_seconds', 'diagnostic_simulation_seconds', 'build_or_cache_seconds', 'peak_allocated_vram_bytes', 'peak_reserved_vram_bytes']) except Exception as error: report['error'] = {'type': type(error).__name__, 'message': str(error)} return _finish(report, checks, differences, used, out_json)