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
66 lines
2.8 KiB
Python
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)
|