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
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
CLI for the neural_sim golden simulator.
|
||||
|
||||
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 golden.json --actual fpga_results.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .layer import FCLayer
|
||||
from .network import Network
|
||||
from . import examples as ex
|
||||
from . import vectors as vec
|
||||
from .compare import compare_results, load_fpga_results
|
||||
|
||||
|
||||
def cmd_random_network(args):
|
||||
rng = np.random.default_rng(args.seed)
|
||||
weights = rng.integers(-args.magnitude, args.magnitude + 1,
|
||||
size=(args.n_neurons, args.n_inputs), dtype=np.int64)
|
||||
layer = FCLayer(weights, activation=args.activation)
|
||||
inputs = rng.integers(-128, 128, size=args.n_inputs, dtype=np.int64)
|
||||
outputs = layer.forward(inputs)
|
||||
print(f"n_inputs={layer.n_inputs} n_neurons={layer.n_neurons} activation={layer.activation}")
|
||||
print(f"inputs: {inputs.tolist()}")
|
||||
print(f"outputs: {outputs.tolist()}")
|
||||
if args.save_weights:
|
||||
with open(args.save_weights, "w") as f:
|
||||
json.dump({"weights": weights.tolist(), "inputs": inputs.tolist(),
|
||||
"outputs": outputs.tolist()}, f, indent=2)
|
||||
print(f"saved to {args.save_weights}")
|
||||
|
||||
|
||||
def cmd_run(args):
|
||||
if args.example not in ex.ALL_EXAMPLES:
|
||||
print(f"unknown example {args.example!r}; choices: {list(ex.ALL_EXAMPLES)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
net = ex.ALL_EXAMPLES[args.example]()
|
||||
rng = np.random.default_rng(args.seed)
|
||||
inputs = rng.integers(-128, 128, size=net.n_inputs, dtype=np.int64)
|
||||
outputs = net.forward(inputs)
|
||||
print(f"example={args.example} n_inputs={net.n_inputs} n_outputs={net.n_outputs}")
|
||||
print(f"inputs: {inputs.tolist()}")
|
||||
print(f"outputs: {outputs.tolist()}")
|
||||
|
||||
|
||||
def cmd_vectors(args):
|
||||
if args.all:
|
||||
vec.export_all_json(args.out)
|
||||
print(f"exported all {len(vec.ALL_GENERATORS)} named vectors to {args.out}")
|
||||
return
|
||||
if args.gen not in vec.ALL_GENERATORS:
|
||||
print(f"unknown generator {args.gen!r}; choices: {list(vec.ALL_GENERATORS)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
v = vec.ALL_GENERATORS[args.gen]()
|
||||
vec.export_json(v, args.out)
|
||||
print(f"generated {v.name!r}: n_inputs={v.n_inputs} n_neurons={v.n_neurons} -> {args.out}")
|
||||
|
||||
|
||||
def cmd_compare(args):
|
||||
expected = vec.load_json(args.expected).expected if args.expected.endswith(".json") and _is_vector_file(args.expected) \
|
||||
else load_fpga_results(args.expected)
|
||||
actual = load_fpga_results(args.actual)
|
||||
report = compare_results(expected, actual)
|
||||
print(report.summary())
|
||||
sys.exit(0 if report.exact_match else 1)
|
||||
|
||||
|
||||
def _is_vector_file(path: str) -> bool:
|
||||
try:
|
||||
with open(path) as f:
|
||||
d = json.load(f)
|
||||
return isinstance(d, dict) and "expected" in d and "weights" in d
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="python -m tools.neural_sim",
|
||||
description="FPGA-Neural V2 golden functional reference simulator")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_rand = sub.add_parser("random-network", help="generate and run a random network")
|
||||
p_rand.add_argument("--n-inputs", type=int, default=8)
|
||||
p_rand.add_argument("--n-neurons", type=int, default=4)
|
||||
p_rand.add_argument("--seed", type=int, default=0)
|
||||
p_rand.add_argument("--magnitude", type=int, default=20, help="max abs weight magnitude")
|
||||
p_rand.add_argument("--activation", choices=["relu", "none"], default="relu")
|
||||
p_rand.add_argument("--save-weights", metavar="PATH", default=None)
|
||||
p_rand.set_defaults(func=cmd_random_network)
|
||||
|
||||
p_run = sub.add_parser("run", help="run one of the built-in example networks")
|
||||
p_run.add_argument("--example", choices=list(ex.ALL_EXAMPLES), default="8to4")
|
||||
p_run.add_argument("--seed", type=int, default=0, help="seed for the random input vector")
|
||||
p_run.set_defaults(func=cmd_run)
|
||||
|
||||
p_vec = sub.add_parser("vectors", help="generate golden test vectors")
|
||||
p_vec.add_argument("--gen", choices=list(vec.ALL_GENERATORS), default=None)
|
||||
p_vec.add_argument("--all", action="store_true", help="export every named generator")
|
||||
p_vec.add_argument("--out", required=True, metavar="PATH")
|
||||
p_vec.set_defaults(func=cmd_vectors)
|
||||
|
||||
p_cmp = sub.add_parser("compare", help="compare FPGA results against Python golden results")
|
||||
p_cmp.add_argument("--expected", required=True, metavar="PATH",
|
||||
help="a vectors-format JSON file (uses its 'expected' field) or a plain results file")
|
||||
p_cmp.add_argument("--actual", required=True, metavar="PATH", help="FPGA-generated results file")
|
||||
p_cmp.set_defaults(func=cmd_compare)
|
||||
|
||||
args = parser.parse_args()
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user