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
83 lines
2.7 KiB
Python
83 lines
2.7 KiB
Python
"""
|
|
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()]
|