`timescale 1ns/1ps // ============================================================ // V3 -- packed_pe.v: real weight-sharing PE for a systolic_group.v // (docs/ARCHITECTURE_ANALYSIS.md S5.6, EXP-0089), derived directly from // packed_slot.v -- SAME activation-fetch (ddr_prefetch_mgr.v), compute // (neural_processor_packed.v), and result-writeback (result_writeback.v) // subsystems, unmodified. The ONLY real difference: this module does // NOT own a layer_prefetch_ctrl.v/layer_weight_buffer.v/weight_tile_ // gather.v of its own -- weight tile data is received via a real, // broadcast, GROUP-shared interface instead (group_tcnt/group_tile_ // data/group_tile_valid), since the whole point of grouping 4 PEs is // ONE real weight fetch shared by all 4 (docs' own quantified // rationale: DDR3 requester count reduction), not 4 independent ones. // // REAL SYNCHRONIZATION (the part that needed real design, not a // trivial extension -- see docs S5.6's own "not a trivial extension" // disclosure): this PE's own `tcnt` IS the join key. It waits in // S_TILEWAIT for `group_tile_valid && (group_tcnt == tcnt)` before // consuming -- a real, self-synchronizing comparison, not a bare // pulse/level race. A PE that's briefly slower than its groupmates // (e.g. its own activation fetch hit a real DDR3 row switch the others // didn't) simply keeps waiting; when it finally reaches S_TILEWAIT for // its own `tcnt`, the comparison is either already true (if the group // had to wait for THIS PE, i.e. this PE IS the slow one) or becomes // true the moment the group's own barrier (systolic_group.v, all 4 // pe_tile_ack seen) lets it advance -- correct regardless of which PE // is momentarily ahead or behind, no risk of double-consuming or // skipping a tile. // // group_n_tiles (broadcast, held stable for the whole job -- all 4 PEs // in a group process the SAME layer, same real weight-stationary // premise this module's own name comes from) replaces packed_slot.v's // own per-instance n_tiles input. // ============================================================ module packed_pe #( parameter DATA_WIDTH = 8, parameter P_IN = 8, parameter ACC_WIDTH = 32, parameter BURST_LEN = 8, parameter ADDR_WIDTH = 26 )( input wire clk, input wire rst, // ---- own per-PE job trigger: x_base_a/b (this PE's own two // activation positions), result_addr_a/b, node_id_a/b -- w_base and // n_tiles are NOT here, they're group-broadcast (see group_n_tiles // below; w_base never reaches this module at all, only the group // controller needs it). ---- input wire job_start, input wire [ADDR_WIDTH-1:0] x_base_a, input wire [ADDR_WIDTH-1:0] x_base_b, input wire [ADDR_WIDTH-1:0] result_addr_a, input wire [ADDR_WIDTH-1:0] result_addr_b, input wire [15:0] node_id_a, input wire [15:0] node_id_b, output reg job_done, // one-cycle pulse output reg signed [DATA_WIDTH-1:0] result_data_a, output reg signed [DATA_WIDTH-1:0] result_data_b, output reg [15:0] result_node_id_a, output reg [15:0] result_node_id_b, output reg [ADDR_WIDTH-1:0] result_addr_a_out, output reg [ADDR_WIDTH-1:0] result_addr_b_out, // ---- group-broadcast weight interface (real, level-held-until- // consumed, same discipline ddr_prefetch_mgr.v's own tile_valid // already established) ---- input wire [15:0] group_n_tiles, input wire [15:0] group_tcnt, input wire [DATA_WIDTH*P_IN-1:0] group_tile_data, input wire group_tile_valid, output reg pe_tile_ack, // one-shot pulse // ---- own activation-fetch + result-writeback arbiter port (still // one per PE -- activation data is NOT shared across PEs, each PE // computes different positions) ---- 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 ); localparam S_IDLE = 3'd0, S_JOBSTART = 3'd1, S_TILEWAIT = 3'd2, S_OPERAND = 3'd3, S_RESULT = 3'd4, S_WRITEBACK = 3'd5; reg [2:0] state; reg [ADDR_WIDTH-1:0] x_base_a_lat, x_base_b_lat; reg [15:0] n_tiles_lat; reg [ADDR_WIDTH-1:0] result_addr_a_lat, result_addr_b_lat; reg [15:0] node_id_a_lat, node_id_b_lat; reg [15:0] tcnt; // ---- ddr_prefetch_mgr.v: own activation-tile look-ahead, exactly // as packed_slot.v already uses it ---- reg ddrpf_job_start; wire ddrpf_tile_valid; reg ddrpf_tile_consume; wire signed [DATA_WIDTH*P_IN-1:0] act_data_a_w, act_data_b_w; wire act_mem_active; wire act_ctrl_req, act_ctrl_wr; wire [ADDR_WIDTH-2:0] act_ctrl_addr; wire [32*BURST_LEN-1:0] act_ctrl_wdata; wire [4*BURST_LEN-1:0] act_ctrl_wmask; ddr_prefetch_mgr #( .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1) ) u_ddrpf ( .clk(clk), .rst(rst), .job_start(ddrpf_job_start), .base_a(x_base_a_lat[ADDR_WIDTH-2:0]), .base_b(x_base_b_lat[ADDR_WIDTH-2:0]), .n_tiles(n_tiles_lat), .tile_valid(ddrpf_tile_valid), .data_a(act_data_a_w), .data_b(act_data_b_w), .tile_consume(ddrpf_tile_consume), .mem_active(act_mem_active), .mem_grant(mem_grant), .ctrl_req(act_ctrl_req), .ctrl_wr(act_ctrl_wr), .ctrl_addr(act_ctrl_addr), .ctrl_wdata(act_ctrl_wdata), .ctrl_wmask(act_ctrl_wmask), .ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy) ); // ---- result_writeback.v: own result, own DDR3 write, exactly as // packed_slot.v already uses it ---- reg wb_start; wire wb_busy, wb_done; wire wb_mem_active; wire wb_ctrl_req, wb_ctrl_wr; wire [ADDR_WIDTH-2:0] wb_ctrl_addr; wire [32*BURST_LEN-1:0] wb_ctrl_wdata; wire [4*BURST_LEN-1:0] wb_ctrl_wmask; result_writeback #( .BURST_LEN(BURST_LEN), .DATA_WIDTH(DATA_WIDTH), .JOB_ADDR_WIDTH(ADDR_WIDTH), .ADDR_WIDTH(ADDR_WIDTH-1) ) u_wb ( .clk(clk), .rst(rst), .start(wb_start), .result_addr_a(result_addr_a_lat), .result_addr_b(result_addr_b_lat), .result_data_a(result_data_a), .result_data_b(result_data_b), .result_node_id_a(result_node_id_a), .result_node_id_b(result_node_id_b), .busy(wb_busy), .done(wb_done), .mem_active(wb_mem_active), .mem_grant(mem_grant), .ctrl_req(wb_ctrl_req), .ctrl_wr(wb_ctrl_wr), .ctrl_addr(wb_ctrl_addr), .ctrl_wdata(wb_ctrl_wdata), .ctrl_wmask(wb_ctrl_wmask), .ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy) ); // mutually exclusive by FSM construction (activation fetch always // finishes each tile's own consume before writeback ever starts, // and writeback only starts once the whole tile loop is done) -- // 2-way mux, one fewer branch than packed_slot.v's own 3-way (no // local weight-fetch mux here, weight is group-broadcast). assign ctrl_req = act_mem_active ? act_ctrl_req : wb_ctrl_req; assign ctrl_wr = act_mem_active ? act_ctrl_wr : wb_ctrl_wr; assign ctrl_addr = act_mem_active ? act_ctrl_addr : wb_ctrl_addr; assign ctrl_wdata = act_mem_active ? act_ctrl_wdata : wb_ctrl_wdata; assign ctrl_wmask = act_mem_active ? act_ctrl_wmask : wb_ctrl_wmask; assign mem_active = act_mem_active || wb_mem_active; // ---- neural_processor_packed.v (unmodified, same as packed_slot.v) ---- reg job_valid_np; wire job_ready_np; reg [1:0] job_activation; reg signed [DATA_WIDTH-1:0] job_bias; reg operand_valid; wire operand_ready; reg signed [DATA_WIDTH*P_IN-1:0] input_data_a_r, input_data_b_r; reg [DATA_WIDTH*P_IN-1:0] weight_data_r; reg tile_last; wire result_valid_np; reg result_ready; wire signed [DATA_WIDTH-1:0] result_data_a_np, result_data_b_np; wire [15:0] result_node_id_a_np, result_node_id_b_np; wire [3:0] np_state; wire np_error; neural_processor_packed #( .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH) ) u_np ( .clk(clk), .rst(rst), .job_valid(job_valid_np), .job_ready(job_ready_np), .job_node_id_a(node_id_a_lat), .job_node_id_b(node_id_b_lat), .job_bias(job_bias), .job_activation(job_activation), .operand_valid(operand_valid), .operand_ready(operand_ready), .input_data_a(input_data_a_r), .input_data_b(input_data_b_r), .weight_data(weight_data_r), .tile_last(tile_last), .result_valid(result_valid_np), .result_ready(result_ready), .result_data_a(result_data_a_np), .result_data_b(result_data_b_np), .result_node_id_a(result_node_id_a_np), .result_node_id_b(result_node_id_b_np), .np_state(np_state), .np_error(np_error) ); localparam ACT_RELU = 2'd1; always @(posedge clk) begin if (rst) begin state <= S_IDLE; job_done <= 1'b0; ddrpf_job_start <= 1'b0; ddrpf_tile_consume <= 1'b0; job_valid_np <= 1'b0; operand_valid<= 1'b0; tile_last <= 1'b0; result_ready <= 1'b0; wb_start <= 1'b0; pe_tile_ack <= 1'b0; job_bias <= {DATA_WIDTH{1'b0}}; job_activation <= ACT_RELU; tcnt <= 16'd0; end else begin job_done <= 1'b0; ddrpf_job_start <= 1'b0; ddrpf_tile_consume <= 1'b0; wb_start <= 1'b0; pe_tile_ack <= 1'b0; case (state) S_IDLE: begin if (job_start) begin x_base_a_lat <= x_base_a; x_base_b_lat <= x_base_b; n_tiles_lat <= group_n_tiles; result_addr_a_lat <= result_addr_a; result_addr_b_lat <= result_addr_b; node_id_a_lat <= node_id_a; node_id_b_lat <= node_id_b; job_bias <= {DATA_WIDTH{1'b0}}; job_activation <= ACT_RELU; tcnt <= 16'd0; job_valid_np <= 1'b1; state <= S_JOBSTART; end end // real, proven sequencing (matches packed_slot.v's own // S_JOBSTART exactly): only kick off the activation // look-ahead loop and enter the tile-consumption join // AFTER neural_processor_packed.v has actually accepted // the job -- issuing operand_valid before job_ready_np // would race its own internal job-acceptance state. S_JOBSTART: begin if (job_valid_np && job_ready_np) begin job_valid_np <= 1'b0; ddrpf_job_start <= 1'b1; // one-shot: kicks off this PE's // own activation look-ahead loop state <= S_TILEWAIT; end end // real join: group's broadcast weight tile (matched by // tcnt, see header) AND this PE's own activation fetch // -- same 2-source join shape packed_slot.v's own // S_TILEWAIT already uses, just with group_tile_valid+ // tcnt-match replacing the local tile_valid. S_TILEWAIT: begin if (group_tile_valid && (group_tcnt == tcnt) && ddrpf_tile_valid) begin weight_data_r <= group_tile_data; input_data_a_r <= act_data_a_w; input_data_b_r <= act_data_b_w; pe_tile_ack <= 1'b1; ddrpf_tile_consume <= 1'b1; tile_last <= (tcnt == n_tiles_lat - 16'd1); operand_valid <= 1'b1; state <= S_OPERAND; end end S_OPERAND: begin if (operand_valid && operand_ready) begin operand_valid <= 1'b0; tile_last <= 1'b0; if (tcnt == n_tiles_lat - 16'd1) begin result_ready <= 1'b1; state <= S_RESULT; end else begin tcnt <= tcnt + 16'd1; state <= S_TILEWAIT; end end end S_RESULT: begin if (result_valid_np) begin result_data_a <= result_data_a_np; result_data_b <= result_data_b_np; result_node_id_a <= result_node_id_a_np; result_node_id_b <= result_node_id_b_np; result_addr_a_out <= result_addr_a_lat; result_addr_b_out <= result_addr_b_lat; result_ready <= 1'b0; wb_start <= 1'b1; state <= S_WRITEBACK; end end S_WRITEBACK: begin if (wb_done) begin job_done <= 1'b1; state <= S_IDLE; end end default: state <= S_IDLE; endcase end end endmodule