Two related Phase 5 additions, both threaded the same way (a new runtime field defaulting to the pre-existing behavior, settable per-layer via the descriptor table or per-run via SET_BASE): Configurable activation functions: - neuron_parallel.v gains a 2-bit `activation` port (ACT_NONE = linear + two-sided INT8 saturate, ACT_RELU = the original hardwired behavior, kept as the default so every pre-existing caller/testbench is unaffected), threaded through neuron_memory.v. - spi_engine.v: SET_BASE sel=6 (single-layer path); the descriptor table gains a 7th byte (multi-layer path). - Verified in neuron_parallel_tb.v (negative pass-through + negative saturation to -128) and end-to-end in spi_neuron_top_runnetwork_tb.v (a real negative accumulator that ACT_RELU would clamp to 0 comes through unclamped under ACT_NONE, over real SPI/RAM). Runtime network width (one bitstream, any topology up to its build-time max, entirely host-configured over SPI): - neuron_parallel.v gains n_inputs_real, bounding its MAC group loop (n_inputs_real/PARALLEL groups instead of the fixed build-time count). neuron_memory.v gains n_inputs_real/n_neurons_real, bounding its X/W RAM-read loop and its neuron loop. All default to the build-time max, so unconnected callers are unaffected. n_inputs_real must stay a multiple of PARALLEL (same constraint N_INPUTS itself is held to at elaboration time, now the caller's runtime responsibility). - spi_engine.v: SET_BASE sel=7/8 (single-layer path); the descriptor table grows to 11 bytes/layer (+n_inputs_real +n_neurons_real, multi-layer path) -- layer_sequencer.v also now copies only n_neurons_real bytes into the ping-pong buffer, not the full build width. - This is real early termination, not bookkeeping: no RAM zero-padding needed for the unused tail, and it measurably completes faster. neuron_parallel_tb.v TEST 7: 3 cycles vs 6 for a reduced-vs-full run, with garbage loaded into the skipped lanes to prove they're never read. neuron_memory_tb.v TEST 5: through the real PSRAM stack, 209 cycles vs 788. layer_sequencer_tb.v proves a reduced n_neurons_real shortens the ping-pong copy-out itself (bytes beyond the real count stay untouched, not just differing). docs/FPGA-NeuralNetwork-Engine.md: §8.1 opcode/SET_BASE table, new "Runtime network width" subsection, Phase 5 checklist, Current Status table, and the "Core architectural principle" statement updated to reflect that topology (not just trained parameters) is now host-configured at runtime up to a build-time ceiling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQV3vS9TXaGDJ5cRfnfidt
201 lines
6.5 KiB
Verilog
201 lines
6.5 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,
|
|
|
|
// Activation function applied to the final accumulator before
|
|
// the INT8 saturate, see ACT_* localparams below. Defaults to
|
|
// ACT_RELU (2'd1) -- the ONLY behavior this module had before
|
|
// this port existed -- so every pre-existing caller that leaves
|
|
// it unconnected (rtl/layer.v and its testbenches) is completely
|
|
// unaffected.
|
|
input [1:0] activation = 2'd1,
|
|
|
|
// Real (runtime) input width for THIS run, in elements -- must
|
|
// be a multiple of PARALLEL (same constraint N_INPUTS itself is
|
|
// held to at elaboration time, just now the caller's runtime
|
|
// responsibility instead of a build-time guard: an n_inputs_real
|
|
// that isn't a PARALLEL multiple, or is 0, reproduces the same
|
|
// "wrong result" / "hangs forever" failure modes documented
|
|
// below for a bad N_INPUTS/PARALLEL pair). Defaults to N_INPUTS
|
|
// (the full build-time width), so any caller that leaves this
|
|
// unconnected processes every group exactly as before this port
|
|
// existed.
|
|
input [15:0] n_inputs_real = N_INPUTS[15:0],
|
|
|
|
output reg signed [DATA_WIDTH-1:0] y,
|
|
output reg busy,
|
|
output reg done
|
|
);
|
|
|
|
// ============================================================
|
|
// ACTIVATION ENCODING
|
|
// ============================================================
|
|
|
|
localparam ACT_NONE = 2'd0; // linear: saturate both directions, no clamp
|
|
localparam ACT_RELU = 2'd1; // max(0, x), then saturate positive (default)
|
|
|
|
// ============================================================
|
|
// 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;
|
|
|
|
// Runtime group count for this run: n_inputs_real / PARALLEL.
|
|
// PARALLEL is a build-time constant, so this divide is a fixed
|
|
// combinational block sized once at synthesis (a shift when
|
|
// PARALLEL is a power of two, as in every config this project
|
|
// uses today), evaluated only at the start of a run -- not on
|
|
// the per-group critical path.
|
|
wire [15:0] groups_real = n_inputs_real / PARALLEL[15:0];
|
|
|
|
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_real[GROUP_INDEX_WIDTH-1:0] - 1'b1) begin
|
|
|
|
acc <= final_acc;
|
|
|
|
case (activation)
|
|
|
|
ACT_NONE: begin
|
|
|
|
// Linear: no zero-clamp, saturate both
|
|
// directions to the INT8 range.
|
|
|
|
if (final_acc > 127) begin
|
|
y <= 8'sd127;
|
|
end else if (final_acc < -128) begin
|
|
y <= -8'sd128;
|
|
end else begin
|
|
y <= final_acc[DATA_WIDTH-1:0];
|
|
end
|
|
|
|
end
|
|
|
|
default: begin // ACT_RELU (also the fallback
|
|
// for any reserved encoding)
|
|
|
|
if (final_acc <= 0) begin
|
|
y <= 0;
|
|
end else if (final_acc > 127) begin
|
|
y <= 8'sd127;
|
|
end else begin
|
|
y <= final_acc[DATA_WIDTH-1:0];
|
|
end
|
|
|
|
end
|
|
|
|
endcase
|
|
|
|
busy <= 0;
|
|
done <= 1;
|
|
|
|
end else begin
|
|
|
|
acc <= acc_next;
|
|
group_index <= group_index + 1'b1;
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
endmodule |