PowerMachine commited on
Commit
28430a8
·
verified ·
1 Parent(s): c5dc6bb

v0.8.0: análise/prova/teste das 4 propostas Qwen3.8 (r-33) — Newton analítico no verificador (convexidade em gamma=1/T), árvore pós-ordem iterativa bit-a-bit com detecção de ciclos, Cayley exato no FNH (4,1× mais rápido), MoE vetorizada com gradiente RESTAURADO (bug v080-1) — 85/85 gates, 99/99 cargo

Browse files
Cargo.lock CHANGED
@@ -734,7 +734,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
734
 
735
  [[package]]
736
  name = "khtst-rust"
737
- version = "0.7.0"
738
  dependencies = [
739
  "anyhow",
740
  "built",
 
734
 
735
  [[package]]
736
  name = "khtst-rust"
737
+ version = "0.8.0"
738
  dependencies = [
739
  "anyhow",
740
  "built",
Cargo.toml CHANGED
@@ -1,8 +1,8 @@
1
  [package]
2
  name = "khtst-rust"
3
- version = "0.7.0"
4
  edition = "2021"
5
- description = "KHTST_RUST — NCM com verificador calibrado, treino conjunto Pensamento↔backbone, MoEs redirecionáveis com atenção própria e autoajuste por telemetria"
6
  license = "Apache-2.0"
7
 
8
  [lib]
 
1
  [package]
2
  name = "khtst-rust"
3
+ version = "0.8.0"
4
  edition = "2021"
5
+ description = "KHTST_RUST — provas e testes das propostas Qwen3.8: Newton analítico (verificador), árvore pós-ordem iterativa, Cayley exato (FNH), MoE vetorizada com gradiente restaurado"
6
  license = "Apache-2.0"
7
 
8
  [lib]
dados/bpe.json ADDED
The diff for this file is too large to render. See raw diff
 
docs/memorias/r-33-analise-provas-qwen-v080.md ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # r-33 — Análise, Provas e Testes das Propostas do Agente Qwen3.8 (v0.8.0)
2
+
3
+ **Escopo**: o usuário pediu *analisar, provar e testar* as 4 propostas de código
4
+ geradas pelo agente Qwen3.8 (verificador.rs, arvore_pesos.rs, fnh.rs,
5
+ moe_redirecionavel.rs), mantendo o princípio da casa: **nenhuma funcionalidade
6
+ SOTA eficaz é removida**; toda mudança com prova matemática, lógica e de lógica
7
+ computacional; provas em markdown; medições honestas.
8
+
9
+ **Metodologia**: (i) verificação matemática simbólica de cada proposta antes de
10
+ qualquer código; (ii) confronto com oráculos independentes (diferenças finitas,
11
+ Gauss–Jordan em f64, SVD/NumPy, implementação de referência v0.6.0/v0.7.0);
12
+ (iii) implementação apenas do que sobrevive a (i)+(ii), como **adição** aos
13
+ métodos existentes (que permanecem como referência/rede de segurança);
14
+ (iv) testes de regressão + determinismo 3×; (v) medição de custo.
15
+
16
+ **Veredito sumário**:
17
+
18
+ | # | Proposta | Veredito | Correções necessárias | Ganho medido |
19
+ |---|----------|----------|----------------------|--------------|
20
+ | 1 | Newton-Raphson na calibração de temperatura | **IDEIA VÁLIDA, CÓDIGO ERRADO** (2 erros matemáticos) | sinal de dL/dT invertido; 1ª diferença usada como 2ª derivada | 14 vs ~44 avaliações; 1,5× tempo |
21
+ | 2 | Agregação fundo-topo iterativa (pós-ordem) | **VÁLIDA com correções de robustez** | sem guarda contra duplicatas/ciclos; sem verificação de alcançabilidade | bit-a-bit igual, sem clones, pilha-safe em 100k nós |
22
+ | 3 | Rotação via Transformada de Cayley | **IDEIA VÁLIDA, CÓDIGO INCOMPILÁVEL/IMPRóprio** | `Tensor::inv()` não existe em candle 0.11; convenção de sinal equivale a `expm(−A)` | 4,1× mais rápido que polar, δ_ort ≤ 8,9e-7 |
23
+ | 4 | Roteamento/perda auxiliar "na GPU" em ops de tensor | **IDEIA VÁLIDA, ESBOÇO INCOMPLETO** (`unimplemented!`) | implementação real revelou o **bug v080-1**: a `fwd` v0.6.0 seccionava o grafo de autograd | gradiente restaurado; MoE −88,7% vs pequena −23,5% |
24
+
25
+ Além das 4 propostas, o processo expôs **2 bugs pré-existentes** (v080-2 no
26
+ `debug_assert` do quantizador; leitura confundida da ablação JEPA v0.6.0) e
27
+ **3 erros meus próprios durante a implementação** (§6 — catalogados com
28
+ causa-raiz, como manda a tradição r-07/r-27).
29
+
30
+ ---
31
+
32
+ ## §2 — Proposta 1: Newton-Raphson na calibração de temperatura (verificador.rs)
33
+
34
+ ### 2.1 Errata da proposta original
35
+
36
+ A proposta calculava:
37
+
38
+ ```text
39
+ dl_dt = (ez − z_target) / (t·t) // (A)
40
+ d2l_dt2 = (l_pert − l) / eps // (B)
41
+ ```
42
+
43
+ **(A) tem o sinal INVERTIDO.** Derivação direta (Teo N-1 abaixo):
44
+ `L'(T) = (z_y − E_p[z])/T²`, e não `(E_p[z] − z_y)/T²`.
45
+
46
+ **(B) não é a segunda derivada.** `[L(T+ε) − L(T)]/ε ≈ L'(T)` é a **primeira**
47
+ diferença (a segunda diferença correta seria `[L(T+ε) − 2L(T) + L(T−ε)]/ε²`).
48
+
49
+ **Consequência combinada**: com o sinal errado em (A) e (B) ≈ L'(T), o passo
50
+ de Newton computado é `Δ = dl_dt/d2l_dt2 ≈ (−L')/(L') = −1`, isto é, **T ← T+1
51
+ a cada iteração** até esbarrar no clamp 10 — a iteração "converge" para o teto
52
+ do colchão em vez de para o mínimo. Um teste de direção (superconfiante ⇒
53
+ L'(1) < 0) captura o erro imediatamente; a proposta não incluía tal teste.
54
+
55
+ ### 2.2 Teo N-1 (derivadas analíticas) ∎
56
+
57
+ Seja `L(T) = (1/n)·Σᵢ [−z_{i,yᵢ}/T + log Σⱼ exp(z_{i,j}/T)]` e
58
+ `pᵢ = softmax(zᵢ/T)`. Então:
59
+
60
+ 1. `L'(T) = médiaᵢ (z_{i,yᵢ} − E_{pᵢ}[zᵢ]) / T²`
61
+ 2. `L''(T) = médiaᵢ [Var_{pᵢ}(zᵢ) − 2T·(z_{i,yᵢ} − E_{pᵢ}[zᵢ])] / T⁴`
62
+
63
+ **Prova.** Por amostra: `d/dT[−z_y/T] = z_y/T²` e
64
+ `d/dT[log Σⱼ e^{z_j/T}] = −E_p[z]/T²` (derivada do log-partition com pesos
65
+ `dp_j/dT = p_j(E_p[z] − z_j)/T²`); somando, (1). Para (2), deriva-se
66
+ `d/dT E_p[z] = (E_p[z]² − E_p[z²])/T² = −Var_p[z]/T²` e aplica a regra do
67
+ produto em `(z_y − E_p[z])·T⁻²`:
68
+ `L''(T) = Var_p[z]/T⁴ − 2(z_y−E_p[z])/T³`, que é (2) reunido sobre T⁴. No
69
+ ótimo `L'(T*)=0 ⇒ z_y = E_p[z]` nas amostras (em média), logo
70
+ `L''(T*) = média Var_{p*}[z]/T*⁴ > 0` (estritamente, salvo p degenerada) —
71
+ mínimo local estrito. ∎
72
+
73
+ **Validação numérica**: teste `n1` — L' e L'' analíticos vs diferenças finitas
74
+ centrais (h = 10⁻², escolhido no equilíbrio ruído-f32/truncamento): acordam em
75
+ < 5·10⁻³ e < 5·10⁻² respectivamente; e a REGRESSÃO DA ERRATA: logits
76
+ superconfiantes ⇒ `L'(1) < 0` (a proposta original daria > 0).
77
+
78
+ ### 2.3 Teo N-2 (convexidade em γ = 1/T — fortalece a V-1) ∎
79
+
80
+ Seja `L̃(γ) = (1/n)·Σᵢ [−γ·z_{i,yᵢ} + log Σⱼ exp(γ·z_{i,j})]`. Então:
81
+
82
+ 1. `L̃''(γ) = médiaᵢ Var_{pᵢ(γ)}(zᵢ) ≥ 0` para **todo** γ > 0 — L̃ é convexa;
83
+ 2. `L(T) = L̃(1/T)` e T ↦ 1/T é bijeção decrescente de (0,∞) ⇒ **L é unimodal
84
+ em T** com mínimo único em `T* = 1/γ*`;
85
+ 3. `L̃'(γ) = médiaᵢ (E_{pᵢ}[zᵢ] − z_{i,yᵢ})` troca o sinal exatamente uma vez.
86
+
87
+ **Prova.** (1): `dp_j/dγ = p_j(z_j − E_p[z])` (derivada do softmax na escala
88
+ γ), logo `d/dγ E_p[z] = E_p[z²] − E_p[z]² = Var_p[z] ≥ 0`; a média preserva a
89
+ desigualdade. (2): composição de convexa com afim-decrescente em T dá
90
+ decrescente- depois crescente- em T (para T < T* γ > γ* e L̃ crescente à
91
+ direita de γ*, etc.). (3): segue de (1)+(2). ∎
92
+
93
+ **Consequências**: (a) a "unimodalidade empírica" que a v0.6.0 verificava por
94
+ teste (v3) agora é **teorema** — o golden-section da V-1 está justificado
95
+ retrospectivamente; (b) a bisseção sobre o sinal de L̃' é válida; (c) o teste
96
+ `n3` (varredura de 64 pontos em T log-espaçados: exatamente 1 troca de sinal)
97
+ valida (3) empiricamente.
98
+
99
+ *Nota estatística (n4b)*: a contribuição de cada amostra para `L̃'(γ)` é
100
+ `Σ_{j≠y} p_j(z_j − z_y)` — ≤ 0 para acertos (z_y = argmax) e ≫ 0 para **erros
101
+ confiantes**. É por isso que a NLL "escuta" os erros: superconfiança genuína
102
+ (exige T* > 1) só ocorre com erros confiantes — o padrão de overfitting. O
103
+ teste n4b reconstrói os dois regimes sinteticamente (95% acertos com margem
104
+ 5,4 + 5% erros com margem 8,4 ⇒ T* > 1; margem 3 com ruído ≈ 0 ⇒ T* = 1/8).
105
+
106
+ ### 2.4 Teo N-3 (convergência do Newton com salvaguarda) ∎
107
+
108
+ O `calibrar_temperatura_newton` itera em **γ** (não em T): a cada passo avalia
109
+ `(L, L̃', L̃''=Var)` em γ; mantém o colchão `[a,b] ⊆ [1/8, 8]` com o invariante
110
+ `L̃'(a) < 0 < L̃'(b)`; o passo de Newton `γ ← γ − L̃'/L̃''` é **aceito apenas
111
+ se cai estritamente dentro de (a,b)** — caso contrário, bisseção geométrica
112
+ `γ ← √(ab)` (o análogo log-T da V-1). Então:
113
+
114
+ 1. **Global**: o colchão contém γ* em todo momento (invariante + N-2.3); cada
115
+ bisseção divide `ln(b/a)` por 2 ⇒ ≤ ⌈log₂(ln 64/ε)⌉ ≈ 35 passos no pior
116
+ caso; termina em |L̃'| < 10⁻¹⁰ ou orçamento.
117
+ 2. **Local (quadrática)**: perto de γ*, `L̃''(γ*) = Var* > 0` e L̃ ∈ C² ⇒ o
118
+ erro satisfaz `|e_{k+1}| ≤ (M/2m)|e_k|²` com `M = sup|L̃'''|`, `m = inf L̃''`
119
+ (Newton–Kantorovich; Ortega & Rheinboldt, §10.2).
120
+ 3. **Em γ nunca há curvatura negativa** (L̃'' = Var ≥ 0 sempre — N-2.1), ao
121
+ contrário de em T, onde L'' pode ser negativa longe do ótimo — por isso a
122
+ iteração foi formulada em γ.
123
+
124
+ ∎
125
+
126
+ ### 2.5 Medição (demo_qwen_v080, n=1024, K=32, superconfiante)
127
+
128
+ | Método | avaliações | tempo | T* | NLL(T*) |
129
+ |--------|-----------|-------|-----|---------|
130
+ | golden-section (V-1, mantido) | ~44 (64 máx) | 8,30 ms | 0,34412 | 0,69928968 |
131
+ | **Newton γ (N-4, novo)** | **14** | 5,47 ms | 0,34408 | 0,69928992 |
132
+
133
+ T* concorda em 3,7·10⁻⁵; NLL difere em 2,4·10⁻⁷ (nível do ε da f32 — ambos
134
+ dentro do ruído do mínimo); **3,1× menos avaliações, 1,5× menos tempo**
135
+ (cada avaliação Newton custa ~2 NLLs pois computa CE + momentos, mas o total
136
+ vence). O `VerificadorNcm::ajustar` agora usa Newton com o golden-section
137
+ como **rede de segurança explícita** (nunca removido): se o Newton falhasse
138
+ (NLL pior que a bruta + 10⁻⁶ — nunca observado), recai na V-1.
139
+
140
+ ---
141
+
142
+ ## §3 — Proposta 2: iteração segura em árvores (arvore_pesos.rs)
143
+
144
+ ### 3.1 Análise da proposta
145
+
146
+ A proposta (pós-ordem com pilha explícita, flags `visitados`, processamento em
147
+ duas fases) está **essencialmente correta** para árvores estritas — é o
148
+ algoritmo clássico de travessia pós-ordem iterativa. Três lacunas:
149
+
150
+ 1. **Filhos duplicados no mesmo pai escapam** da checagem por estado: quando o
151
+ pai lista o filho X duas vezes, o 2º push ocorre ANTES da expansão do 1º
152
+ (a marcação por estado só é verificada na expansão) ⇒ X entra DUAS vezes na
153
+ pós-ordem ⇒ soma dupla silenciosa. **Correção adotada**: flag `empilhado`
154
+ marcado **no momento do push** — re-empilhamento é recusado com erro
155
+ explícito (a recursiva da v0.6.0 somaria duas vezes sem sinalizar).
156
+ 2. **Sem detecção de ciclos**: um ciclo A↔B termina (não laça infinito), mas
157
+ produz ordem inválida e agregado sem sentido. Com a correção (1), a
158
+ re-entrada de ciclo também é recusada no push; adicionalmente o
159
+ `invariantes_ok` ganhou **(iv) simetria pai↔filho** e **(v) alcançabilidade
160
+ BFS da raiz** (Teo T-A9 abaixo) — que capturam ciclos que as checagens
161
+ (i)–(iii) da v0.6.0 deixavam passar (ex.: 2-ciclo desconectado x↔y).
162
+ 3. O comentário "processamento na ordem inversa" estava errado — a pós-ordem
163
+ JÁ é folhas-primeiro; a iteração é direta.
164
+
165
+ ### 3.2 Teo T-A7 (equivalência bit-a-bit com a recursiva) ∎
166
+
167
+ **Lema (ordem)**: a pós-ordem produzida pela pilha com empilhamento reverso
168
+ dos filhos coincide com a ordem de retorno da recursão `agregar_rec`.
169
+ *Prova por indução na altura h*: h = 0 (folha): empilha, reencontra com
170
+ estado 1, desempilha — única na saída; a recursão retorna imediatamente. Passo:
171
+ para um nó com filhos f₁…f_m (empilhados em ordem reversa, processados na
172
+ ordem original), os blocos de pós-ordem das subárvores completam na ordem
173
+ f₁, …, f_m seguidos do próprio nó — exatamente a ordem de retorno da recursão.
174
+ ∎
175
+
176
+ **Teorema**: com a mesma ordem de visitação e a mesma ordem de soma
177
+ (`total = próprio + Σ_f v_f` na ordem f₁…f_m), a sequência de operações de
178
+ ponto flutuante é idêntica ⇒ **resultados bit-a-bit idênticos** à v0.6.0
179
+ (IEEE-754: mesma sequência de ops ⇒ mesmo resultado). Teste `ta7`: 8 árvores
180
+ aleatórias (ramificação E profundidade), totais e vetores `credito` comparados
181
+ por `to_bits()` — iguais em todas. ∎
182
+
183
+ ### 3.3 Teo T-A8 (custo e robustez) ∎
184
+
185
+ Seja V = nós, E = arestas (|E| = V−1 em árvore). A iterativa: tempo O(V+E),
186
+ espaço adicional O(V) (pilha + pós-ordem + 2 flags), **zero alocações por
187
+ nó**. A recursiva v0.6.0: O(V+E) tempo, mas **1 clone de `Vec` por nó**
188
+ (Θ(Σ|filhos|) alocações) e O(profundidade) quadros de pilha — na thread de
189
+ teste (2 MiB) uma cadeia degenerada de ~10⁴–10⁵ nós excede a pilha. A iterativa
190
+ processa **100.000 nós em cadeia** (teste ta7b) sem estourar; a soma global e
191
+ o crédito por enlace estão corretos (T-A2 preservado). Medição (20.012 nós,
192
+ thread principal): 3,63 ms vs 4,06 ms — ~11% mais rápida no caso cadeia (o
193
+ ganho estrutural é a robustez e a eliminação de clones; em árvores rasas os
194
+ tempos são equivalentes). ∎
195
+
196
+ ### 3.4 Teo T-A9 (as checagens (iv)+(v) caracterizam árvore) ∎
197
+
198
+ Com (i) todo não-raiz tem pai listado e a raiz não tem; (iv) todo filho
199
+ listado por u tem pai = u; (v) todo nó é alcançável da raiz por BFS seguindo
200
+ `filhos`: o grafo direcionado é uma **árvore enraizada**. *Prova*: (i)+(iv) ⇒
201
+ a relação pai→filho é consistente e cada nó tem ≤ 1 pai (se u lista f, o pai de
202
+ f é u por (iv); se u' ≠ u também listasse f, o pai de f seria u' ≠ u —
203
+ contradição com "≤ 1"). Se houvesse ciclo, os nós do ciclo teriam pais no
204
+ ciclo; um nó do ciclo alcançável da raiz receberia uma aresta externa do
205
+ caminho da raiz ALÉM da aresta do ciclo — 2 pais, contradição; ciclo
206
+ desconectado viola (v). Logo acíclico + conectado + raiz única = árvore. ∎
207
+ Teste `ta8`: duplicata ⇒ NaN (recusa); assimetria ⇒ erro (iv); 2-ciclo
208
+ desconectado ⇒ erro (v).
209
+
210
+ ### 3.5 Adições conexas
211
+
212
+ `anexar_filho(pai, nome, n_params)` — API pública para anexar subárvores com
213
+ aresta dupla mantendo o índice de adjacência e a partição (1 entrada de
214
+ `credito`/`regulacao` **por enlace** — 2 por aresta; ver errata minha no §6);
215
+ `regular_topo_fundo` também iterativa (pré-ordem, mesma semântica λ^k do PAI —
216
+ teste ta7c); `agregar_fundo_topo_rec` mantida como oráculo de referência.
217
+
218
+ ---
219
+
220
+ ## §4 — Proposta 3: exatidão algébrica em rotações — Cayley (fnh.rs)
221
+
222
+ ### 4.1 Errata da proposta original
223
+
224
+ 1. **`den.inv()` não existe em candle 0.11** (verificado no fonte do crate) —
225
+ a proposta assumia uma API inexistente; o próprio comentário admitia
226
+ "Assumindo implementação de inversão ou solve". Além disso, uma inversão
227
+ por LU/pivoteamento **não é expressável em ops de tensor fixas** e romperia
228
+ o autograd.
229
+ 2. **Convenção de sinal**: `(I − A/2)(I + A/2)⁻¹ = expm(−A + O(A³))` — a
230
+ identidade escalar `(1−t)/(1+t) = e^{−2·artanh(t)}` eleva-se a matrizes
231
+ comutantes (funções de A). A proposta, portanto, constrói a rotação
232
+ **contrária** à série de Taylor do gru-ring (`expm(+A)`). Para o FNH isso é
233
+ indiferente (a família {Cayley(A) : A skew} é a mesma de θ ↦ −θ), mas a
234
+ paridade semântica com o port exige `(I + A/2)(I − A/2)⁻¹`.
235
+ **Verificação independente**: oráculo NumPy (SVD para o fator polar do
236
+ truncado; `expm` por série longa) — com θ ∈ [0,05, 0,32], n = 8:
237
+ `‖polar(trunc) − expm(A)‖ = 0,047` mas `‖(I−A/2)(I+A/2)⁻¹ − expm(A)‖ = 2,51`
238
+ (≈ 2‖A‖ — espelho); com a convenção corrigida a distância cai para o
239
+ esperado O(‖A‖³/12).
240
+
241
+ ### 4.2 Teo F-9 (Cayley ∈ SO(n), invertibilidade e condicionamento) ∎
242
+
243
+ Com A skew-simétrica n×n e B = A/2:
244
+
245
+ 1. **(i) I−B é invertível**: se `(I−B)v = 0` então `v = Bv` e
246
+ `vᵀv = vᵀBv = 0` (vᵀBv = −vᵀBᵀv = −vᵀBv ⇒ = 0 para B skew) ⇒ v = 0. ∎
247
+ 2. **(ii) RᵀR = I** para `R = (I+B)(I−B)⁻¹`: `Rᵀ = (I−B)⁻ᵀ(I+B)ᵀ =
248
+ (I+B)⁻¹(I−B)` (transposta de inversa; (I±B)ᵀ = I∓B). Como I+B e I−B são
249
+ polinômios em B, **comutam**: `(I+B)(I−B) = I−B² = (I−B)(I+B)`. Logo
250
+ `RᵀR = (I+B)⁻¹(I−B)(I+B)(I−B)⁻¹ = I`. ∎
251
+ 3. **(iii) det R = +1**: `det(I−B) = det((I+B)ᵀ) = det(I+B)` ⇒ razão 1. ∎
252
+ 4. **(iv) condicionamento**: eig(B) = ±iλ_k/2 ⇒ eig(I−B) = 1∓iλ_k/2,
253
+ |eig| = √(1+λ_k²/4) ≥ 1; I−B é normal (B skew) ⇒ valores singulares =
254
+ |eig| ⇒ `κ(I−B) = √(1+‖A‖₂²/4) ≤ √(1+clamp²·n(n−1)/16)` — bem
255
+ condicionado **para todo θ no clamp**, sem pivoteamento. ∎
256
+ 5. **Bijução**: Cayley mapea skew(n) sobre {R ∈ SO(n) : −1 ∉ eig(R)} com
257
+ inversa `A = 2(I−R)(I+R)⁻¹`; rotações com ângulo exatamente π são
258
+ inatingíveis — conjunto de medida zero, irrelevante ao treino por
259
+ gradiente (limitação honesta herdada da proposta).
260
+
261
+ ### 4.3 Teo F-10 (inversão por Newton, diferenciável e com convergência provada) ∎
262
+
263
+ A inversa de M = I−B é computada pela iteração `X ← X(2I − MX)` com
264
+ `X₀ = αMᵀ`, `α = 1/(1+‖B‖²_F)`:
265
+
266
+ 1. O erro `E_k = I − MX_k` satisfaz `E_{k+1} = E_k²` (verificação direta:
267
+ `I − MX(2I−MX) = (I−MX)²`) ⇒ convergência **quadrática espectral** quando
268
+ ‖E₀‖₂ < 1.
269
+ 2. `MMᵀ = (I−B)(I+B) = I−B²`; B² é simétrica negativa-semidefinida
270
+ (B² = −BᵀB), eig(B²) ∈ [−‖B‖₂², 0]; então
271
+ `E₀ = I − α(I−B²) = (1−α)I + αB²` tem autovalores em
272
+ `[1−α(1+‖B‖₂²), 1−α] ⊆ [0, ‖B‖²_F/(1+‖B‖²_F)] ⊂ [0,1)` (usa
273
+ ‖B‖₂² ≤ ‖B‖²_F) ⇒ ‖E₀‖₂ < 1 **sempre** — a iteração nunca diverge. ∎
274
+ 3. Custo: 2 matmuls por iteração + teste de resíduo (saída antecipada
275
+ ‖I−MX‖∞ < 10⁻⁷). No pior caso do clamp (todos θ = 0,5, n = 16:
276
+ ‖B‖²_F = 15, ρ = 15/16): ρ^{2^k} < 10⁻⁷ ⇒ k = 8 — teto 12 com margem.
277
+ 4. **Nascimento neutro**: θ = 0 ⇒ B = 0 ⇒ α = 1 EXATO ⇒ X₀ = Mᵀ = I e o
278
+ resíduo |1−α| = 0 encerra na 1ª iteração ⇒ R = I **bit-a-bit com o grafo
279
+ conectado a θ** (gradiente flui no nascimento — teste f9; o caminho polar
280
+ precisa de atalho especial para o mesmo efeito).
281
+ 5. **Diferenciabilidade**: todas as ops (affine, matmul, sub) são rastreadas
282
+ pelo autograd do candle — o gradiente flui através da inversa (não há
283
+ detach). Teste f10: pior caso do clamp confere com Gauss–Jordan f64
284
+ (máx |Δ| < 10⁻³) e δ_ort ≤ 10⁻⁶.
285
+
286
+ ### 4.4 Teo F-11 (blocos 2×2 em fórmula fechada) ∎
287
+
288
+ Para rotação restrita a planos canônicos (bloco-diagonal com blocos
289
+ `S_k = [[0,−s_k],[s_k,0]]`, s_k = θ_k/2), a inversão de `I−S_k` é analítica:
290
+ `(I+S_k)(I−S_k)⁻¹ = [[1−s², −2s],[2s, 1−s²]]/(1+s²)` (produto direto das
291
+ matrizes 2×2; ortogonal por F-9.ii; ângulo α_k com tan(α_k/2) = s_k —
292
+ fórmula do semi-ângulo). A montagem usa 1 matmul com matriz de seleção
293
+ constante (2m, n²) — **sem inversão, sem iteração**, O(n²) entradas
294
+ escritas, zero EXATO fora dos blocos (0·x em f32), nascimento neutro bit-a-bit
295
+ e grafo vivo (teste f11). Família RESTRITA (n/2 graus de liberdade, sem
296
+ mistura entre planos) — indicada quando n é grande e o custo O(n³) das
297
+ rotações completas domina. ∎
298
+
299
+ ### 4.5 Teo F-12 (paridade polar ↔ Cayley) ∎
300
+
301
+ `polar(I + A + A²/2 + A³/6) = expm(A + O(‖A‖⁵))` (o erro de truncamento
302
+ O(A⁴) é **simétrico** — A⁴ é simétrico para A skew — e o fator polar é
303
+ insensível a 1ª ordem à parte simétrica), enquanto
304
+ `Cayley(A) = expm(2·artanh(A/2)) = expm(A − A³/12 + O(A⁵))`. Logo
305
+ `‖R_polar − R_cayley‖_F = O(‖A‖³/12)`. Teste f12 (θ ∈ [0,02, 0,155], n = 8):
306
+ distância 10⁻³–10⁻², razão desvio/distância-à-identidade < 0,25. Ambos os
307
+ modos coexistem (`ModoRotacao::Polar` padrão — compatibilidade v0.7.0 bit-a-bit;
308
+ `Cayley` opt-in). ∎
309
+
310
+ ### 4.6 Medição (n = 16, θ espalhado no clamp, média de 25 rotações)
311
+
312
+ | Modo | tempo/rotação | δ_ort |
313
+ |------|--------------|-------|
314
+ | polar (40 Newton–Schulz + série) | 1,89 ms | 3,0·10⁻⁷ |
315
+ | **Cayley (Newton-inverso adaptativo)** | **0,46 ms** | 8,9·10⁻⁷ |
316
+
317
+ **4,1× mais rápido** com exatidão equivalente (ambos < 10⁻⁶). O ganho cresce
318
+ com n (o polar paga 40 iterações fixas; o Cayley sai cedo perto do nascimento —
319
+ 1–3 iterações típicas).
320
+
321
+ ---
322
+
323
+ ## §5 — Proposta 4: roteamento e perda auxiliar em ops de tensor (moe_redirecionavel.rs)
324
+
325
+ ### 5.1 Análise da proposta
326
+
327
+ O esboço terminava em `unimplemented!("Kernel customizado de Scatter/Gather
328
+ necessário")` — não era código executável. A fórmula da perda auxiliar
329
+ `E·Σ_e f_e·p̄_e` está **correta** (Switch Transformer, Fedus et al. 2022 — e
330
+ já era a da casa, M-R1). A substância (manter roteamento/acúmulo/perda em
331
+ operações de tensor, sem round-trips por especialista) é válida e — como o
332
+ ambiente é CPU — reinterpretada como "ops de tensor rastreadas", não "GPU".
333
+
334
+ ### 5.2 Teo M-R6 (equivalência de valores) ∎
335
+
336
+ `fwd_vetorizado` compartilha com `fwd` o **mesmo auxiliar de seleção**
337
+ (`selecionar_com_capacidade`, extraído verbatim da v0.6.0: mesma ordem de
338
+ candidatos, mesmos desempates estáveis de `sort_by`) ⇒ mesmos tokens aos
339
+ mesmos especialistas, mesma carga, mesmos descartados. O acúmulo usa
340
+ `index_add` na **mesma ordem** de inserção que o loop Rust da v0.6.0 ⇒
341
+ somas na mesma sequência ⇒ **valores bit-a-bit idênticos** (medido:
342
+ max|Δsaída| = 0,0). A perda auxiliar: mesma fórmula com `p̄` do tensor vivo. ∎
343
+
344
+ ### 5.3 Teo M-R7 (o bug v080-1 e sua correção) ∎
345
+
346
+ **O bug**: na `fwd` v0.6.0, a contribuição de cada especialista era
347
+ materializada por `to_vec2::<f32>()` e recriada com `Tensor::from_vec` —
348
+ operação que **cria um novo tensor folha, desconectado do grafo** (o
349
+ `from_vec` não carrega histórico de autograd). Idem os pesos `w` e a perda
350
+ auxiliar (constituída de `from_vec` de valores lidos). Consequência: na
351
+ passada MoE, `∂perda/∂(pesos dos especialistas) = 0` e
352
+ `∂perda/∂(pesos do roteador) = 0` — **apenas a atenção própria treinava** no
353
+ braço MoE (o resíduo `y + roteamento` só conecta `y`).
354
+
355
+ **Prova do bug (medida)**: com `perda = ‖saida‖²`, os gradientes do roteador e
356
+ dos especialistas são **exatamente 0,0** na `fwd` (normas 0/0) e **> 0** na
357
+ `fwd_vetorizada` (1,36·10⁵ / 2,30·10⁶ na medição) — teste mr7.
358
+
359
+ **A correção**: (a) `saida = saida.index_add(&idx, &contrib, 0)` — `IndexAdd`
360
+ é uma op **rastreada** pelo autograd do candle (enum `BackpropOp::IndexAdd`,
361
+ fonte do crate); (b) os pesos `w` são fatiados do PRÓPRIO tensor `probs`
362
+ (`index_select` das linhas + `narrow` da coluna) — gradiente flui ao roteador;
363
+ (c) a perda auxiliar usa `p̄ = probs_local.mean_keepdim(0)` do tensor vivo,
364
+ com `f` constante (padrão Switch: argmax não é diferenciável, a média das
365
+ probabilidades carrega o gradiente). ∎
366
+
367
+ **Corolário (leitura honesta da v0.6.0)**: o resultado publicado em r-27
368
+ "JEPA 3 braços: pequena 0,129 > densa 0,107 > moe 0,059 — roteamento ganha
369
+ 45% da capacidade equiparada" estava **confundido**: no braço MoE só a atenção
370
+ própria treinava; o número refletia atenção+resíduo, não o roteamento. A
371
+ re-ablação com o gradiente restaurado (abaixo) é a leitura correta.
372
+
373
+ **M-R8 (fim-a-fim)**: 5 passos de AdamW pela `fwd_vetorizado` mudam os pesos
374
+ dos especialistas (δ > 0) — regressão permanente do bug.
375
+
376
+ ### 5.4 Re-ablação JEPA 3 braços (gradiente restaurado; 120 passos, lote 64×12, máscara 50%)
377
+
378
+ A inicialização usa o RNG global do candle (não semeado pela API atual) — os
379
+ números variam entre execuções; gates direcionais (moe ≫ pequena em quedas)
380
+ observados em TODAS as execuções. Execução típica:
381
+
382
+ | Braço | perda inicial | perda final | queda |
383
+ |-------|--------------|-------------|-------|
384
+ | pequena (2·d) | 1,000 | 0,765 | −23,5% |
385
+ | densa (capacidade equiparada 2·d·e) | 1,062 | 0,984 | −7,4% |
386
+ | **moe (4+4 especialistas, top-2)** | 0,660 | **0,075** | **−88,7%** |
387
+
388
+ Leitura honesta: (a) o MoE agora treina especialistas+roteador+atenção e a
389
+ queda é dramática; (b) a densa **quase não aprende** (e em algumas execuções
390
+ piora) — com lr 3e-3 fixo e 120 passos, a rede densa grande é instável neste
391
+ orçamento; isso NÃO é evidência de superioridade estrutural permanente do
392
+ MoE, e sim de que o roteamento + capacidade particionada treina melhor neste
393
+ regime — a afirmação forte requer o pareamento wall-clock/capacidade
394
+ (débito herdado V15, mantido); (c) a inicialização do braço moe já parte de
395
+ perda menor (0,66 vs 1,0) porque a saída residual `y + roteamento` com pesos
396
+ ~1/e_local reduz a escala inicial — comparação de QUEDA relativa, não de
397
+ nível absoluto.
398
+
399
+ ### 5.5 Medição de custo
400
+
401
+ | Versão | tempo/passada (n=256, d=32, E=8) |
402
+ |--------|----------------------------------|
403
+ | fwd (referência, round-trips) | 1,93 ms |
404
+ | **fwd_vetorizada (index_add)** | **1,58 ms** |
405
+
406
+ 1,22× mais rápida (o gargalo remanescente é a seleção discreta em Rust —
407
+ O(E·n·log n) — compartilhada por ambas; a proposta de substituí-la por
408
+ top-k em tensor fica como extensão: exigiria `scatter_add` com semântica de
409
+ capacidade não disponível em candle 0.11 sem kernel customizado).
410
+
411
+ ---
412
+
413
+ ## §6 — Bugs catalogados nesta iteração (causa-raiz, tradição r-07/r-27)
414
+
415
+ | ID | Onde | Causa-raiz | Correção |
416
+ |----|------|-----------|----------|
417
+ | v080-1 | moe `fwd` v0.6.0 | `to_vec2`/`from_vec` seccionam o grafo de autograd — especialistas/roteador sem gradiente (exposto pela proposta Qwen nº 4) | `fwd_vetorizado` com `index_add` rastreada; treinos migrados; `fwd` mantida como referência documentada |
418
+ | v080-2 | quantizador `gsq_suave` v0.6.0 (pré-existente) | `debug_assert_eq!(ruido.len(), z.len()/d*k*d/d*k)` — expressão sem sentido (320 ≠ 40) que dispara em TODO build debug; os gates v0.7.0 só passavam em release (assert compilado fora) | fórmula correta `n*k`; testes agora verdes em debug E release |
419
+ | v080-3 (meu) | fnh `rotacao_cayley` (1ª versão) | `mx.affine(-1.0, 2.0)` soma 2 a TODAS as entradas (2·J − MX) em vez de só à diagonal — iteração diverge (NaN) | `ident.affine(2.0,0.0)?.sub(&mx)` = 2I − MX |
420
+ | v080-4 (meu) | arvore `anexar_filho` (1ª versão) | 1 push em `credito`/`regulacao` por aresta, mas são indexadas por ENLACE (2 por aresta dupla) — out-of-bounds | 2 pushes (1 por enlace) |
421
+ | v080-5 (meu) | teste mr8 (1ª versão) | snapshot via `.detach()` **compartilha o storage** com a Var — o `set()` do otimizador sobrescreve a "cópia" ⇒ delta ≡ 0 (falso negativo) | snapshot por `to_vec1` (dados próprios) |
422
+
423
+ O v080-3/4/5 foram capturados pelos próprios testes novos antes de qualquer
424
+ publicação — o processo analisar→provar→testar funcionou como projetado.
425
+
426
+ ---
427
+
428
+ ## §7 — Resultados consolidados (cargo 99/99, 3× determinismo; ver §2–§5)
429
+
430
+ 1. **Newton (verificador)**: 14 vs ~44 avaliações (3,1×), 1,5× tempo, T*
431
+ concorda 3,7·10⁻⁵; golden-section mantido como rede de segurança;
432
+ unimodalidade promovida de empírica a teorema (N-2).
433
+ 2. **Árvore iterativa**: bit-a-bit idêntica (T-A7), 100k nós sem estourar
434
+ pilha (T-A7b), ~11% mais rápida em cadeia de 20k; detecção de
435
+ duplicatas/ciclos (T-A8/T-A9) que a v0.6.0 não tinha.
436
+ 3. **Cayley (FNH)**: 4,1× mais rápido que polar, δ_ort ≤ 8,9·10⁻⁷, det = +1,
437
+ nascimento neutro com grafo vivo, convergência da inversão PROVADA (F-10);
438
+ 2 erratas na proposta (inv() inexistente; sinal expm).
439
+ 4. **MoE vetorizada**: valores bit-a-bit idênticos, 1,22× mais rápida,
440
+ gradiente restaurado (bug v080-1), re-ablação honesta com MoE −88,7%.
441
+ 5. **Regressão**: 84 testes herdados verdes (incluindo os 2 do quantizador
442
+ agora também em debug — v080-2) + 15 novos (n1–n4b, ta7/ta7b/ta7c/ta8,
443
+ f9–f12, mr6/mr7/mr8) = **99/99**; 3 execuções consecutivas idênticas.
444
+
445
+ ## §8 — Débitos honestos remanescentes
446
+
447
+ 1. A ablação JEPA usa o RNG global do candle na inicialização (não semeado) —
448
+ os gates são direcionais; a semeadura determinística da inicialização é
449
+ extensão desejável (expor `candle` seed ou inicializar via `RngDeterministico`).
450
+ 2. A seleção top-k com capacidade permanece em Rust O(E·n·log n) — vetorializá-
451
+ la exigiria kernel customizado (`scatter_add` com semântica de capacidade
452
+ não disponível em candle 0.11); o ganho marginal sobre o já-otimizado
453
+ acúmulo não justifica a complexidade agora.
454
+ 3. O pareamento wall-clock/capacidade entre braços (V15, herdado) segue
455
+ pendente — a nova ablação não fecha essa conta.
456
+ 4. `nll_newton` supera `nll_golden` em 2,4·10⁻⁷ (nível ε-f32): ambos estão
457
+ dentro do ruído do mínimo; um passo de polimento golden pós-Newton
458
+ eliminaria a diferença cosmética se algum gate futuro exigir.
459
+ 5. Cayley não representa rotações de ângulo exatamente π (medida zero) —
460
+ irrelevante ao treino, registrado por completude.
resultados/suite_v080_final.json ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "versao": "0.8.0",
3
+ "blocos": {
4
+ "legado_v070": "73/73",
5
+ "q1_newton": "3/3",
6
+ "q3_cayley": "2/2",
7
+ "q2_arvore": "1/1",
8
+ "q4_moe": "3/3",
9
+ "q5_ablacao": "2/2",
10
+ "cargo": "97/97"
11
+ },
12
+ "gates": {
13
+ "legado_legado_calibracao_soundness.v1_calibracao": true,
14
+ "legado_legado_calibracao_soundness.v2_soundness": true,
15
+ "legado_legado_calibracao_soundness.v3_tau_positivo": true,
16
+ "legado_legado_ciclo_d_sem_oraculo.aprendeu": true,
17
+ "legado_legado_ciclo_d_sem_oraculo.terminou": true,
18
+ "legado_legado_ciclo_d_sem_oraculo.sem_oraculo": true,
19
+ "legado_legado_ciclo_d_sem_oraculo.calibrado": true,
20
+ "legado_legado_ciclo_d_sem_oraculo.ganho_nao_negativo": true,
21
+ "legado_legado_ciclo_d_sem_oraculo.verificador_sem_oraculo": true,
22
+ "legado_legado_escalonamento_n.tp2_ncm": true,
23
+ "legado_legado_fase_c.verificador_sem_oraculo": true,
24
+ "legado_legado_fase_c.calibracao_nll_otima": true,
25
+ "legado_legado_fase_c.ece_aceitavel_para_soundness": true,
26
+ "legado_legado_fase_c.correcao_ativa": true,
27
+ "legado_legado_fase_c.acoplador_sem_timeout": true,
28
+ "legado_legado_fase_c.acoplador_duplex_ativo": true,
29
+ "legado_legado_fase_c.confianca_pos_nao_inferior": true,
30
+ "legado_legado_fase_c.backbone_preservado": true,
31
+ "legado_legado_fase_c.pcgrad_sem_conflito_observado": true,
32
+ "legado_legado_provas_mr.mr1_capacidade": true,
33
+ "legado_legado_provas_mr.mr1_perda_aux": true,
34
+ "legado_legado_provas_mr.mr2_neutralidade": true,
35
+ "legado_legado_provas_mr.mr3_paralelismo": true,
36
+ "legado_legado_provas_mr.mr5_water_filling": true,
37
+ "legado_legado_jepa_moe.jepa_moe_aprende": true,
38
+ "legado_legado_jepa_moe.jepa_moe_vs_densa": true,
39
+ "legado_legado_jepa_moe.jepa_moe_vs_pequena": true,
40
+ "legado_legado_som_moe.moe_refina": true,
41
+ "legado_legado_som_moe.redirect_decodifica": true,
42
+ "legado_legado_som_moe.wf_melhor_uniforme": true,
43
+ "legado_legado_acoplador_duplo.duplex_simultaneo": true,
44
+ "legado_legado_acoplador_duplo.sem_timeout": true,
45
+ "legado_legado_acoplador_duplo.terminou": true,
46
+ "legado_legado_iter.gate_ucb1_encontrou_moe": true,
47
+ "legado_legado_iter.gate_gap_positivo_vs_baseline": true,
48
+ "legado_legado_iter.gate_arrependimento_pequeno": true,
49
+ "legado_legado_iter.gate_regulacao_ocorreu": true,
50
+ "legado_legado_iter.gate_parada_ou_teto": true,
51
+ "legado_legado_iter.gate_melhor_aprendeu": true,
52
+ "legado_legado_iter.a1_tau_quantil": true,
53
+ "legado_legado_iter.a2_b_finito": true,
54
+ "legado_legado_iter.a3_n_reduzido": true,
55
+ "legado_legado_iter.a4_ucb1_achou": true,
56
+ "legado_legado_iter.a5_gate_subiu": true,
57
+ "legado_legado_legada_suite_v": true,
58
+ "legado_legado_cargo_54": true,
59
+ "legado_fnh_f1_so_n_exato": true,
60
+ "legado_fnh_f3_gate_zona_morta": true,
61
+ "legado_fnh_f3_gate_abre": true,
62
+ "legado_fnh_f4_homeostase_theta_congelado": true,
63
+ "legado_fnh_f4_rotacao_aprende_com_punicao": true,
64
+ "legado_fnh_a2_delta_ort_treino_real": true,
65
+ "legado_fnh_a3_punicao_real_medida": true,
66
+ "legado_arvore_quebra_t1_invariantes": true,
67
+ "legado_arvore_quebra_t2_agregacao_exata": true,
68
+ "legado_arvore_quebra_t3_teto_simetrico": true,
69
+ "legado_arvore_quebra_q1_executa_real": true,
70
+ "legado_arvore_quebra_q2_soundness_aceites": true,
71
+ "legado_arvore_quebra_q3_invariantes_pos_quebra": true,
72
+ "legado_arvore_quebra_q4_punicao_registrada": true,
73
+ "legado_arvore_quebra_q5_sintetico_mistura_aceita_rejeita": true,
74
+ "legado_nlp_nlg_n_coerencia_ok": true,
75
+ "legado_nlp_nlg_n_espec_ok": true,
76
+ "legado_nlp_nlg_n_gate_medio_ok": true,
77
+ "legado_nlp_nlg_n_nlg_neutro": true,
78
+ "legado_nlp_nlg_n_nlp_neutro": true,
79
+ "legado_nlp_nlg_n_ctx_real_bigramas_unicos": true,
80
+ "legado_nlp_nlg_s_piso_ok": true,
81
+ "legado_nlp_nlg_s_qe_sobe": true,
82
+ "legado_nlp_nlg_s_stop_dispara": true,
83
+ "legado_nlp_nlg_s_tetos_ok": true,
84
+ "legado_telemetria_painel_v070": true,
85
+ "legado_treino_real_completo": true,
86
+ "q1_newton_menos_avaliacoes": true,
87
+ "q1_newton_mesmo_t": true,
88
+ "q1_newton_nll_nao_piora": true,
89
+ "q3_cayley_ambos_exatos": true,
90
+ "q3_cayley_cayley_mais_rapido": true,
91
+ "q2_arvore_iguais_bit_a_bit": true,
92
+ "q4_moe_bug_v080_1_confirmado": true,
93
+ "q4_moe_gradiente_restaurado": true,
94
+ "q4_moe_valores_iguais": true,
95
+ "q5_ablacao_moe_aprende_3x": true,
96
+ "q5_ablacao_moe_domina_pequena_3x": true,
97
+ "cargo_todos_verdes": true
98
+ },
99
+ "medicoes": {
100
+ "q1": {
101
+ "t_golden": 0.3441249091032295,
102
+ "t_newton": 0.34408817449108714,
103
+ "n_aval_newton": 14,
104
+ "nll_golden": 0.6992896795272827,
105
+ "nll_newton": 0.6992899179458618,
106
+ "tempo_golden_s": 0.00854757,
107
+ "tempo_newton_s": 0.005394317
108
+ },
109
+ "q3": {
110
+ "delta_ort_polar": 2.2211075543847625e-07,
111
+ "delta_ort_cayley": 8.905649464341503e-07,
112
+ "tempo_polar_s": 0.0018697242799999999,
113
+ "tempo_cayley_s": 0.00045946215999999996,
114
+ "speedup": 4.069375985173622
115
+ },
116
+ "q2": {
117
+ "n_nos": 20012,
118
+ "total_iter": 1.0,
119
+ "tempo_iter_s": 0.00387545,
120
+ "tempo_rec_s": 0.004315812
121
+ },
122
+ "q4": {
123
+ "max_dif_valores": 0.0,
124
+ "tempo_ref_s": 0.0018790862,
125
+ "tempo_vet_s": 0.0015372232,
126
+ "grad_roteador_vet": 146917.96875,
127
+ "grad_especialistas_vet": 2082642.984375
128
+ },
129
+ "q5": {
130
+ "quedas_moe_pct": [
131
+ 87.36906150833764,
132
+ 87.36640644795425,
133
+ 90.96589194658534
134
+ ],
135
+ "quedas_pequena_pct": [
136
+ 34.354170840846855,
137
+ 32.05262864676725,
138
+ 50.874721884012516
139
+ ],
140
+ "quedas_densa_pct": [
141
+ -15.90896709522771,
142
+ -5.185563598999177,
143
+ -9.804683063936093
144
+ ],
145
+ "nota": "inicialização via RNG global do candle — gates direcionais"
146
+ }
147
+ },
148
+ "veredito": "85/85"
149
+ }
scripts/68_suite_v080.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """68_suite_v080.py — v0.8.0: VEREDITO CONSOLIDADO das propostas Qwen3.8.
3
+
4
+ Executa demo_qwen_v080 TRÊS vezes (a ablação JEPA usa o RNG global do candle
5
+ na inicialização — gates direcionais agregados sobre execuções), herda os 73
6
+ gates da v0.7.0 (suite_v070_final.json) e adiciona os gates das 4 propostas
7
+ analisadas/provadas/testadas em r-33:
8
+ 1. Newton analítico (verificador) — N-1..N-4
9
+ 2. Árvore pós-ordem iterativa — T-A7..T-A9
10
+ 3. Cayley exato (FNH) — F-9..F-12
11
+ 4. MoE vetorizada com gradiente restaurado (bug v080-1) — M-R6..M-R8
12
+ """
13
+ import json, os, sys
14
+
15
+ def main():
16
+ pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ R = os.path.join(pasta, "resultados")
18
+ rel = {"versao": "0.8.0", "blocos": {}, "gates": {}}
19
+
20
+ def carrega(arq):
21
+ p = os.path.join(R, arq)
22
+ if os.path.exists(p):
23
+ with open(p) as f:
24
+ return json.load(f)
25
+ return None
26
+
27
+ # ---- bloco legado v0.7.0 (se presente)
28
+ legado = carrega("suite_v070_final.json")
29
+ if legado:
30
+ g_legado = legado.get("gates", legado)
31
+ ok = sum(1 for v in g_legado.values() if v is True)
32
+ rel["blocos"]["legado_v070"] = f"{ok}/{len(g_legado)}"
33
+ for k, v in g_legado.items():
34
+ rel["gates"][f"legado_{k}"] = v
35
+
36
+ # ---- demos v0.8.0 (3 execuções)
37
+ import khtst_rust as k
38
+ execucoes = [k.demo_qwen_v080() for _ in range(3)]
39
+
40
+ # (1) Newton vs golden
41
+ nvg = execucoes[0]["newton_vs_golden"]
42
+ for nome, val in nvg["gates"].items():
43
+ rel["gates"][f"q1_newton_{nome}"] = val
44
+ rel["blocos"]["q1_newton"] = f"{sum(1 for v in nvg['gates'].values() if v)}/{len(nvg['gates'])}"
45
+ rel["medicoes"] = {
46
+ "q1": {kk: nvg[kk] for kk in
47
+ ("t_golden", "t_newton", "n_aval_newton", "nll_golden", "nll_newton",
48
+ "tempo_golden_s", "tempo_newton_s")},
49
+ }
50
+
51
+ # (2) Cayley vs polar
52
+ cvp = execucoes[0]["cayley_vs_polar"]
53
+ for nome, val in cvp["gates"].items():
54
+ rel["gates"][f"q3_cayley_{nome}"] = val
55
+ rel["blocos"]["q3_cayley"] = f"{sum(1 for v in cvp['gates'].values() if v)}/{len(cvp['gates'])}"
56
+ rel["medicoes"]["q3"] = {kk: cvp[kk] for kk in
57
+ ("delta_ort_polar", "delta_ort_cayley",
58
+ "tempo_polar_s", "tempo_cayley_s", "speedup")}
59
+
60
+ # (3) árvore iterativa
61
+ arv = execucoes[0]["arvore_iter_vs_rec"]
62
+ for nome, val in arv["gates"].items():
63
+ rel["gates"][f"q2_arvore_{nome}"] = val
64
+ rel["blocos"]["q2_arvore"] = f"{sum(1 for v in arv['gates'].values() if v)}/{len(arv['gates'])}"
65
+ rel["medicoes"]["q2"] = {kk: arv[kk] for kk in
66
+ ("n_nos", "total_iter", "tempo_iter_s", "tempo_rec_s")}
67
+
68
+ # (4) MoE vetorizada (determinística — valores/gradientes)
69
+ moe = execucoes[0]["moe_vetorizada"]
70
+ for nome, val in moe["gates"].items():
71
+ rel["gates"][f"q4_moe_{nome}"] = val
72
+ rel["blocos"]["q4_moe"] = f"{sum(1 for v in moe['gates'].values() if v)}/{len(moe['gates'])}"
73
+ rel["medicoes"]["q4"] = {kk: moe[kk] for kk in
74
+ ("max_dif_valores", "tempo_ref_s", "tempo_vet_s",
75
+ "grad_roteador_vet", "grad_especialistas_vet")}
76
+
77
+ # (5) ablação JEPA 3 braços — gates DIRECIONAIS nas 3 execuções
78
+ quedas_moe, quedas_peq, quedas_den = [], [], []
79
+ for ex in execucoes:
80
+ ab = ex["ablacao_jepa_3_bracos_v080"]
81
+ quedas_moe.append(ab["moe"]["queda_pct"])
82
+ quedas_peq.append(ab["pequena"]["queda_pct"])
83
+ quedas_den.append(ab["densa"]["queda_pct"])
84
+ moe_min = min(quedas_moe)
85
+ peq_max = max(quedas_peq)
86
+ rel["gates"]["q5_ablacao_moe_aprende_3x"] = all(q > 30.0 for q in quedas_moe)
87
+ rel["gates"]["q5_ablacao_moe_domina_pequena_3x"] = moe_min > peq_max
88
+ rel["medicoes"]["q5"] = {
89
+ "quedas_moe_pct": quedas_moe, "quedas_pequena_pct": quedas_peq,
90
+ "quedas_densa_pct": quedas_den,
91
+ "nota": "inicialização via RNG global do candle — gates direcionais",
92
+ }
93
+ rel["blocos"]["q5_ablacao"] = (
94
+ f"{sum(1 for v in (rel['gates']['q5_ablacao_moe_aprende_3x'], rel['gates']['q5_ablacao_moe_domina_pequena_3x']) if v)}/2")
95
+
96
+ # ---- provas cargo (contagem)
97
+ try:
98
+ import subprocess, re
99
+ res = subprocess.run(
100
+ ["cargo", "test", "--manifest-path", os.path.join(pasta, "Cargo.toml")],
101
+ capture_output=True, text=True, timeout=1800,
102
+ env={**os.environ, "PATH": os.environ["PATH"] + ":" + os.path.expanduser("~/.cargo/bin")},
103
+ )
104
+ saida = res.stdout + res.stderr
105
+ linhas = [l for l in saida.splitlines() if "test result:" in l]
106
+ m = re.search(r"(\d+) passed; (\d+) failed", linhas[0] if linhas else "")
107
+ if m:
108
+ total_c = int(m.group(1)) + int(m.group(2))
109
+ rel["blocos"]["cargo"] = f"{m.group(1)}/{total_c}"
110
+ rel["gates"]["cargo_todos_verdes"] = m.group(2) == "0"
111
+ else:
112
+ rel["blocos"]["cargo"] = "sem resultado"
113
+ rel["gates"]["cargo_todos_verdes"] = False
114
+ except Exception as e:
115
+ rel["blocos"]["cargo"] = f"erro: {e}"
116
+ rel["gates"]["cargo_todos_verdes"] = False
117
+
118
+ # ---- veredito
119
+ total = len(rel["gates"])
120
+ ok = sum(1 for v in rel["gates"].values() if v is True)
121
+ rel["veredito"] = f"{ok}/{total}"
122
+ with open(os.path.join(R, "suite_v080_final.json"), "w") as f:
123
+ json.dump(rel, f, indent=1, ensure_ascii=False)
124
+ print(json.dumps({"blocos": rel["blocos"], "veredito": rel["veredito"]},
125
+ indent=1, ensure_ascii=False))
126
+ sys.exit(0 if ok == total else 1)
127
+
128
+ if __name__ == "__main__":
129
+ main()
scripts/69_publicar_v080.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """69_publicar_v080.py — Publicação da v0.8.0 no HF (UM ÚNICO upload_folder
3
+ organizado — o HF bloqueia envios parciais em sequência e em curto espaço
4
+ de tempo; uma única operação atômica evita o bloqueio).
5
+
6
+ Fases:
7
+ 1. inventário da conta + APAGAMENTO de estados de treino remanescentes;
8
+ 2. varredura local do VALOR do token (nenhum arquivo pode contê-lo);
9
+ 3. UM ÚNICO upload_folder com ignore_patterns EXPLÍCITOS (lição v0.7.0);
10
+ 4. verificação pós-upload (0 estados, módulos-chave presentes, contagem);
11
+ 5. varredura REMOTA do valor do token (baixa e inspeciona os arquivos).
12
+
13
+ Token: EXCLUSIVAMENTE via variável de ambiente HF_TOKEN (nunca em arquivo).
14
+ """
15
+ import os, sys, tempfile
16
+
17
+ ARQUIVOS_EXCLUIR_DIRS = {"target", ".cargo", "__pycache__", ".git", ".venv",
18
+ "skills", "externos"}
19
+ ARQUIVOS_EXCLUIR_NOME = {"shard_treino.jsonl", "shard_validacao.jsonl",
20
+ "bpe.json", "rustup-init.sh"}
21
+ SUFIXOS_ESTADO = (".pt", ".pth", ".ckpt", ".safetensors", ".bin", ".npz",
22
+ ".msgpack")
23
+ NOMES_ESTADO = {"gerador_conceitos.json", "tokenizador_conceitos.json",
24
+ "gerador_aritmetica.json"}
25
+
26
+ def coletar(pasta):
27
+ arquivos = {}
28
+ for raiz, dirs, fs in os.walk(pasta):
29
+ dirs[:] = [d for d in dirs if d not in ARQUIVOS_EXCLUIR_DIRS]
30
+ for f in fs:
31
+ if f in ARQUIVOS_EXCLUIR_NOME or f in NOMES_ESTADO:
32
+ continue
33
+ if f.endswith(SUFIXOS_ESTADO) or f.endswith(".estado.json"):
34
+ continue
35
+ if f.endswith(".so") or f in (".gitignore", "Cargo.lock.ori"):
36
+ continue
37
+ if f.startswith("hf_") or f.startswith("intros_"):
38
+ continue # scripts de análise externa (não fazem parte)
39
+ caminho = os.path.join(raiz, f)
40
+ rel = os.path.relpath(caminho, pasta)
41
+ arquivos[rel] = caminho
42
+ return arquivos
43
+
44
+ def varre_token_em_arquivos(token, pares):
45
+ tb = token.encode()
46
+ achados = []
47
+ for rel, p in pares.items():
48
+ try:
49
+ with open(p, "rb") as f:
50
+ if tb in f.read():
51
+ achados.append(rel)
52
+ except OSError:
53
+ pass
54
+ return achados
55
+
56
+ def main():
57
+ from huggingface_hub import HfApi
58
+ token = os.environ.get("HF_TOKEN")
59
+ if not token:
60
+ print("ERRO: HF_TOKEN ausente"); sys.exit(1)
61
+ api = HfApi(token=token)
62
+ repo = "PowerMachine/khtst-rust"
63
+
64
+ # ---------------- 1) inventário da conta + apaga estados ----------------
65
+ print("=== 1) inventário da conta HF (procura estados de treino) ===")
66
+ repos = list(api.list_models(author="PowerMachine"))
67
+ print(f"repos: {len(repos)}")
68
+ total_removidos = 0
69
+ for r in repos:
70
+ try:
71
+ files = api.list_repo_files(repo_id=r.id, repo_type="model")
72
+ except Exception as e:
73
+ print(f" {r.id}: erro ao listar ({e})"); continue
74
+ estados = [f for f in files
75
+ if f.endswith(SUFIXOS_ESTADO)
76
+ or f.split("/")[-1] in NOMES_ESTADO
77
+ or (".meta.json" in f and "checkpoint" in f)]
78
+ if estados:
79
+ print(f" {r.id}: APAGANDO {len(estados)} estados")
80
+ for f in estados:
81
+ api.delete_file(path_in_repo=f, repo_id=r.id, repo_type="model")
82
+ total_removidos += len(estados)
83
+ else:
84
+ print(f" {r.id}: {len(files)} arquivos, 0 estados")
85
+ print(f"estados removidos no total: {total_removidos}")
86
+
87
+ # ---------------- 2) varredura local do token ----------------
88
+ pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
89
+ arquivos = coletar(pasta)
90
+ print(f"\n=== 2) varredura local: {len(arquivos)} arquivos ===")
91
+ achados = varre_token_em_arquivos(token, arquivos)
92
+ if achados:
93
+ print(f"ERRO: valor do token encontrado em: {achados}"); sys.exit(1)
94
+ print("varredura de token local: limpa")
95
+ import subprocess
96
+ home = os.path.expanduser("~")
97
+ try:
98
+ saida = subprocess.run(
99
+ ["grep", "-rl", token, home, "--include=*.py", "--include=*.json",
100
+ "--include=*.md", "--include=*.sh", "--include=*.toml"],
101
+ capture_output=True, text=True, timeout=120)
102
+ if saida.stdout.strip():
103
+ print(f"ERRO: token no home: {saida.stdout.strip()}"); sys.exit(1)
104
+ except Exception:
105
+ pass
106
+ print("varredura do home: limpa")
107
+
108
+ # ---------------- 3) UM ÚNICO upload_folder ----------------
109
+ ignores = (
110
+ [f"{d}/**" for d in ARQUIVOS_EXCLUIR_DIRS]
111
+ + [f"{d}" for d in ARQUIVOS_EXCLUIR_DIRS]
112
+ + list(ARQUIVOS_EXCLUIR_NOME) + list(NOMES_ESTADO)
113
+ + list(SUFIXOS_ESTADO)
114
+ + ["*.so", ".gitignore", "hf_*.py", "hf_*.json", "intros_*.json",
115
+ "*.estado.json"]
116
+ )
117
+ print(f"\n=== 3) upload_folder ÚNICO ({len(arquivos)} arquivos) ===")
118
+ api.upload_folder(
119
+ repo_id=repo, repo_type="model", folder_path=pasta,
120
+ ignore_patterns=ignores, commit_message="v0.8.0: análise/prova/teste "
121
+ "das 4 propostas Qwen3.8 (r-33) — Newton analítico no verificador "
122
+ "(convexidade em gamma=1/T), árvore pós-ordem iterativa bit-a-bit "
123
+ "com detecção de ciclos, Cayley exato no FNH (4,1× mais rápido), "
124
+ "MoE vetorizada com gradiente RESTAURADO (bug v080-1) — 85/85 gates, "
125
+ "99/99 cargo",
126
+ )
127
+ print("upload concluído")
128
+
129
+ # ---------------- 4) verificação pós-upload ----------------
130
+ print("\n=== 4) verificação pós-upload ===")
131
+ files = api.list_repo_files(repo_id=repo, repo_type="model")
132
+ estados = [f for f in files if f.endswith(SUFIXOS_ESTADO)
133
+ or f.split("/")[-1] in NOMES_ESTADO]
134
+ assert not estados, f"estados publicados: {estados}"
135
+ chaves = ["src/verificador.rs", "src/arvore_pesos.rs", "src/fnh.rs",
136
+ "src/moe_redirecionavel.rs", "scripts/68_suite_v080.py",
137
+ "scripts/69_publicar_v080.py",
138
+ "docs/memorias/r-33-analise-provas-qwen-v080.md",
139
+ "resultados/suite_v080_final.json"]
140
+ faltando = [c for c in chaves if c not in files]
141
+ assert not faltando, f"faltando: {faltando}"
142
+ print(f"repo: {len(files)} arquivos | 0 estados | módulos-chave OK")
143
+
144
+ # ---------------- 5) varredura REMOTA do token ----------------
145
+ print("\n=== 5) varredura REMOTA do valor do token ===")
146
+ with tempfile.TemporaryDirectory() as tmp:
147
+ achados_remotos = []
148
+ for f in files:
149
+ if f.endswith((".py", ".rs", ".md", ".json", ".toml", ".sh", ".cfg",
150
+ ".txt", ".jsonl")):
151
+ try:
152
+ p = api.hf_hub_download(repo_id=repo, repo_type="model",
153
+ filename=f, local_dir=tmp)
154
+ with open(p, "rb") as fh:
155
+ if token.encode() in fh.read():
156
+ achados_remotos.append(f)
157
+ except Exception:
158
+ pass
159
+ if achados_remotos:
160
+ print(f"ERRO: token remoto em: {achados_remotos}"); sys.exit(1)
161
+ print("varredura remota: limpa")
162
+ print("\nPUBLICAÇÃO v0.8.0 CONCLUÍDA")
163
+
164
+ if __name__ == "__main__":
165
+ main()
src/arvore_pesos.rs CHANGED
@@ -176,10 +176,122 @@ impl ArvorePesos {
176
  caminho
177
  }
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  /// **Fundo-topo**: agrega a métrica dos filhos no pai (pós-ordem —
180
  /// T-A2: a soma na raiz = soma global de todas as folhas + próprias).
181
  /// Devolve o total agregado na raiz.
 
 
 
 
 
 
182
  pub fn agregar_fundo_topo(&mut self, metrica: &str) -> f64 {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  self.agregar_rec(0, metrica)
184
  }
185
 
@@ -201,20 +313,21 @@ impl ArvorePesos {
201
  /// **Topo-fundo**: regulação desce do nó `origem` com decaimento
202
  /// λ^k por nível de profundidade relativa (T-A3: causal — desce por
203
  /// níveis da filtração da árvore; terminante — profundidade finita).
 
 
204
  pub fn regular_topo_fundo(&mut self, origem: usize, delta: f64, lambda: f64) {
205
  let base = self.nos[origem].profundidade;
206
- self.regular_rec(origem, delta, lambda, base);
207
- }
208
-
209
- fn regular_rec(&mut self, no: usize, delta: f64, lambda: f64, base: usize) {
210
- let k = self.nos[no].profundidade.saturating_sub(base);
211
- let d = delta * lambda.powi(k as i32);
212
- let filhos = self.nos[no].filhos.clone();
213
- for f in filhos {
214
- if let Some(e) = self.enlace(no, f) {
215
- self.regulacao[e] = d; // desce pelo enlace pai→filho
216
  }
217
- self.regular_rec(f, delta, lambda, base);
218
  }
219
  }
220
 
@@ -282,6 +395,40 @@ impl ArvorePesos {
282
  ));
283
  }
284
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  Ok(())
286
  }
