Upload folder using huggingface_hub
Browse files- agent.py +99 -0
- build_rag.py +114 -0
- colab_train.py +215 -0
- data/tokenizer.json +0 -0
- infer.py +135 -0
- model.py +181 -0
- prep.py +250 -0
- push_hf.py +83 -0
- rag.py +163 -0
- rag_finetune.py +129 -0
- tokenizer.py +105 -0
- tools.py +127 -0
- train.py +145 -0
- 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()
|