Files
FPGA-Neural/hardware/v2/sim/tb_memory_manager.v
T
micheleandClaude Sonnet 5 63cac6a7e5 perf(v2): shared activation cache - further 1.66-2.00x real speedup (DEC-0016)
Implements optimization #2 from the final benchmark campaign's own
recommendation, on top of DEC-0015's word-level burst rewrite: a new
shared activation_cache.v module fetches a given activation (X)
vector from PSRAM once instead of once per neuron sharing it - the
exact redundant traffic pattern the dense-layer workloads in this
project's benchmark suite exhibit.

Each memory_manager's own prefetch_engine now fetches WEIGHTS only;
the activation half is requested from the shared cache instead
(single-tag, tile-granular, N_SLOTS request ports, its own real
word-level PSRAM backend via a new dedicated arbiter port).
dataflow_core.v/slot_mem_arbiter.v/neural_multiprocessor.v widened to
N_SLOTS+1 ports to arbitrate the cache's traffic alongside each
slot's weight traffic.

Two real bugs found and fixed during implementation (ERR-0010): a
target-bank/pending-bank race in memory_manager.v's activation-cache
wiring (the same bug class ERR-0006 already fixed once for
pf_target_bank - a later handoff's queued request can overwrite which
bank an earlier, still-in-flight request's ack applies to), and a
repeat of ERR-0009's N_SLOTS=1 zero-width replication bug in
activation_cache.v itself.

Real, measured results: the full final-benchmark campaign (24/24
workload/config combinations) re-verified bit-exact. D-Stress cycles
fall a further 1.66-2.00x on top of DEC-0015 (~4x combined vs the
original byte-level baseline). But the cache's real Fmax cost is much
steeper than DEC-0015's own: N_SLOTS=2 (the recommended default,
DEC-0014) drops from 133.58 to 87.72 MHz (-34%, margin over 80MHz
shrinks from +67% to +9.7%), and N_SLOTS=4 drops to 65.01 MHz - now
FAILING the 80MHz target it previously passed. Combined real
wall-clock speedup vs the original baseline: N=1 3.86x, N=2 2.45x
(both real net wins); N=4 is a real regression once its own now-failing
Fmax is honestly used, though N=4 was never the recommended
configuration.

N_SLOTS=2 remains the recommended default (DEC-0014 unaffected) with
a thinner but still real Fmax margin. Cache hit-detection pipelining
is flagged as concrete follow-up work if N_SLOTS>2 is ever needed with
the cache active - not attempted this round.

Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0016)/
experiments (EXP-0016)/errors (ERR-0010)/development.log, ROADMAP.md
updated.

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

