Files
Mini-Nav/hw/sim/tests/model/test_ref_model_noise.py
SikongJueluo e1bed00cc4 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
2026-05-22 19:04:54 +08:00

222 lines
8.2 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 -*-
"""
参考模型ref_model的纯 Python 单元测试。
本文件不涉及任何 RTL / Verilator 仿真,仅验证 Python 参考模型的正确性。
所有 RTL-vs-模型 的对比测试(如顶层 test_cam_basic.py都依赖此参考模型
因此这里是整个测试体系的「基石」——参考模型如果有 bug所有对比测试都将失效。
测试覆盖:
1. 分组翻转掩码 — 完全速率 (rate=1/1) 的正确位翻转模式
2. 分组翻转掩码 — 零速率 (rate=0/100) 不应产生任何翻转
3. 评分函数语义 — 确认是「匹配位数」而非「汉明距离」
4. 读取噪声模型 — 相同输入 + 相同种子 = 可复现结果
"""
from __future__ import annotations
from model.ref_model import (
generate_grouped_flip_mask,
match_top1_with_read_noise,
xnor_popcount_score,
)
# ==============================================================================
# 测试 1完全速率下的分组翻转掩码生成
# ==============================================================================
def test_grouped_flip_mask_full_rate_one_bit_per_64_bit_group():
"""
验证 generate_grouped_flip_mask 在 rate_num=1, rate_den=1 时的行为。
背景:
- CAM 的 write noise 模块将 512-bit 哈希按 64-bit 分组,每组最多翻转 1 位。
- random_value 的位域含义(每 group 14 bits
bits [5:0] → sample未使用
bits [13:6] → bit_idx选择该组内翻转哪一位
本测试:
- 构造一个 random_value使每个 group 的 bit_idx = group+1
- 断言生成的 mask 恰好有 8 个位被置位(每 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, # 8 组 × 64 bits/组
noise_bits=8, # 每组的 bit_idx 位宽
rate_num=1, # 分子 = 1
rate_den=1, # 分母 = 1 → 100% 概率,每组都翻转
)
# 预期:每组的 bit_idx 位被翻转
expected = 0
for group in range(8):
expected |= 1 << (group * 64 + group + 1)
assert mask == expected
assert mask.bit_count() == 8 # 恰好 8 位被翻转(每组一位)
# ==============================================================================
# 测试 2零速率下不应产生任何翻转
# ==============================================================================
def test_grouped_flip_mask_zero_rate_no_flips():
"""
验证 rate_num=0 时,无论 random_value 为何值mask 都应为 0。
这是写入噪声的「零噪声」配置边界测试——
确保 RTL 参数 WRITE_NOISE_RATE_NUM=0 能真正关闭噪声注入。
"""
mask = generate_grouped_flip_mask(
random_value=(1 << 128) - 1, # 全 1 的 random_value
hash_bits=512,
noise_bits=8,
rate_num=0, # 分子 = 0 → 翻转概率为 0
rate_den=100,
)
assert mask == 0 # mask 必须全 0一个位都不翻
# ==============================================================================
# 测试 3评分函数语义 — 确认是「XNOR 匹配位数」而非「汉明距离」
# ==============================================================================
def test_score_is_bit_match_popcount_not_hamming_distance():
"""
关键语义验证xnor_popcount_score 计算的是匹配位的数量,不是汉明距离。
示例:
query = 0b1010
stored = 0b1000
XNOR = 0b1101 → popcount = 3有 3 个位匹配)
汉明距离 = 1 → 只有一个位不同
为什么这个区分很重要:
- 如果 RTL 或模型错误地使用了汉明距离作为分数,则:
完全匹配的分数会是 0 而非 hash_bits512
Top-K 排序会颠倒(分数低的反而排前面)
- 这会导致整个 CAM 检索系统返回错误结果
"""
query = 0b1010
stored = 0b1000
assert xnor_popcount_score(query, stored, width=4) == 3
# ==============================================================================
# 测试 4读取噪声模型的可复现性确定性种子
# ==============================================================================
def test_read_noise_model_is_reproducible_after_reset_seed():
"""
验证 match_top1_with_read_noise 在相同参数下产生相同结果。
为什么这个测试至关重要:
- RTL 中的 read noise PRNG 使用固定种子 (0x6A09E667F3BCC909)
- 参考模型必须使用相同的种子来复现 RTL 的噪声行为
- 如果两次调用结果不同,说明模型存在非确定性 bug
(如未重置 PRNG 状态、或使用了非确定性随机源)
测试数据:
- 8 行不同模式的 512-bit 哈希全0、全1、稀疏值
- 噪声配置rate=1%, lanes=8, noise_bits=8
"""
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)
# 两次调用的 Top-1 结果和分数数组必须完全一致
assert first.top1_index == second.top1_index
assert first.top1_score == second.top1_score
assert first.scores.tolist() == second.scores.tolist()
# ==============================================================================
# 测试 5Top-K 排序 — 分数降序、平局行号升序
# ==============================================================================
def test_match_topk_from_scores_uses_score_desc_then_row_asc():
"""Top-K 排序规则:分数越大越优先;分数相同时行号越小越优先。"""
from model.ref_model import match_topk_from_scores
import numpy as np
scores = np.array([7, 9, 9, 2, 7], dtype=np.int32)
assert match_topk_from_scores(scores, 4) == [1, 2, 0, 4]
def test_match_topk_scores_rows_by_xnor_popcount():
"""match_topk 通过 xnor_popcount 计算分数,返回排序后的行索引和分数数组。"""
from model.ref_model import match_topk
rows = [0b0000, 0b1111, 0b0011, 0b0101]
query = 0b0000
indices, scores = match_topk(query, rows, width=4, k=3)
assert scores.tolist() == [4, 0, 2, 2]
assert indices == [0, 2, 3]
def test_match_topk_clamps_k_to_row_count():
"""当 k 超过实际行数时,返回所有行(按排序)。"""
from model.ref_model import match_topk
indices, scores = match_topk(0, [0, 1], width=1, k=5)
assert scores.tolist() == [1, 0]
assert indices == [0, 1]
# ==============================================================================
# 测试 6读取噪声 stateful 评分助手的跨查询状态推进
# ==============================================================================
def test_score_rows_with_read_noise_stateful_across_queries():
"""score_rows_with_read_noise 在多次调用间正确推进 lane PRNG 状态。
两次调用使用相同的 rows/query 和零噪声率:
- 分数应一致(无噪声翻转)
- 但 lane states 应该变化PRNG 已推进)
"""
from model.ref_model import score_rows_with_read_noise
rows = [0, 0, 0, 0]
query = 0
lane_states = [1, 2]
scores_1, next_states_1 = score_rows_with_read_noise(
query, rows, lane_states=lane_states, width=128, lanes=2,
noise_bits=2, rate_num=0, rate_den=100,
)
scores_2, next_states_2 = score_rows_with_read_noise(
query, rows, lane_states=next_states_1, width=128, lanes=2,
noise_bits=2, rate_num=0, rate_den=100,
)
assert scores_1.tolist() == [128, 128, 128, 128]
assert scores_2.tolist() == [128, 128, 128, 128]
assert next_states_1 != lane_states
assert next_states_2 != next_states_1