feat(hw/rtl): implement full Top-K CAM search pipeline with serial result output

- add TOPK_K, FIFO_DEPTH, RESULT_SERIAL parameters to cam_params
- add candidate_fifo: synchronous ready/valid FIFO for (row, score) candidates
- add topk_tracker: tracks top-K candidates with clear/ready handshake
- add result_serializer: serializes packed Top-K array into rank-ordered stream
- refactor match_engine_pipeline from 4-state to 8-state Top-K pipeline
- extend cam_top with serial Top-K interface (result_rank/row/score/last)
- add backward-compatible top1_index/top1_score aliases from rank-0 beat
- add comprehensive tests for all new modules
This commit is contained in:
2026-05-19 18:19:05 +08:00
parent 8bcad1f23f
commit e4cbb5e30d
21 changed files with 2152 additions and 124 deletions

View File

@@ -64,6 +64,63 @@ def dut_score_bits(dut):
# ── Helpers ──────────────────────────────────────────────────────────────────
async def collect_topk(dut, timeout_cycles: int = 2000):
"""Collect all serial Top-K result beats with result_ready held high.
Returns list of (rank, row, score, last) tuples.
Raises AssertionError if the stream does not complete within timeout.
"""
dut.result_ready.value = 1
beats = []
for _ in range(timeout_cycles):
if int(dut.result_valid.value):
rank = int(dut.result_rank.value)
row = int(dut.result_row.value)
score = int(dut.result_score.value)
last = int(dut.result_last.value)
beats.append((rank, row, score, last))
if last:
return beats
await RisingEdge(dut.clk)
raise AssertionError("Top-K result stream did not finish")
async def query_topk_once(dut, query, timeout_cycles=2000):
"""Issue a query, collect full serial Top-K stream, and return beats + Top-1 metadata.
Returns (beats, top1_index, top1_score, score_debug).
After this call the full result stream has been consumed and the DUT is idle.
"""
await wait_idle(dut)
dut.query_hash.value = int(query)
dut.query_valid.value = 1
# Wait for handshake
while True:
await RisingEdge(dut.clk)
if int(dut.query_ready.value):
break
dut.query_valid.value = 0
# Consume full serial result stream
beats = await collect_topk(dut, timeout_cycles=timeout_cycles)
# score_debug is available after query completes
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,
)
return beats, beats[0][1], beats[0][2], score_debug
async def reset_dut(dut):
"""Reset the DUT with new handshake interface."""
dut.rst_n.value = 0
@@ -116,41 +173,11 @@ async def write_rows(dut, rows):
async def query_once(dut, query):
"""Issue a query and return (top1_index, top1_score, score_debug)."""
await wait_idle(dut)
dut.query_hash.value = int(query)
dut.query_valid.value = 1
# Wait for handshake
while True:
await RisingEdge(dut.clk)
if int(dut.query_ready.value):
break
dut.query_valid.value = 0
# Wait for result
while int(dut.result_valid.value) == 0:
await RisingEdge(dut.clk)
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,
)
dut.result_ready.value = 1
await RisingEdge(dut.clk)
dut.result_ready.value = 0
"""Issue a query and return (top1_index, top1_score, score_debug).
Consumes the full serial Top-K stream and returns rank-0 data.
"""
_, top1_index, top1_score, score_debug = await query_topk_once(dut, query)
return top1_index, top1_score, score_debug
@@ -187,7 +214,7 @@ async def baseline_no_noise(dut):
query = rows[query_index]
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, query)
beats, top1_index, top1_score, score_debug = await query_topk_once(dut, query)
expected = match_top1(query, rows, width=hash_bits)
@@ -196,6 +223,16 @@ async def baseline_no_noise(dut):
assert top1_index == query_index
assert top1_score == hash_bits
# Serial Top-K stream verification: beats from the single query above
assert beats[0][0] == 0, "First beat must have rank 0"
assert beats[-1][3] == 1, "Last beat must assert result_last"
# Verify top1 aliases match first beat after stream fully consumed
assert int(dut.top1_index.value) == beats[0][1]
assert int(dut.top1_score.value) == beats[0][2]
# Verify returned top1 matches first beat rank0
assert top1_index == beats[0][1]
assert top1_score == beats[0][2]
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)
@@ -564,10 +601,10 @@ async def query_scan_blocks_writes_until_result_consumed(dut):
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)
# Consume full serial stream so the DUT returns idle
beats = await collect_topk(dut, timeout_cycles=2000)
assert len(beats) > 0
assert beats[-1][3] == 1 # last asserted
# ── Test I: Read noise model match ──────────────────────────────────────────