exp: bank-interleaved SDRAM pipelining works in isolation, ~0.3% gain integrated (EXP-0052)

Follow-up to EXP-0051: built sdram_controller_pipelined.v, remapping
addr->bank to low-order bits (today's weight region always maps to bank
0) and adding a shadow-slot ACTIVATE lookahead so a different-bank
request can start its tRCD wait during the current transaction's tail.

Phase A (isolated tb_sdram_controller_pipelined.v, 38/38 bit-exact,
independently re-verified this session): mechanism works, saves exactly
2 cycles (tRCD) per different-bank back-to-back pair, matching the
theoretical ceiling derived before measuring (CAS_LATENCY+BURST_LEN are
serial on the shared data bus regardless of bank, so more than tRCD/tRP
was never on the table).

Phase B (integration, tb_nms_dstress_sdram_pipelined.v, independently
rebuilt/rerun): N=4 49760 cycles (-0.33% vs baseline), N=8 49755
(-0.31%) -- both 256/256 bit-exact. Root cause of the gap: the W port's
request/ready protocol is one-at-a-time, so a second, different-bank
request is essentially never already pending while the first is still
in flight, so the mechanism rarely triggers in the real system even
though it's correct when directly stimulated. Not integrated into
production; kept as additive reference for a possible future
arbiter/backend pipelined-dispatch rewrite (out of scope here, larger
and riskier).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHENedK76onD2Vtc2CMjej
This commit is contained in:
2026-09-16 02:56:51 +02:00
co-authored by Claude Sonnet 5
parent cc5db09f61
commit 03b5cbc25b
6 changed files with 1826 additions and 0 deletions
@@ -0,0 +1,562 @@
`timescale 1ns/1ps
// ================================================================
// EXPERIMENTAL fork of tb_nms_dstress_sdram_unified.v -- instantiates
// nms_neural_multiprocessor_sdram_pipelined.v (single physical SDRAM
// chip, but with sdram_controller_pipelined.v's bank-interleaved
// command pipelining inside) instead of nms_neural_multiprocessor_
// sdram_unified.v. Identical D-Stress workload/golden-model/bit-exact
// verification.
//
// ONLY functional difference vs the original testbench: poke_byte/
// peek_byte/poke_byte_weight/peek_byte_weight no longer compute a
// flat `u_sdram.mem[word_addr]` index by hand (that shortcut relied
// on the ORIGINAL controller's bank-from-TOP-bits decomposition,
// where ROWS/COLS being powers of 2 makes the flat word address
// numerically identical to bank*ROWS*COLS+row*COLS+col). The
// pipelined controller re-slices which address bits mean bank/row/
// col (see sdram_controller_pipelined.v's own header, note (1)), so
// these backdoor helpers instead: (a) decompose the flat word address
// using the EXACT SAME bit ranges as sdram_controller_pipelined.v's
// own addr_bank/addr_row/addr_col wires, then (b) call sdram_model.v's
// own explicit, decomposition-agnostic backdoor_read/backdoor_write
// tasks (bank/row/col-addressed) instead of indexing `mem[]` directly
// -- this guarantees the testbench and the RTL agree on where a given
// byte physically lives, by construction, rather than by two
// independently-maintained flat-index formulas that could silently
// drift apart.
// ================================================================
module tb #(
parameter N_SLOTS_CFG = 2,
parameter PFD_CFG = 8
);
localparam ADDR_WIDTH = 26;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
localparam N_NODES = 1024;
localparam MAX_DEPS = 8;
localparam QUEUE_DEPTH = 8;
localparam NODE_IDW = $clog2(N_NODES);
localparam CLK_PERIOD = 12.5; // 80 MHz
// must match sdram_controller_pipelined.v's own instantiation
// parameters exactly (BURST_LEN=8 hardcoded by sdram_unified_
// backend_pipelined.v, ROW_BITS/COL_BITS/BANK_BITS defaults)
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam ALIGN_BITS = 3; // clog2(BURST_LEN=8)
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 sdram_cke, sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n;
wire [1:0] sdram_ba;
wire [12:0] sdram_a;
wire [15:0] sdram_dq;
wire [1:0] sdram_dqm;
nms_neural_multiprocessor_sdram_pipelined #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH),
.N_SLOTS(N_SLOTS_CFG), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH),
.MAX_TILES(16), .PREFETCH_DISTANCE(PFD_CFG), .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),
.sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n),
.sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n),
.sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm)
);
sdram_model #(.CLK_FREQ_MHZ(80)) u_sdram (
.clk(clk), .cke(sdram_cke), .cs_n(sdram_cs_n), .ras_n(sdram_ras_n),
.cas_n(sdram_cas_n), .we_n(sdram_we_n), .ba(sdram_ba), .a(sdram_a),
.dq(sdram_dq), .dqm(sdram_dqm)
);
// ---- backdoor helpers: decompose a flat 25-bit word address into
// (bank,row,col) using sdram_controller_pipelined.v's own bit
// ranges, then use sdram_model.v's own bank/row/col-addressed
// backdoor tasks -- see header note above ----
function automatic [BANK_BITS-1:0] wa_bank(input [24:0] wa);
wa_bank = wa[ALIGN_BITS +: BANK_BITS];
endfunction
function automatic [COL_BITS-1:0] wa_col(input [24:0] wa);
wa_col = {wa[ALIGN_BITS+BANK_BITS +: (COL_BITS-ALIGN_BITS)], wa[ALIGN_BITS-1:0]};
endfunction
function automatic [ROW_BITS-1:0] wa_row(input [24:0] wa);
wa_row = wa[24 -: ROW_BITS];
endfunction
task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input signed [7:0] val);
reg [24:0] wa;
reg [15:0] cur;
begin
wa = byte_addr[ADDR_WIDTH-1:1];
cur = u_sdram.backdoor_read(wa_bank(wa), wa_row(wa), wa_col(wa));
if (byte_addr[0] == 1'b0) cur[7:0] = val; else cur[15:8] = val;
u_sdram.backdoor_write(wa_bank(wa), wa_row(wa), wa_col(wa), cur);
end
endtask
function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr);
reg [24:0] wa;
reg [15:0] cur;
begin
wa = byte_addr[ADDR_WIDTH-1:1];
cur = u_sdram.backdoor_read(wa_bank(wa), wa_row(wa), wa_col(wa));
peek_byte = (byte_addr[0] == 1'b0) ? cur[7:0] : cur[15:8];
end
endfunction
task automatic poke_byte_weight(input [ADDR_WIDTH-1:0] byte_addr, input signed [7:0] val);
reg [24:0] wa;
reg [15:0] cur;
begin
wa = byte_addr[ADDR_WIDTH-1:1];
cur = u_sdram.backdoor_read(wa_bank(wa), wa_row(wa), wa_col(wa));
if (byte_addr[0] == 1'b0) cur[7:0] = val; else cur[15:8] = val;
u_sdram.backdoor_write(wa_bank(wa), wa_row(wa), wa_col(wa), cur);
end
endtask
function automatic signed [7:0] peek_byte_weight(input [ADDR_WIDTH-1:0] byte_addr);
reg [24:0] wa;
reg [15:0] cur;
begin
wa = byte_addr[ADDR_WIDTH-1:1];
cur = u_sdram.backdoor_read(wa_bank(wa), wa_row(wa), wa_col(wa));
peek_byte_weight = (byte_addr[0] == 1'b0) ? cur[7:0] : cur[15:8];
end
endfunction
function automatic signed [7:0] relu_sat(input integer acc);
begin
if (acc <= 0) relu_sat = 8'sd0;
else if (acc > 127) relu_sat = 8'sd127;
else relu_sat = acc[7:0];
end
endfunction
task automatic register_node(
input [NODE_IDW-1:0] nid,
input [$clog2(MAX_DEPS+1)-1:0] required,
input [MAX_DEPS*NODE_IDW-1:0] producer_ids_packed,
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 = producer_ids_packed;
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
reg measure_en;
integer total_cycles;
integer psram_busy_cycles;
integer ni;
genvar gi;
reg [N_SLOTS_CFG-1:0] slot_busy_bit;
reg [N_SLOTS_CFG-1:0] slot_tile_bit;
integer slot_busy_cycles [0:N_SLOTS_CFG-1];
integer slot_tiles_delivered [0:N_SLOTS_CFG-1];
generate
for (gi = 0; gi < N_SLOTS_CFG; gi = gi + 1) begin : GEN_SLOT_MON
always @(*) begin
slot_busy_bit[gi] = (u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.state != 3'd0);
slot_tile_bit[gi] = u_nmp.u_dataflow_core.GEN_SLOT[gi].mm_operand_valid &&
u_nmp.u_dataflow_core.GEN_SLOT[gi].mm_operand_ready;
end
end
endgenerate
integer active_count;
integer active_hist [0:4];
integer useful_mac_cycles;
integer first_tile_cyc;
integer last_tile_cyc;
integer any_tile_bit;
integer sdram_req_count, sdram_ready_count, sdram_wr_count;
integer sdram_busy_cycles, sdram_refresh_count;
integer sdram_req_start_cyc, sdram_lat_sum, sdram_lat_min, sdram_lat_max, sdram_lat_n;
reg sdram_prev_state_is_refwait;
initial begin
active_hist[0]=0; active_hist[1]=0; active_hist[2]=0; active_hist[3]=0; active_hist[4]=0;
useful_mac_cycles = 0; first_tile_cyc = -1; last_tile_cyc = -1;
sdram_req_count=0; sdram_ready_count=0; sdram_wr_count=0;
sdram_busy_cycles=0; sdram_refresh_count=0;
sdram_req_start_cyc=0; sdram_lat_sum=0; sdram_lat_min=999999; sdram_lat_max=0; sdram_lat_n=0;
sdram_prev_state_is_refwait=1'b0;
end
always @(posedge clk) begin
if (measure_en) begin
active_count = slot_busy_bit[0];
for (ni = 1; ni < N_SLOTS_CFG; ni = ni + 1) active_count = active_count + slot_busy_bit[ni];
active_hist[active_count] <= active_hist[active_count] + 1;
any_tile_bit = slot_tile_bit[0];
for (ni = 1; ni < N_SLOTS_CFG; ni = ni + 1) any_tile_bit = any_tile_bit | slot_tile_bit[ni];
for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1)
if (slot_tile_bit[ni]) useful_mac_cycles <= useful_mac_cycles + 1;
if (any_tile_bit) begin
if (first_tile_cyc < 0) first_tile_cyc <= total_cycles;
last_tile_cyc <= total_cycles;
end
if (u_nmp.u_sdram_backend.u_sdram_ctrl.req) begin
sdram_req_count <= sdram_req_count + 1;
sdram_req_start_cyc <= total_cycles;
if (u_nmp.u_sdram_backend.u_sdram_ctrl.wr) sdram_wr_count <= sdram_wr_count + 1;
end
if (u_nmp.u_sdram_backend.u_sdram_ctrl.ready) begin
sdram_ready_count <= sdram_ready_count + 1;
sdram_lat_sum <= sdram_lat_sum + (total_cycles - sdram_req_start_cyc);
sdram_lat_n <= sdram_lat_n + 1;
if ((total_cycles - sdram_req_start_cyc) < sdram_lat_min) sdram_lat_min <= (total_cycles - sdram_req_start_cyc);
if ((total_cycles - sdram_req_start_cyc) > sdram_lat_max) sdram_lat_max <= (total_cycles - sdram_req_start_cyc);
end
if (u_nmp.u_sdram_backend.u_sdram_ctrl.busy) sdram_busy_cycles <= sdram_busy_cycles + 1;
sdram_prev_state_is_refwait <= (u_nmp.u_sdram_backend.u_sdram_ctrl.state == 5'd9);
if (u_nmp.u_sdram_backend.u_sdram_ctrl.state == 5'd9 && !sdram_prev_state_is_refwait)
sdram_refresh_count <= sdram_refresh_count + 1;
end
end
task automatic report_step17_instrumentation;
real active_pct [0:4];
real util_pct, startup_cycles, drain_cycles;
real sdram_avg_lat, sdram_busy_pct, sdram_bytes_per_cycle;
integer kk, total_tiles_all;
begin
total_tiles_all = 0;
for (kk = 0; kk < N_SLOTS_CFG; kk = kk + 1) total_tiles_all = total_tiles_all + slot_tiles_delivered[kk];
$display(" ---- cycle decomposition ----");
for (kk = 0; kk <= N_SLOTS_CFG; kk = kk + 1) begin
active_pct[kk] = (total_cycles > 0) ? (100.0*active_hist[kk]/total_cycles) : 0.0;
$display(" active_slots=%0d: %0d cycles (%0.2f%%)", kk, active_hist[kk], active_pct[kk]);
end
util_pct = (total_cycles > 0) ? (100.0*useful_mac_cycles/(total_cycles*1.0*N_SLOTS_CFG)) : 0.0;
$display(" useful_mac_cycles (slot-tile-delivery events, summed)=%0d (%0.2f%% of total_cycles*N_SLOTS)", useful_mac_cycles, util_pct);
startup_cycles = (first_tile_cyc >= 0) ? (1.0*first_tile_cyc) : 0.0;
drain_cycles = (last_tile_cyc >= 0) ? (1.0*(total_cycles - last_tile_cyc)) : 0.0;
$display(" startup (cycles before first tile delivered anywhere)=%0.0f", startup_cycles);
$display(" drain (cycles after last tile delivered, until job completion)=%0.0f", drain_cycles);
$display(" ---- SDRAM (pipelined controller) effectiveness ----");
sdram_avg_lat = (sdram_lat_n > 0) ? (1.0*sdram_lat_sum/sdram_lat_n) : 0.0;
sdram_busy_pct = (total_cycles > 0) ? (100.0*sdram_busy_cycles/total_cycles) : 0.0;
sdram_bytes_per_cycle = (total_cycles > 0) ? (8.0*sdram_ready_count/total_cycles) : 0.0;
$display(" sdram_req_count=%0d sdram_ready_count=%0d sdram_wr_count=%0d",
sdram_req_count, sdram_ready_count, sdram_wr_count);
$display(" sdram_busy_cycles=%0d/%0d (%0.2f%%)", sdram_busy_cycles, total_cycles, sdram_busy_pct);
$display(" sdram_refresh_count=%0d", sdram_refresh_count);
$display(" sdram_request_latency: min=%0d max=%0d avg=%0.2f cycles",
sdram_lat_min, sdram_lat_max, sdram_avg_lat);
$display(" sdram_avg_bytes_per_cycle=%0.4f", sdram_bytes_per_cycle);
end
endtask
reg [N_SLOTS_CFG-1:0] slot_could_present_act;
reg [N_SLOTS_CFG-1:0] slot_weight_blocking;
reg [N_SLOTS_CFG-1:0] slot_stalled_this_tile;
reg [31:0] prev_tile_idx [0:N_SLOTS_CFG-1];
integer weight_stall_cycles [0:N_SLOTS_CFG-1];
integer tiles_prefetched_clean [0:N_SLOTS_CFG-1];
integer tiles_consumed_total [0:N_SLOTS_CFG-1];
wire [31:0] slot_tile_idx_w [0:N_SLOTS_CFG-1];
generate
for (gi = 0; gi < N_SLOTS_CFG; gi = gi + 1) begin : GEN_SLOT_PF_MON
assign slot_tile_idx_w[gi] = {16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx};
always @(*) begin
slot_could_present_act[gi] =
({{16{1'b0}}, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx} <
{16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.n_tiles_reg}) &&
({{16{1'b0}}, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx} <
{16'b0, u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.usable_act});
slot_weight_blocking[gi] =
slot_could_present_act[gi] &&
!(u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.tile_idx <
u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.wgt_ready_count) &&
!u_nmp.u_dataflow_core.GEN_SLOT[gi].u_mm.operand_valid;
end
end
endgenerate
always @(posedge clk) begin
if (measure_en) begin
for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1) begin
if (prev_tile_idx[ni] != slot_tile_idx_w[ni]) begin
slot_stalled_this_tile[ni] <= 1'b0;
prev_tile_idx[ni] <= slot_tile_idx_w[ni];
end else if (slot_weight_blocking[ni]) begin
slot_stalled_this_tile[ni] <= 1'b1;
weight_stall_cycles[ni] <= weight_stall_cycles[ni] + 1;
end
if (slot_tile_bit[ni]) begin
tiles_consumed_total[ni] <= tiles_consumed_total[ni] + 1;
if (!slot_stalled_this_tile[ni])
tiles_prefetched_clean[ni] <= tiles_prefetched_clean[ni] + 1;
end
end
end
end
integer jobs_allocated, jobs_completed, wakeups;
integer waiting_sum, ready_sum, dispatched_sum, sample_count;
reg sample_occupancy;
integer scan_i;
integer waiting_now, ready_now, dispatched_now;
always @(posedge clk) begin
if (measure_en) begin
total_cycles <= total_cycles + 1;
if (u_nmp.u_arbiter.owner != 0) psram_busy_cycles <= psram_busy_cycles + 1;
for (ni = 0; ni < N_SLOTS_CFG; ni = ni + 1) begin
if (slot_busy_bit[ni]) slot_busy_cycles[ni] <= slot_busy_cycles[ni] + 1;
if (slot_tile_bit[ni]) slot_tiles_delivered[ni] <= slot_tiles_delivered[ni] + 1;
end
if (u_nmp.u_dataflow_core.dm_ready_valid && u_nmp.u_dataflow_core.dm_ready_ready)
jobs_allocated <= jobs_allocated + 1;
if (u_nmp.u_dataflow_core.dir_job_out_done)
jobs_completed <= jobs_completed + 1;
if (u_nmp.u_dataflow_core.dm_producer_done_valid)
wakeups <= wakeups + 1;
if (sample_occupancy) begin
waiting_now = 0; ready_now = 0; dispatched_now = 0;
for (scan_i = 0; scan_i < N_NODES; scan_i = scan_i + 1) begin
case (u_nmp.u_dataflow_core.u_dep_mgr.node_state[scan_i])
2'd1: waiting_now = waiting_now + 1;
2'd2: ready_now = ready_now + 1;
2'd3: dispatched_now = dispatched_now + 1;
default: ;
endcase
end
waiting_sum <= waiting_sum + waiting_now;
ready_sum <= ready_sum + ready_now;
dispatched_sum <= dispatched_sum + dispatched_now;
sample_count <= sample_count + 1;
end
end
end
task automatic reset_instrumentation(input do_sample_occupancy);
integer k;
begin
active_hist[0]=0; active_hist[1]=0; active_hist[2]=0; active_hist[3]=0; active_hist[4]=0;
useful_mac_cycles = 0; first_tile_cyc = -1; last_tile_cyc = -1;
sdram_req_count=0; sdram_ready_count=0; sdram_wr_count=0;
sdram_busy_cycles=0; sdram_refresh_count=0;
sdram_req_start_cyc=0; sdram_lat_sum=0; sdram_lat_min=999999; sdram_lat_max=0; sdram_lat_n=0;
total_cycles = 0; psram_busy_cycles = 0;
jobs_allocated = 0; jobs_completed = 0; wakeups = 0;
waiting_sum = 0; ready_sum = 0; dispatched_sum = 0; sample_count = 0;
sample_occupancy = do_sample_occupancy;
for (k = 0; k < N_SLOTS_CFG; k = k + 1) begin
slot_busy_cycles[k] = 0;
slot_tiles_delivered[k] = 0;
weight_stall_cycles[k] = 0;
tiles_prefetched_clean[k] = 0;
tiles_consumed_total[k] = 0;
slot_stalled_this_tile[k] = 1'b0;
prev_tile_idx[k] = 32'hFFFFFFFF;
end
end
endtask
task automatic report_instrumentation(input [255:0] label, input integer n_neurons_completed);
integer k, total_tiles;
integer total_weight_stall_cycles, total_tiles_consumed_all, total_tiles_prefetched_clean;
real avg_waiting, avg_ready, avg_dispatched;
real psram_util, sustained_mac_per_cycle, wallclock_us;
real processor_utilization, weight_stall_pct, prefetch_effectiveness_pct;
begin
total_tiles = 0;
for (k = 0; k < N_SLOTS_CFG; k = k + 1) total_tiles = total_tiles + slot_tiles_delivered[k];
avg_waiting = (sample_count > 0) ? (1.0*waiting_sum/sample_count) : 0.0;
avg_ready = (sample_count > 0) ? (1.0*ready_sum/sample_count) : 0.0;
avg_dispatched = (sample_count > 0) ? (1.0*dispatched_sum/sample_count) : 0.0;
psram_util = (total_cycles > 0) ? (100.0*psram_busy_cycles/total_cycles) : 0.0;
sustained_mac_per_cycle = (total_cycles > 0) ? (1.0*total_tiles*P_IN/total_cycles) : 0.0;
wallclock_us = total_cycles * CLK_PERIOD / 1000.0;
$display("---- BENCHMARK REPORT: %0s ----", label);
$display(" total_cycles=%0d wallclock_us=%0.3f", total_cycles, wallclock_us);
$display(" neurons_completed=%0d tiles_delivered(real)=%0d", n_neurons_completed, total_tiles);
$display(" jobs_allocated=%0d jobs_completed=%0d dependency_wakeups=%0d", jobs_allocated, jobs_completed, wakeups);
$display(" shared AR (activation+result) arbiter-side utilization: %0.1f%% (%0d/%0d busy cycles)", psram_util, psram_busy_cycles, total_cycles);
for (k = 0; k < N_SLOTS_CFG; k = k + 1)
$display(" slot %0d: busy=%0d/%0d (%0.1f%%) tiles=%0d", k, slot_busy_cycles[k], total_cycles,
(total_cycles>0)?(100.0*slot_busy_cycles[k]/total_cycles):0.0, slot_tiles_delivered[k]);
if (sample_count > 0)
$display(" dependency_manager avg occupancy: waiting=%0.2f ready=%0.2f dispatched=%0.2f", avg_waiting, avg_ready, avg_dispatched);
$display(" DERIVED: sustained end-to-end MAC/cycle = %0.4f", sustained_mac_per_cycle);
if (n_neurons_completed > 0)
$display(" DERIVED: cycles/neuron = %0.2f", 1.0*total_cycles/n_neurons_completed);
if (total_tiles > 0)
$display(" DERIVED: cycles/tile = %0.2f", 1.0*total_cycles/total_tiles);
total_weight_stall_cycles = 0; total_tiles_consumed_all = 0; total_tiles_prefetched_clean = 0;
for (k = 0; k < N_SLOTS_CFG; k = k + 1) begin
total_weight_stall_cycles = total_weight_stall_cycles + weight_stall_cycles[k];
total_tiles_consumed_all = total_tiles_consumed_all + tiles_consumed_total[k];
total_tiles_prefetched_clean = total_tiles_prefetched_clean + tiles_prefetched_clean[k];
end
weight_stall_pct = (total_cycles > 0) ? (100.0*total_weight_stall_cycles/(total_cycles*N_SLOTS_CFG*1.0)) : 0.0;
prefetch_effectiveness_pct = (total_tiles_consumed_all > 0) ?
(100.0*total_tiles_prefetched_clean/(total_tiles_consumed_all*1.0)) : 0.0;
$display(" [STEP11] PFD=%0d weight_stall_cycles(sum,all slots)=%0d (%0.2f%%)",
PFD_CFG, total_weight_stall_cycles, weight_stall_pct);
$display(" [STEP11] DERIVED: prefetch_effectiveness = %0.2f%%", prefetch_effectiveness_pct);
end
endtask
integer errors, tests;
task automatic run_dense_layer(
input [255:0] label,
input integer n_neurons,
input integer n_tiles_count,
input [NODE_IDW-1:0] node_base,
input [ADDR_WIDTH-1:0] x_base,
input [ADDR_WIDTH-1:0] w_base,
input [ADDR_WIDTH-1:0] res_base,
input sample_occ
);
integer n, t, k, len, acc;
reg signed [7:0] xv, wv, golden, real_y;
reg [MAX_DEPS*NODE_IDW-1:0] no_deps;
integer completed, wd2;
begin
len = n_tiles_count * P_IN;
no_deps = {(MAX_DEPS*NODE_IDW){1'b0}};
for (k = 0; k < len; k = k + 1)
poke_byte(x_base + k, ((k % 8) + 1));
reset_instrumentation(sample_occ);
measure_en = 1'b1;
for (n = 0; n < n_neurons; n = n + 1) begin
acc = 0;
for (t = 0; t < n_tiles_count; t = t + 1) begin
for (k = 0; k < P_IN; k = k + 1) begin
xv = peek_byte(x_base + t*P_IN + k);
wv = (((n + t*P_IN + k) % 8) + 1);
poke_byte_weight(w_base + n*len + t*P_IN + k, wv);
acc = acc + xv*wv;
end
end
golden = relu_sat(acc);
poke_byte(res_base + n, 8'sd0);
register_node(node_base + n[NODE_IDW-1:0], 0, no_deps,
x_base, w_base + n*len, n_tiles_count[15:0], res_base + n);
if ((n % 32) == 0) begin
$display(" [%0s] registered %0d/%0d", label, n+1, n_neurons);
$fflush;
end
end
$display(" [%0s] all %0d neurons registered, waiting for completion...", label, n_neurons);
$fflush;
completed = 0; wd2 = 0;
while (completed < n_neurons && wd2 < 2000000) begin
@(posedge clk);
wd2 = wd2 + 1;
completed = jobs_completed;
if ((wd2 % 20000) == 0) begin
$display(" [%0s] watchdog %0d: completed=%0d/%0d total_cycles=%0d", label, wd2, completed, n_neurons, total_cycles);
$fflush;
end
end
repeat(5) @(posedge clk);
measure_en = 1'b0;
tests = tests + 1;
if (completed < n_neurons) begin
$display("FAIL %0s: only %0d/%0d neurons completed within watchdog", label, completed, n_neurons);
errors = errors + 1;
end else begin : check_block
integer local_errors;
local_errors = 0;
for (n = 0; n < n_neurons; n = n + 1) begin
acc = 0;
for (t = 0; t < n_tiles_count; t = t + 1)
for (k = 0; k < P_IN; k = k + 1)
acc = acc + peek_byte(x_base + t*P_IN + k) * peek_byte_weight(w_base + n*len + t*P_IN + k);
golden = relu_sat(acc);
real_y = peek_byte(res_base + n);
if (real_y !== golden) begin
$display("FAIL %0s neuron %0d: real=%0d golden=%0d", label, n, real_y, golden);
local_errors = local_errors + 1;
end
end
if (local_errors == 0)
$display("PASS %0s: all %0d neurons bit-exact vs golden", label, n_neurons);
else
errors = errors + 1;
end
report_instrumentation(label, n_neurons);
report_step17_instrumentation;
end
endtask
initial begin
errors = 0; tests = 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;
measure_en = 0;
repeat(5) @(posedge clk);
rst = 0;
$display("========================================");
$display("NMS D-Stress benchmark (EXPERIMENTAL PIPELINED SDRAM controller) -- N_SLOTS_CFG=%0d PFD_CFG=%0d", N_SLOTS_CFG, PFD_CFG);
$display("========================================");
wait (u_nmp.u_sdram_backend.u_sdram_ctrl.state == u_nmp.u_sdram_backend.u_sdram_ctrl.S_IDLE);
@(posedge clk);
run_dense_layer("D-Stress", 256, 16, 16'd400, 26'h200000, 26'h010000, 26'h300000, 1'b0);
repeat (4) @(posedge clk);
if (u_nmp.data_ready !== 1'b1) begin
$display("FAIL data_ready: expected 1 after graph completion, got %b", u_nmp.data_ready);
errors = errors + 1;
end else begin
$display("PASS data_ready: correctly asserted after graph completion");
end
$display("========================================");
if (errors == 0)
$display("ALL %0d WORKLOAD SUITES PASSED (N_SLOTS_CFG=%0d, PFD_CFG=%0d, PIPELINED SDRAM)", tests, N_SLOTS_CFG, PFD_CFG);
else
$display("FAILED: %0d/%0d workload suite(s) had errors -- see messages above", errors, tests);
$display("========================================");
$finish;
end
endmodule
@@ -0,0 +1,273 @@
`timescale 1ns/1ps
// ============================================================
// Isolated correctness + cycle-savings regression for
// sdram_controller_pipelined.v, forked from tb_sdram_controller.v's
// own idiom (same do_transaction task style, same sdram_model.v DUT
// pairing). Adds what the original testbench cannot exercise (it
// always waits for `busy` to clear before issuing the next request):
// deliberately pulsing a SECOND req WHILE the controller is still
// mid-transaction, to test the new shadow-pipeline slot.
//
// Covers:
// 1) same correctness battery as the original (write->read,
// sequential, all 4 banks, address limits, pseudo-random) --
// using the ORIGINAL wait-for-ready protocol throughout, so this
// also proves the re-sliced address decomposition (header note
// (1) in sdram_controller_pipelined.v) is a correct bijection.
// 2) DIFFERENT-bank early injection: issue a second request for a
// different bank while the first is still in S_CAS_WAIT, verify
// both results bit-exact AND that the combined cycle count is
// LOWER than 2x the serial baseline.
// 3) SAME-bank consecutive (both via the normal wait-for-ready
// protocol): must cost exactly the same as the original
// controller, no regression.
// 4) refresh spanning an early-injected interleave: run enough
// interleaved pairs to cross >=1 real tREFI interval, watch for
// any "VIOLATION"/"WARNING" from sdram_model.v.
// ============================================================
module tb #(
parameter BURST_LEN = 8,
parameter CLK_FREQ_MHZ = 80
);
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS;
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
localparam ALIGN_BITS = (BURST_LEN<=1) ? 0 : $clog2(BURST_LEN);
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
reg rst;
reg req, wr;
reg [ADDR_WIDTH-1:0] addr;
reg [16*BURST_LEN-1:0] wdata;
reg [2*BURST_LEN-1:0] wmask;
wire [16*BURST_LEN-1:0] rdata;
wire ready, busy;
wire sdram_cke, sdram_cs_n, sdram_ras_n, sdram_cas_n, sdram_we_n;
wire [BANK_BITS-1:0] sdram_ba;
wire [ROW_BITS-1:0] sdram_a;
wire [15:0] sdram_dq;
wire [1:0] sdram_dqm;
sdram_controller_pipelined #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) dut (
.clk(clk), .rst(rst),
.req(req), .wr(wr), .addr(addr), .wdata(wdata), .wmask(wmask), .rdata(rdata), .ready(ready), .busy(busy),
.sdram_cke(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n),
.sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n),
.sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm)
);
sdram_model #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) mem (
.clk(clk), .cke(sdram_cke), .cs_n(sdram_cs_n), .ras_n(sdram_ras_n),
.cas_n(sdram_cas_n), .we_n(sdram_we_n), .ba(sdram_ba), .a(sdram_a),
.dq(sdram_dq), .dqm(sdram_dqm)
);
integer errors, tests;
integer cyc;
always @(posedge clk) if (!rst) cyc <= cyc + 1;
// ---- helper: which bank a given flat word address maps to under
// the PIPELINED decomposition (must match sdram_controller_
// pipelined.v's own addr_bank wire exactly) ----
function automatic [BANK_BITS-1:0] bank_of;
input [ADDR_WIDTH-1:0] a;
begin
bank_of = a[ALIGN_BITS +: BANK_BITS];
end
endfunction
task automatic do_transaction(
input t_wr,
input [ADDR_WIDTH-1:0] t_addr,
input [16*BURST_LEN-1:0] t_wdata,
output [16*BURST_LEN-1:0] t_rdata,
output integer t_cycles
);
integer t0;
begin
@(posedge clk);
while (busy) @(posedge clk);
t0 = cyc;
req = 1'b1; wr = t_wr; addr = t_addr; wdata = t_wdata;
@(posedge clk);
req = 1'b0;
while (!ready) @(posedge clk);
t_rdata = rdata;
t_cycles = cyc - t0;
end
endtask
reg [16*BURST_LEN-1:0] got, wpat;
integer elapsed;
task automatic check_word(input [ADDR_WIDTH-1:0] a, input [15:0] pattern);
integer k;
begin
for (k = 0; k < BURST_LEN; k = k + 1)
wpat[k*16 +: 16] = pattern + k[15:0];
do_transaction(1'b1, a, wpat, got, elapsed);
do_transaction(1'b0, a, {(16*BURST_LEN){1'b0}}, got, elapsed);
tests = tests + 1;
if (got !== wpat) begin
$display("FAIL addr=%0d bank=%0d: got=%h expected=%h", a, bank_of(a), got, wpat);
errors = errors + 1;
end else begin
$display("PASS addr=%0d bank=%0d: burst=%0d bit-exact, cycles=%0d", a, bank_of(a), BURST_LEN, elapsed);
end
end
endtask
// issue a request THIS cycle without waiting for busy/ready --
// the caller is responsible for knowing this is safe (shadow slot
// free, or accepting fallback-to-req_pending semantics otherwise)
task automatic issue_req_now(input t_wr, input [ADDR_WIDTH-1:0] t_addr, input [16*BURST_LEN-1:0] t_wdata);
begin
@(posedge clk);
req = 1'b1; wr = t_wr; addr = t_addr; wdata = t_wdata;
@(posedge clk);
req = 1'b0;
end
endtask
task automatic wait_ready(output [16*BURST_LEN-1:0] t_rdata, output integer t_cyc_at_ready);
begin
// always advance at least one cycle first -- otherwise two
// back-to-back calls can both observe the SAME still-high
// `ready` pulse from the previous call's own exit cycle
// (a single-cycle-wide pulse level-checked with no
// intervening clock edge looks identical to a fresh one).
@(posedge clk);
while (!ready) @(posedge clk);
t_rdata = rdata;
t_cyc_at_ready = cyc;
end
endtask
integer seed;
integer i;
reg [ADDR_WIDTH-1:0] rnd_addr;
initial begin
errors = 0; tests = 0; cyc = 0; seed = 32'hC0FFEE;
rst = 1; req = 0; wr = 0; addr = 0; wdata = 0; wmask = 0;
repeat(5) @(posedge clk);
rst = 0;
while (busy) @(posedge clk);
$display("=== TEST 1: correctness battery (original wait-for-ready protocol) ===");
check_word({ADDR_WIDTH{1'b0}}, 16'hA5A5);
for (i = 0; i < 8; i = i + 1)
check_word(i*BURST_LEN, 16'h1000 + i);
// all 4 banks (bank now comes from LOW bits above the burst
// alignment -- addr values chosen so bank_of() sweeps 0..3)
for (i = 0; i < 4; i = i + 1)
check_word((i << ALIGN_BITS) + (100 << (ALIGN_BITS+BANK_BITS)), 16'h2000 + i);
// pseudo-random
for (i = 0; i < 24; i = i + 1) begin
rnd_addr = ($random(seed) % ((1<<ADDR_WIDTH)/BURST_LEN)) * BURST_LEN;
check_word(rnd_addr, 16'h3000 + i);
end
$display(" TEST 1: %0d/%0d passed so far", tests-errors, tests);
$display("=== TEST 2: SAME-bank consecutive, original protocol -- must match baseline 16-ish cycles/txn, no regression ===");
begin : test2
integer c_a, c_b;
reg [16*BURST_LEN-1:0] junk;
do_transaction(1'b1, (5 << ALIGN_BITS), {(16*BURST_LEN){1'b1}}, junk, c_a);
do_transaction(1'b0, (5 << ALIGN_BITS), {(16*BURST_LEN){1'b0}}, junk, c_b);
$display(" same-bank sequential write/read cycles: %0d / %0d (informational, expect ~identical to original controller's own measured cost)", c_a, c_b);
end
$display("=== TEST 3: DIFFERENT-bank early injection -- measure real cycle savings ===");
begin : test3
reg [ADDR_WIDTH-1:0] addr_bank0, addr_bank1;
reg [16*BURST_LEN-1:0] wpat0, wpat1, rd0, rd1;
integer t0, cyc_ready0, cyc_ready1, k;
addr_bank0 = (10 << ALIGN_BITS); // bank 0
addr_bank1 = (10 << ALIGN_BITS) + (1 << ALIGN_BITS); // bank 1 (adjacent word block)
if (bank_of(addr_bank0) == bank_of(addr_bank1)) begin
$display("FAIL TEST3 setup: addr_bank0/addr_bank1 landed on the SAME bank (%0d) -- test address choice is wrong", bank_of(addr_bank0));
errors = errors + 1;
end else begin
for (k = 0; k < BURST_LEN; k = k + 1) begin
wpat0[k*16 +: 16] = 16'h4000 + k[15:0];
wpat1[k*16 +: 16] = 16'h5000 + k[15:0];
end
// pre-seed both locations via the safe, sequential protocol
do_transaction(1'b1, addr_bank0, wpat0, got, elapsed);
do_transaction(1'b1, addr_bank1, wpat1, got, elapsed);
// now the REAL measurement: issue read A, wait until
// we're inside S_CAS_WAIT (shadow-capturable), inject
// read B for the OTHER bank, then measure total elapsed
// from A's issue to B's ready.
@(posedge clk);
while (busy) @(posedge clk);
t0 = cyc;
issue_req_now(1'b0, addr_bank0, {(16*BURST_LEN){1'b0}});
while (dut.state !== 13) @(posedge clk); // S_CAS_WAIT == 5'd13
if (dut.pipe_valid !== 1'b0)
$display(" (note) shadow slot already occupied when attempting injection -- unexpected for this test");
issue_req_now(1'b0, addr_bank1, {(16*BURST_LEN){1'b0}});
if (dut.pipe_valid !== 1'b1) begin
$display("FAIL TEST3: pipe_valid did not get set after different-bank injection during S_CAS_WAIT");
errors = errors + 1;
end
wait_ready(rd0, cyc_ready0);
wait_ready(rd1, cyc_ready1);
tests = tests + 1;
if (rd0 !== wpat0 || rd1 !== wpat1) begin
$display("FAIL TEST3 data: rd0=%h (exp %h) rd1=%h (exp %h)", rd0, wpat0, rd1, wpat1);
errors = errors + 1;
end else begin
$display("PASS TEST3 data: both banks bit-exact");
end
$display(" TEST3 timing: total cycles A-issue -> B-ready = %0d (serial baseline for 2 back-to-back BURST_LEN=%0d transactions is ~%0d; savings expected ~tRCD per pipelined pair, NOT a multiple-x speedup -- see sdram_controller_pipelined.v header)",
cyc_ready1 - t0, BURST_LEN, 2*(1+2+(BURST_LEN==1?0:3+1)+ (BURST_LEN>1?BURST_LEN-1:0) +2));
end
end
$display("=== TEST 4: refresh spanning interleaved traffic (watch for VIOLATION/WARNING above) ===");
begin : test4
integer t0b, cyc_r0, cyc_r1, j;
reg [ADDR_WIDTH-1:0] ba0, ba1;
reg [16*BURST_LEN-1:0] rr0, rr1;
for (j = 0; j < 60; j = j + 1) begin
ba0 = ((j*3) << ALIGN_BITS);
ba1 = ((j*3+1) << ALIGN_BITS);
if (bank_of(ba0) == bank_of(ba1)) ba1 = ba1 + (1 << ALIGN_BITS);
do_transaction(1'b1, ba0, {(16*BURST_LEN){16'hAA55}}, got, elapsed);
do_transaction(1'b1, ba1, {(16*BURST_LEN){16'h55AA}}, got, elapsed);
@(posedge clk);
while (busy) @(posedge clk);
t0b = cyc;
issue_req_now(1'b0, ba0, {(16*BURST_LEN){1'b0}});
while (dut.state !== 13 && dut.state !== 7) @(posedge clk); // S_CAS_WAIT or back to S_IDLE (refresh could have won)
if (dut.state === 13 && !dut.pipe_valid)
issue_req_now(1'b0, ba1, {(16*BURST_LEN){1'b0}});
wait_ready(rr0, cyc_r0);
if (dut.pipe_valid || dut.state != 7)
wait_ready(rr1, cyc_r1);
end
$display(" TEST4: 60 interleaved read pairs completed (spans real tREFI at CLK_FREQ_MHZ=%0d) -- check log above for VIOLATION/WARNING", CLK_FREQ_MHZ);
end
$display("=== %0d/%0d tests, %0d errors (BURST_LEN=%0d, CLK_FREQ_MHZ=%0d) ===",
tests-errors, tests, errors, BURST_LEN, CLK_FREQ_MHZ);
if (errors == 0) $display("ALL TESTS PASSED (tb_sdram_controller_pipelined, BURST_LEN=%0d, CLK_FREQ_MHZ=%0d)", BURST_LEN, CLK_FREQ_MHZ);
$finish;
end
endmodule