feat: real first step of 4x4 hybrid systolic architecture (EXP-0089)
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
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
`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
|
||||
Reference in New Issue
Block a user