File size: 6,362 Bytes
5407bdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | """Helpers shared by all three parts: tokenizer training, seeding, logging."""
from __future__ import annotations
import json
import random
import time
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
import torch
if TYPE_CHECKING: # only for the return annotation below
from tokenizers import Tokenizer
# Repository root (this file lives at <root>/src/utils.py).
ROOT = Path(__file__).resolve().parent.parent
ASSETS = ROOT / "assets"
RUNS = ROOT / "runs" # checkpoints + metrics; git-ignored
def set_seed(seed: int = 1337):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
def device_of(prefer: str = "cuda") -> torch.device:
return torch.device(prefer if torch.cuda.is_available() else "cpu")
def human(n: float) -> str:
for unit in ["", "K", "M", "B"]:
if abs(n) < 1000:
return f"{n:.2f}{unit}"
n /= 1000.0
return f"{n:.2f}T"
# ---------------------------------------------------------------------------
# Tokenizer
# ---------------------------------------------------------------------------
SPECIALS = ["<pad>", "<bos>", "<eos>", "<unk>", "<vi>", "<ja>", "<en>"]
def train_bpe(text_iter, vocab_size: int, save_path: Path,
specials=SPECIALS) -> "Tokenizer":
"""Train a byte-level BPE tokenizer and save it to ``save_path``.
Byte-level is the point here: Vietnamese diacritics and Japanese kana and
kanji are never romanised in the dataset, so a character- or word-level
vocabulary built from an ASCII assumption would fall apart. Working over
raw UTF-8 bytes means every string is representable with no <unk>, and BPE
then learns whatever multi-byte sequences are frequent enough to earn a
slot -- which for Japanese ends up being common kanji and kana bigrams.
"""
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders
save_path = Path(save_path)
save_path.parent.mkdir(parents=True, exist_ok=True)
tok = Tokenizer(models.BPE(unk_token=None))
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tok.decoder = decoders.ByteLevel()
trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
special_tokens=list(specials),
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
show_progress=True,
)
tok.train_from_iterator(text_iter, trainer=trainer)
tok.save(str(save_path))
return tok
def load_tokenizer(path: Path):
from tokenizers import Tokenizer
return Tokenizer.from_file(str(path))
# ---------------------------------------------------------------------------
# Metrics I/O
# ---------------------------------------------------------------------------
def save_json(obj, path: Path):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(obj, f, indent=2, default=float)
def load_json(path: Path):
with open(path) as f:
return json.load(f)
class Timer:
def __init__(self):
self.t0 = time.perf_counter()
def lap(self) -> float:
t = time.perf_counter()
dt = t - self.t0
self.t0 = t
return dt
@property
def elapsed(self) -> float:
return time.perf_counter() - self.t0
# ---------------------------------------------------------------------------
# Plot style -- one place so every figure in the report matches
# ---------------------------------------------------------------------------
def use_plot_style():
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plt.rcParams.update({
"figure.dpi": 140,
"savefig.dpi": 140,
"font.size": 9,
"axes.grid": True,
"grid.alpha": 0.25,
"grid.linewidth": 0.6,
"axes.spines.top": False,
"axes.spines.right": False,
"legend.frameon": False,
"figure.autolayout": True,
})
return plt
# One colour per entity, assigned in a fixed order and never cycled, so a
# configuration keeps its colour in every figure of the report.
#
# These five are a subset of the documented categorical palette chosen to clear
# the *all-pairs* floors, not just the adjacent ones: the five series in these
# charts are lines that cross each other, so any pair can end up adjacent on
# screen. Verified with ``python -m src.palette_check`` --
# worst CVD dE 13.0 (floor 8.0), worst normal-vision dE 16.3 (hard floor 15.0).
# Slots 2 and 3 sit under 3:1 contrast on the light surface, so the relief rule
# applies and every figure using them carries direct labels or a value table.
SERIES_COLORS = ["#2a78d6", "#eda100", "#e87ba4", "#008300", "#4a3aa7"]
PALETTE = {
"c1_dense": SERIES_COLORS[0],
"c2_moe_4e1a": SERIES_COLORS[1],
"c3_moe_4e2a": SERIES_COLORS[2],
"c4_moe_shared": SERIES_COLORS[3],
"c5_moe_4e2a_wide": SERIES_COLORS[4],
"adamw": SERIES_COLORS[0],
"mars": SERIES_COLORS[1],
"lion": SERIES_COLORS[2],
"muon": SERIES_COLORS[3],
"sophia": SERIES_COLORS[4],
}
# Short, readable names for axes and legends.
LABELS = {
"c1_dense": "C1 dense MLP",
"c2_moe_4e1a": "C2 MoE 4e/top-1",
"c3_moe_4e2a": "C3 MoE 4e/top-2",
"c4_moe_shared": "C4 1 shared + 3e/top-1",
"c5_moe_4e2a_wide": "C5 MoE 4e/top-2 wide",
"adamw": "AdamW",
"mars": "MARS",
"lion": "Lion",
"muon": "Muon",
"sophia": "Sophia-G",
}
# Sequential ramp (one hue, light -> dark) for plain magnitude heatmaps.
SEQUENTIAL = ["#eef4fc", "#cfe0f7", "#a8c8f0", "#7fade6", "#5590d9",
"#2a78d6", "#1f5da8", "#154079"]
# Diverging ramp for the routing heatmaps. Expert share has a meaningful
# midpoint -- 1/E, the perfectly balanced share -- and the question being asked
# is which side of it a cell falls on, so this is a polarity encoding, not a
# magnitude one. Blue <-> red with a neutral grey centre, equal steps per arm.
DIVERGING = ["#154079", "#1f5da8", "#2a78d6", "#7fade6", "#cfe0f7",
"#f0efec",
"#f7d5cf", "#eda28f", "#e0664f", "#c33a2b", "#8f2418"]
|