diff --git a/hw/rtl/cam_core_banked.sv b/hw/rtl/cam_core_banked.sv new file mode 100644 index 0000000..077a1a6 --- /dev/null +++ b/hw/rtl/cam_core_banked.sv @@ -0,0 +1,57 @@ +`timescale 1ns / 1ps +`include "cam_params.svh" + +module cam_core_banked ( + input logic clk, + input logic rst_n, + + input logic wr_valid, + output logic wr_ready, + input logic [(`ROW_BITS)-1:0] wr_row, + input logic [(`HASH_BITS)-1:0] wr_hash, + + input logic rd_valid_i, + input logic [(`ROW_BITS)-1:0] rd_base_row_i, + output logic rd_valid_o, + output logic [(`LANES)*(`ROW_BITS)-1:0] rd_row_ids_o, + output logic [(`LANES)*(`HASH_BITS)-1:0] rd_hashes_o, + output logic [(`LANES)-1:0] rd_lane_valid_o +); + localparam int BANKS = `LANES; + localparam int BANK_DEPTH = `NUM_ROWS / `LANES; + + (* ram_style = "block" *) logic [(`HASH_BITS)-1:0] bank_mem [0:BANKS-1][0:BANK_DEPTH-1]; + + assign wr_ready = 1'b1; + + initial begin + if (`NUM_ROWS % `LANES != 0) $fatal(1, "NUM_ROWS must be divisible by LANES"); + end + + always_ff @(posedge clk) begin + if (wr_valid) begin + bank_mem[wr_row % `LANES][wr_row / `LANES] <= wr_hash; + end + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + rd_valid_o <= 1'b0; + rd_row_ids_o <= '0; + rd_hashes_o <= '0; + rd_lane_valid_o <= '0; + end else begin + rd_valid_o <= rd_valid_i; + rd_lane_valid_o <= rd_valid_i ? {`LANES{1'b1}} : '0; + + if (rd_valid_i && ((rd_base_row_i % `LANES) != 0)) begin + $fatal(1, "rd_base_row_i must be LANES-aligned"); + end + + for (int lane = 0; lane < `LANES; lane++) begin + rd_row_ids_o[lane*`ROW_BITS +: `ROW_BITS] <= rd_base_row_i + lane[(`ROW_BITS)-1:0]; + rd_hashes_o[lane*`HASH_BITS +: `HASH_BITS] <= bank_mem[lane][rd_base_row_i / `LANES]; + end + end + end +endmodule diff --git a/hw/rtl/cam_noisy.sv b/hw/rtl/cam_noisy.sv index 81d02cf..438f0a2 100644 --- a/hw/rtl/cam_noisy.sv +++ b/hw/rtl/cam_noisy.sv @@ -1,189 +1,95 @@ `timescale 1ns / 1ps `include "cam_params.svh" -// Design constraints: -// HASH_BITS % NOISE_BITS == 0 -// GROUP_BITS (= HASH_BITS / NOISE_BITS) == 64 (needed for 6-bit index) -// NOISE_BITS * GROUP_RAND_BITS <= 128 -// 0 <= NOISE_RATE_NUM <= NOISE_RATE_DEN -// NOISE_RATE_DEN > 0 -// NOISE_SEED != 64'd0 -// NOISE_BITS > 0 - module cam_noisy #( - parameter bit NOISE_EN = 1'b1, - parameter int NOISE_RATE_NUM = 70, - parameter int NOISE_RATE_DEN = 100, - parameter logic [63:0] NOISE_SEED = 64'hB504_F32D_B504_F32D, - parameter int NOISE_BITS = 8 + parameter bit WRITE_NOISE_EN = 1'b1, + parameter int WRITE_NOISE_RATE_NUM = 1, + parameter int WRITE_NOISE_RATE_DEN = 100, + parameter int WRITE_NOISE_BITS = 8, + parameter logic [63:0] WRITE_NOISE_SEED = 64'hB504_F32D_B504_F32D, + parameter bit READ_NOISE_EN = 1'b1, + parameter int READ_NOISE_RATE_NUM = 1, + parameter int READ_NOISE_RATE_DEN = 100, + parameter int READ_NOISE_BITS = 8, + parameter logic [63:0] READ_NOISE_SEED = 64'h6A09_E667_F3BC_C909 ) ( - input logic clk, - input logic rst_n, - - // Write interface (handshake) - input logic wr_valid, - output logic wr_ready, - input logic [ (`ROW_BITS)-1:0] wr_addr, - input logic [(`HASH_BITS)-1:0] write_hash, - - // Read interface (passthrough to cam_core, combinational read) - input logic [ (`LANES)*(`ROW_BITS)-1:0] rd_addr_lanes_flat, - output logic [(`LANES)*(`HASH_BITS)-1:0] rd_hash_lanes_flat + input logic clk, + input logic rst_n, + input logic wr_valid, + output logic wr_ready, + input logic [(`ROW_BITS)-1:0] wr_addr, + input logic [(`HASH_BITS)-1:0] write_hash, + input logic rd_valid_i, + input logic [(`ROW_BITS)-1:0] rd_base_row_i, + output logic rd_valid_o, + output logic [(`LANES)*(`ROW_BITS)-1:0] rd_row_ids_o, + output logic [(`LANES)*(`HASH_BITS)-1:0] rd_hashes_o, + output logic [(`LANES)-1:0] rd_lane_valid_o ); - // ── Local parameters ── - localparam int GROUP_BITS = `HASH_BITS / NOISE_BITS; - localparam int BIT_INDEX_BITS = 6; - localparam int SAMPLE_BITS = 8; - localparam int GROUP_RAND_BITS = BIT_INDEX_BITS + SAMPLE_BITS; // 14 - localparam int SAMPLE_RANGE = 2 ** SAMPLE_BITS; // 256 - localparam int THRESHOLD = (NOISE_RATE_NUM * SAMPLE_RANGE) / NOISE_RATE_DEN; - - // ── Elaboration-time parameter checks ── - initial begin - if (NOISE_BITS <= 0) - $fatal(1, "NOISE_BITS must be > 0, got %0d", NOISE_BITS); - if (`HASH_BITS % NOISE_BITS != 0) - $fatal(1, "HASH_BITS (%0d) must be divisible by NOISE_BITS (%0d)", `HASH_BITS, NOISE_BITS); - if (GROUP_BITS != 64) - $fatal(1, "GROUP_BITS (= HASH_BITS/NOISE_BITS) must equal 64 for 6-bit index, got %0d", GROUP_BITS); - if (NOISE_BITS * GROUP_RAND_BITS > 128) - $fatal(1, "NOISE_BITS*GROUP_RAND_BITS must be <= 128, got %0d", NOISE_BITS * GROUP_RAND_BITS); - if (NOISE_RATE_DEN <= 0) - $fatal(1, "NOISE_RATE_DEN must be > 0, got %0d", NOISE_RATE_DEN); - if (NOISE_RATE_NUM < 0 || NOISE_RATE_NUM > NOISE_RATE_DEN) - $fatal(1, "NOISE_RATE_NUM (%0d) must be in [0, NOISE_RATE_DEN=%0d]", NOISE_RATE_NUM, NOISE_RATE_DEN); - if (NOISE_SEED == 64'd0) - $fatal(1, "NOISE_SEED must be nonzero — xorshift128/random128 with zero seed never advances"); - end - - // ── FSM states ── - typedef enum logic [1:0] { - S_IDLE, - S_GEN_MASK, - S_COMMIT - } state_t; - - state_t curr_state, next_state; - - // ── Latch registers ── - logic [(`ROW_BITS)-1:0] addr_q; - logic [(`HASH_BITS)-1:0] write_hash_q; - - // ── Noise generation ── - logic [(`HASH_BITS)-1:0] flip_mask; - logic [127:0] random_num; - - // ── Combinational next-mask helper ── - // Computes flip_mask_next from fresh random_num in one cycle. - logic [(`HASH_BITS)-1:0] flip_mask_next; - always_comb begin - flip_mask_next = '0; - for (int i = 0; i < NOISE_BITS; i++) begin - if (random_num[i * GROUP_RAND_BITS + BIT_INDEX_BITS +: SAMPLE_BITS] < THRESHOLD) begin - flip_mask_next[i * GROUP_BITS + random_num[i * GROUP_RAND_BITS +: BIT_INDEX_BITS]] = 1'b1; - end - end - end - - // ── Noisy hash for cam_core write ── - logic [(`HASH_BITS)-1:0] noisy_hash; - assign noisy_hash = write_hash_q ^ flip_mask; - - // ── wr_ready: only in IDLE ── - assign wr_ready = (curr_state == S_IDLE); - - // ── random128 enable: advance random on accepted noisy write ── - logic random_enable; - assign random_enable = wr_ready && wr_valid && NOISE_EN && (NOISE_RATE_NUM > 0); - - // ── FSM block 1: state register ── - always_ff @(posedge clk or negedge rst_n) begin - if (!rst_n) begin - curr_state <= S_IDLE; - end else begin - curr_state <= next_state; - end - end - - // ── FSM block 2: next-state logic ── - always_comb begin - next_state = curr_state; - case (curr_state) - S_IDLE: begin - if (wr_valid && wr_ready) begin - if (NOISE_EN && (NOISE_RATE_NUM > 0)) next_state = S_GEN_MASK; - else next_state = S_COMMIT; - end - end - - S_GEN_MASK: begin - next_state = S_COMMIT; - end - - S_COMMIT: begin - next_state = S_IDLE; - end - - default: next_state = S_IDLE; - endcase - end - - // ── FSM block 3: state actions / datapath registers ── - always_ff @(posedge clk or negedge rst_n) begin - if (!rst_n) begin - addr_q <= '0; - write_hash_q <= '0; - flip_mask <= '0; - end else begin - case (curr_state) - S_IDLE: begin - flip_mask <= '0; - if (wr_valid && wr_ready) begin - addr_q <= wr_addr; - write_hash_q <= write_hash; - end - end - - S_GEN_MASK: begin - flip_mask <= flip_mask_next; - end - - S_COMMIT: begin - // Write happens via combinational core_wr_en - end - - default: ; // No-op - endcase - end - end - - // ── cam_core instance ── - logic core_wr_en; - logic [ (`ROW_BITS)-1:0] core_wr_row; + // ── Intermediate wires between pipeline stages ── + logic core_wr_valid; + logic [(`ROW_BITS)-1:0] core_wr_row; logic [(`HASH_BITS)-1:0] core_wr_hash; - assign core_wr_en = (curr_state == S_COMMIT); - assign core_wr_row = addr_q; - assign core_wr_hash = noisy_hash; + logic core_rd_valid; + logic [(`LANES)*(`ROW_BITS)-1:0] core_rd_row_ids; + logic [(`LANES)*(`HASH_BITS)-1:0] core_rd_hashes; + logic [(`LANES)-1:0] core_rd_lane_valid; - cam_core u_core ( - .clk (clk), - .rst_n (rst_n), - .wr_en (core_wr_en), - .wr_row (core_wr_row), - .wr_hash (core_wr_hash), - .rd_addr_lanes_flat(rd_addr_lanes_flat), - .rd_hash_lanes_flat(rd_hash_lanes_flat) + // ── Write noise pipeline ── + cam_write_noise #( + .WRITE_NOISE_EN (WRITE_NOISE_EN), + .WRITE_NOISE_RATE_NUM (WRITE_NOISE_RATE_NUM), + .WRITE_NOISE_RATE_DEN (WRITE_NOISE_RATE_DEN), + .WRITE_NOISE_BITS (WRITE_NOISE_BITS), + .WRITE_NOISE_SEED (WRITE_NOISE_SEED) + ) u_write_noise ( + .clk (clk), + .rst_n (rst_n), + .wr_valid (wr_valid), + .wr_ready (wr_ready), + .wr_row (wr_addr), + .wr_hash (write_hash), + .core_wr_valid (core_wr_valid), + .core_wr_row (core_wr_row), + .core_wr_hash (core_wr_hash) ); - // ── Random number generator ── - random128 u_random_128 ( - .clk (clk), - .rst_n (rst_n), - .enable(random_enable), - .seed ({NOISE_SEED, NOISE_SEED}), - .out (random_num) + // ── Banked synchronous BRAM storage ── + cam_core_banked u_core_banked ( + .clk (clk), + .rst_n (rst_n), + .wr_valid (core_wr_valid), + .wr_ready (), + .wr_row (core_wr_row), + .wr_hash (core_wr_hash), + .rd_valid_i (rd_valid_i), + .rd_base_row_i (rd_base_row_i), + .rd_valid_o (core_rd_valid), + .rd_row_ids_o (core_rd_row_ids), + .rd_hashes_o (core_rd_hashes), + .rd_lane_valid_o (core_rd_lane_valid) + ); + + // ── Read noise pipeline ── + cam_read_noise #( + .READ_NOISE_EN (READ_NOISE_EN), + .READ_NOISE_RATE_NUM (READ_NOISE_RATE_NUM), + .READ_NOISE_RATE_DEN (READ_NOISE_RATE_DEN), + .READ_NOISE_BITS (READ_NOISE_BITS), + .READ_NOISE_SEED (READ_NOISE_SEED) + ) u_read_noise ( + .clk (clk), + .rst_n (rst_n), + .valid_i (core_rd_valid), + .row_ids_i (core_rd_row_ids), + .hashes_i (core_rd_hashes), + .lane_valid_i (core_rd_lane_valid), + .valid_o (rd_valid_o), + .row_ids_o (rd_row_ids_o), + .hashes_noisy_o (rd_hashes_o), + .lane_valid_o (rd_lane_valid_o) ); endmodule diff --git a/hw/rtl/cam_params.svh b/hw/rtl/cam_params.svh index 865a2dc..cf62025 100644 --- a/hw/rtl/cam_params.svh +++ b/hw/rtl/cam_params.svh @@ -21,7 +21,7 @@ // Number of CAM rows. `ifndef NUM_ROWS -`define NUM_ROWS 512 +`define NUM_ROWS 4096 `endif // Width of the hash value stored per row. @@ -31,7 +31,7 @@ // Number of parallel comparison lanes. `ifndef LANES -`define LANES 16 +`define LANES 8 `endif // Bits required to represent a row index. @@ -48,7 +48,7 @@ // Used as the initial best-index value before any match is found. localparam logic [(`ROW_BITS)-1:0] TIE_BREAK_SENTINEL = {(`ROW_BITS){1'b1}}; -// Current fixed parameters require NUM_ROWS divisible by LANES. -// Non-divisible tail-group valid-mask is not implemented in this round. +// First banked-pipeline implementation requires NUM_ROWS divisible by LANES. +// Tail lanes and unaligned read batches are intentionally out of scope. `endif // CAM_PARAMS_SVH diff --git a/hw/rtl/cam_read_noise.sv b/hw/rtl/cam_read_noise.sv new file mode 100644 index 0000000..bf6da56 --- /dev/null +++ b/hw/rtl/cam_read_noise.sv @@ -0,0 +1,85 @@ +`timescale 1ns / 1ps +`include "cam_params.svh" + +module cam_read_noise #( + parameter bit READ_NOISE_EN = 1'b1, + parameter int READ_NOISE_RATE_NUM = 1, + parameter int READ_NOISE_RATE_DEN = 100, + parameter int READ_NOISE_BITS = 8, + parameter logic [63:0] READ_NOISE_SEED = 64'h6A09_E667_F3BC_C909 +) ( + input logic clk, + input logic rst_n, + input logic valid_i, + input logic [(`LANES)*(`ROW_BITS)-1:0] row_ids_i, + input logic [(`LANES)*(`HASH_BITS)-1:0] hashes_i, + input logic [(`LANES)-1:0] lane_valid_i, + output logic valid_o, + output logic [(`LANES)*(`ROW_BITS)-1:0] row_ids_o, + output logic [(`LANES)*(`HASH_BITS)-1:0] hashes_noisy_o, + output logic [(`LANES)-1:0] lane_valid_o +); + logic valid_q; + logic [(`LANES)*(`ROW_BITS)-1:0] row_ids_q; + logic [(`LANES)*(`HASH_BITS)-1:0] hashes_q; + logic [(`LANES)-1:0] lane_valid_q; + + logic [127:0] random_num [0:`LANES-1]; + logic [(`HASH_BITS)-1:0] mask [0:`LANES-1]; + + initial begin + if (READ_NOISE_SEED == 64'd0) $fatal(1, "READ_NOISE_SEED must be nonzero"); + end + + generate + for (genvar lane = 0; lane < `LANES; lane++) begin : gen_lane_noise + localparam logic [63:0] LANE_SALT = 64'(lane + 1) * 64'h9E37_79B9_7F4A_7C15; + random128 u_random_read ( + .clk (clk), + .rst_n (rst_n), + .enable(valid_i && lane_valid_i[lane] && READ_NOISE_EN && (READ_NOISE_RATE_NUM > 0)), + .seed ({(READ_NOISE_SEED ^ LANE_SALT), (READ_NOISE_SEED ^ LANE_SALT)}), + .out (random_num[lane]) + ); + + noise_mask_grouped #( + .HASH_BITS (`HASH_BITS), + .NOISE_BITS (READ_NOISE_BITS), + .NOISE_RATE_NUM (READ_NOISE_RATE_NUM), + .NOISE_RATE_DEN (READ_NOISE_RATE_DEN) + ) u_mask ( + .random_i(random_num[lane]), + .mask_o (mask[lane]) + ); + end + endgenerate + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + valid_q <= 1'b0; + row_ids_q <= '0; + hashes_q <= '0; + lane_valid_q <= '0; + valid_o <= 1'b0; + row_ids_o <= '0; + hashes_noisy_o <= '0; + lane_valid_o <= '0; + end else begin + valid_q <= valid_i; + row_ids_q <= row_ids_i; + hashes_q <= hashes_i; + lane_valid_q <= lane_valid_i; + + valid_o <= valid_q; + row_ids_o <= row_ids_q; + lane_valid_o <= lane_valid_q; + for (int lane = 0; lane < `LANES; lane++) begin + if (READ_NOISE_EN && (READ_NOISE_RATE_NUM > 0) && lane_valid_q[lane]) begin + hashes_noisy_o[lane*`HASH_BITS +: `HASH_BITS] <= hashes_q[lane*`HASH_BITS +: `HASH_BITS] ^ mask[lane]; + end else begin + hashes_noisy_o[lane*`HASH_BITS +: `HASH_BITS] <= hashes_q[lane*`HASH_BITS +: `HASH_BITS]; + end + end + end + end +endmodule diff --git a/hw/rtl/cam_top.sv b/hw/rtl/cam_top.sv index 64b5362..0e19a2a 100644 --- a/hw/rtl/cam_top.sv +++ b/hw/rtl/cam_top.sv @@ -2,11 +2,16 @@ `include "cam_params.svh" module cam_top #( - parameter bit NOISE_EN = 1'b1, - parameter int NOISE_RATE_NUM = 1, - parameter int NOISE_RATE_DEN = 100, - parameter int NOISE_BITS = 8, - parameter logic [63:0] NOISE_SEED = 64'hB504_F32D_B504_F32D + parameter bit WRITE_NOISE_EN = 1'b1, + parameter int WRITE_NOISE_RATE_NUM = 1, + parameter int WRITE_NOISE_RATE_DEN = 100, + parameter int WRITE_NOISE_BITS = 8, + parameter logic [63:0] WRITE_NOISE_SEED = 64'hB504_F32D_B504_F32D, + parameter bit READ_NOISE_EN = 1'b0, + parameter int READ_NOISE_RATE_NUM = 1, + parameter int READ_NOISE_RATE_DEN = 100, + parameter int READ_NOISE_BITS = 8, + parameter logic [63:0] READ_NOISE_SEED = 64'h6A09_E667_F3BC_C909 ) ( input logic clk, input logic rst_n, @@ -36,7 +41,7 @@ module cam_top #( ); // ── Internal signals ── - logic storage_wr_ready; // cam_noisy idle + logic storage_wr_ready; // cam_noisy write-side ready logic match_query_ready; // match_engine idle logic match_busy; // match_engine scanning/result pending @@ -45,8 +50,8 @@ module cam_top #( logic match_query_valid; // ── Half-duplex arbitration (write-priority) ── - // When both wr_valid and query_valid are high, write wins. - assign wr_ready = storage_wr_ready && match_query_ready; + // Active query scan blocks new writes. + assign wr_ready = storage_wr_ready && match_query_ready && !match_busy; assign query_ready = storage_wr_ready && match_query_ready && !wr_valid; assign busy = (!storage_wr_ready) || match_busy || (!match_query_ready); @@ -54,42 +59,59 @@ module cam_top #( assign storage_wr_valid = wr_valid && wr_ready; assign match_query_valid = query_valid && query_ready; - // ── Shared read bus ── - wire [(`LANES)*(`ROW_BITS)-1:0] rd_addr_lanes_flat; - wire [(`LANES)*(`HASH_BITS)-1:0] rd_hash_lanes_flat; + // ── Read request/response bus between cam_noisy and match_engine_pipeline ── + wire rd_req_valid; + wire [(`ROW_BITS)-1:0] rd_req_base_row; + wire rd_resp_valid; + wire [(`LANES)*(`ROW_BITS)-1:0] rd_resp_row_ids; + wire [(`LANES)*(`HASH_BITS)-1:0] rd_resp_hashes; + wire [(`LANES)-1:0] rd_resp_lane_valid; cam_noisy #( - .NOISE_EN (NOISE_EN), - .NOISE_RATE_NUM (NOISE_RATE_NUM), - .NOISE_RATE_DEN (NOISE_RATE_DEN), - .NOISE_BITS (NOISE_BITS), - .NOISE_SEED (NOISE_SEED) + .WRITE_NOISE_EN (WRITE_NOISE_EN), + .WRITE_NOISE_RATE_NUM (WRITE_NOISE_RATE_NUM), + .WRITE_NOISE_RATE_DEN (WRITE_NOISE_RATE_DEN), + .WRITE_NOISE_BITS (WRITE_NOISE_BITS), + .WRITE_NOISE_SEED (WRITE_NOISE_SEED), + .READ_NOISE_EN (READ_NOISE_EN), + .READ_NOISE_RATE_NUM (READ_NOISE_RATE_NUM), + .READ_NOISE_RATE_DEN (READ_NOISE_RATE_DEN), + .READ_NOISE_BITS (READ_NOISE_BITS), + .READ_NOISE_SEED (READ_NOISE_SEED) ) u_noisy ( - .clk (clk), - .rst_n (rst_n), - .wr_valid (storage_wr_valid), - .wr_ready (storage_wr_ready), - .wr_addr (wr_addr), - .write_hash (write_hash), - .rd_addr_lanes_flat (rd_addr_lanes_flat), - .rd_hash_lanes_flat (rd_hash_lanes_flat) + .clk (clk), + .rst_n (rst_n), + .wr_valid (storage_wr_valid), + .wr_ready (storage_wr_ready), + .wr_addr (wr_addr), + .write_hash (write_hash), + .rd_valid_i (rd_req_valid), + .rd_base_row_i (rd_req_base_row), + .rd_valid_o (rd_resp_valid), + .rd_row_ids_o (rd_resp_row_ids), + .rd_hashes_o (rd_resp_hashes), + .rd_lane_valid_o (rd_resp_lane_valid) ); - match_engine u_match ( - .clk (clk), - .rst_n (rst_n), - .query_valid (match_query_valid), - .query_ready (match_query_ready), - .query_hash (query_hash), - .result_valid (result_valid), - .result_ready (result_ready), - .result_row (top1_index), - .result_score (top1_score), - .busy (match_busy), - .rd_addr_lanes_flat (rd_addr_lanes_flat), - .rd_hash_lanes_flat (rd_hash_lanes_flat) + match_engine_pipeline u_match ( + .clk (clk), + .rst_n (rst_n), + .query_valid (match_query_valid), + .query_ready (match_query_ready), + .query_hash (query_hash), + .result_valid (result_valid), + .result_ready (result_ready), + .result_row (top1_index), + .result_score (top1_score), + .busy (match_busy), + .rd_valid_o (rd_req_valid), + .rd_base_row_o (rd_req_base_row), + .rd_valid_i (rd_resp_valid), + .rd_row_ids_i (rd_resp_row_ids), + .rd_hashes_i (rd_resp_hashes), + .rd_lane_valid_i (rd_resp_lane_valid) `ifdef SIM_DEBUG - ,.score_debug_flat (score_debug_flat) + ,.score_debug_flat (score_debug_flat) `endif ); diff --git a/hw/rtl/cam_write_noise.sv b/hw/rtl/cam_write_noise.sv new file mode 100644 index 0000000..e4e405e --- /dev/null +++ b/hw/rtl/cam_write_noise.sv @@ -0,0 +1,75 @@ +`timescale 1ns / 1ps +`include "cam_params.svh" + +module cam_write_noise #( + parameter bit WRITE_NOISE_EN = 1'b1, + parameter int WRITE_NOISE_RATE_NUM = 1, + parameter int WRITE_NOISE_RATE_DEN = 100, + parameter int WRITE_NOISE_BITS = 8, + parameter logic [63:0] WRITE_NOISE_SEED = 64'hB504_F32D_B504_F32D +) ( + input logic clk, + input logic rst_n, + input logic wr_valid, + output logic wr_ready, + input logic [(`ROW_BITS)-1:0] wr_row, + input logic [(`HASH_BITS)-1:0] wr_hash, + output logic core_wr_valid, + output logic [(`ROW_BITS)-1:0] core_wr_row, + output logic [(`HASH_BITS)-1:0] core_wr_hash +); + logic pending_q; + logic [(`ROW_BITS)-1:0] row_q; + logic [(`HASH_BITS)-1:0] hash_q; + logic [127:0] random_num; + logic [(`HASH_BITS)-1:0] flip_mask; + + assign wr_ready = !pending_q; + + random128 u_random_write ( + .clk (clk), + .rst_n (rst_n), + .enable(wr_valid && wr_ready && WRITE_NOISE_EN && (WRITE_NOISE_RATE_NUM > 0)), + .seed ({WRITE_NOISE_SEED, WRITE_NOISE_SEED}), + .out (random_num) + ); + + noise_mask_grouped #( + .HASH_BITS (`HASH_BITS), + .NOISE_BITS (WRITE_NOISE_BITS), + .NOISE_RATE_NUM (WRITE_NOISE_RATE_NUM), + .NOISE_RATE_DEN (WRITE_NOISE_RATE_DEN) + ) u_mask ( + .random_i(random_num), + .mask_o (flip_mask) + ); + + initial begin + if (WRITE_NOISE_SEED == 64'd0) $fatal(1, "WRITE_NOISE_SEED must be nonzero"); + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + pending_q <= 1'b0; + row_q <= '0; + hash_q <= '0; + core_wr_valid <= 1'b0; + core_wr_row <= '0; + core_wr_hash <= '0; + end else begin + core_wr_valid <= pending_q; + core_wr_row <= row_q; + if (WRITE_NOISE_EN && (WRITE_NOISE_RATE_NUM > 0)) begin + core_wr_hash <= hash_q ^ flip_mask; + end else begin + core_wr_hash <= hash_q; + end + + pending_q <= wr_valid && wr_ready; + if (wr_valid && wr_ready) begin + row_q <= wr_row; + hash_q <= wr_hash; + end + end + end +endmodule diff --git a/hw/rtl/match_engine_pipeline.sv b/hw/rtl/match_engine_pipeline.sv new file mode 100644 index 0000000..481a89e --- /dev/null +++ b/hw/rtl/match_engine_pipeline.sv @@ -0,0 +1,140 @@ +`timescale 1ns / 1ps +`include "cam_params.svh" + +module match_engine_pipeline ( + input logic clk, + input logic rst_n, + input logic query_valid, + output logic query_ready, + input logic [(`HASH_BITS)-1:0] query_hash, + output logic result_valid, + input logic result_ready, + output logic [(`ROW_BITS)-1:0] result_row, + output logic [(`SCORE_BITS)-1:0] result_score, + output logic busy, + output logic rd_valid_o, + output logic [(`ROW_BITS)-1:0] rd_base_row_o, + input logic rd_valid_i, + input logic [(`LANES)*(`ROW_BITS)-1:0] rd_row_ids_i, + input logic [(`LANES)*(`HASH_BITS)-1:0] rd_hashes_i, + input logic [(`LANES)-1:0] rd_lane_valid_i +`ifdef SIM_DEBUG + ,output logic [(`NUM_ROWS)*(`SCORE_BITS)-1:0] score_debug_flat +`endif +); + typedef enum logic [1:0] {S_IDLE, S_SCAN, S_DRAIN, S_DONE} state_t; + state_t state_q; + + logic [(`HASH_BITS)-1:0] query_q; + logic [(`ROW_BITS)-1:0] issue_base_q; + logic [$clog2(`NUM_ROWS/`LANES+1)-1:0] returned_q; + logic [(`ROW_BITS)-1:0] best_row_q; + logic [(`SCORE_BITS)-1:0] best_score_q; + + logic [(`HASH_BITS)-1:0] match_bits [0:`LANES-1]; + logic score_valid [0:`LANES-1]; + logic [(`ROW_BITS)-1:0] score_row [0:`LANES-1]; + logic [(`SCORE_BITS)-1:0] lane_score [0:`LANES-1]; + logic [(`ROW_BITS)-1:0] batch_row; + logic [(`SCORE_BITS)-1:0] batch_score; + + assign query_ready = (state_q == S_IDLE); + assign result_valid = (state_q == S_DONE); + assign result_row = best_row_q; + assign result_score = best_score_q; + assign busy = (state_q != S_IDLE); + assign rd_valid_o = (state_q == S_SCAN); + assign rd_base_row_o = issue_base_q; + + generate + for (genvar lane = 0; lane < `LANES; lane++) begin : gen_scores + assign match_bits[lane] = ~(query_q ^ rd_hashes_i[lane*`HASH_BITS +: `HASH_BITS]); + popcount_pipeline #( + .WIDTH(`HASH_BITS), + .ROW_BITS(`ROW_BITS), + .OUT_WIDTH(`SCORE_BITS) + ) u_popcount_pipeline ( + .clk(clk), + .rst_n(rst_n), + .valid_i(rd_valid_i && rd_lane_valid_i[lane]), + .row_i(rd_row_ids_i[lane*`ROW_BITS +: `ROW_BITS]), + .bits_i(match_bits[lane]), + .valid_o(score_valid[lane]), + .row_o(score_row[lane]), + .count_o(lane_score[lane]) + ); + end + endgenerate + + always_comb begin + batch_row = score_row[0]; + batch_score = lane_score[0]; + for (int lane = 1; lane < `LANES; lane++) begin + if ((lane_score[lane] > batch_score) || + ((lane_score[lane] == batch_score) && (score_row[lane] < batch_row))) begin + batch_score = lane_score[lane]; + batch_row = score_row[lane]; + end + end + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + state_q <= S_IDLE; + query_q <= '0; + issue_base_q <= '0; + returned_q <= 0; + best_row_q <= '0; + best_score_q <= '0; +`ifdef SIM_DEBUG + score_debug_flat <= '0; +`endif + end else begin + unique case (state_q) + S_IDLE: begin + if (query_valid) begin + query_q <= query_hash; + issue_base_q <= '0; + returned_q <= 0; + best_row_q <= TIE_BREAK_SENTINEL; + best_score_q <= '0; +`ifdef SIM_DEBUG + score_debug_flat <= '0; +`endif + state_q <= S_SCAN; + end + end + S_SCAN: begin + if (issue_base_q + `LANES >= `NUM_ROWS) begin + state_q <= S_DRAIN; + end else begin + issue_base_q <= issue_base_q + `LANES; + end + end + S_DRAIN: begin + if ((returned_q == (`NUM_ROWS / `LANES)) && !score_valid[0]) begin + state_q <= S_DONE; + end + end + S_DONE: begin + if (result_ready) state_q <= S_IDLE; + end + default: state_q <= S_IDLE; + endcase + + if (score_valid[0]) begin + returned_q <= returned_q + 1'b1; + if ((batch_score > best_score_q) || + ((batch_score == best_score_q) && (batch_row < best_row_q))) begin + best_score_q <= batch_score; + best_row_q <= batch_row; + end +`ifdef SIM_DEBUG + for (int lane = 0; lane < `LANES; lane++) begin + score_debug_flat[score_row[lane]*`SCORE_BITS +: `SCORE_BITS] <= lane_score[lane]; + end +`endif + end + end + end +endmodule diff --git a/hw/rtl/noise_mask_grouped.sv b/hw/rtl/noise_mask_grouped.sv new file mode 100644 index 0000000..efc246d --- /dev/null +++ b/hw/rtl/noise_mask_grouped.sv @@ -0,0 +1,37 @@ +`timescale 1ns / 1ps +`include "cam_params.svh" + +module noise_mask_grouped #( + parameter int HASH_BITS = `HASH_BITS, + parameter int NOISE_BITS = 8, + parameter int NOISE_RATE_NUM = 1, + parameter int NOISE_RATE_DEN = 100 +) ( + input logic [127:0] random_i, + output logic [HASH_BITS-1:0] mask_o +); + localparam int GROUP_BITS = HASH_BITS / NOISE_BITS; + localparam int BIT_INDEX_BITS = 6; + localparam int SAMPLE_BITS = 8; + localparam int GROUP_RAND_BITS = BIT_INDEX_BITS + SAMPLE_BITS; + localparam int SAMPLE_RANGE = 1 << SAMPLE_BITS; + localparam int THRESHOLD = (NOISE_RATE_NUM * SAMPLE_RANGE) / NOISE_RATE_DEN; + + initial begin + if (NOISE_BITS <= 0) $fatal(1, "NOISE_BITS must be > 0"); + if (HASH_BITS % NOISE_BITS != 0) $fatal(1, "HASH_BITS must be divisible by NOISE_BITS"); + if (GROUP_BITS != 64) $fatal(1, "GROUP_BITS must be 64 for 6-bit grouped noise"); + if (NOISE_BITS * GROUP_RAND_BITS > 128) $fatal(1, "NOISE_BITS consumes more than 128 random bits"); + if (NOISE_RATE_DEN <= 0) $fatal(1, "NOISE_RATE_DEN must be > 0"); + if (NOISE_RATE_NUM < 0 || NOISE_RATE_NUM > NOISE_RATE_DEN) $fatal(1, "NOISE_RATE_NUM out of range"); + end + + always_comb begin + mask_o = '0; + for (int i = 0; i < NOISE_BITS; i++) begin + if (random_i[i * GROUP_RAND_BITS + BIT_INDEX_BITS +: SAMPLE_BITS] < THRESHOLD) begin + mask_o[i * GROUP_BITS + random_i[i * GROUP_RAND_BITS +: BIT_INDEX_BITS]] = 1'b1; + end + end + end +endmodule diff --git a/hw/rtl/popcount_pipeline.sv b/hw/rtl/popcount_pipeline.sv new file mode 100644 index 0000000..3c5b637 --- /dev/null +++ b/hw/rtl/popcount_pipeline.sv @@ -0,0 +1,72 @@ +`timescale 1ns / 1ps + +module popcount_pipeline #( + parameter int WIDTH = 512, + parameter int ROW_BITS = 12, + parameter int OUT_WIDTH = $clog2(WIDTH + 1) +) ( + input logic clk, + input logic rst_n, + input logic valid_i, + input logic [ROW_BITS-1:0] row_i, + input logic [WIDTH-1:0] bits_i, + output logic valid_o, + output logic [ROW_BITS-1:0] row_o, + output logic [OUT_WIDTH-1:0] count_o +); + localparam int GROUP = 8; + localparam int NUM_GROUPS = WIDTH / GROUP; + localparam int GROUP_COUNT_WIDTH = $clog2(GROUP + 1); + localparam int STAGE1_GROUPS = NUM_GROUPS / 4; + + logic [GROUP_COUNT_WIDTH-1:0] group_counts [0:NUM_GROUPS-1]; + logic [OUT_WIDTH-1:0] partial_q [0:3]; + logic [OUT_WIDTH-1:0] sum_q; + logic [ROW_BITS-1:0] row_s1_q, row_s2_q; + logic valid_s1_q, valid_s2_q; + logic [OUT_WIDTH-1:0] partial_comb [0:3]; + + always_comb begin + for (int g = 0; g < NUM_GROUPS; g++) begin + group_counts[g] = '0; + for (int b = 0; b < GROUP; b++) begin + group_counts[g] = group_counts[g] + bits_i[g*GROUP + b]; + end + end + + for (int p = 0; p < 4; p++) begin + partial_comb[p] = '0; + for (int g = 0; g < STAGE1_GROUPS; g++) begin + partial_comb[p] = partial_comb[p] + group_counts[p*STAGE1_GROUPS + g]; + end + end + end + + always_ff @(posedge clk or negedge rst_n) begin + if (!rst_n) begin + valid_s1_q <= 1'b0; + valid_s2_q <= 1'b0; + valid_o <= 1'b0; + row_s1_q <= '0; + row_s2_q <= '0; + row_o <= '0; + sum_q <= '0; + count_o <= '0; + for (int p = 0; p < 4; p++) partial_q[p] <= '0; + end else begin + valid_s1_q <= valid_i; + row_s1_q <= row_i; + for (int p = 0; p < 4; p++) begin + partial_q[p] <= partial_comb[p]; + end + + valid_s2_q <= valid_s1_q; + row_s2_q <= row_s1_q; + sum_q <= partial_q[0] + partial_q[1] + partial_q[2] + partial_q[3]; + + valid_o <= valid_s2_q; + row_o <= row_s2_q; + count_o <= sum_q; + end + end +endmodule diff --git a/hw/sim/Makefile b/hw/sim/Makefile index ccffbd2..7192c40 100644 --- a/hw/sim/Makefile +++ b/hw/sim/Makefile @@ -1,22 +1,12 @@ SIM ?= verilator TOPLEVEL_LANG ?= verilog -TOPLEVEL := cam_top +TOPLEVEL ?= cam_top -# MODULE ?= tests.test_cam_basic COCOTB_TEST_MODULES ?= tests.test_cam_basic -NUM_ROWS ?= 512 +NUM_ROWS ?= 4096 HASH_BITS ?= 512 -LANES ?= 16 - -# Noise parameters -NOISE_EN ?= 1 -NOISE_RATE_NUM ?= 1 -NOISE_RATE_DEN ?= 100 -NOISE_BITS ?= 8 -# NOISE_SEED cannot be overridden via Makefile due to Verilator -G quoting issues -# with 64'h hex literals. To change the seed, edit the default in cam_top.sv -# (sim top is cam_top, which passes it to cam_noisy). +LANES ?= 8 EXTRA_ARGS += +define+NUM_ROWS=$(NUM_ROWS) +define+HASH_BITS=$(HASH_BITS) +define+LANES=$(LANES) @@ -25,11 +15,42 @@ COMPILE_ARGS += -I$(PWD)/../rtl COMPILE_ARGS += +define+SIM_DEBUG COMPILE_ARGS += $(EXTRA_DEFINES) -# Noise parameter overrides via Verilator -G -COMPILE_ARGS += -GNOISE_EN=$(NOISE_EN) -COMPILE_ARGS += -GNOISE_RATE_NUM=$(NOISE_RATE_NUM) -COMPILE_ARGS += -GNOISE_RATE_DEN=$(NOISE_RATE_DEN) -COMPILE_ARGS += -GNOISE_BITS=$(NOISE_BITS) +# Noise parameters — only passed when explicitly set (non-empty). +# For cam_noisy/cam_top tests, pass WRITE_NOISE_*=... READ_NOISE_*=... +# For individual module tests, leave them unset to skip. +WRITE_NOISE_EN ?= $(NOISE_EN) +WRITE_NOISE_RATE_NUM ?= $(NOISE_RATE_NUM) +WRITE_NOISE_RATE_DEN ?= $(NOISE_RATE_DEN) +WRITE_NOISE_BITS ?= $(NOISE_BITS) +READ_NOISE_EN ?= +READ_NOISE_RATE_NUM ?= +READ_NOISE_RATE_DEN ?= +READ_NOISE_BITS ?= + +ifneq ($(strip $(WRITE_NOISE_EN)),) +COMPILE_ARGS += -GWRITE_NOISE_EN=$(WRITE_NOISE_EN) +endif +ifneq ($(strip $(WRITE_NOISE_RATE_NUM)),) +COMPILE_ARGS += -GWRITE_NOISE_RATE_NUM=$(WRITE_NOISE_RATE_NUM) +endif +ifneq ($(strip $(WRITE_NOISE_RATE_DEN)),) +COMPILE_ARGS += -GWRITE_NOISE_RATE_DEN=$(WRITE_NOISE_RATE_DEN) +endif +ifneq ($(strip $(WRITE_NOISE_BITS)),) +COMPILE_ARGS += -GWRITE_NOISE_BITS=$(WRITE_NOISE_BITS) +endif +ifneq ($(strip $(READ_NOISE_EN)),) +COMPILE_ARGS += -GREAD_NOISE_EN=$(READ_NOISE_EN) +endif +ifneq ($(strip $(READ_NOISE_RATE_NUM)),) +COMPILE_ARGS += -GREAD_NOISE_RATE_NUM=$(READ_NOISE_RATE_NUM) +endif +ifneq ($(strip $(READ_NOISE_RATE_DEN)),) +COMPILE_ARGS += -GREAD_NOISE_RATE_DEN=$(READ_NOISE_RATE_DEN) +endif +ifneq ($(strip $(READ_NOISE_BITS)),) +COMPILE_ARGS += -GREAD_NOISE_BITS=$(READ_NOISE_BITS) +endif # Cleaner terminal output export QUIET ?= 1 @@ -48,6 +69,12 @@ VERILOG_SOURCES += $(PWD)/../rtl/popcount.sv VERILOG_SOURCES += $(PWD)/../rtl/argmax_update.sv VERILOG_SOURCES += $(PWD)/../rtl/cam_core.sv VERILOG_SOURCES += $(PWD)/../rtl/random/random128.sv +VERILOG_SOURCES += $(PWD)/../rtl/noise_mask_grouped.sv +VERILOG_SOURCES += $(PWD)/../rtl/cam_core_banked.sv +VERILOG_SOURCES += $(PWD)/../rtl/cam_write_noise.sv +VERILOG_SOURCES += $(PWD)/../rtl/cam_read_noise.sv +VERILOG_SOURCES += $(PWD)/../rtl/popcount_pipeline.sv +VERILOG_SOURCES += $(PWD)/../rtl/match_engine_pipeline.sv VERILOG_SOURCES += $(PWD)/../rtl/cam_noisy.sv VERILOG_SOURCES += $(PWD)/../rtl/match_engine.sv VERILOG_SOURCES += $(PWD)/../rtl/cam_top.sv diff --git a/hw/sim/model/ref_model.py b/hw/sim/model/ref_model.py index 52c935a..1a3a01c 100644 --- a/hw/sim/model/ref_model.py +++ b/hw/sim/model/ref_model.py @@ -101,6 +101,130 @@ def generate_write_flip_mask( return mask, state +def generate_grouped_flip_mask( + *, + random_value: int, + hash_bits: int, + noise_bits: int, + rate_num: int, + rate_den: int, +) -> int: + """Generate a grouped flip mask from one 128-bit value. + + This is the shared write/read noise model: 8 default 64-bit groups, one + candidate flip per group, 6-bit bit index and 8-bit threshold sample. + It is not independent Bernoulli sampling over all 512 bits. + """ + assert noise_bits > 0 + assert hash_bits % noise_bits == 0 + group_bits = hash_bits // noise_bits + bit_index_bits = 6 + sample_bits = 8 + group_random_bits = bit_index_bits + sample_bits + assert group_bits == 64 + assert noise_bits * group_random_bits <= 128 + assert rate_den > 0 + assert 0 <= rate_num <= rate_den + + sample_range = 1 << sample_bits + threshold = (rate_num * sample_range) // rate_den + mask = 0 + + for group_idx in range(noise_bits): + group_rand = (random_value >> (group_idx * group_random_bits)) & ((1 << group_random_bits) - 1) + bit_idx = group_rand & ((1 << bit_index_bits) - 1) + sample = (group_rand >> bit_index_bits) & (sample_range - 1) + if sample < threshold: + mask |= 1 << (group_idx * group_bits + bit_idx) + + return mask + + +def lane_seed_128(seed: int, lane: int) -> int: + """Derive a nonzero 128-bit lane seed matching the RTL salt convention.""" + mask128 = (1 << 128) - 1 + salt = ((lane + 1) * 0x9E37_79B9_7F4A_7C15) & ((1 << 64) - 1) + mixed64 = (int(seed) ^ salt) & ((1 << 64) - 1) + state = ((mixed64 << 64) | mixed64) & mask128 + assert state != 0 + return state + + +def generate_read_lane_masks( + lane_states: list[int], + *, + hash_bits: int, + noise_bits: int, + rate_num: int, + rate_den: int, + lane_valid: list[bool], +) -> tuple[list[int], list[int]]: + """Advance valid lane PRNG states once and return one mask per lane.""" + next_states: list[int] = [] + masks: list[int] = [] + + for lane, state in enumerate(lane_states): + if lane_valid[lane]: + next_state = xorshift128(state) + mask = generate_grouped_flip_mask( + random_value=next_state, + hash_bits=hash_bits, + noise_bits=noise_bits, + rate_num=rate_num, + rate_den=rate_den, + ) + else: + next_state = state + mask = 0 + next_states.append(next_state) + masks.append(mask) + + return masks, next_states + + +def match_top1_with_read_noise( + query: int, + rows: Sequence[int], + *, + width: int = 512, + lanes: int = 8, + noise_bits: int = 8, + rate_num: int = 1, + rate_den: int = 100, + seed: int = 0x6A09_E667_F3BC_C909, +) -> MatchResult: + """Top-1 matching with dynamic read noise, one query in flight.""" + assert lanes > 0 + assert len(rows) % lanes == 0 + + scores = np.zeros(len(rows), dtype=np.int32) + best_index = 0 + best_score = -1 + lane_states = [lane_seed_128(seed, lane) for lane in range(lanes)] + + for base in range(0, len(rows), lanes): + lane_valid = [True] * lanes + masks, lane_states = generate_read_lane_masks( + lane_states, + hash_bits=width, + noise_bits=noise_bits, + rate_num=rate_num, + rate_den=rate_den, + lane_valid=lane_valid, + ) + + for lane in range(lanes): + row_idx = base + lane + noisy_row = int(rows[row_idx]) ^ masks[lane] + score = xnor_popcount_score(int(query), noisy_row, width) + scores[row_idx] = score + if score > best_score: + best_score = score + best_index = row_idx + + return MatchResult(top1_index=int(best_index), top1_score=int(best_score), scores=scores) + + def random_hashes( rng: np.random.Generator, n: int, diff --git a/hw/sim/tests/test_cam_basic.py b/hw/sim/tests/test_cam_basic.py index 395b899..80ac3ec 100644 --- a/hw/sim/tests/test_cam_basic.py +++ b/hw/sim/tests/test_cam_basic.py @@ -7,14 +7,15 @@ from cocotb.triggers import RisingEdge from model.ref_model import ( # noqa: E402 generate_write_flip_mask, match_top1, + match_top1_with_read_noise, random_hashes, unpack_score_debug_flat, ) -NUM_ROWS = 512 -HASH_BITS = 512 -LANES = 16 -SCORE_BITS = 10 +DEFAULT_NUM_ROWS = 4096 +DEFAULT_HASH_BITS = 512 +DEFAULT_LANES = 8 +DEFAULT_SCORE_BITS = 10 def _get_param(dut, name, default=None): @@ -28,6 +29,38 @@ def _get_param(dut, name, default=None): return default +def dut_num_rows(dut): + val = _get_param(dut, "NUM_ROWS", None) + if val is not None: + return val + # Derive from wr_addr width (ROW_BITS): NUM_ROWS = 2^ROW_BITS + return 1 << len(dut.wr_addr) + + +def dut_hash_bits(dut): + val = _get_param(dut, "HASH_BITS", None) + if val is not None: + return val + # Derive from write_hash signal width + return len(dut.write_hash) + + +def dut_lanes(dut): + val = _get_param(dut, "LANES", None) + if val is not None: + return val + # Derive from rd_resp_row_ids width / ROW_BITS + return len(dut.rd_resp_row_ids) // len(dut.wr_addr) + + +def dut_score_bits(dut): + val = _get_param(dut, "SCORE_BITS", None) + if val is not None: + return val + # Derive from top1_score signal width + return len(dut.top1_score) + + # ── Helpers ────────────────────────────────────────────────────────────────── @@ -57,14 +90,7 @@ async def wait_idle(dut): async def write_row(dut, addr, value): - """Write a single row using wr_valid/wr_ready handshake. - - 1. Wait for system idle - 2. Assert wr_valid + set addr/hash - 3. Wait for handshake (wr_ready=1 on clock edge) - 4. Deassert wr_valid - 5. Wait for wr_ready to return 1 (commit complete) - """ + """Write a single row using wr_valid/wr_ready handshake.""" await wait_idle(dut) dut.wr_addr.value = addr @@ -79,7 +105,7 @@ async def write_row(dut, addr, value): dut.wr_valid.value = 0 - # Wait for cam_noisy to finish GEN_MASK/COMMIT + # Wait for write pipeline to drain await wait_idle(dut) @@ -90,15 +116,7 @@ async def write_rows(dut, rows): async def query_once(dut, query): - """Issue a query and return (top1_index, top1_score, score_debug). - - 1. Wait for system idle - 2. Assert query_valid + set query_hash - 3. Wait for query_ready handshake - 4. Deassert query_valid - 5. Wait for result_valid - 6. Read result, pulse result_ready to consume - """ + """Issue a query and return (top1_index, top1_score, score_debug).""" await wait_idle(dut) dut.query_hash.value = int(query) @@ -119,12 +137,14 @@ async def query_once(dut, query): top1_index = int(dut.top1_index.value) top1_score = int(dut.top1_score.value) + num_rows = dut_num_rows(dut) + score_bits = dut_score_bits(dut) score_debug = None if hasattr(dut, "score_debug_flat"): score_debug = unpack_score_debug_flat( int(dut.score_debug_flat.value), - NUM_ROWS, - SCORE_BITS, + num_rows, + score_bits, ) dut.result_ready.value = 1 @@ -134,66 +154,83 @@ async def query_once(dut, query): return top1_index, top1_score, score_debug -# ── Test A: Baseline (NOISE_EN=0) ──────────────────────────────────────────── +# ── Compile smoke test ──────────────────────────────────────────────────────── + + +@cocotb.test() +async def compile_includes_grouped_noise_helper(dut): + """Compilation test: new grouped noise helper must elaborate with cam_top.""" + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_dut(dut) + assert int(dut.wr_ready.value) in (0, 1) + + +# ── Test A: Baseline (WRITE_NOISE_EN=0) ───────────────────────────────────── @cocotb.test() async def baseline_no_noise(dut): - """Verify write+query works exactly like the old CAM when NOISE_EN=0.""" - noise_en = _get_param(dut, "NOISE_EN", 0) - if noise_en: - dut._log.info("Skipping baseline_no_noise: requires NOISE_EN=0.") + """Verify write+query works exactly like the old CAM when noise disabled.""" + noise_en = _get_param(dut, "WRITE_NOISE_EN", 0) + read_noise_en = _get_param(dut, "READ_NOISE_EN", 0) + if noise_en or read_noise_en: + dut._log.info("Skipping baseline_no_noise: requires noise disabled.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) rng = np.random.default_rng(1) - rows = random_hashes(rng, NUM_ROWS, width=HASH_BITS) - query_index = 123 + rows = random_hashes(rng, num_rows, width=hash_bits) + query_index = min(123, num_rows - 1) query = rows[query_index] await write_rows(dut, rows) top1_index, top1_score, score_debug = await query_once(dut, query) - expected = match_top1(query, rows, width=HASH_BITS) + expected = match_top1(query, rows, width=hash_bits) assert top1_index == expected.top1_index assert top1_score == expected.top1_score assert top1_index == query_index - assert top1_score == HASH_BITS + assert top1_score == hash_bits if score_debug is not None: assert np.array_equal(score_debug, expected.scores) -# ── Test B: Zero noise rate (NOISE_EN=1, RATE_NUM=0) ──────────────────────── +# ── Test B: Zero noise rate (WRITE_NOISE_EN=1, RATE_NUM=0) ────────────────── @cocotb.test() async def zero_rate_noise(dut): - """Noise module connected but THRESHOLD=0 → no flips, equivalent to NOISE_EN=0.""" - noise_en = _get_param(dut, "NOISE_EN", 1) - rate_num = _get_param(dut, "NOISE_RATE_NUM", 1) - if not noise_en or rate_num != 0: - dut._log.info("Skipping zero_rate_noise: requires NOISE_EN=1, RATE_NUM=0.") + """Noise module connected but THRESHOLD=0 → no flips.""" + noise_en = _get_param(dut, "WRITE_NOISE_EN", 1) + rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1) + read_noise_en = _get_param(dut, "READ_NOISE_EN", 0) + if not noise_en or rate_num != 0 or read_noise_en: + dut._log.info("Skipping zero_rate_noise: requires WRITE_NOISE_EN=1, RATE_NUM=0, READ_NOISE_EN=0.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) rng = np.random.default_rng(1) - rows = random_hashes(rng, NUM_ROWS, width=HASH_BITS) - query_index = 123 + rows = random_hashes(rng, num_rows, width=hash_bits) + query_index = min(123, num_rows - 1) query = rows[query_index] await write_rows(dut, rows) top1_index, top1_score, score_debug = await query_once(dut, query) - expected = match_top1(query, rows, width=HASH_BITS) + expected = match_top1(query, rows, width=hash_bits) assert top1_index == expected.top1_index assert top1_score == expected.top1_score assert top1_index == query_index - assert top1_score == HASH_BITS + assert top1_score == hash_bits if score_debug is not None: assert np.array_equal(score_debug, expected.scores) @@ -204,17 +241,12 @@ async def zero_rate_noise(dut): @cocotb.test() async def full_rate_noise(dut): - """NOISE_RATE_NUM=1, NOISE_RATE_DEN=1 → every group flips its selected bit per write. - - At full rate with default NOISE_BITS=8, exactly 8 deterministic bits flip per write - (one selected bit per group), not all 512 bits. We use ref_model.py PRNG to predict - the exact stored rows. - """ - noise_en = _get_param(dut, "NOISE_EN", 1) - rate_num = _get_param(dut, "NOISE_RATE_NUM", 1) - rate_den = _get_param(dut, "NOISE_RATE_DEN", 100) + """WRITE_NOISE_RATE_NUM=1, WRITE_NOISE_RATE_DEN=1 → every group flips.""" + noise_en = _get_param(dut, "WRITE_NOISE_EN", 1) + rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1) + rate_den = _get_param(dut, "WRITE_NOISE_RATE_DEN", 100) if not noise_en or rate_num != 1 or rate_den != 1: - dut._log.info("Skipping full_rate_noise: requires NOISE_EN=1, RATE_NUM=1, RATE_DEN=1.") + dut._log.info("Skipping full_rate_noise: requires WRITE_NOISE_EN=1, RATE_NUM=1, RATE_DEN=1.") return if not hasattr(dut, "score_debug_flat"): dut._log.info("Skipping full_rate_noise: requires SIM_DEBUG (score_debug_flat).") @@ -223,47 +255,41 @@ async def full_rate_noise(dut): cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) - noise_bits = _get_param(dut, "NOISE_BITS", 8) + hash_bits = dut_hash_bits(dut) + num_rows = dut_num_rows(dut) + noise_bits = _get_param(dut, "WRITE_NOISE_BITS", 8) all_zero = 0 - all_one = (1 << HASH_BITS) - 1 + all_one = (1 << hash_bits) - 1 - # Predict stored rows using the same RTL seed convention as exact_noise_model_match. RTL_SEED = 0xB504_F32D_B504_F32D prng_state = (RTL_SEED << 64) | RTL_SEED - # Flip mask for row 0 (all-zero written) flip0, prng_state = generate_write_flip_mask( - prng_state, HASH_BITS, noise_bits, rate_num, rate_den, + prng_state, hash_bits, noise_bits, rate_num, rate_den, ) - expected_row0 = all_zero ^ flip0 # stored value after noise + expected_row0 = all_zero ^ flip0 - # Flip mask for row 1 (all-one written) flip1, prng_state = generate_write_flip_mask( - prng_state, HASH_BITS, noise_bits, rate_num, rate_den, + prng_state, hash_bits, noise_bits, rate_num, rate_den, ) - expected_row1 = all_one ^ flip1 # stored value after noise + expected_row1 = all_one ^ flip1 - # Write all-zero to row 0, all-one to row 1, rest zero - rows = [0] * NUM_ROWS + rows = [0] * num_rows rows[0] = all_zero rows[1] = all_one await write_rows(dut, rows) - # Query expected_row0 → should exactly match row 0's stored value top1_index, top1_score, score_debug = await query_once(dut, expected_row0) assert score_debug is not None, "score_debug required for full_rate_noise" - assert int(score_debug[0]) == HASH_BITS, ( - f"Row 0: expected exact match for predicted stored value, " - f"score={score_debug[0]} != {HASH_BITS}" + assert int(score_debug[0]) == hash_bits, ( + f"Row 0: expected exact match, score={score_debug[0]} != {hash_bits}" ) - # Query expected_row1 → should exactly match row 1's stored value top1_index, top1_score, score_debug = await query_once(dut, expected_row1) - assert score_debug is not None, "score_debug required for full_rate_noise" - assert int(score_debug[1]) == HASH_BITS, ( - f"Row 1: expected exact match for predicted stored value, " - f"score={score_debug[1]} != {HASH_BITS}" + assert score_debug is not None + assert int(score_debug[1]) == hash_bits, ( + f"Row 1: expected exact match, score={score_debug[1]} != {hash_bits}" ) @@ -273,111 +299,117 @@ async def full_rate_noise(dut): @cocotb.test() async def default_noise_reproducible(dut): """Fixed seed → deterministic write noise. Two identical runs produce same results.""" - noise_en = _get_param(dut, "NOISE_EN", 1) + noise_en = _get_param(dut, "WRITE_NOISE_EN", 1) if not noise_en: - dut._log.info("Skipping default_noise_reproducible: requires NOISE_EN=1.") + dut._log.info("Skipping default_noise_reproducible: requires WRITE_NOISE_EN=1.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) rng = np.random.default_rng(42) - rows = random_hashes(rng, NUM_ROWS, width=HASH_BITS) + rows = random_hashes(rng, num_rows, width=hash_bits) - # ── First run ── await write_rows(dut, rows) - query = rows[50] + query = rows[min(50, num_rows - 1)] top1_index_1, top1_score_1, _ = await query_once(dut, query) - # Reset for second run await reset_dut(dut) - # ── Second run with same data ── await write_rows(dut, rows) top1_index_2, top1_score_2, _ = await query_once(dut, query) - # Deterministic: same seed → same PRNG sequence → same stored hashes → same result assert top1_index_1 == top1_index_2 assert top1_score_1 == top1_score_2 -# ── Preserved legacy tests (only meaningful for NOISE_EN=0) ────────────────── +# ── Preserved legacy tests (only meaningful for noise disabled) ────────────── @cocotb.test() async def known_hamming_distance(dut): """Hamming distance verification — exact scores only valid without noise.""" - if _get_param(dut, "NOISE_EN", 1): - dut._log.info("Skipping known_hamming_distance: NOISE_EN=1, stored hashes may differ.") + if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping known_hamming_distance: requires noise disabled.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) + query = 0 - rows = [0] * NUM_ROWS - rows[10] = (1 << 7) - 1 - rows[11] = (1 << 31) - 1 - rows[12] = (1 << 128) - 1 + rows = [0] * num_rows + rows[min(10, num_rows - 1)] = (1 << 7) - 1 + rows[min(11, num_rows - 1)] = (1 << 31) - 1 + rows[min(12, num_rows - 1)] = (1 << 128) - 1 await write_rows(dut, rows) top1_index, top1_score, score_debug = await query_once(dut, query) assert top1_index == 0 - assert top1_score == HASH_BITS + assert top1_score == hash_bits if score_debug is not None: - assert int(score_debug[10]) == HASH_BITS - 7 - assert int(score_debug[11]) == HASH_BITS - 31 - assert int(score_debug[12]) == HASH_BITS - 128 + assert int(score_debug[min(10, num_rows - 1)]) == hash_bits - 7 + assert int(score_debug[min(11, num_rows - 1)]) == hash_bits - 31 + assert int(score_debug[min(12, num_rows - 1)]) == hash_bits - 128 @cocotb.test() async def tie_break_policy(dut): """Tie-break: lowest row index wins — only verified without noise.""" - if _get_param(dut, "NOISE_EN", 1): - dut._log.info("Skipping tie_break_policy: NOISE_EN=1, stored hashes may differ.") + if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping tie_break_policy: requires noise disabled.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) rng = np.random.default_rng(2) - rows = random_hashes(rng, NUM_ROWS, width=HASH_BITS) - query = rows[200] + rows = random_hashes(rng, num_rows, width=hash_bits) + query = rows[min(200, num_rows - 1)] rows[10] = query rows[20] = query - rows[200] = query + rows[min(200, num_rows - 1)] = query await write_rows(dut, rows) top1_index, top1_score, _ = await query_once(dut, query) assert top1_index == 10 - assert top1_score == HASH_BITS + assert top1_score == hash_bits @cocotb.test() async def all_zero_all_one_boundary(dut): """All-zero / all-one boundary — only verified without noise.""" - if _get_param(dut, "NOISE_EN", 1): - dut._log.info("Skipping all_zero_all_one_boundary: NOISE_EN=1, stored hashes may differ.") + if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping all_zero_all_one_boundary: requires noise disabled.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) - rows = [0] * NUM_ROWS + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) + + rows = [0] * num_rows rows[0] = 0 - rows[1] = (1 << HASH_BITS) - 1 + rows[1] = (1 << hash_bits) - 1 query = 0 await write_rows(dut, rows) top1_index, top1_score, score_debug = await query_once(dut, query) - assert top1_score == HASH_BITS + assert top1_score == hash_bits assert top1_index == 0 if score_debug is not None: - assert int(score_debug[0]) == HASH_BITS + assert int(score_debug[0]) == hash_bits assert int(score_debug[1]) == 0 @@ -386,16 +418,15 @@ async def all_zero_all_one_boundary(dut): @cocotb.test() async def exact_noise_model_match(dut): - """Verify RTL stored hashes match ref_model.py for a known seed and rate. - - Writes rows with noise enabled, then queries back via score_debug to - reconstruct stored hashes, and compares against Python model predictions. - """ - noise_en = _get_param(dut, "NOISE_EN", 1) - rate_num = _get_param(dut, "NOISE_RATE_NUM", 1) - rate_den = _get_param(dut, "NOISE_RATE_DEN", 100) + """Verify RTL stored hashes match ref_model.py for a known seed and rate.""" + noise_en = _get_param(dut, "WRITE_NOISE_EN", 1) + rate_num = _get_param(dut, "WRITE_NOISE_RATE_NUM", 1) + rate_den = _get_param(dut, "WRITE_NOISE_RATE_DEN", 100) if not noise_en or rate_num == 0: - dut._log.info("Skipping exact_noise_model_match: requires NOISE_EN=1, RATE_NUM>0.") + dut._log.info("Skipping exact_noise_model_match: requires WRITE_NOISE_EN=1, RATE_NUM>0.") + return + if _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping exact_noise_model_match: requires READ_NOISE_EN=0 (read noise corrupts score comparison).") return if not hasattr(dut, "score_debug_flat"): dut._log.info("Skipping exact_noise_model_match: requires SIM_DEBUG.") @@ -404,39 +435,31 @@ async def exact_noise_model_match(dut): cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) - noise_bits = _get_param(dut, "NOISE_BITS", 8) + hash_bits = dut_hash_bits(dut) + noise_bits = _get_param(dut, "WRITE_NOISE_BITS", 8) - # Use a small subset to keep test fast n_test_rows = 4 rng = np.random.default_rng(99) - rows = random_hashes(rng, n_test_rows, width=HASH_BITS) + rows = random_hashes(rng, n_test_rows, width=hash_bits) - # Predict stored hashes with Python model using the same seed. - # RTL random128 seed: {NOISE_SEED, NOISE_SEED}, default 64'hB504_F32D_B504_F32D. RTL_SEED = 0xB504_F32D_B504_F32D prng_state = (RTL_SEED << 64) | RTL_SEED expected_stored = [] for row in rows: flip, prng_state = generate_write_flip_mask( - prng_state, HASH_BITS, noise_bits, rate_num, rate_den, + prng_state, hash_bits, noise_bits, rate_num, rate_den, ) expected_stored.append(row ^ flip) - # Write only test rows (rest stay at 0 from reset) for idx, val in enumerate(rows): await write_row(dut, idx, val) - # Query all-zero to get Hamming distances (= HASH_BITS - popcount(stored ^ 0) = HASH_BITS - popcount(stored)) - # So popcount(stored) = HASH_BITS - score - # This gives us the number of set bits but not the exact value. - # Instead, query each expected_stored value — it should score HASH_BITS if match is exact. for idx, expected in enumerate(expected_stored): top1_index, top1_score, score_debug = await query_once(dut, expected) - # The stored hash at idx should exactly match expected, so score == HASH_BITS assert score_debug is not None, "score_debug required for mask match verification" - assert int(score_debug[idx]) == HASH_BITS, ( + assert int(score_debug[idx]) == hash_bits, ( f"Row {idx}: expected stored hash to match model prediction, " - f"score={score_debug[idx]} != {HASH_BITS}" + f"score={score_debug[idx]} != {hash_bits}" ) @@ -445,52 +468,160 @@ async def exact_noise_model_match(dut): @cocotb.test() async def half_duplex_write_priority(dut): - """When wr_valid and query_valid are both high, write wins and query is held off. - Only runs with NOISE_EN=0 so stored hashes are predictable. - """ - if _get_param(dut, "NOISE_EN", 1): - dut._log.info("Skipping half_duplex_write_priority: requires NOISE_EN=0 for exact scores.") + """When wr_valid and query_valid are both high, write wins and query is held off.""" + if _get_param(dut, "WRITE_NOISE_EN", 1) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping half_duplex_write_priority: requires noise disabled.") return cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) await reset_dut(dut) - # Write a known value to row 0 - test_val = (1 << HASH_BITS) - 1 # all-ones + hash_bits = dut_hash_bits(dut) + test_val = (1 << hash_bits) - 1 await write_row(dut, 0, test_val) - # Now system is idle: wr_ready=1, query_ready=1 await wait_idle(dut) assert int(dut.wr_ready.value) == 1 assert int(dut.query_ready.value) == 1 - # Drive both wr_valid and query_valid simultaneously dut.wr_valid.value = 1 dut.wr_addr.value = 1 - dut.write_hash.value = 0 # write all-zeros to row 1 + dut.write_hash.value = 0 dut.query_valid.value = 1 - dut.query_hash.value = test_val # query for all-ones (in row 0) + dut.query_hash.value = test_val await RisingEdge(dut.clk) - # Write should have been accepted (wr_ready was 1), query should NOT have been accepted - # because write-priority gates query_ready when wr_valid=1 - wr_accepted = int(dut.wr_ready.value) == 0 # after handshake, wr_ready drops - # query_ready should have been 0 during the simultaneous assertion - # (it's !wr_valid gated), so query was blocked - - # Deassert both dut.wr_valid.value = 0 dut.query_valid.value = 0 - # Wait for write to complete (noise generation + commit) await wait_idle(dut) - # Now query should work — row 0 has all-ones (written first) top1_index, top1_score, _ = await query_once(dut, test_val) assert top1_index == 0 - assert top1_score == HASH_BITS + assert top1_score == hash_bits - # Verify row 1 was written (all-zeros) — query all-zeros top1_index, top1_score, _ = await query_once(dut, 0) assert top1_index == 1 - assert top1_score == HASH_BITS + assert top1_score == hash_bits + + +# ── Test G: Banked pipeline no-noise Top-1 ─────────────────────────────────── + + +@cocotb.test() +async def banked_pipeline_no_noise_top1(dut): + """No-noise banked pipeline returns the same Top-1 as the pure model.""" + if _get_param(dut, "WRITE_NOISE_EN", 0) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping banked_pipeline_no_noise_top1: requires noise disabled.") + return + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_dut(dut) + + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) + rng = np.random.default_rng(7) + rows = random_hashes(rng, num_rows, width=hash_bits) + query_index = min(17, num_rows - 1) + query = rows[query_index] + + await write_rows(dut, rows) + top1_index, top1_score, score_debug = await query_once(dut, query) + expected = match_top1(query, rows, width=hash_bits) + + assert top1_index == expected.top1_index + assert top1_score == expected.top1_score + assert top1_index == query_index + + +# ── Test H: Query scan blocks writes until result consumed ─────────────────── + + +@cocotb.test() +async def query_scan_blocks_writes_until_result_consumed(dut): + """Half-duplex: active query scan deasserts wr_ready.""" + if _get_param(dut, "WRITE_NOISE_EN", 0) or _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping query_scan_blocks_writes: requires noise disabled.") + return + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_dut(dut) + + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) + rows = [0] * num_rows + rows[0] = (1 << hash_bits) - 1 + await write_rows(dut, rows) + + await wait_idle(dut) + dut.query_hash.value = rows[0] + dut.query_valid.value = 1 + await RisingEdge(dut.clk) + dut.query_valid.value = 0 + + dut.wr_valid.value = 1 + dut.wr_addr.value = 1 + dut.write_hash.value = 0 + await RisingEdge(dut.clk) + assert int(dut.wr_ready.value) == 0 + dut.wr_valid.value = 0 + + while int(dut.result_valid.value) == 0: + await RisingEdge(dut.clk) + dut.result_ready.value = 1 + await RisingEdge(dut.clk) + + +# ── Test I: Read noise model match ────────────────────────────────────────── + + +@cocotb.test() +async def read_noise_model_match(dut): + """Read noise uses grouped masks and matches the Python model for one query.""" + if not _get_param(dut, "READ_NOISE_EN", 0): + dut._log.info("Skipping read_noise_model_match: requires READ_NOISE_EN=1.") + return + + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_dut(dut) + + num_rows = dut_num_rows(dut) + hash_bits = dut_hash_bits(dut) + lanes = dut_lanes(dut) + rng = np.random.default_rng(123) + rows = random_hashes(rng, num_rows, width=hash_bits) + + # If write noise is enabled, apply write flip masks to predict stored rows + stored_rows = list(rows) + if _get_param(dut, "WRITE_NOISE_EN", 0): + seed = 0xB504_F32D_B504_F32D + prng_state = (seed << 64) | seed + stored_rows = [] + for row in rows: + flip, prng_state = generate_write_flip_mask( + prng_state, + hash_bits, + _get_param(dut, "WRITE_NOISE_BITS", 8), + _get_param(dut, "WRITE_NOISE_RATE_NUM", 1), + _get_param(dut, "WRITE_NOISE_RATE_DEN", 100), + ) + stored_rows.append(row ^ flip) + + query = rows[min(5, num_rows - 1)] + + await write_rows(dut, rows) + top1_index, top1_score, score_debug = await query_once(dut, query) + + expected = match_top1_with_read_noise( + query, + stored_rows, + width=hash_bits, + lanes=lanes, + noise_bits=_get_param(dut, "READ_NOISE_BITS", 8), + rate_num=_get_param(dut, "READ_NOISE_RATE_NUM", 1), + rate_den=_get_param(dut, "READ_NOISE_RATE_DEN", 100), + seed=0x6A09_E667_F3BC_C909, + ) + + assert top1_index == expected.top1_index + assert top1_score == expected.top1_score + if score_debug is not None: + assert np.array_equal(score_debug, expected.scores) diff --git a/hw/sim/tests/test_cam_core_banked.py b/hw/sim/tests/test_cam_core_banked.py new file mode 100644 index 0000000..d2757d6 --- /dev/null +++ b/hw/sim/tests/test_cam_core_banked.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge + + +async def reset_core(dut): + dut.rst_n.value = 0 + dut.wr_valid.value = 0 + dut.wr_row.value = 0 + dut.wr_hash.value = 0 + dut.rd_valid_i.value = 0 + dut.rd_base_row_i.value = 0 + for _ in range(3): + await RisingEdge(dut.clk) + dut.rst_n.value = 1 + for _ in range(2): + await RisingEdge(dut.clk) + + +@cocotb.test() +async def banked_core_reads_aligned_eight_row_batch_after_one_cycle(dut): + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_core(dut) + + LANES = 8 + ROW_BITS = len(dut.rd_row_ids_o) // LANES + + for row in range(LANES): + dut.wr_valid.value = 1 + dut.wr_row.value = row + dut.wr_hash.value = row + 0x100 + await RisingEdge(dut.clk) + dut.wr_valid.value = 0 + + dut.rd_base_row_i.value = 0 + dut.rd_valid_i.value = 1 + await RisingEdge(dut.clk) + dut.rd_valid_i.value = 0 + + await RisingEdge(dut.clk) + assert int(dut.rd_valid_o.value) == 1 + for lane in range(LANES): + got_row = (int(dut.rd_row_ids_o.value) >> (lane * ROW_BITS)) & ((1 << ROW_BITS) - 1) + got_hash = (int(dut.rd_hashes_o.value) >> (lane * 512)) & ((1 << 512) - 1) + assert got_row == lane + assert got_hash == lane + 0x100 diff --git a/hw/sim/tests/test_cam_read_noise.py b/hw/sim/tests/test_cam_read_noise.py new file mode 100644 index 0000000..bd635b0 --- /dev/null +++ b/hw/sim/tests/test_cam_read_noise.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge + + +async def reset_read_noise(dut): + dut.rst_n.value = 0 + dut.valid_i.value = 0 + dut.row_ids_i.value = 0 + dut.hashes_i.value = 0 + dut.lane_valid_i.value = 0 + for _ in range(3): + await RisingEdge(dut.clk) + dut.rst_n.value = 1 + for _ in range(2): + await RisingEdge(dut.clk) + + +@cocotb.test() +async def read_noise_disabled_forwards_hashes_after_one_stage(dut): + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_read_noise(dut) + + LANES = 8 + ROW_BITS = len(dut.row_ids_i) // LANES + HASH_BITS_PER_LANE = len(dut.hashes_i) // LANES + + hashes = 0 + rows = 0 + for lane in range(LANES): + hashes |= (lane + 0x55) << (lane * HASH_BITS_PER_LANE) + rows |= lane << (lane * ROW_BITS) + + dut.hashes_i.value = hashes + dut.row_ids_i.value = rows + dut.lane_valid_i.value = 0xFF + dut.valid_i.value = 1 + await RisingEdge(dut.clk) + dut.valid_i.value = 0 + await RisingEdge(dut.clk) + await RisingEdge(dut.clk) + + assert int(dut.valid_o.value) == 1 + assert int(dut.hashes_noisy_o.value) == hashes + assert int(dut.row_ids_o.value) == rows + assert int(dut.lane_valid_o.value) == 0xFF diff --git a/hw/sim/tests/test_cam_write_noise.py b/hw/sim/tests/test_cam_write_noise.py new file mode 100644 index 0000000..591ed00 --- /dev/null +++ b/hw/sim/tests/test_cam_write_noise.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge +from model.ref_model import generate_write_flip_mask + + +async def reset_write_noise(dut): + dut.rst_n.value = 0 + dut.wr_valid.value = 0 + dut.wr_row.value = 0 + dut.wr_hash.value = 0 + for _ in range(3): + await RisingEdge(dut.clk) + dut.rst_n.value = 1 + for _ in range(2): + await RisingEdge(dut.clk) + + +@cocotb.test() +async def write_noise_outputs_grouped_noisy_hash(dut): + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_write_noise(dut) + + value = 0x123456789ABCDEF + dut.wr_row.value = 3 + dut.wr_hash.value = value + dut.wr_valid.value = 1 + await RisingEdge(dut.clk) + dut.wr_valid.value = 0 + + while int(dut.core_wr_valid.value) == 0: + await RisingEdge(dut.clk) + + seed = 0xB504_F32D_B504_F32D + flip, _ = generate_write_flip_mask((seed << 64) | seed, 512, 8, 1, 100) + assert int(dut.core_wr_row.value) == 3 + assert int(dut.core_wr_hash.value) == (value ^ flip) diff --git a/hw/sim/tests/test_match_engine_pipeline.py b/hw/sim/tests/test_match_engine_pipeline.py new file mode 100644 index 0000000..c3961e4 --- /dev/null +++ b/hw/sim/tests/test_match_engine_pipeline.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import cocotb +from cocotb.clock import Clock +from cocotb.triggers import RisingEdge + + +async def reset_match(dut): + dut.rst_n.value = 0 + dut.query_valid.value = 0 + dut.query_hash.value = 0 + dut.result_ready.value = 1 + dut.rd_valid_i.value = 0 + dut.rd_row_ids_i.value = 0 + dut.rd_hashes_i.value = 0 + dut.rd_lane_valid_i.value = 0 + for _ in range(3): + await RisingEdge(dut.clk) + dut.rst_n.value = 1 + for _ in range(2): + await RisingEdge(dut.clk) + + +@cocotb.test() +async def match_engine_returns_top1_after_pipeline_drain(dut): + cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start()) + await reset_match(dut) + + LANES = 8 + ROW_BITS = len(dut.rd_row_ids_i) // LANES + HASH_BITS = len(dut.rd_hashes_i) // LANES + NUM_ROWS = 1 << len(dut.rd_base_row_o) + TARGET_ROW = 9 + + query = (1 << HASH_BITS) - 1 + dut.query_hash.value = query + dut.query_valid.value = 1 + await RisingEdge(dut.clk) + dut.query_valid.value = 0 + + for base in range(0, NUM_ROWS, LANES): + while int(dut.rd_valid_o.value) == 0: + await RisingEdge(dut.clk) + assert int(dut.rd_base_row_o.value) == base + rows = 0 + hashes = 0 + for lane in range(LANES): + row_id = base + lane + rows |= row_id << (lane * ROW_BITS) + row_hash = query if row_id == TARGET_ROW else 0 + hashes |= row_hash << (lane * HASH_BITS) + dut.rd_row_ids_i.value = rows + dut.rd_hashes_i.value = hashes + dut.rd_lane_valid_i.value = 0xFF + dut.rd_valid_i.value = 1 + await RisingEdge(dut.clk) + dut.rd_valid_i.value = 0 + + for _ in range(20): + if int(dut.result_valid.value): + break + await RisingEdge(dut.clk) + + assert int(dut.result_valid.value) == 1 + assert int(dut.result_row.value) == TARGET_ROW + assert int(dut.result_score.value) == HASH_BITS diff --git a/hw/sim/tests/test_ref_model_noise.py b/hw/sim/tests/test_ref_model_noise.py new file mode 100644 index 0000000..bfe5714 --- /dev/null +++ b/hw/sim/tests/test_ref_model_noise.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from model.ref_model import ( + generate_grouped_flip_mask, + match_top1_with_read_noise, + xnor_popcount_score, +) + + +def test_grouped_flip_mask_full_rate_one_bit_per_64_bit_group(): + random_value = 0 + for group in range(8): + bit_idx = group + 1 + sample = 0 + random_value |= bit_idx << (group * 14) + random_value |= sample << (group * 14 + 6) + + mask = generate_grouped_flip_mask( + random_value=random_value, + hash_bits=512, + noise_bits=8, + rate_num=1, + rate_den=1, + ) + + expected = 0 + for group in range(8): + expected |= 1 << (group * 64 + group + 1) + + assert mask == expected + assert mask.bit_count() == 8 + + +def test_grouped_flip_mask_zero_rate_no_flips(): + mask = generate_grouped_flip_mask( + random_value=(1 << 128) - 1, + hash_bits=512, + noise_bits=8, + rate_num=0, + rate_den=100, + ) + assert mask == 0 + + +def test_score_is_bit_match_popcount_not_hamming_distance(): + query = 0b1010 + stored = 0b1000 + assert xnor_popcount_score(query, stored, width=4) == 3 + + +def test_read_noise_model_is_reproducible_after_reset_seed(): + rows = [0, (1 << 512) - 1, 0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1357, 0x2468] + query = rows[2] + kwargs = dict( + query=query, + rows=rows, + width=512, + lanes=8, + noise_bits=8, + rate_num=1, + rate_den=100, + seed=0x6A09_E667_F3BC_C909, + ) + + first = match_top1_with_read_noise(**kwargs) + second = match_top1_with_read_noise(**kwargs) + + assert first.top1_index == second.top1_index + assert first.top1_score == second.top1_score + assert first.scores.tolist() == second.scores.tolist()