fix: guard neuron_parallel against invalid N_INPUTS/PARALLEL combos

Both Phase 2 findings (docs/FPGA-NeuralNetwork-Engine.md) shared one
root cause: GROUPS = N_INPUTS / PARALLEL is integer division. When
N_INPUTS is not an exact multiple of PARALLEL, the remainder inputs
were silently dropped from the accumulation (wrong result, no
error); when PARALLEL > N_INPUTS, GROUPS = 0 and the controller's
terminal condition was never met, hanging the neuron forever.

Added a single elaboration-time guard to rtl/neuron_parallel.v: a
`generate` block instantiates a deliberately undefined module when
N_INPUTS % PARALLEL != 0, forcing a hard failure in both simulation
and synthesis instead of a silent wrong answer or a deadlock. Valid
configurations are unaffected (the branch is never elaborated). The
validated datapath (mac8/mac_unit/accumulation/ReLU/saturation) is
untouched -- this is authorized as a scoped exception to the
"core is fixed, do not touch" project policy, for this guard only.

- sim/neuron_parallel_guard_negative_nonmultiple_tb.v and
  sim/neuron_parallel_guard_negative_degenerate_tb.v: negative tests
  that must fail to elaborate; verified both fail with the expected
  "Unknown module type" error.
- sim/parameter_sweep_tb.v: rewritten to valid-configs-only (the
  three configs that used to demonstrate truncation/hang no longer
  compile, by design); added PARALLEL=2 and PARALLEL=4 configs,
  the two best-performing values from
  docs/FPGA-Neural-Datapatch-Benchmark.md.
- Full regression re-run after the RTL change: all existing
  testbenches still pass unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
