coderofpears commited on
Commit
df43f42
·
verified ·
1 Parent(s): 0708af3

Upload folder using huggingface_hub

Browse files
Files changed (14) hide show
  1. agent.py +99 -0
  2. build_rag.py +114 -0
  3. colab_train.py +215 -0
  4. data/tokenizer.json +0 -0
  5. infer.py +135 -0
  6. model.py +181 -0
  7. prep.py +250 -0
  8. push_hf.py +83 -0
  9. rag.py +163 -0
  10. rag_finetune.py +129 -0
  11. tokenizer.py +105 -0
  12. tools.py +127 -0
  13. train.py +145 -0
  14. upload_artifacts.py +58 -0
agent.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — tool-using agent (ReAct loop) with mid-response RAG.
3
+
4
+ The model emits <tool name="...">args</tool> blocks. We parse + execute
5
+ them locally (tools.py) and feed the <result> back, looping until the
6
+ model stops calling tools. Each turn alternates AR <-> diffusion, so the
7
+ agent is literally "sometimes normal, sometimes diffusion".
8
+
9
+ Knowledge injection happens in TWO ways, both mid-response:
10
+ * model-driven : the model calls <tool name="retrieve">q</tool>; we run the
11
+ retriever and feed <result> back, then keep generating.
12
+ * controller-driven : the RAG controller watches the partial response, builds
13
+ a query from the user question + what was just said, and if it finds
14
+ relevant passages it injects <context>...</context> straight into the
15
+ stream and the model continues -- no need for the model to ask.
16
+ """
17
+ import argparse
18
+ import infer
19
+ from tools import execute_tool, parse_tool_calls
20
+ from rag import default_kb
21
+
22
+
23
+ SYSTEM = ("You are clanker, a helpful assistant that can THINK, USE TOOLS, and "
24
+ "READ CONTEXT. When you need to compute, inspect files, or look up "
25
+ "knowledge, emit a tool call like "
26
+ "<tool name=\"calc\">17 * 23</tool>, "
27
+ "<tool name=\"python\">print(2**10)</tool>, "
28
+ "<tool name=\"read_file\">notes.txt</tool>, "
29
+ "<tool name=\"list_dir\">.</tool>, or "
30
+ "<tool name=\"retrieve\">the capital of France</tool>. "
31
+ "Available tools: calc(expr), python(code), read_file(path), "
32
+ "list_dir(path), retrieve(query). If <context>...</context> is "
33
+ "provided, use it to answer. After a <result>...</result> appears, "
34
+ "continue and give the final answer. You may reason in <think>...</think>.")
35
+
36
+
37
+ def run_agent(prompt, max_turns=8, mode_cycle=("diff", "ar"),
38
+ max_new=200, gen_len=160, steps=24,
39
+ rag=True, rag_k=3, rag_budget=2):
40
+ model, tok = infer._MODEL, infer._TOK
41
+ kb = default_kb()
42
+ injected = set()
43
+
44
+ ctx = [tok.bos_id] + tok.encode(
45
+ f"<system>{SYSTEM}</system><user>{prompt}</user><assistant>")
46
+
47
+ print(f"\n=== USER ===\n{prompt}\n")
48
+ for turn in range(max_turns):
49
+ mode = mode_cycle[turn % len(mode_cycle)]
50
+ prev_len = len(ctx)
51
+ print(f"--- turn {turn} [{mode}] ---", flush=True)
52
+ if mode == "ar":
53
+ ids = infer.generate_ar(model, tok, ctx, max_new=max_new)
54
+ else:
55
+ ids = infer.generate_diff(model, tok, ctx, gen_len=gen_len, steps=steps)
56
+ new_ids = ids[prev_len:] # ONLY the newly generated tokens
57
+ ctx = ids # full updated context (no dup)
58
+ text_new = tok.decode(new_ids)
59
+ print(text_new, flush=True)
60
+
61
+ # 1) model-driven tool calls (incl. retrieve) -> mid-response injection
62
+ calls = parse_tool_calls(text_new)
63
+ if calls:
64
+ for name, arg in calls:
65
+ print(f" [tool] {name}({arg[:120]})", flush=True)
66
+ res = execute_tool(name, arg)
67
+ print(f" [result] {res[:300]}", flush=True)
68
+ ctx = ctx + tok.encode(f"<result>{res}</result>")
69
+ continue # keep going within the same answer
70
+
71
+ # 2) controller-driven RAG: inject context mid-response, then continue
72
+ if rag and len(injected) < rag_budget * rag_k:
73
+ q = prompt + " " + text_new[-300:]
74
+ fresh = [p for p in kb.retrieve(q, k=rag_k) if p not in injected]
75
+ if fresh:
76
+ injected.update(fresh)
77
+ block = "<context>\n" + "\n---\n".join(fresh) + "\n</context>"
78
+ ctx = ctx + tok.encode(block)
79
+ print(f" [rag] injected {len(fresh)} passage(s) mid-response",
80
+ flush=True)
81
+ continue
82
+
83
+ break # nothing to do -> answer is final
84
+
85
+ final = tok.decode(ctx)
86
+ i = final.rfind("<assistant>")
87
+ print("\n=== clanker (final) ===\n" + (final[i + 10:] if i >= 0 else final))
88
+ return final
89
+
90
+
91
+ if __name__ == "__main__":
92
+ ap = argparse.ArgumentParser()
93
+ ap.add_argument("prompt", nargs="?", default="What is 17 * 23, and what files are in the current folder?")
94
+ ap.add_argument("--ckpt", default=None)
95
+ ap.add_argument("--turns", type=int, default=8)
96
+ ap.add_argument("--no-rag", action="store_true")
97
+ a = ap.parse_args()
98
+ infer.init(ckpt_path=a.ckpt)
99
+ run_agent(a.prompt, max_turns=a.turns, rag=not a.no_rag)
build_rag.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — build the RAG knowledge base + RAG training data.
3
+
4
+ Run AFTER the tokenizer exists (data/tokenizer.json):
5
+ python build_rag.py
6
+
7
+ * writes a small default knowledge base into data/kb/ and saves its index
8
+ * writes data/rag_corpus.txt : many formatted examples teaching the model
9
+ - to call <tool name="retrieve">q</tool> and use the <result>, and
10
+ - to read <context>...</context> injected mid-response and answer from it.
11
+ This corpus is consumed by rag_finetune.py (continuation training).
12
+ """
13
+ import os
14
+ import json
15
+ import random
16
+
17
+ from rag import KnowledgeBase, DEFAULT_INDEX, KB_DIR
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ DATADIR = os.path.join(HERE, "data")
21
+ CORPUS = os.path.join(DATADIR, "rag_corpus.txt")
22
+
23
+ # A small factual knowledge base. (In practice, point build_rag.py at your own
24
+ # docs via kb.ingest_path(...); this seed makes `retrieve` work out of the box.)
25
+ FACTS = [
26
+ ("geography", "France is a country in Western Europe. Its capital and largest city is Paris. The currency is the euro.",
27
+ "What is the capital of France?", "The capital of France is Paris."),
28
+ ("geography", "Germany's capital is Berlin. The official language is German and the currency is the euro.",
29
+ "What is the capital of Germany?", "The capital of Germany is Berlin."),
30
+ ("geography", "Japan is an island nation in East Asia. Its capital is Tokyo and its highest mountain is Mount Fuji.",
31
+ "What is the capital of Japan?", "The capital of Japan is Tokyo."),
32
+ ("science", "Water is a chemical compound with the formula H2O. It boils at 100 degrees Celsius at standard pressure and freezes at 0 degrees Celsius.",
33
+ "At what temperature does water boil?", "Water boils at 100 degrees Celsius at standard pressure."),
34
+ ("science", "The speed of light in a vacuum is approximately 299,792 kilometers per second.",
35
+ "What is the speed of light?", "The speed of light in a vacuum is about 299,792 km/s."),
36
+ ("science", "The chemical symbol for gold is Au, for silver is Ag, and for iron is Fe.",
37
+ "What is the chemical symbol for gold?", "The chemical symbol for gold is Au."),
38
+ ("history", "World War II lasted from 1939 to 1945. It involved most of the world's nations and ended with the defeat of the Axis powers.",
39
+ "When did World War II end?", "World War II ended in 1945."),
40
+ ("history", "The Declaration of Independence of the United States was adopted on July 4, 1776.",
41
+ "When was the US Declaration of Independence adopted?", "It was adopted on July 4, 1776."),
42
+ ("tech", "Python is a high-level, interpreted programming language. It uses indentation to define blocks and is widely used for data science and AI.",
43
+ "What kind of language is Python?", "Python is a high-level, interpreted programming language."),
44
+ ("tech", "The Transformer is a neural network architecture introduced in 2017 that relies on self-attention instead of recurrence.",
45
+ "What is a Transformer in machine learning?", "A Transformer is a neural architecture from 2017 based on self-attention."),
46
+ ("math", "The value of pi is approximately 3.14159. It is the ratio of a circle's circumference to its diameter.",
47
+ "What is the value of pi?", "Pi is approximately 3.14159."),
48
+ ("math", "Euler's identity is e^(i*pi) + 1 = 0, linking the numbers e, i, pi, 1, and 0.",
49
+ "What is Euler's identity?", "Euler's identity is e^(i*pi) + 1 = 0."),
50
+ ("space", "The Sun is the star at the center of the Solar System. Earth orbits it at an average distance of about 149.6 million kilometers.",
51
+ "What is the Sun?", "The Sun is the star at the center of the Solar System."),
52
+ ("space", "Mars is the fourth planet from the Sun and is often called the Red Planet because of its iron-oxide surface.",
53
+ "Why is Mars called the Red Planet?", "Mars is called the Red Planet due to its iron-oxide (rusty) surface."),
54
+ ("biology", "DNA stands for deoxyribonucleic acid. It carries the genetic instructions used in the growth and functioning of all known living organisms.",
55
+ "What does DNA stand for?", "DNA stands for deoxyribonucleic acid."),
56
+ ("biology", "Photosynthesis is the process by which plants convert light energy, water, and carbon dioxide into glucose and oxygen.",
57
+ "What is photosynthesis?", "Photosynthesis is how plants turn light, water, and CO2 into glucose and oxygen."),
58
+ ("economics", "Inflation is the rate at which the general level of prices for goods and services rises, eroding purchasing power.",
59
+ "What is inflation?", "Inflation is the rise in the general price level, reducing purchasing power."),
60
+ ("economics", "Gross Domestic Product (GDP) is the total monetary value of all finished goods and services produced within a country in a period.",
61
+ "What is GDP?", "GDP is the total value of finished goods and services produced in a country."),
62
+ ("language", "The word 'clanker' in this project is the name of the assistant. It is a from-scratch hybrid diffusion language model.",
63
+ "What is clanker?", "clanker is the name of this assistant, a from-scratch hybrid diffusion language model."),
64
+ ("project", "clankerDiffusion alternates between AR mode (normal next-token) and DIFF mode (masked diffusion) using a learned mode embedding.",
65
+ "What are the two modes of clankerDiffusion?", "AR mode (autoregressive) and DIFF mode (masked diffusion)."),
66
+ ]
67
+
68
+ SYSTEM = ("You are clanker, a helpful assistant that can use tools and read context. "
69
+ "When you need knowledge, call <tool name=\"retrieve\">query</tool>. "
70
+ "If <context>...</context> is provided, answer using it.")
71
+
72
+
73
+ def build_kb():
74
+ os.makedirs(KB_DIR, exist_ok=True)
75
+ # a readable KB file so list_dir/retrieve demo works
76
+ kb_text = "\n\n".join(f"[{topic}]\n{doc}" for topic, doc, _, _ in FACTS)
77
+ with open(os.path.join(KB_DIR, "knowledge.txt"), "w", encoding="utf-8") as f:
78
+ f.write(kb_text)
79
+ kb = KnowledgeBase()
80
+ kb.ingest_path(KB_DIR)
81
+ kb.save(DEFAULT_INDEX)
82
+ return kb
83
+
84
+
85
+ def build_corpus(n_each=400, seed=0):
86
+ random.seed(seed)
87
+ lines = []
88
+ facts = FACTS
89
+ for _ in range(n_each):
90
+ topic, doc, q, a = random.choice(facts)
91
+ # format A: model must RETRIEVE then answer
92
+ lines.append(
93
+ f"<system>{SYSTEM}</system><user>{q}</user>"
94
+ f"<assistant><tool name=\"retrieve\">{q}</tool>"
95
+ f"<result>{doc}</result>{a}</assistant>")
96
+ # format B: context already injected mid-response, model answers from it
97
+ lines.append(
98
+ f"<system>{SYSTEM}</system><user>{q}</user>"
99
+ f"<assistant><context>{doc}</context>{a}</assistant>")
100
+ # format C: a short chain-of-thought variant
101
+ lines.append(
102
+ f"<system>{SYSTEM}</system><user>{q}</user>"
103
+ f"<assistant><think>The relevant knowledge is: {doc}</think>"
104
+ f"{a}</assistant>")
105
+ random.shuffle(lines)
106
+ with open(CORPUS, "w", encoding="utf-8") as f:
107
+ for ln in lines:
108
+ f.write(ln + "\n")
109
+ print(f"[rag] wrote {len(lines)} training examples -> {CORPUS}")
110
+
111
+
112
+ if __name__ == "__main__":
113
+ build_kb()
114
+ build_corpus()
colab_train.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================================================
3
+ clankerDiffusion — Colab A100-80GB training script
4
+ ============================================================================
5
+
6
+ HOW TO RUN IN COLAB (A100 80GB):
7
+
8
+ [Cell 1 — setup, run once]
9
+ !pip install -q torch==2.11.0+cu124 -f https://download.pytorch.org/whl/cu124
10
+ !pip install -q transformers tokenizers datasets accelerate safetensors huggingface_hub numpy
11
+ import os
12
+ os.environ["HF_TOKEN"] = "hf_xxx" # your token (also set in Secrets)
13
+ os.environ["CODE_REPO"] = "clankerDiffusion/base" # from upload_artifacts.py
14
+ os.environ["CKPT_REPO"] = "clankerDiffusion/checkpoints"
15
+
16
+ [Cell 2 — launch in BACKGROUND, then disconnect safely]
17
+ !nohup python colab_train.py > colab_train.log 2>&1 &
18
+ # check later with: !tail -n 30 colab_train.log
19
+ # it checkpoints + uploads to HF every 250 steps until the runtime dies
20
+
21
+ The script trains the SAME from-scratch hybrid model (AR + masked
22
+ diffusion) on FineWeb-edu, resuming if a checkpoint exists, and pushes
23
+ a checkpoint to HuggingFace Hub every 250 steps in a background thread.
24
+ ============================================================================
25
+ """
26
+ import os, sys, json, time, threading, argparse
27
+ import numpy as np
28
+ import torch
29
+ import torch.nn as nn
30
+ import torch.nn.functional as F
31
+ from huggingface_hub import snapshot_download, HfApi
32
+
33
+ CODE_REPO = os.environ.get("CODE_REPO", "clankerDiffusion/base")
34
+ CKPT_REPO = os.environ.get("CKPT_REPO", "clankerDiffusion/checkpoints")
35
+ HF_TOKEN = os.environ.get("HF_TOKEN")
36
+ api = HfApi(token=HF_TOKEN)
37
+
38
+ # ---- pull our model code + tokenizer from HF -------------------------------
39
+ print(f"[colab] downloading code from {CODE_REPO} ...")
40
+ local = snapshot_download(CODE_REPO, repo_type="model")
41
+ sys.path.insert(0, local)
42
+ from model import YKDiff
43
+ from tokenizer import YKTokenizer
44
+
45
+ # ---- big architecture for A100 80GB -------------------------------------
46
+ CFG = dict(
47
+ d_model=2048, n_layers=24, n_heads=16, d_ff=5504,
48
+ max_len=2048, vocab_size=32768,
49
+ )
50
+
51
+ tok = YKTokenizer.load(os.path.join(local, "tokenizer.json"))
52
+ CFG["vocab_size"] = tok.vocab_size
53
+ V = CFG["vocab_size"]
54
+ mask_id, pad_id = tok.mask_id, tok.pad_id
55
+ print(f"[colab] vocab={V}")
56
+
57
+ # ---- streaming fineweb into a rolling token buffer -------------------------
58
+ from datasets import load_dataset
59
+ ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT",
60
+ streaming=True, split="train")
61
+ BUF_CAP = 60_000_000
62
+ buf = []
63
+ _buf_lock = threading.Lock()
64
+
65
+
66
+ def _refill():
67
+ for ex in ds:
68
+ ids = tok.encode(ex["text"])
69
+ with _buf_lock:
70
+ buf.extend(ids)
71
+ if len(buf) > BUF_CAP:
72
+ del buf[: len(buf) - BUF_CAP]
73
+
74
+
75
+ threading.Thread(target=_refill, daemon=True).start()
76
+
77
+
78
+ def sample_batch(batch, seq_len):
79
+ with _buf_lock:
80
+ if len(buf) < seq_len + 1:
81
+ return None
82
+ N = len(buf)
83
+ starts = np.random.randint(0, N - seq_len, size=batch)
84
+ return torch.tensor(
85
+ [buf[s:s + seq_len] for s in starts], dtype=torch.long)
86
+
87
+
88
+ # ---- model -----------------------------------------------------------------
89
+ model = YKDiff(CFG).cuda()
90
+ n_params = sum(p.numel() for p in model.parameters())
91
+ print(f"[colab] params = {n_params/1e9:.2f}B")
92
+ optim = torch.optim.AdamW(model.parameters(), lr=1e-4, betas=(0.9, 0.95),
93
+ weight_decay=0.1)
94
+
95
+ # ---- resume ----------------------------------------------------------------
96
+ CKPT_LOCAL = "/content/clanker_ckpts"
97
+ os.makedirs(CKPT_LOCAL, exist_ok=True)
98
+ step0 = 0
99
+ existing = sorted(f for f in os.listdir(CKPT_LOCAL) if f.endswith(".pt"))
100
+ if existing:
101
+ sd = torch.load(os.path.join(CKPT_LOCAL, existing[-1]), map_location="cuda")
102
+ model.load_state_dict(sd["model"]); optim.load_state_dict(sd["optim"])
103
+ step0 = sd["step"]
104
+ print(f"[colab] resumed step={step0}")
105
+ else:
106
+ # try pulling latest from HF
107
+ try:
108
+ api.create_repo(CKPT_REPO, repo_type="model", exist_ok=True)
109
+ except Exception:
110
+ pass
111
+
112
+
113
+ def _upload(path):
114
+ def _u():
115
+ try:
116
+ api.upload_file(repo_id=CKPT_REPO,
117
+ path_in_repo=os.path.basename(path),
118
+ path_or_fileobj=path, repo_type="model")
119
+ print(f"[colab] uploaded {os.path.basename(path)} -> {CKPT_REPO}",
120
+ flush=True)
121
+ except Exception as e:
122
+ print(f"[colab] upload failed: {e}", flush=True)
123
+ threading.Thread(target=_u, daemon=True).start()
124
+
125
+
126
+ # ---- training loop (hybrid) ----------------------------------------------
127
+ @torch.no_grad()
128
+ def _cosine_lr(step, warmup, total, base, minlr):
129
+ if step < warmup:
130
+ return base * step / warmup
131
+ p = (step - warmup) / max(total - warmup, 1)
132
+ return minlr + 0.5 * (base - minlr) * (1 + np.cos(np.pi * min(p, 1.0)))
133
+
134
+
135
+ BATCH, SEQ, GRAD_ACCUM = 16, 2048, 4
136
+ WARMUP, TOTAL_STEPS = 500, 200_000
137
+ BASE_LR, MIN_LR = 1e-4, 1e-5
138
+ CKPT_EVERY = 250
139
+ amp = torch.cuda.amp.autocast(dtype=torch.bfloat16)
140
+
141
+ step = step0
142
+ model.train()
143
+ t0 = time.time()
144
+ print("[colab] training started.", flush=True)
145
+
146
+ while True: # run as long as possible
147
+ try:
148
+ optim.zero_grad(set_to_none=True)
149
+ for micro in range(GRAD_ACCUM):
150
+ idx = None
151
+ while idx is None:
152
+ idx = sample_batch(BATCH, SEQ)
153
+ time.sleep(0.02)
154
+ idx = idx.cuda()
155
+ mode_ar = (torch.rand(1).item() < 0.5)
156
+ with amp:
157
+ if mode_ar:
158
+ m = torch.zeros(BATCH, dtype=torch.long, device="cuda")
159
+ logits = model(idx, m, t=None)
160
+ loss = F.cross_entropy(
161
+ logits[:, :-1].reshape(-1, V),
162
+ idx[:, 1:].reshape(-1), ignore_index=pad_id)
163
+ else:
164
+ m = torch.ones(BATCH, dtype=torch.long, device="cuda")
165
+ r = torch.rand(BATCH, device="cuda")
166
+ is_mask = torch.rand(BATCH, SEQ, device="cuda") < r[:, None]
167
+ not_pad = idx != pad_id
168
+ masked = idx.clone(); masked[is_mask] = mask_id
169
+ logits = model(masked, m, t=r)
170
+ ce = F.cross_entropy(logits.reshape(-1, V),
171
+ idx.reshape(-1), reduction="none",
172
+ ignore_index=-100)
173
+ ce = ce * is_mask.reshape(-1) * not_pad.reshape(-1)
174
+ denom = (is_mask & not_pad).reshape(-1).sum().clamp(min=1)
175
+ loss = ce.sum() / denom
176
+ (loss / GRAD_ACCUM).backward()
177
+
178
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
179
+ lr = _cosine_lr(step, WARMUP, TOTAL_STEPS, BASE_LR, MIN_LR)
180
+ for g in optim.param_groups:
181
+ g["lr"] = lr
182
+ optim.step()
183
+ step += 1
184
+
185
+ if step % 25 == 0:
186
+ print(f"[colab] step {step} loss~{loss.item():.3f} "
187
+ f"lr={lr:.2e} t={(time.time()-t0)/60:.1f}m", flush=True)
188
+
189
+ if step % CKPT_EVERY == 0:
190
+ # full local ckpt (for resume)
191
+ full = os.path.join(CKPT_LOCAL, f"clanker_{step:07d}.pt")
192
+ torch.save({"model": model.state_dict(),
193
+ "optim": optim.state_dict(),
194
+ "step": step, "cfg": CFG, "vocab": V}, full)
195
+ # light bf16 model-only for HF upload
196
+ lite = os.path.join(CKPT_LOCAL, f"clanker_{step:07d}_lite.pt")
197
+ torch.save({"model": {k: v.to(torch.bfloat16)
198
+ for k, v in model.state_dict().items()},
199
+ "cfg": CFG, "vocab": V, "step": step}, lite)
200
+ print(f"[colab] checkpoint {step}", flush=True)
201
+ _upload(lite)
202
+ # keep only last 2 local full ckpts to save disk
203
+ for old in sorted(f for f in os.listdir(CKPT_LOCAL)
204
+ if f.endswith(".pt") and "lite" not in f)[:-2]:
205
+ os.remove(os.path.join(CKPT_LOCAL, old))
206
+
207
+ except torch.cuda.OutOfMemoryError:
208
+ print("[colab] OOM — skipping step", flush=True)
209
+ optim.zero_grad(set_to_none=True)
210
+ torch.cuda.empty_cache()
211
+ except Exception as e:
212
+ print(f"[colab] step error (continuing): {e}", flush=True)
213
+ torch.cuda.empty_cache()
214
+
215
+ print("[colab] loop ended.")
data/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
infer.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — inference.
3
+
4
+ Three generation modes, all from the SAME weights:
5
+ ar normal autoregressive next-token decoding (causal)
6
+ diff masked discrete diffusion: start the answer fully masked,
7
+ iteratively unmask the most-confident tokens (LLaDA-style remasking)
8
+ hybrid a few AR steps to "think", then diffusion for the answer
9
+ random flips a coin each call -> "sometimes normal, sometimes diffusion"
10
+ """
11
+ import os, json, argparse, random
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from model import YKDiff
15
+ from tokenizer import YKTokenizer
16
+
17
+ HERE = os.path.dirname(os.path.abspath(__file__))
18
+ DATADIR = os.path.join(HERE, "data")
19
+ CKPTDIR = os.path.join(HERE, "checkpoints")
20
+
21
+
22
+ def load(tok_path=None, ckpt_path=None):
23
+ tok_path = tok_path or os.path.join(DATADIR, "tokenizer.json")
24
+ tok = YKTokenizer.load(tok_path)
25
+ if ckpt_path is None:
26
+ ck = sorted([f for f in os.listdir(CKPTDIR) if f.endswith(".pt")])
27
+ if not ck:
28
+ raise SystemExit("no checkpoint found in ./checkpoints")
29
+ ckpt_path = os.path.join(CKPTDIR, ck[-1])
30
+ sd = torch.load(ckpt_path, map_location="cuda")
31
+ cfg = sd["cfg"]
32
+ model = YKDiff(cfg).cuda().eval()
33
+ model.load_state_dict(sd["model"])
34
+ print(f"[infer] loaded {ckpt_path} params={sum(p.numel() for p in model.parameters())/1e6:.1f}M")
35
+ return model, tok
36
+
37
+
38
+ @torch.no_grad()
39
+ def _topp(logits, temp, top_p):
40
+ logits = logits / max(temp, 1e-6)
41
+ if top_p >= 1.0:
42
+ return torch.multinomial(F.softmax(logits, -1), 1).item()
43
+ s, order = torch.sort(logits, descending=True)
44
+ p = F.softmax(s, -1)
45
+ c = torch.cumsum(p, -1)
46
+ keep = order[c <= top_p]
47
+ if len(keep) == 0:
48
+ keep = order[:1]
49
+ return keep[torch.multinomial(F.softmax(logits[keep], -1), 1).item()].item()
50
+
51
+
52
+ @torch.no_grad()
53
+ def generate_ar(model, tok, prompt_ids, max_new=256, temp=0.9, top_p=0.95):
54
+ ids = list(prompt_ids)
55
+ max_len = model.max_len
56
+ for _ in range(max_new):
57
+ ctx = torch.tensor([ids[-max_len:]], device="cuda")
58
+ logits = model(ctx, torch.zeros(1, dtype=torch.long, device="cuda"))[:, -1]
59
+ nxt = _topp(logits[0], temp, top_p)
60
+ if nxt == tok.eos_id:
61
+ break
62
+ ids.append(nxt)
63
+ return ids
64
+
65
+
66
+ @torch.no_grad()
67
+ def generate_diff(model, tok, prompt_ids, gen_len=128, steps=24, temp=1.0, sample=True):
68
+ L = len(prompt_ids) + gen_len
69
+ seq = list(prompt_ids) + [tok.mask_id] * gen_len
70
+ p0 = len(prompt_ids)
71
+ for step in range(steps):
72
+ x = torch.tensor([seq], device="cuda")
73
+ t = torch.full((1,), (steps - step - 1) / steps, device="cuda")
74
+ logits = model(x, torch.ones(1, dtype=torch.long, device="cuda"), t=t)
75
+ gl = logits[0, p0:] # generated-region logits
76
+ probs = F.softmax(gl / max(temp, 1e-6), -1)
77
+ if sample:
78
+ preds = torch.multinomial(probs, 1).squeeze(-1).tolist()
79
+ else:
80
+ preds = probs.argmax(-1).tolist()
81
+ conf = probs.max(-1).values # [gen_len]
82
+ n_mask = int(round((steps - step - 1) / steps * gen_len))
83
+ order = conf.argsort().tolist() # ascending confidence
84
+ mask_set = set(order[:n_mask])
85
+ for j in range(gen_len):
86
+ seq[p0 + j] = tok.mask_id if j in mask_set else preds[j]
87
+ return seq
88
+
89
+
90
+ def build_prompt(tok, system, user):
91
+ return [tok.bos_id] + tok.encode(f"<system>{system}</system><user>{user}</user><assistant>")
92
+
93
+
94
+ def generate(prompt_ids, mode="random", **kw):
95
+ model, tok = _MODEL, _TOK
96
+ if mode == "random":
97
+ mode = "ar" if random.random() < 0.5 else "diff"
98
+ if mode == "ar":
99
+ ids = generate_ar(model, tok, prompt_ids, **kw)
100
+ elif mode == "diff":
101
+ ids = generate_diff(model, tok, prompt_ids, **kw)
102
+ elif mode == "hybrid":
103
+ # think a little in AR, then diffuse the answer
104
+ think = generate_ar(model, tok, prompt_ids, max_new=64, **kw)
105
+ # generate_diff returns the FULL sequence (think + answer)
106
+ ids = generate_diff(model, tok, think, **kw)
107
+ else:
108
+ raise ValueError(mode)
109
+ return tok.decode(ids)
110
+
111
+
112
+ # module-level cache so agent.py can call generate() directly
113
+ _MODEL, _TOK = None, None
114
+
115
+
116
+ def init(tok_path=None, ckpt_path=None):
117
+ global _MODEL, _TOK
118
+ _MODEL, _TOK = load(tok_path, ckpt_path)
119
+ return _MODEL, _TOK
120
+
121
+
122
+ if __name__ == "__main__":
123
+ ap = argparse.ArgumentParser()
124
+ ap.add_argument("prompt", nargs="?", default="What is 17 * 23?")
125
+ ap.add_argument("--mode", default="random")
126
+ ap.add_argument("--max-new", type=int, default=200)
127
+ ap.add_argument("--gen-len", type=int, default=160)
128
+ ap.add_argument("--steps", type=int, default=24)
129
+ ap.add_argument("--ckpt", default=None)
130
+ a = ap.parse_args()
131
+ m, t = init(ckpt_path=a.ckpt)
132
+ ids = build_prompt(t, "You are clanker, a helpful assistant.", a.prompt)
133
+ out = generate(ids, mode=a.mode, max_new=a.max_new,
134
+ gen_len=a.gen_len, steps=a.steps)
135
+ print("\n=== clanker ===\n" + out)
model.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ yk_diffusion: a from-scratch hybrid language model.
3
+
4
+ A single Transformer runs in two modes, selected by a learned mode embedding:
5
+ - AR mode (mode=0): causal attention -> standard next-token autoregressive LM ("normal")
6
+ - DIFF mode (mode=1): bidirectional attention -> masked discrete diffusion denoising
7
+
8
+ A time embedding conditions the diffusion mask ratio (MDLM / LLaDA-style absorbing-state training).
9
+ The same weights serve both behaviours; a <MODE> signal picks which one at inference time.
10
+ """
11
+
12
+ import math
13
+ import torch
14
+ import torch.nn as nn
15
+ import torch.nn.functional as F
16
+ from torch.utils.checkpoint import checkpoint
17
+
18
+
19
+ # ----------------------------------------------------------------------------
20
+ # Building blocks
21
+ # ----------------------------------------------------------------------------
22
+
23
+ class RMSNorm(nn.Module):
24
+ def __init__(self, d, eps=1e-6):
25
+ super().__init__()
26
+ self.eps = eps
27
+ self.weight = nn.Parameter(torch.ones(d))
28
+
29
+ def forward(self, x):
30
+ var = x.pow(2).mean(-1, keepdim=True)
31
+ x = x * torch.rsqrt(var + self.eps)
32
+ return x * self.weight
33
+
34
+
35
+ class RotaryEmbedding(nn.Module):
36
+ def __init__(self, head_dim, max_len=8192, base=10000.0):
37
+ super().__init__()
38
+ inv = 1.0 / (base ** (torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim))
39
+ self.register_buffer("inv_freq", inv)
40
+
41
+ def forward(self, seq_len, device):
42
+ t = torch.arange(seq_len, device=device, dtype=self.inv_freq.dtype)
43
+ freqs = torch.outer(t, self.inv_freq) # [L, head_dim/2]
44
+ emb = torch.cat([freqs, freqs], dim=-1) # [L, head_dim]
45
+ return emb.cos(), emb.sin()
46
+
47
+
48
+ def rotate_half(x):
49
+ x1, x2 = x.chunk(2, dim=-1)
50
+ return torch.cat((-x2, x1), dim=-1)
51
+
52
+
53
+ def apply_rope(x, cos, sin):
54
+ # x: [B, h, L, hd]; cos/sin: [1, 1, L, hd]
55
+ return x * cos + rotate_half(x) * sin
56
+
57
+
58
+ class Attention(nn.Module):
59
+ def __init__(self, d, n_heads):
60
+ super().__init__()
61
+ assert d % n_heads == 0
62
+ self.d = d
63
+ self.n_heads = n_heads
64
+ self.hd = d // n_heads
65
+ self.scale = self.hd ** -0.5
66
+ self.qkv = nn.Linear(d, 3 * d, bias=False)
67
+ self.proj = nn.Linear(d, d, bias=False)
68
+
69
+ def forward(self, x, cos, sin, attn_mask=None):
70
+ B, L, _ = x.shape
71
+ qkv = self.qkv(x).reshape(B, L, 3, self.n_heads, self.hd)
72
+ qkv = qkv.permute(2, 0, 3, 1, 4) # [3, B, h, L, hd]
73
+ q, k, v = qkv[0], qkv[1], qkv[2]
74
+ q = apply_rope(q, cos, sin)
75
+ k = apply_rope(k, cos, sin)
76
+ scores = (q @ k.transpose(-2, -1)) * self.scale
77
+ if attn_mask is not None:
78
+ scores = scores + attn_mask
79
+ attn = scores.softmax(dim=-1)
80
+ out = (attn @ v).transpose(1, 2).reshape(B, L, self.d)
81
+ return self.proj(out)
82
+
83
+
84
+ class MLP(nn.Module):
85
+ def __init__(self, d, d_ff):
86
+ super().__init__()
87
+ self.w1 = nn.Linear(d, d_ff, bias=False)
88
+ self.w3 = nn.Linear(d, d_ff, bias=False)
89
+ self.w2 = nn.Linear(d_ff, d, bias=False)
90
+
91
+ def forward(self, x):
92
+ return self.w2(F.silu(self.w1(x)) * self.w3(x))
93
+
94
+
95
+ class Block(nn.Module):
96
+ def __init__(self, d, n_heads, d_ff):
97
+ super().__init__()
98
+ self.ln1 = RMSNorm(d)
99
+ self.attn = Attention(d, n_heads)
100
+ self.ln2 = RMSNorm(d)
101
+ self.mlp = MLP(d, d_ff)
102
+
103
+ def forward(self, x, cos, sin, attn_mask):
104
+ x = x + self.attn(self.ln1(x), cos, sin, attn_mask)
105
+ x = x + self.mlp(self.ln2(x))
106
+ return x
107
+
108
+
109
+ # ----------------------------------------------------------------------------
110
+ # The model
111
+ # ----------------------------------------------------------------------------
112
+
113
+ class YKDiff(nn.Module):
114
+ def __init__(self, cfg):
115
+ super().__init__()
116
+ self.cfg = cfg
117
+ d = cfg["d_model"]
118
+ self.vocab = cfg["vocab_size"]
119
+ self.max_len = cfg["max_len"]
120
+
121
+ self.tok_emb = nn.Embedding(self.vocab, d)
122
+ self.mode_emb = nn.Embedding(2, d) # 0 = AR, 1 = DIFF
123
+ self.time_emb = nn.Linear(1, d, bias=False) # diffusion mask ratio conditioning
124
+ self.rope = RotaryEmbedding(d // cfg["n_heads"], max_len=self.max_len)
125
+
126
+ self.blocks = nn.ModuleList([
127
+ Block(d, cfg["n_heads"], cfg["d_ff"]) for _ in range(cfg["n_layers"])
128
+ ])
129
+ self.norm = RMSNorm(d)
130
+ self.lm_head = nn.Linear(d, self.vocab, bias=False)
131
+ with torch.no_grad():
132
+ self.lm_head.weight.copy_(self.tok_emb.weight) # weight tying
133
+
134
+ self._causal = None
135
+
136
+ @property
137
+ def device(self):
138
+ return next(self.parameters()).device
139
+
140
+ def _causal_mask(self, L, device):
141
+ if self._causal is None or self._causal.shape[-1] < L:
142
+ m = torch.full((L, L), float("-inf"), device=device)
143
+ m = torch.triu(m, diagonal=1)
144
+ self._causal = m
145
+ return self._causal[:L, :L]
146
+
147
+ def forward(self, idx, mode, t=None, attn_mask=None):
148
+ """
149
+ idx: [B, L] long token ids
150
+ mode: [B] long (0 AR, 1 DIFF)
151
+ t: [B] float|None diffusion mask ratio (None -> 0)
152
+ attn_mask: [L, L]|None explicit mask; if None, AR uses causal, DIFF uses none
153
+ """
154
+ B, L = idx.shape
155
+ x = self.tok_emb(idx)
156
+ x = x + self.mode_emb(mode).unsqueeze(1)
157
+ if t is None:
158
+ t = torch.zeros(B, device=idx.device)
159
+ x = x + self.time_emb(t.unsqueeze(-1)).unsqueeze(1)
160
+
161
+ cos, sin = self.rope(L, idx.device)
162
+ cos = cos.unsqueeze(0).unsqueeze(0) # [1,1,L,hd]
163
+ sin = sin.unsqueeze(0).unsqueeze(0)
164
+
165
+ if attn_mask is None:
166
+ # AR default causal; DIFF default bidirectional (None)
167
+ attn_mask = self._causal_mask(L, idx.device) if mode[0].item() == 0 else None
168
+ else:
169
+ attn_mask = attn_mask.to(idx.device)
170
+
171
+ # gradient checkpointing: trade ~20% compute for ~4x less
172
+ # activation memory, so a big batch fits on 16 GB.
173
+ training = self.training and torch.is_grad_enabled()
174
+ for blk in self.blocks:
175
+ if training:
176
+ x = checkpoint(blk, x, cos, sin, attn_mask,
177
+ use_reentrant=False)
178
+ else:
179
+ x = blk(x, cos, sin, attn_mask)
180
+ x = self.norm(x)
181
+ return self.lm_head(x) # [B, L, vocab]
prep.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Data preparation for yk_diffusion.
3
+
4
+ One streaming pass over FineWeb-edu:
5
+ 1. first N docs -> train the byte-level BPE tokenizer (from scratch)
6
+ 2. remainder -> tokenize and pack into a flat uint16 .bin until token budget
7
+ Then append:
8
+ - synthetic tool-use conversations (guarantees the agent's exact tag format)
9
+ - Glaive function-calling data (best-effort) if it loads
10
+
11
+ Outputs (under ./data):
12
+ tokenizer.json + .meta.json
13
+ train.bin (flat uint16 tokens)
14
+ meta.json ({n_tokens, seq_len, vocab_size})
15
+ """
16
+ import os, sys, json, random, argparse
17
+ import numpy as np
18
+ from datasets import load_dataset
19
+
20
+ import tokenizer as tokmod
21
+ from tokenizer import YKTokenizer, SPECIAL
22
+
23
+ random.seed(1234)
24
+ np.random.seed(1234)
25
+
26
+ OUT = os.path.dirname(os.path.abspath(__file__))
27
+ DATADIR = os.path.join(OUT, "data")
28
+ os.makedirs(DATADIR, exist_ok=True)
29
+
30
+ SEQ_LEN = 1024
31
+ TOK_BUDGET = 900_000_000 # fineweb portion target
32
+ TOK_BUDGET_TOOL = 200_000_000 # synthetic + glaive portion cap
33
+ TOK_TRAIN_DOCS = 60_000 # docs used to train the tokenizer
34
+
35
+
36
+ # --------------------------------------------------------------------------
37
+ # 1) Tokenizer training
38
+ # --------------------------------------------------------------------------
39
+ def train_tokenizer():
40
+ print("[prep] streaming FineWeb-edu to collect tokenizer training docs ...")
41
+ ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT",
42
+ streaming=True, split="train")
43
+ texts = []
44
+ for i, ex in enumerate(ds):
45
+ texts.append(ex["text"])
46
+ if i + 1 >= TOK_TRAIN_DOCS:
47
+ break
48
+ print(f"[prep] collected {len(texts)} docs for tokenizer")
49
+ tok = YKTokenizer().train(
50
+ iter(texts), vocab_size=32768,
51
+ save_path=os.path.join(DATADIR, "tokenizer.json"))
52
+ print(f"[prep] tokenizer trained: vocab={tok.vocab_size}")
53
+ return tok
54
+
55
+
56
+ # --------------------------------------------------------------------------
57
+ # 2) Synthetic tool-use conversations (tag format == agent.py)
58
+ # --------------------------------------------------------------------------
59
+ CALC_TEMPLATES = [
60
+ "What is {a} {op} {b}?", "Compute {a} {op} {b} for me.",
61
+ "Calculate the result of {a} {op} {b}.", "If I have {a} and add/subtract/multiply/divide by {b} what do I get?",
62
+ ]
63
+ OPS = {"+": "plus", "-": "minus", "*": "times", "/": "divided by"}
64
+ PY_SNIPPETS = [
65
+ "print(sum(range(1, {n}+1)))",
66
+ "import math\nprint(round(math.sqrt({n}), 4))",
67
+ "print(sorted([{a}, {b}, {c}]))",
68
+ "print({n} ** 2 + {n})",
69
+ ]
70
+ FILE_Q = [
71
+ "Read the file {path} and tell me what is on the first line.",
72
+ "What is inside {path}?",
73
+ "List the files in {dir}.",
74
+ ]
75
+ SYSTEM = ("You are clanker, a helpful assistant that can THINK and USE TOOLS. "
76
+ "When you need to compute or inspect something, wrap a tool call in "
77
+ "<tool name=\"...\">arguments</tool>. Available tools: calc(expr), "
78
+ "python(code), read_file(path), list_dir(path). After a tool result "
79
+ "appears in <result>...</result>, continue and give the final answer. "
80
+ "You may use <think>...</think> to reason first.")
81
+
82
+
83
+ def gen_synthetic(n=25000):
84
+ out = []
85
+ for _ in range(n):
86
+ kind = random.random()
87
+ if kind < 0.5:
88
+ a = random.randint(2, 999); b = random.randint(2, 999)
89
+ op = random.choice(["+", "-", "*", "/"]); b = max(2, b if op != "/" else random.randint(2, 50))
90
+ if op == "/":
91
+ a = a * b
92
+ ans = eval(f"{a}{op}{b}")
93
+ q = random.choice(CALC_TEMPLATES).format(a=a, b=b, op=OPS[op])
94
+ tool = f'<tool name="calc">{a} {op} {b}</tool>'
95
+ result = str(ans)
96
+ think = f"<think>The user wants {a} {OPS[op]} {b}. I'll use the calculator.</think>"
97
+ elif kind < 0.8:
98
+ n_ = random.randint(3, 200); a = random.randint(1, 50); b = random.randint(1, 50); c = random.randint(1, 50)
99
+ code = random.choice(PY_SNIPPETS).format(n=n_, a=a, b=b, c=c)
100
+ q = f"Run this tiny Python snippet and report the output:\n{code}"
101
+ tool = f'<tool name="python">{code}</tool>'
102
+ try:
103
+ import io, contextlib
104
+ buf = io.StringIO();
105
+ with contextlib.redirect_stdout(buf):
106
+ exec(code, {"__builtins__": __builtins__}, {})
107
+ result = buf.getvalue().strip()
108
+ except Exception as e:
109
+ result = f"error: {e}"
110
+ think = "<think>I can execute this with the python tool.</think>"
111
+ else:
112
+ path = random.choice(["notes.txt", "data/log.csv", "README.md", "config.json"])
113
+ q = random.choice(FILE_Q).format(path=path, dir=random.choice(["src", "data", "."]))
114
+ if "List" in q or "list" in q:
115
+ tool = f'<tool name="list_dir">{path}</tool>'
116
+ result = f"{path}/\n file_a.txt\n file_b.csv"
117
+ else:
118
+ tool = f'<tool name="read_file">{path}</tool>'
119
+ result = f"line 1: hello from {path}"
120
+ think = "<think>I should read the file with the read_file tool.</think>"
121
+
122
+ conv = (f"<bos><system>{SYSTEM}</system>"
123
+ f"<user>{q}</user>"
124
+ f"<assistant>{think}{tool}<result>{result}</result>"
125
+ f"Based on the tool result, the answer is {result}.</assistant><eos>")
126
+ out.append(conv)
127
+ return out
128
+
129
+
130
+ # --------------------------------------------------------------------------
131
+ # 3) Glaive function-calling (best-effort)
132
+ # --------------------------------------------------------------------------
133
+ def gen_glaive(max_examples=20000):
134
+ out = []
135
+ try:
136
+ ds = load_dataset("glaiveai/glaive-function-calling-v2",
137
+ streaming=True, split="train")
138
+ except Exception as e:
139
+ print(f"[prep] Glaive unavailable ({e}); skipping.")
140
+ return out
141
+ for i, ex in enumerate(ds):
142
+ if i >= max_examples:
143
+ break
144
+ try:
145
+ conv = ex["conversations"]
146
+ parts = ["<bos>"]
147
+ for m in conv:
148
+ role = m.get("role") or m.get("from")
149
+ val = m.get("value") or m.get("content") or ""
150
+ if role in ("system", "system_prompt"):
151
+ parts.append(f"<system>{val}</system>")
152
+ elif role in ("human", "user"):
153
+ parts.append(f"<user>{val}</user>")
154
+ elif role in ("gpt", "assistant", "function"):
155
+ # function_call style -> our <tool> tag
156
+ val = val.replace("{\"name\":", "<tool name=\"").replace("\"function_call\"", "")
157
+ parts.append(f"<assistant>{val}</assistant>")
158
+ elif role == "tool":
159
+ parts.append(f"<result>{val}</result>")
160
+ parts.append("<eos>")
161
+ out.append("".join(parts))
162
+ except Exception:
163
+ continue
164
+ print(f"[prep] Glaive converted: {len(out)} examples")
165
+ return out
166
+
167
+
168
+ # --------------------------------------------------------------------------
169
+ # 4) Packing
170
+ # --------------------------------------------------------------------------
171
+ def pack(tok, texts, bin_path, budget, seq_len):
172
+ """Append documents (packed into seq_len chunks) to bin_path until `budget`
173
+ tokens are written. Each call tracks its OWN counter so budgets are
174
+ independent across calls (fineweb vs synthetic vs glaive)."""
175
+ n = 0
176
+ buf = []
177
+ with open(bin_path, "ab") as f:
178
+ for text in texts:
179
+ ids = tok.encode(text)
180
+ if not ids:
181
+ continue
182
+ buf.extend(ids)
183
+ while len(buf) >= seq_len:
184
+ chunk = np.array(buf[:seq_len], dtype=np.uint16)
185
+ f.write(chunk.tobytes())
186
+ buf = buf[seq_len:]
187
+ n += seq_len
188
+ if n >= budget:
189
+ return n
190
+ if buf:
191
+ chunk = np.array(buf[:seq_len], dtype=np.uint16)
192
+ if len(chunk) == seq_len:
193
+ with open(bin_path, "ab") as f:
194
+ f.write(chunk.tobytes())
195
+ n += seq_len
196
+ return n
197
+
198
+
199
+ def main():
200
+ ap = argparse.ArgumentParser()
201
+ ap.add_argument("--seed-tok-only", action="store_true")
202
+ args = ap.parse_args()
203
+
204
+ tok_path = os.path.join(DATADIR, "tokenizer.json")
205
+ if os.path.exists(tok_path):
206
+ print("[prep] loading existing tokenizer")
207
+ tok = YKTokenizer.load(tok_path)
208
+ else:
209
+ tok = train_tokenizer()
210
+
211
+ bin_path = os.path.join(DATADIR, "train.bin")
212
+ if os.path.exists(bin_path):
213
+ os.remove(bin_path)
214
+
215
+ # --- fineweb-edu remainder (same stream, continue) ---
216
+ print("[prep] tokenizing FineWeb-edu (this is the bandwidth-heavy step) ...")
217
+ ds = load_dataset("HuggingFaceFW/fineweb-edu", "sample-10BT",
218
+ streaming=True, split="train")
219
+ gen = iter(ds)
220
+ for _ in range(TOK_TRAIN_DOCS): # advance past tokenizer docs
221
+ next(gen)
222
+ def fineweb_iter():
223
+ for ex in gen:
224
+ yield ex["text"]
225
+ n = pack(tok, fineweb_iter(), bin_path, TOK_BUDGET, SEQ_LEN)
226
+ print(f"[prep] fineweb packed: {n:,} tokens")
227
+
228
+ # --- synthetic tool data ---
229
+ print("[prep] generating synthetic tool-use conversations ...")
230
+ synth = gen_synthetic(60000)
231
+ n2 = pack(tok, synth, bin_path, TOK_BUDGET_TOOL, SEQ_LEN)
232
+ print(f"[prep] synthetic packed: {n2:,} tokens")
233
+
234
+ # --- glaive ---
235
+ gl = gen_glaive(20000)
236
+ n3 = 0
237
+ if gl:
238
+ n3 = pack(tok, gl, bin_path, TOK_BUDGET_TOOL, SEQ_LEN)
239
+
240
+ total = n + n2 + n3
241
+ meta = {"n_tokens": int(total), "seq_len": SEQ_LEN,
242
+ "vocab_size": tok.vocab_size, "path": "data/train.bin"}
243
+ with open(os.path.join(DATADIR, "meta.json"), "w") as f:
244
+ json.dump(meta, f)
245
+ print(f"[prep] DONE. total tokens={total:,} vocab={tok.vocab_size}")
246
+ print(f"[prep] files: {bin_path}")
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()
push_hf.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — push artifacts to HuggingFace (via the `hf` CLI, which
3
+ correctly creates the first commit on a brand-new repo).
4
+
5
+ python push_hf.py --what all # code + tokenizer + rag corpus + train.bin
6
+ python push_hf.py --what code # .py sources + tokenizer + rag corpus
7
+ python push_hf.py --what data # train.bin + tokenizer + rag corpus
8
+ python push_hf.py --what ckpt # latest checkpoints
9
+
10
+ Repos (under your HF user):
11
+ clankerDiffusion-base code + tokenizer + rag corpus
12
+ clankerDiffusion-data train.bin (packed training corpus)
13
+ clankerDiffusion-checkpoints model checkpoints (.pt)
14
+ """
15
+ import os
16
+ import subprocess
17
+ import argparse
18
+
19
+ HERE = os.path.dirname(os.path.abspath(__file__))
20
+ DATADIR = os.path.join(HERE, "data")
21
+ CKPTDIR = os.path.join(HERE, "checkpoints")
22
+
23
+ CODE_REPO = "coderofpears/clankerDiffusion-base"
24
+ DATA_REPO = "coderofpears/clankerDiffusion-data"
25
+ CKPT_REPO = "coderofpears/clankerDiffusion-checkpoints"
26
+
27
+ PY_SOURCES = ["model.py", "tokenizer.py", "prep.py", "train.py", "infer.py",
28
+ "tools.py", "agent.py", "rag.py", "build_rag.py", "rag_finetune.py",
29
+ "colab_train.py", "modal_train.py", "push_hf.py", "upload_artifacts.py"]
30
+
31
+
32
+ def _run(cmd):
33
+ print("+", " ".join(cmd), flush=True)
34
+ subprocess.run(cmd, check=True)
35
+
36
+
37
+ def _upload(repo, local, patterns):
38
+ # upload only the matching files via a temp staging dir
39
+ import tempfile, shutil, fnmatch
40
+ stage = tempfile.mkdtemp()
41
+ try:
42
+ for root, _, files in os.walk(local):
43
+ for f in files:
44
+ rel = os.path.relpath(os.path.join(root, f), local)
45
+ if any(fnmatch.fnmatch(rel, p) or fnmatch.fnmatch(f, p) for p in patterns):
46
+ dst = os.path.join(stage, rel)
47
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
48
+ shutil.copy(os.path.join(root, f), dst)
49
+ if not os.listdir(stage):
50
+ print(f"[hf] nothing matched for {repo}")
51
+ return
52
+ _run(["hf", "upload", repo, stage, "--repo-type", "model"])
53
+ print(f"[hf] -> {repo}")
54
+ finally:
55
+ shutil.rmtree(stage, ignore_errors=True)
56
+
57
+
58
+ def push_code():
59
+ _upload(CODE_REPO, HERE, PY_SOURCES + ["data/tokenizer.json", "data/rag_corpus.txt", "README.md"])
60
+
61
+
62
+ def push_data():
63
+ _upload(DATA_REPO, DATADIR, ["train.bin", "tokenizer.json", "rag_corpus.txt"])
64
+
65
+
66
+ def push_ckpt():
67
+ _upload(CKPT_REPO, CKPTDIR, ["*.pt"])
68
+
69
+
70
+ def main():
71
+ ap = argparse.ArgumentParser()
72
+ ap.add_argument("--what", default="all", choices=["all", "code", "data", "ckpt"])
73
+ a = ap.parse_args()
74
+ if a.what in ("all", "code"):
75
+ push_code()
76
+ if a.what in ("all", "data"):
77
+ push_data()
78
+ if a.what in ("all", "ckpt"):
79
+ push_ckpt()
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
rag.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — local RAG / knowledge-injection subsystem.
3
+
4
+ A small, dependency-free BM25 retriever so the agent can pull relevant
5
+ passages from a local knowledge base and inject them as <context> blocks.
6
+
7
+ Two ways knowledge gets injected:
8
+ 1. Model-driven : the model emits <tool name="retrieve">query</tool> and the
9
+ agent runs it, appending the <result> and continuing (ReAct).
10
+ 2. Controller-driven : the agent's RAG controller watches the response as it
11
+ is generated and, mid-turn, retrieves passages for the current question and
12
+ injects <context>...</context> right into the stream -- "knowledge injection
13
+ even in the middle of a response" -- without the model having to ask.
14
+
15
+ BM25 is used by default (no model downloads, runs anywhere). If
16
+ sentence-transformers is available it is used as an optional re-ranker.
17
+ """
18
+ import os
19
+ import re
20
+ import json
21
+ import math
22
+
23
+ HERE = os.path.dirname(os.path.abspath(__file__))
24
+ DATADIR = os.path.join(HERE, "data")
25
+ KB_DIR = os.path.join(DATADIR, "kb")
26
+ DEFAULT_INDEX = os.path.join(KB_DIR, "index.json")
27
+
28
+ _TOKEN_RE = re.compile(r"[a-z0-9]+")
29
+
30
+
31
+ def _tok(s):
32
+ return _TOKEN_RE.findall(s.lower())
33
+
34
+
35
+ def _chunk(text, size=180, stride=90):
36
+ """Split text into token-window chunks with overlap."""
37
+ toks = _tok(text)
38
+ if not toks:
39
+ return []
40
+ out = []
41
+ i = 0
42
+ while i < len(toks):
43
+ out.append(" ".join(toks[i:i + size]))
44
+ if i + size >= len(toks):
45
+ break
46
+ i += stride
47
+ return out
48
+
49
+
50
+ class KnowledgeBase:
51
+ def __init__(self):
52
+ self.docs = [] # list[str] chunk texts
53
+ self._df = {} # term -> doc freq
54
+ self._N = 0
55
+ self._avgdl = 1.0
56
+ self._built = False
57
+
58
+ # -- ingest -------------------------------------------------------------
59
+ def add_text(self, text, source=""):
60
+ for ch in _chunk(text):
61
+ if ch:
62
+ self.docs.append(ch)
63
+ self._built = False
64
+
65
+ def ingest_path(self, path):
66
+ """Ingest a file or a directory of files into the KB."""
67
+ if os.path.isdir(path):
68
+ files = []
69
+ for root, _, fs in os.walk(path):
70
+ for f in fs:
71
+ if f.lower().endswith((".txt", ".md", ".py", ".csv", ".json", ".log")):
72
+ files.append(os.path.join(root, f))
73
+ else:
74
+ files = [path]
75
+ for fp in files:
76
+ try:
77
+ with open(fp, "r", errors="replace") as fh:
78
+ self.add_text(fh.read(), source=fp)
79
+ except Exception as e:
80
+ print(f"[rag] skip {fp}: {e}")
81
+ print(f"[rag] ingested {len(files)} file(s) -> {len(self.docs)} chunks")
82
+
83
+ # -- index --------------------------------------------------------------
84
+ def _build(self):
85
+ self._df = {}
86
+ self._N = len(self.docs)
87
+ lengths = []
88
+ for d in self.docs:
89
+ seen = set()
90
+ for t in _tok(d):
91
+ seen.add(t)
92
+ lengths.append(len(_tok(d)))
93
+ for t in seen:
94
+ self._df[t] = self._df.get(t, 0) + 1
95
+ self._avgdl = (sum(lengths) / self._N) if self._N else 1.0
96
+ self._built = True
97
+
98
+ def retrieve(self, query, k=4):
99
+ if not self._built:
100
+ self._build()
101
+ if self._N == 0:
102
+ return []
103
+ q_toks = _tok(query)
104
+ if not q_toks:
105
+ return []
106
+ k1, b = 1.5, 0.75
107
+ scores = []
108
+ for d in self.docs:
109
+ dtoks = _tok(d)
110
+ dl = len(dtoks)
111
+ tf = {}
112
+ for t in dtoks:
113
+ tf[t] = tf.get(t, 0) + 1
114
+ s = 0.0
115
+ for t in q_toks:
116
+ if t not in self._df:
117
+ continue
118
+ idf = math.log((self._N - self._df[t] + 0.5) / (self._df[t] + 0.5) + 1.0)
119
+ f = tf.get(t, 0)
120
+ s += idf * (f * (k1 + 1)) / (f + k1 * (1 - b + b * dl / self._avgdl))
121
+ scores.append(s)
122
+ order = sorted(range(self._N), key=lambda i: scores[i], reverse=True)
123
+ return [self.docs[i] for i in order[:k] if scores[i] > 0]
124
+
125
+ # -- persist ------------------------------------------------------------
126
+ def save(self, path=DEFAULT_INDEX):
127
+ os.makedirs(os.path.dirname(path), exist_ok=True)
128
+ json.dump({"docs": self.docs}, open(path, "w"), ensure_ascii=False)
129
+ print(f"[rag] saved index -> {path} ({len(self.docs)} chunks)")
130
+
131
+ @classmethod
132
+ def load(cls, path=DEFAULT_INDEX):
133
+ kb = cls()
134
+ if os.path.exists(path):
135
+ data = json.load(open(path, "r", encoding="utf-8"))
136
+ kb.docs = data.get("docs", [])
137
+ kb._built = False
138
+ print(f"[rag] loaded index {path} ({len(kb.docs)} chunks)")
139
+ return kb
140
+
141
+
142
+ # A process-wide default KB, lazily built from data/kb/.
143
+ _DEFAULT_KB = None
144
+
145
+
146
+ def default_kb():
147
+ global _DEFAULT_KB
148
+ if _DEFAULT_KB is None:
149
+ if os.path.exists(DEFAULT_INDEX):
150
+ _DEFAULT_KB = KnowledgeBase.load(DEFAULT_INDEX)
151
+ elif os.path.isdir(KB_DIR):
152
+ kb = KnowledgeBase()
153
+ kb.ingest_path(KB_DIR)
154
+ _DEFAULT_KB = kb
155
+ else:
156
+ _DEFAULT_KB = KnowledgeBase()
157
+ return _DEFAULT_KB
158
+
159
+
160
+ def retrieve(query, k=4, kb=None):
161
+ """Top-level retrieval used by the `retrieve` tool + RAG controller."""
162
+ kb = kb or default_kb()
163
+ return kb.retrieve(query, k=k)
rag_finetune.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — RAG continuation fine-tune.
3
+
4
+ Loads the latest base checkpoint and continues training on the RAG corpus
5
+ (data/rag_corpus.txt) so the model actually learns to (a) emit
6
+ <tool name="retrieve">q</tool> and use the <result>, and (b) read
7
+ <context>...</context> injected mid-response. Run this AFTER the base
8
+ 24h run (or anytime you have a checkpoint).
9
+
10
+ python rag_finetune.py --hours 3 --batch 24 --ckpt-every 500
11
+
12
+ It reuses the hybrid AR/DIFF loss from train.py's logic (self-contained copy
13
+ below so it doesn't import the training script's __main__).
14
+ """
15
+ import os, json, argparse, time, random
16
+ import torch
17
+ import torch.nn.functional as F
18
+ from torch.utils.data import IterableDataset
19
+
20
+ from model import YKDiff
21
+ from tokenizer import YKTokenizer
22
+ from rag import DEFAULT_INDEX, KnowledgeBase
23
+
24
+ HERE = os.path.dirname(os.path.abspath(__file__))
25
+ DATADIR = os.path.join(HERE, "data")
26
+ CKPTDIR = os.path.join(HERE, "checkpoints")
27
+ CORPUS = os.path.join(DATADIR, "rag_corpus.txt")
28
+ SEQ = 1024
29
+
30
+
31
+ # ---- hybrid loss (mirrors train.py) ---------------------------------------
32
+ def ar_loss(logits, ids, mask):
33
+ sl = ids[:, 1:]; lm = logits[:, :-1]
34
+ return F.cross_entropy(lm.reshape(-1, lm.size(-1)), sl.reshape(-1), reduction="none").mean()
35
+
36
+
37
+ def diff_loss(logits, ids, mask, t):
38
+ B, L, V = logits.shape
39
+ ce = F.cross_entropy(logits.reshape(-1, V), ids.reshape(-1), reduction="none").view(B, L)
40
+ w = (mask * (1.0 - t)[:, None]).reshape(B, L)
41
+ return (ce * w).sum() / w.sum().clamp_min(1.0)
42
+
43
+
44
+ # ---- corpus -> token stream ----------------------------------------------
45
+ class RagTokens(IterableDataset):
46
+ def __init__(self, path, tok, seq):
47
+ self.lines = [l.rstrip("\n") for l in open(path, encoding="utf-8") if l.strip()]
48
+ self.tok = tok
49
+ self.seq = seq
50
+ self.buf = []
51
+
52
+ def _fill(self):
53
+ while len(self.buf) < self.seq + 1:
54
+ line = random.choice(self.lines)
55
+ self.buf.extend(self.tok.encode(line))
56
+
57
+ def __iter__(self):
58
+ while True:
59
+ self._fill()
60
+ chunk = self.buf[: self.seq + 1]
61
+ self.buf = self.buf[self.seq:]
62
+ yield torch.tensor(chunk, dtype=torch.long)
63
+
64
+
65
+ def pack_batch(ds, n):
66
+ ids = torch.stack([next(iter(ds)) for _ in range(n)]) # [B, L+1]
67
+ mask = (ids != ds.tok.pad_id).long()
68
+ return ids, mask
69
+
70
+
71
+ def main():
72
+ ap = argparse.ArgumentParser()
73
+ ap.add_argument("--hours", type=float, default=3.0)
74
+ ap.add_argument("--batch", type=int, default=24)
75
+ ap.add_argument("--ckpt-every", type=int, default=500)
76
+ ap.add_argument("--log-every", type=int, default=25)
77
+ ap.add_argument("--ckpt", default=None)
78
+ a = ap.parse_args()
79
+
80
+ tok = YKTokenizer.load(os.path.join(DATADIR, "tokenizer.json"))
81
+ if a.ckpt is None:
82
+ ck = sorted([f for f in os.listdir(CKPTDIR) if f.endswith(".pt")])
83
+ if not ck:
84
+ raise SystemExit("no checkpoint")
85
+ a.ckpt = os.path.join(CKPTDIR, ck[-1])
86
+ sd = torch.load(a.ckpt, map_location="cuda")
87
+ model = YKDiff(sd["cfg"]).cuda().train()
88
+ model.load_state_dict(sd["model"])
89
+ step0 = sd.get("step", 0)
90
+ print(f"[rag-ft] resume {a.ckpt} step={step0} params={sum(p.numel() for p in model.parameters())/1e6:.1f}M")
91
+
92
+ opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01, betas=(0.9, 0.95))
93
+ sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=max(1, int(a.hours * 60)), eta_min=1e-5)
94
+ ds = RagTokens(CORPUS, tok, SEQ)
95
+
96
+ t0 = time.time()
97
+ limit = a.hours * 3600
98
+ step = step0
99
+ while time.time() - t0 < limit:
100
+ step += 1
101
+ ids, mask = pack_batch(ds, a.batch)
102
+ x = ids[:, :-1].cuda(); y = ids[:, 1:].cuda(); m = mask[:, 1:].cuda()
103
+ if random.random() < 0.5:
104
+ mode = torch.zeros(a.batch, dtype=torch.long, device="cuda")
105
+ t = None
106
+ loss = ar_loss(model(x, mode), y, m)
107
+ else:
108
+ mode = torch.ones(a.batch, dtype=torch.long, device="cuda")
109
+ t = torch.rand(a.batch, device="cuda")
110
+ r = (0.1 + 0.9 * torch.rand(a.batch, device="cuda"))
111
+ inps = torch.where(torch.rand_like(y.float()) < r[:, None], tok.mask_id, y)
112
+ loss = diff_loss(model(inps, mode, t=t), y, m, t)
113
+ opt.zero_grad()
114
+ loss.backward()
115
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
116
+ opt.step(); sched.step()
117
+ if step % a.log_every == 0:
118
+ print(f"[rag-ft] step {step} loss={loss.item():.3f} t={ (time.time()-t0)/60:.1f}m", flush=True)
119
+ if step % a.ckpt_every == 0:
120
+ torch.save({"model": model.state_dict(), "cfg": sd["cfg"], "step": step,
121
+ "rag_ft": True}, os.path.join(CKPTDIR, f"ragft_{step:06d}.pt"))
122
+ print(f"[rag-ft] saved ragft_{step:06d}.pt", flush=True)
123
+ torch.save({"model": model.state_dict(), "cfg": sd["cfg"], "step": step, "rag_ft": True},
124
+ os.path.join(CKPTDIR, "ragft_final.pt"))
125
+ print("[rag-ft] done", flush=True)
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
tokenizer.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ From-scratch byte-level BPE tokenizer, trained on the corpus (no pretrained weights).
3
+ Special tokens are reserved for the hybrid model:
4
+ <pad> <bos> <eos> <mask> <think> </think> <tool> </tool> <result>
5
+ """
6
+ import json
7
+ import os
8
+ from tokenizers import Tokenizer
9
+ from tokenizers.models import BPE
10
+ from tokenizers.trainers import BpeTrainer
11
+ from tokenizers.pre_tokenizers import ByteLevel
12
+ from tokenizers.decoders import ByteLevel as ByteLevelDecoder
13
+ from tokenizers import AddedToken
14
+
15
+ SPECIAL = ["<pad>", "<bos>", "<eos>", "<mask>",
16
+ "<think>", "</think>", "<tool>", "</tool>", "<result>"]
17
+
18
+
19
+ class YKTokenizer:
20
+ def __init__(self, path=None):
21
+ self.path = path
22
+ self.tok = None
23
+ self._ids = {}
24
+
25
+ # ---- training --------------------------------------------------------
26
+ def train(self, iterator, vocab_size=32768, save_path=None):
27
+ assert vocab_size > len(SPECIAL)
28
+ model = BPE(unk_token="<unk>")
29
+ self.tok = Tokenizer(model)
30
+ self.tok.pre_tokenizer = ByteLevel()
31
+ self.tok.decoder = ByteLevelDecoder()
32
+ trainer = BpeTrainer(
33
+ vocab_size=vocab_size,
34
+ special_tokens=SPECIAL + ["<unk>"],
35
+ show_progress=True,
36
+ )
37
+ self.tok.train_from_iterator(iterator, trainer)
38
+ self._refresh()
39
+ if save_path:
40
+ self.save(save_path)
41
+ return self
42
+
43
+ # ---- load / save ----------------------------------------------------
44
+ def save(self, path):
45
+ self.path = path
46
+ os.makedirs(os.path.dirname(path), exist_ok=True)
47
+ self.tok.save(path)
48
+ with open(path + ".meta.json", "w") as f:
49
+ json.dump({"vocab_size": self.vocab_size,
50
+ "special_ids": self._ids}, f)
51
+
52
+ @classmethod
53
+ def load(cls, path):
54
+ obj = cls(path)
55
+ obj.tok = Tokenizer.from_file(path)
56
+ meta = path + ".meta.json"
57
+ if os.path.exists(meta):
58
+ with open(meta) as f:
59
+ m = json.load(f)
60
+ obj._ids = m["special_ids"]
61
+ obj._refresh()
62
+ return obj
63
+
64
+ def _refresh(self):
65
+ self._ids = {s: self.tok.token_to_id(s) for s in SPECIAL}
66
+ self._ids["<unk>"] = self.tok.token_to_id("<unk>")
67
+ self.vocab_size = self.tok.get_vocab_size()
68
+
69
+ # ---- ids -------------------------------------------------------------
70
+ @property
71
+ def pad_id(self): return self._ids["<pad>"]
72
+ @property
73
+ def bos_id(self): return self._ids["<bos>"]
74
+ @property
75
+ def eos_id(self): return self._ids["<eos>"]
76
+ @property
77
+ def mask_id(self): return self._ids["<mask>"]
78
+ @property
79
+ def think_id(self): return self._ids["<think>"]
80
+ @property
81
+ def endthink_id(self): return self._ids["</think>"]
82
+ @property
83
+ def tool_id(self): return self._ids["<tool>"]
84
+ @property
85
+ def endtool_id(self): return self._ids["</tool>"]
86
+ @property
87
+ def result_id(self): return self._ids["<result>"]
88
+
89
+ # ---- encode / decode ------------------------------------------------
90
+ def encode(self, text, add_special=False):
91
+ ids = self.tok.encode(text).ids
92
+ if add_special:
93
+ ids = [self.bos_id] + ids + [self.eos_id]
94
+ return ids
95
+
96
+ def encode_batch(self, texts):
97
+ return [t.ids for t in self.tok.encode_batch(texts)]
98
+
99
+ def decode(self, ids, skip_special=True):
100
+ if skip_special:
101
+ ids = [i for i in ids if i not in self._ids.values() or i == self._ids.get("<unk>", -1)]
102
+ return self.tok.decode(ids, skip_special_tokens=skip_special)
103
+
104
+ def __len__(self):
105
+ return self.vocab_size
tools.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — local tool executor for the agent loop.
3
+
4
+ Tools the model can call via <tool name="...">args</tool> :
5
+ calc(expr) safe arithmetic / math expression
6
+ python(code) run Python, stdout captured (threaded timeout)
7
+ read_file(p) read a text file
8
+ list_dir(p) list a directory
9
+ retrieve(q) RAG search over the local knowledge base (returns passages)
10
+
11
+ NOTE: python() executes locally on YOUR machine. It is sandboxed with a
12
+ restricted builtins set and a wall-clock timeout, but treat it as you would
13
+ any local code. Don't point clanker at untrusted input.
14
+ """
15
+ import ast, math, io, contextlib, os, time, threading, builtins
16
+ from rag import retrieve as _rag_retrieve
17
+
18
+ SAFE_NAMES = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
19
+ SAFE_NAMES.update({"abs": abs, "min": min, "max": max, "round": round,
20
+ "pow": pow, "len": len, "sum": sum, "sorted": sorted,
21
+ "int": int, "float": float, "str": str, "list": list,
22
+ "range": range, "True": True, "False": False, "None": None})
23
+
24
+
25
+ def _safe_eval(expr):
26
+ node = ast.parse(expr, mode="eval").body
27
+
28
+ def ev(n):
29
+ if isinstance(n, ast.BinOp):
30
+ a, b = ev(n.left), ev(n.right)
31
+ if isinstance(n.op, ast.Add): return a + b
32
+ if isinstance(n.op, ast.Sub): return a - b
33
+ if isinstance(n.op, ast.Mult): return a * b
34
+ if isinstance(n.op, ast.Div): return a / b
35
+ if isinstance(n.op, ast.FloorDiv): return a // b
36
+ if isinstance(n.op, ast.Mod): return a % b
37
+ if isinstance(n.op, ast.Pow): return a ** b
38
+ raise ValueError("op")
39
+ if isinstance(n, ast.UnaryOp):
40
+ v = ev(n.operand)
41
+ return -v if isinstance(n.op, ast.USub) else +v
42
+ if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
43
+ return n.value
44
+ if isinstance(n, ast.Call) and isinstance(n.func, ast.Name):
45
+ fn = SAFE_NAMES.get(n.func.id)
46
+ if fn is None: raise ValueError(f"no {n.func.id}")
47
+ return fn(*[ev(a) for a in n.args])
48
+ if isinstance(n, ast.Name):
49
+ if n.id in SAFE_NAMES: return SAFE_NAMES[n.id]
50
+ raise ValueError(f"name {n.id}")
51
+ raise ValueError("bad expr")
52
+ return ev(node)
53
+
54
+
55
+ def _run_python(code, timeout=10.0):
56
+ ns = {"__builtins__": {k: getattr(builtins, k)
57
+ for k in ("print", "len", "range", "list", "dict",
58
+ "tuple", "set", "str", "int", "float",
59
+ "bool", "min", "max", "sum", "sorted",
60
+ "abs", "enumerate", "zip", "map", "filter",
61
+ "open", "round", "pow") if hasattr(builtins, k)}}
62
+ ns["math"] = math
63
+ buf = io.StringIO()
64
+ result = {}
65
+
66
+ def target():
67
+ try:
68
+ with contextlib.redirect_stdout(buf):
69
+ exec(code, ns)
70
+ result["out"] = buf.getvalue()
71
+ except Exception as e:
72
+ result["out"] = buf.getvalue() + f"\nERROR: {type(e).__name__}: {e}"
73
+
74
+ t = threading.Thread(target=target, daemon=True)
75
+ t.start(); t.join(timeout)
76
+ if t.is_alive():
77
+ return f"ERROR: execution timed out after {timeout}s"
78
+ return result.get("out", "").strip() or "(no output)"
79
+
80
+
81
+ def execute_tool(name, arg):
82
+ try:
83
+ if name == "calc":
84
+ val = _safe_eval(arg.strip())
85
+ return f"{arg.strip()} = {val}"
86
+ if name == "python":
87
+ return _run_python(arg)
88
+ if name == "read_file":
89
+ p = arg.strip().strip('"\'')
90
+ if not os.path.exists(p): return f"ERROR: no such file: {p}"
91
+ with open(p, "r", errors="replace") as f:
92
+ return f.read()[:4000]
93
+ if name == "list_dir":
94
+ p = arg.strip().strip('"\'') or "."
95
+ if not os.path.isdir(p): return f"ERROR: no such dir: {p}"
96
+ return "\n".join(sorted(os.listdir(p))[:200])
97
+ if name == "retrieve":
98
+ try:
99
+ passages = _rag_retrieve(arg.strip(), k=4)
100
+ if not passages:
101
+ return "(no relevant context found in the knowledge base)"
102
+ return "\n---\n".join(passages)
103
+ except Exception as e:
104
+ return f"ERROR: retrieve failed: {e}"
105
+ return f"ERROR: unknown tool '{name}'"
106
+ except Exception as e:
107
+ return f"ERROR: {type(e).__name__}: {e}"
108
+
109
+
110
+ def parse_tool_calls(text):
111
+ """Return list of (name, argstring) from <tool name=\"...\">..</tool> blocks."""
112
+ calls = []
113
+ i = 0
114
+ while True:
115
+ s = text.find("<tool", i)
116
+ if s < 0: break
117
+ e = text.find("</tool>", s)
118
+ if e < 0: break
119
+ head = text[s:text.find(">", s) + 1]
120
+ # head like: <tool name="calc">
121
+ import re
122
+ m = re.search(r'name="([^"]+)"', head)
123
+ name = m.group(1) if m else "?"
124
+ arg = text[s + len(head):e].strip()
125
+ calls.append((name, arg))
126
+ i = e + len("</tool>")
127
+ return calls
train.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ clankerDiffusion — local training loop (RTX 5060 Ti, ~5h budget).
3
+
4
+ Hybrid objective (per step, mode chosen at random, p(AR)=0.5):
5
+ AR (mode 0): causal LM cross-entropy over the whole window.
6
+ DIFF (mode 1): MDLM absorbing-state masked diffusion — mask each token
7
+ independently with ratio r~U(0,1); reconstruct masked tokens
8
+ with bidirectional attention, conditioned on r via the time embed.
9
+
10
+ Runs in bf16 (Blackwell), AdamW + cosine LR, grad-clip, checkpoints locally.
11
+ Stops after --hours (default 5) of wall-clock time.
12
+ """
13
+ import os, json, time, argparse
14
+ import numpy as np
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.nn.functional as F
18
+
19
+ from model import YKDiff
20
+ from tokenizer import YKTokenizer
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+ DATADIR = os.path.join(HERE, "data")
24
+ CKPTDIR = os.path.join(HERE, "checkpoints")
25
+ os.makedirs(CKPTDIR, exist_ok=True)
26
+
27
+ # ---- architecture for 16 GB -------------------------------------------------
28
+ CFG = dict(
29
+ d_model=768, n_layers=12, n_heads=12, d_ff=2048,
30
+ max_len=1024, vocab_size=32768,
31
+ )
32
+
33
+
34
+ def load_data():
35
+ meta = json.load(open(os.path.join(DATADIR, "meta.json")))
36
+ arr = np.memmap(os.path.join(DATADIR, "train.bin"), dtype=np.uint16, mode="r")
37
+ CFG["vocab_size"] = meta["vocab_size"]
38
+ CFG["max_len"] = meta["seq_len"]
39
+ print(f"[train] data n_tokens={meta['n_tokens']:,} seq_len={meta['seq_len']} "
40
+ f"vocab={meta['vocab_size']}")
41
+ return arr, meta["seq_len"]
42
+
43
+
44
+ def sample_batch(arr, seq_len, batch):
45
+ N = len(arr)
46
+ starts = np.random.randint(0, N - seq_len, size=batch)
47
+ out = np.stack([arr[s:s + seq_len].astype(np.int64) for s in starts])
48
+ return torch.from_numpy(out).long()
49
+
50
+
51
+ def train(args):
52
+ tok = YKTokenizer.load(os.path.join(DATADIR, "tokenizer.json"))
53
+ arr, seq_len = load_data()
54
+
55
+ model = YKDiff(CFG).cuda()
56
+ n_params = sum(p.numel() for p in model.parameters())
57
+ print(f"[train] model params = {n_params/1e6:.1f}M")
58
+
59
+ optim = torch.optim.AdamW(model.parameters(), lr=args.lr, betas=(0.9, 0.95),
60
+ weight_decay=0.1)
61
+ V = CFG["vocab_size"]
62
+ pad_id = tok.pad_id
63
+ mask_id = tok.mask_id
64
+
65
+ # resume
66
+ step0 = 0
67
+ ckpts = sorted([f for f in os.listdir(CKPTDIR) if f.endswith(".pt")])
68
+ if ckpts and not args.fresh:
69
+ path = os.path.join(CKPTDIR, ckpts[-1])
70
+ sd = torch.load(path, map_location="cuda")
71
+ model.load_state_dict(sd["model"]); optim.load_state_dict(sd["optim"])
72
+ step0 = sd["step"]
73
+ print(f"[train] resumed from {path} step={step0}")
74
+
75
+ model.train()
76
+ amp = torch.cuda.amp.autocast(dtype=torch.bfloat16)
77
+ t0 = time.time()
78
+ limit = args.hours * 3600.0
79
+ step = step0
80
+ running = 0.0
81
+
82
+ while True:
83
+ if time.time() - t0 > limit:
84
+ print(f"[train] wall-clock limit {args.hours}h reached at step {step}")
85
+ break
86
+
87
+ optim.zero_grad(set_to_none=True)
88
+ mode_ar = (torch.rand(1).item() < 0.5)
89
+ idx = sample_batch(arr, seq_len, args.batch).cuda()
90
+
91
+ with amp:
92
+ if mode_ar:
93
+ m = torch.zeros(args.batch, dtype=torch.long, device="cuda")
94
+ logits = model(idx, m, t=None) # causal
95
+ loss = F.cross_entropy(
96
+ logits[:, :-1].reshape(-1, V),
97
+ idx[:, 1:].reshape(-1), ignore_index=pad_id)
98
+ mname = "AR"
99
+ else:
100
+ m = torch.ones(args.batch, dtype=torch.long, device="cuda")
101
+ r = torch.rand(args.batch, device="cuda") # per-sample ratio
102
+ is_mask = torch.rand(args.batch, seq_len, device="cuda") < r[:, None]
103
+ not_pad = idx != pad_id
104
+ masked = idx.clone(); masked[is_mask] = mask_id
105
+ logits = model(masked, m, t=r)
106
+ ce = F.cross_entropy(logits.reshape(-1, V), idx.reshape(-1),
107
+ reduction="none", ignore_index=-100)
108
+ ce = ce * is_mask.reshape(-1) * not_pad.reshape(-1)
109
+ denom = (is_mask & not_pad).reshape(-1).sum().clamp(min=1)
110
+ loss = ce.sum() / denom
111
+ mname = "DIFF"
112
+
113
+ loss.backward()
114
+ nn.utils.clip_grad_norm_(model.parameters(), 1.0)
115
+ optim.step()
116
+
117
+ running = running * 0.9 + float(loss.item()) * 0.1
118
+ step += 1
119
+ if step % args.log_every == 0:
120
+ print(f"[train] step {step} [{mname}] loss={running:.3f} "
121
+ f"t={(time.time()-t0)/60:.1f}m", flush=True)
122
+
123
+ if step % args.ckpt_every == 0:
124
+ path = os.path.join(CKPTDIR, f"clanker_{step:07d}.pt")
125
+ torch.save({"model": model.state_dict(), "optim": optim.state_dict(),
126
+ "step": step, "cfg": CFG, "vocab": V}, path)
127
+ print(f"[train] checkpoint -> {path}", flush=True)
128
+
129
+ # final save
130
+ path = os.path.join(CKPTDIR, f"clanker_{step:07d}_final.pt")
131
+ torch.save({"model": model.state_dict(), "optim": optim.state_dict(),
132
+ "step": step, "cfg": CFG, "vocab": V}, path)
133
+ json.dump(CFG, open(os.path.join(CKPTDIR, "config.json"), "w"))
134
+ print(f"[train] DONE final={path} steps={step}")
135
+
136
+
137
+ if __name__ == "__main__":
138
+ ap = argparse.ArgumentParser()
139
+ ap.add_argument("--hours", type=float, default=5.0)
140
+ ap.add_argument("--batch", type=int, default=32)
141
+ ap.add_argument("--lr", type=float, default=3e-4)
142
+ ap.add_argument("--log-every", type=int, default=25)
143
+ ap.add_argument("--ckpt-every", type=int, default=500)
144
+ ap.add_argument("--fresh", action="store_true")
145
+ train(ap.parse_args())
upload_artifacts.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ upload_artifacts.py — run THIS locally before launching the Colab notebook.
3
+
4
+ Publishes the model code + trained tokenizer + (optionally) your local
5
+ checkpoint to HuggingFace Hub so the Colab script can pull them.
6
+
7
+ set HF_TOKEN=your_huggingface_token
8
+ python upload_artifacts.py # code + tokenizer
9
+ python upload_artifacts.py --with-ckpt # also upload latest local checkpoint
10
+ """
11
+ import os, sys, argparse
12
+ from huggingface_hub import HfApi
13
+
14
+ ROOT = r"C:\Users\User\CalcGPU\clankerDiffusion"
15
+ CODE_REPO = "clankerDiffusion/base"
16
+ CKPT_REPO = "clankerDiffusion/checkpoints"
17
+
18
+
19
+ def main():
20
+ ap = argparse.ArgumentParser()
21
+ ap.add_argument("--with-ckpt", action="store_true")
22
+ a = ap.parse_args()
23
+
24
+ token = os.environ.get("HF_TOKEN")
25
+ if not token:
26
+ sys.exit("set HF_TOKEN first: set HF_TOKEN=hf_xxx")
27
+ api = HfApi(token=token)
28
+
29
+ api.create_repo(CODE_REPO, repo_type="model", exist_ok=True)
30
+ code_files = ["model.py", "tokenizer.py", "train.py", "prep.py", "infer.py",
31
+ os.path.join("data", "tokenizer.json"),
32
+ os.path.join("data", "tokenizer.json.meta.json")]
33
+ for rel in code_files:
34
+ p = os.path.join(ROOT, rel)
35
+ if os.path.exists(p):
36
+ api.upload_file(repo_id=CODE_REPO,
37
+ path_in_repo=os.path.basename(p),
38
+ path_or_fileobj=p)
39
+ print("uploaded", rel)
40
+ else:
41
+ print("skip (missing)", rel)
42
+
43
+ if a.with_ckpt:
44
+ api.create_repo(CKPT_REPO, repo_type="model", exist_ok=True)
45
+ ckpt_dir = os.path.join(ROOT, "checkpoints")
46
+ if os.path.isdir(ckpt_dir):
47
+ pts = sorted(f for f in os.listdir(ckpt_dir) if f.endswith(".pt"))
48
+ if pts:
49
+ p = os.path.join(ckpt_dir, pts[-1])
50
+ api.upload_file(repo_id=CKPT_REPO,
51
+ path_in_repo=os.path.basename(p),
52
+ path_or_fileobj=p)
53
+ print("uploaded checkpoint", pts[-1])
54
+ print("DONE. Colab CODE_REPO =", CODE_REPO)
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()