diff --git a/hardware/v3/rtl/mac2_dsp_packed.v b/hardware/v3/rtl/mac2_dsp_packed.v new file mode 100644 index 0000000..ff73795 --- /dev/null +++ b/hardware/v3/rtl/mac2_dsp_packed.v @@ -0,0 +1,99 @@ +`timescale 1ns/1ps + +// ============================================================ +// v3 (Artix-7 port) -- 2 INT8 MACs sharing one resident weight, packed +// into a single DSP48E1-shaped 25x18 multiply. +// +// Fits this project's own weight-stationary reuse architecture +// (layer_weight_buffer.v, EXP-0057/0058) exactly: one weight stays +// resident and is multiplied against MANY different activations +// (spatial reuse positions). This packs TWO of those activations +// (x0, x1) against the SAME shared weight into one multiply, instead +// of two separate DSP48 multiplies -- doubling effective MAC/DSP +// throughput for exactly this access pattern. +// +// Packing scheme (signed INT8 x0, x1, weight, all in [-128, 127]): +// packed_a = (x1 <<< 16) + sign_extend(x0, 25) (25 bits, matches +// DSP48E1 port A width) +// product = packed_a * weight (33 bits here; +// widens to 43 bits with a real 18-bit weight port on +// actual DSP48E1 silicon) +// +// packed_a is built with a real ARITHMETIC add, not bit concatenation +// -- concatenating two independently sign-extended fields ({sext(x1,9), +// sext(x0,16)}) looks equivalent on paper but is NOT: whenever x0 is +// negative, its own two's-complement encoding contributes an extra +// +2^16 into the concatenated field's value that a real sum x1*2^16+x0 +// does not have (found via exhaustive verification below -- an earlier +// concatenation-based version failed exactly 8,355,840 / 16,777,216 +// vectors, all sharing x0<0). The explicit shift-and-add avoids this +// class of bug entirely by construction. +// +// Because x1's field sits at bit 16 (a multiple of 2^16), the low 16 +// bits of `product` always equal x0*weight exactly, taken as signed +// (modular arithmetic: (x1<<16)*weight is a multiple of 2^16, so it +// never disturbs bits [15:0] of the sum). x0*weight's magnitude is at +// most 128*128=16384, safely inside signed 16-bit range +// (-32768..32767), so no truncation. +// +// Extracting x1*weight from the upper bits needs one correction: an +// arithmetic right-shift of `product` by 16 computes +// floor(product / 2^16), which is x1*weight - 1 (not exactly +// x1*weight) whenever the low-16-bit product (x0*weight) is negative +// -- the classic "borrow" of splitting one real two's-complement sum +// into two fields after the fact (concatenating BEFORE the multiply is +// exact by construction; recovering the two products AFTER a real +// multiply-and-add requires this one correction). Fixed by adding 1 +// back whenever the low product's sign bit is set. +// ============================================================ +module mac2_dsp_packed #( + parameter DATA_WIDTH = 8 +)( + input wire clk, + input wire rst, + + input wire signed [DATA_WIDTH-1:0] weight, // shared, resident + input wire signed [DATA_WIDTH-1:0] x0, + input wire signed [DATA_WIDTH-1:0] x1, + input wire valid_in, + + output reg signed [2*DATA_WIDTH-1:0] p0, // = x0 * weight, exact + output reg signed [2*DATA_WIDTH-1:0] p1, // = x1 * weight, exact + output reg valid_out +); + localparam A_WIDTH = 3*DATA_WIDTH + 1; // 25 for DATA_WIDTH=8 + localparam PROD_WIDTH = A_WIDTH + DATA_WIDTH; // 43 for DATA_WIDTH=8 + + wire signed [A_WIDTH-1:0] x0_sext25 = {{(A_WIDTH-DATA_WIDTH){x0[DATA_WIDTH-1]}}, x0}; + wire signed [A_WIDTH-1:0] x1_shifted = $signed(x1) <<< (2*DATA_WIDTH); + + wire signed [A_WIDTH-1:0] packed_a = x1_shifted + x0_sext25; + + wire signed [PROD_WIDTH-1:0] product = packed_a * weight; + + // NOTE: a Verilog part-select (product[hi:lo]) always yields an + // UNSIGNED value regardless of the source's own `signed` keyword + // (LRM rule -- part-selects are never signed) -- explicit $signed() + // casts below are therefore load-bearing, not decorative: without + // them the arithmetic right shift used to recover p1_raw would + // truncate/zero-extend instead of sign-extending, corrupting every + // case where x1*weight is negative (found via exhaustive + // verification, tb_mac2_dsp_packed.v -- an earlier version without + // these casts, and with an off-by-one in p1_raw's declared width, + // failed ~50% of all 16,777,216 (weight,x0,x1) vectors). + wire signed [2*DATA_WIDTH-1:0] p0_comb = product[2*DATA_WIDTH-1:0]; + wire signed [A_WIDTH+DATA_WIDTH-2*DATA_WIDTH-1:0] p1_raw = $signed(product) >>> (2*DATA_WIDTH); + wire signed [2*DATA_WIDTH-1:0] p1_comb = p1_raw[2*DATA_WIDTH-1:0] + (p0_comb[2*DATA_WIDTH-1] ? 1'b1 : 1'b0); + + always @(posedge clk) begin + if (rst) begin + p0 <= {2*DATA_WIDTH{1'b0}}; + p1 <= {2*DATA_WIDTH{1'b0}}; + valid_out <= 1'b0; + end else begin + p0 <= p0_comb; + p1 <= p1_comb; + valid_out <= valid_in; + end + end +endmodule diff --git a/hardware/v3/rtl/neural_processor_packed.v b/hardware/v3/rtl/neural_processor_packed.v new file mode 100644 index 0000000..1b7c60e --- /dev/null +++ b/hardware/v3/rtl/neural_processor_packed.v @@ -0,0 +1,370 @@ +// ============================================================ +// FPGA-Neural V3 (Artix-7 port) -- Neural Processor, DSP48-packed. +// +// Direct port of hardware/v2/rtl/neural_processor.v (M1), restructured +// for the weight-stationary reuse pattern (layer_weight_buffer.v, +// EXP-0057/0058): ONE resident weight tile is shared by TWO reuse +// positions (job A, job B) processed in lockstep, each tap-lane packing +// its two x*w multiplies into a single DSP48-shaped multiply instead of +// two separate ones (see hardware/v3/rtl/mac2_dsp_packed.v, verified +// exhaustively 16,777,216/16,777,216 bit-exact -- the packing math +// here is the SAME formula, inlined per-lane rather than instantiated, +// to keep this module's own pipeline depth/stage count identical to +// the V2 original for a direct structural comparison). +// +// Pipeline stages match V2's neural_processor.v exactly, just doubled +// on the accumulator side (one accumulate/bias/activation/saturation +// path per job, A and B, sharing the SAME multiply/adder-tree stages +// since they consume the SAME weight stream): +// Stage 0 input alignment (x0_a, x0_b, w0 -- ONE shared weight) +// Stage 1 P_IN packed-MAC lanes: p0[i]=x0_a[i]*w0[i], p1[i]=x0_b[i]*w0[i] +// Stage 2..(1+TREE_LEVELS) TWO balanced adder trees (A and B) +// Stage (2+TREE_LEVELS) TWO accumulators +// Stage (3+TREE_LEVELS) bias add (shared bias/activation -- same +// neuron/filter, different spatial position) +// + activation, per job +// Stage (4+TREE_LEVELS) INT8 saturation / output register, per job +// +// job_bias/job_activation are SHARED between A and B (same resident +// neuron), matching this project's own weight-reuse semantics (a +// neuron/filter's bias and activation type don't vary by spatial +// position -- only its accumulated dot product does). node_id differs +// per job (A and B are different output positions). +// ============================================================ + +module neural_processor_packed #( + parameter DATA_WIDTH = 8, + parameter P_IN = 8, + parameter ACC_WIDTH = 32 +)( + input clk, + input rst, + + // ---- job descriptor (NP_LOAD_JOB) ---- + input job_valid, + output job_ready, + input [15:0] job_node_id_a, + input [15:0] job_node_id_b, + input signed [DATA_WIDTH-1:0] job_bias, // shared (same neuron) + input [1:0] job_activation, // shared (same neuron) + + // ---- operand stream: ONE shared weight stream, TWO activation streams ---- + input operand_valid, + output operand_ready, + input signed [DATA_WIDTH*P_IN-1:0] input_data_a, + input signed [DATA_WIDTH*P_IN-1:0] input_data_b, + input signed [DATA_WIDTH*P_IN-1:0] weight_data, + input tile_last, + + // ---- result stream: two results per job pair, same-cycle ---- + output reg result_valid, + input result_ready, + output reg signed [DATA_WIDTH-1:0] result_data_a, + output reg signed [DATA_WIDTH-1:0] result_data_b, + output reg [15:0] result_node_id_a, + output reg [15:0] result_node_id_b, + + output reg [3:0] np_state, + output reg np_error +); + + localparam ACT_NONE = 2'd0; + localparam ACT_RELU = 2'd1; + + localparam NP_IDLE = 4'd0; + localparam NP_LOAD_JOB = 4'd1; + localparam NP_WAIT_OPERANDS = 4'd2; + localparam NP_FINISH = 4'd3; + localparam NP_WRITE_RESULT = 4'd4; + localparam NP_DONE = 4'd5; + localparam NP_ERROR = 4'd6; + + localparam TREE_LEVELS = $clog2(P_IN); + localparam PROD_WIDTH = 2 * DATA_WIDTH; + + reg signed [DATA_WIDTH-1:0] bias_reg; + reg [1:0] activation_reg; + reg [15:0] node_id_a_reg, node_id_b_reg; + + assign operand_ready = (np_state == NP_WAIT_OPERANDS); + + // ============================================================ + // STAGE 0 -- input alignment + // ============================================================ + reg valid0, last0; + reg signed [DATA_WIDTH-1:0] xa0 [0:P_IN-1]; + reg signed [DATA_WIDTH-1:0] xb0 [0:P_IN-1]; + reg signed [DATA_WIDTH-1:0] w0 [0:P_IN-1]; + + integer gi; + + always @(posedge clk) begin + if (rst) begin + valid0 <= 1'b0; + last0 <= 1'b0; + end else begin + valid0 <= operand_valid && operand_ready; + last0 <= (operand_valid && operand_ready) ? tile_last : 1'b0; + if (operand_valid && operand_ready) begin + for (gi = 0; gi < P_IN; gi = gi + 1) begin + xa0[gi] <= input_data_a[gi*DATA_WIDTH +: DATA_WIDTH]; + xb0[gi] <= input_data_b[gi*DATA_WIDTH +: DATA_WIDTH]; + w0[gi] <= weight_data[gi*DATA_WIDTH +: DATA_WIDTH]; + end + end + end + end + + // ============================================================ + // STAGE 1 -- P_IN packed-MAC lanes (mac2_dsp_packed.v's own + // verified combinational formula, inlined per lane) + // ============================================================ + reg valid1, last1; + reg signed [ACC_WIDTH-1:0] proda1 [0:P_IN-1]; + reg signed [ACC_WIDTH-1:0] prodb1 [0:P_IN-1]; + + localparam A_WIDTH = 3*DATA_WIDTH + 1; + + wire signed [PROD_WIDTH-1:0] pa_comb [0:P_IN-1]; + wire signed [PROD_WIDTH-1:0] pb_comb [0:P_IN-1]; + + genvar gm; + generate + for (gm = 0; gm < P_IN; gm = gm + 1) begin : GEN_MAC_PACKED + wire signed [A_WIDTH-1:0] x0_sext25 = {{(A_WIDTH-DATA_WIDTH){xa0[gm][DATA_WIDTH-1]}}, xa0[gm]}; + wire signed [A_WIDTH-1:0] x1_shifted = $signed(xb0[gm]) <<< (2*DATA_WIDTH); + wire signed [A_WIDTH-1:0] packed_a = x1_shifted + x0_sext25; + wire signed [A_WIDTH+DATA_WIDTH-1:0] product = packed_a * w0[gm]; + + assign pa_comb[gm] = product[PROD_WIDTH-1:0]; + wire signed [A_WIDTH+DATA_WIDTH-2*DATA_WIDTH-1:0] pb_raw = + $signed(product) >>> (2*DATA_WIDTH); + assign pb_comb[gm] = pb_raw[PROD_WIDTH-1:0] + (pa_comb[gm][PROD_WIDTH-1] ? 1'b1 : 1'b0); + end + endgenerate + + always @(posedge clk) begin + if (rst) begin + valid1 <= 1'b0; + last1 <= 1'b0; + end else begin + valid1 <= valid0; + last1 <= last0; + for (gi = 0; gi < P_IN; gi = gi + 1) begin + proda1[gi] <= {{(ACC_WIDTH-PROD_WIDTH){pa_comb[gi][PROD_WIDTH-1]}}, pa_comb[gi]}; + prodb1[gi] <= {{(ACC_WIDTH-PROD_WIDTH){pb_comb[gi][PROD_WIDTH-1]}}, pb_comb[gi]}; + end + end + end + + // ============================================================ + // STAGES 2..(1+TREE_LEVELS) -- TWO balanced adder trees (A, B) + // ============================================================ + wire signed [ACC_WIDTH-1:0] level0a [0:P_IN-1]; + wire signed [ACC_WIDTH-1:0] level0b [0:P_IN-1]; + genvar gz; + generate + for (gz = 0; gz < P_IN; gz = gz + 1) begin : GEN_TREE_L0 + assign level0a[gz] = proda1[gz]; + assign level0b[gz] = prodb1[gz]; + end + endgenerate + + reg [TREE_LEVELS-1:0] valid_tree; + reg [TREE_LEVELS-1:0] last_tree; + reg signed [ACC_WIDTH-1:0] treea [1:TREE_LEVELS][0:P_IN-1]; + reg signed [ACC_WIDTH-1:0] treeb [1:TREE_LEVELS][0:P_IN-1]; + + genvar gl, gn; + generate + for (gl = 0; gl < TREE_LEVELS; gl = gl + 1) begin : GEN_TREE_LEVEL + always @(posedge clk) begin + if (rst) begin + valid_tree[gl] <= 1'b0; + last_tree[gl] <= 1'b0; + end else begin + valid_tree[gl] <= (gl == 0) ? valid1 : valid_tree[gl-1]; + last_tree[gl] <= (gl == 0) ? last1 : last_tree[gl-1]; + end + end + for (gn = 0; gn < (P_IN >> (gl+1)); gn = gn + 1) begin : GEN_TREE_NODE + if (gl == 0) begin : GEN_FROM_LEVEL0 + always @(posedge clk) begin + treea[1][gn] <= level0a[2*gn] + level0a[2*gn+1]; + treeb[1][gn] <= level0b[2*gn] + level0b[2*gn+1]; + end + end else begin : GEN_FROM_TREE + always @(posedge clk) begin + treea[gl+1][gn] <= treea[gl][2*gn] + treea[gl][2*gn+1]; + treeb[gl+1][gn] <= treeb[gl][2*gn] + treeb[gl][2*gn+1]; + end + end + end + end + endgenerate + + wire valid_tree_out = (TREE_LEVELS == 0) ? valid1 : valid_tree[TREE_LEVELS-1]; + wire last_tree_out = (TREE_LEVELS == 0) ? last1 : last_tree[TREE_LEVELS-1]; + wire signed [ACC_WIDTH-1:0] tile_sum_a = (TREE_LEVELS == 0) ? proda1[0] : treea[TREE_LEVELS][0]; + wire signed [ACC_WIDTH-1:0] tile_sum_b = (TREE_LEVELS == 0) ? prodb1[0] : treeb[TREE_LEVELS][0]; + + // ============================================================ + // STAGE (2+TREE_LEVELS) -- TWO accumulators + // ============================================================ + reg signed [ACC_WIDTH-1:0] acc_reg_a, acc_reg_b; + reg valid5, last5; + + always @(posedge clk) begin + if (rst) begin + acc_reg_a <= {ACC_WIDTH{1'b0}}; + acc_reg_b <= {ACC_WIDTH{1'b0}}; + valid5 <= 1'b0; + last5 <= 1'b0; + end else begin + valid5 <= valid_tree_out; + last5 <= last_tree_out; + if (np_state == NP_LOAD_JOB) begin + acc_reg_a <= {ACC_WIDTH{1'b0}}; + acc_reg_b <= {ACC_WIDTH{1'b0}}; + end else if (valid_tree_out) begin + acc_reg_a <= acc_reg_a + tile_sum_a; + acc_reg_b <= acc_reg_b + tile_sum_b; + end + end + end + + // ============================================================ + // STAGE (3+TREE_LEVELS) -- bias add + activation (shared bias/act) + // ============================================================ + wire signed [ACC_WIDTH-1:0] bias_ext = + {{(ACC_WIDTH-DATA_WIDTH){bias_reg[DATA_WIDTH-1]}}, bias_reg}; + + reg valid6, last6; + reg signed [ACC_WIDTH-1:0] final_acc_a, final_acc_b; + + always @(posedge clk) begin + if (rst) begin + valid6 <= 1'b0; + last6 <= 1'b0; + end else begin + valid6 <= valid5; + last6 <= last5; + final_acc_a <= acc_reg_a + bias_ext; + final_acc_b <= acc_reg_b + bias_ext; + end + end + + function automatic signed [DATA_WIDTH-1:0] saturate_activate( + input signed [ACC_WIDTH-1:0] final_acc, + input [1:0] activation + ); + reg sign; + reg upper_all0, upper_all1, in_range, le_zero; + reg signed [DATA_WIDTH-1:0] y_none, y_relu; + begin + sign = final_acc[ACC_WIDTH-1]; + upper_all0 = ~(|final_acc[ACC_WIDTH-1:DATA_WIDTH-1]); + upper_all1 = &final_acc[ACC_WIDTH-1:DATA_WIDTH-1]; + in_range = upper_all0 | upper_all1; + le_zero = sign | ~(|final_acc); + + y_none = in_range ? final_acc[DATA_WIDTH-1:0] + : (sign ? {1'b1, {(DATA_WIDTH-1){1'b0}}} + : {1'b0, {(DATA_WIDTH-1){1'b1}}}); + y_relu = le_zero ? {DATA_WIDTH{1'b0}} + : (upper_all0 ? final_acc[DATA_WIDTH-1:0] + : {1'b0, {(DATA_WIDTH-1){1'b1}}}); + saturate_activate = (activation == ACT_NONE) ? y_none : y_relu; + end + endfunction + + // ============================================================ + // STAGE (4+TREE_LEVELS) -- output register / saturation, per job + // ============================================================ + reg valid7; + reg signed [DATA_WIDTH-1:0] y7_a, y7_b; + + always @(posedge clk) begin + if (rst) begin + valid7 <= 1'b0; + end else begin + valid7 <= last6; + y7_a <= saturate_activate(final_acc_a, activation_reg); + y7_b <= saturate_activate(final_acc_b, activation_reg); + end + end + + wire pipeline_busy = valid0 || valid1 || (|valid_tree) || valid5 || valid6 || valid7; + assign job_ready = (np_state == NP_IDLE) && !pipeline_busy; + + // ============================================================ + // OUTER FSM -- identical shape to V2, both result channels together + // ============================================================ + always @(posedge clk) begin + if (rst) begin + np_state <= NP_IDLE; + np_error <= 1'b0; + result_valid <= 1'b0; + result_data_a <= {DATA_WIDTH{1'b0}}; + result_data_b <= {DATA_WIDTH{1'b0}}; + result_node_id_a <= 16'h0; + result_node_id_b <= 16'h0; + bias_reg <= {DATA_WIDTH{1'b0}}; + activation_reg <= ACT_RELU; + node_id_a_reg <= 16'h0; + node_id_b_reg <= 16'h0; + end else begin + case (np_state) + + NP_IDLE: begin + if (job_valid && job_ready) begin + bias_reg <= job_bias; + activation_reg <= job_activation; + node_id_a_reg <= job_node_id_a; + node_id_b_reg <= job_node_id_b; + np_state <= NP_LOAD_JOB; + end + end + + NP_LOAD_JOB: begin + np_state <= NP_WAIT_OPERANDS; + end + + NP_WAIT_OPERANDS: begin + if (operand_valid && operand_ready && tile_last) begin + np_state <= NP_FINISH; + end + end + + NP_FINISH: begin + if (valid7) begin + result_valid <= 1'b1; + result_data_a <= y7_a; + result_data_b <= y7_b; + result_node_id_a <= node_id_a_reg; + result_node_id_b <= node_id_b_reg; + np_state <= NP_WRITE_RESULT; + end + end + + NP_WRITE_RESULT: begin + if (result_valid && result_ready) begin + result_valid <= 1'b0; + np_state <= NP_DONE; + end + end + + NP_DONE: begin + np_state <= NP_IDLE; + end + + NP_ERROR: begin + end + + default: np_state <= NP_ERROR; + + endcase + end + end + +endmodule diff --git a/hardware/v3/sim/tb_mac2_dsp_packed.v b/hardware/v3/sim/tb_mac2_dsp_packed.v new file mode 100644 index 0000000..bb2199f --- /dev/null +++ b/hardware/v3/sim/tb_mac2_dsp_packed.v @@ -0,0 +1,68 @@ +`timescale 1ns/1ps + +// ============================================================ +// Exhaustive verification of mac2_dsp_packed.v's signed packing +// arithmetic: every (weight, x0, x1) combination in [-128,127]^3 +// (256^3 = 16,777,216 vectors), checked against independent +// Verilog integer multiplication (the "third oracle" convention +// used throughout this project). Checks the COMBINATIONAL packed +// result directly (no per-vector clock edge) for speed -- the +// registered p0/p1 outputs are just a one-cycle pipeline of the +// same combinational value, already covered structurally by every +// other testbench in this project using this same register idiom. +// ============================================================ +module tb; + localparam DATA_WIDTH = 8; + + reg clk = 0; + always #5 clk = ~clk; + reg rst; + + reg signed [DATA_WIDTH-1:0] weight, x0, x1; + reg valid_in; + wire signed [2*DATA_WIDTH-1:0] p0, p1; + wire valid_out; + + mac2_dsp_packed #(.DATA_WIDTH(DATA_WIDTH)) dut ( + .clk(clk), .rst(rst), + .weight(weight), .x0(x0), .x1(x1), .valid_in(valid_in), + .p0(p0), .p1(p1), .valid_out(valid_out) + ); + + integer w, a, b; + integer tests, errors; + integer exp0, exp1; + + initial begin + rst = 1; weight = 0; x0 = 0; x1 = 0; valid_in = 0; + tests = 0; errors = 0; + @(posedge clk); @(posedge clk); + rst = 0; + @(posedge clk); + + for (w = -128; w <= 127; w = w + 1) begin + weight = w[7:0]; + for (a = -128; a <= 127; a = a + 1) begin + x0 = a[7:0]; + for (b = -128; b <= 127; b = b + 1) begin + x1 = b[7:0]; + #1; + tests = tests + 1; + exp0 = a * w; + exp1 = b * w; + if (dut.p0_comb !== exp0[2*DATA_WIDTH-1:0] || dut.p1_comb !== exp1[2*DATA_WIDTH-1:0]) begin + errors = errors + 1; + if (errors <= 20) + $display("FAIL w=%0d x0=%0d x1=%0d: got p0=%0d p1=%0d expected p0=%0d p1=%0d", + w, a, b, $signed(dut.p0_comb), $signed(dut.p1_comb), exp0, exp1); + end + end + end + if (w % 32 == 0) $display("... progress: weight=%0d, tests so far=%0d, errors so far=%0d", w, tests, errors); + end + + $display("=== RESULT: %0d/%0d PASS, %0d errors (exhaustive weight x x0 x x1, 256^3) ===", tests-errors, tests, errors); + if (errors == 0) $display("ALL TESTS PASSED (tb_mac2_dsp_packed) -- exhaustive, mac2_dsp_packed.v is bit-exact"); + $finish; + end +endmodule diff --git a/hardware/v3/sim/tb_neural_processor_packed.v b/hardware/v3/sim/tb_neural_processor_packed.v new file mode 100644 index 0000000..197a51a --- /dev/null +++ b/hardware/v3/sim/tb_neural_processor_packed.v @@ -0,0 +1,233 @@ +`timescale 1ns/1ps + +// ============================================================ +// v3 -- verifies neural_processor_packed.v against TWO instances of +// the real, already-trusted hardware/v2/rtl/neural_processor.v (one +// fed job A's activations, one fed job B's, both fed the SAME shared +// weight stream -- exactly the weight-reuse access pattern this module +// is built for). Same driving convention as hardware/v2/sim/ +// tb_neural_processor.v (side-by-side DUTs, identical operands, +// bit-exact comparison). +// ============================================================ +module tb; + localparam DATA_WIDTH = 8; + localparam P_IN = 8; + localparam ACC_WIDTH = 32; + localparam MAX_N = 64; + + reg clk, rst; + initial begin clk = 0; forever #5 clk = ~clk; end + + integer errors, tests; + + // ---------------- reference: two real V2 neural_processor.v cores ---------------- + reg v2a_job_valid, v2b_job_valid; + wire v2a_job_ready, v2b_job_ready; + reg [15:0] v2a_node_id, v2b_node_id; + reg signed [DATA_WIDTH-1:0] v2_bias; + reg [1:0] v2_activation; + + reg v2_operand_valid; + wire v2a_operand_ready, v2b_operand_ready; + reg signed [DATA_WIDTH*P_IN-1:0] input_data_a, input_data_b, weight_data; + reg v2_tile_last; + + wire v2a_result_valid, v2b_result_valid; + reg v2_result_ready; + wire signed [DATA_WIDTH-1:0] v2a_result_data, v2b_result_data; + wire [15:0] v2a_result_node_id, v2b_result_node_id; + wire [3:0] v2a_np_state, v2b_np_state; + wire v2a_np_error, v2b_np_error; + + neural_processor #(.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)) v2a ( + .clk(clk), .rst(rst), + .job_valid(v2a_job_valid), .job_ready(v2a_job_ready), + .job_node_id(v2a_node_id), .job_bias(v2_bias), .job_activation(v2_activation), + .operand_valid(v2_operand_valid), .operand_ready(v2a_operand_ready), + .input_data(input_data_a), .weight_data(weight_data), .tile_last(v2_tile_last), + .result_valid(v2a_result_valid), .result_ready(v2_result_ready), + .result_data(v2a_result_data), .result_node_id(v2a_result_node_id), + .np_state(v2a_np_state), .np_error(v2a_np_error) + ); + neural_processor #(.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)) v2b ( + .clk(clk), .rst(rst), + .job_valid(v2b_job_valid), .job_ready(v2b_job_ready), + .job_node_id(v2b_node_id), .job_bias(v2_bias), .job_activation(v2_activation), + .operand_valid(v2_operand_valid), .operand_ready(v2b_operand_ready), + .input_data(input_data_b), .weight_data(weight_data), .tile_last(v2_tile_last), + .result_valid(v2b_result_valid), .result_ready(v2_result_ready), + .result_data(v2b_result_data), .result_node_id(v2b_result_node_id), + .np_state(v2b_np_state), .np_error(v2b_np_error) + ); + + // ---------------- DUT: v3 packed neural_processor ---------------- + reg job_valid; + wire job_ready; + reg [15:0] job_node_id_a, job_node_id_b; + reg signed [DATA_WIDTH-1:0] job_bias; + reg [1:0] job_activation; + + reg operand_valid; + wire operand_ready; + reg tile_last; + + wire result_valid; + reg result_ready; + wire signed [DATA_WIDTH-1:0] result_data_a, result_data_b; + wire [15:0] result_node_id_a, result_node_id_b; + wire [3:0] np_state; + wire np_error; + + neural_processor_packed #(.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)) dut ( + .clk(clk), .rst(rst), + .job_valid(job_valid), .job_ready(job_ready), + .job_node_id_a(job_node_id_a), .job_node_id_b(job_node_id_b), + .job_bias(job_bias), .job_activation(job_activation), + .operand_valid(operand_valid), .operand_ready(operand_ready), + .input_data_a(input_data_a), .input_data_b(input_data_b), .weight_data(weight_data), + .tile_last(tile_last), + .result_valid(result_valid), .result_ready(result_ready), + .result_data_a(result_data_a), .result_data_b(result_data_b), + .result_node_id_a(result_node_id_a), .result_node_id_b(result_node_id_b), + .np_state(np_state), .np_error(np_error) + ); + + reg signed [DATA_WIDTH-1:0] xamem [0:MAX_N-1]; + reg signed [DATA_WIDTH-1:0] xbmem [0:MAX_N-1]; + reg signed [DATA_WIDTH-1:0] wmem [0:MAX_N-1]; + integer i, t, k, n_inputs, n_tiles; + integer watchdog; + + task automatic run_case( + input integer n, + input signed [DATA_WIDTH-1:0] bias, + input [1:0] activation, + input [15:0] node_id + ); + begin + @(posedge clk); + tests = tests + 1; + n_inputs = n; + n_tiles = n / P_IN; + + v2_bias = bias; v2_activation = activation; + job_bias = bias; job_activation = activation; + v2a_node_id = node_id; v2b_node_id = node_id + 16'd1; + job_node_id_a = node_id; job_node_id_b = node_id + 16'd1; + + v2a_job_valid = 1; v2b_job_valid = 1; job_valid = 1; + while (!v2a_job_ready || !v2b_job_ready || !job_ready) @(posedge clk); + @(posedge clk); #1; + v2a_job_valid = 0; v2b_job_valid = 0; job_valid = 0; + + for (t = 0; t < n_tiles; t = t + 1) begin + input_data_a = {DATA_WIDTH*P_IN{1'b0}}; + input_data_b = {DATA_WIDTH*P_IN{1'b0}}; + weight_data = {DATA_WIDTH*P_IN{1'b0}}; + for (k = 0; k < P_IN; k = k + 1) begin + input_data_a[k*DATA_WIDTH +: DATA_WIDTH] = xamem[t*P_IN + k]; + input_data_b[k*DATA_WIDTH +: DATA_WIDTH] = xbmem[t*P_IN + k]; + weight_data[k*DATA_WIDTH +: DATA_WIDTH] = wmem[t*P_IN + k]; + end + v2_tile_last = (t == n_tiles - 1); + tile_last = v2_tile_last; + v2_operand_valid = 1; + operand_valid = 1; + while (!v2a_operand_ready || !v2b_operand_ready || !operand_ready) @(posedge clk); + @(posedge clk); #1; + end + // pulse-hardening (same class of bug as consume_done/pf_start/ + // ctrl_req elsewhere today): clearing operand_valid/tile_last + // in the SAME delta as the last handshake's own edge races + // against the three FSMs' own evaluation of that edge, and can + // silently drop the tile_last=1 that should trigger NP_FINISH. + // The #1 above (after the loop's last @(posedge clk)) already + // pushes this clear into a later time step. + v2_operand_valid = 0; + operand_valid = 0; + v2_tile_last = 0; + tile_last = 0; + + v2_result_ready = 1; + result_ready = 1; + watchdog = 0; + while (!(v2a_result_valid && v2b_result_valid && result_valid) && watchdog < 300) begin + @(posedge clk); + watchdog = watchdog + 1; + end + + if (!v2a_result_valid || !v2b_result_valid || !result_valid) begin + $display("FAIL n=%0d: watchdog timeout waiting for results (v2a=%b v2b=%b dut=%b)", + n, v2a_result_valid, v2b_result_valid, result_valid); + errors = errors + 1; + end else begin + if (result_data_a !== v2a_result_data || result_data_b !== v2b_result_data) begin + $display("FAIL n=%0d bias=%0d act=%0d: v2a=%0d v2b=%0d dut_a=%0d dut_b=%0d MISMATCH", + n, bias, activation, v2a_result_data, v2b_result_data, result_data_a, result_data_b); + errors = errors + 1; + end else begin + $display("PASS n=%0d bias=%0d act=%0d: a=%0d b=%0d (bit-exact vs 2x real neural_processor.v)", + n, bias, activation, result_data_a, result_data_b); + end + @(posedge clk); + end + + while (!job_ready || np_state !== 4'd0 || !v2a_job_ready || !v2b_job_ready) @(posedge clk); + end + endtask + + integer li, pi; + initial begin + errors = 0; tests = 0; + rst = 1; + v2a_job_valid=0; v2b_job_valid=0; job_valid=0; + v2a_node_id=0; v2b_node_id=0; job_node_id_a=0; job_node_id_b=0; + v2_bias=0; v2_activation=1; job_bias=0; job_activation=1; + v2_operand_valid=0; operand_valid=0; + input_data_a=0; input_data_b=0; weight_data=0; + v2_tile_last=0; tile_last=0; + v2_result_ready=0; result_ready=0; + repeat(4) @(posedge clk); + rst = 0; + @(posedge clk); + + // ---- functional sweep: several N, several (li,pi)-derived + // deterministic x_a/x_b/w patterns (matches this project's own + // weight-reuse formula style, EXP-0058), both activations ---- + for (li = 0; li < 3; li = li + 1) begin + for (pi = 0; pi < 4; pi = pi + 1) begin + for (i = 0; i < 64; i = i + 1) begin + wmem[i] = $signed(8'((li*17 + i*29 + 13) & 8'hFF)); + xamem[i] = $signed(8'((li*11 + (2*pi)*41 + i*7 + 3) & 8'hFF)); + xbmem[i] = $signed(8'((li*11 + (2*pi+1)*41 + i*7 + 3) & 8'hFF)); + end + run_case(64, $signed(8'((li*3+pi) & 8'hFF)), (pi[0] ? 2'd1 : 2'd0), li*100+pi); + end + end + + // ---- extreme INT8 boundary cases, N=16 ---- + for (i = 0; i < 16; i = i + 1) begin + wmem[i] = (i % 2 == 0) ? -8'sd128 : 8'sd127; + xamem[i] = (i % 3 == 0) ? -8'sd128 : ((i%3==1) ? 8'sd127 : 8'sd0); + xbmem[i] = (i % 3 == 0) ? 8'sd127 : ((i%3==1) ? -8'sd128 : -8'sd1); + end + run_case(16, 8'sd0, 2'd1, 16'd9001); + run_case(16, 8'sd127, 2'd0, 16'd9002); + run_case(16, -8'sd128, 2'd1, 16'd9003); + + // ---- back-to-back jobs, no idle gap (throughput check) ---- + for (i = 0; i < 32; i = i + 1) begin + wmem[i] = $signed(8'((i*5+7) & 8'hFF)); + xamem[i] = $signed(8'((i*3+1) & 8'hFF)); + xbmem[i] = $signed(8'((i*13+2) & 8'hFF)); + end + run_case(32, 8'sd10, 2'd1, 16'd9100); + run_case(32, -8'sd10, 2'd0, 16'd9101); + run_case(32, 8'sd0, 2'd1, 16'd9102); + + $display("=== RESULT: %0d/%0d PASS, %0d errors (neural_processor_packed.v vs 2x real neural_processor.v) ===", + tests-errors, tests, errors); + if (errors == 0) $display("ALL TESTS PASSED (tb_neural_processor_packed)"); + $finish; + end +endmodule