PSRAM page-mode read burst support in psram_controller.v: enables the ISSI IS66WVE4M16EBLL-70BLI's page mode via its configuration-register software-access sequence at boot (disabled by default on the real chip), then keeps CE#/OE# asserted after a read so a same-page continuation only pays tAPA (20ns) instead of a full tAA (70ns) random access, with automatic tCEM-safe session closing. Only a WRITE closes the page -- byte-enable changes do not, since int8_memory_access.v alternates them on nearly every access and an early implementation attempt that treated them as a close condition measured a real regression (53.25->61.25 cycles/edge) before being corrected (53.25->37.53 cycles/edge, +42% gather bandwidth). sim/psram_model.v gained independent tAPA/tAA and tCEM enforcement (with a real Verilog same-timestep event-ordering race found and fixed via a #0 sync) so the regression proves real timing compliance, not just data correctness. New sim/psram_page_mode_tb.v; full 26-file regression suite re-run clean. Real nextpnr-ecp5 Fmax re-measured on the full spi_neuron_top system: 75.73MHz (P2, up from 55.59MHz) and 65.13MHz (P8) -- still under the 80MHz target but not regressed, with the critical path confirmed (not assumed) to remain entirely inside neuron_parallel's accumulate chain, never psram_controller. Also includes this session's other already-validated work: the graph engine (Type #2 sparse-graph network: act_buffer, graph_engine, netasm host assembler), real CABGA381 pinout (.lpf, place&route verified) and physical IRQ_N/DATA_READY_N pins, and Phase 7 timing closure logs -- all previously uncommitted, documented in WORKLOG.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LH3jPeJ3eFMfF2v8SQhpkk
247 lines
9.5 KiB
Verilog
247 lines
9.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
|
|
//
|
|
// PIPELINE STAGE (timing closure, step 2): this now adds bias to
|
|
// the ALREADY-REGISTERED `acc` (the complete sum, latched at the
|
|
// end of the last MAC group -- see the `finishing` stage below),
|
|
// not to `acc_next` combinationally chained onto the same cycle
|
|
// as the last group's own MAC-tree add. Measured on real
|
|
// place&route (see WORKLOG.md "Timing closure"): the previous
|
|
// same-cycle chain [mac8 tree add] -> [bias add] -> [saturate]
|
|
// was the critical path; splitting the bias-add+saturate half
|
|
// into its own cycle (operating on a value already settled in a
|
|
// register) breaks that chain by construction, independent of
|
|
// synthesis/placement heuristics -- unlike the bit-test rewrite
|
|
// below, which measurably shortens the logic but was swamped by
|
|
// seed-to-seed placement noise on its own. Adds exactly one
|
|
// cycle of latency per neuron (`start` to `done`), transparent
|
|
// to every caller's busy/done handshake (neuron_memory.v,
|
|
// layer_sequencer.v, graph_engine.v) -- no protocol change, no
|
|
// caller-visible interface change, only one extra `finishing`
|
|
// clock edge already absorbed by that handshake.
|
|
assign final_acc = acc + bias_ext;
|
|
|
|
// ============================================================
|
|
// SATURATION / ACTIVATION -- bit-test form (timing closure)
|
|
//
|
|
// final_acc (ACC_WIDTH bits) fits the signed DATA_WIDTH range
|
|
// iff bits [ACC_WIDTH-1:DATA_WIDTH-1] are all equal (all 0 ->
|
|
// non-negative and in range, all 1 -> negative and in range) --
|
|
// exactly what sign-extending the truncated DATA_WIDTH-bit value
|
|
// back up to ACC_WIDTH bits would reproduce. Bit-exact
|
|
// equivalent of the previous arithmetic comparisons in every
|
|
// case (verified in sim/neuron_parallel_saturation_bounds_tb.v);
|
|
// no ACT_NONE/ACT_RELU behavior change. NOTE (measured, see
|
|
// WORKLOG.md): on this device/toolchain, Yosys's abc9 mapper
|
|
// maps this wide AND/OR reduction onto CCU2C carry-chain cells
|
|
// too, not a shallow LUT tree -- so on its own this rewrite only
|
|
// trims the chain slightly (fewer CCU2C hops, ~0.2-0.9ns less
|
|
// logic delay measured) rather than eliminating it; kept anyway
|
|
// as a genuine, bit-exact-verified simplification, with the
|
|
// pipeline stage above doing the real timing-closure work.
|
|
// ============================================================
|
|
|
|
wire final_acc_sign = final_acc[ACC_WIDTH-1];
|
|
wire final_acc_upper_all0 = ~(|final_acc[ACC_WIDTH-1:DATA_WIDTH-1]);
|
|
wire final_acc_upper_all1 = &final_acc[ACC_WIDTH-1:DATA_WIDTH-1];
|
|
wire final_acc_in_range = final_acc_upper_all0 | final_acc_upper_all1;
|
|
wire final_acc_le_zero = final_acc_sign | ~(|final_acc);
|
|
|
|
// ACT_NONE: saturate to [-2^(DATA_WIDTH-1), 2^(DATA_WIDTH-1)-1]
|
|
wire signed [DATA_WIDTH-1:0] y_none =
|
|
final_acc_in_range ? final_acc[DATA_WIDTH-1:0]
|
|
: (final_acc_sign ? {1'b1, {(DATA_WIDTH-1){1'b0}}}
|
|
: {1'b0, {(DATA_WIDTH-1){1'b1}}});
|
|
|
|
// ACT_RELU: max(0, final_acc), then saturate positive
|
|
wire signed [DATA_WIDTH-1:0] y_relu =
|
|
final_acc_le_zero ? {DATA_WIDTH{1'b0}}
|
|
: (final_acc_upper_all0 ? final_acc[DATA_WIDTH-1:0]
|
|
: {1'b0, {(DATA_WIDTH-1){1'b1}}});
|
|
|
|
// `finishing`: one-cycle pipeline stage between the last MAC
|
|
// group's accumulate and the bias-add+saturate/activation step
|
|
// (see the `final_acc` comment above). `busy` stays high through
|
|
// it, so it is invisible to every existing start/busy/done
|
|
// caller -- just one extra clock of latency.
|
|
reg finishing;
|
|
|
|
always @(posedge clk) begin
|
|
|
|
if (rst) begin
|
|
|
|
group_index <= 0;
|
|
acc <= 0;
|
|
y <= 0;
|
|
busy <= 0;
|
|
done <= 0;
|
|
finishing <= 0;
|
|
|
|
end else begin
|
|
|
|
done <= 0;
|
|
|
|
if (start && !busy) begin
|
|
|
|
group_index <= 0;
|
|
acc <= 0;
|
|
busy <= 1;
|
|
finishing <= 0;
|
|
|
|
end else if (finishing) begin
|
|
|
|
case (activation)
|
|
|
|
ACT_NONE: y <= y_none; // linear: saturate both directions
|
|
|
|
default: y <= y_relu; // ACT_RELU (also the fallback for any reserved encoding)
|
|
|
|
endcase
|
|
|
|
busy <= 0;
|
|
done <= 1;
|
|
finishing <= 0;
|
|
|
|
end else if (busy) begin
|
|
|
|
if (group_index == groups_real[GROUP_INDEX_WIDTH-1:0] - 1'b1) begin
|
|
|
|
acc <= acc_next; // complete sum, bias not yet added
|
|
finishing <= 1;
|
|
|
|
end else begin
|
|
|
|
acc <= acc_next;
|
|
group_index <= group_index + 1'b1;
|
|
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
endmodule |