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
117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
import pytest
|
|
|
|
from tools.neural_sim.numerics import (
|
|
check_int8, wrap_acc, tile_product_sum, accumulate_tile, add_bias,
|
|
activate_and_saturate, neuron_reference, ACT_NONE, ACT_RELU,
|
|
)
|
|
|
|
|
|
def test_check_int8_accepts_range():
|
|
assert check_int8(-128) == -128
|
|
assert check_int8(127) == 127
|
|
assert check_int8(0) == 0
|
|
|
|
|
|
def test_check_int8_rejects_out_of_range():
|
|
with pytest.raises(ValueError):
|
|
check_int8(128)
|
|
with pytest.raises(ValueError):
|
|
check_int8(-129)
|
|
|
|
|
|
def test_wrap_acc_no_overflow_is_identity():
|
|
assert wrap_acc(1000, acc_width=32) == 1000
|
|
assert wrap_acc(-1000, acc_width=32) == -1000
|
|
|
|
|
|
def test_wrap_acc_true_32bit_wraparound():
|
|
# 2**31 is one past the max positive signed 32-bit value (2**31 - 1)
|
|
# -- must wrap to the most-negative value, exactly like a Verilog
|
|
# `reg signed [31:0]` silently overflowing.
|
|
assert wrap_acc(2**31, acc_width=32) == -(2**31)
|
|
assert wrap_acc(2**31 - 1, acc_width=32) == 2**31 - 1 # exact boundary, no wrap
|
|
assert wrap_acc(-(2**31) - 1, acc_width=32) == 2**31 - 1
|
|
|
|
|
|
def test_tile_product_sum_exact_known_values():
|
|
# 1*1 + 2*1 + ... + 8*1 = 36
|
|
assert tile_product_sum(list(range(1, 9)), [1] * 8) == 36
|
|
|
|
|
|
def test_tile_product_sum_extreme_product():
|
|
# -128 * -128 = 16384, the one INT8xINT8 case that does not fit
|
|
# symmetrically in magnitude terms
|
|
assert tile_product_sum([-128], [-128], acc_width=32) == 16384
|
|
|
|
|
|
def test_tile_product_sum_rejects_non_power_of_two():
|
|
with pytest.raises(ValueError):
|
|
tile_product_sum([1, 2, 3], [1, 1, 1])
|
|
|
|
|
|
def test_tile_product_sum_rejects_out_of_range_input():
|
|
with pytest.raises(ValueError):
|
|
tile_product_sum([200], [1])
|
|
|
|
|
|
def test_accumulate_tile_matches_wrap_acc():
|
|
assert accumulate_tile(10, 20) == 30
|
|
assert accumulate_tile(2**31 - 1, 1) == -(2**31)
|
|
|
|
|
|
def test_add_bias_wraparound():
|
|
assert add_bias(100, 27) == 127
|
|
assert add_bias(2**31 - 1, 127) == wrap_acc(2**31 - 1 + 127)
|
|
|
|
|
|
def test_activate_relu_zeroes_non_positive():
|
|
assert activate_and_saturate(0, activation=ACT_RELU) == 0
|
|
assert activate_and_saturate(-1, activation=ACT_RELU) == 0
|
|
assert activate_and_saturate(-1000000, activation=ACT_RELU) == 0
|
|
|
|
|
|
def test_activate_relu_passes_in_range():
|
|
assert activate_and_saturate(1, activation=ACT_RELU) == 1
|
|
assert activate_and_saturate(127, activation=ACT_RELU) == 127
|
|
|
|
|
|
def test_activate_relu_saturates_positive():
|
|
assert activate_and_saturate(128, activation=ACT_RELU) == 127
|
|
assert activate_and_saturate(1000000, activation=ACT_RELU) == 127
|
|
|
|
|
|
def test_activate_none_passes_full_signed_range():
|
|
assert activate_and_saturate(-128, activation=ACT_NONE) == -128
|
|
assert activate_and_saturate(127, activation=ACT_NONE) == 127
|
|
assert activate_and_saturate(0, activation=ACT_NONE) == 0
|
|
|
|
|
|
def test_activate_none_saturates_both_sides():
|
|
assert activate_and_saturate(128, activation=ACT_NONE) == 127
|
|
assert activate_and_saturate(-129, activation=ACT_NONE) == -128
|
|
assert activate_and_saturate(1000000, activation=ACT_NONE) == 127
|
|
assert activate_and_saturate(-1000000, activation=ACT_NONE) == -128
|
|
|
|
|
|
def test_neuron_reference_simple_positive():
|
|
y = neuron_reference(list(range(1, 9)), [1] * 8, activation=ACT_RELU)
|
|
assert y == 36
|
|
|
|
|
|
def test_neuron_reference_multi_tile_accumulates_across_tiles():
|
|
# two tiles of 8, same weights -- accumulator must carry across tiles
|
|
inputs = [1] * 8 + [1] * 8
|
|
weights = [1] * 8 + [1] * 8
|
|
assert neuron_reference(inputs, weights, activation=ACT_RELU) == 16
|
|
|
|
|
|
def test_neuron_reference_rejects_length_not_multiple_of_p_in():
|
|
with pytest.raises(ValueError):
|
|
neuron_reference([1] * 5, [1] * 5)
|
|
|
|
|
|
def test_neuron_reference_bias_default_zero_matches_no_bias():
|
|
y_default = neuron_reference([1] * 8, [1] * 8)
|
|
y_explicit = neuron_reference([1] * 8, [1] * 8, bias=0)
|
|
assert y_default == y_explicit
|