FASE #1 hardware freeze for FPGA-Neural V2, N4/P8, single external SDRAM (Alliance Memory AS4C4M16SA-6TIN) serving weights, activations, and results through one physical sdram_controller.v instance. Removes the PSRAM dependency (hardware/v1/rtl/psram_controller.v + memory_interface.v) from the V2 physical path entirely -- V1 itself remains fully unmodified, the golden reference. New RTL: sdram_unified_backend.v (2-way W/AR arbitration over one SDRAM controller, real per-byte DQM write masking added to sdram_controller.v for correct single-byte result writes with no read-modify-write), nms_neural_multiprocessor_sdram_unified.v (the frozen top-level). Two real bugs found and fixed via full-system testing before being accepted (ERR-0023): a deadlock and an off-by-one data-shift bug in the new arbitration logic. Real results: N=4 and N=2 D-Stress bit-exact (256/256 neurons), 40 real AUTO REFRESH events interleaved with zero corruption, real Yosys+nextpnr-ecp5 synthesis/P&R for LFE5U-45F-8CABGA381 (149/245 TRELLIS_IO, a real 45-pin reduction from the prior dual-memory design). Timing is MARGINAL (1/8 P&R seeds >=80MHz), reported honestly rather than masked by the best seed. Real, sourced ball-level pinout for the SDRAM bus + clk/rst (39/149 signals, P&R-verified) using the official Lattice ECP5U-45 pinout CSV found on disk during this step's own pre-commit review -- corrects an earlier draft that wrongly assumed no real pinout data was available. Chip readiness: NO. Real, disclosed blockers remain (no physical host interface exists yet -- the RTL's own reg_* ports are a 110-pin raw test-harness bus; clock source/PLL decision; power/configuration component selection) -- see hardware/v2/docs/{HARDWARE_FREEZE, CHIP_READINESS,OPEN_ITEMS}.md for the complete, itemized status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
236 lines
10 KiB
Verilog
236 lines
10 KiB
Verilog
// ============================================================
|
|
// Neural Memory System (NMS) -- STEP 1: bandwidth requirement study.
|
|
//
|
|
// Purpose: measure, using the REAL, unmodified, bit-exact
|
|
// hardware/v2/rtl/neural_processor.v compute pipeline (never
|
|
// reimplemented -- only the memory side is idealized), how processor
|
|
// utilization / sustained MAC-per-cycle depends on:
|
|
// - backing-store LATENCY (cycles) : 0,1,2,4,8,16
|
|
// - backing-store BANDWIDTH (bytes/cycle) : 1,2,4,8,16,32,64
|
|
// - PREFETCH_DEPTH (max outstanding tiles) : 2,4,8
|
|
// for N_SLOTS in {1,2,4,8} (this file compiled once per N_SLOTS value,
|
|
// overriding the N_SLOTS parameter at compile time; all
|
|
// (latency,bandwidth,prefetch_depth) combinations are swept INSIDE one
|
|
// compiled binary using runtime registers on
|
|
// hardware/v2/nms/rtl/ideal_memory_model.v, avoiding a separate
|
|
// recompile per point).
|
|
//
|
|
// The memory side (hardware/v2/nms/rtl/ideal_memory_model.v) is a
|
|
// SIMULATION-ONLY architectural stand-in, never synthesized -- see its
|
|
// own header. Tile CONTENT is irrelevant here (this is a cycle-timing
|
|
// / bandwidth-sizing study, not a correctness test); only the
|
|
// job/operand/result HANDSHAKE timing is real.
|
|
//
|
|
// Every reported number in this file's own output is RTL SIMULATION
|
|
// (Verilator), not a real hardware measurement -- see
|
|
// hardware/v2/logs/experiments.log EXP-0017 for the full
|
|
// classification and hardware/v2/nms/reports/ for raw CSV output.
|
|
// ============================================================
|
|
`timescale 1ns/1ps
|
|
|
|
module tb_bandwidth_study;
|
|
|
|
parameter N_SLOTS = 2;
|
|
parameter P_IN = 8;
|
|
parameter DATA_WIDTH = 8;
|
|
parameter ACC_WIDTH = 32;
|
|
parameter TILE_BYTES = 2 * P_IN; // activation half + weight half per tile
|
|
parameter NTILES = 2048; // tiles per synthetic job (steady-state dominated)
|
|
|
|
localparam TAGW = (N_SLOTS <= 1) ? 1 : $clog2(N_SLOTS);
|
|
|
|
reg clk = 0;
|
|
always #5 clk = ~clk;
|
|
|
|
reg rst;
|
|
|
|
// ---- shared ideal memory model, runtime-configurable ----
|
|
reg [15:0] cfg_latency;
|
|
reg [15:0] cfg_bw_bytes;
|
|
wire [N_SLOTS-1:0] mem_req_valid;
|
|
wire [N_SLOTS-1:0] mem_req_ready;
|
|
wire [N_SLOTS-1:0] mem_resp_valid;
|
|
|
|
ideal_memory_model #(
|
|
.NREQ(N_SLOTS), .TILE_BYTES(TILE_BYTES), .QDEPTH(128)
|
|
) u_mem (
|
|
.clk(clk), .rst(rst),
|
|
.cfg_latency(cfg_latency), .cfg_bw_bytes(cfg_bw_bytes),
|
|
.req_valid(mem_req_valid), .req_ready(mem_req_ready),
|
|
.resp_valid(mem_resp_valid)
|
|
);
|
|
|
|
reg [31:0] prefetch_depth;
|
|
|
|
// ---- per-slot compute + feeder ----
|
|
wire [N_SLOTS-1:0] job_valid_s, job_ready_s;
|
|
wire [N_SLOTS-1:0] operand_valid_s, operand_ready_s;
|
|
wire [N_SLOTS-1:0] tile_last_s;
|
|
wire [N_SLOTS-1:0] result_valid_s;
|
|
wire [3:0] np_state_s [0:N_SLOTS-1];
|
|
|
|
reg [31:0] issued_count [0:N_SLOTS-1];
|
|
reg [31:0] consumed_count [0:N_SLOTS-1];
|
|
reg [31:0] buffered_count [0:N_SLOTS-1];
|
|
reg job_started [0:N_SLOTS-1];
|
|
reg done_flag [0:N_SLOTS-1];
|
|
reg [31:0] done_cycle [0:N_SLOTS-1];
|
|
reg [31:0] stall_count [0:N_SLOTS-1];
|
|
reg [31:0] combo_cycle;
|
|
|
|
genvar s;
|
|
generate
|
|
for (s = 0; s < N_SLOTS; s = s + 1) begin : GEN_SLOT
|
|
wire want_fetch = (issued_count[s] < NTILES) &&
|
|
((issued_count[s] - consumed_count[s]) < prefetch_depth);
|
|
wire tile_consumed = operand_valid_s[s] && operand_ready_s[s];
|
|
|
|
// req_valid driven COMBINATIONALLY from want_fetch (not a
|
|
// latched pending-flag that only re-evaluates want_fetch once
|
|
// per grant round trip) -- ideal_memory_model's own admission
|
|
// is unconstrained (req_ready mirrors req_valid every cycle),
|
|
// so a registered fetch_pending flag would only ever issue one
|
|
// request every ~2 cycles regardless of PREFETCH_DEPTH/
|
|
// bandwidth, an artificial testbench-side throughput cap
|
|
// unrelated to the memory model being studied (found via
|
|
// EXP-0017: utilization plateaued at ~50% even at BW=64,
|
|
// latency=0, PREFETCH_DEPTH=8 -- a suspiciously round,
|
|
// PFD-and-BW-independent ceiling, traced to this feeder
|
|
// issuing at most one admitted request per two cycles).
|
|
assign mem_req_valid[s] = want_fetch;
|
|
assign operand_valid_s[s] = (buffered_count[s] > 0);
|
|
assign tile_last_s[s] = (consumed_count[s] == (NTILES - 1));
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
issued_count[s] <= 32'd0;
|
|
end else begin
|
|
if (mem_req_valid[s] && mem_req_ready[s])
|
|
issued_count[s] <= issued_count[s] + 32'd1;
|
|
end
|
|
end
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
buffered_count[s] <= 32'd0;
|
|
consumed_count[s] <= 32'd0;
|
|
stall_count[s] <= 32'd0;
|
|
end else begin
|
|
case ({mem_resp_valid[s], tile_consumed})
|
|
2'b10: buffered_count[s] <= buffered_count[s] + 32'd1;
|
|
2'b01: buffered_count[s] <= buffered_count[s] - 32'd1;
|
|
default: buffered_count[s] <= buffered_count[s];
|
|
endcase
|
|
if (tile_consumed)
|
|
consumed_count[s] <= consumed_count[s] + 32'd1;
|
|
if ((np_state_s[s] == 4'd2) && !operand_valid_s[s]) // NP_WAIT_OPERANDS
|
|
stall_count[s] <= stall_count[s] + 32'd1;
|
|
end
|
|
end
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
job_started[s] <= 1'b0;
|
|
done_flag[s] <= 1'b0;
|
|
done_cycle[s] <= 32'd0;
|
|
end else begin
|
|
if (!job_started[s] && job_ready_s[s])
|
|
job_started[s] <= 1'b1;
|
|
if (result_valid_s[s] && !done_flag[s]) begin
|
|
done_flag[s] <= 1'b1;
|
|
done_cycle[s] <= combo_cycle;
|
|
end
|
|
end
|
|
end
|
|
|
|
neural_processor #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)
|
|
) u_np (
|
|
.clk(clk), .rst(rst),
|
|
.job_valid(!job_started[s]), .job_ready(job_ready_s[s]),
|
|
.job_node_id(s[15:0]), .job_bias({DATA_WIDTH{1'b0}}), .job_activation(2'd1),
|
|
.operand_valid(operand_valid_s[s]), .operand_ready(operand_ready_s[s]),
|
|
.input_data({(DATA_WIDTH*P_IN){1'b0}}), .weight_data({(DATA_WIDTH*P_IN){1'b0}}),
|
|
.tile_last(tile_last_s[s]),
|
|
.result_valid(result_valid_s[s]), .result_ready(1'b1),
|
|
.result_data(), .result_node_id(),
|
|
.np_state(np_state_s[s]), .np_error()
|
|
);
|
|
end
|
|
endgenerate
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) combo_cycle <= 32'd0;
|
|
else combo_cycle <= combo_cycle + 32'd1;
|
|
end
|
|
|
|
// ---- sweep driver ----
|
|
integer li, bi, pi, s2;
|
|
integer lat_list [0:5];
|
|
integer bw_list [0:7];
|
|
integer pfd_list [0:3];
|
|
integer fh;
|
|
integer all_done;
|
|
integer max_cycle;
|
|
integer min_util_ppm, max_util_ppm, sum_util_ppm;
|
|
real util_r, mac_per_cycle_sys;
|
|
|
|
initial begin
|
|
lat_list[0]=0; lat_list[1]=1; lat_list[2]=2; lat_list[3]=4; lat_list[4]=8; lat_list[5]=16;
|
|
bw_list[0]=1; bw_list[1]=2; bw_list[2]=4; bw_list[3]=8; bw_list[4]=16; bw_list[5]=32; bw_list[6]=64; bw_list[7]=128;
|
|
pfd_list[0]=2; pfd_list[1]=4; pfd_list[2]=8; pfd_list[3]=16;
|
|
|
|
fh = $fopen("/tmp/nms_bandwidth_study.csv", "a");
|
|
|
|
for (pi = 0; pi < 4; pi = pi + 1) begin
|
|
for (li = 0; li < 6; li = li + 1) begin
|
|
for (bi = 0; bi < 8; bi = bi + 1) begin
|
|
// ---- configure and reset for this combo ----
|
|
rst = 1'b1;
|
|
cfg_latency = lat_list[li];
|
|
cfg_bw_bytes = bw_list[bi];
|
|
prefetch_depth = pfd_list[pi];
|
|
repeat (3) @(posedge clk);
|
|
rst = 1'b0;
|
|
|
|
// ---- run until every slot has finished NTILES tiles ----
|
|
all_done = 0;
|
|
while (all_done == 0) begin
|
|
@(posedge clk);
|
|
all_done = 1;
|
|
for (s2 = 0; s2 < N_SLOTS; s2 = s2 + 1)
|
|
if (!done_flag[s2]) all_done = 0;
|
|
end
|
|
|
|
// ---- gather metrics ----
|
|
max_cycle = 0;
|
|
min_util_ppm = 1000000;
|
|
max_util_ppm = 0;
|
|
sum_util_ppm = 0;
|
|
for (s2 = 0; s2 < N_SLOTS; s2 = s2 + 1) begin
|
|
if (done_cycle[s2] > max_cycle) max_cycle = done_cycle[s2];
|
|
end
|
|
for (s2 = 0; s2 < N_SLOTS; s2 = s2 + 1) begin
|
|
util_r = (done_cycle[s2] > 0) ? (1000000.0 * NTILES / done_cycle[s2]) : 0.0;
|
|
if (util_r > 1000000.0) util_r = 1000000.0;
|
|
if (util_r < min_util_ppm) min_util_ppm = util_r;
|
|
if (util_r > max_util_ppm) max_util_ppm = util_r;
|
|
sum_util_ppm = sum_util_ppm + util_r;
|
|
end
|
|
mac_per_cycle_sys = (1.0 * N_SLOTS * P_IN * NTILES) / max_cycle;
|
|
|
|
$fdisplay(fh, "%0d,%0d,%0d,%0d,%0d,%0d,%0.4f,%0.4f,%0.4f,%0.4f",
|
|
N_SLOTS, prefetch_depth, cfg_latency, cfg_bw_bytes, max_cycle, NTILES,
|
|
min_util_ppm/1000000.0, max_util_ppm/1000000.0,
|
|
(sum_util_ppm/N_SLOTS)/1000000.0, mac_per_cycle_sys);
|
|
end
|
|
end
|
|
end
|
|
|
|
$fclose(fh);
|
|
$display("BANDWIDTH_STUDY_DONE N_SLOTS=%0d", N_SLOTS);
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|