achbj
feat: implement project scaffolding, training pipelines, and utility scripts for NovaMind model development
3dfccac | """ | |
| Phase 1b Data Preparation Script | |
| ================================= | |
| Run this on the CPU-only free tier BEFORE starting your GPU session. | |
| Downloads, tokenizes, and saves all training data to disk so that | |
| the GPU session starts training immediately β zero data loading time. | |
| Data sources: | |
| - TinyStories (~2.1M stories, already seen in Phase 1 β good for annealing) | |
| - Wikipedia EN (~6.5M articles, NEVER seen by model β genuinely new data!) | |
| β Phase 1 only used FineWeb-Edu + TinyStories, so Wikipedia is 100% fresh. | |
| β Downloads as cached files (fast), NOT streaming. | |
| Usage: | |
| python3 prepare_data_phase1b.py | |
| Output: | |
| ./data/phase1b_chunks.pt (~3-5 GB on disk) | |
| Time on CPU free tier: | |
| TinyStories download: ~3-5 min | |
| Wikipedia download: ~20-40 min (~21 GB compressed) | |
| Tokenization: ~20-40 min | |
| TOTAL: ~45-90 min (no GPU cost!) | |
| Then on GPU: | |
| bash run_phase1b.sh | |
| β Loads data from disk in ~30 sec, training starts immediately. | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import json | |
| import torch | |
| from pathlib import Path | |
| from tqdm import tqdm | |
| # ββ Configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SEQ_LEN = 2048 | |
| SAVE_PATH = "./data/phase1b_chunks.pt" | |
| META_PATH = "./data/phase1b_chunks_meta.json" | |
| # Data sources β both genuinely complement Phase 1 (FineWeb-Edu + TinyStories): | |
| # Wikipedia: 6.5M articles, NEVER seen in Phase 1 β fresh new knowledge | |
| # TinyStories: already seen, but repeating at lower LR = data annealing (valid!) | |
| # ββ Setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Path("./data").mkdir(exist_ok=True) | |
| print("\n" + "="*60) | |
| print(" Phase 1b Data Preparation") | |
| print("="*60) | |
| print(f" Data sources: Wikipedia EN (new!) + TinyStories (annealing)") | |
| print(f" Seq len: {SEQ_LEN}") | |
| print(f" Save path: {SAVE_PATH}") | |
| print() | |
| # ββ Check if already done ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if Path(SAVE_PATH).exists(): | |
| size_gb = Path(SAVE_PATH).stat().st_size / 1e9 | |
| print(f"β Data file already exists: {SAVE_PATH} ({size_gb:.2f} GB)") | |
| if Path(META_PATH).exists(): | |
| with open(META_PATH) as f: | |
| meta = json.load(f) | |
| print(f" Chunks: {meta['n_chunks']:,} | Tokens: {meta['tokens_B']:.2f}B") | |
| print("\nπ Ready to train! Run: bash run_phase1b.sh") | |
| sys.exit(0) | |
| # ββ Load tokenizer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("π€ Loading tokenizer...") | |
| from transformers import AutoTokenizer | |
| tokenizer = None | |
| for tok_name in [ | |
| "NousResearch/Llama-2-7b-hf", | |
| "meta-llama/Llama-2-7b-hf", | |
| "gpt2", | |
| ]: | |
| try: | |
| tokenizer = AutoTokenizer.from_pretrained(tok_name) | |
| print(f" β {tok_name} β vocab: {tokenizer.vocab_size:,}") | |
| break | |
| except Exception as e: | |
| print(f" β οΈ {tok_name} unavailable: {e}") | |
| if tokenizer is None: | |
| raise RuntimeError("No tokenizer available. Run: huggingface-cli login") | |
| eos = tokenizer.eos_token_id | |
| # ββ Token packing helper βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def pack_texts(texts, desc="packing") -> list: | |
| """Pack text list into [SEQ_LEN+1] overlapping token chunks.""" | |
| chunks, current = [], [] | |
| for text in tqdm(texts, desc=f" {desc}", leave=False): | |
| ids = tokenizer.encode(text, add_special_tokens=False) + [eos] | |
| if len(ids) >= SEQ_LEN + 1: | |
| if current: | |
| chunks.append(current + [eos] * ((SEQ_LEN+1) - len(current))) | |
| current = [] | |
| for i in range(0, len(ids) - SEQ_LEN, SEQ_LEN): | |
| chunks.append(ids[i: i+SEQ_LEN+1]) | |
| else: | |
| if len(current) + len(ids) > SEQ_LEN + 1: | |
| chunks.append(current + [eos] * ((SEQ_LEN+1) - len(current))) | |
| current = [] | |
| current.extend(ids) | |
| if len(current) >= 2: | |
| chunks.append(current + [eos] * ((SEQ_LEN+1) - len(current))) | |
| return chunks | |
| all_chunks = [] | |
| # ββ 1. TinyStories (downloadable files, fast) ββββββββββββββββββββββββββββββββββ | |
| print("\nπ Downloading TinyStories (~1 GB, fast download)...") | |
| from datasets import load_dataset | |
| t0 = time.time() | |
| ts_ds = load_dataset("roneneldan/TinyStories", split="train") | |
| print(f" β {len(ts_ds):,} stories downloaded in {(time.time()-t0)/60:.1f} min") | |
| ts_chunks = pack_texts([r["text"] for r in ts_ds], "Tokenizing TinyStories") | |
| print(f" β {len(ts_chunks):,} chunks ({len(ts_chunks)*SEQ_LEN/1e9:.2f}B tokens)") | |
| all_chunks.extend(ts_chunks) | |
| # ββ 2. Wikipedia EN (downloadable, ~21GB, NEVER seen in Phase 1) ββββββββββββββ | |
| print(f"\nπ Downloading English Wikipedia (genuinely new data for model)...") | |
| print(" Phase 1 only used FineWeb-Edu + TinyStories β Wikipedia is 100% fresh.") | |
| print(" Downloading as cached files (not streaming) β much faster than FineWeb!") | |
| print(" First download: ~20-40 min. Subsequent runs: instant from cache.\n") | |
| t0 = time.time() | |
| try: | |
| wiki_ds = load_dataset("wikipedia", "20220301.en", split="train", | |
| trust_remote_code=True) | |
| elapsed_min = (time.time() - t0) / 60 | |
| print(f" β {len(wiki_ds):,} articles downloaded in {elapsed_min:.1f} min") | |
| wiki_chunks = pack_texts([r["text"] for r in wiki_ds], "Tokenizing Wikipedia") | |
| print(f" β {len(wiki_chunks):,} chunks ({len(wiki_chunks)*SEQ_LEN/1e9:.2f}B tokens)") | |
| all_chunks.extend(wiki_chunks) | |
| except Exception as e: | |
| print(f" β οΈ Wikipedia download failed: {e}") | |
| print(" Falling back to FineWeb-Edu streaming (500K docs)...") | |
| fw_ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT", | |
| split="train", streaming=True) | |
| fw_texts = [] | |
| for i, row in enumerate(tqdm(fw_ds, total=500_000, desc=" FineWeb fallback")): | |
| if i >= 500_000: break | |
| fw_texts.append(row["text"]) | |
| fw_chunks = pack_texts(fw_texts, "Tokenizing FineWeb") | |
| print(f" β {len(fw_chunks):,} chunks") | |
| all_chunks.extend(fw_chunks) | |
| # ββ Save to disk βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print(f"\nπΎ Saving {len(all_chunks):,} total chunks to disk...") | |
| print(" (Saving as int16 to halve file size β auto-cast to int64 when loading)") | |
| # int16 works because vocab_size=32000 < 32767 | |
| tensor = torch.tensor(all_chunks, dtype=torch.int16) | |
| torch.save(tensor, SAVE_PATH) | |
| size_gb = Path(SAVE_PATH).stat().st_size / 1e9 | |
| tokens_B = len(all_chunks) * SEQ_LEN / 1e9 | |
| # Save metadata | |
| meta = { | |
| "n_chunks": len(all_chunks), | |
| "tokens_B": round(tokens_B, 3), | |
| "seq_len": SEQ_LEN, | |
| "sources": ["TinyStories (annealing)", "Wikipedia EN (new data)"], | |
| "dtype": "int16", | |
| "instructions": "Load with: torch.load(path).long()", | |
| } | |
| with open(META_PATH, "w") as f: | |
| json.dump(meta, f, indent=2) | |
| print(f"\n{'='*60}") | |
| print(f" β DATA PREP COMPLETE") | |
| print(f"{'='*60}") | |
| print(f" File: {SAVE_PATH}") | |
| print(f" Size: {size_gb:.2f} GB") | |
| print(f" Chunks: {len(all_chunks):,}") | |
| print(f" Tokens: {tokens_B:.2f}B") | |
| print() | |
| print(" Now start your GPU session and run:") | |
| print(" bash run_phase1b.sh") | |
| print() | |