Files
FPGA-Neural/hardware/v2/sim/tb_neural_multiprocessor.v
T
micheleandClaude Sonnet 5 91bbbe2fe5 feat(v2): M10 Optimization - data-driven findings, completes the V2 roadmap
Final milestone of docs/v2-description.md's §33 roadmap, scoped
exactly to its own mandate: optimize only on data already gathered in
M1-M9, across the pipeline/P_IN/processor-count/scheduling/memory
axes - no speculative new features.

Three concrete, data-driven results:

1. N_SLOTS=8 (numero processor axis): real synthesis + nextpnr-ecp5
   P&R for dataflow_core at N_SLOTS=8, extending M7's N_SLOTS=2/4
   sweep to the real DSP ceiling DEC-0005 predicted. 92.63 MHz
   POST-P&R, PASS at 80MHz, DSP 64/72 (88.9%). DEC-0012 recommends
   N_SLOTS=8 as the practical ceiling for P_IN=8 on the
   LFE5U-45F-8BG381.

2. ACC_WIDTH 24 vs 32 (pipeline axis): a real 6-seed nextpnr-ecp5
   placement sweep (reusing already-synthesized netlists, no new
   synthesis needed) resolves EXP-0002's single-seed
   inconclusiveness. ACC_WIDTH=24 wins on both mean Fmax (+6.2%,
   180.71 vs 170.12 MHz) and seed-to-seed variance (~3.4x tighter),
   on top of its already-known resource advantage. DEC-0013
   recommends ACC_WIDTH=24 as the new default.

3. Stall %/utilization (scheduling/memory axes): testbench-only cycle
   counters added to tb_neural_multiprocessor.v (no RTL touched)
   close DEC-0011's deferred measurement gap with real data - shared
   PSRAM port 81.7% utilized, slot 0 95.2%, slot 1 65.2%, no
   conclusive evidence of harmful fixed-priority starvation at this
   scale.

The 10-milestone V2 roadmap (docs/v2-description.md §33) is now
complete end-to-end: real Verilator simulation, real Yosys synthesis,
real nextpnr-ecp5 place & route for every milestone, fully logged
(EXP-0001..EXP-0013, DEC-0001..DEC-0013, ERR-0001..ERR-0008) with no
invented results (§30) and V1 kept frozen and untouched throughout
(§1/§34).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
2026-09-05 18:58:10 +02:00

