topoexploit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topoexploit-0.1.0.dist-info/METADATA +523 -0
- topoexploit-0.1.0.dist-info/RECORD +23 -0
- topoexploit-0.1.0.dist-info/WHEEL +5 -0
- topoexploit-0.1.0.dist-info/entry_points.txt +6 -0
- topoexploit-0.1.0.dist-info/licenses/LICENSE +661 -0
- topoexploit-0.1.0.dist-info/top_level.txt +1 -0
- topogpt3/__init__.py +125 -0
- topogpt3/__main__.py +49 -0
- topogpt3/api_server.py +1178 -0
- topogpt3/continuation.py +114 -0
- topogpt3/ewc.py +369 -0
- topogpt3/exploitgym_config.py +130 -0
- topogpt3/exploitgym_loader.py +416 -0
- topogpt3/hodge_cm.py +160 -0
- topogpt3/inference.py +731 -0
- topogpt3/inference_hrm.py +1502 -0
- topogpt3/jlens.py +958 -0
- topogpt3/lens_model.py +389 -0
- topogpt3/merged_config.py +187 -0
- topogpt3/model.py +3824 -0
- topogpt3/structured_output.py +251 -0
- topogpt3/train.py +1939 -0
- topogpt3/vanilla_control.py +240 -0
topogpt3/train.py
ADDED
|
@@ -0,0 +1,1939 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TopoGPT3: Grassmannian / Berry-Holonomy extension of TopoGPT2
|
|
4
|
+
|
|
5
|
+
Author: Gris Iscomeback
|
|
6
|
+
License: GPL v3
|
|
7
|
+
|
|
8
|
+
Lo nuevo respecto a model.py
|
|
9
|
+
---------------------------------
|
|
10
|
+
1. Espacio base: Grassmanniana Gr(r, N) sobre el tensor de kernels espectrales
|
|
11
|
+
K(theta) en C^{N_f x N_c}. El estado geometrico vive en U_r(theta) en St(r,N)/U(r),
|
|
12
|
+
con r elegido dinamicamente por el "elbow" del espectro singular de K.
|
|
13
|
+
2. Fisher gap funcional: Delta_F(theta) = lambda_r(Sigma_F) - lambda_{r+1}(Sigma_F)
|
|
14
|
+
estimado por covarianza empirica de gradientes (mini-batch) o por scores.
|
|
15
|
+
3. Conexion de Berry discreta: A_n = i * U_n^dagger (U_{n+1} - U_n)
|
|
16
|
+
y holonomia acumulada U_Gamma = P prod_n exp(-i A_n) en U(r).
|
|
17
|
+
4. Distancia de conjugacion en SU(2) (cuaternionico, r=1 efectivo):
|
|
18
|
+
d_conj(U1, U2) = min_{g in SU(2)} || U1 - g U2 g^{-1} ||_F
|
|
19
|
+
5. Winding W como proxy heuristico barato (rol secundario).
|
|
20
|
+
6. Curriculum por dataset, de mas simple a mas dificil:
|
|
21
|
+
Tier 1: CodeAlpaca (instrucciones cortas)
|
|
22
|
+
Tier 2: Code-Feedback-Filtered (chat / explicacion paso a paso)
|
|
23
|
+
Tier 3: Magicoder-Evol-Instruct-110K (problemas complejos)
|
|
24
|
+
Tier 4: Tiny-The-Stack (codigo real multilenguaje)
|
|
25
|
+
Cada tier mantiene splits train / val / holdout *disjuntos*.
|
|
26
|
+
El conjunto HOLDOUT nunca se ve durante entrenamiento; se usa
|
|
27
|
+
solo para medir generalizacion verdadera al final de cada tier
|
|
28
|
+
y al final del pipeline.
|
|
29
|
+
|
|
30
|
+
Diseno
|
|
31
|
+
------
|
|
32
|
+
- Reutiliza la arquitectura TopoGPT2 (tokenizador BPE, modelo cuaternionico
|
|
33
|
+
con torus + spectral autoencoder) cargandola dinamicamente desde model.py.
|
|
34
|
+
- Anade encima la maquinaria geometrica nueva en GrassmannianTracker.
|
|
35
|
+
- Entrenamiento orquestado por TopoGPT3Trainer que itera 4 tiers en orden,
|
|
36
|
+
con seq_len progresivo y evaluacion en holdout disjunto.
|
|
37
|
+
|
|
38
|
+
CLI
|
|
39
|
+
---
|
|
40
|
+
python train.py --prepare-data descarga + tokeniza los 4 tiers
|
|
41
|
+
python train.py --train entrena el curriculum completo
|
|
42
|
+
python train.py --train --start-tier 2 arranca desde un tier concreto
|
|
43
|
+
python train.py --eval-holdout evalua sobre holdouts disjuntos
|
|
44
|
+
python train.py --resume reanuda desde el ultimo checkpoint
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
import math
|
|
53
|
+
import os
|
|
54
|
+
import sys
|
|
55
|
+
import time
|
|
56
|
+
from collections import deque
|
|
57
|
+
from dataclasses import asdict, dataclass, field
|
|
58
|
+
from datetime import datetime
|
|
59
|
+
from pathlib import Path
|
|
60
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
61
|
+
|
|
62
|
+
import numpy as np
|
|
63
|
+
import torch
|
|
64
|
+
import torch.nn as nn
|
|
65
|
+
import torch.nn.functional as F
|
|
66
|
+
|
|
67
|
+
from .model import (
|
|
68
|
+
BPETokenizer,
|
|
69
|
+
QuaternionSpectralLayer,
|
|
70
|
+
SpectralAutoencoder,
|
|
71
|
+
TopoGPT2,
|
|
72
|
+
TopoGPT2Config,
|
|
73
|
+
set_seed,
|
|
74
|
+
setup_logger,
|
|
75
|
+
)
|
|
76
|
+
from .exploitgym_loader import ExploitGymDataLoader, ExploitGymLoaderConfig
|
|
77
|
+
from .exploitgym_config import TopoExploitConfig
|
|
78
|
+
from .merged_config import TopoMergedConfig
|
|
79
|
+
from .ewc import EWCRegularizer, ReplayBuffer
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# CONFIGURACION
|
|
84
|
+
# ============================================================================
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class TopoGPT3Config:
|
|
88
|
+
"""Configuracion del pipeline TopoGPT3 (Grassmanniana + curriculum)."""
|
|
89
|
+
|
|
90
|
+
# --- Modelo: hereda TopoGPT2 ---
|
|
91
|
+
SCALE: str = "small" # micro | small | medium | gpt2
|
|
92
|
+
DEVICE: str = "cuda" if torch.cuda.is_available() else "cpu"
|
|
93
|
+
RANDOM_SEED: int = 42
|
|
94
|
+
USE_AMP: bool = True
|
|
95
|
+
GRADIENT_CHECKPOINTING: bool = True
|
|
96
|
+
|
|
97
|
+
# --- Hyperparams de entrenamiento ---
|
|
98
|
+
BATCH_SIZE: int = 4
|
|
99
|
+
GRAD_ACCUM_STEPS: int = 8
|
|
100
|
+
LEARNING_RATE: float = 3e-4
|
|
101
|
+
WEIGHT_DECAY: float = 0.1
|
|
102
|
+
GRADIENT_CLIP_NORM: float = 1.0
|
|
103
|
+
WARMUP_RATIO: float = 0.05
|
|
104
|
+
|
|
105
|
+
# --- Curriculum (tiers x epocas) ---
|
|
106
|
+
TIER_EPOCHS: Tuple[int, int, int, int] = (2, 2, 3, 3)
|
|
107
|
+
TIER_SEQ_LEN: Tuple[int, int, int, int] = (128, 192, 256, 256)
|
|
108
|
+
TIER_NAMES: Tuple[str, str, str, str] = (
|
|
109
|
+
"codealpaca",
|
|
110
|
+
"code_feedback",
|
|
111
|
+
"magicoder_evol",
|
|
112
|
+
"tiny_the_stack",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# --- Datasets (HuggingFace IDs) ---
|
|
116
|
+
HF_CODEALPACA: str = "sahil2801/CodeAlpaca-20k"
|
|
117
|
+
HF_CODE_FEEDBACK: str = "m-a-p/CodeFeedback-Filtered-Instruction"
|
|
118
|
+
HF_MAGICODER: str = "ise-uiuc/Magicoder-Evol-Instruct-110K"
|
|
119
|
+
HF_TINY_STACK: str = "bigcode/the-stack-smol"
|
|
120
|
+
# Cadena de fallbacks publicos (sin autenticacion HF) si bigcode/the-stack-smol
|
|
121
|
+
# esta gated. Se prueban en orden hasta que uno funcione.
|
|
122
|
+
HF_TINY_STACK_FALLBACKS: Tuple[str, ...] = (
|
|
123
|
+
"codeparrot/codeparrot-clean-valid",
|
|
124
|
+
"flytech/python-codes-25k",
|
|
125
|
+
"iamtarun/python_code_instructions_18k_alpaca",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# --- Splits ---
|
|
129
|
+
VAL_FRACTION: float = 0.05 # validacion durante entrenamiento
|
|
130
|
+
HOLDOUT_FRACTION: float = 0.05 # NUNCA visto, medicion de generalizacion
|
|
131
|
+
SPLIT_SEED: int = 1337 # determinismo del split por dataset
|
|
132
|
+
|
|
133
|
+
# --- Tokens por tier (limites de seguridad) ---
|
|
134
|
+
MAX_TOKENS_PER_TIER: Tuple[int, int, int, int] = (
|
|
135
|
+
20_000_000, # CodeAlpaca chico
|
|
136
|
+
80_000_000, # CodeFeedback medio
|
|
137
|
+
100_000_000, # Magicoder
|
|
138
|
+
500_000_000, # Tiny-The-Stack ~1GB
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# --- Grassmanniana ---
|
|
142
|
+
GRASS_TRACK_EVERY: int = 200 # steps entre snapshots de U_r
|
|
143
|
+
GRASS_MAX_RANK: int = 16 # tope para r (elbow se calcula <=)
|
|
144
|
+
GRASS_ELBOW_RATIO: float = 0.05 # corte relativo del elbow
|
|
145
|
+
GRASS_FISHER_BATCHES: int = 40 # batches para estimar Sigma_F (>= r_max+2)
|
|
146
|
+
GRASS_FISHER_GRADS: int = 64 # gradientes muestrales para Sigma_F
|
|
147
|
+
|
|
148
|
+
# --- Optimizacion: Gauss para multiplicacion compleja (3 mults en vez de 4) ---
|
|
149
|
+
USE_GAUSS_COMPLEX_MULT: bool = True
|
|
150
|
+
|
|
151
|
+
# --- Mediciones de eficiencia ---
|
|
152
|
+
EFFICIENCY_THROUGHPUT_BATCHES: int = 20 # batches para cronometrar tok/s
|
|
153
|
+
# Factor heuristico para activaciones por token (multiplo de D_MODEL guardado en memoria);
|
|
154
|
+
# 12 corresponde a Q,K,V,attn-out + 2 FFN + residuales por capa.
|
|
155
|
+
GRADIENT_CHECKPOINTING_FACTOR: int = 12
|
|
156
|
+
|
|
157
|
+
# --- Progressive Window Sizes (Sliding Window Attention) ---
|
|
158
|
+
PROGRESSIVE_WINDOW: Tuple[int, int, int, int] = (64, 96, 128, 128)
|
|
159
|
+
|
|
160
|
+
# --- Paths ---
|
|
161
|
+
DATA_CACHE: str = "data_topogpt3"
|
|
162
|
+
CHECKPOINT_DIR: str = "checkpoints_topogpt3"
|
|
163
|
+
LOG_FILE: str = "topogpt3_history.jsonl"
|
|
164
|
+
|
|
165
|
+
# --- Logging / checkpoint ---
|
|
166
|
+
LOG_INTERVAL_STEPS: int = 50
|
|
167
|
+
CHECKPOINT_EVERY_MIN: int = 15
|
|
168
|
+
MAX_CHECKPOINTS: int = 5
|
|
169
|
+
|
|
170
|
+
# --- Limites de seguridad ---
|
|
171
|
+
MAX_TRAIN_BATCHES_PER_EPOCH: int = 0 # 0 = sin limite
|
|
172
|
+
MAX_EVAL_BATCHES: int = 200 # cap a evaluacion para no saturar
|
|
173
|
+
|
|
174
|
+
# --- Anti-forgetting: EWC + Replay ---
|
|
175
|
+
USE_EWC: bool = True # Elastic Weight Consolidation
|
|
176
|
+
EWC_LAMBDA: float = 5000.0 # penalty strength
|
|
177
|
+
EWC_FISHER_BATCHES: int = 50 # batches para estimar Fisher
|
|
178
|
+
USE_REPLAY: bool = True # Experience Replay entre tiers
|
|
179
|
+
REPLAY_RATIO: float = 0.25 # fraccion de replay en batch mixto
|
|
180
|
+
REPLAY_MAX_TIERS: int = 3 # tiers recordados para replay
|
|
181
|
+
|
|
182
|
+
def build_topogpt2_config(self, max_seq_len: int,
|
|
183
|
+
attn_window: Optional[int] = None) -> TopoGPT2Config:
|
|
184
|
+
cfg = TopoGPT2Config(
|
|
185
|
+
DEVICE=self.DEVICE,
|
|
186
|
+
RANDOM_SEED=self.RANDOM_SEED,
|
|
187
|
+
USE_AMP=self.USE_AMP,
|
|
188
|
+
SCALE=self.SCALE,
|
|
189
|
+
BATCH_SIZE=self.BATCH_SIZE,
|
|
190
|
+
GRAD_ACCUM_STEPS=self.GRAD_ACCUM_STEPS,
|
|
191
|
+
LEARNING_RATE=self.LEARNING_RATE,
|
|
192
|
+
WEIGHT_DECAY=self.WEIGHT_DECAY,
|
|
193
|
+
GRADIENT_CLIP_NORM=self.GRADIENT_CLIP_NORM,
|
|
194
|
+
WARMUP_RATIO=self.WARMUP_RATIO,
|
|
195
|
+
GRADIENT_CHECKPOINTING=self.GRADIENT_CHECKPOINTING,
|
|
196
|
+
CHECKPOINT_DIR=self.CHECKPOINT_DIR,
|
|
197
|
+
DATA_DIR=self.DATA_CACHE,
|
|
198
|
+
)
|
|
199
|
+
cfg.MAX_SEQ_LEN = max_seq_len
|
|
200
|
+
if attn_window is not None and attn_window > 0:
|
|
201
|
+
cfg.ATTN_WINDOW = attn_window
|
|
202
|
+
return cfg
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
# ============================================================================
|
|
206
|
+
# GRASSMANNIAN TRACKER
|
|
207
|
+
# ============================================================================
|
|
208
|
+
|
|
209
|
+
class GrassmannianTracker:
|
|
210
|
+
"""
|
|
211
|
+
Observables geometricos sobre la trayectoria SGD.
|
|
212
|
+
|
|
213
|
+
En cada snapshot:
|
|
214
|
+
- Apila los kernels espectrales (kr_*, ki_*) del modelo en
|
|
215
|
+
K(theta) en C^{N_f x N_c}.
|
|
216
|
+
- SVD truncada -> U_r(theta) en St(r,N).
|
|
217
|
+
- Rango r dinamico por elbow de los valores singulares.
|
|
218
|
+
- Gap funcional Delta_F estimado por covarianza de gradientes
|
|
219
|
+
muestrales (proxy de la matriz de Fisher).
|
|
220
|
+
- Conexion de Berry discreta entre snapshots consecutivos:
|
|
221
|
+
A_n = i * U_n^dagger (U_{n+1} - U_n)
|
|
222
|
+
Holonomia acumulada U_Gamma = P prod_n exp(-i A_n) en U(r).
|
|
223
|
+
- Distancia de conjugacion en SU(2) (r=1 efectivo cuaternionico).
|
|
224
|
+
- Winding W como proxy barato.
|
|
225
|
+
|
|
226
|
+
Todos los calculos viven en CPU/float32 para no contaminar AMP.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(self, config: TopoGPT3Config, logger: logging.Logger):
|
|
230
|
+
self.config = config
|
|
231
|
+
self.logger = logger
|
|
232
|
+
self.U_prev: Optional[torch.Tensor] = None
|
|
233
|
+
self.U_holo: Optional[torch.Tensor] = None # holonomia acumulada
|
|
234
|
+
self.U_init: Optional[torch.Tensor] = None # subespacio inicial
|
|
235
|
+
self.winding_accum: float = 0.0
|
|
236
|
+
self.history: List[Dict[str, Any]] = []
|
|
237
|
+
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
# Construccion del tensor de kernels K(theta)
|
|
240
|
+
# ------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _stack_spectral_kernels(model: nn.Module) -> torch.Tensor:
|
|
244
|
+
"""
|
|
245
|
+
Devuelve K(theta) en C^{N_f x N_c}:
|
|
246
|
+
- filas = frecuencias planas (todos los modos espaciales de todos los kernels)
|
|
247
|
+
- columnas = canales (in_q * out_q por componente cuaternionico, sumados)
|
|
248
|
+
"""
|
|
249
|
+
rows: List[torch.Tensor] = []
|
|
250
|
+
with torch.no_grad():
|
|
251
|
+
for name, mod in model.named_modules():
|
|
252
|
+
if isinstance(mod, QuaternionSpectralLayer):
|
|
253
|
+
in_q = mod.in_q
|
|
254
|
+
out_q = mod.out_q
|
|
255
|
+
for c in ("w", "x", "y", "z"):
|
|
256
|
+
kr = getattr(mod, f"kr_{c}").detach().cpu().float()
|
|
257
|
+
ki = getattr(mod, f"ki_{c}").detach().cpu().float()
|
|
258
|
+
K = torch.complex(kr, ki) # [in_q, out_q, H, W']
|
|
259
|
+
K = K.permute(2, 3, 0, 1).contiguous()
|
|
260
|
+
H, Wp, Iq, Oq = K.shape
|
|
261
|
+
K = K.reshape(H * Wp, Iq * Oq)
|
|
262
|
+
rows.append(K)
|
|
263
|
+
if not rows:
|
|
264
|
+
return torch.zeros(1, 1, dtype=torch.complex64)
|
|
265
|
+
max_cols = max(r.shape[1] for r in rows)
|
|
266
|
+
padded = []
|
|
267
|
+
for r in rows:
|
|
268
|
+
if r.shape[1] < max_cols:
|
|
269
|
+
pad = torch.zeros(
|
|
270
|
+
r.shape[0], max_cols - r.shape[1], dtype=r.dtype
|
|
271
|
+
)
|
|
272
|
+
r = torch.cat([r, pad], dim=1)
|
|
273
|
+
padded.append(r)
|
|
274
|
+
return torch.cat(padded, dim=0)
|
|
275
|
+
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
# SVD truncada + eleccion de rango por elbow
|
|
278
|
+
# ------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
def _elbow_rank(self, sigmas: torch.Tensor) -> int:
|
|
281
|
+
"""Punto donde el valor singular cae por debajo de elbow_ratio * sigma_max."""
|
|
282
|
+
s = sigmas.float()
|
|
283
|
+
if s.numel() == 0:
|
|
284
|
+
return 1
|
|
285
|
+
cutoff = self.config.GRASS_ELBOW_RATIO * float(s[0])
|
|
286
|
+
r = int((s > cutoff).sum().item())
|
|
287
|
+
return max(1, min(r, self.config.GRASS_MAX_RANK))
|
|
288
|
+
|
|
289
|
+
def _dominant_subspace(self, K: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, int]:
|
|
290
|
+
"""
|
|
291
|
+
SVD compacta y truncada.
|
|
292
|
+
Devuelve (U_r, sigmas, r) con U_r en C^{N_f x r} ortonormal.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
U, S, _Vh = torch.linalg.svd(K, full_matrices=False)
|
|
296
|
+
except RuntimeError as exc:
|
|
297
|
+
self.logger.warning(f"SVD fallida en K({tuple(K.shape)}): {exc}")
|
|
298
|
+
return torch.eye(K.shape[0], 1, dtype=K.dtype), torch.tensor([1.0]), 1
|
|
299
|
+
r = self._elbow_rank(S.abs())
|
|
300
|
+
return U[:, :r].contiguous(), S[:r].abs(), r
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------
|
|
303
|
+
# Fisher gap por covarianza de gradientes (proxy de Sigma_F)
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _flatten_grads(model: nn.Module, max_per_tensor: int = 256) -> Optional[torch.Tensor]:
|
|
308
|
+
"""Concatena un sub-sample de gradientes para mantener costo acotado."""
|
|
309
|
+
parts: List[torch.Tensor] = []
|
|
310
|
+
for p in model.parameters():
|
|
311
|
+
if p.grad is None or not p.is_floating_point():
|
|
312
|
+
continue
|
|
313
|
+
g = p.grad.detach()
|
|
314
|
+
if torch.isnan(g).any() or torch.isinf(g).any():
|
|
315
|
+
continue
|
|
316
|
+
flat = g.flatten().float().cpu()
|
|
317
|
+
if flat.numel() > max_per_tensor:
|
|
318
|
+
step = max(1, flat.numel() // max_per_tensor)
|
|
319
|
+
flat = flat[::step][:max_per_tensor]
|
|
320
|
+
parts.append(flat)
|
|
321
|
+
if not parts:
|
|
322
|
+
return None
|
|
323
|
+
return torch.cat(parts)
|
|
324
|
+
|
|
325
|
+
def estimate_fisher_gap(
|
|
326
|
+
self,
|
|
327
|
+
model: nn.Module,
|
|
328
|
+
dataloader: Iterable,
|
|
329
|
+
vocab_size: int,
|
|
330
|
+
r_target: int,
|
|
331
|
+
) -> Tuple[float, torch.Tensor, int]:
|
|
332
|
+
"""
|
|
333
|
+
Sigma_F ~= (1/M) sum_m g_m g_m^T (covarianza muestral de gradientes).
|
|
334
|
+
Delta_F = lambda_{r_eff} - lambda_{r_eff+1}, donde r_eff = min(r_target, M-2)
|
|
335
|
+
para no salir del rango efectivo del estimador con M gradientes.
|
|
336
|
+
Devuelve (gap, eigs_desc, r_eff).
|
|
337
|
+
"""
|
|
338
|
+
device = next(model.parameters()).device
|
|
339
|
+
was_training = model.training
|
|
340
|
+
model.train()
|
|
341
|
+
samples: List[torch.Tensor] = []
|
|
342
|
+
n_batches = 0
|
|
343
|
+
for batch in dataloader:
|
|
344
|
+
if n_batches >= self.config.GRASS_FISHER_BATCHES:
|
|
345
|
+
break
|
|
346
|
+
if len(samples) >= self.config.GRASS_FISHER_GRADS:
|
|
347
|
+
break
|
|
348
|
+
bx, by = batch
|
|
349
|
+
bx = bx.to(device, non_blocking=True)
|
|
350
|
+
by = by.to(device, non_blocking=True)
|
|
351
|
+
model.zero_grad(set_to_none=True)
|
|
352
|
+
try:
|
|
353
|
+
logits, _, _ = model(bx)
|
|
354
|
+
loss = F.cross_entropy(
|
|
355
|
+
logits.view(-1, vocab_size), by.view(-1)
|
|
356
|
+
)
|
|
357
|
+
loss.backward()
|
|
358
|
+
except RuntimeError as exc:
|
|
359
|
+
self.logger.warning(f"Fisher backward fallido: {exc}")
|
|
360
|
+
model.zero_grad(set_to_none=True)
|
|
361
|
+
continue
|
|
362
|
+
g = self._flatten_grads(model)
|
|
363
|
+
if g is not None:
|
|
364
|
+
samples.append(g)
|
|
365
|
+
n_batches += 1
|
|
366
|
+
model.zero_grad(set_to_none=True)
|
|
367
|
+
if not was_training:
|
|
368
|
+
model.eval()
|
|
369
|
+
M = len(samples)
|
|
370
|
+
if M < 3:
|
|
371
|
+
empty = torch.zeros(1)
|
|
372
|
+
return float("nan"), empty, 0
|
|
373
|
+
D = min(s.numel() for s in samples)
|
|
374
|
+
G = torch.stack([s[:D] for s in samples], dim=0)
|
|
375
|
+
gram = (G @ G.t()) / max(G.shape[0] - 1, 1)
|
|
376
|
+
try:
|
|
377
|
+
eigs = torch.linalg.eigvalsh(gram).real.clamp(min=0)
|
|
378
|
+
except RuntimeError:
|
|
379
|
+
empty = torch.zeros(1)
|
|
380
|
+
return float("nan"), empty, 0
|
|
381
|
+
eigs, _ = torch.sort(eigs, descending=True)
|
|
382
|
+
r_eff = max(1, min(r_target, M - 2))
|
|
383
|
+
if r_eff >= eigs.numel():
|
|
384
|
+
r_eff = max(1, eigs.numel() - 1)
|
|
385
|
+
gap = float((eigs[r_eff - 1] - eigs[r_eff]).item())
|
|
386
|
+
return gap, eigs, r_eff
|
|
387
|
+
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
# Berry connection y holonomia discreta
|
|
390
|
+
# ------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def _project_unitary(M: torch.Tensor) -> torch.Tensor:
|
|
394
|
+
"""Proyeccion a U(r) por descomposicion polar (M ~= U H -> retorna U)."""
|
|
395
|
+
U_l, _S, Vh = torch.linalg.svd(M, full_matrices=False)
|
|
396
|
+
return U_l @ Vh
|
|
397
|
+
|
|
398
|
+
def update_holonomy(self, U_new: torch.Tensor) -> Optional[torch.Tensor]:
|
|
399
|
+
"""
|
|
400
|
+
Holonomia discreta:
|
|
401
|
+
T_n = U_n^dagger U_{n+1} en C^{r x r} (transporte paralelo discreto)
|
|
402
|
+
U_Gamma <- T_n * U_Gamma (acumulado)
|
|
403
|
+
Tras cada paso, U_Gamma se proyecta a U(r) para evitar deriva numerica.
|
|
404
|
+
"""
|
|
405
|
+
if self.U_prev is None:
|
|
406
|
+
self.U_prev = U_new
|
|
407
|
+
self.U_init = U_new
|
|
408
|
+
self.U_holo = torch.eye(U_new.shape[1], dtype=U_new.dtype)
|
|
409
|
+
return self.U_holo
|
|
410
|
+
# Asegurar misma forma (kernel puede haber cambiado de tamano si r vario)
|
|
411
|
+
r_min = min(self.U_prev.shape[1], U_new.shape[1])
|
|
412
|
+
rows = min(self.U_prev.shape[0], U_new.shape[0])
|
|
413
|
+
Up = self.U_prev[:rows, :r_min]
|
|
414
|
+
Un = U_new[:rows, :r_min]
|
|
415
|
+
T = Up.conj().transpose(0, 1) @ Un
|
|
416
|
+
T = self._project_unitary(T)
|
|
417
|
+
if self.U_holo is None or self.U_holo.shape != T.shape:
|
|
418
|
+
self.U_holo = torch.eye(T.shape[0], dtype=T.dtype)
|
|
419
|
+
self.U_holo = self._project_unitary(T @ self.U_holo)
|
|
420
|
+
self.U_prev = U_new
|
|
421
|
+
return self.U_holo
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def conjugation_distance_su2(U1: torch.Tensor, U2: torch.Tensor) -> float:
|
|
425
|
+
"""
|
|
426
|
+
Para U1, U2 en U(1)/U(2): d_conj(U1, U2) = min_g || U1 - g U2 g^{-1} ||_F.
|
|
427
|
+
En U(1) coincide con |U1 - U2|.
|
|
428
|
+
En SU(2) se reduce a comparar |Tr(U1)| con |Tr(U2)| (clase de conjugacion).
|
|
429
|
+
"""
|
|
430
|
+
n = U1.shape[0]
|
|
431
|
+
if n == 1:
|
|
432
|
+
return float(torch.linalg.norm(U1 - U2).real.item())
|
|
433
|
+
t1 = torch.trace(U1)
|
|
434
|
+
t2 = torch.trace(U2)
|
|
435
|
+
return float(abs(abs(t1) - abs(t2)).item())
|
|
436
|
+
|
|
437
|
+
# ------------------------------------------------------------------
|
|
438
|
+
# Winding heuristico
|
|
439
|
+
# ------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
def _accumulate_winding(self, U_new: torch.Tensor) -> float:
|
|
442
|
+
"""W += (1/2pi) * arg det <U_prev | U_new> acumulado sobre la trayectoria."""
|
|
443
|
+
if self.U_prev is None:
|
|
444
|
+
return self.winding_accum
|
|
445
|
+
r_min = min(self.U_prev.shape[1], U_new.shape[1])
|
|
446
|
+
rows = min(self.U_prev.shape[0], U_new.shape[0])
|
|
447
|
+
overlap = self.U_prev[:rows, :r_min].conj().transpose(0, 1) @ U_new[:rows, :r_min]
|
|
448
|
+
det = torch.linalg.det(overlap)
|
|
449
|
+
phase = float(torch.atan2(det.imag, det.real).item())
|
|
450
|
+
self.winding_accum += phase / (2 * math.pi)
|
|
451
|
+
return self.winding_accum
|
|
452
|
+
|
|
453
|
+
# ------------------------------------------------------------------
|
|
454
|
+
# Snapshot completo
|
|
455
|
+
# ------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
def snapshot(
|
|
458
|
+
self,
|
|
459
|
+
model: nn.Module,
|
|
460
|
+
step: int,
|
|
461
|
+
dataloader: Optional[Iterable] = None,
|
|
462
|
+
vocab_size: Optional[int] = None,
|
|
463
|
+
) -> Dict[str, Any]:
|
|
464
|
+
K = self._stack_spectral_kernels(model)
|
|
465
|
+
U_r, sigmas, r = self._dominant_subspace(K)
|
|
466
|
+
prev_U = self.U_prev
|
|
467
|
+
winding = self._accumulate_winding(U_r)
|
|
468
|
+
holo = self.update_holonomy(U_r)
|
|
469
|
+
d_conj_init = (
|
|
470
|
+
self.conjugation_distance_su2(self.U_init.conj().transpose(0, 1) @ self.U_init,
|
|
471
|
+
U_r.conj().transpose(0, 1) @ U_r)
|
|
472
|
+
if self.U_init is not None else 0.0
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Berry phase = arg det(U_Gamma)
|
|
476
|
+
if holo is not None and holo.numel() > 0:
|
|
477
|
+
det_holo = torch.linalg.det(holo)
|
|
478
|
+
berry = float(torch.atan2(det_holo.imag, det_holo.real).item())
|
|
479
|
+
holo_norm = float(torch.linalg.norm(holo).real.item())
|
|
480
|
+
else:
|
|
481
|
+
berry = 0.0
|
|
482
|
+
holo_norm = 0.0
|
|
483
|
+
|
|
484
|
+
delta_f = float("nan")
|
|
485
|
+
r_eff_fisher = 0
|
|
486
|
+
if dataloader is not None and vocab_size is not None:
|
|
487
|
+
delta_f, eigs, r_eff_fisher = self.estimate_fisher_gap(
|
|
488
|
+
model, dataloader, vocab_size, r
|
|
489
|
+
)
|
|
490
|
+
sigma_max = float(eigs[0].item()) if eigs.numel() > 0 else 0.0
|
|
491
|
+
sigma_min = float(eigs[eigs > 1e-12].min().item()) if (eigs > 1e-12).any() else 0.0
|
|
492
|
+
kappa_F = (sigma_max / sigma_min) if sigma_min > 0 else float("inf")
|
|
493
|
+
else:
|
|
494
|
+
kappa_F = float("inf")
|
|
495
|
+
|
|
496
|
+
snap = {
|
|
497
|
+
"step": step,
|
|
498
|
+
"rank_r": int(r),
|
|
499
|
+
"fisher_r_eff": int(r_eff_fisher),
|
|
500
|
+
"sigma_max": float(sigmas[0].item()) if sigmas.numel() > 0 else 0.0,
|
|
501
|
+
"sigma_min": float(sigmas[-1].item()) if sigmas.numel() > 0 else 0.0,
|
|
502
|
+
"fisher_gap": delta_f,
|
|
503
|
+
"fisher_kappa": kappa_F,
|
|
504
|
+
"berry_phase": berry,
|
|
505
|
+
"winding": winding,
|
|
506
|
+
"holo_frobenius": holo_norm,
|
|
507
|
+
"d_conj_to_init": d_conj_init,
|
|
508
|
+
}
|
|
509
|
+
self.history.append(snap)
|
|
510
|
+
return snap
|
|
511
|
+
|
|
512
|
+
def format_log(self, snap: Dict[str, Any]) -> str:
|
|
513
|
+
df = snap.get("fisher_gap", float("nan"))
|
|
514
|
+
r_eff = snap.get("fisher_r_eff", 0)
|
|
515
|
+
if r_eff <= 0:
|
|
516
|
+
df_str = "skip"
|
|
517
|
+
r_str = "-"
|
|
518
|
+
elif math.isfinite(df):
|
|
519
|
+
df_str = f"{df:.3e}"
|
|
520
|
+
r_str = str(r_eff)
|
|
521
|
+
else:
|
|
522
|
+
df_str = "nan"
|
|
523
|
+
r_str = str(r_eff)
|
|
524
|
+
return (
|
|
525
|
+
f"r={snap['rank_r']:>2d} "
|
|
526
|
+
f"sig=[{snap['sigma_max']:.3e},{snap['sigma_min']:.3e}] "
|
|
527
|
+
f"Delta_F@r{r_str}={df_str} "
|
|
528
|
+
f"berry={snap['berry_phase']:+.3f} "
|
|
529
|
+
f"W={snap['winding']:+.3f} "
|
|
530
|
+
f"||U_G||={snap['holo_frobenius']:.3f} "
|
|
531
|
+
f"d_conj={snap['d_conj_to_init']:.3e}"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
def save(self, path: str) -> None:
|
|
535
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
536
|
+
for s in self.history:
|
|
537
|
+
f.write(json.dumps(s) + "\n")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# ============================================================================
|
|
541
|
+
# GAUSS / KARATSUBA: 3 multiplicaciones reales por multiplicacion compleja
|
|
542
|
+
# ============================================================================
|
|
543
|
+
|
|
544
|
+
def _gauss_complex_contract(self, W: torch.Tensor, X: torch.Tensor) -> torch.Tensor:
|
|
545
|
+
"""Sustituye QuaternionSpectralLayer._contract usando el truco de Gauss.
|
|
546
|
+
|
|
547
|
+
Para (Wr + i Wi)(Xr + i Xi) la version naive requiere 4 productos reales:
|
|
548
|
+
Yr = Wr Xr - Wi Xi
|
|
549
|
+
Yi = Wr Xi + Wi Xr
|
|
550
|
+
Gauss (Karatsuba) baja a 3 productos reales:
|
|
551
|
+
m1 = Wr * Xr
|
|
552
|
+
m2 = Wi * Xi
|
|
553
|
+
m3 = (Wr + Wi) * (Xr + Xi)
|
|
554
|
+
Yr = m1 - m2
|
|
555
|
+
Yi = m3 - m1 - m2
|
|
556
|
+
|
|
557
|
+
Importante (AMP): el _contract original opera sobre complex64 y PyTorch no
|
|
558
|
+
autocastea operaciones complejas; el resultado es complex64. Si dejamos que
|
|
559
|
+
autocast convierta nuestros einsums reales a fp16, la dtype de salida cambia
|
|
560
|
+
y rompe el scatter_add_ corriente abajo en QuaternionTorusBrain. Por eso
|
|
561
|
+
desactivamos autocast aqui y forzamos fp32 para preservar la semantica.
|
|
562
|
+
"""
|
|
563
|
+
device_type = W.device.type if W.device.type in ("cuda", "cpu") else "cpu"
|
|
564
|
+
with torch.amp.autocast(device_type=device_type, enabled=False):
|
|
565
|
+
Wr = W.real.float()
|
|
566
|
+
Wi = W.imag.float()
|
|
567
|
+
Xr = X.real.float()
|
|
568
|
+
Xi = X.imag.float()
|
|
569
|
+
m1 = torch.einsum("iohw,bihw->bohw", Wr, Xr)
|
|
570
|
+
m2 = torch.einsum("iohw,bihw->bohw", Wi, Xi)
|
|
571
|
+
m3 = torch.einsum("iohw,bihw->bohw", Wr + Wi, Xr + Xi)
|
|
572
|
+
Yr = m1 - m2
|
|
573
|
+
Yi = m3 - m1 - m2
|
|
574
|
+
return torch.complex(Yr, Yi)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
_GAUSS_PATCH_APPLIED = False
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def apply_gauss_patch(logger: Optional[logging.Logger] = None) -> None:
|
|
581
|
+
"""Activa la version Gauss de _contract en QuaternionSpectralLayer.
|
|
582
|
+
Idempotente: solo parchea una vez por proceso."""
|
|
583
|
+
global _GAUSS_PATCH_APPLIED
|
|
584
|
+
if _GAUSS_PATCH_APPLIED:
|
|
585
|
+
return
|
|
586
|
+
QuaternionSpectralLayer._contract_original = QuaternionSpectralLayer._contract # type: ignore[attr-defined]
|
|
587
|
+
QuaternionSpectralLayer._contract = _gauss_complex_contract # type: ignore[assignment]
|
|
588
|
+
_GAUSS_PATCH_APPLIED = True
|
|
589
|
+
if logger is not None:
|
|
590
|
+
logger.info("Gauss complex-mult patch aplicado: 3 productos reales por contraccion.")
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# ============================================================================
|
|
594
|
+
# EFFICIENCY METRICS: performance / param, / FLOP, / bandwidth
|
|
595
|
+
# ============================================================================
|
|
596
|
+
|
|
597
|
+
class EfficiencyMetrics:
|
|
598
|
+
"""
|
|
599
|
+
Mide y calcula los tres ratios pedidos:
|
|
600
|
+
|
|
601
|
+
perf_per_param = (1 / val_ppl) / params_M
|
|
602
|
+
perf_per_FLOP = tokens_per_sec / FLOPs_per_sec_aprox
|
|
603
|
+
perf_per_BW = tokens_per_sec / bytes_moved_per_sec_aprox
|
|
604
|
+
|
|
605
|
+
FLOPs estimados con la heuristica de Kaplan/Hoffmann:
|
|
606
|
+
FLOPs_forward_per_token ~= 2 * N_no_embed
|
|
607
|
+
FLOPs_total_per_token ~= 6 * N_no_embed (forward + backward)
|
|
608
|
+
Bandwidth estimada como params_bytes leidos + activations_bytes movidas por step.
|
|
609
|
+
tokens_per_sec se cronometra empiricamente sobre el dataloader.
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
def __init__(self, model: nn.Module, config: TopoGPT3Config,
|
|
613
|
+
logger: logging.Logger, gauss_enabled: bool):
|
|
614
|
+
self.model = model
|
|
615
|
+
self.config = config
|
|
616
|
+
self.logger = logger
|
|
617
|
+
self.gauss_enabled = gauss_enabled
|
|
618
|
+
self.total_params = sum(p.numel() for p in model.parameters())
|
|
619
|
+
self.embed_params = self._embed_params(model)
|
|
620
|
+
self.non_embed_params = self.total_params - self.embed_params
|
|
621
|
+
|
|
622
|
+
@staticmethod
|
|
623
|
+
def _embed_params(model: nn.Module) -> int:
|
|
624
|
+
n = 0
|
|
625
|
+
for name, mod in model.named_modules():
|
|
626
|
+
if isinstance(mod, nn.Embedding):
|
|
627
|
+
n += sum(p.numel() for p in mod.parameters())
|
|
628
|
+
return n
|
|
629
|
+
|
|
630
|
+
@torch.no_grad()
|
|
631
|
+
def measure_throughput(self, dataloader, vocab_size: int) -> Tuple[float, float]:
|
|
632
|
+
"""Devuelve (tokens_por_segundo, segundos_por_step)."""
|
|
633
|
+
self.model.eval()
|
|
634
|
+
device = self.config.DEVICE
|
|
635
|
+
n_batches = self.config.EFFICIENCY_THROUGHPUT_BATCHES
|
|
636
|
+
# Warmup
|
|
637
|
+
warmup_done = False
|
|
638
|
+
for bx, by in dataloader:
|
|
639
|
+
bx = bx.to(device, non_blocking=True)
|
|
640
|
+
self.model(bx)
|
|
641
|
+
if "cuda" in device:
|
|
642
|
+
torch.cuda.synchronize()
|
|
643
|
+
warmup_done = True
|
|
644
|
+
break
|
|
645
|
+
if not warmup_done:
|
|
646
|
+
return 0.0, 0.0
|
|
647
|
+
total_tokens = 0
|
|
648
|
+
t0 = time.time()
|
|
649
|
+
seen = 0
|
|
650
|
+
for bx, by in dataloader:
|
|
651
|
+
if seen >= n_batches:
|
|
652
|
+
break
|
|
653
|
+
bx = bx.to(device, non_blocking=True)
|
|
654
|
+
self.model(bx)
|
|
655
|
+
total_tokens += int(bx.numel())
|
|
656
|
+
seen += 1
|
|
657
|
+
if "cuda" in device:
|
|
658
|
+
torch.cuda.synchronize()
|
|
659
|
+
dt = max(time.time() - t0, 1e-6)
|
|
660
|
+
self.model.train()
|
|
661
|
+
return total_tokens / dt, dt / max(seen, 1)
|
|
662
|
+
|
|
663
|
+
def estimate_flops_per_step(self, batch_size: int, seq_len: int) -> int:
|
|
664
|
+
"""Heuristica: 6 * N_no_embed * tokens (forward + backward)."""
|
|
665
|
+
tokens = batch_size * seq_len
|
|
666
|
+
return int(6 * self.non_embed_params * tokens)
|
|
667
|
+
|
|
668
|
+
def estimate_bytes_per_step(self, batch_size: int, seq_len: int,
|
|
669
|
+
dtype_bytes: int = 2) -> int:
|
|
670
|
+
"""Bandwidth aproximada: lectura de pesos + activaciones por step.
|
|
671
|
+
Asume AMP fp16 (2 bytes); pesos fp32 (4 bytes) leidos una vez."""
|
|
672
|
+
weight_bytes = self.total_params * 4
|
|
673
|
+
activation_bytes = batch_size * seq_len * self.config.GRADIENT_CHECKPOINTING_FACTOR * dtype_bytes
|
|
674
|
+
return weight_bytes + activation_bytes
|
|
675
|
+
|
|
676
|
+
def compute(self, dataloader, vocab_size: int, val_loss: float,
|
|
677
|
+
val_ppl: float, val_acc: float, batch_size: int,
|
|
678
|
+
seq_len: int) -> Dict[str, Any]:
|
|
679
|
+
tok_per_sec, sec_per_step = self.measure_throughput(dataloader, vocab_size)
|
|
680
|
+
flops_step = self.estimate_flops_per_step(batch_size, seq_len)
|
|
681
|
+
bytes_step = self.estimate_bytes_per_step(batch_size, seq_len)
|
|
682
|
+
flops_per_sec = flops_step / max(sec_per_step, 1e-9)
|
|
683
|
+
bytes_per_sec = bytes_step / max(sec_per_step, 1e-9)
|
|
684
|
+
|
|
685
|
+
params_M = self.total_params / 1e6
|
|
686
|
+
perf_quality = (1.0 / max(val_ppl, 1e-6))
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
"params_total": self.total_params,
|
|
690
|
+
"params_non_embed": self.non_embed_params,
|
|
691
|
+
"params_embed": self.embed_params,
|
|
692
|
+
"gauss_complex": self.gauss_enabled,
|
|
693
|
+
"tokens_per_sec": tok_per_sec,
|
|
694
|
+
"sec_per_step": sec_per_step,
|
|
695
|
+
"flops_per_step": flops_step,
|
|
696
|
+
"flops_per_sec": flops_per_sec,
|
|
697
|
+
"bytes_per_step": bytes_step,
|
|
698
|
+
"bytes_per_sec": bytes_per_sec,
|
|
699
|
+
"val_loss": val_loss,
|
|
700
|
+
"val_ppl": val_ppl,
|
|
701
|
+
"val_acc": val_acc,
|
|
702
|
+
# Los tres ratios pedidos:
|
|
703
|
+
"perf_per_param": perf_quality / max(params_M, 1e-9),
|
|
704
|
+
"perf_per_GFLOP_sec": tok_per_sec / max(flops_per_sec / 1e9, 1e-9),
|
|
705
|
+
"perf_per_GBs": tok_per_sec / max(bytes_per_sec / 1e9, 1e-9),
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
def format_log(self, m: Dict[str, Any]) -> str:
|
|
709
|
+
return (
|
|
710
|
+
f"params={m['params_total']/1e6:.2f}M (non-embed {m['params_non_embed']/1e6:.2f}M) "
|
|
711
|
+
f"gauss={m['gauss_complex']} "
|
|
712
|
+
f"tok/s={m['tokens_per_sec']:.1f} "
|
|
713
|
+
f"GFLOPS={m['flops_per_sec']/1e9:.2f} "
|
|
714
|
+
f"GB/s={m['bytes_per_sec']/1e9:.2f}\n"
|
|
715
|
+
f" ratios: 1/ppl_per_Mparam={m['perf_per_param']:.4e} | "
|
|
716
|
+
f"tok/s_per_GFLOP={m['perf_per_GFLOP_sec']:.3e} | "
|
|
717
|
+
f"tok/s_per_GB={m['perf_per_GBs']:.3e}"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
# ============================================================================
|
|
722
|
+
# CURRICULUM DATA LOADER
|
|
723
|
+
# ============================================================================
|
|
724
|
+
|
|
725
|
+
class CodeCurriculumLoader:
|
|
726
|
+
"""
|
|
727
|
+
Carga los 4 datasets, normaliza cada ejemplo a una unica cadena de texto,
|
|
728
|
+
tokeniza con BPE y produce splits train / val / holdout disjuntos.
|
|
729
|
+
|
|
730
|
+
Politica de normalizacion por dataset:
|
|
731
|
+
- CodeAlpaca: "### Instruction\n{i}\n### Input\n{x}\n### Response\n{o}"
|
|
732
|
+
- Code-Feedback: concat de turnos: "<usr> ... </usr>\n<asst> ... </asst>"
|
|
733
|
+
- Magicoder-Evol: "### Problem\n{p}\n### Solution\n{s}"
|
|
734
|
+
- Tiny-The-Stack: texto crudo del archivo (truncado a 32k chars/file)
|
|
735
|
+
|
|
736
|
+
Cache en disco: tokens_{tier}_{split}.bin (int32 memmap) + manifest .json.
|
|
737
|
+
El HOLDOUT se separa con seed fija antes de tokenizar para garantizar
|
|
738
|
+
que la misma muestra nunca aparezca en train o val entre corridas.
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
def __init__(
|
|
742
|
+
self,
|
|
743
|
+
config: TopoGPT3Config,
|
|
744
|
+
tokenizer: BPETokenizer,
|
|
745
|
+
logger: logging.Logger,
|
|
746
|
+
):
|
|
747
|
+
self.config = config
|
|
748
|
+
self.tokenizer = tokenizer
|
|
749
|
+
self.logger = logger
|
|
750
|
+
self.cache_dir = Path(config.DATA_CACHE)
|
|
751
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
752
|
+
|
|
753
|
+
# ------------------------------------------------------------------
|
|
754
|
+
# Normalizacion por dataset
|
|
755
|
+
# ------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
@staticmethod
|
|
758
|
+
def _format_codealpaca(ex: Dict[str, Any]) -> Optional[str]:
|
|
759
|
+
instr = (ex.get("instruction") or "").strip()
|
|
760
|
+
inp = (ex.get("input") or "").strip()
|
|
761
|
+
out = (ex.get("output") or "").strip()
|
|
762
|
+
if not instr or not out:
|
|
763
|
+
return None
|
|
764
|
+
if inp:
|
|
765
|
+
return f"### Instruction\n{instr}\n### Input\n{inp}\n### Response\n{out}\n"
|
|
766
|
+
return f"### Instruction\n{instr}\n### Response\n{out}\n"
|
|
767
|
+
|
|
768
|
+
@staticmethod
|
|
769
|
+
def _format_code_feedback(ex: Dict[str, Any]) -> Optional[str]:
|
|
770
|
+
# m-a-p/CodeFeedback-Filtered-Instruction usa columnas query/answer
|
|
771
|
+
q = (ex.get("query") or ex.get("question") or ex.get("instruction") or "").strip()
|
|
772
|
+
a = (ex.get("answer") or ex.get("response") or ex.get("output") or "").strip()
|
|
773
|
+
if q and a:
|
|
774
|
+
return f"<|user|>\n{q}\n<|assistant|>\n{a}\n"
|
|
775
|
+
# Fallback: forma multi-turno (chat)
|
|
776
|
+
msgs = ex.get("messages") or ex.get("conversations") or []
|
|
777
|
+
parts: List[str] = []
|
|
778
|
+
for m in msgs:
|
|
779
|
+
role = (m.get("role") or m.get("from") or "user").strip().lower()
|
|
780
|
+
content = (m.get("content") or m.get("value") or "").strip()
|
|
781
|
+
if not content:
|
|
782
|
+
continue
|
|
783
|
+
tag = "user" if role in ("user", "human") else "assistant"
|
|
784
|
+
parts.append(f"<|{tag}|>\n{content}\n")
|
|
785
|
+
joined = "".join(parts).strip()
|
|
786
|
+
return joined or None
|
|
787
|
+
|
|
788
|
+
@staticmethod
|
|
789
|
+
def _format_magicoder(ex: Dict[str, Any]) -> Optional[str]:
|
|
790
|
+
prob = (ex.get("instruction") or ex.get("problem") or "").strip()
|
|
791
|
+
sol = (ex.get("response") or ex.get("solution") or ex.get("output") or "").strip()
|
|
792
|
+
if not prob or not sol:
|
|
793
|
+
return None
|
|
794
|
+
return f"### Problem\n{prob}\n### Solution\n{sol}\n"
|
|
795
|
+
|
|
796
|
+
@staticmethod
|
|
797
|
+
def _format_tiny_stack(ex: Dict[str, Any]) -> Optional[str]:
|
|
798
|
+
# bigcode/the-stack-smol usa 'content'; nampdn-ai/tiny-codes usa prompt/response
|
|
799
|
+
content = ex.get("content") or ex.get("text") or ex.get("code") or ""
|
|
800
|
+
if isinstance(content, str) and len(content) >= 30:
|
|
801
|
+
return content[:32_000]
|
|
802
|
+
prompt = (ex.get("prompt") or "").strip()
|
|
803
|
+
response = (ex.get("response") or "").strip()
|
|
804
|
+
if prompt and response:
|
|
805
|
+
return f"### Prompt\n{prompt}\n### Response\n{response}\n"[:32_000]
|
|
806
|
+
return None
|
|
807
|
+
|
|
808
|
+
@classmethod
|
|
809
|
+
def _get_formatter(cls, tier: str):
|
|
810
|
+
return {
|
|
811
|
+
"codealpaca": cls._format_codealpaca,
|
|
812
|
+
"code_feedback": cls._format_code_feedback,
|
|
813
|
+
"magicoder_evol": cls._format_magicoder,
|
|
814
|
+
"tiny_the_stack": cls._format_tiny_stack,
|
|
815
|
+
}[tier]
|
|
816
|
+
|
|
817
|
+
# Por dataset: (attr_de_id, kwargs_extra_para_load_dataset)
|
|
818
|
+
_HF_KWARGS: Dict[str, Tuple[str, Dict[str, Any]]] = {
|
|
819
|
+
"codealpaca": ("HF_CODEALPACA", {}),
|
|
820
|
+
"code_feedback": ("HF_CODE_FEEDBACK", {}),
|
|
821
|
+
"magicoder_evol": ("HF_MAGICODER", {}),
|
|
822
|
+
# Tiny-The-Stack: bigcode/the-stack-smol esta organizado por
|
|
823
|
+
# data/<lenguaje>; tomamos python como muestra por defecto.
|
|
824
|
+
"tiny_the_stack": ("HF_TINY_STACK", {"data_dir": "data/python"}),
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
# ------------------------------------------------------------------
|
|
828
|
+
# Tokenizacion + memmap por tier/split
|
|
829
|
+
# ------------------------------------------------------------------
|
|
830
|
+
|
|
831
|
+
def _tier_paths(self, tier: str) -> Dict[str, Path]:
|
|
832
|
+
return {
|
|
833
|
+
split: self.cache_dir / f"tokens_{tier}_{split}.bin"
|
|
834
|
+
for split in ("train", "val", "holdout")
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
def _manifest_path(self, tier: str) -> Path:
|
|
838
|
+
return self.cache_dir / f"manifest_{tier}.json"
|
|
839
|
+
|
|
840
|
+
def _already_prepared(self, tier: str) -> bool:
|
|
841
|
+
"""True solo si los 3 splits existen, son no-vacios y el manifest concuerda."""
|
|
842
|
+
paths = self._tier_paths(tier)
|
|
843
|
+
manifest = self._manifest_path(tier)
|
|
844
|
+
if not manifest.exists():
|
|
845
|
+
return False
|
|
846
|
+
if not all(p.exists() for p in paths.values()):
|
|
847
|
+
return False
|
|
848
|
+
try:
|
|
849
|
+
with open(manifest, "r", encoding="utf-8") as f:
|
|
850
|
+
meta = json.load(f)
|
|
851
|
+
sizes = meta.get("sizes", {})
|
|
852
|
+
except (json.JSONDecodeError, OSError):
|
|
853
|
+
return False
|
|
854
|
+
for split, p in paths.items():
|
|
855
|
+
disk_n = p.stat().st_size // 4
|
|
856
|
+
if disk_n == 0:
|
|
857
|
+
self.logger.warning(
|
|
858
|
+
f"[{tier}] cache invalido: {p.name} esta vacio; re-preparando."
|
|
859
|
+
)
|
|
860
|
+
return False
|
|
861
|
+
expected = sizes.get(split, 0)
|
|
862
|
+
if expected > 0 and abs(disk_n - expected) > 4:
|
|
863
|
+
self.logger.warning(
|
|
864
|
+
f"[{tier}] cache inconsistente: {p.name} disk={disk_n} "
|
|
865
|
+
f"manifest={expected}; re-preparando."
|
|
866
|
+
)
|
|
867
|
+
return False
|
|
868
|
+
return True
|
|
869
|
+
|
|
870
|
+
def _load_hf_with_fallback(self, tier: str):
|
|
871
|
+
"""Carga el dataset HF; para tiny_the_stack prueba una cadena de fallbacks
|
|
872
|
+
publicos hasta que uno funcione."""
|
|
873
|
+
try:
|
|
874
|
+
from datasets import load_dataset
|
|
875
|
+
except ImportError as exc:
|
|
876
|
+
raise RuntimeError(
|
|
877
|
+
"Requiere `datasets` de HuggingFace: pip install datasets"
|
|
878
|
+
) from exc
|
|
879
|
+
|
|
880
|
+
attr, extra_kwargs = self._HF_KWARGS[tier]
|
|
881
|
+
hf_id = getattr(self.config, attr)
|
|
882
|
+
candidates: List[Tuple[str, Dict[str, Any]]] = [(hf_id, extra_kwargs)]
|
|
883
|
+
if tier == "tiny_the_stack":
|
|
884
|
+
for alt in self.config.HF_TINY_STACK_FALLBACKS:
|
|
885
|
+
candidates.append((alt, {}))
|
|
886
|
+
|
|
887
|
+
last_exc: Optional[Exception] = None
|
|
888
|
+
for cand_id, cand_kwargs in candidates:
|
|
889
|
+
self.logger.info(f"[{tier}] intentando {cand_id} {cand_kwargs} ...")
|
|
890
|
+
try:
|
|
891
|
+
return load_dataset(cand_id, split="train", **cand_kwargs)
|
|
892
|
+
except Exception as exc:
|
|
893
|
+
last_exc = exc
|
|
894
|
+
msg = str(exc).splitlines()[0] if str(exc) else type(exc).__name__
|
|
895
|
+
self.logger.warning(
|
|
896
|
+
f"[{tier}] {cand_id} no accesible: {msg}; probando siguiente."
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
raise RuntimeError(
|
|
900
|
+
f"[{tier}] todos los datasets candidates fallaron. Ultimo error: {last_exc}"
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
def prepare_tier(self, tier_index: int, force: bool = False) -> Dict[str, int]:
|
|
904
|
+
tier = self.config.TIER_NAMES[tier_index]
|
|
905
|
+
if not force and self._already_prepared(tier):
|
|
906
|
+
with open(self._manifest_path(tier), "r", encoding="utf-8") as f:
|
|
907
|
+
return json.load(f)["sizes"]
|
|
908
|
+
|
|
909
|
+
ds = self._load_hf_with_fallback(tier)
|
|
910
|
+
attr, extra_kwargs = self._HF_KWARGS[tier]
|
|
911
|
+
hf_id = getattr(self.config, attr)
|
|
912
|
+
formatter = self._get_formatter(tier)
|
|
913
|
+
rng = np.random.default_rng(self.config.SPLIT_SEED + tier_index)
|
|
914
|
+
n = len(ds)
|
|
915
|
+
idx = np.arange(n)
|
|
916
|
+
rng.shuffle(idx)
|
|
917
|
+
n_hold = max(1, int(n * self.config.HOLDOUT_FRACTION))
|
|
918
|
+
n_val = max(1, int(n * self.config.VAL_FRACTION))
|
|
919
|
+
hold_idx = set(idx[:n_hold].tolist())
|
|
920
|
+
val_idx = set(idx[n_hold:n_hold + n_val].tolist())
|
|
921
|
+
# train_idx implicito = el resto
|
|
922
|
+
|
|
923
|
+
eot = self.tokenizer.eot_token()
|
|
924
|
+
max_tokens = self.config.MAX_TOKENS_PER_TIER[tier_index]
|
|
925
|
+
sizes = {"train": 0, "val": 0, "holdout": 0}
|
|
926
|
+
files = {
|
|
927
|
+
split: open(self._tier_paths(tier)[split], "wb")
|
|
928
|
+
for split in ("train", "val", "holdout")
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
batch_tokens: Dict[str, List[int]] = {k: [] for k in files}
|
|
932
|
+
CHUNK = 1_000_000
|
|
933
|
+
|
|
934
|
+
def flush(split: str) -> None:
|
|
935
|
+
arr = np.array(batch_tokens[split], dtype=np.int32)
|
|
936
|
+
files[split].write(arr.tobytes())
|
|
937
|
+
sizes[split] += arr.size
|
|
938
|
+
batch_tokens[split] = []
|
|
939
|
+
|
|
940
|
+
train_cap_hit = False
|
|
941
|
+
for i, ex in enumerate(ds):
|
|
942
|
+
if i in hold_idx:
|
|
943
|
+
split = "holdout"
|
|
944
|
+
elif i in val_idx:
|
|
945
|
+
split = "val"
|
|
946
|
+
else:
|
|
947
|
+
if train_cap_hit:
|
|
948
|
+
continue
|
|
949
|
+
split = "train"
|
|
950
|
+
|
|
951
|
+
text = formatter(ex)
|
|
952
|
+
if not text:
|
|
953
|
+
continue
|
|
954
|
+
try:
|
|
955
|
+
ids = self.tokenizer.encode(text)
|
|
956
|
+
except Exception:
|
|
957
|
+
continue
|
|
958
|
+
batch_tokens[split].extend(ids)
|
|
959
|
+
batch_tokens[split].append(eot)
|
|
960
|
+
|
|
961
|
+
if len(batch_tokens[split]) >= CHUNK:
|
|
962
|
+
flush(split)
|
|
963
|
+
if sizes["train"] >= max_tokens > 0:
|
|
964
|
+
train_cap_hit = True
|
|
965
|
+
|
|
966
|
+
for split in files:
|
|
967
|
+
if batch_tokens[split]:
|
|
968
|
+
flush(split)
|
|
969
|
+
files[split].close()
|
|
970
|
+
|
|
971
|
+
if sizes["train"] == 0:
|
|
972
|
+
for p in self._tier_paths(tier).values():
|
|
973
|
+
try:
|
|
974
|
+
p.unlink()
|
|
975
|
+
except OSError:
|
|
976
|
+
pass
|
|
977
|
+
raise RuntimeError(
|
|
978
|
+
f"[{tier}] el formateador no produjo ejemplos validos. "
|
|
979
|
+
f"Revisa el esquema del dataset (columnas esperadas vs reales)."
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
manifest = {
|
|
983
|
+
"tier": tier,
|
|
984
|
+
"hf_id": hf_id,
|
|
985
|
+
"hf_kwargs": extra_kwargs,
|
|
986
|
+
"sizes": sizes,
|
|
987
|
+
"split_seed": self.config.SPLIT_SEED + tier_index,
|
|
988
|
+
"saved_at": datetime.utcnow().isoformat(),
|
|
989
|
+
}
|
|
990
|
+
with open(self._manifest_path(tier), "w", encoding="utf-8") as f:
|
|
991
|
+
json.dump(manifest, f, indent=2)
|
|
992
|
+
self.logger.info(
|
|
993
|
+
f"[{tier}] train={sizes['train']:,} val={sizes['val']:,} "
|
|
994
|
+
f"holdout={sizes['holdout']:,} tokens"
|
|
995
|
+
)
|
|
996
|
+
return sizes
|
|
997
|
+
|
|
998
|
+
def open_memmap(self, tier: str, split: str) -> np.memmap:
|
|
999
|
+
path = self._tier_paths(tier)[split]
|
|
1000
|
+
if not path.exists():
|
|
1001
|
+
raise FileNotFoundError(
|
|
1002
|
+
f"Falta {path}. Corre --prepare-data primero."
|
|
1003
|
+
)
|
|
1004
|
+
n = path.stat().st_size // 4
|
|
1005
|
+
return np.memmap(path, dtype=np.int32, mode="r", shape=(n,))
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
# ============================================================================
|
|
1009
|
+
# DATASET DE BLOQUES SOBRE TOKEN STREAM
|
|
1010
|
+
# ============================================================================
|
|
1011
|
+
|
|
1012
|
+
class BlockTokenDataset(torch.utils.data.Dataset):
|
|
1013
|
+
"""
|
|
1014
|
+
Dataset autoregresivo sobre un stream de tokens.
|
|
1015
|
+
Cada item es (x, y) con shape [seq_len].
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
def __init__(self, tokens: np.memmap, seq_len: int):
|
|
1019
|
+
self.tokens = tokens
|
|
1020
|
+
self.seq_len = seq_len
|
|
1021
|
+
self.n_blocks = max(0, (len(tokens) - 1) // seq_len)
|
|
1022
|
+
|
|
1023
|
+
def __len__(self) -> int:
|
|
1024
|
+
return self.n_blocks
|
|
1025
|
+
|
|
1026
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1027
|
+
start = idx * self.seq_len
|
|
1028
|
+
end = start + self.seq_len + 1
|
|
1029
|
+
block = np.array(self.tokens[start:end], dtype=np.int64)
|
|
1030
|
+
x = torch.from_numpy(block[:-1])
|
|
1031
|
+
y = torch.from_numpy(block[1:])
|
|
1032
|
+
return x, y
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# ============================================================================
|
|
1036
|
+
# CHECKPOINTING
|
|
1037
|
+
# ============================================================================
|
|
1038
|
+
|
|
1039
|
+
class CheckpointStore:
|
|
1040
|
+
"""Persiste pesos del modelo + estado del trainer (sin AMP scaler para portabilidad)."""
|
|
1041
|
+
|
|
1042
|
+
def __init__(self, root: str, max_keep: int, logger: logging.Logger):
|
|
1043
|
+
self.root = Path(root)
|
|
1044
|
+
self.root.mkdir(parents=True, exist_ok=True)
|
|
1045
|
+
self.max_keep = max_keep
|
|
1046
|
+
self.logger = logger
|
|
1047
|
+
self.last_save_time = time.time()
|
|
1048
|
+
|
|
1049
|
+
def save(self, tag: str, model: nn.Module, optimizer: torch.optim.Optimizer,
|
|
1050
|
+
state: Dict[str, Any]) -> None:
|
|
1051
|
+
"""Guarda checkpoint atomico en <root>/last/ sobreescribiendo el anterior.
|
|
1052
|
+
|
|
1053
|
+
El argumento `tag` se conserva por compatibilidad pero se ignora: solo
|
|
1054
|
+
existe un checkpoint llamado `last` y los pesos en safetensors.
|
|
1055
|
+
"""
|
|
1056
|
+
from safetensors.torch import save_file as st_save
|
|
1057
|
+
del tag
|
|
1058
|
+
out = self.root / "last"
|
|
1059
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
1060
|
+
model_path = out / "model.safetensors"
|
|
1061
|
+
optim_path = out / "optim.pt"
|
|
1062
|
+
state_path = out / "state.json"
|
|
1063
|
+
tmp_model = model_path.with_suffix(".safetensors.tmp")
|
|
1064
|
+
tmp_optim = optim_path.with_suffix(".pt.tmp")
|
|
1065
|
+
tmp_state = state_path.with_suffix(".json.tmp")
|
|
1066
|
+
|
|
1067
|
+
st_save({k: v.detach().cpu() for k, v in model.state_dict().items()},
|
|
1068
|
+
str(tmp_model))
|
|
1069
|
+
torch.save(optimizer.state_dict(), tmp_optim)
|
|
1070
|
+
with open(tmp_state, "w", encoding="utf-8") as f:
|
|
1071
|
+
json.dump(state, f, indent=2, default=str)
|
|
1072
|
+
os.replace(tmp_model, model_path)
|
|
1073
|
+
os.replace(tmp_optim, optim_path)
|
|
1074
|
+
os.replace(tmp_state, state_path)
|
|
1075
|
+
|
|
1076
|
+
self.last_save_time = time.time()
|
|
1077
|
+
size_mb = model_path.stat().st_size / 1024 / 1024
|
|
1078
|
+
self.logger.info(
|
|
1079
|
+
f"[checkpoint] guardado en {model_path} "
|
|
1080
|
+
f"({size_mb:.1f} MB, step={state.get('global_step', '?')})"
|
|
1081
|
+
)
|
|
1082
|
+
|
|
1083
|
+
def load_latest(self, model: nn.Module, optimizer: torch.optim.Optimizer
|
|
1084
|
+
) -> Optional[Dict[str, Any]]:
|
|
1085
|
+
from safetensors.torch import load_file as st_load
|
|
1086
|
+
latest = self.root / "last"
|
|
1087
|
+
if not (latest / "model.safetensors").exists():
|
|
1088
|
+
legacy = sorted(
|
|
1089
|
+
[p for p in self.root.iterdir()
|
|
1090
|
+
if p.is_dir() and p.name.startswith("step_")
|
|
1091
|
+
and (p / "model.safetensors").exists()],
|
|
1092
|
+
key=lambda p: p.stat().st_mtime,
|
|
1093
|
+
)
|
|
1094
|
+
if not legacy:
|
|
1095
|
+
return None
|
|
1096
|
+
latest = legacy[-1]
|
|
1097
|
+
self.logger.info(f"No existe 'last/', cargando legacy: {latest.name}")
|
|
1098
|
+
sd = st_load(str(latest / "model.safetensors"))
|
|
1099
|
+
model.load_state_dict(sd, strict=False)
|
|
1100
|
+
if (latest / "optim.pt").exists():
|
|
1101
|
+
optimizer.load_state_dict(torch.load(latest / "optim.pt", map_location="cpu", weights_only=True))
|
|
1102
|
+
with open(latest / "state.json", "r", encoding="utf-8") as f:
|
|
1103
|
+
state = json.load(f)
|
|
1104
|
+
self.logger.info(f"Reanudado desde {latest}")
|
|
1105
|
+
return state
|
|
1106
|
+
|
|
1107
|
+
def should_save(self, interval_min: int) -> bool:
|
|
1108
|
+
return (time.time() - self.last_save_time) >= interval_min * 60
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
# ============================================================================
|
|
1112
|
+
# TRAINER CURRICULAR
|
|
1113
|
+
# ============================================================================
|
|
1114
|
+
|
|
1115
|
+
class TopoGPT3Trainer:
|
|
1116
|
+
"""
|
|
1117
|
+
Orquesta el curriculum sobre los 4 tiers.
|
|
1118
|
+
|
|
1119
|
+
Pipeline por tier:
|
|
1120
|
+
1. Abre memmap de tokens (train/val/holdout).
|
|
1121
|
+
2. Construye DataLoaders con seq_len(tier).
|
|
1122
|
+
3. Entrena TIER_EPOCHS[tier] epocas con AMP + grad accum.
|
|
1123
|
+
4. Cada GRASS_TRACK_EVERY steps: snapshot Grassmanniano.
|
|
1124
|
+
5. Al final de cada epoca: eval en VAL.
|
|
1125
|
+
6. Al final del tier: eval en HOLDOUT (datos nunca vistos).
|
|
1126
|
+
7. Checkpoint y avanza al siguiente tier.
|
|
1127
|
+
|
|
1128
|
+
Al final del pipeline: eval en HOLDOUT *combinado* de los 4 tiers.
|
|
1129
|
+
"""
|
|
1130
|
+
|
|
1131
|
+
def __init__(self, config: TopoGPT3Config, start_tier: Optional[int] = None,
|
|
1132
|
+
exploitgym_loader: Optional['ExploitGymDataLoader'] = None,
|
|
1133
|
+
topogpt3_loader: Optional['CodeCurriculumLoader'] = None,
|
|
1134
|
+
merged_config: Optional['TopoMergedConfig'] = None):
|
|
1135
|
+
self.config = config
|
|
1136
|
+
self.exploitgym_loader = exploitgym_loader
|
|
1137
|
+
self.topogpt3_loader = topogpt3_loader
|
|
1138
|
+
self.merged_config = merged_config
|
|
1139
|
+
self.logger = setup_logger("TopoGPT3", "INFO")
|
|
1140
|
+
self.start_tier_explicit = start_tier is not None
|
|
1141
|
+
self.start_tier = 0 if start_tier is None else int(start_tier)
|
|
1142
|
+
if not (0 <= self.start_tier < len(config.TIER_NAMES)):
|
|
1143
|
+
raise ValueError(
|
|
1144
|
+
f"start_tier fuera de rango: {self.start_tier} "
|
|
1145
|
+
f"(valido 0..{len(config.TIER_NAMES) - 1})"
|
|
1146
|
+
)
|
|
1147
|
+
set_seed(config.RANDOM_SEED, config.DEVICE)
|
|
1148
|
+
|
|
1149
|
+
# Activa el truco de Gauss ANTES de instanciar el modelo para que
|
|
1150
|
+
# todas las QuaternionSpectralLayer ya usen 3 productos reales.
|
|
1151
|
+
if config.USE_GAUSS_COMPLEX_MULT:
|
|
1152
|
+
apply_gauss_patch(self.logger)
|
|
1153
|
+
|
|
1154
|
+
# max_seq fijo sobre TODOS los tiers: la forma del embedding posicional
|
|
1155
|
+
# queda estable y los safetensors guardados se cargan sin mismatch
|
|
1156
|
+
# aunque se cambie start_tier entre runs.
|
|
1157
|
+
max_seq = max(config.TIER_SEQ_LEN)
|
|
1158
|
+
max_window = max(config.PROGRESSIVE_WINDOW)
|
|
1159
|
+
self.base_cfg = config.build_topogpt2_config(max_seq, attn_window=max_window)
|
|
1160
|
+
self.tokenizer = BPETokenizer()
|
|
1161
|
+
self.model = TopoGPT2(self.base_cfg).to(config.DEVICE)
|
|
1162
|
+
self.optimizer = torch.optim.AdamW(
|
|
1163
|
+
self.model.parameters(),
|
|
1164
|
+
lr=config.LEARNING_RATE,
|
|
1165
|
+
weight_decay=config.WEIGHT_DECAY,
|
|
1166
|
+
betas=(0.9, 0.95),
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
dev = config.DEVICE
|
|
1170
|
+
self.amp_dtype = torch.float16 if "cuda" in dev else torch.bfloat16
|
|
1171
|
+
self.scaler = torch.amp.GradScaler(
|
|
1172
|
+
dev.split(":")[0],
|
|
1173
|
+
enabled=config.USE_AMP and "cuda" in dev,
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
self.loader = CodeCurriculumLoader(config, self.tokenizer, self.logger)
|
|
1177
|
+
self.tracker = GrassmannianTracker(config, self.logger)
|
|
1178
|
+
self.ckpt = CheckpointStore(config.CHECKPOINT_DIR, config.MAX_CHECKPOINTS, self.logger)
|
|
1179
|
+
self.efficiency = EfficiencyMetrics(
|
|
1180
|
+
self.model, config, self.logger,
|
|
1181
|
+
gauss_enabled=config.USE_GAUSS_COMPLEX_MULT,
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
self.global_step = 0
|
|
1185
|
+
self.completed_tiers: List[str] = []
|
|
1186
|
+
self.tier_metrics: Dict[str, Dict[str, float]] = {}
|
|
1187
|
+
|
|
1188
|
+
# Anti-forgetting: EWC + Replay
|
|
1189
|
+
self.ewc = EWCRegularizer(
|
|
1190
|
+
self.model,
|
|
1191
|
+
lambda_ewc=config.EWC_LAMBDA,
|
|
1192
|
+
fisher_num_samples=config.EWC_FISHER_BATCHES,
|
|
1193
|
+
) if config.USE_EWC else None
|
|
1194
|
+
self.replay = ReplayBuffer(
|
|
1195
|
+
max_tiers=config.REPLAY_MAX_TIERS,
|
|
1196
|
+
replay_ratio=config.REPLAY_RATIO,
|
|
1197
|
+
) if config.USE_REPLAY else None
|
|
1198
|
+
|
|
1199
|
+
# ------------------------------------------------------------------
|
|
1200
|
+
# Preparacion de datos
|
|
1201
|
+
# ------------------------------------------------------------------
|
|
1202
|
+
|
|
1203
|
+
def prepare_all(self, force: bool = False) -> None:
|
|
1204
|
+
if self.merged_config is not None:
|
|
1205
|
+
# Merged mode: prepare both data sources
|
|
1206
|
+
for i, tier in enumerate(self.config.TIER_NAMES):
|
|
1207
|
+
self.logger.info(
|
|
1208
|
+
f"=== preparing tier {i+1}/{len(self.config.TIER_NAMES)}: {tier} ==="
|
|
1209
|
+
)
|
|
1210
|
+
try:
|
|
1211
|
+
if self.merged_config.is_code_tier(i):
|
|
1212
|
+
self.loader.prepare_tier(i, force=force)
|
|
1213
|
+
else:
|
|
1214
|
+
self.exploitgym_loader.prepare_tier(
|
|
1215
|
+
self.merged_config.exploit_tier_index(i), force=force
|
|
1216
|
+
)
|
|
1217
|
+
except RuntimeError as exc:
|
|
1218
|
+
self.logger.error(f"[{tier}] preparation failed: {exc}")
|
|
1219
|
+
return
|
|
1220
|
+
if self.exploitgym_loader is not None:
|
|
1221
|
+
for i, tier in enumerate(self.config.TIER_NAMES):
|
|
1222
|
+
self.logger.info(
|
|
1223
|
+
f"=== preparando exploitgym tier {i+1}/{len(self.config.TIER_NAMES)}: {tier} ==="
|
|
1224
|
+
)
|
|
1225
|
+
try:
|
|
1226
|
+
self.exploitgym_loader.prepare_tier(i, force=force)
|
|
1227
|
+
except RuntimeError as exc:
|
|
1228
|
+
self.logger.error(f"[{tier}] preparacion fallida: {exc}")
|
|
1229
|
+
return
|
|
1230
|
+
for i, tier in enumerate(self.config.TIER_NAMES):
|
|
1231
|
+
self.logger.info(
|
|
1232
|
+
f"=== preparando tier {i+1}/{len(self.config.TIER_NAMES)}: {tier} ==="
|
|
1233
|
+
)
|
|
1234
|
+
try:
|
|
1235
|
+
self.loader.prepare_tier(i, force=force)
|
|
1236
|
+
except RuntimeError as exc:
|
|
1237
|
+
self.logger.error(f"[{tier}] preparacion fallida: {exc}")
|
|
1238
|
+
self.logger.error(f"[{tier}] se omite; otros tiers continuan.")
|
|
1239
|
+
|
|
1240
|
+
# ------------------------------------------------------------------
|
|
1241
|
+
# Loaders por tier
|
|
1242
|
+
# ------------------------------------------------------------------
|
|
1243
|
+
|
|
1244
|
+
def _build_loaders(self, tier_index: int) -> Tuple[
|
|
1245
|
+
torch.utils.data.DataLoader,
|
|
1246
|
+
torch.utils.data.DataLoader,
|
|
1247
|
+
torch.utils.data.DataLoader,
|
|
1248
|
+
]:
|
|
1249
|
+
tier = self.config.TIER_NAMES[tier_index]
|
|
1250
|
+
seq_len = self.config.TIER_SEQ_LEN[tier_index]
|
|
1251
|
+
|
|
1252
|
+
# Determine data source: merged mode uses per-tier source selection
|
|
1253
|
+
use_exploitgym = False
|
|
1254
|
+
if self.merged_config is not None and self.merged_config.is_exploit_tier(tier_index):
|
|
1255
|
+
use_exploitgym = True
|
|
1256
|
+
elif self.exploitgym_loader is not None and self.topogpt3_loader is None:
|
|
1257
|
+
use_exploitgym = True
|
|
1258
|
+
|
|
1259
|
+
if use_exploitgym:
|
|
1260
|
+
loader = self.exploitgym_loader
|
|
1261
|
+
# exploitgym tiers are 0-indexed within exploitgym data
|
|
1262
|
+
egym_idx = tier_index
|
|
1263
|
+
if self.merged_config is not None:
|
|
1264
|
+
egym_idx = self.merged_config.exploit_tier_index(tier_index)
|
|
1265
|
+
train_tok = loader.open_memmap(self.config.TIER_NAMES[egym_idx], "train")
|
|
1266
|
+
val_tok = loader.open_memmap(self.config.TIER_NAMES[egym_idx], "val")
|
|
1267
|
+
hold_tok = loader.open_memmap(self.config.TIER_NAMES[egym_idx], "holdout")
|
|
1268
|
+
else:
|
|
1269
|
+
loader = self.topogpt3_loader if self.topogpt3_loader is not None else self.loader
|
|
1270
|
+
train_tok = loader.open_memmap(tier, "train")
|
|
1271
|
+
val_tok = loader.open_memmap(tier, "val")
|
|
1272
|
+
hold_tok = loader.open_memmap(tier, "holdout")
|
|
1273
|
+
|
|
1274
|
+
train_ds = BlockTokenDataset(train_tok, seq_len)
|
|
1275
|
+
val_ds = BlockTokenDataset(val_tok, seq_len)
|
|
1276
|
+
hold_ds = BlockTokenDataset(hold_tok, seq_len)
|
|
1277
|
+
|
|
1278
|
+
bs = self.config.BATCH_SIZE
|
|
1279
|
+
train_dl = torch.utils.data.DataLoader(
|
|
1280
|
+
train_ds, batch_size=bs, shuffle=True, drop_last=True, num_workers=0
|
|
1281
|
+
)
|
|
1282
|
+
val_dl = torch.utils.data.DataLoader(
|
|
1283
|
+
val_ds, batch_size=bs, shuffle=False, drop_last=True, num_workers=0
|
|
1284
|
+
)
|
|
1285
|
+
hold_dl = torch.utils.data.DataLoader(
|
|
1286
|
+
hold_ds, batch_size=bs, shuffle=False, drop_last=True, num_workers=0
|
|
1287
|
+
)
|
|
1288
|
+
return train_dl, val_dl, hold_dl
|
|
1289
|
+
|
|
1290
|
+
# ------------------------------------------------------------------
|
|
1291
|
+
# LR schedule (cosine con warmup)
|
|
1292
|
+
# ------------------------------------------------------------------
|
|
1293
|
+
|
|
1294
|
+
def _cosine_lr(self, step: int, total_steps: int) -> float:
|
|
1295
|
+
warmup = max(1, int(total_steps * self.config.WARMUP_RATIO))
|
|
1296
|
+
if step < warmup:
|
|
1297
|
+
return self.config.LEARNING_RATE * step / warmup
|
|
1298
|
+
t = (step - warmup) / max(total_steps - warmup, 1)
|
|
1299
|
+
return self.config.LEARNING_RATE * 0.5 * (1.0 + math.cos(math.pi * t))
|
|
1300
|
+
|
|
1301
|
+
def _set_lr(self, lr: float) -> None:
|
|
1302
|
+
for pg in self.optimizer.param_groups:
|
|
1303
|
+
pg["lr"] = lr
|
|
1304
|
+
|
|
1305
|
+
# ------------------------------------------------------------------
|
|
1306
|
+
# Train loop por tier
|
|
1307
|
+
# ------------------------------------------------------------------
|
|
1308
|
+
|
|
1309
|
+
def _train_one_tier(self, tier_index: int) -> Optional[Dict[str, float]]:
|
|
1310
|
+
tier = self.config.TIER_NAMES[tier_index]
|
|
1311
|
+
epochs = self.config.TIER_EPOCHS[tier_index]
|
|
1312
|
+
seq_len = self.config.TIER_SEQ_LEN[tier_index]
|
|
1313
|
+
try:
|
|
1314
|
+
train_dl, val_dl, hold_dl = self._build_loaders(tier_index)
|
|
1315
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
1316
|
+
self.logger.warning(
|
|
1317
|
+
f"[{tier}] saltado: no hay datos preparados ({exc})."
|
|
1318
|
+
)
|
|
1319
|
+
return None
|
|
1320
|
+
if len(train_dl.dataset) == 0:
|
|
1321
|
+
self.logger.warning(f"[{tier}] saltado: train_blocks=0.")
|
|
1322
|
+
return None
|
|
1323
|
+
|
|
1324
|
+
steps_per_epoch = len(train_dl) // self.config.GRAD_ACCUM_STEPS
|
|
1325
|
+
if self.config.MAX_TRAIN_BATCHES_PER_EPOCH > 0:
|
|
1326
|
+
steps_per_epoch = min(
|
|
1327
|
+
steps_per_epoch,
|
|
1328
|
+
self.config.MAX_TRAIN_BATCHES_PER_EPOCH // self.config.GRAD_ACCUM_STEPS,
|
|
1329
|
+
)
|
|
1330
|
+
total_steps = steps_per_epoch * epochs
|
|
1331
|
+
self.logger.info(
|
|
1332
|
+
f"### TIER {tier_index+1}/{len(self.config.TIER_NAMES)}: {tier} "
|
|
1333
|
+
f"| epochs={epochs} seq_len={seq_len} "
|
|
1334
|
+
f"| train_blocks={len(train_dl.dataset)} val={len(val_dl.dataset)} "
|
|
1335
|
+
f"hold={len(hold_dl.dataset)} optim_steps={total_steps}"
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
step_in_tier = 0
|
|
1339
|
+
last_val_loss = float("inf")
|
|
1340
|
+
last_val_acc = 0.0
|
|
1341
|
+
|
|
1342
|
+
for epoch in range(1, epochs + 1):
|
|
1343
|
+
self.model.train()
|
|
1344
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
1345
|
+
running_loss = 0.0
|
|
1346
|
+
running_correct = 0
|
|
1347
|
+
running_total = 0
|
|
1348
|
+
seen = 0
|
|
1349
|
+
batches_since_step = 0
|
|
1350
|
+
|
|
1351
|
+
for batch_idx, (bx, by) in enumerate(train_dl):
|
|
1352
|
+
if (self.config.MAX_TRAIN_BATCHES_PER_EPOCH > 0
|
|
1353
|
+
and batch_idx >= self.config.MAX_TRAIN_BATCHES_PER_EPOCH):
|
|
1354
|
+
break
|
|
1355
|
+
bx = bx.to(self.config.DEVICE, non_blocking=True)
|
|
1356
|
+
by = by.to(self.config.DEVICE, non_blocking=True)
|
|
1357
|
+
|
|
1358
|
+
# Replay: mix in data from previous tiers
|
|
1359
|
+
if self.replay is not None and tier_index > 0:
|
|
1360
|
+
replay_batch = self.replay.sample_batch(bx.shape[0])
|
|
1361
|
+
if replay_batch is not None:
|
|
1362
|
+
rx, ry = replay_batch
|
|
1363
|
+
rx = rx.to(self.config.DEVICE, non_blocking=True)
|
|
1364
|
+
ry = ry.to(self.config.DEVICE, non_blocking=True)
|
|
1365
|
+
# Pad replay to match current seq_len if needed
|
|
1366
|
+
cur_len = bx.shape[1]
|
|
1367
|
+
if rx.shape[1] < cur_len:
|
|
1368
|
+
pad_size = cur_len - rx.shape[1]
|
|
1369
|
+
rx = F.pad(rx, (0, pad_size), value=0)
|
|
1370
|
+
ry = F.pad(ry, (0, pad_size), value=0)
|
|
1371
|
+
elif rx.shape[1] > cur_len:
|
|
1372
|
+
rx = rx[:, :cur_len]
|
|
1373
|
+
ry = ry[:, :cur_len]
|
|
1374
|
+
bx = torch.cat([bx, rx], dim=0)
|
|
1375
|
+
by = torch.cat([by, ry], dim=0)
|
|
1376
|
+
|
|
1377
|
+
lr = self._cosine_lr(step_in_tier, total_steps)
|
|
1378
|
+
self._set_lr(lr)
|
|
1379
|
+
|
|
1380
|
+
with torch.amp.autocast(
|
|
1381
|
+
device_type=self.config.DEVICE.split(":")[0],
|
|
1382
|
+
dtype=self.amp_dtype,
|
|
1383
|
+
enabled=self.config.USE_AMP,
|
|
1384
|
+
):
|
|
1385
|
+
logits, recon_loss, _ = self.model(bx)
|
|
1386
|
+
lm_loss = F.cross_entropy(
|
|
1387
|
+
logits.view(-1, self.base_cfg.VOCAB_SIZE),
|
|
1388
|
+
by.view(-1),
|
|
1389
|
+
)
|
|
1390
|
+
loss = (lm_loss + self.base_cfg.AE_RECON_WEIGHT * recon_loss
|
|
1391
|
+
) / self.config.GRAD_ACCUM_STEPS
|
|
1392
|
+
|
|
1393
|
+
# EWC: penalize deviation from previous tier's optimal params
|
|
1394
|
+
# Dynamic lambda: full for Tier 2, reduced for Tier 3+
|
|
1395
|
+
# Phase boundary: reduced EWC at code→exploit transition
|
|
1396
|
+
if self.ewc is not None and tier_index > 0:
|
|
1397
|
+
if (self.merged_config is not None
|
|
1398
|
+
and tier_index == self.merged_config.PHASE_BOUNDARY):
|
|
1399
|
+
# Phase transition: allow more plasticity
|
|
1400
|
+
ewc_scale = self.merged_config.EWC_PHASE2_SCALE
|
|
1401
|
+
elif tier_index == 1:
|
|
1402
|
+
ewc_scale = 1.0
|
|
1403
|
+
else:
|
|
1404
|
+
ewc_scale = 0.5
|
|
1405
|
+
ewc_loss = (self.ewc.penalty(self.model) * ewc_scale
|
|
1406
|
+
/ self.config.GRAD_ACCUM_STEPS)
|
|
1407
|
+
loss = loss + ewc_loss
|
|
1408
|
+
|
|
1409
|
+
self.scaler.scale(loss).backward()
|
|
1410
|
+
running_loss += float(lm_loss.item())
|
|
1411
|
+
with torch.no_grad():
|
|
1412
|
+
preds = logits.argmax(dim=-1)
|
|
1413
|
+
running_correct += int((preds == by).sum().item())
|
|
1414
|
+
running_total += int(by.numel())
|
|
1415
|
+
seen += 1
|
|
1416
|
+
batches_since_step += 1
|
|
1417
|
+
|
|
1418
|
+
if batches_since_step >= self.config.GRAD_ACCUM_STEPS:
|
|
1419
|
+
self.scaler.unscale_(self.optimizer)
|
|
1420
|
+
torch.nn.utils.clip_grad_norm_(
|
|
1421
|
+
self.model.parameters(), self.config.GRADIENT_CLIP_NORM
|
|
1422
|
+
)
|
|
1423
|
+
self.scaler.step(self.optimizer)
|
|
1424
|
+
self.scaler.update()
|
|
1425
|
+
self.optimizer.zero_grad(set_to_none=True)
|
|
1426
|
+
self.global_step += 1
|
|
1427
|
+
step_in_tier += 1
|
|
1428
|
+
batches_since_step = 0
|
|
1429
|
+
|
|
1430
|
+
if self.global_step % self.config.GRASS_TRACK_EVERY == 0:
|
|
1431
|
+
snap = self.tracker.snapshot(
|
|
1432
|
+
self.model,
|
|
1433
|
+
self.global_step,
|
|
1434
|
+
dataloader=val_dl,
|
|
1435
|
+
vocab_size=self.base_cfg.VOCAB_SIZE,
|
|
1436
|
+
)
|
|
1437
|
+
self.logger.info(
|
|
1438
|
+
f"[grass step={self.global_step}] "
|
|
1439
|
+
+ self.tracker.format_log(snap)
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
if self.global_step % self.config.LOG_INTERVAL_STEPS == 0:
|
|
1443
|
+
train_acc = running_correct / max(running_total, 1)
|
|
1444
|
+
self.logger.info(
|
|
1445
|
+
f"[tier={tier} ep={epoch} step={self.global_step}] "
|
|
1446
|
+
f"lm={running_loss/max(seen,1):.4f} "
|
|
1447
|
+
f"acc={train_acc*100:.2f}% "
|
|
1448
|
+
f"lr={lr:.2e}"
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
if self.ckpt.should_save(self.config.CHECKPOINT_EVERY_MIN):
|
|
1452
|
+
self.ckpt.save(
|
|
1453
|
+
f"step_{self.global_step:07d}",
|
|
1454
|
+
self.model, self.optimizer,
|
|
1455
|
+
self._state_dict(),
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1458
|
+
avg_train = running_loss / max(seen, 1)
|
|
1459
|
+
train_acc = running_correct / max(running_total, 1)
|
|
1460
|
+
val_loss, val_ppl, val_acc = self._evaluate(val_dl)
|
|
1461
|
+
last_val_loss = val_loss
|
|
1462
|
+
last_val_acc = val_acc
|
|
1463
|
+
self.logger.info(
|
|
1464
|
+
f"[tier={tier} epoch {epoch}/{epochs}] "
|
|
1465
|
+
f"train_lm={avg_train:.4f} train_acc={train_acc*100:.2f}% | "
|
|
1466
|
+
f"val_lm={val_loss:.4f} val_acc={val_acc*100:.2f}% val_ppl={val_ppl:.2f}"
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
# Holdout: datos del tier que nunca se vieron durante entrenamiento ni en val
|
|
1470
|
+
hold_loss, hold_ppl, hold_acc = self._evaluate(hold_dl)
|
|
1471
|
+
self.logger.info(
|
|
1472
|
+
f"[tier={tier} HOLDOUT-no-visto] "
|
|
1473
|
+
f"hold_lm={hold_loss:.4f} hold_acc={hold_acc*100:.2f}% hold_ppl={hold_ppl:.2f}"
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
# Mediciones de eficiencia (perf/param, perf/FLOP, perf/BW)
|
|
1477
|
+
eff = self.efficiency.compute(
|
|
1478
|
+
dataloader=val_dl, vocab_size=self.base_cfg.VOCAB_SIZE,
|
|
1479
|
+
val_loss=last_val_loss, val_ppl=math.exp(min(last_val_loss, 20.0)),
|
|
1480
|
+
val_acc=last_val_acc,
|
|
1481
|
+
batch_size=self.config.BATCH_SIZE, seq_len=seq_len,
|
|
1482
|
+
)
|
|
1483
|
+
self.logger.info(f"[tier={tier} EFFICIENCY] " + self.efficiency.format_log(eff))
|
|
1484
|
+
|
|
1485
|
+
self.ckpt.save(
|
|
1486
|
+
f"step_{self.global_step:07d}_end_{tier}",
|
|
1487
|
+
self.model, self.optimizer,
|
|
1488
|
+
self._state_dict(),
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
# Anti-forgetting: snapshot Fisher + register tier for replay
|
|
1492
|
+
if self.ewc is not None:
|
|
1493
|
+
self.logger.info(
|
|
1494
|
+
f"[{tier}] computing EWC fisher for next tier..."
|
|
1495
|
+
)
|
|
1496
|
+
self.ewc.compute_fisher(
|
|
1497
|
+
train_dl, self.base_cfg.VOCAB_SIZE,
|
|
1498
|
+
torch.device(self.config.DEVICE),
|
|
1499
|
+
max_batches=self.config.EWC_FISHER_BATCHES,
|
|
1500
|
+
)
|
|
1501
|
+
ewc_state_path = Path(self.config.CHECKPOINT_DIR) / "last" / "ewc_state.pt"
|
|
1502
|
+
self.ewc.save_state(str(ewc_state_path))
|
|
1503
|
+
|
|
1504
|
+
if self.replay is not None:
|
|
1505
|
+
self.replay.add_tier(tier_index, train_dl)
|
|
1506
|
+
self.logger.info(
|
|
1507
|
+
f"[{tier}] registered for replay (buffer: {len(self.replay._tiers)} tiers)"
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
# Anti-forgetting check: evaluate ALL previous tiers' holdouts
|
|
1511
|
+
self._check_forgetting(tier_index)
|
|
1512
|
+
|
|
1513
|
+
return {
|
|
1514
|
+
"tier": tier,
|
|
1515
|
+
"val_loss": last_val_loss,
|
|
1516
|
+
"val_acc": last_val_acc,
|
|
1517
|
+
"holdout_loss": hold_loss,
|
|
1518
|
+
"holdout_acc": hold_acc,
|
|
1519
|
+
"holdout_ppl": hold_ppl,
|
|
1520
|
+
"efficiency": eff,
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
# ------------------------------------------------------------------
|
|
1524
|
+
# Anti-forgetting: check previous tiers after each tier
|
|
1525
|
+
# ------------------------------------------------------------------
|
|
1526
|
+
|
|
1527
|
+
def _check_forgetting(self, completed_tier_index: int) -> None:
|
|
1528
|
+
"""Evaluate holdout of ALL previous tiers to detect catastrophic forgetting."""
|
|
1529
|
+
if completed_tier_index == 0:
|
|
1530
|
+
return # No previous tiers to check
|
|
1531
|
+
|
|
1532
|
+
self.logger.info("=" * 70)
|
|
1533
|
+
self.logger.info(
|
|
1534
|
+
f"ANTI-FORGETTING CHECK: evaluating holdouts of tiers 0..{completed_tier_index-1}"
|
|
1535
|
+
)
|
|
1536
|
+
for prev_idx in range(completed_tier_index):
|
|
1537
|
+
prev_tier = self.config.TIER_NAMES[prev_idx]
|
|
1538
|
+
try:
|
|
1539
|
+
_, _, hold_tok = self._open_memmaps(prev_idx)
|
|
1540
|
+
prev_seq_len = self.config.TIER_SEQ_LEN[prev_idx]
|
|
1541
|
+
hold_ds = BlockTokenDataset(hold_tok, prev_seq_len)
|
|
1542
|
+
hold_dl = torch.utils.data.DataLoader(
|
|
1543
|
+
hold_ds, batch_size=self.config.BATCH_SIZE,
|
|
1544
|
+
shuffle=False, drop_last=True, num_workers=0,
|
|
1545
|
+
)
|
|
1546
|
+
hold_loss, hold_ppl, hold_acc = self._evaluate(hold_dl)
|
|
1547
|
+
status = "OK" if hold_acc > 0.20 else "FORGOTTEN"
|
|
1548
|
+
self.logger.info(
|
|
1549
|
+
f" [{prev_tier}] hold_loss={hold_loss:.4f} "
|
|
1550
|
+
f"hold_acc={hold_acc*100:.2f}% hold_ppl={hold_ppl:.2f} "
|
|
1551
|
+
f"[{status}]"
|
|
1552
|
+
)
|
|
1553
|
+
except Exception as exc:
|
|
1554
|
+
self.logger.warning(f" [{prev_tier}] check failed: {exc}")
|
|
1555
|
+
self.logger.info("=" * 70)
|
|
1556
|
+
|
|
1557
|
+
def _open_memmaps(self, tier_index: int):
|
|
1558
|
+
"""Open train/val/holdout memmaps for a given tier."""
|
|
1559
|
+
tier = self.config.TIER_NAMES[tier_index]
|
|
1560
|
+
if self.merged_config is not None and self.merged_config.is_exploit_tier(tier_index):
|
|
1561
|
+
egym_idx = self.merged_config.exploit_tier_index(tier_index)
|
|
1562
|
+
return (
|
|
1563
|
+
self.exploitgym_loader.open_memmap(self.config.TIER_NAMES[egym_idx], "train"),
|
|
1564
|
+
self.exploitgym_loader.open_memmap(self.config.TIER_NAMES[egym_idx], "val"),
|
|
1565
|
+
self.exploitgym_loader.open_memmap(self.config.TIER_NAMES[egym_idx], "holdout"),
|
|
1566
|
+
)
|
|
1567
|
+
if self.exploitgym_loader is not None and self.topogpt3_loader is None:
|
|
1568
|
+
return (
|
|
1569
|
+
self.exploitgym_loader.open_memmap(tier, "train"),
|
|
1570
|
+
self.exploitgym_loader.open_memmap(tier, "val"),
|
|
1571
|
+
self.exploitgym_loader.open_memmap(tier, "holdout"),
|
|
1572
|
+
)
|
|
1573
|
+
loader = self.topogpt3_loader if self.topogpt3_loader is not None else self.loader
|
|
1574
|
+
return (
|
|
1575
|
+
loader.open_memmap(tier, "train"),
|
|
1576
|
+
loader.open_memmap(tier, "val"),
|
|
1577
|
+
loader.open_memmap(tier, "holdout"),
|
|
1578
|
+
)
|
|
1579
|
+
|
|
1580
|
+
# ------------------------------------------------------------------
|
|
1581
|
+
# Eval
|
|
1582
|
+
# ------------------------------------------------------------------
|
|
1583
|
+
|
|
1584
|
+
@torch.no_grad()
|
|
1585
|
+
def _evaluate(self, dl: torch.utils.data.DataLoader) -> Tuple[float, float, float]:
|
|
1586
|
+
"""Devuelve (avg_loss, perplexity, token_accuracy)."""
|
|
1587
|
+
self.model.eval()
|
|
1588
|
+
total_loss = 0.0
|
|
1589
|
+
n_batches = 0
|
|
1590
|
+
n_correct = 0
|
|
1591
|
+
n_tokens = 0
|
|
1592
|
+
for i, (bx, by) in enumerate(dl):
|
|
1593
|
+
if i >= self.config.MAX_EVAL_BATCHES:
|
|
1594
|
+
break
|
|
1595
|
+
bx = bx.to(self.config.DEVICE, non_blocking=True)
|
|
1596
|
+
by = by.to(self.config.DEVICE, non_blocking=True)
|
|
1597
|
+
with torch.amp.autocast(
|
|
1598
|
+
device_type=self.config.DEVICE.split(":")[0],
|
|
1599
|
+
dtype=self.amp_dtype,
|
|
1600
|
+
enabled=self.config.USE_AMP,
|
|
1601
|
+
):
|
|
1602
|
+
logits, _, _ = self.model(bx)
|
|
1603
|
+
loss = F.cross_entropy(
|
|
1604
|
+
logits.view(-1, self.base_cfg.VOCAB_SIZE),
|
|
1605
|
+
by.view(-1),
|
|
1606
|
+
)
|
|
1607
|
+
total_loss += float(loss.item())
|
|
1608
|
+
preds = logits.argmax(dim=-1)
|
|
1609
|
+
n_correct += int((preds == by).sum().item())
|
|
1610
|
+
n_tokens += int(by.numel())
|
|
1611
|
+
n_batches += 1
|
|
1612
|
+
self.model.train()
|
|
1613
|
+
if n_batches == 0:
|
|
1614
|
+
return float("inf"), float("inf"), 0.0
|
|
1615
|
+
avg = total_loss / n_batches
|
|
1616
|
+
acc = n_correct / max(n_tokens, 1)
|
|
1617
|
+
return avg, math.exp(min(avg, 20.0)), acc
|
|
1618
|
+
|
|
1619
|
+
# ------------------------------------------------------------------
|
|
1620
|
+
# Estado / checkpoint
|
|
1621
|
+
# ------------------------------------------------------------------
|
|
1622
|
+
|
|
1623
|
+
def _state_dict(self) -> Dict[str, Any]:
|
|
1624
|
+
return {
|
|
1625
|
+
"global_step": self.global_step,
|
|
1626
|
+
"completed_tiers": self.completed_tiers,
|
|
1627
|
+
"tier_metrics": self.tier_metrics,
|
|
1628
|
+
"config": asdict(self.config),
|
|
1629
|
+
"saved_at": datetime.utcnow().isoformat(),
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
# ------------------------------------------------------------------
|
|
1633
|
+
# Loops publicos
|
|
1634
|
+
# ------------------------------------------------------------------
|
|
1635
|
+
|
|
1636
|
+
def run(self) -> None:
|
|
1637
|
+
if self.start_tier_explicit:
|
|
1638
|
+
# El usuario fija el tier inicial: se honra tal cual y se recortan
|
|
1639
|
+
# los completed_tiers a [0, start_tier) para que el rerun no quede
|
|
1640
|
+
# marcado como "ya hecho" en el state.json del checkpoint.
|
|
1641
|
+
effective_start = self.start_tier
|
|
1642
|
+
allowed = set(self.config.TIER_NAMES[:effective_start])
|
|
1643
|
+
dropped = [t for t in self.completed_tiers if t not in allowed]
|
|
1644
|
+
if dropped:
|
|
1645
|
+
self.logger.info(
|
|
1646
|
+
f"--start-tier={effective_start} explicito: "
|
|
1647
|
+
f"se reentrenan tiers previamente completados: {dropped}"
|
|
1648
|
+
)
|
|
1649
|
+
self.completed_tiers = [t for t in self.completed_tiers if t in allowed]
|
|
1650
|
+
else:
|
|
1651
|
+
# Comportamiento por defecto: si hubo auto-resume, avanza
|
|
1652
|
+
# start_tier mas alla de los tiers ya completados.
|
|
1653
|
+
effective_start = self.start_tier
|
|
1654
|
+
for i, name in enumerate(self.config.TIER_NAMES):
|
|
1655
|
+
if name in self.completed_tiers and i == effective_start:
|
|
1656
|
+
effective_start = i + 1
|
|
1657
|
+
|
|
1658
|
+
self.logger.info("=" * 70)
|
|
1659
|
+
self.logger.info("TopoGPT3 - curriculum sobre Grassmanniana")
|
|
1660
|
+
p = self.model.count_params()
|
|
1661
|
+
self.logger.info(f"Parametros: {p['total']:,}")
|
|
1662
|
+
self.logger.info(f"Device: {self.config.DEVICE} | AMP: {self.config.USE_AMP}")
|
|
1663
|
+
if self.completed_tiers:
|
|
1664
|
+
self.logger.info(f"Tiers ya completados: {self.completed_tiers}")
|
|
1665
|
+
self.logger.info(
|
|
1666
|
+
f"Tiers a entrenar: {list(self.config.TIER_NAMES[effective_start:])}"
|
|
1667
|
+
)
|
|
1668
|
+
self.logger.info("=" * 70)
|
|
1669
|
+
|
|
1670
|
+
# Snapshot inicial geometrico (referencia U_ref)
|
|
1671
|
+
snap0 = self.tracker.snapshot(self.model, step=0)
|
|
1672
|
+
self.logger.info("[grass init] " + self.tracker.format_log(snap0))
|
|
1673
|
+
|
|
1674
|
+
for tier_index in range(effective_start, len(self.config.TIER_NAMES)):
|
|
1675
|
+
res = self._train_one_tier(tier_index)
|
|
1676
|
+
tier = self.config.TIER_NAMES[tier_index]
|
|
1677
|
+
if res is None:
|
|
1678
|
+
self.logger.warning(f"[{tier}] tier saltado por falta de datos.")
|
|
1679
|
+
continue
|
|
1680
|
+
if tier not in self.completed_tiers:
|
|
1681
|
+
self.completed_tiers.append(tier)
|
|
1682
|
+
self.tier_metrics[tier] = res
|
|
1683
|
+
|
|
1684
|
+
# Generalizacion combinada: concatenamos todos los holdouts
|
|
1685
|
+
self._eval_combined_holdout()
|
|
1686
|
+
self.tracker.save(str(Path(self.config.CHECKPOINT_DIR) / "grass_history.jsonl"))
|
|
1687
|
+
with open(Path(self.config.CHECKPOINT_DIR) / self.config.LOG_FILE, "w") as f:
|
|
1688
|
+
json.dump({
|
|
1689
|
+
"tier_metrics": self.tier_metrics,
|
|
1690
|
+
"global_step": self.global_step,
|
|
1691
|
+
}, f, indent=2)
|
|
1692
|
+
|
|
1693
|
+
def _eval_combined_holdout(self) -> None:
|
|
1694
|
+
self.logger.info("=" * 70)
|
|
1695
|
+
self.logger.info("EVALUACION FINAL: holdout combinado (datos NUNCA vistos)")
|
|
1696
|
+
results: Dict[str, Tuple[float, float, float]] = {}
|
|
1697
|
+
for i, tier in enumerate(self.config.TIER_NAMES):
|
|
1698
|
+
try:
|
|
1699
|
+
if self.exploitgym_loader is not None:
|
|
1700
|
+
hold = self.exploitgym_loader.open_memmap(tier, "holdout")
|
|
1701
|
+
else:
|
|
1702
|
+
hold = self.loader.open_memmap(tier, "holdout")
|
|
1703
|
+
except (FileNotFoundError, ValueError):
|
|
1704
|
+
continue
|
|
1705
|
+
seq_len = self.config.TIER_SEQ_LEN[i]
|
|
1706
|
+
ds = BlockTokenDataset(hold, seq_len)
|
|
1707
|
+
if len(ds) == 0:
|
|
1708
|
+
continue
|
|
1709
|
+
dl = torch.utils.data.DataLoader(
|
|
1710
|
+
ds, batch_size=self.config.BATCH_SIZE,
|
|
1711
|
+
shuffle=False, drop_last=True, num_workers=0,
|
|
1712
|
+
)
|
|
1713
|
+
loss, ppl, acc = self._evaluate(dl)
|
|
1714
|
+
results[tier] = (loss, ppl, acc)
|
|
1715
|
+
self.logger.info(
|
|
1716
|
+
f" {tier:<20s} loss={loss:.4f} acc={acc*100:.2f}% ppl={ppl:.2f}"
|
|
1717
|
+
)
|
|
1718
|
+
self.tier_metrics["__combined_holdout__"] = {
|
|
1719
|
+
t: {"loss": l, "ppl": p, "acc": a} for t, (l, p, a) in results.items()
|
|
1720
|
+
}
|
|
1721
|
+
self.logger.info("=" * 70)
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
# ============================================================================
|
|
1725
|
+
# CLI
|
|
1726
|
+
# ============================================================================
|
|
1727
|
+
|
|
1728
|
+
def parse_args() -> argparse.Namespace:
|
|
1729
|
+
p = argparse.ArgumentParser(description="TopoGPT3/TopoExploit training")
|
|
1730
|
+
p.add_argument("--prepare-data", action="store_true",
|
|
1731
|
+
help="Descarga y tokeniza los 4 datasets de codigo.")
|
|
1732
|
+
p.add_argument("--prepare-exploitgym", action="store_true",
|
|
1733
|
+
help="Descarga y tokeniza los datos de ExploitGym.")
|
|
1734
|
+
p.add_argument("--train", action="store_true",
|
|
1735
|
+
help="Entrena el pipeline curricular completo.")
|
|
1736
|
+
p.add_argument("--eval-holdout", action="store_true",
|
|
1737
|
+
help="Evalua solo en los holdouts (datos nunca vistos).")
|
|
1738
|
+
p.add_argument("--exploitgym", action="store_true",
|
|
1739
|
+
help="Usar configuracion TopoExploit (122M, ExploitGym data).")
|
|
1740
|
+
p.add_argument("--merged", action="store_true",
|
|
1741
|
+
help="Usar configuracion TopoMerged (6-tier: 3 code + 3 exploit).")
|
|
1742
|
+
p.add_argument("--start-tier", type=int, default=None,
|
|
1743
|
+
help="Tier inicial explicito.")
|
|
1744
|
+
p.add_argument("--scale", type=str, default=None,
|
|
1745
|
+
choices=["micro", "small", "medium", "large", "xlarge", "gpt2"],
|
|
1746
|
+
help="Escala del modelo TopoGPT2 base (default: small, o 'large' si --exploitgym, 'xlarge' si --merged).")
|
|
1747
|
+
p.add_argument("--resume", action="store_true",
|
|
1748
|
+
help="(legacy) Equivalente al comportamiento por defecto.")
|
|
1749
|
+
p.add_argument("--from-scratch", action="store_true",
|
|
1750
|
+
help="Ignora el checkpoint existente y arranca desde cero.")
|
|
1751
|
+
p.add_argument("--force-prepare", action="store_true",
|
|
1752
|
+
help="Fuerza re-tokenizacion ignorando cache.")
|
|
1753
|
+
p.add_argument("--device", type=str, default=None,
|
|
1754
|
+
help="Override de device (cuda|cpu|cuda:0|...)")
|
|
1755
|
+
p.add_argument("--ewc-lambda", type=float, default=None,
|
|
1756
|
+
help="Override de EWC_LAMBDA (fuerza de penalizacion EWC).")
|
|
1757
|
+
p.add_argument("--replay-ratio", type=float, default=None,
|
|
1758
|
+
help="Override de REPLAY_RATIO (fraccion de replay en batch).")
|
|
1759
|
+
p.add_argument("--fisher-batches", type=int, default=None,
|
|
1760
|
+
help="Override de EWC_FISHER_BATCHES (batches para estimar Fisher).")
|
|
1761
|
+
return p.parse_args()
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
def main() -> None:
|
|
1765
|
+
args = parse_args()
|
|
1766
|
+
|
|
1767
|
+
if args.exploitgym:
|
|
1768
|
+
cfg_exploit = TopoExploitConfig()
|
|
1769
|
+
if args.device:
|
|
1770
|
+
cfg_exploit.DEVICE = args.device
|
|
1771
|
+
if args.scale:
|
|
1772
|
+
cfg_exploit.SCALE = args.scale
|
|
1773
|
+
cfg = TopoGPT3Config(SCALE=cfg_exploit.SCALE)
|
|
1774
|
+
cfg.DEVICE = cfg_exploit.DEVICE
|
|
1775
|
+
cfg.USE_AMP = cfg_exploit.USE_AMP
|
|
1776
|
+
cfg.GRADIENT_CHECKPOINTING = cfg_exploit.GRADIENT_CHECKPOINTING
|
|
1777
|
+
cfg.BATCH_SIZE = cfg_exploit.BATCH_SIZE
|
|
1778
|
+
cfg.GRAD_ACCUM_STEPS = cfg_exploit.GRAD_ACCUM_STEPS
|
|
1779
|
+
cfg.LEARNING_RATE = cfg_exploit.LEARNING_RATE
|
|
1780
|
+
cfg.WEIGHT_DECAY = cfg_exploit.WEIGHT_DECAY
|
|
1781
|
+
cfg.GRADIENT_CLIP_NORM = cfg_exploit.GRADIENT_CLIP_NORM
|
|
1782
|
+
cfg.WARMUP_RATIO = cfg_exploit.WARMUP_RATIO
|
|
1783
|
+
cfg.TIER_EPOCHS = cfg_exploit.TIER_EPOCHS
|
|
1784
|
+
cfg.TIER_SEQ_LEN = cfg_exploit.TIER_SEQ_LEN
|
|
1785
|
+
cfg.TIER_NAMES = cfg_exploit.TIER_NAMES
|
|
1786
|
+
cfg.PROGRESSIVE_WINDOW = cfg_exploit.PROGRESSIVE_WINDOW
|
|
1787
|
+
cfg.CHECKPOINT_DIR = cfg_exploit.CHECKPOINT_DIR
|
|
1788
|
+
cfg.LOG_FILE = cfg_exploit.LOG_FILE
|
|
1789
|
+
cfg.GRASS_TRACK_EVERY = cfg_exploit.GRASS_TRACK_EVERY
|
|
1790
|
+
cfg.USE_GAUSS_COMPLEX_MULT = cfg_exploit.USE_GAUSS_COMPLEX_MULT
|
|
1791
|
+
cfg.DATA_CACHE = cfg_exploit.DATA_CACHE
|
|
1792
|
+
cfg.USE_EWC = cfg_exploit.USE_EWC
|
|
1793
|
+
cfg.EWC_LAMBDA = cfg_exploit.EWC_LAMBDA
|
|
1794
|
+
cfg.EWC_FISHER_BATCHES = cfg_exploit.EWC_FISHER_BATCHES
|
|
1795
|
+
cfg.USE_REPLAY = cfg_exploit.USE_REPLAY
|
|
1796
|
+
cfg.REPLAY_RATIO = cfg_exploit.REPLAY_RATIO
|
|
1797
|
+
cfg.REPLAY_MAX_TIERS = cfg_exploit.REPLAY_MAX_TIERS
|
|
1798
|
+
cfg.MAX_TRAIN_BATCHES_PER_EPOCH = cfg_exploit.MAX_TRAIN_BATCHES_PER_EPOCH
|
|
1799
|
+
# Overrides CLI opcionales
|
|
1800
|
+
if args.ewc_lambda is not None:
|
|
1801
|
+
cfg.EWC_LAMBDA = args.ewc_lambda
|
|
1802
|
+
if args.replay_ratio is not None:
|
|
1803
|
+
cfg.REPLAY_RATIO = args.replay_ratio
|
|
1804
|
+
if args.fisher_batches is not None:
|
|
1805
|
+
cfg.EWC_FISHER_BATCHES = args.fisher_batches
|
|
1806
|
+
exploitgym_cfg = cfg_exploit
|
|
1807
|
+
merged_cfg = None
|
|
1808
|
+
elif args.merged:
|
|
1809
|
+
cfg_merged = TopoMergedConfig()
|
|
1810
|
+
if args.device:
|
|
1811
|
+
cfg_merged.DEVICE = args.device
|
|
1812
|
+
if args.scale:
|
|
1813
|
+
cfg_merged.SCALE = args.scale
|
|
1814
|
+
cfg = TopoGPT3Config(SCALE=cfg_merged.SCALE)
|
|
1815
|
+
cfg.DEVICE = cfg_merged.DEVICE
|
|
1816
|
+
cfg.USE_AMP = cfg_merged.USE_AMP
|
|
1817
|
+
cfg.GRADIENT_CHECKPOINTING = cfg_merged.GRADIENT_CHECKPOINTING
|
|
1818
|
+
cfg.BATCH_SIZE = cfg_merged.BATCH_SIZE
|
|
1819
|
+
cfg.GRAD_ACCUM_STEPS = cfg_merged.GRAD_ACCUM_STEPS
|
|
1820
|
+
cfg.LEARNING_RATE = cfg_merged.LEARNING_RATE
|
|
1821
|
+
cfg.WEIGHT_DECAY = cfg_merged.WEIGHT_DECAY
|
|
1822
|
+
cfg.GRADIENT_CLIP_NORM = cfg_merged.GRADIENT_CLIP_NORM
|
|
1823
|
+
cfg.WARMUP_RATIO = cfg_merged.WARMUP_RATIO
|
|
1824
|
+
cfg.TIER_EPOCHS = cfg_merged.TIER_EPOCHS
|
|
1825
|
+
cfg.TIER_SEQ_LEN = cfg_merged.TIER_SEQ_LEN
|
|
1826
|
+
cfg.TIER_NAMES = cfg_merged.TIER_NAMES
|
|
1827
|
+
cfg.PROGRESSIVE_WINDOW = cfg_merged.PROGRESSIVE_WINDOW
|
|
1828
|
+
cfg.CHECKPOINT_DIR = cfg_merged.CHECKPOINT_DIR
|
|
1829
|
+
cfg.LOG_FILE = cfg_merged.LOG_FILE
|
|
1830
|
+
cfg.GRASS_TRACK_EVERY = cfg_merged.GRASS_TRACK_EVERY
|
|
1831
|
+
cfg.USE_GAUSS_COMPLEX_MULT = cfg_merged.USE_GAUSS_COMPLEX_MULT
|
|
1832
|
+
cfg.DATA_CACHE = cfg_merged.DATA_CACHE_TOPOGPT3
|
|
1833
|
+
cfg.USE_EWC = cfg_merged.USE_EWC
|
|
1834
|
+
cfg.EWC_LAMBDA = cfg_merged.EWC_LAMBDA
|
|
1835
|
+
cfg.EWC_FISHER_BATCHES = cfg_merged.EWC_FISHER_BATCHES
|
|
1836
|
+
cfg.USE_REPLAY = cfg_merged.USE_REPLAY
|
|
1837
|
+
cfg.REPLAY_RATIO = cfg_merged.REPLAY_RATIO
|
|
1838
|
+
cfg.REPLAY_MAX_TIERS = cfg_merged.REPLAY_MAX_TIERS
|
|
1839
|
+
cfg.MAX_TRAIN_BATCHES_PER_EPOCH = cfg_merged.MAX_TRAIN_BATCHES_PER_EPOCH
|
|
1840
|
+
if args.ewc_lambda is not None:
|
|
1841
|
+
cfg.EWC_LAMBDA = args.ewc_lambda
|
|
1842
|
+
if args.replay_ratio is not None:
|
|
1843
|
+
cfg.REPLAY_RATIO = args.replay_ratio
|
|
1844
|
+
if args.fisher_batches is not None:
|
|
1845
|
+
cfg.EWC_FISHER_BATCHES = args.fisher_batches
|
|
1846
|
+
exploitgym_cfg = None
|
|
1847
|
+
merged_cfg = cfg_merged
|
|
1848
|
+
else:
|
|
1849
|
+
cfg = TopoGPT3Config(SCALE=args.scale or "small")
|
|
1850
|
+
if args.device:
|
|
1851
|
+
cfg.DEVICE = args.device
|
|
1852
|
+
exploitgym_cfg = None
|
|
1853
|
+
merged_cfg = None
|
|
1854
|
+
|
|
1855
|
+
if not (args.prepare_data or args.prepare_exploitgym or args.train or args.eval_holdout):
|
|
1856
|
+
print("Nada que hacer. Pasa --prepare-data, --prepare-exploitgym, --train o --eval-holdout.",
|
|
1857
|
+
file=sys.stderr)
|
|
1858
|
+
sys.exit(1)
|
|
1859
|
+
|
|
1860
|
+
exploitgym_loader = None
|
|
1861
|
+
topogpt3_loader = None
|
|
1862
|
+
if merged_cfg is not None:
|
|
1863
|
+
# Merged mode: set up both loaders
|
|
1864
|
+
_tmp_tokenizer = BPETokenizer()
|
|
1865
|
+
_tmp_logger = setup_logger("MergedLoader", "INFO")
|
|
1866
|
+
# TopoGPT3 code loader (tiers 0-2)
|
|
1867
|
+
topogpt3_loader = CodeCurriculumLoader(cfg, _tmp_tokenizer, _tmp_logger)
|
|
1868
|
+
# ExploitGym loader (tiers 3-5)
|
|
1869
|
+
egym_loader_cfg = merged_cfg.build_exploit_loader_config()
|
|
1870
|
+
exploitgym_loader = ExploitGymDataLoader(egym_loader_cfg, _tmp_tokenizer, _tmp_logger)
|
|
1871
|
+
elif exploitgym_cfg is not None:
|
|
1872
|
+
loader_cfg = exploitgym_cfg.build_loader_config()
|
|
1873
|
+
_tmp_tokenizer = BPETokenizer()
|
|
1874
|
+
_tmp_logger = setup_logger("ExploitGymLoader", "INFO")
|
|
1875
|
+
exploitgym_loader = ExploitGymDataLoader(loader_cfg, _tmp_tokenizer, _tmp_logger)
|
|
1876
|
+
|
|
1877
|
+
trainer = TopoGPT3Trainer(cfg, start_tier=args.start_tier,
|
|
1878
|
+
exploitgym_loader=exploitgym_loader,
|
|
1879
|
+
topogpt3_loader=topogpt3_loader,
|
|
1880
|
+
merged_config=merged_cfg)
|
|
1881
|
+
|
|
1882
|
+
if args.prepare_exploitgym and exploitgym_cfg is not None:
|
|
1883
|
+
loader_cfg = exploitgym_cfg.build_loader_config()
|
|
1884
|
+
egym_loader = ExploitGymDataLoader(loader_cfg, trainer.tokenizer, trainer.logger)
|
|
1885
|
+
for i in range(len(exploitgym_cfg.TIER_NAMES)):
|
|
1886
|
+
try:
|
|
1887
|
+
trainer.logger.info(
|
|
1888
|
+
f"=== preparing exploitgym tier {i+1}/{len(exploitgym_cfg.TIER_NAMES)} ==="
|
|
1889
|
+
)
|
|
1890
|
+
egym_loader.prepare_tier(i, force=args.force_prepare)
|
|
1891
|
+
except RuntimeError as exc:
|
|
1892
|
+
trainer.logger.error(f"ExploitGym tier {i} failed: {exc}")
|
|
1893
|
+
return
|
|
1894
|
+
|
|
1895
|
+
if args.prepare_data:
|
|
1896
|
+
trainer.prepare_all(force=args.force_prepare)
|
|
1897
|
+
|
|
1898
|
+
ckpt_exists = (Path(cfg.CHECKPOINT_DIR) / "last" / "model.safetensors").exists()
|
|
1899
|
+
legacy_exists = any(
|
|
1900
|
+
(Path(cfg.CHECKPOINT_DIR) / d.name / "model.safetensors").exists()
|
|
1901
|
+
for d in Path(cfg.CHECKPOINT_DIR).iterdir()
|
|
1902
|
+
if d.is_dir() and d.name.startswith("step_")
|
|
1903
|
+
) if Path(cfg.CHECKPOINT_DIR).exists() else False
|
|
1904
|
+
|
|
1905
|
+
should_resume = (not args.from_scratch) and (ckpt_exists or legacy_exists)
|
|
1906
|
+
if args.from_scratch and (ckpt_exists or legacy_exists):
|
|
1907
|
+
trainer.logger.info(
|
|
1908
|
+
"--from-scratch: ignorando checkpoints existentes y arrancando random."
|
|
1909
|
+
)
|
|
1910
|
+
if exploitgym_cfg is not None:
|
|
1911
|
+
transferred_ckpt = (Path(exploitgym_cfg.CHECKPOINT_DIR) / "last" / "model.safetensors")
|
|
1912
|
+
if transferred_ckpt.exists():
|
|
1913
|
+
trainer.logger.info(
|
|
1914
|
+
f"Cargando pesos transferidos desde {transferred_ckpt}..."
|
|
1915
|
+
)
|
|
1916
|
+
from safetensors.torch import load_file as st_load
|
|
1917
|
+
sd = st_load(str(transferred_ckpt))
|
|
1918
|
+
trainer.model.load_state_dict(sd, strict=False)
|
|
1919
|
+
trainer.logger.info("Pesos transferidos cargados correctamente.")
|
|
1920
|
+
|
|
1921
|
+
if should_resume:
|
|
1922
|
+
state = trainer.ckpt.load_latest(trainer.model, trainer.optimizer)
|
|
1923
|
+
if state is not None:
|
|
1924
|
+
trainer.global_step = state.get("global_step", 0)
|
|
1925
|
+
trainer.completed_tiers = state.get("completed_tiers", [])
|
|
1926
|
+
trainer.logger.info(
|
|
1927
|
+
f"Auto-resume desde step={trainer.global_step} "
|
|
1928
|
+
f"completed_tiers={trainer.completed_tiers}"
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
if args.train:
|
|
1932
|
+
trainer.run()
|
|
1933
|
+
|
|
1934
|
+
if args.eval_holdout:
|
|
1935
|
+
trainer._eval_combined_holdout()
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
if __name__ == "__main__":
|
|
1939
|
+
main()
|