Files
Mini-Nav/hw/sim/tests/top/utils.py
SikongJueluo 42d4a9728d 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
2026-05-28 13:05:34 +08:00

338 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
CAM 顶层测试的共享辅助函数。
被 no_noise/、write_noise/、read_noise/ 等配置目录的测试文件共同引用。
提供:
- Verilator 参数读取(带回退推断)
- DUT 复位、空闲等待
- 行写入 / 批量写入(握手协议)
- 查询发起 / Top-K 结果流收集
- score_debug 解包SIM_DEBUG 模式)
所有函数都是 async调用者需要处于 cocotb 协程上下文中。
"""
from __future__ import annotations
import numpy as np
from cocotb.triggers import ReadOnly, RisingEdge, Timer
from model.ref_model import ( # noqa: E402
match_top1,
unpack_score_debug_flat,
)
# ── 默认拓扑参数(当 Verilator 参数不可用时使用) ───────────────────────────
DEFAULT_NUM_ROWS = 4096
DEFAULT_HASH_BITS = 512
DEFAULT_LANES = 8
DEFAULT_SCORE_BITS = 10
# ═══════════════════════════════════════════════════════════════════════════════
# 参数读取(从 Verilator 参数或信号宽度推断)
# ═══════════════════════════════════════════════════════════════════════════════
def get_param(dut, name, default=None):
"""从 DUT 读取 Verilator 暴露的参数值,失败则返回 default。
某些参数仅在特定编译配置下暴露(如 SIM_DEBUG 下的 score_debug_flat
此时回退到 default 是预期行为而非错误。
"""
try:
val = getattr(dut, name, None)
if val is not None:
return int(val.value)
except Exception:
pass
return default
def dut_num_rows(dut):
"""获取 NUM_ROWS优先读参数否则从 wr_addr 位宽推断 (NUM_ROWS = 2^ROW_BITS)。"""
val = get_param(dut, "NUM_ROWS", None)
if val is not None:
return val
return 1 << len(dut.wr_addr)
def dut_hash_bits(dut):
"""获取 HASH_BITS优先读参数否则从 write_hash 信号位宽推断。"""
val = get_param(dut, "HASH_BITS", None)
if val is not None:
return val
return len(dut.write_hash)
def dut_lanes(dut):
"""获取 LANES优先读参数否则从 rd_resp_row_ids / wr_addr 位宽推断。"""
val = get_param(dut, "LANES", None)
if val is not None:
return val
return len(dut.rd_resp_row_ids) // len(dut.wr_addr)
def dut_score_bits(dut):
"""获取 SCORE_BITS优先读参数否则从 top1_score 信号位宽推断。"""
val = get_param(dut, "SCORE_BITS", None)
if val is not None:
return val
return len(dut.top1_score)
# ═══════════════════════════════════════════════════════════════════════════════
# 协议层辅助函数(复位 / 空闲等待 / 行写入 / 查询)
# ═══════════════════════════════════════════════════════════════════════════════
async def reset_dut(dut):
"""复位 DUTrst_n 拉低 5 周期,释放后再等 2 周期。
所有控制信号在复位期间保持无效电平。
result_ready 初始化为 1准备接收结果
"""
dut.rst_n.value = 0
dut.wr_valid.value = 0
dut.wr_addr.value = 0
dut.write_hash.value = 0
dut.query_valid.value = 0
dut.query_hash.value = 0
dut.result_ready.value = 1
for _ in range(5):
await RisingEdge(dut.clk)
dut.rst_n.value = 1
for _ in range(2):
await RisingEdge(dut.clk)
async def wait_idle(dut):
"""等待 DUT 完全空闲wr_ready=1 且 query_ready=1。
这是发起新操作前的前置条件——CAM 是半双工的,
同一时刻只能进行写入或查询。
"""
while not (int(dut.wr_ready.value) and int(dut.query_ready.value)):
await RisingEdge(dut.clk)
async def write_row(dut, addr, value):
"""写入单行:使用 wr_valid/wr_ready 握手协议。
流程:等待空闲 → 驱动地址和数据 → 等待握手完成 → 等待写流水线排空。
"""
await wait_idle(dut)
dut.wr_addr.value = addr
dut.write_hash.value = int(value)
dut.wr_valid.value = 1
while True:
await RisingEdge(dut.clk)
if int(dut.wr_ready.value):
break
dut.wr_valid.value = 0
await wait_idle(dut)
async def write_rows(dut, rows):
"""按顺序写入所有行(行索引 = 数组下标)。"""
for idx, value in enumerate(rows):
await write_row(dut, idx, value)
async def collect_topk(dut, timeout_cycles: int = 2000):
"""收集串行 Top-K 结果流的所有 beat。
保持 result_ready=1逐个时钟周期采样 result_valid
直到 result_last 被断言。
返回:[(rank, row, score, last), ...] 列表
超时则抛出 AssertionError。
注意:此函数会「消耗」整个结果流,调用后 DUT 回到空闲状态。
"""
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")
# ── 默认超时估算 ──────────────────────────────────────────────────
def dut_query_timeout_cycles(dut):
"""基于 DUT 参数估算完整查询(扫描 + 串行结果输出)的超时周期数。
各通道串行输出一个 beat 需要约 24 个流水线周期;
总超时 = ceil(全部行数 / 通道数) * 24 + 固定裕量 2000 周期。
至少返回 2000 周期以防止极小配置下的不合理值。
Example: 4096 rows / 8 lanes → ceil(4096/8) * 24 + 2000 = 14288 cycles.
"""
num_rows = dut_num_rows(dut)
lanes = dut_lanes(dut)
batches = (num_rows + lanes - 1) // lanes # ceil division, no math import
return max(2000, batches * 24 + 2000)
async def query_topk_once(dut, query, timeout_cycles=None):
"""发起一次查询并收集完整的串行 Top-K 结果流。
完整流程:
1. 等待 DUT 空闲
2. 等待 query_ready 为高
3. 通过 query_valid/query_ready 握手发送查询
4. 消费完整的结果流
5. 读取 score_debug_flat如果存在
返回:(beats, top1_index, top1_score, score_debug)
- 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 握手丢失问题。
while not int(dut.query_ready.value):
await RisingEdge(dut.clk)
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
if timeout_cycles is None:
timeout_cycles = dut_query_timeout_cycles(dut)
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")
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,
)
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):
"""发起查询,返回 (top1_index, top1_score, score_debug)。
内部调用 query_topk_once 并消费完整结果流,仅保留 rank-0 数据。
Parameters
----------
timeout_cycles : int or None
传递给 query_topk_once 的超时周期数。None 表示根据 DUT 参数动态估算。
"""
_, top1_index, top1_score, score_debug = await query_topk_once(
dut, query, timeout_cycles=timeout_cycles,
)
return top1_index, top1_score, score_debug
# ═══════════════════════════════════════════════════════════════════════════════
# 便捷验证函数
# ═══════════════════════════════════════════════════════════════════════════════
def assert_baseline_top1(query, rows, top1_index, top1_score, hash_bits,
query_index, score_debug=None):
"""验证无噪声场景下的基线 Top-1 结果。
检查项:
1. Top-1 与 match_top1 参考模型一致
2. 查询自身所在行 → score == hash_bits完全匹配
3. score_debug 数组(如果存在)与模型一致
"""
expected = match_top1(query, rows, width=hash_bits)
assert top1_index == expected.top1_index, (
f"top1_index mismatch: {top1_index} != {expected.top1_index}"
)
assert top1_score == expected.top1_score, (
f"top1_score mismatch: {top1_score} != {expected.top1_score}"
)
assert top1_index == query_index, (
f"Expected query_index={query_index} to match self, got top1_index={top1_index}"
)
assert top1_score == hash_bits, (
f"Self-query should score {hash_bits}, got {top1_score}"
)
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores), (
"score_debug does not match model scores"
)