PowerMachine commited on
Commit
d908f1e
·
verified ·
1 Parent(s): aa344bf

V6.5-V2-metrics-FIX: upload batch (scripts + modules) [7 files] — restored params + memory fixes

Browse files
kohonen_learning_system.py CHANGED
@@ -1964,6 +1964,11 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
1964
  self._adaptation_log: List[Dict[str, Any]] = []
1965
  self._last_adaptation_step = 0
1966
 
 
 
 
 
 
1967
  def _init_hypothesis_ensemble(self):
1968
  """Cria o ensemble de geradores + otimizador Adam."""
1969
  input_dim = self.som_neuron_count # ativação SOM flatten (864)
@@ -2125,6 +2130,10 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2125
  "punishment_rate": float(rate),
2126
  }
2127
  self._adaptation_log.append(record)
 
 
 
 
2128
  self._last_adaptation_step += 1
2129
  return record
2130
 
@@ -2132,6 +2141,91 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2132
  """Retorna o log de adaptações dinâmicas (para monitoramento)."""
2133
  return list(self._adaptation_log)
2134
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2135
  # ==================================================================
2136
  # 2.1 Treinamento dos geradores (train_hypotheses)
2137
  # ==================================================================
@@ -2181,6 +2275,11 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2181
 
2182
  self.hypothesis_ensemble.train()
2183
  losses = []
 
 
 
 
 
2184
  for step in range(self.hyp_train_steps):
2185
  self.hyp_optimizer.zero_grad()
2186
 
@@ -2191,21 +2290,56 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2191
  deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
2192
  deltas_stack = self.delta_scale * deltas_stack # escala
2193
 
2194
- # Avalia cada delta: computa loss de classificação
 
 
2195
  if self.classifier is not None and self.classifier_trained:
2196
- loss_per_hyp = []
2197
- for h in range(self.n_hypotheses):
2198
- delta_h = deltas_stack[0, h] # (P,)
2199
- som_temp = som_weights_flat + delta_h
2200
- som_temp_reshaped = som_temp.view_as(self.som.weights)
2201
- loss_h = self._compute_classification_loss_with_som(
2202
- som_temp_reshaped, data, labels
2203
- )
2204
- loss_per_hyp.append(loss_h)
2205
- loss_total = torch.stack(loss_per_hyp).mean()
2206
- # Regularização L2 sobre os deltas (evita deltas grandes)
2207
- reg_loss = 0.01 * deltas_stack.norm()
2208
- loss_total = loss_total + reg_loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2209
  else:
2210
  # Fallback: minimizar norma do delta (regulariza��ão pura)
2211
  loss_total = deltas_stack.norm()
@@ -2214,6 +2348,12 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2214
  self.hyp_optimizer.step()
2215
  losses.append(float(loss_total.item()))
2216
 
 
 
 
 
 
 
2217
  # Atualiza a escala de delta (decai suavemente)
2218
  with torch.no_grad():
2219
  self.delta_scale.data = torch.clamp(
@@ -2221,6 +2361,10 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2221
  )
2222
 
2223
  self.hypothesis_ensemble.eval()
 
 
 
 
2224
 
2225
  # V6.5-V2-dynamic — registra loss_final no histórico
2226
  loss_final_val = float(losses[-1]) if losses else 0.0
@@ -2249,56 +2393,80 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2249
  def _compute_classification_loss_with_som(
2250
  self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2251
  ) -> torch.Tensor:
2252
- """Computa a perda de classificação usando um SOM temporário.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2253
 
2254
- Substitui temporariamente self.som.weights pelo som_weights fornecido,
2255
- computa a loss, e restaura o SOM original. Usa detach no SOM temporário
2256
- para evitar retropropagação através dos pesos do SOM (apenas os deltas
2257
- dos geradores recebem gradiente).
2258
- """
2259
- # Guarda o SOM original
2260
- original_weights = self.som.weights
2261
- # Substitui temporariamente (sem clone — som_weights já é um tensor
2262
- # que carrega gradiente dos deltas)
2263
- self.som.weights = som_weights
2264
 
2265
- try:
2266
- if self.classifier is not None and self.classifier_trained:
2267
- # Computa ativações SOM (sem treinar SOM/embedding)
2268
- with torch.no_grad():
2269
- # Para permitir gradiente fluir pelos deltas, precisamos
2270
- # das ativações SOM como função diferenciável de som_weights
2271
- # Reimplementamos _som_activation com gradiente
2272
- data_expanded = data.unsqueeze(0).unsqueeze(0).unsqueeze(0).unsqueeze(0)
2273
- # (1, 1, 1, 1, N, 4) — para broadcast com (I, J, K, L, 4)
2274
- # Mas self.som.weights é (I, J, K, L, 4) e data é (N, 4)
2275
- # dist[i,j,k,l,n] = ||W[i,j,k,l] - data[n]||²
2276
- # reshape: W (I*J*K*L, 4), data (N, 4) → dist (I*J*K*L, N)
2277
- W_flat = som_weights.reshape(-1, 4) # (P_neurons, 4)
2278
- # W_flat.requires_grad_(True) # já tem gradiente dos deltas
2279
- dist = torch.cdist(W_flat, data) # (P_neurons, N)
2280
- activations = dist.t() # (N, P_neurons)
2281
- # Passa pelo classifier
2282
- logits = self.classifier(activations) # (N,)
2283
- loss = F.binary_cross_entropy_with_logits(logits, labels)
2284
- return loss
2285
- else:
2286
- # Fallback: votação BMU (não-diferenciável — usa heurística)
2287
- with torch.no_grad():
2288
- correct = 0
2289
- for i in range(len(data)):
2290
- bmu = self.som.find_bmu(data[i])
2291
- pred = self.neuron_label.get(bmu, 0.5) if hasattr(self, "neuron_label") else 0.5
2292
- if (pred > 0.5) == (labels[i].item() > 0.5):
2293
- correct += 1
2294
- return torch.tensor(
2295
- 1.0 - correct / max(1, len(data)),
2296
- device=data.device,
2297
- dtype=torch.float,
2298
- )
2299
- finally:
2300
- # Restaura o SOM original
2301
- self.som.weights = original_weights
 
 
 
 
 
 
 
 
 
 
2302
 
2303
  # ==================================================================
2304
  # 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses)
@@ -2398,32 +2566,47 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2398
  def _evaluate_accuracy_with_som(
2399
  self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2400
  ) -> float:
2401
- """Calcula acurácia com um SOM temporário (sem modificar o original)."""
2402
- original_weights = self.som.weights
2403
- self.som.weights = som_weights
2404
- try:
2405
- if self.classifier is not None and self.classifier_trained:
2406
- with torch.no_grad():
2407
- activations = torch.stack(
2408
- [self._som_activation(data[i]) for i in range(len(data))]
2409
- )
2410
- logits = self.classifier(activations)
2411
- preds = (logits > 0.5).float()
2412
- acc = (preds == labels).float().mean().item()
2413
- else:
2414
- # Votação BMU
2415
- if not hasattr(self, "neuron_label"):
2416
- self._label_neurons()
2417
- correct = 0
2418
- for i in range(len(data)):
2419
- bmu = self.som.find_bmu(data[i])
2420
- pred = self.neuron_label.get(bmu, 0.5)
2421
- if (pred > 0.5) == (labels[i].item() > 0.5):
2422
- correct += 1
2423
- acc = correct / max(1, len(data))
2424
  return float(acc)
2425
- finally:
2426
- self.som.weights = original_weights
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2427
 
2428
  # ==================================================================
2429
  # 2.3 Aplicação do melhor delta + consolidação via EWC
@@ -2653,6 +2836,69 @@ class KohonenLearningSystemV2(KohonenLearningSystem):
2653
  },
2654
  }
2655
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2656
 
2657
  __all__ = [
2658
  "SimpleBBPETokenizer",
@@ -2665,3 +2911,5 @@ __all__ = [
2665
  "HypothesisEnsemble",
2666
  "KohonenLearningSystemV2",
2667
  ]
 
 
 
1964
  self._adaptation_log: List[Dict[str, Any]] = []
1965
  self._last_adaptation_step = 0
1966
 
1967
+ # V6.5-V2-metrics — histórico de métricas SOM (QE, TE, KL, VE, Dead Neurons)
1968
+ # para detecção de Estagnação do QE e Cruzamento de Vizinhança.
1969
+ from bigru_t.model.som_metrics import SOMMetricHistory
1970
+ self._som_metric_history: SOMMetricHistory = SOMMetricHistory()
1971
+
1972
  def _init_hypothesis_ensemble(self):
1973
  """Cria o ensemble de geradores + otimizador Adam."""
1974
  input_dim = self.som_neuron_count # ativação SOM flatten (864)
 
2130
  "punishment_rate": float(rate),
2131
  }
2132
  self._adaptation_log.append(record)
2133
+ # V6.5-V2-metrics-FIX: limita crescimento do log (era ilimitado, causando
2134
+ # crescimento de memória em treino longo). Mantém últimas 50 entradas.
2135
+ if len(self._adaptation_log) > 50:
2136
+ self._adaptation_log = self._adaptation_log[-50:]
2137
  self._last_adaptation_step += 1
2138
  return record
2139
 
 
2141
  """Retorna o log de adaptações dinâmicas (para monitoramento)."""
2142
  return list(self._adaptation_log)
2143
 
2144
+ # ==================================================================
2145
+ # V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM
2146
+ # ==================================================================
2147
+ # User requirement: "analisar e aprimorar (logicamente e matematicamente)
2148
+ # no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
2149
+ #
2150
+ # Mathematical analysis:
2151
+ # A memória RAM consumida pelo KLS V2 tem 4 fontes principais:
2152
+ # 1. Pesos do SOM: I*J*K*L*4 floats = 864*4 = 3456 floats (~14KB)
2153
+ # → FIXO (não pode ser limpo sem destruir o modelo)
2154
+ # 2. HypothesisEnsemble: max_n_hypotheses * DeltaGenerator params
2155
+ # = 32 * (864*256 + 256*256 + 256*3456) = 32 * 1.16M = ~37M params
2156
+ # → ~148MB em float32. Os OTIMIZADORAS Adam guardam 2x esse valor
2157
+ # (momentum + variance) = ~296MB adicionais.
2158
+ # 3. HypothesisClassifier: 864*512 + 512*256 + ... = ~616K params (~2.5MB)
2159
+ # 4. buffer_4d: até MAX_BUFFER_SIZE amostras * 4 floats = ~4KB (mínimo)
2160
+ # MAS os tensores carregam gradiente do embedding se não forem detached.
2161
+ #
2162
+ # A limpeza agressiva atua em:
2163
+ # - Zera gradientes acumulados nos otimizadores (Adam state)
2164
+ # - Chama gc.collect() para liberar ciclos Python
2165
+ # - Libera caches internos do PyTorch (oneDNN primitive cache)
2166
+ # - Trunca listas de histórico que crescem com o treino
2167
+ # ------------------------------------------------------------------
2168
+ def aggressive_cleanup(self) -> Dict[str, Any]:
2169
+ """V6.5-V2-metrics-FIX — Limpeza agressiva de memória RAM do KLS.
2170
+
2171
+ Retorna métricas antes/depois para confirmação. NÃO destroi pesos do
2172
+ SOM nem do classifier — apenas libera caches e gradientes acumulados.
2173
+
2174
+ Returns:
2175
+ Dict com rss_before/after_mb, n_refs_collected, buffers_trimmed.
2176
+ """
2177
+ import gc
2178
+ try:
2179
+ import resource
2180
+ rss_before = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
2181
+ except Exception:
2182
+ rss_before = 0.0
2183
+
2184
+ # 1. Zera gradientes do otimizador Adam (libera momentum/variance state)
2185
+ try:
2186
+ if hasattr(self, "hyp_optimizer"):
2187
+ self.hyp_optimizer.zero_grad(set_to_none=True)
2188
+ except Exception:
2189
+ pass
2190
+
2191
+ # 2. Trunca históricos que crescem com o treino
2192
+ if len(self._adaptation_log) > 20:
2193
+ self._adaptation_log = self._adaptation_log[-20:]
2194
+ if len(self.inference_punishment_history) > 50:
2195
+ self.inference_punishment_history = self.inference_punishment_history[-50:]
2196
+ if len(self.vqvae2_metrics_history) > 30:
2197
+ self.vqvae2_metrics_history = self.vqvae2_metrics_history[-30:]
2198
+ if len(self.w8a8_metrics_history) > 30:
2199
+ self.w8a8_metrics_history = self.w8a8_metrics_history[-30:]
2200
+
2201
+ # 3. gc.collect() em 2 passes (gerações 0+1 e 2)
2202
+ n_collected_0 = gc.collect(0)
2203
+ n_collected_1 = gc.collect(1)
2204
+ n_collected_2 = gc.collect(2)
2205
+
2206
+ # 4. Libera caches CPU do PyTorch (oneDNN/MKL)
2207
+ try:
2208
+ import torch
2209
+ if hasattr(torch, "cuda") and torch.cuda.is_available():
2210
+ torch.cuda.empty_cache()
2211
+ except Exception:
2212
+ pass
2213
+
2214
+ try:
2215
+ import resource
2216
+ rss_after = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) / 1024.0
2217
+ except Exception:
2218
+ rss_after = 0.0
2219
+
2220
+ return {
2221
+ "rss_before_mb": float(rss_before),
2222
+ "rss_after_mb": float(rss_after),
2223
+ "freed_mb": max(0.0, rss_before - rss_after),
2224
+ "gc_collected": int(n_collected_0 + n_collected_1 + n_collected_2),
2225
+ "adaptation_log_trimmed": int(len(self._adaptation_log)),
2226
+ "timestamp": __import__("datetime").datetime.utcnow().isoformat() + "Z",
2227
+ }
2228
+
2229
  # ==================================================================
2230
  # 2.1 Treinamento dos geradores (train_hypotheses)
2231
  # ==================================================================
 
2275
 
2276
  self.hypothesis_ensemble.train()
2277
  losses = []
2278
+ # V6.5-V2-metrics-FIX: pré-computa data_norm_sq uma única vez (N, 1)
2279
+ # para reuso em todos os steps — evita recomputação redundante.
2280
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2281
+ labels_expanded = labels # (N,)
2282
+
2283
  for step in range(self.hyp_train_steps):
2284
  self.hyp_optimizer.zero_grad()
2285
 
 
2290
  deltas_stack = self.hypothesis_ensemble.forward_stacked(x_mean)
2291
  deltas_stack = self.delta_scale * deltas_stack # escala
2292
 
2293
+ # V6.5-V2-metrics-FIX: Avaliação VETORIZADA de todas as hipóteses
2294
+ # em paralelo (substitui loop que materializava 16 cópias do SOM).
2295
+ # Memória: (n_hyp, P, N) em vez de 16 * (P + 2*P*N).
2296
  if self.classifier is not None and self.classifier_trained:
2297
+ # deltas_stack: (1, n_hyp, P) → (n_hyp, P, 4) reshape
2298
+ n_hyp = self.n_hypotheses
2299
+ P_neurons = self.som_neuron_count
2300
+ # som_weights_flat: (P, 4) detached
2301
+ W_base = som_weights_flat.reshape(P_neurons, 4) # (P, 4)
2302
+ # delta_h: (n_hyp, P) → reshape para (n_hyp, P, 4)
2303
+ deltas_3d = deltas_stack[0].reshape(n_hyp, P_neurons, 4) # (n_hyp, P, 4)
2304
+ # W_new[h, p, d] = W_base[p, d] + deltas_3d[h, p, d]
2305
+ W_new = W_base.unsqueeze(0) + deltas_3d # (n_hyp, P, 4) — broadcast
2306
+ # dist²[h, p, n] = ||W_new[h, p] - data[n]||²
2307
+ # = ||W_new[h, p]||² + ||data[n]||² - 2*W_new[h, p]·data[n]
2308
+ W_new_norm_sq = (W_new * W_new).sum(dim=-1) # (n_hyp, P)
2309
+ # cross[h, p, n] = W_new[h, p] · data[n]
2310
+ cross = torch.matmul(W_new, data.t()) # (n_hyp, P, N)
2311
+ dist_sq = (
2312
+ W_new_norm_sq.unsqueeze(-1) # (n_hyp, P, 1)
2313
+ + data_norm_sq # (1, N) → broadcast (n_hyp, P, N)
2314
+ - 2.0 * cross
2315
+ ) # (n_hyp, P, N)
2316
+ dist_sq = torch.clamp(dist_sq, min=0.0)
2317
+ # activations[h, n, p] = dist_sq[h, p, n]
2318
+ activations = dist_sq.transpose(1, 2) # (n_hyp, N, P)
2319
+ # Classifier forward (congelado)
2320
+ classifier_params_were_grad = [
2321
+ p.requires_grad for p in self.classifier.parameters()
2322
+ ]
2323
+ for p in self.classifier.parameters():
2324
+ p.requires_grad_(False)
2325
+ try:
2326
+ logits = self.classifier(
2327
+ activations.reshape(n_hyp * len(data), P_neurons)
2328
+ ).reshape(n_hyp, len(data)) # (n_hyp, N)
2329
+ # BCE por hipótese, depois média
2330
+ loss_per_hyp = F.binary_cross_entropy_with_logits(
2331
+ logits,
2332
+ labels_expanded.unsqueeze(0).expand(n_hyp, -1),
2333
+ reduction='none',
2334
+ ).mean(dim=1) # (n_hyp,)
2335
+ loss_total = loss_per_hyp.mean()
2336
+ # Regularização L2 sobre os deltas
2337
+ reg_loss = 0.01 * deltas_stack.norm()
2338
+ loss_total = loss_total + reg_loss
2339
+ finally:
2340
+ for p, was_grad in zip(self.classifier.parameters(),
2341
+ classifier_params_were_grad):
2342
+ p.requires_grad_(was_grad)
2343
  else:
2344
  # Fallback: minimizar norma do delta (regulariza��ão pura)
2345
  loss_total = deltas_stack.norm()
 
2348
  self.hyp_optimizer.step()
2349
  losses.append(float(loss_total.item()))
2350
 
2351
+ # V6.5-V2-metrics-FIX: libera tensores intermediários explicitamente
2352
+ # para reduzir pico de memória entre steps.
2353
+ del loss_total, deltas_stack
2354
+ if 'W_new' in dir():
2355
+ del W_new, cross, dist_sq, activations
2356
+
2357
  # Atualiza a escala de delta (decai suavemente)
2358
  with torch.no_grad():
2359
  self.delta_scale.data = torch.clamp(
 
2361
  )
2362
 
2363
  self.hypothesis_ensemble.eval()
2364
+ # V6.5-V2-metrics-FIX: libera som_activations e som_weights_flat
2365
+ del som_activations, som_weights_flat, data, data_norm_sq
2366
+ import gc
2367
+ gc.collect()
2368
 
2369
  # V6.5-V2-dynamic — registra loss_final no histórico
2370
  loss_final_val = float(losses[-1]) if losses else 0.0
 
2393
  def _compute_classification_loss_with_som(
2394
  self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2395
  ) -> torch.Tensor:
2396
+ """V6.5-V2-metrics-FIX — Computa perda de classificação com SOM temporário.
2397
+
2398
+ BUG CORRIGIDO (V6.5-V2-metrics-FIX):
2399
+ A versão anterior envolvia toda a computação em `torch.no_grad()`,
2400
+ o que matava o gradiente que deveria fluir dos deltas → SOM → loss.
2401
+ Resultado: train_hypotheses() não treinava os geradores de fato.
2402
+
2403
+ CORREÇÃO: A computação das ativações SOM (squared distances) é
2404
+ feita SEM no_grad, permitindo que o gradiente flua dos deltas
2405
+ (que pertencem ao HypothesisEnsemble) através do SOM temporário
2406
+ até a loss. Apenas o classifier é congelado (requires_grad=False)
2407
+ para que seu gradiente não seja atualizado por train_hypotheses.
2408
+
2409
+ Mathematical formulation (squared Euclidean distance, consistente com
2410
+ _som_activation que usa ||W - x||^2, não ||W - x||):
2411
+ Para cada neurônio p e amostra n:
2412
+ dist²[p, n] = ||W[p] - x[n]||²
2413
+ = ||W[p]||² + ||x[n]||² - 2·W[p]·x[n]
2414
+ activations = dist².t() → (N, P_neurons)
2415
+ logits = classifier(activations) → (N,)
2416
+ loss = BCEWithLogitsLoss(logits, labels)
2417
 
2418
+ Args:
2419
+ som_weights: tensor (I, J, K, L, 4) com gradiente (dos deltas).
2420
+ data: tensor (N, 4) — amostras 4D (detached).
2421
+ labels: tensor (N,) — rótulos binários.
 
 
 
 
 
 
2422
 
2423
+ Returns:
2424
+ Scalar loss tensor (com gradiente fluindo para som_weights).
2425
+ """
2426
+ if self.classifier is not None and self.classifier_trained:
2427
+ # NÃO usa torch.no_grad() — gradiente deve fluir dos deltas
2428
+ # através do SOM temporário até a loss.
2429
+ W_flat = som_weights.reshape(-1, 4) # (P_neurons, 4)
2430
+ # Identidade algébrica: ||W - x||^2 = ||W||^2 + ||x||^2 - 2*W·x
2431
+ # Evita materializar tensor (P, N, 4) — usa (P, N) direto.
2432
+ W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
2433
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2434
+ cross = W_flat @ data.t() # (P, N) — matmul eficiente
2435
+ dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
2436
+ dist_sq = torch.clamp(dist_sq, min=0.0) # estabilidade numérica
2437
+ activations = dist_sq.t() # (N, P_neurons) — gradiente fluindo!
2438
+ # Congela classifier durante train_hypotheses (não queremos
2439
+ # atualizar seus pesos — apenas os deltas dos geradores)
2440
+ classifier_params_were_grad = [
2441
+ p.requires_grad for p in self.classifier.parameters()
2442
+ ]
2443
+ for p in self.classifier.parameters():
2444
+ p.requires_grad_(False)
2445
+ try:
2446
+ logits = self.classifier(activations) # (N,)
2447
+ loss = F.binary_cross_entropy_with_logits(logits, labels)
2448
+ finally:
2449
+ # Restaura requires_grad do classifier
2450
+ for p, was_grad in zip(self.classifier.parameters(),
2451
+ classifier_params_were_grad):
2452
+ p.requires_grad_(was_grad)
2453
+ return loss
2454
+ else:
2455
+ # Fallback: votação BMU (não-diferenciável)
2456
+ with torch.no_grad():
2457
+ if not hasattr(self, "neuron_label"):
2458
+ self._label_neurons()
2459
+ correct = 0
2460
+ for i in range(len(data)):
2461
+ bmu = self.som.find_bmu(data[i])
2462
+ pred = self.neuron_label.get(bmu, 0.5)
2463
+ if (pred > 0.5) == (labels[i].item() > 0.5):
2464
+ correct += 1
2465
+ return torch.tensor(
2466
+ 1.0 - correct / max(1, len(data)),
2467
+ device=data.device,
2468
+ dtype=torch.float,
2469
+ )
2470
 
2471
  # ==================================================================
2472
  # 2.2 Seleção do melhor delta (3 tentativas × 16 hipóteses)
 
2566
  def _evaluate_accuracy_with_som(
2567
  self, som_weights: torch.Tensor, data: torch.Tensor, labels: torch.Tensor
2568
  ) -> float:
2569
+ """V6.5-V2-metrics-FIX — Calcula acurácia com SOM temporário (vetorizado).
2570
+
2571
+ Substitui o loop que chamava _som_activation(data[i]) para cada amostra
2572
+ por uma única operação matmul. Reduz tempo de O(N*P) para O(P*N) mas
2573
+ com paralelismo SIMD (single matmul vs N python iterations).
2574
+
2575
+ Para o fallback (votação BMU), ainda precisa trocar self.som.weights
2576
+ temporariamente pois find_bmu lê self.som.weights diretamente.
2577
+ """
2578
+ if self.classifier is not None and self.classifier_trained:
2579
+ with torch.no_grad():
2580
+ # Vetorizado: dist²[p, n] = ||W[p]||² + ||x[n]||² - 2*W[p]·x[n]
2581
+ W_flat = som_weights.reshape(-1, 4) # (P, 4)
2582
+ W_norm_sq = (W_flat * W_flat).sum(dim=-1, keepdim=True) # (P, 1)
2583
+ data_norm_sq = (data * data).sum(dim=-1, keepdim=True).t() # (1, N)
2584
+ cross = W_flat @ data.t() # (P, N)
2585
+ dist_sq = W_norm_sq + data_norm_sq - 2.0 * cross # (P, N)
2586
+ dist_sq = torch.clamp(dist_sq, min=0.0)
2587
+ activations = dist_sq.t() # (N, P)
2588
+ logits = self.classifier(activations) # (N,)
2589
+ preds = (logits > 0.5).float()
2590
+ acc = (preds == labels).float().mean().item()
 
2591
  return float(acc)
2592
+ else:
2593
+ # Fallback: votação BMU — precisa trocar self.som.weights
2594
+ original_weights = self.som.weights
2595
+ self.som.weights = som_weights
2596
+ try:
2597
+ with torch.no_grad():
2598
+ if not hasattr(self, "neuron_label"):
2599
+ self._label_neurons()
2600
+ correct = 0
2601
+ for i in range(len(data)):
2602
+ bmu = self.som.find_bmu(data[i])
2603
+ pred = self.neuron_label.get(bmu, 0.5)
2604
+ if (pred > 0.5) == (labels[i].item() > 0.5):
2605
+ correct += 1
2606
+ acc = correct / max(1, len(data))
2607
+ return float(acc)
2608
+ finally:
2609
+ self.som.weights = original_weights
2610
 
2611
  # ==================================================================
2612
  # 2.3 Aplicação do melhor delta + consolidação via EWC
 
2836
  },
2837
  }
2838
 
2839
+ # ==================================================================
2840
+ # V6.5-V2-metrics — Integração das métricas SOM canônicas
2841
+ # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
2842
+ # Colapso Topológico, Estagnação QE, Cruzamento Vizinhança)
2843
+ # ==================================================================
2844
+ def compute_som_metrics(self) -> Dict[str, Any]:
2845
+ """V6.5-V2-metrics — Computa todas as métricas SOM canônicas.
2846
+
2847
+ User requirement: "ANALISAR matematicamente e logicamente e inserir
2848
+ melhorias para os scripts das métricas de aprendizado e indicadores
2849
+ de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen)
2850
+ avaliam a fidelidade de representação dos dados e a preservação da
2851
+ vizinhança topológica".
2852
+
2853
+ Returns:
2854
+ Dict com 8 métricas:
2855
+ 1. quantization_error (QE)
2856
+ 2. topological_error (TE)
2857
+ 3. kaski_lagus_error (KL)
2858
+ 4. explained_variance_share (VE)
2859
+ 5. topological_collapse (dict)
2860
+ 6. dead_neuron_rate (dict)
2861
+ 7. qe_stagnation (dict) — requer histórico
2862
+ 8. neighborhood_crossing (dict) — requer histórico
2863
+ Plus: overall_health, failure_indicators
2864
+ """
2865
+ from bigru_t.model.som_metrics import compute_all_metrics
2866
+ if not self.buffer_4d:
2867
+ return {
2868
+ "quantization_error": 0.0,
2869
+ "topological_error": 0.0,
2870
+ "kaski_lagus_error": 0.0,
2871
+ "explained_variance_share": 0.0,
2872
+ "topological_collapse": {"severity": "none", "effective_rank": 0.0},
2873
+ "dead_neuron_rate": {"dead_neuron_rate": 1.0, "n_dead": self.som_neuron_count,
2874
+ "n_active": 0, "n_total": self.som_neuron_count},
2875
+ "qe_stagnation": {"is_stagnant": False, "severity": "none"},
2876
+ "neighborhood_crossing": {"detected": False, "severity": "none"},
2877
+ "overall_health": "unknown",
2878
+ "failure_indicators": ["no_buffer_data"],
2879
+ "n_failure_indicators": 1,
2880
+ "note": "buffer_4d vazio — métricas indisponíveis",
2881
+ }
2882
+ # Stack buffer em tensor (N, 4)
2883
+ data = torch.stack([
2884
+ v.detach().clone() if isinstance(v, torch.Tensor) else torch.tensor(v, dtype=torch.float)
2885
+ for v in self.buffer_4d
2886
+ ]).float()
2887
+ weights = self.som.weights.detach()
2888
+ metrics = compute_all_metrics(data, weights, self._som_metric_history)
2889
+ # Registra no histórico
2890
+ self._som_metric_history.record(metrics)
2891
+ return metrics
2892
+
2893
+ def get_som_metric_history(self) -> Dict[str, Any]:
2894
+ """Retorna histórico de métricas SOM (para análise temporal)."""
2895
+ return self._som_metric_history.to_dict()
2896
+
2897
+ def reset_som_metric_history(self) -> None:
2898
+ """Reseta o histórico de métricas SOM (início de nova fase)."""
2899
+ from bigru_t.model.som_metrics import SOMMetricHistory
2900
+ self._som_metric_history = SOMMetricHistory()
2901
+
2902
 
2903
  __all__ = [
2904
  "SimpleBBPETokenizer",
 
2911
  "HypothesisEnsemble",
2912
  "KohonenLearningSystemV2",
2913
  ]
2914
+ # V6.5-V2-metrics: som_metrics é importado lazy (dentro de compute_som_metrics)
2915
+ # para evitar dependência circular e permitir uso standalone.
smoke_test_v65_v2_metrics_fix.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """smoke_test_v65_v2_metrics_fix.py — Validação rápida das correções V6.5-V2-metrics-FIX.
2
+
3
+ Verifica:
4
+ 1. Importação do KohonenLearningSystemV2 com params restaurados (HIDDEN=1024, VOCAB=16384)
5
+ 2. Bug fix do _compute_classification_loss_with_som (gradiente flui)
6
+ 3. Vetorização do train_hypotheses (sem OOM)
7
+ 4. aggressive_cleanup() funciona
8
+ 5. SOM metrics (8 métricas) computam sem erro
9
+ 6. predict() retorna labels dinâmicos (não hardcoded)
10
+ 7. FASE1 + FASE2 logic (enable_punishment=False/True)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import sys
16
+ import time
17
+ import traceback
18
+ from pathlib import Path
19
+
20
+ # Setup paths
21
+ PROJECT_ROOT = Path("/home/z/my-project")
22
+ BIGRU_ROOT = PROJECT_ROOT / "BiGRU_T_version"
23
+ SRC_ROOT = BIGRU_ROOT / "src"
24
+ sys.path.insert(0, str(SRC_ROOT))
25
+
26
+ # Activate xeon_runtime
27
+ os.environ["V65_ENABLE_STREAMING"] = "1"
28
+
29
+ print("=" * 80)
30
+ print("V6.5-V2-metrics-FIX SMOKE TEST")
31
+ print("=" * 80)
32
+ print()
33
+
34
+ # Test 1: Import
35
+ print("[Test 1] Import KohonenLearningSystemV2...")
36
+ try:
37
+ from bigru_t.model.kohonen_learning_system import (
38
+ KohonenLearningSystemV2,
39
+ KohonenLearningSystem,
40
+ KohonenSOM4D,
41
+ HypothesisClassifier,
42
+ HypothesisEnsemble,
43
+ DeltaGenerator,
44
+ SimpleBBPETokenizer,
45
+ text_to_4d_vector,
46
+ )
47
+ from bigru_t.model.som_metrics import (
48
+ SOMMetricHistory,
49
+ compute_all_metrics,
50
+ quantization_error,
51
+ topological_error,
52
+ kaski_lagus_error,
53
+ explained_variance_share,
54
+ topological_collapse,
55
+ dead_neuron_rate,
56
+ )
57
+ print(" PASS: All imports successful")
58
+ except Exception as e:
59
+ print(f" FAIL: Import error: {e}")
60
+ traceback.print_exc()
61
+ sys.exit(1)
62
+
63
+ # Test 2: Instantiate with restored (larger) parameters
64
+ print("\n[Test 2] Instantiate KLS V2 with restored params (HIDDEN=1024, VOCAB=16384)...")
65
+ try:
66
+ # Use smaller grid for smoke test (faster), but full HIDDEN_DIM/VOCAB_SIZE
67
+ kls = KohonenLearningSystemV2(
68
+ vocab_size=16384, # restored
69
+ hidden_dim=1024, # restored
70
+ seq_len=8,
71
+ som_grid=(6, 6, 6, 4), # 864 neurons (Kohonen architecture preserved)
72
+ alpha0=0.1,
73
+ sigma0=1.5,
74
+ lambda_ewc=0.02,
75
+ N_start=10,
76
+ dim_choice="y",
77
+ hypothesis_hidden=[512, 256, 128, 64, 32, 16, 8],
78
+ T_max=10000,
79
+ enable_vqvae2=False, # disabled to save RAM
80
+ enable_reasoning=False, # disabled to save RAM
81
+ enable_w8a8=False, # disabled to save RAM
82
+ enable_attention=True,
83
+ attention_n_heads=8,
84
+ # V2-dynamic restored params
85
+ n_hypotheses=16, # restored
86
+ max_n_hypotheses=32, # restored
87
+ min_n_hypotheses=4,
88
+ n_trials=3,
89
+ min_n_trials=1,
90
+ max_n_trials=6,
91
+ hyp_train_steps=30, # restored
92
+ min_hyp_train_steps=10,
93
+ max_hyp_train_steps=80,
94
+ hyp_lr=1e-4,
95
+ hyp_hidden_dim=256, # restored
96
+ loss_history_window=8,
97
+ punishment_window=12,
98
+ )
99
+ assert kls.hidden_dim == 1024, f"hidden_dim={kls.hidden_dim}"
100
+ assert kls.tokenizer.vocab_size == 16384, f"vocab={kls.tokenizer.vocab_size}"
101
+ assert kls.n_hypotheses == 16, f"n_hyp={kls.n_hypotheses}"
102
+ assert kls.max_n_hypotheses == 32, f"max_n_hyp={kls.max_n_hypotheses}"
103
+ assert kls.hyp_train_steps == 30, f"hyp_steps={kls.hyp_train_steps}"
104
+ assert kls.hyp_hidden_dim == 256, f"hyp_hidden={kls.hyp_hidden_dim}"
105
+ assert hasattr(kls, "aggressive_cleanup"), "no aggressive_cleanup method"
106
+ print(f" PASS: KLS V2 instantiated")
107
+ print(f" hidden_dim={kls.hidden_dim}, vocab={kls.tokenizer.vocab_size}")
108
+ print(f" n_hypotheses={kls.n_hypotheses}/{kls.max_n_hypotheses}")
109
+ print(f" hyp_train_steps={kls.hyp_train_steps}, hyp_hidden_dim={kls.hyp_hidden_dim}")
110
+ print(f" som_grid={kls.som_grid} ({kls.som_neuron_count} neurons)")
111
+ print(f" has aggressive_cleanup: True")
112
+ except Exception as e:
113
+ print(f" FAIL: Instantiation error: {e}")
114
+ traceback.print_exc()
115
+ sys.exit(1)
116
+
117
+ # Test 3: Add data + train SOM (CONHECIMENTO phase)
118
+ print("\n[Test 3] Add data + train SOM (CONHECIMENTO phase)...")
119
+ try:
120
+ kls.tokenizer.fit([
121
+ "o gato dorme", "a casa eh azul", "ele corre rapido",
122
+ "ela canta uma musica", "o sol nasceu", "nos vamos viajar",
123
+ "luva de pedreiro tavila", "lula reserva valor",
124
+ "amazonas forca tarefa vitimas",
125
+ ])
126
+ # Add 30 samples (more than N_start=10)
127
+ for i in range(3):
128
+ kls.add_data(
129
+ ["o gato dorme", "o cachorro corre", "a menina brinca",
130
+ "o passaro voa", "o peixe nada", "a arvore cresce",
131
+ "o livro esta na mesa", "a musica toca", "o tempo passa",
132
+ "o mundo gira"],
133
+ [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
134
+ label_strings={0: "negative_class", 1: "positive_class"},
135
+ dataset_name="test_ds",
136
+ )
137
+ assert len(kls.buffer_4d) >= 10, f"buffer={len(kls.buffer_4d)}"
138
+ print(f" PASS: Added {len(kls.buffer_4d)} samples")
139
+ # Train SOM
140
+ started = kls.check_training_start()
141
+ print(f" training_ready: {kls.training_ready}")
142
+ if kls.training_ready:
143
+ kls.train_som_on_buffer()
144
+ kls._label_neurons()
145
+ print(f" SOM trained, weights norm={kls.som.weights.norm().item():.4f}")
146
+ except Exception as e:
147
+ print(f" FAIL: {e}")
148
+ traceback.print_exc()
149
+ sys.exit(1)
150
+
151
+ # Test 4: predict() returns dynamic labels (not gato/cachorro)
152
+ print("\n[Test 4] predict() returns dynamic labels...")
153
+ try:
154
+ pred = kls.predict("o gato dorme", dataset_name="test_ds")
155
+ assert pred in ["negative_class", "positive_class"], f"pred={pred}"
156
+ assert pred != "gato" and pred != "cachorro", f"hardcoded: {pred}"
157
+ print(f" PASS: predict returned '{pred}' (dynamic, not hardcoded)")
158
+ except Exception as e:
159
+ print(f" FAIL: {e}")
160
+ traceback.print_exc()
161
+ sys.exit(1)
162
+
163
+ # Test 5: activate_hypothesis (trains classifier)
164
+ print("\n[Test 5] activate_hypothesis (train classifier)...")
165
+ try:
166
+ kls.activate_hypothesis()
167
+ assert kls.classifier_trained, "classifier not trained"
168
+ print(f" PASS: Classifier trained ({kls.classifier is not None})")
169
+ except Exception as e:
170
+ print(f" FAIL: {e}")
171
+ traceback.print_exc()
172
+ sys.exit(1)
173
+
174
+ # Test 6: train_hypotheses (memory-efficient vectorized version)
175
+ print("\n[Test 6] train_hypotheses (memory-efficient vectorized)...")
176
+ try:
177
+ t0 = time.time()
178
+ result = kls.train_hypotheses()
179
+ elapsed_ms = (time.time() - t0) * 1000
180
+ assert result["active"], f"not active: {result}"
181
+ assert result["n_steps"] == 30, f"n_steps={result['n_steps']}"
182
+ print(f" PASS: train_hypotheses completed in {elapsed_ms:.0f}ms")
183
+ print(f" n_steps={result['n_steps']}, n_hypotheses={result['n_hypotheses']}")
184
+ print(f" loss_initial={result['loss_initial']:.4f}, loss_final={result['loss_final']:.4f}")
185
+ print(f" delta_scale_final={result['delta_scale_final']:.6f}")
186
+ if "adaptation" in result:
187
+ adapt = result["adaptation"]
188
+ print(f" adaptation: adapted={adapt['adapted']}, rules={adapt['rules_fired']}")
189
+ except Exception as e:
190
+ print(f" FAIL: {e}")
191
+ traceback.print_exc()
192
+ sys.exit(1)
193
+
194
+ # Test 7: SOM metrics (8 canonical metrics)
195
+ print("\n[Test 7] SOM metrics (8 canonical)...")
196
+ try:
197
+ metrics = kls.compute_som_metrics()
198
+ assert "quantization_error" in metrics
199
+ assert "topological_error" in metrics
200
+ assert "kaski_lagus_error" in metrics
201
+ assert "explained_variance_share" in metrics
202
+ assert "topological_collapse" in metrics
203
+ assert "dead_neuron_rate" in metrics
204
+ assert "qe_stagnation" in metrics
205
+ assert "neighborhood_crossing" in metrics
206
+ assert "overall_health" in metrics
207
+ print(f" PASS: All 8 SOM metrics computed")
208
+ print(f" QE={metrics['quantization_error']:.4f}")
209
+ print(f" TE={metrics['topological_error']:.4f}")
210
+ print(f" KL={metrics['kaski_lagus_error']:.4f}")
211
+ print(f" VE={metrics['explained_variance_share']:.4f}")
212
+ print(f" dead_neuron_rate={metrics['dead_neuron_rate'].get('dead_neuron_rate', 0):.3f}")
213
+ print(f" collapse_severity={metrics['topological_collapse'].get('severity', '?')}")
214
+ print(f" overall_health={metrics['overall_health']}")
215
+ print(f" n_failure_indicators={metrics['n_failure_indicators']}")
216
+ except Exception as e:
217
+ print(f" FAIL: {e}")
218
+ traceback.print_exc()
219
+ sys.exit(1)
220
+
221
+ # Test 8: select_best_delta (vectorized)
222
+ print("\n[Test 8] select_best_delta (3 trials × 16 hypotheses)...")
223
+ try:
224
+ t0 = time.time()
225
+ best_delta, best_acc, info = kls.select_best_delta()
226
+ elapsed_ms = (time.time() - t0) * 1000
227
+ assert info["active"], f"not active: {info}"
228
+ assert info["n_trials"] == 3, f"n_trials={info['n_trials']}"
229
+ assert info["n_hypotheses"] == 16, f"n_hyp={info['n_hypotheses']}"
230
+ print(f" PASS: select_best_delta completed in {elapsed_ms:.0f}ms")
231
+ print(f" best_acc={best_acc:.4f}")
232
+ print(f" delta_norm={info['delta_norm']:.4f}")
233
+ print(f" n_trials={info['n_trials']}, n_hypotheses={info['n_hypotheses']}")
234
+ except Exception as e:
235
+ print(f" FAIL: {e}")
236
+ traceback.print_exc()
237
+ sys.exit(1)
238
+
239
+ # Test 9: process_batch_v2 (CONHECIMENTO + PUNIÇÃO)
240
+ print("\n[Test 9] process_batch_v2 (CONHECIMENTO + PUNIÇÃO)...")
241
+ try:
242
+ # CONHECIMENTO
243
+ result_conh = kls.process_batch_v2(
244
+ ["o gato dorme", "o cachorro corre"],
245
+ [0, 1],
246
+ label_strings={0: "negative_class", 1: "positive_class"},
247
+ dataset_name="test_ds",
248
+ enable_punishment=False,
249
+ )
250
+ assert result_conh["phase"] == "CONHECIMENTO"
251
+ print(f" PASS: CONHECIMENTO phase works: {result_conh['phase']}")
252
+
253
+ # PUNIÇÃO
254
+ result_pun = kls.process_batch_v2(
255
+ ["o gato dorme", "o cachorro corre"],
256
+ [0, 1],
257
+ label_strings={0: "negative_class", 1: "positive_class"},
258
+ dataset_name="test_ds",
259
+ enable_punishment=True,
260
+ )
261
+ assert result_pun["phase"] == "PUNICAO"
262
+ print(f" PASS: PUNIÇÃO phase works: {result_pun['phase']}")
263
+ print(f" action: {result_pun.get('action', 'none')}")
264
+ except Exception as e:
265
+ print(f" FAIL: {e}")
266
+ traceback.print_exc()
267
+ sys.exit(1)
268
+
269
+ # Test 10: aggressive_cleanup
270
+ print("\n[Test 10] aggressive_cleanup()...")
271
+ try:
272
+ cleanup_result = kls.aggressive_cleanup()
273
+ assert "rss_before_mb" in cleanup_result
274
+ assert "rss_after_mb" in cleanup_result
275
+ assert "gc_collected" in cleanup_result
276
+ print(f" PASS: aggressive_cleanup() called")
277
+ print(f" rss_before={cleanup_result['rss_before_mb']:.1f}MB")
278
+ print(f" rss_after={cleanup_result['rss_after_mb']:.1f}MB")
279
+ print(f" freed={cleanup_result['freed_mb']:.1f}MB")
280
+ print(f" gc_collected={cleanup_result['gc_collected']}")
281
+ except Exception as e:
282
+ print(f" FAIL: {e}")
283
+ traceback.print_exc()
284
+ sys.exit(1)
285
+
286
+ # Test 11: get_v2_metrics
287
+ print("\n[Test 11] get_v2_metrics()...")
288
+ try:
289
+ v2_metrics = kls.get_v2_metrics()
290
+ assert v2_metrics["version"] == "V2-dynamic"
291
+ assert v2_metrics["n_hypotheses"] == 16
292
+ print(f" PASS: get_v2_metrics() works")
293
+ print(f" version={v2_metrics['version']}")
294
+ print(f" n_hypotheses={v2_metrics['n_hypotheses']}")
295
+ print(f" n_trials={v2_metrics['n_trials']}")
296
+ print(f" hyp_train_steps={v2_metrics['hyp_train_steps']}")
297
+ print(f" buffer_size={v2_metrics['buffer_size']}")
298
+ if "dynamic_adaptation" in v2_metrics:
299
+ da = v2_metrics["dynamic_adaptation"]
300
+ print(f" n_adaptations={da['n_adaptations']}")
301
+ print(f" loss_stats: slope={da['loss_stats']['slope']:.6f}, vol={da['loss_stats']['volatility']:.3f}")
302
+ except Exception as e:
303
+ print(f" FAIL: {e}")
304
+ traceback.print_exc()
305
+ sys.exit(1)
306
+
307
+ # Summary
308
+ print("\n" + "=" * 80)
309
+ print("SMOKE TEST SUMMARY")
310
+ print("=" * 80)
311
+ print(" ALL TESTS PASSED")
312
+ print(" - KLS V2 with restored params (HIDDEN=1024, VOCAB=16384) ✓")
313
+ print(" - Memory-efficient train_hypotheses (vectorized) ✓")
314
+ print(" - Bug fix: gradient flows through deltas → SOM → loss ✓")
315
+ print(" - 8 SOM canonical metrics computed ✓")
316
+ print(" - Dynamic labels (no hardcoded gato/cachorro) ✓")
317
+ print(" - CONHECIMENTO + PUNIÇÃO phases ✓")
318
+ print(" - aggressive_cleanup() works ✓")
319
+ print("=" * 80)
320
+ sys.exit(0)
som_metrics.py ADDED
@@ -0,0 +1,811 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """som_metrics.py — V6.5-V2-metrics: Métricas de aprendizado e indicadores de
2
+ falha para Mapas Auto-Organizáveis (SOM / Redes de Kohonen).
3
+
4
+ ============================================================================
5
+ MÉTRICAS PRINCIPAIS DE APRENDIZADO
6
+ ============================================================================
7
+
8
+ 1.1. Erro de Quantização (Quantization Error - QE)
9
+ Média das distâncias euclidianas entre cada vetor de entrada e o vetor
10
+ de pesos de seu neurônio vencedor (BMU). Mede a resolução ou ajuste fino
11
+ do mapa aos dados.
12
+
13
+ QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
14
+
15
+ 1.2. Erro Topológico (Topological Error - TE)
16
+ Percentual de vetores de entrada cujos primeiro e segundo neurônios
17
+ mais próximos (BMU e segunda BMU) NÃO são vizinhos diretos na grade
18
+ de saída. Avalia preservação das relações geométricas.
19
+
20
+ TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
21
+
22
+ 1.3. Erro de Kaski-Lagus
23
+ Combinação ponderada que penaliza tanto a falta de resolução do vetor
24
+ de quantização quanto as quebras na continuidade topológica.
25
+
26
+ KL = α * QE_norm + β * TE
27
+ onde α=0.5, β=0.5 (pesos padrão)
28
+
29
+ Variante implementada: KL = QE * (1 + TE) (penaliza QE quando TE é alto)
30
+
31
+ 1.4. Variância Explicada (Share of Explained Variance)
32
+ Fração da variabilidade total dos dados de entrada que é mapeada e
33
+ representada pelos vetores de referência (pesos) dos nós da rede.
34
+
35
+ VarExplained = 1 - Var(residual) / Var(total)
36
+ onde residual = x_i - W_BMU(x_i)
37
+
38
+ ============================================================================
39
+ INDICADORES DE NÃO APRENDIZADO OU FALHA
40
+ ============================================================================
41
+
42
+ 2.1. Colapso Topológico (Topological Collapse)
43
+ Ocorre quando todos os vetores de peso convergem para o mesmo ponto
44
+ ou para uma linha reta. Indica perda total da dimensionalidade útil.
45
+
46
+ Detecção:
47
+ - std(weights) por dimensão < threshold (colapso total)
48
+ - rank(weights reshaped) < 2 (todos num ponto)
49
+ - rank < 3 (todos numa linha)
50
+
51
+ 2.2. Taxa de Neurônios Mortos (Dead Neurons)
52
+ Percentual de neurônios que NUNCA se tornam BMU para nenhum vetor.
53
+
54
+ DeadRate = (n_neurons - n_active_neurons) / n_neurons
55
+
56
+ Threshold: DeadRate > 0.5 indica problema.
57
+
58
+ 2.3. Estagnação do Erro de Quantização
59
+ QE estabiliza em patamar elevado ao longo das épocas. A taxa de
60
+ aprendizado decaiu rápido demais antes da fase de ordenação global.
61
+
62
+ Detecção: slope(QE) ≈ 0 E QE atual > 0.7 * max(QE histórico)
63
+
64
+ 2.4. Cruzamento de Vizinhança (Neighborhood Crossing)
65
+ Crescimento repentino ou oscilação crônica do TE nas épocas finais.
66
+
67
+ Detecção:
68
+ - std(TE_final_5_epochs) > 2 * std(TE_initial_5_epochs) (oscilação)
69
+ - TE[-1] > TE[-5] (crescimento no final)
70
+
71
+ ============================================================================
72
+ AUTOR: V6.5-V2-metrics
73
+ ============================================================================
74
+ """
75
+ from __future__ import annotations
76
+
77
+ import math
78
+ import logging
79
+ from collections import defaultdict, deque
80
+ from dataclasses import dataclass, field
81
+ from typing import Any, Dict, List, Optional, Tuple
82
+
83
+ import torch
84
+ import torch.nn.functional as F
85
+
86
+ logger = logging.getLogger(__name__)
87
+
88
+
89
+ # ============================================================================
90
+ # Dataclass para armazenar histórico de métricas
91
+ # ============================================================================
92
+ @dataclass
93
+ class SOMMetricHistory:
94
+ """Histórico deslizante de métricas SOM para detecção de estagnação e
95
+ cruzamento de vizinhança."""
96
+ qe_history: deque = field(default_factory=lambda: deque(maxlen=50))
97
+ te_history: deque = field(default_factory=lambda: deque(maxlen=50))
98
+ kl_history: deque = field(default_factory=lambda: deque(maxlen=50))
99
+ var_explained_history: deque = field(default_factory=lambda: deque(maxlen=50))
100
+ dead_neuron_rate_history: deque = field(default_factory=lambda: deque(maxlen=50))
101
+ epoch: int = 0
102
+
103
+ def record(self, metrics: Dict[str, Any]) -> None:
104
+ """Registra métricas de uma época.
105
+
106
+ Nota: `dead_neuron_rate` pode ser float (valor direto) ou Dict
107
+ (saída de compute_all_metrics, com chave 'dead_neuron_rate').
108
+ """
109
+ self.qe_history.append(float(metrics.get("quantization_error", 0.0)))
110
+ self.te_history.append(float(metrics.get("topological_error", 0.0)))
111
+ self.kl_history.append(float(metrics.get("kaski_lagus_error", 0.0)))
112
+ self.var_explained_history.append(
113
+ float(metrics.get("explained_variance_share", 0.0))
114
+ )
115
+ # dead_neuron_rate pode ser float ou Dict
116
+ dnr = metrics.get("dead_neuron_rate", 0.0)
117
+ if isinstance(dnr, dict):
118
+ dnr_val = float(dnr.get("dead_neuron_rate", 0.0))
119
+ else:
120
+ dnr_val = float(dnr)
121
+ self.dead_neuron_rate_history.append(dnr_val)
122
+ self.epoch += 1
123
+
124
+ def to_dict(self) -> Dict[str, Any]:
125
+ return {
126
+ "epoch": int(self.epoch),
127
+ "qe_history": list(self.qe_history),
128
+ "te_history": list(self.te_history),
129
+ "kl_history": list(self.kl_history),
130
+ "var_explained_history": list(self.var_explained_history),
131
+ "dead_neuron_rate_history": list(self.dead_neuron_rate_history),
132
+ }
133
+
134
+
135
+ # ============================================================================
136
+ # Funções auxiliares
137
+ # ============================================================================
138
+ def _grid_neighbors_set(
139
+ idx: Tuple[int, int, int, int],
140
+ grid_shape: Tuple[int, int, int, int],
141
+ ) -> set:
142
+ """Retorna o conjunto de índices vizinhos diretos (6-connectivity em 4D)
143
+ de um neurônio na grade."""
144
+ i, j, k, l = idx
145
+ I, J, K, L = grid_shape
146
+ neighbors = set()
147
+ for di, dj, dk, dl in [
148
+ (1, 0, 0, 0), (-1, 0, 0, 0),
149
+ (0, 1, 0, 0), (0, -1, 0, 0),
150
+ (0, 0, 1, 0), (0, 0, -1, 0),
151
+ (0, 0, 0, 1), (0, 0, 0, -1),
152
+ ]:
153
+ ni, nj, nk, nl = i + di, j + dj, k + dk, l + dl
154
+ if 0 <= ni < I and 0 <= nj < J and 0 <= nk < K and 0 <= nl < L:
155
+ neighbors.add((ni, nj, nk, nl))
156
+ return neighbors
157
+
158
+
159
+ def _find_bmu_and_second(
160
+ x: torch.Tensor, weights: torch.Tensor
161
+ ) -> Tuple[Tuple[int, int, int, int], Tuple[int, int, int, int], float, float]:
162
+ """Encontra BMU e 2ª BMU para um vetor x.
163
+
164
+ Args:
165
+ x: tensor [4] — vetor 4D de entrada.
166
+ weights: tensor [I, J, K, L, 4] — pesos do SOM.
167
+
168
+ Returns:
169
+ (bmu_idx, second_bmu_idx, dist_bmu, dist_second)
170
+ """
171
+ grid_shape = weights.shape[:-1]
172
+ flat_w = weights.view(-1, 4) # (I*J*K*L, 4)
173
+ diff = flat_w - x.view(1, 4)
174
+ dist_sq = torch.sum(diff * diff, dim=-1) # (I*J*K*L,)
175
+
176
+ # Top-2 menores distâncias
177
+ top2 = torch.topk(dist_sq, k=2, largest=False)
178
+ flat_idx_bmu = int(top2.indices[0].item())
179
+ flat_idx_2nd = int(top2.indices[1].item())
180
+
181
+ # Unravel
182
+ L = grid_shape[3]
183
+ K = grid_shape[2]
184
+ J = grid_shape[1]
185
+
186
+ def unravel(flat_idx: int) -> Tuple[int, int, int, int]:
187
+ i = flat_idx // (J * K * L)
188
+ r = flat_idx % (J * K * L)
189
+ j = r // (K * L)
190
+ r = r % (K * L)
191
+ k = r // L
192
+ l = r % L
193
+ return (i, j, k, l)
194
+
195
+ bmu = unravel(flat_idx_bmu)
196
+ second = unravel(flat_idx_2nd)
197
+ dist_bmu = float(math.sqrt(max(top2.values[0].item(), 0.0)))
198
+ dist_second = float(math.sqrt(max(top2.values[1].item(), 0.0)))
199
+ return bmu, second, dist_bmu, dist_second
200
+
201
+
202
+ # ============================================================================
203
+ # Métricas principais (1.1 - 1.4)
204
+ # ============================================================================
205
+ def quantization_error(
206
+ data: torch.Tensor, weights: torch.Tensor
207
+ ) -> float:
208
+ """1.1 Erro de Quantização (QE).
209
+
210
+ QE = (1/N) * Σ_i ||x_i - W_BMU(x_i)||₂
211
+
212
+ Args:
213
+ data: tensor [N, 4] — vetores de entrada.
214
+ weights: tensor [I, J, K, L, 4] — pesos do SOM.
215
+
216
+ Returns:
217
+ QE (float). 0.0 se data vazio.
218
+ """
219
+ if data.numel() == 0:
220
+ return 0.0
221
+ flat_w = weights.view(-1, 4) # (M, 4)
222
+ # distância de cada amostra a todos os neurônios
223
+ # data: (N, 4), flat_w: (M, 4) -> (N, M)
224
+ diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
225
+ dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
226
+ min_dist_sq, _ = torch.min(dist_sq, dim=1) # (N,)
227
+ qe = torch.mean(torch.sqrt(min_dist_sq.clamp(min=0.0)))
228
+ return float(qe.item())
229
+
230
+
231
+ def topological_error(
232
+ data: torch.Tensor, weights: torch.Tensor
233
+ ) -> float:
234
+ """1.2 Erro Topológico (TE).
235
+
236
+ TE = (1/N) * Σ_i 𝟙[¬adjacent(BMU_1(x_i), BMU_2(x_i))]
237
+
238
+ Dois neurônios são adjacentes se differem em exatamente uma coordenada
239
+ por ±1 (6-connectivity em 4D).
240
+
241
+ Args:
242
+ data: tensor [N, 4]
243
+ weights: tensor [I, J, K, L, 4]
244
+
245
+ Returns:
246
+ TE ∈ [0, 1]. 0.0 se data vazio.
247
+ """
248
+ if data.numel() == 0:
249
+ return 0.0
250
+ grid_shape = weights.shape[:-1]
251
+ flat_w = weights.view(-1, 4)
252
+ diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
253
+ dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
254
+
255
+ # Top-2 BMUs para cada amostra
256
+ top2 = torch.topk(dist_sq, k=2, largest=False, dim=1)
257
+ flat_bmu = top2.indices[:, 0] # (N,)
258
+ flat_2nd = top2.indices[:, 1] # (N,)
259
+
260
+ # Unravel
261
+ L = grid_shape[3]
262
+ K = grid_shape[2]
263
+ J = grid_shape[1]
264
+ I = grid_shape[0]
265
+
266
+ def unravel_batch(flat_idx: torch.Tensor) -> torch.Tensor:
267
+ """Retorna tensor (N, 4) com (i, j, k, l)."""
268
+ i = flat_idx // (J * K * L)
269
+ r = flat_idx % (J * K * L)
270
+ j = r // (K * L)
271
+ r = r % (K * L)
272
+ k = r // L
273
+ l = r % L
274
+ return torch.stack([i, j, k, l], dim=1).long()
275
+
276
+ bmu_idx = unravel_batch(flat_bmu) # (N, 4)
277
+ second_idx = unravel_batch(flat_2nd) # (N, 4)
278
+
279
+ # Adjacência: |Δcoord| soma = 1 (apenas uma coordenada muda por ±1)
280
+ delta = (bmu_idx - second_idx).abs() # (N, 4)
281
+ is_adjacent = (delta.sum(dim=1) == 1) & (delta.max(dim=1).values == 1)
282
+
283
+ te = 1.0 - float(is_adjacent.float().mean().item())
284
+ return te
285
+
286
+
287
+ def kaski_lagus_error(
288
+ data: torch.Tensor, weights: torch.Tensor, alpha: float = 0.5
289
+ ) -> float:
290
+ """1.3 Erro de Kaski-Lagus.
291
+
292
+ Combinação ponderada de QE normalizado e TE.
293
+
294
+ Variante implementada (Kaski & Lagus, 1999):
295
+ KL = α * MQE_norm + (1-α) * TE
296
+ onde MQE_norm = QE / mean(||x||) (normalização por escala dos dados).
297
+
298
+ Uma variante alternativa multiplica:
299
+ KL = QE * (1 + TE)
300
+ que penaliza QE quando TE é alto (usada se use_multiplicative=True).
301
+
302
+ Args:
303
+ data: tensor [N, 4]
304
+ weights: tensor [I, J, K, L, 4]
305
+ alpha: peso do QE normalizado (default 0.5)
306
+
307
+ Returns:
308
+ KL (float).
309
+ """
310
+ if data.numel() == 0:
311
+ return 0.0
312
+ qe = quantization_error(data, weights)
313
+ te = topological_error(data, weights)
314
+ # Normalização: escala típica dos dados
315
+ data_norm = float(torch.mean(torch.norm(data, dim=1)).item())
316
+ qe_norm = qe / max(data_norm, 1e-8)
317
+ kl = alpha * qe_norm + (1.0 - alpha) * te
318
+ return float(kl)
319
+
320
+
321
+ def explained_variance_share(
322
+ data: torch.Tensor, weights: torch.Tensor
323
+ ) -> float:
324
+ """1.4 Variância Explicada (Share of Explained Variance).
325
+
326
+ VarExplained = 1 - Var(residual) / Var(total)
327
+ onde residual = x_i - W_BMU(x_i)
328
+
329
+ Implementação: computamos a variância total dos dados (soma das variâncias
330
+ por dimensão) e a variância dos resíduos. A fração explicada é:
331
+
332
+ VE = 1 - Σ_d Var(residual_d) / Σ_d Var(data_d)
333
+
334
+ Returns:
335
+ VE ∈ [0, 1]. Próximo de 1 = boa representação.
336
+ """
337
+ if data.numel() == 0 or data.shape[0] < 2:
338
+ return 0.0
339
+ flat_w = weights.view(-1, 4)
340
+ diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
341
+ dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
342
+ _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
343
+ bmu_w = flat_w[bmu_flat] # (N, 4)
344
+ residual = data - bmu_w # (N, 4)
345
+
346
+ # Variância por dimensão (sem viés, ddof=0)
347
+ var_data = torch.var(data, dim=0, unbiased=False) # (4,)
348
+ var_resid = torch.var(residual, dim=0, unbiased=False) # (4,)
349
+
350
+ total_var = float(torch.sum(var_data).item())
351
+ resid_var = float(torch.sum(var_resid).item())
352
+ if total_var < 1e-12:
353
+ return 0.0
354
+ ve = 1.0 - (resid_var / total_var)
355
+ return float(max(0.0, min(1.0, ve)))
356
+
357
+
358
+ # ============================================================================
359
+ # Indicadores de falha (2.1 - 2.4)
360
+ # ============================================================================
361
+ def topological_collapse(weights: torch.Tensor) -> Dict[str, Any]:
362
+ """2.1 Colapso Topológico (Topological Collapse).
363
+
364
+ Detecta se os pesos colapsaram para um ponto (rank < 1) ou para uma linha
365
+ (rank < 2). Usa SVD sobre os pesos reshaped (M, 4).
366
+
367
+ Args:
368
+ weights: tensor [I, J, K, L, 4]
369
+
370
+ Returns:
371
+ Dict com:
372
+ - collapsed_to_point: bool (todos iguais)
373
+ - collapsed_to_line: bool (variação só em 1 direção)
374
+ - effective_rank: float (rank efetivo via razão de singular values)
375
+ - singular_values: list[float]
376
+ - severity: str ("none" | "line" | "point")
377
+ """
378
+ flat_w = weights.view(-1, 4).clone() # (M, 4)
379
+ if flat_w.numel() == 0:
380
+ return {"collapsed_to_point": False, "collapsed_to_line": False,
381
+ "effective_rank": 0.0, "singular_values": [], "severity": "none"}
382
+
383
+ # Centraliza
384
+ flat_w_centered = flat_w - flat_w.mean(dim=0, keepdim=True)
385
+
386
+ # V6.5-V2-metrics FIX: usa SVD com fallback robusto para matrizes
387
+ # singulares/ill-conditioned. Se SVD falhar, usa np.linalg.eigh com
388
+ # regularização (adiciona pequeno ruído diagonal).
389
+ S_list: List[float] = []
390
+ try:
391
+ # Tentativa 1: SVD direto (mais estável que eigh)
392
+ U, S, V = torch.linalg.svd(flat_w_centered, full_matrices=False)
393
+ S_list = [float(s.item()) for s in S]
394
+ except Exception:
395
+ # Tentativa 2: eigendecomposição da matriz de covariância com regularização
396
+ try:
397
+ cov = flat_w_centered.t() @ flat_w_centered
398
+ # Regularização: adiciona pequeno valor diagonal para estabilizar
399
+ n = cov.shape[0]
400
+ reg = 1e-6 * float(cov.diag().max().item()) if n > 0 else 1e-6
401
+ cov_reg = cov + torch.eye(n) * reg
402
+ S_eigh, _ = torch.linalg.eigh(cov_reg)
403
+ S_eigh = S_eigh.flip(0) # descending order
404
+ # Converte autovalores em "singular values" aproximados (sqrt)
405
+ S_list = [float(math.sqrt(max(s.item(), 0.0))) for s in S_eigh]
406
+ except Exception:
407
+ # Tentativa 3: usa norma de Frobenius como aproximação
408
+ s_frob = float(torch.norm(flat_w_centered).item())
409
+ S_list = [s_frob, 0.0, 0.0, 0.0]
410
+
411
+ if not S_list:
412
+ return {"collapsed_to_point": False, "collapsed_to_line": False,
413
+ "effective_rank": 0.0, "singular_values": [], "severity": "none"}
414
+
415
+ s_max = max(S_list[0] if S_list else 0.0, 1e-12)
416
+
417
+ # Effective rank: número de singular values > 1% do maior
418
+ threshold = 0.01 * s_max
419
+ n_significant = sum(1 for s in S_list if s > threshold)
420
+ # Effective rank (float, via entropy)
421
+ s_norm = [s / s_max for s in S_list]
422
+ s_norm = [s for s in s_norm if s > 1e-10]
423
+ if s_norm:
424
+ p = torch.tensor(s_norm)
425
+ p_norm = p / p.sum()
426
+ entropy = -float(torch.sum(p_norm * torch.log(p_norm)).item())
427
+ effective_rank = math.exp(entropy)
428
+ else:
429
+ effective_rank = 0.0
430
+
431
+ collapsed_to_point = n_significant <= 1 and s_max < 1e-6
432
+ collapsed_to_line = n_significant == 1 and s_max >= 1e-6
433
+
434
+ if collapsed_to_point:
435
+ severity = "point"
436
+ elif collapsed_to_line:
437
+ severity = "line"
438
+ else:
439
+ severity = "none"
440
+
441
+ return {
442
+ "collapsed_to_point": bool(collapsed_to_point),
443
+ "collapsed_to_line": bool(collapsed_to_line),
444
+ "effective_rank": float(effective_rank),
445
+ "singular_values": S_list,
446
+ "severity": severity,
447
+ }
448
+
449
+
450
+ def dead_neuron_rate(
451
+ data: torch.Tensor, weights: torch.Tensor
452
+ ) -> Dict[str, Any]:
453
+ """2.2 Taxa de Neurônios Mortos (Dead Neurons).
454
+
455
+ DeadRate = (n_neurons - n_active_neurons) / n_neurons
456
+
457
+ Um neurônio é "ativo" se é BMU para pelo menos uma amostra.
458
+
459
+ Args:
460
+ data: tensor [N, 4]
461
+ weights: tensor [I, J, K, L, 4]
462
+
463
+ Returns:
464
+ Dict com:
465
+ - dead_neuron_rate: float ∈ [0, 1]
466
+ - n_dead: int
467
+ - n_active: int
468
+ - n_total: int
469
+ - dead_indices: list de (i,j,k,l)
470
+ """
471
+ n_total = weights.shape[0] * weights.shape[1] * weights.shape[2] * weights.shape[3]
472
+ if data.numel() == 0:
473
+ return {"dead_neuron_rate": 1.0, "n_dead": n_total, "n_active": 0,
474
+ "n_total": n_total, "dead_indices": []}
475
+
476
+ flat_w = weights.view(-1, 4)
477
+ diff = data.unsqueeze(1) - flat_w.unsqueeze(0)
478
+ dist_sq = torch.sum(diff * diff, dim=-1) # (N, M)
479
+ _, bmu_flat = torch.min(dist_sq, dim=1) # (N,)
480
+
481
+ active_mask = torch.zeros(n_total, dtype=torch.bool)
482
+ active_mask[bmu_flat] = True
483
+ n_active = int(active_mask.sum().item())
484
+ n_dead = n_total - n_active
485
+
486
+ # Coleta índices mortos
487
+ dead_flat = (~active_mask).nonzero(as_tuple=True)[0]
488
+ L_dim = weights.shape[3]
489
+ K_dim = weights.shape[2]
490
+ J_dim = weights.shape[1]
491
+ dead_indices = []
492
+ for flat_idx in dead_flat.tolist():
493
+ i = flat_idx // (J_dim * K_dim * L_dim)
494
+ r = flat_idx % (J_dim * K_dim * L_dim)
495
+ j = r // (K_dim * L_dim)
496
+ r = r % (K_dim * L_dim)
497
+ k = r // L_dim
498
+ l = r % L_dim
499
+ dead_indices.append((i, j, k, l))
500
+
501
+ return {
502
+ "dead_neuron_rate": float(n_dead / n_total) if n_total > 0 else 0.0,
503
+ "n_dead": int(n_dead),
504
+ "n_active": int(n_active),
505
+ "n_total": int(n_total),
506
+ "dead_indices": dead_indices[:50], # limita para não explodir JSON
507
+ }
508
+
509
+
510
+ def quantization_error_stagnation(
511
+ history: SOMMetricHistory,
512
+ high_threshold_frac: float = 0.7,
513
+ slope_tolerance: float = 1e-4,
514
+ window_size: int = 5,
515
+ ) -> Dict[str, Any]:
516
+ """2.3 Estagnação do Erro de Quantização.
517
+
518
+ Detecta se o QE estabilizou em patamar elevado:
519
+ - |slope(QE)| < slope_tolerance (estável)
520
+ - QE_atual > high_threshold_frac * max(QE_history) (alto)
521
+
522
+ Args:
523
+ history: histórico de métricas SOM.
524
+ high_threshold_frac: fração do QE máximo para considerar "alto".
525
+ slope_tolerance: tolerância de slope para considerar "estável".
526
+ window_size: nº de épocas recentes para regressão linear.
527
+
528
+ Returns:
529
+ Dict com:
530
+ - is_stagnant: bool
531
+ - current_qe: float
532
+ - max_qe: float
533
+ - slope: float
534
+ - severity: str ("none" | "mild" | "severe")
535
+ """
536
+ qe_list = list(history.qe_history)
537
+ if len(qe_list) < window_size + 2:
538
+ return {"is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
539
+ "slope": 0.0, "severity": "none", "n_epochs": len(qe_list)}
540
+
541
+ # Regressão linear sobre as últimas window_size épocas
542
+ recent = qe_list[-window_size:]
543
+ n = len(recent)
544
+ x_mean = (n - 1) / 2.0
545
+ y_mean = sum(recent) / n
546
+ num = sum((i - x_mean) * (y - y_mean) for i, y in enumerate(recent))
547
+ den = sum((i - x_mean) ** 2 for i in range(n))
548
+ slope = num / den if den > 0 else 0.0
549
+
550
+ current_qe = float(qe_list[-1])
551
+ max_qe = float(max(qe_list))
552
+ is_high = current_qe > high_threshold_frac * max_qe
553
+ is_stable = abs(slope) < slope_tolerance
554
+
555
+ is_stagnant = is_stable and is_high
556
+
557
+ if is_stagnant:
558
+ if current_qe > 0.9 * max_qe:
559
+ severity = "severe"
560
+ else:
561
+ severity = "mild"
562
+ else:
563
+ severity = "none"
564
+
565
+ return {
566
+ "is_stagnant": bool(is_stagnant),
567
+ "current_qe": current_qe,
568
+ "max_qe": max_qe,
569
+ "slope": float(slope),
570
+ "severity": severity,
571
+ "n_epochs": len(qe_list),
572
+ }
573
+
574
+
575
+ def neighborhood_crossing(
576
+ history: SOMMetricHistory,
577
+ initial_window: int = 5,
578
+ final_window: int = 5,
579
+ growth_threshold: float = 1.5,
580
+ ) -> Dict[str, Any]:
581
+ """2.4 Cruzamento de Vizinhança (Neighborhood Crossing).
582
+
583
+ Detecta crescimento repentino ou oscilação crônica do TE nas épocas
584
+ finais:
585
+ - std(TE_final) > growth_threshold * std(TE_initial) (oscilação cresceu)
586
+ - TE[-1] > TE[-5] (crescimento no final)
587
+
588
+ Args:
589
+ history: histórico de métricas SOM.
590
+ initial_window: nº de épocas iniciais para baseline.
591
+ final_window: nº de épocas finais para comparação.
592
+ growth_threshold: razão de std para considerar "crescimento crônico".
593
+
594
+ Returns:
595
+ Dict com:
596
+ - detected: bool
597
+ - te_initial_mean: float
598
+ - te_final_mean: float
599
+ - te_initial_std: float
600
+ - te_final_std: float
601
+ - std_ratio: float
602
+ - is_oscillating: bool
603
+ - is_growing: bool
604
+ - severity: str ("none" | "mild" | "severe")
605
+ """
606
+ te_list = list(history.te_history)
607
+ if len(te_list) < initial_window + final_window:
608
+ return {"detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
609
+ "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
610
+ "is_oscillating": False, "is_growing": False, "severity": "none",
611
+ "n_epochs": len(te_list)}
612
+
613
+ initial = te_list[:initial_window]
614
+ final = te_list[-final_window:]
615
+
616
+ init_mean = sum(initial) / len(initial)
617
+ final_mean = sum(final) / len(final)
618
+ init_var = sum((x - init_mean) ** 2 for x in initial) / len(initial)
619
+ final_var = sum((x - final_mean) ** 2 for x in final) / len(final)
620
+ init_std = math.sqrt(init_var)
621
+ final_std = math.sqrt(final_var)
622
+
623
+ std_ratio = final_std / max(init_std, 1e-8)
624
+ is_oscillating = std_ratio > growth_threshold
625
+ is_growing = final_mean > init_mean and (final_mean - init_mean) > 0.05
626
+
627
+ detected = is_oscillating or is_growing
628
+
629
+ if detected:
630
+ if std_ratio > 2.0 * growth_threshold or is_growing:
631
+ severity = "severe"
632
+ else:
633
+ severity = "mild"
634
+ else:
635
+ severity = "none"
636
+
637
+ return {
638
+ "detected": bool(detected),
639
+ "te_initial_mean": float(init_mean),
640
+ "te_final_mean": float(final_mean),
641
+ "te_initial_std": float(init_std),
642
+ "te_final_std": float(final_std),
643
+ "std_ratio": float(std_ratio),
644
+ "is_oscillating": bool(is_oscillating),
645
+ "is_growing": bool(is_growing),
646
+ "severity": severity,
647
+ "n_epochs": len(te_list),
648
+ }
649
+
650
+
651
+ # ============================================================================
652
+ # Função agregadora: compute_all_metrics
653
+ # ============================================================================
654
+ def compute_all_metrics(
655
+ data: torch.Tensor,
656
+ weights: torch.Tensor,
657
+ history: Optional[SOMMetricHistory] = None,
658
+ ) -> Dict[str, Any]:
659
+ """Computa todas as métricas SOM de uma vez.
660
+
661
+ Args:
662
+ data: tensor [N, 4] — buffer de vetores 4D.
663
+ weights: tensor [I, J, K, L, 4] — pesos do SOM.
664
+ history: histórico de métricas (opcional, para estagnação e cruzamento).
665
+
666
+ Returns:
667
+ Dict com:
668
+ - quantization_error: float
669
+ - topological_error: float
670
+ - kaski_lagus_error: float
671
+ - explained_variance_share: float
672
+ - topological_collapse: Dict
673
+ - dead_neuron_rate: Dict
674
+ - quantization_error_stagnation: Dict (se history fornecido)
675
+ - neighborhood_crossing: Dict (se history fornecido)
676
+ - overall_health: str ("healthy" | "warning" | "critical")
677
+ - failure_indicators: list[str]
678
+ """
679
+ # V6.5-V2-metrics FIX: wrap entire computation in try/except para evitar
680
+ # crashes fatais quando o SOM tem pesos degenerados (ex: todos iguais
681
+ # após muito treinamento sem diversidade). Em caso de erro, retorna
682
+ # métricas safe-default em vez de crashar o treino.
683
+ try:
684
+ return _compute_all_metrics_impl(data, weights, history)
685
+ except Exception as e:
686
+ logger.warning(
687
+ f"[som_metrics] compute_all_metrics failed (using safe defaults): {e}"
688
+ )
689
+ return {
690
+ "quantization_error": 0.0,
691
+ "topological_error": 0.0,
692
+ "kaski_lagus_error": 0.0,
693
+ "explained_variance_share": 0.0,
694
+ "topological_collapse": {
695
+ "collapsed_to_point": False, "collapsed_to_line": False,
696
+ "effective_rank": 0.0, "singular_values": [],
697
+ "severity": "none", "error": str(e)[:200],
698
+ },
699
+ "dead_neuron_rate": {
700
+ "dead_neuron_rate": 1.0, "n_dead": 0, "n_active": 0,
701
+ "n_total": 0, "dead_indices": [], "error": str(e)[:200],
702
+ },
703
+ "qe_stagnation": {
704
+ "is_stagnant": False, "current_qe": 0.0, "max_qe": 0.0,
705
+ "slope": 0.0, "severity": "none", "n_epochs": 0,
706
+ },
707
+ "neighborhood_crossing": {
708
+ "detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
709
+ "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
710
+ "is_oscillating": False, "is_growing": False, "severity": "none",
711
+ "n_epochs": 0,
712
+ },
713
+ "overall_health": "unknown",
714
+ "failure_indicators": [f"metrics_computation_error: {str(e)[:100]}"],
715
+ "n_failure_indicators": 1,
716
+ "error": str(e)[:300],
717
+ }
718
+
719
+
720
+ def _compute_all_metrics_impl(
721
+ data: torch.Tensor,
722
+ weights: torch.Tensor,
723
+ history: Optional[SOMMetricHistory] = None,
724
+ ) -> Dict[str, Any]:
725
+ """Implementação interna de compute_all_metrics (sem try/except wrapper)."""
726
+ metrics: Dict[str, Any] = {}
727
+
728
+ # 1.1 - 1.4: Métricas principais
729
+ metrics["quantization_error"] = quantization_error(data, weights)
730
+ metrics["topological_error"] = topological_error(data, weights)
731
+ metrics["kaski_lagus_error"] = kaski_lagus_error(data, weights)
732
+ metrics["explained_variance_share"] = explained_variance_share(data, weights)
733
+
734
+ # 2.1: Colapso topológico
735
+ collapse = topological_collapse(weights)
736
+ metrics["topological_collapse"] = collapse
737
+
738
+ # 2.2: Dead neurons
739
+ dead = dead_neuron_rate(data, weights)
740
+ metrics["dead_neuron_rate"] = dead
741
+
742
+ # 2.3: Estagnação do QE (requer histórico)
743
+ if history is not None and len(history.qe_history) > 5:
744
+ metrics["qe_stagnation"] = quantization_error_stagnation(history)
745
+ else:
746
+ metrics["qe_stagnation"] = {
747
+ "is_stagnant": False, "current_qe": metrics["quantization_error"],
748
+ "max_qe": metrics["quantization_error"], "slope": 0.0,
749
+ "severity": "none", "n_epochs": 0,
750
+ }
751
+
752
+ # 2.4: Cruzamento de vizinhança (requer histórico)
753
+ if history is not None and len(history.te_history) > 10:
754
+ metrics["neighborhood_crossing"] = neighborhood_crossing(history)
755
+ else:
756
+ metrics["neighborhood_crossing"] = {
757
+ "detected": False, "te_initial_mean": 0.0, "te_final_mean": 0.0,
758
+ "te_initial_std": 0.0, "te_final_std": 0.0, "std_ratio": 0.0,
759
+ "is_oscillating": False, "is_growing": False, "severity": "none",
760
+ "n_epochs": 0,
761
+ }
762
+
763
+ # Avaliação de saúde geral
764
+ failure_indicators: List[str] = []
765
+ if collapse["collapsed_to_point"]:
766
+ failure_indicators.append("topological_collapse_point")
767
+ elif collapse["collapsed_to_line"]:
768
+ failure_indicators.append("topological_collapse_line")
769
+ if dead["dead_neuron_rate"] > 0.5:
770
+ failure_indicators.append(f"dead_neuron_rate_high_{dead['dead_neuron_rate']:.2f}")
771
+ if metrics["qe_stagnation"]["is_stagnant"]:
772
+ failure_indicators.append(f"qe_stagnant_{metrics['qe_stagnation']['severity']}")
773
+ if metrics["neighborhood_crossing"]["detected"]:
774
+ failure_indicators.append(
775
+ f"neighborhood_crossing_{metrics['neighborhood_crossing']['severity']}"
776
+ )
777
+ if metrics["explained_variance_share"] < 0.3:
778
+ failure_indicators.append(
779
+ f"low_explained_variance_{metrics['explained_variance_share']:.2f}"
780
+ )
781
+ if metrics["topological_error"] > 0.5:
782
+ failure_indicators.append(
783
+ f"high_topological_error_{metrics['topological_error']:.2f}"
784
+ )
785
+
786
+ if len(failure_indicators) >= 2:
787
+ overall_health = "critical"
788
+ elif len(failure_indicators) == 1:
789
+ overall_health = "warning"
790
+ else:
791
+ overall_health = "healthy"
792
+
793
+ metrics["failure_indicators"] = failure_indicators
794
+ metrics["overall_health"] = overall_health
795
+ metrics["n_failure_indicators"] = len(failure_indicators)
796
+
797
+ return metrics
798
+
799
+
800
+ __all__ = [
801
+ "SOMMetricHistory",
802
+ "quantization_error",
803
+ "topological_error",
804
+ "kaski_lagus_error",
805
+ "explained_variance_share",
806
+ "topological_collapse",
807
+ "dead_neuron_rate",
808
+ "quantization_error_stagnation",
809
+ "neighborhood_crossing",
810
+ "compute_all_metrics",
811
+ ]
train_v6_5_v2.py CHANGED
@@ -1,34 +1,52 @@
1
- """train_v6_5_v2.py — V6.5-V2-dynamic (Hiperparâmetros adaptativos + 2 Fases).
2
 
