`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