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
neural_sim — FPGA-Neural V2 golden functional reference
Python simulator = golden functional reference
RTL / P&R = hardware implementation
This package is not a cycle-accurate FPGA simulator. It does not
model clock cycles, SDRAM controller timing, or SPI transaction
timing. Given weights, activations, and a network topology, it
computes the mathematically correct result that the real V2 hardware
(hardware/v2/rtl/neural_processor.v) must reproduce bit-for-bit. Its
job is to be the reference everything else — RTL simulation, and
eventually real hardware — is checked against.
1. Numeric model
INT8 in / INT8 weight / INT32 accumulate / INT8 out, matching
neural_processor.v exactly (re-derived by reading that file, not
assumed — see numerics.py's own module docstring for the full,
line-by-line derivation):
| Stage | Width | Overflow behaviour |
|---|---|---|
| Multiply (INT8 × INT8) | 16-bit signed product | Cannot overflow for INT8 operands |
| Per-tile adder tree (P_IN=8 products) | 32-bit signed | Cannot overflow for P_IN=8 |
| Cross-tile accumulator | 32-bit signed | Wraparound (true two's-complement, matches a Verilog reg signed [31:0]'s silent overflow) |
| Bias add | 32-bit signed | Wraparound |
| Final activation/output | 8-bit signed | Saturating (the only saturating stage) |
numerics.wrap_acc() implements genuine 32-bit wraparound (not
Python's arbitrary-precision integers hiding the boundary) —
tests/test_numerics.py::test_wrap_acc_true_32bit_wraparound proves
2**31 wraps to -(2**31), exactly like the RTL.
This package reuses tools/validation/mac_oracle.py's own
independently-derived two's-complement primitives (to_signed,
mac8_tree) rather than duplicating them — that file already is this
project's own hand-verified oracle for the identical wraparound-add
semantics used by rtl/mac_unit.v/rtl/mac8.v, which
neural_processor.v's own header states is the SAME accumulation
lineage.
2. Neuron equation
y = activate(bias + sum(x[i] * w[i] for i in 0..N_INPUTS-1))
computed tile-by-tile in groups of P_IN (default 8, matching the
frozen hardware reference), with the cross-tile accumulator carrying
state (with wraparound) between tiles — exactly how
neural_processor.v's own pipeline works (one tile enters per cycle,
acc_reg only clears at job load, activation is applied once at the
end after tile_last).
Two independent implementations are provided and cross-tested
(tests/test_neuron.py):
neuron.neuron_scalar— plain Python, easiest to audit line-by-line against the RTL.neuron.neuron_vectorized— NumPy-based (int64 intra-tile dot products, since that stage can never overflow; explicit Python wraparound arithmetic across tiles, so NumPy's own dtype-wraparound behaviour is never silently relied on).
3. Layer equation
output[n] = activate(bias[n] + sum(input[i] * weight[n][i] for i in 0..N_INPUTS-1))
for n in 0..N_NEURONS-1 — layer.FCLayer, matching how a real job
batch is submitted (one WRITE_JOB per neuron, same x_base
activation tile, each with its own w_base/result_addr).
4. Network model (deliberately small scope)
network.Network is an ordered chain of FCLayers. This is a real,
deliberate scope limit, not a hardware limit: the real
dependency_manager.v schedules an arbitrary DAG of neuron jobs via
producer_ids/required fields, not just linear layer chains. A
linear chain is what this first phase implements and verifies; see
"Optional future extension" below for what's deferred.
5. Memory model
memory.MemoryModel reproduces the real, official V2 unified-SDRAM
memory map's logical byte contents and addresses (weights@0x010000,
activations@0x200000, results@0x300000, all within the single 8MB
0x000000–0x7FFFFF space) — not SDRAM cycle timing (see
hardware/v2/nms/rtl/sdram_controller.v / sdram_model.v for that).
write_weight/read_weight/write_activation/read_activation/
write_result/read_result are bounds-checked against each region's
own real base address.
6. Quantization / activation behaviour
Exactly two activation encodings exist in neural_processor.v:
relu(the RTL's owndefaultcase — i.e. any activation code other than exactlyACT_NONEalso produces ReLU):acc <= 0 -> 0;0 < acc <= 127 -> accexactly;acc > 127 -> 127. This is the only activation the real, currently-exposed V2 SPI job protocol applies (spi_host_bridge.v'sWRITE_JOBopcode has no activation-selection field) — matches the V2 LaTeX datasheet's own "Activation: ReLU + INT8 saturate, fixed".none: a two-sided saturating clamp to the full signed INT8 range[-128, 127]— implemented in the module and modeled here for completeness, but not reachable via the real, currently-exposed protocol.
No other quantization/scaling/shift stage exists in the real RTL, and none is invented here.
7. FPGA correspondence
| This simulator | Real hardware |
|---|---|
numerics.py |
hardware/v2/rtl/neural_processor.v (bit-exact, line-referenced) |
memory.py |
hardware/v2/nms/rtl/sdram_controller.v's logical address space (not its timing) |
network.py (linear chains only) |
hardware/v2/rtl/dependency_manager.v (arbitrary DAG — a superset, not yet modeled here) |
| — (not modeled) | spi_host_bridge.v transaction timing, SDRAM refresh/burst timing, tile-scheduling latency |
8. CLI usage
python -m tools.neural_sim random-network --n-inputs 8 --n-neurons 4 --seed 1
python -m tools.neural_sim run --example 8to4 --seed 7
python -m tools.neural_sim vectors --gen d_stress --out vec.json
python -m tools.neural_sim vectors --all --out all_vectors.json
python -m tools.neural_sim compare --expected vec.json --actual fpga_results.json
Built-in examples (--example): 8to1, 8to4, 8to16, 8_8_1 (an
8→8→1 two-layer network — the hidden width is 8, not some other
number, specifically because every layer boundary must stay a
multiple of P_IN=8, the same tiling constraint the real hardware
has).
9. Test-vector generation and export
vectors.py provides six deterministic generators (gen_simple_positive,
gen_signed_mix, gen_extremes, gen_zero, gen_random, gen_dstress
— the last reproducing the existing RTL benchmark's own 256-neuron ×
128-input dimensions). Every vector's expected field is computed by
this package's own golden model, not hand-typed, and stores inputs,
weights, biases, activation, p_in, numeric_format, and seed
(when applicable) — everything a future RTL testbench needs to load a
vector and check its own result, without re-deriving anything by hand:
Python (vectors.py) -> JSON golden vectors -> (future) Verilog testbench -> FPGA
10. FPGA-result comparison
compare.compare_results(expected, actual) returns a ComparisonReport
with exact_match, num_mismatches, first_mismatch_index,
first_mismatch_expected/_actual, and max_abs_diff. 0 mismatches
is the only passing criterion — this package never hides a real
numeric difference behind a tolerance. compare.load_fpga_results(path)
accepts either a JSON list of ints or a plain whitespace/line-separated
results file (a common shape for an RTL testbench's own dump).
Running the tests
python3 -m pytest tools/neural_sim/tests/ -q
96 tests, all passing as of this writing: signed-arithmetic edge cases (including a direct 32-bit wraparound check), neuron scalar-vs-vectorized cross-checks (parametrized across tile counts, activations, and seeds), layer tests, memory bounds/adjacency tests, golden-vector determinism and JSON round-trip tests, and comparison- utility tests.
Optional future extension (not implemented — by design)
This first phase deliberately stops at "the mathematical golden model
is unquestionably correct." A future phase could add, without
changing anything above: a cycle-accurate scheduler model,
N_PROCESSORS ∈ {1,2,4} scheduling, tile scheduling, SDRAM traffic
estimation, SPI transaction modeling, and latency prediction. None of
that exists yet, and this package does not claim to be
"cycle-accurate" anywhere.