ViuAI commited on
Commit
0c19fce
·
verified ·
1 Parent(s): d192183

Upload code/data_builders/build_500m_sarus_1m_dataset.py with huggingface_hub

Browse files
code/data_builders/build_500m_sarus_1m_dataset.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # 🚀 ViuAI Sarus-500M — Master 500K-1M Mega Chat & Knowledge Dataset Builder
3
+ # Target Hugging Face Repo: ViuAI/500M_Sarus_SFT
4
+ # ==============================================================================
5
+ # Seamlessly builds 500,000 to 1,000,000 High-Quality Samples:
6
+ # 1. Ingests local/HF identity (sft_identity.json) & greetings (sft_greetings.json)
7
+ # 2. Ingests all 21 raw capability domains from ViuAI/sarus-sft-data-raw (~288,000 samples)
8
+ # 3. High-speed streams additional Science, World & Indian History, Multi-turn Chat,
9
+ # and Instruction Following from top Hugging Face open datasets.
10
+ # 4. Tokenizes directly with 64,003 vocab into memory-mapped NumPy shards.
11
+ # 5. Auto-uploads raw JSONL + tokenized shards to Hugging Face Hub (ViuAI/500M_Sarus_SFT).
12
+ # ==============================================================================
13
+
14
+ import os
15
+ import sys
16
+ import json
17
+ import re
18
+ import time
19
+ import random
20
+ import shutil
21
+ import argparse
22
+ import numpy as np
23
+ from datasets import load_dataset
24
+ from huggingface_hub import HfApi, hf_hub_download, login
25
+ from tokenizers import Tokenizer
26
+
27
+ if hasattr(sys.stdout, "reconfigure"):
28
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
29
+ if hasattr(sys.stderr, "reconfigure"):
30
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
31
+
32
+ # ------------------------------------------------------------------------------
33
+ # 1. Config & CLI Arguments
34
+ # ------------------------------------------------------------------------------
35
+ parser = argparse.ArgumentParser(description="ViuAI Sarus-500M Mega SFT Dataset Builder (500k-1M)")
36
+ parser.add_argument("--target_samples", type=int, default=600000, help="Target total samples (e.g. 500000 to 1000000)")
37
+ parser.add_argument("--repo_id", type=str, default="ViuAI/500M_Sarus_SFT", help="Hugging Face Dataset Repo ID")
38
+ parser.add_argument("--model_repo", type=str, default="ViuAI/ViuAI-500M", help="Hugging Face Model Repo for Tokenizer/Base weights")
39
+ parser.add_argument("--raw_repo", type=str, default="ViuAI/sarus-sft-data-raw", help="Hugging Face Raw SFT Repo")
40
+ parser.add_argument("--push_to_hf", action="store_true", help="Auto push to Hugging Face Hub")
41
+ parser.add_argument("--hf_token", type=str, default=None, help="Hugging Face API Token")
42
+ parser.add_argument("--max_seq_len", type=int, default=2048, help="Maximum sequence length")
43
+ parser.add_argument("--val_ratio", type=float, default=0.015, help="Validation split ratio")
44
+ args = parser.parse_args()
45
+
46
+ HF_TOKEN = args.hf_token or os.environ.get("HF_TOKEN")
47
+ if HF_TOKEN:
48
+ try:
49
+ login(token=HF_TOKEN)
50
+ print("✅ Logged in to Hugging Face Hub successfully.")
51
+ except Exception as e:
52
+ print(f"⚠️ Notice: HF login warning: {e}")
53
+
54
+ BASE_DIR = os.getcwd()
55
+ OUTPUT_RAW_DIR = os.path.join(BASE_DIR, "data", "500M_Sarus_SFT_raw")
56
+ OUTPUT_TOKENIZED_DIR = os.path.join(BASE_DIR, "tokenized_data", "500M_Sarus_SFT")
57
+ os.makedirs(OUTPUT_RAW_DIR, exist_ok=True)
58
+ os.makedirs(OUTPUT_TOKENIZED_DIR, exist_ok=True)
59
+
60
+ USER_TOKEN = "<|user|>"
61
+ ASSISTANT_TOKEN = "<|assistant|>"
62
+ EOT_TOKEN = "<|endofturn|>"
63
+ EOT_ID = 64002
64
+ PAD_TOKEN_ID = 64002
65
+
66
+ DOMAIN_MAP = {
67
+ 0: "identity_persona",
68
+ 1: "greetings_warm",
69
+ 2: "science_ncert_stem",
70
+ 3: "indian_history_affairs",
71
+ 4: "multiturn_conversations",
72
+ 5: "everyday_chat_empathy",
73
+ 6: "instruction_constraints",
74
+ 7: "practical_logic_qa",
75
+ 8: "workplace_corporate_writing",
76
+ 9: "python_tech_troubleshoot",
77
+ 10: "cooking_kitchen_science",
78
+ 11: "safety_antihallucination"
79
+ }
80
+
81
+ # ------------------------------------------------------------------------------
82
+ # 2. Text Cleaner & Safety Filter
83
+ # ------------------------------------------------------------------------------
84
+ FORBIDDEN_IDENTITIES = [
85
+ (re.compile(r"\b(i am|as an?)\s+(ai\s+language\s+model|large\s+language\s+model)\s+created\s+by\s+openai\b", re.I),
86
+ "I am Sarus, a lightweight cognitive AI developed by ViuAI."),
87
+ (re.compile(r"\b(trained|developed|created)\s+by\s+openai\b", re.I),
88
+ "developed by ViuAI"),
89
+ (re.compile(r"\bchatgpt\b", re.I), "Sarus")
90
+ ]
91
+
92
+ DEVANAGARI_REGEX = re.compile(r"[\u0900-\u097F]")
93
+
94
+ def clean_text_pair(instruction: str, response: str):
95
+ if not instruction or not response:
96
+ return None, None
97
+ inst = instruction.strip()
98
+ resp = response.strip()
99
+
100
+ # Rule: 100% Pure English for Sarus SFT (Zero Devanagari Hindi in English chat mode)
101
+ if DEVANAGARI_REGEX.search(inst) or DEVANAGARI_REGEX.search(resp):
102
+ return None, None
103
+
104
+ # Scrub competitor identities
105
+ for pat, repl in FORBIDDEN_IDENTITIES:
106
+ resp = pat.sub(repl, resp)
107
+
108
+ # Strip any stray <think> or </think> tags
109
+ resp = re.sub(r"\[/?THINK\]", "", resp, flags=re.I)
110
+ resp = re.sub(r"</?think>", "", resp, flags=re.I).strip()
111
+
112
+ if len(inst) < 3 or len(resp) < 5:
113
+ return None, None
114
+
115
+ return inst, resp
116
+
117
+ # ------------------------------------------------------------------------------
118
+ # 3. Tokenizer Setup
119
+ # ------------------------------------------------------------------------------
120
+ def load_tokenizer():
121
+ tok_candidates = [
122
+ os.path.join(BASE_DIR, "tokenizer", "tokenizer.json"),
123
+ os.path.join(BASE_DIR, "tokenizer.json")
124
+ ]
125
+ for p in tok_candidates:
126
+ if os.path.exists(p):
127
+ print(f"✅ Found local tokenizer: {p}")
128
+ return Tokenizer.from_file(p)
129
+
130
+ print("🌐 Downloading tokenizer from Hugging Face...")
131
+ dl = hf_hub_download(repo_id=args.model_repo, filename="tokenizer/tokenizer.json", token=HF_TOKEN)
132
+ os.makedirs(os.path.join(BASE_DIR, "tokenizer"), exist_ok=True)
133
+ shutil.copy(dl, os.path.join(BASE_DIR, "tokenizer", "tokenizer.json"))
134
+ return Tokenizer.from_file(dl)
135
+
136
+ # ------------------------------------------------------------------------------
137
+ # 4. Master Ingestion Engine
138
+ # ------------------------------------------------------------------------------
139
+ def main():
140
+ print("=" * 85)
141
+ print("🚀 ViuAI Sarus-500M — Master 500K-1M SFT Dataset Builder")
142
+ print(f" • Target Sample Count: {args.target_samples:,}")
143
+ print(f" • Output Repo: {args.repo_id}")
144
+ print(f" • Raw Directory: {OUTPUT_RAW_DIR}")
145
+ print(f" • Tokenized Shards: {OUTPUT_TOKENIZED_DIR}")
146
+ print("=" * 85)
147
+
148
+ tokenizer = load_tokenizer()
149
+ all_samples = []
150
+
151
+ # --------------------------------------------------------------------------
152
+ # Step 1: Ingest Local Identity & Greetings
153
+ # --------------------------------------------------------------------------
154
+ print("\n📦 [1/4] Ingesting Identity & Greetings...")
155
+ ident_files = [
156
+ ("data/sft_identity.json", 0),
157
+ ("data/sft_greetings.json", 1),
158
+ ]
159
+ for local_f, dom_id in ident_files:
160
+ p = os.path.join(BASE_DIR, local_f)
161
+ if not os.path.exists(p):
162
+ try:
163
+ print(f" ⬇️ Fetching {local_f} from Hugging Face {args.model_repo}...")
164
+ dl = hf_hub_download(repo_id=args.model_repo, filename=local_f, token=HF_TOKEN)
165
+ p = dl
166
+ except Exception as e:
167
+ print(f" ⚠️ Could not fetch {local_f}: {e}")
168
+ continue
169
+
170
+ with open(p, "r", encoding="utf-8") as fp:
171
+ items = json.load(fp)
172
+ cnt = 0
173
+ for it in items:
174
+ inst, resp = clean_text_pair(it.get("instruction", ""), it.get("response", ""))
175
+ if inst and resp:
176
+ all_samples.append({"instruction": inst, "response": resp, "domain_id": dom_id})
177
+ cnt += 1
178
+ print(f" ✅ Ingested {cnt:,} clean samples from {local_f}")
179
+
180
+ # --------------------------------------------------------------------------
181
+ # Step 2: Ingest Raw 21-Domain Datasets (Science, History, Chat, etc.)
182
+ # --------------------------------------------------------------------------
183
+ print("\n📦 [2/4] Ingesting 21 Core Domains (Science, History, Chat, Workplace)...")
184
+ sft_v1_files = [
185
+ ("d1_ncert_curriculum.jsonl", 2),
186
+ ("d2_indian_history_affairs.jsonl", 3),
187
+ ("d3_everyday_chat_empathy.jsonl", 5),
188
+ ("d4_multiturn_dialogue.jsonl", 4),
189
+ ("d5_instruction_following.jsonl", 6),
190
+ ("d6_typo_robustness_slangs.jsonl", 5),
191
+ ("d7_summarization_tldr.jsonl", 8),
192
+ ("d8_corporate_workplace_writing.jsonl", 8),
193
+ ("d9_cooking_kitchen_science.jsonl", 10),
194
+ ("d10_antihallucination_trick_qa.jsonl", 11),
195
+ ("d11_extraction_table_formatting.jsonl", 8),
196
+ ("d12_comparisons_pros_cons.jsonl", 7),
197
+ ("d13_health_wellness_remedies.jsonl", 2),
198
+ ("d14_career_guidance_interview.jsonl", 8),
199
+ ("d15_tech_troubleshooting.jsonl", 9),
200
+ ("d16_indian_festivals_heritage.jsonl", 3),
201
+ ("d17_finance_budgeting_scam_safety.jsonl", 11),
202
+ ("d18_mental_wellness_exam_habits.jsonl", 5),
203
+ ("d19_creative_writing_scripts.jsonl", 8),
204
+ ("d20_commonsense_practical_logic.jsonl", 7)
205
+ ]
206
+
207
+ for fname, dom_id in sft_v1_files:
208
+ local_p = os.path.join(BASE_DIR, "data", "sft_v1_raw", fname)
209
+ if not os.path.exists(local_p):
210
+ try:
211
+ print(f" ⬇️ Fetching {fname} from {args.raw_repo}...")
212
+ dl = hf_hub_download(repo_id=args.raw_repo, filename=f"sft_v1_raw/{fname}", repo_type="dataset", token=HF_TOKEN)
213
+ local_p = dl
214
+ except Exception as e:
215
+ print(f" ⚠️ Notice: Could not fetch {fname}: {e}")
216
+ continue
217
+
218
+ cnt = 0
219
+ with open(local_p, "r", encoding="utf-8") as fp:
220
+ for line in fp:
221
+ try:
222
+ data = json.loads(line)
223
+ inst, resp = clean_text_pair(data.get("instruction", ""), data.get("response", ""))
224
+ if inst and resp:
225
+ all_samples.append({"instruction": inst, "response": resp, "domain_id": dom_id})
226
+ cnt += 1
227
+ except Exception:
228
+ continue
229
+ print(f" • {fname:38s}: Ingested {cnt:6,d} clean samples")
230
+
231
+ print(f"📊 Current Ingested Total: {len(all_samples):,} samples")
232
+
233
+ # --------------------------------------------------------------------------
234
+ # Step 3: Stream High-Quality Open Datasets up to Target Samples
235
+ # --------------------------------------------------------------------------
236
+ remaining_needed = max(0, args.target_samples - len(all_samples))
237
+ if remaining_needed > 0:
238
+ print(f"\n🌐 [3/4] Streaming {remaining_needed:,} additional high-grade conversational samples from Hugging Face...")
239
+
240
+ streams = [
241
+ ("HuggingFaceH4/ultrachat_200k", "train_sft", 4, int(remaining_needed * 0.40)),
242
+ ("teknium/OpenHermes-2.5", None, 6, int(remaining_needed * 0.35)),
243
+ ("HuggingFaceTB/cosmopedia", "web_samples", 2, int(remaining_needed * 0.25))
244
+ ]
245
+
246
+ for ds_name, split, dom_id, count_target in streams:
247
+ if count_target <= 0:
248
+ continue
249
+ print(f" 🌊 Streaming from {ds_name} (Target: ~{count_target:,} samples)...")
250
+ stream_count = 0
251
+ try:
252
+ ds = load_dataset(ds_name, split=split or "train", streaming=True, token=HF_TOKEN)
253
+ for item in ds:
254
+ inst, resp = "", ""
255
+ if "messages" in item:
256
+ msgs = item["messages"]
257
+ if len(msgs) >= 2 and msgs[0]["role"] == "user" and msgs[1]["role"] == "assistant":
258
+ inst = msgs[0]["content"]
259
+ resp = msgs[1]["content"]
260
+ elif "conversations" in item:
261
+ convs = item["conversations"]
262
+ if len(convs) >= 2:
263
+ inst = convs[0].get("value", "")
264
+ resp = convs[1].get("value", "")
265
+ elif "prompt" in item and "text" in item:
266
+ inst = item["prompt"]
267
+ resp = item["text"]
268
+ elif "instruction" in item and "output" in item:
269
+ inst = item["instruction"]
270
+ resp = item["output"]
271
+
272
+ c_inst, c_resp = clean_text_pair(inst, resp)
273
+ if c_inst and c_resp:
274
+ all_samples.append({"instruction": c_inst, "response": c_resp, "domain_id": dom_id})
275
+ stream_count += 1
276
+ if stream_count >= count_target:
277
+ break
278
+ if stream_count % 25000 == 0:
279
+ print(f" ... Streamed {stream_count:,} / {count_target:,} samples")
280
+ except Exception as e:
281
+ print(f" ⚠️ Notice while streaming {ds_name}: {e}. Continuing with available data.")
282
+ print(f" ✅ Finished {ds_name}: Collected {stream_count:,} samples")
283
+
284
+ total_count = len(all_samples)
285
+ print(f"\n🎉 Total Validated Dataset Size: {total_count:,} samples across {len(DOMAIN_MAP)} domains!")
286
+
287
+ # --------------------------------------------------------------------------
288
+ # Step 4: Tokenize & Pack Memory-Mapped Binary Shards
289
+ # --------------------------------------------------------------------------
290
+ print("\n⚡ [4/4] Shuffling & Packing into NumPy Memory-Mapped Binary Shards...")
291
+ random.seed(42)
292
+ random.shuffle(all_samples)
293
+
294
+ val_count = max(1000, int(total_count * args.val_ratio))
295
+ train_count = total_count - val_count
296
+
297
+ print(f" • Train Split: {train_count:,} samples")
298
+ print(f" • Val Split: {val_count:,} samples")
299
+
300
+ train_tokens, train_labels, train_offsets, train_domains = [], [], [0], []
301
+ val_tokens, val_labels, val_offsets, val_domains = [], [], [0], []
302
+
303
+ t0 = time.time()
304
+ for idx, item in enumerate(all_samples):
305
+ inst = item["instruction"]
306
+ resp = item["response"]
307
+ did = item["domain_id"]
308
+
309
+ prompt_str = f"{USER_TOKEN}\n{inst}{EOT_TOKEN}\n{ASSISTANT_TOKEN}\n"
310
+ resp_str = f"{resp}{EOT_TOKEN}"
311
+
312
+ p_ids = tokenizer.encode(prompt_str).ids
313
+ r_ids = tokenizer.encode(resp_str).ids
314
+
315
+ if len(r_ids) < 4:
316
+ continue
317
+
318
+ sample_tokens = p_ids + r_ids
319
+ sample_labels = [-100] * len(p_ids) + r_ids
320
+
321
+ if len(sample_tokens) > args.max_seq_len:
322
+ sample_tokens = sample_tokens[:args.max_seq_len]
323
+ sample_labels = sample_labels[:args.max_seq_len]
324
+ sample_tokens[-1] = EOT_ID
325
+ sample_labels[-1] = EOT_ID
326
+
327
+ is_val = (idx < val_count)
328
+ if is_val:
329
+ val_tokens.extend(sample_tokens)
330
+ val_labels.extend(sample_labels)
331
+ val_offsets.append(len(val_tokens))
332
+ val_domains.append(did)
333
+ else:
334
+ train_tokens.extend(sample_tokens)
335
+ train_labels.extend(sample_labels)
336
+ train_offsets.append(len(train_tokens))
337
+ train_domains.append(did)
338
+
339
+ if (idx + 1) % 50000 == 0 or (idx + 1) == total_count:
340
+ rate = (idx + 1) / max(0.001, time.time() - t0)
341
+ print(f" • Tokenized {idx + 1:,} / {total_count:,} samples ({rate:.0f} samples/sec)...")
342
+
343
+ # Save to disk
344
+ print("\n💾 Writing binary shards to disk...")
345
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "train_tokens.npy"), np.array(train_tokens, dtype=np.uint16))
346
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "train_labels.npy"), np.array(train_labels, dtype=np.int32))
347
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "train_offsets.npy"), np.array(train_offsets, dtype=np.int64))
348
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "train_domains.npy"), np.array(train_domains, dtype=np.int16))
349
+
350
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "val_tokens.npy"), np.array(val_tokens, dtype=np.uint16))
351
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "val_labels.npy"), np.array(val_labels, dtype=np.int32))
352
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "val_offsets.npy"), np.array(val_offsets, dtype=np.int64))
353
+ np.save(os.path.join(OUTPUT_TOKENIZED_DIR, "val_domains.npy"), np.array(val_domains, dtype=np.int16))
354
+
355
+ metadata = {
356
+ "dataset_name": "500M_Sarus_SFT",
357
+ "total_samples": total_count,
358
+ "train_samples": len(train_offsets) - 1,
359
+ "val_samples": len(val_offsets) - 1,
360
+ "total_train_tokens": len(train_tokens),
361
+ "total_val_tokens": len(val_tokens),
362
+ "domain_names": DOMAIN_MAP,
363
+ "format": "Direct Response (<|user|> / <|assistant|>, Zero <think>)",
364
+ "created_at": time.strftime("%Y-%m-%d %H:%M:%S")
365
+ }
366
+ with open(os.path.join(OUTPUT_TOKENIZED_DIR, "metadata.json"), "w", encoding="utf-8") as fp:
367
+ json.dump(metadata, fp, indent=2)
368
+
369
+ print(f"✅ Tokenized files saved in {OUTPUT_TOKENIZED_DIR}!")
370
+ print(f" • Total Train Tokens: {len(train_tokens):,}")
371
+ print(f" • Total Val Tokens: {len(val_tokens):,}")
372
+
373
+ # --------------------------------------------------------------------------
374
+ # Step 5: Auto-upload to Hugging Face Hub
375
+ # --------------------------------------------------------------------------
376
+ if args.push_to_hf:
377
+ print(f"\n🚀 Uploading dataset to Hugging Face Hub ({args.repo_id})...")
378
+ api = HfApi(token=HF_TOKEN)
379
+ try:
380
+ api.create_repo(repo_id=args.repo_id, repo_type="dataset", exist_ok=True, token=HF_TOKEN)
381
+ api.upload_folder(
382
+ folder_path=OUTPUT_TOKENIZED_DIR,
383
+ repo_id=args.repo_id,
384
+ repo_type="dataset",
385
+ token=HF_TOKEN
386
+ )
387
+ print(f"🎉 SUCCESS! Dataset pushed to https://huggingface.co/datasets/{args.repo_id}")
388
+ except Exception as e:
389
+ print(f"⚠️ Hugging Face upload error: {e}")
390
+
391
+ if __name__ == "__main__":
392
+ main()