PowerMachine commited on
Commit
e383cb5
·
verified ·
1 Parent(s): cbf308e

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 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: orquestrador com cache aprendível + cross-attention.
2
-
3
- Reaproveita a filosofia do orquestrador cooperativo do v13.9.2, mas:
4
- - Cache aprendível (L, d_cache) substitui o "estado de trust" do v13.9.2
5
- - Cross-attention (cache = queries, módulos = keys/values) substitui o
6
- gating por Confidence Gate
7
- - Média sobre as L posições do cache como saída agregada
8
-
9
- O cache é um "roteador aprendível" que decide quais módulos ativos devem
10
- contribuir para a representação final. Diferente do TrustNetwork do v13.9.2
11
- (TD-learning com softmax norm), o OrqCell usa attention pura — a seleção
12
- final dos módulos ativos é feita pelo Lema 1 no UnifiedModel.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 cache aprendível e cross-attention.
23
 
24
  Args:
25
- cache_len: nº de posições no cache aprendível (L)
26
- d_cache: dimensão do cache
 
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, consistente com TransformerUnit)
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(self, H: torch.Tensor) -> torch.Tensor:
71
- """H: (batch, K, d_model_in) → o: (batch, d_cache)"""
 
 
 
 
 
 
 
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
- # Expande cache para o batch
78
- cache = self.cache.unsqueeze(0).expand(batch_size, -1, -1) # (batch, L, d_cache)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
 
80
  # Cross-attention: queries = cache, keys/values = H_proj
81
- # Pre-norm
82
  normed_cache = self.norm1(cache)
83
- normed_H = self.norm1(H_proj) # compartilha norm1 (mesma dimensão)
84
- attn_out, _ = self.cross_attn(normed_cache, normed_H, normed_H, need_weights=False)
 
 
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
- return o
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }