feat(cam): migrate noise generation from xorshift64 to xorshift128

- Replace NOISE_GEN_BITS/NOISE_SAMPLE_BITS parameters with unified NOISE_BITS
- Use xorshift128 (random128) instead of xorshift64 for PRNG
- Add flip_mask_next combinational helper for single-cycle mask computation
- Add random_enable signal to advance PRNG only on accepted noisy writes
- Simplify FSM by removing mask_group_idx counter
- Update parameter validation: GROUP_BITS (= HASH_BITS/NOISE_BITS) must equal 64
- Update ref_model.py and tests to match new seed convention: {seed, seed}
- Update Makefile and sweep_noise.py with renamed parameters
This commit is contained in:
2026-05-05 19:30:51 +08:00
parent 0dd01fb1b7
commit cbafc4524e
8 changed files with 178 additions and 152 deletions

View File

@@ -13,10 +13,10 @@ LANES ?= 16
NOISE_EN ?= 1
NOISE_RATE_NUM ?= 1
NOISE_RATE_DEN ?= 100
NOISE_GEN_BITS ?= 8
NOISE_SAMPLE_BITS ?= 8
NOISE_BITS ?= 8
# NOISE_SEED cannot be overridden via Makefile due to Verilator -G quoting issues
# with 64'h hex literals. To change the seed, edit the default in cam_noisy.sv.
# with 64'h hex literals. To change the seed, edit the default in cam_top.sv
# (sim top is cam_top, which passes it to cam_noisy).
EXTRA_ARGS += +define+NUM_ROWS=$(NUM_ROWS) +define+HASH_BITS=$(HASH_BITS) +define+LANES=$(LANES)
@@ -29,8 +29,7 @@ COMPILE_ARGS += $(EXTRA_DEFINES)
COMPILE_ARGS += -GNOISE_EN=$(NOISE_EN)
COMPILE_ARGS += -GNOISE_RATE_NUM=$(NOISE_RATE_NUM)
COMPILE_ARGS += -GNOISE_RATE_DEN=$(NOISE_RATE_DEN)
COMPILE_ARGS += -GNOISE_GEN_BITS=$(NOISE_GEN_BITS)
COMPILE_ARGS += -GNOISE_SAMPLE_BITS=$(NOISE_SAMPLE_BITS)
COMPILE_ARGS += -GNOISE_BITS=$(NOISE_BITS)
# Cleaner terminal output
export QUIET ?= 1
@@ -48,6 +47,7 @@ endif
VERILOG_SOURCES += $(PWD)/../rtl/popcount.sv
VERILOG_SOURCES += $(PWD)/../rtl/argmax_update.sv
VERILOG_SOURCES += $(PWD)/../rtl/cam_core.sv
VERILOG_SOURCES += $(PWD)/../rtl/random/random128.sv
VERILOG_SOURCES += $(PWD)/../rtl/cam_noisy.sv
VERILOG_SOURCES += $(PWD)/../rtl/match_engine.sv
VERILOG_SOURCES += $(PWD)/../rtl/cam_top.sv

View File

