diff --git a/hardware/v2/logs/experiments.log b/hardware/v2/logs/experiments.log index 48885ff..3fb3df7 100644 --- a/hardware/v2/logs/experiments.log +++ b/hardware/v2/logs/experiments.log @@ -3761,3 +3761,52 @@ correctness-verified (isolated testbench, bit-exact vs golden model) integration BEFORE the next real P&R congestion check -- do not synthesize unverified integration RTL just to get another Fmax number, per this project's own correctness-first standard. + +EXP-0061 -- weight_tile_gather.v: real synthesizable RTL for the +byte-to-tile assembly step EXP-0058 left testbench-only (2026-09-17) + +CONTEXT: EXP-0058's own log entry (tb_neural_processor_layer_reuse.v) +explicitly flagged that assembling P_IN sequential byte-wide +layer_weight_buffer.v reads into one weight_data tile bus was done in +the TESTBENCH driver task, not synthesizable RTL, and named this as +"the natural next M4 Memory Manager deliverable if this architecture +is adopted for the real board" -- V3/XC7A100T is that adoption +(EXP-0059/0060), so this gap needed closing before any real +integration synthesis. + +METHOD: new hardware/v3/rtl/weight_tile_gather.v, a small FSM (IDLE/ +RUN, P_IN+1 cycles/tile) sitting between layer_weight_buffer.v's +byte-wide read port and a P_IN-wide tile_data bus. Deliberately avoids +the runtime-indexed-part-select anti-pattern this project has already +been bitten by twice (neural_director.v's own slot_x_base_r fix, +ERR-0027-class Fmax collapse from a variable-indexed write into a wide +packed register) -- uses a fixed compile-time-constant shift-concat +(`tile_data <= {rd_data, tile_data[DATA_WIDTH*P_IN-1:DATA_WIDTH]}`) +instead. Verified in isolation (hardware/v3/sim/tb_weight_tile_gather.v) +against a real, unmodified layer_weight_buffer.v (hardware/v2/rtl/, +128-byte layer, deterministic non-uniform pattern): sequential tiles, +back-to-back requests with no idle gap, and non-sequential/repeated +(real reuse-position-style) tile requests. + +RESULT: 37/37 tests, 0 errors, bit-exact byte->tile assembly in every +access pattern tested, including the real reuse-position pattern +(same tile requested twice, non-monotonic addresses). + +DECISION: weight_tile_gather.v is verified correct in isolation and +ready to be wired into the full weight-reuse memory path (layer_ +prefetch_ctrl.v -> layer_weight_buffer.v -> weight_tile_gather.v -> +neural_processor_packed.v) for a real end-to-end integration test, +mirroring EXP-0058's own tb_neural_processor_layer_reuse.v methodology +but with real synthesizable gather RTL instead of a testbench-only +gather step, and the packed 2-job core instead of two separate M1 +cores. + +next_action: build that end-to-end integration testbench (real SDRAM +model -> layer_prefetch_ctrl.v -> layer_weight_buffer.v -> +weight_tile_gather.v -> neural_processor_packed.v, independent golden +model), verify bit-exact, THEN (only after that passes) synthesize the +combined path for a real P&R number -- still no neural_director.v +job-pairing changes needed for this step (a single hardcoded layer/ +position-pair sequence is enough to prove the memory path + packed +core compose correctly; Director-level dynamic pairing is a separate, +later increment). diff --git a/hardware/v3/rtl/weight_tile_gather.v b/hardware/v3/rtl/weight_tile_gather.v new file mode 100644 index 0000000..8ba7f7e --- /dev/null +++ b/hardware/v3/rtl/weight_tile_gather.v @@ -0,0 +1,90 @@ +`timescale 1ns/1ps + +// ============================================================ +// V3 -- real synthesizable tile-gather adapter, the piece EXP-0058's +// own log entry flagged as still missing ("a real 'tile gather +// adapter' (8:1 byte-to-tile packer) would be the natural next M4 +// Memory Manager deliverable if this architecture is adopted for the +// real board" -- tb_neural_processor_layer_reuse.v did this step in +// the testbench only, not in RTL). +// +// Sits between layer_weight_buffer.v's byte-wide read port (one +// address = one byte) and neural_processor_packed.v's P_IN-wide +// weight_data tile bus. Sequences P_IN reads, one byte/cycle, and +// assembles them via a FIXED (compile-time-constant) shift-concat -- +// deliberately NOT a runtime-indexed part-select into the wide +// tile_data register. This project has already been bitten by that +// exact anti-pattern twice (neural_director.v's own slot_x_base_r +// fix, ERR-0027-class: a variable-indexed write into a wide packed +// register synthesizes as a real hard-multiplier-fed crossbar, real +// measured Fmax collapse 68.51->~40-47MHz) -- avoided here from the +// start rather than found and fixed later. +// +// Byte read at tile_base+i lands at tile_data[i*DATA_WIDTH +: +// DATA_WIDTH] (i=0 is the FIRST byte read, ends at the LSB end) -- +// matches neural_processor_packed.v's own w0[gi] <= +// weight_data[gi*DATA_WIDTH +: DATA_WIDTH] indexing exactly. +// +// Latency: P_IN+1 cycles from tile_req to tile_valid (1 address-setup +// cycle + P_IN capture-and-advance cycles) -- correctness-first, not +// yet pipelined/overlapped; matches this project's own staged +// performance-after-correctness discipline. +// ============================================================ +module weight_tile_gather #( + parameter DATA_WIDTH = 8, + parameter P_IN = 8, + parameter BUFADDRW = 7 +)( + input wire clk, + input wire rst, + + // ---- control: gather the tile starting at tile_base ---- + input wire tile_req, + input wire [BUFADDRW-1:0] tile_base, + output reg tile_valid, // one-cycle pulse + output reg [DATA_WIDTH*P_IN-1:0] tile_data, + + // ---- layer_weight_buffer.v read port ---- + output reg [BUFADDRW-1:0] rd_addr, + input wire [DATA_WIDTH-1:0] rd_data +); + localparam CNTW = $clog2(P_IN+1); + localparam G_IDLE = 1'b0, G_RUN = 1'b1; + + reg g_state; + reg [CNTW-1:0] byte_cnt; + + always @(posedge clk) begin + if (rst) begin + g_state <= G_IDLE; + tile_valid <= 1'b0; + rd_addr <= {BUFADDRW{1'b0}}; + byte_cnt <= {CNTW{1'b0}}; + tile_data <= {(DATA_WIDTH*P_IN){1'b0}}; + end else begin + tile_valid <= 1'b0; + case (g_state) + G_IDLE: begin + if (tile_req) begin + rd_addr <= tile_base; + byte_cnt <= {CNTW{1'b0}}; + g_state <= G_RUN; + end + end + G_RUN: begin + // rd_data reflects the rd_addr driven last cycle + // (layer_weight_buffer.v's read is combinational). + tile_data <= {rd_data, tile_data[DATA_WIDTH*P_IN-1:DATA_WIDTH]}; + if (byte_cnt == P_IN[CNTW-1:0] - 1'b1) begin + tile_valid <= 1'b1; + g_state <= G_IDLE; + end else begin + rd_addr <= tile_base + byte_cnt + 1'b1; + byte_cnt <= byte_cnt + 1'b1; + end + end + default: g_state <= G_IDLE; + endcase + end + end +endmodule diff --git a/hardware/v3/sim/tb_weight_tile_gather.v b/hardware/v3/sim/tb_weight_tile_gather.v new file mode 100644 index 0000000..e942e78 --- /dev/null +++ b/hardware/v3/sim/tb_weight_tile_gather.v @@ -0,0 +1,143 @@ +`timescale 1ns/1ps + +// ============================================================ +// Isolated correctness test for weight_tile_gather.v, forked against +// a real layer_weight_buffer.v (hardware/v2/rtl/, unmodified) -- +// verifies the byte->tile assembly is bit-exact BEFORE integrating +// with neural_processor_packed.v, per this project's own "verify in +// isolation first" discipline (see feedback-correctness-first- +// verification). +// ============================================================ +module tb; + localparam DATA_WIDTH = 8; + localparam P_IN = 8; + localparam LAYER_DEPTH = 128; + localparam BUFADDRW = $clog2(LAYER_DEPTH); + localparam N_TILES = LAYER_DEPTH / P_IN; + + reg clk = 0; + always #5 clk = ~clk; // 100MHz sim clock, arbitrary for a functional-only check + + reg rst; + integer errors, tests; + + // ---- layer_weight_buffer.v (real, unmodified) ---- + reg fill_we; + reg [BUFADDRW-1:0] fill_addr; + reg [DATA_WIDTH-1:0] fill_data; + reg fill_done; + wire [BUFADDRW-1:0] rd_addr; + wire [DATA_WIDTH-1:0] rd_data; + reg consume_done; + wire active_sel, swapped; + + layer_weight_buffer #( + .DATA_WIDTH(DATA_WIDTH), .LAYER_DEPTH(LAYER_DEPTH) + ) buf_dut ( + .clk(clk), .rst(rst), + .fill_we(fill_we), .fill_addr(fill_addr), .fill_data(fill_data), .fill_done(fill_done), + .rd_addr(rd_addr), .rd_data(rd_data), .consume_done(consume_done), + .active_sel(active_sel), .swapped(swapped) + ); + + // ---- weight_tile_gather.v (DUT) ---- + reg tile_req; + reg [BUFADDRW-1:0] tile_base; + wire tile_valid; + wire [DATA_WIDTH*P_IN-1:0] tile_data; + + weight_tile_gather #( + .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BUFADDRW(BUFADDRW) + ) gather_dut ( + .clk(clk), .rst(rst), + .tile_req(tile_req), .tile_base(tile_base), + .tile_valid(tile_valid), .tile_data(tile_data), + .rd_addr(rd_addr), .rd_data(rd_data) + ); + + // ---- reference layer content: layer_pattern[i] = (i*7+3) & 0xFF + // (deterministic, non-uniform, matches this project's own + // "small non-uniform values" testing convention) ---- + reg [DATA_WIDTH-1:0] layer_pattern [0:LAYER_DEPTH-1]; + integer li; + + task automatic gather_and_check(input [BUFADDRW-1:0] base, input integer tile_idx); + integer k; + reg [DATA_WIDTH*P_IN-1:0] expected; + begin + for (k = 0; k < P_IN; k = k + 1) + expected[k*DATA_WIDTH +: DATA_WIDTH] = layer_pattern[base + k]; + + @(posedge clk); + tile_req = 1'b1; + tile_base = base; + @(posedge clk); + tile_req = 1'b0; + while (!tile_valid) @(posedge clk); + + tests = tests + 1; + if (tile_data !== expected) begin + $display("FAIL tile %0d base=%0d: got=%h expected=%h", tile_idx, base, tile_data, expected); + errors = errors + 1; + end else begin + $display("PASS tile %0d base=%0d: bit-exact %h", tile_idx, base, tile_data); + end + end + endtask + + integer t; + initial begin + errors = 0; tests = 0; + rst = 1; fill_we = 0; fill_addr = 0; fill_data = 0; fill_done = 0; + consume_done = 0; tile_req = 0; tile_base = 0; + + for (li = 0; li < LAYER_DEPTH; li = li + 1) + layer_pattern[li] = (li*7+3) & 8'hFF; + + repeat(3) @(posedge clk); + rst = 0; + @(posedge clk); + + // fill the (inactive) buffer with the reference pattern via + // the real fill_we/fill_addr/fill_data port, then declare it done + for (li = 0; li < LAYER_DEPTH; li = li + 1) begin + @(posedge clk); + fill_we = 1'b1; + fill_addr = li[BUFADDRW-1:0]; + fill_data = layer_pattern[li]; + end + @(posedge clk); + fill_we = 1'b0; + fill_done = 1'b1; + @(posedge clk); + fill_done = 1'b0; + // consume_done pulses too (this buffer's own swap needs both -- + // no real "active" consumption happened yet, but at reset + // active_sel=0 and we just filled buffer 1 (the inactive one at + // reset) -- swap once so reads below hit the buffer we just filled. + consume_done = 1'b1; + @(posedge clk); + consume_done = 1'b0; + while (!swapped) @(posedge clk); // wait for the real swap pulse + @(posedge clk); + + $display("=== TEST 1: sequential tiles, whole layer ==="); + for (t = 0; t < N_TILES; t = t + 1) + gather_and_check(t*P_IN, t); + + $display("=== TEST 2: back-to-back tile_req with no idle gap ==="); + for (t = 0; t < N_TILES; t = t + 1) + gather_and_check(t*P_IN, t); + + $display("=== TEST 3: non-sequential (reuse-position-style) tile requests ==="); + gather_and_check(8*P_IN, 8); + gather_and_check(2*P_IN, 2); + gather_and_check(8*P_IN, 8); // re-request same tile (real reuse pattern) + gather_and_check(15*P_IN, 15); + gather_and_check(0, 0); + + $display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors); + if (errors == 0) $display("ALL TESTS PASSED (tb_weight_tile_gather)"); + $finish; + end +endmodule