File size: 6,434 Bytes
5c6c847 cf9d631 5c6c847 cf9d631 5c6c847 cf9d631 5c6c847 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | """ZeroPoint 架构自检。运行: python3 tests/test_zeropoint.py"""
import itertools
import json
import os
import sys
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, ".."))
sys.path.insert(0, HERE)
import zerocg as zc # noqa: E402
from models import ZeroX # noqa: E402
from models.zeropoint import CHUNK, MemoryMixer # noqa: E402
from test_zerocg import module_grad_check # noqa: E402
zc.set_default_dtype(np.float64)
rng = np.random.default_rng(0)
# 所有开关的组合(消融实验会用到的每一种都要验证两种算法一致)
OPTIONS = [dict(zip(("gate", "write", "init", "learn_lambda", "pos", "ema"), combo))
for combo in itertools.product((True, False), ("softmax", "sigmoid"), ("hier", "uniform"),
(True, False), ("none", "sin"), (False, True))]
def test_parallel_equals_recurrent():
"""训练用的分块并行形式和推理用的递推形式,在所有开关组合下都必须一致。
长度 300:跨好几个块,也远超旧版并行形式 120 字的上限。"""
idx = rng.integers(0, 11, size=(2, 300))
worst = 0.0
for opts in OPTIONS:
np.random.seed(1)
model = ZeroX(vocab_size=11, n_embd=16, n_head=2, n_slot=5, n_layer=2, **opts).eval()
worst = max(worst, np.abs(model(idx).data - model.forward_recurrent(idx)).max())
assert worst < 1e-8, worst
def test_matches_golden_reference():
"""和 tests/golden/zeropoint_ref.json 对答案:防止以后改代码时并行或递推形式悄悄变了。"""
with open(os.path.join(HERE, "golden", "zeropoint_ref.json"), encoding="utf-8") as f:
ref = json.load(f)
cfg = {k[7:]: int(v) for k, v in ref.items() if k.startswith("config:")}
model = ZeroX(**cfg).eval()
model.load_state_dict({k[6:]: np.array(v, dtype=np.float64) for k, v in ref.items()
if k.startswith("param:")})
idx, logits = np.array(ref["idx"]), np.array(ref["logits"])
assert np.abs(model(idx).data - logits).max() < 1e-10
assert np.abs(model.forward_recurrent(idx) - logits).max() < 1e-8
def test_causal_across_chunks():
np.random.seed(2)
model = ZeroX(vocab_size=7, n_embd=16, n_head=2, n_slot=4).eval()
idx = rng.integers(0, 7, size=(1, 3 * CHUNK))
y1 = model(idx).data
p = CHUNK + 5 # 改第二块里的一个字
idx[0, p] = (idx[0, p] + 1) % 7
y2 = model(idx).data
assert np.allclose(y1[0, :p], y2[0, :p]) and not np.allclose(y1[0, p:], y2[0, p:])
def test_ema_state_is_bounded():
"""ema=True:写入恒为常数 v 时,槽会收敛到 v 本身,不会随长度无限变大(ema=False 时会涨到约 v/(1-λ))。"""
np.random.seed(6)
for ema, bounded in ((True, True), (False, False)):
mixer = MemoryMixer(8, 1, 4, ema=ema)
S = mixer.init_state(1) * 0
x = np.ones((1, 8))
for _ in range(3000):
_, S = mixer.step(x, S)
v = np.abs(x @ mixer.value.weight.data + mixer.value.bias.data).max()
assert (np.abs(S).max() <= v * 1.0001) == bounded, (ema, np.abs(S).max(), v)
def test_mixer_gradients():
np.random.seed(3)
module_grad_check(MemoryMixer(8, 2, 3), rng.standard_normal((2, 6, 8)))
def test_mixer_gradients_across_chunks():
"""梯度要能穿过块与块之间传递的状态,也要传到可学习的初始状态 s0。"""
np.random.seed(3)
mixer = MemoryMixer(4, 1, 2)
module_grad_check(mixer, rng.standard_normal((1, CHUNK + 3, 4)))
assert np.abs(mixer.s0.grad).max() > 0
def test_initial_state_is_learned():
model = ZeroX(vocab_size=5, n_embd=8, n_head=2, n_slot=3, n_layer=1)
assert np.allclose(model.init_state(3)[0][2], model.blocks[0].mix.s0.data)
assert "blocks.0.mix.s0" in dict(model.named_parameters())
def test_retention_init_is_hierarchical():
lam = MemoryMixer(16, 2, 6, max_half_life=64).retention().data
half_life = np.log(0.5) / np.log(lam[0])
assert np.all(np.diff(lam[0]) > 0) and abs(half_life[0] - 1.5) < 1e-3 and abs(half_life[-1] - 64) < 1e-2
def test_usage_recorded():
model = ZeroX(vocab_size=5, n_embd=8, n_head=2, n_slot=3, n_layer=1)
model(rng.integers(0, 5, size=(2, 10)))
read, write = model.blocks[0].mix.usage
assert read.shape == write.shape == (2, 3) and np.allclose(read.sum(-1), 1)
def test_state_is_constant_size():
model = ZeroX(vocab_size=5, n_embd=16, n_head=2, n_slot=4, n_layer=3)
states = model.init_state(1)
for t in range(300):
_, states = model.step(np.array([t % 5]), states, t)
assert sum(s.size for s in states) == model.state_floats() == 3 * 2 * 4 * 8
assert all(np.isfinite(s).all() for s in states)
def test_float32_long_sequence_is_finite():
"""float32 下 1000 个字的分块并行不溢出,并且和递推形式一致。"""
zc.set_default_dtype(np.float32)
try:
np.random.seed(5)
model = ZeroX(vocab_size=9, n_embd=16, n_head=2, n_slot=6).eval()
idx = rng.integers(0, 9, size=(1, 1000))
par = model(idx).data
assert par.dtype == np.float32 and np.isfinite(par).all()
assert np.abs(par - model.forward_recurrent(idx)).max() < 1e-3
finally:
zc.set_default_dtype(np.float64)
def test_learns_to_copy_from_far_back():
"""记忆任务:开头一个字,中间 40 个干扰字(跨块),最后复述开头那个字。"""
np.random.seed(4)
V, gap = 6, 40
model = ZeroX(vocab_size=V + 1, n_embd=32, n_head=2, n_slot=8, n_layer=1)
opt = zc.optim.AdamW(model.parameters(), lr=0.01)
for _ in range(300):
first = np.random.randint(0, V, 32)
idx = np.concatenate([first[:, None], np.full((32, gap), V)], axis=1)
loss = zc.functional.cross_entropy(model(idx)[np.arange(32), gap], first)
opt.zero_grad()
loss.backward()
opt.step()
assert loss.item() < 0.05, loss.item()
if __name__ == "__main__":
tests = [(k, v) for k, v in list(globals().items()) if k.startswith("test_")]
failed = 0
for name, fn in tests:
try:
fn()
print(f" ok {name}")
except Exception as e: # noqa: BLE001
failed += 1
print(f" FAIL {name}: {type(e).__name__}: {e}")
print(f"\n{len(tests) - failed}/{len(tests)} 通过")
sys.exit(1 if failed else 0)
|