Compactbot commited on
Commit
031ae95
·
verified ·
1 Parent(s): 1f16bc8

Add verified causal-v4 experiment source

Browse files
recurrent_ctx_interaction_v4_causal.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Recurrent Depth × Context Length Interaction at Tiny Scale (v2 - memory fix)
3
+ """
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ import math
8
+ import time
9
+ import json
10
+
11
+ # --- Config ---
12
+ D_MODEL = 256
13
+ N_HEADS = 4
14
+ VOCAB = 256
15
+ BATCH = 32
16
+ STEPS = 2000
17
+ LR = 3e-4
18
+ WARMUP = 200
19
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
20
+ SEED = 42
21
+
22
+ # --- Data: pre-generate tensors (memory efficient) ---
23
+ def load_tinystories_bytes(max_train=50_000_000, max_val=2_000_000):
24
+ with open('/work/data/tinystories.txt', 'rb') as f:
25
+ raw = f.read()
26
+ # Split into train/val: 95/5
27
+ split = int(len(raw) * 0.95)
28
+ train_bytes = raw[:split][:max_train]
29
+ val_bytes = raw[split:][:max_val]
30
+
31
+ # Pre-generate all (x, y) pairs as contiguous tensors
32
+ def make_pairs(data, seq_len=256):
33
+ n = len(data) // seq_len
34
+ arr = torch.tensor(list(data[:n * seq_len + 1]), dtype=torch.long)
35
+ x = arr[:-1].view(n, seq_len)
36
+ y = arr[1:].view(n, seq_len)
37
+ return x, y
38
+
39
+ train_x, train_y = make_pairs(train_bytes)
40
+ val_x, val_y = make_pairs(val_bytes)
41
+ print(f' Train: {train_x.shape[0]} sequences, Val: {val_x.shape[0]} sequences')
42
+ return train_x, train_y, val_x, val_y
43
+
44
+ # --- Model ---
45
+ class TransformerBlock(nn.Module):
46
+ def __init__(self, d_model, n_heads, d_ff):
47
+ super().__init__()
48
+ self.ln1 = nn.LayerNorm(d_model)
49
+ self.attn = nn.MultiheadAttention(d_model, n_heads, batch_first=True)
50
+ self.ln2 = nn.LayerNorm(d_model)
51
+ self.ffn = nn.Sequential(
52
+ nn.Linear(d_model, d_ff),
53
+ nn.GELU(),
54
+ nn.Linear(d_ff, d_model)
55
+ )
56
+
57
+ def forward(self, x):
58
+ h = self.ln1(x)
59
+ causal_mask = torch.ones(h.size(1), h.size(1), device=h.device, dtype=torch.bool).triu(1)
60
+ attn_out, _ = self.attn(h, h, h, attn_mask=causal_mask, need_weights=False)
61
+ x = x + attn_out
62
+ x = x + self.ffn(self.ln2(x))
63
+ return x
64
+
65
+ class TinyGPT(nn.Module):
66
+ def __init__(self, d_model, n_heads, d_ff, n_blocks, recurrent=False, ctx_len=128):
67
+ super().__init__()
68
+ self.d_model = d_model
69
+ self.ctx_len = ctx_len
70
+ self.recurrent = recurrent
71
+ self.n_blocks = n_blocks
72
+
73
+ self.tok_emb = nn.Embedding(VOCAB, d_model)
74
+ self.pos_emb = nn.Embedding(ctx_len, d_model)
75
+
76
+ if recurrent:
77
+ self.block = TransformerBlock(d_model, n_heads, d_ff)
78
+ else:
79
+ self.blocks = nn.ModuleList([
80
+ TransformerBlock(d_model, n_heads, d_ff) for _ in range(n_blocks)
81
+ ])
82
+
83
+ self.ln_f = nn.LayerNorm(d_model)
84
+ self.head = nn.Linear(d_model, VOCAB, bias=False)
85
+ self.head.weight = self.tok_emb.weight
86
+
87
+ def forward(self, idx, targets=None):
88
+ B, T = idx.shape
89
+ T = min(T, self.ctx_len)
90
+ idx = idx[:, :T]
91
+ if targets is not None:
92
+ targets = targets[:, :T]
93
+
94
+ pos = torch.arange(T, device=idx.device).unsqueeze(0)
95
+ x = self.tok_emb(idx) + self.pos_emb(pos)
96
+
97
+ if self.recurrent:
98
+ for _ in range(self.n_blocks):
99
+ x = self.block(x)
100
+ else:
101
+ for block in self.blocks:
102
+ x = block(x)
103
+
104
+ x = self.ln_f(x)
105
+ logits = self.head(x)
106
+
107
+ if targets is not None:
108
+ loss = F.cross_entropy(logits.reshape(-1, VOCAB), targets.reshape(-1))
109
+ return loss
110
+ return logits
111
+
112
+ def count_params(model):
113
+ return sum(p.numel() for p in model.parameters())
114
+
115
+ def train(model, train_x, train_y, val_x, val_y, steps=STEPS, batch=BATCH, lr=LR, warmup=WARMUP):
116
+ model = model.to(DEVICE)
117
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr, betas=(0.9, 0.95), weight_decay=0.01)
118
+
119
+ def get_lr(step):
120
+ if step < warmup:
121
+ return lr * step / max(warmup, 1)
122
+ return lr * (0.5 + 0.5 * math.cos(math.pi * (step - warmup) / max(steps - warmup, 1)))
123
+
124
+ n_train = train_x.shape[0]
125
+ model.train()
126
+ losses = []
127
+ val_losses = []
128
+ best_val = float("inf")
129
+
130
+ for step in range(steps):
131
+ optimizer.param_groups[0]["lr"] = get_lr(step)
132
+
133
+ idx = torch.randint(0, n_train, (batch,))
134
+ x = train_x[idx].to(DEVICE)
135
+ y = train_y[idx].to(DEVICE)
136
+
137
+ loss = model(x, y)
138
+ loss.backward()
139
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
140
+ optimizer.step()
141
+ optimizer.zero_grad()
142
+
143
+ losses.append(loss.item())
144
+
145
+ if (step + 1) % 200 == 0:
146
+ model.eval()
147
+ val_loss = 0.0
148
+ val_count = 0
149
+ with torch.no_grad():
150
+ for i in range(0, val_x.shape[0], batch):
151
+ vx = val_x[i:i+batch].to(DEVICE)
152
+ vy = val_y[i:i+batch].to(DEVICE)
153
+ vl = model(vx, vy)
154
+ val_loss += vl.item()
155
+ val_count += 1
156
+ avg_val = val_loss / max(val_count, 1)
157
+ val_losses.append((step + 1, avg_val))
158
+ if avg_val < best_val:
159
+ best_val = avg_val
160
+ model.train()
161
+
162
+ # Final validation
163
+ model.eval()
164
+ val_loss = 0.0
165
+ val_count = 0
166
+ with torch.no_grad():
167
+ for i in range(0, val_x.shape[0], batch):
168
+ vx = val_x[i:i+batch].to(DEVICE)
169
+ vy = val_y[i:i+batch].to(DEVICE)
170
+ vl = model(vx, vy)
171
+ val_loss += vl.item()
172
+ val_count += 1
173
+ final_val = val_loss / max(val_count, 1)
174
+
175
+ return {
176
+ "final_train_loss": losses[-1],
177
+ "avg_train_loss_last_100": sum(losses[-100:]) / 100,
178
+ "best_val_loss": best_val,
179
+ "final_val_loss": final_val,
180
+ "final_val_ppl": math.exp(final_val),
181
+ "val_curve": val_losses,
182
+ }
183
+
184
+ def main():
185
+ torch.manual_seed(SEED)
186
+ if torch.cuda.is_available():
187
+ torch.cuda.manual_seed_all(SEED)
188
+
189
+ print("=" * 60)
190
+ print("Recurrent Depth × Context Length Interaction")
191
+ print("=" * 60)
192
+ print(f"Device: {DEVICE}")
193
+ if torch.cuda.is_available():
194
+ free, total = torch.cuda.mem_get_info()
195
+ print(f"GPU: {free/1e9:.1f} GB free / {total/1e9:.1f} GB total")
196
+
197
+ print("\nLoading data...")
198
+ train_x, train_y, val_x, val_y = load_tinystories_bytes()
199
+
200
+ configs = [
201
+ ("recurrent_ctx64", True, 4, 2048, 64),
202
+ ("recurrent_ctx256", True, 4, 2048, 256),
203
+ ("stacked_ctx64", False, 4, 128, 64),
204
+ ("stacked_ctx256", False, 4, 128, 256),
205
+ ]
206
+
207
+ results = {}
208
+ for name, recurrent, n_blocks, d_ff, ctx_len in configs:
209
+ print(f"\n{'='*40}")
210
+ print(f"Config: {name}")
211
+ print(f" recurrent={recurrent}, blocks={n_blocks}, d_ff={d_ff}, ctx={ctx_len}")
212
+
213
+ model = TinyGPT(D_MODEL, N_HEADS, d_ff, n_blocks, recurrent=recurrent, ctx_len=ctx_len)
214
+ n_params = count_params(model)
215
+ print(f" Params: {n_params:,}")
216
+
217
+ t0 = time.time()
218
+ res = train(model, train_x, train_y, val_x, val_y)
219
+ elapsed = time.time() - t0
220
+ res["params"] = n_params
221
+ res["train_time_s"] = elapsed
222
+ results[name] = res
223
+
224
+ print(f" Train loss (final): {res['final_train_loss']:.4f}")
225
+ print(f" Val PPL: {res['final_val_ppl']:.2f}")
226
+ print(f" Time: {elapsed:.1f}s")
227
+
228
+ # Free GPU memory between configs
229
+ del model
230
+ torch.cuda.empty_cache()
231
+
232
+ # Summary
233
+ print("\n" + "=" * 60)
234
+ print("SUMMARY")
235
+ print("=" * 60)
236
+ print(f"\n{'Config':<25} {'Params':>10} {'Val PPL':>10} {'Time':>8}")
237
+ print("-" * 60)
238
+ for name, res in results.items():
239
+ print(f"{name:<25} {res['params']:>10,} {res['final_val_ppl']:>10.2f} {res['train_time_s']:>7.1f}s")
240
+
241
+ # Interaction analysis
242
+ print("\n--- Interaction Analysis ---")
243
+ rec_64 = results["recurrent_ctx64"]["final_val_ppl"]
244
+ rec_256 = results["recurrent_ctx256"]["final_val_ppl"]
245
+ stk_64 = results["stacked_ctx64"]["final_val_ppl"]
246
+ stk_256 = results["stacked_ctx256"]["final_val_ppl"]
247
+
248
+ rec_gain = rec_64 - rec_256
249
+ stk_gain = stk_64 - stk_256
250
+
251
+ print(f"Recurrent: ctx64={rec_64:.2f} → ctx256={rec_256:.2f} (Δ={rec_gain:+.2f})")
252
+ print(f"Stacked: ctx64={stk_64:.2f} → ctx256={stk_256:.2f} (Δ={stk_gain:+.2f})")
253
+ print(f"\nRecurrent advantage at ctx64: {stk_64 - rec_64:+.2f}")
254
+ print(f"Recurrent advantage at ctx256: {stk_256 - rec_256:+.2f}")
255
+
256
+ if rec_gain > 0.5 and stk_gain < 0.3:
257
+ print("\n>>> FINDING: Longer context disproportionately benefits recurrent models!")
258
+ elif abs(rec_gain) < 0.3 and abs(stk_gain) < 0.3:
259
+ print("\n>>> FINDING: Context length has minimal effect on either architecture.")
260
+ elif rec_gain < 0 and stk_gain < 0:
261
+ print("\n>>> FINDING: Longer context HURTS both architectures (attention dilution).")
262
+ else:
263
+ print(f"\n>>> Mixed: rec_gain={rec_gain:+.2f}, stk_gain={stk_gain:+.2f}")
264
+
265
+ out = {
266
+ "experiment": "recurrent_ctx_interaction",
267
+ "question": "Does recurrent depth amplify the benefit of longer context at tiny scale?",
268
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
269
+ "config": {
270
+ "d_model": D_MODEL,
271
+ "n_heads": N_HEADS,
272
+ "vocab": VOCAB,
273
+ "batch": BATCH,
274
+ "steps": STEPS,
275
+ "lr": LR,
276
+ "device": DEVICE,
277
+ },
278
+ "results": results,
279
+ "analysis": {
280
+ "recurrent_ctx_gain": rec_gain,
281
+ "stacked_ctx_gain": stk_gain,
282
+ "recurrent_advantage_ctx64": stk_64 - rec_64,
283
+ "recurrent_advantage_ctx256": stk_256 - rec_256,
284
+ }
285
+ }
286
+
287
+ out_path = "/work/work/recurrent_ctx_interaction_results.json"
288
+ with open(out_path, "w") as f:
289
+ json.dump(out, f, indent=2)
290
+ print(f"\nSaved to {out_path}")
291
+
292
+ if __name__ == "__main__":
293
+ main()
294
+
295
+ def preflight_case():
296
+ """CPU-only correctness fixture using the real TinyGPT and shifted TinyStories bytes."""
297
+ from pathlib import Path
298
+ import torch
299
+ source = Path('/work/data/tinystories.txt')
300
+ with source.open('rb') as f:
301
+ raw = f.read(130)
302
+ if len(raw) != 130:
303
+ raise ValueError('Need at least 130 real training bytes for causal preflight')
304
+ tokens = torch.tensor(list(raw), dtype=torch.long)
305
+ x = torch.stack((tokens[0:64], tokens[65:129]))
306
+ shifted_y = torch.stack((tokens[1:65], tokens[66:130]))
307
+ model = TinyGPT(d_model=32, n_heads=4, d_ff=64, n_blocks=2, recurrent=True, ctx_len=64).cpu()
308
+ return model, x, shifted_y, [] # No dedicated memory-write modules; recurrence reuses TransformerBlock weights.