Files
FPGA-Neural/hardware/v3/rtl/neural_director_grouped.v
T
micheleandClaude Sonnet 5 6d06404428 feat: real N=4/N=8 timing curve -- N=8 REALLY CLOSES (0 failing endpoints, EXP-0095)
Fixed a real, previously-untested N_GROUPS=1 edge case in
neural_director_grouped.v (bare $clog2(N_GROUPS) invalid for
N_GROUPS=1, same class of bug sdram_arbiter_n.v's own SELW guard
already handles -- applied the same fix here and in
n16_system_ddr3_top.v's own job_out_group_w wire).

Real synth_design -generic N_GROUPS=<n> P&R sweep (same RTL/arbiter as
EXP-0094's real N=16 result):
  N=4  (N_GROUPS=1): WNS=-0.005ns, 2 failing endpoints
  N=8  (N_GROUPS=2): WNS=0.000ns, 0 failing endpoints -- REALLY CLOSED
  N=16 (N_GROUPS=4): WNS=-0.338ns, 60 failing endpoints (EXP-0094)

N=8 is a real, new, closed P&R signoff -- 8x N=2's parallelism with
the same already-verified systolic RTL. Answers the user's own
question directly: the design does not fail until somewhere between
N=8 and N=16, not at N=4 or N=8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
2026-09-21 07:56:19 +02:00

290 lines
14 KiB
Verilog

`timescale 1ns/1ps
// ================================================================
// V3 -- Neural Director, GROUPED variant (EXP-0089/EXP-0090), forked
// from neural_director_packed.v for dispatching to systolic_group.v
// instances instead of flat packed_slot.v instances.
//
// REAL, DIRECT EXTENSION of neural_director_packed.v's own already-
// proven pairing discipline -- NOT a redesign. That module dispatches
// the 2 OLDEST queue entries together, requiring them to share
// w_base/n_tiles (one packed core = 2 positions sharing one weight
// stream). This module dispatches the 8 OLDEST queue entries together
// (GROUP_SIZE=8, matching systolic_group.v's own real, fixed 4 PEs x
// 2 lanes each), requiring ALL EIGHT to share w_base/n_tiles -- same
// real reasoning, same real failure mode if violated (the queue simply
// stops draining, a visible, diagnosable symptom, never a silent
// mis-pair), just a wider match window.
//
// REAL, DELIBERATE NON-CHANGE: the host-facing job_in_* submission
// interface is BYTE-FOR-BYTE IDENTICAL to neural_director_packed.v's
// own -- one job descriptor (x_base/w_base/n_tiles/result_addr/
// node_id) per push, exactly like today. The ESP32/SPI protocol
// (spi_host_bridge_v3.v's own WRITE_JOB opcode) needs ZERO real
// changes to use this Director -- the host just submits 8 individual
// jobs sharing the same w_base/n_tiles instead of 2, exactly the same
// real submission pattern already required today, just a wider batch.
// This was a deliberate design goal, not an accident: keeping the
// host-facing contract unchanged means this Director can be swapped
// in without touching any already-verified host-side firmware
// contract or SPI opcode.
// ================================================================
module neural_director_grouped #(
parameter ADDR_WIDTH = 26,
parameter N_GROUPS = 4,
parameter QUEUE_DEPTH = 16,
// real fix (found via real synthesis, N_GROUPS=1 real N=4 scaling
// test): bare `$clog2(N_GROUPS)` is 0 for N_GROUPS=1, producing an
// invalid `[-1:0]` part-select everywhere below -- same real edge
// case sdram_arbiter_n.v's own `SELW` localparam already guards
// against, applied here too.
localparam GROUP_IDX_WIDTH = (N_GROUPS <= 1) ? 1 : $clog2(N_GROUPS)
)(
input wire clk,
input wire rst,
// ---- job submission: identical single-job-descriptor interface
// to neural_director_packed.v -- see header ----
input wire job_in_valid,
output wire job_in_ready,
input wire [ADDR_WIDTH-1:0] job_in_x_base,
input wire [ADDR_WIDTH-1:0] job_in_w_base,
input wire [15:0] job_in_n_tiles,
input wire [ADDR_WIDTH-1:0] job_in_result_addr,
input wire [15:0] job_in_node_id,
// ---- per-group job control (arrayed, N_GROUPS wide). Each group
// gets ONE shared w_base/n_tiles and 4 PEs' worth of x_base_a/b +
// result_addr_a/b + node_id_a/b (8 positions total) -- flattened
// as 4*ADDR_WIDTH / 4*16 buses, matching systolic_group.v's own
// real pe_x_base_a/pe_x_base_b/etc port shapes exactly. ----
output wire [N_GROUPS-1:0] group_job_start,
output wire [ADDR_WIDTH*N_GROUPS-1:0] group_w_base,
output wire [16*N_GROUPS-1:0] group_n_tiles,
output wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_x_base_a,
output wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_x_base_b,
output wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_result_addr_a,
output wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_result_addr_b,
output wire [4*16*N_GROUPS-1:0] group_pe_node_id_a,
output wire [4*16*N_GROUPS-1:0] group_pe_node_id_b,
input wire [N_GROUPS-1:0] group_job_done,
output reg job_out_done, // one-cycle pulse
output reg [GROUP_IDX_WIDTH-1:0] job_out_group,
output reg [3:0] dir_state,
output reg dir_error,
output wire queue_empty
);
localparam GROUP_SIZE = 8; // 4 PEs x 2 lanes each, matches systolic_group.v's own fixed shape
localparam DIR_IDLE = 4'd0;
localparam DIR_SCAN_READY = 4'd1;
localparam DIR_ALLOCATE = 4'd2;
localparam DIR_ERROR = 4'd3;
localparam Q_ADDR_WIDTH = $clog2(QUEUE_DEPTH);
reg [ADDR_WIDTH-1:0] q_x_base [0:QUEUE_DEPTH-1];
reg [ADDR_WIDTH-1:0] q_w_base [0:QUEUE_DEPTH-1];
reg [15:0] q_n_tiles [0:QUEUE_DEPTH-1];
reg [ADDR_WIDTH-1:0] q_result_addr [0:QUEUE_DEPTH-1];
reg [15:0] q_node_id [0:QUEUE_DEPTH-1];
reg [Q_ADDR_WIDTH-1:0] q_head, q_tail;
reg [Q_ADDR_WIDTH:0] q_count;
wire q_empty = (q_count == 0);
assign queue_empty = q_empty;
wire q_full = (q_count == QUEUE_DEPTH[Q_ADDR_WIDTH:0]);
wire q_has_octet = (q_count >= GROUP_SIZE[Q_ADDR_WIDTH:0]);
assign job_in_ready = !q_full;
// real wrapping index for the k-th oldest entry (k=0..7), same
// wrap-around style neural_director_packed.v's own q_head_plus1
// already established, generalized to an 8-wide offset table.
wire [Q_ADDR_WIDTH-1:0] q_idx [0:7];
genvar qk;
generate
for (qk = 0; qk < 8; qk = qk + 1) begin : GEN_QIDX
// real, deliberate width widening BEFORE the wrap compare --
// computing q_head+qk at only Q_ADDR_WIDTH bits could
// silently overflow/wrap in the addition itself (e.g.
// q_head=14, qk=7, QUEUE_DEPTH=16 needs 5 bits to represent
// 21 correctly before reducing mod 16), giving a WRONG
// index rather than an out-of-range one -- a real, silent
// correctness bug, not just a corner case to assume away.
wire [Q_ADDR_WIDTH:0] q_sum = {1'b0, q_head} + qk[Q_ADDR_WIDTH:0];
assign q_idx[qk] = (q_sum >= QUEUE_DEPTH[Q_ADDR_WIDTH:0])
? (q_sum - QUEUE_DEPTH[Q_ADDR_WIDTH:0])
: q_sum[Q_ADDR_WIDTH-1:0];
end
endgenerate
// the 8 oldest entries share a resident weight iff w_base AND
// n_tiles ALL match (checked pairwise against entry 0, same real
// reasoning as neural_director_packed.v's own pair_ready -- a
// coincidentally-equal w_base with mismatched n_tiles must not be
// wrongly accepted).
wire octet_match =
(q_w_base[q_idx[1]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[1]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[2]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[2]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[3]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[3]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[4]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[4]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[5]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[5]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[6]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[6]] == q_n_tiles[q_idx[0]]) &&
(q_w_base[q_idx[7]] == q_w_base[q_idx[0]]) && (q_n_tiles[q_idx[7]] == q_n_tiles[q_idx[0]]);
wire group_ready = q_has_octet && octet_match;
reg [N_GROUPS-1:0] group_busy;
wire [N_GROUPS-1:0] group_free = ~group_busy;
wire any_group_free = |group_free;
reg [GROUP_IDX_WIDTH-1:0] free_group_idx;
integer fi;
always @(*) begin
free_group_idx = {GROUP_IDX_WIDTH{1'b0}};
for (fi = N_GROUPS-1; fi >= 0; fi = fi - 1) begin
if (group_free[fi]) free_group_idx = fi[GROUP_IDX_WIDTH-1:0];
end
end
// per-group output storage -- N_GROUPS parallel constant-indexed
// writes (same anti-runtime-indexed-part-select discipline
// neural_director_packed.v's own slot_x_base_r already established).
reg group_job_start_r [0:N_GROUPS-1];
reg [ADDR_WIDTH-1:0] group_w_base_r [0:N_GROUPS-1];
reg [15:0] group_n_tiles_r [0:N_GROUPS-1];
reg [ADDR_WIDTH-1:0] group_pe_x_base_a_r [0:N_GROUPS-1][0:3];
reg [ADDR_WIDTH-1:0] group_pe_x_base_b_r [0:N_GROUPS-1][0:3];
reg [ADDR_WIDTH-1:0] group_pe_result_addr_a_r [0:N_GROUPS-1][0:3];
reg [ADDR_WIDTH-1:0] group_pe_result_addr_b_r [0:N_GROUPS-1][0:3];
reg [15:0] group_pe_node_id_a_r [0:N_GROUPS-1][0:3];
reg [15:0] group_pe_node_id_b_r [0:N_GROUPS-1][0:3];
genvar gg, gp;
generate
for (gg = 0; gg < N_GROUPS; gg = gg + 1) begin : GEN_GROUP_OUT
assign group_job_start[gg] = group_job_start_r[gg];
assign group_w_base[gg*ADDR_WIDTH +: ADDR_WIDTH] = group_w_base_r[gg];
assign group_n_tiles[gg*16 +: 16] = group_n_tiles_r[gg];
for (gp = 0; gp < 4; gp = gp + 1) begin : GEN_PE_OUT
assign group_pe_x_base_a[(gg*4+gp)*ADDR_WIDTH +: ADDR_WIDTH] = group_pe_x_base_a_r[gg][gp];
assign group_pe_x_base_b[(gg*4+gp)*ADDR_WIDTH +: ADDR_WIDTH] = group_pe_x_base_b_r[gg][gp];
assign group_pe_result_addr_a[(gg*4+gp)*ADDR_WIDTH +: ADDR_WIDTH] = group_pe_result_addr_a_r[gg][gp];
assign group_pe_result_addr_b[(gg*4+gp)*ADDR_WIDTH +: ADDR_WIDTH] = group_pe_result_addr_b_r[gg][gp];
assign group_pe_node_id_a[(gg*4+gp)*16 +: 16] = group_pe_node_id_a_r[gg][gp];
assign group_pe_node_id_b[(gg*4+gp)*16 +: 16] = group_pe_node_id_b_r[gg][gp];
end
end
endgenerate
reg [GROUP_IDX_WIDTH-1:0] done_group_idx;
integer di;
always @(*) begin
done_group_idx = {GROUP_IDX_WIDTH{1'b0}};
for (di = N_GROUPS-1; di >= 0; di = di - 1) begin
if (group_job_done[di]) done_group_idx = di[GROUP_IDX_WIDTH-1:0];
end
end
integer pi;
always @(posedge clk) begin
if (rst) begin
dir_state <= DIR_IDLE;
dir_error <= 1'b0;
q_head <= {Q_ADDR_WIDTH{1'b0}};
q_tail <= {Q_ADDR_WIDTH{1'b0}};
q_count <= {(Q_ADDR_WIDTH+1){1'b0}};
group_busy <= {N_GROUPS{1'b0}};
for (fi = 0; fi < N_GROUPS; fi = fi + 1) begin
group_job_start_r[fi] <= 1'b0;
group_w_base_r[fi] <= {ADDR_WIDTH{1'b0}};
group_n_tiles_r[fi] <= 16'b0;
for (pi = 0; pi < 4; pi = pi + 1) begin
group_pe_x_base_a_r[fi][pi] <= {ADDR_WIDTH{1'b0}};
group_pe_x_base_b_r[fi][pi] <= {ADDR_WIDTH{1'b0}};
group_pe_result_addr_a_r[fi][pi] <= {ADDR_WIDTH{1'b0}};
group_pe_result_addr_b_r[fi][pi] <= {ADDR_WIDTH{1'b0}};
group_pe_node_id_a_r[fi][pi] <= 16'b0;
group_pe_node_id_b_r[fi][pi] <= 16'b0;
end
end
job_out_done <= 1'b0;
job_out_group <= {GROUP_IDX_WIDTH{1'b0}};
end else begin
for (fi = 0; fi < N_GROUPS; fi = fi + 1) group_job_start_r[fi] <= 1'b0;
job_out_done <= 1'b0;
if (job_in_valid && job_in_ready) begin
q_x_base[q_tail] <= job_in_x_base;
q_w_base[q_tail] <= job_in_w_base;
q_n_tiles[q_tail] <= job_in_n_tiles;
q_result_addr[q_tail] <= job_in_result_addr;
q_node_id[q_tail] <= job_in_node_id;
q_tail <= (q_tail == QUEUE_DEPTH[Q_ADDR_WIDTH-1:0]-1'b1) ? {Q_ADDR_WIDTH{1'b0}} : q_tail + 1'b1;
end
group_busy <= group_busy & ~group_job_done;
if (|group_job_done) begin
job_out_done <= 1'b1;
job_out_group <= done_group_idx;
end
case (dir_state)
DIR_IDLE: begin
dir_state <= DIR_SCAN_READY;
end
DIR_SCAN_READY: begin
if (group_ready && any_group_free) begin
dir_state <= DIR_ALLOCATE;
end
end
DIR_ALLOCATE: begin
for (fi = 0; fi < N_GROUPS; fi = fi + 1) begin
if (fi[GROUP_IDX_WIDTH-1:0] == free_group_idx) begin
group_job_start_r[fi] <= 1'b1;
group_w_base_r[fi] <= q_w_base[q_idx[0]]; // all 8 match, checked by group_ready
group_n_tiles_r[fi] <= q_n_tiles[q_idx[0]];
for (pi = 0; pi < 4; pi = pi + 1) begin
group_pe_x_base_a_r[fi][pi] <= q_x_base[q_idx[pi*2]];
group_pe_x_base_b_r[fi][pi] <= q_x_base[q_idx[pi*2+1]];
group_pe_result_addr_a_r[fi][pi] <= q_result_addr[q_idx[pi*2]];
group_pe_result_addr_b_r[fi][pi] <= q_result_addr[q_idx[pi*2+1]];
group_pe_node_id_a_r[fi][pi] <= q_node_id[q_idx[pi*2]];
group_pe_node_id_b_r[fi][pi] <= q_node_id[q_idx[pi*2+1]];
end
end
end
group_busy[free_group_idx] <= 1'b1;
q_head <= q_idx[7] + 1'b1 == QUEUE_DEPTH[Q_ADDR_WIDTH-1:0]
? {Q_ADDR_WIDTH{1'b0}} : q_idx[7] + 1'b1;
dir_state <= DIR_SCAN_READY;
end
DIR_ERROR: begin
end
default: dir_state <= DIR_ERROR;
endcase
// q_count: +1 per accepted push, -8 per dispatched OCTET
case ({job_in_valid && job_in_ready,
(dir_state == DIR_SCAN_READY) && group_ready && any_group_free})
2'b10: q_count <= q_count + 1'b1;
2'b01: q_count <= q_count - GROUP_SIZE[Q_ADDR_WIDTH:0];
2'b11: q_count <= q_count - GROUP_SIZE[Q_ADDR_WIDTH:0] + 1'b1;
2'b00: q_count <= q_count;
endcase
end
end
endmodule