summaryrefslogtreecommitdiff
path: root/tests/test_models.py
blob: b6fec4d51956f8c6f779227cdf8cf897a22ac347 (plain)
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
import torch
import torch.nn.functional as F

from src.config import Config
from src.data import build_examples, make_batch
from src.model_api import build_model


def _batch(cfg, inputs=(2, 7, 42, 99)):
    ex = build_examples(list(inputs), cfg)
    return make_batch(ex, cfg)


def test_rnn_shapes_and_halting_range():
    cfg = Config(model="rnn", max_steps=20, min_steps=2)
    m = build_model(cfg)
    b = _batch(cfg)
    out = m(b["x"], b["y_in"])
    assert out["logits"].shape == (4, b["y"].shape[1], cfg.vocab)
    s = out["halt_steps"]
    assert s.shape == (4,)
    assert bool((s >= cfg.min_steps).all())
    assert bool((s <= cfg.max_steps).all())


def test_rnn_fixed_k_ablation():
    cfg = Config(model="rnn", halting=False, max_steps=20)
    m = build_model(cfg)
    b = _batch(cfg)
    out = m(b["x"], b["y_in"])
    assert torch.allclose(out["halt_steps"], torch.full((4,), 20.0))


def test_transformer_shapes():
    cfg = Config(model="transformer")
    m = build_model(cfg)
    b = _batch(cfg)
    out = m(b["x"], b["y_in"])
    assert out["logits"].shape == (4, b["y"].shape[1], cfg.vocab)
    assert out["halt_steps"] is None


def test_transformer_integers_mode():
    cfg = Config(model="transformer", vocab_mode="integers")
    m = build_model(cfg)
    b = _batch(cfg)
    out = m(b["x"], b["y_in"])
    assert out["logits"].shape == (4, b["y"].shape[1], cfg.vocab)


def test_rnn_fully_tied_no_gru_decoder():
    """Regression: design review fix — everything recurrent must be the ONE tied cell."""
    import torch.nn as nn
    cfg = Config(model="rnn")
    m = build_model(cfg)
    assert not any(isinstance(mod, nn.GRUCell) for mod in m.modules())
    assert not any(isinstance(mod, (nn.GRU, nn.LSTM, nn.RNN)) for mod in m.modules())


def test_param_counts_logged_not_gated():
    r = build_model(Config(model="rnn")).param_count()
    t = build_model(Config(model="transformer")).param_count()
    assert r > 1000 and t > 1000


def test_forward_backward_no_nan_both_models():
    for model_name in ("rnn", "transformer"):
        cfg = Config(model=model_name)
        m = build_model(cfg)
        b = _batch(cfg)
        out = m(b["x"], b["y_in"])
        y_safe = b["y"].clamp(max=cfg.vocab - 1)
        loss = F.cross_entropy(out["logits"].reshape(-1, cfg.vocab), y_safe.reshape(-1), reduction="none")
        mask = b["y_mask"].float().reshape(-1)
        loss = (loss * mask).sum() / mask.sum()
        loss.backward()
        assert torch.isfinite(loss).item(), f"{model_name} loss NaN"


def test_greedy_decode_shape():
    from src.model_api import greedy_decode
    cfg = Config(model="rnn")
    m = build_model(cfg)
    x = torch.tensor([[4, 2], [9, 9]], dtype=torch.long)
    gen = greedy_decode(m, x, cfg)
    assert gen.shape == (2, cfg.max_out_len)