Upload src/xscript/runmatrix.py with huggingface_hub
Browse files- src/xscript/runmatrix.py +131 -0
src/xscript/runmatrix.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate per-run training configs from a base yaml.
|
| 2 |
+
|
| 3 |
+
The design covers 9 mixtures (5 monolingual + 4 EN-anchored bilingual
|
| 4 |
+
mixtures) x 2 tokenizer conditions (starved / destarved) for one chosen BPE
|
| 5 |
+
flavor = 18 runs at 30B tokens each. Four of them (EN-DE and EN-AR under both
|
| 6 |
+
conditions) are additionally extended to 100B via a WSD cooldown branch off a
|
| 7 |
+
long stable trunk -- the cheap-extension mechanism.
|
| 8 |
+
|
| 9 |
+
A GPU-hour budget shortfall (2026-07-16: ~1200h available vs ~2615h needed
|
| 10 |
+
for the full 30-run all-pairs matrix) forced cutting the 12 non-EN-anchored
|
| 11 |
+
pairwise mixtures (de-fr, de-ar, de-zh, fr-ar, fr-zh, ar-zh) that a prior
|
| 12 |
+
expansion had added here; reverted to the original EN-anchored-only design.
|
| 13 |
+
Their partial checkpoints (0-12.4B tokens) are left on disk, unused.
|
| 14 |
+
|
| 15 |
+
A run's config is this base block plus: name, langs, probs, tok_name, schedule
|
| 16 |
+
overrides, and (for cooldowns) a `branch` pointing at a trunk checkpoint.
|
| 17 |
+
|
| 18 |
+
Run-name convention (kept in sync with eval.bts):
|
| 19 |
+
"<mix>__<tok>" e.g. en-ar__bl_destarved (30B deliverable)
|
| 20 |
+
"<mix>__<tok>__trunk" long stable trunk for extension
|
| 21 |
+
"<mix>__<tok>__100b" 100B cooldown deliverable
|
| 22 |
+
where <mix> is "en" or "en-ar" etc., <tok> is "<flavor>_<condition>".
|
| 23 |
+
"""
|
| 24 |
+
import copy
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
from .langs import LANGS, ANCHOR, PARTNERS, tok_conditions, tok_name
|
| 28 |
+
from .paths import run_dir
|
| 29 |
+
|
| 30 |
+
BILINGUAL_MIXTURES = [f"{ANCHOR}-{p}" for p in PARTNERS]
|
| 31 |
+
MIXTURES = [*LANGS, *BILINGUAL_MIXTURES]
|
| 32 |
+
# Cells extended to 100B via cooldown branch (plan's recommendation): one
|
| 33 |
+
# same-script (EN-DE) and one cross-script (EN-AR) partner, both tokenizer
|
| 34 |
+
# conditions = 4 extended runs. The same-vs-cross contrast at 100B is the point.
|
| 35 |
+
EXTENDED_MIXTURES = [f"{ANCHOR}-de", f"{ANCHOR}-ar"]
|
| 36 |
+
TRUNK_TOKENS = 80e9
|
| 37 |
+
# cooldown budgets: {final_total_tokens: (branch_at_trunk_tokens, decay_tokens)}
|
| 38 |
+
COOLDOWNS = {30e9: (24e9, 6e9), 100e9: (80e9, 20e9)}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def load_base(path) -> dict:
|
| 42 |
+
from . import _yaml
|
| 43 |
+
return _yaml.load(path)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def mix_langs(mix: str) -> list[str]:
|
| 47 |
+
return mix.split("-")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _stamp(base: dict, name: str, mix: str, tok: str) -> dict:
|
| 51 |
+
cfg = copy.deepcopy(base)
|
| 52 |
+
cfg["name"] = name
|
| 53 |
+
cfg["langs"] = mix_langs(mix)
|
| 54 |
+
cfg["tok_name"] = tok
|
| 55 |
+
cfg.setdefault("probs", None) # None -> uniform (token-level 50/50 bilingual)
|
| 56 |
+
return cfg
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def wsd_run(base, mix, tok) -> dict:
|
| 60 |
+
"""Self-contained warmup+stable+decay to 30B (base schedule as-is)."""
|
| 61 |
+
return _stamp(base, f"{mix}__{tok}", mix, tok)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def trunk_run(base, mix, tok) -> dict:
|
| 65 |
+
cfg = _stamp(base, f"{mix}__{tok}__trunk", mix, tok)
|
| 66 |
+
warm = cfg["schedule"]["warmup_tokens"]
|
| 67 |
+
cfg["schedule"] = {**cfg["schedule"],
|
| 68 |
+
"stable_tokens": TRUNK_TOKENS - warm, "decay_tokens": 0}
|
| 69 |
+
cfg["train"] = {**cfg["train"],
|
| 70 |
+
"stable_marks": sorted({b for b, _ in COOLDOWNS.values()})}
|
| 71 |
+
return cfg
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def cooldown_run(base, mix, tok, total_tokens) -> dict:
|
| 75 |
+
branch_at, decay = COOLDOWNS[total_tokens]
|
| 76 |
+
suffix = "" if total_tokens == 30e9 else f"__{int(total_tokens/1e9)}b"
|
| 77 |
+
cfg = _stamp(base, f"{mix}__{tok}{suffix}", mix, tok)
|
| 78 |
+
cfg["schedule"] = {**cfg["schedule"], "warmup_tokens": 0,
|
| 79 |
+
"stable_tokens": 0, "decay_tokens": decay}
|
| 80 |
+
trunk_ckpt = (run_dir(f"{mix}__{tok}__trunk") / "checkpoints"
|
| 81 |
+
/ f"stable_{int(branch_at/1e6)}M.pt")
|
| 82 |
+
cfg["branch"] = {"from": str(trunk_ckpt), "load_optim": True}
|
| 83 |
+
return cfg
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def all_runs(base: dict, flavor: str) -> dict[str, dict]:
|
| 87 |
+
"""Every run needed for the full experiment, keyed by run name."""
|
| 88 |
+
runs: dict[str, dict] = {}
|
| 89 |
+
for mix in MIXTURES:
|
| 90 |
+
for cond in tok_conditions(flavor):
|
| 91 |
+
tok = tok_name(flavor, cond)
|
| 92 |
+
if mix in EXTENDED_MIXTURES:
|
| 93 |
+
runs[f"{mix}__{tok}__trunk"] = trunk_run(base, mix, tok)
|
| 94 |
+
for total in COOLDOWNS:
|
| 95 |
+
r = cooldown_run(base, mix, tok, total)
|
| 96 |
+
runs[r["name"]] = r
|
| 97 |
+
else:
|
| 98 |
+
r = wsd_run(base, mix, tok)
|
| 99 |
+
runs[r["name"]] = r
|
| 100 |
+
return runs
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def all_30b_runs(base: dict, flavor: str) -> dict[str, dict]:
|
| 104 |
+
"""The 30 independent 30B cells, without 100B-extension trunks.
|
| 105 |
+
|
| 106 |
+
This mode is useful when the immediate experiment scope is only 30B. In
|
| 107 |
+
the full matrix, four of these names are cooldown branches from shared
|
| 108 |
+
trunks; here every name instead gets the base self-contained WSD schedule.
|
| 109 |
+
"""
|
| 110 |
+
runs: dict[str, dict] = {}
|
| 111 |
+
for mix in MIXTURES:
|
| 112 |
+
for cond in tok_conditions(flavor):
|
| 113 |
+
tok = tok_name(flavor, cond)
|
| 114 |
+
run = wsd_run(base, mix, tok)
|
| 115 |
+
runs[run["name"]] = run
|
| 116 |
+
return runs
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def get_run(base_path, flavor: str, name: str, only_30b: bool = False) -> dict:
|
| 120 |
+
base = load_base(base_path)
|
| 121 |
+
runs = all_30b_runs(base, flavor) if only_30b else all_runs(base, flavor)
|
| 122 |
+
if name not in runs:
|
| 123 |
+
raise KeyError(f"unknown run '{name}'. Available:\n " +
|
| 124 |
+
"\n ".join(sorted(runs)))
|
| 125 |
+
return runs[name]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def list_runs(base_path, flavor: str, only_30b: bool = False) -> list[str]:
|
| 129 |
+
base = load_base(base_path)
|
| 130 |
+
runs = all_30b_runs(base, flavor) if only_30b else all_runs(base, flavor)
|
| 131 |
+
return sorted(runs)
|