Upload ANLP Assignment 2 Model Card, Configs, Report, and Evaluation Deliverables
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- ANLP_Assignment2_Colab_Runner.ipynb +463 -0
- README.md +117 -0
- assets/.gitkeep +1 -0
- assets/beam_search_runtime.png +3 -0
- assets/expert_heatmap.png +3 -0
- assets/optimizer_dynamics.png +3 -0
- assets/part1/expert_heatmap_matched_active.png +3 -0
- assets/part1/expert_heatmap_shared_routed.png +3 -0
- assets/part1/expert_heatmap_top1.png +3 -0
- assets/part1/expert_heatmap_top2.png +3 -0
- assets/part1/summary_dense.json +30 -0
- assets/part1/summary_matched_active.json +30 -0
- assets/part1/summary_shared_routed.json +30 -0
- assets/part1/summary_top1.json +30 -0
- assets/part1/summary_top2.json +30 -0
- assets/part2/optimizer_benchmark_results.json +272 -0
- assets/part2/optimizer_dynamics.png +3 -0
- assets/part3/beam_search_runtime.png +3 -0
- assets/part3/decoding_summary.json +44 -0
- assets/part3/raw_generations.json +0 -0
- assets/tokenizer_multilingual.json +846 -0
- config.json +20 -0
- configs/part1_dense.yaml +16 -0
- configs/part1_moe_matched_active.yaml +18 -0
- configs/part1_moe_shared_routed.yaml +20 -0
- configs/part1_moe_top1.yaml +19 -0
- configs/part1_moe_top2.yaml +19 -0
- configs/part2_adamw.yaml +5 -0
- configs/part2_adan.yaml +5 -0
- configs/part2_lion.yaml +4 -0
- configs/part2_muon.yaml +7 -0
- configs/part2_sophia.yaml +7 -0
- configs/part3_decoding.yaml +22 -0
- generation_config.json +11 -0
- main.py +197 -0
- report.pdf +3 -0
- src/part1/__init__.py +25 -0
- src/part1/config.py +129 -0
- src/part1/dataset.py +171 -0
- src/part1/model.py +202 -0
- src/part1/moe.py +183 -0
- src/part1/train.py +323 -0
- src/part2/__init__.py +26 -0
- src/part2/dataset.py +57 -0
- src/part2/optimizers.py +482 -0
- src/part2/train.py +324 -0
- src/part3/__init__.py +29 -0
- src/part3/decoding.py +266 -0
- 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 |
+
"[](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 |
+
[](https://forge.coreweave.com/wandb/chiranjeevi-kumaravadivel-iiit-hyderabad/ANLP-Assignment-2)
|
| 35 |
+
[](https://github.com/Chiranjeevi-2020/ANLP_assignment2)
|
| 36 |
+
[](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
|
assets/expert_heatmap.png
ADDED
|
Git LFS Details
|
assets/optimizer_dynamics.png
ADDED
|
Git LFS Details
|
assets/part1/expert_heatmap_matched_active.png
ADDED
|
Git LFS Details
|
assets/part1/expert_heatmap_shared_routed.png
ADDED
|
Git LFS Details
|
assets/part1/expert_heatmap_top1.png
ADDED
|
Git LFS Details
|
assets/part1/expert_heatmap_top2.png
ADDED
|
Git LFS Details
|
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
|
assets/part3/beam_search_runtime.png
ADDED
|
Git LFS Details
|
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}")
|