`timescale 1ns/1ps // ============================================================ // EXP-0090 -- isolated correctness test for neural_director_grouped.v // (does NOT instantiate real systolic_group.v -- this test verifies // the Director's own queue/octet-matching/dispatch logic in isolation, // same "one variable at a time" discipline as every other new module // in this project). Checks: // 1. 8 matching job descriptors (same w_base/n_tiles) correctly // dispatch as ONE group job, with the right per-PE x_base_a/b // assignment (positions 0,1 -> PE0 a/b, 2,3 -> PE1 a/b, etc). // 2. A queue with a MISMATCHED w_base among the first 8 correctly // STALLS (does not dispatch, does not error, does not silently // mis-pair) -- matches neural_director_packed.v's own real, // established "wrong dispatch must never happen" standard. // 3. group_job_done correctly frees the group for a second dispatch. // 4. Queue wraparound (q_head/q_tail crossing the QUEUE_DEPTH // boundary) is exercised, not just a cold-start scenario. // ============================================================ module tb; localparam ADDR_WIDTH = 26; localparam N_GROUPS = 4; localparam QUEUE_DEPTH = 16; localparam CLK_PERIOD_NS = 10.0; reg clk = 0; always #(CLK_PERIOD_NS/2.0) clk = ~clk; reg rst; reg job_in_valid; wire job_in_ready; reg [ADDR_WIDTH-1:0] job_in_x_base, job_in_w_base, job_in_result_addr; reg [15:0] job_in_n_tiles, job_in_node_id; wire [N_GROUPS-1:0] group_job_start; wire [ADDR_WIDTH*N_GROUPS-1:0] group_w_base; wire [16*N_GROUPS-1:0] group_n_tiles; wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_x_base_a, group_pe_x_base_b; wire [4*ADDR_WIDTH*N_GROUPS-1:0] group_pe_result_addr_a, group_pe_result_addr_b; wire [4*16*N_GROUPS-1:0] group_pe_node_id_a, group_pe_node_id_b; reg [N_GROUPS-1:0] group_job_done; wire job_out_done; wire [$clog2(N_GROUPS)-1:0] job_out_group; wire [3:0] dir_state; wire dir_error; wire queue_empty; neural_director_grouped #( .ADDR_WIDTH(ADDR_WIDTH), .N_GROUPS(N_GROUPS), .QUEUE_DEPTH(QUEUE_DEPTH) ) dut ( .clk(clk), .rst(rst), .job_in_valid(job_in_valid), .job_in_ready(job_in_ready), .job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base), .job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr), .job_in_node_id(job_in_node_id), .group_job_start(group_job_start), .group_w_base(group_w_base), .group_n_tiles(group_n_tiles), .group_pe_x_base_a(group_pe_x_base_a), .group_pe_x_base_b(group_pe_x_base_b), .group_pe_result_addr_a(group_pe_result_addr_a), .group_pe_result_addr_b(group_pe_result_addr_b), .group_pe_node_id_a(group_pe_node_id_a), .group_pe_node_id_b(group_pe_node_id_b), .group_job_done(group_job_done), .job_out_done(job_out_done), .job_out_group(job_out_group), .dir_state(dir_state), .dir_error(dir_error), .queue_empty(queue_empty) ); integer errors, tests; // real, root-caused fix (not guessed): driving job_in_valid on // @(posedge clk) -- the SAME edge the DUT's own always block // samples on -- races the DUT when submit_job is called back-to- // back with zero real simulated gap (as submit_octet's own tight // loop does): confirmed via real signal tracing that this // produced a genuine DOUBLE registration, every logical push // landing in TWO consecutive real queue slots with identical data // (not a cosmetic/display artifact -- the DUT's own q_tail/q_count // genuinely advanced twice per call). Standard, established fix: // drive stimulus on the OPPOSITE edge (@(negedge clk)) from what // the DUT samples on, so a value change can never race the DUT's // own posedge-triggered sampling -- same underlying race family as // this project's own documented "testbench stimulus must use // nonblocking assignment" lesson (CLAUDE.md), now also confirmed // to require edge separation, not just assignment-type discipline, // for tight back-to-back pulse sequences with no natural gap. task automatic submit_job(input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb, input [15:0] nt, input [ADDR_WIDTH-1:0] ra, input [15:0] nid); begin @(negedge clk); job_in_valid = 1'b1; job_in_x_base = xb; job_in_w_base = wb; job_in_n_tiles = nt; job_in_result_addr = ra; job_in_node_id = nid; @(negedge clk); job_in_valid = 1'b0; end endtask // submit an octet of 8 matching (same w_base/n_tiles) jobs at // positions base_pos..base_pos+7 task automatic submit_octet(input [ADDR_WIDTH-1:0] wb, input [15:0] nt, input integer base_pos); integer k; begin for (k = 0; k < 8; k = k + 1) submit_job(26'h10000 + base_pos + k, wb, nt, 26'h9000 + base_pos + k, base_pos + k); end endtask integer wd; task automatic wait_group_dispatch(input integer max_wd); begin wd = 0; while (!(|group_job_start) && wd < max_wd) begin @(posedge clk); wd = wd + 1; end end endtask integer g, p; task automatic check_dispatch(input [ADDR_WIDTH-1:0] wb, input [15:0] nt, input integer base_pos); begin tests = tests + 1; wait_group_dispatch(200); if (!(|group_job_start)) begin $display("FAIL base_pos=%0d: TIMEOUT waiting for group_job_start", base_pos); errors = errors + 1; end else begin g = -1; for (p = 0; p < N_GROUPS; p = p + 1) if (group_job_start[p]) g = p; if (group_w_base[g*ADDR_WIDTH +: ADDR_WIDTH] !== wb || group_n_tiles[g*16 +: 16] !== nt) begin $display("FAIL base_pos=%0d: group%0d w_base/n_tiles mismatch (got w=%0h n=%0d exp w=%0h n=%0d)", base_pos, g, group_w_base[g*ADDR_WIDTH +: ADDR_WIDTH], group_n_tiles[g*16 +: 16], wb, nt); errors = errors + 1; end else begin for (p = 0; p < 4; p = p + 1) begin if (group_pe_x_base_a[(g*4+p)*ADDR_WIDTH +: ADDR_WIDTH] !== (26'h10000 + base_pos + p*2) || group_pe_x_base_b[(g*4+p)*ADDR_WIDTH +: ADDR_WIDTH] !== (26'h10000 + base_pos + p*2 + 1)) begin $display("FAIL base_pos=%0d group%0d PE%0d: x_base_a/b mismatch (got a=%0h b=%0h)", base_pos, g, p, group_pe_x_base_a[(g*4+p)*ADDR_WIDTH +: ADDR_WIDTH], group_pe_x_base_b[(g*4+p)*ADDR_WIDTH +: ADDR_WIDTH]); errors = errors + 1; end end $display("PASS base_pos=%0d: dispatched to group%0d, w_base=%0h n_tiles=%0d, PE x_base assignment correct", base_pos, g, wb, nt); end // simulate the group finishing its job after a few cycles repeat (5) @(posedge clk); group_job_done[g] = 1'b1; @(posedge clk); group_job_done[g] = 1'b0; end end endtask initial begin errors = 0; tests = 0; rst = 1; job_in_valid = 0; job_in_x_base = 0; job_in_w_base = 0; job_in_n_tiles = 0; job_in_result_addr = 0; job_in_node_id = 0; group_job_done = 0; repeat(5) @(posedge clk); rst = 0; @(posedge clk); $display("=== test 1: single octet, correct group dispatch + PE x_base assignment ==="); submit_octet(26'h1000, 16'd16, 0); check_dispatch(26'h1000, 16'd16, 0); $display("=== test 2: second octet, DIFFERENT w_base, correct dispatch ==="); submit_octet(26'h2000, 16'd32, 100); check_dispatch(26'h2000, 16'd32, 100); $display("=== test 3: mismatched w_base among the 8 oldest -- must STALL, not mis-dispatch ==="); // 7 matching + 1 mismatched. Real, established Director // behavior (same as neural_director_packed.v's own pairing // rule): once a mismatched entry is within the oldest-8 // window, q_head can never advance past it (nothing before it // can ever be dispatched without it) -- the queue permanently // stalls, a visible, diagnosable symptom, matching this // module's own disclosed real design. There is no in-band // recovery from a real submitter mistake like this (same real // limitation neural_director_packed.v already has for pairs) -- // a real reset is the only way to clear it, which is exactly // what this test does before moving on, not a workaround. submit_job(26'h10000+200, 26'h3000, 16'd8, 26'h9000+200, 200); submit_job(26'h10000+201, 26'h3000, 16'd8, 26'h9000+201, 201); submit_job(26'h10000+202, 26'h3000, 16'd8, 26'h9000+202, 202); submit_job(26'h10000+203, 26'h3000, 16'd8, 26'h9000+203, 203); submit_job(26'h10000+204, 26'h3000, 16'd8, 26'h9000+204, 204); submit_job(26'h10000+205, 26'h3000, 16'd8, 26'h9000+205, 205); submit_job(26'h10000+206, 26'h3000, 16'd8, 26'h9000+206, 206); submit_job(26'h10000+207, 26'h4000 /* MISMATCH */, 16'd8, 26'h9000+207, 207); tests = tests + 1; wait_group_dispatch(300); if (|group_job_start) begin $display("FAIL: group dispatched despite a real w_base mismatch among the 8 oldest entries -- WRONG DISPATCH"); errors = errors + 1; end else begin $display("PASS: correctly stalled (no dispatch) on mismatched octet, dir_error=%0b, queue_empty=%0b", dir_error, queue_empty); end // real reset to clear the deliberately-stalled queue before // continuing -- not a workaround, the only real recovery path. rst = 1; job_in_valid = 0; group_job_done = 0; repeat(5) @(posedge clk); rst = 0; @(posedge clk); $display("=== test 4: queue wraparound (QUEUE_DEPTH=%0d boundary) ===", QUEUE_DEPTH); submit_octet(26'h5000, 16'd4, 400); check_dispatch(26'h5000, 16'd4, 400); $display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors); if (errors == 0) $display("ALL TESTS PASSED (tb_neural_director_grouped)"); $finish; end endmodule