Chiru20 commited on
Commit
f90f02b
·
1 Parent(s): 2978042

Upload ANLP Assignment 2 Model Card, Configs, Report, and Evaluation Deliverables

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. ANLP_Assignment2_Colab_Runner.ipynb +463 -0
  3. README.md +117 -0
  4. assets/.gitkeep +1 -0
  5. assets/beam_search_runtime.png +3 -0
  6. assets/expert_heatmap.png +3 -0
  7. assets/optimizer_dynamics.png +3 -0
  8. assets/part1/expert_heatmap_matched_active.png +3 -0
  9. assets/part1/expert_heatmap_shared_routed.png +3 -0
  10. assets/part1/expert_heatmap_top1.png +3 -0
  11. assets/part1/expert_heatmap_top2.png +3 -0
  12. assets/part1/summary_dense.json +30 -0
  13. assets/part1/summary_matched_active.json +30 -0
  14. assets/part1/summary_shared_routed.json +30 -0
  15. assets/part1/summary_top1.json +30 -0
  16. assets/part1/summary_top2.json +30 -0
  17. assets/part2/optimizer_benchmark_results.json +272 -0
  18. assets/part2/optimizer_dynamics.png +3 -0
  19. assets/part3/beam_search_runtime.png +3 -0
  20. assets/part3/decoding_summary.json +44 -0
  21. assets/part3/raw_generations.json +0 -0
  22. assets/tokenizer_multilingual.json +846 -0
  23. config.json +20 -0
  24. configs/part1_dense.yaml +16 -0
  25. configs/part1_moe_matched_active.yaml +18 -0
  26. configs/part1_moe_shared_routed.yaml +20 -0
  27. configs/part1_moe_top1.yaml +19 -0
  28. configs/part1_moe_top2.yaml +19 -0
  29. configs/part2_adamw.yaml +5 -0
  30. configs/part2_adan.yaml +5 -0
  31. configs/part2_lion.yaml +4 -0
  32. configs/part2_muon.yaml +7 -0
  33. configs/part2_sophia.yaml +7 -0
  34. configs/part3_decoding.yaml +22 -0
  35. generation_config.json +11 -0
  36. main.py +197 -0
  37. report.pdf +3 -0
  38. src/part1/__init__.py +25 -0
  39. src/part1/config.py +129 -0
  40. src/part1/dataset.py +171 -0
  41. src/part1/model.py +202 -0
  42. src/part1/moe.py +183 -0
  43. src/part1/train.py +323 -0
  44. src/part2/__init__.py +26 -0
  45. src/part2/dataset.py +57 -0
  46. src/part2/optimizers.py +482 -0
  47. src/part2/train.py +324 -0
  48. src/part3/__init__.py +29 -0
  49. src/part3/decoding.py +266 -0
  50. src/part3/evaluate.py +238 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.pdf filter=lfs diff=lfs merge=lfs -text
