|
|
|
|
| """
|
| PamparV3 — Modelo principal con arquitectura 2D + Mixed Selectivity.
|
|
|
| Arquitectura:
|
| tok_emb [48K, 640]
|
| → TalamoInicial (LLAVES + attn_proj + context_conv)
|
| → terr_acts [B, L, 4], zona_acts [B, L, 52]
|
| → 4 streams inicializados desde tok_emb
|
| → [NivelProfundo × n_levels]
|
| cada nivel: attn compartida + re-routing
|
| + 1 FFN compartido + 4 ContextModulator (FiLM)
|
| + lateral gates (fibras blancas)
|
| → norm_f (RMSNorm)
|
| → lm_head (weight-tied con tok_emb)
|
|
|
| Mixed Selectivity: 1 FFN compartido por nivel, modulado por 63 indicadores
|
| contextuales (zona_acts + terr_acts + depth + conf + stream_id).
|
| La misma memoria se lee de formas diferentes según el contexto.
|
| Ahorro: ~32M params vs 4 FFN independientes por nivel.
|
|
|
| 4 streams × 5 niveles = grilla 2D donde:
|
| - La profundidad refina el significado (como capas corticales)
|
| - La anchura especializa por tipo de token (como áreas corticales)
|
| - Las lateral gates comunican áreas a cada nivel (como fibras blancas)
|
| - El re-routing adapta qué área lidera según el contexto acumulado
|
| - Los moduladores permiten mixed selectivity (misma neurona, roles múltiples)
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
|
|
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| import torch.utils.checkpoint
|
|
|
| from .bloques import NivelProfundo, RMSNorm
|
| from .config import PRESET_V3, ConfigV3
|
| from .talamo import TalamoInicial
|
|
|
| if TYPE_CHECKING:
|
| from .engrama_stream import BancoEngrama
|
|
|
|
|
| class PamparV3(nn.Module):
|
| """
|
| PAMPAr-Coder v3: arquitectura 2D con streams especializados y profundidad.
|
|
|
| ~110M parámetros con PRESET_V3 (dim=640, 5 niveles, vocab=48K).
|
| """
|
|
|
| def __init__(self, config: ConfigV3 = PRESET_V3):
|
| super().__init__()
|
| self.config = config
|
|
|
|
|
| self.tok_emb = nn.Embedding(config.vocab_size, config.dim)
|
| self.emb_drop = nn.Dropout(config.dropout)
|
|
|
|
|
| self.talamo = TalamoInicial(config)
|
|
|
|
|
| self.niveles = nn.ModuleList(
|
| [NivelProfundo(config, nivel_idx=i) for i in range(config.n_levels)]
|
| )
|
|
|
|
|
| self.norm_f = RMSNorm(config.dim)
|
|
|
|
|
| self.lm_head = nn.Linear(config.dim, config.vocab_size, bias=False)
|
|
|
|
|
|
|
| self.lm_head.weight = self.tok_emb.weight
|
|
|
|
|
| self._init_weights()
|
|
|
| def _init_weights(self) -> None:
|
| """Inicialización estilo GPT-NeoX / Llama: N(0, 0.02)."""
|
|
|
| def _init(module: nn.Module) -> None:
|
| if isinstance(module, nn.Linear):
|
| nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| if module.bias is not None:
|
| nn.init.zeros_(module.bias)
|
| elif isinstance(module, nn.Embedding):
|
| nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
|
|
| self.apply(_init)
|
|
|
| def registrar_tokenizer(self, tokenizer: object) -> None:
|
| """Registra el tokenizer en el Tálamo para que LLAVES funcione."""
|
| self.talamo.registrar_tokenizer(tokenizer)
|
|
|
| def _enable_kv_cache(self) -> None:
|
| """Enable KV cache mode for generation (inference only)."""
|
| for nivel in self.niveles:
|
| nivel.attn._use_kv_cache = True
|
| nivel.attn._kv_cache = None
|
| nivel.attn._start_pos = 0
|
|
|
| def _disable_kv_cache(self) -> None:
|
| """Disable KV cache and free cached tensors."""
|
| for nivel in self.niveles:
|
| nivel.attn._use_kv_cache = False
|
| nivel.attn._kv_cache = None
|
| nivel.attn._start_pos = 0
|
|
|
| def _set_cache_pos(self, pos: int) -> None:
|
| """Set the position offset for RoPE in all attention layers."""
|
| for nivel in self.niveles:
|
| nivel.attn._start_pos = pos
|
|
|
| def set_train_norm_clamp(self, enabled: bool) -> None:
|
| """Activa/desactiva norm clamping durante training en todos los niveles."""
|
| for nivel in self.niveles:
|
| nivel._train_norm_clamp = enabled
|
|
|
| def _combinar_streams(
|
| self,
|
| streams: List[torch.Tensor],
|
| terr_acts: torch.Tensor,
|
| ) -> torch.Tensor:
|
| """
|
| Combina los 4 streams en una representación unificada.
|
|
|
| Ponderada por activación territorial: el stream más activo domina.
|
| Normalizada para que los pesos sumen 1.
|
|
|
| Args:
|
| streams: [n_streams × [B, L, D]]
|
| terr_acts: [B, L, n_streams]
|
| Returns:
|
| x: [B, L, D]
|
| """
|
|
|
| weights = F.softmax(terr_acts, dim=-1)
|
| return sum(
|
| streams[t] * weights[:, :, t : t + 1] for t in range(self.config.n_streams)
|
| )
|
|
|
| def forward(
|
| self,
|
| input_ids: torch.Tensor,
|
| targets: Optional[torch.Tensor] = None,
|
| use_early_exit: bool = False,
|
| banco_engrama: Optional[BancoEngrama] = None,
|
| ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Dict]:
|
| """
|
| Forward pass de PamparV3.
|
|
|
| Args:
|
| input_ids: [B, L] token IDs
|
| targets: [B, L] labels (-100 = ignorar)
|
| use_early_exit: salir antes si confianza suficiente
|
| banco_engrama: banco de engramas para inyección (None = sin inyección)
|
|
|
| Returns:
|
| logits: [B, L, vocab_size]
|
| loss: scalar (si targets provisto)
|
| info: {'exit_nivel': int, 'terr_acts': tensor}
|
| """
|
| B, L = input_ids.shape
|
|
|
|
|
| x = self.emb_drop(self.tok_emb(input_ids))
|
|
|
|
|
| terr_acts, zona_acts = self.talamo(x, input_ids)
|
|
|
|
|
|
|
|
|
| streams: List[torch.Tensor] = [x.clone() for _ in range(self.config.n_streams)]
|
|
|
|
|
| info: Dict = {"exit_nivel": self.config.n_levels, "terr_acts": terr_acts}
|
|
|
| for i, nivel in enumerate(self.niveles):
|
| if self.config.use_checkpoint and self.training and not use_early_exit:
|
|
|
|
|
| def create_checkpoint_fn(n):
|
| def fn(*stream_tensors):
|
| s_list = list(stream_tensors[:-2])
|
| ta = stream_tensors[-2]
|
| za = stream_tensors[-1]
|
| new_s, new_ta, _ = n(
|
| s_list,
|
| ta,
|
| TalamoInicial.agregar_fn,
|
| zona_acts=za,
|
| )
|
| return (*new_s, new_ta)
|
|
|
| return fn
|
|
|
| result = torch.utils.checkpoint.checkpoint(
|
| create_checkpoint_fn(nivel),
|
| *streams,
|
| terr_acts,
|
| zona_acts,
|
| use_reentrant=False,
|
| )
|
| streams = list(result[: self.config.n_streams])
|
| terr_acts = result[self.config.n_streams]
|
| conf = 0.0
|
| else:
|
| streams, terr_acts, conf = nivel(
|
| streams,
|
| terr_acts,
|
| TalamoInicial.agregar_fn,
|
| banco_engrama=banco_engrama,
|
| zona_acts=zona_acts,
|
| )
|
|
|
|
|
| if use_early_exit and conf > self.config.umbral_exit:
|
| if i >= self.config.capas_min - 1:
|
| info["exit_nivel"] = i + 1
|
| break
|
|
|
|
|
| x_final = self._combinar_streams(streams, terr_acts)
|
|
|
|
|
| x_final = self.norm_f(x_final)
|
| logits = self.lm_head(x_final)
|
|
|
|
|
| loss = None
|
| if targets is not None:
|
| loss = F.cross_entropy(
|
| logits.reshape(-1, self.config.vocab_size),
|
| targets.reshape(-1),
|
| ignore_index=-100,
|
| )
|
|
|
| return logits, loss, info
|
|
|
| @torch.no_grad()
|
| def generate(
|
| self,
|
| prompt_ids: torch.Tensor,
|
| max_tokens: int = 256,
|
| temperature: float = 0.8,
|
| top_k: int = 50,
|
| top_p: float = 0.95,
|
| banco_engrama: Optional[BancoEngrama] = None,
|
| ) -> torch.Tensor:
|
| """
|
| Generación autoregresiva con KV cache y nucleus sampling.
|
|
|
| Usa prefill (procesa todo el prompt de una sola vez) +
|
| decode (un token por paso, reutilizando el KV cache).
|
|
|
| Args:
|
| prompt_ids: [1, L] prompt tokenizado
|
| max_tokens: máximo tokens a generar
|
| temperature: diversidad (menor = más determinista)
|
| top_k: Top-K sampling (0 = desactivado)
|
| top_p: Nucleus sampling (1.0 = desactivado)
|
| banco_engrama: banco de engramas para inyección adaptativa
|
|
|
| Returns:
|
| [1, L+N] tokens generados
|
| """
|
| self.eval()
|
| generated = prompt_ids.clone()
|
| prompt_len = prompt_ids.shape[1]
|
|
|
| try:
|
| self._enable_kv_cache()
|
|
|
|
|
| self._set_cache_pos(0)
|
| logits, _, _ = self.forward(
|
| prompt_ids,
|
| use_early_exit=False,
|
| banco_engrama=banco_engrama,
|
| )
|
| logits = logits[:, -1, :] / temperature
|
|
|
| for _ in range(max_tokens):
|
|
|
| if top_k > 0:
|
| v, _ = logits.topk(top_k)
|
| logits[logits < v[:, [-1]]] = float("-inf")
|
|
|
|
|
| if top_p < 1.0:
|
| sorted_logits, sorted_idx = logits.sort(descending=True)
|
| cumprobs = sorted_logits.softmax(dim=-1).cumsum(dim=-1)
|
| remove_mask = cumprobs - sorted_logits.softmax(dim=-1) > top_p
|
| sorted_logits[remove_mask] = float("-inf")
|
| logits = torch.zeros_like(logits).scatter(
|
| 1,
|
| sorted_idx,
|
| sorted_logits,
|
| )
|
|
|
|
|
| if torch.isnan(logits).any() or torch.isinf(logits).any():
|
| logits = torch.nan_to_num(logits, nan=0.0, posinf=1e4, neginf=-1e4)
|
|
|
| probs = F.softmax(logits, dim=-1)
|
| if torch.isnan(probs).any() or (probs < 0).any():
|
| probs = torch.ones_like(probs) / probs.shape[-1]
|
|
|
| next_tok = torch.multinomial(probs, 1)
|
| generated = torch.cat([generated, next_tok], dim=1)
|
|
|
|
|
| if generated.shape[0] == 1 and next_tok.item() == 0:
|
| break
|
|
|
|
|
| cur_pos = generated.shape[1] - 1
|
| if cur_pos >= self.config.max_seq_len:
|
| break
|
|
|
| self._set_cache_pos(cur_pos)
|
| logits, _, _ = self.forward(
|
| next_tok,
|
| use_early_exit=False,
|
| banco_engrama=banco_engrama,
|
| )
|
| logits = logits[:, -1, :] / temperature
|
|
|
| finally:
|
| self._disable_kv_cache()
|
|
|
| return generated
|
|
|
| def count_params(self) -> Dict[str, int]:
|
| """Cuenta parámetros por componente."""
|
| return {
|
| "embeddings": self.tok_emb.weight.numel(),
|
| "talamo_inicial": sum(p.numel() for p in self.talamo.parameters()),
|
| "niveles": sum(p.numel() for p in self.niveles.parameters()),
|
| "norm_f": sum(p.numel() for p in self.norm_f.parameters()),
|
| "total": sum(p.numel() for p in self.parameters()),
|
| "total_sin_embedding": sum(
|
| p.numel()
|
| for name, p in self.named_parameters()
|
| if "tok_emb" not in name
|
| ),
|
| }
|
|
|
| def describe(self) -> str:
|
| """Descripción legible de la arquitectura."""
|
| p = self.count_params()
|
| cfg = self.config
|
| mem = cfg.memory_estimate_mb()
|
| return (
|
| f"PamparV3\n"
|
| f" Arquitectura: {cfg.n_streams} streams × {cfg.n_levels} niveles (2D)\n"
|
| f" Dimensiones: dim={cfg.dim}, heads={cfg.n_heads} "
|
| f"(GQA {cfg.n_kv_heads} KV), ffn_hidden={cfg.ffn_hidden}\n"
|
| f" Vocab: {cfg.vocab_size:,} tokens, seq_len={cfg.max_seq_len}\n"
|
| f" Parámetros: {p['total'] / 1e6:.1f}M total "
|
| f"({p['total_sin_embedding'] / 1e6:.1f}M sin embedding)\n"
|
| f" VRAM model: {mem['model_fp16_mb']}MB (fp16)\n"
|
| f" VRAM training: {mem['training_total_mb']}MB (model+grad+Adam)\n"
|
| f" KV cache: {mem['kv_cache_inference_mb']}MB "
|
| f"(batch=1, seq=4096)\n"
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def crear_modelo_v3(config: ConfigV3 = PRESET_V3) -> PamparV3:
|
| """Crea un modelo PamparV3 con la configuración dada."""
|
| return PamparV3(config)
|
|
|