Soham Jain commited on
Initial commit of GPT-2 46M SwiGLU Dense baseline model architecture and configs
Browse files- configuration_gpt2.py +36 -0
- generate_plots_and_report.py +207 -0
- modeling_gpt2.py +167 -0
- train.py +390 -0
- upload_project_hf.py +84 -0
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)
|