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
293 lines
13 KiB
Verilog
293 lines
13 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// EXP-0089 -- isolated correctness test for systolic_group.v: one
|
|
// group of 4 packed_pe.v instances sharing ONE real weight fetch
|
|
// (broadcast, not a literal PE-to-PE systolic shift register -- see
|
|
// systolic_group.v's own header for the real, user-confirmed design
|
|
// choice). Same real discipline as every other new module in this
|
|
// project (act_tile_fetch.v EXP-0079, ddr_prefetch_mgr.v EXP-0083,
|
|
// result_writeback.v EXP-0088): verify in isolation, with a real
|
|
// memory backend and a real, independently-reproduced golden model,
|
|
// BEFORE any Director/SPI-protocol integration.
|
|
//
|
|
// Real backend: burst_mem_model32.v (same EXP-0084 model every other
|
|
// v3 isolated testbench uses) + sdram_arbiter_n.v with NUM_REQ=5 (1
|
|
// group-level weight-fetch requester + 4 independent per-PE
|
|
// activation-fetch/writeback requesters) -- sdram_arbiter_n.v's own
|
|
// NUM_REQ already generalizes to this without any change, confirmed
|
|
// by direct reuse here, not by inspection.
|
|
//
|
|
// Runs TWO consecutive group jobs (different positions/weights each
|
|
// time) specifically to catch any "forgot to clear a per-job latch"
|
|
// bug in the group's own barrier state (pe_acked/pe_done_latch) --
|
|
// a single-job test would not exercise that reset path at all.
|
|
// ============================================================
|
|
module tb;
|
|
localparam BURST_LEN = 8;
|
|
localparam SDRAM_ADDR_WIDTH = 25;
|
|
localparam CLK_FREQ_MHZ = 64;
|
|
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
|
|
|
|
localparam DATA_WIDTH = 8;
|
|
localparam P_IN = 8;
|
|
localparam ACC_WIDTH = 32;
|
|
localparam ADDR_WIDTH = 26;
|
|
localparam N_INPUTS = 128;
|
|
localparam N_TILES = N_INPUTS/P_IN;
|
|
localparam LAYER_BYTES = N_INPUTS;
|
|
localparam WORDS_PER_LAYER = LAYER_BYTES/2;
|
|
localparam NUM_REQ = 5; // 1 group weight-fetch + 4 PE activation/writeback
|
|
|
|
reg clk = 0;
|
|
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
|
|
reg rst;
|
|
integer cyc;
|
|
always @(posedge clk) if (!rst) cyc <= cyc + 1;
|
|
|
|
// ---- real burst-memory backend, shared via a real 5-way arbiter ----
|
|
wire ctrl_req, ctrl_wr;
|
|
wire [SDRAM_ADDR_WIDTH-1:0] ctrl_addr;
|
|
wire [32*BURST_LEN-1:0] ctrl_wdata;
|
|
wire [4*BURST_LEN-1:0] ctrl_wmask;
|
|
wire [32*BURST_LEN-1:0] ctrl_rdata;
|
|
wire ctrl_ready, ctrl_busy;
|
|
|
|
reg wpre_req, wpre_wr;
|
|
reg [SDRAM_ADDR_WIDTH-1:0] wpre_addr;
|
|
reg [32*BURST_LEN-1:0] wpre_wdata;
|
|
reg pre_active;
|
|
|
|
wire [NUM_REQ-1:0] arb_active, arb_grant, arb_req, arb_wr;
|
|
wire [NUM_REQ*SDRAM_ADDR_WIDTH-1:0] arb_addr;
|
|
wire [NUM_REQ*32*BURST_LEN-1:0] arb_wdata, arb_rdata;
|
|
wire [NUM_REQ*4*BURST_LEN-1:0] arb_wmask;
|
|
wire [NUM_REQ-1:0] arb_ready, arb_busy;
|
|
|
|
wire real_ctrl_req, real_ctrl_wr;
|
|
wire [SDRAM_ADDR_WIDTH-1:0] real_ctrl_addr;
|
|
wire [32*BURST_LEN-1:0] real_ctrl_wdata;
|
|
wire [4*BURST_LEN-1:0] real_ctrl_wmask;
|
|
|
|
sdram_arbiter_n #(.NUM_REQ(NUM_REQ), .ADDR_WIDTH(SDRAM_ADDR_WIDTH), .BURST_LEN(BURST_LEN)) u_arb (
|
|
.clk(clk), .rst(rst),
|
|
.req_active(arb_active), .req_grant(arb_grant),
|
|
.req_req(arb_req), .req_wr(arb_wr), .req_addr(arb_addr),
|
|
.req_wdata(arb_wdata), .req_wmask(arb_wmask),
|
|
.req_rdata(arb_rdata), .req_ready(arb_ready), .req_busy(arb_busy),
|
|
.ctrl_req(real_ctrl_req), .ctrl_wr(real_ctrl_wr), .ctrl_addr(real_ctrl_addr),
|
|
.ctrl_wdata(real_ctrl_wdata), .ctrl_wmask(real_ctrl_wmask),
|
|
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
|
|
);
|
|
|
|
assign ctrl_req = pre_active ? wpre_req : real_ctrl_req;
|
|
assign ctrl_wr = pre_active ? wpre_wr : real_ctrl_wr;
|
|
assign ctrl_addr = pre_active ? wpre_addr : real_ctrl_addr;
|
|
assign ctrl_wdata = pre_active ? wpre_wdata : real_ctrl_wdata;
|
|
assign ctrl_wmask = pre_active ? {(4*BURST_LEN){1'b0}} : real_ctrl_wmask;
|
|
|
|
burst_mem_model32 #(
|
|
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(SDRAM_ADDR_WIDTH)
|
|
) u_mem (
|
|
.clk(clk), .rst(rst),
|
|
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr), .wdata(ctrl_wdata), .wmask(ctrl_wmask),
|
|
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy)
|
|
);
|
|
|
|
function automatic signed [7:0] weight_byte(input integer li, input integer t);
|
|
reg [7:0] tmp;
|
|
begin
|
|
tmp = (li*17 + t*29 + 13) & 8'hFF;
|
|
weight_byte = $signed(tmp);
|
|
end
|
|
endfunction
|
|
function automatic signed [7:0] input_byte(input integer li, input integer pos, input integer t);
|
|
reg [7:0] tmp;
|
|
begin
|
|
tmp = (li*11 + pos*41 + t*7 + 3) & 8'hFF;
|
|
input_byte = $signed(tmp);
|
|
end
|
|
endfunction
|
|
|
|
task automatic sdram_write_burst(input [SDRAM_ADDR_WIDTH-1:0] word_addr, input [32*BURST_LEN-1:0] data);
|
|
begin
|
|
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
|
wpre_req = 1'b1; wpre_wr = 1'b1; wpre_addr = word_addr; wpre_wdata = data;
|
|
@(posedge clk); wpre_req = 1'b0;
|
|
while (!ctrl_ready) @(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
task automatic preload_sdram_layer(input integer li);
|
|
integer bi, wb, tt;
|
|
reg [32*BURST_LEN-1:0] burst_data;
|
|
begin
|
|
for (bi = 0; bi < (LAYER_BYTES/(4*BURST_LEN)); bi = bi + 1) begin
|
|
for (wb = 0; wb < BURST_LEN; wb = wb + 1) begin
|
|
tt = bi*(4*BURST_LEN) + wb*4;
|
|
burst_data[wb*32 +: 32] = {weight_byte(li, tt+3), weight_byte(li, tt+2),
|
|
weight_byte(li, tt+1), weight_byte(li, tt)};
|
|
end
|
|
sdram_write_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), burst_data);
|
|
end
|
|
end
|
|
endtask
|
|
|
|
localparam [ADDR_WIDTH-1:0] ACT_MEM_BASE = 26'h10000;
|
|
function automatic [ADDR_WIDTH-1:0] act_x_base(input integer li, input integer pos);
|
|
act_x_base = ACT_MEM_BASE + (li*16 + pos) * ((N_TILES/4)*BURST_LEN);
|
|
endfunction
|
|
|
|
task automatic preload_sdram_activation(input integer li, input integer pos);
|
|
integer tq, qi;
|
|
reg [32*BURST_LEN-1:0] burst_data;
|
|
reg [ADDR_WIDTH-1:0] base;
|
|
begin
|
|
base = act_x_base(li, pos);
|
|
for (tq = 0; tq < N_TILES/4; tq = tq + 1) begin
|
|
burst_data = {(32*BURST_LEN){1'b0}};
|
|
for (qi = 0; qi < 4; qi = qi + 1)
|
|
burst_data[qi*64 +: 64] = {input_byte(li, pos, (4*tq+qi)*P_IN + 7), input_byte(li, pos, (4*tq+qi)*P_IN + 6),
|
|
input_byte(li, pos, (4*tq+qi)*P_IN + 5), input_byte(li, pos, (4*tq+qi)*P_IN + 4),
|
|
input_byte(li, pos, (4*tq+qi)*P_IN + 3), input_byte(li, pos, (4*tq+qi)*P_IN + 2),
|
|
input_byte(li, pos, (4*tq+qi)*P_IN + 1), input_byte(li, pos, (4*tq+qi)*P_IN + 0)};
|
|
sdram_write_burst(base[SDRAM_ADDR_WIDTH-1:0] + tq*BURST_LEN, burst_data);
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// ---- systolic_group.v (DUT) ----
|
|
reg job_start;
|
|
reg [ADDR_WIDTH-1:0] w_base;
|
|
reg [15:0] n_tiles_in;
|
|
reg [4*ADDR_WIDTH-1:0] pe_x_base_a, pe_x_base_b, pe_result_addr_a, pe_result_addr_b;
|
|
reg [4*16-1:0] pe_node_id_a, pe_node_id_b;
|
|
wire job_done;
|
|
wire [4*DATA_WIDTH-1:0] pe_result_data_a, pe_result_data_b;
|
|
wire [4*16-1:0] pe_result_node_id_a, pe_result_node_id_b;
|
|
wire [4*ADDR_WIDTH-1:0] pe_result_addr_a_out, pe_result_addr_b_out;
|
|
|
|
wire grp_mem_active;
|
|
wire [3:0] pe_mem_active;
|
|
|
|
systolic_group #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
|
|
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
.job_start(job_start), .w_base(w_base), .n_tiles(n_tiles_in),
|
|
.pe_x_base_a(pe_x_base_a), .pe_x_base_b(pe_x_base_b),
|
|
.pe_result_addr_a(pe_result_addr_a), .pe_result_addr_b(pe_result_addr_b),
|
|
.pe_node_id_a(pe_node_id_a), .pe_node_id_b(pe_node_id_b),
|
|
.job_done(job_done),
|
|
.pe_result_data_a(pe_result_data_a), .pe_result_data_b(pe_result_data_b),
|
|
.pe_result_node_id_a(pe_result_node_id_a), .pe_result_node_id_b(pe_result_node_id_b),
|
|
.pe_result_addr_a_out(pe_result_addr_a_out), .pe_result_addr_b_out(pe_result_addr_b_out),
|
|
.mem_active(grp_mem_active), .mem_grant(arb_grant[0]),
|
|
.ctrl_req(arb_req[0]), .ctrl_wr(arb_wr[0]), .ctrl_addr(arb_addr[0*SDRAM_ADDR_WIDTH +: SDRAM_ADDR_WIDTH]),
|
|
.ctrl_wdata(arb_wdata[0*32*BURST_LEN +: 32*BURST_LEN]), .ctrl_wmask(arb_wmask[0*4*BURST_LEN +: 4*BURST_LEN]),
|
|
.ctrl_rdata(arb_rdata[0*32*BURST_LEN +: 32*BURST_LEN]), .ctrl_ready(arb_ready[0]), .ctrl_busy(arb_busy[0]),
|
|
.pe_mem_active(pe_mem_active), .pe_mem_grant(arb_grant[4:1]),
|
|
.pe_ctrl_req(arb_req[4:1]), .pe_ctrl_wr(arb_wr[4:1]),
|
|
.pe_ctrl_addr(arb_addr[1*SDRAM_ADDR_WIDTH +: 4*SDRAM_ADDR_WIDTH]),
|
|
.pe_ctrl_wdata(arb_wdata[1*32*BURST_LEN +: 4*32*BURST_LEN]),
|
|
.pe_ctrl_wmask(arb_wmask[1*4*BURST_LEN +: 4*4*BURST_LEN]),
|
|
.pe_ctrl_rdata(arb_rdata[1*32*BURST_LEN +: 4*32*BURST_LEN]),
|
|
.pe_ctrl_ready(arb_ready[4:1]), .pe_ctrl_busy(arb_busy[4:1])
|
|
);
|
|
|
|
assign arb_active[0] = grp_mem_active;
|
|
assign arb_active[4:1] = pe_mem_active;
|
|
|
|
integer errors, tests;
|
|
integer li_i, gi, wd;
|
|
integer acc, s;
|
|
reg signed [DATA_WIDTH-1:0] expected [0:7]; // 4 PEs x 2 lanes
|
|
|
|
task automatic run_group_job(input integer li, input integer pos_base);
|
|
integer pe, lane, pos, tt;
|
|
reg [DATA_WIDTH-1:0] got_a, got_b;
|
|
begin
|
|
tests = tests + 1;
|
|
@(posedge clk);
|
|
job_start = 1'b1;
|
|
w_base = li*WORDS_PER_LAYER;
|
|
n_tiles_in = N_TILES[15:0];
|
|
for (pe = 0; pe < 4; pe = pe + 1) begin
|
|
pe_x_base_a[pe*ADDR_WIDTH +: ADDR_WIDTH] = act_x_base(li, pos_base + pe*2);
|
|
pe_x_base_b[pe*ADDR_WIDTH +: ADDR_WIDTH] = act_x_base(li, pos_base + pe*2 + 1);
|
|
pe_result_addr_a[pe*ADDR_WIDTH +: ADDR_WIDTH] = 26'h9000 + pe*2;
|
|
pe_result_addr_b[pe*ADDR_WIDTH +: ADDR_WIDTH] = 26'h9000 + pe*2 + 1;
|
|
pe_node_id_a[pe*16 +: 16] = li*100 + pos_base + pe*2;
|
|
pe_node_id_b[pe*16 +: 16] = li*100 + pos_base + pe*2 + 1;
|
|
end
|
|
@(posedge clk);
|
|
job_start = 1'b0;
|
|
|
|
// real, independently-reproduced golden model (same formula
|
|
// as tb_packed_slot.v's own, applied per PE/lane)
|
|
for (pe = 0; pe < 4; pe = pe + 1) begin
|
|
for (lane = 0; lane < 2; lane = lane + 1) begin
|
|
pos = pos_base + pe*2 + lane;
|
|
acc = 0;
|
|
for (tt = 0; tt < N_INPUTS; tt = tt + 1)
|
|
acc = acc + (input_byte(li, pos, tt) * weight_byte(li, tt));
|
|
s = acc;
|
|
if (s <= 0) expected[pe*2+lane] = 0;
|
|
else if (s > 127) expected[pe*2+lane] = 8'sd127;
|
|
else expected[pe*2+lane] = s[DATA_WIDTH-1:0];
|
|
end
|
|
end
|
|
|
|
wd = 0;
|
|
while (!job_done && wd < 4000) begin @(posedge clk); wd = wd + 1; end
|
|
if (!job_done) begin
|
|
$display("FAIL li=%0d pos_base=%0d: TIMEOUT waiting for group job_done", li, pos_base);
|
|
errors = errors + 1;
|
|
end else begin
|
|
for (pe = 0; pe < 4; pe = pe + 1) begin
|
|
got_a = pe_result_data_a[pe*DATA_WIDTH +: DATA_WIDTH];
|
|
got_b = pe_result_data_b[pe*DATA_WIDTH +: DATA_WIDTH];
|
|
if (got_a !== expected[pe*2] || got_b !== expected[pe*2+1]) begin
|
|
$display("FAIL li=%0d pos_base=%0d PE%0d: got_a=%0d got_b=%0d expected_a=%0d expected_b=%0d",
|
|
li, pos_base, pe, $signed(got_a), $signed(got_b),
|
|
$signed(expected[pe*2]), $signed(expected[pe*2+1]));
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS li=%0d pos_base=%0d PE%0d: a=%0d b=%0d (systolic_group.v)",
|
|
li, pos_base, pe, $signed(got_a), $signed(got_b));
|
|
end
|
|
end
|
|
end
|
|
end
|
|
endtask
|
|
|
|
initial begin
|
|
errors = 0; tests = 0; cyc = 0;
|
|
rst = 1; pre_active = 1'b1;
|
|
wpre_req = 0; wpre_wr = 0; wpre_addr = 0; wpre_wdata = 0;
|
|
job_start = 0; w_base = 0; n_tiles_in = 0;
|
|
pe_x_base_a = 0; pe_x_base_b = 0; pe_result_addr_a = 0; pe_result_addr_b = 0;
|
|
pe_node_id_a = 0; pe_node_id_b = 0;
|
|
repeat(5) @(posedge clk);
|
|
rst = 0;
|
|
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
|
|
|
$display("=== preload SDRAM: 2 layers' weights + 16 activation positions ===");
|
|
for (li_i = 0; li_i < 2; li_i = li_i + 1) begin
|
|
preload_sdram_layer(li_i);
|
|
for (gi = 0; gi < 8; gi = gi + 1) preload_sdram_activation(li_i, gi);
|
|
end
|
|
@(posedge clk);
|
|
pre_active = 1'b0;
|
|
|
|
$display("=== systolic_group.v: 2 group jobs (4 PEs x 2 lanes each) ===");
|
|
run_group_job(0, 0);
|
|
run_group_job(1, 0);
|
|
|
|
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
|
|
if (errors == 0) $display("ALL TESTS PASSED (tb_systolic_group)");
|
|
$finish;
|
|
end
|
|
endmodule
|