287
  }
@@ -392,4 +539,128 @@ mod testes {
392
  let v = a.nos[f].metricas["confianca"];
393
  assert!((v - 0.0).abs() < 1e-12, "T-A5: 0,1 − 0,1 = 0");
394
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }
 
176
  caminho
177
  }
178
 
179
+ /// Anexa um filho com ARESTA DUPLA ao nó `pai` (nome, n_params) —
180
+ /// mantém o índice de adjacência e a partição do índice global (o
181
+ /// intervalo do novo nó continua o índice). Devolve o índice do novo nó.
182
+ /// v0.8.0: habilita subárvores profundas (teste T-A7b) sem reconstruir
183
+ /// a árvore — a pós-ordem iterativa não depende da profundidade.
184
+ pub fn anexar_filho(&mut self, pai: usize, nome: &str, n_params: usize) -> usize {
185
+ let idx = self.nos.len();
186
+ let prof = self.nos[pai].profundidade + 1;
187
+ let ini = self.n_params;
188
+ let fim = ini + n_params.max(1);
189
+ self.nos.push(NoArvore {
190
+ nome: nome.to_string(),
191
+ pai: Some(pai),
192
+ filhos: Vec::new(),
193
+ ini,
194
+ fim,
195
+ profundidade: prof,
196
+ metricas: HashMap::new(),
197
+ });
198
+ self.por_nome.insert(nome.to_string(), idx);
199
+ self.nos[pai].filhos.push(idx);
200
+ // aresta dupla pai↔filho (novo id_par)
201
+ let id_par = self.enlaces.len() / 2;
202
+ self.adj.push(Vec::new());
203
+ self.adj[pai].push(self.enlaces.len());
204
+ self.enlaces.push(Enlace { id_par, de: pai, para: idx });
205
+ self.adj[idx].push(self.enlaces.len());
206
+ self.enlaces.push(Enlace { id_par, de: idx, para: pai });
207
+ // 1 entrada por ENLACE (2 por aresta dupla) — credito/regulacao são
208
+ // indexadas por enlace, não por aresta
209
+ self.credito.push(0.0);
210
+ self.credito.push(0.0);
211
+ self.regulacao.push(0.0);
212
+ self.regulacao.push(0.0);
213
+ self.n_params = fim;
214
+ idx
215
+ }
216
+
217
  /// **Fundo-topo**: agrega a métrica dos filhos no pai (pós-ordem —
