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
148 lines
4.0 KiB
Verilog
148 lines
4.0 KiB
Verilog
module neuron_parallel #(
|
|
parameter DATA_WIDTH = 8,
|
|
parameter N_INPUTS = 32,
|
|
parameter PARALLEL = 8,
|
|
parameter ACC_WIDTH = 32
|
|
)(
|
|
input clk,
|
|
input rst,
|
|
input start,
|
|
|
|
input signed [DATA_WIDTH*N_INPUTS-1:0] x_bus,
|
|
input signed [DATA_WIDTH*N_INPUTS-1:0] w_bus,
|
|
input signed [DATA_WIDTH-1:0] bias,
|
|
|
|
output reg signed [DATA_WIDTH-1:0] y,
|
|
output reg busy,
|
|
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 GROUP_INDEX_WIDTH =
|
|
(GROUPS <= 1) ? 1 : $clog2(GROUPS);
|
|
|
|
reg [GROUP_INDEX_WIDTH-1:0] group_index;
|
|
|
|
reg signed [ACC_WIDTH-1:0] acc;
|
|
|
|
wire signed [DATA_WIDTH*PARALLEL-1:0] x_group;
|
|
wire signed [DATA_WIDTH*PARALLEL-1:0] w_group;
|
|
|
|
wire signed [ACC_WIDTH-1:0] acc_next;
|
|
|
|
wire signed [ACC_WIDTH-1:0] bias_ext;
|
|
wire signed [ACC_WIDTH-1:0] final_acc;
|
|
|
|
assign x_group =
|
|
x_bus[
|
|
group_index*PARALLEL*DATA_WIDTH
|
|
+: PARALLEL*DATA_WIDTH
|
|
];
|
|
|
|
assign w_group =
|
|
w_bus[
|
|
group_index*PARALLEL*DATA_WIDTH
|
|
+: PARALLEL*DATA_WIDTH
|
|
];
|
|
|
|
mac8 #(
|
|
.DATA_WIDTH(DATA_WIDTH),
|
|
.ACC_WIDTH(ACC_WIDTH),
|
|
.PARALLEL(PARALLEL)
|
|
) u_mac8 (
|
|
.x_bus(x_group),
|
|
.w_bus(w_group),
|
|
.acc_in(acc),
|
|
.acc_out(acc_next)
|
|
);
|
|
|
|
// Sign extension INT8 -> INT32
|
|
assign bias_ext =
|
|
{{(ACC_WIDTH-DATA_WIDTH){bias[DATA_WIDTH-1]}}, bias};
|
|
|
|
// Accumulazione finale + bias
|
|
assign final_acc = acc_next + bias_ext;
|
|
|
|
always @(posedge clk) begin
|
|
|
|
if (rst) begin
|
|
|
|
group_index <= 0;
|
|
acc <= 0;
|
|
y <= 0;
|
|
busy <= 0;
|
|
done <= 0;
|
|
|
|
end else begin
|
|
|
|
done <= 0;
|
|
|
|
if (start && !busy) begin
|
|
|
|
group_index <= 0;
|
|
acc <= 0;
|
|
busy <= 1;
|
|
|
|
end else if (busy) begin
|
|
|
|
if (group_index == GROUPS-1) begin
|
|
|
|
acc <= final_acc;
|
|
|
|
// ReLU
|
|
if (final_acc <= 0) begin
|
|
y <= 0;
|
|
end
|
|
|
|
// Saturazione INT32 -> INT8
|
|
else if (final_acc > 127) begin
|
|
y <= 8'sd127;
|
|
end
|
|
|
|
else begin
|
|
y <= final_acc[DATA_WIDTH-1:0];
|
|
end
|
|
|
|
busy <= 0;
|
|
done <= 1;
|
|
|
|
end else begin
|
|
|
|
acc <= acc_next;
|
|
group_index <= group_index + 1'b1;
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
endmodule |