File size: 15,290 Bytes
8d9996a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
"""Equivalence proofs: cumsum attention vs einsum reference.

Open-heart rule: any production-path change must be PROVEN mathematically
equivalent to the reference before it may wrap a live checkpoint.

Covered here (opt 1, 2026-08-22):
  - forward outputs match (with and without state carry)
  - gradients wrt q, k, v match
  - carried final states (S_final, z_final) match
  - both match the scalar looped reference (_linear_attention_causal_one_head)
"""
import sys
from pathlib import Path

import pytest
import torch

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from fractus.nn.attention import FractalLinearAttention
from fractus.nn.stats import elu_plus_one


ATOL = 1e-4   # float32 accumulation-order tolerance
RTOL = 1e-4


def _make(attn, G, L=32, D=64, seed=0):
    torch.manual_seed(seed)
    q = torch.rand(G, L, D) + 0.5          # positive like elu+1 features
    k = torch.rand(G, L, D) + 0.5
    v = torch.randn(G, L, D) * 0.5
    S0 = torch.randn(G, D, D) * 0.05
    z0 = torch.rand(G, D) + 0.5            # z from positive features
    return q, k, v, S0, z0


def test_forward_matches_einsum_no_carry():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, _, _ = _make(attn, G=6)
    y_ref = attn._linear_attention_causal_einsum(q, k, v)
    y_new = attn._linear_attention_causal_cumsum(q, k, v)
    assert torch.allclose(y_ref, y_new, atol=ATOL, rtol=RTOL), \
        f"max diff {(y_ref - y_new).abs().max().item():.3e}"


def test_forward_matches_einsum_with_carry():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=6)
    y_ref, (Sr, zr) = attn._linear_attention_causal_einsum(q, k, v, carry=(S0, z0))
    y_new, (Sn, zn) = attn._linear_attention_causal_cumsum(q, k, v, carry=(S0, z0))
    assert torch.allclose(y_ref, y_new, atol=ATOL, rtol=RTOL)
    assert torch.allclose(Sr, Sn, atol=ATOL * 10, rtol=RTOL)   # S sums are larger
    assert torch.allclose(zr, zn, atol=ATOL, rtol=RTOL)


def test_gradients_match_with_carry():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=4)

    grads = {}
    for name, fn in (("einsum", attn._linear_attention_causal_einsum),
                     ("cumsum", attn._linear_attention_causal_cumsum)):
        qg = q.clone().requires_grad_(True)
        kg = k.clone().requires_grad_(True)
        vg = v.clone().requires_grad_(True)
        y, _ = fn(qg, kg, vg, carry=(S0, z0))
        # weighted loss so every element matters (avoid symmetric cancellation)
        w = torch.linspace(0.1, 1.0, y.numel()).view(y.shape)
        (y * w).sum().backward()
        grads[name] = (qg.grad.clone(), kg.grad.clone(), vg.grad.clone())

    for i, dim in enumerate(("q", "k", "v")):
        gr, gn = grads["einsum"][i], grads["cumsum"][i]
        assert torch.allclose(gr, gn, atol=1e-3, rtol=1e-3), \
            f"grad[{dim}] max diff {(gr - gn).abs().max().item():.3e}"


def test_both_match_looped_reference():
    """The original scalar loop is the ultimate ground truth."""
    torch.manual_seed(3)
    attn = FractalLinearAttention(d_model=64, n_heads=1, d_head=64, n_levels=1)
    G, L, D = 2, 16, 64
    q = torch.rand(G, L, D) + 0.5
    k = torch.rand(G, L, D) + 0.5
    v = torch.randn(G, L, D) * 0.5

    y_loop = attn._linear_attention_causal_one_head(q, k, v)
    y_einsum = attn._linear_attention_causal_einsum(q, k, v)
    y_cumsum = attn._linear_attention_causal_cumsum(q, k, v)

    assert torch.allclose(y_loop, y_einsum, atol=1e-4, rtol=1e-4)
    assert torch.allclose(y_loop, y_cumsum, atol=1e-4, rtol=1e-4)


def test_chunked_matches_einsum_no_carry():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    for L in (32, 128):   # includes multi-block case
        q, k, v, _, _ = _make(attn, G=6, L=L, seed=L)
        y_ref = attn._linear_attention_causal_einsum(q, k, v)
        y_ch = attn._linear_attention_causal_chunked(q, k, v, block=16)
        assert torch.allclose(y_ref, y_ch, atol=ATOL * 2, rtol=RTOL), \
            f"L={L}: max diff {(y_ref - y_ch).abs().max().item():.3e}"