218
  /// T-A2: a soma na raiz = soma global de todas as folhas + próprias).
219
  /// Devolve o total agregado na raiz.
220
+ /// v0.8.0: **ITERATIVA** (proposta Qwen-3.8 nº 2 VALIDADA, com correções
221
+ /// de robustez) — pilha explícita no heap, SEM recursão e SEM clones de
222
+ /// `filhos`: O(V+E) tempo, O(V) espaço, zero alocação por nó; segura
223
+ /// para árvores profundas degeneradas (T-A7b). Equivalência BIT-A-BIT
224
+ /// com a recursiva (T-A7): mesma ordem de pós-ordem e mesma ordem de
225
+ /// soma — prova em r-33 §3.
226
  pub fn agregar_fundo_topo(&mut self, metrica: &str) -> f64 {
227
+ match self.agregar_pos_ordem(metrica) {
228
+ Ok(v) => v,
229
+ Err(e) => {
230
+ // estrutura malformada (ciclo/filho duplicado): a recursiva
231
+ // somaria duas vezes ou não terminaria — aqui é explícito
232
+ log::warn!("agregar_fundo_topo: {e}");
233
+ f64::NAN
234
+ }
235
+ }
236
+ }
237
+
238
+ /// Pós-ordem iterativa com verificação estrutural — erro em ciclo ou
239
+ /// filho duplicado. O flag `empilhado` é marcado NO MOMENTO DO PUSH:
240
+ /// um nó empilhado duas vezes (duplicata no mesmo pai, ou re-entrada de
241
+ /// ciclo) é recusado imediatamente — a marcação pós-expansão deixaria
242
+ /// escapar o caso "pai lista o filho duas vezes" (o 2º push ocorre
243
+ /// antes da expansão do 1º — errata da própria correção, r-33 §3.4).
244
+ fn agregar_pos_ordem(&mut self, metrica: &str) -> Result<f64, String> {
245
+ let n = self.nos.len();
246
+ if n == 0 {
247
+ return Ok(0.0);
248
+ }
249
+ // fase 1: ordem de visita (pós-ordem: filhos ANTES do pai)
250
+ let mut estado = vec![0u8; n]; // 0 = novo, 1 = filhos empilhados
251
+ let mut empilhado = vec![false; n]; // marcado NO PUSH (unicidade)
252
+ let mut pilha: Vec<usize> = Vec::with_capacity(n);
253
+ let mut pos_ordem: Vec<usize> = Vec::with_capacity(n);
254
+ pilha.push(0usize);
255
+ empilhado[0] = true;
256
+ while let Some(&atual) = pilha.last() {
257
+ if estado[atual] == 0 {
258
+ estado[atual] = 1;
259
+ for &f in self.nos[atual].filhos.iter().rev() {
260
+ if f >= n {
261
+ return Err(format!("filho {f} fora do intervalo (nó {atual})"));
262
+ }
263
+ if empilhado[f] {
264
+ return Err(format!(
265
+ "filho {f} duplicado ou ciclo detectado no nó {atual}"
266
+ ));
267
+ }
268
+ empilhado[f] = true;
269
+ pilha.push(f);
270
+ }
271
+ } else {
272
+ pilha.pop();
273
+ pos_ordem.push(atual);
274
+ }
275
+ }
276
+ // fase 2: processa folhas primeiro (a pós-ordem já é fundo-topo)
277
+ for &no in pos_ordem.iter() {
278
+ let mut total = self.nos[no].metricas.get(metrica).copied().unwrap_or(0.0);
279
+ for &f in self.nos[no].filhos.iter() {
280
+ let v_filho = self.nos[f].metricas.get(metrica).copied().unwrap_or(0.0);
281
+ total += v_filho; // MESMA ordem da recursiva (bit-a-bit — T-A7)
282
+ if let Some(e) = self.enlace(f, no) {
283
+ self.credito[e] = v_filho; // crédito sobe filho→pai
284
+ }
285
+ }
286
+ self.nos[no].metricas.insert(metrica.to_string(), total);
287
+ }
288
+ Ok(self.nos[0].metricas.get(metrica).copied().unwrap_or(0.0))
289
+ }
290
+
291
+ /// Referência RECURSIVA (v0.6.0) — mantida como oráculo de equivalência
292
+ /// do teste T-A7 e documentação do ganho (não usa em produção: clone de
293
+ /// `filhos` por nó + quadros de pilha O(profundidade)).
294
+ pub fn agregar_fundo_topo_rec(&mut self, metrica: &str) -> f64 {
295
  self.agregar_rec(0, metrica)
296
  }
