259 lines
16 KiB
Python
259 lines
16 KiB
Python
"""Synthetic CPU/mocked-CUDA contract tests; never run a real input generator."""
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import types
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import numpy as np
|
|
|
|
import conditioning_inputs as c
|
|
|
|
|
|
def req(rows):
|
|
values = np.asarray(rows, np.int32).reshape(-1, 2)
|
|
return {'tick': values[:, 0].copy(), 'neuron_idx': values[:, 1].copy()}
|
|
|
|
|
|
class FakeTensor:
|
|
def __init__(self, values, device):
|
|
self.values = np.array(values, copy=True)
|
|
self.device, self.dtype = device, self.values.dtype
|
|
|
|
def __getitem__(self, key): return FakeTensor(self.values[key], self.device)
|
|
def __lt__(self, other): return FakeTensor(self.values < other.values, self.device)
|
|
def cpu(self): return self
|
|
def numpy(self): return self.values.copy()
|
|
|
|
|
|
class FakeTorch:
|
|
"""Observable adapter, not a CUDA or random-number emulation."""
|
|
float32 = np.dtype(np.float32)
|
|
__version__ = 'SYNTHETIC-CONTRACT-MOCK-NOT-TORCH'
|
|
version = types.SimpleNamespace(cuda=None)
|
|
|
|
def __init__(self, uniforms, truncate=False):
|
|
self.uniforms = uniforms
|
|
self.calls, self.seeds, self.truncate = [], [], truncate
|
|
self.cuda = types.SimpleNamespace(is_available=lambda: True,
|
|
get_device_name=lambda device: 'SYNTHETIC-NOT-A-REAL-GPU', get_device_capability=lambda device: (0, 0),
|
|
get_device_properties=lambda device: types.SimpleNamespace())
|
|
|
|
def get_default_dtype(self): return self.float32
|
|
def device(self, name): return name
|
|
|
|
def Generator(self, device):
|
|
owner = self
|
|
class Generator:
|
|
def manual_seed(self, value):
|
|
owner.seeds.append(value); self.seed = value & 0xffffffff if owner.truncate else value
|
|
def initial_seed(self): return self.seed
|
|
self.calls.append(('generator', device)); return Generator()
|
|
|
|
def as_tensor(self, values, dtype, device):
|
|
self.calls.append(('probability', np.array(values, copy=True), dtype, device))
|
|
return FakeTensor(values, device)
|
|
|
|
def rand(self, shape, generator, device):
|
|
self.calls.append(('rand', shape, generator.initial_seed(), device))
|
|
if shape != self.uniforms.shape: raise AssertionError('Unexpected draw shape')
|
|
return FakeTensor(self.uniforms, device)
|
|
|
|
|
|
class InputsTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.targets = np.array([19, 3], np.int32)
|
|
self.rates = np.array([5000, 10000], np.float32)
|
|
|
|
def test_frozen_probability_dtype_order_signed_zero_and_no_mutation(self):
|
|
targets = np.arange(6, dtype=np.int32)
|
|
rates = np.array([0., -0., np.nextafter(np.float32(0), np.float32(1)), .1, 123.456, 10000.], np.float32)
|
|
before = rates.tobytes(); rates.setflags(write=False)
|
|
actual = c.probabilities_f32(targets, rates)
|
|
self.assertEqual(actual.dtype, np.float32)
|
|
self.assertEqual(actual.tobytes(), (rates * .1 / 1000.).tobytes())
|
|
self.assertEqual(rates.tobytes(), before)
|
|
|
|
def test_uniform_threshold_is_strict_and_bernoulli_not_poisson_counts(self):
|
|
p = np.array([0, .5, 1], np.float32)
|
|
u = np.array([[0, .5, np.nextafter(np.float32(1), np.float32(0))], [0, .499, 0]], np.float32)
|
|
forced = c.forced_from_uniforms(u, p)
|
|
np.testing.assert_array_equal(forced, [[False, False, True], [False, True, True]])
|
|
self.assertEqual(forced.dtype, np.bool_)
|
|
|
|
def test_original_PN_column_order_then_sparse_tick_global_sort_only(self):
|
|
forced = np.array([[True, True], [True, False], [False, True]], np.bool_)
|
|
before = forced.copy(); self.targets.setflags(write=False)
|
|
result = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=3)
|
|
np.testing.assert_array_equal(result['requests']['tick'], [0, 0, 1, 2])
|
|
np.testing.assert_array_equal(result['requests']['neuron_idx'], [3, 19, 19, 3])
|
|
np.testing.assert_array_equal(forced, before)
|
|
self.assertEqual(result['audit']['duplicate_requests_removed'], 0)
|
|
|
|
def test_half_open_window_masks_full_matrix_including_empty_window(self):
|
|
forced = np.ones((5, 2), np.bool_)
|
|
result = c.requests_from_forced(forced, self.targets, cs_start_tick=1, cs_stop_tick=4)
|
|
self.assertEqual(result['audit']['full_matrix_boolean_hits'], 10)
|
|
self.assertEqual(result['audit']['outside_CS_boolean_hits_masked'], 4)
|
|
self.assertEqual(set(result['requests']['tick']), {1, 2, 3})
|
|
empty = c.requests_from_forced(forced, self.targets, cs_start_tick=2, cs_stop_tick=2)
|
|
self.assertEqual(len(empty['requests']['tick']), 0)
|
|
self.assertEqual(empty['audit']['outside_CS_boolean_hits_masked'], 10)
|
|
|
|
def test_full_63bit_seed_single_full_draw_and_mock_scope_receipt(self):
|
|
seed = (1 << 62) + (1 << 32) + 19
|
|
fake = FakeTorch(np.array([[.2, .6], [.7, .7], [.1, .2], [.8, .1], [.1, .1]], np.float32))
|
|
with patch.object(c, '_load_torch', return_value=fake):
|
|
result = c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=5, cs_start_tick=1, cs_stop_tick=4,
|
|
device='cuda:0', max_uniform_draws=10)
|
|
self.assertEqual(fake.seeds, [seed]); draws = [x for x in fake.calls if x[0] == 'rand']
|
|
self.assertEqual(draws, [('rand', (5, 2), seed, 'cuda:0')])
|
|
a = result['audit']; self.assertEqual(a['seed_hex'], f'{seed:016x}')
|
|
self.assertEqual(a['numpy_version'], np.__version__)
|
|
self.assertEqual(a['torch_version'], fake.__version__)
|
|
self.assertEqual(a['device_name'], 'SYNTHETIC-NOT-A-REAL-GPU')
|
|
self.assertFalse(a['CUDA_sequence_equivalence_tested_here'])
|
|
self.assertFalse(a['CUDA_high_seed_bit_sequence_influence_tested_here'])
|
|
self.assertFalse(a['full_old_L_input_identity_claimed'])
|
|
self.assertEqual(a['uniform_scalar_draws'], 10)
|
|
self.assertEqual((a['max_uniform_draws'], a['uniform_matrix_bytes']), (10, 40))
|
|
self.assertEqual(a['device_identity'], {'uuid': None, 'pci_bus_id': None, 'query_error': None})
|
|
np.testing.assert_array_equal(result['requests']['tick'], [1, 2, 2, 3])
|
|
np.testing.assert_array_equal(result['requests']['neuron_idx'], [3, 3, 19, 3])
|
|
json.dumps(a, allow_nan=False)
|
|
|
|
def test_high_seed_bits_preserved_and_bad_consumer_detected_before_draw(self):
|
|
for seed in (0, 17, (1 << 32) + 17, (1 << 63) - 1):
|
|
fake = FakeTorch(np.zeros((1, 2), np.float32))
|
|
with patch.object(c, '_load_torch', return_value=fake):
|
|
result = c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=1, cs_start_tick=0, cs_stop_tick=1,
|
|
device='cuda:0', max_uniform_draws=2)
|
|
self.assertEqual(result['audit']['generator_initial_seed'], seed)
|
|
self.assertEqual(fake.seeds, [seed])
|
|
fake = FakeTorch(np.zeros((1, 2), np.float32), truncate=True)
|
|
with patch.object(c, '_load_torch', return_value=fake), self.assertRaisesRegex(ValueError, 'complete semantic seed'):
|
|
c.generate_cs_requests(self.targets, self.rates, seed=(1 << 32) + 17, steps=1, cs_start_tick=0, cs_stop_tick=1,
|
|
device='cuda:0', max_uniform_draws=2)
|
|
self.assertFalse(any(x[0] == 'rand' for x in fake.calls))
|
|
|
|
def test_invalid_seed_clock_or_rate_never_imports_torch(self):
|
|
for seed in (-1, 2**63, True, np.int64(3), 3.):
|
|
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
|
|
c.generate_cs_requests(self.targets, self.rates, seed=seed, steps=5, cs_start_tick=0, cs_stop_tick=5,
|
|
device='cuda:0', max_uniform_draws=10)
|
|
loader.assert_not_called()
|
|
for start, stop in ((-1, 1), (2, 1), (0, 6), (False, 1)):
|
|
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
|
|
c.generate_cs_requests(self.targets, self.rates, seed=3, steps=5, cs_start_tick=start, cs_stop_tick=stop,
|
|
device='cuda:0', max_uniform_draws=10)
|
|
loader.assert_not_called()
|
|
|
|
def test_invalid_arrays_and_no_clipping_or_duplicate_merging(self):
|
|
for rates in (np.array([-1, 2], np.float32), np.array([np.nan, 2], np.float32), np.array([np.inf, 2], np.float32),
|
|
np.array([10001, 2], np.float32), self.rates.astype(np.float64), self.rates[:1]):
|
|
with self.assertRaises(ValueError): c.probabilities_f32(self.targets, rates)
|
|
for targets in (np.array([3, 3], np.int32), np.array([-1, 3], np.int32), np.array([166700, 3], np.int32), self.targets.astype(np.int64)):
|
|
with self.assertRaises(ValueError): c.probabilities_f32(targets, self.rates)
|
|
with self.assertRaises(ValueError): c.probabilities_f32(self.targets, self.rates, dt_ms=.2)
|
|
with self.assertRaises(ValueError): c.requests_from_forced(np.ones((2, 2), np.uint8), self.targets, cs_start_tick=0, cs_stop_tick=2)
|
|
|
|
def test_no_cpu_fallback_or_changed_default_float_dtype(self):
|
|
for variant in ('cpu', 'unavailable', 'float64'):
|
|
fake = FakeTorch(np.zeros((1, 2), np.float32)); device = 'cuda:0'
|
|
if variant == 'cpu': device = 'cpu'
|
|
if variant == 'unavailable': fake.cuda.is_available = lambda: False
|
|
if variant == 'float64': fake.get_default_dtype = lambda: np.dtype(np.float64)
|
|
with patch.object(c, '_load_torch', return_value=fake), self.assertRaises(ValueError):
|
|
c.generate_cs_requests(self.targets, self.rates, seed=3, steps=1, cs_start_tick=0, cs_stop_tick=1,
|
|
device=device, max_uniform_draws=2)
|
|
self.assertFalse(any(x[0] == 'rand' for x in fake.calls))
|
|
|
|
def test_explicit_device_and_budget_required_and_budget_checked_before_torch(self):
|
|
common = dict(seed=3, steps=5, cs_start_tick=0, cs_stop_tick=1)
|
|
for missing in ({}, {'device': 'cuda:0'}, {'max_uniform_draws': 10}):
|
|
with patch.object(c, '_load_torch') as loader, self.assertRaises(TypeError):
|
|
c.generate_cs_requests(self.targets, self.rates, **common, **missing)
|
|
loader.assert_not_called()
|
|
for budget in (0, -1, 9, True, 10., np.int64(10)):
|
|
with patch.object(c, '_load_torch') as loader, self.assertRaises(ValueError):
|
|
c.generate_cs_requests(self.targets, self.rates, **common, device='cuda:0', max_uniform_draws=budget)
|
|
loader.assert_not_called()
|
|
# Full 5x2 matrix admission, not merely the 1-tick active CS window.
|
|
fake = FakeTorch(np.zeros((5, 2), np.float32))
|
|
with patch.object(c, '_load_torch', return_value=fake):
|
|
result = c.generate_cs_requests(self.targets, self.rates, **common, device='cuda:1', max_uniform_draws=10)
|
|
self.assertEqual(result['audit']['execution_device'], 'cuda:1')
|
|
self.assertEqual(result['audit']['uniform_scalar_draws'], 10)
|
|
|
|
def test_optional_device_uuid_recorded_as_reported_or_explicitly_unavailable(self):
|
|
fake = FakeTorch(np.zeros((1, 2), np.float32))
|
|
fake.cuda.get_device_properties = lambda device: types.SimpleNamespace(uuid='SYNTHETIC-UUID', pci_bus_id='SYNTHETIC-PCI')
|
|
args = dict(seed=3, steps=1, cs_start_tick=0, cs_stop_tick=1, device='cuda:7', max_uniform_draws=2)
|
|
with patch.object(c, '_load_torch', return_value=fake):
|
|
audit = c.generate_cs_requests(self.targets, self.rates, **args)['audit']
|
|
self.assertEqual(audit['device_identity'], {'uuid': 'SYNTHETIC-UUID', 'pci_bus_id': 'SYNTHETIC-PCI', 'query_error': None})
|
|
self.assertFalse(audit['device_identity_verified_against_external_pin'])
|
|
def unavailable(device): raise RuntimeError('Synthetic property unavailable')
|
|
fake.cuda.get_device_properties = unavailable
|
|
with patch.object(c, '_load_torch', return_value=fake):
|
|
audit = c.generate_cs_requests(self.targets, self.rates, **args)['audit']
|
|
self.assertIsNone(audit['device_identity']['uuid'])
|
|
self.assertEqual(audit['device_identity']['query_error'], 'RuntimeError: Synthetic property unavailable')
|
|
|
|
def test_returned_audited_arrays_are_owned_readonly_and_inputs_not_frozen(self):
|
|
forced = np.ones((2, 2), np.bool_)
|
|
generated = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=2)
|
|
merged = c.merge_requests(req([[0, 9]]), req([[1, 8]]), steps=2)
|
|
for value in (*generated['requests'].values(), *merged['requests'].values(), merged['source_mask']):
|
|
self.assertTrue(value.flags.owndata)
|
|
self.assertFalse(value.flags.writeable)
|
|
with self.assertRaises(ValueError): value[0] = 0
|
|
self.assertTrue(forced.flags.writeable)
|
|
self.assertTrue(self.targets.flags.writeable)
|
|
|
|
def test_empty_targets_and_p0_p1_are_explicit_valid_limits(self):
|
|
result = c.requests_from_forced(np.zeros((3, 0), np.bool_), np.empty(0, np.int32), cs_start_tick=0, cs_stop_tick=3)
|
|
self.assertEqual(result['requests']['tick'].dtype, np.int32); self.assertEqual(len(result['requests']['tick']), 0)
|
|
np.testing.assert_array_equal(c.probabilities_f32(self.targets, np.array([0, 10000], np.float32)), [0, 1])
|
|
|
|
def test_merging_disjoint_sources_is_sorted_without_mutating_inputs(self):
|
|
cs, us = req([[0, 9], [3, 4]]), req([[0, 2], [2, 5]])
|
|
for value in [*cs.values(), *us.values()]: value.setflags(write=False)
|
|
result = c.merge_requests(cs, us, steps=4)
|
|
np.testing.assert_array_equal(result['requests']['tick'], [0, 0, 2, 3])
|
|
np.testing.assert_array_equal(result['requests']['neuron_idx'], [2, 9, 5, 4])
|
|
np.testing.assert_array_equal(result['source_mask'], [2, 1, 2, 1])
|
|
self.assertEqual(result['audit']['explicit_boolean_overlap_collapses'], 0)
|
|
|
|
def test_collision_rejected_by_default_or_explicitly_audited_boolean_union(self):
|
|
cs, us = req([[0, 9], [3, 4]]), req([[0, 9], [2, 5], [3, 4]])
|
|
with self.assertRaises(c.RequestConflictError) as error: c.merge_requests(cs, us, steps=4)
|
|
self.assertEqual(error.exception.audit['cross_source_conflict_count'], 2)
|
|
result = c.merge_requests(cs, us, steps=4, conflict_policy='boolean_union')
|
|
np.testing.assert_array_equal(result['source_mask'], [3, 2, 3])
|
|
a = result['audit']; self.assertEqual((a['CS_attempts'], a['US_attempts'], a['output_requests']), (2, 3, 3))
|
|
self.assertEqual(a['explicit_boolean_overlap_collapses'], 2)
|
|
self.assertEqual([x['combined_boolean_requests'] for x in a['conflicts']], [1, 1])
|
|
json.dumps(a, allow_nan=False)
|
|
|
|
def test_within_source_duplicate_unsorted_and_bad_range_never_deduped(self):
|
|
for rows in ([[0, 3], [0, 3]], [[1, 3], [0, 3]], [[4, 3]], [[0, -1]]):
|
|
with self.assertRaises(ValueError): c.merge_requests(req(rows), req([]), steps=4, conflict_policy='boolean_union')
|
|
with self.assertRaises(ValueError): c.merge_requests(req([]), req([]), steps=4, conflict_policy='clip')
|
|
|
|
def test_frozen_AK_pure_conversion_AST_matches_for_full_window(self):
|
|
local = Path(__file__).resolve().parent.parent / 'apl-timing-validation-v1/encoder_inputs.py'
|
|
raw = local.read_bytes(); self.assertEqual(hashlib.sha256(raw).hexdigest(), c.AK_ENCODER_SHA)
|
|
tree = ast.parse(raw); node = next(n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name == 'requests_from_forced')
|
|
namespace = {'np': np}; exec(compile(ast.Module(body=[node], type_ignores=[]), str(local), 'exec'), namespace)
|
|
forced = np.array([[True, True], [False, True], [True, False]], np.bool_)
|
|
expected = namespace['requests_from_forced'](forced, self.targets)
|
|
actual = c.requests_from_forced(forced, self.targets, cs_start_tick=0, cs_stop_tick=3)['requests']
|
|
np.testing.assert_array_equal(expected, np.column_stack([actual['tick'], actual['neuron_idx']]))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|