@@ -53,50 +53,50 @@ def match_top1(
)
def xorshift64(state: int) -> int:
"""64-bit XOR-shift PRNG, single step. Matches RTL behavior."""
mask64 = (1 << 64) - 1
s = state & mask64
s ^= (s << 13) & mask64
s ^= (s >> 7) & mask64
s ^= (s << 17) & mask64
return s
def xorshift128(state: int) -> int:
"""128-bit xorshift PRNG, single step. Matches random128.sv."""
mask32 = (1 << 32) - 1
mask128 = (1 << 128) - 1
s = state & mask128
x = (s >> 96) & mask32
y = (s >> 64) & mask32
z = (s >> 32) & mask32
w = s & mask32
t = (x ^ ((x << 11) & mask32)) & mask32
next_x = y
next_y = z
next_z = w
next_w = (w ^ (w >> 19) ^ t ^ (t >> 8)) & mask32
return ((next_x << 96) | (next_y << 64) | (next_z << 32) | next_w) & mask128
def generate_write_flip_mask(
prng_state: int,
hash_bits: int,
noise_gen_bits: int,
noise_sample_bits: int,
noise_bits: int,
rate_num: int,
rate_den: int,
) -> tuple[int, int]:
"""
Generate write-noise flip mask.
Returns (flip_mask, next_prng_state).
Matches RTL multi-cycle GEN_MASK behavior.
"""Generate one write-noise flip mask using one xorshift128 step."""
assert hash_bits % noise_bits == 0
group_bits = hash_bits // noise_bits
bit_index_bits = 6
sample_bits = 8
group_random_bits = bit_index_bits + sample_bits
assert group_bits == 64
assert noise_bits * group_random_bits <= 128
Each cycle processes noise_gen_bits bit decisions:
- Advance xorshift64 → 64-bit output
- Split into noise_gen_bits x noise_sample_bits-bit samples
- Each sample < THRESHOLD → that bit flips
"""
assert hash_bits % noise_gen_bits == 0
assert noise_gen_bits * noise_sample_bits == 64
mask = 0
state = prng_state
sample_range = 1 << noise_sample_bits
sample_range = 1 << sample_bits
threshold = (rate_num * sample_range) // rate_den
for bit_offset in range(0, hash_bits, noise_gen_bits):
# Advance PRNG
state = xorshift64(state)
# Split into noise_gen_bits independent samples
for b in range(noise_gen_bits):
sample_b = (state >> (b * noise_sample_bits)) & (sample_range - 1)
if sample_b < threshold:
mask |= (1 << (bit_offset + b))
state = xorshift128(prng_state)
mask = 0
for group_idx in range(noise_bits):
group_rand = (state >> (group_idx * group_random_bits)) & ((1 << group_random_bits) - 1)
bit_idx = group_rand & ((1 << bit_index_bits) - 1)
sample = (group_rand >> bit_index_bits) & (sample_range - 1)
if sample < threshold:
mask |= 1 << (group_idx * group_bits + bit_idx)
return mask, state

View File

@@ -22,17 +22,20 @@ def apply_write_noise(
width: int,
rate_num: int,
rate_den: int,
noise_gen_bits: int = 8,
noise_sample_bits: int = 8,
noise_bits: int = 8,
seed: int = 0,
) -> list[int]:
"""Apply write-noise flip masks to every row, returning noisy copies."""
"""Apply write-noise flip masks to every row, returning noisy copies.
*seed* is a 64-bit value (RTL NOISE_SEED). It is duplicated to form
the 128-bit xorshift initial state: {seed, seed}.
"""
noisy: list[int] = []
state = seed
state = (seed << 64) | seed
mask_w = (1 << width) - 1
for row in rows:
flip, state = generate_write_flip_mask(
state, width, noise_gen_bits, noise_sample_bits, rate_num, rate_den
state, width, noise_bits, rate_num, rate_den
)
noisy.append((row ^ flip) & mask_w)
return noisy
@@ -44,8 +47,7 @@ def main() -> None:
parser.add_argument("--queries", type=int, default=128)
parser.add_argument("--width", type=int, default=512)
parser.add_argument("--seed", type=int, default=1234)
parser.add_argument("--noise-gen-bits", type=int, default=8)
parser.add_argument("--noise-sample-bits", type=int, default=8)
parser.add_argument("--noise-bits", type=int, default=8)
parser.add_argument(
"--rates",
type=float,
@@ -80,8 +82,7 @@ def main() -> None:
width=args.width,
rate_num=rate_num,
rate_den=rate_den,
noise_gen_bits=args.noise_gen_bits,
noise_sample_bits=args.noise_sample_bits,
noise_bits=args.noise_bits,
seed=args.seed,
)

View File

