jvonrad commited on
Commit
80c9326
·
verified ·
1 Parent(s): f4fc57b

Upload src/xscript/data/tokcorpus.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/xscript/data/tokcorpus.py +220 -0
src/xscript/data/tokcorpus.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tokenizer-training corpora from raw (unfiltered) FineWeb / FineWeb2.
2
+
3
+ Both tokenizer conditions are sampled from the SAME corpus family as the
4
+ model-training pools (data/fineweb.py's FineWeb-HQ / FineWeb2-HQ), just the
5
+ unfiltered releases -- HuggingFaceFW/fineweb (English) and
6
+ HuggingFaceFW/fineweb-2 (everyone else). This is a deliberate deviation from
7
+ ATLAS's literal MADLAD-400-trained tokenizer: it removes a tokenizer-corpus-
8
+ vs-model-corpus domain-mismatch confound that could otherwise hit AR/ZH
9
+ differently than DE/FR (MADLAD's non-Latin-script cleaning/LangID is less
10
+ consistent than FineWeb's), at the cost of no longer being a byte-for-byte
11
+ ATLAS replication for the starved tokenizer's source text.
12
+
13
+ starved -- ATLAS-style: T=100 temperature sampling (p_l ~ n_l^(1/100),
14
+ i.e. near-uniform) over ~419 languages -- English (raw FineWeb)
15
+ plus the ~418 largest FineWeb2 language-script configs by
16
+ volume. "Largest by volume" is the FineWeb2 analogue of how
17
+ MADLAD-400's own ~419-language "clean" set was itself
18
+ determined (languages with enough clean text to clear a
19
+ volume floor), so it preserves the same selection logic, just
20
+ applied to a different corpus.
21
+ destarved -- our 5 study languages only; per-language byte budgets scaled
22
+ by the FLORES+ byte premium so *content* (not bytes) is
23
+ uniform across languages.
24
+
25
+ Both stream parquet with column pruning (reusing data.fineweb's `_iter_texts`),
26
+ so only the `text` column is ever pulled.
27
+ """
28
+ import json
29
+ import random
30
+ import urllib.request
31
+ from pathlib import Path
32
+
33
+ from ..langs import LANGS
34
+ from ..paths import MANIFEST_CACHE, TOK_CORPORA, ensure
35
+ from .fineweb import _iter_texts
36
+
37
+ FINEWEB_EN_REPO = "HuggingFaceFW/fineweb"
38
+ FINEWEB2_REPO = "HuggingFaceFW/fineweb-2"
39
+ N_STARVED_LANGS = 419 # matches MADLAD-400/ATLAS's ~419-language scale
40
+
41
+ # SentencePiece skips sentences longer than max_sentence_length (default 4192
42
+ # bytes); we pre-split long documents so no text is silently dropped.
43
+ MAX_LINE_BYTES = 4000
44
+
45
+
46
+ def _get_json(url: str) -> dict:
47
+ with urllib.request.urlopen(url, timeout=120) as r:
48
+ return json.loads(r.read().decode("utf-8"))
49
+
50
+
51
+ def _fineweb2_size_manifest(refresh: bool = False) -> dict[str, int]:
52
+ """{config_name: num_bytes_original_files} for every FineWeb2 language-script config."""
53
+ cache = ensure(MANIFEST_CACHE) / "fineweb2_sizes.json"
54
+ if cache.exists() and not refresh:
55
+ return json.loads(cache.read_text())
56
+ d = _get_json(f"https://datasets-server.huggingface.co/size?dataset={FINEWEB2_REPO}")
57
+ sizes = {c["config"]: c["num_bytes_original_files"] for c in d["size"]["configs"]}
58
+ cache.write_text(json.dumps(sizes))
59
+ print(f"[tokcorpus] fetched FineWeb2 sizes for {len(sizes)} configs")
60
+ return sizes
61
+
62
+
63
+ def _fineweb_en_size(refresh: bool = False) -> int:
64
+ cache = ensure(MANIFEST_CACHE) / "fineweb_en_size.json"
65
+ if cache.exists() and not refresh:
66
+ return json.loads(cache.read_text())["bytes"]
67
+ d = _get_json(f"https://datasets-server.huggingface.co/size?dataset={FINEWEB_EN_REPO}&config=default")
68
+ n = d["size"]["config"]["num_bytes_original_files"]
69
+ cache.write_text(json.dumps({"bytes": n}))
70
+ return n
71
+
72
+
73
+ def select_starved_languages(n_langs: int = N_STARVED_LANGS) -> dict[str, int]:
74
+ """{code: available_bytes} for the starved condition's language universe.
75
+
76
+ "en" (raw FineWeb) plus the (n_langs-1) largest FineWeb2 configs by volume.
77
+ Our other 4 study languages (de/fr/ar/zh) are always near the top of that
78
+ ranking on volume alone (verified: ranks 2-13 of 1314), so no forced
79
+ inclusion is needed.
80
+ """
81
+ sizes = _fineweb2_size_manifest()
82
+ top = sorted(sizes.items(), key=lambda kv: -kv[1])[: n_langs - 1]
83
+ universe = {"en": _fineweb_en_size(), **dict(top)}
84
+ return universe
85
+
86
+
87
+ def _source_for_code(code: str) -> tuple[str, str]:
88
+ """(repo, subdir) of raw FineWeb-family text for a language code.
89
+
90
+ `code` is either "en", one of our other 4 study codes, or a raw FineWeb2
91
+ language-script config name (a starved-only competing language).
92
+ """
93
+ if code == "en":
94
+ return FINEWEB_EN_REPO, "data"
95
+ if code in LANGS:
96
+ code = LANGS[code].fineweb_subdir # study code -> FineWeb2 config name
97
+ return FINEWEB2_REPO, f"data/{code}/train"
98
+
99
+
100
+ def _list_parquet_files(repo: str, subdir: str) -> list[str]:
101
+ cache = ensure(MANIFEST_CACHE / "parquet_files") / f"{repo.replace('/', '__')}__{subdir.replace('/', '_')}.json"
102
+ if cache.exists():
103
+ return json.loads(cache.read_text())
104
+ from huggingface_hub import HfApi
105
+ api = HfApi()
106
+ files = [e.path for e in api.list_repo_tree(repo, subdir, repo_type="dataset", recursive=True)
107
+ if e.__class__.__name__ == "RepoFile" and e.path.endswith(".parquet")]
108
+ # dump-stratified sources (raw FineWeb's CC-MAIN-*/) interleave across dumps
109
+ # for a temporally representative sample; FineWeb2's per-language train/
110
+ # files have no such structure, so plain sort is enough.
111
+ by_dump: dict[str, list[str]] = {}
112
+ for f in sorted(files):
113
+ parts = f[len(subdir):].strip("/").split("/")
114
+ key = parts[0] if len(parts) > 1 and "CC-MAIN" in parts[0] else ""
115
+ by_dump.setdefault(key, []).append(f)
116
+ if len(by_dump) > 1:
117
+ out = []
118
+ for i in range(max(len(v) for v in by_dump.values())):
119
+ for d in sorted(by_dump):
120
+ if i < len(by_dump[d]):
121
+ out.append(by_dump[d][i])
122
+ files = out
123
+ else:
124
+ files = sorted(files)
125
+ cache.write_text(json.dumps(files))
126
+ return files
127
+
128
+
129
+ def _split_line(text: str):
130
+ """Yield non-empty lines, splitting any line over MAX_LINE_BYTES at whitespace."""
131
+ for ln in text.split("\n"):
132
+ ln = ln.strip()
133
+ if not ln:
134
+ continue
135
+ enc = ln.encode("utf-8")
136
+ while len(enc) > MAX_LINE_BYTES:
137
+ cut = enc[:MAX_LINE_BYTES].rfind(b" ")
138
+ if cut < MAX_LINE_BYTES // 2:
139
+ cut = MAX_LINE_BYTES
140
+ while cut > 0 and (enc[cut] & 0xC0) == 0x80: # don't split mid UTF-8 char
141
+ cut -= 1
142
+ yield enc[:cut].decode("utf-8", errors="replace").strip()
143
+ enc = enc[cut:].lstrip()
144
+ if enc:
145
+ yield enc.decode("utf-8", errors="replace")
146
+
147
+
148
+ def _collect(code: str, budget_bytes: int, out_path: Path, seed: int = 0) -> int:
149
+ """Stream parquet files (shuffled order) for one language until budget is met."""
150
+ repo, subdir = _source_for_code(code)
151
+ files = list(_list_parquet_files(repo, subdir))
152
+ random.Random(seed).shuffle(files)
153
+ got = 0
154
+ with open(out_path, "w", encoding="utf-8") as out:
155
+ for f in files:
156
+ if got >= budget_bytes:
157
+ break
158
+ try:
159
+ for doc in _iter_texts(repo, f):
160
+ for ln in _split_line(doc):
161
+ out.write(ln + "\n")
162
+ got += len(ln.encode("utf-8")) + 1
163
+ if got >= budget_bytes:
164
+ break
165
+ except Exception as exc:
166
+ print(f"[tokcorpus] WARN {repo}/{f} failed mid-stream: {exc}")
167
+ return got
168
+
169
+
170
+ def build_starved(total_bytes: float = 4e9, T: float = 100.0, seed: int = 0,
171
+ n_langs: int = N_STARVED_LANGS) -> Path:
172
+ """T-temperature sample over ~419 FineWeb/FineWeb2 languages (ATLAS-scale replication)."""
173
+ universe = select_starved_languages(n_langs)
174
+ out_dir = ensure(TOK_CORPORA / "starved")
175
+ weights = {l: b ** (1.0 / T) for l, b in universe.items()}
176
+ z = sum(weights.values())
177
+ stats = {}
178
+ for i, code in enumerate(sorted(universe)):
179
+ budget = int(total_bytes * weights[code] / z)
180
+ out_path = out_dir / f"{code}.txt"
181
+ if out_path.exists() and out_path.stat().st_size >= 0.9 * budget:
182
+ stats[code] = {"budget": budget, "bytes": out_path.stat().st_size, "cached": True}
183
+ continue
184
+ got = _collect(code, budget, out_path, seed=seed + i)
185
+ stats[code] = {"budget": budget, "bytes": got}
186
+ print(f"[starved] {code}: {got/1e6:.1f}MB / budget {budget/1e6:.1f}MB "
187
+ f"({i+1}/{len(universe)})")
188
+ (out_dir / "stats.json").write_text(json.dumps(
189
+ {"total_bytes": total_bytes, "T": T, "n_langs": len(universe), "per_lang": stats},
190
+ indent=2))
191
+ return out_dir
192
+
193
+
194
+ def build_destarved(total_bytes: float = 4e9, seed: int = 0) -> Path:
195
+ """5 study languages; byte budgets scaled by FLORES+ byte premium (content-uniform)."""
196
+ from ..byte_premium import load_premiums
197
+ premiums = load_premiums()
198
+ out_dir = ensure(TOK_CORPORA / "destarved")
199
+ z = sum(premiums[l] for l in LANGS)
200
+ stats = {}
201
+ for i, code in enumerate(LANGS):
202
+ budget = int(total_bytes * premiums[code] / z)
203
+ out_path = out_dir / f"{code}.txt"
204
+ if out_path.exists() and out_path.stat().st_size >= 0.9 * budget:
205
+ stats[code] = {"budget": budget, "bytes": out_path.stat().st_size, "cached": True}
206
+ continue
207
+ got = _collect(code, budget, out_path, seed=seed + i)
208
+ stats[code] = {"budget": budget, "bytes": got}
209
+ print(f"[destarved] {code}: {got/1e6:.1f}MB / budget {budget/1e6:.1f}MB")
210
+ (out_dir / "stats.json").write_text(json.dumps(
211
+ {"total_bytes": total_bytes, "premiums": premiums, "per_lang": stats}, indent=2))
212
+ return out_dir
213
+
214
+
215
+ def corpus_files(condition: str) -> list[Path]:
216
+ d = TOK_CORPORA / condition
217
+ files = sorted(d.glob("*.txt"))
218
+ if not files:
219
+ raise FileNotFoundError(f"no corpus at {d} - run `xscript tok-corpus --condition {condition}`")
220
+ return files