kolbrian commited on
Commit
9affe33
·
verified ·
1 Parent(s): 9da43d4

code: model/train variants + minimal champion

Browse files
code/model_canon_HEAD.py ADDED
@@ -0,0 +1,701 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Full definition of a GPT Language Model, all of it in this single file.
3
+ Includes:
4
+ - cheap Q gate
5
+ - Q-MLP gate (128 hidden)
6
+ - Q-MLP gate (192 hidden) -- param-matched control for Q+A MLP
7
+ - full-width Q gate
8
+ - Q head-shared elementwise gate
9
+ - full-width X gate
10
+ - bottleneck X gate
11
+ - paper-faithful X variants:
12
+ - head-specific elementwise G1
13
+ - head-specific headwise G1
14
+ - head-shared elementwise G1
15
+ - Q+A dual-signal variants:
16
+ - cheap Q+A gate (shared Linear 128->64 applied per head/token)
17
+ - Q+A MLP gate (shared MLP 128->128->64 applied per head/token)
18
+ - legacy Q+A head-shared elementwise gate (shared Linear 128->64; kept for compatibility)
19
+ - bilinear diagonal Q+A gate (learned elementwise q*y interaction)
20
+ - normed Q+A gate (RMSNorm on q and y before shared 128->64 mix)
21
+ - lowrank Q+A gate (shared 128->16->64 bottleneck mix)
22
+ - soft-QA gate (independent learned soft blends for q and y)
23
+ - per-dimension soft-QA gate (independent 64-dim learned blend scales for q and y)
24
+ """
25
+
26
+ import math
27
+ import inspect
28
+ from dataclasses import dataclass
29
+ import torch
30
+ import torch.nn as nn
31
+ from torch.nn import functional as F
32
+
33
+ class RMSNorm(nn.Module):
34
+ def __init__(self, dim: int, eps: float = 1e-6):
35
+ super().__init__()
36
+ self.eps = eps
37
+ self.weight = nn.Parameter(torch.ones(dim))
38
+ def forward(self, x):
39
+ rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
40
+ return x / rms * self.weight
41
+
42
+ class LayerNorm(nn.Module):
43
+ def __init__(self, ndim, bias):
44
+ super().__init__()
45
+ self.weight = nn.Parameter(torch.ones(ndim))
46
+ self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None
47
+ def forward(self, input):
48
+ return F.layer_norm(input, self.weight.shape, self.weight, self.bias, 1e-5)
49
+
50
+ class CausalSelfAttention(nn.Module):
51
+ def __init__(self, config):
52
+ super().__init__()
53
+ assert config.n_embd % config.n_head == 0
54
+ self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
55
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
56
+
57
+ self.qv_variant = config.qv_variant
58
+ hs = config.n_embd // config.n_head
59
+
60
+ # --------------------------------------------------
61
+ # Q-conditioned gates
62
+ # --------------------------------------------------
63
+ if self.qv_variant in ['dynamic', 'dynamic_swiglu']:
64
+ self.qv_gate_proj = nn.Linear(hs, hs, bias=False)
65
+
66
+ if self.qv_variant == 'dynamic_qconditioned_mlp128':
67
+ self.q_gate_fc1 = nn.Linear(hs, 128, bias=False)
68
+ self.q_gate_fc2 = nn.Linear(128, hs, bias=False)
69
+
70
+ if self.qv_variant == 'dynamic_qconditioned_mlp192':
71
+ self.q_gate_fc1_192 = nn.Linear(hs, 192, bias=False)
72
+ self.q_gate_fc2_192 = nn.Linear(192, hs, bias=False)
73
+
74
+ if self.qv_variant == 'dynamic_qconditioned_fullwidth':
75
+ self.q_gate_proj_full = nn.Linear(config.n_embd, config.n_embd, bias=False)
76
+
77
+ if self.qv_variant == 'dynamic_qconditioned_fullwidth_headspecific':
78
+ # full-width Q source (768) with separate 768->64 map for each head
79
+ self.q_gate_proj_full_headspecific = nn.Parameter(torch.empty(config.n_head, config.n_embd, hs))
80
+ nn.init.normal_(self.q_gate_proj_full_headspecific, mean=0.0, std=0.02)
81
+
82
+ if self.qv_variant == 'dynamic_q_headshared_elementwise':
83
+ self.q_gate_headshared = nn.Linear(hs, hs, bias=False)
84
+
85
+ # --------------------------------------------------
86
+ # X-conditioned gates
87
+ # --------------------------------------------------
88
+ if self.qv_variant == 'dynamic_xconditioned_g1':
89
+ self.x_gate_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
90
+
91
+ if self.qv_variant == 'dynamic_xconditioned_fullwidth_headspecific':
92
+ # full-width X source (768) with separate 768->64 map for each head
93
+ self.x_gate_proj_full_headspecific = nn.Parameter(torch.empty(config.n_head, config.n_embd, hs))
94
+ nn.init.normal_(self.x_gate_proj_full_headspecific, mean=0.0, std=0.02)
95
+
96
+ if self.qv_variant == 'dynamic_xconditioned_bottleneck':
97
+ self.x_gate_bottleneck = nn.Linear(config.n_embd, hs, bias=False)
98
+
99
+ if self.qv_variant == 'dynamic_x_g1_headspecific_elementwise':
100
+ self.x_gate_headspecific_elementwise = nn.Parameter(torch.empty(config.n_head, hs, hs))
101
+ nn.init.normal_(self.x_gate_headspecific_elementwise, mean=0.0, std=0.02)
102
+
103
+ if self.qv_variant == 'dynamic_x_g1_headspecific_headwise':
104
+ self.x_gate_headspecific_headwise = nn.Parameter(torch.empty(config.n_head, hs, 1))
105
+ nn.init.normal_(self.x_gate_headspecific_headwise, mean=0.0, std=0.02)
106
+
107
+ if self.qv_variant == 'dynamic_x_g1_headshared_elementwise':
108
+ self.x_gate_headshared_elementwise = nn.Linear(hs, hs, bias=False)
109
+
110
+ # --------------------------------------------------
111
+ # Q+A dual-signal gates
112
+ # --------------------------------------------------
113
+ # random gates have no learned parameters — nothing to init here
114
+ # dot product gates also have no learned parameters — nothing to init here
115
+
116
+ if self.qv_variant == 'dynamic_a_conditioned':
117
+ # A-only: per-head Linear(64->64), conditioned on y only
118
+ self.a_gate_proj = nn.Linear(hs, hs, bias=False)
119
+
120
+ if self.qv_variant == 'dynamic_qa_conditioned':
121
+ # cheap Q+A: shared Linear(128->64) applied to each head/token
122
+ self.qa_gate_proj = nn.Linear(hs * 2, hs, bias=False)
123
+
124
+ if self.qv_variant == 'dynamic_qa_conditioned_headspecific':
125
+ # true head-specific Q+A: separate 128->64 matrix for each head
126
+ self.qa_gate_proj_headspecific = nn.Parameter(torch.empty(config.n_head, hs * 2, hs))
127
+ nn.init.normal_(self.qa_gate_proj_headspecific, mean=0.0, std=0.02)
128
+
129
+ if self.qv_variant == 'dynamic_qa_conditioned_mlp128':
130
+ # main Q+A MLP: per-head MLP(128->128->64)
131
+ self.qa_gate_fc1 = nn.Linear(hs * 2, 128, bias=False)
132
+ self.qa_gate_fc2 = nn.Linear(128, hs, bias=False)
133
+
134
+ if self.qv_variant == 'dynamic_qa_headshared_elementwise':
135
+ # legacy compatibility variant: shared Linear(128->64) applied per head/token
136
+ self.qa_gate_headshared = nn.Linear(hs * 2, hs, bias=False)
137
+
138
+ if self.qv_variant == 'dynamic_qa_bilinear_diag':
139
+ # learned elementwise interaction on q*y; 64 params per layer
140
+ self.qa_bilinear_diag = nn.Parameter(torch.ones(hs))
141
+
142
+ if self.qv_variant == 'dynamic_qa_conditioned_normed':
143
+ # RMSNorm q and y separately before shared Linear(128->64)
144
+ self.qa_q_norm = RMSNorm(hs)
145
+ self.qa_y_norm = RMSNorm(hs)
146
+ self.qa_gate_proj_normed = nn.Linear(hs * 2, hs, bias=False)
147
+
148
+ if self.qv_variant == 'dynamic_qa_conditioned_normed_yonly':
149
+ # RMSNorm y only, raw q, before shared Linear(128->64)
150
+ self.qa_y_norm_yonly = RMSNorm(hs)
151
+ self.qa_gate_proj_normed_yonly = nn.Linear(hs * 2, hs, bias=False)
152
+
153
+ if self.qv_variant == 'dynamic_qa_conditioned_normed_qonly':
154
+ # RMSNorm q only, raw y, before shared Linear(128->64)
155
+ self.qa_q_norm_qonly = RMSNorm(hs)
156
+ self.qa_gate_proj_normed_qonly = nn.Linear(hs * 2, hs, bias=False)
157
+
158
+ if self.qv_variant == 'dynamic_qa_conditioned_softq':
159
+ # Learnable blend between RMSNorm(q) and raw q, with raw y
160
+ self.qa_q_norm_soft = RMSNorm(hs)
161
+ self.qa_gate_proj_softq = nn.Linear(hs * 2, hs, bias=False)
162
+ self.qa_q_blend_alpha = nn.Parameter(torch.tensor(0.5))
163
+
164
+ if self.qv_variant == 'dynamic_qa_conditioned_softqa':
165
+ # Independent learnable blends for RMSNorm(q)/raw q and RMSNorm(y)/raw y
166
+ self.qa_q_norm_softqa = RMSNorm(hs)
167
+ self.qa_y_norm_softqa = RMSNorm(hs)
168
+ self.qa_gate_proj_softqa = nn.Linear(hs * 2, hs, bias=False)
169
+ self.qa_q_blend_alpha = nn.Parameter(torch.tensor(0.5))
170
+ self.qa_y_blend_alpha = nn.Parameter(torch.tensor(0.5))
171
+
172
+ if self.qv_variant == 'dynamic_qa_conditioned_softqa_perlayer':
173
+ # Per-layer independent learnable blends for RMSNorm(q)/raw q and RMSNorm(y)/raw y
174
+ self.qa_q_norm_softqa_pl = RMSNorm(hs)
175
+ self.qa_y_norm_softqa_pl = RMSNorm(hs)
176
+ self.qa_gate_proj_softqa_pl = nn.Linear(hs * 2, hs, bias=False)
177
+ self.qa_q_blend_alpha = nn.Parameter(torch.tensor(0.0))
178
+ self.qa_y_blend_alpha = nn.Parameter(torch.tensor(0.0))
179
+
180
+ if self.qv_variant == 'dynamic_qa_conditioned_softqa_perdim':
181
+ # Per-dimension independent learnable blends for RMSNorm(q)/raw q and RMSNorm(y)/raw y
182
+ self.qa_q_norm_softqa_pd = RMSNorm(hs)
183
+ self.qa_y_norm_softqa_pd = RMSNorm(hs)
184
+ self.qa_gate_proj_softqa_pd = nn.Linear(hs * 2, hs, bias=False)
185
+ self.qa_q_dim_scale = nn.Parameter(torch.zeros(hs))
186
+ self.qa_y_dim_scale = nn.Parameter(torch.zeros(hs))
187
+
188
+ if self.qv_variant == 'dynamic_qa_conditioned_softqa_perdim_informed':
189
+ # Per-dimension independent learnable blends with informed initialization:
190
+ # 1.0459 = sigmoid^-1(0.74), matching the Phase 17 converged scalar value.
191
+ self.qa_q_norm_softqa_pd = RMSNorm(hs)
192
+ self.qa_y_norm_softqa_pd = RMSNorm(hs)
193
+ self.qa_gate_proj_softqa_pd = nn.Linear(hs * 2, hs, bias=False)
194
+ self.qa_q_dim_scale = nn.Parameter(torch.full((hs,), 1.0459))
195
+ self.qa_y_dim_scale = nn.Parameter(torch.full((hs,), 1.0459))
196
+
197
+ if self.qv_variant == 'dynamic_qa_conditioned_softqa_perdim_free':
198
+ # Per-dimension independent learnable blends with tiny random symmetry breaking:
199
+ # start near sigmoid(scale)=0.5, but each dimension differs slightly.
200
+ self.qa_q_norm_softqa_pd = RMSNorm(hs)
201
+ self.qa_y_norm_softqa_pd = RMSNorm(hs)
202
+ self.qa_gate_proj_softqa_pd = nn.Linear(hs * 2, hs, bias=False)
203
+ self.qa_q_dim_scale = nn.Parameter(torch.empty(hs))
204
+ self.qa_y_dim_scale = nn.Parameter(torch.empty(hs))
205
+ nn.init.normal_(self.qa_q_dim_scale, mean=0.0, std=0.01)
206
+ nn.init.normal_(self.qa_y_dim_scale, mean=0.0, std=0.01)
207
+
208
+ if self.qv_variant == 'dynamic_qa_conditioned_lowrank16':
209
+ # shared low-rank Q+A mixer: 128->16->64 per head/token
210
+ self.qa_gate_lowrank_fc1 = nn.Linear(hs * 2, 16, bias=False)
211
+ self.qa_gate_lowrank_fc2 = nn.Linear(16, hs, bias=False)
212
+
213
+ # --------------------------------------------------
214
+ # Static / normalization controls
215
+ # --------------------------------------------------
216
+ if self.qv_variant == 'post_rmsnorm_y':
217
+ self.post_attn_norm = RMSNorm(hs)
218
+ elif self.qv_variant == 'static_gate':
219
+ self.static_gate_param = nn.Parameter(torch.zeros(config.n_embd))
220
+ elif self.qv_variant == 'static_gate_prehead':
221
+ self.static_gate_prehead_param = nn.Parameter(torch.zeros(config.n_head, hs))
222
+
223
+ self.q_norm = None
224
+ self.v_norm = None
225
+ self.qv_modulator = None
226
+ variant = getattr(config, 'qv_variant', 'none')
227
+ if variant in ['qvnorm']:
228
+ self.q_norm = RMSNorm(hs)
229
+ if variant in ['vnorm', 'qvnorm']:
230
+ self.v_norm = RMSNorm(hs)
231
+
232
+ self.attn_dropout = nn.Dropout(config.dropout)
233
+ self.resid_dropout = nn.Dropout(config.dropout)
234
+ self.n_head = config.n_head
235
+ self.n_embd = config.n_embd
236
+ self.dropout = config.dropout
237
+ self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
238
+ if not self.flash:
239
+ self.register_buffer(
240
+ "bias",
241
+ torch.tril(torch.ones(config.block_size, config.block_size)).view(
242
+ 1, 1, config.block_size, config.block_size
243
+ ),
244
+ )
245
+
246
+ def forward(self, x, x_prenorm=None):
247
+ B, T, C = x.size()
248
+ hs = C // self.n_head
249
+
250
+ q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
251
+ k = k.view(B, T, self.n_head, hs).transpose(1, 2)
252
+ q = q.view(B, T, self.n_head, hs).transpose(1, 2)
253
+ v = v.view(B, T, self.n_head, hs).transpose(1, 2)
254
+
255
+ if self.q_norm is not None:
256
+ q = self.q_norm(q)
257
+ if self.v_norm is not None:
258
+ v = self.v_norm(v)
259
+
260
+ if self.flash:
261
+ y = torch.nn.functional.scaled_dot_product_attention(
262
+ q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0
263
+ )
264
+ else:
265
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
266
+ att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
267
+ att = F.softmax(att, dim=-1)
268
+ att = self.attn_dropout(att)
269
+ y = att @ v
270
+
271
+ # --------------------------------------------------
272
+ # Gate application — y shape: (B, n_head, T, hs)
273
+ # --------------------------------------------------
274
+
275
+ # Q-conditioned gates
276
+ if self.qv_variant in ['dynamic', 'dynamic_swiglu']:
277
+ gate_logit = self.qv_gate_proj(q)
278
+ gate = torch.sigmoid(gate_logit) if self.qv_variant == 'dynamic' else gate_logit * torch.sigmoid(gate_logit)
279
+ y = y * gate
280
+
281
+ elif self.qv_variant == 'dynamic_qconditioned_mlp128':
282
+ gate_hidden = F.silu(self.q_gate_fc1(q))
283
+ gate_logit = self.q_gate_fc2(gate_hidden)
284
+ gate = torch.sigmoid(gate_logit)
285
+ y = y * gate
286
+
287
+ elif self.qv_variant == 'dynamic_qconditioned_mlp192':
288
+ gate_hidden = F.silu(self.q_gate_fc1_192(q))
289
+ gate_logit = self.q_gate_fc2_192(gate_hidden)
290
+ gate = torch.sigmoid(gate_logit)
291
+ y = y * gate
292
+
293
+ elif self.qv_variant == 'dynamic_qconditioned_fullwidth':
294
+ q_full = q.transpose(1, 2).contiguous().view(B, T, C)
295
+ gate_logit = self.q_gate_proj_full(q_full)
296
+ gate = torch.sigmoid(gate_logit)
297
+ gate = gate.view(B, T, self.n_head, hs).transpose(1, 2)
298
+ y = y * gate
299
+
300
+ elif self.qv_variant == 'dynamic_qconditioned_fullwidth_headspecific':
301
+ q_full = q.transpose(1, 2).contiguous().view(B, T, C)
302
+ gate_logit = torch.einsum('btc,hce->bhte', q_full, self.q_gate_proj_full_headspecific)
303
+ gate = torch.sigmoid(gate_logit)
304
+ y = y * gate
305
+
306
+ elif self.qv_variant == 'dynamic_q_headshared_elementwise':
307
+ gate_logit = self.q_gate_headshared(q)
308
+ gate = torch.sigmoid(gate_logit)
309
+ y = y * gate
310
+
311
+ # Static / normalization controls
312
+ elif self.qv_variant == 'post_rmsnorm_y':
313
+ y = self.post_attn_norm(y)
314
+
315
+ elif self.qv_variant == 'static_gate':
316
+ gate = torch.sigmoid(self.static_gate_param)
317
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
318
+ y = y * gate[None, None, :]
319
+
320
+ elif self.qv_variant == 'static_gate_prehead':
321
+ gate = torch.sigmoid(self.static_gate_prehead_param)
322
+ y = y * gate[None, :, None, :]
323
+
324
+ # X-conditioned gates
325
+ elif self.qv_variant == 'dynamic_xconditioned_g1':
326
+ gate_logit = self.x_gate_proj(x_prenorm)
327
+ gate = torch.sigmoid(gate_logit)
328
+ gate = gate.view(B, T, self.n_head, hs).transpose(1, 2)
329
+ y = y * gate
330
+
331
+ elif self.qv_variant == 'dynamic_xconditioned_fullwidth_headspecific':
332
+ gate_logit = torch.einsum('btc,hce->bhte', x_prenorm, self.x_gate_proj_full_headspecific)
333
+ gate = torch.sigmoid(gate_logit)
334
+ y = y * gate
335
+
336
+ elif self.qv_variant == 'dynamic_xconditioned_bottleneck':
337
+ z = self.x_gate_bottleneck(x_prenorm)
338
+ gate = torch.sigmoid(z)
339
+ gate = gate.unsqueeze(1)
340
+ y = y * gate
341
+
342
+ elif self.qv_variant == 'dynamic_x_g1_headspecific_elementwise':
343
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
344
+ gate_logit = torch.einsum('bhtd,hde->bhte', xh, self.x_gate_headspecific_elementwise)
345
+ gate = torch.sigmoid(gate_logit)
346
+ y = y * gate
347
+
348
+ elif self.qv_variant == 'dynamic_x_g1_headspecific_headwise':
349
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
350
+ gate_logit = torch.einsum('bhtd,hde->bhte', xh, self.x_gate_headspecific_headwise)
351
+ gate = torch.sigmoid(gate_logit)
352
+ y = y * gate
353
+
354
+ elif self.qv_variant == 'dynamic_x_g1_headshared_elementwise':
355
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
356
+ gate_logit = self.x_gate_headshared_elementwise(xh)
357
+ gate = torch.sigmoid(gate_logit)
358
+ y = y * gate
359
+
360
+ # Random / ablation gates — no learned params
361
+ elif self.qv_variant == 'dynamic_random_gate':
362
+ # Uniform random [0,1] gate — tests if location matters at all
363
+ gate = torch.rand_like(y)
364
+ y = y * gate
365
+
366
+ elif self.qv_variant == 'dynamic_ones_gate':
367
+ # Always 1.0 — pure identity, sanity check (should == baseline)
368
+ pass # y unchanged
369
+
370
+ elif self.qv_variant == 'dynamic_random_normal':
371
+ # Normal(0.5, 0.2) gate clamped to [0,1] — different noise shape
372
+ gate = torch.randn_like(y) * 0.2 + 0.5
373
+ gate = gate.clamp(0.0, 1.0)
374
+ y = y * gate
375
+
376
+ elif self.qv_variant == 'dynamic_bernoulli_gate':
377
+ # Binary gate: randomly zeros 50% of head dims — spicy dropout variant
378
+ gate = torch.bernoulli(torch.full_like(y, 0.5))
379
+ # Scale by 2.0 to preserve expected value (like standard dropout)
380
+ y = y * gate * 2.0
381
+
382
+ # A-only gate
383
+ elif self.qv_variant == 'dynamic_a_conditioned':
384
+ gate_logit = self.a_gate_proj(y)
385
+ gate = torch.sigmoid(gate_logit)
386
+ y = y * gate
387
+
388
+ # Q+A dual-signal gates
389
+ elif self.qv_variant == 'dynamic_qa_conditioned':
390
+ # cheap Q+A: cat(q, y) -> shared Linear(128->64) -> sigmoid
391
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
392
+ gate_logit = self.qa_gate_proj(gate_in)
393
+ gate = torch.sigmoid(gate_logit)
394
+ y = y * gate
395
+
396
+ elif self.qv_variant == 'dynamic_qa_conditioned_headspecific':
397
+ # true head-specific Q+A: per-head (128->64) -> sigmoid
398
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
399
+ gate_logit = torch.einsum('bhtd,hde->bhte', gate_in, self.qa_gate_proj_headspecific)
400
+ gate = torch.sigmoid(gate_logit)
401
+ y = y * gate
402
+
403
+ elif self.qv_variant == 'dynamic_qa_conditioned_mlp128':
404
+ # main Q+A MLP: cat(q, y) -> Linear(128->128) -> SiLU -> Linear(128->64) -> sigmoid
405
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
406
+ gate_hidden = F.silu(self.qa_gate_fc1(gate_in))
407
+ gate_logit = self.qa_gate_fc2(gate_hidden)
408
+ gate = torch.sigmoid(gate_logit)
409
+ y = y * gate
410
+
411
+ elif self.qv_variant == 'dynamic_qa_headshared_elementwise':
412
+ # legacy compatibility variant: cat(q, y) -> shared Linear(128->64) -> sigmoid
413
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
414
+ gate_logit = self.qa_gate_headshared(gate_in)
415
+ gate = torch.sigmoid(gate_logit)
416
+ y = y * gate
417
+
418
+ elif self.qv_variant == 'dynamic_qa_bilinear_diag':
419
+ # learned diagonal bilinear interaction: sigmoid((q * y * w) / sqrt(d_head))
420
+ gate_logit = (q * y * self.qa_bilinear_diag[None, None, None, :]) / math.sqrt(hs)
421
+ gate = torch.sigmoid(gate_logit)
422
+ y = y * gate
423
+
424
+ elif self.qv_variant == 'dynamic_qa_conditioned_normed':
425
+ qn = self.qa_q_norm(q)
426
+ yn = self.qa_y_norm(y)
427
+ gate_in = torch.cat([qn, yn], dim=-1)
428
+ gate_logit = self.qa_gate_proj_normed(gate_in)
429
+ gate = torch.sigmoid(gate_logit)
430
+ y = y * gate
431
+
432
+ elif self.qv_variant == 'dynamic_qa_conditioned_normed_yonly':
433
+ yn = self.qa_y_norm_yonly(y)
434
+ gate_in = torch.cat([q, yn], dim=-1)
435
+ gate_logit = self.qa_gate_proj_normed_yonly(gate_in)
436
+ gate = torch.sigmoid(gate_logit)
437
+ y = y * gate
438
+
439
+ elif self.qv_variant == 'dynamic_qa_conditioned_normed_qonly':
440
+ qn = self.qa_q_norm_qonly(q)
441
+ gate_in = torch.cat([qn, y], dim=-1)
442
+ gate_logit = self.qa_gate_proj_normed_qonly(gate_in)
443
+ gate = torch.sigmoid(gate_logit)
444
+ y = y * gate
445
+
446
+ elif self.qv_variant == 'dynamic_qa_conditioned_softq':
447
+ qn = self.qa_q_norm_soft(q)
448
+ alpha = torch.sigmoid(self.qa_q_blend_alpha)
449
+ q_blend = alpha * qn + (1.0 - alpha) * q
450
+ gate_in = torch.cat([q_blend, y], dim=-1)
451
+ gate_logit = self.qa_gate_proj_softq(gate_in)
452
+ gate = torch.sigmoid(gate_logit)
453
+ y = y * gate
454
+
455
+ elif self.qv_variant == 'dynamic_qa_conditioned_softqa':
456
+ qn = self.qa_q_norm_softqa(q)
457
+ yn = self.qa_y_norm_softqa(y)
458
+ alpha_q = torch.sigmoid(self.qa_q_blend_alpha)
459
+ alpha_y = torch.sigmoid(self.qa_y_blend_alpha)
460
+ q_blend = alpha_q * qn + (1.0 - alpha_q) * q
461
+ y_blend = alpha_y * yn + (1.0 - alpha_y) * y
462
+ gate_in = torch.cat([q_blend, y_blend], dim=-1)
463
+ gate_logit = self.qa_gate_proj_softqa(gate_in)
464
+ gate = torch.sigmoid(gate_logit)
465
+ y = y * gate
466
+
467
+ elif self.qv_variant == 'dynamic_qa_conditioned_softqa_perlayer':
468
+ qn = self.qa_q_norm_softqa_pl(q)
469
+ yn = self.qa_y_norm_softqa_pl(y)
470
+ alpha_q = torch.sigmoid(self.qa_q_blend_alpha)
471
+ alpha_y = torch.sigmoid(self.qa_y_blend_alpha)
472
+ q_blend = alpha_q * qn + (1.0 - alpha_q) * q
473
+ y_blend = alpha_y * yn + (1.0 - alpha_y) * y
474
+ gate_in = torch.cat([q_blend, y_blend], dim=-1)
475
+ gate_logit = self.qa_gate_proj_softqa_pl(gate_in)
476
+ gate = torch.sigmoid(gate_logit)
477
+ y = y * gate
478
+
479
+ elif self.qv_variant in ['dynamic_qa_conditioned_softqa_perdim', 'dynamic_qa_conditioned_softqa_perdim_informed', 'dynamic_qa_conditioned_softqa_perdim_free']:
480
+ qn = self.qa_q_norm_softqa_pd(q)
481
+ yn = self.qa_y_norm_softqa_pd(y)
482
+ scale_q = torch.sigmoid(self.qa_q_dim_scale).to(dtype=q.dtype, device=q.device)
483
+ scale_y = torch.sigmoid(self.qa_y_dim_scale).to(dtype=y.dtype, device=y.device)
484
+ q_blend = scale_q * qn + (1.0 - scale_q) * q
485
+ y_blend = scale_y * yn + (1.0 - scale_y) * y
486
+ gate_in = torch.cat([q_blend, y_blend], dim=-1)
487
+ gate_logit = self.qa_gate_proj_softqa_pd(gate_in)
488
+ gate = torch.sigmoid(gate_logit)
489
+ y = y * gate
490
+
491
+ elif self.qv_variant == 'dynamic_qa_conditioned_lowrank16':
492
+ gate_in = torch.cat([q, y], dim=-1)
493
+ gate_hidden = F.silu(self.qa_gate_lowrank_fc1(gate_in))
494
+ gate_logit = self.qa_gate_lowrank_fc2(gate_hidden)
495
+ gate = torch.sigmoid(gate_logit)
496
+ y = y * gate
497
+
498
+ # Zero-parameter dot product gates — raw Q·y geometry
499
+ elif self.qv_variant == 'dynamic_dot_scalar':
500
+ # scalar gate: sigmoid(sum(q*y) / sqrt(d_head))
501
+ # one scalar per head per token, zero params
502
+ hs = q.shape[-1]
503
+ dot = (q * y).sum(dim=-1, keepdim=True) # (B, n_head, T, 1)
504
+ gate = torch.sigmoid(dot / math.sqrt(hs)) # scalar broadcast
505
+ y = y * gate
506
+
507
+ elif self.qv_variant == 'dynamic_dot_elementwise':
508
+ # elementwise gate: sigmoid(q*y / sqrt(d_head))
509
+ # same shape as cheap_qa gate but zero params
510
+ hs = q.shape[-1]
511
+ gate = torch.sigmoid((q * y) / math.sqrt(hs)) # (B, n_head, T, 64)
512
+ y = y * gate
513
+
514
+ if self.qv_variant != 'static_gate':
515
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
516
+
517
+ y = self.resid_dropout(self.c_proj(y))
518
+ return y
519
+
520
+ class MLP(nn.Module):
521
+ def __init__(self, config):
522
+ super().__init__()
523
+ self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
524
+ self.gelu = nn.GELU()
525
+ self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
526
+ self.dropout = nn.Dropout(config.dropout)
527
+ def forward(self, x):
528
+ x = self.c_fc(x)
529
+ x = self.gelu(x)
530
+ x = self.c_proj(x)
531
+ x = self.dropout(x)
532
+ return x
533
+
534
+ class Block(nn.Module):
535
+ def __init__(self, config):
536
+ super().__init__()
537
+ self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
538
+ self.attn = CausalSelfAttention(config)
539
+ self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
540
+ self.mlp = MLP(config)
541
+ def forward(self, x):
542
+ x_norm = self.ln_1(x)
543
+ attn_out = self.attn(x_norm, x_norm)
544
+ x = x + attn_out
545
+ x = x + self.mlp(self.ln_2(x))
546
+ return x
547
+
548
+ @dataclass
549
+ class GPTConfig:
550
+ block_size: int = 1024
551
+ vocab_size: int = 50304
552
+ n_layer: int = 12
553
+ n_head: int = 12
554
+ n_embd: int = 768
555
+ dropout: float = 0.0
556
+ bias: bool = True
557
+ qv_variant: str = 'none'
558
+
559
+ class GPT(nn.Module):
560
+ def __init__(self, config):
561
+ super().__init__()
562
+ assert config.vocab_size is not None
563
+ assert config.block_size is not None
564
+ self.config = config
565
+ self.transformer = nn.ModuleDict(dict(
566
+ wte = nn.Embedding(config.vocab_size, config.n_embd),
567
+ wpe = nn.Embedding(config.block_size, config.n_embd),
568
+ drop = nn.Dropout(config.dropout),
569
+ h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
570
+ ln_f = LayerNorm(config.n_embd, bias=config.bias),
571
+ ))
572
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
573
+ self.transformer.wte.weight = self.lm_head.weight
574
+ self.apply(self._init_weights)
575
+ for pn, p in self.named_parameters():
576
+ if pn.endswith('c_proj.weight'):
577
+ torch.nn.init.normal_(p, mean=0.0, std=0.02/math.sqrt(2 * config.n_layer))
578
+ print("number of parameters: %.2fM" % (self.get_num_params()/1e6,))
579
+
580
+ def get_num_params(self, non_embedding=True):
581
+ n_params = sum(p.numel() for p in self.parameters())
582
+ if non_embedding:
583
+ n_params -= self.transformer.wpe.weight.numel()
584
+ return n_params
585
+
586
+ def _init_weights(self, module):
587
+ if isinstance(module, nn.Linear):
588
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
589
+ if module.bias is not None:
590
+ torch.nn.init.zeros_(module.bias)
591
+ elif isinstance(module, nn.Embedding):
592
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
593
+
594
+ def forward(self, idx, targets=None):
595
+ device = idx.device
596
+ b, t = idx.size()
597
+ assert t <= self.config.block_size
598
+ pos = torch.arange(0, t, dtype=torch.long, device=device)
599
+ tok_emb = self.transformer.wte(idx)
600
+ pos_emb = self.transformer.wpe(pos)
601
+ x = self.transformer.drop(tok_emb + pos_emb)
602
+ for block in self.transformer.h:
603
+ x = block(x)
604
+ x = self.transformer.ln_f(x)
605
+ if targets is not None:
606
+ logits = self.lm_head(x)
607
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
608
+ else:
609
+ logits = self.lm_head(x[:, [-1], :])
610
+ loss = None
611
+ return logits, loss
612
+
613
+ def crop_block_size(self, block_size):
614
+ assert block_size <= self.config.block_size
615
+ self.config.block_size = block_size
616
+ self.transformer.wpe.weight = nn.Parameter(self.transformer.wpe.weight[:block_size])
617
+ for block in self.transformer.h:
618
+ if hasattr(block.attn, 'bias'):
619
+ block.attn.bias = block.attn.bias[:, :, :block_size, :block_size]
620
+
621
+ @classmethod
622
+ def from_pretrained(cls, model_type, override_args=None):
623
+ assert model_type in {'gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl'}
624
+ override_args = override_args or {}
625
+ assert all(k == 'dropout' for k in override_args)
626
+ from transformers import GPT2LMHeadModel
627
+ print("loading weights from pretrained gpt: %s" % model_type)
628
+ config_args = {
629
+ 'gpt2': dict(n_layer=12, n_head=12, n_embd=768),
630
+ 'gpt2-medium': dict(n_layer=24, n_head=16, n_embd=1024),
631
+ 'gpt2-large': dict(n_layer=36, n_head=20, n_embd=1280),
632
+ 'gpt2-xl': dict(n_layer=48, n_head=25, n_embd=1600),
633
+ }[model_type]
634
+ config_args['vocab_size'] = 50257
635
+ config_args['block_size'] = 1024
636
+ config_args['bias'] = True
637
+ if 'dropout' in override_args:
638
+ config_args['dropout'] = override_args['dropout']
639
+ config = GPTConfig(**config_args)
640
+ model = GPT(config)
641
+ sd = model.state_dict()
642
+ sd_keys = [k for k in sd.keys() if not k.endswith('.attn.bias')]
643
+ model_hf = GPT2LMHeadModel.from_pretrained(model_type)
644
+ sd_hf = model_hf.state_dict()
645
+ sd_keys_hf = [k for k in sd_hf.keys() if not k.endswith('.attn.masked_bias') and not k.endswith('.attn.bias')]
646
+ transposed = ['attn.c_attn.weight', 'attn.c_proj.weight', 'mlp.c_fc.weight', 'mlp.c_proj.weight']
647
+ assert len(sd_keys_hf) == len(sd_keys)
648
+ for k in sd_keys_hf:
649
+ if any(k.endswith(w) for w in transposed):
650
+ assert sd_hf[k].shape[::-1] == sd[k].shape
651
+ with torch.no_grad():
652
+ sd[k].copy_(sd_hf[k].t())
653
+ else:
654
+ assert sd_hf[k].shape == sd[k].shape
655
+ with torch.no_grad():
656
+ sd[k].copy_(sd_hf[k])
657
+ return model
658
+
659
+ def configure_optimizers(self, weight_decay, learning_rate, betas, device_type):
660
+ param_dict = {pn: p for pn, p in self.named_parameters() if p.requires_grad}
661
+ decay_params = [p for n, p in param_dict.items() if p.dim() >= 2]
662
+ nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2]
663
+ optim_groups = [
664
+ {'params': decay_params, 'weight_decay': weight_decay},
665
+ {'params': nodecay_params, 'weight_decay': 0.0}
666
+ ]
667
+ num_decay_params = sum(p.numel() for p in decay_params)
668
+ num_nodecay_params = sum(p.numel() for p in nodecay_params)
669
+ print(f"num decayed parameter tensors: {len(decay_params)}, with {num_decay_params:,} parameters")
670
+ print(f"num non-decayed parameter tensors: {len(nodecay_params)}, with {num_nodecay_params:,} parameters")
671
+ fused_available = 'fused' in inspect.signature(torch.optim.AdamW).parameters
672
+ use_fused = fused_available and device_type == 'cuda'
673
+ optimizer = torch.optim.AdamW(optim_groups, lr=learning_rate, betas=betas, fused=use_fused)
674
+ print(f"using fused AdamW: {use_fused}")
675
+ return optimizer
676
+
677
+ def estimate_mfu(self, fwdbwd_per_iter, dt):
678
+ N = self.get_num_params()
679
+ cfg = self.config
680
+ L, H, Q, T = cfg.n_layer, cfg.n_head, cfg.n_embd // cfg.n_head, cfg.block_size
681
+ flops_per_token = 6 * N + 12 * L * H * Q * T
682
+ flops_per_fwdbwd = flops_per_token * T
683
+ flops_per_iter = flops_per_fwdbwd * fwdbwd_per_iter
684
+ flops_achieved = flops_per_iter * (1.0 / dt)
685
+ flops_promised = 312e12
686
+ mfu = flops_achieved / flops_promised
687
+ return mfu
688
+
689
+ @torch.no_grad()
690
+ def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
691
+ for _ in range(max_new_tokens):
692
+ idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
693
+ logits, _ = self(idx_cond)
694
+ logits = logits[:, -1, :] / temperature
695
+ if top_k is not None:
696
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
697
+ logits[logits < v[:, [-1]]] = -float('Inf')
698
+ probs = F.softmax(logits, dim=-1)
699
+ idx_next = torch.multinomial(probs, num_samples=1)
700
+ idx = torch.cat((idx, idx_next), dim=1)
701
+ return idx
code/model_cheap_qa_minimal.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Minimal single-variant GPT — cheap_qa champion ONLY.
3
+
4
+ Distilled from the 19-variant env_canon model.py. Every other gate branch has been
5
+ removed; the ONLY conditioning path is the cheap_qa gate:
6
+
7
+ cheap_qa == qv_variant "dynamic_qa_conditioned"
8
+ gate = sigmoid( Linear(2*head_dim -> head_dim) ( cat(q, y) ) )
9
+ y = y * gate # applied at G1: post-SDPA, pre-W_O (c_proj)
10
+
11
+ Champion result (NanoGPT 124M, OpenWebText, 10k iters, seeds 42/1337/123):
12
+ baseline 4.4708 -> cheap_qa 4.3897 (-0.0811 CE, -1.81%), 98,304 params (+0.079%).
13
+
14
+ >>> THE G1 SEAM IS MARKED BELOW WITH: # === G1 SEAM ===
15
+ That block is the entire port surface. Resize the gate for a different attention
16
+ by changing head_dim (hs); the gate is always Linear(2*hs -> hs) on cat(q, y).
17
+ """
18
+
19
+ import math
20
+ import inspect
21
+ from dataclasses import dataclass
22
+ import torch
23
+ import torch.nn as nn
24
+ from torch.nn import functional as F
25
+
26
+
27
+ class LayerNorm(nn.Module):
28
+ def __init__(self, ndim, bias):
29
+ super().__init__()
30
+ self.weight = nn.Parameter(torch.ones(ndim))
31
+ self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None
32
+
33
+ def forward(self, input):
34
+ return F.layer_norm(input, self.weight.shape, self.weight, self.bias, 1e-5)
35
+
36
+
37
+ class CausalSelfAttention(nn.Module):
38
+ def __init__(self, config):
39
+ super().__init__()
40
+ assert config.n_embd % config.n_head == 0
41
+ self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
42
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias) # <- W_O
43
+
44
+ hs = config.n_embd // config.n_head # head_dim (64 for the 124M config)
45
+
46
+ # === cheap_qa gate parameters ===
47
+ # cat(q, y) has width 2*hs (=128 when hs=64); gate maps it back to hs (=64).
48
+ # This is the ONLY learned gate in this minimal model.
49
+ self.qa_gate_proj = nn.Linear(hs * 2, hs, bias=False) # Linear(128 -> 64)
50
+
51
+ self.attn_dropout = nn.Dropout(config.dropout)
52
+ self.resid_dropout = nn.Dropout(config.dropout)
53
+ self.n_head = config.n_head
54
+ self.n_embd = config.n_embd
55
+ self.dropout = config.dropout
56
+ self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
57
+ if not self.flash:
58
+ self.register_buffer(
59
+ "bias",
60
+ torch.tril(torch.ones(config.block_size, config.block_size)).view(
61
+ 1, 1, config.block_size, config.block_size
62
+ ),
63
+ )
64
+
65
+ def forward(self, x):
66
+ B, T, C = x.size()
67
+ hs = C // self.n_head
68
+
69
+ # Project to q, k, v and split into heads. q and v are (B, n_head, T, hs).
70
+ q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
71
+ k = k.view(B, T, self.n_head, hs).transpose(1, 2)
72
+ q = q.view(B, T, self.n_head, hs).transpose(1, 2)
73
+ v = v.view(B, T, self.n_head, hs).transpose(1, 2)
74
+
75
+ # Scaled dot-product attention -> y is the attention output (the "A" in Q+A).
76
+ if self.flash:
77
+ y = torch.nn.functional.scaled_dot_product_attention(
78
+ q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0
79
+ )
80
+ else:
81
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
82
+ att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
83
+ att = F.softmax(att, dim=-1)
84
+ att = self.attn_dropout(att)
85
+ y = att @ v
86
+
87
+ # =========================== G1 SEAM ============================
88
+ # cheap_qa gate. Inputs available here: q and y, both (B, n_head, T, hs).
89
+ # Location: strictly AFTER SDPA, strictly BEFORE the W_O projection (c_proj).
90
+ # To port to a different attention: keep this exact 4-line body; only hs changes.
91
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 2*hs) == cat(query, attn_output)
92
+ gate_logit = self.qa_gate_proj(gate_in) # Linear(2*hs -> hs)
93
+ gate = torch.sigmoid(gate_logit) # (B, n_head, T, hs) in (0, 1)
94
+ y = y * gate # elementwise gate on the attention output
95
+ # ========================= END G1 SEAM ==========================
96
+
97
+ # Merge heads back and apply the output projection W_O.
98
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
99
+ y = self.resid_dropout(self.c_proj(y)) # c_proj == W_O
100
+ return y
101
+
102
+
103
+ class MLP(nn.Module):
104
+ def __init__(self, config):
105
+ super().__init__()
106
+ self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
107
+ self.gelu = nn.GELU()
108
+ self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
109
+ self.dropout = nn.Dropout(config.dropout)
110
+
111
+ def forward(self, x):
112
+ x = self.c_fc(x)
113
+ x = self.gelu(x)
114
+ x = self.c_proj(x)
115
+ x = self.dropout(x)
116
+ return x
117
+
118
+
119
+ class Block(nn.Module):
120
+ def __init__(self, config):
121
+ super().__init__()
122
+ self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
123
+ self.attn = CausalSelfAttention(config)
124
+ self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
125
+ self.mlp = MLP(config)
126
+
127
+ def forward(self, x):
128
+ x = x + self.attn(self.ln_1(x))
129
+ x = x + self.mlp(self.ln_2(x))
130
+ return x
131
+
132
+
133
+ @dataclass
134
+ class GPTConfig:
135
+ block_size: int = 1024
136
+ vocab_size: int = 50304 # NOTE: 50304 (GPT-2 50257 rounded up for efficiency)
137
+ n_layer: int = 12
138
+ n_head: int = 12
139
+ n_embd: int = 768 # -> head_dim = 768 / 12 = 64, so the gate is Linear(128 -> 64)
140
+ dropout: float = 0.0
141
+ bias: bool = True
142
+ # qv_variant is fixed to cheap_qa in this minimal model; no field needed.
143
+
144
+
145
+ class GPT(nn.Module):
146
+ def __init__(self, config):
147
+ super().__init__()
148
+ assert config.vocab_size is not None
149
+ assert config.block_size is not None
150
+ self.config = config
151
+ self.transformer = nn.ModuleDict(dict(
152
+ wte=nn.Embedding(config.vocab_size, config.n_embd),
153
+ wpe=nn.Embedding(config.block_size, config.n_embd),
154
+ drop=nn.Dropout(config.dropout),
155
+ h=nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
156
+ ln_f=LayerNorm(config.n_embd, bias=config.bias),
157
+ ))
158
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
159
+ self.transformer.wte.weight = self.lm_head.weight # weight tying
160
+ self.apply(self._init_weights)
161
+ for pn, p in self.named_parameters():
162
+ if pn.endswith('c_proj.weight'):
163
+ torch.nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * config.n_layer))
164
+ print("number of parameters: %.2fM" % (self.get_num_params() / 1e6,))
165
+
166
+ def get_num_params(self, non_embedding=True):
167
+ n_params = sum(p.numel() for p in self.parameters())
168
+ if non_embedding:
169
+ n_params -= self.transformer.wpe.weight.numel()
170
+ return n_params
171
+
172
+ def _init_weights(self, module):
173
+ if isinstance(module, nn.Linear):
174
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
175
+ if module.bias is not None:
176
+ torch.nn.init.zeros_(module.bias)
177
+ elif isinstance(module, nn.Embedding):
178
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
179
+
180
+ def forward(self, idx, targets=None):
181
+ device = idx.device
182
+ b, t = idx.size()
183
+ assert t <= self.config.block_size
184
+ pos = torch.arange(0, t, dtype=torch.long, device=device)
185
+ tok_emb = self.transformer.wte(idx)
186
+ pos_emb = self.transformer.wpe(pos)
187
+ x = self.transformer.drop(tok_emb + pos_emb)
188
+ for block in self.transformer.h:
189
+ x = block(x)
190
+ x = self.transformer.ln_f(x)
191
+ if targets is not None:
192
+ logits = self.lm_head(x)
193
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
194
+ else:
195
+ logits = self.lm_head(x[:, [-1], :])
196
+ loss = None
197
+ return logits, loss
198
+
199
+ def crop_block_size(self, block_size):
200
+ assert block_size <= self.config.block_size
201
+ self.config.block_size = block_size
202
+ self.transformer.wpe.weight = nn.Parameter(self.transformer.wpe.weight[:block_size])
203
+ for block in self.transformer.h:
204
+ if hasattr(block.attn, 'bias'):
205
+ block.attn.bias = block.attn.bias[:, :, :block_size, :block_size]
206
+
207
+ def configure_optimizers(self, weight_decay, learning_rate, betas, device_type):
208
+ param_dict = {pn: p for pn, p in self.named_parameters() if p.requires_grad}
209
+ decay_params = [p for n, p in param_dict.items() if p.dim() >= 2]
210
+ nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2]
211
+ optim_groups = [
212
+ {'params': decay_params, 'weight_decay': weight_decay},
213
+ {'params': nodecay_params, 'weight_decay': 0.0},
214
+ ]
215
+ num_decay_params = sum(p.numel() for p in decay_params)
216
+ num_nodecay_params = sum(p.numel() for p in nodecay_params)
217
+ print(f"num decayed parameter tensors: {len(decay_params)}, with {num_decay_params:,} parameters")
218
+ print(f"num non-decayed parameter tensors: {len(nodecay_params)}, with {num_nodecay_params:,} parameters")
219
+ fused_available = 'fused' in inspect.signature(torch.optim.AdamW).parameters
220
+ use_fused = fused_available and device_type == 'cuda'
221
+ optimizer = torch.optim.AdamW(optim_groups, lr=learning_rate, betas=betas, fused=use_fused)
222
+ print(f"using fused AdamW: {use_fused}")
223
+ return optimizer
224
+
225
+ def estimate_mfu(self, fwdbwd_per_iter, dt):
226
+ N = self.get_num_params()
227
+ cfg = self.config
228
+ L, H, Q, T = cfg.n_layer, cfg.n_head, cfg.n_embd // cfg.n_head, cfg.block_size
229
+ flops_per_token = 6 * N + 12 * L * H * Q * T
230
+ flops_per_fwdbwd = flops_per_token * T
231
+ flops_per_iter = flops_per_fwdbwd * fwdbwd_per_iter
232
+ flops_achieved = flops_per_iter * (1.0 / dt)
233
+ flops_promised = 312e12
234
+ return flops_achieved / flops_promised
235
+
236
+ @torch.no_grad()
237
+ def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
238
+ for _ in range(max_new_tokens):
239
+ idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
240
+ logits, _ = self(idx_cond)
241
+ logits = logits[:, -1, :] / temperature
242
+ if top_k is not None:
243
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
244
+ logits[logits < v[:, [-1]]] = -float('Inf')
245
+ probs = F.softmax(logits, dim=-1)
246
+ idx_next = torch.multinomial(probs, num_samples=1)
247
+ idx = torch.cat((idx, idx_next), dim=1)
248
+ return idx
code/model_wslbridge.py ADDED
@@ -0,0 +1,604 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Full definition of a GPT Language Model, all of it in this single file.
3
+ Includes:
4
+ - cheap Q gate
5
+ - Q-MLP gate (128 hidden)
6
+ - Q-MLP gate (192 hidden) -- param-matched control for Q+A MLP
7
+ - full-width Q gate
8
+ - Q head-shared elementwise gate
9
+ - full-width X gate
10
+ - bottleneck X gate
11
+ - paper-faithful X variants:
12
+ - head-specific elementwise G1
13
+ - head-specific headwise G1
14
+ - head-shared elementwise G1
15
+ - Q+A dual-signal variants:
16
+ - cheap Q+A gate (shared Linear 128->64 applied per head/token)
17
+ - Q+A MLP gate (shared MLP 128->128->64 applied per head/token)
18
+ - legacy Q+A head-shared elementwise gate (shared Linear 128->64; kept for compatibility)
19
+ - bilinear diagonal Q+A gate (learned elementwise q*y interaction)
20
+ - normed Q+A gate (RMSNorm on q and y before shared 128->64 mix)
21
+ - lowrank Q+A gate (shared 128->16->64 bottleneck mix)
22
+ - normed cheap Q gate (RMSNorm on q before shared 64->64 mix)
23
+ - normed Q-MLP gate (RMSNorm on q before shared 64->128->64 MLP)
24
+ """
25
+
26
+ import math
27
+ import inspect
28
+ from dataclasses import dataclass
29
+ import torch
30
+ import torch.nn as nn
31
+ from torch.nn import functional as F
32
+
33
+ class RMSNorm(nn.Module):
34
+ def __init__(self, dim: int, eps: float = 1e-6):
35
+ super().__init__()
36
+ self.eps = eps
37
+ self.weight = nn.Parameter(torch.ones(dim))
38
+ def forward(self, x):
39
+ rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
40
+ return x / rms * self.weight
41
+
42
+ class LayerNorm(nn.Module):
43
+ def __init__(self, ndim, bias):
44
+ super().__init__()
45
+ self.weight = nn.Parameter(torch.ones(ndim))
46
+ self.bias = nn.Parameter(torch.zeros(ndim)) if bias else None
47
+ def forward(self, input):
48
+ return F.layer_norm(input, self.weight.shape, self.weight, self.bias, 1e-5)
49
+
50
+ class CausalSelfAttention(nn.Module):
51
+ def __init__(self, config):
52
+ super().__init__()
53
+ assert config.n_embd % config.n_head == 0
54
+ self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
55
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
56
+
57
+ self.qv_variant = config.qv_variant
58
+ hs = config.n_embd // config.n_head
59
+
60
+ # --------------------------------------------------
61
+ # Q-conditioned gates
62
+ # --------------------------------------------------
63
+ if self.qv_variant in ['dynamic', 'dynamic_swiglu']:
64
+ self.qv_gate_proj = nn.Linear(hs, hs, bias=False)
65
+
66
+ if self.qv_variant == 'dynamic_qconditioned_mlp128':
67
+ self.q_gate_fc1 = nn.Linear(hs, 128, bias=False)
68
+ self.q_gate_fc2 = nn.Linear(128, hs, bias=False)
69
+
70
+ if self.qv_variant == 'dynamic_qconditioned_normed':
71
+ self.q_gate_norm = RMSNorm(hs)
72
+ self.q_gate_proj_normed = nn.Linear(hs, hs, bias=False)
73
+
74
+ if self.qv_variant == 'dynamic_qconditioned_mlp128_normed':
75
+ self.q_gate_norm_mlp128 = RMSNorm(hs)
76
+ self.q_gate_fc1_normed = nn.Linear(hs, 128, bias=False)
77
+ self.q_gate_fc2_normed = nn.Linear(128, hs, bias=False)
78
+
79
+ if self.qv_variant == 'dynamic_qconditioned_mlp192':
80
+ self.q_gate_fc1_192 = nn.Linear(hs, 192, bias=False)
81
+ self.q_gate_fc2_192 = nn.Linear(192, hs, bias=False)
82
+
83
+ if self.qv_variant == 'dynamic_qconditioned_fullwidth':
84
+ self.q_gate_proj_full = nn.Linear(config.n_embd, config.n_embd, bias=False)
85
+
86
+ if self.qv_variant == 'dynamic_qconditioned_fullwidth_headspecific':
87
+ # full-width Q source (768) with separate 768->64 map for each head
88
+ self.q_gate_proj_full_headspecific = nn.Parameter(torch.empty(config.n_head, config.n_embd, hs))
89
+ nn.init.normal_(self.q_gate_proj_full_headspecific, mean=0.0, std=0.02)
90
+
91
+ if self.qv_variant == 'dynamic_q_headshared_elementwise':
92
+ self.q_gate_headshared = nn.Linear(hs, hs, bias=False)
93
+
94
+ # --------------------------------------------------
95
+ # X-conditioned gates
96
+ # --------------------------------------------------
97
+ if self.qv_variant == 'dynamic_xconditioned_g1':
98
+ self.x_gate_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
99
+
100
+ if self.qv_variant == 'dynamic_xconditioned_fullwidth_headspecific':
101
+ # full-width X source (768) with separate 768->64 map for each head
102
+ self.x_gate_proj_full_headspecific = nn.Parameter(torch.empty(config.n_head, config.n_embd, hs))
103
+ nn.init.normal_(self.x_gate_proj_full_headspecific, mean=0.0, std=0.02)
104
+
105
+ if self.qv_variant == 'dynamic_xconditioned_bottleneck':
106
+ self.x_gate_bottleneck = nn.Linear(config.n_embd, hs, bias=False)
107
+
108
+ if self.qv_variant == 'dynamic_x_g1_headspecific_elementwise':
109
+ self.x_gate_headspecific_elementwise = nn.Parameter(torch.empty(config.n_head, hs, hs))
110
+ nn.init.normal_(self.x_gate_headspecific_elementwise, mean=0.0, std=0.02)
111
+
112
+ if self.qv_variant == 'dynamic_x_g1_headspecific_headwise':
113
+ self.x_gate_headspecific_headwise = nn.Parameter(torch.empty(config.n_head, hs, 1))
114
+ nn.init.normal_(self.x_gate_headspecific_headwise, mean=0.0, std=0.02)
115
+
116
+ if self.qv_variant == 'dynamic_x_g1_headshared_elementwise':
117
+ self.x_gate_headshared_elementwise = nn.Linear(hs, hs, bias=False)
118
+
119
+ # --------------------------------------------------
120
+ # Q+A dual-signal gates
121
+ # --------------------------------------------------
122
+ # random gates have no learned parameters — nothing to init here
123
+ # dot product gates also have no learned parameters — nothing to init here
124
+
125
+ if self.qv_variant == 'dynamic_a_conditioned':
126
+ # A-only: per-head Linear(64->64), conditioned on y only
127
+ self.a_gate_proj = nn.Linear(hs, hs, bias=False)
128
+
129
+ if self.qv_variant == 'dynamic_qa_conditioned':
130
+ # cheap Q+A: shared Linear(128->64) applied to each head/token
131
+ self.qa_gate_proj = nn.Linear(hs * 2, hs, bias=False)
132
+
133
+ if self.qv_variant == 'dynamic_qa_conditioned_headspecific':
134
+ # true head-specific Q+A: separate 128->64 matrix for each head
135
+ self.qa_gate_proj_headspecific = nn.Parameter(torch.empty(config.n_head, hs * 2, hs))
136
+ nn.init.normal_(self.qa_gate_proj_headspecific, mean=0.0, std=0.02)
137
+
138
+ if self.qv_variant == 'dynamic_qa_conditioned_mlp128':
139
+ # main Q+A MLP: per-head MLP(128->128->64)
140
+ self.qa_gate_fc1 = nn.Linear(hs * 2, 128, bias=False)
141
+ self.qa_gate_fc2 = nn.Linear(128, hs, bias=False)
142
+
143
+ if self.qv_variant == 'dynamic_qa_headshared_elementwise':
144
+ # legacy compatibility variant: shared Linear(128->64) applied per head/token
145
+ self.qa_gate_headshared = nn.Linear(hs * 2, hs, bias=False)
146
+
147
+ if self.qv_variant == 'dynamic_qa_bilinear_diag':
148
+ # learned elementwise interaction on q*y; 64 params per layer
149
+ self.qa_bilinear_diag = nn.Parameter(torch.ones(hs))
150
+
151
+ if self.qv_variant == 'dynamic_qa_conditioned_normed':
152
+ # RMSNorm q and y separately before shared Linear(128->64)
153
+ self.qa_q_norm = RMSNorm(hs)
154
+ self.qa_y_norm = RMSNorm(hs)
155
+ self.qa_gate_proj_normed = nn.Linear(hs * 2, hs, bias=False)
156
+
157
+ if self.qv_variant == 'dynamic_qa_conditioned_lowrank16':
158
+ # shared low-rank Q+A mixer: 128->16->64 per head/token
159
+ self.qa_gate_lowrank_fc1 = nn.Linear(hs * 2, 16, bias=False)
160
+ self.qa_gate_lowrank_fc2 = nn.Linear(16, hs, bias=False)
161
+
162
+ # --------------------------------------------------
163
+ # Static / normalization controls
164
+ # --------------------------------------------------
165
+ if self.qv_variant == 'post_rmsnorm_y':
166
+ self.post_attn_norm = RMSNorm(hs)
167
+ elif self.qv_variant == 'static_gate':
168
+ self.static_gate_param = nn.Parameter(torch.zeros(config.n_embd))
169
+ elif self.qv_variant == 'static_gate_prehead':
170
+ self.static_gate_prehead_param = nn.Parameter(torch.zeros(config.n_head, hs))
171
+
172
+ self.q_norm = None
173
+ self.v_norm = None
174
+ self.qv_modulator = None
175
+ variant = getattr(config, 'qv_variant', 'none')
176
+ if variant in ['qvnorm']:
177
+ self.q_norm = RMSNorm(hs)
178
+ if variant in ['vnorm', 'qvnorm']:
179
+ self.v_norm = RMSNorm(hs)
180
+
181
+ self.attn_dropout = nn.Dropout(config.dropout)
182
+ self.resid_dropout = nn.Dropout(config.dropout)
183
+ self.n_head = config.n_head
184
+ self.n_embd = config.n_embd
185
+ self.dropout = config.dropout
186
+ self.flash = hasattr(torch.nn.functional, 'scaled_dot_product_attention')
187
+ if not self.flash:
188
+ self.register_buffer(
189
+ "bias",
190
+ torch.tril(torch.ones(config.block_size, config.block_size)).view(
191
+ 1, 1, config.block_size, config.block_size
192
+ ),
193
+ )
194
+
195
+ def forward(self, x, x_prenorm=None):
196
+ B, T, C = x.size()
197
+ hs = C // self.n_head
198
+
199
+ q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
200
+ k = k.view(B, T, self.n_head, hs).transpose(1, 2)
201
+ q = q.view(B, T, self.n_head, hs).transpose(1, 2)
202
+ v = v.view(B, T, self.n_head, hs).transpose(1, 2)
203
+
204
+ if self.q_norm is not None:
205
+ q = self.q_norm(q)
206
+ if self.v_norm is not None:
207
+ v = self.v_norm(v)
208
+
209
+ if self.flash:
210
+ y = torch.nn.functional.scaled_dot_product_attention(
211
+ q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0
212
+ )
213
+ else:
214
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
215
+ att = att.masked_fill(self.bias[:, :, :T, :T] == 0, float('-inf'))
216
+ att = F.softmax(att, dim=-1)
217
+ att = self.attn_dropout(att)
218
+ y = att @ v
219
+
220
+ # --------------------------------------------------
221
+ # Gate application — y shape: (B, n_head, T, hs)
222
+ # --------------------------------------------------
223
+
224
+ # Q-conditioned gates
225
+ if self.qv_variant in ['dynamic', 'dynamic_swiglu']:
226
+ gate_logit = self.qv_gate_proj(q)
227
+ gate = torch.sigmoid(gate_logit) if self.qv_variant == 'dynamic' else gate_logit * torch.sigmoid(gate_logit)
228
+ y = y * gate
229
+
230
+ elif self.qv_variant == 'dynamic_qconditioned_mlp128':
231
+ gate_hidden = F.silu(self.q_gate_fc1(q))
232
+ gate_logit = self.q_gate_fc2(gate_hidden)
233
+ gate = torch.sigmoid(gate_logit)
234
+ y = y * gate
235
+
236
+ elif self.qv_variant == 'dynamic_qconditioned_normed':
237
+ qn = self.q_gate_norm(q)
238
+ gate_logit = self.q_gate_proj_normed(qn)
239
+ gate = torch.sigmoid(gate_logit)
240
+ y = y * gate
241
+
242
+ elif self.qv_variant == 'dynamic_qconditioned_mlp128_normed':
243
+ qn = self.q_gate_norm_mlp128(q)
244
+ gate_hidden = F.silu(self.q_gate_fc1_normed(qn))
245
+ gate_logit = self.q_gate_fc2_normed(gate_hidden)
246
+ gate = torch.sigmoid(gate_logit)
247
+ y = y * gate
248
+
249
+ elif self.qv_variant == 'dynamic_qconditioned_mlp192':
250
+ gate_hidden = F.silu(self.q_gate_fc1_192(q))
251
+ gate_logit = self.q_gate_fc2_192(gate_hidden)
252
+ gate = torch.sigmoid(gate_logit)
253
+ y = y * gate
254
+
255
+ elif self.qv_variant == 'dynamic_qconditioned_fullwidth':
256
+ q_full = q.transpose(1, 2).contiguous().view(B, T, C)
257
+ gate_logit = self.q_gate_proj_full(q_full)
258
+ gate = torch.sigmoid(gate_logit)
259
+ gate = gate.view(B, T, self.n_head, hs).transpose(1, 2)
260
+ y = y * gate
261
+
262
+ elif self.qv_variant == 'dynamic_qconditioned_fullwidth_headspecific':
263
+ q_full = q.transpose(1, 2).contiguous().view(B, T, C)
264
+ gate_logit = torch.einsum('btc,hce->bhte', q_full, self.q_gate_proj_full_headspecific)
265
+ gate = torch.sigmoid(gate_logit)
266
+ y = y * gate
267
+
268
+ elif self.qv_variant == 'dynamic_q_headshared_elementwise':
269
+ gate_logit = self.q_gate_headshared(q)
270
+ gate = torch.sigmoid(gate_logit)
271
+ y = y * gate
272
+
273
+ # Static / normalization controls
274
+ elif self.qv_variant == 'post_rmsnorm_y':
275
+ y = self.post_attn_norm(y)
276
+
277
+ elif self.qv_variant == 'static_gate':
278
+ gate = torch.sigmoid(self.static_gate_param)
279
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
280
+ y = y * gate[None, None, :]
281
+
282
+ elif self.qv_variant == 'static_gate_prehead':
283
+ gate = torch.sigmoid(self.static_gate_prehead_param)
284
+ y = y * gate[None, :, None, :]
285
+
286
+ # X-conditioned gates
287
+ elif self.qv_variant == 'dynamic_xconditioned_g1':
288
+ gate_logit = self.x_gate_proj(x_prenorm)
289
+ gate = torch.sigmoid(gate_logit)
290
+ gate = gate.view(B, T, self.n_head, hs).transpose(1, 2)
291
+ y = y * gate
292
+
293
+ elif self.qv_variant == 'dynamic_xconditioned_fullwidth_headspecific':
294
+ gate_logit = torch.einsum('btc,hce->bhte', x_prenorm, self.x_gate_proj_full_headspecific)
295
+ gate = torch.sigmoid(gate_logit)
296
+ y = y * gate
297
+
298
+ elif self.qv_variant == 'dynamic_xconditioned_bottleneck':
299
+ z = self.x_gate_bottleneck(x_prenorm)
300
+ gate = torch.sigmoid(z)
301
+ gate = gate.unsqueeze(1)
302
+ y = y * gate
303
+
304
+ elif self.qv_variant == 'dynamic_x_g1_headspecific_elementwise':
305
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
306
+ gate_logit = torch.einsum('bhtd,hde->bhte', xh, self.x_gate_headspecific_elementwise)
307
+ gate = torch.sigmoid(gate_logit)
308
+ y = y * gate
309
+
310
+ elif self.qv_variant == 'dynamic_x_g1_headspecific_headwise':
311
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
312
+ gate_logit = torch.einsum('bhtd,hde->bhte', xh, self.x_gate_headspecific_headwise)
313
+ gate = torch.sigmoid(gate_logit)
314
+ y = y * gate
315
+
316
+ elif self.qv_variant == 'dynamic_x_g1_headshared_elementwise':
317
+ xh = x_prenorm.view(B, T, self.n_head, hs).transpose(1, 2)
318
+ gate_logit = self.x_gate_headshared_elementwise(xh)
319
+ gate = torch.sigmoid(gate_logit)
320
+ y = y * gate
321
+
322
+ # Random / ablation gates — no learned params
323
+ elif self.qv_variant == 'dynamic_random_gate':
324
+ # Uniform random [0,1] gate — tests if location matters at all
325
+ gate = torch.rand_like(y)
326
+ y = y * gate
327
+
328
+ elif self.qv_variant == 'dynamic_ones_gate':
329
+ # Always 1.0 — pure identity, sanity check (should == baseline)
330
+ pass # y unchanged
331
+
332
+ elif self.qv_variant == 'dynamic_random_normal':
333
+ # Normal(0.5, 0.2) gate clamped to [0,1] — different noise shape
334
+ gate = torch.randn_like(y) * 0.2 + 0.5
335
+ gate = gate.clamp(0.0, 1.0)
336
+ y = y * gate
337
+
338
+ elif self.qv_variant == 'dynamic_bernoulli_gate':
339
+ # Binary gate: randomly zeros 50% of head dims — spicy dropout variant
340
+ gate = torch.bernoulli(torch.full_like(y, 0.5))
341
+ # Scale by 2.0 to preserve expected value (like standard dropout)
342
+ y = y * gate * 2.0
343
+
344
+ # A-only gate
345
+ elif self.qv_variant == 'dynamic_a_conditioned':
346
+ gate_logit = self.a_gate_proj(y)
347
+ gate = torch.sigmoid(gate_logit)
348
+ y = y * gate
349
+
350
+ # Q+A dual-signal gates
351
+ elif self.qv_variant == 'dynamic_qa_conditioned':
352
+ # cheap Q+A: cat(q, y) -> shared Linear(128->64) -> sigmoid
353
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
354
+ gate_logit = self.qa_gate_proj(gate_in)
355
+ gate = torch.sigmoid(gate_logit)
356
+ y = y * gate
357
+
358
+ elif self.qv_variant == 'dynamic_qa_conditioned_headspecific':
359
+ # true head-specific Q+A: per-head (128->64) -> sigmoid
360
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
361
+ gate_logit = torch.einsum('bhtd,hde->bhte', gate_in, self.qa_gate_proj_headspecific)
362
+ gate = torch.sigmoid(gate_logit)
363
+ y = y * gate
364
+
365
+ elif self.qv_variant == 'dynamic_qa_conditioned_mlp128':
366
+ # main Q+A MLP: cat(q, y) -> Linear(128->128) -> SiLU -> Linear(128->64) -> sigmoid
367
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
368
+ gate_hidden = F.silu(self.qa_gate_fc1(gate_in))
369
+ gate_logit = self.qa_gate_fc2(gate_hidden)
370
+ gate = torch.sigmoid(gate_logit)
371
+ y = y * gate
372
+
373
+ elif self.qv_variant == 'dynamic_qa_headshared_elementwise':
374
+ # legacy compatibility variant: cat(q, y) -> shared Linear(128->64) -> sigmoid
375
+ gate_in = torch.cat([q, y], dim=-1) # (B, n_head, T, 128)
376
+ gate_logit = self.qa_gate_headshared(gate_in)
377
+ gate = torch.sigmoid(gate_logit)
378
+ y = y * gate
379
+
380
+ elif self.qv_variant == 'dynamic_qa_bilinear_diag':
381
+ # learned diagonal bilinear interaction: sigmoid((q * y * w) / sqrt(d_head))
382
+ gate_logit = (q * y * self.qa_bilinear_diag[None, None, None, :]) / math.sqrt(hs)
383
+ gate = torch.sigmoid(gate_logit)
384
+ y = y * gate
385
+
386
+ elif self.qv_variant == 'dynamic_qa_conditioned_normed':
387
+ qn = self.qa_q_norm(q)
388
+ yn = self.qa_y_norm(y)
389
+ gate_in = torch.cat([qn, yn], dim=-1)
390
+ gate_logit = self.qa_gate_proj_normed(gate_in)
391
+ gate = torch.sigmoid(gate_logit)
392
+ y = y * gate
393
+
394
+ elif self.qv_variant == 'dynamic_qa_conditioned_lowrank16':
395
+ gate_in = torch.cat([q, y], dim=-1)
396
+ gate_hidden = F.silu(self.qa_gate_lowrank_fc1(gate_in))
397
+ gate_logit = self.qa_gate_lowrank_fc2(gate_hidden)
398
+ gate = torch.sigmoid(gate_logit)
399
+ y = y * gate
400
+
401
+ # Zero-parameter dot product gates — raw Q·y geometry
402
+ elif self.qv_variant == 'dynamic_dot_scalar':
403
+ # scalar gate: sigmoid(sum(q*y) / sqrt(d_head))
404
+ # one scalar per head per token, zero params
405
+ hs = q.shape[-1]
406
+ dot = (q * y).sum(dim=-1, keepdim=True) # (B, n_head, T, 1)
407
+ gate = torch.sigmoid(dot / math.sqrt(hs)) # scalar broadcast
408
+ y = y * gate
409
+
410
+ elif self.qv_variant == 'dynamic_dot_elementwise':
411
+ # elementwise gate: sigmoid(q*y / sqrt(d_head))
412
+ # same shape as cheap_qa gate but zero params
413
+ hs = q.shape[-1]
414
+ gate = torch.sigmoid((q * y) / math.sqrt(hs)) # (B, n_head, T, 64)
415
+ y = y * gate
416
+
417
+ if self.qv_variant != 'static_gate':
418
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
419
+
420
+ y = self.resid_dropout(self.c_proj(y))
421
+ return y
422
+
423
+ class MLP(nn.Module):
424
+ def __init__(self, config):
425
+ super().__init__()
426
+ self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
427
+ self.gelu = nn.GELU()
428
+ self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
429
+ self.dropout = nn.Dropout(config.dropout)
430
+ def forward(self, x):
431
+ x = self.c_fc(x)
432
+ x = self.gelu(x)
433
+ x = self.c_proj(x)
434
+ x = self.dropout(x)
435
+ return x
436
+
437
+ class Block(nn.Module):
438
+ def __init__(self, config):
439
+ super().__init__()
440
+ self.ln_1 = LayerNorm(config.n_embd, bias=config.bias)
441
+ self.attn = CausalSelfAttention(config)
442
+ self.ln_2 = LayerNorm(config.n_embd, bias=config.bias)
443
+ self.mlp = MLP(config)
444
+ def forward(self, x):
445
+ x_norm = self.ln_1(x)
446
+ attn_out = self.attn(x_norm, x_norm)
447
+ x = x + attn_out
448
+ x = x + self.mlp(self.ln_2(x))
449
+ return x
450
+
451
+ @dataclass
452
+ class GPTConfig:
453
+ block_size: int = 1024
454
+ vocab_size: int = 50304
455
+ n_layer: int = 12
456
+ n_head: int = 12
457
+ n_embd: int = 768
458
+ dropout: float = 0.0
459
+ bias: bool = True
460
+ qv_variant: str = 'none'
461
+
462
+ class GPT(nn.Module):
463
+ def __init__(self, config):
464
+ super().__init__()
465
+ assert config.vocab_size is not None
466
+ assert config.block_size is not None
467
+ self.config = config
468
+ self.transformer = nn.ModuleDict(dict(
469
+ wte = nn.Embedding(config.vocab_size, config.n_embd),
470
+ wpe = nn.Embedding(config.block_size, config.n_embd),
471
+ drop = nn.Dropout(config.dropout),
472
+ h = nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
473
+ ln_f = LayerNorm(config.n_embd, bias=config.bias),
474
+ ))
475
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
476
+ self.transformer.wte.weight = self.lm_head.weight
477
+ self.apply(self._init_weights)
478
+ for pn, p in self.named_parameters():
479
+ if pn.endswith('c_proj.weight'):
480
+ torch.nn.init.normal_(p, mean=0.0, std=0.02/math.sqrt(2 * config.n_layer))
481
+ print("number of parameters: %.2fM" % (self.get_num_params()/1e6,))
482
+
483
+ def get_num_params(self, non_embedding=True):
484
+ n_params = sum(p.numel() for p in self.parameters())
485
+ if non_embedding:
486
+ n_params -= self.transformer.wpe.weight.numel()
487
+ return n_params
488
+
489
+ def _init_weights(self, module):
490
+ if isinstance(module, nn.Linear):
491
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
492
+ if module.bias is not None:
493
+ torch.nn.init.zeros_(module.bias)
494
+ elif isinstance(module, nn.Embedding):
495
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
496
+
497
+ def forward(self, idx, targets=None):
498
+ device = idx.device
499
+ b, t = idx.size()
500
+ assert t <= self.config.block_size
501
+ pos = torch.arange(0, t, dtype=torch.long, device=device)
502
+ tok_emb = self.transformer.wte(idx)
503
+ pos_emb = self.transformer.wpe(pos)
504
+ x = self.transformer.drop(tok_emb + pos_emb)
505
+ for block in self.transformer.h:
506
+ x = block(x)
507
+ x = self.transformer.ln_f(x)
508
+ if targets is not None:
509
+ logits = self.lm_head(x)
510
+ loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)
511
+ else:
512
+ logits = self.lm_head(x[:, [-1], :])
513
+ loss = None
514
+ return logits, loss
515
+
516
+ def crop_block_size(self, block_size):
517
+ assert block_size <= self.config.block_size
518
+ self.config.block_size = block_size
519
+ self.transformer.wpe.weight = nn.Parameter(self.transformer.wpe.weight[:block_size])
520
+ for block in self.transformer.h:
521
+ if hasattr(block.attn, 'bias'):
522
+ block.attn.bias = block.attn.bias[:, :, :block_size, :block_size]
523
+
524
+ @classmethod
525
+ def from_pretrained(cls, model_type, override_args=None):
526
+ assert model_type in {'gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl'}
527
+ override_args = override_args or {}
528
+ assert all(k == 'dropout' for k in override_args)
529
+ from transformers import GPT2LMHeadModel
530
+ print("loading weights from pretrained gpt: %s" % model_type)
531
+ config_args = {
532
+ 'gpt2': dict(n_layer=12, n_head=12, n_embd=768),
533
+ 'gpt2-medium': dict(n_layer=24, n_head=16, n_embd=1024),
534
+ 'gpt2-large': dict(n_layer=36, n_head=20, n_embd=1280),
535
+ 'gpt2-xl': dict(n_layer=48, n_head=25, n_embd=1600),
536
+ }[model_type]
537
+ config_args['vocab_size'] = 50257
538
+ config_args['block_size'] = 1024
539
+ config_args['bias'] = True
540
+ if 'dropout' in override_args:
541
+ config_args['dropout'] = override_args['dropout']
542
+ config = GPTConfig(**config_args)
543
+ model = GPT(config)
544
+ sd = model.state_dict()
545
+ sd_keys = [k for k in sd.keys() if not k.endswith('.attn.bias')]
546
+ model_hf = GPT2LMHeadModel.from_pretrained(model_type)
547
+ sd_hf = model_hf.state_dict()
548
+ sd_keys_hf = [k for k in sd_hf.keys() if not k.endswith('.attn.masked_bias') and not k.endswith('.attn.bias')]
549
+ transposed = ['attn.c_attn.weight', 'attn.c_proj.weight', 'mlp.c_fc.weight', 'mlp.c_proj.weight']
550
+ assert len(sd_keys_hf) == len(sd_keys)
551
+ for k in sd_keys_hf:
552
+ if any(k.endswith(w) for w in transposed):
553
+ assert sd_hf[k].shape[::-1] == sd[k].shape
554
+ with torch.no_grad():
555
+ sd[k].copy_(sd_hf[k].t())
556
+ else:
557
+ assert sd_hf[k].shape == sd[k].shape
558
+ with torch.no_grad():
559
+ sd[k].copy_(sd_hf[k])
560
+ return model
561
+
562
+ def configure_optimizers(self, weight_decay, learning_rate, betas, device_type):
563
+ param_dict = {pn: p for pn, p in self.named_parameters() if p.requires_grad}
564
+ decay_params = [p for n, p in param_dict.items() if p.dim() >= 2]
565
+ nodecay_params = [p for n, p in param_dict.items() if p.dim() < 2]
566
+ optim_groups = [
567
+ {'params': decay_params, 'weight_decay': weight_decay},
568
+ {'params': nodecay_params, 'weight_decay': 0.0}
569
+ ]
570
+ num_decay_params = sum(p.numel() for p in decay_params)
571
+ num_nodecay_params = sum(p.numel() for p in nodecay_params)
572
+ print(f"num decayed parameter tensors: {len(decay_params)}, with {num_decay_params:,} parameters")
573
+ print(f"num non-decayed parameter tensors: {len(nodecay_params)}, with {num_nodecay_params:,} parameters")
574
+ fused_available = 'fused' in inspect.signature(torch.optim.AdamW).parameters
575
+ use_fused = fused_available and device_type == 'cuda'
576
+ optimizer = torch.optim.AdamW(optim_groups, lr=learning_rate, betas=betas, fused=use_fused)
577
+ print(f"using fused AdamW: {use_fused}")
578
+ return optimizer
579
+
580
+ def estimate_mfu(self, fwdbwd_per_iter, dt):
581
+ N = self.get_num_params()
582
+ cfg = self.config
583
+ L, H, Q, T = cfg.n_layer, cfg.n_head, cfg.n_embd // cfg.n_head, cfg.block_size
584
+ flops_per_token = 6 * N + 12 * L * H * Q * T
585
+ flops_per_fwdbwd = flops_per_token * T
586
+ flops_per_iter = flops_per_fwdbwd * fwdbwd_per_iter
587
+ flops_achieved = flops_per_iter * (1.0 / dt)
588
+ flops_promised = 312e12
589
+ mfu = flops_achieved / flops_promised
590
+ return mfu
591
+
592
+ @torch.no_grad()
593
+ def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
594
+ for _ in range(max_new_tokens):
595
+ idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
596
+ logits, _ = self(idx_cond)
597
+ logits = logits[:, -1, :] / temperature
598
+ if top_k is not None:
599
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
600
+ logits[logits < v[:, [-1]]] = -float('Inf')
601
+ probs = F.softmax(logits, dim=-1)
602
+ idx_next = torch.multinomial(probs, num_samples=1)
603
+ idx = torch.cat((idx, idx_next), dim=1)
604
+ return idx
code/train_canon_HEAD.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training script with qv_variant choices extended for Phase 6 Q+A variants (normed + lowrank).
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import math
8
+ import pickle
9
+ import argparse
10
+ from contextlib import nullcontext
11
+
12
+ import numpy as np
13
+ import torch
14
+ from torch.nn.parallel import DistributedDataParallel as DDP
15
+ from torch.distributed import init_process_group, destroy_process_group
16
+
17
+ from model import GPTConfig, GPT
18
+
19
+ out_dir = 'out-shakespeare-char'
20
+ eval_interval = 250
21
+ log_interval = 10
22
+ eval_iters = 200
23
+ eval_only = False
24
+ always_save_checkpoint = False
25
+ wandb_log = False
26
+ wandb_project = 'shakespeare-char'
27
+ wandb_run_name = 'mini-gpt'
28
+ dataset = 'shakespeare_char'
29
+ gradient_accumulation_steps = 1
30
+ batch_size = 64
31
+ block_size = 256
32
+ n_layer = 6
33
+ n_head = 6
34
+ n_embd = 384
35
+ dropout = 0.2
36
+ bias = False
37
+ learning_rate = 1e-3
38
+ max_iters = 5000
39
+ lr_decay_iters = 5000
40
+ min_lr = 1e-4
41
+ beta1 = 0.9
42
+ beta2 = 0.99
43
+ warmup_iters = 100
44
+ device = 'cuda'
45
+ dtype = 'bfloat16' if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else 'float16'
46
+ compile = False
47
+
48
+ config_keys = [k for k, v in globals().items() if not k.startswith('_') and isinstance(v, (int, float, bool, str))]
49
+ parser = argparse.ArgumentParser(description='Train a mini-GPT')
50
+ parser.add_argument('--out_dir', type=str, default=out_dir)
51
+ parser.add_argument('--eval_interval', type=int, default=eval_interval)
52
+ parser.add_argument('--log_interval', type=int, default=log_interval)
53
+ parser.add_argument('--eval_iters', type=int, default=eval_iters)
54
+ parser.add_argument('--eval_only', action='store_true')
55
+ parser.add_argument('--always_save_checkpoint', action='store_true')
56
+ parser.add_argument('--wandb_log', action='store_true')
57
+ parser.add_argument('--wandb_project', type=str, default=wandb_project)
58
+ parser.add_argument('--wandb_run_name', type=str, default=wandb_run_name)
59
+ parser.add_argument('--dataset', type=str, default=dataset)
60
+ parser.add_argument('--gradient_accumulation_steps', type=int, default=gradient_accumulation_steps)
61
+ parser.add_argument('--batch_size', type=int, default=batch_size)
62
+ parser.add_argument('--block_size', type=int, default=block_size)
63
+ parser.add_argument('--n_layer', type=int, default=n_layer)
64
+ parser.add_argument('--n_head', type=int, default=n_head)
65
+ parser.add_argument('--n_embd', type=int, default=n_embd)
66
+ parser.add_argument('--dropout', type=float, default=dropout)
67
+ parser.add_argument('--bias', action='store_true')
68
+ parser.add_argument('--learning_rate', type=float, default=learning_rate)
69
+ parser.add_argument('--max_iters', type=int, default=max_iters)
70
+ parser.add_argument('--lr_decay_iters', type=int, default=lr_decay_iters)
71
+ parser.add_argument('--min_lr', type=float, default=min_lr)
72
+ parser.add_argument('--beta1', type=float, default=beta1)
73
+ parser.add_argument('--beta2', type=float, default=beta2)
74
+ parser.add_argument('--warmup_iters', type=int, default=warmup_iters)
75
+ parser.add_argument('--device', type=str, default=device)
76
+ parser.add_argument('--dtype', type=str, default=dtype)
77
+ parser.add_argument('--compile', action='store_true')
78
+ parser.add_argument('--seed', type=int, default=1337, help='random seed')
79
+ parser.add_argument('--qv_variant', type=str, default='none',
80
+ choices=[
81
+ # baseline
82
+ 'none',
83
+ # normalization variants
84
+ 'vnorm',
85
+ 'qvnorm',
86
+ # static gates
87
+ 'static_gate',
88
+ 'static_gate_prehead',
89
+ # post-attn normalization
90
+ 'post_rmsnorm_y',
91
+ # Q-conditioned gates
92
+ 'dynamic',
93
+ 'dynamic_swiglu',
94
+ 'dynamic_qconditioned_mlp128',
95
+ 'dynamic_qconditioned_mlp192',
96
+ 'dynamic_qconditioned_fullwidth',
97
+ 'dynamic_qconditioned_fullwidth_headspecific',
98
+ 'dynamic_q_headshared_elementwise',
99
+ # X-conditioned gates
100
+ 'dynamic_xconditioned_g1',
101
+ 'dynamic_xconditioned_fullwidth_headspecific',
102
+ 'dynamic_xconditioned_bottleneck',
103
+ 'dynamic_x_g1_headspecific_elementwise',
104
+ 'dynamic_x_g1_headspecific_headwise',
105
+ 'dynamic_x_g1_headshared_elementwise',
106
+ # Random / ablation gates
107
+ 'dynamic_random_gate',
108
+ 'dynamic_ones_gate',
109
+ 'dynamic_random_normal',
110
+ 'dynamic_bernoulli_gate',
111
+ # Zero-parameter dot product gates
112
+ 'dynamic_dot_scalar',
113
+ 'dynamic_dot_elementwise',
114
+ # A-only gate
115
+ 'dynamic_a_conditioned',
116
+ # Q+A dual-signal gates
117
+ 'dynamic_qa_conditioned',
118
+ 'dynamic_qa_conditioned_headspecific',
119
+ 'dynamic_qa_conditioned_mlp128',
120
+ 'dynamic_qa_headshared_elementwise',
121
+ 'dynamic_qa_bilinear_diag',
122
+ 'dynamic_qa_conditioned_normed',
123
+ 'dynamic_qa_conditioned_normed_yonly',
124
+ 'dynamic_qa_conditioned_normed_qonly',
125
+ 'dynamic_qa_conditioned_softq',
126
+ 'dynamic_qa_conditioned_softqa',
127
+ 'dynamic_qa_conditioned_softqa_perlayer',
128
+ 'dynamic_qa_conditioned_softqa_perdim',
129
+ 'dynamic_qa_conditioned_softqa_perdim_informed',
130
+ 'dynamic_qa_conditioned_softqa_perdim_free',
131
+ 'dynamic_qa_conditioned_lowrank16',
132
+ # legacy
133
+ 'dynamic_postconcat_matched',
134
+ ],
135
+ help='QV experiment variant')
136
+
137
+ args = parser.parse_args()
138
+ for k in config_keys:
139
+ if hasattr(args, k):
140
+ globals()[k] = getattr(args, k)
141
+ config = {k: globals()[k] for k in config_keys}
142
+
143
+ ddp = int(os.environ.get('RANK', -1)) != -1
144
+ if ddp:
145
+ init_process_group(backend='nccl')
146
+ ddp_rank = int(os.environ['RANK'])
147
+ ddp_local_rank = int(os.environ['LOCAL_RANK'])
148
+ ddp_world_size = int(os.environ['WORLD_SIZE'])
149
+ device = f'cuda:{ddp_local_rank}'
150
+ torch.cuda.set_device(device)
151
+ master_process = ddp_rank == 0
152
+ seed_offset = ddp_rank
153
+ gradient_accumulation_steps //= ddp_world_size
154
+ else:
155
+ master_process = True
156
+ seed_offset = 0
157
+ ddp_world_size = 1
158
+
159
+ tokens_per_iter = gradient_accumulation_steps * ddp_world_size * batch_size * block_size
160
+ print(f"tokens per iteration will be: {tokens_per_iter:,}")
161
+
162
+ if master_process:
163
+ os.makedirs(out_dir, exist_ok=True)
164
+
165
+ torch.manual_seed(args.seed + seed_offset)
166
+ torch.backends.cuda.matmul.allow_tf32 = True
167
+ torch.backends.cudnn.allow_tf32 = True
168
+
169
+ device_type = 'cuda' if 'cuda' in device else 'cpu'
170
+ ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
171
+ ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)
172
+
173
+ data_dir = os.path.join('data', dataset)
174
+
175
+ # OPEN MEMMAPS ONCE, REUSE FOREVER
176
+ train_data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r')
177
+ val_data = np.memmap(os.path.join(data_dir, 'val.bin'), dtype=np.uint16, mode='r')
178
+
179
+ def get_batch(split):
180
+ data = train_data if split == 'train' else val_data
181
+ ix = torch.randint(len(data) - block_size, (batch_size,))
182
+ x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix])
183
+ y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix])
184
+ if device_type == 'cuda':
185
+ x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True)
186
+ else:
187
+ x, y = x.to(device), y.to(device)
188
+ return x, y
189
+
190
+ iter_num = 0
191
+ best_val_loss = 1e9
192
+
193
+ meta_path = os.path.join(data_dir, 'meta.pkl')
194
+ meta_vocab_size = None
195
+ if os.path.exists(meta_path):
196
+ with open(meta_path, 'rb') as f:
197
+ meta = pickle.load(f)
198
+ meta_vocab_size = meta['vocab_size']
199
+ print(f"found vocab_size = {meta_vocab_size} (inside {meta_path})")
200
+
201
+ model_args = dict(n_layer=n_layer, n_head=n_head, n_embd=n_embd, block_size=block_size,
202
+ bias=bias, vocab_size=None, dropout=dropout)
203
+
204
+ if meta_vocab_size is None:
205
+ print("defaulting to vocab_size of GPT-2 to 50304 (50257 rounded up for efficiency)")
206
+ model_args['vocab_size'] = meta_vocab_size if meta_vocab_size is not None else 50304
207
+
208
+ gptconf = GPTConfig(**model_args)
209
+ gptconf.qv_variant = args.qv_variant
210
+ model = GPT(gptconf)
211
+ model.to(device)
212
+
213
+ scaler = torch.cuda.amp.GradScaler(enabled=(dtype == 'float16'))
214
+ optimizer = model.configure_optimizers(1e-1, learning_rate, (beta1, beta2), device_type)
215
+
216
+ if compile:
217
+ print("compiling the model... (takes a ~minute)")
218
+ model = torch.compile(model)
219
+
220
+ @torch.no_grad()
221
+ def estimate_loss():
222
+ out = {}
223
+ model.eval()
224
+ for split in ['train', 'val']:
225
+ losses = torch.zeros(eval_iters)
226
+ for k in range(eval_iters):
227
+ X, Y = get_batch(split)
228
+ with ctx:
229
+ logits, loss = model(X, Y)
230
+ losses[k] = loss.item()
231
+ out[split] = losses.mean()
232
+ model.train()
233
+ return out
234
+
235
+ def get_lr(it):
236
+ if it < warmup_iters:
237
+ return learning_rate * (it + 1) / (warmup_iters + 1)
238
+ if it > lr_decay_iters:
239
+ return min_lr
240
+ decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
241
+ coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
242
+ return min_lr + coeff * (learning_rate - min_lr)
243
+
244
+ if wandb_log and master_process:
245
+ import wandb
246
+ wandb.init(project=wandb_project, name=wandb_run_name, config=config)
247
+
248
+ X, Y = get_batch('train')
249
+ t0 = time.time()
250
+ local_iter_num = 0
251
+ raw_model = model.module if hasattr(model, 'module') else model
252
+ running_mfu = -1.0
253
+
254
+ while True:
255
+ lr = get_lr(iter_num)
256
+ for param_group in optimizer.param_groups:
257
+ param_group['lr'] = lr
258
+
259
+ if iter_num % eval_interval == 0 and master_process:
260
+ losses = estimate_loss()
261
+ print(f"step {iter_num}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
262
+ if losses['val'] < best_val_loss or always_save_checkpoint:
263
+ best_val_loss = losses['val']
264
+ if iter_num > 0:
265
+ checkpoint = {
266
+ 'model': raw_model.state_dict(),
267
+ 'optimizer': optimizer.state_dict(),
268
+ 'model_args': model_args,
269
+ 'iter_num': iter_num,
270
+ 'best_val_loss': best_val_loss,
271
+ 'config': config,
272
+ }
273
+ print(f"saving checkpoint to {out_dir}")
274
+ torch.save(checkpoint, os.path.join(out_dir, 'ckpt.pt'))
275
+
276
+ if iter_num == 0 and eval_only:
277
+ break
278
+
279
+ for micro_step in range(gradient_accumulation_steps):
280
+ with ctx:
281
+ logits, loss = model(X, Y)
282
+ loss = loss / gradient_accumulation_steps
283
+ X, Y = get_batch('train')
284
+ scaler.scale(loss).backward()
285
+
286
+ scaler.unscale_(optimizer)
287
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
288
+ scaler.step(optimizer)
289
+ scaler.update()
290
+ optimizer.zero_grad(set_to_none=True)
291
+
292
+ t1 = time.time()
293
+ dt = t1 - t0
294
+ t0 = t1
295
+ if iter_num % log_interval == 0 and master_process:
296
+ lossf = loss.item() * gradient_accumulation_steps
297
+ if local_iter_num >= 5:
298
+ mfu = raw_model.estimate_mfu(batch_size * gradient_accumulation_steps, dt)
299
+ running_mfu = mfu if running_mfu == -1.0 else 0.9 * running_mfu + 0.1 * mfu
300
+ print(f"iter {iter_num}: loss {lossf:.4f}, time {dt*1000:.2f}ms, mfu {running_mfu*100:.2f}%")
301
+ iter_num += 1
302
+ local_iter_num += 1
303
+
304
+ if iter_num > max_iters:
305
+ break
306
+
307
+ if ddp:
308
+ destroy_process_group()
code/train_wslbridge.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training script with qv_variant choices extended for Phase 7 normed Q-only variants.
3
+ """
4
+
5
+ import os
6
+ import time
7
+ import math
8
+ import pickle
9
+ import argparse
10
+ from contextlib import nullcontext
11
+
12
+ import numpy as np
13
+ import torch
14
+ from torch.nn.parallel import DistributedDataParallel as DDP
15
+ from torch.distributed import init_process_group, destroy_process_group
16
+
17
+ from model import GPTConfig, GPT
18
+
19
+ out_dir = 'out-shakespeare-char'
20
+ eval_interval = 250
21
+ log_interval = 10
22
+ eval_iters = 200
23
+ eval_only = False
24
+ always_save_checkpoint = False
25
+ wandb_log = False
26
+ wandb_project = 'shakespeare-char'
27
+ wandb_run_name = 'mini-gpt'
28
+ dataset = 'shakespeare_char'
29
+ gradient_accumulation_steps = 1
30
+ batch_size = 64
31
+ block_size = 256
32
+ n_layer = 6
33
+ n_head = 6
34
+ n_embd = 384
35
+ dropout = 0.2
36
+ bias = False
37
+ learning_rate = 1e-3
38
+ max_iters = 5000
39
+ lr_decay_iters = 5000
40
+ min_lr = 1e-4
41
+ beta1 = 0.9
42
+ beta2 = 0.99
43
+ warmup_iters = 100
44
+ device = 'cuda'
45
+ dtype = 'bfloat16' if torch.cuda.is_available() and torch.cuda.is_bf16_supported() else 'float16'
46
+ compile = False
47
+
48
+ config_keys = [k for k, v in globals().items() if not k.startswith('_') and isinstance(v, (int, float, bool, str))]
49
+ parser = argparse.ArgumentParser(description='Train a mini-GPT')
50
+ parser.add_argument('--out_dir', type=str, default=out_dir)
51
+ parser.add_argument('--eval_interval', type=int, default=eval_interval)
52
+ parser.add_argument('--log_interval', type=int, default=log_interval)
53
+ parser.add_argument('--eval_iters', type=int, default=eval_iters)
54
+ parser.add_argument('--eval_only', action='store_true')
55
+ parser.add_argument('--always_save_checkpoint', action='store_true')
56
+ parser.add_argument('--wandb_log', action='store_true')
57
+ parser.add_argument('--wandb_project', type=str, default=wandb_project)
58
+ parser.add_argument('--wandb_run_name', type=str, default=wandb_run_name)
59
+ parser.add_argument('--dataset', type=str, default=dataset)
60
+ parser.add_argument('--gradient_accumulation_steps', type=int, default=gradient_accumulation_steps)
61
+ parser.add_argument('--batch_size', type=int, default=batch_size)
62
+ parser.add_argument('--block_size', type=int, default=block_size)
63
+ parser.add_argument('--n_layer', type=int, default=n_layer)
64
+ parser.add_argument('--n_head', type=int, default=n_head)
65
+ parser.add_argument('--n_embd', type=int, default=n_embd)
66
+ parser.add_argument('--dropout', type=float, default=dropout)
67
+ parser.add_argument('--bias', action='store_true')
68
+ parser.add_argument('--learning_rate', type=float, default=learning_rate)
69
+ parser.add_argument('--max_iters', type=int, default=max_iters)
70
+ parser.add_argument('--lr_decay_iters', type=int, default=lr_decay_iters)
71
+ parser.add_argument('--min_lr', type=float, default=min_lr)
72
+ parser.add_argument('--beta1', type=float, default=beta1)
73
+ parser.add_argument('--beta2', type=float, default=beta2)
74
+ parser.add_argument('--warmup_iters', type=int, default=warmup_iters)
75
+ parser.add_argument('--device', type=str, default=device)
76
+ parser.add_argument('--dtype', type=str, default=dtype)
77
+ parser.add_argument('--compile', action='store_true')
78
+ parser.add_argument('--seed', type=int, default=1337, help='random seed')
79
+ parser.add_argument('--qv_variant', type=str, default='none',
80
+ choices=[
81
+ # baseline
82
+ 'none',
83
+ # normalization variants
84
+ 'vnorm',
85
+ 'qvnorm',
86
+ # static gates
87
+ 'static_gate',
88
+ 'static_gate_prehead',
89
+ # post-attn normalization
90
+ 'post_rmsnorm_y',
91
+ # Q-conditioned gates
92
+ 'dynamic',
93
+ 'dynamic_swiglu',
94
+ 'dynamic_qconditioned_mlp128',
95
+ 'dynamic_qconditioned_normed',
96
+ 'dynamic_qconditioned_mlp128_normed',
97
+ 'dynamic_qconditioned_mlp192',
98
+ 'dynamic_qconditioned_fullwidth',
99
+ 'dynamic_qconditioned_fullwidth_headspecific',
100
+ 'dynamic_q_headshared_elementwise',
101
+ # X-conditioned gates
102
+ 'dynamic_xconditioned_g1',
103
+ 'dynamic_xconditioned_fullwidth_headspecific',
104
+ 'dynamic_xconditioned_bottleneck',
105
+ 'dynamic_x_g1_headspecific_elementwise',
106
+ 'dynamic_x_g1_headspecific_headwise',
107
+ 'dynamic_x_g1_headshared_elementwise',
108
+ # Random / ablation gates
109
+ 'dynamic_random_gate',
110
+ 'dynamic_ones_gate',
111
+ 'dynamic_random_normal',
112
+ 'dynamic_bernoulli_gate',
113
+ # Zero-parameter dot product gates
114
+ 'dynamic_dot_scalar',
115
+ 'dynamic_dot_elementwise',
116
+ # A-only gate
117
+ 'dynamic_a_conditioned',
118
+ # Q+A dual-signal gates
119
+ 'dynamic_qa_conditioned',
120
+ 'dynamic_qa_conditioned_headspecific',
121
+ 'dynamic_qa_conditioned_mlp128',
122
+ 'dynamic_qa_headshared_elementwise',
123
+ 'dynamic_qa_bilinear_diag',
124
+ 'dynamic_qa_conditioned_normed',
125
+ 'dynamic_qa_conditioned_lowrank16',
126
+ # legacy
127
+ 'dynamic_postconcat_matched',
128
+ ],
129
+ help='QV experiment variant')
130
+
131
+ args = parser.parse_args()
132
+ for k in config_keys:
133
+ if hasattr(args, k):
134
+ globals()[k] = getattr(args, k)
135
+ config = {k: globals()[k] for k in config_keys}
136
+
137
+ ddp = int(os.environ.get('RANK', -1)) != -1
138
+ if ddp:
139
+ init_process_group(backend='nccl')
140
+ ddp_rank = int(os.environ['RANK'])
141
+ ddp_local_rank = int(os.environ['LOCAL_RANK'])
142
+ ddp_world_size = int(os.environ['WORLD_SIZE'])
143
+ device = f'cuda:{ddp_local_rank}'
144
+ torch.cuda.set_device(device)
145
+ master_process = ddp_rank == 0
146
+ seed_offset = ddp_rank
147
+ gradient_accumulation_steps //= ddp_world_size
148
+ else:
149
+ master_process = True
150
+ seed_offset = 0
151
+ ddp_world_size = 1
152
+
153
+ tokens_per_iter = gradient_accumulation_steps * ddp_world_size * batch_size * block_size
154
+ print(f"tokens per iteration will be: {tokens_per_iter:,}")
155
+
156
+ if master_process:
157
+ os.makedirs(out_dir, exist_ok=True)
158
+
159
+ torch.manual_seed(args.seed + seed_offset)
160
+ torch.backends.cuda.matmul.allow_tf32 = True
161
+ torch.backends.cudnn.allow_tf32 = True
162
+
163
+ device_type = 'cuda' if 'cuda' in device else 'cpu'
164
+ ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[dtype]
165
+ ctx = nullcontext() if device_type == 'cpu' else torch.amp.autocast(device_type=device_type, dtype=ptdtype)
166
+
167
+ data_dir = os.path.join('data', dataset)
168
+
169
+ # OPEN MEMMAPS ONCE, REUSE FOREVER
170
+ train_data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r')
171
+ val_data = np.memmap(os.path.join(data_dir, 'val.bin'), dtype=np.uint16, mode='r')
172
+
173
+ def get_batch(split):
174
+ data = train_data if split == 'train' else val_data
175
+ ix = torch.randint(len(data) - block_size, (batch_size,))
176
+ x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix])
177
+ y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix])
178
+ if device_type == 'cuda':
179
+ x, y = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True)
180
+ else:
181
+ x, y = x.to(device), y.to(device)
182
+ return x, y
183
+
184
+ iter_num = 0
185
+ best_val_loss = 1e9
186
+
187
+ meta_path = os.path.join(data_dir, 'meta.pkl')
188
+ meta_vocab_size = None
189
+ if os.path.exists(meta_path):
190
+ with open(meta_path, 'rb') as f:
191
+ meta = pickle.load(f)
192
+ meta_vocab_size = meta['vocab_size']
193
+ print(f"found vocab_size = {meta_vocab_size} (inside {meta_path})")
194
+
195
+ model_args = dict(n_layer=n_layer, n_head=n_head, n_embd=n_embd, block_size=block_size,
196
+ bias=bias, vocab_size=None, dropout=dropout)
197
+
198
+ if meta_vocab_size is None:
199
+ print("defaulting to vocab_size of GPT-2 to 50304 (50257 rounded up for efficiency)")
200
+ model_args['vocab_size'] = meta_vocab_size if meta_vocab_size is not None else 50304
201
+
202
+ gptconf = GPTConfig(**model_args)
203
+ gptconf.qv_variant = args.qv_variant
204
+ model = GPT(gptconf)
205
+ model.to(device)
206
+
207
+ scaler = torch.cuda.amp.GradScaler(enabled=(dtype == 'float16'))
208
+ optimizer = model.configure_optimizers(1e-1, learning_rate, (beta1, beta2), device_type)
209
+
210
+ if compile:
211
+ print("compiling the model... (takes a ~minute)")
212
+ model = torch.compile(model)
213
+
214
+ @torch.no_grad()
215
+ def estimate_loss():
216
+ out = {}
217
+ model.eval()
218
+ for split in ['train', 'val']:
219
+ losses = torch.zeros(eval_iters)
220
+ for k in range(eval_iters):
221
+ X, Y = get_batch(split)
222
+ with ctx:
223
+ logits, loss = model(X, Y)
224
+ losses[k] = loss.item()
225
+ out[split] = losses.mean()
226
+ model.train()
227
+ return out
228
+
229
+ def get_lr(it):
230
+ if it < warmup_iters:
231
+ return learning_rate * (it + 1) / (warmup_iters + 1)
232
+ if it > lr_decay_iters:
233
+ return min_lr
234
+ decay_ratio = (it - warmup_iters) / (lr_decay_iters - warmup_iters)
235
+ coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
236
+ return min_lr + coeff * (learning_rate - min_lr)
237
+
238
+ if wandb_log and master_process:
239
+ import wandb
240
+ wandb.init(project=wandb_project, name=wandb_run_name, config=config)
241
+
242
+ X, Y = get_batch('train')
243
+ t0 = time.time()
244
+ local_iter_num = 0
245
+ raw_model = model.module if hasattr(model, 'module') else model
246
+ running_mfu = -1.0
247
+
248
+ while True:
249
+ lr = get_lr(iter_num)
250
+ for param_group in optimizer.param_groups:
251
+ param_group['lr'] = lr
252
+
253
+ if iter_num % eval_interval == 0 and master_process:
254
+ losses = estimate_loss()
255
+ print(f"step {iter_num}: train loss {losses['train']:.4f}, val loss {losses['val']:.4f}")
256
+ if losses['val'] < best_val_loss or always_save_checkpoint:
257
+ best_val_loss = losses['val']
258
+ if iter_num > 0:
259
+ checkpoint = {
260
+ 'model': raw_model.state_dict(),
261
+ 'optimizer': optimizer.state_dict(),
262
+ 'model_args': model_args,
263
+ 'iter_num': iter_num,
264
+ 'best_val_loss': best_val_loss,
265
+ 'config': config,
266
+ }
267
+ print(f"saving checkpoint to {out_dir}")
268
+ torch.save(checkpoint, os.path.join(out_dir, 'ckpt.pt'))
269
+
270
+ if iter_num == 0 and eval_only:
271
+ break
272
+
273
+ for micro_step in range(gradient_accumulation_steps):
274
+ with ctx:
275
+ logits, loss = model(X, Y)
276
+ loss = loss / gradient_accumulation_steps
277
+ X, Y = get_batch('train')
278
+ scaler.scale(loss).backward()
279
+
280
+ scaler.unscale_(optimizer)
281
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
282
+ scaler.step(optimizer)
283
+ scaler.update()
284
+ optimizer.zero_grad(set_to_none=True)
285
+
286
+ t1 = time.time()
287
+ dt = t1 - t0
288
+ t0 = t1
289
+ if iter_num % log_interval == 0 and master_process:
290
+ lossf = loss.item() * gradient_accumulation_steps
291
+ if local_iter_num >= 5:
292
+ mfu = raw_model.estimate_mfu(batch_size * gradient_accumulation_steps, dt)
293
+ running_mfu = mfu if running_mfu == -1.0 else 0.9 * running_mfu + 0.1 * mfu
294
+ print(f"iter {iter_num}: loss {lossf:.4f}, time {dt*1000:.2f}ms, mfu {running_mfu*100:.2f}%")
295
+ iter_num += 1
296
+ local_iter_num += 1
297
+
298
+ if iter_num > max_iters:
299
+ break
300
+
301
+ if ddp:
302
+ destroy_process_group()