Files
FPGA-Neural/hardware/v2/nms/rtl/weight_prefetch_engine_wide.v
T
micheleandClaude Sonnet 5 8d83d97bde feat: SDRAM 8MB->64MB upgrade (AS4C32M16SB-7BIN) + N_SLOTS=8 support
Memory upgrade, at the user's own explicit request: Alliance Memory
AS4C4M16SA-6TIN (64Mbit/8MB) -> AS4C32M16SB-7BIN (512Mbit/64MB, 54-ball
TFBGA), the largest same-family SDR SDRAM Alliance Memory offers.
Real-datasheet-driven (whole AS4C4M16SA/AS4C8M16SA/AS4C16M16SA/
AS4C32M16SA family investigated): 13 row bits (was 12, one new FPGA
pin sdram_a[12]/ball F1), 10 column bits (was 8), real -7-grade AC
timing (tRCD/tRP improved to 15ns, tREFI halved to 7.8us for the
doubled row count). sdram_controller.v and sdram_model.v gained real
ROW_BITS/COL_BITS/BANK_BITS parameters (was hardcoded 12/8/2).

ADDR_WIDTH widened 23->26 bits across the live instantiation tree.
This required a real SPI protocol change (spi_host_bridge.v): a 26-bit
byte address no longer fits in 3 bytes -- every address field widened
3->4 bytes (WRITE_JOB 15->18 payload bytes, WRITE_MEM/READ_MEM header
5->6 bytes).

Found and fixed two real timing regressions via nextpnr-ecp5 P&R
(not assumed): neural_director.v's own runtime-indexed demux write
(ERR-0027, was silently synthesizing an extra MULT18X18D) and
nms_activation_fill_ctrl_v3.v's own linear N_SLOTS-wide max-scan
(ERR-0028, became dominant at N_SLOTS=8) -- both replaced with
constant-indexed/tree-based equivalents, bit-exact same behavior,
confirmed via full D-Stress N=2/4/8 regression (identical cycle
counts). N_SLOTS=4 now fully closes timing at 64MHz (8/8 seeds);
N_SLOTS=8 significantly improved but not yet fully reliable (5/8
seeds) -- honestly disclosed, not claimed complete.

Full regression re-verified: sdram_controller (461/461, 18 configs),
tb_sdram_boundary (21/21), D-Stress N=2/4/8 (bit-exact), spi_host_bridge
(18/18), board-level SPI smoke test (11/11), unified backend (40/40).

See hardware/v2/docs/MEMORY_UPGRADE_64MB_N8.md for the full
investigation, and errors.log/decisions.log (ERR-0027, ERR-0028,
DEC-0039) for the complete root-cause writeups.

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

163 lines
7.7 KiB
Verilog

