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
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// ============================================================
|
||||
// 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
|
||||
Reference in New Issue
Block a user