297
 
 
313
  /// **Topo-fundo**: regulação desce do nó `origem` com decaimento
314
  /// λ^k por nível de profundidade relativa (T-A3: causal — desce por
315
  /// níveis da filtração da árvore; terminante — profundidade finita).
316
+ /// v0.8.0: ITERATIVA (pré-ordem com pilha explícita — mesmo ganho da
317
+ /// agregação: sem clones, sem recursão; mesma ordem de escrita).
318
  pub fn regular_topo_fundo(&mut self, origem: usize, delta: f64, lambda: f64) {
319
  let base = self.nos[origem].profundidade;
320
+ let mut pilha = vec![origem];
321
+ while let Some(u) = pilha.pop() {
322
+ // k do PAI u (semântica v0.6.0 exata: a aresta u→f usa λ^k(u))
323
+ let k = self.nos[u].profundidade.saturating_sub(base);
324
+ let d = delta * lambda.powi(k as i32);
325
+ for &f in self.nos[u].filhos.iter().rev() {
326
+ if let Some(e) = self.enlace(u, f) {
327
+ self.regulacao[e] = d; // desce pelo enlace pai→filho
328
+ }
329
+ pilha.push(f);
330
  }
 
331
  }
332
  }
333
 
 
395
  ));
396
  }
397
  }
398
+ // (iv) simetria pai↔filho (v0.8.0 — T-A8): todo filho listado por u
399
+ // deve ter pai = u (captura o 2-ciclo A↔B anexado à raiz, que as
400
+ // checagens (i)-(iii) deixam passar)
401
+ for (u, no) in self.nos.iter().enumerate() {
402
+ for &f in &no.filhos {
403
+ if self.nos[f].pai != Some(u) {
404
+ return Err(format!(
405
+ "nó {u} lista filho {f} cujo pai é {:?} (assimetria)",
406
+ self.nos[f].pai
407
+ ));
408
+ }
409
+ }
410
+ }
411
+ // (v) alcançabilidade (v0.8.0 — T-A9): todo nó atingível da raiz —
412
+ // com pai único + simetria (iv), isso equivale a ACICLICO+conectado
413
+ let mut visto = vec![false; self.nos.len()];
414
+ let mut fila = vec![0usize];
415
+ visto[0] = true;
416
+ let mut alcancados = 1usize;
417
+ while let Some(u) = fila.pop() {
418
+ for &f in &self.nos[u].filhos {
419
+ if !visto[f] {
420
+ visto[f] = true;
421
+ alcancados += 1;
422
+ fila.push(f);
423
+ }
424
+ }
425
+ }
426
+ if alcancados != self.nos.len() {
427
+ return Err(format!(
428
+ "{} nós inalcançáveis da raiz (ciclo desconectado?)",
429
+ self.nos.len() - alcancados
430
+ ));
431
+ }
432
  Ok(())
433
  }
434
  }
 
539
  let v = a.nos[f].metricas["confianca"];
540
  assert!((v - 0.0).abs() < 1e-12, "T-A5: 0,1 − 0,1 = 0");
541
  }
542
+
543
+ /// **T-A7 (v0.8.0):** equivalência BIT-A-BIT da agregação iterativa com
544
+ /// a recursiva em árvores aleatórias (mesma pós-ordem, mesma ordem de
545
+ /// soma — prova r-33 §3.3). Proposta Qwen-3.8 nº 2 validada.
546
+ #[test]
547
+ fn ta7_equivalencia_bit_a_bit() {
548
+ let mut rng = crate::rng::RngDeterministico::novo(909);
549
+ for tentativa in 0..8u32 {
550
+ let mut a = arvore_exemplo();
551
+ // anexa subárvores aleatórias (ramificação E profundidade)
552
+ for j in 0..12u32 {
553
+ let pai = rng.inteiro(a.n_nos());
554
+ let nome = format!("extra_{tentativa}_{j}");
555
+ a.anexar_filho(pai, &nome, 1 + rng.inteiro(4));
556
+ }
557
+ // métricas locais aleatórias (registradas para repor depois)
558
+ let locais: Vec<f64> = (0..a.n_nos())
559
+ .map(|no| {
560
+ let v = rng.normal(0.0, 2.0);
561
+ a.nos[no].metricas.insert("punicao".into(), v);
562
+ v
563
+ })
564
+ .collect();
565
+ let esperado: f64 = locais.iter().sum();
566
+ a.invariantes_ok().expect("árvore aleatória válida");
567
+ // iterativa
568
+ let total_it = a.agregar_fundo_topo("punicao");
569
+ let credito_it = a.credito.clone();
570
+ // repõe as métricas locais e roda a recursiva (oráculo v0.6.0)
571
+ for (no, v) in locais.iter().enumerate() {
572
+ a.nos[no].metricas.insert("punicao".into(), *v);
573
+ }
574
+ let total_rec = a.agregar_fundo_topo_rec("punicao");
575
+ let credito_rec = a.credito.clone();
576
+ assert_eq!(
577
+ total_it.to_bits(), total_rec.to_bits(),
578
+ "T-A7: total bit-a-bit (tentativa {tentativa}: {total_it} vs {total_rec})"
579
+ );
580
+ for (i, (x, y)) in credito_it.iter().zip(credito_rec.iter()).enumerate() {
581
+ assert_eq!(x.to_bits(), y.to_bits(), "T-A7: crédito[{i}] bit-a-bit");
582
+ }
583
+ assert!(
584
+ (total_it - esperado).abs() < 1e-9,
585
+ "T-A7: soma global (tentativa {tentativa})"
586
+ );
587
+ }
588
+ }
589
+
590
+ /// **T-A7b (v0.8.0):** cadeia degenerada de 100.000 nós — a iterativa
591
+ /// completa SEM estourar a pilha (a recursiva exigiria ~100k quadros,
592
+ /// excedendo o limite padrão de 8 MiB — risco documentado da v0.6.0;
593
+ /// por isso ela NÃO é executada aqui).
594
+ #[test]
595
+ fn ta7b_cadeia_profunda_100k() {
596
+ let mut a = arvore_exemplo();
597
+ let mut pai = 1usize; // tronco
598
+ for i in 0..100_000u32 {
599
+ pai = a.anexar_filho(pai, &format!("c{i}"), 1);
600
+ }
601
+ assert_eq!(a.n_nos(), 100_000 + 12);
602
+ a.nos[pai].metricas.insert("conf".into(), 1.0);
603
+ let total = a.agregar_fundo_topo("conf");
604
+ assert!((total - 1.0).abs() < 1e-12, "T-A7b: total {total}");
605
+ // o crédito sobe INTEIRO por toda a cadeia (T-A2)
606
+ let e = a.enlace(pai, a.nos[pai].pai.unwrap()).unwrap();
607
+ assert!((a.credito[e] - 1.0).abs() < 1e-12, "T-A7b: crédito na folha");
608
+ assert!(a.invariantes_ok().is_ok(), "T-A7b: invariantes na cadeia");
609
+ }
610
+
611
+ /// **T-A7c (v0.8.0):** regulação iterativa preserva a semântica λ^k do
612
+ /// PAI (v0.6.0) em subárvores de profundidade 3.
613
+ #[test]
614
+ fn ta7c_regular_iterativa_equivalente() {
615
+ let mut a = arvore_exemplo();
616
+ let n1 = a.anexar_filho(1, "sub1", 2); // profundidade 2 (sob o tronco)
617
+ let n2 = a.anexar_filho(n1, "sub2", 2); // profundidade 3
618
+ let _n3 = a.anexar_filho(n2, "sub3", 2); // profundidade 4
619
+ let raiz = 0usize;
620
+ a.regular_topo_fundo(raiz, 0.8, 0.5);
621
+ // aresta raiz→tronco: k(raiz) = 0 ⇒ λ⁰
622
+ let e = a.enlace(raiz, 1).unwrap();
623
+ assert!((a.regulacao[e] - 0.8).abs() < 1e-12, "T-A7c: nível raiz");
624
+ // aresta tronco→sub1: k(tronco) = 1 ⇒ 0,8·0,5
625
+ let e1 = a.enlace(1, n1).unwrap();
626
+ assert!((a.regulacao[e1] - 0.4).abs() < 1e-12, "T-A7c: nível 1");
627
+ // aresta sub1→sub2: k(sub1) = 2 ⇒ 0,8·0,25
628
+ let e2 = a.enlace(n1, n2).unwrap();
629
+ assert!((a.regulacao[e2] - 0.2).abs() < 1e-12, "T-A7c: nível 2");
630
+ assert!(a.invariantes_ok().is_ok());
631
+ }
632
+
633
+ /// **T-A8 (v0.8.0):** detecção de estruturas malformadas — filho
634
+ /// duplicado (a iterativa recusa; a recursiva somaria DUAS vezes) e
635
+ /// ciclos (checagens (iv) simetria e (v) alcançabilidade — que as
636
+ /// checagens (i)-(iii) da v0.6.0 deixavam passar).
637
+ #[test]
638
+ fn ta8_deteccao_ciclos_e_duplicatas() {
639
+ let mut a = arvore_exemplo();
640
+ // (a) filho duplicado: raiz lista o tronco duas vezes
641
+ a.nos[0].filhos.push(1);
642
+ let total = a.agregar_fundo_topo("punicao");
643
+ assert!(total.is_nan(), "T-A8: duplicata deve recusar (NaN), não somar 2×");
644
+ a.nos[0].filhos.pop();
645
+ assert!(a.invariantes_ok().is_ok(), "T-A8: restaurada");
646
+ // (b) assimetria pai↔filho: tronco lista o quantizador (pai = raiz)
647
+ a.nos[1].filhos.push(2);
648
+ let err = a.invariantes_ok().unwrap_err();
649
+ assert!(err.contains("assimetria"), "T-A8(iv): {err}");
650
+ a.nos[1].filhos.pop();
651
+ // (c) 2-ciclo desconectado x↔y: passa (i)-(iv), capturado por (v)
652
+ let x = a.nos.len();
653
+ let y = x + 1;
654
+ a.nos.push(NoArvore {
655
+ nome: "x".into(), pai: Some(y), filhos: vec![y],
656
+ ini: 94, fim: 95, profundidade: 9, metricas: HashMap::new(),
657
+ });
658
+ a.nos.push(NoArvore {
659
+ nome: "y".into(), pai: Some(x), filhos: vec![x],
660
+ ini: 95, fim: 96, profundidade: 9, metricas: HashMap::new(),
661
+ });
662
+ a.n_params = 96;
663
+ let err = a.invariantes_ok().unwrap_err();
664
+ assert!(err.contains("inalcançáveis"), "T-A8(v): {err}");
665
+ }
666
  }
src/fnh.rs CHANGED
@@ -58,6 +58,21 @@ impl Ativacao {
58
  }
59
  }
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  /// Rotação aprendível em SO(n): R = polar(I + A + A²/2 + A³/6) com A skew
62
  /// parametrizada por θ ∈ R^{n(n−1)/2} (port de `SkewRotation`; a retração
63
  /// polar é o acréscimo v0.7.0 — Teo F-1).