@@ -9,7 +9,6 @@ from model.ref_model import ( # noqa: E402
match_top1,
random_hashes,
unpack_score_debug_flat,
xorshift64,
)
NUM_ROWS = 512
@@ -205,19 +204,45 @@ async def zero_rate_noise(dut):
@cocotb.test()
async def full_rate_noise(dut):
"""THRESHOLD=256 → all bits flip. stored == ~written."""
"""NOISE_RATE_NUM=1, NOISE_RATE_DEN=1 → every group flips its selected bit per write.
At full rate with default NOISE_BITS=8, exactly 8 deterministic bits flip per write
(one selected bit per group), not all 512 bits. We use ref_model.py PRNG to predict
the exact stored rows.
"""
noise_en = _get_param(dut, "NOISE_EN", 1)
rate_num = _get_param(dut, "NOISE_RATE_NUM", 1)
rate_den = _get_param(dut, "NOISE_RATE_DEN", 100)
if not noise_en or rate_num != 1 or rate_den != 1:
dut._log.info("Skipping full_rate_noise: requires NOISE_EN=1, 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)
noise_bits = _get_param(dut, "NOISE_BITS", 8)
all_zero = 0
all_one = (1 << HASH_BITS) - 1
# Predict stored rows using the same RTL seed convention as exact_noise_model_match.
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = (RTL_SEED << 64) | RTL_SEED
# Flip mask for row 0 (all-zero written)
flip0, prng_state = generate_write_flip_mask(
prng_state, HASH_BITS, noise_bits, rate_num, rate_den,
)
expected_row0 = all_zero ^ flip0 # stored value after noise
# Flip mask for row 1 (all-one written)
flip1, prng_state = generate_write_flip_mask(
prng_state, HASH_BITS, noise_bits, rate_num, rate_den,
)
expected_row1 = all_one ^ flip1 # stored value after noise
# Write all-zero to row 0, all-one to row 1, rest zero
rows = [0] * NUM_ROWS
rows[0] = all_zero
@@ -225,20 +250,21 @@ async def full_rate_noise(dut):
await write_rows(dut, rows)
# After 100% flip: row 0 stored = ~0 = all_one, row 1 stored = ~all_one = all_zero
# Query all-zero → matches row 1 (stored all_zero) with score = HASH_BITS
top1_index, top1_score, _ = await query_once(dut, all_zero)
assert top1_index == 1
assert top1_score == HASH_BITS
# Query expected_row0 → should exactly match row 0's stored value
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 for predicted stored value, "
f"score={score_debug[0]} != {HASH_BITS}"
)
# Query all-one → matches row 0 (stored all_one) with score = HASH_BITS
top1_index, top1_score, _ = await query_once(dut, all_one)
assert top1_index == 0
assert top1_score == HASH_BITS
# Query all-zero → score against row 0 (stored all_one) = 0
# Re-query to verify: row 0 stored is all_one, query all_zero → score 0
# But top1 picks row 1 with score HASH_BITS, so top1_index=1
# Query expected_row1 → should exactly match row 1's stored value
top1_index, top1_score, score_debug = await query_once(dut, expected_row1)
assert score_debug is not None, "score_debug required for full_rate_noise"
assert int(score_debug[1]) == HASH_BITS, (
f"Row 1: expected exact match for predicted stored value, "
f"score={score_debug[1]} != {HASH_BITS}"
)
# ── Test D: Default ~1% noise, reproducible ────────────────────────────────
@@ -378,22 +404,21 @@ async def exact_noise_model_match(dut):
cocotb.start_soon(Clock(dut.clk, 10, unit="ns").start())
await reset_dut(dut)
noise_gen_bits = _get_param(dut, "NOISE_GEN_BITS", 8)
noise_sample_bits = _get_param(dut, "NOISE_SAMPLE_BITS", 8)
noise_bits = _get_param(dut, "NOISE_BITS", 8)
# Use a small subset to keep test fast
n_test_rows = 4
rng = np.random.default_rng(99)
rows = random_hashes(rng, n_test_rows, width=HASH_BITS)
# Predict stored hashes with Python model using the same seed
# RTL default seed: 64'hB504_F32D_B504_F32D
# Predict stored hashes with Python model using the same seed.
# RTL random128 seed: {NOISE_SEED, NOISE_SEED}, default 64'hB504_F32D_B504_F32D.
RTL_SEED = 0xB504_F32D_B504_F32D
prng_state = RTL_SEED
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_gen_bits, noise_sample_bits, rate_num, rate_den,
prng_state, HASH_BITS, noise_bits, rate_num, rate_den,
)
expected_stored.append(row ^ flip)