| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import os |
| import math |
| import time |
| import logging |
| import threading |
| import traceback |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| import gradio as gr |
| import sentencepiece as spm |
| from starlette.middleware import Middleware |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
| from typing import Optional |
| from huggingface_hub import hf_hub_download |
|
|
| |
| |
| |
| |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") |
| logger = logging.getLogger("mtp") |
|
|
| |
| |
| |
| |
| DEVICE = os.environ.get("DEVICE") or ("cuda" if torch.cuda.is_available() else "cpu") |
|
|
| if DEVICE == "cpu": |
| |
| |
| torch.set_num_threads(max(1, os.cpu_count() or 1)) |
| |
| |
| try: |
| torch.set_num_interop_threads(1) |
| except RuntimeError: |
| pass |
|
|
| torch.set_grad_enabled(False) |
|
|
| |
| |
| _HAS_SDPA = hasattr(F, "scaled_dot_product_attention") |
|
|
| REPO_ID = os.environ.get("MTP_REPO_ID", "TeszenAI/MTP-2.7") |
| FILENAME = os.environ.get("MTP_FILENAME", "MTP2_7_MODEL.pt") |
|
|
| |
| |
| |
| ALLOWED_ORIGINS = [ |
| o.strip() for o in os.environ.get("MTP_ALLOWED_ORIGINS", "*").split(",") if o.strip() |
| ] or ["*"] |
|
|
| |
| |
| |
| MAX_INPUT_CHARS = 4000 |
|
|
| |
| |
| |
| DEBUG_ERRORS = os.environ.get("MTP_DEBUG_ERRORS", "0") == "1" |
|
|
| |
| |
| |
| |
| |
| |
| |
| _generation_lock = threading.Lock() |
|
|
| |
| def rotate_half(x): |
| x1, x2 = x.chunk(2, dim=-1) |
| return torch.cat((-x2, x1), dim=-1) |
|
|
| def apply_rope(q, k, cos, sin): |
| cos = cos.unsqueeze(0).unsqueeze(0) |
| sin = sin.unsqueeze(0).unsqueeze(0) |
| q_rot = (q * cos) + (rotate_half(q) * sin) |
| k_rot = (k * cos) + (rotate_half(k) * sin) |
| return q_rot, k_rot |
|
|
| class RotaryEmbedding(nn.Module): |
| def __init__(self, head_dim, max_seq_len, base=10000): |
| super().__init__() |
| inv_freq = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim)) |
| self.register_buffer("inv_freq", inv_freq, persistent=False) |
| self._build_cache(max_seq_len) |
|
|
| def _build_cache(self, seq_len): |
| t = torch.arange(seq_len, dtype=self.inv_freq.dtype, device=self.inv_freq.device) |
| freqs = torch.einsum("i,j->ij", t, self.inv_freq) |
| emb = torch.cat((freqs, freqs), dim=-1) |
| self.register_buffer("cos_cached", emb.cos(), persistent=False) |
| self.register_buffer("sin_cached", emb.sin(), persistent=False) |
| self.max_seq_len_cached = seq_len |
|
|
| def forward(self, seq_len, device, dtype, offset=0): |
| |
| |
| |
| if offset + seq_len > self.max_seq_len_cached: |
| self._build_cache(offset + seq_len) |
| cos = self.cos_cached[offset:offset + seq_len].to(device=device, dtype=dtype) |
| sin = self.sin_cached[offset:offset + seq_len].to(device=device, dtype=dtype) |
| return cos, sin |
|
|
|
|
| class CausalSelfAttention(nn.Module): |
| def __init__(self, n_embd, n_head, block_size, dropout): |
| super().__init__() |
| self.n_head = n_head |
| self.head_dim = n_embd // n_head |
| self.qkv = nn.Linear(n_embd, 3 * n_embd) |
| self.proj = nn.Linear(n_embd, n_embd) |
| self.attn_dropout = nn.Dropout(dropout) |
| self.resid_dropout = nn.Dropout(dropout) |
| mask = torch.tril(torch.ones(block_size, block_size)).view(1, 1, block_size, block_size) |
| self.register_buffer("mask", mask) |
|
|
| def forward(self, x, cos, sin, past_kv=None, use_cache=False): |
| B, T, C = x.shape |
| qkv = self.qkv(x) |
| q, k, v = qkv.split(C, dim=2) |
| q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2) |
| k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2) |
| v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2) |
|
|
| |
| |
| |
| |
| q, k = apply_rope(q, k, cos, sin) |
|
|
| if past_kv is not None: |
| past_k, past_v = past_kv |
| k = torch.cat([past_k, k], dim=2) |
| v = torch.cat([past_v, v], dim=2) |
|
|
| present_kv = (k, v) if use_cache else None |
|
|
| is_causal = (past_kv is None) and (T > 1) |
|
|
| if _HAS_SDPA: |
| out = F.scaled_dot_product_attention( |
| q, k, v, attn_mask=None, dropout_p=0.0, is_causal=is_causal, |
| ) |
| else: |
| att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim) |
| if is_causal: |
| Tk = k.size(-2) |
| causal_mask = torch.tril(torch.ones(T, Tk, device=x.device, dtype=torch.bool)) |
| att = att.masked_fill(~causal_mask, float("-inf")) |
| att = F.softmax(att, dim=-1) |
| att = self.attn_dropout(att) |
| out = att @ v |
|
|
| out = out.transpose(1, 2).contiguous().view(B, T, C) |
| out = self.resid_dropout(self.proj(out)) |
| return out, present_kv |
|
|
|
|
| class SwiGLU(nn.Module): |
| def __init__(self, n_embd, dropout): |
| super().__init__() |
| hidden = int(2 * (4 * n_embd) / 3) |
| hidden = ((hidden + 7) // 8) * 8 |
| self.w_gate = nn.Linear(n_embd, hidden, bias=False) |
| self.w_up = nn.Linear(n_embd, hidden, bias=False) |
| self.w_down = nn.Linear(hidden, n_embd, bias=False) |
| self.dropout = nn.Dropout(dropout) |
|
|
| def forward(self, x): |
| return self.dropout(self.w_down(F.silu(self.w_gate(x)) * self.w_up(x))) |
|
|
|
|
| class RMSNorm(nn.Module): |
| """Debe coincidir exactamente con la version de entrenamiento. No |
| necesita ningun cambio para funcionar con KV-cache: normaliza cada |
| posicion de forma independiente, igual que LayerNorm.""" |
| def __init__(self, dim, eps=1e-6): |
| super().__init__() |
| self.eps = eps |
| self.weight = nn.Parameter(torch.ones(dim)) |
|
|
| def forward(self, x): |
| norm = x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps) |
| return norm * self.weight |
|
|
|
|
| class Block(nn.Module): |
| def __init__(self, n_embd, n_head, block_size, dropout): |
| super().__init__() |
| self.ln1 = RMSNorm(n_embd) |
| self.attn = CausalSelfAttention(n_embd, n_head, block_size, dropout) |
| self.ln2 = RMSNorm(n_embd) |
| self.ff = SwiGLU(n_embd, dropout) |
|
|
| def forward(self, x, cos, sin, past_kv=None, use_cache=False): |
| attn_out, present_kv = self.attn(self.ln1(x), cos, sin, past_kv=past_kv, use_cache=use_cache) |
| x = x + attn_out |
| x = x + self.ff(self.ln2(x)) |
| return x, present_kv |
|
|
|
|
| class MTP(nn.Module): |
| def __init__(self, vocab_size, block_size, n_layer, n_head, n_embd, dropout): |
| super().__init__() |
| self.block_size = block_size |
| self.head_dim = n_embd // n_head |
| self.tok_emb = nn.Embedding(vocab_size, n_embd) |
| self.rope = RotaryEmbedding(self.head_dim, max_seq_len=block_size) |
| self.drop = nn.Dropout(dropout) |
| self.blocks = nn.ModuleList([Block(n_embd, n_head, block_size, dropout) for _ in range(n_layer)]) |
| self.ln_f = RMSNorm(n_embd) |
| self.lm_head = nn.Linear(n_embd, vocab_size, bias=False) |
| self.lm_head.weight = self.tok_emb.weight |
|
|
| def forward(self, idx, past_key_values=None, use_cache=False, pos_offset=0): |
| B, T = idx.shape |
| x = self.tok_emb(idx) |
| x = self.drop(x) |
| cos, sin = self.rope(T, idx.device, x.dtype, offset=pos_offset) |
|
|
| new_past = [] if use_cache else None |
| for i, block in enumerate(self.blocks): |
| past_kv = past_key_values[i] if past_key_values is not None else None |
| x, present_kv = block(x, cos, sin, past_kv=past_kv, use_cache=use_cache) |
| if use_cache: |
| new_past.append(present_kv) |
|
|
| x = self.ln_f(x) |
| logits = self.lm_head(x) |
| return logits, new_past |
|
|
|
|
| |
| def _download_checkpoint_with_retries(repo_id, filename, max_retries=5): |
| """Reintenta la descarga con backoff. hf_hub_download puede fallar por un |
| corte de red momentaneo o un problema del backend Xet de HF; sin esto, |
| un solo fallo transitorio tira abajo el arranque completo del Space.""" |
| last_err = None |
| for attempt in range(1, max_retries + 1): |
| try: |
| logger.info(f"Descargando checkpoint desde el Hub (intento {attempt}/{max_retries})...") |
| return hf_hub_download(repo_id=repo_id, filename=filename) |
| except Exception as e: |
| last_err = e |
| logger.warning(f"Fallo la descarga del checkpoint: {e}") |
| if attempt < max_retries: |
| time.sleep(5 * attempt) |
| raise RuntimeError(f"No se pudo descargar el checkpoint tras {max_retries} intentos") from last_err |
|
|
| ckpt_path = _download_checkpoint_with_retries(REPO_ID, FILENAME) |
| checkpoint = torch.load(ckpt_path, map_location=DEVICE) |
|
|
| cfg = checkpoint["config"] |
| special = checkpoint["special_tokens"] |
| gen_defaults = checkpoint["generation_defaults"] |
|
|
| PAD_ID, BOS_ID, EOS_ID, UNK_ID = special["pad_id"], special["bos_id"], special["eos_id"], special["unk_id"] |
|
|
| |
| |
| |
| |
| sp = spm.SentencePieceProcessor() |
| sp.load_from_serialized_proto(checkpoint["spm_model_bytes"]) |
|
|
| model = MTP( |
| vocab_size=cfg["vocab_size"], block_size=cfg["block_size"], |
| n_layer=cfg["n_layer"], n_head=cfg["n_head"], |
| n_embd=cfg["n_embd"], dropout=cfg["dropout"], |
| ).to(DEVICE) |
| model.load_state_dict(checkpoint["model_state_dict"]) |
| model.eval() |
|
|
| BLOCK_SIZE = cfg["block_size"] |
|
|
| logger.info( |
| f"MTP cargado ({checkpoint['meta']['model_name']}, " |
| f"entrenado con {checkpoint['meta']['trained_examples']} ejemplos) " |
| f"| device={DEVICE} | SDPA={'sí' if _HAS_SDPA else 'no (fallback manual)'}" |
| ) |
|
|
|
|
| import re as _re_indent |
|
|
| def protect_indentation(text): |
| """Debe coincidir exactamente con la funcion usada en el entrenamiento.""" |
| lines = text.split("\n") |
| new_lines = [] |
| for line in lines: |
| stripped = line.lstrip(" ") |
| n_spaces = len(line) - len(stripped) |
| n_levels = n_spaces // 4 |
| remainder = n_spaces % 4 |
| if n_levels > 0: |
| prefix = " " + " ".join(["<tab>"] * n_levels) + " " + " " * remainder |
| else: |
| prefix = " " * remainder |
| new_lines.append(prefix + stripped) |
| text = "\n".join(new_lines) |
| def _repl(m): |
| n = len(m.group()) |
| return " " + " ".join(["<nl>"] * n) + " " |
| text = _re_indent.sub(r"\n+", _repl, text) |
| return text |
|
|
|
|
| def restore_indentation(text): |
| text = _re_indent.sub(r"(<nl>\s*)+", lambda m: "\n" * m.group().count("<nl>"), text) |
| text = _re_indent.sub(r"(<tab>\s*)+", lambda m: " " * m.group().count("<tab>"), text) |
| return text |
|
|
|
|
| def encode_text(s): |
| return sp.encode(protect_indentation(s), out_type=int) |
|
|
|
|
| def decode_ids(ids): |
| text = sp.decode([i for i in ids if i not in (PAD_ID, BOS_ID, EOS_ID)]) |
| return restore_indentation(text) |
|
|
|
|
| def _block_repeated_ngrams(generated_ids, logits, ngram_size): |
| """Prohibe repetir literalmente un n-grama ya generado en esta misma |
| respuesta (tecnica de decoding tipo GPT-2/3, no un modelo de n-gramas: |
| el modelo que predice sigue siendo 100% transformer con KV-cache).""" |
| if ngram_size <= 0 or len(generated_ids) < ngram_size: |
| return logits |
| prefix = tuple(generated_ids[-(ngram_size - 1):]) |
| banned = set() |
| for i in range(len(generated_ids) - ngram_size + 1): |
| if tuple(generated_ids[i:i + ngram_size - 1]) == prefix: |
| banned.add(generated_ids[i + ngram_size - 1]) |
| if banned: |
| logits[0, list(banned)] = float("-inf") |
| return logits |
|
|
|
|
| |
| @torch.inference_mode() |
| def generate(idx, max_new_tokens, temperature, top_k, top_p, repetition_penalty, no_repeat_ngram_size=3): |
| past_key_values = None |
| cache_len = 0 |
|
|
| for _ in range(max_new_tokens): |
| total_len = idx.shape[1] |
|
|
| if total_len <= BLOCK_SIZE: |
| if past_key_values is None: |
| |
| logits, past_key_values = model(idx, use_cache=True) |
| cache_len = total_len |
| else: |
| |
| |
| last_token = idx[:, -1:] |
| logits, past_key_values = model( |
| last_token, |
| past_key_values=past_key_values, |
| use_cache=True, |
| pos_offset=cache_len, |
| ) |
| cache_len += 1 |
| logits = logits[:, -1, :] |
| else: |
| |
| |
| |
| idx_cond = idx[:, -BLOCK_SIZE:] |
| logits, past_key_values = model(idx_cond, use_cache=True) |
| cache_len = BLOCK_SIZE |
| logits = logits[:, -1, :] |
|
|
| logits = logits / max(temperature, 1e-5) |
|
|
| if repetition_penalty and repetition_penalty != 1.0: |
| |
| |
| unique_ids = torch.unique(idx[0]) |
| logits[0, unique_ids] /= repetition_penalty |
|
|
| logits = _block_repeated_ngrams(idx[0].tolist(), logits, no_repeat_ngram_size) |
|
|
| if top_k is not None and top_k > 0: |
| v, _ = torch.topk(logits, min(top_k, logits.size(-1))) |
| logits[logits < v[:, [-1]]] = float("-inf") |
|
|
| probs = F.softmax(logits, dim=-1) |
|
|
| if top_p is not None and 0 < top_p < 1: |
| sorted_probs, sorted_idx = torch.sort(probs, descending=True) |
| cum_probs = torch.cumsum(sorted_probs, dim=-1) |
| cutoff = cum_probs > top_p |
| cutoff[:, 1:] = cutoff[:, :-1].clone() |
| cutoff[:, 0] = False |
| sorted_probs[cutoff] = 0.0 |
| sorted_probs = sorted_probs / sorted_probs.sum(dim=-1, keepdim=True) |
| next_id = sorted_idx.gather(-1, torch.multinomial(sorted_probs, 1)) |
| else: |
| next_id = torch.multinomial(probs, num_samples=1) |
|
|
| idx = torch.cat([idx, next_id], dim=1) |
| if next_id.item() == EOS_ID: |
| break |
|
|
| return idx |
|
|
|
|
| def run_inference(text, max_new_tokens=None, temperature=None, top_k=None, top_p=None, repetition_penalty=None, no_repeat_ngram_size=None): |
| """Núcleo de generación, reutilizado por la UI de Gradio y por la API /generate. |
| No reduce calidad por estar en CPU: usa exactamente el mismo muestreo |
| (top_k + top_p + repetition_penalty + bloqueo de n-gramas repetidos) que |
| en la Celda 2 de entrenamiento, solo que ahora con KV-cache es notablemente |
| más rápido en respuestas largas.""" |
| max_new_tokens = int(max_new_tokens) if max_new_tokens else gen_defaults["max_new_tokens"] |
| temperature = float(temperature) if temperature is not None else gen_defaults["temperature"] |
| top_k = int(top_k) if top_k is not None else gen_defaults["top_k"] |
| top_p = float(top_p) if top_p is not None else gen_defaults["top_p"] |
| repetition_penalty = float(repetition_penalty) if repetition_penalty is not None else gen_defaults["repetition_penalty"] |
| no_repeat_ngram_size = int(no_repeat_ngram_size) if no_repeat_ngram_size is not None else gen_defaults.get("no_repeat_ngram_size", 3) |
|
|
| |
| |
| |
| MAX_TOKENS_HARD_LIMIT = 700 |
| max_new_tokens = max(1, min(max_new_tokens, MAX_TOKENS_HARD_LIMIT)) |
|
|
| if len(text) > MAX_INPUT_CHARS: |
| logger.warning(f"Input de {len(text)} caracteres recortado a {MAX_INPUT_CHARS}") |
| text = text[:MAX_INPUT_CHARS] |
|
|
| prefix = f"Usuario: {text}\nMTP: " |
| ids = [BOS_ID] + encode_text(prefix) |
| idx = torch.tensor([ids], dtype=torch.long, device=DEVICE) |
|
|
| |
| |
| |
| with _generation_lock: |
| out = generate(idx, max_new_tokens, temperature, top_k, top_p, repetition_penalty, no_repeat_ngram_size) |
|
|
| new_ids = out[0].tolist()[len(ids):] |
| return decode_ids(new_ids).strip() |
|
|
|
|
| def chat_fn(message, history, max_new_tokens, temperature, top_k, top_p, repetition_penalty): |
| return run_inference(message, max_new_tokens, temperature, top_k, top_p, repetition_penalty) |
|
|
|
|
| |
| with gr.Blocks(title="MTP-2.5 Chat") as demo: |
| gr.Markdown(f"# MTP-2.5\nModelo GPT (RoPE + SwiGLU + RMSNorm) entrenado desde cero, tokenizer BPE. Ejecutándose en {DEVICE.upper()}.") |
|
|
| with gr.Accordion("Parámetros de generación", open=False): |
| max_new_tokens_ui = gr.Slider(16, 4000, value=gen_defaults["max_new_tokens"], step=10, label="max_new_tokens") |
| temperature_ui = gr.Slider(0.1, 2.0, value=gen_defaults["temperature"], step=0.05, label="temperature") |
| top_k_ui = gr.Slider(0, 100, value=gen_defaults["top_k"], step=1, label="top_k") |
| top_p_ui = gr.Slider(0.1, 1.0, value=gen_defaults["top_p"], step=0.05, label="top_p") |
| repetition_penalty_ui = gr.Slider(1.0, 2.0, value=gen_defaults["repetition_penalty"], step=0.05, |
| label="repetition_penalty") |
|
|
| chatbot = gr.ChatInterface( |
| fn=chat_fn, |
| additional_inputs=[max_new_tokens_ui, temperature_ui, top_k_ui, top_p_ui, repetition_penalty_ui], |
| title=None, |
| examples=[ |
| ["Hola, ¿cómo estás?"], |
| ["¿Cuánto es 8 + 5?"], |
| ["Explícame qué es un algoritmo."], |
| ], |
| cache_examples=False, |
| ) |
|
|
| demo.queue(max_size=16) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| class GenerateRequest(BaseModel): |
| text: str |
| max_tokens: Optional[int] = None |
| temperature: Optional[float] = None |
| top_k: Optional[int] = None |
| top_p: Optional[float] = None |
| repetition_penalty: Optional[float] = None |
| no_repeat_ngram_size: Optional[int] = None |
|
|
|
|
| PORT = int(os.environ.get("PORT", 7860)) |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=PORT, |
| prevent_thread_lock=True, |
| ssr_mode=False, |
| app_kwargs={ |
| "middleware": [ |
| Middleware(CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_methods=["*"], allow_headers=["*"]), |
| ] |
| }, |
| ) |
|
|
| app = demo.app |
|
|
|
|
| @app.post("/generate") |
| def generate_endpoint(req: GenerateRequest): |
| if not req.text or not req.text.strip(): |
| return {"reply": "Escribe algo para que pueda responder."} |
| try: |
| reply = run_inference( |
| req.text, |
| max_new_tokens=req.max_tokens, |
| temperature=req.temperature, |
| top_k=req.top_k, |
| top_p=req.top_p, |
| repetition_penalty=req.repetition_penalty, |
| no_repeat_ngram_size=req.no_repeat_ngram_size, |
| ) |
| if not reply: |
| reply = "No pude generar una respuesta." |
| return {"reply": reply} |
| except Exception as e: |
| |
| |
| |
| |
| logger.error(f"Error generando respuesta: {e}\n{traceback.format_exc()}") |
| reply = f"Error del modelo: {e}" if DEBUG_ERRORS else "Ocurrió un error al generar la respuesta. Intenta de nuevo en un momento." |
| return {"reply": reply} |
|
|
|
|
| @app.get("/generate") |
| def generate_health(): |
| |
| return {"status": "ok", "info": "Usa POST con JSON {text, max_tokens, temperature}"} |
|
|
|
|
| @app.get("/health") |
| def health(): |
| |
| |
| |
| return { |
| "status": "ok", |
| "model": checkpoint["meta"]["model_name"], |
| "device": DEVICE, |
| "trained_examples": checkpoint["meta"]["trained_examples"], |
| } |
|
|
|
|
| |
| |
| |
| demo.block_thread() |