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" 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 "" in generated: parts = generated.split("", 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("", "").replace("", "").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("", "").replace("", "").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 "" in buf: buf = buf.split("", 1)[1] in_think = True if in_think: if "" in buf: ttext, buf = buf.split("", 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)