`timescale 1ns/1ps // ============================================================ // V3 -- act_tile_fetch.v: REAL activation-tile fetch engine, closing // the gap packed_slot.v's own header has disclosed since EXP-0062 // ("a real activation fetch engine ... is a separate, later // deliverable, NOT built here"). This is that deliverable. // // WHY A SEPARATE, SIMPLE ENGINE (not a prefetch/buffer pair like the // weight path): weights are reused across M reuse-positions per // Director-dispatched pair, so prefetching them once into an on-chip // buffer (layer_prefetch_ctrl.v/layer_weight_buffer.v) amortizes real // DDR3 latency across many reads. Activation data has NO such reuse // -- each position's activation tile is read exactly once per job -- // so buffering it on-chip would only add complexity for zero benefit. // This engine reads DIRECTLY from DDR3 per tile instead. // // MEMORY LAYOUT CONVENTION v3 (EXP-0084, real, disclosed, and REQUIRED // of whoever prepares activation data in DDR3 -- documented in the // physical realization doc too): FOUR consecutive tiles (P_IN=8 INT8 // values each, 64 bits each) share ONE full BURST_LEN=8-word burst -- // since EXP-0084's real 32-bit DDR3 channel widening, one burst is now // 8*32=256 bits (up from 128 bits at the old 16-bit width), and 4 // tiles of 64 bits exactly fill it (100% utilization, same packing // EFFICIENCY as EXP-0081's "2 tiles fill a 128-bit burst" -- this is // NOT a further bytes-per-MAC reduction beyond EXP-0081's already- // optimal 1 byte/MAC, it is what's REQUIRED to keep that same 100% // utilization at the new, larger burst size instead of leaving half // of it newly wasted). Tile index within the burst selects a quarter: // tile parity 0/1/2/3 (tcnt[1:0]) -> bits [63:0]/[127:64]/[191:128]/ // [255:192] of the burst response. Tile t's burst address is // `base + (t>>2)*BURST_LEN`. // // WHY THIS IS TIMING-SAFE (the thing EXP-0079 deliberately avoided): // the tile index's own low 2 bits (which quarter of the burst to use) // are known at REQUEST time, not at response time -- registered into // `sel_lat` the SAME cycle `tcnt` is latched, many ui_clk cycles // BEFORE the real DDR3 round-trip completes and `ctrl_rdata` becomes // valid. The eventual data-select mux (a real `case` on the registered // 2-bit `sel_lat`, not a runtime-indexed part-select expression -- // deliberately written as explicit constant-offset case arms, see // below) therefore selects using an already-long-stable registered // value, never bits racing the read data itself -- this is NOT the // runtime-indexed-part-select-on-the-critical-path pattern weight_ // tile_gather.v's own header (EXP-0061) warned about; that pattern is // about a select signal arriving LATE/simultaneously with the data it // gates. Same real discipline EXP-0081 already established for the // 1-bit case, now extended to 2 bits -- confirmed via a real P&R // re-check after this change (see the log), not just asserted. // // PROTOCOL: one request (`req` pulse + base_a/base_b/tcnt) triggers // TWO SEQUENTIAL burst reads (lane A then lane B) over the SAME // shared ctrl port packed_slot.v already owns -- reusing the EXACT // port layer_prefetch_ctrl.v uses during S_PREFETCH, since that // phase has already finished (weight data is on-chip by the time // this engine runs) and the port is genuinely free. Follows the same // combinational-first-grant discipline as every other one-shot-pulse // requester in this project (EXP-0066): `mem_active` must be visible // to the arbiter the SAME cycle it asserts, `ctrl_req` is only issued // after `mem_grant` is observed, never blind. // ============================================================ module act_tile_fetch #( parameter DATA_WIDTH = 8, parameter P_IN = 8, parameter BURST_LEN = 8, parameter ADDR_WIDTH = 25 // word address, matches the shared ctrl port's own convention )( input wire clk, input wire rst, input wire req, // one-shot pulse input wire [ADDR_WIDTH-1:0] base_a, input wire [ADDR_WIDTH-1:0] base_b, input wire [15:0] tcnt, output reg valid, // one-cycle pulse, data_a/data_b valid output reg signed [DATA_WIDTH*P_IN-1:0] data_a, output reg signed [DATA_WIDTH*P_IN-1:0] data_b, output wire mem_active, input wire mem_grant, output reg ctrl_req, output reg ctrl_wr, output reg [ADDR_WIDTH-1:0] ctrl_addr, output wire [32*BURST_LEN-1:0] ctrl_wdata, output wire [4*BURST_LEN-1:0] ctrl_wmask, input wire [32*BURST_LEN-1:0] ctrl_rdata, input wire ctrl_ready, input wire ctrl_busy ); assign ctrl_wdata = {(32*BURST_LEN){1'b0}}; assign ctrl_wmask = {(4*BURST_LEN){1'b0}}; // read-only engine, mask unused localparam S_IDLE = 3'd0, S_MEMWAIT = 3'd1, S_REQ_A = 3'd2, S_GAP = 3'd3, // wait for ctrl_busy to clear before firing lane B's request S_REQ_B = 3'd4; reg [2:0] state; reg [ADDR_WIDTH-1:0] base_a_lat, base_b_lat; reg [15:0] tcnt_lat; reg [1:0] sel_lat; // registered at request time -- see header assign mem_active = (state != S_IDLE); // burst index = tcnt/4 (integer division -- four tiles share one burst) wire [ADDR_WIDTH-1:0] tile_offset = {{(ADDR_WIDTH-14){1'b0}}, tcnt_lat[15:2]} * BURST_LEN[ADDR_WIDTH-1:0]; always @(posedge clk) begin if (rst) begin state <= S_IDLE; ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {ADDR_WIDTH{1'b0}}; valid <= 1'b0; data_a <= {(DATA_WIDTH*P_IN){1'b0}}; data_b <= {(DATA_WIDTH*P_IN){1'b0}}; base_a_lat <= {ADDR_WIDTH{1'b0}}; base_b_lat <= {ADDR_WIDTH{1'b0}}; tcnt_lat <= 16'd0; sel_lat <= 2'd0; end else begin ctrl_req <= 1'b0; valid <= 1'b0; case (state) S_IDLE: begin if (req) begin base_a_lat <= base_a; base_b_lat <= base_b; tcnt_lat <= tcnt; sel_lat <= tcnt[1:0]; state <= S_MEMWAIT; end end S_MEMWAIT: begin if (mem_grant) begin ctrl_addr <= base_a_lat + tile_offset; ctrl_wr <= 1'b0; ctrl_req <= 1'b1; state <= S_REQ_A; end end S_REQ_A: begin if (ctrl_ready) begin // explicit constant-offset case arms, not a // runtime-indexed part-select expression -- see // header (EXP-0084, extends EXP-0081's same // discipline from 1 to 2 select bits). case (sel_lat) 2'd0: data_a <= ctrl_rdata[0 +: DATA_WIDTH*P_IN]; 2'd1: data_a <= ctrl_rdata[64 +: DATA_WIDTH*P_IN]; 2'd2: data_a <= ctrl_rdata[128 +: DATA_WIDTH*P_IN]; 2'd3: data_a <= ctrl_rdata[192 +: DATA_WIDTH*P_IN]; endcase ctrl_addr <= base_b_lat + tile_offset; ctrl_wr <= 1'b0; state <= S_GAP; end end S_GAP: begin // the shared controller may still be finishing its // own internal completion sequence for lane A's // request for one more cycle after ctrl_ready // pulsed (mig_native_adapter.v's own S_DONE state // keeps `busy` asserted through it) -- wait for // !ctrl_busy before firing lane B's request, // instead of assuming back-to-back is safe. if (!ctrl_busy) begin ctrl_req <= 1'b1; state <= S_REQ_B; end end S_REQ_B: begin if (ctrl_ready) begin case (sel_lat) 2'd0: data_b <= ctrl_rdata[0 +: DATA_WIDTH*P_IN]; 2'd1: data_b <= ctrl_rdata[64 +: DATA_WIDTH*P_IN]; 2'd2: data_b <= ctrl_rdata[128 +: DATA_WIDTH*P_IN]; 2'd3: data_b <= ctrl_rdata[192 +: DATA_WIDTH*P_IN]; endcase valid <= 1'b1; state <= S_IDLE; end end default: state <= S_IDLE; endcase end end endmodule