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 +1 -1
- Cargo.toml +2 -2
- dados/bpe.json +0 -0
- docs/memorias/r-33-analise-provas-qwen-v080.md +460 -0
- resultados/suite_v080_final.json +149 -0
- scripts/68_suite_v080.py +129 -0
- scripts/69_publicar_v080.py +165 -0
- src/arvore_pesos.rs +282 -11
- src/fnh.rs +353 -1
- src/lib.rs +198 -1
- src/moe_redirecionavel.rs +277 -32
- src/quantizador.rs +5 -1
- src/verificador.rs +259 -1
Cargo.lock
CHANGED
|
@@ -734,7 +734,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
| 734 |
|
| 735 |
[[package]]
|
| 736 |
name = "khtst-rust"
|
| 737 |
-
version = "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.
|
| 4 |
edition = "2021"
|
| 5 |
-
description = "KHTST_RUST —
|
| 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 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 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
|
| 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.
|
| 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(<, &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(<, &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 |
-
//
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 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 |
-
|
| 490 |
-
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(<, &at, t).unwrap();
|
| 425 |
+
let h = 1e-2; // f32: equilíbrio ruído/truncamento (r-33 §2.4)
|
| 426 |
+
let l_mais = nll_temperatura(<, &at, t + h).unwrap();
|
| 427 |
+
let l_zero = nll_temperatura(<, &at, t).unwrap();
|
| 428 |
+
let l_menos = nll_temperatura(<, &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(<, &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(<, &at).unwrap();
|
| 454 |
+
let (t_n, nll_1, nll_n, n_aval) =
|
| 455 |
+
calibrar_temperatura_newton(<, &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(<, &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(<, &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(<2, &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 |
}
|