#!/usr/bin/env python3 """ visualizar_flujo.py — Visualizador interactivo del flujo de datos en PamparV3. Genera un HTML standalone con 4 paneles: 1. Embeddings iniciales (PCA 2D) — dónde vive cada token en el espacio 2. Mapa de atención por nivel — qué tokens miran a cuáles 3. Norma de activaciones por nivel/stream — cómo crece/decae la señal 4. Benchmark de velocidad y memoria Uso: python visualizar_flujo.py "Hola, me llamo Pampar" python visualizar_flujo.py --texto "def suma(a, b): return a + b" python visualizar_flujo.py # usa texto por defecto """ from __future__ import annotations import argparse import sys import time from pathlib import Path import torch import torch.nn.functional as F # ── Setup de paths ────────────────────────────────────────────────────────── ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(Path(__file__).parent)) # ── Captura de activaciones via hooks ─────────────────────────────────────── class ActivationCapture: """Registra hooks en PamparV3 y captura tensores del forward pass.""" def __init__(self): self.handles: list = [] self.embeddings: torch.Tensor | None = None # [B, L, dim] self.nivel_outputs: list[torch.Tensor] = [] # por nivel: [B, L, dim] self.attn_weights: list[torch.Tensor] = [] # por nivel: [B, H, L, L] self.stream_norms: list[list[float]] = [] # por nivel: [4 streams] def attach(self, model) -> None: """Registra hooks en el modelo.""" # Hook en embedding def hook_emb(module, inp, out): self.embeddings = out.detach().cpu() self.handles.append(model.tok_emb.register_forward_hook(hook_emb)) # Hook en cada NivelProfundo for i, nivel in enumerate(model.niveles): def make_nivel_hook(idx): def hook(module, inp, out): # out = (streams_list, terr_acts, conf) # streams_list es una lista de n_streams tensores [B, L, D] streams_out = out[0] if isinstance(out, (tuple, list)) else out if isinstance(streams_out, (list, tuple)): x = torch.stack([s.detach().cpu() for s in streams_out]).mean(0) else: x = streams_out.detach().cpu() self.nivel_outputs.append(x) # Norma por stream (cada cuarto del dim como proxy de stream) B, L, D = x.shape chunk = max(1, D // 4) norms = [ x[:, :, i * chunk : min((i + 1) * chunk, D)] .norm(dim=-1) .mean() .item() for i in range(4) ] self.stream_norms.append(norms) return hook self.handles.append(nivel.register_forward_hook(make_nivel_hook(i))) # Hook en la atención del nivel def make_attn_hook(idx): def hook(module, inp, out): # Recalcular pesos de atención desde el input x = inp[0] B, L, D = x.shape H = module.n_heads Hkv = module.n_kv_heads head_dim = module.head_dim # Q, K usando nombres reales del BloqueAttn q = module.q_proj(x).view(B, L, H, head_dim).transpose(1, 2) k = module.k_proj(x).view(B, L, Hkv, head_dim).transpose(1, 2) k = module._repeat_kv(k) # GQA expand scale = head_dim**-0.5 scores = ( torch.matmul(q.float(), k.float().transpose(-2, -1)) * scale ) # Máscara causal mask = torch.triu( torch.ones(L, L, device=x.device), diagonal=1 ).bool() scores = scores.masked_fill( mask.unsqueeze(0).unsqueeze(0), float("-inf") ) weights = F.softmax(scores, dim=-1) self.attn_weights.append(weights.detach().cpu()) return hook self.handles.append(nivel.attn.register_forward_hook(make_attn_hook(i))) def detach(self) -> None: for h in self.handles: h.remove() self.handles.clear() # ── PCA manual (sin sklearn) ──────────────────────────────────────────────── def pca_2d(matrix: torch.Tensor) -> torch.Tensor: """Reduce [N, D] a [N, 2] via PCA (SVD).""" m = matrix.float() m = m - m.mean(0, keepdim=True) _, _, V = torch.svd(m) return m @ V[:, :2] # ── Generación del HTML ───────────────────────────────────────────────────── def build_html( tokens: list[str], capture: ActivationCapture, text: str, elapsed_ms: float, mem_mb: float, ) -> str: n_tokens = len(tokens) n_levels = len(capture.attn_weights) # ── Datos embedding PCA ───────────────────────────────────────────────── emb = capture.embeddings[0] # [L, dim] if n_tokens >= 2: coords = pca_2d(emb).tolist() else: coords = [[0.0, 0.0]] * n_tokens emb_x = [c[0] for c in coords] emb_y = [c[1] for c in coords] emb_norm = emb.norm(dim=-1).tolist() # ── Datos atención — promedio de cabezas por nivel ─────────────────────── attn_data = [] for lvl_w in capture.attn_weights: # lvl_w: [B, H, L, L] → promedio de H → [L, L] avg = lvl_w[0].mean(0).tolist() attn_data.append(avg) # ── Normas por nivel/stream ────────────────────────────────────────────── stream_norms = capture.stream_norms # [[s0,s1,s2,s3], ...] por nivel # ── Serializar a JSON inline ───────────────────────────────────────────── import json tokens_json = json.dumps(tokens) emb_x_json = json.dumps(emb_x) emb_y_json = json.dumps(emb_y) emb_norm_json = json.dumps(emb_norm) attn_json = json.dumps(attn_data) stream_norms_json = json.dumps(stream_norms) n_levels_json = n_levels elapsed_json = elapsed_ms mem_json = mem_mb text_escaped = text.replace('"', '\\"') return f"""