summaryrefslogtreecommitdiff
path: root/src/gpt2_jlens.py
blob: b55a65f909f870c6984e36a28d7e43edf66cca82 (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
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
"""
GPT-2 Small J-lens — efficient sampling approach.

Instead of computing J-lens for all 50257 tokens, sample:
- 100 most common + 100 rarest = 200 tokens for frequency test
- 1000 random tokens for effective rank estimation

Each backward pass takes ~50ms on K2200.
200 tokens × 3 batches × 3 layers = 1800 backward passes ≈ 90s
"""
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import torch, numpy as np, pickle, time, subprocess

try:
    import transformers
except ImportError:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "transformers==4.44.0", "accelerate", "requests"])
    import transformers
import requests

device = 'cuda'

def main():
    print("=" * 60)
    print("GPT-2 SMALL J-LENS — Frequency + Dimensionality Tests")
    print("=" * 60)
    
    # Load model
    model = transformers.GPT2LMHeadModel.from_pretrained(
        "openai-community/gpt2", torch_dtype=torch.float32).to(device)
    model.eval()
    tokenizer = transformers.GPT2Tokenizer.from_pretrained("openai-community/gpt2")
    
    d_model = model.config.n_embd
    n_layers = model.config.n_layer
    vocab_size = model.config.vocab_size
    print(f"GPT-2 Small: {n_layers} layers, d={d_model}, V={vocab_size}, V/d={vocab_size/d_model:.1f}x")
    
    # Load corpus
    text = requests.get(
        "https://raw.githubusercontent.com/karpathy/nanoGPT/master/data/shakespeare_char/input.txt"
    ).text[:200000]
    tokens = tokenizer(text, return_tensors='np', truncation=True, max_length=2000)['input_ids'][0]
    print(f"Corpus: {len(tokens)} tokens")
    
    # Estimate token frequencies in our corpus
    from collections import Counter
    freq = Counter(tokens.tolist())
    total = len(tokens)
    sorted_by_freq = sorted(freq.items(), key=lambda x: x[1], reverse=True)
    
    # Pick: 50 most common, 50 rarest
    sample_tokens = [t for t, _ in sorted_by_freq[:50]] + [t for t, _ in sorted_by_freq[-50:]]
    sample_tokens = list(set(sample_tokens))  # dedupe
    print(f"Sampling {len(sample_tokens)} tokens for frequency test")
    
    # J-lens computation
    n_batches = 3
    seq_len = 32
    layers_to_test = [3, 6, 9]  # early, middle, late
    
    # Accumulators: {layer: {token_id: [norm_sum, count]}}
    accum = {layer: {tid: [0.0, 0] for tid in sample_tokens} for layer in layers_to_test}
    
    for batch_i in range(n_batches):
        ix = torch.randint(0, len(tokens) - seq_len - 1, (1,))
        x = torch.from_numpy(tokens[ix[0]:ix[0]+seq_len].astype(np.int64)).unsqueeze(0).to(device)
        
        for layer_idx in layers_to_test:
            # Capture residual at this layer
            resid_captured = {}
            def hook(module, inp, out):
                h = out[0] if isinstance(out, tuple) else out
                resid_captured['val'] = h
            
            target = model.transformer.h[layer_idx]
            handle = target.register_forward_hook(hook)
            
            # Forward (no no_grad — we need gradients)
            result = model(x)
            handle.remove()
            
            logits = result.logits  # (1, seq_len, V)
            resid = resid_captured['val']  # (1, seq_len, d)
            
            for tid in sample_tokens:
                token_logprob = torch.nn.functional.log_softmax(logits, dim=-1)[:, :, tid].sum()
                try:
                    grad = torch.autograd.grad(token_logprob, resid, retain_graph=True)[0]
                    norm = grad.norm().item()
                    accum[layer_idx][tid][0] += norm
                    accum[layer_idx][tid][1] += 1
                except:
                    pass
            
            del logits, result, resid
            torch.cuda.empty_cache()
        
        print(f"  Batch {batch_i+1}/{n_batches} done")
    
    # Results
    print(f"\n{'='*60}")
    print("FREQUENCY vs J-LENS NORM (GPT-2 Small)")
    print(f"{'='*60}")
    
    for layer_idx in layers_to_test:
        print(f"\n--- Layer {layer_idx} ---")
        
        norms = {}
        for tid in sample_tokens:
            s, c = accum[layer_idx][tid]
            if c > 0:
                norms[tid] = s / c
        
        # Token frequencies
        freqs = {tid: freq.get(tid, 0)/total*100 for tid in norms}
        
        # Correlation
        n_arr = np.array(list(norms.values()))
        f_arr = np.array([freqs[t] for t in norms])
        corr = np.corrcoef(n_arr, f_arr)[0, 1]
        
        # Top/bottom by norm
        sorted_tokens = sorted(norms.items(), key=lambda x: x[1], reverse=True)
        print(f"  Top 5 by J-lens norm:")
        for tid, n in sorted_tokens[:5]:
            tok_str = tokenizer.decode([tid]).replace('\n', '\\n')
            print(f"    '{tok_str}' (freq={freqs[tid]:.3f}%): norm={n:.4f}")
        print(f"  Bottom 5 by J-lens norm:")
        for tid, n in sorted_tokens[-5:]:
            tok_str = tokenizer.decode([tid]).replace('\n', '\\n')
            print(f"    '{tok_str}' (freq={freqs[tid]:.3f}%): norm={n:.4f}")
        print(f"  Pearson r(norm, freq): {corr:.3f}")
    
    print(f"\n{'='*60}")
    print("RESULTS SUMMARY")
    print(f"{'='*60}")
    print(f"  nanoGPT (V/d=0.2x): full-rank J-space, r=-0.65 freq correlation")
    print(f"  GPT-2   (V/d=65x):  {'r=' + str(corr)}")
    print(f"  HYPOTHESIS: frequency anti-correlation persists at scale")


if __name__ == '__main__':
    main()