`timescale 1ns/1ps // ============================================================ // V3 -- systolic_group.v: real group controller for the "4 groups x // 4-PE weight-stationary chains" hybrid scaling direction // (docs/ARCHITECTURE_ANALYSIS.md S5.6, EXP-0089). User-confirmed real // design choice (2026-09-20, explicit A/B decision): SHARED-WEIGHT // BROADCAST, not a literal PE-to-PE systolic shift register -- one // real weight fetch per group of 4 PEs (instead of 4 independent // fetches), each PE then computes its own, independent activation // positions in parallel. This achieves the doc's own real, quantified // rationale (reduce redundant weight-fetch DDR3 traffic 4x per group) // with far less real risk/complexity than a literal systolic pipeline // -- no inter-PE result propagation, no pipeline fill/drain at chain // boundaries, both of which the doc's own S5.6 explicitly flagged as // "a real, new design, not a trivial extension". // // STRUCTURE: ONE real layer_prefetch_ctrl.v + layer_weight_buffer.v + // weight_tile_gather.v (unmodified, identical instances to what // packed_slot.v already owns per-slot -- just now group-level, shared // by reference not duplicated), driving 4x packed_pe.v (packed_slot.v // minus its own weight-fetch, see packed_pe.v's own header) via a // real, level-held, tcnt-tagged broadcast bus. // // REAL BARRIER SYNCHRONIZATION (the actual new design, not asserted // correct without real verification -- see tb_systolic_group.v): the // group only advances to tile N+1's weight fetch once ALL 4 PEs have // ack'd tile N (`pe_tile_ack[i]`, individually latched since PEs may // consume at different real cycles -- e.g. one PE's own activation // fetch hit a real DDR3 row switch the others didn't). This is a real, // necessary extension of the "S_TILEWAIT join" discipline every other // module in this project already uses for 2-source joins (weight + // activation) -- here it's a 4-way join (one group weight source, 4 // independent PE acks), same underlying principle: never advance past // a shared resource until every real consumer has confirmed it read // what it needed. // ============================================================ module systolic_group #( parameter DATA_WIDTH = 8, parameter P_IN = 8, parameter ACC_WIDTH = 32, parameter BURST_LEN = 8, parameter ADDR_WIDTH = 26, parameter LAYER_BYTES = 128, parameter BUFADDRW = $clog2(LAYER_BYTES) )( input wire clk, input wire rst, // ---- group-level job dispatch: ONE shared w_base/n_tiles (all 4 // PEs process the SAME layer, weight-stationary), 4x independent // per-PE x_base_a/b/result_addr_a/b/node_id_a/b ---- input wire job_start, input wire [ADDR_WIDTH-1:0] w_base, input wire [15:0] n_tiles, input wire [4*ADDR_WIDTH-1:0] pe_x_base_a, pe_x_base_b, input wire [4*ADDR_WIDTH-1:0] pe_result_addr_a, pe_result_addr_b, input wire [4*16-1:0] pe_node_id_a, pe_node_id_b, output reg job_done, // one-cycle pulse, ALL 4 PEs done output wire [4*DATA_WIDTH-1:0] pe_result_data_a, pe_result_data_b, output wire [4*16-1:0] pe_result_node_id_a, pe_result_node_id_b, output wire [4*ADDR_WIDTH-1:0] pe_result_addr_a_out, pe_result_addr_b_out, // ---- group's own single arbiter port, for the shared weight // fetch only -- each of the 4 PEs still owns ITS OWN separate // arbiter port for activation-fetch+writeback (see top-level // integration; not this module's own concern) ---- output wire mem_active, input wire mem_grant, output wire ctrl_req, output wire ctrl_wr, output wire [ADDR_WIDTH-2:0] ctrl_addr, output wire [32*BURST_LEN-1:0] ctrl_wdata, output wire [4*BURST_LEN-1:0] ctrl_wmask, input wire [32*BURST_LEN-1:0] ctrl_rdata, input wire ctrl_ready, input wire ctrl_busy, // ---- 4 independent per-PE arbiter ports (activation-fetch + // result-writeback, NOT shared -- flattened NUM_PE*width buses) ---- output wire [3:0] pe_mem_active, input wire [3:0] pe_mem_grant, output wire [3:0] pe_ctrl_req, output wire [3:0] pe_ctrl_wr, output wire [4*(ADDR_WIDTH-1)-1:0] pe_ctrl_addr, output wire [4*32*BURST_LEN-1:0] pe_ctrl_wdata, output wire [4*4*BURST_LEN-1:0] pe_ctrl_wmask, input wire [4*32*BURST_LEN-1:0] pe_ctrl_rdata, input wire [3:0] pe_ctrl_ready, input wire [3:0] pe_ctrl_busy ); localparam S_IDLE = 4'd0, S_MEMWAIT = 4'd1, S_PREFETCH = 4'd2, S_SWAP = 4'd3, S_PEJOBSTART = 4'd4, S_TILELOOP = 4'd5, S_WAITDONE = 4'd6; reg [3:0] state; reg [ADDR_WIDTH-1:0] w_base_lat; reg [15:0] n_tiles_lat; reg [15:0] tcnt; // ---- shared weight fetch: layer_prefetch_ctrl.v -> layer_weight_ // buffer.v -> weight_tile_gather.v, IDENTICAL instances to what // packed_slot.v already owns per-slot, just group-level now ---- reg pf_start; wire pf_busy, pf_done; wire pf_fill_we; wire [BUFADDRW-1:0] pf_fill_addr; wire [DATA_WIDTH-1:0] pf_fill_data; reg consume_done; layer_prefetch_ctrl #( .DATA_WIDTH(DATA_WIDTH), .LAYER_BYTES(LAYER_BYTES), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1) ) u_pf ( .clk(clk), .rst(rst), .start(pf_start), .layer_base(w_base_lat[ADDR_WIDTH-2:0]), .busy(pf_busy), .done(pf_done), .fill_we(pf_fill_we), .fill_addr(pf_fill_addr), .fill_data(pf_fill_data), .ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr), .ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask), .ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy) ); assign mem_active = (state == S_MEMWAIT) || (state == S_PREFETCH); wire [BUFADDRW-1:0] lwb_rd_addr; wire [DATA_WIDTH-1:0] lwb_rd_data; layer_weight_buffer #(.DATA_WIDTH(DATA_WIDTH), .LAYER_DEPTH(LAYER_BYTES)) u_lwb ( .clk(clk), .rst(rst), .fill_we(pf_fill_we), .fill_addr(pf_fill_addr), .fill_data(pf_fill_data), .fill_done(pf_done), .rd_addr(lwb_rd_addr), .rd_data(lwb_rd_data), .consume_done(consume_done), .active_sel(), .swapped() ); reg tile_req; reg [BUFADDRW-1:0] tile_base; wire tile_valid; wire [DATA_WIDTH*P_IN-1:0] tile_data; wire [15:0] tcnt_next = tcnt + 16'd1; weight_tile_gather #( .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BUFADDRW(BUFADDRW) ) u_gather ( .clk(clk), .rst(rst), .tile_req(tile_req), .tile_base(tile_base), .tile_valid(tile_valid), .tile_data(tile_data), .rd_addr(lwb_rd_addr), .rd_data(lwb_rd_data) ); // ---- real broadcast bus to all 4 PEs (level-held, tcnt-tagged -- // see packed_pe.v's own header for the real join discipline) ---- reg group_tile_valid; reg [DATA_WIDTH*P_IN-1:0] group_tile_data_r; reg [3:0] pe_acked; // per-PE ack latch, cleared each tile wire [3:0] pe_job_start_w = {4{(state == S_PEJOBSTART)}}; wire [3:0] pe_job_done_w; wire [3:0] pe_acked_pulse; genvar gi; generate for (gi = 0; gi < 4; gi = gi + 1) begin : GEN_PE packed_pe #( .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH) ) u_pe ( .clk(clk), .rst(rst), .job_start(pe_job_start_w[gi]), .x_base_a(pe_x_base_a[gi*ADDR_WIDTH +: ADDR_WIDTH]), .x_base_b(pe_x_base_b[gi*ADDR_WIDTH +: ADDR_WIDTH]), .result_addr_a(pe_result_addr_a[gi*ADDR_WIDTH +: ADDR_WIDTH]), .result_addr_b(pe_result_addr_b[gi*ADDR_WIDTH +: ADDR_WIDTH]), .node_id_a(pe_node_id_a[gi*16 +: 16]), .node_id_b(pe_node_id_b[gi*16 +: 16]), .job_done(pe_job_done_w[gi]), .result_data_a(pe_result_data_a[gi*DATA_WIDTH +: DATA_WIDTH]), .result_data_b(pe_result_data_b[gi*DATA_WIDTH +: DATA_WIDTH]), .result_node_id_a(pe_result_node_id_a[gi*16 +: 16]), .result_node_id_b(pe_result_node_id_b[gi*16 +: 16]), .result_addr_a_out(pe_result_addr_a_out[gi*ADDR_WIDTH +: ADDR_WIDTH]), .result_addr_b_out(pe_result_addr_b_out[gi*ADDR_WIDTH +: ADDR_WIDTH]), .group_n_tiles(n_tiles_lat), .group_tcnt(tcnt), .group_tile_data(group_tile_data_r), .group_tile_valid(group_tile_valid), .pe_tile_ack(pe_acked_pulse[gi]), .mem_active(pe_mem_active[gi]), .mem_grant(pe_mem_grant[gi]), .ctrl_req(pe_ctrl_req[gi]), .ctrl_wr(pe_ctrl_wr[gi]), .ctrl_addr(pe_ctrl_addr[gi*(ADDR_WIDTH-1) +: (ADDR_WIDTH-1)]), .ctrl_wdata(pe_ctrl_wdata[gi*32*BURST_LEN +: 32*BURST_LEN]), .ctrl_wmask(pe_ctrl_wmask[gi*4*BURST_LEN +: 4*BURST_LEN]), .ctrl_rdata(pe_ctrl_rdata[gi*32*BURST_LEN +: 32*BURST_LEN]), .ctrl_ready(pe_ctrl_ready[gi]), .ctrl_busy(pe_ctrl_busy[gi]) ); end endgenerate reg [3:0] pe_done_latch; // combinational "what pe_acked/pe_done_latch would be if we also // fold in THIS cycle's own pulses" -- used both to DECIDE the // barrier this cycle (no extra latency) and, explicitly, as what // gets written back when the barrier hasn't cleared yet. Kept as // named wires (not relying on nonblocking-assignment-order // last-write-wins semantics) so the real intent is unambiguous to // a future reader, not just technically correct. wire [3:0] pe_acked_next = pe_acked | pe_acked_pulse; wire [3:0] pe_done_latch_next = pe_done_latch | pe_job_done_w; always @(posedge clk) begin if (rst) begin state <= S_IDLE; job_done <= 1'b0; pf_start <= 1'b0; consume_done <= 1'b0; tile_req <= 1'b0; group_tile_valid <= 1'b0; pe_acked <= 4'b0; pe_done_latch <= 4'b0; tcnt <= 16'd0; end else begin job_done <= 1'b0; pf_start <= 1'b0; consume_done <= 1'b0; tile_req <= 1'b0; // real race, deliberately handled: a PE's own job_done can // pulse the SAME cycle S_TILELOOP's barrier clears for the // LAST tile (i.e. the same cycle the group transitions to // S_WAITDONE) -- its own downstream writeback can complete // before the group has even formally entered S_WAITDONE. // Accumulate unconditionally, every cycle, so no early // pe_job_done_w pulse is ever missed; S_WAITDONE's own // success branch below explicitly overrides this back to 0 // (Verilog's own last-nonblocking-write-wins rule within // one always block -- intentional here, unlike pe_acked's // window which is fully contained inside S_TILELOOP and so // uses the more explicit if/else form instead). pe_done_latch <= pe_done_latch_next; case (state) S_IDLE: begin if (job_start) begin w_base_lat <= w_base; n_tiles_lat <= n_tiles; tcnt <= 16'd0; state <= S_MEMWAIT; end end S_MEMWAIT: begin if (mem_grant) begin pf_start <= 1'b1; state <= S_PREFETCH; end end S_PREFETCH: begin if (pf_done) begin consume_done <= 1'b1; state <= S_SWAP; end end S_SWAP: begin // one settle cycle, same real reason packed_slot.v's // own S_SWAP exists (layer_weight_buffer.v's do_swap). state <= S_PEJOBSTART; end S_PEJOBSTART: begin // pe_job_start_w is combinational on (state == // S_PEJOBSTART), so all 4 PEs see job_start the // SAME cycle -- real, established one-shot-pulse // discipline (EXP-0066), now applied 4-way. tile_req <= 1'b1; tile_base <= tcnt[BUFADDRW-1:0]*P_IN[BUFADDRW-1:0]; state <= S_TILELOOP; end // real barrier: hold this tile's data broadcast until // ALL 4 PEs have ack'd it (pe_acked_next all-ones), THEN // advance tcnt and issue the next weight_tile_gather // request -- see this module's own header for why a // bare pulse/level race would be unsafe here. pe_acked // is explicitly written on EVERY path through this // state (either reset to 0 when the barrier clears, or // folded forward via pe_acked_next otherwise) -- never // relies on assignment order elsewhere in the block. S_TILELOOP: begin if (tile_valid && !group_tile_valid) begin group_tile_data_r <= tile_data; group_tile_valid <= 1'b1; end if (group_tile_valid && (&pe_acked_next)) begin group_tile_valid <= 1'b0; pe_acked <= 4'b0; if (tcnt == n_tiles_lat - 16'd1) begin state <= S_WAITDONE; end else begin tcnt <= tcnt_next; tile_req <= 1'b1; tile_base <= tcnt_next[BUFADDRW-1:0]*P_IN[BUFADDRW-1:0]; state <= S_TILELOOP; end end else begin pe_acked <= pe_acked_next; end end S_WAITDONE: begin if (&pe_done_latch_next) begin pe_done_latch <= 4'b0; job_done <= 1'b1; state <= S_IDLE; end end default: state <= S_IDLE; endcase end end endmodule