qwen2
ExocoreV1 / trainer.py
ChoruYt's picture
ExocoreV1: multi-thinking, web search, auto-load, loss chart, architecture tree, no system prompt
5e7ce6a verified
Raw
History Blame Contribute Delete
17.1 kB
import os
import math
import json
import time
import threading
import torch
import torch.nn as nn
from torch.nn import functional as F
training_state = {
"running": False,
"epoch": 0,
"total_epochs": 0,
"step": 0,
"total_steps": 0,
"loss": None,
"best_loss": None,
"loss_history": [],
"log": [],
"done": False,
"error": None,
"model_name": None,
}
_stop_flag = threading.Event()
class MultiHeadSelfAttention(nn.Module):
def __init__(self, n_embd, n_head, block_size, dropout):
super().__init__()
assert n_embd % n_head == 0
self.n_head = n_head
self.n_embd = n_embd
self.head_size = n_embd // n_head
self.c_attn = nn.Linear(n_embd, 3 * n_embd, bias=False)
self.c_proj = nn.Linear(n_embd, n_embd, bias=False)
self.attn_drop = nn.Dropout(dropout)
self.resid_drop = nn.Dropout(dropout)
self.register_buffer("mask", torch.tril(torch.ones(block_size, block_size)))
def forward(self, x):
B, T, C = x.size()
q, k, v = self.c_attn(x).split(self.n_embd, dim=2)
k = k.view(B, T, self.n_head, self.head_size).transpose(1, 2)
q = q.view(B, T, self.n_head, self.head_size).transpose(1, 2)
v = v.view(B, T, self.n_head, self.head_size).transpose(1, 2)
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(self.head_size))
att = att.masked_fill(self.mask[:T, :T] == 0, float('-inf'))
att = F.softmax(att, dim=-1)
att = self.attn_drop(att)
y = att @ v
y = y.transpose(1, 2).contiguous().view(B, T, C)
return self.resid_drop(self.c_proj(y))
class FeedForward(nn.Module):
def __init__(self, n_embd, dropout):
super().__init__()
self.net = nn.Sequential(
nn.Linear(n_embd, 4 * n_embd),
nn.GELU(),
nn.Linear(4 * n_embd, n_embd),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Block(nn.Module):
def __init__(self, n_embd, n_head, block_size, dropout):
super().__init__()
self.ln1 = nn.LayerNorm(n_embd)
self.attn = MultiHeadSelfAttention(n_embd, n_head, block_size, dropout)
self.ln2 = nn.LayerNorm(n_embd)
self.ff = FeedForward(n_embd, dropout)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ff(self.ln2(x))
return x
class MiniGPT(nn.Module):
def __init__(self, vocab_size, block_size, n_embd, n_layer, n_head, dropout):
super().__init__()
self.block_size = block_size
self.tok_emb = nn.Embedding(vocab_size, n_embd)
self.pos_emb = nn.Embedding(block_size, n_embd)
self.drop = nn.Dropout(dropout)
self.blocks = nn.Sequential(*[
Block(n_embd, n_head, block_size, dropout) for _ in range(n_layer)
])
self.ln_f = nn.LayerNorm(n_embd)
self.head = nn.Linear(n_embd, vocab_size, bias=False)
self.apply(self._init_weights)
def _init_weights(self, module):
if isinstance(module, (nn.Linear, nn.Embedding)):
nn.init.normal_(module.weight, mean=0.0, std=0.02)
if isinstance(module, nn.Linear) and module.bias is not None:
nn.init.zeros_(module.bias)
def forward(self, idx, targets=None):
B, T = idx.size()
pos = torch.arange(T, device=idx.device)
x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
x = self.blocks(x)
x = self.ln_f(x)
logits = self.head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens, temperature=0.8, top_k=40):
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.block_size:]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
if top_k is not None:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float('-inf')
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
idx = torch.cat((idx, idx_next), dim=1)
return idx
def get_batch(data, block_size, batch_size, device):
ix = torch.randint(len(data) - block_size, (batch_size,))
x = torch.stack([data[i:i+block_size] for i in ix])
y = torch.stack([data[i+1:i+block_size+1] for i in ix])
return x.to(device), y.to(device)
def start_training(text, config, model_name):
global training_state, _stop_flag
_stop_flag.clear()
training_state = {
"running": True,
"epoch": 0,
"total_epochs": config["epochs"],
"step": 0,
"total_steps": 0,
"loss": None,
"best_loss": None,
"loss_history": [],
"log": [],
"done": False,
"error": None,
"model_name": model_name,
}
def run():
try:
device = "cpu"
block_size = config.get("block_size", 128)
batch_size = config.get("batch_size", 32)
n_embd = config.get("n_embd", 256)
n_layer = config.get("n_layer", 4)
n_head = config.get("n_head", 4)
dropout = config.get("dropout", 0.1)
lr = config.get("lr", 3e-4)
epochs = config.get("epochs", 5)
chars = sorted(set(text))
vocab_size = len(chars)
stoi = {c: i for i, c in enumerate(chars)}
itos = {i: c for i, c in enumerate(chars)}
data = torch.tensor([stoi[c] for c in text], dtype=torch.long)
steps_per_epoch = max(1, len(data) // (block_size * batch_size))
total_steps = steps_per_epoch * epochs
training_state["total_steps"] = total_steps
model = MiniGPT(vocab_size, block_size, n_embd, n_layer, n_head, dropout).to(device)
param_count = sum(p.numel() for p in model.parameters())
training_state["log"].append(f"Model ready: {param_count:,} parameters | Vocab: {vocab_size} chars")
optimizer = torch.optim.AdamW(model.parameters(), lr=lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=total_steps)
best_loss = float('inf')
global_step = 0
for epoch in range(1, epochs + 1):
if _stop_flag.is_set():
training_state["log"].append("Training stopped by user.")
break
training_state["epoch"] = epoch
epoch_loss = 0.0
for step in range(steps_per_epoch):
if _stop_flag.is_set():
break
xb, yb = get_batch(data, block_size, batch_size, device)
_, loss = model(xb, yb)
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
scheduler.step()
global_step += 1
epoch_loss += loss.item()
training_state["step"] = global_step
training_state["loss"] = round(loss.item(), 4)
avg_loss = epoch_loss / steps_per_epoch
training_state["log"].append(f"Epoch {epoch}/{epochs} — avg loss: {avg_loss:.4f}")
training_state["loss_history"].append({"epoch": epoch, "loss": round(avg_loss, 4)})
if avg_loss < best_loss:
best_loss = avg_loss
training_state["best_loss"] = round(best_loss, 4)
save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
os.makedirs(save_dir, exist_ok=True)
save_path = os.path.join(save_dir, f"{model_name}.pt")
torch.save({
"model_state": model.state_dict(),
"config": {
"vocab_size": vocab_size,
"block_size": block_size,
"n_embd": n_embd,
"n_layer": n_layer,
"n_head": n_head,
"dropout": dropout,
},
"stoi": stoi,
"itos": itos,
"model_name": model_name,
}, save_path)
training_state["log"].append(f"Model saved: models/trained/{model_name}.pt")
training_state["log"].append(f"Best loss: {best_loss:.4f}")
training_state["done"] = True
training_state["running"] = False
except Exception as e:
training_state["error"] = str(e)
training_state["running"] = False
training_state["done"] = True
t = threading.Thread(target=run, daemon=True)
t.start()
def stop_training():
_stop_flag.set()
def get_trained_models():
save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
if not os.path.exists(save_dir):
return []
results = []
for f in os.listdir(save_dir):
if f.endswith(".pt"):
results.append({"name": f, "type": "pt"})
elif os.path.isdir(os.path.join(save_dir, f)) and os.path.exists(os.path.join(save_dir, f, "config.json")):
results.append({"name": f, "type": "hf"})
return results
# ── MiniGPT inference ─────────────────────────────────────────────────────────
_inference_cache = {}
def load_trained_model(model_name):
if model_name in _inference_cache:
return _inference_cache[model_name]
save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
path = os.path.join(save_dir, model_name)
checkpoint = torch.load(path, map_location="cpu", weights_only=False)
cfg = checkpoint["config"]
model = MiniGPT(
vocab_size=cfg["vocab_size"],
block_size=cfg["block_size"],
n_embd=cfg["n_embd"],
n_layer=cfg["n_layer"],
n_head=cfg["n_head"],
dropout=0.0,
)
model.load_state_dict(checkpoint["model_state"])
model.eval()
_inference_cache[model_name] = {
"model": model,
"stoi": checkpoint["stoi"],
"itos": checkpoint["itos"],
"block_size": cfg["block_size"],
}
return _inference_cache[model_name]
def generate_text(model_name, prompt, max_tokens=200, temperature=0.8, top_k=40, think_mode=False):
mc = load_trained_model(model_name)
model = mc["model"]
stoi = mc["stoi"]
itos = mc["itos"]
block_size = mc["block_size"]
formatted = f"User: {prompt}\n<think>" if think_mode else prompt
encoded = [stoi.get(c, 0) for c in formatted]
idx = torch.tensor([encoded], dtype=torch.long)
out = model.generate(idx, max_new_tokens=max_tokens, temperature=temperature, top_k=top_k)
tokens = out[0].tolist()
full_output = "".join(itos.get(i, "") for i in tokens)
generated = full_output[len(formatted):]
if think_mode:
if "</think>" in generated:
parts = generated.split("</think>", 1)
return {"think_block": parts[0].strip(), "response": parts[1].strip(), "thinking": True}
return {"think_block": generated.strip(), "response": generated.strip(), "thinking": True}
return {"response": generated, "thinking": False}
# ── ExocoreV1 inference ───────────────────────────────────────────────────────
_exocore_cache = {}
def is_exocore_pt(model_name):
save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
meta_path = os.path.join(save_dir, model_name + ".meta")
if os.path.exists(meta_path):
try:
with open(meta_path, "r") as f:
meta = json.load(f)
return meta.get("exocore_type") in ("qwen3", "exocoreV1")
except Exception:
pass
path = os.path.join(save_dir, model_name)
if os.path.exists(path) and path.endswith(".pt"):
try:
ckpt = torch.load(path, map_location="cpu", weights_only=False)
return ckpt.get("exocore_type") in ("qwen3", "exocoreV1")
except Exception:
pass
return False
def load_exocore_model(model_name):
import gc
if model_name in _exocore_cache:
return _exocore_cache[model_name]
from exocore_model import ExocoreLM
save_dir = os.path.join(os.path.dirname(__file__), "models", "trained")
path = os.path.join(save_dir, model_name)
ckpt = torch.load(path, map_location="cpu", weights_only=False)
cfg = ckpt["config"]
tok_json = ckpt["tokenizer_json"]
model = ExocoreLM(cfg)
model.load_state_dict(ckpt["model_state"], strict=True)
del ckpt
gc.collect()
model.eval()
from tokenizers import Tokenizer
tok = Tokenizer.from_str(tok_json)
_exocore_cache[model_name] = {"model": model, "tokenizer": tok, "config": cfg, "name": "ExocoreV1"}
return _exocore_cache[model_name]
def generate_exocore_stream(model_name, messages, max_tokens=512, temperature=0.7,
think_mode=False, deep_think=False, search_context=None):
from exocore_model import build_chat_prompt, IM_END, EOT
mc = load_exocore_model(model_name)
tok = mc["tokenizer"]
model = mc["model"]
cfg = mc["config"]
max_pos = min(cfg.get("max_position_embeddings", 8192), 4096)
def sample_next(ids):
ctx = ids[:, -min(ids.shape[1], max_pos):]
with torch.no_grad():
logits, _ = model(ctx)
logits = logits[:, -1, :].float() / max(temperature, 1e-5)
v, _ = torch.topk(logits, min(50, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = torch.softmax(logits, dim=-1)
return torch.multinomial(probs, 1)
def encode(prompt):
enc = tok.encode(prompt)
return torch.tensor([enc.ids], dtype=torch.long)
def collect_tokens(ids, max_t, stop_ids=(IM_END, EOT)):
out_ids = []
for _ in range(max_t):
nxt = sample_next(ids)
tid = nxt.item()
ids = torch.cat([ids, nxt], dim=1)
if tid in stop_ids:
break
out_ids.append(tid)
return tok.decode(out_ids), ids
if deep_think:
think1_prompt = build_chat_prompt(messages, think=True, search_context=search_context)
think1_raw, _ = collect_tokens(encode(think1_prompt), min(max_tokens * 2, 1024))
think1 = think1_raw.replace("<think>", "").replace("</think>", "").strip()
think2_prompt = build_chat_prompt(messages, think=True, prior_thinking=think1, search_context=search_context)
think2_raw, _ = collect_tokens(encode(think2_prompt), min(max_tokens, 512))
think2 = think2_raw.replace("<think>", "").replace("</think>", "").strip()
combined = f"[Pass 1]\n{think1}\n\n[Pass 2]\n{think2}"
yield ("think_block", combined)
answer_prompt = build_chat_prompt(messages, think=False, prior_thinking=combined, search_context=search_context)
ids = encode(answer_prompt)
for _ in range(max_tokens):
nxt = sample_next(ids)
tid = nxt.item()
ids = torch.cat([ids, nxt], dim=1)
if tid in (IM_END, EOT):
break
yield ("token", tok.decode([tid]))
else:
prompt = build_chat_prompt(messages, think=think_mode, search_context=search_context)
ids = encode(prompt)
buf = ""
in_think = False
think_done = not think_mode
think_buf = ""
think_sent = False
for _ in range(max_tokens):
nxt = sample_next(ids)
tid = nxt.item()
ids = torch.cat([ids, nxt], dim=1)
if tid in (IM_END, EOT):
break
buf += tok.decode([tid])
if not think_done:
if not in_think and "<think>" in buf:
buf = buf.split("<think>", 1)[1]
in_think = True
if in_think:
if "</think>" in buf:
ttext, buf = buf.split("</think>", 1)
think_buf += ttext
think_done = True
in_think = False
if not think_sent:
yield ("think_block", think_buf.strip())
think_sent = True
else:
think_buf += buf
buf = ""
continue
if not in_think and buf:
yield ("token", buf)
buf = ""
if buf and not in_think:
yield ("token", buf)