`timescale 1ns/1ps
// ============================================================
// Neural Memory System (NMS) -- STEP14 Part A: parameterized-width
// experimental weight prefetch engine.
//
// SIMULATION-ONLY / EXPLORATORY (same status as ideal_memory_model.v,
// STEP11's own EXP-0017/23): establishes the ARCHITECTURAL requirement
// (what logical weight-path width removes the fetch-rate bottleneck
// EXP-0026/0027 identified) BEFORE committing to any specific real
// hardware implementation. Generalizes weight_prefetch_engine.v's own
// continuous cross-tile-boundary streaming design (STEP11, unchanged
// in spirit) to an arbitrary MEM_DATA_WIDTH instead of the real V1
// PSRAM's fixed 16 bits. Byte-lane enables (mem_lb_n/mem_ub_n) are
// dropped at this level of abstraction -- not meaningful for a
// logical bus wider than 16 bits; the real 16-bit interface (with
// lane enables) is reintroduced separately by the A5 packing adapter
// (weight_fetch_pack_adapter.v) that connects this engine's logical
// wide requests to the REAL, unmodified V1 PSRAM chain.
//
// WORDS_PER_TILE generalizes to
// ceil(DATA_WIDTH*P_IN / MEM_DATA_WIDTH), clamped to a minimum of 1
// (a bus wider than one full tile still costs exactly 1 transaction,
// with the surplus bits simply unused -- this experiment does not
// attempt multi-tile-per-transaction bursting).
// ============================================================
module weight_prefetch_engine_wide #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ADDR_WIDTH = 26,
parameter MAX_TILES = 16,
parameter PREFETCH_DISTANCE = 8,
parameter MEM_DATA_WIDTH = 64, // 16, 32, 64, 128 -- the STEP14 Part A sweep parameter
parameter TIW = (MAX_TILES <= 1) ? 1 : $clog2(MAX_TILES),
parameter CNTW = $clog2(MAX_TILES+1),
// ceil(TILE_BITS / MEM_DATA_WIDTH), minimum 1
parameter TILE_BITS = DATA_WIDTH*P_IN,
parameter WORDS_PER_TILE = (TILE_BITS + MEM_DATA_WIDTH - 1) / MEM_DATA_WIDTH,
parameter WIW = $clog2(WORDS_PER_TILE+1)
)(
input wire clk,
input wire rst,
input wire job_active,
input wire [ADDR_WIDTH-1:0] w_base, // byte address
input wire [15:0] n_tiles,
input wire [CNTW-1:0] consumed_count,
output reg wgt_fill_we,
output reg [TIW-1:0] wgt_fill_addr,
output reg [DATA_WIDTH*P_IN-1:0] wgt_fill_data,
output reg [CNTW-1:0] ready_count,
// ---- logical wide memory port (ideal_memory_model_wide.v) ----
output reg mem_req,
output reg [ADDR_WIDTH-1:0] mem_addr, // byte address of this transaction's first byte
input wire [MEM_DATA_WIDTH-1:0] mem_rdata,
input wire mem_ready
);
localparam BYTES_PER_WORD = MEM_DATA_WIDTH/8;
// The backing store is packed at the tile's OWN natural byte size
// (TILE_BITS/8 = P_IN*DATA_WIDTH/8, e.g. 8 bytes for P_IN=8/
// DATA_WIDTH=8), regardless of MEM_DATA_WIDTH. This equals
// WORDS_PER_TILE*BYTES_PER_WORD whenever MEM_DATA_WIDTH<=TILE_BITS
// (no waste, e.g. 16/32/64-bit busses), but NOT when
// MEM_DATA_WIDTH>TILE_BITS (e.g. a 128-bit bus fetching a 64-bit
// tile in one transaction, using only its low half) -- using
// WORDS_PER_TILE*BYTES_PER_WORD as the inter-tile address stride
// in that case would double-count the unused surplus bits as real
// address space and skip over the next tile's actual data in the
// packed backing store. TILE_BYTES is the correct stride always.
localparam TILE_BYTES = TILE_BITS/8;
reg [CNTW-1:0] fetch_tile;
reg [WIW-1:0] fetch_word;
reg req_outstanding;
reg [WORDS_PER_TILE*MEM_DATA_WIDTH-1:0] tile_buf; // oversized scratch, only low TILE_BITS used
// Final-word tile assembly, selected at ELABORATION time
// (WORDS_PER_TILE is a parameter) via generate -- avoids an
// invalid zero/negative-width part-select on tile_buf when
// WORDS_PER_TILE==1 (bus wider than one full tile: no "earlier
// words" exist at all, the ternary-operator alternative would
// still be elaborated structurally by most tools even though
// never selected at runtime).
wire [TILE_BITS-1:0] final_word_tile_data;
generate
if (WORDS_PER_TILE == 1) begin : GEN_ASSEMBLE_SINGLE
assign final_word_tile_data = mem_rdata[TILE_BITS-1:0];
end else begin : GEN_ASSEMBLE_MULTI
// Yosys' Verilog frontend rejects a part-select applied
// directly to a concatenation ({a,b}[msb:lsb]); Verilator
// accepts it, but real synthesis requires an intermediate
// signal instead.
wire [WORDS_PER_TILE*MEM_DATA_WIDTH-1:0] assembled_full;
assign assembled_full = {mem_rdata, tile_buf[(WORDS_PER_TILE-1)*MEM_DATA_WIDTH-1:0]};
assign final_word_tile_data = assembled_full[TILE_BITS-1:0];
end
endgenerate
wire [31:0] window_limit = {{(32-CNTW){1'b0}}, consumed_count} + PREFETCH_DISTANCE;
wire more_to_fetch = job_active &&
({{(16-CNTW){1'b0}}, fetch_tile} < n_tiles) &&
({{(32-CNTW){1'b0}}, fetch_tile} < window_limit);
always @(posedge clk) begin
if (rst) begin
fetch_tile <= {CNTW{1'b0}};
fetch_word <= {WIW{1'b0}};
ready_count <= {CNTW{1'b0}};
req_outstanding <= 1'b0;
mem_req <= 1'b0;
wgt_fill_we <= 1'b0;
end else begin
mem_req <= 1'b0;
wgt_fill_we <= 1'b0;
if (!job_active) begin
fetch_tile <= {CNTW{1'b0}};
fetch_word <= {WIW{1'b0}};
ready_count <= {CNTW{1'b0}};
req_outstanding <= 1'b0;
end else if (mem_ready && req_outstanding) begin
req_outstanding <= 1'b0;
tile_buf[fetch_word*MEM_DATA_WIDTH +: MEM_DATA_WIDTH] <= mem_rdata;
if (fetch_word == WORDS_PER_TILE[WIW-1:0] - 1'b1) begin
wgt_fill_we <= 1'b1;
wgt_fill_addr <= fetch_tile[TIW-1:0];
wgt_fill_data <= final_word_tile_data;
ready_count <= ready_count + 1'b1;
fetch_tile <= fetch_tile + 1'b1;
fetch_word <= {WIW{1'b0}};
if (({{(16-CNTW){1'b0}}, fetch_tile + 1'b1} < n_tiles) &&
({{(32-CNTW){1'b0}}, fetch_tile + 1'b1} < window_limit)) begin
mem_req <= 1'b1;
mem_addr <= w_base + (fetch_tile + 1'b1) * TILE_BYTES[CNTW-1:0];
req_outstanding <= 1'b1;
end
end else begin
fetch_word <= fetch_word + 1'b1;
mem_req <= 1'b1;
// next word within the SAME tile (only reached when
// WORDS_PER_TILE>1, i.e. MEM_DATA_WIDTH<=TILE_BITS,
// where WORDS_PER_TILE*BYTES_PER_WORD==TILE_BYTES
// exactly -- no surplus/waste in that regime):
// byte offset = fetch_tile*TILE_BYTES + (fetch_word+1)*BYTES_PER_WORD
mem_addr <= w_base + fetch_tile*TILE_BYTES[CNTW-1:0] +
({{(CNTW-WIW){1'b0}}, fetch_word} + 1'b1) * BYTES_PER_WORD[CNTW-1:0];
req_outstanding <= 1'b1;
end
end else if (!req_outstanding && more_to_fetch) begin
mem_req <= 1'b1;
mem_addr <= w_base + fetch_tile*TILE_BYTES[CNTW-1:0];
req_outstanding <= 1'b1;
end
end
end
endmodule