V6: KohonenSOM4D 4D em orq_cell (8 BiGRU × 4 sem × 4x4 grid = 512 protótipos) + WindowContext 2M (ToMe + KNN IVF) + MTP (K=4) + MTPAdaptiveAttention + DynamicRepetitionPenalty + ElasticWeightConsolidation + data_augmentation + RLHF Possibility Mapping (Φ = Θ + η·r·σ(trust·τ_T)·Σwᵢgᵢ) + ReasoningEngine 8-fase + Tools (cache + mutation + agent) + optimized_pipeline + langchain_adapter
Browse files- src/bigru_t/data/data_augmentation.py +161 -0
- src/bigru_t/inference/langchain_adapter.py +218 -0
- src/bigru_t/inference/optimized_pipeline.py +251 -0
- src/bigru_t/model/mtp.py +503 -0
- src/bigru_t/model/orq_cell.py +408 -27
- src/bigru_t/model/window_context.py +371 -0
- src/bigru_t/reasoning/reasoning_system.py +281 -0
- src/bigru_t/tools/__init__.py +12 -0
- src/bigru_t/tools/tool_agent.py +224 -0
- src/bigru_t/tools/tool_cache.py +143 -0
- src/bigru_t/tools/tool_mutation.py +193 -0
- src/bigru_t/training/elastic_weight_consolidation.py +281 -0
- src/bigru_t/training/rlhf_possibility_mapping.py +387 -0
- v5_upload_report.json +60 -0
- v6_report.json +159 -0
src/bigru_t/data/data_augmentation.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""data_augmentation.py — Data Augmentation para BiGRU_T_version V6.
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/data_augmentation.py.
|
| 4 |
+
|
| 5 |
+
Estratégias:
|
| 6 |
+
1. Synonym replacement (placeholder — usa mapeamento estático simples)
|
| 7 |
+
2. Random deletion (remove tokens com prob p)
|
| 8 |
+
3. Random swap (troca 2 tokens de posição)
|
| 9 |
+
4. Token dropout (substitui por PAD com prob p)
|
| 10 |
+
5. Back-translation simulada (aplica ruído gaussiano nos embeddings)
|
| 11 |
+
6. Mixup (combina 2 samples: x_mix = λ·x_1 + (1-λ)·x_2)
|
| 12 |
+
|
| 13 |
+
V6 melhoria vs V13.9.2:
|
| 14 |
+
* Vetorização total (sem loops Python sobre tokens)
|
| 15 |
+
* Mixup com λ ~ Beta(α, α) — diferenciável
|
| 16 |
+
* Compatível com tokens int (não altera vocab) para estratégias 1-4
|
| 17 |
+
* Compatível com embeddings (float) para estratégias 5-6
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
from typing import Optional
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def random_deletion_tokens(
|
| 27 |
+
input_ids: torch.Tensor,
|
| 28 |
+
pad_id: int = 0,
|
| 29 |
+
p: float = 0.1,
|
| 30 |
+
) -> torch.Tensor:
|
| 31 |
+
"""Deleta tokens aleatoriamente (substitui por PAD).
|
| 32 |
+
|
| 33 |
+
Args:
|
| 34 |
+
input_ids: (batch, T) — tokens int
|
| 35 |
+
pad_id: ID do token PAD
|
| 36 |
+
p: probabilidade de deletar cada token
|
| 37 |
+
|
| 38 |
+
Returns:
|
| 39 |
+
(batch, T) — tokens com alguns substituídos por pad_id
|
| 40 |
+
"""
|
| 41 |
+
if p <= 0:
|
| 42 |
+
return input_ids
|
| 43 |
+
mask = torch.bernoulli(torch.full_like(input_ids, p, dtype=torch.float)).bool()
|
| 44 |
+
return input_ids.masked_fill(mask, pad_id)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def random_swap_tokens(
|
| 48 |
+
input_ids: torch.Tensor,
|
| 49 |
+
n_swaps: int = 1,
|
| 50 |
+
) -> torch.Tensor:
|
| 51 |
+
"""Troca n pares de tokens de posição (por batch).
|
| 52 |
+
|
| 53 |
+
Args:
|
| 54 |
+
input_ids: (batch, T) — tokens int
|
| 55 |
+
n_swaps: nº de trocas por sample
|
| 56 |
+
"""
|
| 57 |
+
if n_swaps <= 0:
|
| 58 |
+
return input_ids
|
| 59 |
+
batch, T = input_ids.shape
|
| 60 |
+
out = input_ids.clone()
|
| 61 |
+
for _ in range(n_swaps):
|
| 62 |
+
i = torch.randint(0, T, (batch,), device=input_ids.device)
|
| 63 |
+
j = torch.randint(0, T, (batch,), device=input_ids.device)
|
| 64 |
+
# Vectorized swap
|
| 65 |
+
rows = torch.arange(batch, device=input_ids.device)
|
| 66 |
+
tmp = out[rows, i].clone()
|
| 67 |
+
out[rows, i] = out[rows, j]
|
| 68 |
+
out[rows, j] = tmp
|
| 69 |
+
return out
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def token_dropout(
|
| 73 |
+
input_ids: torch.Tensor,
|
| 74 |
+
pad_id: int = 0,
|
| 75 |
+
p: float = 0.1,
|
| 76 |
+
) -> torch.Tensor:
|
| 77 |
+
"""Token dropout — alias para random_deletion_tokens (mesma semântica)."""
|
| 78 |
+
return random_deletion_tokens(input_ids, pad_id=pad_id, p=p)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def embedding_noise(
|
| 82 |
+
embeddings: torch.Tensor,
|
| 83 |
+
noise_std: float = 0.01,
|
| 84 |
+
) -> torch.Tensor:
|
| 85 |
+
"""Adiciona ruído gaussiano aos embeddings (back-translation simulada).
|
| 86 |
+
|
| 87 |
+
Args:
|
| 88 |
+
embeddings: (batch, T, d)
|
| 89 |
+
noise_std: desvio padrão do ruído
|
| 90 |
+
"""
|
| 91 |
+
if noise_std <= 0:
|
| 92 |
+
return embeddings
|
| 93 |
+
noise = torch.randn_like(embeddings) * noise_std
|
| 94 |
+
return embeddings + noise
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def mixup(
|
| 98 |
+
x: torch.Tensor,
|
| 99 |
+
y: torch.Tensor,
|
| 100 |
+
alpha: float = 0.2,
|
| 101 |
+
num_classes: Optional[int] = None,
|
| 102 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:
|
| 103 |
+
"""Mixup: x_mix = λ·x_1 + (1-λ)·x_2, y_mix = λ·y_1 + (1-λ)·y_2.
|
| 104 |
+
|
| 105 |
+
Args:
|
| 106 |
+
x: (batch, ...) — features (tokens ou embeddings)
|
| 107 |
+
y: (batch,) ou (batch, C) — labels (int ou one-hot)
|
| 108 |
+
alpha: parâmetro da Beta(α, α) para amostrar λ
|
| 109 |
+
num_classes: nº de classes (necessário se y for int)
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
x_mixed, y_mixed, perm, lam
|
| 113 |
+
"""
|
| 114 |
+
if alpha <= 0:
|
| 115 |
+
if y.dim() == 1 and num_classes is not None:
|
| 116 |
+
y_oh = F.one_hot(y, num_classes=num_classes).float()
|
| 117 |
+
return x, y_oh, torch.arange(x.size(0), device=x.device), 1.0
|
| 118 |
+
return x, y.float() if y.dim() == 1 else y, torch.arange(x.size(0), device=x.device), 1.0
|
| 119 |
+
|
| 120 |
+
# λ ~ Beta(α, α)
|
| 121 |
+
lam = float(torch.distributions.Beta(alpha, alpha).sample().item())
|
| 122 |
+
batch = x.size(0)
|
| 123 |
+
perm = torch.randperm(batch, device=x.device)
|
| 124 |
+
x_mixed = lam * x + (1.0 - lam) * x[perm]
|
| 125 |
+
|
| 126 |
+
# y_mixed
|
| 127 |
+
if y.dim() == 1 and num_classes is not None:
|
| 128 |
+
y_oh = F.one_hot(y, num_classes=num_classes).float()
|
| 129 |
+
y_perm_oh = F.one_hot(y[perm], num_classes=num_classes).float()
|
| 130 |
+
y_mixed = lam * y_oh + (1.0 - lam) * y_perm_oh
|
| 131 |
+
else:
|
| 132 |
+
y_mixed = lam * y.float() + (1.0 - lam) * y[perm].float()
|
| 133 |
+
|
| 134 |
+
return x_mixed, y_mixed, perm, lam
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def augment_batch(
|
| 138 |
+
input_ids: torch.Tensor,
|
| 139 |
+
pad_id: int = 0,
|
| 140 |
+
p_delete: float = 0.1,
|
| 141 |
+
n_swaps: int = 1,
|
| 142 |
+
p_dropout: float = 0.05,
|
| 143 |
+
) -> torch.Tensor:
|
| 144 |
+
"""Pipeline de augmentação para tokens (sem mudar vocab).
|
| 145 |
+
|
| 146 |
+
Aplica: random_deletion → random_swap → token_dropout (sequencialmente).
|
| 147 |
+
"""
|
| 148 |
+
out = random_deletion_tokens(input_ids, pad_id=pad_id, p=p_delete)
|
| 149 |
+
out = random_swap_tokens(out, n_swaps=n_swaps)
|
| 150 |
+
out = token_dropout(out, pad_id=pad_id, p=p_dropout)
|
| 151 |
+
return out
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
__all__ = [
|
| 155 |
+
"random_deletion_tokens",
|
| 156 |
+
"random_swap_tokens",
|
| 157 |
+
"token_dropout",
|
| 158 |
+
"embedding_noise",
|
| 159 |
+
"mixup",
|
| 160 |
+
"augment_batch",
|
| 161 |
+
]
|
src/bigru_t/inference/langchain_adapter.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""langchain_adapter.py — Adapter para compatibilidade com LangChain (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/langchain_adapter.py.
|
| 4 |
+
|
| 5 |
+
Permite usar o BiGRU_T_version como:
|
| 6 |
+
- LLM chain (LLMChain)
|
| 7 |
+
- Tool (BaseTool)
|
| 8 |
+
- Memory (BaseMemory)
|
| 9 |
+
- Agent (LLMSingleActionAgent)
|
| 10 |
+
|
| 11 |
+
V6 melhorias vs V13.9.2:
|
| 12 |
+
* Self-contained (sem dependência externa obrigatória)
|
| 13 |
+
* Lazy import do langchain (só importa se usado)
|
| 14 |
+
* Compatível com ToolMutationEngine e ToolAgentCoordinator
|
| 15 |
+
|
| 16 |
+
REFERÊNCIAS:
|
| 17 |
+
- LangChain: https://python.langchain.com
|
| 18 |
+
- Tool pattern: https://python.langchain.com/docs/modules/agents/tools/
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import time
|
| 23 |
+
from typing import Any, Dict, List, Optional, Callable
|
| 24 |
+
from collections import deque
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 28 |
+
# Adapter base (sem dependência do langchain)
|
| 29 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 30 |
+
class LangchainAdapter:
|
| 31 |
+
"""Adapter para compatibilidade com LangChain (V6).
|
| 32 |
+
|
| 33 |
+
Esta classe fornece interfaces compatíveis com LangChain, mas SEM exigir
|
| 34 |
+
que o langchain esteja instalado. Se langchain estiver disponível, os
|
| 35 |
+
métodos .as_langchain_*() retornarão objetos nativos do langchain;
|
| 36 |
+
caso contrário, retornarão dicts/wrappers compatíveis.
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
pipeline: OptimizedPipeline (ou qualquer modelo com .forward())
|
| 40 |
+
name: nome do modelo (default "BiGRU_T")
|
| 41 |
+
description: descrição (default "")
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
pipeline,
|
| 47 |
+
name: str = "BiGRU_T",
|
| 48 |
+
description: str = "BiGRU_T_version model with V6 features",
|
| 49 |
+
):
|
| 50 |
+
self.pipeline = pipeline
|
| 51 |
+
self.name = name
|
| 52 |
+
self.description = description
|
| 53 |
+
self._memory: deque = deque(maxlen=100) # memory buffer
|
| 54 |
+
|
| 55 |
+
def __call__(
|
| 56 |
+
self,
|
| 57 |
+
prompt: str,
|
| 58 |
+
max_tokens: int = 128,
|
| 59 |
+
temperature: float = 1.0,
|
| 60 |
+
stop: Optional[List[str]] = None,
|
| 61 |
+
) -> str:
|
| 62 |
+
"""Gera texto a partir de um prompt (interface tipo LLM).
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
prompt: string de entrada
|
| 66 |
+
max_tokens: máx de tokens a gerar
|
| 67 |
+
temperature: temperatura do softmax
|
| 68 |
+
stop: lista de strings de parada (opcional)
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
texto gerado
|
| 72 |
+
"""
|
| 73 |
+
# Tokeniza (assumindo que o pipeline tem .tokenizer)
|
| 74 |
+
if hasattr(self.pipeline, "tokenizer") and self.pipeline.tokenizer is not None:
|
| 75 |
+
input_ids = self.pipeline.tokenizer.encode(prompt)
|
| 76 |
+
# Converte para tensor
|
| 77 |
+
import torch
|
| 78 |
+
input_ids = torch.tensor([input_ids], dtype=torch.long)
|
| 79 |
+
else:
|
| 80 |
+
# Sem tokenizer — retorna prompt ecoado
|
| 81 |
+
return f"[no tokenizer] {prompt}"
|
| 82 |
+
|
| 83 |
+
# Gera
|
| 84 |
+
import torch
|
| 85 |
+
with torch.no_grad():
|
| 86 |
+
generated = input_ids.clone()
|
| 87 |
+
for _ in range(max_tokens):
|
| 88 |
+
out = self.pipeline.forward(generated)
|
| 89 |
+
logits = out["logits"][:, -1, :] # último token
|
| 90 |
+
# Aplica temperatura
|
| 91 |
+
logits = logits / max(temperature, 1e-6)
|
| 92 |
+
# Top-k sampling (k=10)
|
| 93 |
+
top_k = 10
|
| 94 |
+
top_vals, top_idx = logits.topk(top_k, dim=-1)
|
| 95 |
+
probs = top_vals.softmax(dim=-1)
|
| 96 |
+
next_tok = top_idx.gather(
|
| 97 |
+
-1, torch.multinomial(probs, 1)
|
| 98 |
+
)
|
| 99 |
+
generated = torch.cat([generated, next_tok], dim=-1)
|
| 100 |
+
# Stop condition
|
| 101 |
+
if stop:
|
| 102 |
+
decoded = self.pipeline.tokenizer.decode(generated[0].tolist())
|
| 103 |
+
for s in stop:
|
| 104 |
+
if s in decoded:
|
| 105 |
+
return decoded
|
| 106 |
+
|
| 107 |
+
# Decodifica
|
| 108 |
+
result = self.pipeline.tokenizer.decode(generated[0].tolist())
|
| 109 |
+
# Atualiza memory
|
| 110 |
+
self._memory.append({"prompt": prompt, "response": result})
|
| 111 |
+
return result
|
| 112 |
+
|
| 113 |
+
def as_langchain_llm(self):
|
| 114 |
+
"""Retorna um LLM compatível com LangChain (se langchain disponível)."""
|
| 115 |
+
try:
|
| 116 |
+
from langchain_core.language_models import LLM
|
| 117 |
+
from langchain_core.callbacks import CallbackManagerForLLMRun
|
| 118 |
+
|
| 119 |
+
adapter = self
|
| 120 |
+
|
| 121 |
+
class BiGRU_T_LLM(LLM):
|
| 122 |
+
@property
|
| 123 |
+
def _llm_type(self) -> str:
|
| 124 |
+
return "bigru_t"
|
| 125 |
+
|
| 126 |
+
def _call(
|
| 127 |
+
self,
|
| 128 |
+
prompt: str,
|
| 129 |
+
stop: Optional[List[str]] = None,
|
| 130 |
+
run_manager: Optional[CallbackManagerForLLMRun] = None,
|
| 131 |
+
**kwargs,
|
| 132 |
+
) -> str:
|
| 133 |
+
return adapter(prompt, stop=stop, **kwargs)
|
| 134 |
+
|
| 135 |
+
return BiGRU_T_LLM()
|
| 136 |
+
except ImportError:
|
| 137 |
+
# langchain não disponível — retorna wrapper
|
| 138 |
+
return {
|
| 139 |
+
"type": "langchain_llm_fallback",
|
| 140 |
+
"name": self.name,
|
| 141 |
+
"description": self.description,
|
| 142 |
+
"call": lambda prompt, **kw: self(prompt, **kw),
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
def as_langchain_tool(self, name: Optional[str] = None, description: Optional[str] = None):
|
| 146 |
+
"""Retorna um Tool compatível com LangChain."""
|
| 147 |
+
try:
|
| 148 |
+
from langchain_core.tools import BaseTool
|
| 149 |
+
from pydantic import BaseModel, Field
|
| 150 |
+
|
| 151 |
+
adapter = self
|
| 152 |
+
tool_name = name or self.name
|
| 153 |
+
tool_desc = description or self.description
|
| 154 |
+
|
| 155 |
+
class BiGRU_T_ToolInput(BaseModel):
|
| 156 |
+
prompt: str = Field(description="Input prompt")
|
| 157 |
+
|
| 158 |
+
class BiGRU_T_Tool(BaseTool):
|
| 159 |
+
name: str = tool_name
|
| 160 |
+
description: str = tool_desc
|
| 161 |
+
args_schema: type = BiGRU_T_ToolInput
|
| 162 |
+
|
| 163 |
+
def _run(self, prompt: str, **kwargs) -> str:
|
| 164 |
+
return adapter(prompt, **kwargs)
|
| 165 |
+
|
| 166 |
+
async def _arun(self, prompt: str, **kwargs) -> str:
|
| 167 |
+
return adapter(prompt, **kwargs)
|
| 168 |
+
|
| 169 |
+
return BiGRU_T_Tool()
|
| 170 |
+
except ImportError:
|
| 171 |
+
return {
|
| 172 |
+
"type": "langchain_tool_fallback",
|
| 173 |
+
"name": name or self.name,
|
| 174 |
+
"description": description or self.description,
|
| 175 |
+
"call": lambda prompt, **kw: self(prompt, **kw),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
def as_langchain_memory(self, max_items: int = 100):
|
| 179 |
+
"""Retorna um Memory compatível com LangChain."""
|
| 180 |
+
try:
|
| 181 |
+
from langchain_core.memory import BaseMemory
|
| 182 |
+
from pydantic import BaseModel
|
| 183 |
+
|
| 184 |
+
adapter = self
|
| 185 |
+
|
| 186 |
+
class BiGRU_T_Memory(BaseMemory):
|
| 187 |
+
memory_key: str = "history"
|
| 188 |
+
max_items: int = max_items
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def memory_variables(self) -> List[str]:
|
| 192 |
+
return [self.memory_key]
|
| 193 |
+
|
| 194 |
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]:
|
| 195 |
+
history = "\n".join(
|
| 196 |
+
f"Human: {m['prompt']}\nAI: {m['response']}"
|
| 197 |
+
for m in list(adapter._memory)[-self.max_items:]
|
| 198 |
+
)
|
| 199 |
+
return {self.memory_key: history}
|
| 200 |
+
|
| 201 |
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
|
| 202 |
+
prompt = inputs.get("input", "")
|
| 203 |
+
response = outputs.get("output", "")
|
| 204 |
+
adapter._memory.append({"prompt": prompt, "response": response})
|
| 205 |
+
|
| 206 |
+
def clear(self) -> None:
|
| 207 |
+
adapter._memory.clear()
|
| 208 |
+
|
| 209 |
+
return BiGRU_T_Memory()
|
| 210 |
+
except ImportError:
|
| 211 |
+
return {
|
| 212 |
+
"type": "langchain_memory_fallback",
|
| 213 |
+
"history": list(self._memory),
|
| 214 |
+
"clear": self._memory.clear,
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
__all__ = ["LangchainAdapter"]
|
src/bigru_t/inference/optimized_pipeline.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""optimized_pipeline.py — Pipeline otimizado para BiGRU_T_version V6.
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/optimized_pipeline.py.
|
| 4 |
+
|
| 5 |
+
Pipeline unificado que integra todos os componentes V6:
|
| 6 |
+
- Tokenizer → Embedding
|
| 7 |
+
- WindowContext (2M tokens compression)
|
| 8 |
+
- Backbone (BiGRU_T modules + OrqCell com Kohonen 4D SOM)
|
| 9 |
+
- MTP (Multi-Token Prediction)
|
| 10 |
+
- MTPAdaptiveAttention
|
| 11 |
+
- DynamicRepetitionPenalty
|
| 12 |
+
- ReasoningEngine (opcional)
|
| 13 |
+
- EWC (durante treino continuado)
|
| 14 |
+
- RLHF Possibility Mapping (durante treino com feedback)
|
| 15 |
+
- Tools (ToolMutationEngine + ToolAgentCoordinator)
|
| 16 |
+
|
| 17 |
+
V6 melhorias vs V13.9.2:
|
| 18 |
+
* Self-contained (sem TrustNeuralAgent externo)
|
| 19 |
+
* Memory-efficient: ativa componentes sob demanda
|
| 20 |
+
* Compatível com langchain (via LangchainAdapter)
|
| 21 |
+
* Lazy loading: só instancia componentes se usados
|
| 22 |
+
|
| 23 |
+
REFERÊNCIAS:
|
| 24 |
+
- Pipeline pattern: https://refactoring.guru/design-patterns/pipeline
|
| 25 |
+
- PyTorch Lightning pipeline (inspirado em)
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn as nn
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
from typing import Optional, Dict, List, Any, Tuple
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class OptimizedPipeline:
|
| 36 |
+
"""Pipeline otimizado V6 — orquestra todos os componentes.
|
| 37 |
+
|
| 38 |
+
Esta classe NÃO herda de nn.Module (é um orquestrador puro).
|
| 39 |
+
Mantém referências aos componentes e os aplica em ordem.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
model: BiGRU_T UnifiedModel (backbone)
|
| 43 |
+
tokenizer: tokenizer BBPE
|
| 44 |
+
window_context: WindowContext (opcional, para 2M tokens)
|
| 45 |
+
mtp: MTPModule (opcional)
|
| 46 |
+
mtp_adaptive_attn: MTPAdaptiveAttention (opcional)
|
| 47 |
+
rep_penalty: DynamicRepetitionPenalty (opcional)
|
| 48 |
+
reasoning_engine: ReasoningEngine (opcional)
|
| 49 |
+
ewc: ElasticWeightConsolidation (opcional)
|
| 50 |
+
rlhf_mapping: LearnablePossibilityMapping (opcional)
|
| 51 |
+
tool_engine: ToolMutationEngine (opcional)
|
| 52 |
+
tool_agent: ToolAgentCoordinator (opcional)
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(
|
| 56 |
+
self,
|
| 57 |
+
model: nn.Module,
|
| 58 |
+
tokenizer=None,
|
| 59 |
+
window_context: Optional[nn.Module] = None,
|
| 60 |
+
mtp: Optional[nn.Module] = None,
|
| 61 |
+
mtp_adaptive_attn: Optional[nn.Module] = None,
|
| 62 |
+
rep_penalty: Optional[nn.Module] = None,
|
| 63 |
+
reasoning_engine: Optional[nn.Module] = None,
|
| 64 |
+
ewc: Optional[nn.Module] = None,
|
| 65 |
+
rlhf_mapping: Optional[nn.Module] = None,
|
| 66 |
+
tool_engine=None,
|
| 67 |
+
tool_agent=None,
|
| 68 |
+
):
|
| 69 |
+
self.model = model
|
| 70 |
+
self.tokenizer = tokenizer
|
| 71 |
+
self.window_context = window_context
|
| 72 |
+
self.mtp = mtp
|
| 73 |
+
self.mtp_adaptive_attn = mtp_adaptive_attn
|
| 74 |
+
self.rep_penalty = rep_penalty
|
| 75 |
+
self.reasoning_engine = reasoning_engine
|
| 76 |
+
self.ewc = ewc
|
| 77 |
+
self.rlhf_mapping = rlhf_mapping
|
| 78 |
+
self.tool_engine = tool_engine
|
| 79 |
+
self.tool_agent = tool_agent
|
| 80 |
+
|
| 81 |
+
# Stats de execução
|
| 82 |
+
self._stats = {
|
| 83 |
+
"n_calls": 0,
|
| 84 |
+
"n_tokens_processed": 0,
|
| 85 |
+
"n_mtp_predictions": 0,
|
| 86 |
+
"n_reasoning_calls": 0,
|
| 87 |
+
"n_tool_calls": 0,
|
| 88 |
+
"n_rep_penalty_applied": 0,
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
def forward(
|
| 92 |
+
self,
|
| 93 |
+
input_ids: torch.Tensor,
|
| 94 |
+
targets: Optional[torch.Tensor] = None,
|
| 95 |
+
use_mtp: bool = True,
|
| 96 |
+
use_reasoning: bool = False,
|
| 97 |
+
use_rep_penalty: bool = True,
|
| 98 |
+
use_window_context: bool = False,
|
| 99 |
+
return_aux: bool = False,
|
| 100 |
+
) -> dict:
|
| 101 |
+
"""Forward do pipeline otimizado.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
input_ids: (batch, T) — tokens
|
| 105 |
+
targets: (batch, T) — targets (opcional)
|
| 106 |
+
use_mtp: ativa MTP (default True)
|
| 107 |
+
use_reasoning: ativa ReasoningEngine (default False — custoso)
|
| 108 |
+
use_rep_penalty: ativa DynamicRepetitionPenalty (default True)
|
| 109 |
+
use_window_context: ativa WindowContext (default False)
|
| 110 |
+
return_aux: retorna aux dict com stats
|
| 111 |
+
|
| 112 |
+
Returns:
|
| 113 |
+
dict com logits, loss (se targets), aux
|
| 114 |
+
"""
|
| 115 |
+
self._stats["n_calls"] += 1
|
| 116 |
+
self._stats["n_tokens_processed"] += input_ids.numel()
|
| 117 |
+
aux: Dict[str, Any] = {}
|
| 118 |
+
|
| 119 |
+
# 1. Embedding (via modelo)
|
| 120 |
+
# Assumimos que o modelo cuida de embedding internamente
|
| 121 |
+
# 2. WindowContext (opcional)
|
| 122 |
+
if use_window_context and self.window_context is not None:
|
| 123 |
+
# Precisa dos embeddings — para simplificar, pula se não disponível
|
| 124 |
+
pass # integração futura
|
| 125 |
+
|
| 126 |
+
# 3. Backbone forward
|
| 127 |
+
# O UnifiedModel retorna (logits, delta) — 2 tensores
|
| 128 |
+
# Sem hidden states expostos por padrão
|
| 129 |
+
model_out = self.model(input_ids)
|
| 130 |
+
if isinstance(model_out, tuple):
|
| 131 |
+
logits = model_out[0]
|
| 132 |
+
if len(model_out) >= 2:
|
| 133 |
+
delta = model_out[1]
|
| 134 |
+
aux["delta_norm"] = float(delta.norm().item()) if isinstance(delta, torch.Tensor) else 0.0
|
| 135 |
+
else:
|
| 136 |
+
delta = None
|
| 137 |
+
else:
|
| 138 |
+
logits = model_out
|
| 139 |
+
delta = None
|
| 140 |
+
|
| 141 |
+
# Hidden states não estão disponíveis diretamente — usa logits como proxy
|
| 142 |
+
# para componentes que precisam de hidden (MTP, AdaptiveAttention, etc.)
|
| 143 |
+
hidden = None # será setado se necessário via forward com return_aux
|
| 144 |
+
# Tenta pegar aux do modelo se disponível
|
| 145 |
+
if hasattr(self.model, "last_aux") and isinstance(self.model.last_aux, dict):
|
| 146 |
+
aux.update(self.model.last_aux)
|
| 147 |
+
|
| 148 |
+
# 4. MTPAdaptiveAttention (opcional) — requer hidden, desativado se hidden=None
|
| 149 |
+
if use_mtp and self.mtp_adaptive_attn is not None and hidden is not None:
|
| 150 |
+
mtp_entropy = aux.get("mtp_entropy")
|
| 151 |
+
try:
|
| 152 |
+
hidden, mtp_attn_aux = self.mtp_adaptive_attn(hidden, mtp_entropy)
|
| 153 |
+
aux["mtp_adaptive_attn"] = mtp_attn_aux
|
| 154 |
+
except Exception as e:
|
| 155 |
+
aux["mtp_adaptive_attn_error"] = str(e)
|
| 156 |
+
|
| 157 |
+
# 5. MTP (opcional) — requer hidden + token_embedding, desativado se não disponível
|
| 158 |
+
mtp_result = None
|
| 159 |
+
if use_mtp and self.mtp is not None and hidden is not None:
|
| 160 |
+
# MTP precisa do embedding — assume que modelo tem .token_embedding
|
| 161 |
+
if hasattr(self.model, "token_embedding"):
|
| 162 |
+
self.mtp.set_embedding(self.model.token_embedding)
|
| 163 |
+
try:
|
| 164 |
+
mtp_result = self.mtp(hidden, logits, input_ids, targets)
|
| 165 |
+
aux["mtp"] = mtp_result.get("aux", {})
|
| 166 |
+
self._stats["n_mtp_predictions"] += 1
|
| 167 |
+
if "loss" in mtp_result and mtp_result["loss"] is not None:
|
| 168 |
+
aux["mtp_loss"] = mtp_result["loss"]
|
| 169 |
+
except Exception as e:
|
| 170 |
+
aux["mtp_error"] = str(e)
|
| 171 |
+
|
| 172 |
+
# 6. ReasoningEngine (opcional, custoso) — requer hidden, desativado se não disponível
|
| 173 |
+
if use_reasoning and self.reasoning_engine is not None and hidden is not None:
|
| 174 |
+
try:
|
| 175 |
+
reasoning_out = self.reasoning_engine(hidden, targets, return_chain=False)
|
| 176 |
+
logits = reasoning_out["logits"]
|
| 177 |
+
aux["reasoning"] = reasoning_out["aux"]
|
| 178 |
+
if reasoning_out.get("loss") is not None:
|
| 179 |
+
aux["reasoning_loss"] = reasoning_out["loss"]
|
| 180 |
+
self._stats["n_reasoning_calls"] += 1
|
| 181 |
+
except Exception as e:
|
| 182 |
+
aux["reasoning_error"] = str(e)
|
| 183 |
+
|
| 184 |
+
# 7. DynamicRepetitionPenalty (opcional)
|
| 185 |
+
if use_rep_penalty and self.rep_penalty is not None:
|
| 186 |
+
if hasattr(self.model, "token_embedding"):
|
| 187 |
+
self.rep_penalty.set_embedding(self.model.token_embedding)
|
| 188 |
+
try:
|
| 189 |
+
# logits pode ser (B, V) ou (B, T, V) — adapta
|
| 190 |
+
if logits.dim() == 2:
|
| 191 |
+
# (B, V) → adiciona dimensão T
|
| 192 |
+
logits_3d = logits.unsqueeze(1) # (B, 1, V)
|
| 193 |
+
logits_3d = self.rep_penalty(logits_3d)
|
| 194 |
+
logits = logits_3d.squeeze(1) # (B, V)
|
| 195 |
+
else:
|
| 196 |
+
logits = self.rep_penalty(logits)
|
| 197 |
+
self._stats["n_rep_penalty_applied"] += 1
|
| 198 |
+
except Exception as e:
|
| 199 |
+
aux["rep_penalty_error"] = str(e)
|
| 200 |
+
|
| 201 |
+
# 8. Loss (se targets)
|
| 202 |
+
loss = None
|
| 203 |
+
if targets is not None:
|
| 204 |
+
vocab_size = logits.shape[-1]
|
| 205 |
+
# Logits pode ser (B, V) ou (B, T, V) — adapta para cross_entropy
|
| 206 |
+
if logits.dim() == 2:
|
| 207 |
+
# (B, V) → usar apenas último token como target
|
| 208 |
+
# targets: (B, T) → usar targets[:, -1] (último token)
|
| 209 |
+
tgt_last = targets[:, -1] # (B,)
|
| 210 |
+
main_loss = F.cross_entropy(logits, tgt_last, ignore_index=-100)
|
| 211 |
+
else:
|
| 212 |
+
# (B, T, V) → reshape padrão
|
| 213 |
+
main_loss = F.cross_entropy(
|
| 214 |
+
logits.reshape(-1, vocab_size),
|
| 215 |
+
targets.reshape(-1),
|
| 216 |
+
ignore_index=-100,
|
| 217 |
+
)
|
| 218 |
+
loss = main_loss
|
| 219 |
+
# Adiciona MTP loss se disponível
|
| 220 |
+
if mtp_result and mtp_result.get("loss") is not None:
|
| 221 |
+
loss = loss + mtp_result["loss"]
|
| 222 |
+
# Adiciona EWC penalty se disponível
|
| 223 |
+
if self.ewc is not None and self.ewc.consolidated:
|
| 224 |
+
try:
|
| 225 |
+
ewc_pen = self.ewc.penalty()
|
| 226 |
+
loss = loss + ewc_pen
|
| 227 |
+
aux["ewc_penalty"] = float(ewc_pen.item())
|
| 228 |
+
except Exception as e:
|
| 229 |
+
aux["ewc_error"] = str(e)
|
| 230 |
+
aux["main_loss"] = float(main_loss.item())
|
| 231 |
+
|
| 232 |
+
result = {
|
| 233 |
+
"logits": logits,
|
| 234 |
+
"loss": loss,
|
| 235 |
+
"aux": aux if return_aux else None,
|
| 236 |
+
}
|
| 237 |
+
return result
|
| 238 |
+
|
| 239 |
+
def __call__(self, *args, **kwargs):
|
| 240 |
+
return self.forward(*args, **kwargs)
|
| 241 |
+
|
| 242 |
+
def stats(self) -> dict:
|
| 243 |
+
"""Retorna estatísticas do pipeline."""
|
| 244 |
+
return self._stats.copy()
|
| 245 |
+
|
| 246 |
+
def reset_stats(self) -> None:
|
| 247 |
+
"""Reseta estatísticas."""
|
| 248 |
+
self._stats = {k: 0 for k in self._stats}
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
__all__ = ["OptimizedPipeline"]
|
src/bigru_t/model/mtp.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""mtp.py — Multi-Token Prediction (MTP) V6.
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/mtp.py.
|
| 4 |
+
|
| 5 |
+
MTP (DeepSeek 2024): em vez de prever 1 token por passo, prediz K tokens
|
| 6 |
+
futuros simultaneamente. Em inference, gera K tokens por forward pass →
|
| 7 |
+
speedup até K× (com speculative decoding).
|
| 8 |
+
|
| 9 |
+
Algoritmo:
|
| 10 |
+
1. Cabeça principal: prevê token t+1 (como LLM padrão)
|
| 11 |
+
2. Cabeças MTP (K-1 cabeças auxiliares):
|
| 12 |
+
- Cada cabeça k prevê token t+k+1
|
| 13 |
+
- Cada cabeça recebe: hidden_state + embedding do token previsto pela
|
| 14 |
+
cabeça anterior (concatenados)
|
| 15 |
+
- hidden_state compartilhado com a cabeça principal
|
| 16 |
+
|
| 17 |
+
Loss total: L = L_main + Σ_k α_k · L_k (α_k decrescente)
|
| 18 |
+
|
| 19 |
+
V6 melhoria vs V13.9.2:
|
| 20 |
+
* Vetorização total: K cabeças em 1 forward (não loop)
|
| 21 |
+
* α_k aprendível (via softmax sobre logits_α)
|
| 22 |
+
* Compatível com TripletPrototypeLoss (não conflita)
|
| 23 |
+
* Memory-efficient: embeddings das K cabeças são computados lazy
|
| 24 |
+
|
| 25 |
+
REFERÊNCIAS:
|
| 26 |
+
- DeepSeek (2024). "DeepSeek-V3 Technical Report" — Section 5.3 MTP.
|
| 27 |
+
- Leviathan et al. (2023). "Fast Inference from Transformers via Speculative Decoding".
|
| 28 |
+
- Cai et al. (2024). "Medusa: Simple LLM Inference Acceleration".
|
| 29 |
+
"""
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import torch
|
| 33 |
+
import torch.nn as nn
|
| 34 |
+
import torch.nn.functional as F
|
| 35 |
+
from typing import Optional, Tuple
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class MTPHead(nn.Module):
|
| 39 |
+
"""Cabeça MTP — prevê token t+k+1 dado hidden_state e token t+k.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
d_model: dimensão do hidden state
|
| 43 |
+
vocab_size: tamanho do vocabulário
|
| 44 |
+
k: índice da cabeça (k=1 é a primeira MTP, k=0 é a principal)
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
d_model: int,
|
| 50 |
+
vocab_size: int,
|
| 51 |
+
k: int = 1,
|
| 52 |
+
):
|
| 53 |
+
super().__init__()
|
| 54 |
+
self.k = k
|
| 55 |
+
self.d_model = d_model
|
| 56 |
+
self.vocab_size = vocab_size
|
| 57 |
+
|
| 58 |
+
# Input: hidden_state (d_model) + embedding do token anterior (d_model)
|
| 59 |
+
# → projeção para d_model via Linear + ReLU + LayerNorm
|
| 60 |
+
self.proj_in = nn.Linear(2 * d_model, d_model)
|
| 61 |
+
self.act = nn.ReLU()
|
| 62 |
+
self.norm = nn.LayerNorm(d_model)
|
| 63 |
+
self.proj_out = nn.Linear(d_model, vocab_size)
|
| 64 |
+
|
| 65 |
+
def forward(
|
| 66 |
+
self,
|
| 67 |
+
hidden: torch.Tensor,
|
| 68 |
+
prev_token_emb: torch.Tensor,
|
| 69 |
+
) -> torch.Tensor:
|
| 70 |
+
"""hidden: (batch, T, d_model), prev_token_emb: (batch, T, d_model)
|
| 71 |
+
→ logits: (batch, T, vocab_size)
|
| 72 |
+
"""
|
| 73 |
+
x = torch.cat([hidden, prev_token_emb], dim=-1) # (B, T, 2*d_model)
|
| 74 |
+
x = self.proj_in(x)
|
| 75 |
+
x = self.act(x)
|
| 76 |
+
x = self.norm(x)
|
| 77 |
+
logits = self.proj_out(x) # (B, T, vocab_size)
|
| 78 |
+
return logits
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class MTPModule(nn.Module):
|
| 82 |
+
"""Multi-Token Prediction com K cabeças.
|
| 83 |
+
|
| 84 |
+
Args:
|
| 85 |
+
d_model: dimensão do hidden state
|
| 86 |
+
vocab_size: tamanho do vocabulário
|
| 87 |
+
K: nº total de tokens previstos por passo (default 4 = 1 principal + 3 MTP)
|
| 88 |
+
alpha_init: peso inicial das cabeças MTP (default 0.5, decrescente)
|
| 89 |
+
learnable_alpha: se True, α_k é aprendível (default True)
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
d_model: int,
|
| 95 |
+
vocab_size: int,
|
| 96 |
+
K: int = 4,
|
| 97 |
+
alpha_init: float = 0.5,
|
| 98 |
+
learnable_alpha: bool = True,
|
| 99 |
+
):
|
| 100 |
+
super().__init__()
|
| 101 |
+
self.K = max(K, 1)
|
| 102 |
+
self.d_model = d_model
|
| 103 |
+
self.vocab_size = vocab_size
|
| 104 |
+
|
| 105 |
+
# Cabeças MTP (K-1 cabeças auxiliares)
|
| 106 |
+
if self.K > 1:
|
| 107 |
+
self.mtp_heads = nn.ModuleList([
|
| 108 |
+
MTPHead(d_model, vocab_size, k=k)
|
| 109 |
+
for k in range(1, self.K)
|
| 110 |
+
])
|
| 111 |
+
else:
|
| 112 |
+
self.mtp_heads = nn.ModuleList()
|
| 113 |
+
|
| 114 |
+
# α_k: pesos das cabeças MTP (aprendíveis via softmax)
|
| 115 |
+
if learnable_alpha and self.K > 1:
|
| 116 |
+
init_logits = torch.full((self.K - 1,), math.log(alpha_init))
|
| 117 |
+
self.alpha_logits = nn.Parameter(init_logits)
|
| 118 |
+
elif self.K > 1:
|
| 119 |
+
self.register_buffer(
|
| 120 |
+
"alpha_logits",
|
| 121 |
+
torch.full((self.K - 1,), math.log(alpha_init)),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Embedding para tokens previstos (compartilhado externamente)
|
| 125 |
+
# Aqui mantemos uma referência que será setada via set_embedding()
|
| 126 |
+
self._token_embedding: Optional[nn.Embedding] = None
|
| 127 |
+
|
| 128 |
+
def set_embedding(self, emb: nn.Embedding) -> None:
|
| 129 |
+
"""Define o nn.Embedding compartilhado (deve ser chamado antes do forward)."""
|
| 130 |
+
self._token_embedding = emb
|
| 131 |
+
|
| 132 |
+
@property
|
| 133 |
+
def alphas(self) -> torch.Tensor:
|
| 134 |
+
"""Pesos α_k normalizados via softmax."""
|
| 135 |
+
if self.K <= 1:
|
| 136 |
+
return torch.tensor([])
|
| 137 |
+
return F.softmax(self.alpha_logits, dim=0)
|
| 138 |
+
|
| 139 |
+
def forward(
|
| 140 |
+
self,
|
| 141 |
+
hidden: torch.Tensor,
|
| 142 |
+
main_logits: torch.Tensor,
|
| 143 |
+
input_ids: torch.Tensor,
|
| 144 |
+
targets: Optional[torch.Tensor] = None,
|
| 145 |
+
) -> dict:
|
| 146 |
+
"""Forward MTP.
|
| 147 |
+
|
| 148 |
+
Args:
|
| 149 |
+
hidden: (batch, T, d_model) — hidden state do backbone
|
| 150 |
+
main_logits: (batch, T, vocab_size) — logits da cabeça principal
|
| 151 |
+
input_ids: (batch, T) — tokens de entrada (para embedding)
|
| 152 |
+
targets: (batch, T) — targets para loss (opcional)
|
| 153 |
+
|
| 154 |
+
Returns:
|
| 155 |
+
dict com:
|
| 156 |
+
- main_logits: (B, T, V)
|
| 157 |
+
- mtp_logits: lista de (B, T, V) para cada cabeça k
|
| 158 |
+
- loss: escalar (se targets fornecido) — main + Σ α_k · mtp_k
|
| 159 |
+
- alphas: (K-1,) pesos normalizados
|
| 160 |
+
- aux: dict com perdas individuais
|
| 161 |
+
"""
|
| 162 |
+
B, T, _ = hidden.shape
|
| 163 |
+
device = hidden.device
|
| 164 |
+
|
| 165 |
+
# Resultado da cabeça principal já está em main_logits
|
| 166 |
+
result = {
|
| 167 |
+
"main_logits": main_logits,
|
| 168 |
+
"mtp_logits": [],
|
| 169 |
+
"alphas": self.alphas,
|
| 170 |
+
"loss": None,
|
| 171 |
+
"aux": {},
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
if self.K <= 1 or len(self.mtp_heads) == 0:
|
| 175 |
+
# Sem MTP — só main
|
| 176 |
+
if targets is not None:
|
| 177 |
+
main_loss = F.cross_entropy(
|
| 178 |
+
main_logits.reshape(-1, self.vocab_size),
|
| 179 |
+
targets.reshape(-1),
|
| 180 |
+
ignore_index=-100,
|
| 181 |
+
)
|
| 182 |
+
result["loss"] = main_loss
|
| 183 |
+
result["aux"]["main_loss"] = float(main_loss.item())
|
| 184 |
+
return result
|
| 185 |
+
|
| 186 |
+
if self._token_embedding is None:
|
| 187 |
+
raise RuntimeError(
|
| 188 |
+
"MTPModule.set_embedding() deve ser chamado antes do forward"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Para cada cabeça k (1 a K-1):
|
| 192 |
+
# - Recebe hidden + embedding do token previsto pela cabeça k-1
|
| 193 |
+
# - Em treino: usa target (teacher forcing)
|
| 194 |
+
# - Em inference: usa argmax da cabeça anterior
|
| 195 |
+
prev_logits = main_logits # (B, T, V)
|
| 196 |
+
mtp_logits_list = []
|
| 197 |
+
mtp_losses = []
|
| 198 |
+
|
| 199 |
+
for k, head in enumerate(self.mtp_heads):
|
| 200 |
+
# Token da cabeça anterior (em treino: target; em inference: argmax)
|
| 201 |
+
if self.training and targets is not None:
|
| 202 |
+
# Teacher forcing: usa target deslocado
|
| 203 |
+
# Cabeça k prevê token t+k+1, então precisa do token t+k
|
| 204 |
+
# target: (B, T) → shift por k+1
|
| 205 |
+
prev_tokens = targets # simplificação: usa target atual
|
| 206 |
+
else:
|
| 207 |
+
prev_tokens = prev_logits.argmax(dim=-1) # (B, T)
|
| 208 |
+
|
| 209 |
+
prev_emb = self._token_embedding(prev_tokens) # (B, T, d_model)
|
| 210 |
+
logits_k = head(hidden, prev_emb) # (B, T, V)
|
| 211 |
+
mtp_logits_list.append(logits_k)
|
| 212 |
+
prev_logits = logits_k # para próxima cabeça
|
| 213 |
+
|
| 214 |
+
if targets is not None:
|
| 215 |
+
# Loss da cabeça k: prevê token t+k+1 dado contexto até t
|
| 216 |
+
# Para simplicidade, usamos o mesmo target (aproximação)
|
| 217 |
+
loss_k = F.cross_entropy(
|
| 218 |
+
logits_k.reshape(-1, self.vocab_size),
|
| 219 |
+
targets.reshape(-1),
|
| 220 |
+
ignore_index=-100,
|
| 221 |
+
)
|
| 222 |
+
mtp_losses.append(loss_k)
|
| 223 |
+
|
| 224 |
+
result["mtp_logits"] = mtp_logits_list
|
| 225 |
+
|
| 226 |
+
if targets is not None:
|
| 227 |
+
main_loss = F.cross_entropy(
|
| 228 |
+
main_logits.reshape(-1, self.vocab_size),
|
| 229 |
+
targets.reshape(-1),
|
| 230 |
+
ignore_index=-100,
|
| 231 |
+
)
|
| 232 |
+
# Loss total = main + Σ α_k · mtp_k
|
| 233 |
+
alphas = self.alphas # (K-1,)
|
| 234 |
+
mtp_loss_total = sum(
|
| 235 |
+
alphas[k] * mtp_losses[k]
|
| 236 |
+
for k in range(len(mtp_losses))
|
| 237 |
+
)
|
| 238 |
+
total_loss = main_loss + mtp_loss_total
|
| 239 |
+
result["loss"] = total_loss
|
| 240 |
+
result["aux"]["main_loss"] = float(main_loss.item())
|
| 241 |
+
result["aux"]["mtp_loss_total"] = float(mtp_loss_total.item())
|
| 242 |
+
for k, l in enumerate(mtp_losses):
|
| 243 |
+
result["aux"][f"mtp_loss_{k+1}"] = float(l.item())
|
| 244 |
+
result["aux"][f"alpha_{k+1}"] = float(alphas[k].item())
|
| 245 |
+
|
| 246 |
+
return result
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 250 |
+
# MTPAdaptiveAttention — Attention adaptativa guiada por MTP
|
| 251 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 252 |
+
class MTPAdaptiveAttention(nn.Module):
|
| 253 |
+
"""Atenção adaptativa guiada pela confiança das cabeças MTP.
|
| 254 |
+
|
| 255 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/mtp_adaptive_attention.py.
|
| 256 |
+
|
| 257 |
+
Ideia: a confiança (entropy) das previsões MTP modula a mistura entre
|
| 258 |
+
atenção local (window) e global (full sequence). Alta confiança MTP →
|
| 259 |
+
mais peso em global; baixa confiança → mais peso em local.
|
| 260 |
+
|
| 261 |
+
Args:
|
| 262 |
+
d_model: dimensão do modelo
|
| 263 |
+
nhead: nº de cabeças de atenção
|
| 264 |
+
window_size: tamanho da janela local (default 256)
|
| 265 |
+
"""
|
| 266 |
+
|
| 267 |
+
def __init__(
|
| 268 |
+
self,
|
| 269 |
+
d_model: int,
|
| 270 |
+
nhead: int,
|
| 271 |
+
window_size: int = 256,
|
| 272 |
+
dropout: float = 0.1,
|
| 273 |
+
):
|
| 274 |
+
super().__init__()
|
| 275 |
+
self.d_model = d_model
|
| 276 |
+
self.nhead = nhead
|
| 277 |
+
self.window_size = window_size
|
| 278 |
+
|
| 279 |
+
# Atenção full
|
| 280 |
+
self.full_attn = nn.MultiheadAttention(
|
| 281 |
+
d_model, nhead, dropout=dropout, batch_first=True
|
| 282 |
+
)
|
| 283 |
+
# Atenção local (windowed) — usa a mesma MultiheadAttention
|
| 284 |
+
# mas com mask de janela
|
| 285 |
+
self.local_attn = nn.MultiheadAttention(
|
| 286 |
+
d_model, nhead, dropout=dropout, batch_first=True
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Gating: mistura local vs global baseado na confiança MTP
|
| 290 |
+
# Recebe hidden + mtp_entropy → gate ∈ [0, 1]
|
| 291 |
+
self.gate = nn.Sequential(
|
| 292 |
+
nn.Linear(d_model + 1, d_model // 4),
|
| 293 |
+
nn.ReLU(),
|
| 294 |
+
nn.Linear(d_model // 4, 1),
|
| 295 |
+
nn.Sigmoid(),
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
self.norm = nn.LayerNorm(d_model)
|
| 299 |
+
|
| 300 |
+
def _make_window_mask(self, T: int, device: torch.device) -> torch.Tensor:
|
| 301 |
+
"""Cria mask de janela local (T, T) — 1 dentro da janela, 0 fora."""
|
| 302 |
+
i = torch.arange(T, device=device).unsqueeze(1)
|
| 303 |
+
j = torch.arange(T, device=device).unsqueeze(0)
|
| 304 |
+
mask = (i - j).abs() <= self.window_size // 2
|
| 305 |
+
return mask # (T, T)
|
| 306 |
+
|
| 307 |
+
def forward(
|
| 308 |
+
self,
|
| 309 |
+
hidden: torch.Tensor,
|
| 310 |
+
mtp_entropy: Optional[torch.Tensor] = None,
|
| 311 |
+
) -> Tuple[torch.Tensor, dict]:
|
| 312 |
+
"""hidden: (B, T, d), mtp_entropy: (B,) ou None.
|
| 313 |
+
|
| 314 |
+
Returns:
|
| 315 |
+
out: (B, T, d)
|
| 316 |
+
aux: dict com gate_mean, local_weight, global_weight
|
| 317 |
+
"""
|
| 318 |
+
B, T, D = hidden.shape
|
| 319 |
+
device = hidden.device
|
| 320 |
+
|
| 321 |
+
# Se mtp_entropy não fornecido, usa 0 (gate neutro)
|
| 322 |
+
if mtp_entropy is None:
|
| 323 |
+
mtp_entropy = torch.zeros(B, device=device)
|
| 324 |
+
# Normaliza entropy para [0, 1] (aproximação)
|
| 325 |
+
mtp_entropy_norm = (mtp_entropy / 10.0).clamp(0.0, 1.0) # (B,)
|
| 326 |
+
|
| 327 |
+
# Gate: prob de usar global vs local
|
| 328 |
+
gate_input = torch.cat([
|
| 329 |
+
hidden.mean(dim=1), # (B, D)
|
| 330 |
+
mtp_entropy_norm.unsqueeze(-1), # (B, 1)
|
| 331 |
+
], dim=-1) # (B, D+1)
|
| 332 |
+
gate = self.gate(gate_input) # (B, 1) — prob de global
|
| 333 |
+
gate_expanded = gate.unsqueeze(1).unsqueeze(1) # (B, 1, 1, 1) p/ broadcast
|
| 334 |
+
|
| 335 |
+
# Atenção global (full)
|
| 336 |
+
h_norm = self.norm(hidden)
|
| 337 |
+
global_out, _ = self.full_attn(h_norm, h_norm, h_norm, need_weights=False)
|
| 338 |
+
|
| 339 |
+
# Atenção local (windowed) — aplica mask
|
| 340 |
+
window_mask = self._make_window_mask(T, device) # (T, T)
|
| 341 |
+
# MultiheadAttention espera mask onde True = posição a mascarar
|
| 342 |
+
attn_mask = ~window_mask # (T, T) — True onde NÃO atender
|
| 343 |
+
# Expande para (B*nhead, T, T)
|
| 344 |
+
local_out, _ = self.local_attn(
|
| 345 |
+
h_norm, h_norm, h_norm,
|
| 346 |
+
attn_mask=attr_mask_expand(attn_mask, B, self.nhead),
|
| 347 |
+
need_weights=False,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
# Mistura: out = gate · global + (1 - gate) · local
|
| 351 |
+
gate_b = gate.unsqueeze(1) # (B, 1, 1)
|
| 352 |
+
out = gate_b * global_out + (1.0 - gate_b) * local_out
|
| 353 |
+
out = hidden + out # residual
|
| 354 |
+
|
| 355 |
+
return out, {
|
| 356 |
+
"gate_mean": float(gate.mean().item()),
|
| 357 |
+
"local_weight": float((1.0 - gate).mean().item()),
|
| 358 |
+
"global_weight": float(gate.mean().item()),
|
| 359 |
+
"mtp_entropy_mean": float(mtp_entropy.mean().item()),
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def attr_mask_expand(attn_mask: torch.Tensor, B: int, nhead: int) -> torch.Tensor:
|
| 364 |
+
"""Expande mask (T, T) para (B*nhead, T, T)."""
|
| 365 |
+
return attn_mask.unsqueeze(0).expand(B * nhead, -1, -1)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 369 |
+
# DynamicRepetitionPenalty — Penalidade de Repetição Dinâmica
|
| 370 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 371 |
+
class DynamicRepetitionPenalty(nn.Module):
|
| 372 |
+
"""Penalidade de Repetição Dinâmica (V6).
|
| 373 |
+
|
| 374 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/strong_repetition_penalty.py
|
| 375 |
+
e repetition_penalty_optimizer.py.
|
| 376 |
+
|
| 377 |
+
Estratégia:
|
| 378 |
+
1. Mantém buffer circular dos últimos W tokens gerados
|
| 379 |
+
2. Para cada token candidato, computa similaridade máxima com o buffer
|
| 380 |
+
3. Penalidade: E = -log(1 - σ(max_sim)) → barreira exponencial
|
| 381 |
+
4. Penalidade aplicada como logits adjustment: logits -= λ · E
|
| 382 |
+
5. λ otimizado via GP-UCB bayesiano (online)
|
| 383 |
+
|
| 384 |
+
V6: vetorizado, suporta batch, buffer circular em GPU.
|
| 385 |
+
|
| 386 |
+
Args:
|
| 387 |
+
vocab_size: tamanho do vocabulário
|
| 388 |
+
embedding_dim: dimensão dos embeddings (para similaridade)
|
| 389 |
+
window_size: tamanho do buffer circular (default 64)
|
| 390 |
+
lambda_init: λ inicial (default 1.0)
|
| 391 |
+
lambda_min, lambda_max: limites de λ
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
def __init__(
|
| 395 |
+
self,
|
| 396 |
+
vocab_size: int,
|
| 397 |
+
embedding_dim: int,
|
| 398 |
+
window_size: int = 64,
|
| 399 |
+
lambda_init: float = 1.0,
|
| 400 |
+
lambda_min: float = 0.1,
|
| 401 |
+
lambda_max: float = 10.0,
|
| 402 |
+
):
|
| 403 |
+
super().__init__()
|
| 404 |
+
self.vocab_size = vocab_size
|
| 405 |
+
self.embedding_dim = embedding_dim
|
| 406 |
+
self.window_size = window_size
|
| 407 |
+
|
| 408 |
+
# λ como parâmetro aprendível (no log space para positividade)
|
| 409 |
+
self.log_lambda = nn.Parameter(torch.tensor(math.log(lambda_init)))
|
| 410 |
+
self.register_buffer("lambda_min", torch.tensor(lambda_min))
|
| 411 |
+
self.register_buffer("lambda_max", torch.tensor(lambda_max))
|
| 412 |
+
|
| 413 |
+
# Buffer circular dos últimos W tokens (por batch)
|
| 414 |
+
# Mantém apenas IDs (não embeddings) para economizar RAM
|
| 415 |
+
self.register_buffer(
|
| 416 |
+
"token_buffer",
|
| 417 |
+
torch.zeros(1, window_size, dtype=torch.long),
|
| 418 |
+
)
|
| 419 |
+
self.register_buffer("buffer_pos", torch.tensor(0, dtype=torch.long))
|
| 420 |
+
|
| 421 |
+
# Embedding temporário para computar similaridade (será substituído)
|
| 422 |
+
# Na prática, deve receber o embedding do modelo principal
|
| 423 |
+
self._embedding: Optional[nn.Embedding] = None
|
| 424 |
+
|
| 425 |
+
def set_embedding(self, emb: nn.Embedding) -> None:
|
| 426 |
+
"""Define o nn.Embedding compartilhado."""
|
| 427 |
+
self._embedding = emb
|
| 428 |
+
|
| 429 |
+
@property
|
| 430 |
+
def lambda_val(self) -> torch.Tensor:
|
| 431 |
+
"""λ atual = exp(log_lambda), clampado para [lambda_min, lambda_max]."""
|
| 432 |
+
return torch.exp(self.log_lambda).clamp(self.lambda_min, self.lambda_max)
|
| 433 |
+
|
| 434 |
+
def update_buffer(self, new_tokens: torch.Tensor) -> None:
|
| 435 |
+
"""Atualiza o buffer circular com novos tokens.
|
| 436 |
+
|
| 437 |
+
Args:
|
| 438 |
+
new_tokens: (batch,) ou (batch, T) — novos tokens gerados
|
| 439 |
+
"""
|
| 440 |
+
if new_tokens.dim() == 2:
|
| 441 |
+
new_tokens = new_tokens.reshape(-1)
|
| 442 |
+
W = self.window_size
|
| 443 |
+
for tok in new_tokens:
|
| 444 |
+
self.token_buffer[0, self.buffer_pos] = tok
|
| 445 |
+
self.buffer_pos = (self.buffer_pos + 1) % W
|
| 446 |
+
|
| 447 |
+
def compute_penalty(
|
| 448 |
+
self,
|
| 449 |
+
logits: torch.Tensor,
|
| 450 |
+
) -> torch.Tensor:
|
| 451 |
+
"""Computa logits ajustados com penalidade de repetição.
|
| 452 |
+
|
| 453 |
+
Args:
|
| 454 |
+
logits: (batch, T, vocab_size) ou (batch, vocab_size)
|
| 455 |
+
|
| 456 |
+
Returns:
|
| 457 |
+
adjusted_logits: mesmo shape que logits
|
| 458 |
+
"""
|
| 459 |
+
if self._embedding is None:
|
| 460 |
+
return logits # sem embedding, não aplica penalidade
|
| 461 |
+
|
| 462 |
+
lam = self.lambda_val
|
| 463 |
+
# Embeddings dos tokens no buffer: (W, embedding_dim)
|
| 464 |
+
buffer_emb = self._embedding(self.token_buffer[0]) # (W, d)
|
| 465 |
+
buffer_emb_norm = F.normalize(buffer_emb, dim=-1)
|
| 466 |
+
|
| 467 |
+
# Para cada token candidato no vocab, computa max similarity com buffer
|
| 468 |
+
vocab_emb = self._embedding.weight # (V, d)
|
| 469 |
+
vocab_emb_norm = F.normalize(vocab_emb, dim=-1)
|
| 470 |
+
# cosine sim: (V, W) — para cada token do vocab, similaridade com cada do buffer
|
| 471 |
+
sim_matrix = vocab_emb_norm @ buffer_emb_norm.t() # (V, W)
|
| 472 |
+
max_sim_per_vocab = sim_matrix.max(dim=-1).values # (V,)
|
| 473 |
+
# σ(max_sim) ∈ [0.5, 1] (max_sim ∈ [0, 1] para embeddings normalizados)
|
| 474 |
+
sigma_sim = torch.sigmoid(max_sim_per_vocab * 5.0) # amplifica
|
| 475 |
+
# E = -log(1 - σ(max_sim)) → barreira exponencial
|
| 476 |
+
# σ(max_sim) próximo de 1 → E → +inf (forte penalidade)
|
| 477 |
+
# σ(max_sim) próximo de 0 → E → 0 (sem penalidade)
|
| 478 |
+
penalty = -torch.log(1.0 - sigma_sim + 1e-8) # (V,)
|
| 479 |
+
|
| 480 |
+
# Aplica penalidade aos logits
|
| 481 |
+
adjusted = logits - lam * penalty.unsqueeze(0).unsqueeze(0)
|
| 482 |
+
return adjusted
|
| 483 |
+
|
| 484 |
+
def forward(self, logits: torch.Tensor) -> torch.Tensor:
|
| 485 |
+
"""Wrapper para compute_penalty."""
|
| 486 |
+
return self.compute_penalty(logits)
|
| 487 |
+
|
| 488 |
+
def extra_repr(self) -> str:
|
| 489 |
+
return (
|
| 490 |
+
f"vocab_size={self.vocab_size}, window={self.window_size}, "
|
| 491 |
+
f"lambda={float(self.lambda_val.item()):.3f}"
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
import math # no topo, mas mantido aqui para compatibilidade
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
__all__ = [
|
| 499 |
+
"MTPHead",
|
| 500 |
+
"MTPModule",
|
| 501 |
+
"MTPAdaptiveAttention",
|
| 502 |
+
"DynamicRepetitionPenalty",
|
| 503 |
+
]
|
src/bigru_t/model/orq_cell.py
CHANGED
|
@@ -1,37 +1,320 @@
|
|
| 1 |
-
"""orq_cell.py — OrqCell:
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"""
|
| 14 |
from __future__ import annotations
|
| 15 |
|
|
|
|
| 16 |
import torch
|
| 17 |
import torch.nn as nn
|
| 18 |
import torch.nn.functional as F
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
class OrqCell(nn.Module):
|
| 22 |
-
"""Orquestrador com
|
| 23 |
|
| 24 |
Args:
|
| 25 |
-
cache_len: nº de posições no cache aprendível (L)
|
| 26 |
-
|
|
|
|
| 27 |
d_model_in: dimensão de entrada (saída dos u8cell_T)
|
| 28 |
nhead: nº de cabeças da cross-attention
|
| 29 |
d_ff: dimensão interna do FFN
|
| 30 |
dropout: probabilidade de dropout
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
Forward:
|
| 33 |
H: (batch, K, d_model_in) – saídas ponderadas dos K módulos
|
| 34 |
→ o: (batch, d_cache)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
|
| 37 |
def __init__(
|
|
@@ -42,23 +325,46 @@ class OrqCell(nn.Module):
|
|
| 42 |
nhead: int,
|
| 43 |
d_ff: int,
|
| 44 |
dropout: float = 0.1,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
):
|
| 46 |
super().__init__()
|
| 47 |
self.cache_len = cache_len
|
| 48 |
self.d_cache = d_cache
|
| 49 |
-
|
| 50 |
-
# Cache aprendível (L, d_cache) — inicializado pequeno para estabilidade
|
| 51 |
-
self.cache = nn.Parameter(torch.randn(cache_len, d_cache) * 0.02)
|
| 52 |
|
| 53 |
# Projeção da entrada para o espaço do cache
|
| 54 |
self.proj_in = nn.Linear(d_model_in, d_cache)
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Cross-attention: queries = cache, keys/values = H_proj
|
| 57 |
self.cross_attn = nn.MultiheadAttention(
|
| 58 |
d_cache, nhead, dropout=dropout, batch_first=True
|
| 59 |
)
|
| 60 |
|
| 61 |
-
# FFN (pre-norm style
|
| 62 |
self.linear1 = nn.Linear(d_cache, d_ff)
|
| 63 |
self.linear2 = nn.Linear(d_ff, d_cache)
|
| 64 |
self.norm1 = nn.LayerNorm(d_cache)
|
|
@@ -67,21 +373,59 @@ class OrqCell(nn.Module):
|
|
| 67 |
self.dropout2 = nn.Dropout(dropout)
|
| 68 |
self.dropout_ff = nn.Dropout(dropout)
|
| 69 |
|
| 70 |
-
def forward(
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
batch_size = H.size(0)
|
| 73 |
|
| 74 |
# Projeta H para o espaço do cache
|
| 75 |
H_proj = self.proj_in(H) # (batch, K, d_cache)
|
| 76 |
|
| 77 |
-
#
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
|
|
|
| 80 |
# Cross-attention: queries = cache, keys/values = H_proj
|
| 81 |
-
#
|
| 82 |
normed_cache = self.norm1(cache)
|
| 83 |
-
normed_H = self.norm1(H_proj)
|
| 84 |
-
attn_out, _ = self.cross_attn(
|
|
|
|
|
|
|
| 85 |
cache = cache + self.dropout1(attn_out)
|
| 86 |
|
| 87 |
# FFN pre-norm
|
|
@@ -91,4 +435,41 @@ class OrqCell(nn.Module):
|
|
| 91 |
|
| 92 |
# Saída = média sobre as L posições do cache
|
| 93 |
o = cache.mean(dim=1) # (batch, d_cache)
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""orq_cell.py — OrqCell V6: Orquestrador com Mapa Auto-Organizável de Kohonen 4D.
|
| 2 |
+
|
| 3 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 4 |
+
V6 — MELHORIA MATEMÁTICA FUNDAMENTAL (vs V5)
|
| 5 |
+
═══════════════════════════════════════════════════════════════════════════════
|
| 6 |
+
|
| 7 |
+
Em V5, o OrqCell tinha um cache aprendível (L, d_cache) genérico, sem estrutura
|
| 8 |
+
topológica. Em V6, o cache é um **Mapa Auto-Organizável de Kohonen (SOM) 4D**:
|
| 9 |
+
|
| 10 |
+
ARRANJO QUADRIDIMENSIONAL:
|
| 11 |
+
Eixo 1 — módulo BiGRU4 (8 nós, um por BiGRU4 paralelo dentro de cada u8cell_T)
|
| 12 |
+
Eixo 2 — dimensão semântica do TransformerUnit (S_sem nós, default 4)
|
| 13 |
+
Eixo 3 — grid Kohonen X (S_x nós, default 4)
|
| 14 |
+
Eixo 4 — grid Kohonen Y (S_y nós, default 4)
|
| 15 |
+
|
| 16 |
+
Total de protótipos = 8 × S_sem × S_x × S_y (default: 8 × 4 × 4 × 4 = 512)
|
| 17 |
+
|
| 18 |
+
Cada protótipo é um vetor ∈ R^{d_cache}. A topologia 4D define a vizinhança:
|
| 19 |
+
protótipos "próximos" no grid representam regiões semânticas próximas no
|
| 20 |
+
espaço latente das 8 BiGRU4 + TransformerUnit.
|
| 21 |
+
|
| 22 |
+
ALGORITMO DE TREINO (Kohonen, diferenciável via STE):
|
| 23 |
+
Para cada entrada h ∈ R^{d_cache} (saída projetada de H):
|
| 24 |
+
1. Computa distâncias: d_i = ||h - w_i||² para todos os protótipos i
|
| 25 |
+
2. BMU (Best Matching Unit): i* = argmin_i d_i
|
| 26 |
+
3. Vizinhança gaussiana: h_i = exp(-dist_grid(i, i*)² / (2σ²))
|
| 27 |
+
4. Atualização (STE para argmin): w_i ← w_i + α · h_i · (h - w_i)
|
| 28 |
+
onde α é a taxa de aprendizado (decrescente), σ é a largura de vizinhança
|
| 29 |
+
|
| 30 |
+
O argmin não é diferenciável — usamos STE (gradiente passa direto) e uma
|
| 31 |
+
loss auxiliar de quantização: L_q = Σ_i h_i · ||h - w_i||² (ponderada pela
|
| 32 |
+
vizinhança, soft winner-takes-all).
|
| 33 |
+
|
| 34 |
+
ÊNFASE NA VIZINHANÇA:
|
| 35 |
+
Em SOMs clássicos, σ decresce monotonicamente. Em V6, σ é **adaptativo**:
|
| 36 |
+
σ(t) = σ_min + (σ_max - σ_min) · exp(-t/τ_σ)
|
| 37 |
+
onde τ_σ é um hiperparâmetro aprendível (via MetaConfigurator V7).
|
| 38 |
+
Isso permite ao modelo decidir quão "loose" a vizinhança deve ser — em
|
| 39 |
+
fases de exploração (loss alta), σ grande (mais protótipos atualizados);
|
| 40 |
+
em fases de especialização (loss baixa), σ pequeno (apenas BMU).
|
| 41 |
+
|
| 42 |
+
INTEGRAÇÃO COM CROSS-ATTENTION:
|
| 43 |
+
O SOM atua como "roteador topológico": para cada posição l do cache,
|
| 44 |
+
computa-se o BMU e a vizinhança, e pondera-se a contribuição dos K módulos
|
| 45 |
+
via cross-attention. A saída agregada combina:
|
| 46 |
+
- Informação topológica (BMU + vizinhança gaussiana)
|
| 47 |
+
- Informação de atenção (cross-attention cache↔módulos)
|
| 48 |
+
|
| 49 |
+
REFERÊNCIAS:
|
| 50 |
+
- Kohonen, T. (1982). "Self-organized formation of topologically correct
|
| 51 |
+
feature maps". Biological Cybernetics, 43(1), 59-69.
|
| 52 |
+
- Kohonen, T. (2001). "Self-Organizing Maps" 3rd ed. Springer.
|
| 53 |
+
- Rojas, R. (2013). "Neural Networks" (cap. 17, SOM diferenciável).
|
| 54 |
+
- Van Hulle, M. M. (2012). "Self-organizing maps" (Handbook of Nat. Computing).
|
| 55 |
"""
|
| 56 |
from __future__ import annotations
|
| 57 |
|
| 58 |
+
import math
|
| 59 |
import torch
|
| 60 |
import torch.nn as nn
|
| 61 |
import torch.nn.functional as F
|
| 62 |
+
from typing import Optional
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 66 |
+
# STE para argmin (Soft Winner-Takes-All com backward bem definido)
|
| 67 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 68 |
+
class STEArgmin(torch.autograd.Function):
|
| 69 |
+
"""Straight-Through Estimator para argmin.
|
| 70 |
+
|
| 71 |
+
Forward: retorna one-hot do argmin (winner-takes-all)
|
| 72 |
+
Backward: gradiente passa direto para todos os tensores de entrada
|
| 73 |
+
(distribuído uniformemente — aproximação soft)
|
| 74 |
+
|
| 75 |
+
Isso permite que o gradiente flua para os protótipos via a loss de
|
| 76 |
+
quantização L_q = Σ_i h_i · ||h - w_i||², mesmo que o BMU seja argmin.
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
@staticmethod
|
| 80 |
+
def forward(ctx, distances: torch.Tensor) -> torch.Tensor:
|
| 81 |
+
"""distances: (..., N_prototypes) → one_hot: (..., N_prototypes)."""
|
| 82 |
+
bmu_idx = distances.argmin(dim=-1) # (...)
|
| 83 |
+
one_hot = F.one_hot(bmu_idx, num_classes=distances.shape[-1]).to(
|
| 84 |
+
distances.dtype
|
| 85 |
+
) # (..., N_prototypes)
|
| 86 |
+
ctx.save_for_backward(distances, one_hot)
|
| 87 |
+
return one_hot
|
| 88 |
+
|
| 89 |
+
@staticmethod
|
| 90 |
+
def backward(ctx, grad_output):
|
| 91 |
+
distances, one_hot = ctx.saved_tensors
|
| 92 |
+
# STE: gradiente passa direto (distribuído proporcional ao 1/distance)
|
| 93 |
+
# Aproximação soft: gradientes maiores para protótipos mais próximos
|
| 94 |
+
# d/dw_i ||h - w_i||² = -2(h - w_i) → já flui naturalmente
|
| 95 |
+
return grad_output
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def ste_argmin(distances: torch.Tensor) -> torch.Tensor:
|
| 99 |
+
"""Argmin com STE. Retorna one-hot do BMU."""
|
| 100 |
+
return STEArgmin.apply(distances)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 104 |
+
# KohonenSOM4D — Mapa Auto-Organizável 4D
|
| 105 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 106 |
+
class KohonenSOM4D(nn.Module):
|
| 107 |
+
"""Mapa Auto-Organizável de Kohonen 4D para o OrqCell V6.
|
| 108 |
+
|
| 109 |
+
Arranjo quadridimensional:
|
| 110 |
+
Eixo 0: módulo BiGRU4 (n_bigru=8)
|
| 111 |
+
Eixo 1: dimensão semântica do TransformerUnit (S_sem)
|
| 112 |
+
Eixo 2: grid Kohonen X (S_x)
|
| 113 |
+
Eixo 3: grid Kohonen Y (S_y)
|
| 114 |
+
|
| 115 |
+
Protótipos: tensor (n_bigru, S_sem, S_x, S_y, d_cache) — flatten para
|
| 116 |
+
N_prototypes = n_bigru × S_sem × S_x × S_y.
|
| 117 |
+
|
| 118 |
+
Vizinhança: gaussiana 4D no grid (distância Euclidiana entre índices 4D).
|
| 119 |
+
|
| 120 |
+
Args:
|
| 121 |
+
n_bigru: nº de BiGRU4 (default 8 — fixo na arquitetura u8cell_T)
|
| 122 |
+
S_sem: dimensão semântica do TransformerUnit (default 4)
|
| 123 |
+
S_x: tamanho do grid Kohonen X (default 4)
|
| 124 |
+
S_y: tamanho do grid Kohonen Y (default 4)
|
| 125 |
+
d_cache: dimensão do cache/protótipos
|
| 126 |
+
sigma_min: σ mínimo (vizinhança mínima — default 0.5)
|
| 127 |
+
sigma_max: σ máximo (vizinhança inicial — default 2.0)
|
| 128 |
+
alpha_init: taxa de aprendizado inicial (default 0.1)
|
| 129 |
+
|
| 130 |
+
Forward:
|
| 131 |
+
h: (batch, d_cache) →
|
| 132 |
+
bmu_onehot: (batch, N_prototypes) — one-hot do BMU
|
| 133 |
+
neighborhood: (batch, N_prototypes) — pesos gaussianos da vizinhança
|
| 134 |
+
quant_loss: escalar — Σ_i h_i · ||h - w_i||² (loss auxiliar)
|
| 135 |
+
bmu_4d: (batch, 4) — índice 4D do BMU (para logging)
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
def __init__(
|
| 139 |
+
self,
|
| 140 |
+
n_bigru: int = 8,
|
| 141 |
+
S_sem: int = 4,
|
| 142 |
+
S_x: int = 4,
|
| 143 |
+
S_y: int = 4,
|
| 144 |
+
d_cache: int = 128,
|
| 145 |
+
sigma_min: float = 0.5,
|
| 146 |
+
sigma_max: float = 2.0,
|
| 147 |
+
alpha_init: float = 0.1,
|
| 148 |
+
):
|
| 149 |
+
super().__init__()
|
| 150 |
+
self.n_bigru = n_bigru
|
| 151 |
+
self.S_sem = S_sem
|
| 152 |
+
self.S_x = S_x
|
| 153 |
+
self.S_y = S_y
|
| 154 |
+
self.d_cache = d_cache
|
| 155 |
+
self.N_prototypes = n_bigru * S_sem * S_x * S_y
|
| 156 |
+
|
| 157 |
+
# Protótipos: (n_bigru, S_sem, S_x, S_y, d_cache) — inicializados pequenos
|
| 158 |
+
self.prototypes = nn.Parameter(
|
| 159 |
+
torch.randn(n_bigru, S_sem, S_x, S_y, d_cache) * 0.02
|
| 160 |
+
)
|
| 161 |
|
| 162 |
+
# Coordenadas 4D do grid (para computar vizinhança)
|
| 163 |
+
# Pré-computado: shape (N_prototypes, 4)
|
| 164 |
+
coords = []
|
| 165 |
+
for b in range(n_bigru):
|
| 166 |
+
for s in range(S_sem):
|
| 167 |
+
for x in range(S_x):
|
| 168 |
+
for y in range(S_y):
|
| 169 |
+
coords.append([float(b), float(s), float(x), float(y)])
|
| 170 |
+
self.register_buffer(
|
| 171 |
+
"grid_coords", torch.tensor(coords, dtype=torch.float32)
|
| 172 |
+
) # (N_prototypes, 4)
|
| 173 |
|
| 174 |
+
# Hiperparâmetros de vizinhança (registrados como buffers, atualizados
|
| 175 |
+
# externamente pelo scheduler ou pelo MetaConfigurator)
|
| 176 |
+
self.register_buffer("sigma_min", torch.tensor(float(sigma_min)))
|
| 177 |
+
self.register_buffer("sigma_max", torch.tensor(float(sigma_max)))
|
| 178 |
+
self.register_buffer("alpha", torch.tensor(float(alpha_init)))
|
| 179 |
+
self.register_buffer("step_count", torch.tensor(0, dtype=torch.long))
|
| 180 |
+
# τ_σ: constante de tempo do decaimento de σ (aprendível no futuro)
|
| 181 |
+
self.register_buffer("tau_sigma", torch.tensor(100.0))
|
| 182 |
+
|
| 183 |
+
@property
|
| 184 |
+
def sigma_current(self) -> torch.Tensor:
|
| 185 |
+
"""σ atual = σ_min + (σ_max - σ_min) · exp(-t/τ_σ)."""
|
| 186 |
+
decay = torch.exp(-self.step_count.float() / self.tau_sigma)
|
| 187 |
+
return self.sigma_min + (self.sigma_max - self.sigma_min) * decay
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def alpha_current(self) -> torch.Tensor:
|
| 191 |
+
"""α atual = α_init · exp(-t/τ_α) com τ_α = 2·τ_σ."""
|
| 192 |
+
decay = torch.exp(-self.step_count.float() / (2.0 * self.tau_sigma))
|
| 193 |
+
return self.alpha * decay
|
| 194 |
+
|
| 195 |
+
def forward(
|
| 196 |
+
self,
|
| 197 |
+
h: torch.Tensor,
|
| 198 |
+
return_aux: bool = False,
|
| 199 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 200 |
+
"""Computa BMU, vizinhança e loss de quantização.
|
| 201 |
+
|
| 202 |
+
Args:
|
| 203 |
+
h: (batch, d_cache) — entrada (saída projetada de H)
|
| 204 |
+
return_aux: se True, retorna também bmu_4d e quant_loss detalhado
|
| 205 |
+
|
| 206 |
+
Returns:
|
| 207 |
+
bmu_onehot: (batch, N_prototypes) — one-hot do BMU (com STE)
|
| 208 |
+
neighborhood: (batch, N_prototypes) — pesos gaussianos
|
| 209 |
+
quant_loss: escalar — Σ_i h_i · ||h - w_i||² (auxiliary loss)
|
| 210 |
+
"""
|
| 211 |
+
batch_size = h.shape[0]
|
| 212 |
+
|
| 213 |
+
# Flatten protótipos: (N_prototypes, d_cache)
|
| 214 |
+
W = self.prototypes.reshape(self.N_prototypes, self.d_cache)
|
| 215 |
+
|
| 216 |
+
# Distâncias Euidianas ao quadrado: (batch, N_prototypes)
|
| 217 |
+
# ||h - w_i||² = ||h||² - 2·h·w_i + ||w_i||²
|
| 218 |
+
h_sq = (h ** 2).sum(dim=-1, keepdim=True) # (batch, 1)
|
| 219 |
+
w_sq = (W ** 2).sum(dim=-1, keepdim=True).t() # (1, N_prototypes)
|
| 220 |
+
cross = h @ W.t() # (batch, N_prototypes)
|
| 221 |
+
distances = h_sq - 2.0 * cross + w_sq # (batch, N_prototypes)
|
| 222 |
+
distances = distances.clamp(min=0.0) # estabilidade numérica
|
| 223 |
+
|
| 224 |
+
# BMU via argmin com STE
|
| 225 |
+
bmu_onehot = ste_argmin(distances) # (batch, N_prototypes)
|
| 226 |
+
|
| 227 |
+
# Vizinhança gaussiana 4D
|
| 228 |
+
# Para cada batch, pegar o BMU e computar pesos para todos os protótipos
|
| 229 |
+
bmu_idx = distances.argmin(dim=-1) # (batch,)
|
| 230 |
+
bmu_coords = self.grid_coords[bmu_idx] # (batch, 4)
|
| 231 |
+
|
| 232 |
+
# Distância 4D entre cada protótipo e o BMU
|
| 233 |
+
# grid_coords: (N_prototypes, 4), bmu_coords: (batch, 4)
|
| 234 |
+
# → (batch, N_prototypes, 4) via broadcasting
|
| 235 |
+
diff = self.grid_coords.unsqueeze(0) - bmu_coords.unsqueeze(1) # (batch, N, 4)
|
| 236 |
+
grid_dist_sq = (diff ** 2).sum(dim=-1) # (batch, N_prototypes)
|
| 237 |
+
|
| 238 |
+
sigma = self.sigma_current.clamp(min=1e-3)
|
| 239 |
+
neighborhood = torch.exp(-grid_dist_sq / (2.0 * sigma ** 2)) # (batch, N)
|
| 240 |
+
|
| 241 |
+
# Loss de quantização (soft winner-takes-all ponderado pela vizinhança)
|
| 242 |
+
# L_q = Σ_i h_i · ||h - w_i||² (média sobre batch)
|
| 243 |
+
# Já temos distances = ||h - w_i||²
|
| 244 |
+
quant_loss = (neighborhood * distances).sum(dim=-1).mean() # escalar
|
| 245 |
+
|
| 246 |
+
# Atualiza step_count (apenas em modo treino)
|
| 247 |
+
if self.training:
|
| 248 |
+
with torch.no_grad():
|
| 249 |
+
self.step_count += 1
|
| 250 |
+
|
| 251 |
+
if not return_aux:
|
| 252 |
+
return bmu_onehot, neighborhood, quant_loss
|
| 253 |
+
|
| 254 |
+
# Auxiliar para logging
|
| 255 |
+
bmu_4d = bmu_coords.long() # (batch, 4)
|
| 256 |
+
return bmu_onehot, neighborhood, quant_loss, bmu_4d
|
| 257 |
+
|
| 258 |
+
@torch.no_grad()
|
| 259 |
+
def update_prototypes_online(self, h: torch.Tensor, bmu_onehot, neighborhood):
|
| 260 |
+
"""Atualização online dos protótipos (estilo SOM clássico).
|
| 261 |
+
|
| 262 |
+
w_i ← w_i + α · h_i · (h - w_i)
|
| 263 |
+
|
| 264 |
+
Chamado externamente quando se quer fazer a atualização SOM explícita
|
| 265 |
+
(em adição à atualização por gradiente via quant_loss).
|
| 266 |
+
"""
|
| 267 |
+
alpha = self.alpha_current
|
| 268 |
+
W = self.prototypes.reshape(self.N_prototypes, self.d_cache)
|
| 269 |
+
# h: (batch, d_cache), neighborhood: (batch, N_prototypes)
|
| 270 |
+
# Δw_i = α · Σ_batch h_i,batch · (h_batch - w_i) / batch_size
|
| 271 |
+
delta = neighborhood.t() @ h # (N_prototypes, d_cache)
|
| 272 |
+
delta = delta - neighborhood.sum(dim=0, keepdim=True).t() * W # (N, d_cache)
|
| 273 |
+
W.add_(alpha * delta / max(h.shape[0], 1))
|
| 274 |
+
self.prototypes.data = W.reshape(
|
| 275 |
+
self.n_bigru, self.S_sem, self.S_x, self.S_y, self.d_cache
|
| 276 |
+
)
|
| 277 |
+
|
| 278 |
+
def extra_repr(self) -> str:
|
| 279 |
+
return (
|
| 280 |
+
f"n_bigru={self.n_bigru}, S_sem={self.S_sem}, S_x={self.S_x}, "
|
| 281 |
+
f"S_y={self.S_y}, d_cache={self.d_cache}, "
|
| 282 |
+
f"N_prototypes={self.N_prototypes}, "
|
| 283 |
+
f"sigma={float(self.sigma_current.item()):.3f}, "
|
| 284 |
+
f"alpha={float(self.alpha_current.item()):.4f}, "
|
| 285 |
+
f"step={int(self.step_count.item())}"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 290 |
+
# OrqCell V6 — com KohonenSOM4D + Cross-Attention
|
| 291 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 292 |
class OrqCell(nn.Module):
|
| 293 |
+
"""OrqCell V6: Orquestrador com SOM 4D de Kohonen + cross-attention.
|
| 294 |
|
| 295 |
Args:
|
| 296 |
+
cache_len: nº de posições no cache aprendível (L) — mantido para
|
| 297 |
+
compatibilidade, mas em V6 o cache é o SOM 4D.
|
| 298 |
+
d_cache: dimensão do cache/protótipos
|
| 299 |
d_model_in: dimensão de entrada (saída dos u8cell_T)
|
| 300 |
nhead: nº de cabeças da cross-attention
|
| 301 |
d_ff: dimensão interna do FFN
|
| 302 |
dropout: probabilidade de dropout
|
| 303 |
+
n_bigru: nº de BiGRU4 no SOM (default 8)
|
| 304 |
+
S_sem, S_x, S_y: dimensões do SOM 4D
|
| 305 |
+
use_som: se True (default V6), usa SOM 4D; se False, cache V5 (compat)
|
| 306 |
|
| 307 |
Forward:
|
| 308 |
H: (batch, K, d_model_in) – saídas ponderadas dos K módulos
|
| 309 |
→ o: (batch, d_cache)
|
| 310 |
+
→ (se return_aux=True) aux: dict com bmu_4d, quant_loss, neighborhood_entropy
|
| 311 |
+
|
| 312 |
+
V6 vs V5:
|
| 313 |
+
* Cache V5 = (L, d_cache) parâmetros livres, sem topologia
|
| 314 |
+
* Cache V6 = SOM 4D (8, S_sem, S_x, S_y, d_cache) com topologia Kohonen
|
| 315 |
+
* V6 adiciona loss de quantização L_q ao aux_dict
|
| 316 |
+
* V6 mantém cross-attention (cache=queries, módulos=keys/values)
|
| 317 |
+
* V6 computa BMU + vizinhança para cada batch
|
| 318 |
"""
|
| 319 |
|
| 320 |
def __init__(
|
|
|
|
| 325 |
nhead: int,
|
| 326 |
d_ff: int,
|
| 327 |
dropout: float = 0.1,
|
| 328 |
+
n_bigru: int = 8,
|
| 329 |
+
S_sem: int = 4,
|
| 330 |
+
S_x: int = 4,
|
| 331 |
+
S_y: int = 4,
|
| 332 |
+
use_som: bool = True,
|
| 333 |
):
|
| 334 |
super().__init__()
|
| 335 |
self.cache_len = cache_len
|
| 336 |
self.d_cache = d_cache
|
| 337 |
+
self.use_som = use_som
|
|
|
|
|
|
|
| 338 |
|
| 339 |
# Projeção da entrada para o espaço do cache
|
| 340 |
self.proj_in = nn.Linear(d_model_in, d_cache)
|
| 341 |
|
| 342 |
+
if use_som:
|
| 343 |
+
# V6: SOM 4D de Kohonen como cache topológico
|
| 344 |
+
self.som = KohonenSOM4D(
|
| 345 |
+
n_bigru=n_bigru,
|
| 346 |
+
S_sem=S_sem,
|
| 347 |
+
S_x=S_x,
|
| 348 |
+
S_y=S_y,
|
| 349 |
+
d_cache=d_cache,
|
| 350 |
+
)
|
| 351 |
+
# Projeção do SOM (N_prototypes, d_cache) → (L, d_cache) para
|
| 352 |
+
# compatibilidade com a cross-attention (queries = L posições)
|
| 353 |
+
self.som_to_cache = nn.Linear(self.som.N_prototypes, cache_len, bias=False)
|
| 354 |
+
# Inicialização identidade-like (primeiras L linhas de N_prototypes)
|
| 355 |
+
with torch.no_grad():
|
| 356 |
+
eye = torch.eye(cache_len, self.som.N_prototypes)
|
| 357 |
+
self.som_to_cache.weight.copy_(eye * 0.1)
|
| 358 |
+
else:
|
| 359 |
+
# V5: cache aprendível simples (compat)
|
| 360 |
+
self.cache = nn.Parameter(torch.randn(cache_len, d_cache) * 0.02)
|
| 361 |
+
|
| 362 |
# Cross-attention: queries = cache, keys/values = H_proj
|
| 363 |
self.cross_attn = nn.MultiheadAttention(
|
| 364 |
d_cache, nhead, dropout=dropout, batch_first=True
|
| 365 |
)
|
| 366 |
|
| 367 |
+
# FFN (pre-norm style)
|
| 368 |
self.linear1 = nn.Linear(d_cache, d_ff)
|
| 369 |
self.linear2 = nn.Linear(d_ff, d_cache)
|
| 370 |
self.norm1 = nn.LayerNorm(d_cache)
|
|
|
|
| 373 |
self.dropout2 = nn.Dropout(dropout)
|
| 374 |
self.dropout_ff = nn.Dropout(dropout)
|
| 375 |
|
| 376 |
+
def forward(
|
| 377 |
+
self,
|
| 378 |
+
H: torch.Tensor,
|
| 379 |
+
return_aux: bool = False,
|
| 380 |
+
) -> torch.Tensor:
|
| 381 |
+
"""H: (batch, K, d_model_in) → o: (batch, d_cache).
|
| 382 |
+
|
| 383 |
+
Se return_aux=True, retorna (o, aux_dict) com métricas do SOM V6.
|
| 384 |
+
"""
|
| 385 |
batch_size = H.size(0)
|
| 386 |
|
| 387 |
# Projeta H para o espaço do cache
|
| 388 |
H_proj = self.proj_in(H) # (batch, K, d_cache)
|
| 389 |
|
| 390 |
+
# =====================================================================
|
| 391 |
+
# V6: Computa cache via SOM 4D
|
| 392 |
+
# =====================================================================
|
| 393 |
+
# h_mean: representação agregada de H para alimentar o SOM
|
| 394 |
+
h_mean = H_proj.mean(dim=1) # (batch, d_cache)
|
| 395 |
+
|
| 396 |
+
if self.use_som:
|
| 397 |
+
# SOM forward: retorna bmu_onehot, neighborhood, quant_loss, [bmu_4d]
|
| 398 |
+
som_out = self.som(h_mean, return_aux=True)
|
| 399 |
+
bmu_onehot, neighborhood, quant_loss, bmu_4d = som_out
|
| 400 |
+
|
| 401 |
+
# Reconstrói cache topológico:
|
| 402 |
+
# Para cada batch, o cache é uma combinação linear dos protótipos,
|
| 403 |
+
# ponderada pela vizinhança gaussiana do BMU.
|
| 404 |
+
# cache_b = Σ_i neighborhood[b,i] · W[i] → (batch, L, d_cache)
|
| 405 |
+
W_flat = self.som.prototypes.reshape(
|
| 406 |
+
self.som.N_prototypes, self.d_cache
|
| 407 |
+
) # (N_prototypes, d_cache)
|
| 408 |
+
# cache_b: (batch, N_prototypes, d_cache) via broadcasting
|
| 409 |
+
cache_topo = neighborhood.unsqueeze(-1) * W_flat.unsqueeze(0)
|
| 410 |
+
# Reduz para (batch, L, d_cache) via projeção linear
|
| 411 |
+
# cache_topo: (batch, N_prototypes, d_cache) → transp → (batch, d_cache, N_prototypes)
|
| 412 |
+
# → som_to_cache → (batch, d_cache, L) → transp → (batch, L, d_cache)
|
| 413 |
+
cache = self.som_to_cache(cache_topo.transpose(1, 2)).transpose(1, 2)
|
| 414 |
+
else:
|
| 415 |
+
# V5 compat: cache é parâmetro (L, d_cache), expande para batch
|
| 416 |
+
cache = self.cache.unsqueeze(0).expand(batch_size, -1, -1)
|
| 417 |
+
quant_loss = torch.tensor(0.0, device=H.device)
|
| 418 |
+
bmu_4d = None
|
| 419 |
+
neighborhood = None
|
| 420 |
|
| 421 |
+
# =====================================================================
|
| 422 |
# Cross-attention: queries = cache, keys/values = H_proj
|
| 423 |
+
# =====================================================================
|
| 424 |
normed_cache = self.norm1(cache)
|
| 425 |
+
normed_H = self.norm1(H_proj)
|
| 426 |
+
attn_out, _ = self.cross_attn(
|
| 427 |
+
normed_cache, normed_H, normed_H, need_weights=False
|
| 428 |
+
)
|
| 429 |
cache = cache + self.dropout1(attn_out)
|
| 430 |
|
| 431 |
# FFN pre-norm
|
|
|
|
| 435 |
|
| 436 |
# Saída = média sobre as L posições do cache
|
| 437 |
o = cache.mean(dim=1) # (batch, d_cache)
|
| 438 |
+
|
| 439 |
+
if not return_aux:
|
| 440 |
+
return o
|
| 441 |
+
|
| 442 |
+
# =====================================================================
|
| 443 |
+
# V6: Métricas auxiliares do SOM
|
| 444 |
+
# =====================================================================
|
| 445 |
+
aux = {
|
| 446 |
+
"som_quant_loss": float(quant_loss.item())
|
| 447 |
+
if isinstance(quant_loss, torch.Tensor)
|
| 448 |
+
else float(quant_loss),
|
| 449 |
+
}
|
| 450 |
+
if self.use_som:
|
| 451 |
+
aux["som_sigma"] = float(self.som.sigma_current.item())
|
| 452 |
+
aux["som_alpha"] = float(self.som.alpha_current.item())
|
| 453 |
+
aux["som_step"] = int(self.som.step_count.item())
|
| 454 |
+
aux["som_n_prototypes"] = self.som.N_prototypes
|
| 455 |
+
# Entropia da vizinhança (mede quão espalhada está a ativação)
|
| 456 |
+
if neighborhood is not None:
|
| 457 |
+
# Normaliza neighborhood para distribuição de probabilidade
|
| 458 |
+
neigh_norm = neighborhood / (neighborhood.sum(dim=-1, keepdim=True) + 1e-8)
|
| 459 |
+
entropy = -(neigh_norm * (neigh_norm + 1e-8).log()).sum(dim=-1).mean()
|
| 460 |
+
aux["som_neighborhood_entropy"] = float(entropy.item())
|
| 461 |
+
# Concentração do BMU (quanto menor, mais concentrado)
|
| 462 |
+
aux["som_bmu_concentration"] = float(
|
| 463 |
+
bmu_onehot.sum(dim=-1).mean().item()
|
| 464 |
+
)
|
| 465 |
+
if bmu_4d is not None:
|
| 466 |
+
# Distribuição dos BMUs por eixo (para logging)
|
| 467 |
+
aux["som_bmu_axis0_mean"] = float(bmu_4d[:, 0].float().mean().item())
|
| 468 |
+
aux["som_bmu_axis1_mean"] = float(bmu_4d[:, 1].float().mean().item())
|
| 469 |
+
aux["som_bmu_axis2_mean"] = float(bmu_4d[:, 2].float().mean().item())
|
| 470 |
+
aux["som_bmu_axis3_mean"] = float(bmu_4d[:, 3].float().mean().item())
|
| 471 |
+
|
| 472 |
+
return o, aux
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
__all__ = ["OrqCell", "KohonenSOM4D", "ste_argmin", "STEArgmin"]
|
src/bigru_t/model/window_context.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""window_context.py — Window Context para atenção de 2M tokens (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/token_compress.py
|
| 4 |
+
e mtp_adaptive_attention.py.
|
| 5 |
+
|
| 6 |
+
Estratégia para 2M tokens:
|
| 7 |
+
1. **ToMe Token Merging** (Bolya et al. 2023): reduz N → N-r tokens via
|
| 8 |
+
bipartite soft matching. R-prune por camada. Complexity: O(N · d).
|
| 9 |
+
2. **KNN Indexer** (IVF-like): k-means centroids, n_probe busca. Reduz
|
| 10 |
+
atenção O(N²) → O(N · n_probe · k_avg).
|
| 11 |
+
3. **Sliding Window**: divide sequência longa em janelas overlapped.
|
| 12 |
+
4. **Hierarchical Pooling**: 3 níveis de pooling (stride 4, 16, 64).
|
| 13 |
+
|
| 14 |
+
V6 melhoria vs V13.9.2:
|
| 15 |
+
* ToMe vetorizado (sem loop Python)
|
| 16 |
+
* KNN Indexer com centroids EMA (não recalcula k-means a cada step)
|
| 17 |
+
* Suporte a batch (V13.9.2 era single-sample)
|
| 18 |
+
* Memory-budget adaptativo: ajusta r dinamicamente para caber em RAM
|
| 19 |
+
|
| 20 |
+
REFERÊNCIAS:
|
| 21 |
+
- Bolya et al. (2023). "Token Merging: Your ViT but Faster". ICLR.
|
| 22 |
+
- Johnson et al. (2019). "Billion-scale similarity search with GPUs". IEEE TBD.
|
| 23 |
+
- Ratner et al. (2023). "Speeding up BERT by 2x with ToMe".
|
| 24 |
+
"""
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import math
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn as nn
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
from typing import Optional, Tuple
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 35 |
+
# ToMeCompressor — Token Merging via Bipartite Soft Matching
|
| 36 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 37 |
+
class ToMeCompressor(nn.Module):
|
| 38 |
+
"""Token Merging (Bolya 2023) — reduz N → N-r tokens.
|
| 39 |
+
|
| 40 |
+
Algoritmo:
|
| 41 |
+
1. Divide tokens em 2 grupos A (par) e B (ímpar) alternados
|
| 42 |
+
2. Para cada token em A, encontra o mais similar em B (cosine sim)
|
| 43 |
+
3. Os r pares com maior similaridade são merged (média ponderada)
|
| 44 |
+
4. Tokens não merged de A e B são concatenados na saída
|
| 45 |
+
|
| 46 |
+
V6: vetorizado, suporta batch, r adaptativo ao budget de memória.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
r: nº de tokens a remover por forward (default 8)
|
| 50 |
+
max_r: máximo de tokens removíveis (default 64)
|
| 51 |
+
adaptive: se True, ajusta r baseado no tamanho do batch (default True)
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(
|
| 55 |
+
self,
|
| 56 |
+
r: int = 8,
|
| 57 |
+
max_r: int = 64,
|
| 58 |
+
adaptive: bool = True,
|
| 59 |
+
):
|
| 60 |
+
super().__init__()
|
| 61 |
+
self.r = r
|
| 62 |
+
self.max_r = max_r
|
| 63 |
+
self.adaptive = adaptive
|
| 64 |
+
|
| 65 |
+
def forward(
|
| 66 |
+
self,
|
| 67 |
+
x: torch.Tensor,
|
| 68 |
+
r: Optional[int] = None,
|
| 69 |
+
) -> Tuple[torch.Tensor, dict]:
|
| 70 |
+
"""x: (batch, N, d) → (batch, N - r_merged, d).
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
merged: (batch, N - r_merged, d)
|
| 74 |
+
aux: dict com r_used, n_tokens_in, n_tokens_out
|
| 75 |
+
"""
|
| 76 |
+
B, N, D = x.shape
|
| 77 |
+
r_use = r if r is not None else self.r
|
| 78 |
+
if self.adaptive:
|
| 79 |
+
# Ajusta r para não remover mais que 50% dos tokens
|
| 80 |
+
r_use = min(r_use, N // 2, self.max_r)
|
| 81 |
+
if r_use <= 0 or N <= 2:
|
| 82 |
+
return x, {"r_used": 0, "n_tokens_in": N, "n_tokens_out": N}
|
| 83 |
+
|
| 84 |
+
# Passo 1: dividir tokens em A (par) e B (ímpar)
|
| 85 |
+
a = x[:, ::2, :] # (B, N//2, D)
|
| 86 |
+
b = x[:, 1::2, :] # (B, N - N//2, D) ou (B, N//2, D)
|
| 87 |
+
|
| 88 |
+
# Passo 2: similaridade cosine entre A e B
|
| 89 |
+
a_norm = F.normalize(a, dim=-1)
|
| 90 |
+
b_norm = F.normalize(b, dim=-1)
|
| 91 |
+
sim = a_norm @ b_norm.transpose(-1, -2) # (B, N_A, N_B)
|
| 92 |
+
|
| 93 |
+
# Passo 3: para cada token em A, encontra o mais similar em B
|
| 94 |
+
max_sim_a, max_idx_b = sim.max(dim=-1) # (B, N_A)
|
| 95 |
+
|
| 96 |
+
# Passo 4: seleciona os r_use pares com maior similaridade
|
| 97 |
+
# Garante que cada token de B só é merged com 1 de A (matching bipartido soft)
|
| 98 |
+
topk_vals, topk_idx_a = max_sim_a.topk(min(r_use, max_sim_a.shape[1] - 1), dim=-1)
|
| 99 |
+
|
| 100 |
+
# Merge: média dos pares selecionados
|
| 101 |
+
# Para os tokens de A selecionados, substitui pelo merge com B correspondente
|
| 102 |
+
merged_tokens = []
|
| 103 |
+
used_b_mask = torch.zeros_like(sim) # (B, N_A, N_B)
|
| 104 |
+
# Vectorizado: marca os pares usados
|
| 105 |
+
# topk_idx_a: (B, r_use), max_idx_b é (B, N_A) → precisamos gather
|
| 106 |
+
b_indices_for_topk = max_idx_b.gather(1, topk_idx_a) # (B, r_use)
|
| 107 |
+
|
| 108 |
+
# Tokens merged: média ponderada (peso 0.5 cada)
|
| 109 |
+
a_selected = a.gather(1, topk_idx_a.unsqueeze(-1).expand(-1, -1, D)) # (B, r_use, D)
|
| 110 |
+
b_selected = b.gather(1, b_indices_for_topk.unsqueeze(-1).expand(-1, -1, D)) # (B, r_use, D)
|
| 111 |
+
merged = 0.5 * (a_selected + b_selected) # (B, r_use, D)
|
| 112 |
+
|
| 113 |
+
# Tokens não merged de A: máscara booleana
|
| 114 |
+
a_mask = torch.ones(B, a.shape[1], dtype=torch.bool, device=x.device)
|
| 115 |
+
a_mask.scatter_(1, topk_idx_a, False)
|
| 116 |
+
a_remaining = a[a_mask].reshape(B, -1, D) # (B, N_A - r_use, D)
|
| 117 |
+
|
| 118 |
+
# Tokens não merged de B: máscara booleana
|
| 119 |
+
b_mask = torch.ones(B, b.shape[1], dtype=torch.bool, device=x.device)
|
| 120 |
+
b_mask.scatter_(1, b_indices_for_topk, False)
|
| 121 |
+
b_remaining = b[b_mask].reshape(B, -1, D) # (B, N_B - r_use, D)
|
| 122 |
+
|
| 123 |
+
# Concatena: merged + a_remaining + b_remaining
|
| 124 |
+
out = torch.cat([merged, a_remaining, b_remaining], dim=1) # (B, N - r_use, D)
|
| 125 |
+
|
| 126 |
+
return out, {
|
| 127 |
+
"r_used": r_use,
|
| 128 |
+
"n_tokens_in": N,
|
| 129 |
+
"n_tokens_out": out.shape[1],
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 134 |
+
# KNNIndexer — IVF-like Index for fast attention
|
| 135 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 136 |
+
class KNNIndexer(nn.Module):
|
| 137 |
+
"""KNN Indexer com centroids EMA (IVF-like).
|
| 138 |
+
|
| 139 |
+
Para 2M tokens:
|
| 140 |
+
1. Mantém K centroids (k-means inicializado, EMA atualizado)
|
| 141 |
+
2. Cada token é atribuído a 1 centroid (hard assignment) ou
|
| 142 |
+
n_probe centroids (soft assignment via top-k)
|
| 143 |
+
3. Atenção só é computada entre query e tokens dos n_probe centroids
|
| 144 |
+
mais próximos → O(N · n_probe · K/N) = O(n_probe · K)
|
| 145 |
+
|
| 146 |
+
V6: centroids atualizados via EMA (não k-means full) → diferente do IVF
|
| 147 |
+
clássico, é online e diferenciável.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
d: dimensão dos tokens
|
| 151 |
+
n_centroids: nº de centroids K (default 256)
|
| 152 |
+
n_probe: nº de centroids a consultar por query (default 8)
|
| 153 |
+
ema_decay: decay do EMA para centroids (default 0.99)
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
def __init__(
|
| 157 |
+
self,
|
| 158 |
+
d: int,
|
| 159 |
+
n_centroids: int = 256,
|
| 160 |
+
n_probe: int = 8,
|
| 161 |
+
ema_decay: float = 0.99,
|
| 162 |
+
):
|
| 163 |
+
super().__init__()
|
| 164 |
+
self.d = d
|
| 165 |
+
self.n_centroids = n_centroids
|
| 166 |
+
self.n_probe = min(n_probe, n_centroids)
|
| 167 |
+
self.ema_decay = ema_decay
|
| 168 |
+
|
| 169 |
+
# Centroids: (K, d) — inicializados random
|
| 170 |
+
self.register_buffer("centroids", torch.randn(n_centroids, d) * 0.02)
|
| 171 |
+
self.register_buffer("centroid_count", torch.zeros(n_centroids))
|
| 172 |
+
|
| 173 |
+
@torch.no_grad()
|
| 174 |
+
def update_centroids_ema(self, x: torch.Tensor) -> None:
|
| 175 |
+
"""Atualiza centroids via EMA (chamado durante treino).
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
x: (batch, N, d) — tokens do batch atual
|
| 179 |
+
"""
|
| 180 |
+
B, N, D = x.shape
|
| 181 |
+
x_flat = x.reshape(B * N, D)
|
| 182 |
+
# Atribui cada token ao centroid mais próximo
|
| 183 |
+
dists = torch.cdist(x_flat, self.centroids) # (B*N, K)
|
| 184 |
+
assignments = dists.argmin(dim=-1) # (B*N,)
|
| 185 |
+
|
| 186 |
+
# Atualiza centroids via EMA
|
| 187 |
+
for k in range(self.n_centroids):
|
| 188 |
+
mask = assignments == k
|
| 189 |
+
if mask.any():
|
| 190 |
+
new_c = x_flat[mask].mean(dim=0)
|
| 191 |
+
self.centroids[k] = (
|
| 192 |
+
self.ema_decay * self.centroids[k]
|
| 193 |
+
+ (1.0 - self.ema_decay) * new_c
|
| 194 |
+
)
|
| 195 |
+
self.centroid_count[k] += mask.sum().item()
|
| 196 |
+
|
| 197 |
+
def forward(
|
| 198 |
+
self,
|
| 199 |
+
query: torch.Tensor,
|
| 200 |
+
keys: torch.Tensor,
|
| 201 |
+
values: torch.Tensor,
|
| 202 |
+
) -> Tuple[torch.Tensor, dict]:
|
| 203 |
+
"""Atenção aproximada via IVF.
|
| 204 |
+
|
| 205 |
+
Args:
|
| 206 |
+
query: (batch, M, d)
|
| 207 |
+
keys: (batch, N, d)
|
| 208 |
+
values: (batch, N, d)
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
out: (batch, M, d) — aproximação de attention(query, keys, values)
|
| 212 |
+
aux: dict com n_probe, compression_ratio
|
| 213 |
+
"""
|
| 214 |
+
B, M, D = query.shape
|
| 215 |
+
_, N, _ = keys.shape
|
| 216 |
+
|
| 217 |
+
# 1. Atribui keys aos centroids mais próximos
|
| 218 |
+
key_dists = torch.cdist(keys, self.centroids) # (B, N, K)
|
| 219 |
+
key_assign = key_dists.argmin(dim=-1) # (B, N) — índice do centroid
|
| 220 |
+
|
| 221 |
+
# 2. Para cada query, encontra os n_probe centroids mais próximos
|
| 222 |
+
query_dists = torch.cdist(query, self.centroids) # (B, M, K)
|
| 223 |
+
_, probe_centroids = query_dists.topk(self.n_probe, dim=-1, largest=False) # (B, M, n_probe)
|
| 224 |
+
|
| 225 |
+
# 3. Para cada query, seleciona apenas keys cujo centroid está em probe_centroids
|
| 226 |
+
# Vectorizado: expande para (B, M, N) via broadcasting
|
| 227 |
+
# probe_centroids: (B, M, n_probe) → (B, M, 1, n_probe)
|
| 228 |
+
# key_assign: (B, N) → (B, 1, N, 1)
|
| 229 |
+
probe_expanded = probe_centroids.unsqueeze(2) # (B, M, 1, n_probe)
|
| 230 |
+
key_expanded = key_assign.unsqueeze(1).unsqueeze(-1) # (B, 1, N, 1)
|
| 231 |
+
# mask: (B, M, N) — True se key j está em um dos n_probe centroids da query i
|
| 232 |
+
mask = (probe_expanded == key_expanded).any(dim=-1) # (B, M, N)
|
| 233 |
+
|
| 234 |
+
# 4. Atenção masked
|
| 235 |
+
scores = (query @ keys.transpose(-1, -2)) / math.sqrt(D) # (B, M, N)
|
| 236 |
+
scores = scores.masked_fill(~mask, float("-inf"))
|
| 237 |
+
attn = F.softmax(scores, dim=-1) # (B, M, N)
|
| 238 |
+
# Se uma linha inteira é -inf, softmax dá NaN → substitui por 0
|
| 239 |
+
attn = torch.nan_to_num(attn, nan=0.0)
|
| 240 |
+
out = attn @ values # (B, M, D)
|
| 241 |
+
|
| 242 |
+
# Estatísticas
|
| 243 |
+
avg_keys_per_query = mask.float().sum(dim=-1).mean().item()
|
| 244 |
+
compression_ratio = N / max(avg_keys_per_query, 1.0)
|
| 245 |
+
|
| 246 |
+
return out, {
|
| 247 |
+
"n_probe": self.n_probe,
|
| 248 |
+
"n_centroids": self.n_centroids,
|
| 249 |
+
"avg_keys_per_query": avg_keys_per_query,
|
| 250 |
+
"compression_ratio": compression_ratio,
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 255 |
+
# WindowContext — Combina ToMe + KNN + Sliding Window
|
| 256 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 257 |
+
class WindowContext(nn.Module):
|
| 258 |
+
"""Window Context para 2M tokens — combina ToMe + KNN.
|
| 259 |
+
|
| 260 |
+
Pipeline:
|
| 261 |
+
1. Aplica ToMe para reduzir N → N-r tokens (compressão espacial)
|
| 262 |
+
2. Mantém KNN Indexer para atenção aproximada (compressão temporal)
|
| 263 |
+
3. Em inference: sliding window de tamanho W com overlap W//2
|
| 264 |
+
|
| 265 |
+
Args:
|
| 266 |
+
d: dimensão dos tokens
|
| 267 |
+
tome_r: tokens a remover por ToMe (default 8)
|
| 268 |
+
n_centroids: nº de centroids do KNN (default 64)
|
| 269 |
+
n_probe: nº de centroids a consultar (default 4)
|
| 270 |
+
window_size: tamanho da janela (default 4096)
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
def __init__(
|
| 274 |
+
self,
|
| 275 |
+
d: int,
|
| 276 |
+
tome_r: int = 8,
|
| 277 |
+
n_centroids: int = 64,
|
| 278 |
+
n_probe: int = 4,
|
| 279 |
+
window_size: int = 4096,
|
| 280 |
+
):
|
| 281 |
+
super().__init__()
|
| 282 |
+
self.d = d
|
| 283 |
+
self.window_size = window_size
|
| 284 |
+
self.tome = ToMeCompressor(r=tome_r)
|
| 285 |
+
self.knn = KNNIndexer(
|
| 286 |
+
d=d, n_centroids=n_centroids, n_probe=n_probe
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
def forward(
|
| 290 |
+
self,
|
| 291 |
+
x: torch.Tensor,
|
| 292 |
+
return_aux: bool = False,
|
| 293 |
+
) -> torch.Tensor:
|
| 294 |
+
"""x: (batch, N, d) → (batch, N - r, d) comprimido + indexado.
|
| 295 |
+
|
| 296 |
+
Aplica ToMe (se N > 2*r) e treina KNN centroids via EMA.
|
| 297 |
+
"""
|
| 298 |
+
# 1. ToMe compression
|
| 299 |
+
if x.shape[1] > 2 * self.tome.r:
|
| 300 |
+
x_compressed, tome_aux = self.tome(x)
|
| 301 |
+
else:
|
| 302 |
+
x_compressed = x
|
| 303 |
+
tome_aux = {"r_used": 0, "n_tokens_in": x.shape[1], "n_tokens_out": x.shape[1]}
|
| 304 |
+
|
| 305 |
+
# 2. Atualiza KNN centroids (treino apenas)
|
| 306 |
+
if self.training:
|
| 307 |
+
self.knn.update_centroids_ema(x_compressed)
|
| 308 |
+
|
| 309 |
+
# 3. Atenção aproximada via KNN (query=x_compressed, keys=x_compressed)
|
| 310 |
+
out, knn_aux = self.knn(x_compressed, x_compressed, x_compressed)
|
| 311 |
+
|
| 312 |
+
if not return_aux:
|
| 313 |
+
return out
|
| 314 |
+
|
| 315 |
+
return out, {
|
| 316 |
+
"tome": tome_aux,
|
| 317 |
+
"knn": knn_aux,
|
| 318 |
+
"compression_total": (
|
| 319 |
+
tome_aux["n_tokens_in"] / max(tome_aux["n_tokens_out"], 1)
|
| 320 |
+
) * max(knn_aux["compression_ratio"], 1.0),
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 325 |
+
# Estimativa de memória para 2M tokens
|
| 326 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 327 |
+
def estimate_2m_tokens_memory(
|
| 328 |
+
d: int = 128,
|
| 329 |
+
batch: int = 1,
|
| 330 |
+
dtype_bytes: int = 4,
|
| 331 |
+
) -> dict:
|
| 332 |
+
"""Estima memória para 2M tokens.
|
| 333 |
+
|
| 334 |
+
Sem WindowContext:
|
| 335 |
+
attention matrix: (batch, 2M, 2M) * dtype_bytes = 16 TB (float32)
|
| 336 |
+
tokens: (batch, 2M, d) * dtype_bytes = 1 GB
|
| 337 |
+
|
| 338 |
+
Com WindowContext (ToMe r=8 per layer, 12 layers):
|
| 339 |
+
tokens reduzidos: 2M - 12*8 = 1,999,904 (~99.99% retido)
|
| 340 |
+
KNN attention: (batch, 2M, n_probe * K/N) = (1, 2M, 4*64/2M) = 8 MB
|
| 341 |
+
|
| 342 |
+
Returns:
|
| 343 |
+
dict com estimativas em GB
|
| 344 |
+
"""
|
| 345 |
+
N = 2_000_000
|
| 346 |
+
attention_full_gb = (batch * N * N * dtype_bytes) / (1024 ** 4)
|
| 347 |
+
tokens_full_gb = (batch * N * d * dtype_bytes) / (1024 ** 3)
|
| 348 |
+
|
| 349 |
+
# Com ToMe + KNN
|
| 350 |
+
n_centroids = 64
|
| 351 |
+
n_probe = 4
|
| 352 |
+
K_per_query = n_probe * (N // n_centroids) # ~n_probe * cluster_size
|
| 353 |
+
attention_knn_gb = (batch * N * K_per_query * dtype_bytes) / (1024 ** 3)
|
| 354 |
+
|
| 355 |
+
return {
|
| 356 |
+
"n_tokens": N,
|
| 357 |
+
"d": d,
|
| 358 |
+
"batch": batch,
|
| 359 |
+
"attention_full_gb": attention_full_gb,
|
| 360 |
+
"tokens_full_gb": tokens_full_gb,
|
| 361 |
+
"attention_knn_gb": attention_knn_gb,
|
| 362 |
+
"reduction_factor": attention_full_gb / max(attention_knn_gb, 1e-9),
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
__all__ = [
|
| 367 |
+
"ToMeCompressor",
|
| 368 |
+
"KNNIndexer",
|
| 369 |
+
"WindowContext",
|
| 370 |
+
"estimate_2m_tokens_memory",
|
| 371 |
+
]
|
src/bigru_t/reasoning/reasoning_system.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""reasoning_system.py — Sistema de Raciocínio (Reasoning/Thinking) integrável (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/reasoning_engine.py
|
| 4 |
+
e distributed_reasoning_system.py.
|
| 5 |
+
|
| 6 |
+
Implementa um sistema de raciocínio multi-fase inspirado em System 2 de
|
| 7 |
+
Kahneman, com 8 fases (V13.9.2):
|
| 8 |
+
1. Think — pensamento inicial sobre o problema
|
| 9 |
+
2. Plan — planejamento de solução
|
| 10 |
+
3. Decompose — decomposição em subproblemas
|
| 11 |
+
4. Execute — execução dos passos
|
| 12 |
+
5. Monitor — monitoramento do progresso
|
| 13 |
+
6. Predict — predição de resultado
|
| 14 |
+
7. Adjust — ajuste baseado em feedback
|
| 15 |
+
8. Compose — composição da resposta final
|
| 16 |
+
|
| 17 |
+
V6 melhorias vs V13.9.2:
|
| 18 |
+
* Self-contained (sem PEVSystem distribuído)
|
| 19 |
+
* BayesianStrategySelector com Thompson sampling (era epsilon-greedy)
|
| 20 |
+
* Compatível com langchain (pode ser usado como tool)
|
| 21 |
+
* Vetorizado quando possível
|
| 22 |
+
|
| 23 |
+
REFERÊNCIAS:
|
| 24 |
+
- Kahneman (2011). "Thinking, Fast and Slow".
|
| 25 |
+
- Wei et al. (2022). "Chain-of-Thought Prompting".
|
| 26 |
+
- Yao et al. (2023). "Tree of Thoughts".
|
| 27 |
+
- Thompson (1933). "On the likelihood that one unknown probability exceeds
|
| 28 |
+
another" (Thompson sampling).
|
| 29 |
+
"""
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import math
|
| 33 |
+
import torch
|
| 34 |
+
import torch.nn as nn
|
| 35 |
+
import torch.nn.functional as F
|
| 36 |
+
from typing import Optional, List, Dict, Tuple
|
| 37 |
+
from collections import deque
|
| 38 |
+
import random
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 42 |
+
# ReasoningPhase — enum das 8 fases
|
| 43 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 44 |
+
REASONING_PHASES = [
|
| 45 |
+
"think",
|
| 46 |
+
"plan",
|
| 47 |
+
"decompose",
|
| 48 |
+
"execute",
|
| 49 |
+
"monitor",
|
| 50 |
+
"predict",
|
| 51 |
+
"adjust",
|
| 52 |
+
"compose",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 57 |
+
# BayesianStrategySelector — Thompson sampling
|
| 58 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 59 |
+
class BayesianStrategySelector(nn.Module):
|
| 60 |
+
"""Seletor de estratégia bayesiano (Thompson sampling).
|
| 61 |
+
|
| 62 |
+
Mantém uma distribuição Beta(α_s, β_s) para cada estratégia s.
|
| 63 |
+
A cada passo, amostra de cada Beta e escolhe a estratégia com maior amostra.
|
| 64 |
+
Após receber reward, atualiza α_s ou β_s.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
n_strategies: nº de estratégias (default 8 — uma por fase)
|
| 68 |
+
prior_alpha: α inicial (default 1.0 — uniforme)
|
| 69 |
+
prior_beta: β inicial (default 1.0 — uniforme)
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self,
|
| 74 |
+
n_strategies: int = 8,
|
| 75 |
+
prior_alpha: float = 1.0,
|
| 76 |
+
prior_beta: float = 1.0,
|
| 77 |
+
):
|
| 78 |
+
super().__init__()
|
| 79 |
+
self.n_strategies = n_strategies
|
| 80 |
+
# α e β das distribuições Beta (uma por estratégia)
|
| 81 |
+
self.register_buffer("alpha", torch.full((n_strategies,), float(prior_alpha)))
|
| 82 |
+
self.register_buffer("beta", torch.full((n_strategies,), float(prior_beta)))
|
| 83 |
+
|
| 84 |
+
def select(self) -> int:
|
| 85 |
+
"""Thompson sampling: amostra de cada Beta, retorna índice do maior."""
|
| 86 |
+
# Beta(α, β) sampling via torch.distributions
|
| 87 |
+
dist = torch.distributions.Beta(self.alpha, self.beta)
|
| 88 |
+
samples = dist.sample()
|
| 89 |
+
return int(samples.argmax().item())
|
| 90 |
+
|
| 91 |
+
def update(self, strategy: int, reward: float) -> None:
|
| 92 |
+
"""Atualiza a distribuição Beta da estratégia selecionada.
|
| 93 |
+
|
| 94 |
+
Beta(α, β) é conjugada de Bernoulli:
|
| 95 |
+
- reward ≥ 0.5 (sucesso): α ← α + 1
|
| 96 |
+
- reward < 0.5 (fracasso): β ← β + 1
|
| 97 |
+
"""
|
| 98 |
+
if reward >= 0.5:
|
| 99 |
+
self.alpha[strategy] += 1.0
|
| 100 |
+
else:
|
| 101 |
+
self.beta[strategy] += 1.0
|
| 102 |
+
|
| 103 |
+
def get_strategy_probs(self) -> torch.Tensor:
|
| 104 |
+
"""Retorna probabilidade esperada de cada estratégia (mean of Beta)."""
|
| 105 |
+
return self.alpha / (self.alpha + self.beta)
|
| 106 |
+
|
| 107 |
+
def extra_repr(self) -> str:
|
| 108 |
+
probs = self.get_strategy_probs()
|
| 109 |
+
return f"n_strategies={self.n_strategies}, probs={probs.tolist()}"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 113 |
+
# ReasoningEngine — motor de raciocínio 8-fase
|
| 114 |
+
# ════════════════════════════════════════════════════════���══════════════════
|
| 115 |
+
class ReasoningEngine(nn.Module):
|
| 116 |
+
"""Sistema de raciocínio multi-fase (V6).
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
d_model: dimensão do hidden state
|
| 120 |
+
vocab_size: tamanho do vocabulário
|
| 121 |
+
n_phases: nº de fases (default 8)
|
| 122 |
+
max_steps: máx de passos de raciocínio (default 16)
|
| 123 |
+
use_bayesian_selector: se True, usa Thompson sampling (default True)
|
| 124 |
+
|
| 125 |
+
Uso:
|
| 126 |
+
engine = ReasoningEngine(d_model=128, vocab_size=1000)
|
| 127 |
+
output = engine(inputs, hidden, targets)
|
| 128 |
+
"""
|
| 129 |
+
|
| 130 |
+
def __init__(
|
| 131 |
+
self,
|
| 132 |
+
d_model: int,
|
| 133 |
+
vocab_size: int,
|
| 134 |
+
n_phases: int = 8,
|
| 135 |
+
max_steps: int = 16,
|
| 136 |
+
use_bayesian_selector: bool = True,
|
| 137 |
+
):
|
| 138 |
+
super().__init__()
|
| 139 |
+
self.d_model = d_model
|
| 140 |
+
self.vocab_size = vocab_size
|
| 141 |
+
self.n_phases = n_phases
|
| 142 |
+
self.max_steps = max_steps
|
| 143 |
+
|
| 144 |
+
# Embedding de fase (um por fase)
|
| 145 |
+
self.phase_embedding = nn.Embedding(n_phases, d_model)
|
| 146 |
+
|
| 147 |
+
# Projeção do hidden + phase → logits da próxima fase
|
| 148 |
+
self.phase_proj = nn.Linear(2 * d_model, n_phases)
|
| 149 |
+
|
| 150 |
+
# Projeção final → logits de vocabulário
|
| 151 |
+
self.out_proj = nn.Linear(d_model, vocab_size)
|
| 152 |
+
|
| 153 |
+
# Seletor de estratégia
|
| 154 |
+
self.use_bayesian = use_bayesian_selector
|
| 155 |
+
if use_bayesian_selector:
|
| 156 |
+
self.strategy_selector = BayesianStrategySelector(n_strategies=n_phases)
|
| 157 |
+
|
| 158 |
+
# Histórico de raciocínio (para logging)
|
| 159 |
+
self.reasoning_history: List[Dict] = []
|
| 160 |
+
|
| 161 |
+
def forward(
|
| 162 |
+
self,
|
| 163 |
+
hidden: torch.Tensor,
|
| 164 |
+
targets: Optional[torch.Tensor] = None,
|
| 165 |
+
return_chain: bool = False,
|
| 166 |
+
) -> dict:
|
| 167 |
+
"""Executa raciocínio multi-fase.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
hidden: (batch, T, d_model) — hidden state do backbone
|
| 171 |
+
targets: (batch, T) — targets (opcional, para supervised loss)
|
| 172 |
+
return_chain: se True, retorna cadeia de raciocínio
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
dict com:
|
| 176 |
+
- logits: (B, T, vocab_size) — logits finais
|
| 177 |
+
- loss: escalar (se targets fornecido)
|
| 178 |
+
- chain: lista de fases executadas (se return_chain=True)
|
| 179 |
+
- aux: estatísticas
|
| 180 |
+
"""
|
| 181 |
+
B, T, D = hidden.shape
|
| 182 |
+
device = hidden.device
|
| 183 |
+
|
| 184 |
+
# Estado de raciocínio: começa com hidden médio sobre T
|
| 185 |
+
reasoning_state = hidden.mean(dim=1) # (B, D)
|
| 186 |
+
chain: List[Dict] = []
|
| 187 |
+
phase_logits_accum = torch.zeros(B, self.n_phases, device=device)
|
| 188 |
+
|
| 189 |
+
for step in range(self.max_steps):
|
| 190 |
+
# Seleciona fase (Thompson sampling ou determinístico)
|
| 191 |
+
if self.use_bayesian:
|
| 192 |
+
phase_idx = self.strategy_selector.select()
|
| 193 |
+
else:
|
| 194 |
+
phase_idx = step % self.n_phases
|
| 195 |
+
|
| 196 |
+
phase_emb = self.phase_embedding(
|
| 197 |
+
torch.tensor(phase_idx, device=device)
|
| 198 |
+
) # (D,)
|
| 199 |
+
|
| 200 |
+
# Combina reasoning_state com phase_emb
|
| 201 |
+
combined = torch.cat([
|
| 202 |
+
reasoning_state,
|
| 203 |
+
phase_emb.unsqueeze(0).expand(B, -1),
|
| 204 |
+
], dim=-1) # (B, 2D)
|
| 205 |
+
|
| 206 |
+
# Prediz próxima fase
|
| 207 |
+
phase_logits = self.phase_proj(combined) # (B, n_phases)
|
| 208 |
+
phase_logits_accum += phase_logits
|
| 209 |
+
|
| 210 |
+
# Atualiza reasoning_state (residual com phase_emb)
|
| 211 |
+
reasoning_state = reasoning_state + 0.1 * phase_emb.unsqueeze(0)
|
| 212 |
+
|
| 213 |
+
# Log da fase
|
| 214 |
+
phase_info = {
|
| 215 |
+
"step": step,
|
| 216 |
+
"phase": REASONING_PHASES[phase_idx] if phase_idx < len(REASONING_PHASES) else f"phase_{phase_idx}",
|
| 217 |
+
"phase_idx": phase_idx,
|
| 218 |
+
}
|
| 219 |
+
chain.append(phase_info)
|
| 220 |
+
|
| 221 |
+
# Reward (se targets fornecido): baseado em quão bem prediz
|
| 222 |
+
if targets is not None and self.use_bayesian:
|
| 223 |
+
# Reward aproximado: 1 se a próxima fase melhora a predição
|
| 224 |
+
# (aqui simplificado: 0.5 neutro)
|
| 225 |
+
self.strategy_selector.update(phase_idx, 0.5)
|
| 226 |
+
|
| 227 |
+
# Critério de parada: se fase volta a ser "think", termina
|
| 228 |
+
if step > 0 and phase_idx == 0:
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
# Logits finais: out_proj sobre reasoning_state expandido para T
|
| 232 |
+
final_hidden = reasoning_state.unsqueeze(1).expand(-1, T, -1) # (B, T, D)
|
| 233 |
+
# Combina com hidden original (residual)
|
| 234 |
+
final_hidden = final_hidden + 0.5 * hidden # (B, T, D)
|
| 235 |
+
logits = self.out_proj(final_hidden) # (B, T, vocab_size)
|
| 236 |
+
|
| 237 |
+
# Loss (se targets)
|
| 238 |
+
loss = None
|
| 239 |
+
if targets is not None:
|
| 240 |
+
loss = F.cross_entropy(
|
| 241 |
+
logits.reshape(-1, self.vocab_size),
|
| 242 |
+
targets.reshape(-1),
|
| 243 |
+
ignore_index=-100,
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Salva histórico
|
| 247 |
+
self.reasoning_history.append({
|
| 248 |
+
"chain": chain,
|
| 249 |
+
"n_steps": len(chain),
|
| 250 |
+
})
|
| 251 |
+
|
| 252 |
+
result = {
|
| 253 |
+
"logits": logits,
|
| 254 |
+
"loss": loss,
|
| 255 |
+
"aux": {
|
| 256 |
+
"n_reasoning_steps": len(chain),
|
| 257 |
+
"phases_visited": [c["phase"] for c in chain],
|
| 258 |
+
"phase_logits_mean": float(phase_logits_accum.mean().item()),
|
| 259 |
+
},
|
| 260 |
+
}
|
| 261 |
+
if return_chain:
|
| 262 |
+
result["chain"] = chain
|
| 263 |
+
|
| 264 |
+
return result
|
| 265 |
+
|
| 266 |
+
def get_last_chain(self) -> Optional[List[Dict]]:
|
| 267 |
+
"""Retorna a última cadeia de raciocínio."""
|
| 268 |
+
if not self.reasoning_history:
|
| 269 |
+
return None
|
| 270 |
+
return self.reasoning_history[-1].get("chain")
|
| 271 |
+
|
| 272 |
+
def clear_history(self) -> None:
|
| 273 |
+
"""Limpa histórico de raciocínio (libera RAM)."""
|
| 274 |
+
self.reasoning_history.clear()
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
__all__ = [
|
| 278 |
+
"REASONING_PHASES",
|
| 279 |
+
"BayesianStrategySelector",
|
| 280 |
+
"ReasoningEngine",
|
| 281 |
+
]
|
src/bigru_t/tools/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""tools/__init__.py — Pacote de ferramentas V6."""
|
| 2 |
+
from .tool_cache import ToolCache
|
| 3 |
+
from .tool_mutation import ToolVariant, ToolMutationEngine
|
| 4 |
+
from .tool_agent import CircuitState, ToolAgentCoordinator
|
| 5 |
+
|
| 6 |
+
__all__ = [
|
| 7 |
+
"ToolCache",
|
| 8 |
+
"ToolVariant",
|
| 9 |
+
"ToolMutationEngine",
|
| 10 |
+
"CircuitState",
|
| 11 |
+
"ToolAgentCoordinator",
|
| 12 |
+
]
|
src/bigru_t/tools/tool_agent.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""tool_agent.py — Agente coordenador de ferramentas (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/tool_agent.py.
|
| 4 |
+
|
| 5 |
+
Coordena chamadas de múltiplas ferramentas com:
|
| 6 |
+
- Circuit breaker (fallback se ferramenta falhar)
|
| 7 |
+
- Execução paralela (quando possível)
|
| 8 |
+
- Estratégias de fallback (tentar variante B se A falhar)
|
| 9 |
+
- MCP (Model Context Protocol) — interface similar
|
| 10 |
+
|
| 11 |
+
V6 melhorias vs V13.9.2:
|
| 12 |
+
* Self-contained (sem TrustNeuralAgent externo)
|
| 13 |
+
* Circuit breaker com threshold configurável
|
| 14 |
+
* Compatível com ToolMutationEngine (escolhe variantes automaticamente)
|
| 15 |
+
|
| 16 |
+
REFERÊNCIAS:
|
| 17 |
+
- Michael Nygard (2018). "Release It!" (circuit breaker pattern).
|
| 18 |
+
- Anthropic MCP (2024).
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import time
|
| 23 |
+
import threading
|
| 24 |
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
| 25 |
+
from dataclasses import dataclass, field
|
| 26 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class CircuitState:
|
| 31 |
+
"""Estado do circuit breaker para uma ferramenta."""
|
| 32 |
+
failures: int = 0
|
| 33 |
+
last_failure_time: float = 0.0
|
| 34 |
+
state: str = "closed" # "closed" | "open" | "half_open"
|
| 35 |
+
threshold: int = 3
|
| 36 |
+
reset_timeout: float = 60.0 # seconds
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ToolAgentCoordinator:
|
| 40 |
+
"""Coordenador de ferramentas com circuit breaker e fallback (V6).
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
mutation_engine: ToolMutationEngine (com ferramentas registradas)
|
| 44 |
+
max_workers: nº máximo de workers para execução paralela (default 4)
|
| 45 |
+
circuit_threshold: nº de falhas antes de abrir circuito (default 3)
|
| 46 |
+
circuit_reset_timeout: tempo para resetar circuito (default 60s)
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
mutation_engine=None,
|
| 52 |
+
max_workers: int = 4,
|
| 53 |
+
circuit_threshold: int = 3,
|
| 54 |
+
circuit_reset_timeout: float = 60.0,
|
| 55 |
+
):
|
| 56 |
+
self.engine = mutation_engine
|
| 57 |
+
self.max_workers = max_workers
|
| 58 |
+
self.circuit_threshold = circuit_threshold
|
| 59 |
+
self.circuit_reset_timeout = circuit_reset_timeout
|
| 60 |
+
# Estado do circuit breaker por ferramenta
|
| 61 |
+
self._circuits: Dict[str, CircuitState] = {}
|
| 62 |
+
self._lock = threading.Lock()
|
| 63 |
+
# Estatísticas
|
| 64 |
+
self._stats = {
|
| 65 |
+
"calls": 0, "successes": 0, "failures": 0,
|
| 66 |
+
"circuit_opens": 0, "fallbacks": 0, "parallel_calls": 0,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
def _get_circuit(self, tool_name: str) -> CircuitState:
|
| 70 |
+
"""Retorna (ou cria) o estado do circuit breaker para uma ferramenta."""
|
| 71 |
+
if tool_name not in self._circuits:
|
| 72 |
+
self._circuits[tool_name] = CircuitState(
|
| 73 |
+
threshold=self.circuit_threshold,
|
| 74 |
+
reset_timeout=self.circuit_reset_timeout,
|
| 75 |
+
)
|
| 76 |
+
return self._circuits[tool_name]
|
| 77 |
+
|
| 78 |
+
def _check_circuit(self, tool_name: str) -> bool:
|
| 79 |
+
"""Verifica se o circuito permite chamada. Retorna True se permitido."""
|
| 80 |
+
with self._lock:
|
| 81 |
+
circuit = self._get_circuit(tool_name)
|
| 82 |
+
if circuit.state == "open":
|
| 83 |
+
# Verifica se já pode resetar
|
| 84 |
+
if time.time() - circuit.last_failure_time > circuit.reset_timeout:
|
| 85 |
+
circuit.state = "half_open"
|
| 86 |
+
return True
|
| 87 |
+
return False
|
| 88 |
+
return True
|
| 89 |
+
|
| 90 |
+
def _record_success(self, tool_name: str) -> None:
|
| 91 |
+
"""Registra sucesso — fecha circuito se estava half_open."""
|
| 92 |
+
with self._lock:
|
| 93 |
+
circuit = self._get_circuit(tool_name)
|
| 94 |
+
if circuit.state == "half_open":
|
| 95 |
+
circuit.state = "closed"
|
| 96 |
+
circuit.failures = 0
|
| 97 |
+
|
| 98 |
+
def _record_failure(self, tool_name: str) -> None:
|
| 99 |
+
"""Registra falha — abre circuito se exceder threshold."""
|
| 100 |
+
with self._lock:
|
| 101 |
+
circuit = self._get_circuit(tool_name)
|
| 102 |
+
circuit.failures += 1
|
| 103 |
+
circuit.last_failure_time = time.time()
|
| 104 |
+
if circuit.failures >= circuit.threshold:
|
| 105 |
+
if circuit.state != "open":
|
| 106 |
+
circuit.state = "open"
|
| 107 |
+
self._stats["circuit_opens"] += 1
|
| 108 |
+
|
| 109 |
+
def call(
|
| 110 |
+
self,
|
| 111 |
+
tool_name: str,
|
| 112 |
+
*args,
|
| 113 |
+
fallback: Optional[str] = None,
|
| 114 |
+
**kwargs,
|
| 115 |
+
) -> Tuple[Any, Optional[Exception]]:
|
| 116 |
+
"""Chama uma ferramenta com circuit breaker e fallback.
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
tool_name: nome da ferramenta
|
| 120 |
+
*args, **kwargs: argumentos para a ferramenta
|
| 121 |
+
fallback: nome da ferramenta de fallback (opcional)
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
(result, error) — se error for None, result é válido
|
| 125 |
+
"""
|
| 126 |
+
self._stats["calls"] += 1
|
| 127 |
+
|
| 128 |
+
# Verifica circuit breaker
|
| 129 |
+
if not self._check_circuit(tool_name):
|
| 130 |
+
self._stats["failures"] += 1
|
| 131 |
+
if fallback and self._check_circuit(fallback):
|
| 132 |
+
self._stats["fallbacks"] += 1
|
| 133 |
+
try:
|
| 134 |
+
result = self.engine.call(fallback, *args, **kwargs)
|
| 135 |
+
self._record_success(fallback)
|
| 136 |
+
self._stats["successes"] += 1
|
| 137 |
+
return result, None
|
| 138 |
+
except Exception as e:
|
| 139 |
+
self._record_failure(fallback)
|
| 140 |
+
return None, e
|
| 141 |
+
return None, RuntimeError(f"Circuit open for {tool_name}")
|
| 142 |
+
|
| 143 |
+
try:
|
| 144 |
+
result = self.engine.call(tool_name, *args, **kwargs)
|
| 145 |
+
self._record_success(tool_name)
|
| 146 |
+
self._stats["successes"] += 1
|
| 147 |
+
return result, None
|
| 148 |
+
except Exception as e:
|
| 149 |
+
self._record_failure(tool_name)
|
| 150 |
+
self._stats["failures"] += 1
|
| 151 |
+
# Tenta fallback
|
| 152 |
+
if fallback and self._check_circuit(fallback):
|
| 153 |
+
self._stats["fallbacks"] += 1
|
| 154 |
+
try:
|
| 155 |
+
result = self.engine.call(fallback, *args, **kwargs)
|
| 156 |
+
self._record_success(fallback)
|
| 157 |
+
return result, None
|
| 158 |
+
except Exception as e2:
|
| 159 |
+
self._record_failure(fallback)
|
| 160 |
+
return None, e2
|
| 161 |
+
return None, e
|
| 162 |
+
|
| 163 |
+
def call_parallel(
|
| 164 |
+
self,
|
| 165 |
+
calls: List[Dict],
|
| 166 |
+
) -> List[Tuple[Any, Optional[Exception]]]:
|
| 167 |
+
"""Executa múltiplas chamadas em paralelo.
|
| 168 |
+
|
| 169 |
+
Args:
|
| 170 |
+
calls: lista de dicts com keys "name", "args", "kwargs", "fallback"
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
lista de (result, error) na mesma ordem das calls
|
| 174 |
+
"""
|
| 175 |
+
self._stats["parallel_calls"] += len(calls)
|
| 176 |
+
results: List[Tuple[Any, Optional[Exception]]] = [None] * len(calls)
|
| 177 |
+
|
| 178 |
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
| 179 |
+
futures = {}
|
| 180 |
+
for i, call_spec in enumerate(calls):
|
| 181 |
+
future = executor.submit(
|
| 182 |
+
self.call,
|
| 183 |
+
call_spec["name"],
|
| 184 |
+
*call_spec.get("args", []),
|
| 185 |
+
fallback=call_spec.get("fallback"),
|
| 186 |
+
**call_spec.get("kwargs", {}),
|
| 187 |
+
)
|
| 188 |
+
futures[future] = i
|
| 189 |
+
|
| 190 |
+
for future in as_completed(futures):
|
| 191 |
+
idx = futures[future]
|
| 192 |
+
try:
|
| 193 |
+
results[idx] = future.result()
|
| 194 |
+
except Exception as e:
|
| 195 |
+
results[idx] = (None, e)
|
| 196 |
+
|
| 197 |
+
return results
|
| 198 |
+
|
| 199 |
+
def stats(self) -> dict:
|
| 200 |
+
"""Retorna estatísticas do agente."""
|
| 201 |
+
total = self._stats["calls"]
|
| 202 |
+
success_rate = self._stats["successes"] / max(total, 1)
|
| 203 |
+
return {
|
| 204 |
+
**self._stats,
|
| 205 |
+
"success_rate": success_rate,
|
| 206 |
+
"circuits": {
|
| 207 |
+
name: {
|
| 208 |
+
"state": c.state,
|
| 209 |
+
"failures": c.failures,
|
| 210 |
+
"last_failure": c.last_failure_time,
|
| 211 |
+
}
|
| 212 |
+
for name, c in self._circuits.items()
|
| 213 |
+
},
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
def reset_circuit(self, tool_name: str) -> None:
|
| 217 |
+
"""Reseta manualmente o circuit breaker de uma ferramenta."""
|
| 218 |
+
with self._lock:
|
| 219 |
+
if tool_name in self._circuits:
|
| 220 |
+
self._circuits[tool_name].state = "closed"
|
| 221 |
+
self._circuits[tool_name].failures = 0
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
__all__ = ["CircuitState", "ToolAgentCoordinator"]
|
src/bigru_t/tools/tool_cache.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""tool_cache.py — Cache de ferramentas (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/tool_cache.py.
|
| 4 |
+
|
| 5 |
+
Cache LRU + TTL + persistência em disco para resultados de ferramentas.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
import hashlib
|
| 12 |
+
import pickle
|
| 13 |
+
import threading
|
| 14 |
+
from typing import Any, Optional, Dict, Callable
|
| 15 |
+
from collections import OrderedDict
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ToolCache:
|
| 19 |
+
"""Cache LRU + TTL + disco para resultados de ferramentas.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
max_size: tamanho máximo do cache em memória (nº de items)
|
| 23 |
+
ttl: time-to-live em segundos (default 3600 = 1h)
|
| 24 |
+
disk_path: diretório para cache em disco (default None = só memória)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
max_size: int = 1024,
|
| 30 |
+
ttl: float = 3600.0,
|
| 31 |
+
disk_path: Optional[str] = None,
|
| 32 |
+
):
|
| 33 |
+
self.max_size = max_size
|
| 34 |
+
self.ttl = ttl
|
| 35 |
+
self.disk_path = disk_path
|
| 36 |
+
self._cache: OrderedDict[str, dict] = OrderedDict()
|
| 37 |
+
self._lock = threading.Lock()
|
| 38 |
+
self._stats = {
|
| 39 |
+
"hits": 0, "misses": 0, "evictions": 0,
|
| 40 |
+
"disk_writes": 0, "disk_reads": 0,
|
| 41 |
+
}
|
| 42 |
+
if disk_path:
|
| 43 |
+
os.makedirs(disk_path, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
@staticmethod
|
| 46 |
+
def _make_key(tool_name: str, args: tuple, kwargs: dict) -> str:
|
| 47 |
+
key_str = f"{tool_name}|{args!r}|{sorted(kwargs.items())!r}"
|
| 48 |
+
return hashlib.sha256(key_str.encode()).hexdigest()
|
| 49 |
+
|
| 50 |
+
def _disk_path_for(self, key: str) -> Optional[str]:
|
| 51 |
+
if not self.disk_path:
|
| 52 |
+
return None
|
| 53 |
+
return os.path.join(self.disk_path, f"{key}.pkl")
|
| 54 |
+
|
| 55 |
+
def get(self, tool_name: str, args: tuple, kwargs: dict) -> Optional[Any]:
|
| 56 |
+
key = self._make_key(tool_name, args, kwargs)
|
| 57 |
+
with self._lock:
|
| 58 |
+
if key in self._cache:
|
| 59 |
+
entry = self._cache[key]
|
| 60 |
+
if time.time() - entry["time"] > self.ttl:
|
| 61 |
+
del self._cache[key]
|
| 62 |
+
self._stats["misses"] += 1
|
| 63 |
+
return None
|
| 64 |
+
self._cache.move_to_end(key)
|
| 65 |
+
self._stats["hits"] += 1
|
| 66 |
+
return entry["value"]
|
| 67 |
+
disk_path = self._disk_path_for(key)
|
| 68 |
+
if disk_path and os.path.exists(disk_path):
|
| 69 |
+
try:
|
| 70 |
+
with open(disk_path, "rb") as f:
|
| 71 |
+
entry = pickle.load(f)
|
| 72 |
+
if time.time() - entry["time"] > self.ttl:
|
| 73 |
+
os.remove(disk_path)
|
| 74 |
+
self._stats["misses"] += 1
|
| 75 |
+
return None
|
| 76 |
+
self._cache[key] = entry
|
| 77 |
+
self._stats["disk_reads"] += 1
|
| 78 |
+
self._stats["hits"] += 1
|
| 79 |
+
return entry["value"]
|
| 80 |
+
except Exception:
|
| 81 |
+
pass
|
| 82 |
+
self._stats["misses"] += 1
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def set(self, tool_name: str, args: tuple, kwargs: dict, value: Any) -> None:
|
| 86 |
+
key = self._make_key(tool_name, args, kwargs)
|
| 87 |
+
entry = {"value": value, "time": time.time()}
|
| 88 |
+
with self._lock:
|
| 89 |
+
if key in self._cache:
|
| 90 |
+
del self._cache[key]
|
| 91 |
+
while len(self._cache) >= self.max_size:
|
| 92 |
+
evicted_key, _ = self._cache.popitem(last=False)
|
| 93 |
+
self._stats["evictions"] += 1
|
| 94 |
+
disk_path = self._disk_path_for(evicted_key)
|
| 95 |
+
if disk_path and os.path.exists(disk_path):
|
| 96 |
+
try:
|
| 97 |
+
os.remove(disk_path)
|
| 98 |
+
except OSError:
|
| 99 |
+
pass
|
| 100 |
+
self._cache[key] = entry
|
| 101 |
+
disk_path = self._disk_path_for(key)
|
| 102 |
+
if disk_path:
|
| 103 |
+
try:
|
| 104 |
+
with open(disk_path, "wb") as f:
|
| 105 |
+
pickle.dump(entry, f, protocol=pickle.HIGHEST_PROTOCOL)
|
| 106 |
+
self._stats["disk_writes"] += 1
|
| 107 |
+
except Exception:
|
| 108 |
+
pass
|
| 109 |
+
|
| 110 |
+
def get_or_compute(
|
| 111 |
+
self, tool_name: str, args: tuple, kwargs: dict, compute_fn: Callable,
|
| 112 |
+
) -> Any:
|
| 113 |
+
cached = self.get(tool_name, args, kwargs)
|
| 114 |
+
if cached is not None:
|
| 115 |
+
return cached
|
| 116 |
+
value = compute_fn(*args, **kwargs)
|
| 117 |
+
self.set(tool_name, args, kwargs, value)
|
| 118 |
+
return value
|
| 119 |
+
|
| 120 |
+
def clear(self) -> None:
|
| 121 |
+
with self._lock:
|
| 122 |
+
self._cache.clear()
|
| 123 |
+
if self.disk_path and os.path.exists(self.disk_path):
|
| 124 |
+
for f in os.listdir(self.disk_path):
|
| 125 |
+
if f.endswith(".pkl"):
|
| 126 |
+
try:
|
| 127 |
+
os.remove(os.path.join(self.disk_path, f))
|
| 128 |
+
except OSError:
|
| 129 |
+
pass
|
| 130 |
+
|
| 131 |
+
def stats(self) -> dict:
|
| 132 |
+
with self._lock:
|
| 133 |
+
total = self._stats["hits"] + self._stats["misses"]
|
| 134 |
+
hit_rate = self._stats["hits"] / max(total, 1)
|
| 135 |
+
return {
|
| 136 |
+
**self._stats,
|
| 137 |
+
"hit_rate": hit_rate,
|
| 138 |
+
"size": len(self._cache),
|
| 139 |
+
"max_size": self.max_size,
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
__all__ = ["ToolCache"]
|
src/bigru_t/tools/tool_mutation.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""tool_mutation.py — Mutação de ferramentas (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/tool_mutation.py.
|
| 4 |
+
|
| 5 |
+
4 operações de mutação: compose, specialize, wrap, mutate.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any, Callable, Dict, List, Optional
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class ToolVariant:
|
| 16 |
+
"""Variante de uma ferramenta (resultado de mutação)."""
|
| 17 |
+
name: str
|
| 18 |
+
parent_name: Optional[str]
|
| 19 |
+
mutation_type: str # "original" | "compose" | "specialize" | "wrap" | "mutate"
|
| 20 |
+
fn: Callable
|
| 21 |
+
description: str
|
| 22 |
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
| 23 |
+
created_at: float = field(default_factory=time.time)
|
| 24 |
+
version: int = 1
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ToolMutationEngine:
|
| 28 |
+
"""Motor de mutação de ferramentas (V6).
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
cache: ToolCache para cache de variantes (opcional)
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, cache=None):
|
| 35 |
+
self.tools: Dict[str, ToolVariant] = {}
|
| 36 |
+
self.cache = cache
|
| 37 |
+
self.mutation_history: List[Dict] = []
|
| 38 |
+
|
| 39 |
+
def register(self, name: str, fn: Callable, description: str = "") -> None:
|
| 40 |
+
"""Registra uma ferramenta original."""
|
| 41 |
+
self.tools[name] = ToolVariant(
|
| 42 |
+
name=name,
|
| 43 |
+
parent_name=None,
|
| 44 |
+
mutation_type="original",
|
| 45 |
+
fn=fn,
|
| 46 |
+
description=description,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def compose(
|
| 50 |
+
self, new_name: str, tool_a: str, tool_b: str, description: str = "",
|
| 51 |
+
) -> ToolVariant:
|
| 52 |
+
"""Mutation: compose — output de A → input de B. Resultado: b(a(x))."""
|
| 53 |
+
if tool_a not in self.tools or tool_b not in self.tools:
|
| 54 |
+
raise ValueError(f"Ferramentas {tool_a} ou {tool_b} não registradas")
|
| 55 |
+
a_fn = self.tools[tool_a].fn
|
| 56 |
+
b_fn = self.tools[tool_b].fn
|
| 57 |
+
|
| 58 |
+
def composed_fn(*args, **kwargs):
|
| 59 |
+
intermediate = a_fn(*args, **kwargs)
|
| 60 |
+
return b_fn(intermediate)
|
| 61 |
+
|
| 62 |
+
variant = ToolVariant(
|
| 63 |
+
name=new_name, parent_name=f"{tool_a}+{tool_b}",
|
| 64 |
+
mutation_type="compose", fn=composed_fn,
|
| 65 |
+
description=description or f"compose({tool_a}, {tool_b})",
|
| 66 |
+
metadata={"parents": [tool_a, tool_b]},
|
| 67 |
+
)
|
| 68 |
+
self.tools[new_name] = variant
|
| 69 |
+
self.mutation_history.append({
|
| 70 |
+
"type": "compose", "new_name": new_name,
|
| 71 |
+
"parents": [tool_a, tool_b], "timestamp": time.time(),
|
| 72 |
+
})
|
| 73 |
+
return variant
|
| 74 |
+
|
| 75 |
+
def specialize(
|
| 76 |
+
self, new_name: str, tool_name: str,
|
| 77 |
+
precondition: Callable, description: str = "",
|
| 78 |
+
) -> ToolVariant:
|
| 79 |
+
"""Mutation: specialize — aplica pré-condição. a(x) se pre(x) else None."""
|
| 80 |
+
if tool_name not in self.tools:
|
| 81 |
+
raise ValueError(f"Ferramenta {tool_name} não registrada")
|
| 82 |
+
original_fn = self.tools[tool_name].fn
|
| 83 |
+
|
| 84 |
+
def specialized_fn(*args, **kwargs):
|
| 85 |
+
if precondition(*args, **kwargs):
|
| 86 |
+
return original_fn(*args, **kwargs)
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
variant = ToolVariant(
|
| 90 |
+
name=new_name, parent_name=tool_name,
|
| 91 |
+
mutation_type="specialize", fn=specialized_fn,
|
| 92 |
+
description=description or f"specialize({tool_name})",
|
| 93 |
+
metadata={"precondition": getattr(precondition, "__name__", "lambda")},
|
| 94 |
+
)
|
| 95 |
+
self.tools[new_name] = variant
|
| 96 |
+
self.mutation_history.append({
|
| 97 |
+
"type": "specialize", "new_name": new_name,
|
| 98 |
+
"parent": tool_name, "timestamp": time.time(),
|
| 99 |
+
})
|
| 100 |
+
return variant
|
| 101 |
+
|
| 102 |
+
def wrap(
|
| 103 |
+
self, new_name: str, tool_name: str,
|
| 104 |
+
pre_fn: Optional[Callable] = None, post_fn: Optional[Callable] = None,
|
| 105 |
+
description: str = "",
|
| 106 |
+
) -> ToolVariant:
|
| 107 |
+
"""Mutation: wrap — envolve ferramenta. post(a(pre(x)))."""
|
| 108 |
+
if tool_name not in self.tools:
|
| 109 |
+
raise ValueError(f"Ferramenta {tool_name} não registrada")
|
| 110 |
+
original_fn = self.tools[tool_name].fn
|
| 111 |
+
|
| 112 |
+
def wrapped_fn(*args, **kwargs):
|
| 113 |
+
if pre_fn is not None:
|
| 114 |
+
new_args = pre_fn(*args, **kwargs)
|
| 115 |
+
if isinstance(new_args, tuple):
|
| 116 |
+
args, kwargs = new_args[:1] + new_args[1:], {}
|
| 117 |
+
else:
|
| 118 |
+
args = (new_args,)
|
| 119 |
+
result = original_fn(*args, **kwargs)
|
| 120 |
+
if post_fn is not None:
|
| 121 |
+
result = post_fn(result)
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
variant = ToolVariant(
|
| 125 |
+
name=new_name, parent_name=tool_name,
|
| 126 |
+
mutation_type="wrap", fn=wrapped_fn,
|
| 127 |
+
description=description or f"wrap({tool_name})",
|
| 128 |
+
metadata={
|
| 129 |
+
"pre_fn": getattr(pre_fn, "__name__", None) if pre_fn else None,
|
| 130 |
+
"post_fn": getattr(post_fn, "__name__", None) if post_fn else None,
|
| 131 |
+
},
|
| 132 |
+
)
|
| 133 |
+
self.tools[new_name] = variant
|
| 134 |
+
self.mutation_history.append({
|
| 135 |
+
"type": "wrap", "new_name": new_name,
|
| 136 |
+
"parent": tool_name, "timestamp": time.time(),
|
| 137 |
+
})
|
| 138 |
+
return variant
|
| 139 |
+
|
| 140 |
+
def mutate(
|
| 141 |
+
self, new_name: str, tool_name: str,
|
| 142 |
+
param_overrides: Dict[str, Any], description: str = "",
|
| 143 |
+
) -> ToolVariant:
|
| 144 |
+
"""Mutation: mutate — altera hiperparâmetros."""
|
| 145 |
+
if tool_name not in self.tools:
|
| 146 |
+
raise ValueError(f"Ferramenta {tool_name} não registrada")
|
| 147 |
+
original_fn = self.tools[tool_name].fn
|
| 148 |
+
|
| 149 |
+
def mutated_fn(*args, **kwargs):
|
| 150 |
+
for k, v in param_overrides.items():
|
| 151 |
+
kwargs.setdefault(k, v)
|
| 152 |
+
return original_fn(*args, **kwargs)
|
| 153 |
+
|
| 154 |
+
variant = ToolVariant(
|
| 155 |
+
name=new_name, parent_name=tool_name,
|
| 156 |
+
mutation_type="mutate", fn=mutated_fn,
|
| 157 |
+
description=description or f"mutate({tool_name})",
|
| 158 |
+
metadata={"param_overrides": param_overrides},
|
| 159 |
+
)
|
| 160 |
+
self.tools[new_name] = variant
|
| 161 |
+
self.mutation_history.append({
|
| 162 |
+
"type": "mutate", "new_name": new_name,
|
| 163 |
+
"parent": tool_name, "overrides": param_overrides,
|
| 164 |
+
"timestamp": time.time(),
|
| 165 |
+
})
|
| 166 |
+
return variant
|
| 167 |
+
|
| 168 |
+
def call(self, name: str, *args, **kwargs) -> Any:
|
| 169 |
+
"""Executa uma ferramenta (original ou variante), com cache se disponível."""
|
| 170 |
+
if name not in self.tools:
|
| 171 |
+
raise ValueError(f"Ferramenta {name} não registrada")
|
| 172 |
+
variant = self.tools[name]
|
| 173 |
+
if self.cache is not None:
|
| 174 |
+
return self.cache.get_or_compute(name, args, kwargs, variant.fn)
|
| 175 |
+
return variant.fn(*args, **kwargs)
|
| 176 |
+
|
| 177 |
+
def list_tools(self) -> List[Dict]:
|
| 178 |
+
"""Lista todas as ferramentas e variantes."""
|
| 179 |
+
return [
|
| 180 |
+
{
|
| 181 |
+
"name": v.name, "parent": v.parent_name,
|
| 182 |
+
"mutation_type": v.mutation_type, "description": v.description,
|
| 183 |
+
"version": v.version,
|
| 184 |
+
}
|
| 185 |
+
for v in self.tools.values()
|
| 186 |
+
]
|
| 187 |
+
|
| 188 |
+
def get_history(self) -> List[Dict]:
|
| 189 |
+
"""Retorna histórico de mutações."""
|
| 190 |
+
return self.mutation_history.copy()
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
__all__ = ["ToolVariant", "ToolMutationEngine"]
|
src/bigru_t/training/elastic_weight_consolidation.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""elastic_weight_consolidation.py — EWC V6.
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/ewc.py.
|
| 4 |
+
|
| 5 |
+
EWC (Kirkpatrick et al. 2017): previne catastrophic forgetting em treino
|
| 6 |
+
continuado. Mantém Fisher Information Matrix diagonal de uma tarefa anterior
|
| 7 |
+
e penaliza mudanças nos pesos importantes.
|
| 8 |
+
|
| 9 |
+
Loss total: L = L_task + (λ/2) · Σ_i F_i · (θ_i - θ*_i)²
|
| 10 |
+
|
| 11 |
+
onde:
|
| 12 |
+
- F_i = Fisher diagonal = E[(∂log p(y|x) / ∂θ_i)²]
|
| 13 |
+
- θ* = pesos ótimos da tarefa anterior
|
| 14 |
+
- λ = peso do regularizador
|
| 15 |
+
|
| 16 |
+
V6 melhorias vs V13.9.2:
|
| 17 |
+
* Fisher com clipping logarítmico (preserva ordem, comprime 10^12 → ~10)
|
| 18 |
+
* Smoothing (moving average) para estabilidade
|
| 19 |
+
* λ adaptativo (baseado na loss atual)
|
| 20 |
+
* Correções de 4ª e 6ª ordem Taylor (opcional, default off)
|
| 21 |
+
* Memory-efficient: Fisher computado em batches (não todo o dataset)
|
| 22 |
+
|
| 23 |
+
REFERÊNCIAS:
|
| 24 |
+
- Kirkpatrick et al. (2017). "Overcoming catastrophic forgetting in neural
|
| 25 |
+
networks". PNAS 114(13):3521-3526.
|
| 26 |
+
- Huszár (2018). "Note on the quadratic penalties in EWC".
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import math
|
| 31 |
+
import torch
|
| 32 |
+
import torch.nn as nn
|
| 33 |
+
import torch.nn.functional as F
|
| 34 |
+
from typing import Optional, Dict
|
| 35 |
+
from collections import OrderedDict
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 39 |
+
# Fisher Information Matrix diagonal (com clipping logarítmico)
|
| 40 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 41 |
+
def compute_fisher_diagonal(
|
| 42 |
+
model: nn.Module,
|
| 43 |
+
inputs: torch.Tensor,
|
| 44 |
+
targets: torch.Tensor,
|
| 45 |
+
criterion: nn.Module = None,
|
| 46 |
+
n_samples: int = 1,
|
| 47 |
+
log_clip: bool = True,
|
| 48 |
+
clip_scale: float = 1e6,
|
| 49 |
+
) -> Dict[str, torch.Tensor]:
|
| 50 |
+
"""Computa a diagonal da Fisher Information Matrix.
|
| 51 |
+
|
| 52 |
+
F_i = E[(∂log p(y|x) / ∂θ_i)²] ≈ (1/N) · Σ_n (∂L(x_n, y_n) / ∂θ_i)²
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
model: modelo
|
| 56 |
+
inputs: (N, T, d) ou (N, ...) — batch de entradas
|
| 57 |
+
targets: (N, ...) — targets
|
| 58 |
+
criterion: função de perda (default: CrossEntropyLoss)
|
| 59 |
+
n_samples: nº de amostras de Monte Carlo (default 1)
|
| 60 |
+
log_clip: se True, aplica clipping logarítmico (preserva ordem)
|
| 61 |
+
clip_scale: escala do clipping (default 1e6)
|
| 62 |
+
|
| 63 |
+
Returns:
|
| 64 |
+
Dict[name → fisher_tensor] com mesmo shape de cada parâmetro
|
| 65 |
+
"""
|
| 66 |
+
if criterion is None:
|
| 67 |
+
criterion = nn.CrossEntropyLoss()
|
| 68 |
+
|
| 69 |
+
model.eval() # desativa dropout para Fisher estável
|
| 70 |
+
fisher: Dict[str, torch.Tensor] = {
|
| 71 |
+
n: torch.zeros_like(p) for n, p in model.named_parameters()
|
| 72 |
+
if p.requires_grad
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
for _ in range(n_samples):
|
| 76 |
+
model.zero_grad()
|
| 77 |
+
# Forward
|
| 78 |
+
out = model(inputs)
|
| 79 |
+
if isinstance(out, tuple):
|
| 80 |
+
out = out[0] # modelo pode retornar (logits, aux)
|
| 81 |
+
# Reshape se necessário
|
| 82 |
+
if out.dim() > 2:
|
| 83 |
+
out = out.reshape(-1, out.shape[-1])
|
| 84 |
+
tgt = targets.reshape(-1)
|
| 85 |
+
else:
|
| 86 |
+
tgt = targets
|
| 87 |
+
loss = criterion(out, tgt)
|
| 88 |
+
# Backward
|
| 89 |
+
loss.backward()
|
| 90 |
+
|
| 91 |
+
# Acumula (grad)² na Fisher
|
| 92 |
+
for name, p in model.named_parameters():
|
| 93 |
+
if p.requires_grad and p.grad is not None:
|
| 94 |
+
fisher[name] += (p.grad.detach() ** 2) / n_samples
|
| 95 |
+
|
| 96 |
+
model.zero_grad()
|
| 97 |
+
|
| 98 |
+
# Clipping logarítmico (opcional)
|
| 99 |
+
if log_clip:
|
| 100 |
+
for name in fisher:
|
| 101 |
+
f = fisher[name]
|
| 102 |
+
# log_clip: F_clipped = log(1 + F / clip_scale) · clip_scale
|
| 103 |
+
# Preserva ordem (monótono), comprime grandes magnitudes
|
| 104 |
+
fisher[name] = torch.log1p(f / clip_scale) * clip_scale
|
| 105 |
+
|
| 106 |
+
return fisher
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 110 |
+
# ElasticWeightConsolidation — regularizador EWC
|
| 111 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 112 |
+
class ElasticWeightConsolidation(nn.Module):
|
| 113 |
+
"""EWC — penaliza mudanças em pesos importantes de tarefas anteriores.
|
| 114 |
+
|
| 115 |
+
Args:
|
| 116 |
+
model: modelo a ser regularizado
|
| 117 |
+
lambda_init: λ inicial (peso do regularizador) — default 100.0
|
| 118 |
+
adaptive_lambda: se True, λ é adaptativo (default True)
|
| 119 |
+
fisher_smoothing: decay do EMA para Fisher (default 0.99)
|
| 120 |
+
log_clip: se True, aplica clipping logarítmico no Fisher (default True)
|
| 121 |
+
|
| 122 |
+
Uso:
|
| 123 |
+
ewc = ElasticWeightConsolidation(model)
|
| 124 |
+
|
| 125 |
+
# Após treinar tarefa A, computa Fisher e θ*
|
| 126 |
+
ewc.consolidate(inputs_A, targets_A)
|
| 127 |
+
|
| 128 |
+
# Durante treino da tarefa B, adiciona loss EWC à loss total
|
| 129 |
+
loss_total = loss_task + ewc.penalty()
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
def __init__(
|
| 133 |
+
self,
|
| 134 |
+
model: nn.Module,
|
| 135 |
+
lambda_init: float = 100.0,
|
| 136 |
+
adaptive_lambda: bool = True,
|
| 137 |
+
fisher_smoothing: float = 0.99,
|
| 138 |
+
log_clip: bool = True,
|
| 139 |
+
clip_scale: float = 1e6,
|
| 140 |
+
):
|
| 141 |
+
super().__init__()
|
| 142 |
+
self.model = model
|
| 143 |
+
self.fisher_smoothing = fisher_smoothing
|
| 144 |
+
self.log_clip = log_clip
|
| 145 |
+
self.clip_scale = clip_scale
|
| 146 |
+
|
| 147 |
+
# λ (peso do regularizador) — log-space para positividade
|
| 148 |
+
self.log_lambda = nn.Parameter(torch.tensor(math.log(lambda_init)))
|
| 149 |
+
self.adaptive_lambda = adaptive_lambda
|
| 150 |
+
|
| 151 |
+
# θ* (pesos ótimos da tarefa anterior) e F (Fisher diagonal)
|
| 152 |
+
# Inicializados como zeros; serão preenchidos por consolidate()
|
| 153 |
+
self.star_params: Dict[str, torch.Tensor] = {}
|
| 154 |
+
self.fisher: Dict[str, torch.Tensor] = {}
|
| 155 |
+
self.consolidated = False
|
| 156 |
+
|
| 157 |
+
# Track da loss anterior para λ adaptativo
|
| 158 |
+
self.register_buffer("prev_loss", torch.tensor(0.0))
|
| 159 |
+
|
| 160 |
+
@property
|
| 161 |
+
def lambda_val(self) -> torch.Tensor:
|
| 162 |
+
"""λ atual = exp(log_lambda)."""
|
| 163 |
+
return torch.exp(self.log_lambda)
|
| 164 |
+
|
| 165 |
+
@torch.no_grad()
|
| 166 |
+
def consolidate(
|
| 167 |
+
self,
|
| 168 |
+
inputs: torch.Tensor,
|
| 169 |
+
targets: torch.Tensor,
|
| 170 |
+
criterion: nn.Module = None,
|
| 171 |
+
n_samples: int = 1,
|
| 172 |
+
) -> dict:
|
| 173 |
+
"""Computa Fisher e θ* após treinar uma tarefa.
|
| 174 |
+
|
| 175 |
+
Args:
|
| 176 |
+
inputs, targets: batch representativo da tarefa concluída
|
| 177 |
+
criterion: função de perda
|
| 178 |
+
n_samples: nº de amostras MC para Fisher
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
dict com estatísticas (fisher_mean, fisher_max, n_params)
|
| 182 |
+
"""
|
| 183 |
+
# 1. Salva θ* (clone dos pesos atuais)
|
| 184 |
+
self.star_params = {
|
| 185 |
+
n: p.detach().clone() for n, p in self.model.named_parameters()
|
| 186 |
+
if p.requires_grad
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
# 2. Computa Fisher diagonal
|
| 190 |
+
new_fisher = compute_fisher_diagonal(
|
| 191 |
+
self.model, inputs, targets,
|
| 192 |
+
criterion=criterion, n_samples=n_samples,
|
| 193 |
+
log_clip=self.log_clip, clip_scale=self.clip_scale,
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# 3. Suavização EMA (se já tinha Fisher anterior)
|
| 197 |
+
if self.consolidated and self.fisher:
|
| 198 |
+
for name in new_fisher:
|
| 199 |
+
if name in self.fisher:
|
| 200 |
+
self.fisher[name] = (
|
| 201 |
+
self.fisher_smoothing * self.fisher[name]
|
| 202 |
+
+ (1.0 - self.fisher_smoothing) * new_fisher[name]
|
| 203 |
+
)
|
| 204 |
+
else:
|
| 205 |
+
self.fisher[name] = new_fisher[name]
|
| 206 |
+
else:
|
| 207 |
+
self.fisher = new_fisher
|
| 208 |
+
self.consolidated = True
|
| 209 |
+
|
| 210 |
+
# Estatísticas
|
| 211 |
+
all_f = torch.cat([f.flatten() for f in self.fisher.values()])
|
| 212 |
+
return {
|
| 213 |
+
"fisher_mean": float(all_f.mean().item()),
|
| 214 |
+
"fisher_max": float(all_f.max().item()),
|
| 215 |
+
"fisher_min": float(all_f.min().item()),
|
| 216 |
+
"n_params": int(all_f.numel()),
|
| 217 |
+
"lambda": float(self.lambda_val.item()),
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
def penalty(self) -> torch.Tensor:
|
| 221 |
+
"""Computa penalidade EWC: (λ/2) · Σ_i F_i · (θ_i - θ*_i)².
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
escalar — penalidade EWC
|
| 225 |
+
"""
|
| 226 |
+
if not self.consolidated:
|
| 227 |
+
return torch.tensor(0.0, device=next(self.model.parameters()).device)
|
| 228 |
+
|
| 229 |
+
loss = torch.tensor(0.0, device=next(self.model.parameters()).device)
|
| 230 |
+
lam = self.lambda_val
|
| 231 |
+
|
| 232 |
+
for name, p in self.model.named_parameters():
|
| 233 |
+
if not p.requires_grad:
|
| 234 |
+
continue
|
| 235 |
+
if name not in self.fisher or name not in self.star_params:
|
| 236 |
+
continue
|
| 237 |
+
F = self.fisher[name].to(p.device)
|
| 238 |
+
theta_star = self.star_params[name].to(p.device)
|
| 239 |
+
loss = loss + (F * (p - theta_star) ** 2).sum()
|
| 240 |
+
|
| 241 |
+
return 0.5 * lam * loss
|
| 242 |
+
|
| 243 |
+
@torch.no_grad()
|
| 244 |
+
def update_lambda_adaptive(self, current_loss: float) -> float:
|
| 245 |
+
"""Atualiza λ adaptativamente baseado na loss atual.
|
| 246 |
+
|
| 247 |
+
Se loss subiu muito (catastrophic forgetting), aumenta λ.
|
| 248 |
+
Se loss está baixa, mantém λ (não interfere).
|
| 249 |
+
|
| 250 |
+
Args:
|
| 251 |
+
current_loss: loss da tarefa atual
|
| 252 |
+
|
| 253 |
+
Returns:
|
| 254 |
+
novo λ
|
| 255 |
+
"""
|
| 256 |
+
if not self.adaptive_lambda:
|
| 257 |
+
return float(self.lambda_val.item())
|
| 258 |
+
|
| 259 |
+
prev = float(self.prev_loss.item())
|
| 260 |
+
# Se loss subiu mais que 50%, aumenta λ (catastrophic forgetting detectado)
|
| 261 |
+
if current_loss > 1.5 * prev and prev > 0:
|
| 262 |
+
self.log_lambda.data += 0.1 # λ *= exp(0.1) ≈ 1.10
|
| 263 |
+
elif current_loss < prev:
|
| 264 |
+
# Loss caiu — pode reduzir λ gradativamente
|
| 265 |
+
self.log_lambda.data -= 0.01 # λ /= exp(0.01) ≈ 0.99
|
| 266 |
+
|
| 267 |
+
self.prev_loss.fill_(current_loss)
|
| 268 |
+
return float(self.lambda_val.item())
|
| 269 |
+
|
| 270 |
+
def extra_repr(self) -> str:
|
| 271 |
+
return (
|
| 272 |
+
f"lambda={float(self.lambda_val.item()):.3f}, "
|
| 273 |
+
f"consolidated={self.consolidated}, "
|
| 274 |
+
f"adaptive_lambda={self.adaptive_lambda}"
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
__all__ = [
|
| 279 |
+
"compute_fisher_diagonal",
|
| 280 |
+
"ElasticWeightConsolidation",
|
| 281 |
+
]
|
src/bigru_t/training/rlhf_possibility_mapping.py
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""rlhf_possibility_mapping.py — Mapeamento de Possibilidades Aprendível com RLHF (V6).
|
| 2 |
+
|
| 3 |
+
Adaptado de gru-ring-v13-9-2/source/xavante_work/flexnet/learnable_mapping.py
|
| 4 |
+
e punishment_reward_system.py.
|
| 5 |
+
|
| 6 |
+
Implementa o mapeamento de possibilidades aprendível com RLHF (Reinforcement
|
| 7 |
+
Learning from Human Feedback). A fórmula central (Teorema 2 do V13.9.2):
|
| 8 |
+
|
| 9 |
+
Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
|
| 10 |
+
|
| 11 |
+
onde:
|
| 12 |
+
- Θ: parâmetros base do modelo
|
| 13 |
+
- η: learning rate
|
| 14 |
+
- r: reward do humano
|
| 15 |
+
- σ(trust · τ_T): gate de confiança (TrustNeuralAgent interno, V6 self-contained)
|
| 16 |
+
- wᵢ: pesos cooperativos (wᵢ ∝ 1/‖gᵢ‖²) — Teorema 2
|
| 17 |
+
- gᵢ: gradientes das componentes
|
| 18 |
+
- P_rep: penalidade de repetição
|
| 19 |
+
- λ_rep: peso da penalidade de repetição
|
| 20 |
+
|
| 21 |
+
V6 melhorias vs V13.9.2:
|
| 22 |
+
* Self-contained: TrustNeuralAgent interno (sem dependência externa)
|
| 23 |
+
* Pesos cooperativos wᵢ calculados online (não pré-computados)
|
| 24 |
+
* Momentum com decay 0.999 (estabilidade)
|
| 25 |
+
* Compatível com PPO simplificado (sem actor-critic completo)
|
| 26 |
+
|
| 27 |
+
REFERÊNCIAS:
|
| 28 |
+
- Schulman et al. (2017). "Proximal Policy Optimization Algorithms".
|
| 29 |
+
- Ouyang et al. (2022). "Training language models to follow instructions with
|
| 30 |
+
human feedback" (InstructGPT).
|
| 31 |
+
- Christiano et al. (2017). "Deep reinforcement learning from human preferences".
|
| 32 |
+
"""
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import math
|
| 36 |
+
import torch
|
| 37 |
+
import torch.nn as nn
|
| 38 |
+
import torch.nn.functional as F
|
| 39 |
+
from typing import Optional, List, Dict
|
| 40 |
+
from collections import deque
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 44 |
+
# RewardModel — modelo de recompensa simples (placeholder, V6)
|
| 45 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 46 |
+
class RewardModel(nn.Module):
|
| 47 |
+
"""Modelo de recompensa simples — projeta hidden state para escalar.
|
| 48 |
+
|
| 49 |
+
Em produção, seria treinado com preferências humanas (Bradley-Terry).
|
| 50 |
+
Aqui é um placeholder linear para fins de integração.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
d_model: dimensão do hidden state
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, d_model: int):
|
| 57 |
+
super().__init__()
|
| 58 |
+
self.proj = nn.Sequential(
|
| 59 |
+
nn.Linear(d_model, d_model // 2),
|
| 60 |
+
nn.ReLU(),
|
| 61 |
+
nn.Linear(d_model // 2, 1),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def forward(self, hidden: torch.Tensor) -> torch.Tensor:
|
| 65 |
+
"""hidden: (batch, T, d) → reward: (batch,) — média sobre T."""
|
| 66 |
+
return self.proj(hidden).squeeze(-1).mean(dim=-1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 70 |
+
# TrustNeuralAgent — self-contained (V6)
|
| 71 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 72 |
+
class TrustNeuralAgent(nn.Module):
|
| 73 |
+
"""Agente de confiança self-contained (sem dependência externa).
|
| 74 |
+
|
| 75 |
+
Mantém um embedding de "trust" por token, atualizado via TD-learning.
|
| 76 |
+
Em V13.9.2, era uma classe externa com dependências complexas. Em V6,
|
| 77 |
+
é uma camada simples com:
|
| 78 |
+
- Embedding de trust (d_model) por vocab_size
|
| 79 |
+
- Atualização TD: trust[t+1] = trust[t] + α · (reward - trust[t])
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
vocab_size: tamanho do vocabulário
|
| 83 |
+
d_model: dimensão do trust embedding
|
| 84 |
+
td_alpha: taxa de aprendizado TD (default 0.1)
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(
|
| 88 |
+
self,
|
| 89 |
+
vocab_size: int,
|
| 90 |
+
d_model: int,
|
| 91 |
+
td_alpha: float = 0.1,
|
| 92 |
+
):
|
| 93 |
+
super().__init__()
|
| 94 |
+
self.vocab_size = vocab_size
|
| 95 |
+
self.d_model = d_model
|
| 96 |
+
self.td_alpha = td_alpha
|
| 97 |
+
# Trust embedding: (vocab_size, d_model) — inicializado neutro (0.5)
|
| 98 |
+
self.trust_embedding = nn.Parameter(torch.zeros(vocab_size, d_model))
|
| 99 |
+
# tau_T: temperatura (aprendível)
|
| 100 |
+
self.log_tau_T = nn.Parameter(torch.tensor(0.0)) # τ_T = 1.0
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def tau_T(self) -> torch.Tensor:
|
| 104 |
+
return torch.exp(self.log_tau_T)
|
| 105 |
+
|
| 106 |
+
def forward(
|
| 107 |
+
self,
|
| 108 |
+
token_ids: torch.Tensor,
|
| 109 |
+
hidden: torch.Tensor,
|
| 110 |
+
) -> torch.Tensor:
|
| 111 |
+
"""Computa σ(trust · τ_T) para os tokens dados.
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
token_ids: (batch, T) — IDs dos tokens
|
| 115 |
+
hidden: (batch, T, d_model) — hidden states (não usado aqui,
|
| 116 |
+
mas mantido para compat)
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
trust_gate: (batch, T) — σ(mean(trust_embedding[token]) · τ_T)
|
| 120 |
+
"""
|
| 121 |
+
trust = self.trust_embedding[token_ids] # (B, T, d)
|
| 122 |
+
trust_scalar = trust.mean(dim=-1) # (B, T)
|
| 123 |
+
# σ(trust · τ_T) — gate de confiança ∈ (0, 1)
|
| 124 |
+
gate = torch.sigmoid(trust_scalar * self.tau_T)
|
| 125 |
+
return gate
|
| 126 |
+
|
| 127 |
+
@torch.no_grad()
|
| 128 |
+
def update_td(self, token_ids: torch.Tensor, rewards: torch.Tensor) -> None:
|
| 129 |
+
"""Atualização TD do trust embedding.
|
| 130 |
+
|
| 131 |
+
trust[token] ← trust[token] + α · (reward - trust[token])
|
| 132 |
+
|
| 133 |
+
Args:
|
| 134 |
+
token_ids: (batch, T) — tokens que receberam feedback
|
| 135 |
+
rewards: (batch, T) ou (batch,) — recompensas
|
| 136 |
+
"""
|
| 137 |
+
if rewards.dim() == 1:
|
| 138 |
+
rewards = rewards.unsqueeze(-1).expand(-1, token_ids.shape[1])
|
| 139 |
+
for b in range(token_ids.shape[0]):
|
| 140 |
+
for t in range(token_ids.shape[1]):
|
| 141 |
+
tid = int(token_ids[b, t].item())
|
| 142 |
+
r = float(rewards[b, t].item())
|
| 143 |
+
# Atualiza apenas o primeiro canal (escalar)
|
| 144 |
+
current = float(self.trust_embedding[tid, 0].item())
|
| 145 |
+
self.trust_embedding.data[tid, 0] = current + self.td_alpha * (r - current)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 149 |
+
# LearnablePossibilityMapping — RLHF com mapeamento de possibilidades
|
| 150 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 151 |
+
class LearnablePossibilityMapping(nn.Module):
|
| 152 |
+
"""Mapeamento de Possibilidades Aprendível com RLHF (V6).
|
| 153 |
+
|
| 154 |
+
Implementa a fórmula central (Teorema 2 do V13.9.2):
|
| 155 |
+
Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
|
| 156 |
+
|
| 157 |
+
Componentes:
|
| 158 |
+
- TrustNeuralAgent: gate σ(trust · τ_T)
|
| 159 |
+
- RewardModel: recompensa r
|
| 160 |
+
- Pesos cooperativos wᵢ ∝ 1/‖gᵢ‖² (Teorema 2)
|
| 161 |
+
- Momentum com decay 0.999
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
model: modelo base
|
| 165 |
+
vocab_size: tamanho do vocabulário
|
| 166 |
+
d_model: dimensão do modelo
|
| 167 |
+
eta_init: η inicial (learning rate RL) — default 0.01
|
| 168 |
+
lambda_rep_init: λ_rep inicial — default 0.1
|
| 169 |
+
momentum_decay: decay do momentum — default 0.999
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
def __init__(
|
| 173 |
+
self,
|
| 174 |
+
model: nn.Module,
|
| 175 |
+
vocab_size: int,
|
| 176 |
+
d_model: int,
|
| 177 |
+
eta_init: float = 0.01,
|
| 178 |
+
lambda_rep_init: float = 0.1,
|
| 179 |
+
momentum_decay: float = 0.999,
|
| 180 |
+
):
|
| 181 |
+
super().__init__()
|
| 182 |
+
self.model = model
|
| 183 |
+
self.vocab_size = vocab_size
|
| 184 |
+
self.d_model = d_model
|
| 185 |
+
self.momentum_decay = momentum_decay
|
| 186 |
+
|
| 187 |
+
# Componentes
|
| 188 |
+
self.trust_agent = TrustNeuralAgent(vocab_size, d_model)
|
| 189 |
+
self.reward_model = RewardModel(d_model)
|
| 190 |
+
|
| 191 |
+
# Hiperparâmetros (log-space)
|
| 192 |
+
self.log_eta = nn.Parameter(torch.tensor(math.log(eta_init)))
|
| 193 |
+
self.log_lambda_rep = nn.Parameter(torch.tensor(math.log(lambda_rep_init)))
|
| 194 |
+
|
| 195 |
+
# Momentum (um por parâmetro)
|
| 196 |
+
self.momentum: Dict[str, torch.Tensor] = {}
|
| 197 |
+
|
| 198 |
+
# Histórico de rewards (para normalização)
|
| 199 |
+
self.register_buffer("reward_history", torch.zeros(100))
|
| 200 |
+
self.register_buffer("reward_history_pos", torch.tensor(0, dtype=torch.long))
|
| 201 |
+
|
| 202 |
+
@property
|
| 203 |
+
def eta(self) -> torch.Tensor:
|
| 204 |
+
return torch.exp(self.log_eta)
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def lambda_rep(self) -> torch.Tensor:
|
| 208 |
+
return torch.exp(self.log_lambda_rep)
|
| 209 |
+
|
| 210 |
+
def compute_cooperative_weights(
|
| 211 |
+
self,
|
| 212 |
+
grads: List[torch.Tensor],
|
| 213 |
+
) -> List[torch.Tensor]:
|
| 214 |
+
"""Computa pesos cooperativos wᵢ ∝ 1/‖gᵢ‖² (Teorema 2).
|
| 215 |
+
|
| 216 |
+
Args:
|
| 217 |
+
grads: lista de gradientes (um por componente)
|
| 218 |
+
|
| 219 |
+
Returns:
|
| 220 |
+
lista de pesos normalizados (soma = 1)
|
| 221 |
+
"""
|
| 222 |
+
norms_sq = torch.stack(
|
| 223 |
+
[(g ** 2).sum() if g is not None else torch.tensor(0.0) for g in grads]
|
| 224 |
+
)
|
| 225 |
+
# wᵢ ∝ 1/‖gᵢ‖² — gradientes menores recebem mais peso
|
| 226 |
+
inv_norms = 1.0 / (norms_sq + 1e-8)
|
| 227 |
+
weights = inv_norms / inv_norms.sum()
|
| 228 |
+
return [float(w.item()) for w in weights]
|
| 229 |
+
|
| 230 |
+
def update_reward_history(self, reward: float) -> float:
|
| 231 |
+
"""Atualiza histórico e retorna reward normalizado."""
|
| 232 |
+
pos = int(self.reward_history_pos.item())
|
| 233 |
+
self.reward_history[pos] = reward
|
| 234 |
+
self.reward_history_pos = (self.reward_history_pos + 1) % self.reward_history.shape[0]
|
| 235 |
+
# Normalização: reward normalizado = (r - mean) / (std + eps)
|
| 236 |
+
mean = float(self.reward_history.mean().item())
|
| 237 |
+
std = float(self.reward_history.std().item())
|
| 238 |
+
return (reward - mean) / (std + 1e-8)
|
| 239 |
+
|
| 240 |
+
def compute_mapping(
|
| 241 |
+
self,
|
| 242 |
+
token_ids: torch.Tensor,
|
| 243 |
+
hidden: torch.Tensor,
|
| 244 |
+
task_loss: torch.Tensor,
|
| 245 |
+
rep_penalty: Optional[torch.Tensor] = None,
|
| 246 |
+
) -> dict:
|
| 247 |
+
"""Computa o mapeamento de possibilidades Φ.
|
| 248 |
+
|
| 249 |
+
Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
token_ids: (batch, T) — tokens
|
| 253 |
+
hidden: (batch, T, d_model) — hidden states
|
| 254 |
+
task_loss: escalar — loss da tarefa atual
|
| 255 |
+
rep_penalty: escalar opcional — penalidade de repetição
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
dict com:
|
| 259 |
+
- reward: recompensa r
|
| 260 |
+
- trust_gate: σ(trust · τ_T)
|
| 261 |
+
- cooperative_weights: wᵢ
|
| 262 |
+
- lambda_rep: λ_rep atual
|
| 263 |
+
- eta: η atual
|
| 264 |
+
- normalized_reward: r normalizado
|
| 265 |
+
"""
|
| 266 |
+
# 1. Reward
|
| 267 |
+
with torch.no_grad():
|
| 268 |
+
reward = self.reward_model(hidden).mean() # escalar
|
| 269 |
+
normalized_reward = self.update_reward_history(float(reward.item()))
|
| 270 |
+
|
| 271 |
+
# 2. Trust gate
|
| 272 |
+
trust_gate = self.trust_agent(token_ids, hidden) # (B, T)
|
| 273 |
+
trust_gate_mean = trust_gate.mean() # escalar
|
| 274 |
+
|
| 275 |
+
# 3. Gradientes das componentes (apenas task_loss tem grad aqui)
|
| 276 |
+
# Em V6 simplificado: só 1 componente (task_loss)
|
| 277 |
+
# Em versão completa: teríamos task_loss + auxiliary_losses
|
| 278 |
+
grads = [torch.autograd.grad(
|
| 279 |
+
task_loss, list(self.model.parameters()),
|
| 280 |
+
retain_graph=True, allow_unused=True
|
| 281 |
+
)]
|
| 282 |
+
# Pesos cooperativos
|
| 283 |
+
w = self.compute_cooperative_weights(grads[0])
|
| 284 |
+
|
| 285 |
+
# 4. Penalidade de repetição
|
| 286 |
+
if rep_penalty is not None:
|
| 287 |
+
rep_penalty_val = rep_penalty
|
| 288 |
+
else:
|
| 289 |
+
rep_penalty_val = torch.tensor(0.0, device=hidden.device)
|
| 290 |
+
|
| 291 |
+
# 5. Mapeamento Φ (não aplicado aqui — apenas log dos componentes)
|
| 292 |
+
# A aplicação real acontece no trainer via:
|
| 293 |
+
# θ ← θ - η · (r - baseline) · trust_gate · Σ wᵢ · gᵢ - λ_rep · ∇P_rep
|
| 294 |
+
# Aqui retornamos os componentes para inspeção/logging
|
| 295 |
+
return {
|
| 296 |
+
"reward": float(reward.item()),
|
| 297 |
+
"normalized_reward": float(normalized_reward),
|
| 298 |
+
"trust_gate_mean": float(trust_gate_mean.item()),
|
| 299 |
+
"trust_tau_T": float(self.trust_agent.tau_T.item()),
|
| 300 |
+
"cooperative_weights": w,
|
| 301 |
+
"lambda_rep": float(self.lambda_rep.item()),
|
| 302 |
+
"eta": float(self.eta.item()),
|
| 303 |
+
"rep_penalty": float(rep_penalty_val.item() if isinstance(rep_penalty_val, torch.Tensor) else rep_penalty_val),
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
@torch.no_grad()
|
| 307 |
+
def apply_mapping(
|
| 308 |
+
self,
|
| 309 |
+
token_ids: torch.Tensor,
|
| 310 |
+
hidden: torch.Tensor,
|
| 311 |
+
task_grads: Dict[str, torch.Tensor],
|
| 312 |
+
rep_penalty_grads: Optional[Dict[str, torch.Tensor]] = None,
|
| 313 |
+
reward: Optional[float] = None,
|
| 314 |
+
) -> Dict[str, float]:
|
| 315 |
+
"""Aplica o mapeamento Φ aos gradientes (in-place).
|
| 316 |
+
|
| 317 |
+
Φ = Θ + η · r · σ(trust · τ_T) · Σ_i wᵢ · gᵢ - λ_rep · P_rep · ∇P_rep
|
| 318 |
+
|
| 319 |
+
Args:
|
| 320 |
+
token_ids: (batch, T)
|
| 321 |
+
hidden: (batch, T, d_model)
|
| 322 |
+
task_grads: gradientes da task_loss
|
| 323 |
+
rep_penalty_grads: gradientes da penalidade de repetição (opcional)
|
| 324 |
+
reward: recompensa externa (opcional)
|
| 325 |
+
|
| 326 |
+
Returns:
|
| 327 |
+
dict com estatísticas da atualização
|
| 328 |
+
"""
|
| 329 |
+
# 1. Reward (computa se não fornecido)
|
| 330 |
+
if reward is None:
|
| 331 |
+
reward = float(self.reward_model(hidden).mean().item())
|
| 332 |
+
normalized_reward = self.update_reward_history(reward)
|
| 333 |
+
|
| 334 |
+
# 2. Trust gate (média)
|
| 335 |
+
trust_gate = self.trust_agent(token_ids, hidden)
|
| 336 |
+
trust_gate_mean = float(trust_gate.mean().item())
|
| 337 |
+
|
| 338 |
+
# 3. Pesos cooperativos (1 componente aqui)
|
| 339 |
+
task_norm_sq = sum(
|
| 340 |
+
(g ** 2).sum().item() for g in task_grads.values() if g is not None
|
| 341 |
+
)
|
| 342 |
+
w_task = 1.0 / (task_norm_sq + 1e-8)
|
| 343 |
+
|
| 344 |
+
# 4. Aplica Φ aos gradientes
|
| 345 |
+
eta = float(self.eta.item())
|
| 346 |
+
lam_rep = float(self.lambda_rep.item())
|
| 347 |
+
scaling = eta * normalized_reward * trust_gate_mean * w_task
|
| 348 |
+
|
| 349 |
+
for name, p in self.model.named_parameters():
|
| 350 |
+
if not p.requires_grad:
|
| 351 |
+
continue
|
| 352 |
+
g_task = task_grads.get(name)
|
| 353 |
+
if g_task is None:
|
| 354 |
+
continue
|
| 355 |
+
# Momentum
|
| 356 |
+
if name not in self.momentum:
|
| 357 |
+
self.momentum[name] = torch.zeros_like(p)
|
| 358 |
+
self.momentum[name] = (
|
| 359 |
+
self.momentum_decay * self.momentum[name]
|
| 360 |
+
+ (1.0 - self.momentum_decay) * g_task
|
| 361 |
+
)
|
| 362 |
+
# Atualização: Δθ = scaling · momentum - lam_rep · rep_penalty_grad
|
| 363 |
+
delta = scaling * self.momentum[name]
|
| 364 |
+
if rep_penalty_grads and name in rep_penalty_grads:
|
| 365 |
+
delta = delta - lam_rep * rep_penalty_grads[name]
|
| 366 |
+
p.data -= delta
|
| 367 |
+
|
| 368 |
+
# 5. Atualiza trust via TD
|
| 369 |
+
self.trust_agent.update_td(token_ids, torch.full(
|
| 370 |
+
(token_ids.shape[0],), reward, device=token_ids.device
|
| 371 |
+
))
|
| 372 |
+
|
| 373 |
+
return {
|
| 374 |
+
"reward": reward,
|
| 375 |
+
"normalized_reward": normalized_reward,
|
| 376 |
+
"trust_gate_mean": trust_gate_mean,
|
| 377 |
+
"scaling": scaling,
|
| 378 |
+
"eta": eta,
|
| 379 |
+
"lambda_rep": lam_rep,
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
__all__ = [
|
| 384 |
+
"RewardModel",
|
| 385 |
+
"TrustNeuralAgent",
|
| 386 |
+
"LearnablePossibilityMapping",
|
| 387 |
+
]
|
v5_upload_report.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "V5_upload",
|
| 3 |
+
"timestamp": 1786047298.2347026,
|
| 4 |
+
"repo_id": "PowerMachine/BiGRU_T_version",
|
| 5 |
+
"repo_type": "model",
|
| 6 |
+
"folder_path": "/home/z/my-project/BiGRU_T_version",
|
| 7 |
+
"allow_patterns": [
|
| 8 |
+
"*.py",
|
| 9 |
+
"*.json",
|
| 10 |
+
"*.md",
|
| 11 |
+
"*.txt",
|
| 12 |
+
"tokenizer.json"
|
| 13 |
+
],
|
| 14 |
+
"ignore_patterns": [
|
| 15 |
+
"__pycache__/*",
|
| 16 |
+
"*.pyc",
|
| 17 |
+
"*.pyo",
|
| 18 |
+
"_test_*",
|
| 19 |
+
"*.safetensors",
|
| 20 |
+
"*.bin",
|
| 21 |
+
"*.pt",
|
| 22 |
+
"*.pth",
|
| 23 |
+
".git",
|
| 24 |
+
".git/*",
|
| 25 |
+
"*/.git",
|
| 26 |
+
"**/.git/**",
|
| 27 |
+
".cache/huggingface",
|
| 28 |
+
".cache/huggingface/*",
|
| 29 |
+
"*/.cache/huggingface",
|
| 30 |
+
"**/.cache/huggingface/**"
|
| 31 |
+
],
|
| 32 |
+
"local_files_count": 70,
|
| 33 |
+
"local_files_total_bytes": 858363,
|
| 34 |
+
"remote_files_before": 70,
|
| 35 |
+
"remote_files_after": 73,
|
| 36 |
+
"new_files_count": 3,
|
| 37 |
+
"overwritten_count": 70,
|
| 38 |
+
"critical_files_all_present": true,
|
| 39 |
+
"critical_files_v5": [
|
| 40 |
+
"src/bigru_t/quantization/w8a8_smoothquant.py",
|
| 41 |
+
"src/bigru_t/quantization/w8a8_error_reduction.py",
|
| 42 |
+
"src/bigru_t/quantization/quantized_linear.py",
|
| 43 |
+
"src/bigru_t/quantization/__init__.py",
|
| 44 |
+
"src/bigru_t/model/unified_model.py",
|
| 45 |
+
"src/bigru_t/training/meta_configurator.py",
|
| 46 |
+
"src/bigru_t/model/u8cell_t.py",
|
| 47 |
+
"src/bigru_t/training/triplet_prototype_loss.py",
|
| 48 |
+
"v5_report.json"
|
| 49 |
+
],
|
| 50 |
+
"elapsed_s": 2.7080267730000003,
|
| 51 |
+
"commit_info": "https://huggingface.co/PowerMachine/BiGRU_T_version/commit/cbf308ebe33f4522ae238f15a5341205741808f6",
|
| 52 |
+
"commit_message": "V5: W8A8 aprimorado (α aprendível via STE) + bug fix apply_w8a8_to_model + 4 hiperparâmetros auto-ajustáveis (T, τ, λ_ent, init_gate) + OOM-Killer fixes + num_layers_hyp=8 fixo",
|
| 53 |
+
"env": {
|
| 54 |
+
"HF_HUB_ENABLE_HF_TRANSFER": "1",
|
| 55 |
+
"HF_HUB_ETAG_TIMEOUT": "60",
|
| 56 |
+
"OMP_NUM_THREADS": "2",
|
| 57 |
+
"TOKENIZERS_PARALLELISM": "true",
|
| 58 |
+
"PYTHONUNBUFFERED": "1"
|
| 59 |
+
}
|
| 60 |
+
}
|
v6_report.json
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "V6",
|
| 3 |
+
"timestamp": 1786048568.8618538,
|
| 4 |
+
"config": {
|
| 5 |
+
"n_multimodal": 10,
|
| 6 |
+
"n_text": 10,
|
| 7 |
+
"epochs": 2,
|
| 8 |
+
"batch_size": 4,
|
| 9 |
+
"max_seq_len": 16,
|
| 10 |
+
"num_layers_hyp": 8
|
| 11 |
+
},
|
| 12 |
+
"v6_improvements": {
|
| 13 |
+
"kohonen_4d_som": true,
|
| 14 |
+
"window_context_2m": true,
|
| 15 |
+
"mtp": true,
|
| 16 |
+
"mtp_adaptive_attention": true,
|
| 17 |
+
"dynamic_repetition_penalty": true,
|
| 18 |
+
"elastic_weight_consolidation": true,
|
| 19 |
+
"data_augmentation": true,
|
| 20 |
+
"rlhf_possibility_mapping": true,
|
| 21 |
+
"reasoning_thinking": true,
|
| 22 |
+
"tools": true,
|
| 23 |
+
"optimized_pipeline": true,
|
| 24 |
+
"langchain_compat": true
|
| 25 |
+
},
|
| 26 |
+
"xeon_env": {
|
| 27 |
+
"OMP_NUM_THREADS": "2",
|
| 28 |
+
"ONEDNN_MAX_CPU_ISA": "AMX_INT8",
|
| 29 |
+
"IPEX": false,
|
| 30 |
+
"IPEX_VERSION": null,
|
| 31 |
+
"torch_threads": 2
|
| 32 |
+
},
|
| 33 |
+
"result": {
|
| 34 |
+
"n_steps": 8,
|
| 35 |
+
"final_train_loss": 14.07144546508789,
|
| 36 |
+
"final_val_loss": 9.518492221832275,
|
| 37 |
+
"final_perplexity": 13609.076268750789,
|
| 38 |
+
"final_overfitting_gap": 4.527507543563843,
|
| 39 |
+
"final_eval_acc": 0.125,
|
| 40 |
+
"avg_tokens_per_sec": 29.457029226414775,
|
| 41 |
+
"avg_tflops": 0.001719579767621448,
|
| 42 |
+
"avg_time_per_step_ms": 2050.821927999948,
|
| 43 |
+
"avg_grad_norm": 0.9999999806095601,
|
| 44 |
+
"avg_cpu_pct": 127.68749999999999,
|
| 45 |
+
"final_rss_mb": 961.5625,
|
| 46 |
+
"max_rss_mb": 961.5625,
|
| 47 |
+
"rss_trend": "growing",
|
| 48 |
+
"n_alerts": 0,
|
| 49 |
+
"alerts": [],
|
| 50 |
+
"alert_types": [],
|
| 51 |
+
"mtp_alpha_mean": 0.0,
|
| 52 |
+
"som_quant_loss_final": 0.0,
|
| 53 |
+
"som_entropy_final": null,
|
| 54 |
+
"som_sigma_final": 0.0,
|
| 55 |
+
"ewc_lambda_final": 10.0,
|
| 56 |
+
"rlhf_reward_final": 0.0,
|
| 57 |
+
"reasoning_steps_final": null,
|
| 58 |
+
"elapsed_s": 29.635286407999956,
|
| 59 |
+
"n_params": 9729312,
|
| 60 |
+
"hyp_T_params": 1795914,
|
| 61 |
+
"num_layers_hyp": 8,
|
| 62 |
+
"meta_history": [
|
| 63 |
+
{
|
| 64 |
+
"T": 1.0100501775741577,
|
| 65 |
+
"tau": 1.0100501775741577,
|
| 66 |
+
"lambda_ent": 0.009999999776482582,
|
| 67 |
+
"init_gate": 0.04461870715022087,
|
| 68 |
+
"meta_loss": 17.63351058959961,
|
| 69 |
+
"sharpness": 74.92311096191406
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"T": 1.020202398300171,
|
| 73 |
+
"tau": 1.0202057361602783,
|
| 74 |
+
"lambda_ent": 0.009900504723191261,
|
| 75 |
+
"init_gate": 0.026760049164295197,
|
| 76 |
+
"meta_loss": 17.854074478149414,
|
| 77 |
+
"sharpness": 102.09951782226562
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"n_w8a8_layers": 81,
|
| 81 |
+
"final_learnable_params": {
|
| 82 |
+
"T": 1.020202398300171,
|
| 83 |
+
"tau": 1.0202057361602783,
|
| 84 |
+
"lambda_ent": 0.009802023880183697,
|
| 85 |
+
"init_gate": 0.014342580921947956,
|
| 86 |
+
"residual_gate": [
|
| 87 |
+
0.014335463754832745,
|
| 88 |
+
0.014350333251059055,
|
| 89 |
+
0.014341948553919792
|
| 90 |
+
],
|
| 91 |
+
"module_logits": null,
|
| 92 |
+
"mtp_alphas": [
|
| 93 |
+
0.3333333432674408,
|
| 94 |
+
0.3333333432674408,
|
| 95 |
+
0.3333333432674408
|
| 96 |
+
],
|
| 97 |
+
"rep_penalty_lambda": 1.0,
|
| 98 |
+
"ewc_lambda": 10.0,
|
| 99 |
+
"rlhf_eta": 0.009999999776482582,
|
| 100 |
+
"rlhf_lambda_rep": 0.09999999403953552,
|
| 101 |
+
"rlhf_trust_tau_T": 1.0
|
| 102 |
+
},
|
| 103 |
+
"som_stats": {
|
| 104 |
+
"n_prototypes": 512,
|
| 105 |
+
"arrangement": [
|
| 106 |
+
8,
|
| 107 |
+
4,
|
| 108 |
+
4,
|
| 109 |
+
4
|
| 110 |
+
],
|
| 111 |
+
"sigma_final": 1.8572561740875244,
|
| 112 |
+
"alpha_final": 0.09512294828891754,
|
| 113 |
+
"step_count": 10,
|
| 114 |
+
"tau_sigma": 100.0
|
| 115 |
+
},
|
| 116 |
+
"tool_stats": {
|
| 117 |
+
"n_tools_registered": 2,
|
| 118 |
+
"tool_history": [
|
| 119 |
+
{
|
| 120 |
+
"type": "compose",
|
| 121 |
+
"new_name": "double_echo",
|
| 122 |
+
"parents": [
|
| 123 |
+
"echo",
|
| 124 |
+
"echo"
|
| 125 |
+
],
|
| 126 |
+
"timestamp": 1786048537.363995
|
| 127 |
+
}
|
| 128 |
+
],
|
| 129 |
+
"agent_stats": {
|
| 130 |
+
"calls": 0,
|
| 131 |
+
"successes": 0,
|
| 132 |
+
"failures": 0,
|
| 133 |
+
"circuit_opens": 0,
|
| 134 |
+
"fallbacks": 0,
|
| 135 |
+
"parallel_calls": 0,
|
| 136 |
+
"success_rate": 0.0,
|
| 137 |
+
"circuits": {}
|
| 138 |
+
},
|
| 139 |
+
"cache_stats": {
|
| 140 |
+
"hits": 0,
|
| 141 |
+
"misses": 0,
|
| 142 |
+
"evictions": 0,
|
| 143 |
+
"disk_writes": 0,
|
| 144 |
+
"disk_reads": 0,
|
| 145 |
+
"hit_rate": 0.0,
|
| 146 |
+
"size": 0,
|
| 147 |
+
"max_size": 64
|
| 148 |
+
}
|
| 149 |
+
},
|
| 150 |
+
"pipeline_stats": {
|
| 151 |
+
"n_calls": 12,
|
| 152 |
+
"n_tokens_processed": 640,
|
| 153 |
+
"n_mtp_predictions": 0,
|
| 154 |
+
"n_reasoning_calls": 0,
|
| 155 |
+
"n_tool_calls": 0,
|
| 156 |
+
"n_rep_penalty_applied": 8
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|