@@ -70,6 +85,10 @@ pub struct RotacaoSkew {
70
  pub clamp_theta: f64,
71
  /// iterações Newton–Schulz da retração polar.
72
  pub iters_polar: usize,
 
 
 
 
73
  }
74
 
75
  impl RotacaoSkew {
@@ -80,6 +99,8 @@ impl RotacaoSkew {
80
  n,
81
  clamp_theta: 0.5,
82
  iters_polar: 40,
 
 
83
  })
84
  }
85
 
@@ -112,6 +133,16 @@ impl RotacaoSkew {
112
  Ok(tri.sub(&tri.transpose(0, 1)?)?) // A = U − Uᵀ (skew exato)
113
  }
114
 
 
 
 
 
 
 
 
 
 
 
115
  /// R = polar(expm_trunc(A)) ∈ SO(n) EXATO (δ_ort ≤ 1e-6 — teste F-1).
116
  /// Diferenciável em θ (todas as ops são matmul/afim — autograd flui).
117
  /// Atalho de nascimento: se o valor do truncado é EXATAMENTE I (θ=0 ⇒
@@ -119,7 +150,7 @@ impl RotacaoSkew {
119
  /// (evita a flakiness da σ aleatória da iteração de potência, 3% dos
120
  /// casos deixa resíduo ~1e-8) e o GRAFO permanece conectado a θ
121
  /// (gradiente flui — sem deadlock).
122
- pub fn rotacao(&self) -> Result<Tensor> {
123
  let a = self.skew()?;
124
  let a2 = a.matmul(&a)?;
125
  let a3 = a2.matmul(&a)?;
@@ -136,12 +167,106 @@ impl RotacaoSkew {
136
  retrair_polar(&trunc, self.iters_polar)
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  /// δ_ort(R) = ‖RᵀR − I‖_F (monitor).
140
  pub fn erro_ortogonalidade(&self) -> Result<f64> {
141
  erro_ortogonalidade(&self.rotacao()?)
142
  }
143
  }
144
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  /// SmoothQuant (Xiao et al. 2022) — port do `SmoothQuantCompressor`.
146
  /// s_j = max|X_j|^α / max|W_j|^(1−α) via EMA; a migração compensada
147
  /// (W·diag(s), X/diag(s)) é EXATAMENTE neutra na saída (Teo F-6 — prova
@@ -598,4 +723,231 @@ mod testes {
598
  assert!(mx.is_finite() && mx > 0.0, "F-8: saída quantizada finita");
599
  Ok(())
600
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  }
 
58
  }
59
  }
60
 
61
+ /// Modo de construção da rotação (v0.8.0 — proposta Qwen-3.8 nº 3).
62
+ /// O padrão `Polar` preserva a semântica v0.7.0 bit-a-bit (port fiel do
63
+ /// gru-ring: série truncada projetada ao SO(n)); `Cayley` é a alternativa
64
+ /// algébrica EXATA sem série infinita (ver `rotacao_cayley`).
65
+ #[derive(Clone, Copy, Debug, PartialEq, Default)]
66
+ pub enum ModoRotacao {
67
+ /// R = polar(I + A + A²/2 + A³/6) — Newton–Schulz (~82 matmuls n³).
68
+ #[default]
69
+ Polar,
70
+ /// R = (I − A/2)(I + A/2)⁻¹ — Cayley (Teo F-9); inversão pela iteração
71
+ /// de Newton X ← X(2I − MX) com saída antecipada (típico 3–8 matmuls
72
+ /// duplos, teto 12) — diferenciável e EXATA em aritmética exata.
73
+ Cayley,
74
+ }
75
+
76
  /// Rotação aprendível em SO(n): R = polar(I + A + A²/2 + A³/6) com A skew
77
  /// parametrizada por θ ∈ R^{n(n−1)/2} (port de `SkewRotation`; a retração
78
  /// polar é o acréscimo v0.7.0 — Teo F-1).
 
85
  pub clamp_theta: f64,
86
  /// iterações Newton–Schulz da retração polar.
87
  pub iters_polar: usize,
88
+ /// modo de construção (v0.8.0 — padrão Polar: compatibilidade v0.7.0).
89
+ pub modo: ModoRotacao,
90
+ /// teto de iterações da inversão de Newton do modo Cayley (F-10).
91
+ pub iters_cayley: usize,
92
  }
93
 
94
  impl RotacaoSkew {
 
99
  n,
100
  clamp_theta: 0.5,
101
  iters_polar: 40,
102
+ modo: ModoRotacao::Polar,
103
+ iters_cayley: 12,
104
  })
105
  }
106
 
 
133
  Ok(tri.sub(&tri.transpose(0, 1)?)?) // A = U − Uᵀ (skew exato)
134
  }
135
 
136
+ /// R ∈ SO(n) no modo configurado (v0.8.0: despacha para polar ou
137
+ /// Cayley — mesmo contrato: ortogonal exata, nascimento neutro,
138
+ /// diferenciável em θ).
139
+ pub fn rotacao(&self) -> Result<Tensor> {
140
+ match self.modo {
141
+ ModoRotacao::Polar => self.rotacao_polar(),
142
+ ModoRotacao::Cayley => self.rotacao_cayley(),
143
+ }
144
+ }
145
+
146
  /// R = polar(expm_trunc(A)) ∈ SO(n) EXATO (δ_ort ≤ 1e-6 — teste F-1).
147
  /// Diferenciável em θ (todas as ops são matmul/afim — autograd flui).
148
  /// Atalho de nascimento: se o valor do truncado é EXATAMENTE I (θ=0 ⇒
 
150
  /// (evita a flakiness da σ aleatória da iteração de potência, 3% dos
151
  /// casos deixa resíduo ~1e-8) e o GRAFO permanece conectado a θ
152
  /// (gradiente flui — sem deadlock).
153
+ pub fn rotacao_polar(&self) -> Result<Tensor> {
154
  let a = self.skew()?;
155
  let a2 = a.matmul(&a)?;
156
  let a3 = a2.matmul(&a)?;
 
167
  retrair_polar(&trunc, self.iters_polar)
168
  }
169
 
170
+ /// **F-9/F-10 (v0.8.0):** Transformada de Cayley
171
+ /// `R = (I + A/2)(I − A/2)⁻¹` com A = skew(θ) — proposta Qwen-3.8 nº 3
172
+ /// CORRIGIDA em DOIS pontos: (a) candle 0.11 **não possui `Tensor::inv()``
173
+ /// (a proposta assumia a API) — a inversão é feita pela iteração de
174
+ /// Newton para inversos `X ← X(2I − MX)`, 100% diferenciável, com
175
+ /// escalonamento inicial `X₀ = αMᵀ, α = 1/(1+‖B‖²_F)` de convergência
176
+ /// PROVADA (F-10); (b) **convenção de sinal** — a forma original
177
+ /// `(I − A/2)(I + A/2)⁻¹` é ortogonal, mas equivale a `expm(−A + O(A³))`
178
+ /// (identidade escalar (1−t)/(1+t) = e^{−2·artanh t} elevada a matrizes
179
+ /// comutantes); a forma aqui adotada `(I + A/2)(I − A/2)⁻¹ = expm(+A +
180
+ /// O(A³))` preserva a paridade com a série de Taylor do gru-ring
181
+ /// (verificado contra oráculo NumPy/SVD — r-33 §4.6).
182
+ ///
183
+ /// Propriedades (provas r-33 §4):
184
+ /// - **SO(n) exato em aritmética exata**: RᵀR = I e det R = +1
185
+ /// (comutação de polinômios em A; F-9.ii/iii);
186
+ /// - **Denominador sempre invertível E bem condicionado**: eig(I−A/2)
187
+ /// = 1 ± iλ/2 ⇒ |eig| = √(1+λ²/4) ≥ 1 ⇒ κ ≤ √(1+‖A‖₂²/4), limitado
188
+ /// pelo clamp — sem necessidade de LU/pivoteamento (F-9.iv);
189
+ /// - **Nascimento neutro**: θ=0 ⇒ B=0 ⇒ α=1 ⇒ X₀=Mᵀ=I e o resíduo
190
+ /// |1−α| = 0 já encerra na 1ª iteração ⇒ R = I bit-a-bit COM o grafo
191
+ /// conectado a θ (gradiente flui no nascimento — melhor que o atalho
192
+ /// do polar, que precisa de caso especial);
193
+ /// - **Limitação honesta**: −1 nunca é autovalor (ângulo exatamente π é
194
+ /// inatingível — conjunto de medida zero, irrelevante ao treino).
195
+ pub fn rotacao_cayley(&self) -> Result<Tensor> {
196
+ let a = self.skew()?;
197
+ let dev = a.device();
198
+ let ident = Tensor::eye(self.n, DType::F32, &dev)?;
199
+ let half = a.affine(0.5, 0.0)?; // B = A/2 (skew)
200
+ let m = ident.sub(&half)?; // M = I − A/2 — invertível (F-9.i)
201
+ // X₀ = αMᵀ com α = 1/(1+‖B‖²_F) ⇒ ‖I − MX₀‖₂ < 1 (F-10)
202
+ let bf2 = half.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
203
+ let alpha = 1.0 / (1.0 + bf2);
204
+ let mut x = m.transpose(0, 1)?.affine(alpha, 0.0)?;
205
+ // Newton para inversos: E_{k+1} = E_k² (quadrático); saída
206
+ // antecipada quando o resíduo ‖I − MX‖∞ < 1e-7 (típico: 1–3 iters
207
+ // perto do nascimento; ≤ 9 no pior caso do clamp, teto 12)
208
+ for _ in 0..self.iters_cayley {
209
+ let mx = m.matmul(&x)?;
210
+ let residuo = (&ident - &mx)?.abs()?.max_all()?.to_scalar::<f32>()? as f64;
211
+ if residuo < 1e-7 {
212
+ break;
213
+ }
214
+ // 2I − MX: soma 2 SÓ na diagonal (affine somaria em TODAS as
215
+ // entradas — bug corrigido: 2·J − MX diverge)
216
+ let dois_i_menos = ident.affine(2.0, 0.0)?.sub(&mx)?;
217
+ x = x.matmul(&dois_i_menos)?;
218
+ }
219
+ let num = ident.add(&half)?; // I + A/2
220
+ num.matmul(&x) // R = (I + A/2)(I − A/2)⁻¹ ≈ expm(+A)
221
+ }
222
+
223
  /// δ_ort(R) = ‖RᵀR − I‖_F (monitor).
224
  pub fn erro_ortogonalidade(&self) -> Result<f64> {
225
  erro_ortogonalidade(&self.rotacao()?)
226
  }
227
  }
228
 
229
+ /// **F-11 (v0.8.0):** rotação por planos canônicos (bloco-diagonal 2×2)
230
+ /// via Cayley em fórmula FECHADA — SEM inversão de matriz e SEM iterações.
231
+ /// Com s_k = θ_k/2:
232
+ /// `R = ⊕_k 1/(1+s_k²) · [[1−s_k², −2s_k], [2s_k, 1−s_k²]]`
233
+ /// (inversão analítica exata de [[1, s], [−s, 1]]; convenção expm(+A) —
234
+ /// Teo F-11 — r-33 §4.5).
235
+ /// `theta_planos`: (n/2,) — a rotação age em n/2 planos canônicos sem
236
+ /// mistura entre eles (família RESTRITA, n/2 graus de liberdade).
237
+ /// Diferenciável (elementwise + 1 matmul de seleção) e nascimento neutro
238
+ /// (θ=0 ⇒ R = I bit-a-bit com grafo conectado a θ). Indicada quando o
239
+ /// custo O(n³) das rotações completas domina (n grande).
240
+ pub fn rotacao_cayley_bloco(theta_planos: &Tensor) -> Result<Tensor> {
241
+ let dev = theta_planos.device();
242
+ let m = theta_planos.elem_count();
243
+ let n = 2 * m;
244
+ // s = θ/2; c = (1−s²)/(1+s²); t = 2s/(1+s²) — elementwise, grafo VIVO
245
+ let s = theta_planos.flatten_all()?.affine(0.5, 0.0)?;
246
+ let s2 = s.sqr()?;
247
+ let um_menos = s2.affine(-1.0, 1.0)?; // 1−s²
248
+ let um_mais = s2.affine(1.0, 1.0)?; // 1+s²
249
+ let c = um_menos.broadcast_div(&um_mais)?;
250
+ let t = s.affine(2.0, 0.0)?.broadcast_div(&um_mais)?;
251
+ // intercala [c_0, t_0, c_1, t_1, ...] → (1, 2m)
252
+ let par = Tensor::stack(&[&c, &t], 0)? // (2, m)
253
+ .transpose(0, 1)? // (m, 2) = [[c_0, t_0], ...]
254
+ .reshape((1, 2 * m))?;
255
+ // matriz de seleção S (2m, n²) CONSTANTE: linha 2k (c_k) marca as
256
+ // diagonais do bloco k; linha 2k+1 (t_k) marca (−acima, +abaixo)
257
+ // (convenção expm(+A): R_k = [[c, −t], [t, c]])
258
+ let mut sel = vec![0f32; 2 * m * n * n];
259
+ for k in 0..m {
260
+ let (a, b) = (2 * k, 2 * k + 1);
261
+ sel[2 * k * n * n + a * n + a] = 1.0;
262
+ sel[2 * k * n * n + b * n + b] = 1.0;
263
+ sel[(2 * k + 1) * n * n + a * n + b] = -1.0;
264
+ sel[(2 * k + 1) * n * n + b * n + a] = 1.0;
265
+ }
266
+ let s_mat = Tensor::from_vec(sel, (2 * m, n * n), &dev)?;
267
+ Ok(par.matmul(&s_mat)?.reshape((n, n))?)
268
+ }
269
+
270
  /// SmoothQuant (Xiao et al. 2022) — port do `SmoothQuantCompressor`.
271
  /// s_j = max|X_j|^α / max|W_j|^(1−α) via EMA; a migração compensada
272
  /// (W·diag(s), X/diag(s)) é EXATAMENTE neutra na saída (Teo F-6 — prova
 
723
  assert!(mx.is_finite() && mx > 0.0, "F-8: saída quantizada finita");
724
  Ok(())
725
  }
726
+
727
+ /// determinante por eliminação gaussiana em f64 (auxiliar dos testes).
728
+ fn det_f64(t: &Tensor) -> f64 {
729
+ let n = t.dims()[0];
730
+ let mut m: Vec<Vec<f64>> = t
731
+ .to_vec2::<f32>()
732
+ .unwrap()
733
+ .iter()
734
+ .map(|r| r.iter().map(|x| *x as f64).collect())
735
+ .collect();
736
+ let mut det = 1.0;
737
+ for col in 0..n {
738
+ let mut pivo = col;
739
+ for r in col + 1..n {
740
+ if m[r][col].abs() > m[pivo][col].abs() {
741
+ pivo = r;
742
+ }
743
+ }
744
+ if m[pivo][col].abs() < 1e-12 {
745
+ return 0.0;
746
+ }
747
+ if pivo != col {
748
+ m.swap(pivo, col);
749
+ det = -det;
750
+ }
751
+ det *= m[col][col];
752
+ for r in col + 1..n {
753
+ let f = m[r][col] / m[col][col];
754
+ for c in col..n {
755
+ m[r][c] -= f * m[col][c];
756
+ }
757
+ }
758
+ }
759
+ det
760
+ }
761
+
762
+ /// inversão Gauss–Jordan em f64 (auxiliar — oráculo do teste F-10).
763
+ fn inv_f64(t: &Tensor) -> Vec<Vec<f64>> {
764
+ let n = t.dims()[0];
765
+ let mut a: Vec<Vec<f64>> = t
766
+ .to_vec2::<f32>()
767
+ .unwrap()
768
+ .iter()
769
+ .map(|r| r.iter().map(|x| *x as f64).collect())
770
+ .collect();
771
+ let mut inv = vec![vec![0.0; n]; n];
772
+ for i in 0..n {
773
+ inv[i][i] = 1.0;
774
+ }
775
+ for col in 0..n {
776
+ let mut pivo = col;
777
+ for r in col + 1..n {
778
+ if a[r][col].abs() > a[pivo][col].abs() {
779
+ pivo = r;
780
+ }
781
+ }
782
+ a.swap(pivo, col);
783
+ inv.swap(pivo, col);
784
+ let p = a[col][col];
785
+ for c in 0..n {
786
+ a[col][c] /= p;
787
+ inv[col][c] /= p;
788
+ }
789
+ for r in 0..n {
790
+ if r != col && a[r][col].abs() > 1e-15 {
791
+ let f = a[r][col];
792
+ for c in 0..n {
793
+ a[r][c] -= f * a[col][c];
794
+ inv[r][c] -= f * inv[col][c];
795
+ }
796
+ }
797
+ }
798
+ }
799
+ inv
800
+ }
801
+
802
+ /// **F-9 (v0.8.0):** Cayley ∈ SO(n) — δ_ort ≤ 1e-6, det = +1, e
803
+ /// nascimento I bit-a-bit COM o grafo conectado a θ (gradiente flui —
804
+ /// propriedade que a forma fechada dá de graça, sem o atalho especial
805
+ /// do caminho polar). Perda assimétrica ⟨W, R⟩ para expor o sinal.
806
+ #[test]
807
+ fn f9_cayley_so_n_exato() -> Result<()> {
808
+ let dev = Device::Cpu;
809
+ let r = RotacaoSkew::nova(12, &dev)?;
810
+ let th: Vec<f32> = (0..12 * 11 / 2).map(|i| ((i as f32) * 0.07) % 0.5).collect();
811
+ r.theta.set(&Tensor::from_vec(th, (12 * 11 / 2,), &dev)?)?;
812
+ let rot = r.rotacao_cayley()?;
813
+ let delta = erro_ortogonalidade(&rot)?;
814
+ assert!(delta < 1e-6, "F-9: δ_ort = {delta} (esperado ≤ 1e-6)");
815
+ let d = det_f64(&rot);
816
+ assert!((d - 1.0).abs() < 1e-4, "F-9: det = {d} (deve ser +1)");
817
+ // nascimento: θ=0 ⇒ R = I bit-a-bit (α=1 exato ⇒ 1ª iteração encerra)
818
+ let r0 = RotacaoSkew::nova(8, &dev)?;
819
+ let ident = r0.rotacao_cayley()?;
820
+ let dif = (&ident - &Tensor::eye(8, DType::F32, &dev)?)?
821
+ .abs()?
822
+ .max_all()?
823
+ .to_scalar::<f32>()?;
824
+ assert_eq!(dif, 0.0, "F-9: θ=0 ⇒ R = I bit-a-bit");
825
+ // grafo VIVO no nascimento: ⟨W, R⟩ com W não-simétrico ⇒ grad ≠ 0
826
+ let mut rng2 = rng();
827
+ let w = randn(&mut rng2, &[8, 8], &dev)?;
828
+ let perda = (&w * &ident)?.sum_all()?;
829
+ let grads = perda.backward()?;
830
+ let g = grads
831
+ .get(&r0.theta)
832
+ .map(|t| t.flatten_all().unwrap().to_vec1::<f32>().unwrap())
833
+ .unwrap_or_default();
834
+ assert!(
835
+ g.iter().any(|v| v.abs() > 1e-9),
836
+ "F-9: gradiente flui a θ no nascimento (grafo conectado sem atalho)"
837
+ );
838
+ Ok(())
839
+ }
840
+
841
+ /// **F-10 (v0.8.0):** Newton-inverso converge no PIOR CASO do clamp
842
+ /// (todos os θ = 0,5) e o resultado confere com a inversão explícita
843
+ /// Gauss–Jordan em f64.
844
+ #[test]
845
+ fn f10_newton_inverso_pior_caso() -> Result<()> {
846
+ let dev = Device::Cpu;
847
+ let n = 16;
848
+ let r = RotacaoSkew::nova(n, &dev)?;
849
+ let th: Vec<f32> = vec![0.5f32; n * (n - 1) / 2];
850
+ r.theta.set(&Tensor::from_vec(th, (n * (n - 1) / 2,), &dev)?)?;
851
+ let rot = r.rotacao_cayley()?;
852
+ let delta = erro_ortogonalidade(&rot)?;
853
+ assert!(delta < 1e-6, "F-10: δ_ort pior caso = {delta}");
854
+ // oráculo explícito: R = (I + B)·(I − B)⁻¹ em f64 (convenção expm(+A))
855
+ let a = r.skew()?;
856
+ let av = a.to_vec2::<f32>()?;
857
+ let mut b = vec![vec![0.0f64; n]; n];
858
+ for i in 0..n {
859
+ for j in 0..n {
860
+ b[i][j] = 0.5 * av[i][j] as f64;
861
+ }
862
+ }
863
+ let bt = Tensor::from_vec(
864
+ b.iter().flatten().map(|x| *x as f32).collect::<Vec<f32>>(),
865
+ (n, n),
866
+ &dev,
867
+ )?;
868
+ let um_b = Tensor::eye(n, DType::F32, &dev)?.add(&bt)?;
869
+ let um_menos_b = Tensor::eye(n, DType::F32, &dev)?.sub(&bt)?;
870
+ let inv = inv_f64(&um_menos_b);
871
+ let inv_t = Tensor::from_vec(
872
+ inv.iter().flatten().map(|x| *x as f32).collect::<Vec<f32>>(),
873
+ (n, n),
874
+ &dev,
875
+ )?;
876
+ let explicito = um_b.matmul(&inv_t)?;
877
+ let dif = (&rot - &explicito)?.abs()?.max_all()?.to_scalar::<f32>()?;
878
+ assert!(dif < 1e-3, "F-10: max|R_newton − R_gaussjordan| = {dif}");
879
+ Ok(())
880
+ }
881
+
882
+ /// **F-11 (v0.8.0):** bloco 2×2 — ortogonal exata, bloque-diagonal
883
+ /// (zero fora dos planos), nascimento I bit-a-bit, gradiente flui.
884
+ #[test]
885
+ fn f11_cayley_bloco() -> Result<()> {
886
+ let dev = Device::Cpu;
887
+ let th = Tensor::from_vec(vec![0.3f32, -0.7, 1.2, 0.05], (4,), &dev)?;
888
+ let r = rotacao_cayley_bloco(&th)?;
889
+ assert_eq!(r.dims(), [8, 8], "F-11: 4 planos ⇒ 8×8");
890
+ let delta = erro_ortogonalidade(&r)?;
891
+ assert!(delta < 1e-6, "F-11: δ_ort = {delta}");
892
+ // estrutura bloque-diagonal: zero EXATO fora dos blocos 2×2
893
+ let rv = r.to_vec2::<f32>()?;
894
+ for i in 0..8 {
895
+ for j in 0..8 {
896
+ if (i / 2) != (j / 2) {
897
+ assert_eq!(rv[i][j], 0.0, "F-11: fora do bloco ({i},{j})");
898
+ }
899
+ }
900
+ }
901
+ // nascimento: θ=0 ⇒ R = I bit-a-bit
902
+ let th_var = Var::from_tensor(&Tensor::zeros((4,), DType::F32, &dev)?)?;
903
+ let rz = rotacao_cayley_bloco(th_var.as_tensor())?;
904
+ let dif = (&rz - &Tensor::eye(8, DType::F32, &dev)?)?
905
+ .abs()?
906
+ .max_all()?
907
+ .to_scalar::<f32>()?;
908
+ assert_eq!(dif, 0.0, "F-11: nascimento neutro bit-a-bit");
909
+ // grafo vivo: gradiente de ⟨W, R⟩ em θ flui
910
+ let mut rng2 = rng();
911
+ let w = randn(&mut rng2, &[8, 8], &dev)?;
912
+ let perda = (&w * &rz)?.sum_all()?;
913
+ let grads = perda.backward()?;
914
+ let g = grads
915
+ .get(&th_var)
916
+ .map(|t| t.flatten_all().unwrap().to_vec1::<f32>().unwrap())
917
+ .unwrap_or_default();
918
+ assert!(
919
+ g.iter().any(|v| v.abs() > 1e-9),
920
+ "F-11: gradiente flui a θ (grafo conectado)"
921
+ );
922
+ Ok(())
923
+ }
924
+
925
+ /// **F-12 (v0.8.0):** Cayley ≈ polar para θ pequeno (ambos ≈ expm(+A) —
926
+ /// com a convenção corrigida F-9(b): polar-série com erro O(θ⁴),
927
+ /// Cayley = expm(2·artanh(A/2)) = expm(+A − A³/12 + O(A⁵)); a distância
928
+ /// mútua é O(‖A‖³/12), verificada contra oráculo NumPy/SVD — r-33 §4.6).
929
+ #[test]
930
+ fn f12_cayley_vs_polar_teta_pequeno() -> Result<()> {
931
+ let dev = Device::Cpu;
932
+ let n = 8;
933
+ let r = RotacaoSkew::nova(n, &dev)?;
934
+ let th: Vec<f32> = (0..n * (n - 1) / 2).map(|i| 0.02 + 0.005 * i as f32).collect();
935
+ r.theta.set(&Tensor::from_vec(th, (n * (n - 1) / 2,), &dev)?)?;
936
+ let rp = r.rotacao_polar()?;
937
+ let rc = r.rotacao_cayley()?;
938
+ let dif = (&rp - &rc)?.sqr()?.sum_all()?.to_scalar::<f32>()?.sqrt();
939
+ let d_id = (&rp - &Tensor::eye(n, DType::F32, &dev)?)?
940
+ .sqr()?
941
+ .sum_all()?
942
+ .to_scalar::<f32>()?
943
+ .sqrt();
944
+ assert!(
945
+ dif < 2e-2,
946
+ "F-12: ‖R_polar − R_cayley‖_F = {dif} (θ pequeno deve coincidir)"
947
+ );
948
+ // honestidade: desvio O(‖A‖³/12) contra distância O(‖A‖) à identidade
949
+ let razao = dif / d_id.max(1e-9);
950
+ assert!(razao < 0.25, "F-12: razão desvio/distância = {razao}");
951
+ Ok(())
952
+ }
953
  }
src/lib.rs CHANGED
@@ -45,7 +45,7 @@ pub mod build_info {
45
 
46
  #[pyfunction]
47
  fn versao() -> &'static str {
48
- "0.7.0 (KHTST_RUST — FNH com gate de punição + árvore de pesos indexada c/ arestas duplas + quebra-cabeça do Pensamento + NLP/NLG + decodificador especulativo + auto-ajuste SOM limitado + correções v070-1/2/3)"
49
  }
50
 
51
  #[pyfunction]
@@ -1967,6 +1967,201 @@ fn demo_autoajuste_som(py: Python) -> PyResult<PyObject> {
1967
  Ok(pythonize::pythonize(py, &d_)?.into())
1968
  }
1969
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1970
  #[pymodule]
1971
  fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
1972
  pyo3_log::init();
@@ -2011,6 +2206,8 @@ fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
2011
  m.add_function(wrap_pyfunction!(quebra_cabeca_real, m)?)?;
2012
  m.add_function(wrap_pyfunction!(demo_nlp_nlg, m)?)?;
2013
  m.add_function(wrap_pyfunction!(demo_autoajuste_som, m)?)?;
 
 
2014
  m.add_class::<BpePy>()?;
2015
  m.add_class::<ConfigPy>()?;
2016
  m.add_class::<PonteDuplaPy>()?;
 
45
 
46
  #[pyfunction]
47
  fn versao() -> &'static str {
48
+ "khtst-rust-0.8.0"
49
  }
50
 
51
  #[pyfunction]
 
1967
  Ok(pythonize::pythonize(py, &d_)?.into())
1968
  }
1969
 
1970
+ /// **v0.8.0 — análise/prova/teste das 4 propostas Qwen3.8** (r-33): mede
1971
+ /// custo e exatidão de (1) Newton analítico vs golden-section (verificador),
1972
+ /// (2) Cayley vs polar (FNH), (3) árvore pós-ordem iterativa vs recursiva,
1973
+ /// (4) MoE vetorizada vs referência (valores + gradiente restaurado — bug
1974
+ /// v080-1), e re-executa a ablação JEPA de 3 braços com a correção.
1975
+ #[pyfunction]
1976
+ fn demo_qwen_v080(py: Python) -> PyResult<PyObject> {
1977
+ use candle_core::{Device, Tensor};
1978
+ use std::time::Instant;
1979
+ let dev = Device::Cpu;
1980
+
1981
+ // ---------- (1) Newton vs golden-section (verificador) ----------
1982
+ let mut rng = crate::rng::RngDeterministico::novo(808);
1983
+ let (n, k) = (1024usize, 32usize);
1984
+ let mut lv = Vec::with_capacity(n * k);
1985
+ let mut av = Vec::with_capacity(n);
1986
+ for _ in 0..n {
1987
+ let alvo = rng.inteiro(k) as u32;
1988
+ av.push(alvo);
1989
+ for j in 0..k {
1990
+ let base = if j as u32 == alvo { 3.0 } else { 0.0 };
1991
+ lv.push(base + rng.normal(0.0, 1.0) as f32);
1992
+ }
1993
+ }
1994
+ let lt = Tensor::from_vec(lv, (n, k), &dev).unwrap();
1995
+ let at = Tensor::from_vec(av, (n,), &dev).unwrap();
1996
+ let t0 = Instant::now();
1997
+ let (t_g, _, nll_g) = verificador::calibrar_temperatura(&lt, &at).unwrap();
1998
+ let tempo_golden = t0.elapsed().as_secs_f64();
1999
+ let t0 = Instant::now();
2000
+ let (t_n, _, nll_n, n_aval) =
2001
+ verificador::calibrar_temperatura_newton(&lt, &at, 40).unwrap();
2002
+ let tempo_newton = t0.elapsed().as_secs_f64();
2003
+
2004
+ // ---------- (2) Cayley vs polar (FNH) ----------
2005
+ let n_rot = 16usize;
2006
+ let r = fnh::RotacaoSkew::nova(n_rot, &dev).unwrap();
2007
+ let th: Vec<f32> = (0..n_rot * (n_rot - 1) / 2)
2008
+ .map(|i| ((i as f32) * 0.11) % 0.5)
2009
+ .collect();
2010
+ r.theta
2011
+ .set(&Tensor::from_vec(th, (n_rot * (n_rot - 1) / 2,), &dev).unwrap())
2012
+ .unwrap();
2013
+ let rep = 25usize;
2014
+ let t0 = Instant::now();
2015
+ let mut delta_polar = 0.0;
2016
+ for _ in 0..rep {
2017
+ delta_polar = crate::stiefel::erro_ortogonalidade(&r.rotacao_polar().unwrap()).unwrap();
2018
+ }
2019
+ let tempo_polar = t0.elapsed().as_secs_f64() / rep as f64;
2020
+ let t0 = Instant::now();
2021
+ let mut delta_cayley = 0.0;
2022
+ for _ in 0..rep {
2023
+ delta_cayley = crate::stiefel::erro_ortogonalidade(&r.rotacao_cayley().unwrap()).unwrap();
2024
+ }
2025
+ let tempo_cayley = t0.elapsed().as_secs_f64() / rep as f64;
2026
+
2027
+ // ---------- (3) árvore iterativa vs recursiva ----------
2028
+ let mut arv = arvore_pesos::ArvorePesos::nova(&std::collections::HashMap::new());
2029
+ let mut pai = 1usize;
2030
+ for i in 0..20_000u32 {
2031
+ pai = arv.anexar_filho(pai, &format!("c{i}"), 1);
2032
+ }
2033
+ arv.nos[pai].metricas.insert("m".into(), 1.0);
2034
+ let t0 = Instant::now();
2035
+ let total_it = arv.agregar_fundo_topo("m");
2036
+ let tempo_arv_it = t0.elapsed().as_secs_f64();
2037
+ // repõe TODAS as métricas locais (a agregação sobrescreveu com os
2038
+ // agregados) — só a folha tem valor; o resto volta a 0 — e roda a
2039
+ // recursiva (profundidade 20k ok na thread principal do Python)
2040
+ for no in arv.nos.iter_mut() {
2041
+ no.metricas.insert("m".into(), 0.0);
2042
+ }
2043
+ arv.nos[pai].metricas.insert("m".into(), 1.0);
2044
+ let t0 = Instant::now();
2045
+ let total_rec = arv.agregar_fundo_topo_rec("m");
2046
+ let tempo_arv_rec = t0.elapsed().as_secs_f64();
2047
+
2048
+ // ---------- (4) MoE vetorizada: valores + gradiente + custo ----------
2049
+ let (nm, dm, em) = (256usize, 32usize, 8usize);
2050
+ let xv: Vec<f32> = (0..nm * dm).map(|_| rng.normal(0.0, 1.0) as f32).collect();
2051
+ let x = Tensor::from_vec(xv, (nm, dm), &dev).unwrap();
2052
+ let moe = moe_redirecionavel::MoeRedirecionavel::novo(dm, em, 2, nm, &dev).unwrap();
2053
+ let s_ref = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
2054
+ let s_vet = moe.fwd_vetorizado(&x, moe_redirecionavel::Papel::Codificador).unwrap();
2055
+ let dif_moe = (&s_ref.saida - &s_vet.saida)
2056
+ .unwrap()
2057
+ .abs()
2058
+ .unwrap()
2059
+ .max_all()
2060
+ .unwrap()
2061
+ .to_scalar::<f32>()
2062
+ .unwrap() as f64;
2063
+ let rep_moe = 10usize;
2064
+ let t0 = Instant::now();
2065
+ for _ in 0..rep_moe {
2066
+ let _ = moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap();
2067
+ }
2068
+ let tempo_moe_ref = t0.elapsed().as_secs_f64() / rep_moe as f64;
2069
+ let t0 = Instant::now();
2070
+ for _ in 0..rep_moe {
2071
+ let _ = moe.fwd_vetorizado(&x, moe_redirecionavel::Papel::Codificador).unwrap();
2072
+ }
2073
+ let tempo_moe_vet = t0.elapsed().as_secs_f64() / rep_moe as f64;
2074
+ // normas de gradiente (bug v080-1: fwd = 0; vetorizada > 0)
2075
+ let norma = |vet: bool| -> (f64, f64) {
2076
+ let s = if vet {
2077
+ moe.fwd_vetorizado(&x, moe_redirecionavel::Papel::Codificador).unwrap()
2078
+ } else {
2079
+ moe.fwd(&x, moe_redirecionavel::Papel::Codificador).unwrap()
2080
+ };
2081
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
2082
+ let gs = perda.backward().unwrap();
2083
+ let mut nr = 0.0;
2084
+ for v in moe.roteador.vars() {
2085
+ if let Some(g) = gs.get(&v) {
2086
+ nr += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
2087
+ }
2088
+ }
2089
+ let mut ne = 0.0;
2090
+ for e in &moe.especialistas {
2091
+ for v in e.vars() {
2092
+ if let Some(g) = gs.get(&v) {
2093
+ ne += g.sqr().unwrap().sum_all().unwrap().to_scalar::<f32>().unwrap() as f64;
2094
+ }
2095
+ }
2096
+ }
2097
+ (nr, ne)
2098
+ };
2099
+ let (grad_ref_r, grad_ref_e) = norma(false);
2100
+ let (grad_vet_r, grad_vet_e) = norma(true);
2101
+
2102
+ // ---------- (5) re-ablação JEPA 3 braços (gradiente restaurado) ----------
2103
+ let xv: Vec<f32> = (0..64 * 12).map(|_| rng.normal(0.0, 1.0) as f32).collect();
2104
+ let xj = Tensor::from_vec(xv, (64, 12), &dev).unwrap();
2105
+ let mut ablac = serde_json::Map::new();
2106
+ for modo in ["pequena", "densa", "moe"] {
2107
+ let rj = moe_redirecionavel::treinar_jepa_com_moe(&xj, modo, 120, 0.5, 42).unwrap();
2108
+ ablac.insert(
2109
+ modo.into(),
2110
+ serde_json::json!({
2111
+ "perda_inicial": rj.perda_pred_inicial,
2112
+ "perda_final": rj.perda_pred_final,
2113
+ "queda_pct": 100.0 * (1.0 - rj.perda_pred_final / rj.perda_pred_inicial.max(1e-9)),
2114
+ }),
2115
+ );
2116
+ }
2117
+
2118
+ let d_ = serde_json::json!({
2119
+ "versao": "qwen-v080-1.0",
2120
+ "newton_vs_golden": {
2121
+ "t_golden": t_g, "t_newton": t_n, "dif_t": (t_g - t_n).abs(),
2122
+ "nll_golden": nll_g, "nll_newton": nll_n,
2123
+ "nll_dif": (nll_g - nll_n).abs(),
2124
+ "n_aval_newton": n_aval,
2125
+ "tempo_golden_s": tempo_golden, "tempo_newton_s": tempo_newton,
2126
+ "speedup": tempo_golden / tempo_newton.max(1e-9),
2127
+ "gates": {
2128
+ "mesmo_t": (t_g - t_n).abs() < 1e-3,
2129
+ "nll_nao_piora": nll_n <= nll_g + 1e-4,
2130
+ "menos_avaliacoes": n_aval < 36,
2131
+ },
2132
+ },
2133
+ "cayley_vs_polar": {
2134
+ "delta_ort_polar": delta_polar, "delta_ort_cayley": delta_cayley,
2135
+ "tempo_polar_s": tempo_polar, "tempo_cayley_s": tempo_cayley,
2136
+ "speedup": tempo_polar / tempo_cayley.max(1e-9),
2137
+ "gates": {
2138
+ "ambos_exatos": delta_polar < 1e-6 && delta_cayley < 1e-6,
2139
+ "cayley_mais_rapido": tempo_cayley < tempo_polar,
2140
+ },
2141
+ },
2142
+ "arvore_iter_vs_rec": {
2143
+ "n_nos": arv.n_nos(),
2144
+ "total_iter": total_it, "total_rec": total_rec,
2145
+ "bit_a_bit": total_it.to_bits() == total_rec.to_bits(),
2146
+ "tempo_iter_s": tempo_arv_it, "tempo_rec_s": tempo_arv_rec,
2147
+ "gates": { "iguais_bit_a_bit": total_it == total_rec },
2148
+ },
2149
+ "moe_vetorizada": {
2150
+ "max_dif_valores": dif_moe,
2151
+ "tempo_ref_s": tempo_moe_ref, "tempo_vet_s": tempo_moe_vet,
2152
+ "grad_roteador_ref": grad_ref_r, "grad_especialistas_ref": grad_ref_e,
2153
+ "grad_roteador_vet": grad_vet_r, "grad_especialistas_vet": grad_vet_e,
2154
+ "gates": {
2155
+ "valores_iguais": dif_moe < 1e-4,
2156
+ "bug_v080_1_confirmado": grad_ref_r == 0.0 && grad_ref_e == 0.0,
2157
+ "gradiente_restaurado": grad_vet_r > 0.0 && grad_vet_e > 0.0,
2158
+ },
2159
+ },
2160
+ "ablacao_jepa_3_bracos_v080": ablac,
2161
+ });
2162
+ Ok(pythonize::pythonize(py, &d_)?.into())
2163
+ }
2164
+
2165
  #[pymodule]
2166
  fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
2167
  pyo3_log::init();
 
2206
  m.add_function(wrap_pyfunction!(quebra_cabeca_real, m)?)?;
2207
  m.add_function(wrap_pyfunction!(demo_nlp_nlg, m)?)?;
2208
  m.add_function(wrap_pyfunction!(demo_autoajuste_som, m)?)?;
2209
+ // v0.8.0 — análise/prova/teste das propostas Qwen3.8
2210
+ m.add_function(wrap_pyfunction!(demo_qwen_v080, m)?)?;
2211
  m.add_class::<BpePy>()?;
2212
  m.add_class::<ConfigPy>()?;
2213
  m.add_class::<PonteDuplaPy>()?;