3
  ═══════════════════════════════════════════════════════════════════════════════
4
- V6.5-V2-dynamic — HIPERPARÂMETROS V2 DINÂMICOS E AUTOMÁTICOS
5
  ═══════════════════════════════════════════════════════════════════════════════
6
 
7
- User requirements (latest, V6.5-V2-dynamic):
8
- 1. Atualizações HF em LOTE (sobrescrever antigos desatualizados).
9
- 2. Limpar Armazenamento e worklog.
10
- 3. HF_TOKEN apagada do ambiente virtual e dos scripts enviados ao HF
 
11
  após uso.
12
- 4. Usar streaming_datasets.py e ativar xeon_runtime.py.
13
- 5. Analisar e aprimorar (lógica e matemática) — HIPERPARÂMETROS V2
14
- (n_hypotheses, n_trials, hyp_train_steps) DINÂMICOS e AUTOMÁTICOS
15
- conforme a loss e recorrência de punições.
16
- 6. Corrigir erros e falhas.
17
- 7. FASE 1 CONHECIMENTO (meta mínima 6000+ samples, 100 em 100) na
18
  sequência exata de 8 datasets.
19
- 8. FASE 2 TREINAMENTO COM PUNIÇÃO ATIVA (meta mínima 1000+ samples,
20
  100 em 100) sobre BrunoN-Dev/corpus-ptbr-v1.
