PowerMachine commited on
Commit
d867bda
·
verified ·
1 Parent(s): e30765f

V6.5-V2-metrics-FIX-2: fix HF organization — remove 9 root-level duplicates, upload 13 files to correct src/bigru_t/ paths

Browse files
attention_multimodal.py DELETED
@@ -1,114 +0,0 @@
1
- """
2
- Xavante - attention_multimodal.py
3
- Responsabilidade: Multi-head attention multimodal (Teorema 11.1/11.2).
4
- Suporta attention entre modalidades e dentro de modalidade.
5
- """
6
- from __future__ import annotations
7
-
8
- import logging
9
- import math
10
- from typing import Optional
11
-
12
- import torch
13
- import torch.nn as nn
14
- import torch.nn.functional as F
15
-
16
- logger = logging.getLogger(__name__)
17
-
18
-
19
- class MultiHeadAttention(nn.Module):
20
- """
21
- MHA padrão com suporte a:
22
- - Mascara causal
23
- - Mascara multimodal (mod crossing)
24
- - Attention 2M tokens via chunked attention (Teorema 6.1)
25
- """
26
-
27
- def __init__(
28
- self,
29
- d_model: int,
30
- n_heads: int = 8,
31
- dropout: float = 0.0,
32
- max_chunk: int = 4096,
33
- ):
34
- super().__init__()
35
- assert d_model % n_heads == 0
36
- self.d_model = d_model
37
- self.n_heads = n_heads
38
- self.d_head = d_model // n_heads
39
- self.max_chunk = max_chunk
40
- self.qkv = nn.Linear(d_model, 3 * d_model, bias=True)
41
- self.out = nn.Linear(d_model, d_model)
42
- self.dropout = nn.Dropout(dropout)
43
-
44
- def forward(
45
- self,
46
- x: torch.Tensor,
47
- mask: Optional[torch.Tensor] = None,
48
- kv: Optional[torch.Tensor] = None,
49
- ) -> torch.Tensor:
50
- B, L, D = x.shape
51
- if kv is None:
52
- qkv = self.qkv(x)
53
- q, k, v = qkv.chunk(3, dim=-1)
54
- else:
55
- q = self.qkv(x)[:, :, :D]
56
- kv_proj = self.qkv(kv)
57
- k = kv_proj[:, :, D : 2 * D]
58
- v = kv_proj[:, :, 2 * D :]
59
- # reshape para heads
60
- q = q.view(B, L, self.n_heads, self.d_head).transpose(1, 2)
61
- k = k.view(B, -1, self.n_heads, self.d_head).transpose(1, 2)
62
- v = v.view(B, -1, self.n_heads, self.d_head).transpose(1, 2)
63
-
64
- # Chunked attention para sequencias longas (Teorema 6.1)
65
- if L > self.max_chunk:
66
- # Normalize mask to 4D for chunked path
67
- if mask is not None and mask.dim() == 2:
68
- mask = mask.unsqueeze(0).unsqueeze(0)
69
- elif mask is not None and mask.dim() == 3:
70
- mask = mask.unsqueeze(1)
71
- return self._chunked_attention(q, k, v, mask)
72
-
73
- scale = 1.0 / math.sqrt(self.d_head)
74
- attn = (q @ k.transpose(-2, -1)) * scale # [B, H, L, L]
75
- if mask is not None:
76
- attn = attn.masked_fill(mask == 0, float("-inf"))
77
- attn = F.softmax(attn, dim=-1)
78
- attn = self.dropout(attn)
79
- out = attn @ v # [B, H, L, d_head]
80
- out = out.transpose(1, 2).contiguous().view(B, L, D)
81
- return self.out(out)
82
-
83
- def _chunked_attention(
84
- self,
85
- q: torch.Tensor,
86
- k: torch.Tensor,
87
- v: torch.Tensor,
88
- mask: Optional[torch.Tensor],
89
- ) -> torch.Tensor:
90
- """Attention em blocos para janelas de 2M tokens (memoria controlada)."""
91
- B, H, L, d = q.shape
92
- Lk = k.shape[2]
93
- chunk = self.max_chunk
94
- outs = []
95
- scale = 1.0 / math.sqrt(d)
96
- for i in range(0, L, chunk):
97
- qi = q[:, :, i : i + chunk]
98
- out_chunk = []
99
- for j in range(0, Lk, chunk):
100
- kj = k[:, :, j : j + chunk]
101
- vj = v[:, :, j : j + chunk]
102
- attn = (qi @ kj.transpose(-2, -1)) * scale
103
- if mask is not None:
104
- m_chunk = mask[:, :, i : i + chunk, j : j + chunk]
105
- attn = attn.masked_fill(m_chunk == 0, float("-inf"))
106
- attn = F.softmax(attn, dim=-1)
107
- out_chunk.append(attn @ vj)
108
- outs.append(torch.cat(out_chunk, dim=2))
109
- out = torch.cat(outs, dim=2)
110
- out = out.transpose(1, 2).contiguous().view(B, L, self.n_heads * d)
111
- return self.out(out)
112
-
113
-
114
- __all__ = ["MultiHeadAttention"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
 
train_v6_5_v2.py → scripts/train_v6_5_v2.py RENAMED
File without changes
src/bigru_t/data/streaming_datasets.py CHANGED
@@ -34,6 +34,8 @@ logger = logging.getLogger(__name__)
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
  DEFAULT_DATASETS = [
38
  "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1",
39
  "Madras1/corpus-ptbr-v2",
@@ -48,8 +50,27 @@ DEFAULT_DATASETS = [
48
  "adalbertojunior/punctuation-ptbr",
49
  "iara-project/news-articles-ptbr-dataset",
50
  "manoela/noticias_ptbr",
 
51
  ]
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  # Dataset format descriptors — Lista priorizada de campos de texto
54
  DATASET_FORMATS: Dict[str, Dict[str, Any]] = {
55
  "CEIA-POSITIVO/ultrachat_br_clustred_balanced_v1": {
@@ -273,6 +294,30 @@ DATASET_FORMATS: Dict[str, Dict[str, Any]] = {
273
  "config": None,
274
  "format_template": "title_text_concat",
275
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  }
277
 
278
 
@@ -939,7 +984,12 @@ if __name__ == "__main__":
939
  print(f" Configured datasets ({len(DEFAULT_DATASETS)}):")
940
  for i, ds in enumerate(DEFAULT_DATASETS, 1):
941
  print(f" {i}. {ds}")
942
- assert len(DEFAULT_DATASETS) == 13
 
 
 
 
 
943
  print(f" OK\n")
944
 
945
  print("=== ALL STREAMING TESTS PASSED ===")
 
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",
 
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": {
 
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
 
 
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 ===")
src/bigru_t/model/kohonen_learning_system.py CHANGED
@@ -581,6 +581,16 @@ class KohonenLearningSystem:
581
 
582
  self.buffer_4d = []
583
  self.buffer_labels = []
 
 
 
 
 
 
 
 
 
 
584
  self.training_ready = False
585
  self.N = N_start
586
  self.dim_choice = dim_choice
@@ -727,19 +737,24 @@ class KohonenLearningSystem:
727
  # Registra uma ferramenta interna: consultar SOM
728
  def som_query_tool(query: str) -> str:
729
  """Ferramenta: consulta o SOM do KLS para responder."""
730
- pred = self.predict(query)
731
- bmu_info = ""
732
- if self.buffer_4d:
733
- try:
734
- vec = text_to_4d_vector(
735
- query, self.tokenizer, self.embedding,
736
- self.hidden_dim, self.seq_len,
737
- self.time_counter, self.T_max,
738
- )
739
- bmu = self.som.find_bmu(vec)
740
- bmu_info = f" | BMU={bmu}"
741
- except Exception:
742
- pass
 
 
 
 
 
743
  return f"prediction={pred}{bmu_info}"
744
  self.reasoning_engine.register_tool(
745
  "som_query", som_query_tool,
@@ -980,23 +995,36 @@ class KohonenLearningSystem:
980
  if dataset_name is not None:
981
  self.last_dataset_used = dataset_name
982
 
983
- for sent, lab in zip(sentences, labels):
984
- self.time_counter += 1
985
- vec = text_to_4d_vector(
986
- sent,
987
- self.tokenizer,
988
- self.embedding,
989
- self.hidden_dim,
990
- self.seq_len,
991
- self.time_counter,
992
- self.T_max,
993
- attention=self.attention, # V6.5-attn
994
- attention_metrics=self.attention_metrics, # V6.5-attn
995
- )
996
- self.buffer_4d.append(vec)
997
- self.buffer_labels.append(lab)
998
- dim_val = round(vec[self.dim_index].item(), 2)
999
- self.histogram[dim_val] += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
 
1001
  def check_training_start(self) -> bool:
1002
  """Inicia treino quando algum bucket do histograma atinge N."""
@@ -1009,20 +1037,27 @@ class KohonenLearningSystem:
1009
  return False
1010
 
1011
  def train_som_on_buffer(self):
1012
- """Treina SOM por 5 épocas sobre o buffer atual (com Fisher accum)."""
 
 
 
 
 
1013
  if not self.buffer_4d:
1014
  return
1015
- data = torch.stack(self.buffer_4d)
1016
- for _ in range(5): # épocas de treino rápido
1017
- perm = torch.randperm(len(data))
1018
- for idx in perm:
1019
- x = data[idx]
1020
- bmu = self.som.find_bmu(x)
1021
- acc_fisher = (
1022
- self.punishment_count == 0
1023
- and self.som.old_weights_w is None
1024
- )
1025
- self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
 
 
1026
 
1027
  # V6.5 — Ativa VQ-VAE-2 compressor no pipeline
1028
  if self.enable_vqvae2 and self.vqvae2_compressor is not None:
@@ -1039,7 +1074,16 @@ class KohonenLearningSystem:
1039
  # O ThreadPoolExecutor (n_workers=4) só é efetivamente utilizado quando
1040
  # execute_parallel() é chamado com múltiplas ferramentas. Caso contrário,
1041
  # os workers ficam idle e o recurso é desperdiçado.
1042
- self._exercise_tool_coordinator_workers()
 
 
 
 
 
 
 
 
 
1043
 
1044
  def _exercise_tool_coordinator_workers(self) -> Dict[str, Any]:
1045
  """V6.5-final — Exercita os workers do tool_coordinator em paralelo.
@@ -1350,30 +1394,38 @@ class KohonenLearningSystem:
1350
  return dist.flatten()
1351
 
1352
  def _label_neurons(self):
1353
- """Rotula neurônios por votação majoritária sobre o buffer."""
 
 
 
1354
  self.neuron_label = {}
1355
  if not self.buffer_4d:
1356
  return
1357
- data = torch.stack(self.buffer_4d)
1358
  labels = torch.tensor(self.buffer_labels)
1359
  votes = defaultdict(lambda: [0, 0])
1360
- for i in range(len(data)):
1361
- bmu = self.som.find_bmu(data[i])
1362
- votes[bmu][int(labels[i].item())] += 1
 
1363
  for bmu, v in votes.items():
1364
  self.neuron_label[bmu] = 1.0 if v[1] > v[0] else 0.0
1365
 
1366
  def evaluate_classification(self) -> float:
1367
- """Acurácia sobre o buffer atual."""
 
 
 
1368
  if not self.buffer_4d:
1369
  return 1.0
1370
- data = torch.stack(self.buffer_4d)
1371
  labels = torch.tensor(self.buffer_labels).float()
1372
  correct = 0
1373
- for i in range(len(data)):
1374
- pred = self._predict_single(data[i])
1375
- if (pred > 0.5) == (labels[i] > 0.5):
1376
- correct += 1
 
1377
  return correct / len(data)
1378
 
1379
  def _predict_single(self, x):
@@ -1503,6 +1555,12 @@ class KohonenLearningSystem:
1503
  - fallback: default {0:'negative', 1:'positive'}
1504
  5. return label_str [ou (label_str, prob) se return_proba=True]
1505
 
 
 
 
 
 
 
1506
  Args:
1507
  sentence: texto de entrada.
1508
  dataset_name: opcional — nome do dataset para disambiguar rótulos.
@@ -1512,18 +1570,20 @@ class KohonenLearningSystem:
1512
  str (ou Tuple[str, float]) — rótulo dinâmico extraído dos datasets.
1513
  """
1514
  self.time_counter += 1 # mantém coerência temporal
1515
- vec = text_to_4d_vector(
1516
- sentence,
1517
- self.tokenizer,
1518
- self.embedding,
1519
- self.hidden_dim,
1520
- self.seq_len,
1521
- self.time_counter,
1522
- self.T_max,
1523
- attention=self.attention, # V6.5-attn
1524
- attention_metrics=self.attention_metrics, # V6.5-attn
1525
- )
1526
- prob = self._predict_single(vec)
 
 
1527
  # CORREÇÃO MATEMÁTICA: label_int derivado de prob, não hardcoded
1528
  label_int = 1 if prob > 0.5 else 0
1529
  # Rótulo textual DINÂMICO extraído do label_registry
@@ -1624,22 +1684,29 @@ class KohonenLearningSystem:
1624
  self.last_dataset_used = dataset_name
1625
 
1626
  # 2. Compute vec_4d for the misclassified sentence
1627
- self.time_counter += 1
1628
- vec = text_to_4d_vector(
1629
- sentence,
1630
- self.tokenizer,
1631
- self.embedding,
1632
- self.hidden_dim,
1633
- self.seq_len,
1634
- self.time_counter,
1635
- self.T_max,
1636
- attention=self.attention,
1637
- attention_metrics=self.attention_metrics,
1638
- )
 
 
 
1639
 
1640
  # 3. Add to buffer + update histogram (mirror add_data)
1641
  self.buffer_4d.append(vec)
1642
  self.buffer_labels.append(int(correct_label_int))
 
 
 
 
1643
  dim_val = round(vec[self.dim_index].item(), 2)
1644
  self.histogram[dim_val] += 1
1645
 
@@ -1780,3 +1847,1396 @@ class KohonenLearningSystem:
1780
  # V6.5-fix — Dynamic label registry state
1781
  "label_registry": self.get_label_registry_state(),
1782
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
  self.buffer_4d = []
583
  self.buffer_labels = []
584
+ # V6.5-V2-memfix — Sliding window limit for buffer_4d to prevent OOM.
585
+ # User requirement: "resolver falhas de lógica e de bugs que estejam
586
+ # causando alto consumo de memória sem distorcer a arquitetura Kohonen".
587
+ # The buffer stores vec_4d tensors for SOM training and neuron labeling.
588
+ # Without a limit, streaming 8000+ samples accumulates 8000 tensors,
589
+ # each retaining the embedding computation graph (16M params), causing
590
+ # OOM (3.5GB RSS observed). Sliding window of 512 keeps recent samples
591
+ # for SOM updates while bounding memory. The Kohonen architecture (SOM
592
+ # grid, BMU, Gaussian neighborhood, EWC) is NOT changed.
593
+ self.buffer_max_size = 512
594
  self.training_ready = False
595
  self.N = N_start
596
  self.dim_choice = dim_choice
 
737
  # Registra uma ferramenta interna: consultar SOM
738
  def som_query_tool(query: str) -> str:
739
  """Ferramenta: consulta o SOM do KLS para responder."""
740
+ # V6.5-V2-metrics-FIX-2 — no_grad para evitar building de grafo
741
+ # (cada chamada de text_to_4d_vector sem no_grad retém ~100MB
742
+ # de grafo computacional: embedding 16M params + attention + SVD)
743
+ with torch.no_grad():
744
+ pred = self.predict(query)
745
+ bmu_info = ""
746
+ if self.buffer_4d:
747
+ try:
748
+ self.time_counter += 1
749
+ vec = text_to_4d_vector(
750
+ query, self.tokenizer, self.embedding,
751
+ self.hidden_dim, self.seq_len,
752
+ self.time_counter, self.T_max,
753
+ )
754
+ bmu = self.som.find_bmu(vec)
755
+ bmu_info = f" | BMU={bmu}"
756
+ except Exception:
757
+ pass
758
  return f"prediction={pred}{bmu_info}"
759
  self.reasoning_engine.register_tool(
760
  "som_query", som_query_tool,
 
995
  if dataset_name is not None:
996
  self.last_dataset_used = dataset_name
997
 
998
+ # V6.5-V2-memfix — Use torch.no_grad() to avoid building computation
999
+ # graphs during vec_4d computation. The embedding graph (16M params)
1000
+ # would otherwise be retained by every tensor in buffer_4d, causing OOM
1001
+ # with 8000+ samples. The vec_4d is a 4-float summary (x,y,z,w) that
1002
+ # does NOT need gradient — it feeds into SOM (Hebbian, no backprop) and
1003
+ # the HypothesisClassifier (which trains its own weights).
1004
+ with torch.no_grad():
1005
+ for sent, lab in zip(sentences, labels):
1006
+ self.time_counter += 1
1007
+ vec = text_to_4d_vector(
1008
+ sent,
1009
+ self.tokenizer,
1010
+ self.embedding,
1011
+ self.hidden_dim,
1012
+ self.seq_len,
1013
+ self.time_counter,
1014
+ self.T_max,
1015
+ attention=self.attention, # V6.5-attn
1016
+ attention_metrics=self.attention_metrics, # V6.5-attn
1017
+ )
1018
+ # CRITICAL: detach to break any residual graph + clone for safety
1019
+ self.buffer_4d.append(vec.detach().clone())
1020
+ self.buffer_labels.append(lab)
1021
+ dim_val = round(vec[self.dim_index].item(), 2)
1022
+ self.histogram[dim_val] += 1
1023
+ # V6.5-V2-memfix — Enforce sliding window limit
1024
+ if len(self.buffer_4d) > self.buffer_max_size:
1025
+ overflow = len(self.buffer_4d) - self.buffer_max_size
1026
+ self.buffer_4d = self.buffer_4d[-self.buffer_max_size:]
1027
+ self.buffer_labels = self.buffer_labels[-self.buffer_max_size:]
1028
 
1029
  def check_training_start(self) -> bool:
1030
  """Inicia treino quando algum bucket do histograma atinge N."""
 
1037
  return False
1038
 
1039
  def train_som_on_buffer(self):
1040
+ """Treina SOM por 5 épocas sobre o buffer atual (com Fisher accum).
1041
+
1042
+ V6.5-V2-memfix — Wrapped in torch.no_grad() to prevent graph building
1043
+ during SOM Hebbian updates. The SOM uses competitive learning (BMU +
1044
+ Gaussian neighborhood), not backprop — no gradient is needed.
1045
+ """
1046
  if not self.buffer_4d:
1047
  return
1048
+ # V6.5-V2-memfix — detach+stack to ensure no graph leaks from buffer
1049
+ data = torch.stack(self.buffer_4d).detach()
1050
+ with torch.no_grad():
1051
+ for _ in range(5): # épocas de treino rápido
1052
+ perm = torch.randperm(len(data))
1053
+ for idx in perm:
1054
+ x = data[idx]
1055
+ bmu = self.som.find_bmu(x)
1056
+ acc_fisher = (
1057
+ self.punishment_count == 0
1058
+ and self.som.old_weights_w is None
1059
+ )
1060
+ self.som.update_weights(x, bmu, accumulate_fisher=acc_fisher)
1061
 
1062
  # V6.5 — Ativa VQ-VAE-2 compressor no pipeline
1063
  if self.enable_vqvae2 and self.vqvae2_compressor is not None:
 
1074
  # O ThreadPoolExecutor (n_workers=4) só é efetivamente utilizado quando
1075
  # execute_parallel() é chamado com múltiplas ferramentas. Caso contrário,
1076
  # os workers ficam idle e o recurso é desperdiçado.
1077
+ #
1078
+ # V6.5-V2-metrics-FIX-2: Reduzido de TODAS as chamadas para a cada 5
1079
+ # chamadas (counter-based). O exercício dos workers não é crítico para
1080
+ # o aprendizado do SOM — é apenas para manter os threads ativos.
1081
+ # Chamar a cada batch causava OOM (4 parallel predict() calls com
1082
+ # graph building = 400MB+ por batch). Com no_grad (aplicado acima) +
1083
+ # redução de frequência, o overhead é mínimo.
1084
+ self._tool_coordinator_call_count = getattr(self, "_tool_coordinator_call_count", 0) + 1
1085
+ if self._tool_coordinator_call_count % 5 == 0:
1086
+ self._exercise_tool_coordinator_workers()
1087
 
1088
  def _exercise_tool_coordinator_workers(self) -> Dict[str, Any]:
1089
  """V6.5-final — Exercita os workers do tool_coordinator em paralelo.
 
1394
  return dist.flatten()
1395
 
1396
  def _label_neurons(self):
1397
+ """Rotula neurônios por votação majoritária sobre o buffer.
1398
+
1399
+ V6.5-V2-memfix — no_grad to prevent graph building during BMU search.
1400
+ """
1401
  self.neuron_label = {}
1402
  if not self.buffer_4d:
1403
  return
1404
+ data = torch.stack(self.buffer_4d).detach()
1405
  labels = torch.tensor(self.buffer_labels)
1406
  votes = defaultdict(lambda: [0, 0])
1407
+ with torch.no_grad():
1408
+ for i in range(len(data)):
1409
+ bmu = self.som.find_bmu(data[i])
1410
+ votes[bmu][int(labels[i].item())] += 1
1411
  for bmu, v in votes.items():
1412
  self.neuron_label[bmu] = 1.0 if v[1] > v[0] else 0.0
1413
 
1414
  def evaluate_classification(self) -> float:
1415
+ """Acurácia sobre o buffer atual.
1416
+
1417
+ V6.5-V2-memfix — no_grad to prevent graph building during evaluation.
1418
+ """
1419
  if not self.buffer_4d:
1420
  return 1.0
1421
+ data = torch.stack(self.buffer_4d).detach()
1422
  labels = torch.tensor(self.buffer_labels).float()
1423
  correct = 0
1424
+ with torch.no_grad():
1425
+ for i in range(len(data)):
1426
+ pred = self._predict_single(data[i])
1427
+ if (pred > 0.5) == (labels[i] > 0.5):
1428
+ correct += 1
1429
  return correct / len(data)
1430
 
1431
  def _predict_single(self, x):
 
1555
  - fallback: default {0:'negative', 1:'positive'}
1556
  5. return label_str [ou (label_str, prob) se return_proba=True]
1557
 
1558
+ V6.5-V2-metrics-FIX-2: Wrapped in torch.no_grad() to prevent computation
1559
+ graph building during inference. Without this, each predict() call
1560
+ builds a graph that includes the embedding (16M params), attention
1561
+ forward pass, and SVD — consuming ~100MB+ per call. With 4 parallel
1562
+ tool_coordinator workers calling predict(), this caused OOM (3.5GB RSS).
1563
+
1564
  Args:
1565
  sentence: texto de entrada.
1566
  dataset_name: opcional — nome do dataset para disambiguar rótulos.
 
1570
  str (ou Tuple[str, float]) — rótulo dinâmico extraído dos datasets.
1571
  """
1572
  self.time_counter += 1 # mantém coerência temporal
1573
+ # V6.5-V2-metrics-FIX-2 — no_grad para evitar building de grafo em inference
1574
+ with torch.no_grad():
1575
+ vec = text_to_4d_vector(
1576
+ sentence,
1577
+ self.tokenizer,
1578
+ self.embedding,
1579
+ self.hidden_dim,
1580
+ self.seq_len,
1581
+ self.time_counter,
1582
+ self.T_max,
1583
+ attention=self.attention, # V6.5-attn
1584
+ attention_metrics=self.attention_metrics, # V6.5-attn
1585
+ )
1586
+ prob = self._predict_single(vec)
1587
  # CORREÇÃO MATEMÁTICA: label_int derivado de prob, não hardcoded
1588
  label_int = 1 if prob > 0.5 else 0
1589
  # Rótulo textual DINÂMICO extraído do label_registry
 
1684
  self.last_dataset_used = dataset_name
1685
 
1686
  # 2. Compute vec_4d for the misclassified sentence
1687
+ # V6.5-V2-memfix — no_grad + detach to prevent graph accumulation
1688
+ with torch.no_grad():
1689
+ self.time_counter += 1
1690
+ vec = text_to_4d_vector(
1691
+ sentence,
1692
+ self.tokenizer,
1693
+ self.embedding,
1694
+ self.hidden_dim,
1695
+ self.seq_len,
1696
+ self.time_counter,
1697
+ self.T_max,
1698
+ attention=self.attention,
1699
+ attention_metrics=self.attention_metrics,
1700
+ )
1701
+ vec = vec.detach().clone()
1702
 
1703
  # 3. Add to buffer + update histogram (mirror add_data)
1704
  self.buffer_4d.append(vec)
1705
  self.buffer_labels.append(int(correct_label_int))
1706
+ # Enforce sliding window limit (mirror add_data)
1707
+ if len(self.buffer_4d) > self.buffer_max_size:
1708
+ self.buffer_4d = self.buffer_4d[-self.buffer_max_size:]
1709
+ self.buffer_labels = self.buffer_labels[-self.buffer_max_size:]
1710
  dim_val = round(vec[self.dim_index].item(), 2)
1711
  self.histogram[dim_val] += 1
1712
 
 
1847
  # V6.5-fix — Dynamic label registry state
1848
  "label_registry": self.get_label_registry_state(),
1849
  }
1850
+
1851
+
1852
+ # ============================================================================
1853
+ # V6.5-V2 — HypothesisEnsemble (16 hipóteses) + KohonenLearningSystemV2
1854
+ # ============================================================================
1855
+ # User requirement: "Formato de Aprendizado com 16 Hipóteses e 3 Tentativas"
1856
+ #
1857
+ # Fluxo:
1858
+ # 1. Fase sem punição (streaming N-datasets) de CONHECIMENTO:
1859
+ # 1.1. Acumulação de conhecimento → ajuste dos pesos do SOM via regra
1860
+ # de Kohonen.
1861
+ # 1.2. Construção do MAPA-SOM (estado consolidado) com treinamento
1862
+ # contínuo.
1863
+ #
1864
+ # 2. Fase com punição (streaming M-datasets):
1865
+ # 2.1. Cada evento de erro (punição) dispara 3 tentativas de ajuste.
1866
+ # 2.2. Em cada tentativa, 16 geradores paralelos de hipóteses produzem
1867
+ # candidatos a delta.
1868
+ # 2.3. O melhor delta (que maximiza a acurácia no buffer) é aplicado
1869
+ # ao MAPA-SOM.
1870
+ # 2.4. Após a aplicação, consolidação via EWC (para evitar esquecimento
1871
+ # catastrófico).
1872
+ # 2.5. O ciclo é reiniciado sem loops infinitos (número fixo de
1873
+ # tentativas).
1874
+ #
1875
+ # Mathematical analysis:
1876
+ # - DeltaGenerator: g_h(x) → δ_h ∈ ℝ^P (h = 1..16)
1877
+ # onde P = I*J*K*L*4 (parâmetros do SOM achatados)
1878
+ # - HypothesisEnsemble: H = {g_1, ..., g_16}
1879
+ # - Avaliação: a_h = accuracy(SOM + δ_h, buffer) para cada h
1880
+ # - Seleção: δ* = argmax_h a_h
1881
+ # - Aplicação: SOM ← SOM + δ*
1882
+ # - Consolidação: set_ewc_reference() (Fisher information em w)
1883
+ #
1884
+ # Treinamento dos geradores (train_hypotheses):
1885
+ # - Para cada punição, executar hyp_train_steps passos de Adam
1886
+ # - Loss = mean_h(BCEWithLogitsLoss(classifier(SOM+δ_h), labels))
1887
+ # - δ_h = scale * g_h(som_activation_mean)
1888
+ # - scale é um parâmetro treinável (começa em 0.01, clamp [0.001, 0.1])
1889
+ # ============================================================================
1890
+
1891
+
1892
+ class DeltaGenerator(nn.Module):
1893
+ """Gerador de delta para ajuste do MAPA-SOM.
1894
+
1895
+ Entrada: vetor de ativação SOM (distâncias ao grid) ou vetor 4D médio.
1896
+ Saída: delta com a mesma dimensionalidade do SOM achatado.
1897
+
1898
+ Arquitetura: Linear → ReLU → Linear → ReLU → Linear (Xavier init gain=0.01)
1899
+ """
1900
+
1901
+ def __init__(self, input_dim: int, output_dim: int, hidden_dim: int = 256):
1902
+ super().__init__()
1903
+ self.net = nn.Sequential(
1904
+ nn.Linear(input_dim, hidden_dim),
1905
+ nn.ReLU(),
1906
+ nn.Linear(hidden_dim, hidden_dim),
1907
+ nn.ReLU(),
1908
+ nn.Linear(hidden_dim, output_dim),
1909
+ )
1910
+ # Inicialização Xavier para garantir deltas pequenos no início
1911
+ for layer in self.net:
1912
+ if isinstance(layer, nn.Linear):
1913
+ nn.init.xavier_uniform_(layer.weight, gain=0.01)
1914
+ nn.init.zeros_(layer.bias)
1915
+
1916
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
1917
+ return self.net(x)
1918
+
1919
+
1920
+ class HypothesisEnsemble(nn.Module):
1921
+ """Conjunto de N geradores de hipóteses paralelos (com active_count dinâmico).
1922
+
1923
+ V6.5-V2-dynamic: pré-aloca max_n_hypotheses geradores, mas usa apenas
1924
+ os primeiros `active_count` em forward/forward_stacked. Isto permite
1925
+ ajustar dinamicamente o número de hipóteses ativas sem recriar o módulo.
1926
+
1927
+ Args:
1928
+ input_dim: dimensionalidade do vetor de ativação SOM (som_neuron_count).
1929
+ output_dim: número de parâmetros do SOM achatados (I*J*K*L*4).
1930
+ n_hypotheses: número ATIVO inicial de geradores (default 16).
1931
+ max_n_hypotheses: número MÁXIMO pré-alocado (default 32).
1932
+ hidden_dim: largura das camadas internas de cada gerador.
1933
+ """
1934
+
1935
+ def __init__(
1936
+ self,
1937
+ input_dim: int,
1938
+ output_dim: int,
1939
+ n_hypotheses: int = 16,
1940
+ max_n_hypotheses: int = 32,
1941
+ hidden_dim: int = 256,
1942
+ ):
1943
+ super().__init__()
1944
+ # Garante que n_hypotheses <= max_n_hypotheses
1945
+ n_hypotheses = max(1, min(n_hypotheses, max_n_hypotheses))
1946
+ self.n_hypotheses = n_hypotheses # compat (== active_count inicial)
1947
+ self.max_n_hypotheses = max_n_hypotheses
1948
+ self.active_count = n_hypotheses
1949
+ # Pré-aloca max_n_hypotheses geradores (mesmo os inativos terão params
1950
+ # inicializados — mas não receberão gradiente até serem ativados, pois
1951
+ # forward_stacked itera apenas até active_count).
1952
+ self.generators = nn.ModuleList(
1953
+ [DeltaGenerator(input_dim, output_dim, hidden_dim)
1954
+ for _ in range(max_n_hypotheses)]
1955
+ )
1956
+
1957
+ def set_active_count(self, n: int) -> None:
1958
+ """Ajusta dinamicamente o número de geradores ativos."""
1959
+ n = max(1, min(int(n), self.max_n_hypotheses))
1960
+ self.active_count = n
1961
+ self.n_hypotheses = n # mantém compat com código legado
1962
+
1963
+ def forward(self, x: torch.Tensor) -> List[torch.Tensor]:
1964
+ """Retorna lista de `active_count` deltas (cada um (batch, output_dim))."""
1965
+ return [self.generators[i](x) for i in range(self.active_count)]
1966
+
1967
+ def forward_stacked(self, x: torch.Tensor) -> torch.Tensor:
1968
+ """Retorna tensor (batch, active_count, output_dim) para vetorização."""
1969
+ return torch.stack(
1970
+ [self.generators[i](x) for i in range(self.active_count)], dim=1
1971
+ )
1972
+
1973
+
1974
+ class KohonenLearningSystemV2(KohonenLearningSystem):
1975
+ """Versão V2 do KLS com hipóteses dinâmicas e tentativas adaptativas.
1976
+
1977
+ Herda toda a infraestrutura do KLS V6.5 (SOM 4D, EWC, VQ-VAE-2, W8A8,
1978
+ attention, reasoning_engine, label_registry dinâmico) e adiciona:
1979
+
1980
+ a. HypothesisEnsemble: até max_n_hypotheses geradores de delta (default 32,
1981
+ com n_hypotheses ativos = 16 inicialmente).
1982
+ b. select_best_delta: avalia n_trials tentativas × n_hypotheses hipóteses.
1983
+ c. apply_best_delta_and_consolidate: aplica o delta e chama set_ewc_reference.
1984
+ d. train_hypotheses: treina os geradores via Adam + BCE loss.
1985
+ e. DynamicHyperparameterController: ajusta automaticamente n_hypotheses,
1986
+ n_trials e hyp_train_steps com base em:
1987
+ - histórico de loss (slope + volatilidade)
1988
+ - recorrência de punições (taxa nos últimos K batches)
1989
+
1990
+ O protocolo de punição é estendido:
1991
+ - punishment_count == 1 → treinar hipóteses (train_hypotheses) +
1992
+ activate_hypothesis (classifier)
1993
+ - punishment_count == 2 → selecionar melhor delta (n_trials ×
1994
+ n_hypotheses) + aplicar + set_ewc_reference + reset
1995
+
1996
+ V6.5-V2-dynamic:
1997
+ Após cada train_hypotheses() e após cada evento de punição, o
1998
+ _adapt_hyperparameters() é chamado para re-ajustar (n_hypotheses,
1999
+ n_trials, hyp_train_steps) conforme a perda e a recorrência de punições.
2000
+ """
2001
+
2002
+ def __init__(self, *args, **kwargs):
2003
+ # Extrai parâmetros V2 antes de passar ao pai
2004
+ self.n_hypotheses = kwargs.pop("n_hypotheses", 16)
2005
+ self.max_n_hypotheses = kwargs.pop("max_n_hypotheses", 32)
2006
+ # Garante coerência
2007
+ self.n_hypotheses = max(1, min(self.n_hypotheses, self.max_n_hypotheses))
2008
+ self.n_trials = kwargs.pop("n_trials", 3)
2009
+ self.hyp_train_steps = kwargs.pop("hyp_train_steps", 30)
2010
+ self.hyp_lr = kwargs.pop("hyp_lr", 1e-4)
2011
+ self.hyp_hidden_dim = kwargs.pop("hyp_hidden_dim", 256)
2012
+
2013
+ # V6.5-V2-dynamic — limites para adaptação automática
2014
+ self.min_n_hypotheses = kwargs.pop("min_n_hypotheses", 4)
2015
+ self.max_n_trials = kwargs.pop("max_n_trials", 6)
2016
+ self.min_n_trials = kwargs.pop("min_n_trials", 1)
2017
+ self.max_hyp_train_steps = kwargs.pop("max_hyp_train_steps", 80)
2018
+ self.min_hyp_train_steps = kwargs.pop("min_hyp_train_steps", 10)
2019
+ # Janela de observação
2020
+ self.loss_history_window = kwargs.pop("loss_history_window", 8)
2021
+ self.punishment_window = kwargs.pop("punishment_window", 12)
2022
+
2023
+ super().__init__(*args, **kwargs)
2024
+
2025
+ # Inicialização do ensemble de hipóteses
2026
+ self._init_hypothesis_ensemble()
2027
+
2028
+ # V6.5-V2-dynamic — estado do controlador adaptativo
2029
+ self._loss_history: List[float] = [] # últimos loss_final de train_hypotheses
2030
+ self._punishment_history: List[int] = [] # 1 se houve punição no batch, 0 caso contrário
2031
+ self._adaptation_log: List[Dict[str, Any]] = []
2032
+ self._last_adaptation_step = 0
2033
+
2034
+ # V6.5-V2-metrics-FIX-2 — contadores cumulativos para monitoramento
2035
+ # User requirement: "quantos passos de treino de hipóteses usado"
2036
+ self._total_hyp_steps_executed: int = 0
2037
+ self._n_train_hyp_calls: int = 0
2038
+
2039
+ # V6.5-V2-metrics — histórico de métricas SOM (QE, TE, KL, VE, Dead Neurons)
2040
+ # para detecção de Estagnação do QE e Cruzamento de Vizinhança.
2041
+ from bigru_t.model.som_metrics import SOMMetricHistory
2042
+ self._som_metric_history: SOMMetricHistory = SOMMetricHistory()
2043
+
2044
+ def _init_hypothesis_ensemble(self):
2045
+ """Cria o ensemble de geradores + otimizador Adam."""
2046
+ input_dim = self.som_neuron_count # ativação SOM flatten (864)
2047
+ output_dim = self.som.weights.numel() # I*J*K*L*4 (864*4 = 3456)
2048
+ self.hypothesis_ensemble = HypothesisEnsemble(
2049
+ input_dim=input_dim,
2050
+ output_dim=output_dim,
2051
+ n_hypotheses=self.n_hypotheses,
2052
+ max_n_hypotheses=self.max_n_hypotheses,
2053
+ hidden_dim=self.hyp_hidden_dim,
2054
+ )
2055
+ self.hyp_optimizer = torch.optim.Adam(
2056
+ self.hypothesis_ensemble.parameters(), lr=self.hyp_lr
2057
+ )
2058
+ # delta_scale: parâmetro treinável para controlar magnitude dos deltas
2059
+ # começa pequeno (0.01) para evitar explosão no início
2060
+ self.delta_scale = nn.Parameter(torch.tensor(0.01))
2061
+
2062
+ # ==================================================================
2063
+ # V6.5-V2-dynamic — Controlador adaptativo de hiperparâmetros
2064
+ # ==================================================================
2065
+ def _record_loss(self, loss_final: float) -> None:
2066
+ """Adiciona um loss_final ao histórico (janela deslizante)."""
2067
+ self._loss_history.append(float(loss_final))
2068
+ if len(self._loss_history) > self.loss_history_window:
2069
+ self._loss_history = self._loss_history[-self.loss_history_window:]
2070
+
2071
+ def _record_punishment_event(self, had_punishment: bool) -> None:
2072
+ """Registra se houve punição no batch atual (janela deslizante)."""
2073
+ self._punishment_history.append(1 if had_punishment else 0)
2074
+ if len(self._punishment_history) > self.punishment_window:
2075
+ self._punishment_history = self._punishment_history[-self.punishment_window:]
2076
+
2077
+ def _compute_loss_stats(self) -> Dict[str, float]:
2078
+ """Calcula slope e volatilidade do histórico de loss.
2079
+
2080
+ Returns:
2081
+ {"slope": float, "volatility": float, "mean": float, "n": int}
2082
+ """
2083
+ n = len(self._loss_history)
2084
+ if n < 2:
2085
+ return {"slope": 0.0, "volatility": 0.0, "mean": 0.0, "n": n}
2086
+ losses = list(self._loss_history)
2087
+ mean_loss = sum(losses) / n
2088
+ # Slope por mínimos quadrados: y = a*x + b, x = [0..n-1]
2089
+ x_mean = (n - 1) / 2.0
2090
+ y_mean = mean_loss
2091
+ num = sum((i - x_mean) * (losses[i] - y_mean) for i in range(n))
2092
+ den = sum((i - x_mean) ** 2 for i in range(n))
2093
+ slope = num / den if den > 0 else 0.0
2094
+ # Volatilidade = std / |mean| (CV)
2095
+ variance = sum((l - y_mean) ** 2 for l in losses) / n
2096
+ std = variance ** 0.5
2097
+ volatility = std / max(abs(mean_loss), 1e-8)
2098
+ return {
2099
+ "slope": float(slope),
2100
+ "volatility": float(volatility),
2101
+ "mean": float(mean_loss),
2102
+ "std": float(std),
2103
+ "n": int(n),
2104
+ }
2105
+
2106
+ def _compute_punishment_rate(self) -> float:
2107
+ """Calcula a taxa de punição na janela observada."""
2108
+ if not self._punishment_history:
2109
+ return 0.0
2110
+ return float(sum(self._punishment_history) / len(self._punishment_history))
2111
+
2112
+ def _adapt_hyperparameters(self, trigger: str = "auto") -> Dict[str, Any]:
2113
+ """V6.5-V2-dynamic — Ajusta n_hypotheses, n_trials, hyp_train_steps.
2114
+
2115
+ Regras (matematicamente justificadas):
2116
+ - loss_volatility > 0.30 e n_hyp < max → n_hyp += 2 (mais diversidade)
2117
+ - loss_volatility < 0.05 e n_hyp > min → n_hyp -= 2 (menos desperdício)
2118
+ - loss_slope > -0.001 (lento/estagnado) e steps < max → steps += 5
2119
+ - loss_slope < -0.01 (rápido) e steps > min → steps -= 5
2120
+ - punishment_rate > 0.30 e n_trials < max → n_trials += 1
2121
+ - punishment_rate < 0.05 e n_trials > min → n_trials -= 1
2122
+
2123
+ Args:
2124
+ trigger: "auto" (após train_hypotheses), "punishment" (após punição),
2125
+ "manual" (chamada externa).
2126
+
2127
+ Returns:
2128
+ Dict com estado anterior, novo estado, e regras disparadas.
2129
+ """
2130
+ before = {
2131
+ "n_hypotheses": int(self.n_hypotheses),
2132
+ "n_trials": int(self.n_trials),
2133
+ "hyp_train_steps": int(self.hyp_train_steps),
2134
+ }
2135
+ stats = self._compute_loss_stats()
2136
+ rate = self._compute_punishment_rate()
2137
+ rules_fired: List[str] = []
2138
+
2139
+ # 1. Ajuste de n_hypotheses (com base em volatilidade)
2140
+ if stats["n"] >= 3:
2141
+ if stats["volatility"] > 0.30 and self.n_hypotheses < self.max_n_hypotheses:
2142
+ new_n = min(self.n_hypotheses + 2, self.max_n_hypotheses)
2143
+ if new_n != self.n_hypotheses:
2144
+ self.n_hypotheses = new_n
2145
+ self.hypothesis_ensemble.set_active_count(new_n)
2146
+ rules_fired.append(
2147
+ f"n_hyp+2 (vol={stats['volatility']:.3f} → {new_n})"
2148
+ )
2149
+ elif stats["volatility"] < 0.05 and self.n_hypotheses > self.min_n_hypotheses:
2150
+ new_n = max(self.n_hypotheses - 2, self.min_n_hypotheses)
2151
+ if new_n != self.n_hypotheses:
2152
+ self.n_hypotheses = new_n
2153
+ self.hypothesis_ensemble.set_active_count(new_n)
2154
+ rules_fired.append(
2155
+ f"n_hyp-2 (vol={stats['volatility']:.3f} → {new_n})"
2156
+ )
2157
+
2158
+ # 2. Ajuste de hyp_train_steps (com base em slope)
2159
+ if stats["n"] >= 3:
2160
+ if stats["slope"] > -0.001 and self.hyp_train_steps < self.max_hyp_train_steps:
2161
+ new_steps = min(self.hyp_train_steps + 5, self.max_hyp_train_steps)
2162
+ if new_steps != self.hyp_train_steps:
2163
+ self.hyp_train_steps = new_steps
2164
+ rules_fired.append(
2165
+ f"steps+5 (slope={stats['slope']:.5f} → {new_steps})"
2166
+ )
2167
+ elif stats["slope"] < -0.01 and self.hyp_train_steps > self.min_hyp_train_steps:
2168
+ new_steps = max(self.hyp_train_steps - 5, self.min_hyp_train_steps)
2169
+ if new_steps != self.hyp_train_steps:
2170
+ self.hyp_train_steps = new_steps
2171
+ rules_fired.append(
2172
+ f"steps-5 (slope={stats['slope']:.5f} → {new_steps})"
2173
+ )
2174
+
2175
+ # 3. Ajuste de n_trials (com base em recorrência de punições)
2176
+ if len(self._punishment_history) >= 3:
2177
+ if rate > 0.30 and self.n_trials < self.max_n_trials:
2178
+ new_t = min(self.n_trials + 1, self.max_n_trials)
2179
+ if new_t != self.n_trials:
2180
+ self.n_trials = new_t
2181
+ rules_fired.append(f"n_trials+1 (rate={rate:.3f} → {new_t})")
2182
+ elif rate < 0.05 and self.n_trials > self.min_n_trials:
2183
+ new_t = max(self.n_trials - 1, self.min_n_trials)
2184
+ if new_t != self.n_trials:
2185
+ self.n_trials = new_t
2186
+ rules_fired.append(f"n_trials-1 (rate={rate:.3f} → {new_t})")
2187
+
2188
+ after = {
2189
+ "n_hypotheses": int(self.n_hypotheses),
2190
+ "n_trials": int(self.n_trials),
2191
+ "hyp_train_steps": int(self.hyp_train_steps),
2192
+ }
2193
+ adapted = before != after
2194
+ record = {
2195
+ "trigger": trigger,
2196
+ "step": int(self._last_adaptation_step),
2197
+ "before": before,
2198
+ "after": after,
2199
+ "adapted": bool(adapted),
2200
+ "rules_fired": rules_fired,
2201
+ "loss_stats": stats,
2202
+ "punishment_rate": float(rate),
2203
+ }
2204
+ self._adaptation_log.append(record)
2205
+ # V6.5-V2-metrics-FIX: limita crescimento do log (era ilimitado, causando
2206
+ # crescimento de memória em treino longo). Mantém últimas 50 entradas.
2207
+ if len(self._adaptation_log) > 50:
2208
+ self._adaptation_log = self._adaptation_log[-50:]
2209
+ self._last_adaptation_step += 1
2210
+ return record
2211
+
2212
+ def get_adaptation_log(self) -> List[Dict[str, Any]]:
2213
+ """Retorna o log de adaptações dinâmicas (para monitoramento)."""
2214
+ return list(self._adaptation_log)
2215
+
2216
+ # ==================================================================
2217
+ # V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM
2218
+ # ==================================================================
2219
+ # User requirement: "analisar e aprimorar (logicamente e matematicamente)
2220
+ # no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
2221
+ #
2222
+ # Mathematical analysis:
2223
+ # A memória RAM consumida pelo KLS V2 tem 4 fontes principais:
2224
+ # 1. Pesos do SOM: I*J*K*L*4 floats = 864*4 = 3456 floats (~14KB)
2225
+ # → FIXO (não pode ser limpo sem destruir o modelo)
2226
+ # 2. HypothesisEnsemble: max_n_hypotheses * DeltaGenerator params
2227
+ # = 32 * (864*256 + 256*256 + 256*3456) = 32 * 1.16M = ~37M params
2228
+ # → ~148MB em float32. Os OTIMIZADORAS Adam guardam 2x esse valor
2229
+ # (momentum + variance) = ~296MB adicionais.
2230
+ # 3. HypothesisClassifier: 864*512 + 512*256 + ... = ~616K params (~2.5MB)
2231
+ # 4. buffer_4d: até MAX_BUFFER_SIZE amostras * 4 floats = ~4KB (mínimo)
2232
+ # MAS os tensores carregam gradiente do embedding se não forem detached.
2233
+ #
2234
+ # A limpeza agressiva atua em:
2235
+ # - Zera gradientes acumulados nos otimizadores (Adam state)
2236
+ # - Chama gc.collect() para liberar ciclos Python
2237
+ # - Libera caches internos do PyTorch (oneDNN primitive cache)
2238
+ # - Trunca listas de histórico que crescem com o treino
2239
+ # ------------------------------------------------------------------
2240
+ def aggressive_cleanup(self) -> Dict[str, Any]:
2241
+ """V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM do KLS.
2242
+
2243
+ Retorna métricas antes/depois para confirmação. NÃO destroi pesos do
2244
+ SOM nem do classifier — apenas libera caches e gradientes acumulados.
2245
+
2246
+ Returns:
2247
+ Dict com rss_before/after_mb, n_refs_collected, buffers_trimmed.
2248
+ """
2249
+ import gc
2250
+ try:
2251
+ import resource
2252
+ rss_before = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
2253
+ except Exception:
2254
+ rss_before = 0.0
2255
+
2256
+ # 1. Zera gradientes do otimizador Adam (libera momentum/variance state)
2257
+ try:
2258
+ if hasattr(self, "hyp_optimizer"):
2259
+ self.hyp_optimizer.zero_grad(set_to_none=True)
2260
+ except Exception:
2261
+ pass
2262
+
2263
+ # 2. Trunca históricos que crescem com o treino
2264
+ if len(self._adaptation_log) > 20:
2265
+ self._adaptation_log = self._adaptation_log[-20:]
2266
+ if len(self.inference_punishment_history) > 50:
2267
+ self.inference_punishment_history = self.inference_punishment_history[-50:]
2268
+ if len(self.vqvae2_metrics_history) > 30:
2269
+ self.vqvae2_metrics_history = self.vqvae2_metrics_history[-30:]
2270
+ if len(self.w8a8_metrics_history) > 30:
2271
+ self.w8a8_metrics_history = self.w8a8_metrics_history[-30:]
2272
+
2273
+ # 3. gc.collect() em 2 passes (gerações 0+1 e 2)
2274
+ n_collected_0 = gc.collect(0)
2275
+ n_collected_1 = gc.collect(1)
2276
+ n_collected_2 = gc.collect(2)
2277
+
2278
+ # 4. Libera caches CPU do PyTorch (oneDNN/MKL)
2279
+ try:
2280
+ import torch
2281
+ if hasattr(torch, "cuda") and torch.cuda.is_available():
2282
+ torch.cuda.empty_cache()
2283
+ except Exception:
2284
+ pass
2285
+
2286
+ try:
2287
+ import resource
2288
+ rss_after = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
2289
+ except Exception:
2290
+ rss_after = 0.0
2291
+
2292
+ return {
2293
+ "rss_before_mb": float(rss_before),
2294
+ "rss_after_mb": float(rss_after),
2295
+ "freed_mb": max(0.0, rss_before - rss_after),
2296
+ "gc_collected": int(n_collected_0 + n_collected_1 + n_collected_2),
2297
+ "adaptation_log_trimmed": int(len(self._adaptation_log)),
2298
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
2299
+ }
2300
+
2301
+ # ==================================================================
2302
+ # 2.1 Treinamento dos geradores (train_hypotheses)
2303
+ # ==================================================================
2304
+ def train_hypotheses(
2305
+ self,
2306
+ buffer_4d: Optional[List[torch.Tensor]] = None,
2307
+ buffer_labels: Optional[List[int]] = None,
2308
+ ) -> Dict[str, Any]:
2309
+ """Treina os 16 geradores para produzir deltas que melhorem a classificação.
2310
+
2311
+ Usa uma cópia do SOM para avaliar o efeito de cada delta e
2312
+ retropropaga o erro de classificação para os parâmetros dos geradores.
2313
+
2314
+ Mathematical formulation:
2315
+ Para cada step s = 1..hyp_train_steps:
2316
+ 1. x_state = mean(som_activations(buffer)) # (1, P_som)
2317
+ 2. δ_h = scale * g_h(x_state) para h = 1..16
2318
+ 3. loss_h = BCE(classifier(SOM+δ_h), labels)
2319
+ 4. loss = mean_h(loss_h) + λ * ||δ||² (regularização)
2320
+ 5. θ_{s+1} = θ_s - η * ∂loss/∂θ
2321
+
2322
+ Returns:
2323
+ Dict com métricas do treino (loss inicial, final, n_steps).
2324
+ """
2325
+ if buffer_4d is None:
2326
+ buffer_4d = self.buffer_4d
2327
+ if buffer_labels is None:
2328
+ buffer_labels = self.buffer_labels
2329
+ if not buffer_4d:
2330
+ return {"active": False, "reason": "empty_buffer"}
2331
+
2332
+ import time as _time
2333
+ t0 = _time.time()
2334
+
2335
+ # Prepara dados
2336
+ data = torch.stack(buffer_4d).detach() # (N, 4)
2337
+ labels = torch.tensor(buffer_labels, dtype=torch.float, device=data.device)
2338
+
2339
+ # Computa ativações SOM para cada amostra (sem gradiente)
2340
+ with torch.no_grad():
2341
+ som_activations = torch.stack(
2342
+ [self._som_activation(data[i]) for i in range(len(data))]
2343
+ ) # (N, P_som)
2344
+
2345
+ # Snapshot dos pesos atuais do SOM (para simular ajustes)
2346
+ som_weights_flat = self.som.weights.detach().clone().flatten() # (P,)
2347
+
2348
+ self.hypothesis_ensemble.train()
2349
+ losses = []
2350
+ # V6.5-V2-metrics-FIX: pré-computa data_norm_sq uma única vez (N, 1)
2351
+ # para reuso em todos os steps — evita recomputação redundante.
2352
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2353
+ labels_expanded = labels # (N,)
2354
+
2355
+ for step in range(self.hyp_train_steps):
2356
+ self.hyp_optimizer.zero_grad()
2357
+
2358
+ # Ativação média como representação do estado do SOM
2359
+ x_mean = som_activations.mean(dim=0, keepdim=True) # (1, P_som)
2360
+
2361
+ # Gera deltas: (1, n_hypotheses, P)
2362
+ deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
2363
+ deltas_stack = self.delta_scale * deltas_stack # escala
2364
+
2365
+ # V6.5-V2-metrics-FIX: Avaliação VETORIZADA de todas as hipóteses
2366
+ # em paralelo (substitui loop que materializava 16 cópias do SOM).
2367
+ # Memória: (n_hyp, P, N) em vez de 16 * (P + 2*P*N).
2368
+ if self.classifier is not None and self.classifier_trained:
2369
+ # deltas_stack: (1, n_hyp, P) → (n_hyp, P, 4) reshape
2370
+ n_hyp = self.n_hypotheses
2371
+ P_neurons = self.som_neuron_count
2372
+ # som_weights_flat: (P, 4) detached
2373
+ W_base = som_weights_flat.reshape(P_neurons, 4) # (P, 4)
2374
+ # delta_h: (n_hyp, P) → reshape para (n_hyp, P, 4)
2375
+ deltas_3d = deltas_stack[0].reshape(n_hyp, P_neurons, 4) # (n_hyp, P, 4)
2376
+ # W_new[h, p, d] = W_base[p, d] + deltas_3d[h, p, d]
2377
+ W_new = W_base.unsqueeze(0) + deltas_3d # (n_hyp, P, 4) — broadcast
2378
+ # dist²[h, p, n] = ||W_new[h, p] - data[n]||²
2379
+ # = ||W_new[h, p]||² + ||data[n]||² - 2*W_new[h, p]·data[n]
2380
+ W_new_norm_sq = (W_new * W_new).sum(dim=-1) # (n_hyp, P)
2381
+ # cross[h, p, n] = W_new[h, p] · data[n]
2382
+ cross = torch.matmul(W_new, data.t()) # (n_hyp, P, N)
2383
+ dist_sq = (
2384
+ W_new_norm_sq.unsqueeze(-1) # (n_hyp, P, 1)
2385
+ + data_norm_sq # (1, N) → broadcast (n_hyp, P, N)
2386
+ - 2.0 * cross
2387
+ ) # (n_hyp, P, N)
2388
+ dist_sq = torch.clamp(dist_sq, min=0.0)
2389
+ # activations[h, n, p] = dist_sq[h, p, n]
2390
+ activations = dist_sq.transpose(1, 2) # (n_hyp, N, P)
2391
+ # Classifier forward (congelado)
2392
+ classifier_params_were_grad = [
2393
+ p.requires_grad for p in self.classifier.parameters()
2394
+ ]
2395
+ for p in self.classifier.parameters():
2396
+ p.requires_grad_(False)
2397
+ try:
2398
+ logits = self.classifier(
2399
+ activations.reshape(n_hyp * len(data), P_neurons)
2400
+ ).reshape(n_hyp, len(data)) # (n_hyp, N)
2401
+ # BCE por hipótese, depois média
2402
+ loss_per_hyp = F.binary_cross_entropy_with_logits(
2403
+ logits,
2404
+ labels_expanded.unsqueeze(0).expand(n_hyp, -1),
2405
+ reduction='none',
2406
+ ).mean(dim=1) # (n_hyp,)
2407
+ loss_total = loss_per_hyp.mean()
2408
+ # Regularização L2 sobre os deltas
2409
+ reg_loss = 0.01 * deltas_stack.norm()
2410
+ loss_total = loss_total + reg_loss
2411
+ finally:
2412
+ for p, was_grad in zip(self.classifier.parameters(),
2413
+ classifier_params_were_grad):
2414
+ p.requires_grad_(was_grad)
2415
+ else:
2416
+ # Fallback: minimizar norma do delta (regularização pura)
2417
+ loss_total = deltas_stack.norm()
2418
+
2419
+ loss_total.backward()
2420
+ self.hyp_optimizer.step()
2421
+ losses.append(float(loss_total.item()))
2422
+
2423
+ # V6.5-V2-metrics-FIX: libera tensores intermediários explicitamente
2424
+ # para reduzir pico de memória entre steps.
2425
+ del loss_total, deltas_stack
2426
+ if 'W_new' in dir():
2427
+ del W_new, cross, dist_sq, activations
2428
+
2429
+ # Atualiza a escala de delta (decai suavemente)
2430
+ with torch.no_grad():
2431
+ self.delta_scale.data = torch.clamp(
2432
+ self.delta_scale.data * 0.99, 0.001, 0.1
2433
+ )
2434
+
2435
+ self.hypothesis_ensemble.eval()
2436
+ # V6.5-V2-metrics-FIX: libera som_activations e som_weights_flat
2437
+ del som_activations, som_weights_flat, data, data_norm_sq
2438
+ import gc
2439
+ gc.collect()
2440
+
2441
+ # V6.5-V2-dynamic — registra loss_final no histórico
2442
+ loss_final_val = float(losses[-1]) if losses else 0.0
2443
+ self._record_loss(loss_final_val)
2444
+
2445
+ # V6.5-V2-metrics-FIX-2 — atualiza contadores cumulativos
2446
+ # User requirement: "quantos passos de treino de hipóteses usado"
2447
+ self._total_hyp_steps_executed += int(self.hyp_train_steps)
2448
+ self._n_train_hyp_calls += 1
2449
+
2450
+ result = {
2451
+ "active": True,
2452
+ "n_steps": self.hyp_train_steps,
2453
+ "n_hypotheses": self.n_hypotheses,
2454
+ "loss_initial": float(losses[0]) if losses else 0.0,
2455
+ "loss_final": loss_final_val,
2456
+ "loss_mean": float(sum(losses) / len(losses)) if losses else 0.0,
2457
+ "delta_scale_final": float(self.delta_scale.item()),
2458
+ "elapsed_ms": float((_time.time() - t0) * 1000),
2459
+ # V6.5-V2-metrics-FIX-2 — contadores cumulativos para monitoramento
2460
+ "total_hyp_steps_executed": int(self._total_hyp_steps_executed),
2461
+ "n_train_hyp_calls": int(self._n_train_hyp_calls),
2462
+ }
2463
+
2464
+ # V6.5-V2-dynamic — dispara adaptação automática de hiperparâmetros
2465
+ try:
2466
+ adapt_record = self._adapt_hyperparameters(trigger="auto")
2467
+ result["adaptation"] = adapt_record
2468
+ except Exception as adapt_err:
2469
+ result["adaptation_error"] = str(adapt_err)[:200]
2470
+
2471
+ return result
2472
+
2473
+ def _compute_classification_loss_with_som(
2474
+ self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2475
+ ) -> torch.Tensor:
2476
+ """V6.5-V2-metrics-FIX — Computa perda de classificação com SOM temporário.
2477
+
2478
+ BUG CORRIGIDO (V6.5-V2-metrics-FIX):
2479
+ A versão anterior envolvia toda a computação em `torch.no_grad()`,
2480
+ o que matava o gradiente que deveria fluir dos deltas → SOM → loss.
2481
+ Resultado: train_hypotheses() não treinava os geradores de fato.
2482
+
2483
+ CORREÇÃO: A computação das ativações SOM (squared distances) é
2484
+ feita SEM no_grad, permitindo que o gradiente flua dos deltas
2485
+ (que pertencem ao HypothesisEnsemble) através do SOM temporário
2486
+ até a loss. Apenas o classifier é congelado (requires_grad=False)
2487
+ para que seu gradiente não seja atualizado por train_hypotheses.
2488
+
2489
+ Mathematical formulation (squared Euclidean distance, consistente com
2490
+ _som_activation que usa ||W - x||^2, não ||W - x||):
2491
+ Para cada neurônio p e amostra n:
2492
+ dist²[p, n] = ||W[p] - x[n]||²
2493
+ = ||W[p]||² + ||x[n]||² - 2·W[p]·x[n]
2494
+ activations = dist².t() → (N, P_neurons)
2495
+ logits = classifier(activations) → (N,)
2496
+ loss = BCEWithLogitsLoss(logits, labels)
2497
+
2498
+ Args:
2499
+ som_weights: tensor (I, J, K, L, 4) com gradiente (dos deltas).
2500
+ data: tensor (N, 4) — amostras 4D (detached).
2501
+ labels: tensor (N,) — rótulos binários.
2502
+
2503
+ Returns:
2504
+ Scalar loss tensor (com gradiente fluindo para som_weights).
2505
+ """
2506
+ if self.classifier is not None and self.classifier_trained:
2507
+ # NÃO usa torch.no_grad() — gradiente deve fluir dos deltas
2508
+ # através do SOM temporário até a loss.
2509
+ W_flat = som_weights.reshape(-1, 4) # (P_neurons, 4)
2510
+ # Identidade algébrica: ||W - x||^2 = ||W||^2 + ||x||^2 - 2*W·x
2511
+ # Evita materializar tensor (P, N, 4) — usa (P, N) direto.
2512
+ W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
2513
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2514
+ cross = W_flat @ data.t() # (P, N) — matmul eficiente
2515
+ dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
2516
+ dist_sq = torch.clamp(dist_sq, min=0.0) # estabilidade numérica
2517
+ activations = dist_sq.t() # (N, P_neurons) — gradiente fluindo!
2518
+ # Congela classifier durante train_hypotheses (não queremos
2519
+ # atualizar seus pesos — apenas os deltas dos geradores)
2520
+ classifier_params_were_grad = [
2521
+ p.requires_grad for p in self.classifier.parameters()
2522
+ ]
2523
+ for p in self.classifier.parameters():
2524
+ p.requires_grad_(False)
2525
+ try:
2526
+ logits = self.classifier(activations) # (N,)
2527
+ loss = F.binary_cross_entropy_with_logits(logits, labels)
2528
+ finally:
2529
+ # Restaura requires_grad do classifier
2530
+ for p, was_grad in zip(self.classifier.parameters(),
2531
+ classifier_params_were_grad):
2532
+ p.requires_grad_(was_grad)
2533
+ return loss
2534
+ else:
2535
+ # Fallback: votação BMU (não-diferenciável)
2536
+ with torch.no_grad():
2537
+ if not hasattr(self, "neuron_label"):
2538
+ self._label_neurons()
2539
+ correct = 0
2540
+ for i in range(len(data)):
2541
+ bmu = self.som.find_bmu(data[i])
2542
+ pred = self.neuron_label.get(bmu, 0.5)
2543
+ if (pred > 0.5) == (labels[i].item() > 0.5):
2544
+ correct += 1
2545
+ return torch.tensor(
2546
+ 1.0 - correct / max(1, len(data)),
2547
+ device=data.device,
2548
+ dtype=torch.float,
2549
+ )
2550
+
2551
+ # ==================================================================
2552
+ # 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses)
2553
+ # ==================================================================
2554
+ def select_best_delta(
2555
+ self,
2556
+ buffer_4d: Optional[List[torch.Tensor]] = None,
2557
+ buffer_labels: Optional[List[int]] = None,
2558
+ n_trials: Optional[int] = None,
2559
+ ) -> Tuple[Optional[torch.Tensor], float, Dict[str, Any]]:
2560
+ """Avalia 3 tentativas × 16 hipóteses e retorna o melhor delta e sua acurácia.
2561
+
2562
+ Args:
2563
+ buffer_4d, buffer_labels: opcional — usa self.buffer_* se None.
2564
+ n_trials: opcional — usa self.n_trials se None.
2565
+
2566
+ Returns:
2567
+ Tuple (best_delta, best_acc, info_dict):
2568
+ - best_delta: tensor (P,) ou None se buffer vazio
2569
+ - best_acc: acurácia obtida com o melhor delta
2570
+ - info_dict: métricas da seleção (n_trials, n_hypotheses, etc.)
2571
+ """
2572
+ if buffer_4d is None:
2573
+ buffer_4d = self.buffer_4d
2574
+ if buffer_labels is None:
2575
+ buffer_labels = self.buffer_labels
2576
+ if not buffer_4d:
2577
+ return None, -1.0, {"active": False, "reason": "empty_buffer"}
2578
+
2579
+ if n_trials is None:
2580
+ n_trials = self.n_trials
2581
+
2582
+ import time as _time
2583
+ t0 = _time.time()
2584
+
2585
+ data = torch.stack(buffer_4d).detach()
2586
+ labels = torch.tensor(buffer_labels, dtype=torch.float, device=data.device)
2587
+
2588
+ # Computa ativação SOM média
2589
+ with torch.no_grad():
2590
+ som_activations = torch.stack(
2591
+ [self._som_activation(data[i]) for i in range(len(data))]
2592
+ )
2593
+ x_state = som_activations.mean(dim=0, keepdim=True)
2594
+
2595
+ best_delta = None
2596
+ best_acc = -1.0
2597
+ trial_results = []
2598
+
2599
+ som_flat = self.som.weights.detach().clone().flatten()
2600
+
2601
+ self.hypothesis_ensemble.eval()
2602
+ for trial in range(n_trials):
2603
+ # Adiciona ruído à entrada para diversificar tentativas (trial > 0)
2604
+ if trial > 0:
2605
+ x_noisy = x_state + 0.01 * torch.randn_like(x_state)
2606
+ else:
2607
+ x_noisy = x_state
2608
+
2609
+ with torch.no_grad():
2610
+ deltas_stack = self.hypothesis_ensemble.forward_stacked(x_noisy)
2611
+ deltas_stack = self.delta_scale * deltas_stack
2612
+
2613
+ # Avalia cada uma das 16 hipóteses
2614
+ trial_accs = []
2615
+ for h in range(self.n_hypotheses):
2616
+ delta_h = deltas_stack[0, h]
2617
+ som_temp = som_flat + delta_h
2618
+ som_temp_reshaped = som_temp.view_as(self.som.weights)
2619
+ acc = self._evaluate_accuracy_with_som(
2620
+ som_temp_reshaped, data, labels
2621
+ )
2622
+ trial_accs.append(acc)
2623
+ if acc > best_acc:
2624
+ best_acc = acc
2625
+ best_delta = delta_h.clone()
2626
+
2627
+ trial_results.append({
2628
+ "trial": trial + 1,
2629
+ "best_acc_in_trial": float(max(trial_accs)) if trial_accs else 0.0,
2630
+ "mean_acc_in_trial": float(sum(trial_accs) / len(trial_accs)) if trial_accs else 0.0,
2631
+ "best_hyp_idx": int(trial_accs.index(max(trial_accs))) if trial_accs else -1,
2632
+ })
2633
+
2634
+ info = {
2635
+ "active": True,
2636
+ "n_trials": n_trials,
2637
+ "n_hypotheses": self.n_hypotheses,
2638
+ "best_acc": float(best_acc),
2639
+ "delta_norm": float(best_delta.norm().item()) if best_delta is not None else 0.0,
2640
+ "delta_scale": float(self.delta_scale.item()),
2641
+ "trials": trial_results,
2642
+ "elapsed_ms": float((_time.time() - t0) * 1000),
2643
+ }
2644
+ return best_delta, best_acc, info
2645
+
2646
+ def _evaluate_accuracy_with_som(
2647
+ self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2648
+ ) -> float:
2649
+ """V6.5-V2-metrics-FIX — Calcula acurácia com SOM temporário (vetorizado).
2650
+
2651
+ Substitui o loop que chamava _som_activation(data[i]) para cada amostra
2652
+ por uma única operação matmul. Reduz tempo de O(N*P) para O(P*N) mas
2653
+ com paralelismo SIMD (single matmul vs N python iterations).
2654
+
2655
+ Para o fallback (votação BMU), ainda precisa trocar self.som.weights
2656
+ temporariamente pois find_bmu lê self.som.weights diretamente.
2657
+ """
2658
+ if self.classifier is not None and self.classifier_trained:
2659
+ with torch.no_grad():
2660
+ # Vetorizado: dist²[p, n] = ||W[p]||² + ||x[n]||² - 2*W[p]·x[n]
2661
+ W_flat = som_weights.reshape(-1, 4) # (P, 4)
2662
+ W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
2663
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2664
+ cross = W_flat @ data.t() # (P, N)
2665
+ dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
2666
+ dist_sq = torch.clamp(dist_sq, min=0.0)
2667
+ activations = dist_sq.t() # (N, P)
2668
+ logits = self.classifier(activations) # (N,)
2669
+ preds = (logits > 0.5).float()
2670
+ acc = (preds == labels).float().mean().item()
2671
+ return float(acc)
2672
+ else:
2673
+ # Fallback: votação BMU — precisa trocar self.som.weights
2674
+ original_weights = self.som.weights
2675
+ self.som.weights = som_weights
2676
+ try:
2677
+ with torch.no_grad():
2678
+ if not hasattr(self, "neuron_label"):
2679
+ self._label_neurons()
2680
+ correct = 0
2681
+ for i in range(len(data)):
2682
+ bmu = self.som.find_bmu(data[i])
2683
+ pred = self.neuron_label.get(bmu, 0.5)
2684
+ if (pred > 0.5) == (labels[i].item() > 0.5):
2685
+ correct += 1
2686
+ acc = correct / max(1, len(data))
2687
+ return float(acc)
2688
+ finally:
2689
+ self.som.weights = original_weights
2690
+
2691
+ # ==================================================================
2692
+ # 2.3 Aplicação do melhor delta + consolidação via EWC
2693
+ # ==================================================================
2694
+ def apply_best_delta_and_consolidate(
2695
+ self,
2696
+ buffer_4d: Optional[List[torch.Tensor]] = None,
2697
+ buffer_labels: Optional[List[int]] = None,
2698
+ ) -> Dict[str, Any]:
2699
+ """Seleciona o melhor delta, aplica ao SOM e executa EWC.
2700
+
2701
+ Pipeline:
2702
+ 1. select_best_delta: avalia 3 tentativas × 16 hipóteses
2703
+ 2. Aplica δ* ao SOM: SOM ← SOM + δ*
2704
+ 3. Re-avalia acurácia após aplicação
2705
+ 4. Consolida via EWC: set_ewc_reference() (Fisher em w)
2706
+
2707
+ Returns:
2708
+ Dict com métricas (acc_before, acc_after, delta_norm, etc.)
2709
+ """
2710
+ import time as _time
2711
+ t0 = _time.time()
2712
+
2713
+ # Avalia acurácia ANTES da aplicação
2714
+ acc_before = self.evaluate_classification()
2715
+
2716
+ # Seleciona melhor delta
2717
+ best_delta, best_acc, sel_info = self.select_best_delta(
2718
+ buffer_4d, buffer_labels
2719
+ )
2720
+
2721
+ if best_delta is None:
2722
+ return {
2723
+ "active": False,
2724
+ "reason": "no_valid_delta",
2725
+ "elapsed_ms": float((_time.time() - t0) * 1000),
2726
+ }
2727
+
2728
+ # V6.5-V2-metrics-FIX-2 — Verifica se best_delta contém NaN/Inf ANTES
2729
+ # de aplicar ao SOM. User requirement: "investigar QE e KL resultando
2730
+ # em NAN". Se o delta tem NaN (devido a gradientes explosivos em
2731
+ # train_hypotheses), aplicar ao SOM contaminaria os pesos e propagaria
2732
+ # NaN para todas as métricas subsequentes (QE, KL, etc.).
2733
+ if torch.isnan(best_delta).any() or torch.isinf(best_delta).any():
2734
+ n_nan = int(torch.isnan(best_delta).sum().item())
2735
+ n_inf = int(torch.isinf(best_delta).sum().item())
2736
+ logger = logging.getLogger(__name__) if 'logging' in dir() else None
2737
+ import logging as _logging
2738
+ _logging.getLogger(__name__).warning(
2739
+ f"[V6.5-V2-metrics-FIX-2] best_delta contains NaN/Inf "
2740
+ f"(nan={n_nan}, inf={n_inf}). SKIPPING application to SOM "
2741
+ f"to prevent weight contamination. Investigate train_hypotheses "
2742
+ f"for gradient explosion (consider lowering hyp_lr or adding "
2743
+ f"gradient clipping)."
2744
+ )
2745
+ return {
2746
+ "active": False,
2747
+ "reason": f"best_delta_has_nan (nan={n_nan}, inf={n_inf})",
2748
+ "delta_norm": float(best_delta.norm().item()),
2749
+ "elapsed_ms": float((_time.time() - t0) * 1000),
2750
+ "nan_detected": True,
2751
+ }
2752
+
2753
+ # Aplica o delta ao SOM
2754
+ with torch.no_grad():
2755
+ som_flat = self.som.weights.flatten()
2756
+ som_flat_new = som_flat + best_delta
2757
+ # V6.5-V2-metrics-FIX-2 — verificação pós-aplicação: se os pesos
2758
+ # do SOM ficaram com NaN (deveria não acontecer após o check acima,
2759
+ # mas defense in depth), reverte para o estado anterior.
2760
+ if torch.isnan(som_flat_new).any() or torch.isinf(som_flat_new).any():
2761
+ import logging as _logging
2762
+ _logging.getLogger(__name__).error(
2763
+ f"[V6.5-V2-metrics-FIX-2] SOM weights became NaN/Inf after "
2764
+ f"delta application. REVERTING to previous weights."
2765
+ )
2766
+ else:
2767
+ self.som.weights = som_flat_new.view_as(self.som.weights)
2768
+
2769
+ # Re-avalia acurácia APÓS aplicação
2770
+ acc_after = self.evaluate_classification()
2771
+
2772
+ # Re-rotula neurônios (BMU labels podem ter mudado)
2773
+ self._label_neurons()
2774
+
2775
+ # Consolida via EWC (Fisher information em w)
2776
+ try:
2777
+ self.som.set_ewc_reference()
2778
+ ewc_set = True
2779
+ except Exception as e:
2780
+ ewc_set = False
2781
+ sel_info["ewc_error"] = str(e)[:200]
2782
+
2783
+ return {
2784
+ "active": True,
2785
+ "acc_before": float(acc_before),
2786
+ "acc_after": float(acc_after),
2787
+ "best_acc_during_selection": float(best_acc),
2788
+ "delta_norm": float(best_delta.norm().item()),
2789
+ "delta_scale": float(self.delta_scale.item()),
2790
+ "ewc_reference_set": bool(ewc_set),
2791
+ "selection_info": sel_info,
2792
+ "elapsed_ms": float((_time.time() - t0) * 1000),
2793
+ }
2794
+
2795
+ # ==================================================================
2796
+ # 2.4 Sobrescrita do process_batch para o novo protocolo V2
2797
+ # ==================================================================
2798
+ def process_batch_v2(
2799
+ self,
2800
+ sentences: List[str],
2801
+ labels: List[int],
2802
+ label_strings: Optional[Dict[int, str]] = None,
2803
+ dataset_name: Optional[str] = None,
2804
+ enable_punishment: bool = True,
2805
+ ) -> Dict[str, Any]:
2806
+ """Versão V2 do process_batch com 3 tentativas e 16 hipóteses.
2807
+
2808
+ Protocolo:
2809
+ - CONHECIMENTO (enable_punishment=False): apenas adiciona dados e
2810
+ treina SOM (sem aplicar punição).
2811
+ - TREINAMENTO COM PUNIÇÃO (enable_punishment=True):
2812
+ * acc < 1.0 → punishment_count += 1
2813
+ * punishment_count == 1: treinar 16 hipóteses + activate_hypothesis
2814
+ * punishment_count == 2: aplicar melhor delta (3×16) + EWC + reset
2815
+
2816
+ Args:
2817
+ sentences, labels, label_strings, dataset_name: mesmos de process_batch.
2818
+ enable_punishment: se False, apenas acumula conhecimento (fase 1).
2819
+
2820
+ Returns:
2821
+ Dict com métricas do batch processado.
2822
+ """
2823
+ import time as _time
2824
+ t0 = _time.time()
2825
+ result: Dict[str, Any] = {
2826
+ "phase": "CONHECIMENTO" if not enable_punishment else "PUNICAO",
2827
+ "dataset": dataset_name,
2828
+ "n_samples": len(sentences),
2829
+ "action": "none",
2830
+ "cycle_completed": False,
2831
+ }
2832
+
2833
+ # 1. Adiciona dados ao buffer
2834
+ self.add_data(
2835
+ sentences, labels,
2836
+ label_strings=label_strings,
2837
+ dataset_name=dataset_name,
2838
+ )
2839
+
2840
+ # 2. Verifica início de treino
2841
+ training_started = self.check_training_start()
2842
+ if training_started or self.training_ready:
2843
+ # 3. Treina SOM no buffer (regra de Kohonen)
2844
+ self.train_som_on_buffer()
2845
+ self._label_neurons()
2846
+ result["som_trained"] = True
2847
+
2848
+ if not self.training_ready:
2849
+ result["elapsed_ms"] = float((_time.time() - t0) * 1000)
2850
+ return result
2851
+
2852
+ # 4. Fase de CONHECIMENTO: para aqui, sem aplicar punição
2853
+ if not enable_punishment:
2854
+ acc = self.evaluate_classification()
2855
+ result["accuracy"] = float(acc)
2856
+ result["elapsed_ms"] = float((_time.time() - t0) * 1000)
2857
+ return result
2858
+
2859
+ # 5. Fase de PUNIÇÃO: avalia e aplica protocolo V2
2860
+ acc = self.evaluate_classification()
2861
+ result["accuracy"] = float(acc)
2862
+
2863
+ if acc < 1.0:
2864
+ self.punishment_count += 1
2865
+ self.success_count = 0
2866
+
2867
+ # V6.5-V2-dynamic — registra evento de punição
2868
+ self._record_punishment_event(True)
2869
+
2870
+ if self.punishment_count == 1:
2871
+ # 1ª punição: treinar hipóteses + ativar classificador
2872
+ hyp_info = self.train_hypotheses()
2873
+ if self.classifier is None:
2874
+ self.activate_hypothesis()
2875
+ else:
2876
+ # Re-treina classifier com buffer atual
2877
+ self.activate_hypothesis()
2878
+ result["action"] = "train_hypotheses_and_activate"
2879
+ result["hypotheses_training"] = hyp_info
2880
+
2881
+ elif self.punishment_count >= 2:
2882
+ # 2ª punição: selecionar melhor delta (n_trials × n_hyp) + aplicar + EWC
2883
+ apply_info = self.apply_best_delta_and_consolidate()
2884
+ result["action"] = "apply_best_delta_and_consolidate"
2885
+ result["delta_application"] = apply_info
2886
+
2887
+ # V6.5-V2-dynamic — adapta hiperparâmetros após punição recorrente
2888
+ try:
2889
+ adapt_record = self._adapt_hyperparameters(trigger="punishment")
2890
+ result["adaptation"] = adapt_record
2891
+ except Exception as adapt_err:
2892
+ result["adaptation_error"] = str(adapt_err)[:200]
2893
+
2894
+ # Reset do ciclo (mirror training)
2895
+ self.training_ready = False
2896
+ self.punishment_count = 0
2897
+ self.success_count = 0
2898
+ self.histogram.clear()
2899
+ # Mantém um sliding window do buffer para preservar contexto
2900
+ if len(self.buffer_4d) > 256:
2901
+ self.buffer_4d = self.buffer_4d[-256:]
2902
+ self.buffer_labels = self.buffer_labels[-256:]
2903
+ result["cycle_completed"] = True
2904
+ else:
2905
+ # acc == 1.0: sucesso
2906
+ self.punishment_count = 0
2907
+ self.success_count += 1
2908
+ # V6.5-V2-dynamic — registra sucesso (sem punição)
2909
+ self._record_punishment_event(False)
2910
+ result["action"] = "success"
2911
+
2912
+ result["elapsed_ms"] = float((_time.time() - t0) * 1000)
2913
+ return result
2914
+
2915
+ # ==================================================================
2916
+ # 2.5 API de monitoramento V2
2917
+ # ==================================================================
2918
+ def get_v2_metrics(self) -> Dict[str, Any]:
2919
+ """Retorna métricas específicas da versão V2 (inclui adaptação dinâmica)."""
2920
+ return {
2921
+ "version": "V2-dynamic",
2922
+ "n_hypotheses": int(self.n_hypotheses),
2923
+ "n_hypotheses_active": int(self.hypothesis_ensemble.active_count),
2924
+ "max_n_hypotheses": int(self.max_n_hypotheses),
2925
+ "n_trials": int(self.n_trials),
2926
+ "hyp_train_steps": int(self.hyp_train_steps),
2927
+ "hyp_lr": float(self.hyp_lr),
2928
+ "delta_scale": float(self.delta_scale.item()),
2929
+ "n_generators": int(len(self.hypothesis_ensemble.generators)),
2930
+ "punishment_count": int(self.punishment_count),
2931
+ "success_count": int(self.success_count),
2932
+ "training_ready": bool(self.training_ready),
2933
+ "classifier_trained": bool(self.classifier_trained),
2934
+ "ewc_reference_set": bool(self.som.old_weights_w is not None),
2935
+ "buffer_size": int(len(self.buffer_4d)),
2936
+ # V6.5-V2-metrics-FIX-2 — contadores cumulativos (user requirement:
2937
+ # "quantos passos de treino de hipóteses usado")
2938
+ "total_hyp_steps_executed": int(getattr(self, "_total_hyp_steps_executed", 0)),
2939
+ "n_train_hyp_calls": int(getattr(self, "_n_train_hyp_calls", 0)),
2940
+ "dynamic_adaptation": {
2941
+ "loss_history_len": int(len(self._loss_history)),
2942
+ "loss_stats": self._compute_loss_stats(),
2943
+ "punishment_rate": float(self._compute_punishment_rate()),
2944
+ "punishment_window_size": int(len(self._punishment_history)),
2945
+ "n_adaptations": int(len(self._adaptation_log)),
2946
+ "limits": {
2947
+ "min_n_hypotheses": int(self.min_n_hypotheses),
2948
+ "max_n_hypotheses": int(self.max_n_hypotheses),
2949
+ "min_n_trials": int(self.min_n_trials),
2950
+ "max_n_trials": int(self.max_n_trials),
2951
+ "min_hyp_train_steps": int(self.min_hyp_train_steps),
2952
+ "max_hyp_train_steps": int(self.max_hyp_train_steps),
2953
+ },
2954
+ "last_5_adaptations": self._adaptation_log[-5:] if self._adaptation_log else [],
2955
+ },
2956
+ }
2957
+
2958
+ # ==================================================================
2959
+ # V6.5-V2-metrics-FIX-2 — Detecção de atividade de hipóteses e neurônios
2960
+ # ==================================================================
2961
+ # User requirement (latest): "Aprimorar a FASE2 PUNITIVA investigar QE e KL
2962
+ # resultando em NAN, acrescentar detecção da atividade das layers (quantas
2963
+ # ativadas) de hipóteses e das ativações dos neurônios, quantos passos de
2964
+ # treino de hipóteses usado".
2965
+ #
2966
+ # Mathematical formulation:
2967
+ #
2968
+ # Hipótese ativa: Um gerador g_h (h ∈ {1..n_hypotheses}) é considerado
2969
+ # "ativo" em um batch se o delta que ele produz tem norma L2 > threshold
2970
+ # (default 1e-6). Deltas com norma ≈ 0 indicam que o gerador está
2971
+ # produzindo saída trivial (morto) — não contribui para a diversidade
2972
+ # do ensemble.
2973
+ #
2974
+ # active_h = 1 if ||δ_h||₂ > threshold else 0
2975
+ # n_active_hypotheses = Σ_h active_h
2976
+ #
2977
+ # Neurônio ativo: Um neurônio (i,j,k,l) é "ativo" se é BMU para pelo
2978
+ # menos uma amostra do buffer atual. Reuso da lógica de dead_neuron_rate,
2979
+ # mas exposto como métrica explícita para monitoramento contínuo.
2980
+ #
2981
+ # n_active_neurons = |{W_bmu(x_i) : i ∈ buffer}|
2982
+ # n_dead_neurons = n_total - n_active_neurons
2983
+ # neuron_activation_rate = n_active_neurons / n_total
2984
+ #
2985
+ # Passos de treino de hipóteses: Número total de passos de Adam executados
2986
+ # em train_hypotheses() desde o início do treino. Cumulativo ao longo de
2987
+ # todas as chamadas (punições).
2988
+ #
2989
+ # total_hyp_train_steps = Σ_call self.hyp_train_steps (na época da call)
2990
+ # ------------------------------------------------------------------
2991
+ def detect_hypothesis_activity(
2992
+ self,
2993
+ threshold: float = 1e-6,
2994
+ ) -> Dict[str, Any]:
2995
+ """V6.5-V2-metrics-FIX-2 — Detecta quantas hipóteses estão ativas.
2996
+
2997
+ User requirement: "acrescentar detecção da atividade das layers
2998
+ (quantas ativadas) de hipóteses".
2999
+
3000
+ Executa um forward pass do HypothesisEnsemble sobre a ativação média
3001
+ do SOM no buffer atual e conta quantos dos active_count geradores
3002
+ produzem deltas com norma > threshold.
3003
+
3004
+ Args:
3005
+ threshold: norma L2 mínima para considerar um delta "ativo".
3006
+
3007
+ Returns:
3008
+ Dict com:
3009
+ - n_hypotheses_active: int (quantas hipóteses produziram delta ≠ 0)
3010
+ - n_hypotheses_total: int (active_count atual do ensemble)
3011
+ - max_n_hypotheses: int (limite máximo pré-alocado)
3012
+ - delta_norms: list[float] (norma de cada delta, para inspeção)
3013
+ - active_mask: list[bool] (True para hipóteses ativas)
3014
+ - activation_rate: float (n_active / n_total)
3015
+ - mean_delta_norm: float (média das normas)
3016
+ - max_delta_norm: float (maior norma)
3017
+ - threshold_used: float
3018
+ """
3019
+ if not self.buffer_4d:
3020
+ return {
3021
+ "n_hypotheses_active": 0,
3022
+ "n_hypotheses_total": int(self.hypothesis_ensemble.active_count),
3023
+ "max_n_hypotheses": int(self.max_n_hypotheses),
3024
+ "delta_norms": [],
3025
+ "active_mask": [],
3026
+ "activation_rate": 0.0,
3027
+ "mean_delta_norm": 0.0,
3028
+ "max_delta_norm": 0.0,
3029
+ "threshold_used": float(threshold),
3030
+ "reason": "empty_buffer",
3031
+ }
3032
+ try:
3033
+ import torch as _torch
3034
+ with _torch.no_grad():
3035
+ # Computa ativação SOM média do buffer
3036
+ data = _torch.stack(self.buffer_4d).detach()
3037
+ som_activations = _torch.stack(
3038
+ [self._som_activation(data[i]) for i in range(len(data))]
3039
+ )
3040
+ x_mean = som_activations.mean(dim=0, keepdim=True) # (1, P_som)
3041
+
3042
+ # Forward do ensemble: retorna (1, n_hyp, P)
3043
+ deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
3044
+ deltas_stack = self.delta_scale * deltas_stack # aplica escala
3045
+
3046
+ # Norma L2 de cada delta: (n_hyp,)
3047
+ delta_norms = deltas_stack[0].norm(dim=-1) # (n_hyp,)
3048
+ delta_norms_list = [float(x) for x in delta_norms]
3049
+ active_mask = [bool(x > threshold) for x in delta_norms_list]
3050
+ n_active = sum(active_mask)
3051
+
3052
+ # Libera tensores
3053
+ del deltas_stack, som_activations, data, x_mean, delta_norms
3054
+
3055
+ return {
3056
+ "n_hypotheses_active": int(n_active),
3057
+ "n_hypotheses_total": int(self.hypothesis_ensemble.active_count),
3058
+ "max_n_hypotheses": int(self.max_n_hypotheses),
3059
+ "delta_norms": delta_norms_list,
3060
+ "active_mask": active_mask,
3061
+ "activation_rate": float(n_active / max(1, self.hypothesis_ensemble.active_count)),
3062
+ "mean_delta_norm": float(sum(delta_norms_list) / max(1, len(delta_norms_list))),
3063
+ "max_delta_norm": float(max(delta_norms_list) if delta_norms_list else 0.0),
3064
+ "threshold_used": float(threshold),
3065
+ }
3066
+ except Exception as e:
3067
+ return {
3068
+ "n_hypotheses_active": 0,
3069
+ "n_hypotheses_total": int(self.hypothesis_ensemble.active_count),
3070
+ "max_n_hypotheses": int(self.max_n_hypotheses),
3071
+ "delta_norms": [],
3072
+ "active_mask": [],
3073
+ "activation_rate": 0.0,
3074
+ "mean_delta_norm": 0.0,
3075
+ "max_delta_norm": 0.0,
3076
+ "threshold_used": float(threshold),
3077
+ "error": str(e)[:200],
3078
+ }
3079
+
3080
+ def count_active_neurons(self) -> Dict[str, Any]:
3081
+ """V6.5-V2-metrics-FIX-2 — Conta neurônios ativos (BMUs) no buffer.
3082
+
3083
+ User requirement: "acrescentar (...) das ativações dos neurônios".
3084
+
3085
+ Para cada amostra no buffer_4d, encontra o BMU. Conta quantos
3086
+ neurônios únicos são BMUs (n_active_neurons) e quantos nunca são
3087
+ BMUs (n_dead_neurons = n_total - n_active).
3088
+
3089
+ Returns:
3090
+ Dict com:
3091
+ - n_active_neurons: int (neurônios que são BMU para ≥1 amostra)
3092
+ - n_dead_neurons: int (neurônios que nunca são BMU)
3093
+ - n_total_neurons: int (total de neurônios no SOM)
3094
+ - neuron_activation_rate: float (n_active / n_total)
3095
+ - bmu_distribution: dict {flat_idx: count} (top-20 mais frequentes)
3096
+ """
3097
+ if not self.buffer_4d:
3098
+ return {
3099
+ "n_active_neurons": 0,
3100
+ "n_dead_neurons": int(self.som_neuron_count),
3101
+ "n_total_neurons": int(self.som_neuron_count),
3102
+ "neuron_activation_rate": 0.0,
3103
+ "bmu_distribution": {},
3104
+ "reason": "empty_buffer",
3105
+ }
3106
+ try:
3107
+ import torch as _torch
3108
+ with _torch.no_grad():
3109
+ data = _torch.stack(self.buffer_4d).detach()
3110
+ flat_w = self.som.weights.view(-1, 4).detach()
3111
+ # (N, M) distâncias
3112
+ diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
3113
+ dist_sq = _torch.sum(diff * diff, dim=-1)
3114
+ _, bmu_flat = _torch.min(dist_sq, dim=1) # (N,)
3115
+ bmu_list = bmu_flat.tolist()
3116
+
3117
+ unique_bmus = set(bmu_list)
3118
+ n_active = len(unique_bmus)
3119
+ n_total = self.som_neuron_count
3120
+
3121
+ # Distribuição dos BMUs (top-20)
3122
+ from collections import Counter
3123
+ bmu_counts = Counter(bmu_list)
3124
+ bmu_distribution = dict(bmu_counts.most_common(20))
3125
+
3126
+ del data, flat_w, diff, dist_sq, bmu_flat
3127
+ return {
3128
+ "n_active_neurons": int(n_active),
3129
+ "n_dead_neurons": int(n_total - n_active),
3130
+ "n_total_neurons": int(n_total),
3131
+ "neuron_activation_rate": float(n_active / max(1, n_total)),
3132
+ "bmu_distribution": {str(k): int(v) for k, v in bmu_distribution.items()},
3133
+ }
3134
+ except Exception as e:
3135
+ return {
3136
+ "n_active_neurons": 0,
3137
+ "n_dead_neurons": int(self.som_neuron_count),
3138
+ "n_total_neurons": int(self.som_neuron_count),
3139
+ "neuron_activation_rate": 0.0,
3140
+ "bmu_distribution": {},
3141
+ "error": str(e)[:200],
3142
+ }
3143
+
3144
+ def get_hyp_train_steps_used(self) -> Dict[str, Any]:
3145
+ """V6.5-V2-metrics-FIX-2 — Retorna quantos passos de treino de hipóteses
3146
+ foram usados até agora.
3147
+
3148
+ User requirement: "quantos passos de treino de hipóteses usado".
3149
+
3150
+ Returns:
3151
+ Dict com:
3152
+ - current_hyp_train_steps: int (valor ATUAL de self.hyp_train_steps)
3153
+ - total_hyp_train_steps_executed: int (cumulativo ao longo do treino)
3154
+ - n_train_hypotheses_calls: int (número de chamadas a train_hypotheses)
3155
+ - min_hyp_train_steps: int (limite inferior dinâmico)
3156
+ - max_hyp_train_steps: int (limite superior dinâmico)
3157
+ """
3158
+ return {
3159
+ "current_hyp_train_steps": int(self.hyp_train_steps),
3160
+ "total_hyp_train_steps_executed": int(getattr(self, "_total_hyp_steps_executed", 0)),
3161
+ "n_train_hypotheses_calls": int(getattr(self, "_n_train_hyp_calls", 0)),
3162
+ "min_hyp_train_steps": int(self.min_hyp_train_steps),
3163
+ "max_hyp_train_steps": int(self.max_hyp_train_steps),
3164
+ }
3165
+
3166
+ # ==================================================================
3167
+ # V6.5-V2-metrics — Integração das métricas SOM canônicas
3168
+ # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
3169
+ # Colapso Topológico, Estagnação QE, Cruzamento Vizinhança)
3170
+ # ==================================================================
3171
+ def compute_som_metrics(self) -> Dict[str, Any]:
3172
+ """V6.5-V2-metrics — Computa todas as métricas SOM canônicas.
3173
+
3174
+ User requirement: "ANALISAR matematicamente e logicamente e inserir
3175
+ melhorias para os scripts das métricas de aprendizado e indicadores
3176
+ de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen)
3177
+ avaliam a fidelidade de representação dos dados e a preservação da
3178
+ vizinhança topológica".
3179
+
3180
+ Returns:
3181
+ Dict com 8 métricas:
3182
+ 1. quantization_error (QE)
3183
+ 2. topological_error (TE)
3184
+ 3. kaski_lagus_error (KL)
3185
+ 4. explained_variance_share (VE)
3186
+ 5. topological_collapse (dict)
3187
+ 6. dead_neuron_rate (dict)
3188
+ 7. qe_stagnation (dict) — requer histórico
3189
+ 8. neighborhood_crossing (dict) — requer histórico
3190
+ Plus: overall_health, failure_indicators
3191
+ """
3192
+ from bigru_t.model.som_metrics import compute_all_metrics
3193
+ if not self.buffer_4d:
3194
+ return {
3195
+ "quantization_error": 0.0,
3196
+ "topological_error": 0.0,
3197
+ "kaski_lagus_error": 0.0,
3198
+ "explained_variance_share": 0.0,
3199
+ "topological_collapse": {"severity": "none", "effective_rank": 0.0},
3200
+ "dead_neuron_rate": {"dead_neuron_rate": 1.0, "n_dead": self.som_neuron_count,
3201
+ "n_active": 0, "n_total": self.som_neuron_count},
3202
+ "qe_stagnation": {"is_stagnant": False, "severity": "none"},
3203
+ "neighborhood_crossing": {"detected": False, "severity": "none"},
3204
+ "overall_health": "unknown",
3205
+ "failure_indicators": ["no_buffer_data"],
3206
+ "n_failure_indicators": 1,
3207
+ "note": "buffer_4d vazio — métricas indisponíveis",
3208
+ }
3209
+ # Stack buffer em tensor (N, 4)
3210
+ data = torch.stack([
3211
+ v.detach().clone() if isinstance(v, torch.Tensor) else torch.tensor(v, dtype=torch.float)
3212
+ for v in self.buffer_4d
3213
+ ]).float()
3214
+ weights = self.som.weights.detach()
3215
+ metrics = compute_all_metrics(data, weights, self._som_metric_history)
3216
+ # Registra no histórico
3217
+ self._som_metric_history.record(metrics)
3218
+ return metrics
3219
+
3220
+ def get_som_metric_history(self) -> Dict[str, Any]:
3221
+ """Retorna histórico de métricas SOM (para análise temporal)."""
3222
+ return self._som_metric_history.to_dict()
3223
+
3224
+ def reset_som_metric_history(self) -> None:
3225
+ """Reseta o histórico de métricas SOM (início de nova fase)."""
3226
+ from bigru_t.model.som_metrics import SOMMetricHistory
3227
+ self._som_metric_history = SOMMetricHistory()
3228
+
3229
+
3230
+ __all__ = [
3231
+ "SimpleBBPETokenizer",
3232
+ "positional_encoding",
3233
+ "text_to_4d_vector",
3234
+ "KohonenSOM4D",
3235
+ "HypothesisClassifier",
3236
+ "KohonenLearningSystem",
3237
+ "DeltaGenerator",
3238
+ "HypothesisEnsemble",
3239
+ "KohonenLearningSystemV2",
3240
+ ]
3241
+ # V6.5-V2-metrics: som_metrics é importado lazy (dentro de compute_som_metrics)
3242
+ # para evitar dependência circular e permitir uso standalone.
som_metrics.py → src/bigru_t/model/som_metrics.py RENAMED
File without changes
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 ===")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vqvae2_hierarchical.py DELETED
@@ -1,200 +0,0 @@
1
- """
2
- Xavante - vqvae2_hierarchical.py
3
- Responsabilidade: VQ-VAE-2 hierarquica (Teorema 13.1/13.2/13.3).
4
- Reutiliza implementacao validada do FlexNet com EMA + Goose VQ + dead code restart.
5
- Referencia: Razavi et al. 2019 - Generating Diverse High-Fidelity Images with VQ-VAE-2.
6
- """
7
- from __future__ import annotations
8
-
9
- import logging
10
- import math
11
- from typing import List, Tuple
12
-
13
- import torch
14
- import torch.nn as nn
15
- import torch.nn.functional as F
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class VectorQuantizerEMA(nn.Module):
21
- """
22
- VQ com atualizacao EMA do codebook (validado no FlexNet).
23
- Inclui: dead code restart, commitment loss, Goose VQ.
24
- v6 aprimoramentos:
25
- - Diversity loss (entropy bonus) para forcar uso uniforme do codebook
26
- - Commitment warmup: beta_t = beta_0 * min(1, t/warmup)
27
- - EMA restart mais agressivo (decay adaptativo)
28
- """
29
-
30
- def __init__(
31
- self,
32
- num_embeddings: int,
33
- embedding_dim: int,
34
- commitment_cost: float = 0.25,
35
- decay: float = 0.99,
36
- epsilon: float = 1e-5,
37
- dead_threshold: int = 1,
38
- diversity_weight: float = 0.1,
39
- warmup_steps: int = 500,
40
- ):
41
- super().__init__()
42
- self.num_embeddings = num_embeddings
43
- self.embedding_dim = embedding_dim
44
- self.commitment_cost = commitment_cost
45
- self.decay = decay
46
- self.epsilon = epsilon
47
- self.dead_threshold = dead_threshold
48
- self.diversity_weight = diversity_weight
49
- self.warmup_steps = warmup_steps
50
- self._step = 0
51
-
52
- embed = torch.empty(num_embeddings, embedding_dim)
53
- nn.init.normal_(embed, mean=0.0, std=0.02)
54
- self.register_buffer("embeddings", embed)
55
- self.register_buffer("ema_count", torch.zeros(num_embeddings))
56
- self.register_buffer("ema_weight", embed.clone())
57
- self.register_buffer("usage_count", torch.zeros(num_embeddings, dtype=torch.long))
58
-
59
- def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
60
- """
61
- x: [B, D, H, W] ou [B, D]
62
- Retorna (quantized, loss, encoding_indices)
63
- """
64
- if x.dim() == 4:
65
- # [B, D, H, W] -> [B, H, W, D] -> [B*H*W, D]
66
- B, D, H, W = x.shape
67
- flat = x.permute(0, 2, 3, 1).reshape(-1, self.embedding_dim)
68
- else:
69
- # [B, D] -> [B, D]
70
- flat = x.reshape(-1, self.embedding_dim)
71
- # Distancia
72
- distances = (
73
- flat.pow(2).sum(1, keepdim=True)
74
- - 2 * flat @ self.embeddings.t()
75
- + self.embeddings.pow(2).sum(1)
76
- )
77
- indices = torch.argmin(distances, dim=1)
78
- one_hot = F.one_hot(indices, self.num_embeddings).type(flat.dtype)
79
- quantized = one_hot @ self.embeddings
80
- # Uso
81
- self.usage_count.index_add_(0, indices, torch.ones_like(indices))
82
-
83
- # EMA update
84
- if self.training:
85
- with torch.no_grad():
86
- count = one_hot.sum(0)
87
- ema_count_new = self.decay * self.ema_count + (1 - self.decay) * count
88
- total = ema_count_new.sum()
89
- ema_count_new = (ema_count_new + self.epsilon) / (total + self.num_embeddings * self.epsilon) * total
90
- dw = one_hot.t() @ flat
91
- self.ema_weight = self.decay * self.ema_weight + (1 - self.decay) * dw
92
- self.ema_count = ema_count_new
93
- self.embeddings.copy_(self.ema_weight / self.ema_count.unsqueeze(1))
94
-
95
- # Dead code restart
96
- dead = self.usage_count < self.dead_threshold
97
- if dead.any() and flat.shape[0] > 0:
98
- dead_indices = dead.nonzero(as_tuple=True)[0]
99
- n_dead = dead_indices.shape[0]
100
- # Sample min(n_dead, flat.shape[0]) candidates from flat
101
- n_replace = min(n_dead, flat.shape[0])
102
- if n_replace > 0:
103
- idx = torch.randperm(flat.shape[0])[:n_replace].to(flat.device)
104
- # Substituir apenas n_replace codigos mortos
105
- self.embeddings.data[dead_indices[:n_replace]] = flat[idx].detach()
106
-
107
- # Commitment loss com warmup
108
- self._step += 1
109
- beta = self.commitment_cost * min(1.0, self._step / max(self.warmup_steps, 1))
110
- e_latent_loss = F.mse_loss(quantized.detach(), flat)
111
- q_latent_loss = F.mse_loss(quantized, flat.detach())
112
- # Diversity loss: -H(p) onde p = freq de uso de cada code
113
- # Maximizar entropia = forçar uso uniforme do codebook
114
- if self.diversity_weight > 0 and self.training:
115
- code_probs = one_hot.float().mean(dim=0) # [num_embeddings]
116
- code_probs = code_probs.clamp_min(1e-10)
117
- entropy = -(code_probs * code_probs.log()).sum()
118
- diversity_loss = -entropy / math.log(self.num_embeddings) # normalize to [0, 1]
119
- else:
120
- diversity_loss = torch.tensor(0.0, device=x.device)
121
- loss = q_latent_loss + beta * e_latent_loss + self.diversity_weight * diversity_loss
122
-
123
- # Straight-through
124
- quantized = flat + (quantized - flat).detach()
125
- # Restore shape
126
- if x.dim() == 4:
127
- quantized = quantized.view(B, H, W, D).permute(0, 3, 1, 2).contiguous()
128
- indices = indices.view(B, H, W)
129
- else:
130
- quantized = quantized.view_as(x)
131
- return quantized, loss, indices
132
-
133
-
134
- class HierarchicalVQVAE2(nn.Module):
135
- """
136
- VQ-VAE-2 com 2 niveis: top (global) e bottom (local).
137
- Mapeia: Teorema 13.1/13.2/13.3.
138
- """
139
-
140
- def __init__(
141
- self,
142
- in_channels: int = 3,
143
- hidden_dim: int = 64,
144
- top_codebook: int = 128,
145
- bottom_codebook: int = 512,
146
- commitment_cost: float = 0.25,
147
- decay: float = 0.99,
148
- ):
149
- super().__init__()
150
- self.encoder_bottom = nn.Sequential(
151
- nn.Conv2d(in_channels, hidden_dim, 4, 2, 1),
152
- nn.GELU(),
153
- nn.Conv2d(hidden_dim, hidden_dim, 4, 2, 1),
154
- nn.GELU(),
155
- nn.Conv2d(hidden_dim, hidden_dim, 3, 1, 1),
156
- )
157
- self.encoder_top = nn.Sequential(
158
- nn.Conv2d(hidden_dim, hidden_dim, 4, 2, 1),
159
- nn.GELU(),
160
- nn.Conv2d(hidden_dim, hidden_dim, 4, 2, 1),
161
- nn.GELU(),
162
- nn.Conv2d(hidden_dim, hidden_dim, 3, 1, 1),
163
- )
164
- self.vq_top = VectorQuantizerEMA(top_codebook, hidden_dim, commitment_cost, decay)
165
- self.vq_bottom = VectorQuantizerEMA(bottom_codebook, hidden_dim, commitment_cost, decay)
166
- self.decoder_top = nn.Sequential(
167
- nn.ConvTranspose2d(hidden_dim, hidden_dim, 4, 2, 1),
168
- nn.GELU(),
169
- nn.ConvTranspose2d(hidden_dim, hidden_dim, 4, 2, 1),
170
- nn.GELU(),
171
- )
172
- self.decoder_bottom = nn.Sequential(
173
- nn.ConvTranspose2d(hidden_dim * 2, hidden_dim, 4, 2, 1),
174
- nn.GELU(),
175
- nn.ConvTranspose2d(hidden_dim, hidden_dim, 4, 2, 1),
176
- nn.GELU(),
177
- nn.Conv2d(hidden_dim, in_channels, 3, 1, 1),
178
- )
179
-
180
- def forward(self, x: torch.Tensor) -> dict:
181
- # Bottom encode
182
- ze_b = self.encoder_bottom(x)
183
- # Top encode
184
- ze_t = self.encoder_top(ze_b)
185
- zq_t, loss_t, idx_t = self.vq_top(ze_t)
186
- # Decodifica top e soma com bottom
187
- dec_t = self.decoder_top(zq_t)
188
- # Concat com bottom quantized
189
- zq_b, loss_b, idx_b = self.vq_bottom(ze_b)
190
- cat = torch.cat([dec_t, zq_b], dim=1)
191
- x_recon = self.decoder_bottom(cat)
192
- return {
193
- "reconstruction": x_recon,
194
- "loss": loss_t + loss_b,
195
- "top_indices": idx_t,
196
- "bottom_indices": idx_b,
197
- }
198
-
199
-
200
- __all__ = ["VectorQuantizerEMA", "HierarchicalVQVAE2"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vqvae2_hierarchical_flexnet.py DELETED
@@ -1,513 +0,0 @@
1
- """
2
- vqvae2_hierarchical — VQ-VAE-2 (Razavi 2019) hierárquico com 2 níveis de codebook.
3
-
4
- Arquitetura:
5
- - Top level: codebook K_top captura estrutura global (low-resolution)
6
- - Bottom level: codebook K_bot captura detalhes locais (high-resolution)
7
- - Cada nível tem encoder/decoder proprio
8
- - Residual quantization: bottom level quantiza o RESÍDUO do top level
9
-
10
- Loss total:
11
- L = ||x - D(top_q, bot_q)||²
12
- + ||sg[z_top] - top_q||² + β·||z_top - sg[top_q]||²
13
- + ||sg[z_bot] - bot_q||² + β·||z_bot - sg[bot_q]||²
14
-
15
- Implementa:
16
- - EMA updates em ambos codebooks
17
- - Dead code restart em ambos codebooks
18
- - Goose VQ com schedule de temperatura (alto→baixo)
19
-
20
- Setup dos testes (solicitado):
21
- - batch=256, K_top=512, K_bot=512
22
- - Goose τ schedule: começa 2.0, decai para 0.5 via cosine
23
- - EMA decay γ=0.99
24
- - Dead code restart a cada 3 épocas
25
- """
26
-
27
- from __future__ import annotations
28
- from typing import Dict, Optional, Tuple
29
-
30
- import math
31
- import numpy as np
32
- import torch
33
- import torch.nn as nn
34
- import torch.nn.functional as F
35
-
36
-
37
- # ============================================================
38
- # v12.8: Normalizações alternativas (substituem LayerNorm v12.7)
39
- # ============================================================
40
-
41
- class RMSNorm(nn.Module):
42
- """RMSNorm — preserva magnitude (não subtrai média como LayerNorm).
43
-
44
- y = x / sqrt(mean(x²) + eps) * gamma
45
-
46
- Vantagens sobre LayerNorm:
47
- - Mais barato (sem cálculo de variância)
48
- - Preserva magnitude do sinal (importante para VQ-VAE)
49
- - Zhang & Sennrich 2019
50
- """
51
-
52
- def __init__(self, dim: int, eps: float = 1e-6):
53
- super().__init__()
54
- self.gamma = nn.Parameter(torch.ones(dim))
55
- self.eps = eps
56
-
57
- def forward(self, x: torch.Tensor) -> torch.Tensor:
58
- rms = torch.sqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
59
- return self.gamma * x / rms
60
-
61
-
62
- def apply_weightnorm(module: nn.Module) -> nn.Module:
63
- """Aplica weight normalization (EnCodec-style) a um módulo Linear.
64
-
65
- Salakhutdinov & Wang 2016: w = g * v / ||v||, onde g é escalar aprendido.
66
-
67
- Vantagens:
68
- - Normaliza apenas o peso, não a ativação
69
- - Preserva estatística do input (importante para VQ-VAE)
70
- - Usado em EnCodec (Défossez 2022) e SoundStream (Zeghidour 2021)
71
- """
72
- return nn.utils.weight_norm(module)
73
-
74
-
75
- class HierarchicalVectorQuantizer(nn.Module):
76
- """Quantizador hierárquico com 2 níveis (top + bottom) e técnicas anti-collapse.
77
-
78
- Args:
79
- num_codes_top: K_top (codebook de topo, captura estrutura global)
80
- num_codes_bot: K_bot (codebook de base, captura detalhes)
81
- code_dim: D (dimensão compartilhada)
82
- beta: peso do commitment loss
83
- ema_decay: γ para EMA (default 0.99)
84
- dead_code_threshold: N_k abaixo do qual código é morto
85
- dead_code_restart_every: a cada quantas épocas verificar
86
- goose_temp_init: temperatura Goose inicial (default 2.0)
87
- goose_temp_final: temperatura Goose final (default 0.5)
88
- goose_schedule: 'cosine' ou 'linear'
89
- total_epochs: total de épocas (para schedule)
90
- """
91
-
92
- def __init__(
93
- self,
94
- num_codes_top: int = 512,
95
- num_codes_bot: int = 512,
96
- code_dim: int = 32,
97
- beta: float = 0.25,
98
- ema_decay: float = 0.99,
99
- dead_code_threshold: float = 1.0,
100
- dead_code_restart_every: int = 3,
101
- goose_temp_init: float = 2.0,
102
- goose_temp_final: float = 0.5,
103
- goose_schedule: str = "cosine",
104
- total_epochs: int = 25,
105
- norm_type: str = "none", # v12.8: "none" | "weightnorm" | "rmsnorm"
106
- ):
107
- super().__init__()
108
- self.num_codes_top = num_codes_top
109
- self.num_codes_bot = num_codes_bot
110
- self.code_dim = code_dim
111
- self.beta = beta
112
- self.ema_decay = ema_decay
113
- self.dead_code_threshold = dead_code_threshold
114
- self.dead_code_restart_every = dead_code_restart_every
115
- self.goose_temp_init = goose_temp_init
116
- self.goose_temp_final = goose_temp_final
117
- self.goose_schedule = goose_schedule
118
- self.total_epochs = total_epochs
119
- self.norm_type = norm_type # v12.8
120
-
121
- # Codebooks (buffers, atualizados via EMA).
122
- self.register_buffer('codebook_top', torch.randn(num_codes_top, code_dim) * 0.05)
123
- self.register_buffer('codebook_bot', torch.randn(num_codes_bot, code_dim) * 0.05)
124
- # EMA state para top.
125
- self.register_buffer('ema_cluster_top', torch.zeros(num_codes_top))
126
- self.register_buffer('ema_w_top', torch.zeros(num_codes_top, code_dim))
127
- # EMA state para bottom.
128
- self.register_buffer('ema_cluster_bot', torch.zeros(num_codes_bot))
129
- self.register_buffer('ema_w_bot', torch.zeros(num_codes_bot, code_dim))
130
-
131
- # v12.8: Normalização opcional dentro do VQ (substitui LayerNorm v12.7 que degradou).
132
- # - "none": sem normalização (preserva estatística original — RECOMENDADO para VQ-VAE)
133
- # - "weightnorm": weight normalization (EnCodec-style, preserva magnitude)
134
- # - "rmsnorm": RMSNorm (preserva magnitude, mais barato que LayerNorm)
135
- # Nota: LayerNorm v12.7 foi REMOVIDO pois degradou resultados (17× pior loss).
136
- if norm_type == "rmsnorm":
137
- # RMSNorm: y = x / sqrt(mean(x²) + eps) * gamma
138
- self.rms_pre_top = RMSNorm(code_dim)
139
- self.rms_pre_bot = RMSNorm(code_dim)
140
- self.rms_post_combine = RMSNorm(code_dim)
141
- else:
142
- # "none" ou "weightnorm" (weightnorm é aplicado nos encoders, não aqui).
143
- self.rms_pre_top = nn.Identity()
144
- self.rms_pre_bot = nn.Identity()
145
- self.rms_post_combine = nn.Identity()
146
-
147
- # Contador de épocas.
148
- self.register_buffer('epoch_counter', torch.zeros(1, dtype=torch.long))
149
-
150
- # Estatísticas de uso.
151
- self.register_buffer('usage_top', torch.zeros(num_codes_top))
152
- self.register_buffer('usage_bot', torch.zeros(num_codes_bot))
153
-
154
- # Temperatura Goose atual (atualizada via update_temperature).
155
- self.register_buffer('current_temp', torch.tensor(goose_temp_init))
156
-
157
- def update_temperature(self, grad_recon: float = None, grad_vq: float = None):
158
- """Atualiza temperatura Goose baseada na época atual (schedule).
159
-
160
- v13.9: Se grad_recon e grad_vq fornecidos, usa mas_weight adaptativo:
161
- τ* = ||∇L_recon|| / ||∇L_vq||
162
- Caso contrário, usa cosine decay padrão.
163
- """
164
- epoch = int(self.epoch_counter.item())
165
- progress = min(epoch / max(self.total_epochs, 1), 1.0)
166
-
167
- if grad_recon is not None and grad_vq is not None and grad_vq > 1e-8:
168
- # v13.9: τ* adaptativo via mas_weight formula.
169
- from .mathematical_optimizations import compute_goose_tau_adaptive
170
- prev_tau = float(self.current_temp)
171
- temp = compute_goose_tau_adaptive(grad_recon, grad_vq, prev_tau,
172
- tau_min=self.goose_temp_final,
173
- tau_max=self.goose_temp_init)
174
- elif self.goose_schedule == "cosine":
175
- temp = self.goose_temp_final + 0.5 * (self.goose_temp_init - self.goose_temp_final) * \
176
- (1 + math.cos(math.pi * progress))
177
- else: # linear
178
- temp = self.goose_temp_init + (self.goose_temp_final - self.goose_temp_init) * progress
179
- self.current_temp.fill_(temp)
180
- return float(temp)
181
-
182
- def _quantize_level(self, z: torch.Tensor, codebook: torch.Tensor,
183
- ema_cluster: torch.Tensor, ema_w: torch.Tensor,
184
- use_goose: bool = True) -> Tuple[torch.Tensor, torch.Tensor, int]:
185
- """Quantiza um nível (top ou bottom) com EMA + Goose opcional."""
186
- B, D = z.shape
187
- # Distâncias.
188
- dist = (
189
- z.pow(2).sum(dim=1, keepdim=True)
190
- + codebook.pow(2).sum(dim=1)
191
- - 2 * z @ codebook.t()
192
- ) # (B, K)
193
-
194
- if use_goose and self.training:
195
- # Goose VQ: Gumbel-softmax sampling.
196
- logits = -dist / max(float(self.current_temp), 1e-6)
197
- codes_soft = F.gumbel_softmax(logits, tau=float(self.current_temp), hard=True)
198
- codes = codes_soft.argmax(dim=1)
199
- z_q = codes_soft @ codebook
200
- else:
201
- # argmin (eval ou fallback).
202
- codes = dist.argmin(dim=1)
203
- z_q = codebook[codes]
204
-
205
- # EMA update (apenas em treino).
206
- n_restarted = 0
207
- if self.training:
208
- one_hot = F.one_hot(codes, codebook.size(0)).float()
209
- new_cluster = one_hot.sum(dim=0)
210
- # v13.3fix: Save OLD values before EMA update for cb_update computation
211
- old_cluster = ema_cluster.clone()
212
- old_w = ema_w.clone()
213
- ema_cluster.mul_(self.ema_decay).add_(new_cluster, alpha=1 - self.ema_decay)
214
- new_w = one_hot.t() @ z
215
- ema_w.mul_(self.ema_decay).add_(new_w, alpha=1 - self.ema_decay)
216
- # v13.3: Correções matemáticas no EMA (mal-conditioning do codebook).
217
- # Use OLD (pre-update) values for cb_update, as required by proper EMA formula.
218
- n_old = old_cluster.sum()
219
- # 1. Smoothing do cluster size (reduz condition number).
220
- cluster_mean = old_cluster.mean()
221
- cluster_smoothed = 0.9 * old_cluster + 0.1 * cluster_mean
222
- # 2. Clipping logarítmico do cluster size (comprime escala dinâmica).
223
- eps_cb = 1e-5
224
- cluster_clipped = torch.log1p(cluster_smoothed / eps_cb) / \
225
- torch.log1p(cluster_smoothed.max() / eps_cb + 1e-12)
226
- # 3. Renormalizar para magnitude original.
227
- cluster_final = cluster_clipped * n_old / (cluster_clipped.sum() + 1e-12)
228
- smoothed = (cluster_final + eps_cb) / (n_old + codebook.size(0) * eps_cb) * n_old
229
- # v13.3: Clamp do codebook update para prevenir NaN/Inf.
230
- # Use OLD ema_w (pre-update) for cb_update
231
- cb_update = old_w / smoothed.unsqueeze(1)
232
- cb_update = torch.clamp(cb_update, -1e4, 1e4)
233
- codebook.copy_(cb_update)
234
-
235
- # Dead code restart.
236
- if (int(self.epoch_counter.item()) + 1) % self.dead_code_restart_every == 0:
237
- dead_mask = ema_cluster < self.dead_code_threshold
238
- n_dead = int(dead_mask.sum().item())
239
- if n_dead > 0:
240
- rand_idx = torch.randint(0, B, (n_dead,), device=z.device)
241
- replacement = z[rand_idx]
242
- codebook.data[dead_mask] = replacement.detach()
243
- ema_cluster[dead_mask] = 1.0
244
- ema_w[dead_mask] = replacement.detach()
245
- n_restarted = n_dead
246
- # v13.1: limpeza leve após dead code restart (libera tensores temporários).
247
- import gc as _gc
248
- _gc.collect(0)
249
-
250
- return z_q, codes, n_restarted
251
-
252
- def forward(self, z_e: torch.Tensor) -> Dict[str, torch.Tensor]:
253
- """
254
- z_e: (B, D) — saída contínua do encoder.
255
-
256
- Retorna dict com:
257
- z_q_top: (B, D) — vetores quantizados do nível top
258
- z_q_bot: (B, D) — vetores quantizados do nível bottom (residual)
259
- z_q_combined: (B, D) — z_q_top + z_q_bot (soma)
260
- codes_top: (B,)
261
- codes_bot: (B,)
262
- loss: escalar (commitment top + bottom)
263
- stats: dict com estatísticas
264
- """
265
- # v12.8: RMSNorm pré-top (preserva magnitude, mais estável que LayerNorm).
266
- z_e_norm = self.rms_pre_top(z_e)
267
-
268
- # Nível top: quantiza z_e (normalizado) diretamente.
269
- z_q_top, codes_top, n_restart_top = self._quantize_level(
270
- z_e_norm, self.codebook_top, self.ema_cluster_top, self.ema_w_top,
271
- use_goose=True,
272
- )
273
-
274
- # Nível bottom: quantiza o RESÍDUO z_e - z_q_top.detach() (com RMSNorm).
275
- residual = z_e - z_q_top.detach()
276
- residual_norm = self.rms_pre_bot(residual)
277
- z_q_bot, codes_bot, n_restart_bot = self._quantize_level(
278
- residual_norm, self.codebook_bot, self.ema_cluster_bot, self.ema_w_bot,
279
- use_goose=True,
280
- )
281
-
282
- # Commitment losses (computadas com z_e original, não normalizado).
283
- commit_top = F.mse_loss(z_e, z_q_top.detach())
284
- commit_bot = F.mse_loss(residual, z_q_bot.detach())
285
- loss = self.beta * (commit_top + commit_bot)
286
-
287
- # Straight-through.
288
- z_q_top_st = z_e + (z_q_top - z_e).detach()
289
- z_q_bot_st = residual + (z_q_bot - residual).detach()
290
- z_q_combined = z_q_top_st + z_q_bot_st
291
- # v12.8: RMSNorm pós-combinação (preserva magnitude, mais estável que LayerNorm).
292
- z_q_combined = self.rms_post_combine(z_q_combined)
293
-
294
- # Estatísticas.
295
- with torch.no_grad():
296
- self.usage_top.add_(torch.bincount(codes_top, minlength=self.num_codes_top).float())
297
- self.usage_bot.add_(torch.bincount(codes_bot, minlength=self.num_codes_bot).float())
298
- n_used_top = int((self.usage_top > 0).sum().item())
299
- n_used_bot = int((self.usage_bot > 0).sum().item())
300
-
301
- def _cb_ppl(codes, K):
302
- if len(codes) == 0:
303
- return 0.0
304
- from collections import Counter
305
- counter = Counter(codes.tolist())
306
- probs = np.array([c / len(codes) for c in counter.values()])
307
- entropy = -np.sum(probs * np.log(probs + 1e-12))
308
- return float(math.exp(entropy)) if entropy > 0 else 0.0
309
-
310
- cb_ppl_top = _cb_ppl(codes_top, self.num_codes_top)
311
- cb_ppl_bot = _cb_ppl(codes_bot, self.num_codes_bot)
312
-
313
- stats = {
314
- 'n_used_top': n_used_top,
315
- 'n_used_bot': n_used_bot,
316
- 'n_dead_top': self.num_codes_top - n_used_top,
317
- 'n_dead_bot': self.num_codes_bot - n_used_bot,
318
- 'usage_ratio_top': n_used_top / self.num_codes_top,
319
- 'usage_ratio_bot': n_used_bot / self.num_codes_bot,
320
- 'codebook_ppl_top': cb_ppl_top,
321
- 'codebook_ppl_bot': cb_ppl_bot,
322
- 'n_restarted_top': n_restart_top,
323
- 'n_restarted_bot': n_restart_bot,
324
- 'goose_temp': float(self.current_temp),
325
- }
326
-
327
- return {
328
- 'z_q_top': z_q_top_st,
329
- 'z_q_bot': z_q_bot_st,
330
- 'z_q_combined': z_q_combined,
331
- 'codes_top': codes_top,
332
- 'codes_bot': codes_bot,
333
- 'loss': loss,
334
- 'stats': stats,
335
- }
336
-
337
- def increment_epoch(self):
338
- self.epoch_counter += 1
339
- self.update_temperature()
340
-
341
- def reset_usage_stats(self):
342
- self.usage_top.zero_()
343
- self.usage_bot.zero_()
344
-
345
-
346
- class HierarchicalVQVAE2(nn.Module):
347
- """VQ-VAE-2 completo com encoder/decoder hierárquico para múltiplas modalidades.
348
-
349
- Arquitetura:
350
- x → [Encoder shared] → z_e
351
- → [Top VQ] → z_q_top (estrutura global)
352
- → [Bot VQ] → z_q_bot (detalhes residuais)
353
- → [Decoder shared] → x_recon
354
- """
355
-
356
- def __init__(
357
- self,
358
- modalities: Dict[str, int],
359
- code_dim: int = 32,
360
- num_codes_top: int = 512,
361
- num_codes_bot: int = 512,
362
- hidden: int = 128,
363
- beta: float = 0.25,
364
- ema_decay: float = 0.99,
365
- dead_code_threshold: float = 1.0,
366
- dead_code_restart_every: int = 3,
367
- goose_temp_init: float = 2.0,
368
- goose_temp_final: float = 0.5,
369
- goose_schedule: str = "cosine",
370
- total_epochs: int = 25,
371
- norm_type: str = "none", # v12.8: "none" | "weightnorm" | "rmsnorm"
372
- rmsnorm_in_vq: bool = False, # v12.8: RMSNorm dentro do VQ (default False)
373
- ):
374
- super().__init__()
375
- self.modality_names = list(modalities.keys())
376
- self.modality_dims = modalities
377
- self.code_dim = code_dim
378
- self.norm_type = norm_type # v12.8
379
-
380
- # v12.8: Encoders e decoders por modalidade.
381
- # - "none": LayerNorm interno dos MLPs (default, melhor validado em v12.7)
382
- # - "weightnorm": aplica weight_norm nos Linear (EnCodec-style)
383
- # - "rmsnorm": usa RMSNorm em vez de LayerNorm entre camadas MLP
384
- def make_encoder(dim, hidden, code_dim, norm_type):
385
- if norm_type == "weightnorm":
386
- layers = [
387
- nn.utils.weight_norm(nn.Linear(dim, hidden)), nn.GELU(),
388
- nn.utils.weight_norm(nn.Linear(hidden, hidden // 2)), nn.GELU(),
389
- nn.utils.weight_norm(nn.Linear(hidden // 2, code_dim)),
390
- ]
391
- elif norm_type == "rmsnorm":
392
- layers = [
393
- nn.Linear(dim, hidden), RMSNorm(hidden), nn.GELU(),
394
- nn.Linear(hidden, hidden // 2), RMSNorm(hidden // 2), nn.GELU(),
395
- nn.Linear(hidden // 2, code_dim),
396
- ]
397
- else: # "none" — usa LayerNorm interno (default, melhor validado)
398
- layers = [
399
- nn.Linear(dim, hidden), nn.LayerNorm(hidden), nn.GELU(),
400
- nn.Linear(hidden, hidden // 2), nn.LayerNorm(hidden // 2), nn.GELU(),
401
- nn.Linear(hidden // 2, code_dim),
402
- ]
403
- return nn.Sequential(*layers)
404
-
405
- def make_decoder(code_dim, hidden, dim, norm_type):
406
- if norm_type == "weightnorm":
407
- layers = [
408
- nn.utils.weight_norm(nn.Linear(code_dim, hidden // 2)), nn.GELU(),
409
- nn.utils.weight_norm(nn.Linear(hidden // 2, hidden)), nn.GELU(),
410
- nn.utils.weight_norm(nn.Linear(hidden, dim)),
411
- ]
412
- elif norm_type == "rmsnorm":
413
- layers = [
414
- nn.Linear(code_dim, hidden // 2), RMSNorm(hidden // 2), nn.GELU(),
415
- nn.Linear(hidden // 2, hidden), RMSNorm(hidden), nn.GELU(),
416
- nn.Linear(hidden, dim),
417
- ]
418
- else: # "none"
419
- layers = [
420
- nn.Linear(code_dim, hidden // 2), nn.LayerNorm(hidden // 2), nn.GELU(),
421
- nn.Linear(hidden // 2, hidden), nn.LayerNorm(hidden), nn.GELU(),
422
- nn.Linear(hidden, dim),
423
- ]
424
- return nn.Sequential(*layers)
425
-
426
- self.encoders = nn.ModuleDict({
427
- name: make_encoder(dim, hidden, code_dim, norm_type)
428
- for name, dim in modalities.items()
429
- })
430
- self.decoders = nn.ModuleDict({
431
- name: make_decoder(code_dim, hidden, dim, norm_type)
432
- for name, dim in modalities.items()
433
- })
434
-
435
- # VQ hierárquico compartilhado entre todas as modalidades.
436
- # v12.8: RMSNorm opcional dentro do VQ (substitui LayerNorm v12.7 que degradou).
437
- vq_norm_type = "rmsnorm" if (rmsnorm_in_vq and norm_type == "rmsnorm") else "none"
438
- self.vq = HierarchicalVectorQuantizer(
439
- num_codes_top=num_codes_top,
440
- num_codes_bot=num_codes_bot,
441
- code_dim=code_dim,
442
- beta=beta,
443
- ema_decay=ema_decay,
444
- dead_code_threshold=dead_code_threshold,
445
- dead_code_restart_every=dead_code_restart_every,
446
- goose_temp_init=goose_temp_init,
447
- goose_temp_final=goose_temp_final,
448
- goose_schedule=goose_schedule,
449
- total_epochs=total_epochs,
450
- norm_type=vq_norm_type,
451
- )
452
-
453
- def forward(self, batch: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
454
- """batch: {modality_name: (B, input_dim)}."""
455
- out = {'reconstructions': {}, 'codes_top': {}, 'codes_bot': {}, 'vq_loss': 0.0,
456
- 'recon_loss': 0.0, 'stats': {}}
457
- total_vq = 0.0
458
- total_recon = 0.0
459
- n = 0
460
- all_codes_top = []
461
- all_codes_bot = []
462
- vq_out_first_stats = None # Capture stats from first VQ forward
463
- for name, x in batch.items():
464
- if name not in self.encoders:
465
- continue
466
- # v13.3: Sanitização de NaN/Inf no input (resolve bug identificado no teste adversarial).
467
- x = torch.nan_to_num(x, nan=0.0, posinf=1e4, neginf=-1e4)
468
- # v12.8: forward limpo (sem LayerNorms extras v12.7 que degradaram resultados).
469
- # Encoder (com normalização interna configurável via norm_type).
470
- z_e = self.encoders[name](x)
471
- # v13.3: Sanitização após encoder (pode gerar NaN com inputs extremos).
472
- z_e = torch.nan_to_num(z_e, nan=0.0, posinf=1e4, neginf=-1e4)
473
- # VQ hierárquico (com RMSNorm opcional via rmsnorm_in_vq).
474
- vq_out = self.vq(z_e)
475
- # Capture stats from the first modality's VQ forward (no redundant second pass)
476
- if vq_out_first_stats is None:
477
- vq_out_first_stats = vq_out['stats']
478
- z_q = vq_out['z_q_combined']
479
- # v13.3: Sanitização após VQ (quantização pode produzir valores extremos).
480
- z_q = torch.nan_to_num(z_q, nan=0.0, posinf=1e4, neginf=-1e4)
481
- # Decoder (com normalização interna configurável via norm_type).
482
- x_recon = self.decoders[name](z_q)
483
- # v13.3: Sanitização final da reconstrução.
484
- x_recon = torch.nan_to_num(x_recon, nan=0.0, posinf=1e4, neginf=-1e4)
485
- recon_loss = F.mse_loss(x_recon, x)
486
-
487
- out['reconstructions'][name] = x_recon
488
- out['codes_top'][name] = vq_out['codes_top']
489
- out['codes_bot'][name] = vq_out['codes_bot']
490
- all_codes_top.append(vq_out['codes_top'])
491
- all_codes_bot.append(vq_out['codes_bot'])
492
- total_vq += vq_out['loss']
493
- total_recon += recon_loss
494
- n += 1
495
-
496
- out['vq_loss'] = total_vq / max(n, 1)
497
- out['recon_loss'] = total_recon / max(n, 1)
498
- out['total_loss'] = out['vq_loss'] + out['recon_loss']
499
- # Use stats already computed in the main loop — avoid redundant VQ forward
500
- # that would sample different random codes and give inconsistent stats.
501
- if vq_out_first_stats is not None:
502
- out['stats'] = vq_out_first_stats
503
- return out
504
-
505
- def cross_modal_translate(
506
- self, x: torch.Tensor, src: str, tgt: str
507
- ) -> torch.Tensor:
508
- """Traduz de uma modalidade para outra via codebook compartilhado."""
509
- # v12.8: forward limpo (sem LayerNorms extras v12.7).
510
- z_e = self.encoders[src](x)
511
- vq_out = self.vq(z_e)
512
- z_q = vq_out['z_q_combined']
513
- return self.decoders[tgt](z_q)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ]