feat: neural_sim Python golden functional reference simulator
Adds tools/neural_sim/, a NumPy-based reference implementation of the FPGA-Neural V2 numeric model (INT8 in/weight, 32-bit wraparound accumulation, ReLU+saturate out), derived directly from hardware/v2/rtl/neural_processor.v (not assumed) and reusing tools/validation/mac_oracle.py's own pre-existing, hand-verified two's-complement primitives rather than duplicating them. Provides: neuron/layer/network models, a logical memory model of the real V2 SDRAM map (weights/activations/results), deterministic test-vector generators (simple/signed/extremes/zero/random/D-Stress 256x128) with JSON golden-vector export, an FPGA-vs-Python bit-exact comparison utility, four example networks, a CLI (`python -m tools.neural_sim ...`), and a 96-test pytest suite (all passing) covering signed-arithmetic edge cases (including a direct 32-bit wraparound proof), scalar-vs-vectorized neuron cross-checks, layer/memory/vector/comparison tests. This is a golden functional reference (bit-exact numeric result), explicitly NOT a cycle-accurate FPGA simulator -- see tools/neural_sim/README.md for the full scope statement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
FPGA-vs-Python comparison utility (B10). Loads FPGA-generated results
|
||||
and compares them against this package's own Python golden results.
|
||||
The primary pass/fail criterion is 0 mismatches -- exact bit-exact
|
||||
match, never a tolerance-based "close enough" comparison (per this
|
||||
project's own explicit "for the true bit-exact model, do not hide
|
||||
numerical differences behind tolerances" instruction).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComparisonReport:
|
||||
exact_match: bool
|
||||
total: int
|
||||
num_mismatches: int
|
||||
first_mismatch_index: Optional[int]
|
||||
first_mismatch_expected: Optional[int]
|
||||
first_mismatch_actual: Optional[int]
|
||||
max_abs_diff: int
|
||||
|
||||
def summary(self) -> str:
|
||||
if self.exact_match:
|
||||
return f"EXACT MATCH: {self.total}/{self.total} outputs bit-exact, 0 mismatches"
|
||||
return (
|
||||
f"MISMATCH: {self.num_mismatches}/{self.total} outputs differ "
|
||||
f"(first at index {self.first_mismatch_index}: "
|
||||
f"expected={self.first_mismatch_expected} actual={self.first_mismatch_actual}, "
|
||||
f"max_abs_diff={self.max_abs_diff})"
|
||||
)
|
||||
|
||||
|
||||
def compare_results(expected: List[int], actual: List[int]) -> ComparisonReport:
|
||||
if len(expected) != len(actual):
|
||||
raise ValueError(
|
||||
f"length mismatch: expected has {len(expected)} outputs, "
|
||||
f"actual has {len(actual)} -- cannot compare index-by-index"
|
||||
)
|
||||
|
||||
total = len(expected)
|
||||
num_mismatches = 0
|
||||
first_idx = None
|
||||
first_exp = None
|
||||
first_act = None
|
||||
max_abs_diff = 0
|
||||
|
||||
for i, (e, a) in enumerate(zip(expected, actual)):
|
||||
if e != a:
|
||||
num_mismatches += 1
|
||||
if first_idx is None:
|
||||
first_idx, first_exp, first_act = i, e, a
|
||||
max_abs_diff = max(max_abs_diff, abs(e - a))
|
||||
|
||||
return ComparisonReport(
|
||||
exact_match=(num_mismatches == 0),
|
||||
total=total,
|
||||
num_mismatches=num_mismatches,
|
||||
first_mismatch_index=first_idx,
|
||||
first_mismatch_expected=first_exp,
|
||||
first_mismatch_actual=first_act,
|
||||
max_abs_diff=max_abs_diff,
|
||||
)
|
||||
|
||||
|
||||
def load_fpga_results(path: str) -> List[int]:
|
||||
"""Loads FPGA-generated results from either a JSON list of ints, or
|
||||
a plain text file with one (whitespace-separated) integer per
|
||||
line -- a common shape for an RTL testbench's own $display/
|
||||
$writememh-style dump."""
|
||||
with open(path) as f:
|
||||
text = f.read()
|
||||
try:
|
||||
data = json.loads(text)
|
||||
if isinstance(data, dict) and "results" in data:
|
||||
data = data["results"]
|
||||
return [int(v) for v in data]
|
||||
except json.JSONDecodeError:
|
||||
return [int(tok) for tok in text.split()]
|
||||
Reference in New Issue
Block a user