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
259 lines
11 KiB
Verilog
259 lines
11 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// NMS STEP11 -- correctness testbench for weight_prefetch_engine.v.
|
|
// Real memory model (sim_word_mem, 2-cycle latency, same convention
|
|
// as tb_nms_dataflow_core.v/STEP8), a real weight SRAM
|
|
// (nms_weight_packed.v, N_SLOTS=1 for this isolated test), bit-exact
|
|
// data checking against a known fill pattern.
|
|
//
|
|
// Covers: n_tiles in {0,1,2,PFD,PFD+1,MAX_TILES-1,MAX_TILES}, PFD in
|
|
// {1,2,4,8}, back-to-back jobs (reset-between-jobs via job_active
|
|
// falling/rising, no explicit reset pulse), and a real memory-latency
|
|
// variant (sim_word_mem with an injected extra wait) to confirm the
|
|
// engine never double-fetches or overwrites a not-yet-committed tile.
|
|
// ============================================================
|
|
module sim_word_mem #(
|
|
parameter ADDR_WIDTH = 23,
|
|
parameter DEPTH = 4096,
|
|
parameter EXTRA_WAIT = 0
|
|
)(
|
|
input wire clk, rst,
|
|
input wire req, wr,
|
|
input wire [ADDR_WIDTH-1:0] addr,
|
|
input wire [15:0] wdata,
|
|
input wire lb_n, ub_n,
|
|
output reg [15:0] rdata,
|
|
output reg ready
|
|
);
|
|
reg [15:0] mem [0:DEPTH-1];
|
|
reg [3:0] state;
|
|
reg [ADDR_WIDTH-1:0] addr_reg;
|
|
reg [7:0] wait_cnt;
|
|
localparam ST_IDLE=0, ST_WAIT=1, ST_EXTRA=2;
|
|
always @(posedge clk) begin
|
|
if (rst) begin state<=ST_IDLE; ready<=0; rdata<=0; end
|
|
else begin
|
|
ready <= 0;
|
|
case (state)
|
|
ST_IDLE: if (req) begin
|
|
addr_reg <= addr;
|
|
if (wr) begin
|
|
if (!lb_n) mem[addr][7:0] <= wdata[7:0];
|
|
if (!ub_n) mem[addr][15:8] <= wdata[15:8];
|
|
end
|
|
wait_cnt <= EXTRA_WAIT[7:0];
|
|
state <= ST_WAIT;
|
|
end
|
|
ST_WAIT: begin
|
|
if (wait_cnt != 0) begin
|
|
wait_cnt <= wait_cnt - 1'b1;
|
|
end else begin
|
|
rdata <= mem[addr_reg];
|
|
ready <= 1;
|
|
state <= ST_IDLE;
|
|
end
|
|
end
|
|
default: state <= ST_IDLE;
|
|
endcase
|
|
end
|
|
end
|
|
endmodule
|
|
|
|
module tb;
|
|
parameter ADDR_WIDTH = 23;
|
|
parameter DATA_WIDTH = 8;
|
|
parameter P_IN = 8;
|
|
parameter MAX_TILES = 16;
|
|
parameter PFD = 4;
|
|
parameter EXTRA_WAIT = 0;
|
|
localparam TIW = $clog2(MAX_TILES);
|
|
localparam CNTW = $clog2(MAX_TILES+1);
|
|
|
|
reg clk = 0;
|
|
always #5 clk = ~clk;
|
|
reg rst;
|
|
|
|
reg job_active;
|
|
reg [ADDR_WIDTH-1:0] w_base;
|
|
reg [15:0] n_tiles;
|
|
reg [CNTW-1:0] consumed_count;
|
|
|
|
wire wgt_fill_we;
|
|
wire [TIW-1:0] wgt_fill_addr;
|
|
wire [DATA_WIDTH*P_IN-1:0] wgt_fill_data;
|
|
wire [CNTW-1:0] ready_count;
|
|
|
|
wire mem_req, mem_wr;
|
|
wire [ADDR_WIDTH-1:0] mem_addr;
|
|
wire [15:0] mem_wdata, mem_rdata;
|
|
wire mem_lb_n, mem_ub_n, mem_ready;
|
|
|
|
weight_prefetch_engine #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH),
|
|
.MAX_TILES(MAX_TILES), .PREFETCH_DISTANCE(PFD)
|
|
) dut (
|
|
.clk(clk), .rst(rst),
|
|
.job_active(job_active), .w_base(w_base), .n_tiles(n_tiles),
|
|
.consumed_count(consumed_count),
|
|
.wgt_fill_we(wgt_fill_we), .wgt_fill_addr(wgt_fill_addr), .wgt_fill_data(wgt_fill_data),
|
|
.ready_count(ready_count),
|
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr), .mem_wdata(mem_wdata),
|
|
.mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n), .mem_rdata(mem_rdata), .mem_ready(mem_ready)
|
|
);
|
|
|
|
sim_word_mem #(.ADDR_WIDTH(ADDR_WIDTH), .DEPTH(4096), .EXTRA_WAIT(EXTRA_WAIT)) u_mem (
|
|
.clk(clk), .rst(rst), .req(mem_req), .wr(mem_wr), .addr(mem_addr), .wdata(mem_wdata),
|
|
.lb_n(mem_lb_n), .ub_n(mem_ub_n), .rdata(mem_rdata), .ready(mem_ready)
|
|
);
|
|
|
|
// real weight SRAM (N_SLOTS=1) -- same module as production
|
|
reg wgt_rd_en;
|
|
reg [TIW-1:0] wgt_rd_addr;
|
|
wire signed [DATA_WIDTH*P_IN-1:0] wgt_rd_data;
|
|
nms_weight_packed #(.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .N_SLOTS(1), .MAX_TILES(MAX_TILES)) u_sram (
|
|
.clk(clk), .rst(rst),
|
|
.fill_we(wgt_fill_we), .fill_addr_flat(wgt_fill_addr), .fill_data_flat(wgt_fill_data),
|
|
.rd_en(wgt_rd_en), .rd_addr_flat(wgt_rd_addr), .rd_data_flat(wgt_rd_data)
|
|
);
|
|
|
|
task automatic poke_word(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] val);
|
|
reg [ADDR_WIDTH-2:0] wa;
|
|
begin
|
|
wa = byte_addr[ADDR_WIDTH-1:1];
|
|
if (byte_addr[0]==1'b0) u_mem.mem[wa][7:0] = val; else u_mem.mem[wa][15:8] = val;
|
|
end
|
|
endtask
|
|
|
|
integer errors, tests;
|
|
|
|
// fill PSRAM with a distinct pattern per tile: tile t, lane k -> (t*8+k) mod 251 (avoid trivial repeats)
|
|
task automatic fill_pattern(input [ADDR_WIDTH-1:0] base, input integer count);
|
|
integer t, k;
|
|
begin
|
|
for (t = 0; t < count; t = t + 1)
|
|
for (k = 0; k < P_IN; k = k + 1)
|
|
poke_word(base + t*P_IN + k, (t*8+k) % 251);
|
|
end
|
|
endtask
|
|
|
|
// simulated consumer: follows ready_count with a small lag (models
|
|
// a real nms_memory_manager.v consuming tiles about as fast as
|
|
// they arrive) -- driven every cycle while a job is active, unless
|
|
// freeze_consumer is set (dedicated windowing-cap test below).
|
|
reg freeze_consumer;
|
|
always @(posedge clk) begin
|
|
if (rst || !job_active) consumed_count <= {CNTW{1'b0}};
|
|
else if (!freeze_consumer && consumed_count < ready_count) consumed_count <= consumed_count + 1'b1;
|
|
end
|
|
|
|
task automatic run_job(input [ADDR_WIDTH-1:0] base, input integer count, input integer watchdog);
|
|
integer wd, t, k;
|
|
reg [7:0] expected;
|
|
begin
|
|
w_base = base; n_tiles = count[15:0];
|
|
job_active = 1'b1;
|
|
wd = 0;
|
|
while (ready_count < count[CNTW-1:0] && wd < watchdog) begin @(posedge clk); wd = wd + 1; end
|
|
@(posedge clk); #1;
|
|
tests = tests + 1;
|
|
if (ready_count !== count[CNTW-1:0]) begin
|
|
$display("FAIL n_tiles=%0d PFD=%0d: ready_count=%0d expected=%0d (watchdog=%0d)", count, PFD, ready_count, count, wd);
|
|
errors = errors + 1;
|
|
end else begin
|
|
// verify every tile's data bit-exact
|
|
for (t = 0; t < count; t = t + 1) begin
|
|
wgt_rd_addr = t[TIW-1:0]; wgt_rd_en = 1'b1;
|
|
@(posedge clk); @(posedge clk); #1; // 2-cycle SRAM read latency
|
|
for (k = 0; k < P_IN; k = k + 1) begin
|
|
expected = (t*8+k) % 251;
|
|
if (wgt_rd_data[k*DATA_WIDTH +: DATA_WIDTH] !== expected) begin
|
|
$display("FAIL n_tiles=%0d PFD=%0d tile=%0d lane=%0d: got=%0d expected=%0d",
|
|
count, PFD, t, k, wgt_rd_data[k*DATA_WIDTH +: DATA_WIDTH], expected);
|
|
errors = errors + 1;
|
|
end
|
|
end
|
|
end
|
|
$display("PASS n_tiles=%0d PFD=%0d EXTRA_WAIT=%0d: ready_count=%0d, all tiles bit-exact (cycles=%0d)",
|
|
count, PFD, EXTRA_WAIT, ready_count, wd);
|
|
end
|
|
job_active = 1'b0;
|
|
repeat(3) @(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
initial begin
|
|
errors = 0; tests = 0;
|
|
rst = 1; job_active = 0; w_base = 0; n_tiles = 0; consumed_count = 0; wgt_rd_en = 0; wgt_rd_addr = 0; freeze_consumer = 0;
|
|
repeat(4) @(posedge clk);
|
|
rst = 0;
|
|
@(posedge clk);
|
|
|
|
fill_pattern(23'h1000, MAX_TILES);
|
|
|
|
// edge cases explicitly requested: 0,1,2,PFD,PFD+1,MAX_TILES-1,MAX_TILES
|
|
// (PFD/PFD+1 only make sense as a job size when they fit inside
|
|
// MAX_TILES -- the weight SRAM itself only holds MAX_TILES
|
|
// tiles, so PFD>=MAX_TILES's own "PFD,PFD+1" cases are simply
|
|
// n/a, not a real scenario to test; MAX_TILES-1/MAX_TILES below
|
|
// already cover the near-/at-capacity edge regardless of PFD)
|
|
run_job(23'h1000, 0, 200);
|
|
run_job(23'h1000, 1, 200);
|
|
run_job(23'h1000, 2, 200);
|
|
if (PFD < MAX_TILES) begin
|
|
run_job(23'h1000, PFD, 500);
|
|
run_job(23'h1000, PFD+1, 500);
|
|
end
|
|
run_job(23'h1000, MAX_TILES-1, 2000);
|
|
run_job(23'h1000, MAX_TILES, 2000);
|
|
|
|
// back-to-back jobs, no explicit reset between them (job_active
|
|
// falling then rising, exercising the "!job_active" reset path)
|
|
run_job(23'h1000, 3, 500);
|
|
run_job(23'h1000, 5, 500);
|
|
|
|
// ERR-0015 regression: PREFETCH_DISTANCE >= 2**CNTW (here 32,
|
|
// CNTW=5 bits for MAX_TILES=16) must NOT silently truncate to 0
|
|
// and deadlock (window_limit==consumed_count forever). Only
|
|
// meaningful when PFD is actually >= 32; skipped otherwise so
|
|
// this file stays valid across all PFD values it's compiled
|
|
// with (per the sweep in EXP-0023/EXP-0024).
|
|
if (PFD >= 32) begin : err0015_large_pfd_test
|
|
run_job(23'h1000, MAX_TILES, 2000);
|
|
end
|
|
|
|
// dedicated windowing-cap test: consumer NEVER consumes
|
|
// (freeze_consumer=1, consumed_count stuck at 0) -- ready_count
|
|
// must stop advancing at exactly min(PFD,MAX_TILES) tiles (the
|
|
// n_tiles/MAX_TILES ceiling binds first whenever PFD>=MAX_TILES,
|
|
// e.g. the ERR-0015 large-PFD case above), never fetching
|
|
// further ahead than whichever bound applies, and must NEVER
|
|
// re-fetch/overwrite once frozen (checked by waiting well past
|
|
// when an unbounded engine would have finished all MAX_TILES,
|
|
// then confirming ready_count is still exactly at that bound).
|
|
begin : window_cap_test
|
|
integer wd2;
|
|
integer expected_cap;
|
|
tests = tests + 1;
|
|
expected_cap = (PFD < MAX_TILES) ? PFD : MAX_TILES;
|
|
freeze_consumer = 1'b1;
|
|
w_base = 23'h1000; n_tiles = MAX_TILES[15:0];
|
|
job_active = 1'b1;
|
|
for (wd2 = 0; wd2 < 500; wd2 = wd2 + 1) @(posedge clk);
|
|
if (ready_count !== expected_cap[CNTW-1:0]) begin
|
|
$display("FAIL windowing cap: ready_count=%0d expected exactly %0d (PFD=%0d, MAX_TILES=%0d, consumer frozen at 0)", ready_count, expected_cap, PFD, MAX_TILES);
|
|
errors = errors + 1;
|
|
end else
|
|
$display("PASS windowing cap: ready_count correctly capped at %0d (PFD=%0d, MAX_TILES=%0d) with consumer frozen", expected_cap, PFD, MAX_TILES);
|
|
job_active = 1'b0;
|
|
freeze_consumer = 1'b0;
|
|
repeat(3) @(posedge clk);
|
|
end
|
|
|
|
$display("=== %0d/%0d tests, %0d errors (PFD=%0d, MAX_TILES=%0d, EXTRA_WAIT=%0d) ===",
|
|
tests-errors, tests, errors, PFD, MAX_TILES, EXTRA_WAIT);
|
|
if (errors == 0) $display("ALL TESTS PASSED (tb_weight_prefetch, PFD=%0d, EXTRA_WAIT=%0d)", PFD, EXTRA_WAIT);
|
|
$finish;
|
|
end
|
|
endmodule
|