perf(v2): word-level burst reads - 2.24-2.37x real wall-clock speedup (DEC-0015)

Implements optimization #1 from the final benchmark campaign's own
recommendation: exploit psram_controller.v's already-implemented
page-mode support (confirmed present by direct inspection) by
fetching multiple bytes per real backend transaction instead of one
at a time.

Root cause addressed: int8_memory_access.v (the byte-level backend
prefetch_engine.v originally sat on) already converts every 8-bit
logical request into a full 16-bit PSRAM word access internally
(mem_addr <= addr >> 1), discarding half of every word it already
paid for. prefetch_engine.v/memory_manager.v now speak
memory_interface.v's own 16-bit word protocol directly, bypassing
int8_memory_access.v entirely - which remains untouched, still frozen
V1 (§1/§34); V2 simply reuses the lower layer of the same frozen
chain instead of the byte-splitting layer on top of it, the same
"reuse what fits" precedent slot_mem_arbiter.v already set.
slot_mem_arbiter.v and neural_multiprocessor.v widened to match
(lb_n/ub_n added, master port wired directly to memory_interface.v).

Real, measured results: M4's own single-job testbench shows 49-56%
fewer cycles (166->84, 446->204, 728->322, all still bit-exact). The
full final-benchmark campaign (24/24 workload/config combinations)
re-verified bit-exact with D-Stress's real wall-clock time (cycles /
real POST-P&R Fmax) improving 2.24-2.37x across every N_SLOTS tested,
against a small real Fmax cost (unchanged at N=1, -6.2% at N=2, -1.2%
at N=4).

tb_neural_multiprocessor.v (M8) and tb_benchmark_suite.v (final
campaign) needed zero changes - both treat neural_multiprocessor.v as
a black box. Only tb_memory_manager.v (M4, rewired to skip
int8_memory_access.v) and tb_dataflow_core.v (M7, behavioral model
widened to word-level) needed updates.