This commit is contained in:
2026-09-02 14:41:49 +02:00
co-authored by Claude Sonnet 5
parent 9b9859a104
commit 1a6f0ba2ef
14 changed files with 45001 additions and 62768 deletions
+40
View File
@@ -70,3 +70,43 @@
- 2026-09-02T01:15 — [FASE 8] — Rigenerato artefatto tracciato sim/parameter_sweep_sim + sim/parameter_sweep.vcd per coerenza con la convenzione del repo. - 2026-09-02T01:15 — [FASE 8] — Rigenerato artefatto tracciato sim/parameter_sweep_sim + sim/parameter_sweep.vcd per coerenza con la convenzione del repo.
- 2026-09-02T01:16 — [FASE 8] — Nessuna modifica a rtl/neuron_parallel.v in questa fase (core di calcolo non toccato, come da policy di progetto). I due limiti trovati (troncamento su non-multiplo, hang su PARALLEL>N_INPUTS) sono documentati come "Findings" nella Fase 2 della roadmap (docs/FPGA-NeuralNetwork-Engine.md) e proposti come azione futura per Fase 3/7, non risolti ora. - 2026-09-02T01:16 — [FASE 8] — Nessuna modifica a rtl/neuron_parallel.v in questa fase (core di calcolo non toccato, come da policy di progetto). I due limiti trovati (troncamento su non-multiplo, hang su PARALLEL>N_INPUTS) sono documentati come "Findings" nella Fase 2 della roadmap (docs/FPGA-NeuralNetwork-Engine.md) e proposti come azione futura per Fase 3/7, non risolti ora.
- 2026-09-02T01:18 — [FASE 8] — Aggiornato docs/FPGA-NeuralNetwork-Engine.md, sezione "## Phase 2 — Parameter Sweep": aggiunta checklist di completamento e sezione "Findings" con i due limiti documentati sopra. - 2026-09-02T01:18 — [FASE 8] — Aggiornato docs/FPGA-NeuralNetwork-Engine.md, sezione "## Phase 2 — Parameter Sweep": aggiunta checklist di completamento e sezione "Findings" con i due limiti documentati sopra.
## Fase: Fix dei findings di Fase 2 (guardia elaboration-time) (2026-09-02)
- 2026-09-02T02:00 — [FASE 9] — Utente chiede se i due finding di Fase 2 (troncamento su N_INPUTS non multiplo di PARALLEL; hang su PARALLEL>N_INPUTS) siano errori da correggere o si possano lasciare, segnalando che dai benchmark PARALLEL=2 dà i risultati migliori (PARALLEL=4 leggermente meno) — vedi docs/FPGA-Neural-Datapatch-Benchmark.md.
- 2026-09-02T02:02 — [FASE 9] — Letto docs/FPGA-Neural-Datapatch-Benchmark.md, tabella comparativa (§13, N_INPUTS=256, N_NEURONS=4): PARALLEL=16 FAIL (52.13 MHz), PARALLEL=8 FAIL (61.71 MHz), PARALLEL=4 FAIL (75.01 MHz), PARALLEL=2 PASS (87.88 MHz, unico sopra 80 MHz). Confermato che N_INPUTS e PARALLEL sono parametri Verilog fissati in fase di sintesi (non registri configurabili via SPI a runtime): il rischio dei due finding è quindi "errore di chi istanzia il modulo per un certo layer", non un rischio da campo/runtime.
- 2026-09-02T02:05 — [FASE 9] — Analisi di rischio comunicata all'utente: Finding 2 (hang, PARALLEL>N_INPUTS) trascurabile con PARALLEL reale=2/4 (richiederebbe un layer con 1-3 input, non realistico). Finding 1 (troncamento silenzioso) più concreto: con PARALLEL=2 basta un N_INPUTS dispari per perdere silenziosamente l'ultimo input, senza segnalazione — proposta una guardia a elaboration-time (nessuna modifica al datapath validato).
- 2026-09-02T02:10 — [FASE 9] — Proposta dettagliata verificata isolatamente PRIMA di toccare l'RTL: creato in scratchpad un modulo di test con blocco `generate` che istanzia un modulo indefinito `neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL` quando `N_INPUTS % PARALLEL != 0`. Compilato con Icarus (`iverilog -g2012`) sia con parametri validi (32/8, exit=0, nessun errore) sia invalidi (30/8, exit=1, errore "Unknown module type: neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL"). Idioma confermato portabile (standard Verilog generate + elaborazione modulo, non system task da simulazione tipo $error/$fatal che i tool di sintesi spesso ignorano).
- 2026-09-02T02:15 — [FASE 9] — Presentata la proposta all'utente (unica modifica prevista: blocco `generate` in rtl/neuron_parallel.v, nessun'altra riga toccata) e chiesta conferma esplicita prima di modificare il core "validato/non riscrivere" secondo la policy di progetto stabilita a inizio sessione.
- 2026-09-02T02:20 — [FASE 9] — Utente autorizza ESPLICITAMENTE e SOLO per questa modifica a toccare ciò che era dichiarato fisso ("core is correct, don't touch/rewrite"), con richiesta di annotazione dettagliata nel log (questa sezione).
- 2026-09-02T02:22 — [FASE 9] — MODIFICA A rtl/neuron_parallel.v: inserito blocco `generate` subito prima di `localparam GROUPS = N_INPUTS / PARALLEL;`:
```verilog
generate
if (N_INPUTS % PARALLEL != 0) begin : PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL invalid_parameter_combination();
end
endgenerate
```
Nessun'altra riga del file toccata. Il datapath di calcolo (mac8, accumulo, bias, ReLU, saturazione) è invariato. Per configurazioni valide (N_INPUTS%PARALLEL==0) il ramo generate non viene mai elaborato: zero impatto.
- 2026-09-02T02:25 — [FASE 9] — REGRESSIONE COMPLETA post-modifica, per verificare che nessuna configurazione valida esistente sia stata rotta. Compilato ed eseguito con `iverilog -g2012 -o <tmp> rtl/*.v [sim/psram_model.v] sim/<tb>.v` + `vvp`:
- int8_memory_access_tb: PASS (non usa neuron_parallel, invariato)
- layer_tb: PASS - ALL 8 NEURONS (N_INPUTS=32 PARALLEL=8, esatto)
- memory_interface_tb: PASS (non usa neuron_parallel, invariato)
- neuron_parallel_tb: PASS - ALL TESTS PASSED (N_INPUTS=32 PARALLEL=8, esatto)
- parametric_tb: PASS (N_INPUTS=256 PARALLEL=32, esatto)
- psram_controller_tb: PASS (non usa neuron_parallel, invariato)
- int8_psram_integration_tb: PASS (non usa neuron_parallel, invariato)
- neuron_memory_tb: PASS, tutti e 5 i test (N_INPUTS=32 PARALLEL=8, esatto)
Nessuna regressione: tutte le configurazioni preesistenti erano già multipli esatti, quindi il ramo `generate` della guardia non si attiva mai per esse.
- 2026-09-02T02:30 — [FASE 9] — Creati due test NEGATIVI dedicati (devono FALLIRE la compilazione per definizione, quello è il test):
- sim/neuron_parallel_guard_negative_nonmultiple_tb.v: istanzia neuron_parallel con N_INPUTS=30, PARALLEL=8 (il caso di troncamento del Finding 1).
- sim/neuron_parallel_guard_negative_degenerate_tb.v: istanzia neuron_parallel con N_INPUTS=4, PARALLEL=8 (il caso di hang/Finding 2).
Ogni file contiene nell'header il comando esatto di verifica e l'errore atteso, per poter essere ri-eseguito manualmente in futuro come regressione "negativa" (nessun binario/vcd generabile per questi, dato che l'elaborazione fallisce sempre by design).
- 2026-09-02T02:32 — [FASE 9] — Eseguita verifica di entrambi i test negativi: `iverilog -g2012 -o <tmp> rtl/*.v sim/neuron_parallel_guard_negative_nonmultiple_tb.v` -> exit=1, errore "Unknown module type: neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL" a rtl/neuron_parallel.v:45. Stesso esito identico per la variante degenerate_tb.v. Confermato: la guardia scatta per ENTRAMBI i finding con lo stesso, unico controllo (`N_INPUTS % PARALLEL != 0` intercetta sia il troncamento sia il caso degenere PARALLEL>N_INPUTS, perché quest'ultimo implica resto non nullo salvo N_INPUTS=0).
- 2026-09-02T02:40 — [FASE 9] — Riscritto sim/parameter_sweep_tb.v: rimosse le CONFIG B, C, E (non più compilabili per design, la loro copertura "negativa" è ora nei due file dedicati sopra). Aggiunte due nuove config valide allineate ai risultati reali del benchmark ECP5:
- CONFIG F: N_INPUTS=32 PARALLEL=2 (GROUPS=16) — configurazione a timing migliore (87.88 MHz, unica PASS a 80MHz nel benchmark).
- CONFIG G: N_INPUTS=32 PARALLEL=4 (GROUPS=8) — seconda scelta per timing.
Mantenute CONFIG A (32/8) e D (64/32) come sanity check di regressione. Rimosso il watchdog a ciclo (non più necessario: la guardia elimina la possibilità di hang per qualunque config che compili).
- 2026-09-02T02:42 — [FASE 9] — Compilato ed eseguito sim/parameter_sweep_tb.v aggiornato: TUTTE E 4 LE CONFIG PASSANO (A: y=32, D: y=64, F: y=32 con PARALLEL=2, G: y=32 con PARALLEL=4). Rigenerati gli artefatti tracciati sim/parameter_sweep_sim e sim/parameter_sweep.vcd.
- 2026-09-02T02:45 — [FASE 9] — Aggiornato docs/FPGA-NeuralNetwork-Engine.md, sezione "## Phase 2 — Parameter Sweep": la sezione "Findings" è stata aggiornata da "non ancora risolti" a "FIXED (2026-09-02)", con descrizione della guardia, riferimento ai due test negativi e al nuovo sweep positivo con PARALLEL=2/4.
- 2026-09-02T02:46 — [FASE 9] — CONCLUSIONE: entrambi i finding di Fase 2 sono ora chiusi tramite un'unica guardia a elaboration-time in rtl/neuron_parallel.v, verificata sia in positivo (nessuna regressione sulle config valide esistenti + nuove config F/G con PARALLEL=2/4) sia in negativo (entrambi i casi patologici falliscono ora la compilazione con un errore esplicito invece di produrre un risultato silenziosamente errato o un hang). Modifica autorizzata esplicitamente dall'utente in deroga alla policy "core non toccare", limitata a questo unico blocco generate.
+24 -17
View File
@@ -600,25 +600,32 @@ including configurations where the number of inputs is not an exact multiple of
- [x] Degenerate config, PARALLEL > N_INPUTS (4×8) - [x] Degenerate config, PARALLEL > N_INPUTS (4×8)
- [x] Sweep testbench: `sim/parameter_sweep_tb.v` - [x] Sweep testbench: `sim/parameter_sweep_tb.v`
**Findings (current RTL behavior, not yet fixed):** **Findings — FIXED (2026-09-02):**
- `neuron_parallel.v` computes `GROUPS = N_INPUTS / PARALLEL` with - `neuron_parallel.v` computed `GROUPS = N_INPUTS / PARALLEL` with
integer division. When `N_INPUTS` is **not** an exact multiple of integer division. When `N_INPUTS` was **not** an exact multiple of
`PARALLEL`, only the first `GROUPS * PARALLEL` inputs are ever read `PARALLEL`, only the first `GROUPS * PARALLEL` inputs were ever
by the accumulator — the remainder is silently dropped (no error, read by the accumulator — the remainder was silently dropped (no
no warning). Confirmed for 30×8 → only 24 of 30 inputs summed, and error, no warning). Confirmed for 30×8 → only 24 of 30 inputs
20×16 → only 16 of 20 inputs summed. summed, and 20×16 → only 16 of 20 inputs summed.
- If `PARALLEL > N_INPUTS`, `GROUPS = 0` and the controller's - If `PARALLEL > N_INPUTS`, `GROUPS = 0` and the controller's
`group_index == GROUPS-1` terminal condition is never satisfied: `group_index == GROUPS-1` terminal condition was never satisfied:
the neuron enters `busy` and never asserts `done` (confirmed hang, the neuron entered `busy` and never asserted `done` (confirmed
500-cycle watchdog in the sweep bench). This is a design hang, 500-cycle watchdog in the sweep bench).
constraint (`PARALLEL` must not exceed `N_INPUTS`, and should - Both share the same root cause (`N_INPUTS % PARALLEL != 0`,
divide it exactly) that is not currently guarded in RTL. degenerate `PARALLEL > N_INPUTS` included) and both are now
- Action: either enforce `N_INPUTS % PARALLEL == 0` and rejected at **elaboration time**, in simulation and synthesis
`PARALLEL <= N_INPUTS` at the caller/config level, or extend alike, by a `generate` guard added to `neuron_parallel.v`
`neuron_parallel.v` to handle a partial final group. Not addressed (instantiates a deliberately undefined module when the parameter
in this phase — core datapath left untouched per current project combination is invalid — zero cost, zero behavior change for any
policy; tracked here for Phase 3/7. valid configuration). The validated datapath itself
(mac8/mac_unit/accumulation/ReLU/saturation) was **not** modified.
See `sim/neuron_parallel_guard_negative_nonmultiple_tb.v` and
`sim/neuron_parallel_guard_negative_degenerate_tb.v` for the
negative-test proof, and `sim/parameter_sweep_tb.v` for the
updated positive sweep (now valid-configs-only, including
`PARALLEL=2` and `PARALLEL=4`, the two best-performing values from
`docs/FPGA-Neural-Datapatch-Benchmark.md`).
## Phase 3 — Memory Architecture ## Phase 3 — Memory Architecture
+29
View File
@@ -17,6 +17,35 @@ module neuron_parallel #(
output reg done output reg done
); );
// ============================================================
// PARAMETER GUARD
//
// PARALLEL must evenly divide N_INPUTS. If it does not:
//
// - GROUPS = N_INPUTS / PARALLEL truncates (integer division),
// and the remainder inputs are silently never read by the
// accumulator: WRONG result, no error, no warning.
//
// - If PARALLEL > N_INPUTS, GROUPS = 0 and the controller's
// terminal condition (group_index == GROUPS-1) is never
// satisfied: the neuron hangs forever (busy stays high,
// done is never asserted).
//
// Both failure modes were confirmed empirically in
// sim/parameter_sweep_tb.v (Phase 2 of the roadmap). Rather than
// changing the validated datapath, this forces an elaboration-
// time failure in BOTH simulation and synthesis by instantiating
// a deliberately undefined module when the condition is
// violated. When N_INPUTS % PARALLEL == 0 this generate branch
// is never elaborated, so valid configurations are unaffected.
// ============================================================
generate
if (N_INPUTS % PARALLEL != 0) begin : PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL invalid_parameter_combination();
end
endgenerate
localparam GROUPS = N_INPUTS / PARALLEL; localparam GROUPS = N_INPUTS / PARALLEL;
localparam GROUP_INDEX_WIDTH = localparam GROUP_INDEX_WIDTH =
(GROUPS <= 1) ? 1 : $clog2(GROUPS); (GROUPS <= 1) ? 1 : $clog2(GROUPS);
+1 -1
View File
@@ -1,5 +1,5 @@
$date $date
Wed Sep 2 14:15:53 2026 Wed Sep 2 14:41:17 2026
$end $end
$version $version
Icarus Verilog Icarus Verilog
+4658 -4658
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
$date $date
Wed Sep 2 14:06:08 2026 Wed Sep 2 14:41:21 2026
$end $end
$version $version
Icarus Verilog Icarus Verilog
+25496 -25496
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,5 +1,5 @@
$date $date
Wed Sep 2 14:15:53 2026 Wed Sep 2 14:41:17 2026
$end $end
$version $version
Icarus Verilog Icarus Verilog
@@ -0,0 +1,41 @@
`timescale 1ns/1ps
// ================================================================
// NEGATIVE TEST - intentionally invalid parameter combination.
//
// This file must FAIL TO COMPILE/ELABORATE. That failure is the
// test: it proves the PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
// guard in rtl/neuron_parallel.v rejects the degenerate case
// PARALLEL > N_INPUTS (Phase 2 finding: N_INPUTS=4, PARALLEL=8 used
// to leave GROUPS=0, causing the controller to hang forever instead
// of erroring).
//
// Verify with:
// iverilog -g2012 -o /tmp/out rtl/*.v \
// sim/neuron_parallel_guard_negative_degenerate_tb.v
//
// Expected: nonzero exit status and
// "error: Unknown module type:
// neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL"
// ================================================================
module tb;
neuron_parallel #(
.DATA_WIDTH(8),
.N_INPUTS(4),
.PARALLEL(8),
.ACC_WIDTH(32)
) u_invalid (
.clk(1'b0),
.rst(1'b0),
.start(1'b0),
.x_bus(32'b0),
.w_bus(32'b0),
.bias(8'sd0),
.y(),
.busy(),
.done()
);
endmodule
@@ -0,0 +1,41 @@
`timescale 1ns/1ps
// ================================================================
// NEGATIVE TEST - intentionally invalid parameter combination.
//
// This file must FAIL TO COMPILE/ELABORATE. That failure is the
// test: it proves the PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL
// guard in rtl/neuron_parallel.v rejects configurations where
// N_INPUTS is not an exact multiple of PARALLEL (Phase 2 finding:
// N_INPUTS=30, PARALLEL=8 used to silently drop the last 6 inputs
// instead of erroring).
//
// Verify with:
// iverilog -g2012 -o /tmp/out rtl/*.v \
// sim/neuron_parallel_guard_negative_nonmultiple_tb.v
//
// Expected: nonzero exit status and
// "error: Unknown module type:
// neuron_parallel_requires_N_INPUTS_multiple_of_PARALLEL"
// ================================================================
module tb;
neuron_parallel #(
.DATA_WIDTH(8),
.N_INPUTS(30),
.PARALLEL(8),
.ACC_WIDTH(32)
) u_invalid (
.clk(1'b0),
.rst(1'b0),
.start(1'b0),
.x_bus(240'b0),
.w_bus(240'b0),
.bias(8'sd0),
.y(),
.busy(),
.done()
);
endmodule
+5069 -5069
View File
File diff suppressed because one or more lines are too long
+2250 -18345
View File
File diff suppressed because it is too large Load Diff
+7216 -8918
View File
File diff suppressed because one or more lines are too long
+134 -262
View File
@@ -4,19 +4,30 @@
// PHASE 2 - PARAMETER SWEEP // PHASE 2 - PARAMETER SWEEP
// //
// Roadmap requirement (docs/FPGA-NeuralNetwork-Engine.md, Phase 2): // Roadmap requirement (docs/FPGA-NeuralNetwork-Engine.md, Phase 2):
// validate multiple combinations of N_INPUTS / N_NEURONS / PARALLEL, // validate multiple combinations of N_INPUTS / N_NEURONS / PARALLEL.
// including configurations where N_INPUTS is NOT an exact multiple
// of PARALLEL.
// //
// neuron_parallel.v computes: // HISTORY:
// localparam GROUPS = N_INPUTS / PARALLEL; // The original version of this bench included non-exact-multiple
// which is an INTEGER division. When N_INPUTS is not an exact // configs (N_INPUTS=30/PARALLEL=8, N_INPUTS=20/PARALLEL=16) and a
// multiple of PARALLEL, the remainder inputs are silently never // degenerate PARALLEL>N_INPUTS config (N_INPUTS=4/PARALLEL=8). Those
// read by the accumulator (GEN_TREE only ever selects the first // exposed two silent-failure modes in rtl/neuron_parallel.v:
// GROUPS*PARALLEL inputs). This bench characterizes that behavior // - non-exact multiples: remainder inputs silently dropped (wrong
// instead of assuming it does not exist, and uses a cycle-count // result, no error).
// watchdog (never a blocking `wait`) so a config that never // - PARALLEL > N_INPUTS: GROUPS=0, controller never asserts done
// asserts `done` is reported instead of hanging the simulation. // (permanent hang).
// Both are now rejected at elaboration time by the
// PARAMETER_ERROR_N_INPUTS_NOT_MULTIPLE_OF_PARALLEL guard added to
// rtl/neuron_parallel.v, so those three configs would no longer
// compile -- which is the intended fix. Their negative-test coverage
// (proving the guard actually fires) lives in:
// sim/neuron_parallel_guard_negative_nonmultiple_tb.v
// sim/neuron_parallel_guard_negative_degenerate_tb.v
//
// This bench now sweeps only VALID (exact-multiple) configurations,
// including PARALLEL=2 and PARALLEL=4 -- the two best-performing
// parallelism values found in docs/FPGA-Neural-Datapatch-Benchmark.md
// (PARALLEL=2 is the only tested config that meets 80 MHz; PARALLEL=4
// is a close second).
// ================================================================ // ================================================================
module tb; module tb;
@@ -30,7 +41,6 @@ module tb;
end end
integer errors; integer errors;
integer findings;
// ============================================================ // ============================================================
// CONFIG A - baseline, exact multiple (sanity check) // CONFIG A - baseline, exact multiple (sanity check)
@@ -60,62 +70,6 @@ module tb;
.y(y_a), .busy(busy_a), .done(done_a) .y(y_a), .busy(busy_a), .done(done_a)
); );
// ============================================================
// CONFIG B - non-exact multiple
// N_INPUTS=30 PARALLEL=8 -> GROUPS=3 (24 inputs actually summed)
// ============================================================
localparam B_DATA_WIDTH = 8;
localparam B_N_INPUTS = 30;
localparam B_PARALLEL = 8;
localparam B_ACC_WIDTH = 32;
reg start_b;
reg signed [B_DATA_WIDTH*B_N_INPUTS-1:0] x_bus_b;
reg signed [B_DATA_WIDTH*B_N_INPUTS-1:0] w_bus_b;
reg signed [B_DATA_WIDTH-1:0] bias_b;
wire signed [B_DATA_WIDTH-1:0] y_b;
wire busy_b, done_b;
neuron_parallel #(
.DATA_WIDTH(B_DATA_WIDTH),
.N_INPUTS(B_N_INPUTS),
.PARALLEL(B_PARALLEL),
.ACC_WIDTH(B_ACC_WIDTH)
) u_b (
.clk(clk), .rst(rst), .start(start_b),
.x_bus(x_bus_b), .w_bus(w_bus_b), .bias(bias_b),
.y(y_b), .busy(busy_b), .done(done_b)
);
// ============================================================
// CONFIG C - non-exact multiple, different PARALLEL
// N_INPUTS=20 PARALLEL=16 -> GROUPS=1 (16 inputs actually summed)
// ============================================================
localparam C_DATA_WIDTH = 8;
localparam C_N_INPUTS = 20;
localparam C_PARALLEL = 16;
localparam C_ACC_WIDTH = 32;
reg start_c;
reg signed [C_DATA_WIDTH*C_N_INPUTS-1:0] x_bus_c;
reg signed [C_DATA_WIDTH*C_N_INPUTS-1:0] w_bus_c;
reg signed [C_DATA_WIDTH-1:0] bias_c;
wire signed [C_DATA_WIDTH-1:0] y_c;
wire busy_c, done_c;
neuron_parallel #(
.DATA_WIDTH(C_DATA_WIDTH),
.N_INPUTS(C_N_INPUTS),
.PARALLEL(C_PARALLEL),
.ACC_WIDTH(C_ACC_WIDTH)
) u_c (
.clk(clk), .rst(rst), .start(start_c),
.x_bus(x_bus_c), .w_bus(w_bus_c), .bias(bias_c),
.y(y_c), .busy(busy_c), .done(done_c)
);
// ============================================================ // ============================================================
// CONFIG D - exact multiple, wide parallelism (sanity check) // CONFIG D - exact multiple, wide parallelism (sanity check)
// N_INPUTS=64 PARALLEL=32 -> GROUPS=2 // N_INPUTS=64 PARALLEL=32 -> GROUPS=2
@@ -145,42 +99,70 @@ module tb;
); );
// ============================================================ // ============================================================
// CONFIG E - degenerate: PARALLEL > N_INPUTS // CONFIG F - PARALLEL=2 (best timing per benchmark)
// N_INPUTS=4 PARALLEL=8 -> GROUPS=0 // N_INPUTS=32 PARALLEL=2 -> GROUPS=16
// Watchdog-guarded: expected to NOT complete (documents the
// constraint "PARALLEL must not exceed N_INPUTS").
// ============================================================ // ============================================================
localparam E_DATA_WIDTH = 8; localparam F_DATA_WIDTH = 8;
localparam E_N_INPUTS = 4; localparam F_N_INPUTS = 32;
localparam E_PARALLEL = 8; localparam F_PARALLEL = 2;
localparam E_ACC_WIDTH = 32; localparam F_ACC_WIDTH = 32;
reg start_e; reg start_f;
reg signed [E_DATA_WIDTH*E_N_INPUTS-1:0] x_bus_e; reg signed [F_DATA_WIDTH*F_N_INPUTS-1:0] x_bus_f;
reg signed [E_DATA_WIDTH*E_N_INPUTS-1:0] w_bus_e; reg signed [F_DATA_WIDTH*F_N_INPUTS-1:0] w_bus_f;
reg signed [E_DATA_WIDTH-1:0] bias_e; reg signed [F_DATA_WIDTH-1:0] bias_f;
wire signed [E_DATA_WIDTH-1:0] y_e; wire signed [F_DATA_WIDTH-1:0] y_f;
wire busy_e, done_e; wire busy_f, done_f;
neuron_parallel #( neuron_parallel #(
.DATA_WIDTH(E_DATA_WIDTH), .DATA_WIDTH(F_DATA_WIDTH),
.N_INPUTS(E_N_INPUTS), .N_INPUTS(F_N_INPUTS),
.PARALLEL(E_PARALLEL), .PARALLEL(F_PARALLEL),
.ACC_WIDTH(E_ACC_WIDTH) .ACC_WIDTH(F_ACC_WIDTH)
) u_e ( ) u_f (
.clk(clk), .rst(rst), .start(start_e), .clk(clk), .rst(rst), .start(start_f),
.x_bus(x_bus_e), .w_bus(w_bus_e), .bias(bias_e), .x_bus(x_bus_f), .w_bus(w_bus_f), .bias(bias_f),
.y(y_e), .busy(busy_e), .done(done_e) .y(y_f), .busy(busy_f), .done(done_f)
);
// ============================================================
// CONFIG G - PARALLEL=4 (close second per benchmark)
// N_INPUTS=32 PARALLEL=4 -> GROUPS=8
// ============================================================
localparam G_DATA_WIDTH = 8;
localparam G_N_INPUTS = 32;
localparam G_PARALLEL = 4;
localparam G_ACC_WIDTH = 32;
reg start_g;
reg signed [G_DATA_WIDTH*G_N_INPUTS-1:0] x_bus_g;
reg signed [G_DATA_WIDTH*G_N_INPUTS-1:0] w_bus_g;
reg signed [G_DATA_WIDTH-1:0] bias_g;
wire signed [G_DATA_WIDTH-1:0] y_g;
wire busy_g, done_g;
neuron_parallel #(
.DATA_WIDTH(G_DATA_WIDTH),
.N_INPUTS(G_N_INPUTS),
.PARALLEL(G_PARALLEL),
.ACC_WIDTH(G_ACC_WIDTH)
) u_g (
.clk(clk), .rst(rst), .start(start_g),
.x_bus(x_bus_g), .w_bus(w_bus_g), .bias(bias_g),
.y(y_g), .busy(busy_g), .done(done_g)
); );
// ============================================================ // ============================================================
// MAIN // MAIN
//
// Every config here is a VALID (exact-multiple) parameter
// combination, so a plain blocking `wait(done)` is safe -- the
// elaboration guard already rejects anything that could hang.
// ============================================================ // ============================================================
integer max_cycles;
integer count; integer count;
reg timed_out;
initial begin initial begin
@@ -189,26 +171,22 @@ module tb;
rst = 1; rst = 1;
errors = 0; errors = 0;
findings = 0;
max_cycles = 500;
start_a = 0; x_bus_a = 0; w_bus_a = 0; bias_a = 0; start_a = 0; x_bus_a = 0; w_bus_a = 0; bias_a = 0;
start_b = 0; x_bus_b = 0; w_bus_b = 0; bias_b = 0;
start_c = 0; x_bus_c = 0; w_bus_c = 0; bias_c = 0;
start_d = 0; x_bus_d = 0; w_bus_d = 0; bias_d = 0; start_d = 0; x_bus_d = 0; w_bus_d = 0; bias_d = 0;
start_e = 0; x_bus_e = 0; w_bus_e = 0; bias_e = 0; start_f = 0; x_bus_f = 0; w_bus_f = 0; bias_f = 0;
start_g = 0; x_bus_g = 0; w_bus_g = 0; bias_g = 0;
repeat (2) @(posedge clk); repeat (2) @(posedge clk);
rst = 0; rst = 0;
$display(""); $display("");
$display("========================================"); $display("========================================");
$display("PHASE 2 - PARAMETER SWEEP"); $display("PHASE 2 - PARAMETER SWEEP (guarded, valid configs only)");
$display("========================================"); $display("========================================");
// -------------------------------------------------------- // --------------------------------------------------------
// CONFIG A: all x=1, all w=1, bias=0 // CONFIG A: all x=1, all w=1, bias=0 -> expect 32
// full sum = 32, exact multiple -> expect 32
// -------------------------------------------------------- // --------------------------------------------------------
for (count = 0; count < A_N_INPUTS; count = count + 1) begin for (count = 0; count < A_N_INPUTS; count = count + 1) begin
x_bus_a[count*A_DATA_WIDTH +: A_DATA_WIDTH] = 8'sd1; x_bus_a[count*A_DATA_WIDTH +: A_DATA_WIDTH] = 8'sd1;
@@ -218,128 +196,22 @@ module tb;
@(posedge clk); start_a <= 1'b1; @(posedge clk); start_a <= 1'b1;
@(posedge clk); start_a <= 1'b0; @(posedge clk); start_a <= 1'b0;
wait (done_a);
timed_out = 1'b0;
count = 0;
while (!done_a && !timed_out) begin
@(posedge clk);
count = count + 1;
if (count > max_cycles) timed_out = 1'b1;
end
@(posedge clk); @(posedge clk);
$display(""); $display("");
$display("CONFIG A: N_INPUTS=%0d PARALLEL=%0d (exact, GROUPS=%0d)", $display("CONFIG A: N_INPUTS=%0d PARALLEL=%0d (GROUPS=%0d)",
A_N_INPUTS, A_PARALLEL, A_N_INPUTS/A_PARALLEL); A_N_INPUTS, A_PARALLEL, A_N_INPUTS/A_PARALLEL);
if (timed_out) begin $display(" y = %0d expected = 32", y_a);
$display(" RESULT: TIMEOUT (did not assert done within %0d cycles)", max_cycles); if (y_a !== 8'sd32) begin
$display(" FAIL");
errors = errors + 1; errors = errors + 1;
end else begin end else begin
$display(" y = %0d expected = 32", y_a); $display(" PASS");
if (y_a !== 8'sd32) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end
end end
// -------------------------------------------------------- // --------------------------------------------------------
// CONFIG B: all x=1, all w=1, bias=0 // CONFIG D: all x=1, all w=1, bias=0 -> expect 64
// N_INPUTS=30, PARALLEL=8 -> GROUPS=3 -> only first 24
// inputs are actually summed by the current RTL.
// full-sum expectation would be 30; RTL-truncated
// expectation is 24. We check against the RTL-truncated
// value and flag the mismatch vs. the full sum as a
// documented finding (not a failure of this bench).
// --------------------------------------------------------
for (count = 0; count < B_N_INPUTS; count = count + 1) begin
x_bus_b[count*B_DATA_WIDTH +: B_DATA_WIDTH] = 8'sd1;
w_bus_b[count*B_DATA_WIDTH +: B_DATA_WIDTH] = 8'sd1;
end
bias_b = 0;
@(posedge clk); start_b <= 1'b1;
@(posedge clk); start_b <= 1'b0;
timed_out = 1'b0;
count = 0;
while (!done_b && !timed_out) begin
@(posedge clk);
count = count + 1;
if (count > max_cycles) timed_out = 1'b1;
end
@(posedge clk);
$display("");
$display("CONFIG B: N_INPUTS=%0d PARALLEL=%0d (NON-exact, GROUPS=%0d, %0d inputs actually read)",
B_N_INPUTS, B_PARALLEL, B_N_INPUTS/B_PARALLEL,
(B_N_INPUTS/B_PARALLEL)*B_PARALLEL);
if (timed_out) begin
$display(" RESULT: TIMEOUT (did not assert done within %0d cycles)", max_cycles);
errors = errors + 1;
end else begin
$display(" y = %0d RTL-truncated expected = 24 full-sum (NOT met) = 30", y_b);
if (y_b !== 8'sd24) begin
$display(" FAIL (unexpected value for current RTL behavior)");
errors = errors + 1;
end else begin
$display(" PASS (matches current RTL truncation behavior)");
end
if (y_b !== B_N_INPUTS[7:0]) begin
$display(" FINDING: last %0d input(s) are silently ignored (GROUPS = N_INPUTS/PARALLEL truncates)",
B_N_INPUTS - (B_N_INPUTS/B_PARALLEL)*B_PARALLEL);
findings = findings + 1;
end
end
// --------------------------------------------------------
// CONFIG C: same characterization, different sizes
// N_INPUTS=20, PARALLEL=16 -> GROUPS=1 -> only first 16 read
// --------------------------------------------------------
for (count = 0; count < C_N_INPUTS; count = count + 1) begin
x_bus_c[count*C_DATA_WIDTH +: C_DATA_WIDTH] = 8'sd1;
w_bus_c[count*C_DATA_WIDTH +: C_DATA_WIDTH] = 8'sd1;
end
bias_c = 0;
@(posedge clk); start_c <= 1'b1;
@(posedge clk); start_c <= 1'b0;
timed_out = 1'b0;
count = 0;
while (!done_c && !timed_out) begin
@(posedge clk);
count = count + 1;
if (count > max_cycles) timed_out = 1'b1;
end
@(posedge clk);
$display("");
$display("CONFIG C: N_INPUTS=%0d PARALLEL=%0d (NON-exact, GROUPS=%0d, %0d inputs actually read)",
C_N_INPUTS, C_PARALLEL, C_N_INPUTS/C_PARALLEL,
(C_N_INPUTS/C_PARALLEL)*C_PARALLEL);
if (timed_out) begin
$display(" RESULT: TIMEOUT (did not assert done within %0d cycles)", max_cycles);
errors = errors + 1;
end else begin
$display(" y = %0d RTL-truncated expected = 16 full-sum (NOT met) = 20", y_c);
if (y_c !== 8'sd16) begin
$display(" FAIL (unexpected value for current RTL behavior)");
errors = errors + 1;
end else begin
$display(" PASS (matches current RTL truncation behavior)");
end
if (y_c !== C_N_INPUTS[7:0]) begin
$display(" FINDING: last %0d input(s) are silently ignored (GROUPS = N_INPUTS/PARALLEL truncates)",
C_N_INPUTS - (C_N_INPUTS/C_PARALLEL)*C_PARALLEL);
findings = findings + 1;
end
end
// --------------------------------------------------------
// CONFIG D: all x=1, all w=1, bias=0
// full sum = 64, exact multiple -> expect 64
// -------------------------------------------------------- // --------------------------------------------------------
for (count = 0; count < D_N_INPUTS; count = count + 1) begin for (count = 0; count < D_N_INPUTS; count = count + 1) begin
x_bus_d[count*D_DATA_WIDTH +: D_DATA_WIDTH] = 8'sd1; x_bus_d[count*D_DATA_WIDTH +: D_DATA_WIDTH] = 8'sd1;
@@ -349,76 +221,76 @@ module tb;
@(posedge clk); start_d <= 1'b1; @(posedge clk); start_d <= 1'b1;
@(posedge clk); start_d <= 1'b0; @(posedge clk); start_d <= 1'b0;
wait (done_d);
timed_out = 1'b0;
count = 0;
while (!done_d && !timed_out) begin
@(posedge clk);
count = count + 1;
if (count > max_cycles) timed_out = 1'b1;
end
@(posedge clk); @(posedge clk);
$display(""); $display("");
$display("CONFIG D: N_INPUTS=%0d PARALLEL=%0d (exact, GROUPS=%0d)", $display("CONFIG D: N_INPUTS=%0d PARALLEL=%0d (GROUPS=%0d)",
D_N_INPUTS, D_PARALLEL, D_N_INPUTS/D_PARALLEL); D_N_INPUTS, D_PARALLEL, D_N_INPUTS/D_PARALLEL);
if (timed_out) begin $display(" y = %0d expected = 64", y_d);
$display(" RESULT: TIMEOUT (did not assert done within %0d cycles)", max_cycles); if (y_d !== 8'sd64) begin
$display(" FAIL");
errors = errors + 1; errors = errors + 1;
end else begin end else begin
$display(" y = %0d expected = 64", y_d); $display(" PASS");
if (y_d !== 8'sd64) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end
end end
// -------------------------------------------------------- // --------------------------------------------------------
// CONFIG E: degenerate PARALLEL > N_INPUTS -> GROUPS=0 // CONFIG F: PARALLEL=2, all x=1, all w=1, bias=0 -> expect 32
// We EXPECT this to time out. If it ever completes, that
// is itself worth flagging (behavior changed).
// -------------------------------------------------------- // --------------------------------------------------------
for (count = 0; count < E_N_INPUTS; count = count + 1) begin for (count = 0; count < F_N_INPUTS; count = count + 1) begin
x_bus_e[count*E_DATA_WIDTH +: E_DATA_WIDTH] = 8'sd1; x_bus_f[count*F_DATA_WIDTH +: F_DATA_WIDTH] = 8'sd1;
w_bus_e[count*E_DATA_WIDTH +: E_DATA_WIDTH] = 8'sd1; w_bus_f[count*F_DATA_WIDTH +: F_DATA_WIDTH] = 8'sd1;
end end
bias_e = 0; bias_f = 0;
@(posedge clk); start_e <= 1'b1; @(posedge clk); start_f <= 1'b1;
@(posedge clk); start_e <= 1'b0; @(posedge clk); start_f <= 1'b0;
wait (done_f);
timed_out = 1'b0;
count = 0;
while (!done_e && !timed_out) begin
@(posedge clk);
count = count + 1;
if (count > max_cycles) timed_out = 1'b1;
end
@(posedge clk); @(posedge clk);
$display(""); $display("");
$display("CONFIG E: N_INPUTS=%0d PARALLEL=%0d (DEGENERATE, GROUPS=%0d)", $display("CONFIG F: N_INPUTS=%0d PARALLEL=%0d (GROUPS=%0d) -- best timing per benchmark",
E_N_INPUTS, E_PARALLEL, E_N_INPUTS/E_PARALLEL); F_N_INPUTS, F_PARALLEL, F_N_INPUTS/F_PARALLEL);
if (timed_out) begin $display(" y = %0d expected = 32", y_f);
$display(" RESULT: TIMEOUT as expected (done never asserted within %0d cycles)", max_cycles); if (y_f !== 8'sd32) begin
$display(" FINDING: PARALLEL > N_INPUTS (GROUPS=0) hangs neuron_parallel forever -- design constraint, not currently guarded in RTL"); $display(" FAIL");
findings = findings + 1;
end else begin
$display(" RESULT: completed with y=%0d (unexpected -- previously assumed to hang)", y_e);
errors = errors + 1; errors = errors + 1;
end else begin
$display(" PASS");
end
// --------------------------------------------------------
// CONFIG G: PARALLEL=4, all x=1, all w=1, bias=0 -> expect 32
// --------------------------------------------------------
for (count = 0; count < G_N_INPUTS; count = count + 1) begin
x_bus_g[count*G_DATA_WIDTH +: G_DATA_WIDTH] = 8'sd1;
w_bus_g[count*G_DATA_WIDTH +: G_DATA_WIDTH] = 8'sd1;
end
bias_g = 0;
@(posedge clk); start_g <= 1'b1;
@(posedge clk); start_g <= 1'b0;
wait (done_g);
@(posedge clk);
$display("");
$display("CONFIG G: N_INPUTS=%0d PARALLEL=%0d (GROUPS=%0d) -- close second per benchmark",
G_N_INPUTS, G_PARALLEL, G_N_INPUTS/G_PARALLEL);
$display(" y = %0d expected = 32", y_g);
if (y_g !== 8'sd32) begin
$display(" FAIL");
errors = errors + 1;
end else begin
$display(" PASS");
end end
$display(""); $display("");
$display("========================================"); $display("========================================");
$display("PARAMETER SWEEP SUMMARY");
$display(" errors = %0d", errors);
$display(" findings = %0d (documented limitations, not bench failures)", findings);
if (errors == 0) if (errors == 0)
$display("PARAMETER SWEEP: PASSED (all configs behaved as characterized)"); $display("PARAMETER SWEEP: PASSED (%0d valid configs)", 4);
else else
$display("PARAMETER SWEEP: FAILED"); $display("PARAMETER SWEEP: FAILED (%0d errors)", errors);
$display("========================================"); $display("========================================");
$display(""); $display("");