def test_chunked_matches_einsum_with_carry():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=6, L=64)
    y_ref, (Sr, zr) = attn._linear_attention_causal_einsum(q, k, v, carry=(S0, z0))
    y_ch, (Sc, zc) = attn._linear_attention_causal_chunked(q, k, v, carry=(S0, z0), block=16)
    assert torch.allclose(y_ref, y_ch, atol=ATOL * 4, rtol=RTOL), \
        f"max diff {(y_ref - y_ch).abs().max().item():.3e}"
    assert torch.allclose(Sr, Sc, atol=1e-2, rtol=1e-3)
    assert torch.allclose(zr, zc, atol=ATOL, rtol=RTOL)


def test_chunked_gradients_match():
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=4, L=32)

    grads = {}
    for name, fn in (("einsum", attn._linear_attention_causal_einsum),
                     ("chunked", lambda *a, **kw: attn._linear_attention_causal_chunked(*a, block=16, **kw))):
        qg = q.clone().requires_grad_(True)
        kg = k.clone().requires_grad_(True)
        vg = v.clone().requires_grad_(True)
        y, _ = fn(qg, kg, vg, carry=(S0, z0))
        w = torch.linspace(0.1, 1.0, y.numel()).view(y.shape)
        (y * w).sum().backward()
        grads[name] = (qg.grad.clone(), kg.grad.clone(), vg.grad.clone())

    for i, dim in enumerate(("q", "k", "v")):
        gr, gn = grads["einsum"][i], grads["chunked"][i]
        assert torch.allclose(gr, gn, atol=5e-3, rtol=5e-3), \
            f"grad[{dim}] max diff {(gr - gn).abs().max().item():.3e}"


def test_chunked_ragged_length_falls_back():
    """Non-multiple lengths must still be exact via the cumsum fallback."""
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, _, _ = _make(attn, G=6, L=50)
    y_ref = attn._linear_attention_causal_einsum(q, k, v)
    y_ch = attn._linear_attention_causal_chunked(q, k, v, block=16)
    assert torch.allclose(y_ref, y_ch, atol=ATOL * 2, rtol=RTOL)


def test_impl_switch_affects_dispatch():
    from fractus.nn import attention as A
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=6, L=64)

    prev = A._ACTIVE_IMPL
    try:
        A.set_attention_impl("chunked")
        y_ch, _ = attn._linear_attention_causal_vectorized(q, k, v, carry=(S0, z0))
        A.set_attention_impl("cumsum")
        y_cs, _ = attn._linear_attention_causal_vectorized(q, k, v, carry=(S0, z0))
        y_ref, _ = attn._linear_attention_causal_einsum(q, k, v, carry=(S0, z0))
        assert torch.allclose(y_ref, y_ch, atol=ATOL * 4, rtol=RTOL)
        assert torch.allclose(y_ref, y_cs, atol=ATOL, rtol=RTOL)
    finally:
        A.set_attention_impl(prev)


# ---------------------------------------------------------------------------
# Chunked cross-entropy equivalence (opt 3)
# ---------------------------------------------------------------------------

def _ce_case(N=1024, d=128, V=50257, seed=7):
    torch.manual_seed(seed)
    h = torch.randn(N, d) * 0.5
    w = torch.randn(V, d) * 0.05
    t = torch.randint(0, V, (N,))
    return h, w, t


def test_ce_loss_matches_dense():
    from fractus.nn.ce import chunked_cross_entropy
    import torch.nn.functional as F

    h, w, t = _ce_case()
    logits = F.linear(h, w)
    ref = F.cross_entropy(logits.float(), t)
    got = chunked_cross_entropy(h, w, t, ce_chunk=333)  # ragged chunks
    assert torch.allclose(ref, got, atol=1e-4, rtol=1e-5), \
        f"ref={ref.item():.6f} got={got.item():.6f}"


