feat: add hardware retrieval cycle performance measurement

Add cycle-level performance measurement for hardware CAM retrieval benchmarks
to complement existing quality metrics.

- Add query_topk_once_with_latency with accept→first/last cycle timing
- Add QueryTiming dataclass and summarize_query_timings helper
- Integrate cycle performance into benchmark outputs (CSV + Markdown)
- Log RETRIEVAL_PERF_RESULT with cycles/query and queries/cycle
- Update experiment docs with hardware cycle performance section
- Add unit tests for summarize_query_timings and output writers
This commit is contained in:
2026-05-27 20:28:50 +08:00
parent 715a2c2ed6
commit 42d4a9728d
4 changed files with 304 additions and 20 deletions

View File

@@ -15,7 +15,7 @@ CAM 顶层测试的共享辅助函数。
from __future__ import annotations
import numpy as np
from cocotb.triggers import RisingEdge
from cocotb.triggers import ReadOnly, RisingEdge, Timer
from model.ref_model import ( # noqa: E402
match_top1,
unpack_score_debug_flat,
@@ -202,28 +202,73 @@ async def query_topk_once(dut, query, timeout_cycles=None):
- beats: [(rank, row, score, last), ...]
- score_debug: np.ndarray 或 NoneSIM_DEBUG 模式)
"""
beats, top1_index, top1_score, score_debug, _ = await query_topk_once_with_latency(
dut, query, timeout_cycles=timeout_cycles,
)
return beats, top1_index, top1_score, score_debug
async def query_topk_once_with_latency(dut, query, timeout_cycles=None):
"""发起一次查询、收集完整 Top-K 结果流,并返回周期计数。
返回:(beats, top1_index, top1_score, score_debug, timing)
``timing`` 字段:
- accept_to_first_result_cycles: query 接受到首个 result_valid beat
- accept_to_last_result_cycles: query 接受到 result_last beatTop-K 完成)
- total_query_cycles: 从拉高 query_valid 到 Top-K 完成的总上升沿数
``query_ready`` 是组合信号,握手周期在上升沿前采样;结果信号在
ReadOnly settled phase 采样,避免重新引入 query_ready 采样时序问题。
"""
await wait_idle(dut)
dut.query_hash.value = int(query)
# 等待 query_ready 为高DUT 已就绪),避免组合逻辑下降沿导致的
# valid&&ready 握手丢失问题
# valid&&ready 握手丢失问题
while not int(dut.query_ready.value):
await RisingEdge(dut.clk)
# assert query_valid 覆盖一个上升沿完成握手
edge_count = 0
dut.query_valid.value = 1
dut.result_ready.value = 1
await RisingEdge(dut.clk)
edge_count += 1
q_ready = int(dut.query_ready.value)
assert q_ready, "Query accept handshake was missed despite query_ready pre-wait"
accept_edge = edge_count
dut.query_valid.value = 0
# 若调用者未指定超时,根据 DUT 参数动态估算
if timeout_cycles is None:
timeout_cycles = dut_query_timeout_cycles(dut)
# 消费完整串行结果流
beats = await collect_topk(dut, timeout_cycles=timeout_cycles)
beats = []
first_result_edge = None
last_result_edge = None
for _ in range(timeout_cycles):
await RisingEdge(dut.clk)
edge_count += 1
await ReadOnly()
if int(dut.result_valid.value):
if first_result_edge is None:
first_result_edge = edge_count
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:
last_result_edge = edge_count
await Timer(1, units="step")
break
await Timer(1, units="step")
if first_result_edge is None or last_result_edge is None:
raise AssertionError("Top-K result stream did not finish")
# score_debug 在查询完成后可用(需 SIM_DEBUG 编译)
num_rows = dut_num_rows(dut)
score_bits = dut_score_bits(dut)
score_debug = None
@@ -234,7 +279,12 @@ async def query_topk_once(dut, query, timeout_cycles=None):
score_bits,
)
return beats, beats[0][1], beats[0][2], score_debug
timing = {
"accept_to_first_result_cycles": int(first_result_edge - accept_edge),
"accept_to_last_result_cycles": int(last_result_edge - accept_edge),
"total_query_cycles": int(edge_count),
}
return beats, beats[0][1], beats[0][2], score_debug, timing
async def query_once(dut, query, timeout_cycles=None):