338 lines
15 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// M4 testbench (docs/v2-description.md §12/§13/§15/§20): full
// end-to-end stack -- memory_manager.v + prefetch_engine.v (V2, M4)
// driving a REAL hardware/v2/rtl/neural_processor.v (M1) on one side,
// and the REAL, UNMODIFIED hardware/v1 PSRAM backend chain
// (int8_memory_access -> memory_interface -> psram_controller ->
// psram_model) on the other -- exactly the layering §15 mandates
// ("Memory Manager -> Memory Backend Interface -> PSRAM Controller"),
// with the backend reused byte-for-byte from the frozen V1 tree.
//
// Verified with Verilator (see decisions.log DEC-0004).
//
// Coverage:
// - end-to-end job: PSRAM pre-loaded with real X/W bytes at known
// addresses, memory_manager fetches them (double-buffered
// prefetch across multiple tiles), feeds neural_processor, and
// writes the computed result back to PSRAM -- read back
// independently afterward and checked against a hand-computed
// expectation (an oracle independent of the RTL under test).
// - multi-tile job (prefetch actually has to overlap tile N+1's
// fetch with tile N's compute, not just single-tile).
// - "poison" bytes surrounding the real operand region, to catch
// any off-by-one addressing error in prefetch_engine.
// ============================================================
module tb;
localparam ADDR_WIDTH = 23;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
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
// ---- memory_manager <-> neural_processor ----
reg job_start;
reg [ADDR_WIDTH-1:0] x_base, w_base, result_addr;
reg [15:0] n_tiles;
wire job_done;
wire mm_operand_valid, mm_operand_ready;
wire signed [DATA_WIDTH*P_IN-1:0] mm_input_data, mm_weight_data;
wire mm_tile_last;
wire mm_result_valid, mm_result_ready;
wire signed [DATA_WIDTH-1:0] mm_result_data;
// ---- memory_manager's own WEIGHT backend port (word-level Memory
// Backend Interface, post-M10 DEC-0015) ----
wire mm_mem_req, mm_mem_wr;
wire [ADDR_WIDTH-1:0] mm_mem_addr; // WORD address
wire [15:0] mm_mem_wdata;
wire mm_mem_lb_n, mm_mem_ub_n;
wire [15:0] mm_mem_rdata;
wire mm_mem_ready;
// ---- shared activation_cache (M10+, DEC-0016) -- N_SLOTS=1 here
// (a single memory_manager instance), routed through a real 2-port
// arbiter (weight port + cache port) into the SAME real
// memory_interface, mirroring dataflow_core.v/neural_multiprocessor.v's
// own real structure exactly, just scoped down to one slot. ----
wire xc_req;
wire [ADDR_WIDTH-1:0] xc_x_base;
wire [15:0] xc_tile_idx;
wire xc_ack;
wire signed [DATA_WIDTH*P_IN-1:0] xc_tile_x;
wire xc_mem_req, xc_mem_wr;
wire [ADDR_WIDTH-1:0] xc_mem_addr;
wire [15:0] xc_mem_wdata;
wire xc_mem_lb_n, xc_mem_ub_n;
wire [15:0] xc_mem_rdata;
wire xc_mem_ready;
activation_cache #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(1)
) u_xcache (
.clk(clk), .rst(rst),
.req(xc_req), .req_x_base(xc_x_base), .req_tile_idx(xc_tile_idx),
.ack(xc_ack), .tile_x_out(xc_tile_x),
.mem_req(xc_mem_req), .mem_wr(xc_mem_wr), .mem_addr(xc_mem_addr), .mem_wdata(xc_mem_wdata),
.mem_lb_n(xc_mem_lb_n), .mem_ub_n(xc_mem_ub_n),
.mem_rdata(xc_mem_rdata), .mem_ready(xc_mem_ready)
);
wire [1:0] arb2_req = {xc_mem_req, mm_mem_req};
wire [1:0] arb2_wr = {xc_mem_wr, mm_mem_wr};
wire [ADDR_WIDTH*2-1:0] arb2_addr = {xc_mem_addr, mm_mem_addr};
wire [31:0] arb2_wdata = {xc_mem_wdata, mm_mem_wdata};
wire [1:0] arb2_lb_n = {xc_mem_lb_n, mm_mem_lb_n};
wire [1:0] arb2_ub_n = {xc_mem_ub_n, mm_mem_ub_n};
wire [31:0] arb2_rdata;
wire [1:0] arb2_ready;
assign mm_mem_rdata = arb2_rdata[15:0];
assign mm_mem_ready = arb2_ready[0];
assign xc_mem_rdata = arb2_rdata[31:16];
assign xc_mem_ready = arb2_ready[1];
wire arb_m_req, arb_m_wr;
wire [ADDR_WIDTH-1:0] arb_m_addr;
wire [15:0] arb_m_wdata;
wire arb_m_lb_n, arb_m_ub_n;
wire [15:0] arb_m_rdata;
wire arb_m_ready;
slot_mem_arbiter #(.ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(2)) u_arb2 (
.clk(clk), .rst(rst),
.s_req(arb2_req), .s_wr(arb2_wr), .s_addr(arb2_addr),
.s_wdata(arb2_wdata), .s_lb_n(arb2_lb_n), .s_ub_n(arb2_ub_n),
.s_rdata(arb2_rdata), .s_ready(arb2_ready),
.m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
.m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
.m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
);
memory_manager #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH)
) u_mm (
.clk(clk), .rst(rst),
.job_start(job_start), .x_base(x_base), .w_base(w_base),
.n_tiles(n_tiles), .result_addr(result_addr), .job_done(job_done),
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
.result_valid(mm_result_valid), .result_ready(mm_result_ready), .result_data(mm_result_data),
.xc_req(xc_req), .xc_x_base(xc_x_base), .xc_tile_idx(xc_tile_idx),
.xc_ack(xc_ack), .xc_tile_x(xc_tile_x),
.mem_req(mm_mem_req), .mem_wr(mm_mem_wr), .mem_addr(mm_mem_addr), .mem_wdata(mm_mem_wdata),
.mem_lb_n(mm_mem_lb_n), .mem_ub_n(mm_mem_ub_n),
.mem_rdata(mm_mem_rdata), .mem_ready(mm_mem_ready)
);
// ---- real Neural Processor (M1), driven entirely by memory_manager ----
reg job_valid_np;
wire job_ready_np;
wire result_valid_np;
wire signed [DATA_WIDTH-1:0] result_data_np;
wire [15:0] result_node_id_np;
wire [3:0] np_state;
wire np_error;
neural_processor #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)
) u_np (
.clk(clk), .rst(rst),
.job_valid(job_valid_np), .job_ready(job_ready_np),
.job_node_id(16'h0), .job_bias(8'sd0), .job_activation(2'd1), // ACT_RELU
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
.result_valid(result_valid_np), .result_ready(mm_result_ready),
.result_data(result_data_np), .result_node_id(result_node_id_np),
.np_state(np_state), .np_error(np_error)
);
assign mm_result_valid = result_valid_np;
assign mm_result_data = result_data_np;
// job_valid_np must pulse once per memory_manager job, synchronized
// to job_start (both start a "job" at the same moment: memory_manager
// begins prefetching tile 0 while neural_processor waits in NP_IDLE
// until tile 0 actually arrives, exactly like any other operand
// producer feeding it).
always @(posedge clk) begin
if (rst) job_valid_np <= 1'b0;
else if (job_start) job_valid_np <= 1'b1;
else if (job_valid_np && job_ready_np) job_valid_np <= 1'b0;
end
// ---- REAL, unmodified V1 backend chain (memory_interface ->
// psram_controller; int8_memory_access no longer in this datapath,
// see memory_manager.v's own header, DEC-0015) ----
wire pc_mem_req, pc_mem_wr;
wire [ADDR_WIDTH-1:0] pc_mem_addr;
wire [PSRAM_DATA_WIDTH-1:0] pc_mem_wdata;
wire pc_mem_lb_n, pc_mem_ub_n;
wire [PSRAM_DATA_WIDTH-1:0] pc_mem_rdata;
wire pc_mem_ready;
memory_interface #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH)) u_memif (
.clk(clk), .rst(rst),
.req(arb_m_req), .wr(arb_m_wr), .addr(arb_m_addr), .wdata(arb_m_wdata),
.lb_n(arb_m_lb_n), .ub_n(arb_m_ub_n),
.rdata(arb_m_rdata), .ready(arb_m_ready),
.mem_req(pc_mem_req), .mem_wr(pc_mem_wr), .mem_addr(pc_mem_addr), .mem_wdata(pc_mem_wdata),
.mem_lb_n(pc_mem_lb_n), .mem_ub_n(pc_mem_ub_n),
.mem_rdata(pc_mem_rdata), .mem_ready(pc_mem_ready)
);
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;
psram_controller #(
.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH), .CLK_FREQ_MHZ(80)
) u_psram_ctrl (
.clk(clk), .rst(rst),
.mem_req(pc_mem_req), .mem_wr(pc_mem_wr), .mem_addr(pc_mem_addr), .mem_wdata(pc_mem_wdata),
.mem_lb_n(pc_mem_lb_n), .mem_ub_n(pc_mem_ub_n),
.mem_rdata(pc_mem_rdata), .mem_ready(pc_mem_ready),
.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)
);
// ---- helper: poke one byte directly into psram_model's backing
// array (test setup only, bypasses the real write path -- same
// convention as hardware/v1/sim's own testbenches that pre-load
// psram_model for read-side tests). ----
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
task automatic peek_byte(input [ADDR_WIDTH-1:0] byte_addr, output [7:0] val);
reg [ADDR_WIDTH-2:0] word_addr;
begin
word_addr = byte_addr[ADDR_WIDTH-1:1];
val = (byte_addr[0] == 1'b0) ? u_psram.mem[word_addr][7:0] : u_psram.mem[word_addr][15:8];
end
endtask
integer errors, tests;
integer i;
function automatic signed [7:0] expect_relu(input integer acc);
begin
if (acc <= 0) expect_relu = 0;
else if (acc > 127) expect_relu = 127;
else expect_relu = acc[7:0];
end
endfunction
task automatic run_job(
input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb,
input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr,
input signed [7:0] exp_y
);
integer wd;
reg [7:0] rb;
begin
@(posedge clk);
tests = tests + 1;
x_base = xb; w_base = wb; n_tiles = nt; result_addr = resaddr;
job_start = 1'b1;
@(posedge clk);
job_start = 1'b0;
wd = 0;
while (!job_done && wd < 2000) begin
@(posedge clk);
wd = wd + 1;
end
if (!job_done) begin
$display("FAIL job xb=%0d: no job_done within watchdog (%0d cycles)", xb, wd);
errors = errors + 1;
end else begin
peek_byte(resaddr, rb);
if (rb !== exp_y[7:0]) begin
$display("FAIL job xb=%0d: PSRAM result byte=%0d expected=%0d (%0d cycles)", xb, $signed(rb), exp_y, wd);
errors = errors + 1;
end else begin
$display("PASS job xb=%0d: PSRAM result byte=%0d correct, %0d cycles, n_tiles=%0d", xb, $signed(rb), wd, nt);
end
end
end
endtask
initial begin
errors = 0; tests = 0;
rst = 1; job_start = 0; x_base = 0; w_base = 0; n_tiles = 0; result_addr = 0;
repeat(5) @(posedge clk);
rst = 0;
// Wait for the real PSRAM controller's power-up sequence
// (~150us @ 80MHz) before issuing any request -- same
// requirement/convention documented in
// hardware/v1/docs/FPGA-NeuralNetwork-Engine.md's
// WRITE_RAM/READ_RAM backpressure warning.
wait (u_psram_ctrl.state == u_psram_ctrl.STATE_IDLE);
@(posedge clk);
// ---- pre-load PSRAM: X at 0x1000, W at 0x2000, 3 tiles
// (24 inputs), with "poison" bytes immediately before/after
// the real region to catch any off-by-one in prefetch_engine's
// addressing. ----
for (i = -4; i < 24+4; i = i + 1) begin
poke_byte(23'h1000 + i, 8'sd99); // poison
poke_byte(23'h2000 + i, 8'sd99); // poison
end
for (i = 0; i < 24; i = i + 1) begin
poke_byte(23'h1000 + i, 8'sd2); // X = 2
poke_byte(23'h2000 + i, 8'sd3); // W = 3
end
// acc = 24 * 2 * 3 = 144 -> ACT_RELU saturates to 127
run_job(23'h1000, 23'h2000, 16'd3, 23'h3000, expect_relu(144));
// ---- second job: 1 tile (8 inputs), smaller, no saturation ----
for (i = 0; i < 8; i = i + 1) begin
poke_byte(23'h4000 + i, 8'sd1); // X = 1
poke_byte(23'h5000 + i, 8'sd4); // W = 4
end
// acc = 8*1*4 = 32
run_job(23'h4000, 23'h5000, 16'd1, 23'h3001, expect_relu(32));
// ---- third job: 5 tiles (40 inputs), exercises the
// steady-state double-buffer swap across more than 2 tiles. ----
for (i = 0; i < 40; i = i + 1) begin
poke_byte(23'h6000 + i, 8'sd1); // X = 1
poke_byte(23'h7000 + i, 8'sd1); // W = 1
end
// acc = 40*1*1 = 40
run_job(23'h6000, 23'h7000, 16'd5, 23'h3002, expect_relu(40));
$display("========================================");
if (errors == 0)
$display("ALL %0d TESTS PASSED (memory_manager + prefetch_engine, real V1 PSRAM backend, real neural_processor)", tests);
else
$display("FAILED: %0d/%0d test(s) had errors -- see messages above", errors, tests);
$display("========================================");
$finish;
end
endmodule