File size: 6,947 Bytes
3f6e26d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
Numerical parity test: CUDA causal TTT vs the torch reference.

Validates `src/models/ttt_cuda` against `causal_block_fast_weight_swish_glu`
from `src/models/ttt.py`, at the REAL vision-expert shapes:
    head_dim=64, d_h=64, num_heads=12, 256 image tokens, chunk_size=256,
    with the global VLM-context pre-update (ctx present), muon_update_steps=0.

Checks:
  1. output max-abs-err vs torch reference (bf16 tolerance)
  2. updated fast-weight (w0/w1/w2) max-abs-err
  3. causality: perturbing query token p must not change outputs < p
     (only meaningful when chunk_size < L; tested with a small chunk_size too)

Run (needs a GPU; timing is NOT measured here so a shared card is fine):
    TORCHDYNAMO_DISABLE=1 PYTHONPATH="$PWD" \
      /mnt/afs-h200/yuyangcheng/venvs/fla_triton32/bin/python -m src.models.test_ttt_cuda
"""
import os
import sys

import torch

# eager reference (compile disabled keeps it simple + matches training env)
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
try:
    torch._dynamo.config.suppress_errors = True
except Exception:
    pass

from src.models.ttt import causal_block_fast_weight_swish_glu
from src.models import ttt_cuda


def _mk(B, L, d, dh, T_ctx, dtype, device, seed=0):
    g = torch.Generator(device=device).manual_seed(seed)
    rk = lambda *s: torch.randn(*s, generator=g, device=device, dtype=dtype)
    w0 = rk(B, d, dh)
    w1 = rk(B, dh, d)
    w2 = rk(B, d, dh)
    q = rk(B, L, d)
    k = rk(B, L, d)
    v = rk(B, L, d)
    # lr is fp32 and positive (softplus output in the real path)
    lr0 = torch.rand(B, L, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    lr1 = torch.rand(B, L, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    lr2 = torch.rand(B, L, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    ck = rk(B, T_ctx, d)
    cv = rk(B, T_ctx, d)
    cl0 = torch.rand(B, T_ctx, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    cl1 = torch.rand(B, T_ctx, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    cl2 = torch.rand(B, T_ctx, 1, generator=g, device=device, dtype=torch.float32) * 0.02 + 0.001
    return dict(w0=w0, w1=w1, w2=w2, q=q, k=k, v=v, lr0=lr0, lr1=lr1, lr2=lr2,
                ck=ck, cv=cv, cl0=cl0, cl1=cl1, cl2=cl2)


def _run_ref(t, chunk_size):
    return causal_block_fast_weight_swish_glu(
        t["w0"].clone(), t["w1"].clone(), t["w2"].clone(),
        t["q"], t["k"], t["v"], t["lr0"], t["lr1"], t["lr2"],
        chunk_size=chunk_size, muon_update_steps=0,
        vlm_k=t["ck"], vlm_v=t["cv"], vlm_lr0=t["cl0"], vlm_lr1=t["cl1"], vlm_lr2=t["cl2"],
    )


def _run_cuda(t, chunk_size):
    return ttt_cuda.causal_ttt_forward(
        t["w0"].clone(), t["w1"].clone(), t["w2"].clone(),
        t["q"], t["k"], t["v"], t["lr0"], t["lr1"], t["lr2"],
        chunk_size=chunk_size,
        vlm_k=t["ck"], vlm_v=t["cv"], vlm_lr0=t["cl0"], vlm_lr1=t["cl1"], vlm_lr2=t["cl2"],
    )


def _err(a, b):
    return (a.float() - b.float()).abs().max().item()


def _rel_err(a, b):
    # max abs err normalized by the reference's max abs magnitude
    denom = a.float().abs().max().item() + 1e-9
    return (a.float() - b.float()).abs().max().item() / denom


def main():
    if not torch.cuda.is_available():
        print("SKIP: no CUDA device")
        return 0
    dev = "cuda"
    # bf16: the real training/inference dtype; fp32 also tested for tighter bound.
    results = []
    # ---- fp32: strict absolute/relative parity (the math must be correct) ----
    for chunk_size in (256, 64):  # 256 = real config (1 chunk); 64 = multi-chunk causal
        t = _mk(B=24, L=256, d=64, dh=64, T_ctx=32, dtype=torch.float32, device=dev, seed=1)
        o_ref, w0r, w1r, w2r = _run_ref(t, chunk_size)
        try:
            o_c, w0c, w1c, w2c = _run_cuda(t, chunk_size)
        except RuntimeError as e:
            print(f"FAIL build/run (fp32, cs={chunk_size}): {e}")
            return 1
        eo = _rel_err(o_ref, o_c)
        ew = max(_err(w0r, w0c), _err(w1r, w1c), _err(w2r, w2c))
        tol = 2e-3
        ok = eo < tol and ew < tol
        print(f"[{'OK ' if ok else 'BAD'}] fp32          cs={chunk_size:3d} | "
              f"out_relerr={eo:.2e} w_err={ew:.2e} (tol={tol:.0e})")
        results.append(ok)

    # ---- bf16: the reference is ITSELF only bf16-accurate, so an absolute tol
    # is meaningless. Fair criterion: CUDA-bf16 must be no less accurate than
    # torch-bf16, both measured against the fp32 ground truth. Allow 1.5x slack.
    for chunk_size in (256, 64):
        # same bf16 inputs for all three; fp32 truth = those inputs upcast.
        t16 = _mk(B=24, L=256, d=64, dh=64, T_ctx=32, dtype=torch.bfloat16, device=dev, seed=1)
        t32 = {kk: (vv.float() if torch.is_tensor(vv) and vv.dtype == torch.bfloat16 else vv)
               for kk, vv in t16.items()}
        o_truth, w0t, w1t, w2t = _run_ref(t32, chunk_size)     # fp32 ground truth
        o_tref, w0tr, w1tr, w2tr = _run_ref(t16, chunk_size)   # torch bf16
        o_cu, w0cu, w1cu, w2cu = _run_cuda(t16, chunk_size)    # cuda bf16
        # output relative error vs fp32 truth
        ref_oerr = _rel_err(o_truth, o_tref)
        cu_oerr = _rel_err(o_truth, o_cu)
        # weight error vs fp32 truth
        ref_werr = max(_err(w0t, w0tr), _err(w1t, w1tr), _err(w2t, w2tr))
        cu_werr = max(_err(w0t, w0cu), _err(w1t, w1cu), _err(w2t, w2cu))
        ok = cu_oerr <= ref_oerr * 1.5 + 1e-6 and cu_werr <= ref_werr * 1.5 + 1e-6
        print(f"[{'OK ' if ok else 'BAD'}] bf16          cs={chunk_size:3d} | "
              f"out: cuda={cu_oerr:.2e} vs torch={ref_oerr:.2e} | "
              f"w: cuda={cu_werr:.2e} vs torch={ref_werr:.2e} (cuda<=1.5x torch)")
        results.append(ok)

    # causality (fp32, small chunk so it's a real test): perturb q token p,
    # outputs at positions < p (in earlier chunks) must be unchanged.
    t = _mk(B=4, L=128, d=64, dh=64, T_ctx=16, dtype=torch.float32, device=dev, seed=2)
    cs = 32
    o0, *_ = _run_cuda(t, cs)
    p = 100
    t2 = {kk: (vv.clone() if torch.is_tensor(vv) else vv) for kk, vv in t.items()}
    t2["q"][:, p, :] += 1.0
    t2["k"][:, p, :] += 1.0
    t2["v"][:, p, :] += 1.0
    o1, *_ = _run_cuda(t2, cs)
    # positions strictly before p's chunk start must be bitwise-ish unchanged
    chunk_start = (p // cs) * cs
    delta_before = (o0[:, :chunk_start] - o1[:, :chunk_start]).abs().max().item()
    delta_after = (o0[:, p:] - o1[:, p:]).abs().max().item()
    causal_ok = delta_before < 1e-4 and delta_after > 1e-3
    print(f"[{'OK ' if causal_ok else 'BAD'}] causality | "
          f"before_p_delta={delta_before:.2e} (want~0) after_p_delta={delta_after:.2e} (want>0)")
    results.append(causal_ok)

    allok = all(results)
    print("\n=== PARITY", "PASS ===" if allok else "FAIL ===")
    return 0 if allok else 1


if __name__ == "__main__":
    sys.exit(main())