fix(pipeline): add S_WAIT_READ_RESP state to fix read-noise PRNG timing

- Emit rd_valid_o for exactly one combinational cycle before waiting for
  read response, ensuring the read-noise PRNG advances once per batch issue
- Fix query handshake: wait for query_ready before asserting query_valid
  to avoid valid&&ready handshake drops on clock edges
- Add dynamic timeout estimation in test utilities based on DUT parameters
- Update test-top Makefile to run all noise configurations by default
- Remove uv run prefix from cocotb-config Makefile invocation
This commit is contained in:
2026-05-22 15:06:26 +08:00
parent 424cf6e1d3
commit 29f4cc91f6
7 changed files with 60 additions and 26 deletions

View File

@@ -50,13 +50,6 @@ remote-dry cmd:
# ── CAM verification ──────────────────────────────────────────────────────────
# Run all 4 integrated CAM configurations (no-noise, write-only, read-only, combined)
cam-test-all:
just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=0 READ_NOISE_EN=0"
just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 READ_NOISE_EN=0"
just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=0 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100"
just remote "make -C hw/sim clean && make -C hw/sim test-top NUM_ROWS=64 LANES=8 HASH_BITS=512 WRITE_NOISE_EN=1 WRITE_NOISE_RATE_NUM=1 WRITE_NOISE_RATE_DEN=100 READ_NOISE_EN=1 READ_NOISE_RATE_NUM=1 READ_NOISE_RATE_DEN=100"
# Run all CAM tests in one pass (model + top + all modules)
cam-test-full:
just remote "make -C hw/sim clean && make -C hw/sim test-all"

View File

@@ -15,3 +15,4 @@ outputs
**/__pycache__
**/sim_build
**/__marimo__
*.fst

View File

@@ -31,9 +31,10 @@ module match_engine_pipeline (
//==========================================================================
// State encoding
//==========================================================================
typedef enum logic [2:0] {
typedef enum logic [3:0] {
S_IDLE,
S_ISSUE_READ,
S_WAIT_READ_RESP,
S_WAIT_SCORE,
S_STAGE_CANDIDATES,
S_PUSH_CANDIDATES,
@@ -373,6 +374,13 @@ module match_engine_pipeline (
end
S_ISSUE_READ: begin
// Emit rd_valid_o for exactly one cycle (combinational), then
// transition to S_WAIT_READ_RESP. This ensures the read-noise
// PRNG in cam_read_noise only advances once per batch issue.
state_q <= S_WAIT_READ_RESP;
end
S_WAIT_READ_RESP: begin
if (rd_valid_i) begin
// Capture staging data for popcount pipeline
rd_stage_valid_q <= 1'b1;
@@ -380,8 +388,7 @@ module match_engine_pipeline (
rd_stage_hashes_q <= rd_hashes_i;
rd_stage_lane_valid_q <= rd_lane_valid_i;
// Remember the lane mask for this batch (used in S_WAIT_SCORE
// to determine batch_scores_done — survives until overwritten
// by the next S_ISSUE_READ).
// to determine batch_scores_done).
batch_lane_mask_q <= rd_lane_valid_i;
issue_base_q <= issue_base_q + `LANES;
issued_batches_q <= issued_batches_q + 1;

View File

@@ -7,7 +7,7 @@ TOP_CONFIGS := no_noise write_noise read_noise
help:
@echo "Available hw/sim targets:"
@echo " make test-model"
@echo " make test-top # 只运行默认顶层配置 (no_noise)"
@echo " make test-top # 默认运行所有顶层噪声配置"
@echo " make test-top-all # 运行所有顶层噪声配置"
@echo " make test-top-no_noise # 无噪声集成测试"
@echo " make test-top-write_noise # 写入噪声集成测试"
@@ -20,7 +20,7 @@ help:
test-all: test-model test-top-all test-modules
test-top: test-top-no_noise
test-top: test-top-all
test-top-all: $(TOP_CONFIGS:%=test-top-%)

View File

@@ -79,4 +79,4 @@ COMPILE_ARGS += -Wno-WIDTHEXPAND
COMPILE_ARGS += -Wno-UNOPTFLAT
endif
include $(shell uv run cocotb-config --makefiles)/Makefile.sim
include $(shell cocotb-config --makefiles)/Makefile.sim

View File

@@ -35,6 +35,7 @@ from model.ref_model import (
from tests.top.utils import (
collect_topk,
dut_hash_bits,
dut_lanes,
dut_num_rows,
query_once,
query_topk_once,
@@ -44,7 +45,6 @@ from tests.top.utils import (
write_rows,
)
# ═══════════════════════════════════════════════════════════════════════════════
# 编译冒烟测试 — 验证 cam_top 能正确 elaborate
# ═══════════════════════════════════════════════════════════════════════════════
@@ -343,6 +343,8 @@ async def query_scan_blocks_writes_until_result_consumed(dut):
dut.wr_valid.value = 0
# Consume full serial stream so the DUT returns idle
beats = await collect_topk(dut, timeout_cycles=2000)
beats = await collect_topk(
dut, timeout_cycles=max(2000, (dut_num_rows(dut) // dut_lanes(dut)) * 24 + 100)
)
assert len(beats) > 0
assert beats[-1][3] == 1 # last asserted

View File

@@ -170,14 +170,33 @@ async def collect_topk(dut, timeout_cycles: int = 2000):
raise AssertionError("Top-K result stream did not finish")
async def query_topk_once(dut, query, timeout_cycles=2000):
# ── 默认超时估算 ──────────────────────────────────────────────────
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_valid/query_ready 握手发送查询
3. 消费完整的结果流
4. 读取 score_debug_flat如果存在
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), ...]
@@ -186,16 +205,21 @@ async def query_topk_once(dut, query, timeout_cycles=2000):
await wait_idle(dut)
dut.query_hash.value = int(query)
dut.query_valid.value = 1
# 等待查询握手完成
while True:
# 等待 query_ready 为高DUT 已就绪),避免组合逻辑下降沿导致的
# valid&&ready 握手丢失问题
while not int(dut.query_ready.value):
await RisingEdge(dut.clk)
if int(dut.query_ready.value):
break
# assert query_valid 覆盖一个上升沿完成握手
dut.query_valid.value = 1
await RisingEdge(dut.clk)
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)
@@ -213,12 +237,19 @@ async def query_topk_once(dut, query, timeout_cycles=2000):
return beats, beats[0][1], beats[0][2], score_debug
async def query_once(dut, query):
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)
_, top1_index, top1_score, score_debug = await query_topk_once(
dut, query, timeout_cycles=timeout_cycles,
)
return top1_index, top1_score, score_debug