""" Data preparation for clankerDiffusion. One streaming pass over FineWeb-edu (+ a Wikipedia slice for world knowledge, + synthetic tool-use and RAG/retrieval examples that teach the special tags): 1. first N docs -> train the byte-level BPE tokenizer (from scratch) 2. remainder -> tokenize and pack into a flat uint16 .bin until token budget Outputs (under --out-dir, default ./data): tokenizer.json meta.json train.bin (flat uint16 tokens) meta.json ({n_tokens, seq_len, vocab_size}) Designed so EVERY training environment (local / Modal / Kaggle / TPU) can regenerate the corpus itself with fast HF egress -- no 2 GB file transfer needed. Use --scale to grow the corpus (e.g. --scale 4 for a multi-billion-token run). """ import os, json, random, argparse import numpy as np from datasets import load_dataset import tokenizer as tokmod from tokenizer import YKTokenizer, SPECIAL from build_rag import FACTS, SYSTEM as RAG_SYSTEM random.seed(1234) np.random.seed(1234) OUT = os.path.dirname(os.path.abspath(__file__)) DATADIR = os.path.join(OUT, "data") os.makedirs(DATADIR, exist_ok=True) SEQ_LEN = 1024 TOK_TRAIN_DOCS = 80_000 TOK_BUDGET_FINEWEB = 900_000_000 # scaled by --scale TOK_BUDGET_WIKI = 300_000_000 # scaled by --scale TOK_BUDGET_TOOL = 250_000_000 # scaled by --scale TOK_BUDGET_RAG = 150_000_000 # scaled by --scale # -------------------------------------------------------------------------- # 1) Tokenizer training # -------------------------------------------------------------------------- def train_tokenizer(out_dir=DATADIR): print("[prep] streaming FineWeb-edu to collect tokenizer training docs ...") ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT", streaming=True, split="train") texts = [] for i, ex in enumerate(ds): texts.append(ex["text"]) if i + 1 >= TOK_TRAIN_DOCS: break print(f"[prep] collected {len(texts)} docs for tokenizer") tok = YKTokenizer().train( iter(texts), vocab_size=32768, save_path=os.path.join(out_dir, "tokenizer.json")) print(f"[prep] tokenizer trained: vocab={tok.vocab_size}") return tok # -------------------------------------------------------------------------- # 2) Synthetic tool-use conversations (tag format == agent.py) # -------------------------------------------------------------------------- CALC_TEMPLATES = [ "What is {a} {op} {b}?", "Compute {a} {op} {b} for me.", "Calculate the result of {a} {op} {b}.", "If I start at {a} and apply {op} {b}, what do I get?", ] OPS = {"+": "plus", "-": "minus", "*": "times", "/": "divided by"} PY_SNIPPETS = [ "print(sum(range(1, {n}+1)))", "import math\nprint(round(math.sqrt({n}), 4))", "print(sorted([{a}, {b}, {c}]))", "print({n} ** 2 + {n})", "s='clanker'; print(s[::-1])", ] FILE_Q = [ "Read the file {path} and tell me what is on the first line.", "What is inside {path}?", "List the files in {dir}.", ] SYSTEM = ("You are clanker, a helpful assistant that can THINK, USE TOOLS, and " "USE MEMORY. You may reason in ... at any point, " "interleaved with actions. Wrap tool calls in " "arguments. Available tools: calc(expr), " "python(code), read_file(path), list_dir(path), retrieve(query). After " "a tool result appears in ..., continue and give the " "final answer. If ... is provided, use it. You keep " "facts in a secondary memory: KEYVALUE to " "store, KEY to recall (result returns inside " "...), and KEY to forget.") def gen_synthetic(n=60000): out = [] for _ in range(n): kind = random.random() if kind < 0.45: a = random.randint(2, 999); b = random.randint(2, 999) op = random.choice(["+", "-", "*", "/"]) b = max(2, b if op != "/" else random.randint(2, 50)) if op == "/": a = a * b ans = eval(f"{a}{op}{b}") q = random.choice(CALC_TEMPLATES).format(a=a, b=b, op=OPS[op]) tool = f'{a} {op} {b}' result = str(ans) think = f"The user wants {a} {OPS[op]} {b}. I'll use the calculator." elif kind < 0.8: n_ = random.randint(3, 200); a = random.randint(1, 50); b = random.randint(1, 50); c = random.randint(1, 50) code = random.choice(PY_SNIPPETS).format(n=n_, a=a, b=b, c=c) q = f"Run this tiny Python snippet and report the output:\n{code}" tool = f'{code}' try: import io, contextlib buf = io.StringIO() with contextlib.redirect_stdout(buf): exec(code, {"__builtins__": __builtins__}, {}) result = buf.getvalue().strip() except Exception as e: result = f"error: {e}" think = "I can execute this with the python tool." else: path = random.choice(["notes.txt", "data/log.csv", "README.md", "config.json"]) q = random.choice(FILE_Q).format(path=path, dir=random.choice(["src", "data", "."])) if "List" in q or "list" in q: tool = f'{path}' result = f"{path}/\n file_a.txt\n file_b.csv" else: tool = f'{path}' result = f"line 1: hello from {path}" think = "I should read the file with the read_file tool." conv = (f"{SYSTEM}" f"{q}" f"{think}{tool}{result}" f"Based on the tool result, the answer is {result}.") out.append(conv) return out # -------------------------------------------------------------------------- # 2b) Interleaved-thinking + learned-memory examples. # Teaches the model to reason step-by-step *between* tool calls and to # persist/recall facts via // against the # side store (see memstore.py). Thinking is INTERLEAVED: think, act, # think, answer -- not one big block at the start. # -------------------------------------------------------------------------- MEM_FACTS = [ ("project:clanker", "clanker is a hybrid AR/diffusion LM with learned memory."), ("user:name", "The user's name is Ada."), ("user:likes", "The user likes concise answers and tool use."), ("fact:pi", "pi is approximately 3.14159."), ("fact:capitals", "The capital of France is Paris; of Japan is Tokyo."), ("pref:format", "Prefer reasoning before tool calls."), ] MEM_KEYS = [k for k, _ in MEM_FACTS] def gen_memory(n=30000): out = [] for _ in range(n): key, val = random.choice(MEM_FACTS) mode = random.random() if mode < 0.4: # write then read back (persistence demo) conv = (f"{SYSTEM}" f"Remember that {val}" f"I should store this in secondary memory " f"so I can recall it later." f"{key}{val}" f"Stored. Now I can read it back to confirm." f"{key}{val}" f"Got it -- I'll remember {val}") elif mode < 0.75: # read an existing fact, interleaved with reasoning conv = (f"{SYSTEM}" f"What do you know about {key}?" f"Let me pull this from secondary memory." f"{key}{val}" f"That matches what I stored. " f"Based on memory: {val}") else: # evict conv = (f"{SYSTEM}" f"Forget {key}." f"I'll remove it from secondary memory." f"{key}Done, I forgot {key}.") out.append(conv) return out # Multi-step reasoning with INTERLEAVED think/act/think/answer. # Each entry: (question, calc_expr, final_answer) REASON_QA = [ ("A train travels 60 km/h for 2 hours, then 90 km/h for 1 hour. Total distance?", "60*2 + 90*1", "210 km"), ("If I buy 3 items at $4.50 each and a $2 tax, total cost?", "3*4.50 + 2", "$15.50"), ("A rectangle is 8 by 5. Area and perimeter?", "8*5", "area 40, perimeter 26"), ("Compound 5% on $1000 for 2 years?", "1000*1.05**2", "$1102.50"), ("Mix 2L at 10C with 3L at 40C, final temp?", "(2*10+3*40)/5", "28C"), ] def gen_interleaved(n=30000): out = [] for _ in range(n): q, expr, ans = random.choice(REASON_QA) try: res = str(eval(expr)) except Exception: res = "?" conv = (f"{SYSTEM}" f"{q}" f"Break it into parts." f"{expr}" f"{res}" f"That gives the first part; combine with the rest. " f"The answer is {ans}.") out.append(conv) return out # -------------------------------------------------------------------------- # 3) RAG / retrieval examples (teach and ) # -------------------------------------------------------------------------- def gen_rag(n=40000): out = [] for _ in range(n): topic, doc, q, a = random.choice(FACTS) mode = random.random() if mode < 0.5: conv = (f"{RAG_SYSTEM}{q}" f"{q}" f"{doc}{a}") elif mode < 0.85: conv = (f"{RAG_SYSTEM}{q}" f"Let me check the provided context." f"{doc}{a}") else: conv = (f"{RAG_SYSTEM}{q}" f"{doc}{a}") out.append(conv) return out # -------------------------------------------------------------------------- # 4) Glaive function-calling (best-effort) # -------------------------------------------------------------------------- def gen_glaive(max_examples=20000): out = [] try: ds = load_dataset("glaiveai/glaive-function-calling-v2", streaming=True, split="train") except Exception as e: print(f"[prep] Glaive unavailable ({e}); skipping.") return out for i, ex in enumerate(ds): if i >= max_examples: break try: conv = ex["conversations"] parts = [""] for m in conv: role = m.get("role") or m.get("from") val = m.get("value") or m.get("content") or "" if role in ("system", "system_prompt"): parts.append(f"{val}") elif role in ("human", "user"): parts.append(f"{val}") elif role in ("gpt", "assistant", "function"): val = val.replace("{\"name\":", "{val}") elif role == "tool": parts.append(f"{val}") parts.append("") out.append("".join(parts)) except Exception: continue print(f"[prep] Glaive converted: {len(out)} examples") return out # -------------------------------------------------------------------------- # 5) Packing # -------------------------------------------------------------------------- def pack(tok, texts, bin_path, budget, seq_len): n = 0 buf = [] with open(bin_path, "ab") as f: for text in texts: ids = tok.encode(text) if not ids: continue buf.extend(ids) while len(buf) >= seq_len: chunk = np.array(buf[:seq_len], dtype=np.uint16) f.write(chunk.tobytes()) buf = buf[seq_len:] n += seq_len if n >= budget: return n if buf: chunk = np.array(buf[:seq_len], dtype=np.uint16) if len(chunk) == seq_len: with open(bin_path, "ab") as f: f.write(chunk.tobytes()) n += seq_len return n def main(): ap = argparse.ArgumentParser() ap.add_argument("--out-dir", default=DATADIR) ap.add_argument("--scale", type=float, default=1.0, help="multiply token budgets (e.g. 4 -> ~4x more data)") ap.add_argument("--no-wiki", action="store_true") ap.add_argument("--no-rag", action="store_true") ap.add_argument("--no-glaive", action="store_true") ap.add_argument("--no-mem", action="store_true") args = ap.parse_args() out_dir = args.out_dir os.makedirs(out_dir, exist_ok=True) scale = args.scale bw_fw = int(TOK_BUDGET_FINEWEB * scale) bw_wiki = int(TOK_BUDGET_WIKI * scale) bw_tool = int(TOK_BUDGET_TOOL * scale) bw_rag = int(TOK_BUDGET_RAG * scale) tok_path = os.path.join(out_dir, "tokenizer.json") if os.path.exists(tok_path): print("[prep] loading existing tokenizer") tok = YKTokenizer.load(tok_path) else: # try to reuse the canonical tokenizer from HF (keeps all runs compatible) try: print("[prep] no local tokenizer; downloading canonical one from HF ...") from huggingface_hub import hf_hub_download tok_path = hf_hub_download( repo_id="coderofpears/clankerDiffusion-base", filename="data/tokenizer.json", repo_type="model", local_dir=out_dir, token=os.environ.get("HF_TOKEN")) tok = YKTokenizer.load(tok_path) except Exception as e: print(f"[prep] HF tokenizer download failed ({e}); training a new one.") tok = train_tokenizer(out_dir) bin_path = os.path.join(out_dir, "train.bin") if os.path.exists(bin_path): os.remove(bin_path) # --- fineweb-edu --- print(f"[prep] FineWeb-edu (budget {bw_fw:,}) ...") ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT", streaming=True, split="train") gen = iter(ds) for _ in range(TOK_TRAIN_DOCS): next(gen) def fineweb_iter(): for ex in gen: yield ex["text"] n = pack(tok, fineweb_iter(), bin_path, bw_fw, SEQ_LEN) print(f"[prep] fineweb packed: {n:,} tokens") # --- wikipedia (world knowledge) --- if not args.no_wiki: print(f"[prep] Wikipedia (budget {bw_wiki:,}) ...") try: wds = load_dataset("wikipedia", "20220301.en", streaming=True, split="train") def wiki_iter(): for ex in wds: yield ex["text"] nw = pack(tok, wiki_iter(), bin_path, bw_wiki, SEQ_LEN) print(f"[prep] wikipedia packed: {nw:,} tokens") n += nw except Exception as e: print(f"[prep] wikipedia skipped: {e}") # --- synthetic tool data (incl. interleaved reasoning) --- synth = gen_synthetic(int(60_000 * scale) + 60000) inter = gen_interleaved(int(30_000 * scale) + 30000) n2 = pack(tok, synth + inter, bin_path, bw_tool, SEQ_LEN) print(f"[prep] synthetic tool packed: {n2:,} tokens") # --- learned-memory data --- if not args.no_mem: mem = gen_memory(int(30_000 * scale) + 30000) nm = pack(tok, mem, bin_path, int(bw_tool * 0.6), SEQ_LEN) print(f"[prep] memory packed: {nm:,} tokens") n2 += nm # --- RAG / retrieval data --- if not args.no_rag: rag = gen_rag(int(40_000 * scale) + 40000) nr = pack(tok, rag, bin_path, bw_rag, SEQ_LEN) print(f"[prep] RAG packed: {nr:,} tokens") n2 += nr # --- glaive --- if not args.no_glaive: gl = gen_glaive(20000) n3 = pack(tok, gl, bin_path, bw_tool, SEQ_LEN) if gl else 0 else: n3 = 0 total = n + n2 + n3 meta = {"n_tokens": int(total), "seq_len": SEQ_LEN, "vocab_size": tok.vocab_size, "path": "train.bin", "scale": scale} with open(os.path.join(out_dir, "meta.json"), "w") as f: json.dump(meta, f) print(f"[prep] DONE. total tokens={total:,} vocab={tok.vocab_size}") print(f"[prep] files in {out_dir}") if __name__ == "__main__": main()