Files
Mini-Nav/hw/sim/tests/top/write_noise/test_write_noise.py
SikongJueluo 424cf6e1d3 refactor(hw/sim): reorganize CAM top-level tests into per-noise-config suites
Split the monolithic test_cam_basic.py into separate test suites
organized by noise configuration (no_noise, write_noise, read_noise),
with shared utilities extracted to tests/top/utils.py.

- Remove test_cam_basic.py; add no_noise/, write_noise/, read_noise/
  test suites with Makefiles that set noise parameters statically
- Extract helpers (reset_dut, write_rows, query_once, collect_topk,
  etc.) into tests/top/utils.py
- Update hw/sim/Makefile with per-config test targets and a
  test-top-all meta-target
- Update scripts/run_cam_correctness.py to build per-directory
  instead of centrally, removing inline parameter overrides
- Add __init__.py for result_serializer and topk_tracker module tests
- Expand docstrings in test_ref_model_noise.py with architectural
  context and test rationale
2026-05-21 21:22:12 +08:00

275 lines
10 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 写入噪声Write Noise集成测试 —— 专用配置。
本文件测试 WRITE_NOISE_EN=1, READ_NOISE_EN=0 配置下,
写入噪声模块的正确性。默认噪声率约 1%NUM=1, DEN=100
=== 测试列表 ===
1. default_noise_reproducible — 固定种子 = 确定性噪声,两次运行结果一致
2. exact_noise_model_match — RTL 存储的哈希与 ref_model.py 的 PRNG 掩码逐位匹配
3. zero_rate_noise — 写入噪声模块连接但 RATE_NUM=0 → 无翻转
4. full_rate_noise — 100% 写入噪声率,与 Python 模型对比
=== 架构背景 ===
写入噪声流水线位置Write Noise → Banked Core Storage → Read Noise → Match Engine
本测试覆盖完整的 cam_top 链路,写入噪声为唯一活跃噪声源。
=== Makefile 子目标 ===
test-zero-rate : make test-zero-rate (WRITE_NOISE_RATE_NUM=0)
test-full-rate : make test-full-rate (WRITE_NOISE_RATE_NUM=1, RATE_DEN=1, SIM_DEBUG)
"""
from __future__ import annotations
import cocotb
import numpy as np
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge
from model.ref_model import (
generate_write_flip_mask,
match_top1,
random_hashes,
)
from tests.top.utils import (
collect_topk,
dut_hash_bits,
dut_num_rows,
get_param,
query_once,
query_topk_once,
reset_dut,
wait_idle,
write_row,
write_rows,
)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 1默认噪声 ~1%、可复现
# ── 固定种子 → 两次相同写入产生相同结果
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def default_noise_reproducible(dut):
"""可复现性测试:相同种子、相同数据 → 两次独立运行结果一致。
流程:
1. 写入全部行,查询 row 50 → 记录 top1_index 和 top1_score
2. 复位
3. 再次写入相同数据,查询相同行 → 记录结果
4. 断言两次结果完全一致
如果结果不一致,说明 RTL 的 PRNG 状态没有正确复位,
或存在跨运行的状态残留。
"""
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
num_rows = dut_num_rows(dut)
hash_bits = dut_hash_bits(dut)
rng = np.random.default_rng(42)
rows = random_hashes(rng, num_rows, width=hash_bits)
await write_rows(dut, rows)
query = rows[min(50, num_rows - 1)]
top1_index_1, top1_score_1, _ = await query_once(dut, query)
await reset_dut(dut)
await write_rows(dut, rows)
top1_index_2, top1_score_2, _ = await query_once(dut, query)
assert top1_index_1 == top1_index_2
assert top1_score_1 == top1_score_2
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 2精确 RTL-vs-模型 PRNG 掩码匹配
# ── RTL 存储的哈希与 ref_model.py 生成的掩码逐位一致
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def exact_noise_model_match(dut):
"""精确噪声模型匹配RTL 的 PRNG 输出必须与 Python 参考模型逐位一致。
测试方法:
1. 用固定 RTL seed 和已知噪声参数,在 Python 中预计算每行的 flip 掩码
2. 预期存储值 = 原始值 XOR flip_mask
3. 写入原始值到 RTL查询预期存储值
4. 断言每行 score = HASH_BITS完全匹配
这验证了 RTL 的 LFSR 实现与 Python 模型的 PRNG 使用相同的
多项式、相同的位宽、相同的种子初始化序列。
"""
if not hasattr(dut, "score_debug_flat"):
dut._log.info("Skipping exact_noise_model_match: requires SIM_DEBUG.")
return
rtol = None
atol = None
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
noise_bits = get_param(dut, "WRITE_NOISE_BITS", 8)
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
n_test_rows = 4
rng = np.random.default_rng(99)
rows = random_hashes(rng, n_test_rows, width=hash_bits)
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
expected_stored = []
for row in rows:
flip, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_stored.append(row ^ flip)
for idx, val in enumerate(rows):
await write_row(dut, idx, val)
for idx, expected in enumerate(expected_stored):
top1_index, top1_score, score_debug = await query_once(dut, expected)
assert score_debug is not None, (
"score_debug required for mask match verification"
)
assert int(score_debug[idx]) == hash_bits, (
f"Row {idx}: expected stored hash to match model prediction, "
f"score={score_debug[idx]} != {hash_bits}"
)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 3零噪声率WRITE_NOISE_EN=1, RATE_NUM=0
# ── 噪声模块已连接但翻转概率为 0 → 行为应与无噪声一致
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def zero_rate_noise(dut):
"""零速率噪声WRITE_NOISE_RATE_NUM=0 时不应有任何位被翻转。
这是噪声模块的边界测试——验证 RATE_NUM=0 确实禁用了翻转,
而非产生「默认速率」的噪声。
"""
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
if rate_num != 0:
dut._log.info(
"Skipping zero_rate_noise: requires WRITE_NOISE_RATE_NUM=0."
)
return
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
num_rows = dut_num_rows(dut)
hash_bits = dut_hash_bits(dut)
rng = np.random.default_rng(1)
rows = random_hashes(rng, num_rows, width=hash_bits)
query_index = min(123, num_rows - 1)
query = rows[query_index]
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, query)
expected = match_top1(query, rows, width=hash_bits)
assert top1_index == expected.top1_index
assert top1_score == expected.top1_score
assert top1_index == query_index
assert top1_score == hash_bits
if score_debug is not None:
assert np.array_equal(score_debug, expected.scores)
# ═══════════════════════════════════════════════════════════════════════════════
# 测试 4100% 噪声率RATE_NUM=1, RATE_DEN=1
# ── 每组都翻转 → 精确验证 PRNG 掩码生成
# ═══════════════════════════════════════════════════════════════════════════════
@cocotb.test()
async def full_rate_noise(dut):
"""完全速率噪声:每组 100% 翻转概率。
使用固定 RTL seed (0xB504F32DB504F32D),用 Python 模型预计算
写入全 0 和全 1 行后应存储的哈希值,然后验证 RTL 实际存储的哈希
与模型预测完全一致。
这是最低容忍度的噪声测试——要求 score_debug_flatSIM_DEBUG
且每行的分数必须精确等于 HASH_BITS。
"""
rate_num = get_param(dut, "WRITE_NOISE_RATE_NUM", 1)
rate_den = get_param(dut, "WRITE_NOISE_RATE_DEN", 100)
if rate_num != 1 or rate_den != 1:
dut._log.info(
"Skipping full_rate_noise: requires WRITE_NOISE_RATE_NUM=1, RATE_DEN=1."
)
return
if not hasattr(dut, "score_debug_flat"):
dut._log.info(
"Skipping full_rate_noise: requires SIM_DEBUG (score_debug_flat)."
)
return
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
hash_bits = dut_hash_bits(dut)
num_rows = dut_num_rows(dut)
noise_bits = get_param(dut, "WRITE_NOISE_BITS", 8)
all_zero = 0
all_one = (1 << hash_bits) - 1
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
flip0, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_row0 = all_zero ^ flip0
flip1, prng_state = generate_write_flip_mask(
prng_state,
hash_bits,
noise_bits,
rate_num,
rate_den,
)
expected_row1 = all_one ^ flip1
rows = [0] * num_rows
rows[0] = all_zero
rows[1] = all_one
await write_rows(dut, rows)
top1_index, top1_score, score_debug = await query_once(dut, expected_row0)
assert score_debug is not None, "score_debug required for full_rate_noise"
assert int(score_debug[0]) == hash_bits, (
f"Row 0: expected exact match, score={score_debug[0]} != {hash_bits}"
)
top1_index, top1_score, score_debug = await query_once(dut, expected_row1)
assert score_debug is not None
assert int(score_debug[1]) == hash_bits, (
f"Row 1: expected exact match, score={score_debug[1]} != {hash_bits}"
)