feat(v2): scaffold hardware/v1 frozen baseline + M1 Neural Processor
Begins the V2 Neural Multiprocessor / Dataflow architecture per docs/v2-description.md, per explicit user request to freeze V1 and start V2 development, copying from V1 what's needed. Scaffold: - hardware/v1/: byte-exact, read-only copy of the current V1 codebase (rtl, testbenches, tools, constraints, a representative subset of synthesis results, and reference docs) -- verified identical via diff/cmp against the live top-level tree before being made filesystem-read-only. The live top-level tree is untouched and remains the project's "production" V1 (see hardware/v1/README.md and hardware/v2/logs/decisions.log DEC-0001 for why copy-not-move). - hardware/v2/: mandatory structure (rtl/sim/constraints/synthesis/ reports/scripts/logs/docs) plus the full logging system required by the spec (development/architecture/simulation/synthesis/timing/ benchmark/decisions/experiments/errors.log). M1 -- Neural Processor (hardware/v2/rtl/neural_processor.v): - 8-stage pipelined perceptron unit (P_IN=8): input align, 8 multipliers, 3-level adder tree, accumulator, bias+activation, INT8 saturation. Genuine 1-tile/cycle throughput, not just a wider combinational datapath. - 7-state FSM (NP_IDLE..NP_ERROR per docs/v2-description.md §6, with 4 baseline states merged into NP_WAIT_OPERANDS -- see decisions.log DEC-0002); valid/ready/data/last stream interfaces per §7. - Bit-exact vs the frozen hardware/v1/rtl/neuron_parallel.v + mac8.v + mac_unit.v: 7/7 tests pass (hardware/v2/sim/tb_neural_processor.v), covering regular/mixed-sign/extreme-INT8 vectors, both activations, a zero-idle-gap back-to-back-tiles throughput check, and an 8-tile job -- verified with Verilator (see below for why). - Real synthesis + place&route (Yosys + nextpnr-ecp5): 0 CHECK problems, Fmax 183.12 MHz at ACC_WIDTH=32 (PASS at 80MHz, ~3x V1's isolated PARALLEL=8 Fmax of 61.71 MHz) and 176.21 MHz at ACC_WIDTH=24 (a user-requested comparison experiment, also bit-exact-verified; see experiments.log EXP-0001/EXP-0002 and benchmark.log). Three real bugs found and resolved during M1 development (full diagnostic record in errors.log): - Two independent, reproducible Icarus Verilog v13.0 scheduling defects (ERR-0001, ERR-0002) that silently produced wrong simulation results for standard sequential Verilog -- confirmed via Verilator 5.050 giving correct results on the same minimal repros. Verilator is now the trusted simulator for hardware/v2/ (decisions.log DEC-0004); Icarus's affected protocol-violation check was removed from the RTL and deferred architecturally to the Neural Director (DEC-0003) rather than chased further. - One real RTL bug (ERR-0003): last0 wasn't gated like valid0, letting a "last tile" tag leak into the pipeline ahead of its actual valid tile on back-to-back jobs. Fixed and verified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
@@ -0,0 +1,611 @@
|
||||
`timescale 1ns/1ps
|
||||
|
||||
// ================================================================
|
||||
// GRAPH_ENGINE (Phase G3 -- Type #2 sparse-graph network)
|
||||
//
|
||||
// Orchestrates an arbitrary feed-forward DAG of neurons on top of
|
||||
// the validated neuron_parallel core, exactly the way neuron_memory
|
||||
// / layer_sequencer orchestrate the dense Type #1 path -- the
|
||||
// compute datapath itself (neuron_parallel/mac8/mac_unit) is
|
||||
// instantiated unmodified, never edited. See docs spec §2-§7 for
|
||||
// the full design rationale; this header only covers what a reader
|
||||
// of THIS file needs.
|
||||
//
|
||||
// REGISTER REUSE (no new SET_BASE selectors beyond sel 9/10):
|
||||
// dense (#1) and graph (#2) modes are mutually exclusive at runtime
|
||||
// (net_type dispatch, see spi_engine.v), so this module reuses three
|
||||
// existing SET_BASE-driven registers for a different purpose while
|
||||
// in graph mode -- documented here since it is not spelled out
|
||||
// verbatim in the data-format spec:
|
||||
// x_base -> base address of the N_in input bytes in PSRAM
|
||||
// (same register/opcode host already uses to
|
||||
// preload inputs for dense mode)
|
||||
// table_base -> base of the graph descriptor table (§4.2),
|
||||
// instead of the dense layer descriptor table
|
||||
// buf_a_base -> out_base: where the n_out output bytes are
|
||||
// copied to in PSRAM at the end of the run
|
||||
// (dense mode's ping-pong buffer A is unused
|
||||
// while graph mode runs, so no conflict)
|
||||
// n_inputs_real -> N_in: number of graph input ids (0..N_in-1),
|
||||
// instead of a dense layer's real input count
|
||||
//
|
||||
// OUTPUT COPY, FOLDED INTO THE MAIN LOOP: §6 describes a WRITE_OUTPUTS
|
||||
// state as a separate pass after all neurons are computed. Since the
|
||||
// spec's own out_id ordering guarantee (§4.4: "out_ids ... finiscono
|
||||
// naturalmente con gli id piu alti") makes the output set EXACTLY
|
||||
// the last n_out entries of the descriptor table, this engine copies
|
||||
// a neuron's result to out_base the moment it computes it, if that
|
||||
// neuron is one of the last n_out (WRITE_ACT / WRITE_OUT below) --
|
||||
// no second pass over act_buf or extra id bookkeeping needed.
|
||||
//
|
||||
// GUARD (§7): a load-time check that src_id < out_id and
|
||||
// src_id < N_TOTAL is done per-edge, right as each edge's src_id is
|
||||
// read. On violation, `err` is raised (sticky until `rst`) and the
|
||||
// run stops immediately -- no further memory traffic, no result
|
||||
// written -- instead of silently computing a wrong answer. This
|
||||
// module ALSO checks out_id < N_TOTAL and n_conn_padded (post-
|
||||
// PARALLEL-padding) <= MAX_CONN for the same reason (out-of-range
|
||||
// addressing / MAX_CONN overflow are just as unsafe as the two
|
||||
// checks the spec calls out by name, and "stop instead of silently
|
||||
// misbehaving" is the whole point of this section) -- an addition
|
||||
// beyond the literal spec text, noted here for anyone diffing
|
||||
// against it.
|
||||
//
|
||||
// PARALLEL-multiple padding (§2.6) is entirely a HOST/assembler
|
||||
// responsibility: the edge blocks in PSRAM already physically
|
||||
// contain the zero-weight padding edges tools/netasm inserts, so
|
||||
// this engine just reads n_conn_padded = ceil(n_conn/PARALLEL)*
|
||||
// PARALLEL physical edges per neuron -- no special-casing here, and
|
||||
// neuron_parallel's own n_inputs_real convention (must already be a
|
||||
// PARALLEL multiple) is satisfied by construction. A neuron with 0
|
||||
// real connections still needs padding to a full PARALLEL group from
|
||||
// the host: n_conn_padded==0 is treated as a load-time error (see
|
||||
// guard above) rather than being forwarded to neuron_parallel, which
|
||||
// would hang on it (documented hang mode of neuron_parallel/GROUPS=0,
|
||||
// see rtl/neuron_parallel.v).
|
||||
//
|
||||
// ACTIVATION BUFFER GATHER TIMING: act_buffer's read port is
|
||||
// registered (1-cycle latency, see rtl/act_buffer.v). This engine
|
||||
// drives act_buf's read address directly (combinationally) from the
|
||||
// edge's src_id as soon as it is known (after the edge's 2nd byte),
|
||||
// and only consumes rd_data one FULL state later (ST_GATHER), by
|
||||
// which point at least two more PSRAM byte round-trips (weight +
|
||||
// reserved bytes of the same edge) have elapsed -- comfortably more
|
||||
// than the 1 cycle act_buffer needs, on any realistic memory. See
|
||||
// the ST_EDGE_WAIT / ST_GATHER case comments below.
|
||||
// ================================================================
|
||||
|
||||
module graph_engine #(
|
||||
parameter ADDR_WIDTH = 23,
|
||||
parameter DATA_WIDTH = 8,
|
||||
parameter ACC_WIDTH = 32,
|
||||
parameter PARALLEL = 8,
|
||||
parameter MAX_CONN = 32, // build-time max real+padding connections per neuron (must be a PARALLEL multiple -- enforced by neuron_parallel's own elaboration-time guard)
|
||||
parameter N_TOTAL = 4096 // activation buffer depth / signal id space ceiling (§2)
|
||||
)(
|
||||
input wire clk,
|
||||
input wire rst,
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Trigger / status (from spi_engine's RUN_NETWORK dispatch when
|
||||
// net_type == graph)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
input wire run_start, // one-cycle pulse
|
||||
output reg busy,
|
||||
output reg done, // one-cycle pulse
|
||||
output reg err, // sticky guard-violation flag (§7), cleared on rst or the next run_start
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Config registers (see REGISTER REUSE note above for x_base /
|
||||
// table_base / buf_a_base / n_inputs_real)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
input wire [ADDR_WIDTH-1:0] x_base,
|
||||
input wire [ADDR_WIDTH-1:0] table_base,
|
||||
input wire [ADDR_WIDTH-1:0] out_base,
|
||||
input wire [15:0] n_inputs_graph, // N_in
|
||||
input wire [15:0] num_neurons_graph, // SET_BASE sel 9
|
||||
input wire [15:0] n_out, // SET_BASE sel 10
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Byte-level RAM master port (own arbiter port; shared with
|
||||
// layer_sequencer's at the top level -- the two run modes are
|
||||
// mutually exclusive, see rtl/spi_neuron_top.v)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
output reg ram_req,
|
||||
output reg ram_wr,
|
||||
output reg [ADDR_WIDTH-1:0] ram_addr,
|
||||
output reg signed [7:0] ram_wdata,
|
||||
|
||||
input wire signed [7:0] ram_rdata,
|
||||
input wire ram_ready
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// DERIVED WIDTHS
|
||||
// ============================================================
|
||||
|
||||
localparam BUF_ADDR_WIDTH = $clog2(N_TOTAL);
|
||||
localparam CONN_IDX_WIDTH = (MAX_CONN <= 1) ? 1 : $clog2(MAX_CONN + 1);
|
||||
localparam PAR_SHIFT = $clog2(PARALLEL);
|
||||
|
||||
// ============================================================
|
||||
// STATES / FSM REGISTERS
|
||||
//
|
||||
// Declared before the submodule instantiations below because
|
||||
// act_buffer's read address is wired directly (combinationally)
|
||||
// to src_id_acc.
|
||||
// ============================================================
|
||||
|
||||
localparam ST_IDLE = 4'd0;
|
||||
localparam ST_COPY_IN_RD = 4'd1;
|
||||
localparam ST_COPY_IN_WAIT = 4'd2;
|
||||
localparam ST_DESC_RD = 4'd3;
|
||||
localparam ST_DESC_WAIT = 4'd4;
|
||||
localparam ST_EDGE_RD = 4'd5;
|
||||
localparam ST_EDGE_WAIT = 4'd6;
|
||||
localparam ST_GATHER = 4'd7;
|
||||
localparam ST_START_N = 4'd8;
|
||||
localparam ST_WAIT_N = 4'd9;
|
||||
localparam ST_WRITE_OUT_ISS = 4'd10;
|
||||
localparam ST_WRITE_OUT_WAIT = 4'd11;
|
||||
localparam ST_ERROR = 4'd12;
|
||||
|
||||
reg [3:0] state;
|
||||
|
||||
reg [15:0] in_idx;
|
||||
reg [15:0] neuron_idx;
|
||||
|
||||
reg [ADDR_WIDTH-1:0] desc_addr;
|
||||
reg [3:0] desc_byte_idx;
|
||||
reg [23:0] conn_ptr_acc;
|
||||
reg [15:0] n_conn_acc;
|
||||
reg [15:0] out_id_acc;
|
||||
|
||||
reg [CONN_IDX_WIDTH-1:0] conn_i;
|
||||
reg [1:0] edge_byte_idx;
|
||||
reg [15:0] src_id_acc;
|
||||
reg [7:0] weight_acc;
|
||||
|
||||
// Last n_out descriptor-table entries are the output sinks (§4.4):
|
||||
// entries are in ascending out_id order, so this is a static
|
||||
// range check on neuron_idx, no separate id lookup needed.
|
||||
wire [15:0] out_threshold = num_neurons_graph - n_out;
|
||||
wire is_output_sink = (neuron_idx >= out_threshold);
|
||||
wire [15:0] out_offset = neuron_idx - out_threshold;
|
||||
|
||||
// ============================================================
|
||||
// ACTIVATION BUFFER (private to this engine -- dense mode does
|
||||
// not use it at all)
|
||||
// ============================================================
|
||||
|
||||
reg act_wr_en;
|
||||
reg [BUF_ADDR_WIDTH-1:0] act_wr_addr;
|
||||
reg signed [DATA_WIDTH-1:0] act_wr_data;
|
||||
|
||||
wire signed [DATA_WIDTH-1:0] act_rd_data;
|
||||
|
||||
act_buffer #(
|
||||
.N_TOTAL(N_TOTAL),
|
||||
.DATA_WIDTH(DATA_WIDTH)
|
||||
) u_act_buffer (
|
||||
.clk(clk),
|
||||
.wr_en(act_wr_en), .wr_addr(act_wr_addr), .wr_data(act_wr_data),
|
||||
.rd_addr(src_id_acc[BUF_ADDR_WIDTH-1:0]), .rd_data(act_rd_data)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// NEURON (private instance, reused across every graph neuron --
|
||||
// same "one neuron computed at a time, memory-bound" design as
|
||||
// neuron_memory.v)
|
||||
// ============================================================
|
||||
|
||||
reg signed [DATA_WIDTH-1:0] x_mem [0:MAX_CONN-1];
|
||||
reg signed [DATA_WIDTH-1:0] w_mem [0:MAX_CONN-1];
|
||||
|
||||
wire signed [DATA_WIDTH*MAX_CONN-1:0] x_bus;
|
||||
wire signed [DATA_WIDTH*MAX_CONN-1:0] w_bus;
|
||||
|
||||
genvar gi;
|
||||
generate
|
||||
for (gi = 0; gi < MAX_CONN; gi = gi + 1) begin : GEN_BUS
|
||||
assign x_bus[gi*DATA_WIDTH +: DATA_WIDTH] = x_mem[gi];
|
||||
assign w_bus[gi*DATA_WIDTH +: DATA_WIDTH] = w_mem[gi];
|
||||
end
|
||||
endgenerate
|
||||
|
||||
reg neuron_start;
|
||||
reg signed [7:0] bias_reg;
|
||||
reg [1:0] activation_reg;
|
||||
reg [15:0] n_conn_padded_reg;
|
||||
|
||||
wire signed [7:0] neuron_y;
|
||||
wire neuron_busy;
|
||||
wire neuron_done;
|
||||
|
||||
neuron_parallel #(
|
||||
.DATA_WIDTH(DATA_WIDTH),
|
||||
.N_INPUTS(MAX_CONN),
|
||||
.PARALLEL(PARALLEL),
|
||||
.ACC_WIDTH(ACC_WIDTH)
|
||||
) u_neuron (
|
||||
.clk(clk), .rst(rst), .start(neuron_start),
|
||||
.x_bus(x_bus), .w_bus(w_bus), .bias(bias_reg),
|
||||
.activation(activation_reg),
|
||||
.n_inputs_real(n_conn_padded_reg),
|
||||
.y(neuron_y), .busy(neuron_busy), .done(neuron_done)
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// MAIN FSM
|
||||
// ============================================================
|
||||
|
||||
integer ri;
|
||||
|
||||
always @(posedge clk) begin
|
||||
|
||||
if (rst) begin
|
||||
|
||||
state <= ST_IDLE;
|
||||
busy <= 1'b0;
|
||||
done <= 1'b0;
|
||||
err <= 1'b0;
|
||||
|
||||
in_idx <= 16'h0;
|
||||
neuron_idx <= 16'h0;
|
||||
|
||||
desc_addr <= {ADDR_WIDTH{1'b0}};
|
||||
desc_byte_idx <= 4'h0;
|
||||
conn_ptr_acc <= 24'h0;
|
||||
n_conn_acc <= 16'h0;
|
||||
out_id_acc <= 16'h0;
|
||||
n_conn_padded_reg <= 16'h0;
|
||||
|
||||
conn_i <= {CONN_IDX_WIDTH{1'b0}};
|
||||
edge_byte_idx <= 2'h0;
|
||||
src_id_acc <= 16'h0;
|
||||
weight_acc <= 8'h0;
|
||||
|
||||
bias_reg <= 8'sd0;
|
||||
activation_reg <= 2'd1;
|
||||
|
||||
act_wr_en <= 1'b0;
|
||||
act_wr_addr <= {BUF_ADDR_WIDTH{1'b0}};
|
||||
act_wr_data <= 8'sd0;
|
||||
|
||||
neuron_start <= 1'b0;
|
||||
|
||||
ram_req <= 1'b0;
|
||||
ram_wr <= 1'b0;
|
||||
ram_addr <= {ADDR_WIDTH{1'b0}};
|
||||
ram_wdata <= 8'sd0;
|
||||
|
||||
for (ri = 0; ri < MAX_CONN; ri = ri + 1) begin
|
||||
x_mem[ri] <= 8'sd0;
|
||||
w_mem[ri] <= 8'sd0;
|
||||
end
|
||||
|
||||
end else begin
|
||||
|
||||
// --------------------------------------------------
|
||||
// Default pulses
|
||||
// --------------------------------------------------
|
||||
ram_req <= 1'b0;
|
||||
act_wr_en <= 1'b0;
|
||||
neuron_start <= 1'b0;
|
||||
done <= 1'b0;
|
||||
|
||||
case (state)
|
||||
|
||||
// =============================================
|
||||
// IDLE / ERROR: both accept run_start identically.
|
||||
// A fresh run_start clears a stale `err` and gives
|
||||
// the engine another attempt.
|
||||
// =============================================
|
||||
|
||||
ST_IDLE, ST_ERROR: begin
|
||||
|
||||
if (run_start) begin
|
||||
|
||||
busy <= 1'b1;
|
||||
err <= 1'b0;
|
||||
in_idx <= 16'h0;
|
||||
neuron_idx <= 16'h0;
|
||||
desc_addr <= table_base;
|
||||
|
||||
state <= ST_COPY_IN_RD;
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// COPY_INPUTS: act_buf[0..N_in-1] <- PSRAM[x_base..]
|
||||
// =============================================
|
||||
|
||||
ST_COPY_IN_RD: begin
|
||||
|
||||
ram_req <= 1'b1;
|
||||
ram_wr <= 1'b0;
|
||||
ram_addr <= x_base + in_idx;
|
||||
|
||||
state <= ST_COPY_IN_WAIT;
|
||||
|
||||
end
|
||||
|
||||
ST_COPY_IN_WAIT: begin
|
||||
|
||||
if (ram_ready) begin
|
||||
|
||||
act_wr_en <= 1'b1;
|
||||
act_wr_addr <= in_idx[BUF_ADDR_WIDTH-1:0];
|
||||
act_wr_data <= ram_rdata;
|
||||
|
||||
if (in_idx == n_inputs_graph - 16'd1) begin
|
||||
|
||||
// BUG-006 fix (docs/validation/bugs.md):
|
||||
// num_neurons_graph==0 has no neuron to
|
||||
// process at all -- the original code
|
||||
// always computed at least "neuron 0"
|
||||
// before its own termination check
|
||||
// (`neuron_idx == num_neurons_graph-1`,
|
||||
// further down) could even run, and that
|
||||
// check had the same unguarded-wraparound
|
||||
// structure as BUG-002/003/004/005. Report
|
||||
// done immediately instead of entering the
|
||||
// descriptor-read loop (real-edge-guard
|
||||
// protection incidentally limited the
|
||||
// practical damage here, per
|
||||
// docs/validation/06-graph-engine.md, but
|
||||
// the structural gap is closed properly
|
||||
// now rather than left to that
|
||||
// side-effect).
|
||||
if (num_neurons_graph == 16'h0) begin
|
||||
busy <= 1'b0;
|
||||
done <= 1'b1;
|
||||
state <= ST_IDLE;
|
||||
end else begin
|
||||
desc_byte_idx <= 4'h0;
|
||||
state <= ST_DESC_RD;
|
||||
end
|
||||
|
||||
end else begin
|
||||
in_idx <= in_idx + 16'd1;
|
||||
state <= ST_COPY_IN_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// READ_DESC: 11-byte graph descriptor (§4.2)
|
||||
// =============================================
|
||||
|
||||
ST_DESC_RD: begin
|
||||
|
||||
ram_req <= 1'b1;
|
||||
ram_wr <= 1'b0;
|
||||
ram_addr <= desc_addr + desc_byte_idx;
|
||||
|
||||
state <= ST_DESC_WAIT;
|
||||
|
||||
end
|
||||
|
||||
ST_DESC_WAIT: begin
|
||||
|
||||
if (ram_ready) begin
|
||||
|
||||
case (desc_byte_idx)
|
||||
4'd0: conn_ptr_acc[23:16] <= ram_rdata;
|
||||
4'd1: conn_ptr_acc[15:8] <= ram_rdata;
|
||||
4'd2: conn_ptr_acc[7:0] <= ram_rdata;
|
||||
4'd3: n_conn_acc[15:8] <= ram_rdata;
|
||||
4'd4: n_conn_acc[7:0] <= ram_rdata;
|
||||
4'd5: out_id_acc[15:8] <= ram_rdata;
|
||||
4'd6: out_id_acc[7:0] <= ram_rdata;
|
||||
4'd7: activation_reg <= ram_rdata[1:0];
|
||||
4'd8: bias_reg <= ram_rdata;
|
||||
default: ; // reserved bytes 9-10, ignored
|
||||
endcase
|
||||
|
||||
if (desc_byte_idx == 4'd10) begin
|
||||
|
||||
// n_conn_acc is fully committed (bytes
|
||||
// 3-4, several cycles ago) by now.
|
||||
n_conn_padded_reg <=
|
||||
((n_conn_acc + (PARALLEL - 1)) >> PAR_SHIFT) << PAR_SHIFT;
|
||||
|
||||
conn_i <= {CONN_IDX_WIDTH{1'b0}};
|
||||
edge_byte_idx <= 2'h0;
|
||||
state <= ST_EDGE_RD;
|
||||
|
||||
end else begin
|
||||
desc_byte_idx <= desc_byte_idx + 4'd1;
|
||||
state <= ST_DESC_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// EDGE stream + gather (§4.3, §7)
|
||||
// =============================================
|
||||
|
||||
ST_EDGE_RD: begin
|
||||
|
||||
// n_conn_padded_reg == 0 means the host sent a
|
||||
// neuron with no padded connections at all --
|
||||
// would forward n_inputs_real=0 to neuron_parallel,
|
||||
// which hangs (GROUPS=0 failure mode). Treated as
|
||||
// a load-time error instead (§7 rationale).
|
||||
if (n_conn_padded_reg == 16'h0) begin
|
||||
|
||||
err <= 1'b1;
|
||||
busy <= 1'b0;
|
||||
state <= ST_ERROR;
|
||||
|
||||
end else begin
|
||||
|
||||
ram_req <= 1'b1;
|
||||
ram_wr <= 1'b0;
|
||||
ram_addr <= conn_ptr_acc[ADDR_WIDTH-1:0] + (conn_i * 4) + edge_byte_idx;
|
||||
|
||||
state <= ST_EDGE_WAIT;
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
ST_EDGE_WAIT: begin
|
||||
|
||||
if (ram_ready) begin
|
||||
|
||||
case (edge_byte_idx)
|
||||
2'd0: src_id_acc[15:8] <= ram_rdata;
|
||||
2'd1: src_id_acc[7:0] <= ram_rdata;
|
||||
2'd2: weight_acc <= ram_rdata;
|
||||
default: ; // reserved byte, ignored
|
||||
endcase
|
||||
|
||||
if (edge_byte_idx == 2'd3) begin
|
||||
|
||||
edge_byte_idx <= 2'h0;
|
||||
|
||||
// §7 guard: src_id must be a strictly
|
||||
// earlier-computed signal, and both ids
|
||||
// must fit the activation buffer.
|
||||
if (src_id_acc >= out_id_acc ||
|
||||
src_id_acc >= N_TOTAL[15:0] ||
|
||||
out_id_acc >= N_TOTAL[15:0]) begin
|
||||
|
||||
err <= 1'b1;
|
||||
busy <= 1'b0;
|
||||
state <= ST_ERROR;
|
||||
|
||||
end else begin
|
||||
|
||||
// act_buf.rd_addr is wired directly
|
||||
// to src_id_acc (see instantiation
|
||||
// above); it has been stable since
|
||||
// this cycle already (both bytes of
|
||||
// src_id are committed) and will
|
||||
// still be stable through the
|
||||
// ST_GATHER cycle below.
|
||||
state <= ST_GATHER;
|
||||
|
||||
end
|
||||
|
||||
end else begin
|
||||
edge_byte_idx <= edge_byte_idx + 2'd1;
|
||||
state <= ST_EDGE_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// GATHER: consume act_buf's registered read data
|
||||
// (valid: rd_addr has been stable since the START
|
||||
// of this same edge's byte 1, i.e. well over a full
|
||||
// PSRAM byte round-trip ago -- see header comment).
|
||||
// =============================================
|
||||
|
||||
ST_GATHER: begin
|
||||
|
||||
x_mem[conn_i] <= act_rd_data;
|
||||
w_mem[conn_i] <= $signed(weight_acc);
|
||||
|
||||
if (conn_i == n_conn_padded_reg[CONN_IDX_WIDTH-1:0] - 1'b1) begin
|
||||
conn_i <= {CONN_IDX_WIDTH{1'b0}};
|
||||
state <= ST_START_N;
|
||||
end else begin
|
||||
conn_i <= conn_i + 1'b1;
|
||||
state <= ST_EDGE_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// START / WAIT neuron_parallel
|
||||
// =============================================
|
||||
|
||||
ST_START_N: begin
|
||||
neuron_start <= 1'b1;
|
||||
state <= ST_WAIT_N;
|
||||
end
|
||||
|
||||
ST_WAIT_N: begin
|
||||
|
||||
if (neuron_done) begin
|
||||
|
||||
act_wr_en <= 1'b1;
|
||||
act_wr_addr <= out_id_acc[BUF_ADDR_WIDTH-1:0];
|
||||
act_wr_data <= neuron_y;
|
||||
|
||||
if (is_output_sink) begin
|
||||
state <= ST_WRITE_OUT_ISS;
|
||||
end else if (neuron_idx == num_neurons_graph - 16'd1) begin
|
||||
busy <= 1'b0;
|
||||
done <= 1'b1;
|
||||
state <= ST_IDLE;
|
||||
end else begin
|
||||
neuron_idx <= neuron_idx + 16'd1;
|
||||
desc_addr <= desc_addr + 11;
|
||||
desc_byte_idx <= 4'h0;
|
||||
state <= ST_DESC_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
// =============================================
|
||||
// Fold WRITE_OUTPUTS into the loop for sink neurons
|
||||
// =============================================
|
||||
|
||||
ST_WRITE_OUT_ISS: begin
|
||||
|
||||
ram_req <= 1'b1;
|
||||
ram_wr <= 1'b1;
|
||||
ram_addr <= out_base + out_offset;
|
||||
ram_wdata <= neuron_y;
|
||||
|
||||
state <= ST_WRITE_OUT_WAIT;
|
||||
|
||||
end
|
||||
|
||||
ST_WRITE_OUT_WAIT: begin
|
||||
|
||||
if (ram_ready) begin
|
||||
|
||||
if (neuron_idx == num_neurons_graph - 16'd1) begin
|
||||
busy <= 1'b0;
|
||||
done <= 1'b1;
|
||||
state <= ST_IDLE;
|
||||
end else begin
|
||||
neuron_idx <= neuron_idx + 16'd1;
|
||||
desc_addr <= desc_addr + 11;
|
||||
desc_byte_idx <= 4'h0;
|
||||
state <= ST_DESC_RD;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
default: begin
|
||||
state <= ST_IDLE;
|
||||
end
|
||||
|
||||
endcase
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
endmodule
|
||||
Reference in New Issue
Block a user