BHARGAV REDDY commited on
Commit
fa6e9f8
·
verified ·
1 Parent(s): 20bb6fd

Upload Base/scripts/build_english_curriculum_1b.py with huggingface_hub

Browse files
Base/scripts/build_english_curriculum_1b.py ADDED
@@ -0,0 +1,608 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Build a new 1B-token English curriculum corpus for continued pretraining.
3
+
4
+ Goal:
5
+ Improve English understanding, grammar, vocabulary, and meaningful prose
6
+ without just reusing the existing litdata_pretrain_final mix.
7
+
8
+ Curriculum mix:
9
+ 1. Simple Wikipedia -> simpler explanatory English
10
+ 2. Wikipedia tail -> formal, fact-dense English not used earlier
11
+ 3. FineWeb-Edu tail -> educational/tutorial English not used earlier
12
+ 4. PG-19 books -> long-form prose, dialogue, vocabulary richness
13
+
14
+ Output:
15
+ Parquet files with one 'text' column in:
16
+ Base/data/filtered_english_curriculum_1b/
17
+
18
+ Then convert to LitData with:
19
+ python Base/scripts/prepare_litdata.py \
20
+ --filtered_dir Base/data/filtered_english_curriculum_1b \
21
+ --output_dir Base/data/litdata_english_curriculum_1b \
22
+ --label ENGLISH_CURRICULUM_1B
23
+ """
24
+
25
+ import argparse
26
+ import hashlib
27
+ import json
28
+ import re
29
+ import time
30
+ import unicodedata
31
+ from collections import Counter
32
+ from pathlib import Path
33
+
34
+ import pyarrow as pa
35
+ import pyarrow.parquet as pq
36
+
37
+
38
+ TOKENS_PER_WORD = 1.3
39
+ DEFAULT_TARGET_TOKENS = 1_000_000_000
40
+ DEFAULT_SHARES = {
41
+ "simplewiki": 0.15,
42
+ "wiki_tail": 0.20,
43
+ "fineweb_tail": 0.25,
44
+ "pg19": 0.40,
45
+ }
46
+ DEFAULT_WIKI_SKIP = 250_000
47
+ DEFAULT_FINEWEB_SKIP = 250_000
48
+ DEFAULT_FINEWEB_MIN_SCORE = 4.0
49
+ FLUSH_DOCS = 5_000
50
+
51
+ WIKI_SKIP_PATTERNS = re.compile(
52
+ r"(disambiguation|list of|lists of|index of|outline of|"
53
+ r"wikipedia:|template:|category:|portal:|module:|mediawiki:)",
54
+ re.IGNORECASE,
55
+ )
56
+
57
+ CONTROL_CHARS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
58
+ RE_URL = re.compile(r"https?://\S+|www\.\S+", re.I)
59
+ RE_HTML_TAG = re.compile(r"</?[a-zA-Z][a-zA-Z0-9]*(?:\s[^>]*)?\s*/?>")
60
+ RE_HTML_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
61
+ RE_REPEATED_LINE = re.compile(r"^(.{20,})\n(?:\1\n?)+", re.M)
62
+ RE_MULTI_NEWLINE = re.compile(r"\n{4,}")
63
+ RE_MULTI_SPACE = re.compile(r"[ \t]{2,}")
64
+ RE_TRAILING_SPACE = re.compile(r"[ \t]+$", re.M)
65
+ RE_NO_SPACE_AFTER_PERIOD = re.compile(r"([.!?])([A-Z])")
66
+ RE_SPACE_BEFORE_PUNCT = re.compile(r"\s+([.,;:!?])")
67
+ RE_DOUBLE_PERIOD = re.compile(r"\.{2}(?!\.)")
68
+ RE_CJK = re.compile(r"[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]{3,}")
69
+ RE_ARABIC = re.compile(r"[\u0600-\u06ff]{5,}")
70
+ RE_CYRILLIC = re.compile(r"[\u0400-\u04ff]{5,}")
71
+ RE_DEVANAGARI = re.compile(r"[\u0900-\u097f]{5,}")
72
+
73
+ PG_START_MARKERS = [
74
+ "*** START OF THE PROJECT GUTENBERG EBOOK",
75
+ "*** START OF THIS PROJECT GUTENBERG EBOOK",
76
+ "START OF THE PROJECT GUTENBERG EBOOK",
77
+ ]
78
+ PG_END_MARKERS = [
79
+ "*** END OF THE PROJECT GUTENBERG EBOOK",
80
+ "*** END OF THIS PROJECT GUTENBERG EBOOK",
81
+ "END OF THE PROJECT GUTENBERG EBOOK",
82
+ ]
83
+
84
+
85
+ def clean_text_basic(text: str) -> str:
86
+ text = unicodedata.normalize("NFKC", text)
87
+ text = CONTROL_CHARS.sub("", text)
88
+ text = RE_HTML_COMMENT.sub("", text)
89
+ text = RE_HTML_TAG.sub("", text)
90
+ text = RE_URL.sub("", text)
91
+ text = RE_REPEATED_LINE.sub(r"\1", text)
92
+ text = RE_DOUBLE_PERIOD.sub(".", text)
93
+ text = RE_NO_SPACE_AFTER_PERIOD.sub(r"\1 \2", text)
94
+ text = RE_SPACE_BEFORE_PUNCT.sub(r"\1", text)
95
+ text = RE_MULTI_SPACE.sub(" ", text)
96
+ text = RE_MULTI_NEWLINE.sub("\n\n\n", text)
97
+ text = RE_TRAILING_SPACE.sub("", text)
98
+ text = text.replace("\u2018", "'").replace("\u2019", "'")
99
+ text = text.replace("\u201c", '"').replace("\u201d", '"')
100
+ text = text.replace("\u2013", "-").replace("\u2014", " - ")
101
+ text = text.replace("\u2026", "...")
102
+ text = text.replace("\u00a0", " ")
103
+ text = "\n".join(line.strip() for line in text.splitlines())
104
+ return text.strip()
105
+
106
+
107
+ def is_high_quality(text: str, min_chars: int, min_words: int, alpha_ratio: float = 0.68) -> bool:
108
+ if len(text) < min_chars:
109
+ return False
110
+
111
+ words = text.split()
112
+ word_count = len(words)
113
+ if word_count < min_words:
114
+ return False
115
+
116
+ alpha = sum(c.isalpha() for c in text)
117
+ if alpha / max(len(text), 1) < alpha_ratio:
118
+ return False
119
+
120
+ avg_word_len = sum(len(word) for word in words) / max(word_count, 1)
121
+ if avg_word_len < 2.5 or avg_word_len > 15:
122
+ return False
123
+
124
+ url_hits = len(RE_URL.findall(text))
125
+ if url_hits > word_count * 0.02:
126
+ return False
127
+
128
+ if RE_CJK.search(text) or RE_ARABIC.search(text) or RE_CYRILLIC.search(text) or RE_DEVANAGARI.search(text):
129
+ return False
130
+
131
+ sentences = [segment.strip() for segment in re.split(r"[.!?]+", text) if len(segment.strip()) > 12]
132
+ if len(sentences) < 3:
133
+ return False
134
+
135
+ sentence_lengths = [len(sentence.split()) for sentence in sentences]
136
+ avg_sentence_words = sum(sentence_lengths) / max(len(sentence_lengths), 1)
137
+ if avg_sentence_words < 5 or avg_sentence_words > 40:
138
+ return False
139
+
140
+ if word_count >= 120:
141
+ unique_word_ratio = len(set(word.lower() for word in words)) / word_count
142
+ if unique_word_ratio < 0.22:
143
+ return False
144
+
145
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
146
+ if len(lines) > 5:
147
+ unique_line_ratio = len(set(lines)) / len(lines)
148
+ if unique_line_ratio < 0.55:
149
+ return False
150
+
151
+ return True
152
+
153
+
154
+ def normalize_for_hash(text: str) -> str:
155
+ return " ".join(text.lower().split())[:8192]
156
+
157
+
158
+ def fingerprint(text: str) -> int:
159
+ digest = hashlib.blake2b(normalize_for_hash(text).encode("utf-8"), digest_size=8).digest()
160
+ return int.from_bytes(digest, byteorder="little", signed=False)
161
+
162
+
163
+ def approx_tokens(text: str) -> int:
164
+ return max(1, int(len(text.split()) * TOKENS_PER_WORD))
165
+
166
+
167
+ def strip_project_gutenberg(text: str) -> str:
168
+ cleaned = text
169
+ upper = cleaned.upper()
170
+
171
+ for marker in PG_START_MARKERS:
172
+ pos = upper.find(marker)
173
+ if pos != -1:
174
+ start = cleaned.find("\n", pos)
175
+ if start != -1:
176
+ cleaned = cleaned[start + 1:]
177
+ upper = cleaned.upper()
178
+ break
179
+
180
+ for marker in PG_END_MARKERS:
181
+ pos = upper.find(marker)
182
+ if pos != -1:
183
+ cleaned = cleaned[:pos]
184
+ break
185
+
186
+ return cleaned.strip()
187
+
188
+
189
+ def split_pg19_passages(title: str, text: str, min_words: int = 220, target_words: int = 1200, max_words: int = 1800):
190
+ paragraphs = []
191
+ for paragraph in re.split(r"\n\s*\n", text):
192
+ cleaned = clean_text_basic(paragraph)
193
+ if len(cleaned.split()) >= 35:
194
+ paragraphs.append(cleaned)
195
+
196
+ current = []
197
+ current_words = 0
198
+ passage_idx = 0
199
+
200
+ for paragraph in paragraphs:
201
+ paragraph_words = len(paragraph.split())
202
+ if current and current_words >= target_words and current_words + paragraph_words > max_words:
203
+ passage_idx += 1
204
+ body = "\n\n".join(current)
205
+ yield f"{title}\n\n{body}", passage_idx
206
+ current = [paragraph]
207
+ current_words = paragraph_words
208
+ continue
209
+
210
+ current.append(paragraph)
211
+ current_words += paragraph_words
212
+
213
+ if current_words >= max_words:
214
+ passage_idx += 1
215
+ body = "\n\n".join(current)
216
+ yield f"{title}\n\n{body}", passage_idx
217
+ current = []
218
+ current_words = 0
219
+
220
+ if current and current_words >= min_words:
221
+ passage_idx += 1
222
+ body = "\n\n".join(current)
223
+ yield f"{title}\n\n{body}", passage_idx
224
+
225
+
226
+ class ParquetSink:
227
+ def __init__(self, output_dir: Path):
228
+ self.output_dir = output_dir
229
+ self.output_dir.mkdir(parents=True, exist_ok=True)
230
+ self.buffers = {}
231
+ self.file_counts = Counter()
232
+
233
+ def add(self, source_name: str, text: str):
234
+ bucket = self.buffers.setdefault(source_name, [])
235
+ bucket.append(text)
236
+ if len(bucket) >= FLUSH_DOCS:
237
+ self.flush(source_name)
238
+
239
+ def flush(self, source_name: str):
240
+ bucket = self.buffers.get(source_name)
241
+ if not bucket:
242
+ return
243
+ file_idx = self.file_counts[source_name]
244
+ output_path = self.output_dir / f"{source_name}_{file_idx:04d}.parquet"
245
+ pq.write_table(pa.table({"text": bucket}), str(output_path))
246
+ self.file_counts[source_name] += 1
247
+ self.buffers[source_name] = []
248
+
249
+ def finalize(self):
250
+ for source_name in list(self.buffers):
251
+ self.flush(source_name)
252
+
253
+
254
+ def try_load_streaming_dataset(candidates, split="train"):
255
+ from datasets import load_dataset
256
+
257
+ errors = []
258
+ for repo_id, config_name in candidates:
259
+ try:
260
+ if config_name is None:
261
+ dataset = load_dataset(repo_id, split=split, streaming=True, trust_remote_code=False)
262
+ else:
263
+ dataset = load_dataset(repo_id, config_name, split=split, streaming=True, trust_remote_code=False)
264
+ resolved = f"{repo_id} [{config_name}]" if config_name else repo_id
265
+ print(f" Loaded source: {resolved}")
266
+ return dataset
267
+ except Exception as exc:
268
+ errors.append(f"{repo_id} [{config_name}]: {exc}")
269
+
270
+ raise RuntimeError("Unable to load streaming dataset. Tried:\n " + "\n ".join(errors))
271
+
272
+
273
+ def maybe_keep(text: str, seen_hashes: set, min_chars: int, min_words: int, alpha_ratio: float = 0.68):
274
+ cleaned = clean_text_basic(text)
275
+ if not is_high_quality(cleaned, min_chars=min_chars, min_words=min_words, alpha_ratio=alpha_ratio):
276
+ return None, "quality"
277
+
278
+ key = fingerprint(cleaned)
279
+ if key in seen_hashes:
280
+ return None, "duplicate"
281
+
282
+ seen_hashes.add(key)
283
+ return cleaned, None
284
+
285
+
286
+ def log_progress(name: str, accepted_tokens: int, accepted_docs: int, started_at: float):
287
+ elapsed = max(time.time() - started_at, 1.0)
288
+ rate = accepted_tokens / elapsed
289
+ print(f" {name}: docs={accepted_docs:,} est_tokens={accepted_tokens:,} rate={rate:,.0f} tok/s elapsed={elapsed/60:.1f}m")
290
+
291
+
292
+ def build_simplewiki(target_tokens: int, sink: ParquetSink, seen_hashes: set, stats: dict):
293
+ dataset = try_load_streaming_dataset([
294
+ ("wikimedia/wikipedia", "20231101.simple"),
295
+ ("wikimedia/wikipedia", "20231101.simplewiki"),
296
+ ("wikimedia/wikipedia", "20231101.simple_en"),
297
+ ])
298
+
299
+ started_at = time.time()
300
+ accepted_tokens = 0
301
+ accepted_docs = 0
302
+ skipped = Counter()
303
+
304
+ for article in dataset:
305
+ title = (article.get("title") or "").strip()
306
+ raw_text = article.get("text") or ""
307
+
308
+ if WIKI_SKIP_PATTERNS.search(title):
309
+ skipped["meta"] += 1
310
+ continue
311
+
312
+ candidate, reason = maybe_keep(f"{title}\n\n{raw_text}", seen_hashes, min_chars=250, min_words=50, alpha_ratio=0.70)
313
+ if candidate is None:
314
+ skipped[reason] += 1
315
+ continue
316
+
317
+ sink.add("simplewiki", candidate)
318
+ accepted_docs += 1
319
+ accepted_tokens += approx_tokens(candidate)
320
+
321
+ if accepted_docs % 2_000 == 0:
322
+ log_progress("simplewiki", accepted_tokens, accepted_docs, started_at)
323
+
324
+ if accepted_tokens >= target_tokens:
325
+ break
326
+
327
+ stats["simplewiki"] = {
328
+ "accepted_docs": accepted_docs,
329
+ "accepted_tokens": accepted_tokens,
330
+ "skipped": dict(skipped),
331
+ }
332
+
333
+
334
+ def build_wiki_tail(target_tokens: int, skip_qualifying: int, sink: ParquetSink, seen_hashes: set, stats: dict):
335
+ dataset = try_load_streaming_dataset([
336
+ ("wikimedia/wikipedia", "20231101.en"),
337
+ ])
338
+
339
+ started_at = time.time()
340
+ accepted_tokens = 0
341
+ accepted_docs = 0
342
+ qualified_seen = 0
343
+ skipped = Counter()
344
+
345
+ for article in dataset:
346
+ title = (article.get("title") or "").strip()
347
+ raw_text = article.get("text") or ""
348
+
349
+ if WIKI_SKIP_PATTERNS.search(title):
350
+ skipped["meta"] += 1
351
+ continue
352
+
353
+ basic = clean_text_basic(f"{title}\n\n{raw_text}")
354
+ if not is_high_quality(basic, min_chars=800, min_words=120, alpha_ratio=0.70):
355
+ skipped["quality"] += 1
356
+ continue
357
+
358
+ qualified_seen += 1
359
+ if qualified_seen <= skip_qualifying:
360
+ skipped["already_used_window"] += 1
361
+ continue
362
+
363
+ key = fingerprint(basic)
364
+ if key in seen_hashes:
365
+ skipped["duplicate"] += 1
366
+ continue
367
+
368
+ seen_hashes.add(key)
369
+ sink.add("wiki_tail", basic)
370
+ accepted_docs += 1
371
+ accepted_tokens += approx_tokens(basic)
372
+
373
+ if accepted_docs % 2_000 == 0:
374
+ log_progress("wiki_tail", accepted_tokens, accepted_docs, started_at)
375
+
376
+ if accepted_tokens >= target_tokens:
377
+ break
378
+
379
+ stats["wiki_tail"] = {
380
+ "accepted_docs": accepted_docs,
381
+ "accepted_tokens": accepted_tokens,
382
+ "qualified_seen": qualified_seen,
383
+ "skipped": dict(skipped),
384
+ }
385
+
386
+
387
+ def build_fineweb_tail(target_tokens: int, skip_qualifying: int, min_score: float, sink: ParquetSink, seen_hashes: set, stats: dict):
388
+ dataset = try_load_streaming_dataset([
389
+ ("HuggingFaceFW/fineweb-edu", "sample-10BT"),
390
+ ])
391
+
392
+ started_at = time.time()
393
+ accepted_tokens = 0
394
+ accepted_docs = 0
395
+ qualified_seen = 0
396
+ skipped = Counter()
397
+
398
+ for doc in dataset:
399
+ score = doc.get("score", 0)
400
+ if not isinstance(score, (int, float)):
401
+ try:
402
+ score = float(score)
403
+ except (TypeError, ValueError):
404
+ skipped["bad_score"] += 1
405
+ continue
406
+
407
+ if score < min_score:
408
+ skipped["score"] += 1
409
+ continue
410
+
411
+ raw_text = doc.get("text") or ""
412
+ basic = clean_text_basic(raw_text)
413
+ if not is_high_quality(basic, min_chars=500, min_words=80, alpha_ratio=0.68):
414
+ skipped["quality"] += 1
415
+ continue
416
+
417
+ qualified_seen += 1
418
+ if qualified_seen <= skip_qualifying:
419
+ skipped["already_used_window"] += 1
420
+ continue
421
+
422
+ key = fingerprint(basic)
423
+ if key in seen_hashes:
424
+ skipped["duplicate"] += 1
425
+ continue
426
+
427
+ seen_hashes.add(key)
428
+ sink.add("fineweb_tail", basic)
429
+ accepted_docs += 1
430
+ accepted_tokens += approx_tokens(basic)
431
+
432
+ if accepted_docs % 2_000 == 0:
433
+ log_progress("fineweb_tail", accepted_tokens, accepted_docs, started_at)
434
+
435
+ if accepted_tokens >= target_tokens:
436
+ break
437
+
438
+ stats["fineweb_tail"] = {
439
+ "accepted_docs": accepted_docs,
440
+ "accepted_tokens": accepted_tokens,
441
+ "qualified_seen": qualified_seen,
442
+ "skipped": dict(skipped),
443
+ }
444
+
445
+
446
+ def build_pg19(target_tokens: int, sink: ParquetSink, seen_hashes: set, stats: dict):
447
+ dataset = try_load_streaming_dataset([
448
+ ("pg19", None),
449
+ ])
450
+
451
+ started_at = time.time()
452
+ accepted_tokens = 0
453
+ accepted_docs = 0
454
+ processed_books = 0
455
+ skipped = Counter()
456
+
457
+ for book in dataset:
458
+ title = (book.get("short_book_title") or book.get("book_title") or book.get("title") or "Untitled Book").strip()
459
+ raw_text = book.get("text") or ""
460
+ stripped = strip_project_gutenberg(raw_text)
461
+ stripped = clean_text_basic(stripped)
462
+ if len(stripped.split()) < 1_000:
463
+ skipped["short_book"] += 1
464
+ continue
465
+
466
+ processed_books += 1
467
+ for passage, _ in split_pg19_passages(title, stripped):
468
+ candidate, reason = maybe_keep(passage, seen_hashes, min_chars=900, min_words=180, alpha_ratio=0.72)
469
+ if candidate is None:
470
+ skipped[reason] += 1
471
+ continue
472
+
473
+ sink.add("pg19", candidate)
474
+ accepted_docs += 1
475
+ accepted_tokens += approx_tokens(candidate)
476
+
477
+ if accepted_docs % 2_000 == 0:
478
+ log_progress("pg19", accepted_tokens, accepted_docs, started_at)
479
+
480
+ if accepted_tokens >= target_tokens:
481
+ stats["pg19"] = {
482
+ "accepted_docs": accepted_docs,
483
+ "accepted_tokens": accepted_tokens,
484
+ "processed_books": processed_books,
485
+ "skipped": dict(skipped),
486
+ }
487
+ return
488
+
489
+ stats["pg19"] = {
490
+ "accepted_docs": accepted_docs,
491
+ "accepted_tokens": accepted_tokens,
492
+ "processed_books": processed_books,
493
+ "skipped": dict(skipped),
494
+ }
495
+
496
+
497
+ def allocate_targets(total_target: int):
498
+ raw_targets = {name: int(total_target * share) for name, share in DEFAULT_SHARES.items()}
499
+ remainder = total_target - sum(raw_targets.values())
500
+ raw_targets["pg19"] += remainder
501
+ return raw_targets
502
+
503
+
504
+ def main():
505
+ parser = argparse.ArgumentParser(description="Build a new 1B-token English curriculum corpus")
506
+ parser.add_argument("--target_tokens", type=int, default=DEFAULT_TARGET_TOKENS)
507
+ parser.add_argument("--output_dir", type=str, default="Base/data/filtered_english_curriculum_1b")
508
+ parser.add_argument("--wiki_skip", type=int, default=DEFAULT_WIKI_SKIP)
509
+ parser.add_argument("--fineweb_skip", type=int, default=DEFAULT_FINEWEB_SKIP)
510
+ parser.add_argument("--fineweb_min_score", type=float, default=DEFAULT_FINEWEB_MIN_SCORE)
511
+ args = parser.parse_args()
512
+
513
+ output_dir = Path(args.output_dir)
514
+ output_dir.mkdir(parents=True, exist_ok=True)
515
+
516
+ targets = allocate_targets(args.target_tokens)
517
+ sink = ParquetSink(output_dir)
518
+ seen_hashes = set()
519
+ stats = {}
520
+ started_at = time.time()
521
+
522
+ print("=" * 78)
523
+ print(" BUILD 1B ENGLISH CURRICULUM CORPUS")
524
+ print("=" * 78)
525
+ print(f" Output dir : {output_dir}")
526
+ print(f" Target tokens: {args.target_tokens:,}")
527
+ print(f" Targets : {targets}")
528
+ print(f" Wiki skip : {args.wiki_skip:,}")
529
+ print(f" FineWeb skip : {args.fineweb_skip:,}")
530
+ print(f" FineWeb min : {args.fineweb_min_score}")
531
+ print("=" * 78)
532
+
533
+ build_simplewiki(targets["simplewiki"], sink, seen_hashes, stats)
534
+ build_wiki_tail(targets["wiki_tail"], args.wiki_skip, sink, seen_hashes, stats)
535
+ build_fineweb_tail(targets["fineweb_tail"], args.fineweb_skip, args.fineweb_min_score, sink, seen_hashes, stats)
536
+ build_pg19(targets["pg19"], sink, seen_hashes, stats)
537
+
538
+ sink.finalize()
539
+
540
+ total_tokens = sum(source_stats["accepted_tokens"] for source_stats in stats.values())
541
+ total_docs = sum(source_stats["accepted_docs"] for source_stats in stats.values())
542
+ total_minutes = (time.time() - started_at) / 60
543
+
544
+ report_lines = [
545
+ "=" * 78,
546
+ " ENGLISH CURRICULUM 1B BUILD REPORT",
547
+ "=" * 78,
548
+ "",
549
+ f"Output directory: {output_dir}",
550
+ f"Target tokens: {args.target_tokens:,}",
551
+ f"Actual tokens: {total_tokens:,}",
552
+ f"Accepted docs: {total_docs:,}",
553
+ f"Unique hashes: {len(seen_hashes):,}",
554
+ f"Elapsed minutes: {total_minutes:.1f}",
555
+ "",
556
+ "Source breakdown:",
557
+ ]
558
+
559
+ for source_name in ("simplewiki", "wiki_tail", "fineweb_tail", "pg19"):
560
+ source_stats = stats.get(source_name, {})
561
+ report_lines.append(f" {source_name}:")
562
+ report_lines.append(f" accepted_docs: {source_stats.get('accepted_docs', 0):,}")
563
+ report_lines.append(f" accepted_tokens: {source_stats.get('accepted_tokens', 0):,}")
564
+ if "qualified_seen" in source_stats:
565
+ report_lines.append(f" qualified_seen: {source_stats['qualified_seen']:,}")
566
+ if "processed_books" in source_stats:
567
+ report_lines.append(f" processed_books: {source_stats['processed_books']:,}")
568
+ skipped = source_stats.get("skipped", {})
569
+ if skipped:
570
+ report_lines.append(" skipped:")
571
+ for reason, count in sorted(skipped.items(), key=lambda item: (-item[1], item[0])):
572
+ report_lines.append(f" {reason}: {count:,}")
573
+ report_lines.append("")
574
+
575
+ report_lines.extend([
576
+ "Curriculum notes:",
577
+ " - Simple Wikipedia improves easier explanatory English.",
578
+ " - Wikipedia tail adds formal, well-edited factual prose.",
579
+ " - FineWeb-Edu tail adds tutorials and educational explanations.",
580
+ " - PG-19 adds long-form grammar, dialogue, and vocabulary range.",
581
+ " - OpenWebText is intentionally not reused here.",
582
+ "",
583
+ "Next steps:",
584
+ " 1. Tokenize to LitData:",
585
+ " python Base/scripts/prepare_litdata.py --filtered_dir Base/data/filtered_english_curriculum_1b --output_dir Base/data/litdata_english_curriculum_1b --label ENGLISH_CURRICULUM_1B",
586
+ " 2. Continue pretraining:",
587
+ " python train.py --config train_continue_english_1b.yaml --init_model_path Base/out/pretrain/luna_100m/final/lit_model.pth",
588
+ "",
589
+ "=" * 78,
590
+ ])
591
+
592
+ report_text = "\n".join(report_lines)
593
+ print("\n" + report_text)
594
+
595
+ report_path = output_dir / "BUILD_REPORT.txt"
596
+ report_path.write_text(report_text, encoding="utf-8")
597
+
598
+ stats_path = output_dir / "build_stats.json"
599
+ stats_path.write_text(json.dumps({
600
+ "target_tokens": args.target_tokens,
601
+ "actual_tokens": total_tokens,
602
+ "accepted_docs": total_docs,
603
+ "sources": stats,
604
+ }, indent=2), encoding="utf-8")
605
+
606
+
607
+ if __name__ == "__main__":
608
+ main()