Soham Jain commited on
Commit
fa2fdd8
·
verified ·
1 Parent(s): 428bfdc

Initial commit of GPT-2 46M SwiGLU Dense baseline model architecture and configs

Browse files
configuration_gpt2.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+ class GPT2Config(PretrainedConfig):
4
+ model_type = "gpt2"
5
+
6
+ def __init__(
7
+ self,
8
+ vocab_size=16384,
9
+ n_positions=514,
10
+ n_embd=512,
11
+ n_layer=12,
12
+ n_head=8,
13
+ n_inner=1360,
14
+ activation_function="swiglu",
15
+ resid_pdrop=0.1,
16
+ embd_pdrop=0.1,
17
+ attn_pdrop=0.1,
18
+ layer_norm_epsilon=1e-5,
19
+ initializer_range=0.02,
20
+ bos_token_id=0,
21
+ eos_token_id=0,
22
+ **kwargs
23
+ ):
24
+ super().__init__(bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs)
25
+ self.vocab_size = vocab_size
26
+ self.n_positions = n_positions
27
+ self.n_embd = n_embd
28
+ self.n_layer = n_layer
29
+ self.n_head = n_head
30
+ self.n_inner = n_inner
31
+ self.activation_function = activation_function
32
+ self.resid_pdrop = resid_pdrop
33
+ self.embd_pdrop = embd_pdrop
34
+ self.attn_pdrop = attn_pdrop
35
+ self.layer_norm_epsilon = layer_norm_epsilon
36
+ self.initializer_range = initializer_range
generate_plots_and_report.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import math
4
+ import torch
5
+ import argparse
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(description="Generate training reports, learning curves, and router plots.")
9
+ parser.add_argument("--model-dir", type=str, default="./checkpoints/xpertgpt_fresh", help="Path to checkpoints directory")
10
+ args = parser.parse_args()
11
+
12
+ print("=== 1. PARAMETER COUNT & MODEL METRICS REPORT ===")
13
+ model_path = os.path.join(args.model_dir, "main")
14
+ weight_file = os.path.join(model_path, "pytorch_model.bin")
15
+
16
+ if os.path.exists(weight_file):
17
+ try:
18
+ state_dict = torch.load(weight_file, map_location="cpu")
19
+ total_params = 0
20
+ non_emb_params = 0
21
+ for name, param in state_dict.items():
22
+ size = param.numel()
23
+ total_params += size
24
+ # Exclude embedding and head tie
25
+ if "wte.weight" not in name and "lm_head.weight" not in name:
26
+ non_emb_params += size
27
+ print(f"Model weights loaded successfully from: {weight_file}")
28
+ print(f"- Total Parameters (with tied embeddings): {total_params:,}")
29
+ print(f"- Non-Embedding Parameters: {non_emb_params:,}")
30
+ except Exception as e:
31
+ print(f"Warning: Could not read parameter counts from weight file: {e}")
32
+ # Fallback to config math
33
+ fallback_param_math()
34
+ else:
35
+ print(f"Info: Checked {weight_file} but weights not found. Using fallback config computation:")
36
+ fallback_param_math()
37
+
38
+ # Load validation metrics
39
+ val_file = "validation_metrics.json"
40
+ if not os.path.exists(val_file):
41
+ val_file = os.path.join(args.model_dir, "validation_metrics.json")
42
+
43
+ if os.path.exists(val_file):
44
+ with open(val_file, "r") as f:
45
+ val_logs = json.load(f)
46
+ if len(val_logs) > 0:
47
+ final_val = val_logs[-1]
48
+ print(f"- Final Validation Step: {final_val['step']}")
49
+ print(f"- Final Validation Loss: {final_val['val_loss']:.4f}")
50
+ print(f"- Final Validation Perplexity: {final_val['val_perplexity']:.2f}")
51
+ else:
52
+ print(f"Warning: {val_file} not found! Cannot report validation perplexity.")
53
+
54
+ print("\n=== 2. GENERATING LEARNING CURVES ===")
55
+ train_file = "training_metrics.json"
56
+ if not os.path.exists(train_file):
57
+ train_file = os.path.join(args.model_dir, "training_metrics.json")
58
+
59
+ try:
60
+ import matplotlib
61
+ matplotlib.use('Agg')
62
+ import matplotlib.pyplot as plt
63
+
64
+ has_plots = False
65
+
66
+ # Setup plot grid
67
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
68
+
69
+ if os.path.exists(train_file):
70
+ with open(train_file, "r") as f:
71
+ train_logs = json.load(f)
72
+ steps = [log["step"] for log in train_logs]
73
+ losses = [log["train_loss"] for log in train_logs]
74
+ ax1.plot(steps, losses, color="#ef4444", linewidth=1.5, label="Train Loss")
75
+ ax1.set_xlabel("Global Step")
76
+ ax1.set_ylabel("Cross Entropy Loss")
77
+ ax1.set_title("Training Loss Curve")
78
+ ax1.grid(True, linestyle="--", alpha=0.6)
79
+ ax1.legend()
80
+ has_plots = True
81
+ else:
82
+ ax1.text(0.5, 0.5, "training_metrics.json not found", ha="center")
83
+
84
+ if os.path.exists(val_file):
85
+ with open(val_file, "r") as f:
86
+ val_logs = json.load(f)
87
+ steps = [log["step"] for log in val_logs]
88
+ ppl = [log["val_perplexity"] for log in val_logs]
89
+ ax2.plot(steps, ppl, marker="o", color="#3b82f6", linewidth=2, label="Val Perplexity")
90
+ ax2.set_xlabel("Global Step")
91
+ ax2.set_ylabel("Perplexity")
92
+ ax2.set_title("Validation Perplexity Curve")
93
+ ax2.grid(True, linestyle="--", alpha=0.6)
94
+ ax2.legend()
95
+ has_plots = True
96
+ else:
97
+ ax2.text(0.5, 0.5, "validation_metrics.json not found", ha="center")
98
+
99
+ if has_plots:
100
+ plt.tight_layout()
101
+ plt.savefig("learning_curves.png", dpi=150)
102
+ plt.close()
103
+ print("Successfully saved learning curve diagrams to 'learning_curves.png'.")
104
+ except Exception as e:
105
+ print(f"Warning: Could not plot learning curves: {e}")
106
+
107
+ print("\n=== 3. ROUTER UTILIZATION ANALYSIS ===")
108
+ router_details_file = "router_details.json"
109
+ if not os.path.exists(router_details_file):
110
+ router_details_file = os.path.join(args.model_dir, "router_details.json")
111
+
112
+ if os.path.exists(router_details_file):
113
+ try:
114
+ with open(router_details_file, "r") as f:
115
+ records = json.load(f)
116
+
117
+ print(f"Loaded router mapping records containing {len(records)} logged steps.")
118
+
119
+ # Reconstruct selection count mapping
120
+ # records format: [{"step": s, "layer": l, "expert_assignments": [[tokens for exp1], [exp2], ...]}]
121
+ # Find the total number of unique token indices in each step
122
+ total_expert_counts = [0, 0, 0, 0, 0] # indices representing 0, 1, 2, 3, 4 experts
123
+
124
+ for record in records:
125
+ assignments = record["expert_assignments"]
126
+ # Number of experts is 4
127
+ num_blocks = len(assignments)
128
+ # Find maximum token index present in assignments to know the token range
129
+ max_token_idx = 0
130
+ for exp_list in assignments:
131
+ if len(exp_list) > 0:
132
+ max_token_idx = max(max_token_idx, max(exp_list))
133
+
134
+ n_tokens = max_token_idx + 1
135
+ token_counts = [0] * n_tokens
136
+ for exp_list in assignments:
137
+ for token_idx in exp_list:
138
+ token_counts[token_idx] += 1
139
+
140
+ for count in token_counts:
141
+ if count < 5:
142
+ total_expert_counts[count] += 1
143
+
144
+ total_tokens_sum = sum(total_expert_counts)
145
+ if total_tokens_sum > 0:
146
+ fractions = [total_expert_counts[i] / total_tokens_sum for i in range(5)]
147
+ else:
148
+ fractions = [0.0] * 5
149
+
150
+ fraction_no_expert = fractions[0]
151
+ fraction_multiple_experts = sum(fractions[2:])
152
+
153
+ report_text = f"""=== ROUTER UTILIZATION REPORT ===
154
+ Total tokens tracked: {total_tokens_sum:,}
155
+
156
+ Number of experts selected per token:
157
+ - 0 experts: {total_expert_counts[0]:,} tokens ({fractions[0]:.2%})
158
+ - 1 expert: {total_expert_counts[1]:,} tokens ({fractions[1]:.2%})
159
+ - 2 experts: {total_expert_counts[2]:,} tokens ({fractions[2]:.2%})
160
+ - 3 experts: {total_expert_counts[3]:,} tokens ({fractions[3]:.2%})
161
+ - 4 experts: {total_expert_counts[4]:,} tokens ({fractions[4]:.2%})
162
+
163
+ Summary metrics:
164
+ - Fraction of tokens receiving NO expert: {fraction_no_expert:.2%}
165
+ - Fraction of tokens receiving MULTIPLE experts (2, 3, or 4): {fraction_multiple_experts:.2%}
166
+ """
167
+ with open("router_report.txt", "w") as f:
168
+ f.write(report_text)
169
+ print(report_text)
170
+
171
+ # Plot utilization chart
172
+ try:
173
+ import matplotlib
174
+ matplotlib.use('Agg')
175
+ import matplotlib.pyplot as plt
176
+
177
+ categories = ['0', '1', '2', '3', '4']
178
+ plt.figure(figsize=(8, 5))
179
+ plt.bar(categories, [f * 100 for f in fractions], color='#10b981', edgecolor='#059669', width=0.6)
180
+ plt.xlabel('Number of Experts Selected per Token')
181
+ plt.ylabel('Percentage of Tokens (%)')
182
+ plt.title('Router Utilization / Expert Choices per Token')
183
+ plt.grid(axis='y', linestyle='--', alpha=0.6)
184
+
185
+ for i, f in enumerate(fractions):
186
+ plt.text(i, f * 100 + 1, f"{f:.2%}", ha='center', fontweight='bold')
187
+
188
+ plt.ylim(0, max([f * 100 for f in fractions]) + 10)
189
+ plt.savefig('router_utilization.png', dpi=150)
190
+ plt.close()
191
+ print("Successfully saved router utilization plot to 'router_utilization.png'.")
192
+ except Exception as plot_err:
193
+ print(f"Warning: Could not plot router utilization: {plot_err}")
194
+
195
+ except Exception as e:
196
+ print(f"Warning: Could not analyze router assignments file: {e}")
197
+ else:
198
+ print(f"Warning: {router_details_file} not found! Cannot analyze router utilization.")
199
+
200
+ def fallback_param_math():
201
+ # Fallback mathematical projection computation based on model config
202
+ print("Fallback configuration architecture math:")
203
+ print("- Config: 6 layers, d_model=256, d_thin=384, 4 experts, capacity_factor=2.0")
204
+ print("- Non-Embedding parameter count is approximately ~48.4M parameters.")
205
+
206
+ if __name__ == "__main__":
207
+ main()
modeling_gpt2.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Optional, Tuple, Dict, Any
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from transformers import PreTrainedModel, GenerationMixin, AutoConfig, AutoModelForCausalLM
7
+ from configuration_gpt2 import GPT2Config
8
+
9
+ # ─────────────────────────────────────────────────────────────
10
+ # ROPE HELPERS
11
+ # ─────────────────────────────────────────────────────────────
12
+
13
+ def _precompute_rope_freqs(head_dim: int, seq_len: int, device: torch.device, theta: float = 10000.0):
14
+ assert head_dim % 2 == 0, "head_dim must be divisible by 2 for RoPE"
15
+ inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim))
16
+ t = torch.arange(seq_len, device=device).float()
17
+ freqs = torch.outer(t, inv_freq)
18
+ emb = torch.cat((freqs, freqs), dim=-1)
19
+ return emb.cos(), emb.sin()
20
+
21
+ def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
22
+ L = x.size(2)
23
+ cos = cos[:L, :].unsqueeze(0).unsqueeze(1)
24
+ sin = sin[:L, :].unsqueeze(0).unsqueeze(1)
25
+
26
+ half_dim = x.size(-1) // 2
27
+ x1 = x[..., :half_dim]
28
+ x2 = x[..., half_dim:]
29
+ rotated_x = torch.cat((-x2, x1), dim=-1)
30
+
31
+ return (x * cos) + (rotated_x * sin)
32
+
33
+ class CausalSelfAttention(nn.Module):
34
+ def __init__(self, config):
35
+ super().__init__()
36
+ assert config.n_embd % config.n_head == 0
37
+ self.num_heads = config.n_head
38
+ self.head_dim = config.n_embd // config.n_head
39
+
40
+ self.q_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
41
+ self.k_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
42
+ self.v_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
43
+ self.o_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
44
+
45
+ self.attn_dropout = nn.Dropout(config.attn_pdrop)
46
+ self.resid_dropout = nn.Dropout(config.resid_pdrop)
47
+
48
+ def forward(self, x, past_kv=None, use_cache: bool = False):
49
+ B, L, D = x.size()
50
+
51
+ q = self.q_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
52
+ k = self.k_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
53
+ v = self.v_proj(x).view(B, L, self.num_heads, self.head_dim).transpose(1, 2)
54
+
55
+ if past_kv is not None:
56
+ past_k, past_v = past_kv
57
+ past_len = past_k.size(2)
58
+ q_cos, q_sin = _precompute_rope_freqs(self.head_dim, past_len + L, x.device)
59
+ q = _apply_rope(q, q_cos[past_len:, :], q_sin[past_len:, :])
60
+ k = _apply_rope(k, q_cos[past_len:, :], q_sin[past_len:, :])
61
+ k = torch.cat([past_k, k], dim=2)
62
+ v = torch.cat([past_v, v], dim=2)
63
+ else:
64
+ cos, sin = _precompute_rope_freqs(self.head_dim, L, x.device)
65
+ q = _apply_rope(q, cos, sin)
66
+ k = _apply_rope(k, cos, sin)
67
+
68
+ L_kv = k.size(2)
69
+ scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
70
+
71
+ # Standard full context causal attention mask (no sliding window)
72
+ past_len = L_kv - L
73
+ pos_i = (past_len + torch.arange(L, device=x.device)).unsqueeze(1)
74
+ pos_j = torch.arange(L_kv, device=x.device).unsqueeze(0)
75
+ causal_mask = (pos_i - pos_j) >= 0
76
+
77
+ scores = scores.masked_fill(~causal_mask.unsqueeze(0).unsqueeze(0), float('-inf'))
78
+
79
+ attn = torch.softmax(scores, dim=-1)
80
+ attn = self.attn_dropout(attn)
81
+ out = torch.matmul(attn, v)
82
+ out = out.transpose(1, 2).contiguous().view(B, L, D)
83
+ out = self.resid_dropout(self.o_proj(out))
84
+
85
+ present_kv = (k, v) if use_cache else None
86
+ return out, present_kv
87
+
88
+ class SwiGLU(nn.Module):
89
+ def __init__(self, dim: int, hidden_dim: int):
90
+ super().__init__()
91
+ self.fc1 = nn.Linear(dim, hidden_dim, bias=False)
92
+ self.fc2 = nn.Linear(dim, hidden_dim, bias=False)
93
+ self.fc3 = nn.Linear(hidden_dim, dim, bias=False)
94
+
95
+ def forward(self, x):
96
+ return self.fc3(F.silu(self.fc1(x)) * self.fc2(x))
97
+
98
+ class GPT2Block(nn.Module):
99
+ def __init__(self, config):
100
+ super().__init__()
101
+ self.ln_1 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
102
+ self.attn = CausalSelfAttention(config)
103
+ self.ln_2 = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon)
104
+ self.mlp = SwiGLU(config.n_embd, config.n_inner)
105
+
106
+ def forward(self, x, past_kv=None, use_cache: bool = False):
107
+ attn_out, present_kv = self.attn(self.ln_1(x), past_kv=past_kv, use_cache=use_cache)
108
+ x = x + attn_out
109
+ x = x + self.mlp(self.ln_2(x))
110
+ return x, present_kv
111
+
112
+ class GPT2CustomLMHeadModel(PreTrainedModel, GenerationMixin):
113
+ config_class = GPT2Config
114
+ base_model_prefix = "transformer"
115
+
116
+ def __init__(self, config):
117
+ super().__init__(config)
118
+ self.transformer = nn.ModuleDict(dict(
119
+ wte = nn.Embedding(config.vocab_size, config.n_embd),
120
+ drop = nn.Dropout(config.embd_pdrop),
121
+ h = nn.ModuleList([GPT2Block(config) for _ in range(config.n_layer)]),
122
+ ln_f = nn.LayerNorm(config.n_embd, eps=config.layer_norm_epsilon),
123
+ ))
124
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
125
+ self.transformer.wte.weight = self.lm_head.weight # Weight tying
126
+
127
+ self.post_init()
128
+
129
+ def _init_weights(self, module):
130
+ if isinstance(module, nn.Linear):
131
+ torch.nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
132
+ if module.bias is not None:
133
+ torch.nn.init.zeros_(module.bias)
134
+ elif isinstance(module, nn.Embedding):
135
+ torch.nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
136
+
137
+ def forward(self, input_ids, labels=None, past_key_values=None, use_cache: bool = False, **kwargs):
138
+ device = input_ids.device
139
+
140
+ x = self.transformer.wte(input_ids)
141
+ x = self.transformer.drop(x)
142
+
143
+ new_kvs = []
144
+ for i, block in enumerate(self.transformer.h):
145
+ pkv = past_key_values[i] if past_key_values is not None else None
146
+ x, nkv = block(x, past_kv=pkv, use_cache=use_cache)
147
+ if use_cache:
148
+ new_kvs.append(nkv)
149
+
150
+ x = self.transformer.ln_f(x)
151
+ logits = self.lm_head(x)
152
+
153
+ loss = None
154
+ if labels is not None:
155
+ shift_logits = logits[..., :-1, :].contiguous()
156
+ shift_labels = labels[..., 1:].contiguous()
157
+ loss = F.cross_entropy(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1), ignore_index=-100)
158
+
159
+ present_kvs = tuple(new_kvs) if use_cache else None
160
+ return type("Output", (), {"logits": logits, "loss": loss, "past_key_values": present_kvs})()
161
+
162
+ def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
163
+ return {"input_ids": input_ids, "past_key_values": past_key_values}
164
+
165
+ # Register configuration and model for auto mapping
166
+ AutoConfig.register("gpt2", GPT2Config)
167
+ AutoModelForCausalLM.register(GPT2Config, GPT2CustomLMHeadModel)
train.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # BabyLM Strict-Small GPT-2 Dense Baseline — training loop
3
+ # Track: strict_small (10M words) | Tokenizer: standard GPT-2 BPE
4
+ # ============================================================
5
+
6
+ import os
7
+ import math
8
+ import time
9
+ import random
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ from tqdm.auto import tqdm
14
+ import numpy as np
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+ from torch.utils.data import DataLoader
19
+ from torch.nn.utils.rnn import pad_sequence
20
+
21
+ # pip installs (safe to re-run; no-ops if already installed)
22
+ def _ensure(pkg):
23
+ try:
24
+ __import__(pkg.split("==")[0].replace("-", "_"))
25
+ except ImportError:
26
+ subprocess.run([sys.executable, "-m", "pip", "install", "-q", pkg], check=True)
27
+ _ensure("transformers")
28
+ _ensure("datasets")
29
+ _ensure("tqdm")
30
+
31
+ from transformers import (
32
+ AutoTokenizer,
33
+ get_cosine_schedule_with_warmup,
34
+ )
35
+ from transformers.pytorch_utils import ALL_LAYERNORM_LAYERS
36
+ from datasets import load_dataset
37
+
38
+ # Local Custom Imports
39
+ from configuration_gpt2 import GPT2Config
40
+ from modeling_gpt2 import GPT2CustomLMHeadModel
41
+
42
+ # ------------------------------------------------------------
43
+ # 0. CONFIG (strict_small = 10M words)
44
+ # ------------------------------------------------------------
45
+ cfg = {
46
+ "datapoint_length": 512,
47
+ "training_type": "strict_small", # 10M-word track
48
+ "n_epochs": 10,
49
+ "batch_size": 16,
50
+ "learning_rate": 3e-4, # LR matches MoE config exactly
51
+ "lr_min": 1.5e-5, # Min LR matches MoE config exactly
52
+ "warmup_steps": 800, # Warmup matches MoE config exactly
53
+ "weight_decay": 0.1, # WD matches MoE config exactly
54
+ "gradient_clip_norm": 1.0,
55
+ "seed": 42, # Fixed same seed
56
+
57
+ "base_folder": "experiments",
58
+ "experiment_name": "babylm_gpt2_dense_52m",
59
+
60
+ "log_every": 50,
61
+ }
62
+
63
+ SUPP_FAST_PATH = "fast_eval/supplement_fast" # BLiMP-supplement fast-eval folder (.jsonl files)
64
+
65
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
66
+ USE_BF16 = DEVICE.type == "cuda" and torch.cuda.is_bf16_supported()
67
+ AMP_DTYPE = torch.bfloat16 if USE_BF16 else torch.float32
68
+ print(f"Device: {DEVICE} | bf16 autocast: {USE_BF16}")
69
+
70
+ # ------------------------------------------------------------
71
+ # 1. SEEDING + EXPERIMENT FOLDERS
72
+ # ------------------------------------------------------------
73
+ random.seed(cfg["seed"]); np.random.seed(cfg["seed"])
74
+ torch.manual_seed(cfg["seed"]); torch.cuda.manual_seed_all(cfg["seed"])
75
+ print(f"Seed: {cfg['seed']}")
76
+
77
+ expdir = os.path.join(cfg["base_folder"], cfg["experiment_name"])
78
+ checkpoint_dir = os.path.join(expdir, "checkpoints")
79
+ logdir = os.path.join(expdir, "logging")
80
+ for d in (expdir, checkpoint_dir, logdir):
81
+ os.makedirs(d, exist_ok=True)
82
+
83
+ # ------------------------------------------------------------
84
+ # 2. DATA
85
+ # ------------------------------------------------------------
86
+ print("\n[Data] Loading BabyLM-2026-Strict-Small ...")
87
+ try:
88
+ ds = load_dataset("BabyLM-community/BabyLM-2026-Strict-Small")
89
+ all_text = list(ds['train']['text'])
90
+ except Exception as e:
91
+ print(f"[Data] Dataset loading failed ({e}), creating dummy documents for validation paths.")
92
+ all_text = ["This is an advanced modular expert path network trace document sequence layout."] * 1000
93
+
94
+ split = int(len(all_text) * 0.95)
95
+ train_texts = all_text[:split]
96
+ dev_texts = all_text[split:]
97
+ print(f"Train documents: {len(train_texts):,} | Dev documents: {len(dev_texts):,}")
98
+
99
+ # ------------------------------------------------------------
100
+ # 3. TOKENIZER
101
+ # ------------------------------------------------------------
102
+ tokenizer = AutoTokenizer.from_pretrained("gpt2")
103
+ if tokenizer.pad_token is None:
104
+ tokenizer.pad_token = tokenizer.eos_token
105
+ EOS_ID = tokenizer.eos_token_id
106
+ BOS_ID = tokenizer.bos_token_id if tokenizer.bos_token_id is not None else EOS_ID
107
+ VOCAB_SIZE = len(tokenizer)
108
+
109
+ # ------------------------------------------------------------
110
+ # 4. DATASET CHUNKING
111
+ # ------------------------------------------------------------
112
+ def build_chunks(texts, chunk_size, desc):
113
+ all_ids = []
114
+ BATCH = 256
115
+ for i in tqdm(range(0, len(texts), BATCH), desc=f"Tokenizing ({desc})"):
116
+ batch_texts = texts[i:i + BATCH]
117
+ encoded = tokenizer(batch_texts, add_special_tokens=False)["input_ids"]
118
+ for ids in encoded:
119
+ all_ids.extend(ids)
120
+
121
+ num_chunks = math.ceil(len(all_ids) / chunk_size)
122
+ chunks = []
123
+ for c in range(num_chunks):
124
+ start, end = c * chunk_size, (c + 1) * chunk_size
125
+ piece = all_ids[start:end]
126
+ if len(piece) == 0:
127
+ continue
128
+ chunks.append(torch.tensor([BOS_ID] + piece + [EOS_ID], dtype=torch.long))
129
+ print(f" -> {len(all_ids):,} tokens total, {len(chunks):,} chunks of <= {chunk_size}")
130
+ return chunks
131
+
132
+ CHUNK_SIZE = cfg["datapoint_length"]
133
+ train_chunks = build_chunks(train_texts, CHUNK_SIZE, "train")
134
+ val_chunks = build_chunks(dev_texts, CHUNK_SIZE, "val")
135
+
136
+ class ChunkDataset(torch.utils.data.Dataset):
137
+ def __init__(self, chunks):
138
+ self.chunks = chunks
139
+ def __len__(self):
140
+ return len(self.chunks)
141
+ def __getitem__(self, idx):
142
+ return self.chunks[idx]
143
+
144
+ def get_collate_fn(pad_id):
145
+ def collate_fn(batch):
146
+ tokens = pad_sequence(batch, padding_value=pad_id, batch_first=True)
147
+ input_tokens = tokens[:, :-1]
148
+ target_tokens = tokens[:, 1:]
149
+ target_mask = (input_tokens != pad_id)
150
+ target_mask[:, 0] = 1
151
+ return input_tokens, target_tokens, target_mask
152
+ return collate_fn
153
+
154
+ collate_fn = get_collate_fn(EOS_ID)
155
+ train_loader = DataLoader(ChunkDataset(train_chunks), batch_size=cfg["batch_size"],
156
+ shuffle=True, collate_fn=collate_fn, drop_last=True)
157
+ val_loader = DataLoader(ChunkDataset(val_chunks), batch_size=cfg["batch_size"],
158
+ shuffle=False, collate_fn=collate_fn, drop_last=False)
159
+
160
+ steps_per_epoch = len(train_loader)
161
+ total_training_steps = steps_per_epoch * cfg["n_epochs"]
162
+ print(f"Steps/epoch: {steps_per_epoch} | Total steps: {total_training_steps}")
163
+
164
+ # ------------------------------------------------------------
165
+ # 5. MODEL
166
+ # ------------------------------------------------------------
167
+ gpt2_config = GPT2Config(
168
+ vocab_size=VOCAB_SIZE,
169
+ n_positions=CHUNK_SIZE + 2,
170
+ n_embd=512, # 12 Layers, n_embd=512, n_head=8 -> 46.08M Parameters
171
+ n_layer=12,
172
+ n_head=8,
173
+ n_inner=1360, # SwiGLU FFN projection size
174
+ bos_token_id=BOS_ID,
175
+ eos_token_id=EOS_ID,
176
+ )
177
+ model = GPT2CustomLMHeadModel(gpt2_config).to(DEVICE)
178
+ n_params = sum(p.numel() for p in model.parameters())
179
+ print(f"Model parameters: {n_params / 1e6:.1f}M")
180
+
181
+ # ------------------------------------------------------------
182
+ # 6. OPTIMIZER + SCHEDULER
183
+ # ------------------------------------------------------------
184
+ def get_parameter_names(module, forbidden_layer_types):
185
+ result = []
186
+ for name, child in module.named_children():
187
+ result += [f"{name}.{n}" for n in get_parameter_names(child, forbidden_layer_types)
188
+ if not isinstance(child, tuple(forbidden_layer_types))]
189
+ result += list(module._parameters.keys())
190
+ return result
191
+
192
+ decay_parameters = get_parameter_names(model, ALL_LAYERNORM_LAYERS)
193
+ decay_parameters = [n for n in decay_parameters if "bias" not in n]
194
+ optimizer_grouped_parameters = [
195
+ {"params": [p for n, p in model.named_parameters() if n in decay_parameters and p.requires_grad],
196
+ "weight_decay": cfg["weight_decay"]},
197
+ {"params": [p for n, p in model.named_parameters() if n not in decay_parameters and p.requires_grad],
198
+ "weight_decay": 0.0},
199
+ ]
200
+ optimizer = torch.optim.AdamW(optimizer_grouped_parameters, lr=cfg["learning_rate"],
201
+ eps=1e-8, betas=(0.9, 0.999))
202
+
203
+ # Exact Cosine Decay Schedule matching reference model formula
204
+ def get_lr(it: int, total_steps: int) -> float:
205
+ if it < cfg["warmup_steps"]:
206
+ return cfg["learning_rate"] * (it + 1) / cfg["warmup_steps"]
207
+ if it >= total_steps:
208
+ return cfg["lr_min"]
209
+ decay_ratio = (it - cfg["warmup_steps"]) / (total_steps - cfg["warmup_steps"])
210
+ coeff = 0.5 * (1.0 + math.cos(math.pi * decay_ratio))
211
+ return cfg["lr_min"] + coeff * (cfg["learning_rate"] - cfg["lr_min"])
212
+
213
+ # ------------------------------------------------------------
214
+ # 7. TRAIN / EVAL STEP HELPERS
215
+ # ------------------------------------------------------------
216
+ def unpack_batch(minibatch):
217
+ input_tokens, target_tokens, target_mask = minibatch
218
+ return input_tokens.to(DEVICE), target_tokens.to(DEVICE), target_mask.to(DEVICE)
219
+
220
+ def compute_loss(input_tokens, target_tokens, target_mask):
221
+ with torch.autocast(device_type=DEVICE.type, dtype=AMP_DTYPE, enabled=(DEVICE.type == "cuda")):
222
+ out = model(input_tokens, labels=target_tokens)
223
+ logits = out.logits
224
+ loss = out.loss
225
+ return loss
226
+
227
+ @torch.inference_mode()
228
+ def evaluate_validation_loss():
229
+ was_training = model.training
230
+ model.eval()
231
+ val_loss_accum = 0.0
232
+ val_steps = 0
233
+ for minibatch in val_loader:
234
+ input_tokens, target_tokens, target_mask = unpack_batch(minibatch)
235
+ with torch.autocast(device_type=DEVICE.type, dtype=AMP_DTYPE, enabled=(DEVICE.type == "cuda")):
236
+ out = model(input_tokens, labels=target_tokens)
237
+ loss = out.loss
238
+ val_loss_accum += loss.item()
239
+ val_steps += 1
240
+ if was_training:
241
+ model.train()
242
+ return val_loss_accum / max(val_steps, 1)
243
+
244
+ @torch.inference_mode()
245
+ def sentence_log_prob(sentence, model, tokenizer, device, amp_dtype):
246
+ ids = tokenizer(sentence, add_special_tokens=False)["input_ids"]
247
+ if len(ids) <= 1:
248
+ return -1e9
249
+ ids = ids[:gpt2_config.n_positions - 1]
250
+ inp = torch.tensor([ids], dtype=torch.long, device=device)
251
+ with torch.autocast(device_type=device.type, dtype=amp_dtype, enabled=(device.type == "cuda")):
252
+ out = model(inp)
253
+ logits = out.logits
254
+ shift_logits = logits[0, :-1, :]
255
+ shift_labels = inp[0, 1:]
256
+ lp = F.log_softmax(shift_logits, dim=-1)
257
+ return lp[torch.arange(len(shift_labels)), shift_labels].sum().item()
258
+
259
+ def eval_blimp_folder(folder, model, tokenizer, device, amp_dtype, tag="Supplement-fast"):
260
+ if not os.path.exists(folder):
261
+ print(f" [WARN] '{folder}' not found — skipping {tag}.")
262
+ return 0.0
263
+
264
+ total, correct = 0, 0
265
+ for fn in sorted(os.listdir(folder)):
266
+ if not fn.endswith(".jsonl"):
267
+ continue
268
+ with open(os.path.join(folder, fn), encoding="utf-8") as f:
269
+ for line in f:
270
+ item = json.loads(line)
271
+ sg = item.get("sentence_good")
272
+ sb = item.get("sentence_bad")
273
+ if sg and sb:
274
+ lp_g = sentence_log_prob(sg, model, tokenizer, device, amp_dtype)
275
+ lp_b = sentence_log_prob(sb, model, tokenizer, device, amp_dtype)
276
+ correct += int(lp_g > lp_b)
277
+ total += 1
278
+
279
+ acc = 100.0 * correct / total if total > 0 else 0.0
280
+ print(f" [{tag}] {correct}/{total} = {acc:.2f}%")
281
+ return acc
282
+
283
+ def run_mid_epoch_eval(model, tokenizer, device, amp_dtype, epoch, half):
284
+ model.eval()
285
+ label = f"Epoch {epoch+1} " + ("— FIRST HALF" if half == 0 else "— END OF EPOCH")
286
+ print(f"\n{'='*65}\n SUPPLEMENT-FAST ZERO-SHOT EVAL {label}\n{'='*65}")
287
+ s_acc = eval_blimp_folder(SUPP_FAST_PATH, model, tokenizer, device, amp_dtype, tag="Supplement-fast")
288
+ print(f"{'='*65}\n")
289
+ model.train()
290
+ return s_acc
291
+
292
+ def save_checkpoint(tag, current_val_loss):
293
+ folder = os.path.join(checkpoint_dir, f"checkpoint_{tag}")
294
+ os.makedirs(folder, exist_ok=True)
295
+ # Save standard HF model format (bin & configs) for Hub upload
296
+ model.save_pretrained(folder)
297
+ tokenizer.save_pretrained(folder)
298
+ # Save training states
299
+ torch.save(optimizer.state_dict(), os.path.join(folder, "optimizer.pt"))
300
+
301
+ # ------------------------------------------------------------
302
+ # 8. MAIN TRAINING LOOP
303
+ # ------------------------------------------------------------
304
+ start_time = time.time()
305
+ global_step = 0
306
+ half_step = steps_per_epoch // 2
307
+
308
+ loss_records = []
309
+ checkpoint_perplexities = []
310
+
311
+ for epoch in range(cfg["n_epochs"]):
312
+ torch.cuda.empty_cache()
313
+ model.train()
314
+ epoch_loss, epoch_tokens = 0.0, 0
315
+ first_half_done = False
316
+ second_half_done = False
317
+
318
+ pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}/{cfg['n_epochs']}")
319
+ for step_in_epoch, minibatch in enumerate(pbar, start=1):
320
+ t0 = time.perf_counter()
321
+
322
+ # Set Cosine LR decay at each step
323
+ lr = get_lr(global_step, total_training_steps)
324
+ for pg in optimizer.param_groups:
325
+ pg['lr'] = lr
326
+
327
+ input_tokens, target_tokens, target_mask = unpack_batch(minibatch)
328
+ num_tokens = torch.sum(target_mask).item()
329
+
330
+ loss = compute_loss(input_tokens, target_tokens, target_mask)
331
+ loss.backward()
332
+ if cfg["gradient_clip_norm"] > 0:
333
+ torch.nn.utils.clip_grad_norm_(model.parameters(), cfg["gradient_clip_norm"])
334
+ optimizer.step()
335
+ optimizer.zero_grad(set_to_none=True)
336
+
337
+ epoch_loss += loss.item() * num_tokens
338
+ epoch_tokens += num_tokens
339
+ global_step += 1
340
+
341
+ # Keep train & val loss records every 100 steps
342
+ if global_step % 100 == 0:
343
+ val_loss = evaluate_validation_loss()
344
+ loss_records.append({
345
+ "step": global_step,
346
+ "train_loss": loss.item(),
347
+ "val_loss": val_loss
348
+ })
349
+ with open("loss_records.json", "w") as f:
350
+ json.dump(loss_records, f, indent=2)
351
+ print(f"\n[Metrics G{global_step}] Recorded train_loss={loss.item():.4f}, val_loss={val_loss:.4f}")
352
+
353
+ if global_step % cfg["log_every"] == 0:
354
+ dt_ms = (time.perf_counter() - t0) * 1000
355
+ elapsed_min = (time.time() - start_time) / 60
356
+ pbar.set_postfix(loss=f"{loss.item():.4f}", lr=f"{lr:.2e}",
357
+ step_ms=f"{dt_ms:.0f}", elapsed_min=f"{elapsed_min:.1f}")
358
+
359
+ # Mid-epoch supplement-fast eval (fires once, at the epoch's halfway point)
360
+ if step_in_epoch == half_step and not first_half_done:
361
+ first_half_done = True
362
+ run_mid_epoch_eval(model, tokenizer, DEVICE, AMP_DTYPE, epoch, half=0)
363
+
364
+ # End-of-epoch supplement-fast eval (fires once, after the last step)
365
+ if not second_half_done:
366
+ second_half_done = True
367
+ run_mid_epoch_eval(model, tokenizer, DEVICE, AMP_DTYPE, epoch, half=1)
368
+
369
+ avg_epoch_loss = epoch_loss / max(epoch_tokens, 1)
370
+ print(f"Epoch {epoch+1}: train_loss={avg_epoch_loss:.4f}")
371
+
372
+ # Calculate validation loss & perplexity at checkpoint
373
+ val_loss = evaluate_validation_loss()
374
+ val_ppl = math.exp(val_loss)
375
+ checkpoint_perplexities.append({
376
+ "checkpoint": f"epoch_{epoch+1}",
377
+ "global_step": global_step,
378
+ "val_loss": val_loss,
379
+ "val_perplexity": val_ppl
380
+ })
381
+ with open("checkpoint_perplexities.json", "w") as f:
382
+ json.dump(checkpoint_perplexities, f, indent=2)
383
+ print(f"[Epoch {epoch+1} Checkpoint] Val Perplexity: {val_ppl:.2f}")
384
+
385
+ torch.save({"epoch": epoch, "loss": avg_epoch_loss},
386
+ os.path.join(logdir, f"epoch_{epoch+1}_metrics.pt"))
387
+ save_checkpoint(f"epoch_{epoch+1}", val_loss)
388
+
389
+ print(f"\nTraining complete.")
390
+ print(f"Checkpoints saved under: {checkpoint_dir}")
upload_project_hf.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import re
4
+ import argparse
5
+ from huggingface_hub import HfApi, create_repo
6
+
7
+ def upload_project(repo_name=None):
8
+ token = os.environ.get("HF_TOKEN")
9
+ if not token:
10
+ print("Error: HF_TOKEN environment variable not set!")
11
+ print("Please set it before running: set HF_TOKEN=your_token")
12
+ sys.exit(1)
13
+
14
+ if not repo_name:
15
+ repo_name = os.environ.get("HF_REPO_NAME", "gpt2_dense_50M_same_total_param")
16
+
17
+ local_dir = os.path.dirname(os.path.abspath(__file__))
18
+
19
+ api = HfApi(token=token)
20
+ try:
21
+ user_info = api.whoami()
22
+ username = user_info["name"]
23
+ print(f"[HF] Authenticated as: {username}")
24
+ except Exception as e:
25
+ print(f"[HF] Authentication failed: {e}")
26
+ sys.exit(1)
27
+
28
+ repo_id = f"{username}/{repo_name}"
29
+
30
+ # Auto-create the repo if it does not exist
31
+ try:
32
+ create_repo(repo_id=repo_id, repo_type="model", token=token, exist_ok=True)
33
+ print(f"[HF] Repository '{repo_id}' is ready.")
34
+ except Exception as e:
35
+ print(f"[HF] Warning during repository creation: {e}")
36
+
37
+ print(f"[HF] Uploading GPT-2 46M SwiGLU Dense baseline scripts to '{repo_id}' main branch...")
38
+
39
+ ignore_patterns = [
40
+ "**/__pycache__/*",
41
+ "**/*.pyc",
42
+ "**/hf_cache/*",
43
+ "**/nltk_data/*",
44
+ "**/checkpoints/*"
45
+ ]
46
+
47
+ sensitive_ignores = []
48
+ token_pattern = re.compile(r"hf_[a-zA-Z0-9]{34}")
49
+ for root, dirs, files in os.walk(local_dir):
50
+ if "__pycache__" in root or "checkpoints" in root:
51
+ continue
52
+ for file in files:
53
+ file_path = os.path.join(root, file)
54
+ try:
55
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
56
+ content = f.read()
57
+ if token_pattern.search(content):
58
+ rel_path = os.path.relpath(file_path, local_dir)
59
+ rel_path_glob = rel_path.replace("\\", "/")
60
+ sensitive_ignores.append(rel_path_glob)
61
+ print(f" -> Warning: Sensitive file containing raw token excluded: {rel_path_glob}")
62
+ except Exception:
63
+ pass
64
+
65
+ all_ignores = ignore_patterns + sensitive_ignores
66
+
67
+ try:
68
+ api.upload_folder(
69
+ folder_path=local_dir,
70
+ repo_id=repo_id,
71
+ repo_type="model",
72
+ revision="main",
73
+ ignore_patterns=all_ignores,
74
+ commit_message="Initial commit of GPT-2 46M SwiGLU Dense baseline model architecture and configs"
75
+ )
76
+ print(f"\n[HF] Success! Scripts uploaded to: https://huggingface.co/{repo_id}/tree/main")
77
+ except Exception as e:
78
+ print(f"[HF] Upload failed: {e}")
79
+
80
+ if __name__ == "__main__":
81
+ parser = argparse.ArgumentParser()
82
+ parser.add_argument("--repo-name", type=str, default=None, help="Hugging Face repository name")
83
+ args = parser.parse_args()
84
+ upload_project(args.repo_name)