ANLP_Assignment2_Colab_Runner.ipynb ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# ANLP Assignment 2: Mixture of Experts, Pretraining Optimizers, and Decoding Strategies\n",
8
+ "### Complete Self-Contained GPU Runner for Google Colab Pro (T4 / V100 / A100)\n",
9
+ "\n",
10
+ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Chiranjeevi-2020/ANLP_assignment2/blob/main/colab/ANLP_Assignment2_Colab_Runner.ipynb)\n",
11
+ "\n",
12
+ "Welcome! This notebook executes all three core sections of **Assignment 2 (Monsoon 2026, IIIT Hyderabad)**:\n",
13
+ "1. **Part 1: Mixture of Experts (MoE) (40 marks)** \u2014 5 FFN variants, parameter parity matching, Switch auxiliary loss, and language specialization heatmaps on `belumind/en-vi-ja-curated-500k-triplets`.\n",
14
+ "2. **Part 2: Custom Optimizers from Scratch (30 marks)** \u2014 Category 1 (AdamW), Category 2 (Adan), Category 3 (Lion), Category 4 (Muon), and Bonus (Sophia) pretraining dynamics on `browndw/human-ai-parallel-corpus`.\n",
15
+ "3. **Part 3: Decoding Strategies from Scratch (30 marks)** \u2014 Greedy, Top-k, Top-p, and Beam Search ($B \\in \\{1, 2, 4\\}$) on `EleutherAI/Pythia-160M` evaluated on `hamishivi/ROCStories` (1,000 samples) using strictly `model.forward()`.\n",
16
+ "\n",
17
+ "---\n",
18
+ "### \ud83d\udc76 Explain Like I'm 5 (ELI5): What Are We Doing in This Notebook?\n",
19
+ "Imagine you are building a team of smart robots to read and write stories:\n",
20
+ "- **Part 1 (Mixture of Experts)**: Instead of asking one big robot to know every language, we have a smart receptionist that sends Vietnamese words to one expert robot and Japanese words to another expert robot! They share the work so each robot does what it's best at.\n",
21
+ "- **Part 2 (Optimizers)**: How do robots learn from their mistakes? An optimizer is like a coach telling the robot how big of a step to take. We test 5 different coaches from scratch (like Muon, which uses matrix geometry, and Lion, which saves half the memory) to see who trains the robot fastest!\n",
22
+ "- **Part 3 (Decoding Strategies)**: When the robot writes the next word of a story, how does it choose? Greedy always takes the highest score; Top-k and Top-p pick from the best fun choices so the story isn't boring; Beam search looks ahead like a chess master to find the best storyline!\n",
23
+ "\n",
24
+ "---\n",
25
+ "### \u23f1\ufe0f Expected Runtimes on Google Colab Pro\n",
26
+ "| Pipeline Stage | Fast Verification / Smoke Test | Full Dataset Training (Colab Pro A100/V100) |\n",
27
+ "| :--- | :--- | :--- |\n",
28
+ "| **Environment & Git Setup** | ~30 seconds | ~30 seconds |\n",
29
+ "| **Part 1: 5 MoE Ablations** | ~2 minutes (dummy tensors) | ~45-60 minutes (A100) / ~90 minutes (T4) |\n",
30
+ "| **Part 2: 5 Optimizers Dynamics** | ~1 minute (warmup) | ~30-40 minutes (A100) / ~60 minutes (T4) |\n",
31
+ "| **Part 3: Decoding Benchmark (1,000 samples)** | ~30 seconds (10 samples) | ~10-15 minutes (1,000 samples on A100) |\n",
32
+ "| **Total End-to-End Execution** | **~5 minutes** | **~1.5 to 2.5 hours** |\n",
33
+ "\n",
34
+ "---\n",
35
+ "### \ud83d\udcc1 Where Are Files & Checkpoints Generated?\n",
36
+ "- Trained Model Checkpoints: `checkpoints/part1/` and `checkpoints/part2/`\n",
37
+ "- Heatmaps & Plots: `assets/expert_heatmap.png`, `assets/optimizer_dynamics.png`, `assets/beam_search_runtime.png`\n",
38
+ "- Numerical Results & Logs: `assets/part2/optimizer_benchmark_results.json`, `assets/part3/decoding_summary.json`\n",
39
+ "- At the end of this notebook, a single cell zips all outputs into `submission_artifacts.zip` for instant local download."
40
+ ]
41
+ },
42
+ {
43
+ "cell_type": "markdown",
44
+ "metadata": {},
45
+ "source": [
46
+ "## Step 1: Clone Repository & Set Working Directory (ELI5: Put our project on the computer)\n",
47
+ "\n",
48
+ "Google Colab starts with an empty `/content` folder. This cell automatically:\n",
49
+ "1. Checks Colab Secrets for a `GH_TOKEN` (needed if your repo is **private**).\n",
50
+ "2. Clones your GitHub repository `ANLP_assignment2` into `/content/ANLP_assignment2`.\n",
51
+ "3. Changes the active working directory into the repository root.\n",
52
+ "4. Pulls the latest commits from branch `main`.\n",
53
+ "\n",
54
+ "**If your repo is private**, add a secret named `GH_TOKEN` in the \ud83d\udd11 Secrets tab with a [GitHub Personal Access Token](https://github.com/settings/tokens) (Classic, with `repo` scope). Toggle **Notebook access** ON.\n",
55
+ "\n",
56
+ "**If your repo is public**, no token is needed \u2014 it just works!"
57
+ ]
58
+ },
59
+ {
60
+ "cell_type": "code",
61
+ "execution_count": null,
62
+ "metadata": {},
63
+ "outputs": [],
64
+ "source": [
65
+ "import os, sys, subprocess\n",
66
+ "\n",
67
+ "REPO_DIR = \"/content/ANLP_assignment2\"\n",
68
+ "\n",
69
+ "# --- Fetch GitHub token from Colab Secrets Vault for private repo access ---\n",
70
+ "gh_token = None\n",
71
+ "try:\n",
72
+ " from google.colab import userdata\n",
73
+ " for name in ['GH_TOKEN', 'GITHUB_TOKEN', 'github_token', 'gh_token']:\n",
74
+ " try:\n",
75
+ " gh_token = userdata.get(name)\n",
76
+ " if gh_token:\n",
77
+ " print(f'\\U0001f513 Found GitHub token in Colab Secrets (key: {name})')\n",
78
+ " break\n",
79
+ " except Exception:\n",
80
+ " pass\n",
81
+ "except ImportError:\n",
82
+ " pass # Not running in Colab\n",
83
+ "\n",
84
+ "# --- Build clone URL (with or without token) ---\n",
85
+ "if gh_token:\n",
86
+ " clone_url = f'https://{gh_token}@github.com/Chiranjeevi-2020/ANLP_assignment2.git'\n",
87
+ "else:\n",
88
+ " clone_url = 'https://github.com/Chiranjeevi-2020/ANLP_assignment2.git'\n",
89
+ " print('\\u26a0\\ufe0f No GitHub token found in Colab Secrets.')\n",
90
+ " print(' If your repo is PRIVATE, add a secret named GH_TOKEN with a GitHub Personal Access Token.')\n",
91
+ " print(' If your repo is PUBLIC, this will work without a token.')\n",
92
+ "\n",
93
+ "# --- Clone if not already present ---\n",
94
+ "if not os.path.exists(REPO_DIR):\n",
95
+ " result = subprocess.run(['git', 'clone', clone_url, REPO_DIR], capture_output=True, text=True)\n",
96
+ " if result.returncode != 0:\n",
97
+ " print(f'\\u274c Clone failed: {result.stderr.strip()}')\n",
98
+ " print('\\n\\U0001f527 FIX: Your repo is likely PRIVATE. Do ONE of these:')\n",
99
+ " print(' Option A (Easiest): Go to https://github.com/Chiranjeevi-2020/ANLP_assignment2/settings')\n",
100
+ " print(' Scroll to \"Danger Zone\" -> \"Change visibility\" -> Make it Public')\n",
101
+ " print(' Option B: Create a GitHub Personal Access Token at https://github.com/settings/tokens')\n",
102
+ " print(' Add it to Colab Secrets (\\U0001f511 icon) as GH_TOKEN with Notebook access ON')\n",
103
+ " raise RuntimeError('Repository clone failed. See instructions above.')\n",
104
+ " else:\n",
105
+ " print(f'\\u2705 Successfully cloned repository!')\n",
106
+ "\n",
107
+ "# --- Navigate into repository root ---\n",
108
+ "%cd {REPO_DIR}\n",
109
+ "\n",
110
+ "# --- Pull latest changes ---\n",
111
+ "!git pull origin main --ff-only 2>/dev/null || true\n",
112
+ "\n",
113
+ "# --- Add to Python path ---\n",
114
+ "if REPO_DIR not in sys.path:\n",
115
+ " sys.path.insert(0, REPO_DIR)\n",
116
+ "\n",
117
+ "print(f'\\n\\u2705 Project Root: {os.getcwd()}')\n",
118
+ "print(f'\\u2705 Files: {sorted([f for f in os.listdir(\".\") if not f.startswith(\".\")][:15])}')\n"
119
+ ]
120
+ },
121
+ {
122
+ "cell_type": "markdown",
123
+ "metadata": {},
124
+ "source": [
125
+ "## Step 2: GPU Diagnostics & Setup (ELI5: Check that the supercomputer is awake)"
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": null,
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "# 1. Verify GPU assignment\n",
135
+ "!nvidia-smi\n",
136
+ "import torch\n",
137
+ "print(f\"CUDA Available: {torch.cuda.is_available()}\")\n",
138
+ "if torch.cuda.is_available():\n",
139
+ " print(f\"Allocated GPU: {torch.cuda.get_device_name(0)}\")\n",
140
+ " print(f\"Device Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB\")"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "markdown",
145
+ "metadata": {},
146
+ "source": [
147
+ "## Step 3: Install Required Libraries (ELI5: Download the tools our robot needs)"
148
+ ]
149
+ },
150
+ {
151
+ "cell_type": "code",
152
+ "execution_count": null,
153
+ "metadata": {},
154
+ "outputs": [],
155
+ "source": [
156
+ "# Install core NLP, model evaluation, and dashboard packages\n",
157
+ "!pip install -q pytest accelerate torch transformers datasets tokenizers sacrebleu evaluate wandb matplotlib seaborn einops huggingface_hub"
158
+ ]
159
+ },
160
+ {
161
+ "cell_type": "markdown",
162
+ "metadata": {},
163
+ "source": [
164
+ "## Step 4: Authenticate WandB & Hugging Face via Colab Secrets Vault (ELI5: Safe Login)\n",
165
+ "\n",
166
+ "This cell securely fetches your API tokens from Google Colab's **Secrets Vault** without exposing them in code:\n",
167
+ "\n",
168
+ "### \ud83d\udd11 How to set up your secrets in Colab (One-Time Setup):\n",
169
+ "1. Click the **Key icon (\ud83d\udd11)** on Google Colab's left sidebar (Secrets panel).\n",
170
+ "2. Click **+ Add new secret**.\n",
171
+ "3. Secret 1: Name = `WANDB_API_KEY`, Value = *(your WandB API key from [wandb.ai/authorize](https://wandb.ai/authorize))*\n",
172
+ "4. Secret 2: Name = `HF_TOKEN`, Value = *(your Hugging Face token from [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens))*\n",
173
+ "5. \u26a0\ufe0f **CRITICAL**: Toggle **Notebook access** to **ON** for both secrets!\n",
174
+ "\n",
175
+ "*If secrets are not added, the notebook will operate in safe offline mode with zero crashes.*"
176
+ ]
177
+ },
178
+ {
179
+ "cell_type": "code",
180
+ "execution_count": null,
181
+ "metadata": {},
182
+ "outputs": [],
183
+ "source": [
184
+ "import os\n",
185
+ "\n",
186
+ "# 1. Fetch WandB Secret from Colab Secrets Vault\n",
187
+ "wandb_key = None\n",
188
+ "try:\n",
189
+ " from google.colab import userdata\n",
190
+ " for name in ['WANDB_API_KEY', 'wandb_api_key', 'WANDB', 'wandb']:\n",
191
+ " try:\n",
192
+ " wandb_key = userdata.get(name)\n",
193
+ " if wandb_key:\n",
194
+ " print(f\"[OK] Found WandB secret under key: '{name}'\")\n",
195
+ " break\n",
196
+ " except Exception as e:\n",
197
+ " err_msg = str(e).lower()\n",
198
+ " if \"notebook access\" in err_msg or \"notebookaccesserror\" in type(e).__name__.lower():\n",
199
+ " print(f\"[WARNING] Secret '{name}' exists in Colab Secrets Vault, but 'Notebook access' toggle is OFF! Please turn ON the toggle switch next to '{name}' in the Colab Secrets (\ud83d\udd11) tab.\")\n",
200
+ "except Exception:\n",
201
+ " pass\n",
202
+ "\n",
203
+ "if wandb_key:\n",
204
+ " os.environ['WANDB_API_KEY'] = wandb_key\n",
205
+ " try:\n",
206
+ " import wandb\n",
207
+ " wandb.login(key=wandb_key)\n",
208
+ " print(\"[SUCCESS] Successfully authenticated WandB via Colab Secrets!\")\n",
209
+ " except Exception as e:\n",
210
+ " print(f\"[NOTE] WandB login notice: {e}\")\n",
211
+ "else:\n",
212
+ " print(\"[INFO] WandB secret not found in Secrets Vault. Operating in safe offline mode (wandb disabled).\")\n",
213
+ " os.environ['WANDB_MODE'] = 'offline'\n",
214
+ "\n",
215
+ "# 2. Fetch Hugging Face Secret from Colab Secrets Vault\n",
216
+ "hf_token = None\n",
217
+ "try:\n",
218
+ " from google.colab import userdata\n",
219
+ " for name in ['HF_TOKEN', 'hf_token', 'HUGGINGFACE_TOKEN', 'huggingface_token', 'HF', 'huggingface']:\n",
220
+ " try:\n",
221
+ " hf_token = userdata.get(name)\n",
222
+ " if hf_token:\n",
223
+ " print(f\"[OK] Found Hugging Face secret under key: '{name}'\")\n",
224
+ " break\n",
225
+ " except Exception as e:\n",
226
+ " err_msg = str(e).lower()\n",
227
+ " if \"notebook access\" in err_msg or \"notebookaccesserror\" in type(e).__name__.lower():\n",
228
+ " print(f\"[WARNING] Secret '{name}' exists in Colab Secrets Vault, but 'Notebook access' toggle is OFF! Please turn ON the toggle switch next to '{name}' in the Colab Secrets (\ud83d\udd11) tab.\")\n",
229
+ "except Exception:\n",
230
+ " pass\n",
231
+ "\n",
232
+ "if hf_token:\n",
233
+ " os.environ['HF_TOKEN'] = hf_token\n",
234
+ " os.environ['HUGGING_FACE_HUB_TOKEN'] = hf_token\n",
235
+ " try:\n",
236
+ " from huggingface_hub import login\n",
237
+ " login(token=hf_token, add_to_git_credential=True)\n",
238
+ " print(\"[SUCCESS] Successfully authenticated Hugging Face via Colab Secrets!\")\n",
239
+ " except Exception as e:\n",
240
+ " print(f\"[NOTE] Hugging Face login notice: {e}\")\n",
241
+ "else:\n",
242
+ " print(\"[INFO] HF secret not found in Secrets Vault. Public datasets and models will download normally.\")"
243
+ ]
244
+ },
245
+ {
246
+ "cell_type": "markdown",
247
+ "metadata": {},
248
+ "source": [
249
+ "## Step 5: Verify Mathematical Integrity & Test Suite (ELI5: Double check all formulas and code tests)"
250
+ ]
251
+ },
252
+ {
253
+ "cell_type": "code",
254
+ "execution_count": null,
255
+ "metadata": {},
256
+ "outputs": [],
257
+ "source": [
258
+ "# Runs comprehensive pytest suite across all 22 mathematical tests and main verify\n",
259
+ "!python -m pytest tests/ -v\n",
260
+ "!python main.py verify"
261
+ ]
262
+ },
263
+ {
264
+ "cell_type": "markdown",
265
+ "metadata": {},
266
+ "source": [
267
+ "## Step 6: Part 1 \u2014 Mixture of Experts (MoE) Execution\n",
268
+ "### ELI5:\n",
269
+ "We test 5 designs:\n",
270
+ "1. **Dense**: 1 big standard feedforward brain.\n",
271
+ "2. **Top-1 MoE**: 4 experts, 1 chosen per token.\n",
272
+ "3. **Top-2 MoE**: 4 experts, 2 chosen per token.\n",
273
+ "4. **Shared + Routed**: 1 expert always stays on, plus 1 routed expert chosen from 3.\n",
274
+ "5. **Matched Active**: 4 experts with doubled width so active parameters match the dense baseline exactly.\n",
275
+ "\n",
276
+ "We train on `belumind/en-vi-ja-curated-500k-triplets` to translate Vietnamese & Japanese to English, track routing with Switch auxiliary loss, and extract language heatmaps."
277
+ ]
278
+ },
279
+ {
280
+ "cell_type": "code",
281
+ "execution_count": null,
282
+ "metadata": {},
283
+ "outputs": [],
284
+ "source": [
285
+ "# Run Part 1 training across all 5 architectural ablations\n",
286
+ "# To run a quick smoke run, keep samples=2000, epochs=1.\n",
287
+ "# For full training, increase samples=10000, epochs=3.\n",
288
+ "\n",
289
+ "ablations = [\"dense\", \"top1\", \"top2\", \"shared_routed\", \"matched_active\"]\n",
290
+ "for abl in ablations:\n",
291
+ " print(f\"\\n{'='*25} RUNNING PART 1 ABLATION: {abl.upper()} {'='*25}\")\n",
292
+ " !python main.py part1 --ablation {abl} --train --samples 2000 --epochs 1"
293
+ ]
294
+ },
295
+ {
296
+ "cell_type": "markdown",
297
+ "metadata": {},
298
+ "source": [
299
+ "## Step 7: Part 2 \u2014 Custom Optimizers Pretraining Dynamics\n",
300
+ "### ELI5:\n",
301
+ "We pretrain our dense model on `browndw/human-ai-parallel-corpus` using 5 scratch optimizers:\n",
302
+ "1. **AdamW**: Standard baseline with decoupled weight decay.\n",
303
+ "2. **Adan**: Looks at gradient differences to smooth out bumpy steps.\n",
304
+ "3. **Lion**: Uses only `+1` or `-1` signs, cutting memory in half!\n",
305
+ "4. **Muon**: Uses Newton-Schulz polynomial iterations to rotate matrices without distortion.\n",
306
+ "5. **Sophia**: Uses second-order diagonal curvature and clipping to handle steep hills.\n",
307
+ "\n",
308
+ "We log validation loss and test BLEU at $0.1\\times$ intervals to replicate Figure 2 of *Fantastic Pretraining Optimizers*."
309
+ ]
310
+ },
311
+ {
312
+ "cell_type": "code",
313
+ "execution_count": null,
314
+ "metadata": {},
315
+ "outputs": [],
316
+ "source": [
317
+ "# Run Part 2 Pretraining Dynamics benchmark for all 5 optimizers\n",
318
+ "# tokens=100000 provides quick validation; tokens=500000 gives full Chinchilla curve.\n",
319
+ "!python main.py part2 --optimizer all --train --tokens 100000"
320
+ ]
321
+ },
322
+ {
323
+ "cell_type": "markdown",
324
+ "metadata": {},
325
+ "source": [
326
+ "## Step 8: Part 3 \u2014 Decoding Strategies on ROCStories\n",
327
+ "### ELI5:\n",
328
+ "Using `EleutherAI/Pythia-160M` without calling `model.generate()`, we generate story endings for 1,000 ROCStories samples using:\n",
329
+ "- **Greedy**: Always pick top token.\n",
330
+ "- **Top-k ($k=40, T=0.8$)**: Pick from top 40 words.\n",
331
+ "- **Top-p ($p=0.9, T=0.8$)**: Pick from the top 90% probability mass.\n",
332
+ "- **Beam Search ($B=1, 2, 4$)**: Keep the best $B$ candidate story paths.\n",
333
+ "\n",
334
+ "We measure perplexity, accuracy, and confirm linear latency scaling with respect to beam width $B$."
335
+ ]
336
+ },
337
+ {
338
+ "cell_type": "code",
339
+ "execution_count": null,
340
+ "metadata": {},
341
+ "outputs": [],
342
+ "source": [
343
+ "# Run Part 3 Decoding Benchmark on 1000 samples\n",
344
+ "!python main.py part3 --model_name EleutherAI/Pythia-160M --samples 1000 --output_dir assets/part3"
345
+ ]
346
+ },
347
+ {
348
+ "cell_type": "markdown",
349
+ "metadata": {},
350
+ "source": [
351
+ "## Step 9: View Visualizations (ELI5: Look at the pretty pictures our robots made!)"
352
+ ]
353
+ },
354
+ {
355
+ "cell_type": "code",
356
+ "execution_count": null,
357
+ "metadata": {},
358
+ "outputs": [],
359
+ "source": [
360
+ "import os\n",
361
+ "from IPython.display import Image, display\n",
362
+ "\n",
363
+ "figs = [\n",
364
+ " (\"MoE Language Specialization Heatmap\", \"assets/expert_heatmap.png\"),\n",
365
+ " (\"Optimizer Pretraining Dynamics (Fig 2)\", \"assets/optimizer_dynamics.png\"),\n",
366
+ " (\"Beam Search Latency vs Beam Width\", \"assets/beam_search_runtime.png\")\n",
367
+ "]\n",
368
+ "\n",
369
+ "for title, path in figs:\n",
370
+ " if os.path.exists(path):\n",
371
+ " print(f\"\\n=== {title} ({path}) ===\")\n",
372
+ " display(Image(path))\n",
373
+ " else:\n",
374
+ " print(f\"Note: {path} not yet generated.\")"
375
+ ]
376
+ },
377
+ {
378
+ "cell_type": "markdown",
379
+ "metadata": {},
380
+ "source": [
381
+ "## Step 10: Download All Artifacts Locally (ELI5: Pack all homework into a neat zip bag to submit)"
382
+ ]
383
+ },
384
+ {
385
+ "cell_type": "code",
386
+ "execution_count": null,
387
+ "metadata": {},
388
+ "outputs": [],
389
+ "source": [
390
+ "# Zips all assets, figures, configs, and json results for submission\n",
391
+ "!zip -r submission_artifacts.zip assets/ report.pdf configs/\n",
392
+ "\n",
393
+ "try:\n",
394
+ " from google.colab import files\n",
395
+ " files.download('submission_artifacts.zip')\n",
396
+ " print(\"Downloaded submission_artifacts.zip!\")\n",
397
+ "except Exception as e:\n",
398
+ " print(f\"Running outside Google Colab environment. File saved as submission_artifacts.zip in current directory ({e}).\")"
399
+ ]
400
+ },
401
+ {
402
+ "cell_type": "markdown",
403
+ "metadata": {},
404
+ "source": [
405
+ "## Step 11: Syncing Your Executed Notebook & Results to GitHub (ELI5: Save your work online)\n",
406
+ "\n",
407
+ "There are two easy ways to get your executed notebook and results on GitHub:\n",
408
+ "\n",
409
+ "### Option A: Save Executed Notebook Directly via Colab Menu (Recommended & Fastest \ud83d\ude80)\n",
410
+ "To save this completed notebook with all its live execution outputs, logs, and plots:\n",
411
+ "1. Click **File** (top left menu of Colab) $\\to$ **Save a copy in GitHub**.\n",
412
+ "2. Select Repository: `Chiranjeevi-2020/ANLP_assignment2`.\n",
413
+ "3. Select Branch: `main`.\n",
414
+ "4. File path: `colab/ANLP_Assignment2_Colab_Runner.ipynb`.\n",
415
+ "5. Click **OK**. Google Colab commits the executed notebook directly to your GitHub repo!\n",
416
+ "\n",
417
+ "### Option B: Push Generated Assets Directly from this Colab Cell (Optional)\n",
418
+ "If you have a GitHub Personal Access Token saved in Colab Secrets as `GH_TOKEN` or `GITHUB_TOKEN`:"
419
+ ]
420
+ },
421
+ {
422
+ "cell_type": "code",
423
+ "execution_count": null,
424
+ "metadata": {},
425
+ "outputs": [],
426
+ "source": [
427
+ "# Optional: Push generated figures and results directly from Colab VM\n",
428
+ "try:\n",
429
+ " from google.colab import userdata\n",
430
+ " gh_token = None\n",
431
+ " for name in ['GH_TOKEN', 'GITHUB_TOKEN', 'github_token']:\n",
432
+ " try:\n",
433
+ " gh_token = userdata.get(name)\n",
434
+ " if gh_token: break\n",
435
+ " except Exception: pass\n",
436
+ " \n",
437
+ " if gh_token:\n",
438
+ " !git config user.email \"chiranjeevi.kumaravadivel@gmail.com\"\n",
439
+ " !git config user.name \"Chiranjeevi-2020\"\n",
440
+ " !git add assets/ configs/ report.pdf\n",
441
+ " !git commit -m \"Colab GPU execution: update high-res training dynamics and assets\"\n",
442
+ " !git push https://{gh_token}@github.com/Chiranjeevi-2020/ANLP_assignment2.git main\n",
443
+ " print(\"Successfully pushed Colab training assets to GitHub!\")\n",
444
+ " else:\n",
445
+ " print(\"No GH_TOKEN found in Colab Secrets. Use Option A (File -> Save a copy in GitHub) or download submission_artifacts.zip from Step 10.\")\n",
446
+ "except Exception as e:\n",
447
+ " print(f\"Git push notice: {e}\")"
448
+ ]
449
+ }
450
+ ],
451
+ "metadata": {
452
+ "accelerator": "GPU",
453
+ "colab": {
454
+ "provenance": []
455
+ },
456
+ "language_info": {
457
+ "name": "python",
458
+ "version": "3.12"
459
+ }
460
+ },
461
+ "nbformat": 4,
462
+ "nbformat_minor": 2
463
+ }
README.md ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - en
4
+ - vi
5
+ - ja
6
+ tags:
7
+ - nlp
8
+ - mixture-of-experts
9
+ - moe
10
+ - transformer
11
+ - optimizers
12
+ - decoding-strategies
13
+ - adamw
14
+ - adan
15
+ - lion
16
+ - muon
17
+ - sophia
18
+ license: mit
19
+ datasets:
20
+ - belumind/en-vi-ja-curated-500k-triplets
21
+ - browndw/human-ai-parallel-corpus
22
+ - hamishivi/ROCStories
23
+ metrics:
24
+ - bleu
25
+ - perplexity
26
+ pipeline_tag: text-generation
27
+ ---
28
+
29
+ # Advanced Natural Language Processing (ANLP) - Assignment 2
30
+
31
+ **Author:** Chiranjeevi Kumaravadivel | **Roll Number:** `2023122010`
32
+ **Monsoon 2026** | International Institute of Information Technology, Hyderabad (IIIT-H)
33
+
34
+ [![Weights & Biases](https://img.shields.io/badge/Weights%20%26%20Biases-Dashboard%20Live-orange)](https://forge.coreweave.com/wandb/chiranjeevi-kumaravadivel-iiit-hyderabad/ANLP-Assignment-2)
35
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-blue)](https://github.com/Chiranjeevi-2020/ANLP_assignment2)
36
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Chiranjeevi-2020/ANLP_assignment2/blob/main/colab/ANLP_Assignment2_Colab_Runner.ipynb)
37
+
38
+ ---
39
+
40
+ ## 🔗 Official Submission Deliverables
41
+ - **Hugging Face Model Repository:** [https://huggingface.co/Chiru20/ANLP_assignment2](https://huggingface.co/Chiru20/ANLP_assignment2)
42
+ - **Weights & Biases (WandB) Dashboard:** [https://forge.coreweave.com/wandb/chiranjeevi-kumaravadivel-iiit-hyderabad/ANLP-Assignment-2](https://forge.coreweave.com/wandb/chiranjeevi-kumaravadivel-iiit-hyderabad/ANLP-Assignment-2)
43
+ - **GitHub Source Code:** [https://github.com/Chiranjeevi-2020/ANLP_assignment2](https://github.com/Chiranjeevi-2020/ANLP_assignment2)
44
+ - **Official Technical Report (PDF):** [`report.pdf`](./report.pdf) (8 pages, 11pt, matching IIIT-H assignment formatting)
45
+
46
+ ---
47
+
48
+ ## 📖 Model & Task Overview
49
+
50
+ This repository hosts the official experimental deliverables for **ANLP Assignment 2 (Monsoon 2026)** at IIIT Hyderabad:
51
+
52
+ ### 1. Mixture of Experts (MoE) Architecture (Part 1)
53
+ - **Task:** Multilingual translation from non-romanized Vietnamese (`<vi>`) and Japanese (`<ja>`) to English (`<en>`) using `belumind/en-vi-ja-curated-500k-triplets`.
54
+ - **5 Feed-Forward Network (FFN) Ablations:**
55
+ 1. **Dense Baseline:** Single shared feed-forward layer ($D_{\text{ff}} = 1024$).
56
+ 2. **Top-1 MoE:** 4 experts ($d_e = 256$), 1 active per token.
57
+ 3. **Top-2 MoE:** 4 experts ($d_e = 256$), 2 active per token with weighted softmax combination.
58
+ 4. **Shared + Routed MoE:** 1 dedicated shared expert + 1 routed expert from 3 candidates.
59
+ 5. **Matched-Active MoE:** 4 wide experts ($d_e = 1024$), active parameters match Dense baseline exactly (525K active per layer).
60
+ - **Load Balancing:** Switch Transformers auxiliary loss ($\alpha = 0.01$):
61
+ $$\mathcal{L}_{\text{aux}} = \alpha \cdot N \sum_{i=1}^N f_i \cdot P_i$$
62
+ - **Language Specialization Heatmap:** Preserved in `assets/part1/expert_heatmap_matched_active.png`.
63
+
64
+ | FFN Ablation Variant | Total Parameters | Active Parameters | Final Validation Loss | Test BLEU |
65
+ | :--- | :--- | :--- | :--- | :--- |
66
+ | **Dense Baseline** | 8.90M | 8.90M | 2.88 | 13.82 |
67
+ | **Top-1 MoE** | 8.91M (+0.12%) | 7.86M (-11.7%) | 2.96 | 13.15 |
68
+ | **Top-2 MoE** | 8.91M (+0.12%) | 8.21M (-7.8%) | 2.96 | 13.10 |
69
+ | **Shared + Routed** | 8.91M (+0.12%) | 8.21M (-7.8%) | 2.96 | 13.08 |
70
+ | **Matched-Active MoE** | 12.06M | 8.91M (+0.08%) | **2.94** | **13.30** |
71
+
72
+ ---
73
+
74
+ ### 2. Custom Optimizers from Scratch (Part 2)
75
+ - **Task:** Dense Transformer pretraining dynamics on `browndw/human-ai-parallel-corpus` replicating Figure 2 of *Wen et al. (2025)*.
76
+ - **Implemented Strictly from Scratch via `torch.optim.Optimizer`:**
77
+ - **Category 1 (AdamW):** Decoupled weight decay $\theta_{t} = \theta_{t-1}(1 - \eta \lambda) - \frac{\eta}{\sqrt{v_t} + \epsilon} m_t$.
78
+ - **Category 2 (Adan):** Adaptive Nesterov momentum with gradient difference tracking $v_k = (1 - \beta_2) v_{k-1} + \beta_2 (g_k - g_{k-1})$.
79
+ - **Category 3 (Lion):** Sign-based momentum updating $\theta_t = \theta_{t-1} - \eta \cdot \text{sign}(c_t)$, cutting second-moment memory in half.
80
+ - **Category 4 (Muon):** Matrix polar decomposition via Quintic Newton-Schulz polynomial iterations ($a=3.4445, b=-4.7750, c=2.0315$).
81
+ - **Bonus (Sophia-G):** Lightweight Hessian curvature estimation with coordinate-wise clipping $\text{clip}(m_t / \max(h_t, \epsilon), \rho)$.
82
+
83
+ | Optimizer | Final Val Loss | Test BLEU | Memory Per Param | Relative Convergence Rate |
84
+ | :--- | :--- | :--- | :--- | :--- |
85
+ | **CustomMuon** | **3.89** | **10.85** | 8 bytes (2D only) | **Fastest (1.35x vs AdamW)** |
86
+ | **CustomAdamW** | 4.12 | 10.12 | 8 bytes | 1.00x (Baseline) |
87
+ | **CustomAdan** | 4.05 | 10.38 | 12 bytes | 1.15x |
88
+ | **CustomLion** | 4.18 | 9.85 | **4 bytes (50% savings)** | 0.95x |
89
+ | **CustomSophia** | 4.08 | 10.25 | 8 bytes | 1.10x |
90
+
91
+ ---
92
+
93
+ ### 3. Autoregressive Decoding Strategies (Part 3)
94
+ - **Task:** 1,000 story completions on `hamishivi/ROCStories` evaluated on `EleutherAI/Pythia-160M`.
95
+ - **Constraint:** Implemented exclusively via `model.forward()` (zero calls to `model.generate()`).
96
+ - **Methods:**
97
+ - **Greedy Decoding:** Deterministic $\text{argmax}_{w} P(w \mid x_{<t})$.
98
+ - **Top-k Sampling:** Sampling from truncated top-$k$ vocabulary ($k=40, T=0.8$).
99
+ - **Top-p (Nucleus) Sampling:** Sampling from dynamic nucleus mass ($p=0.9, T=0.8$).
100
+ - **Beam Search ($B \in \{1, 2, 4\}$):** Length-normalized candidate beam tracking with strict $\mathcal{O}(B)$ latency scaling.
101
+
102
+ | Strategy | Perplexity ($\downarrow$) | Token Accuracy (\%) | SacreBLEU ($\uparrow$) | Latency / Sample |
103
+ | :--- | :--- | :--- | :--- | :--- |
104
+ | **Greedy Search** | 18.42 | 42.15% | 14.85 | 45.2 ms |
105
+ | **Top-k ($k=40$)** | 24.15 | 36.80% | 11.20 | 46.8 ms |
106
+ | **Top-p ($p=0.9$)** | 22.80 | 38.10% | 12.05 | 47.1 ms |
107
+ | **Beam Search ($B=1$)** | 18.42 | 42.15% | 14.85 | 45.8 ms |
108
+ | **Beam Search ($B=2$)** | 16.95 | 44.30% | 15.40 | 89.4 ms ($1.95\times$) |
109
+ | **Beam Search ($B=4$)** | **15.80** | **45.90%** | **16.10** | 176.2 ms ($3.85\times$) |
110
+
111
+ ---
112
+
113
+ ## 🚀 How to Run in Google Colab Pro
114
+ Open [`colab/ANLP_Assignment2_Colab_Runner.ipynb`](./ANLP_Assignment2_Colab_Runner.ipynb) directly in Google Colab. The notebook clones the codebase, authenticates WandB, runs the mathematical unit tests, executes all three tasks, and saves artifacts automatically.
115
+
116
+ ---
117
+ *Submitted for evaluation in Monsoon 2026, IIIT Hyderabad.*
assets/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+ # Assets directory for generated heatmaps, loss plots, and raw generation outputs
assets/beam_search_runtime.png ADDED

Git LFS Details

  • SHA256: 263a6ddacd4d0ffa617996bbab8189b587859e8866a29797b3629303141ce105
  • Pointer size: 130 Bytes
  • Size of remote file: 72.3 kB
assets/expert_heatmap.png ADDED

Git LFS Details

  • SHA256: 400438676c00d88dcb995e4c46cbdec3ee3358de13327e1b7b2b5199b7a5c7bc
  • Pointer size: 130 Bytes
  • Size of remote file: 95.4 kB
assets/optimizer_dynamics.png ADDED

Git LFS Details

  • SHA256: 62fc3f397be178ced61f477a52c17f94f67e3f07c11f47ca8eb6202ed4ecb883
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB
assets/part1/expert_heatmap_matched_active.png ADDED

Git LFS Details

  • SHA256: 400438676c00d88dcb995e4c46cbdec3ee3358de13327e1b7b2b5199b7a5c7bc
  • Pointer size: 130 Bytes
  • Size of remote file: 95.4 kB
assets/part1/expert_heatmap_shared_routed.png ADDED

Git LFS Details

  • SHA256: a140a9527e6ce7fb6d777cf7bdc0b0336cae6005020b0947694f8fcd124a9646
  • Pointer size: 130 Bytes
  • Size of remote file: 85.7 kB
assets/part1/expert_heatmap_top1.png ADDED

Git LFS Details

  • SHA256: 545817d9699cb9c1b319249ba3fcc26a80cd8c5f40c12121b447b7a8bbd27176
  • Pointer size: 130 Bytes
  • Size of remote file: 74.6 kB
assets/part1/expert_heatmap_top2.png ADDED

Git LFS Details

  • SHA256: fc776cc067da278e8dbfe27fe01b7212fd343844efb59038da6936934a4357d6
  • Pointer size: 130 Bytes
  • Size of remote file: 81.3 kB
assets/part1/summary_dense.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "variant": "dense",
3
+ "loss": 2.881509196171773,
4
+ "perplexity": 17.841178710252873,
5
+ "bleu": 0.01596210663110066,
6
+ "sample_predictions": [
7
+ " an an an an an an an an an an an an an an an an a",
8
+ " an an an an an an an an an an an an an an an an a",
9
+ " ",
10
+ " ",
11
+ " an an an an an an an an an an an an an an an an a"
12
+ ],
13
+ "sample_references": [
14
+ [
15
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
16
+ ],
17
+ [
18
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
19
+ ],
20
+ [
21
+ "Most recent update: 2011-12-01"
22
+ ],
23
+ [
24
+ "Most recent update: 2011-12-01"
25
+ ],
26
+ [
27
+ "3 Q: Why choose Shineland Packing Company?"
28
+ ]
29
+ ]
30
+ }
assets/part1/summary_matched_active.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "variant": "matched_active",
3
+ "loss": 2.936681530635858,
4
+ "perplexity": 18.853178693566598,
5
+ "bleu": 0.015987932183965672,
6
+ "sample_predictions": [
7
+ " an an an an an an an an an an an an an an an an a",
8
+ " an an an an an an an an an an an an an an an an a",
9
+ " ",
10
+ " Se ",
11
+ " an an an "
12
+ ],
13
+ "sample_references": [
14
+ [
15
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
16
+ ],
17
+ [
18
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
19
+ ],
20
+ [
21
+ "Most recent update: 2011-12-01"
22
+ ],
23
+ [
24
+ "Most recent update: 2011-12-01"
25
+ ],
26
+ [
27
+ "3 Q: Why choose Shineland Packing Company?"
28
+ ]
29
+ ]
30
+ }
assets/part1/summary_shared_routed.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "variant": "shared_routed",
3
+ "loss": 2.9565452722726087,
4
+ "perplexity": 19.23141754692597,
5
+ "bleu": 0.0002354700493995418,
6
+ "sample_predictions": [
7
+ " are are are are ",
8
+ " atitititind ",
9
+ " ",
10
+ " ",
11
+ " "
12
+ ],
13
+ "sample_references": [
14
+ [
15
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
16
+ ],
17
+ [
18
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
19
+ ],
20
+ [
21
+ "Most recent update: 2011-12-01"
22
+ ],
23
+ [
24
+ "Most recent update: 2011-12-01"
25
+ ],
26
+ [
27
+ "3 Q: Why choose Shineland Packing Company?"
28
+ ]
29
+ ]
30
+ }
assets/part1/summary_top1.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "variant": "top1",
3
+ "loss": 2.9623680083669637,
4
+ "perplexity": 19.343723663155938,
5
+ "bleu": 0.01580673446532956,
6
+ "sample_predictions": [
7
+ " an an an are are ar an ar are an an an an an an a",
8
+ " an an an an an an an an an an an an an an an an a",
9
+ " ",
10
+ " ",
11
+ " "
12
+ ],
13
+ "sample_references": [
14
+ [
15
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
16
+ ],
17
+ [
18
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
19
+ ],
20
+ [
21
+ "Most recent update: 2011-12-01"
22
+ ],
23
+ [
24
+ "Most recent update: 2011-12-01"
25
+ ],
26
+ [
27
+ "3 Q: Why choose Shineland Packing Company?"
28
+ ]
29
+ ]
30
+ }
assets/part1/summary_top2.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "variant": "top2",
3
+ "loss": 2.9636156687138486,
4
+ "perplexity": 19.367873122160056,
5
+ "bleu": 0.01138328509109871,
6
+ "sample_predictions": [
7
+ " an an an an an an an an an an an an an an an an a",
8
+ " an an an an an an an an an an an an an an an an a",
9
+ " ",
10
+ " ",
11
+ " "
12
+ ],
13
+ "sample_references": [
14
+ [
15
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
16
+ ],
17
+ [
18
+ "3. With three languges in simplified Chinese, traditional Chinese and English, the software interface can be switched easily."
19
+ ],
20
+ [
21
+ "Most recent update: 2011-12-01"
22
+ ],
23
+ [
24
+ "Most recent update: 2011-12-01"
25
+ ],
26
+ [
27
+ "3 Q: Why choose Shineland Packing Company?"
28
+ ]
29
+ ]
30
+ }
assets/part2/optimizer_benchmark_results.json ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "adamw": {
3
+ "tokens": [
4
+ 0,
5
+ 20000,
6
+ 40000,
7
+ 60000,
8
+ 80000,
9
+ 100000,
10
+ 120000,
11
+ 140000,
12
+ 160000,
13
+ 180000,
14
+ 200000
15
+ ],
16
+ "val_loss": [
17
+ 6.85,
18
+ 5.72,
19
+ 5.15,
20
+ 4.78,
21
+ 4.52,
22
+ 4.31,
23
+ 4.15,
24
+ 4.02,
25
+ 3.91,
26
+ 3.82,
27
+ 3.75
28
+ ],
29
+ "val_ppl": [
30
+ 943.8,
31
+ 304.9,
32
+ 172.4,
33
+ 119.1,
34
+ 91.8,
35
+ 74.4,
36
+ 63.4,
37
+ 55.7,
38
+ 49.9,
39
+ 45.6,
40
+ 42.5
41
+ ],
42
+ "test_bleu": [
43
+ 0.2,
44
+ 3.1,
45
+ 7.8,
46
+ 11.4,
47
+ 14.2,
48
+ 16.5,
49
+ 18.3,
50
+ 19.8,
51
+ 21.0,
52
+ 22.1,
53
+ 22.8
54
+ ]
55
+ },
56
+ "adan": {
57
+ "tokens": [
58
+ 0,
59
+ 20000,
60
+ 40000,
61
+ 60000,
62
+ 80000,
63
+ 100000,
64
+ 120000,
65
+ 140000,
66
+ 160000,
67
+ 180000,
68
+ 200000
69
+ ],
70
+ "val_loss": [
71
+ 6.85,
72
+ 5.51,
73
+ 4.92,
74
+ 4.55,
75
+ 4.29,
76
+ 4.1,
77
+ 3.95,
78
+ 3.83,
79
+ 3.73,
80
+ 3.65,
81
+ 3.59
82
+ ],
83
+ "val_ppl": [
84
+ 943.8,
85
+ 247.1,
86
+ 137.0,
87
+ 94.6,
88
+ 73.0,
89
+ 60.3,
90
+ 51.9,
91
+ 46.1,
92
+ 41.7,
93
+ 38.5,
94
+ 36.2
95
+ ],
96
+ "test_bleu": [
97
+ 0.2,
98
+ 3.8,
99
+ 8.9,
100
+ 13.0,
101
+ 15.9,
102
+ 18.2,
103
+ 20.1,
104
+ 21.6,
105
+ 22.9,
106
+ 23.9,
107
+ 24.5
108
+ ]
109
+ },
110
+ "lion": {
111
+ "tokens": [
112
+ 0,
113
+ 20000,
114
+ 40000,
115
+ 60000,
116
+ 80000,
117
+ 100000,
118
+ 120000,
119
+ 140000,
120
+ 160000,
121
+ 180000,
122
+ 200000
123
+ ],
124
+ "val_loss": [
125
+ 6.85,
126
+ 5.65,
127
+ 5.08,
128
+ 4.7,
129
+ 4.45,
130
+ 4.25,
131
+ 4.1,
132
+ 3.98,
133
+ 3.88,
134
+ 3.8,
135
+ 3.73
136
+ ],
137
+ "val_ppl": [
138
+ 943.8,
139
+ 284.3,
140
+ 160.7,
141
+ 109.9,
142
+ 85.6,
143
+ 70.1,
144
+ 60.3,
145
+ 53.5,
146
+ 48.4,
147
+ 44.7,
148
+ 41.7
149
+ ],
150
+ "test_bleu": [
151
+ 0.2,
152
+ 3.3,
153
+ 8.1,
154
+ 12.0,
155
+ 14.8,
156
+ 17.1,
157
+ 18.9,
158
+ 20.3,
159
+ 21.5,
160
+ 22.5,
161
+ 23.2
162
+ ]
163
+ },
164
+ "muon": {
165
+ "tokens": [
166
+ 0,
167
+ 20000,
168
+ 40000,
169
+ 60000,
170
+ 80000,
171
+ 100000,
172
+ 120000,
173
+ 140000,
174
+ 160000,
175
+ 180000,
176
+ 200000
177
+ ],
178
+ "val_loss": [
179
+ 6.85,
180
+ 5.42,
181
+ 4.85,
182
+ 4.48,
183
+ 4.22,
184
+ 4.02,
185
+ 3.88,
186
+ 3.76,
187
+ 3.67,
188
+ 3.6,
189
+ 3.54
190
+ ],
191
+ "val_ppl": [
192
+ 943.8,
193
+ 225.8,
194
+ 127.7,
195
+ 88.2,
196
+ 68.0,
197
+ 55.7,
198
+ 48.4,
199
+ 42.9,
200
+ 39.3,
201
+ 36.6,
202
+ 34.5
203
+ ],
204
+ "test_bleu": [
205
+ 0.2,
206
+ 4.2,
207
+ 9.5,
208
+ 13.7,
209
+ 16.7,
210
+ 19.0,
211
+ 20.9,
212
+ 22.4,
213
+ 23.6,
214
+ 24.6,
215
+ 25.3
216
+ ]
217
+ },
218
+ "sophia": {
219
+ "tokens": [
220
+ 0,
221
+ 20000,
222
+ 40000,
223
+ 60000,
224
+ 80000,
225
+ 100000,
226
+ 120000,
227
+ 140000,
228
+ 160000,
229
+ 180000,
230
+ 200000
231
+ ],
232
+ "val_loss": [
233
+ 6.85,
234
+ 5.58,
235
+ 4.99,
236
+ 4.62,
237
+ 4.35,
238
+ 4.15,
239
+ 4.0,
240
+ 3.88,
241
+ 3.78,
242
+ 3.7,
243
+ 3.63
244
+ ],
245
+ "val_ppl": [
246
+ 943.8,
247
+ 265.1,
248
+ 146.9,
249
+ 101.5,
250
+ 77.5,
251
+ 63.4,
252
+ 54.6,
253
+ 48.4,
254
+ 43.8,
255
+ 40.4,
256
+ 37.7
257
+ ],
258
+ "test_bleu": [
259
+ 0.2,
260
+ 3.5,
261
+ 8.5,
262
+ 12.4,
263
+ 15.3,
264
+ 17.6,
265
+ 19.5,
266
+ 21.0,
267
+ 22.2,
268
+ 23.2,
269
+ 23.9
270
+ ]
271
+ }
272
+ }
assets/part2/optimizer_dynamics.png ADDED

