exp: N=16 timing closure fixed (EXP-0056), weight-reuse gives real 7.16x memory speedup without DDR3 (EXP-0057)
EXP-0056: N_SLOTS=16 failed timing on LFE5U-85F (23-24MHz vs 64MHz
target). First hypothesis (dependency_manager.v's serial ready-scan)
was wrong but real -- built and verified priority_encoder_lsb.v (a
generic recursive tree encoder) and dependency_manager_fast.v, bit-
exact equivalent to the original, but integrated it made no real
difference (24.26MHz). The real cause, found from nextpnr's own
critical-path report: nms_activation_fill_ctrl_v3.v's balanced max-
tree was only ever extended to N_SLOTS in {1,2,4,8}, silently falling
back to the original slow scan for 16. Added the missing case
(nms_activation_fill_ctrl_v3_n16.v), verified isolated (10017/10017)
and functionally (D-Stress N=16 still 256/256 bit-exact). Real result:
71.01MHz, PASS at 64MHz (single seed so far).
EXP-0057: built layer_weight_buffer.v, a double-buffered per-layer
weight scratchpad (fill one buffer in the background from SDRAM while
compute reads many times from the other -- weight-stationary reuse,
as opposed to D-Stress's own deliberately zero-reuse pattern). Wired
to the real sdram_controller_openrow.v + sdram_model.v, no new
hardware. For the same 32768 bytes of useful data: zero-reuse costs
27048 real cycles, reuse costs 3777 -- 7.16x real measured speedup on
the SAME SDR SDRAM, no DDR3, no clock change. This is the answer to
whether DDR3 is necessary for a workload class that actually has
reuse (e.g. conv-style face recognition, unlike D-Stress) -- it isn't,
at least not for this reason.
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,150 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0056 -- bit-exact equivalence check: dependency_manager.v
|
||||
// (baseline) vs dependency_manager_fast.v (priority_encoder_lsb.v
|
||||
// fix), at N_NODES=1024 -- the REAL config this project's own
|
||||
// D-Stress benchmark uses, not the small hand-crafted DAG the
|
||||
// original tb_dependency_manager.v exercises. Both DUTs driven by the
|
||||
// IDENTICAL random stimulus every cycle (registration + producer-done
|
||||
// events), every output compared cycle-by-cycle. Pure random
|
||||
// (required/producer_ids need not form a realistic DAG -- this
|
||||
// module's own behavior is well-defined for ANY input sequence, and
|
||||
// bit-exact equivalence for ANY sequence is exactly the property that
|
||||
// needs proving here).
|
||||
// ============================================================
|
||||
module tb;
|
||||
localparam N_NODES = 1024;
|
||||
localparam MAX_DEPS = 4;
|
||||
localparam ADDR_WIDTH = 26;
|
||||
localparam NODE_IDW = $clog2(N_NODES);
|
||||
|
||||
reg clk, rst;
|
||||
initial begin clk = 0; forever #5 clk = ~clk; end
|
||||
|
||||
reg reg_valid;
|
||||
reg [NODE_IDW-1:0] reg_node_id;
|
||||
reg [$clog2(MAX_DEPS+1)-1:0] reg_required;
|
||||
reg [MAX_DEPS*NODE_IDW-1:0] reg_producer_ids;
|
||||
reg [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr;
|
||||
reg [15:0] reg_n_tiles;
|
||||
|
||||
reg producer_done_valid;
|
||||
reg [NODE_IDW-1:0] producer_done_node_id;
|
||||
|
||||
reg ready_ready;
|
||||
|
||||
wire reg_ready_a, reg_ready_b;
|
||||
wire ready_valid_a, ready_valid_b;
|
||||
wire [NODE_IDW-1:0] ready_node_id_a, ready_node_id_b;
|
||||
wire [ADDR_WIDTH-1:0] ready_x_base_a, ready_x_base_b;
|
||||
wire [ADDR_WIDTH-1:0] ready_w_base_a, ready_w_base_b;
|
||||
wire [15:0] ready_n_tiles_a, ready_n_tiles_b;
|
||||
wire [ADDR_WIDTH-1:0] ready_result_addr_a, ready_result_addr_b;
|
||||
wire any_pending_a, any_pending_b;
|
||||
|
||||
dependency_manager #(
|
||||
.N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .ADDR_WIDTH(ADDR_WIDTH)
|
||||
) dut_base (
|
||||
.clk(clk), .rst(rst),
|
||||
.reg_valid(reg_valid), .reg_ready(reg_ready_a), .reg_node_id(reg_node_id),
|
||||
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
|
||||
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
|
||||
.reg_result_addr(reg_result_addr),
|
||||
.producer_done_valid(producer_done_valid), .producer_done_node_id(producer_done_node_id),
|
||||
.ready_valid(ready_valid_a), .ready_ready(ready_ready), .ready_node_id(ready_node_id_a),
|
||||
.ready_x_base(ready_x_base_a), .ready_w_base(ready_w_base_a),
|
||||
.ready_n_tiles(ready_n_tiles_a), .ready_result_addr(ready_result_addr_a),
|
||||
.any_pending(any_pending_a)
|
||||
);
|
||||
|
||||
dependency_manager_fast #(
|
||||
.N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .ADDR_WIDTH(ADDR_WIDTH)
|
||||
) dut_fast (
|
||||
.clk(clk), .rst(rst),
|
||||
.reg_valid(reg_valid), .reg_ready(reg_ready_b), .reg_node_id(reg_node_id),
|
||||
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
|
||||
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
|
||||
.reg_result_addr(reg_result_addr),
|
||||
.producer_done_valid(producer_done_valid), .producer_done_node_id(producer_done_node_id),
|
||||
.ready_valid(ready_valid_b), .ready_ready(ready_ready), .ready_node_id(ready_node_id_b),
|
||||
.ready_x_base(ready_x_base_b), .ready_w_base(ready_w_base_b),
|
||||
.ready_n_tiles(ready_n_tiles_b), .ready_result_addr(ready_result_addr_b),
|
||||
.any_pending(any_pending_b)
|
||||
);
|
||||
|
||||
integer errors, tests, cyc;
|
||||
integer seed, i;
|
||||
integer next_id;
|
||||
reg [NODE_IDW-1:0] rnd_id;
|
||||
|
||||
task automatic check_equal;
|
||||
begin
|
||||
tests = tests + 1;
|
||||
if (reg_ready_a !== reg_ready_b || ready_valid_a !== ready_valid_b ||
|
||||
any_pending_a !== any_pending_b ||
|
||||
(ready_valid_a && (ready_node_id_a !== ready_node_id_b ||
|
||||
ready_x_base_a !== ready_x_base_b ||
|
||||
ready_w_base_a !== ready_w_base_b ||
|
||||
ready_n_tiles_a !== ready_n_tiles_b ||
|
||||
ready_result_addr_a !== ready_result_addr_b))) begin
|
||||
$display("FAIL @cycle %0d: base(reg_ready=%b ready_valid=%b node=%0d any_pending=%b) fast(reg_ready=%b ready_valid=%b node=%0d any_pending=%b)",
|
||||
cyc, reg_ready_a, ready_valid_a, ready_node_id_a, any_pending_a,
|
||||
reg_ready_b, ready_valid_b, ready_node_id_b, any_pending_b);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
always @(posedge clk) if (!rst) cyc <= cyc + 1;
|
||||
|
||||
initial begin
|
||||
errors = 0; tests = 0; cyc = 0; seed = 32'hFEEDFACE;
|
||||
rst = 1; reg_valid = 0; reg_node_id = 0; reg_required = 0; reg_producer_ids = 0;
|
||||
reg_x_base = 0; reg_w_base = 0; reg_n_tiles = 0; reg_result_addr = 0;
|
||||
producer_done_valid = 0; producer_done_node_id = 0;
|
||||
ready_ready = 1;
|
||||
repeat(5) @(posedge clk);
|
||||
rst = 0;
|
||||
|
||||
$display("=== random stimulus, N_NODES=1024, 20000 cycles ===");
|
||||
next_id = 0;
|
||||
for (i = 0; i < 20000; i = i + 1) begin
|
||||
@(posedge clk);
|
||||
#1; // let combinational outputs settle before sampling/comparing
|
||||
|
||||
// registration: ~15% of cycles, sequential node_id (avoids
|
||||
// double-registering the same id, which the module itself
|
||||
// does not need to tolerate -- caller's own responsibility,
|
||||
// same as the real Director/graph-loader upstream)
|
||||
reg_valid = (($random(seed) % 100) < 15) && (next_id < N_NODES);
|
||||
if (reg_valid) begin
|
||||
reg_node_id = next_id[NODE_IDW-1:0];
|
||||
reg_required = $random(seed) % (MAX_DEPS+1);
|
||||
reg_x_base = $random(seed);
|
||||
reg_w_base = $random(seed);
|
||||
reg_n_tiles = $random(seed);
|
||||
reg_result_addr = $random(seed);
|
||||
reg_producer_ids = {$random(seed), $random(seed)}; // random bits, need not be a valid/realistic producer graph
|
||||
next_id = next_id + 1;
|
||||
end
|
||||
|
||||
// producer-done: ~10% of cycles, random already-issued id
|
||||
producer_done_valid = (($random(seed) % 100) < 10) && (next_id > 0);
|
||||
if (producer_done_valid) begin
|
||||
rnd_id = ($random(seed) % next_id);
|
||||
producer_done_node_id = rnd_id;
|
||||
end
|
||||
|
||||
// ready_ready: randomly withhold backpressure sometimes,
|
||||
// to exercise the "ready_valid held, not yet accepted" path
|
||||
ready_ready = (($random(seed) % 100) < 80);
|
||||
|
||||
check_equal;
|
||||
end
|
||||
|
||||
$display("=== %0d/%0d cycles matched, %0d mismatches ===", tests-errors, tests, errors);
|
||||
if (errors == 0) $display("ALL TESTS PASSED (tb_dependency_manager_fast, bit-exact vs baseline)");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,260 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0057 -- real measured comparison: weight-stationary layer reuse
|
||||
// (layer_weight_buffer.v, double-buffered, background-prefetched)
|
||||
// vs the zero-reuse D-Stress-style pattern (every read is its own
|
||||
// independent external fetch), BOTH driven through the SAME real,
|
||||
// already-verified open-row SDR SDRAM controller (sdram_controller_
|
||||
// openrow.v, EXP-0054) and behavioral chip model (sdram_model.v) --
|
||||
// no DDR3, no clock change, the exact hardware this project already
|
||||
// has. Answers directly: does weight reuse alone (no new memory
|
||||
// hardware) close enough of the gap that DDR3 stops being necessary
|
||||
// for a workload class that actually has reuse (e.g. a CNN layer),
|
||||
// as opposed to D-Stress's own deliberately zero-reuse worst case?
|
||||
//
|
||||
// SAME total useful-byte-consumption for both cases (fair
|
||||
// comparison): L=16 "layers" x M=16 reuses x LAYER_DEPTH=128 bytes =
|
||||
// 32768 total byte-reads -- identical to D-Stress's own 256x128=32768
|
||||
// total bytes this whole project has been benchmarked against all
|
||||
// session.
|
||||
// REUSE case: L x LAYER_DEPTH = 2048 bytes actually fetched from
|
||||
// SDRAM (each layer's weights fetched ONCE, reused
|
||||
// M times from the local double buffer).
|
||||
// ZERO-REUSE case: L x M x LAYER_DEPTH = 32768 bytes fetched (every
|
||||
// single read is independent, matching D-Stress).
|
||||
// ============================================================
|
||||
module tb;
|
||||
localparam BURST_LEN = 8;
|
||||
localparam ROW_BITS = 13;
|
||||
localparam COL_BITS = 10;
|
||||
localparam BANK_BITS = 2;
|
||||
localparam ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS;
|
||||
localparam ALIGN_BITS = $clog2(BURST_LEN);
|
||||
localparam CLK_FREQ_MHZ = 64;
|
||||
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
|
||||
|
||||
localparam LAYER_DEPTH = 128; // bytes/layer weight block (matches P_IN*MAX_TILES=8*16 tile convention)
|
||||
localparam L = 16; // number of layers
|
||||
localparam M = 16; // reuses per layer (e.g. spatial positions a filter slides across)
|
||||
localparam WORDS_PER_LAYER = LAYER_DEPTH/2; // sdram_controller word=16-bit
|
||||
localparam BURSTS_PER_LAYER = LAYER_DEPTH/(2*BURST_LEN); // 16-byte (8-word) transactions per layer
|
||||
|
||||
reg clk = 0;
|
||||
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
|
||||
reg rst;
|
||||
|
||||
integer cyc;
|
||||
always @(posedge clk) if (!rst) cyc <= cyc + 1;
|
||||
|
||||
// ================= shared physical SDRAM (real open-row controller) =================
|
||||
reg ctrl_req, ctrl_wr;
|
||||
reg [ADDR_WIDTH-1:0] ctrl_addr;
|
||||
reg [16*BURST_LEN-1:0] ctrl_wdata;
|
||||
reg [2*BURST_LEN-1:0] ctrl_wmask;
|
||||
wire [16*BURST_LEN-1:0] ctrl_rdata;
|
||||
wire ctrl_ready, ctrl_busy;
|
||||
wire cke, cs_n, ras_n, cas_n, we_n;
|
||||
wire [BANK_BITS-1:0] ba;
|
||||
wire [ROW_BITS-1:0] a;
|
||||
wire [15:0] dq;
|
||||
wire [1:0] dqm;
|
||||
|
||||
sdram_controller_openrow #(
|
||||
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN),
|
||||
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
|
||||
) u_ctrl (
|
||||
.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),
|
||||
.sdram_cke(cke), .sdram_cs_n(cs_n), .sdram_ras_n(ras_n), .sdram_cas_n(cas_n), .sdram_we_n(we_n),
|
||||
.sdram_ba(ba), .sdram_a(a), .sdram_dq(dq), .sdram_dqm(dqm)
|
||||
);
|
||||
sdram_model #(
|
||||
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
|
||||
) u_mem (
|
||||
.clk(clk), .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n),
|
||||
.ba(ba), .a(a), .dq(dq), .dqm(dqm)
|
||||
);
|
||||
|
||||
task automatic sdram_write_burst(input [ADDR_WIDTH-1:0] word_addr, input [16*BURST_LEN-1:0] data);
|
||||
begin
|
||||
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
||||
ctrl_req = 1'b1; ctrl_wr = 1'b1; ctrl_addr = word_addr; ctrl_wdata = data; ctrl_wmask = {(2*BURST_LEN){1'b0}};
|
||||
@(posedge clk); ctrl_req = 1'b0;
|
||||
while (!ctrl_ready) @(posedge clk);
|
||||
end
|
||||
endtask
|
||||
task automatic sdram_read_burst(input [ADDR_WIDTH-1:0] word_addr, output [16*BURST_LEN-1:0] data);
|
||||
begin
|
||||
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
||||
ctrl_req = 1'b1; ctrl_wr = 1'b0; ctrl_addr = word_addr; ctrl_wmask = {(2*BURST_LEN){1'b0}};
|
||||
@(posedge clk); ctrl_req = 1'b0;
|
||||
while (!ctrl_ready) @(posedge clk);
|
||||
data = ctrl_rdata;
|
||||
end
|
||||
endtask
|
||||
|
||||
// ================= layer_weight_buffer under test =================
|
||||
reg fill_we;
|
||||
reg [$clog2(LAYER_DEPTH)-1:0] fill_addr;
|
||||
reg [7:0] fill_data;
|
||||
reg fill_done;
|
||||
reg [$clog2(LAYER_DEPTH)-1:0] rd_addr;
|
||||
wire [7:0] rd_data;
|
||||
reg consume_done;
|
||||
wire active_sel, swapped;
|
||||
|
||||
layer_weight_buffer #(.DATA_WIDTH(8), .LAYER_DEPTH(LAYER_DEPTH)) u_lwb (
|
||||
.clk(clk), .rst(rst),
|
||||
.fill_we(fill_we), .fill_addr(fill_addr), .fill_data(fill_data), .fill_done(fill_done),
|
||||
.rd_addr(rd_addr), .rd_data(rd_data), .consume_done(consume_done),
|
||||
.active_sel(active_sel), .swapped(swapped)
|
||||
);
|
||||
|
||||
integer errors, tests;
|
||||
|
||||
// pre-load SDRAM with L distinct layer patterns, at word address layer_idx*WORDS_PER_LAYER
|
||||
task automatic preload_sdram_layers;
|
||||
integer li, bi;
|
||||
reg [16*BURST_LEN-1:0] burst_data;
|
||||
integer wb;
|
||||
begin
|
||||
for (li = 0; li < L; li = li + 1) begin
|
||||
for (bi = 0; bi < BURSTS_PER_LAYER; bi = bi + 1) begin
|
||||
for (wb = 0; wb < BURST_LEN; wb = wb + 1)
|
||||
burst_data[wb*16 +: 16] = {8'(8'h20+li), 8'(bi*BURST_LEN+wb)};
|
||||
sdram_write_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), burst_data);
|
||||
end
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
// fetch layer li's weights (bulk sequential, BURSTS_PER_LAYER transactions)
|
||||
// into the layer_weight_buffer's inactive side
|
||||
task automatic prefetch_layer(input integer li);
|
||||
integer bi, wb;
|
||||
reg [16*BURST_LEN-1:0] burst_data;
|
||||
begin
|
||||
for (bi = 0; bi < BURSTS_PER_LAYER; bi = bi + 1) begin
|
||||
sdram_read_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), burst_data);
|
||||
for (wb = 0; wb < BURST_LEN; wb = wb + 1) begin
|
||||
@(posedge clk);
|
||||
fill_we = 1'b1;
|
||||
fill_addr = (bi*BURST_LEN + wb) & (2*BURST_LEN-1) | (bi*2*BURST_LEN); // byte index within layer
|
||||
fill_addr = bi*(2*BURST_LEN) + wb*2; // low byte of word wb
|
||||
fill_data = burst_data[wb*16 +: 8];
|
||||
@(posedge clk);
|
||||
fill_addr = bi*(2*BURST_LEN) + wb*2 + 1; // high byte of word wb
|
||||
fill_data = burst_data[wb*16+8 +: 8];
|
||||
end
|
||||
end
|
||||
@(posedge clk); fill_we = 1'b0;
|
||||
fill_done = 1'b1; @(posedge clk); fill_done = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic consume_layer_check(input integer li, input integer errors_before, output integer errors_after);
|
||||
integer r, k;
|
||||
reg [7:0] expected;
|
||||
begin
|
||||
errors_after = errors_before;
|
||||
for (r = 0; r < M; r = r + 1) begin
|
||||
for (k = 0; k < LAYER_DEPTH; k = k + 1) begin
|
||||
rd_addr = k[$clog2(LAYER_DEPTH)-1:0];
|
||||
#1;
|
||||
tests = tests + 1;
|
||||
expected = 8'(8'h20+li) ; // high byte of the 16-bit word pattern for even k, low byte pattern for odd k -- see preload
|
||||
// preload packed {8'h20+li, byte_idx} per WORD (16-bit): low byte = byte_idx, high byte = 8'h20+li
|
||||
if (k[0] == 1'b0) expected = {1'b0, k[7:1]}; // low byte of word = WORD index (bi*BURST_LEN+wb), i.e. k/2 -- see preload_sdram_layers
|
||||
else expected = 8'(8'h20+li); // high byte of word = layer tag
|
||||
if (rd_data !== expected) begin
|
||||
$display("FAIL layer=%0d reuse=%0d k=%0d: expected %h got %h", li, r, k, expected, rd_data);
|
||||
errors_after = errors_after + 1;
|
||||
end
|
||||
@(posedge clk);
|
||||
end
|
||||
end
|
||||
consume_done = 1'b1; @(posedge clk); consume_done = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
integer li_i;
|
||||
integer t0, total_cycles_reuse, total_cycles_zeroreuse;
|
||||
|
||||
// zero-reuse baseline: L*M independent reads, each LAYER_DEPTH bytes,
|
||||
// NO local buffering -- every single "reuse" goes straight to SDRAM,
|
||||
// matching D-Stress's own access pattern exactly (through the SAME
|
||||
// real open-row controller).
|
||||
task automatic zero_reuse_baseline;
|
||||
integer li, r, bi;
|
||||
reg [16*BURST_LEN-1:0] junk;
|
||||
begin
|
||||
for (li = 0; li < L; li = li + 1) begin
|
||||
for (r = 0; r < M; r = r + 1) begin
|
||||
for (bi = 0; bi < BURSTS_PER_LAYER; bi = bi + 1) begin
|
||||
sdram_read_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), junk);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
errors = 0; tests = 0; cyc = 0;
|
||||
rst = 1; ctrl_req = 0; ctrl_wr = 0; ctrl_addr = 0; ctrl_wdata = 0; ctrl_wmask = 0;
|
||||
fill_we = 0; fill_addr = 0; fill_data = 0; fill_done = 0; rd_addr = 0; consume_done = 0;
|
||||
repeat(5) @(posedge clk);
|
||||
rst = 0;
|
||||
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
||||
|
||||
$display("=== preload SDRAM with %0d distinct layer patterns ===", L);
|
||||
preload_sdram_layers;
|
||||
|
||||
$display("=== REUSE case correctness pass: %0d layers x %0d reuses, double-buffered background prefetch (data check only, not timed) ===", L, M);
|
||||
prefetch_layer(0);
|
||||
consume_done = 1'b1; @(posedge clk); consume_done = 1'b0; // trigger initial swap
|
||||
for (li_i = 0; li_i < L; li_i = li_i + 1) begin
|
||||
fork
|
||||
consume_layer_check(li_i, errors, errors);
|
||||
begin
|
||||
if (li_i+1 < L) prefetch_layer(li_i+1);
|
||||
end
|
||||
join
|
||||
end
|
||||
$display(" correctness: %0d/%0d, %0d errors", tests-errors, tests, errors);
|
||||
|
||||
// ---- FAIR timing comparison: measure ONLY the real SDRAM
|
||||
// fetch cost in each case (the actual question this benchmark
|
||||
// exists to answer -- how much does reuse reduce dependence on
|
||||
// external memory bandwidth). Compute-side consumption cost is
|
||||
// deliberately excluded from BOTH measurements here -- it is
|
||||
// identical in both cases (same neural_processor.v pipeline
|
||||
// rate regardless of where weights come from) and including it
|
||||
// asymmetrically was a real bug in an earlier version of this
|
||||
// testbench (see EXP-0057 writeup). ----
|
||||
$display("=== REUSE case: pure SDRAM fetch time for %0d layers (%0d bytes total) ===", L, L*LAYER_DEPTH);
|
||||
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
||||
t0 = cyc;
|
||||
for (li_i = 0; li_i < L; li_i = li_i + 1) prefetch_layer(li_i);
|
||||
total_cycles_reuse = cyc - t0;
|
||||
$display(" REUSE: %0d cycles to fetch %0d bytes from SDRAM (each layer fetched ONCE, reused %0d x locally)",
|
||||
total_cycles_reuse, L*LAYER_DEPTH, M);
|
||||
|
||||
$display("=== ZERO-REUSE baseline: pure SDRAM fetch time for %0d bytes (every reuse fetched independently) ===", L*M*LAYER_DEPTH);
|
||||
@(posedge clk); while (ctrl_busy) @(posedge clk);
|
||||
t0 = cyc;
|
||||
zero_reuse_baseline;
|
||||
total_cycles_zeroreuse = cyc - t0;
|
||||
$display(" ZERO-REUSE: %0d cycles to fetch %0d bytes from SDRAM",
|
||||
total_cycles_zeroreuse, L*M*LAYER_DEPTH);
|
||||
|
||||
$display("=== RESULT ===");
|
||||
$display(" REUSE case data correctness: %0d/%0d, %0d errors", tests-errors, tests, errors);
|
||||
$display(" REAL measured speedup from weight reuse alone (SAME hardware, no DDR3, no clock change): %0f x",
|
||||
total_cycles_zeroreuse * 1.0 / total_cycles_reuse);
|
||||
|
||||
if (errors == 0) $display("ALL DATA CHECKS PASSED (tb_layer_reuse_vs_zero_reuse)");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,141 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0057 -- isolated correctness check for layer_weight_buffer.v:
|
||||
// fill buffer A, swap, read A many times while filling B, swap again
|
||||
// (order-independent: sometimes fill_done arrives first, sometimes
|
||||
// consume_done does), verify data integrity and correct buffer
|
||||
// selection throughout.
|
||||
// ============================================================
|
||||
module tb;
|
||||
localparam DATA_WIDTH = 8;
|
||||
localparam LAYER_DEPTH = 128;
|
||||
localparam ADDRW = $clog2(LAYER_DEPTH);
|
||||
|
||||
reg clk = 0;
|
||||
always #5 clk = ~clk;
|
||||
reg rst;
|
||||
|
||||
reg fill_we;
|
||||
reg [ADDRW-1:0] fill_addr;
|
||||
reg [DATA_WIDTH-1:0] fill_data;
|
||||
reg fill_done;
|
||||
reg [ADDRW-1:0] rd_addr;
|
||||
wire [DATA_WIDTH-1:0] rd_data;
|
||||
reg consume_done;
|
||||
wire active_sel;
|
||||
wire swapped;
|
||||
|
||||
layer_weight_buffer #(
|
||||
.DATA_WIDTH(DATA_WIDTH), .LAYER_DEPTH(LAYER_DEPTH)
|
||||
) dut (
|
||||
.clk(clk), .rst(rst),
|
||||
.fill_we(fill_we), .fill_addr(fill_addr), .fill_data(fill_data), .fill_done(fill_done),
|
||||
.rd_addr(rd_addr), .rd_data(rd_data), .consume_done(consume_done),
|
||||
.active_sel(active_sel), .swapped(swapped)
|
||||
);
|
||||
|
||||
integer errors, tests, i;
|
||||
|
||||
task automatic fill_layer(input [7:0] pattern_base);
|
||||
integer k;
|
||||
begin
|
||||
for (k = 0; k < LAYER_DEPTH; k = k + 1) begin
|
||||
@(posedge clk);
|
||||
fill_we = 1'b1; fill_addr = k[ADDRW-1:0]; fill_data = pattern_base + k[7:0];
|
||||
end
|
||||
@(posedge clk);
|
||||
fill_we = 1'b0;
|
||||
fill_done = 1'b1;
|
||||
@(posedge clk);
|
||||
fill_done = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
task automatic read_and_check_layer(input [7:0] pattern_base, input integer n_reuses);
|
||||
integer r, k;
|
||||
begin
|
||||
for (r = 0; r < n_reuses; r = r + 1) begin
|
||||
for (k = 0; k < LAYER_DEPTH; k = k + 1) begin
|
||||
rd_addr = k[ADDRW-1:0];
|
||||
#1;
|
||||
tests = tests + 1;
|
||||
if (rd_data !== (pattern_base + k[7:0])) begin
|
||||
$display("FAIL reuse=%0d addr=%0d: expected %0d got %0d", r, k, pattern_base+k[7:0], rd_data);
|
||||
errors = errors + 1;
|
||||
end
|
||||
@(posedge clk);
|
||||
end
|
||||
end
|
||||
consume_done = 1'b1;
|
||||
@(posedge clk);
|
||||
consume_done = 1'b0;
|
||||
end
|
||||
endtask
|
||||
|
||||
initial begin
|
||||
errors = 0; tests = 0;
|
||||
rst = 1; fill_we = 0; fill_addr = 0; fill_data = 0; fill_done = 0;
|
||||
rd_addr = 0; consume_done = 0;
|
||||
repeat(3) @(posedge clk);
|
||||
rst = 0;
|
||||
|
||||
$display("=== fill layer 0 (pattern 0x10), swap in ===");
|
||||
fill_layer(8'h10);
|
||||
if (active_sel !== 1'b0) begin
|
||||
$display("FAIL: expected active_sel=0 before any swap (fill alone must not swap)");
|
||||
errors = errors + 1;
|
||||
end
|
||||
// consume_done from reset state (never asserted yet) + fill_done just latched -> not swapped yet
|
||||
// now assert consume_done once (simulating "nothing to consume yet, first layer") to trigger the swap
|
||||
consume_done = 1'b1; @(posedge clk); consume_done = 1'b0;
|
||||
@(posedge clk); #1; // swap logic is 2-cycle latency from the triggering pulse; let it settle
|
||||
if (active_sel !== 1'b1) begin
|
||||
$display("FAIL: expected active_sel=1 after first swap, got %b", active_sel);
|
||||
errors = errors + 1;
|
||||
end
|
||||
tests = tests + 1;
|
||||
|
||||
$display("=== read layer 0 (now active, 5 reuses), meanwhile fill layer 1 (pattern 0x40) ===");
|
||||
fork
|
||||
read_and_check_layer(8'h10, 5);
|
||||
fill_layer(8'h40);
|
||||
join
|
||||
@(posedge clk); #1;
|
||||
if (active_sel !== 1'b0) begin
|
||||
$display("FAIL: expected active_sel=0 after second swap (back to buffer 0, now holding layer1 data), got %b", active_sel);
|
||||
errors = errors + 1;
|
||||
end
|
||||
tests = tests + 1;
|
||||
|
||||
$display("=== read layer 1 (pattern 0x40, 3 reuses), meanwhile fill layer 2 (pattern 0x80) -- fill finishes FIRST this time ===");
|
||||
fork
|
||||
begin
|
||||
fill_layer(8'h80);
|
||||
end
|
||||
begin
|
||||
#50; // let fill get a head start, so fill_done lands before consume_done
|
||||
read_and_check_layer(8'h40, 3);
|
||||
end
|
||||
join
|
||||
@(posedge clk); #1;
|
||||
if (active_sel !== 1'b1) begin
|
||||
$display("FAIL: expected active_sel=1 after third swap, got %b", active_sel);
|
||||
errors = errors + 1;
|
||||
end
|
||||
tests = tests + 1;
|
||||
|
||||
$display("=== read layer 2 (pattern 0x80, 4 reuses), verify final data ===");
|
||||
read_and_check_layer(8'h80, 4);
|
||||
consume_done = 1'b1; @(posedge clk); consume_done = 1'b0; // extra pulse, no fill pending: must NOT swap without a fill_done
|
||||
if (active_sel !== 1'b1) begin
|
||||
$display("FAIL: consume_done alone (no matching fill_done) must not cause a swap, got active_sel=%b", active_sel);
|
||||
errors = errors + 1;
|
||||
end
|
||||
tests = tests + 1;
|
||||
|
||||
$display("=== %0d/%0d passed, %0d errors ===", tests-errors, tests, errors);
|
||||
if (errors == 0) $display("ALL TESTS PASSED (tb_layer_weight_buffer)");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,72 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0056 -- isolated correctness check for the N_SLOTS==16
|
||||
// balanced max-tree added to nms_activation_fill_ctrl_v3_n16.v,
|
||||
// against the SAME flat-scan reference the original module's own
|
||||
// GEN_MAXTREE_FALLBACK uses (the two must always agree -- that
|
||||
// fallback path is explicitly documented as "correct but not
|
||||
// optimized", i.e. the golden reference for what ANY replacement
|
||||
// must compute). Random 16-way max over 10000 vectors.
|
||||
// ============================================================
|
||||
module tb;
|
||||
reg [15:0] v [0:15];
|
||||
integer i, k, seed, errors, tests;
|
||||
reg [15:0] ref_max;
|
||||
|
||||
// exact mirror of nms_activation_fill_ctrl_v3_n16.v's own
|
||||
// GEN_MAXTREE_N16 combinational tree
|
||||
wire [15:0] m0 = (v[0] > v[1]) ? v[0] : v[1];
|
||||
wire [15:0] m1 = (v[2] > v[3]) ? v[2] : v[3];
|
||||
wire [15:0] m2 = (v[4] > v[5]) ? v[4] : v[5];
|
||||
wire [15:0] m3 = (v[6] > v[7]) ? v[6] : v[7];
|
||||
wire [15:0] m4 = (v[8] > v[9]) ? v[8] : v[9];
|
||||
wire [15:0] m5 = (v[10] > v[11]) ? v[10] : v[11];
|
||||
wire [15:0] m6 = (v[12] > v[13]) ? v[12] : v[13];
|
||||
wire [15:0] m7 = (v[14] > v[15]) ? v[14] : v[15];
|
||||
wire [15:0] m01 = (m0 > m1) ? m0 : m1;
|
||||
wire [15:0] m23 = (m2 > m3) ? m2 : m3;
|
||||
wire [15:0] m45 = (m4 > m5) ? m4 : m5;
|
||||
wire [15:0] m67 = (m6 > m7) ? m6 : m7;
|
||||
wire [15:0] m0123 = (m01 > m23) ? m01 : m23;
|
||||
wire [15:0] m4567 = (m45 > m67) ? m45 : m67;
|
||||
wire [15:0] max_final = (m0123 > m4567) ? m0123 : m4567;
|
||||
|
||||
initial begin
|
||||
errors = 0; tests = 0; seed = 32'hA5A5F00D;
|
||||
|
||||
// targeted: all-zero, max at every single position
|
||||
for (k = 0; k < 16; k = k + 1) v[k] = 16'h0;
|
||||
#1; tests = tests + 1;
|
||||
if (max_final !== 16'h0) begin
|
||||
$display("FAIL all-zero: got %0d", max_final); errors = errors + 1;
|
||||
end
|
||||
for (i = 0; i < 16; i = i + 1) begin
|
||||
for (k = 0; k < 16; k = k + 1) v[k] = 16'h1;
|
||||
v[i] = 16'hFFFF;
|
||||
#1; tests = tests + 1;
|
||||
if (max_final !== 16'hFFFF) begin
|
||||
$display("FAIL max-at-%0d: got %0d", i, max_final); errors = errors + 1;
|
||||
end
|
||||
end
|
||||
|
||||
// random
|
||||
for (i = 0; i < 10000; i = i + 1) begin
|
||||
ref_max = 16'h0;
|
||||
for (k = 0; k < 16; k = k + 1) begin
|
||||
v[k] = $random(seed);
|
||||
if (v[k] > ref_max) ref_max = v[k];
|
||||
end
|
||||
#1;
|
||||
tests = tests + 1;
|
||||
if (max_final !== ref_max) begin
|
||||
$display("FAIL random iter=%0d: expected %0d got %0d", i, ref_max, max_final);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end
|
||||
|
||||
$display("=== %0d/%0d passed, %0d errors ===", tests-errors, tests, errors);
|
||||
if (errors == 0) $display("ALL TESTS PASSED (tb_maxtree_n16)");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,133 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0056 -- isolated correctness check for priority_encoder_lsb.v
|
||||
// against a trivial behavioral reference (linear scan, allowed to be
|
||||
// slow since it is testbench-only), at both a small width (16,
|
||||
// dependency_manager.v's own module default) and the real large width
|
||||
// this fix targets (1024, the real N_NODES this project actually
|
||||
// uses for D-Stress). Exhaustive at WIDTH=16 (65536 patterns), random
|
||||
// at WIDTH=1024 (exhaustive is infeasible: 2^1024 patterns).
|
||||
// ============================================================
|
||||
module tb;
|
||||
localparam W_SMALL = 16;
|
||||
localparam IDXW_SMALL = $clog2(W_SMALL);
|
||||
|
||||
reg [W_SMALL-1:0] in_small;
|
||||
wire [IDXW_SMALL-1:0] idx_small;
|
||||
wire valid_small;
|
||||
|
||||
priority_encoder_lsb #(.WIDTH(W_SMALL)) dut_small (
|
||||
.in(in_small), .idx(idx_small), .valid(valid_small)
|
||||
);
|
||||
|
||||
localparam W_BIG = 1024;
|
||||
localparam IDXW_BIG = $clog2(W_BIG);
|
||||
|
||||
reg [W_BIG-1:0] in_big;
|
||||
wire [IDXW_BIG-1:0] idx_big;
|
||||
wire valid_big;
|
||||
|
||||
priority_encoder_lsb #(.WIDTH(W_BIG)) dut_big (
|
||||
.in(in_big), .idx(idx_big), .valid(valid_big)
|
||||
);
|
||||
|
||||
function automatic integer ref_lowest_set_bit(input [W_BIG-1:0] v, input integer width);
|
||||
integer k;
|
||||
begin
|
||||
ref_lowest_set_bit = -1;
|
||||
for (k = width-1; k >= 0; k = k - 1)
|
||||
if (v[k]) ref_lowest_set_bit = k;
|
||||
end
|
||||
endfunction
|
||||
|
||||
integer errors, tests;
|
||||
integer i, ref_idx;
|
||||
integer seed;
|
||||
|
||||
initial begin
|
||||
errors = 0; tests = 0; seed = 32'hDEC0DE;
|
||||
|
||||
$display("=== TEST 1: WIDTH=16, exhaustive (65536 patterns) ===");
|
||||
for (i = 0; i < 65536; i = i + 1) begin
|
||||
in_small = i[W_SMALL-1:0];
|
||||
#1;
|
||||
ref_idx = ref_lowest_set_bit(i[W_BIG-1:0], W_SMALL);
|
||||
tests = tests + 1;
|
||||
if (ref_idx == -1) begin
|
||||
if (valid_small !== 1'b0) begin
|
||||
$display("FAIL pattern=%b: expected valid=0, got valid=%b", in_small, valid_small);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end else begin
|
||||
if (valid_small !== 1'b1 || idx_small !== ref_idx[IDXW_SMALL-1:0]) begin
|
||||
$display("FAIL pattern=%b: expected idx=%0d valid=1, got idx=%0d valid=%b",
|
||||
in_small, ref_idx, idx_small, valid_small);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
$display(" TEST 1: %0d/%0d passed", tests-errors, tests);
|
||||
|
||||
$display("=== TEST 2: WIDTH=1024, targeted + random (10000 patterns) ===");
|
||||
// targeted: all-zero, single-bit at every position, all-ones
|
||||
in_big = {W_BIG{1'b0}};
|
||||
#1;
|
||||
tests = tests + 1;
|
||||
if (valid_big !== 1'b0) begin
|
||||
$display("FAIL all-zero: expected valid=0, got valid=%b", valid_big);
|
||||
errors = errors + 1;
|
||||
end
|
||||
for (i = 0; i < W_BIG; i = i + 1) begin
|
||||
in_big = {W_BIG{1'b0}};
|
||||
in_big[i] = 1'b1;
|
||||
#1;
|
||||
tests = tests + 1;
|
||||
if (valid_big !== 1'b1 || idx_big !== i[IDXW_BIG-1:0]) begin
|
||||
$display("FAIL single-bit@%0d: expected idx=%0d valid=1, got idx=%0d valid=%b",
|
||||
i, i, idx_big, valid_big);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end
|
||||
in_big = {W_BIG{1'b1}};
|
||||
#1;
|
||||
ref_idx = ref_lowest_set_bit(in_big, W_BIG);
|
||||
tests = tests + 1;
|
||||
if (valid_big !== 1'b1 || idx_big !== ref_idx[IDXW_BIG-1:0]) begin
|
||||
$display("FAIL all-ones: expected idx=%0d valid=1, got idx=%0d valid=%b",
|
||||
ref_idx, idx_big, valid_big);
|
||||
errors = errors + 1;
|
||||
end
|
||||
// random
|
||||
for (i = 0; i < 10000; i = i + 1) begin
|
||||
in_big = {$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed),
|
||||
$random(seed), $random(seed), $random(seed), $random(seed)};
|
||||
#1;
|
||||
ref_idx = ref_lowest_set_bit(in_big, W_BIG);
|
||||
tests = tests + 1;
|
||||
if (ref_idx == -1) begin
|
||||
if (valid_big !== 1'b0) begin
|
||||
$display("FAIL random iter=%0d: expected valid=0, got valid=%b", i, valid_big);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end else begin
|
||||
if (valid_big !== 1'b1 || idx_big !== ref_idx[IDXW_BIG-1:0]) begin
|
||||
$display("FAIL random iter=%0d: expected idx=%0d valid=1, got idx=%0d valid=%b",
|
||||
i, ref_idx, idx_big, valid_big);
|
||||
errors = errors + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
$display(" TEST 2: %0d/%0d passed", tests-errors, tests);
|
||||
|
||||
$display("=== %0d/%0d total, %0d errors ===", tests-errors, tests, errors);
|
||||
if (errors == 0) $display("ALL TESTS PASSED (tb_priority_encoder_lsb)");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
Reference in New Issue
Block a user