The "real parallel scaling is flat beyond N_SLOTS=2" finding (DEC-0014)
still holds - this optimization made the shared PSRAM port more
efficient per transaction, not multi-ported - so N_SLOTS=2 remains
the recommended default.

Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0015)/
experiments (EXP-0015)/development.log.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-05 20:35:19 +02:00
co-authored by Claude Sonnet 5
parent 3cdaeaee35
commit e4a5540b6e
14 changed files with 489 additions and 168 deletions
+11 -6
View File
@@ -75,12 +75,16 @@ module dataflow_core #(
// ---- per-slot Memory Backend Interface (arrayed, one per slot --
// see file header on why arbitration to one shared PSRAM port is
// NOT done here) ----
// NOT done here). WORD-level (16-bit) post-M10 (decisions.log
// DEC-0015) -- see memory_manager.v/prefetch_engine.v's own
// headers for why. ----
output wire [N_SLOTS-1:0] slot_mem_req,
output wire [N_SLOTS-1:0] slot_mem_wr,
output wire [ADDR_WIDTH*N_SLOTS-1:0] slot_mem_addr,
output wire signed [8*N_SLOTS-1:0] slot_mem_wdata,
input wire signed [8*N_SLOTS-1:0] slot_mem_rdata,
output wire [ADDR_WIDTH*N_SLOTS-1:0] slot_mem_addr, // WORD address
output wire [16*N_SLOTS-1:0] slot_mem_wdata,
output wire [N_SLOTS-1:0] slot_mem_lb_n,
output wire [N_SLOTS-1:0] slot_mem_ub_n,
input wire [16*N_SLOTS-1:0] slot_mem_rdata,
input wire [N_SLOTS-1:0] slot_mem_ready
);
@@ -175,8 +179,9 @@ module dataflow_core #(
.result_valid(mm_result_valid), .result_ready(mm_result_ready), .result_data(mm_result_data),
.mem_req(slot_mem_req[g]), .mem_wr(slot_mem_wr[g]),
.mem_addr(slot_mem_addr[g*ADDR_WIDTH +: ADDR_WIDTH]),
.mem_wdata(slot_mem_wdata[g*8 +: 8]),
.mem_rdata(slot_mem_rdata[g*8 +: 8]), .mem_ready(slot_mem_ready[g])
.mem_wdata(slot_mem_wdata[g*16 +: 16]),
.mem_lb_n(slot_mem_lb_n[g]), .mem_ub_n(slot_mem_ub_n[g]),
.mem_rdata(slot_mem_rdata[g*16 +: 16]), .mem_ready(slot_mem_ready[g])
);
reg job_valid_np;
+46 -13
View File
@@ -3,13 +3,24 @@
// ================================================================
// FPGA-Neural V2 -- Memory Manager (M4, docs/v2-description.md §12/§15)
//
// Sits between a single Neural Processor (M1) and the byte-level
// Memory Backend Interface (hardware/v1/rtl/int8_memory_access.v,
// Sits between a single Neural Processor (M1) and the WORD-level
// Memory Backend Interface (hardware/v1/rtl/memory_interface.v,
// reused UNMODIFIED, per §15 -- "NON iniziare modificando il
// controller PSRAM. Mantenere inizialmente il backend esistente").
// The processor sees only "data available" (operand_valid/ready,
// tile_last) -- never PSRAM request/wait cycles directly (§12).
//
// Post-M10 (decisions.log DEC-0015): this port talks directly to
// memory_interface.v's own 16-bit word interface instead of routing
// through int8_memory_access.v's byte-splitting layer -- every real
// transaction now moves a full PSRAM word (2 bytes) instead of
// discarding half of one, halving the real transaction count for
// prefetch_engine's own reads. int8_memory_access.v itself is
// untouched (still frozen V1); V2 simply no longer instantiates it in
// this datapath, reusing the lower (word-level) layer directly
// instead, the same "reuse what fits" precedent slot_mem_arbiter.v
// already set for hardware/v1/rtl/mem_arbiter.v.
//
// Double-buffered prefetch (§13): while the processor consumes tile
// N from bank "current", this module retargets the single
// prefetch_engine instance (M4) at bank "next" to fetch tile N+1
@@ -67,12 +78,19 @@ module memory_manager #(
output reg result_ready,
input wire signed [DATA_WIDTH-1:0] result_data,
// ---- Memory Backend Interface (matches int8_memory_access.v) ----
// ---- Memory Backend Interface (word-level, matches
// hardware/v1/rtl/memory_interface.v's contract exactly -- see
// prefetch_engine.v's own header and decisions.log DEC-0015 for
// why this is now word- rather than byte-level: int8_memory_access.v
// is no longer in the datapath, each transaction moves a full
// 16-bit PSRAM word instead of discarding half of it) ----
output wire mem_req,
output wire mem_wr,
output wire [ADDR_WIDTH-1:0] mem_addr,
output wire signed [7:0] mem_wdata,
input wire signed [7:0] mem_rdata,
output wire [ADDR_WIDTH-1:0] mem_addr, // WORD address
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
);
@@ -122,10 +140,11 @@ module memory_manager #(
// (never both at once, by construction -- see file header)
// selects which one actually reaches the real output port,
// avoiding a two-driver conflict on mem_req/mem_wr/mem_addr/
// mem_wdata.
// mem_wdata/mem_lb_n/mem_ub_n.
wire pf_mem_req, pf_mem_wr;
wire [ADDR_WIDTH-1:0] pf_mem_addr;
wire signed [7:0] pf_mem_wdata;
wire [15:0] pf_mem_wdata;
wire pf_mem_lb_n, pf_mem_ub_n;
prefetch_engine #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH)
@@ -135,12 +154,14 @@ module memory_manager #(
.fetch_busy(pf_busy), .fetch_done(pf_done),
.tile_x(pf_tile_x), .tile_w(pf_tile_w),
.mem_req(pf_mem_req), .mem_wr(pf_mem_wr), .mem_addr(pf_mem_addr), .mem_wdata(pf_mem_wdata),
.mem_lb_n(pf_mem_lb_n), .mem_ub_n(pf_mem_ub_n),
.mem_rdata(mem_rdata), .mem_ready(mem_ready)
);
reg wr_mem_req;
reg [ADDR_WIDTH-1:0] wr_mem_addr;
reg signed [7:0] wr_mem_wdata;
reg [ADDR_WIDTH-1:0] wr_mem_addr; // WORD address
reg [15:0] wr_mem_wdata;
reg wr_mem_lb_n, wr_mem_ub_n;
// wr_mem_req is SET while state==MM_WRITE_RESULT but only becomes
// valid (via NBA) the FOLLOWING cycle, i.e. while state==MM_DONE --
@@ -153,6 +174,8 @@ module memory_manager #(
assign mem_wr = wr_active ? 1'b1 : pf_mem_wr;
assign mem_addr = wr_active ? wr_mem_addr : pf_mem_addr;
assign mem_wdata = wr_active ? wr_mem_wdata : pf_mem_wdata;
assign mem_lb_n = wr_active ? wr_mem_lb_n : pf_mem_lb_n;
assign mem_ub_n = wr_active ? wr_mem_ub_n : pf_mem_ub_n;
always @(posedge clk) begin
if (rst) begin
@@ -169,7 +192,9 @@ module memory_manager #(
tile_idx <= 16'h0;
wr_mem_req <= 1'b0;
wr_mem_addr <= {ADDR_WIDTH{1'b0}};
wr_mem_wdata <= 8'sd0;
wr_mem_wdata <= 16'h0000;
wr_mem_lb_n <= 1'b1;
wr_mem_ub_n <= 1'b1;
pf_pending <= 1'b0;
end else begin
job_done <= 1'b0;
@@ -282,7 +307,15 @@ module memory_manager #(
MM_WAIT_RESULT: begin
result_ready <= 1'b1;
if (result_valid && result_ready) begin
wr_mem_wdata <= result_data;
// Replicate int8_memory_access.v's own byte-
// select convention exactly (addr[0]==0 -> low
// byte, addr[0]==1 -> high byte) since that
// module is no longer in the datapath -- see
// prefetch_engine.v's header/decisions.log
// DEC-0015.
wr_mem_wdata <= result_addr_reg[0] ? {result_data, 8'h00} : {8'h00, result_data};
wr_mem_lb_n <= result_addr_reg[0] ? 1'b1 : 1'b0;
wr_mem_ub_n <= result_addr_reg[0] ? 1'b0 : 1'b1;
state <= MM_WRITE_RESULT;
end
end
@@ -292,7 +325,7 @@ module memory_manager #(
// tiles to fetch for this job), so driving the shared
// backend port directly is safe -- see file header.
wr_mem_req <= 1'b1;
wr_mem_addr <= result_addr_reg;
wr_mem_addr <= result_addr_reg[ADDR_WIDTH-1:1]; // byte -> word
state <= MM_DONE;
end
+41 -37
View File
@@ -8,17 +8,29 @@
// The real, hardware-facing top-level: dataflow_core.v (M7) with its
// N_SLOTS independent Memory Backend Interface ports funneled through
// a new generic arbiter (slot_mem_arbiter.v, M8) down to the REAL,
// UNMODIFIED hardware/v1 PSRAM backend chain --
// int8_memory_access -> memory_interface -> psram_controller
// -- exactly the chain hardware/v2/sim/tb_memory_manager.v (M4)
// already proved correct for ONE memory_manager port. This module is
// the first point M3 (per DEC-0009) and M2 (per DEC-0006) BOTH
// deferred to: N_SLOTS memory_manager instances genuinely sharing one
// physical PSRAM port.
// UNMODIFIED hardware/v1 PSRAM backend chain -- exactly the chain
// hardware/v2/sim/tb_memory_manager.v (M4) already proved correct for
// ONE memory_manager port. This module is the first point M3 (per
// DEC-0009) and M2 (per DEC-0006) BOTH deferred to: N_SLOTS
// memory_manager instances genuinely sharing one physical PSRAM port.
//
// dataflow_core.v itself is NOT modified -- its per-slot interface
// (DEC-0009) is exactly what makes it pluggable into an arbiter here
// without touching M7's own file.
// Post-M10 (decisions.log DEC-0015): the chain is now
// memory_interface -> psram_controller
// -- int8_memory_access.v is no longer instantiated here.
// int8_memory_access itself is untouched (still frozen V1, §1/§34);
// V2 simply reuses the lower (word-level) layer of the same frozen
// chain directly, since prefetch_engine.v/memory_manager.v now speak
// memory_interface's own 16-bit word protocol natively (see those
// modules' headers for why: every real transaction now moves a full
// PSRAM word instead of discarding half of it, halving the number of
// real backend round-trips per tile fetch).
//
// dataflow_core.v itself is NOT modified in its own control logic --
// its per-slot interface (DEC-0009) is exactly what makes it pluggable
// into an arbiter here without touching M7's own file (only the
// WIDTH of that per-slot interface changed, from 8 to 16 bits plus
// lb_n/ub_n, a mechanical consequence of DEC-0015, not a redesign of
// dataflow_core's own scheduling/dependency logic).
// ================================================================
module neural_multiprocessor #(
@@ -59,10 +71,12 @@ module neural_multiprocessor #(
output wire psram_zz_n
);
// ---- dataflow_core (M7, unmodified) ----
// ---- dataflow_core (M7, control logic unmodified; per-slot
// backend port widened to 16-bit + lb_n/ub_n per DEC-0015) ----
wire [N_SLOTS-1:0] slot_mem_req, slot_mem_wr;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_mem_addr;
wire signed [8*N_SLOTS-1:0] slot_mem_wdata, slot_mem_rdata;
wire [16*N_SLOTS-1:0] slot_mem_wdata, slot_mem_rdata;
wire [N_SLOTS-1:0] slot_mem_lb_n, slot_mem_ub_n;
wire [N_SLOTS-1:0] slot_mem_ready;
dataflow_core #(
@@ -75,14 +89,16 @@ module neural_multiprocessor #(
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
.reg_result_addr(reg_result_addr),
.slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr),
.slot_mem_wdata(slot_mem_wdata), .slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready)
.slot_mem_wdata(slot_mem_wdata), .slot_mem_lb_n(slot_mem_lb_n), .slot_mem_ub_n(slot_mem_ub_n),
.slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready)
);
// ---- N_SLOTS -> 1 arbiter (M8, new) ----
// ---- N_SLOTS -> 1 arbiter (M8, word-level per DEC-0015) ----
wire arb_m_req, arb_m_wr;
wire [ADDR_WIDTH-1:0] arb_m_addr;
wire signed [7:0] arb_m_wdata;
wire signed [7:0] arb_m_rdata;
wire [15:0] arb_m_wdata;
wire arb_m_lb_n, arb_m_ub_n;
wire [15:0] arb_m_rdata;
wire arb_m_ready;
slot_mem_arbiter #(
@@ -90,28 +106,16 @@ module neural_multiprocessor #(
) u_arbiter (
.clk(clk), .rst(rst),
.s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr),
.s_wdata(slot_mem_wdata), .s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
.s_wdata(slot_mem_wdata), .s_lb_n(slot_mem_lb_n), .s_ub_n(slot_mem_ub_n),
.s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready),
.m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata),
.m_lb_n(arb_m_lb_n), .m_ub_n(arb_m_ub_n),
.m_rdata(arb_m_rdata), .m_ready(arb_m_ready)
);
// ---- real, unmodified V1 PSRAM backend chain ----
wire if_mem_req, if_mem_wr;
wire [ADDR_WIDTH-1:0] if_mem_addr;
wire [PSRAM_DATA_WIDTH-1:0] if_mem_wdata;
wire if_mem_lb_n, if_mem_ub_n;
wire [PSRAM_DATA_WIDTH-1:0] if_mem_rdata;
wire if_mem_ready;
int8_memory_access #(.ADDR_WIDTH(ADDR_WIDTH)) u_int8 (
.clk(clk), .rst(rst),
.req(arb_m_req), .wr(arb_m_wr), .addr(arb_m_addr), .wdata(arb_m_wdata),
.rdata(arb_m_rdata), .ready(arb_m_ready),
.mem_req(if_mem_req), .mem_wr(if_mem_wr), .mem_addr(if_mem_addr), .mem_wdata(if_mem_wdata),
.mem_lb_n(if_mem_lb_n), .mem_ub_n(if_mem_ub_n),
.mem_rdata(if_mem_rdata), .mem_ready(if_mem_ready)
);
// ---- real, unmodified V1 PSRAM backend chain (memory_interface
// -> psram_controller; int8_memory_access no longer in this
// datapath -- see file header, DEC-0015) ----
wire pc_mem_req, pc_mem_wr;
wire [ADDR_WIDTH-1:0] pc_mem_addr;
wire [PSRAM_DATA_WIDTH-1:0] pc_mem_wdata;
@@ -121,9 +125,9 @@ module neural_multiprocessor #(
memory_interface #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH)) u_memif (
.clk(clk), .rst(rst),
.req(if_mem_req), .wr(if_mem_wr), .addr(if_mem_addr), .wdata(if_mem_wdata),
.lb_n(if_mem_lb_n), .ub_n(if_mem_ub_n),
.rdata(if_mem_rdata), .ready(if_mem_ready),
.req(arb_m_req), .wr(arb_m_wr), .addr(arb_m_addr), .wdata(arb_m_wdata),
.lb_n(arb_m_lb_n), .ub_n(arb_m_ub_n),
.rdata(arb_m_rdata), .ready(arb_m_ready),
.mem_req(pc_mem_req), .mem_wr(pc_mem_wr), .mem_addr(pc_mem_addr), .mem_wdata(pc_mem_wdata),
.mem_lb_n(pc_mem_lb_n), .mem_ub_n(pc_mem_ub_n),
.mem_rdata(pc_mem_rdata), .mem_ready(pc_mem_ready)
+73 -36
View File
@@ -1,26 +1,43 @@
`timescale 1ns/1ps
// ================================================================
// FPGA-Neural V2 -- Prefetch Engine (M4, docs/v2-description.md §13)
// FPGA-Neural V2 -- Prefetch Engine (M4, docs/v2-description.md §13;
// word-level burst rewrite post-M10 -- see hardware/v2/logs/
// decisions.log DEC-0015)
//
// Fetches ONE tile (P_IN activation bytes + P_IN weight bytes) from
// the byte-level Memory Backend Interface into a pair of output
// registers, sequentially (2*P_IN single-byte transactions -- the
// same byte-at-a-time convention hardware/v1/rtl/neuron_memory.v
// already uses against the same backend, reused unmodified here).
// the WORD-level Memory Backend Interface, P_IN/2 sixteen-bit
// transactions per array instead of P_IN single-byte ones.
//
// This module fetches exactly one tile per fetch_start pulse; the
// double-buffering strategy itself (§13: compute tile N while
// prefetching tile N+1, swap, repeat) is memory_manager.v's
// responsibility -- it retargets this single engine at whichever
// bank currently needs refilling, so no internal arbitration between
// multiple fetch engines sharing the backend port is ever needed.
// WHY: hardware/v1/rtl/int8_memory_access.v (the byte-level backend
// this engine originally sat on) converts every 8-bit logical request
// into a FULL 16-bit PSRAM word access internally (mem_addr <= addr
// >> 1, one byte lane selected via lb_n/ub_n) -- so a byte-at-a-time
// fetch was ALREADY paying for two bytes of real PSRAM bandwidth per
// transaction while only using one. This engine now talks directly to
// hardware/v1/rtl/memory_interface.v's own 16-bit word interface
// (skipping int8_memory_access.v entirely -- both are frozen V1 files,
// unmodified either way, §1/§34; V2 is simply choosing to reuse the
// lower layer instead of the byte-splitting one on top of it, the
// same "reuse what fits" precedent already set by slot_mem_arbiter.v
// not reusing hardware/v1/rtl/mem_arbiter.v verbatim). psram_controller.v's
// own real page-mode support (already implemented, unmodified) then
// serves consecutive same-page word reads faster than a cold access --
// this engine's job is simply to stop discarding half of every word it
// already paid for, and to halve the number of real backend
// round-trips needed per tile.
//
// The backend port (mem_req/mem_wr/mem_addr/mem_wdata/mem_rdata/
// mem_ready) matches hardware/v1/rtl/int8_memory_access.v's contract
// exactly -- this engine can sit directly on top of that unmodified
// V1 module (which itself sits on memory_interface.v ->
// psram_controller.v, also unmodified, per §15).
// CONSTRAINT: P_IN must be even, and x_addr/w_addr must be word-
// aligned (even BYTE addresses) -- each 16-bit transaction covers
// BYTE addresses {addr, addr+1} as {low byte, high byte} (matches
// int8_memory_access.v's own addr[0] convention exactly, replicated
// here since that module is no longer in the datapath). A host/loader
// placing X/W tile arrays at even byte offsets (already true of every
// address used in this project's own testbenches) satisfies this
// with no special handling.
//
// The double-buffering strategy itself (§13) remains memory_manager.v's
// responsibility -- unchanged by this rewrite.
// ================================================================
module prefetch_engine #(
@@ -32,18 +49,22 @@ module prefetch_engine #(
input wire rst,
input wire fetch_start,
input wire [ADDR_WIDTH-1:0] x_addr, // base addr of this tile's P_IN X bytes
input wire [ADDR_WIDTH-1:0] w_addr, // base addr of this tile's P_IN W bytes
input wire [ADDR_WIDTH-1:0] x_addr, // BYTE address, word-aligned
input wire [ADDR_WIDTH-1:0] w_addr, // BYTE address, word-aligned
output reg fetch_busy,
output reg fetch_done, // one-cycle pulse
output reg signed [DATA_WIDTH*P_IN-1:0] tile_x,
output reg signed [DATA_WIDTH*P_IN-1:0] tile_w,
// ---- word-level Memory Backend Interface (matches
// hardware/v1/rtl/memory_interface.v's contract exactly) ----
output reg mem_req,
output reg mem_wr,
output reg [ADDR_WIDTH-1:0] mem_addr,
output reg signed [7:0] mem_wdata,
input wire signed [7:0] mem_rdata,
output reg [ADDR_WIDTH-1:0] mem_addr, // WORD address
output reg [15:0] mem_wdata,
output reg mem_lb_n,
output reg mem_ub_n,
input wire [15:0] mem_rdata,
input wire mem_ready
);
@@ -52,19 +73,27 @@ module prefetch_engine #(
localparam ST_READ_W = 2'd2;
localparam ST_DONE = 2'd3;
localparam WORDS_PER_TILE = P_IN/2;
localparam WIW = $clog2(WORDS_PER_TILE+1);
reg [1:0] state;
reg [$clog2(P_IN+1)-1:0] byte_idx;
reg [WIW-1:0] word_idx;
wire [ADDR_WIDTH-1:0] x_word_base = x_addr[ADDR_WIDTH-1:1];
wire [ADDR_WIDTH-1:0] w_word_base = w_addr[ADDR_WIDTH-1:1];
always @(posedge clk) begin
if (rst) begin
state <= ST_IDLE;
byte_idx <= 0;
word_idx <= 0;
fetch_busy <= 1'b0;
fetch_done <= 1'b0;
mem_req <= 1'b0;
mem_wr <= 1'b0;
mem_addr <= {ADDR_WIDTH{1'b0}};
mem_wdata <= 8'sd0;
mem_wdata <= 16'h0000;
mem_lb_n <= 1'b1;
mem_ub_n <= 1'b1;
end else begin
mem_req <= 1'b0;
fetch_done <= 1'b0;
@@ -74,42 +103,50 @@ module prefetch_engine #(
ST_IDLE: begin
if (fetch_start) begin
fetch_busy <= 1'b1;
byte_idx <= 0;
word_idx <= 0;
mem_req <= 1'b1;
mem_wr <= 1'b0;
mem_addr <= x_addr;
mem_addr <= x_word_base;
mem_lb_n <= 1'b0; // both byte lanes -- fetch the whole word
mem_ub_n <= 1'b0;
state <= ST_READ_X;
end
end
ST_READ_X: begin
if (mem_ready) begin
tile_x[byte_idx*DATA_WIDTH +: DATA_WIDTH] <= mem_rdata;
if (byte_idx == P_IN[$clog2(P_IN+1)-1:0] - 1'b1) begin
byte_idx <= 0;
tile_x[word_idx*16 +: 16] <= mem_rdata;
if (word_idx == WORDS_PER_TILE[WIW-1:0] - 1'b1) begin
word_idx <= 0;
mem_req <= 1'b1;
mem_wr <= 1'b0;
mem_addr <= w_addr;
mem_addr <= w_word_base;
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b0;
state <= ST_READ_W;
end else begin
byte_idx <= byte_idx + 1'b1;
word_idx <= word_idx + 1'b1;
mem_req <= 1'b1;
mem_wr <= 1'b0;
mem_addr <= x_addr + byte_idx + 1'b1;
mem_addr <= x_word_base + word_idx + 1'b1;
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b0;
end
end
end
ST_READ_W: begin
if (mem_ready) begin
tile_w[byte_idx*DATA_WIDTH +: DATA_WIDTH] <= mem_rdata;
if (byte_idx == P_IN[$clog2(P_IN+1)-1:0] - 1'b1) begin
tile_w[word_idx*16 +: 16] <= mem_rdata;
if (word_idx == WORDS_PER_TILE[WIW-1:0] - 1'b1) begin
state <= ST_DONE;
end else begin
byte_idx <= byte_idx + 1'b1;
word_idx <= word_idx + 1'b1;
mem_req <= 1'b1;
mem_wr <= 1'b0;
mem_addr <= w_addr + byte_idx + 1'b1;
mem_addr <= w_word_base + word_idx + 1'b1;
mem_lb_n <= 1'b0;
mem_ub_n <= 1'b0;
end
end
end
+51 -33
View File
@@ -9,13 +9,18 @@
// funneling N_SLOTS independent memory_manager backend ports down to
// the ONE physical PSRAM port a real chip actually has.
//
// WORD-level (16-bit, + lb_n/ub_n) post-M10 (decisions.log DEC-0015):
// arbitrates hardware/v1/rtl/memory_interface.v's own port shape
// directly (int8_memory_access.v is no longer in this datapath -- see
// memory_manager.v/prefetch_engine.v's own headers for why).
//
// Inspired by (NOT copied from -- see hardware/v2/logs/decisions.log
// DEC-0006's own note) hardware/v1/rtl/mem_arbiter.v: same
// single-owner-until-ready-pulse discipline (a port, once granted,
// holds the shared master port until ITS OWN transaction's m_ready
// pulse, then releases -- no queuing/pipelining needed, since every
// requester already issues a clean one-cycle req pulse matching
// int8_memory_access's own contract). Generalized from V1's fixed
// memory_interface's own contract). Generalized from V1's fixed
// 4 named ports (A/B/C/D) to a parametric N_PORTS array, since
// dataflow_core.v's N_SLOTS is itself a parameter.
//
@@ -30,21 +35,20 @@
// IMPORTANT (found via real concurrent-slot simulation, see
// hardware/v2/logs/errors.log ERR-0008): each port's own s_req is a
// FIRE-AND-FORGET single-cycle pulse (prefetch_engine.v/
// memory_manager.v's own byte-level backend protocol -- M4 verified
// it only against a DIRECT 1:1 connection to int8_memory_access,
// which is always free to accept it since there is exactly one
// requester). A naive "grant only while req is live" arbiter silently
// DROPS a pulse that arrives while the shared bus is owned by another
// port, hanging that slot's prefetch/writeback forever. Every
// incoming s_req is therefore LATCHED into a per-port `pending`
// register (capturing wr/addr/wdata the same cycle) regardless of
// arbiter state -- the same single-entry "queue, don't drop the
// request" idiom already used by memory_manager's own pf_pending
// register (ERR-0006 fix #1). Grants are drawn from `pending`, never
// from a live s_req directly, which adds a uniform minimum 1-cycle
// latency to every byte transaction (a real, measured cost of sharing
// one PSRAM port -- see timing.log/benchmark.log EXP-0009) but never
// drops a request.
// memory_manager.v's own backend protocol -- M4 verified it only
// against a DIRECT 1:1 connection to the backend, which is always
// free to accept it since there is exactly one requester). A naive
// "grant only while req is live" arbiter silently DROPS a pulse that
// arrives while the shared bus is owned by another port, hanging that
// slot's prefetch/writeback forever. Every incoming s_req is therefore
// LATCHED into a per-port `pending` register (capturing wr/addr/wdata/
// lb_n/ub_n the same cycle) regardless of arbiter state -- the same
// single-entry "queue, don't drop the request" idiom already used by
// memory_manager's own pf_pending register (ERR-0006 fix #1). Grants
// are drawn from `pending`, never from a live s_req directly, which
// adds a uniform minimum 1-cycle latency to every transaction (a real,
// measured cost of sharing one PSRAM port -- see timing.log/
// benchmark.log EXP-0009) but never drops a request.
// ================================================================
module slot_mem_arbiter #(
@@ -57,17 +61,21 @@ module slot_mem_arbiter #(
// ---- N_PORTS requester side (one per dataflow_core slot) ----
input wire [N_PORTS-1:0] s_req,
input wire [N_PORTS-1:0] s_wr,
input wire [ADDR_WIDTH*N_PORTS-1:0] s_addr,
input wire signed [8*N_PORTS-1:0] s_wdata,
output reg signed [8*N_PORTS-1:0] s_rdata,
input wire [ADDR_WIDTH*N_PORTS-1:0] s_addr, // WORD address
input wire [16*N_PORTS-1:0] s_wdata,
input wire [N_PORTS-1:0] s_lb_n,
input wire [N_PORTS-1:0] s_ub_n,
output reg [16*N_PORTS-1:0] s_rdata,
output reg [N_PORTS-1:0] s_ready,
// ---- single shared master port (-> int8_memory_access) ----
// ---- single shared master port (-> memory_interface.v) ----
output reg m_req,
output reg m_wr,
output reg [ADDR_WIDTH-1:0] m_addr,
output reg signed [7:0] m_wdata,
input wire signed [7:0] m_rdata,
output reg [15:0] m_wdata,
output reg m_lb_n,
output reg m_ub_n,
input wire [15:0] m_rdata,
input wire m_ready
);
@@ -81,8 +89,10 @@ module slot_mem_arbiter #(
// is never silently dropped while the bus is owned by another port.
reg [N_PORTS-1:0] pending;
reg [ADDR_WIDTH*N_PORTS-1:0] pending_addr;
reg signed [8*N_PORTS-1:0] pending_wdata;
reg [16*N_PORTS-1:0] pending_wdata;
reg [N_PORTS-1:0] pending_wr;
reg [N_PORTS-1:0] pending_lb_n;
reg [N_PORTS-1:0] pending_ub_n;
// Fixed lowest-index-wins priority scan over PENDING requests (not
// raw s_req -- see file header).
@@ -107,13 +117,17 @@ module slot_mem_arbiter #(
owner <= OWNER_NONE;
pending <= {N_PORTS{1'b0}};
pending_addr <= {(ADDR_WIDTH*N_PORTS){1'b0}};
pending_wdata <= {(8*N_PORTS){1'b0}};
pending_wdata <= {(16*N_PORTS){1'b0}};
pending_wr <= {N_PORTS{1'b0}};
pending_lb_n <= {N_PORTS{1'b1}};
pending_ub_n <= {N_PORTS{1'b1}};
m_req <= 1'b0;
m_wr <= 1'b0;
m_addr <= {ADDR_WIDTH{1'b0}};
m_wdata <= 8'sd0;
s_rdata <= {(8*N_PORTS){1'b0}};
m_wdata <= 16'h0000;
m_lb_n <= 1'b1;
m_ub_n <= 1'b1;
s_rdata <= {(16*N_PORTS){1'b0}};
s_ready <= {N_PORTS{1'b0}};
end else begin
m_req <= 1'b0;
@@ -129,10 +143,12 @@ module slot_mem_arbiter #(
// the cycle it is granted.
for (pi = 0; pi < N_PORTS; pi = pi + 1) begin
if (s_req[pi]) begin
pending[pi] <= 1'b1;
pending_wr[pi] <= s_wr[pi];
pending_addr[pi*ADDR_WIDTH +: ADDR_WIDTH] <= s_addr[pi*ADDR_WIDTH +: ADDR_WIDTH];
pending_wdata[pi*8 +: 8] <= s_wdata[pi*8 +: 8];
pending[pi] <= 1'b1;
pending_wr[pi] <= s_wr[pi];
pending_lb_n[pi] <= s_lb_n[pi];
pending_ub_n[pi] <= s_ub_n[pi];
pending_addr[pi*ADDR_WIDTH +: ADDR_WIDTH] <= s_addr[pi*ADDR_WIDTH +: ADDR_WIDTH];
pending_wdata[pi*16 +: 16] <= s_wdata[pi*16 +: 16];
end
end
@@ -141,8 +157,10 @@ module slot_mem_arbiter #(
owner <= grant_idx + 1'b1;
m_req <= 1'b1;
m_wr <= pending_wr[grant_idx];
m_lb_n <= pending_lb_n[grant_idx];
m_ub_n <= pending_ub_n[grant_idx];
m_addr <= pending_addr[grant_idx*ADDR_WIDTH +: ADDR_WIDTH];
m_wdata <= pending_wdata[grant_idx*8 +: 8];
m_wdata <= pending_wdata[grant_idx*16 +: 16];
pending[grant_idx] <= 1'b0;
end
end else begin
@@ -153,8 +171,8 @@ module slot_mem_arbiter #(
// of bug already hit/fixed at ERR-0006/M2/M6).
for (pi = 0; pi < N_PORTS; pi = pi + 1) begin
if (owner == pi[PIDXW-1:0] + 1'b1) begin
s_rdata[pi*8 +: 8] <= m_rdata;
s_ready[pi] <= 1'b1;
s_rdata[pi*16 +: 16] <= m_rdata;
s_ready[pi] <= 1'b1;
end
end
owner <= OWNER_NONE;