Download src/xscript/data/tokcorpus.py from jvonrad/xscript-eval: direct link, hf CLI and curl.
- Browser
- Download file 9.76 kB
-
https://huggingface.co/jvonrad/xscript-eval/resolve/8e89aee4fa6aa1deca4140af2ec08ff10cc07db3/src/xscript/data/tokcorpus.py
- Command line
-
hf download hf://jvonrad/xscript-eval@8e89aee4fa6aa1deca4140af2ec08ff10cc07db3/src/xscript/data/tokcorpus.py
-
curl -L -o tokcorpus.py https://huggingface.co/jvonrad/xscript-eval/resolve/8e89aee4fa6aa1deca4140af2ec08ff10cc07db3/src/xscript/data/tokcorpus.py
9.76 kB
| """Tokenizer-training corpora from raw (unfiltered) FineWeb / FineWeb2. | |
| Both tokenizer conditions are sampled from the SAME corpus family as the | |
| model-training pools (data/fineweb.py's FineWeb-HQ / FineWeb2-HQ), just the | |
| unfiltered releases -- HuggingFaceFW/fineweb (English) and | |
| HuggingFaceFW/fineweb-2 (everyone else). This is a deliberate deviation from | |
| ATLAS's literal MADLAD-400-trained tokenizer: it removes a tokenizer-corpus- | |
| vs-model-corpus domain-mismatch confound that could otherwise hit AR/ZH | |
| differently than DE/FR (MADLAD's non-Latin-script cleaning/LangID is less | |
| consistent than FineWeb's), at the cost of no longer being a byte-for-byte | |
| ATLAS replication for the starved tokenizer's source text. | |
| starved -- ATLAS-style: T=100 temperature sampling (p_l ~ n_l^(1/100), | |
| i.e. near-uniform) over ~419 languages -- English (raw FineWeb) | |
| plus the ~418 largest FineWeb2 language-script configs by | |
| volume. "Largest by volume" is the FineWeb2 analogue of how | |
| MADLAD-400's own ~419-language "clean" set was itself | |
| determined (languages with enough clean text to clear a | |
| volume floor), so it preserves the same selection logic, just | |
| applied to a different corpus. | |
| destarved -- our 5 study languages only; per-language byte budgets scaled | |
| by the FLORES+ byte premium so *content* (not bytes) is | |
| uniform across languages. | |
| Both stream parquet with column pruning (reusing data.fineweb's `_iter_texts`), | |
| so only the `text` column is ever pulled. | |
| """ | |
| import json | |
| import random | |
| import urllib.request | |
| from pathlib import Path | |
| from ..langs import LANGS | |
| from ..paths import MANIFEST_CACHE, TOK_CORPORA, ensure | |
| from .fineweb import _iter_texts | |
| FINEWEB_EN_REPO = "HuggingFaceFW/fineweb" | |
| FINEWEB2_REPO = "HuggingFaceFW/fineweb-2" | |
| N_STARVED_LANGS = 419 # matches MADLAD-400/ATLAS's ~419-language scale | |
| # SentencePiece skips sentences longer than max_sentence_length (default 4192 | |
| # bytes); we pre-split long documents so no text is silently dropped. | |
| MAX_LINE_BYTES = 4000 | |
| def _get_json(url: str) -> dict: | |
| with urllib.request.urlopen(url, timeout=120) as r: | |
| return json.loads(r.read().decode("utf-8")) | |
| def _fineweb2_size_manifest(refresh: bool = False) -> dict[str, int]: | |
| """{config_name: num_bytes_original_files} for every FineWeb2 language-script config.""" | |
| cache = ensure(MANIFEST_CACHE) / "fineweb2_sizes.json" | |
| if cache.exists() and not refresh: | |
| return json.loads(cache.read_text()) | |
| d = _get_json(f"https://datasets-server.huggingface.co/size?dataset={FINEWEB2_REPO}") | |
| sizes = {c["config"]: c["num_bytes_original_files"] for c in d["size"]["configs"]} | |
| cache.write_text(json.dumps(sizes)) | |
| print(f"[tokcorpus] fetched FineWeb2 sizes for {len(sizes)} configs") | |
| return sizes | |
| def _fineweb_en_size(refresh: bool = False) -> int: | |
| cache = ensure(MANIFEST_CACHE) / "fineweb_en_size.json" | |
| if cache.exists() and not refresh: | |
| return json.loads(cache.read_text())["bytes"] | |
| d = _get_json(f"https://datasets-server.huggingface.co/size?dataset={FINEWEB_EN_REPO}&config=default") | |
| n = d["size"]["config"]["num_bytes_original_files"] | |
| cache.write_text(json.dumps({"bytes": n})) | |
| return n | |
| def select_starved_languages(n_langs: int = N_STARVED_LANGS) -> dict[str, int]: | |
| """{code: available_bytes} for the starved condition's language universe. | |
| "en" (raw FineWeb) plus the (n_langs-1) largest FineWeb2 configs by volume. | |
| Our other 4 study languages (de/fr/ar/zh) are always near the top of that | |
| ranking on volume alone (verified: ranks 2-13 of 1314), so no forced | |
| inclusion is needed. | |
| """ | |
| sizes = _fineweb2_size_manifest() | |
| top = sorted(sizes.items(), key=lambda kv: -kv[1])[: n_langs - 1] | |
| universe = {"en": _fineweb_en_size(), **dict(top)} | |
| return universe | |
| def _source_for_code(code: str) -> tuple[str, str]: | |
| """(repo, subdir) of raw FineWeb-family text for a language code. | |
| `code` is either "en", one of our other 4 study codes, or a raw FineWeb2 | |
| language-script config name (a starved-only competing language). | |
| """ | |
| if code == "en": | |
| return FINEWEB_EN_REPO, "data" | |
| if code in LANGS: | |
| code = LANGS[code].fineweb_subdir # study code -> FineWeb2 config name | |
| return FINEWEB2_REPO, f"data/{code}/train" | |
| def _list_parquet_files(repo: str, subdir: str) -> list[str]: | |
| cache = ensure(MANIFEST_CACHE / "parquet_files") / f"{repo.replace('/', '__')}__{subdir.replace('/', '_')}.json" | |
| if cache.exists(): | |
| return json.loads(cache.read_text()) | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| files = [e.path for e in api.list_repo_tree(repo, subdir, repo_type="dataset", recursive=True) | |
| if e.__class__.__name__ == "RepoFile" and e.path.endswith(".parquet")] | |
| # dump-stratified sources (raw FineWeb's CC-MAIN-*/) interleave across dumps | |
| # for a temporally representative sample; FineWeb2's per-language train/ | |
| # files have no such structure, so plain sort is enough. | |
| by_dump: dict[str, list[str]] = {} | |
| for f in sorted(files): | |
| parts = f[len(subdir):].strip("/").split("/") | |
| key = parts[0] if len(parts) > 1 and "CC-MAIN" in parts[0] else "" | |
| by_dump.setdefault(key, []).append(f) | |
| if len(by_dump) > 1: | |
| out = [] | |
| for i in range(max(len(v) for v in by_dump.values())): | |
| for d in sorted(by_dump): | |
| if i < len(by_dump[d]): | |
| out.append(by_dump[d][i]) | |
| files = out | |
| else: | |
| files = sorted(files) | |
| cache.write_text(json.dumps(files)) | |
| return files | |
| def _split_line(text: str): | |
| """Yield non-empty lines, splitting any line over MAX_LINE_BYTES at whitespace.""" | |
| for ln in text.split("\n"): | |
| ln = ln.strip() | |
| if not ln: | |
| continue | |
| enc = ln.encode("utf-8") | |
| while len(enc) > MAX_LINE_BYTES: | |
| cut = enc[:MAX_LINE_BYTES].rfind(b" ") | |
| if cut < MAX_LINE_BYTES // 2: | |
| cut = MAX_LINE_BYTES | |
| while cut > 0 and (enc[cut] & 0xC0) == 0x80: # don't split mid UTF-8 char | |
| cut -= 1 | |
| yield enc[:cut].decode("utf-8", errors="replace").strip() | |
| enc = enc[cut:].lstrip() | |
| if enc: | |
| yield enc.decode("utf-8", errors="replace") | |
| def _collect(code: str, budget_bytes: int, out_path: Path, seed: int = 0) -> int: | |
| """Stream parquet files (shuffled order) for one language until budget is met.""" | |
| repo, subdir = _source_for_code(code) | |
| files = list(_list_parquet_files(repo, subdir)) | |
| random.Random(seed).shuffle(files) | |
| got = 0 | |
| with open(out_path, "w", encoding="utf-8") as out: | |
| for f in files: | |
| if got >= budget_bytes: | |
| break | |
| try: | |
| for doc in _iter_texts(repo, f): | |
| for ln in _split_line(doc): | |
| out.write(ln + "\n") | |
| got += len(ln.encode("utf-8")) + 1 | |
| if got >= budget_bytes: | |
| break | |
| except Exception as exc: | |
| print(f"[tokcorpus] WARN {repo}/{f} failed mid-stream: {exc}") | |
| return got | |
| def build_starved(total_bytes: float = 4e9, T: float = 100.0, seed: int = 0, | |
| n_langs: int = N_STARVED_LANGS) -> Path: | |
| """T-temperature sample over ~419 FineWeb/FineWeb2 languages (ATLAS-scale replication).""" | |
| universe = select_starved_languages(n_langs) | |
| out_dir = ensure(TOK_CORPORA / "starved") | |
| weights = {l: b ** (1.0 / T) for l, b in universe.items()} | |
| z = sum(weights.values()) | |
| stats = {} | |
| for i, code in enumerate(sorted(universe)): | |
| budget = int(total_bytes * weights[code] / z) | |
| out_path = out_dir / f"{code}.txt" | |
| if out_path.exists() and out_path.stat().st_size >= 0.9 * budget: | |
| stats[code] = {"budget": budget, "bytes": out_path.stat().st_size, "cached": True} | |
| continue | |
| got = _collect(code, budget, out_path, seed=seed + i) | |
| stats[code] = {"budget": budget, "bytes": got} | |
| print(f"[starved] {code}: {got/1e6:.1f}MB / budget {budget/1e6:.1f}MB " | |
| f"({i+1}/{len(universe)})") | |
| (out_dir / "stats.json").write_text(json.dumps( | |
| {"total_bytes": total_bytes, "T": T, "n_langs": len(universe), "per_lang": stats}, | |
| indent=2)) | |
| return out_dir | |
| def build_destarved(total_bytes: float = 4e9, seed: int = 0) -> Path: | |
| """5 study languages; byte budgets scaled by FLORES+ byte premium (content-uniform).""" | |
| from ..byte_premium import load_premiums | |
| premiums = load_premiums() | |
| out_dir = ensure(TOK_CORPORA / "destarved") | |
| z = sum(premiums[l] for l in LANGS) | |
| stats = {} | |
| for i, code in enumerate(LANGS): | |
| budget = int(total_bytes * premiums[code] / z) | |
| out_path = out_dir / f"{code}.txt" | |
| if out_path.exists() and out_path.stat().st_size >= 0.9 * budget: | |
| stats[code] = {"budget": budget, "bytes": out_path.stat().st_size, "cached": True} | |
| continue | |
| got = _collect(code, budget, out_path, seed=seed + i) | |
| stats[code] = {"budget": budget, "bytes": got} | |
| print(f"[destarved] {code}: {got/1e6:.1f}MB / budget {budget/1e6:.1f}MB") | |
| (out_dir / "stats.json").write_text(json.dumps( | |
| {"total_bytes": total_bytes, "premiums": premiums, "per_lang": stats}, indent=2)) | |
| return out_dir | |
| def corpus_files(condition: str) -> list[Path]: | |
| d = TOK_CORPORA / condition | |
| files = sorted(d.glob("*.txt")) | |
| if not files: | |
| raise FileNotFoundError(f"no corpus at {d} - run `xscript tok-corpus --condition {condition}`") | |
| return files | |