def test_ce_gradients_match_dense():
    from fractus.nn.ce import chunked_cross_entropy
    import torch.nn.functional as F

    h, w, t = _ce_case(N=512, d=64, V=2000)

    hg = h.clone().requires_grad_(True)
    wg = w.clone().requires_grad_(True)
    loss_dense = F.cross_entropy(F.linear(hg, wg).float(), t)
    loss_dense.backward()

    hc = h.clone().requires_grad_(True)
    wc = w.clone().requires_grad_(True)
    loss_chunk = chunked_cross_entropy(hc, wc, t, ce_chunk=128)
    loss_chunk.backward()

    assert torch.allclose(loss_dense, loss_chunk, atol=1e-5, rtol=1e-5)
    assert torch.allclose(hg.grad, hc.grad, atol=1e-4, rtol=1e-3), \
        f"h grad max diff {(hg.grad - hc.grad).abs().max().item():.3e}"
    # weight grad: rows never touched by targets are zero in BOTH paths;
    # compare only touched rows to avoid dense-vs-chunk zero-row noise
    touched = torch.zeros(V := 2000, dtype=torch.bool).scatter_(0, t, True)
    assert torch.allclose(wg.grad[touched], wc.grad[touched], atol=1e-5, rtol=1e-3)


def test_engine_tick_chunk_train_ce_matches_train():
    """End-to-end: tick_chunk_train_ce loss == CE(tick_chunk_train logits).

    Weights must be CLONED via load_state_dict — two consecutive constructions
    under one seed draw DIFFERENT parameters (the original bug behind
    dense=63.7 vs chunked=61.2). With identical weights and the same
    production attention kernel the forward is deterministic (zero-init
    thought state), so any residual gap is CE reduction order only (~1e-6).
    """
    import torch.nn.functional as F
    from fractus.continuous_engine import ContinuousThoughtEngine

    cfg = dict(vocab_size=1000, d_model=64, n_heads=1, d_head=64,
               n_levels=2, n_oscillators=8, coupling_rank=4,
               n_experts=4, top_k=2, expert_d_ff=64, siren_rank=16,
               n_layers=2)
    torch.manual_seed(5)
    eng_d = ContinuousThoughtEngine(**cfg)
    eng_c = ContinuousThoughtEngine(**cfg)
    eng_c.load_state_dict(eng_d.state_dict())
    torch.manual_seed(6)
    toks = torch.randint(0, 1000, (2, 33))
    chunk, target = toks[:, :-1], toks[:, 1:]

    losses = []
    for eng, mode in ((eng_d, "dense"), (eng_c, "chunked")):
        eng.reset_thought(batch_size=2)
        if mode == "dense":
            logits, lb = eng.tick_chunk_train(chunk)
            ce = F.cross_entropy(logits.reshape(-1, logits.size(-1)),
                                 target.reshape(-1))
        else:
            ce, lb = eng.tick_chunk_train_ce(chunk, target, ce_chunk=17)
        losses.append((ce.detach(), lb.detach()))

    assert torch.allclose(losses[0][0], losses[1][0], atol=1e-4, rtol=1e-4), \
        f"dense={losses[0][0].item():.5f} chunked={losses[1][0].item():.5f}"
    assert torch.allclose(losses[0][1], losses[1][1])


def test_dispatcher_uses_production_path():
    """The engine calls _linear_attention_causal_vectorized — it must route to
    the cumsum implementation and stay equivalent to the reference."""
    attn = FractalLinearAttention(d_model=128, n_heads=2, d_head=64, n_levels=2)
    q, k, v, S0, z0 = _make(attn, G=6)
    y_ref, st_ref = attn._linear_attention_causal_einsum(q, k, v, carry=(S0, z0))
    y_disp, st_disp = attn._linear_attention_causal_vectorized(q, k, v, carry=(S0, z0))
    assert torch.allclose(y_ref, y_disp, atol=ATOL, rtol=RTOL)
    assert torch.allclose(st_ref[0], st_disp[0], atol=ATOL * 10, rtol=RTOL)


