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
299 lines
11 KiB
Verilog
299 lines
11 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ================================================================
|
|
// GRAPH FORMAT TESTBENCH (Phase G2)
|
|
//
|
|
// Validates the Type #2 (graph) on-disk data format from §4.2/§4.3
|
|
// of the spec, byte-exact, through the REAL memory stack
|
|
// (int8_memory_access + memory_interface + psram_controller +
|
|
// psram_model) -- same harness as sim/int8_psram_integration_tb.v.
|
|
// No new RTL: this phase is about the FORMAT, not new hardware.
|
|
//
|
|
// Graph under test is the worked example from §3:
|
|
// 4 inputs (id 0..3). n4 = f(x0*5 + x1*(-3) + bias=2), relu.
|
|
// n5 = f(act[n4]*2 + x2*7 + bias=0), none. output = n5 (id 5).
|
|
//
|
|
// Graph descriptor table (11 bytes/entry, MSB-first, entries in
|
|
// out_id order) at table_base = 0x000000:
|
|
// entry0 (n4, out_id=4): conn_ptr=0x000100 n_conn=2 out_id=4
|
|
// activation=ACT_RELU(1) bias=2 reserved=0
|
|
// entry1 (n5, out_id=5): conn_ptr=0x000108 n_conn=2 out_id=5
|
|
// activation=ACT_NONE(0) bias=0 reserved=0
|
|
//
|
|
// Edge blocks (4 bytes/edge: src_id uint16 BE, weight int8,
|
|
// reserved=0):
|
|
// n4 @ 0x000100: (src=0,w=5), (src=1,w=-3)
|
|
// n5 @ 0x000108: (src=4,w=2), (src=2,w=7)
|
|
// ================================================================
|
|
|
|
module tb;
|
|
|
|
localparam ADDR_WIDTH = 23;
|
|
localparam DATA_WIDTH = 16;
|
|
localparam CLK_PERIOD = 12.5; // 80 MHz
|
|
|
|
localparam ACT_NONE = 8'd0;
|
|
localparam ACT_RELU = 8'd1;
|
|
|
|
reg clk;
|
|
reg rst;
|
|
|
|
reg req;
|
|
reg wr;
|
|
reg [ADDR_WIDTH-1:0] addr;
|
|
reg signed [7:0] wdata;
|
|
|
|
wire signed [7:0] rdata;
|
|
wire ready;
|
|
|
|
wire mem_req;
|
|
wire mem_wr;
|
|
wire [ADDR_WIDTH-1:0] mem_addr;
|
|
wire [DATA_WIDTH-1:0] mem_wdata;
|
|
wire mem_lb_n;
|
|
wire mem_ub_n;
|
|
wire [DATA_WIDTH-1:0] mem_rdata;
|
|
wire mem_ready;
|
|
|
|
wire psram_mem_req;
|
|
wire psram_mem_wr;
|
|
wire [ADDR_WIDTH-1:0] psram_mem_addr;
|
|
wire [DATA_WIDTH-1:0] psram_mem_wdata;
|
|
wire psram_mem_lb_n;
|
|
wire psram_mem_ub_n;
|
|
wire [DATA_WIDTH-1:0] psram_mem_rdata;
|
|
wire psram_mem_ready;
|
|
|
|
wire [ADDR_WIDTH-1:0] psram_a;
|
|
wire [DATA_WIDTH-1:0] psram_dq;
|
|
wire psram_ce_n;
|
|
wire psram_oe_n;
|
|
wire psram_we_n;
|
|
wire psram_lb_n;
|
|
wire psram_ub_n;
|
|
wire psram_zz_n;
|
|
|
|
int8_memory_access #(
|
|
.ADDR_WIDTH(ADDR_WIDTH)
|
|
) int8_access (
|
|
.clk(clk), .rst(rst),
|
|
.req(req), .wr(wr), .addr(addr), .wdata(wdata),
|
|
.rdata(rdata), .ready(ready),
|
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr),
|
|
.mem_wdata(mem_wdata), .mem_lb_n(mem_lb_n), .mem_ub_n(mem_ub_n),
|
|
.mem_rdata(mem_rdata), .mem_ready(mem_ready)
|
|
);
|
|
|
|
memory_interface #(
|
|
.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH)
|
|
) memory_if (
|
|
.clk(clk), .rst(rst),
|
|
.req(mem_req), .wr(mem_wr), .addr(mem_addr), .wdata(mem_wdata),
|
|
.lb_n(mem_lb_n), .ub_n(mem_ub_n),
|
|
.rdata(mem_rdata), .ready(mem_ready),
|
|
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr), .mem_addr(psram_mem_addr),
|
|
.mem_wdata(psram_mem_wdata), .mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
|
|
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready)
|
|
);
|
|
|
|
psram_controller #(
|
|
.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH), .CLK_FREQ_MHZ(80)
|
|
) psram_ctrl (
|
|
.clk(clk), .rst(rst),
|
|
.mem_req(psram_mem_req), .mem_wr(psram_mem_wr), .mem_addr(psram_mem_addr),
|
|
.mem_wdata(psram_mem_wdata), .mem_lb_n(psram_mem_lb_n), .mem_ub_n(psram_mem_ub_n),
|
|
.mem_rdata(psram_mem_rdata), .mem_ready(psram_mem_ready),
|
|
.psram_a(psram_a), .psram_dq(psram_dq),
|
|
.psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n),
|
|
.psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n)
|
|
);
|
|
|
|
psram_model #(
|
|
.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(DATA_WIDTH), .DEPTH(16384)
|
|
) psram (
|
|
.clk(clk), .a(psram_a), .dq(psram_dq),
|
|
.ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n),
|
|
.lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n)
|
|
);
|
|
|
|
initial begin
|
|
clk = 1'b0;
|
|
forever #(CLK_PERIOD / 2.0) clk = ~clk;
|
|
end
|
|
|
|
integer errors;
|
|
|
|
task write_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] data);
|
|
begin
|
|
@(posedge clk);
|
|
addr <= byte_addr;
|
|
wdata <= $signed(data);
|
|
wr <= 1'b1;
|
|
req <= 1'b1;
|
|
@(posedge clk);
|
|
req <= 1'b0;
|
|
wait (ready);
|
|
@(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
task read_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] expected, input [255:0] name);
|
|
begin
|
|
@(posedge clk);
|
|
addr <= byte_addr;
|
|
wr <= 1'b0;
|
|
req <= 1'b1;
|
|
@(posedge clk);
|
|
req <= 1'b0;
|
|
wait (ready);
|
|
if (rdata !== $signed(expected)) begin
|
|
$display("FAIL %0s addr=0x%06x expected=0x%02x got=0x%02x", name, byte_addr, expected, rdata);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS %0s addr=0x%06x data=0x%02x", name, byte_addr, rdata);
|
|
end
|
|
@(posedge clk);
|
|
end
|
|
endtask
|
|
|
|
// §4.2 graph descriptor entry: 11 bytes, MSB-first.
|
|
task write_graph_desc(
|
|
input [ADDR_WIDTH-1:0] base,
|
|
input [23:0] conn_ptr,
|
|
input [15:0] n_conn,
|
|
input [15:0] out_id,
|
|
input [7:0] activation,
|
|
input [7:0] bias
|
|
);
|
|
begin
|
|
write_byte(base + 0, conn_ptr[23:16]);
|
|
write_byte(base + 1, conn_ptr[15:8]);
|
|
write_byte(base + 2, conn_ptr[7:0]);
|
|
write_byte(base + 3, n_conn[15:8]);
|
|
write_byte(base + 4, n_conn[7:0]);
|
|
write_byte(base + 5, out_id[15:8]);
|
|
write_byte(base + 6, out_id[7:0]);
|
|
write_byte(base + 7, activation);
|
|
write_byte(base + 8, bias);
|
|
write_byte(base + 9, 8'h00); // reserved
|
|
write_byte(base + 10, 8'h00); // reserved
|
|
end
|
|
endtask
|
|
|
|
// §4.3 edge: 4 bytes, src_id uint16 BE, weight int8, reserved.
|
|
task write_edge(
|
|
input [ADDR_WIDTH-1:0] base,
|
|
input [15:0] src_id,
|
|
input [7:0] weight
|
|
);
|
|
begin
|
|
write_byte(base + 0, src_id[15:8]);
|
|
write_byte(base + 1, src_id[7:0]);
|
|
write_byte(base + 2, weight);
|
|
write_byte(base + 3, 8'h00); // reserved
|
|
end
|
|
endtask
|
|
|
|
localparam TABLE_BASE = 23'h000000;
|
|
localparam N4_EDGES = 23'h000100;
|
|
localparam N5_EDGES = 23'h000108;
|
|
|
|
initial begin
|
|
|
|
errors = 0;
|
|
req = 1'b0; wr = 1'b0; addr = 0; wdata = 0;
|
|
rst = 1'b1;
|
|
repeat (5) @(posedge clk);
|
|
rst = 1'b0;
|
|
|
|
wait (psram_ctrl.state == psram_ctrl.STATE_IDLE);
|
|
|
|
$display("");
|
|
$display("========================================");
|
|
$display("GRAPH FORMAT (Type #2) BYTE-EXACT TEST");
|
|
$display("========================================");
|
|
$display("");
|
|
|
|
// ---- write the descriptor table (2 entries) ----
|
|
write_graph_desc(TABLE_BASE + 0*11, N4_EDGES, 16'd2, 16'd4, ACT_RELU, 8'sd2);
|
|
write_graph_desc(TABLE_BASE + 1*11, N5_EDGES, 16'd2, 16'd5, ACT_NONE, 8'sd0);
|
|
|
|
// ---- write the edge blocks ----
|
|
write_edge(N4_EDGES + 0*4, 16'd0, 8'sd5);
|
|
write_edge(N4_EDGES + 1*4, 16'd1, -8'sd3);
|
|
write_edge(N5_EDGES + 0*4, 16'd4, 8'sd2);
|
|
write_edge(N5_EDGES + 1*4, 16'd2, 8'sd7);
|
|
|
|
$display("-- write phase done, reading back byte-exact --");
|
|
$display("");
|
|
|
|
// ---- read back entry 0 (n4) byte-exact ----
|
|
read_byte(TABLE_BASE+0, 8'h00, "n4.conn_ptr[23:16]");
|
|
read_byte(TABLE_BASE+1, 8'h01, "n4.conn_ptr[15:8]");
|
|
read_byte(TABLE_BASE+2, 8'h00, "n4.conn_ptr[7:0]");
|
|
read_byte(TABLE_BASE+3, 8'h00, "n4.n_conn[15:8]");
|
|
read_byte(TABLE_BASE+4, 8'h02, "n4.n_conn[7:0]");
|
|
read_byte(TABLE_BASE+5, 8'h00, "n4.out_id[15:8]");
|
|
read_byte(TABLE_BASE+6, 8'h04, "n4.out_id[7:0]");
|
|
read_byte(TABLE_BASE+7, ACT_RELU, "n4.activation");
|
|
read_byte(TABLE_BASE+8, 8'h02, "n4.bias");
|
|
read_byte(TABLE_BASE+9, 8'h00, "n4.reserved0");
|
|
read_byte(TABLE_BASE+10, 8'h00, "n4.reserved1");
|
|
|
|
// ---- read back entry 1 (n5) byte-exact ----
|
|
read_byte(TABLE_BASE+11, 8'h00, "n5.conn_ptr[23:16]");
|
|
read_byte(TABLE_BASE+12, 8'h01, "n5.conn_ptr[15:8]");
|
|
read_byte(TABLE_BASE+13, 8'h08, "n5.conn_ptr[7:0]");
|
|
read_byte(TABLE_BASE+14, 8'h00, "n5.n_conn[15:8]");
|
|
read_byte(TABLE_BASE+15, 8'h02, "n5.n_conn[7:0]");
|
|
read_byte(TABLE_BASE+16, 8'h00, "n5.out_id[15:8]");
|
|
read_byte(TABLE_BASE+17, 8'h05, "n5.out_id[7:0]");
|
|
read_byte(TABLE_BASE+18, ACT_NONE, "n5.activation");
|
|
read_byte(TABLE_BASE+19, 8'h00, "n5.bias");
|
|
read_byte(TABLE_BASE+20, 8'h00, "n5.reserved0");
|
|
read_byte(TABLE_BASE+21, 8'h00, "n5.reserved1");
|
|
|
|
// ---- read back n4's edges byte-exact ----
|
|
read_byte(N4_EDGES+0, 8'h00, "n4.e0.src_id[15:8]");
|
|
read_byte(N4_EDGES+1, 8'h00, "n4.e0.src_id[7:0]");
|
|
read_byte(N4_EDGES+2, 8'h05, "n4.e0.weight");
|
|
read_byte(N4_EDGES+3, 8'h00, "n4.e0.reserved");
|
|
read_byte(N4_EDGES+4, 8'h00, "n4.e1.src_id[15:8]");
|
|
read_byte(N4_EDGES+5, 8'h01, "n4.e1.src_id[7:0]");
|
|
read_byte(N4_EDGES+6, 8'hFD, "n4.e1.weight(-3)");
|
|
read_byte(N4_EDGES+7, 8'h00, "n4.e1.reserved");
|
|
|
|
// ---- read back n5's edges byte-exact ----
|
|
read_byte(N5_EDGES+0, 8'h00, "n5.e0.src_id[15:8]");
|
|
read_byte(N5_EDGES+1, 8'h04, "n5.e0.src_id[7:0]");
|
|
read_byte(N5_EDGES+2, 8'h02, "n5.e0.weight");
|
|
read_byte(N5_EDGES+3, 8'h00, "n5.e0.reserved");
|
|
read_byte(N5_EDGES+4, 8'h00, "n5.e1.src_id[15:8]");
|
|
read_byte(N5_EDGES+5, 8'h02, "n5.e1.src_id[7:0]");
|
|
read_byte(N5_EDGES+6, 8'h07, "n5.e1.weight");
|
|
read_byte(N5_EDGES+7, 8'h00, "n5.e1.reserved");
|
|
|
|
// ---- overlap/independence sanity check: rewriting n4's
|
|
// bias must not disturb n5's descriptor or any edge byte ----
|
|
write_byte(TABLE_BASE+8, 8'sd9);
|
|
read_byte(TABLE_BASE+8, 8'h09, "n4.bias overwritten");
|
|
read_byte(TABLE_BASE+19, 8'h00, "n5.bias unaffected by n4 rewrite");
|
|
read_byte(N4_EDGES+2, 8'h05, "n4.e0.weight unaffected by n4 desc rewrite");
|
|
|
|
$display("");
|
|
if (errors == 0) begin
|
|
$display("========================================");
|
|
$display("GRAPH FORMAT TEST PASSED (0 errors)");
|
|
$display("========================================");
|
|
end else begin
|
|
$display("========================================");
|
|
$display("GRAPH FORMAT TEST FAILED (%0d errors)", errors);
|
|
$display("========================================");
|
|
$fatal;
|
|
end
|
|
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|