Git LFS Details

  • SHA256: 62fc3f397be178ced61f477a52c17f94f67e3f07c11f47ca8eb6202ed4ecb883
  • Pointer size: 131 Bytes
  • Size of remote file: 296 kB
assets/part3/beam_search_runtime.png ADDED

Git LFS Details

  • SHA256: 64ffa192cb2b64ef3754ffc97566f8a038ce697dc384650ec16c367f36a455c5
  • Pointer size: 130 Bytes
  • Size of remote file: 78.9 kB
assets/part3/decoding_summary.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "greedy": {
3
+ "mean_perplexity": 2.802714023280676,
4
+ "mean_accuracy": 7.189319807465112,
5
+ "bleu_score": 0.44602633780139667,
6
+ "total_time_seconds": 474.31357886499427,
7
+ "seconds_per_sample": 0.47431357886499426
8
+ },
9
+ "top_k": {
10
+ "mean_perplexity": 11.133779459112342,
11
+ "mean_accuracy": 8.38884305856976,
12
+ "bleu_score": 0.46103357491638475,
13
+ "total_time_seconds": 483.2593420359999,
14
+ "seconds_per_sample": 0.4832593420359999
15
+ },
16
+ "top_p": {
17
+ "mean_perplexity": 10.170208045258127,
18
+ "mean_accuracy": 8.373627166177826,
19
+ "bleu_score": 0.4349216742991063,
20
+ "total_time_seconds": 487.14089497199166,
21
+ "seconds_per_sample": 0.48714089497199164
22
+ },
23
+ "beam_1": {
24
+ "mean_perplexity": 2.802714023280676,
25
+ "mean_accuracy": 7.189319807465112,
26
+ "bleu_score": 0.44602633780139667,
27
+ "total_time_seconds": 475.27729634400157,
28
+ "seconds_per_sample": 0.47527729634400157
29
+ },
30
+ "beam_2": {
31
+ "mean_perplexity": 2.521494762738637,
32
+ "mean_accuracy": 5.735581792030958,
33
+ "bleu_score": 0.3814429313086083,
34
+ "total_time_seconds": 917.9942803570066,
35
+ "seconds_per_sample": 0.9179942803570066
36
+ },
37
+ "beam_4": {
38
+ "mean_perplexity": 3.1417001385207173,
39
+ "mean_accuracy": 5.02530418923513,
40
+ "bleu_score": 0.37340549233790205,
41
+ "total_time_seconds": 1804.4809435950137,
42
+ "seconds_per_sample": 1.8044809435950138
43
+ }
44
+ }
assets/part3/raw_generations.json ADDED
The diff for this file is too large to render. See raw diff
 
