PowerMachine commited on
Commit
1f3d188
·
verified ·
1 Parent(s): 5455a95

V6.5-V2-metrics-FIX: cleanup batch — remove .pt files + duplicate root-level modules + obsolete modules (17 files)

Browse files
hyp_t.py DELETED
@@ -1,100 +0,0 @@
1
- """hyp_t.py — HypT: cabeça de hipótese (correção delta). Implementa o Lema 3.
2
-
3
- A Hipótese é treinada para cancelar o ruído de quantização W8A8:
4
- y_final = y_hat + delta
5
- onde delta = hyp_T(o).
6
-
7
- Diferenças vs. TrainT:
8
- - Recebe `o` opcionalmente destacado (stop_grad) para isolar o sinal de
9
- correção do gradiente principal
10
- - Mesma arquitetura (TransformerEncoder) — apenas o tratamento de gradiente
11
- difere
12
-
13
- Reaproveita a filosofia do HypothesisNetwork do v13.9.2 (16 hipóteses com
14
- mapeamento LoRA), mas simplificada: 1 única hipótese global (delta) em vez
15
- de 16 hipóteses paralelas. A multiplexação é feita pelo OrqCell + Lema 1.
16
-
17
- ============================================================================
18
- V6.4 — pgvector_lookup REMOVIDO (NÃO É MAIS NECESSÁRIO)
19
- ============================================================================
20
- Conforme directive do usuário:
21
- "pgvector_lookup não é mais necessário pela lógica do script seguinte"
22
-
23
- O KohonenLearningSystem (kohonen_refactored/kohonen_learning_system.py)
24
- contém o método find_bmu que realiza busca nearest-neighbor sobre o grid
25
- 4D do SOM (864 neurônios para grid (6,6,6,4)), substituindo qualquer
26
- lookup pgvector externo. A decisão de aplicar punição é delegada ao
27
- protocolo interno do KohonenLearningSystem (1ª punição → activate_hypothesis,
28
- 2ª punição → set_ewc_reference + reset).
29
-
30
- Consequentemente, HypT volta ao seu papel original: gerar a correção
31
- delta = hyp_T(o) sem consultar mapa externo. A integração com o SOM é
32
- feita no nível do trainer (KohonenLearningSystem.process_batch), não no
33
- nível da hipótese.
34
-
35
- Os contadores de punição (n_complete_skip, n_incomplete_apply, etc.)
36
- foram removidos pois não há mais decisão de skip baseada em pgvector.
37
- """
38
- from __future__ import annotations
39
-
40
- import torch
41
- import torch.nn as nn
42
-
43
-
44
- class HypT(nn.Module):
45
- """Transformer de hipótese: gera correção delta.
46
-
47
- Args:
48
- d_input: dimensão de entrada (saída do OrqCell = d_cache)
49
- d_model: dimensão interna
50
- nhead: nº de cabeças
51
- d_ff: dimensão do FFN
52
- output_dim: dimensão de saída (igual ao TrainT)
53
- num_layers: nº de camadas
54
- dropout: probabilidade de dropout
55
-
56
- Forward:
57
- o: (batch, d_input), stop_grad: bool → delta: (batch, output_dim)
58
-
59
- Se stop_grad=True, aplica o.detach() no início — Lema 3 (isolar correção
60
- do gradiente principal).
61
- """
62
-
63
- def __init__(
64
- self,
65
- d_input: int,
66
- d_model: int,
67
- nhead: int,
68
- d_ff: int,
69
- output_dim: int,
70
- num_layers: int = 2,
71
- dropout: float = 0.1,
72
- ):
73
- super().__init__()
74
- self.d_input = d_input
75
- self.d_model = d_model
76
- self.output_dim = output_dim
77
-
78
- self.proj_in = nn.Linear(d_input, d_model)
79
- encoder_layer = nn.TransformerEncoderLayer(
80
- d_model, nhead, d_ff, dropout=dropout, batch_first=True
81
- )
82
- self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
83
- self.proj_out = nn.Linear(d_model, output_dim)
84
-
85
- def forward(self, o: torch.Tensor, stop_grad: bool = True) -> torch.Tensor:
86
- """o: (batch, d_input) → delta: (batch, output_dim)
87
-
88
- V6.4: SEM consulta pgvector — delta é computado diretamente.
89
- A decisão de aplicar ou não a correção é delegada ao
90
- KohonenLearningSystem (que usa find_bmu internamente).
91
- """
92
- # Lema 3: stop_grad_hyp isola o sinal de correção
93
- if stop_grad:
94
- o = o.detach()
95
-
96
- x = o.unsqueeze(1) # (batch, 1, d_input)
97
- x = self.proj_in(x) # (batch, 1, d_model)
98
- x = self.encoder(x) # (batch, 1, d_model)
99
- delta = self.proj_out(x.squeeze(1)) # (batch, output_dim)
100
- return delta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
kohonen_learning_system.py DELETED
The diff for this file is too large to render. See raw diff
 
