Files
FPGA-Neural/tools/neural_sim/vectors.py
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

145 lines
5.0 KiB
Python

"""
Deterministic FPGA-style test vectors + golden-vector JSON export
(B8/B9). Every generator below returns a TestVector whose `expected`
field is computed by this SAME package's own golden model
(layer.FCLayer / numerics.neuron_reference) -- i.e. the vector is
self-consistent and re-verifiable by construction (see
tests/test_vectors.py's own round-trip test), not hand-typed.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field, asdict
from typing import List, Optional
import numpy as np
from .numerics import ACT_RELU, DEFAULT_P_IN
from .layer import FCLayer
@dataclass
class TestVector:
name: str
inputs: List[int]
weights: List[List[int]] # shape (n_neurons, n_inputs)
biases: List[int] # shape (n_neurons,)
activation: str
p_in: int
expected: List[int] # shape (n_neurons,)
numeric_format: str = "int8 in / int8 weight / int32 accumulate (wraparound) / int8 out (saturate)"
seed: Optional[int] = None
@property
def n_neurons(self) -> int:
return len(self.expected)
@property
def n_inputs(self) -> int:
return len(self.inputs)
def _make_vector(name: str, inputs, weights, biases=None, activation: str = ACT_RELU,
p_in: int = DEFAULT_P_IN, seed: Optional[int] = None) -> TestVector:
layer = FCLayer(weights, biases=biases, activation=activation, p_in=p_in)
expected = layer.forward(inputs).tolist()
biases_list = layer.biases.tolist()
return TestVector(
name=name, inputs=list(int(v) for v in inputs),
weights=[[int(w) for w in row] for row in layer.weights.tolist()],
biases=biases_list, activation=activation, p_in=p_in,
expected=[int(e) for e in expected], seed=seed,
)
# ---- Test 1: simple positive ----
def gen_simple_positive(p_in: int = DEFAULT_P_IN) -> TestVector:
"""Small positive inputs/weights, manually predictable:
inputs=[1..p_in], weights=all 1s -> sum = p_in*(p_in+1)/2."""
inputs = list(range(1, p_in + 1))
weights = [[1] * p_in]
return _make_vector("simple_positive", inputs, weights)
# ---- Test 2: signed values ----
def gen_signed_mix(p_in: int = DEFAULT_P_IN) -> TestVector:
"""Alternating positive/negative INT8 inputs and weights."""
inputs = [((-1) ** i) * (10 + i) for i in range(p_in)]
weights = [[((-1) ** (i + 1)) * (5 + i) for i in range(p_in)]]
return _make_vector("signed_mix", inputs, weights)
# ---- Test 3: extremes ----
def gen_extremes(p_in: int = DEFAULT_P_IN) -> TestVector:
"""-128/127 combinations designed to stress multiplication (the one
genuinely asymmetric INT8xINT8 case, -128*-128=16384, is
deliberately included) and accumulation across p_in terms."""
inputs = [(-128 if i % 2 == 0 else 127) for i in range(p_in)]
weights = [[(-128 if i % 2 == 1 else 127) for i in range(p_in)]]
return _make_vector("extremes", inputs, weights)
# ---- Test 4: zero ----
def gen_zero(p_in: int = DEFAULT_P_IN) -> TestVector:
inputs = [0] * p_in
weights = [[0] * p_in]
return _make_vector("zero", inputs, weights)
# ---- Test 5: random (fixed seed) ----
def gen_random(seed: int = 1234, n_inputs: int = 64, n_neurons: int = 4,
p_in: int = DEFAULT_P_IN) -> TestVector:
rng = np.random.default_rng(seed)
inputs = rng.integers(-128, 128, size=n_inputs, dtype=np.int64)
weights = rng.integers(-128, 128, size=(n_neurons, n_inputs), dtype=np.int64)
return _make_vector(f"random_seed{seed}", inputs, weights, p_in=p_in, seed=seed)
# ---- Test 6: D-Stress (256 neurons x 128 inputs, reproducing the
# existing RTL D-Stress benchmark's own dimensions) ----
def gen_dstress(seed: int = 42, n_neurons: int = 256, n_inputs: int = 128,
p_in: int = DEFAULT_P_IN) -> TestVector:
rng = np.random.default_rng(seed)
inputs = rng.integers(-128, 128, size=n_inputs, dtype=np.int64)
weights = rng.integers(-128, 128, size=(n_neurons, n_inputs), dtype=np.int64)
return _make_vector("d_stress", inputs, weights, p_in=p_in, seed=seed)
ALL_GENERATORS = {
"simple_positive": gen_simple_positive,
"signed_mix": gen_signed_mix,
"extremes": gen_extremes,
"zero": gen_zero,
"random": gen_random,
"d_stress": gen_dstress,
}
def generate_all() -> dict:
return {name: fn() for name, fn in ALL_GENERATORS.items()}
# ---- golden-vector export/import (machine-readable, JSON) ----
def to_dict(vector: TestVector) -> dict:
return asdict(vector)
def export_json(vector: TestVector, path: str) -> None:
with open(path, "w") as f:
json.dump(to_dict(vector), f, indent=2)
def load_json(path: str) -> TestVector:
with open(path) as f:
d = json.load(f)
return TestVector(**d)
def export_all_json(path: str) -> None:
"""Export every named generator's vector into one JSON file, keyed
by name -- convenient for a future RTL testbench harness to load
once and iterate."""
vectors = generate_all()
with open(path, "w") as f:
json.dump({name: to_dict(v) for name, v in vectors.items()}, f, indent=2)