Adds packed_pe.v (packed_slot.v's compute+activation-fetch+writeback subsystem, reusing ddr_prefetch_mgr.v/neural_processor_packed.v/ result_writeback.v completely unmodified, with its own private weight-fetch removed) and systolic_group.v (one real layer_prefetch_ ctrl.v+layer_weight_buffer.v+weight_tile_gather.v shared by 4x packed_pe.v via a real, barrier-synchronized broadcast bus). Real design choice confirmed with the user before writing any RTL (AskUserQuestion, concrete topology preview): shared-weight broadcast, not a literal PE-to-PE systolic shift register -- achieves the real, quantified rationale (4x reduction in redundant weight-fetch DDR3 traffic per group of 4 PEs) with much lower real risk than genuine inter-PE pipeline fill/drain. The real new design is the barrier: each PE's own tcnt is the join key against the group's broadcast tcnt, self-synchronizing regardless of which PE is momentarily ahead/behind (e.g. a real DDR3 row-switch stall on one PE's own activation fetch). Found and fixed a real bug during verification (not by inspection): the first full test run reported every result as undefined despite every control-flow signal tracing correctly -- root-caused via real signal tracing down to a 5-way test arbiter bus mis-sliced at the wrong slot offset (single-bit handshake buses happened to use a correct range and masked it from the control-flow trace; only the wide, byte-offset buses were wrong). Verified in isolation (tb_systolic_group.v, real Icarus xsim, real sdram_arbiter_n.v generalized to NUM_REQ=5 with zero changes): 8/8 PASS across 2 consecutive group jobs (exercising the barrier's own per-job reset path, not just cold start). Deliberately scoped to the isolated mechanism only, per this project's "one variable at a time" discipline -- Director/SPI job dispatch for group jobs, a real N=16 top-level, and real P&R are real, disclosed next steps, not done here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
323 lines
14 KiB
Verilog
323 lines
14 KiB
Verilog
`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
|