Implements optimization #2 from the final benchmark campaign's own recommendation, on top of DEC-0015's word-level burst rewrite: a new shared activation_cache.v module fetches a given activation (X) vector from PSRAM once instead of once per neuron sharing it - the exact redundant traffic pattern the dense-layer workloads in this project's benchmark suite exhibit. Each memory_manager's own prefetch_engine now fetches WEIGHTS only; the activation half is requested from the shared cache instead (single-tag, tile-granular, N_SLOTS request ports, its own real word-level PSRAM backend via a new dedicated arbiter port). dataflow_core.v/slot_mem_arbiter.v/neural_multiprocessor.v widened to N_SLOTS+1 ports to arbitrate the cache's traffic alongside each slot's weight traffic. Two real bugs found and fixed during implementation (ERR-0010): a target-bank/pending-bank race in memory_manager.v's activation-cache wiring (the same bug class ERR-0006 already fixed once for pf_target_bank - a later handoff's queued request can overwrite which bank an earlier, still-in-flight request's ack applies to), and a repeat of ERR-0009's N_SLOTS=1 zero-width replication bug in activation_cache.v itself. Real, measured results: the full final-benchmark campaign (24/24 workload/config combinations) re-verified bit-exact. D-Stress cycles fall a further 1.66-2.00x on top of DEC-0015 (~4x combined vs the original byte-level baseline). But the cache's real Fmax cost is much steeper than DEC-0015's own: N_SLOTS=2 (the recommended default, DEC-0014) drops from 133.58 to 87.72 MHz (-34%, margin over 80MHz shrinks from +67% to +9.7%), and N_SLOTS=4 drops to 65.01 MHz - now FAILING the 80MHz target it previously passed. Combined real wall-clock speedup vs the original baseline: N=1 3.86x, N=2 2.45x (both real net wins); N=4 is a real regression once its own now-failing Fmax is honestly used, though N=4 was never the recommended configuration. N_SLOTS=2 remains the recommended default (DEC-0014 unaffected) with a thinner but still real Fmax margin. Cache hit-detection pipelining is flagged as concrete follow-up work if N_SLOTS>2 is ever needed with the cache active - not attempted this round. Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0016)/ experiments (EXP-0016)/errors (ERR-0010)/development.log, ROADMAP.md updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
263 lines
13 KiB
Verilog
263 lines
13 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// FPGA-Neural V2 -- Dataflow Core (M7, docs/v2-description.md §17)
|
|
//
|
|
// First full integration: Dependency Manager (M6) -> Neural Director
|
|
// (M5) -> N_SLOTS x (Memory Manager (M4) + Neural Processor (M1)).
|
|
//
|
|
// JOBS (node registration)
|
|
// |
|
|
// +-----------------+
|
|
// | Dependency |
|
|
// | Manager (M6) |
|
|
// +--------+--------+
|
|
// | ready_valid/ready (a node whose deps resolved)
|
|
// +--------v--------+
|
|
// | Neural Director |
|
|
// | (M5) |
|
|
// +--------+--------+
|
|
// | slot_job_start/x_base/w_base/n_tiles/result_addr
|
|
// +----------+----------+
|
|
// v v v
|
|
// Memory Memory Memory (one per slot, M4)
|
|
// Manager Manager Manager
|
|
// | | |
|
|
// Neural Neural Neural (one per slot, M1)
|
|
// Processor Processor Processor
|
|
//
|
|
// A slot's job_done feeds back to the Director (frees the slot) AND,
|
|
// via the node_id the Director itself tracked for that slot
|
|
// (slot_node_id), becomes a producer_done event fed to the
|
|
// Dependency Manager -- closing the loop: a node's completion can now
|
|
// wake up every OTHER node that depended on it, without any external
|
|
// component gluing the two together.
|
|
//
|
|
// Scope (see hardware/v2/logs/decisions.log DEC-0009):
|
|
// - activation_buffer.v/weight_buffer.v/result_buffer.v (M3, BRAM-
|
|
// backed FIFOs) are NOT instantiated here -- superseded by a
|
|
// different, measurement-driven shared cache (activation_cache.v,
|
|
// post-M10 DEC-0016, see below), not the original M3 modules
|
|
// themselves.
|
|
// - each slot's Memory Backend Interface port is exposed SEPARATELY
|
|
// (N_SLOTS independent ports) rather than arbitrated down to one
|
|
// shared PSRAM master -- real PSRAM integration (including whatever
|
|
// arbitration N_SLOTS>1 requires) is done one level up, in
|
|
// neural_multiprocessor.v (M8).
|
|
//
|
|
// Post-M10 (decisions.log DEC-0016): a single shared activation_cache
|
|
// instance sits alongside the N_SLOTS memory_managers, serving the
|
|
// ACTIVATION (X) half of each tile fetch -- in the realistic dense-
|
|
// layer workloads this project benchmarks, many neurons share the
|
|
// exact same X vector, and fetching it from PSRAM once instead of
|
|
// once per memory_manager instance is real, measured, redundant-
|
|
// traffic elimination (see hardware/v2/docs/benchmarks/
|
|
// final-benchmark.md's own recommendation #2). Each memory_manager's
|
|
// own prefetch_engine now fetches WEIGHTS only. The exposed
|
|
// slot_mem_* arrays are sized N_SLOTS+1: indices [0, N_SLOTS) are the
|
|
// per-slot memory_managers' own weight+write-back backend ports
|
|
// (unchanged in spirit from before), index [N_SLOTS] is the shared
|
|
// activation_cache's own backend port -- all N_SLOTS+1 arbitrated
|
|
// together by neural_multiprocessor.v's slot_mem_arbiter.v (N_PORTS
|
|
// widened to N_SLOTS+1 there to match).
|
|
// ================================================================
|
|
|
|
module dataflow_core #(
|
|
parameter DATA_WIDTH = 8,
|
|
parameter P_IN = 8,
|
|
parameter ACC_WIDTH = 32,
|
|
parameter ADDR_WIDTH = 23,
|
|
parameter N_SLOTS = 4,
|
|
parameter N_NODES = 16,
|
|
parameter MAX_DEPS = 4,
|
|
parameter QUEUE_DEPTH = 8
|
|
)(
|
|
input wire clk,
|
|
input wire rst,
|
|
|
|
// ---- node registration (host / graph loader -> Dependency Manager) ----
|
|
input wire reg_valid,
|
|
output wire reg_ready,
|
|
input wire [$clog2(N_NODES)-1:0] reg_node_id,
|
|
input wire [$clog2(MAX_DEPS+1)-1:0] reg_required,
|
|
input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids,
|
|
input wire [ADDR_WIDTH-1:0] reg_x_base,
|
|
input wire [ADDR_WIDTH-1:0] reg_w_base,
|
|
input wire [15:0] reg_n_tiles,
|
|
input wire [ADDR_WIDTH-1:0] reg_result_addr,
|
|
|
|
// ---- Memory Backend Interface, arrayed N_SLOTS+1 wide (see file
|
|
// header: indices [0,N_SLOTS) are the per-slot memory_managers'
|
|
// own weight+write-back ports, index [N_SLOTS] is the shared
|
|
// activation_cache's own port). 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:0] slot_mem_req,
|
|
output wire [N_SLOTS:0] slot_mem_wr,
|
|
output wire [ADDR_WIDTH*(N_SLOTS+1)-1:0] slot_mem_addr, // WORD address
|
|
output wire [16*(N_SLOTS+1)-1:0] slot_mem_wdata,
|
|
output wire [N_SLOTS:0] slot_mem_lb_n,
|
|
output wire [N_SLOTS:0] slot_mem_ub_n,
|
|
input wire [16*(N_SLOTS+1)-1:0] slot_mem_rdata,
|
|
input wire [N_SLOTS:0] slot_mem_ready
|
|
);
|
|
|
|
localparam NODE_IDW = $clog2(N_NODES);
|
|
|
|
// ---- Dependency Manager (M6) ----
|
|
wire dm_ready_valid;
|
|
wire dm_ready_ready;
|
|
wire [NODE_IDW-1:0] dm_ready_node_id;
|
|
wire [ADDR_WIDTH-1:0] dm_ready_x_base, dm_ready_w_base, dm_ready_result_addr;
|
|
wire [15:0] dm_ready_n_tiles;
|
|
|
|
wire dm_producer_done_valid;
|
|
wire [NODE_IDW-1:0] dm_producer_done_node_id;
|
|
|
|
dependency_manager #(
|
|
.N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .ADDR_WIDTH(ADDR_WIDTH)
|
|
) u_dep_mgr (
|
|
.clk(clk), .rst(rst),
|
|
.reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id),
|
|
.reg_required(reg_required), .reg_producer_ids(reg_producer_ids),
|
|
.reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles),
|
|
.reg_result_addr(reg_result_addr),
|
|
.producer_done_valid(dm_producer_done_valid), .producer_done_node_id(dm_producer_done_node_id),
|
|
.ready_valid(dm_ready_valid), .ready_ready(dm_ready_ready), .ready_node_id(dm_ready_node_id),
|
|
.ready_x_base(dm_ready_x_base), .ready_w_base(dm_ready_w_base),
|
|
.ready_n_tiles(dm_ready_n_tiles), .ready_result_addr(dm_ready_result_addr)
|
|
);
|
|
|
|
// node_id is 16 bits on the Director/Memory Manager side (matches
|
|
// neural_processor.v's own job_node_id width) but NODE_IDW bits on
|
|
// the Dependency Manager side (sized to N_NODES) -- zero-extended
|
|
// crossing the boundary, truncated coming back (safe as long as
|
|
// N_NODES <= 65536, always true for any NODE_IDW <= 16).
|
|
wire [15:0] dm_ready_node_id_ext = {{(16-NODE_IDW){1'b0}}, dm_ready_node_id};
|
|
|
|
// ---- Neural Director (M5) ----
|
|
wire [N_SLOTS-1:0] dir_slot_job_start;
|
|
wire [ADDR_WIDTH*N_SLOTS-1:0] dir_slot_x_base, dir_slot_w_base, dir_slot_result_addr;
|
|
wire [16*N_SLOTS-1:0] dir_slot_n_tiles, dir_slot_node_id;
|
|
wire [N_SLOTS-1:0] dir_slot_job_done;
|
|
wire dir_job_out_done;
|
|
wire [$clog2(N_SLOTS)-1:0] dir_job_out_slot;
|
|
wire [3:0] dir_state;
|
|
wire dir_error;
|
|
|
|
neural_director #(
|
|
.ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .QUEUE_DEPTH(QUEUE_DEPTH)
|
|
) u_director (
|
|
.clk(clk), .rst(rst),
|
|
.job_in_valid(dm_ready_valid), .job_in_ready(dm_ready_ready),
|
|
.job_in_x_base(dm_ready_x_base), .job_in_w_base(dm_ready_w_base),
|
|
.job_in_n_tiles(dm_ready_n_tiles), .job_in_result_addr(dm_ready_result_addr),
|
|
.job_in_node_id(dm_ready_node_id_ext),
|
|
.slot_job_start(dir_slot_job_start), .slot_x_base(dir_slot_x_base), .slot_w_base(dir_slot_w_base),
|
|
.slot_n_tiles(dir_slot_n_tiles), .slot_result_addr(dir_slot_result_addr),
|
|
.slot_node_id(dir_slot_node_id), .slot_job_done(dir_slot_job_done),
|
|
.job_out_done(dir_job_out_done), .job_out_slot(dir_job_out_slot),
|
|
.dir_state(dir_state), .dir_error(dir_error)
|
|
);
|
|
|
|
// job_out_slot indexes slot_node_id to recover which node just
|
|
// completed -- this becomes the Dependency Manager's own
|
|
// producer_done event, closing the wake-up loop.
|
|
wire [15:0] completed_node_id_16 = dir_slot_node_id[dir_job_out_slot*16 +: 16];
|
|
assign dm_producer_done_valid = dir_job_out_done;
|
|
assign dm_producer_done_node_id = completed_node_id_16[NODE_IDW-1:0];
|
|
|
|
// ---- shared activation_cache request bus (one port per slot,
|
|
// collected here for the cache instance below) ----
|
|
wire [N_SLOTS-1:0] xc_req;
|
|
wire [ADDR_WIDTH*N_SLOTS-1:0] xc_x_base;
|
|
wire [16*N_SLOTS-1:0] xc_tile_idx;
|
|
wire [N_SLOTS-1:0] xc_ack;
|
|
wire signed [DATA_WIDTH*P_IN*N_SLOTS-1:0] xc_tile_x;
|
|
|
|
// ---- N_SLOTS x (Memory Manager (M4) + Neural Processor (M1)) ----
|
|
genvar g;
|
|
generate
|
|
for (g = 0; g < N_SLOTS; g = g + 1) begin : GEN_SLOT
|
|
|
|
wire mm_operand_valid, mm_operand_ready;
|
|
wire signed [DATA_WIDTH*P_IN-1:0] mm_input_data, mm_weight_data;
|
|
wire mm_tile_last;
|
|
wire mm_result_valid, mm_result_ready;
|
|
wire signed [DATA_WIDTH-1:0] mm_result_data;
|
|
|
|
memory_manager #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH)
|
|
) u_mm (
|
|
.clk(clk), .rst(rst),
|
|
.job_start(dir_slot_job_start[g]),
|
|
.x_base(dir_slot_x_base[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.w_base(dir_slot_w_base[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.n_tiles(dir_slot_n_tiles[g*16 +: 16]),
|
|
.result_addr(dir_slot_result_addr[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.job_done(dir_slot_job_done[g]),
|
|
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
|
|
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
|
|
.result_valid(mm_result_valid), .result_ready(mm_result_ready), .result_data(mm_result_data),
|
|
.xc_req(xc_req[g]),
|
|
.xc_x_base(xc_x_base[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.xc_tile_idx(xc_tile_idx[g*16 +: 16]),
|
|
.xc_ack(xc_ack[g]),
|
|
.xc_tile_x(xc_tile_x[g*DATA_WIDTH*P_IN +: DATA_WIDTH*P_IN]),
|
|
.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*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;
|
|
wire job_ready_np;
|
|
wire result_valid_np;
|
|
wire signed [DATA_WIDTH-1:0] result_data_np;
|
|
wire [3:0] np_state;
|
|
wire np_error;
|
|
|
|
neural_processor #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)
|
|
) u_np (
|
|
.clk(clk), .rst(rst),
|
|
.job_valid(job_valid_np), .job_ready(job_ready_np),
|
|
.job_node_id(16'h0), .job_bias(8'sd0), .job_activation(2'd1),
|
|
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
|
|
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
|
|
.result_valid(result_valid_np), .result_ready(mm_result_ready),
|
|
.result_data(result_data_np), .result_node_id(),
|
|
.np_state(np_state), .np_error(np_error)
|
|
);
|
|
assign mm_result_valid = result_valid_np;
|
|
assign mm_result_data = result_data_np;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) job_valid_np <= 1'b0;
|
|
else if (dir_slot_job_start[g]) job_valid_np <= 1'b1;
|
|
else if (job_valid_np && job_ready_np) job_valid_np <= 1'b0;
|
|
end
|
|
|
|
end
|
|
endgenerate
|
|
|
|
// ---- shared activation_cache (M10+, DEC-0016) -- serves the
|
|
// ACTIVATION half of every slot's tile fetch, using arbiter port
|
|
// index N_SLOTS (the last one) for its own PSRAM traffic on a
|
|
// cache miss. ----
|
|
activation_cache #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS)
|
|
) u_activation_cache (
|
|
.clk(clk), .rst(rst),
|
|
.req(xc_req), .req_x_base(xc_x_base), .req_tile_idx(xc_tile_idx),
|
|
.ack(xc_ack), .tile_x_out(xc_tile_x),
|
|
.mem_req(slot_mem_req[N_SLOTS]), .mem_wr(slot_mem_wr[N_SLOTS]),
|
|
.mem_addr(slot_mem_addr[N_SLOTS*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.mem_wdata(slot_mem_wdata[N_SLOTS*16 +: 16]),
|
|
.mem_lb_n(slot_mem_lb_n[N_SLOTS]), .mem_ub_n(slot_mem_ub_n[N_SLOTS]),
|
|
.mem_rdata(slot_mem_rdata[N_SLOTS*16 +: 16]), .mem_ready(slot_mem_ready[N_SLOTS])
|
|
);
|
|
|
|
endmodule
|