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)