exp: layer_prefetch_ctrl.v, real synthesizable RTL for layer-weight-reuse prefetch, fixes a real address-truncation bug (EXP-0057b)
Built the real FSM version of EXP-0057's own task-based prefetch pattern (bulk-sequential layer fetch via sdram_controller_openrow.v into layer_weight_buffer.v), so it's an actual instantiable module, not just a simulation convenience. Found and fixed a real bug in the process: cur_fill_addr's own address arithmetic bit-sliced BYTES_PER_BURST down to too few bits (BYTES_PER_BURST[BIDXW-1:0]), silently truncating 16 to 0 -- every burst's bytes landed at fill offset 0-15 instead of their real position, overwriting each other (only each layer's last burst survived). Root cause: misapplied a widening idiom used safely elsewhere in this codebase to a case where the target width was actually too small. Found via a standalone control-flow debug test first, then tracing data once control-flow was cleared. Verified: 8192/8192 bit-exact, 0 errors (was 512/8192 before the fix) through the real controller + SDRAM model, 16 layers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ============================================================
|
||||
// EXP-0057 -- layer prefetch controller: bulk-sequential fetch of one
|
||||
// layer's weights from the real SDRAM controller (sdram_controller_
|
||||
// openrow.v's own req/wr/addr/wdata/wmask -> rdata/ready/busy
|
||||
// contract, BURST_LEN words per transaction) into a layer_weight_
|
||||
// buffer.v's inactive side. Real RTL version of the exact access
|
||||
// pattern tb_layer_reuse_vs_zero_reuse.v's own prefetch_layer task
|
||||
// already measured (7.16x real memory-side speedup vs zero-reuse,
|
||||
// same hardware, see that testbench's own header).
|
||||
//
|
||||
// One layer = LAYER_BYTES bytes, fetched as LAYER_BYTES/(2*BURST_LEN)
|
||||
// back-to-back BURST_LEN-word transactions starting at layer_base
|
||||
// (word address). Sequential -> lands in the SAME open row for any
|
||||
// layer that fits within one row (1024 columns = 256 tile-blocks at
|
||||
// BURST_LEN=8 -- true for any real layer size this project's own
|
||||
// target models use), so this composes directly with EXP-0054's
|
||||
// open-row policy without needing anything special here.
|
||||
//
|
||||
// Each captured burst (ctrl_rdata, 16*BURST_LEN bits) is LATCHED
|
||||
// locally before draining -- does not assume the controller holds
|
||||
// rdata stable beyond the cycle `ready` pulses (its own documented
|
||||
// contract is "valid the same cycle ready pulses", nothing more).
|
||||
// Drained one byte/cycle via a flat byte-index counter (drain_cnt)
|
||||
// indexing directly into the latched burst -- no separate word/byte
|
||||
// sub-counters to keep in sync, deliberately simpler than a first
|
||||
// draft of this module that tracked them separately and was harder to
|
||||
// convince correct by inspection.
|
||||
// ============================================================
|
||||
module layer_prefetch_ctrl #(
|
||||
parameter DATA_WIDTH = 8,
|
||||
parameter LAYER_BYTES = 128,
|
||||
parameter BURST_LEN = 8,
|
||||
parameter ADDR_WIDTH = 25, // matches sdram_controller_openrow.v's own word-address convention
|
||||
parameter BUFADDRW = (LAYER_BYTES <= 1) ? 1 : $clog2(LAYER_BYTES)
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
// ---- job control ----
|
||||
input wire start, // pulse: begin fetching `layer_base` into the inactive buffer
|
||||
input wire [ADDR_WIDTH-1:0] layer_base, // word address of this layer's weights in SDRAM
|
||||
output reg busy,
|
||||
output reg done, // pulse: matches layer_weight_buffer.v's own fill_done
|
||||
|
||||
// ---- layer_weight_buffer.v fill side ----
|
||||
output reg fill_we,
|
||||
output reg [BUFADDRW-1:0] fill_addr,
|
||||
output reg [DATA_WIDTH-1:0] fill_data,
|
||||
|
||||
// ---- sdram_controller_openrow.v (or plain sdram_controller.v --
|
||||
// identical port contract) ----
|
||||
output reg ctrl_req,
|
||||
output wire ctrl_wr, // always 0: read-only
|
||||
output reg [ADDR_WIDTH-1:0] ctrl_addr,
|
||||
output wire [16*BURST_LEN-1:0] ctrl_wdata, // unused (read-only), tied off
|
||||
output wire [2*BURST_LEN-1:0] ctrl_wmask, // unused (read-only), tied off
|
||||
input wire [16*BURST_LEN-1:0] ctrl_rdata,
|
||||
input wire ctrl_ready,
|
||||
input wire ctrl_busy
|
||||
);
|
||||
localparam BYTES_PER_BURST = 2*BURST_LEN;
|
||||
localparam BURSTS_PER_LAYER = LAYER_BYTES/BYTES_PER_BURST;
|
||||
localparam BIDXW = (BURSTS_PER_LAYER <= 1) ? 1 : $clog2(BURSTS_PER_LAYER);
|
||||
localparam DIDXW = $clog2(BYTES_PER_BURST);
|
||||
|
||||
assign ctrl_wr = 1'b0;
|
||||
assign ctrl_wdata = {(16*BURST_LEN){1'b0}};
|
||||
assign ctrl_wmask = {(2*BURST_LEN){1'b0}};
|
||||
|
||||
localparam S_IDLE = 3'd0,
|
||||
S_WAIT = 3'd1,
|
||||
S_DRAIN = 3'd2,
|
||||
S_TAIL = 3'd3;
|
||||
|
||||
reg [2:0] state;
|
||||
reg [BIDXW-1:0] burst_idx;
|
||||
reg [DIDXW-1:0] drain_cnt;
|
||||
reg [ADDR_WIDTH-1:0] base_lat;
|
||||
reg [16*BURST_LEN-1:0] burst_lat;
|
||||
|
||||
// combinational: which byte of the layer is currently being drained
|
||||
wire [BUFADDRW-1:0] cur_fill_addr = burst_idx * BYTES_PER_BURST + drain_cnt;
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (rst) begin
|
||||
state <= S_IDLE;
|
||||
busy <= 1'b0;
|
||||
done <= 1'b0;
|
||||
fill_we <= 1'b0;
|
||||
fill_addr <= {BUFADDRW{1'b0}};
|
||||
fill_data <= {DATA_WIDTH{1'b0}};
|
||||
ctrl_req <= 1'b0;
|
||||
ctrl_addr <= {ADDR_WIDTH{1'b0}};
|
||||
burst_idx <= {BIDXW{1'b0}};
|
||||
drain_cnt <= {DIDXW{1'b0}};
|
||||
base_lat <= {ADDR_WIDTH{1'b0}};
|
||||
burst_lat <= {(16*BURST_LEN){1'b0}};
|
||||
end else begin
|
||||
ctrl_req <= 1'b0;
|
||||
fill_we <= 1'b0;
|
||||
done <= 1'b0;
|
||||
|
||||
case (state)
|
||||
S_IDLE: begin
|
||||
busy <= 1'b0;
|
||||
if (start) begin
|
||||
busy <= 1'b1;
|
||||
base_lat <= layer_base;
|
||||
burst_idx <= {BIDXW{1'b0}};
|
||||
ctrl_req <= 1'b1;
|
||||
ctrl_addr <= layer_base;
|
||||
state <= S_WAIT;
|
||||
end
|
||||
end
|
||||
S_WAIT: begin
|
||||
if (ctrl_ready) begin
|
||||
burst_lat <= ctrl_rdata;
|
||||
drain_cnt <= {DIDXW{1'b0}};
|
||||
state <= S_DRAIN;
|
||||
end
|
||||
end
|
||||
S_DRAIN: begin
|
||||
fill_we <= 1'b1;
|
||||
fill_addr <= cur_fill_addr;
|
||||
fill_data <= burst_lat[drain_cnt*8 +: 8];
|
||||
|
||||
if (drain_cnt == BYTES_PER_BURST - 1) begin
|
||||
// this cycle drains the LAST byte of this burst
|
||||
if (burst_idx == BURSTS_PER_LAYER - 1) begin
|
||||
// last burst of the layer too -- one more
|
||||
// cycle for this final fill_we to land, then done
|
||||
state <= S_IDLE; // will be overridden below to a tail state
|
||||
end else begin
|
||||
burst_idx <= burst_idx + 1'b1;
|
||||
ctrl_req <= 1'b1;
|
||||
ctrl_addr <= base_lat + ((burst_idx + 1'b1) * BURST_LEN[ADDR_WIDTH-1:0]);
|
||||
state <= S_WAIT;
|
||||
end
|
||||
end else begin
|
||||
drain_cnt <= drain_cnt + 1'b1;
|
||||
end
|
||||
|
||||
if (drain_cnt == BYTES_PER_BURST - 1 &&
|
||||
burst_idx == BURSTS_PER_LAYER - 1) begin
|
||||
state <= S_TAIL;
|
||||
end
|
||||
end
|
||||
S_TAIL: begin
|
||||
// the last fill_we (asserted combinationally in the
|
||||
// S_DRAIN cycle above) is landing on THIS clock edge's
|
||||
// rising edge as far as layer_weight_buffer.v is
|
||||
// concerned (fill_we/_addr/_data were registered
|
||||
// outputs of the previous cycle) -- signal done now.
|
||||
busy <= 1'b0;
|
||||
done <= 1'b1;
|
||||
state <= S_IDLE;
|
||||
end
|
||||
default: state <= S_IDLE;
|
||||
endcase
|
||||
end
|
||||
end
|
||||
endmodule
|
||||
Reference in New Issue
Block a user