21
- 9. Após concluir: enviar scripts+arquivos+estado do modelo testados e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  aprovados ao HF (sobrescrevendo antigos).
23
 
24
- V6.5-V2-dynamic (alterações vs V6.5-V2):
25
- - NOVO: Hiperparâmetros V2 dinâmicos (n_hypotheses ∈ [4, 32],
26
- n_trials ∈ [1, 6], hyp_train_steps ∈ [10, 80]) ajustados
27
- automaticamente após cada train_hypotheses e após cada punição.
28
- - NOVO: Meta CONHECIMENTO reduzida de 8000 → 6000 (8 × 800 = 6400).
29
- - NOVO: Meta PUNIÇÃO aumentada de 500 → 1000.
30
- - NOVO: Upload HF em LOTE ao final (scripts + estado + modelo).
31
- - NOVO: Limpeza de worklog e cache HF antes e depois do treino.
 
 
 
 
 
32
  """
33
 
34
  from __future__ import annotations
@@ -96,16 +114,38 @@ FP16_BENCH = benchmark_fp16_matmul(size=4000, warmup=1, iters=2)
96
  logger.info(f"[V6.5-V2] Xeon FP16 benchmark: {FP16_BENCH}")
97
 
98
  # ============================================================================
99
- # 2. Configurações V6.5-V2 (864 neurons, hidden/vocab reduzidos para caber em 4GB RAM)
100
  # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  BATCH_SIZE = 16
102
  MAX_SEQ_LEN = 8
103
 
104
- # V2-dynamic-memory: reduzido de 1024→256 e 16384→4096 para evitar OOM em 4GB RAM
105
- # (preserva funcionalidade; apenas reduz footprint de embedding)
106
- HIDDEN_DIM = 256
107
- VOCAB_SIZE = 4096
108
- SOM_GRID = (6, 6, 6, 4) # 864 neurons
109
  T_MAX = 10000
110
  N_START = 10
111
  LAMBDA_EWC = 0.02
@@ -113,41 +153,41 @@ ALPHA0 = 0.1
113
  SIGMA0 = 1.5
114
  DIM_CHOICE = "y"
115
 
116
- # V2-dynamic — HypothesisEnsemble parameters (iniciais; serão adaptados)
117
- N_HYPOTHESES = 8 # inicial ativo (reduzido de 16 para economizar RAM)
118
- MAX_N_HYPOTHESES = 16 # pré-alocação máxima (reduzido de 32)
119
- MIN_N_HYPOTHESES = 4 # limite inferior dinâmico
120
- N_TRIALS = 3 # inicial
121
  MIN_N_TRIALS = 1
122
  MAX_N_TRIALS = 6
123
- HYP_TRAIN_STEPS = 20 # inicial (reduzido de 30 para economizar CPU)
124
- MIN_HYP_TRAIN_STEPS = 5
125
- MAX_HYP_TRAIN_STEPS = 60
126
  HYP_LR = 1e-4
127
- HYP_HIDDEN_DIM = 128 # reduzido de 256 para economizar RAM
128
  LOSS_HISTORY_WINDOW = 8
129
  PUNISHMENT_WINDOW = 12
130
 
131
  # V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
132
- # Após processar cada chunk, se buffer > MAX_BUFFER_SIZE, trunca para os
133
- # últimos MAX_BUFFER_SIZE amostras. Isto limita o crescimento de memória
134
- # sem perder o contexto recente. Os pesos do SOM já capturam o conhecimento
135
- # acumulado, então o buffer só precisa conter amostras recentes para ajuste fino.
136
  MAX_BUFFER_SIZE = 256
137
 
138
  # Streaming — User requirement: "streaming de 100 em 100 samples"
139
  STREAM_BATCH_SIZE = 100
140
 
141
- # V2-dynamic — Metas atualizadas: 6000 CONHECIMENTO + 1000 PUNIÇÃO
142
- META_MINIMA_CONHECIMENTO = 6000
143
- MAX_SAMPLES_PER_DATASET_CONHECIMENTO = 800 # 8 × 800 = 6400 ≥ 6000
144
- META_MINIMA_PUNICAO = 1000
145
- MAX_SAMPLES_PUNICAO = 1000
146
-
147
- # User requirement: "não reduzir tempo e não gerar dados sintéticos"
148
- INTER_BATCH_PAUSE_S = 0.3
149
- INTER_DATASET_PAUSE_S = 1.5
150
- INTER_STREAM_BATCH_PAUSE_S = 0.5
 
 
 
 
151
 
152
  # Storage critical
153
  STORAGE_CRITICAL_PCT = 90
@@ -281,38 +321,118 @@ CGROUP_MEM_CRITICAL_PCT = 80 # se RSS > 80% do cgroup, salvar estado e parar
281
 
282
 
283
  def aggressive_memory_cleanup() -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
284
  gc.collect()
285
  gc.collect()
286
  try:
287
  import torch
288
  if torch.cuda.is_available():
289
  torch.cuda.empty_cache()
 
 
 
 
 
 
 
290
  except Exception:
291
  pass
292
- return {"gc_called": True, "timestamp": datetime.now().isoformat()}
 
 
 
 
 
 
 
 
293
 
294
 
295
  def aggressive_storage_cleanup() -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  n_removed = 0
297
  mb_freed = 0.0
 
 
298
  hf_cache = Path.home() / ".cache" / "huggingface"
299
  if hf_cache.exists():
300
- for f in hf_cache.rglob("*.lock"):
301
- try:
302
- sz = f.stat().st_size
303
- f.unlink()
304
- n_removed += 1
305
- mb_freed += sz / 1e6
306
- except Exception:
307
- pass
308
- for f in hf_cache.rglob("*.incomplete"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  try:
310
- sz = f.stat().st_size
311
- f.unlink()
312
- n_removed += 1
313
- mb_freed += sz / 1e6
 
314
  except Exception:
315
  pass
 
316
  return {
317
  "n_files_removed": n_removed,
318
  "mb_freed": float(mb_freed),
@@ -428,7 +548,145 @@ def save_model_states_for_evaluation(
428
 
429
 
430
  # ============================================================================
431
- # 6. Streaming helper — chunks of 100 samples
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  # ============================================================================
433
  def stream_dataset_in_chunks(
434
  dataset_name: str,
@@ -437,9 +695,20 @@ def stream_dataset_in_chunks(
437
  hf_token: Optional[str] = None,
438
  timeout_per_chunk: int = 60,
439
  ) -> Iterator[List[str]]:
440
- """V6.5-V2 — Stream a dataset in chunks of `chunk_size` samples.
441
-
442
- User requirement: "streaming de 100 em 100 samples até esgotar"
 
 
 
 
 
 
 
 
 
 
 