smoke_test_v65_v2_metrics_fix.py DELETED
@@ -1,320 +0,0 @@
1
- """smoke_test_v65_v2_metrics_fix.py — Validação rápida das correções V6.5-V2-metrics-FIX.
2
-
3
- Verifica:
4
- 1. Importação do KohonenLearningSystemV2 com params restaurados (HIDDEN=1024, VOCAB=16384)
5
- 2. Bug fix do _compute_classification_loss_with_som (gradiente flui)
6
- 3. Vetorização do train_hypotheses (sem OOM)
7
- 4. aggressive_cleanup() funciona
8
- 5. SOM metrics (8 métricas) computam sem erro
9
- 6. predict() retorna labels dinâmicos (não hardcoded)
10
- 7. FASE1 + FASE2 logic (enable_punishment=False/True)
11
- """
12
- from __future__ import annotations
13
-
14
- import os
15
- import sys
16
- import time
17
- import traceback
18
- from pathlib import Path
19
-
20
- # Setup paths
21
- PROJECT_ROOT = Path("/home/z/my-project")
22
- BIGRU_ROOT = PROJECT_ROOT / "BiGRU_T_version"
23
- SRC_ROOT = BIGRU_ROOT / "src"
24
- sys.path.insert(0, str(SRC_ROOT))
25
-
26
- # Activate xeon_runtime
27
- os.environ["V65_ENABLE_STREAMING"] = "1"
28
-
29
- print("=" * 80)
30
- print("V6.5-V2-metrics-FIX SMOKE TEST")
31
- print("=" * 80)
32
- print()
33
-
34
- # Test 1: Import
35
- print("[Test 1] Import KohonenLearningSystemV2...")
36
- try:
37
- from bigru_t.model.kohonen_learning_system import (
38
- KohonenLearningSystemV2,
39
- KohonenLearningSystem,
40
- KohonenSOM4D,
41
- HypothesisClassifier,
42
- HypothesisEnsemble,
43
- DeltaGenerator,
44
- SimpleBBPETokenizer,
45
- text_to_4d_vector,
46
- )
47
- from bigru_t.model.som_metrics import (
48
- SOMMetricHistory,
49
- compute_all_metrics,
50
- quantization_error,
51
- topological_error,
52
- kaski_lagus_error,
53
- explained_variance_share,
54
- topological_collapse,
55
- dead_neuron_rate,
56
- )
57
- print(" PASS: All imports successful")
58
- except Exception as e:
59
- print(f" FAIL: Import error: {e}")
60
- traceback.print_exc()
61
- sys.exit(1)
62
-
63
- # Test 2: Instantiate with restored (larger) parameters
64
- print("\n[Test 2] Instantiate KLS V2 with restored params (HIDDEN=1024, VOCAB=16384)...")
65
- try:
66
- # Use smaller grid for smoke test (faster), but full HIDDEN_DIM/VOCAB_SIZE
67
- kls = KohonenLearningSystemV2(
68
- vocab_size=16384, # restored
69
- hidden_dim=1024, # restored
70
- seq_len=8,
71
- som_grid=(6, 6, 6, 4), # 864 neurons (Kohonen architecture preserved)
72
- alpha0=0.1,
73
- sigma0=1.5,
74
- lambda_ewc=0.02,
75
- N_start=10,
76
- dim_choice="y",
77
- hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8],
78
- T_max=10000,
79
- enable_vqvae2=False, # disabled to save RAM
80
- enable_reasoning=False, # disabled to save RAM
81
- enable_w8a8=False, # disabled to save RAM
82
- enable_attention=True,
83
- attention_n_heads=8,
84
- # V2-dynamic restored params
85
- n_hypotheses=16, # restored
86
- max_n_hypotheses=32, # restored
87
- min_n_hypotheses=4,
88
- n_trials=3,
89
- min_n_trials=1,
90
- max_n_trials=6,
91
- hyp_train_steps=30, # restored
92
- min_hyp_train_steps=10,
93
- max_hyp_train_steps=80,
94
- hyp_lr=1e-4,
95
- hyp_hidden_dim=256, # restored
96
- loss_history_window=8,
97
- punishment_window=12,
98
- )
99
- assert kls.hidden_dim == 1024, f"hidden_dim={kls.hidden_dim}"
100
- assert kls.tokenizer.vocab_size == 16384, f"vocab={kls.tokenizer.vocab_size}"
101
- assert kls.n_hypotheses == 16, f"n_hyp={kls.n_hypotheses}"
102
- assert kls.max_n_hypotheses == 32, f"max_n_hyp={kls.max_n_hypotheses}"
103
- assert kls.hyp_train_steps == 30, f"hyp_steps={kls.hyp_train_steps}"
104
- assert kls.hyp_hidden_dim == 256, f"hyp_hidden={kls.hyp_hidden_dim}"
105
- assert hasattr(kls, "aggressive_cleanup"), "no aggressive_cleanup method"
106
- print(f" PASS: KLS V2 instantiated")
107
- print(f" hidden_dim={kls.hidden_dim}, vocab={kls.tokenizer.vocab_size}")
108
- print(f" n_hypotheses={kls.n_hypotheses}/{kls.max_n_hypotheses}")
109
- print(f" hyp_train_steps={kls.hyp_train_steps}, hyp_hidden_dim={kls.hyp_hidden_dim}")
110
- print(f" som_grid={kls.som_grid} ({kls.som_neuron_count} neurons)")
111
- print(f" has aggressive_cleanup: True")
112
- except Exception as e:
113
- print(f" FAIL: Instantiation error: {e}")
114
- traceback.print_exc()
115
- sys.exit(1)
116
-
117
- # Test 3: Add data + train SOM (CONHECIMENTO phase)
118
- print("\n[Test 3] Add data + train SOM (CONHECIMENTO phase)...")
119
- try:
120
- kls.tokenizer.fit([
121
- "o gato dorme", "a casa eh azul", "ele corre rapido",
122
- "ela canta uma musica", "o sol nasceu", "nos vamos viajar",
123
- "luva de pedreiro tavila", "lula reserva valor",
124
- "amazonas forca tarefa vitimas",
125
- ])
126
- # Add 30 samples (more than N_start=10)
127
- for i in range(3):
128
- kls.add_data(
129
- ["o gato dorme", "o cachorro corre", "a menina brinca",
130
- "o passaro voa", "o peixe nada", "a arvore cresce",
131
- "o livro esta na mesa", "a musica toca", "o tempo passa",
132
- "o mundo gira"],
133
- [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
134
- label_strings={0: "negative_class", 1: "positive_class"},
135
- dataset_name="test_ds",
136
- )
137
- assert len(kls.buffer_4d) >= 10, f"buffer={len(kls.buffer_4d)}"
138
- print(f" PASS: Added {len(kls.buffer_4d)} samples")
139
- # Train SOM
140
- started = kls.check_training_start()
141
- print(f" training_ready: {kls.training_ready}")
142
- if kls.training_ready:
143
- kls.train_som_on_buffer()
144
- kls._label_neurons()
145
- print(f" SOM trained, weights norm={kls.som.weights.norm().item():.4f}")
146
- except Exception as e:
147
- print(f" FAIL: {e}")
148
- traceback.print_exc()
149
- sys.exit(1)
150
-
151
- # Test 4: predict() returns dynamic labels (not gato/cachorro)
152
- print("\n[Test 4] predict() returns dynamic labels...")
153
- try:
154
- pred = kls.predict("o gato dorme", dataset_name="test_ds")
155
- assert pred in ["negative_class", "positive_class"], f"pred={pred}"
156
- assert pred != "gato" and pred != "cachorro", f"hardcoded: {pred}"
157
- print(f" PASS: predict returned '{pred}' (dynamic, not hardcoded)")
158
- except Exception as e:
159
- print(f" FAIL: {e}")
160
- traceback.print_exc()
161
- sys.exit(1)
162
-
163
- # Test 5: activate_hypothesis (trains classifier)
164
- print("\n[Test 5] activate_hypothesis (train classifier)...")
165
- try:
166
- kls.activate_hypothesis()
167
- assert kls.classifier_trained, "classifier not trained"
168
- print(f" PASS: Classifier trained ({kls.classifier is not None})")
169
- except Exception as e:
170
- print(f" FAIL: {e}")
171
- traceback.print_exc()
172
- sys.exit(1)
173
-
174
- # Test 6: train_hypotheses (memory-efficient vectorized version)
175
- print("\n[Test 6] train_hypotheses (memory-efficient vectorized)...")
176
- try:
177
- t0 = time.time()
178
- result = kls.train_hypotheses()
179
- elapsed_ms = (time.time() - t0) * 1000
180
- assert result["active"], f"not active: {result}"
181
- assert result["n_steps"] == 30, f"n_steps={result['n_steps']}"
182
- print(f" PASS: train_hypotheses completed in {elapsed_ms:.0f}ms")
183
- print(f" n_steps={result['n_steps']}, n_hypotheses={result['n_hypotheses']}")
184
- print(f" loss_initial={result['loss_initial']:.4f}, loss_final={result['loss_final']:.4f}")
185
- print(f" delta_scale_final={result['delta_scale_final']:.6f}")
186
- if "adaptation" in result:
187
- adapt = result["adaptation"]
188
- print(f" adaptation: adapted={adapt['adapted']}, rules={adapt['rules_fired']}")
189
- except Exception as e:
190
- print(f" FAIL: {e}")
191
- traceback.print_exc()
192
- sys.exit(1)
193
-
194
- # Test 7: SOM metrics (8 canonical metrics)
195
- print("\n[Test 7] SOM metrics (8 canonical)...")
196
- try:
197
- metrics = kls.compute_som_metrics()
198
- assert "quantization_error" in metrics
199
- assert "topological_error" in metrics
200
- assert "kaski_lagus_error" in metrics
201
- assert "explained_variance_share" in metrics
202
- assert "topological_collapse" in metrics
203
- assert "dead_neuron_rate" in metrics
204
- assert "qe_stagnation" in metrics
205
- assert "neighborhood_crossing" in metrics
206
- assert "overall_health" in metrics
207
- print(f" PASS: All 8 SOM metrics computed")
208
- print(f" QE={metrics['quantization_error']:.4f}")
209
- print(f" TE={metrics['topological_error']:.4f}")
210
- print(f" KL={metrics['kaski_lagus_error']:.4f}")
211
- print(f" VE={metrics['explained_variance_share']:.4f}")
212
- print(f" dead_neuron_rate={metrics['dead_neuron_rate'].get('dead_neuron_rate', 0):.3f}")
213
- print(f" collapse_severity={metrics['topological_collapse'].get('severity', '?')}")
214
- print(f" overall_health={metrics['overall_health']}")
215
- print(f" n_failure_indicators={metrics['n_failure_indicators']}")
216
- except Exception as e:
217
- print(f" FAIL: {e}")
218
- traceback.print_exc()
219
- sys.exit(1)
220
-
221
- # Test 8: select_best_delta (vectorized)
222
- print("\n[Test 8] select_best_delta (3 trials × 16 hypotheses)...")
223
- try:
224
- t0 = time.time()
225
- best_delta, best_acc, info = kls.select_best_delta()
226
- elapsed_ms = (time.time() - t0) * 1000
227
- assert info["active"], f"not active: {info}"
228
- assert info["n_trials"] == 3, f"n_trials={info['n_trials']}"
229
- assert info["n_hypotheses"] == 16, f"n_hyp={info['n_hypotheses']}"
230
- print(f" PASS: select_best_delta completed in {elapsed_ms:.0f}ms")
231
- print(f" best_acc={best_acc:.4f}")
232
- print(f" delta_norm={info['delta_norm']:.4f}")
233
- print(f" n_trials={info['n_trials']}, n_hypotheses={info['n_hypotheses']}")
234
- except Exception as e:
235
- print(f" FAIL: {e}")
236
- traceback.print_exc()
237
- sys.exit(1)
238
-
239
- # Test 9: process_batch_v2 (CONHECIMENTO + PUNIÇÃO)
240
- print("\n[Test 9] process_batch_v2 (CONHECIMENTO + PUNIÇÃO)...")
241
- try:
242
- # CONHECIMENTO
243
- result_conh = kls.process_batch_v2(
244
- ["o gato dorme", "o cachorro corre"],
245
- [0, 1],
246
- label_strings={0: "negative_class", 1: "positive_class"},
247
- dataset_name="test_ds",
248
- enable_punishment=False,
249
- )
250
- assert result_conh["phase"] == "CONHECIMENTO"
251
- print(f" PASS: CONHECIMENTO phase works: {result_conh['phase']}")
252
-
253
- # PUNIÇÃO
254
- result_pun = kls.process_batch_v2(
255
- ["o gato dorme", "o cachorro corre"],
256
- [0, 1],
257
- label_strings={0: "negative_class", 1: "positive_class"},
258
- dataset_name="test_ds",
259
- enable_punishment=True,
260
- )
261
- assert result_pun["phase"] == "PUNICAO"
262
- print(f" PASS: PUNIÇÃO phase works: {result_pun['phase']}")
263
- print(f" action: {result_pun.get('action', 'none')}")
264
- except Exception as e:
265
- print(f" FAIL: {e}")
266
- traceback.print_exc()
267
- sys.exit(1)
268
-
269
- # Test 10: aggressive_cleanup
270
- print("\n[Test 10] aggressive_cleanup()...")
271
- try:
272
- cleanup_result = kls.aggressive_cleanup()
273
- assert "rss_before_mb" in cleanup_result
274
- assert "rss_after_mb" in cleanup_result
275
- assert "gc_collected" in cleanup_result
276
- print(f" PASS: aggressive_cleanup() called")
277
- print(f" rss_before={cleanup_result['rss_before_mb']:.1f}MB")
278
- print(f" rss_after={cleanup_result['rss_after_mb']:.1f}MB")
279
- print(f" freed={cleanup_result['freed_mb']:.1f}MB")
280
- print(f" gc_collected={cleanup_result['gc_collected']}")
281
- except Exception as e:
282
- print(f" FAIL: {e}")
283
- traceback.print_exc()
284
- sys.exit(1)
285
-
286
- # Test 11: get_v2_metrics
287
- print("\n[Test 11] get_v2_metrics()...")
288
- try:
289
- v2_metrics = kls.get_v2_metrics()
290
- assert v2_metrics["version"] == "V2-dynamic"
291
- assert v2_metrics["n_hypotheses"] == 16
292
- print(f" PASS: get_v2_metrics() works")
293
- print(f" version={v2_metrics['version']}")
294
- print(f" n_hypotheses={v2_metrics['n_hypotheses']}")
295
- print(f" n_trials={v2_metrics['n_trials']}")
296
- print(f" hyp_train_steps={v2_metrics['hyp_train_steps']}")
297
- print(f" buffer_size={v2_metrics['buffer_size']}")
298
- if "dynamic_adaptation" in v2_metrics:
299
- da = v2_metrics["dynamic_adaptation"]
300
- print(f" n_adaptations={da['n_adaptations']}")
301
- print(f" loss_stats: slope={da['loss_stats']['slope']:.6f}, vol={da['loss_stats']['volatility']:.3f}")
302
- except Exception as e:
303
- print(f" FAIL: {e}")
304
- traceback.print_exc()
305
- sys.exit(1)
306
-
307
- # Summary
308
- print("\n" + "=" * 80)
309
- print("SMOKE TEST SUMMARY")
310
- print("=" * 80)
311
- print(" ALL TESTS PASSED")
312
- print(" - KLS V2 with restored params (HIDDEN=1024, VOCAB=16384) ✓")
313
- print(" - Memory-efficient train_hypotheses (vectorized) ✓")
314
- print(" - Bug fix: gradient flows through deltas → SOM → loss ✓")
315
- print(" - 8 SOM canonical metrics computed ✓")
316
- print(" - Dynamic labels (no hardcoded gato/cachorro) ✓")
317
- print(" - CONHECIMENTO + PUNIÇÃO phases ✓")
318
- print(" - aggressive_cleanup() works ✓")
319
- print("=" * 80)
320
- sys.exit(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
som_metrics.py DELETED
@@ -1,811 +0,0 @@
1
- """som_metrics.py — V6.5-V2-metrics: Métricas de aprendizado e indicadores de
2
- falha para Mapas Auto-Organizáveis (SOM / Redes de Kohonen).
3
-
4
- ============================================================================
5
- MÉTRICAS PRINCIPAIS DE APRENDIZADO
6
- ============================================================================
7
-
8
- 1.1. Erro de Quantização (Quantization Error - QE)
9
- Média das distâncias euclidianas entre cada vetor de entrada e o vetor
10
- de pesos de seu neurônio vencedor (BMU). Mede a resolução ou ajuste fino
11
- do mapa aos dados.
12
-
13
- QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
14
-
15
- 1.2. Erro Topológico (Topological Error - TE)
16
- Percentual de vetores de entrada cujos primeiro e segundo neurônios
17
- mais próximos (BMU e segunda BMU) NÃO são vizinhos diretos na grade
18
- de saída. Avalia preservação das relações geométricas.
19
-
20
- TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
21
-
22
- 1.3. Erro de Kaski-Lagus
23
- Combinação ponderada que penaliza tanto a falta de resolução do vetor
24
- de quantização quanto as quebras na continuidade topológica.
25
-
26
- KL = α * QE_norm + β * TE
27
- onde α=0.5, β=0.5 (pesos padrão)
28
-
29
- Variante implementada: KL = QE * (1 + TE) (penaliza QE quando TE é alto)
30
-
31
- 1.4. Variância Explicada (Share of Explained Variance)
32
- Fração da variabilidade total dos dados de entrada que é mapeada e
33
- representada pelos vetores de referência (pesos) dos nós da rede.
34
-
35
- VarExplained = 1 - Var(residual) / Var(total)
36
- onde residual = x_i - W_BMU(x_i)
37
-
38
- ============================================================================
39
- INDICADORES DE NÃO APRENDIZADO OU FALHA
40
- ============================================================================
41
-
42
- 2.1. Colapso Topológico (Topological Collapse)
43
- Ocorre quando todos os vetores de peso convergem para o mesmo ponto
44
- ou para uma linha reta. Indica perda total da dimensionalidade útil.
45
-
46
- Detecção:
47
- - std(weights) por dimensão < threshold (colapso total)
48
- - rank(weights reshaped) < 2 (todos num ponto)
49
- - rank < 3 (todos numa linha)
50
-
51
- 2.2. Taxa de Neurônios Mortos (Dead Neurons)
52
- Percentual de neurônios que NUNCA se tornam BMU para nenhum vetor.
53
-
54
- DeadRate = (n_neurons - n_active_neurons) / n_neurons
55
-
56
- Threshold: DeadRate > 0.5 indica problema.
57
-
58
- 2.3. Estagnação do Erro de Quantização
59
- QE estabiliza em patamar elevado ao longo das épocas. A taxa de
60
- aprendizado decaiu rápido demais antes da fase de ordenação global.
61
-
62
- Detecção: slope(QE) ≈ 0 E QE atual > 0.7 * max(QE histórico)
63
-
64
- 2.4. Cruzamento de Vizinhança (Neighborhood Crossing)
65
- Crescimento repentino ou oscilação crônica do TE nas épocas finais.
66
-
67
- Detecção:
68
- - std(TE_final_5_epochs) > 2 * std(TE_initial_5_epochs) (oscilação)
69
- - TE[-1] > TE[-5] (crescimento no final)
70
-
71
- ============================================================================
72
- AUTOR: V6.5-V2-metrics
73
- ============================================================================
74
- """
75
- from __future__ import annotations
76
-
77
- import math
78
- import logging
79
- from collections import defaultdict, deque
80
- from dataclasses import dataclass, field
81
- from typing import Any, Dict, List, Optional, Tuple
82
-
83
- import torch
84
- import torch.nn.functional as F
85
-
86
- logger = logging.getLogger(__name__)
87
-
88
-
89
- # ============================================================================
90
- # Dataclass para armazenar histórico de métricas
91
- # ============================================================================
92
- @dataclass
93
- class SOMMetricHistory:
94
- """Histórico deslizante de métricas SOM para detecção de estagnação e
95
- cruzamento de vizinhança."""
96
- qe_history: deque = field(default_factory=lambda: deque(maxlen=50))
97
- te_history: deque = field(default_factory=lambda: deque(maxlen=50))
98
- kl_history: deque = field(default_factory=lambda: deque(maxlen=50))
99
- var_explained_history: deque = field(default_factory=lambda: deque(maxlen=50))
100
- dead_neuron_rate_history: deque = field(default_factory=lambda: deque(maxlen=50))
101
- epoch: int = 0
102
-
103
- def record(self, metrics: Dict[str, Any]) -> None:
104
- """Registra métricas de uma época.
105
-
106
- Nota: `dead_neuron_rate` pode ser float (valor direto) ou Dict
107
- (saída de compute_all_metrics, com chave 'dead_neuron_rate').
108
- """
109
- self.qe_history.append(float(metrics.get("quantization_error", 0.0)))
110
- self.te_history.append(float(metrics.get("topological_error", 0.0)))
111
- self.kl_history.append(float(metrics.get("kaski_lagus_error", 0.0)))
112
- self.var_explained_history.append(
113
- float(metrics.get("explained_variance_share", 0.0))
114
- )
115
- # dead_neuron_rate pode ser float ou Dict
116
- dnr = metrics.get("dead_neuron_rate", 0.0)
117
- if isinstance(dnr, dict):
118
- dnr_val = float(dnr.get("dead_neuron_rate", 0.0))
119
- else:
120
- dnr_val = float(dnr)
121
- self.dead_neuron_rate_history.append(dnr_val)
122
- self.epoch += 1
123
-
124
- def to_dict(self) -> Dict[str, Any]:
125
- return {
126
- "epoch": int(self.epoch),
127
- "qe_history": list(self.qe_history),
128
- "te_history": list(self.te_history),
129
- "kl_history": list(self.kl_history),
130
- "var_explained_history": list(self.var_explained_history),
131
- "dead_neuron_rate_history": list(self.dead_neuron_rate_history),
132
- }
133
-
134
-
135
- # ============================================================================
136
- # Funções auxiliares
137
- # ============================================================================
138
- def _grid_neighbors_set(
139
- idx: Tuple[int, int, int, int],
140
- grid_shape: Tuple[int, int, int, int],
141
- ) -> set:
142
- """Retorna o conjunto de índices vizinhos diretos (6-connectivity em 4D)
143
- de um neurônio na grade."""
144
- i, j, k, l = idx
145
- I, J, K, L = grid_shape
146
- neighbors = set()
147
- for di, dj, dk, dl in [
148
- (1, 0, 0, 0), (-1, 0, 0, 0),
149
- (0, 1, 0, 0), (0, -1, 0, 0),
150
- (0, 0, 1, 0), (0, 0, -1, 0),
151
- (0, 0, 0, 1), (0, 0, 0, -1),
152
- ]:
153
- ni, nj, nk, nl = i + di, j + dj, k + dk, l + dl
154
- if 0 <= ni < I and 0 <= nj < J and 0 <= nk < K and 0 <= nl < L:
155
- neighbors.add((ni, nj, nk, nl))
156
- return neighbors
157
-
158
-
159
- def _find_bmu_and_second(
160
- x: torch.Tensor, weights: torch.Tensor
161
- ) -> Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int], float, float]:
162
- """Encontra BMU e 2ª BMU para um vetor x.
163
-
164
- Args:
165
- x: tensor [4] — vetor 4D de entrada.
166
- weights: tensor [I, J, K, L, 4] — pesos do SOM.
167
-
168
- Returns:
169
- (bmu_idx, second_bmu_idx, dist_bmu, dist_second)
170
- """
171
- grid_shape = weights.shape[:-1]
172
- flat_w = weights.view(-1, 4) # (I*J*K*L, 4)
173
- diff = flat_w - x.view(1, 4)
174
- dist_sq = torch.sum(diff * diff, dim=-1) # (I*J*K*L,)
175
-
176
- # Top-2 menores distâncias
177
- top2 = torch.topk(dist_sq, k=2, largest=False)
178
- flat_idx_bmu = int(top2.indices[0].item())
179
- flat_idx_2nd = int(top2.indices[1].item())
180
-
181
- # Unravel
182
- L = grid_shape[3]
183
- K = grid_shape[2]
184
- J = grid_shape[1]
185
-
186
- def unravel(flat_idx: int) -> Tuple[int, int, int, int]:
187
- i = flat_idx // (J * K * L)
188
- r = flat_idx % (J * K * L)
189
- j = r // (K * L)
190
- r = r % (K * L)
191
- k = r // L
192
- l = r % L
193
- return (i, j, k, l)
194
-
195
- bmu = unravel(flat_idx_bmu)
196
- second = unravel(flat_idx_2nd)
197
- dist_bmu = float(math.sqrt(max(top2.values[0].item(), 0.0)))
198
- dist_second = float(math.sqrt(max(top2.values[1].item(), 0.0)))
199
- return bmu, second, dist_bmu, dist_second
200
-
201
-
202
- # ============================================================================
203
- # Métricas principais (1.1 - 1.4)
204
- # ============================================================================
205
- def quantization_error(
206
- data: torch.Tensor, weights: torch.Tensor
207
- ) -> float:
208
- """1.1 Erro de Quantização (QE).
209
-
210
- QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
211
-
212
- Args:
213
- data: tensor [N, 4] — vetores de entrada.
214
- weights: tensor [I, J, K, L, 4] — pesos do SOM.
215
-
216
- Returns:
217
- QE (float). 0.0 se data vazio.
218
- """
219
- if data.numel() == 0:
220
- return 0.0
221
- flat_w = weights.view(-1, 4) # (M, 4)
222
- # distância de cada amostra a todos os neurônios
223
- # data: (N, 4), flat_w: (M, 4) -> (N, M)
224
- diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
225
- dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
226
- min_dist_sq, _ = torch.min(dist_sq, dim=1) # (N,)
227
- qe = torch.mean(torch.sqrt(min_dist_sq.clamp(min=0.0)))
228
- return float(qe.item())
229
-
230
-
231
- def topological_error(
232
- data: torch.Tensor, weights: torch.Tensor
233
- ) -> float:
234
- """1.2 Erro Topológico (TE).
235
-
236
- TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
237
-
238
- Dois neurônios são adjacentes se differem em exatamente uma coordenada
239
- por ±1 (6-connectivity em 4D).
240
-
241
- Args:
242
- data: tensor [N, 4]
243
- weights: tensor [I, J, K, L, 4]
244
-
245
- Returns:
246
- TE ∈ [0, 1]. 0.0 se data vazio.
247
- """
248
- if data.numel() == 0:
249
- return 0.0
250
- grid_shape = weights.shape[:-1]
251
- flat_w = weights.view(-1, 4)
252
- diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
253
- dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
254
-
255
- # Top-2 BMUs para cada amostra
256
- top2 = torch.topk(dist_sq, k=2, largest=False, dim=1)
257
- flat_bmu = top2.indices[:, 0] # (N,)
258
- flat_2nd = top2.indices[:, 1] # (N,)
259
-
260
- # Unravel
261
- L = grid_shape[3]
262
- K = grid_shape[2]
263
- J = grid_shape[1]
264
- I = grid_shape[0]
265
-
266
- def unravel_batch(flat_idx: torch.Tensor) -> torch.Tensor:
267
- """Retorna tensor (N, 4) com (i, j, k, l)."""
268
- i = flat_idx // (J * K * L)
269
- r = flat_idx % (J * K * L)
270
- j = r // (K * L)
271
- r = r % (K * L)
272
- k = r // L
273
- l = r % L
274
- return torch.stack([i, j, k, l], dim=1).long()
275
-
276
- bmu_idx = unravel_batch(flat_bmu) # (N, 4)
277
- second_idx = unravel_batch(flat_2nd) # (N, 4)
278
-
279
- # Adjacência: |Δcoord| soma = 1 (apenas uma coordenada muda por ±1)
280
- delta = (bmu_idx - second_idx).abs() # (N, 4)
281
- is_adjacent = (delta.sum(dim=1) == 1) & (delta.max(dim=1).values == 1)
282
-
283
- te = 1.0 - float(is_adjacent.float().mean().item())
284
- return te
285
-
286
-
287
- def kaski_lagus_error(
288
- data: torch.Tensor, weights: torch.Tensor, alpha: float = 0.5
289
- ) -> float:
290
- """1.3 Erro de Kaski-Lagus.
291
-
292
- Combinação ponderada de QE normalizado e TE.
293
-
294
- Variante implementada (Kaski & Lagus, 1999):
295
- KL = α * MQE_norm + (1-α) * TE
296
- onde MQE_norm = QE / mean(||x||) (normalização por escala dos dados).
297
-
298
- Uma variante alternativa multiplica:
299
- KL = QE * (1 + TE)
300
- que penaliza QE quando TE é alto (usada se use_multiplicative=True).
301
-
302
- Args:
303
- data: tensor [N, 4]
304
- weights: tensor [I, J, K, L, 4]
305
- alpha: peso do QE normalizado (default 0.5)
306
-
307
- Returns:
308
- KL (float).
309
- """
310
- if data.numel() == 0:
311
- return 0.0
312
- qe = quantization_error(data, weights)
313
- te = topological_error(data, weights)
314
- # Normalização: escala típica dos dados
315
- data_norm = float(torch.mean(torch.norm(data, dim=1)).item())
316
- qe_norm = qe / max(data_norm, 1e-8)
317
- kl = alpha * qe_norm + (1.0 - alpha) * te
318
- return float(kl)
319
-
320
-
321
- def explained_variance_share(
322
- data: torch.Tensor, weights: torch.Tensor
323
- ) -> float:
324
- """1.4 Variância Explicada (Share of Explained Variance).
325
-
326
- VarExplained = 1 - Var(residual) / Var(total)
327
- onde residual = x_i - W_BMU(x_i)
328
-
329
- Implementação: computamos a variância total dos dados (soma das variâncias
330
- por dimensão) e a variância dos resíduos. A fração explicada é:
331
-
332
- VE = 1 - Σ_d Var(residual_d) / Σ_d Var(data_d)
333
-
334
- Returns:
335
- VE ∈ [0, 1]. Próximo de 1 = boa representação.
336
- """
337
- if data.numel() == 0 or data.shape[0] < 2:
338
- return 0.0
339
- flat_w = weights.view(-1, 4)
340
- diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
341
- dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
342
- _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
343
- bmu_w = flat_w[bmu_flat] # (N, 4)
344
- residual = data - bmu_w # (N, 4)
345
-
346
- # Variância por dimensão (sem viés, ddof=0)
347
- var_data = torch.var(data, dim=0, unbiased=False) # (4,)
348
- var_resid = torch.var(residual, dim=0, unbiased=False) # (4,)
349
-
350
- total_var = float(torch.sum(var_data).item())
351
- resid_var = float(torch.sum(var_resid).item())
352
- if total_var < 1e-12:
353
- return 0.0
354
- ve = 1.0 - (resid_var / total_var)
355
- return float(max(0.0, min(1.0, ve)))
356
-
357
-
358
- # ============================================================================
359
- # Indicadores de falha (2.1 - 2.4)
360
- # ============================================================================
361
- def topological_collapse(weights: torch.Tensor) -> Dict[str, Any]:
362
- """2.1 Colapso Topológico (Topological Collapse).
363
-
364
- Detecta se os pesos colapsaram para um ponto (rank < 1) ou para uma linha
365
- (rank < 2). Usa SVD sobre os pesos reshaped (M, 4).
366
-
367
- Args:
368
- weights: tensor [I, J, K, L, 4]
369
-
370
- Returns:
371
- Dict com:
372
- - collapsed_to_point: bool (todos iguais)
373
- - collapsed_to_line: bool (variação só em 1 direção)
374
- - effective_rank: float (rank efetivo via razão de singular values)
375
- - singular_values: list[float]
376
- - severity: str ("none" | "line" | "point")
377
- """
378
- flat_w = weights.view(-1, 4).clone() # (M, 4)
379
- if flat_w.numel() == 0:
380
- return {"collapsed_to_point": False, "collapsed_to_line": False,
381
- "effective_rank": 0.0, "singular_values": [], "severity": "none"}
382
-
383
- # Centraliza
384
- flat_w_centered = flat_w - flat_w.mean(dim=0, keepdim=True)
385
-
386
- # V6.5-V2-metrics FIX: usa SVD com fallback robusto para matrizes
387
- # singulares/ill-conditioned. Se SVD falhar, usa np.linalg.eigh com
388
- # regularização (adiciona pequeno ruído diagonal).
389
- S_list: List[float] = []
390
- try:
391
- # Tentativa 1: SVD direto (mais estável que eigh)
392
- U, S, V = torch.linalg.svd(flat_w_centered, full_matrices=False)
393
- S_list = [float(s.item()) for s in S]
394
- except Exception:
395
- # Tentativa 2: eigendecomposição da matriz de covariância com regularização
396
- try:
397
- cov = flat_w_centered.t() @ flat_w_centered
398
- # Regularização: adiciona pequeno valor diagonal para estabilizar
399
- n = cov.shape[0]
400
- reg = 1e-6 * float(cov.diag().max().item()) if n > 0 else 1e-6
401
- cov_reg = cov + torch.eye(n) * reg
402
- S_eigh, _ = torch.linalg.eigh(cov_reg)
403
- S_eigh = S_eigh.flip(0) # descending order
404
- # Converte autovalores em "singular values" aproximados (sqrt)
405
- S_list = [float(math.sqrt(max(s.item(), 0.0))) for s in S_eigh]
406
- except Exception:
407
- # Tentativa 3: usa norma de Frobenius como aproximação
408
- s_frob = float(torch.norm(flat_w_centered).item())
409
- S_list = [s_frob, 0.0, 0.0, 0.0]
410
-
411
- if not S_list:
412
- return {"collapsed_to_point": False, "collapsed_to_line": False,
413
- "effective_rank": 0.0, "singular_values": [], "severity": "none"}
414
-
415
- s_max = max(S_list[0] if S_list else 0.0, 1e-12)
416
-
417
- # Effective rank: número de singular values > 1% do maior
418
- threshold = 0.01 * s_max
419
- n_significant = sum(1 for s in S_list if s > threshold)
420
- # Effective rank (float, via entropy)
421
- s_norm = [s / s_max for s in S_list]
422
- s_norm = [s for s in s_norm if s > 1e-10]
423
- if s_norm:
424
- p = torch.tensor(s_norm)
425
- p_norm = p / p.sum()
426
- entropy = -float(torch.sum(p_norm * torch.log(p_norm)).item())
427
- effective_rank = math.exp(entropy)
428
- else:
429
- effective_rank = 0.0
430
-
431
- collapsed_to_point = n_significant <= 1 and s_max < 1e-6
432
- collapsed_to_line = n_significant == 1 and s_max >= 1e-6
433
-
434
- if collapsed_to_point:
435
- severity = "point"
436
- elif collapsed_to_line:
437
- severity = "line"
438
- else:
439
- severity = "none"
440
-
441
- return {
442
- "collapsed_to_point": bool(collapsed_to_point),
443
- "collapsed_to_line": bool(collapsed_to_line),
444
- "effective_rank": float(effective_rank),
445
- "singular_values": S_list,
446
- "severity": severity,
447
- }
448
-
449
-
450
- def dead_neuron_rate(
451
- data: torch.Tensor, weights: torch.Tensor
452
- ) -> Dict[str, Any]:
453
- """2.2 Taxa de Neurônios Mortos (Dead Neurons).
454
-
455
- DeadRate = (n_neurons - n_active_neurons) / n_neurons
456
-
457
- Um neurônio é "ativo" se é BMU para pelo menos uma amostra.
458
-
459
- Args:
460
- data: tensor [N, 4]
461
- weights: tensor [I, J, K, L, 4]
462
-
463
- Returns:
464
- Dict com:
465
- - dead_neuron_rate: float ∈ [0, 1]
466
- - n_dead: int
467
- - n_active: int
468
- - n_total: int
469
- - dead_indices: list de (i,j,k,l)
470
- """
471
- n_total = weights.shape[0] * weights.shape[1] * weights.shape[2] * weights.shape[3]
472
- if data.numel() == 0:
473
- return {"dead_neuron_rate": 1.0, "n_dead": n_total, "n_active": 0,
474
- "n_total": n_total, "dead_indices": []}
475
-
476
- flat_w = weights.view(-1, 4)
477
- diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
478
- dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
479
- _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
480
-
481
- active_mask = torch.zeros(n_total, dtype=torch.bool)
482
- active_mask[bmu_flat] = True
483
- n_active = int(active_mask.sum().item())
484
- n_dead = n_total - n_active
485
-
486
- # Coleta índices mortos
487
- dead_flat = (~active_mask).nonzero(as_tuple=True)[0]
488
- L_dim = weights.shape[3]
489
- K_dim = weights.shape[2]
490
- J_dim = weights.shape[1]
491
- dead_indices = []
492
- for flat_idx in dead_flat.tolist():
493
- i = flat_idx // (J_dim * K_dim * L_dim)
494
- r = flat_idx % (J_dim * K_dim * L_dim)
495
- j = r // (K_dim * L_dim)
496
- r = r % (K_dim * L_dim)
497
- k = r // L_dim
498
- l = r % L_dim
499
- dead_indices.append((i, j, k, l))
500
-
501
- return {
502
- "dead_neuron_rate": float(n_dead / n_total) if n_total > 0 else 0.0,
503
- "n_dead": int(n_dead),
504
- "n_active": int(n_active),
505
- "n_total": int(n_total),
506
- "dead_indices": dead_indices[:50], # limita para não explodir JSON
507
- }
508
-
509
-
510
- def quantization_error_stagnation(
511
- history: SOMMetricHistory,
512
- high_threshold_frac: float = 0.7,
513
- slope_tolerance: float = 1e-4,
514
- window_size: int = 5,
515
- ) -> Dict[str, Any]:
516
- """2.3 Estagnação do Erro de Quantização.
517
-
518
- Detecta se o QE estabilizou em patamar elevado:
519
- - |slope(QE)| < slope_tolerance (estável)
520
- - QE_atual > high_threshold_frac * max(QE_history) (alto)
521
-
522
- Args:
523
- history: histórico de métricas SOM.
524
- high_threshold_frac: fração do QE máximo para considerar "alto".
525
- slope_tolerance: tolerância de slope para considerar "estável".
526
- window_size: nº de épocas recentes para regressão linear.
527
-
528
- Returns:
529
- Dict com:
530
- - is_stagnant: bool
531
- - current_qe: float
532
- - max_qe: float
533
- - slope: float
534
- - severity: str ("none" | "mild" | "severe")
535
- """
536
- qe_list = list(history.qe_history)
537
- if len(qe_list) < window_size + 2:
538
- return {"is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
539
- "slope": 0.0, "severity": "none", "n_epochs": len(qe_list)}
540
-
541
- # Regressão linear sobre as últimas window_size épocas
542
- recent = qe_list[-window_size:]
543
- n = len(recent)
544
- x_mean = (n - 1) / 2.0
545
- y_mean = sum(recent) / n
546
- num = sum((i - x_mean) * (y - y_mean) for i, y in enumerate(recent))
547
- den = sum((i - x_mean) ** 2 for i in range(n))
548
- slope = num / den if den > 0 else 0.0
549
-
550
- current_qe = float(qe_list[-1])
551
- max_qe = float(max(qe_list))
552
- is_high = current_qe > high_threshold_frac * max_qe
553
- is_stable = abs(slope) < slope_tolerance
554
-
555
- is_stagnant = is_stable and is_high
556
-
557
- if is_stagnant:
558
- if current_qe > 0.9 * max_qe:
559
- severity = "severe"
560
- else:
561
- severity = "mild"
562
- else:
563
- severity = "none"
564
-
565
- return {
566
- "is_stagnant": bool(is_stagnant),
567
- "current_qe": current_qe,
568
- "max_qe": max_qe,
569
- "slope": float(slope),
570
- "severity": severity,
571
- "n_epochs": len(qe_list),
572
- }
573
-
574
-
575
- def neighborhood_crossing(
576
- history: SOMMetricHistory,
577
- initial_window: int = 5,
578
- final_window: int = 5,
579
- growth_threshold: float = 1.5,
580
- ) -> Dict[str, Any]:
581
- """2.4 Cruzamento de Vizinhança (Neighborhood Crossing).
582
-
583
- Detecta crescimento repentino ou oscilação crônica do TE nas épocas
584
- finais:
585
- - std(TE_final) > growth_threshold * std(TE_initial) (oscilação cresceu)
586
- - TE[-1] > TE[-5] (crescimento no final)
587
-
588
- Args:
589
- history: histórico de métricas SOM.
590
- initial_window: nº de épocas iniciais para baseline.
591
- final_window: nº de épocas finais para comparação.
592
- growth_threshold: razão de std para considerar "crescimento crônico".
593
-
594
- Returns:
595
- Dict com:
596
- - detected: bool
597
- - te_initial_mean: float
598
- - te_final_mean: float
599
- - te_initial_std: float
600
- - te_final_std: float
601
- - std_ratio: float
602
- - is_oscillating: bool
603
- - is_growing: bool
604
- - severity: str ("none" | "mild" | "severe")
605
- """
606
- te_list = list(history.te_history)
607
- if len(te_list) < initial_window + final_window:
608
- return {"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
609
- "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
610
- "is_oscillating": False, "is_growing": False, "severity": "none",
611
- "n_epochs": len(te_list)}
612
-
613
- initial = te_list[:initial_window]
614
- final = te_list[-final_window:]
615
-
616
- init_mean = sum(initial) / len(initial)
617
- final_mean = sum(final) / len(final)
618
- init_var = sum((x - init_mean) ** 2 for x in initial) / len(initial)
619
- final_var = sum((x - final_mean) ** 2 for x in final) / len(final)
620
- init_std = math.sqrt(init_var)
621
- final_std = math.sqrt(final_var)
622
-
623
- std_ratio = final_std / max(init_std, 1e-8)
624
- is_oscillating = std_ratio > growth_threshold
625
- is_growing = final_mean > init_mean and (final_mean - init_mean) > 0.05
626
-
627
- detected = is_oscillating or is_growing
628
-
629
- if detected:
630
- if std_ratio > 2.0 * growth_threshold or is_growing:
631
- severity = "severe"
632
- else:
633
- severity = "mild"
634
- else:
635
- severity = "none"
636
-
637
- return {
638
- "detected": bool(detected),
639
- "te_initial_mean": float(init_mean),
640
- "te_final_mean": float(final_mean),
641
- "te_initial_std": float(init_std),
642
- "te_final_std": float(final_std),
643
- "std_ratio": float(std_ratio),
644
- "is_oscillating": bool(is_oscillating),
645
- "is_growing": bool(is_growing),
646
- "severity": severity,
647
- "n_epochs": len(te_list),
648
- }
649
-
650
-
651
- # ============================================================================
652
- # Função agregadora: compute_all_metrics
653
- # ============================================================================
654
- def compute_all_metrics(
655
- data: torch.Tensor,
656
- weights: torch.Tensor,
657
- history: Optional[SOMMetricHistory] = None,
658
- ) -> Dict[str, Any]:
659
- """Computa todas as métricas SOM de uma vez.
660
-
661
- Args:
662
- data: tensor [N, 4] — buffer de vetores 4D.
663
- weights: tensor [I, J, K, L, 4] — pesos do SOM.
664
- history: histórico de métricas (opcional, para estagnação e cruzamento).
665
-
666
- Returns:
667
- Dict com:
668
- - quantization_error: float
669
- - topological_error: float
670
- - kaski_lagus_error: float
671
- - explained_variance_share: float
672
- - topological_collapse: Dict
673
- - dead_neuron_rate: Dict
674
- - quantization_error_stagnation: Dict (se history fornecido)
675
- - neighborhood_crossing: Dict (se history fornecido)
676
- - overall_health: str ("healthy" | "warning" | "critical")
677
- - failure_indicators: list[str]
678
- """
679
- # V6.5-V2-metrics FIX: wrap entire computation in try/except para evitar
680
- # crashes fatais quando o SOM tem pesos degenerados (ex: todos iguais
681
- # após muito treinamento sem diversidade). Em caso de erro, retorna
682
- # métricas safe-default em vez de crashar o treino.
683
- try:
684
- return _compute_all_metrics_impl(data, weights, history)
685
- except Exception as e:
686
- logger.warning(
687
- f"[som_metrics] compute_all_metrics failed (using safe defaults): {e}"
688
- )
689
- return {
690
- "quantization_error": 0.0,
691
- "topological_error": 0.0,
692
- "kaski_lagus_error": 0.0,
693
- "explained_variance_share": 0.0,
694
- "topological_collapse": {
695
- "collapsed_to_point": False, "collapsed_to_line": False,
696
- "effective_rank": 0.0, "singular_values": [],
697
- "severity": "none", "error": str(e)[:200],
698
- },
699
- "dead_neuron_rate": {
700
- "dead_neuron_rate": 1.0, "n_dead": 0, "n_active": 0,
701
- "n_total": 0, "dead_indices": [], "error": str(e)[:200],
702
- },
703
- "qe_stagnation": {
704
- "is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
705
- "slope": 0.0, "severity": "none", "n_epochs": 0,
706
- },
707
- "neighborhood_crossing": {
708
- "detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
709
- "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
710
- "is_oscillating": False, "is_growing": False, "severity": "none",
711
- "n_epochs": 0,
712
- },
713
- "overall_health": "unknown",
714
- "failure_indicators": [f"metrics_computation_error: {str(e)[:100]}"],
715
- "n_failure_indicators": 1,
716
- "error": str(e)[:300],
717
- }
718
-
719
-
720
- def _compute_all_metrics_impl(
721
- data: torch.Tensor,
722
- weights: torch.Tensor,
723
- history: Optional[SOMMetricHistory] = None,
724
- ) -> Dict[str, Any]:
725
- """Implementação interna de compute_all_metrics (sem try/except wrapper)."""
726
- metrics: Dict[str, Any] = {}
727
-
728
- # 1.1 - 1.4: Métricas principais
729
- metrics["quantization_error"] = quantization_error(data, weights)
730
- metrics["topological_error"] = topological_error(data, weights)
731
- metrics["kaski_lagus_error"] = kaski_lagus_error(data, weights)
732
- metrics["explained_variance_share"] = explained_variance_share(data, weights)
733
-
734
- # 2.1: Colapso topológico
735
- collapse = topological_collapse(weights)
736
- metrics["topological_collapse"] = collapse
737
-
738
- # 2.2: Dead neurons
739
- dead = dead_neuron_rate(data, weights)
740
- metrics["dead_neuron_rate"] = dead
741
-
742
- # 2.3: Estagnação do QE (requer histórico)
743
- if history is not None and len(history.qe_history) > 5:
744
- metrics["qe_stagnation"] = quantization_error_stagnation(history)
745
- else:
746
- metrics["qe_stagnation"] = {
747
- "is_stagnant": False, "current_qe": metrics["quantization_error"],
748
- "max_qe": metrics["quantization_error"], "slope": 0.0,
749
- "severity": "none", "n_epochs": 0,
750
- }
751
-
752
- # 2.4: Cruzamento de vizinhança (requer histórico)
753
- if history is not None and len(history.te_history) > 10:
754
- metrics["neighborhood_crossing"] = neighborhood_crossing(history)
755
- else:
756
- metrics["neighborhood_crossing"] = {
757
- "detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
758
- "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
759
- "is_oscillating": False, "is_growing": False, "severity": "none",
760
- "n_epochs": 0,
761
- }
762
-
763
- # Avaliação de saúde geral
764
- failure_indicators: List[str] = []
765
- if collapse["collapsed_to_point"]:
766
- failure_indicators.append("topological_collapse_point")
767
- elif collapse["collapsed_to_line"]:
768
- failure_indicators.append("topological_collapse_line")
769
- if dead["dead_neuron_rate"] > 0.5:
770
- failure_indicators.append(f"dead_neuron_rate_high_{dead['dead_neuron_rate']:.2f}")
771
- if metrics["qe_stagnation"]["is_stagnant"]:
772
- failure_indicators.append(f"qe_stagnant_{metrics['qe_stagnation']['severity']}")
773
- if metrics["neighborhood_crossing"]["detected"]:
774
- failure_indicators.append(
775
- f"neighborhood_crossing_{metrics['neighborhood_crossing']['severity']}"
776
- )
777
- if metrics["explained_variance_share"] < 0.3:
778
- failure_indicators.append(
779
- f"low_explained_variance_{metrics['explained_variance_share']:.2f}"
780
- )
781
- if metrics["topological_error"] > 0.5:
782
- failure_indicators.append(
783
- f"high_topological_error_{metrics['topological_error']:.2f}"
784
- )
785
-
786
- if len(failure_indicators) >= 2:
787
- overall_health = "critical"
788
- elif len(failure_indicators) == 1:
789
- overall_health = "warning"
790
- else:
791
- overall_health = "healthy"
792
-
793
- metrics["failure_indicators"] = failure_indicators
794
- metrics["overall_health"] = overall_health
795
- metrics["n_failure_indicators"] = len(failure_indicators)
796
-
797
- return metrics
798
-
799
-
800
- __all__ = [
801
- "SOMMetricHistory",
802
- "quantization_error",
803
- "topological_error",
804
- "kaski_lagus_error",
805
- "explained_variance_share",
806
- "topological_collapse",
807
- "dead_neuron_rate",
808
- "quantization_error_stagnation",
809
- "neighborhood_crossing",
810
- "compute_all_metrics",
811
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/inference/langchain_adapter.py DELETED
@@ -1,218 +0,0 @@
1
- """langchain_adapter.py — Adapter para compatibilidade com LangChain (V6).
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/langchain_adapter.py.
4
-
5
- Permite usar o BiGRU_T_version como:
6
- - LLM chain (LLMChain)
7
- - Tool (BaseTool)
8
- - Memory (BaseMemory)
9
- - Agent (LLMSingleActionAgent)
10
-
11
- V6 melhorias vs V13.9.2:
12
- * Self-contained (sem dependência externa obrigatória)
13
- * Lazy import do langchain (só importa se usado)
14
- * Compatível com ToolMutationEngine e ToolAgentCoordinator
15
-
16
- REFERÊNCIAS:
17
- - LangChain: https://python.langchain.com
18
- - Tool pattern: https://python.langchain.com/docs/modules/agents/tools/
19
- """
20
- from __future__ import annotations
21
-
22
- import time
23
- from typing import Any, Dict, List, Optional, Callable
24
- from collections import deque
25
-
26
-
27
- # ═══════════════════════════════════════════════════════════════════════════
28
- # Adapter base (sem dependência do langchain)
29
- # ═══════════════════════════════════════════════════════════════════════════
30
- class LangchainAdapter:
31
- """Adapter para compatibilidade com LangChain (V6).
32
-
33
- Esta classe fornece interfaces compatíveis com LangChain, mas SEM exigir
34
- que o langchain esteja instalado. Se langchain estiver disponível, os
35
- métodos .as_langchain_*() retornarão objetos nativos do langchain;
36
- caso contrário, retornarão dicts/wrappers compatíveis.
37
-
38
- Args:
39
- pipeline: OptimizedPipeline (ou qualquer modelo com .forward())
40
- name: nome do modelo (default "BiGRU_T")
41
- description: descrição (default "")
42
- """
43
-
44
- def __init__(
45
- self,
46
- pipeline,
47
- name: str = "BiGRU_T",
48
- description: str = "BiGRU_T_version model with V6 features",
49
- ):
50
- self.pipeline = pipeline
51
- self.name = name
52
- self.description = description
53
- self._memory: deque = deque(maxlen=100) # memory buffer
54
-
55
- def __call__(
56
- self,
57
- prompt: str,
58
- max_tokens: int = 128,
59
- temperature: float = 1.0,
60
- stop: Optional[List[str]] = None,
61
- ) -> str:
62
- """Gera texto a partir de um prompt (interface tipo LLM).
63
-
64
- Args:
65
- prompt: string de entrada
66
- max_tokens: máx de tokens a gerar
67
- temperature: temperatura do softmax
68
- stop: lista de strings de parada (opcional)
69
-
70
- Returns:
71
- texto gerado
72
- """
73
- # Tokeniza (assumindo que o pipeline tem .tokenizer)
74
- if hasattr(self.pipeline, "tokenizer") and self.pipeline.tokenizer is not None:
75
- input_ids = self.pipeline.tokenizer.encode(prompt)
76
- # Converte para tensor
77
- import torch
78
- input_ids = torch.tensor([input_ids], dtype=torch.long)
79
- else:
80
- # Sem tokenizer — retorna prompt ecoado
81
- return f"[no tokenizer] {prompt}"
82
-
83
- # Gera
84
- import torch
85
- with torch.no_grad():
86
- generated = input_ids.clone()
87
- for _ in range(max_tokens):
88
- out = self.pipeline.forward(generated)
89
- logits = out["logits"][:, -1, :] # último token
90
- # Aplica temperatura
91
- logits = logits / max(temperature, 1e-6)
92
- # Top-k sampling (k=10)
93
- top_k = 10
94
- top_vals, top_idx = logits.topk(top_k, dim=-1)
95
- probs = top_vals.softmax(dim=-1)
96
- next_tok = top_idx.gather(
97
- -1, torch.multinomial(probs, 1)
98
- )
99
- generated = torch.cat([generated, next_tok], dim=-1)
100
- # Stop condition
101
- if stop:
102
- decoded = self.pipeline.tokenizer.decode(generated[0].tolist())
103
- for s in stop:
104
- if s in decoded:
105
- return decoded
106
-
107
- # Decodifica
108
- result = self.pipeline.tokenizer.decode(generated[0].tolist())
109
- # Atualiza memory
110
- self._memory.append({"prompt": prompt, "response": result})
111
- return result
112
-
113
- def as_langchain_llm(self):
114
- """Retorna um LLM compatível com LangChain (se langchain disponível)."""
115
- try:
116
- from langchain_core.language_models import LLM
117
- from langchain_core.callbacks import CallbackManagerForLLMRun
118
-
119
- adapter = self
120
-
121
- class BiGRU_T_LLM(LLM):
122
- @property
123
- def _llm_type(self) -> str:
124
- return "bigru_t"
125
-
126
- def _call(
127
- self,
128
- prompt: str,
129
- stop: Optional[List[str]] = None,
130
- run_manager: Optional[CallbackManagerForLLMRun] = None,
131
- **kwargs,
132
- ) -> str:
133
- return adapter(prompt, stop=stop, **kwargs)
134
-
135
- return BiGRU_T_LLM()
136
- except ImportError:
137
- # langchain não disponível — retorna wrapper
138
- return {
139
- "type": "langchain_llm_fallback",
140
- "name": self.name,
141
- "description": self.description,
142
- "call": lambda prompt, **kw: self(prompt, **kw),
143
- }
144
-
145
- def as_langchain_tool(self, name: Optional[str] = None, description: Optional[str] = None):
146
- """Retorna um Tool compatível com LangChain."""
147
- try:
148
- from langchain_core.tools import BaseTool
149
- from pydantic import BaseModel, Field
150
-
151
- adapter = self
152
- tool_name = name or self.name
153
- tool_desc = description or self.description
154
-
155
- class BiGRU_T_ToolInput(BaseModel):
156
- prompt: str = Field(description="Input prompt")
157
-
158
- class BiGRU_T_Tool(BaseTool):
159
- name: str = tool_name
160
- description: str = tool_desc
161
- args_schema: type = BiGRU_T_ToolInput
162
-
163
- def _run(self, prompt: str, **kwargs) -> str:
164
- return adapter(prompt, **kwargs)
165
-
166
- async def _arun(self, prompt: str, **kwargs) -> str:
167
- return adapter(prompt, **kwargs)
168
-
169
- return BiGRU_T_Tool()
170
- except ImportError:
171
- return {
172
- "type": "langchain_tool_fallback",
173
- "name": name or self.name,
174
- "description": description or self.description,
175
- "call": lambda prompt, **kw: self(prompt, **kw),
176
- }
177
-
178
- def as_langchain_memory(self, max_items: int = 100):
179
- """Retorna um Memory compatível com LangChain."""
180
- try:
181
- from langchain_core.memory import BaseMemory
182
- from pydantic import BaseModel
183
-
184
- adapter = self
185
-
186
- class BiGRU_T_Memory(BaseMemory):
187
- memory_key: str = "history"
188
- max_items: int = max_items
189
-
190
- @property
191
- def memory_variables(self) -> List[str]:
192
- return [self.memory_key]
193
-
194
- def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]:
195
- history = "\n".join(
196
- f"Human: {m['prompt']}\nAI: {m['response']}"
197
- for m in list(adapter._memory)[-self.max_items:]
198
- )
199
- return {self.memory_key: history}
200
-
201
- def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
202
- prompt = inputs.get("input", "")
203
- response = outputs.get("output", "")
204
- adapter._memory.append({"prompt": prompt, "response": response})
205
-
206
- def clear(self) -> None:
207
- adapter._memory.clear()
208
-
209
- return BiGRU_T_Memory()
210
- except ImportError:
211
- return {
212
- "type": "langchain_memory_fallback",
213
- "history": list(self._memory),
214
- "clear": self._memory.clear,
215
- }
216
-
217
-
218
- __all__ = ["LangchainAdapter"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/inference/optimized_pipeline.py DELETED
@@ -1,251 +0,0 @@
1
- """optimized_pipeline.py — Pipeline otimizado para BiGRU_T_version V6.
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/optimized_pipeline.py.
4
-
5
- Pipeline unificado que integra todos os componentes V6:
6
- - Tokenizer → Embedding
7
- - WindowContext (2M tokens compression)
8
- - Backbone (BiGRU_T modules + OrqCell com Kohonen 4D SOM)
9
- - MTP (Multi-Token Prediction)
10
- - MTPAdaptiveAttention
11
- - DynamicRepetitionPenalty
12
- - ReasoningEngine (opcional)
13
- - EWC (durante treino continuado)
14
- - RLHF Possibility Mapping (durante treino com feedback)
15
- - Tools (ToolMutationEngine + ToolAgentCoordinator)
16
-
17
- V6 melhorias vs V13.9.2:
18
- * Self-contained (sem TrustNeuralAgent externo)
19
- * Memory-efficient: ativa componentes sob demanda
20
- * Compatível com langchain (via LangchainAdapter)
21
- * Lazy loading: só instancia componentes se usados
22
-
23
- REFERÊNCIAS:
24
- - Pipeline pattern: https://refactoring.guru/design-patterns/pipeline
25
- - PyTorch Lightning pipeline (inspirado em)
26
- """
27
- from __future__ import annotations
28
-
29
- import torch
30
- import torch.nn as nn
31
- import torch.nn.functional as F
32
- from typing import Optional, Dict, List, Any, Tuple
33
-
34
-
35
- class OptimizedPipeline:
36
- """Pipeline otimizado V6 — orquestra todos os componentes.
37
-
38
- Esta classe NÃO herda de nn.Module (é um orquestrador puro).
39
- Mantém referências aos componentes e os aplica em ordem.
40
-
41
- Args:
42
- model: BiGRU_T UnifiedModel (backbone)
43
- tokenizer: tokenizer BBPE
44
- window_context: WindowContext (opcional, para 2M tokens)
45
- mtp: MTPModule (opcional)
46
- mtp_adaptive_attn: MTPAdaptiveAttention (opcional)
47
- rep_penalty: DynamicRepetitionPenalty (opcional)
48
- reasoning_engine: ReasoningEngine (opcional)
49
- ewc: ElasticWeightConsolidation (opcional)
50
- rlhf_mapping: LearnablePossibilityMapping (opcional)
51
- tool_engine: ToolMutationEngine (opcional)
52
- tool_agent: ToolAgentCoordinator (opcional)
53
- """
54
-
55
- def __init__(
56
- self,
57
- model: nn.Module,
58
- tokenizer=None,
59
- window_context: Optional[nn.Module] = None,
60
- mtp: Optional[nn.Module] = None,
61
- mtp_adaptive_attn: Optional[nn.Module] = None,
62
- rep_penalty: Optional[nn.Module] = None,
63
- reasoning_engine: Optional[nn.Module] = None,
64
- ewc: Optional[nn.Module] = None,
65
- rlhf_mapping: Optional[nn.Module] = None,
66
- tool_engine=None,
67
- tool_agent=None,
68
- ):
69
- self.model = model
70
- self.tokenizer = tokenizer
71
- self.window_context = window_context
72
- self.mtp = mtp
73
- self.mtp_adaptive_attn = mtp_adaptive_attn
74
- self.rep_penalty = rep_penalty
75
- self.reasoning_engine = reasoning_engine
76
- self.ewc = ewc
77
- self.rlhf_mapping = rlhf_mapping
78
- self.tool_engine = tool_engine
79
- self.tool_agent = tool_agent
80
-
81
- # Stats de execução
82
- self._stats = {
83
- "n_calls": 0,
84
- "n_tokens_processed": 0,
85
- "n_mtp_predictions": 0,
86
- "n_reasoning_calls": 0,
87
- "n_tool_calls": 0,
88
- "n_rep_penalty_applied": 0,
89
- }
90
-
91
- def forward(
92
- self,
93
- input_ids: torch.Tensor,
94
- targets: Optional[torch.Tensor] = None,
95
- use_mtp: bool = True,
96
- use_reasoning: bool = False,
97
- use_rep_penalty: bool = True,
98
- use_window_context: bool = False,
99
- return_aux: bool = False,
100
- ) -> dict:
101
- """Forward do pipeline otimizado.
102
-
103
- Args:
104
- input_ids: (batch, T) — tokens
105
- targets: (batch, T) — targets (opcional)
106
- use_mtp: ativa MTP (default True)
107
- use_reasoning: ativa ReasoningEngine (default False — custoso)
108
- use_rep_penalty: ativa DynamicRepetitionPenalty (default True)
109
- use_window_context: ativa WindowContext (default False)
110
- return_aux: retorna aux dict com stats
111
-
112
- Returns:
113
- dict com logits, loss (se targets), aux
114
- """
115
- self._stats["n_calls"] += 1
116
- self._stats["n_tokens_processed"] += input_ids.numel()
117
- aux: Dict[str, Any] = {}
118
-
119
- # 1. Embedding (via modelo)
120
- # Assumimos que o modelo cuida de embedding internamente
121
- # 2. WindowContext (opcional)
122
- if use_window_context and self.window_context is not None:
123
- # Precisa dos embeddings — para simplificar, pula se não disponível
124
- pass # integração futura
125
-
126
- # 3. Backbone forward
127
- # O UnifiedModel retorna (logits, delta) — 2 tensores
128
- # Sem hidden states expostos por padrão
129
- model_out = self.model(input_ids)
130
- if isinstance(model_out, tuple):
131
- logits = model_out[0]
132
- if len(model_out) >= 2:
133
- delta = model_out[1]
134
- aux["delta_norm"] = float(delta.norm().item()) if isinstance(delta, torch.Tensor) else 0.0
135
- else:
136
- delta = None
137
- else:
138
- logits = model_out
139
- delta = None
140
-
141
- # Hidden states não estão disponíveis diretamente — usa logits como proxy
142
- # para componentes que precisam de hidden (MTP, AdaptiveAttention, etc.)
143
- hidden = None # será setado se necessário via forward com return_aux
144
- # Tenta pegar aux do modelo se disponível
145
- if hasattr(self.model, "last_aux") and isinstance(self.model.last_aux, dict):
146
- aux.update(self.model.last_aux)
147
-
148
- # 4. MTPAdaptiveAttention (opcional) — requer hidden, desativado se hidden=None
149
- if use_mtp and self.mtp_adaptive_attn is not None and hidden is not None:
150
- mtp_entropy = aux.get("mtp_entropy")
151
- try:
152
- hidden, mtp_attn_aux = self.mtp_adaptive_attn(hidden, mtp_entropy)
153
- aux["mtp_adaptive_attn"] = mtp_attn_aux
154
- except Exception as e:
155
- aux["mtp_adaptive_attn_error"] = str(e)
156
-
157
- # 5. MTP (opcional) — requer hidden + token_embedding, desativado se não disponível
158
- mtp_result = None
159
- if use_mtp and self.mtp is not None and hidden is not None:
160
- # MTP precisa do embedding — assume que modelo tem .token_embedding
161
- if hasattr(self.model, "token_embedding"):
162
- self.mtp.set_embedding(self.model.token_embedding)
163
- try:
164
- mtp_result = self.mtp(hidden, logits, input_ids, targets)
165
- aux["mtp"] = mtp_result.get("aux", {})
166
- self._stats["n_mtp_predictions"] += 1
167
- if "loss" in mtp_result and mtp_result["loss"] is not None:
168
- aux["mtp_loss"] = mtp_result["loss"]
169
- except Exception as e:
170
- aux["mtp_error"] = str(e)
171
-
172
- # 6. ReasoningEngine (opcional, custoso) — requer hidden, desativado se não disponível
173
- if use_reasoning and self.reasoning_engine is not None and hidden is not None:
174
- try:
175
- reasoning_out = self.reasoning_engine(hidden, targets, return_chain=False)
176
- logits = reasoning_out["logits"]
177
- aux["reasoning"] = reasoning_out["aux"]
178
- if reasoning_out.get("loss") is not None:
179
- aux["reasoning_loss"] = reasoning_out["loss"]
180
- self._stats["n_reasoning_calls"] += 1
181
- except Exception as e:
182
- aux["reasoning_error"] = str(e)
183
-
184
- # 7. DynamicRepetitionPenalty (opcional)
185
- if use_rep_penalty and self.rep_penalty is not None:
186
- if hasattr(self.model, "token_embedding"):
187
- self.rep_penalty.set_embedding(self.model.token_embedding)
188
- try:
189
- # logits pode ser (B, V) ou (B, T, V) — adapta
190
- if logits.dim() == 2:
191
- # (B, V) → adiciona dimensão T
192
- logits_3d = logits.unsqueeze(1) # (B, 1, V)
193
- logits_3d = self.rep_penalty(logits_3d)
194
- logits = logits_3d.squeeze(1) # (B, V)
195
- else:
196
- logits = self.rep_penalty(logits)
197
- self._stats["n_rep_penalty_applied"] += 1
198
- except Exception as e:
199
- aux["rep_penalty_error"] = str(e)
200
-
201
- # 8. Loss (se targets)
202
- loss = None
203
- if targets is not None:
204
- vocab_size = logits.shape[-1]
205
- # Logits pode ser (B, V) ou (B, T, V) — adapta para cross_entropy
206
- if logits.dim() == 2:
207
- # (B, V) → usar apenas último token como target
208
- # targets: (B, T) → usar targets[:, -1] (último token)
209
- tgt_last = targets[:, -1] # (B,)
210
- main_loss = F.cross_entropy(logits, tgt_last, ignore_index=-100)
211
- else:
212
- # (B, T, V) → reshape padrão
213
- main_loss = F.cross_entropy(
214
- logits.reshape(-1, vocab_size),
215
- targets.reshape(-1),
216
- ignore_index=-100,
217
- )
218
- loss = main_loss
219
- # Adiciona MTP loss se disponível
220
- if mtp_result and mtp_result.get("loss") is not None:
221
- loss = loss + mtp_result["loss"]
222
- # Adiciona EWC penalty se disponível
223
- if self.ewc is not None and self.ewc.consolidated:
224
- try:
225
- ewc_pen = self.ewc.penalty()
226
- loss = loss + ewc_pen
227
- aux["ewc_penalty"] = float(ewc_pen.item())
228
- except Exception as e:
229
- aux["ewc_error"] = str(e)
230
- aux["main_loss"] = float(main_loss.item())
231
-
232
- result = {
233
- "logits": logits,
234
- "loss": loss,
235
- "aux": aux if return_aux else None,
236
- }
237
- return result
238
-
239
- def __call__(self, *args, **kwargs):
240
- return self.forward(*args, **kwargs)
241
-
242
- def stats(self) -> dict:
243
- """Retorna estatísticas do pipeline."""
244
- return self._stats.copy()
245
-
246
- def reset_stats(self) -> None:
247
- """Reseta estatísticas."""
248
- self._stats = {k: 0 for k in self._stats}
249
-
250
-
251
- __all__ = ["OptimizedPipeline"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/model/module_selector.py DELETED
@@ -1,100 +0,0 @@
1
- """module_selector.py — Lema 1: seleção de módulos via softmax com temperatura.
2
-
3
- Implementa o Lema 1 (Desacoplamento via atenção hierárquica).
4
-
5
- Mantém logits aprendíveis `module_logits ∈ R^K` para K módulos u8cell_T.
6
- A seleção é feita por:
7
-
8
- alpha = softmax(module_logits / T)
9
-
10
- onde T (temperatura) é controlada pelo MetaConfigurator (Lema 4).
11
-
12
- A regularização de entropia (opcional) penaliza distribuições uniformes:
13
-
14
- L_ent = -lambda_ent * sum(alpha * log(alpha + eps))
15
-
16
- forçando especialização (poucos módulos com peso alto).
17
-
18
- Reaproveita a filosofia da Confidence Gate do v13.9.2, mas:
19
- - Substitui o gate binário por softmax diferenciável
20
- - Adiciona regularização de entropia (não havia no v13.9.2)
21
- - A temperatura é hiperparâmetro aprendível (no v13.9.2 era fixa)
22
- """
23
- from __future__ import annotations
24
-
25
- import math
26
-
27
- import torch
28
- import torch.nn as nn
29
- import torch.nn.functional as F
30
-
31
-
32
- class ModuleSelector(nn.Module):
33
- """Seletor de módulos via softmax com temperatura (Lema 1).
34
-
35
- Args:
36
- max_modules: nº máximo de módulos K
37
- lambda_ent: peso da regularização de entropia (0 = desativada)
38
-
39
- Forward:
40
- temperature: float → alpha: (K,), entropy_reg: scalar
41
-
42
- O forward retorna os pesos alpha e a penalização de entropia (a ser
43
- somada à loss principal).
44
- """
45
-
46
- def __init__(self, max_modules: int, lambda_ent: float = 0.01):
47
- super().__init__()
48
- self.max_modules = max_modules
49
- self.lambda_ent = lambda_ent
50
-
51
- # V3 BUGFIX: init com pequeno ruído aleatório (em vez de zeros) para
52
- # quebrar a simetria. Com zeros, softmax(0/T) = uniforme, e o gradiente
53
- # w.r.t. module_logits é o mesmo para todos os módulos → nenhum aprende
54
- # a se diferenciar. Com ruído, cada módulo tem um leve viés inicial
55
- # que o otimizador pode amplificar.
56
- # Scale = 1/sqrt(K) para manter variance razoável.
57
- init_scale = 1.0 / (max_modules ** 0.5)
58
- self.module_logits = nn.Parameter(
59
- torch.randn(max_modules) * init_scale
60
- )
61
-
62
- def forward(self, temperature=1.0) -> tuple[torch.Tensor, torch.Tensor]:
63
- """Retorna (alpha, entropy_reg).
64
-
65
- alpha: (K,) — pesos de seleção dos módulos (soma = 1)
66
- entropy_reg: scalar — penalização de entropia (a somar à loss)
67
-
68
- V3 BUGFIX: temperature pode ser tensor (do MetaConfigurator) ou float.
69
- Usamos torch.clamp (diferenciável) em vez de max() (que pode quebrar
70
- o grafo computacional quando temperature é tensor 0-dim).
71
- """
72
- # Garante que temperature é um tensor (preserva grafo se for tensor)
73
- if not isinstance(temperature, torch.Tensor):
74
- temperature = torch.tensor(float(temperature), device=self.module_logits.device)
75
- # Softmax com temperatura (Lema 1) — torch.clamp é diferenciável
76
- # T alta → distribuição uniforme; T baixa → argmax (especialização)
77
- T_clamped = torch.clamp(temperature, min=1e-6)
78
- alpha = F.softmax(self.module_logits / T_clamped, dim=0) # (K,)
79
-
80
- # Regularização de entropia (minimizar H(alpha) → especialização)
81
- # H(alpha) = -sum(alpha * log(alpha + eps))
82
- # Para MINIMIZAR H, somamos +lambda_ent * H(alpha) à loss
83
- # (gradient descent reduz H)
84
- eps = 1e-8
85
- entropy = -(alpha * torch.log(alpha + eps)).sum()
86
- entropy_reg = self.lambda_ent * entropy # scalar
87
-
88
- return alpha, entropy_reg
89
-
90
- def get_active_modules(self, threshold: float = 0.01) -> int:
91
- """Retorna nº efetivo de módulos ativos (alpha > threshold)."""
92
- with torch.no_grad():
93
- alpha = F.softmax(self.module_logits, dim=0)
94
- active = (alpha > threshold).sum().item()
95
- return active
96
-
97
- def get_alpha_distribution(self) -> torch.Tensor:
98
- """Retorna a distribuição alpha atual (para logging)."""
99
- with torch.no_grad():
100
- return F.softmax(self.module_logits, dim=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/model/mtp.py DELETED
@@ -1,503 +0,0 @@
1
- """mtp.py — Multi-Token Prediction (MTP) V6.
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/mtp.py.
4
-
5
- MTP (DeepSeek 2024): em vez de prever 1 token por passo, prediz K tokens
6
- futuros simultaneamente. Em inference, gera K tokens por forward pass →
7
- speedup até K× (com speculative decoding).
8
-
9
- Algoritmo:
10
- 1. Cabeça principal: prevê token t+1 (como LLM padrão)
11
- 2. Cabeças MTP (K-1 cabeças auxiliares):
12
- - Cada cabeça k prevê token t+k+1
13
- - Cada cabeça recebe: hidden_state + embedding do token previsto pela
14
- cabeça anterior (concatenados)
15
- - hidden_state compartilhado com a cabeça principal
16
-
17
- Loss total: L = L_main + Σ_k α_k · L_k (α_k decrescente)
18
-
19
- V6 melhoria vs V13.9.2:
20
- * Vetorização total: K cabeças em 1 forward (não loop)
21
- * α_k aprendível (via softmax sobre logits_α)
22
- * Compatível com TripletPrototypeLoss (não conflita)
23
- * Memory-efficient: embeddings das K cabeças são computados lazy
24
-
25
- REFERÊNCIAS:
26
- - DeepSeek (2024). "DeepSeek-V3 Technical Report" — Section 5.3 MTP.
27
- - Leviathan et al. (2023). "Fast Inference from Transformers via Speculative Decoding".
28
- - Cai et al. (2024). "Medusa: Simple LLM Inference Acceleration".
29
- """
30
- from __future__ import annotations
31
-
32
- import torch
33
- import torch.nn as nn
34
- import torch.nn.functional as F
35
- from typing import Optional, Tuple
36
-
37
-
38
- class MTPHead(nn.Module):
39
- """Cabeça MTP — prevê token t+k+1 dado hidden_state e token t+k.
40
-
41
- Args:
42
- d_model: dimensão do hidden state
43
- vocab_size: tamanho do vocabulário
44
- k: índice da cabeça (k=1 é a primeira MTP, k=0 é a principal)
45
- """
46
-
47
- def __init__(
48
- self,
49
- d_model: int,
50
- vocab_size: int,
51
- k: int = 1,
52
- ):
53
- super().__init__()
54
- self.k = k
55
- self.d_model = d_model
56
- self.vocab_size = vocab_size
57
-
58
- # Input: hidden_state (d_model) + embedding do token anterior (d_model)
59
- # → projeção para d_model via Linear + ReLU + LayerNorm
60
- self.proj_in = nn.Linear(2 * d_model, d_model)
61
- self.act = nn.ReLU()
62
- self.norm = nn.LayerNorm(d_model)
63
- self.proj_out = nn.Linear(d_model, vocab_size)
64
-
65
- def forward(
66
- self,
67
- hidden: torch.Tensor,
68
- prev_token_emb: torch.Tensor,
69
- ) -> torch.Tensor:
70
- """hidden: (batch, T, d_model), prev_token_emb: (batch, T, d_model)
71
- → logits: (batch, T, vocab_size)
72
- """
73
- x = torch.cat([hidden, prev_token_emb], dim=-1) # (B, T, 2*d_model)
74
- x = self.proj_in(x)
75
- x = self.act(x)
76
- x = self.norm(x)
77
- logits = self.proj_out(x) # (B, T, vocab_size)
78
- return logits
79
-
80
-
81
- class MTPModule(nn.Module):
82
- """Multi-Token Prediction com K cabeças.
83
-
84
- Args:
85
- d_model: dimensão do hidden state
86
- vocab_size: tamanho do vocabulário
87
- K: nº total de tokens previstos por passo (default 4 = 1 principal + 3 MTP)
88
- alpha_init: peso inicial das cabeças MTP (default 0.5, decrescente)
89
- learnable_alpha: se True, α_k é aprendível (default True)
90
- """
91
-
92
- def __init__(
93
- self,
94
- d_model: int,
95
- vocab_size: int,
96
- K: int = 4,
97
- alpha_init: float = 0.5,
98
- learnable_alpha: bool = True,
99
- ):
100
- super().__init__()
101
- self.K = max(K, 1)
102
- self.d_model = d_model
103
- self.vocab_size = vocab_size
104
-
105
- # Cabeças MTP (K-1 cabeças auxiliares)
106
- if self.K > 1:
107
- self.mtp_heads = nn.ModuleList([
108
- MTPHead(d_model, vocab_size, k=k)
109
- for k in range(1, self.K)
110
- ])
111
- else:
112
- self.mtp_heads = nn.ModuleList()
113
-
114
- # α_k: pesos das cabeças MTP (aprendíveis via softmax)
115
- if learnable_alpha and self.K > 1:
116
- init_logits = torch.full((self.K - 1,), math.log(alpha_init))
117
- self.alpha_logits = nn.Parameter(init_logits)
118
- elif self.K > 1:
119
- self.register_buffer(
120
- "alpha_logits",
121
- torch.full((self.K - 1,), math.log(alpha_init)),
122
- )
123
-
124
- # Embedding para tokens previstos (compartilhado externamente)
125
- # Aqui mantemos uma referência que será setada via set_embedding()
126
- self._token_embedding: Optional[nn.Embedding] = None
127
-
128
- def set_embedding(self, emb: nn.Embedding) -> None:
129
- """Define o nn.Embedding compartilhado (deve ser chamado antes do forward)."""
130
- self._token_embedding = emb
131
-
132
- @property
133
- def alphas(self) -> torch.Tensor:
134
- """Pesos α_k normalizados via softmax."""
135
- if self.K <= 1:
136
- return torch.tensor([])
137
- return F.softmax(self.alpha_logits, dim=0)
138
-
139
- def forward(
140
- self,
141
- hidden: torch.Tensor,
142
- main_logits: torch.Tensor,
143
- input_ids: torch.Tensor,
144
- targets: Optional[torch.Tensor] = None,
145
- ) -> dict:
146
- """Forward MTP.
147
-
148
- Args:
149
- hidden: (batch, T, d_model) — hidden state do backbone
150
- main_logits: (batch, T, vocab_size) — logits da cabeça principal
151
- input_ids: (batch, T) — tokens de entrada (para embedding)
152
- targets: (batch, T) — targets para loss (opcional)
153
-
154
- Returns:
155
- dict com:
156
- - main_logits: (B, T, V)
157
- - mtp_logits: lista de (B, T, V) para cada cabeça k
158
- - loss: escalar (se targets fornecido) — main + Σ α_k · mtp_k
159
- - alphas: (K-1,) pesos normalizados
160
- - aux: dict com perdas individuais
161
- """
162
- B, T, _ = hidden.shape
163
- device = hidden.device
164
-
165
- # Resultado da cabeça principal já está em main_logits
166
- result = {
167
- "main_logits": main_logits,
168
- "mtp_logits": [],
169
- "alphas": self.alphas,
170
- "loss": None,
171
- "aux": {},
172
- }
173
-
174
- if self.K <= 1 or len(self.mtp_heads) == 0:
175
- # Sem MTP — só main
176
- if targets is not None:
177
- main_loss = F.cross_entropy(
178
- main_logits.reshape(-1, self.vocab_size),
179
- targets.reshape(-1),
180
- ignore_index=-100,
181
- )
182
- result["loss"] = main_loss
183
- result["aux"]["main_loss"] = float(main_loss.item())
184
- return result
185
-
186
- if self._token_embedding is None:
187
- raise RuntimeError(
188
- "MTPModule.set_embedding() deve ser chamado antes do forward"
189
- )
190
-
191
- # Para cada cabeça k (1 a K-1):
192
- # - Recebe hidden + embedding do token previsto pela cabeça k-1
193
- # - Em treino: usa target (teacher forcing)
194
- # - Em inference: usa argmax da cabeça anterior
195
- prev_logits = main_logits # (B, T, V)
196
- mtp_logits_list = []
197
- mtp_losses = []
198
-
199
- for k, head in enumerate(self.mtp_heads):
200
- # Token da cabeça anterior (em treino: target; em inference: argmax)
201
- if self.training and targets is not None:
202
- # Teacher forcing: usa target deslocado
203
- # Cabeça k prevê token t+k+1, então precisa do token t+k
204
- # target: (B, T) → shift por k+1
205
- prev_tokens = targets # simplificação: usa target atual
206
- else:
207
- prev_tokens = prev_logits.argmax(dim=-1) # (B, T)
208
-
209
- prev_emb = self._token_embedding(prev_tokens) # (B, T, d_model)
210
- logits_k = head(hidden, prev_emb) # (B, T, V)
211
- mtp_logits_list.append(logits_k)
212
- prev_logits = logits_k # para próxima cabeça
213
-
214
- if targets is not None:
215
- # Loss da cabeça k: prevê token t+k+1 dado contexto até t
216
- # Para simplicidade, usamos o mesmo target (aproximação)
217
- loss_k = F.cross_entropy(
218
- logits_k.reshape(-1, self.vocab_size),
219
- targets.reshape(-1),
220
- ignore_index=-100,
221
- )
222
- mtp_losses.append(loss_k)
223
-
224
- result["mtp_logits"] = mtp_logits_list
225
-
226
- if targets is not None:
227
- main_loss = F.cross_entropy(
228
- main_logits.reshape(-1, self.vocab_size),
229
- targets.reshape(-1),
230
- ignore_index=-100,
231
- )
232
- # Loss total = main + Σ α_k · mtp_k
233
- alphas = self.alphas # (K-1,)
234
- mtp_loss_total = sum(
235
- alphas[k] * mtp_losses[k]
236
- for k in range(len(mtp_losses))
237
- )
238
- total_loss = main_loss + mtp_loss_total
239
- result["loss"] = total_loss
240
- result["aux"]["main_loss"] = float(main_loss.item())
241
- result["aux"]["mtp_loss_total"] = float(mtp_loss_total.item())
242
- for k, l in enumerate(mtp_losses):
243
- result["aux"][f"mtp_loss_{k+1}"] = float(l.item())
244
- result["aux"][f"alpha_{k+1}"] = float(alphas[k].item())
245
-
246
- return result
247
-
248
-
249
- # ═══════════════════════════════════════════════════════════════════════════
250
- # MTPAdaptiveAttention — Attention adaptativa guiada por MTP
251
- # ═══════════════════════════════════════════════════════════════════════════
252
- class MTPAdaptiveAttention(nn.Module):
253
- """Atenção adaptativa guiada pela confiança das cabeças MTP.
254
-
255
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/mtp_adaptive_attention.py.
256
-
257
- Ideia: a confiança (entropy) das previsões MTP modula a mistura entre
258
- atenção local (window) e global (full sequence). Alta confiança MTP →
259
- mais peso em global; baixa confiança → mais peso em local.
260
-
261
- Args:
262
- d_model: dimensão do modelo
263
- nhead: nº de cabeças de atenção
264
- window_size: tamanho da janela local (default 256)
265
- """
266
-
267
- def __init__(
268
- self,
269
- d_model: int,
270
- nhead: int,
271
- window_size: int = 256,
272
- dropout: float = 0.1,
273
- ):
274
- super().__init__()
275
- self.d_model = d_model
276
- self.nhead = nhead
277
- self.window_size = window_size
278
-
279
- # Atenção full
280
- self.full_attn = nn.MultiheadAttention(
281
- d_model, nhead, dropout=dropout, batch_first=True
282
- )
283
- # Atenção local (windowed) — usa a mesma MultiheadAttention
284
- # mas com mask de janela
285
- self.local_attn = nn.MultiheadAttention(
286
- d_model, nhead, dropout=dropout, batch_first=True
287
- )
288
-
289
- # Gating: mistura local vs global baseado na confiança MTP
290
- # Recebe hidden + mtp_entropy → gate ∈ [0, 1]
291
- self.gate = nn.Sequential(
292
- nn.Linear(d_model + 1, d_model // 4),
293
- nn.ReLU(),
294
- nn.Linear(d_model // 4, 1),
295
- nn.Sigmoid(),
296
- )
297
-
298
- self.norm = nn.LayerNorm(d_model)
299
-
300
- def _make_window_mask(self, T: int, device: torch.device) -> torch.Tensor:
301
- """Cria mask de janela local (T, T) — 1 dentro da janela, 0 fora."""
302
- i = torch.arange(T, device=device).unsqueeze(1)
303
- j = torch.arange(T, device=device).unsqueeze(0)
304
- mask = (i - j).abs() <= self.window_size // 2
305
- return mask # (T, T)
306
-
307
- def forward(
308
- self,
309
- hidden: torch.Tensor,
310
- mtp_entropy: Optional[torch.Tensor] = None,
311
- ) -> Tuple[torch.Tensor, dict]:
312
- """hidden: (B, T, d), mtp_entropy: (B,) ou None.
313
-
314
- Returns:
315
- out: (B, T, d)
316
- aux: dict com gate_mean, local_weight, global_weight
317
- """
318
- B, T, D = hidden.shape
319
- device = hidden.device
320
-
321
- # Se mtp_entropy não fornecido, usa 0 (gate neutro)
322
- if mtp_entropy is None:
323
- mtp_entropy = torch.zeros(B, device=device)
324
- # Normaliza entropy para [0, 1] (aproximação)
325
- mtp_entropy_norm = (mtp_entropy / 10.0).clamp(0.0, 1.0) # (B,)
326
-
327
- # Gate: prob de usar global vs local
328
- gate_input = torch.cat([
329
- hidden.mean(dim=1), # (B, D)
330
- mtp_entropy_norm.unsqueeze(-1), # (B, 1)
331
- ], dim=-1) # (B, D+1)
332
- gate = self.gate(gate_input) # (B, 1) — prob de global
333
- gate_expanded = gate.unsqueeze(1).unsqueeze(1) # (B, 1, 1, 1) p/ broadcast
334
-
335
- # Atenção global (full)
336
- h_norm = self.norm(hidden)
337
- global_out, _ = self.full_attn(h_norm, h_norm, h_norm, need_weights=False)
338
-
339
- # Atenção local (windowed) — aplica mask
340
- window_mask = self._make_window_mask(T, device) # (T, T)
341
- # MultiheadAttention espera mask onde True = posição a mascarar
342
- attn_mask = ~window_mask # (T, T) — True onde NÃO atender
343
- # Expande para (B*nhead, T, T)
344
- local_out, _ = self.local_attn(
345
- h_norm, h_norm, h_norm,
346
- attn_mask=attr_mask_expand(attn_mask, B, self.nhead),
347
- need_weights=False,
348
- )
349
-
350
- # Mistura: out = gate · global + (1 - gate) · local
351
- gate_b = gate.unsqueeze(1) # (B, 1, 1)
352
- out = gate_b * global_out + (1.0 - gate_b) * local_out
353
- out = hidden + out # residual
354
-
355
- return out, {
356
- "gate_mean": float(gate.mean().item()),
357
- "local_weight": float((1.0 - gate).mean().item()),
358
- "global_weight": float(gate.mean().item()),
359
- "mtp_entropy_mean": float(mtp_entropy.mean().item()),
360
- }
361
-
362
-
363
- def attr_mask_expand(attn_mask: torch.Tensor, B: int, nhead: int) -> torch.Tensor:
364
- """Expande mask (T, T) para (B*nhead, T, T)."""
365
- return attn_mask.unsqueeze(0).expand(B * nhead, -1, -1)
366
-
367
-
368
- # ═══════════════════════════════════════════════════════════════════════════
369
- # DynamicRepetitionPenalty — Penalidade de Repetição Dinâmica
370
- # ═══════════════════════════════════════════════════════════════════════════
371
- class DynamicRepetitionPenalty(nn.Module):
372
- """Penalidade de Repetição Dinâmica (V6).
373
-
374
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/strong_repetition_penalty.py
375
- e repetition_penalty_optimizer.py.
376
-
377
- Estratégia:
378
- 1. Mantém buffer circular dos últimos W tokens gerados
379
- 2. Para cada token candidato, computa similaridade máxima com o buffer
380
- 3. Penalidade: E = -log(1 - σ(max_sim)) → barreira exponencial
381
- 4. Penalidade aplicada como logits adjustment: logits -= λ · E
382
- 5. λ otimizado via GP-UCB bayesiano (online)
383
-
384
- V6: vetorizado, suporta batch, buffer circular em GPU.
385
-
386
- Args:
387
- vocab_size: tamanho do vocabulário
388
- embedding_dim: dimensão dos embeddings (para similaridade)
389
- window_size: tamanho do buffer circular (default 64)
390
- lambda_init: λ inicial (default 1.0)
391
- lambda_min, lambda_max: limites de λ
392
- """
393
-
394
- def __init__(
395
- self,
396
- vocab_size: int,
397
- embedding_dim: int,
398
- window_size: int = 64,
399
- lambda_init: float = 1.0,
400
- lambda_min: float = 0.1,
401
- lambda_max: float = 10.0,
402
- ):
403
- super().__init__()
404
- self.vocab_size = vocab_size
405
- self.embedding_dim = embedding_dim
406
- self.window_size = window_size
407
-
408
- # λ como parâmetro aprendível (no log space para positividade)
409
- self.log_lambda = nn.Parameter(torch.tensor(math.log(lambda_init)))
410
- self.register_buffer("lambda_min", torch.tensor(lambda_min))
411
- self.register_buffer("lambda_max", torch.tensor(lambda_max))
412
-
413
- # Buffer circular dos últimos W tokens (por batch)
414
- # Mantém apenas IDs (não embeddings) para economizar RAM
415
- self.register_buffer(
416
- "token_buffer",
417
- torch.zeros(1, window_size, dtype=torch.long),
418
- )
419
- self.register_buffer("buffer_pos", torch.tensor(0, dtype=torch.long))
420
-
421
- # Embedding temporário para computar similaridade (será substituído)
422
- # Na prática, deve receber o embedding do modelo principal
423
- self._embedding: Optional[nn.Embedding] = None
424
-
425
- def set_embedding(self, emb: nn.Embedding) -> None:
426
- """Define o nn.Embedding compartilhado."""
427
- self._embedding = emb
428
-
429
- @property
430
- def lambda_val(self) -> torch.Tensor:
431
- """λ atual = exp(log_lambda), clampado para [lambda_min, lambda_max]."""
432
- return torch.exp(self.log_lambda).clamp(self.lambda_min, self.lambda_max)
433
-
434
- def update_buffer(self, new_tokens: torch.Tensor) -> None:
435
- """Atualiza o buffer circular com novos tokens.
436
-
437
- Args:
438
- new_tokens: (batch,) ou (batch, T) — novos tokens gerados
439
- """
440
- if new_tokens.dim() == 2:
441
- new_tokens = new_tokens.reshape(-1)
442
- W = self.window_size
443
- for tok in new_tokens:
444
- self.token_buffer[0, self.buffer_pos] = tok
445
- self.buffer_pos = (self.buffer_pos + 1) % W
446
-
447
- def compute_penalty(
448
- self,
449
- logits: torch.Tensor,
450
- ) -> torch.Tensor:
451
- """Computa logits ajustados com penalidade de repetição.
452
-
453
- Args:
454
- logits: (batch, T, vocab_size) ou (batch, vocab_size)
455
-
456
- Returns:
457
- adjusted_logits: mesmo shape que logits
458
- """
459
- if self._embedding is None:
460
- return logits # sem embedding, não aplica penalidade
461
-
462
- lam = self.lambda_val
463
- # Embeddings dos tokens no buffer: (W, embedding_dim)
464
- buffer_emb = self._embedding(self.token_buffer[0]) # (W, d)
465
- buffer_emb_norm = F.normalize(buffer_emb, dim=-1)
466
-
467
- # Para cada token candidato no vocab, computa max similarity com buffer
468
- vocab_emb = self._embedding.weight # (V, d)
469
- vocab_emb_norm = F.normalize(vocab_emb, dim=-1)
470
- # cosine sim: (V, W) — para cada token do vocab, similaridade com cada do buffer
471
- sim_matrix = vocab_emb_norm @ buffer_emb_norm.t() # (V, W)
472
- max_sim_per_vocab = sim_matrix.max(dim=-1).values # (V,)
473
- # σ(max_sim) ∈ [0.5, 1] (max_sim ∈ [0, 1] para embeddings normalizados)
474
- sigma_sim = torch.sigmoid(max_sim_per_vocab * 5.0) # amplifica
475
- # E = -log(1 - σ(max_sim)) → barreira exponencial
476
- # σ(max_sim) próximo de 1 → E → +inf (forte penalidade)
477
- # σ(max_sim) próximo de 0 → E → 0 (sem penalidade)
478
- penalty = -torch.log(1.0 - sigma_sim + 1e-8) # (V,)
479
-
480
- # Aplica penalidade aos logits
481
- adjusted = logits - lam * penalty.unsqueeze(0).unsqueeze(0)
482
- return adjusted
483
-
484
- def forward(self, logits: torch.Tensor) -> torch.Tensor:
485
- """Wrapper para compute_penalty."""
486
- return self.compute_penalty(logits)
487
-
488
- def extra_repr(self) -> str:
489
- return (
490
- f"vocab_size={self.vocab_size}, window={self.window_size}, "
491
- f"lambda={float(self.lambda_val.item()):.3f}"
492
- )
493
-
494
-
495
- import math # no topo, mas mantido aqui para compatibilidade
496
-
497
-
498
- __all__ = [
499
- "MTPHead",
500
- "MTPModule",
501
- "MTPAdaptiveAttention",
502
- "DynamicRepetitionPenalty",
503
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/model/train_t.py DELETED
@@ -1,63 +0,0 @@
1
- """train_t.py — TrainT: cabeça de predição principal.
2
-
3
- No v13.9.2, a predição principal é feita pela última camada cooperativa
4
- + projection ao vocabulário. No BiGRU_T_version, a predição é desacoplada
5
- da extração de features (feita pelo OrqCell + u8cell_T) e feita por um
6
- TransformerEncoder simples sobre a representação agregada `o`.
7
-
8
- Diferenças vs. HypT:
9
- - TrainT recebe `o` COM gradiente (caminho principal)
10
- - HypT recebe `o` SEM gradiente quando stop_grad_hyp=True (Lema 3)
11
- """
12
- from __future__ import annotations
13
-
14
- import torch
15
- import torch.nn as nn
16
-
17
-
18
- class TrainT(nn.Module):
19
- """Transformer encoder para predição principal.
20
-
21
- Args:
22
- d_input: dimensão de entrada (saída do OrqCell = d_cache)
23
- d_model: dimensão interna do Transformer
24
- nhead: nº de cabeças
25
- d_ff: dimensão do FFN
26
- output_dim: dimensão de saída (vocab_size para LM, num_classes para classificação)
27
- num_layers: nº de camadas do TransformerEncoder
28
- dropout: probabilidade de dropout
29
-
30
- Forward:
31
- o: (batch, d_input) → y_hat: (batch, output_dim)
32
- """
33
-
34
- def __init__(
35
- self,
36
- d_input: int,
37
- d_model: int,
38
- nhead: int,
39
- d_ff: int,
40
- output_dim: int,
41
- num_layers: int = 2,
42
- dropout: float = 0.1,
43
- ):
44
- super().__init__()
45
- self.d_input = d_input
46
- self.d_model = d_model
47
- self.output_dim = output_dim
48
-
49
- self.proj_in = nn.Linear(d_input, d_model)
50
- encoder_layer = nn.TransformerEncoderLayer(
51
- d_model, nhead, d_ff, dropout=dropout, batch_first=True
52
- )
53
- self.encoder = nn.TransformerEncoder(encoder_layer, num_layers)
54
- self.proj_out = nn.Linear(d_model, output_dim)
55
-
56
- def forward(self, o: torch.Tensor) -> torch.Tensor:
57
- """o: (batch, d_input) → y_hat: (batch, output_dim)"""
58
- # Adiciona dimensão de sequência (token único)
59
- x = o.unsqueeze(1) # (batch, 1, d_input)
60
- x = self.proj_in(x) # (batch, 1, d_model)
61
- x = self.encoder(x) # (batch, 1, d_model)
62
- y_hat = self.proj_out(x.squeeze(1)) # (batch, output_dim)
63
- return y_hat
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/model/window_context.py DELETED
@@ -1,371 +0,0 @@
1
- """window_context.py — Window Context para atenção de 2M tokens (V6).
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/token_compress.py
4
- e mtp_adaptive_attention.py.
5
-
6
- Estratégia para 2M tokens:
7
- 1. **ToMe Token Merging** (Bolya et al. 2023): reduz N → N-r tokens via
8
- bipartite soft matching. R-prune por camada. Complexity: O(N · d).
9
- 2. **KNN Indexer** (IVF-like): k-means centroids, n_probe busca. Reduz
10
- atenção O(N²) → O(N · n_probe · k_avg).
11
- 3. **Sliding Window**: divide sequência longa em janelas overlapped.
12
- 4. **Hierarchical Pooling**: 3 níveis de pooling (stride 4, 16, 64).
13
-
14
- V6 melhoria vs V13.9.2:
15
- * ToMe vetorizado (sem loop Python)
16
- * KNN Indexer com centroids EMA (não recalcula k-means a cada step)
17
- * Suporte a batch (V13.9.2 era single-sample)
18
- * Memory-budget adaptativo: ajusta r dinamicamente para caber em RAM
19
-
20
- REFERÊNCIAS:
21
- - Bolya et al. (2023). "Token Merging: Your ViT but Faster". ICLR.
22
- - Johnson et al. (2019). "Billion-scale similarity search with GPUs". IEEE TBD.
23
- - Ratner et al. (2023). "Speeding up BERT by 2x with ToMe".
24
- """
25
- from __future__ import annotations
26
-
27
- import math
28
- import torch
29
- import torch.nn as nn
30
- import torch.nn.functional as F
31
- from typing import Optional, Tuple
32
-
33
-
34
- # ═══════════════════════════════════════════════════════════════════════════
35
- # ToMeCompressor — Token Merging via Bipartite Soft Matching
36
- # ═══════════════════════════════════════════════════════════════════════════
37
- class ToMeCompressor(nn.Module):
38
- """Token Merging (Bolya 2023) — reduz N → N-r tokens.
39
-
40
- Algoritmo:
41
- 1. Divide tokens em 2 grupos A (par) e B (ímpar) alternados
42
- 2. Para cada token em A, encontra o mais similar em B (cosine sim)
43
- 3. Os r pares com maior similaridade são merged (média ponderada)
44
- 4. Tokens não merged de A e B são concatenados na saída
45
-
46
- V6: vetorizado, suporta batch, r adaptativo ao budget de memória.
47
-
48
- Args:
49
- r: nº de tokens a remover por forward (default 8)
50
- max_r: máximo de tokens removíveis (default 64)
51
- adaptive: se True, ajusta r baseado no tamanho do batch (default True)
52
- """
53
-
54
- def __init__(
55
- self,
56
- r: int = 8,
57
- max_r: int = 64,
58
- adaptive: bool = True,
59
- ):
60
- super().__init__()
61
- self.r = r
62
- self.max_r = max_r
63
- self.adaptive = adaptive
64
-
65
- def forward(
66
- self,
67
- x: torch.Tensor,
68
- r: Optional[int] = None,
69
- ) -> Tuple[torch.Tensor, dict]:
70
- """x: (batch, N, d) → (batch, N - r_merged, d).
71
-
72
- Returns:
73
- merged: (batch, N - r_merged, d)
74
- aux: dict com r_used, n_tokens_in, n_tokens_out
75
- """
76
- B, N, D = x.shape
77
- r_use = r if r is not None else self.r
78
- if self.adaptive:
79
- # Ajusta r para não remover mais que 50% dos tokens
80
- r_use = min(r_use, N // 2, self.max_r)
81
- if r_use <= 0 or N <= 2:
82
- return x, {"r_used": 0, "n_tokens_in": N, "n_tokens_out": N}
83
-
84
- # Passo 1: dividir tokens em A (par) e B (ímpar)
85
- a = x[:, ::2, :] # (B, N//2, D)
86
- b = x[:, 1::2, :] # (B, N - N//2, D) ou (B, N//2, D)
87
-
88
- # Passo 2: similaridade cosine entre A e B
89
- a_norm = F.normalize(a, dim=-1)
90
- b_norm = F.normalize(b, dim=-1)
91
- sim = a_norm @ b_norm.transpose(-1, -2) # (B, N_A, N_B)
92
-
93
- # Passo 3: para cada token em A, encontra o mais similar em B
94
- max_sim_a, max_idx_b = sim.max(dim=-1) # (B, N_A)
95
-
96
- # Passo 4: seleciona os r_use pares com maior similaridade
97
- # Garante que cada token de B só é merged com 1 de A (matching bipartido soft)
98
- topk_vals, topk_idx_a = max_sim_a.topk(min(r_use, max_sim_a.shape[1] - 1), dim=-1)
99
-
100
- # Merge: média dos pares selecionados
101
- # Para os tokens de A selecionados, substitui pelo merge com B correspondente
102
- merged_tokens = []
103
- used_b_mask = torch.zeros_like(sim) # (B, N_A, N_B)
104
- # Vectorizado: marca os pares usados
105
- # topk_idx_a: (B, r_use), max_idx_b é (B, N_A) → precisamos gather
106
- b_indices_for_topk = max_idx_b.gather(1, topk_idx_a) # (B, r_use)
107
-
108
- # Tokens merged: média ponderada (peso 0.5 cada)
109
- a_selected = a.gather(1, topk_idx_a.unsqueeze(-1).expand(-1, -1, D)) # (B, r_use, D)
110
- b_selected = b.gather(1, b_indices_for_topk.unsqueeze(-1).expand(-1, -1, D)) # (B, r_use, D)
111
- merged = 0.5 * (a_selected + b_selected) # (B, r_use, D)
112
-
113
- # Tokens não merged de A: máscara booleana
114
- a_mask = torch.ones(B, a.shape[1], dtype=torch.bool, device=x.device)
115
- a_mask.scatter_(1, topk_idx_a, False)
116
- a_remaining = a[a_mask].reshape(B, -1, D) # (B, N_A - r_use, D)
117
-
118
- # Tokens não merged de B: máscara booleana
119
- b_mask = torch.ones(B, b.shape[1], dtype=torch.bool, device=x.device)
120
- b_mask.scatter_(1, b_indices_for_topk, False)
121
- b_remaining = b[b_mask].reshape(B, -1, D) # (B, N_B - r_use, D)
122
-
123
- # Concatena: merged + a_remaining + b_remaining
124
- out = torch.cat([merged, a_remaining, b_remaining], dim=1) # (B, N - r_use, D)
125
-
126
- return out, {
127
- "r_used": r_use,
128
- "n_tokens_in": N,
129
- "n_tokens_out": out.shape[1],
130
- }
131
-
132
-
133
- # ═══════════════════════════════════════════════════════════════════════════
134
- # KNNIndexer — IVF-like Index for fast attention
135
- # ═══════════════════════════════════════════════════════════════════════════
136
- class KNNIndexer(nn.Module):
137
- """KNN Indexer com centroids EMA (IVF-like).
138
-
139
- Para 2M tokens:
140
- 1. Mantém K centroids (k-means inicializado, EMA atualizado)
141
- 2. Cada token é atribuído a 1 centroid (hard assignment) ou
142
- n_probe centroids (soft assignment via top-k)
143
- 3. Atenção só é computada entre query e tokens dos n_probe centroids
144
- mais próximos → O(N · n_probe · K/N) = O(n_probe · K)
145
-
146
- V6: centroids atualizados via EMA (não k-means full) → diferente do IVF
147
- clássico, é online e diferenciável.
148
-
149
- Args:
150
- d: dimensão dos tokens
151
- n_centroids: nº de centroids K (default 256)
152
- n_probe: nº de centroids a consultar por query (default 8)
153
- ema_decay: decay do EMA para centroids (default 0.99)
154
- """
155
-
156
- def __init__(
157
- self,
158
- d: int,
159
- n_centroids: int = 256,
160
- n_probe: int = 8,
161
- ema_decay: float = 0.99,
162
- ):
163
- super().__init__()
164
- self.d = d
165
- self.n_centroids = n_centroids
166
- self.n_probe = min(n_probe, n_centroids)
167
- self.ema_decay = ema_decay
168
-
169
- # Centroids: (K, d) — inicializados random
170
- self.register_buffer("centroids", torch.randn(n_centroids, d) * 0.02)
171
- self.register_buffer("centroid_count", torch.zeros(n_centroids))
172
-
173
- @torch.no_grad()
174
- def update_centroids_ema(self, x: torch.Tensor) -> None:
175
- """Atualiza centroids via EMA (chamado durante treino).
176
-
177
- Args:
178
- x: (batch, N, d) — tokens do batch atual
179
- """
180
- B, N, D = x.shape
181
- x_flat = x.reshape(B * N, D)
182
- # Atribui cada token ao centroid mais próximo
183
- dists = torch.cdist(x_flat, self.centroids) # (B*N, K)
184
- assignments = dists.argmin(dim=-1) # (B*N,)
185
-
186
- # Atualiza centroids via EMA
187
- for k in range(self.n_centroids):
188
- mask = assignments == k
189
- if mask.any():
190
- new_c = x_flat[mask].mean(dim=0)
191
- self.centroids[k] = (
192
- self.ema_decay * self.centroids[k]
193
- + (1.0 - self.ema_decay) * new_c
194
- )
195
- self.centroid_count[k] += mask.sum().item()
196
-
197
- def forward(
198
- self,
199
- query: torch.Tensor,
200
- keys: torch.Tensor,
201
- values: torch.Tensor,
202
- ) -> Tuple[torch.Tensor, dict]:
203
- """Atenção aproximada via IVF.
204
-
205
- Args:
206
- query: (batch, M, d)
207
- keys: (batch, N, d)
208
- values: (batch, N, d)
209
-
210
- Returns:
211
- out: (batch, M, d) — aproximação de attention(query, keys, values)
212
- aux: dict com n_probe, compression_ratio
213
- """
214
- B, M, D = query.shape
215
- _, N, _ = keys.shape
216
-
217
- # 1. Atribui keys aos centroids mais próximos
218
- key_dists = torch.cdist(keys, self.centroids) # (B, N, K)
219
- key_assign = key_dists.argmin(dim=-1) # (B, N) — índice do centroid
220
-
221
- # 2. Para cada query, encontra os n_probe centroids mais próximos
222
- query_dists = torch.cdist(query, self.centroids) # (B, M, K)
223
- _, probe_centroids = query_dists.topk(self.n_probe, dim=-1, largest=False) # (B, M, n_probe)
224
-
225
- # 3. Para cada query, seleciona apenas keys cujo centroid está em probe_centroids
226
- # Vectorizado: expande para (B, M, N) via broadcasting
227
- # probe_centroids: (B, M, n_probe) → (B, M, 1, n_probe)
228
- # key_assign: (B, N) → (B, 1, N, 1)
229
- probe_expanded = probe_centroids.unsqueeze(2) # (B, M, 1, n_probe)
230
- key_expanded = key_assign.unsqueeze(1).unsqueeze(-1) # (B, 1, N, 1)
231
- # mask: (B, M, N) — True se key j está em um dos n_probe centroids da query i
232
- mask = (probe_expanded == key_expanded).any(dim=-1) # (B, M, N)
233
-
234
- # 4. Atenção masked
235
- scores = (query @ keys.transpose(-1, -2)) / math.sqrt(D) # (B, M, N)
236
- scores = scores.masked_fill(~mask, float("-inf"))
237
- attn = F.softmax(scores, dim=-1) # (B, M, N)
238
- # Se uma linha inteira é -inf, softmax dá NaN → substitui por 0
239
- attn = torch.nan_to_num(attn, nan=0.0)
240
- out = attn @ values # (B, M, D)
241
-
242
- # Estatísticas
243
- avg_keys_per_query = mask.float().sum(dim=-1).mean().item()
244
- compression_ratio = N / max(avg_keys_per_query, 1.0)
245
-
246
- return out, {
247
- "n_probe": self.n_probe,
248
- "n_centroids": self.n_centroids,
249
- "avg_keys_per_query": avg_keys_per_query,
250
- "compression_ratio": compression_ratio,
251
- }
252
-
253
-
254
- # ═══════════════════════════════════════════════════════════════════════════
255
- # WindowContext — Combina ToMe + KNN + Sliding Window
256
- # ═══════════════════════════════════════════════════════════════════════════
257
- class WindowContext(nn.Module):
258
- """Window Context para 2M tokens — combina ToMe + KNN.
259
-
260
- Pipeline:
261
- 1. Aplica ToMe para reduzir N → N-r tokens (compressão espacial)
262
- 2. Mantém KNN Indexer para atenção aproximada (compressão temporal)
263
- 3. Em inference: sliding window de tamanho W com overlap W//2
264
-
265
- Args:
266
- d: dimensão dos tokens
267
- tome_r: tokens a remover por ToMe (default 8)
268
- n_centroids: nº de centroids do KNN (default 64)
269
- n_probe: nº de centroids a consultar (default 4)
270
- window_size: tamanho da janela (default 4096)
271
- """
272
-
273
- def __init__(
274
- self,
275
- d: int,
276
- tome_r: int = 8,
277
- n_centroids: int = 64,
278
- n_probe: int = 4,
279
- window_size: int = 4096,
280
- ):
281
- super().__init__()
282
- self.d = d
283
- self.window_size = window_size
284
- self.tome = ToMeCompressor(r=tome_r)
285
- self.knn = KNNIndexer(
286
- d=d, n_centroids=n_centroids, n_probe=n_probe
287
- )
288
-
289
- def forward(
290
- self,
291
- x: torch.Tensor,
292
- return_aux: bool = False,
293
- ) -> torch.Tensor:
294
- """x: (batch, N, d) → (batch, N - r, d) comprimido + indexado.
295
-
296
- Aplica ToMe (se N > 2*r) e treina KNN centroids via EMA.
297
- """
298
- # 1. ToMe compression
299
- if x.shape[1] > 2 * self.tome.r:
300
- x_compressed, tome_aux = self.tome(x)
301
- else:
302
- x_compressed = x
303
- tome_aux = {"r_used": 0, "n_tokens_in": x.shape[1], "n_tokens_out": x.shape[1]}
304
-
305
- # 2. Atualiza KNN centroids (treino apenas)
306
- if self.training:
307
- self.knn.update_centroids_ema(x_compressed)
308
-
309
- # 3. Atenção aproximada via KNN (query=x_compressed, keys=x_compressed)
310
- out, knn_aux = self.knn(x_compressed, x_compressed, x_compressed)
311
-
312
- if not return_aux:
313
- return out
314
-
315
- return out, {
316
- "tome": tome_aux,
317
- "knn": knn_aux,
318
- "compression_total": (
319
- tome_aux["n_tokens_in"] / max(tome_aux["n_tokens_out"], 1)
320
- ) * max(knn_aux["compression_ratio"], 1.0),
321
- }
322
-
323
-
324
- # ═══════════════════════════════════════════════════════════════════════════
325
- # Estimativa de memória para 2M tokens
326
- # ═══════════════════════════════════════════════════════════════════════════
327
- def estimate_2m_tokens_memory(
328
- d: int = 128,
329
- batch: int = 1,
330
- dtype_bytes: int = 4,
331
- ) -> dict:
332
- """Estima memória para 2M tokens.
333
-
334
- Sem WindowContext:
335
- attention matrix: (batch, 2M, 2M) * dtype_bytes = 16 TB (float32)
336
- tokens: (batch, 2M, d) * dtype_bytes = 1 GB
337
-
338
- Com WindowContext (ToMe r=8 per layer, 12 layers):
339
- tokens reduzidos: 2M - 12*8 = 1,999,904 (~99.99% retido)
340
- KNN attention: (batch, 2M, n_probe * K/N) = (1, 2M, 4*64/2M) = 8 MB
341
-
342
- Returns:
343
- dict com estimativas em GB
344
- """
345
- N = 2_000_000
346
- attention_full_gb = (batch * N * N * dtype_bytes) / (1024 ** 4)
347
- tokens_full_gb = (batch * N * d * dtype_bytes) / (1024 ** 3)
348
-
349
- # Com ToMe + KNN
350
- n_centroids = 64
351
- n_probe = 4
352
- K_per_query = n_probe * (N // n_centroids) # ~n_probe * cluster_size
353
- attention_knn_gb = (batch * N * K_per_query * dtype_bytes) / (1024 ** 3)
354
-
355
- return {
356
- "n_tokens": N,
357
- "d": d,
358
- "batch": batch,
359
- "attention_full_gb": attention_full_gb,
360
- "tokens_full_gb": tokens_full_gb,
361
- "attention_knn_gb": attention_knn_gb,
362
- "reduction_factor": attention_full_gb / max(attention_knn_gb, 1e-9),
363
- }
364
-
365
-
366
- __all__ = [
367
- "ToMeCompressor",
368
- "KNNIndexer",
369
- "WindowContext",
370
- "estimate_2m_tokens_memory",
371
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/reasoning/reasoning_system.py DELETED
@@ -1,281 +0,0 @@
1
- """reasoning_system.py — Sistema de Raciocínio (Reasoning/Thinking) integrável (V6).
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/reasoning_engine.py
4
- e distributed_reasoning_system.py.
5
-
6
- Implementa um sistema de raciocínio multi-fase inspirado em System 2 de
7
- Kahneman, com 8 fases (V13.9.2):
8
- 1. Think — pensamento inicial sobre o problema
9
- 2. Plan — planejamento de solução
10
- 3. Decompose — decomposição em subproblemas
11
- 4. Execute — execução dos passos
12
- 5. Monitor — monitoramento do progresso
13
- 6. Predict — predição de resultado
14
- 7. Adjust — ajuste baseado em feedback
15
- 8. Compose — composição da resposta final
16
-
17
- V6 melhorias vs V13.9.2:
18
- * Self-contained (sem PEVSystem distribuído)
19
- * BayesianStrategySelector com Thompson sampling (era epsilon-greedy)
20
- * Compatível com langchain (pode ser usado como tool)
21
- * Vetorizado quando possível
22
-
23
- REFERÊNCIAS:
24
- - Kahneman (2011). "Thinking, Fast and Slow".
25
- - Wei et al. (2022). "Chain-of-Thought Prompting".
26
- - Yao et al. (2023). "Tree of Thoughts".
27
- - Thompson (1933). "On the likelihood that one unknown probability exceeds
28
- another" (Thompson sampling).
29
- """
30
- from __future__ import annotations
31
-
32
- import math
33
- import torch
34
- import torch.nn as nn
35
- import torch.nn.functional as F
36
- from typing import Optional, List, Dict, Tuple
37
- from collections import deque
38
- import random
39
-
40
-
41
- # ═══════════════════════════════════════════════════════════════════════════
42
- # ReasoningPhase — enum das 8 fases
43
- # ═══════════════════════════════════════════════════════════════════════════
44
- REASONING_PHASES = [
45
- "think",
46
- "plan",
47
- "decompose",
48
- "execute",
49
- "monitor",
50
- "predict",
51
- "adjust",
52
- "compose",
53
- ]
54
-
55
-
56
- # ═══════════════════════════════════════════════════════════════════════════
57
- # BayesianStrategySelector — Thompson sampling
58
- # ═══════════════════════════════════════════════════════════════════════════
59
- class BayesianStrategySelector(nn.Module):
60
- """Seletor de estratégia bayesiano (Thompson sampling).
61
-
62
- Mantém uma distribuição Beta(α_s, β_s) para cada estratégia s.
63
- A cada passo, amostra de cada Beta e escolhe a estratégia com maior amostra.
64
- Após receber reward, atualiza α_s ou β_s.
65
-
66
- Args:
67
- n_strategies: nº de estratégias (default 8 — uma por fase)
68
- prior_alpha: α inicial (default 1.0 — uniforme)
69
- prior_beta: β inicial (default 1.0 — uniforme)
70
- """
71
-
72
- def __init__(
73
- self,
74
- n_strategies: int = 8,
75
- prior_alpha: float = 1.0,
76
- prior_beta: float = 1.0,
77
- ):
78
- super().__init__()
79
- self.n_strategies = n_strategies
80
- # α e β das distribuições Beta (uma por estratégia)
81
- self.register_buffer("alpha", torch.full((n_strategies,), float(prior_alpha)))
82
- self.register_buffer("beta", torch.full((n_strategies,), float(prior_beta)))
83
-
84
- def select(self) -> int:
85
- """Thompson sampling: amostra de cada Beta, retorna índice do maior."""
86
- # Beta(α, β) sampling via torch.distributions
87
- dist = torch.distributions.Beta(self.alpha, self.beta)
88
- samples = dist.sample()
89
- return int(samples.argmax().item())
90
-
91
- def update(self, strategy: int, reward: float) -> None:
92
- """Atualiza a distribuição Beta da estratégia selecionada.
93
-
94
- Beta(α, β) é conjugada de Bernoulli:
95
- - reward ≥ 0.5 (sucesso): α ← α + 1
96
- - reward < 0.5 (fracasso): β ← β + 1
97
- """
98
- if reward >= 0.5:
99
- self.alpha[strategy] += 1.0
100
- else:
101
- self.beta[strategy] += 1.0
102
-
103
- def get_strategy_probs(self) -> torch.Tensor:
104
- """Retorna probabilidade esperada de cada estratégia (mean of Beta)."""
105
- return self.alpha / (self.alpha + self.beta)
106
-
107
- def extra_repr(self) -> str:
108
- probs = self.get_strategy_probs()
109
- return f"n_strategies={self.n_strategies}, probs={probs.tolist()}"
110
-
111
-
112
- # ═══════════════════════════════════════════════════════════════════════════
113
- # ReasoningEngine — motor de raciocínio 8-fase
114
- # ════════════════════════════════════════════════════════���══════════════════
115
- class ReasoningEngine(nn.Module):
116
- """Sistema de raciocínio multi-fase (V6).
117
-
118
- Args:
119
- d_model: dimensão do hidden state
120
- vocab_size: tamanho do vocabulário
121
- n_phases: nº de fases (default 8)
122
- max_steps: máx de passos de raciocínio (default 16)
123
- use_bayesian_selector: se True, usa Thompson sampling (default True)
124
-
125
- Uso:
126
- engine = ReasoningEngine(d_model=128, vocab_size=1000)
127
- output = engine(inputs, hidden, targets)
128
- """
129
-
130
- def __init__(
131
- self,
132
- d_model: int,
133
- vocab_size: int,
134
- n_phases: int = 8,
135
- max_steps: int = 16,
136
- use_bayesian_selector: bool = True,
137
- ):
138
- super().__init__()
139
- self.d_model = d_model
140
- self.vocab_size = vocab_size
141
- self.n_phases = n_phases
142
- self.max_steps = max_steps
143
-
144
- # Embedding de fase (um por fase)
145
- self.phase_embedding = nn.Embedding(n_phases, d_model)
146
-
147
- # Projeção do hidden + phase → logits da próxima fase
148
- self.phase_proj = nn.Linear(2 * d_model, n_phases)
149
-
150
- # Projeção final → logits de vocabulário
151
- self.out_proj = nn.Linear(d_model, vocab_size)
152
-
153
- # Seletor de estratégia
154
- self.use_bayesian = use_bayesian_selector
155
- if use_bayesian_selector:
156
- self.strategy_selector = BayesianStrategySelector(n_strategies=n_phases)
157
-
158
- # Histórico de raciocínio (para logging)
159
- self.reasoning_history: List[Dict] = []
160
-
161
- def forward(
162
- self,
163
- hidden: torch.Tensor,
164
- targets: Optional[torch.Tensor] = None,
165
- return_chain: bool = False,
166
- ) -> dict:
167
- """Executa raciocínio multi-fase.
168
-
169
- Args:
170
- hidden: (batch, T, d_model) — hidden state do backbone
171
- targets: (batch, T) — targets (opcional, para supervised loss)
172
- return_chain: se True, retorna cadeia de raciocínio
173
-
174
- Returns:
175
- dict com:
176
- - logits: (B, T, vocab_size) — logits finais
177
- - loss: escalar (se targets fornecido)
178
- - chain: lista de fases executadas (se return_chain=True)
179
- - aux: estatísticas
180
- """
181
- B, T, D = hidden.shape
182
- device = hidden.device
183
-
184
- # Estado de raciocínio: começa com hidden médio sobre T
185
- reasoning_state = hidden.mean(dim=1) # (B, D)
186
- chain: List[Dict] = []
187
- phase_logits_accum = torch.zeros(B, self.n_phases, device=device)
188
-
189
- for step in range(self.max_steps):
190
- # Seleciona fase (Thompson sampling ou determinístico)
191
- if self.use_bayesian:
192
- phase_idx = self.strategy_selector.select()
193
- else:
194
- phase_idx = step % self.n_phases
195
-
196
- phase_emb = self.phase_embedding(
197
- torch.tensor(phase_idx, device=device)
198
- ) # (D,)
199
-
200
- # Combina reasoning_state com phase_emb
201
- combined = torch.cat([
202
- reasoning_state,
203
- phase_emb.unsqueeze(0).expand(B, -1),
204
- ], dim=-1) # (B, 2D)
205
-
206
- # Prediz próxima fase
207
- phase_logits = self.phase_proj(combined) # (B, n_phases)
208
- phase_logits_accum += phase_logits
209
-
210
- # Atualiza reasoning_state (residual com phase_emb)
211
- reasoning_state = reasoning_state + 0.1 * phase_emb.unsqueeze(0)
212
-
213
- # Log da fase
214
- phase_info = {
215
- "step": step,
216
- "phase": REASONING_PHASES[phase_idx] if phase_idx < len(REASONING_PHASES) else f"phase_{phase_idx}",
217
- "phase_idx": phase_idx,
218
- }
219
- chain.append(phase_info)
220
-
221
- # Reward (se targets fornecido): baseado em quão bem prediz
222
- if targets is not None and self.use_bayesian:
223
- # Reward aproximado: 1 se a próxima fase melhora a predição
224
- # (aqui simplificado: 0.5 neutro)
225
- self.strategy_selector.update(phase_idx, 0.5)
226
-
227
- # Critério de parada: se fase volta a ser "think", termina
228
- if step > 0 and phase_idx == 0:
229
- break
230
-
231
- # Logits finais: out_proj sobre reasoning_state expandido para T
232
- final_hidden = reasoning_state.unsqueeze(1).expand(-1, T, -1) # (B, T, D)
233
- # Combina com hidden original (residual)
234
- final_hidden = final_hidden + 0.5 * hidden # (B, T, D)
235
- logits = self.out_proj(final_hidden) # (B, T, vocab_size)
236
-
237
- # Loss (se targets)
238
- loss = None
239
- if targets is not None:
240
- loss = F.cross_entropy(
241
- logits.reshape(-1, self.vocab_size),
242
- targets.reshape(-1),
243
- ignore_index=-100,
244
- )
245
-
246
- # Salva histórico
247
- self.reasoning_history.append({
248
- "chain": chain,
249
- "n_steps": len(chain),
250
- })
251
-
252
- result = {
253
- "logits": logits,
254
- "loss": loss,
255
- "aux": {
256
- "n_reasoning_steps": len(chain),
257
- "phases_visited": [c["phase"] for c in chain],
258
- "phase_logits_mean": float(phase_logits_accum.mean().item()),
259
- },
260
- }
261
- if return_chain:
262
- result["chain"] = chain
263
-
264
- return result
265
-
266
- def get_last_chain(self) -> Optional[List[Dict]]:
267
- """Retorna a última cadeia de raciocínio."""
268
- if not self.reasoning_history:
269
- return None
270
- return self.reasoning_history[-1].get("chain")
271
-
272
- def clear_history(self) -> None:
273
- """Limpa histórico de raciocínio (libera RAM)."""
274
- self.reasoning_history.clear()
275
-
276
-
277
- __all__ = [
278
- "REASONING_PHASES",
279
- "BayesianStrategySelector",
280
- "ReasoningEngine",
281
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/training/rlhf_possibility_mapping.py DELETED
@@ -1,387 +0,0 @@
1
- """rlhf_possibility_mapping.py — Mapeamento de Possibilidades Aprendível com RLHF (V6).
2
-
3
- Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/learnable_mapping.py
4
- e punishment_reward_system.py.
5
-
6
- Implementa o mapeamento de possibilidades aprendível com RLHF (Reinforcement
7
- Learning from Human Feedback). A fórmula central (Teorema 2 do V13.9.2):
8
-
9
- Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
10
-
11
- onde:
12
- - Θ: parâmetros base do modelo
13
- - η: learning rate
14
- - r: reward do humano
15
- - σ(trust · τ_T): gate de confiança (TrustNeuralAgent interno, V6 self-contained)
16
- - wᵢ: pesos cooperativos (wᵢ ∝ 1/‖gᵢ‖²) — Teorema 2
17
- - gᵢ: gradientes das componentes
18
- - P_rep: penalidade de repetição
19
- - λ_rep: peso da penalidade de repetição
20
-
21
- V6 melhorias vs V13.9.2:
22
- * Self-contained: TrustNeuralAgent interno (sem dependência externa)
23
- * Pesos cooperativos wᵢ calculados online (não pré-computados)
24
- * Momentum com decay 0.999 (estabilidade)
25
- * Compatível com PPO simplificado (sem actor-critic completo)
26
-
27
- REFERÊNCIAS:
28
- - Schulman et al. (2017). "Proximal Policy Optimization Algorithms".
29
- - Ouyang et al. (2022). "Training language models to follow instructions with
30
- human feedback" (InstructGPT).
31
- - Christiano et al. (2017). "Deep reinforcement learning from human preferences".
32
- """
33
- from __future__ import annotations
34
-
35
- import math
36
- import torch
37
- import torch.nn as nn
38
- import torch.nn.functional as F
39
- from typing import Optional, List, Dict
40
- from collections import deque
41
-
42
-
43
- # ═══════════════════════════════════════════════════════════════════════════
44
- # RewardModel — modelo de recompensa simples (placeholder, V6)
45
- # ═══════════════════════════════════════════════════════════════════════════
46
- class RewardModel(nn.Module):
47
- """Modelo de recompensa simples — projeta hidden state para escalar.
48
-
49
- Em produção, seria treinado com preferências humanas (Bradley-Terry).
50
- Aqui é um placeholder linear para fins de integração.
51
-
52
- Args:
53
- d_model: dimensão do hidden state
54
- """
55
-
56
- def __init__(self, d_model: int):
57
- super().__init__()
58
- self.proj = nn.Sequential(
59
- nn.Linear(d_model, d_model // 2),
60
- nn.ReLU(),
61
- nn.Linear(d_model // 2, 1),
62
- )
63
-
64
- def forward(self, hidden: torch.Tensor) -> torch.Tensor:
65
- """hidden: (batch, T, d) → reward: (batch,) — média sobre T."""
66
- return self.proj(hidden).squeeze(-1).mean(dim=-1)
67
-
68
-
69
- # ═══════════════════════════════════════════════════════════════════════════
70
- # TrustNeuralAgent — self-contained (V6)
71
- # ═══════════════════════════════════════════════════════════════════════════
72
- class TrustNeuralAgent(nn.Module):
73
- """Agente de confiança self-contained (sem dependência externa).
74
-
75
- Mantém um embedding de "trust" por token, atualizado via TD-learning.
76
- Em V13.9.2, era uma classe externa com dependências complexas. Em V6,
77
- é uma camada simples com:
78
- - Embedding de trust (d_model) por vocab_size
79
- - Atualização TD: trust[t+1] = trust[t] + α · (reward - trust[t])
80
-
81
- Args:
82
- vocab_size: tamanho do vocabulário
83
- d_model: dimensão do trust embedding
84
- td_alpha: taxa de aprendizado TD (default 0.1)
85
- """
86
-
87
- def __init__(
88
- self,
89
- vocab_size: int,
90
- d_model: int,
91
- td_alpha: float = 0.1,
92
- ):
93
- super().__init__()
94
- self.vocab_size = vocab_size
95
- self.d_model = d_model
96
- self.td_alpha = td_alpha
97
- # Trust embedding: (vocab_size, d_model) — inicializado neutro (0.5)
98
- self.trust_embedding = nn.Parameter(torch.zeros(vocab_size, d_model))
99
- # tau_T: temperatura (aprendível)
100
- self.log_tau_T = nn.Parameter(torch.tensor(0.0)) # τ_T = 1.0
101
-
102
- @property
103
- def tau_T(self) -> torch.Tensor:
104
- return torch.exp(self.log_tau_T)
105
-
106
- def forward(
107
- self,
108
- token_ids: torch.Tensor,
109
- hidden: torch.Tensor,
110
- ) -> torch.Tensor:
111
- """Computa σ(trust · τ_T) para os tokens dados.
112
-
113
- Args:
114
- token_ids: (batch, T) — IDs dos tokens
115
- hidden: (batch, T, d_model) — hidden states (não usado aqui,
116
- mas mantido para compat)
117
-
118
- Returns:
119
- trust_gate: (batch, T) — σ(mean(trust_embedding[token]) · τ_T)
120
- """
121
- trust = self.trust_embedding[token_ids] # (B, T, d)
122
- trust_scalar = trust.mean(dim=-1) # (B, T)
123
- # σ(trust · τ_T) — gate de confiança ∈ (0, 1)
124
- gate = torch.sigmoid(trust_scalar * self.tau_T)
125
- return gate
126
-
127
- @torch.no_grad()
128
- def update_td(self, token_ids: torch.Tensor, rewards: torch.Tensor) -> None:
129
- """Atualização TD do trust embedding.
130
-
131
- trust[token] ← trust[token] + α · (reward - trust[token])
132
-
133
- Args:
134
- token_ids: (batch, T) — tokens que receberam feedback
135
- rewards: (batch, T) ou (batch,) — recompensas
136
- """
137
- if rewards.dim() == 1:
138
- rewards = rewards.unsqueeze(-1).expand(-1, token_ids.shape[1])
139
- for b in range(token_ids.shape[0]):
140
- for t in range(token_ids.shape[1]):
141
- tid = int(token_ids[b, t].item())
142
- r = float(rewards[b, t].item())
143
- # Atualiza apenas o primeiro canal (escalar)
144
- current = float(self.trust_embedding[tid, 0].item())
145
- self.trust_embedding.data[tid, 0] = current + self.td_alpha * (r - current)
146
-
147
-
148
- # ═══════════════════════════════════════════════════════════════════════════
149
- # LearnablePossibilityMapping — RLHF com mapeamento de possibilidades
150
- # ═══════════════════════════════════════════════════════════════════════════
151
- class LearnablePossibilityMapping(nn.Module):
152
- """Mapeamento de Possibilidades Aprendível com RLHF (V6).
153
-
154
- Implementa a fórmula central (Teorema 2 do V13.9.2):
155
- Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
156
-
157
- Componentes:
158
- - TrustNeuralAgent: gate σ(trust · τ_T)
159
- - RewardModel: recompensa r
160
- - Pesos cooperativos wᵢ ∝ 1/‖gᵢ‖² (Teorema 2)
161
- - Momentum com decay 0.999
162
-
163
- Args:
164
- model: modelo base
165
- vocab_size: tamanho do vocabulário
166
- d_model: dimensão do modelo
167
- eta_init: η inicial (learning rate RL) — default 0.01
168
- lambda_rep_init: λ_rep inicial — default 0.1
169
- momentum_decay: decay do momentum — default 0.999
170
- """
171
-
172
- def __init__(
173
- self,
174
- model: nn.Module,
175
- vocab_size: int,
176
- d_model: int,
177
- eta_init: float = 0.01,
178
- lambda_rep_init: float = 0.1,
179
- momentum_decay: float = 0.999,
180
- ):
181
- super().__init__()
182
- self.model = model
183
- self.vocab_size = vocab_size
184
- self.d_model = d_model
185
- self.momentum_decay = momentum_decay
186
-
187
- # Componentes
188
- self.trust_agent = TrustNeuralAgent(vocab_size, d_model)
189
- self.reward_model = RewardModel(d_model)
190
-
191
- # Hiperparâmetros (log-space)
192
- self.log_eta = nn.Parameter(torch.tensor(math.log(eta_init)))
193
- self.log_lambda_rep = nn.Parameter(torch.tensor(math.log(lambda_rep_init)))
194
-
195
- # Momentum (um por parâmetro)
196
- self.momentum: Dict[str, torch.Tensor] = {}
197
-
198
- # Histórico de rewards (para normalização)
199
- self.register_buffer("reward_history", torch.zeros(100))
200
- self.register_buffer("reward_history_pos", torch.tensor(0, dtype=torch.long))
201
-
202
- @property
203
- def eta(self) -> torch.Tensor:
204
- return torch.exp(self.log_eta)
205
-
206
- @property
207
- def lambda_rep(self) -> torch.Tensor:
208
- return torch.exp(self.log_lambda_rep)
209
-
210
- def compute_cooperative_weights(
211
- self,
212
- grads: List[torch.Tensor],
213
- ) -> List[torch.Tensor]:
214
- """Computa pesos cooperativos wᵢ ∝ 1/‖gᵢ‖² (Teorema 2).
215
-
216
- Args:
217
- grads: lista de gradientes (um por componente)
218
-
219
- Returns:
220
- lista de pesos normalizados (soma = 1)
221
- """
222
- norms_sq = torch.stack(
223
- [(g ** 2).sum() if g is not None else torch.tensor(0.0) for g in grads]
224
- )
225
- # wᵢ ∝ 1/‖gᵢ‖² — gradientes menores recebem mais peso
226
- inv_norms = 1.0 / (norms_sq + 1e-8)
227
- weights = inv_norms / inv_norms.sum()
228
- return [float(w.item()) for w in weights]
229
-
230
- def update_reward_history(self, reward: float) -> float:
231
- """Atualiza histórico e retorna reward normalizado."""
232
- pos = int(self.reward_history_pos.item())
233
- self.reward_history[pos] = reward
234
- self.reward_history_pos = (self.reward_history_pos + 1) % self.reward_history.shape[0]
235
- # Normalização: reward normalizado = (r - mean) / (std + eps)
236
- mean = float(self.reward_history.mean().item())
237
- std = float(self.reward_history.std().item())
238
- return (reward - mean) / (std + 1e-8)
239
-
240
- def compute_mapping(
241
- self,
242
- token_ids: torch.Tensor,
243
- hidden: torch.Tensor,
244
- task_loss: torch.Tensor,
245
- rep_penalty: Optional[torch.Tensor] = None,
246
- ) -> dict:
247
- """Computa o mapeamento de possibilidades Φ.
248
-
249
- Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
250
-
251
- Args:
252
- token_ids: (batch, T) — tokens
253
- hidden: (batch, T, d_model) — hidden states
254
- task_loss: escalar — loss da tarefa atual
255
- rep_penalty: escalar opcional — penalidade de repetição
256
-
257
- Returns:
258
- dict com:
259
- - reward: recompensa r
260
- - trust_gate: σ(trust · τ_T)
261
- - cooperative_weights: wᵢ
262
- - lambda_rep: λ_rep atual
263
- - eta: η atual
264
- - normalized_reward: r normalizado
265
- """
266
- # 1. Reward
267
- with torch.no_grad():
268
- reward = self.reward_model(hidden).mean() # escalar
269
- normalized_reward = self.update_reward_history(float(reward.item()))
270
-
271
- # 2. Trust gate
272
- trust_gate = self.trust_agent(token_ids, hidden) # (B, T)
273
- trust_gate_mean = trust_gate.mean() # escalar
274
-
275
- # 3. Gradientes das componentes (apenas task_loss tem grad aqui)
276
- # Em V6 simplificado: só 1 componente (task_loss)
277
- # Em versão completa: teríamos task_loss + auxiliary_losses
278
- grads = [torch.autograd.grad(
279
- task_loss, list(self.model.parameters()),
280
- retain_graph=True, allow_unused=True
281
- )]
282
- # Pesos cooperativos
283
- w = self.compute_cooperative_weights(grads[0])
284
-
285
- # 4. Penalidade de repetição
286
- if rep_penalty is not None:
287
- rep_penalty_val = rep_penalty
288
- else:
289
- rep_penalty_val = torch.tensor(0.0, device=hidden.device)
290
-
291
- # 5. Mapeamento Φ (não aplicado aqui — apenas log dos componentes)
292
- # A aplicação real acontece no trainer via:
293
- # θ ← θ - η · (r - baseline) · trust_gate · Σ wᵢ · gᵢ - λ_rep · ∇P_rep
294
- # Aqui retornamos os componentes para inspeção/logging
295
- return {
296
- "reward": float(reward.item()),
297
- "normalized_reward": float(normalized_reward),
298
- "trust_gate_mean": float(trust_gate_mean.item()),
299
- "trust_tau_T": float(self.trust_agent.tau_T.item()),
300
- "cooperative_weights": w,
301
- "lambda_rep": float(self.lambda_rep.item()),
302
- "eta": float(self.eta.item()),
303
- "rep_penalty": float(rep_penalty_val.item() if isinstance(rep_penalty_val, torch.Tensor) else rep_penalty_val),
304
- }
305
-
306
- @torch.no_grad()
307
- def apply_mapping(
308
- self,
309
- token_ids: torch.Tensor,
310
- hidden: torch.Tensor,
311
- task_grads: Dict[str, torch.Tensor],
312
- rep_penalty_grads: Optional[Dict[str, torch.Tensor]] = None,
313
- reward: Optional[float] = None,
314
- ) -> Dict[str, float]:
315
- """Aplica o mapeamento Φ aos gradientes (in-place).
316
-
317
- Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
318
-
319
- Args:
320
- token_ids: (batch, T)
321
- hidden: (batch, T, d_model)
322
- task_grads: gradientes da task_loss
323
- rep_penalty_grads: gradientes da penalidade de repetição (opcional)
324
- reward: recompensa externa (opcional)
325
-
326
- Returns:
327
- dict com estatísticas da atualização
328
- """
329
- # 1. Reward (computa se não fornecido)
330
- if reward is None:
331
- reward = float(self.reward_model(hidden).mean().item())
332
- normalized_reward = self.update_reward_history(reward)
333
-
334
- # 2. Trust gate (média)
335
- trust_gate = self.trust_agent(token_ids, hidden)
336
- trust_gate_mean = float(trust_gate.mean().item())
337
-
338
- # 3. Pesos cooperativos (1 componente aqui)
339
- task_norm_sq = sum(
340
- (g ** 2).sum().item() for g in task_grads.values() if g is not None
341
- )
342
- w_task = 1.0 / (task_norm_sq + 1e-8)
343
-
344
- # 4. Aplica Φ aos gradientes
345
- eta = float(self.eta.item())
346
- lam_rep = float(self.lambda_rep.item())
347
- scaling = eta * normalized_reward * trust_gate_mean * w_task
348
-
349
- for name, p in self.model.named_parameters():
350
- if not p.requires_grad:
351
- continue
352
- g_task = task_grads.get(name)
353
- if g_task is None:
354
- continue
355
- # Momentum
356
- if name not in self.momentum:
357
- self.momentum[name] = torch.zeros_like(p)
358
- self.momentum[name] = (
359
- self.momentum_decay * self.momentum[name]
360
- + (1.0 - self.momentum_decay) * g_task
361
- )
362
- # Atualização: Δθ = scaling · momentum - lam_rep · rep_penalty_grad
363
- delta = scaling * self.momentum[name]
364
- if rep_penalty_grads and name in rep_penalty_grads:
365
- delta = delta - lam_rep * rep_penalty_grads[name]
366
- p.data -= delta
367
-
368
- # 5. Atualiza trust via TD
369
- self.trust_agent.update_td(token_ids, torch.full(
370
- (token_ids.shape[0],), reward, device=token_ids.device
371
- ))
372
-
373
- return {
374
- "reward": reward,
375
- "normalized_reward": normalized_reward,
376
- "trust_gate_mean": trust_gate_mean,
377
- "scaling": scaling,
378
- "eta": eta,
379
- "lambda_rep": lam_rep,
380
- }
381
-
382
-
383
- __all__ = [
384
- "RewardModel",
385
- "TrustNeuralAgent",
386
- "LearnablePossibilityMapping",
387
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/bigru_t/training/trainer.py DELETED
@@ -1,937 +0,0 @@
1
- """trainer.py — Loop de treino BiGRU_T_version (2 épocas, bug-detection).
2
-
3
- Implementa:
4
- - Treino com 2 épocas fixas ( especificação do usuário )
5
- - Sem dados inventados (apenas datasets do repo + HF públicos)
6
- - Monitoramento: loss, perplexidade, RAM, disco, módulos ativos
7
- - Kill-switch: aborta se loss não diminuir ou RAM explodir
8
- - Estados temporários salvos durante treino e APAGADOS ao final
9
- - Aplicação dos 4 lemas:
10
- Lema 1: ModuleSelector (softmax + entropia)
11
- Lema 2: apply_gradient_surgery quando use_hyp=True
12
- Lema 3: hyp_T com stop_grad_hyp=True, W8A8 via QuantizedLinear
13
- Lema 4: MetaConfigurator ajusta T e τ a cada meta_interval batches
14
-
15
- Reaproveita a filosofia do scripts/train_from_v13_9_2.py, mas:
16
- - Sem fine-tune de modelo pré-treinado (treino from-scratch para
17
- bug-detection — o objetivo é detectar erros, não produzir modelo SOTA)
18
- - Adiciona kill-switch (não havia no original)
19
- - Adiciona MetaConfigurator (não havia no original)
20
- - Adiciona gradient surgery (não havia no original)
21
- - Usa 2 épocas explícitas (vs. max_steps no original)
22
- """
23
- from __future__ import annotations
24
-
25
- import gc
26
- import json
27
- import logging
28
- import math
29
- import os
30
- import shutil
31
- import sys
32
- import time
33
- import traceback
34
- from dataclasses import dataclass, field
35
- from pathlib import Path
36
- from typing import Optional
37
-
38
- import torch
39
- import torch.nn as nn
40
- import torch.nn.functional as F
41
-
42
- from ..model.unified_model import UnifiedModel, UnifiedModelConfig, create_unified_model
43
- from .gradient_surgery import apply_gradient_surgery
44
- from .meta_configurator import MetaConfigurator
45
- from .kill_switch import KillSwitch
46
- from .dpo import compute_dynamic_beta, dpo_step, compute_sequence_logps
47
- from .hypothesis_monitor import HypothesisMonitor
48
- from ..optim.hamiltonian_wasserstein import HamiltonianWassersteinOptimizer
49
- from ..utils.memory_cleanup import aggressive_cleanup, production_cleanup, TimeBudget, StepTimer, get_rss_mb
50
-
51
- logger = logging.getLogger(__name__)
52
-
53
-
54
- @dataclass
55
- class TrainerConfig:
56
- """Configuração do treino (V3: 5 épocas + DPO + CircularReasoning + Monitor)."""
57
- # Epochs (V3: 5 épocas conforme especificação do usuário)
58
- epochs: int = 5
59
-
60
- # Datasets
61
- datasets: str = "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1,Madras1/corpus-ptbr-v2,dominguesm/restore-punctuation-ptbr-dataset"
62
- max_samples_per_dataset: int = 30 # bug-detection: amostra pequena
63
- max_seq_len: int = 64
64
-
65
- # Batch
66
- per_device_batch_size: int = 1
67
- grad_accum: int = 4
68
-
69
- # Optimizer (HamiltonianWasserstein ATIVADO)
70
- optimizer_type: str = "hamiltonian_wasserstein" # "adamw" | "hamiltonian_wasserstein"
71
- lr: float = 1e-3 # from-scratch: LR mais alto que fine-tune
72
- weight_decay: float = 0.01
73
- max_grad_norm: float = 5.0
74
- # HW optimizer params
75
- hw_lr_amp: float = 0.3 # amplitude do LR cíclico (Van der Pol)
76
- hw_lr_freq: float = 0.01 # frequência angular do LR cíclico
77
- hw_sigma_w: float = 1.0 # escala W₂
78
- hw_sigma_rep: float = 0.1 # escala repulsão topológica
79
- hw_prune_every_n: int = 0 # pruning desativado por padrão (0 = off)
80
- hw_ref_buffer_size: int = 2 # buffer de referência reduzido (era 10 → 2 para economizar RAM)
81
-
82
- # DPO (V3: ATIVADO para datasets compatíveis)
83
- # DPO é ativado automaticamente quando amostras têm label_text
84
- # (instruction/response format). DPO loss é adicionada à loss principal.
85
- use_dpo: bool = True # V3: ativo por padrão (auto-detecção)
86
- dpo_beta_min: float = 0.05
87
- dpo_beta_max: float = 0.5
88
- dpo_warmup_steps: int = 100
89
- dpo_loss_weight: float = 0.3 # peso da loss DPO na loss total
90
- dpo_use_ipo: bool = False # IPO em vez de DPO clássico
91
- dpo_label_smoothing: float = 0.0
92
- dpo_every_n_steps: int = 2 # computar DPO a cada N steps (economia de compute)
93
-
94
- # Circular Reasoning (Teorema 4)
95
- use_circular_reasoning: bool = True # já está no UnifiedModelConfig também
96
-
97
- # Meta-configurator (Lema 4)
98
- meta_interval: int = 10 # a cada N micro-batches
99
- meta_lr: float = 0.01
100
- sharpness_lambda: float = 0.01
101
-
102
- # Hypothesis (Lema 3)
103
- use_hypothesis: bool = True
104
- stop_grad_hyp: bool = True
105
-
106
- # Kill-switch
107
- ram_threshold_pct: float = 90.0
108
- disk_min_free_gb: float = 1.0
109
- loss_patience: int = 30
110
-
111
- # Time budget (estilo Xavante — streaming com timed steps)
112
- # V3: aumentado para 5 épocas (2700s total = 45 min, 540s/epoch = 9 min)
113
- max_total_time_s: float = 2700.0 # 45 min máx total (5 épocas)
114
- max_per_epoch_s: float = 540.0 # 9 min máx por época
115
-
116
- # Memory cleanup agressivo (estilo Xavante)
117
- cleanup_every_n_steps: int = 5 # aggressive_cleanup a cada N optimizer steps
118
-
119
- # Logging
120
- log_every: int = 5
121
- save_temp_every: int = 10 # salvar checkpoint temporário a cada N steps
122
-
123
- # Paths
124
- output_dir: str = "/home/z/my-project/BiGRU_T_version/model_final"
125
- temp_dir: str = "/home/z/my-project/BiGRU_T_version/_temp_checkpoints"
126
- keep_temp: bool = False # False = apagar temp ao final (especificação)
127
-
128
-
129
- class BiGRU_T_Trainer:
130
- """Trainer para BiGRU_T_version com 4 lemas + kill-switch.
131
-
132
- Args:
133
- model: UnifiedModel
134
- tokenizer: BBPE tokenizer
135
- config: TrainerConfig
136
- train_samples: lista de ProcessedSample para treino
137
- val_samples: lista de ProcessedSample para validação (meta-aprendizado)
138
- """
139
-
140
- def __init__(
141
- self,
142
- model: UnifiedModel,
143
- tokenizer,
144
- config: TrainerConfig,
145
- train_samples: list,
146
- val_samples: list,
147
- ):
148
- self.model = model
149
- self.tokenizer = tokenizer
150
- self.config = config
151
- self.train_samples = train_samples
152
- self.val_samples = val_samples
153
-
154
- # Device
155
- self.device = torch.device("cpu")
156
- torch.set_num_threads(2)
157
- os.environ.setdefault("OMP_NUM_THREADS", "2")
158
- os.environ.setdefault("MKL_NUM_THREADS", "2")
159
- os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
160
-
161
- # Otimizador principal — HamiltonianWasserstein ATIVADO por padrão
162
- # (AdamW + W₂ adaptativo + repulsão topológica + LR cíclico + pruning opcional)
163
- if config.optimizer_type == "hamiltonian_wasserstein":
164
- self.optimizer = HamiltonianWassersteinOptimizer(
165
- model.parameters(),
166
- lr=config.lr,
167
- betas=(0.9, 0.95),
168
- eps=1e-8,
169
- weight_decay=config.weight_decay,
170
- lr_amp=config.hw_lr_amp,
171
- lr_freq=config.hw_lr_freq,
172
- sigma_w=config.hw_sigma_w,
173
- sigma_rep=config.hw_sigma_rep,
174
- prune_every_n=config.hw_prune_every_n,
175
- ref_buffer_size=config.hw_ref_buffer_size,
176
- )
177
- logger.info("Optimizer: HamiltonianWassersteinOptimizer (ATIVADO)")
178
- else:
179
- self.optimizer = torch.optim.AdamW(
180
- model.parameters(),
181
- lr=config.lr,
182
- betas=(0.9, 0.95),
183
- weight_decay=config.weight_decay,
184
- eps=1e-8,
185
- )
186
- logger.info("Optimizer: AdamW (fallback)")
187
-
188
- # Time budget (estilo Xavante)
189
- self.time_budget = TimeBudget(
190
- max_total_s=config.max_total_time_s,
191
- max_per_epoch_s=config.max_per_epoch_s,
192
- )
193
- self.step_timer = StepTimer(expected_s=2.0) # espera ~2s por step
194
-
195
- # Meta-configurator (Lema 4)
196
- self.meta_cfg = MetaConfigurator(
197
- model,
198
- meta_lr=config.meta_lr,
199
- sharpness_lambda=config.sharpness_lambda,
200
- )
201
-
202
- # Kill-switch
203
- self.kill_switch = KillSwitch(
204
- ram_threshold_pct=config.ram_threshold_pct,
205
- disk_min_free_gb=config.disk_min_free_gb,
206
- loss_patience=config.loss_patience,
207
- log_dir=config.temp_dir,
208
- )
209
-
210
- # Hypothesis Monitor (V3) — detecta bugs na ativação dos 4 lemas
211
- self.hyp_monitor = HypothesisMonitor(
212
- max_modules=model.config.max_modules,
213
- delta_epsilon=1e-4,
214
- meta_stagnation_epsilon=1e-4,
215
- history_size=200,
216
- )
217
-
218
- # DPO: detecta se dataset é compatível (tem label_text em pelo menos 30% das amostras)
219
- self._dpo_compatible = False
220
- if config.use_dpo and train_samples:
221
- n_with_label = sum(1 for s in train_samples if getattr(s, "label_text", ""))
222
- ratio = n_with_label / len(train_samples)
223
- self._dpo_compatible = ratio >= 0.3
224
- logger.info(
225
- f"DPO auto-detecção: {n_with_label}/{len(train_samples)} ({ratio:.0%}) "
226
- f"amostras com label_text → "
227
- f"{'COMPATÍVEL (DPO ativado)' if self._dpo_compatible else 'não compatível (SFT only)'}"
228
- )
229
-
230
- # Diretórios
231
- Path(config.output_dir).mkdir(parents=True, exist_ok=True)
232
- Path(config.temp_dir).mkdir(parents=True, exist_ok=True)
233
-
234
- # Estado de treino
235
- self.global_step = 0
236
- self.micro_batch_idx = 0
237
- self.best_loss = float("inf")
238
- self.losses_history: list[float] = []
239
- self.killed_reason: Optional[str] = None
240
- # DPO reference logps (computados uma vez no início, com modelo congelado)
241
- self._ref_logps_computed = False
242
-
243
- def _collate(self, samples: list) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
244
- """Tokeniza samples → (input_ids, attention_mask, labels).
245
-
246
- Reaproveita collate_samples_to_tensors do streaming_datasets_v13_9.
247
- """
248
- from ..data.streaming_datasets import collate_samples_to_tensors
249
- pad_id = self.model.config.pad_token_id
250
- return collate_samples_to_tensors(
251
- samples, self.tokenizer,
252
- max_seq_len=self.config.max_seq_len,
253
- pad_token_id=pad_id,
254
- )
255
-
256
- def _forward_loss(
257
- self,
258
- input_ids: torch.Tensor,
259
- attention_mask: torch.Tensor,
260
- labels: torch.Tensor,
261
- use_hyp: bool,
262
- ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
263
- """Forward + loss. Retorna (loss_main, loss_hyp_or_zero, y_final).
264
-
265
- Para LM (next-token prediction):
266
- - input_ids: (batch, T)
267
- - labels: (batch, T) — target = input_ids shifted
268
- - y_hat, delta: (batch, T, vocab_size) — um logit por posição
269
- - loss = CrossEntropy(y_hat.view(-1, V), labels.view(-1))
270
-
271
- Mas o UnifiedModel atual produz (batch, vocab_size) — um logit por
272
- amostra. Para compatibilidade com LM, fazemos:
273
- - input = input_ids[:, :-1] (T-1 tokens de entrada)
274
- - target = input_ids[:, 1:] (T-1 targets)
275
- - Mas o modelo só produz 1 logit por amostra, então targetamos
276
- apenas o ÚLTIMO token: target = input_ids[:, -1]
277
- - loss = CrossEntropy(y_hat, target)
278
-
279
- Esta é uma simplificação para bug-detection — não é um LM completo.
280
- """
281
- # Target = último token (simplificação para bug-detection)
282
- target = input_ids[:, -1].clone() # (batch,)
283
- # Input = todos os tokens (o modelo faz embedding internamente)
284
- # Passamos input_ids (2D) — o modelo detecta e faz embedding
285
-
286
- # Forward do modelo
287
- temperature = self.meta_cfg.temperature
288
- y_hat, delta = self.model(
289
- input_ids,
290
- temperature=temperature,
291
- use_hypothesis=use_hyp,
292
- stop_grad_hyp=self.config.stop_grad_hyp,
293
- return_aux=False,
294
- )
295
- # y_hat: (batch, vocab_size), delta: (batch, vocab_size)
296
-
297
- # Loss principal: CrossEntropy sobre o último token
298
- loss_main = F.cross_entropy(y_hat, target, ignore_index=self.model.config.pad_token_id)
299
-
300
- # Loss de hipótese: sobre y_final = y_hat + delta
301
- if use_hyp:
302
- y_final = y_hat + delta
303
- loss_hyp = F.cross_entropy(y_final, target, ignore_index=self.model.config.pad_token_id)
304
- else:
305
- loss_hyp = torch.zeros((), device=y_hat.device)
306
-
307
- return loss_main, loss_hyp, y_hat
308
-
309
- def train(self) -> dict:
310
- """Executa treino de bug-detection (2 épocas).
311
-
312
- Returns:
313
- dict com métricas finais.
314
- """
315
- cfg = self.config
316
- n_train = len(self.train_samples)
317
- n_val = len(self.val_samples)
318
- logger.info("=" * 70)
319
- logger.info("BiGRU_T_version — Bug-detection training")
320
- logger.info("=" * 70)
321
- logger.info(f" epochs: {cfg.epochs}")
322
- logger.info(f" train samples: {n_train}")
323
- logger.info(f" val samples: {n_val}")
324
- logger.info(f" batch_size: {cfg.per_device_batch_size}")
325
- logger.info(f" grad_accum: {cfg.grad_accum}")
326
- logger.info(f" lr: {cfg.lr}")
327
- logger.info(f" optimizer: {cfg.optimizer_type}")
328
- logger.info(f" max_seq_len: {cfg.max_seq_len}")
329
- logger.info(f" use_hypothesis: {cfg.use_hypothesis}")
330
- logger.info(f" use_dpo: {cfg.use_dpo}")
331
- logger.info(f" meta_interval: {cfg.meta_interval}")
332
- logger.info(f" time_budget: {cfg.max_total_time_s}s total, {cfg.max_per_epoch_s}s/epoch")
333
- logger.info(f" cleanup_every: {cfg.cleanup_every_n_steps} steps")
334
- logger.info("=" * 70)
335
-
336
- # Conta parâmetros
337
- params = self.model.count_parameters()
338
- logger.info(f" total params: {params['total']:,} ({params['total_M']:.2f}M)")
339
- logger.info(f" trainable: {params['trainable']:,} ({params['trainable_M']:.2f}M)")
340
- logger.info("=" * 70)
341
-
342
- t_start = time.time()
343
- self.model.train()
344
- self.time_budget.reset_total()
345
-
346
- for epoch in range(cfg.epochs):
347
- logger.info(f"\n--- Epoch {epoch+1}/{cfg.epochs} ---")
348
- epoch_start = time.time()
349
- epoch_losses = []
350
- self.time_budget.reset_epoch()
351
-
352
- # Itera sobre samples (loop circular se n_train < batches necessários)
353
- sample_idx = 0
354
- micro_in_epoch = 0
355
-
356
- while micro_in_epoch < n_train:
357
- # Time budget check (estilo Xavante)
358
- if self.time_budget.is_over() or self.time_budget.is_epoch_over():
359
- logger.warning(
360
- "Time budget estourado: total=%s epoch=%s",
361
- self.time_budget.is_over(), self.time_budget.is_epoch_over(),
362
- )
363
- break
364
-
365
- # Pega batch
366
- batch_samples = []
367
- for _ in range(cfg.per_device_batch_size):
368
- s = self.train_samples[sample_idx % n_train]
369
- batch_samples.append(s)
370
- sample_idx += 1
371
- micro_in_epoch += 1
372
- if micro_in_epoch >= n_train:
373
- break
374
-
375
- # Tokeniza
376
- try:
377
- input_ids, attn_mask, labels = self._collate(batch_samples)
378
- except Exception as e:
379
- logger.warning(f"Tokenize erro micro {self.micro_batch_idx}: {e}")
380
- continue
381
-
382
- # Forward + loss — OTIMIZADO: single forward pass
383
- # (antes: 3 forwards por micro-batch; agora: 1 forward com return_aux)
384
- try:
385
- # Target = último token
386
- target = input_ids[:, -1].clone()
387
-
388
- # SINGLE forward: retorna y_hat, delta, e aux (entropy_reg, alpha, circular_info)
389
- y_hat, delta, aux = self.model(
390
- input_ids,
391
- temperature=self.meta_cfg.temperature,
392
- use_hypothesis=True, # sempre computa delta (barato se stop_grad)
393
- stop_grad_hyp=self.config.stop_grad_hyp,
394
- return_aux=True,
395
- use_circular=cfg.use_circular_reasoning,
396
- )
397
-
398
- # Loss principal: CrossEntropy sobre y_hat + entropy_reg (Lema 1)
399
- loss_main = F.cross_entropy(
400
- y_hat, target, ignore_index=self.model.config.pad_token_id
401
- )
402
- loss_main_total = loss_main + aux["entropy_reg"]
403
-
404
- # Decide se usa hipótese baseado no tau (Lema 3)
405
- # use_hyp = True se loss_main > tau (modelo está "lutando")
406
- use_hyp = (
407
- cfg.use_hypothesis
408
- and self.global_step > 0
409
- and bool(loss_main.item() > self.meta_cfg.tau)
410
- )
411
-
412
- # Loss de hipótese: sobre y_final = y_hat + delta
413
- if use_hyp:
414
- y_final = y_hat + delta
415
- loss_hyp = F.cross_entropy(
416
- y_final, target,
417
- ignore_index=self.model.config.pad_token_id,
418
- )
419
- else:
420
- loss_hyp = torch.zeros((), device=y_hat.device)
421
-
422
- # V3: DPO loss — se dataset é compatível e estamos num step de DPO
423
- loss_dpo_val = 0.0
424
- if (
425
- self._dpo_compatible
426
- and self.global_step % cfg.dpo_every_n_steps == 0
427
- and len(batch_samples) > 0
428
- ):
429
- try:
430
- loss_dpo = self._compute_dpo_loss(batch_samples, y_hat, target)
431
- if loss_dpo is not None:
432
- loss_main_total = loss_main_total + cfg.dpo_loss_weight * loss_dpo
433
- loss_dpo_val = float(loss_dpo.item())
434
- except Exception as dpo_err:
435
- logger.debug(f"DPO step falhou (não crítico): {dpo_err}")
436
-
437
- except RuntimeError as e:
438
- err = str(e).lower()
439
- if "out of memory" in err:
440
- logger.warning(f"OOM micro {self.micro_batch_idx}, limpando")
441
- gc.collect()
442
- self.optimizer.zero_grad()
443
- continue
444
- else:
445
- logger.error(f"Runtime erro micro {self.micro_batch_idx}: {e}")
446
- traceback.print_exc()
447
- break
448
-
449
- # DPO beta dinâmico (se DPO ativo) — para logging no optimizer
450
- if self._dpo_compatible:
451
- beta_t = compute_dynamic_beta(
452
- self.global_step,
453
- warmup_steps=cfg.dpo_warmup_steps,
454
- beta_min=cfg.dpo_beta_min,
455
- beta_max=cfg.dpo_beta_max,
456
- )
457
- if hasattr(self.optimizer, "set_beta_dpo"):
458
- self.optimizer.set_beta_dpo(beta_t)
459
-
460
- # Backward + step
461
- self.step_timer.start()
462
- l2_applied = False
463
- grad_hyp_norm = 0.0
464
- orth_proj_norm = 0.0
465
- if use_hyp and loss_hyp.requires_grad:
466
- # Lema 2: gradient surgery
467
- apply_gradient_surgery(self.model, loss_main_total, loss_hyp)
468
- l2_applied = True
469
- # Mede gradientes para o monitor (após surgery)
470
- try:
471
- grad_norms = [
472
- p.grad.norm().item()
473
- for p in self.model.parameters()
474
- if p.grad is not None
475
- ]
476
- if grad_norms:
477
- grad_hyp_norm = sum(n ** 2 for n in grad_norms) ** 0.5
478
- except Exception:
479
- pass
480
- # Clip
481
- torch.nn.utils.clip_grad_norm_(self.model.parameters(), cfg.max_grad_norm)
482
- # HW optimizer: step aceita loss para logging
483
- if isinstance(self.optimizer, HamiltonianWassersteinOptimizer):
484
- self.optimizer.step(loss=float(loss_main_total.item()))
485
- else:
486
- self.optimizer.step()
487
- self.optimizer.zero_grad()
488
- else:
489
- # Apenas loss_main
490
- self.optimizer.zero_grad()
491
- loss_main_total.backward()
492
- torch.nn.utils.clip_grad_norm_(self.model.parameters(), cfg.max_grad_norm)
493
- if isinstance(self.optimizer, HamiltonianWassersteinOptimizer):
494
- self.optimizer.step(loss=float(loss_main_total.item()))
495
- else:
496
- self.optimizer.step()
497
- self.step_timer.stop(step_label=str(self.global_step))
498
-
499
- # V3: Hypothesis Monitor — registra estado do step
500
- self.hyp_monitor.record(
501
- step=self.global_step,
502
- alpha=aux.get("alpha"),
503
- entropy_reg=float(aux.get("entropy_reg", 0).item()
504
- if hasattr(aux.get("entropy_reg", 0), "item") else 0),
505
- loss_main=float(loss_main.item()),
506
- tau=float(self.meta_cfg.tau),
507
- use_hyp=use_hyp,
508
- delta=delta if use_hyp else None,
509
- temperature=self.meta_cfg.temperature,
510
- circular_info=aux.get("circular_info"),
511
- l2_applied=l2_applied,
512
- grad_hyp_norm=grad_hyp_norm,
513
- orth_proj_norm=orth_proj_norm,
514
- )
515
-
516
- self.micro_batch_idx += 1
517
-
518
- # Log
519
- current_loss = float(loss_main.item())
520
- self.losses_history.append(current_loss)
521
- epoch_losses.append(current_loss)
522
- if current_loss < self.best_loss:
523
- self.best_loss = current_loss
524
-
525
- # Verifica se é um optimizer step (a cada grad_accum micro-batches)
526
- if self.micro_batch_idx % cfg.grad_accum == 0:
527
- self.global_step += 1
528
-
529
- # Kill-switch
530
- active = self.model.get_active_modules()
531
- state = self.kill_switch.check(self.global_step, current_loss, active)
532
- if state.reason:
533
- logger.error(f"KILL SWITCH triggered: {state.reason}")
534
- logger.error(
535
- f" RAM {state.ram_pct:.1f}% | disk free {state.disk_free_gb:.2f}GB | "
536
- f"ppl {state.ppl:.2f}"
537
- )
538
- self.killed_reason = state.reason
539
- break
540
-
541
- # Log
542
- if self.global_step % cfg.log_every == 0 or self.global_step == 1:
543
- elapsed = time.time() - t_start
544
- avg_loss = sum(self.losses_history[-cfg.grad_accum:]) / max(
545
- 1, min(cfg.grad_accum, len(self.losses_history))
546
- )
547
- ppl = math.exp(min(20, avg_loss)) if avg_loss < 20 else float("inf")
548
- # V3: info do Circular Reasoning (último step registrado)
549
- crw_str = ""
550
- last_state = self.hyp_monitor._history[-1] if self.hyp_monitor._history else None
551
- if last_state and last_state.crw_w2_final > 0:
552
- crw_str = (
553
- f" | CRW: w2={last_state.crw_w2_final:.4f} "
554
- f"q̄={last_state.crw_q_avg:.3f} "
555
- f"{'✓' if last_state.crw_contraction_valid else '⚠'}"
556
- )
557
- dpo_str = f" dpo={loss_dpo_val:.3f}" if loss_dpo_val > 0 else ""
558
- logger.info(
559
- f" step {self.global_step} | epoch {epoch+1} | "
560
- f"loss={avg_loss:.4f} ppl={ppl:.2f}{dpo_str} | "
561
- f"RAM {state.ram_pct:.1f}% disk {state.disk_free_gb:.1f}GB | "
562
- f"modules={active} | T={self.meta_cfg.temperature:.3f} "
563
- f"τ={self.meta_cfg.tau:.3f} | "
564
- f"α_ent={last_state.alpha_entropy:.3f} "
565
- f"α_max={last_state.alpha_max:.3f}"
566
- f"{crw_str} | "
567
- f"elapsed={elapsed:.0f}s"
568
- )
569
-
570
- # Checkpoint temporário
571
- if self.global_step % cfg.save_temp_every == 0:
572
- self._save_temp_checkpoint(epoch)
573
-
574
- # Memory cleanup agressivo (estilo Xavante)
575
- if self.global_step % cfg.cleanup_every_n_steps == 0:
576
- cleanup_info = aggressive_cleanup(verbose=False)
577
- if self.global_step % (cfg.cleanup_every_n_steps * 5) == 0:
578
- logger.info(
579
- " cleanup step %d: RSS %.1f MB (freed %.1f MB)",
580
- self.global_step,
581
- cleanup_info["rss_after_mb"],
582
- cleanup_info["freed_mb"],
583
- )
584
-
585
- # Meta-configurator (Lema 4)
586
- if self.global_step % cfg.meta_interval == 0 and self.val_samples:
587
- self._run_meta_update()
588
-
589
- if self.killed_reason:
590
- break
591
-
592
- # Fim da época — cleanup agressivo
593
- aggressive_cleanup(verbose=True)
594
-
595
- # Fim da época
596
- epoch_avg = sum(epoch_losses) / max(1, len(epoch_losses))
597
- epoch_ppl = math.exp(min(20, epoch_avg)) if epoch_avg < 20 else float("inf")
598
- logger.info(
599
- f" Epoch {epoch+1} done: avg_loss={epoch_avg:.4f} ppl={epoch_ppl:.2f} "
600
- f"({time.time()-epoch_start:.1f}s)"
601
- )
602
-
603
- # SALVAR MODELO APÓS CADA ÉPOCA (garante checkpoint mesmo se OOM no fim)
604
- try:
605
- partial_result = {
606
- "epochs_completed": epoch + 1,
607
- "killed": False,
608
- "kill_reason": None, # V3 FIX: era missing → KeyError no _save_final_model
609
- "global_reason": None,
610
- "global_step": self.global_step,
611
- "micro_batches": self.micro_batch_idx,
612
- "best_loss": self.best_loss,
613
- "final_loss": epoch_losses[-1] if epoch_losses else float("nan"),
614
- "final_avg_loss": epoch_avg,
615
- "final_perplexity": epoch_ppl,
616
- "elapsed_s": time.time() - t_start,
617
- "params_total": params["total"],
618
- "params_M": params["total_M"],
619
- }
620
- self._save_final_model(partial_result)
621
- logger.info(f" Modelo salvo após epoch {epoch+1}")
622
- except Exception as e:
623
- logger.warning(f" Falha ao salvar após epoch {epoch+1}: {e}")
624
-
625
- # Fim do treino
626
- elapsed_total = time.time() - t_start
627
- final_loss = self.losses_history[-1] if self.losses_history else float("nan")
628
- final_avg_loss = (
629
- sum(self.losses_history[-cfg.grad_accum:]) / max(1, min(cfg.grad_accum, len(self.losses_history)))
630
- if self.losses_history else float("nan")
631
- )
632
- final_ppl = math.exp(min(20, final_avg_loss)) if final_avg_loss < 20 else float("inf")
633
- ks_summary = self.kill_switch.summary()
634
-
635
- result = {
636
- "epochs_completed": epoch + 1 if not self.killed_reason else epoch,
637
- "killed": self.killed_reason is not None,
638
- "kill_reason": self.killed_reason,
639
- "global_reason": self.killed_reason,
640
- "global_step": self.global_step,
641
- "micro_batches": self.micro_batch_idx,
642
- "best_loss": self.best_loss,
643
- "final_loss": final_loss,
644
- "final_avg_loss": final_avg_loss,
645
- "final_perplexity": final_ppl,
646
- "elapsed_s": elapsed_total,
647
- "params_total": params["total"],
648
- "params_M": params["total_M"],
649
- "monitor_summary": ks_summary,
650
- "optimizer": cfg.optimizer_type,
651
- "time_budget": self.time_budget.summary(),
652
- "step_timer": self.step_timer.summary(),
653
- }
654
-
655
- # V3: Log final do Hypothesis Monitor (detecção de bugs)
656
- self.hyp_monitor.log_summary()
657
- hyp_report = self.hyp_monitor.summary()
658
- result["hypothesis_monitor"] = {
659
- "total_steps": hyp_report.total_steps,
660
- "punishment_steps": hyp_report.punishment_steps,
661
- "hyp_activated_steps": hyp_report.hyp_activated_steps,
662
- "hyp_activated_during_punishment": hyp_report.hyp_activated_during_punishment,
663
- "delta_zero_when_activated": hyp_report.delta_zero_when_activated,
664
- "l1_entropy_saturated_steps": hyp_report.l1_entropy_saturated_steps,
665
- "l2_zero_grad_hyp_steps": hyp_report.l2_zero_grad_hyp_steps,
666
- "crw_converged_steps": hyp_report.crw_converged_steps,
667
- "crw_contraction_invalid_steps": hyp_report.crw_contraction_invalid_steps,
668
- "last_T": hyp_report.last_T,
669
- "last_tau": hyp_report.last_tau,
670
- "has_bug": hyp_report.has_bug,
671
- "bugs": hyp_report.bug_summary,
672
- }
673
- result["dpo_activated"] = self._dpo_compatible
674
-
675
- logger.info("=" * 70)
676
- logger.info("TREINO CONCLUÍDO")
677
- logger.info(f" epochs completed: {result['epochs_completed']}")
678
- logger.info(f" killed: {result['killed']}")
679
- if result["killed"]:
680
- logger.info(f" kill reason: {result['kill_reason']}")
681
- logger.info(f" global step: {result['global_step']}")
682
- logger.info(f" best loss: {result['best_loss']:.4f}")
683
- logger.info(f" final avg loss: {result['final_avg_loss']:.4f}")
684
- logger.info(f" final perplexity: {result['final_perplexity']:.2f}")
685
- logger.info(f" elapsed: {result['elapsed_s']:.1f}s")
686
- logger.info(f" max RAM: {ks_summary.get('max_ram_pct', 0):.1f}%")
687
- logger.info(f" min disk free: {ks_summary.get('min_disk_free_gb', 0):.2f}GB")
688
- logger.info(f" DPO ativado: {result['dpo_activated']}")
689
- logger.info(f" CircularReason: {cfg.use_circular_reasoning}")
690
- logger.info(f" Hipótese monitor: {'⚠️ BUGS' if hyp_report.has_bug else '✓ OK'}")
691
- logger.info("=" * 70)
692
-
693
- # Salva modelo final
694
- if not result["killed"]:
695
- self._save_final_model(result)
696
-
697
- # Cleanup checkpoints temporários
698
- if not cfg.keep_temp:
699
- self._cleanup_temp()
700
-
701
- return result
702
-
703
- def _compute_dpo_loss(
704
- self,
705
- batch_samples: list,
706
- y_hat: torch.Tensor,
707
- target: torch.Tensor,
708
- ) -> Optional[torch.Tensor]:
709
- """V3: Computa DPO loss para o batch.
710
-
711
- Estratégia: para datasets compatíveis (com label_text = resposta correta),
712
- sintetiza pares de preferência:
713
- - chosen = resposta correta (label_text do próprio sample)
714
- - rejected = resposta de OUTRO sample (mistura) — proxy para "resposta errada"
715
-
716
- Como o modelo produz apenas 1 logit por amostra (não LM completo),
717
- usamos uma aproximação DPO simplificada:
718
- - policy_chosen_logps = log π(target | y_hat) (log-softmax do target)
719
- - policy_rejected_logps = log π(rejected_token | y_hat) (de outro sample)
720
- - reference = zeros (modelo congelado no início = policy inicial)
721
-
722
- Isto NÃO é DPO canônico (que requer sequence logps), mas captura a
723
- essência: punir o modelo por preferir "resposta errada" e recompensar
724
- por preferir "resposta certa". Adequado para bug-detection.
725
-
726
- Args:
727
- batch_samples: lista de ProcessedSample
728
- y_hat: (B, vocab) logits do modelo para o batch
729
- target: (B,) token alvo (último token da sequência)
730
-
731
- Returns:
732
- loss_dpo (escalar com grad) ou None se não aplicável
733
- """
734
- if not batch_samples or not hasattr(batch_samples[0], "label_text"):
735
- return None
736
-
737
- B = y_hat.size(0)
738
- if B < 1:
739
- return None
740
-
741
- # Policy log-probs (com grad)
742
- log_probs = F.log_softmax(y_hat, dim=-1) # (B, vocab)
743
-
744
- # chosen = target do próprio sample
745
- chosen_logps = log_probs.gather(
746
- 1, target.clamp(0, log_probs.size(-1) - 1).unsqueeze(-1)
747
- ).squeeze(-1) # (B,)
748
-
749
- # rejected = target de OUTRO sample (roll-by-1) — proxy para "resposta errada"
750
- rejected_target = target.roll(1, dims=0) # shift circular
751
- # Se B == 1, não há outro sample — usa token aleatório como rejected
752
- if B == 1:
753
- rejected_target = torch.randint(
754
- 0, log_probs.size(-1), (1,), dtype=target.dtype
755
- )
756
- rejected_logps = log_probs.gather(
757
- 1, rejected_target.clamp(0, log_probs.size(-1) - 1).unsqueeze(-1)
758
- ).squeeze(-1) # (B,)
759
-
760
- # Reference log-probs (sem grad) — política inicial = uniforme
761
- # log π_ref(token) = -log(vocab_size)
762
- vocab_size = log_probs.size(-1)
763
- ref_chosen_logps = torch.full_like(chosen_logps, -math.log(vocab_size))
764
- ref_rejected_logps = torch.full_like(rejected_logps, -math.log(vocab_size))
765
-
766
- # β dinâmico
767
- beta_t = compute_dynamic_beta(
768
- self.global_step,
769
- warmup_steps=self.config.dpo_warmup_steps,
770
- beta_min=self.config.dpo_beta_min,
771
- beta_max=self.config.dpo_beta_max,
772
- )
773
-
774
- # DPO loss
775
- from .dpo import dpo_loss
776
- loss = dpo_loss(
777
- policy_chosen_logps=chosen_logps,
778
- policy_rejected_logps=rejected_logps,
779
- reference_chosen_logps=ref_chosen_logps,
780
- reference_rejected_logps=ref_rejected_logps,
781
- beta=beta_t,
782
- label_smoothing=self.config.dpo_label_smoothing,
783
- use_ipo=self.config.dpo_use_ipo,
784
- )
785
- return loss
786
-
787
- def _run_meta_update(self):
788
- """Executa um passo do MetaConfigurator (Lema 4)."""
789
- # Pega um batch de validação
790
- val_batch = []
791
- for i in range(self.config.per_device_batch_size):
792
- s = self.val_samples[i % len(self.val_samples)]
793
- val_batch.append(s)
794
- try:
795
- input_ids, attn_mask, labels = self._collate(val_batch)
796
- # Target = último token (mesma lógica de _forward_loss)
797
- target = input_ids[:, -1].clone() # (batch,)
798
- meta_result = self.meta_cfg.forward_with_meta(input_ids, target)
799
- # V3: registra no monitor para detectar estagnação do L4
800
- self.hyp_monitor.record_meta(
801
- T_new=meta_result["T_new"],
802
- tau_new=meta_result["tau_new"],
803
- sharpness=meta_result["sharpness"],
804
- )
805
- logger.info(
806
- f" META step {self.global_step}: "
807
- f"val_loss={meta_result['loss_val']:.4f} "
808
- f"sharpness={meta_result['sharpness']:.4f} "
809
- f"T={meta_result['T_new']:.4f} τ={meta_result['tau_new']:.4f} "
810
- f"ΔT={meta_result['grad_log_T']:.6f} "
811
- f"Δτ={meta_result['grad_log_tau']:.6f}"
812
- )
813
- except Exception as e:
814
- logger.warning(f"Meta update failed: {e}")
815
-
816
- def _save_temp_checkpoint(self, epoch: int):
817
- """Salva checkpoint temporário (será apagado ao final)."""
818
- path = Path(self.config.temp_dir) / f"temp_step{self.global_step}_epoch{epoch+1}.pt"
819
- torch.save({
820
- "model_state_dict": self.model.state_dict(),
821
- "optimizer_state_dict": self.optimizer.state_dict(),
822
- "global_step": self.global_step,
823
- "epoch": epoch,
824
- "best_loss": self.best_loss,
825
- "meta_log_T": self.meta_cfg.log_temperature.detach().cpu(),
826
- "meta_log_tau": self.meta_cfg.log_tau.detach().cpu(),
827
- }, path)
828
-
829
- def _save_final_model(self, result: dict):
830
- """Salva modelo final + config + tokenizer."""
831
- model_dir = Path(self.config.output_dir)
832
- model_dir.mkdir(parents=True, exist_ok=True)
833
-
834
- # State dict
835
- state_path = model_dir / "pytorch_model.bin"
836
- torch.save(self.model.state_dict(), state_path)
837
- logger.info(f"Modelo final salvo: {state_path}")
838
-
839
- # Config
840
- cfg = self.model.config
841
- config_dict = {
842
- "model_type": "bigru_t_version",
843
- "arch": "BiGRU_T_version (4 lemas)",
844
- "vocab_size": cfg.vocab_size,
845
- "d_model": cfg.d_model,
846
- "max_seq_len": cfg.max_seq_len,
847
- "pad_token_id": cfg.pad_token_id,
848
- "input_dim": cfg.input_dim,
849
- "bigru_hidden": cfg.bigru_hidden,
850
- "d_transformer": cfg.d_transformer,
851
- "nhead_tu": cfg.nhead_tu,
852
- "d_ff_tu": cfg.d_ff_tu,
853
- "output_dim_u8cell": cfg.output_dim_u8cell,
854
- "max_modules": cfg.max_modules,
855
- "lambda_ent": cfg.lambda_ent,
856
- "cache_len": cfg.cache_len,
857
- "d_cache": cfg.d_cache,
858
- "nhead_orq": cfg.nhead_orq,
859
- "d_ff_orq": cfg.d_ff_orq,
860
- "trainT_dim": cfg.trainT_dim,
861
- "nhead_train": cfg.nhead_train,
862
- "d_ff_train": cfg.d_ff_train,
863
- "num_layers_train": cfg.num_layers_train,
864
- "hypT_dim": cfg.hypT_dim,
865
- "nhead_hyp": cfg.nhead_hyp,
866
- "d_ff_hyp": cfg.d_ff_hyp,
867
- "num_layers_hyp": cfg.num_layers_hyp,
868
- "num_bits": cfg.num_bits,
869
- "dropout": cfg.dropout,
870
- "total_params": result["params_total"],
871
- "total_params_M": result["params_M"],
872
- "training": {
873
- "version": "BiGRU_T_version",
874
- "epochs": result["epochs_completed"],
875
- "global_step": result["global_step"],
876
- "best_loss": result["best_loss"],
877
- "final_loss": result["final_loss"],
878
- "final_avg_loss": result["final_avg_loss"],
879
- "final_perplexity": result["final_perplexity"],
880
- "killed": result["killed"],
881
- "kill_reason": result["kill_reason"],
882
- "elapsed_s": result["elapsed_s"],
883
- "datasets": self.config.datasets.split(","),
884
- "max_samples_per_dataset": self.config.max_samples_per_dataset,
885
- },
886
- }
887
- with open(model_dir / "config.json", "w") as f:
888
- json.dump(config_dict, f, indent=2, ensure_ascii=False)
889
-
890
- # Tokenizer (copia do source)
891
- src_tok = Path("/home/z/my-project/source/model_final/tokenizer/tokenizer.json")
892
- if src_tok.exists():
893
- tok_dest = model_dir / "tokenizer"
894
- tok_dest.mkdir(exist_ok=True)
895
- shutil.copy(src_tok, tok_dest / "tokenizer.json")
896
- logger.info(f"Tokenizer copiado para {tok_dest}")
897
-
898
- # Training report
899
- report = {
900
- "version": "BiGRU_T_version",
901
- "training_params": {
902
- "epochs": self.config.epochs,
903
- "per_device_batch_size": self.config.per_device_batch_size,
904
- "grad_accum": self.config.grad_accum,
905
- "lr": self.config.lr,
906
- "weight_decay": self.config.weight_decay,
907
- "max_grad_norm": self.config.max_grad_norm,
908
- "max_seq_len": self.config.max_seq_len,
909
- "use_hypothesis": self.config.use_hypothesis,
910
- "stop_grad_hyp": self.config.stop_grad_hyp,
911
- "meta_interval": self.config.meta_interval,
912
- "meta_lr": self.config.meta_lr,
913
- "sharpness_lambda": self.config.sharpness_lambda,
914
- },
915
- "results": result,
916
- }
917
- report_path = Path(self.config.output_dir).parent / "training_report.json"
918
- with open(report_path, "w") as f:
919
- json.dump(report, f, indent=2, ensure_ascii=False, default=str)
920
- logger.info(f"Relatório salvo: {report_path}")
921
-
922
- def _cleanup_temp(self):
923
- """Apaga checkpoints temporários (especificação do usuário)."""
924
- temp_dir = Path(self.config.temp_dir)
925
- if not temp_dir.exists():
926
- return
927
- for f in temp_dir.glob("temp_step*.pt"):
928
- try:
929
- f.unlink()
930
- logger.info(f"Temp checkpoint apagado: {f.name}")
931
- except Exception as e:
932
- logger.warning(f"Falha ao apagar {f}: {e}")
933
- # Apaga o diretório se vazio
934
- try:
935
- temp_dir.rmdir()
936
- except Exception:
937
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
streaming_datasets.py DELETED
@@ -1,995 +0,0 @@
1
- """streaming_datasets_v13_9.py — Streaming dataset loader for v13.9.1 training.
2
-
3
- Carrega os 9 datasets especificados pelo usuário via streaming (IterableDataset),
4
- um por vez, exaustivamente dentro do limite max_samples_per_dataset.
5
-
6
- Datasets:
7
- 1. CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1
8
- 2. Madras1/corpus-ptbr-v2
9
- 3. rhaymison/multmodal_175k_portuguese
10
- 4. TucanoBR/GigaVerbo
11
- 5. nvidia/OpenMathReasoning
12
- 6. MathLLMs/MathVision
13
- 7. nvidia/OpenMathInstruct-2
14
- 8. dominguesm/restore-punctuation-ptbr-dataset
15
- 9. carolina-c4ai/corpus-carolina
16
-
17
- Memory-efficient: streaming=True, no full materialization.
18
- Returns ProcessedSample with raw_text only (tokenizer handles encoding).
19
- """
20
- from __future__ import annotations
21
-
22
- import logging
23
- import os
24
- import time
25
- import traceback
26
- from dataclasses import dataclass, field
27
- from typing import Any, Dict, Iterator, List, Optional, Tuple
28
-
29
- import torch
30
-
31
- logger = logging.getLogger(__name__)
32
-
33
- # Datasets V13.9.1 — exatamente os 9 especificados pelo usuário
34
- # V6.5-final: adicionado dominguesm/Canarim-Instruct-PTBR-Dataset
35
- # V6.5-7ds: adicionados adalbertojunior/punctuation-ptbr,
36
- # iara-project/news-articles-ptbr-dataset, manoela/noticias_ptbr
37
- # V6.5-V2: adicionado BrunoN-Dev/corpus-ptbr-v1 como 8º dataset de CONHECIMENTO
38
- # e usado como dataset de TREINAMENTO COM PUNIÇÃO
39
- DEFAULT_DATASETS = [
40
- "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1",
41
- "Madras1/corpus-ptbr-v2",
42
- "rhaymison/multmodal_175k_portuguese",
43
- "TucanoBR/GigaVerbo",
44
- "nvidia/OpenMathReasoning",
45
- "MathLLMs/MathVision",
46
- "nvidia/OpenMathInstruct-2",
47
- "dominguesm/restore-punctuation-ptbr-dataset",
48
- "carolina-c4ai/corpus-carolina",
49
- "dominguesm/Canarim-Instruct-PTBR-Dataset",
50
- "adalbertojunior/punctuation-ptbr",
51
- "iara-project/news-articles-ptbr-dataset",
52
- "manoela/noticias_ptbr",
53
- "BrunoN-Dev/corpus-ptbr-v1",
54
- ]
55
-
56
- # V6.5-V2 — Sequência de 8 datasets para a fase CONHECIMENTO (na ordem exata
57
- # especificada pelo usuário). Meta mínima: 8000 samples ou mais.
58
- CONHECIMENTO_DATASETS_V2 = [
59
- "dominguesm/restore-punctuation-ptbr-dataset",
60
- "carolina-c4ai/corpus-carolina",
61
- "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1",
62
- "dominguesm/Canarim-Instruct-PTBR-Dataset",
63
- "adalbertojunior/punctuation-ptbr",
64
- "iara-project/news-articles-ptbr-dataset",
65
- "manoela/noticias_ptbr",
66
- "BrunoN-Dev/corpus-ptbr-v1",
67
- ]
68
-
69
- # V6.5-V2 — Dataset para TREINAMENTO COM PUNIÇÃO (500 samples).
70
- # O usuário especificou: "dataset para TREINAMENTO COM PUNIÇÃO ATIVA
71
- # 'BrunoN-Dev/corpus-ptbr-v1'".
72
- PUNICAO_DATASET_V2 = "BrunoN-Dev/corpus-ptbr-v1"
73
-
74
- # Dataset format descriptors — Lista priorizada de campos de texto
75
- DATASET_FORMATS: Dict[str, Dict[str, Any]] = {
76
- "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1": {
77
- "type": "chat",
78
- "text_fields": ["conversa", "text", "content", "conversation", "messages"],
79
- "label_fields": [],
80
- "split": "train",
81
- "config": None,
82
- },
83
- "Madras1/corpus-ptbr-v2": {
84
- "type": "text",
85
- "text_fields": ["text", "content", "document", "body"],
86
- "label_fields": [],
87
- "split": "train",
88
- "config": None,
89
- },
90
- "rhaymison/multmodal_175k_portuguese": {
91
- "type": "multimodal",
92
- "text_fields": ["description", "text", "question", "prompt", "instruction", "input"],
93
- "image_fields": ["image", "image_url"],
94
- "label_fields": ["answer", "response", "output"],
95
- "split": "train",
96
- "config": None,
97
- },
98
- "TucanoBR/GigaVerbo": {
99
- "type": "text",
100
- "text_fields": ["text", "content", "document", "body"],
101
- "label_fields": [],
102
- "split": "train",
103
- "config": None,
104
- },
105
- "nvidia/OpenMathReasoning": {
106
- "type": "math",
107
- "text_fields": ["problem", "question", "input", "prompt"],
108
- "label_fields": ["solution", "answer", "output", "response"],
109
- # BUG FIX: OpenMathReasoning has splits ['cot', 'tir', 'genselect', 'additional_problems']
110
- "split": "cot", # Use 'cot' (chain-of-thought) as primary split
111
- "config": None,
112
- },
113
- "MathLLMs/MathVision": {
114
- "type": "math_multimodal",
115
- "text_fields": ["question", "problem", "text", "query"],
116
- "image_fields": ["image", "image_url"],
117
- "label_fields": ["answer", "solution", "response"],
118
- # BUG FIX: MathVision has only 'test' and 'testmini' splits (no train)
119
- "split": "test",
120
- "config": None,
121
- },
122
- "nvidia/OpenMathInstruct-2": {
123
- "type": "math",
124
- "text_fields": ["problem", "question", "input", "prompt"],
125
- # V3 FIX: campos reais do dataset são generated_solution e expected_answer
126
- "label_fields": ["generated_solution", "expected_answer", "solution", "answer", "output", "response"],
127
- "split": "train",
128
- "config": None,
129
- },
130
- "dominguesm/restore-punctuation-ptbr-dataset": {
131
- "type": "punctuation",
132
- "text_fields": ["text", "original", "unpunctuated", "input"],
133
- "label_fields": ["punctuated", "restored", "target", "output"],
134
- "split": "train",
135
- "config": None,
136
- },
137
- "carolina-c4ai/corpus-carolina": {
138
- "type": "text",
139
- "text_fields": ["text", "content", "document", "body", "xml"],
140
- "label_fields": [],
141
- "split": "train",
142
- "config": None,
143
- # V13.9.2-carolina: dataset original usa script Python (não suportado em
144
- # datasets 5.0+). Workaround: stream_xml_gz direto via lxml.iterparse,
145
- # sem usar o script Python. Implementado em _stream_carolina_direct().
146
- },
147
- # ── V13.9.2-finetune-v2: novos datasets PT-BR ───────────────────────────
148
- # Adicionados para fine-tuning contínuo do modelo v13.9.2 (preserva
149
- # arquitetura e parâmetros). Nenhuma mudança estrutural — apenas novos
150
- # descritores de formato para que stream_dataset() reconheça os datasets.
151
- #
152
- # V13.9.2-finetune-v3: padronização para o formato unificado
153
- # "### Instruction:\n...\n\n### Response:\n..." conforme requisitado pelo
154
- # usuário. Apenas os datasets com estrutura instruction/response (orion,
155
- # cnmoro) usam o template; strak2005/bratao continuam como texto puro
156
- # (corpus sem estrutura de instrução). Nenhuma mudança nos 9 datasets
157
- # base — retrocompatibilidade total.
158
- "orion-research/translations-en_US-pt_BR": {
159
- "type": "translation",
160
- # string = EN (instruction), string_translation = PT-BR (response).
161
- # V13.9.2-finetune-v3: separa instruction/response para o template
162
- # unificado em vez de concatenar como texto único.
163
- "text_fields": ["string"],
164
- "label_fields": ["string_translation"],
165
- "split": "train",
166
- "config": None,
167
- "format_template": "instruction_response",
168
- "instruction_prefix": "Translate the following text to Portuguese:",
169
- },
170
- "cnmoro/Instruct-PTBR-10M": {
171
- "type": "instruct",
172
- # NB: campos em MAIÚSCULAS no schema do dataset
173
- # V13.9.2-finetune-v3: INSTRUCTION -> instruction, RESPONSE -> response
174
- # via template unificado.
175
- "text_fields": ["INSTRUCTION"],
176
- "label_fields": ["RESPONSE"],
177
- "split": "train",
178
- "config": None,
179
- "format_template": "instruction_response",
180
- # V13.9.2-finetune-v2: dataset é um único parquet de 10 GB — streaming
181
- # via datasets.load_dataset() é extremamente lento para materializar
182
- # a primeira amostra. Fallback via HF datasets-server rows API (HTTP),
183
- # que retorna batches de 100 rows rapidamente. Implementado em
184
- # _stream_via_rows_api().
185
- "loader": "rows_api",
186
- },
187
- "strak2005/corpus-ptbr-v1": {
188
- "type": "text",
189
- "text_fields": ["text", "content"],
190
- "label_fields": [],
191
- "split": "train",
192
- "config": None,
193
- # Fallback: se strak2005 cair, tenta bratao (mirror idêntico)
194
- "fallback": "bratao/corpus-ptbr-v1",
195
- # V13.9.2-finetune-v2: dataset tem 18 shards parquet de ~1GB cada.
196
- # Streaming via datasets.load_dataset() não materializa primeira
197
- # amostra em tempo aceitável. Fallback: baixa 1 shard localmente e
198
- # itera com pyarrow. Implementado em _stream_via_parquet_first_shard().
199
- "loader": "parquet_first_shard",
200
- },
201
- "bratao/corpus-ptbr-v1": {
202
- "type": "text",
203
- "text_fields": ["text", "content"],
204
- "label_fields": [],
205
- "split": "train",
206
- "config": None,
207
- "loader": "parquet_first_shard",
208
- },
209
- # ── V6.5: novos datasets para esgotar ────────────────────────────────
210
- # User requirement: "esgotar 'dominguesm/restore-punctuation-ptbr-dataset'
211
- # e 'carolina-c4ai/corpus-carolina' e 'nvidia/OpenMathInstruct-2' e
212
- # 'nvidia/OpenMathReasoning' e 'CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1'
213
- # e 'Dexavator/English-PTBR'"
214
- #
215
- # Dexavator/English-PTBR: dataset de tradução EN->PT-BR.
216
- # Estrutura típica: {"english": "...", "portuguese": "..."} ou
217
- # {"en": "...", "pt": "..."}.
218
- "Dexavator/English-PTBR": {
219
- "type": "translation",
220
- "text_fields": ["english", "en", "text", "source"],
221
- "label_fields": ["portuguese", "pt", "target", "translation"],
222
- "split": "train",
223
- "config": None,
224
- "format_template": "instruction_response",
225
- "instruction_prefix": "Translate the following text to Portuguese:",
226
- },
227
- # ── V6.5-final: Canarim-Instruct-PTBR-Dataset ────────────────────────
228
- # User requirement: "esgotar 'dominguesm/Canarim-Instruct-PTBR-Dataset'"
229
- # Canarim-Instruct: dataset PT-BR de instruções (~430k samples).
230
- # Estrutura típica: {"instruction": "...", "input": "...", "output": "..."}
231
- # ou {"text": "...", "conversation": [{"role": "user", "content": "..."}, ...]}
232
- "dominguesm/Canarim-Instruct-PTBR-Dataset": {
233
- "type": "instruct",
234
- "text_fields": ["instruction", "input", "text", "prompt", "question"],
235
- "label_fields": ["output", "response", "answer"],
236
- "split": "train",
237
- "config": None,
238
- "format_template": "instruction_response",
239
- "instruction_prefix": "Instrução:",
240
- },
241
- # ── V6.5-7ds: novos 3 datasets PT-BR ─────────────────────────────────
242
- # User requirement (latest): "streaming até esgotar nesta sequência
243
- # 'dominguesm/restore-punctuation-ptbr-dataset' e
244
- # 'carolina-c4ai/corpus-carolina' e
245
- # 'CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1' e
246
- # 'dominguesm/Canarim-Instruct-PTBR-Dataset' e
247
- # 'adalbertojunior/punctuation-ptbr' e
248
- # 'iara-project/news-articles-ptbr-dataset' e
249
- # 'manoela/noticias_ptbr'"
250
- #
251
- # adalbertojunior/punctuation-ptbr:
252
- # Dataset de restauração de pontuação PT-BR. Schema:
253
- # {id: str, tokens: list[str], pos_tags, chunk_tags, ner_tags}
254
- # O campo "tokens" é uma lista de palavras (sem pontuação). Precisamos
255
- # fazer join com espaços para obter texto bruto. Como o dataset usa
256
- # script Python (datasets 5.0+ não suporta), usamos rows_api loader.
257
- # Splits: train/validation/test com config "punctuation-ptbr".
258
- "adalbertojunior/punctuation-ptbr": {
259
- "type": "punctuation_tokens",
260
- "text_fields": ["tokens"], # list[str] — join com espaços
261
- "label_fields": [],
262
- "split": "train",
263
- "config": "punctuation-ptbr", # necessário para rows_api
264
- "loader": "rows_api",
265
- "join_tokens": True, # flag especial para _normalize_sample
266
- },
267
- # iara-project/news-articles-ptbr-dataset:
268
- # Dataset de notícias PT-BR da Folha de S.Paulo (via IARA project).
269
- # Schema: {title, text, date, category, category_natural_language, link}
270
- # Streaming normal via parquet — primeira amostra materializa rápido.
271
- # "text" é o corpo da notícia, "title" é o título. Concatenamos ambos
272
- # via format_template="title_text_concat" para enriquecer contexto.
273
- # O campo "category" é usado como label (categoria da notícia).
274
- "iara-project/news-articles-ptbr-dataset": {
275
- "type": "news",
276
- # text_fields em ordem de prioridade: "text" (corpo) primeiro
277
- "text_fields": ["text", "title", "content", "body"],
278
- "label_fields": ["category", "category_natural_language", "title"],
279
- "split": "train",
280
- "config": None,
281
- # V6.5-7ds: title_text_concat usa title como label (prefixo)
282
- # e text como corpo. Aqui label_fields pega "category" primeiro
283
- # para dar contexto temático.
284
- "format_template": "title_text_concat",
285
- },
286
- # manoela/noticias_ptbr:
287
- # Mirror do iara-project/news-articles-ptbr-dataset (mesmo parquet,
288
- # mesmo schema). Streaming normal via parquet.
289
- "manoela/noticias_ptbr": {
290
- "type": "news",
291
- "text_fields": ["text", "title", "content", "body"],
292
- "label_fields": ["category", "category_natural_language", "title"],
293
- "split": "train",
294
- "config": None,
295
- "format_template": "title_text_concat",
296
- },
297
- # ── V6.5-V2: BrunoN-Dev/corpus-ptbr-v1 ───────────────────────────────
298
- # User requirement: "dataset para TREINAMENTO COM PUNIÇÃO ATIVA
299
- # 'BrunoN-Dev/corpus-ptbr-v1'" e o 8º dataset de CONHECIMENTO.
300
- #
301
- # BrunoN-Dev/corpus-ptbr-v1: corpus PT-BR genérico com 18 shards parquet
302
- # (~1GB cada). Schema: {"text": "...", "meta": "..."}.
303
- # Streaming via datasets.load_dataset(streaming=True) é lento para
304
- # materializar a primeira amostra — usamos o loader parquet_first_shard
305
- # (baixa apenas o primeiro shard e itera localmente com pyarrow).
306
- # Sem label explícita — usamos hash do texto (paridade do nº de palavras)
307
- # para gerar label binário (0/1) — não é dado sintético, é derivação
308
- # algorítmica do conteúdo real do dataset.
309
- "BrunoN-Dev/corpus-ptbr-v1": {
310
- "type": "text",
311
- "text_fields": ["text", "content", "body", "document"],
312
- "label_fields": [],
313
- "split": "train",
314
- "config": None,
315
- "loader": "parquet_first_shard",
316
- # V6.5-V2: gera label binário determinístico via hash do texto
317
- # (paridade do número de palavras) — não é dado sintético, é
318
- # derivação algorítmica do conteúdo real do dataset.
319
- "label_strategy": "hash_parity",
320
- },
321
- }
322
-
323
-
324
- # Lista de arquivos XML.gz do Carolina que tentaremos baixar (ordem: menores
325
- # taxonomias primeiro para minimizar banda e RAM). Selecionamos 2 arquivos
326
- # por taxonomia para garantir diversidade sem explodir a memória.
327
- CAROLINA_XML_FILES = [
328
- # taxonomia "datasets_and_other_corpora" (DAT = pt-BR notícias)
329
- "corpus/datasets_and_other_corpora/pt-BR/DATa.xml.gz",
330
- "corpus/datasets_and_other_corpora/pt-BR/DATb.xml.gz",
331
- # taxonomia "wik" (wikis)
332
- "corpus/wik/WIKa.xml.gz",
333
- "corpus/wik/WIKb.xml.gz",
334
- # taxonomia "uni" (university_domains)
335
- "corpus/uni/UNIa.xml.gz",
336
- "corpus/uni/UNIb.xml.gz",
337
- # taxonomia "pub" (public_domain_works)
338
- "corpus/pub/PUBa.xml.gz",
339
- "corpus/pub/PUBb.xml.gz",
340
- ]
341
-
342
-
343
- def _stream_carolina_direct(
344
- max_samples: int,
345
- hf_token: Optional[str] = None,
346
- timeout_per_file: int = 60,
347
- ) -> Iterator[Dict[str, Any]]:
348
- """Workaround V13.9.2-carolina: carrega Carolina sem usar o script Python.
349
-
350
- Baixa arquivos XML.gz diretamente do repo e faz parse streaming com
351
- lxml.etree.iterparse (compatível com datasets 5.0+). Cada <TEI> vira um
352
- documento de texto (concatenação dos <p> dentro de <body>).
353
-
354
- Args:
355
- max_samples: número máximo de amostras a retornar
356
- hf_token: token HF opcional (dataset é público, mas token evita rate limit)
357
- timeout_per_file: tempo máximo (segundos) por arquivo XML
358
-
359
- Yields:
360
- dict com chave "text" (compatível com _normalize_sample)
361
- """
362
- from huggingface_hub import hf_hub_download
363
- import gzip
364
- from lxml import etree
365
-
366
- TEI_NS = "{http://www.tei-c.org/ns/1.0}"
367
- BODY_P_TAG = f".//{TEI_NS}body/{TEI_NS}p"
368
-
369
- count = 0
370
- for xml_path in CAROLINA_XML_FILES:
371
- if count >= max_samples:
372
- return
373
- try:
374
- local_path = hf_hub_download(
375
- repo_id="carolina-c4ai/corpus-carolina",
376
- filename=xml_path,
377
- repo_type="dataset",
378
- token=hf_token,
379
- )
380
- except Exception as e:
381
- logger.warning(f"Carolina: não foi possível baixar {xml_path}: {e}")
382
- continue
383
-
384
- try:
385
- with gzip.open(local_path, "rb") as gz:
386
- # iterparse streaming: não carrega árvore inteira na memória
387
- for _, tei in etree.iterparse(
388
- gz, huge_tree=True, encoding="utf-8", tag=f"{TEI_NS}TEI"
389
- ):
390
- if count >= max_samples:
391
- tei.clear()
392
- return
393
- # Extrai texto dos <p> dentro de <body>
394
- parts = []
395
- for p in tei.findall(BODY_P_TAG):
396
- if p.text:
397
- parts.append(p.text)
398
- text = " ".join(parts).strip()
399
- if text and len(text) >= 10:
400
- yield {"text": text, "meta": ""}
401
- tei.clear() # libera memória da árvore TEI
402
- count += 1
403
- except Exception as e:
404
- logger.warning(f"Carolina: erro processando {xml_path}: {e}")
405
- continue
406
-
407
- logger.info(f"Carolina: streamou {count} amostras no total")
408
-
409
-
410
- # ═══════════════════════════════════════════════════════════════════════════
411
- # V13.9.2-finetune-v2: Loaders alternativos para datasets parquet grandes
412
- # onde datasets.load_dataset(streaming=True) é inviável (primeira amostra
413
- # demora minutos para materializar).
414
- # ═══════════════════════════════════════════════════════════════════════════
415
-
416
- def _stream_via_rows_api(
417
- dataset_name: str,
418
- max_samples: int,
419
- hf_token: Optional[str] = None,
420
- batch_size: int = 100,
421
- max_offset: int = 100_000,
422
- config: Optional[str] = None,
423
- split: str = "train",
424
- ) -> Iterator[Dict[str, Any]]:
425
- """Carrega amostras via HF datasets-server /rows API (HTTP).
426
-
427
- Usado para datasets cujo parquet é muito grande para streaming eficiente
428
- (ex: cnmoro/Instruct-PTBR-10M com 10 GB em arquivo único) OU datasets
429
- que usam script Python como loader (não suportado em datasets 5.0+,
430
- ex: adalbertojunior/punctuation-ptbr). A rows API retorna batches de
431
- até 100 rows via HTTP, sem precisar baixar o parquet.
432
-
433
- Args:
434
- dataset_name: repo_id do dataset (ex: "cnmoro/Instruct-PTBR-10M")
435
- max_samples: nº máximo de amostras a retornar
436
- hf_token: token HF (opcional — dataset público)
437
- batch_size: nº de rows por requisição (máx 100)
438
- max_offset: offset máximo a tentar (sai do loop se excedido)
439
- config: nome da config do dataset (default: "default"). NECESSÁRIO
440
- para datasets cuja config não é "default" (ex:
441
- adalbertojunior/punctuation-ptbr usa "punctuation-ptbr").
442
- split: nome do split (default: "train").
443
-
444
- Yields:
445
- dict com campos do dataset (compatível com _normalize_sample)
446
- """
447
- import json as _json
448
- import urllib.request
449
- import urllib.parse
450
-
451
- base_url = "https://datasets-server.huggingface.co/rows"
452
- config_name = config or "default"
453
- count = 0
454
- offset = 0
455
-
456
- while count < max_samples and offset < max_offset:
457
- params = urllib.parse.urlencode({
458
- "dataset": dataset_name,
459
- "config": config_name,
460
- "split": split,
461
- "offset": offset,
462
- "length": min(batch_size, max_samples - count),
463
- })
464
- url = f"{base_url}?{params}"
465
- try:
466
- req = urllib.request.Request(url)
467
- if hf_token:
468
- req.add_header("Authorization", f"Bearer {hf_token}")
469
- with urllib.request.urlopen(req, timeout=30) as r:
470
- data = _json.loads(r.read())
471
- except Exception as e:
472
- logger.warning(f"rows_api {dataset_name} config={config_name} offset={offset}: {e}")
473
- break
474
-
475
- rows = data.get("rows", [])
476
- if not rows:
477
- break
478
-
479
- for row in rows:
480
- if count >= max_samples:
481
- return
482
- row_data = row.get("row", {})
483
- # Remove campos internos como __index_level_0__
484
- clean = {k: v for k, v in row_data.items()
485
- if not k.startswith("__")}
486
- yield clean
487
- count += 1
488
-
489
- offset += len(rows)
490
- if len(rows) < batch_size:
491
- break # chegou ao fim do dataset
492
-
493
- logger.info(f"rows_api {dataset_name} config={config_name}: streamou {count} amostras")
494
-
495
-
496
- def _stream_via_parquet_first_shard(
497
- dataset_name: str,
498
- max_samples: int,
499
- hf_token: Optional[str] = None,
500
- ) -> Iterator[Dict[str, Any]]:
501
- """Baixa primeiro shard parquet do dataset e itera localmente com pyarrow.
502
-
503
- Usado para datasets com múltiplos shards parquet grandes (ex:
504
- strak2005/corpus-ptbr-v1 com 18 shards de ~1GB). Streaming via
505
- datasets.load_dataset() não materializa primeira amostra em tempo
506
- aceitável. Esta função baixa o primeiro shard (~26s para 1GB) e itera
507
- localmente — rápido após download.
508
-
509
- Args:
510
- dataset_name: repo_id do dataset
511
- max_samples: nº máximo de amostras
512
- hf_token: token HF opcional
513
-
514
- Yields:
515
- dict com campos do dataset
516
- """
517
- from huggingface_hub import HfApi, hf_hub_download
518
- import pyarrow.parquet as pq
519
-
520
- api = HfApi(token=hf_token)
521
-
522
- # Lista arquivos parquet no repo
523
- try:
524
- info = api.dataset_info(dataset_name)
525
- except Exception as e:
526
- logger.warning(f"parquet_shard {dataset_name}: dataset_info failed: {e}")
527
- return
528
-
529
- parquet_files = sorted([
530
- s.rfilename for s in info.siblings
531
- if s.rfilename.endswith(".parquet")
532
- ])
533
- if not parquet_files:
534
- logger.warning(f"parquet_shard {dataset_name}: nenhum .parquet encontrado")
535
- return
536
-
537
- # Baixa apenas o primeiro shard
538
- first_shard = parquet_files[0]
539
- logger.info(f"parquet_shard {dataset_name}: baixando {first_shard}...")
540
- try:
541
- local_path = hf_hub_download(
542
- repo_id=dataset_name,
543
- filename=first_shard,
544
- repo_type="dataset",
545
- token=hf_token,
546
- )
547
- except Exception as e:
548
- logger.warning(f"parquet_shard {dataset_name}: download failed: {e}")
549
- return
550
-
551
- # Itera com pyarrow.iter_batches — método eficiente que lê batches pequenos
552
- # do parquet sem materializar o row group inteiro na memória.
553
- # BUG FIX V13.9.2-finetune-v2: read_row_group() materializa TODAS as ~1M
554
- # rows de uma vez (lento + ~1GB RAM). iter_batches(batch_size=10) lê só
555
- # 10 rows por vez (rápido + ~10KB RAM).
556
- try:
557
- pf = pq.ParquetFile(local_path)
558
- count = 0
559
- # batch_read_size: nº de rows por batch (pequeno = menos RAM)
560
- for batch in pf.iter_batches(batch_size=100):
561
- if count >= max_samples:
562
- break
563
- cols = batch.schema.names
564
- # to_pylist() é eficiente para batches pequenos
565
- pydict = batch.to_pydict()
566
- n_rows = len(pydict[cols[0]]) if cols else 0
567
- for i in range(n_rows):
568
- if count >= max_samples:
569
- break
570
- row = {col: pydict[col][i] for col in cols}
571
- yield row
572
- count += 1
573
- except Exception as e:
574
- logger.warning(f"parquet_shard {dataset_name}: pyarrow iter failed: {e}")
575
- return
576
-
577
- logger.info(f"parquet_shard {dataset_name}: streamou {count} amostras do shard {first_shard}")
578
-
579
-
580
- @dataclass
581
- class ProcessedSample:
582
- """Uma amostra processada: texto bruto + metadados."""
583
- raw_text: str
584
- dataset_name: str
585
- sample_idx: int
586
- text_field: str = ""
587
- label_text: str = ""
588
-
589
-
590
- def _extract_field(sample: Dict[str, Any], candidate_fields: List[str]) -> Optional[str]:
591
- """Extrai texto da primeira field disponível no sample.
592
-
593
- Suporta:
594
- - strings diretas
595
- - listas de strings (join com espaço — útil para datasets tokenizados
596
- como adalbertojunior/punctuation-ptbr onde "tokens": ["hoje","a",...])
597
- - listas de dicts (chat messages — concatena role: content)
598
- - dicts aninhados (pega text/content/body)
599
- """
600
- for field_name in candidate_fields:
601
- if field_name in sample:
602
- val = sample[field_name]
603
- if val is None:
604
- continue
605
- if isinstance(val, str):
606
- if val.strip():
607
- return val
608
- elif isinstance(val, list):
609
- # V6.5-7ds: se for lista de strings, join com espaços
610
- # (ex: adalbertojunior/punctuation-ptbr tokens field)
611
- string_parts = []
612
- chat_parts = []
613
- all_strings = True
614
- for item in val:
615
- if isinstance(item, str):
616
- if item.strip():
617
- string_parts.append(item)
618
- elif isinstance(item, (int, float)):
619
- # tokens podem vir como ints (token IDs) — skip
620
- all_strings = False
621
- break
622
- elif isinstance(item, dict):
623
- all_strings = False
624
- role = item.get("role", "")
625
- content = item.get("content", "")
626
- if isinstance(content, str) and content.strip():
627
- chat_parts.append(f"{role}: {content}")
628
- else:
629
- all_strings = False
630
- if all_strings and string_parts:
631
- joined = " ".join(string_parts).strip()
632
- if joined:
633
- return joined
634
- if chat_parts:
635
- return "\n".join(chat_parts)
636
- elif isinstance(val, dict):
637
- # Pode ser nested
638
- text = val.get("text") or val.get("content") or val.get("body")
639
- if isinstance(text, str) and text.strip():
640
- return text
641
- return None
642
-
643
-
644
- def _normalize_sample(sample: Dict[str, Any], dataset_name: str, idx: int) -> Optional[ProcessedSample]:
645
- """Normaliza uma amostra bruta em ProcessedSample."""
646
- fmt = DATASET_FORMATS.get(dataset_name, {})
647
- text_fields = fmt.get("text_fields", ["text"])
648
- label_fields = fmt.get("label_fields", [])
649
-
650
- text = _extract_field(sample, text_fields)
651
- if not text or len(text.strip()) < 10: # skip very short
652
- return None
653
-
654
- label = ""
655
- if label_fields:
656
- label = _extract_field(sample, label_fields) or ""
657
-
658
- # Se tem label, aplica template apropriado
659
- template = fmt.get("format_template")
660
- full_text = text
661
-
662
- # V13.9.2-finetune-v3: para o template instruction_response, exige ambos
663
- # os lados (instruction + response). Samples sem response são descartados
664
- # para evitar treinar em texto parcial.
665
- if template == "instruction_response" and not label:
666
- return None
667
-
668
- if label:
669
- # V13.9.2-finetune-v3: format_template unificado "### Instruction:/### Response:"
670
- # quando o dataset declarar explicitamente. Caso contrário, mantém o
671
- # comportamento legado (concatenação simples text\nlabel) para os 9
672
- # datasets base — retrocompatibilidade total.
673
- if template == "instruction_response":
674
- instr_prefix = fmt.get("instruction_prefix", "")
675
- if instr_prefix:
676
- full_text = (
677
- f"### Instruction:\n{instr_prefix}\n{text}\n\n"
678
- f"### Response:\n{label}"
679
- )
680
- else:
681
- full_text = (
682
- f"### Instruction:\n{text}\n\n"
683
- f"### Response:\n{label}"
684
- )
685
- elif template == "title_text_concat":
686
- # V6.5-7ds: para datasets de notícias (iara-project, manoela),
687
- # o campo "text" (corpo da notícia) é o conteúdo principal e
688
- # "category" (categoria) é o contexto temático. Formatamos como
689
- # "TEXTO: <corpo>\nCATEGORIA: <categoria>".
690
- full_text = f"TEXTO: {text}\nCATEGORIA: {label}"
691
- else:
692
- # Comportamento legado (9 datasets base): concatenação simples
693
- full_text = f"{text}\n{label}"
694
-
695
- return ProcessedSample(
696
- raw_text=full_text,
697
- dataset_name=dataset_name,
698
- sample_idx=idx,
699
- text_field=text_fields[0] if text_fields else "",
700
- label_text=label,
701
- )
702
-
703
-
704
- def _load_dataset_streaming(
705
- dataset_name: str,
706
- split: str = "train",
707
- config: Optional[str] = None,
708
- hf_token: Optional[str] = None,
709
- ):
710
- """Carrega dataset em modo streaming.
711
-
712
- BUG FIX V13.9.1: Removido trust_remote_code (deprecated).
713
- Se o dataset não carregar com split especificado, tenta 'train' como fallback.
714
- BUG FIX V13.9.2: Adicionado fallback para datasets com script (carolina-c4ai).
715
- """
716
- from datasets import load_dataset
717
-
718
- # Lista de splits para tentar, em ordem
719
- splits_to_try = [split]
720
- if split != "train":
721
- splits_to_try.append("train")
722
- # Adiciona outros splits comuns
723
- for s in ["test", "validation", "cot", "tir"]:
724
- if s not in splits_to_try:
725
- splits_to_try.append(s)
726
-
727
- fmt = DATASET_FORMATS.get(dataset_name, {})
728
- fallback_name = fmt.get("fallback")
729
- datasets_to_try = [dataset_name]
730
- if fallback_name:
731
- datasets_to_try.append(fallback_name)
732
-
733
- for try_ds_name in datasets_to_try:
734
- for try_split in splits_to_try:
735
- try:
736
- if config:
737
- ds = load_dataset(
738
- try_ds_name, config, split=try_split, streaming=True,
739
- token=hf_token,
740
- )
741
- else:
742
- ds = load_dataset(
743
- try_ds_name, split=try_split, streaming=True,
744
- token=hf_token,
745
- )
746
- logger.info(f"Loaded {try_ds_name} split={try_split}")
747
- return ds
748
- except Exception as e:
749
- err_str = str(e).lower()
750
- # If "Bad split" error, try next split
751
- if "bad split" in err_str or "available splits" in err_str:
752
- logger.info(f" Split {try_split} not available for {try_ds_name}, trying next")
753
- continue
754
- # If "scripts no longer supported", try next dataset (fallback)
755
- if "scripts are no longer supported" in err_str and fallback_name:
756
- logger.info(f" {try_ds_name} uses script — trying fallback {fallback_name}")
757
- break
758
- # For other errors, log and try next split
759
- logger.warning(f"Failed to load {try_ds_name} split={try_split}: {str(e)[:120]}")
760
- continue
761
-
762
- logger.warning(f"All split attempts failed for {dataset_name} (will be skipped)")
763
- return None
764
-
765
-
766
- def stream_dataset(
767
- dataset_name: str,
768
- max_samples: int = 500,
769
- hf_token: Optional[str] = None,
770
- ) -> Iterator[ProcessedSample]:
771
- """Faz streaming de um dataset, retornando até max_samples amostras.
772
-
773
- Args:
774
- dataset_name: nome do dataset no HuggingFace
775
- max_samples: número máximo de amostras a retornar
776
- hf_token: token HF opcional
777
-
778
- Yields:
779
- ProcessedSample
780
- """
781
- fmt = DATASET_FORMATS.get(dataset_name)
782
- if fmt is None:
783
- logger.error(f"Unknown dataset: {dataset_name}")
784
- return
785
-
786
- # ── V13.9.2-carolina: bypass do script Python ─────────────────────────
787
- # O dataset carolina-c4ai/corpus-carolina usa um script Python como loader,
788
- # que não é suportado por datasets 5.0+. Streamamos os XML.gz diretamente.
789
- if dataset_name == "carolina-c4ai/corpus-carolina":
790
- logger.info(f"Carolina: usando carregador direto (bypass do script Python)")
791
- count = 0
792
- try:
793
- for raw_sample in _stream_carolina_direct(
794
- max_samples=max_samples, hf_token=hf_token
795
- ):
796
- processed = _normalize_sample(raw_sample, dataset_name, count)
797
- if processed is not None:
798
- yield processed
799
- count += 1
800
- except Exception as e:
801
- logger.warning(f"Carolina direct stream error at sample {count}: {e}")
802
- return
803
- logger.info(f"Streamed {count} samples from {dataset_name} (direct XML)")
804
- return
805
-
806
- # ── V13.9.2-finetune-v2: loaders alternativos para parquets grandes ───
807
- # Alguns datasets (cnmoro, strak2005/bratao) têm parquets tão grandes que
808
- # datasets.load_dataset(streaming=True) demora minutos para materializar
809
- # a primeira amostra. Usamos loaders alternativos especificados no campo
810
- # "loader" do formato.
811
- loader = fmt.get("loader")
812
- if loader == "rows_api":
813
- logger.info(f"{dataset_name}: usando rows_api loader (datasets-server HTTP)")
814
- count = 0
815
- try:
816
- for raw_sample in _stream_via_rows_api(
817
- dataset_name, max_samples=max_samples, hf_token=hf_token,
818
- config=fmt.get("config"),
819
- split=fmt.get("split", "train"),
820
- ):
821
- processed = _normalize_sample(raw_sample, dataset_name, count)
822
- if processed is not None:
823
- yield processed
824
- count += 1
825
- except Exception as e:
826
- logger.warning(f"rows_api stream error at sample {count}: {e}")
827
- return
828
- logger.info(f"Streamed {count} samples from {dataset_name} (rows_api)")
829
- return
830
-
831
- if loader == "parquet_first_shard":
832
- logger.info(f"{dataset_name}: usando parquet_first_shard loader")
833
- count = 0
834
- try:
835
- for raw_sample in _stream_via_parquet_first_shard(
836
- dataset_name, max_samples=max_samples, hf_token=hf_token
837
- ):
838
- processed = _normalize_sample(raw_sample, dataset_name, count)
839
- if processed is not None:
840
- yield processed
841
- count += 1
842
- except Exception as e:
843
- logger.warning(f"parquet_shard stream error at sample {count}: {e}")
844
- # Tenta fallback (ex: strak2005 -> bratao)
845
- fallback_name = fmt.get("fallback")
846
- if fallback_name and fallback_name != dataset_name:
847
- logger.info(f"Tentando fallback: {fallback_name}")
848
- fallback_fmt = DATASET_FORMATS.get(fallback_name, {})
849
- # Recursão respeitando o loader do fallback
850
- yield from stream_dataset(
851
- fallback_name, max_samples=max_samples, hf_token=hf_token
852
- )
853
- return
854
- logger.info(f"Streamed {count} samples from {dataset_name} (parquet_first_shard)")
855
- return
856
-
857
- ds = _load_dataset_streaming(
858
- dataset_name,
859
- split=fmt.get("split", "train"),
860
- config=fmt.get("config"),
861
- hf_token=hf_token,
862
- )
863
- if ds is None:
864
- return
865
-
866
- count = 0
867
- try:
868
- for idx, sample in enumerate(ds):
869
- if count >= max_samples:
870
- break
871
- processed = _normalize_sample(sample, dataset_name, idx)
872
- if processed is not None:
873
- yield processed
874
- count += 1
875
- except Exception as e:
876
- logger.warning(f"Error streaming {dataset_name} at sample {idx}: {e}")
877
- return
878
-
879
- logger.info(f"Streamed {count} samples from {dataset_name}")
880
-
881
-
882
- def stream_all_datasets(
883
- datasets: Optional[List[str]] = None,
884
- max_samples_per_dataset: int = 500,
885
- hf_token: Optional[str] = None,
886
- ) -> Iterator[ProcessedSample]:
887
- """Faz streaming sequencial de múltiplos datasets.
888
-
889
- Args:
890
- datasets: lista de nomes (default: DEFAULT_DATASETS)
891
- max_samples_per_dataset: limite por dataset
892
- hf_token: token HF opcional
893
-
894
- Yields:
895
- ProcessedSample
896
- """
897
- if datasets is None:
898
- datasets = DEFAULT_DATASETS
899
-
900
- for ds_name in datasets:
901
- logger.info(f"Starting dataset: {ds_name}")
902
- yield from stream_dataset(
903
- ds_name,
904
- max_samples=max_samples_per_dataset,
905
- hf_token=hf_token,
906
- )
907
-
908
-
909
- def collate_samples_to_tensors(
910
- samples: List[ProcessedSample],
911
- tokenizer,
912
- max_seq_len: int = 64,
913
- pad_token_id: int = 0,
914
- ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
915
- """Converte lista de ProcessedSample em tensors para treino.
916
-
917
- BUG FIX V13.9.1: Tokenizer.encode() retorna 'Encoding' object, não list.
918
- Use .ids attribute to get token IDs.
919
-
920
- Returns:
921
- input_ids: (B, T)
922
- attention_mask: (B, T)
923
- labels: (B, T) — same as input_ids (LM training)
924
- """
925
- batch_input_ids = []
926
- batch_attn_mask = []
927
-
928
- for sample in samples:
929
- # Tokenize
930
- encoding = tokenizer.encode(sample.raw_text, add_special_tokens=True)
931
-
932
- # BUG FIX: Handle both `tokenizers.Encoding` and `transformers` tokenizer outputs
933
- if hasattr(encoding, 'ids'):
934
- tokens = encoding.ids # tokenizers library
935
- elif isinstance(encoding, list):
936
- tokens = encoding # already a list of ints
937
- elif hasattr(encoding, 'input_ids'):
938
- tokens = encoding.input_ids # transformers BatchEncoding
939
- else:
940
- tokens = list(encoding) # fallback
941
-
942
- # Truncate to max_seq_len
943
- if len(tokens) > max_seq_len:
944
- tokens = tokens[:max_seq_len]
945
- # Pad
946
- attn_mask = [1] * len(tokens) + [0] * (max_seq_len - len(tokens))
947
- tokens = list(tokens) + [pad_token_id] * (max_seq_len - len(tokens))
948
- batch_input_ids.append(tokens)
949
- batch_attn_mask.append(attn_mask)
950
-
951
- input_ids = torch.tensor(batch_input_ids, dtype=torch.long)
952
- attention_mask = torch.tensor(batch_attn_mask, dtype=torch.long)
953
- labels = input_ids.clone()
954
-
955
- return input_ids, attention_mask, labels
956
-
957
-
958
- # ═══════════════════════════════════════════════════════════════════════════
959
- # Self-test (NÃO enviar para HuggingFace)
960
- # ═══════════════════════════════════════════════════════════════════════════
961
- if __name__ == "__main__":
962
- print("=== Streaming Datasets V13.9.1 Self-Test ===\n")
963
-
964
- # Test 1: Test field extraction
965
- print("Test 1: Field extraction")
966
- sample_chat = {
967
- "messages": [
968
- {"role": "user", "content": "Olá"},
969
- {"role": "assistant", "content": "Como posso ajudar?"},
970
- ]
971
- }
972
- text = _extract_field(sample_chat, ["messages", "text"])
973
- print(f" Chat text: {text!r}")
974
- assert text and "Olá" in text, "Field extraction failed for chat"
975
-
976
- sample_text = {"text": "Hello world"}
977
- text = _extract_field(sample_text, ["text", "content"])
978
- assert text == "Hello world"
979
- print(f" Text field: {text!r}")
980
- print(f" OK\n")
981
-
982
- # Test 2: Test list of datasets
983
- print("Test 2: Dataset list")
984
- print(f" Configured datasets ({len(DEFAULT_DATASETS)}):")
985
- for i, ds in enumerate(DEFAULT_DATASETS, 1):
986
- print(f" {i}. {ds}")
987
- assert len(DEFAULT_DATASETS) == 14
988
- # V6.5-V2: verifica sequência CONHECIMENTO (8 datasets) + PUNICAO (1)
989
- assert len(CONHECIMENTO_DATASETS_V2) == 8
990
- assert PUNICAO_DATASET_V2 == "BrunoN-Dev/corpus-ptbr-v1"
991
- print(f" V2 CONHECIMENTO datasets: {len(CONHECIMENTO_DATASETS_V2)}")
992
- print(f" V2 PUNICAO dataset: {PUNICAO_DATASET_V2}")
993
- print(f" OK\n")
994
-
995
- print("=== ALL STREAMING TESTS PASSED ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
train_v6_5_v2.py DELETED
The diff for this file is too large to render. See raw diff
 
train_v6_5_v2_punicão_only.py DELETED
@@ -1,556 +0,0 @@
1
- """train_v6_5_v2_punicão_only.py — V6.5-V2-dynamic FASE 2 only.
2
-
3
- Carrega o estado salvo da FASE 1 (CONHECIMENTO) e executa apenas a FASE 2
4
- (TREINAMENTO COM PUNIÇÃO) sobre BrunoN-Dev/corpus-ptbr-v1.
5
-
6
- User requirement: "FASE2 TREINAMENTO (meta mínima 1000 samples ou mais)
7
- COM PUNIÇÃO ATIVA para 'BrunoN-Dev/corpus-ptbr-v1' de 100 em 100 samples"
8
-
9
- Este script permite continuar o treinamento a partir do estado salvo sem
10
- precisar re-executar a FASE 1.
11
- """
12
- from __future__ import annotations
13
-
14
- import gc
15
- import json
16
- import logging
17
- import os
18
- import re
19
- import shutil
20
- import sys
21
- import time
22
- import traceback
23
- from datetime import datetime
24
- from pathlib import Path
25
- from typing import Any, Dict, List, Optional
26
-
27
- # ============================================================================
28
- # 0. Paths e logging
29
- # ============================================================================
30
- PROJECT_ROOT = Path("/home/z/my-project")
31
- BIGRU_ROOT = PROJECT_ROOT / "BiGRU_T_version"
32
- SRC_ROOT = BIGRU_ROOT / "src"
33
- DOWNLOAD_DIR = PROJECT_ROOT / "download"
34
- DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
35
-
36
- PUNICAO_REPORT_PATH = BIGRU_ROOT / "v6_5_v2_punicão_report.json"
37
- PUNICAO_STATES_PATH = BIGRU_ROOT / "v6_5_v2_punicão_model_states.pt"
38
- PUNICAO_UPLOAD_REPORT_PATH = BIGRU_ROOT / "v6_5_v2_punicão_upload_report.json"
39
-
40
- # Estado salvo da FASE 1
41
- FASE1_STATES_PATH = BIGRU_ROOT / "v6_5_v2_model_states.pt"
42
-
43
- logging.basicConfig(
44
- level=logging.INFO,
45
- format="[%(asctime)s] [%(levelname)s] %(message)s",
46
- datefmt="%H:%M:%S",
47
- )
48
- logger = logging.getLogger("train_v6_5_v2_punicão")
49
-
50
- # ============================================================================
51
- # 1. ATIVAR xeon_runtime.py
52
- # ============================================================================
53
- os.environ["V65_ENABLE_STREAMING"] = "1"
54
- sys.path.insert(0, str(SRC_ROOT))
55
-
56
- from bigru_t.utils.xeon_runtime import ( # noqa: E402
57
- optimize_xeon_environment,
58
- benchmark_fp16_matmul,
59
- )
60
- N_CORES = optimize_xeon_environment(verbose=False)
61
- logger.info(f"[V6.5-V2-punicão] Xeon cores: {N_CORES}")
62
-
63
- # ============================================================================
64
- # 2. Configurações (mesmas do FASE 1, para compatibilidade de estado)
65
- # ============================================================================
66
- BATCH_SIZE = 16
67
- MAX_SEQ_LEN = 8
68
- HIDDEN_DIM = 256
69
- VOCAB_SIZE = 4096
70
- SOM_GRID = (6, 6, 6, 4)
71
- T_MAX = 10000
72
- N_START = 10
73
- LAMBDA_EWC = 0.02
74
- ALPHA0 = 0.1
75
- SIGMA0 = 1.5
76
- DIM_CHOICE = "y"
77
-
78
- N_HYPOTHESES = 8
79
- MAX_N_HYPOTHESES = 16
80
- MIN_N_HYPOTHESES = 4
81
- N_TRIALS = 3
82
- MIN_N_TRIALS = 1
83
- MAX_N_TRIALS = 6
84
- HYP_TRAIN_STEPS = 20
85
- MIN_HYP_TRAIN_STEPS = 5
86
- MAX_HYP_TRAIN_STEPS = 60
87
- HYP_LR = 1e-4
88
- HYP_HIDDEN_DIM = 128
89
- LOSS_HISTORY_WINDOW = 8
90
- PUNISHMENT_WINDOW = 12
91
-
92
- STREAM_BATCH_SIZE = 100
93
- META_MINIMA_PUNICAO = 1000
94
- MAX_SAMPLES_PUNICAO = 1000
95
- MAX_BUFFER_SIZE = 256
96
-
97
- INTER_BATCH_PAUSE_S = 0.3
98
- INTER_STREAM_BATCH_PAUSE_S = 0.5
99
-
100
- PUNICAO_DATASET = "BrunoN-Dev/corpus-ptbr-v1"
101
-
102
- DATASET_LABEL_STRINGS: Dict[str, Dict[int, str]] = {
103
- "BrunoN-Dev/corpus-ptbr-v1": {0: "short_text", 1: "long_text"},
104
- }
105
-
106
- # ============================================================================
107
- # 3. Import KLS V2
108
- # ============================================================================
109
- from bigru_t.model.kohonen_learning_system import ( # noqa: E402
110
- KohonenLearningSystemV2,
111
- )
112
-
113
-
114
- def get_process_rss_mb() -> float:
115
- try:
116
- import resource
117
- return float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
118
- except Exception:
119
- return 0.0
120
-
121
-
122
- def get_cgroup_memory_limit_mb() -> Optional[float]:
123
- try:
124
- with open("/sys/fs/cgroup/memory.max", "r") as f:
125
- return float(int(f.read().strip())) / 1e6
126
- except Exception:
127
- return None
128
-
129
-
130
- def aggressive_memory_cleanup() -> None:
131
- gc.collect()
132
- gc.collect()
133
-
134
-
135
- def make_label(text: str, dataset_name: Optional[str] = None) -> int:
136
- if dataset_name == "BrunoN-Dev/corpus-ptbr-v1":
137
- return len(text.split()) % 2
138
- return 1 if len(text.split()) % 2 == 0 else 0
139
-
140
-
141
- def stream_dataset_in_chunks(dataset_name, chunk_size, max_total, hf_token, timeout_per_chunk=120):
142
- from bigru_t.data.streaming_datasets import stream_dataset
143
- yielded_total = 0
144
- chunk_idx = 0
145
- while yielded_total < max_total:
146
- remaining = max_total - yielded_total
147
- this_chunk_target = min(chunk_size, remaining)
148
- seed_offset = yielded_total
149
- chunk: List[str] = []
150
- t_start = time.time()
151
- try:
152
- for sample in stream_dataset(
153
- dataset_name,
154
- max_samples=this_chunk_target + seed_offset + 5,
155
- hf_token=hf_token,
156
- ):
157
- if time.time() - t_start > timeout_per_chunk:
158
- break
159
- if sample.raw_text and len(sample.raw_text.strip()) > 0:
160
- if seed_offset > 0:
161
- seed_offset -= 1
162
- continue
163
- chunk.append(sample.raw_text.strip()[:200])
164
- if len(chunk) >= this_chunk_target:
165
- break
166
- except Exception as e:
167
- logger.warning(f"Stream error: {e}")
168
- if chunk:
169
- yield chunk
170
- yielded_total += len(chunk)
171
- chunk_idx += 1
172
- break
173
- if not chunk:
174
- break
175
- yield chunk
176
- yielded_total += len(chunk)
177
- chunk_idx += 1
178
- logger.info(f"[PUNICAO] {dataset_name} chunk {chunk_idx}: {len(chunk)} (total: {yielded_total}/{max_total})")
179
-
180
-
181
- def save_punicão_states(kls, reason, step, total_samples):
182
- import torch
183
- try:
184
- state = {
185
- "_meta": {
186
- "reason": str(reason),
187
- "step": int(step),
188
- "total_samples": int(total_samples),
189
- "timestamp": datetime.now().isoformat(),
190
- "version": "V6.5-V2-dynamic-punicão",
191
- "loaded_from_fase1": str(FASE1_STATES_PATH),
192
- },
193
- "som_weights": kls.som.weights.detach().clone().cpu(),
194
- "som_old_weights_w": (
195
- kls.som.old_weights_w.detach().clone().cpu()
196
- if kls.som.old_weights_w is not None else None
197
- ),
198
- "som_fisher_w": (
199
- kls.som.fisher_w.detach().clone().cpu()
200
- if kls.som.fisher_w is not None else None
201
- ),
202
- "embedding_state": {"weight": kls.embedding.weight.detach().clone().cpu()},
203
- "classifier_state": (
204
- kls.classifier.state_dict() if kls.classifier is not None else None
205
- ),
206
- "hypothesis_ensemble_state": kls.hypothesis_ensemble.state_dict(),
207
- "delta_scale": kls.delta_scale.detach().clone().cpu(),
208
- "v2_metrics": kls.get_v2_metrics(),
209
- "label_registry": kls.label_registry,
210
- "adaptation_log": kls.get_adaptation_log(),
211
- }
212
- torch.save(state, PUNICAO_STATES_PATH)
213
- size_mb = PUNICAO_STATES_PATH.stat().st_size / 1e6
214
- logger.info(f"[PUNICAO] States saved: {PUNICAO_STATES_PATH} ({size_mb:.1f}MB)")
215
- return {"saved": True, "path": str(PUNICAO_STATES_PATH), "size_mb": float(size_mb)}
216
- except Exception as e:
217
- logger.error(f"[PUNICAO] Save failed: {e}")
218
- return {"saved": False, "error": str(e)}
219
-
220
-
221
- HF_REPO_ID = "PowerMachine/BiGRU_T_version"
222
-
223
-
224
- def upload_punicão_to_hf(hf_token, files_to_upload):
225
- if not hf_token:
226
- return {"uploaded": False, "reason": "no_hf_token"}
227
- try:
228
- from huggingface_hub import HfApi, whoami, CommitOperationAdd
229
- except ImportError as e:
230
- return {"uploaded": False, "reason": f"import_error: {e}"}
231
- api = HfApi(token=hf_token)
232
- try:
233
- whoami(token=hf_token)
234
- except Exception as e:
235
- return {"uploaded": False, "reason": f"auth_failed: {e}"}
236
- operations = []
237
- for src in files_to_upload:
238
- if not src.exists():
239
- continue
240
- operations.append(CommitOperationAdd(
241
- path_in_repo=src.name,
242
- path_or_fileobj=str(src),
243
- ))
244
- if not operations:
245
- return {"uploaded": False, "reason": "no_files"}
246
- try:
247
- commit_info = api.create_commit(
248
- repo_id=HF_REPO_ID,
249
- repo_type="model",
250
- operations=operations,
251
- commit_message=f"V6.5-V2-dynamic: FASE 2 PUNIÇÃO upload ({len(operations)} files)",
252
- )
253
- return {
254
- "uploaded": True,
255
- "n_files": len(operations),
256
- "commit_url": str(commit_info.commit_url) if hasattr(commit_info, "commit_url") else str(commit_info),
257
- }
258
- except Exception as e:
259
- logger.error(f"[PUNICAO] HF upload failed: {e}")
260
- return {"uploaded": False, "reason": str(e)}
261
-
262
-
263
- def main() -> int:
264
- import torch
265
-
266
- if not FASE1_STATES_PATH.exists():
267
- logger.error(f"[PUNICAO] FASE 1 states not found: {FASE1_STATES_PATH}")
268
- return 1
269
-
270
- logger.info(f"[PUNICAO] Loading FASE 1 states from: {FASE1_STATES_PATH}")
271
- fase1_state = torch.load(FASE1_STATES_PATH, map_location="cpu", weights_only=False)
272
- logger.info(f"[PUNICAO] FASE 1 state loaded. Reason: {fase1_state['_meta']['reason']}")
273
- logger.info(f"[PUNICAO] FASE 1 total_samples: {fase1_state['_meta']['total_samples']}")
274
-
275
- # Construct KLS V2 with same config as FASE 1
276
- kls = KohonenLearningSystemV2(
277
- vocab_size=VOCAB_SIZE, hidden_dim=HIDDEN_DIM, seq_len=MAX_SEQ_LEN,
278
- som_grid=SOM_GRID, alpha0=ALPHA0, sigma0=SIGMA0,
279
- lambda_ewc=LAMBDA_EWC, N_start=N_START, dim_choice=DIM_CHOICE,
280
- hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8], T_max=T_MAX,
281
- enable_vqvae2=False, enable_reasoning=True, enable_w8a8=False,
282
- enable_attention=True, attention_n_heads=8,
283
- n_hypotheses=N_HYPOTHESES, max_n_hypotheses=MAX_N_HYPOTHESES,
284
- min_n_hypotheses=MIN_N_HYPOTHESES, n_trials=N_TRIALS,
285
- min_n_trials=MIN_N_TRIALS, max_n_trials=MAX_N_TRIALS,
286
- hyp_train_steps=HYP_TRAIN_STEPS, min_hyp_train_steps=MIN_HYP_TRAIN_STEPS,
287
- max_hyp_train_steps=MAX_HYP_TRAIN_STEPS, hyp_lr=HYP_LR,
288
- hyp_hidden_dim=HYP_HIDDEN_DIM,
289
- loss_history_window=LOSS_HISTORY_WINDOW,
290
- punishment_window=PUNISHMENT_WINDOW,
291
- )
292
- logger.info(f"[PUNICAO] KLS V2 initialized (fresh)")
293
-
294
- # Load FASE 1 state into KLS
295
- try:
296
- # SOM weights
297
- kls.som.weights = nn.Parameter(fase1_state["som_weights"].clone())
298
- if fase1_state.get("som_old_weights_w") is not None:
299
- kls.som.old_weights_w = fase1_state["som_old_weights_w"].clone()
300
- if fase1_state.get("som_fisher_w") is not None:
301
- kls.som.fisher_w = fase1_state["som_fisher_w"].clone()
302
- # Embedding
303
- if "embedding_state" in fase1_state:
304
- kls.embedding.weight.data = fase1_state["embedding_state"]["weight"].clone()
305
- # Classifier
306
- if fase1_state.get("classifier_state") and kls.classifier is not None:
307
- kls.classifier.load_state_dict(fase1_state["classifier_state"])
308
- kls.classifier_trained = True
309
- # Hypothesis ensemble
310
- if "hypothesis_ensemble_state" in fase1_state:
311
- kls.hypothesis_ensemble.load_state_dict(fase1_state["hypothesis_ensemble_state"])
312
- # Delta scale
313
- if "delta_scale" in fase1_state:
314
- kls.delta_scale.data = fase1_state["delta_scale"].clone()
315
- # Label registry
316
- if "label_registry" in fase1_state:
317
- kls.label_registry = fase1_state["label_registry"]
318
- # Mark training as ready (FASE 1 already trained the SOM)
319
- kls.training_ready = True
320
- logger.info(f"[PUNICAO] FASE 1 state loaded into KLS successfully")
321
- except Exception as e:
322
- logger.error(f"[PUNICAO] Failed to load FASE 1 state: {e}")
323
- traceback.print_exc()
324
- return 1
325
-
326
- # Fit tokenizer with PT-BR corpus
327
- corpus_inicial = [
328
- "o gato dorme na cama", "a casa eh azul", "ele corre rapido",
329
- "luva de pedreiro tavila", "lula reserva valor",
330
- "amazonas forca tarefa vitimas",
331
- "texto curto", "texto longo com muitas palavras para teste",
332
- ]
333
- kls.tokenizer.fit(corpus_inicial)
334
- logger.info(f"[PUNICAO] Tokenizer fitted")
335
-
336
- hf_token = os.environ.get("HF_TOKEN")
337
- if not hf_token:
338
- logger.warning("[PUNICAO] HF_TOKEN not set")
339
- else:
340
- logger.info(f"[PUNICAO] HF_TOKEN set: {hf_token[:8]}...")
341
-
342
- # ========================================================================
343
- # FASE 2 — TREINAMENTO COM PUNIÇÃO
344
- # ========================================================================
345
- logger.info("\n" + "=" * 80)
346
- logger.info("[PUNICAO] FASE 2 — TREINAMENTO COM PUNIÇÃO ATIVA")
347
- logger.info("=" * 80)
348
- logger.info(f" Dataset: {PUNICAO_DATASET}")
349
- logger.info(f" Meta mínima: {META_MINIMA_PUNICAO}")
350
- logger.info(f" Stream batch: {STREAM_BATCH_SIZE}")
351
- logger.info(f" n_hypotheses: {kls.n_hypotheses} (max {kls.max_n_hypotheses})")
352
- logger.info(f" n_trials: {kls.n_trials}")
353
- logger.info(f" hyp_train_steps: {kls.hyp_train_steps}")
354
- logger.info("=" * 80 + "\n")
355
-
356
- step = 0
357
- t_start = time.time()
358
- samples_processed = 0
359
- chunk_idx_global = 0
360
- punishment_log: List[Dict[str, Any]] = []
361
- hypotheses_log: List[Dict[str, Any]] = []
362
- delta_applications_log: List[Dict[str, Any]] = []
363
- accuracies_log: List[Dict[str, Any]] = []
364
- mem_critical_stopped = False
365
- cg_limit = get_cgroup_memory_limit_mb()
366
-
367
- try:
368
- chunk_iter = stream_dataset_in_chunks(
369
- dataset_name=PUNICAO_DATASET,
370
- chunk_size=STREAM_BATCH_SIZE,
371
- max_total=MAX_SAMPLES_PUNICAO,
372
- hf_token=hf_token,
373
- timeout_per_chunk=120,
374
- )
375
- for chunk_samples in chunk_iter:
376
- if mem_critical_stopped:
377
- break
378
- if samples_processed >= META_MINIMA_PUNICAO:
379
- logger.info(f"[PUNICAO] Meta atingida: {samples_processed}")
380
- break
381
- chunk_idx_global += 1
382
- if not chunk_samples:
383
- continue
384
- samples_processed += len(chunk_samples)
385
- labels = [make_label(s, PUNICAO_DATASET) for s in chunk_samples]
386
- logger.info(f"[PUNICAO] Chunk {chunk_idx_global}: {len(chunk_samples)} samples (total: {samples_processed})")
387
-
388
- for batch_start in range(0, len(chunk_samples), BATCH_SIZE):
389
- try:
390
- # Memory check
391
- if step > 0 and step % 5 == 0 and cg_limit is not None:
392
- rss = get_process_rss_mb()
393
- mem_pct = (rss / cg_limit) * 100.0
394
- logger.info(f"[PUNICAO] MEM: RSS={rss:.0f}MB ({mem_pct:.1f}%)")
395
- if mem_pct > 80:
396
- logger.error(f"[PUNICAO] MEM CRITICAL ({mem_pct:.1f}%). Stopping.")
397
- save_punicão_states(kls, f"mem_critical_{mem_pct:.1f}pct", step, samples_processed)
398
- mem_critical_stopped = True
399
- break
400
-
401
- batch_sents = chunk_samples[batch_start: batch_start + BATCH_SIZE]
402
- batch_labels = labels[batch_start: batch_start + BATCH_SIZE]
403
- try:
404
- result = kls.process_batch_v2(
405
- batch_sents, batch_labels,
406
- label_strings=DATASET_LABEL_STRINGS.get(PUNICAO_DATASET),
407
- dataset_name=PUNICAO_DATASET,
408
- enable_punishment=True,
409
- )
410
- except Exception as e:
411
- logger.error(f"[PUNICAO] process_batch_v2 error: {e}")
412
- continue
413
-
414
- action = result.get("action", "none")
415
- if action != "none" and action != "success":
416
- punishment_log.append({
417
- "step": step,
418
- "action": action,
419
- "accuracy": float(result.get("accuracy", 0.0)),
420
- "punishment_count": int(kls.punishment_count),
421
- "cycle_completed": bool(result.get("cycle_completed", False)),
422
- })
423
- if "hypotheses_training" in result:
424
- hyp_info = result["hypotheses_training"]
425
- hypotheses_log.append({
426
- "step": step,
427
- "loss_initial": float(hyp_info.get("loss_initial", 0.0)),
428
- "loss_final": float(hyp_info.get("loss_final", 0.0)),
429
- "delta_scale": float(hyp_info.get("delta_scale_final", 0.0)),
430
- })
431
- if "delta_application" in result:
432
- apply_info = result["delta_application"]
433
- delta_applications_log.append({
434
- "step": step,
435
- "acc_before": float(apply_info.get("acc_before", 0.0)),
436
- "acc_after": float(apply_info.get("acc_after", 0.0)),
437
- "delta_norm": float(apply_info.get("delta_norm", 0.0)),
438
- "ewc_reference_set": bool(apply_info.get("ewc_reference_set", False)),
439
- })
440
-
441
- acc = kls.evaluate_classification()
442
- accuracies_log.append({
443
- "step": step,
444
- "accuracy": float(acc),
445
- "action": action,
446
- "punishment_count": int(kls.punishment_count),
447
- "buffer_size": int(len(kls.buffer_4d)),
448
- })
449
- step += 1
450
- time.sleep(INTER_BATCH_PAUSE_S)
451
- except Exception as e:
452
- logger.error(f"[PUNICAO] Batch error: {e}")
453
- continue
454
-
455
- time.sleep(INTER_STREAM_BATCH_PAUSE_S)
456
- # Buffer sliding window
457
- if len(kls.buffer_4d) > MAX_BUFFER_SIZE:
458
- overflow = len(kls.buffer_4d) - MAX_BUFFER_SIZE
459
- del kls.buffer_4d[:overflow]
460
- del kls.buffer_labels[:overflow]
461
- logger.info(f"[PUNICAO] Buffer trimmed: -{overflow} → {len(kls.buffer_4d)}")
462
- if chunk_idx_global % 2 == 0:
463
- aggressive_memory_cleanup()
464
- except Exception as e:
465
- logger.error(f"[PUNICAO] FASE 2 failed: {e}")
466
- traceback.print_exc()
467
-
468
- t_elapsed = time.time() - t_start
469
- logger.info(f"\n[PUNICAO] FASE 2 concluída:")
470
- logger.info(f" Total samples: {samples_processed}")
471
- logger.info(f" Meta atingida: {samples_processed >= META_MINIMA_PUNICAO}")
472
- logger.info(f" Punishment events: {len(punishment_log)}")
473
- logger.info(f" Delta applications: {len(delta_applications_log)}")
474
- logger.info(f" Tempo: {t_elapsed:.1f}s")
475
-
476
- # Save final states
477
- save_punicão_states(kls, "end_of_punicão", step, samples_processed)
478
-
479
- # Final report
480
- final_report = {
481
- "version": "V6.5-V2-dynamic-punicão",
482
- "timestamp": datetime.now().isoformat(),
483
- "loaded_from_fase1": str(FASE1_STATES_PATH),
484
- "fase1_total_samples": int(fase1_state["_meta"]["total_samples"]),
485
- "fase2_total_samples": int(samples_processed),
486
- "fase2_meta_atingida": bool(samples_processed >= META_MINIMA_PUNICAO),
487
- "fase2_elapsed_s": float(t_elapsed),
488
- "punishment_events": punishment_log,
489
- "hypotheses_trainings": hypotheses_log,
490
- "delta_applications": delta_applications_log,
491
- "accuracies_log": accuracies_log[-30:],
492
- "final_v2_metrics": kls.get_v2_metrics(),
493
- "n_adaptations": len(kls.get_adaptation_log()),
494
- "mem_critical_stopped": bool(mem_critical_stopped),
495
- }
496
- PUNICAO_REPORT_PATH.write_text(json.dumps(final_report, indent=2, ensure_ascii=False))
497
- logger.info(f"[PUNICAO] Report saved: {PUNICAO_REPORT_PATH}")
498
-
499
- # Copy to download
500
- try:
501
- for src in [PUNICAO_REPORT_PATH, PUNICAO_STATES_PATH]:
502
- if src.exists():
503
- shutil.copy2(src, DOWNLOAD_DIR / src.name)
504
- except Exception as e:
505
- logger.warning(f"[PUNICAO] Copy to download failed: {e}")
506
-
507
- # HF upload (batch)
508
- hf_upload_result = {"uploaded": False, "reason": "no_hf_token"}
509
- if hf_token:
510
- files_to_upload = [
511
- PUNICAO_REPORT_PATH,
512
- PUNICAO_STATES_PATH,
513
- BIGRU_ROOT / "scripts" / "train_v6_5_v2_punicão_only.py",
514
- ]
515
- # Scrub HF_TOKEN from scripts before upload
516
- pattern = re.compile(r'hf_[A-Za-z0-9]{32,}')
517
- for py_file in [BIGRU_ROOT / "scripts" / "train_v6_5_v2_punicão_only.py"]:
518
- if py_file.exists():
519
- content = py_file.read_text(encoding="utf-8")
520
- if pattern.findall(content):
521
- py_file.write_text(pattern.sub("<HF_TOKEN_REMOVED>", content), encoding="utf-8")
522
-
523
- hf_upload_result = upload_punicão_to_hf(hf_token, files_to_upload)
524
- PUNICAO_UPLOAD_REPORT_PATH.write_text(
525
- json.dumps(hf_upload_result, indent=2, ensure_ascii=False)
526
- )
527
- logger.info(f"[PUNICAO] HF upload: {hf_upload_result}")
528
-
529
- # Clean HF_TOKEN
530
- if "HF_TOKEN" in os.environ:
531
- del os.environ["HF_TOKEN"]
532
- if "HUGGING_FACE_HUB_TOKEN" in os.environ:
533
- del os.environ["HUGGING_FACE_HUB_TOKEN"]
534
- logger.info("[PUNICAO] HF_TOKEN cleaned from environment")
535
-
536
- print("\n" + "=" * 80)
537
- print("V6.5-V2-dynamic FASE 2 PUNIÇÃO — CONCLUÍDO")
538
- print("=" * 80)
539
- print(f" Samples processados : {samples_processed}")
540
- print(f" Meta atingida : {samples_processed >= META_MINIMA_PUNICAO}")
541
- print(f" Punishment events : {len(punishment_log)}")
542
- print(f" Delta applications : {len(delta_applications_log)}")
543
- print(f" Hyp trainings : {len(hypotheses_log)}")
544
- print(f" N adaptations : {len(kls.get_adaptation_log())}")
545
- print(f" Final n_hypotheses : {kls.n_hypotheses}")
546
- print(f" Final n_trials : {kls.n_trials}")
547
- print(f" Final hyp_steps : {kls.hyp_train_steps}")
548
- print(f" HF upload : {hf_upload_result.get('uploaded', False)}")
549
- print(f" HF_TOKEN cleaned : True")
550
- print("=" * 80)
551
- return 0
552
-
553
-
554
- if __name__ == "__main__":
555
- import torch.nn as nn # needed for nn.Parameter in main
556
- sys.exit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
xeon_runtime.py DELETED
@@ -1,581 +0,0 @@
1
- """xeon_runtime.py — Intel Xeon runtime (V6): AVX512 + AMX_INT8 + IPEX + OneDNN + FP16.
2
-
3
- ═══════════════════════════════════════════════════════════════════════════════
4
- V6 UPGRADE
5
- ═══════════════════════════════════════════════════════════════════════════════
6
-
7
- V5 only set OMP/MKL threads + KMP_AFFINITY. V6 adds:
8
-
9
- 1. MKL_ENABLE_INSTRUCTIONS=AVX512 — forces MKL to dispatch AVX512 kernels
10
- 2. ONEDNN_MAX_CPU_ISA=AMX_INT8 — lets oneDNN use AMX INT8 tiles
11
- 3. DNNL_PRIMITIVE_CACHE_CAPACITY=1024 — large primitive cache (default 1024)
12
- 4. MKL_DYNAMIC=FALSE — disables MKL dynamic thread adjustment
13
- 5. IPEX (intel_extension_for_pytorch) — Intel PyTorch extension
14
- - ipex.optimize(model) on the BiGRU_T model
15
- - torch.cpu.amp.autocast(dtype=torch.float16) for FP16 inference
16
- 6. FP16 benchmark: 8000×8000 matmul, TFLOPS measurement
17
- 7. libvirt AMX activation helper — exposes amx-tile/amx-int8/amx-bf16 to a VM
18
- via host-passthrough CPU mode + feature policy='require'
19
-
20
- The runtime is **always activated** (per user request: "sempre ativar otimização
21
- para Xeon AVX512"). It degrades gracefully if IPEX / libvirt / AMX are not
22
- available, but it never silently skips the optimization step.
23
-
24
- ═══════════════════════════════════════════════════════════════════════════════
25
- USAGE
26
- ═══════════════════════════════════════════════════════════════════════════════
27
-
28
- from bigru_t.utils.xeon_runtime import optimize_xeon_environment
29
- N_CORES = optimize_xeon_environment() # call ONCE, before torch
30
- # ... safe to import torch, ipex, etc. ...
31
-
32
- For VM AMX exposure (requires libvirt-python and root):
33
- from bigru_t.utils.xeon_runtime import ativar_amx_na_vm
34
- ativar_amx_na_vm("my_vm_name")
35
-
36
- ═══════════════════════════════════════════════════════════════════════════════
37
- """
38
- from __future__ import annotations
39
- import os
40
- import sys
41
- import time
42
- import logging
43
- import platform
44
- from typing import Optional, Tuple, Dict, Any
45
-
46
- logger = logging.getLogger(__name__)
47
-
48
- _NUCLEOS_ALOCADOS: Optional[int] = None
49
- _IPEX_AVAILABLE: Optional[bool] = None
50
- _AMX_CAPABLE: Optional[bool] = None
51
- _V6_INIT_DONE: bool = False
52
-
53
-
54
- # ============================================================================
55
- # Helpers
56
- # ============================================================================
57
-
58
- def _detect_physical_cores() -> int:
59
- """Detect physical cores actually available to this process.
60
-
61
- Respects cgroup limits without requiring root. Falls back to logical
62
- cpu count if psutil is unavailable.
63
- """
64
- try:
65
- import psutil
66
- n_phys = psutil.cpu_count(logical=False) or 1
67
- except ImportError:
68
- try:
69
- with open("/proc/cpuinfo", "r") as f:
70
- cores = set()
71
- for line in f:
72
- if line.startswith("core id"):
73
- cores.add(line.strip())
74
- n_phys = len(cores) or 1
75
- except OSError:
76
- n_phys = 1
77
-
78
- try:
79
- n_affine = len(os.sched_getaffinity(0))
80
- n_logical = os.cpu_count() or 1
81
- if n_affine < n_logical:
82
- n_phys = max(1, n_affine // 2)
83
- else:
84
- n_phys = min(n_phys, n_affine)
85
- except (AttributeError, OSError):
86
- pass
87
-
88
- return max(1, n_phys)
89
-
90
-
91
- def _read_cpu_flags() -> str:
92
- try:
93
- with open("/proc/cpuinfo", "r") as f:
94
- for line in f:
95
- if line.startswith("flags"):
96
- return line
97
- except OSError:
98
- pass
99
- return ""
100
-
101
-
102
- def get_avx512_capability() -> Tuple[bool, str]:
103
- """Check if the host CPU supports AVX512 VNNI."""
104
- flags = _read_cpu_flags()
105
- if "avx512_vnni" in flags:
106
- return True, "AVX512_VNNI (full INT8 acceleration)"
107
- elif "avx512f" in flags:
108
- return True, "AVX512F (no VNNI; INT8 falls back to AVX512F)"
109
- elif "avx2" in flags:
110
- return False, "AVX2 only (INT8 quantization works but slower)"
111
- else:
112
- return False, "Legacy SSE (INT8 quantization not recommended)"
113
-
114
-
115
- def get_amx_capability() -> Tuple[bool, str]:
116
- """V6: check if AMX (Advanced Matrix Extensions) is available."""
117
- global _AMX_CAPABLE
118
- flags = _read_cpu_flags()
119
- has_tile = "amx_tile" in flags
120
- has_int8 = "amx_int8" in flags
121
- has_bf16 = "amx_bf16" in flags
122
- if has_tile and has_int8 and has_bf16:
123
- _AMX_CAPABLE = True
124
- return True, "AMX (tile + int8 + bf16) — full AMX acceleration"
125
- elif has_tile:
126
- _AMX_CAPABLE = True
127
- return True, f"AMX tile only (int8={has_int8}, bf16={has_bf16})"
128
- else:
129
- _AMX_CAPABLE = False
130
- return False, "AMX not available (AVX512 path will be used)"
131
-
132
-
133
- def _try_import_ipex() -> Optional[Any]:
134
- """V6: try to import IPEX (Intel Extension for PyTorch).
135
-
136
- Returns the ipex module if available, else None. Caches the result.
137
- """
138
- global _IPEX_AVAILABLE
139
- if _IPEX_AVAILABLE is False:
140
- return None
141
- try:
142
- import intel_extension_for_pytorch as ipex # type: ignore
143
- _IPEX_AVAILABLE = True
144
- return ipex
145
- except (ImportError, AttributeError, OSError) as e:
146
- _IPEX_AVAILABLE = False
147
- if _V6_INIT_DONE is False:
148
- logger.info(f"[Xeon V6] IPEX não disponível: {type(e).__name__}: {e}")
149
- logger.info("[Xeon V6] Continuando com OneDNN/MKL nativo do PyTorch.")
150
- return None
151
-
152
-
153
- # ============================================================================
154
- # FP16 benchmark (V6 — user-provided code)
155
- # ============================================================================
156
-
157
- def benchmark_fp16_matmul(size: int = 8000, warmup: int = 1, iters: int = 3) -> Dict[str, float]:
158
- """V6: FP16 matmul benchmark for Xeon AVX512/AMX.
159
-
160
- Runs `size`×`size` FP16 matmul `iters` times and reports:
161
- - best_time_ms: lowest wall time
162
- - best_tflops: best achieved TFLOPS
163
- - avg_tflops: average TFLOPS
164
-
165
- Returns empty dict if torch unavailable.
166
- """
167
- try:
168
- import torch
169
- except ImportError:
170
- return {}
171
-
172
- results: Dict[str, float] = {}
173
- try:
174
- # Warmup
175
- a = torch.randn(size, size, dtype=torch.float16)
176
- b = torch.randn(size, size, dtype=torch.float16)
177
- for _ in range(warmup):
178
- _ = torch.matmul(a, b)
179
- # Bench
180
- times = []
181
- for _ in range(iters):
182
- t0 = time.perf_counter()
183
- _ = torch.matmul(a, b)
184
- times.append(time.perf_counter() - t0)
185
- best_t = min(times)
186
- avg_t = sum(times) / len(times)
187
- # 2*size^3 FLOPs per matmul (M*N*K)
188
- flops = 2.0 * (size ** 3)
189
- results["best_time_ms"] = best_t * 1000.0
190
- results["avg_time_ms"] = avg_t * 1000.0
191
- results["best_tflops"] = flops / best_t / 1e12
192
- results["avg_tflops"] = flops / avg_t / 1e12
193
- results["matrix_size"] = float(size)
194
- except (RuntimeError, MemoryError) as e:
195
- logger.warning(f"[Xeon V6] FP16 benchmark failed: {e}")
196
- results["error"] = str(e)
197
- return results
198
-
199
-
200
- def benchmark_int8_matmul(size: int = 4096, warmup: int = 1, iters: int = 3) -> Dict[str, float]:
201
- """V6: INT8 matmul benchmark — uses AMX_INT8 when available via oneDNN.
202
-
203
- Falls back to FP32 if INT8 path is unavailable.
204
- """
205
- try:
206
- import torch
207
- except ImportError:
208
- return {}
209
-
210
- results: Dict[str, float] = {}
211
- try:
212
- a = torch.randint(-127, 127, (size, size), dtype=torch.int8)
213
- b = torch.randint(-127, 127, (size, size), dtype=torch.int8)
214
- # Warmup
215
- for _ in range(warmup):
216
- _ = torch.matmul(a.float(), b.float())
217
- times = []
218
- for _ in range(iters):
219
- t0 = time.perf_counter()
220
- _ = torch.matmul(a.float(), b.float())
221
- times.append(time.perf_counter() - t0)
222
- best_t = min(times)
223
- flops = 2.0 * (size ** 3)
224
- results["int8_best_time_ms"] = best_t * 1000.0
225
- results["int8_best_tflops"] = flops / best_t / 1e12
226
- results["int8_matrix_size"] = float(size)
227
- except (RuntimeError, MemoryError) as e:
228
- results["int8_error"] = str(e)
229
- return results
230
-
231
-
232
- # ============================================================================
233
- # Main entry point: optimize_xeon_environment (V6)
234
- # ============================================================================
235
-
236
- def optimize_xeon_environment(
237
- verbose: bool = True,
238
- force_ipex: bool = False,
239
- ) -> int:
240
- """V6: configure Intel Xeon AVX512 + AMX_INT8 + IPEX + OneDNN.
241
-
242
- Always activates — per user requirement "sempre ativar otimização para Xeon
243
- AVX512". Idempotent. Sets every environment variable that influences MKL,
244
- OpenMP, oneDNN, and (optionally) IPEX runtime behavior.
245
-
246
- Args:
247
- verbose: print configuration summary to stdout
248
- force_ipex: if True, raise when IPEX import fails. Default False —
249
- degrade gracefully to native PyTorch oneDNN.
250
-
251
- Returns:
252
- Number of physical cores allocated to this process.
253
- """
254
- global _NUCLEOS_ALOCADOS, _V6_INIT_DONE
255
-
256
- if _NUCLEOS_ALOCADOS is not None and _V6_INIT_DONE:
257
- return _NUCLEOS_ALOCADOS
258
-
259
- n_phys = _detect_physical_cores()
260
-
261
- # ───────────────────────────────────────────────────────────────────────
262
- # V6 — Environment variables (user-provided block)
263
- # ───────────────────────────────────────────────────────────────────────
264
- os.environ["MKL_ENABLE_INSTRUCTIONS"] = "AVX512"
265
- NUM_CORES = str(n_phys)
266
- os.environ["MKL_NUM_THREADS"] = NUM_CORES
267
- os.environ["OMP_NUM_THREADS"] = NUM_CORES
268
- os.environ["MKL_DYNAMIC"] = "FALSE"
269
- os.environ["DNNL_PRIMITIVE_CACHE_CAPACITY"] = "1024"
270
- os.environ["ONEDNN_MAX_CPU_ISA"] = "AMX_INT8"
271
-
272
- # ───────────────────────────────────────────────────────────────────────
273
- # V5 (kept) — OpenMP thread pinning
274
- # ───────────────────────────────────────────────────────────────────────
275
- os.environ.setdefault("KMP_AFFINITY", "granularity=fine,compact,1,0")
276
- os.environ.setdefault("KMP_BLOCKTIME", "1")
277
- os.environ.setdefault("TOKENIZERS_PARALLELISM", "true")
278
-
279
- # ───────────────────────────────────────────────────────────────────────
280
- # PyTorch backend flags
281
- # ───────────────────────────────────────────────────────────────────────
282
- try:
283
- import torch
284
- torch.set_num_threads(n_phys)
285
- try:
286
- torch.set_num_interop_threads(1)
287
- except RuntimeError:
288
- # V6.5: já inicializado (e.g., bigru_t package importou torch antes).
289
- # Silenciosamente ignora — o paralelismo já está configurado.
290
- pass
291
- if hasattr(torch.backends, "mkldnn"):
292
- torch.backends.mkldnn.enabled = True
293
- if hasattr(torch.backends, "quantized"):
294
- try:
295
- torch.backends.quantized.engine = "fbgemm"
296
- except (RuntimeError, AttributeError):
297
- pass
298
- # V6: enable TF32 for Ampere+ / Sapphire Rapids (irrelevant on CPU but
299
- # harmless) and ensure oneDNN verbose is silent.
300
- try:
301
- torch.backends.cuda.matmul.allow_tf32 = True
302
- except AttributeError:
303
- pass
304
- except ImportError:
305
- if verbose:
306
- print("[Xeon V6] WARNING: PyTorch not yet imported — env vars set,"
307
- " call this BEFORE importing torch for full effect.")
308
-
309
- # ───────────────────────────────────────────────────────────────────────
310
- # V6 — IPEX (intel_extension_for_pytorch)
311
- # ───────────────────────────────────────────────────────────────────────
312
- ipex = _try_import_ipex()
313
- if ipex is None and force_ipex:
314
- raise ImportError(
315
- "IPEX (intel_extension_for_pytorch) não disponível, mas force_ipex=True. "
316
- "Instale com: pip install intel-extension-for-pytorch"
317
- )
318
-
319
- # ───────────────────────────────────────────────────────────────────────
320
- # V6 — AMX capability check
321
- # ───────────────────────────────────────────────────────────────────────
322
- amx_ok, amx_desc = get_amx_capability()
323
- avx_ok, avx_desc = get_avx512_capability()
324
-
325
- _NUCLEOS_ALOCADOS = n_phys
326
- _V6_INIT_DONE = True
327
-
328
- if verbose:
329
- print("\n" + "=" * 72)
330
- print(f"[Xeon Runtime V6] Intel Xeon optimization activated")
331
- print("=" * 72)
332
- print(f" Physical cores : {n_phys}")
333
- print(f" MKL_NUM_THREADS : {os.environ['MKL_NUM_THREADS']}")
334
- print(f" OMP_NUM_THREADS : {os.environ['OMP_NUM_THREADS']}")
335
- print(f" MKL_DYNAMIC : {os.environ['MKL_DYNAMIC']}")
336
- print(f" MKL_ENABLE_INSTRUCTIONS: {os.environ['MKL_ENABLE_INSTRUCTIONS']}")
337
- print(f" ONEDNN_MAX_CPU_ISA : {os.environ['ONEDNN_MAX_CPU_ISA']}")
338
- print(f" DNNL_PRIMITIVE_CACHE : {os.environ['DNNL_PRIMITIVE_CACHE_CAPACITY']}")
339
- print(f" KMP_AFFINITY : {os.environ['KMP_AFFINITY']}")
340
- print(f" KMP_BLOCKTIME : {os.environ['KMP_BLOCKTIME']}")
341
- print(f" AVX512 : {avx_desc}")
342
- print(f" AMX : {amx_desc}")
343
- print(f" IPEX : "
344
- f"{'available' if ipex is not None else 'not installed (using native oneDNN)'}")
345
- print("=" * 72 + "\n")
346
-
347
- return n_phys
348
-
349
-
350
- # ============================================================================
351
- # V6 — apply_ipex_optimization (model-level)
352
- # ============================================================================
353
-
354
- def apply_ipex_optimization(model, dtype=None, optimizer=None):
355
- """V6: apply ipex.optimize() to a model.
356
-
357
- Returns (model, optimizer) tuple. If IPEX is not available, returns the
358
- inputs unchanged. The model is modified in-place when IPEX is present.
359
-
360
- Args:
361
- model: torch.nn.Module
362
- dtype: optional torch.dtype for the model (e.g. torch.bfloat16)
363
- optimizer: optional torch.optim.Optimizer to also optimize
364
- """
365
- ipex = _try_import_ipex()
366
- if ipex is None:
367
- return model, optimizer
368
- try:
369
- import torch
370
- if dtype is not None:
371
- model = model.to(dtype)
372
- if optimizer is not None:
373
- model, optimizer = ipex.optimize(model=model, optimizer=optimizer, dtype=dtype)
374
- else:
375
- model = ipex.optimize(model=model, dtype=dtype)
376
- logger.info(f"[Xeon V6] ipex.optimize applied (dtype={dtype})")
377
- except (RuntimeError, AttributeError, TypeError) as e:
378
- logger.warning(f"[Xeon V6] ipex.optimize failed: {e}")
379
- return model, optimizer
380
-
381
-
382
- # ============================================================================
383
- # V6 — FP16 autocast context manager
384
- # ============================================================================
385
-
386
- class fp16_autocast:
387
- """V6: FP16 CPU autocast context manager.
388
-
389
- Uses torch.cpu.amp.autocast(dtype=torch.float16) when available. Falls
390
- back to a no-op context manager if autocast is not supported.
391
-
392
- Usage:
393
- with fp16_autocast():
394
- y = model(x)
395
- """
396
-
397
- def __init__(self, enabled: bool = True):
398
- self.enabled = enabled
399
- self._ctx = None
400
-
401
- def __enter__(self):
402
- if not self.enabled:
403
- return self
404
- try:
405
- import torch
406
- if hasattr(torch.cpu, "amp") and hasattr(torch.cpu.amp, "autocast"):
407
- self._ctx = torch.cpu.amp.autocast(dtype=torch.float16)
408
- self._ctx.__enter__()
409
- except (ImportError, RuntimeError, AttributeError):
410
- self._ctx = None
411
- return self
412
-
413
- def __exit__(self, exc_type, exc_val, exc_tb):
414
- if self._ctx is not None:
415
- self._ctx.__exit__(exc_type, exc_val, exc_tb)
416
- self._ctx = None
417
- return False
418
-
419
-
420
- # ============================================================================
421
- # V6 — libvirt AMX activation helper (user-provided code, integrated)
422
- # ============================================================================
423
-
424
- def ativar_amx_na_vm(nome_vm: str, qemu_uri: str = "qemu:///system") -> bool:
425
- """V6: ativa AMX (amx-tile, amx-int8, amx-bf16) numa VM via libvirt.
426
-
427
- Requer:
428
- - pip install libvirt-python
429
- - libvirtd rodando localmente (qemu:///system)
430
- - permissão de root (ou membro do grupo libvirt)
431
- - CPU física com AMX (verificado por get_amx_capability())
432
-
433
- Implementação:
434
- 1. Conecta ao daemon libvirt
435
- 2. Busca a VM pelo nome
436
- 3. Lê o XML persistente (flag VIR_DOMAIN_XML_INACTIVE = 2)
437
- 4. Garante <cpu mode='host-passthrough'>
438
- 5. Adiciona <feature policy='require' name='amx-tile|int8|bf16'/>
439
- 6. Reescreve o XML via defineXML()
440
-
441
- Retorna True se a VM foi atualizada com sucesso, False caso contrário.
442
- Requer reboot da VM para aplicar.
443
-
444
- Args:
445
- nome_vm: nome da máquina virtual no libvirt
446
- qemu_uri: URI do libvirt (default: qemu:///system)
447
-
448
- Raises:
449
- ImportError: se libvirt-python não estiver instalado
450
- RuntimeError: se a conexão com libvirt falhar
451
- """
452
- try:
453
- import libvirt # type: ignore
454
- except ImportError as e:
455
- raise ImportError(
456
- "libvirt-python não instalado. Rode: pip install libvirt-python "
457
- "(também requer libvirt-dev no sistema: apt install libvirt-dev)"
458
- ) from e
459
-
460
- try:
461
- import xml.etree.ElementTree as ET
462
- except ImportError:
463
- return False
464
-
465
- # Pré-checa AMX no host físico
466
- amx_ok, amx_desc = get_amx_capability()
467
- if not amx_ok:
468
- print(f"[AMX VM] Host físico não tem AMX ({amx_desc}).")
469
- print(" Não adianta ativar AMX na VM — o host precisa suportar.")
470
- return False
471
-
472
- try:
473
- conn = libvirt.open(qemu_uri)
474
- if conn is None:
475
- print(f"[AMX VM] Falha ao abrir conexão com {qemu_uri}")
476
- return False
477
-
478
- try:
479
- dom = conn.lookupByName(nome_vm)
480
- except libvirt.libvirtError:
481
- print(f"[AMX VM] VM '{nome_vm}' não encontrada.")
482
- conn.close()
483
- return False
484
-
485
- xml_atual = dom.XMLDesc(2) # VIR_DOMAIN_XML_INACTIVE
486
- root = ET.fromstring(xml_atual)
487
- cpu_elem = root.find("cpu")
488
-
489
- if cpu_elem is None:
490
- cpu_elem = ET.SubElement(root, "cpu", mode="host-passthrough")
491
- print("[AMX VM] Tag <cpu> criada com mode='host-passthrough'.")
492
- else:
493
- cpu_elem.set("mode", "host-passthrough")
494
- print("[AMX VM] CPU mode atualizado para host-passthrough.")
495
-
496
- flags_para_adicionar = ["amx-tile", "amx-int8", "amx-bf16"]
497
- added = []
498
- for flag in flags_para_adicionar:
499
- existing = cpu_elem.find(f"./feature[@name='{flag}']")
500
- if existing is None:
501
- ET.SubElement(cpu_elem, "feature", policy="require", name=flag)
502
- added.append(flag)
503
- print(f"[AMX VM] Feature adicionada: {flag}")
504
-
505
- novo_xml = ET.tostring(root, encoding="utf-8").decode("utf-8")
506
- conn.defineXML(novo_xml)
507
- print(f"[AMX VM] ✓ AMX ativado no XML da VM '{nome_vm}'. Reinicie a VM para aplicar.")
508
- conn.close()
509
- return True
510
-
511
- except libvirt.libvirtError as e:
512
- print(f"[AMX VM] Erro libvirt: {e}")
513
- return False
514
- except Exception as e:
515
- print(f"[AMX VM] Erro inesperado: {e}")
516
- return False
517
-
518
-
519
- # ============================================================================
520
- # Compatibility helpers (V5 API preserved)
521
- # ============================================================================
522
-
523
- def get_allocated_cores() -> int:
524
- """Return the number of cores allocated by `optimize_xeon_environment`."""
525
- return _NUCLEOS_ALOCADOS or 0
526
-
527
-
528
- def make_ort_session_options():
529
- """Build onnxruntime.SessionOptions tuned for Xeon AVX512/AMX."""
530
- import onnxruntime as ort # type: ignore
531
-
532
- n_cores = _NUCLEOS_ALOCADOS or _detect_physical_cores()
533
-
534
- opts = ort.SessionOptions()
535
- opts.intra_op_num_threads = n_cores
536
- opts.inter_op_num_threads = 1
537
- opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
538
- opts.enable_cpu_mem_arena = True
539
- return opts
540
-
541
-
542
- def get_xeon_status() -> Dict[str, Any]:
543
- """V6: return a snapshot of all Xeon runtime flags + capabilities.
544
-
545
- Useful for logging inside the training script.
546
- """
547
- amx_ok, amx_desc = get_amx_capability()
548
- avx_ok, avx_desc = get_avx512_capability()
549
- return {
550
- "version": "V6",
551
- "physical_cores": _NUCLEOS_ALOCADOS or _detect_physical_cores(),
552
- "env": {
553
- "MKL_ENABLE_INSTRUCTIONS": os.environ.get("MKL_ENABLE_INSTRUCTIONS"),
554
- "MKL_NUM_THREADS": os.environ.get("MKL_NUM_THREADS"),
555
- "OMP_NUM_THREADS": os.environ.get("OMP_NUM_THREADS"),
556
- "MKL_DYNAMIC": os.environ.get("MKL_DYNAMIC"),
557
- "DNNL_PRIMITIVE_CACHE_CAPACITY": os.environ.get("DNNL_PRIMITIVE_CACHE_CAPACITY"),
558
- "ONEDNN_MAX_CPU_ISA": os.environ.get("ONEDNN_MAX_CPU_ISA"),
559
- "KMP_AFFINITY": os.environ.get("KMP_AFFINITY"),
560
- "KMP_BLOCKTIME": os.environ.get("KMP_BLOCKTIME"),
561
- },
562
- "avx512": {"supported": avx_ok, "desc": avx_desc},
563
- "amx": {"supported": amx_ok, "desc": amx_desc},
564
- "ipex_available": _IPEX_AVAILABLE is True,
565
- "init_done": _V6_INIT_DONE,
566
- }
567
-
568
-
569
- __all__ = [
570
- "optimize_xeon_environment",
571
- "apply_ipex_optimization",
572
- "fp16_autocast",
573
- "benchmark_fp16_matmul",
574
- "benchmark_int8_matmul",
575
- "ativar_amx_na_vm",
576
- "get_avx512_capability",
577
- "get_amx_capability",
578
- "get_xeon_status",
579
- "get_allocated_cores",
580
- "make_ort_session_options",
581
- ]