src/moe_redirecionavel.rs CHANGED
@@ -189,6 +189,13 @@ impl MoeRedirecionavel {
189
 
190
  /// Passada por papel: atenção própria → roteamento top-k DENTRO do
191
  /// subconjunto de especialistas do papel → capacidade → soma ponderada.
 
 
 
 
 
 
 
192
  pub fn fwd(&self, x: &Tensor, papel: Papel) -> Result<SaidaMoeR> {
193
  let y = self.atencao.fwd(x)?; // (n, d) — atenção própria independente
194
  let n = y.dim(0)?;
@@ -196,7 +203,6 @@ impl MoeRedirecionavel {
196
  let dev = y.device();
197
  let do_papel = self.indices_do_papel(papel);
198
  let e_local = do_papel.len();
199
- let mut carga = vec![0usize; self.especialistas.len()];
200
  let mut descartados = 0usize;
201
  let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
202
  let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
@@ -225,34 +231,18 @@ impl MoeRedirecionavel {
225
  }
226
  })
227
  .collect();
228
- // roteamento top-k dentro do papel, com capacidade por especialista
229
- let mut usados: Vec<usize> = Vec::new();
230
- for (local_e, &ei) in do_papel.iter().enumerate() {
231
- let _ = local_e;
232
- // candidatos ordenados por prob do especialista ei
233
- let mut cand: Vec<(usize, f32)> = (0..n).map(|i| (i, probs[i][ei])).collect();
234
- cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
235
- let mut escolhidos: Vec<usize> = Vec::new();
236
- for (i, _) in cand {
237
- if carga[ei] >= self.capacidade {
238
- break;
239
- }
240
- // ei ∈ top-k(papel) do token i?
241
- let mut ordem: Vec<(f32, usize)> = do_papel
242
- .iter()
243
- .map(|&g| (probs[i][g], g))
244
- .collect();
245
- ordem.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
246
- let top: Vec<usize> = ordem.iter().rev().take(self.n_top).map(|(_, g)| *g).collect();
247
- if top.contains(&ei) {
248
- escolhidos.push(i);
249
- carga[ei] += 1;
250
- }
251
- }
252
- if escolhidos.is_empty() {
253
- continue;
254
- }
255
- usados.push(ei);
256
  let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
257
  let n_e = idx.len();
258
  let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
@@ -316,6 +306,91 @@ impl MoeRedirecionavel {
316
  Ok(SaidaMoeR { saida, perda_aux, carga, descartados, usados })
317
  }
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  /// **M-R3:** passadas Codificador e Decodificador em DUAS THREADS
320
  /// (DAGs independentes — especialistas disjuntos por papel). Devolve
321
  /// (enc, dec, tempo_paralelo_s, tempo_sequencial_s, speedup).
@@ -354,6 +429,49 @@ impl MoeRedirecionavel {
354
  // M-R5 — alocação water-filling por QE
355
  // ---------------------------------------------------------------------------
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  /// Aloca `capacidade_total` especialistas entre células com QE `qes`
358
  /// proporcionalmente ao QE (Cauchy–Schwarz: minimiza Σ q_i²/c_i).
359
  /// Devolve c_i ≥ 1 com Σ c_i = capacidade_total (se capacidade ≥ células).
@@ -486,8 +604,10 @@ pub fn treinar_jepa_com_moe(
486
  z.sqr()?.mean_all()?, 0))
487
  }
488
  Braco::Moe(m) => {
489
- let s_enc = m.fwd(x, Papel::Codificador)?;
490
- let s_dec = m.fwd(&s_enc.saida, Papel::Decodificador)?;
 
 
491
  let dif = (&s_dec.saida - &alvo)?;
492
  let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
493
  // perda total = preditiva + aux balanceamento (ambos papéis)
@@ -559,7 +679,7 @@ pub fn treinar_jepa_com_moe(
559
  };
560
  }
561
  if let Braco::Moe(m) = &braco {
562
- let s = m.fwd(&contexto, Papel::Codificador)?;
563
  descartados = s.descartados;
564
  }
565
  Ok(RelatorioJepaMoe {
@@ -933,6 +1053,131 @@ mod testes {
933
  let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
934
  assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
935
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
  }
937
 
938
  #[cfg(test)]
 
189
 
190
  /// Passada por papel: atenção própria → roteamento top-k DENTRO do
191
  /// subconjunto de especialistas do papel → capacidade → soma ponderada.
192
+ ///
193
+ /// ⚠️ **v0.8.0 — bug v080-1 catalogado (r-33 §5):** esta versão
194
+ /// materializa os valores com `to_vec2`/`from_vec` no acúmulo, o que
195
+ /// SECCIONA o grafo de autograd — especialistas e roteador NÃO recebem
196
+ /// gradiente por esta saída (só a atenção própria treina). Mantida como
197
+ /// referência de VALORES (o teste M-R6 verifica a equivalência numérica
198
+ /// com a versão vetorizada); para TREINO use `fwd_vetorizado`.
199
  pub fn fwd(&self, x: &Tensor, papel: Papel) -> Result<SaidaMoeR> {
200
  let y = self.atencao.fwd(x)?; // (n, d) — atenção própria independente
201
  let n = y.dim(0)?;
 
203
  let dev = y.device();
204
  let do_papel = self.indices_do_papel(papel);
205
  let e_local = do_papel.len();
 
206
  let mut descartados = 0usize;
207
  let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
208
  let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
 
231
  }
232
  })
233
  .collect();
234
+ // seleção top-k com capacidade (código extraído — MESMA semântica
235
+ // de desempate da v0.6.0, compartilhada com fwd_vetorizado)
236
+ let (designacao, carga) = selecionar_com_capacidade(
237
+ &probs,
238
+ &do_papel,
239
+ n,
240
+ self.n_top,
241
+ self.capacidade,
242
+ self.especialistas.len(),
243
+ );
244
+ let usados: Vec<usize> = designacao.iter().map(|(ei, _)| *ei).collect();
245
+ for (ei, escolhidos) in designacao {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
247
  let n_e = idx.len();
248
  let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
 
306
  Ok(SaidaMoeR { saida, perda_aux, carga, descartados, usados })
307
  }
308
 
309
+ /// **M-R6 (v0.8.0):** passada VETORIZADA — roteamento, acúmulo e perda
310
+ /// auxiliar em OPERAÇÕES DE TENSOR rastreadas (proposta Qwen-3.8 nº 4
311
+ /// IMPLEMENTADA de verdade: o esboço original terminava em
312
+ /// `unimplemented!()`; a fórmula da perda auxiliar `E·Σ f_e·p̄_e`
313
+ /// estava CORRETA — Switch Transformer — e é mantida).
314
+ ///
315
+ /// **Correção do bug v080-1:** a acumulação usa `index_add` (op
316
+ /// rastreada pelo autograd do candle — `IndexAdd` em op.rs) em vez do
317
+ /// round-trip `to_vec2`/`from_vec`; os pesos de roteamento saem do
318
+ /// PRÓPRIO tensor `probs` (vivo até o roteador); a perda auxiliar usa
319
+ /// `p̄` do tensor vivo com `f` constante (padrão Switch: argmax é
320
+ /// não-diferenciável, a média de probs carrega o gradiente). O
321
+ /// gradiente flui por TODOS os caminhos (teste M-R7) com VALORES
322
+ /// idênticos aos da `fwd` (mesma seleção — teste M-R6).
323
+ pub fn fwd_vetorizado(&self, x: &Tensor, papel: Papel) -> Result<SaidaMoeR> {
324
+ let y = self.atencao.fwd(x)?; // (n, d)
325
+ let n = y.dim(0)?;
326
+ let d = y.dim(1)?;
327
+ let dev = y.device();
328
+ let e_total = self.especialistas.len();
329
+ let do_papel = self.indices_do_papel(papel);
330
+ let e_local = do_papel.len();
331
+ // 1) máscara do papel + softmax EM TENSOR — grafo VIVO até o roteador
332
+ let mut mascara_v = vec![f32::NEG_INFINITY; e_total];
333
+ for &e in &do_papel {
334
+ mascara_v[e] = 0.0;
335
+ }
336
+ let mascara = Tensor::from_vec(mascara_v, (1, e_total), &dev)?;
337
+ let logits = self.roteador.fwd(&y)?;
338
+ let mascarados = logits.broadcast_add(&mascara)?;
339
+ let probs = candle_nn::ops::softmax(&mascarados, candle_core::D::Minus1)?; // (n, E) VIVO
340
+ // 2) seleção discreta (única materialização — o roteamento duro é
341
+ // não-diferenciável por natureza; os PESOS ficam vivos no passo 3)
342
+ let probs_v = probs.to_vec2::<f32>()?;
343
+ let (designacao, carga) =
344
+ selecionar_com_capacidade(&probs_v, &do_papel, n, self.n_top, self.capacidade, e_total);
345
+ let usados: Vec<usize> = designacao.iter().map(|(ei, _)| *ei).collect();
346
+ // 3) acúmulo VIVO: index_add é rastreada — especialistas recebem
347
+ // gradiente pelas contribuições, e o roteador pelos pesos w
348
+ let mut saida = Tensor::zeros((n, d), candle_core::DType::F32, &dev)?;
349
+ let mut peso_total = Tensor::zeros((n, 1), candle_core::DType::F32, &dev)?;
350
+ for (ei, escolhidos) in &designacao {
351
+ let idx: Vec<u32> = escolhidos.iter().map(|&i| i as u32).collect();
352
+ let n_e = idx.len();
353
+ let idx_t = Tensor::from_vec(idx, (n_e,), &dev)?;
354
+ let tile = y.index_select(&idx_t, 0)?; // VIVO
355
+ let y_e = self.especialistas[*ei].fwd(&tile)?; // VIVO (gradiente!)
356
+ // w = probs[i][ei] VIVO: gradiente flui ao roteador
357
+ let linhas = probs.index_select(&idx_t, 0)?; // (n_e, E) VIVO
358
+ let w_t = linhas.narrow(1, *ei, 1)?.contiguous()?; // (n_e, 1) VIVO
359
+ let contrib = y_e.broadcast_mul(&w_t)?; // (n_e, d) VIVO
360
+ saida = saida.index_add(&idx_t, &contrib, 0)?; // VIVO (M-R6)
361
+ peso_total = peso_total.index_add(&idx_t, &w_t, 0)?; // VIVO
362
+ }
363
+ // 4) normalização idêntica à fwd (clamp 1e-6 nos descartados)
364
+ let pt_v = peso_total.to_vec2::<f32>()?; // estatística apenas
365
+ let mut descartados = 0usize;
366
+ for i in 0..n {
367
+ if pt_v[i][0] == 0.0 {
368
+ descartados += 1;
369
+ }
370
+ }
371
+ let denom = peso_total.clamp(1e-6, f64::INFINITY)?;
372
+ let saida = saida.broadcast_div(&denom)?;
373
+ let saida = (&y + &saida)?; // residual — como a fwd
374
+ // 5) perda auxiliar EM TENSOR (fórmula M-R1/Switch: e_local·Σ f_e·p̄_e)
375
+ let cols: Vec<u32> = do_papel.iter().map(|&e| e as u32).collect();
376
+ let cols_t = Tensor::from_vec(cols, (e_local.max(1),), &dev)?;
377
+ let probs_local = probs.index_select(&cols_t, 1)?; // (n, e_local) VIVO
378
+ let argmax_v = probs_local.argmax(candle_core::D::Minus1)?.to_vec1::<u32>()?;
379
+ let mut f = vec![0f32; e_local.max(1)];
380
+ for &a in &argmax_v {
381
+ f[a as usize] += 1.0;
382
+ }
383
+ if n > 0 {
384
+ for v in f.iter_mut() {
385
+ *v /= n as f32;
386
+ }
387
+ }
388
+ let f_t = Tensor::from_vec(f, (e_local.max(1),), &dev)?;
389
+ let p_barra = probs_local.mean_keepdim(0)?.flatten_all()?; // (e_local,) VIVO
390
+ let perda_aux = ((&f_t * &p_barra)?.sum_all()? * (e_local.max(1) as f64))?;
391
+ Ok(SaidaMoeR { saida, perda_aux, carga, descartados, usados })
392
+ }
393
+
394
  /// **M-R3:** passadas Codificador e Decodificador em DUAS THREADS
395
  /// (DAGs independentes — especialistas disjuntos por papel). Devolve
396
  /// (enc, dec, tempo_paralelo_s, tempo_sequencial_s, speedup).
 
429
  // M-R5 — alocação water-filling por QE
430
  // ---------------------------------------------------------------------------
431
 
432
+ /// Seleção top-k com capacidade por especialista (v0.8.0 — extraída
433
+ /// VERBATIM do loop da `fwd` v0.6.0: mesma ordem de candidatos, mesmos
434
+ /// desempates estáveis de `sort_by`). Compartilhada por `fwd` e
435
+ /// `fwd_vetorizado` ⇒ as duas atribuem EXATAMENTE os mesmos tokens aos
436
+ /// mesmos especialistas (base do teste M-R6 — equivalência de valores).
437
+ /// Devolve (designação por especialista, carga global por especialista).
438
+ fn selecionar_com_capacidade(
439
+ probs: &[Vec<f32>],
440
+ do_papel: &[usize],
441
+ n: usize,
442
+ n_top: usize,
443
+ capacidade: usize,
444
+ n_especialistas: usize,
445
+ ) -> (Vec<(usize, Vec<usize>)>, Vec<usize>) {
446
+ let mut carga = vec![0usize; n_especialistas];
447
+ let mut designacao: Vec<(usize, Vec<usize>)> = Vec::new();
448
+ for &ei in do_papel.iter() {
449
+ // candidatos ordenados por prob do especialista ei (desc)
450
+ let mut cand: Vec<(usize, f32)> = (0..n).map(|i| (i, probs[i][ei])).collect();
451
+ cand.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
452
+ let mut escolhidos: Vec<usize> = Vec::new();
453
+ for (i, _) in cand {
454
+ if carga[ei] >= capacidade {
455
+ break;
456
+ }
457
+ // ei está no top-k(papel) do token i?
458
+ let mut ordem: Vec<(f32, usize)> =
459
+ do_papel.iter().map(|&g| (probs[i][g], g)).collect();
460
+ ordem.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
461
+ let top: Vec<usize> = ordem.iter().rev().take(n_top).map(|(_, g)| *g).collect();
462
+ if top.contains(&ei) {
463
+ escolhidos.push(i);
464
+ carga[ei] += 1;
465
+ }
466
+ }
467
+ if escolhidos.is_empty() {
468
+ continue;
469
+ }
470
+ designacao.push((ei, escolhidos));
471
+ }
472
+ (designacao, carga)
473
+ }
474
+
475
  /// Aloca `capacidade_total` especialistas entre células com QE `qes`
476
  /// proporcionalmente ao QE (Cauchy–Schwarz: minimiza Σ q_i²/c_i).
477
  /// Devolve c_i ≥ 1 com Σ c_i = capacidade_total (se capacidade ≥ células).
 
604
  z.sqr()?.mean_all()?, 0))
605
  }
606
  Braco::Moe(m) => {
607
+ // v0.8.0 (bug v080-1): fwd_vetorizado — gradiente flui aos
608
+ // especialistas e ao roteador (a fwd seccionava o grafo)
609
+ let s_enc = m.fwd_vetorizado(x, Papel::Codificador)?;
610
+ let s_dec = m.fwd_vetorizado(&s_enc.saida, Papel::Decodificador)?;
611
  let dif = (&s_dec.saida - &alvo)?;
612
  let lp = (dif.sqr()?.sum_all()? / (n as f64 * d as f64))?;
613
  // perda total = preditiva + aux balanceamento (ambos papéis)
 
679
  };
680
  }
681
  if let Braco::Moe(m) = &braco {
682
+ let s = m.fwd_vetorizado(&contexto, Papel::Codificador)?;
683
  descartados = s.descartados;
684
  }
685
  Ok(RelatorioJepaMoe {
 
1053
  let r = treinar_jepa_com_moe(&x, "moe", 60, 0.5, 42).unwrap();
1054
  assert!(r.perda_pred_final < r.perda_pred_inicial, "perda preditiva cai");
1055
  }
1056
+
1057
+ /// **M-R6 (v0.8.0):** equivalência de VALORES entre `fwd` e
1058
+ /// `fwd_vetorizado` — mesma seleção (auxiliar compartilhado), mesma
1059
+ /// saída, mesma perda auxiliar, mesma carga/descartados.
1060
+ #[test]
1061
+ fn mr6_equivalencia_de_valores() {
1062
+ let dev = Device::Cpu;
1063
+ for (n, d, e, k, cap, seed) in [
1064
+ (64usize, 16usize, 6usize, 2usize, 64usize, 3u64),
1065
+ (96, 24, 8, 2, 96, 5),
1066
+ (48, 12, 4, 1, 24, 9), // capacidade apertada: força descartados
1067
+ ] {
1068
+ let x = dados_sinteticos(n, d, seed);
1069
+ let moe = MoeRedirecionavel::novo(d, e, k, cap, &dev).unwrap();
1070
+ let s_ref = moe.fwd(&x, Papel::Codificador).unwrap();
1071
+ let s_vet = moe.fwd_vetorizado(&x, Papel::Codificador).unwrap();
1072
+ assert_eq!(s_ref.carga, s_vet.carga, "M-R6: mesma carga (n={n})");
1073
+ assert_eq!(s_ref.usados, s_vet.usados, "M-R6: mesmos usados");
1074
+ assert_eq!(s_ref.descartados, s_vet.descartados, "M-R6: mesmos descartados");
1075
+ let dif = (&s_ref.saida - &s_vet.saida).unwrap()
1076
+ .abs().unwrap().max_all().unwrap().to_scalar::<f32>().unwrap();
1077
+ assert!(dif < 1e-4, "M-R6: max|Δsaída| = {dif} (n={n}, cap={cap})");
1078
+ let pa = s_ref.perda_aux.to_scalar::<f32>().unwrap();
1079
+ let pb = s_vet.perda_aux.to_scalar::<f32>().unwrap();
1080
+ assert!(
1081
+ (pa - pb).abs() < 1e-4,
1082
+ "M-R6: perda auxiliar {pa:.6} vs {pb:.6} (n={n})"
1083
+ );
1084
+ }
1085
+ }
1086
+
1087
+ /// **M-R7 (v0.8.0):** restauração do fluxo de gradiente — bug v080-1.
1088
+ /// Com a `fwd` (referência de valores) os especialistas e o roteador
1089
+ /// NÃO recebem gradiente (grafo seccionado por to_vec2/from_vec); com
1090
+ /// a `fwd_vetorizado` TODOS recebem (index_add/index_select vivos).
1091
+ #[test]
1092
+ fn mr7_restauracao_do_gradiente() {
1093
+ let dev = Device::Cpu;
1094
+ let x = dados_sinteticos(48, 12, 13);
1095
+ let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
1096
+ let norma_grads = |vetorizada: bool| -> (f64, f64) {
1097
+ let s = if vetorizada {
1098
+ moe.fwd_vetorizado(&x, Papel::Codificador).unwrap()
1099
+ } else {
1100
+ moe.fwd(&x, Papel::Codificador).unwrap()
1101
+ };
1102
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
1103
+ let grads = perda.backward().unwrap();
1104
+ let mut n_roteador = 0.0f64;
1105
+ for v in moe.roteador.vars() {
1106
+ if let Some(g) = grads.get(&v) {
1107
+ n_roteador += g.sqr().unwrap().sum_all().unwrap()
1108
+ .to_scalar::<f32>().unwrap() as f64;
1109
+ }
1110
+ }
1111
+ let mut n_experts = 0.0f64;
1112
+ for e in &moe.especialistas {
1113
+ for v in e.vars() {
1114
+ if let Some(g) = grads.get(&v) {
1115
+ n_experts += g.sqr().unwrap().sum_all().unwrap()
1116
+ .to_scalar::<f32>().unwrap() as f64;
1117
+ }
1118
+ }
1119
+ }
1120
+ (n_roteador, n_experts)
1121
+ };
1122
+ // (a) o bug v080-1 é REAL: na fwd, roteador e especialistas têm
1123
+ // gradiente ZERO (grafo seccionado) — documentado, não corrigido na
1124
+ // fwd (referência de valores preservada por decisão de projeto)
1125
+ let (r_ref, e_ref) = norma_grads(false);
1126
+ assert!(
1127
+ r_ref == 0.0 && e_ref == 0.0,
1128
+ "M-R7: bug v080-1 documentado — fwd secciona o grafo (roteador {r_ref}, experts {e_ref})"
1129
+ );
1130
+ // (b) a correção: na vetorizada, TODOS recebem gradiente
1131
+ let (r_vet, e_vet) = norma_grads(true);
1132
+ assert!(
1133
+ r_vet > 0.0,
1134
+ "M-R7: roteador recebe gradiente na vetorizada ({r_vet})"
1135
+ );
1136
+ assert!(
1137
+ e_vet > 0.0,
1138
+ "M-R7: especialistas recebem gradiente na vetorizada ({e_vet})"
1139
+ );
1140
+ }
1141
+
1142
+ /// **M-R8 (v0.8.0):** fim-a-fim — com a vetorizada, o treino REALMENTE
1143
+ /// atualiza os pesos dos especialistas (regressão do bug v080-1: antes
1144
+ /// só a atenção própria treinava no braço MoE).
1145
+ #[test]
1146
+ fn mr8_treino_atualiza_especialistas() {
1147
+ let dev = Device::Cpu;
1148
+ let x = dados_sinteticos(48, 12, 17);
1149
+ let moe = MoeRedirecionavel::novo(12, 6, 2, 48, &dev).unwrap();
1150
+ // snapshot com dados PRÓPRIOS (to_vec1) — detach() compartilha o
1151
+ // storage e o set() do otimizador sobrescreveria a "cópia"
1152
+ let snapshot: Vec<Vec<f32>> = moe
1153
+ .especialistas
1154
+ .iter()
1155
+ .map(|e| {
1156
+ e.vars()[0].as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap()
1157
+ })
1158
+ .collect();
1159
+ // treino curto com gradiente fluindo
1160
+ let vars = moe.vars();
1161
+ let mut opt = crate::treino::AdamWManual::novo(vars.clone(), 3e-3, 0.0);
1162
+ for _ in 0..5 {
1163
+ let s = moe.fwd_vetorizado(&x, Papel::Codificador).unwrap();
1164
+ let perda = s.saida.sqr().unwrap().sum_all().unwrap();
1165
+ let grads = crate::treino::grads_achatados(&perda, &vars).unwrap();
1166
+ opt.passo(grads);
1167
+ }
1168
+ let mut delta_max = 0.0f64;
1169
+ for (e, antes) in moe.especialistas.iter().zip(snapshot.iter()) {
1170
+ let agora = e.vars()[0].as_tensor().flatten_all().unwrap()
1171
+ .to_vec1::<f32>().unwrap();
1172
+ let dif = agora.iter().zip(antes.iter()).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
1173
+ delta_max = delta_max.max(dif as f64);
1174
+ }
1175
+ let _ = &vars;
1176
+ assert!(
1177
+ delta_max > 0.0,
1178
+ "M-R8: especialistas devem mudar no treino (delta_max={delta_max})"
1179
+ );
1180
+ }
1181
  }
1182
 
1183
  #[cfg(test)]
