Files
micheleandClaude Sonnet 5 9b5d1055b8 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
2026-09-06 19:51:33 +02:00

66 lines
2.8 KiB
Python

"""
Neuron model: y = activation(bias + sum(x[i]*w[i] for i in 0..N_INPUTS-1)),
matching neural_processor.v (P_IN=8 baseline, parameterizable). Two
independent implementations are provided -- a plain-Python scalar
reference (numerics.neuron_reference) and a NumPy-vectorized one below
-- and tests/test_neuron.py proves they always agree, bit-exact,
across every test-vector category.
"""
from __future__ import annotations
import numpy as np
from .numerics import (
DEFAULT_ACC_WIDTH, DEFAULT_DATA_WIDTH, DEFAULT_P_IN,
ACT_RELU, accumulate_tile, add_bias, activate_and_saturate,
check_int8, neuron_reference, wrap_acc,
)
def neuron_scalar(inputs, weights, bias: int = 0, activation: str = ACT_RELU,
p_in: int = DEFAULT_P_IN) -> int:
"""Plain-Python scalar reference -- a thin, explicit re-export of
numerics.neuron_reference (kept as its own name so neuron.py has a
clearly-named "scalar" counterpart to neuron_vectorized below)."""
return neuron_reference(inputs, weights, bias=bias, activation=activation, p_in=p_in)
def neuron_vectorized(inputs, weights, bias: int = 0, activation: str = ACT_RELU,
p_in: int = DEFAULT_P_IN,
acc_width: int = DEFAULT_ACC_WIDTH,
data_width: int = DEFAULT_DATA_WIDTH) -> int:
"""
NumPy-vectorized equivalent of neuron_scalar. Uses int64 numpy
accumulation WITHIN one tile only (exact -- a single P_IN=8 tile's
product-sum can never exceed a few hundred thousand in magnitude,
nowhere near int64's range, so no numpy overflow risk there), then
applies the SAME explicit 32-bit-wraparound Python arithmetic as the
scalar path across tiles -- deliberately NOT letting NumPy's own
int32 dtype silently wrap with platform-dependent behaviour, per
this project's own "do not allow implicit integer promotion to hide
overflow" requirement.
"""
x = np.asarray(inputs, dtype=np.int64)
w = np.asarray(weights, dtype=np.int64)
if x.shape != w.shape:
raise ValueError("inputs and weights must have the same shape")
if x.size == 0 or x.size % p_in != 0:
raise ValueError(f"len(inputs)={x.size} must be a nonzero multiple of p_in={p_in}")
for v in x.tolist():
check_int8(v, "input")
for v in w.tolist():
check_int8(v, "weight")
n_tiles = x.size // p_in
x_tiles = x.reshape(n_tiles, p_in)
w_tiles = w.reshape(n_tiles, p_in)
# exact per-tile dot products (int64, no overflow risk)
tile_sums = np.einsum("ij,ij->i", x_tiles, w_tiles)
acc = 0
for tile_sum in tile_sums.tolist():
acc = accumulate_tile(acc, int(tile_sum), acc_width=acc_width)
final_acc = add_bias(acc, bias, acc_width=acc_width, data_width=data_width)
return activate_and_saturate(final_acc, activation=activation, data_width=data_width)