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
182 lines
8.8 KiB
Verilog
182 lines
8.8 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// Neural Memory System (NMS) -- STEP11: real weight prefetch engine.
|
|
//
|
|
// Replaces prefetch_engine.v's per-tile single-shot usage inside
|
|
// nms_memory_manager.v with a CONTINUOUS, multi-tile fetch stream.
|
|
//
|
|
// Real analysis (see docs/architecture/nms_weight_prefetch.md and
|
|
// hardware/v2/logs/development.log): the real backend
|
|
// (hardware/v1/rtl/memory_interface.v -> psram_controller.v) is a
|
|
// fire-and-forget, ONE-transaction-in-flight-at-a-time protocol (a
|
|
// single mem_req pulse, wait for mem_ready, that IS the transaction --
|
|
// no wire-level pipelining is physically possible against this real
|
|
// backend, matching the real PSRAM's own single physical port). So
|
|
// "multiple outstanding requests" cannot mean multiple simultaneous
|
|
// WORD transactions -- it means eliminating the CONTROL-PLANE
|
|
// overhead the old design paid at every tile boundary (prefetch_engine
|
|
// return-to-IDLE, fetch_done pulse, nms_memory_manager's own
|
|
// !pf_done-gated restart, ERR-0013) and letting the fetch stream run
|
|
// CONTINUOUSLY across tile boundaries, queueing up to
|
|
// PREFETCH_DISTANCE tiles' worth of lookahead ahead of consumption
|
|
// instead of restarting control state once per tile.
|
|
//
|
|
// Tiles are always fetched in strict sequential order (0..n_tiles-1,
|
|
// never reordered, never re-fetched) -- so no per-tile state array is
|
|
// needed; two monotonic counters (fetch progress, consumption
|
|
// progress) fully describe the system, exactly like the superseded
|
|
// design, but the FETCH counter now advances continuously instead of
|
|
// stalling at each tile boundary.
|
|
// ============================================================
|
|
module weight_prefetch_engine #(
|
|
parameter DATA_WIDTH = 8,
|
|
parameter P_IN = 8,
|
|
parameter ADDR_WIDTH = 23,
|
|
parameter MAX_TILES = 16,
|
|
parameter PREFETCH_DISTANCE = 8,
|
|
parameter TIW = (MAX_TILES <= 1) ? 1 : $clog2(MAX_TILES),
|
|
parameter CNTW = $clog2(MAX_TILES+1),
|
|
parameter WORDS_PER_TILE = P_IN/2,
|
|
parameter WIW = $clog2(WORDS_PER_TILE+1)
|
|
)(
|
|
input wire clk,
|
|
input wire rst,
|
|
|
|
// ---- job control (level-held while a job is running; the
|
|
// consumer -- nms_memory_manager.v -- resets its OWN tile_idx to 0
|
|
// on job_start, this engine mirrors that via job_active falling/
|
|
// rising) ----
|
|
input wire job_active,
|
|
input wire [ADDR_WIDTH-1:0] w_base, // byte address, word-aligned
|
|
input wire [15:0] n_tiles,
|
|
input wire [CNTW-1:0] consumed_count, // consumer's own tile_idx, bounds lookahead
|
|
|
|
// ---- fill port into nms_weight_packed.v (this slot's own private lane) ----
|
|
output reg wgt_fill_we,
|
|
output reg [TIW-1:0] wgt_fill_addr,
|
|
output reg [DATA_WIDTH*P_IN-1:0] wgt_fill_data,
|
|
|
|
// ---- status to consumer: tiles 0..ready_count-1 are fully resident ----
|
|
output reg [CNTW-1:0] ready_count,
|
|
|
|
// ---- real word-level PSRAM backend (matches
|
|
// hardware/v1/rtl/memory_interface.v's contract exactly, same as
|
|
// prefetch_engine.v's own real, proven usage) ----
|
|
output reg mem_req,
|
|
output wire mem_wr,
|
|
output reg [ADDR_WIDTH-1:0] mem_addr,
|
|
output wire [15:0] mem_wdata,
|
|
output wire mem_lb_n,
|
|
output wire mem_ub_n,
|
|
input wire [15:0] mem_rdata,
|
|
input wire mem_ready
|
|
);
|
|
|
|
assign mem_wr = 1'b0;
|
|
assign mem_wdata = 16'h0000;
|
|
assign mem_lb_n = 1'b0; // fetch the whole word, both byte lanes
|
|
assign mem_ub_n = 1'b0;
|
|
|
|
// fetch_tile/fetch_word: the NEXT word to be requested (or, while
|
|
// req_outstanding, the word CURRENTLY in flight).
|
|
reg [CNTW-1:0] fetch_tile;
|
|
reg [WIW-1:0] fetch_word;
|
|
reg req_outstanding;
|
|
reg [DATA_WIDTH*P_IN-1:0] tile_buf;
|
|
|
|
wire [ADDR_WIDTH-1:0] w_word_base = w_base[ADDR_WIDTH-1:1];
|
|
|
|
// Don't fetch past n_tiles, and don't get more than
|
|
// PREFETCH_DISTANCE tiles ahead of the consumer's own progress --
|
|
// the real, configurable lookahead window (STEP11's own
|
|
// requirement). consumed_count is the consumer's tile_idx
|
|
// (registered, one cycle old at most -- fine, this only bounds a
|
|
// SOFT bandwidth-shaping window, not a correctness-critical value:
|
|
// over-fetching by one extra tile due to a one-cycle-stale compare
|
|
// is harmless, the SRAM has room for the whole vector regardless).
|
|
//
|
|
// window_limit/the comparison below are computed in a FIXED 32-bit
|
|
// width, wide enough to hold PREFETCH_DISTANCE undamaged for any
|
|
// realistic parameter value -- an earlier version truncated
|
|
// PREFETCH_DISTANCE down to CNTW bits before adding it
|
|
// (PREFETCH_DISTANCE[CNTW-1:0]), which silently wrapped PFD=32 to 0
|
|
// at MAX_TILES=16 (CNTW=5 bits), making window_limit==consumed_count
|
|
// and more_to_fetch permanently false -- a full, real deadlock (see
|
|
// errors.log ERR-0015, same TIW/CNTW-truncation bug class as
|
|
// ERR-0014, this time on the newly-introduced PREFETCH_DISTANCE
|
|
// parameter itself rather than a tile counter).
|
|
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
|
|
// mirrors the consumer's own job_start reset (nms_
|
|
// memory_manager.v resets its tile_idx the same way)
|
|
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
|
|
// A word just completed. Commit it, THEN -- same
|
|
// cycle, not next -- decide the very next request
|
|
// (same tile's next word, or the following tile's
|
|
// first word): this is what makes the fetch stream
|
|
// genuinely continuous across tile boundaries, not
|
|
// just within one tile the way prefetch_engine.v's own
|
|
// design already was. Computed with blocking-style
|
|
// "next state" locals so both the commit and the next
|
|
// request land in a single, unambiguous set of NBAs.
|
|
req_outstanding <= 1'b0;
|
|
tile_buf[fetch_word*16 +: 16] <= 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 <= {mem_rdata, tile_buf[DATA_WIDTH*P_IN-17:0]};
|
|
ready_count <= ready_count + 1'b1;
|
|
fetch_tile <= fetch_tile + 1'b1;
|
|
fetch_word <= {WIW{1'b0}};
|
|
// start the NEXT tile's first word immediately if
|
|
// the (post-increment) tile is still within bounds
|
|
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_word_base + (fetch_tile + 1'b1) * WORDS_PER_TILE[CNTW-1:0];
|
|
req_outstanding <= 1'b1;
|
|
end
|
|
end else begin
|
|
fetch_word <= fetch_word + 1'b1;
|
|
mem_req <= 1'b1;
|
|
mem_addr <= w_word_base + fetch_tile*WORDS_PER_TILE[CNTW-1:0] + {{(ADDR_WIDTH-WIW){1'b0}}, fetch_word} + 1'b1;
|
|
req_outstanding <= 1'b1;
|
|
end
|
|
end else if (!req_outstanding && more_to_fetch) begin
|
|
// reached only when nothing has ever been requested
|
|
// yet for this job (the very first word) -- every
|
|
// subsequent request is issued from the branch above,
|
|
// in the same cycle its predecessor's mem_ready
|
|
// arrives, with zero gap, including across tile
|
|
// boundaries.
|
|
mem_req <= 1'b1;
|
|
mem_addr <= w_word_base + fetch_tile*WORDS_PER_TILE[CNTW-1:0] + {{(ADDR_WIDTH-WIW){1'b0}}, fetch_word};
|
|
req_outstanding <= 1'b1;
|
|
end
|
|
end
|
|
end
|
|
|
|
endmodule
|