#!/usr/bin/env python3 """Bounded numerical parity check for PyTorch and native transformer inference.""" from __future__ import annotations import argparse import hashlib import math import struct import subprocess import sys import tempfile import zlib from pathlib import Path from typing import Sequence import numpy as np import train_transformer as training PROBE_INPUT_MAGIC = b"SZTPRB01" PROBE_OUTPUT_MAGIC = b"SZTPOU01" PROBE_VERSION = 1 PROBE_INPUT_HEADER = struct.Struct("<8sIIIII") PROBE_OUTPUT_HEADER = struct.Struct("<8sIII") MAXIMUM_CASES = 64 OUTPUT_FLOAT_COUNT = 45 OUTPUT_NAMES = ( "improvement_logit", "improvement_probability", "expected_defect_gain", "improvement_probability_variance", "expected_defect_gain_variance", *(f"plane_probability[{index}]" for index in range(training.FACE_COUNT)), *(f"plane_probability_variance[{index}]" for index in range(training.FACE_COUNT)), *(f"move_probability[{index}]" for index in range(training.MOVE_COUNT)), *(f"move_probability_variance[{index}]" for index in range(training.MOVE_COUNT)), *(f"scale_probability[{index}]" for index in range(training.SCALE_COUNT)), *(f"scale_probability_variance[{index}]" for index in range(training.SCALE_COUNT)), ) def parse_args(argv: Sequence[str]) -> argparse.Namespace: repository = Path(__file__).resolve().parents[1] parser = argparse.ArgumentParser( description=( "Compare a serialized SZTRNK01 model through CPU FP32 PyTorch and " "the native TransformerRanker implementation." ) ) parser.add_argument("--repository", default=str(repository)) parser.add_argument( "--model", default="results/search/neural/transformer/current.sztf" ) parser.add_argument( "--native-probe", help="TransformerRankerSelfTest executable containing --parity-probe", ) parser.add_argument("--cases", type=int, default=8) parser.add_argument("--absolute-tolerance", type=float, default=2.0e-4) parser.add_argument("--relative-tolerance", type=float, default=2.0e-4) parser.add_argument("--timeout-seconds", type=float, default=30.0) parser.add_argument("--torch-threads", type=int, default=1) parser.add_argument("--device", choices=("cpu", "cuda"), default="cpu") return parser.parse_args(argv) def resolve_path(repository: Path, value: str) -> Path: path = Path(value).expanduser() if not path.is_absolute(): path = repository / path return path.resolve() def find_native_probe(repository: Path, explicit: str | None) -> Path: if explicit: path = resolve_path(repository, explicit) if not path.is_file(): raise FileNotFoundError(f"native probe does not exist: {path}") return path candidates = ( repository / "build/msbuild/bin/x64/Release/TransformerRankerSelfTest.exe", repository / "build/vs2026/Release/transformer_ranker_selftest.exe", repository / "build/TransformerRankerSelfTest.exe", ) for candidate in candidates: if candidate.is_file(): return candidate.resolve() raise FileNotFoundError( "native probe was not found; build transformer_ranker_selftest or pass " "--native-probe" ) def validate_options(args: argparse.Namespace) -> None: if not 1 <= args.cases <= MAXIMUM_CASES: raise ValueError(f"--cases must be in 1..{MAXIMUM_CASES}") for name in ("absolute_tolerance", "relative_tolerance"): value = float(getattr(args, name)) if not math.isfinite(value) or value < 0.0: raise ValueError(f"--{name.replace('_', '-')} must be finite and non-negative") if not math.isfinite(args.timeout_seconds) or not 1.0 <= args.timeout_seconds <= 300.0: raise ValueError("--timeout-seconds must be in 1..300") if not 1 <= args.torch_threads <= 64: raise ValueError("--torch-threads must be in 1..64") def load_model( path: Path, device: training.torch.device ) -> tuple[list[training.RankerMember], str]: model_bytes = path.read_bytes() expected_float_count = training.expected_payload_float_count() expected_payload_bytes = expected_float_count * 4 expected_size = training.MODEL_HEADER_BYTES + expected_payload_bytes if len(model_bytes) != expected_size: raise ValueError( f"model size mismatch: {len(model_bytes)} != {expected_size}" ) if model_bytes[:8] != training.MODEL_MAGIC: raise ValueError("model magic is invalid") format_version, feature_version, objective_version, approved = struct.unpack_from( "<4I", model_bytes, 8 ) dimensions = struct.unpack_from("<9I", model_bytes, 24) expected_dimensions = ( training.FACE_COUNT, training.FACE_FEATURES, training.GLOBAL_FEATURES, training.MODEL_WIDTH, training.ATTENTION_HEADS, training.LAYER_COUNT, training.FEED_FORWARD_WIDTH, training.ENSEMBLE_SIZE, training.TOPOLOGY_COUNT, ) if ( format_version != training.MODEL_FORMAT_VERSION or feature_version != training.FEATURE_FORMAT_VERSION or objective_version != training.OBJECTIVE_VERSION or approved != 1 or dimensions != expected_dimensions or struct.unpack_from(" tuple[np.ndarray, np.ndarray, np.ndarray]: # Every value is an integer divided by 64, so both sides receive the same # exactly representable float32 bits. The bounded formula covers signs, # magnitudes, face positions and several topology embeddings. sample = np.arange(case_count, dtype=np.int64)[:, None, None] face = np.arange(training.FACE_COUNT, dtype=np.int64)[None, :, None] component = np.arange(training.FACE_FEATURES, dtype=np.int64)[None, None, :] face_features = ( ((sample * 37 + face * 17 + component * 13) % 129) - 64 ).astype(np.float32) / np.float32(64.0) global_component = np.arange(training.GLOBAL_FEATURES, dtype=np.int64)[None, :] global_features = ( ( (np.arange(case_count, dtype=np.int64)[:, None] * 29 + global_component * 11) % 129 ) - 64 ).astype(np.float32) / np.float32(64.0) topology = ( (np.arange(case_count, dtype=np.uint32) * np.uint32(17) + np.uint32(42)) % np.uint32(training.TOPOLOGY_COUNT) ) return ( np.ascontiguousarray(face_features, dtype=" None: case_count = int(face.shape[0]) payload = bytearray( PROBE_INPUT_HEADER.pack( PROBE_INPUT_MAGIC, PROBE_VERSION, case_count, training.FACE_COUNT, training.FACE_FEATURES, training.GLOBAL_FEATURES, ) ) for index in range(case_count): payload.extend(struct.pack(" tuple[np.ndarray, np.ndarray]: data = path.read_bytes() if len(data) < PROBE_OUTPUT_HEADER.size: raise ValueError("native probe output is truncated") magic, version, case_count, float_count = PROBE_OUTPUT_HEADER.unpack_from(data) if ( magic != PROBE_OUTPUT_MAGIC or version != PROBE_VERSION or case_count != expected_cases or float_count != OUTPUT_FLOAT_COUNT ): raise ValueError("native probe output header is invalid") record_size = 4 + OUTPUT_FLOAT_COUNT * 4 if len(data) != PROBE_OUTPUT_HEADER.size + case_count * record_size: raise ValueError("native probe output has trailing or truncated data") finite = np.empty(case_count, dtype=np.bool_) output = np.empty((case_count, OUTPUT_FLOAT_COUNT), dtype=np.float32) offset = PROBE_OUTPUT_HEADER.size for index in range(case_count): flag = struct.unpack_from(" np.ndarray: face_tensor = training.torch.from_numpy(face).to(device) global_tensor = training.torch.from_numpy(global_features).to(device) topology_tensor = training.torch.from_numpy(topology.astype(np.int64)).to(device) probabilities = [] gains = [] plane_probabilities = [] move_probabilities = [] scale_probabilities = [] for member in members: value, plane, move, scale = member( face_tensor, global_tensor, topology_tensor ) probabilities.append(training.torch.sigmoid(value[:, 0])) gains.append(value[:, 1]) plane_probabilities.append(training.torch.softmax(plane, dim=-1)) move_probabilities.append(training.torch.softmax(move, dim=-1)) scale_probabilities.append(training.torch.softmax(scale, dim=-1)) def aggregate(values: list[training.torch.Tensor]): stacked = training.torch.stack(values) mean = stacked.mean(dim=0) variance = ((stacked - mean.unsqueeze(0)) ** 2).mean(dim=0) return mean, variance probability, probability_variance = aggregate(probabilities) gain, gain_variance = aggregate(gains) plane, plane_variance = aggregate(plane_probabilities) move, move_variance = aggregate(move_probabilities) scale, scale_variance = aggregate(scale_probabilities) improvement_logit = training.torch.logit( probability.clamp(1.0e-6, 1.0 - 1.0e-6) ) result = training.torch.cat( ( improvement_logit[:, None], probability[:, None], gain[:, None], probability_variance[:, None], gain_variance[:, None], plane, plane_variance, move, move_variance, scale, scale_variance, ), dim=1, ) if result.shape[1] != OUTPUT_FLOAT_COUNT or not result.isfinite().all(): raise RuntimeError("PyTorch reference emitted invalid output") return result.cpu().numpy() def report_comparison( native: np.ndarray, reference: np.ndarray, absolute_tolerance: float, relative_tolerance: float, ) -> bool: difference = np.abs(native.astype(np.float64) - reference.astype(np.float64)) tolerance = absolute_tolerance + relative_tolerance * np.abs( reference.astype(np.float64) ) max_absolute_flat = int(np.argmax(difference)) absolute_case, absolute_field = np.unravel_index( max_absolute_flat, difference.shape ) relative = difference / np.maximum( np.maximum(np.abs(native), np.abs(reference)).astype(np.float64), 1.0e-12 ) max_relative_flat = int(np.argmax(relative)) relative_case, relative_field = np.unravel_index( max_relative_flat, relative.shape ) ratio = difference / np.maximum(tolerance, np.finfo(np.float64).tiny) max_ratio_flat = int(np.argmax(ratio)) ratio_case, ratio_field = np.unravel_index(max_ratio_flat, ratio.shape) print( "max_abs_diff=" f"{difference[absolute_case, absolute_field]:.9e} " f"case={absolute_case} field={OUTPUT_NAMES[absolute_field]} " f"native={native[absolute_case, absolute_field]:.9e} " f"pytorch={reference[absolute_case, absolute_field]:.9e}" ) print( "max_rel_diff=" f"{relative[relative_case, relative_field]:.9e} " f"case={relative_case} field={OUTPUT_NAMES[relative_field]} " f"native={native[relative_case, relative_field]:.9e} " f"pytorch={reference[relative_case, relative_field]:.9e}" ) print( "max_tolerance_ratio=" f"{ratio[ratio_case, ratio_field]:.9e} " f"case={ratio_case} field={OUTPUT_NAMES[ratio_field]}" ) return bool(np.all(difference <= tolerance)) def main(argv: Sequence[str] | None = None) -> int: args = parse_args(argv or sys.argv[1:]) validate_options(args) repository = Path(args.repository).expanduser().resolve() model_path = resolve_path(repository, args.model) native_probe = find_native_probe(repository, args.native_probe) if not model_path.is_file(): raise FileNotFoundError(f"model does not exist: {model_path}") if args.device == "cuda" and not training.torch.cuda.is_available(): raise RuntimeError("--device cuda requested but CUDA PyTorch is unavailable") training.torch.set_num_threads(args.torch_threads) training.torch.set_float32_matmul_precision("highest") if training.torch.cuda.is_available(): training.torch.backends.cuda.matmul.allow_tf32 = False training.torch.backends.cudnn.allow_tf32 = False device = training.torch.device(args.device) members, model_digest = load_model(model_path, device) face, global_features, topology = make_cases(args.cases) reference = pytorch_reference( members, face, global_features, topology, device ) with tempfile.TemporaryDirectory(prefix="szilassi_transformer_parity_") as temp: input_path = Path(temp) / "input.bin" output_path = Path(temp) / "native.bin" write_probe_input(input_path, face, global_features, topology) command = [ str(native_probe), "--parity-probe", str(model_path), str(input_path), str(output_path), ] completed = subprocess.run( command, cwd=repository, check=False, capture_output=True, text=True, timeout=args.timeout_seconds, ) if completed.returncode != 0: raise RuntimeError( f"native probe failed with exit code {completed.returncode}: " f"{completed.stderr.strip() or completed.stdout.strip()}" ) finite, native = read_probe_output(output_path, args.cases) if not finite.all(): bad_cases = np.flatnonzero(~finite).tolist() raise RuntimeError(f"native inference fell back for cases: {bad_cases}") print(f"model_sha256={model_digest}") print( f"device={device.type} cases={args.cases} " f"compared_floats={native.size} torch_threads={args.torch_threads}" ) passed = report_comparison( native, reference, args.absolute_tolerance, args.relative_tolerance, ) print( f"tolerance=atol:{args.absolute_tolerance:.9e}," f"rtol:{args.relative_tolerance:.9e} status={'PASS' if passed else 'FAIL'}" ) return 0 if passed else 1 if __name__ == "__main__": try: raise SystemExit(main()) except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as exc: print(f"Transformer parity check failed: {exc}", file=sys.stderr) raise SystemExit(2) from exc