src/quantizador.rs CHANGED
@@ -67,7 +67,11 @@ impl CustomOp3 for GsqSuave {
67
  let c = _s2.as_slice::<f32>()?;
68
  let (k, _dc) = (_l2.shape().dims()[0], _l2.shape().dims()[1]);
69
  let ruido = s3.as_slice::<f32>()?;
70
- debug_assert_eq!(ruido.len(), z.len() / d * k * d / d * k); // (n,k)
 
 
 
 
71
  // logits = z·Cᵀ/τ + g
72
  let mut logits = vec![0f32; n * k];
73
  for i in 0..n {
 
67
  let c = _s2.as_slice::<f32>()?;
68
  let (k, _dc) = (_l2.shape().dims()[0], _l2.shape().dims()[1]);
69
  let ruido = s3.as_slice::<f32>()?;
70
+ // v0.8.0 (bug v080-2, pré-existente): a fórmula anterior
71
+ // `z.len()/d*k*d/d*k` avaliava para n·k²·(…) sem sentido (320 ≠ 40
72
+ // no teste) e disparava em TODO build debug — os gates da v0.7.0 só
73
+ // passavam em release (debug_assert compilado fora). Correta: n·k.
74
+ debug_assert_eq!(ruido.len(), n * k); // (n, k)
75
  // logits = z·Cᵀ/τ + g
76
  let mut logits = vec![0f32; n * k];
77
  for i in 0..n {
src/verificador.rs CHANGED
@@ -94,6 +94,115 @@ pub fn calibrar_temperatura(logits: &Tensor, alvos: &Tensor) -> Result<(f64, f64
94
  Ok((t_estrela, nll_1, nll_estrela))
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  /// ECE (Expected Calibration Error) por faixas de confiança.
98
  pub fn ece(confs: &[f64], corretos: &[bool], n_faixas: usize) -> f64 {
99
  if confs.is_empty() || confs.len() != corretos.len() {
@@ -150,8 +259,23 @@ impl VerificadorNcm {
150
  }
151
 
152
  /// Ajusta T* em (logits, alvos) retidos e mede o ECE antes/depois.
 
 
153
  pub fn ajustar(&mut self, logits: &Tensor, alvos: &Tensor) -> Result<(f64, f64)> {
154
- let (t, nll1, nllt) = calibrar_temperatura(logits, alvos)?;
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  self.temperatura = t;
156
  self.n_ll_antes = nll1;
157
  self.n_ll_depois = nllt;
@@ -289,4 +413,138 @@ mod testes {
289
  .to_scalar::<f32>().unwrap();
290
  assert!(d == 0.0, "determinístico nos logits do presente");
291
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
 
94
  Ok((t_estrela, nll_1, nll_estrela))
95
  }
96
 
97
+ /// Derivadas analíticas da NLL de temperatura — base do Newton-Raphson
98
+ /// (proposta Qwen-3.8 nº 1, **CORRIGIDA**: o original inverteu o sinal de
99
+ /// dL/dT — `(E_p[z] − z_y)/T²` em vez de `(z_y − E_p[z])/T²` — e usou a
100
+ /// primeira diferença `[L(T+ε)−L(T)]/ε` como se fosse a SEGUNDA derivada;
101
+ /// com os dois erros combinados o passo Δ ≈ −1 empurra T para cima a cada
102
+ /// iteração até o clamp — errata completa em r-33 §2). Com p = softmax(z/T):
103
+ /// `L(T) = média_i [−z_{i,y_i}/T + log Σ_j e^{z_{i,j}/T}]`
104
+ /// `L'(T) = média_i ( z_{i,y_i} − E_{p_i}[z_i] ) / T²`
105
+ /// `L''(T)= média_i ( Var_{p_i}(z_i) − 2T·(z_{i,y_i} − E_{p_i}[z_i]) ) / T⁴`
106
+ /// (Teo N-1 — derivação direta; no ótimo L'(T*)=0 ⇒ L''(T*) = Var/T⁴ > 0.)
107
+ fn grad_hess_temperatura(logits: &Tensor, alvos: &Tensor, t: f64) -> Result<(f64, f64, f64)> {
108
+ let n = logits.dim(0)?;
109
+ let escalado = logits.affine(1.0 / t, 0.0)?;
110
+ let ce = candle_nn::loss::cross_entropy(&escalado, alvos)?;
111
+ let l = ce.to_scalar::<f32>()? as f64;
112
+ let p = candle_nn::ops::softmax(&escalado, candle_core::D::Minus1)?;
113
+ // E_p[z] e E_p[z²] por linha — Var_p(z) = E[z²] − (E[z])² em UMA passada
114
+ let ez = (&p * logits)?.sum_keepdim(candle_core::D::Minus1)?;
115
+ let ez2 = (&p * &logits.sqr()?)?.sum_keepdim(candle_core::D::Minus1)?;
116
+ let var = (&ez2 - &(&ez * &ez)?)?;
117
+ // z_y por linha via gather
118
+ let idx = alvos.reshape((n, 1))?;
119
+ let zt = logits.gather(&idx, candle_core::D::Minus1)?;
120
+ let ez_m = ez.mean_all()?.to_scalar::<f32>()? as f64;
121
+ let var_m = var.mean_all()?.to_scalar::<f32>()? as f64;
122
+ let zt_m = zt.mean_all()?.to_scalar::<f32>()? as f64;
123
+ let dif = zt_m - ez_m;
124
+ let t2 = t * t;
125
+ let dl = dif / t2;
126
+ let d2l = (var_m - 2.0 * t * dif) / (t2 * t2);
127
+ Ok((l, dl, d2l))
128
+ }
129
+
130
+ /// **N-4:** calibra T* por **Newton-Raphson com derivadas analíticas**
131
+ /// (convergência quadrática — Teo N-3) e **salvaguarda de bisseção
132
+ /// geométrica** sobre o sinal de L̃'. A iteração é feita em **γ = 1/T**:
133
+ /// L̃(γ) = NLL(softmax(γz)) é CONVEXA (N-2: L̃''(γ) = Var_p(z) ≥ 0
134
+ /// SEMPRE — não apenas perto do ótimo), com
135
+ /// `L̃'(γ) = E_p[z] − z_y` e `L̃''(γ) = Var_p(z)` — o Newton em γ nunca
136
+ /// encontra curvatura negativa e o colchão γ ∈ [1/8, 8] (⇔ T ∈ [1/8, 8],
137
+ /// o mesmo da V-1) é mantido como invariante com a bisseção de salvaguarda.
138
+ /// Pior caso: bisseção pura, ⌈log₂(ln 64/ε)⌉ ≈ 35 passos; típico: 4–12
139
+ /// avaliações (vs ~44 do golden-section). Devolve (T*, nll(1), nll(T*), n_aval).
140
+ pub fn calibrar_temperatura_newton(
141
+ logits: &Tensor,
142
+ alvos: &Tensor,
143
+ max_iters: usize,
144
+ ) -> Result<(f64, f64, f64, usize)> {
145
+ let nll_em = |t: f64| nll_temperatura(logits, alvos, t);
146
+ let nll_1 = nll_em(1.0)?;
147
+ // (L, L̃'(γ), L̃''(γ)) com escalado = γ·z (p = softmax(γz)):
148
+ // L̃' = média(E_p[z] − z_y); L̃'' = média(Var_p(z)) ≥ 0 (N-2)
149
+ let gh_gamma = |g: f64| -> Result<(f64, f64, f64)> {
150
+ let n = logits.dim(0)?;
151
+ let escalado = logits.affine(g, 0.0)?;
152
+ let ce = candle_nn::loss::cross_entropy(&escalado, alvos)?;
153
+ let l = ce.to_scalar::<f32>()? as f64;
154
+ let p = candle_nn::ops::softmax(&escalado, candle_core::D::Minus1)?;
155
+ let ez = (&p * logits)?.sum_keepdim(candle_core::D::Minus1)?;
156
+ let ez2 = (&p * &logits.sqr()?)?.sum_keepdim(candle_core::D::Minus1)?;
157
+ let var = (&ez2 - &(&ez * &ez)?)?;
158
+ let idx = alvos.reshape((n, 1))?;
159
+ let zt = logits.gather(&idx, candle_core::D::Minus1)?;
160
+ let ez_m = ez.mean_all()?.to_scalar::<f32>()? as f64;
161
+ let var_m = var.mean_all()?.to_scalar::<f32>()? as f64;
162
+ let zt_m = zt.mean_all()?.to_scalar::<f32>()? as f64;
163
+ Ok((l, ez_m - zt_m, var_m))
164
+ };
165
+ let (lo, hi) = (1.0f64 / 8.0, 8.0f64); // colchão em γ ⇔ T ∈ [1/8, 8]
166
+ // mínimo na borda? (convexa: L̃' não troca sinal dentro do colchão)
167
+ let (_, dlo, _) = gh_gamma(lo)?;
168
+ if dlo >= 0.0 {
169
+ // mínimo em γ=lo ⇒ T* = 8 (esfriar ao máximo)
170
+ return Ok((1.0 / lo, nll_1, nll_em(1.0 / lo)?, 1));
171
+ }
172
+ let (_, dhi, _) = gh_gamma(hi)?;
173
+ if dhi <= 0.0 {
174
+ // mínimo em γ=hi ⇒ T* = 1/8 (aquecer ao máximo)
175
+ return Ok((1.0 / hi, nll_1, nll_em(1.0 / hi)?, 1));
176
+ }
177
+ // invariante do colchão: L̃'(a) < 0 < L̃'(b) (L̃ convexa ⇒ válida)
178
+ let (mut a, mut b) = (lo, hi);
179
+ let mut g = 1.0f64; // chute inicial γ=1 (T=1, incalibrado)
180
+ let mut melhor = (1.0f64, nll_1); // (T, NLL)
181
+ let mut n_aval = 2usize;
182
+ for _ in 0..max_iters {
183
+ let (l, dp, d2p) = gh_gamma(g)?;
184
+ n_aval += 1;
185
+ let t = 1.0 / g;
186
+ if l < melhor.1 {
187
+ melhor = (t, l);
188
+ }
189
+ if dp.abs() < 1e-10 || (b - a) < 1e-12 {
190
+ break;
191
+ }
192
+ // atualiza o colchão pelo sinal de L̃' (bisseção válida — N-2)
193
+ if dp < 0.0 {
194
+ a = a.max(g);
195
+ } else {
196
+ b = b.min(g);
197
+ }
198
+ // Newton em γ: L̃'' = Var ≥ 0 SEMPRE (convexidade global);
199
+ // passo aceito só DENTRO do colchão, senão bisseção geométrica
200
+ let gn = if d2p > 1e-12 { g - dp / d2p } else { f64::NAN };
201
+ g = if gn.is_finite() && gn > a && gn < b { gn } else { (a * b).sqrt() };
202
+ }
203
+ Ok((melhor.0, nll_1, melhor.1, n_aval))
204
+ }
205
+
206
  /// ECE (Expected Calibration Error) por faixas de confiança.
207
  pub fn ece(confs: &[f64], corretos: &[bool], n_faixas: usize) -> f64 {
208
  if confs.is_empty() || confs.len() != corretos.len() {
 
259
  }
260
 
261
  /// Ajusta T* em (logits, alvos) retidos e mede o ECE antes/depois.
262
+ /// v0.8.0: Newton-Raphson analítico (N-4) com salvaguarda — o
263
+ /// golden-section (V-1) permanece como verificação/rede de segurança.
264
  pub fn ajustar(&mut self, logits: &Tensor, alvos: &Tensor) -> Result<(f64, f64)> {
265
+ let (mut t, mut nll1, mut nllt) = (1.0, 0.0, 0.0);
266
+ let mut ok = false;
267
+ // Newton com salvaguarda (4× o orçamento típico de avaliações)
268
+ if let Ok((tn, n1, nt, _)) = calibrar_temperatura_newton(logits, alvos, 40) {
269
+ if nt.is_finite() && nt <= n1 + 1e-6 {
270
+ (t, nll1, nllt) = (tn, n1, nt);
271
+ ok = true;
272
+ }
273
+ }
274
+ if !ok {
275
+ // rede de segurança: golden-section (V-1) — nunca removida
276
+ let (tg, n1g, ntg) = calibrar_temperatura(logits, alvos)?;
277
+ (t, nll1, nllt) = (tg, n1g, ntg);
278
+ }
279
  self.temperatura = t;
280
  self.n_ll_antes = nll1;
281
  self.n_ll_depois = nllt;
 
413
  .to_scalar::<f32>().unwrap();
414
  assert!(d == 0.0, "determinístico nos logits do presente");
415
  }
416
+
417
+ /// **N-1:** gradiente/curvatura analíticos batem com diferenças finitas
418
+ /// centralmente — validação numérica da derivação do Teo N-1 (e errata
419
+ /// da proposta Qwen-3.8 nº 1: sinal de L' e ordem da diferença finita).
420
+ #[test]
421
+ fn n1_grad_hess_analiticos_vs_finitas() {
422
+ let (lt, at) = logits_sinteticos(512, 16, 2.0, 23);
423
+ let t = 1.3f64;
424
+ let (_, dl, d2l) = grad_hess_temperatura(&lt, &at, t).unwrap();
425
+ let h = 1e-2; // f32: equilíbrio ruído/truncamento (r-33 §2.4)
426
+ let l_mais = nll_temperatura(&lt, &at, t + h).unwrap();
427
+ let l_zero = nll_temperatura(&lt, &at, t).unwrap();
428
+ let l_menos = nll_temperatura(&lt, &at, t - h).unwrap();
429
+ let dl_fd = (l_mais - l_menos) / (2.0 * h);
430
+ // SEGUNDA diferença (a proposta original usava a PRIMEIRA)
431
+ let d2l_fd = (l_mais - 2.0 * l_zero + l_menos) / (h * h);
432
+ assert!(
433
+ (dl - dl_fd).abs() < 5e-3,
434
+ "N-1: L' analítico {dl:.6} vs finita {dl_fd:.6}"
435
+ );
436
+ assert!(
437
+ (d2l - d2l_fd).abs() < 5e-2,
438
+ "N-1: L'' analítico {d2l:.6} vs finita {d2l_fd:.6}"
439
+ );
440
+ // REGRESSÃO DA ERRATA: superconfiante (escala 2) ⇒ T* > 1 ⇒ L'(T=1)
441
+ // deve ser NEGATIVO (L desce em direção a T*). O sinal invertido da
442
+ // proposta daria positivo — e o Newton divergiria para o clamp 10.
443
+ let (_, dl_em_1, _) = grad_hess_temperatura(&lt, &at, 1.0).unwrap();
444
+ assert!(dl_em_1 < 0.0, "N-1: L'(1)={dl_em_1} deve ser < 0 (superconfiante)");
445
+ }
446
+
447
+ /// **N-2/N-4:** Newton com salvaguarda encontra o MESMO T* do
448
+ /// golden-section (V-1) com MENOS avaliações de NLL.
449
+ #[test]
450
+ fn n2_newton_igual_golden_mais_rapido() {
451
+ for (escala, semente) in [(1.0f32, 31u64), (3.0, 37), (0.3, 41)] {
452
+ let (lt, at) = logits_sinteticos(1024, 32, escala, semente);
453
+ let (t_g, _, nll_g) = calibrar_temperatura(&lt, &at).unwrap();
454
+ let (t_n, nll_1, nll_n, n_aval) =
455
+ calibrar_temperatura_newton(&lt, &at, 40).unwrap();
456
+ assert!(
457
+ (t_g - t_n).abs() < 1e-3,
458
+ "N-2: T* golden {t_g:.6} vs newton {t_n:.6} (escala {escala})"
459
+ );
460
+ assert!(
461
+ nll_n <= nll_g + 1e-4,
462
+ "N-4: NLL newton {nll_n:.6} ≤ golden {nll_g:.6} + 1e-4"
463
+ );
464
+ assert!(
465
+ nll_n <= nll_1 + 1e-6,
466
+ "N-4: NLL calibrada {nll_n:.6} ≤ bruta {nll_1:.6}"
467
+ );
468
+ // golden ~44 avaliações típicas (66 no teto); Newton ≤ 36
469
+ assert!(
470
+ n_aval < 36,
471
+ "N-4: newton usou {n_aval} avaliações (esperado < 36)"
472
+ );
473
+ }
474
+ }
475
+
476
+ /// **N-2 (verificação do teorema):** L' troca o sinal EXATAMENTE uma
477
+ /// vez no colchão (unimodalidade — NLL convexa em γ = 1/T).
478
+ #[test]
479
+ fn n3_sinal_de_l_troca_uma_vez() {
480
+ let (lt, at) = logits_sinteticos(2048, 16, 2.5, 43);
481
+ let mut trocas = 0usize;
482
+ let mut sinal_ant: Option<f64> = None;
483
+ for i in 0..64 {
484
+ let t = ((i as f64 / 63.0 * (8.0f64.ln() - (1.0f64 / 8.0).ln()))
485
+ + (1.0f64 / 8.0).ln())
486
+ .exp();
487
+ let (_, dl, _) = grad_hess_temperatura(&lt, &at, t).unwrap();
488
+ let s: f64 = if dl < 0.0 { -1.0 } else { 1.0 };
489
+ if let Some(sa) = sinal_ant {
490
+ if s != sa {
491
+ trocas += 1;
492
+ }
493
+ }
494
+ sinal_ant = Some(s);
495
+ }
496
+ assert_eq!(trocas, 1, "N-2: L' deve trocar o sinal exatamente 1 vez (trocas={trocas})");
497
+ }
498
+
499
+ /// **N-4 (casos extremos):** direção correta da calibração nos dois
500
+ /// regimes — superconfiante (ERROS confiantes: margem alta com ruído
501
+ /// moderado ⇒ L̃'(1) = média(E_p[z] − z_y) > 0 ⇒ γ* < 1 ⇒ T* > 1,
502
+ /// esfria) e subconfiante (margem modesta com ruído mínimo: corretos
503
+ /// dominam com p_y < 1 ⇒ T* < 1, aquece). Nota: a contribuição de cada
504
+ /// exemplo para L̃' é Σ_{j≠y} p_j(z_j − z_y) ≤ 0 para acertos e ≫ 0
505
+ /// para erros confiantes — é por isso que a NLL "escuta" os erros.
506
+ #[test]
507
+ fn n4b_casos_extremos() {
508
+ let dev = Device::Cpu;
509
+ // (a) superconfiante (padrão de overfitting): 95% acertos com
510
+ // margem 5,4 (p_y ≈ 0,97) e 5% ERROS confiantes com margem 8,4
511
+ // (p_errado ≈ 1) ⇒ conf ≈ 0,97 > acc = 0,95 ⇒ T* > 1. A
512
+ // contribuição dos erros para L̃'(1) é ≈ +8,4·0,05 = +0,42 vs
513
+ // −0,15 dos acertos ⇒ L̃'(1) > 0 ⇒ γ* < 1 ⇒ T* > 1 (esfria).
514
+ let mut rng = crate::rng::RngDeterministico::novo(47);
515
+ let (n, k) = (2048usize, 8usize);
516
+ let mut l = Vec::with_capacity(n * k);
517
+ let mut a = Vec::with_capacity(n);
518
+ for _ in 0..n {
519
+ let alvo = rng.inteiro(k) as u32;
520
+ a.push(alvo);
521
+ if rng.uniforme() < 0.05 {
522
+ let mut w = rng.inteiro(k) as u32;
523
+ if w == alvo {
524
+ w = (w + 1) % k as u32;
525
+ }
526
+ for j in 0..k {
527
+ l.push(if j as u32 == w { 8.4 } else { 0.0 });
528
+ }
529
+ } else {
530
+ for j in 0..k {
531
+ let base = if j as u32 == alvo { 5.4 } else { 0.0 };
532
+ l.push(base + rng.normal(0.0, 0.25) as f32);
533
+ }
534
+ }
535
+ }
536
+ let lt = Tensor::from_vec(l, (n, k), &dev).unwrap();
537
+ let at = Tensor::from_vec(a, (n,), &dev).unwrap();
538
+ let (t_n, nll_1, nll_n, _) = calibrar_temperatura_newton(&lt, &at, 40).unwrap();
539
+ assert!(t_n > 1.0, "N-4b: superconfiante exige T*>1 (T*={t_n})");
540
+ assert!(nll_n <= nll_1 + 1e-6);
541
+ assert!((1.0 / 8.0..=8.0).contains(&t_n), "N-4b: T* dentro do colchão");
542
+ // (b) subconfiante: margem 3 com ruído mínimo ⇒ p_y ≈ 0,74 < acc
543
+ // ≈ 1 ⇒ T* < 1 (aquece; medido na borda quente 1/8)
544
+ let (lt2, at2) = logits_sinteticos(512, 8, 0.1, 53);
545
+ let (t_n2, nll_12, nll_n2, _) = calibrar_temperatura_newton(&lt2, &at2, 40).unwrap();
546
+ assert!(t_n2 < 1.0, "N-4b: subconfiante exige T*<1 (T*={t_n2})");
547
+ assert!(nll_n2 <= nll_12 + 1e-6, "N-4b: NLL não piora");
548
+ assert!((1.0 / 8.0..=8.0).contains(&t_n2));
549
+ }
550
  }