Files
FPGA-Neural/hardware/v2/ddr3/tb_litedram_bandwidth.v
T
micheleandClaude Sonnet 5 1ce78dff6e 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
2026-09-16 12:01:11 +02:00

165 lines
6.9 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// DDR3 exploration -- real measured sustained bandwidth of the
// generated litedram_core_sim.v (LiteDRAM standalone core, ECP5DDRPHY,
// DDR3 MT41K256M16 x16/512MB, sys_clk_freq=75MHz -- config in
// hardware/v2/ddr3/litedram_gen/ecp5_85f_ddr3_mt41k256m16.yml, the
// SAME real chip+clock the real ECPIX-5 board ships with, not a
// tuned/optimistic guess).
//
// Drives the native port (cmd/wdata/rdata, standard LiteX stream
// handshake -- see litedram/common.py's own cmd_description/
// wdata_description/rdata_description) directly, in strict lockstep
// per transaction (issue cmd, wait ready, push/pull the matching data
// phase, wait ready) -- this is a conservative lower bound on
// achievable bandwidth (no command pipelining attempted), reported as
// such, not claimed as the ceiling.
//
// N_TRANSACTIONS sequential 128-bit (16-byte) writes, then the same
// addresses read back and checked bit-exact against the write
// pattern, with real cycle counts converted to real MB/s using the
// declared sys_clk_freq.
// ============================================================
module tb;
localparam ADDR_WIDTH = 25;
localparam DATA_WIDTH = 128;
localparam WE_WIDTH = DATA_WIDTH/8;
localparam real SYS_CLK_FREQ_MHZ = 75.0;
localparam real CLK_PERIOD_NS = 1000.0/SYS_CLK_FREQ_MHZ;
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
integer cyc;
always @(posedge clk) cyc <= cyc + 1;
wire init_done, init_error, user_clk, user_rst;
reg [ADDR_WIDTH-1:0] cmd_addr;
wire cmd_ready;
reg cmd_valid, cmd_we;
wire [DATA_WIDTH-1:0] rdata_data;
reg rdata_ready;
wire rdata_valid;
reg [DATA_WIDTH-1:0] wdata_data;
wire wdata_ready;
reg wdata_valid;
reg [WE_WIDTH-1:0] wdata_we;
// wb_ctrl_* left disconnected (tied off) -- this benchmark drives
// the native port only, no CSR/wishbone control path needed.
wire wb_ctrl_ack, wb_ctrl_err;
wire [31:0] wb_ctrl_dat_r;
litedram_core_sim dut (
.clk(clk),
.init_done(init_done), .init_error(init_error),
.sim_trace(1'b0),
.user_clk(user_clk), .user_rst(user_rst),
.user_port_native_0_cmd_addr(cmd_addr),
.user_port_native_0_cmd_ready(cmd_ready),
.user_port_native_0_cmd_valid(cmd_valid),
.user_port_native_0_cmd_we(cmd_we),
.user_port_native_0_rdata_data(rdata_data),
.user_port_native_0_rdata_ready(rdata_ready),
.user_port_native_0_rdata_valid(rdata_valid),
.user_port_native_0_wdata_data(wdata_data),
.user_port_native_0_wdata_ready(wdata_ready),
.user_port_native_0_wdata_valid(wdata_valid),
.user_port_native_0_wdata_we(wdata_we),
.wb_ctrl_ack(wb_ctrl_ack), .wb_ctrl_adr(30'h0), .wb_ctrl_bte(2'h0),
.wb_ctrl_cti(3'h0), .wb_ctrl_cyc(1'b0), .wb_ctrl_dat_r(wb_ctrl_dat_r),
.wb_ctrl_dat_w(32'h0), .wb_ctrl_err(wb_ctrl_err), .wb_ctrl_sel(4'h0),
.wb_ctrl_stb(1'b0), .wb_ctrl_we(1'b0)
);
task automatic do_write(input [ADDR_WIDTH-1:0] a, input [DATA_WIDTH-1:0] d);
begin
cmd_valid = 1'b1; cmd_we = 1'b1; cmd_addr = a;
@(posedge clk);
while (!cmd_ready) @(posedge clk);
cmd_valid = 1'b0;
wdata_valid = 1'b1; wdata_data = d; wdata_we = {WE_WIDTH{1'b1}};
@(posedge clk);
while (!wdata_ready) @(posedge clk);
wdata_valid = 1'b0;
end
endtask
task automatic do_read(input [ADDR_WIDTH-1:0] a, output [DATA_WIDTH-1:0] d);
begin
cmd_valid = 1'b1; cmd_we = 1'b0; cmd_addr = a;
@(posedge clk);
while (!cmd_ready) @(posedge clk);
cmd_valid = 1'b0;
rdata_ready = 1'b1;
while (!rdata_valid) @(posedge clk);
d = rdata_data;
@(posedge clk);
rdata_ready = 1'b0;
end
endtask
localparam N_TRANSACTIONS = 256;
integer i, t0, t1, write_cycles, read_cycles, errors;
reg [DATA_WIDTH-1:0] got;
real write_mb_s, read_mb_s;
initial begin
cmd_valid = 0; cmd_we = 0; cmd_addr = 0;
wdata_valid = 0; wdata_data = 0; wdata_we = 0;
rdata_ready = 0;
errors = 0; cyc = 0;
// NOTE: this is a CPU-less standalone core (cpu: None) -- init_done
// is normally driven by BIOS software over the wishbone CSR bus
// (real litedram known behavior, see enjoy-digital/litedram
// issue #106 / PR #286: "enable the user port unconditionally in
// CPU-less cases"). With no CPU attached, init_done never
// asserts on its own -- the user port is intentionally usable
// without waiting for it in this configuration. Give the
// power-on reset counters real time to settle, then proceed.
$display("=== CPU-less core: not gating on init_done (see litedram issue #106) -- settling power-on reset ===");
repeat(2000) @(posedge clk);
$display(" init_done=%b init_error=%b at cycle %0d (%0.2f us) -- proceeding regardless (informational only)",
init_done, init_error, cyc, cyc*CLK_PERIOD_NS/1000.0);
$display("=== WRITE: %0d sequential 128-bit transactions ===", N_TRANSACTIONS);
t0 = cyc;
for (i = 0; i < N_TRANSACTIONS; i = i + 1)
do_write(i, {8{16'(16'hA000 + i)}});
t1 = cyc;
write_cycles = t1 - t0;
write_mb_s = (N_TRANSACTIONS * (DATA_WIDTH/8)) / (write_cycles * CLK_PERIOD_NS / 1000.0) / 1.0e6 * 1.0e6;
// (bytes) / (seconds) -> bytes/s; convert to MB/s
write_mb_s = (N_TRANSACTIONS * (DATA_WIDTH/8) * 1.0) / (write_cycles * CLK_PERIOD_NS * 1.0e-9) / 1.0e6;
$display(" %0d cycles, %00.3f us, REAL measured write bandwidth = %0.2f MB/s",
write_cycles, write_cycles*CLK_PERIOD_NS/1000.0, write_mb_s);
$display("=== READ: %0d sequential 128-bit transactions, bit-exact check ===", N_TRANSACTIONS);
t0 = cyc;
for (i = 0; i < N_TRANSACTIONS; i = i + 1) begin
do_read(i, got);
if (got !== {8{16'(16'hA000 + i)}}) begin
$display("FAIL addr=%0d got=%h", i, got);
errors = errors + 1;
end
end
t1 = cyc;
read_cycles = t1 - t0;
read_mb_s = (N_TRANSACTIONS * (DATA_WIDTH/8) * 1.0) / (read_cycles * CLK_PERIOD_NS * 1.0e-9) / 1.0e6;
$display(" %0d cycles, %0.3f us, REAL measured read bandwidth = %0.2f MB/s",
read_cycles, read_cycles*CLK_PERIOD_NS/1000.0, read_mb_s);
$display("=== %0d/%0d bit-exact, %0d errors ===", N_TRANSACTIONS-errors, N_TRANSACTIONS, errors);
if (errors == 0) $display("ALL DATA BIT-EXACT (tb_litedram_bandwidth)");
$finish;
end
initial begin
#2000000; // 2ms real-time safety watchdog
$display("WATCHDOG TIMEOUT -- init_done never asserted or benchmark hung");
$finish;
end
endmodule