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
284 lines
13 KiB
Verilog
284 lines
13 KiB
Verilog
// ============================================================
|
|
// Neural Memory System (NMS) -- shared Activation fill controller.
|
|
//
|
|
// One instance per neural_memory_system (shared across all N_SLOTS),
|
|
// backing nms_activation_replicated.v's single broadcast-write fill
|
|
// port. Owns ONE prefetch_engine.v instance (real word-level PSRAM
|
|
// fetch, DEC-0015 convention, reused verbatim -- it is generic
|
|
// P_IN-byte-tile fetch logic, not weight-specific despite its name).
|
|
//
|
|
// Single-tag design (same honest limitation as the superseded
|
|
// hardware/v2/rtl/activation_cache.v, DEC-0016): tracks ONE resident
|
|
// x_base at a time. Refills (resident_count resets to 0, restarts
|
|
// fetching from tile 0) whenever the lowest-indexed currently-active
|
|
// slot's own x_base differs from what is resident -- correct always,
|
|
// but can thrash under interleaved, genuinely-different-x_base
|
|
// concurrent traffic; not exercised by this project's own realistic
|
|
// dense-layer workloads (shared-producer dispatch, many slots given
|
|
// the SAME x_base together).
|
|
//
|
|
// resident_count extends to the MAX n_tiles needed by any currently
|
|
// active slot that shares resident_tag (not just the reference slot
|
|
// that triggered the refill), so a later-joining slot with a deeper
|
|
// need is served without a second refill.
|
|
// ============================================================
|
|
module nms_activation_fill_ctrl_v3 #(
|
|
parameter DATA_WIDTH = 8,
|
|
parameter P_IN = 8,
|
|
parameter N_SLOTS = 4,
|
|
parameter ADDR_WIDTH = 26,
|
|
parameter MAX_TILES = 16,
|
|
// TIW indexes the SRAM fill address (0..MAX_TILES-1); CNTW is for
|
|
// resident_count, which must represent the VALUE MAX_TILES itself
|
|
// (e.g. a fully-resident 16-tile vector with MAX_TILES=16) -- one
|
|
// bit wider than TIW, same distinction/bug as
|
|
// nms_memory_manager.v's own tile_idx/wgt_fetched (see that file's
|
|
// header for the real deadlock this caused before the fix).
|
|
parameter TIW = (MAX_TILES <= 1) ? 1 : $clog2(MAX_TILES),
|
|
parameter CNTW = $clog2(MAX_TILES+1)
|
|
)(
|
|
input wire clk,
|
|
input wire rst,
|
|
|
|
// ---- per-slot job status (levels, held while that slot's job is active) ----
|
|
input wire [N_SLOTS-1:0] job_active,
|
|
input wire [N_SLOTS*ADDR_WIDTH-1:0] x_base_flat,
|
|
input wire [N_SLOTS*16-1:0] n_tiles_flat,
|
|
|
|
// ---- broadcast status (every slot compares this against its own x_base) ----
|
|
output reg [ADDR_WIDTH-1:0] resident_tag,
|
|
output reg [CNTW-1:0] resident_count,
|
|
|
|
// ---- fill port into nms_activation_replicated.v ----
|
|
output wire fill_we,
|
|
output wire [TIW-1:0] fill_addr,
|
|
output wire [DATA_WIDTH*P_IN-1:0] fill_data,
|
|
|
|
// ---- real word-level PSRAM backend (arbitrated externally) ----
|
|
output wire mem_req,
|
|
output wire mem_wr,
|
|
output wire [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
|
|
);
|
|
|
|
integer i;
|
|
|
|
// ---- desired x_base: lowest-indexed currently-active slot (fixed
|
|
// priority -- simple, not fairness-critical here since this only
|
|
// decides which TAG to chase, not who gets bandwidth) ----
|
|
reg desired_valid;
|
|
reg [ADDR_WIDTH-1:0] desired_x_base;
|
|
|
|
always @* begin
|
|
desired_valid = 1'b0;
|
|
desired_x_base = {ADDR_WIDTH{1'b0}};
|
|
for (i = N_SLOTS-1; i >= 0; i = i - 1) begin
|
|
if (job_active[i]) begin
|
|
desired_valid = 1'b1;
|
|
desired_x_base = x_base_flat[i*ADDR_WIDTH +: ADDR_WIDTH];
|
|
end
|
|
end
|
|
end
|
|
|
|
// ---- STEP14/EXP-0029->EXP-0030: max_n_tiles computation split
|
|
// into TWO pipeline stages, since registering ONLY its final use
|
|
// (v2, DEC-0026) left the computation ITSELF as the new critical
|
|
// path (EXP-0030, N=4 Fmax=72.78MHz, still FAIL@80MHz): the
|
|
// original single-cycle logic mixed, PER SLOT, a 23-bit tag
|
|
// equality check (x_base_flat[i]==resident_tag) together with an
|
|
// N_SLOTS-wide SEQUENTIALLY-CHAINED 16-bit running-max fold (each
|
|
// iteration's update depends on the previous one) -- both
|
|
// combinational, both in the same cycle as the register that
|
|
// captures the result.
|
|
//
|
|
// Stage 1 (independent per-slot work, no chain dependency between
|
|
// slots): register a per-slot "counts toward this refill" mask
|
|
// (job_active[i] && tag-match) and, gated by that mask, each
|
|
// slot's own n_tiles value (0 if it doesn't count) -- N_SLOTS
|
|
// independent 23-bit equality checks, no data dependency between
|
|
// slots, so their combined depth does not grow with N_SLOTS the
|
|
// way a sequential fold does.
|
|
// Stage 2 (the actual reduction): fold the REGISTERED, already-
|
|
// masked per-slot values into max_n_tiles_reg -- still an
|
|
// N_SLOTS-wide sequential chain (same fold as before), but now
|
|
// operating alone, without the equality check sharing the same
|
|
// cycle.
|
|
reg [15:0] n_tiles_masked [0:N_SLOTS-1];
|
|
genvar gsi;
|
|
generate
|
|
for (gsi = 0; gsi < N_SLOTS; gsi = gsi + 1) begin : GEN_MASK
|
|
wire slot_counts = job_active[gsi] &&
|
|
(x_base_flat[gsi*ADDR_WIDTH +: ADDR_WIDTH] == resident_tag);
|
|
always @(posedge clk) begin
|
|
if (rst) n_tiles_masked[gsi] <= 16'h0;
|
|
else n_tiles_masked[gsi] <= slot_counts ? n_tiles_flat[gsi*16 +: 16] : 16'h0;
|
|
end
|
|
end
|
|
endgenerate
|
|
|
|
// Balanced binary max-tree (log2(N_SLOTS) comparison levels)
|
|
// instead of the flat N_SLOTS-wide sequential scan this file's own
|
|
// header comment above already flagged as "an N_SLOTS-wide
|
|
// sequential chain". Found and fixed this session: that chain's
|
|
// own carry-chain critical path became the DOMINANT critical path
|
|
// at N_SLOTS=8 (real nextpnr-ecp5 P&R: Fmax collapsed to ~40MHz,
|
|
// failing the 64MHz target across every measured seed). A tree
|
|
// has the SAME single-cycle combinational timing as the scan it
|
|
// replaces (max_n_tiles_reg is still registered exactly one cycle
|
|
// behind n_tiles_masked -- no FSM/latency change, purely a
|
|
// combinational-depth reduction: log2(N_SLOTS) levels instead of
|
|
// N_SLOTS).
|
|
//
|
|
// Written as explicit, uniquely-named per-level wires (NOT a
|
|
// multi-dimensional generate-indexed array) -- a first attempt
|
|
// using a shared 2D `wire max_tree[level][idx]` array triggered a
|
|
// real simulator UNOPTFLAT "circular combinational logic" warning.
|
|
// The actual dependency graph IS acyclic (level L+1 only ever
|
|
// reads level L), but that tool's array-flattening circularity
|
|
// check could not prove that for a shared 2D array; distinctly-
|
|
// named per-level wires sidestep the ambiguity entirely for both
|
|
// simulation and synthesis. N_SLOTS is a power of two for every
|
|
// real configuration this project uses (1/2/4/8); anything else
|
|
// falls back, explicitly, to the original flat scan (correct but not
|
|
// optimized) rather than silently doing the wrong thing.
|
|
reg [15:0] max_n_tiles_reg;
|
|
generate
|
|
if (N_SLOTS == 1) begin : GEN_MAXTREE_N1
|
|
always @(posedge clk) begin
|
|
if (rst) max_n_tiles_reg <= 16'h0;
|
|
else max_n_tiles_reg <= n_tiles_masked[0];
|
|
end
|
|
end else if (N_SLOTS == 2) begin : GEN_MAXTREE_N2
|
|
wire [15:0] max_final = (n_tiles_masked[0] > n_tiles_masked[1]) ? n_tiles_masked[0] : n_tiles_masked[1];
|
|
always @(posedge clk) begin
|
|
if (rst) max_n_tiles_reg <= 16'h0;
|
|
else max_n_tiles_reg <= max_final;
|
|
end
|
|
end else if (N_SLOTS == 4) begin : GEN_MAXTREE_N4
|
|
wire [15:0] m0 = (n_tiles_masked[0] > n_tiles_masked[1]) ? n_tiles_masked[0] : n_tiles_masked[1];
|
|
wire [15:0] m1 = (n_tiles_masked[2] > n_tiles_masked[3]) ? n_tiles_masked[2] : n_tiles_masked[3];
|
|
wire [15:0] max_final = (m0 > m1) ? m0 : m1;
|
|
always @(posedge clk) begin
|
|
if (rst) max_n_tiles_reg <= 16'h0;
|
|
else max_n_tiles_reg <= max_final;
|
|
end
|
|
end else if (N_SLOTS == 8) begin : GEN_MAXTREE_N8
|
|
wire [15:0] m0 = (n_tiles_masked[0] > n_tiles_masked[1]) ? n_tiles_masked[0] : n_tiles_masked[1];
|
|
wire [15:0] m1 = (n_tiles_masked[2] > n_tiles_masked[3]) ? n_tiles_masked[2] : n_tiles_masked[3];
|
|
wire [15:0] m2 = (n_tiles_masked[4] > n_tiles_masked[5]) ? n_tiles_masked[4] : n_tiles_masked[5];
|
|
wire [15:0] m3 = (n_tiles_masked[6] > n_tiles_masked[7]) ? n_tiles_masked[6] : n_tiles_masked[7];
|
|
wire [15:0] m01 = (m0 > m1) ? m0 : m1;
|
|
wire [15:0] m23 = (m2 > m3) ? m2 : m3;
|
|
wire [15:0] max_final = (m01 > m23) ? m01 : m23;
|
|
always @(posedge clk) begin
|
|
if (rst) max_n_tiles_reg <= 16'h0;
|
|
else max_n_tiles_reg <= max_final;
|
|
end
|
|
end else begin : GEN_MAXTREE_FALLBACK
|
|
reg [15:0] max_n_tiles_comb_fallback;
|
|
integer j;
|
|
always @* begin
|
|
max_n_tiles_comb_fallback = 16'h0;
|
|
for (j = 0; j < N_SLOTS; j = j + 1)
|
|
if (n_tiles_masked[j] > max_n_tiles_comb_fallback)
|
|
max_n_tiles_comb_fallback = n_tiles_masked[j];
|
|
end
|
|
always @(posedge clk) begin
|
|
if (rst) max_n_tiles_reg <= 16'h0;
|
|
else max_n_tiles_reg <= max_n_tiles_comb_fallback;
|
|
end
|
|
end
|
|
endgenerate
|
|
|
|
localparam ST_IDLE = 1'd0;
|
|
localparam ST_FETCH = 1'd1;
|
|
reg state;
|
|
|
|
reg pf_start;
|
|
reg [ADDR_WIDTH-1:0] pf_addr;
|
|
wire pf_busy, pf_done;
|
|
wire signed [DATA_WIDTH*P_IN-1:0] pf_tile;
|
|
|
|
prefetch_engine #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH)
|
|
) u_pf (
|
|
.clk(clk), .rst(rst),
|
|
.fetch_start(pf_start), .w_addr(pf_addr),
|
|
.fetch_busy(pf_busy), .fetch_done(pf_done), .tile_w(pf_tile),
|
|
.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)
|
|
);
|
|
|
|
reg fill_we_reg;
|
|
reg [TIW-1:0] fill_addr_reg;
|
|
reg [DATA_WIDTH*P_IN-1:0] fill_data_reg;
|
|
assign fill_we = fill_we_reg;
|
|
assign fill_addr = fill_addr_reg;
|
|
assign fill_data = fill_data_reg;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
resident_tag <= {ADDR_WIDTH{1'b1}}; // sentinel: matches no real x_base at reset
|
|
resident_count <= {CNTW{1'b0}};
|
|
state <= ST_IDLE;
|
|
pf_start <= 1'b0;
|
|
fill_we_reg <= 1'b0;
|
|
end else begin
|
|
pf_start <= 1'b0;
|
|
fill_we_reg <= 1'b0;
|
|
|
|
// latch a completed fetch into the replicated activation
|
|
// memory's broadcast fill port
|
|
if (pf_done) begin
|
|
fill_we_reg <= 1'b1;
|
|
fill_addr_reg <= resident_count[TIW-1:0]; // valid: gated < max_n_tiles <= MAX_TILES
|
|
fill_data_reg <= pf_tile;
|
|
resident_count <= resident_count + 1'b1;
|
|
end
|
|
|
|
// refill trigger: the reference slot wants a DIFFERENT tag,
|
|
// and the fetch engine is genuinely idle (never interrupt an
|
|
// in-flight fetch -- same discipline as memory_manager.v's
|
|
// own pf_pending guard, ERR-0006). Stays in ST_IDLE (not
|
|
// ST_FETCH): only updates resident_tag/resident_count here;
|
|
// the ST_IDLE case below is what actually issues pf_start,
|
|
// reading the NEW resident_tag starting next cycle -- this
|
|
// path must NOT itself jump to ST_FETCH without a matching
|
|
// pf_start, or the engine would sit in ST_FETCH forever
|
|
// waiting for a pf_done that was never triggered.
|
|
if (desired_valid && (desired_x_base != resident_tag) && !pf_busy && (state == ST_IDLE)) begin
|
|
resident_tag <= desired_x_base;
|
|
resident_count <= {CNTW{1'b0}};
|
|
end
|
|
|
|
case (state)
|
|
ST_IDLE: begin
|
|
// stay idle: fetching further tiles for the CURRENT
|
|
// tag (if any active slot still needs more) is
|
|
// handled below, symmetric to the refill case.
|
|
if (!pf_busy && !pf_start && (resident_count < max_n_tiles_reg) &&
|
|
desired_valid && (desired_x_base == resident_tag)) begin
|
|
pf_start <= 1'b1;
|
|
pf_addr <= resident_tag + (resident_count * P_IN[ADDR_WIDTH-1:0]);
|
|
state <= ST_FETCH;
|
|
end
|
|
end
|
|
ST_FETCH: begin
|
|
if (pf_done) begin
|
|
// resident_count already bumped above this cycle;
|
|
// decide whether more remain once back in IDLE.
|
|
state <= ST_IDLE;
|
|
end
|
|
end
|
|
default: state <= ST_IDLE;
|
|
endcase
|
|
end
|
|
end
|
|
|
|
endmodule
|