Files
FPGA-Neural/hardware/v2/synthesis/harness_neural_processor_array.v
micheleandClaude Sonnet 5 3026dcd997 feat(v2): M2 Neural Processor Array, N_PROCESSORS resource sweep
Implements M2 of the V2 roadmap: neural_processor_array.v instantiates
N_PROCESSORS independent neural_processor (M1) units, each with its
own dedicated point-to-point job/operand/result interface -- no shared
bus or mux at this level (arbitration is explicitly the Neural
Director's job, M5).

Verified with Verilator (tb_neural_processor_array.v, N_PROCESSORS=4):
7/7 tests pass, including a same-cycle 4-way concurrent launch with
different tile counts and a staggered-start test where a
later-launched, shorter job completes before an earlier-launched,
longer one -- confirming genuine independent concurrent execution
(§18/§34: a blocked/busy processor must not block the others).

Real resource/timing sweep for N_PROCESSORS in {1,2,4,8} (Yosys +
nextpnr-ecp5, real place&route): Fmax stays above the 80MHz target
throughout (159.11 -> 134.70 MHz), but MULT18X18D usage scales
linearly and reaches 88% of the LFE5U-45F's 72 DSPs at N=8 while
LUT/FF stay under 6% -- DSP, not LUT/FF/routing, is the first hard
ceiling on N_PROCESSORS at P_IN=8 (decisions.log DEC-0005). Measured
via a dedicated synthesis-only timing harness after the array's wide
per-processor buses were found to exhaust the device's TRELLIS_IO pin
budget as a bare top-level module beyond N=1 (errors.log ERR-0005) --
not a logic limit, an artifact of testing the array in isolation
before the Memory Manager/Director (M4/M5) exist to consume those
ports on-chip.

Full log trail (development/experiments/errors/decisions/simulation/
synthesis/timing/benchmark.log) in hardware/v2/logs/ per the project's
logging mandate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-05 14:15:11 +02:00

127 lines
6.1 KiB
Verilog

// ============================================================
// SYNTHESIS-ONLY TIMING HARNESS -- NOT a functional deliverable.
//
// neural_processor_array.v's real ports (per-processor wide
// input_data/weight_data buses, per-processor job/result fields) are
// meant to be driven on-chip by the Memory Manager (M4) and Neural
// Director (M5), which do not exist yet. Synthesizing the array
// standalone with every one of those bits exposed as a real
// TRELLIS_IO pin exhausts the ECP5's ~245 available I/O well before
// N_PROCESSORS=2 (see hardware/v2/logs/errors.log ERR-0005) -- a
// packaging artifact of this specific isolated measurement, NOT a
// logic/timing limit of the array itself.
//
// This harness replaces the wide external buses with an internal
// free-running LFSR (so the data ports are not synthesized away as
// constants) and reduces the outputs to a small XOR-reduced checksum,
// keeping only clk/rst/seed/checksum as real top-level pins. This
// gives a representative Fmax for the array's OWN logic/routing
// congestion, uninflated and unconstrained by an artificial pin
// budget that will not exist once M4/M5 land.
// ============================================================
module harness_neural_processor_array #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter N_PROCESSORS = 4
)(
input clk,
input rst,
input [7:0] seed,
output [7:0] checksum
);
reg [31:0] lfsr;
always @(posedge clk) begin
if (rst) lfsr <= {24'h0, seed} | 32'h1;
else lfsr <= {lfsr[30:0], lfsr[31] ^ lfsr[21] ^ lfsr[1] ^ lfsr[0]};
end
// Each processor gets a DISTINCT data slice (LFSR rotated by its
// own index) so Yosys cannot common-subexpression-eliminate
// N_PROCESSORS identical instances down to one -- that would
// silently defeat the point of the N_PROCESSORS sweep.
genvar hgi;
wire [N_PROCESSORS-1:0] job_valid;
wire [16*N_PROCESSORS-1:0] job_node_id;
wire signed [DATA_WIDTH*N_PROCESSORS-1:0] job_bias;
wire [2*N_PROCESSORS-1:0] job_activation;
wire [N_PROCESSORS-1:0] operand_valid;
wire signed [DATA_WIDTH*P_IN*N_PROCESSORS-1:0] input_data;
wire signed [DATA_WIDTH*P_IN*N_PROCESSORS-1:0] weight_data;
wire [N_PROCESSORS-1:0] tile_last;
wire [N_PROCESSORS-1:0] result_ready;
genvar hgk;
generate
for (hgi = 0; hgi < N_PROCESSORS; hgi = hgi + 1) begin : GEN_HARNESS_LANE
wire [31:0] rot = {lfsr[hgi:0], lfsr[31:hgi+1]};
assign job_valid[hgi] = rot[0];
assign job_node_id[hgi*16 +: 16] = rot[15:0];
assign job_bias[hgi*DATA_WIDTH +: DATA_WIDTH] = rot[7:0];
assign job_activation[hgi*2 +: 2] = rot[1:0];
assign operand_valid[hgi] = rot[2];
assign tile_last[hgi] = rot[3];
assign result_ready[hgi] = rot[4];
// Each of the P_IN MAC lanes WITHIN this processor also
// needs a distinct value -- otherwise all P_IN
// multiplications are identical and Yosys collapses them
// to a single shared MULT18X18D (observed: N=1 synthesized
// to just 1 multiplier instead of P_IN=8).
for (hgk = 0; hgk < P_IN; hgk = hgk + 1) begin : GEN_HARNESS_MAC_LANE
wire [31:0] lane_rot = {rot[hgk:0], rot[31:hgk+1]};
assign input_data[hgi*DATA_WIDTH*P_IN + hgk*DATA_WIDTH +: DATA_WIDTH] = lane_rot[7:0];
assign weight_data[hgi*DATA_WIDTH*P_IN + hgk*DATA_WIDTH +: DATA_WIDTH] = lane_rot[15:8];
end
end
endgenerate
wire [N_PROCESSORS-1:0] job_ready;
wire [N_PROCESSORS-1:0] operand_ready;
wire [N_PROCESSORS-1:0] result_valid;
wire signed [DATA_WIDTH*N_PROCESSORS-1:0] result_data;
wire [16*N_PROCESSORS-1:0] result_node_id;
wire [4*N_PROCESSORS-1:0] np_state;
wire [N_PROCESSORS-1:0] np_error;
neural_processor_array #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.N_PROCESSORS(N_PROCESSORS)
) dut (
.clk(clk), .rst(rst),
.job_valid(job_valid), .job_ready(job_ready),
.job_node_id(job_node_id), .job_bias(job_bias), .job_activation(job_activation),
.operand_valid(operand_valid), .operand_ready(operand_ready),
.input_data(input_data), .weight_data(weight_data), .tile_last(tile_last),
.result_valid(result_valid), .result_ready(result_ready),
.result_data(result_data), .result_node_id(result_node_id),
.np_state(np_state), .np_error(np_error)
);
// Fold in a real bit from EVERY processor's wide outputs
// (result_data/result_node_id/np_state), not just processor 0's
// slice -- otherwise the arithmetic datapath of every processor
// but one has no observable path to any output at all, and Yosys
// correctly (from a pure logic-equivalence standpoint) strips it
// out as dead logic, silently defeating the N_PROCESSORS sweep.
wire [N_PROCESSORS-1:0] result_data_lsb;
wire [N_PROCESSORS-1:0] result_node_id_lsb;
wire [N_PROCESSORS-1:0] np_state_lsb;
generate
for (hgi = 0; hgi < N_PROCESSORS; hgi = hgi + 1) begin : GEN_CHK_LANE
assign result_data_lsb[hgi] = result_data[hgi*DATA_WIDTH];
assign result_node_id_lsb[hgi] = result_node_id[hgi*16];
assign np_state_lsb[hgi] = np_state[hgi*4];
end
endgenerate
reg [7:0] chk;
always @(posedge clk) begin
if (rst) chk <= 8'h0;
else chk <= chk ^ job_ready ^ operand_ready ^ result_valid ^ np_error
^ result_data_lsb ^ result_node_id_lsb ^ np_state_lsb;
end
assign checksum = chk;
endmodule