Fixes all 7 bugs found in the FPGA-Neural re-certification campaign (docs/validation/bugs.md, CERTIFICATION.md), per campaign policy that fixes land as a commit separate from the analysis work (commits 313a199..77e74db): - BUG-005 (CRITICAL): layer_sequencer.v -- RUN_NETWORK(num_layers=0) ran through 256 fabricated layers reading arbitrary PSRAM data as descriptors. Now an immediate no-op. - BUG-007 (CRITICAL): spi_engine.v -- SET_NET_TYPE received mid-run remapped the arbiter mux and hung the in-progress engine. Now rejected while graph_busy/seq_busy, verified not to partially apply. - BUG-002 (MEDIA): neuron_parallel.v -- N_INPUTS=0 bypassed the elaboration-time guard, leaving x_bus/w_bus undriven. Guard extended to reject N_INPUTS==0. - BUG-003 (MEDIA): neuron_parallel.v -- n_inputs_real=0 at runtime had inconsistent behavior across repeated runs. Now an explicit early-out via the existing "finishing" completion path. - BUG-004 (BASSA): neuron_memory.v -- n_neurons_real=0 silently ignored the limit. Fixed at all three entry points into the vulnerable termination checks (STATE_READ_X, STATE_READ_W, and the X->W dispatch). - BUG-006 (BASSA): graph_engine.v -- num_neurons_graph=0 relied on an incidental guard rather than a real one. Now an explicit no-op. - BUG-001 (INFO): removed sim/top.v, confirmed dead code from the pre-INT8 Q8.8 era. Every bug-reproduction testbench is rewritten from observe-only to hard-assert the fixed behavior (sim/*_bug00[2-7]*_tb.v), verified individually and via a full regression (44 testbenches, 43 PASS, 0 FAIL/ERROR, 1 benchmark by design). Re-verified on the real toolchain (Yosys synth_ecp5 + nextpnr-ecp5): 0 constraint errors, Fmax 68.65 MHz (was 67.91 MHz, within known placement noise), critical path structurally unchanged (neuron_parallel/mac8 accumulator carry chain). Updates docs/validation/bugs.md and CERTIFICATION.md to reflect the resolved state, and docs/FPGA-NeuralNetwork-Engine.md + the LaTeX datasheet (IT/EN) with inline notes on each fixed edge case, closing the datasheet/RTL gap flagged in C.13 of the original certification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
176 lines
6.8 KiB
Python
176 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fase 0 (docs/validation/00-inventario.md) regression runner.
|
|
|
|
No such script existed in the repo before this -- every prior "N testbenches,
|
|
all pass" claim in WORKLOG.md was produced by manually assembling per-test
|
|
iverilog command lines, never re-run from a single reproducible harness. This
|
|
script builds the module dependency graph directly from the source (regex
|
|
over instantiation sites), not from memory/WORKLOG claims, then compiles and
|
|
runs every sim/*_tb.v fresh with `iverilog -g2012` + `vvp`.
|
|
|
|
Usage: python3 tools/run_regression.py [--keep] [pattern]
|
|
--keep keep the compiled .out files in /tmp/regression (default: cleaned)
|
|
pattern only run testbenches whose filename contains this substring
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# Testbenches that are DELIBERATELY meant to fail elaboration (they verify a
|
|
# compile-time guard by trying to violate it -- see each file's own header
|
|
# comment for the citation). A naive PASS/FAIL-marker scan misclassifies
|
|
# these as broken; confirmed by reading the source, not assumed.
|
|
EXPECTED_COMPILE_FAIL = {
|
|
"neuron_parallel_guard_negative_degenerate",
|
|
"neuron_parallel_guard_negative_nonmultiple",
|
|
"neuron_parallel_bug002_n_inputs_zero",
|
|
}
|
|
# Benchmarks: print measured numbers, no PASS/FAIL verdict by design (see
|
|
# each file's own header -- same category as sim/flash_latency_bench.v,
|
|
# which isn't even named *_tb.v and so isn't picked up by this glob at all,
|
|
# an inconsistent naming convention worth flagging in the inventory).
|
|
BENCHMARK_NO_VERDICT = {
|
|
"graph_engine_bandwidth",
|
|
}
|
|
|
|
# Every module-defining file under rtl/ and the sim/ behavioral models used
|
|
# as test doubles (flash_model.v, psram_model.v) -- sim/top.v is EXCLUDED
|
|
# deliberately: it's dead code (references a FRAC_BITS parameter that no
|
|
# longer exists on neuron_parallel.v, confirmed failing to elaborate on its
|
|
# own, see docs/validation/00-inventario.md).
|
|
SOURCE_FILES = sorted(
|
|
glob.glob(os.path.join(REPO, "rtl", "*.v"))
|
|
+ [os.path.join(REPO, "sim", "flash_model.v"), os.path.join(REPO, "sim", "psram_model.v")]
|
|
)
|
|
|
|
MODULE_RE = re.compile(r"^\s*module\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE)
|
|
# Matches "modname instname (" or "modname #(" instantiation sites, not
|
|
# "module modname" definitions and not plain calls/keywords.
|
|
INST_RE = re.compile(
|
|
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s+(?:#\s*\(|[A-Za-z_][A-Za-z0-9_]*\s*\()",
|
|
re.MULTILINE,
|
|
)
|
|
KEYWORDS = {
|
|
"if", "else", "for", "while", "case", "begin", "end", "assign", "wire",
|
|
"reg", "input", "output", "inout", "parameter", "localparam", "function",
|
|
"task", "always", "initial", "module", "endmodule", "generate",
|
|
"endgenerate", "genvar", "integer", "real", "signed", "unsigned",
|
|
}
|
|
|
|
def module_defined_in(path):
|
|
txt = open(path, encoding="utf-8", errors="replace").read()
|
|
return MODULE_RE.findall(txt)
|
|
|
|
def instantiated_modules(path):
|
|
txt = open(path, encoding="utf-8", errors="replace").read()
|
|
found = set(INST_RE.findall(txt)) - KEYWORDS
|
|
return found
|
|
|
|
def build_module_map():
|
|
m = {}
|
|
for f in SOURCE_FILES:
|
|
for name in module_defined_in(f):
|
|
m.setdefault(name, f)
|
|
return m
|
|
|
|
def resolve_deps(tb_path, module_map):
|
|
needed_files = {tb_path}
|
|
frontier = instantiated_modules(tb_path)
|
|
seen_files = set()
|
|
while frontier:
|
|
mod = frontier.pop()
|
|
if mod not in module_map:
|
|
continue
|
|
f = module_map[mod]
|
|
if f in needed_files:
|
|
continue
|
|
needed_files.add(f)
|
|
if f in seen_files:
|
|
continue
|
|
seen_files.add(f)
|
|
frontier |= instantiated_modules(f)
|
|
return sorted(needed_files)
|
|
|
|
def main():
|
|
keep = "--keep" in sys.argv
|
|
args = [a for a in sys.argv[1:] if a != "--keep"]
|
|
pattern = args[0] if args else ""
|
|
|
|
module_map = build_module_map()
|
|
tbs = sorted(glob.glob(os.path.join(REPO, "sim", "*_tb.v")))
|
|
tbs = [t for t in tbs if pattern in os.path.basename(t)]
|
|
|
|
results = []
|
|
workdir = tempfile.mkdtemp(prefix="regression_")
|
|
for tb in tbs:
|
|
name = os.path.basename(tb)[:-len("_tb.v")]
|
|
files = resolve_deps(tb, module_map)
|
|
out = os.path.join(workdir, name + ".out")
|
|
cc = subprocess.run(
|
|
["iverilog", "-g2012", "-o", out] + files,
|
|
cwd=REPO, capture_output=True, text=True,
|
|
)
|
|
if cc.returncode != 0:
|
|
if name in EXPECTED_COMPILE_FAIL:
|
|
results.append((name, "PASS (compile-time guard fired as designed)", cc.stderr.strip().splitlines()[-2:], files))
|
|
else:
|
|
results.append((name, "COMPILE_FAIL", cc.stderr.strip(), files))
|
|
continue
|
|
elif name in EXPECTED_COMPILE_FAIL:
|
|
results.append((name, "FAIL (was expected to NOT compile, but it compiled)", [], files))
|
|
continue
|
|
run = subprocess.run(["vvp", out], cwd=REPO, capture_output=True, text=True, timeout=120)
|
|
combined = run.stdout + run.stderr
|
|
low = combined.lower()
|
|
if name in BENCHMARK_NO_VERDICT:
|
|
status = "BENCHMARK (no pass/fail verdict by design)" if run.returncode == 0 else f"RUNTIME_ERROR(rc={run.returncode})"
|
|
elif "fail" in low and "PASSED" not in combined and "PASS" not in combined:
|
|
status = "FAIL"
|
|
elif run.returncode != 0:
|
|
status = f"RUNTIME_ERROR(rc={run.returncode})"
|
|
elif "PASSED" in combined or "PASS" in combined or "ALL TESTS" in combined:
|
|
status = "PASS"
|
|
else:
|
|
status = "UNKNOWN (no PASS/FAIL marker found)"
|
|
results.append((name, status, combined.strip().splitlines()[-3:], files))
|
|
|
|
print(f"{'TESTBENCH':45s} {'RESULT'}")
|
|
print("-" * 70)
|
|
n_pass = n_fail = n_other = 0
|
|
for name, status, tail, files in results:
|
|
print(f"{name:45s} {status}")
|
|
if status.startswith("PASS"):
|
|
n_pass += 1
|
|
elif status.startswith("BENCHMARK"):
|
|
n_other += 1
|
|
for line in (tail if isinstance(tail, list) else [tail]):
|
|
print(f" | {line}")
|
|
elif status.startswith("FAIL") or status.startswith("COMPILE_FAIL") or status.startswith("RUNTIME_ERROR"):
|
|
n_fail += 1
|
|
for line in (tail if isinstance(tail, list) else [tail]):
|
|
print(f" | {line}")
|
|
else:
|
|
n_other += 1
|
|
for line in (tail if isinstance(tail, list) else [tail]):
|
|
print(f" | {line}")
|
|
|
|
print("-" * 70)
|
|
print(f"TOTAL: {len(results)} PASS: {n_pass} FAIL/ERROR: {n_fail} OTHER/UNKNOWN: {n_other}")
|
|
|
|
if not keep:
|
|
import shutil
|
|
shutil.rmtree(workdir, ignore_errors=True)
|
|
else:
|
|
print(f"Compiled binaries kept in {workdir}")
|
|
|
|
sys.exit(1 if (n_fail or n_other) else 0)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|