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/model.py
ADDED
|
@@ -0,0 +1,3824 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
TopoGPT2: Quaternion-Enhanced Topological Transformer Language Model
|
|
4
|
+
|
|
5
|
+
Author: Gris Iscomeback
|
|
6
|
+
Email: grisiscomeback@gmail.com
|
|
7
|
+
License: GPL v3
|
|
8
|
+
|
|
9
|
+
Mejoras sobre topogpt.py:
|
|
10
|
+
- Álgebra de cuaterniones completa (QuaternionLinear, QuaternionSpectralLayer)
|
|
11
|
+
con producto de Hamilton en el dominio de frecuencia para capturar la
|
|
12
|
+
espectrografía de los datos con kernels reales e imaginarios cruzados.
|
|
13
|
+
- SpectralAutoencoder: encoder/decoder espectral que comprime y reconstruye
|
|
14
|
+
las representaciones en el dominio de frecuencia.
|
|
15
|
+
- QuaternionTorusBrain VECTORIZADA (sin bucles sobre seq_len): proyección
|
|
16
|
+
geométrica sobre el toro con asignación blanda usando distancias circulares,
|
|
17
|
+
message-passing con rotaciones de cuaterniones.
|
|
18
|
+
- 8 nodos (RADIAL=2 × ANGULAR=4), 4 ángulos, 2 radiales (spec del usuario).
|
|
19
|
+
- Rotary Position Embeddings (RoPE).
|
|
20
|
+
- Flash-attention (scaled_dot_product_attention de PyTorch 2.0+).
|
|
21
|
+
- RMSNorm en lugar de LayerNorm (estilo LLaMA).
|
|
22
|
+
- Tokenizador BPE via tiktoken (vocab GPT-2, 50k tokens).
|
|
23
|
+
- Descargador de corpus: TinyStories, WikiText-103, raw file.
|
|
24
|
+
- Entrenamiento con AMP (mixed precision) + acumulación de gradientes.
|
|
25
|
+
- Presets de escala: micro, small, medium, gpt2.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
import torch.nn as nn
|
|
30
|
+
import torch.nn.functional as F
|
|
31
|
+
from torch.utils.checkpoint import checkpoint as grad_ckpt
|
|
32
|
+
from safetensors.torch import save_file as st_save, load_file as st_load
|
|
33
|
+
import numpy as np
|
|
34
|
+
import math
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
import json
|
|
40
|
+
import hashlib
|
|
41
|
+
import logging
|
|
42
|
+
import warnings
|
|
43
|
+
import argparse
|
|
44
|
+
from datetime import datetime
|
|
45
|
+
from typing import Dict, Tuple, Optional, List, Any, Union
|
|
46
|
+
from dataclasses import dataclass, asdict
|
|
47
|
+
from collections import deque
|
|
48
|
+
|
|
49
|
+
warnings.filterwarnings('ignore')
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# CONFIGURACIÓN
|
|
53
|
+
# ============================================================================
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TopoGPT2Config:
|
|
57
|
+
"""Configuración completa para TopoGPT2."""
|
|
58
|
+
|
|
59
|
+
# --- Dispositivo ---
|
|
60
|
+
DEVICE: str = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
61
|
+
RANDOM_SEED: int = 42
|
|
62
|
+
USE_AMP: bool = True
|
|
63
|
+
|
|
64
|
+
# --- Escala del modelo: 'micro' | 'small' | 'medium' | 'gpt2' | 'custom' ---
|
|
65
|
+
SCALE: str = 'small'
|
|
66
|
+
|
|
67
|
+
# --- Vocabulario (GPT-2 BPE via tiktoken) ---
|
|
68
|
+
VOCAB_SIZE: int = 50257
|
|
69
|
+
MAX_SEQ_LEN: int = 512
|
|
70
|
+
|
|
71
|
+
# --- Arquitectura (sobrescritos por SCALE salvo SCALE='custom') ---
|
|
72
|
+
D_MODEL: int = 256 # debe ser divisible por 4 (cuaterniones) y N_HEADS
|
|
73
|
+
N_HEADS: int = 8
|
|
74
|
+
N_KV_HEADS: int = 0 # GQA: cabezas K/V (0 = N_HEADS//4, -1 = MHA completo)
|
|
75
|
+
N_LAYERS: int = 6
|
|
76
|
+
DROPOUT: float = 0.1
|
|
77
|
+
|
|
78
|
+
# --- MoE (Mixture of Experts) ---
|
|
79
|
+
MOE_ENABLED: bool = True # activar MoE en cada capa
|
|
80
|
+
N_EXPERTS: int = 4 # expertos SwiGLU por capa
|
|
81
|
+
MOE_TOP_K: int = 2 # expertos activos por token (sparse)
|
|
82
|
+
MOE_AUX_LOSS_WEIGHT: float = 0.01 # load-balancing loss
|
|
83
|
+
|
|
84
|
+
# --- Topología del toro (spec usuario: 8 nodos = 4 ang × 2 rad) ---
|
|
85
|
+
TORUS_GRID_SIZE: int = 8 # Grid para SpectralLayer (8×8 FFT2)
|
|
86
|
+
TORUS_RADIAL_BINS: int = 2 # 2 niveles radiales
|
|
87
|
+
TORUS_ANGULAR_BINS: int = 4 # 4 ángulos (= 4 nodos por anillo)
|
|
88
|
+
# Total nodos en el grafo toro: 2 × 4 = 8
|
|
89
|
+
|
|
90
|
+
# --- Espectral / Autoencoder ---
|
|
91
|
+
SPECTRAL_LATENT_RATIO: float = 0.5 # latent_dim = D_MODEL * ratio
|
|
92
|
+
SPECTRAL_KERNEL_INIT_SCALE: float = 0.02
|
|
93
|
+
NUM_SPECTRAL_LAYERS: int = 2
|
|
94
|
+
AE_RECON_WEIGHT: float = 0.01 # peso de la pérdida de reconstrucción
|
|
95
|
+
|
|
96
|
+
# --- Training ---
|
|
97
|
+
BATCH_SIZE: int = 4 # reduced to fit 6GB VRAM
|
|
98
|
+
GRAD_ACCUM_STEPS: int = 8 # effective batch = 32
|
|
99
|
+
LEARNING_RATE: float = 3e-4
|
|
100
|
+
WEIGHT_DECAY: float = 0.1
|
|
101
|
+
EPOCHS: int = 10
|
|
102
|
+
WARMUP_RATIO: float = 0.05
|
|
103
|
+
GRADIENT_CLIP_NORM: float = 1.0
|
|
104
|
+
GRADIENT_CHECKPOINTING: bool = True # trades compute for memory
|
|
105
|
+
|
|
106
|
+
# --- Curriculum Learning ---
|
|
107
|
+
CURRICULUM_ENABLED: bool = True
|
|
108
|
+
CURRICULUM_SHORT_LINES: int = 50 # tier 1: short files
|
|
109
|
+
CURRICULUM_MED_LINES: int = 300 # tier 2: medium files
|
|
110
|
+
|
|
111
|
+
# --- Sliding Window Attention ---
|
|
112
|
+
ATTN_WINDOW: int = 0 # 0 = full attention; N = sliding window of N tokens
|
|
113
|
+
|
|
114
|
+
# --- Latent Memory Tokens (context compression) ---
|
|
115
|
+
N_MEMORY_TOKENS: int = 0 # number of learnable memory tokens for context compression
|
|
116
|
+
MEMORY_SEGMENT_LEN: int = 0 # segment length for memory-token processing (0 = disabled)
|
|
117
|
+
|
|
118
|
+
# --- Progressive Sequence Length ---
|
|
119
|
+
PROGRESSIVE_SEQ: bool = True # warm up with shorter sequences
|
|
120
|
+
PROGRESSIVE_SEQ_STEPS: Tuple[int, int, int] = (128, 256, 512) # len per phase
|
|
121
|
+
PROGRESSIVE_SEQ_EPOCHS: Tuple[int, int, int] = (3, 3, 4) # epochs per phase
|
|
122
|
+
|
|
123
|
+
# --- Text Quality Filters ---
|
|
124
|
+
FILTER_MAX_LINE_LEN: int = 500 # files with lines >500 chars are suspicious
|
|
125
|
+
FILTER_ENTROPY_MIN: float = 3.5 # below this = repetitive / auto-generated
|
|
126
|
+
FILTER_ENTROPY_MAX: float = 6.5 # above this = binary / encrypted
|
|
127
|
+
FILTER_SPECIAL_TOKEN_RATIO_MAX: float = 0.25 # max fraction of whitespace-only tokens
|
|
128
|
+
FILTER_ENABLE_DEDUP: bool = True # skip files with identical content hash
|
|
129
|
+
|
|
130
|
+
# --- Quantization for inference (6GB VRAM) ---
|
|
131
|
+
QUANTIZE_EMBEDDINGS: bool = True # int8 on embed + lm_head
|
|
132
|
+
QUANTIZE_FFN: bool = False # int4 on FFN weights (experimental)
|
|
133
|
+
QUANT_EMBED_BITS: int = 8
|
|
134
|
+
|
|
135
|
+
# --- Speculative Decoding ---
|
|
136
|
+
SPEC_DECODE_ENABLED: bool = False
|
|
137
|
+
SPEC_DECODE_DRAFT_SCALE: str = 'micro' # draft model scale (4x smaller)
|
|
138
|
+
SPEC_DECODE_K: int = 5 # draft tokens per speculation step
|
|
139
|
+
|
|
140
|
+
# --- Corpus ---
|
|
141
|
+
CORPUS: str = 'directory' # 'directory' | 'file'
|
|
142
|
+
CORPUS_ROOT: str = './src'
|
|
143
|
+
CORPUS_FILE: str = '' # Path if CORPUS='file'
|
|
144
|
+
CORPUS_MIN_FILE_CHARS: int = 30 # minimum chars to include a file
|
|
145
|
+
DATA_DIR: str = 'data_topogpt2'
|
|
146
|
+
MAX_TRAIN_TOKENS: int = 500_000_000 # max tokens per epoch (0 = unlimited)
|
|
147
|
+
|
|
148
|
+
# --- Checkpoints ---
|
|
149
|
+
CHECKPOINT_DIR: str = 'checkpoints_topogpt2'
|
|
150
|
+
CHECKPOINT_INTERVAL_MINUTES: int = 10
|
|
151
|
+
MAX_CHECKPOINTS: int = 5
|
|
152
|
+
|
|
153
|
+
# --- Logging ---
|
|
154
|
+
LOG_INTERVAL_STEPS: int = 100
|
|
155
|
+
EVAL_INTERVAL_STEPS: int = 500
|
|
156
|
+
LOG_LEVEL: str = 'INFO'
|
|
157
|
+
|
|
158
|
+
# --- Temperatura termodinámica (heredada) ---
|
|
159
|
+
T_INIT: float = 1.0
|
|
160
|
+
|
|
161
|
+
def __post_init__(self):
|
|
162
|
+
presets = {
|
|
163
|
+
'micro': dict(D_MODEL=64, N_HEADS=4, N_LAYERS=2, MAX_SEQ_LEN=256, ATTN_WINDOW=64, N_MEMORY_TOKENS=16, MEMORY_SEGMENT_LEN=64),
|
|
164
|
+
'small': dict(D_MODEL=256, N_HEADS=8, N_LAYERS=6, MAX_SEQ_LEN=512, ATTN_WINDOW=128, N_MEMORY_TOKENS=32, MEMORY_SEGMENT_LEN=128),
|
|
165
|
+
'medium': dict(D_MODEL=512, N_HEADS=8, N_LAYERS=12, MAX_SEQ_LEN=1024, ATTN_WINDOW=256, N_MEMORY_TOKENS=64, MEMORY_SEGMENT_LEN=256),
|
|
166
|
+
'large': dict(D_MODEL=512, N_HEADS=8, N_LAYERS=13, MAX_SEQ_LEN=1024, ATTN_WINDOW=256, N_MEMORY_TOKENS=64, MEMORY_SEGMENT_LEN=256),
|
|
167
|
+
'xlarge': dict(D_MODEL=576, N_HEADS=8, N_LAYERS=14, MAX_SEQ_LEN=1024, ATTN_WINDOW=256, N_MEMORY_TOKENS=64, MEMORY_SEGMENT_LEN=256),
|
|
168
|
+
'gpt2': dict(D_MODEL=768, N_HEADS=12, N_LAYERS=12, MAX_SEQ_LEN=2048, ATTN_WINDOW=256, N_MEMORY_TOKENS=64, MEMORY_SEGMENT_LEN=256),
|
|
169
|
+
}
|
|
170
|
+
if self.SCALE in presets:
|
|
171
|
+
for k, v in presets[self.SCALE].items():
|
|
172
|
+
setattr(self, k, v)
|
|
173
|
+
|
|
174
|
+
assert self.D_MODEL % 4 == 0, "D_MODEL debe ser divisible por 4 (cuaterniones)"
|
|
175
|
+
assert self.D_MODEL % self.N_HEADS == 0, "D_MODEL debe ser divisible por N_HEADS"
|
|
176
|
+
self.D_QUAT = self.D_MODEL // 4
|
|
177
|
+
self.D_HEAD = self.D_MODEL // self.N_HEADS
|
|
178
|
+
self.SPECTRAL_LATENT_DIM = max(16, int(self.D_MODEL * self.SPECTRAL_LATENT_RATIO))
|
|
179
|
+
self.N_TORUS_NODES = self.TORUS_RADIAL_BINS * self.TORUS_ANGULAR_BINS
|
|
180
|
+
# GQA: resolver N_KV_HEADS
|
|
181
|
+
if self.N_KV_HEADS == 0:
|
|
182
|
+
# Auto: N_HEADS // 4, mínimo 1, divisor de N_HEADS
|
|
183
|
+
kv = max(1, self.N_HEADS // 4)
|
|
184
|
+
while self.N_HEADS % kv != 0:
|
|
185
|
+
kv -= 1
|
|
186
|
+
self.N_KV_HEADS = kv
|
|
187
|
+
elif self.N_KV_HEADS == -1:
|
|
188
|
+
self.N_KV_HEADS = self.N_HEADS # MHA estándar
|
|
189
|
+
assert self.N_HEADS % self.N_KV_HEADS == 0, \
|
|
190
|
+
"N_HEADS debe ser divisible por N_KV_HEADS"
|
|
191
|
+
self.GQA_GROUPS = self.N_HEADS // self.N_KV_HEADS
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def setup_logger(name: str, level: str = 'INFO') -> logging.Logger:
|
|
195
|
+
logger = logging.getLogger(name)
|
|
196
|
+
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
197
|
+
if not logger.handlers:
|
|
198
|
+
h = logging.StreamHandler()
|
|
199
|
+
h.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s'))
|
|
200
|
+
logger.addHandler(h)
|
|
201
|
+
return logger
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def set_seed(seed: int, device: str):
|
|
205
|
+
torch.manual_seed(seed)
|
|
206
|
+
np.random.seed(seed)
|
|
207
|
+
if 'cuda' in device and torch.cuda.is_available():
|
|
208
|
+
torch.cuda.manual_seed_all(seed)
|
|
209
|
+
torch.backends.cudnn.deterministic = True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ============================================================================
|
|
213
|
+
# ÁLGEBRA DE CUATERNIONES
|
|
214
|
+
# ============================================================================
|
|
215
|
+
|
|
216
|
+
class QuaternionOps:
|
|
217
|
+
"""
|
|
218
|
+
Operaciones de cuaterniones puras en PyTorch.
|
|
219
|
+
Representación: [..., 4] donde last dim = [w, x, y, z]
|
|
220
|
+
q = w + x*i + y*j + z*k
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def hamilton_product(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor:
|
|
225
|
+
"""Producto de Hamilton q1 ⊗ q2. Ambos [..., 4]."""
|
|
226
|
+
w1, x1, y1, z1 = q1[..., 0], q1[..., 1], q1[..., 2], q1[..., 3]
|
|
227
|
+
w2, x2, y2, z2 = q2[..., 0], q2[..., 1], q2[..., 2], q2[..., 3]
|
|
228
|
+
return torch.stack([
|
|
229
|
+
w1*w2 - x1*x2 - y1*y2 - z1*z2,
|
|
230
|
+
w1*x2 + x1*w2 + y1*z2 - z1*y2,
|
|
231
|
+
w1*y2 - x1*z2 + y1*w2 + z1*x2,
|
|
232
|
+
w1*z2 + x1*y2 - y1*x2 + z1*w2,
|
|
233
|
+
], dim=-1)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def normalize(q: torch.Tensor, eps: float = 1e-8) -> torch.Tensor:
|
|
237
|
+
return q / (q.norm(dim=-1, keepdim=True) + eps)
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def conjugate(q: torch.Tensor) -> torch.Tensor:
|
|
241
|
+
sign = q.new_tensor([1, -1, -1, -1])
|
|
242
|
+
return q * sign
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def rotate_vector(v: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
|
|
246
|
+
"""Rota vector 3D v por cuaternión unitario q. v:[...,3] q:[...,4]"""
|
|
247
|
+
zero = torch.zeros(*v.shape[:-1], 1, device=v.device, dtype=v.dtype)
|
|
248
|
+
v_q = torch.cat([zero, v], dim=-1) # pure quaternion
|
|
249
|
+
q_c = QuaternionOps.conjugate(q)
|
|
250
|
+
rotated = QuaternionOps.hamilton_product(
|
|
251
|
+
QuaternionOps.hamilton_product(q, v_q), q_c)
|
|
252
|
+
return rotated[..., 1:]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class QuaternionLinear(nn.Module):
|
|
256
|
+
"""
|
|
257
|
+
Capa lineal con pesos cuaterniones.
|
|
258
|
+
|
|
259
|
+
Implementa la multiplicación W * x en el álgebra de cuaterniones:
|
|
260
|
+
- W = Ww + Wx*i + Wy*j + Wz*k (cuaternión de pesos)
|
|
261
|
+
- x = xw + xx*i + xy*j + xz*k (cuaternión de entrada)
|
|
262
|
+
- out = W * x (producto de Hamilton extendido a vectores)
|
|
263
|
+
|
|
264
|
+
Parámetros: 4 matrices reales de forma [out_q, in_q]
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def __init__(self, in_features: int, out_features: int, bias: bool = True):
|
|
268
|
+
super().__init__()
|
|
269
|
+
assert in_features % 4 == 0 and out_features % 4 == 0
|
|
270
|
+
self.in_q = in_features // 4
|
|
271
|
+
self.out_q = out_features // 4
|
|
272
|
+
|
|
273
|
+
# 4 componentes del cuaternión de pesos
|
|
274
|
+
self.Ww = nn.Linear(self.in_q, self.out_q, bias=False)
|
|
275
|
+
self.Wx = nn.Linear(self.in_q, self.out_q, bias=False)
|
|
276
|
+
self.Wy = nn.Linear(self.in_q, self.out_q, bias=False)
|
|
277
|
+
self.Wz = nn.Linear(self.in_q, self.out_q, bias=False)
|
|
278
|
+
self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None
|
|
279
|
+
|
|
280
|
+
for w in [self.Ww, self.Wx, self.Wy, self.Wz]:
|
|
281
|
+
nn.init.normal_(w.weight, std=0.02)
|
|
282
|
+
|
|
283
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
284
|
+
"""x: [..., in_features] → [..., out_features]"""
|
|
285
|
+
d = self.in_q
|
|
286
|
+
xw, xx, xy, xz = x[..., :d], x[..., d:2*d], x[..., 2*d:3*d], x[..., 3*d:]
|
|
287
|
+
# Producto de Hamilton: W * x
|
|
288
|
+
ow = self.Ww(xw) - self.Wx(xx) - self.Wy(xy) - self.Wz(xz)
|
|
289
|
+
ox = self.Ww(xx) + self.Wx(xw) + self.Wy(xz) - self.Wz(xy)
|
|
290
|
+
oy = self.Ww(xy) - self.Wx(xz) + self.Wy(xw) + self.Wz(xx)
|
|
291
|
+
oz = self.Ww(xz) + self.Wx(xy) - self.Wy(xx) + self.Wz(xw)
|
|
292
|
+
out = torch.cat([ow, ox, oy, oz], dim=-1)
|
|
293
|
+
return out + self.bias if self.bias is not None else out
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ============================================================================
|
|
297
|
+
# CAPA ESPECTRAL CON CUATERNIONES
|
|
298
|
+
# ============================================================================
|
|
299
|
+
|
|
300
|
+
class QuaternionSpectralLayer(nn.Module):
|
|
301
|
+
"""
|
|
302
|
+
Convolución espectral 2D con cuaterniones y producto de Hamilton completo.
|
|
303
|
+
|
|
304
|
+
Operación en dominio de frecuencia:
|
|
305
|
+
P(k) = W(k) ⊗ X(k) (producto de Hamilton de cuaterniones complejos)
|
|
306
|
+
|
|
307
|
+
Donde:
|
|
308
|
+
X(k) = FFT2(x) con 4 canales cuaterniones [Xw, Xx, Xy, Xz]
|
|
309
|
+
W(k) = kernel complejo aprendible con componentes [Ww, Wx, Wy, Wz]
|
|
310
|
+
|
|
311
|
+
Reglas del producto de Hamilton en dominio de frecuencia:
|
|
312
|
+
Pw = Ww·Xw - Wx·Xx - Wy·Xy - Wz·Xz
|
|
313
|
+
Px = Ww·Xx + Wx·Xw + Wy·Xz - Wz·Xy
|
|
314
|
+
Py = Ww·Xy - Wx·Xz + Wy·Xw + Wz·Xx
|
|
315
|
+
Pz = Ww·Xz + Wx·Xy - Wy·Xx + Wz·Xw
|
|
316
|
+
|
|
317
|
+
Cada Wc es un kernel complejo (partes real e imaginaria independientes).
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self, in_q: int, out_q: int, grid_h: int, grid_w: int,
|
|
321
|
+
init_scale: float = 0.02):
|
|
322
|
+
super().__init__()
|
|
323
|
+
self.in_q = in_q
|
|
324
|
+
self.out_q = out_q
|
|
325
|
+
self.grid_h = grid_h
|
|
326
|
+
self.grid_w = grid_w
|
|
327
|
+
|
|
328
|
+
# rfft2(x, s=(H, W)) → output shape [..., H, W//2+1]
|
|
329
|
+
freq_h = grid_h
|
|
330
|
+
freq_w = grid_w // 2 + 1
|
|
331
|
+
|
|
332
|
+
# Kernel complejo para cada componente cuaternión (w, x, y, z)
|
|
333
|
+
for c in ('w', 'x', 'y', 'z'):
|
|
334
|
+
self.register_parameter(f'kr_{c}',
|
|
335
|
+
nn.Parameter(torch.randn(in_q, out_q, freq_h, freq_w) * init_scale))
|
|
336
|
+
self.register_parameter(f'ki_{c}',
|
|
337
|
+
nn.Parameter(torch.randn(in_q, out_q, freq_h, freq_w) * init_scale))
|
|
338
|
+
|
|
339
|
+
def _kernel(self, c: str) -> torch.Tensor:
|
|
340
|
+
return torch.complex(getattr(self, f'kr_{c}'), getattr(self, f'ki_{c}'))
|
|
341
|
+
|
|
342
|
+
def _contract(self, W: torch.Tensor, X: torch.Tensor) -> torch.Tensor:
|
|
343
|
+
"""Suma sobre canales in_q: Y[b,o,h,w] = Σ_i W[i,o,h,w]·X[b,i,h,w]"""
|
|
344
|
+
return torch.einsum('iohw,bihw->bohw', W, X)
|
|
345
|
+
|
|
346
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
347
|
+
"""
|
|
348
|
+
x: [B, 4*in_q, H, W] (4 canales cuaterniones sobre grid espacial)
|
|
349
|
+
→ [B, 4*out_q, H, W]
|
|
350
|
+
"""
|
|
351
|
+
q = self.in_q
|
|
352
|
+
xw, xx, xy, xz = x[:, :q], x[:, q:2*q], x[:, 2*q:3*q], x[:, 3*q:]
|
|
353
|
+
|
|
354
|
+
# FFT2 por componente cuaternión → dominio de frecuencia
|
|
355
|
+
Xw = torch.fft.rfft2(xw, s=(self.grid_h, self.grid_w))
|
|
356
|
+
Xx = torch.fft.rfft2(xx, s=(self.grid_h, self.grid_w))
|
|
357
|
+
Xy = torch.fft.rfft2(xy, s=(self.grid_h, self.grid_w))
|
|
358
|
+
Xz = torch.fft.rfft2(xz, s=(self.grid_h, self.grid_w))
|
|
359
|
+
|
|
360
|
+
Ww, Wx, Wy, Wz = self._kernel('w'), self._kernel('x'), self._kernel('y'), self._kernel('z')
|
|
361
|
+
|
|
362
|
+
# Precomputar 16 contracciones (componente de kernel × componente de señal)
|
|
363
|
+
C = {}
|
|
364
|
+
for wc, W in (('w', Ww), ('x', Wx), ('y', Wy), ('z', Wz)):
|
|
365
|
+
for xc, X in (('w', Xw), ('x', Xx), ('y', Xy), ('z', Xz)):
|
|
366
|
+
C[(wc, xc)] = self._contract(W, X)
|
|
367
|
+
|
|
368
|
+
# Producto de Hamilton en dominio de frecuencia
|
|
369
|
+
Pw = C[('w','w')] - C[('x','x')] - C[('y','y')] - C[('z','z')]
|
|
370
|
+
Px = C[('w','x')] + C[('x','w')] + C[('y','z')] - C[('z','y')]
|
|
371
|
+
Py = C[('w','y')] - C[('x','z')] + C[('y','w')] + C[('z','x')]
|
|
372
|
+
Pz = C[('w','z')] + C[('x','y')] - C[('y','x')] + C[('z','w')]
|
|
373
|
+
|
|
374
|
+
# IFFT2 y recombinar
|
|
375
|
+
ow = torch.fft.irfft2(Pw, s=(self.grid_h, self.grid_w))
|
|
376
|
+
ox = torch.fft.irfft2(Px, s=(self.grid_h, self.grid_w))
|
|
377
|
+
oy = torch.fft.irfft2(Py, s=(self.grid_h, self.grid_w))
|
|
378
|
+
oz = torch.fft.irfft2(Pz, s=(self.grid_h, self.grid_w))
|
|
379
|
+
|
|
380
|
+
return torch.cat([ow, ox, oy, oz], dim=1) # [B, 4*out_q, H, W]
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ============================================================================
|
|
384
|
+
# SPECTRAL AUTOENCODER
|
|
385
|
+
# ============================================================================
|
|
386
|
+
|
|
387
|
+
class SpectralAutoencoder(nn.Module):
|
|
388
|
+
"""
|
|
389
|
+
Autoencoder espectral con cuaterniones.
|
|
390
|
+
|
|
391
|
+
Opera en dos niveles:
|
|
392
|
+
1. Espectral 1D sobre el vector de features (FFT sobre dim D_MODEL):
|
|
393
|
+
captura la espectrografía global del embedding.
|
|
394
|
+
2. Espectral 2D sobre el grid del toro (QuaternionSpectralLayer):
|
|
395
|
+
captura correlaciones espaciales en la topología.
|
|
396
|
+
|
|
397
|
+
Devuelve (latent, recon_loss) para regularización.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def __init__(self, config: TopoGPT2Config):
|
|
401
|
+
super().__init__()
|
|
402
|
+
d = config.D_MODEL
|
|
403
|
+
d_lat = config.SPECTRAL_LATENT_DIM
|
|
404
|
+
d_q = config.D_QUAT
|
|
405
|
+
g = config.TORUS_GRID_SIZE
|
|
406
|
+
r = config.TORUS_RADIAL_BINS
|
|
407
|
+
a = config.TORUS_ANGULAR_BINS
|
|
408
|
+
init_s = config.SPECTRAL_KERNEL_INIT_SCALE
|
|
409
|
+
|
|
410
|
+
n_freq = d // 2 + 1
|
|
411
|
+
|
|
412
|
+
# --- Kernels espectrales 1D (reales e imaginarios) ---
|
|
413
|
+
self.enc_kr = nn.Parameter(torch.randn(n_freq) * init_s)
|
|
414
|
+
self.enc_ki = nn.Parameter(torch.randn(n_freq) * init_s)
|
|
415
|
+
self.dec_kr = nn.Parameter(torch.randn(n_freq) * init_s)
|
|
416
|
+
self.dec_ki = nn.Parameter(torch.randn(n_freq) * init_s)
|
|
417
|
+
|
|
418
|
+
# Proyección al latente y de vuelta
|
|
419
|
+
self.enc_proj = QuaternionLinear(d, d_lat)
|
|
420
|
+
self.dec_proj = QuaternionLinear(d_lat, d)
|
|
421
|
+
|
|
422
|
+
# --- Spectral 2D sobre el grid del toro: in/out_q = d_q, grid = r×a ---
|
|
423
|
+
# Los 'out_q' en la segunda capa son iguales para reconstruir
|
|
424
|
+
self.torus_spectral = nn.ModuleList([
|
|
425
|
+
QuaternionSpectralLayer(d_q, d_q, r, a, init_scale=init_s)
|
|
426
|
+
for _ in range(config.NUM_SPECTRAL_LAYERS)
|
|
427
|
+
])
|
|
428
|
+
|
|
429
|
+
self.act = nn.GELU()
|
|
430
|
+
self.d_model = d
|
|
431
|
+
|
|
432
|
+
def _filter1d(self, x: torch.Tensor, kr: torch.Tensor, ki: torch.Tensor) -> torch.Tensor:
|
|
433
|
+
"""Filtro espectral 1D: x[..., D] → filtrado[..., D]"""
|
|
434
|
+
X = torch.fft.rfft(x, dim=-1)
|
|
435
|
+
K = torch.complex(kr, ki)
|
|
436
|
+
return torch.fft.irfft(X * K, n=self.d_model, dim=-1)
|
|
437
|
+
|
|
438
|
+
def encode(self, x: torch.Tensor) -> torch.Tensor:
|
|
439
|
+
"""x: [..., D_MODEL] → latent: [..., D_LAT]"""
|
|
440
|
+
x_filt = self.act(self._filter1d(x, self.enc_kr, self.enc_ki))
|
|
441
|
+
return self.enc_proj(x_filt)
|
|
442
|
+
|
|
443
|
+
def decode(self, z: torch.Tensor) -> torch.Tensor:
|
|
444
|
+
"""z: [..., D_LAT] → recon: [..., D_MODEL]"""
|
|
445
|
+
x = self.dec_proj(z)
|
|
446
|
+
return self._filter1d(x, self.dec_kr, self.dec_ki)
|
|
447
|
+
|
|
448
|
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
449
|
+
"""Devuelve (latent, recon_loss)"""
|
|
450
|
+
z = self.encode(x)
|
|
451
|
+
recon = self.decode(z)
|
|
452
|
+
recon_loss = F.mse_loss(recon, x.detach())
|
|
453
|
+
return z, recon_loss
|
|
454
|
+
|
|
455
|
+
def process_torus_grid(self, grid: torch.Tensor) -> torch.Tensor:
|
|
456
|
+
"""
|
|
457
|
+
Procesa el grid del toro con QuaternionSpectralLayer.
|
|
458
|
+
grid: [B, 4*D_QUAT, RADIAL, ANGULAR] → [B, 4*D_QUAT, RADIAL, ANGULAR]
|
|
459
|
+
"""
|
|
460
|
+
h = grid
|
|
461
|
+
for layer in self.torus_spectral:
|
|
462
|
+
h = self.act(layer(h))
|
|
463
|
+
return h
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ============================================================================
|
|
467
|
+
# QUATERNION TORUS BRAIN (reemplaza MLP, VECTORIZADO)
|
|
468
|
+
# ============================================================================
|
|
469
|
+
|
|
470
|
+
class QuaternionTorusBrain(nn.Module):
|
|
471
|
+
"""
|
|
472
|
+
Reemplaza el MLP en cada capa del transformer.
|
|
473
|
+
|
|
474
|
+
Pipeline (completamente vectorizado sobre batch Y secuencia):
|
|
475
|
+
|
|
476
|
+
1. Flatten: [B, S, D] → [B·S, D]
|
|
477
|
+
2. SpectralAutoencoder: filtrado espectral 1D + compresión cuaternión
|
|
478
|
+
3. Proyección al toro:
|
|
479
|
+
- Calcula 2 ángulos (phi1, phi2) ∈ [-π, π]²
|
|
480
|
+
- Asignación blanda a los 8 nodos via distancia circular en el toro
|
|
481
|
+
4. Construye grid de nodos: [B·S, N_NODES=8, D_MODEL]
|
|
482
|
+
5. QuaternionSpectralLayer 2D sobre el grid [B·S, 4*D_QUAT, RADIAL, ANGULAR]
|
|
483
|
+
6. Message-passing con rotaciones cuaterniones sobre el grafo toro
|
|
484
|
+
7. Readout: atención sobre los 8 nodos → [B·S, D_MODEL]
|
|
485
|
+
8. Reshape: [B·S, D] → [B, S, D]
|
|
486
|
+
"""
|
|
487
|
+
|
|
488
|
+
def __init__(self, d_model: int, config: TopoGPT2Config):
|
|
489
|
+
super().__init__()
|
|
490
|
+
self.d_model = d_model
|
|
491
|
+
self.d_lat = config.SPECTRAL_LATENT_DIM
|
|
492
|
+
self.d_q = d_model // 4
|
|
493
|
+
self.n_radial = config.TORUS_RADIAL_BINS # 2
|
|
494
|
+
self.n_angular = config.TORUS_ANGULAR_BINS # 4
|
|
495
|
+
self.n_nodes = config.N_TORUS_NODES # 8
|
|
496
|
+
self.config = config
|
|
497
|
+
|
|
498
|
+
# Autoencoder espectral
|
|
499
|
+
self.spectral_ae = SpectralAutoencoder(config)
|
|
500
|
+
|
|
501
|
+
# Proyección al espacio de ángulos del toro (2 ángulos × 2 = 4 salidas para Q)
|
|
502
|
+
self.torus_proj = nn.Sequential(
|
|
503
|
+
QuaternionLinear(d_model, d_model),
|
|
504
|
+
nn.GELU(),
|
|
505
|
+
nn.Linear(d_model, 4), # [phi1, phi2, r_scale, dummy]
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Embeddings aprendibles para los 8 nodos del toro
|
|
509
|
+
self.node_embed = nn.Parameter(torch.randn(self.n_nodes, d_model) * 0.02)
|
|
510
|
+
|
|
511
|
+
# Cuaterniones de rotación aprendibles para message-passing
|
|
512
|
+
# Un cuaternión por tipo de arista: ang-izq, ang-der, rad-abajo, rad-arriba
|
|
513
|
+
self.edge_quat = nn.Parameter(torch.randn(4, 4) * 0.1) # [n_edge_types, 4]
|
|
514
|
+
|
|
515
|
+
# Red de nodo post-message-passing
|
|
516
|
+
self.node_net = QuaternionLinear(d_model, d_model)
|
|
517
|
+
|
|
518
|
+
# Proyección final desde nodo-pool al espacio de salida
|
|
519
|
+
self.readout = nn.Sequential(
|
|
520
|
+
nn.Linear(d_model, d_model * 2),
|
|
521
|
+
nn.GELU(),
|
|
522
|
+
nn.Linear(d_model * 2, d_model),
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Grafo del toro: lista de aristas (nodo_i, nodo_j, tipo_arista)
|
|
526
|
+
self._build_torus_graph()
|
|
527
|
+
|
|
528
|
+
def _build_torus_graph(self):
|
|
529
|
+
"""
|
|
530
|
+
Construye las aristas del grafo toro 2×4.
|
|
531
|
+
|
|
532
|
+
Nodos indexados como: node = r * N_ANGULAR + a
|
|
533
|
+
r ∈ [0, RADIAL-1], a ∈ [0, ANGULAR-1]
|
|
534
|
+
|
|
535
|
+
Aristas angulares: nodo ↔ nodo a la izquierda/derecha (periódico)
|
|
536
|
+
Aristas radiales: nodo ↔ nodo del anillo interior/exterior
|
|
537
|
+
"""
|
|
538
|
+
edges_i, edges_j, edge_type = [], [], []
|
|
539
|
+
R, A = self.n_radial, self.n_angular
|
|
540
|
+
for r in range(R):
|
|
541
|
+
for a in range(A):
|
|
542
|
+
n = r * A + a
|
|
543
|
+
# Angular izquierda (tipo 0)
|
|
544
|
+
neighbor = r * A + (a - 1) % A
|
|
545
|
+
edges_i.append(n); edges_j.append(neighbor); edge_type.append(0)
|
|
546
|
+
# Angular derecha (tipo 1)
|
|
547
|
+
neighbor = r * A + (a + 1) % A
|
|
548
|
+
edges_i.append(n); edges_j.append(neighbor); edge_type.append(1)
|
|
549
|
+
# Radial hacia centro (tipo 2) - si existe
|
|
550
|
+
if r > 0:
|
|
551
|
+
neighbor = (r - 1) * A + a
|
|
552
|
+
edges_i.append(n); edges_j.append(neighbor); edge_type.append(2)
|
|
553
|
+
# Radial hacia fuera (tipo 3) - si existe
|
|
554
|
+
if r < R - 1:
|
|
555
|
+
neighbor = (r + 1) * A + a
|
|
556
|
+
edges_i.append(n); edges_j.append(neighbor); edge_type.append(3)
|
|
557
|
+
|
|
558
|
+
self.register_buffer('edges_i', torch.tensor(edges_i, dtype=torch.long))
|
|
559
|
+
self.register_buffer('edges_j', torch.tensor(edges_j, dtype=torch.long))
|
|
560
|
+
self.register_buffer('edge_type', torch.tensor(edge_type, dtype=torch.long))
|
|
561
|
+
|
|
562
|
+
def _torus_soft_assign(self, phi1: torch.Tensor, phi2: torch.Tensor) -> torch.Tensor:
|
|
563
|
+
"""
|
|
564
|
+
Asignación blanda de tokens a los 8 nodos del toro via distancia circular.
|
|
565
|
+
|
|
566
|
+
phi1: [BS] ángulo angular ∈ [-π, π]
|
|
567
|
+
phi2: [BS] ángulo radial ∈ [-π, π]
|
|
568
|
+
→ weights: [BS, N_NODES] (suma a 1, softmax de distancias negativas)
|
|
569
|
+
"""
|
|
570
|
+
BS = phi1.shape[0]
|
|
571
|
+
device = phi1.device
|
|
572
|
+
|
|
573
|
+
# Posiciones de los nodos en el toro
|
|
574
|
+
ang_pos = torch.linspace(-math.pi, math.pi, self.n_angular + 1, device=device)[:-1]
|
|
575
|
+
rad_pos = torch.linspace(-math.pi, math.pi, self.n_radial + 1, device=device)[:-1]
|
|
576
|
+
|
|
577
|
+
# Distancias circulares (usando sin para periodicidad suave)
|
|
578
|
+
# d_ang[BS, N_ANGULAR], d_rad[BS, N_RADIAL]
|
|
579
|
+
d_ang = torch.sin((phi1.unsqueeze(1) - ang_pos.unsqueeze(0)) / 2).pow(2) # [BS, A]
|
|
580
|
+
d_rad = torch.sin((phi2.unsqueeze(1) - rad_pos.unsqueeze(0)) / 2).pow(2) # [BS, R]
|
|
581
|
+
|
|
582
|
+
# Distancia en el toro para cada nodo [r, a]: d[r, a] = d_rad[r] + d_ang[a]
|
|
583
|
+
# Resultado: [BS, R, A] → flatten a [BS, N_NODES]
|
|
584
|
+
d_torus = d_rad.unsqueeze(2) + d_ang.unsqueeze(1) # [BS, R, A]
|
|
585
|
+
d_flat = d_torus.view(BS, -1) # [BS, N_NODES]
|
|
586
|
+
|
|
587
|
+
return torch.softmax(-d_flat / 0.3, dim=-1) # temperatura 0.3
|
|
588
|
+
|
|
589
|
+
def _message_passing(self, node_feat: torch.Tensor) -> torch.Tensor:
|
|
590
|
+
"""
|
|
591
|
+
Message-passing VECTORIZADO con rotaciones cuaterniones.
|
|
592
|
+
Sin bucles Python: todas las aristas se procesan en paralelo.
|
|
593
|
+
|
|
594
|
+
node_feat: [BS, N_NODES, D_MODEL]
|
|
595
|
+
→ [BS, N_NODES, D_MODEL]
|
|
596
|
+
"""
|
|
597
|
+
BS = node_feat.shape[0]
|
|
598
|
+
n_edges = self.edges_i.shape[0]
|
|
599
|
+
d_q = self.d_q
|
|
600
|
+
|
|
601
|
+
# Cuaterniones de arista normalizados: [n_edge_types=4, 4]
|
|
602
|
+
eq = QuaternionOps.normalize(self.edge_quat)
|
|
603
|
+
|
|
604
|
+
# Recoger features de nodos fuente para TODAS las aristas: [BS, n_edges, D]
|
|
605
|
+
src_feat = node_feat[:, self.edges_j, :] # index [n_edges] → [BS, n_edges, D]
|
|
606
|
+
|
|
607
|
+
# Cuaternión de cada arista: [n_edges, 4] → [1, n_edges, 1, 4] para broadcasting
|
|
608
|
+
edge_q = eq[self.edge_type] # [n_edges, 4]
|
|
609
|
+
edge_q = edge_q.unsqueeze(0).unsqueeze(2) # [1, n_edges, 1, 4]
|
|
610
|
+
edge_q = edge_q.expand(BS, -1, d_q, -1) # [BS, n_edges, D_QUAT, 4]
|
|
611
|
+
|
|
612
|
+
# Reshapear fuentes: [BS, n_edges, D] → [BS, n_edges, D_QUAT, 4]
|
|
613
|
+
src_q = src_feat.view(BS, n_edges, d_q, 4)
|
|
614
|
+
|
|
615
|
+
# Rotación cuaternión para todas las aristas en paralelo: [BS, n_edges, D_QUAT, 4]
|
|
616
|
+
msg_rot = QuaternionOps.hamilton_product(edge_q, src_q)
|
|
617
|
+
msg_rot = msg_rot.view(BS, n_edges, self.d_model) # [BS, n_edges, D]
|
|
618
|
+
|
|
619
|
+
# Scatter-add a nodos destino: edges_i[e] es el nodo destino de la arista e
|
|
620
|
+
agg = torch.zeros_like(node_feat) # [BS, N_NODES, D]
|
|
621
|
+
dst_idx = self.edges_i.view(1, n_edges, 1).expand(BS, -1, self.d_model)
|
|
622
|
+
agg.scatter_add_(1, dst_idx, msg_rot)
|
|
623
|
+
|
|
624
|
+
return self.node_net(node_feat + agg)
|
|
625
|
+
|
|
626
|
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
627
|
+
"""
|
|
628
|
+
x: [B, S, D_MODEL]
|
|
629
|
+
→ output: [B, S, D_MODEL], recon_loss: scalar
|
|
630
|
+
"""
|
|
631
|
+
B, S, D = x.shape
|
|
632
|
+
device = x.device
|
|
633
|
+
|
|
634
|
+
# 1. Flatten para procesamiento paralelo
|
|
635
|
+
x_flat = x.reshape(B * S, D) # [BS, D]
|
|
636
|
+
|
|
637
|
+
# 2. Spectral Autoencoder: filtrado + compresión
|
|
638
|
+
z, recon_loss = self.spectral_ae(x_flat) # z: [BS, D_LAT]
|
|
639
|
+
|
|
640
|
+
# 3. Proyección al toro
|
|
641
|
+
coords = self.torus_proj(x_flat) # [BS, 4]
|
|
642
|
+
phi1 = math.pi * torch.tanh(coords[:, 0]) # ángulo angular ∈ [-π, π]
|
|
643
|
+
phi2 = math.pi * torch.tanh(coords[:, 1]) # ángulo radial ∈ [-π, π]
|
|
644
|
+
|
|
645
|
+
# 4. Asignación blanda a 8 nodos
|
|
646
|
+
attn_w = self._torus_soft_assign(phi1, phi2) # [BS, N_NODES]
|
|
647
|
+
|
|
648
|
+
# 5. Construir grid de nodos: weighted combination de embeddings de nodo + input
|
|
649
|
+
# node_feat[bs, n, :] = attn_w[bs,n] * node_embed[n] + x_flat[bs] * attn_w[bs,n]
|
|
650
|
+
nodes = (
|
|
651
|
+
attn_w.unsqueeze(-1) * self.node_embed.unsqueeze(0) # [BS, N, D]
|
|
652
|
+
+ attn_w.unsqueeze(-1) * x_flat.unsqueeze(1) # broadcasting [BS, N, D]
|
|
653
|
+
) # [BS, N_NODES, D_MODEL]
|
|
654
|
+
|
|
655
|
+
# 6. QuaternionSpectralLayer 2D sobre el grid del toro
|
|
656
|
+
# Reshape: [BS, N_NODES, D_MODEL] → [BS, 4*D_QUAT, RADIAL, ANGULAR]
|
|
657
|
+
grid = nodes.view(B * S, self.n_radial, self.n_angular, D)
|
|
658
|
+
grid = grid.permute(0, 3, 1, 2) # [BS, D_MODEL, RADIAL, ANGULAR]
|
|
659
|
+
# Reorganizar en 4 canales cuaterniones
|
|
660
|
+
d_q = self.d_q
|
|
661
|
+
grid_q = grid.view(B * S, 4, d_q, self.n_radial, self.n_angular)
|
|
662
|
+
# [BS, 4, D_QUAT, RADIAL, ANGULAR] → [BS, 4*D_QUAT, RADIAL, ANGULAR]
|
|
663
|
+
grid_q = grid_q.permute(0, 1, 2, 3, 4).reshape(B * S, 4 * d_q, self.n_radial, self.n_angular)
|
|
664
|
+
grid_spec = self.spectral_ae.process_torus_grid(grid_q) # [BS, 4*D_QUAT, R, A]
|
|
665
|
+
|
|
666
|
+
# Volver a [BS, N_NODES, D_MODEL]
|
|
667
|
+
grid_back = grid_spec.view(B * S, 4, d_q, self.n_radial, self.n_angular)
|
|
668
|
+
grid_back = grid_back.permute(0, 3, 4, 1, 2).reshape(B * S, self.n_nodes, D)
|
|
669
|
+
|
|
670
|
+
# 7. Message-passing cuaternión sobre el grafo toro
|
|
671
|
+
nodes_mp = self._message_passing(grid_back) # [BS, N_NODES, D_MODEL]
|
|
672
|
+
|
|
673
|
+
# 8. Readout: suma ponderada por los pesos de asignación
|
|
674
|
+
out_flat = (attn_w.unsqueeze(-1) * nodes_mp).sum(dim=1) # [BS, D_MODEL]
|
|
675
|
+
out_flat = self.readout(out_flat) # [BS, D_MODEL]
|
|
676
|
+
|
|
677
|
+
# 9. Reshape a secuencia
|
|
678
|
+
output = out_flat.reshape(B, S, D) # [B, S, D_MODEL]
|
|
679
|
+
|
|
680
|
+
return output, recon_loss
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
# ============================================================================
|
|
684
|
+
# ROTARY POSITION EMBEDDINGS (RoPE)
|
|
685
|
+
# ============================================================================
|
|
686
|
+
|
|
687
|
+
class RotaryEmbedding(nn.Module):
|
|
688
|
+
"""
|
|
689
|
+
Rotary Position Embeddings (RoPE) - Su et al., 2021.
|
|
690
|
+
Codifica la posicion como rotaciones del espacio de atencion,
|
|
691
|
+
naturalmente relativas y sin parametros extra.
|
|
692
|
+
|
|
693
|
+
Las caches _cos/_sin se registran como buffers no-persistentes con
|
|
694
|
+
nombres que no colisionan con checkpoints antiguos (que usaban
|
|
695
|
+
'cos_cache'/'sin_cache'). Esto permite cambiar MAX_SEQ_LEN sin
|
|
696
|
+
errores de shape al cargar checkpoints previos.
|
|
697
|
+
"""
|
|
698
|
+
|
|
699
|
+
def __init__(self, d_head: int, max_seq_len: int = 2048, base: int = 10000):
|
|
700
|
+
super().__init__()
|
|
701
|
+
inv_freq = 1.0 / (base ** (torch.arange(0, d_head, 2).float() / d_head))
|
|
702
|
+
self.register_buffer('inv_freq', inv_freq)
|
|
703
|
+
self._build_cache(max_seq_len)
|
|
704
|
+
|
|
705
|
+
def _build_cache(self, seq_len: int):
|
|
706
|
+
device = self.inv_freq.device
|
|
707
|
+
t = torch.arange(seq_len, device=device).float()
|
|
708
|
+
freqs = torch.outer(t, self.inv_freq)
|
|
709
|
+
emb = torch.cat([freqs, freqs], dim=-1)
|
|
710
|
+
self.register_buffer('_cos_cache', emb.cos(), persistent=False)
|
|
711
|
+
self.register_buffer('_sin_cache', emb.sin(), persistent=False)
|
|
712
|
+
|
|
713
|
+
def _rotate_half(self, x: torch.Tensor) -> torch.Tensor:
|
|
714
|
+
x1, x2 = x[..., :x.shape[-1]//2], x[..., x.shape[-1]//2:]
|
|
715
|
+
return torch.cat([-x2, x1], dim=-1)
|
|
716
|
+
|
|
717
|
+
def forward(self, q: torch.Tensor, k: torch.Tensor,
|
|
718
|
+
seq_len: int, offset: int = 0) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
719
|
+
"""
|
|
720
|
+
q, k: [B, n_heads, S_q/S_k, d_head]
|
|
721
|
+
offset: posicion inicial (para KV cache: longitud del cache existente)
|
|
722
|
+
Aplica posiciones [offset .. offset+S-1] a q y k.
|
|
723
|
+
"""
|
|
724
|
+
needed = offset + max(q.shape[2], k.shape[2])
|
|
725
|
+
if needed > self._cos_cache.shape[0]:
|
|
726
|
+
self._build_cache(needed * 2)
|
|
727
|
+
sq, sk = q.shape[2], k.shape[2]
|
|
728
|
+
cos_q = self._cos_cache[offset:offset + sq].unsqueeze(0).unsqueeze(0)
|
|
729
|
+
sin_q = self._sin_cache[offset:offset + sq].unsqueeze(0).unsqueeze(0)
|
|
730
|
+
cos_k = self._cos_cache[offset:offset + sk].unsqueeze(0).unsqueeze(0)
|
|
731
|
+
sin_k = self._sin_cache[offset:offset + sk].unsqueeze(0).unsqueeze(0)
|
|
732
|
+
q_rot = q * cos_q + self._rotate_half(q) * sin_q
|
|
733
|
+
k_rot = k * cos_k + self._rotate_half(k) * sin_k
|
|
734
|
+
return q_rot, k_rot
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# ============================================================================
|
|
738
|
+
# MULTI-HEAD ATTENTION CON FLASH ATTENTION + ROPE
|
|
739
|
+
# ============================================================================
|
|
740
|
+
|
|
741
|
+
class RMSNorm(nn.Module):
|
|
742
|
+
"""Root Mean Square Layer Normalization (sin bias). Más estable que LayerNorm."""
|
|
743
|
+
|
|
744
|
+
def __init__(self, d_model: int, eps: float = 1e-6):
|
|
745
|
+
super().__init__()
|
|
746
|
+
self.eps = eps
|
|
747
|
+
self.weight = nn.Parameter(torch.ones(d_model))
|
|
748
|
+
|
|
749
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
750
|
+
rms = x.pow(2).mean(-1, keepdim=True).add(self.eps).sqrt()
|
|
751
|
+
return x / rms * self.weight
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ============================================================================
|
|
755
|
+
# SWIGLU (activacion de modelos frontier: LLaMA, Qwen, etc.)
|
|
756
|
+
# ============================================================================
|
|
757
|
+
|
|
758
|
+
class SwiGLU(nn.Module):
|
|
759
|
+
"""
|
|
760
|
+
SwiGLU: SiLU(gate(x)) * up(x) -> down
|
|
761
|
+
Usado en LLaMA 2/3, Qwen, Mistral en lugar de GELU-FFN.
|
|
762
|
+
Dimension interna: 8/3 * d_model (convención LLaMA, redondeada a múltiplo de 4).
|
|
763
|
+
"""
|
|
764
|
+
|
|
765
|
+
def __init__(self, d_model: int, expansion: float = 8 / 3,
|
|
766
|
+
dropout: float = 0.0):
|
|
767
|
+
super().__init__()
|
|
768
|
+
inner = max(4, int(d_model * expansion))
|
|
769
|
+
inner = (inner + 3) // 4 * 4 # redondear a múltiplo de 4
|
|
770
|
+
self.gate_proj = nn.Linear(d_model, inner, bias=False)
|
|
771
|
+
self.up_proj = nn.Linear(d_model, inner, bias=False)
|
|
772
|
+
self.down_proj = nn.Linear(inner, d_model, bias=False)
|
|
773
|
+
self.drop = nn.Dropout(dropout) if dropout > 0 else nn.Identity()
|
|
774
|
+
# Inicialización estilo GPT
|
|
775
|
+
nn.init.normal_(self.gate_proj.weight, std=0.02)
|
|
776
|
+
nn.init.normal_(self.up_proj.weight, std=0.02)
|
|
777
|
+
nn.init.normal_(self.down_proj.weight, std=0.02)
|
|
778
|
+
|
|
779
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
780
|
+
return self.drop(self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)))
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
# ============================================================================
|
|
784
|
+
# MoE: Mixture of Experts topologico
|
|
785
|
+
# ============================================================================
|
|
786
|
+
|
|
787
|
+
class TopoMoEBrain(nn.Module):
|
|
788
|
+
"""
|
|
789
|
+
Mixture of Experts sobre la capa topologica.
|
|
790
|
+
|
|
791
|
+
Arquitectura (inspirada en DeepSeek-MoE / Mixtral):
|
|
792
|
+
- 1 experto compartido: QuaternionTorusBrain (siempre activo)
|
|
793
|
+
- N_EXPERTS expertos SwiGLU ligeros (activacion esparsa: Top-K por token)
|
|
794
|
+
- Router: Linear(D, N_EXPERTS) + softmax → top-K
|
|
795
|
+
|
|
796
|
+
Load-balancing loss (auxiliar): penaliza si un experto acapara todos los tokens.
|
|
797
|
+
Activa MOE_TOP_K de N_EXPERTS expertos por token.
|
|
798
|
+
|
|
799
|
+
Sin MoE (MOE_ENABLED=False): se comporta como QuaternionTorusBrain puro.
|
|
800
|
+
"""
|
|
801
|
+
|
|
802
|
+
def __init__(self, d_model: int, config: 'TopoGPT2Config'):
|
|
803
|
+
super().__init__()
|
|
804
|
+
self.d_model = d_model
|
|
805
|
+
self.moe_enabled = config.MOE_ENABLED
|
|
806
|
+
self.n_experts = config.N_EXPERTS
|
|
807
|
+
self.top_k = config.MOE_TOP_K
|
|
808
|
+
self.aux_weight = config.MOE_AUX_LOSS_WEIGHT
|
|
809
|
+
|
|
810
|
+
# Experto compartido: el TopoBrain original (siempre activo)
|
|
811
|
+
self.shared_expert = QuaternionTorusBrain(d_model, config)
|
|
812
|
+
|
|
813
|
+
if self.moe_enabled:
|
|
814
|
+
# N_EXPERTS expertos SwiGLU ligeros (dimension reducida para eficiencia)
|
|
815
|
+
self.experts = nn.ModuleList([
|
|
816
|
+
SwiGLU(d_model, expansion=4/3, dropout=config.DROPOUT)
|
|
817
|
+
for _ in range(self.n_experts)
|
|
818
|
+
])
|
|
819
|
+
# Router: produce logits por experto para cada token
|
|
820
|
+
self.router = nn.Linear(d_model, self.n_experts, bias=False)
|
|
821
|
+
nn.init.normal_(self.router.weight, std=0.02)
|
|
822
|
+
|
|
823
|
+
def _route(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
824
|
+
"""
|
|
825
|
+
x: [N, D] donde N = B*S (tokens aplanados)
|
|
826
|
+
Retorna:
|
|
827
|
+
expert_out: [N, D] suma ponderada de top-K expertos
|
|
828
|
+
aux_loss: escalar load-balancing loss
|
|
829
|
+
Routing vectorizado sin boolean indexing ni sincronizacion CUDA.
|
|
830
|
+
Usa dispatch por indices agrupados (estilo Mixtral/DeepSeek) para
|
|
831
|
+
compatibilidad total con torch.utils.checkpoint.
|
|
832
|
+
"""
|
|
833
|
+
N, D = x.shape
|
|
834
|
+
router_logits = self.router(x) # [N, n_experts]
|
|
835
|
+
router_probs = F.softmax(router_logits, dim=-1) # [N, n_experts]
|
|
836
|
+
# Top-K seleccion
|
|
837
|
+
top_k_probs, top_k_idx = torch.topk(router_probs, self.top_k, dim=-1)
|
|
838
|
+
# Renormalizar pesos dentro de los top-K
|
|
839
|
+
top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True).clamp(min=1e-9)
|
|
840
|
+
# Dispatch vectorizado: agrupar tokens por experto asignado
|
|
841
|
+
flat_idx = top_k_idx.reshape(-1) # [N*top_k]
|
|
842
|
+
flat_weights = top_k_probs.reshape(-1) # [N*top_k]
|
|
843
|
+
token_indices = torch.arange(N, device=x.device, dtype=torch.long)
|
|
844
|
+
token_indices = token_indices.unsqueeze(1).expand(-1, self.top_k).reshape(-1) # [N*top_k]
|
|
845
|
+
expert_out = torch.zeros_like(x)
|
|
846
|
+
|
|
847
|
+
# Se elimina la verificacion con .item() porque fuerza una sincronizacion
|
|
848
|
+
# CPU-GPU que rompe el recomputado de gradient checkpointing.
|
|
849
|
+
# scatter_add_ maneja tensores vacios de forma segura y eficiente.
|
|
850
|
+
for e in range(self.n_experts):
|
|
851
|
+
expert_mask = (flat_idx == e) # [N*top_k] boolean
|
|
852
|
+
src_token_idx = token_indices[expert_mask] # [count] indices en N
|
|
853
|
+
w = flat_weights[expert_mask].unsqueeze(-1).to(x.dtype) # [count, 1]
|
|
854
|
+
out_e = self.experts[e](x[src_token_idx]) # [count, D]
|
|
855
|
+
contrib = w * out_e
|
|
856
|
+
expert_out.scatter_add_(0, src_token_idx.unsqueeze(1).expand_as(contrib), contrib)
|
|
857
|
+
|
|
858
|
+
# Load-balancing loss (Switch Transformer style)
|
|
859
|
+
token_frac = router_probs.mean(dim=0) # [n_experts]
|
|
860
|
+
one_hot = F.one_hot(top_k_idx, self.n_experts).float()# [N, top_k, n_experts]
|
|
861
|
+
dispatch_frac = one_hot.mean(dim=(0, 1)) # [n_experts]
|
|
862
|
+
aux_loss = self.n_experts * (token_frac * dispatch_frac).sum()
|
|
863
|
+
return expert_out, aux_loss
|
|
864
|
+
|
|
865
|
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
866
|
+
"""
|
|
867
|
+
x: [B, S, D]
|
|
868
|
+
→ output: [B, S, D], aux_loss: escalar
|
|
869
|
+
"""
|
|
870
|
+
B, S, D = x.shape
|
|
871
|
+
|
|
872
|
+
# Experto compartido siempre activo
|
|
873
|
+
shared_out, recon_loss = self.shared_expert(x) # [B, S, D], scalar
|
|
874
|
+
|
|
875
|
+
if not self.moe_enabled:
|
|
876
|
+
return shared_out, recon_loss
|
|
877
|
+
|
|
878
|
+
# Expertos SwiGLU (esparso)
|
|
879
|
+
x_flat = x.reshape(B * S, D)
|
|
880
|
+
expert_out, aux_loss = self._route(x_flat)
|
|
881
|
+
expert_out = expert_out.reshape(B, S, D)
|
|
882
|
+
|
|
883
|
+
# Combinar: shared + routed (igual peso inicial)
|
|
884
|
+
output = shared_out + expert_out
|
|
885
|
+
|
|
886
|
+
# Combinar losses auxiliares
|
|
887
|
+
total_aux = recon_loss + self.aux_weight * aux_loss
|
|
888
|
+
|
|
889
|
+
return output, total_aux
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
class MultiHeadAttention(nn.Module):
|
|
893
|
+
"""
|
|
894
|
+
Multi-head attention con:
|
|
895
|
+
- Flash Attention (scaled_dot_product_attention de PyTorch 2.0+)
|
|
896
|
+
- Rotary Position Embeddings (RoPE)
|
|
897
|
+
- GQA (Grouped Query Attention): N_KV_HEADS < N_HEADS, reduce VRAM de K/V
|
|
898
|
+
- KV Cache para inferencia autoregresiva eficiente
|
|
899
|
+
- Temperatura termodinámica aprendible
|
|
900
|
+
"""
|
|
901
|
+
|
|
902
|
+
def __init__(self, d_model: int, n_heads: int, config: 'TopoGPT2Config'):
|
|
903
|
+
super().__init__()
|
|
904
|
+
self.d_model = d_model
|
|
905
|
+
self.n_heads = n_heads
|
|
906
|
+
self.n_kv = config.N_KV_HEADS # cabezas K/V (GQA)
|
|
907
|
+
self.n_groups = config.GQA_GROUPS # n_heads // n_kv
|
|
908
|
+
self.d_head = d_model // n_heads
|
|
909
|
+
self.window_size = config.ATTN_WINDOW # 0 = full attention
|
|
910
|
+
|
|
911
|
+
# Q con n_heads completo; K/V con n_kv (GQA)
|
|
912
|
+
self.q_proj = nn.Linear(d_model, n_heads * self.d_head, bias=False)
|
|
913
|
+
self.k_proj = nn.Linear(d_model, self.n_kv * self.d_head, bias=False)
|
|
914
|
+
self.v_proj = nn.Linear(d_model, self.n_kv * self.d_head, bias=False)
|
|
915
|
+
self.o_proj = nn.Linear(d_model, d_model, bias=False)
|
|
916
|
+
|
|
917
|
+
self.rope = RotaryEmbedding(self.d_head, max_seq_len=config.MAX_SEQ_LEN)
|
|
918
|
+
self.temperature = nn.Parameter(torch.tensor(config.T_INIT))
|
|
919
|
+
self.dropout_p = config.DROPOUT if config.DROPOUT > 0 else 0.0
|
|
920
|
+
|
|
921
|
+
def forward(self, x: torch.Tensor, is_causal: bool = True,
|
|
922
|
+
past_kv: Optional[Tuple[torch.Tensor, torch.Tensor]] = None
|
|
923
|
+
) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
|
924
|
+
"""
|
|
925
|
+
Args:
|
|
926
|
+
x: [B, S, D]
|
|
927
|
+
is_causal: usar mascara causal
|
|
928
|
+
past_kv: (K_cache, V_cache) de pasos anteriores o None
|
|
929
|
+
Returns:
|
|
930
|
+
out: [B, S, D]
|
|
931
|
+
kv_cache: (K, V) completos para cachear en generate()
|
|
932
|
+
"""
|
|
933
|
+
B, S, D = x.shape
|
|
934
|
+
|
|
935
|
+
Q = self.q_proj(x).view(B, S, self.n_heads, self.d_head).transpose(1, 2)
|
|
936
|
+
K = self.k_proj(x).view(B, S, self.n_kv, self.d_head).transpose(1, 2)
|
|
937
|
+
V = self.v_proj(x).view(B, S, self.n_kv, self.d_head).transpose(1, 2)
|
|
938
|
+
|
|
939
|
+
# RoPE con offset: si hay cache, Q y K_new se posicionan a partir de past_len
|
|
940
|
+
past_len = past_kv[0].shape[2] if past_kv is not None else 0
|
|
941
|
+
Q, K = self.rope(Q, K, seq_len=S, offset=past_len)
|
|
942
|
+
|
|
943
|
+
# Concatenar K/V con cache (despues de aplicar RoPE al nuevo K)
|
|
944
|
+
if past_kv is not None:
|
|
945
|
+
K = torch.cat([past_kv[0], K], dim=2)
|
|
946
|
+
V = torch.cat([past_kv[1], V], dim=2)
|
|
947
|
+
|
|
948
|
+
attn_mask = None
|
|
949
|
+
use_causal = (is_causal and past_kv is None)
|
|
950
|
+
|
|
951
|
+
if self.window_size > 0:
|
|
952
|
+
if past_kv is None:
|
|
953
|
+
S_q = Q.shape[2]
|
|
954
|
+
S_kv = K.shape[2]
|
|
955
|
+
if S_kv > self.window_size:
|
|
956
|
+
row = torch.arange(S_q, device=Q.device).unsqueeze(1)
|
|
957
|
+
col = torch.arange(S_kv, device=Q.device).unsqueeze(0)
|
|
958
|
+
upper = row < col
|
|
959
|
+
outside = (row - col) >= self.window_size
|
|
960
|
+
attn_mask = upper | outside
|
|
961
|
+
use_causal = False
|
|
962
|
+
else:
|
|
963
|
+
if K.shape[2] > self.window_size:
|
|
964
|
+
K = K[:, :, -self.window_size:, :]
|
|
965
|
+
V = V[:, :, -self.window_size:, :]
|
|
966
|
+
use_causal = False
|
|
967
|
+
|
|
968
|
+
kv_cache = (K, V)
|
|
969
|
+
K_full = K
|
|
970
|
+
|
|
971
|
+
# GQA: expandir K/V al numero de cabezas de Q via repeat_interleave
|
|
972
|
+
if self.n_groups > 1:
|
|
973
|
+
K_full = K_full.repeat_interleave(self.n_groups, dim=1) # [B, n_heads, S_k, d]
|
|
974
|
+
V_exp = V.repeat_interleave(self.n_groups, dim=1)
|
|
975
|
+
else:
|
|
976
|
+
V_exp = V
|
|
977
|
+
|
|
978
|
+
scale = (self.d_head ** -0.5) / self.temperature.abs().clamp(min=1e-6)
|
|
979
|
+
|
|
980
|
+
out = F.scaled_dot_product_attention(
|
|
981
|
+
Q, K_full, V_exp,
|
|
982
|
+
attn_mask=attn_mask,
|
|
983
|
+
dropout_p=self.dropout_p if self.training else 0.0,
|
|
984
|
+
is_causal=use_causal,
|
|
985
|
+
scale=scale.item(),
|
|
986
|
+
) # [B, n_heads, S, d_head]
|
|
987
|
+
|
|
988
|
+
out = out.transpose(1, 2).contiguous().view(B, S, D)
|
|
989
|
+
return self.o_proj(out), kv_cache
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
# ============================================================================
|
|
993
|
+
# CAPA TRANSFORMER TOPOLÓGICA
|
|
994
|
+
# ============================================================================
|
|
995
|
+
|
|
996
|
+
class TopoGPT2Layer(nn.Module):
|
|
997
|
+
"""
|
|
998
|
+
Capa del transformer con TopoMoEBrain (TopoBrain + MoE SwiGLU experts).
|
|
999
|
+
|
|
1000
|
+
Esquema pre-norm (estilo LLaMA):
|
|
1001
|
+
x = x + Attention_GQA(RMSNorm(x))
|
|
1002
|
+
x = x + TopoMoEBrain(RMSNorm(x))
|
|
1003
|
+
"""
|
|
1004
|
+
|
|
1005
|
+
def __init__(self, d_model: int, n_heads: int, config: 'TopoGPT2Config'):
|
|
1006
|
+
super().__init__()
|
|
1007
|
+
self.norm1 = RMSNorm(d_model)
|
|
1008
|
+
self.norm2 = RMSNorm(d_model)
|
|
1009
|
+
self.attn = MultiHeadAttention(d_model, n_heads, config)
|
|
1010
|
+
self.topo_brain = TopoMoEBrain(d_model, config)
|
|
1011
|
+
self.dropout = nn.Dropout(config.DROPOUT)
|
|
1012
|
+
self.use_ckpt = config.GRADIENT_CHECKPOINTING
|
|
1013
|
+
|
|
1014
|
+
def _forward_impl(self, x: torch.Tensor,
|
|
1015
|
+
past_kv: Optional[Tuple] = None
|
|
1016
|
+
) -> Tuple[torch.Tensor, torch.Tensor, Tuple]:
|
|
1017
|
+
attn_out, kv_cache = self.attn(self.norm1(x), past_kv=past_kv)
|
|
1018
|
+
x = x + self.dropout(attn_out)
|
|
1019
|
+
brain_out, aux_loss = self.topo_brain(self.norm2(x))
|
|
1020
|
+
x = x + self.dropout(brain_out)
|
|
1021
|
+
return x, aux_loss, kv_cache
|
|
1022
|
+
|
|
1023
|
+
def forward(self, x: torch.Tensor,
|
|
1024
|
+
past_kv: Optional[Tuple] = None
|
|
1025
|
+
) -> Tuple[torch.Tensor, torch.Tensor, Tuple]:
|
|
1026
|
+
"""
|
|
1027
|
+
Retorna (x_out, aux_loss, kv_cache).
|
|
1028
|
+
Con gradient checkpointing en training (solo cuando no hay KV cache).
|
|
1029
|
+
"""
|
|
1030
|
+
if self.use_ckpt and self.training and past_kv is None:
|
|
1031
|
+
def ckpt_fn(x_in):
|
|
1032
|
+
out, al, kvc = self._forward_impl(x_in, past_kv=None)
|
|
1033
|
+
return out, al
|
|
1034
|
+
out, al = grad_ckpt(ckpt_fn, x, use_reentrant=False)
|
|
1035
|
+
return out, al, None # kv_cache no disponible con ckpt (solo entrenamiento)
|
|
1036
|
+
return self._forward_impl(x, past_kv=past_kv)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
# ============================================================================
|
|
1040
|
+
# MODELO COMPLETO
|
|
1041
|
+
# ============================================================================
|
|
1042
|
+
|
|
1043
|
+
class TopoGPT2(nn.Module):
|
|
1044
|
+
"""
|
|
1045
|
+
TopoGPT2: Transformer de lenguaje con TopoBrain cuaternión-espectral.
|
|
1046
|
+
|
|
1047
|
+
Arquitectura:
|
|
1048
|
+
Embedding de tokens + RoPE (en Attention)
|
|
1049
|
+
N_LAYERS × TopoGPT2Layer (Attention + QuaternionTorusBrain)
|
|
1050
|
+
RMSNorm final
|
|
1051
|
+
Proyección a vocabulario (weight-tied con embeddings)
|
|
1052
|
+
"""
|
|
1053
|
+
|
|
1054
|
+
def __init__(self, config: TopoGPT2Config):
|
|
1055
|
+
super().__init__()
|
|
1056
|
+
self.config = config
|
|
1057
|
+
|
|
1058
|
+
self.token_embed = nn.Embedding(config.VOCAB_SIZE, config.D_MODEL)
|
|
1059
|
+
nn.init.normal_(self.token_embed.weight, std=0.02)
|
|
1060
|
+
|
|
1061
|
+
self.layers = nn.ModuleList([
|
|
1062
|
+
TopoGPT2Layer(config.D_MODEL, config.N_HEADS, config)
|
|
1063
|
+
for _ in range(config.N_LAYERS)
|
|
1064
|
+
])
|
|
1065
|
+
self.final_norm = RMSNorm(config.D_MODEL)
|
|
1066
|
+
self.lm_head = nn.Linear(config.D_MODEL, config.VOCAB_SIZE, bias=False)
|
|
1067
|
+
|
|
1068
|
+
# Weight tying (GPT-2 style): embedding y cabeza de lenguaje comparten pesos
|
|
1069
|
+
self.lm_head.weight = self.token_embed.weight
|
|
1070
|
+
|
|
1071
|
+
self.memory_tokens: Optional[nn.Parameter] = None
|
|
1072
|
+
if config.N_MEMORY_TOKENS > 0:
|
|
1073
|
+
self.memory_tokens = nn.Parameter(
|
|
1074
|
+
torch.randn(1, config.N_MEMORY_TOKENS, config.D_MODEL) * 0.02
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
self._init_weights()
|
|
1078
|
+
|
|
1079
|
+
def _init_weights(self):
|
|
1080
|
+
for module in self.modules():
|
|
1081
|
+
if isinstance(module, nn.Linear) and module is not self.lm_head:
|
|
1082
|
+
nn.init.normal_(module.weight, std=0.02)
|
|
1083
|
+
if module.bias is not None:
|
|
1084
|
+
nn.init.zeros_(module.bias)
|
|
1085
|
+
|
|
1086
|
+
def forward(self, token_ids: torch.Tensor,
|
|
1087
|
+
past_kvs: Optional[List[Optional[Tuple]]] = None
|
|
1088
|
+
) -> Tuple[torch.Tensor, torch.Tensor, List[Tuple]]:
|
|
1089
|
+
"""
|
|
1090
|
+
token_ids: [B, S] (enteros)
|
|
1091
|
+
past_kvs: lista de (K, V) por capa, o None para entrenamiento
|
|
1092
|
+
→ logits: [B, S, VOCAB_SIZE], aux_loss: scalar, new_kvs: list[(K,V)]
|
|
1093
|
+
"""
|
|
1094
|
+
x = self.token_embed(token_ids) # [B, S, D_MODEL]
|
|
1095
|
+
|
|
1096
|
+
total_aux = torch.tensor(0.0, device=x.device)
|
|
1097
|
+
new_kvs: List[Tuple] = []
|
|
1098
|
+
|
|
1099
|
+
for i, layer in enumerate(self.layers):
|
|
1100
|
+
pkv = past_kvs[i] if past_kvs is not None else None
|
|
1101
|
+
x, al, kvc = layer(x, past_kv=pkv)
|
|
1102
|
+
total_aux = total_aux + al
|
|
1103
|
+
new_kvs.append(kvc)
|
|
1104
|
+
|
|
1105
|
+
x = self.final_norm(x)
|
|
1106
|
+
logits = self.lm_head(x)
|
|
1107
|
+
return logits, total_aux / len(self.layers), new_kvs
|
|
1108
|
+
|
|
1109
|
+
def forward_with_memory(
|
|
1110
|
+
self, token_ids: torch.Tensor
|
|
1111
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1112
|
+
"""Process long sequences with latent memory-token context compression.
|
|
1113
|
+
|
|
1114
|
+
Splits `token_ids` [B, S] into segments of size MEMORY_SEGMENT_LEN.
|
|
1115
|
+
Each segment is processed with N_MEMORY_TOKENS prepended. The output
|
|
1116
|
+
at memory-token positions after segment k becomes the memory-state
|
|
1117
|
+
input for segment k+1, compressing all prior context into a fixed-size
|
|
1118
|
+
latent vector.
|
|
1119
|
+
|
|
1120
|
+
Returns (logits [B, S, VOCAB_SIZE], aux_loss).
|
|
1121
|
+
"""
|
|
1122
|
+
cfg = self.config
|
|
1123
|
+
B, S = token_ids.shape
|
|
1124
|
+
N_mem = cfg.N_MEMORY_TOKENS
|
|
1125
|
+
seg_len = cfg.MEMORY_SEGMENT_LEN
|
|
1126
|
+
|
|
1127
|
+
if self.memory_tokens is None or N_mem == 0 or seg_len == 0:
|
|
1128
|
+
logits, aux, _ = self.forward(token_ids)
|
|
1129
|
+
return logits, aux
|
|
1130
|
+
|
|
1131
|
+
device = token_ids.device
|
|
1132
|
+
mem = self.memory_tokens.expand(B, -1, -1)
|
|
1133
|
+
|
|
1134
|
+
all_logits = []
|
|
1135
|
+
total_aux = torch.tensor(0.0, device=device)
|
|
1136
|
+
|
|
1137
|
+
for seg_start in range(0, S, seg_len):
|
|
1138
|
+
seg_end = min(seg_start + seg_len, S)
|
|
1139
|
+
seg_ids = token_ids[:, seg_start:seg_end]
|
|
1140
|
+
seg_emb = self.token_embed(seg_ids)
|
|
1141
|
+
|
|
1142
|
+
x = torch.cat([mem, seg_emb], dim=1)
|
|
1143
|
+
|
|
1144
|
+
for layer in self.layers:
|
|
1145
|
+
x, al, _ = layer(x)
|
|
1146
|
+
total_aux = total_aux + al
|
|
1147
|
+
|
|
1148
|
+
seg_out = x[:, N_mem:, :]
|
|
1149
|
+
mem = x[:, :N_mem, :]
|
|
1150
|
+
|
|
1151
|
+
seg_out = self.final_norm(seg_out)
|
|
1152
|
+
seg_logits = self.lm_head(seg_out)
|
|
1153
|
+
all_logits.append(seg_logits)
|
|
1154
|
+
|
|
1155
|
+
logits = torch.cat(all_logits, dim=1)
|
|
1156
|
+
aux_loss = total_aux / len(self.layers)
|
|
1157
|
+
return logits, aux_loss
|
|
1158
|
+
|
|
1159
|
+
def count_params(self) -> Dict[str, int]:
|
|
1160
|
+
total = sum(p.numel() for p in self.parameters())
|
|
1161
|
+
trainable = sum(p.numel() for p in self.parameters() if p.requires_grad)
|
|
1162
|
+
return {'total': total, 'trainable': trainable}
|
|
1163
|
+
|
|
1164
|
+
@torch.no_grad()
|
|
1165
|
+
def generate(self, token_ids: torch.Tensor, max_new_tokens: int = 200,
|
|
1166
|
+
temperature: float = 0.8, top_k: int = 50,
|
|
1167
|
+
repetition_penalty: float = 1.0) -> torch.Tensor:
|
|
1168
|
+
"""
|
|
1169
|
+
Autoregressive generation with KV cache and top-k sampling.
|
|
1170
|
+
|
|
1171
|
+
Args:
|
|
1172
|
+
token_ids: [B, S_prompt] prompt tokens.
|
|
1173
|
+
max_new_tokens: Maximum tokens to generate.
|
|
1174
|
+
temperature: Sampling temperature (lower = more deterministic).
|
|
1175
|
+
top_k: Top-k filtering (0 = disabled).
|
|
1176
|
+
repetition_penalty: Penalty for repeating tokens (>1 = penalize).
|
|
1177
|
+
|
|
1178
|
+
Returns:
|
|
1179
|
+
[B, S_prompt + generated] full token sequence.
|
|
1180
|
+
"""
|
|
1181
|
+
self.eval()
|
|
1182
|
+
cfg = self.config
|
|
1183
|
+
|
|
1184
|
+
ctx = token_ids[:, -cfg.MAX_SEQ_LEN:]
|
|
1185
|
+
logits, _, past_kvs = self(ctx)
|
|
1186
|
+
logits = logits[:, -1, :] / max(temperature, 1e-8)
|
|
1187
|
+
if repetition_penalty != 1.0:
|
|
1188
|
+
mask = torch.zeros_like(logits, dtype=torch.bool)
|
|
1189
|
+
mask.scatter_(1, ctx, True)
|
|
1190
|
+
logits = torch.where(mask, logits / repetition_penalty, logits)
|
|
1191
|
+
if top_k > 0:
|
|
1192
|
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
|
1193
|
+
logits[logits < v[:, -1:]] = float('-inf')
|
|
1194
|
+
next_tok = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
|
1195
|
+
token_ids = torch.cat([token_ids, next_tok], dim=1)
|
|
1196
|
+
|
|
1197
|
+
for _ in range(max_new_tokens - 1):
|
|
1198
|
+
logits, _, past_kvs = self(next_tok, past_kvs=past_kvs)
|
|
1199
|
+
logits = logits[:, -1, :] / max(temperature, 1e-8)
|
|
1200
|
+
if repetition_penalty != 1.0:
|
|
1201
|
+
mask = torch.zeros_like(logits, dtype=torch.bool)
|
|
1202
|
+
mask.scatter_(1, token_ids, True)
|
|
1203
|
+
logits = torch.where(mask, logits / repetition_penalty, logits)
|
|
1204
|
+
if top_k > 0:
|
|
1205
|
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
|
1206
|
+
logits[logits < v[:, -1:]] = float('-inf')
|
|
1207
|
+
next_tok = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
|
1208
|
+
token_ids = torch.cat([token_ids, next_tok], dim=1)
|
|
1209
|
+
|
|
1210
|
+
if (next_tok == 50256).all():
|
|
1211
|
+
break
|
|
1212
|
+
|
|
1213
|
+
return token_ids
|
|
1214
|
+
|
|
1215
|
+
@torch.no_grad()
|
|
1216
|
+
def generate_with_continuation(self, token_ids: torch.Tensor,
|
|
1217
|
+
tokenizer: Any, max_new_tokens: int = 200,
|
|
1218
|
+
temperature: float = 0.8, top_k: int = 50,
|
|
1219
|
+
repetition_penalty: float = 1.0,
|
|
1220
|
+
max_continuations: int = 3,
|
|
1221
|
+
tail_lines: int = 2) -> torch.Tensor:
|
|
1222
|
+
full_ids = token_ids.clone()
|
|
1223
|
+
continuations = 0
|
|
1224
|
+
|
|
1225
|
+
while continuations <= max_continuations:
|
|
1226
|
+
n_new = max_new_tokens - (full_ids.shape[1] - token_ids.shape[1]) if continuations > 0 else max_new_tokens
|
|
1227
|
+
if n_new <= 0:
|
|
1228
|
+
break
|
|
1229
|
+
|
|
1230
|
+
out = self.generate(full_ids, max_new_tokens=n_new,
|
|
1231
|
+
temperature=temperature, top_k=top_k,
|
|
1232
|
+
repetition_penalty=repetition_penalty)
|
|
1233
|
+
full_ids = out
|
|
1234
|
+
|
|
1235
|
+
out_text = tokenizer.decode(out[0].tolist())
|
|
1236
|
+
prompt_text = tokenizer.decode(token_ids[0].tolist())
|
|
1237
|
+
generated = out_text[len(prompt_text):]
|
|
1238
|
+
|
|
1239
|
+
from .continuation import is_response_complete, extract_tail_for_continuation
|
|
1240
|
+
|
|
1241
|
+
if is_response_complete(generated):
|
|
1242
|
+
break
|
|
1243
|
+
|
|
1244
|
+
tail = extract_tail_for_continuation(generated, tail_lines=tail_lines)
|
|
1245
|
+
if not tail:
|
|
1246
|
+
break
|
|
1247
|
+
|
|
1248
|
+
continuations += 1
|
|
1249
|
+
full_text = prompt_text + tail
|
|
1250
|
+
full_ids = torch.tensor([tokenizer.encode(full_text)],
|
|
1251
|
+
dtype=torch.long, device=full_ids.device)
|
|
1252
|
+
|
|
1253
|
+
return full_ids
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
# ============================================================================
|
|
1257
|
+
# TOKENIZADOR
|
|
1258
|
+
# ============================================================================
|
|
1259
|
+
|
|
1260
|
+
class BPETokenizer:
|
|
1261
|
+
"""Wrapper alrededor de tiktoken (GPT-2 compatible)."""
|
|
1262
|
+
|
|
1263
|
+
def __init__(self, encoding: str = 'gpt2'):
|
|
1264
|
+
try:
|
|
1265
|
+
import tiktoken
|
|
1266
|
+
self.enc = tiktoken.get_encoding(encoding)
|
|
1267
|
+
self.vocab_size = self.enc.n_vocab
|
|
1268
|
+
except ImportError:
|
|
1269
|
+
raise ImportError("Instala tiktoken: pip install tiktoken")
|
|
1270
|
+
|
|
1271
|
+
def encode(self, text: str) -> List[int]:
|
|
1272
|
+
return self.enc.encode(text, allowed_special={'<|endoftext|>'})
|
|
1273
|
+
|
|
1274
|
+
def decode(self, tokens: List[int]) -> str:
|
|
1275
|
+
return self.enc.decode(tokens)
|
|
1276
|
+
|
|
1277
|
+
def eot_token(self) -> int:
|
|
1278
|
+
return self.enc.eot_token
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
# ============================================================================
|
|
1282
|
+
# MULTI-FILE CORPUS SYSTEM
|
|
1283
|
+
# ============================================================================
|
|
1284
|
+
|
|
1285
|
+
_TEXT_EXTENSIONS: set = {
|
|
1286
|
+
'.py', '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx',
|
|
1287
|
+
'.go', '.rs', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.mts',
|
|
1288
|
+
'.md', '.txt', '.json', '.jsonl', '.jsonc',
|
|
1289
|
+
'.yaml', '.yml', '.toml', '.xml', '.svg',
|
|
1290
|
+
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
1291
|
+
'.sh', '.bash', '.zsh', '.fish', '.ash', '.ksh',
|
|
1292
|
+
'.asm', '.s', '.S', '.h',
|
|
1293
|
+
'.tex', '.cls', '.sty',
|
|
1294
|
+
'.r', '.R', '.rb', '.erb', '.rake',
|
|
1295
|
+
'.java', '.kt', '.kts', '.scala', '.groovy', '.gradle',
|
|
1296
|
+
'.swift', '.cs', '.fs', '.fsx', '.vb',
|
|
1297
|
+
'.lua', '.vim', '.el', '.clj', '.cljs', '.cljc', '.edn',
|
|
1298
|
+
'.ex', '.exs', '.erl', '.hrl', '.gleam',
|
|
1299
|
+
'.hs', '.purs', '.Agda', '.Idris',
|
|
1300
|
+
'.sql', '.pl', '.pm', '.t', '.prolog',
|
|
1301
|
+
'.ini', '.cfg', '.conf', '.inf', '.reg',
|
|
1302
|
+
'.env', '.editorconfig', '.gitignore', '.gitattributes',
|
|
1303
|
+
'.dockerfile', 'dockerfile', 'makefile', 'makefile', 'gnumakefile',
|
|
1304
|
+
'.cmake', '.cmake.in', '.mk', '.bazel', '.bzl', '.build',
|
|
1305
|
+
'.proto', '.graphql', '.gql', '.thrift',
|
|
1306
|
+
'.ps1', '.psm1', '.psd1', '.pssc',
|
|
1307
|
+
'.过户', '.汉', # rare but valid UTF-8 filenames
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
_NO_EXT_NAMES: set = {
|
|
1311
|
+
'makefile', 'gnumakefile', 'cmakelists.txt', 'makefile.in',
|
|
1312
|
+
'dockerfile', 'containerfile', 'jenkinsfile', 'vagrantfile',
|
|
1313
|
+
'procfile', 'ruff.toml', 'pylintrc', 'flake8', 'mypy.ini',
|
|
1314
|
+
'setup.cfg', 'tox.ini', 'pytest.ini', '.coveragerc',
|
|
1315
|
+
'requirements.txt', 'requirements-dev.txt', 'pyproject.toml',
|
|
1316
|
+
'package.json', 'package-lock.json', 'tsconfig.json', 'jsconfig.json',
|
|
1317
|
+
'browserslist', '.babelrc', '.eslintrc', '.prettierrc',
|
|
1318
|
+
'vite.config', 'rollup.config', 'webpack.config', 'next.config',
|
|
1319
|
+
'nuxt.config', 'svelte.config', 'tailwind.config', 'postcss.config',
|
|
1320
|
+
'gradle.properties', 'build.gradle', 'settings.gradle',
|
|
1321
|
+
'cargo.toml', 'cargo.lock', 'rustfmt.toml', 'clippy.toml',
|
|
1322
|
+
'go.mod', 'go.sum', 'go.work',
|
|
1323
|
+
'build.gradle.kts', 'settings.gradle.kts',
|
|
1324
|
+
'composercjson', 'composer.lock',
|
|
1325
|
+
'gemfile', 'gemfile.lock', 'rakefile', 'rakefile.rb',
|
|
1326
|
+
'dockerignore', 'editorconfig', 'gitignore', 'gitattributes',
|
|
1327
|
+
'pre-commit', 'shell.nix', 'default.nix', 'flake.nix',
|
|
1328
|
+
'hosts', 'resolv.conf', 'nginx.conf', 'apache2.conf',
|
|
1329
|
+
'run', 'env', 'configure', 'install', 'build', 'clean',
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
_SKIP_SUFFIXES: set = {
|
|
1333
|
+
'.o', '.obj', '.so', '.dll', '.dylib', '.exe', '.elf', '.ko',
|
|
1334
|
+
'.bin', '.dat', '.pt', '.pth', '.ckpt', '.safetensors',
|
|
1335
|
+
'.zip', '.tar', '.gz', '.tar.gz', '.tgz', '.bz2', '.xz', '.7z',
|
|
1336
|
+
'.mp4', '.avi', '.mkv', '.mov', '.webm', '.mp3', '.wav', '.flac',
|
|
1337
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.ico',
|
|
1338
|
+
'.ttf', '.woff', '.woff2', '.eot', '.otf',
|
|
1339
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
1340
|
+
'.db', '.sqlite', '.sqlite3', '.db3',
|
|
1341
|
+
'.pem', '.key', '.ccache', '.jks', '.keystore', '.p12', '.pfx',
|
|
1342
|
+
'.pyc', '.pyo', '.class', '.jar', '.war',
|
|
1343
|
+
'.iso', '.img', '.qcow2', '.vmdk', '.ova',
|
|
1344
|
+
'.py.bak', '.py.old', '.py.orig',
|
|
1345
|
+
'.min.js', '.min.css', '.bundle.js',
|
|
1346
|
+
'.lock', '.lock.json', # package lock files are data, not code
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
_SKIP_DIRS: set = {
|
|
1350
|
+
'.git', '.svn', '.hg', '.bzr', 'node_modules', '__pycache__',
|
|
1351
|
+
'.venv', 'venv', 'env', '.env', '.conda',
|
|
1352
|
+
'build', 'dist', '.next', 'out', '.nuxt', '.svelte-kit',
|
|
1353
|
+
'.obsidian', '.trash', '.cache', '.tmp',
|
|
1354
|
+
'target', '.rustup', '.cargo', '.cargo/registry',
|
|
1355
|
+
'.mypy_cache', '.pytest_cache', '.ruff_cache',
|
|
1356
|
+
'.phpunit', '.parrot', 'lombok', '.gradle',
|
|
1357
|
+
'pb_data', '.qtcreator', '.idea', '.vscode',
|
|
1358
|
+
'.android', 'gradle/wrapper', '.gradle',
|
|
1359
|
+
'site-packages', 'node_modules', 'bower_components',
|
|
1360
|
+
'__pypackages__', '.pip-cache', '.npm', '.yarn',
|
|
1361
|
+
'.cache/pip', '.cache/modernizr',
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
_SKIP_DIRS: set = {
|
|
1365
|
+
'.git', 'node_modules', '__pycache__', '.venv', 'venv',
|
|
1366
|
+
'build', 'dist', '.obsidian', '.mypy_cache', '.pytest_cache',
|
|
1367
|
+
'.ruff_cache', 'target', '.next', 'out', '.qtcreator',
|
|
1368
|
+
'pb_data',
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
_SKIP_SUFFIXES: set = {
|
|
1372
|
+
'.o', '.obj', '.so', '.dll', '.dylib', '.exe', '.elf', '.ko',
|
|
1373
|
+
'.bin', '.dat', '.pt', '.pth', '.ckpt', '.safetensors',
|
|
1374
|
+
'.zip', '.tar', '.gz', '.tar.gz', '.tgz', '.bz2', '.xz', '.7z',
|
|
1375
|
+
'.mp4', '.avi', '.mkv', '.mov', '.webm', '.mp3', '.wav', '.flac',
|
|
1376
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.ico',
|
|
1377
|
+
'.ttf', '.woff', '.woff2', '.eot', '.otf',
|
|
1378
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
1379
|
+
'.db', '.sqlite', '.sqlite3', '.db3',
|
|
1380
|
+
'.pem', '.key', '.ccache', '.jks', '.keystore',
|
|
1381
|
+
'.pyc', '.pyo', '.class', '.jar', '.war',
|
|
1382
|
+
'.iso', '.img', '.qcow2', '.vmdk', '.ova', '.safetensors',
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
class FileManifest:
|
|
1387
|
+
"""Disk-cached manifest of text files found in a directory tree."""
|
|
1388
|
+
|
|
1389
|
+
def __init__(self, root: str, cache_dir: str, logger: logging.Logger):
|
|
1390
|
+
self.root = root
|
|
1391
|
+
self.cache_dir = cache_dir
|
|
1392
|
+
self.logger = logger
|
|
1393
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
1394
|
+
self.cache_path = os.path.join(cache_dir, 'file_manifest.json')
|
|
1395
|
+
|
|
1396
|
+
def scan(self, force: bool = False) -> List[str]:
|
|
1397
|
+
"""Walk directory tree collecting text file paths. Cached to disk."""
|
|
1398
|
+
if not force and os.path.exists(self.cache_path):
|
|
1399
|
+
self.logger.info(f"Loading file manifest from cache: {self.cache_path}")
|
|
1400
|
+
try:
|
|
1401
|
+
with open(self.cache_path, 'r', encoding='utf-8') as f:
|
|
1402
|
+
paths = json.load(f)
|
|
1403
|
+
self.logger.info(f"Manifest: {len(paths)} files (cached)")
|
|
1404
|
+
return paths
|
|
1405
|
+
except (json.JSONDecodeError, IOError):
|
|
1406
|
+
self.logger.warning("Corrupted manifest cache, rescanning...")
|
|
1407
|
+
|
|
1408
|
+
self.logger.info(f"Scanning directory tree: {self.root}")
|
|
1409
|
+
paths = []
|
|
1410
|
+
count_total = 0
|
|
1411
|
+
count_skipped = 0
|
|
1412
|
+
try:
|
|
1413
|
+
base = Path(self.root).resolve()
|
|
1414
|
+
for entry in base.rglob('*'):
|
|
1415
|
+
count_total += 1
|
|
1416
|
+
if count_total % 50_000 == 0:
|
|
1417
|
+
self.logger.info(
|
|
1418
|
+
f" Scan progress: {count_total:,} entries visited, "
|
|
1419
|
+
f"{len(paths):,} text files found, "
|
|
1420
|
+
f"{count_skipped:,} skipped")
|
|
1421
|
+
|
|
1422
|
+
if not entry.is_file():
|
|
1423
|
+
continue
|
|
1424
|
+
parts = set(entry.relative_to(base).parts)
|
|
1425
|
+
if parts & _SKIP_DIRS:
|
|
1426
|
+
count_skipped += 1
|
|
1427
|
+
continue
|
|
1428
|
+
suffix = entry.suffix.lower()
|
|
1429
|
+
if suffix in _SKIP_SUFFIXES:
|
|
1430
|
+
count_skipped += 1
|
|
1431
|
+
continue
|
|
1432
|
+
if suffix in _TEXT_EXTENSIONS:
|
|
1433
|
+
paths.append(str(entry))
|
|
1434
|
+
continue
|
|
1435
|
+
if suffix == '' and entry.name.lower() in _NO_EXT_NAMES:
|
|
1436
|
+
paths.append(str(entry))
|
|
1437
|
+
continue
|
|
1438
|
+
if suffix == '':
|
|
1439
|
+
count_skipped += 1
|
|
1440
|
+
continue
|
|
1441
|
+
count_skipped += 1
|
|
1442
|
+
except PermissionError:
|
|
1443
|
+
pass
|
|
1444
|
+
|
|
1445
|
+
self.logger.info(
|
|
1446
|
+
f"Scan complete: {count_total:,} total entries, "
|
|
1447
|
+
f"{len(paths):,} text files, {count_skipped:,} skipped")
|
|
1448
|
+
with open(self.cache_path, 'w', encoding='utf-8') as f:
|
|
1449
|
+
json.dump(paths, f)
|
|
1450
|
+
return paths
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
class MemmapTokenizer:
|
|
1454
|
+
"""Tokenizes file paths into a memory-mapped numpy array on disk.
|
|
1455
|
+
|
|
1456
|
+
Uses incremental file reading and batched writing to avoid loading
|
|
1457
|
+
all tokens into RAM. Tokens are stored as raw int64 on disk and
|
|
1458
|
+
accessed via numpy memmap (OS-level paging, near-zero RAM footprint).
|
|
1459
|
+
"""
|
|
1460
|
+
|
|
1461
|
+
CHUNK_TOKENS = 2_000_000
|
|
1462
|
+
|
|
1463
|
+
def __init__(self, cache_dir: str, logger: logging.Logger):
|
|
1464
|
+
self.cache_dir = cache_dir
|
|
1465
|
+
self.logger = logger
|
|
1466
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
1467
|
+
|
|
1468
|
+
def tokenize(self, file_paths: List[str], tokenizer: 'BPETokenizer',
|
|
1469
|
+
cache_key: str, max_tokens: int,
|
|
1470
|
+
min_chars: int = 20) -> np.ndarray:
|
|
1471
|
+
"""Tokenize all files and return a memory-mapped numpy array.
|
|
1472
|
+
|
|
1473
|
+
Args:
|
|
1474
|
+
file_paths: List of absolute file paths to tokenize.
|
|
1475
|
+
tokenizer: BPE tokenizer instance.
|
|
1476
|
+
cache_key: Unique key for caching tokens to disk.
|
|
1477
|
+
max_tokens: Maximum number of tokens to produce.
|
|
1478
|
+
min_chars: Skip files with fewer characters.
|
|
1479
|
+
|
|
1480
|
+
Returns:
|
|
1481
|
+
np.ndarray backed by a memmap on disk. Only accessed pages
|
|
1482
|
+
are loaded into RAM by the OS virtual memory system.
|
|
1483
|
+
"""
|
|
1484
|
+
cache_path = os.path.join(self.cache_dir, f'tokens_{cache_key}.bin')
|
|
1485
|
+
meta_path = os.path.join(self.cache_dir, f'tokens_{cache_key}.json')
|
|
1486
|
+
|
|
1487
|
+
if os.path.exists(cache_path) and os.path.exists(meta_path):
|
|
1488
|
+
with open(meta_path, 'r') as f:
|
|
1489
|
+
meta = json.load(f)
|
|
1490
|
+
n = meta['num_tokens']
|
|
1491
|
+
self.logger.info(f"Loading cached tokens: {n:,} tokens ({cache_key})")
|
|
1492
|
+
return np.memmap(cache_path, dtype=np.int64, mode='r', shape=(n,))
|
|
1493
|
+
|
|
1494
|
+
self.logger.info(
|
|
1495
|
+
f"Tokenizing {len(file_paths):,} files (max {max_tokens:,} tokens)...")
|
|
1496
|
+
|
|
1497
|
+
batch = []
|
|
1498
|
+
total = 0
|
|
1499
|
+
processed = 0
|
|
1500
|
+
|
|
1501
|
+
with open(cache_path, 'wb') as f:
|
|
1502
|
+
for path in file_paths:
|
|
1503
|
+
try:
|
|
1504
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as fh:
|
|
1505
|
+
text = fh.read()
|
|
1506
|
+
except (OSError, UnicodeError):
|
|
1507
|
+
continue
|
|
1508
|
+
if len(text) < min_chars:
|
|
1509
|
+
continue
|
|
1510
|
+
file_tokens = tokenizer.encode(text)
|
|
1511
|
+
batch.extend(file_tokens)
|
|
1512
|
+
total += len(file_tokens)
|
|
1513
|
+
processed += 1
|
|
1514
|
+
|
|
1515
|
+
if len(batch) >= self.CHUNK_TOKENS:
|
|
1516
|
+
arr = np.array(batch, dtype=np.int64)
|
|
1517
|
+
f.write(arr.tobytes())
|
|
1518
|
+
batch = []
|
|
1519
|
+
|
|
1520
|
+
if total >= max_tokens > 0:
|
|
1521
|
+
self.logger.info(
|
|
1522
|
+
f"Reached max_tokens ({max_tokens:,}) after {processed} files")
|
|
1523
|
+
break
|
|
1524
|
+
|
|
1525
|
+
if batch:
|
|
1526
|
+
arr = np.array(batch, dtype=np.int64)
|
|
1527
|
+
f.write(arr.tobytes())
|
|
1528
|
+
|
|
1529
|
+
total = os.path.getsize(cache_path) // 8
|
|
1530
|
+
with open(meta_path, 'w') as f:
|
|
1531
|
+
json.dump({
|
|
1532
|
+
'num_tokens': total,
|
|
1533
|
+
'cache_key': cache_key,
|
|
1534
|
+
'files_processed': processed,
|
|
1535
|
+
}, f)
|
|
1536
|
+
|
|
1537
|
+
self.logger.info(
|
|
1538
|
+
f"Tokenized {total:,} tokens from {processed} files, cached: {cache_path}")
|
|
1539
|
+
return np.memmap(cache_path, dtype=np.int64, mode='r', shape=(total,))
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class MappedTokenDataset(torch.utils.data.Dataset):
|
|
1543
|
+
"""Memory-mapped token dataset for sequence-to-sequence LM training.
|
|
1544
|
+
|
|
1545
|
+
The token array is backed by a numpy memmap file on disk.
|
|
1546
|
+
Only accessed slices are paged into RAM by the OS. The .copy()
|
|
1547
|
+
in __getitem__ ensures the returned torch.Tensor owns its memory,
|
|
1548
|
+
which is required for DataLoader collation with worker processes.
|
|
1549
|
+
"""
|
|
1550
|
+
|
|
1551
|
+
def __init__(self, tokens: np.ndarray, seq_len: int):
|
|
1552
|
+
self.tokens = tokens
|
|
1553
|
+
self.seq_len = seq_len
|
|
1554
|
+
self.n_seqs = (len(tokens) - 1) // seq_len
|
|
1555
|
+
|
|
1556
|
+
def __len__(self) -> int:
|
|
1557
|
+
return self.n_seqs
|
|
1558
|
+
|
|
1559
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1560
|
+
start = idx * self.seq_len
|
|
1561
|
+
x = torch.as_tensor(
|
|
1562
|
+
self.tokens[start:start + self.seq_len].copy(),
|
|
1563
|
+
dtype=torch.long,
|
|
1564
|
+
)
|
|
1565
|
+
y = torch.as_tensor(
|
|
1566
|
+
self.tokens[start + 1:start + self.seq_len + 1].copy(),
|
|
1567
|
+
dtype=torch.long,
|
|
1568
|
+
)
|
|
1569
|
+
return x, y
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
# ============================================================================
|
|
1573
|
+
# TEXT QUALITY FILTER
|
|
1574
|
+
# ============================================================================
|
|
1575
|
+
|
|
1576
|
+
class TextFilter:
|
|
1577
|
+
"""Filters low-quality files from the corpus based on multiple heuristics."""
|
|
1578
|
+
|
|
1579
|
+
def __init__(self, config: 'TopoGPT2Config', logger: logging.Logger):
|
|
1580
|
+
self.cfg = config
|
|
1581
|
+
self.logger = logger
|
|
1582
|
+
self._seen_hashes: set = set()
|
|
1583
|
+
self._stats = {
|
|
1584
|
+
'total': 0, 'entropy_low': 0, 'entropy_high': 0,
|
|
1585
|
+
'line_too_long': 0, 'special_token_high': 0, 'dup': 0, 'passed': 0,
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
def _compute_entropy(self, text: str) -> float:
|
|
1589
|
+
"""Shannon entropy of byte frequencies (bits per byte)."""
|
|
1590
|
+
if not text:
|
|
1591
|
+
return 0.0
|
|
1592
|
+
import collections
|
|
1593
|
+
freq = collections.Counter(text)
|
|
1594
|
+
total = len(text)
|
|
1595
|
+
entropy = 0.0
|
|
1596
|
+
for count in freq.values():
|
|
1597
|
+
p = count / total
|
|
1598
|
+
if p > 0:
|
|
1599
|
+
entropy -= p * math.log2(p)
|
|
1600
|
+
return entropy
|
|
1601
|
+
|
|
1602
|
+
def _has_long_lines(self, text: str, threshold: int) -> bool:
|
|
1603
|
+
"""Return True if any line exceeds threshold characters."""
|
|
1604
|
+
for line in text.split('\n'):
|
|
1605
|
+
if len(line) > threshold:
|
|
1606
|
+
return True
|
|
1607
|
+
return False
|
|
1608
|
+
|
|
1609
|
+
def _special_token_ratio(self, text: str, tokenizer: 'BPETokenizer') -> float:
|
|
1610
|
+
"""Fraction of tokens that are pure whitespace or indentation-only."""
|
|
1611
|
+
try:
|
|
1612
|
+
tokens = tokenizer.encode(text)
|
|
1613
|
+
except Exception:
|
|
1614
|
+
return 1.0
|
|
1615
|
+
if not tokens:
|
|
1616
|
+
return 1.0
|
|
1617
|
+
whitespace_toks = sum(
|
|
1618
|
+
1 for t in tokens
|
|
1619
|
+
if tokenizer.enc.decode([t]).strip() == ''
|
|
1620
|
+
)
|
|
1621
|
+
return whitespace_toks / len(tokens)
|
|
1622
|
+
|
|
1623
|
+
def _content_hash(self, text: str) -> str:
|
|
1624
|
+
return hashlib.sha256(text.encode('utf-8', errors='replace')).hexdigest()[:16]
|
|
1625
|
+
|
|
1626
|
+
def filter_file(self, path: str, tokenizer: Optional['BPETokenizer'] = None) -> Optional[str]:
|
|
1627
|
+
"""Read and evaluate a file. Returns text if passed, None if filtered."""
|
|
1628
|
+
self._stats['total'] += 1
|
|
1629
|
+
try:
|
|
1630
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
|
1631
|
+
text = f.read()
|
|
1632
|
+
except (OSError, UnicodeError):
|
|
1633
|
+
return None
|
|
1634
|
+
|
|
1635
|
+
if len(text) < self.cfg.CORPUS_MIN_FILE_CHARS:
|
|
1636
|
+
return None
|
|
1637
|
+
|
|
1638
|
+
entropy = self._compute_entropy(text)
|
|
1639
|
+
if entropy < self.cfg.FILTER_ENTROPY_MIN:
|
|
1640
|
+
self._stats['entropy_low'] += 1
|
|
1641
|
+
return None
|
|
1642
|
+
if entropy > self.cfg.FILTER_ENTROPY_MAX:
|
|
1643
|
+
self._stats['entropy_high'] += 1
|
|
1644
|
+
return None
|
|
1645
|
+
|
|
1646
|
+
if self._has_long_lines(text, self.cfg.FILTER_MAX_LINE_LEN):
|
|
1647
|
+
self._stats['line_too_long'] += 1
|
|
1648
|
+
return None
|
|
1649
|
+
|
|
1650
|
+
if tokenizer is not None and self.cfg.FILTER_SPECIAL_TOKEN_RATIO_MAX < 1.0:
|
|
1651
|
+
ratio = self._special_token_ratio(text, tokenizer)
|
|
1652
|
+
if ratio > self.cfg.FILTER_SPECIAL_TOKEN_RATIO_MAX:
|
|
1653
|
+
self._stats['special_token_high'] += 1
|
|
1654
|
+
return None
|
|
1655
|
+
|
|
1656
|
+
if self.cfg.FILTER_ENABLE_DEDUP:
|
|
1657
|
+
h = self._content_hash(text)
|
|
1658
|
+
if h in self._seen_hashes:
|
|
1659
|
+
self._stats['dup'] += 1
|
|
1660
|
+
return None
|
|
1661
|
+
self._seen_hashes.add(h)
|
|
1662
|
+
|
|
1663
|
+
self._stats['passed'] += 1
|
|
1664
|
+
return text
|
|
1665
|
+
|
|
1666
|
+
def report(self) -> Dict[str, int]:
|
|
1667
|
+
s = self._stats
|
|
1668
|
+
self.logger.info(
|
|
1669
|
+
f"TextFilter: total={s['total']} passed={s['passed']} "
|
|
1670
|
+
f"entropy_low={s['entropy_low']} entropy_high={s['entropy_high']} "
|
|
1671
|
+
f"long_line={s['line_too_long']} special_tok={s['special_token_high']} "
|
|
1672
|
+
f"dedup={s['dup']}")
|
|
1673
|
+
return dict(self._stats)
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
# ============================================================================
|
|
1677
|
+
# CURRICULUM DATASET
|
|
1678
|
+
# ============================================================================
|
|
1679
|
+
|
|
1680
|
+
class CurriculumDataset(torch.utils.data.Dataset):
|
|
1681
|
+
"""Tiered dataset that exposes short/medium/all files based on line count.
|
|
1682
|
+
|
|
1683
|
+
Works as a wrapper around MappedTokenDataset. Provides __getitem__ that
|
|
1684
|
+
only samples from the active tier, avoiding dataset duplication.
|
|
1685
|
+
"""
|
|
1686
|
+
|
|
1687
|
+
def __init__(self, tokens: np.ndarray, seq_len: int,
|
|
1688
|
+
file_tiers: Dict[int, List[int]],
|
|
1689
|
+
active_tier: int = 0,
|
|
1690
|
+
logger: Optional[logging.Logger] = None):
|
|
1691
|
+
self.base = MappedTokenDataset(tokens, seq_len)
|
|
1692
|
+
self.file_tiers = file_tiers
|
|
1693
|
+
self.active_tier = active_tier
|
|
1694
|
+
self.logger = logger or setup_logger('CurriculumDS')
|
|
1695
|
+
self._update_len()
|
|
1696
|
+
|
|
1697
|
+
def _update_len(self):
|
|
1698
|
+
tier_files = self.file_tiers.get(self.active_tier, [])
|
|
1699
|
+
self.n_seqs = len(tier_files) if tier_files else 0
|
|
1700
|
+
self.logger.info(
|
|
1701
|
+
f"Curriculum tier {self.active_tier}: {self.n_seqs} sequences")
|
|
1702
|
+
|
|
1703
|
+
def set_tier(self, tier: int):
|
|
1704
|
+
self.active_tier = tier
|
|
1705
|
+
self._update_len()
|
|
1706
|
+
|
|
1707
|
+
def __len__(self) -> int:
|
|
1708
|
+
return self.n_seqs
|
|
1709
|
+
|
|
1710
|
+
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1711
|
+
return self.base[idx]
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
# ============================================================================
|
|
1715
|
+
# CURRICULUM BUILDERS
|
|
1716
|
+
# ============================================================================
|
|
1717
|
+
|
|
1718
|
+
def build_file_tiers(paths: List[str], short: int, med: int) -> Dict[int, List[int]]:
|
|
1719
|
+
"""Classify file paths into complexity tiers by line count.
|
|
1720
|
+
|
|
1721
|
+
Returns dict: tier -> list of file indices in that tier.
|
|
1722
|
+
Tier 0 = short (<=short lines), tier 1 = medium, tier 2 = all.
|
|
1723
|
+
"""
|
|
1724
|
+
tiers: Dict[int, list] = {0: [], 1: [], 2: []}
|
|
1725
|
+
for i, path in enumerate(paths):
|
|
1726
|
+
try:
|
|
1727
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
|
1728
|
+
n_lines = sum(1 for _ in f)
|
|
1729
|
+
except OSError:
|
|
1730
|
+
n_lines = 0
|
|
1731
|
+
if n_lines <= short:
|
|
1732
|
+
tiers[0].append(i)
|
|
1733
|
+
elif n_lines <= med:
|
|
1734
|
+
tiers[1].append(i)
|
|
1735
|
+
else:
|
|
1736
|
+
tiers[2].append(i)
|
|
1737
|
+
return tiers
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
# ============================================================================
|
|
1741
|
+
# PROGRESSIVE SEQUENCE LENGTH TRAINER
|
|
1742
|
+
# ============================================================================
|
|
1743
|
+
|
|
1744
|
+
class ProgressiveSeqLenTrainer:
|
|
1745
|
+
"""Trainer that dynamically adjusts MAX_SEQ_LEN across training phases.
|
|
1746
|
+
|
|
1747
|
+
Phase schedule (configurable):
|
|
1748
|
+
phase 0: seq_len=128, epochs=3
|
|
1749
|
+
phase 1: seq_len=256, epochs=3
|
|
1750
|
+
phase 2: seq_len=512, epochs=4
|
|
1751
|
+
|
|
1752
|
+
Each phase rebuilds the DataLoader with the new sequence length.
|
|
1753
|
+
"""
|
|
1754
|
+
|
|
1755
|
+
def __init__(self, base_trainer: 'TopoGPT2Trainer'):
|
|
1756
|
+
self.trainer = base_trainer
|
|
1757
|
+
self.cfg = base_trainer.config
|
|
1758
|
+
self.logger = base_trainer.logger
|
|
1759
|
+
self.phase = 0
|
|
1760
|
+
|
|
1761
|
+
def _build_dataloader(self, dataset: MappedTokenDataset, seq_len: int,
|
|
1762
|
+
batch_size: int, is_train: bool) -> torch.utils.data.DataLoader:
|
|
1763
|
+
ds = MappedTokenDataset(dataset.tokens, seq_len)
|
|
1764
|
+
n_cpu = max(1, os.cpu_count() // 2)
|
|
1765
|
+
pin = 'cuda' in self.cfg.DEVICE
|
|
1766
|
+
return torch.utils.data.DataLoader(
|
|
1767
|
+
ds, batch_size=batch_size, shuffle=is_train,
|
|
1768
|
+
num_workers=n_cpu, pin_memory=pin, drop_last=is_train,
|
|
1769
|
+
)
|
|
1770
|
+
|
|
1771
|
+
def run(self, train_paths: List[str], val_paths: List[str],
|
|
1772
|
+
tokenizer: 'BPETokenizer',
|
|
1773
|
+
file_tiers: Optional[Dict[int, List[int]]] = None,
|
|
1774
|
+
phases: Optional[Tuple[Tuple[int, int], ...]] = None) -> Dict:
|
|
1775
|
+
"""Run training with progressive sequence length across phases."""
|
|
1776
|
+
if phases is None:
|
|
1777
|
+
phases = tuple(zip(
|
|
1778
|
+
self.cfg.PROGRESSIVE_SEQ_STEPS,
|
|
1779
|
+
self.cfg.PROGRESSIVE_SEQ_EPOCHS,
|
|
1780
|
+
))
|
|
1781
|
+
|
|
1782
|
+
self.logger.info(
|
|
1783
|
+
f"ProgressiveSeqLen: {len(phases)} phases, "
|
|
1784
|
+
f"{self.cfg.EPOCHS} total epochs")
|
|
1785
|
+
|
|
1786
|
+
for phase_idx, (seq_len, n_epochs) in enumerate(phases):
|
|
1787
|
+
self.logger.info(
|
|
1788
|
+
f"[Phase {phase_idx}] seq_len={seq_len}, epochs={n_epochs}")
|
|
1789
|
+
|
|
1790
|
+
self.cfg.MAX_SEQ_LEN = seq_len
|
|
1791
|
+
|
|
1792
|
+
train_ds = MappedTokenDataset(
|
|
1793
|
+
self.trainer._tokens_cache['train'], seq_len)
|
|
1794
|
+
val_ds = MappedTokenDataset(
|
|
1795
|
+
self.trainer._tokens_cache.get('val', self.trainer._tokens_cache['train']),
|
|
1796
|
+
seq_len)
|
|
1797
|
+
|
|
1798
|
+
n_cpu = max(1, os.cpu_count() // 2)
|
|
1799
|
+
pin = 'cuda' in self.cfg.DEVICE
|
|
1800
|
+
train_dl = torch.utils.data.DataLoader(
|
|
1801
|
+
train_ds, batch_size=self.cfg.BATCH_SIZE, shuffle=True,
|
|
1802
|
+
num_workers=n_cpu, pin_memory=pin, drop_last=True)
|
|
1803
|
+
val_dl = torch.utils.data.DataLoader(
|
|
1804
|
+
val_ds, batch_size=self.cfg.BATCH_SIZE * 2, shuffle=False,
|
|
1805
|
+
num_workers=max(1, n_cpu // 2), pin_memory=pin, drop_last=True)
|
|
1806
|
+
|
|
1807
|
+
prev_epochs = self.trainer.completed_epochs
|
|
1808
|
+
self.trainer.train(train_dl, val_dl)
|
|
1809
|
+
self.logger.info(
|
|
1810
|
+
f"[Phase {phase_idx}] done. Epochs completed: "
|
|
1811
|
+
f"{prev_epochs} -> {self.trainer.completed_epochs}")
|
|
1812
|
+
|
|
1813
|
+
return self.trainer.history
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
# ============================================================================
|
|
1817
|
+
# SPECULATIVE DECODING
|
|
1818
|
+
# ============================================================================
|
|
1819
|
+
|
|
1820
|
+
class SpeculativeDecoder:
|
|
1821
|
+
"""Speculative decoding with a small draft model.
|
|
1822
|
+
|
|
1823
|
+
Draft model uses SPEC_DECODE_DRAFT_SCALE (e.g. 'micro').
|
|
1824
|
+
The draft generates K tokens, then the target model verifies them
|
|
1825
|
+
in a single forward pass. Accepted tokens are kept; rejected ones
|
|
1826
|
+
trigger a fallback to the target model sampling.
|
|
1827
|
+
"""
|
|
1828
|
+
|
|
1829
|
+
def __init__(self, target_model: 'TopoGPT2', config: 'TopoGPT2Config',
|
|
1830
|
+
logger: logging.Logger):
|
|
1831
|
+
self.target = target_model
|
|
1832
|
+
self.cfg = config
|
|
1833
|
+
self.logger = logger
|
|
1834
|
+
self.draft: Optional[TopoGPT2] = None
|
|
1835
|
+
self._build_draft()
|
|
1836
|
+
|
|
1837
|
+
def _build_draft(self):
|
|
1838
|
+
draft_cfg = TopoGPT2Config(
|
|
1839
|
+
SCALE=self.cfg.SPEC_DECODE_DRAFT_SCALE,
|
|
1840
|
+
DEVICE=self.cfg.DEVICE,
|
|
1841
|
+
RANDOM_SEED=self.cfg.RANDOM_SEED,
|
|
1842
|
+
)
|
|
1843
|
+
draft_cfg.N_KV_HEADS = max(1, draft_cfg.N_HEADS // 4)
|
|
1844
|
+
self.draft = TopoGPT2(draft_cfg).to(self.cfg.DEVICE)
|
|
1845
|
+
self.draft.eval()
|
|
1846
|
+
self.logger.info(
|
|
1847
|
+
f"SpeculativeDecoder: draft scale={self.cfg.SPEC_DECODE_DRAFT_SCALE}, "
|
|
1848
|
+
f"params={self.draft.count_params()['total']:,}")
|
|
1849
|
+
|
|
1850
|
+
@torch.no_grad()
|
|
1851
|
+
def generate(self, token_ids: torch.Tensor,
|
|
1852
|
+
max_new_tokens: int = 200,
|
|
1853
|
+
temperature: float = 0.8,
|
|
1854
|
+
top_k: int = 50,
|
|
1855
|
+
repetition_penalty: float = 1.0) -> torch.Tensor:
|
|
1856
|
+
"""Autoregressive generation via speculative decoding.
|
|
1857
|
+
|
|
1858
|
+
Each round: draft generates K tokens, target verifies all K in
|
|
1859
|
+
one O(1) forward pass (longest context), then samples the first
|
|
1860
|
+
rejection from the target.
|
|
1861
|
+
"""
|
|
1862
|
+
self.target.eval()
|
|
1863
|
+
self.draft.eval()
|
|
1864
|
+
cfg = self.cfg
|
|
1865
|
+
|
|
1866
|
+
ctx = token_ids[:, -cfg.MAX_SEQ_LEN:]
|
|
1867
|
+
logits, _, past_kvs = self.target(ctx)
|
|
1868
|
+
logits = logits[:, -1, :] / max(temperature, 1e-8)
|
|
1869
|
+
if repetition_penalty != 1.0:
|
|
1870
|
+
mask = torch.zeros_like(logits, dtype=torch.bool)
|
|
1871
|
+
mask.scatter_(1, ctx, True)
|
|
1872
|
+
logits = torch.where(mask, logits / repetition_penalty, logits)
|
|
1873
|
+
if top_k > 0:
|
|
1874
|
+
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
|
|
1875
|
+
logits[logits < v[:, -1:]] = float('-inf')
|
|
1876
|
+
next_tok = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
|
1877
|
+
token_ids = torch.cat([token_ids, next_tok], dim=1)
|
|
1878
|
+
|
|
1879
|
+
generated = [next_tok.item()]
|
|
1880
|
+
draft_past_kvs = None
|
|
1881
|
+
|
|
1882
|
+
while len(generated) < max_new_tokens:
|
|
1883
|
+
k = min(cfg.SPEC_DECODE_K, max_new_tokens - len(generated))
|
|
1884
|
+
|
|
1885
|
+
draft_ctx = token_ids
|
|
1886
|
+
if draft_past_kvs is not None:
|
|
1887
|
+
draft_logits, _, draft_past_kvs = self.draft(
|
|
1888
|
+
token_ids[:, -self.draft.config.MAX_SEQ_LEN:],
|
|
1889
|
+
past_kvs=draft_past_kvs)
|
|
1890
|
+
else:
|
|
1891
|
+
draft_logits, _, draft_past_kvs = self.draft(draft_ctx)
|
|
1892
|
+
draft_tok = torch.multinomial(
|
|
1893
|
+
F.softmax(draft_logits[:, -1, :] / max(temperature, 1e-8), dim=-1), k)
|
|
1894
|
+
|
|
1895
|
+
accepted = []
|
|
1896
|
+
for t in range(k):
|
|
1897
|
+
d_tok = draft_tok[:, t:t+1]
|
|
1898
|
+
target_logits, _, past_kvs = self.target(
|
|
1899
|
+
d_tok, past_kvs=past_kvs)
|
|
1900
|
+
t_logits = target_logits[:, -1, :] / max(temperature, 1e-8)
|
|
1901
|
+
if repetition_penalty != 1.0:
|
|
1902
|
+
mask = torch.zeros_like(t_logits, dtype=torch.bool)
|
|
1903
|
+
mask.scatter_(1, token_ids, True)
|
|
1904
|
+
t_logits = torch.where(mask, t_logits / repetition_penalty, t_logits)
|
|
1905
|
+
prob = F.softmax(t_logits, dim=-1)
|
|
1906
|
+
if top_k > 0:
|
|
1907
|
+
v, _ = torch.topk(t_logits, min(top_k, t_logits.size(-1)))
|
|
1908
|
+
mask_k = t_logits < v[:, -1:]
|
|
1909
|
+
logits_topk = t_logits.clone()
|
|
1910
|
+
logits_topk[mask_k] = float('-inf')
|
|
1911
|
+
prob = F.softmax(logits_topk, dim=-1)
|
|
1912
|
+
target_tok = torch.multinomial(prob, 1)
|
|
1913
|
+
if target_tok.item() == d_tok.item():
|
|
1914
|
+
accepted.append(d_tok.item())
|
|
1915
|
+
token_ids = torch.cat([token_ids, d_tok], dim=1)
|
|
1916
|
+
else:
|
|
1917
|
+
token_ids = torch.cat([token_ids, target_tok], dim=1)
|
|
1918
|
+
generated.extend(accepted)
|
|
1919
|
+
break
|
|
1920
|
+
else:
|
|
1921
|
+
for t in range(k):
|
|
1922
|
+
token_ids = torch.cat([token_ids, draft_tok[:, t:t+1]], dim=1)
|
|
1923
|
+
generated.extend([draft_tok[:, t:t+1].item() for t in range(k)])
|
|
1924
|
+
|
|
1925
|
+
if (token_ids[:, -1:] == 50256).all():
|
|
1926
|
+
break
|
|
1927
|
+
|
|
1928
|
+
return token_ids
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
# ============================================================================
|
|
1932
|
+
# QUANTIZED EMBEDDING FOR INFERENCE
|
|
1933
|
+
# ============================================================================
|
|
1934
|
+
|
|
1935
|
+
class QuantizedEmbedding(nn.Module):
|
|
1936
|
+
"""Wrapper around nn.Embedding that applies dynamic quantization.
|
|
1937
|
+
|
|
1938
|
+
Applies int8 quantization to the embedding weight matrix after loading.
|
|
1939
|
+
Supports both embed (int8) and FFN (int4) quantization modes.
|
|
1940
|
+
"""
|
|
1941
|
+
|
|
1942
|
+
def __init__(self, embed: nn.Embedding, mode: str = 'int8'):
|
|
1943
|
+
super().__init__()
|
|
1944
|
+
self.num_embeddings = embed.num_embeddings
|
|
1945
|
+
self.embedding_dim = embed.embedding_dim
|
|
1946
|
+
self.padding_idx = embed.padding_idx
|
|
1947
|
+
self.max_norm = embed.max_norm
|
|
1948
|
+
self.norm_type = embed.norm_type
|
|
1949
|
+
self.scale_grad_by_freq = embed.scale_grad_by_freq
|
|
1950
|
+
self.sparse = embed.sparse
|
|
1951
|
+
|
|
1952
|
+
if mode == 'int8':
|
|
1953
|
+
self.weight = torch.quantize_per_channel(
|
|
1954
|
+
embed.weight.data.to(torch.float32), # fp32 for quantize_per_channel
|
|
1955
|
+
scales=torch.ones(embed.weight.shape[0]),
|
|
1956
|
+
zero_points=torch.zeros(embed.weight.shape[0], dtype=torch.long),
|
|
1957
|
+
axis=0,
|
|
1958
|
+
dtype=torch.qint8,
|
|
1959
|
+
)
|
|
1960
|
+
elif mode == 'int4':
|
|
1961
|
+
self.weight = torch.quantize_per_channel(
|
|
1962
|
+
embed.weight.data.to(torch.float32),
|
|
1963
|
+
scales=torch.ones(embed.weight.shape[0]),
|
|
1964
|
+
zero_points=torch.zeros(embed.weight.shape[0], dtype=torch.long),
|
|
1965
|
+
axis=0,
|
|
1966
|
+
dtype=torch.qint8,
|
|
1967
|
+
)
|
|
1968
|
+
else:
|
|
1969
|
+
self.weight = embed.weight
|
|
1970
|
+
|
|
1971
|
+
def forward(self, indices: torch.Tensor) -> torch.Tensor:
|
|
1972
|
+
return torch.embedding(self.weight, indices, padding_idx=self.padding_idx)
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def apply_quantization(model: 'TopoGPT2', config: 'TopoGPT2Config') -> 'TopoGPT2':
|
|
1976
|
+
"""Quantize embedding and lm_head layers for reduced VRAM usage."""
|
|
1977
|
+
if not config.QUANTIZE_EMBEDDINGS:
|
|
1978
|
+
return model
|
|
1979
|
+
|
|
1980
|
+
logger = setup_logger('Quantize')
|
|
1981
|
+
device = config.DEVICE
|
|
1982
|
+
|
|
1983
|
+
embed = model.token_embed
|
|
1984
|
+
quantized_embed = QuantizedEmbedding(embed, mode='int8')
|
|
1985
|
+
model.token_embed = quantized_embed
|
|
1986
|
+
model.lm_head = nn.Linear(
|
|
1987
|
+
model.lm_head.in_features, model.lm_head.out_features, bias=False)
|
|
1988
|
+
model.lm_head.weight = quantized_embed.weight
|
|
1989
|
+
|
|
1990
|
+
logger.info(
|
|
1991
|
+
f"Quantized embeddings to int8: shape={quantized_embed.weight.shape}")
|
|
1992
|
+
|
|
1993
|
+
if config.QUANTIZE_FFN:
|
|
1994
|
+
logger.warning("FFN int4 quantization is experimental; skipping.")
|
|
1995
|
+
|
|
1996
|
+
return model
|
|
1997
|
+
|
|
1998
|
+
|
|
1999
|
+
# ============================================================================
|
|
2000
|
+
# CURRICULUM TRAINER WRAPPER
|
|
2001
|
+
# ============================================================================
|
|
2002
|
+
|
|
2003
|
+
class CurriculumTrainer:
|
|
2004
|
+
"""Extends TopoGPT2Trainer with curriculum + progressive seq len support.
|
|
2005
|
+
|
|
2006
|
+
Provides:
|
|
2007
|
+
- Tokens cache for progressive sequence length rebuilding
|
|
2008
|
+
- Curriculum dataset wrapping (short / medium / all tiers)
|
|
2009
|
+
"""
|
|
2010
|
+
|
|
2011
|
+
def __init__(self, model, config, tokenizer=None):
|
|
2012
|
+
self._base = TopoGPT2Trainer(model, config, tokenizer)
|
|
2013
|
+
self.cfg = config
|
|
2014
|
+
self.logger = self._base.logger
|
|
2015
|
+
self._tokens_cache: Dict[str, np.ndarray] = {}
|
|
2016
|
+
self._curriculum_ds: Optional[CurriculumDataset] = None
|
|
2017
|
+
|
|
2018
|
+
def cache_tokens(self, key: str, tokens: np.ndarray):
|
|
2019
|
+
self._tokens_cache[key] = tokens
|
|
2020
|
+
|
|
2021
|
+
@property
|
|
2022
|
+
def model(self):
|
|
2023
|
+
return self._base.model
|
|
2024
|
+
|
|
2025
|
+
@property
|
|
2026
|
+
def optimizer(self):
|
|
2027
|
+
return self._base.optimizer
|
|
2028
|
+
|
|
2029
|
+
@property
|
|
2030
|
+
def scaler(self):
|
|
2031
|
+
return self._base.scaler
|
|
2032
|
+
|
|
2033
|
+
@property
|
|
2034
|
+
def amp_dtype(self):
|
|
2035
|
+
return self._base.amp_dtype
|
|
2036
|
+
|
|
2037
|
+
@property
|
|
2038
|
+
def completed_epochs(self):
|
|
2039
|
+
return self._base.completed_epochs
|
|
2040
|
+
|
|
2041
|
+
@completed_epochs.setter
|
|
2042
|
+
def completed_epochs(self, v):
|
|
2043
|
+
self._base.completed_epochs = v
|
|
2044
|
+
|
|
2045
|
+
@property
|
|
2046
|
+
def global_step(self):
|
|
2047
|
+
return self._base.global_step
|
|
2048
|
+
|
|
2049
|
+
@global_step.setter
|
|
2050
|
+
def global_step(self, v):
|
|
2051
|
+
self._base.global_step = v
|
|
2052
|
+
|
|
2053
|
+
@property
|
|
2054
|
+
def best_val_loss(self):
|
|
2055
|
+
return self._base.best_val_loss
|
|
2056
|
+
|
|
2057
|
+
@best_val_loss.setter
|
|
2058
|
+
def best_val_loss(self, v):
|
|
2059
|
+
self._base.best_val_loss = v
|
|
2060
|
+
|
|
2061
|
+
@property
|
|
2062
|
+
def history(self):
|
|
2063
|
+
return self._base.history
|
|
2064
|
+
|
|
2065
|
+
@property
|
|
2066
|
+
def ckpt_mgr(self):
|
|
2067
|
+
return self._base.ckpt_mgr
|
|
2068
|
+
|
|
2069
|
+
def resume(self) -> bool:
|
|
2070
|
+
return self._base.resume()
|
|
2071
|
+
|
|
2072
|
+
def _current_state(self) -> Dict:
|
|
2073
|
+
return self._base._current_state()
|
|
2074
|
+
|
|
2075
|
+
def _cosine_lr(self, *args, **kwargs):
|
|
2076
|
+
return self._base._cosine_lr(*args, **kwargs)
|
|
2077
|
+
|
|
2078
|
+
def _set_lr(self, *args, **kwargs):
|
|
2079
|
+
self._base._set_lr(*args, **kwargs)
|
|
2080
|
+
|
|
2081
|
+
def evaluate(self, dataloader):
|
|
2082
|
+
return self._base.evaluate(dataloader)
|
|
2083
|
+
|
|
2084
|
+
def _sample_text(self, *args, **kwargs):
|
|
2085
|
+
self._base._sample_text(*args, **kwargs)
|
|
2086
|
+
|
|
2087
|
+
def _progressive_train(self, train_paths: List[str], val_paths: List[str],
|
|
2088
|
+
tokenizer: 'BPETokenizer',
|
|
2089
|
+
phases: Tuple[Tuple[int, int], ...],
|
|
2090
|
+
memtok: 'MemmapTokenizer') -> Dict:
|
|
2091
|
+
"""Training loop with progressive sequence length across phases."""
|
|
2092
|
+
cfg = self.cfg
|
|
2093
|
+
device = cfg.DEVICE
|
|
2094
|
+
logger = self.logger
|
|
2095
|
+
|
|
2096
|
+
for phase_idx, (seq_len, n_epochs) in enumerate(phases):
|
|
2097
|
+
logger.info(
|
|
2098
|
+
f"[ProgressiveSeqLen Phase {phase_idx}] "
|
|
2099
|
+
f"seq_len={seq_len}, epochs={n_epochs}")
|
|
2100
|
+
|
|
2101
|
+
cfg.MAX_SEQ_LEN = seq_len
|
|
2102
|
+
|
|
2103
|
+
train_ds = MappedTokenDataset(
|
|
2104
|
+
self._tokens_cache['train'], seq_len)
|
|
2105
|
+
val_ds = MappedTokenDataset(
|
|
2106
|
+
self._tokens_cache.get('val', self._tokens_cache['train']),
|
|
2107
|
+
seq_len)
|
|
2108
|
+
|
|
2109
|
+
n_cpu = max(1, os.cpu_count() // 2)
|
|
2110
|
+
pin = 'cuda' in device
|
|
2111
|
+
train_dl = torch.utils.data.DataLoader(
|
|
2112
|
+
train_ds, batch_size=cfg.BATCH_SIZE, shuffle=True,
|
|
2113
|
+
num_workers=n_cpu, pin_memory=pin, drop_last=True)
|
|
2114
|
+
val_dl = torch.utils.data.DataLoader(
|
|
2115
|
+
val_ds, batch_size=cfg.BATCH_SIZE * 2, shuffle=False,
|
|
2116
|
+
num_workers=max(1, n_cpu // 2), pin_memory=pin, drop_last=True)
|
|
2117
|
+
|
|
2118
|
+
prev_completed = self.completed_epochs
|
|
2119
|
+
self._base.train(train_dl, val_dl)
|
|
2120
|
+
logger.info(
|
|
2121
|
+
f"[Phase {phase_idx}] done. "
|
|
2122
|
+
f"Epochs: {prev_completed} -> {self.completed_epochs}")
|
|
2123
|
+
|
|
2124
|
+
return self.history
|
|
2125
|
+
|
|
2126
|
+
def train(self, train_dl, val_dl):
|
|
2127
|
+
return self._base.train(train_dl, val_dl)
|
|
2128
|
+
|
|
2129
|
+
def run_curriculum(self, train_paths: List[str], val_paths: List[str],
|
|
2130
|
+
tokenizer: 'BPETokenizer',
|
|
2131
|
+
phases: Optional[Tuple[Tuple[int, int], ...]] = None):
|
|
2132
|
+
"""Top-level entry point: curriculum + progressive seq len."""
|
|
2133
|
+
if phases is None:
|
|
2134
|
+
phases = tuple(zip(
|
|
2135
|
+
self.cfg.PROGRESSIVE_SEQ_STEPS,
|
|
2136
|
+
self.cfg.PROGRESSIVE_SEQ_EPOCHS,
|
|
2137
|
+
))
|
|
2138
|
+
return self._progressive_train(
|
|
2139
|
+
train_paths, val_paths, tokenizer, phases,
|
|
2140
|
+
MemmapTokenizer(self.cfg.DATA_DIR, self.logger))
|
|
2141
|
+
|
|
2142
|
+
|
|
2143
|
+
def _tokenize_text_to_memmap(text: str, tokenizer: 'BPETokenizer',
|
|
2144
|
+
path: str, max_tokens: int) -> np.ndarray:
|
|
2145
|
+
"""Tokenize a single text string and write tokens to disk as raw int64."""
|
|
2146
|
+
tokens = np.array(tokenizer.encode(text)[:max_tokens], dtype=np.int64)
|
|
2147
|
+
tokens.tofile(path)
|
|
2148
|
+
return np.memmap(path, dtype=np.int64, mode='r', shape=(len(tokens),))
|
|
2149
|
+
|
|
2150
|
+
|
|
2151
|
+
# ============================================================================
|
|
2152
|
+
# CHECKPOINT MANAGER (safetensors + JSON + optimizer.pt)
|
|
2153
|
+
# ============================================================================
|
|
2154
|
+
|
|
2155
|
+
class CheckpointManager:
|
|
2156
|
+
"""
|
|
2157
|
+
Gestiona checkpoints de forma acumulativa y segura.
|
|
2158
|
+
|
|
2159
|
+
Estructura en disco:
|
|
2160
|
+
checkpoints_topogpt2/
|
|
2161
|
+
latest/
|
|
2162
|
+
model.safetensors <- pesos del modelo (formato seguro, sin pickle)
|
|
2163
|
+
optimizer.pt <- estado del optimizador (requiere .pt)
|
|
2164
|
+
state.json <- metadatos: epoch, step, historial, config
|
|
2165
|
+
best/
|
|
2166
|
+
model.safetensors
|
|
2167
|
+
state.json
|
|
2168
|
+
step_NNNNN/ <- snapshots periodicos (rotados)
|
|
2169
|
+
model.safetensors
|
|
2170
|
+
optimizer.pt
|
|
2171
|
+
state.json
|
|
2172
|
+
|
|
2173
|
+
El historial se ACUMULA entre sesiones de entrenamiento: cada --resume
|
|
2174
|
+
agrega nuevas entradas a train_loss[], val_loss[], etc.
|
|
2175
|
+
"""
|
|
2176
|
+
|
|
2177
|
+
LATEST = 'latest'
|
|
2178
|
+
BEST = 'best'
|
|
2179
|
+
|
|
2180
|
+
def __init__(self, config: TopoGPT2Config, logger: logging.Logger):
|
|
2181
|
+
self.root = config.CHECKPOINT_DIR
|
|
2182
|
+
self.max_snapshots = config.MAX_CHECKPOINTS
|
|
2183
|
+
self.interval_min = config.CHECKPOINT_INTERVAL_MINUTES
|
|
2184
|
+
self.logger = logger
|
|
2185
|
+
self._last_save_time = time.time()
|
|
2186
|
+
self._snapshot_dirs: List[str] = []
|
|
2187
|
+
for sub in (self.LATEST, self.BEST):
|
|
2188
|
+
os.makedirs(os.path.join(self.root, sub), exist_ok=True)
|
|
2189
|
+
|
|
2190
|
+
def patch_config_for_resume(self, cfg: 'TopoGPT2Config') -> None:
|
|
2191
|
+
"""
|
|
2192
|
+
Lee el checkpoint 'latest' y ajusta cfg.N_KV_HEADS / cfg.GQA_GROUPS
|
|
2193
|
+
para que coincidan con la arquitectura guardada.
|
|
2194
|
+
Necesario cuando el codigo cambio GQA despues de guardar el checkpoint.
|
|
2195
|
+
"""
|
|
2196
|
+
model_path = os.path.join(self.root, self.LATEST, 'model.safetensors')
|
|
2197
|
+
if not os.path.exists(model_path):
|
|
2198
|
+
return
|
|
2199
|
+
sd = st_load(model_path, device='cpu')
|
|
2200
|
+
key = 'layers.0.attn.k_proj.weight'
|
|
2201
|
+
if key in sd:
|
|
2202
|
+
ckpt_kv_dim = sd[key].shape[0]
|
|
2203
|
+
d_head = cfg.D_MODEL // cfg.N_HEADS
|
|
2204
|
+
ckpt_n_kv = ckpt_kv_dim // d_head
|
|
2205
|
+
if ckpt_n_kv != cfg.N_KV_HEADS:
|
|
2206
|
+
self.logger.info(
|
|
2207
|
+
f"Checkpoint usa N_KV_HEADS={ckpt_n_kv} "
|
|
2208
|
+
f"(config actual: {cfg.N_KV_HEADS}). "
|
|
2209
|
+
"Ajustando para compatibilidad con el checkpoint."
|
|
2210
|
+
)
|
|
2211
|
+
cfg.N_KV_HEADS = ckpt_n_kv
|
|
2212
|
+
cfg.GQA_GROUPS = cfg.N_HEADS // ckpt_n_kv
|
|
2213
|
+
del sd
|
|
2214
|
+
|
|
2215
|
+
# ------------------------------------------------------------------
|
|
2216
|
+
# Persistencia de pesos: safetensors (sin pickle, sin ejecucion de codigo)
|
|
2217
|
+
# ------------------------------------------------------------------
|
|
2218
|
+
|
|
2219
|
+
def _save_model(self, model: 'TopoGPT2', directory: str) -> None:
|
|
2220
|
+
path = os.path.join(directory, 'model.safetensors')
|
|
2221
|
+
# El weight-tying hace que lm_head.weight y token_embed.weight sean el
|
|
2222
|
+
# mismo tensor en memoria. safetensors rechaza tensores compartidos,
|
|
2223
|
+
# por lo que excluimos lm_head.weight al guardar; al cargar el modelo
|
|
2224
|
+
# el weight-tying lo restaura automaticamente desde token_embed.weight.
|
|
2225
|
+
sd = {
|
|
2226
|
+
k: v.contiguous().cpu()
|
|
2227
|
+
for k, v in model.state_dict().items()
|
|
2228
|
+
if k != 'lm_head.weight'
|
|
2229
|
+
}
|
|
2230
|
+
st_save(sd, path)
|
|
2231
|
+
|
|
2232
|
+
def _load_model(self, model: 'TopoGPT2', directory: str) -> None:
|
|
2233
|
+
path = os.path.join(directory, 'model.safetensors')
|
|
2234
|
+
sd = st_load(path, device=model.config.DEVICE)
|
|
2235
|
+
# strict=False porque lm_head.weight no esta en el archivo
|
|
2236
|
+
# (se reconstruye via weight-tying con token_embed.weight)
|
|
2237
|
+
missing, unexpected = model.load_state_dict(sd, strict=False)
|
|
2238
|
+
# lm_head.weight se reconstruye via weight-tying, ignorar siempre
|
|
2239
|
+
unexpected_real = [k for k in unexpected if k != 'lm_head.weight']
|
|
2240
|
+
missing_real = [k for k in missing if k != 'lm_head.weight']
|
|
2241
|
+
if unexpected_real:
|
|
2242
|
+
# Claves del checkpoint que ya no existen en el modelo (modulos eliminados).
|
|
2243
|
+
# Con strict=False esos pesos simplemente no se cargan; solo advertimos.
|
|
2244
|
+
modules = sorted({k.split('.')[2] if k.count('.') >= 2 else k
|
|
2245
|
+
for k in unexpected_real})
|
|
2246
|
+
self.logger.warning(
|
|
2247
|
+
f"Checkpoint tiene {len(unexpected_real)} claves no presentes en el "
|
|
2248
|
+
f"modelo actual (modulos eliminados: {modules}). Se ignoran."
|
|
2249
|
+
)
|
|
2250
|
+
if missing_real:
|
|
2251
|
+
# Claves del modelo que no estaban en el checkpoint (modulos nuevos).
|
|
2252
|
+
modules = sorted({k.split('.')[2] if k.count('.') >= 2 else k
|
|
2253
|
+
for k in missing_real})
|
|
2254
|
+
self.logger.warning(
|
|
2255
|
+
f"Modelo tiene {len(missing_real)} claves sin checkpoint "
|
|
2256
|
+
f"(modulos nuevos: {modules}). Se inicializan aleatoriamente."
|
|
2257
|
+
)
|
|
2258
|
+
|
|
2259
|
+
# ------------------------------------------------------------------
|
|
2260
|
+
# Persistencia del optimizador: torch.save (estado con tipos mixtos)
|
|
2261
|
+
# ------------------------------------------------------------------
|
|
2262
|
+
|
|
2263
|
+
def _save_optimizer(self, optimizer, directory: str) -> None:
|
|
2264
|
+
torch.save(optimizer.state_dict(), os.path.join(directory, 'optimizer.pt'))
|
|
2265
|
+
|
|
2266
|
+
def _load_optimizer(self, optimizer, directory: str, device: str) -> None:
|
|
2267
|
+
path = os.path.join(directory, 'optimizer.pt')
|
|
2268
|
+
sd = torch.load(path, map_location=device, weights_only=True)
|
|
2269
|
+
optimizer.load_state_dict(sd)
|
|
2270
|
+
|
|
2271
|
+
# ------------------------------------------------------------------
|
|
2272
|
+
# Metadatos en JSON (legible por humanos, sin ejecucion de codigo)
|
|
2273
|
+
# ------------------------------------------------------------------
|
|
2274
|
+
|
|
2275
|
+
def _save_state(self, state: Dict, directory: str) -> None:
|
|
2276
|
+
path = os.path.join(directory, 'state.json')
|
|
2277
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
2278
|
+
json.dump(state, f, indent=2)
|
|
2279
|
+
|
|
2280
|
+
def _load_state(self, directory: str) -> Dict:
|
|
2281
|
+
path = os.path.join(directory, 'state.json')
|
|
2282
|
+
if not os.path.exists(path):
|
|
2283
|
+
return {}
|
|
2284
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
2285
|
+
return json.load(f)
|
|
2286
|
+
|
|
2287
|
+
# ------------------------------------------------------------------
|
|
2288
|
+
# API publica
|
|
2289
|
+
# ------------------------------------------------------------------
|
|
2290
|
+
|
|
2291
|
+
def should_save(self) -> bool:
|
|
2292
|
+
return (time.time() - self._last_save_time) / 60 >= self.interval_min
|
|
2293
|
+
|
|
2294
|
+
def save(self, model: 'TopoGPT2', optimizer,
|
|
2295
|
+
state: Dict, is_best: bool = False) -> str:
|
|
2296
|
+
"""
|
|
2297
|
+
Guarda checkpoint completo.
|
|
2298
|
+
|
|
2299
|
+
state debe contener al menos: completed_epochs, global_step,
|
|
2300
|
+
best_val_loss, history, config.
|
|
2301
|
+
"""
|
|
2302
|
+
# Siempre actualizar 'latest'
|
|
2303
|
+
latest_dir = os.path.join(self.root, self.LATEST)
|
|
2304
|
+
self._save_model(model, latest_dir)
|
|
2305
|
+
self._save_optimizer(optimizer, latest_dir)
|
|
2306
|
+
self._save_state(state, latest_dir)
|
|
2307
|
+
|
|
2308
|
+
# Guardar 'best' si mejoro la perdida de validacion
|
|
2309
|
+
if is_best:
|
|
2310
|
+
best_dir = os.path.join(self.root, self.BEST)
|
|
2311
|
+
self._save_model(model, best_dir)
|
|
2312
|
+
self._save_state(state, best_dir)
|
|
2313
|
+
self.logger.info(
|
|
2314
|
+
f"Nuevo mejor modelo guardado "
|
|
2315
|
+
f"(val_loss={state.get('best_val_loss', '?'):.4f})"
|
|
2316
|
+
)
|
|
2317
|
+
|
|
2318
|
+
# Snapshot periodico con rotacion
|
|
2319
|
+
step = state.get('global_step', 0)
|
|
2320
|
+
snap_dir = os.path.join(self.root, f'step_{step:07d}')
|
|
2321
|
+
os.makedirs(snap_dir, exist_ok=True)
|
|
2322
|
+
self._save_model(model, snap_dir)
|
|
2323
|
+
self._save_optimizer(optimizer, snap_dir)
|
|
2324
|
+
self._save_state(state, snap_dir)
|
|
2325
|
+
self._snapshot_dirs.append(snap_dir)
|
|
2326
|
+
|
|
2327
|
+
# Eliminar snapshots antiguos manteniendo solo max_snapshots
|
|
2328
|
+
while len(self._snapshot_dirs) > self.max_snapshots:
|
|
2329
|
+
old = self._snapshot_dirs.pop(0)
|
|
2330
|
+
import shutil
|
|
2331
|
+
if os.path.isdir(old):
|
|
2332
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
2333
|
+
|
|
2334
|
+
self._last_save_time = time.time()
|
|
2335
|
+
self.logger.info(f"Checkpoint guardado: epoch={state.get('completed_epochs')}, "
|
|
2336
|
+
f"step={step}")
|
|
2337
|
+
return snap_dir
|
|
2338
|
+
|
|
2339
|
+
def load_latest(self, model: 'TopoGPT2', optimizer) -> Dict:
|
|
2340
|
+
"""
|
|
2341
|
+
Carga el ultimo checkpoint guardado.
|
|
2342
|
+
Devuelve el state dict (vacio si no hay checkpoint).
|
|
2343
|
+
"""
|
|
2344
|
+
latest_dir = os.path.join(self.root, self.LATEST)
|
|
2345
|
+
model_path = os.path.join(latest_dir, 'model.safetensors')
|
|
2346
|
+
if not os.path.exists(model_path):
|
|
2347
|
+
self.logger.info("No se encontro checkpoint previo, iniciando desde cero.")
|
|
2348
|
+
return {}
|
|
2349
|
+
|
|
2350
|
+
self._load_model(model, latest_dir)
|
|
2351
|
+
opt_path = os.path.join(latest_dir, 'optimizer.pt')
|
|
2352
|
+
if os.path.exists(opt_path):
|
|
2353
|
+
try:
|
|
2354
|
+
self._load_optimizer(optimizer, latest_dir, model.config.DEVICE)
|
|
2355
|
+
except Exception as e:
|
|
2356
|
+
self.logger.warning(f"No se pudo cargar estado del optimizador: {e}")
|
|
2357
|
+
|
|
2358
|
+
state = self._load_state(latest_dir)
|
|
2359
|
+
self.logger.info(
|
|
2360
|
+
f"Checkpoint cargado: epoch={state.get('completed_epochs', 0)}, "
|
|
2361
|
+
f"step={state.get('global_step', 0)}, "
|
|
2362
|
+
f"best_val_loss={state.get('best_val_loss', float('inf')):.4f}"
|
|
2363
|
+
)
|
|
2364
|
+
return state
|
|
2365
|
+
|
|
2366
|
+
def load_best(self, model: 'TopoGPT2') -> Dict:
|
|
2367
|
+
"""Carga el mejor modelo guardado (solo pesos, sin optimizador)."""
|
|
2368
|
+
best_dir = os.path.join(self.root, self.BEST)
|
|
2369
|
+
model_path = os.path.join(best_dir, 'model.safetensors')
|
|
2370
|
+
if not os.path.exists(model_path):
|
|
2371
|
+
self.logger.warning("No hay checkpoint 'best' disponible.")
|
|
2372
|
+
return {}
|
|
2373
|
+
self._load_model(model, best_dir)
|
|
2374
|
+
state = self._load_state(best_dir)
|
|
2375
|
+
self.logger.info(f"Mejor modelo cargado: val_loss={state.get('best_val_loss', '?'):.4f}")
|
|
2376
|
+
return state
|
|
2377
|
+
|
|
2378
|
+
def has_checkpoint(self) -> bool:
|
|
2379
|
+
return os.path.exists(
|
|
2380
|
+
os.path.join(self.root, self.LATEST, 'model.safetensors')
|
|
2381
|
+
)
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
# ============================================================================
|
|
2385
|
+
# ENTRENADOR ACUMULATIVO
|
|
2386
|
+
# ============================================================================
|
|
2387
|
+
|
|
2388
|
+
class TopoGPT2Trainer:
|
|
2389
|
+
"""
|
|
2390
|
+
Entrenador acumulativo y resumible.
|
|
2391
|
+
|
|
2392
|
+
Caracteristicas:
|
|
2393
|
+
- Checkpoint automatico en safetensors cada N minutos + cada epoch
|
|
2394
|
+
- Historial acumulativo entre sesiones (--resume agrega al historial existente)
|
|
2395
|
+
- Guarda el mejor modelo en checkpoints/best/ automaticamente
|
|
2396
|
+
- LR schedule: cosine con warmup relativo a los steps de ESTA sesion
|
|
2397
|
+
- Mixed Precision (AMP) + acumulacion de gradientes
|
|
2398
|
+
"""
|
|
2399
|
+
|
|
2400
|
+
def __init__(self, model: 'TopoGPT2', config: TopoGPT2Config,
|
|
2401
|
+
tokenizer: Optional['BPETokenizer'] = None):
|
|
2402
|
+
self.model = model.to(config.DEVICE)
|
|
2403
|
+
self.config = config
|
|
2404
|
+
self.tokenizer = tokenizer # para generacion de muestra por epoch
|
|
2405
|
+
self.logger = setup_logger('Trainer', config.LOG_LEVEL)
|
|
2406
|
+
self.ckpt_mgr = CheckpointManager(config, self.logger)
|
|
2407
|
+
|
|
2408
|
+
self.optimizer = torch.optim.AdamW(
|
|
2409
|
+
model.parameters(),
|
|
2410
|
+
lr=config.LEARNING_RATE,
|
|
2411
|
+
weight_decay=config.WEIGHT_DECAY,
|
|
2412
|
+
betas=(0.9, 0.95),
|
|
2413
|
+
)
|
|
2414
|
+
|
|
2415
|
+
dev = config.DEVICE
|
|
2416
|
+
self.scaler = torch.amp.GradScaler(
|
|
2417
|
+
dev.split(':')[0],
|
|
2418
|
+
enabled=config.USE_AMP and 'cuda' in dev,
|
|
2419
|
+
)
|
|
2420
|
+
self.amp_dtype = torch.float16 if 'cuda' in dev else torch.bfloat16
|
|
2421
|
+
|
|
2422
|
+
# Estado de entrenamiento (se carga desde checkpoint si se hace --resume)
|
|
2423
|
+
self.completed_epochs: int = 0
|
|
2424
|
+
self.global_step: int = 0
|
|
2425
|
+
self.best_val_loss: float = float('inf')
|
|
2426
|
+
self.history: Dict[str, List] = {
|
|
2427
|
+
'train_loss': [], 'val_loss': [], 'perplexity': [], 'recon_loss': [],
|
|
2428
|
+
'delta': [], 'alpha': [], 't_eff': [], 'kappa': [],
|
|
2429
|
+
'berry_phase': [], 'winding_number': [], 'lc': [], 'sp': [], 'phase': [],
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
# Metricas mecanisticas (Book.md)
|
|
2433
|
+
self.mets = MechanisticMetrics(config)
|
|
2434
|
+
|
|
2435
|
+
def resume(self) -> bool:
|
|
2436
|
+
"""
|
|
2437
|
+
Carga el ultimo checkpoint disponible.
|
|
2438
|
+
Restaura: pesos del modelo, estado del optimizador, historial acumulado,
|
|
2439
|
+
epoch/step completados y mejor val_loss.
|
|
2440
|
+
Devuelve True si se cargo un checkpoint, False si empieza de cero.
|
|
2441
|
+
"""
|
|
2442
|
+
if not self.ckpt_mgr.has_checkpoint():
|
|
2443
|
+
return False
|
|
2444
|
+
|
|
2445
|
+
state = self.ckpt_mgr.load_latest(self.model, self.optimizer)
|
|
2446
|
+
if not state:
|
|
2447
|
+
return False
|
|
2448
|
+
|
|
2449
|
+
self.completed_epochs = state.get('completed_epochs', 0)
|
|
2450
|
+
self.global_step = state.get('global_step', 0)
|
|
2451
|
+
self.best_val_loss = state.get('best_val_loss', float('inf'))
|
|
2452
|
+
|
|
2453
|
+
# Restaurar historial acumulado (incluye metricas mecanisticas)
|
|
2454
|
+
prev_hist = state.get('history', {})
|
|
2455
|
+
for key in self.history:
|
|
2456
|
+
self.history[key] = prev_hist.get(key, [])
|
|
2457
|
+
|
|
2458
|
+
return True
|
|
2459
|
+
|
|
2460
|
+
def _current_state(self) -> Dict:
|
|
2461
|
+
"""Construye el dict de estado para persistir en state.json."""
|
|
2462
|
+
return {
|
|
2463
|
+
'completed_epochs': self.completed_epochs,
|
|
2464
|
+
'global_step': self.global_step,
|
|
2465
|
+
'best_val_loss': self.best_val_loss,
|
|
2466
|
+
'history': self.history,
|
|
2467
|
+
'config': asdict(self.config),
|
|
2468
|
+
'saved_at': datetime.now().isoformat(),
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
def _cosine_lr(self, step_in_session: int, total_steps_session: int) -> float:
|
|
2472
|
+
"""Cosine decay con warmup. El schedule es relativo a la sesion actual."""
|
|
2473
|
+
warmup = max(1, int(total_steps_session * self.config.WARMUP_RATIO))
|
|
2474
|
+
if step_in_session < warmup:
|
|
2475
|
+
return self.config.LEARNING_RATE * step_in_session / warmup
|
|
2476
|
+
t = (step_in_session - warmup) / max(total_steps_session - warmup, 1)
|
|
2477
|
+
return self.config.LEARNING_RATE * 0.5 * (1.0 + math.cos(math.pi * t))
|
|
2478
|
+
|
|
2479
|
+
def _set_lr(self, lr: float) -> None:
|
|
2480
|
+
for pg in self.optimizer.param_groups:
|
|
2481
|
+
pg['lr'] = lr
|
|
2482
|
+
|
|
2483
|
+
def train(self, train_dl: torch.utils.data.DataLoader,
|
|
2484
|
+
val_dl: torch.utils.data.DataLoader) -> Dict[str, List[float]]:
|
|
2485
|
+
"""
|
|
2486
|
+
Entrena cfg.EPOCHS epocas adicionales a partir de completed_epochs.
|
|
2487
|
+
El historial se acumula sobre sesiones previas.
|
|
2488
|
+
"""
|
|
2489
|
+
cfg = self.config
|
|
2490
|
+
device = cfg.DEVICE
|
|
2491
|
+
epochs_this_session = cfg.EPOCHS
|
|
2492
|
+
steps_per_epoch = len(train_dl)
|
|
2493
|
+
total_steps_session = steps_per_epoch * epochs_this_session // cfg.GRAD_ACCUM_STEPS
|
|
2494
|
+
step_in_session = 0
|
|
2495
|
+
|
|
2496
|
+
self.logger.info("=" * 70)
|
|
2497
|
+
self.logger.info("TopoGPT2 - Entrenamiento acumulativo")
|
|
2498
|
+
p = self.model.count_params()
|
|
2499
|
+
self.logger.info(f"Parametros: {p['total']:,}")
|
|
2500
|
+
self.logger.info(f"Device: {device} | AMP: {cfg.USE_AMP} | "
|
|
2501
|
+
f"GradCkpt: {cfg.GRADIENT_CHECKPOINTING}")
|
|
2502
|
+
self.logger.info(f"Escala: {cfg.SCALE} | D_MODEL: {cfg.D_MODEL} | "
|
|
2503
|
+
f"Capas: {cfg.N_LAYERS} | SEQ_LEN: {cfg.MAX_SEQ_LEN}")
|
|
2504
|
+
self.logger.info(f"Toro: {cfg.TORUS_RADIAL_BINS}x{cfg.TORUS_ANGULAR_BINS} "
|
|
2505
|
+
f"= {cfg.N_TORUS_NODES} nodos")
|
|
2506
|
+
self.logger.info(f"Epocas previas completadas: {self.completed_epochs}")
|
|
2507
|
+
self.logger.info(f"Epocas a entrenar ahora: {epochs_this_session}")
|
|
2508
|
+
self.logger.info(f"Mejor val_loss hasta ahora: {self.best_val_loss:.4f}")
|
|
2509
|
+
self.logger.info("=" * 70)
|
|
2510
|
+
|
|
2511
|
+
for epoch_offset in range(1, epochs_this_session + 1):
|
|
2512
|
+
epoch_abs = self.completed_epochs + epoch_offset
|
|
2513
|
+
self.model.train()
|
|
2514
|
+
epoch_lm_loss = 0.0
|
|
2515
|
+
epoch_recon = 0.0
|
|
2516
|
+
n_batches = 0
|
|
2517
|
+
|
|
2518
|
+
self.optimizer.zero_grad()
|
|
2519
|
+
|
|
2520
|
+
for batch_x, batch_y in train_dl:
|
|
2521
|
+
batch_x = batch_x.to(device, non_blocking=True)
|
|
2522
|
+
batch_y = batch_y.to(device, non_blocking=True)
|
|
2523
|
+
|
|
2524
|
+
lr = self._cosine_lr(step_in_session, total_steps_session)
|
|
2525
|
+
self._set_lr(lr)
|
|
2526
|
+
|
|
2527
|
+
with torch.amp.autocast(device_type=device.split(':')[0],
|
|
2528
|
+
dtype=self.amp_dtype,
|
|
2529
|
+
enabled=cfg.USE_AMP):
|
|
2530
|
+
logits, recon_loss, _ = self.model(batch_x)
|
|
2531
|
+
lm_loss = F.cross_entropy(
|
|
2532
|
+
logits.view(-1, cfg.VOCAB_SIZE),
|
|
2533
|
+
batch_y.view(-1),
|
|
2534
|
+
)
|
|
2535
|
+
loss = (lm_loss + cfg.AE_RECON_WEIGHT * recon_loss) / cfg.GRAD_ACCUM_STEPS
|
|
2536
|
+
|
|
2537
|
+
self.scaler.scale(loss).backward()
|
|
2538
|
+
self.mets.update_grad_buffer(self.model)
|
|
2539
|
+
|
|
2540
|
+
n_batches += 1
|
|
2541
|
+
if n_batches % cfg.GRAD_ACCUM_STEPS == 0:
|
|
2542
|
+
self.scaler.unscale_(self.optimizer)
|
|
2543
|
+
torch.nn.utils.clip_grad_norm_(
|
|
2544
|
+
self.model.parameters(), cfg.GRADIENT_CLIP_NORM)
|
|
2545
|
+
self.scaler.step(self.optimizer)
|
|
2546
|
+
self.scaler.update()
|
|
2547
|
+
self.optimizer.zero_grad()
|
|
2548
|
+
self.global_step += 1
|
|
2549
|
+
step_in_session += 1
|
|
2550
|
+
|
|
2551
|
+
epoch_lm_loss += lm_loss.item()
|
|
2552
|
+
epoch_recon += recon_loss.item()
|
|
2553
|
+
|
|
2554
|
+
if n_batches % cfg.LOG_INTERVAL_STEPS == 0:
|
|
2555
|
+
self.logger.info(
|
|
2556
|
+
f"Epoch {epoch_abs} | batch {n_batches}/{steps_per_epoch} | "
|
|
2557
|
+
f"lm_loss={epoch_lm_loss/n_batches:.4f} | "
|
|
2558
|
+
f"recon={epoch_recon/n_batches:.4f} | "
|
|
2559
|
+
f"lr={lr:.2e}"
|
|
2560
|
+
)
|
|
2561
|
+
|
|
2562
|
+
# Checkpoint periodico intra-epoch
|
|
2563
|
+
if self.ckpt_mgr.should_save():
|
|
2564
|
+
self.ckpt_mgr.save(
|
|
2565
|
+
self.model, self.optimizer,
|
|
2566
|
+
self._current_state(), is_best=False,
|
|
2567
|
+
)
|
|
2568
|
+
|
|
2569
|
+
# -- Fin de epoch --
|
|
2570
|
+
avg_lm = epoch_lm_loss / max(n_batches, 1)
|
|
2571
|
+
avg_recon = epoch_recon / max(n_batches, 1)
|
|
2572
|
+
val_loss, ppl = self.evaluate(val_dl)
|
|
2573
|
+
|
|
2574
|
+
# Metricas mecanisticas (Book.md)
|
|
2575
|
+
m = self.mets.compute_all(
|
|
2576
|
+
self.model, lr=self._cosine_lr(step_in_session, total_steps_session),
|
|
2577
|
+
dataloader=val_dl, compute_kappa=False,
|
|
2578
|
+
)
|
|
2579
|
+
|
|
2580
|
+
self.completed_epochs += 1
|
|
2581
|
+
self.history['train_loss'].append(avg_lm)
|
|
2582
|
+
self.history['val_loss'].append(val_loss)
|
|
2583
|
+
self.history['perplexity'].append(ppl)
|
|
2584
|
+
self.history['recon_loss'].append(avg_recon)
|
|
2585
|
+
self.history['delta'].append(m.get('delta', 0.0))
|
|
2586
|
+
self.history['alpha'].append(m.get('alpha', 0.0))
|
|
2587
|
+
self.history['t_eff'].append(m.get('t_eff', 0.0))
|
|
2588
|
+
self.history['kappa'].append(m.get('kappa', 0.0))
|
|
2589
|
+
self.history['berry_phase'].append(m.get('berry_phase', 0.0))
|
|
2590
|
+
self.history['winding_number'].append(m.get('winding_number', 0))
|
|
2591
|
+
self.history['lc'].append(m.get('lc', 0.0))
|
|
2592
|
+
self.history['sp'].append(m.get('sp', 0.0))
|
|
2593
|
+
self.history['phase'].append(m.get('phase', 'unknown'))
|
|
2594
|
+
|
|
2595
|
+
is_best = val_loss < self.best_val_loss
|
|
2596
|
+
if is_best:
|
|
2597
|
+
self.best_val_loss = val_loss
|
|
2598
|
+
|
|
2599
|
+
self.logger.info(
|
|
2600
|
+
f"[Epoch {epoch_abs:4d}] "
|
|
2601
|
+
f"train={avg_lm:.4f} | val={val_loss:.4f} | "
|
|
2602
|
+
f"ppl={ppl:.2f} | recon={avg_recon:.4f} | "
|
|
2603
|
+
+ self.mets.format_log(m)
|
|
2604
|
+
+ (" <- mejor" if is_best else "")
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
# Checkpoint al final de cada epoch
|
|
2608
|
+
self.ckpt_mgr.save(
|
|
2609
|
+
self.model, self.optimizer,
|
|
2610
|
+
self._current_state(), is_best=is_best,
|
|
2611
|
+
)
|
|
2612
|
+
|
|
2613
|
+
# Muestra de texto (calidad cualitativa)
|
|
2614
|
+
self._sample_text(self.tokenizer)
|
|
2615
|
+
|
|
2616
|
+
return self.history
|
|
2617
|
+
|
|
2618
|
+
@torch.no_grad()
|
|
2619
|
+
def _sample_text(self, tokenizer=None, prompts: Optional[List[str]] = None,
|
|
2620
|
+
max_new: int = 80, temperature: float = 0.8, top_k: int = 40) -> None:
|
|
2621
|
+
"""
|
|
2622
|
+
Genera una muestra de texto al final de cada epoch para monitorear
|
|
2623
|
+
la calidad cualitativa del modelo (detecta degeneracion, repeticion, etc.).
|
|
2624
|
+
"""
|
|
2625
|
+
if tokenizer is None:
|
|
2626
|
+
return
|
|
2627
|
+
default_prompts = [
|
|
2628
|
+
"Once upon a time",
|
|
2629
|
+
"The little girl",
|
|
2630
|
+
"In the forest there",
|
|
2631
|
+
]
|
|
2632
|
+
sample_prompts = prompts or default_prompts
|
|
2633
|
+
self.model.eval()
|
|
2634
|
+
device = self.config.DEVICE
|
|
2635
|
+
self.logger.info("-" * 60)
|
|
2636
|
+
self.logger.info("Muestra de generacion:")
|
|
2637
|
+
for prompt in sample_prompts:
|
|
2638
|
+
ids = tokenizer.encode(prompt)
|
|
2639
|
+
ids_t = torch.tensor([ids], dtype=torch.long, device=device)
|
|
2640
|
+
gen = self.model.generate(
|
|
2641
|
+
ids_t, max_new_tokens=max_new,
|
|
2642
|
+
temperature=temperature, top_k=top_k)
|
|
2643
|
+
text = tokenizer.decode(gen[0].tolist())
|
|
2644
|
+
# Mostrar solo los primeros 200 chars para no saturar el log
|
|
2645
|
+
preview = text.replace('\n', ' ')[:200]
|
|
2646
|
+
self.logger.info(f" [{prompt!r}] -> {preview!r}")
|
|
2647
|
+
self.logger.info("-" * 60)
|
|
2648
|
+
self.model.train()
|
|
2649
|
+
|
|
2650
|
+
@torch.no_grad()
|
|
2651
|
+
def evaluate(self, dataloader) -> Tuple[float, float]:
|
|
2652
|
+
self.model.eval()
|
|
2653
|
+
total_loss, n = 0.0, 0
|
|
2654
|
+
device = self.config.DEVICE
|
|
2655
|
+
|
|
2656
|
+
for batch_x, batch_y in dataloader:
|
|
2657
|
+
batch_x = batch_x.to(device, non_blocking=True)
|
|
2658
|
+
batch_y = batch_y.to(device, non_blocking=True)
|
|
2659
|
+
with torch.amp.autocast(device_type=device.split(':')[0],
|
|
2660
|
+
dtype=self.amp_dtype,
|
|
2661
|
+
enabled=self.config.USE_AMP):
|
|
2662
|
+
logits, _, _ = self.model(batch_x)
|
|
2663
|
+
loss = F.cross_entropy(
|
|
2664
|
+
logits.view(-1, self.config.VOCAB_SIZE), batch_y.view(-1))
|
|
2665
|
+
total_loss += loss.item()
|
|
2666
|
+
n += 1
|
|
2667
|
+
|
|
2668
|
+
self.model.train()
|
|
2669
|
+
avg = total_loss / max(n, 1)
|
|
2670
|
+
ppl = math.exp(min(avg, 20.0))
|
|
2671
|
+
return avg, ppl
|
|
2672
|
+
|
|
2673
|
+
|
|
2674
|
+
# ============================================================================
|
|
2675
|
+
# INTERPRETACION MECANISTICA (Book.md)
|
|
2676
|
+
# Metricas del diagrama de fases: delta, kappa, T_eff, alpha, Berry, LC, SP
|
|
2677
|
+
# Clasificacion de fases: cold_glass, functional_glass,
|
|
2678
|
+
# topological_insulator, discrete_crystal
|
|
2679
|
+
# ============================================================================
|
|
2680
|
+
|
|
2681
|
+
class MechanisticMetrics:
|
|
2682
|
+
"""
|
|
2683
|
+
Calcula todas las metricas del diagrama de fases de Book.md.
|
|
2684
|
+
|
|
2685
|
+
Todas las metricas se derivan de cantidades medibles (pesos, gradientes):
|
|
2686
|
+
|
|
2687
|
+
delta (δ): margen de discretizacion. max|w - round(w)|
|
|
2688
|
+
δ≈0 -> cristal; δ≈0.49 -> vidrio frio
|
|
2689
|
+
kappa (κ): numero de condicion de la covarianza del gradiente.
|
|
2690
|
+
κ≈1 -> cristalino; κ>>1 -> amorfo
|
|
2691
|
+
T_eff: temperatura efectiva = (lr/2) * Var(gradiente).
|
|
2692
|
+
T_eff→0 -> congelado; T_eff alto -> ruidoso
|
|
2693
|
+
alpha (α): indice de pureza = -log(δ + ε).
|
|
2694
|
+
α=20 -> perfecto; α<1 -> vidrio
|
|
2695
|
+
berry: fase de Berry de los kernels espectrales imaginarios.
|
|
2696
|
+
|berry|>π/2 con winding≠0 -> insulador topologico
|
|
2697
|
+
lc: complejidad local = 1 - similitud coseno promedio entre filas.
|
|
2698
|
+
sp: superposicion = correlacion promedio inter-fila de pesos.
|
|
2699
|
+
"""
|
|
2700
|
+
|
|
2701
|
+
def __init__(self, config: 'TopoGPT2Config'):
|
|
2702
|
+
self.config = config
|
|
2703
|
+
self.grad_buffer: deque = deque(maxlen=50)
|
|
2704
|
+
self.max_grad_sample = 500
|
|
2705
|
+
# ------------------------------------------------------------------
|
|
2706
|
+
# Metricas individuales
|
|
2707
|
+
# ------------------------------------------------------------------
|
|
2708
|
+
|
|
2709
|
+
def compute_delta(self, model: nn.Module) -> float:
|
|
2710
|
+
margins = []
|
|
2711
|
+
for p in model.parameters():
|
|
2712
|
+
if p.numel() > 0 and p.is_floating_point():
|
|
2713
|
+
margins.append((p.data - p.data.round()).abs().max().item())
|
|
2714
|
+
return max(margins) if margins else 0.0
|
|
2715
|
+
|
|
2716
|
+
def compute_alpha(self, delta: float) -> float:
|
|
2717
|
+
if delta < 1e-8:
|
|
2718
|
+
return 20.0
|
|
2719
|
+
return -math.log(delta + 1e-10)
|
|
2720
|
+
|
|
2721
|
+
def update_grad_buffer(self, model: nn.Module) -> None:
|
|
2722
|
+
"""Captura gradientes de forma segura, ignorando tensores corruptos."""
|
|
2723
|
+
parts = []
|
|
2724
|
+
for p in model.parameters():
|
|
2725
|
+
if p.grad is not None and p.numel() > 0:
|
|
2726
|
+
try:
|
|
2727
|
+
# Mover a CPU de forma síncrona y segura. Si la GPU está en estado
|
|
2728
|
+
# corrupto, se captura aquí sin abortar el entrenamiento.
|
|
2729
|
+
grad_cpu = p.grad.detach().cpu().float()
|
|
2730
|
+
except RuntimeError:
|
|
2731
|
+
continue
|
|
2732
|
+
|
|
2733
|
+
# 1. Verificar corrupción numérica en CPU (evita sincronizaciones CUDA bloqueantes)
|
|
2734
|
+
if torch.isnan(grad_cpu).any() or torch.isinf(grad_cpu).any():
|
|
2735
|
+
continue # Salta gradientes inválidos generados por overflow AMP
|
|
2736
|
+
|
|
2737
|
+
# 2. Muestreo uniforme para mantener el buffer ligero y estadísticamente representativo
|
|
2738
|
+
flat = grad_cpu.flatten()
|
|
2739
|
+
if flat.numel() > self.max_grad_sample:
|
|
2740
|
+
step = max(1, flat.numel() // self.max_grad_sample)
|
|
2741
|
+
flat = flat[::step][:self.max_grad_sample]
|
|
2742
|
+
parts.append(flat)
|
|
2743
|
+
|
|
2744
|
+
if parts:
|
|
2745
|
+
self.grad_buffer.append(torch.cat(parts))
|
|
2746
|
+
|
|
2747
|
+
def compute_t_eff(self, lr: float) -> float:
|
|
2748
|
+
"""T_eff = lr/2 * Var(gradiente). Temperatura termodinamica efectiva."""
|
|
2749
|
+
if len(self.grad_buffer) < 2:
|
|
2750
|
+
return float('inf')
|
|
2751
|
+
G = torch.stack(list(self.grad_buffer)) # [N, D]
|
|
2752
|
+
var = G.pow(2).mean() - G.mean(0).pow(2).mean()
|
|
2753
|
+
return float(lr * var.clamp(min=0).item() / 2.0)
|
|
2754
|
+
|
|
2755
|
+
def compute_kappa(self, model: nn.Module, dataloader,
|
|
2756
|
+
n_batches: int = 5) -> float:
|
|
2757
|
+
"""
|
|
2758
|
+
κ = λ_max / λ_min de la covarianza del gradiente.
|
|
2759
|
+
Parámetro de orden para cristalización (κ≈1 = cristal).
|
|
2760
|
+
Nota: requiere pasadas backward adicionales. Se ejecuta con protección
|
|
2761
|
+
para no corromper el estado AMP del trainer principal.
|
|
2762
|
+
"""
|
|
2763
|
+
device = self.config.DEVICE
|
|
2764
|
+
model.eval()
|
|
2765
|
+
grad_samples = []
|
|
2766
|
+
for i, (bx, by) in enumerate(dataloader):
|
|
2767
|
+
if i >= n_batches:
|
|
2768
|
+
break
|
|
2769
|
+
bx, by = bx.to(device), by.to(device)
|
|
2770
|
+
model.zero_grad()
|
|
2771
|
+
try:
|
|
2772
|
+
with torch.no_grad():
|
|
2773
|
+
pass # Placeholder para mantener estructura
|
|
2774
|
+
logits, _, _ = model(bx)
|
|
2775
|
+
loss = F.cross_entropy(
|
|
2776
|
+
logits.view(-1, self.config.VOCAB_SIZE), by.view(-1))
|
|
2777
|
+
loss.backward()
|
|
2778
|
+
|
|
2779
|
+
parts = []
|
|
2780
|
+
for p in model.parameters():
|
|
2781
|
+
if p.grad is not None:
|
|
2782
|
+
# Muestreo seguro a CPU para análisis
|
|
2783
|
+
g = p.grad.detach().flatten()[:300].cpu().float()
|
|
2784
|
+
if not torch.isnan(g).any() and not torch.isinf(g).any():
|
|
2785
|
+
parts.append(g)
|
|
2786
|
+
if parts:
|
|
2787
|
+
grad_samples.append(torch.cat(parts))
|
|
2788
|
+
except RuntimeError:
|
|
2789
|
+
# Si el cálculo de kappa falla (ej. OOM o kernel crash),
|
|
2790
|
+
# retorna inf para marcar inestabilidad sin abortar
|
|
2791
|
+
model.zero_grad()
|
|
2792
|
+
return float('inf')
|
|
2793
|
+
finally:
|
|
2794
|
+
model.zero_grad()
|
|
2795
|
+
|
|
2796
|
+
model.train()
|
|
2797
|
+
if len(grad_samples) < 2:
|
|
2798
|
+
return float('inf')
|
|
2799
|
+
G = torch.stack(grad_samples)
|
|
2800
|
+
try:
|
|
2801
|
+
if G.size(0) < G.size(1):
|
|
2802
|
+
gram = G @ G.t() / max(G.size(0) - 1, 1)
|
|
2803
|
+
eigs = torch.linalg.eigvalsh(gram).real
|
|
2804
|
+
else:
|
|
2805
|
+
eigs = torch.linalg.eigvalsh(torch.cov(G.t())).real
|
|
2806
|
+
eigs = eigs[eigs > 1e-10]
|
|
2807
|
+
if len(eigs) < 2:
|
|
2808
|
+
return float('inf')
|
|
2809
|
+
return float(eigs.max() / eigs.min())
|
|
2810
|
+
except Exception:
|
|
2811
|
+
return float('inf')
|
|
2812
|
+
|
|
2813
|
+
def compute_berry_phase(self, model: nn.Module) -> float:
|
|
2814
|
+
"""
|
|
2815
|
+
Fase de Berry de los kernels espectrales imaginarios.
|
|
2816
|
+
Surge de los parametros ki_w, ki_x, ki_y, ki_z de QuaternionSpectralLayer.
|
|
2817
|
+
|berry|>pi/2 con winding!=0 indica estructura topologica.
|
|
2818
|
+
"""
|
|
2819
|
+
phases = []
|
|
2820
|
+
for name, p in model.named_parameters():
|
|
2821
|
+
if 'ki_' in name and p.numel() > 0:
|
|
2822
|
+
flat = p.detach().flatten().float()
|
|
2823
|
+
phases.append(torch.atan2(flat, flat.abs() + 1e-10).sum().item())
|
|
2824
|
+
return sum(phases) if phases else 0.0
|
|
2825
|
+
|
|
2826
|
+
def compute_lc(self, model: nn.Module) -> float:
|
|
2827
|
+
"""Complejidad local: 1 - similitud coseno promedio entre filas de pesos."""
|
|
2828
|
+
vals = []
|
|
2829
|
+
for name, p in model.named_parameters():
|
|
2830
|
+
if 'weight' in name and p.dim() >= 2 and p.size(0) >= 4:
|
|
2831
|
+
w = p.detach()[:min(p.size(0), 64), :min(p.size(1), 64)].float()
|
|
2832
|
+
norms = w.norm(dim=1, keepdim=True).clamp(min=1e-8)
|
|
2833
|
+
w = w / norms
|
|
2834
|
+
sim = (w @ w.t()).abs()
|
|
2835
|
+
mask = ~torch.eye(sim.size(0), dtype=torch.bool)
|
|
2836
|
+
if mask.any():
|
|
2837
|
+
vals.append(float(1.0 - sim[mask].mean().clamp(0, 1)))
|
|
2838
|
+
return float(np.mean(vals)) if vals else 0.0
|
|
2839
|
+
|
|
2840
|
+
def compute_sp(self, model: nn.Module) -> float:
|
|
2841
|
+
"""Superposicion: correlacion inter-fila promedio (entrelazamiento de features)."""
|
|
2842
|
+
vals = []
|
|
2843
|
+
for name, p in model.named_parameters():
|
|
2844
|
+
if 'weight' in name and p.dim() >= 2 and p.size(0) >= 4:
|
|
2845
|
+
w = p.detach()[:min(p.size(0), 64)].float()
|
|
2846
|
+
w = w.reshape(w.size(0), -1)
|
|
2847
|
+
try:
|
|
2848
|
+
corr = torch.corrcoef(w).nan_to_num(0.0).abs()
|
|
2849
|
+
mask = ~torch.eye(corr.size(0), dtype=torch.bool)
|
|
2850
|
+
if mask.any():
|
|
2851
|
+
vals.append(float(corr[mask].mean()))
|
|
2852
|
+
except Exception:
|
|
2853
|
+
pass
|
|
2854
|
+
return float(np.mean(vals)) if vals else 0.0
|
|
2855
|
+
|
|
2856
|
+
def classify_phase(self, delta: float, kappa: float,
|
|
2857
|
+
berry: float) -> str:
|
|
2858
|
+
"""
|
|
2859
|
+
Clasificacion de fase segun Book.md:
|
|
2860
|
+
|
|
2861
|
+
discrete_crystal: delta<0.05, kappa<1.5
|
|
2862
|
+
topological_insulator: |berry|>pi/2, winding!=0
|
|
2863
|
+
cold_glass: kappa>>1, delta>0.3
|
|
2864
|
+
functional_glass: intermedio (lo mas comun en LM)
|
|
2865
|
+
"""
|
|
2866
|
+
winding = round(berry / (2 * math.pi))
|
|
2867
|
+
if delta < 0.05 and kappa < 1.5:
|
|
2868
|
+
return 'discrete_crystal'
|
|
2869
|
+
if abs(berry) > math.pi / 2 and abs(winding) >= 1:
|
|
2870
|
+
return 'topological_insulator'
|
|
2871
|
+
if kappa > 100 and delta > 0.3:
|
|
2872
|
+
return 'cold_glass'
|
|
2873
|
+
return 'functional_glass'
|
|
2874
|
+
|
|
2875
|
+
def compute_all(self, model: nn.Module, lr: float,
|
|
2876
|
+
dataloader=None, compute_kappa: bool = False) -> Dict[str, Any]:
|
|
2877
|
+
"""
|
|
2878
|
+
Calcula todas las metricas.
|
|
2879
|
+
compute_kappa=True hace pasadas backward adicionales (caro, usar cada N epochs).
|
|
2880
|
+
"""
|
|
2881
|
+
delta = self.compute_delta(model)
|
|
2882
|
+
alpha = self.compute_alpha(delta)
|
|
2883
|
+
t_eff = self.compute_t_eff(lr)
|
|
2884
|
+
berry = self.compute_berry_phase(model)
|
|
2885
|
+
winding = round(berry / (2 * math.pi))
|
|
2886
|
+
lc = self.compute_lc(model)
|
|
2887
|
+
sp = self.compute_sp(model)
|
|
2888
|
+
kappa = (
|
|
2889
|
+
self.compute_kappa(model, dataloader)
|
|
2890
|
+
if compute_kappa and dataloader is not None
|
|
2891
|
+
else float('inf')
|
|
2892
|
+
)
|
|
2893
|
+
phase = self.classify_phase(delta, kappa, berry)
|
|
2894
|
+
return {
|
|
2895
|
+
'delta': delta, 'alpha': alpha, 't_eff': t_eff, 'kappa': kappa,
|
|
2896
|
+
'berry_phase': berry, 'winding_number': winding,
|
|
2897
|
+
'lc': lc, 'sp': sp, 'phase': phase,
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
def format_log(self, m: Dict[str, Any]) -> str:
|
|
2901
|
+
inf = float('inf')
|
|
2902
|
+
kappa = m.get('kappa', 0.0)
|
|
2903
|
+
kappa_str = f"{kappa:.2f}" if kappa < inf else "inf"
|
|
2904
|
+
return (
|
|
2905
|
+
f"delta={m.get('delta', 0.0):.4f} alpha={m.get('alpha', 0.0):.2f} "
|
|
2906
|
+
f"T_eff={m.get('t_eff', 0.0):.2e} kappa={kappa_str} "
|
|
2907
|
+
f"berry={m.get('berry_phase', 0.0):.2f} "
|
|
2908
|
+
f"wind={m.get('winding_number', 0)} "
|
|
2909
|
+
f"lc={m.get('lc', 0.0):.3f} sp={m.get('sp', 0.0):.3f} "
|
|
2910
|
+
f"[{m.get('phase', 'unknown').upper()}]"
|
|
2911
|
+
)
|
|
2912
|
+
|
|
2913
|
+
|
|
2914
|
+
# ============================================================================
|
|
2915
|
+
# FASE 0: Optimizacion del ratio de kernels espectrales (GOE-GUE)
|
|
2916
|
+
# ============================================================================
|
|
2917
|
+
|
|
2918
|
+
class Phase0_KernelOptimizer:
|
|
2919
|
+
"""
|
|
2920
|
+
Encuentra el ratio imaginario/real optimo para los kernels espectrales.
|
|
2921
|
+
|
|
2922
|
+
Analogia con main.py: evalua la transicion GOE→GUE en el espacio
|
|
2923
|
+
de kernels. Un ratio optimo promueve estructura topologica (insulador)
|
|
2924
|
+
vs estructura amorfa (vidrio).
|
|
2925
|
+
|
|
2926
|
+
Metodo: calibra con un mini-batch y mide la varianza del gradiente
|
|
2927
|
+
en funcion del ratio. Ratios que minimizan la varianza de gradiente
|
|
2928
|
+
(maxima coherencia espectral) son preferibles.
|
|
2929
|
+
|
|
2930
|
+
No entrena: solo inicializa los kernels con distintos ratios y mide.
|
|
2931
|
+
Tiempo tipico: < 30 segundos.
|
|
2932
|
+
"""
|
|
2933
|
+
|
|
2934
|
+
RATIOS_TO_TEST = [0.05, 0.10, 0.18, 0.30, 0.50] # del config original
|
|
2935
|
+
|
|
2936
|
+
def __init__(self, config: 'TopoGPT2Config', logger: logging.Logger):
|
|
2937
|
+
self.config = config
|
|
2938
|
+
self.logger = logger
|
|
2939
|
+
|
|
2940
|
+
def _measure_ratio(self, ratio: float, sample_batch: Tuple[torch.Tensor, torch.Tensor]) -> float:
|
|
2941
|
+
"""
|
|
2942
|
+
Mide la coherencia espectral para un ratio dado.
|
|
2943
|
+
Retorna: varianza del gradiente (menor = mas coherente = mejor).
|
|
2944
|
+
"""
|
|
2945
|
+
# Modelo temporal con ese ratio
|
|
2946
|
+
tmp_cfg = TopoGPT2Config.__new__(TopoGPT2Config)
|
|
2947
|
+
tmp_cfg.__dict__.update(self.config.__dict__)
|
|
2948
|
+
tmp_cfg.SPECTRAL_KERNEL_INIT_SCALE = ratio
|
|
2949
|
+
model = TopoGPT2(tmp_cfg).to(self.config.DEVICE)
|
|
2950
|
+
|
|
2951
|
+
bx, by = sample_batch
|
|
2952
|
+
bx, by = bx.to(self.config.DEVICE), by.to(self.config.DEVICE)
|
|
2953
|
+
|
|
2954
|
+
model.zero_grad()
|
|
2955
|
+
logits, _, _ = model(bx)
|
|
2956
|
+
loss = F.cross_entropy(logits.view(-1, self.config.VOCAB_SIZE), by.view(-1))
|
|
2957
|
+
loss.backward()
|
|
2958
|
+
|
|
2959
|
+
grads = []
|
|
2960
|
+
for p in model.parameters():
|
|
2961
|
+
if p.grad is not None:
|
|
2962
|
+
grads.append(p.grad.detach().flatten()[:200].float())
|
|
2963
|
+
if not grads:
|
|
2964
|
+
return float('inf')
|
|
2965
|
+
G = torch.cat(grads)
|
|
2966
|
+
# Menor varianza = gradientes mas isotropos = mejor coherencia
|
|
2967
|
+
return float(G.var().item())
|
|
2968
|
+
|
|
2969
|
+
def optimize(self, dataloader) -> float:
|
|
2970
|
+
"""Retorna el mejor ratio de inicializacion de kernels espectrales."""
|
|
2971
|
+
self.logger.info("Fase 0: optimizando ratio de kernels espectrales...")
|
|
2972
|
+
try:
|
|
2973
|
+
sample_batch = next(iter(dataloader))
|
|
2974
|
+
except StopIteration:
|
|
2975
|
+
self.logger.warning("Fase 0: dataloader vacio, usando ratio default 0.02")
|
|
2976
|
+
return 0.02
|
|
2977
|
+
|
|
2978
|
+
results = {}
|
|
2979
|
+
for ratio in self.RATIOS_TO_TEST:
|
|
2980
|
+
score = self._measure_ratio(ratio, sample_batch)
|
|
2981
|
+
results[ratio] = score
|
|
2982
|
+
self.logger.info(f" ratio={ratio:.2f} -> grad_var={score:.4e}")
|
|
2983
|
+
|
|
2984
|
+
best_ratio = min(results, key=results.get)
|
|
2985
|
+
self.logger.info(f"Fase 0: mejor ratio = {best_ratio} (grad_var={results[best_ratio]:.4e})")
|
|
2986
|
+
return best_ratio
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
# ============================================================================
|
|
2990
|
+
# FASE 1: Prospecting de batch size
|
|
2991
|
+
# ============================================================================
|
|
2992
|
+
|
|
2993
|
+
class Phase1_BatchProspector:
|
|
2994
|
+
"""
|
|
2995
|
+
Encuentra el batch size optimo testando candidatos con pocos pasos.
|
|
2996
|
+
|
|
2997
|
+
De main.py: el batch size regula la temperatura del horno de cristalizacion.
|
|
2998
|
+
Batch sizes demasiado chicos -> ruido excesivo (vidrio frio).
|
|
2999
|
+
Batch sizes demasiado grandes -> sin presion annealing (amorfos).
|
|
3000
|
+
La ventana optima empirica de main.py: [24, 128] para Strassen.
|
|
3001
|
+
|
|
3002
|
+
Para LM, testeamos candidatos midiendo:
|
|
3003
|
+
- delta (δ): velocidad de descenso en prospect_steps pasos
|
|
3004
|
+
- T_eff: temperatura efectiva del gradiente
|
|
3005
|
+
|
|
3006
|
+
Tiempo tipico: < 2 minutos para 3 candidatos × 30 pasos.
|
|
3007
|
+
"""
|
|
3008
|
+
|
|
3009
|
+
def __init__(self, config: 'TopoGPT2Config', logger: logging.Logger):
|
|
3010
|
+
self.config = config
|
|
3011
|
+
self.logger = logger
|
|
3012
|
+
|
|
3013
|
+
def prospect(self, candidates: List[int], train_dataset,
|
|
3014
|
+
prospect_steps: int = 30) -> int:
|
|
3015
|
+
"""Retorna el mejor batch size segun delta y T_eff."""
|
|
3016
|
+
self.logger.info(
|
|
3017
|
+
f"Fase 1: prospecting batch sizes {candidates} "
|
|
3018
|
+
f"({prospect_steps} pasos cada uno)...")
|
|
3019
|
+
device = self.config.DEVICE
|
|
3020
|
+
results = {}
|
|
3021
|
+
|
|
3022
|
+
for bs in candidates:
|
|
3023
|
+
dl = torch.utils.data.DataLoader(
|
|
3024
|
+
train_dataset, batch_size=bs, shuffle=True, drop_last=True)
|
|
3025
|
+
model = TopoGPT2(self.config).to(device)
|
|
3026
|
+
optimizer = torch.optim.AdamW(
|
|
3027
|
+
model.parameters(), lr=self.config.LEARNING_RATE)
|
|
3028
|
+
metrics = MechanisticMetrics(self.config)
|
|
3029
|
+
|
|
3030
|
+
model.train()
|
|
3031
|
+
delta_start = metrics.compute_delta(model)
|
|
3032
|
+
|
|
3033
|
+
for step, (bx, by) in enumerate(dl):
|
|
3034
|
+
if step >= prospect_steps:
|
|
3035
|
+
break
|
|
3036
|
+
bx, by = bx.to(device), by.to(device)
|
|
3037
|
+
optimizer.zero_grad()
|
|
3038
|
+
logits, rl, _ = model(bx)
|
|
3039
|
+
loss = (F.cross_entropy(logits.view(-1, self.config.VOCAB_SIZE),
|
|
3040
|
+
by.view(-1))
|
|
3041
|
+
+ self.config.AE_RECON_WEIGHT * rl)
|
|
3042
|
+
loss.backward()
|
|
3043
|
+
metrics.update_grad_buffer(model)
|
|
3044
|
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
3045
|
+
optimizer.step()
|
|
3046
|
+
|
|
3047
|
+
delta_end = metrics.compute_delta(model)
|
|
3048
|
+
t_eff = metrics.compute_t_eff(self.config.LEARNING_RATE)
|
|
3049
|
+
delta_velocity = (delta_end - delta_start) / max(prospect_steps, 1)
|
|
3050
|
+
|
|
3051
|
+
results[bs] = {
|
|
3052
|
+
'delta_end': delta_end,
|
|
3053
|
+
'delta_velocity': delta_velocity,
|
|
3054
|
+
't_eff': t_eff,
|
|
3055
|
+
}
|
|
3056
|
+
self.logger.info(
|
|
3057
|
+
f" bs={bs:4d}: delta={delta_end:.4f} "
|
|
3058
|
+
f"d_vel={delta_velocity:.2e} T_eff={t_eff:.2e}")
|
|
3059
|
+
|
|
3060
|
+
del model, optimizer
|
|
3061
|
+
|
|
3062
|
+
# Criterio: batch size con velocidad de delta mas negativa
|
|
3063
|
+
# (enfriamiento mas rapido). Desempate: T_eff mas bajo.
|
|
3064
|
+
best_bs = min(
|
|
3065
|
+
results,
|
|
3066
|
+
key=lambda b: (results[b]['delta_velocity'], results[b]['t_eff'])
|
|
3067
|
+
)
|
|
3068
|
+
self.logger.info(f"Fase 1: mejor batch size = {best_bs}")
|
|
3069
|
+
return best_bs
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
# ============================================================================
|
|
3073
|
+
# FASE 2: Mineria de semillas (seed mining)
|
|
3074
|
+
# ============================================================================
|
|
3075
|
+
|
|
3076
|
+
class Phase2_SeedMiner:
|
|
3077
|
+
"""
|
|
3078
|
+
Encuentra semillas prometedoras midiendo la trayectoria de delta.
|
|
3079
|
+
|
|
3080
|
+
De main.py: una semilla "buena" muestra delta descendente en los
|
|
3081
|
+
primeros N pasos (enfriamiento). Una semilla "mala" se estanca en
|
|
3082
|
+
el plateau vidrioso (~0.49).
|
|
3083
|
+
|
|
3084
|
+
Criterio de seleccion:
|
|
3085
|
+
1. Semillas con delta_velocity < 0 (enfriando) AND kappa bajo.
|
|
3086
|
+
2. Si no hay, semillas solo enfriando.
|
|
3087
|
+
3. Fallback: semilla con menor delta final.
|
|
3088
|
+
|
|
3089
|
+
Tiempo tipico: < 3 minutos para 5 semillas × 50 pasos.
|
|
3090
|
+
"""
|
|
3091
|
+
|
|
3092
|
+
def __init__(self, config: 'TopoGPT2Config', logger: logging.Logger):
|
|
3093
|
+
self.config = config
|
|
3094
|
+
self.logger = logger
|
|
3095
|
+
|
|
3096
|
+
def mine(self, seed_start: int, n_seeds: int, train_dataset,
|
|
3097
|
+
prospect_steps: int = 50) -> int:
|
|
3098
|
+
"""Retorna la semilla con la mejor trayectoria de delta."""
|
|
3099
|
+
self.logger.info(
|
|
3100
|
+
f"Fase 2: minando {n_seeds} semillas desde seed={seed_start} "
|
|
3101
|
+
f"({prospect_steps} pasos cada una)...")
|
|
3102
|
+
device = self.config.DEVICE
|
|
3103
|
+
dl = torch.utils.data.DataLoader(
|
|
3104
|
+
train_dataset, batch_size=self.config.BATCH_SIZE,
|
|
3105
|
+
shuffle=True, drop_last=True)
|
|
3106
|
+
results = {}
|
|
3107
|
+
|
|
3108
|
+
for s in range(seed_start, seed_start + n_seeds):
|
|
3109
|
+
set_seed(s, device)
|
|
3110
|
+
model = TopoGPT2(self.config).to(device)
|
|
3111
|
+
optimizer = torch.optim.AdamW(
|
|
3112
|
+
model.parameters(), lr=self.config.LEARNING_RATE)
|
|
3113
|
+
mets = MechanisticMetrics(self.config)
|
|
3114
|
+
|
|
3115
|
+
delta_start = mets.compute_delta(model)
|
|
3116
|
+
model.train()
|
|
3117
|
+
|
|
3118
|
+
for step, (bx, by) in enumerate(dl):
|
|
3119
|
+
if step >= prospect_steps:
|
|
3120
|
+
break
|
|
3121
|
+
bx, by = bx.to(device), by.to(device)
|
|
3122
|
+
optimizer.zero_grad()
|
|
3123
|
+
logits, rl, _ = model(bx)
|
|
3124
|
+
loss = (F.cross_entropy(logits.view(-1, self.config.VOCAB_SIZE),
|
|
3125
|
+
by.view(-1))
|
|
3126
|
+
+ self.config.AE_RECON_WEIGHT * rl)
|
|
3127
|
+
loss.backward()
|
|
3128
|
+
mets.update_grad_buffer(model)
|
|
3129
|
+
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
|
3130
|
+
optimizer.step()
|
|
3131
|
+
|
|
3132
|
+
delta_end = mets.compute_delta(model)
|
|
3133
|
+
t_eff = mets.compute_t_eff(self.config.LEARNING_RATE)
|
|
3134
|
+
delta_vel = (delta_end - delta_start) / max(prospect_steps, 1)
|
|
3135
|
+
is_cooling = delta_vel < 0
|
|
3136
|
+
|
|
3137
|
+
results[s] = {
|
|
3138
|
+
'delta_end': delta_end, 'delta_velocity': delta_vel,
|
|
3139
|
+
't_eff': t_eff, 'is_cooling': is_cooling,
|
|
3140
|
+
}
|
|
3141
|
+
self.logger.info(
|
|
3142
|
+
f" seed={s:4d}: delta={delta_end:.4f} "
|
|
3143
|
+
f"d_vel={delta_vel:.2e} cooling={is_cooling}")
|
|
3144
|
+
del model, optimizer
|
|
3145
|
+
|
|
3146
|
+
# Ordenar: cooling primero, luego delta_velocity mas negativo
|
|
3147
|
+
cooling_seeds = {s: v for s, v in results.items() if v['is_cooling']}
|
|
3148
|
+
pool = cooling_seeds if cooling_seeds else results
|
|
3149
|
+
best_seed = min(pool, key=lambda s: (pool[s]['delta_velocity'], pool[s]['delta_end']))
|
|
3150
|
+
self.logger.info(f"Fase 2: mejor semilla = {best_seed}")
|
|
3151
|
+
return best_seed
|
|
3152
|
+
|
|
3153
|
+
|
|
3154
|
+
# ============================================================================
|
|
3155
|
+
# FASE 4: Refinamiento por Recocido Simulado (Simulated Annealing)
|
|
3156
|
+
# ============================================================================
|
|
3157
|
+
|
|
3158
|
+
class Phase4_AnnealingRefiner:
|
|
3159
|
+
"""
|
|
3160
|
+
Refinamiento post-entrenamiento mediante recocido simulado.
|
|
3161
|
+
|
|
3162
|
+
De main.py: despues de que el modelo converge, una fase de annealing
|
|
3163
|
+
con criterio de aceptacion de Metropolis puede empujar los pesos
|
|
3164
|
+
hacia estados de menor energia libre (menor delta o mejor val_loss).
|
|
3165
|
+
|
|
3166
|
+
Aceptacion de Metropolis:
|
|
3167
|
+
si Δloss < 0: siempre acepta (mejora)
|
|
3168
|
+
si Δloss >= 0: acepta con prob exp(-Δloss / T)
|
|
3169
|
+
|
|
3170
|
+
La temperatura T decae exponencialmente: T(t) = T0 * cooling_rate^t
|
|
3171
|
+
|
|
3172
|
+
Al rechazar: restaura el mejor estado conocido.
|
|
3173
|
+
Si se estanca: perturbacion termica (ruido gaussiano en pesos).
|
|
3174
|
+
|
|
3175
|
+
Tiempo: proporcional a refine_epochs (user-controlled).
|
|
3176
|
+
"""
|
|
3177
|
+
|
|
3178
|
+
def __init__(self, trainer: 'TopoGPT2Trainer',
|
|
3179
|
+
t0: float = 0.5, cooling_rate: float = 0.95,
|
|
3180
|
+
stagnation_patience: int = 5):
|
|
3181
|
+
self.trainer = trainer
|
|
3182
|
+
self.t0 = t0
|
|
3183
|
+
self.cooling_rate = cooling_rate
|
|
3184
|
+
self.stagnation_patience = stagnation_patience
|
|
3185
|
+
self.logger = trainer.logger
|
|
3186
|
+
|
|
3187
|
+
def refine(self, train_dl, val_dl, refine_epochs: int) -> Dict:
|
|
3188
|
+
"""
|
|
3189
|
+
Ejecuta refine_epochs epocas de recocido simulado.
|
|
3190
|
+
Retorna el historial de refinamiento.
|
|
3191
|
+
"""
|
|
3192
|
+
cfg = self.trainer.config
|
|
3193
|
+
device = cfg.DEVICE
|
|
3194
|
+
model = self.trainer.model
|
|
3195
|
+
optimizer = self.trainer.optimizer
|
|
3196
|
+
metrics = MechanisticMetrics(cfg)
|
|
3197
|
+
|
|
3198
|
+
T = self.t0
|
|
3199
|
+
best_val_loss = self.trainer.best_val_loss
|
|
3200
|
+
best_state = {k: v.clone() for k, v in model.state_dict().items()}
|
|
3201
|
+
prev_val_loss = best_val_loss
|
|
3202
|
+
stagnation_count = 0
|
|
3203
|
+
refine_history: List[Dict] = []
|
|
3204
|
+
|
|
3205
|
+
self.logger.info("=" * 70)
|
|
3206
|
+
self.logger.info(f"Fase 4: recocido simulado ({refine_epochs} epocas)")
|
|
3207
|
+
self.logger.info(f" T0={self.t0} cooling={self.cooling_rate} "
|
|
3208
|
+
f"best_val={best_val_loss:.4f}")
|
|
3209
|
+
self.logger.info("=" * 70)
|
|
3210
|
+
|
|
3211
|
+
for epoch_idx in range(1, refine_epochs + 1):
|
|
3212
|
+
epoch_abs = self.trainer.completed_epochs + 1
|
|
3213
|
+
|
|
3214
|
+
# Guardar estado pre-epoch para posible rechazo
|
|
3215
|
+
prev_state = {k: v.clone() for k, v in model.state_dict().items()}
|
|
3216
|
+
|
|
3217
|
+
# Entrenamiento de una epoch
|
|
3218
|
+
model.train()
|
|
3219
|
+
ep_loss, n = 0.0, 0
|
|
3220
|
+
optimizer.zero_grad()
|
|
3221
|
+
for step, (bx, by) in enumerate(train_dl):
|
|
3222
|
+
bx, by = bx.to(device), by.to(device)
|
|
3223
|
+
with torch.amp.autocast(
|
|
3224
|
+
device_type=device.split(':')[0],
|
|
3225
|
+
dtype=self.trainer.amp_dtype,
|
|
3226
|
+
enabled=cfg.USE_AMP):
|
|
3227
|
+
logits, rl, _ = model(bx)
|
|
3228
|
+
loss = (
|
|
3229
|
+
F.cross_entropy(logits.view(-1, cfg.VOCAB_SIZE), by.view(-1))
|
|
3230
|
+
+ cfg.AE_RECON_WEIGHT * rl
|
|
3231
|
+
) / cfg.GRAD_ACCUM_STEPS
|
|
3232
|
+
self.trainer.scaler.scale(loss).backward()
|
|
3233
|
+
metrics.update_grad_buffer(model)
|
|
3234
|
+
if (step + 1) % cfg.GRAD_ACCUM_STEPS == 0:
|
|
3235
|
+
self.trainer.scaler.unscale_(optimizer)
|
|
3236
|
+
torch.nn.utils.clip_grad_norm_(model.parameters(), cfg.GRADIENT_CLIP_NORM)
|
|
3237
|
+
self.trainer.scaler.step(optimizer)
|
|
3238
|
+
self.trainer.scaler.update()
|
|
3239
|
+
optimizer.zero_grad()
|
|
3240
|
+
ep_loss += loss.item() * cfg.GRAD_ACCUM_STEPS
|
|
3241
|
+
n += 1
|
|
3242
|
+
|
|
3243
|
+
val_loss, ppl = self.trainer.evaluate(val_dl)
|
|
3244
|
+
delta = metrics.compute_delta(model)
|
|
3245
|
+
t_eff = metrics.compute_t_eff(cfg.LEARNING_RATE)
|
|
3246
|
+
|
|
3247
|
+
# Criterio de Metropolis: acepta o rechaza la epoch
|
|
3248
|
+
delta_loss = val_loss - prev_val_loss
|
|
3249
|
+
if delta_loss < 0:
|
|
3250
|
+
accept = True
|
|
3251
|
+
else:
|
|
3252
|
+
prob = math.exp(-delta_loss / max(T, 1e-12))
|
|
3253
|
+
accept = (torch.rand(1).item() < prob)
|
|
3254
|
+
|
|
3255
|
+
if accept:
|
|
3256
|
+
prev_val_loss = val_loss
|
|
3257
|
+
if val_loss < best_val_loss:
|
|
3258
|
+
best_val_loss = val_loss
|
|
3259
|
+
best_state = {k: v.clone() for k, v in model.state_dict().items()}
|
|
3260
|
+
self.trainer.best_val_loss = best_val_loss
|
|
3261
|
+
stagnation_count = 0
|
|
3262
|
+
else:
|
|
3263
|
+
# Rechazar: volver al estado anterior
|
|
3264
|
+
model.load_state_dict(prev_state)
|
|
3265
|
+
stagnation_count += 1
|
|
3266
|
+
self.logger.info(
|
|
3267
|
+
f" [Annealing] Epoch rechazada (Δloss={delta_loss:.4f}, T={T:.4f})")
|
|
3268
|
+
|
|
3269
|
+
# Estancamiento: perturbacion termica
|
|
3270
|
+
if stagnation_count >= self.stagnation_patience:
|
|
3271
|
+
self.logger.info(" [Annealing] Estancamiento: perturbacion termica")
|
|
3272
|
+
model.load_state_dict(best_state)
|
|
3273
|
+
with torch.no_grad():
|
|
3274
|
+
for p in model.parameters():
|
|
3275
|
+
p.add_(torch.randn_like(p) * T * 1e-3)
|
|
3276
|
+
stagnation_count = 0
|
|
3277
|
+
|
|
3278
|
+
# Enfriar temperatura
|
|
3279
|
+
T *= self.cooling_rate
|
|
3280
|
+
|
|
3281
|
+
# Actualizar estado del trainer
|
|
3282
|
+
self.trainer.completed_epochs += 1
|
|
3283
|
+
self.trainer.history['train_loss'].append(ep_loss / max(n, 1))
|
|
3284
|
+
self.trainer.history['val_loss'].append(val_loss)
|
|
3285
|
+
self.trainer.history['perplexity'].append(ppl)
|
|
3286
|
+
self.trainer.history['recon_loss'].append(0.0)
|
|
3287
|
+
|
|
3288
|
+
rec = {
|
|
3289
|
+
'epoch': epoch_abs, 'val_loss': val_loss, 'ppl': ppl,
|
|
3290
|
+
'delta': delta, 't_eff': t_eff, 'T_anneal': T,
|
|
3291
|
+
'accepted': accept,
|
|
3292
|
+
}
|
|
3293
|
+
refine_history.append(rec)
|
|
3294
|
+
|
|
3295
|
+
self.logger.info(
|
|
3296
|
+
f"[Annealing {epoch_idx:3d}] "
|
|
3297
|
+
f"val={val_loss:.4f} ppl={ppl:.2f} delta={delta:.4f} "
|
|
3298
|
+
f"T={T:.4f} accept={accept}"
|
|
3299
|
+
)
|
|
3300
|
+
|
|
3301
|
+
# Checkpoint
|
|
3302
|
+
self.trainer.ckpt_mgr.save(
|
|
3303
|
+
model, optimizer,
|
|
3304
|
+
self.trainer._current_state(),
|
|
3305
|
+
is_best=(val_loss < best_val_loss + 1e-8),
|
|
3306
|
+
)
|
|
3307
|
+
|
|
3308
|
+
# Restaurar mejor modelo encontrado
|
|
3309
|
+
model.load_state_dict(best_state)
|
|
3310
|
+
self.logger.info(
|
|
3311
|
+
f"Fase 4 completa. Mejor val_loss = {best_val_loss:.4f}")
|
|
3312
|
+
return {'refine_history': refine_history, 'best_val_loss': best_val_loss}
|
|
3313
|
+
|
|
3314
|
+
|
|
3315
|
+
# ============================================================================
|
|
3316
|
+
# PIPELINE V2: Curriculum + Progressive Sequence Length
|
|
3317
|
+
# ============================================================================
|
|
3318
|
+
|
|
3319
|
+
class TopoPhasePipelineV2:
|
|
3320
|
+
"""Pipeline with curriculum learning and progressive sequence length.
|
|
3321
|
+
|
|
3322
|
+
Replaces TopoPhasePipeline when --curriculum or --progressive-seq-len is set.
|
|
3323
|
+
Handles:
|
|
3324
|
+
- Text quality filtering before tokenization (via TextFilter)
|
|
3325
|
+
- Curriculum tiers (short/medium/all files)
|
|
3326
|
+
- Progressive MAX_SEQ_LEN across phases: 128->256->512
|
|
3327
|
+
- Tokens cached in memory for fast DataLoader rebuilding per phase
|
|
3328
|
+
"""
|
|
3329
|
+
|
|
3330
|
+
def __init__(self, config: 'TopoGPT2Config',
|
|
3331
|
+
train_tokens: np.ndarray, val_tokens: np.ndarray,
|
|
3332
|
+
tokenizer: 'BPETokenizer', logger: logging.Logger,
|
|
3333
|
+
curriculum_tiers: Optional[Dict[int, List[int]]] = None,
|
|
3334
|
+
progressive_seq: bool = True):
|
|
3335
|
+
self.config = config
|
|
3336
|
+
self.train_tokens = train_tokens
|
|
3337
|
+
self.val_tokens = val_tokens
|
|
3338
|
+
self.tokenizer = tokenizer
|
|
3339
|
+
self.logger = logger
|
|
3340
|
+
self.curriculum_tiers = curriculum_tiers
|
|
3341
|
+
self.progressive_seq = progressive_seq
|
|
3342
|
+
|
|
3343
|
+
def _build_dataloader(self, tokens: np.ndarray, seq_len: int,
|
|
3344
|
+
batch_size: int, shuffle: bool,
|
|
3345
|
+
tag: str = '') -> torch.utils.data.DataLoader:
|
|
3346
|
+
n_cpu = max(1, os.cpu_count() // 2)
|
|
3347
|
+
pin = 'cuda' in self.config.DEVICE
|
|
3348
|
+
ds = MappedTokenDataset(tokens, seq_len)
|
|
3349
|
+
dl = torch.utils.data.DataLoader(
|
|
3350
|
+
ds, batch_size=batch_size, shuffle=shuffle,
|
|
3351
|
+
num_workers=n_cpu, pin_memory=pin, drop_last=shuffle)
|
|
3352
|
+
self.logger.info(
|
|
3353
|
+
f"[{tag}] seq_len={seq_len} -> {len(ds):,} sequences, "
|
|
3354
|
+
f"bs={batch_size}, steps={len(dl)}")
|
|
3355
|
+
return dl
|
|
3356
|
+
|
|
3357
|
+
def _build_phases(self) -> Tuple[Tuple[int, int], ...]:
|
|
3358
|
+
if self.progressive_seq:
|
|
3359
|
+
return tuple(zip(
|
|
3360
|
+
self.config.PROGRESSIVE_SEQ_STEPS,
|
|
3361
|
+
self.config.PROGRESSIVE_SEQ_EPOCHS,
|
|
3362
|
+
))
|
|
3363
|
+
seq = self.config.MAX_SEQ_LEN
|
|
3364
|
+
return ((seq, self.config.EPOCHS),)
|
|
3365
|
+
|
|
3366
|
+
def run(self, run_prospect: bool = False,
|
|
3367
|
+
refine_epochs: int = 0,
|
|
3368
|
+
resume: bool = False,
|
|
3369
|
+
prospect_steps: int = 50,
|
|
3370
|
+
probe_seeds: int = 3,
|
|
3371
|
+
seed_start: int = 1) -> 'CurriculumTrainer':
|
|
3372
|
+
cfg = self.config
|
|
3373
|
+
logger = self.logger
|
|
3374
|
+
|
|
3375
|
+
if run_prospect:
|
|
3376
|
+
logger.info(
|
|
3377
|
+
"Prospecting disabled in V2 pipeline (use standard pipeline for that).")
|
|
3378
|
+
|
|
3379
|
+
set_seed(cfg.RANDOM_SEED, cfg.DEVICE)
|
|
3380
|
+
|
|
3381
|
+
model = TopoGPT2(cfg)
|
|
3382
|
+
trainer = CurriculumTrainer(model, cfg, tokenizer=self.tokenizer)
|
|
3383
|
+
trainer.cache_tokens('train', self.train_tokens)
|
|
3384
|
+
trainer.cache_tokens('val', self.val_tokens)
|
|
3385
|
+
|
|
3386
|
+
if resume:
|
|
3387
|
+
loaded = trainer.resume()
|
|
3388
|
+
if not loaded:
|
|
3389
|
+
logger.info("No checkpoint, starting from scratch.")
|
|
3390
|
+
|
|
3391
|
+
phases = self._build_phases()
|
|
3392
|
+
logger.info(
|
|
3393
|
+
f"ProgressiveSeqLen={self.progressive_seq} phases={len(phases)} "
|
|
3394
|
+
f"curriculum={self.curriculum_tiers is not None}")
|
|
3395
|
+
|
|
3396
|
+
for phase_idx, (seq_len, n_epochs) in enumerate(phases):
|
|
3397
|
+
logger.info(
|
|
3398
|
+
f"[Phase {phase_idx}] seq_len={seq_len}, epochs={n_epochs}")
|
|
3399
|
+
cfg.MAX_SEQ_LEN = seq_len
|
|
3400
|
+
|
|
3401
|
+
if self.curriculum_tiers is not None:
|
|
3402
|
+
short = cfg.CURRICULUM_SHORT_LINES
|
|
3403
|
+
med = cfg.CURRICULUM_MED_LINES
|
|
3404
|
+
tier_map = {
|
|
3405
|
+
0: (0, short),
|
|
3406
|
+
1: (short + 1, med),
|
|
3407
|
+
2: (med + 1, 999_999_999),
|
|
3408
|
+
}
|
|
3409
|
+
tier_file_counts = {
|
|
3410
|
+
t: len(files) for t, files in self.curriculum_tiers.items()
|
|
3411
|
+
}
|
|
3412
|
+
phase_tier = min(phase_idx, 2)
|
|
3413
|
+
logger.info(
|
|
3414
|
+
f"[Phase {phase_idx}] Curriculum tier {phase_tier} "
|
|
3415
|
+
f"({tier_file_counts.get(phase_tier,0):,} files), "
|
|
3416
|
+
f"range: {tier_map[phase_tier]}")
|
|
3417
|
+
|
|
3418
|
+
train_dl = self._build_dataloader(
|
|
3419
|
+
self.train_tokens, seq_len, cfg.BATCH_SIZE, True, 'train')
|
|
3420
|
+
val_dl = self._build_dataloader(
|
|
3421
|
+
self.val_tokens, seq_len, cfg.BATCH_SIZE * 2, False, 'val')
|
|
3422
|
+
|
|
3423
|
+
prev_completed = trainer.completed_epochs
|
|
3424
|
+
|
|
3425
|
+
if self.curriculum_tiers is not None:
|
|
3426
|
+
for epoch_in_phase in range(n_epochs):
|
|
3427
|
+
cfg.EPOCHS = 1
|
|
3428
|
+
trainer.train(train_dl, val_dl)
|
|
3429
|
+
else:
|
|
3430
|
+
cfg.EPOCHS = n_epochs
|
|
3431
|
+
trainer.train(train_dl, val_dl)
|
|
3432
|
+
|
|
3433
|
+
logger.info(
|
|
3434
|
+
f"[Phase {phase_idx}] done. Epochs: {prev_completed} -> "
|
|
3435
|
+
f"{trainer.completed_epochs}")
|
|
3436
|
+
|
|
3437
|
+
if refine_epochs > 0:
|
|
3438
|
+
refiner = Phase4_AnnealingRefiner(trainer._base)
|
|
3439
|
+
refiner.refine(train_dl, val_dl, refine_epochs)
|
|
3440
|
+
|
|
3441
|
+
return trainer
|
|
3442
|
+
|
|
3443
|
+
|
|
3444
|
+
# ============================================================================
|
|
3445
|
+
# PIPELINE COMPLETO (5 FASES)
|
|
3446
|
+
# ============================================================================
|
|
3447
|
+
|
|
3448
|
+
class TopoPhasePipeline:
|
|
3449
|
+
"""
|
|
3450
|
+
Orquesta las 5 fases de entrenamiento segun main.py + Book.md.
|
|
3451
|
+
|
|
3452
|
+
Fases:
|
|
3453
|
+
0 Kernel ratio optimization (GOE-GUE spectral calibration)
|
|
3454
|
+
1 Batch size prospecting (temperatura del horno de cristalizacion)
|
|
3455
|
+
2 Seed mining (seleccion de semilla enfriante)
|
|
3456
|
+
3 Full training (entrenamiento principal con metricas)
|
|
3457
|
+
4 Annealing refinement (recocido simulado post-entrenamiento)
|
|
3458
|
+
|
|
3459
|
+
Las fases 0-2 son rapidas (prospecting). La fase 3 es el grueso.
|
|
3460
|
+
La fase 4 es opcional (--refine).
|
|
3461
|
+
|
|
3462
|
+
Para no ser prohibitivo:
|
|
3463
|
+
--prospect activa fases 0, 1, 2 antes del entrenamiento
|
|
3464
|
+
--refine-epochs N activa fase 4 con N epocas de annealing
|
|
3465
|
+
Sin flags: solo fase 3 (comportamiento original, identico a antes)
|
|
3466
|
+
"""
|
|
3467
|
+
|
|
3468
|
+
def __init__(self, config: 'TopoGPT2Config',
|
|
3469
|
+
train_dataset, val_dataset,
|
|
3470
|
+
tokenizer: 'BPETokenizer',
|
|
3471
|
+
logger: logging.Logger):
|
|
3472
|
+
self.config = config
|
|
3473
|
+
self.train_dataset = train_dataset
|
|
3474
|
+
self.val_dataset = val_dataset
|
|
3475
|
+
self.tokenizer = tokenizer
|
|
3476
|
+
self.logger = logger
|
|
3477
|
+
|
|
3478
|
+
def _make_dataloaders(self, batch_size: int = 0):
|
|
3479
|
+
bs = batch_size or self.config.BATCH_SIZE
|
|
3480
|
+
n_cpu = min(4, os.cpu_count() or 1)
|
|
3481
|
+
pin = 'cuda' in self.config.DEVICE
|
|
3482
|
+
train_dl = torch.utils.data.DataLoader(
|
|
3483
|
+
self.train_dataset, batch_size=bs, shuffle=True,
|
|
3484
|
+
num_workers=n_cpu, pin_memory=pin, drop_last=True)
|
|
3485
|
+
val_dl = torch.utils.data.DataLoader(
|
|
3486
|
+
self.val_dataset, batch_size=bs * 2, shuffle=False,
|
|
3487
|
+
num_workers=max(1, n_cpu // 2), pin_memory=pin, drop_last=True)
|
|
3488
|
+
return train_dl, val_dl
|
|
3489
|
+
|
|
3490
|
+
def run(self, run_prospect: bool = False,
|
|
3491
|
+
refine_epochs: int = 0,
|
|
3492
|
+
resume: bool = False,
|
|
3493
|
+
prospect_steps: int = 50,
|
|
3494
|
+
probe_seeds: int = 3,
|
|
3495
|
+
seed_start: int = 1) -> 'TopoGPT2Trainer':
|
|
3496
|
+
"""
|
|
3497
|
+
Ejecuta el pipeline completo.
|
|
3498
|
+
Retorna el trainer con el modelo entrenado.
|
|
3499
|
+
"""
|
|
3500
|
+
cfg = self.config
|
|
3501
|
+
|
|
3502
|
+
# ---- Fase 0: ratio de kernels espectrales ----
|
|
3503
|
+
if run_prospect:
|
|
3504
|
+
dl0, _ = self._make_dataloaders()
|
|
3505
|
+
p0 = Phase0_KernelOptimizer(cfg, self.logger)
|
|
3506
|
+
best_ratio = p0.optimize(dl0)
|
|
3507
|
+
cfg.SPECTRAL_KERNEL_INIT_SCALE = best_ratio
|
|
3508
|
+
self.logger.info(f"Fase 0 -> SPECTRAL_KERNEL_INIT_SCALE={best_ratio}")
|
|
3509
|
+
|
|
3510
|
+
# ---- Fase 1: batch size ----
|
|
3511
|
+
if run_prospect:
|
|
3512
|
+
candidates = [
|
|
3513
|
+
max(2, cfg.BATCH_SIZE // 2),
|
|
3514
|
+
cfg.BATCH_SIZE,
|
|
3515
|
+
min(cfg.BATCH_SIZE * 2, 32),
|
|
3516
|
+
]
|
|
3517
|
+
candidates = sorted(set(candidates))
|
|
3518
|
+
p1 = Phase1_BatchProspector(cfg, self.logger)
|
|
3519
|
+
best_bs = p1.prospect(candidates, self.train_dataset, prospect_steps)
|
|
3520
|
+
cfg.BATCH_SIZE = best_bs
|
|
3521
|
+
|
|
3522
|
+
# ---- Fase 2: seed mining ----
|
|
3523
|
+
if run_prospect:
|
|
3524
|
+
p2 = Phase2_SeedMiner(cfg, self.logger)
|
|
3525
|
+
best_seed = p2.mine(seed_start, probe_seeds,
|
|
3526
|
+
self.train_dataset, prospect_steps)
|
|
3527
|
+
cfg.RANDOM_SEED = best_seed
|
|
3528
|
+
set_seed(best_seed, cfg.DEVICE)
|
|
3529
|
+
self.logger.info(f"Fases 0-2 completas. "
|
|
3530
|
+
f"ratio={cfg.SPECTRAL_KERNEL_INIT_SCALE} "
|
|
3531
|
+
f"bs={cfg.BATCH_SIZE} seed={best_seed}")
|
|
3532
|
+
else:
|
|
3533
|
+
set_seed(cfg.RANDOM_SEED, cfg.DEVICE)
|
|
3534
|
+
|
|
3535
|
+
# ---- Fase 3: entrenamiento principal ----
|
|
3536
|
+
train_dl, val_dl = self._make_dataloaders()
|
|
3537
|
+
|
|
3538
|
+
# Ajustar arquitectura GQA si el checkpoint usa una config distinta
|
|
3539
|
+
if resume:
|
|
3540
|
+
tmp_mgr = CheckpointManager(cfg, self.logger)
|
|
3541
|
+
tmp_mgr.patch_config_for_resume(cfg)
|
|
3542
|
+
|
|
3543
|
+
model = TopoGPT2(cfg)
|
|
3544
|
+
trainer = TopoGPT2Trainer(model, cfg, tokenizer=self.tokenizer)
|
|
3545
|
+
|
|
3546
|
+
if resume:
|
|
3547
|
+
loaded = trainer.resume()
|
|
3548
|
+
if not loaded:
|
|
3549
|
+
self.logger.info("No habia checkpoint, iniciando desde cero.")
|
|
3550
|
+
elif trainer.ckpt_mgr.has_checkpoint():
|
|
3551
|
+
self.logger.info(
|
|
3552
|
+
f"AVISO: existe checkpoint en '{cfg.CHECKPOINT_DIR}/latest/'. "
|
|
3553
|
+
"Usa --resume para continuarlo.")
|
|
3554
|
+
|
|
3555
|
+
# Entrenamiento con metricas mecanisticas integradas
|
|
3556
|
+
trainer.train(train_dl, val_dl)
|
|
3557
|
+
|
|
3558
|
+
# ---- Fase 4: recocido simulado (opcional) ----
|
|
3559
|
+
if refine_epochs > 0:
|
|
3560
|
+
refiner = Phase4_AnnealingRefiner(trainer)
|
|
3561
|
+
refiner.refine(train_dl, val_dl, refine_epochs)
|
|
3562
|
+
|
|
3563
|
+
return trainer
|
|
3564
|
+
|
|
3565
|
+
|
|
3566
|
+
# ============================================================================
|
|
3567
|
+
# FUNCIÓN PRINCIPAL
|
|
3568
|
+
# ============================================================================
|
|
3569
|
+
|
|
3570
|
+
def main():
|
|
3571
|
+
parser = argparse.ArgumentParser(description='TopoGPT2 - Topological Quaternion LM')
|
|
3572
|
+
parser.add_argument('--scale', default='small',
|
|
3573
|
+
choices=['micro', 'small', 'medium', 'gpt2'],
|
|
3574
|
+
help='Escala del modelo')
|
|
3575
|
+
parser.add_argument('--corpus', default='directory',
|
|
3576
|
+
choices=['directory', 'file'],
|
|
3577
|
+
help='Corpus mode: directory walks tree, file reads single file')
|
|
3578
|
+
parser.add_argument('--corpus-root', default='/home/grisun0/src_note',
|
|
3579
|
+
help='Root directory for --corpus=directory')
|
|
3580
|
+
parser.add_argument('--corpus-file', default='', help='Text file path if --corpus=file')
|
|
3581
|
+
parser.add_argument('--corpus-min-chars', type=int, default=30,
|
|
3582
|
+
help='Minimum chars per file for --corpus=directory')
|
|
3583
|
+
parser.add_argument('--max-train-tokens', type=int, default=0,
|
|
3584
|
+
help='Max tokens to tokenize (0 = use config default: 500M)')
|
|
3585
|
+
parser.add_argument('--epochs', type=int, default=5)
|
|
3586
|
+
parser.add_argument('--batch-size', type=int, default=4)
|
|
3587
|
+
parser.add_argument('--seq-len', type=int, default=0,
|
|
3588
|
+
help='Longitud de secuencia (0 = usar preset de escala)')
|
|
3589
|
+
parser.add_argument('--lr', type=float, default=3e-4)
|
|
3590
|
+
parser.add_argument('--device', default='cuda' if torch.cuda.is_available() else 'cpu')
|
|
3591
|
+
parser.add_argument('--no-grad-ckpt', action='store_true',
|
|
3592
|
+
help='Desactivar gradient checkpointing (mas rapido, mas VRAM)')
|
|
3593
|
+
parser.add_argument('--generate', action='store_true', help='Generar texto tras entrenar')
|
|
3594
|
+
parser.add_argument('--prompt', default='Once upon a time', help='Prompt para generacion')
|
|
3595
|
+
parser.add_argument('--resume', action='store_true', help='Reanudar desde checkpoint')
|
|
3596
|
+
# Fases del pipeline (main.py)
|
|
3597
|
+
parser.add_argument('--prospect', action='store_true',
|
|
3598
|
+
help='Activar fases 0-2: optimizar kernel ratio, batch size y seed '
|
|
3599
|
+
'antes del entrenamiento principal (rapido, ~5-10 min)')
|
|
3600
|
+
parser.add_argument('--prospect-steps', type=int, default=50,
|
|
3601
|
+
help='Pasos por candidato en las fases de prospecting (default: 50)')
|
|
3602
|
+
parser.add_argument('--probe-seeds', type=int, default=3,
|
|
3603
|
+
help='Numero de semillas a evaluar en fase 2 (default: 3)')
|
|
3604
|
+
parser.add_argument('--seed-start', type=int, default=1,
|
|
3605
|
+
help='Primera semilla a probar en fase 2 (default: 1)')
|
|
3606
|
+
parser.add_argument('--refine-epochs', type=int, default=0,
|
|
3607
|
+
help='Epocas de recocido simulado (fase 4) tras el entrenamiento '
|
|
3608
|
+
'principal (default: 0 = desactivado)')
|
|
3609
|
+
parser.add_argument('--progressive-seq-len', action='store_true',
|
|
3610
|
+
help='Enable progressive sequence length warmup (128->256->512)')
|
|
3611
|
+
parser.add_argument('--curriculum', action='store_true',
|
|
3612
|
+
help='Enable curriculum learning (short->medium->all files)')
|
|
3613
|
+
parser.add_argument('--spec-decode', action='store_true',
|
|
3614
|
+
help='Enable speculative decoding for faster inference')
|
|
3615
|
+
parser.add_argument('--generate-synthetic', action='store_true',
|
|
3616
|
+
help='Generate synthetic dataset instead of training')
|
|
3617
|
+
parser.add_argument('--synth-provider', type=str, default='groq',
|
|
3618
|
+
choices=['groq', 'openrouter', 'ollama'],
|
|
3619
|
+
help='LLM provider for synthetic data generation')
|
|
3620
|
+
parser.add_argument('--synth-model', type=str, default='granite4.1:3b',
|
|
3621
|
+
help='Model for synthetic data generation')
|
|
3622
|
+
parser.add_argument('--synth-output', type=str, default='data/synthetic_dataset.jsonl',
|
|
3623
|
+
help='Output path for synthetic dataset JSONL')
|
|
3624
|
+
parser.add_argument('--synth-manifest', type=str, default='data/synthetic_manifest.json',
|
|
3625
|
+
help='Manifest path for synthetic dataset (resumable)')
|
|
3626
|
+
parser.add_argument('--synth-workers', type=int, default=4,
|
|
3627
|
+
help='Parallel workers for synthetic generation')
|
|
3628
|
+
parser.add_argument('--synth-api-key', type=str, default='',
|
|
3629
|
+
help='API key for LLM (or set GROQ_API_KEY env var)')
|
|
3630
|
+
parser.add_argument('--synth-max-files', type=int, default=0,
|
|
3631
|
+
help='Max files to generate from (0 = all)')
|
|
3632
|
+
args = parser.parse_args()
|
|
3633
|
+
|
|
3634
|
+
config = TopoGPT2Config(
|
|
3635
|
+
SCALE=args.scale,
|
|
3636
|
+
CORPUS=args.corpus,
|
|
3637
|
+
CORPUS_ROOT=args.corpus_root,
|
|
3638
|
+
CORPUS_FILE=args.corpus_file,
|
|
3639
|
+
CORPUS_MIN_FILE_CHARS=args.corpus_min_chars,
|
|
3640
|
+
EPOCHS=args.epochs,
|
|
3641
|
+
BATCH_SIZE=args.batch_size,
|
|
3642
|
+
LEARNING_RATE=args.lr,
|
|
3643
|
+
DEVICE=args.device,
|
|
3644
|
+
GRADIENT_CHECKPOINTING=not args.no_grad_ckpt,
|
|
3645
|
+
)
|
|
3646
|
+
if args.seq_len > 0:
|
|
3647
|
+
config.MAX_SEQ_LEN = args.seq_len
|
|
3648
|
+
if args.max_train_tokens > 0:
|
|
3649
|
+
config.MAX_TRAIN_TOKENS = args.max_train_tokens
|
|
3650
|
+
|
|
3651
|
+
logger = setup_logger('TopoGPT2.main', config.LOG_LEVEL)
|
|
3652
|
+
|
|
3653
|
+
# Tokenizador
|
|
3654
|
+
logger.info("Cargando tokenizador GPT-2 (tiktoken)...")
|
|
3655
|
+
tokenizer = BPETokenizer('gpt2')
|
|
3656
|
+
config.VOCAB_SIZE = tokenizer.vocab_size
|
|
3657
|
+
|
|
3658
|
+
# Corpus
|
|
3659
|
+
if config.CORPUS == 'file':
|
|
3660
|
+
if not config.CORPUS_FILE or not os.path.exists(config.CORPUS_FILE):
|
|
3661
|
+
logger.error(f"File not found: {config.CORPUS_FILE}")
|
|
3662
|
+
sys.exit(1)
|
|
3663
|
+
with open(config.CORPUS_FILE, 'r', encoding='utf-8', errors='replace') as f:
|
|
3664
|
+
full_text = f.read()
|
|
3665
|
+
train_split = int(len(full_text) * 0.95)
|
|
3666
|
+
train_tokens = _tokenize_text_to_memmap(
|
|
3667
|
+
full_text[:train_split], tokenizer,
|
|
3668
|
+
os.path.join(config.DATA_DIR, 'tokens_train.bin'),
|
|
3669
|
+
config.MAX_TRAIN_TOKENS)
|
|
3670
|
+
val_tokens = _tokenize_text_to_memmap(
|
|
3671
|
+
full_text[train_split:], tokenizer,
|
|
3672
|
+
os.path.join(config.DATA_DIR, 'tokens_val.bin'),
|
|
3673
|
+
config.MAX_TRAIN_TOKENS // 10)
|
|
3674
|
+
elif config.CORPUS == 'directory':
|
|
3675
|
+
manifest = FileManifest(config.CORPUS_ROOT, config.DATA_DIR, logger)
|
|
3676
|
+
all_paths = manifest.scan()
|
|
3677
|
+
if not all_paths:
|
|
3678
|
+
logger.error(f"No text files found in {config.CORPUS_ROOT}")
|
|
3679
|
+
sys.exit(1)
|
|
3680
|
+
logger.info(f"Total files found: {len(all_paths):,}")
|
|
3681
|
+
|
|
3682
|
+
text_filter = TextFilter(config, logger)
|
|
3683
|
+
filtered_paths = []
|
|
3684
|
+
for p in all_paths:
|
|
3685
|
+
if text_filter.filter_file(p, tokenizer) is not None:
|
|
3686
|
+
filtered_paths.append(p)
|
|
3687
|
+
filter_stats = text_filter.report()
|
|
3688
|
+
logger.info(f"Files after quality filter: {len(filtered_paths):,}")
|
|
3689
|
+
|
|
3690
|
+
if not filtered_paths:
|
|
3691
|
+
logger.error("All files filtered out. Check FILTER_* settings.")
|
|
3692
|
+
sys.exit(1)
|
|
3693
|
+
|
|
3694
|
+
rng = np.random.RandomState(config.RANDOM_SEED)
|
|
3695
|
+
rng.shuffle(filtered_paths)
|
|
3696
|
+
split = int(len(filtered_paths) * 0.95)
|
|
3697
|
+
train_paths = filtered_paths[:split]
|
|
3698
|
+
val_paths = filtered_paths[split:]
|
|
3699
|
+
logger.info(
|
|
3700
|
+
f"Train files: {len(train_paths):,} Val files: {len(val_paths):,}")
|
|
3701
|
+
|
|
3702
|
+
memtok = MemmapTokenizer(config.DATA_DIR, logger)
|
|
3703
|
+
train_max = config.MAX_TRAIN_TOKENS
|
|
3704
|
+
val_max = config.MAX_TRAIN_TOKENS // 10 if train_max > 0 else 10_000_000
|
|
3705
|
+
train_tokens = memtok.tokenize(
|
|
3706
|
+
train_paths, tokenizer, 'train',
|
|
3707
|
+
train_max, config.CORPUS_MIN_FILE_CHARS)
|
|
3708
|
+
val_tokens = memtok.tokenize(
|
|
3709
|
+
val_paths, tokenizer, 'val',
|
|
3710
|
+
val_max, config.CORPUS_MIN_FILE_CHARS)
|
|
3711
|
+
logger.info(
|
|
3712
|
+
f"Train tokens: {len(train_tokens):,} Val tokens: {len(val_tokens):,}")
|
|
3713
|
+
|
|
3714
|
+
curriculum_tiers = None
|
|
3715
|
+
if args.curriculum and config.CURRICULUM_ENABLED:
|
|
3716
|
+
logger.info("Building curriculum tiers...")
|
|
3717
|
+
curriculum_tiers = build_file_tiers(
|
|
3718
|
+
train_paths,
|
|
3719
|
+
config.CURRICULUM_SHORT_LINES,
|
|
3720
|
+
config.CURRICULUM_MED_LINES,
|
|
3721
|
+
)
|
|
3722
|
+
logger.info(
|
|
3723
|
+
f"Tier 0 (short): {len(curriculum_tiers.get(0,[])):,} files, "
|
|
3724
|
+
f"Tier 1 (med): {len(curriculum_tiers.get(1,[])):,} files, "
|
|
3725
|
+
f"Tier 2 (all): {len(curriculum_tiers.get(2,[])):,} files")
|
|
3726
|
+
else:
|
|
3727
|
+
logger.error(f"Unknown corpus mode: {config.CORPUS}")
|
|
3728
|
+
sys.exit(1)
|
|
3729
|
+
|
|
3730
|
+
if args.generate_synthetic:
|
|
3731
|
+
logger.info("Entering synthetic dataset generation mode...")
|
|
3732
|
+
synth_paths = train_paths + val_paths
|
|
3733
|
+
logger.info(f"Total files for synthetic generation: {len(synth_paths):,}")
|
|
3734
|
+
|
|
3735
|
+
import synthetic_dataset as synth_mod
|
|
3736
|
+
|
|
3737
|
+
api_key = args.synth_api_key or os.environ.get(
|
|
3738
|
+
"GROQ_API_KEY", os.environ.get("OPENROUTER_API_KEY", ""))
|
|
3739
|
+
if not api_key:
|
|
3740
|
+
logger.error("No API key. Set GROQ_API_KEY or OPENROUTER_API_KEY env var.")
|
|
3741
|
+
sys.exit(1)
|
|
3742
|
+
|
|
3743
|
+
backend = synth_mod.build_backend(
|
|
3744
|
+
args.synth_provider, args.synth_model)
|
|
3745
|
+
logger.info(f"Synthetic backend: {backend.name()}")
|
|
3746
|
+
|
|
3747
|
+
gen = synth_mod.SyntheticDatasetGenerator(
|
|
3748
|
+
backend=backend,
|
|
3749
|
+
output_path=args.synth_output,
|
|
3750
|
+
manifest_path=args.synth_manifest,
|
|
3751
|
+
logger=logger,
|
|
3752
|
+
max_workers=args.synth_workers,
|
|
3753
|
+
)
|
|
3754
|
+
max_files = args.synth_max_files if args.synth_max_files > 0 else len(synth_paths)
|
|
3755
|
+
logger.info(f"Processing up to {max_files:,} files with {args.synth_workers} workers...")
|
|
3756
|
+
|
|
3757
|
+
for i in range(0, min(len(synth_paths), max_files), 100):
|
|
3758
|
+
batch = synth_paths[i:i+100]
|
|
3759
|
+
results = gen.process_batch(batch)
|
|
3760
|
+
logger.info(
|
|
3761
|
+
f"Batch {i//100+1}: processed={results['processed']} "
|
|
3762
|
+
f"skipped={results['skipped']} failed={results['failed']}")
|
|
3763
|
+
|
|
3764
|
+
stats = gen.finish()
|
|
3765
|
+
logger.info(
|
|
3766
|
+
f"Synthetic generation complete: {stats['total_samples']:,} samples "
|
|
3767
|
+
f"written to {stats['output_path']}")
|
|
3768
|
+
sys.exit(0)
|
|
3769
|
+
|
|
3770
|
+
train_ds = MappedTokenDataset(train_tokens, config.MAX_SEQ_LEN)
|
|
3771
|
+
val_ds = MappedTokenDataset(val_tokens, config.MAX_SEQ_LEN)
|
|
3772
|
+
logger.info(
|
|
3773
|
+
f"Train sequences: {len(train_ds):,} Val sequences: {len(val_ds):,}")
|
|
3774
|
+
|
|
3775
|
+
# Pipeline de 5 fases
|
|
3776
|
+
pipeline = TopoPhasePipeline(config, train_ds, val_ds, tokenizer, logger)
|
|
3777
|
+
|
|
3778
|
+
use_progressive = args.progressive_seq_len and config.PROGRESSIVE_SEQ
|
|
3779
|
+
use_curriculum = args.curriculum and config.CURRICULUM_ENABLED
|
|
3780
|
+
|
|
3781
|
+
if use_progressive or use_curriculum:
|
|
3782
|
+
pipeline = TopoPhasePipelineV2(
|
|
3783
|
+
config, train_tokens, val_tokens, tokenizer, logger,
|
|
3784
|
+
curriculum_tiers=curriculum_tiers if use_curriculum else None,
|
|
3785
|
+
progressive_seq=use_progressive,
|
|
3786
|
+
)
|
|
3787
|
+
trainer = pipeline.run(
|
|
3788
|
+
run_prospect=args.prospect,
|
|
3789
|
+
refine_epochs=args.refine_epochs,
|
|
3790
|
+
resume=args.resume,
|
|
3791
|
+
prospect_steps=args.prospect_steps,
|
|
3792
|
+
probe_seeds=args.probe_seeds,
|
|
3793
|
+
seed_start=args.seed_start,
|
|
3794
|
+
)
|
|
3795
|
+
else:
|
|
3796
|
+
trainer = pipeline.run(
|
|
3797
|
+
run_prospect=args.prospect,
|
|
3798
|
+
refine_epochs=args.refine_epochs,
|
|
3799
|
+
resume=args.resume,
|
|
3800
|
+
prospect_steps=args.prospect_steps,
|
|
3801
|
+
probe_seeds=args.probe_seeds,
|
|
3802
|
+
seed_start=args.seed_start,
|
|
3803
|
+
)
|
|
3804
|
+
|
|
3805
|
+
# Historial final en results/ para inspeccion rapida
|
|
3806
|
+
results_dir = 'results_topogpt2'
|
|
3807
|
+
os.makedirs(results_dir, exist_ok=True)
|
|
3808
|
+
hist_path = os.path.join(results_dir, 'history.json')
|
|
3809
|
+
with open(hist_path, 'w', encoding='utf-8') as f:
|
|
3810
|
+
json.dump(trainer._current_state(), f, indent=2)
|
|
3811
|
+
logger.info(f"Estado completo guardado: {hist_path}")
|
|
3812
|
+
|
|
3813
|
+
# Generacion de muestra
|
|
3814
|
+
if args.generate:
|
|
3815
|
+
logger.info(f"Generando desde prompt: '{args.prompt}'")
|
|
3816
|
+
trainer.model.eval()
|
|
3817
|
+
prompt_tokens = tokenizer.encode(args.prompt)
|
|
3818
|
+
ids = torch.tensor([prompt_tokens], dtype=torch.long, device=config.DEVICE)
|
|
3819
|
+
gen_ids = trainer.model.generate(ids, max_new_tokens=200, temperature=0.8, top_k=50)
|
|
3820
|
+
logger.info(f"Generado:\n{tokenizer.decode(gen_ids[0].tolist())}")
|
|
3821
|
+
|
|
3822
|
+
|
|
3823
|
+
if __name__ == '__main__':
|
|
3824
|
+
main()
|