Download data/scripts/kaggle_collector.py from ViuAI/ViuMini-Dense-360M: direct link, hf CLI and curl.
- Browser
- Download file 21.2 kB
-
https://huggingface.co/ViuAI/ViuMini-Dense-360M/resolve/main/data/scripts/kaggle_collector.py
- Command line
-
hf download hf://ViuAI/ViuMini-Dense-360M/data/scripts/kaggle_collector.py
-
curl -L -o kaggle_collector.py https://huggingface.co/ViuAI/ViuMini-Dense-360M/resolve/main/data/scripts/kaggle_collector.py
21.2 kB
| """ | |
| Kaggle Cloud Data Collector for ViuMini-MoE-242M. | |
| Streams massive open datasets, cleans with stable blake2b hash dedup, | |
| chunks into ~150MB Parquet shards (configurable via --shard_mb, default 150), | |
| uploads to Hugging Face dataset repo `ViuAI/viu-mini-raw-pretrain`, and immediately deletes | |
| local temporary files to keep Kaggle disk usage < 2GB. | |
| Features: | |
| - Smart Auto-Resume: Automatically queries HF dataset repo, finds existing shards, | |
| starts numbering at (max_shard_id + 1), and fast-skips already processed rows. | |
| - ~150MB Shards: keeps HF viewer + DataLoader stable (old 1GB shards caused 1.6GB | |
| english files + viewer HTTP 500). Row cap 100k also enforced. | |
| - Zero Sub-Sampling: Ingests full corpora without artificial truncation. | |
| Usage on Kaggle: | |
| python data/scripts/kaggle_collector.py --stream all --repo ViuAI/viu-mini-raw-pretrain --shard_mb 150 | |
| python data/scripts/kaggle_collector.py --stream hindi --repo ViuAI/viu-mini-raw-pretrain --shard_mb 150 | |
| """ | |
| import argparse | |
| import gc | |
| import gzip | |
| import hashlib | |
| import itertools | |
| import json | |
| import os | |
| from pathlib import Path | |
| import re | |
| import sys | |
| import tempfile | |
| import time | |
| DEV = re.compile(r'[\u0900-\u097F]') | |
| LAT = re.compile(r'[A-Za-z]') | |
| def get_hf_token(): | |
| """Retrieve HF token from Kaggle secrets, Colab secrets, env var, or user input.""" | |
| # 1. Check Kaggle secrets | |
| try: | |
| from kaggle_secrets import UserSecretsClient | |
| tok = UserSecretsClient().get_secret("HF_TOKEN") | |
| if tok: | |
| return tok | |
| except Exception: | |
| pass | |
| # 2. Check Google Colab secrets | |
| try: | |
| from google.colab import userdata | |
| tok = userdata.get("HF_TOKEN") | |
| if tok: | |
| return tok | |
| except Exception: | |
| pass | |
| # 3. Check environment variable | |
| tok = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") | |
| if tok: | |
| return tok | |
| return None | |
| def stable_hash(s: str) -> int: | |
| """Stable 64-bit hash (PYTHONHASHSEED-independent). hash() is randomized per run, | |
| so dedup was unstable across Kaggle restarts -> duplicates. Use blake2b.""" | |
| return int.from_bytes(hashlib.blake2b(s.encode("utf-8"), digest_size=8).digest(), "big") | |
| def dev_ratio(s: str) -> float: | |
| dev = len(DEV.findall(s)) | |
| lat = len(LAT.findall(s)) | |
| tot = dev + lat | |
| return dev / max(tot, 1) | |
| def is_valid_line(s: str, lang: str) -> bool: | |
| if len(s) < 10 or len(s.split()) < 3: | |
| return False | |
| if lang == "bilingual": | |
| return len(s) >= 15 | |
| r = dev_ratio(s) | |
| if lang == "hinglish": | |
| # Must have Latin letters, Devanagari ratio <= 30% | |
| return bool(LAT.search(s)) and r <= 0.3 | |
| elif lang == "hindi": | |
| # Must have Devanagari script | |
| return bool(DEV.search(s)) | |
| elif lang == "english": | |
| # Must have Latin, zero Devanagari | |
| return bool(LAT.search(s)) and not bool(DEV.search(s)) | |
| return True | |
| def apply_skip(ds, skip_rows: int, source_name: str): | |
| """Applies fast skipping or falls back to itertools.islice.""" | |
| if skip_rows <= 0: | |
| return ds | |
| try: | |
| ds = ds.skip(skip_rows) | |
| print(f"[resume] Fast-skipped {skip_rows:,} rows from {source_name}.") | |
| return ds | |
| except Exception as e: | |
| print(f"[resume] Fast-skip failed ({e}), using iterator slice for {source_name}...") | |
| return itertools.islice(ds, skip_rows, None) | |
| def stream_hindi_data(skip_rows: int = 0): | |
| """Streams full IndicCorpV2 hin_Deva (152M rows) and Sangraha.""" | |
| try: | |
| from datasets import load_dataset | |
| print("[source] Loading ai4bharat/IndicCorpV2 (indiccorp_v2, hin_Deva) streaming...") | |
| ds = load_dataset("ai4bharat/IndicCorpV2", "indiccorp_v2", split="hin_Deva", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "IndicCorpV2") | |
| for r in ds: | |
| txt = (r.get("text") or "").strip() | |
| if txt: | |
| yield txt, "indiccorp_v2" | |
| except Exception as e: | |
| print(f"[warn] IndicCorpV2 stream error: {e}, falling back to sangraha...") | |
| try: | |
| from datasets import load_dataset | |
| ds = load_dataset("ai4bharat/sangraha", "verified", split="hin", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "sangraha") | |
| for r in ds: | |
| txt = (r.get("text") or "").strip() | |
| if txt: | |
| yield txt, "sangraha" | |
| except Exception as e2: | |
| print(f"[error] Sangraha stream error: {e2}") | |
| def stream_english_data(skip_rows: int = 0): | |
| """Streams Fineweb-edu / TinyStories educational slices.""" | |
| from datasets import load_dataset | |
| print("[source] Loading HuggingFaceFW/fineweb-edu (sample-10BT or train) streaming...") | |
| try: | |
| ds = load_dataset("HuggingFaceFW/fineweb-edu", split="train", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "fineweb-edu") | |
| for r in ds: | |
| txt = (r.get("text") or "").strip() | |
| if txt: | |
| yield txt, "fineweb-edu" | |
| except Exception as e: | |
| print(f"[warn] fineweb-edu fallback to TinyStories: {e}") | |
| ds = load_dataset("roneneldan/TinyStories", split="train", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "tinystories") | |
| for r in ds: | |
| txt = (r.get("text") or "").strip() | |
| if txt: | |
| yield txt, "tinystories" | |
| def stream_hinglish_data(skip_rows: int = 0): | |
| """Streams sk-community/romanized_hindi and conversational corpora.""" | |
| from datasets import load_dataset | |
| print("[source] Loading sk-community/romanized_hindi streaming...") | |
| try: | |
| ds = load_dataset("sk-community/romanized_hindi", split="train", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "romanized_hindi") | |
| for r in ds: | |
| txt = (r.get("Transliterated Hindi") or r.get("hi_rom") or r.get("text") or "").strip() | |
| if txt: | |
| yield txt, "romanized_hindi" | |
| except Exception as e: | |
| print(f"[warn] sk-community/romanized_hindi stream error: {e}") | |
| def stream_domain_data(): | |
| """Streams curated 26-pillar knowledge from collect_domains.""" | |
| try: | |
| from data.scripts.collect_domains import generate_domain_samples | |
| for rec in generate_domain_samples(): | |
| yield rec["text"], rec["source"], rec["lang"], rec["domain"] | |
| except ImportError: | |
| try: | |
| from collect_domains import generate_domain_samples | |
| for rec in generate_domain_samples(): | |
| yield rec["text"], rec["source"], rec["lang"], rec["domain"] | |
| except Exception as e: | |
| print(f"[warn] domain generator import failed: {e}") | |
| def stream_translation_data(skip_rows: int = 0): | |
| """Streams parallel translation data from cfilt/iitb-english-hindi & Romanized Hindi.""" | |
| from datasets import load_dataset | |
| print("[source] Loading cfilt/iitb-english-hindi parallel translation streaming...") | |
| try: | |
| ds = load_dataset("cfilt/iitb-english-hindi", split="train", streaming=True) | |
| ds = apply_skip(ds, skip_rows, "iitb-english-hindi") | |
| for r in ds: | |
| trans = r.get("translation") or {} | |
| en = (trans.get("en") or "").strip() | |
| hi = (trans.get("hi") or "").strip() | |
| if en and hi and len(en) >= 10 and len(hi) >= 10: | |
| text = f"English: {en}\nHindi: {hi}" | |
| yield text, "iitb-english-hindi", "bilingual", "translation" | |
| except Exception as e: | |
| print(f"[warn] iitb translation stream fallback: {e}") | |
| def get_existing_shards_info(api, repo_id: str, stream_name: str): | |
| """ | |
| Checks HF repo for existing shards in {stream_name}/. | |
| Returns (next_shard_id, max_shard_id, existing_count). | |
| """ | |
| if not api: | |
| return 0, -1, 0 | |
| try: | |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset") | |
| prefix = f"{stream_name}/train-" | |
| shard_ids = [] | |
| for f in files: | |
| if f.startswith(prefix) and (f.endswith(".parquet") or f.endswith(".jsonl.gz")): | |
| base = f[len(prefix):] | |
| num_str = base.split(".")[0] | |
| if num_str.isdigit(): | |
| shard_ids.append(int(num_str)) | |
| if shard_ids: | |
| max_id = max(shard_ids) | |
| return max_id + 1, max_id, len(shard_ids) | |
| except Exception as e: | |
| print(f"[warn] Could not inspect existing shards for {stream_name}: {e}") | |
| return 0, -1, 0 | |
| def write_and_upload_shard(records, shard_id, stream_name, repo_id, api, dry_run=False, tmp_dir=None): | |
| """ | |
| Writes records to a compressed parquet shard, uploads to HF, | |
| and immediately removes the local file and frees memory. | |
| """ | |
| if tmp_dir is None: | |
| tmp_dir = Path(tempfile.gettempdir()) / "viu_shards" | |
| tmp_path = Path(tmp_dir) / stream_name | |
| tmp_path.mkdir(parents=True, exist_ok=True) | |
| file_name = f"train-{shard_id:05d}.parquet" | |
| out_file = tmp_path / file_name | |
| remote_path = f"{stream_name}/{file_name}" | |
| try: | |
| import pandas as pd | |
| df = pd.DataFrame(records) | |
| df.to_parquet(out_file, index=False, compression="snappy") | |
| del df | |
| gc.collect() | |
| except Exception as e: | |
| # Fallback to jsonl.gz if parquet/pandas is unavailable | |
| file_name = f"train-{shard_id:05d}.jsonl.gz" | |
| out_file = tmp_path / file_name | |
| remote_path = f"{stream_name}/{file_name}" | |
| with gzip.open(out_file, "wt", encoding="utf-8") as f: | |
| for r in records: | |
| f.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| gc.collect() | |
| size_mb = out_file.stat().st_size / (1024 * 1024) | |
| print(f"[shard] Serialized {file_name} with {len(records):,} rows ({size_mb:.2f} MB)") | |
| if dry_run: | |
| print(f"[dry-run] Skipped upload of {out_file} to {repo_id}/{remote_path}") | |
| out_file.unlink(missing_ok=True) | |
| return True | |
| # Upload to HF with retry and exponential backoff + 429 rate limit handler (Overnight resilient) | |
| retries = 999 | |
| success = False | |
| for attempt in range(1, retries + 1): | |
| try: | |
| api.upload_file( | |
| path_or_fileobj=str(out_file), | |
| path_in_repo=remote_path, | |
| repo_id=repo_id, | |
| repo_type="dataset", | |
| commit_message=f"Upload {remote_path} ({len(records):,} rows, {size_mb:.1f} MB)" | |
| ) | |
| print(f"[hf] Uploaded successfully: {remote_path} ({size_mb:.2f} MB)") | |
| success = True | |
| break | |
| except Exception as err: | |
| err_str = str(err) | |
| if "429" in err_str or "rate limit" in err_str.lower(): | |
| wait_time = 120 | |
| m = re.search(r"Retry after (\d+) seconds", err_str) | |
| if m: | |
| wait_time = int(m.group(1)) + 5 | |
| print(f"[rate-limit] HF 128 commits/hr limit hit. Waiting {wait_time}s before retry ({attempt}/{retries})...") | |
| time.sleep(wait_time) | |
| else: | |
| print(f"[warn] Upload attempt {attempt}/{retries} failed: {err}") | |
| time.sleep(4 * attempt) | |
| # Immediately delete local temporary shard to keep Kaggle disk clean | |
| out_file.unlink(missing_ok=True) | |
| if not success: | |
| print(f"[error] Failed to upload {remote_path} after {retries} retries!") | |
| return success | |
| def run_collector(stream_name, repo_id, shard_mb=150, shard_rows=100_000, | |
| resume=True, skip_rows=None, limit=None, dry_run=False, token=None): | |
| from huggingface_hub import HfApi | |
| api = None | |
| if not dry_run: | |
| if not token: | |
| token = get_hf_token() | |
| if not token: | |
| print("[error] HF token missing! Set HF_TOKEN environment variable or Kaggle secret.") | |
| sys.exit(1) | |
| api = HfApi(token=token) | |
| try: | |
| api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True) | |
| print(f"[hf] Verified dataset repo: {repo_id}") | |
| except Exception as e: | |
| print(f"[warn] create_repo warning: {e}") | |
| # Determine starting shard_id and rows to skip if resuming | |
| # FIX: old code assumed 100k rows/shard even for 1.6GB english shards (~3-4M rows), | |
| # causing massive duplication on resume. Now shard_rows is enforced, so | |
| # skip = (max_id+1) * shard_rows is correct. For legacy 1GB shards, require | |
| # explicit --skip_rows or manual backfill (see docs/HF_REPAIR_RUNBOOK.md). | |
| shard_id = 0 | |
| actual_skip = 0 | |
| if resume and not dry_run: | |
| next_id, max_id, count = get_existing_shards_info(api, repo_id, stream_name) | |
| if count > 0: | |
| shard_id = next_id | |
| if skip_rows is None: | |
| actual_skip = (max_id + 1) * shard_rows | |
| print(f"[resume] Found {count} existing shards in '{stream_name}' (up to train-{max_id:05d}.parquet).") | |
| print(f"[resume] Resuming at shard_id={shard_id} (train-{shard_id:05d}.parquet), auto-skipping ~{actual_skip:,} rows.") | |
| print(f"[resume] NOTE: legacy 1GB shards (english/hindi-big) used variable rows. " | |
| f"If resuming those, pass explicit --skip_rows. See docs/HF_REPAIR_RUNBOOK.md") | |
| else: | |
| actual_skip = skip_rows | |
| print(f"[resume] Resuming at shard_id={shard_id}, skipping specified {actual_skip:,} rows.") | |
| else: | |
| actual_skip = skip_rows or 0 | |
| else: | |
| actual_skip = skip_rows or 0 | |
| # Target byte calculation: Snappy parquet ~2.7x for hindi text, ~1.6x for english. | |
| # FIX: old default 1000MB caused 1.6GB english shards -> HF viewer 500 + OOM. | |
| # New default 150MB + hard row cap 100k keeps shards 30-150MB, viewer-stable. | |
| # Conservative ratio 2.0 to avoid overshoot on english. | |
| target_raw_bytes = int(shard_mb * 1024 * 1024 * 2.0) | |
| if stream_name == "domains": | |
| # Domain samples are curated; use smaller chunk threshold to flush promptly | |
| target_raw_bytes = int(min(shard_mb, 150) * 1024 * 1024 * 2.7) | |
| print(f"\n=== Starting Collector: stream={stream_name}, repo={repo_id} ===") | |
| print(f" Target shard size: ~{shard_mb} MB compressed | Start shard: train-{shard_id:05d}.parquet | Skip: {actual_skip:,} rows") | |
| seen_hashes = set() | |
| current_records = [] | |
| current_raw_bytes = 0 | |
| total_kept = 0 | |
| start_time = time.time() | |
| last_log_time = start_time | |
| # Determine generator | |
| if stream_name == "hindi": | |
| gen = ((txt, src, "hindi", "general_hindi") for txt, src in stream_hindi_data(skip_rows=actual_skip)) | |
| elif stream_name == "english": | |
| gen = ((txt, src, "english", "general_english") for txt, src in stream_english_data(skip_rows=actual_skip)) | |
| elif stream_name == "hinglish": | |
| gen = ((txt, src, "hinglish", "general_hinglish") for txt, src in stream_hinglish_data(skip_rows=actual_skip)) | |
| elif stream_name == "domains": | |
| gen = stream_domain_data() | |
| elif stream_name == "translation": | |
| gen = stream_translation_data(skip_rows=actual_skip) | |
| else: | |
| raise ValueError(f"Unknown stream: {stream_name}") | |
| for item in gen: | |
| txt, src, lang, dom = item[0], item[1], item[2], item[3] | |
| s = txt.strip() | |
| if not is_valid_line(s, lang): | |
| continue | |
| # In-flight stable-hash deduplication (FIX: hash() was seed-randomized) | |
| # NOTE: seen_hashes grows with total rows. For >10M-row runs on Kaggle, | |
| # memory can blow up. We cap at 5M entries then clear oldest via sampling | |
| # reset to keep RAM <2GB (small duplicate leak acceptable vs OOM). | |
| h = stable_hash(s) | |
| if h in seen_hashes: | |
| continue | |
| seen_hashes.add(h) | |
| if len(seen_hashes) > 5_000_000: | |
| print("[dedup] seen_hashes >5M, clearing to bound RAM (minor dup leak accepted)") | |
| seen_hashes.clear() | |
| row_bytes = len(s.encode('utf-8')) | |
| current_records.append({ | |
| "text": s, | |
| "lang": lang, | |
| "source": src, | |
| "domain": dom | |
| }) | |
| current_raw_bytes += row_bytes | |
| total_kept += 1 | |
| # Periodic status log every 100,000 rows or every 30 seconds | |
| now = time.time() | |
| if total_kept % 100_000 == 0 or (now - last_log_time) >= 30.0: | |
| elapsed = max(now - start_time, 1) | |
| rate = total_kept / elapsed | |
| est_parquet_mb = (current_raw_bytes / 2.7) / (1024 * 1024) | |
| print(f"[progress] {total_kept:,} rows | Shard buffer: {len(current_records):,} rows (~{est_parquet_mb:.1f} MB est. parquet) | {rate:.1f} rows/s") | |
| last_log_time = now | |
| # Flush condition: Target MB reached OR explicit row cap reached | |
| hit_bytes = current_raw_bytes >= target_raw_bytes | |
| hit_rows = (shard_rows is not None and len(current_records) >= shard_rows) | |
| if hit_bytes or hit_rows: | |
| write_and_upload_shard(current_records, shard_id, stream_name, repo_id, api, dry_run=dry_run) | |
| shard_id += 1 | |
| current_records = [] | |
| current_raw_bytes = 0 | |
| gc.collect() | |
| if limit and total_kept >= limit: | |
| print(f"[limit] Reached limit of {limit:,} rows.") | |
| break | |
| # Flush remaining records | |
| if current_records: | |
| write_and_upload_shard(current_records, shard_id, stream_name, repo_id, api, dry_run=dry_run) | |
| shard_id += 1 | |
| current_records = [] | |
| gc.collect() | |
| elapsed = max(time.time() - start_time, 1) | |
| print(f"=== Collector Finished: {total_kept:,} total rows in this session ({elapsed:.1f}s, {total_kept/elapsed:.1f} rows/s) ===") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Kaggle High-Throughput Cloud Stream Collector") | |
| parser.add_argument("--stream", type=str, required=True, | |
| choices=["hindi", "english", "hinglish", "domains", "translation", "all"], | |
| help="Data stream to collect (or 'all' for sequential run)") | |
| parser.add_argument("--repo", type=str, default="ViuAI/viu-mini-raw-pretrain", help="HF dataset repo ID") | |
| parser.add_argument("--shard_mb", type=int, default=150, help="Target compressed Parquet shard size in MB (default 150 MB, viewer-stable; old 1000 MB caused 1.6GB shards + viewer 500)") | |
| parser.add_argument("--shard_rows", type=int, default=100000, help="Row cap per shard (default 100k; whichever hits first: bytes or rows)") | |
| parser.add_argument("--no-resume", action="store_true", help="Do not auto-resume; start from shard 0 and row 0") | |
| parser.add_argument("--skip_rows", type=int, default=None, help="Explicitly skip N rows before streaming") | |
| parser.add_argument("--limit", type=int, default=None, help="Optional max rows to collect") | |
| parser.add_argument("--dry-run", action="store_true", help="Run without uploading to HF") | |
| parser.add_argument("--token", type=str, default=None, help="HF API token") | |
| args = parser.parse_args() | |
| resume = not args.no_resume | |
| if args.stream == "all": | |
| # Prioritize Hinglish and English so they accumulate immediately alongside Hindi | |
| streams = ["domains", "translation", "hinglish", "english", "hindi"] | |
| for s in streams: | |
| print(f"\n==========================================") | |
| print(f" STARTING STREAM: {s.upper()}") | |
| print(f"==========================================") | |
| # Smart check: if domains or translation are already fully uploaded, proceed immediately | |
| if resume and not args.dry_run: | |
| try: | |
| from huggingface_hub import HfApi | |
| check_api = HfApi(token=args.token or get_hf_token()) | |
| _, _, existing_cnt = get_existing_shards_info(check_api, args.repo, s) | |
| if s == "domains" and existing_cnt >= 2: | |
| print(f"[all] Stream 'domains' already fully uploaded ({existing_cnt} shards). Skipping to next...") | |
| continue | |
| if s == "translation" and existing_cnt >= 13: | |
| print(f"[all] Stream 'translation' already fully uploaded ({existing_cnt} shards). Skipping to next...") | |
| continue | |
| except Exception as e_chk: | |
| print(f"[warn] Existing check error for {s}: {e_chk}") | |
| run_collector( | |
| stream_name=s, | |
| repo_id=args.repo, | |
| shard_mb=args.shard_mb, | |
| shard_rows=args.shard_rows, | |
| resume=resume, | |
| skip_rows=args.skip_rows, | |
| limit=args.limit, | |
| dry_run=args.dry_run, | |
| token=args.token | |
| ) | |
| else: | |
| run_collector( | |
| stream_name=args.stream, | |
| repo_id=args.repo, | |
| shard_mb=args.shard_mb, | |
| shard_rows=args.shard_rows, | |
| resume=resume, | |
| skip_rows=args.skip_rows, | |
| limit=args.limit, | |
| dry_run=args.dry_run, | |
| token=args.token | |
| ) | |
| if __name__ == "__main__": | |
| main() | |