mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-23 07:02:44 -05:00
129 lines
6.8 KiB
Python
129 lines
6.8 KiB
Python
"""Frozen development parity and repeated paired CPU inference benchmark.
|
|
|
|
Run with OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1. Never loads test arrays or
|
|
trains a model. The float32 feature probe is diagnostic and cannot be exported.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import platform
|
|
import time
|
|
import numpy as np
|
|
from scipy.fft import rfft
|
|
from .espfi import load_development
|
|
from .espfi_generalize import features
|
|
from .espfi_kernel import predict_features
|
|
from .espfi_runtime import BufferedKernelBundle, SPEC, load_state
|
|
from .espfi_train import digest, scores
|
|
|
|
|
|
def float32_probe(values):
|
|
"""Rejected unless both real and stress parity pass; no deployment API."""
|
|
x = np.asarray(values, dtype=np.float32)
|
|
logged = np.log(np.maximum(x, np.float32(1e-6)))
|
|
centered = logged - logged.mean(-1, keepdims=True)
|
|
relative = centered / np.maximum(np.sqrt(np.mean(centered**2, axis=(1, 2), keepdims=True)), 1e-6)
|
|
temporal = np.stack([v.mean(-1) for v in np.array_split(relative, 4, axis=-1)], axis=-1)
|
|
power = np.abs(rfft(relative, axis=-1))**2 / (190**2)
|
|
bands = np.stack([power[..., a:b].sum(-1) for a, b in
|
|
((1, 4), (4, 12), (12, 32), (32, 96))], axis=-1)
|
|
return np.concatenate((temporal.reshape(len(x), -1), bands.reshape(len(x), -1),
|
|
np.sqrt(np.mean(relative**2, axis=-1))), axis=1)
|
|
|
|
|
|
def benchmark(manifest, model, repetitions=1000, rounds=3):
|
|
if not 100 <= repetitions <= 10000 or not 3 <= rounds <= 10:
|
|
raise ValueError('Expected 100..10000 pairs and 3..10 rounds')
|
|
if any(os.environ.get(k) != '1' for k in ('OPENBLAS_NUM_THREADS', 'OMP_NUM_THREADS')):
|
|
raise ValueError('Set OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 before launch')
|
|
parts, provenance = load_development(manifest)
|
|
x, y = parts['validation']['x'], parts['validation']['y']
|
|
state = load_state(model)
|
|
reference = lambda v: predict_features(state, features(v, SPEC['family']))
|
|
models = {name: BufferedKernelBundle(model, fuse=fuse) for name, fuse in
|
|
(('buffered', False), ('fused', True))}
|
|
calls = {'reference': reference, **{k: v.predict for k, v in models.items()}}
|
|
expected = reference(x)
|
|
parity = {}
|
|
for name, candidate in models.items():
|
|
actual = candidate.predict(x)
|
|
error = float(np.abs(expected-actual).max())
|
|
equal = bool(np.array_equal(expected.argmax(1), actual.argmax(1)))
|
|
if error > 1e-4 or not equal:
|
|
raise ValueError(f'{name} failed parity: {error}, predictions_equal={equal}')
|
|
parity[name] = {'max_abs_logit_error': error, 'predictions_equal': equal,
|
|
'retained_model_bytes': sum(v.nbytes for v in candidate.state.values()),
|
|
'workspace_bytes': candidate.workspace_bytes}
|
|
for i in range(30):
|
|
for call in calls.values():
|
|
call(x[i % len(x):i % len(x)+1])
|
|
timings = []
|
|
names = list(calls)
|
|
for round_index in range(rounds):
|
|
elapsed = {name: [] for name in names}
|
|
for i in range(repetitions):
|
|
sample = x[i % len(x):i % len(x)+1]
|
|
order = names[(i+round_index) % 3:] + names[:(i+round_index) % 3]
|
|
if i % 2:
|
|
order = order[::-1]
|
|
for name in order:
|
|
start = time.perf_counter_ns()
|
|
calls[name](sample)
|
|
elapsed[name].append((time.perf_counter_ns()-start)/1e6)
|
|
timings.append({name: {'p50_ms': float(np.percentile(v, 50)),
|
|
'p95_ms': float(np.percentile(v, 95))} for name, v in elapsed.items()})
|
|
for name in models:
|
|
parity[name]['speed_gate_passed'] = all(
|
|
r[name]['p50_ms'] <= .9*r['reference']['p50_ms'] and
|
|
r[name]['p95_ms'] <= r['reference']['p95_ms'] for r in timings)
|
|
# This stress signal deliberately exposes cancellation in single precision.
|
|
rng = np.random.default_rng(7)
|
|
stress = np.stack((np.ones((52, 190)), np.full((52, 190), 1e8),
|
|
1e8 * (1+rng.normal(0, 1e-8, (52, 190)))))
|
|
probe_error = float(np.abs(reference(stress)-predict_features(state, float32_probe(stress))).max())
|
|
probe_dev = predict_features(state, float32_probe(x))
|
|
report = {'evidence': 'MEASURED', 'promotionAuthorized': False,
|
|
'prediction_count': len(x), 'tolerance': 1e-4, 'parity': parity,
|
|
'rounds': timings, 'pairs_per_round': repetitions,
|
|
'development': scores(y, expected.argmax(1), 7),
|
|
'float32_feature_probe': {'evidence': 'SYNTHETIC stress plus development parity',
|
|
'stress_max_abs_logit_error': probe_error,
|
|
'development_max_abs_logit_error': float(np.abs(expected-probe_dev).max()),
|
|
'development_predictions_equal': bool(np.array_equal(expected.argmax(1), probe_dev.argmax(1))),
|
|
'stress_gate_passed': probe_error <= 1e-4, 'exported': False},
|
|
'model_sha256': digest(Path(model)/'model.safetensors'),
|
|
'metadata_sha256': digest(Path(model)/'metadata.json'),
|
|
'source_sha256': {name: digest(Path(__file__).with_name(name)) for name in
|
|
('espfi_runtime.py', 'espfi_runtime_benchmark.py', 'espfi_generalize.py', 'espfi_kernel.py')},
|
|
'runtime': {'python': platform.python_version(), 'numpy': np.__version__,
|
|
'machine': platform.machine(), 'threads': 1},
|
|
'provenance': provenance,
|
|
'scope': 'Rotating paired batch1 calls cycling development windows; includes checks, '
|
|
'features and classifier; excludes acquisition, pooling, IO. Shared CPU, '
|
|
'not edge hardware. Retained tensors/workspace are not total process RAM. '
|
|
'No training or final-test evaluation. Repeatedly observed development set.'}
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--manifest', required=True)
|
|
parser.add_argument('--model', required=True)
|
|
parser.add_argument('--output', required=True)
|
|
parser.add_argument('--repetitions', type=int, default=1000)
|
|
parser.add_argument('--rounds', type=int, default=3)
|
|
args = parser.parse_args()
|
|
output = Path(args.output)
|
|
# Reserve without overwriting a prior receipt; failed runs stay incomplete.
|
|
with output.open('x') as handle:
|
|
handle.write('{"status":"running","promotionAuthorized":false}\n')
|
|
report = benchmark(args.manifest, args.model, args.repetitions, args.rounds)
|
|
report['status'] = 'complete'
|
|
output.write_text(json.dumps(report, indent=2, allow_nan=False)+'\n')
|
|
print(json.dumps({k: v for k, v in report.items() if k not in ('provenance', 'source_sha256', 'development')}, indent=2))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|