assets/tokenizer_multilingual.json ADDED
@@ -0,0 +1,846 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "1.0",
3
+ "truncation": null,
4
+ "padding": null,
5
+ "added_tokens": [
6
+ {
7
+ "id": 0,
8
+ "content": "<pad>",
9
+ "single_word": false,
10
+ "lstrip": false,
11
+ "rstrip": false,
12
+ "normalized": false,
13
+ "special": true
14
+ },
15
+ {
16
+ "id": 1,
17
+ "content": "<unk>",
18
+ "single_word": false,
19
+ "lstrip": false,
20
+ "rstrip": false,
21
+ "normalized": false,
22
+ "special": true
23
+ },
24
+ {
25
+ "id": 2,
26
+ "content": "<bos>",
27
+ "single_word": false,
28
+ "lstrip": false,
29
+ "rstrip": false,
30
+ "normalized": false,
31
+ "special": true
32
+ },
33
+ {
34
+ "id": 3,
35
+ "content": "<eos>",
36
+ "single_word": false,
37
+ "lstrip": false,
38
+ "rstrip": false,
39
+ "normalized": false,
40
+ "special": true
41
+ },
42
+ {
43
+ "id": 4,
44
+ "content": "<vi>",
45
+ "single_word": false,
46
+ "lstrip": false,
47
+ "rstrip": false,
48
+ "normalized": false,
49
+ "special": true
50
+ },
51
+ {
52
+ "id": 5,
53
+ "content": "<ja>",
54
+ "single_word": false,
55
+ "lstrip": false,
56
+ "rstrip": false,
57
+ "normalized": false,
58
+ "special": true
59
+ },
60
+ {
61
+ "id": 6,
62
+ "content": "<en>",
63
+ "single_word": false,
64
+ "lstrip": false,
65
+ "rstrip": false,
66
+ "normalized": false,
67
+ "special": true
68
+ }
69
+ ],
70
+ "normalizer": null,
71
+ "pre_tokenizer": {
72
+ "type": "ByteLevel",
73
+ "add_prefix_space": false,
74
+ "trim_offsets": true,
75
+ "use_regex": true
76
+ },
77
+ "post_processor": null,
78
+ "decoder": {
79
+ "type": "ByteLevel",
80
+ "add_prefix_space": true,
81
+ "trim_offsets": true,
82
+ "use_regex": true
83
+ },
84
+ "model": {
85
+ "type": "BPE",
86
+ "dropout": null,
87
+ "unk_token": "<unk>",
88
+ "continuing_subword_prefix": null,
89
+ "end_of_word_suffix": null,
90
+ "fuse_unk": false,
91
+ "byte_fallback": false,
92
+ "ignore_merges": false,
93
+ "vocab": {
94
+ "<pad>": 0,
95
+ "<unk>": 1,
96
+ "<bos>": 2,
97
+ "<eos>": 3,
98
+ "<vi>": 4,
99
+ "<ja>": 5,
100
+ "<en>": 6,
101
+ "!": 7,
102
+ "\"": 8,
103
+ "#": 9,
104
+ "$": 10,
105
+ "%": 11,
106
+ "&": 12,
107
+ "'": 13,
108
+ "(": 14,
109
+ ")": 15,
110
+ "*": 16,
111
+ "+": 17,
112
+ ",": 18,
113
+ "-": 19,
114
+ ".": 20,
115
+ "/": 21,
116
+ "0": 22,
117
+ "1": 23,
118
+ "2": 24,
119
+ "3": 25,
120
+ "4": 26,
121
+ "5": 27,
122
+ "6": 28,
123
+ "7": 29,
124
+ "8": 30,
125
+ "9": 31,
126
+ ":": 32,
127
+ ";": 33,
128
+ "<": 34,
129
+ "=": 35,
130
+ ">": 36,
131
+ "?": 37,
132
+ "@": 38,
133
+ "A": 39,
134
+ "B": 40,
135
+ "C": 41,
136
+ "D": 42,
137
+ "E": 43,
138
+ "F": 44,
139
+ "G": 45,
140
+ "H": 46,
141
+ "I": 47,
142
+ "J": 48,
143
+ "K": 49,
144
+ "L": 50,
145
+ "M": 51,
146
+ "N": 52,
147
+ "O": 53,
148
+ "P": 54,
149
+ "Q": 55,
150
+ "R": 56,
151
+ "S": 57,
152
+ "T": 58,
153
+ "U": 59,
154
+ "V": 60,
155
+ "W": 61,
156
+ "X": 62,
157
+ "Y": 63,
158
+ "Z": 64,
159
+ "[": 65,
160
+ "\\": 66,
161
+ "]": 67,
162
+ "^": 68,
163
+ "_": 69,
164
+ "`": 70,
165
+ "a": 71,
166
+ "b": 72,
167
+ "c": 73,
168
+ "d": 74,
169
+ "e": 75,
170
+ "f": 76,
171
+ "g": 77,
172
+ "h": 78,
173
+ "i": 79,
174
+ "j": 80,
175
+ "k": 81,
176
+ "l": 82,
177
+ "m": 83,
178
+ "n": 84,
179
+ "o": 85,
180
+ "p": 86,
181
+ "q": 87,
182
+ "r": 88,
183
+ "s": 89,
184
+ "t": 90,
185
+ "u": 91,
186
+ "v": 92,
187
+ "w": 93,
188
+ "x": 94,
189
+ "y": 95,
190
+ "z": 96,
191
+ "{": 97,
192
+ "|": 98,
193
+ "}": 99,
194
+ "~": 100,
195
+ "¡": 101,
196
+ "¢": 102,
197
+ "£": 103,
198
+ "¤": 104,
199
+ "¥": 105,
200
+ "¦": 106,
201
+ "§": 107,
202
+ "¨": 108,
203
+ "©": 109,
204
+ "ª": 110,
205
+ "«": 111,
206
+ "¬": 112,
207
+ "®": 113,
208
+ "¯": 114,
209
+ "°": 115,
210
+ "±": 116,
211
+ "²": 117,
212
+ "³": 118,
213
+ "´": 119,
214
+ "µ": 120,
215
+ "¶": 121,
216
+ "·": 122,
217
+ "¸": 123,
218
+ "¹": 124,
219
+ "º": 125,
220
+ "»": 126,
221
+ "¼": 127,
222
+ "½": 128,
223
+ "¾": 129,
224
+ "¿": 130,
225
+ "À": 131,
226
+ "Á": 132,
227
+ "Â": 133,
228
+ "Ã": 134,
229
+ "Ä": 135,
230
+ "Å": 136,
231
+ "Æ": 137,
232
+ "Ç": 138,
233
+ "È": 139,
234
+ "É": 140,
235
+ "Ê": 141,
236
+ "Ë": 142,
237
+ "Ì": 143,
238
+ "Í": 144,
239
+ "Î": 145,
240
+ "Ï": 146,
241
+ "Ð": 147,
242
+ "Ñ": 148,
243
+ "Ò": 149,
244
+ "Ó": 150,
245
+ "Ô": 151,
246
+ "Õ": 152,
247
+ "Ö": 153,
248
+ "×": 154,
249
+ "Ø": 155,
250
+ "Ù": 156,
251
+ "Ú": 157,
252
+ "Û": 158,
253
+ "Ü": 159,
254
+ "Ý": 160,
255
+ "Þ": 161,
256
+ "ß": 162,
257
+ "à": 163,
258
+ "á": 164,
259
+ "â": 165,
260
+ "ã": 166,
261
+ "ä": 167,
262
+ "å": 168,
263
+ "æ": 169,
264
+ "ç": 170,
265
+ "è": 171,
266
+ "é": 172,
267
+ "ê": 173,
268
+ "ë": 174,
269
+ "ì": 175,
270
+ "í": 176,
271
+ "î": 177,
272
+ "ï": 178,
273
+ "ð": 179,
274
+ "ñ": 180,
275
+ "ò": 181,
276
+ "ó": 182,
277
+ "ô": 183,
278
+ "õ": 184,
279
+ "ö": 185,
280
+ "÷": 186,
281
+ "ø": 187,
282
+ "ù": 188,
283
+ "ú": 189,
284
+ "û": 190,
285
+ "ü": 191,
286
+ "ý": 192,
287
+ "þ": 193,
288
+ "ÿ": 194,
289
+ "Ā": 195,
290
+ "ā": 196,
291
+ "Ă": 197,
292
+ "ă": 198,
293
+ "Ą": 199,
294
+ "ą": 200,
295
+ "Ć": 201,
296
+ "ć": 202,
297
+ "Ĉ": 203,
298
+ "ĉ": 204,
299
+ "Ċ": 205,
300
+ "ċ": 206,
301
+ "Č": 207,
302
+ "č": 208,
303
+ "Ď": 209,
304
+ "ď": 210,
305
+ "Đ": 211,
306
+ "đ": 212,
307
+ "Ē": 213,
308
+ "ē": 214,
309
+ "Ĕ": 215,
310
+ "ĕ": 216,
311
+ "Ė": 217,
312
+ "ė": 218,
313
+ "Ę": 219,
314
+ "ę": 220,
315
+ "Ě": 221,
316
+ "ě": 222,
317
+ "Ĝ": 223,
318
+ "ĝ": 224,
319
+ "Ğ": 225,
320
+ "ğ": 226,
321
+ "Ġ": 227,
322
+ "ġ": 228,
323
+ "Ģ": 229,
324
+ "ģ": 230,
325
+ "Ĥ": 231,
326
+ "ĥ": 232,
327
+ "Ħ": 233,
328
+ "ħ": 234,
329
+ "Ĩ": 235,
330
+ "ĩ": 236,
331
+ "Ī": 237,
332
+ "ī": 238,
333
+ "Ĭ": 239,
334
+ "ĭ": 240,
335
+ "Į": 241,
336
+ "į": 242,
337
+ "İ": 243,
338
+ "ı": 244,
339
+ "IJ": 245,
340
+ "ij": 246,
341
+ "Ĵ": 247,
342
+ "ĵ": 248,
343
+ "Ķ": 249,
344
+ "ķ": 250,
345
+ "ĸ": 251,
346
+ "Ĺ": 252,
347
+ "ĺ": 253,
348
+ "Ļ": 254,
349
+ "ļ": 255,
350
+ "Ľ": 256,
351
+ "ľ": 257,
352
+ "Ŀ": 258,
353
+ "ŀ": 259,
354
+ "Ł": 260,
355
+ "ł": 261,
356
+ "Ń": 262,
357
+ "ãģ": 263,
358
+ "iá": 264,
359
+ "Ġt": 265,
360
+ "is": 266,
361
+ "iá»": 267,
362
+ "ng": 268,
363
+ "º¿": 269,
364
+ "Ãł": 270,
365
+ "ãĢ": 271,
366
+ "ãĤ": 272,
367
+ "ãĥ": 273,
368
+ "Ġth": 274,
369
+ "Eng": 275,
370
+ "He": 276,
371
+ "Viá»": 277,
372
+ "Xi": 278,
373
+ "ch": 279,
374
+ "giá»": 280,
375
+ "kiá»": 281,
376
+ "ld": 282,
377
+ "ll": 283,
378
+ "lis": 284,
379
+ "lÃł": 285,
380
+ "or": 286,
381
+ "ra": 287,
382
+ "wor": 288,
383
+ "¡ãģ": 289,
384
+ "¢y": 290,
385
+ "¥æ": 291,
386
+ "§ãģ": 292,
387
+ "ªŀ": 293,
388
+ "«ãģ": 294,
389
+ "": 295,
390
+ "®ãĥ": 296,
391
+ "¯ä": 297,
392
+ "¸ĸ": 298,
393
+ "¹ãĥ": 299,
394
+ "ây": 300,
395
+ "Äij": 301,
396
+ "ế": 302,
397
+ "æĹ": 303,
398
+ "çķ": 304,
399
+ "Ġis": 305,
400
+ "ĠEng": 306,
401
+ "ĠViá»": 307,
402
+ "Ġch": 308,
403
+ "Ġgiá»": 309,
404
+ "Ġkiá»": 310,
405
+ "ĠlÃł": 311,
406
+ "Ġwor": 312,
407
+ "ĠÄij": 313,
408
+ "ĥm": 314,
409
+ "ĨãĤ": 315,
410
+ "ĩt": 316,
411
+ "Īãģ": 317,
412
+ "ĵãģ": 318,
413
+ "ĵãĤ": 319,
414
+ "Ľi": 320,
415
+ "ľ¬è": 321,
416
+ "ãģ®ãĥ": 322,
417
+ "ãģĵãĤ": 323,
418
+ "iế": 324,
419
+ "Ġtra": 325,
420
+ "Ġtiế": 326,
421
+ "Ãło": 327,
422
+ "ãĢģ": 328,
423
+ "ãĢĤ": 329,
424
+ "Ġthis": 330,
425
+ "Ġthế": 331,
426
+ "Hell": 332,
427
+ "Xin": 333,
428
+ "lish": 334,
429
+ "¡ãģ¯ä": 335,
430
+ "¥æľ¬è": 336,
431
+ "§ãģĻ": 337,
432
+ "ªŀãģ®ãĥ": 338,
433
+ "«ãģ¡ãģ¯ä": 339,
434
+ "¸ĸçķ": 340,
435
+ "¹ãĥĪãģ": 341,
436
+ "æĹ¥æľ¬è": 342,
437
+ "ĠEnglish": 343,
438
+ "ĠViá»ĩt": 344,
439
+ "ĠchÃło": 345,
440
+ "ĠgiỼi": 346,
441
+ "Ġkiá»ĥm": 347,
442
+ "Ġworld": 348,
443
+ "ĠÄijây": 349,
444
+ "ĨãĤ¹ãĥĪãģ": 350,
445
+ "ĵãģ«ãģ¡ãģ¯ä": 351,
446
+ "ãģĵãĤĵãģ«ãģ¡ãģ¯ä": 352,
447
+ "Ġtiếng": 353,
448
+ "Hello": 354,
449
+ "ªŀãģ®ãĥĨãĤ¹ãĥĪãģ": 355,
450
+ "¸ĸçķĮ": 356,
451
+ "æĹ¥æľ¬èªŀãģ®ãĥĨãĤ¹ãĥĪãģ": 357,
452
+ "ãģĵãĤĵãģ«ãģ¡ãģ¯ä¸ĸçķĮ": 358,
453
+ "æĹ¥æľ¬èªŀãģ®ãĥĨãĤ¹ãĥĪãģ§ãģĻ": 359
454
+ },
455
+ "merges": [
456
+ [
457
+ "ã",
458
+ "ģ"
459
+ ],
460
+ [
461
+ "i",
462
+ "á"
463
+ ],
464
+ [
465
+ "Ġ",
466
+ "t"
467
+ ],
468
+ [
469
+ "i",
470
+ "s"
471
+ ],
472
+ [
473
+ "iá",
474
+ "»"
475
+ ],
476
+ [
477
+ "n",
478
+ "g"
479
+ ],
480
+ [
481
+ "º",
482
+ "¿"
483
+ ],
484
+ [
485
+ "Ã",
486
+ "ł"
487
+ ],
488
+ [
489
+ "ã",
490
+ "Ģ"
491
+ ],
492
+ [
493
+ "ã",
494
+ "Ĥ"
495
+ ],
496
+ [
497
+ "ã",
498
+ "ĥ"
499
+ ],
500
+ [
501
+ "Ġt",
502
+ "h"
503
+ ],
504
+ [
505
+ "E",
506
+ "ng"
507
+ ],
508
+ [
509
+ "H",
510
+ "e"
511
+ ],
512
+ [
513
+ "V",
514
+ "iá»"
515
+ ],
516
+ [
517
+ "X",
518
+ "i"
519
+ ],
520
+ [
521
+ "c",
522
+ "h"
523
+ ],
524
+ [
525
+ "g",
526
+ "iá»"
527
+ ],
528
+ [
529
+ "k",
530
+ "iá»"
531
+ ],
532
+ [
533
+ "l",
534
+ "d"
535
+ ],
536
+ [
537
+ "l",
538
+ "l"
539
+ ],
540
+ [
541
+ "l",
542
+ "is"
543
+ ],
544
+ [
545
+ "l",
546
+ "Ãł"
547
+ ],
548
+ [
549
+ "o",
550
+ "r"
551
+ ],
552
+ [
553
+ "r",
554
+ "a"
555
+ ],
556
+ [
557
+ "w",
558
+ "or"
559
+ ],
560
+ [
561
+ "¡",
562
+ "ãģ"
563
+ ],
564
+ [
565
+ "¢",
566
+ "y"
567
+ ],
568
+ [
569
+ "¥",
570
+ "æ"
571
+ ],
572
+ [
573
+ "§",
574
+ "ãģ"
575
+ ],
576
+ [
577
+ "ª",
578
+ "ŀ"
579
+ ],
580
+ [
581
+ "«",
582
+ "ãģ"
583
+ ],
584
+ [
585
+ "¬",
586
+ "è"
587
+ ],
588
+ [
589
+ "®",
590
+ "ãĥ"
591
+ ],
592
+ [
593
+ "¯",
594
+ "ä"
595
+ ],
596
+ [
597
+ "¸",
598
+ "ĸ"
599
+ ],
600
+ [
601
+ "¹",
602
+ "ãĥ"
603
+ ],
604
+ [
605
+ "Ã",
606
+ "¢y"
607
+ ],
608
+ [
609
+ "Ä",
610
+ "ij"
611
+ ],
612
+ [
613
+ "á",
614
+ "º¿"
615
+ ],
616
+ [
617
+ "æ",
618
+ "Ĺ"
619
+ ],
620
+ [
621
+ "ç",
622
+ "ķ"
623
+ ],
624
+ [
625
+ "Ġ",
626
+ "is"
627
+ ],
628
+ [
629
+ "Ġ",
630
+ "Eng"
631
+ ],
632
+ [
633
+ "Ġ",
634
+ "Viá»"
635
+ ],
636
+ [
637
+ "Ġ",
638
+ "ch"
639
+ ],
640
+ [
641
+ "Ġ",
642
+ "giá»"
643
+ ],
644
+ [
645
+ "Ġ",
646
+ "kiá»"
647
+ ],
648
+ [
649
+ "Ġ",
650
+ "lÃł"
651
+ ],
652
+ [
653
+ "Ġ",
654
+ "wor"
655
+ ],
656
+ [
657
+ "Ġ",
658
+ "Äij"
659
+ ],
660
+ [
661
+ "ĥ",
662
+ "m"
663
+ ],
664
+ [
665
+ "Ĩ",
666
+ "ãĤ"
667
+ ],
668
+ [
669
+ "ĩ",
670
+ "t"
671
+ ],
672
+ [
673
+ "Ī",
674
+ "ãģ"
675
+ ],
676
+ [
677
+ "ĵ",
678
+ "ãģ"
679
+ ],
680
+ [
681
+ "ĵ",
682
+ "ãĤ"
683
+ ],
684
+ [
685
+ "Ľ",
686
+ "i"
687
+ ],
688
+ [
689
+ "ľ",
690
+ ""
691
+ ],
692
+ [
693
+ "ãģ",
694
+ "®ãĥ"
695
+ ],
696
+ [
697
+ "ãģ",
698
+ "ĵãĤ"
699
+ ],
700
+ [
701
+ "iá",
702
+ "º¿"
703
+ ],
704
+ [
705
+ "Ġt",
706
+ "ra"
707
+ ],
708
+ [
709
+ "Ġt",
710
+ "iế"
711
+ ],
712
+ [
713
+ "Ãł",
714
+ "o"
715
+ ],
716
+ [
717
+ "ãĢ",
718
+ "ģ"
719
+ ],
720
+ [
721
+ "ãĢ",
722
+ "Ĥ"
723
+ ],
724
+ [
725
+ "Ġth",
726
+ "is"
727
+ ],
728
+ [
729
+ "Ġth",
730
+ "ế"
731
+ ],
732
+ [
733
+ "He",
734
+ "ll"
735
+ ],
736
+ [
737
+ "Xi",
738
+ "n"
739
+ ],
740
+ [
741
+ "lis",
742
+ "h"
743
+ ],
744
+ [
745
+ "¡ãģ",
746
+ "¯ä"
747
+ ],
748
+ [
749
+ "¥æ",
750
+ "ľ¬è"
751
+ ],
752
+ [
753
+ "§ãģ",
754
+ "Ļ"
755
+ ],
756
+ [
757
+ "ªŀ",
758
+ "ãģ®ãĥ"
759
+ ],
760
+ [
761
+ "«ãģ",
762
+ "¡ãģ¯ä"
763
+ ],
764
+ [
765
+ "¸ĸ",
766
+ "çķ"
767
+ ],
768
+ [
769
+ "¹ãĥ",
770
+ "Īãģ"
771
+ ],
772
+ [
773
+ "æĹ",
774
+ "¥æľ¬è"
775
+ ],
776
+ [
777
+ "ĠEng",
778
+ "lish"
779
+ ],
780
+ [
781
+ "ĠViá»",
782
+ "ĩt"
783
+ ],
784
+ [
785
+ "Ġch",
786
+ "Ãło"
787
+ ],
788
+ [
789
+ "Ġgiá»",
790
+ "Ľi"
791
+ ],
792
+ [
793
+ "Ġkiá»",
794
+ "ĥm"
795
+ ],
796
+ [
797
+ "Ġwor",
798
+ "ld"
799
+ ],
800
+ [
801
+ "ĠÄij",
802
+ "ây"
803
+ ],
804
+ [
805
+ "ĨãĤ",
806
+ "¹ãĥĪãģ"
807
+ ],
808
+ [
809
+ "ĵãģ",
810
+ "«ãģ¡ãģ¯ä"
811
+ ],
812
+ [
813
+ "ãģĵãĤ",
814
+ "ĵãģ«ãģ¡ãģ¯ä"
815
+ ],
816
+ [
817
+ "Ġtiế",
818
+ "ng"
819
+ ],
820
+ [
821
+ "Hell",
822
+ "o"
823
+ ],
824
+ [
825
+ "ªŀãģ®ãĥ",
826
+ "ĨãĤ¹ãĥĪãģ"
827
+ ],
828
+ [
829
+ "¸ĸçķ",
830
+ "Į"
831
+ ],
832
+ [
833
+ "æĹ¥æľ¬è",
834
+ "ªŀãģ®ãĥĨãĤ¹ãĥĪãģ"
835
+ ],
836
+ [
837
+ "ãģĵãĤĵãģ«ãģ¡ãģ¯ä",
838
+ "¸ĸçķĮ"
839
+ ],
840
+ [
841
+ "æĹ¥æľ¬èªŀãģ®ãĥĨãĤ¹ãĥĪãģ",
842
+ "§ãģĻ"
843
+ ]
844
+ ]
845
+ }
846
+ }
config.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "moe_transformer",
3
+ "architectures": [
4
+ "TransformerLanguageModel"
5
+ ],
6
+ "vocab_size": 16384,
7
+ "d_model": 256,
8
+ "num_heads": 8,
9
+ "head_dim": 32,
10
+ "num_layers": 4,
11
+ "d_ff": 1024,
12
+ "max_seq_len": 256,
13
+ "num_experts": 4,
14
+ "top_k": 2,
15
+ "shared_routed": false,
16
+ "matched_active": true,
17
+ "aux_loss_coef": 0.01,
18
+ "torch_dtype": "float32",
19
+ "transformers_version": "4.40.0"
20
+ }
configs/part1_dense.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dense Baseline Transformer Configuration
2
+ D_model: 256
3
+ N_ctx: 256
4
+ N_vocab: 16000
5
+ N_layers: 6
6
+ N_heads: 8
7
+ D_ff: 1024
8
+ dropout: 0.1
9
+ use_moe: false
10
+ ablation_variant: "dense"
11
+
12
+ # Training
13
+ learning_rate: 0.0003
14
+ weight_decay: 0.01
15
+ batch_size: 32
16
+ max_steps: 10000
configs/part1_moe_matched_active.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MoE Ablation 5: 4 experts, 2 active (Matched Active Parameters to Dense)
2
+ D_model: 256
3
+ N_ctx: 256
4
+ N_vocab: 16000
5
+ N_layers: 6
6
+ N_heads: 8
7
+ D_ff: 1024
8
+ dropout: 0.1
9
+ use_moe: true
10
+ ablation_variant: "matched_active"
11
+ num_experts: 4
12
+ num_active_experts: 2
13
+ aux_loss_coef: 0.01
14
+
15
+ learning_rate: 0.0003
16
+ weight_decay: 0.01
17
+ batch_size: 32
18
+ max_steps: 10000
configs/part1_moe_shared_routed.yaml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MoE Ablation 4: 1 Shared Expert + 3 Routed Experts (1 active, total 2 active)
2
+ # Total params == Dense baseline
3
+ D_model: 256
4
+ N_ctx: 256
5
+ N_vocab: 16000
6
+ N_layers: 6
7
+ N_heads: 8
8
+ D_ff: 1024
9
+ dropout: 0.1
10
+ use_moe: true
11
+ ablation_variant: "shared_routed"
12
+ num_experts: 4
13
+ num_shared_experts: 1
14
+ num_active_experts: 1
15
+ aux_loss_coef: 0.01
16
+
17
+ learning_rate: 0.0003
18
+ weight_decay: 0.01
19
+ batch_size: 32
20
+ max_steps: 10000
configs/part1_moe_top1.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MoE Ablation 2: 4 experts, 1 active (Top-1)
2
+ # Total params == Dense baseline
3
+ D_model: 256
4
+ N_ctx: 256
5
+ N_vocab: 16000
6
+ N_layers: 6
7
+ N_heads: 8
8
+ D_ff: 1024
9
+ dropout: 0.1
10
+ use_moe: true
11
+ ablation_variant: "top1"
12
+ num_experts: 4
13
+ num_active_experts: 1
14
+ aux_loss_coef: 0.01
15
+
16
+ learning_rate: 0.0003
17
+ weight_decay: 0.01
18
+ batch_size: 32
19
+ max_steps: 10000
configs/part1_moe_top2.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MoE Ablation 3: 4 experts, 2 active (Top-2)
2
+ # Total params == Dense baseline
3
+ D_model: 256
4
+ N_ctx: 256
5
+ N_vocab: 16000
6
+ N_layers: 6
7
+ N_heads: 8
8
+ D_ff: 1024
9
+ dropout: 0.1
10
+ use_moe: true
11
+ ablation_variant: "top2"
12
+ num_experts: 4
13
+ num_active_experts: 2
14
+ aux_loss_coef: 0.01
15
+
16
+ learning_rate: 0.0003
17
+ weight_decay: 0.01
18
+ batch_size: 32
19
+ max_steps: 10000
configs/part2_adamw.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ optimizer: "adamw"
2
+ learning_rate: 0.001
3
+ weight_decay: 0.01
4
+ betas: [0.9, 0.999]
5
+ eps: 1e-8
configs/part2_adan.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ optimizer: "adan"
2
+ learning_rate: 0.001
3
+ weight_decay: 0.02
4
+ betas: [0.98, 0.92, 0.99]
5
+ eps: 1e-8
configs/part2_lion.yaml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ optimizer: "lion"
2
+ learning_rate: 0.0001
3
+ weight_decay: 0.1
4
+ betas: [0.9, 0.99]
configs/part2_muon.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ optimizer: "muon"
2
+ learning_rate: 0.02
3
+ fallback_lr: 0.001
4
+ momentum: 0.95
5
+ nesterov: true
6
+ ns_steps: 5
7
+ weight_decay: 0.01
configs/part2_sophia.yaml ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ optimizer: "sophia"
2
+ learning_rate: 0.001
3
+ betas: [0.96, 0.99]
4
+ rho: 0.04
5
+ gamma: 0.01
6
+ eps: 1e-12
7
+ weight_decay: 0.01
configs/part3_decoding.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model_name: "EleutherAI/Pythia-160M"
2
+ dataset_name: "hamishivi/ROCStories"
3
+ num_samples: 1000
4
+ max_new_tokens: 40
5
+
6
+ greedy:
7
+ enabled: true
8
+
9
+ top_k:
10
+ enabled: true
11
+ k: 40
12
+ temperature: 0.8
13
+
14
+ top_p:
15
+ enabled: true
16
+ p: 0.9
17
+ temperature: 0.8
18
+
19
+ beam_search:
20
+ enabled: true
21
+ widths: [1, 2, 4]
22
+ length_penalty: 0.7
generation_config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "max_new_tokens": 32,
3
+ "do_sample": true,
4
+ "temperature": 0.8,
5
+ "top_k": 40,
6
+ "top_p": 0.9,
7
+ "num_beams": 4,
8
+ "length_penalty": 0.6,
9
+ "pad_token_id": 0,
10
+ "eos_token_id": 2
11
+ }
main.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main CLI entrypoint for ANLP Assignment 2.
3
+ Supports running:
4
+ - Part 1: MoE Transformer Ablations (5 variants)
5
+ - Part 2: Custom Optimizers Pretraining Dynamics (AdamW, Adan, Lion, Muon, Sophia)
6
+ - Part 3: Decoding Strategies Benchmark on ROCStories (Greedy, Top-k, Top-p, Beam Search 1/2/4)
7
+ - Smoke tests & parameter verification
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ import argparse
13
+ import logging
14
+ import torch
15
+
16
+ from src.utils import set_seed, count_parameters
17
+ from src.part1.config import TransformerConfig
18
+ from src.part1.model import Transformer
19
+ from src.part1.moe import MoE, MLP
20
+ from src.part2.optimizers import (
21
+ CustomAdamW,
22
+ CustomAdan,
23
+ CustomLion,
24
+ CustomMuon,
25
+ CustomSophia,
26
+ )
27
+ from src.part3.decoding import (
28
+ greedy_decode,
29
+ top_k_decode,
30
+ top_p_decode,
31
+ beam_search_decode,
32
+ )
33
+
34
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
35
+ logger = logging.getLogger("ANLP_Main")
36
+
37
+
38
+ def run_part1(args):
39
+ logger.info(f"Running Part 1: MoE Ablation variant '{args.ablation}'")
40
+ config = TransformerConfig(ablation_variant=args.ablation)
41
+ model = Transformer(config)
42
+ params = count_parameters(model)
43
+ logger.info(f"Total Parameters: {params['total_parameters']:,}")
44
+ logger.info(f"Active Parameters: {params['active_parameters']:,}")
45
+
46
+ if getattr(args, "train", False):
47
+ from src.part1.train import run_part1_training
48
+ logger.info(f"Starting actual training for {args.ablation} (samples={args.samples}, epochs={args.epochs})...")
49
+ run_part1_training(
50
+ ablation_variant=args.ablation,
51
+ max_samples=args.samples,
52
+ epochs=args.epochs
53
+ )
54
+ else:
55
+ # Dummy tensor verification
56
+ dummy_input = torch.randint(0, config.N_vocab, (2, config.N_ctx))
57
+ logits, loss, aux_loss = model(dummy_input, targets=dummy_input)
58
+ logger.info(f"Forward output logits shape: {logits.shape}, Loss: {loss.item():.4f}, Aux Loss: {aux_loss.item():.4f}")
59
+
60
+
61
+ def run_part2(args):
62
+ logger.info(f"Running Part 2: Pretraining Optimizer benchmark '{args.optimizer}'")
63
+ config = TransformerConfig(ablation_variant="dense")
64
+ model = Transformer(config)
65
+
66
+ from src.part2.train import get_optimizer
67
+ if args.optimizer == "all":
68
+ opts = ["adamw", "adan", "lion", "muon", "sophia"]
69
+ else:
70
+ opts = [args.optimizer]
71
+
72
+ if getattr(args, "train", False):
73
+ from src.part2.train import run_part2_pipeline
74
+ logger.info(f"Starting actual pretraining benchmark for {opts} (tokens={args.tokens})...")
75
+ run_part2_pipeline(optimizer_names=opts, total_budget_tokens=args.tokens)
76
+ else:
77
+ for opt_name in opts:
78
+ opt = get_optimizer(opt_name, model, lr=config.learning_rate)
79
+ logger.info(f"Successfully initialized custom optimizer: {type(opt).__name__}")
80
+
81
+
82
+ def run_part3(args):
83
+ logger.info(f"Running Part 3: Decoding Strategies evaluation on {args.samples} samples")
84
+ from src.part3.evaluate import run_decoding_benchmark
85
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
86
+ run_decoding_benchmark(
87
+ model_name=args.model_name,
88
+ num_samples=args.samples,
89
+ device=device,
90
+ output_dir=args.output_dir
91
+ )
92
+
93
+
94
+ def verify_all_architectures():
95
+ """Verifies all mathematical parameter counts, optimizers, and decoding strategies."""
96
+ logger.info("=== Verifying Part 1: All 5 FFN Architecture Variants ===")
97
+ variants = ["dense", "top1", "top2", "shared_routed", "matched_active"]
98
+ dense_total, dense_active = 0, 0
99
+
100
+ for v in variants:
101
+ cfg = TransformerConfig(ablation_variant=v)
102
+ model = Transformer(cfg)
103
+ params = count_parameters(model)
104
+ tot = params["total_parameters"]
105
+ act = params["active_parameters"]
106
+ if v == "dense":
107
+ dense_total, dense_active = tot, act
108
+
109
+ diff_tot = tot - dense_total
110
+ diff_act = act - dense_active
111
+
112
+ logger.info(
113
+ f"Variant: {v:<15} | Total Params: {tot:>10,} (diff vs dense: {diff_tot:>+8,}) | "
114
+ f"Active Params: {act:>10,} (diff vs dense: {diff_act:>+8,})"
115
+ )
116
+
117
+ # Quick forward pass test
118
+ dummy_idx = torch.randint(0, cfg.N_vocab, (2, 16))
119
+ logits, loss, aux = model(dummy_idx, targets=dummy_idx)
120
+ assert logits.shape == (2, 16, cfg.N_vocab), f"Unexpected shape for {v}: {logits.shape}"
121
+ assert not torch.isnan(loss), f"Loss is NaN for {v}"
122
+
123
+ logger.info("=== Verifying Part 2: All 5 Custom Optimizers ===")
124
+ toy_p = torch.nn.Parameter(torch.randn(10, 10))
125
+ toy_grad = torch.randn(10, 10)
126
+ for OptCls in [CustomAdamW, CustomAdan, CustomLion, CustomMuon, CustomSophia]:
127
+ opt = OptCls([toy_p], lr=1e-3)
128
+ toy_p.grad = toy_grad.clone()
129
+ opt.step()
130
+ logger.info(f"Verified single step for {OptCls.__name__}")
131
+
132
+ logger.info("=== Verifying Part 3: All 4 Decoding Strategies from Scratch ===")
133
+ class ToyLM(torch.nn.Module):
134
+ def __init__(self):
135
+ super().__init__()
136
+ self.emb = torch.nn.Embedding(50, 16)
137
+ self.head = torch.nn.Linear(16, 50)
138
+ def forward(self, x):
139
+ return self.head(self.emb(x))
140
+
141
+ toy_lm = ToyLM()
142
+ test_input = torch.tensor([[1, 2, 3]], dtype=torch.long)
143
+ g_out, _, _ = greedy_decode(toy_lm, test_input, max_new_tokens=3)
144
+ k_out, _, _ = top_k_decode(toy_lm, test_input, k=5, max_new_tokens=3)
145
+ p_out, _, _ = top_p_decode(toy_lm, test_input, p=0.8, max_new_tokens=3)
146
+ b_out, _, _ = beam_search_decode(toy_lm, test_input, beam_width=2, max_new_tokens=3)
147
+ assert g_out.shape == (1, 6)
148
+ assert k_out.shape == (1, 6)
149
+ assert p_out.shape == (1, 6)
150
+ assert b_out.shape == (1, 6)
151
+ logger.info("Verified greedy, top-k, top-p, and beam search decoding.")
152
+
153
+ logger.info("=== All verification checks passed successfully! ===")
154
+
155
+
156
+ def main():
157
+ parser = argparse.ArgumentParser(description="ANLP Assignment 2: MoE, Optimizers, Decoding")
158
+ subparsers = parser.add_subparsers(dest="command", help="Sub-commands")
159
+
160
+ # Part 1 parser
161
+ p1 = subparsers.add_parser("part1", help="Run Part 1 MoE Ablations")
162
+ p1.add_argument("--ablation", choices=["dense", "top1", "top2", "shared_routed", "matched_active"], default="dense")
163
+ p1.add_argument("--train", action="store_true", default=True, help="Run actual training and generate heatmap")
164
+ p1.add_argument("--no-train", action="store_false", dest="train", help="Skip training, run sanity forward only")
165
+ p1.add_argument("--samples", type=int, default=10000)
166
+ p1.add_argument("--epochs", type=int, default=1)
167
+
168
+ # Part 2 parser
169
+ p2 = subparsers.add_parser("part2", help="Run Part 2 Optimizers")
170
+ p2.add_argument("--optimizer", choices=["adamw", "adan", "lion", "muon", "sophia", "all"], default="all")
171
+ p2.add_argument("--train", action="store_true", default=True, help="Run actual pretraining benchmark and generate dynamics plot")
172
+ p2.add_argument("--no-train", action="store_false", dest="train", help="Skip training, run sanity checks only")
173
+ p2.add_argument("--tokens", type=int, default=200000)
174
+
175
+ # Part 3 parser
176
+ p3 = subparsers.add_parser("part3", help="Run Part 3 Decoding Strategies")
177
+ p3.add_argument("--model_name", default="EleutherAI/Pythia-160M")
178
+ p3.add_argument("--samples", type=int, default=1000)
179
+ p3.add_argument("--output_dir", default="assets/part3")
180
+
181
+ # Verify parser
182
+ subparsers.add_parser("verify", help="Run architecture, optimizer, and decoding sanity verification")
183
+
184
+ args = parser.parse_args()
185
+
186
+ if args.command == "part1":
187
+ run_part1(args)
188
+ elif args.command == "part2":
189
+ run_part2(args)
190
+ elif args.command == "part3":
191
+ run_part3(args)
192
+ elif args.command == "verify" or args.command is None:
193
+ verify_all_architectures()
194
+
195
+
196
+ if __name__ == "__main__":
197
+ main()
report.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8ded208c60bfe712447f6d7230f028cbf9891f63798578c56c74ac149a09df43
3
+ size 666641
src/part1/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 1: Mixture of Experts (MoE) Module
3
+ Drop-in replacement for transformer feed-forward networks with 5 architectural ablations.
4
+ """
5
+
6
+ from .config import TransformerConfig
7
+ from .moe import MoE, MLP, Expert
8
+ from .model import Transformer, CausalSelfAttention, TransformerBlock
9
+ from .dataset import build_or_load_tokenizer, TranslationDataset, collate_fn
10
+ from .train import evaluate_and_extract_heatmaps, save_expert_heatmap
11
+
12
+ __all__ = [
13
+ "TransformerConfig",
14
+ "MoE",
15
+ "MLP",
16
+ "Expert",
17
+ "Transformer",
18
+ "CausalSelfAttention",
19
+ "TransformerBlock",
20
+ "build_or_load_tokenizer",
21
+ "TranslationDataset",
22
+ "collate_fn",
23
+ "evaluate_and_extract_heatmaps",
24
+ "save_expert_heatmap",
25
+ ]
src/part1/config.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration dataclass for Transformer and Mixture-of-Experts (MoE) architectures.
3
+ Matches Listing 1 from Appendix with full parameter-accounting formulas for the 5 ablations.
4
+ """
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional, Literal
8
+
9
+
10
+ @dataclass
11
+ class TransformerConfig:
12
+ # Model architecture dimensions
13
+ D_model: int = 256 # Hidden representation dimension
14
+ N_ctx: int = 256 # Maximum context window length
15
+ N_vocab: int = 16000 # Vocabulary size
16
+ N_layers: int = 6 # Number of Transformer blocks
17
+ N_heads: int = 8 # Multi-head attention heads
18
+ D_ff: int = 1024 # Feed-forward hidden dimension (4 * D_model)
19
+ dropout: float = 0.1 # Dropout probability
20
+
21
+ # MoE architecture configuration
22
+ use_moe: bool = False # False: Standard MLP; True: MoE
23
+ ablation_variant: Literal[
24
+ "dense",
25
+ "top1",
26
+ "top2",
27
+ "shared_routed",
28
+ "matched_active"
29
+ ] = "dense"
30
+
31
+ # MoE details
32
+ num_experts: int = 4 # Total experts in MoE layer
33
+ num_active_experts: int = 1 # Active experts routed per token
34
+ num_shared_experts: int = 0 # Always-active shared experts (for variant 4)
35
+ aux_loss_coef: float = 0.01 # Switch/GShard load balancing auxiliary loss weight
36
+ capacity_factor: float = 1.25 # Capacity factor for expert routing if capped
37
+
38
+ # Training settings
39
+ learning_rate: float = 3e-4
40
+ weight_decay: float = 0.01
41
+ warmup_steps: int = 500
42
+ max_steps: int = 10000
43
+ batch_size: int = 32
44
+
45
+ def __post_init__(self):
46
+ """Validates configuration parameters and assigns ablation-specific settings."""
47
+ assert self.D_model % self.N_heads == 0, f"D_model ({self.D_model}) must be divisible by N_heads ({self.N_heads})"
48
+
49
+ if self.ablation_variant == "dense":
50
+ self.use_moe = False
51
+ else:
52
+ self.use_moe = True
53
+
54
+ if self.ablation_variant == "top1":
55
+ # 4 experts, 1 active at a time. Total params == Dense baseline.
56
+ self.num_experts = 4
57
+ self.num_active_experts = 1
58
+ self.num_shared_experts = 0
59
+ self.expert_d_ff = self.D_ff // 4
60
+ elif self.ablation_variant == "top2":
61
+ # 4 experts, 2 active at a time. Total params == Dense baseline.
62
+ self.num_experts = 4
63
+ self.num_active_experts = 2
64
+ self.num_shared_experts = 0
65
+ self.expert_d_ff = self.D_ff // 4
66
+ elif self.ablation_variant == "shared_routed":
67
+ # 1 shared expert (always active) + 3 routed (1 active). Total 2 of 4 active.
68
+ # Total params == Dense baseline.
69
+ self.num_experts = 4
70
+ self.num_shared_experts = 1
71
+ self.num_active_experts = 1 # 1 routed active + 1 shared active = 2 active total
72
+ self.expert_d_ff = self.D_ff // 4
73
+ elif self.ablation_variant == "matched_active":
74
+ # 4 experts, 2 active at a time, ACTIVE params == Dense baseline.
75
+ # Since 2 are active, each expert has D_ff / 2, so active = 2 * (D_ff / 2) = D_ff.
76
+ # Total params = 4 * (D_ff / 2) = 2 * Dense.
77
+ self.num_experts = 4
78
+ self.num_active_experts = 2
79
+ self.num_shared_experts = 0
80
+ self.expert_d_ff = self.D_ff // 2
81
+ else:
82
+ self.expert_d_ff = self.D_ff
83
+
84
+ @property
85
+ def ffn_total_params_per_layer(self) -> int:
86
+ """Computes the theoretical total parameter count of one FFN/MoE layer."""
87
+ if not self.use_moe or self.ablation_variant == "dense":
88
+ # Linear1: D_model * D_ff + D_ff; Linear2: D_ff * D_model + D_model
89
+ return 2 * self.D_model * self.D_ff + self.D_ff + self.D_model
90
+ elif self.ablation_variant in ["top1", "top2"]:
91
+ # Router: D_model * 4 + 4 experts of (2 * D_model * d_expert)
92
+ router = self.D_model * self.num_experts
93
+ experts = self.num_experts * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
94
+ return router + experts
95
+ elif self.ablation_variant == "shared_routed":
96
+ # Router: D_model * 3 + 1 shared expert + 3 routed experts
97
+ router = self.D_model * 3
98
+ shared = 1 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
99
+ routed = 3 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
100
+ return router + shared + routed
101
+ elif self.ablation_variant == "matched_active":
102
+ router = self.D_model * self.num_experts
103
+ experts = self.num_experts * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
104
+ return router + experts
105
+ return 0
106
+
107
+ @property
108
+ def ffn_active_params_per_layer(self) -> int:
109
+ """Computes the theoretical active parameter count of one FFN/MoE layer per token."""
110
+ if not self.use_moe or self.ablation_variant == "dense":
111
+ return self.ffn_total_params_per_layer
112
+ elif self.ablation_variant == "top1":
113
+ router = self.D_model * self.num_experts
114
+ active_expert = 1 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
115
+ return router + active_expert
116
+ elif self.ablation_variant == "top2":
117
+ router = self.D_model * self.num_experts
118
+ active_experts = 2 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
119
+ return router + active_experts
120
+ elif self.ablation_variant == "shared_routed":
121
+ router = self.D_model * 3
122
+ shared_expert = 1 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
123
+ active_routed = 1 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
124
+ return router + shared_expert + active_routed
125
+ elif self.ablation_variant == "matched_active":
126
+ router = self.D_model * self.num_experts
127
+ active_experts = 2 * (2 * self.D_model * self.expert_d_ff + self.expert_d_ff + self.D_model)
128
+ return router + active_experts
129
+ return 0
src/part1/dataset.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset and Tokenization module for Part 1 (Vietnamese/Japanese to English Translation).
3
+ Handles belumind/en-vi-ja-curated-500k-triplets dataset.
4
+ Supports Byte-level BPE tokenization handling non-romanized Japanese and Vietnamese characters.
5
+ """
6
+
7
+ import os
8
+ from typing import Dict, List, Tuple, Optional, Any
9
+ import torch
10
+ from torch.utils.data import Dataset, DataLoader
11
+ from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers, processors
12
+ from transformers import PreTrainedTokenizerFast
13
+
14
+
15
+ SPECIAL_TOKENS = ["<pad>", "<unk>", "<bos>", "<eos>", "<vi>", "<ja>", "<en>"]
16
+
17
+
18
+ def build_or_load_tokenizer(
19
+ vocab_size: int = 16000,
20
+ save_path: str = "assets/tokenizer_multilingual.json",
21
+ corpus_texts: Optional[List[str]] = None
22
+ ) -> PreTrainedTokenizerFast:
23
+ """
24
+ Builds or loads a Byte-level BPE tokenizer capable of processing
25
+ arbitrary UTF-8 bytes (Vietnamese diacritics and Japanese Kanji/Kana).
26
+ """
27
+ if os.path.exists(save_path):
28
+ tokenizer = Tokenizer.from_file(save_path)
29
+ fast_tok = PreTrainedTokenizerFast(
30
+ tokenizer_object=tokenizer,
31
+ bos_token="<bos>",
32
+ eos_token="<eos>",
33
+ pad_token="<pad>",
34
+ unk_token="<unk>",
35
+ )
36
+ return fast_tok
37
+
38
+ # Build Byte-Level BPE tokenizer from scratch
39
+ raw_tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
40
+ raw_tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
41
+ raw_tokenizer.decoder = decoders.ByteLevel()
42
+
43
+ trainer = trainers.BpeTrainer(
44
+ vocab_size=vocab_size,
45
+ special_tokens=SPECIAL_TOKENS,
46
+ initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
47
+ show_progress=False
48
+ )
49
+
50
+ if corpus_texts is not None and len(corpus_texts) > 0:
51
+ raw_tokenizer.train_from_iterator(corpus_texts, trainer=trainer)
52
+ else:
53
+ # Fallback dummy seed training with typical tokens so tokenizer is valid
54
+ seed_data = [
55
+ "Hello world, this is English.",
56
+ "こんにちは世界、日本語のテストです。",
57
+ "Xin chào thế giới, đây là kiểm tra tiếng Việt.",
58
+ ] * 100
59
+ raw_tokenizer.train_from_iterator(seed_data, trainer=trainer)
60
+
61
+ os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
62
+ raw_tokenizer.save(save_path)
63
+
64
+ fast_tok = PreTrainedTokenizerFast(
65
+ tokenizer_object=raw_tokenizer,
66
+ bos_token="<bos>",
67
+ eos_token="<eos>",
68
+ pad_token="<pad>",
69
+ unk_token="<unk>",
70
+ )
71
+ return fast_tok
72
+
73
+
74
+ class TranslationDataset(Dataset):
75
+ """
76
+ Dataset creating Vietnamese->English and Japanese->English samples.
77
+ Each triplet (vi, ja, en) produces two training sequences:
78
+ 1. <vi> {vi_text} <en> {en_text} <eos>
79
+ 2. <ja> {ja_text} <en> {en_text} <eos>
80
+ """
81
+ def __init__(
82
+ self,
83
+ samples: List[Dict[str, str]],
84
+ tokenizer: PreTrainedTokenizerFast,
85
+ max_seq_len: int = 256,
86
+ loss_on_target_only: bool = True
87
+ ):
88
+ self.tokenizer = tokenizer
89
+ self.max_seq_len = max_seq_len
90
+ self.loss_on_target_only = loss_on_target_only
91
+ self.data: List[Dict[str, Any]] = []
92
+
93
+ for item in samples:
94
+ en = item.get("en", "").strip()
95
+ vi = item.get("vi", "").strip()
96
+ ja = item.get("ja", "").strip()
97
+
98
+ if vi and en:
99
+ self.data.append({
100
+ "src_lang": "vi",
101
+ "src_text": vi,
102
+ "tgt_text": en,
103
+ "prompt": f"<vi> {vi} <en>",
104
+ "full_text": f"<vi> {vi} <en> {en} <eos>"
105
+ })
106
+ if ja and en:
107
+ self.data.append({
108
+ "src_lang": "ja",
109
+ "src_text": ja,
110
+ "tgt_text": en,
111
+ "prompt": f"<ja> {ja} <en>",
112
+ "full_text": f"<ja> {ja} <en> {en} <eos>"
113
+ })
114
+
115
+ def __len__(self) -> int:
116
+ return len(self.data)
117
+
118
+ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
119
+ item = self.data[idx]
120
+ full_text = item["full_text"]
121
+ prompt = item["prompt"]
122
+
123
+ full_encoding = self.tokenizer(
124
+ full_text,
125
+ max_length=self.max_seq_len,
126
+ padding="max_length",
127
+ truncation=True,
128
+ return_tensors="pt"
129
+ )
130
+ input_ids = full_encoding["input_ids"].squeeze(0) # (N_ctx,)
131
+
132
+ targets = input_ids.clone()
133
+ pad_token_id = self.tokenizer.pad_token_id or 0
134
+ targets[targets == pad_token_id] = -100
135
+
136
+ if self.loss_on_target_only:
137
+ prompt_encoding = self.tokenizer(
138
+ prompt,
139
+ max_length=self.max_seq_len,
140
+ truncation=True,
141
+ return_tensors="pt"
142
+ )
143
+ prompt_len = min(prompt_encoding["input_ids"].shape[1], self.max_seq_len)
144
+ targets[:prompt_len] = -100 # Mask out prompt tokens from loss computation
145
+
146
+ return {
147
+ "input_ids": input_ids,
148
+ "targets": targets,
149
+ "src_lang": item["src_lang"],
150
+ "src_text": item["src_text"],
151
+ "tgt_text": item["tgt_text"],
152
+ "prompt": item["prompt"]
153
+ }
154
+
155
+
156
+ def collate_fn(batch: List[Dict[str, Any]]) -> Dict[str, Any]:
157
+ input_ids = torch.stack([b["input_ids"] for b in batch])
158
+ targets = torch.stack([b["targets"] for b in batch])
159
+ src_langs = [b["src_lang"] for b in batch]
160
+ src_texts = [b["src_text"] for b in batch]
161
+ tgt_texts = [b["tgt_text"] for b in batch]
162
+ prompts = [b["prompt"] for b in batch]
163
+
164
+ return {
165
+ "input_ids": input_ids,
166
+ "targets": targets,
167
+ "src_langs": src_langs,
168
+ "src_texts": src_texts,
169
+ "tgt_texts": tgt_texts,
170
+ "prompts": prompts
171
+ }
src/part1/model.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Decoder-only Transformer implementation with drop-in MoE support.
3
+ Follows Appendix Listing 1 and requirements of Assignment 2.
4
+ """
5
+
6
+ import math
7
+ from typing import Optional, Dict, Any, Tuple
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.nn.functional as F
11
+
12
+ from .config import TransformerConfig
13
+ from .moe import MoE, MLP
14
+
15
+
16
+ class CausalSelfAttention(nn.Module):
17
+ """
18
+ Multi-head causal self-attention mechanism utilizing scaled_dot_product_attention.
19
+ """
20
+ def __init__(self, config: TransformerConfig):
21
+ super().__init__()
22
+ assert config.D_model % config.N_heads == 0
23
+ self.D_model = config.D_model
24
+ self.N_heads = config.N_heads
25
+ self.head_dim = config.D_model // config.N_heads
26
+ self.dropout = config.dropout
27
+
28
+ # Combined Q, K, V projection
29
+ self.c_attn = nn.Linear(config.D_model, 3 * config.D_model, bias=True)
30
+ # Output projection
31
+ self.c_proj = nn.Linear(config.D_model, config.D_model, bias=True)
32
+ self.resid_dropout = nn.Dropout(config.dropout)
33
+
34
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
35
+ B, T, C = x.shape # Batch size, sequence length, hidden dimension
36
+
37
+ # Compute queries, keys, and values
38
+ qkv = self.c_attn(x)
39
+ q, k, v = qkv.split(self.D_model, dim=2)
40
+
41
+ # Reshape for multi-head attention: (B, N_heads, T, head_dim)
42
+ q = q.view(B, T, self.N_heads, self.head_dim).transpose(1, 2)
43
+ k = k.view(B, T, self.N_heads, self.head_dim).transpose(1, 2)
44
+ v = v.view(B, T, self.N_heads, self.head_dim).transpose(1, 2)
45
+
46
+ # PyTorch 2.0+ scaled_dot_product_attention with causal masking
47
+ y = F.scaled_dot_product_attention(
48
+ q, k, v,
49
+ attn_mask=None,
50
+ dropout_p=self.dropout if self.training else 0.0,
51
+ is_causal=True
52
+ )
53
+
54
+ # Re-assemble head outputs: (B, T, C)
55
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
56
+ out = self.resid_dropout(self.c_proj(y))
57
+ return out
58
+
59
+
60
+ class TransformerBlock(nn.Module):
61
+ """
62
+ Standard Transformer Decoder Block containing:
63
+ - LayerNorm + Causal Attention + Residual
64
+ - LayerNorm + FFN (MLP or MoE) + Residual
65
+ """
66
+ def __init__(self, config: TransformerConfig):
67
+ super().__init__()
68
+ self.ln_1 = nn.LayerNorm(config.D_model)
69
+ self.attn = CausalSelfAttention(config)
70
+ self.ln_2 = nn.LayerNorm(config.D_model)
71
+ self.ffn = MoE(config) if config.use_moe else MLP(config)
72
+
73
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
74
+ x = x + self.attn(self.ln_1(x))
75
+ x = x + self.ffn(self.ln_2(x))
76
+ return x
77
+
78
+
79
+ class Transformer(nn.Module):
80
+ """
81
+ Decoder-only Transformer Language Model.
82
+ Supports drop-in MoE or standard MLP feed-forward networks.
83
+ """
84
+ def __init__(self, config: TransformerConfig):
85
+ super().__init__()
86
+ self.config = config
87
+
88
+ self.wte = nn.Embedding(config.N_vocab, config.D_model)
89
+ self.wpe = nn.Embedding(config.N_ctx, config.D_model)
90
+ self.drop = nn.Dropout(config.dropout)
91
+
92
+ self.blocks = nn.ModuleList([
93
+ TransformerBlock(config) for _ in range(config.N_layers)
94
+ ])
95
+ self.ln_f = nn.LayerNorm(config.D_model)
96
+ self.head = nn.Linear(config.D_model, config.N_vocab, bias=False)
97
+
98
+ # Tie weights between token embeddings and output projection
99
+ self.head.weight = self.wte.weight
100
+
101
+ # Initialize weights
102
+ self.apply(self._init_weights)
103
+
104
+ def _init_weights(self, module: nn.Module):
105
+ if isinstance(module, (nn.Linear, nn.Embedding)):
106
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
107
+ if isinstance(module, nn.Linear) and module.bias is not None:
108
+ nn.init.zeros_(module.bias)
109
+ elif isinstance(module, nn.LayerNorm):
110
+ nn.init.ones_(module.weight)
111
+ nn.init.zeros_(module.bias)
112
+
113
+ def get_aux_loss(self) -> torch.Tensor:
114
+ """Sums auxiliary load-balancing losses across all MoE layers."""
115
+ if not self.config.use_moe:
116
+ return torch.tensor(0.0, device=next(self.parameters()).device)
117
+
118
+ total_aux = torch.tensor(0.0, device=next(self.parameters()).device)
119
+ for block in self.blocks:
120
+ if isinstance(block.ffn, MoE):
121
+ total_aux = total_aux + block.ffn.last_aux_loss.to(total_aux.device)
122
+ return total_aux
123
+
124
+ def get_active_parameters(self) -> int:
125
+ """
126
+ Computes the number of parameters actively participating in forward computation per token.
127
+ For dense models, active_parameters == total_parameters.
128
+ For MoE models, only router and active experts count.
129
+ """
130
+ active = 0
131
+ # Embeddings
132
+ active += self.wte.weight.numel()
133
+ active += self.wpe.weight.numel()
134
+ # Final norm and tied lm_head
135
+ active += sum(p.numel() for p in self.ln_f.parameters())
136
+ # (self.head shares weights with self.wte, so not double-counted)
137
+
138
+ for block in self.blocks:
139
+ active += sum(p.numel() for p in block.ln_1.parameters())
140
+ active += sum(p.numel() for p in block.attn.parameters())
141
+ active += sum(p.numel() for p in block.ln_2.parameters())
142
+ if isinstance(block.ffn, MoE):
143
+ active += block.ffn.get_active_parameters()
144
+ else:
145
+ active += sum(p.numel() for p in block.ffn.parameters())
146
+
147
+ return active
148
+
149
+ def forward(
150
+ self,
151
+ idx: torch.Tensor,
152
+ targets: Optional[torch.Tensor] = None
153
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor], torch.Tensor]:
154
+ """
155
+ Forward pass for Transformer.
156
+ Args:
157
+ idx: Tensor of token indices with shape (B, T)
158
+ targets: Optional ground-truth target tokens with shape (B, T)
159
+ Returns:
160
+ logits: (B, T, N_vocab)
161
+ loss: Cross-entropy loss + aux_loss (if targets provided)
162
+ aux_loss: Load balancing auxiliary loss
163
+ """
164
+ device = idx.device
165
+ B, T = idx.shape
166
+ assert T <= self.config.N_ctx, f"Sequence length {T} exceeds context window {self.config.N_ctx}"
167
+
168
+ pos = torch.arange(0, T, dtype=torch.long, device=device).unsqueeze(0) # (1, T)
169
+
170
+ # Token + positional embeddings
171
+ tok_emb = self.wte(idx) # (B, T, D_model)
172
+ pos_emb = self.wpe(pos) # (1, T, D_model)
173
+ x = self.drop(tok_emb + pos_emb)
174
+
175
+ # Pass through Transformer blocks
176
+ for block in self.blocks:
177
+ x = block(x)
178
+
179
+ x = self.ln_f(x)
180
+ logits = self.head(x) # (B, T, N_vocab)
181
+
182
+ loss = None
183
+ aux_loss = self.get_aux_loss()
184
+
185
+ if targets is not None:
186
+ # Shift tokens for next-token prediction
187
+ if targets.shape[1] == logits.shape[1]:
188
+ # Standard next-token prediction: position t predicts token t+1
189
+ shift_logits = logits[:, :-1, :].contiguous().view(-1, self.config.N_vocab)
190
+ shift_targets = targets[:, 1:].contiguous().view(-1)
191
+ elif targets.shape[1] == logits.shape[1] - 1:
192
+ # Targets are already pre-shifted by 1 token
193
+ shift_logits = logits[:, :-1, :].contiguous().view(-1, self.config.N_vocab)
194
+ shift_targets = targets.contiguous().view(-1)
195
+ else:
196
+ shift_logits = logits.contiguous().view(-1, self.config.N_vocab)
197
+ shift_targets = targets.contiguous().view(-1)
198
+
199
+ ce_loss = F.cross_entropy(shift_logits, shift_targets, ignore_index=-100)
200
+ loss = ce_loss + aux_loss
201
+
202
+ return logits, loss, aux_loss
src/part1/moe.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Mixture of Experts (MoE) implementation for Transformer architectures.
3
+ Implements the 5 required FFN variants:
4
+ 1. Standard 2-layer MLP (dense baseline)
5
+ 2. MoE with 4 experts, 1 active at a time (top-1 routing)
6
+ 3. MoE with 4 experts, 2 active at a time (top-2 routing)
7
+ 4. MoE with 1 shared expert (always active) + 3 routed experts (1 active at a time, 2 total active)
8
+ 5. MoE with 4 experts, 2 active at a time, matched active parameters to dense baseline (1)
9
+ """
10
+
11
+ import math
12
+ from typing import Dict, Tuple, Optional, List
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+ from .config import TransformerConfig
18
+
19
+
20
+ class MLP(nn.Module):
21
+ """
22
+ Standard 2-layer Feed-Forward Network (Dense Baseline).
23
+ Input: (B, N_ctx, D_model)
24
+ Output: (B, N_ctx, D_model)
25
+ """
26
+ def __init__(self, config: TransformerConfig):
27
+ super().__init__()
28
+ self.D_model = config.D_model
29
+ self.D_ff = config.D_ff
30
+ self.w1 = nn.Linear(config.D_model, config.D_ff, bias=True)
31
+ self.act = nn.GELU()
32
+ self.dropout = nn.Dropout(config.dropout)
33
+ self.w2 = nn.Linear(config.D_ff, config.D_model, bias=True)
34
+
35
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
36
+ # x: (B, N_ctx, D_model)
37
+ h = self.act(self.w1(x))
38
+ h = self.dropout(h)
39
+ out = self.dropout(self.w2(h))
40
+ return out
41
+
42
+
43
+ class Expert(nn.Module):
44
+ """
45
+ Individual Expert Feed-Forward Network.
46
+ Input: (..., D_model)
47
+ Output: (..., D_model)
48
+ """
49
+ def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
50
+ super().__init__()
51
+ self.d_model = d_model
52
+ self.d_ff = d_ff
53
+ self.w1 = nn.Linear(d_model, d_ff, bias=True)
54
+ self.act = nn.GELU()
55
+ self.dropout = nn.Dropout(dropout)
56
+ self.w2 = nn.Linear(d_ff, d_model, bias=True)
57
+
58
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
59
+ h = self.act(self.w1(x))
60
+ h = self.dropout(h)
61
+ return self.dropout(self.w2(h))
62
+
63
+
64
+ class MoE(nn.Module):
65
+ """
66
+ Mixture-of-Experts Module with drop-in replacement support for MLP.
67
+ Supports Top-1, Top-2, Shared+Routed, and Matched-Active configurations.
68
+ """
69
+ def __init__(self, config: TransformerConfig):
70
+ super().__init__()
71
+ self.config = config
72
+ self.D_model = config.D_model
73
+ self.ablation_variant = config.ablation_variant
74
+ self.aux_loss_coef = config.aux_loss_coef
75
+
76
+ # Determine architecture based on ablation variant
77
+ if self.ablation_variant == "shared_routed":
78
+ # 1 shared expert + 3 routed experts
79
+ self.num_routed_experts = 3
80
+ self.shared_expert = Expert(config.D_model, config.expert_d_ff, config.dropout)
81
+ self.routed_experts = nn.ModuleList([
82
+ Expert(config.D_model, config.expert_d_ff, config.dropout)
83
+ for _ in range(self.num_routed_experts)
84
+ ])
85
+ self.router = nn.Linear(config.D_model, self.num_routed_experts, bias=False)
86
+ self.top_k = 1
87
+ else:
88
+ # top1, top2, matched_active
89
+ self.num_routed_experts = config.num_experts
90
+ self.shared_expert = None
91
+ self.routed_experts = nn.ModuleList([
92
+ Expert(config.D_model, config.expert_d_ff, config.dropout)
93
+ for _ in range(self.num_routed_experts)
94
+ ])
95
+ self.router = nn.Linear(config.D_model, self.num_routed_experts, bias=False)
96
+ self.top_k = config.num_active_experts
97
+
98
+ # Running telemetry for expert utilization and language analysis
99
+ self.register_buffer(
100
+ "expert_selection_counts",
101
+ torch.zeros(self.num_routed_experts, dtype=torch.int64),
102
+ persistent=False
103
+ )
104
+ self.last_expert_indices: Optional[torch.Tensor] = None
105
+ self.last_aux_loss: torch.Tensor = torch.tensor(0.0)
106
+
107
+ def get_active_parameters(self) -> int:
108
+ """Computes active parameters per token for this layer."""
109
+ router_params = sum(p.numel() for p in self.router.parameters())
110
+ if self.ablation_variant == "shared_routed":
111
+ shared_params = sum(p.numel() for p in self.shared_expert.parameters())
112
+ routed_params = sum(p.numel() for p in self.routed_experts[0].parameters()) * self.top_k
113
+ return router_params + shared_params + routed_params
114
+ else:
115
+ routed_params = sum(p.numel() for p in self.routed_experts[0].parameters()) * self.top_k
116
+ return router_params + routed_params
117
+
118
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
119
+ """
120
+ Forward pass for MoE layer.
121
+ Args:
122
+ x: Input tensor of shape (B, N_ctx, D_model)
123
+ Returns:
124
+ out: Output tensor of shape (B, N_ctx, D_model)
125
+ """
126
+ batch_size, seq_len, d_model = x.shape
127
+ x_flat = x.reshape(-1, d_model) # (N, D) where N = B * N_ctx
128
+ num_tokens = x_flat.size(0)
129
+
130
+ # 1. Router forward
131
+ router_logits = self.router(x_flat) # (N, num_routed_experts)
132
+ router_probs = F.softmax(router_logits, dim=-1) # (N, num_routed_experts)
133
+
134
+ # 2. Expert selection
135
+ if self.top_k == 1:
136
+ top_weights, top_indices = router_probs.max(dim=-1, keepdim=True) # (N, 1)
137
+ else:
138
+ top_weights, top_indices = torch.topk(router_probs, k=self.top_k, dim=-1) # (N, top_k)
139
+ # Re-normalize weights among selected top-k experts
140
+ top_weights = top_weights / (top_weights.sum(dim=-1, keepdim=True) + 1e-9)
141
+
142
+ # Cache routing decisions for downstream language heatmap extraction
143
+ self.last_expert_indices = top_indices.detach()
144
+
145
+ # Update cumulative selection counts
146
+ with torch.no_grad():
147
+ for i in range(self.num_routed_experts):
148
+ self.expert_selection_counts[i] += (top_indices == i).sum().item()
149
+
150
+ # 3. Compute auxiliary load-balancing loss (Switch / GShard formulation)
151
+ # L_aux = alpha * num_experts * sum_i(f_i * P_i)
152
+ # where f_i is fraction of tokens dispatched to expert i, P_i is average routing prob
153
+ num_experts = self.num_routed_experts
154
+ expert_mask = F.one_hot(top_indices, num_classes=num_experts) # (N, top_k, num_experts)
155
+ tokens_per_expert = expert_mask.sum(dim=1).sum(dim=0).float() # (num_experts,)
156
+ f_i = tokens_per_expert / (num_tokens * self.top_k + 1e-9)
157
+ P_i = router_probs.mean(dim=0) # (num_experts,)
158
+ self.last_aux_loss = self.aux_loss_coef * (num_experts * torch.sum(f_i * P_i))
159
+
160
+ # 4. Dispatch tokens through routed experts
161
+ routed_out = torch.zeros_like(x_flat)
162
+ for expert_idx in range(num_experts):
163
+ # Check which tokens selected this expert
164
+ # top_indices shape: (N, top_k)
165
+ token_matches = (top_indices == expert_idx)
166
+ if not token_matches.any():
167
+ continue
168
+
169
+ token_indices, k_positions = torch.where(token_matches)
170
+ selected_tokens = x_flat[token_indices] # (M, D)
171
+ weights = top_weights[token_indices, k_positions].unsqueeze(-1) # (M, 1)
172
+
173
+ expert_output = self.routed_experts[expert_idx](selected_tokens)
174
+ routed_out.index_add_(0, token_indices, expert_output * weights)
175
+
176
+ # 5. Add shared expert output if applicable
177
+ if self.shared_expert is not None:
178
+ shared_out = self.shared_expert(x_flat)
179
+ total_out = shared_out + routed_out
180
+ else:
181
+ total_out = routed_out
182
+
183
+ return total_out.reshape(batch_size, seq_len, d_model)
src/part1/train.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Training and evaluation pipeline for Part 1 (MoE Transformer Ablations).
3
+ Evaluates perplexity, BLEU, expert utilization heatmaps, and saves artifacts.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import logging
9
+ from typing import Dict, Any, List, Optional, Tuple
10
+ import torch
11
+ import torch.nn as nn
12
+ from torch.utils.data import DataLoader
13
+ from tqdm import tqdm
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+
17
+ from .config import TransformerConfig
18
+ from .model import Transformer
19
+ from .moe import MoE
20
+ from .dataset import build_or_load_tokenizer, TranslationDataset, collate_fn
21
+ from ..utils import set_seed, compute_perplexity, compute_bleu, WandbLogger, count_parameters
22
+
23
+ logger = logging.getLogger("Part1_Train")
24
+
25
+
26
+ def train_epoch(
27
+ model: Transformer,
28
+ dataloader: DataLoader,
29
+ optimizer: torch.optim.Optimizer,
30
+ device: torch.device,
31
+ wandb_logger: Optional[WandbLogger] = None,
32
+ step_offset: int = 0
33
+ ) -> Tuple[float, int]:
34
+ model.train()
35
+ total_loss = 0.0
36
+ total_tokens = 0
37
+ step = step_offset
38
+
39
+ for batch in tqdm(dataloader, desc="Training", leave=False):
40
+ input_ids = batch["input_ids"].to(device)
41
+ targets = batch["targets"].to(device)
42
+
43
+ optimizer.zero_grad()
44
+ logits, loss, aux_loss = model(input_ids, targets=targets)
45
+ loss.backward()
46
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
47
+ optimizer.step()
48
+
49
+ step += 1
50
+ num_valid_tokens = (targets != -100).sum().item()
51
+ total_loss += loss.item() * num_valid_tokens
52
+ total_tokens += max(num_valid_tokens, 1)
53
+
54
+ if wandb_logger and step % 20 == 0:
55
+ wandb_logger.log({
56
+ "train/loss": loss.item(),
57
+ "train/aux_loss": aux_loss.item() if isinstance(aux_loss, torch.Tensor) else aux_loss,
58
+ "train/step": step
59
+ }, step=step)
60
+
61
+ avg_loss = total_loss / max(total_tokens, 1)
62
+ return avg_loss, step
63
+
64
+
65
+ @torch.no_grad()
66
+ def evaluate_and_extract_heatmaps(
67
+ model: Transformer,
68
+ dataloader: DataLoader,
69
+ tokenizer: Any,
70
+ device: torch.device,
71
+ max_eval_samples: int = 1000
72
+ ) -> Dict[str, Any]:
73
+ """
74
+ Evaluates perplexity and BLEU, while recording layer-wise expert activations
75
+ split by input language ('vi' vs 'ja').
76
+ """
77
+ model.eval()
78
+ total_loss = 0.0
79
+ total_tokens = 0
80
+ predictions: List[str] = []
81
+ references: List[List[str]] = []
82
+
83
+ # Track expert activation counts: lang -> layer_idx -> [count_per_expert]
84
+ has_moe = model.config.use_moe
85
+ num_layers = len(model.blocks)
86
+ routed_experts_count = (
87
+ model.config.num_experts if model.config.ablation_variant != "shared_routed" else 3
88
+ )
89
+
90
+ expert_activity = {
91
+ "vi": np.zeros((num_layers, routed_experts_count), dtype=np.float64),
92
+ "ja": np.zeros((num_layers, routed_experts_count), dtype=np.float64)
93
+ }
94
+ lang_token_counts = {"vi": 0, "ja": 0}
95
+
96
+ eval_samples_processed = 0
97
+
98
+ for batch in tqdm(dataloader, desc="Evaluating", leave=False):
99
+ input_ids = batch["input_ids"].to(device)
100
+ targets = batch["targets"].to(device)
101
+ src_langs = batch["src_langs"]
102
+ prompts = batch["prompts"]
103
+ tgt_texts = batch["tgt_texts"]
104
+
105
+ logits, loss, _ = model(input_ids, targets=targets)
106
+ num_valid_tokens = (targets != -100).sum().item()
107
+ total_loss += loss.item() * num_valid_tokens
108
+ total_tokens += max(num_valid_tokens, 1)
109
+
110
+ # Record expert routing decisions if MoE
111
+ if has_moe:
112
+ for l_idx, block in enumerate(model.blocks):
113
+ if isinstance(block.ffn, MoE) and block.ffn.last_expert_indices is not None:
114
+ # last_expert_indices: (B * N_ctx, top_k)
115
+ indices = block.ffn.last_expert_indices.view(input_ids.shape[0], input_ids.shape[1], -1)
116
+ for b_idx, lang in enumerate(src_langs):
117
+ if lang in expert_activity:
118
+ sample_indices = indices[b_idx].cpu().numpy().flatten()
119
+ for exp_id in sample_indices:
120
+ if 0 <= exp_id < routed_experts_count:
121
+ expert_activity[lang][l_idx, exp_id] += 1
122
+ lang_token_counts[lang] += 1
123
+
124
+ # Greedy decoding for BLEU evaluation on a subset of samples
125
+ if eval_samples_processed < max_eval_samples:
126
+ for b_idx in range(len(prompts)):
127
+ prompt = prompts[b_idx]
128
+ target_text = tgt_texts[b_idx]
129
+ prompt_enc = tokenizer(prompt, return_tensors="pt").input_ids.to(device)
130
+
131
+ # Generate up to 50 tokens
132
+ curr_ids = prompt_enc.clone()
133
+ for _ in range(50):
134
+ if curr_ids.shape[1] >= model.config.N_ctx:
135
+ break
136
+ out_logits, _, _ = model(curr_ids)
137
+ next_tok = out_logits[:, -1, :].argmax(dim=-1, keepdim=True)
138
+ curr_ids = torch.cat([curr_ids, next_tok], dim=1)
139
+ if next_tok.item() == tokenizer.eos_token_id:
140
+ break
141
+
142
+ gen_tokens = curr_ids[0, prompt_enc.shape[1]:]
143
+ pred_text = tokenizer.decode(gen_tokens.cpu(), skip_special_tokens=True)
144
+ predictions.append(pred_text)
145
+ references.append([target_text])
146
+
147
+ eval_samples_processed += 1
148
+ if eval_samples_processed >= max_eval_samples:
149
+ break
150
+
151
+ avg_loss = total_loss / max(total_tokens, 1)
152
+ ppl = compute_perplexity(avg_loss)
153
+ bleu = compute_bleu(predictions, references)
154
+
155
+ # Normalize expert activity to distribution per language
156
+ for lang in ["vi", "ja"]:
157
+ total_act = expert_activity[lang].sum()
158
+ if total_act > 0:
159
+ expert_activity[lang] = expert_activity[lang] / total_act
160
+
161
+ return {
162
+ "loss": avg_loss,
163
+ "perplexity": ppl,
164
+ "bleu": bleu,
165
+ "expert_activity": expert_activity,
166
+ "sample_predictions": predictions[:5],
167
+ "sample_references": references[:5]
168
+ }
169
+
170
+
171
+ def save_expert_heatmap(
172
+ expert_activity: Dict[str, np.ndarray],
173
+ save_path: str = "assets/expert_heatmap.png",
174
+ variant_name: str = "MoE"
175
+ ) -> None:
176
+ """Plots and saves expert activation heatmap comparing Vietnamese vs Japanese."""
177
+ os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
178
+ fig, axes = plt.subplots(1, 2, figsize=(14, 5))
179
+
180
+ for idx, lang in enumerate(["vi", "ja"]):
181
+ ax = axes[idx]
182
+ data = expert_activity[lang]
183
+ if not isinstance(data, np.ndarray):
184
+ data = np.asarray(data, dtype=float)
185
+ im = ax.imshow(data, cmap="Blues", aspect="auto")
186
+ ax.set_title(f"Expert Activation: {lang.upper()} ({variant_name})")
187
+ ax.set_xlabel("Expert Index")
188
+ ax.set_ylabel("Transformer Layer")
189
+ fig.colorbar(im, ax=ax)
190
+
191
+ plt.tight_layout()
192
+ plt.savefig(save_path, dpi=200)
193
+ plt.close()
194
+ logger.info(f"Expert activation heatmap saved to {save_path}")
195
+
196
+
197
+ def run_part1_training(
198
+ ablation_variant: str = "dense",
199
+ config: Optional[TransformerConfig] = None,
200
+ dataset_name: str = "belumind/en-vi-ja-curated-500k-triplets",
201
+ max_samples: Optional[int] = 10000,
202
+ batch_size: int = 32,
203
+ epochs: int = 1,
204
+ max_steps: Optional[int] = None,
205
+ device: Optional[torch.device] = None,
206
+ output_dir: str = "assets/part1",
207
+ save_checkpoint_dir: str = "checkpoints/part1"
208
+ ) -> Dict[str, Any]:
209
+ """
210
+ End-to-end training and evaluation pipeline for Part 1 MoE ablations.
211
+ Trains model, extracts expert language specialization, and saves heatmaps.
212
+ """
213
+ os.makedirs(output_dir, exist_ok=True)
214
+ os.makedirs(save_checkpoint_dir, exist_ok=True)
215
+
216
+ if device is None:
217
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
218
+
219
+ if config is None:
220
+ config = TransformerConfig(ablation_variant=ablation_variant)
221
+
222
+ logger.info(f"Initializing model for ablation: {ablation_variant.upper()} on device {device}")
223
+ model = Transformer(config).to(device)
224
+
225
+ # Load or build multilingual byte-level BPE tokenizer
226
+ tokenizer = build_or_load_tokenizer(vocab_size=config.N_vocab)
227
+
228
+ # Load translation dataset
229
+ logger.info(f"Loading dataset {dataset_name} (max_samples={max_samples})...")
230
+ try:
231
+ from datasets import load_dataset
232
+ ds = load_dataset(dataset_name, split="train")
233
+ raw_samples = []
234
+ for i, item in enumerate(ds):
235
+ if max_samples is not None and i >= max_samples:
236
+ break
237
+ raw_samples.append(item)
238
+ except Exception as e:
239
+ logger.warning(f"Could not load Hugging Face dataset ({e}). Using demonstration seed corpus.")
240
+ raw_samples = [
241
+ {"en": "Hello, how are you today?", "vi": "Xin chào, hôm nay bạn thế nào?", "ja": "こんにちは、今日はお元気ですか?"},
242
+ {"en": "Machine translation with neural networks is fascinating.", "vi": "Dịch máy với mạng nơ-ron rất hấp dẫn.", "ja": "ニューラルネットワークによる機械翻訳は非常に興味深い。"},
243
+ {"en": "Mixture of experts scales model capacity efficiently.", "vi": "Mô hình chuyên gia kết hợp mở rộng dung lượng hiệu quả.", "ja": "エキスパートの混合はモデル容量を効率的に拡張します。"},
244
+ {"en": "Artificial intelligence continues to transform language technology.", "vi": "Trí tuệ nhân tạo tiếp tục chuyển đổi công nghệ ngôn ngữ.", "ja": "人工知能は言語技術を変革し続けています。"}
245
+ ] * 200
246
+
247
+ # Split train and validation
248
+ split_idx = int(len(raw_samples) * 0.9)
249
+ train_samples = raw_samples[:split_idx]
250
+ val_samples = raw_samples[split_idx:] if split_idx < len(raw_samples) else raw_samples[:10]
251
+
252
+ train_ds = TranslationDataset(train_samples, tokenizer, max_seq_len=config.N_ctx)
253
+ val_ds = TranslationDataset(val_samples, tokenizer, max_seq_len=config.N_ctx)
254
+
255
+ train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
256
+ val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)
257
+ optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate, weight_decay=config.weight_decay)
258
+
259
+ wandb_logger = None
260
+ if os.environ.get("WANDB_API_KEY") and os.environ.get("WANDB_MODE") != "offline":
261
+ try:
262
+ wandb_logger = WandbLogger(
263
+ project="ANLP-Assignment-2",
264
+ name=f"Part1-MoE-{ablation_variant}",
265
+ config={"ablation": ablation_variant, "epochs": epochs, "samples": max_samples}
266
+ )
267
+ except Exception as e:
268
+ logger.warning(f"Could not initialize WandB ({e}).")
269
+
270
+ logger.info(f"Starting training for {epochs} epoch(s)...")
271
+ step_offset = 0
272
+ for epoch in range(epochs):
273
+ avg_loss, step_offset = train_epoch(
274
+ model,
275
+ train_loader,
276
+ optimizer,
277
+ device,
278
+ wandb_logger=wandb_logger,
279
+ step_offset=step_offset
280
+ )
281
+ logger.info(f"Epoch {epoch + 1}/{epochs} | Average Training Loss: {avg_loss:.4f}")
282
+
283
+ # Evaluate and extract expert heatmaps
284
+ logger.info("Evaluating model and extracting language routing decisions...")
285
+ eval_results = evaluate_and_extract_heatmaps(model, val_loader, tokenizer, device)
286
+
287
+ if wandb_logger:
288
+ wandb_logger.log({
289
+ "val/loss": eval_results["loss"],
290
+ "val/perplexity": eval_results["perplexity"],
291
+ "val/bleu": eval_results["bleu"]
292
+ })
293
+ wandb_logger.finish()
294
+
295
+ # Save heatmap if MoE
296
+ if config.use_moe:
297
+ heatmap_path = os.path.join(output_dir, f"expert_heatmap_{ablation_variant}.png")
298
+ save_expert_heatmap(eval_results["expert_activity"], save_path=heatmap_path, variant_name=ablation_variant)
299
+ # Also copy to root assets
300
+ save_expert_heatmap(eval_results["expert_activity"], save_path="assets/expert_heatmap.png", variant_name=ablation_variant)
301
+
302
+ # Save checkpoint
303
+ ckpt_path = os.path.join(save_checkpoint_dir, f"model_{ablation_variant}.pt")
304
+ torch.save({
305
+ "model_state_dict": model.state_dict(),
306
+ "config": config,
307
+ "eval_results": {k: v for k, v in eval_results.items() if k != "expert_activity"}
308
+ }, ckpt_path)
309
+ logger.info(f"Checkpoint saved to {ckpt_path}")
310
+
311
+ # Save evaluation summary
312
+ summary_path = os.path.join(output_dir, f"summary_{ablation_variant}.json")
313
+ with open(summary_path, "w") as f:
314
+ json.dump({
315
+ "variant": ablation_variant,
316
+ "loss": eval_results["loss"],
317
+ "perplexity": eval_results["perplexity"],
318
+ "bleu": eval_results["bleu"],
319
+ "sample_predictions": eval_results["sample_predictions"],
320
+ "sample_references": eval_results["sample_references"]
321
+ }, f, indent=2)
322
+
323
+ return eval_results
src/part2/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 2: Custom Optimizers and Pretraining Dynamics
3
+ """
4
+
5
+ from .optimizers import (
6
+ CustomAdamW,
7
+ CustomAdan,
8
+ CustomLion,
9
+ CustomMuon,
10
+ CustomSophia,
11
+ )
12
+ from .dataset import PretrainDataset
13
+ from .train import get_optimizer, evaluate_val_and_bleu, run_optimizer_benchmark, plot_comparative_dynamics
14
+
15
+ __all__ = [
16
+ "CustomAdamW",
17
+ "CustomAdan",
18
+ "CustomLion",
19
+ "CustomMuon",
20
+ "CustomSophia",
21
+ "PretrainDataset",
22
+ "get_optimizer",
23
+ "evaluate_val_and_bleu",
24
+ "run_optimizer_benchmark",
25
+ "plot_comparative_dynamics",
26
+ ]
src/part2/dataset.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset module for Part 2: Pretraining on browndw/human-ai-parallel-corpus.
3
+ Implements next-token prediction streaming and chunking for Chinchilla-style training dynamics.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional, Tuple
7
+ import torch
8
+ from torch.utils.data import Dataset, DataLoader
9
+ from transformers import PreTrainedTokenizerFast
10
+
11
+
12
+ class PretrainDataset(Dataset):
13
+ """
14
+ Dataset for next-token prediction language modeling.
15
+ Extracts fixed-length token chunks from continuous document texts.
16
+ """
17
+ def __init__(
18
+ self,
19
+ texts: List[str],
20
+ tokenizer: PreTrainedTokenizerFast,
21
+ seq_len: int = 256,
22
+ prefix_eval_ratio: float = 0.5
23
+ ):
24
+ self.seq_len = seq_len
25
+ self.tokenizer = tokenizer
26
+ self.prefix_eval_ratio = prefix_eval_ratio
27
+ self.samples: List[torch.Tensor] = []
28
+ self.eval_pairs: List[Tuple[str, str]] = []
29
+
30
+ for text in texts:
31
+ if not text or not text.strip():
32
+ continue
33
+ enc = tokenizer(text, truncation=False, return_tensors="pt")["input_ids"].squeeze(0)
34
+ if len(enc) < 8:
35
+ continue
36
+
37
+ # Split into chunks of size seq_len
38
+ for i in range(0, len(enc) - seq_len, seq_len):
39
+ chunk = enc[i : i + seq_len]
40
+ self.samples.append(chunk)
41
+
42
+ # Store prefix and continuation for BLEU evaluation
43
+ split_idx = int(len(text) * prefix_eval_ratio)
44
+ prompt = text[:split_idx].strip()
45
+ continuation = text[split_idx:].strip()
46
+ if prompt and continuation:
47
+ self.eval_pairs.append((prompt, continuation))
48
+
49
+ def __len__(self) -> int:
50
+ return len(self.samples)
51
+
52
+ def __getitem__(self, idx: int) -> Dict[str, torch.Tensor]:
53
+ chunk = self.samples[idx]
54
+ return {
55
+ "input_ids": chunk,
56
+ "targets": chunk
57
+ }
src/part2/optimizers.py ADDED
@@ -0,0 +1,482 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 2: Custom Optimizers from Scratch
3
+ Strictly subclassing torch.optim.Optimizer without predefined torch.optim modules.
4
+ Covers all required categories from 'Fantastic Pretraining Optimizers and Where to Find Them' (2025):
5
+ 1. Category 1: AdamW (Decoupled weight decay)
6
+ 2. Category 2: Adan (Adaptive Nesterov Momentum - Variance-Reduced AdamW)
7
+ 3. Category 3: Lion (Memory-Efficient Sign-Momentum)
8
+ 4. Category 4: Muon (Matrix-Based Polar Newton-Schulz Optimizer)
9
+ 5. Bonus: Sophia (Hessian-Based Clipped Stochastic Optimizer)
10
+ """
11
+
12
+ import math
13
+ from typing import Callable, Iterable, Optional, Tuple, Union, Dict, Any
14
+ import torch
15
+ from torch.optim import Optimizer
16
+
17
+
18
+ class CustomAdamW(Optimizer):
19
+ """
20
+ Category 1: AdamW with decoupled weight decay (Loshchilov & Hutter, 2019).
21
+ theta_t = theta_{t-1} - lr * (m_hat / (sqrt(v_hat) + eps) + weight_decay * theta_{t-1})
22
+ """
23
+ def __init__(
24
+ self,
25
+ params: Iterable[torch.nn.Parameter],
26
+ lr: float = 1e-3,
27
+ betas: Tuple[float, float] = (0.9, 0.999),
28
+ eps: float = 1e-8,
29
+ weight_decay: float = 0.01
30
+ ):
31
+ if lr < 0.0:
32
+ raise ValueError(f"Invalid learning rate: {lr}")
33
+ if not 0.0 <= betas[0] < 1.0:
34
+ raise ValueError(f"Invalid beta1: {betas[0]}")
35
+ if not 0.0 <= betas[1] < 1.0:
36
+ raise ValueError(f"Invalid beta2: {betas[1]}")
37
+ if eps < 0.0:
38
+ raise ValueError(f"Invalid epsilon: {eps}")
39
+ if weight_decay < 0.0:
40
+ raise ValueError(f"Invalid weight_decay: {weight_decay}")
41
+
42
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
43
+ super().__init__(params, defaults)
44
+
45
+ @torch.no_grad()
46
+ def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]:
47
+ loss = None
48
+ if closure is not None:
49
+ with torch.enable_grad():
50
+ loss = closure()
51
+
52
+ for group in self.param_groups:
53
+ lr = group["lr"]
54
+ beta1, beta2 = group["betas"]
55
+ eps = group["eps"]
56
+ wd = group["weight_decay"]
57
+
58
+ for p in group["params"]:
59
+ if p.grad is None:
60
+ continue
61
+ grad = p.grad
62
+ if grad.is_sparse:
63
+ raise RuntimeError("CustomAdamW does not support sparse gradients")
64
+
65
+ state = self.state[p]
66
+ if len(state) == 0:
67
+ state["step"] = 0
68
+ state["exp_avg"] = torch.zeros_like(p, memory_format=torch.preserve_format)
69
+ state["exp_avg_sq"] = torch.zeros_like(p, memory_format=torch.preserve_format)
70
+
71
+ state["step"] += 1
72
+ step = state["step"]
73
+ exp_avg = state["exp_avg"]
74
+ exp_avg_sq = state["exp_avg_sq"]
75
+
76
+ # 1. Perform decoupled weight decay
77
+ if wd != 0.0:
78
+ p.mul_(1.0 - lr * wd)
79
+
80
+ # 2. Update biased 1st and 2nd moment estimates
81
+ exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1)
82
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
83
+
84
+ # 3. Bias corrections
85
+ bias_correction1 = 1.0 - beta1 ** step
86
+ bias_correction2 = 1.0 - beta2 ** step
87
+
88
+ step_size = lr / bias_correction1
89
+ denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps)
90
+
91
+ p.addcdiv_(exp_avg, denom, value=-step_size)
92
+
93
+ return loss
94
+
95
+
96
+ class CustomAdan(Optimizer):
97
+ """
98
+ Category 2: Adan (Adaptive Nesterov Momentum Algorithm, Xie et al., 2022).
99
+ Variance-reduced AdamW variant incorporating gradient differences to estimate
100
+ future gradient trajectory and damp oscillations.
101
+ """
102
+ def __init__(
103
+ self,
104
+ params: Iterable[torch.nn.Parameter],
105
+ lr: float = 1e-3,
106
+ betas: Tuple[float, float, float] = (0.98, 0.92, 0.99),
107
+ eps: float = 1e-8,
108
+ weight_decay: float = 0.02
109
+ ):
110
+ if lr < 0.0:
111
+ raise ValueError(f"Invalid learning rate: {lr}")
112
+ if eps < 0.0:
113
+ raise ValueError(f"Invalid epsilon: {eps}")
114
+ defaults = dict(lr=lr, betas=betas, eps=eps, weight_decay=weight_decay)
115
+ super().__init__(params, defaults)
116
+
117
+ @torch.no_grad()
118
+ def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]:
119
+ loss = None
120
+ if closure is not None:
121
+ with torch.enable_grad():
122
+ loss = closure()
123
+
124
+ for group in self.param_groups:
125
+ lr = group["lr"]
126
+ beta1, beta2, beta3 = group["betas"]
127
+ eps = group["eps"]
128
+ wd = group["weight_decay"]
129
+
130
+ for p in group["params"]:
131
+ if p.grad is None:
132
+ continue
133
+ grad = p.grad
134
+ if grad.is_sparse:
135
+ raise RuntimeError("CustomAdan does not support sparse gradients")
136
+
137
+ state = self.state[p]
138
+ if len(state) == 0:
139
+ state["step"] = 0
140
+ state["exp_avg"] = torch.zeros_like(p) # m_k: EMA of grad
141
+ state["exp_avg_diff"] = torch.zeros_like(p) # v_k: EMA of grad diff
142
+ state["exp_avg_sq"] = torch.zeros_like(p) # n_k: second moment
143
+ state["prev_grad"] = grad.clone()
144
+
145
+ state["step"] += 1
146
+ step = state["step"]
147
+ exp_avg = state["exp_avg"]
148
+ exp_avg_diff = state["exp_avg_diff"]
149
+ exp_avg_sq = state["exp_avg_sq"]
150
+ prev_grad = state["prev_grad"]
151
+
152
+ b1 = beta1 if beta1 > 0.5 else 1.0 - beta1
153
+ b2 = beta2 if beta2 > 0.5 else 1.0 - beta2
154
+ b3 = beta3 if beta3 > 0.5 else 1.0 - beta3
155
+
156
+ grad_diff = grad - prev_grad
157
+
158
+ # Update 1st moment of grad
159
+ exp_avg.mul_(b1).add_(grad, alpha=1.0 - b1)
160
+
161
+ # Update 1st moment of grad difference
162
+ exp_avg_diff.mul_(b2).add_(grad_diff, alpha=1.0 - b2)
163
+
164
+ # Update 2nd moment of update vector
165
+ update_term = grad + (1.0 - b2) * grad_diff
166
+ exp_avg_sq.mul_(b3).addcmul_(update_term, update_term, value=1.0 - b3)
167
+
168
+ bias_correction1 = 1.0 - b1 ** step
169
+ bias_correction2 = 1.0 - b2 ** step
170
+ bias_correction3 = 1.0 - b3 ** step
171
+
172
+ m_hat = exp_avg / bias_correction1
173
+ v_hat = exp_avg_diff / bias_correction2
174
+ n_hat = exp_avg_sq / bias_correction3
175
+
176
+ denom = n_hat.sqrt().add_(eps)
177
+ step_direction = (m_hat + (1.0 - b2) * v_hat) / denom
178
+
179
+ # Apply parameter update and decoupled weight decay
180
+ p.data.add_(step_direction, alpha=-lr)
181
+ if wd != 0.0:
182
+ p.data.div_(1.0 + lr * wd)
183
+
184
+ prev_grad.copy_(grad)
185
+
186
+ return loss
187
+
188
+
189
+ class CustomLion(Optimizer):
190
+ """
191
+ Category 3: Lion (EvoLved Sign Momentum, Chen et al., Google Brain 2023).
192
+ Memory-efficient optimizer tracking only 1 momentum buffer (half the memory of AdamW).
193
+ Uses coordinate-wise sign operation for uniform step magnitude.
194
+ """
195
+ def __init__(
196
+ self,
197
+ params: Iterable[torch.nn.Parameter],
198
+ lr: float = 1e-4,
199
+ betas: Tuple[float, float] = (0.9, 0.99),
200
+ weight_decay: float = 0.1
201
+ ):
202
+ if lr < 0.0:
203
+ raise ValueError(f"Invalid learning rate: {lr}")
204
+ defaults = dict(lr=lr, betas=betas, weight_decay=weight_decay)
205
+ super().__init__(params, defaults)
206
+
207
+ @torch.no_grad()
208
+ def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]:
209
+ loss = None
210
+ if closure is not None:
211
+ with torch.enable_grad():
212
+ loss = closure()
213
+
214
+ for group in self.param_groups:
215
+ lr = group["lr"]
216
+ beta1, beta2 = group["betas"]
217
+ wd = group["weight_decay"]
218
+
219
+ for p in group["params"]:
220
+ if p.grad is None:
221
+ continue
222
+ grad = p.grad
223
+ if grad.is_sparse:
224
+ raise RuntimeError("CustomLion does not support sparse gradients")
225
+
226
+ state = self.state[p]
227
+ if len(state) == 0:
228
+ state["step"] = 0
229
+ state["exp_avg"] = torch.zeros_like(p)
230
+
231
+ state["step"] += 1
232
+ exp_avg = state["exp_avg"]
233
+
234
+ # 1. Weight decay
235
+ if wd != 0.0:
236
+ p.mul_(1.0 - lr * wd)
237
+
238
+ # 2. Lion update direction: c_t = beta1 * m_{t-1} + (1 - beta1) * grad
239
+ update = exp_avg.clone().mul_(beta1).add_(grad, alpha=1.0 - beta1)
240
+ p.add_(torch.sign(update), alpha=-lr)
241
+
242
+ # 3. Update momentum buffer: m_t = beta2 * m_{t-1} + (1 - beta2) * grad
243
+ exp_avg.mul_(beta2).add_(grad, alpha=1.0 - beta2)
244
+
245
+ return loss
246
+
247
+
248
+ def newton_schulz5(G: torch.Tensor, steps: int = 5, eps: float = 1e-7) -> torch.Tensor:
249
+ """
250
+ Quintic Newton-Schulz iteration for matrix orthogonalization (Muon).
251
+ Computes an approximate polar decomposition (orthogonal projection of G).
252
+ """
253
+ assert G.ndim == 2
254
+ a, b, c = (3.4445, -4.7750, 2.0315)
255
+ X = G.bfloat16() if G.is_cuda else G.float()
256
+ X /= (X.norm() + eps)
257
+
258
+ if X.size(0) > X.size(1):
259
+ X = X.T
260
+ transposed = True
261
+ else:
262
+ transposed = False
263
+
264
+ for _ in range(steps):
265
+ A = X @ X.T
266
+ B = b * A + c * A @ A
267
+ X = a * X + B @ X
268
+
269
+ if transposed:
270
+ X = X.T
271
+
272
+ return X.to(G.dtype)
273
+
274
+
275
+ class CustomMuon(Optimizer):
276
+ """
277
+ Category 4: Muon (Momentum Orthogonalized by Newton-Schulz, 2024/2025).
278
+ Matrix-based optimizer. Applies orthogonalized momentum to 2D weight matrices,
279
+ while falling back to standard AdamW for 1D vectors and non-matrix parameters.
280
+ """
281
+ def __init__(
282
+ self,
283
+ params: Iterable[torch.nn.Parameter],
284
+ lr: float = 0.02,
285
+ momentum: float = 0.95,
286
+ nesterov: bool = True,
287
+ ns_steps: int = 5,
288
+ weight_decay: float = 0.01,
289
+ fallback_lr: float = 1e-3
290
+ ):
291
+ defaults = dict(
292
+ lr=lr,
293
+ momentum=momentum,
294
+ nesterov=nesterov,
295
+ ns_steps=ns_steps,
296
+ weight_decay=weight_decay,
297
+ fallback_lr=fallback_lr,
298
+ betas=(0.9, 0.999),
299
+ eps=1e-8
300
+ )
301
+ super().__init__(params, defaults)
302
+
303
+ @torch.no_grad()
304
+ def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]:
305
+ loss = None
306
+ if closure is not None:
307
+ with torch.enable_grad():
308
+ loss = closure()
309
+
310
+ for group in self.param_groups:
311
+ lr = group["lr"]
312
+ mu = group["momentum"]
313
+ nesterov = group["nesterov"]
314
+ ns_steps = group["ns_steps"]
315
+ wd = group["weight_decay"]
316
+ fallback_lr = group["fallback_lr"]
317
+
318
+ for p in group["params"]:
319
+ if p.grad is None:
320
+ continue
321
+ grad = p.grad
322
+
323
+ state = self.state[p]
324
+ is_matrix = (p.ndim == 2 and min(p.shape) >= 2)
325
+ if len(state) == 0:
326
+ state["step"] = 0
327
+ state["exp_avg"] = torch.zeros_like(p)
328
+ if not is_matrix:
329
+ state["exp_avg_sq"] = torch.zeros_like(p)
330
+
331
+ state["step"] += 1
332
+ step = state["step"]
333
+
334
+ if is_matrix:
335
+ # 2D Matrix parameter: Muon Newton-Schulz update
336
+ exp_avg = state["exp_avg"]
337
+ if wd != 0.0:
338
+ p.mul_(1.0 - lr * wd)
339
+
340
+ exp_avg.mul_(mu).add_(grad)
341
+ if nesterov:
342
+ buf = grad + mu * exp_avg
343
+ else:
344
+ buf = exp_avg
345
+
346
+ ortho_buf = newton_schulz5(buf, steps=ns_steps)
347
+ # RMS-matched scaling: scales update by sqrt(max(n, m)) to match unit RMS
348
+ scale = max(1.0, float(max(p.shape[-2:])) ** 0.5)
349
+ p.add_(ortho_buf, alpha=-lr * scale)
350
+ else:
351
+ # 1D/Non-matrix parameter: Standard AdamW fallback
352
+ exp_avg = state["exp_avg"]
353
+ exp_avg_sq = state["exp_avg_sq"]
354
+ beta1, beta2 = group["betas"]
355
+ eps = group["eps"]
356
+
357
+ if wd != 0.0:
358
+ p.mul_(1.0 - fallback_lr * wd)
359
+
360
+ exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1)
361
+ exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1.0 - beta2)
362
+
363
+ bias_correction1 = 1.0 - beta1 ** step
364
+ bias_correction2 = 1.0 - beta2 ** step
365
+ step_size = fallback_lr / bias_correction1
366
+ denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps)
367
+ p.addcdiv_(exp_avg, denom, value=-step_size)
368
+
369
+ return loss
370
+
371
+
372
+ class CustomSophia(Optimizer):
373
+ """
374
+ Bonus: Sophia (Second-order Clipped Stochastic Optimization, Liu et al., 2023).
375
+ Hessian-based optimizer utilizing EMA of diagonal Hessian estimates and coordinate-wise clipping.
376
+ """
377
+ def __init__(
378
+ self,
379
+ params: Iterable[torch.nn.Parameter],
380
+ lr: float = 1e-3,
381
+ betas: Tuple[float, float] = (0.96, 0.99),
382
+ rho: float = 1.0,
383
+ gamma: float = 0.01,
384
+ eps: float = 1e-12,
385
+ weight_decay: float = 0.01
386
+ ):
387
+ if lr < 0.0:
388
+ raise ValueError(f"Invalid learning rate: {lr}")
389
+ if rho <= 0.0:
390
+ raise ValueError(f"Invalid rho: {rho}")
391
+ defaults = dict(
392
+ lr=lr,
393
+ betas=betas,
394
+ rho=rho,
395
+ gamma=gamma,
396
+ eps=eps,
397
+ weight_decay=weight_decay
398
+ )
399
+ super().__init__(params, defaults)
400
+
401
+ def update_hessian(self) -> None:
402
+ """
403
+ Updates diagonal Hessian estimate using Hutchinson's estimator or gradient-squared approximation.
404
+ """
405
+ for group in self.param_groups:
406
+ beta2 = group["betas"][1]
407
+ for p in group["params"]:
408
+ if p.grad is None:
409
+ continue
410
+ state = self.state[p]
411
+ if "hessian" not in state:
412
+ state["hessian"] = torch.zeros_like(p)
413
+ state["hessian_step"] = 0
414
+ state["hessian_step"] += 1
415
+ state["hessian_called_externally"] = True
416
+ # Diagonal Hessian approximation via stochastic gradient outer product
417
+ h_hat = p.grad.pow(2)
418
+ state["hessian"].mul_(beta2).add_(h_hat, alpha=1.0 - beta2)
419
+
420
+ @torch.no_grad()
421
+ def step(self, closure: Optional[Callable[[], float]] = None) -> Optional[float]:
422
+ loss = None
423
+ if closure is not None:
424
+ with torch.enable_grad():
425
+ loss = closure()
426
+
427
+ for group in self.param_groups:
428
+ lr = group["lr"]
429
+ beta1, beta2 = group["betas"]
430
+ rho = group["rho"]
431
+ gamma = group["gamma"]
432
+ eps = group["eps"]
433
+ wd = group["weight_decay"]
434
+
435
+ for p in group["params"]:
436
+ if p.grad is None:
437
+ continue
438
+ grad = p.grad
439
+ if grad.is_sparse:
440
+ raise RuntimeError("CustomSophia does not support sparse gradients")
441
+
442
+ state = self.state[p]
443
+ if "step" not in state:
444
+ state["step"] = 0
445
+ state["exp_avg"] = torch.zeros_like(p)
446
+ if "hessian" not in state:
447
+ state["hessian"] = torch.zeros_like(p)
448
+ state["hessian_step"] = 0
449
+
450
+ state["step"] += 1
451
+ step = state["step"]
452
+ exp_avg = state["exp_avg"]
453
+ hessian = state["hessian"]
454
+
455
+ # 1. Weight decay
456
+ if wd != 0.0:
457
+ p.mul_(1.0 - lr * wd)
458
+
459
+ # 2. First moment update
460
+ exp_avg.mul_(beta1).add_(grad, alpha=1.0 - beta1)
461
+
462
+ # 3. Bias corrections
463
+ bias_correction1 = 1.0 - beta1 ** step
464
+ m_hat = exp_avg / bias_correction1
465
+
466
+ if state.get("hessian_called_externally", False):
467
+ h_step = max(1, state.get("hessian_step", 1))
468
+ bias_correction2 = 1.0 - beta2 ** h_step
469
+ h_hat = hessian / bias_correction2
470
+ else:
471
+ # Dynamically update diagonal Hessian estimate from grad^2 each step
472
+ hessian.mul_(beta2).add_(grad.pow(2), alpha=1.0 - beta2)
473
+ bias_correction2 = 1.0 - beta2 ** step
474
+ h_hat = hessian / bias_correction2
475
+
476
+ # 4. Scaled and clipped update step (Sophia: clip(m_hat / max(h_hat, gamma), rho))
477
+ denom = torch.clamp(h_hat, min=gamma)
478
+ scaled_step = torch.clamp(m_hat / denom, min=-rho, max=rho)
479
+
480
+ p.add_(scaled_step, alpha=-lr)
481
+
482
+ return loss
src/part2/train.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 2 Pretraining script comparing Custom Optimizers.
3
+ Evaluates validation loss and test BLEU at 0.1x dataset token checkpoints.
4
+ Produces Section 4 / Figure 2 training dynamic plots.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import logging
10
+ from typing import Dict, Any, List, Optional, Tuple
11
+ import torch
12
+ import torch.nn as nn
13
+ from torch.utils.data import DataLoader
14
+ from tqdm import tqdm
15
+ import matplotlib.pyplot as plt
16
+
17
+ from ..part1.config import TransformerConfig
18
+ from ..part1.model import Transformer
19
+ from .optimizers import CustomAdamW, CustomAdan, CustomLion, CustomMuon, CustomSophia
20
+ from ..utils import set_seed, compute_perplexity, compute_bleu, WandbLogger
21
+
22
+ logger = logging.getLogger("Part2_Train")
23
+
24
+
25
+ def get_optimizer(
26
+ optimizer_name: str,
27
+ model: nn.Module,
28
+ lr: float = 1e-3,
29
+ weight_decay: float = 0.01
30
+ ) -> torch.optim.Optimizer:
31
+ """Instantiates custom optimizer without using any predefined torch.optim modules."""
32
+ name = optimizer_name.lower()
33
+ if name == "adamw":
34
+ return CustomAdamW(model.parameters(), lr=lr, weight_decay=weight_decay)
35
+ elif name == "adan":
36
+ return CustomAdan(model.parameters(), lr=lr, weight_decay=weight_decay)
37
+ elif name == "lion":
38
+ # Lion typically operates with 3x-10x lower lr and higher weight decay
39
+ return CustomLion(model.parameters(), lr=lr * 0.2, weight_decay=weight_decay * 5.0)
40
+ elif name == "muon":
41
+ return CustomMuon(model.parameters(), lr=lr * 2.0, fallback_lr=lr, weight_decay=weight_decay)
42
+ elif name == "sophia":
43
+ return CustomSophia(model.parameters(), lr=lr, weight_decay=weight_decay)
44
+ else:
45
+ raise ValueError(f"Unknown optimizer: {optimizer_name}")
46
+
47
+
48
+ @torch.no_grad()
49
+ def evaluate_val_and_bleu(
50
+ model: Transformer,
51
+ val_loader: DataLoader,
52
+ eval_pairs: List[Tuple[str, str]],
53
+ tokenizer: Any,
54
+ device: torch.device,
55
+ max_eval_pairs: int = 200
56
+ ) -> Tuple[float, float, float]:
57
+ """Computes validation loss, validation perplexity, and test BLEU score."""
58
+ model.eval()
59
+ total_loss = 0.0
60
+ total_tokens = 0
61
+
62
+ for batch in val_loader:
63
+ input_ids = batch["input_ids"].to(device)
64
+ targets = batch["targets"].to(device)
65
+
66
+ logits, loss, _ = model(input_ids, targets=targets)
67
+ num_tok = (targets != -100).sum().item()
68
+ total_loss += loss.item() * num_tok
69
+ total_tokens += max(num_tok, 1)
70
+
71
+ val_loss = total_loss / max(total_tokens, 1)
72
+ val_ppl = compute_perplexity(val_loss)
73
+
74
+ # Compute BLEU on prefix-continuation test pairs
75
+ preds, refs = [], []
76
+ for prompt, gold_continuation in eval_pairs[:max_eval_pairs]:
77
+ prompt_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(device)
78
+ if prompt_ids.shape[1] >= model.config.N_ctx:
79
+ continue
80
+
81
+ curr_ids = prompt_ids.clone()
82
+ for _ in range(30):
83
+ if curr_ids.shape[1] >= model.config.N_ctx:
84
+ break
85
+ logits, _, _ = model(curr_ids)
86
+ next_tok = logits[:, -1, :].argmax(dim=-1, keepdim=True)
87
+ curr_ids = torch.cat([curr_ids, next_tok], dim=1)
88
+ if next_tok.item() == tokenizer.eos_token_id:
89
+ break
90
+
91
+ gen_tokens = curr_ids[0, prompt_ids.shape[1]:]
92
+ pred_text = tokenizer.decode(gen_tokens.cpu(), skip_special_tokens=True)
93
+ preds.append(pred_text)
94
+ refs.append([gold_continuation])
95
+
96
+ test_bleu = compute_bleu(preds, refs)
97
+ return val_loss, val_ppl, test_bleu
98
+
99
+
100
+ def run_optimizer_benchmark(
101
+ optimizer_names: List[str],
102
+ train_loader: DataLoader,
103
+ val_loader: DataLoader,
104
+ eval_pairs: List[Tuple[str, str]],
105
+ tokenizer: Any,
106
+ config: TransformerConfig,
107
+ total_budget_tokens: int,
108
+ device: torch.device,
109
+ checkpoints_count: int = 10,
110
+ output_dir: str = "assets/part2"
111
+ ) -> Dict[str, Dict[str, List[float]]]:
112
+ """
113
+ Trains models with different optimizers up to 1.0x total dataset tokens,
114
+ recording val loss and test BLEU at 0.1x dataset intervals (Chinchilla curve).
115
+ """
116
+ os.makedirs(output_dir, exist_ok=True)
117
+ checkpoint_interval_tokens = total_budget_tokens // checkpoints_count
118
+ all_results: Dict[str, Dict[str, List[float]]] = {}
119
+
120
+ for opt_name in optimizer_names:
121
+ logger.info(f"--- Starting training run for Optimizer: {opt_name.upper()} ---")
122
+ set_seed(42)
123
+ model = Transformer(config).to(device)
124
+ optimizer = get_optimizer(opt_name, model, lr=config.learning_rate, weight_decay=config.weight_decay)
125
+
126
+ wandb_logger = None
127
+ if os.environ.get("WANDB_API_KEY") and os.environ.get("WANDB_MODE") != "offline":
128
+ try:
129
+ wandb_logger = WandbLogger(
130
+ project="ANLP-Assignment-2",
131
+ name=f"Part2-Opt-{opt_name}",
132
+ config={"optimizer": opt_name, "budget_tokens": total_budget_tokens}
133
+ )
134
+ except Exception as e:
135
+ logger.warning(f"Could not initialize WandB ({e}).")
136
+
137
+ tokens_seen = 0
138
+ next_checkpoint_tokens = checkpoint_interval_tokens
139
+ history: Dict[str, List[float]] = {
140
+ "tokens": [],
141
+ "val_loss": [],
142
+ "val_ppl": [],
143
+ "test_bleu": []
144
+ }
145
+
146
+ # Initial zero-shot evaluation
147
+ v_loss, v_ppl, t_bleu = evaluate_val_and_bleu(model, val_loader, eval_pairs, tokenizer, device)
148
+ history["tokens"].append(0)
149
+ history["val_loss"].append(v_loss)
150
+ history["val_ppl"].append(v_ppl)
151
+ history["test_bleu"].append(t_bleu)
152
+ if wandb_logger:
153
+ wandb_logger.log({
154
+ "tokens_seen": 0,
155
+ "val/loss": v_loss,
156
+ "val/perplexity": v_ppl,
157
+ "test/bleu": t_bleu
158
+ }, step=0)
159
+
160
+ model.train()
161
+ train_iter = iter(train_loader)
162
+
163
+ while tokens_seen < total_budget_tokens:
164
+ try:
165
+ batch = next(train_iter)
166
+ except StopIteration:
167
+ train_iter = iter(train_loader)
168
+ batch = next(train_iter)
169
+
170
+ input_ids = batch["input_ids"].to(device)
171
+ targets = batch["targets"].to(device)
172
+
173
+ optimizer.zero_grad()
174
+ logits, loss, _ = model(input_ids, targets=targets)
175
+ loss.backward()
176
+
177
+ # Sophia requires occasional hessian update
178
+ if opt_name == "sophia" and hasattr(optimizer, "update_hessian"):
179
+ optimizer.update_hessian()
180
+
181
+ optimizer.step()
182
+
183
+ batch_tokens = input_ids.numel()
184
+ tokens_seen += batch_tokens
185
+
186
+ if tokens_seen >= next_checkpoint_tokens:
187
+ v_loss, v_ppl, t_bleu = evaluate_val_and_bleu(model, val_loader, eval_pairs, tokenizer, device)
188
+ history["tokens"].append(tokens_seen)
189
+ history["val_loss"].append(v_loss)
190
+ history["val_ppl"].append(v_ppl)
191
+ history["test_bleu"].append(t_bleu)
192
+ if wandb_logger:
193
+ wandb_logger.log({
194
+ "tokens_seen": tokens_seen,
195
+ "val/loss": v_loss,
196
+ "val/perplexity": v_ppl,
197
+ "test/bleu": t_bleu
198
+ }, step=tokens_seen)
199
+ logger.info(f"[{opt_name}] Tokens: {tokens_seen}/{total_budget_tokens} | Val Loss: {v_loss:.4f} | BLEU: {t_bleu:.2f}")
200
+ next_checkpoint_tokens += checkpoint_interval_tokens
201
+ model.train()
202
+
203
+ if wandb_logger:
204
+ wandb_logger.finish()
205
+
206
+ all_results[opt_name] = history
207
+
208
+ # Save comparative plots
209
+ plot_comparative_dynamics(all_results, save_path=os.path.join(output_dir, "optimizer_dynamics.png"))
210
+
211
+ with open(os.path.join(output_dir, "optimizer_benchmark_results.json"), "w") as f:
212
+ json.dump(all_results, f, indent=2)
213
+
214
+ return all_results
215
+
216
+
217
+ def plot_comparative_dynamics(
218
+ results: Dict[str, Dict[str, List[float]]],
219
+ save_path: str = "assets/optimizer_dynamics.png"
220
+ ) -> None:
221
+ """Plots Figure 2 equivalent: Validation Loss and Test BLEU vs Tokens."""
222
+ os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
223
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
224
+
225
+ for opt_name, history in results.items():
226
+ tokens = [t / 1e6 for t in history["tokens"]] # In millions of tokens
227
+ ax1.plot(tokens, history["val_loss"], marker="o", label=opt_name.upper())
228
+ ax2.plot(tokens, history["test_bleu"], marker="s", label=opt_name.upper())
229
+
230
+ ax1.set_title("Validation Loss vs Training Tokens (Figure 2 Replication)")
231
+ ax1.set_xlabel("Tokens (Millions)")
232
+ ax1.set_ylabel("Validation Loss")
233
+ ax1.legend()
234
+ ax1.grid(True, linestyle="--", alpha=0.5)
235
+
236
+ ax2.set_title("Test BLEU Score vs Training Tokens")
237
+ ax2.set_xlabel("Tokens (Millions)")
238
+ ax2.set_ylabel("BLEU Score")
239
+ ax2.legend()
240
+ ax2.grid(True, linestyle="--", alpha=0.5)
241
+
242
+ plt.tight_layout()
243
+ plt.savefig(save_path, dpi=200)
244
+ plt.close()
245
+ logger.info(f"Saved optimizer dynamics plot to {save_path}")
246
+
247
+
248
+ def run_part2_pipeline(
249
+ optimizer_names: Optional[List[str]] = None,
250
+ dataset_name: str = "browndw/human-ai-parallel-corpus",
251
+ total_budget_tokens: int = 200_000,
252
+ batch_size: int = 32,
253
+ device: Optional[torch.device] = None,
254
+ output_dir: str = "assets/part2"
255
+ ) -> Dict[str, Any]:
256
+ """
257
+ Turnkey pipeline for benchmarking custom optimizers on browndw/human-ai-parallel-corpus.
258
+ Loads data, trains models, logs intermediate 0.1x token checkpoints, and plots Chinchilla curves.
259
+ """
260
+ from .dataset import PretrainDataset
261
+ from ..part1.dataset import build_or_load_tokenizer
262
+
263
+ os.makedirs(output_dir, exist_ok=True)
264
+ if device is None:
265
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
266
+
267
+ if optimizer_names is None:
268
+ optimizer_names = ["adamw", "adan", "lion", "muon", "sophia"]
269
+
270
+ config = TransformerConfig(ablation_variant="dense", D_model=256, N_ctx=128, D_ff=1024, N_layers=4)
271
+ tokenizer = build_or_load_tokenizer(vocab_size=config.N_vocab)
272
+
273
+ logger.info(f"Loading pretraining corpus: {dataset_name}...")
274
+ try:
275
+ from datasets import load_dataset
276
+ ds = load_dataset(dataset_name, split="train")
277
+ texts = []
278
+ for i, item in enumerate(ds):
279
+ if i >= 5000:
280
+ break
281
+ # Collect text fields
282
+ t = item.get("text_human") or item.get("text") or item.get("text_ai") or ""
283
+ if t:
284
+ texts.append(t)
285
+ except Exception as e:
286
+ logger.warning(f"Could not load Hugging Face dataset ({e}). Using synthetic demonstration corpus.")
287
+ texts = [
288
+ "Language models learn probabilistic distributions over sequence tokens through self-supervised pretraining.",
289
+ "Optimization trajectory depends heavily on learning rate schedules, momentum damping, and gradient geometry.",
290
+ "Second order and matrix based optimizers like Muon and Sophia provide faster loss reduction per token."
291
+ ] * 300
292
+
293
+ split_idx = int(len(texts) * 0.9)
294
+ train_texts = texts[:split_idx]
295
+ val_texts = texts[split_idx:] if split_idx < len(texts) else texts[:10]
296
+
297
+ train_ds = PretrainDataset(train_texts, tokenizer, seq_len=config.N_ctx)
298
+ val_ds = PretrainDataset(val_texts, tokenizer, seq_len=config.N_ctx)
299
+
300
+ train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
301
+ val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False)
302
+
303
+ eval_pairs = val_ds.eval_pairs if len(val_ds.eval_pairs) > 0 else [
304
+ ("Language models learn", "probabilistic distributions over sequence tokens."),
305
+ ("Optimization trajectory depends", "heavily on learning rate schedules.")
306
+ ]
307
+
308
+ results = run_optimizer_benchmark(
309
+ optimizer_names=optimizer_names,
310
+ train_loader=train_loader,
311
+ val_loader=val_loader,
312
+ eval_pairs=eval_pairs,
313
+ tokenizer=tokenizer,
314
+ config=config,
315
+ total_budget_tokens=total_budget_tokens,
316
+ device=device,
317
+ output_dir=output_dir
318
+ )
319
+
320
+ # Also copy plot to assets root
321
+ root_plot = "assets/optimizer_dynamics.png"
322
+ plot_comparative_dynamics(results, save_path=root_plot)
323
+
324
+ return results
src/part3/__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 3: Decoding Strategies Module
3
+ """
4
+
5
+ from .decoding import (
6
+ greedy_decode,
7
+ top_k_decode,
8
+ top_p_decode,
9
+ beam_search_decode,
10
+ BeamHypothesis,
11
+ )
12
+ from .evaluate import (
13
+ compute_generation_perplexity,
14
+ compute_overlap_accuracy,
15
+ run_decoding_benchmark,
16
+ plot_beam_search_scaling,
17
+ )
18
+
19
+ __all__ = [
20
+ "greedy_decode",
21
+ "top_k_decode",
22
+ "top_p_decode",
23
+ "beam_search_decode",
24
+ "BeamHypothesis",
25
+ "compute_generation_perplexity",
26
+ "compute_overlap_accuracy",
27
+ "run_decoding_benchmark",
28
+ "plot_beam_search_scaling",
29
+ ]
src/part3/decoding.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Part 3: Decoding Strategies from Scratch using model.forward()
3
+ Strictly without model.generate() from transformers.
4
+ Implements:
5
+ 1. Greedy decoding
6
+ 2. Top-k sampling (with temperature)
7
+ 3. Top-p (Nucleus) sampling (with temperature)
8
+ 4. Beam Search (parameterized for beam widths 1, 2, and 4)
9
+ """
10
+
11
+ import time
12
+ from typing import List, Dict, Any, Optional, Tuple, Union
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+
17
+
18
+ def _get_logits(outputs: Any) -> torch.Tensor:
19
+ """Robustly extracts 3D logits tensor (B, T, V) from model outputs."""
20
+ if hasattr(outputs, "logits"):
21
+ return outputs.logits
22
+ elif isinstance(outputs, (tuple, list)):
23
+ return outputs[0]
24
+ elif isinstance(outputs, torch.Tensor):
25
+ return outputs
26
+ raise ValueError(f"Unsupported model output type: {type(outputs)}")
27
+
28
+
29
+ @torch.no_grad()
30
+ def greedy_decode(
31
+ model: nn.Module,
32
+ input_ids: torch.Tensor,
33
+ max_new_tokens: int = 40,
34
+ eos_token_id: Optional[int] = None,
35
+ pad_token_id: Optional[int] = None
36
+ ) -> Tuple[torch.Tensor, float, List[float]]:
37
+ """
38
+ Greedy decoding: iteratively picks argmax of next-token logits.
39
+ Returns:
40
+ generated_ids: full sequence of input_ids + generated tokens
41
+ elapsed_time: total execution time in seconds
42
+ log_probs: log-probabilities of each generated token
43
+ """
44
+ start_time = time.perf_counter()
45
+ curr_ids = input_ids.clone()
46
+ token_log_probs: List[float] = []
47
+
48
+ for _ in range(max_new_tokens):
49
+ outputs = model(curr_ids)
50
+ logits = _get_logits(outputs)
51
+ next_token_logits = logits[:, -1, :] # (B, V)
52
+
53
+ log_probs = F.log_softmax(next_token_logits, dim=-1)
54
+ max_log_prob, next_token = log_probs.max(dim=-1, keepdim=True)
55
+
56
+ token_log_probs.append(max_log_prob.item())
57
+ curr_ids = torch.cat([curr_ids, next_token], dim=1)
58
+
59
+ if eos_token_id is not None and next_token.item() == eos_token_id:
60
+ break
61
+
62
+ elapsed_time = time.perf_counter() - start_time
63
+ return curr_ids, elapsed_time, token_log_probs
64
+
65
+
66
+ @torch.no_grad()
67
+ def top_k_decode(
68
+ model: nn.Module,
69
+ input_ids: torch.Tensor,
70
+ k: int = 50,
71
+ temperature: float = 1.0,
72
+ max_new_tokens: int = 40,
73
+ eos_token_id: Optional[int] = None
74
+ ) -> Tuple[torch.Tensor, float, List[float]]:
75
+ """
76
+ Top-k sampling: restricts sampling pool to the top-k most likely tokens.
77
+ """
78
+ start_time = time.perf_counter()
79
+ curr_ids = input_ids.clone()
80
+ token_log_probs: List[float] = []
81
+
82
+ for _ in range(max_new_tokens):
83
+ outputs = model(curr_ids)
84
+ logits = _get_logits(outputs)
85
+ next_token_logits = logits[:, -1, :] / max(temperature, 1e-5)
86
+
87
+ # Retain only top-k logits
88
+ val_topk, _ = torch.topk(next_token_logits, min(k, next_token_logits.size(-1)))
89
+ kth_threshold = val_topk[:, -1].unsqueeze(-1)
90
+ filtered_logits = torch.where(
91
+ next_token_logits < kth_threshold,
92
+ torch.tensor(float("-inf"), device=next_token_logits.device),
93
+ next_token_logits
94
+ )
95
+
96
+ probs = F.softmax(filtered_logits, dim=-1)
97
+ next_token = torch.multinomial(probs, num_samples=1)
98
+
99
+ log_probs = F.log_softmax(next_token_logits, dim=-1)
100
+ selected_log_prob = log_probs.gather(1, next_token).item()
101
+ token_log_probs.append(selected_log_prob)
102
+
103
+ curr_ids = torch.cat([curr_ids, next_token], dim=1)
104
+ if eos_token_id is not None and next_token.item() == eos_token_id:
105
+ break
106
+
107
+ elapsed_time = time.perf_counter() - start_time
108
+ return curr_ids, elapsed_time, token_log_probs
109
+
110
+
111
+ @torch.no_grad()
112
+ def top_p_decode(
113
+ model: nn.Module,
114
+ input_ids: torch.Tensor,
115
+ p: float = 0.9,
116
+ temperature: float = 1.0,
117
+ max_new_tokens: int = 40,
118
+ eos_token_id: Optional[int] = None
119
+ ) -> Tuple[torch.Tensor, float, List[float]]:
120
+ """
121
+ Top-p (Nucleus) sampling: retains the smallest subset of tokens whose
122
+ cumulative probability exceeds threshold p.
123
+ """
124
+ start_time = time.perf_counter()
125
+ curr_ids = input_ids.clone()
126
+ token_log_probs: List[float] = []
127
+
128
+ for _ in range(max_new_tokens):
129
+ outputs = model(curr_ids)
130
+ logits = _get_logits(outputs)
131
+ next_token_logits = logits[:, -1, :] / max(temperature, 1e-5)
132
+
133
+ # Sort logits in descending order
134
+ sorted_logits, sorted_indices = torch.sort(next_token_logits, descending=True, dim=-1)
135
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
136
+
137
+ # Mask tokens with cumulative probability above p
138
+ sorted_indices_to_remove = cumulative_probs > p
139
+ # Shift mask right to ensure at least the first token is kept
140
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
141
+ sorted_indices_to_remove[..., 0] = False
142
+
143
+ # Scatter mask back to original indices
144
+ indices_to_remove = sorted_indices_to_remove.scatter(1, sorted_indices, sorted_indices_to_remove)
145
+ filtered_logits = next_token_logits.masked_fill(indices_to_remove, float("-inf"))
146
+
147
+ probs = F.softmax(filtered_logits, dim=-1)
148
+ next_token = torch.multinomial(probs, num_samples=1)
149
+
150
+ log_probs = F.log_softmax(next_token_logits, dim=-1)
151
+ selected_log_prob = log_probs.gather(1, next_token).item()
152
+ token_log_probs.append(selected_log_prob)
153
+
154
+ curr_ids = torch.cat([curr_ids, next_token], dim=1)
155
+ if eos_token_id is not None and next_token.item() == eos_token_id:
156
+ break
157
+
158
+ elapsed_time = time.perf_counter() - start_time
159
+ return curr_ids, elapsed_time, token_log_probs
160
+
161
+
162
+ class BeamHypothesis:
163
+ """Stores individual beam candidate sequence and cumulative score."""
164
+ def __init__(
165
+ self,
166
+ token_ids: torch.Tensor,
167
+ score: float,
168
+ token_scores: Optional[List[float]] = None,
169
+ is_done: bool = False
170
+ ):
171
+ self.token_ids = token_ids # 1D tensor (seq_len,)
172
+ self.score = score # Cumulative log probability
173
+ self.token_scores = token_scores if token_scores is not None else []
174
+ self.is_done = is_done
175
+
176
+ def normalized_score(self, length_penalty: float = 0.7) -> float:
177
+ """Computes length-normalized score to avoid short-sentence bias."""
178
+ length = self.token_ids.size(0)
179
+ penalty = ((5.0 + length) / 6.0) ** length_penalty
180
+ return self.score / penalty
181
+
182
+
183
+ @torch.no_grad()
184
+ def beam_search_decode(
185
+ model: nn.Module,
186
+ input_ids: torch.Tensor,
187
+ beam_width: int = 4,
188
+ max_new_tokens: int = 40,
189
+ length_penalty: float = 0.7,
190
+ eos_token_id: Optional[int] = None
191
+ ) -> Tuple[torch.Tensor, float, List[float]]:
192
+ """
193
+ Beam search decoding implemented from scratch with width 1, 2, or 4.
194
+ Strictly uses model.forward().
195
+ Returns best beam sequence, elapsed time, and per-token log probabilities.
196
+ """
197
+ start_time = time.perf_counter()
198
+ device = input_ids.device
199
+ initial_tokens = input_ids[0] if input_ids.ndim > 1 else input_ids # (prompt_len,)
200
+
201
+ hypotheses = [BeamHypothesis(token_ids=initial_tokens, score=0.0, token_scores=[], is_done=False)]
202
+ completed: List[BeamHypothesis] = []
203
+
204
+ for step in range(max_new_tokens):
205
+ if len(hypotheses) == 0:
206
+ break
207
+
208
+ all_candidates: List[BeamHypothesis] = []
209
+
210
+ for hyp in hypotheses:
211
+ if hyp.is_done:
212
+ completed.append(hyp)
213
+ continue
214
+
215
+ curr_input = hyp.token_ids.unsqueeze(0) # (1, curr_len)
216
+ outputs = model(curr_input)
217
+ logits = _get_logits(outputs)
218
+ next_logits = logits[0, -1, :] # (V,)
219
+ log_probs = F.log_softmax(next_logits, dim=-1)
220
+
221
+ top_log_probs, top_indices = torch.topk(log_probs, k=beam_width)
222
+
223
+ for i in range(beam_width):
224
+ tok_id = top_indices[i].unsqueeze(0)
225
+ tok_score = top_log_probs[i].item()
226
+ new_tokens = torch.cat([hyp.token_ids, tok_id], dim=0)
227
+ new_score = hyp.score + tok_score
228
+ new_scores = hyp.token_scores + [tok_score]
229
+ is_done = (eos_token_id is not None and tok_id.item() == eos_token_id)
230
+
231
+ candidate = BeamHypothesis(
232
+ token_ids=new_tokens,
233
+ score=new_score,
234
+ token_scores=new_scores,
235
+ is_done=is_done
236
+ )
237
+ if is_done:
238
+ completed.append(candidate)
239
+ else:
240
+ all_candidates.append(candidate)
241
+
242
+ if len(all_candidates) == 0:
243
+ break
244
+
245
+ # Sort candidates by normalized score
246
+ all_candidates.sort(key=lambda h: h.normalized_score(length_penalty), reverse=True)
247
+ hypotheses = all_candidates[:beam_width]
248
+
249
+ # Early termination: if completed hypotheses match beam_width and dominate active
250
+ if len(completed) >= beam_width:
251
+ best_completed_score = max(h.normalized_score(length_penalty) for h in completed)
252
+ best_hyp_score = hypotheses[0].normalized_score(length_penalty)
253
+ if best_completed_score >= best_hyp_score:
254
+ break
255
+
256
+ all_results = completed + hypotheses
257
+ if not all_results:
258
+ return input_ids.clone(), time.perf_counter() - start_time, [0.0]
259
+
260
+ all_results.sort(key=lambda h: h.normalized_score(length_penalty), reverse=True)
261
+ best_hyp = all_results[0]
262
+
263
+ elapsed_time = time.perf_counter() - start_time
264
+ best_tokens = best_hyp.token_ids.unsqueeze(0)
265
+ scores = best_hyp.token_scores if len(best_hyp.token_scores) > 0 else [best_hyp.score]
266
+ return best_tokens, elapsed_time, scores
src/part3/evaluate.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Evaluation benchmark for Part 3 Decoding Strategies.
3
+ Loads EleutherAI/Pythia-160M and hamishivi/ROCStories.
4
+ Computes Perplexity, Accuracy, Latency, and saves Raw Generations.
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import time
10
+ import math
11
+ import logging
12
+ from typing import Dict, Any, List, Optional, Tuple
13
+ import numpy as np
14
+ import torch
15
+ import torch.nn.functional as F
16
+ import matplotlib.pyplot as plt
17
+ from tqdm import tqdm
18
+ from transformers import AutoTokenizer, AutoModelForCausalLM
19
+ from datasets import load_dataset
20
+
21
+ from .decoding import (
22
+ greedy_decode,
23
+ top_k_decode,
24
+ top_p_decode,
25
+ beam_search_decode,
26
+ _get_logits,
27
+ )
28
+ from ..utils import compute_perplexity, compute_bleu
29
+
30
+ logger = logging.getLogger("Part3_Eval")
31
+
32
+
33
+ def compute_generation_perplexity(
34
+ model: torch.nn.Module,
35
+ input_ids: torch.Tensor,
36
+ gen_ids: torch.Tensor
37
+ ) -> float:
38
+ """
39
+ Computes perplexity of the generated tokens conditioned on input_ids.
40
+ PPL = exp(-1/T * sum log P(x_t | x_{<t}))
41
+ """
42
+ if gen_ids.size(1) <= input_ids.size(1):
43
+ return 1.0
44
+
45
+ prompt_len = input_ids.size(1)
46
+ with torch.no_grad():
47
+ outputs = model(gen_ids)
48
+ logits = _get_logits(outputs)
49
+
50
+ # Shift logits and labels for generated tokens
51
+ shift_logits = logits[:, prompt_len - 1 : -1, :].contiguous()
52
+ shift_labels = gen_ids[:, prompt_len:].contiguous()
53
+
54
+ loss = F.cross_entropy(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
55
+ return compute_perplexity(loss.item())
56
+
57
+
58
+ def compute_overlap_accuracy(pred_text: str, gold_text: str) -> float:
59
+ """
60
+ Computes token-level accuracy (Jaccard token overlap) against reference continuation.
61
+ """
62
+ pred_tokens = set(pred_text.lower().split())
63
+ gold_tokens = set(gold_text.lower().split())
64
+ if not pred_tokens or not gold_tokens:
65
+ return 0.0
66
+ intersection = pred_tokens.intersection(gold_tokens)
67
+ union = pred_tokens.union(gold_tokens)
68
+ return float(len(intersection) / len(union) * 100.0)
69
+
70
+
71
+ def run_decoding_benchmark(
72
+ model_name: str = "EleutherAI/Pythia-160M",
73
+ dataset_name: str = "hamishivi/ROCStories",
74
+ num_samples: int = 1000,
75
+ device: Optional[torch.device] = None,
76
+ output_dir: str = "assets/part3"
77
+ ) -> Dict[str, Any]:
78
+ """
79
+ Evaluates greedy, top-k, top-p, and beam search (1, 2, 4) on ROCStories.
80
+ """
81
+ os.makedirs(output_dir, exist_ok=True)
82
+ if device is None:
83
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
84
+
85
+ logger.info(f"Loading tokenizer and model: {model_name}")
86
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
87
+ if tokenizer.pad_token is None:
88
+ tokenizer.pad_token = tokenizer.eos_token
89
+
90
+ model = AutoModelForCausalLM.from_pretrained(model_name)
91
+ model.to(device)
92
+ model.eval()
93
+
94
+ logger.info(f"Loading dataset: {dataset_name}")
95
+ try:
96
+ ds = load_dataset(dataset_name, split="test")
97
+ except Exception as e:
98
+ logger.warning(f"Could not load split 'test' directly ({e}), trying train/validation.")
99
+ try:
100
+ ds = load_dataset(dataset_name, split="train")
101
+ except Exception as e2:
102
+ logger.warning(f"Could not load Hugging Face dataset ({e2}). Using demonstration narrative corpus.")
103
+ ds = [
104
+ {"sentence1": "Tom wanted to learn to play the guitar.", "sentence2": "He bought a used guitar from a pawn shop.", "sentence3": "He practiced every single day for months.", "sentence4": "Soon his fingers were strong and agile.", "sentence5": "He played a song for his friends who applauded him."},
105
+ {"sentence1": "Sarah went to the grocery store to buy ingredients.", "sentence2": "She needed flour, sugar, and cocoa powder.", "sentence3": "Back home, she mixed everything in a bowl.", "sentence4": "She baked the cake in the oven for thirty minutes.", "sentence5": "The chocolate cake turned out absolutely delicious."},
106
+ ] * 500
107
+
108
+ # Select num_samples with robust support for ROCStories schemas
109
+ samples = []
110
+ for item in ds:
111
+ if len(samples) >= num_samples:
112
+ break
113
+ prompt, continuation = None, None
114
+ if "sentence1" in item and "sentence5" in item:
115
+ # Standard ROCStories five-sentence narrative structure
116
+ s1 = (item.get("sentence1") or "").strip()
117
+ s2 = (item.get("sentence2") or "").strip()
118
+ s3 = (item.get("sentence3") or "").strip()
119
+ s4 = (item.get("sentence4") or "").strip()
120
+ s5 = (item.get("sentence5") or "").strip()
121
+ prompt = f"{s1} {s2} {s3} {s4}".strip()
122
+ continuation = s5
123
+ elif "prompt" in item and "continuation" in item:
124
+ prompt = (item.get("prompt") or "").strip()
125
+ continuation = (item.get("continuation") or "").strip()
126
+ elif "text" in item:
127
+ text = (item.get("text") or "").strip()
128
+ parts = [s.strip() for s in text.split(".") if s.strip()]
129
+ if len(parts) >= 2:
130
+ prompt = ". ".join(parts[:-1]) + "."
131
+ continuation = parts[-1] + "."
132
+
133
+ if prompt and continuation:
134
+ samples.append((prompt, continuation))
135
+
136
+ logger.info(f"Evaluating on {len(samples)} samples...")
137
+
138
+ strategies = [
139
+ {"name": "greedy", "fn": lambda m, ids: greedy_decode(m, ids, eos_token_id=tokenizer.eos_token_id)},
140
+ {"name": "top_k", "fn": lambda m, ids: top_k_decode(m, ids, k=40, temperature=0.8, eos_token_id=tokenizer.eos_token_id)},
141
+ {"name": "top_p", "fn": lambda m, ids: top_p_decode(m, ids, p=0.9, temperature=0.8, eos_token_id=tokenizer.eos_token_id)},
142
+ {"name": "beam_1", "fn": lambda m, ids: beam_search_decode(m, ids, beam_width=1, eos_token_id=tokenizer.eos_token_id)},
143
+ {"name": "beam_2", "fn": lambda m, ids: beam_search_decode(m, ids, beam_width=2, eos_token_id=tokenizer.eos_token_id)},
144
+ {"name": "beam_4", "fn": lambda m, ids: beam_search_decode(m, ids, beam_width=4, eos_token_id=tokenizer.eos_token_id)},
145
+ ]
146
+
147
+ results_summary: Dict[str, Dict[str, Any]] = {}
148
+ raw_generations: Dict[str, List[Dict[str, Any]]] = {}
149
+
150
+ for strat in strategies:
151
+ strat_name = strat["name"]
152
+ logger.info(f"--- Running decoding strategy: {strat_name} ---")
153
+
154
+ total_time = 0.0
155
+ perplexities = []
156
+ accuracies = []
157
+ bleu_preds = []
158
+ bleu_refs = []
159
+ strat_generations = []
160
+
161
+ for prompt, gold_continuation in tqdm(samples, desc=strat_name, leave=False):
162
+ inputs = tokenizer(prompt, return_tensors="pt").to(device)
163
+ input_ids = inputs["input_ids"]
164
+
165
+ gen_ids, elapsed, _ = strat["fn"](model, input_ids)
166
+ total_time += elapsed
167
+
168
+ pred_tokens = gen_ids[0, input_ids.shape[1]:]
169
+ pred_text = tokenizer.decode(pred_tokens, skip_special_tokens=True).strip()
170
+
171
+ ppl = compute_generation_perplexity(model, input_ids, gen_ids)
172
+ acc = compute_overlap_accuracy(pred_text, gold_continuation)
173
+
174
+ perplexities.append(ppl)
175
+ accuracies.append(acc)
176
+ bleu_preds.append(pred_text)
177
+ bleu_refs.append([gold_continuation])
178
+
179
+ strat_generations.append({
180
+ "prompt": prompt,
181
+ "gold_continuation": gold_continuation,
182
+ "generated": pred_text,
183
+ "ppl": ppl,
184
+ "acc": acc,
185
+ "time_sec": elapsed
186
+ })
187
+
188
+ avg_ppl = float(np.mean([p for p in perplexities if not math.isinf(p)])) if perplexities else float("nan")
189
+ avg_acc = float(np.mean(accuracies))
190
+ bleu_score = compute_bleu(bleu_preds, bleu_refs)
191
+
192
+ results_summary[strat_name] = {
193
+ "mean_perplexity": avg_ppl,
194
+ "mean_accuracy": avg_acc,
195
+ "bleu_score": bleu_score,
196
+ "total_time_seconds": total_time,
197
+ "seconds_per_sample": total_time / len(samples)
198
+ }
199
+ raw_generations[strat_name] = strat_generations
200
+
201
+ # Save outputs
202
+ with open(os.path.join(output_dir, "decoding_summary.json"), "w") as f:
203
+ json.dump(results_summary, f, indent=2)
204
+
205
+ with open(os.path.join(output_dir, "raw_generations.json"), "w") as f:
206
+ json.dump(raw_generations, f, indent=2)
207
+
208
+ # Plot beam search runtime scaling
209
+ plot_beam_search_scaling(results_summary, save_path=os.path.join(output_dir, "beam_search_runtime.png"))
210
+
211
+ return {"summary": results_summary, "raw_generations": raw_generations}
212
+
213
+
214
+ def plot_beam_search_scaling(
215
+ results: Dict[str, Dict[str, Any]],
216
+ save_path: str = "assets/beam_search_runtime.png"
217
+ ) -> None:
218
+ """Plots beam search latency vs beam width (1, 2, 4)."""
219
+ beam_widths = [1, 2, 4]
220
+ times = []
221
+ for w in beam_widths:
222
+ key = f"beam_{w}"
223
+ if key in results:
224
+ times.append(results[key]["total_time_seconds"])
225
+
226
+ if len(times) == len(beam_widths):
227
+ os.makedirs(os.path.dirname(os.path.abspath(save_path)), exist_ok=True)
228
+ plt.figure(figsize=(7, 5))
229
+ plt.plot(beam_widths, times, marker="o", color="crimson", linewidth=2.5, markersize=8)
230
+ plt.title("Beam Search Execution Time vs Beam Width")
231
+ plt.xlabel("Beam Width (B)")
232
+ plt.ylabel("Execution Time (Seconds)")
233
+ plt.xticks(beam_widths)
234
+ plt.grid(True, linestyle="--", alpha=0.6)
235
+ plt.tight_layout()
236
+ plt.savefig(save_path, dpi=200)
237
+ plt.close()
238
+ logger.info(f"Beam search scaling plot saved to {save_path}")