227 lines
10 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// M8 testbench (docs/v2-description.md §15/§16/§20):
// neural_multiprocessor.v -- dataflow_core.v (M7, unmodified) sharing
// the REAL, UNMODIFIED V1 PSRAM backend chain (int8_memory_access ->
// memory_interface -> psram_controller -> real psram_model) across
// N_SLOTS=2 concurrent memory_manager instances for the first time,
// through the new slot_mem_arbiter.v (M8).
//
// Same DAG shape as tb_dataflow_core.v (M7's own test), REPLACING the
// per-slot behavioral memories with the single real PSRAM chain --
// this is the actual M8 measurement: does real arbitration/contention
// across genuinely-concurrent slots work correctly against real PSRAM
// timing (not an idealized 2-cycle behavioral model)?
//
// node0 (x=2,w=3,8in -> acc=48) --+
// +--> node2 (x=1,w=5,8in -> acc=40)
// node1 (x=1,w=1,8in -> acc=8) --+
//
// node0 and node1 are registered back-to-back with NO dependencies,
// so both are dispatched to the two available slots essentially
// simultaneously -- both memory_manager instances will genuinely
// contend for the one real PSRAM port at the same time, exercising
// slot_mem_arbiter.v's arbitration for real (not just in isolation).
//
// Verified with Verilator (decisions.log DEC-0004).
// ============================================================
module tb;
localparam ADDR_WIDTH = 23;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
localparam N_SLOTS = 2;
localparam N_NODES = 8;
localparam MAX_DEPS = 4;
localparam QUEUE_DEPTH = 4;
localparam NODE_IDW = $clog2(N_NODES);
localparam PSRAM_DATA_WIDTH = 16;
localparam CLK_PERIOD = 12.5; // 80 MHz, matches psram_controller's CLK_FREQ_MHZ
reg clk, rst;
initial begin clk = 1'b0; forever #(CLK_PERIOD/2.0) clk = ~clk; end
reg reg_valid;
wire reg_ready;
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;
wire [ADDR_WIDTH-1:0] psram_a;
wire [PSRAM_DATA_WIDTH-1:0] psram_dq;
wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n;
neural_multiprocessor #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.PSRAM_DATA_WIDTH(PSRAM_DATA_WIDTH), .CLK_FREQ_MHZ(80)
) u_nmp (
.clk(clk), .rst(rst),
.reg_valid(reg_valid), .reg_ready(reg_ready), .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),
.psram_a(psram_a), .psram_dq(psram_dq),
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
);
psram_model #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH), .DEPTH(16384)) u_psram (
.clk(clk), .a(psram_a), .dq(psram_dq),
.ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n),
.lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n)
);
task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] val);
reg [ADDR_WIDTH-2:0] word_addr;
begin
word_addr = byte_addr[ADDR_WIDTH-1:1];
if (byte_addr[0] == 1'b0)
u_psram.mem[word_addr][7:0] = val;
else
u_psram.mem[word_addr][15:8] = val;
end
endtask
function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr);
reg [ADDR_WIDTH-2:0] word_addr;
begin
word_addr = byte_addr[ADDR_WIDTH-1:1];
peek_byte = (byte_addr[0] == 1'b0) ? u_psram.mem[word_addr][7:0] : u_psram.mem[word_addr][15:8];
end
endfunction
task automatic register_node(
input [NODE_IDW-1:0] nid,
input [$clog2(MAX_DEPS+1)-1:0] required,
input [NODE_IDW-1:0] p0, input [NODE_IDW-1:0] p1,
input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb,
input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr
);
begin
@(posedge clk);
reg_node_id = nid;
reg_required = required;
reg_producer_ids = {NODE_IDW*MAX_DEPS{1'b0}};
reg_producer_ids[0*NODE_IDW +: NODE_IDW] = p0;
reg_producer_ids[1*NODE_IDW +: NODE_IDW] = p1;
reg_x_base = xb; reg_w_base = wb; reg_n_tiles = nt; reg_result_addr = resaddr;
reg_valid = 1'b1;
while (!reg_ready) @(posedge clk);
@(posedge clk);
reg_valid = 1'b0;
end
endtask
integer errors, tests;
integer i, wd;
// ---- M10 addition (decisions.log DEC-0011 closure): real
// cycle-accounting instrumentation, TESTBENCH-ONLY (no RTL
// touched) -- counts, from the moment node registration begins
// (measure_en) to $finish, how many cycles the shared PSRAM port
// is actually busy (slot_mem_arbiter's own `owner` field != NONE)
// and how many cycles each slot's memory_manager is busy (state
// != MM_IDLE), giving real SIMULATED stall %/utilization numbers
// for the exact same EXP-0009 scenario instead of leaving them
// unmeasured. ----
reg measure_en;
integer measured_cycles;
integer psram_busy_cycles;
integer slot0_busy_cycles, slot1_busy_cycles;
always @(posedge clk) begin
if (measure_en) begin
measured_cycles <= measured_cycles + 1;
if (u_nmp.u_arbiter.owner != 0) psram_busy_cycles <= psram_busy_cycles + 1;
if (u_nmp.u_dataflow_core.GEN_SLOT[0].u_mm.state != 3'd0) slot0_busy_cycles <= slot0_busy_cycles + 1;
if (u_nmp.u_dataflow_core.GEN_SLOT[1].u_mm.state != 3'd0) slot1_busy_cycles <= slot1_busy_cycles + 1;
end
end
initial begin
errors = 0; tests = 0;
measure_en = 1'b0; measured_cycles = 0; psram_busy_cycles = 0;
slot0_busy_cycles = 0; slot1_busy_cycles = 0;
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;
repeat(5) @(posedge clk);
rst = 0;
// Real PSRAM power-up sequence (~150us @ 80MHz) -- same
// requirement/convention as tb_memory_manager.v (M4).
wait (u_nmp.u_psram_ctrl.state == u_nmp.u_psram_ctrl.STATE_IDLE);
@(posedge clk);
measure_en = 1'b1;
for (i = 0; i < 8; i = i + 1) begin
poke_byte(23'h10+i, 8'sd2); poke_byte(23'h20+i, 8'sd3); // node0: x=2,w=3
poke_byte(23'h30+i, 8'sd1); poke_byte(23'h40+i, 8'sd1); // node1: x=1,w=1
poke_byte(23'h50+i, 8'sd1); poke_byte(23'h60+i, 8'sd5); // node2: x=1,w=5
end
poke_byte(23'h70, 8'sd0); poke_byte(23'h71, 8'sd0); poke_byte(23'h72, 8'sd0);
// node0, node1: no dependencies -- dispatched back-to-back, so
// BOTH slots start genuinely concurrent PSRAM traffic through
// the shared arbiter at essentially the same time.
register_node(0, 0, 0, 0, 23'h10, 23'h20, 16'd1, 23'h70);
register_node(1, 0, 0, 0, 23'h30, 23'h40, 16'd1, 23'h71);
register_node(2, 2, 0, 1, 23'h50, 23'h60, 16'd1, 23'h72);
tests = tests + 1;
wd = 0;
while ((peek_byte(23'h70)==0 || peek_byte(23'h71)==0) && wd < 20000) begin
if (peek_byte(23'h72) !== 8'sd0) begin
$display("FAIL: node2 completed before both node0 and node1 finished");
errors = errors + 1;
end
@(posedge clk); wd = wd + 1;
end
$display("PASS: node2 did not complete before both its dependencies did (checked every cycle up to wd=%0d)", wd);
wd = 0;
while (peek_byte(23'h72)==0 && wd < 20000) begin @(posedge clk); wd = wd + 1; end
repeat(10) @(posedge clk);
tests = tests + 3;
if (peek_byte(23'h70) !== 8'sd48) begin
$display("FAIL node0: result=%0d expected 48", peek_byte(23'h70));
errors = errors + 1;
end else $display("PASS node0: result=48 via real PSRAM + shared arbiter");
if (peek_byte(23'h71) !== 8'sd8) begin
$display("FAIL node1: result=%0d expected 8", peek_byte(23'h71));
errors = errors + 1;
end else $display("PASS node1: result=8 via real PSRAM + shared arbiter (concurrent with node0)");
if (peek_byte(23'h72) !== 8'sd40) begin
$display("FAIL node2: result=%0d expected 40", peek_byte(23'h72));
errors = errors + 1;
end else $display("PASS node2: result=40, dispatched only after BOTH producers genuinely completed, real PSRAM end-to-end");
measure_en = 1'b0;
$display("========================================");
$display("M10 cycle-accounting (DEC-0011 closure, real SIMULATED data):");
$display(" measured cycles (from first registration to completion): %0d", measured_cycles);
$display(" shared PSRAM port busy: %0d/%0d cycles (%0.1f%% utilization, %0.1f%% idle)",
psram_busy_cycles, measured_cycles,
100.0*psram_busy_cycles/measured_cycles, 100.0*(measured_cycles-psram_busy_cycles)/measured_cycles);
$display(" slot 0 (memory_manager) busy: %0d/%0d cycles (%0.1f%% utilization)",
slot0_busy_cycles, measured_cycles, 100.0*slot0_busy_cycles/measured_cycles);
$display(" slot 1 (memory_manager) busy: %0d/%0d cycles (%0.1f%% utilization)",
slot1_busy_cycles, measured_cycles, 100.0*slot1_busy_cycles/measured_cycles);
$display("========================================");
if (errors == 0)
$display("ALL %0d TESTS PASSED (neural_multiprocessor, real V1 PSRAM chain shared across N_SLOTS=%0d via slot_mem_arbiter)", tests, N_SLOTS);
else
$display("FAILED: %0d/%0d test(s) had errors -- see messages above", errors, tests);
$display("========================================");
$finish;
end
endmodule