PAMPAr-Coder / scripts /ghidra_trainer.py
lucas-mella's picture
feat: upload PAMPAr-Coder code and documentation
a2d6a0d verified
Raw
History Blame Contribute Delete
35.5 kB
#!/usr/bin/env python3
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Lucas Ricardo Mella Chillemi
"""
PAMPAr Ghidra-Trainer β€” SFT con monitorizaciΓ³n GhidraProbe en tiempo real.
Entrena PamparV3 con el dataset master_sft.jsonl (1253 ejemplos ΓΊnicos)
monitorizando con GhidraProbe cada N pasos para detectar regresiones
en routing, normas y balance de streams ANTES de que destruyan el modelo.
Lecciones de 18 rounds previos:
- NO train-norm-clamp (destruye gradientes β†’ Score 85β†’74)
- NO alpha-exit (descalibra la confianza)
- Proteger CE por encima de todo
- El GhidraProbe detecta problemas que el loss no muestra
Uso:
python scripts/ghidra_trainer.py
python scripts/ghidra_trainer.py --max-pasos 800 --probe-cada 50
python scripts/ghidra_trainer.py --data data/master_sft.jsonl --lr 2e-7
"""
from __future__ import annotations
import argparse
import dataclasses
from dataclasses import dataclass
import json
import logging
import math
import random
import sys
import time
from collections import deque
from pathlib import Path
from typing import Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.nn.utils as nn_utils
import sentencepiece as spm
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from pampar.coder.v3 import PamparV3, ConfigV3, PRESET_V3
from pampar.coder.v3.talamo import TalamoInicial
from pampar.coder.v3.llaves import clasificar_token
from pampar.coder.v3.zonas import ZONA_TERRITORIO
from pampar.coder.v3.ghidra_probe import GhidraProbe, STREAM_NAMES
logger = logging.getLogger("ghidra_trainer")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
_MARCADORES = ["### Solution:", "### Protocolo:", "### Scan:", "### Response:"]
# ─────────────────────────────────────────────────────────────────
# Data
# ─────────────────────────────────────────────────────────────────
def _cargar_datos(ruta: Path) -> list[str]:
textos: list[str] = []
with open(ruta, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
obj = json.loads(line)
texto = obj.get("text", "")
if texto:
textos.append(texto)
logger.info("Cargados %d ejemplos desde %s", len(textos), ruta.name)
return textos
def _tokenizar_con_mascara(
textos: list[str],
tokenizer: spm.SentencePieceProcessor,
max_len: int,
) -> list[tuple[list[int], list[bool]]]:
chunks: list[tuple[list[int], list[bool]]] = []
for texto in textos:
ids_full = tokenizer.Encode(texto)
if len(ids_full) < 8:
continue
ids_full = ids_full[: max_len + 1]
n_prefijo = len(ids_full)
for marcador in _MARCADORES:
pos = texto.find(marcador)
if pos >= 0:
ids_pre = tokenizer.Encode(texto[: pos + len(marcador)])
n_prefijo = min(len(ids_pre), len(ids_full))
break
mascara = [False] * n_prefijo + [True] * (len(ids_full) - n_prefijo)
chunks.append((ids_full, mascara))
return chunks
def _hacer_batch(
chunks: list[tuple[list[int], list[bool]]],
indices: list[int],
device: torch.device,
max_seq_len: int,
) -> tuple[torch.Tensor, torch.Tensor]:
sels = [chunks[i] for i in indices]
max_len = min(max(len(ids) for ids, _ in sels), max_seq_len + 1)
padded_ids: list[list[int]] = []
padded_mask: list[list[bool]] = []
for ids, mask in sels:
t = ids[:max_len]
m = mask[:max_len]
pad_len = max_len - len(t)
padded_ids.append(t + [0] * pad_len)
padded_mask.append(m + [False] * pad_len)
tokens = torch.tensor(padded_ids, dtype=torch.long, device=device)
mascara = torch.tensor(padded_mask, dtype=torch.bool, device=device)
return tokens, mascara
# ─────────────────────────────────────────────────────────────────
# Territory Table
# ─────────────────────────────────────────────────────────────────
def _build_territory_table(
tokenizer: spm.SentencePieceProcessor,
) -> torch.Tensor:
vocab_size = tokenizer.GetPieceSize()
table = torch.zeros(vocab_size, dtype=torch.long)
for token_id in range(vocab_size):
piece = tokenizer.IdToPiece(token_id)
zona, _conf = clasificar_token(piece)
table[token_id] = ZONA_TERRITORIO[zona].value
return table
# ─────────────────────────────────────────────────────────────────
# Losses (simplificado: CE + routing solo)
# ─────────────────────────────────────────────────────────────────
def forward_neuro(
model: PamparV3,
input_ids: torch.Tensor,
) -> tuple[torch.Tensor, dict]:
config = model.config
n_streams = config.n_streams
x = model.emb_drop(model.tok_emb(input_ids))
terr_acts, zona_acts_n0 = model.talamo(x, input_ids)
streams = [x.clone() for _ in range(n_streams)]
all_terr_acts: list[torch.Tensor] = [terr_acts]
all_stream_norms: list[torch.Tensor] = []
for nivel in model.niveles:
streams, terr_acts, _ = nivel(streams, terr_acts, TalamoInicial.agregar_fn)
all_terr_acts.append(terr_acts)
norms = torch.stack(
[streams[t].norm(dim=-1).mean() for t in range(n_streams)]
)
all_stream_norms.append(norms)
x_final = model._combinar_streams(streams, terr_acts)
x_final = model.norm_f(x_final)
logits = model.lm_head(x_final)
return logits, {
"all_terr_acts": all_terr_acts,
"all_stream_norms": all_stream_norms,
}
def calcular_loss_routing(
all_terr_acts: list[torch.Tensor],
input_ids: torch.Tensor,
territory_table: torch.Tensor,
focal_gamma: float = 0.0,
) -> torch.Tensor:
"""Routing loss con focal loss opcional (gamma>0 focaliza en tokens mal ruteados)."""
device = input_ids.device
weights = torch.tensor(
[min(10.0, (48000 / 169) ** 0.5),
1.0,
min(10.0, (48000 / 72) ** 0.5),
min(10.0, (48000 / 8) ** 0.5)],
device=device, dtype=torch.float32,
)
targets = territory_table.to(device)[input_ids]
total = torch.tensor(0.0, device=device)
for ta in all_terr_acts:
L_ta = ta.shape[1]
t = targets[:, :L_ta]
if focal_gamma > 0:
logprobs = F.log_softmax(ta.reshape(-1, 4), dim=-1)
t_flat = t.reshape(-1)
p_t = logprobs.exp().gather(1, t_flat.unsqueeze(1)).squeeze(1)
focal_w = (1.0 - p_t).detach() ** focal_gamma
per_token = F.nll_loss(logprobs, t_flat, weight=weights, reduction="none")
total = total + (focal_w * per_token).mean()
else:
total = total + F.cross_entropy(
ta.reshape(-1, 4), t.reshape(-1), weight=weights
)
return total / len(all_terr_acts)
def calcular_loss_balance(all_stream_norms: list[torch.Tensor]) -> torch.Tensor:
"""Coeficiente de variaciΓ³n: std/mean. Siempre positivo, bounded."""
total = torch.tensor(0.0, device=all_stream_norms[0].device)
for norms in all_stream_norms:
mean_n = norms.mean()
std_n = norms.std()
total = total + std_n / mean_n.clamp(min=1e-8)
return total / len(all_stream_norms)
# ─────────────────────────────────────────────────────────────────
# GhidraProbe Diagnosis (durante training)
# ─────────────────────────────────────────────────────────────────
@dataclass
class ProbeSnapshot:
"""MΓ©tricas clave extraΓ­das del GhidraProbe en un paso."""
paso: int
# Normas por nivel [5]
norms_per_level: list[float]
# Routing: % de tokens dominados por SEMA en nivel final
sema_dominance: float
# Routing diversity: std promedio de terr_acts
routing_std: float
# LLAVES utilizaciΓ³n
llaves_nonzero_pct: float
# Stream balance: ratio min/max norm en nivel final
stream_balance: float
# Lateral scales promedio
lateral_scales_avg: list[float]
@dataclass
class ProbeBaseline:
"""LΓ­nea base del modelo pre-entrenamiento para detectar regresiones."""
norms_per_level: list[float]
sema_dominance: float
routing_std: float
llaves_nonzero_pct: float
stream_balance: float
PROBE_SENTENCES = [
"def fibonacci(n):",
"import os\nfrom pathlib import Path",
"if x > 0 and y < 10:",
"class DataLoader:",
]
def _run_probe(
probe: GhidraProbe,
modelo: PamparV3,
tokenizer: spm.SentencePieceProcessor,
device: torch.device,
paso: int,
) -> ProbeSnapshot:
"""Ejecuta GhidraProbe sobre frases de test y extrae mΓ©tricas clave."""
modelo.eval()
# Promediar mΓ©tricas sobre varias frases para estabilidad
all_norms: list[list[float]] = []
all_sema_pct: list[float] = []
all_routing_std: list[float] = []
all_llaves_pct: list[float] = []
all_balance: list[float] = []
all_lat_scales: list[list[float]] = []
for sentence in PROBE_SENTENCES:
probe.reset()
ids = tokenizer.Encode(sentence)
input_ids = torch.tensor([ids], device=device)
with torch.no_grad():
modelo(input_ids)
cap = probe.report()
# Normas por nivel
level_norms = []
for nc in cap.niveles:
avg_norm = sum(nc.streams_out_norms) / max(len(nc.streams_out_norms), 1)
level_norms.append(avg_norm)
all_norms.append(level_norms)
# SEMA dominance en nivel final
if cap.niveles:
last = cap.niveles[-1]
terr = last.terr_acts_mean
total_terr = sum(terr) if terr else 1.0
sema_pct = (terr[1] / total_terr * 100) if len(terr) > 1 and total_terr > 0 else 0.0
all_sema_pct.append(sema_pct)
# Routing std
if terr:
mean_t = sum(terr) / len(terr)
std_t = (sum((v - mean_t) ** 2 for v in terr) / len(terr)) ** 0.5
all_routing_std.append(std_t)
# Stream balance
if last.streams_out_norms:
mn = min(last.streams_out_norms)
mx = max(last.streams_out_norms)
all_balance.append(mn / mx if mx > 0 else 0.0)
# Lateral scales
if last.lateral_scales:
all_lat_scales.append(last.lateral_scales)
# LLAVES
if cap.talamo:
all_llaves_pct.append(cap.talamo.llaves_nonzero_pct)
modelo.train()
# Promediar
n_levels = max(len(n) for n in all_norms) if all_norms else 5
avg_norms = []
for lvl in range(n_levels):
vals = [n[lvl] for n in all_norms if lvl < len(n)]
avg_norms.append(sum(vals) / max(len(vals), 1))
avg_lat = []
if all_lat_scales:
n_s = len(all_lat_scales[0])
for s in range(n_s):
vals = [ls[s] for ls in all_lat_scales if s < len(ls)]
avg_lat.append(sum(vals) / max(len(vals), 1))
return ProbeSnapshot(
paso=paso,
norms_per_level=avg_norms,
sema_dominance=sum(all_sema_pct) / max(len(all_sema_pct), 1),
routing_std=sum(all_routing_std) / max(len(all_routing_std), 1),
llaves_nonzero_pct=sum(all_llaves_pct) / max(len(all_llaves_pct), 1),
stream_balance=sum(all_balance) / max(len(all_balance), 1),
lateral_scales_avg=avg_lat,
)
def _print_probe_report(
snap: ProbeSnapshot,
baseline: Optional[ProbeBaseline] = None,
) -> None:
"""Imprime reporte del GhidraProbe con deltas vs baseline."""
print(f"\n {'─' * 60}")
print(f" GHIDRA PROBE @ paso {snap.paso}")
print(f" {'─' * 60}")
# Normas por nivel
norm_parts = []
for i, n in enumerate(snap.norms_per_level):
delta = ""
if baseline and i < len(baseline.norms_per_level):
d = n - baseline.norms_per_level[i]
sign = "+" if d >= 0 else ""
color = "\033[31m" if abs(d) / max(baseline.norms_per_level[i], 1) > 0.2 else "\033[32m"
delta = f" ({color}{sign}{d:.1f}\033[0m)"
norm_parts.append(f"N{i}={n:.1f}{delta}")
print(f" Normas: {' | '.join(norm_parts)}")
# SEMA dominance
sema_delta = ""
if baseline:
d = snap.sema_dominance - baseline.sema_dominance
sign = "+" if d >= 0 else ""
color = "\033[31m" if d > 5 else "\033[32m" if d < -2 else "\033[33m"
sema_delta = f" ({color}{sign}{d:.1f}%\033[0m)"
sema_color = "\033[31m" if snap.sema_dominance > 60 else "\033[33m" if snap.sema_dominance > 40 else "\033[32m"
print(f" SEMA dom: {sema_color}{snap.sema_dominance:.1f}%\033[0m{sema_delta}")
# Routing diversity
rstd_delta = ""
if baseline:
d = snap.routing_std - baseline.routing_std
sign = "+" if d >= 0 else ""
rstd_delta = f" ({sign}{d:.4f})"
print(f" Route std: {snap.routing_std:.4f}{rstd_delta}")
# LLAVES
ll_delta = ""
if baseline:
d = snap.llaves_nonzero_pct - baseline.llaves_nonzero_pct
sign = "+" if d >= 0 else ""
ll_delta = f" ({sign}{d:.1f}%)"
print(f" LLAVES: {snap.llaves_nonzero_pct:.1f}% non-zero{ll_delta}")
# Stream balance
bal_delta = ""
if baseline:
d = snap.stream_balance - baseline.stream_balance
sign = "+" if d >= 0 else ""
bal_delta = f" ({sign}{d:.3f})"
bal_color = "\033[32m" if snap.stream_balance > 0.5 else "\033[33m" if snap.stream_balance > 0.2 else "\033[31m"
print(f" Balance: {bal_color}{snap.stream_balance:.3f}\033[0m{bal_delta}")
# Lateral scales
if snap.lateral_scales_avg:
scales_str = " | ".join(
f"{STREAM_NAMES[i]}={v:.4f}" for i, v in enumerate(snap.lateral_scales_avg)
)
print(f" Lat scale: {scales_str}")
print(f" {'─' * 60}")
def _check_regression(
snap: ProbeSnapshot,
baseline: ProbeBaseline,
history: list[ProbeSnapshot],
) -> list[str]:
"""Detecta seΓ±ales de regresiΓ³n comparando con baseline."""
warnings: list[str] = []
# 1. Normas explotando (>2x baseline en cualquier nivel)
for i, n in enumerate(snap.norms_per_level):
if i < len(baseline.norms_per_level):
ratio = n / max(baseline.norms_per_level[i], 1)
if ratio > 2.0:
warnings.append(
f"NORMA N{i} explotando: {n:.1f} ({ratio:.1f}x baseline)"
)
# 2. SEMA dominance aumentando (colapso territorial)
if snap.sema_dominance > baseline.sema_dominance + 10:
warnings.append(
f"SEMA dominance subiendo: {snap.sema_dominance:.1f}% "
f"(baseline {baseline.sema_dominance:.1f}%)"
)
# 3. Stream balance degradando
if snap.stream_balance < baseline.stream_balance * 0.5:
warnings.append(
f"Stream balance degradando: {snap.stream_balance:.3f} "
f"(baseline {baseline.stream_balance:.3f})"
)
# 4. Tendencia de las ΓΊltimas 3 probes (normas subiendo consistentemente)
if len(history) >= 3:
last3 = history[-3:]
for lvl in range(min(5, len(snap.norms_per_level))):
vals = [h.norms_per_level[lvl] for h in last3 if lvl < len(h.norms_per_level)]
if len(vals) == 3 and all(vals[j] < vals[j + 1] for j in range(2)):
growth = vals[-1] / max(vals[0], 1)
if growth > 1.3:
warnings.append(
f"N{lvl} normas subiendo 3 probes consecutivas "
f"({vals[0]:.0f}β†’{vals[-1]:.0f})"
)
return warnings
# ─────────────────────────────────────────────────────────────────
# Training Step
# ─────────────────────────────────────────────────────────────────
def paso_entrenamiento(
modelo: PamparV3,
optimizer: torch.optim.Optimizer,
tokens: torch.Tensor,
mascara: torch.Tensor,
max_grad_norm: float,
alpha_diff: float,
alpha_balance: float,
territory_table: torch.Tensor,
warmup_factor: float = 1.0,
focal_gamma: float = 0.0,
) -> dict[str, float]:
"""Un paso de entrenamiento: CE + routing + balance (sin exit, sin norm)."""
modelo.train()
optimizer.zero_grad(set_to_none=True)
input_ids = tokens[:, :-1]
targets = tokens[:, 1:]
loss_mask = mascara[:, 1:]
logits, neuro_info = forward_neuro(modelo, input_ids)
B, T, V = logits.shape
# CE Loss principal
targets_masked = targets.masked_fill(~loss_mask, -100)
loss_ce = F.cross_entropy(
logits.reshape(B * T, V),
targets_masked.reshape(B * T),
ignore_index=-100,
)
# Solo routing + balance (lecciones de 18 rounds: no exit, no norm train)
loss_diff = calcular_loss_routing(
neuro_info["all_terr_acts"], input_ids, territory_table,
focal_gamma=focal_gamma,
)
loss_balance = calcular_loss_balance(neuro_info["all_stream_norms"])
wf = warmup_factor
loss = (
loss_ce
+ wf * alpha_diff * loss_diff
+ wf * alpha_balance * loss_balance
)
# Si CE_mask era todo False (ej: agents sin marcador), CE=0 β†’ no entrenar
n_valid = loss_mask.sum().item()
if n_valid == 0:
return {"ce": 0.0, "diff": 0.0, "balance": 0.0, "total": 0.0, "skipped": True}
if loss.isnan() or loss.isinf():
logger.warning("Loss inestable, skipping step")
return {"ce": 0.0, "diff": 0.0, "balance": 0.0, "total": 0.0, "skipped": True}
loss.backward()
nn_utils.clip_grad_norm_(modelo.parameters(), max_grad_norm)
optimizer.step()
return {
"ce": float(loss_ce.detach()),
"diff": float(loss_diff.detach()),
"balance": float(loss_balance.detach()),
"total": float(loss.detach()),
"skipped": False,
}
# ─────────────────────────────────────────────────────────────────
# Utils
# ─────────────────────────────────────────────────────────────────
def _cosine_lr(
paso: int, warmup: int, total: int, lr_max: float, lr_min: float,
) -> float:
if paso < warmup:
return lr_max * (paso + 1) / warmup
progreso = (paso - warmup) / max(1, total - warmup)
return lr_min + 0.5 * (lr_max - lr_min) * (1 + math.cos(math.pi * progreso))
def _guardar(
ruta: Path, modelo: PamparV3, optimizer: torch.optim.Optimizer, paso: int,
) -> None:
ruta.parent.mkdir(parents=True, exist_ok=True)
torch.save(
{
"modelo": modelo.state_dict(),
"optimizer": optimizer.state_dict(),
"paso_global": paso,
"config": dataclasses.asdict(modelo.config),
"tipo": "ghidra_trainer",
},
ruta,
)
logger.info("Checkpoint guardado -> %s (paso %d)", ruta.name, paso)
# ─────────────────────────────────────────────────────────────────
# CLI + main
# ─────────────────────────────────────────────────────────────────
def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="PAMPAr Ghidra-Trainer: SFT con monitoreo GhidraProbe",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
p.add_argument(
"--checkpoint-in", type=Path,
default=ROOT / "checkpoints" / "v3_neuro_v9.pt",
)
p.add_argument(
"--checkpoint-out", type=Path,
default=ROOT / "checkpoints" / "v3_ghidra_v1.pt",
)
p.add_argument(
"--tokenizer", type=Path,
default=ROOT / "data" / "tokenizer" / "pampar_48k.model",
)
p.add_argument(
"--data", type=Path,
default=ROOT / "data" / "master_sft.jsonl",
)
# Learning rates
p.add_argument("--lr", type=float, default=3e-7)
p.add_argument("--lr-min", type=float, default=3e-8)
p.add_argument("--lr-routing", type=float, default=1e-3)
p.add_argument("--lr-routing-min", type=float, default=1e-4)
# Training config
p.add_argument("--warmup", type=int, default=30)
p.add_argument("--max-pasos", type=int, default=1000)
p.add_argument("--epochs", type=int, default=20)
p.add_argument("--batch-size", type=int, default=1)
p.add_argument("--seq-len", type=int, default=256)
p.add_argument("--max-grad-norm", type=float, default=1.0)
p.add_argument("--guardar-cada", type=int, default=200)
# Aux losses (conservador: solo routing + balance)
p.add_argument("--alpha-diff", type=float, default=0.5)
p.add_argument("--alpha-balance", type=float, default=0.05)
p.add_argument("--focal-gamma", type=float, default=0.0,
help="Focal loss gamma (0=standard CE, 2+=focus on wrong tokens)")
p.add_argument("--aux-warmup", type=int, default=50)
# GhidraProbe
p.add_argument("--probe-cada", type=int, default=50,
help="Cada cuantos pasos ejecutar GhidraProbe")
p.add_argument("--abort-on-regression", action="store_true",
help="Detener si GhidraProbe detecta regresion severa")
p.add_argument("--device", type=str, default="auto")
p.add_argument("--seed", type=int, default=42)
return p.parse_args()
def main() -> None:
args = _parse_args()
random.seed(args.seed)
torch.manual_seed(args.seed)
device = torch.device(
"cuda"
if args.device == "auto" and torch.cuda.is_available()
else args.device
if args.device != "auto"
else "cpu"
)
logger.info("Device: %s", device)
if device.type == "cuda":
torch.cuda.manual_seed(args.seed)
logger.info(
"GPU: %s (%.1f GiB)",
torch.cuda.get_device_name(0),
torch.cuda.get_device_properties(0).total_memory / 1e9,
)
# Tokenizer
tok = spm.SentencePieceProcessor()
tok.Load(str(args.tokenizer))
logger.info("Tokenizer vocab=%d", tok.GetPieceSize())
# Modelo
if not args.checkpoint_in.exists():
logger.error("Checkpoint no encontrado: %s", args.checkpoint_in)
sys.exit(1)
payload = torch.load(
args.checkpoint_in, map_location=device, weights_only=False,
)
config = ConfigV3(**payload["config"]) if "config" in payload else PRESET_V3
modelo = PamparV3(config).to(device)
modelo.load_state_dict(payload["modelo"])
modelo.registrar_tokenizer(tok)
logger.info(
"Cargado '%s' (tipo: %s)", args.checkpoint_in.name, payload.get("tipo", "?"),
)
del payload
n_params = sum(p.numel() for p in modelo.parameters() if p.requires_grad)
logger.info("PamparV3 %.1fM params", n_params / 1e6)
# Datos
if not args.data.exists():
logger.error("Dataset no encontrado: %s", args.data)
sys.exit(1)
textos = _cargar_datos(args.data)
chunks = _tokenizar_con_mascara(textos, tok, args.seq_len)
logger.info("Chunks tokenizados: %d (seq_len=%d)", len(chunks), args.seq_len)
del textos
pct_solution = sum(sum(m) for _, m in chunks) / max(
1, sum(len(ids) for ids, _ in chunks),
)
logger.info("Porcentaje tokens en zona Solution: %.1f%%", pct_solution * 100)
# Territory table
territory_table = _build_territory_table(tok)
# ── GhidraProbe setup ─────────────────────────────────────────
probe = GhidraProbe(modelo)
logger.info("GhidraProbe instalado (%d hooks)", len(probe._hooks))
# Capturar baseline ANTES de entrenar
logger.info("Capturando baseline GhidraProbe...")
snap_baseline = _run_probe(probe, modelo, tok, device, paso=0)
baseline = ProbeBaseline(
norms_per_level=snap_baseline.norms_per_level[:],
sema_dominance=snap_baseline.sema_dominance,
routing_std=snap_baseline.routing_std,
llaves_nonzero_pct=snap_baseline.llaves_nonzero_pct,
stream_balance=snap_baseline.stream_balance,
)
_print_probe_report(snap_baseline)
probe_history: list[ProbeSnapshot] = [snap_baseline]
# ── Optimizer ─────────────────────────────────────────────────
routing_names = {"talamo", "talamo_nivel", "lateral"}
routing_params: list[nn.Parameter] = []
main_params: list[nn.Parameter] = []
for name, p in modelo.named_parameters():
parts = name.split(".")
if any(rn in parts for rn in routing_names):
routing_params.append(p)
else:
main_params.append(p)
n_routing = sum(p.numel() for p in routing_params)
n_main = sum(p.numel() for p in main_params)
logger.info(
"Param groups: routing=%.1fK (lr=%.1e) | main=%.1fM (lr=%.1e)",
n_routing / 1e3, args.lr_routing, n_main / 1e6, args.lr,
)
optimizer = torch.optim.AdamW(
[
{"params": main_params, "lr": args.lr},
{"params": routing_params, "lr": args.lr_routing},
],
betas=(0.9, 0.95),
weight_decay=0.01,
eps=1e-8,
)
pasos_por_epoch = max(1, len(chunks) // args.batch_size)
total_pasos = min(args.max_pasos, args.epochs * pasos_por_epoch)
logger.info(
"Ghidra-Training: %d pasos | %d chunks | "
"alphas: diff=%.3f balance=%.3f | focal_gamma=%.1f | probe cada %d pasos",
total_pasos, len(chunks),
args.alpha_diff, args.alpha_balance, args.focal_gamma, args.probe_cada,
)
# ── Training Loop ─────────────────────────────────────────────
paso = 0
effective_steps = 0
t0 = time.time()
losses_ce: deque[float] = deque(maxlen=100)
losses_total: deque[float] = deque(maxlen=100)
best_ce = float("inf")
best_paso = 0
aborted = False
try:
for epoch in range(args.epochs):
idx = list(range(len(chunks)))
random.shuffle(idx)
logger.info("== Epoch %d/%d ==", epoch + 1, args.epochs)
for i in range(0, len(idx) - args.batch_size + 1, args.batch_size):
batch_idx = idx[i : i + args.batch_size]
tokens, mascara = _hacer_batch(
chunks, batch_idx, device, args.seq_len,
)
# LR scheduling
lr_main = _cosine_lr(
paso, args.warmup, total_pasos, args.lr, args.lr_min,
)
lr_rout = _cosine_lr(
paso, args.warmup, total_pasos,
args.lr_routing, args.lr_routing_min,
)
optimizer.param_groups[0]["lr"] = lr_main
optimizer.param_groups[1]["lr"] = lr_rout
wf = min(1.0, paso / max(1, args.aux_warmup))
loss_dict = paso_entrenamiento(
modelo, optimizer, tokens, mascara,
args.max_grad_norm, args.alpha_diff, args.alpha_balance,
territory_table, warmup_factor=wf,
focal_gamma=args.focal_gamma,
)
if not loss_dict.get("skipped", False) and loss_dict["ce"] > 0:
losses_ce.append(loss_dict["ce"])
losses_total.append(loss_dict["total"])
effective_steps += 1
# Track best CE
avg_ce = sum(losses_ce) / len(losses_ce)
if avg_ce < best_ce and paso > args.warmup:
best_ce = avg_ce
best_paso = paso
paso += 1
# Log cada 10 pasos
if paso % 10 == 0:
avg_ce = sum(losses_ce) / max(1, len(losses_ce))
elapsed = time.time() - t0
logger.info(
"paso %4d/%d | CE=%.3f diff=%.3f bal=%.3f "
"total=%.3f lr=%.1e (%.1f p/s)",
paso, total_pasos,
loss_dict["ce"], loss_dict["diff"],
loss_dict["balance"], avg_ce,
lr_rout, paso / elapsed,
)
# GhidraProbe cada N pasos
if paso % args.probe_cada == 0:
snap = _run_probe(probe, modelo, tok, device, paso)
_print_probe_report(snap, baseline)
probe_history.append(snap)
# Check regression
warnings = _check_regression(snap, baseline, probe_history)
if warnings:
print(f"\n \033[33m{'!' * 40}")
print(f" GHIDRA WARNINGS @ paso {paso}:")
for w in warnings:
print(f" - {w}")
print(f" {'!' * 40}\033[0m\n")
if args.abort_on_regression and len(warnings) >= 2:
logger.error(
"Abortando: %d warnings de regresion", len(warnings),
)
aborted = True
break
# Guardar checkpoint periΓ³dico
if paso % args.guardar_cada == 0:
_guardar(args.checkpoint_out, modelo, optimizer, paso)
if paso >= args.max_pasos:
break
if paso >= args.max_pasos or aborted:
break
except KeyboardInterrupt:
logger.info("Interrumpido, guardando...")
# ── Cleanup GhidraProbe ───────────────────────────────────────
probe.detach()
# ── Final probe + save ────────────────────────────────────────
probe_final = GhidraProbe(modelo)
snap_final = _run_probe(probe_final, modelo, tok, device, paso)
probe_final.detach()
_guardar(args.checkpoint_out, modelo, optimizer, paso)
# ── Reporte final ─────────────────────────────────────────────
elapsed = time.time() - t0
avg_ce = sum(losses_ce) / max(1, len(losses_ce))
print(f"\n{'=' * 60}")
print(f" GHIDRA-TRAINING COMPLETADO")
print(f"{'=' * 60}")
print(f" Pasos: {paso} ({effective_steps} efectivos)")
print(f" Tiempo: {int(elapsed // 60)}m{int(elapsed % 60):02d}s")
print(f" Loss CE avg: {avg_ce:.3f}")
print(f" PPL: {math.exp(min(avg_ce, 10)):.1f}")
print(f" Best CE avg: {best_ce:.3f} (paso {best_paso})")
print(f" Checkpoint: {args.checkpoint_out}")
if aborted:
print(f" \033[31mABORTADO por regresion detectada\033[0m")
print(f"\n --- Baseline vs Final ---")
print(f" {'Metrica':<20} {'Baseline':>10} {'Final':>10} {'Delta':>10}")
print(f" {'─' * 54}")
for i in range(len(snap_final.norms_per_level)):
b = baseline.norms_per_level[i] if i < len(baseline.norms_per_level) else 0
f_val = snap_final.norms_per_level[i]
print(f" {'Norm N' + str(i):<20} {b:>10.1f} {f_val:>10.1f} {f_val - b:>+10.1f}")
print(f" {'SEMA dominance':<20} {baseline.sema_dominance:>9.1f}% {snap_final.sema_dominance:>9.1f}% {snap_final.sema_dominance - baseline.sema_dominance:>+9.1f}%")
print(f" {'Routing std':<20} {baseline.routing_std:>10.4f} {snap_final.routing_std:>10.4f} {snap_final.routing_std - baseline.routing_std:>+10.4f}")
print(f" {'Stream balance':<20} {baseline.stream_balance:>10.3f} {snap_final.stream_balance:>10.3f} {snap_final.stream_balance - baseline.stream_balance:>+10.3f}")
# Guardar historial de probes como JSON
probe_log = args.checkpoint_out.with_suffix(".probe.json")
probe_entries = []
for s in probe_history + [snap_final]:
probe_entries.append({
"paso": s.paso,
"norms": s.norms_per_level,
"sema_dom": s.sema_dominance,
"routing_std": s.routing_std,
"llaves_pct": s.llaves_nonzero_pct,
"balance": s.stream_balance,
})
with open(probe_log, "w", encoding="utf-8") as f:
json.dump(probe_entries, f, indent=2)
print(f"\n Probe log: {probe_log.name}")
print(f"\n Verificar con Brain Scanner:")
print(
f" python -X utf8 scripts/brain_scanner.py --suite --device cuda"
f" --checkpoint {args.checkpoint_out}",
)
print(f"{'=' * 60}\n")
if __name__ == "__main__":
main()