-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtorch_tinyshakespeare.py
More file actions
140 lines (118 loc) · 5.03 KB
/
Copy pathtorch_tinyshakespeare.py
File metadata and controls
140 lines (118 loc) · 5.03 KB
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
"""TinyShakespeare 4-way A/B (PyTorch).
The scale test. If the substrate-attention ranking holds on
1.1MB of real English, it's a paper-grade result, not a tiny-toy
artifact.
Setup:
- TinyShakespeare corpus (~1.1MB, vocab ~65)
- Single-block transformer (the regime where substrate-L3 won
most decisively at 73-char scale)
- Random windows from the full corpus
- Larger d_model (32) for real-vocab work
- More steps (1000) for real training
- 5 seeds for stat
"""
from __future__ import annotations
import argparse
import json
import math
import random
import statistics
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_4way import (
lcg, make_matrix, crt_pe,
AttentionL0, AttentionL1, AttentionL2, AttentionL3,
TransformerModel,
)
def load_corpus():
p = Path(__file__).parent.parent / "transformerless_lm" / "tinyshakespeare.txt"
return p.read_text()
def build_vocab(text: str):
chars = sorted(set(text))
lookup = {c: i for i, c in enumerate(chars)}
return chars, lookup
def train_arm(variant: str, ids: torch.Tensor, vocab_size: int, seq_len: int,
d_model: int, ff_dim: int, lr: float, steps: int, seed: int):
torch.manual_seed(seed)
random.seed(seed)
model = TransformerModel(variant, vocab_size, d_model, ff_dim, seq_len, seed)
optimizer = torch.optim.AdamW(model.parameters(), lr=lr,
betas=(0.9, 0.999), eps=1e-8)
n = len(ids)
tail_losses = []
for step in range(steps):
# Random window from anywhere in the corpus.
start = random.randint(0, n - seq_len - 2)
window = ids[start:start + seq_len]
targets = ids[start + 1:start + 1 + seq_len]
logits = model(window)
loss = F.cross_entropy(logits, targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if step >= steps - 50:
tail_losses.append(loss.item())
n_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
return sum(tail_losses) / len(tail_losses), n_params
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--seeds", type=str, default="42,7,123,2026,1")
parser.add_argument("--steps", type=int, default=1500)
parser.add_argument("--lr", type=float, default=0.005)
parser.add_argument("--seq-len", type=int, default=32)
parser.add_argument("--d-model", type=int, default=32)
parser.add_argument("--ff-dim", type=int, default=64)
parser.add_argument("--out", type=str, default="results_torch_tinyshakespeare.json")
args = parser.parse_args()
text = load_corpus()
chars, lookup = build_vocab(text)
vocab_size = len(chars)
ids = torch.tensor([lookup[c] for c in text], dtype=torch.long)
seeds = [int(s) for s in args.seeds.split(",")]
variants = ["L0", "L1", "L2", "L3"]
print(f"=== TinyShakespeare 4-way A/B (PyTorch) ===")
print(f"corpus: {len(text):,} chars, vocab={vocab_size}")
print(f"setup: seq={args.seq_len} d={args.d_model} ff={args.ff_dim}")
print(f" steps={args.steps} lr={args.lr} seeds={seeds}\n", flush=True)
results = {}
for v in variants:
losses = []
n_params = 0
for seed in seeds:
loss, n_params = train_arm(v, ids, vocab_size, args.seq_len,
args.d_model, args.ff_dim, args.lr,
args.steps, seed)
losses.append(loss)
print(f" [{v}] seed={seed} loss={loss:.4f}", flush=True)
results[v] = {"losses": losses, "n_params": n_params,
"mean": sum(losses) / len(losses),
"std": statistics.stdev(losses) if len(losses) > 1 else 0.0}
print(f"[{v}] params={n_params} mean={results[v]['mean']:.4f} "
f"std={results[v]['std']:.4f}\n", flush=True)
print("\n=== Summary vs L0 ===")
base_mean = results["L0"]["mean"]
base_losses = results["L0"]["losses"]
for v in variants:
wins = sum(1 for x, b in zip(results[v]["losses"], base_losses) if x < b)
rel = (results[v]["mean"] - base_mean) / base_mean * 100
marker = "—" if v == "L0" else f"{rel:+.1f}%"
print(f" {v}: mean={results[v]['mean']:.4f} vs L0: {marker:>8} "
f"wins={wins}/{len(base_losses)}")
print()
l3_mean = results["L3"]["mean"]
delta = (l3_mean - base_mean) / base_mean * 100
if l3_mean < base_mean:
print(f"[TinyShakespeare-SCALE WIN] L3 beats L0 by {delta:.1f}% on 1.1MB corpus.")
print(" Substrate-as-attention-replacement holds at real-corpus scale.")
else:
print(f"[SCALE LIMIT FOUND] L3 LOSES to L0 by {delta:.1f}% at this scale.")
print(" Substrate advantage is scale-bounded; investigate.")
out_path = Path(__file__).parent / args.out
with open(out_path, "w") as f:
json.dump({"results": results, "config": vars(args)}, f,
indent=2, default=float)
print(f"\nWrote {out_path}")
if __name__ == "__main__":
main()