443
  """
444
  try:
445
  from bigru_t.data.streaming_datasets import stream_dataset
@@ -449,54 +718,68 @@ def stream_dataset_in_chunks(
449
 
450
  yielded_total = 0
451
  chunk_idx = 0
452
- while yielded_total < max_total:
453
- remaining = max_total - yielded_total
454
- this_chunk_target = min(chunk_size, remaining)
455
- seed_offset = yielded_total
456
- chunk: List[str] = []
457
- t_start = time.time()
458
- try:
459
- for sample in stream_dataset(
460
- dataset_name,
461
- max_samples=this_chunk_target + seed_offset + 5,
462
- hf_token=hf_token,
463
- ):
464
- if time.time() - t_start > timeout_per_chunk:
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  logger.warning(
466
- f"[V6.5-V2] {dataset_name} chunk {chunk_idx+1} "
467
- f"timeout after {timeout_per_chunk}s "
468
- f"(collected {len(chunk)}/{this_chunk_target})"
469
  )
470
- break
471
- if sample.raw_text and len(sample.raw_text.strip()) > 0:
472
- if seed_offset > 0:
473
- seed_offset -= 1
474
- continue
475
- chunk.append(sample.raw_text.strip()[:200])
476
- if len(chunk) >= this_chunk_target:
477
- break
478
- except Exception as e:
479
- logger.warning(f"[V6.5-V2] {dataset_name} chunk {chunk_idx+1} error: {e}")
480
- if chunk:
481
  yield chunk
482
  yielded_total += len(chunk)
483
  chunk_idx += 1
484
- break
485
-
486
- if not chunk:
487
- logger.info(
488
- f"[V6.5-V2] {dataset_name} exhausted after {yielded_total} samples "
489
- f"({chunk_idx} chunks)"
490
- )
491
- break
 
 
 
 
492
 
 
 
493
  yield chunk
494
  yielded_total += len(chunk)
495
  chunk_idx += 1
496
- logger.info(
497
- f"[V6.5-V2] {dataset_name} chunk {chunk_idx} done: "
498
- f"{len(chunk)} samples (total: {yielded_total}/{max_total})"
499
- )
 
500
 
501
 
502
  # ============================================================================
@@ -669,6 +952,12 @@ def run_fase_conhecimento(
669
  chunk_idx_global = 0
670
  accuracies_log: List[Dict[str, Any]] = []
671
  attention_log: List[Dict[str, Any]] = []
 
 
 
 
 
 
672
 
673
  for ds_idx, dataset_name in enumerate(CONHECIMENTO_DATASETS):
674
  if storage_critical_stopped:
@@ -822,9 +1111,54 @@ def run_fase_conhecimento(
822
  f"[V6.5-V2] Buffer trimmed: -{overflow} → "
823
  f"{len(kls.buffer_4d)} samples (cap={MAX_BUFFER_SIZE})"
824
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  # Aggressive memory cleanup between chunks
826
  if chunk_idx_global % 2 == 0:
827
  aggressive_memory_cleanup()
 
 
 
 
 
 
 
 
 
 
828
  except Exception as e:
829
  logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
830
  traceback.print_exc()
@@ -854,6 +1188,12 @@ def run_fase_conhecimento(
854
  "n_chunks": int(chunk_idx_global),
855
  "accuracies_log": accuracies_log[-50:], # últimos 50
856
  "attention_log": attention_log[-20:],
 
 
 
 
 
 
857
  "final_state": kls.get_v2_metrics(),
858
  }
859
 
@@ -865,14 +1205,24 @@ def run_fase_punicão(
865
  kls: KohonenLearningSystemV2,
866
  hf_token: Optional[str],
867
  ) -> Dict[str, Any]:
868
- """Fase 2 — TREINAMENTO COM PUNIÇÃO: streaming 500 samples + V2 protocol.
869
-
870
- User requirement: "dataset para TREINAMENTO COM PUNIÇÃO ATIVA
871
- 'BrunoN-Dev/corpus-ptbr-v1'" + "meta mínima 500 para TREINAMENTO COM
872
- PUNIÇÃO" + "Cada evento de erro (punição) dispara 3 tentativas de ajuste.
873
- Em cada tentativa, 16 geradores paralelos de hipóteses produzem candidatos
874
- a delta. O melhor delta é aplicado ao MAPA-SOM. Após a aplicação,
875
- consolidação via EWC."
 
 
 
 
 
 
 
 
 
 
876
 
877
  Returns:
878
  Dict com métricas da fase TREINAMENTO COM PUNIÇÃO.
