Download scripts/prepare_data.py from CodeSoft/MetaDiffusion-150M-ChatBase: direct link, hf CLI and curl.
- Browser
- Download file 16.5 kB
-
https://huggingface.co/CodeSoft/MetaDiffusion-150M-ChatBase/resolve/main/scripts/prepare_data.py
- Command line
-
hf download hf://CodeSoft/MetaDiffusion-150M-ChatBase/scripts/prepare_data.py
-
curl -L -o prepare_data.py https://huggingface.co/CodeSoft/MetaDiffusion-150M-ChatBase/resolve/main/scripts/prepare_data.py
16.5 kB
| #!/usr/bin/env python3 | |
| """ | |
| prepare_data.py: Download HuggingFaceH4/no_robots and pre-tokenize it as ChatML | |
| for MetaDiffusion chat SFT. | |
| Output (in --data-dir): | |
| train.pt list of {"input_ids": LongTensor, "assistant_start": int, | |
| "assistant_end": int} | |
| val.pt same, held-out | |
| tokenizer/ Supra tokenizer with ChatML + rainbow tokens added | |
| stats.json counts and length stats | |
| The assistant region (content + trailing <|im_end|>) is the only part that | |
| will be masked during training; everything before it is visible prompt. | |
| Usage: | |
| python3 prepare_data.py --model-path ../hf_release --data-dir data/no_robots_chatml | |
| """ | |
| import argparse | |
| import hashlib | |
| import json | |
| import random | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from datasets import load_dataset | |
| from transformers import AutoTokenizer | |
| IM_START = "<|im_start|>" | |
| IM_END = "<|im_end|>" | |
| RESERVED = "<|reserved|>" # filler so id 32000 stays free for [MASK] | |
| CHAT_TOKENS = [IM_START, IM_END] + [f"<|r{i}|>" for i in range(1, 8)] # rainbow pads | |
| BASE_VOCAB = 32000 # Supra tokenizer entry count (ids 0..31999) | |
| def add_chat_tokens(tokenizer): | |
| """Add chat + rainbow tokens at ids 32001..32009. | |
| The base tokenizer has 32000 entries, so the first added token would take | |
| id 32000, which is the diffusion [MASK] id. A reserved filler token takes | |
| that slot first; mask id 32000 must never exist in the tokenizer. | |
| """ | |
| if len(tokenizer) == BASE_VOCAB: | |
| tokenizer.add_special_tokens({"additional_special_tokens": [RESERVED]}) | |
| n = tokenizer.add_special_tokens( | |
| {"additional_special_tokens": CHAT_TOKENS} | |
| ) | |
| im_start = tokenizer.convert_tokens_to_ids(IM_START) | |
| im_end = tokenizer.convert_tokens_to_ids(IM_END) | |
| assert im_start == 32001, f"im_start id {im_start} != 32001 (collides with [MASK])" | |
| assert im_end == 32002, f"im_end id {im_end} != 32002" | |
| print(f"[*] Added {n} special tokens, vocab now {len(tokenizer)}") | |
| print(f"[*] im_start={im_start} im_end={im_end} " | |
| f"rainbow={[tokenizer.convert_tokens_to_ids(f'<|r{i}|>') for i in range(1, 8)]}") | |
| return n | |
| def format_segment(role: str, content: str) -> str: | |
| return f"{IM_START}{role}\n{content}{IM_END}" | |
| def load_messages(dataset_name: str): | |
| """Return a list of message lists for one dataset name.""" | |
| if dataset_name == "no_robots": | |
| ds = load_dataset("HuggingFaceH4/no_robots", split="train") | |
| return [list(row["messages"]) for row in ds] | |
| if dataset_name == "alpaca": | |
| ds = load_dataset("yahma/alpaca-cleaned", split="train") | |
| out = [] | |
| for row in ds: | |
| user = row["instruction"] | |
| if row.get("input"): | |
| user += f"\n\n{row['input']}" | |
| out.append([{"role": "user", "content": user}, | |
| {"role": "assistant", "content": row["output"]}]) | |
| return out | |
| if dataset_name == "dolly": | |
| ds = load_dataset("databricks/databricks-dolly-15k", split="train") | |
| out = [] | |
| for row in ds: | |
| user = row["instruction"] | |
| if row.get("context"): | |
| user += f"\n\n{row['context']}" | |
| out.append([{"role": "user", "content": user}, | |
| {"role": "assistant", "content": row["response"]}]) | |
| return out | |
| if dataset_name == "smol-smoltalk": | |
| # The actual SFT set for SmolLM2-135M-Instruct: 484K short, high-quality | |
| # conversations designed for <1B models (no function calling, no | |
| # advanced math). Cap with --max-examples (e.g. 60000) for a 150M model. | |
| ds = load_dataset("HuggingFaceTB/smol-smoltalk", split="train") | |
| return [list(row["messages"]) for row in ds] | |
| if dataset_name == "math": | |
| return load_math_data() | |
| raise ValueError(f"Unknown dataset: {dataset_name} " | |
| f"(choose from: no_robots, alpaca, dolly, smol-smoltalk, math)") | |
| MATH_QUESTION_TEMPLATES = { | |
| "add": ["What is {a} + {b}?", "What is {a} plus {b}?", "Add {a} and {b}.", | |
| "What does {a} + {b} equal?"], | |
| "sub": ["What is {a} - {b}?", "What is {a} minus {b}?", "Subtract {b} from {a}.", | |
| "What does {a} - {b} equal?"], | |
| "mul": ["What is {a} × {b}?", "What is {a} times {b}?", "Multiply {a} by {b}.", | |
| "What does {a} × {b} equal?"], | |
| "div": ["What is {a} ÷ {b}?", "What is {a} divided by {b}?", "Divide {a} by {b}.", | |
| "What does {a} ÷ {b} equal?"], | |
| } | |
| MATH_ANSWER_TEMPLATES = ["The answer is {r}.", "It is {r}.", "{r}"] | |
| def load_math_data(seed: int = 42): | |
| """Exhaustive basic-arithmetic QA pairs (add/sub/mul/div), ChatML messages. | |
| A 150M model learns arithmetic by memorization, so cover EVERY pair in a | |
| small range rather than sampling: add a<=b in 1..99, sub b<a in 1..99, | |
| times tables 1..12, exact divisions 1..12. | |
| """ | |
| rng = random.Random(seed) | |
| pairs = [] # (op, a, b, result) | |
| for a in range(1, 100): | |
| for b in range(a, 100): | |
| pairs.append(("add", a, b, a + b)) | |
| for a in range(2, 100): | |
| for b in range(1, a): | |
| pairs.append(("sub", a, b, a - b)) | |
| for a in range(1, 13): | |
| for b in range(1, 13): | |
| pairs.append(("mul", a, b, a * b)) | |
| for b in range(1, 13): | |
| for q in range(1, 13): | |
| pairs.append(("div", b * q, b, q)) | |
| rng.shuffle(pairs) | |
| out = [] | |
| for op, a, b, r in pairs: | |
| question = rng.choice(MATH_QUESTION_TEMPLATES[op]).format(a=a, b=b) | |
| answer = rng.choice(MATH_ANSWER_TEMPLATES).format(r=r) | |
| out.append([{"role": "user", "content": question}, | |
| {"role": "assistant", "content": answer}]) | |
| print(f" (synthetic arithmetic: {len(out)} pairs, " | |
| f"add {sum(1 for p in pairs if p[0]=='add')}, " | |
| f"sub {sum(1 for p in pairs if p[0]=='sub')}, " | |
| f"mul {sum(1 for p in pairs if p[0]=='mul')}, " | |
| f"div {sum(1 for p in pairs if p[0]=='div')})") | |
| return out | |
| def example_key(input_ids_tensor): | |
| """Compact dedup key: SHA-256 of token ids. | |
| Tuple keys for 484K x 600-token conversations cost ~5 GB of set memory; | |
| digests cost ~50 MB. (Hashing was never the bottleneck; per-segment | |
| tokenizer.encode() calls were.) | |
| """ | |
| return hashlib.sha256( | |
| input_ids_tensor.numpy().astype(np.uint32).tobytes() | |
| ).digest() | |
| def build_conv_segments(messages): | |
| """Split a conversation into ChatML segment strings + roles.""" | |
| segs, roles = [], [] | |
| for m in messages: | |
| role = m.get("role", "user") | |
| content = m.get("content", "") | |
| if not content.strip(): | |
| continue | |
| segs.append(format_segment(role, content)) | |
| roles.append(role) | |
| return segs, roles | |
| def reconstruct_example(seg_ids, roles, tokenizer, max_len, max_resp): | |
| """Rebuild a conversation from pre-encoded segments (tokenize_example | |
| logic, but with tokenization already done in batch).""" | |
| ids = [] | |
| assistant_ranges = [] | |
| for role, seg in zip(roles, seg_ids): | |
| start = len(ids) | |
| ids.extend(seg) | |
| if role == "assistant": | |
| assistant_ranges.append((start, len(ids))) | |
| if not ids or not assistant_ranges: | |
| return None | |
| a0, a1 = assistant_ranges[-1] | |
| if a1 <= a0: | |
| return None | |
| # Cap the response length, always keeping the trailing <|im_end|> | |
| if a1 - a0 > max_resp: | |
| im_end_id = tokenizer.convert_tokens_to_ids(IM_END) | |
| ids = ids[:a0] + ids[a0:a0 + max_resp - 1] + [im_end_id] | |
| a1 = a0 + max_resp | |
| # Truncate from the front (history), keep the target response intact | |
| resp = ids[a0:a1] | |
| if len(resp) > max_len: | |
| resp = resp[:max_len] | |
| a1 = a0 + len(resp) | |
| hist = ids[:a0] | |
| room = max_len - len(resp) | |
| if len(hist) > room: | |
| hist = hist[len(hist) - room:] if room > 0 else [] | |
| ids = hist + resp | |
| return { | |
| "input_ids": torch.tensor(ids, dtype=torch.long), | |
| "assistant_start": len(hist), | |
| "assistant_end": len(ids), | |
| } | |
| def tokenize_and_split(convs, math_indices, tokenizer, val_size, max_len, | |
| max_resp, seed=42): | |
| """Batched tokenize + reconstruct + dedup + val split. | |
| convs: list of (roles, segs) from build_conv_segments. | |
| math_indices: conversation indices exempt from dedup (math repeats are | |
| intentional). Returns (train_examples, val_examples, skipped, dupes). | |
| """ | |
| all_seg_strs = [s for _, segs in convs for s in segs] | |
| print(f"[*] Encoding {len(all_seg_strs)} segments (batched)...") | |
| encoded = [] | |
| CHUNK = 200_000 | |
| for i in range(0, len(all_seg_strs), CHUNK): | |
| chunk = all_seg_strs[i:i + CHUNK] | |
| encoded.extend(tokenizer(chunk, add_special_tokens=False)["input_ids"]) | |
| rng = random.Random(seed) | |
| idxs = list(range(len(convs))) | |
| rng.shuffle(idxs) | |
| val_idxs = set(idxs[:val_size]) | |
| train_examples, val_examples = [], [] | |
| skipped = dupes = 0 | |
| seen = set() | |
| ptr = 0 | |
| for i, (roles, segs) in enumerate(convs): | |
| seg_ids = encoded[ptr:ptr + len(segs)] | |
| ptr += len(segs) | |
| ex = reconstruct_example(seg_ids, roles, tokenizer, max_len, max_resp) | |
| if ex is None: | |
| skipped += 1 | |
| continue | |
| if i not in math_indices: | |
| key = example_key(ex["input_ids"]) | |
| if key in seen: | |
| dupes += 1 | |
| continue | |
| seen.add(key) | |
| (val_examples if i in val_idxs else train_examples).append(ex) | |
| return train_examples, val_examples, skipped, dupes | |
| def save_dataset(out_dir, train_examples, val_examples, tokenizer, | |
| skipped, dupes): | |
| """Save train.pt / val.pt / stats.json and print the summary.""" | |
| out = Path(out_dir) | |
| torch.save({"examples": train_examples}, out / "train.pt") | |
| torch.save({"examples": val_examples}, out / "val.pt") | |
| lens = [e["input_ids"].numel() for e in train_examples] | |
| stats = { | |
| "train": len(train_examples), | |
| "val": len(val_examples), | |
| "skipped": skipped, | |
| "dupes": dupes, | |
| "avg_tokens": sum(lens) / len(lens) if lens else 0, | |
| "max_tokens": max(lens) if lens else 0, | |
| "vocab_size": len(tokenizer), | |
| "im_start_id": tokenizer.convert_tokens_to_ids(IM_START), | |
| "im_end_id": tokenizer.convert_tokens_to_ids(IM_END), | |
| "rainbow_ids": [tokenizer.convert_tokens_to_ids(f"<|r{i}|>") for i in range(1, 8)], | |
| "mask_token_id": 32000, | |
| } | |
| with open(out / "stats.json", "w") as f: | |
| json.dump(stats, f, indent=2) | |
| print(f"[*] Done: {stats['train']} train / {stats['val']} val " | |
| f"(skipped {skipped}, dupes {dupes})") | |
| print(f"[*] Avg tokens per example: {stats['avg_tokens']:.0f} (max {stats['max_tokens']})") | |
| print(f"[*] im_start={stats['im_start_id']} im_end={stats['im_end_id']} " | |
| f"rainbow={stats['rainbow_ids']}") | |
| return stats | |
| def tokenize_example(tokenizer, messages, max_len: int, max_resp: int = 256): | |
| """Tokenize a conversation segment-wise; return ids + assistant bounds. | |
| The target (last assistant response) is capped at `max_resp` tokens | |
| including its trailing <|im_end|>: long web-text targets teach rambling, | |
| and the terminator must stay in the target so the model learns to emit it. | |
| """ | |
| ids = [] | |
| assistant_ranges = [] # (start, end) per message, in token space | |
| for m in messages: | |
| role = m.get("role", "user") | |
| content = m.get("content", "") | |
| if not content.strip(): | |
| continue | |
| seg = format_segment(role, content) | |
| seg_ids = tokenizer.encode(seg, add_special_tokens=False) | |
| start = len(ids) | |
| ids.extend(seg_ids) | |
| if role == "assistant": | |
| assistant_ranges.append((start, len(ids))) | |
| if not ids or not assistant_ranges: | |
| return None | |
| # Target turn = the LAST assistant response (LLaDA-MoE style) | |
| a0, a1 = assistant_ranges[-1] | |
| if a1 <= a0: | |
| return None | |
| # Cap the response length, always keeping the trailing <|im_end|> | |
| if a1 - a0 > max_resp: | |
| im_end_id = tokenizer.convert_tokens_to_ids(IM_END) | |
| ids = ids[:a0] + ids[a0:a0 + max_resp - 1] + [im_end_id] | |
| a1 = a0 + max_resp | |
| # Truncate from the front (history), keep the target response intact | |
| resp = ids[a0:a1] | |
| if len(resp) > max_len: | |
| resp = resp[:max_len] | |
| a1 = a0 + len(resp) | |
| hist = ids[:a0] | |
| room = max_len - len(resp) | |
| if len(hist) > room: | |
| hist = hist[len(hist) - room:] if room > 0 else [] | |
| ids = hist + resp | |
| return { | |
| "input_ids": torch.tensor(ids, dtype=torch.long), | |
| "assistant_start": len(hist), | |
| "assistant_end": len(ids), | |
| } | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Prepare no_robots as ChatML for MetaDiffusion SFT") | |
| parser.add_argument("--model-path", default="../hf_release", help="Dir with tokenizer.json (base model)") | |
| parser.add_argument("--data-dir", default="data/no_robots_chatml", help="Output dir") | |
| parser.add_argument("--datasets", default="no_robots", | |
| help="Comma list: no_robots, alpaca, dolly, smol-smoltalk, " | |
| "math (e.g. no_robots,alpaca,dolly,smol-smoltalk,math)") | |
| parser.add_argument("--max-examples", type=int, default=0, | |
| help="Cap per downloaded dataset (0 = no cap). smol-smoltalk " | |
| "is 484K; use ~60000 for a 150M model. Does not apply " | |
| "to synthetic math (exhaustive coverage is the point).") | |
| parser.add_argument("--val-size", type=int, default=500, help="Held-out examples") | |
| parser.add_argument("--max-len", type=int, default=1024, help="Max tokens per example") | |
| parser.add_argument("--max-resp-tokens", type=int, default=256, | |
| help="Cap on target response tokens (keeps <|im_end|>)") | |
| parser.add_argument("--math-repeat", type=int, default=1, | |
| help="Upsample synthetic math: N passes over all pairs " | |
| "(different phrasings per pass; 3-4 helps a 150M " | |
| "model memorize the mapping)") | |
| parser.add_argument("--seed", type=int, default=42) | |
| args = parser.parse_args() | |
| out = Path(args.data_dir) | |
| out.mkdir(parents=True, exist_ok=True) | |
| print(f"[*] Loading tokenizer from {args.model_path}") | |
| tokenizer = AutoTokenizer.from_pretrained(args.model_path) | |
| add_chat_tokens(tokenizer) | |
| tokenizer.save_pretrained(out / "tokenizer") | |
| convs = [] # (roles, segs) per conversation | |
| math_indices = set() | |
| for name in args.datasets.split(","): | |
| name = name.strip() | |
| print(f"[*] Loading dataset: {name}") | |
| if name == "math": | |
| # Upsample: several passes over the same pairs, each pass re-rolling | |
| # phrasings (fresh seed). Math repeats are NOT deduped: identical | |
| # copies are intentional extra gradient steps (memorization needs | |
| # repetition, not variety). | |
| start = len(convs) | |
| for rep in range(args.math_repeat): | |
| msgs = load_math_data(seed=args.seed + rep) | |
| for messages in msgs: | |
| segs, roles = build_conv_segments(messages) | |
| convs.append((roles, segs)) | |
| print(f" pass {rep + 1}/{args.math_repeat}: {len(msgs)}") | |
| math_indices.update(range(start, len(convs))) | |
| else: | |
| msgs = load_messages(name) | |
| if args.max_examples > 0 and len(msgs) > args.max_examples: | |
| rng_cap = random.Random(args.seed) | |
| rng_cap.shuffle(msgs) | |
| msgs = msgs[: args.max_examples] | |
| print(f" capped to {len(msgs)}") | |
| for messages in msgs: | |
| segs, roles = build_conv_segments(messages) | |
| convs.append((roles, segs)) | |
| print(f" {len(msgs)} conversations") | |
| print(f"[*] Total: {len(convs)} conversations") | |
| train_examples, val_examples, skipped, dupes = tokenize_and_split( | |
| convs, math_indices, tokenizer, args.val_size, args.max_len, | |
| args.max_resp_tokens, args.seed) | |
| save_dataset(out, train_examples, val_examples, tokenizer, skipped, dupes) | |
| if __name__ == "__main__": | |
| main() | |