def test_engine_end_to_end_chunk_equivalence():
    """Two-level proof around CTEBlock.tick_chunk_core:

    Level 1 (strict): the attention stage — the code we actually replaced —
      must match the einsum reference tightly on the exact flattened shapes
      the engine feeds it, carry included.

    Level 2 (documented): downstream of attention, the von Mises gate + topk
      are DISCRETE. A float32-rounding difference near a gate tie can flip
      which expert a token routes to, producing a localized output jump far
      above rounding scale. This is not an implementation error — it is the
      measure-zero boundary behavior of argmax under two equally-valid
      summation orders. We therefore assert the mismatch is RARE and bounded,
      not zero, and print it for the log.
    """
    from fractus.continuous_engine import CTEBlock

    cfg = dict(d_model=128, n_heads=2, d_head=64, n_levels=2,
               n_oscillators=8, coupling_rank=4, n_experts=8,
               top_k=2, expert_d_ff=128, siren_rank=32)
    torch.manual_seed(11)
    blk_r = CTEBlock(**cfg)
    blk_n = CTEBlock(**cfg)
    # same trap as the engine CE test: one shared seed does NOT give two
    # constructions identical weights — clone explicitly.
    blk_n.load_state_dict(blk_r.state_dict())

    # ---- Level 1: attention stage, engine shapes -------------------------
    attn = blk_r.attn
    nH, dH, nL = attn.n_heads, attn.d_head, attn.n_levels
    B, C = 3, 64
    torch.manual_seed(12)
    h_normed = torch.randn(B, C, cfg["d_model"])
    q_all = h_normed.view(B, C, nH, dH)
    k_all = h_normed.view(B, C, nH, dH)
    v_all = h_normed.view(B, C, nH, dH)
    offsets = attn.level_offsets
    q_feat = elu_plus_one(q_all.unsqueeze(1) + offsets.view(nL, 1, 1, 1), alpha=1.0)
    k_feat = elu_plus_one(k_all.unsqueeze(1) + offsets.view(nL, 1, 1, 1), alpha=1.0)
    v_lev = v_all.unsqueeze(1).expand(B, nL, C, nH, dH)
    qf = q_feat.permute(0, 1, 3, 2, 4).reshape(B * nL * nH, C, dH)
    kf = k_feat.permute(0, 1, 3, 2, 4).reshape(B * nL * nH, C, dH)
    vf = v_lev.permute(0, 1, 3, 2, 4).reshape(B * nL * nH, C, dH)
    S0 = torch.rand(B * nL * nH, dH, dH) * 0.01
    z0 = torch.rand(B * nL * nH, dH) + 0.5

    y_ref, (Sr, zr) = attn._linear_attention_causal_einsum(qf, kf, vf, carry=(S0, z0))
    y_new, (Sn, zn) = attn._linear_attention_causal_vectorized(qf, kf, vf, carry=(S0, z0))
    assert torch.allclose(y_ref, y_new, atol=ATOL * 4, rtol=RTOL), \
        f"attention y max diff {(y_ref - y_new).abs().max().item():.3e}"
    assert torch.allclose(Sr, Sn, atol=1e-2, rtol=1e-3)
    assert torch.allclose(zr, zn, atol=ATOL * 10, rtol=RTOL)

    # ---- Level 2: full block with flip tolerance -------------------------
    torch.manual_seed(13)
    h_in = torch.randn(B, C, cfg["d_model"])
    results = []
    for blk, mode in ((blk_r, "einsum"), (blk_n, "production")):
        orig = blk.attn._linear_attention_causal_vectorized
        if mode == "einsum":
            blk.attn._linear_attention_causal_vectorized = \
                lambda q, k, v, carry=None, _ref=blk.attn._linear_attention_causal_einsum: \
                _ref(q, k, v, carry=carry)
        try:
            out, lb = blk.tick_chunk_core(h_in.clone())
            results.append((out.detach(), lb.detach()))
        finally:
            blk.attn._linear_attention_causal_vectorized = orig

    out_r, lb_r = results[0]
    out_n, lb_n = results[1]
    diff = (out_r - out_n).abs()
    frac_mismatch = (diff > 1e-3).float().mean().item()
    # routing flips, when they occur, touch a tiny fraction of positions;
    # everything else matches at rounding scale.
    assert diff.median() < 1e-4, f"median diff {diff.median().item():.3e} too large"
    assert frac_mismatch < 0.05, \
        f"{frac_mismatch:.1%} of elements differ >1e-3 — too many for tie flips"
    assert abs(float(lb_r) - float(lb_n)) < 0.5
    print(f"\n[end-to-end] median diff {diff.median().item():.2e}, "
          f"max {diff.max().item():.2e}, mismatch>1e-3: {frac_mismatch:.2%}")


if __name__ == "__main__":
    sys.exit(pytest.main([__file__, "-v"]))