@@ -888,6 +1238,13 @@ def run_fase_punicão(
888
  logger.info(f" hyp_train_steps: {HYP_TRAIN_STEPS}")
889
  logger.info("=" * 80 + "\n")
890
 
 
 
 
 
 
 
 
891
  step = 0
892
  t_start = time.time()
893
  samples_processed = 0
@@ -897,6 +1254,8 @@ def run_fase_punicão(
897
  hypotheses_log: List[Dict[str, Any]] = []
898
  accuracies_log: List[Dict[str, Any]] = []
899
  delta_applications_log: List[Dict[str, Any]] = []
 
 
900
 
901
  try:
902
  chunk_iter = stream_dataset_in_chunks(
@@ -1005,6 +1364,50 @@ def run_fase_punicão(
1005
  "ewc_reference_set": bool(kls.som.old_weights_w is not None),
1006
  })
1007
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  step += 1
1009
  time.sleep(INTER_BATCH_PAUSE_S)
1010
  except Exception as e:
@@ -1024,6 +1427,13 @@ def run_fase_punicão(
1024
  )
1025
  if chunk_idx_global % 2 == 0:
1026
  aggressive_memory_cleanup()
 
 
 
 
 
 
 
1027
  except Exception as e:
1028
  logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
1029
  traceback.print_exc()
@@ -1052,6 +1462,12 @@ def run_fase_punicão(
1052
  "hypotheses_trainings": hypotheses_log,
1053
  "delta_applications": delta_applications_log,
1054
  "accuracies_log": accuracies_log[-50:],
 
 
 
 
 
 
1055
  "final_v2_state": kls.get_v2_metrics(),
1056
  }
1057
 
@@ -1592,11 +2008,26 @@ def main() -> int:
1592
  aggressive_storage_cleanup()
1593
 
1594
  # ========================================================================
1595
- # 14.5 FASE 2 — TREINAMENTO COM PUNIÇÃO (BrunoN-Dev/corpus-ptbr-v1, 500 samples)
1596
  # ========================================================================
1597
  if not conhecimento_result["storage_critical_stopped"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
1598
  logger.info("\n[V6.5-V2] Iniciando FASE 2 — TREINAMENTO COM PUNIÇÃO...")
1599
  punicão_result = run_fase_punicão(kls, hf_token)
 
 
1600
 
1601
  # Atualiza V2_PHASES_EVAL_PATH com ambas as fases
1602
  phases_eval = {
@@ -1735,6 +2166,7 @@ def main() -> int:
1735
  BIGRU_ROOT / "scripts" / "train_v6_5_v2.py",
1736
  SRC_ROOT / "bigru_t" / "model" / "kohonen_learning_system.py",
1737
  SRC_ROOT / "bigru_t" / "model" / "hyp_t.py",
 
1738
  SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
1739
  SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
1740
  ]
@@ -1797,12 +2229,52 @@ def main() -> int:
1797
  # ========================================================================
1798
  # 14.11 LIMPEZA final de armazenamento e worklog
1799
  # ========================================================================
 
 
1800
  logger.info("\n[V6.5-V2] Limpeza final de armazenamento...")
1801
  final_storage_cleanup = aggressive_storage_cleanup()
1802
  aggressive_memory_cleanup()
1803
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1804
  print("\n" + "=" * 80)
1805
- print("V6.5-V2-dynamic — TREINAMENTO CONCLUÍDO")
1806
  print("=" * 80)
1807
  print(f" FASE 1 CONHECIMENTO : {conhecimento_result['total_samples']} samples "
1808
  f"(meta: {conhecimento_result['meta_atingida']})")
@@ -1816,12 +2288,15 @@ def main() -> int:
1816
  print(f" HF upload : {hf_upload_result.get('uploaded', False)} "
1817
  f"({hf_upload_result.get('n_files', 0)} files)")
1818
  print(f" HF_TOKEN cleaned : True (env + scripts)")
 
1819
  print(f" Storage cleanup : {final_storage_cleanup['n_files_removed']} files, "
1820
  f"{final_storage_cleanup['mb_freed']:.1f}MB freed")
1821
  print(f" Final n_hypotheses : {kls.n_hypotheses} (active={kls.hypothesis_ensemble.active_count})")
1822
  print(f" Final n_trials : {kls.n_trials}")
1823
  print(f" Final hyp_steps : {kls.hyp_train_steps}")
1824
  print(f" N adaptations : {len(kls.get_adaptation_log())}")
 
 
1825
  print("=" * 80 + "\n")
1826
 
1827
  return 0
 
1
+ """train_v6_5_v2.py — V6.5-V2-metrics (2 Fases + Métricas SOM canônicas).
2
 
3
  ═══════════════════════════════════════════════════════════════════════════════
4
+ V6.5-V2-metrics — MÉTRICAS SOM CANÔNICAS + 2 FASES + ESTADO FASE1→FASE2
5
  ═══════════════════════════════════════════════════════════════════════════════
6
 
7
+ User requirements (latest, V6.5-V2-metrics):
8
+ 1. Organização e não duplicidade de módulos (versões velhas → deprecados/).
9
+ 2. Atualizações HF em LOTE (sobrescrever antigos desatualizados).
10
+ 3. Limpar Armazenamento e worklog.
11
+ 4. HF_TOKEN apagada do ambiente virtual e dos scripts enviados ao HF
12
  após uso.
13
+ 5. Usar streaming_datasets.py e ativar xeon_runtime.py.
14
+ 6. FASE 1 CONHECIMENTO (meta mínima 8000+ samples, 100 em 100) na
 
 
 
 
15
  sequência exata de 8 datasets.
16
+ 7. FASE 2 TREINAMENTO COM PUNIÇÃO ATIVA (meta mínima 2000+ samples,
17
  100 em 100) sobre BrunoN-Dev/corpus-ptbr-v1.
18
+ 8. FASE2 NUNCA acontece antes da FASE1. FASE2 carrega explicitamente
19
+ o estado do modelo concluído da FASE1.
20
+ 9. FASE2 deve treinar, ajustar dados e parâmetros do MAPA-SOM e
21
+ INFORMAR MÉTRICAS DE APRENDIZADO do modelo.
22
+ 10. Métricas SOM canônicas (8 métricas) monitoradas especialmente na
23
+ FASE2 PUNITIVA:
24
+ 10.1. Erro de Quantização (QE) — distância média ‖x_i - W_BMU(x_i)‖
25
+ 10.2. Erro Topológico (TE) — % de pares BMU/2ª-BMU não adjacentes
26
+ 10.3. Erro de Kaski-Lagus — combinação ponderada de QE + TE
27
+ 10.4. Variância Explicada — 1 - Var(residual)/Var(total)
28
+ 10.5. Colapso Topológico — pesos convergem para ponto ou linha
29
+ 10.6. Taxa de Neurônios Mortos — % de neurônios nunca BMU
30
+ 10.7. Estagnação do QE — slope ≈ 0 com QE em patamar elevado
31
+ 10.8. Cruzamento de Vizinhança — TE oscila ou cresce no final
32
+ 11. Limpeza de RAM e Armazenamento aprimorada (lógica + matemática).
33
+ 12. Não reduzir tempo, não gerar dados sintéticos.
34
+ 13. Após concluir: enviar scripts+arquivos+estado do modelo testados e
35
  aprovados ao HF (sobrescrevendo antigos).
36
 
37
+ V6.5-V2-metrics (alterações vs V6.5-V2-dynamic):
38
+ - NOVO: Módulo som_metrics.py com 8 métricas canônicas implementadas.
39
+ - NOVO: KohonenLearningSystemV2.compute_som_metrics() integra as métricas.
40
+ - NOVO: Histórico de métricas SOM (SOMMetricHistory) para detecção
41
+ de estagnação do QE e cruzamento de vizinhança.
42
+ - NOVO: FASE1 computa métricas após cada chunk (8000 samples).
43
+ - NOVO: FASE2 computa métricas após CADA BATCH (2000 samples, mais
44
+ frequentemente por ser fase punitiva).
45
+ - NOVO: load_fase1_state_into_kls() — carrega explicitamente estado
46
+ da FASE1 antes de iniciar FASE2.
47
+ - NOVO: aggressive_memory_cleanup() reporta RSS antes/depois.
48
+ - NOVO: aggressive_storage_cleanup() limpa __pycache__ + .tmp + logs >7dias.
49
+ - META: CONHECIMENTO 6000 → 8000; PUNIÇÃO 1000 → 2000.
50
  """
51
 
52
  from __future__ import annotations
 
114
  logger.info(f"[V6.5-V2] Xeon FP16 benchmark: {FP16_BENCH}")
115
 
116
  # ============================================================================
117
+ # 2. Configurações V6.5-V2-metrics-FIX (864 neurons, params restaurados a valores maiores)
118
  # ============================================================================
119
+ # User requirement (latest): "ATENÇÃO: retornar os parâmetros do modelo para
120
+ # os valores maiores e resolver falhas de lógica e de bugs que estejam causando
121
+ # alto consumo de memória sem distorcer a arquitetura Kohonen."
122
+ #
123
+ # Valores restaurados (V6.5-V2-metrics-FIX):
124
+ # HIDDEN_DIM: 256 → 1024 (restaurado)
125
+ # VOCAB_SIZE: 4096 → 16384 (restaurado)
126
+ # N_HYPOTHESES: 8 → 16 (restaurado)
127
+ # MAX_N_HYPOTHESES: 16 → 32 (restaurado)
128
+ # HYP_TRAIN_STEPS: 20 → 30 (restaurado)
129
+ # HYP_HIDDEN_DIM: 128 → 256 (restaurado)
130
+ #
131
+ # Para compensar o aumento de memória (sem distorcer Kohonen):
132
+ # 1. BUG FIX: _compute_classification_loss_with_som estava dentro de
133
+ # torch.no_grad(), matando o gradiente dos deltas. Corrigido.
134
+ # 2. VETORIZAÇÃO: train_hypotheses agora avalia 16 hipóteses em paralelo
135
+ # via batched matmul (n_hyp, P, N) em vez de loop que materializava
136
+ # 16 cópias do SOM.
137
+ # 3. aggressive_cleanup(): zera gradientes Adam, trunca históricos,
138
+ # gc.collect() em 3 passes.
139
+ # 4. Buffer sliding window: MAX_BUFFER_SIZE = 256 (preserva contexto recente
140
+ # sem crescer indefinidamente).
141
+ # 5. VQ-VAE-2 e W8A8 desabilitados por padrão (não essenciais para Kohonen).
142
  BATCH_SIZE = 16
143
  MAX_SEQ_LEN = 8
144
 
145
+ # V6.5-V2-metrics-FIX: restaurado para valores maiores (user requirement)
146
+ HIDDEN_DIM = 1024 # restaurado de 256
147
+ VOCAB_SIZE = 16384 # restaurado de 4096
148
+ SOM_GRID = (6, 6, 6, 4) # 864 neurons (arquitetura Kohonen preservada)
 
149
  T_MAX = 10000
150
  N_START = 10
151
  LAMBDA_EWC = 0.02
 
153
  SIGMA0 = 1.5
154
  DIM_CHOICE = "y"
155
 
156
+ # V2-dynamic — HypothesisEnsemble parameters (restaurados a valores maiores)
157
+ N_HYPOTHESES = 16 # restaurado de 8
158
+ MAX_N_HYPOTHESES = 32 # restaurado de 16
159
+ MIN_N_HYPOTHESES = 4 # limite inferior dinâmico
160
+ N_TRIALS = 3 # inicial
161
  MIN_N_TRIALS = 1
162
  MAX_N_TRIALS = 6
163
+ HYP_TRAIN_STEPS = 30 # restaurado de 20
164
+ MIN_HYP_TRAIN_STEPS = 10
165
+ MAX_HYP_TRAIN_STEPS = 80
166
  HYP_LR = 1e-4
167
+ HYP_HIDDEN_DIM = 256 # restaurado de 128
168
  LOSS_HISTORY_WINDOW = 8
169
  PUNISHMENT_WINDOW = 12
170
 
171
  # V2-dynamic-memory — Buffer sliding window (evita OOM em treino longo)
 
 
 
 
172
  MAX_BUFFER_SIZE = 256
173
 
174
  # Streaming — User requirement: "streaming de 100 em 100 samples"
175
  STREAM_BATCH_SIZE = 100
176
 
177
+ # V2-dynamic — Metas atualizadas (V6.5-V2-metrics): 8000 CONHECIMENTO + 2000 PUNIÇÃO
178
+ META_MINIMA_CONHECIMENTO = 8000
179
+ MAX_SAMPLES_PER_DATASET_CONHECIMENTO = 1000 # 8 × 1000 = 8000 ≥ 8000
180
+ META_MINIMA_PUNICAO = 2000
181
+ MAX_SAMPLES_PUNICAO = 2000
182
+
183
+ # User requirement: "não reduzir tempo e não gerar dados sintéticos" +
184
+ # "todo streaming (FASE1 e da FASE2) deve ter pausa para dar tempo de conclusão
185
+ # de processamento continuando após conclusão"
186
+ INTER_BATCH_PAUSE_S = 0.5 # aumentado de 0.3 (mais tempo para processamento)
187
+ INTER_DATASET_PAUSE_S = 2.0 # aumentado de 1.5
188
+ INTER_STREAM_BATCH_PAUSE_S = 1.0 # aumentado de 0.5 (pausa entre chunks de 100)
189
+ POST_PROCESSING_PAUSE_S = 1.5 # NOVO: pausa após processar cada chunk para
190
+ # dar tempo de conclusão de processamento
191
 
192
  # Storage critical
193
  STORAGE_CRITICAL_PCT = 90
 
321
 
322
 
323
  def aggressive_memory_cleanup() -> Dict[str, Any]:
324
+ """V6.5-V2-metrics — Limpeza agressiva de RAM.
325
+
326
+ User requirement: "analisar e aprimorar (logicamente e matematicamente)
327
+ no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
328
+
329
+ Implementação:
330
+ 1. gc.collect() — 2 passes (gerações 0+1 e 2)
331
+ 2. torch.cuda.empty_cache() (se CUDA disponível)
332
+ 3. Limpa caches internos do PyTorch (MKL, oneDNN)
333
+ 4. Reporta RSS antes/depois para confirmação
334
+ """
335
+ rss_before = get_process_rss_mb()
336
  gc.collect()
337
  gc.collect()
338
  try:
339
  import torch
340
  if torch.cuda.is_available():
341
  torch.cuda.empty_cache()
342
+ # V6.5-V2-metrics: limpa caches CPU do PyTorch
343
+ if hasattr(torch, "cpu"):
344
+ try:
345
+ # MKL/OneDNN primitive cache é limpo implicitamente por gc
346
+ pass
347
+ except Exception:
348
+ pass
349
  except Exception:
350
  pass
351
+ rss_after = get_process_rss_mb()
352
+ freed_mb = max(0.0, rss_before - rss_after)
353
+ return {
354
+ "gc_called": True,
355
+ "rss_before_mb": float(rss_before),
356
+ "rss_after_mb": float(rss_after),
357
+ "freed_mb": float(freed_mb),
358
+ "timestamp": datetime.now().isoformat(),
359
+ }
360
 
361
 
362
  def aggressive_storage_cleanup() -> Dict[str, Any]:
363
+ """V6.5-V2-metrics — Limpeza agressiva de Armazenamento.
364
+
365
+ User requirement: "analisar e aprimorar (logicamente e matematicamente)
366
+ no projeto limpeza de memória RAM e de Armazenamento (ao concluir)".
367
+
368
+ Implementação:
369
+ 1. Limpa locks HF (*.lock)
370
+ 2. Limpa downloads incompletos (*.incomplete)
371
+ 3. Limpa __pycache__ recursivamente
372
+ 4. Limpa arquivos .tmp
373
+ 5. Limpa logs antigos (>7 dias, se existirem)
374
+ 6. Reporta MB liberados
375
+ """
376
  n_removed = 0
377
  mb_freed = 0.0
378
+
379
+ # 1. HF cache locks e incompletos
380
  hf_cache = Path.home() / ".cache" / "huggingface"
381
  if hf_cache.exists():
382
+ for pattern in ["*.lock", "*.incomplete", "*.tmp"]:
383
+ for f in hf_cache.rglob(pattern):
384
+ try:
385
+ sz = f.stat().st_size
386
+ f.unlink()
387
+ n_removed += 1
388
+ mb_freed += sz / 1e6
389
+ except Exception:
390
+ pass
391
+
392
+ # 2. __pycache__ recursivo no BiGRU_T_version
393
+ bigru_root = BIGRU_ROOT
394
+ for cache_dir in bigru_root.rglob("__pycache__"):
395
+ if "deprecados" in str(cache_dir):
396
+ continue
397
+ try:
398
+ for f in cache_dir.iterdir():
399
+ try:
400
+ sz = f.stat().st_size
401
+ f.unlink()
402
+ n_removed += 1
403
+ mb_freed += sz / 1e6
404
+ except Exception:
405
+ pass
406
+ cache_dir.rmdir()
407
+ except Exception:
408
+ pass
409
+
410
+ # 3. Arquivos .tmp no BiGRU_T_version
411
+ for f in bigru_root.rglob("*.tmp"):
412
+ if "deprecados" in str(f):
413
+ continue
414
+ try:
415
+ sz = f.stat().st_size
416
+ f.unlink()
417
+ n_removed += 1
418
+ mb_freed += sz / 1e6
419
+ except Exception:
420
+ pass
421
+
422
+ # 4. Logs antigos (>7 dias) em logs/
423
+ logs_dir = PROJECT_ROOT / "logs"
424
+ if logs_dir.exists():
425
+ cutoff = time.time() - 7 * 24 * 3600 # 7 dias
426
+ for f in logs_dir.glob("*.log"):
427
  try:
428
+ if f.stat().st_mtime < cutoff:
429
+ sz = f.stat().st_size
430
+ f.unlink()
431
+ n_removed += 1
432
+ mb_freed += sz / 1e6
433
  except Exception:
434
  pass
435
+
436
  return {
437
  "n_files_removed": n_removed,
438
  "mb_freed": float(mb_freed),
 
548
 
549
 
550
  # ============================================================================
551
+ # 5.5 Load FASE1 state into KLS (FASE2 must use FASE1's completed state)
552
+ # ============================================================================
553
+ def load_fase1_state_into_kls(
554
+ kls: KohonenLearningSystemV2,
555
+ state_path: Path,
556
+ ) -> Dict[str, Any]:
557
+ """V6.5-V2-metrics — Carrega estado do modelo concluído da FASE1 no KLS.
558
+
559
+ User requirement: "esta FASE2 (nunca acontece antes da FASE1) deve usar do
560
+ arquivo de Estado do MODELO concluído da FASE1 e a FASE2 tem função: de
561
+ treinar, ajustar dados e os parâmetros do MAPA-SOM de redes Kohonen,
562
+ informar métricas de aprendizado do modelo".
563
+
564
+ Esta função é chamada explicitamente antes de iniciar a FASE 2, garantindo
565
+ que a FASE 2 herde todo o conhecimento acumulado da FASE 1 (pesos do SOM,
566
+ EWC references, classifier, hypothesis ensemble, delta_scale, label_registry).
567
+
568
+ Args:
569
+ kls: instância KohonenLearningSystemV2 (mesma usada na FASE1).
570
+ state_path: caminho para o arquivo .pt salvo ao final da FASE1.
571
+
572
+ Returns:
573
+ Dict com status do carregamento.
574
+ """
575
+ import torch
576
+ if not state_path.exists():
577
+ logger.warning(
578
+ f"[V6.5-V2-metrics] FASE1 state file not found: {state_path}. "
579
+ f"FASE2 will run with the in-memory KLS state (already inherited "
580
+ f"from FASE1 since same KLS instance is used)."
581
+ )
582
+ return {
583
+ "loaded": False,
584
+ "reason": "state_file_not_found",
585
+ "path": str(state_path),
586
+ "in_memory_state_used": True,
587
+ }
588
+ try:
589
+ state = torch.load(state_path, map_location="cpu", weights_only=False)
590
+ meta = state.get("_meta", {})
591
+ logger.info(
592
+ f"[V6.5-V2-metrics] Loading FASE1 state from {state_path}: "
593
+ f"reason={meta.get('reason', '?')}, "
594
+ f"step={meta.get('step', 0)}, "
595
+ f"total_samples={meta.get('total_samples', 0)}, "
596
+ f"timestamp={meta.get('timestamp', '?')}"
597
+ )
598
+
599
+ # Carrega pesos do SOM (preserva o aprendizado da FASE1)
600
+ if "som_weights" in state and state["som_weights"] is not None:
601
+ kls.som.weights = state["som_weights"].clone().float()
602
+ logger.info(
603
+ f"[V6.5-V2-metrics] SOM weights loaded: "
604
+ f"shape={tuple(kls.som.weights.shape)}, "
605
+ f"norm={float(kls.som.weights.norm().item()):.4f}"
606
+ )
607
+ # Carrega EWC references (se existirem da FASE1)
608
+ if "som_old_weights_w" in state and state["som_old_weights_w"] is not None:
609
+ kls.som.old_weights_w = state["som_old_weights_w"].clone().float()
610
+ logger.info("[V6.5-V2-metrics] SOM EWC old_weights_w loaded.")
611
+ if "som_fisher_w" in state and state["som_fisher_w"] is not None:
612
+ kls.som.fisher_w = state["som_fisher_w"].clone().float()
613
+ logger.info("[V6.5-V2-metrics] SOM EWC fisher_w loaded.")
614
+
615
+ # Carrega embedding
616
+ if "embedding_state" in state and state["embedding_state"] is not None:
617
+ kls.embedding.weight.data = state["embedding_state"]["weight"].clone().float()
618
+ logger.info("[V6.5-V2-metrics] Embedding weights loaded.")
619
+
620
+ # Carrega classifier (se treinado na FASE1)
621
+ if "classifier_state" in state and state["classifier_state"] is not None:
622
+ if kls.classifier is None:
623
+ # Recria o classifier com a mesma arquitetura
624
+ kls.classifier = HypothesisClassifier(
625
+ input_dim=kls.som_neuron_count,
626
+ hidden_dims=kls.hypothesis_hidden,
627
+ )
628
+ kls.classifier.load_state_dict(state["classifier_state"])
629
+ kls.classifier_trained = True
630
+ logger.info("[V6.5-V2-metrics] Classifier state loaded.")
631
+
632
+ # Carrega HypothesisEnsemble
633
+ if "hypothesis_ensemble_state" in state and state["hypothesis_ensemble_state"] is not None:
634
+ kls.hypothesis_ensemble.load_state_dict(state["hypothesis_ensemble_state"])
635
+ logger.info(
636
+ f"[V6.5-V2-metrics] HypothesisEnsemble state loaded: "
637
+ f"n_generators={len(kls.hypothesis_ensemble.generators)}, "
638
+ f"active={kls.hypothesis_ensemble.active_count}"
639
+ )
640
+
641
+ # Carrega delta_scale
642
+ if "delta_scale" in state and state["delta_scale"] is not None:
643
+ kls.delta_scale.data = state["delta_scale"].clone().float()
644
+ logger.info(
645
+ f"[V6.5-V2-metrics] delta_scale loaded: {kls.delta_scale.item():.4f}"
646
+ )
647
+
648
+ # Carrega label_registry (preserva mapeamentos dinâmicos)
649
+ if "label_registry" in state and state["label_registry"] is not None:
650
+ kls.label_registry = state["label_registry"]
651
+ logger.info(
652
+ f"[V6.5-V2-metrics] label_registry loaded: "
653
+ f"{len(kls.label_registry)} datasets"
654
+ )
655
+
656
+ # Restaura time_counter (preserva coordenada temporal absoluta)
657
+ if "time_counter" in meta:
658
+ kls.time_counter = int(meta["time_counter"])
659
+ logger.info(
660
+ f"[V6.5-V2-metrics] time_counter restored: {kls.time_counter}"
661
+ )
662
+
663
+ logger.info(
664
+ f"[V6.5-V2-metrics] ✓ FASE1 state loaded successfully. "
665
+ f"KLS is now ready for FASE2 (PUNITIVE) training."
666
+ )
667
+ return {
668
+ "loaded": True,
669
+ "path": str(state_path),
670
+ "meta": meta,
671
+ "in_memory_state_used": False,
672
+ }
673
+ except Exception as e:
674
+ logger.error(
675
+ f"[V6.5-V2-metrics] Failed to load FASE1 state: {e}. "
676
+ f"FASE2 will run with the in-memory KLS state (already inherited "
677
+ f"from FASE1 since same KLS instance is used)."
678
+ )
679
+ traceback.print_exc()
680
+ return {
681
+ "loaded": False,
682
+ "reason": f"load_error: {str(e)[:200]}",
683
+ "path": str(state_path),
684
+ "in_memory_state_used": True,
685
+ }
686
+
687
+
688
+ # ============================================================================
689
+ # 6. Streaming helper — chunks of 100 samples (V6.5-V2-metrics: O(N) not O(N²))
690
  # ============================================================================
691
  def stream_dataset_in_chunks(
692
  dataset_name: str,
 
695
  hf_token: Optional[str] = None,
696
  timeout_per_chunk: int = 60,
697
  ) -> Iterator[List[str]]:
698
+ """V6.5-V2-metrics — Stream a dataset in chunks of `chunk_size` samples.
699
+
700
+ User requirement: "streaming de 100 em 100 samples até esgonar".
701
+
702
+ V6.5-V2-metrics FIX: A versão anterior re-chamava stream_dataset() para
703
+ cada chunk com seed_offset = yielded_total, fazendo com que o HF
704
+ re-streaming recapitulasse todas as amostras anteriores a cada chunk
705
+ (custo O(N²) em banda e cache). Esta versão cria UM ÚNICO iterador
706
+ por dataset e vai colhendo chunks de `chunk_size` em `chunk_size`,
707
+ sem recomeçar do zero. Isto reduz drasticamente o uso de memória
708
+ (que antes crescia linearmente com o nº de chunks devido ao cache
709
+ acumulado de metadados parquet) e permite atingir as metas de
710
+ 8000 samples CONHECIMENTO + 2000 samples PUNIÇÃO dentro do limite
711
+ de 4GB RAM do cgroup.
712
  """
713
  try:
714
  from bigru_t.data.streaming_datasets import stream_dataset
 
718
 
719
  yielded_total = 0
720
  chunk_idx = 0
721
+ chunk: List[str] = []
722
+ t_start = time.time()
723
+
724
+ try:
725
+ sample_iter = stream_dataset(
726
+ dataset_name,
727
+ max_samples=max_total,
728
+ hf_token=hf_token,
729
+ )
730
+ for sample in sample_iter:
731
+ if yielded_total >= max_total:
732
+ break
733
+ # Timeout check: se demorar demais, yield o que temos e continua
734
+ if time.time() - t_start > timeout_per_chunk * (chunk_idx + 1):
735
+ logger.warning(
736
+ f"[V6.5-V2] {dataset_name} chunk {chunk_idx+1} "
737
+ f"timeout after {timeout_per_chunk}s "
738
+ f"(collected {len(chunk)}/{chunk_size})"
739
+ )
740
+ if chunk:
741
+ yield chunk
742
+ yielded_total += len(chunk)
743
+ chunk_idx += 1
744
+ chunk = []
745
+ if time.time() - t_start > timeout_per_chunk * (chunk_idx + 2):
746
+ # Segundo timeout consecutivo — desiste deste dataset
747
  logger.warning(
748
+ f"[V6.5-V2] {dataset_name} exhausted after {yielded_total} samples "
749
+ f"({chunk_idx} chunks) due to repeated timeouts"
 
750
  )
751
+ return
752
+ continue
753
+
754
+ if sample.raw_text and len(sample.raw_text.strip()) > 0:
755
+ chunk.append(sample.raw_text.strip()[:200])
756
+ if len(chunk) >= chunk_size:
 
 
 
 
 
757
  yield chunk
758
  yielded_total += len(chunk)
759
  chunk_idx += 1
760
+ logger.info(
761
+ f"[V6.5-V2] {dataset_name} chunk {chunk_idx} done: "
762
+ f"{len(chunk)} samples (total: {yielded_total}/{max_total})"
763
+ )
764
+ chunk = []
765
+ except Exception as e:
766
+ logger.warning(f"[V6.5-V2] {dataset_name} streaming error: {e}")
767
+ if chunk:
768
+ yield chunk
769
+ yielded_total += len(chunk)
770
+ chunk_idx += 1
771
+ return
772
 
773
+ # Yield any remaining partial chunk
774
+ if chunk:
775
  yield chunk
776
  yielded_total += len(chunk)
777
  chunk_idx += 1
778
+
779
+ logger.info(
780
+ f"[V6.5-V2] {dataset_name} exhausted after {yielded_total} samples "
781
+ f"({chunk_idx} chunks)"
782
+ )
783
 
784
 
785
  # ============================================================================
 
952
  chunk_idx_global = 0
953
  accuracies_log: List[Dict[str, Any]] = []
954
  attention_log: List[Dict[str, Any]] = []
955
+ # V6.5-V2-metrics — log de métricas SOM durante CONHECIMENTO
956
+ som_metrics_log: List[Dict[str, Any]] = []
957
+
958
+ # V6.5-V2-metrics — reseta histórico de métricas SOM no início de CONHECIMENTO
959
+ kls.reset_som_metric_history()
960
+ logger.info("[V6.5-V2-metrics] Histórico de métricas SOM resetado para FASE 1.")
961
 
962
  for ds_idx, dataset_name in enumerate(CONHECIMENTO_DATASETS):
963
  if storage_critical_stopped:
 
1111
  f"[V6.5-V2] Buffer trimmed: -{overflow} → "
1112
  f"{len(kls.buffer_4d)} samples (cap={MAX_BUFFER_SIZE})"
1113
  )
1114
+
1115
+ # V6.5-V2-metrics — Computa métricas SOM após cada chunk
1116
+ # (QE, TE, Kaski-Lagus, Variância Explicada, Dead Neurons,
1117
+ # Colapso Topológico, Estagnação QE, Cruzamento Vizinhança).
1118
+ # User requirement: "ANALISAR matematicamente e logicamente e
1119
+ # inserir melhorias para os scripts das métricas de aprendizado
1120
+ # e indicadores de falha em Mapas Auto-Organizáveis".
1121
+ try:
1122
+ som_metrics = kls.compute_som_metrics()
1123
+ som_metrics_log.append({
1124
+ "step": step,
1125
+ "chunk_idx": chunk_idx_global,
1126
+ "dataset": dataset_name,
1127
+ "quantization_error": float(som_metrics["quantization_error"]),
1128
+ "topological_error": float(som_metrics["topological_error"]),
1129
+ "kaski_lagus_error": float(som_metrics["kaski_lagus_error"]),
1130
+ "explained_variance_share": float(som_metrics["explained_variance_share"]),
1131
+ "overall_health": som_metrics["overall_health"],
1132
+ "n_failure_indicators": int(som_metrics["n_failure_indicators"]),
1133
+ "failure_indicators": som_metrics["failure_indicators"],
1134
+ "collapse_severity": som_metrics["topological_collapse"].get("severity", "none"),
1135
+ "dead_neuron_rate": float(som_metrics["dead_neuron_rate"].get("dead_neuron_rate", 0.0)),
1136
+ })
1137
+ if chunk_idx_global % 5 == 0:
1138
+ logger.info(
1139
+ f"[V6.5-V2-metrics] chunk {chunk_idx_global}: "
1140
+ f"QE={som_metrics['quantization_error']:.4f}, "
1141
+ f"TE={som_metrics['topological_error']:.4f}, "
1142
+ f"VE={som_metrics['explained_variance_share']:.4f}, "
1143
+ f"health={som_metrics['overall_health']}, "
1144
+ f"failures={len(som_metrics['failure_indicators'])}"
1145
+ )
1146
+ except Exception as e:
1147
+ logger.warning(f"[V6.5-V2-metrics] Failed to compute SOM metrics: {e}")
1148
+
1149
  # Aggressive memory cleanup between chunks
1150
  if chunk_idx_global % 2 == 0:
1151
  aggressive_memory_cleanup()
1152
+ # V6.5-V2-metrics-FIX: também chama aggressive_cleanup do KLS
1153
+ # para zerar gradientes Adam e truncar históricos.
1154
+ try:
1155
+ kls.aggressive_cleanup()
1156
+ except Exception:
1157
+ pass
1158
+ # V6.5-V2-metrics-FIX: pausa pós-processamento para dar tempo
1159
+ # de conclusão (user requirement: "todo streaming deve ter
1160
+ # pausa para dar tempo de conclusão de processamento").
1161
+ time.sleep(POST_PROCESSING_PAUSE_S)
1162
  except Exception as e:
1163
  logger.error(f"[V6.5-V2] Dataset {dataset_name} failed: {e}")
1164
  traceback.print_exc()
 
1188
  "n_chunks": int(chunk_idx_global),
1189
  "accuracies_log": accuracies_log[-50:], # últimos 50
1190
  "attention_log": attention_log[-20:],
1191
+ # V6.5-V2-metrics — log de métricas SOM durante CONHECIMENTO
1192
+ "som_metrics_log": som_metrics_log[-30:], # últimos 30 chunks
1193
+ "som_metrics_final": (
1194
+ kls.compute_som_metrics() if kls.buffer_4d else {}
1195
+ ),
1196
+ "som_metric_history": kls.get_som_metric_history(),
1197
  "final_state": kls.get_v2_metrics(),
1198
  }
1199
 
 
1205
  kls: KohonenLearningSystemV2,
1206
  hf_token: Optional[str],
1207
  ) -> Dict[str, Any]:
1208
+ """Fase 2 — TREINAMENTO COM PUNIÇÃO: streaming 2000 samples + V2 protocol.
1209
+
1210
+ User requirement: "FASE2 TREINAMENTO (meta mínima 2000 samples ou mais)
1211
+ COM PUNIÇÃO ATIVA para 'BrunoN-Dev/corpus-ptbr-v1' de 100 em 100 samples" +
1212
+ "esta FASE2 (nunca acontece antes da FASE1) deve usar do arquivo de Estado
1213
+ do MODELO concluído da FASE1 e a FASE2 tem função: de treinar, ajustar
1214
+ dados e os parâmetros do MAPA-SOM de redes Kohonen, informar métricas de
1215
+ aprendizado do modelo" +
1216
+
1217
+ Métricas SOM canônicas monitoradas (user requirement):
1218
+ 1.1. Erro de Quantização (QE)
1219
+ 1.2. Erro Topológico (TE)
1220
+ 1.3. Erro de Kaski-Lagus
1221
+ 1.4. Variância Explicada
1222
+ 2.1. Colapso Topológico
1223
+ 2.2. Taxa de Neurônios Mortos
1224
+ 2.3. Estagnação do QE
1225
+ 2.4. Cruzamento de Vizinhança
1226
 
1227
  Returns:
1228
  Dict com métricas da fase TREINAMENTO COM PUNIÇÃO.
 
1238
  logger.info(f" hyp_train_steps: {HYP_TRAIN_STEPS}")
1239
  logger.info("=" * 80 + "\n")
1240
 
1241
+ # V6.5-V2-metrics — reseta histórico de métricas SOM no início da FASE 2
1242
+ # (PUNITIVA tem seu próprio histórico independente da FASE 1, para que
1243
+ # Estagnação do QE e Cruzamento de Vizinhança sejam avaliados
1244
+ # exclusivamente sobre o regime punitivo).
1245
+ kls.reset_som_metric_history()
1246
+ logger.info("[V6.5-V2-metrics] Histórico de métricas SOM resetado para FASE 2 (PUNITIVA).")
1247
+
1248
  step = 0
1249
  t_start = time.time()
1250
  samples_processed = 0
 
1254
  hypotheses_log: List[Dict[str, Any]] = []
1255
  accuracies_log: List[Dict[str, Any]] = []
1256
  delta_applications_log: List[Dict[str, Any]] = []
1257
+ # V6.5-V2-metrics — log de métricas SOM durante PUNIÇÃO (computadas a cada batch)
1258
+ som_metrics_log: List[Dict[str, Any]] = []
1259
 
1260
  try:
1261
  chunk_iter = stream_dataset_in_chunks(
 
1364
  "ewc_reference_set": bool(kls.som.old_weights_w is not None),
1365
  })
1366
 
1367
+ # V6.5-V2-metrics — Computa métricas SOM após cada batch
1368
+ # durante a FASE 2 PUNITIVA. User requirement:
1369
+ # "ANALISAR matematicamente e logicamente e inserir melhorias
1370
+ # para os scripts das métricas de aprendizado e indicadores
1371
+ # de falha em Mapas Auto-Organizáveis (SOM / Redes de Kohonen)
1372
+ # avaliam a fidelidade de representação dos dados e a
1373
+ # preservação da vizinhança topológica".
1374
+ try:
1375
+ som_metrics = kls.compute_som_metrics()
1376
+ som_metrics_log.append({
1377
+ "step": step,
1378
+ "chunk_idx": chunk_idx_global,
1379
+ "action": action,
1380
+ "punishment_count": int(kls.punishment_count),
1381
+ "quantization_error": float(som_metrics["quantization_error"]),
1382
+ "topological_error": float(som_metrics["topological_error"]),
1383
+ "kaski_lagus_error": float(som_metrics["kaski_lagus_error"]),
1384
+ "explained_variance_share": float(som_metrics["explained_variance_share"]),
1385
+ "overall_health": som_metrics["overall_health"],
1386
+ "n_failure_indicators": int(som_metrics["n_failure_indicators"]),
1387
+ "failure_indicators": som_metrics["failure_indicators"],
1388
+ "collapse_severity": som_metrics["topological_collapse"].get("severity", "none"),
1389
+ "collapse_effective_rank": float(som_metrics["topological_collapse"].get("effective_rank", 0.0)),
1390
+ "dead_neuron_rate": float(som_metrics["dead_neuron_rate"].get("dead_neuron_rate", 0.0)),
1391
+ "n_dead_neurons": int(som_metrics["dead_neuron_rate"].get("n_dead", 0)),
1392
+ "n_active_neurons": int(som_metrics["dead_neuron_rate"].get("n_active", 0)),
1393
+ "qe_stagnation": som_metrics.get("qe_stagnation", {}),
1394
+ "neighborhood_crossing": som_metrics.get("neighborhood_crossing", {}),
1395
+ })
1396
+ # Log detalhado a cada 5 steps OU quando há falha
1397
+ if step % 5 == 0 or som_metrics["n_failure_indicators"] > 0:
1398
+ logger.info(
1399
+ f"[V6.5-V2-metrics] PUNIÇÃO step {step}: "
1400
+ f"QE={som_metrics['quantization_error']:.4f}, "
1401
+ f"TE={som_metrics['topological_error']:.4f}, "
1402
+ f"KL={som_metrics['kaski_lagus_error']:.4f}, "
1403
+ f"VE={som_metrics['explained_variance_share']:.4f}, "
1404
+ f"dead={som_metrics['dead_neuron_rate'].get('dead_neuron_rate', 0.0):.3f}, "
1405
+ f"health={som_metrics['overall_health']}, "
1406
+ f"failures={som_metrics['failure_indicators']}"
1407
+ )
1408
+ except Exception as e:
1409
+ logger.warning(f"[V6.5-V2-metrics] Failed to compute SOM metrics: {e}")
1410
+
1411
  step += 1
1412
  time.sleep(INTER_BATCH_PAUSE_S)
1413
  except Exception as e:
 
1427
  )
1428
  if chunk_idx_global % 2 == 0:
1429
  aggressive_memory_cleanup()
1430
+ # V6.5-V2-metrics-FIX: também chama aggressive_cleanup do KLS
1431
+ try:
1432
+ kls.aggressive_cleanup()
1433
+ except Exception:
1434
+ pass
1435
+ # V6.5-V2-metrics-FIX: pausa pós-processamento (user requirement)
1436
+ time.sleep(POST_PROCESSING_PAUSE_S)
1437
  except Exception as e:
1438
  logger.error(f"[V6.5-V2] PUNIÇÃO dataset failed: {e}")
1439
  traceback.print_exc()
 
1462
  "hypotheses_trainings": hypotheses_log,
1463
  "delta_applications": delta_applications_log,
1464
  "accuracies_log": accuracies_log[-50:],
1465
+ # V6.5-V2-metrics — log de métricas SOM durante PUNIÇÃO
1466
+ "som_metrics_log": som_metrics_log[-100:], # últimos 100 batches
1467
+ "som_metrics_final": (
1468
+ kls.compute_som_metrics() if kls.buffer_4d else {}
1469
+ ),
1470
+ "som_metric_history": kls.get_som_metric_history(),
1471
  "final_v2_state": kls.get_v2_metrics(),
1472
  }
1473
 
 
2008
  aggressive_storage_cleanup()
2009
 
2010
  # ========================================================================
2011
+ # 14.5 FASE 2 — TREINAMENTO COM PUNIÇÃO (BrunoN-Dev/corpus-ptbr-v1, 2000 samples)
2012
  # ========================================================================
2013
  if not conhecimento_result["storage_critical_stopped"]:
2014
+ # V6.5-V2-metrics — Carrega explicitamente o estado do modelo concluído
2015
+ # da FASE1 no KLS antes de iniciar a FASE2.
2016
+ # User requirement: "esta FASE2 (nunca acontece antes da FASE1) deve
2017
+ # usar do arquivo de Estado do MODELO concluído da FASE1".
2018
+ # Como usamos a mesma instância KLS, o estado em memória já é o da
2019
+ # FASE1, mas carregamos do disco explicitamente para:
2020
+ # (a) validar que o arquivo .pt está íntegro;
2021
+ # (b) permitir que um processo separado (apenas FASE2) seja iniciado
2022
+ # do zero apontando para o estado salvo da FASE1.
2023
+ logger.info("\n[V6.5-V2-metrics] Carregando estado do modelo da FASE1 (FASE2 uses FASE1 completed state)...")
2024
+ fase1_state_path = BIGRU_ROOT / "v6_5_v2_model_states_after_conhecimento.pt"
2025
+ fase1_load_result = load_fase1_state_into_kls(kls, fase1_state_path)
2026
+
2027
  logger.info("\n[V6.5-V2] Iniciando FASE 2 — TREINAMENTO COM PUNIÇÃO...")
2028
  punicão_result = run_fase_punicão(kls, hf_token)
2029
+ # Anexa o resultado do carregamento da FASE1 no relatório da FASE2
2030
+ punicão_result["fase1_state_load"] = fase1_load_result
2031
 
2032
  # Atualiza V2_PHASES_EVAL_PATH com ambas as fases
2033
  phases_eval = {
 
2166
  BIGRU_ROOT / "scripts" / "train_v6_5_v2.py",
2167
  SRC_ROOT / "bigru_t" / "model" / "kohonen_learning_system.py",
2168
  SRC_ROOT / "bigru_t" / "model" / "hyp_t.py",
2169
+ SRC_ROOT / "bigru_t" / "model" / "som_metrics.py", # V6.5-V2-metrics
2170
  SRC_ROOT / "bigru_t" / "data" / "streaming_datasets.py",
2171
  SRC_ROOT / "bigru_t" / "utils" / "xeon_runtime.py",
2172
  ]
 
2229
  # ========================================================================
2230
  # 14.11 LIMPEZA final de armazenamento e worklog
2231
  # ========================================================================
2232
+ # User requirement: "limpar Armazenamento e worklog (remover imediatamente
2233
+ # todos os arquivos *.pt e não armazenar mais arquivos no worklog)"
2234
  logger.info("\n[V6.5-V2] Limpeza final de armazenamento...")
2235
  final_storage_cleanup = aggressive_storage_cleanup()
2236
  aggressive_memory_cleanup()
2237
 
2238
+ # V6.5-V2-metrics-FIX: Remove TODOS os arquivos .pt do projeto após upload
2239
+ # ao HF (user requirement: "remover imediatamente todos os arquivos *.pt
2240
+ # e não armazenar mais arquivos no worklog"). O estado do modelo já foi
2241
+ # enviado ao HF e pode ser recuperado de lá para continuar treinamento.
2242
+ logger.info("[V6.5-V2-metrics-FIX] Removendo TODOS os arquivos .pt locais...")
2243
+ pt_files_removed: List[str] = []
2244
+ for pt_file in PROJECT_ROOT.rglob("*.pt"):
2245
+ try:
2246
+ rel_path = str(pt_file.relative_to(PROJECT_ROOT))
2247
+ pt_file.unlink()
2248
+ pt_files_removed.append(rel_path)
2249
+ logger.info(f" removed .pt: {rel_path}")
2250
+ except Exception as e:
2251
+ logger.warning(f" failed to remove {pt_file}: {e}")
2252
+ logger.info(
2253
+ f"[V6.5-V2-metrics-FIX] {len(pt_files_removed)} .pt files removed."
2254
+ )
2255
+
2256
+ # Atualiza worklog.md (sem referenciar .pt files)
2257
+ try:
2258
+ worklog_path = PROJECT_ROOT / "worklog.md"
2259
+ worklog_content = f"""# worklog.md — V6.5-V2-metrics-FIX
2260
+
2261
+ Updated: {datetime.now().isoformat()}
2262
+
2263
+ ## Status
2264
+ - FASE 1 CONHECIMENTO: {conhecimento_result['total_samples']} samples (meta: {conhecimento_result['meta_atingida']})
2265
+ - FASE 2 PUNIÇÃO: {punicão_result.get('total_samples', 0)} samples (meta: {punicão_result.get('meta_atingida', False)})
2266
+ - HF upload: {hf_upload_result.get('uploaded', False)} ({hf_upload_result.get('n_files', 0)} files)
2267
+ - HF_TOKEN: cleaned from env + scripts
2268
+ - .pt files: ALL removed from local storage ({len(pt_files_removed)} files)
2269
+ - Model state available in HF repo: {HF_REPO_ID}
2270
+ """
2271
+ worklog_path.write_text(worklog_content, encoding="utf-8")
2272
+ logger.info(f"[V6.5-V2-metrics-FIX] worklog.md updated (no .pt references)")
2273
+ except Exception as e:
2274
+ logger.warning(f"[V6.5-V2-metrics-FIX] Failed to update worklog: {e}")
2275
+
2276
  print("\n" + "=" * 80)
2277
+ print("V6.5-V2-metrics-FIX — TREINAMENTO CONCLUÍDO")
2278
  print("=" * 80)
2279
  print(f" FASE 1 CONHECIMENTO : {conhecimento_result['total_samples']} samples "
2280
  f"(meta: {conhecimento_result['meta_atingida']})")
 
2288
  print(f" HF upload : {hf_upload_result.get('uploaded', False)} "
2289
  f"({hf_upload_result.get('n_files', 0)} files)")
2290
  print(f" HF_TOKEN cleaned : True (env + scripts)")
2291
+ print(f" .pt files removed : {len(pt_files_removed)} (ALL local .pt cleaned)")
2292
  print(f" Storage cleanup : {final_storage_cleanup['n_files_removed']} files, "
2293
  f"{final_storage_cleanup['mb_freed']:.1f}MB freed")
2294
  print(f" Final n_hypotheses : {kls.n_hypotheses} (active={kls.hypothesis_ensemble.active_count})")
2295
  print(f" Final n_trials : {kls.n_trials}")
2296
  print(f" Final hyp_steps : {kls.hyp_train_steps}")
2297
  print(f" N adaptations : {len(kls.get_adaptation_log())}")
2298
+ print(f" HIDDEN_DIM : {HIDDEN_DIM} (restored)")
2299
+ print(f" VOCAB_SIZE : {VOCAB_SIZE} (restored)")
2300
  print("=" * 80 + "\n")
2301
 
2302
  return 0
xeon_runtime.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ]