feat(retrieval): add CAM retrieval benchmark with topk scoring and read noise support

- Add cocotb benchmark infrastructure under hw/sim/benchmarks/retrieval/ with Makefile
- Implement test_retrieval_benchmark.py supporting configurable topk-k, read/write noise
- Add cluster-based synthetic dataset generator with configurable bit-flip rates
- Add reference model functions: match_topk, match_topk_from_scores, score_rows_with_read_noise
- Add .justfile shortcuts: cam-test-retrieval-no-noise, cam-test-retrieval-read-noise
- Add TOPK_K to Verilator EXTRA_ARGS via cocotb-common.mk
- Add unit tests for topk sorting logic and stateful read-noise scoring
This commit is contained in:
2026-05-22 19:01:43 +08:00
parent 29f4cc91f6
commit e1bed00cc4
8 changed files with 503 additions and 2 deletions

View File

@@ -53,6 +53,30 @@ def match_top1(
)
def match_topk_from_scores(scores: Sequence[int], k: int) -> list[int]:
"""Return row indices sorted by score desc, row index asc (HW tie-break)."""
if k <= 0:
raise ValueError("k must be greater than 0")
return sorted(range(len(scores)), key=lambda idx: (-int(scores[idx]), idx))[: min(k, len(scores))]
def match_topk(
query: int,
rows: Sequence[int],
*,
width: int = 512,
k: int = 5,
) -> tuple[list[int], np.ndarray]:
"""Pure Top-K matching — noise is already baked into rows if needed.
Returns (list of row indices in rank order, NumPy score array).
"""
scores = np.zeros(len(rows), dtype=np.int32)
for idx, row in enumerate(rows):
scores[idx] = xnor_popcount_score(int(query), int(row), width)
return match_topk_from_scores(scores, k), scores
def xorshift128(state: int) -> int:
"""128-bit xorshift PRNG, single step. Matches random128.sv."""
mask32 = (1 << 32) - 1
@@ -182,6 +206,49 @@ def generate_read_lane_masks(
return masks, next_states
def score_rows_with_read_noise(
query: int,
rows: Sequence[int],
*,
lane_states: Sequence[int],
width: int = 512,
lanes: int = 8,
noise_bits: int = 8,
rate_num: int = 1,
rate_den: int = 100,
) -> tuple[np.ndarray, list[int]]:
"""Score one query with read noise and return updated lane PRNG states.
Unlike match_top1_with_read_noise(), this helper is stateful across calls:
callers pass current lane states in and receive the next states back.
This matches a DUT that is reset once, then serves multiple queries.
"""
assert lanes > 0
assert len(rows) % lanes == 0
assert len(lane_states) == lanes
scores = np.zeros(len(rows), dtype=np.int32)
next_lane_states = [int(state) for state in lane_states]
for base in range(0, len(rows), lanes):
lane_valid = [True] * lanes
masks, next_lane_states = generate_read_lane_masks(
next_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]) ^ int(masks[lane])
scores[row_idx] = xnor_popcount_score(int(query), noisy_row, width)
return scores, next_lane_states
def match_top1_with_read_noise(
query: int,
rows: Sequence[int],