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:
2026-09-16 12:01:11 +02:00
co-authored by Claude Sonnet 5
parent fce8ff2d66
commit 1ce78dff6e
25 changed files with 25848 additions and 0 deletions
@@ -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
+141
View File
@@ -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
+72
View File
@@ -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
+133
View File
@@ -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