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
74 lines
3.0 KiB
Python
74 lines
3.0 KiB
Python
"""
|
|
Fully-connected layer: N_INPUTS -> N_NEURONS, each neuron with its own
|
|
weight vector (and, optionally, its own bias), all sharing one
|
|
activation setting -- matching how a real V2 job batch is submitted
|
|
(one WRITE_JOB per neuron, all against the same x_base activation
|
|
tile, each with its own w_base/result_addr).
|
|
|
|
output[n] = activation(bias[n] + sum(input[i]*weight[n][i] for i in
|
|
0..N_INPUTS-1)), computed with the EXACT same tile-wise, wraparound-
|
|
accumulate-then-saturate semantics as numerics.neuron_reference.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from .numerics import ACT_RELU, DEFAULT_P_IN, check_int8
|
|
from .neuron import neuron_vectorized
|
|
|
|
|
|
class FCLayer:
|
|
"""
|
|
weights: array-like, shape (n_neurons, n_inputs), signed INT8.
|
|
biases: array-like, shape (n_neurons,), signed INT8 (default: all
|
|
zero, matching the real V2 protocol's own lack of a bias
|
|
field -- see numerics.py's own module docstring).
|
|
activation: 'relu' (default, matches the real, currently-exposed
|
|
V2 system) or 'none'.
|
|
p_in: tile width (default 8, matches the frozen P_IN=8 reference).
|
|
"""
|
|
|
|
def __init__(self, weights, biases=None, activation: str = ACT_RELU,
|
|
p_in: int = DEFAULT_P_IN):
|
|
self.weights = np.asarray(weights, dtype=np.int64)
|
|
if self.weights.ndim != 2:
|
|
raise ValueError("weights must be 2D: (n_neurons, n_inputs)")
|
|
self.n_neurons, self.n_inputs = self.weights.shape
|
|
if self.n_inputs % p_in != 0:
|
|
raise ValueError(f"n_inputs={self.n_inputs} must be a multiple of p_in={p_in}")
|
|
for row in self.weights.tolist():
|
|
for w in row:
|
|
check_int8(w, "weight")
|
|
|
|
if biases is None:
|
|
self.biases = np.zeros(self.n_neurons, dtype=np.int64)
|
|
else:
|
|
self.biases = np.asarray(biases, dtype=np.int64)
|
|
if self.biases.shape != (self.n_neurons,):
|
|
raise ValueError("biases must have shape (n_neurons,)")
|
|
for b in self.biases.tolist():
|
|
check_int8(b, "bias")
|
|
|
|
self.activation = activation
|
|
self.p_in = p_in
|
|
|
|
def forward(self, inputs) -> np.ndarray:
|
|
"""inputs: array-like, shape (n_inputs,), signed INT8. Returns
|
|
an int64 numpy array of shape (n_neurons,) -- each element is
|
|
an exact signed INT8 value (kept as int64 purely for easy
|
|
downstream composition; every value is guaranteed in
|
|
[-128, 127])."""
|
|
inputs = np.asarray(inputs, dtype=np.int64)
|
|
if inputs.shape != (self.n_inputs,):
|
|
raise ValueError(f"inputs must have shape ({self.n_inputs},), got {inputs.shape}")
|
|
for v in inputs.tolist():
|
|
check_int8(v, "input")
|
|
|
|
outputs = np.empty(self.n_neurons, dtype=np.int64)
|
|
for n in range(self.n_neurons):
|
|
outputs[n] = neuron_vectorized(
|
|
inputs, self.weights[n], bias=int(self.biases[n]),
|
|
activation=self.activation, p_in=self.p_in,
|
|
)
|
|
return outputs
|