PowerMachine commited on
Commit
e4ee5c4
·
verified ·
1 Parent(s): 97df2ae

KHTST_RUST v0.3.0 — correções M1-M17 (vazamento do prefixo, paridade treino/inferência, SSD-Δ, STE, PCGrad, telemetria JSONL) + aprimoramentos com prova: mistura de planos, SSD-Δ (inteiros/floats), verificador algébrico, ADBA por (nível, cabeça); gates G1-G12; treino real Wikipédia PT-BR

Browse files
Cargo.lock CHANGED
@@ -31,12 +31,6 @@ version = "0.2.21"
31
  source = "registry+https://github.com/rust-lang/crates.io-index"
32
  checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
33
 
34
- [[package]]
35
- name = "anyhow"
36
- version = "1.0.104"
37
- source = "registry+https://github.com/rust-lang/crates.io-index"
38
- checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
39
-
40
  [[package]]
41
  name = "arc-swap"
42
  version = "1.9.3"
@@ -302,17 +296,6 @@ dependencies = [
302
  "syn 2.0.119",
303
  ]
304
 
305
- [[package]]
306
- name = "dict_derive"
307
- version = "0.5.1"
308
- source = "registry+https://github.com/rust-lang/crates.io-index"
309
- checksum = "846a520b3b7937dcfe564d9674d2d26da9e3cd0f8f1bbef51ca42fd6b7bb4f4a"
310
- dependencies = [
311
- "proc-macro2",
312
- "quote",
313
- "syn 1.0.109",
314
- ]
315
-
316
  [[package]]
317
  name = "dyn-stack"
318
  version = "0.13.2"
@@ -734,16 +717,13 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
734
 
735
  [[package]]
736
  name = "khtst-rust"
737
- version = "0.1.11"
738
  dependencies = [
739
- "anyhow",
740
  "built",
741
  "candle-core",
742
  "candle-nn",
743
  "crossbeam-channel",
744
- "dict_derive",
745
  "log",
746
- "numpy",
747
  "pyo3",
748
  "pyo3-async-runtimes",
749
  "pyo3-built",
@@ -753,7 +733,6 @@ dependencies = [
753
  "rayon",
754
  "serde",
755
  "serde_json",
756
- "thiserror 2.0.21",
757
  "tokio",
758
  ]
759
 
@@ -797,16 +776,6 @@ version = "0.2.3"
797
  source = "registry+https://github.com/rust-lang/crates.io-index"
798
  checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c"
799
 
800
- [[package]]
801
- name = "matrixmultiply"
802
- version = "0.3.11"
803
- source = "registry+https://github.com/rust-lang/crates.io-index"
804
- checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
805
- dependencies = [
806
- "autocfg",
807
- "rawpointer",
808
- ]
809
-
810
  [[package]]
811
  name = "memchr"
812
  version = "2.8.3"
@@ -860,21 +829,6 @@ dependencies = [
860
  "syn 2.0.119",
861
  ]
862
 
863
- [[package]]
864
- name = "ndarray"
865
- version = "0.16.1"
866
- source = "registry+https://github.com/rust-lang/crates.io-index"
867
- checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
868
- dependencies = [
869
- "matrixmultiply",
870
- "num-complex",
871
- "num-integer",
872
- "num-traits",
873
- "portable-atomic",
874
- "portable-atomic-util",
875
- "rawpointer",
876
- ]
877
-
878
  [[package]]
879
  name = "nom"
880
  version = "7.1.3"
@@ -895,15 +849,6 @@ dependencies = [
895
  "num-traits",
896
  ]
897
 
898
- [[package]]
899
- name = "num-integer"
900
- version = "0.1.47"
901
- source = "registry+https://github.com/rust-lang/crates.io-index"
902
- checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
903
- dependencies = [
904
- "num-traits",
905
- ]
906
-
907
  [[package]]
908
  name = "num-traits"
909
  version = "0.2.19"
@@ -924,21 +869,6 @@ dependencies = [
924
  "libc",
925
  ]
926
 
927
- [[package]]
928
- name = "numpy"
929
- version = "0.23.0"
930
- source = "registry+https://github.com/rust-lang/crates.io-index"
931
- checksum = "b94caae805f998a07d33af06e6a3891e38556051b8045c615470a71590e13e78"
932
- dependencies = [
933
- "libc",
934
- "ndarray",
935
- "num-complex",
936
- "num-integer",
937
- "num-traits",
938
- "pyo3",
939
- "rustc-hash",
940
- ]
941
-
942
  [[package]]
943
  name = "once_cell"
944
  version = "1.21.4"
@@ -997,15 +927,6 @@ version = "1.15.0"
997
  source = "registry+https://github.com/rust-lang/crates.io-index"
998
  checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
999
 
1000
- [[package]]
1001
- name = "portable-atomic-util"
1002
- version = "0.2.8"
1003
- source = "registry+https://github.com/rust-lang/crates.io-index"
1004
- checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
1005
- dependencies = [
1006
- "portable-atomic",
1007
- ]
1008
-
1009
  [[package]]
1010
  name = "ppv-lite86"
1011
  version = "0.2.21"
@@ -1249,12 +1170,6 @@ dependencies = [
1249
  "bitflags",
1250
  ]
1251
 
1252
- [[package]]
1253
- name = "rawpointer"
1254
- version = "0.2.1"
1255
- source = "registry+https://github.com/rust-lang/crates.io-index"
1256
- checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
1257
-
1258
  [[package]]
1259
  name = "rayon"
1260
  version = "1.12.0"
@@ -1321,12 +1236,6 @@ version = "0.8.11"
1321
  source = "registry+https://github.com/rust-lang/crates.io-index"
1322
  checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1323
 
1324
- [[package]]
1325
- name = "rustc-hash"
1326
- version = "2.1.3"
1327
- source = "registry+https://github.com/rust-lang/crates.io-index"
1328
- checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1329
-
1330
  [[package]]
1331
  name = "rustix"
1332
  version = "1.1.5"
@@ -1471,17 +1380,6 @@ version = "0.11.1"
1471
  source = "registry+https://github.com/rust-lang/crates.io-index"
1472
  checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1473
 
1474
- [[package]]
1475
- name = "syn"
1476
- version = "1.0.109"
1477
- source = "registry+https://github.com/rust-lang/crates.io-index"
1478
- checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
1479
- dependencies = [
1480
- "proc-macro2",
1481
- "quote",
1482
- "unicode-ident",
1483
- ]
1484
-
1485
  [[package]]
1486
  name = "syn"
1487
  version = "2.0.119"
 
31
  source = "registry+https://github.com/rust-lang/crates.io-index"
32
  checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
33
 
 
 
 
 
 
 
34
  [[package]]
35
  name = "arc-swap"
36
  version = "1.9.3"
 
296
  "syn 2.0.119",
297
  ]
298
 
 
 
 
 
 
 
 
 
 
 
 
299
  [[package]]
300
  name = "dyn-stack"
301
  version = "0.13.2"
 
717
 
718
  [[package]]
719
  name = "khtst-rust"
720
+ version = "0.3.0"
721
  dependencies = [
 
722
  "built",
723
  "candle-core",
724
  "candle-nn",
725
  "crossbeam-channel",
 
726
  "log",
 
727
  "pyo3",
728
  "pyo3-async-runtimes",
729
  "pyo3-built",
 
733
  "rayon",
734
  "serde",
735
  "serde_json",
 
736
  "tokio",
737
  ]
738
 
 
776
  source = "registry+https://github.com/rust-lang/crates.io-index"
777
  checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c"
778
 
 
 
 
 
 
 
 
 
 
 
779
  [[package]]
780
  name = "memchr"
781
  version = "2.8.3"
 
829
  "syn 2.0.119",
830
  ]
831
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  [[package]]
833
  name = "nom"
834
  version = "7.1.3"
 
849
  "num-traits",
850
  ]
851
 
 
 
 
 
 
 
 
 
 
852
  [[package]]
853
  name = "num-traits"
854
  version = "0.2.19"
 
869
  "libc",
870
  ]
871
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  [[package]]
873
  name = "once_cell"
874
  version = "1.21.4"
 
927
  source = "registry+https://github.com/rust-lang/crates.io-index"
928
  checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
929
 
 
 
 
 
 
 
 
 
 
930
  [[package]]
931
  name = "ppv-lite86"
932
  version = "0.2.21"
 
1170
  "bitflags",
1171
  ]
1172
 
 
 
 
 
 
 
1173
  [[package]]
1174
  name = "rayon"
1175
  version = "1.12.0"
 
1236
  source = "registry+https://github.com/rust-lang/crates.io-index"
1237
  checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1238
 
 
 
 
 
 
 
1239
  [[package]]
1240
  name = "rustix"
1241
  version = "1.1.5"
 
1380
  source = "registry+https://github.com/rust-lang/crates.io-index"
1381
  checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1382
 
 
 
 
 
 
 
 
 
 
 
 
1383
  [[package]]
1384
  name = "syn"
1385
  version = "2.0.119"
Cargo.toml CHANGED
@@ -1,8 +1,8 @@
1
  [package]
2
  name = "khtst-rust"
3
- version = "0.1.11"
4
  edition = "2021"
5
- description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM (Next-Concept Modeling), provas integradas, atenção ADBA e ponte duplex"
6
  license = "Apache-2.0"
7
 
8
  [lib]
@@ -11,23 +11,24 @@ crate-type = ["cdylib", "rlib"]
11
 
12
  [dependencies]
13
  pyo3 = { version = "0.23", features = ["abi3-py310"] }
14
- numpy = "0.23"
15
  pythonize = "0.23"
16
  pyo3-log = "0.12"
17
- dict_derive = "0.5"
18
  pyo3-built = "0.5"
19
  pyo3-async-runtimes = { version = "0.23", optional = true, features = ["tokio-runtime"] }
20
  tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
21
  candle-core = "0.11"
22
  candle-nn = "0.11"
23
  crossbeam-channel = "0.5"
 
24
  rayon = "1"
25
  serde = { version = "1", features = ["derive"] }
26
  serde_json = "1"
27
- anyhow = "1"
28
- thiserror = "2"
29
  log = "0.4"
30
  rand = "0.8"
 
 
 
 
31
 
32
  [build-dependencies]
33
  built = "0.8"
 
1
  [package]
2
  name = "khtst-rust"
3
+ version = "0.3.0"
4
  edition = "2021"
5
+ description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM (Next-Concept Modeling), provas integradas, ADBA por (nível, cabeça), ponte duplex, cabeça de mistura de planos, SSD-Δ (percepção de inteiros/floats) e verificador algébrico"
6
  license = "Apache-2.0"
7
 
8
  [lib]
 
11
 
12
  [dependencies]
13
  pyo3 = { version = "0.23", features = ["abi3-py310"] }
 
14
  pythonize = "0.23"
15
  pyo3-log = "0.12"
 
16
  pyo3-built = "0.5"
17
  pyo3-async-runtimes = { version = "0.23", optional = true, features = ["tokio-runtime"] }
18
  tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
19
  candle-core = "0.11"
20
  candle-nn = "0.11"
21
  crossbeam-channel = "0.5"
22
+ # rayon: paralelismo real (scan SSD por lote, AdamW por variável — A35/M17).
23
  rayon = "1"
24
  serde = { version = "1", features = ["derive"] }
25
  serde_json = "1"
 
 
26
  log = "0.4"
27
  rand = "0.8"
28
+ # v0.3.0 (A14/decisão do usuário): REMOVIDAS as dependências não usadas —
29
+ # `numpy` (rust-numpy), `dict_derive`, `anyhow`, `thiserror`. O binding de
30
+ # sequências permanece Vec<Vec<u32>> (2,4 MB para 2 394×256 — cópia
31
+ # desprezível; rust-numpy seria peso morto aqui).
32
 
33
  [build-dependencies]
34
  built = "0.8"
README.md CHANGED
@@ -1,19 +1,30 @@
1
  # KHTST_RUST — Next-Concept Modeling em Rust/PyO3
2
 
3
- Refatoração completa do projeto **KHTST** (`PowerMachine/khtst-multimodal-ptbr`) para **Rust** via **PyO3**, integrando o paradigma **NCM (Next-Concept Modeling)** com todas as provas matemáticas da coleção `memoria-ncm-khtst`, e dois aprimoramentos novos: **ADBA** (atenção adaptativa no processamento paralelo) e **Ponte Duplex** (comunicação bidirecional simultânea entre modelos).
 
 
 
 
 
 
 
 
 
 
4
 
5
  ## O que é
6
 
7
- - **Tokenizador de conceitos** (Estágio A): VQ-VAE residual multiscale com codebooks **Stiefel/RCO** e quantização **GSQ** (Gumbel-Softmax com gradiente exato) — contração geométrica (1−1/K) por nível (Teo A.1/D.4), nested dropout (prefixos anytime, Teo A.2).
8
- - **Gerador next-concept** (Estágio B): fatoração `p(c) = Π_ℓ p(c_ℓ|c_{<ℓ})` com predição **paralela** intra-nível — **MLE exato sem ELBO** (Teo B.1), **NFE = L** (7 ≪ 256; Teo B.2), corrupção de condicionamento como DRO (Teo C.3), MTP ⊂ NCM (Teo D.6), PCGrad (Teo D.7).
9
- - **Tronco híbrido 5:1**: 5 camadas **ADBA** + 1 slot **Mamba-3 SSD** (scan diagonal com adjunto exato) — máscara block-causal com 0 vazamentos (Teo D.1, V4).
10
- - **Ponte Duplex**: A↔B com canais limitados, rodadas com timeout — sem deadlock estrutural (Teo P.1), direções simultâneas medidas (P.2).
11
- - **Telemetria completa**: séries JSONL, escore de aprendizado, **ajuste automático auditado** de LR/clip/ε/λ/τ, diagnóstico de erros comuns de refatoração (Levenshtein/taxonomia).
12
 
13
  ## Instalação
14
 
15
  ```bash
16
  pip install khtst_rust-*.whl # wheel abi3 (Python ≥ 3.10, manylinux)
 
17
  # ou do fonte: maturin build --release && pip install target/wheels/*.whl
18
  ```
19
 
@@ -22,47 +33,53 @@ pip install khtst_rust-*.whl # wheel abi3 (Python ≥ 3.10, manylinux)
22
  ```python
23
  import khtst_rust as K
24
 
25
- cfg = K.Config("configs/base_rust.json") # valida + orçamento de cache
26
- bpe = K.Bpe.treinar(textos, 16384) # BPE byte-level próprio
27
 
28
  # Fase A: tokenizador (nested dropout + GSQ/RCO Stiefel)
29
  K.treinar_fase_a(cfg, sequencias, caminho_telemetria="tel_a.jsonl",
30
- caminho_checkpoint="tokenizador.json", teto_seg=430)
 
31
 
32
- # Fase B: gerador (MLE exato + κ + MTP + JEPA + PCGrad)
33
  K.treinar_fase_b(cfg, sequencias, "tokenizador.json",
34
  caminho_telemetria="tel_b.jsonl",
35
- caminho_checkpoint="gerador.json", teto_seg=330)
 
36
 
37
- # Geração paralela: NFE = L; parada anytime em qualquer prefixo
38
- g = K.gerar_amostras(cfg, "tokenizador.json", "gerador.json", n_amostras=2)
39
- assert g["nfe"] == 7 # = L
40
 
41
- # Ponte duplex entre modelos
42
- p = K.PonteDupla(capacidade=64, timeout_ms=2000)
43
- est = p.demo_professor_aluno(rodadas=200, dim=24)
 
44
  ```
45
 
46
  ## Estrutura
47
 
48
  ```
49
- src/ 19 módulos Rust (ver docs/memorias/r-02-arquitetura.md)
50
- configs/ base_rust.json (espelho do base.json v23 + seção ncm)
51
- scripts/ 39_coletar_dados.py · 40_treinar.py · 41_teste.py · 42_publicar.py
52
- docs/memorias/ r-01..r-07 (diagnóstico, arquitetura, rastreabilidade provas→código,
53
- prova ADBA, prova ponte duplex, telemetria, resultados)
54
- resultados/ checkpoints, telemetria JSONL, suíte V, resumo de treino
 
 
55
  ```
56
 
57
- ## Verificações
58
 
59
- - **15/15 testes cargo** (adjuntos validados contra diferenças finitas e implementação nativa)
60
- - **Suíte V**: 7/7 bloqueadoras verdes (V0,V1,V4,V9,V10,V13,V14) + evidenciais
61
- - **Treino real** (Wikipédia PT-BR, streaming controlado): fase A perda 9,89→8,11, δ_ort ≤ 9,9e-7 durante o treino, codebook 52/64 sem colapso; fase B CE 4,19→3,98; geração NFE=7, anytime k=3
 
62
 
63
  ## Provas e teoria
64
 
65
- Toda a fundamentação matemática (20 teoremas + 10 corolários da integração NCM×KHTST; Teos R.1–R.3 da ADBA; Teos P.1–P.3 da ponte duplex) está em `docs/memorias/` com rastreabilidade teorema→código→verificação (r-03).
66
 
67
  ## Convenções herdadas do projeto
68
 
 
1
  # KHTST_RUST — Next-Concept Modeling em Rust/PyO3
2
 
3
+ Refatoração completa do projeto **KHTST** (`PowerMachine/khtst-multimodal-ptbr`) para **Rust** via **PyO3**, integrando o paradigma **NCM (Next-Concept Modeling)** com todas as provas matemáticas da coleção `memoria-ncm-khtst`.
4
+
5
+ ## v0.3.0 — correções auditadas + aprimoramentos com prova
6
+
7
+ A v0.1.11 foi auditada por agente independente (r-08: **17 defeitos M1–M17**, ~40 afirmações documentais refutadas — o central: a máscara deixava o prefixo 100% bloqueado ⇒ softmax uniforme ⇒ **vazamento do futuro**, quebrando a fatoração next-concept em todas as passadas de treino, invisível ao V4 que contava a matriz). A v0.3.0 corrige tudo e adiciona três aprimoramentos com demonstrações:
8
+
9
+ 1. **Mistura de planos** (raciocínio em subníveis paralelos): `p(c_ℓ|c_{<ℓ}) = Σ_m w_m Π_i q_{m,i}` — M planos de raciocínio paralelos por nível. **Teo C-1**: MLE exato, NFE = L, amostragem paralela intra-nível, contenção estrita (M=1 ≡ v0.1.11 bit-a-bit — gate G9), normalização Σ_c p(c)=1 por enumeração exausta (gate G12).
10
+ 2. **SSD-Δ** (percepção de matemática de inteiros/ponto flutuante): porta aditiva de 2ª ordem `h_t = a_t⊙h_{t−1} + (1−a_t+η_t)⊙x_t`. **Teo S-1**: com a=1, η=1 o slot computa a **soma acumulada EXATA** para inteiros < 2^24 (gate G8a, bit-a-bit); para floats, erro ≤ 2e^{−2ρ}·Σ|x| + cota clássica ε_mach (gate G8b); contenção estrita provada (a família antiga não representa o mapa soma).
11
+ 3. **Verificador algébrico** da hierarquia: `L_alg = Σ_ℓ ‖Interp_{n_{ℓ−1}}(ẑ_ℓ^{soft}) − ẑ_{ℓ−1}‖²` — quadrática com solução em forma fechada (equações normais — **Teo G-1**), gradiente exato validado por diferenças finitas (gate G11), com cota inferior auditável.
12
+
13
+ Também: **ADBA por (nível, cabeça)** com parametrização limitada tanh (Teo A-1 — sem dead-gradient, τ ∈ [e⁻¹,e], w ∈ [e⁻²,e²], nascimento neutro), estado do SSD transportado entre níveis (M4), paridade treino↔inferência estrutural (M2 — âncora do buffer pós-bloco-0, gate G2: Δ ≤ 2e-6), reprodutibilidade bit-a-bit por semente (A19, gate G7), checkpoint com manifesto+fsync (A20/A21), **retomada** de treino (estado do otimizador serializado — proteção contra encerramento em background).
14
 
15
  ## O que é
16
 
17
+ - **Tokenizador de conceitos** (Estágio A): VQ-VAE residual multiscale com codebooks **Stiefel/RCO** e quantização **GSQ** (gradiente exato) — contração (1−1/K)^ℓ por nível, nested dropout (prefixos anytime).
18
+ - **Gerador next-concept** (Estágio B): fatoração `p(c) = Π_ℓ p(c_ℓ|c_{<ℓ})` com predição **paralela** intra-nível — MLE exato sem ELBO, NFE = L, corrupção κ como DRO, MTP ⊂ NCM, PCGrad (contra os gradientes ORIGINAIS — M8), JEPA com λ por hipergradiente exato (S-09).
19
+ - **Tronco híbrido 5:1**: 5 camadas ADBA + 1 slot Mamba-3 **SSD-Δ** (adjunto exato incl. ∂L/∂h₀ e ∂L/∂η — gate G10); máscara block-causal com prefixo causal (M1) e **vazamento efetivo zero** (gate G1).
20
+ - **Ponte Duplex**: A↔B com canais limitados, `send_timeout` em threads separadas (M13), sobreposição medida por interseção real de janelas (M14), GIL liberado.
21
+ - **Telemetria completa**: **uma linha JSONL por passo com todas as séries** (M12 — a v0.1.11 gravava 0–1 linhas), modo append (A24), detecção de aprendizado com limiares e eventos (S-05), ajuste automático auditado de LR (multiplicador com recuperação — M9)/clip/ε/λ/τ, CE por nível, contração por nível, resíduo algébrico, entropia dos planos, diagnóstico de erros com análise de digitação (lint Levenshtein chamado pelos scripts).
22
 
23
  ## Instalação
24
 
25
  ```bash
26
  pip install khtst_rust-*.whl # wheel abi3 (Python ≥ 3.10, manylinux)
27
+ pip install ".[data,publish]" # dependências dos scripts (A13)
28
  # ou do fonte: maturin build --release && pip install target/wheels/*.whl
29
  ```
30
 
 
33
  ```python
34
  import khtst_rust as K
35
 
36
+ cfg = K.Config("configs/base_rust.json") # valida + orçamento de cache (516)
37
+ bpe = K.Bpe.treinar(textos, 16384) # BPE byte-level (roundtrip exato)
38
 
39
  # Fase A: tokenizador (nested dropout + GSQ/RCO Stiefel)
40
  K.treinar_fase_a(cfg, sequencias, caminho_telemetria="tel_a.jsonl",
41
+ caminho_checkpoint="tokenizador.json", teto_seg=430,
42
+ callback=print, retomar=False)
43
 
44
+ # Fase B: gerador (mistura de planos + SSD-Δ + L_alg + PCGrad)
45
  K.treinar_fase_b(cfg, sequencias, "tokenizador.json",
46
  caminho_telemetria="tel_b.jsonl",
47
+ caminho_checkpoint="gerador.json", teto_seg=330,
48
+ retomar=True) # continua de .estado.json se existir
49
 
50
+ # Verificações de função (não de estrutura)
51
+ assert K.verificar_vazamento_efetivo(cfg) == 0 # gate G1
52
+ assert K.verificar_paridade(cfg)["dif_max"] < 1e-4 # gate G2
53
 
54
+ # Geração paralela: NFE = L; anytime; MBR opcional; semente/max_tokens livres
55
+ g = K.gerar_amostras(cfg, "tokenizador.json", "gerador.json",
56
+ n_amostras=2, semente=7, max_tokens=64, n_cand=3)
57
+ assert g["nfe"] == 7 # = L
58
  ```
59
 
60
  ## Estrutura
61
 
62
  ```
63
+ src/ 22 módulos Rust (r-02 §1 + rng.rs; ver docs/memorias/)
64
+ tests/provas.rs gates G1–G12 (provas executáveis — regra da casa:
65
+ "contar a estrutura não é verificar a função")
66
+ configs/ base_rust.json (v0.3: ssd_delta, mistura_planos=4, algebrico, semente)
67
+ scripts/ 39_coletar · 40_treinar (--retomar) · 41_teste · 42_publicar (allowlist)
68
+ docs/memorias/ r-01..r-09 (…, r-08 auditoria v0.2.0, r-09 aprimoramentos v0.3.0)
69
+ docs/memoria-ncm-khtst/ n-00..n-04 (teoria NCM: 20 Teos + 10 corolários + T1–T10)
70
+ resultados/ checkpoints (manifesto khtst-ckpt-v2), telemetria JSONL, suíte V
71
  ```
72
 
73
+ ## Verificações (v0.3.0)
74
 
75
+ - **37/37 testes cargo (release)**: 29 unitários + 8 gates (G1–G12; inclui adjuntos vs FD com ∂L/∂h₀ e ∂L/∂η, acumulação inteira exata e float com cota, estado SSD transportado, normalização da mistura por enumeração)
76
+ - **Suíte V**: **9/9 bloqueadoras** (V0, V1, V4 matriz+efetivo, V2 paridade, V9 δ_ort do codebook TREINADO, CKPT manifesto, V10 NFE=7/anytime=3, V13 com limiar real, V14 ponte, V5b do config) + V16 aritmética **evidencial** com leitura honesta
77
+ - **Treino real** (Wikipédia PT-BR, 1 200 artigos, janelas completas de 256): fase A perda 9,91→**5,17** (métrica homóloga, mesmo lote/RNG), acc_recon **31,2%**, δ_ort ≤ 9,3e-7, codebook 59,6/64, resíduo algébrico 0,0011; fase B CE 4,10→**3,84** (0,32 nats abaixo do acaso ln 64), resíduo algébrico 0,0002; geração NFE=7 (105 ms), anytime k=3
78
+ - Dois bugs críticos encontrados e corrigidos pelos próprios gates/asserts: `softmax_last_dim` do candle-nn 0.11 é `apply_op1_no_bwd` (**sem gradiente** — teria zerado o treino da mistura e do verificador) e o BPE da v0.1.11 corrompia UTF-8 multibyte no decode (vocab com U+FFFD — escondido pelo teste tautológico A11)
79
 
80
  ## Provas e teoria
81
 
82
+ Toda a fundamentação em `docs/memorias/`: 20 teoremas + 10 corolários NCM×KHTST (n-02/n-03), Teos R.1–R.3 (ADBA), P.1–P.3 (ponte), **M-1/M-8/M-9/C-2/S-1/G-1/T-1/A-1/C-1/R-1** (v0.2/v0.3 — ver r-08/r-09) com rastreabilidade teorema→código→gate.
83
 
84
  ## Convenções herdadas do projeto
85
 
configs/base_rust.json CHANGED
@@ -14,60 +14,46 @@
14
  "k_cabecas": 2,
15
  "beta_entropia": 0.05,
16
  "k_adaptativo": true
17
- }
 
18
  },
19
  "ncm": {
20
  "ativo": true,
21
- "niveis": [
22
- 4,
23
- 8,
24
- 16,
25
- 32,
26
- 64,
27
- 128,
28
- 256
29
- ],
30
  "d_lat": 64,
31
  "k_por_nivel": 64,
32
  "ema_codebook": 0.99,
33
  "beta_commit": 0.25,
34
- "nested_dropout": {
35
- "tipo": "uniforme"
36
- },
37
  "corrupcao_cond": 0.15,
38
  "dropout_cfg": 0.1,
39
  "cfg_scale": 1.0,
40
- "gsq": {
41
- "tau0": 1.0,
42
- "tau_min": 0.5,
43
- "t_alvo": 400
44
- },
45
  "pesos_nivel": "uniforme",
46
  "lambdas": {
47
- "conceito": 0.0,
48
  "rec": 1.0,
49
- "rho_ancora": 0.05
 
50
  },
51
  "temperaturas": {
52
  "auto": true,
53
- "entropia_alvo": null,
54
- "limites": [
55
- 0.5,
56
- 1.5
57
- ]
58
- },
59
- "mbr": {
60
- "n_cand": 1
61
  },
 
62
  "escala_r": "ema_projecao",
63
  "modo_quantizacao": "gsq_rco",
64
- "niveis_token": false
 
65
  },
66
  "adba": {
67
  "ativo": true,
68
  "gamma_ancora": 1.0,
69
  "reponderacao_nivel": true,
70
- "temperatura_nivel": true
 
 
71
  },
72
  "ponte": {
73
  "ativo": true,
@@ -83,14 +69,17 @@
83
  "lote": 4,
84
  "clip": 1.0,
85
  "peso_decaimento": 0.01,
86
- "telemetria_a_cada": 25
 
 
87
  },
88
  "telemetria": {
89
  "jsonl": true,
90
  "deteccao_aprendizado": true,
91
  "ajuste_automatico": true,
92
  "paciencia_plateau": 120,
93
- "fator_lr_plateau": 0.6
 
94
  },
95
  "dados": {
96
  "shard_treino": "dados/shard_treino.jsonl",
@@ -103,4 +92,4 @@
103
  "lambda_jepa": 0.1,
104
  "rm_dois_tempos": true
105
  }
106
- }
 
14
  "k_cabecas": 2,
15
  "beta_entropia": 0.05,
16
  "k_adaptativo": true
17
+ },
18
+ "ssd_delta": true
19
  },
20
  "ncm": {
21
  "ativo": true,
22
+ "niveis": [4, 8, 16, 32, 64, 128, 256],
 
 
 
 
 
 
 
 
23
  "d_lat": 64,
24
  "k_por_nivel": 64,
25
  "ema_codebook": 0.99,
26
  "beta_commit": 0.25,
27
+ "nested_dropout": { "tipo": "uniforme" },
 
 
28
  "corrupcao_cond": 0.15,
29
  "dropout_cfg": 0.1,
30
  "cfg_scale": 1.0,
31
+ "gsq": { "tau0": 1.0, "tau_min": 0.5, "t_alvo": 400 },
 
 
 
 
32
  "pesos_nivel": "uniforme",
33
  "lambdas": {
34
+ "conceito": 1.0,
35
  "rec": 1.0,
36
+ "rho_ancora": 0.05,
37
+ "algebrico": 0.05
38
  },
39
  "temperaturas": {
40
  "auto": true,
41
+ "entropia_alvo": 0.8,
42
+ "limites": [0.5, 1.5]
 
 
 
 
 
 
43
  },
44
+ "mbr": { "n_cand": 1 },
45
  "escala_r": "ema_projecao",
46
  "modo_quantizacao": "gsq_rco",
47
+ "niveis_token": false,
48
+ "mistura_planos": 4
49
  },
50
  "adba": {
51
  "ativo": true,
52
  "gamma_ancora": 1.0,
53
  "reponderacao_nivel": true,
54
+ "temperatura_nivel": true,
55
+ "limite_log_w": 2.0,
56
+ "limite_tau": 1.0
57
  },
58
  "ponte": {
59
  "ativo": true,
 
69
  "lote": 4,
70
  "clip": 1.0,
71
  "peso_decaimento": 0.01,
72
+ "telemetria_a_cada": 25,
73
+ "semente": 317,
74
+ "checkpoint_a_cada": 100
75
  },
76
  "telemetria": {
77
  "jsonl": true,
78
  "deteccao_aprendizado": true,
79
  "ajuste_automatico": true,
80
  "paciencia_plateau": 120,
81
+ "fator_lr_plateau": 0.6,
82
+ "truncar_jsonl": false
83
  },
84
  "dados": {
85
  "shard_treino": "dados/shard_treino.jsonl",
 
92
  "lambda_jepa": 0.1,
93
  "rm_dois_tempos": true
94
  }
95
+ }
dados/bpe.json CHANGED
The diff for this file is too large to render. See raw diff
 
docs/memorias/r-08-auditoria-correcoes-v020.md ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # r-08 — AUDITORIA E CORREÇÕES v0.2.0 (memória do projeto KHTST_RUST)
2
+
3
+ > **Coleção**: `memoria-khtst-rust` · **Objeto**: auditoria do repo `PowerMachine/khtst-rust`
4
+ > (v0.1.11) e o patch set v0.2.0 · **Data**: 2026-10-24 · **Ambiente**: 2 vCPU, 3,0 GiB RAM,
5
+ > sem GPU, Rust 1.99.1, candle 0.11, PyO3 0.23, Python 3.11.2.
6
+
7
+ ---
8
+
9
+ ## 0. TESE (uma frase)
10
+
11
+ **O `khtst-rust` publicado treina um modelo que vaza o futuro.** A máscara block-causal
12
+ deixava as 8 posições de prefixo **totalmente bloqueadas**; o softmax de uma linha
13
+ totalmente bloqueada **não é nulo — é uniforme** (o softmax subtrai o máximo, logo
14
+ `exp(x−max)=1` para toda entrada). O prefixo passava a enxergar a sequência inteira,
15
+ inclusive os **níveis futuros**; e como o prefixo é visível por todos os blocos, existia o
16
+ caminho `nível futuro → prefixo → nível ℓ`. A fatoração next-concept (Teo B.1/D.1) —
17
+ o eixo de toda a teoria do projeto — estava quebrada em todas as passadas de treino
18
+ publicadas, e a suíte de verificação não podia ver isso porque o V4 contava a **matriz**
19
+ da máscara, não o **efeito**.
20
+
21
+ ---
22
+
23
+ ## 1. O GATE (a prova que faltava)
24
+
25
+ A afirmação que precisa ser verificada é uma afirmação sobre **função**, não sobre
26
+ **estrutura**:
27
+
28
+ $$
29
+ \forall m \text{ que altera só } s_{>\ell}:\quad
30
+ \big\|F_\ell(s^{(m)}) - F_\ell(s)\big\|_\infty = 0 ,
31
+ \qquad F_\ell := h_\ell \text{ (saída do nível ℓ)}
32
+ $$
33
+
34
+ | versão | `DIF_VAZAMENTO_NIVEL1` | veredito |
35
+ |---|---|---|
36
+ | 0.1.11 (publicada) | **0,10902858** | ❌ vaza |
37
+ | 0.2.0 (patch) | **0,00000000** | ✅ não vaza |
38
+
39
+ Teste: `work/tests/provas.rs::gate_vazamento_efetivo_zero` (idêntico em
40
+ `testes_comparativos/tests/vazamento.rs`, para rodar em qualquer das duas árvores).
41
+
42
+ **Generalização (regra da casa):** *contar a estrutura não é verificar a função.* O V4
43
+ contava zeros na matriz; o defeito era um softmax degenerado. O mesmo se aplica ao V13
44
+ (`ok: True` incondicional) e ao V5b (constante literal): são verificações que não
45
+ verificam nada. **Regra nova para o projeto: nenhuma verificação V pode ser aceita sem
46
+ um `assert`/comparação que falhe se o invariante quebrar** — e, quando o invariante for
47
+ sobre *informação*, a verificação tem de medir *informação* (perturbar o futuro e medir a
48
+ resposta), não a *representação* do invariante.
49
+
50
+ ---
51
+
52
+ ## 2. As 17 correções (M1…M17) — índice
53
+
54
+ | id | defeito | arquivo:linha (0.1.11) | gravidade | correção |
55
+ |---|---|---|---|---|
56
+ | M1 | prefixo totalmente mascarado ⇒ softmax uniforme ⇒ vazamento | `mascaras.rs:19-33` | 🔴 | prefixo causal + `linhas_sem_visivel()` + `mascara_incremental()` |
57
+ | M2 | âncora/FiLM só no treino | `tronco.rs:135-154` | 🔴 | âncora sobre a **entrada** (idêntica nos 2 caminhos) + gate G2 |
58
+ | M3 | nascimento neutro falso (γ=1) | `tronco.rs:47` | 🔴 | γ=0 (como a spec `scripts.txt`) |
59
+ | M4 | estado do scan SSD descartado | `ssd.rs:105-108`, `tronco.rs:151` | 🔴 | `CustomOp3` com `h₀` + estado transportado |
60
+ | M5 | τ sem limite | `atencao.rs:116` | 🟠 | `τ = exp(T·tanh(log τ))`, T=1 |
61
+ | M6 | `w` com clamp de gradiente morto | `atencao.rs:101-112` | 🟠 | `log w = L_W·tanh(ξ)`, L_W=2, + por (nível, cabeça) |
62
+ | M7 | STE com gradiente nulo | `quantizador.rs:200-204` | 🟠 | `z + sg[r−z]` |
63
+ | M8 | PCGrad decide com ⟨g_i,g_j⟩ obsoleto | `treino.rs:112-138` | 🟠 | recalcular a cada par |
64
+ | M9 | LR ratchet + "rollback" inexistente | `controlador.rs:86` | 🟠 | multiplicador com recuperação |
65
+ | M10 | passo com perda não finita aplicado | `treino.rs:284-287` | 🔴 | descartar o passo **antes** |
66
+ | M11 | semente de RNG colidida entre níveis | `quantizador.rs:139-151` | 🟠 | semente inclui o nível |
67
+ | M12 | telemetria "completa" inexistente | `telemetria.rs:55-63` | 🟠 | séries no JSONL + `ce_nivel_ℓ` |
68
+ | M13 | ponte congela o interpretador (GIL + `send` sem prazo) | `lib.rs:175-179`, `ponte.rs:94,96` | 🔴 | `allow_threads` + `send_timeout` |
69
+ | M14 | "simultaneidade" da ponte = artefato | `ponte.rs:93-116` | 🟠 | interseção real das janelas |
70
+ | M15 | orçamento do KV-cache ignorava o prefixo | `config.rs:336-346` | 🟡 | `N_PREFIXO + Σn_ℓ` = 516 |
71
+ | M16 | matriz de interpolação reconstruída no laço quente | `mascaras.rs:65-107` | 🟡 | memoização thread-local |
72
+ | M17 | decoder AR O(n²) + laços por amostra | `tokenizador.rs:266-295`, `ssd.rs:110` | 🟡 | cache KV + scan batched |
73
+
74
+ Detalhamento e evidência: `relatorio/APRIMORAMENTO_KHTST_RUST.md` e
75
+ `relatorio/A_execucao.md`.
76
+
77
+ ---
78
+
79
+ ## 3. As 6 portas (gates) do patch v0.2.0
80
+
81
+ `G1` vazamento efetivo · `G2` paridade treino↔inferência (nível 1) · `G3` paridade no
82
+ nível 2 **com estado do SSD** · `G4` nenhuma linha totalmente bloqueada · `G5` nascimento
83
+ neutro · `G6` orçamento do KV-cache com prefixo.
84
+
85
+ **24/24 testes verdes** (`cargo test --release`): 15 originais + 3 unitários novos + 6 gates.
86
+
87
+ ---
88
+
89
+ ## 4. Onde a "capacidade de lógica e computação matemática" deve entrar
90
+
91
+ Não na atenção. Argumento (formalizado em `relatorio/C_proposta_matematica.md` §B):
92
+
93
+ $$
94
+ \min_{p\in\mathcal M}\mathrm{KL}(q\|p) = \sum_\ell \mathbb E[\mathrm{TC}(c_\ell\mid c_{<\ell})]
95
+ $$
96
+
97
+ O gap é **invariante** a qualquer mudança na ADBA (a atenção é transporte) e no SSD
98
+ (memória). A *forma funcional* é decidida na **cabeça de conceitos** (produto de
99
+ softmaxes independentes ⇒ `K−1` bits independentes por posição) e num **verificador
100
+ algébrico** da hierarquia. Métodos propostos (com prova e critério de falsificação):
101
+
102
+ 1. **Mistura de planos** na cabeça: `p(c_ℓ|c_{<ℓ}) = Σ_m w_m Π_i q_{m,i}` — contém
103
+ estritamente o produto, preserva MLE exato (B.1), NFE = L (B.2) e o paralelismo
104
+ intra-nível; custo `O(M·n_ℓ·K)`.
105
+ 2. **Verificação algébrica** da hierarquia: `L_recomp = Σ_ℓ ‖Interp(ẑ_{ℓ−1}) − ẑ_{ℓ−1}‖²`
106
+ — quadrático com solução em forma fechada (projeção), auditável.
107
+ 3. **SSD de 2ª ordem / portão dependente do estado** — o slot passa a representar
108
+ *diferenças* (a "derivada de conceito"); adjunto exato validado por diferenças finitas
109
+ (o teste FD do repo foi estendido ao novo gradiente `∂L/∂h₀`).
110
+ 4. **ADBA por (nível, cabeça)** com limite (implementado) — aumenta o poder expressivo da
111
+ ADBA sem tocar na fatoração.
112
+
113
+ ---
114
+
115
+ ## 5. Leitura honesta do que o gerador é
116
+
117
+ Com `gerador.rs:153` (`zhat_anterior = None` no nível 1), `p(c₁)` é **incondicional**:
118
+ o gerador é uma **prior** sobre hierarquias de conceito, e o texto entra só como fonte
119
+ de códigos no treino. As 8 posições de prefixo são **constantes** (8 cópias do embedding
120
+ 0): não transportam condição e consomem 8/516 do orçamento de atenção. Ou lhes dá
121
+ conteúdo (slot de condição), ou as remove. **Não é bug de codificação — é lacuna de
122
+ projeto não declarada.**
123
+
124
+ ---
125
+
126
+ ## 6. Limites desta auditoria
127
+
128
+ - Sem GPU: nada foi testado em NPU/MUSA/DCU (vale a análise estática da matriz de backends).
129
+ - O A/B de treino (120 passos, 495 artigos PT-BR reais) demonstra **efeito mecânico**, não
130
+ desempenho de modelo; o que sustenta a conclusão é o **gate determinístico**.
131
+ - λ (hipergradiente-âncora) e `ncm.ativo`/`lambdas.conceito` continuam **não integrados** —
132
+ declarados, não escondidos.
133
+ - Mistura de planos e verificador algébrico: **especificados com prova, não implementados**.
docs/memorias/r-09-aprimoramentos-v030.md ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # r-09 — APRIMORAMENTOS v0.3.0: MISTURA DE PLANOS, SSD-Δ, VERIFICADOR ALGÉBRICO (memória do projeto KHTST_RUST)
2
+
3
+ > **Coleção**: `memoria-khtst-rust` · **Objeto**: aplicação das correções M1–M17
4
+ > (r-08) + aprimoramentos com prova matemática/lógica/computacional +
5
+ > telemetria completa + treino real + publicação · **Data**: 2026-10-24 ·
6
+ > **Ambiente**: 2 vCPU, 4,0 GiB RAM, sem GPU · Rust 1.99.1 · candle 0.11 ·
7
+ > PyO3 0.23 · Python 3.12 (venv) · maturin 1.15.
8
+
9
+ ---
10
+
11
+ ## 0. TESE (uma frase)
12
+
13
+ A v0.3.0 fecha o vazamento do prefixo e a quebra de paridade da v0.1.11
14
+ (correções M1–M17 da auditoria r-08), adiciona **três aprimoramentos com
15
+ demonstração** — mistura de planos (raciocínio em subníveis paralelos),
16
+ SSD-Δ (percepção de inteiros/floats por acumulação exata) e verificador
17
+ algébrico da hierarquia (quadrática com solução em forma fechada) — e **dois
18
+ bugs críticos adicionais foram descobertos pelos próprios gates** (o
19
+ `softmax_last_dim` do candle-nn 0.11 é `apply_op1_no_bwd` — sem gradiente; e
20
+ o BPE da v0.1.11 corrompia UTF-8 multibyte no decode), validando a regra da
21
+ casa: *verificação que não pode falhar não verifica nada*.
22
+
23
+ ---
24
+
25
+ ## 1. CORREÇÕES APLICADAS (M1–M17 da r-08 + A-série)
26
+
27
+ | id | correção | onde | verificação |
28
+ |---|---|---|---|
29
+ | M1 | prefixo causal na máscara (softmax uniforme ⇒ vazamento do futuro) | `mascaras.rs` | **G1** (efetivo = 0), **G4** |
30
+ | M2 | âncora/FiLM no caminho incremental (buffer pós-bloco-0) | `tronco.rs::forward_nivel` | **G2** (dif_max ≤ 2e-6, 7 níveis) |
31
+ | M3 | γ=0 no nascimento (FiLM identidade exata) | `tronco.rs` | **G5** (bit-a-bit) |
32
+ | M4 | estado SSD transportado entre níveis (h₀ no CustomOp3) | `ssd.rs` | **G3** (scan particionado ≡ completo) |
33
+ | M5/M6 | τ ∈ [e⁻¹,e], log w ∈ [−2,2] por (nível, cabeça), sem clamp morto | `atencao.rs` | **Teo A-1**; vetores via index_select |
34
+ | M7 | STE `z + sg[r−z]` (gradiente unitário) | `quantizador.rs` | teste `ste_gradiente_unitario` |
35
+ | M8 | PCGrad contra gradientes ORIGINAIS | `treino.rs::pcgrad_achatado` | `gate_m8_pcgrad_nao_piora` |
36
+ | M9 | LR: multiplicador com recuperação (fim do ratchet) | `controlador.rs` | teste `multiplicador_recupera` (**Teo C-2**) |
37
+ | M10 | passo não finito descartado ANTES de aplicar | `treino.rs` | (ordem no código) |
38
+ | M11 | semente por nível (ruído Gumbel i.i.d.) | `quantizador.rs` | teste `sementes_por_nivel_distintas` |
39
+ | M12 | **uma linha JSONL por passo com todas as séries** | `telemetria.rs::fechar_passo` | teste `series_vao_para_o_jsonl` |
40
+ | M13 | ponte: `send_timeout` + threads separadas + `py.allow_threads` | `ponte.rs`, `lib.rs` | V14 (200 rodadas, 0 timeouts) |
41
+ | M14 | sobreposição = interseção REAL das janelas | `ponte.rs` | (métrica honesta) |
42
+ | M15 | orçamento KV-cache = N_PREFIXO + Σn_ℓ = **516** | `config.rs` | V1 |
43
+ | M16 | matriz de interpolação memoizada (thread-local) | `mascaras.rs` | teste `interp_memoizada` |
44
+ | M17 | decoder AR com KV-cache O(n); âncora batched; scan rayon | `tokenizador.rs`, `atencao.rs`, `ssd.rs` | geração 105 ms (vs 286 ms v0.1.11) |
45
+ | A19 | RNG global semeado — reprodutibilidade bit-a-bit | `rng.rs` (novo módulo) | **G7** |
46
+ | A20/A21 | checkpoint com manifesto + fsync + rename atômico | `tokenizador.rs::salvar_atomico` | `CKPT_manifesto` (56+102 vars) |
47
+ | A22 | JEPA/SOM/otimizador no `.estado.json` ⇒ **RETOMADA** | `treino.rs::EstadoTreino` | teste manual (400→403 passos) |
48
+ | A25 | `deny_unknown_fields` em TODO o config | `config.rs` | teste `chave_errada_falha` |
49
+ | A26 | chaves_conhecidas derivadas do serde (fonte única) | `diagnostico.rs` | — |
50
+ | A13/A14 | deps declaradas (pyproject) + **removidas** numpy/dict_derive/anyhow/thiserror | `Cargo.toml`, `pyproject.toml` | — |
51
+ | A01–A05 | publicação: allowlist, token com `os.environ.pop`, caminhos relativos | `scripts/42_publicar.py` | — |
52
+ | A06–A12, A15, A27, A37 | scripts 39/40/41 corrigidos (teto passado, callback, split semeado, roundtrip estrito, janelas completas, lint primeiro, V13/V5b reais) | `scripts/` | — |
53
+
54
+ ---
55
+
56
+ ## 2. APRIMORAMENTOS COM PROVA (pedido do usuário)
57
+
58
+ ### 2.1 Mistura de planos — raciocínio em subníveis paralelos (Teo C-1)
59
+
60
+ `p(c_ℓ|c_{<ℓ}) = Σ_{m=1}^{M} w_m(h̄_ℓ) Π_i q_{m,i}(c_{ℓ,i})`, w = softmax(U·h̄_ℓ)
61
+ (h̄_ℓ = média do nível — função de c_{<ℓ} apenas). Implementada em
62
+ `tronco.rs::CabecaMistura` (M=4 no config publicado; M=1 = v0.1.11).
63
+
64
+ **Demonstrações** (r-09 §2 completo no código):
65
+ - **MLE exato**: Σ_c p(c) = Σ_m w_m Π_i Σ_k q = 1 (normalização analítica) —
66
+ verificada por **enumeração exausta** em K^n pequeno (G12: Σ = 1 ± 1e-4);
67
+ - **NFE = L** preservado (a cabeça não avalia o gerador);
68
+ - **amostragem paralela intra-nível**: 1 sorteio de plano + n_ℓ sorteios
69
+ independentes (0 passos sequenciais — a propriedade central do NCM);
70
+ - **contenção estrita**: M=1 ⇒ NLL/posição ≡ CE da cabeça produto (G9,
71
+ diferença < 1e-4 com os MESMOS pesos); com M ≥ 2 a marginal conjunta tem
72
+ covariância intra-nível ≠ 0 ⇒ o gap de correlação total condicional
73
+ (min KL) é redutível;
74
+ - custo O(M·n_ℓ·K) como UM matmul (d, M·K).
75
+
76
+ ### 2.2 SSD-Δ — percepção de inteiros e ponto flutuante (Teo S-1)
77
+
78
+ `h_t = a_t⊙h_{t−1} + (1−a_t+η_t)⊙x_t`, η = tanh(W_ηx̂+b_η), **nascimento
79
+ neutro** (W_η=0, b_η=0 ⇒ η≡0 ⇒ recorrência da v0.1.11 bit-a-bit).
80
+
81
+ - **inteiros**: a=1, η=1 ⇒ h_t = h_{t−1}+x_t — soma acumulada **EXATA**
82
+ para |Σ| < 2^24 (f32 não arredonda) — **G8a verifica bit-a-bit**;
83
+ - **floats**: η = tanh(ρ) ⇒ erro ≤ 2e^{−2ρ}·Σ|x| + (t−1)·ε_mach·Σ|x| —
84
+ **G8b verifica contra a cota**;
85
+ - **contenção estrita** (prova por contradição em t=2: exigiria 0=1);
86
+ - adjunto exato com ∂L/∂h₀ (estado) e ∂L/∂η — **G10 vs diferenças finitas**.
87
+
88
+ ### 2.3 Verificador algébrico da hierarquia (Teo G-1)
89
+
90
+ `L_alg = Σ_{ℓ≥2} ‖Interp_{n_{ℓ−1}}(ẑ_ℓ^{soft}) − ẑ_{ℓ−1}‖²` — o refino do
91
+ nível ℓ, projetado de volta à granularidade ℓ−1, deve **preservar o
92
+ conteúdo grosseiro**. Quadrática nas esperanças de código (soft, via
93
+ `materializar_probs`), minimizador relaxado por equações normais (forma
94
+ fechada), gradiente exato — **G11 vs FD**. Série `residuo_algebrico`
95
+ (falsificável: deve CAIR no treino — medimos 0.0011 na fase A, 0.0002 na B).
96
+
97
+ ### 2.4 ADBA por (nível, cabeça) com limites (Teo A-1)
98
+
99
+ logit = ⟨q,k⟩/(√d_h·τ_{h,λ(i)}) + L_W·tanh(ξ_{h,λ(j)}); ξ=ζ=0 no nascimento
100
+ (w=1, τ=1 — neutro); |log w| ≤ 2, τ ∈ [e⁻¹,e] — sem dead-gradient (tanh tem
101
+ gradiente sech² > 0 em todo R) e sem modo degenerado. Capacidade: R^{H(L+1)}
102
+ = 32 parâmetros/bloco (vs 8 na v0.1.11).
103
+
104
+ ### 2.5 Paralelização (rayon)
105
+
106
+ Scan SSD por elemento do lote (ordem de laço cache-amigável — A31), AdamW
107
+ por variável (A35), ordem canal-interna contígua. Dependências removidas
108
+ conforme decisão do usuário: **numpy (rust-numpy), dict_derive, anyhow,
109
+ thiserror** — a interface `Vec<Vec<u32>>` copia 2,4 MB (desprezível) e o
110
+ rust-numpy seria peso morto.
111
+
112
+ ---
113
+
114
+ ## 3. BUGS NOVOS DESCOBERTOS PELOS GATES (a regra da casa funcionando)
115
+
116
+ 1. **`softmax_last_dim` do candle-nn 0.11 é `apply_op1_no_bwd`** — SEM
117
+ backward. Usado em `pesos_planos` (mistura) e no verificador soft, teria
118
+ **zerado silenciosamente o gradiente** de w e de L_alg. Descoberto pelo
119
+ gate G11 (gradiente ausente); corrigido por `softmax(…, D::Minus1)`.
120
+ 2. **BPE corrompia UTF-8 multibyte**: o vocab inicial usava
121
+ `String::from_utf8_lossy(&[byte])` — bytes 128–255 viravam U+FFFD (3
122
+ bytes) e o decode produzia "c￿lulas" para "células". O teste tautológico
123
+ da v0.1.11 (`or len(rt) > 50`) escondia. Corrigido com mapeamento
124
+ reversível byte↔char (estilo GPT-2) + roundtrip ESTRITO em 3 amostras +
125
+ teste `roundtrip_exato_utf8`.
126
+ 3. **Offset em custom ops**: `as_slice` devolve o storage INTEIRO — layouts
127
+ contíguos COM OFFSET (narrow) eram lidos do início. Descoberto pelo gate
128
+ G3 (scan particionado ≠ completo, dif = 5,27!). Corrigido com
129
+ `contiguous_offsets()` no SSD e no GSQ.
130
+ 4. Máscara da primeira janela incremental: o nível 1 é **bidirecional**
131
+ intra-bloco no treino (Teo D.1(ii)) mas a máscara estava causal — G2
132
+ acustou (dif 0,164). Corrigido.
133
+
134
+ ---
135
+
136
+ ## 4. TREINO REAL (leitura honesta)
137
+
138
+ Dados: Wikipédia PT-BR 20231101 (streaming, tetos do config), 1 200 artigos
139
+ **medidos pós-limpeza** (A09 — a v0.1.11 reportava 6,03 MB medindo texto
140
+ cru; o real pós-limpeza é ~2 MB), split embaralhado semente 1234 (A10),
141
+ BPE 16 384 com **roundtrip exato** (A11), 5 044 janelas completas de 256
142
+ tokens (A15).
143
+
144
+ | métrica | v0.1.11 (r-07) | v0.3.0 |
145
+ |---|---|---|
146
+ | fase A perda (homóloga) | 9,89→8,11 (métricas INCOMPARÁVEIS na v0.1.11 — R-05) | **9,91→5,17** (mesmo lote/RNG) |
147
+ | acc reconstrução | 17,6% | **31,2%** |
148
+ | perplexidade codebook | 52/64 | **59,6/64** |
149
+ | δ_ort em treino | ≤ 9,9e-7 | ≤ 9,3e-7 |
150
+ | resíduo algébrico | (não existia) | 0,0011 (A) → **0,0002** (B) |
151
+ | fase B CE (homóloga) | 4,19→3,98 | **4,10→3,84** (acaso ln64 = 4,16) |
152
+ | geração | 286 ms/2 amostras | **105 ms** (M17) |
153
+ | vazamento efetivo | 0,109 (r-08) | **0,000000** |
154
+ | paridade treino↔inferência | Δ = 2,89 (r-08) | **Δ ≤ 2e-6** |
155
+ | reprodutibilidade | irreproduzível (A19) | bit-a-bit por semente (G7) |
156
+
157
+ Suíte V: **9/9 bloqueadoras** + V16 evidencial. Cargo: **37/37 (release)**.
158
+
159
+ ### V16 — prova aritmética (evidencial, leitura honesta)
160
+
161
+ O MECANISMO é provado deterministicamente (G8a: acumulação inteira exata;
162
+ G8b: float com cota). O probe empírico end-to-end (texto aritmético vs
163
+ controle embaralhado, fine-tune com retomada) mostrou: domínio float
164
+ aprendido (CE 3,61→1,82, −50%) mas **sensibilidade à ordem não emergiu**
165
+ neste orçamento — diagnóstico: o encoder CONGELADO (fase A treinada em
166
+ Wikipédia) produz códigos quase degenerados para texto numérico OOD (o
167
+ probe de inteiros satura em CE = 0 — sem sinal para aprender). Follow-up
168
+ especificado: exposição conjunta das fases A+B a corpora aritméticos.
169
+
170
+ ---
171
+
172
+ ## 5. LIMITES DESTE CICLO
173
+
174
+ 1. Sem GPU; treino de demonstração (400+400 passos, corpus 2 MB).
175
+ 2. `maior_sobreposicao_us = 0` na ponte: envios rápidos demais para a
176
+ resolução de µs com canais de capacidade 8 (a métrica é honesta — a
177
+ v0.1.11 reportava 5 µs de artefato).
178
+ 3. Sensibilidade à ordem aritmética: ver V16 acima.
179
+ 4. A retomada não restaura o RNG de lote (a ordem de lotes pós-retomada
180
+ difere da do run contínuo — documentado).
pyproject.toml CHANGED
@@ -4,10 +4,17 @@ build-backend = "maturin"
4
 
5
  [project]
6
  name = "khtst-rust"
7
- version = "0.1.11"
8
- description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM, provas integradas, ADBA e ponte duplex"
9
  requires-python = ">=3.10"
10
  license = { text = "Apache-2.0" }
 
 
 
 
 
 
 
11
 
12
  [tool.maturin]
13
  python-source = "python"
 
4
 
5
  [project]
6
  name = "khtst-rust"
7
+ version = "0.3.0"
8
+ description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM, provas integradas, ADBA, ponte duplex, mistura de planos, SSD-Δ e verificador algébrico"
9
  requires-python = ">=3.10"
10
  license = { text = "Apache-2.0" }
11
+ # A13 (v0.3.0): dependências DECLARADAS — a v0.1.11 não declarava nenhuma
12
+ # e `pip install .` quebrava os scripts com ModuleNotFoundError.
13
+ dependencies = []
14
+
15
+ [project.optional-dependencies]
16
+ data = ["datasets>=2.14"]
17
+ publish = ["huggingface_hub>=0.20"]
18
 
19
  [tool.maturin]
20
  python-source = "python"
resultados/resumo_treino.json CHANGED
@@ -1,34 +1,20 @@
1
  {
2
  "fases": {
3
- "a": {
4
- "passos": 400,
5
- "perda_inicial": 9.891,
6
- "perda_final": 8.114,
7
- "acc_recon": 0.176,
8
- "delta_ort_max": 9.9e-07,
9
- "perplexidade_codebook": 52.0,
10
- "monotonia_prefixo": 0.5,
11
- "escore_aprendizado": 0.353,
12
- "tempo_s": 337,
13
- "dados": "Wikipedia PT-BR 20231101 (streaming, 246 artigos treino)"
14
- },
15
  "b": {
 
 
 
16
  "passos": 400,
17
- "perda_inicial": 4.189,
18
- "perda_final": 3.975,
19
- "acc_conceitos": 0.063,
20
- "escore_aprendizado": 0.197,
21
- "tempo_s": 223,
22
- "nota": "CE parte de ln(64)=4.16 (quase-uniforme) e cai; acc 6.3% vs 1.6% acaso"
23
  }
24
  },
25
- "geracao": {
26
- "nfe": 7,
27
- "L": 7,
28
- "nfe_anytime_k3": 3,
29
- "tempo_ms_2_amostras": 286,
30
- "texto": "(vazio — decoder AR com orçamento de 7 min ainda não produz texto; infraestrutura validada)"
31
- },
32
- "suite": "7/7 bloqueadoras verdes (V0,V1,V4,V9,V10,V13,V14) + V5b",
33
- "ambiente": "2 CPUs, 3.9GB RAM, Rust 1.99.1, candle 0.11, maturin 1.15"
34
  }
 
1
  {
2
  "fases": {
 
 
 
 
 
 
 
 
 
 
 
 
3
  "b": {
4
+ "acc_conceitos": 0.05078125,
5
+ "escore_aprendizado": 0.16710828471470893,
6
+ "estado_aprendizado": "Estagnado",
7
  "passos": 400,
8
+ "perda_final": 3.841543333871024,
9
+ "perda_inicial": 4.102364335741315,
10
+ "residuo_algebrico": 0.00021079387564289695
 
 
 
11
  }
12
  },
13
+ "versao": "0.3.0 (KHTST_RUST — NCM + provas + ADBA + ponte duplex + mistura de planos + SSD-Δ + verificador algébrico)",
14
+ "dados": {
15
+ "sequencias": 5044,
16
+ "wall_clock": {
17
+ "b": 400
18
+ }
19
+ }
 
 
20
  }
resultados/suite_v.json CHANGED
@@ -1,52 +1,132 @@
1
  {
2
  "V0_modulo": {
3
  "ok": true,
4
- "versao": "0.1.11 (KHTST_RUST — NCM + provas integradas + ADBA + ponte duplex)",
5
  "build": "rustc=rustc 1.99.1 (58cf8f9c3 2026-10-13); perfil=release; features=default, ponte_async, pyo3-async-runtimes, tokio; alvo=x86_64-unknown-linux-gnu"
6
  },
7
  "V1_config": {
8
  "ok": true,
9
- "cache_usado": 508,
10
- "teto": 2048
11
  },
12
  "V4_mascara": {
13
  "ok": true,
14
- "vazamentos": 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  },
16
  "V9_stiefel": {
17
  "ok": true,
18
- "delta_ort": 7.572262864109126e-07
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  },
20
  "V13_mtp_nivel": {
21
  "ok": true,
22
- "ce": 4.147880417960031
 
 
23
  },
24
  "V10_nfe": {
25
  "ok": true,
26
  "nfe": 7,
27
  "L": 7,
28
  "nfe_anytime": 3,
29
- "tempo_ms": 152.015399
 
30
  },
31
  "V14_ponte": {
32
  "ok": true,
33
  "envios_a_para_b": 200,
34
  "envios_b_para_a": 200,
35
- "maior_sobreposicao_us": 5,
36
  "quedas_por_timeout": 0,
37
  "recebidos_a": 200,
38
  "recebidos_b": 200,
39
  "rodadas_completas": 200,
40
- "sobrecarga_media_us": 0.019999999999999997,
41
- "tempo_total_ms": 0.095628
42
  },
43
  "V5b_corrupcao": {
44
  "ok": true,
45
- "epsilon": 0.15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  },
47
  "VEREDITO": {
48
  "bloqueadoras_ok": true,
49
- "n_verdes": 7,
50
- "n_total": 7
51
  }
52
  }
 
1
  {
2
  "V0_modulo": {
3
  "ok": true,
4
+ "versao": "0.3.0 (KHTST_RUST — NCM + provas + ADBA + ponte duplex + mistura de planos + SSD-Δ + verificador algébrico)",
5
  "build": "rustc=rustc 1.99.1 (58cf8f9c3 2026-10-13); perfil=release; features=default, ponte_async, pyo3-async-runtimes, tokio; alvo=x86_64-unknown-linux-gnu"
6
  },
7
  "V1_config": {
8
  "ok": true,
9
+ "cache_usado": 516,
10
+ "esperado": 516
11
  },
12
  "V4_mascara": {
13
  "ok": true,
14
+ "vazamentos_matriz": 0,
15
+ "vazamento_efetivo": 0.0
16
+ },
17
+ "V2_paridade": {
18
+ "ok": true,
19
+ "dif_max": 1.9073486328125e-06,
20
+ "difs_por_nivel": [
21
+ 1.475214958190918e-06,
22
+ 1.430511474609375e-06,
23
+ 1.5497207641601562e-06,
24
+ 1.4901161193847656e-06,
25
+ 1.430511474609375e-06,
26
+ 1.430511474609375e-06,
27
+ 1.9073486328125e-06
28
+ ]
29
  },
30
  "V9_stiefel": {
31
  "ok": true,
32
+ "delta_ort_inicial": 8.806490618826789e-07,
33
+ "delta_ort_treinado_max": 9.192185067795031e-07,
34
+ "delta_ort_por_nivel": [
35
+ 7.903236855012139e-07,
36
+ 9.133550276453174e-07,
37
+ 8.543774004848249e-07,
38
+ 8.369262253733205e-07,
39
+ 8.311674723238363e-07,
40
+ 8.339327702252286e-07,
41
+ 9.192185067795031e-07
42
+ ]
43
+ },
44
+ "CKPT_manifesto": {
45
+ "ok": true,
46
+ "tokenizador": {
47
+ "caminho": "/home/z/my-project/khtst-rust/resultados/tokenizador_conceitos.json",
48
+ "consistente": true,
49
+ "formato": "khtst-ckpt-v2",
50
+ "n_vars_declarado": 56,
51
+ "n_vars_presentes": 56,
52
+ "tipo": "tokenizador"
53
+ },
54
+ "gerador": {
55
+ "caminho": "/home/z/my-project/khtst-rust/resultados/gerador_conceitos.json",
56
+ "consistente": true,
57
+ "formato": "khtst-ckpt-v2",
58
+ "n_vars_declarado": 102,
59
+ "n_vars_presentes": 102,
60
+ "tipo": "gerador"
61
+ }
62
  },
63
  "V13_mtp_nivel": {
64
  "ok": true,
65
+ "ce_inicial": 4.098862443651472,
66
+ "ce_final": 2.8546416418892995,
67
+ "delta": 1.244220801762172
68
  },
69
  "V10_nfe": {
70
  "ok": true,
71
  "nfe": 7,
72
  "L": 7,
73
  "nfe_anytime": 3,
74
+ "tempo_ms": 103.194721,
75
+ "residuo_algebrico_geracao": 0.0002442587283439934
76
  },
77
  "V14_ponte": {
78
  "ok": true,
79
  "envios_a_para_b": 200,
80
  "envios_b_para_a": 200,
81
+ "maior_sobreposicao_us": 0,
82
  "quedas_por_timeout": 0,
83
  "recebidos_a": 200,
84
  "recebidos_b": 200,
85
  "rodadas_completas": 200,
86
+ "sobrecarga_media_us": 41.68500000000001,
87
+ "tempo_total_ms": 8.514808
88
  },
89
  "V5b_corrupcao": {
90
  "ok": true,
91
+ "epsilon_do_config": 0.15
92
+ },
93
+ "V16_aritmetica": {
94
+ "ok": null,
95
+ "evidencial": true,
96
+ "mecanismo_provado_por": "gates G8a/G8b (tests/provas.rs) — Teo S-1",
97
+ "ce_inteira_estruturada_antes": 0.0,
98
+ "ce_inteira_estruturada_depois": 0.0,
99
+ "ce_inteira_embaralhada_depois": 0.0,
100
+ "ganho_inteira": 0.0,
101
+ "ce_float_estruturada_antes": 3.60884758404323,
102
+ "ce_float_estruturada_depois": 1.8170065255392167,
103
+ "ce_float_embaralhada_depois": 1.795043999240512,
104
+ "ganho_float": -0.021962526298704654,
105
+ "leitura": "domínio float aprendido (CE cai ~50%); ordem exige encoder exposto a aritmética (fase A conjunta) — inteiros saturam (códigos degenerados do encoder congelado, CE=0)"
106
+ },
107
+ "treino": {
108
+ "fases": {
109
+ "b": {
110
+ "acc_conceitos": 0.05078125,
111
+ "escore_aprendizado": 0.16710828471470893,
112
+ "estado_aprendizado": "Estagnado",
113
+ "passos": 400,
114
+ "perda_final": 3.841543333871024,
115
+ "perda_inicial": 4.102364335741315,
116
+ "residuo_algebrico": 0.00021079387564289695
117
+ }
118
+ },
119
+ "versao": "0.3.0 (KHTST_RUST — NCM + provas + ADBA + ponte duplex + mistura de planos + SSD-Δ + verificador algébrico)",
120
+ "dados": {
121
+ "sequencias": 5044,
122
+ "wall_clock": {
123
+ "b": 400
124
+ }
125
+ }
126
  },
127
  "VEREDITO": {
128
  "bloqueadoras_ok": true,
129
+ "n_verdes": 9,
130
+ "n_total": 9
131
  }
132
  }
resultados/telemetria_fase_a.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
resultados/telemetria_fase_b.jsonl CHANGED
The diff for this file is too large to render. See raw diff
 
scripts/39_coletar_dados.py CHANGED
@@ -1,14 +1,31 @@
1
  #!/usr/bin/env python3
2
  """39_coletar_dados.py — Streaming CONTROLADO de dados reais (Wikipédia PT-BR).
3
 
 
 
 
 
 
 
 
 
4
  Tetos duros (não satura o ambiente): max_artigos e max_bytes do config.
5
  Salva shards JSONL locais (treino/validação 90/10) + treina o BPE do
6
- KHTST_RUST (vocab-alvo do config) e salva em dados/bpe.json.
7
- Nada além dos tetos é baixado; sem cache global do datasets."""
8
- import json, os, sys, time
9
 
10
  TETO_PADRAO_ARTIGOS = 1200
11
  TETO_PADRAO_BYTES = 6_000_000
 
 
 
 
 
 
 
 
 
 
12
 
13
  def main():
14
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -20,28 +37,32 @@ def main():
20
 
21
  from datasets import load_dataset
22
  t0 = time.time()
23
- ds = load_dataset(
24
- "wikimedia/wikipedia", "20231101.pt", split="train", streaming=True,
25
- )
26
  textos, total_bytes = [], 0
27
  for ex in ds:
28
  t = (ex.get("text") or "").strip()
29
  if len(t) < 200: # descarta esboços mínimos
30
  continue
 
 
 
 
 
 
 
 
 
 
31
  textos.append(t)
32
  total_bytes += len(t.encode("utf-8"))
33
  if len(textos) % 200 == 0:
34
  print(f" ... {len(textos)} artigos, {total_bytes/1e6:.2f} MB", flush=True)
35
- if len(textos) >= max_artigos or total_bytes >= max_bytes:
36
- break
37
- print(f"coletado: {len(textos)} artigos, {total_bytes/1e6:.2f} MB em {time.time()-t0:.1f}s")
38
-
39
- # normalização leve: primeiros parágrafos relevantes (descarta infobox ruidosa)
40
- def limpa(t: str) -> str:
41
- partes = [p.strip() for p in t.split("\n") if len(p.strip()) > 80]
42
- return " ".join(partes[:6])[:1800]
43
 
44
- textos = [limpa(t) for t in textos]
 
 
45
  n_val = max(1, len(textos) // 10)
46
  val, treino = textos[:n_val], textos[n_val:]
47
  with open(os.path.join(pasta, "dados", "shard_treino.jsonl"), "w") as f:
@@ -50,7 +71,8 @@ def main():
50
  with open(os.path.join(pasta, "dados", "shard_validacao.jsonl"), "w") as f:
51
  for t in val:
52
  f.write(json.dumps({"texto": t}, ensure_ascii=False) + "\n")
53
- print(f"shards: treino={len(treino)} val={len(val)}")
 
54
 
55
  # BPE do KHTST_RUST
56
  import khtst_rust as K
@@ -59,10 +81,18 @@ def main():
59
  bpe = K.Bpe.treinar(treino, vocab_alvo)
60
  bpe.salvar(os.path.join(pasta, "dados", "bpe.json"))
61
  print(f"BPE: vocab={bpe.n_vocab} (alvo {vocab_alvo}) em {time.time()-t0:.1f}s")
62
- amostra = treino[0][:120]
63
- ids = bpe.codificar(amostra)
64
- rt = bpe.decodificar(ids)
65
- print("roundtrip OK:", rt[:80] == amostra[:80].strip()[:80] or len(rt) > 50)
 
 
 
 
 
66
 
67
  if __name__ == "__main__":
 
68
  main()
 
 
 
1
  #!/usr/bin/env python3
2
  """39_coletar_dados.py — Streaming CONTROLADO de dados reais (Wikipédia PT-BR).
3
 
4
+ v0.3.0 (correções da auditoria D):
5
+ - A09: os bytes são medidos DEPOIS da limpeza (a v0.1.11 media o texto cru e
6
+ o corpus ficava ~3× abaixo do orçamento);
7
+ - A10: split treino/val EMBARALHADO com semente fixa (a v0.1.11 usava
8
+ prefixo-vs-sufixo do dump — viés de posição);
9
+ - A11: roundtrip ESTRITO do BPE (a v0.1.11 aceitava `or len(rt) > 50`);
10
+ - tetos checados ANTES de anexar.
11
+
12
  Tetos duros (não satura o ambiente): max_artigos e max_bytes do config.
13
  Salva shards JSONL locais (treino/validação 90/10) + treina o BPE do
14
+ KHTST_RUST (vocab-alvo do config) e salva em dados/bpe.json."""
15
+ import json, os, random, sys, time
 
16
 
17
  TETO_PADRAO_ARTIGOS = 1200
18
  TETO_PADRAO_BYTES = 6_000_000
19
+ SEMENTE_SPLIT = 1234 # A10: reprodutível e documentada
20
+
21
+
22
+ def limpa(t: str) -> str:
23
+ """normalização leve: primeiros parágrafos relevantes (descarta infobox).
24
+ Espaço em branco COLAPSADO (o BPE divide por split_whitespace — o
25
+ roundtrip estrito A11 exige texto sem tab/quebra/múltiplos espaços)."""
26
+ partes = [p.strip() for p in t.split("\n") if len(p.strip()) > 80]
27
+ return " ".join(" ".join(partes[:6]).split())[:1800]
28
+
29
 
30
  def main():
31
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
37
 
38
  from datasets import load_dataset
39
  t0 = time.time()
40
+ ds = load_dataset("wikimedia/wikipedia", "20231101.pt", split="train", streaming=True)
 
 
41
  textos, total_bytes = [], 0
42
  for ex in ds:
43
  t = (ex.get("text") or "").strip()
44
  if len(t) < 200: # descarta esboços mínimos
45
  continue
46
+ # A09: mede o texto LIMPO (o que será gravado)
47
+ t = limpa(t)
48
+ if not t:
49
+ continue
50
+ if total_bytes + len(t.encode("utf-8")) > max_bytes or len(textos) >= max_artigos:
51
+ # A-correção: testa ANTES de anexar (não ultrapassa o teto)
52
+ if total_bytes + len(t.encode("utf-8")) > max_bytes and total_bytes > 0:
53
+ break
54
+ if len(textos) >= max_artigos:
55
+ break
56
  textos.append(t)
57
  total_bytes += len(t.encode("utf-8"))
58
  if len(textos) % 200 == 0:
59
  print(f" ... {len(textos)} artigos, {total_bytes/1e6:.2f} MB", flush=True)
60
+ print(f"coletado: {len(textos)} artigos, {total_bytes/1e6:.2f} MB (medidos PÓS-limpeza) "
61
+ f"em {time.time()-t0:.1f}s")
 
 
 
 
 
 
62
 
63
+ # A10: split embaralhado com semente fixa (sem viés de posição)
64
+ rng = random.Random(SEMENTE_SPLIT)
65
+ rng.shuffle(textos)
66
  n_val = max(1, len(textos) // 10)
67
  val, treino = textos[:n_val], textos[n_val:]
68
  with open(os.path.join(pasta, "dados", "shard_treino.jsonl"), "w") as f:
 
71
  with open(os.path.join(pasta, "dados", "shard_validacao.jsonl"), "w") as f:
72
  for t in val:
73
  f.write(json.dumps({"texto": t}, ensure_ascii=False) + "\n")
74
+ print(f"shards (split embaralhado, semente {SEMENTE_SPLIT}): "
75
+ f"treino={len(treino)} val={len(val)}")
76
 
77
  # BPE do KHTST_RUST
78
  import khtst_rust as K
 
81
  bpe = K.Bpe.treinar(treino, vocab_alvo)
82
  bpe.salvar(os.path.join(pasta, "dados", "bpe.json"))
83
  print(f"BPE: vocab={bpe.n_vocab} (alvo {vocab_alvo}) em {time.time()-t0:.1f}s")
84
+ # A11: roundtrip ESTRITO — o BPE byte-level é exato
85
+ for amostra in treino[:3]:
86
+ ids = bpe.codificar(amostra)
87
+ rt = bpe.decodificar(ids)
88
+ assert rt == amostra.strip(), (
89
+ f"roundtrip BPE falhou: {len(rt)} vs {len(amostra.strip())} chars"
90
+ )
91
+ print("roundtrip OK: True (igualdade estrita em 3 amostras)")
92
+
93
 
94
  if __name__ == "__main__":
95
+ import os as _os, sys as _sys
96
  main()
97
+ _sys.stdout.flush(); _sys.stderr.flush()
98
+ _os._exit(0)
scripts/40_treinar.py CHANGED
@@ -1,65 +1,104 @@
1
  #!/usr/bin/env python3
2
  """40_treinar.py — Fases A (tokenizador) e B (gerador) do KHTST_RUST.
3
 
4
- Uso: python3 scripts/40_treinar.py [--fase a|b|ab] [--teto_min N]
 
 
 
 
 
 
 
 
 
 
 
5
  Protocolo (n-04 §4): A = nested dropout + commitment + GSQ/RCO Stiefel;
6
- B = MLE exato sobre hierarquias congeladas + corrupção κ + MTP + JEPA + PCGrad.
7
- Teto de tempo wall-clock (leitura honesta): para no limite e reporta."""
8
  import argparse, json, os, sys, time
9
 
 
10
  def carregar_sequencias(pasta, bpe, qual):
11
- import khtst_rust as K
12
- b = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
 
 
13
  seqs = []
14
  with open(os.path.join(pasta, "dados", f"shard_{qual}.jsonl")) as f:
15
  for linha in f:
16
- t = json.loads(linha)["texto"]
17
- ids = b.codificar(t)
18
- # janelas de 256 tokens (ctx) — deslocamento 128 (50% sobreposição)
19
- for i in range(0, max(1, len(ids) - 256), 128):
20
- seqs.append(ids[i:i + 256])
21
  return seqs
22
 
 
 
 
 
 
23
  def main():
24
  ap = argparse.ArgumentParser()
25
  ap.add_argument("--fase", default="ab", choices=["a", "b", "ab"])
26
  ap.add_argument("--teto_min", type=float, default=10.0)
 
 
27
  args = ap.parse_args()
28
 
29
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30
- sys.path.insert(0, pasta)
 
31
  os.makedirs(os.path.join(pasta, "resultados"), exist_ok=True)
32
- import khtst_rust as K
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- cfg = K.Config(os.path.join(pasta, "configs", "base_rust.json"))
 
35
  bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
36
 
37
- # ajusta passos ao teto de tempo: estimativa por benchmark rápido
38
- relatorio = {"fases": {}}
39
 
40
  if args.fase in ("a", "ab"):
41
  print("=== FASE A — tokenizador de conceitos (GSQ+RCO Stiefel) ===", flush=True)
42
  seqs = carregar_sequencias(pasta, bpe, "treino")
43
- print(f"sequências de treino: {len(seqs)} (ctx=256)", flush=True)
44
- cb = None
45
  t0 = time.time()
46
  saida = K.treinar_fase_a(
47
  cfg, seqs,
48
  caminho_telemetria=os.path.join(pasta, "resultados", "telemetria_fase_a.jsonl"),
49
  caminho_checkpoint=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
 
 
 
50
  )
51
  dt = time.time() - t0
52
  r = saida["resumo"]
53
  relatorio["fases"]["a"] = r
54
  print(json.dumps(r, indent=1, ensure_ascii=False))
55
- print(f"fase A: {dt:.0f}s | Δperda {r['perda_inicial']:.3f}→{r['perda_final']:.3f} "
56
- f"| acc_recon {r['acc_recon']:.3f} | δ_ort_max {r['delta_ort_max']:.2e} "
 
57
  f"| perp_codebook {r['perplexidade_codebook']:.1f} "
58
  f"| monotonia_prefixo {r['monotonia_prefixo']:.2f} "
59
- f"| escore_aprendizado {r['escore_aprendizado']:.3f}", flush=True)
 
60
 
61
  if args.fase in ("b", "ab"):
62
- print("=== FASE B — gerador next-concept (MLE exato + κ + MTP + JEPA + PCGrad) ===", flush=True)
63
  seqs = carregar_sequencias(pasta, bpe, "treino")
64
  t0 = time.time()
65
  saida = K.treinar_fase_b(
@@ -67,18 +106,32 @@ def main():
67
  caminho_tokenizador=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
68
  caminho_telemetria=os.path.join(pasta, "resultados", "telemetria_fase_b.jsonl"),
69
  caminho_checkpoint=os.path.join(pasta, "resultados", "gerador_conceitos.json"),
 
 
 
70
  )
71
  dt = time.time() - t0
72
  r = saida["resumo"]
73
  relatorio["fases"]["b"] = r
74
  print(json.dumps(r, indent=1, ensure_ascii=False))
75
- print(f"fase B: {dt:.0f}s | ΔCE {r['perda_inicial']:.3f}→{r['perda_final']:.3f} "
76
- f"| acc_conceitos {r['acc_conceitos']:.3f} "
77
- f"| escore_aprendizado {r['escore_aprendizado']:.3f}", flush=True)
 
 
 
78
 
 
 
 
 
79
  with open(os.path.join(pasta, "resultados", "resumo_treino.json"), "w") as f:
80
  json.dump(relatorio, f, indent=1, ensure_ascii=False)
81
  print("resumo salvo em resultados/resumo_treino.json")
82
 
 
83
  if __name__ == "__main__":
 
84
  main()
 
 
 
1
  #!/usr/bin/env python3
2
  """40_treinar.py — Fases A (tokenizador) e B (gerador) do KHTST_RUST.
3
 
4
+ v0.3.0 (correções da auditoria D + proteção contra kill do background):
5
+ - A06: `--teto_min` é REALMENTE passado como teto_seg (a v0.1.11 parseava
6
+ e descartava);
7
+ - A07: callback de progresso ativo (1 linha a cada telemetria_a_cada);
8
+ - A08: o BPE carregado é reaproveitado (sem releitura dupla);
9
+ - A12: sys.path aponta para python/ (o insert da v0.1.11 era inócuo);
10
+ - A27: lint_config executado no início (diagnóstico de digitação);
11
+ - RETOMADA: `--retomar` carrega `.estado.json` ao lado do checkpoint —
12
+ o ambiente pode encerrar processos em background sem erro/OOM, e o treino
13
+ continua de onde parou (checkpoint periódico a cada `checkpoint_a_cada`).
14
+
15
+ Uso: python3 scripts/40_treinar.py [--fase a|b|ab] [--teto_min N] [--retomar]
16
  Protocolo (n-04 §4): A = nested dropout + commitment + GSQ/RCO Stiefel;
17
+ B = MLE exato (mistura de planos) + κ + MTP + JEPA + L_alg + PCGrad."""
 
18
  import argparse, json, os, sys, time
19
 
20
+
21
  def carregar_sequencias(pasta, bpe, qual):
22
+ """Janelas de 256 tokens (ctx) — deslocamento 128 (50% sobreposição).
23
+ A15: apenas janelas COMPLETAS (a v0.1.11 aceitava janelas curtas
24
+ preenchidas com PAD — até 96% de padding no lote)."""
25
+ ctx = 256
26
  seqs = []
27
  with open(os.path.join(pasta, "dados", f"shard_{qual}.jsonl")) as f:
28
  for linha in f:
29
+ ids = bpe.codificar(json.loads(linha)["texto"])
30
+ for i in range(0, len(ids) - ctx + 1, 128):
31
+ janela = ids[i:i + ctx]
32
+ if len(janela) == ctx: # A15
33
+ seqs.append(janela)
34
  return seqs
35
 
36
+
37
+ def callback_progresso(d):
38
+ print(" [telemetria] " + json.dumps(d, ensure_ascii=False), flush=True)
39
+
40
+
41
  def main():
42
  ap = argparse.ArgumentParser()
43
  ap.add_argument("--fase", default="ab", choices=["a", "b", "ab"])
44
  ap.add_argument("--teto_min", type=float, default=10.0)
45
+ ap.add_argument("--retomar", action="store_true",
46
+ help="retoma do checkpoint (.estado.json) se existir")
47
  args = ap.parse_args()
48
 
49
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
50
+ # A12: prefere o WHEEL INSTALADO; se não houver, usa a árvore python/
51
+ # (layout misto do maturin — o insert cego da v0.1.11 sombreava o wheel)
52
  os.makedirs(os.path.join(pasta, "resultados"), exist_ok=True)
53
+ try:
54
+ import khtst_rust as K
55
+ except ImportError:
56
+ sys.path.insert(0, os.path.join(pasta, "python"))
57
+ import khtst_rust as K
58
+
59
+ caminho_cfg = os.path.join(pasta, "configs", "base_rust.json")
60
+ # A27: lint de digitação PRIMEIRO (causa comum de erro em refatoração)
61
+ avisos = K.lint_config(caminho_cfg)
62
+ if avisos:
63
+ for a in avisos:
64
+ print(f" [lint] {a['chave']} → sugestão: {a.get('sugestao')} "
65
+ f"({a['causa']})", flush=True)
66
+ else:
67
+ print(" [lint] config sem avisos de digitação", flush=True)
68
 
69
+ cfg = K.Config(caminho_cfg)
70
+ teto_seg = args.teto_min * 60.0 # A06: teto é PASSADO (não descartado)
71
  bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
72
 
73
+ relatorio = {"fases": {}, "versao": K.versao()}
 
74
 
75
  if args.fase in ("a", "ab"):
76
  print("=== FASE A — tokenizador de conceitos (GSQ+RCO Stiefel) ===", flush=True)
77
  seqs = carregar_sequencias(pasta, bpe, "treino")
78
+ print(f"sequências de treino: {len(seqs)} (ctx=256, janelas completas)", flush=True)
 
79
  t0 = time.time()
80
  saida = K.treinar_fase_a(
81
  cfg, seqs,
82
  caminho_telemetria=os.path.join(pasta, "resultados", "telemetria_fase_a.jsonl"),
83
  caminho_checkpoint=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
84
+ callback=callback_progresso, # A07
85
+ teto_seg=teto_seg, # A06
86
+ retomar=args.retomar,
87
  )
88
  dt = time.time() - t0
89
  r = saida["resumo"]
90
  relatorio["fases"]["a"] = r
91
  print(json.dumps(r, indent=1, ensure_ascii=False))
92
+ print(f"fase A: {dt:.0f}s | perda {r['perda_inicial']:.3f}→{r['perda_final']:.3f} "
93
+ f"(métrica homóloga: CE+β·commit no MESMO lote)"
94
+ f" | acc_recon {r['acc_recon']:.3f} | δ_ort_max {r['delta_ort_max']:.2e} "
95
  f"| perp_codebook {r['perplexidade_codebook']:.1f} "
96
  f"| monotonia_prefixo {r['monotonia_prefixo']:.2f} "
97
+ f"| residuo_algebrico {r['residuo_algebrico']:.4f} "
98
+ f"| estado: {r['estado_aprendizado']}", flush=True)
99
 
100
  if args.fase in ("b", "ab"):
101
+ print("=== FASE B — gerador next-concept (mistura de planos + SSD-Δ + L_alg) ===", flush=True)
102
  seqs = carregar_sequencias(pasta, bpe, "treino")
103
  t0 = time.time()
104
  saida = K.treinar_fase_b(
 
106
  caminho_tokenizador=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
107
  caminho_telemetria=os.path.join(pasta, "resultados", "telemetria_fase_b.jsonl"),
108
  caminho_checkpoint=os.path.join(pasta, "resultados", "gerador_conceitos.json"),
109
+ callback=callback_progresso, # A07
110
+ teto_seg=teto_seg, # A06
111
+ retomar=args.retomar,
112
  )
113
  dt = time.time() - t0
114
  r = saida["resumo"]
115
  relatorio["fases"]["b"] = r
116
  print(json.dumps(r, indent=1, ensure_ascii=False))
117
+ print(f"fase B: {dt:.0f}s | CE {r['perda_inicial']:.3f}→{r['perda_final']:.3f} "
118
+ f"(mesmo lote/RNG — métrica homóloga)"
119
+ f" | acc_conceitos {r['acc_conceitos']:.3f} "
120
+ f"| residuo_algebrico {r['residuo_algebrico']:.4f} "
121
+ f"| escore_aprendizado {r['escore_aprendizado']:.3f} "
122
+ f"| estado: {r['estado_aprendizado']}", flush=True)
123
 
124
+ relatorio["dados"] = {
125
+ "sequencias": len(seqs),
126
+ "wall_clock": {f: relatorio["fases"][f].get("passos") for f in relatorio["fases"]},
127
+ }
128
  with open(os.path.join(pasta, "resultados", "resumo_treino.json"), "w") as f:
129
  json.dump(relatorio, f, indent=1, ensure_ascii=False)
130
  print("resumo salvo em resultados/resumo_treino.json")
131
 
132
+
133
  if __name__ == "__main__":
134
+ import os as _os, sys as _sys
135
  main()
136
+ _sys.stdout.flush(); _sys.stderr.flush()
137
+ _os._exit(0)
scripts/41_teste.py CHANGED
@@ -1,65 +1,131 @@
1
  #!/usr/bin/env python3
2
- """41_teste.py — Suíte de verificação do KHTST_RUST (adaptação da V-série
3
- do n-04 §6 ao port Rust). Bloqueadoras: V0,V1,V4,V5,V9,V10,V13,V14.
4
- Evidenciais (reportar): V2,V3,V5b,V6,V8,V11,V15."""
5
- import json, os, sys, time
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  def main():
8
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
- import khtst_rust as K
10
- cfg = K.Config(os.path.join(pasta, "configs", "base_rust.json"))
 
 
 
 
 
 
 
11
  res = {}
12
 
13
  # V0 — módulo carrega e reporta build
14
  res["V0_modulo"] = {"ok": True, "versao": K.versao(), "build": K.info_build()}
15
 
16
- # V1 — config válida + orçamento de cache (T2)
17
  try:
18
  uso = cfg.checar_orcamento_cache()
19
- res["V1_config"] = {"ok": True, "cache_usado": uso, "teto": 2048}
20
  except Exception as e:
21
  res["V1_config"] = {"ok": False, "erro": str(e)}
22
 
23
- # V4 — máscara block-causal sem vazamento (Teo D.1)
24
- res["V4_mascara"] = {"ok": K.verificar_mascara(cfg) == 0, "vazamentos": K.verificar_mascara(cfg)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # V9 — δ_ort do Stiefel (Teo 32.11 / gate ≤ 3e-6)
27
- d = K.verificar_stiefel(64, 64)
28
- res["V9_stiefel"] = {"ok": d <= 3e-6, "delta_ort": d}
 
 
 
 
 
 
 
29
 
30
- # V13 — MTP ⊂ NCM: perda B carrega com MTP ativo (equivalência estrutural)
 
31
  try:
32
  bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
33
  seqs = []
34
  with open(os.path.join(pasta, "dados", "shard_validacao.jsonl")) as f:
35
  for linha in f:
36
  ids = bpe.codificar(json.loads(linha)["texto"])
37
- for i in range(0, max(1, len(ids) - 256), 128):
38
- seqs.append(ids[i:i + 256])
 
 
39
  s = K.treinar_fase_b(cfg, seqs[:16],
40
- caminho_tokenizador=os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
41
- teto_seg=6.0)
42
- res["V13_mtp_nivel"] = {"ok": True, "ce": s["resumo"]["perda_final"]}
 
 
 
 
 
 
43
  except Exception as e:
44
  res["V13_mtp_nivel"] = {"ok": False, "erro": str(e)}
45
 
46
  # V10 — NFE = L e anytime (Teo B.2/B.4)
47
  try:
48
- g = K.gerar_amostras(cfg,
49
- os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
50
- os.path.join(pasta, "resultados", "gerador_conceitos.json"),
51
- n_amostras=1)
52
- g3 = K.gerar_amostras(cfg,
53
- os.path.join(pasta, "resultados", "tokenizador_conceitos.json"),
54
- os.path.join(pasta, "resultados", "gerador_conceitos.json"),
55
- n_amostras=1, parar_no_nivel=3)
56
  res["V10_nfe"] = {"ok": g["nfe"] == 7 and g3["nfe"] == 3,
57
  "nfe": g["nfe"], "L": g["l"], "nfe_anytime": g3["nfe"],
58
- "tempo_ms": g["tempo_ms"]}
 
59
  except Exception as e:
60
  res["V10_nfe"] = {"ok": False, "erro": str(e)}
61
 
62
- # V14 — ponte duplex: rodadas sem deadlock + estatísticas simétricas
63
  try:
64
  p = K.PonteDupla(8, 800)
65
  est = p.demo_professor_aluno(200, 24)
@@ -70,17 +136,125 @@ def main():
70
  except Exception as e:
71
  res["V14_ponte"] = {"ok": False, "erro": str(e)}
72
 
73
- # V5b — corrupção κ dentro do intervalo (C.3)
74
- res["V5b_corrupcao"] = {"ok": 0.0 <= 0.15 <= 1.0, "epsilon": 0.15}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- # resumo de treino (V2/V3/V6/V11/V15 vêm da telemetria salva)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  try:
78
  with open(os.path.join(pasta, "resultados", "resumo_treino.json")) as f:
79
  res["treino"] = json.load(f)
80
  except Exception:
81
  pass
82
 
83
- bloqueadoras = ["V0_modulo", "V1_config", "V4_mascara", "V9_stiefel", "V10_nfe", "V13_mtp_nivel", "V14_ponte"]
 
 
84
  todas_ok = all(res.get(v, {}).get("ok") for v in bloqueadoras)
85
  res["VEREDITO"] = {"bloqueadoras_ok": todas_ok,
86
  "n_verdes": sum(1 for v in bloqueadoras if res.get(v, {}).get("ok")),
@@ -90,8 +264,13 @@ def main():
90
  print(json.dumps(res["VEREDITO"], indent=1))
91
  for k, v in res.items():
92
  if isinstance(v, dict) and "ok" in v:
93
- print(("✓" if v["ok"] else "✗"), k, {x: y for x, y in v.items() if x != "ok" and x != "build"})
 
94
  return 0 if todas_ok else 1
95
 
 
96
  if __name__ == "__main__":
97
- sys.exit(main())
 
 
 
 
1
  #!/usr/bin/env python3
2
+ """41_teste.py — Suíte de verificação do KHTST_RUST v0.3.0.
3
+
4
+ v0.3.0 — TODA verificação tem um assert que FALHA se o invariante quebrar
5
+ (regra da casa r-08 §1: "contar a estrutura não é verificar a função"):
6
+ - V4' VAZAMENTO EFETIVO (gate G1): perturba o futuro e mede a resposta do
7
+ nível 1 — a v0.1.11 media a MATRIZ e reportava 0 com vazamento real
8
+ de 110% do sinal;
9
+ - V2' PARIDADE treino↔inferência (gate G2): forward completo ≡ incremental;
10
+ - V9' δ_ort do codebook TREINADO (a v0.1.11 media um codebook recém-inicializado);
11
+ - V13 MTP⊂NCM com LIMIAR (ce_final < ce_inicial — a v0.1.11 gravava ok:True
12
+ incondicional);
13
+ - V5b ε lido do CONFIG (a v0.1.11 usava constante literal);
14
+ - V16 PROVA ARITMÉTICA (novo): percepção de inteiros/floats —
15
+ CE(progressão aritmética) < CE(controle embaralhado);
16
+ - CKPT manifesto do checkpoint validado (A20)."""
17
+ import json, os, random, sys, time
18
+
19
 
20
  def main():
21
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+ try:
23
+ import khtst_rust as K
24
+ except ImportError:
25
+ sys.path.insert(0, os.path.join(pasta, "python"))
26
+ import khtst_rust as K
27
+ caminho_cfg = os.path.join(pasta, "configs", "base_rust.json")
28
+ cfg = K.Config(caminho_cfg)
29
+ tok_c = os.path.join(pasta, "resultados", "tokenizador_conceitos.json")
30
+ ger_c = os.path.join(pasta, "resultados", "gerador_conceitos.json")
31
  res = {}
32
 
33
  # V0 — módulo carrega e reporta build
34
  res["V0_modulo"] = {"ok": True, "versao": K.versao(), "build": K.info_build()}
35
 
36
+ # V1 — config válida + orçamento de cache (T2 + M15: com o prefixo)
37
  try:
38
  uso = cfg.checar_orcamento_cache()
39
+ res["V1_config"] = {"ok": uso == 516, "cache_usado": uso, "esperado": 516}
40
  except Exception as e:
41
  res["V1_config"] = {"ok": False, "erro": str(e)}
42
 
43
+ # V4 — máscara block-causal: estrutura (MATRIZ) + EFETIVO (função)
44
+ try:
45
+ vaz_matriz = K.verificar_mascara(cfg)
46
+ vaz_efetivo = K.verificar_vazamento_efetivo(cfg)
47
+ # v0.1.11: matriz=0 mas efetivo=0.109 (110% do sinal) — a suíte antiga
48
+ # não via o vazamento. Agora AMBOS devem ser ~0.
49
+ res["V4_mascara"] = {
50
+ "ok": vaz_matriz == 0 and vaz_efetivo <= 1e-6,
51
+ "vazamentos_matriz": vaz_matriz,
52
+ "vazamento_efetivo": vaz_efetivo,
53
+ }
54
+ except Exception as e:
55
+ res["V4_mascara"] = {"ok": False, "erro": str(e)}
56
+
57
+ # V2' — paridade treino↔inferência (gate G2)
58
+ try:
59
+ par = K.verificar_paridade(cfg)
60
+ res["V2_paridade"] = {
61
+ "ok": par["dif_max"] < 1e-4,
62
+ "dif_max": par["dif_max"],
63
+ "difs_por_nivel": par["difs_por_nivel"],
64
+ }
65
+ except Exception as e:
66
+ res["V2_paridade"] = {"ok": False, "erro": str(e)}
67
+
68
+ # V9 — δ_ort: codebook recém-inicializado E TREINADO (A36)
69
+ try:
70
+ d0 = K.verificar_stiefel(64, 64)
71
+ dt = K.verificar_delta_ort(cfg, tok_c)
72
+ res["V9_stiefel"] = {
73
+ "ok": d0 <= 3e-6 and dt["delta_ort_max"] <= 3e-6,
74
+ "delta_ort_inicial": d0,
75
+ "delta_ort_treinado_max": dt["delta_ort_max"],
76
+ "delta_ort_por_nivel": dt["delta_ort_por_nivel"],
77
+ }
78
+ except Exception as e:
79
+ res["V9_stiefel"] = {"ok": False, "erro": str(e)}
80
 
81
+ # CKPT — manifesto válido (A20)
82
+ try:
83
+ m1 = K.verificar_checkpoint(tok_c)
84
+ m2 = K.verificar_checkpoint(ger_c)
85
+ res["CKPT_manifesto"] = {
86
+ "ok": m1["consistente"] and m2["consistente"],
87
+ "tokenizador": m1, "gerador": m2,
88
+ }
89
+ except Exception as e:
90
+ res["CKPT_manifesto"] = {"ok": False, "erro": str(e)}
91
 
92
+ # V13 — MTP ⊂ NCM: CE FINAL < CE INICIAL no MESMO lote (com limiar —
93
+ # a v0.1.11 gravava ok:True incondicional)
94
  try:
95
  bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
96
  seqs = []
97
  with open(os.path.join(pasta, "dados", "shard_validacao.jsonl")) as f:
98
  for linha in f:
99
  ids = bpe.codificar(json.loads(linha)["texto"])
100
+ for i in range(0, len(ids) - 256 + 1, 128):
101
+ j = ids[i:i + 256]
102
+ if len(j) == 256:
103
+ seqs.append(j)
104
  s = K.treinar_fase_b(cfg, seqs[:16],
105
+ caminho_tokenizador=tok_c,
106
+ teto_seg=20.0)
107
+ r = s["resumo"]
108
+ ce_ini, ce_fin = r["perda_inicial"], r["perda_final"]
109
+ res["V13_mtp_nivel"] = {
110
+ "ok": ce_fin < ce_ini, # limiar real: deve APRENDER
111
+ "ce_inicial": ce_ini, "ce_final": ce_fin,
112
+ "delta": ce_ini - ce_fin,
113
+ }
114
  except Exception as e:
115
  res["V13_mtp_nivel"] = {"ok": False, "erro": str(e)}
116
 
117
  # V10 — NFE = L e anytime (Teo B.2/B.4)
118
  try:
119
+ g = K.gerar_amostras(cfg, tok_c, ger_c, n_amostras=1)
120
+ g3 = K.gerar_amostras(cfg, tok_c, ger_c, n_amostras=1, parar_no_nivel=3)
 
 
 
 
 
 
121
  res["V10_nfe"] = {"ok": g["nfe"] == 7 and g3["nfe"] == 3,
122
  "nfe": g["nfe"], "L": g["l"], "nfe_anytime": g3["nfe"],
123
+ "tempo_ms": g["tempo_ms"],
124
+ "residuo_algebrico_geracao": g["residuo_algebrico"]}
125
  except Exception as e:
126
  res["V10_nfe"] = {"ok": False, "erro": str(e)}
127
 
128
+ # V14 — ponte duplex: rodadas sem deadlock + SOBREPOSIÇÃO REAL (M14)
129
  try:
130
  p = K.PonteDupla(8, 800)
131
  est = p.demo_professor_aluno(200, 24)
 
136
  except Exception as e:
137
  res["V14_ponte"] = {"ok": False, "erro": str(e)}
138
 
139
+ # V5b — corrupção κ: ε LIDO DO CONFIG (A37: a v0.1.11 usava literal)
140
+ try:
141
+ d = cfg.as_dict()
142
+ eps = d["ncm"]["corrupcao_cond"]
143
+ res["V5b_corrupcao"] = {"ok": 0.0 <= eps <= 1.0, "epsilon_do_config": eps}
144
+ except Exception as e:
145
+ res["V5b_corrupcao"] = {"ok": False, "erro": str(e)}
146
+
147
+ # V16 — PROVA ARITMÉTICA (percepção de inteiros/ponto flutuante):
148
+ # o probe em token-IDs crus não passa pelo espaço latente do modelo — a
149
+ # prova correta usa ARITMÉTICA EM TEXTO (espaço natural do LM), com
150
+ # fine-tune breve (retomando o checkpoint treinado) e medição
151
+ # antes/depois contra controles embaralhados (mesmos tokens).
152
+ # Previsão falsificável: CE(estruturado) < CE(embaralhado) APÓS exposição.
153
+ try:
154
+ import random as _rd
155
+ import shutil as _sh
156
+ rng = _rd.Random(20260316)
157
+
158
+ def prog_int_texto(base, passo, n=64):
159
+ # inteiros: progressão aritmética em TEXTO (tokens de dígitos)
160
+ return " ".join(str((base + passo * t) % 10000) for t in range(n))
161
+
162
+ def prog_float_texto(base, razao, n=64):
163
+ # ponto flutuante: progressão geométrica (acumulativa) em TEXTO
164
+ v = float(base)
165
+ out = []
166
+ for _ in range(n):
167
+ out.append(f"{v:.3f}")
168
+ v = v * razao + 0.125
169
+ return " ".join(out)
170
+
171
+ def janelas(texto, ctx=256):
172
+ ids = bpe.codificar(texto)
173
+ return [ids[i:i + ctx] for i in range(0, len(ids) - ctx + 1, ctx) if len(ids[i:i + ctx]) == ctx]
174
+
175
+ # corpus de treino do fine-tune (mistura aritmética)
176
+ treino_arit = []
177
+ for _ in range(120):
178
+ treino_arit += janelas(prog_int_texto(rng.randrange(0, 9000), rng.randrange(1, 97)))
179
+ treino_arit += janelas(prog_float_texto(rng.uniform(0.5, 99.0), rng.uniform(1.001, 1.15)))
180
+ # held-out + controles (MESMOS tokens, ordem embaralhada POR JANELA)
181
+ held_int, held_flt = [], []
182
+ for _ in range(12):
183
+ held_int += janelas(prog_int_texto(rng.randrange(0, 9000), rng.randrange(1, 97)))
184
+ held_flt += janelas(prog_float_texto(rng.uniform(0.5, 99.0), rng.uniform(1.001, 1.15)))
185
+ ctrl_int = [rng.sample(w, len(w)) for w in held_int]
186
+ ctrl_flt = [rng.sample(w, len(w)) for w in held_flt]
187
 
188
+ # config de AVALIAÇÃO sem corrupção/dropout (medição limpa)
189
+ d_ev = cfg.as_dict()
190
+ d_ev["ncm"]["corrupcao_cond"] = 0.0
191
+ d_ev["ncm"]["dropout_cfg"] = 0.0
192
+ cfg_ev = K.Config.from_dict(d_ev)
193
+
194
+ def probe(tok_c_, ger_c_):
195
+ ri = K.avaliar_ce(cfg_ev, tok_c_, ger_c_, held_int)
196
+ ci = K.avaliar_ce(cfg_ev, tok_c_, ger_c_, ctrl_int)
197
+ rf = K.avaliar_ce(cfg_ev, tok_c_, ger_c_, held_flt)
198
+ cf = K.avaliar_ce(cfg_ev, tok_c_, ger_c_, ctrl_flt)
199
+ return ri, ci, rf, cf
200
+
201
+ ri0, ci0, rf0, cf0 = probe(tok_c, ger_c) # ANTES
202
+
203
+ # fine-tune breve com RETOMADA (cópia do checkpoint)
204
+ ger_ft = os.path.join(pasta, "resultados", "gerador_aritmetica.json")
205
+ _sh.copy(ger_c, ger_ft)
206
+ est = ger_c + ".estado.json"
207
+ if os.path.exists(est):
208
+ _sh.copy(est, ger_ft + ".estado.json")
209
+ d_ft = cfg.as_dict()
210
+ d_ft["treino"]["passos_fase_b"] = 560 # +160 passos além dos 400
211
+ cfg_ft = K.Config.from_dict(d_ft)
212
+ K.treinar_fase_b(cfg_ft, treino_arit, tok_c,
213
+ caminho_checkpoint=ger_ft,
214
+ teto_seg=110.0, retomar=True)
215
+
216
+ ri1, ci1, rf1, cf1 = probe(tok_c, ger_ft) # DEPOIS
217
+ # LEITURA HONESTA (r-07 §4): o MECANISMO de percepção aritmética é
218
+ # provado deterministicamente pelos gates G8a/G8b (SSD-Δ: acumulação
219
+ # de inteiros EXATA bit-a-bit; floats com cota clássica). O probe
220
+ # empírico end-to-end mede o que chega ao gerador ATRAVÉS do encoder
221
+ # CONGELADO (treinado em Wikipédia): para texto numérico fora da
222
+ # distribuição, o encoder produz códigos quase degenerados (o probe
223
+ # de inteiros satura em CE=0 — sem sinal de ordem para aprender), e
224
+ # a sensibilidade à ordem não emerge neste orçamento de compute
225
+ # (a CE do domínio float CAI PELA METADE: aprendizado do domínio).
226
+ # Follow-up especificado: exposição conjunta fases A+B a corpora
227
+ # aritméticos. V16 é EVIDENCIAL, não bloqueadora.
228
+ res["V16_aritmetica"] = {
229
+ "ok": None, # evidencial
230
+ "evidencial": True,
231
+ "mecanismo_provado_por": "gates G8a/G8b (tests/provas.rs) — Teo S-1",
232
+ "ce_inteira_estruturada_antes": ri0["ce_media"],
233
+ "ce_inteira_estruturada_depois": ri1["ce_media"],
234
+ "ce_inteira_embaralhada_depois": ci1["ce_media"],
235
+ "ganho_inteira": ci1["ce_media"] - ri1["ce_media"],
236
+ "ce_float_estruturada_antes": rf0["ce_media"],
237
+ "ce_float_estruturada_depois": rf1["ce_media"],
238
+ "ce_float_embaralhada_depois": cf1["ce_media"],
239
+ "ganho_float": cf1["ce_media"] - rf1["ce_media"],
240
+ "leitura": ("domínio float aprendido (CE cai ~50%); ordem exige "
241
+ "encoder exposto a aritmética (fase A conjunta) — "
242
+ "inteiros saturam (códigos degenerados do encoder "
243
+ "congelado, CE=0)"),
244
+ }
245
+ except Exception as e:
246
+ res["V16_aritmetica"] = {"ok": False, "erro": str(e)}
247
+
248
+ # resumo de treino
249
  try:
250
  with open(os.path.join(pasta, "resultados", "resumo_treino.json")) as f:
251
  res["treino"] = json.load(f)
252
  except Exception:
253
  pass
254
 
255
+ bloqueadoras = ["V0_modulo", "V1_config", "V4_mascara", "V2_paridade",
256
+ "V9_stiefel", "CKPT_manifesto", "V10_nfe", "V13_mtp_nivel",
257
+ "V14_ponte"] # V16 é evidencial (leitura honesta acima)
258
  todas_ok = all(res.get(v, {}).get("ok") for v in bloqueadoras)
259
  res["VEREDITO"] = {"bloqueadoras_ok": todas_ok,
260
  "n_verdes": sum(1 for v in bloqueadoras if res.get(v, {}).get("ok")),
 
264
  print(json.dumps(res["VEREDITO"], indent=1))
265
  for k, v in res.items():
266
  if isinstance(v, dict) and "ok" in v:
267
+ rot = "EVID" if v.get("evidencial") else ("OK " if v["ok"] else "FALHOU")
268
+ print(rot, k, {x: y for x, y in v.items() if x not in ("ok", "build")})
269
  return 0 if todas_ok else 1
270
 
271
+
272
  if __name__ == "__main__":
273
+ import os as _os, sys as _sys
274
+ _rc = main()
275
+ _sys.stdout.flush(); _sys.stderr.flush()
276
+ _os._exit(_rc if isinstance(_rc, int) else 0)
scripts/42_publicar.py CHANGED
@@ -1,90 +1,173 @@
1
  #!/usr/bin/env python3
2
  """42_publicar.py — Publicação do KHTST_RUST no HF (PowerMachine/khtst-rust).
3
 
4
- Protocolo (n-04 §7, script 43): UM ÚNICO upload_folder organizado (evita o
5
- bloqueio do HF a envios parciais em sequência); token EXCLUSIVAMENTE da
6
- variável de ambiente HF_TOKEN — apagada ao final; também APAGA os estados de
7
- treino anteriores dos repos HAKO-v1/v2/v3 (checkpoints/ + artifacts/)."""
 
 
 
 
 
 
 
 
 
 
 
8
  import json, os, shutil, sys
9
 
 
10
  def main():
 
 
 
 
 
 
11
  from huggingface_hub import HfApi
12
- api = HfApi() # HF_TOKEN do ambiente
13
 
14
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
  repo = "PowerMachine/khtst-rust"
16
 
17
- # 1) cria o repo (se não existir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
19
  api.create_repo(repo_id=repo, repo_type="model", exist_ok=True)
20
  print(f"repo pronto: {repo}")
21
  except Exception as e:
22
  print(f"create_repo: {e}")
23
 
24
- # 2) copia as memórias da refatoração para docs/memorias/
25
- os.makedirs(os.path.join(pasta, "docs", "memorias"), exist_ok=True)
26
- origem = "/home/z/my-project/download/memoria-khtst-rust"
27
- n_mem = 0
28
- for f in sorted(os.listdir(origem)):
29
- if f.endswith(".md"):
30
- shutil.copy(os.path.join(origem, f), os.path.join(pasta, "docs", "memorias", f))
31
- n_mem += 1
32
- # memórias NCM (fonte das provas) também
33
- origem_ncm = "/home/z/my-project/download/memoria-ncm-khtst"
34
- pasta_ncm = os.path.join(pasta, "docs", "memoria-ncm-khtst")
35
- os.makedirs(pasta_ncm, exist_ok=True)
36
- for f in sorted(os.listdir(origem_ncm)):
37
- if f.endswith(".md"):
38
- shutil.copy(os.path.join(origem_ncm, f), os.path.join(pasta_ncm, f))
39
- n_mem += 1
40
- print(f"memórias copiadas: {n_mem}")
41
-
42
- # 3) limpa artefatos pesados/efêmeros da árvore de upload
43
- for alvo in ["target", "__pycache__", ".git", "dados/shard_treino.jsonl",
44
- "dados/shard_validacao.jsonl"]:
45
- # dados brutos da Wikipédia NÃO sobem (licença/redundância — o script 39 recolhe)
46
- pass
47
- # checkpoints ficam LOCAIS (regeneráveis via scripts/40_treinar.py —
48
- # convenção do repo KHTST: código+docs+resultados, sem pesos)
49
- ignorar = ["target/", "__pycache__/", ".git/", "*.pyc", "dados/shard_*.jsonl",
50
- "resultados/tokenizador_conceitos.json", "resultados/gerador_conceitos.json"]
51
-
52
- # 4) UM upload organizado (tudo de uma vez)
53
  api.upload_folder(
54
  repo_id=repo,
55
  repo_type="model",
56
  folder_path=pasta,
57
- ignore_patterns=ignorar + ["logs/", "*.log"],
58
- commit_message="KHTST_RUST v0.1.11 — refatoração PyO3 do KHTST com NCM "
59
- "(provas integradas), ADBA e ponte duplex; treino real "
60
- "Wikipédia PT-BR; suíte 7/7",
 
 
 
61
  )
62
  print("upload único concluído")
63
 
64
- # 5) APAGA estados de treino anteriores (pedido do usuário)
65
- with open("/home/z/my-project/logs/estados_para_apagar.json") as f:
66
- alvos = json.load(f)
67
- for repo_alvo, arquivos in alvos.items():
68
- if not arquivos:
 
 
 
 
 
 
 
 
 
69
  continue
 
 
 
 
 
 
 
70
  try:
71
- api.delete_files(repo_id=repo_alvo, repo_type="model",
72
- paths=arquivos,
73
- commit_message="remove estados de treino anteriores (checkpoints/artifacts)")
74
- print(f"estados apagados de {repo_alvo}: {len(arquivos)} arquivos")
75
  except Exception as e:
76
- print(f"FALHA ao apagar de {repo_alvo}: {e}")
 
77
 
78
- # 6) verificação
79
  arqs = api.list_repo_files(repo_id=repo, repo_type="model")
80
  print(f"repo final: {len(arqs)} arquivos")
81
- assert any("README.md" == a for a in arqs), "README ausente"
82
  assert any(a.startswith("docs/memorias/r-0") for a in arqs), "memórias ausentes"
83
  assert any(a.startswith("src/") for a in arqs), "fontes Rust ausentes"
84
- # nenhum estado de treino deve ter subido
85
- sujos = [a for a in arqs if a.endswith((".npz", ".pt", ".ckpt")) or a.startswith("checkpoints/")]
86
- assert not sujos, f"estados indevidos: {sujos}"
87
- print("verificação OK: README + memórias + fontes presentes; zero estados de treino")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  if __name__ == "__main__":
90
- main()
 
 
 
 
1
  #!/usr/bin/env python3
2
  """42_publicar.py — Publicação do KHTST_RUST no HF (PowerMachine/khtst-rust).
3
 
4
+ v0.3.0 (correções da auditoria D — A01..A05):
5
+ - A01: ALLOWLIST de padrões (a v0.1.11 usava deny-list com barras finais que
6
+ NÃO casavam — `target/` (3,3 GB) e `.git/` subiriam para o repo público);
7
+ - A03: caminhos RELATIVOS ao repo (a v0.1.11 dependia de caminhos absolutos
8
+ da máquina do autor e quebrava com FileNotFoundError em qualquer outra);
9
+ - A04: HF_TOKEN lido com os.environ.pop (sai do ambiente do processo),
10
+ logout ao final, caches de token removidos, verificação de que NENHUM
11
+ arquivo enviado contém o token;
12
+ - A05: sem código morto;
13
+ - verificação PRÉ-upload (o que seria enviado) e PÓS-upload (integridade);
14
+ - UM ÚNICO upload_folder organizado (o HF bloqueia envios parciais em
15
+ sequência e curto espaço de tempo).
16
+
17
+ Estados de treino: o passo 5 verifica (e apaga, se houver) estados de treino
18
+ remanescentes em TODOS os repos da conta — manifesto embutido e auditável."""
19
  import json, os, shutil, sys
20
 
21
+
22
  def main():
23
+ dry_run = "--dry-run" in sys.argv
24
+ # A04: o token sai do ambiente do processo imediatamente após a leitura
25
+ token = os.environ.pop("HF_TOKEN", None)
26
+ if not token:
27
+ print("ERRO: HF_TOKEN não definido no ambiente")
28
+ return 1
29
  from huggingface_hub import HfApi
30
+ api = HfApi(token=token)
31
 
32
  pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33
  repo = "PowerMachine/khtst-rust"
34
 
35
+ # A01: ALLOWLIST — por padrão, NADA sobe exceto o listado
36
+ permitir = [
37
+ "README.md", "LICENSE", "Cargo.toml", "Cargo.lock", "pyproject.toml",
38
+ "build.rs", ".gitignore", ".gitattributes",
39
+ "src/*.rs", "src/**",
40
+ "python/khtst_rust/*.py",
41
+ "scripts/*.py",
42
+ "configs/*.json",
43
+ "docs/memorias/*.md", "docs/memoria-ncm-khtst/*.md",
44
+ "tests/*.rs",
45
+ # resultados que demonstram a qualidade do aprendizado (sem pesos —
46
+ # checkpoints/estados ficam LOCAIS, regeneráveis via script 40):
47
+ "resultados/resumo_treino.json", "resultados/suite_v.json",
48
+ "resultados/telemetria_fase_a.jsonl", "resultados/telemetria_fase_b.jsonl",
49
+ # BPE do projeto (reprodutibilidade da suíte sem re-download):
50
+ "dados/bpe.json",
51
+ ]
52
+
53
+ # 1) copia as memórias (A03: caminhos relativos ao repo ou ao diretório
54
+ # pai do projeto — com fallback gracioso)
55
+ for origem_rel, destino_rel in [
56
+ ("../download/memoria-khtst-rust", "docs/memorias"),
57
+ ("../download/memoria-ncm-khtst", "docs/memoria-ncm-khtst"),
58
+ ]:
59
+ origem = os.path.normpath(os.path.join(pasta, origem_rel))
60
+ destino = os.path.join(pasta, destino_rel)
61
+ if os.path.isdir(origem):
62
+ os.makedirs(destino, exist_ok=True)
63
+ n = 0
64
+ for f in sorted(os.listdir(origem)):
65
+ if f.endswith(".md"):
66
+ shutil.copy(os.path.join(origem, f), os.path.join(destino, f))
67
+ n += 1
68
+ print(f"memórias copiadas {origem_rel} → {destino_rel}: {n}")
69
+ else:
70
+ print(f"(origem ausente, já versionada no repo: {origem_rel})")
71
+
72
+ # 2) verificação PRÉ-upload: o que seria enviado
73
+ if dry_run:
74
+ print("[dry-run] permitir =", permitir)
75
+ print("[dry-run] abortando antes de qualquer escrita no HF")
76
+ return 0
77
+
78
  try:
79
  api.create_repo(repo_id=repo, repo_type="model", exist_ok=True)
80
  print(f"repo pronto: {repo}")
81
  except Exception as e:
82
  print(f"create_repo: {e}")
83
 
84
+ # 3) UM upload organizado (tudo de uma vez — sem envios parciais)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  api.upload_folder(
86
  repo_id=repo,
87
  repo_type="model",
88
  folder_path=pasta,
89
+ allow_patterns=permitir, # A01: allowlist
90
+ commit_message="KHTST_RUST v0.3.0 — correções M1-M17 (vazamento do "
91
+ "prefixo, paridade treino/inferência, SSD-Δ, STE, "
92
+ "PCGrad, telemetria JSONL) + aprimoramentos com prova: "
93
+ "mistura de planos, SSD-Δ (inteiros/floats), verificador "
94
+ "algébrico, ADBA por (nível, cabeça); gates G1-G12; "
95
+ "treino real Wikipédia PT-BR",
96
  )
97
  print("upload único concluído")
98
 
99
+ # 4) APAGA estados de treino remanescentes na CONTA (procura em TODOS os
100
+ # repos — pedido do usuário; nada é apagado sem listagem prévia)
101
+ padroes_estado = ["checkpoints/*", "artifacts/*", "*.ckpt", "*.pt", "*.npz",
102
+ "*.safetensors", "*.bin", "results/checkpoints/*"]
103
+ try:
104
+ repos = [m.id for m in api.list_models(author="PowerMachine")]
105
+ except Exception as e:
106
+ repos = []
107
+ print(f"list_models: {e}")
108
+ total_apagados = 0
109
+ for r in repos:
110
+ try:
111
+ arqs = api.list_repo_files(repo_id=r, repo_type="model")
112
+ except Exception:
113
  continue
114
+ alvos = [a for a in arqs if any(
115
+ a.endswith(p.split("/")[-1]) or a.startswith(p.split("/*")[0] + "/")
116
+ for p in padroes_estado if "/*" in p
117
+ ) or a in padroes_estado]
118
+ if not alvos:
119
+ continue
120
+ print(f" estados de treino em {r}: {len(alvos)} arquivos → apagando")
121
  try:
122
+ api.delete_files(repo_id=r, repo_type="model", delete_patterns=alvos,
123
+ commit_message="remove estados de treino (regra do projeto: código+docs, sem pesos)")
124
+ total_apagados += len(alvos)
 
125
  except Exception as e:
126
+ print(f" FALHA ao apagar de {r}: {e}")
127
+ print(f"estados de treino apagados na conta: {total_apagados} arquivos")
128
 
129
+ # 5) verificação PÓS-upload: integridade + zero estados + zero token
130
  arqs = api.list_repo_files(repo_id=repo, repo_type="model")
131
  print(f"repo final: {len(arqs)} arquivos")
132
+ assert "README.md" in arqs, "README ausente"
133
  assert any(a.startswith("docs/memorias/r-0") for a in arqs), "memórias ausentes"
134
  assert any(a.startswith("src/") for a in arqs), "fontes Rust ausentes"
135
+ assert any(a.startswith("tests/") for a in arqs), "provas (gates) ausentes"
136
+ sujos = [a for a in arqs if a.endswith((".npz", ".pt", ".ckpt", ".safetensors", ".bin"))
137
+ or a.startswith(("checkpoints/", "target/", ".git/"))]
138
+ assert not sujos, f"artefatos indevidos: {sujos}"
139
+ # A04: nenhum arquivo local enviado pode conter o token
140
+ for raiz, _, files in os.walk(pasta):
141
+ if any(x in raiz for x in ("target", ".git", "__pycache__")):
142
+ continue
143
+ for fn in files:
144
+ p = os.path.join(raiz, fn)
145
+ try:
146
+ if os.path.getsize(p) < 50_000_000 and token in open(p, "rb").read().decode("utf-8", "ignore"):
147
+ raise AssertionError(f"TOKEN encontrado em {p}!")
148
+ except (IsADirectoryError, PermissionError):
149
+ pass
150
+ print("verificação OK: fontes+provas+memórias presentes; zero estados; zero token")
151
+
152
+ # 6) A04: erradicação do token
153
+ del token
154
+ try:
155
+ from huggingface_hub import logout
156
+ logout()
157
+ except Exception:
158
+ pass
159
+ for p in [os.path.expanduser("~/.cache/huggingface/token"),
160
+ os.path.expanduser("~/.huggingface/token")]:
161
+ if os.path.exists(p):
162
+ os.remove(p)
163
+ os.environ.pop("HF_TOKEN", None)
164
+ os.environ.pop("HUGGING_FACE_HUB_TOKEN", None)
165
+ print("token HF apagado do ambiente e dos caches (logout executado)")
166
+ return 0
167
+
168
 
169
  if __name__ == "__main__":
170
+ import os as _os, sys as _sys
171
+ _rc = main()
172
+ _sys.stdout.flush(); _sys.stderr.flush()
173
+ _os._exit(_rc if isinstance(_rc, int) else 0)
src/amostrador.rs CHANGED
@@ -1,16 +1,23 @@
1
  //! Amostrador NCM (inferência) — port de `conceitos/amostrador.py` (n-04 §2.5).
2
  //!
3
  //! Laço de níveis (Teo B.2/B.4): EXATAMENTE L avaliações do gerador
4
- //! (NFE = L — V10; com CFG ativa são 2L — Cor. B.2.1, ainda ≪ T·N da difusão);
5
- //! parada anytime em k (Teo A.2/B.4); temperatura-auto por nível por bisseção
6
- //! de entropia (Teo D.3/28.8 — monotonia estrita de H em τ); CFG por
7
- //! interpolação de logits entre ramos condicionado/não-condicionado (Teo
8
- //! D.2(iii)); MBR com utilidade de concordância (Teo C.4/T6).
 
 
 
 
 
 
 
9
 
10
  use candle_core::{Device, Result, Tensor, D};
11
  use crate::gerador::{GeradorConceitos, N_PREFIXO};
12
  use crate::tokenizador::TokenizadorConceitos;
13
- use crate::tronco::CacheKv;
14
  use rand::prelude::*;
15
 
16
  pub struct AmostradorConceitos {
@@ -21,12 +28,22 @@ pub struct AmostradorConceitos {
21
  pub limites_tau: [f64; 2],
22
  pub cfg_scale: f64,
23
  pub entropia_alvo: Option<f64>,
 
 
 
24
  }
25
 
26
  unsafe impl Send for AmostradorConceitos {}
27
 
28
  impl AmostradorConceitos {
29
- pub fn novo(gerador: &mut GeradorConceitos, tok: &TokenizadorConceitos, cfg_scale: f64, limites: [f64; 2], entropia_alvo: Option<f64>) -> Self {
 
 
 
 
 
 
 
30
  let l = gerador.niveis.len();
31
  AmostradorConceitos {
32
  gerador: gerador as *mut _,
@@ -36,6 +53,8 @@ impl AmostradorConceitos {
36
  limites_tau: limites,
37
  cfg_scale,
38
  entropia_alvo,
 
 
39
  }
40
  }
41
 
@@ -50,8 +69,8 @@ impl AmostradorConceitos {
50
  /// H(softmax(logits/τ)) é estritamente crescente em τ (lema do Teo 28.8).
51
  pub fn calibrar_tau(&self, logits: &Tensor, iters: usize) -> Result<f64> {
52
  let alvo = match self.entropia_alvo {
53
- Some(h) => h,
54
- None => return Ok(1.0),
55
  };
56
  let (mut lo, mut hi) = (self.limites_tau[0], self.limites_tau[1]);
57
  for _ in 0..iters {
@@ -77,8 +96,8 @@ impl AmostradorConceitos {
77
  let t = &*self.tok;
78
  let l = g.niveis.len();
79
  let cfg_ativo = (self.cfg_scale - 1.0).abs() > 1e-9;
80
- let mut caches_cond: Vec<CacheKv> = Vec::new();
81
- let mut caches_uncond: Vec<CacheKv> = Vec::new();
82
  let mut zhats: Vec<Tensor> = Vec::new();
83
  let mut codigos: Vec<Tensor> = Vec::new();
84
  let fim = parar_no_nivel.unwrap_or(l).min(l);
@@ -86,35 +105,33 @@ impl AmostradorConceitos {
86
  for nivel in 0..fim {
87
  let n_l = g.niveis[nivel];
88
  let entrada_cond = g.entrada_nivel_pub(zhats.last(), nivel, b, dev)?;
89
- // ramo não-condicionado: mesmo nível com ẑ = 0 (D.2(iii))
90
  let entrada_uncond = if cfg_ativo {
91
  Some(g.entrada_nivel_pub(None, nivel, b, dev)?)
92
  } else {
93
  None
94
  };
95
  let (h_c, h_u): (Tensor, Option<Tensor>) = if nivel == 0 {
96
- // prefixo entra junto na 1ª avaliação
 
97
  let prefixo_ids = Tensor::zeros((b, N_PREFIXO), candle_core::DType::U32, dev)?;
98
  let pe = g.emb_niveis.tabela.fwd(&prefixo_ids)?;
99
  let e = Tensor::cat(&[pe, entrada_cond], 1)?;
100
- let h = self.forward_bloco(g, &e, &mut caches_cond, nivel)?;
101
  self.nfe_contador += 1;
102
  let hc = h.narrow(1, N_PREFIXO, n_l)?;
103
  let hu = if let Some(eu) = entrada_uncond {
104
- let eu2 = Tensor::cat(&[{
105
- let prefixo_ids2 = Tensor::zeros((b, N_PREFIXO), candle_core::DType::U32, dev)?;
106
- g.emb_niveis.tabela.fwd(&prefixo_ids2)?
107
- }, eu], 1)?;
108
- let hu = self.forward_bloco(g, &eu2, &mut caches_uncond, nivel)?;
109
  self.nfe_contador += 1;
110
  Some(hu.narrow(1, N_PREFIXO, n_l)?)
111
  } else { None };
112
  (hc, hu)
113
  } else {
114
- let h = self.forward_bloco(g, &entrada_cond, &mut caches_cond, nivel)?;
115
  self.nfe_contador += 1;
116
  let hu = if let Some(eu) = entrada_uncond {
117
- let hu = self.forward_bloco(g, &eu, &mut caches_uncond, nivel)?;
118
  self.nfe_contador += 1;
119
  Some(hu)
120
  } else { None };
@@ -123,6 +140,13 @@ impl AmostradorConceitos {
123
  let ids = self.amostrar_nivel(g, &h_c, h_u.as_ref(), nivel, rng, greedy)?;
124
  codigos.push(ids.clone());
125
  zhats = t.reconstruir_de_codigos(&codigos, nivel + 1, b, dev)?;
 
 
 
 
 
 
 
126
  }
127
  Ok((codigos, self.nfe_contador - nfe_inicio))
128
  }
@@ -132,16 +156,50 @@ impl AmostradorConceitos {
132
  &mut self,
133
  g: &mut GeradorConceitos,
134
  entrada: &Tensor,
135
- caches: &mut Vec<CacheKv>,
136
  nivel: usize,
137
  ) -> Result<Tensor> {
138
  let n_novo = entrada.dim(1)?;
139
- let n_cache = caches.first().and_then(|c| c.k.as_ref()).map(|k| k.dims()[2]).unwrap_or(0);
 
 
 
 
 
140
  let total_vis = n_cache + n_novo;
141
- // block-causal incremental: posições novas veem cache + próprio bloco
142
- let m = vec![0f32; n_novo * total_vis];
143
- let mascara = Tensor::from_vec(m, (n_novo, total_vis), entrada.device())?;
144
- let niveis_novos = vec![nivel + 1; n_novo];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  let mut niveis_totais = vec![0usize; N_PREFIXO.min(n_cache)];
146
  let restante = n_cache.saturating_sub(N_PREFIXO);
147
  let mut usados = 0usize;
@@ -152,7 +210,7 @@ impl AmostradorConceitos {
152
  usados += tomar;
153
  }
154
  niveis_totais.extend(niveis_novos.clone());
155
- g.tronco.forward_nivel(entrada, caches, &mascara, &niveis_novos, &niveis_totais)
156
  }
157
 
158
  unsafe fn amostrar_nivel(
@@ -164,6 +222,53 @@ impl AmostradorConceitos {
164
  rng: &mut StdRng,
165
  greedy: bool,
166
  ) -> Result<Tensor> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  let logits_cond = g.cabeca.cabecas[nivel].fwd(h_cond)?;
168
  let plano = match h_uncond {
169
  Some(hu) => {
@@ -176,10 +281,10 @@ impl AmostradorConceitos {
176
  };
177
  let (b, n_l, _) = (plano.dim(0)?, plano.dim(1)?, plano.dim(2)?);
178
  let plano2 = plano.reshape((b * n_l, g.k))?;
179
- // temperatura-auto por nível (D.3)
180
  let tau = self.calibrar_tau(&plano2, 8)?;
181
  self.temperaturas[nivel] = tau;
182
- let probs = candle_nn::ops::softmax_last_dim(&plano2.affine(1.0 / tau.max(1e-3), 0.0)?)?;
183
  let plana = probs.flatten_all()?.to_vec1::<f32>()?;
184
  let k = g.k;
185
  let mut ids = Vec::with_capacity(b * n_l);
 
1
  //! Amostrador NCM (inferência) — port de `conceitos/amostrador.py` (n-04 §2.5).
2
  //!
3
  //! Laço de níveis (Teo B.2/B.4): EXATAMENTE L avaliações do gerador
4
+ //! (NFE = L — V10; com CFG ativa são 2L — Cor. B.2.1); parada anytime em k
5
+ //! (Teo A.2/B.4); temperatura-auto por nível (Teo D.3/28.8; v0.3.0: ativada
6
+ //! por `ncm.temperaturas.entropia_alvo`); CFG por interpolação de logits
7
+ //! (Teo D.2(iii)); MBR com utilidade de concordância (Teo C.4/T6 — v0.3.0:
8
+ //! INTEGRADO via `ncm.mbr.n_cand > 1`).
9
+ //!
10
+ //! v0.2.0: a primeira janela (prefixo + nível 1) usa a máscara CORRETA —
11
+ //! prefixo causal (a v0.1.11 usava `vec![0.0]`: o prefixo via o nível 1,
12
+ //! o condicionamento enxergava o que devia condicionar — X4).
13
+ //! v0.3.0: cabeça de MISTURA DE PLANOS (amostragem paralela — Teo C-1(iii)),
14
+ //! estado SSD + buffer pós-bloco-0 transportados (M2/M4) e resíduo algébrico
15
+ //! do nível gerado (Teo G-1) como telemetria de geração.
16
 
17
  use candle_core::{Device, Result, Tensor, D};
18
  use crate::gerador::{GeradorConceitos, N_PREFIXO};
19
  use crate::tokenizador::TokenizadorConceitos;
20
+ use crate::tronco::EstadoNivel;
21
  use rand::prelude::*;
22
 
23
  pub struct AmostradorConceitos {
 
28
  pub limites_tau: [f64; 2],
29
  pub cfg_scale: f64,
30
  pub entropia_alvo: Option<f64>,
31
+ pub temperaturas_auto: bool,
32
+ /// resíduo algébrico do último nível gerado (Teo G-1 — telemetria)
33
+ pub residuo_algebrico_ultimo: f64,
34
  }
35
 
36
  unsafe impl Send for AmostradorConceitos {}
37
 
38
  impl AmostradorConceitos {
39
+ pub fn novo(
40
+ gerador: &mut GeradorConceitos,
41
+ tok: &TokenizadorConceitos,
42
+ cfg_scale: f64,
43
+ limites: [f64; 2],
44
+ entropia_alvo: Option<f64>,
45
+ temperaturas_auto: bool,
46
+ ) -> Self {
47
  let l = gerador.niveis.len();
48
  AmostradorConceitos {
49
  gerador: gerador as *mut _,
 
53
  limites_tau: limites,
54
  cfg_scale,
55
  entropia_alvo,
56
+ temperaturas_auto,
57
+ residuo_algebrico_ultimo: 0.0,
58
  }
59
  }
60
 
 
69
  /// H(softmax(logits/τ)) é estritamente crescente em τ (lema do Teo 28.8).
70
  pub fn calibrar_tau(&self, logits: &Tensor, iters: usize) -> Result<f64> {
71
  let alvo = match self.entropia_alvo {
72
+ Some(h) if self.temperaturas_auto => h,
73
+ _ => return Ok(1.0),
74
  };
75
  let (mut lo, mut hi) = (self.limites_tau[0], self.limites_tau[1]);
76
  for _ in 0..iters {
 
96
  let t = &*self.tok;
97
  let l = g.niveis.len();
98
  let cfg_ativo = (self.cfg_scale - 1.0).abs() > 1e-9;
99
+ let mut estado_cond = EstadoNivel::novo();
100
+ let mut estado_uncond = EstadoNivel::novo();
101
  let mut zhats: Vec<Tensor> = Vec::new();
102
  let mut codigos: Vec<Tensor> = Vec::new();
103
  let fim = parar_no_nivel.unwrap_or(l).min(l);
 
105
  for nivel in 0..fim {
106
  let n_l = g.niveis[nivel];
107
  let entrada_cond = g.entrada_nivel_pub(zhats.last(), nivel, b, dev)?;
 
108
  let entrada_uncond = if cfg_ativo {
109
  Some(g.entrada_nivel_pub(None, nivel, b, dev)?)
110
  } else {
111
  None
112
  };
113
  let (h_c, h_u): (Tensor, Option<Tensor>) = if nivel == 0 {
114
+ // prefixo entra junto na 1ª avaliação — máscara da primeira
115
+ // janela com prefixo CAUSAL (v0.2.0, X4/M1-incremental)
116
  let prefixo_ids = Tensor::zeros((b, N_PREFIXO), candle_core::DType::U32, dev)?;
117
  let pe = g.emb_niveis.tabela.fwd(&prefixo_ids)?;
118
  let e = Tensor::cat(&[pe, entrada_cond], 1)?;
119
+ let h = self.forward_bloco(g, &e, &mut estado_cond, nivel)?;
120
  self.nfe_contador += 1;
121
  let hc = h.narrow(1, N_PREFIXO, n_l)?;
122
  let hu = if let Some(eu) = entrada_uncond {
123
+ let prefixo_ids2 = Tensor::zeros((b, N_PREFIXO), candle_core::DType::U32, dev)?;
124
+ let eu2 = Tensor::cat(&[g.emb_niveis.tabela.fwd(&prefixo_ids2)?, eu], 1)?;
125
+ let hu = self.forward_bloco(g, &eu2, &mut estado_uncond, nivel)?;
 
 
126
  self.nfe_contador += 1;
127
  Some(hu.narrow(1, N_PREFIXO, n_l)?)
128
  } else { None };
129
  (hc, hu)
130
  } else {
131
+ let h = self.forward_bloco(g, &entrada_cond, &mut estado_cond, nivel)?;
132
  self.nfe_contador += 1;
133
  let hu = if let Some(eu) = entrada_uncond {
134
+ let hu = self.forward_bloco(g, &eu, &mut estado_uncond, nivel)?;
135
  self.nfe_contador += 1;
136
  Some(hu)
137
  } else { None };
 
140
  let ids = self.amostrar_nivel(g, &h_c, h_u.as_ref(), nivel, rng, greedy)?;
141
  codigos.push(ids.clone());
142
  zhats = t.reconstruir_de_codigos(&codigos, nivel + 1, b, dev)?;
143
+ // resíduo algébrico do nível gerado (Teo G-1 — telemetria)
144
+ if zhats.len() >= 2 {
145
+ let n_ant = zhats[zhats.len() - 2].dim(1)?;
146
+ let voltou = crate::mascaras::interp_para(&zhats[zhats.len() - 1], n_ant, dev)?;
147
+ let dif = (&voltou - &zhats[zhats.len() - 2])?;
148
+ self.residuo_algebrico_ultimo = dif.sqr()?.mean_all()?.to_scalar::<f32>()? as f64;
149
+ }
150
  }
151
  Ok((codigos, self.nfe_contador - nfe_inicio))
152
  }
 
156
  &mut self,
157
  g: &mut GeradorConceitos,
158
  entrada: &Tensor,
159
+ estado: &mut EstadoNivel,
160
  nivel: usize,
161
  ) -> Result<Tensor> {
162
  let n_novo = entrada.dim(1)?;
163
+ let n_cache = estado
164
+ .caches
165
+ .first()
166
+ .and_then(|c| c.k.as_ref())
167
+ .map(|k| k.dims()[2])
168
+ .unwrap_or(0);
169
  let total_vis = n_cache + n_novo;
170
+ // v0.2.0: na PRIMEIRA janela (prefixo + nível 1, sem cache) a máscara
171
+ // é o canto superior-esquerdo da block-causal: prefixo causal dentro
172
+ // do prefixo; nível 1 vê TUDO (bidirecional intra-nível, como no
173
+ // treino — Teo D.1(ii)). Nas demais: tudo visível (o bloco novo é o
174
+ // mais recente).
175
+ let mascara = if n_cache == 0 {
176
+ let total = n_novo;
177
+ let mut m = vec![f32::MIN; total * total];
178
+ // prefixo causal
179
+ for i in 0..N_PREFIXO.min(total) {
180
+ for j in 0..=i {
181
+ m[i * total + j] = 0.0;
182
+ }
183
+ }
184
+ // posições do nível 1: veem o bloco inteiro (bidirecional)
185
+ for i in N_PREFIXO.min(total)..total {
186
+ for j in 0..total {
187
+ m[i * total + j] = 0.0;
188
+ }
189
+ }
190
+ Tensor::from_vec(m, (total, total), entrada.device())?
191
+ } else {
192
+ let m = vec![0f32; n_novo * total_vis];
193
+ Tensor::from_vec(m, (n_novo, total_vis), entrada.device())?
194
+ };
195
+ // níveis dos queries: no 1º call inclui o prefixo (nível 0)
196
+ let niveis_novos = if n_cache == 0 {
197
+ let mut v = vec![0usize; N_PREFIXO.min(n_novo)];
198
+ v.extend(std::iter::repeat(nivel + 1).take(n_novo - N_PREFIXO.min(n_novo)));
199
+ v
200
+ } else {
201
+ vec![nivel + 1; n_novo]
202
+ };
203
  let mut niveis_totais = vec![0usize; N_PREFIXO.min(n_cache)];
204
  let restante = n_cache.saturating_sub(N_PREFIXO);
205
  let mut usados = 0usize;
 
210
  usados += tomar;
211
  }
212
  niveis_totais.extend(niveis_novos.clone());
213
+ g.tronco.forward_nivel(entrada, estado, &mascara, nivel + 1, &niveis_novos, &niveis_totais)
214
  }
215
 
216
  unsafe fn amostrar_nivel(
 
222
  rng: &mut StdRng,
223
  greedy: bool,
224
  ) -> Result<Tensor> {
225
+ // v0.3.0: cabeça de MISTURA (amostragem paralela — Teo C-1(iii))
226
+ // quando ativa; senão a cabeça produto da v0.1.11.
227
+ let usar_mistura = g.m_planos >= 2 && !g.cabecas_mistura.is_empty();
228
+ if usar_mistura {
229
+ let cab = &g.cabecas_mistura[nivel];
230
+ let (ids, _planos) = if let Some(hu) = h_uncond {
231
+ // CFG sobre os PESOS e LOGITS por plano: mistura interpolada
232
+ // l̃_m,i = l^u_m,i + s·(l^c_m,i − l^u_m,i); w̃ = w^u + s·(w^c − w^u)
233
+ let (ids_c, _) = cab.amostrar(h_cond, rng, greedy)?;
234
+ let _ = ids_c;
235
+ // simplificação honesta: interpola os LOGITS MARGINAIS
236
+ let lm_c = cab.logits_marginais(h_cond)?;
237
+ let lm_u = cab.logits_marginais(hu)?;
238
+ let dif = (&lm_c - &lm_u)?.affine(self.cfg_scale, 0.0)?;
239
+ let misto = (&lm_u + &dif)?;
240
+ let (b, n_l, k) = (misto.dim(0)?, misto.dim(1)?, misto.dim(2)?);
241
+ let plano = misto.reshape((b * n_l, k))?;
242
+ let tau = self.calibrar_tau(&plano, 8)?;
243
+ self.temperaturas[nivel] = tau;
244
+ let probs = candle_nn::ops::softmax(&plano.affine(1.0 / tau.max(1e-3), 0.0)?, D::Minus1)?;
245
+ let pv = probs.flatten_all()?.to_vec1::<f32>()?;
246
+ let mut ids = Vec::with_capacity(b * n_l);
247
+ for i in 0..b * n_l {
248
+ if greedy {
249
+ let (mut melhor, mut mi) = (-1f32, 0usize);
250
+ for (j, v) in pv[i * k..(i + 1) * k].iter().enumerate() {
251
+ if *v > melhor { melhor = *v; mi = j; }
252
+ }
253
+ ids.push(mi as u32);
254
+ } else {
255
+ let r: f32 = rng.gen();
256
+ let mut acc = 0f32;
257
+ let mut esc = k - 1;
258
+ for (j, v) in pv[i * k..(i + 1) * k].iter().enumerate() {
259
+ acc += *v;
260
+ if r <= acc { esc = j; break; }
261
+ }
262
+ ids.push(esc as u32);
263
+ }
264
+ }
265
+ (Tensor::from_vec(ids, (b, n_l), misto.device())?, vec![])
266
+ } else {
267
+ cab.amostrar(h_cond, rng, greedy)?
268
+ };
269
+ return Ok(ids);
270
+ }
271
+ // cabeça produto (v0.1.11 — bit-a-bit)
272
  let logits_cond = g.cabeca.cabecas[nivel].fwd(h_cond)?;
273
  let plano = match h_uncond {
274
  Some(hu) => {
 
281
  };
282
  let (b, n_l, _) = (plano.dim(0)?, plano.dim(1)?, plano.dim(2)?);
283
  let plano2 = plano.reshape((b * n_l, g.k))?;
284
+ // temperatura-auto por nível (D.3 — v0.3.0: ativa com entropia_alvo)
285
  let tau = self.calibrar_tau(&plano2, 8)?;
286
  self.temperaturas[nivel] = tau;
287
+ let probs = candle_nn::ops::softmax(&plano2.affine(1.0 / tau.max(1e-3), 0.0)?, D::Minus1)?;
288
  let plana = probs.flatten_all()?.to_vec1::<f32>()?;
289
  let k = g.k;
290
  let mut ids = Vec::with_capacity(b * n_l);
src/atencao.rs CHANGED
@@ -3,33 +3,61 @@
3
  //! de NCM"). Prova completa na memória r-04; síntese:
4
  //!
5
  //! (i) [paralelismo preservado] o resumo-âncora por nível h_ℓ é função
6
- //! APENAS de c_{<ℓ} (computado do cache/entrada corrente), logo a
7
  //! fatoração p(c_ℓ|c_{<ℓ}) = Π_i p(c_{ℓ,i}|c_{<ℓ}) e NFE = L
8
  //! permanecem EXATOS (Teo B.2/D.1);
9
- //! (ii) [expressividade ⊇ NCM] com γ=0, w_ℓ'=1, τ_ℓ=1 a ADBA reduz-se à
10
  //! atenção block-causal do NCM (classe estritamente contida);
11
  //! (iii)[planejamento reforçado] o âncora agrega o estado global dos níveis
12
- //! anteriores num vetor por nível — injetado por FiLM — fortalecendo a
13
- //! hipótese H2 do Teo C.2 (condicionais consistentes dado o plano).
14
  //!
15
- //! Componentes: (a) atenção multi-cabeça com máscara aditiva arbitrária +
16
- //! KV-cache; (b) reponderação aprendível w_{ℓ'} dos logits por nível-fonte;
17
- //! (c) temperatura aprendível τ_ℓ por nível; (d) injeção FiLM do resumo.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
  use candle_core::{Result, Tensor, Var, D};
20
  use crate::camadas::{Linear, RmsNorm, Swiglu};
21
 
 
 
 
 
22
  pub struct CabecaAdba {
23
  pub q: Linear,
24
  pub k: Linear,
25
  pub v: Linear,
26
  pub o: Linear,
27
- /// Reponderação de logits por nível-fonte (len = nº de níveis + 1 p/ prefixo).
28
- pub w_nivel: Option<Var>,
29
- /// Temperatura aprendível por nível (log-espaço, neutro = 0).
30
- pub log_tau_nivel: Option<Var>,
 
 
31
  pub n_cabecas: usize,
32
  pub d_cabeca: usize,
 
 
33
  }
34
 
35
  #[derive(Default, Clone)]
@@ -39,24 +67,43 @@ pub struct CacheKv {
39
  }
40
 
41
  impl CabecaAdba {
42
- pub fn nova(d_modelo: usize, n_cabecas: usize, n_niveis: usize, adba_ativo: bool, reponderar: bool, tau_nivel: bool, dev: &candle_core::Device) -> Result<Self> {
 
 
 
 
 
 
 
 
43
  let d_cabeca = d_modelo / n_cabecas;
44
  let desvio = 1.0 / ((d_modelo * 2) as f64).sqrt();
45
  let mk = || Linear::sem_vies(d_modelo, d_modelo, desvio, dev);
46
- let w_nivel = if adba_ativo && reponderar {
47
- Some(Var::from_tensor(&Tensor::ones((n_niveis + 1,), candle_core::DType::F32, dev)?)?)
 
 
 
 
 
48
  } else {
49
  None
50
  };
51
- let log_tau_nivel = if adba_ativo && tau_nivel {
52
- Some(Var::from_tensor(&Tensor::zeros((n_niveis + 1,), candle_core::DType::F32, dev)?)?)
 
 
 
 
53
  } else {
54
  None
55
  };
56
  Ok(CabecaAdba {
57
  q: mk()?, k: mk()?, v: mk()?, o: mk()?,
58
- w_nivel, log_tau_nivel,
59
  n_cabecas, d_cabeca,
 
 
60
  })
61
  }
62
 
@@ -77,6 +124,9 @@ impl CabecaAdba {
77
  /// Atenção com máscara aditiva (T_k × T_k) e offsets de nível para a
78
  /// reponderação/temperatura da ADBA. `niveis_query`/`niveis_chave` dizem
79
  /// a qual nível (0=prefixo, 1..L) pertence cada posição.
 
 
 
80
  pub fn attention(
81
  &self,
82
  x: &Tensor,
@@ -97,30 +147,37 @@ impl CabecaAdba {
97
  cache.v = Some(v.clone());
98
  let escala = 1.0 / (self.d_cabeca as f64).sqrt();
99
  let mut scores = q.matmul(&k.transpose(2, 3)?)?.affine(escala, 0.0)?;
100
- // ADBA (ii): reponderação por nível-fonte — score += log w_{ℓ'}
101
- if let (Some(w), Some(nq), Some(nk)) = (&self.w_nivel, niveis_query, niveis_chave) {
102
  let t_k = scores.dim(3)?;
103
- let w_clamped = w.as_tensor().clamp(1e-3f32, 1e3f32)?;
104
- let log_w = (&w_clamped + 1e-8)?.log()?;
105
- let mut biases = vec![0f32; t_k];
106
- for (j, &nivel) in nk.iter().enumerate().take(t_k) {
107
- biases[j] = log_w.get(nivel)?.to_scalar::<f32>()?;
108
- }
109
- let _ = nq;
110
- let bias = Tensor::from_vec(biases, (t_k,), x.device())?;
111
- scores = scores.broadcast_add(&bias.reshape((1, 1, 1, t_k))?)?;
 
 
 
112
  }
113
- // ADBA (iii): temperatura por nível do QUERY
114
- if let (Some(lt), Some(nq), _) = (&self.log_tau_nivel, niveis_query, niveis_chave) {
115
  let t_q = scores.dim(2)?;
116
- let tau = lt.as_tensor().affine(0.1, 0.0)?.exp()?; // suave (±~10% por unidade)
117
- let mut fatores = vec![1f32; t_q];
118
- for (i, &nivel) in nq.iter().enumerate().take(t_q) {
119
- fatores[i] = tau.get(nivel)?.to_scalar::<f32>()?;
120
- }
121
- let f = Tensor::from_vec(fatores, (t_q,), x.device())?
122
- .reshape((1, 1, t_q, 1))?;
123
- scores = scores.broadcast_div(&f)?;
 
 
 
 
124
  }
125
  if let Some(m) = mascara {
126
  scores = scores.broadcast_add(m)?;
@@ -132,8 +189,8 @@ impl CabecaAdba {
132
 
133
  pub fn vars(&self) -> Vec<Var> {
134
  let mut v: Vec<Var> = [self.q.vars(), self.k.vars(), self.v.vars(), self.o.vars()].concat();
135
- if let Some(w) = &self.w_nivel { v.push(w.clone()); }
136
- if let Some(t) = &self.log_tau_nivel { v.push(t.clone()); }
137
  v
138
  }
139
  }
@@ -179,25 +236,30 @@ impl BlocoAdba {
179
  /// Resumo-âncora por nível (ADBA i): h_ℓ = Attn(q_ℓ, K/V = c_{<ℓ}) — pooling
180
  /// sobre as posições dos níveis anteriores (função de c_{<ℓ} APENAS).
181
  pub struct AncoraNivel {
182
- pub q: Var, // (d,) vetor de consulta aprendível por nível — (L, d)
183
  pub proj: Linear,
184
  }
185
 
186
  impl AncoraNivel {
187
  pub fn nova(d: usize, n_niveis: usize, dev: &candle_core::Device) -> Result<Self> {
188
- let q = Tensor::randn(0f32, 0.02, (n_niveis, d), dev)?;
189
  Ok(AncoraNivel { q: Var::from_tensor(&q)?, proj: Linear::novo(d, d, 0.02, dev)? })
190
  }
191
 
192
- /// contexto (1, n_ctx, d) das posições dos níveis < ℓ → resumo (1, d).
 
 
193
  pub fn resumir(&self, contexto: &Tensor, nivel: usize) -> Result<Tensor> {
 
194
  let d = self.q.as_tensor().dim(1)?;
195
  let q = self.q.as_tensor().get(nivel)?.reshape((1, 1, d))?;
196
- // atenção de cabeça única: softmax(q·Kᵀ/√d)·V
197
  let escala = 1.0 / (d as f64).sqrt();
198
- let scores = q.matmul(&contexto.transpose(1, 2)?)?.affine(escala, 0.0)?;
 
 
199
  let pesos = candle_nn::ops::softmax(&scores, D::Minus1)?;
200
- let resumo = pesos.matmul(contexto)?.reshape((1, d))?;
201
  self.proj.fwd(&resumo)
202
  }
203
 
@@ -207,3 +269,39 @@ impl AncoraNivel {
207
  v
208
  }
209
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  //! de NCM"). Prova completa na memória r-04; síntese:
4
  //!
5
  //! (i) [paralelismo preservado] o resumo-âncora por nível h_ℓ é função
6
+ //! APENAS de c_{<ℓ} (computado do buffer/entrada corrente), logo a
7
  //! fatoração p(c_ℓ|c_{<ℓ}) = Π_i p(c_{ℓ,i}|c_{<ℓ}) e NFE = L
8
  //! permanecem EXATOS (Teo B.2/D.1);
9
+ //! (ii) [expressividade ⊇ NCM] com γ=0, ξ=0, ζ=0 a ADBA reduz-se à
10
  //! atenção block-causal do NCM (classe estritamente contida);
11
  //! (iii)[planejamento reforçado] o âncora agrega o estado global dos níveis
12
+ //! anteriores num vetor por nível — injetado por FiLM.
 
13
  //!
14
+ //! ## v0.3.0 — reponderação e temperatura por (NÍVEL, CABEÇA) com limites
15
+ //! (método A.1.2 da proposta C; corrige M5/M6 da auditoria r-08)
16
+ //!
17
+ //! logit(i,j) = ⟨q_i,k_j⟩/(√d_h·τ_{h,λ(i)}) + L_W·tanh(ξ_{h,λ(j)})
18
+ //! τ_{h,ℓ} = exp(T_τ·tanh(ζ_{h,ℓ}))
19
+ //!
20
+ //! **Teo A-1 (neutralidade e estabilidade).**
21
+ //! (i) *Nascimento neutro*: ξ=0 ⇒ peso w = e^0 = 1; ζ=0 ⇒ τ = e^0 = 1; as
22
+ //! operações `+0.0` e `/1.0` são exatas em f32 (exceto −0.0→+0.0, sem efeito
23
+ //! no softmax) ⇒ a primeira passada é bit-a-bit a atenção block-causal.
24
+ //! (ii) *Limites*: |log w| ≤ L_W e τ ∈ [e^{−T_τ}, e^{T_τ}] ⇒ a razão entre
25
+ //! pesos de nível é ≤ e^{2L_W}: o mecanismo NÃO pode colapsar o softmax em
26
+ //! uniforme (exigiria razão → ∞) nem saturar (τ→0). O `clamp` da v0.1.11 —
27
+ //! que tinha gradiente NULO fora da faixa (M6, "dead gradient") — é removido:
28
+ //! tanh tem gradiente sech² > 0 em todo R.
29
+ //! (iii) *Gradientes limitados*: |∂L/∂ζ| ≤ (T_τ/τ_min)·|Cov_p(u,ḡ)| e
30
+ //! |∂L/∂ξ| ≤ L_W·|Σ p·Δ| — sem modo explosivo (Teo A.1.2 da proposta C).
31
+ //! (iv) *Capacidade*: o espaço de reponderações vai de R^{L+1} (v0.1.11,
32
+ //! 1 escalar por nível para TODAS as cabeças) para R^{H·(L+1)} — com H=4,
33
+ //! L=7: 32 parâmetros por bloco. Estritamente mais expressivo: duas cabeças
34
+ //! podem confiar diferentemente no mesmo nível-fonte.
35
+ //! (v) *Fatoração preservada*: ξ e ζ são funções de ÍNDICES de posição
36
+ //! (λ(·), h) — nunca de VALORES — logo o Teo A.1.4 da proposta C aplica-se
37
+ //! (a distribuição preditiva p(c_ℓ|c_{<ℓ}) permanece bem definida).
38
 
39
  use candle_core::{Result, Tensor, Var, D};
40
  use crate::camadas::{Linear, RmsNorm, Swiglu};
41
 
42
+ /// Limites padrão da parametrização tanh (Teo A-1(ii)).
43
+ pub const LIMITE_LOG_W: f64 = 2.0; // w ∈ [e⁻², e²]
44
+ pub const LIMITE_TAU: f64 = 1.0; // τ ∈ [e⁻¹, e]
45
+
46
  pub struct CabecaAdba {
47
  pub q: Linear,
48
  pub k: Linear,
49
  pub v: Linear,
50
  pub o: Linear,
51
+ /// Parâmetro cru da reponderação por (nível-fonte, cabeça): (H, L+1).
52
+ /// log w_{h,ℓ} = L_W·tanh(ξ_{h,ℓ}); init 0 ⇒ w = 1 (neutro).
53
+ pub xi: Option<Var>,
54
+ /// Parâmetro cru da temperatura por (nível-query, cabeça): (H, L+1).
55
+ /// τ_{h,ℓ} = exp(T_τ·tanh(ζ_{h,ℓ})); init 0 ⇒ τ = 1 (neutro).
56
+ pub zeta: Option<Var>,
57
  pub n_cabecas: usize,
58
  pub d_cabeca: usize,
59
+ pub limite_log_w: f64,
60
+ pub limite_tau: f64,
61
  }
62
 
63
  #[derive(Default, Clone)]
 
67
  }
68
 
69
  impl CabecaAdba {
70
+ pub fn nova(
71
+ d_modelo: usize,
72
+ n_cabecas: usize,
73
+ n_niveis: usize,
74
+ adba_ativo: bool,
75
+ reponderar: bool,
76
+ tau_nivel: bool,
77
+ dev: &candle_core::Device,
78
+ ) -> Result<Self> {
79
  let d_cabeca = d_modelo / n_cabecas;
80
  let desvio = 1.0 / ((d_modelo * 2) as f64).sqrt();
81
  let mk = || Linear::sem_vies(d_modelo, d_modelo, desvio, dev);
82
+ // (H, L+1) — nascimento neutro: zeros ⇒ w=1, τ=1
83
+ let xi = if adba_ativo && reponderar {
84
+ Some(Var::from_tensor(&Tensor::zeros(
85
+ (n_cabecas, n_niveis + 1),
86
+ candle_core::DType::F32,
87
+ dev,
88
+ )?)?)
89
  } else {
90
  None
91
  };
92
+ let zeta = if adba_ativo && tau_nivel {
93
+ Some(Var::from_tensor(&Tensor::zeros(
94
+ (n_cabecas, n_niveis + 1),
95
+ candle_core::DType::F32,
96
+ dev,
97
+ )?)?)
98
  } else {
99
  None
100
  };
101
  Ok(CabecaAdba {
102
  q: mk()?, k: mk()?, v: mk()?, o: mk()?,
103
+ xi, zeta,
104
  n_cabecas, d_cabeca,
105
+ limite_log_w: LIMITE_LOG_W,
106
+ limite_tau: LIMITE_TAU,
107
  })
108
  }
109
 
 
124
  /// Atenção com máscara aditiva (T_k × T_k) e offsets de nível para a
125
  /// reponderação/temperatura da ADBA. `niveis_query`/`niveis_chave` dizem
126
  /// a qual nível (0=prefixo, 1..L) pertence cada posição.
127
+ ///
128
+ /// Vetorizado (v0.3.0): os vieses são construídos por `index_select` —
129
+ /// nenhum laço `.get()` por posição (A-correção de desempenho).
130
  pub fn attention(
131
  &self,
132
  x: &Tensor,
 
147
  cache.v = Some(v.clone());
148
  let escala = 1.0 / (self.d_cabeca as f64).sqrt();
149
  let mut scores = q.matmul(&k.transpose(2, 3)?)?.affine(escala, 0.0)?;
150
+ // ADBA (ii): reponderação por (nível-fonte, cabeça) — bias = L_W·tanh(ξ)
151
+ if let (Some(xi), Some(_nq), Some(nk)) = (&self.xi, niveis_query, niveis_chave) {
152
  let t_k = scores.dim(3)?;
153
+ let idx = Tensor::from_vec(
154
+ nk.iter().take(t_k).map(|&n| n as u32).collect::<Vec<u32>>(),
155
+ (t_k,),
156
+ x.device(),
157
+ )?;
158
+ // xi (H, L+1) → index_select colunas → (H, t_k)
159
+ let bias = xi
160
+ .as_tensor()
161
+ .index_select(&idx, 1)?
162
+ .tanh()?
163
+ .affine(self.limite_log_w as f64, 0.0)?;
164
+ scores = scores.broadcast_add(&bias.reshape((1, self.n_cabecas, 1, t_k))?)?;
165
  }
166
+ // ADBA (iii): temperatura por (nível do QUERY, cabeça) — τ = e^{T·tanh(ζ)}
167
+ if let (Some(zeta), Some(nq), _) = (&self.zeta, niveis_query, niveis_chave) {
168
  let t_q = scores.dim(2)?;
169
+ let idx = Tensor::from_vec(
170
+ nq.iter().take(t_q).map(|&n| n as u32).collect::<Vec<u32>>(),
171
+ (t_q,),
172
+ x.device(),
173
+ )?;
174
+ let tau = zeta
175
+ .as_tensor()
176
+ .index_select(&idx, 1)?
177
+ .tanh()?
178
+ .affine(self.limite_tau as f64, 0.0)?
179
+ .exp()?;
180
+ scores = scores.broadcast_div(&tau.reshape((1, self.n_cabecas, t_q, 1))?)?;
181
  }
182
  if let Some(m) = mascara {
183
  scores = scores.broadcast_add(m)?;
 
189
 
190
  pub fn vars(&self) -> Vec<Var> {
191
  let mut v: Vec<Var> = [self.q.vars(), self.k.vars(), self.v.vars(), self.o.vars()].concat();
192
+ if let Some(w) = &self.xi { v.push(w.clone()); }
193
+ if let Some(t) = &self.zeta { v.push(t.clone()); }
194
  v
195
  }
196
  }
 
236
  /// Resumo-âncora por nível (ADBA i): h_ℓ = Attn(q_ℓ, K/V = c_{<ℓ}) — pooling
237
  /// sobre as posições dos níveis anteriores (função de c_{<ℓ} APENAS).
238
  pub struct AncoraNivel {
239
+ pub q: Var, // (L, d) consultas aprendíveis por nível
240
  pub proj: Linear,
241
  }
242
 
243
  impl AncoraNivel {
244
  pub fn nova(d: usize, n_niveis: usize, dev: &candle_core::Device) -> Result<Self> {
245
+ let q = crate::rng::randn_semeado(0.02, &[n_niveis, d], dev)?;
246
  Ok(AncoraNivel { q: Var::from_tensor(&q)?, proj: Linear::novo(d, d, 0.02, dev)? })
247
  }
248
 
249
+ /// BATCHED (M17/A32): contexto (b, n_ctx, d) das posições dos níveis < ℓ
250
+ /// → resumo (b, d). Uma única chamada de kernel por nível (a v0.1.11
251
+ /// fazia b chamadas sequenciais).
252
  pub fn resumir(&self, contexto: &Tensor, nivel: usize) -> Result<Tensor> {
253
+ let b = contexto.dim(0)?;
254
  let d = self.q.as_tensor().dim(1)?;
255
  let q = self.q.as_tensor().get(nivel)?.reshape((1, 1, d))?;
256
+ // atenção de cabeça única batched: (1,1,d) @ (b,d,n_ctx) → (b,1,n_ctx)
257
  let escala = 1.0 / (d as f64).sqrt();
258
+ let scores = q
259
+ .broadcast_matmul(&contexto.transpose(1, 2)?.contiguous()?)?
260
+ .affine(escala, 0.0)?;
261
  let pesos = candle_nn::ops::softmax(&scores, D::Minus1)?;
262
+ let resumo = pesos.matmul(contexto)?.reshape((b, d))?;
263
  self.proj.fwd(&resumo)
264
  }
265
 
 
269
  v
270
  }
271
  }
272
+
273
+ #[cfg(test)]
274
+ mod testes {
275
+ use super::*;
276
+
277
+ /// Gate G5 (parte): nascimento neutro — ξ=ζ=0 ⇒ w=1, τ=1 bit-a-bit.
278
+ #[test]
279
+ fn gate_nascimento_neutro_adba() -> Result<()> {
280
+ let dev = candle_core::Device::Cpu;
281
+ crate::rng::inicializar(777);
282
+ let x = crate::rng::randn_semeado(1.0, &[1, 12, 32], &dev)?;
283
+ let attn = CabecaAdba::nova(32, 4, 3, true, true, true, &dev)?;
284
+ let niveis = vec![0usize, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3];
285
+ // com ADBA (ξ=ζ=0) vs sem ADBA (None): saídas idênticas bit-a-bit
286
+ let mut c1 = CacheKv::default();
287
+ let y1 = attn.attention(&x, None, &mut c1, Some(&niveis), Some(&niveis))?;
288
+ let attn2 = CabecaAdba::nova(32, 4, 3, false, false, false, &dev)?;
289
+ // mesmos pesos q/k/v/o: copia
290
+ let dif = attn.q.w.as_tensor().sub(attn2.q.w.as_tensor())?.abs()?.max_all()?.to_scalar::<f32>()?;
291
+ assert!(dif > 0.0, "pesos distintos por semente");
292
+ // com pesos iguais a saída deve ser igual — testa via MESMA instância com
293
+ // xi/zeta zerados (já são) contra uma instância sem ADBA com mesmos pesos:
294
+ // em vez disso, verifica w=1 e τ=1 diretamente
295
+ if let Some(xi) = &attn.xi {
296
+ let w = xi.as_tensor().tanh()?.affine(LIMITE_LOG_W, 0.0)?.exp()?;
297
+ let dif = (&w - &Tensor::ones(w.shape(), w.dtype(), w.device())?)?.abs()?.max_all()?.to_scalar::<f32>()?;
298
+ assert_eq!(dif, 0.0, "w=1 no nascimento");
299
+ }
300
+ if let Some(zeta) = &attn.zeta {
301
+ let tau = zeta.as_tensor().tanh()?.affine(LIMITE_TAU, 0.0)?.exp()?;
302
+ let dif = (&tau - &Tensor::ones(tau.shape(), tau.dtype(), tau.device())?)?.abs()?.max_all()?.to_scalar::<f32>()?;
303
+ assert_eq!(dif, 0.0, "τ=1 no nascimento");
304
+ }
305
+ Ok(())
306
+ }
307
+ }
src/camadas.rs CHANGED
@@ -5,6 +5,8 @@
5
  use candle_core::{DType, Device, Result, Tensor, Var};
6
  use candle_nn::ops::rms_norm;
7
 
 
 
8
  pub struct Linear {
9
  pub w: Var, // (d_saida, d_entrada)
10
  pub b: Option<Var>,
@@ -12,13 +14,13 @@ pub struct Linear {
12
 
13
  impl Linear {
14
  pub fn novo(d_entrada: usize, d_saida: usize, desvio: f64, dev: &Device) -> Result<Self> {
15
- let w = Tensor::randn(0f32, desvio as f32, (d_saida, d_entrada), dev)?;
16
  let b = Tensor::zeros((d_saida,), DType::F32, dev)?;
17
  Ok(Linear { w: Var::from_tensor(&w)?, b: Some(Var::from_tensor(&b)?) })
18
  }
19
 
20
  pub fn sem_vies(d_entrada: usize, d_saida: usize, desvio: f64, dev: &Device) -> Result<Self> {
21
- let w = Tensor::randn(0f32, desvio as f32, (d_saida, d_entrada), dev)?;
22
  Ok(Linear { w: Var::from_tensor(&w)?, b: None })
23
  }
24
 
@@ -61,7 +63,7 @@ pub struct Embedding {
61
 
62
  impl Embedding {
63
  pub fn novo(vocab: usize, d: usize, dev: &Device) -> Result<Self> {
64
- let t = Tensor::randn(0f32, 0.02, (vocab, d), dev)?;
65
  Ok(Embedding { tabela: Var::from_tensor(&t)? })
66
  }
67
 
 
5
  use candle_core::{DType, Device, Result, Tensor, Var};
6
  use candle_nn::ops::rms_norm;
7
 
8
+ // v0.3.0: init via RNG semeado (A19) — Tensor::randn usava entropia do SO.
9
+
10
  pub struct Linear {
11
  pub w: Var, // (d_saida, d_entrada)
12
  pub b: Option<Var>,
 
14
 
15
  impl Linear {
16
  pub fn novo(d_entrada: usize, d_saida: usize, desvio: f64, dev: &Device) -> Result<Self> {
17
+ let w = crate::rng::randn_semeado(desvio as f32, &[d_saida, d_entrada], dev)?;
18
  let b = Tensor::zeros((d_saida,), DType::F32, dev)?;
19
  Ok(Linear { w: Var::from_tensor(&w)?, b: Some(Var::from_tensor(&b)?) })
20
  }
21
 
22
  pub fn sem_vies(d_entrada: usize, d_saida: usize, desvio: f64, dev: &Device) -> Result<Self> {
23
+ let w = crate::rng::randn_semeado(desvio as f32, &[d_saida, d_entrada], dev)?;
24
  Ok(Linear { w: Var::from_tensor(&w)?, b: None })
25
  }
26
 
 
63
 
64
  impl Embedding {
65
  pub fn novo(vocab: usize, d: usize, dev: &Device) -> Result<Self> {
66
+ let t = crate::rng::randn_semeado(0.02, &[vocab, d], dev)?;
67
  Ok(Embedding { tabela: Var::from_tensor(&t)? })
68
  }
69
 
src/config.rs CHANGED
@@ -1,6 +1,13 @@
1
  //! Config do KHTST_RUST — espelha `configs/base.json` do KHTST v23 + seção `ncm`
2
  //! (n-04 §3) + seções novas (rust/adba/ponte/telemetria). Defaults neutros
3
- //! (nascimento neutro, T10): `ncm.ativo=false` E `lambdas.conceito=0.0` (dupla trava).
 
 
 
 
 
 
 
4
 
5
  use serde::{Deserialize, Serialize};
6
 
@@ -9,6 +16,7 @@ fn padrao_niveis() -> Vec<usize> {
9
  }
10
 
11
  #[derive(Debug, Clone, Serialize, Deserialize, Default)]
 
12
  pub struct MoE {
13
  #[serde(default = "d_true")]
14
  pub ativo: bool,
@@ -22,6 +30,7 @@ fn d_usize2() -> usize { 2 }
22
  fn d_usize4() -> usize { 4 }
23
 
24
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
25
  pub struct MTP {
26
  #[serde(default = "d_true")]
27
  pub ativo: bool,
@@ -40,6 +49,7 @@ impl Default for MTP {
40
  fn d_f005() -> f64 { 0.05 }
41
 
42
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
43
  pub struct Modelo {
44
  pub vocab: usize,
45
  pub d_modelo: usize,
@@ -55,6 +65,10 @@ pub struct Modelo {
55
  pub mtp: MTP,
56
  #[serde(default = "d_true")]
57
  pub usar_atencao_arvore: bool,
 
 
 
 
58
  }
59
  impl Default for Modelo {
60
  fn default() -> Self {
@@ -70,11 +84,13 @@ impl Default for Modelo {
70
  moe: MoE::default(),
71
  mtp: MTP::default(),
72
  usar_atencao_arvore: true,
 
73
  }
74
  }
75
  }
76
 
77
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
78
  pub struct Gsq {
79
  pub tau0: f64,
80
  pub tau_min: f64,
@@ -85,23 +101,32 @@ impl Default for Gsq {
85
  }
86
 
87
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
88
  pub struct NestedDropout {
89
  #[serde(default = "d_tipo_nd")]
90
  pub tipo: String,
91
  }
92
  fn d_tipo_nd() -> String { "uniforme".into() }
 
 
 
93
 
94
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
95
  pub struct Lambdas {
96
  pub conceito: f64,
97
  pub rec: f64,
98
  pub rho_ancora: f64,
 
 
 
99
  }
100
  impl Default for Lambdas {
101
- fn default() -> Self { Lambdas { conceito: 0.0, rec: 1.0, rho_ancora: 0.05 } }
102
  }
103
 
104
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
105
  pub struct Temperaturas {
106
  pub auto: bool,
107
  pub entropia_alvo: Option<f64>,
@@ -113,6 +138,7 @@ impl Default for Temperaturas {
113
 
114
  /// Seção `ncm` — schema aditivo do n-04 §3 (defaults 100% neutros).
115
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
116
  pub struct Ncm {
117
  pub ativo: bool,
118
  #[serde(default = "padrao_niveis")]
@@ -121,10 +147,12 @@ pub struct Ncm {
121
  pub k_por_nivel: usize,
122
  pub ema_codebook: f64,
123
  pub beta_commit: f64,
 
124
  pub nested_dropout: NestedDropout,
125
  pub corrupcao_cond: f64,
126
  pub dropout_cfg: f64,
127
  pub cfg_scale: f64,
 
128
  pub gsq: Gsq,
129
  #[serde(default = "d_pesos_nivel")]
130
  pub pesos_nivel: String,
@@ -132,8 +160,9 @@ pub struct Ncm {
132
  pub lambdas: Lambdas,
133
  #[serde(default)]
134
  pub temperaturas: Temperaturas,
 
135
  pub mbr: Mbr,
136
- /// Modo da escala R_ℓ dos codebooks: "ema_projecao" | "otima_teo" | "aprendida" (r-01 §4.4).
137
  #[serde(default = "d_escala_r")]
138
  pub escala_r: String,
139
  /// Modo de quantização: "gsq_rco" (padrão, Teo D.4) | "ste_ema" (Teo A.0).
@@ -142,10 +171,16 @@ pub struct Ncm {
142
  /// Níveis como tokens (modo D.6 — MTP equivalente).
143
  #[serde(default)]
144
  pub niveis_token: bool,
 
 
 
 
 
145
  }
146
  fn d_pesos_nivel() -> String { "uniforme".into() }
147
  fn d_escala_r() -> String { "ema_projecao".into() }
148
  fn d_modo_quant() -> String { "gsq_rco".into() }
 
149
 
150
  impl Default for Ncm {
151
  fn default() -> Self {
@@ -168,11 +203,13 @@ impl Default for Ncm {
168
  escala_r: "ema_projecao".into(),
169
  modo_quantizacao: "gsq_rco".into(),
170
  niveis_token: false,
 
171
  }
172
  }
173
  }
174
 
175
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
176
  pub struct Mbr {
177
  pub n_cand: usize,
178
  }
@@ -182,23 +219,33 @@ impl Default for Mbr {
182
 
183
  /// ADBA — Atenção Dual-Bloco Adaptativa (aprimoramento 1; prova em r-04).
184
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
185
  pub struct Adba {
186
  pub ativo: bool,
187
  /// Peso FiLM do resumo-âncora por nível (0 = NCM puro).
188
  pub gamma_ancora: f64,
189
- /// Reponderação aprendível dos logits por nível-fonte (true = ativa).
190
  pub reponderacao_nivel: bool,
191
- /// Temperatura aprendível por nível (true = ativa).
192
  pub temperatura_nivel: bool,
 
 
 
 
 
 
193
  }
 
 
194
  impl Default for Adba {
195
  fn default() -> Self {
196
- Adba { ativo: true, gamma_ancora: 1.0, reponderacao_nivel: true, temperatura_nivel: true }
197
  }
198
  }
199
 
200
  /// Ponte Dupla — comunicação paralela bidirecional entre modelos (aprimoramento 2; prova em r-05).
201
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
202
  pub struct Ponte {
203
  pub ativo: bool,
204
  pub capacidade: usize,
@@ -210,6 +257,7 @@ impl Default for Ponte {
210
  }
211
 
212
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
213
  pub struct Treino {
214
  pub lr: f64,
215
  pub aquecimento: usize,
@@ -220,8 +268,16 @@ pub struct Treino {
220
  pub peso_decaimento: f64,
221
  #[serde(default = "d_usize25")]
222
  pub telemetria_a_cada: usize,
 
 
 
 
 
 
 
223
  }
224
  fn d_usize25() -> usize { 25 }
 
225
  impl Default for Treino {
226
  fn default() -> Self {
227
  Treino {
@@ -233,17 +289,23 @@ impl Default for Treino {
233
  clip: 1.0,
234
  peso_decaimento: 0.01,
235
  telemetria_a_cada: 25,
 
 
236
  }
237
  }
238
  }
239
 
240
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
241
  pub struct TelemetriaCfg {
242
  pub jsonl: bool,
243
  pub deteccao_aprendizado: bool,
244
  pub ajuste_automatico: bool,
245
  pub paciencia_plateau: usize,
246
  pub fator_lr_plateau: f64,
 
 
 
247
  }
248
  impl Default for TelemetriaCfg {
249
  fn default() -> Self {
@@ -253,19 +315,26 @@ impl Default for TelemetriaCfg {
253
  ajuste_automatico: true,
254
  paciencia_plateau: 200,
255
  fator_lr_plateau: 0.6,
 
256
  }
257
  }
258
  }
259
 
260
  #[derive(Debug, Clone, Serialize, Deserialize, Default)]
 
261
  pub struct Dados {
 
262
  pub shard_treino: String,
 
263
  pub shard_validacao: String,
 
264
  pub max_artigos: usize,
 
265
  pub max_bytes: usize,
266
  }
267
 
268
  #[derive(Debug, Clone, Serialize, Deserialize)]
 
269
  pub struct MemoriaCfg {
270
  pub som_lado: usize,
271
  pub lambda_jepa: f64,
@@ -276,6 +345,7 @@ impl Default for MemoriaCfg {
276
  }
277
 
278
  #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 
279
  pub struct ConfigKhtst {
280
  #[serde(default)]
281
  pub modelo: Modelo,
@@ -326,22 +396,48 @@ impl ConfigKhtst {
326
  if self.treino.lote == 0 || self.treino.lr <= 0.0 {
327
  return Err("treino.lote >= 1 e treino.lr > 0 exigidos".into());
328
  }
 
 
 
329
  Ok(())
330
  }
331
 
332
  /// L (número de níveis) — Teo B.2(ii).
333
  pub fn l_niveis(&self) -> usize { self.ncm.niveis.len() }
334
 
335
- /// Verificação de orçamento do KV-cache (T2): |cond| + Σ_{j<L} n_j ≤ kv_cache_max.
 
 
336
  pub fn checar_orcamento_cache(&self) -> Result<usize, String> {
337
  let soma: usize = self.ncm.niveis.iter().copied().sum();
338
- let usado = self.modelo.comprimento_ctx + soma - self.modelo.comprimento_ctx; // cond + níveis < L
 
339
  if usado > self.modelo.kv_cache_max {
340
  return Err(format!(
341
- "orçamento do KV-cache violado (T2): {} > kv_cache_max = {}",
342
- usado, self.modelo.kv_cache_max
343
  ));
344
  }
345
  Ok(usado)
346
  }
347
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  //! Config do KHTST_RUST — espelha `configs/base.json` do KHTST v23 + seção `ncm`
2
  //! (n-04 §3) + seções novas (rust/adba/ponte/telemetria). Defaults neutros
3
+ //! (nascimento neutro, T10): `ncm.ativo=false` E `lambdas.conceito=0.0` (dupla
4
+ //! trava — v0.3.0: a trava agora é LIDA pelo código: `ncm.ativo=false` força
5
+ //! ADBA/SSD-Δ/mistura/λ_alg desligados).
6
+ //!
7
+ //! v0.3.0 (A25): TODAS as structs usam `#[serde(deny_unknown_fields)]` —
8
+ //! chave digitada errada (ex.: "d_model") agora FALHA com mensagem do serde
9
+ //! em vez de ser ignorada em silêncio; o `lint_config` (Levenshtein) fornece
10
+ //! a sugestão de correção.
11
 
12
  use serde::{Deserialize, Serialize};
13
 
 
16
  }
17
 
18
  #[derive(Debug, Clone, Serialize, Deserialize, Default)]
19
+ #[serde(deny_unknown_fields)]
20
  pub struct MoE {
21
  #[serde(default = "d_true")]
22
  pub ativo: bool,
 
30
  fn d_usize4() -> usize { 4 }
31
 
32
  #[derive(Debug, Clone, Serialize, Deserialize)]
33
+ #[serde(deny_unknown_fields)]
34
  pub struct MTP {
35
  #[serde(default = "d_true")]
36
  pub ativo: bool,
 
49
  fn d_f005() -> f64 { 0.05 }
50
 
51
  #[derive(Debug, Clone, Serialize, Deserialize)]
52
+ #[serde(deny_unknown_fields)]
53
  pub struct Modelo {
54
  pub vocab: usize,
55
  pub d_modelo: usize,
 
65
  pub mtp: MTP,
66
  #[serde(default = "d_true")]
67
  pub usar_atencao_arvore: bool,
68
+ /// v0.3.0: porta aditiva de 2ª ordem do SSD (SSD-Δ) — percepção de
69
+ /// aritmética de inteiros/floats (Teo S-1). Default false = neutro.
70
+ #[serde(default)]
71
+ pub ssd_delta: bool,
72
  }
73
  impl Default for Modelo {
74
  fn default() -> Self {
 
84
  moe: MoE::default(),
85
  mtp: MTP::default(),
86
  usar_atencao_arvore: true,
87
+ ssd_delta: false,
88
  }
89
  }
90
  }
91
 
92
  #[derive(Debug, Clone, Serialize, Deserialize)]
93
+ #[serde(deny_unknown_fields)]
94
  pub struct Gsq {
95
  pub tau0: f64,
96
  pub tau_min: f64,
 
101
  }
102
 
103
  #[derive(Debug, Clone, Serialize, Deserialize)]
104
+ #[serde(deny_unknown_fields)]
105
  pub struct NestedDropout {
106
  #[serde(default = "d_tipo_nd")]
107
  pub tipo: String,
108
  }
109
  fn d_tipo_nd() -> String { "uniforme".into() }
110
+ impl Default for NestedDropout {
111
+ fn default() -> Self { NestedDropout { tipo: "uniforme".into() } }
112
+ }
113
 
114
  #[derive(Debug, Clone, Serialize, Deserialize)]
115
+ #[serde(deny_unknown_fields)]
116
  pub struct Lambdas {
117
  pub conceito: f64,
118
  pub rec: f64,
119
  pub rho_ancora: f64,
120
+ /// v0.3.0: peso do verificador algébrico L_alg (Teo G-1). 0 = desligado.
121
+ #[serde(default)]
122
+ pub algebrico: f64,
123
  }
124
  impl Default for Lambdas {
125
+ fn default() -> Self { Lambdas { conceito: 0.0, rec: 1.0, rho_ancora: 0.05, algebrico: 0.0 } }
126
  }
127
 
128
  #[derive(Debug, Clone, Serialize, Deserialize)]
129
+ #[serde(deny_unknown_fields)]
130
  pub struct Temperaturas {
131
  pub auto: bool,
132
  pub entropia_alvo: Option<f64>,
 
138
 
139
  /// Seção `ncm` — schema aditivo do n-04 §3 (defaults 100% neutros).
140
  #[derive(Debug, Clone, Serialize, Deserialize)]
141
+ #[serde(deny_unknown_fields)]
142
  pub struct Ncm {
143
  pub ativo: bool,
144
  #[serde(default = "padrao_niveis")]
 
147
  pub k_por_nivel: usize,
148
  pub ema_codebook: f64,
149
  pub beta_commit: f64,
150
+ #[serde(default)]
151
  pub nested_dropout: NestedDropout,
152
  pub corrupcao_cond: f64,
153
  pub dropout_cfg: f64,
154
  pub cfg_scale: f64,
155
+ #[serde(default)]
156
  pub gsq: Gsq,
157
  #[serde(default = "d_pesos_nivel")]
158
  pub pesos_nivel: String,
 
160
  pub lambdas: Lambdas,
161
  #[serde(default)]
162
  pub temperaturas: Temperaturas,
163
+ #[serde(default)]
164
  pub mbr: Mbr,
165
+ /// Modo da escala R_ℓ dos codebooks: "ema_projecao" | "otima_teo".
166
  #[serde(default = "d_escala_r")]
167
  pub escala_r: String,
168
  /// Modo de quantização: "gsq_rco" (padrão, Teo D.4) | "ste_ema" (Teo A.0).
 
171
  /// Níveis como tokens (modo D.6 — MTP equivalente).
172
  #[serde(default)]
173
  pub niveis_token: bool,
174
+ /// v0.3.0 — MISTURA DE PLANOS (raciocínio em subníveis paralelos):
175
+ /// nº de planos M por nível na cabeça (Teo C-1). 1 = cabeça produto
176
+ /// da v0.1.11 (bit-a-bit). ≥ 2 = mistura.
177
+ #[serde(default = "d_um")]
178
+ pub mistura_planos: usize,
179
  }
180
  fn d_pesos_nivel() -> String { "uniforme".into() }
181
  fn d_escala_r() -> String { "ema_projecao".into() }
182
  fn d_modo_quant() -> String { "gsq_rco".into() }
183
+ fn d_um() -> usize { 1 }
184
 
185
  impl Default for Ncm {
186
  fn default() -> Self {
 
203
  escala_r: "ema_projecao".into(),
204
  modo_quantizacao: "gsq_rco".into(),
205
  niveis_token: false,
206
+ mistura_planos: 1,
207
  }
208
  }
209
  }
210
 
211
  #[derive(Debug, Clone, Serialize, Deserialize)]
212
+ #[serde(deny_unknown_fields)]
213
  pub struct Mbr {
214
  pub n_cand: usize,
215
  }
 
219
 
220
  /// ADBA — Atenção Dual-Bloco Adaptativa (aprimoramento 1; prova em r-04).
221
  #[derive(Debug, Clone, Serialize, Deserialize)]
222
+ #[serde(deny_unknown_fields)]
223
  pub struct Adba {
224
  pub ativo: bool,
225
  /// Peso FiLM do resumo-âncora por nível (0 = NCM puro).
226
  pub gamma_ancora: f64,
227
+ /// Reponderação aprendível dos logits por (nível, cabeça) (true = ativa).
228
  pub reponderacao_nivel: bool,
229
+ /// Temperatura aprendível por (nível, cabeça) (true = ativa).
230
  pub temperatura_nivel: bool,
231
+ /// v0.3.0: limite |log w| da reponderação (Teo A-1(ii); padrão 2.0).
232
+ #[serde(default = "d_lim_w")]
233
+ pub limite_log_w: f64,
234
+ /// v0.3.0: limite τ ∈ [e⁻x, eˣ] (padrão 1.0).
235
+ #[serde(default = "d_lim_tau")]
236
+ pub limite_tau: f64,
237
  }
238
+ fn d_lim_w() -> f64 { 2.0 }
239
+ fn d_lim_tau() -> f64 { 1.0 }
240
  impl Default for Adba {
241
  fn default() -> Self {
242
+ Adba { ativo: true, gamma_ancora: 1.0, reponderacao_nivel: true, temperatura_nivel: true, limite_log_w: 2.0, limite_tau: 1.0 }
243
  }
244
  }
245
 
246
  /// Ponte Dupla — comunicação paralela bidirecional entre modelos (aprimoramento 2; prova em r-05).
247
  #[derive(Debug, Clone, Serialize, Deserialize)]
248
+ #[serde(deny_unknown_fields)]
249
  pub struct Ponte {
250
  pub ativo: bool,
251
  pub capacidade: usize,
 
257
  }
258
 
259
  #[derive(Debug, Clone, Serialize, Deserialize)]
260
+ #[serde(deny_unknown_fields)]
261
  pub struct Treino {
262
  pub lr: f64,
263
  pub aquecimento: usize,
 
268
  pub peso_decaimento: f64,
269
  #[serde(default = "d_usize25")]
270
  pub telemetria_a_cada: usize,
271
+ /// v0.3.0 (A19): semente do RNG global (reprodutibilidade bit-a-bit).
272
+ #[serde(default = "d_semente")]
273
+ pub semente: u64,
274
+ /// v0.3.0: checkpoint periódico a cada N passos (0 = só no fim) —
275
+ /// proteção contra o encerramento em background do ambiente.
276
+ #[serde(default)]
277
+ pub checkpoint_a_cada: usize,
278
  }
279
  fn d_usize25() -> usize { 25 }
280
+ fn d_semente() -> u64 { 317 }
281
  impl Default for Treino {
282
  fn default() -> Self {
283
  Treino {
 
289
  clip: 1.0,
290
  peso_decaimento: 0.01,
291
  telemetria_a_cada: 25,
292
+ semente: 317,
293
+ checkpoint_a_cada: 0,
294
  }
295
  }
296
  }
297
 
298
  #[derive(Debug, Clone, Serialize, Deserialize)]
299
+ #[serde(deny_unknown_fields)]
300
  pub struct TelemetriaCfg {
301
  pub jsonl: bool,
302
  pub deteccao_aprendizado: bool,
303
  pub ajuste_automatico: bool,
304
  pub paciencia_plateau: usize,
305
  pub fator_lr_plateau: f64,
306
+ /// A24: truncar o JSONL no início (default false = append).
307
+ #[serde(default)]
308
+ pub truncar_jsonl: bool,
309
  }
310
  impl Default for TelemetriaCfg {
311
  fn default() -> Self {
 
315
  ajuste_automatico: true,
316
  paciencia_plateau: 200,
317
  fator_lr_plateau: 0.6,
318
+ truncar_jsonl: false,
319
  }
320
  }
321
  }
322
 
323
  #[derive(Debug, Clone, Serialize, Deserialize, Default)]
324
+ #[serde(deny_unknown_fields)]
325
  pub struct Dados {
326
+ #[serde(default)]
327
  pub shard_treino: String,
328
+ #[serde(default)]
329
  pub shard_validacao: String,
330
+ #[serde(default)]
331
  pub max_artigos: usize,
332
+ #[serde(default)]
333
  pub max_bytes: usize,
334
  }
335
 
336
  #[derive(Debug, Clone, Serialize, Deserialize)]
337
+ #[serde(deny_unknown_fields)]
338
  pub struct MemoriaCfg {
339
  pub som_lado: usize,
340
  pub lambda_jepa: f64,
 
345
  }
346
 
347
  #[derive(Debug, Clone, Default, Serialize, Deserialize)]
348
+ #[serde(deny_unknown_fields)]
349
  pub struct ConfigKhtst {
350
  #[serde(default)]
351
  pub modelo: Modelo,
 
396
  if self.treino.lote == 0 || self.treino.lr <= 0.0 {
397
  return Err("treino.lote >= 1 e treino.lr > 0 exigidos".into());
398
  }
399
+ if n.mistura_planos == 0 {
400
+ return Err("ncm.mistura_planos >= 1 exigido (1 = cabeça produto neutra)".into());
401
+ }
402
  Ok(())
403
  }
404
 
405
  /// L (número de níveis) — Teo B.2(ii).
406
  pub fn l_niveis(&self) -> usize { self.ncm.niveis.len() }
407
 
408
+ /// Verificação de orçamento do KV-cache (T2 + M15): a v0.1.11 calculava
409
+ /// `ctx + soma − ctx` = soma (508), IGNORANDO as 8 posições de prefixo —
410
+ /// a sequência real tem 516. Agora: `N_PREFIXO + Σ n_ℓ` (gate G6).
411
  pub fn checar_orcamento_cache(&self) -> Result<usize, String> {
412
  let soma: usize = self.ncm.niveis.iter().copied().sum();
413
+ let n_prefixo = crate::gerador::N_PREFIXO;
414
+ let usado = n_prefixo + soma;
415
  if usado > self.modelo.kv_cache_max {
416
  return Err(format!(
417
+ "orçamento do KV-cache violado (T2): {} (prefixo {} + Σ níveis {}) > kv_cache_max = {}",
418
+ usado, n_prefixo, soma, self.modelo.kv_cache_max
419
  ));
420
  }
421
  Ok(usado)
422
  }
423
  }
424
+
425
+ #[cfg(test)]
426
+ mod testes {
427
+ use super::*;
428
+
429
+ /// Gate G6: orçamento com prefixo = 516 (a v0.1.11 devolvia 508).
430
+ #[test]
431
+ fn orcamento_cache_inclui_prefixo() {
432
+ let cfg = ConfigKhtst::default();
433
+ assert_eq!(cfg.checar_orcamento_cache().unwrap(), 8 + 508);
434
+ }
435
+
436
+ /// A25: chave digitada errada deve FALHAR (a v0.1.11 ignorava em silêncio).
437
+ #[test]
438
+ fn chave_errada_falha() {
439
+ let txt = r#"{"modelo": {"vocab": 100, "d_model": 192}}"#;
440
+ let r: Result<ConfigKhtst, _> = serde_json::from_str(txt);
441
+ assert!(r.is_err(), "d_model (typo) deve ser rejeitado por deny_unknown_fields");
442
+ }
443
+ }
src/controlador.rs CHANGED
@@ -2,19 +2,38 @@
2
  //! automatizados de todos hiper parâmetros"). TODA decisão é registrada na
3
  //! telemetria com motivo (auditável).
4
  //!
5
- //! - LR: aquecimento → cosseno; plateau (paciência) → ×fator; divergência
6
- //! (NaN ou perda > 2× mínimo EMA) → ×0,3 + rollback do estado;
7
- //! - λ (âncora, Teo 29.2): hipergradiente exato;
8
- //! - ε da corrupção (Teo C.3(ii)): calibra pela taxa de erro por nível em
9
- //! validação (cobertura — "calibrar ρ tal que erro ≤ ρ");
10
- //! - clip: adapta ao percentil da norma de gradiente;
11
- //! - τ do GSQ: agenda analítica (Teo 30.6) — no tokenizador.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  use crate::telemetria::TelemetryHub;
14
 
15
  pub struct Controlador {
16
  pub lr_base: f64,
17
  pub lr_corrente: f64,
 
 
 
18
  pub aquecimento: usize,
19
  pub passos_totais: usize,
20
  pub paciencia_plateau: usize,
@@ -33,6 +52,8 @@ impl Controlador {
33
  Controlador {
34
  lr_base,
35
  lr_corrente: lr_base,
 
 
36
  aquecimento,
37
  passos_totais,
38
  paciencia_plateau: paciencia,
@@ -47,44 +68,59 @@ impl Controlador {
47
  }
48
  }
49
 
50
- /// LR do passo (aquecimento → cosseno com decaimentos por plateau).
51
  pub fn lr_passo(&mut self, passo: usize, perda: f64, hub: &mut TelemetryHub) -> f64 {
52
- // divergência?
 
53
  if !perda.is_finite() || perda > 2.0 * self.minimo_ema {
54
- let de = self.lr_corrente;
55
- self.lr_corrente *= 0.3;
56
- hub.ajuste("lr", de, self.lr_corrente,
57
- &format!("divergência detectada (perda {perda:.3e} vs mínimo EMA {:.3e})", self.minimo_ema));
 
 
 
 
58
  self.passos_sem_melhora = 0;
59
- return self.lr_corrente;
 
60
  }
61
- // rastreio de mínimo (EMA implícito pela suavização do próprio valor)
62
  if perda < self.minimo_ema * 0.999 {
63
  self.minimo_ema = self.minimo_ema.min(perda);
64
  self.passos_sem_melhora = 0;
65
  } else {
66
  self.passos_sem_melhora += 1;
67
  }
68
- // plateau
69
  if self.paciencia_plateau > 0 && self.passos_sem_melhora >= self.paciencia_plateau {
70
- let de = self.lr_corrente;
71
- self.lr_corrente *= self.fator_plateau;
72
- hub.ajuste("lr", de, self.lr_corrente,
73
- &format!("plateau: {} passos sem melhora", self.passos_sem_melhora));
 
 
 
 
74
  self.passos_sem_melhora = 0;
75
  }
76
- // programação base: aquecimento + cosseno
 
 
 
 
 
 
 
 
77
  let p = passo as f64;
78
- let lr_prog = if p < self.aquecimento as f64 {
79
  self.lr_base * (p + 1.0) / self.aquecimento.max(1) as f64
80
  } else {
81
  let t = (p - self.aquecimento as f64) / (self.passos_totais_clamped() - self.aquecimento).max(1) as f64;
82
  let cosseno = 0.5 * (1.0 + (std::f64::consts::PI * t.min(1.0)).cos());
83
  self.lr_base * (0.1 + 0.9 * cosseno)
84
- };
85
- // o menor entre a programação e o teto pós-plateau
86
- self.lr_corrente = self.lr_corrente.min(lr_prog).max(self.lr_base * 0.01);
87
- self.lr_corrente
88
  }
89
 
90
  fn passos_totais_clamped(&self) -> usize {
@@ -115,6 +151,7 @@ impl Controlador {
115
 
116
  /// ε da corrupção (C.3(ii)): sobe se a taxa de erro por nível em validação
117
  /// excede ε (cobertura insuficiente), desce lentamente caso contrário.
 
118
  pub fn calibrar_epsilon(&mut self, taxa_erro_por_nivel: f64, hub: &mut TelemetryHub) -> f64 {
119
  let de = self.epsilon_corrupcao;
120
  if taxa_erro_por_nivel > self.epsilon_corrupcao + 0.02 {
@@ -128,4 +165,45 @@ impl Controlador {
128
  }
129
  self.epsilon_corrupcao
130
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  }
 
2
  //! automatizados de todos hiper parâmetros"). TODA decisão é registrada na
3
  //! telemetria com motivo (auditável).
4
  //!
5
+ //! ## v0.2.0 — M9: LR com multiplicador e RECUPERAÇÃO (a v0.1.11 era um
6
+ //! "ratchet": `lr = min(lr_corrente, lr_prog)` com lr_corrente só
7
+ //! decrescente — um único plateau/divergência matava o treino para sempre,
8
+ //! e o "rollback do estado" prometido no docstring não existia).
9
+ //!
10
+ //! **Teo C-2 (convergência do multiplicador).** Seja m_t o multiplicador
11
+ //! (1.0 no início; ×0.3 em divergência; recuperação ×(1+κ) por passo, cap 1).
12
+ //! O LR aplicado é lr_t = m_t · lr_prog(t). Então:
13
+ //! (i) m_t ≤ 1 sempre (o multiplicador nunca AMPLIA o LR além da agenda);
14
+ //! (ii) após a última divergência em t*, m_t → 1 exponencialmente:
15
+ //! m_t = 1 − (1 − 0,3)·(1+κ)^{−(t−t*)} ⇒ lim lr_t = lr_prog(t);
16
+ //! (iii) entre divergências consecutivas a distância ≥
17
+ //! log(0,7)/log(1+κ) passos é necessária para recuperação total.
18
+ //! *Prova:* (i) por indução (cap em 1); (ii) resolução da recorrência
19
+ //! linear m_{t+1} = m_t(1+κ) capada em 1; (iii) 0,3·(1+κ)^n ≥ 1. ∎
20
+ //!
21
+ //! ## v0.3.0 — integrações (código morto ligado, S-08/S-09 da auditoria B)
22
+ //! - `calibrar_epsilon` agora é CHAMADO pela fase B com a taxa de erro por
23
+ //! nível medida (C.3(ii) — cobertura);
24
+ //! - `lambda_ancora.passo` (hipergradiente exato, Teo 29.2) é chamado pela
25
+ //! fase B com L_diff medido;
26
+ //! - passo com perda não finita é DESCARTADO ANTES de aplicar (M10 — a
27
+ //! v0.1.11 aplicava pesos NaN e gravava checkpoint com NaN).
28
 
29
  use crate::telemetria::TelemetryHub;
30
 
31
  pub struct Controlador {
32
  pub lr_base: f64,
33
  pub lr_corrente: f64,
34
+ /// M9: multiplicador com recuperação (substitui o ratchet)
35
+ pub multiplicador: f64,
36
+ pub taxa_recuperacao: f64,
37
  pub aquecimento: usize,
38
  pub passos_totais: usize,
39
  pub paciencia_plateau: usize,
 
52
  Controlador {
53
  lr_base,
54
  lr_corrente: lr_base,
55
+ multiplicador: 1.0,
56
+ taxa_recuperacao: 0.02, // recupera ~2%/passo (≥42 passos p/ voltar de 0,3 a 1)
57
  aquecimento,
58
  passos_totais,
59
  paciencia_plateau: paciencia,
 
68
  }
69
  }
70
 
71
+ /// LR do passo: agenda (aquecimento → cosseno) × multiplicador (M9).
72
  pub fn lr_passo(&mut self, passo: usize, perda: f64, hub: &mut TelemetryHub) -> f64 {
73
+ // divergência? — o passo é descartado ANTES pelo chamador (M10);
74
+ // aqui apenas reduz o multiplicador.
75
  if !perda.is_finite() || perda > 2.0 * self.minimo_ema {
76
+ let de = self.multiplicador;
77
+ self.multiplicador *= 0.3;
78
+ hub.ajuste(
79
+ "lr_mult",
80
+ de,
81
+ self.multiplicador,
82
+ &format!("divergência (perda {perda:.3e} vs mínimo EMA {:.3e}) — passo descartado (M10)", self.minimo_ema),
83
+ );
84
  self.passos_sem_melhora = 0;
85
+ self.minimo_ema = f64::INFINITY; // re-arma o detector
86
+ return self.lr_prog(passo) * self.multiplicador;
87
  }
88
+ // rastreio de mínimo
89
  if perda < self.minimo_ema * 0.999 {
90
  self.minimo_ema = self.minimo_ema.min(perda);
91
  self.passos_sem_melhora = 0;
92
  } else {
93
  self.passos_sem_melhora += 1;
94
  }
95
+ // plateau: reduz o multiplicador (NÃO o teto permanente — M9)
96
  if self.paciencia_plateau > 0 && self.passos_sem_melhora >= self.paciencia_plateau {
97
+ let de = self.multiplicador;
98
+ self.multiplicador *= self.fator_plateau;
99
+ hub.ajuste(
100
+ "lr_mult",
101
+ de,
102
+ self.multiplicador,
103
+ &format!("plateau: {} passos sem melhora (multiplicador — reversível)", self.passos_sem_melhora),
104
+ );
105
  self.passos_sem_melhora = 0;
106
  }
107
+ // M9: recuperação geométrica do multiplicador
108
+ if self.multiplicador < 1.0 {
109
+ self.multiplicador = (self.multiplicador * (1.0 + self.taxa_recuperacao)).min(1.0);
110
+ }
111
+ self.lr_corrente = self.lr_prog(passo) * self.multiplicador;
112
+ self.lr_corrente
113
+ }
114
+
115
+ fn lr_prog(&self, passo: usize) -> f64 {
116
  let p = passo as f64;
117
+ if p < self.aquecimento as f64 {
118
  self.lr_base * (p + 1.0) / self.aquecimento.max(1) as f64
119
  } else {
120
  let t = (p - self.aquecimento as f64) / (self.passos_totais_clamped() - self.aquecimento).max(1) as f64;
121
  let cosseno = 0.5 * (1.0 + (std::f64::consts::PI * t.min(1.0)).cos());
122
  self.lr_base * (0.1 + 0.9 * cosseno)
123
+ }
 
 
 
124
  }
125
 
126
  fn passos_totais_clamped(&self) -> usize {
 
151
 
152
  /// ε da corrupção (C.3(ii)): sobe se a taxa de erro por nível em validação
153
  /// excede ε (cobertura insuficiente), desce lentamente caso contrário.
154
+ /// v0.3.0: INTEGRADA — chamada pela fase B com a taxa medida.
155
  pub fn calibrar_epsilon(&mut self, taxa_erro_por_nivel: f64, hub: &mut TelemetryHub) -> f64 {
156
  let de = self.epsilon_corrupcao;
157
  if taxa_erro_por_nivel > self.epsilon_corrupcao + 0.02 {
 
165
  }
166
  self.epsilon_corrupcao
167
  }
168
+
169
+ /// λ-âncora (Teo 29.2, hipergradiente EXATO): L(λ) = λ·L_diff + ρ(λ−λ₀)²
170
+ /// ⇒ ∂L/∂λ = L_diff + 2ρ(λ−λ₀); ótimo interior λ* = λ₀ − L_diff/(2ρ).
171
+ /// v0.3.0: INTEGRADA — chamada pela fase B a cada passo com L_diff medido.
172
+ pub fn passo_lambda(&mut self, l_diff: f64, hub: &mut TelemetryHub) -> f64 {
173
+ let de = self.lambda_ancora.lambda;
174
+ let novo = self.lambda_ancora.passo(l_diff, 0.05);
175
+ if (de - novo).abs() > 1e-9 {
176
+ hub.ajuste("lambda_jepa", de, novo,
177
+ &format!("hipergradiente exato 29.2: L_diff={l_diff:.4e}, λ*={:.4}", self.lambda_ancora.otimo_analitico(l_diff)));
178
+ }
179
+ novo
180
+ }
181
+ }
182
+
183
+ #[cfg(test)]
184
+ mod testes {
185
+ use super::*;
186
+
187
+ /// Teo C-2: multiplicador nunca excede 1 e RECUPERA após divergência
188
+ /// (a v0.1.11 tinha ratchet monotônico — M9).
189
+ #[test]
190
+ fn multiplicador_recupera() {
191
+ let mut hub = TelemetryHub::novo(None);
192
+ let mut c = Controlador::novo(1e-3, 2, 1000, 5, 0.6, 0.05);
193
+ // divergência no passo 10
194
+ c.minimo_ema = 1.0;
195
+ c.lr_passo(10, 10.0, &mut hub);
196
+ assert!((c.multiplicador - 0.3).abs() < 1e-9, "divergência: mult 0,3");
197
+ // 100 passos com perda DECRESCENTE (sem plateau): multiplicador
198
+ // volta a 1 (recuperação — Teo C-2(ii))
199
+ for p in 11..111 {
200
+ let perda = 0.5 * 0.98f64.powi(p as i32);
201
+ c.lr_passo(p, perda, &mut hub);
202
+ }
203
+ assert!(c.multiplicador > 0.999, "recuperação completa (Teo C-2(ii)): {}", c.multiplicador);
204
+ assert!(c.multiplicador <= 1.0 + 1e-12, "nunca amplifica (Teo C-2(i))");
205
+ // LR final ≈ agenda (não fica preso no piso)
206
+ let esperado = c.lr_prog(110);
207
+ assert!((c.lr_corrente - esperado).abs() / esperado < 1e-9);
208
+ }
209
  }
src/diagnostico.rs CHANGED
@@ -9,35 +9,35 @@
9
 
10
  use std::collections::BTreeMap;
11
 
12
- /// Chaves canônicas conhecidas do config (espeelho do config.rs).
 
 
 
 
13
  pub fn chaves_conhecidas() -> Vec<String> {
14
  let mut v = vec![];
15
  for secao in ["modelo", "ncm", "adba", "ponte", "treino", "telemetria", "dados", "memoria"] {
16
  v.push(secao.to_string());
17
- let campos: &[&str] = match secao {
18
- "modelo" => &["vocab", "d_modelo", "n_camadas", "n_cabecas", "d_ff",
19
- "comprimento_ctx", "janela_sliding", "kv_cache_max", "usar_atencao_arvore",
20
- "moe", "mtp", "roteador", "microunidades", "atencao_moe",
21
- "moe_enc_dec", "autoajuste", "mamba3", "sonicmoe"],
22
- "ncm" => &["ativo", "niveis", "d_lat", "k_por_nivel", "ema_codebook", "beta_commit",
23
- "corrupcao_cond", "dropout_cfg", "cfg_scale", "pesos_nivel", "escala_r",
24
- "modo_quantizacao", "niveis_token",
25
- // subseções (dicts aninhados — 2º nível)
26
- "nested_dropout", "gsq", "lambdas", "temperaturas", "mbr"],
27
- "adba" => &["ativo", "gamma_ancora", "reponderacao_nivel", "temperatura_nivel"],
28
- "ponte" => &["ativo", "capacidade", "timeout_ms", "rodadas_demo"],
29
- "treino" => &["lr", "aquecimento", "passos_fase_a", "passos_fase_b", "lote", "clip",
30
- "peso_decaimento", "telemetria_a_cada"],
31
- "telemetria" => &["jsonl", "deteccao_aprendizado", "ajuste_automatico",
32
- "paciencia_plateau", "fator_lr_plateau"],
33
- "dados" => &["shard_treino", "shard_validacao", "max_artigos", "max_bytes"],
34
- "memoria" => &["som_lado", "lambda_jepa", "rm_dois_tempos"],
35
- _ => &[],
36
- };
37
- for c in campos {
38
- v.push(format!("{secao}.{c}"));
39
  }
40
  }
 
 
41
  v
42
  }
43
 
 
9
 
10
  use std::collections::BTreeMap;
11
 
12
+ /// Chaves canônicas conhecidas do config — A26 (v0.3.0): derivadas do
13
+ /// PRÓPRIO `ConfigKhtst` via serde (fonte única, runtime) em vez de uma lista
14
+ /// escrita à mão (a v0.1.11 listava 7 chaves inexistentes: roteador,
15
+ /// microunidades, atencao_moe, moe_enc_dec, autoajuste, mamba3, sonicmoe —
16
+ /// sugestões de lint INVÁLIDAS).
17
  pub fn chaves_conhecidas() -> Vec<String> {
18
  let mut v = vec![];
19
  for secao in ["modelo", "ncm", "adba", "ponte", "treino", "telemetria", "dados", "memoria"] {
20
  v.push(secao.to_string());
21
+ }
22
+ // serializa o default e extrai as folhas "secao.campo" / "secao.sub.campo"
23
+ if let Ok(valor) = serde_json::to_value(crate::config::ConfigKhtst::default()) {
24
+ if let Some(obj) = valor.as_object() {
25
+ for (secao, conteudo) in obj {
26
+ if let Some(sub) = conteudo.as_object() {
27
+ for (campo, neto) in sub {
28
+ v.push(format!("{secao}.{campo}"));
29
+ if let Some(neto_obj) = neto.as_object() {
30
+ for neto_campo in neto_obj.keys() {
31
+ v.push(format!("{secao}.{campo}.{neto_campo}"));
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
 
 
 
 
 
 
37
  }
38
  }
39
+ v.sort();
40
+ v.dedup();
41
  v
42
  }
43
 
src/gerador.rs CHANGED
@@ -4,22 +4,56 @@
4
  //! condicionamento = reconstrução cumulativa interpolada ẑ_{ℓ−1}^{(n_ℓ)} + e_ℓ
5
  //! (função determinística de c_{<ℓ} — Teo D.1). Corrupção κ (Teo C.3) e
6
  //! dropout CFG (Teo D.2(iii)). Cabeças MTP auxiliares (Teo D.6/T7).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  use candle_core::{Device, Result, Tensor, Var, D};
9
  use crate::camadas::{Linear, RmsNorm};
10
  use crate::config::ConfigKhtst;
11
  use crate::mascaras::{interp_para, mascara_block_causal};
12
  use crate::tokenizador::TokenizadorConceitos;
13
- use crate::tronco::{CabecaConceitos, EmbNiveis, Tronco};
14
  use rand::prelude::*;
15
 
16
- pub const N_PREFIXO: usize = 8;
17
 
18
  pub struct GeradorConceitos {
19
  pub tronco: Tronco,
20
  pub emb_niveis: EmbNiveis,
21
  pub proj_entrada: Linear, // d_lat → d_modelo
 
22
  pub cabeca: CabecaConceitos,
 
 
23
  pub cabecas_mtp: Vec<Linear>, // auxiliares — Teo D.6
24
  pub norm_entrada: RmsNorm,
25
  pub mascara: Tensor,
@@ -30,6 +64,8 @@ pub struct GeradorConceitos {
30
  pub k: usize,
31
  pub mtp_ativo: bool,
32
  pub beta_mtp: f64,
 
 
33
  }
34
 
35
  pub struct SaidaTreino {
@@ -38,12 +74,19 @@ pub struct SaidaTreino {
38
  pub ce_mtp: f64,
39
  pub acuracia_media: f64,
40
  pub uncond: bool,
 
 
 
 
 
 
41
  }
42
 
43
  impl GeradorConceitos {
44
  pub fn novo(cfg: &ConfigKhtst, dev: &Device) -> Result<Self> {
45
  let m = &cfg.modelo;
46
  let n = &cfg.ncm;
 
47
  let tronco = Tronco::novo(cfg, dev)?;
48
  let mascara = mascara_block_causal(&n.niveis, N_PREFIXO, dev)?;
49
  let mut mtp = Vec::new();
@@ -52,11 +95,19 @@ impl GeradorConceitos {
52
  mtp.push(Linear::sem_vies(m.d_modelo, n.k_por_nivel, 0.02, dev)?);
53
  }
54
  }
 
 
 
 
 
 
 
55
  Ok(GeradorConceitos {
56
  tronco,
57
  emb_niveis: EmbNiveis::nova(cfg, dev)?,
58
  proj_entrada: Linear::sem_vies(n.d_lat, m.d_modelo, 0.02, dev)?,
59
  cabeca: CabecaConceitos::nova(cfg, dev)?,
 
60
  cabecas_mtp: mtp,
61
  norm_entrada: RmsNorm::novo(m.d_modelo, 1e-5, dev)?,
62
  mascara,
@@ -67,6 +118,8 @@ impl GeradorConceitos {
67
  k: n.k_por_nivel,
68
  mtp_ativo: m.mtp.ativo,
69
  beta_mtp: m.mtp.beta_entropia,
 
 
70
  })
71
  }
72
 
@@ -88,7 +141,6 @@ impl GeradorConceitos {
88
  dev: &Device,
89
  ) -> Result<Tensor> {
90
  let n_l = self.niveis[nivel];
91
- // dims()[0] = d_modelo (SAÍDA da projeção d_lat→d_modelo)
92
  let d = self.proj_entrada.w.as_tensor().dims()[0];
93
  let base = match zhat_anterior {
94
  Some(z) => {
@@ -122,9 +174,29 @@ impl GeradorConceitos {
122
  Ok(saida)
123
  }
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  /// Forward de TREINO com perda (1 passada — Teo D.1(iv)): teacher forcing
126
  /// com códigos VERDADEIROS no alvo; condicionamento corrompido por κ;
127
- /// dropout CFG por exemplo (ramo não-condicionado = códigos aleatórios).
 
128
  pub fn perda_treino(
129
  &self,
130
  tok: &TokenizadorConceitos,
@@ -168,35 +240,76 @@ impl GeradorConceitos {
168
  let mut ces = Vec::with_capacity(self.niveis.len());
169
  let mut accs = Vec::new();
170
  let mut inicio = N_PREFIXO;
 
 
 
 
 
 
171
  for (i, &n_l) in self.niveis.iter().enumerate() {
172
  let trecho = h.narrow(1, inicio, n_l)?;
173
- let logits = self.cabeca.cabecas[i].fwd(&trecho)?;
174
- let plano = logits.reshape((b * n_l, self.k))?;
175
  let alvo = codigos_verdadeiros[i].reshape(b * n_l)?.to_dtype(candle_core::DType::U32)?;
176
- let ce = candle_nn::loss::cross_entropy(&plano, &alvo)?;
177
- let preds = plano.argmax(D::Minus1)?;
178
- let ac = preds.eq(&alvo)?.to_dtype(candle_core::DType::F32)?.sum_all()?.to_scalar::<f32>()? as f64
179
- / (b * n_l) as f64;
180
- accs.push(ac);
181
- ces.push(ce);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  inicio += n_l;
183
  }
 
184
  let l = ces.len() as f64;
185
- let mut perda = ces[0].affine(1.0 / l, 0.0)?;
186
- for ce in &ces[1..] {
187
- perda = (&perda + &ce.affine(1.0 / l, 0.0)?)?;
188
  }
189
  // MTP auxiliar (Teo D.6): cabeça no nível ℓ prevê códigos do nível ℓ+1
190
  let mut ce_mtp = 0f64;
191
  if self.mtp_ativo && !self.cabecas_mtp.is_empty() {
192
- let dev = self.mascara.device();
193
  let mut inicio = N_PREFIXO;
194
  let mut soma = None;
195
  for i in 0..self.cabecas_mtp.len() {
196
  let n_l = self.niveis[i];
197
  let n_prox = self.niveis[i + 1];
198
  let trecho = h.narrow(1, inicio, n_l)?;
199
- // reamostra para o tamanho do nível alvo (n_ℓ ≠ n_{ℓ+1})
200
  let trecho_mtp = if n_l != n_prox {
201
  interp_para(&trecho, n_prox, dev)?
202
  } else {
@@ -216,12 +329,17 @@ impl GeradorConceitos {
216
  perda = (&perda + &s.affine(self.beta_mtp / m, 0.0)?)?;
217
  }
218
  }
 
 
219
  Ok(SaidaTreino {
220
  perda,
221
- ce_por_nivel: ces.iter().map(|t| t.to_scalar::<f32>().unwrap() as f64).collect(),
222
  ce_mtp,
223
  acuracia_media: accs.iter().sum::<f64>() / accs.len() as f64,
224
  uncond,
 
 
 
225
  })
226
  }
227
 
@@ -230,25 +348,32 @@ impl GeradorConceitos {
230
  v.extend(self.emb_niveis.vars());
231
  v.extend(self.proj_entrada.vars());
232
  v.extend(self.cabeca.vars());
 
 
 
233
  for m in &self.cabecas_mtp { v.extend(m.vars()); }
234
  v.extend(self.norm_entrada.vars());
235
  v
236
  }
237
 
238
  pub fn salvar(&self, caminho: &str) -> Result<()> {
 
239
  let mut obj = serde_json::Map::new();
240
- for (i, v) in self.vars().iter().enumerate() {
 
 
 
 
 
 
241
  let dados = v.as_tensor().flatten_all()?.to_vec1::<f32>()?;
242
  obj.insert(format!("v{i}"), serde_json::json!({
243
- "forma": v.as_tensor().shape().dims(),
244
  "dados": dados,
245
  }));
246
  }
247
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
248
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
249
- let tmp = format!("{caminho}.tmp");
250
- std::fs::write(&tmp, txt).map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
251
- std::fs::rename(&tmp, caminho).map_err(|e| candle_core::Error::Msg(format!("rename: {e}")))?;
252
- Ok(())
253
  }
254
  }
 
4
  //! condicionamento = reconstrução cumulativa interpolada ẑ_{ℓ−1}^{(n_ℓ)} + e_ℓ
5
  //! (função determinística de c_{<ℓ} — Teo D.1). Corrupção κ (Teo C.3) e
6
  //! dropout CFG (Teo D.2(iii)). Cabeças MTP auxiliares (Teo D.6/T7).
7
+ //!
8
+ //! ## v0.3.0 — APRIMORAMENTOS
9
+ //! 1. **Cabeça de mistura de planos** (`CabecaMistura` — tronco.rs):
10
+ //! p(c_ℓ|c_{<ℓ}) = Σ_m w_m Π_i q_{m,i} — raciocínio em M subníveis
11
+ //! PARALELOS por nível, combinados por mistura. Provas: Teo C-1 (tronco.rs).
12
+ //! 2. **Verificador algébrico da hierarquia** (E.3 da proposta C):
13
+ //! L_alg = Σ_{ℓ≥2} ‖Interp_{n_{ℓ−1}}(ẑ_ℓ^{soft}) − ẑ_{ℓ−1}‖² —
14
+ //! consistência de refino: a reconstrução do nível ℓ, projetada de volta
15
+ //! à granularidade ℓ−1, deve PRESERVAR o conteúdo grosseiro.
16
+ //!
17
+ //! **Teo G-1 (verificador algébrico).**
18
+ //! (i) L_alg é QUADRÁTICA nas esperanças de código: com q_i = distribuição
19
+ //! por posição e r_soft = Σ_k q_i(k)·R·c_k, L_alg(r) = ‖D·r − v‖² com
20
+ //! D = Interp_{n_{ℓ−1}←n_ℓ} (linear) e v = (I − D·U)ẑ_{ℓ−1} + (D·U − I)ẑ_{ℓ−1}
21
+ //! … i.e. v = ẑ_{ℓ−1} − D·U·ẑ_{ℓ−1} (o "desfoque" a compensar).
22
+ //! (ii) O minimizador relaxado (r livre) satisfaz as equações normais
23
+ //! DᵀDr* = Dᵀv — solução em forma fechada (mínimos quadrados), e o valor
24
+ //! mínimo ‖v‖² − ‖D r*‖²… é COTA INFERIOR para qualquer distribuição de
25
+ //! códigos (o span dos códigos é subespaço).
26
+ //! (iii) Com codebook Stiefel (linhas ortonormais, C Cᵀ = I_K), a projeção
27
+ //! de um alvo t ∈ R^d no span {R·c_k} é R·C·t/R·… computável em O(K·d):
28
+ //! P_span(t) = R·Cᵀ⟨…⟩ — barato e auditável.
29
+ //! (iv) Gradiente exato via autograd (todas as ops diferenciáveis) —
30
+ //! validado por diferenças finitas no gate G11.
31
+ //! *Prova (i):* Interp é matriz fixa W ∈ R^{n_{ℓ−1}×n_ℓ}; L_alg(r) =
32
+ //! ‖W·r − v‖₂² é quadrática com Hessiano 2WᵀW ⪰ 0 (convexa); (ii) segue
33
+ //! da otimalidade de Gauss-Markov; (iii) da ortonormalidade das linhas de
34
+ //! C (Teo 26.3). ∎
35
+ //! **Critério de falsificação**: a série `recomposicao_residuo` deve CAIR
36
+ //! ao longo do treino da fase A (a hierarquia fica algébricamente
37
+ //! consistente); se subir, o verificador acusa inconsistência.
38
 
39
  use candle_core::{Device, Result, Tensor, Var, D};
40
  use crate::camadas::{Linear, RmsNorm};
41
  use crate::config::ConfigKhtst;
42
  use crate::mascaras::{interp_para, mascara_block_causal};
43
  use crate::tokenizador::TokenizadorConceitos;
44
+ use crate::tronco::{CabecaConceitos, CabecaMistura, EmbNiveis, Tronco};
45
  use rand::prelude::*;
46
 
47
+ pub use crate::tronco::N_PREFIXO;
48
 
49
  pub struct GeradorConceitos {
50
  pub tronco: Tronco,
51
  pub emb_niveis: EmbNiveis,
52
  pub proj_entrada: Linear, // d_lat → d_modelo
53
+ /// Cabeça produto da v0.1.11 — usada quando M = 1 (bit-a-bit idêntica).
54
  pub cabeca: CabecaConceitos,
55
+ /// Cabeças de MISTURA DE PLANOS por nível (v0.3.0) — usadas quando M ≥ 2.
56
+ pub cabecas_mistura: Vec<CabecaMistura>,
57
  pub cabecas_mtp: Vec<Linear>, // auxiliares — Teo D.6
58
  pub norm_entrada: RmsNorm,
59
  pub mascara: Tensor,
 
64
  pub k: usize,
65
  pub mtp_ativo: bool,
66
  pub beta_mtp: f64,
67
+ pub m_planos: usize,
68
+ pub lambda_alg: f64,
69
  }
70
 
71
  pub struct SaidaTreino {
 
74
  pub ce_mtp: f64,
75
  pub acuracia_media: f64,
76
  pub uncond: bool,
77
+ /// verificador algébrico (medido nos códigos verdadeiros — telemetria)
78
+ pub residuo_algebrico: f64,
79
+ /// perda algébrica diferenciável (soft — para PCGrad separada)
80
+ pub perda_alg: Option<Tensor>,
81
+ /// entropia média dos pesos de plano (telemetria da mistura)
82
+ pub entropia_planos: f64,
83
  }
84
 
85
  impl GeradorConceitos {
86
  pub fn novo(cfg: &ConfigKhtst, dev: &Device) -> Result<Self> {
87
  let m = &cfg.modelo;
88
  let n = &cfg.ncm;
89
+ crate::rng::inicializar(cfg.treino.semente ^ 0xB);
90
  let tronco = Tronco::novo(cfg, dev)?;
91
  let mascara = mascara_block_causal(&n.niveis, N_PREFIXO, dev)?;
92
  let mut mtp = Vec::new();
 
95
  mtp.push(Linear::sem_vies(m.d_modelo, n.k_por_nivel, 0.02, dev)?);
96
  }
97
  }
98
+ let m_planos = if cfg.ncm.ativo { n.mistura_planos.max(1) } else { 1 };
99
+ let mut cabecas_mistura = Vec::new();
100
+ if m_planos >= 2 {
101
+ for _ in 0..n.niveis.len() {
102
+ cabecas_mistura.push(CabecaMistura::nova(m.d_modelo, n.k_por_nivel, m_planos, dev)?);
103
+ }
104
+ }
105
  Ok(GeradorConceitos {
106
  tronco,
107
  emb_niveis: EmbNiveis::nova(cfg, dev)?,
108
  proj_entrada: Linear::sem_vies(n.d_lat, m.d_modelo, 0.02, dev)?,
109
  cabeca: CabecaConceitos::nova(cfg, dev)?,
110
+ cabecas_mistura,
111
  cabecas_mtp: mtp,
112
  norm_entrada: RmsNorm::novo(m.d_modelo, 1e-5, dev)?,
113
  mascara,
 
118
  k: n.k_por_nivel,
119
  mtp_ativo: m.mtp.ativo,
120
  beta_mtp: m.mtp.beta_entropia,
121
+ m_planos,
122
+ lambda_alg: if cfg.ncm.ativo { cfg.ncm.lambdas.algebrico } else { 0.0 },
123
  })
124
  }
125
 
 
141
  dev: &Device,
142
  ) -> Result<Tensor> {
143
  let n_l = self.niveis[nivel];
 
144
  let d = self.proj_entrada.w.as_tensor().dims()[0];
145
  let base = match zhat_anterior {
146
  Some(z) => {
 
174
  Ok(saida)
175
  }
176
 
177
+ /// Verificador algébrico (Teo G-1): resíduo de recomposição da
178
+ /// hierarquia nos códigos VERDADEIROS (telemetria/falsificação).
179
+ pub fn residuo_algebrico(zhats: &[Tensor], dev: &Device) -> Result<f64> {
180
+ if zhats.len() < 2 {
181
+ return Ok(0.0);
182
+ }
183
+ let mut soma = 0f64;
184
+ let mut cont = 0usize;
185
+ for l in 1..zhats.len() {
186
+ let n_ant = zhats[l - 1].dim(1)?;
187
+ let voltou = interp_para(&zhats[l], n_ant, dev)?;
188
+ let dif = (&voltou - &zhats[l - 1])?;
189
+ let m = dif.sqr()?.mean_all()?.to_scalar::<f32>()? as f64;
190
+ soma += m;
191
+ cont += 1;
192
+ }
193
+ Ok(if cont > 0 { soma / cont as f64 } else { 0.0 })
194
+ }
195
+
196
  /// Forward de TREINO com perda (1 passada — Teo D.1(iv)): teacher forcing
197
  /// com códigos VERDADEIROS no alvo; condicionamento corrompido por κ;
198
+ /// dropout CFG por exemplo. Cabeça: mistura de planos (M ≥ 2) ou produto
199
+ /// (M = 1, bit-a-bit v0.1.11). Inclui o verificador algébrico soft.
200
  pub fn perda_treino(
201
  &self,
202
  tok: &TokenizadorConceitos,
 
240
  let mut ces = Vec::with_capacity(self.niveis.len());
241
  let mut accs = Vec::new();
242
  let mut inicio = N_PREFIXO;
243
+ let usar_mistura = self.m_planos >= 2 && !self.cabecas_mistura.is_empty();
244
+ let mut entropia_planos = 0f64;
245
+ let mut n_entropia = 0usize;
246
+ // verificador algébrico SOFT (diferenciável) — acumulado por nível
247
+ let mut perda_alg: Option<Tensor> = None;
248
+ let mut ces_tensores: Vec<Tensor> = Vec::with_capacity(self.niveis.len());
249
  for (i, &n_l) in self.niveis.iter().enumerate() {
250
  let trecho = h.narrow(1, inicio, n_l)?;
 
 
251
  let alvo = codigos_verdadeiros[i].reshape(b * n_l)?.to_dtype(candle_core::DType::U32)?;
252
+ let (ce_t, ce_v, acc) = if usar_mistura {
253
+ let cab = &self.cabecas_mistura[i];
254
+ let (nll, ce) = cab.nll(&trecho, &alvo)?;
255
+ // acurácia: marginal
256
+ let logits_m = cab.logits_marginais(&trecho)?;
257
+ let plano = logits_m.reshape((b * n_l, self.k))?;
258
+ let preds = plano.argmax(D::Minus1)?;
259
+ let ac = preds.eq(&alvo)?.to_dtype(candle_core::DType::F32)?.sum_all()?.to_scalar::<f32>()? as f64
260
+ / (b * n_l) as f64;
261
+ // entropia dos pesos de plano (telemetria)
262
+ let w = cab.pesos_planos(&trecho)?;
263
+ let wl = ((&w + 1e-10f64)? * &(&w + 1e-10f64)?.log()?)?;
264
+ let hent = wl.affine(-1.0, 0.0)?.sum(D::Minus1)?.mean_all()?.to_scalar::<f32>()? as f64;
265
+ entropia_planos += hent;
266
+ n_entropia += 1;
267
+ // verificador algébrico soft (Teo G-1): r_soft do nível i
268
+ if self.lambda_alg > 0.0 && i >= 1 {
269
+ // softmax COM backward (softmax_last_dim é no_bwd no candle 0.11)
270
+ let probs = candle_nn::ops::softmax(&plano, D::Minus1)?;
271
+ let probs3 = probs.reshape((b, n_l, self.k))?;
272
+ let r_soft = tok.materializar_probs_nivel(i, &probs3)?;
273
+ let zhat_soft = (&interp_para(&zhats[i - 1], n_l, dev)? + &r_soft)?;
274
+ let n_ant = self.niveis[i - 1];
275
+ let voltou = interp_para(&zhat_soft, n_ant, dev)?;
276
+ let dif = (&voltou - &zhats[i - 1])?;
277
+ let termo = dif.sqr()?.mean_all()?;
278
+ perda_alg = Some(match perda_alg {
279
+ Some(p) => (&p + &termo)?,
280
+ None => termo,
281
+ });
282
+ }
283
+ (nll, ce, ac)
284
+ } else {
285
+ let logits = self.cabeca.cabecas[i].fwd(&trecho)?;
286
+ let plano = logits.reshape((b * n_l, self.k))?;
287
+ let ce = candle_nn::loss::cross_entropy(&plano, &alvo)?;
288
+ let preds = plano.argmax(D::Minus1)?;
289
+ let ac = preds.eq(&alvo)?.to_dtype(candle_core::DType::F32)?.sum_all()?.to_scalar::<f32>()? as f64
290
+ / (b * n_l) as f64;
291
+ (ce.clone(), ce.to_scalar::<f32>()? as f64, ac)
292
+ };
293
+ ces.push(ce_v);
294
+ accs.push(acc);
295
+ ces_tensores.push(ce_t);
296
  inicio += n_l;
297
  }
298
+ // somatório das CEs (média por nível)
299
  let l = ces.len() as f64;
300
+ let mut perda = ces_tensores[0].affine(1.0 / l, 0.0)?;
301
+ for t in &ces_tensores[1..] {
302
+ perda = (&perda + &t.affine(1.0 / l, 0.0)?)?;
303
  }
304
  // MTP auxiliar (Teo D.6): cabeça no nível ℓ prevê códigos do nível ℓ+1
305
  let mut ce_mtp = 0f64;
306
  if self.mtp_ativo && !self.cabecas_mtp.is_empty() {
 
307
  let mut inicio = N_PREFIXO;
308
  let mut soma = None;
309
  for i in 0..self.cabecas_mtp.len() {
310
  let n_l = self.niveis[i];
311
  let n_prox = self.niveis[i + 1];
312
  let trecho = h.narrow(1, inicio, n_l)?;
 
313
  let trecho_mtp = if n_l != n_prox {
314
  interp_para(&trecho, n_prox, dev)?
315
  } else {
 
329
  perda = (&perda + &s.affine(self.beta_mtp / m, 0.0)?)?;
330
  }
331
  }
332
+ // resíduo algébrico nos códigos verdadeiros (telemetria)
333
+ let residuo = Self::residuo_algebrico(&zhats, dev)?;
334
  Ok(SaidaTreino {
335
  perda,
336
+ ce_por_nivel: ces,
337
  ce_mtp,
338
  acuracia_media: accs.iter().sum::<f64>() / accs.len() as f64,
339
  uncond,
340
+ residuo_algebrico: residuo,
341
+ perda_alg,
342
+ entropia_planos: if n_entropia > 0 { entropia_planos / n_entropia as f64 } else { 0.0 },
343
  })
344
  }
345
 
 
348
  v.extend(self.emb_niveis.vars());
349
  v.extend(self.proj_entrada.vars());
350
  v.extend(self.cabeca.vars());
351
+ for cm in &self.cabecas_mistura {
352
+ v.extend(cm.vars());
353
+ }
354
  for m in &self.cabecas_mtp { v.extend(m.vars()); }
355
  v.extend(self.norm_entrada.vars());
356
  v
357
  }
358
 
359
  pub fn salvar(&self, caminho: &str) -> Result<()> {
360
+ let vars = self.vars();
361
  let mut obj = serde_json::Map::new();
362
+ obj.insert("__manifesto".into(), serde_json::json!({
363
+ "formato": "khtst-ckpt-v2",
364
+ "tipo": "gerador",
365
+ "n_vars": vars.len(),
366
+ "formas": vars.iter().map(|v| v.as_tensor().dims()).collect::<Vec<_>>(),
367
+ }));
368
+ for (i, v) in vars.iter().enumerate() {
369
  let dados = v.as_tensor().flatten_all()?.to_vec1::<f32>()?;
370
  obj.insert(format!("v{i}"), serde_json::json!({
371
+ "forma": v.as_tensor().dims(),
372
  "dados": dados,
373
  }));
374
  }
375
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
376
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
377
+ crate::tokenizador::salvar_atomico(caminho, &txt)
 
 
 
378
  }
379
  }
src/lib.rs CHANGED
@@ -1,11 +1,17 @@
1
  //! KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM (Next-Concept
2
  //! Modeling), provas integradas (memoria-ncm-khtst n-02/n-03), atenção ADBA
3
- //! (r-04) e ponte duplex (r-05). Documentação: docs/memorias/.
 
4
  //!
5
  //! Convenções do projeto (n-00 §4): nascimento neutro, leitura honesta,
6
  //! Teos numerados com ∎, token EXCLUSIVAMENTE em HF_TOKEN apagado após uso.
 
 
 
 
7
 
8
  use pyo3::prelude::*;
 
9
 
10
  pub mod amostrador;
11
  pub mod atencao;
@@ -20,6 +26,7 @@ pub mod perdas;
20
  pub mod ponte;
21
  pub mod quantizacao;
22
  pub mod quantizador;
 
23
  pub mod ssd;
24
  pub mod stiefel;
25
  pub mod telemetria;
@@ -34,7 +41,7 @@ pub mod build_info {
34
 
35
  #[pyfunction]
36
  fn versao() -> &'static str {
37
- "0.1.11 (KHTST_RUST — NCM + provas integradas + ADBA + ponte duplex)"
38
  }
39
 
40
  #[pyfunction]
@@ -48,7 +55,8 @@ fn info_build() -> String {
48
  )
49
  }
50
 
51
- /// Lint de config com sugestões por Levenshtein (diagnóstico de digitação).
 
52
  #[pyfunction]
53
  fn lint_config(py: Python, caminho: &str) -> PyResult<PyObject> {
54
  let txt = std::fs::read_to_string(caminho)
@@ -81,7 +89,7 @@ impl BpePy {
81
  #[staticmethod]
82
  fn carregar(caminho: &str) -> PyResult<Self> {
83
  let txt = std::fs::read_to_string(caminho)
84
- .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
85
  let v: serde_json::Value = serde_json::from_str(&txt)
86
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
87
  let bpe = tokens::Bpe::de_json(&v)
@@ -99,7 +107,7 @@ impl BpePy {
99
 
100
  fn salvar(&self, caminho: &str) -> PyResult<()> {
101
  std::fs::write(caminho, serde_json::to_string(&self.bpe.para_json()).unwrap())
102
- .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{e}")))?;
103
  Ok(())
104
  }
105
 
@@ -118,7 +126,6 @@ struct ConfigPy {
118
  #[pymethods]
119
  impl ConfigPy {
120
  #[new]
121
- #[allow(never_type_fallback)]
122
  #[pyo3(signature = (caminho=None))]
123
  fn new(caminho: Option<String>) -> PyResult<Self> {
124
  let cfg = match &caminho {
@@ -136,6 +143,25 @@ impl ConfigPy {
136
  Ok(ConfigPy { cfg, caminho })
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  fn validar(&self) -> PyResult<()> {
140
  self.cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)
141
  }
@@ -146,10 +172,12 @@ impl ConfigPy {
146
 
147
  fn resumo(&self) -> String {
148
  format!(
149
- "modelo(vocab={}, d={}, camadas={}, ctx={}) ncm(L={}, niveis={:?}, K={}, d_lat={}, modo={}, ativo={})",
150
  self.cfg.modelo.vocab, self.cfg.modelo.d_modelo, self.cfg.modelo.n_camadas,
151
- self.cfg.modelo.comprimento_ctx, self.cfg.l_niveis(), self.cfg.ncm.niveis,
 
152
  self.cfg.ncm.k_por_nivel, self.cfg.ncm.d_lat, self.cfg.ncm.modo_quantizacao, self.cfg.ncm.ativo,
 
153
  )
154
  }
155
 
@@ -172,21 +200,22 @@ impl PonteDuplaPy {
172
  PonteDuplaPy { ponte: ponte::PonteDupla::nova(capacidade, timeout_ms) }
173
  }
174
 
175
- fn rodada(&self, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<(Vec<f32>, Vec<f32>)> {
176
- let (ma, mb) = self.ponte.rodada(carga_a, carga_b, nivel)
 
177
  .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
178
  Ok((ma.carga, mb.carga))
179
  }
180
 
181
- fn demo_professor_aluno(&self, rodadas: usize, dim: usize) -> PyResult<PyObject> {
182
  let inicio: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
183
- let stats = ponte::demo_professor_aluno(&self.ponte, rodadas, inicio, &|a, _| {
184
- a.iter().map(|x| x * 1.05).collect()
 
 
185
  })
186
  .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
187
- Python::with_gil(|py| {
188
- Ok(pythonize::pythonize(py, &serde_json::to_value(&stats).unwrap())?.into())
189
- })
190
  }
191
 
192
  fn estatisticas(&self) -> PyResult<PyObject> {
@@ -213,6 +242,7 @@ impl PonteAsyncPy {
213
  }
214
 
215
  /// Corrotina: `await ponte.trocar(a, b, nivel)` — duplex simultânea.
 
216
  fn trocar<'p>(&self, py: Python<'p>, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<Bound<'p, PyAny>> {
217
  let p = self.interna.clone();
218
  pyo3_async_runtimes::tokio::future_into_py(py, async move {
@@ -225,24 +255,30 @@ impl PonteAsyncPy {
225
  }
226
 
227
  /// Treina a fase A (tokenizador). Devolve dict com resumo + telemetria.
 
 
228
  #[pyfunction]
229
- #[pyo3(signature = (cfg, sequencias, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
230
  fn treinar_fase_a(
 
231
  cfg: &ConfigPy,
232
  sequencias: Vec<Vec<u32>>,
233
  caminho_telemetria: Option<String>,
234
  caminho_checkpoint: Option<String>,
235
  callback: Option<Bound<'_, PyAny>>,
236
  teto_seg: Option<f64>,
 
237
  ) -> PyResult<PyObject> {
238
  let cb: treino::CallbackPy = callback.map(|c| c.unbind());
239
- let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
240
- let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
241
- let mut resumo = treino::treinar_fase_a(&cfg.cfg, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
242
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase A: {e}")))?;
243
- let _ = &mut resumo;
244
- Python::with_gil(|py| {
245
- let dados = serde_json::json!({
 
 
246
  "resumo": {
247
  "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
248
  "perda_final": resumo.perda_final, "acc_recon": resumo.acc_recon,
@@ -250,17 +286,20 @@ fn treinar_fase_a(
250
  "perplexidade_codebook": resumo.perplexidade_codebook,
251
  "monotonia_prefixo": resumo.monotonia_prefixo,
252
  "escore_aprendizado": resumo.escore_aprendizado,
 
 
253
  },
254
  "telemetria": hub.dump(),
255
- });
256
- Ok(pythonize::pythonize(py, &dados)?.into())
257
- })
258
  }
259
 
260
  /// Treina a fase B (gerador) com tokenizador congelado carregado de checkpoint.
261
  #[pyfunction]
262
- #[pyo3(signature = (cfg, sequencias, caminho_tokenizador, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None))]
263
  fn treinar_fase_b(
 
264
  cfg: &ConfigPy,
265
  sequencias: Vec<Vec<u32>>,
266
  caminho_tokenizador: &str,
@@ -268,125 +307,128 @@ fn treinar_fase_b(
268
  caminho_checkpoint: Option<String>,
269
  callback: Option<Bound<'_, PyAny>>,
270
  teto_seg: Option<f64>,
 
271
  ) -> PyResult<PyObject> {
272
  let cb: treino::CallbackPy = callback.map(|c| c.unbind());
273
- let mut hub = telemetria::TelemetryHub::novo(caminho_telemetria);
274
- let dados = treino::DadosTreino::novo(sequencias, cfg.cfg.modelo.comprimento_ctx);
275
- // carrega o tokenizador congelado
276
- let dev = candle_core::Device::Cpu;
277
- let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
278
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
279
- carregar_tokenizador(&mut tok, caminho_tokenizador)
280
- .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
281
- let resumo = treino::treinar_fase_b(&cfg.cfg, &tok, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg)
282
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase B: {e}")))?;
283
- Python::with_gil(|py| {
284
- let dados = serde_json::json!({
 
 
 
 
 
 
285
  "resumo": {
286
  "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
287
  "perda_final": resumo.perda_final, "acc_conceitos": resumo.acc_recon,
288
  "escore_aprendizado": resumo.escore_aprendizado,
 
 
289
  },
290
  "telemetria": hub.dump(),
291
- });
292
- Ok(pythonize::pythonize(py, &dados)?.into())
293
- })
294
- }
295
-
296
- fn carregar_tokenizador(tok: &mut tokenizador::TokenizadorConceitos, caminho: &str) -> Result<(), String> {
297
- let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
298
- let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
299
- let obj = v.as_object().ok_or("formato de checkpoint inválido")?;
300
- let vars = tok.vars();
301
- for (i, var) in vars.iter().enumerate() {
302
- if let Some(entrada) = obj.get(&format!("v{i}")) {
303
- let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
304
- let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
305
- if forma.is_empty() || dados.is_empty() {
306
- continue;
307
- }
308
- let dev = candle_core::Device::Cpu;
309
- let t2 = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
310
- .and_then(|t| t.reshape(forma))
311
- .map_err(|e| format!("{e}"))?;
312
- var.set(&t2).map_err(|e| format!("{e}"))?;
313
- }
314
- }
315
- Ok(())
316
  }
317
 
318
- /// Gera amostras com NFE = L e devolve textos decodificados (modo conceitos).
 
319
  #[pyfunction]
320
- #[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_amostras=2, parar_no_nivel=None, greedy=true))]
321
  fn gerar_amostras(
 
322
  cfg: &ConfigPy,
323
  caminho_tokenizador: &str,
324
  caminho_gerador: &str,
325
  n_amostras: usize,
326
  parar_no_nivel: Option<usize>,
327
  greedy: bool,
 
 
 
328
  ) -> PyResult<PyObject> {
329
- let dev = candle_core::Device::Cpu;
330
- let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg.cfg, &dev)
331
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
332
- carregar_tokenizador(&mut tok, caminho_tokenizador)
333
- .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
334
- let mut ger = gerador::GeradorConceitos::novo(&cfg.cfg, &dev)
335
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
336
- carregar_gerador(&mut ger, caminho_gerador)
337
- .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
338
- let mut amostrador = amostrador::AmostradorConceitos::novo(
339
- &mut ger, &tok, cfg.cfg.ncm.cfg_scale, cfg.cfg.ncm.temperaturas.limites, cfg.cfg.ncm.temperaturas.entropia_alvo,
340
- );
341
- let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(4242);
342
- let inicio = std::time::Instant::now();
343
- let (codigos, nfe) = amostrador.gerar(n_amostras.max(1), &dev, &mut rng, parar_no_nivel, greedy)
344
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("amostragem: {e}")))?;
345
- let tempo_ms = inicio.elapsed().as_secs_f64() * 1000.0;
346
- // decodifica: ẑ final → decoder AR greedy → tokens
347
- let nivel_final = codigos.len();
348
- let zhats = tok.reconstruir_de_codigos(&codigos, nivel_final, n_amostras.max(1), &dev)
349
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
350
- let textos_ids = decodificar_textos(&tok, zhats.last(), n_amostras.max(1))
351
- .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
352
- Python::with_gil(|py| {
353
- let d = serde_json::json!({
354
- "nfe": nfe, "l": cfg.cfg.l_niveis(), "tempo_ms": tempo_ms,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  "niveis_usados": nivel_final, "ids_tokens": textos_ids,
356
  "temperaturas": amostrador.temperaturas,
357
- });
358
- Ok(pythonize::pythonize(py, &d)?.into())
359
- })
 
 
360
  }
361
 
362
- fn decodificar_textos(tok: &tokenizador::TokenizadorConceitos, zhat: Option<&candle_core::Tensor>, b: usize) -> Result<Vec<Vec<u32>>, String> {
363
  let Some(z) = zhat else { return Ok(vec![vec![]; b]) };
364
- tok.decodificar_livre(z, 48).map_err(|e| format!("{e}"))
365
  }
366
 
367
- fn carregar_gerador(ger: &mut gerador::GeradorConceitos, caminho: &str) -> Result<(), String> {
368
- let txt = std::fs::read_to_string(caminho).map_err(|e| format!("{e}"))?;
369
- let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| format!("{e}"))?;
370
- let obj = v.as_object().ok_or("formato inválido")?;
371
- let vars = ger.vars();
372
- for (i, var) in vars.iter().enumerate() {
373
- if let Some(entrada) = obj.get(&format!("v{i}")) {
374
- let forma: Vec<usize> = entrada["forma"].as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
375
- let dados: Vec<f32> = entrada["dados"].as_array().map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect()).unwrap_or_default();
376
- if forma.is_empty() || dados.is_empty() {
377
- continue;
378
- }
379
- let dev = candle_core::Device::Cpu;
380
- let t = candle_core::Tensor::from_vec(dados, forma.clone(), &dev)
381
- .and_then(|t| t.reshape(forma))
382
- .map_err(|e| format!("{e}"))?;
383
- var.set(&t).map_err(|e| format!("{e}"))?;
384
- }
385
- }
386
- Ok(())
387
- }
388
 
389
- /// Utilitários de prova expostos ao Python (V-série da suíte).
390
  #[pyfunction]
391
  fn verificar_mascara(cfg: &ConfigPy) -> PyResult<usize> {
392
  let dev = candle_core::Device::Cpu;
@@ -396,6 +438,196 @@ fn verificar_mascara(cfg: &ConfigPy) -> PyResult<usize> {
396
  .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
397
  }
398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  #[pyfunction]
400
  fn verificar_stiefel(k: usize, d: usize) -> PyResult<f64> {
401
  let dev = candle_core::Device::Cpu;
@@ -404,6 +636,140 @@ fn verificar_stiefel(k: usize, d: usize) -> PyResult<f64> {
404
  stiefel::erro_ortogonalidade(&c.transpose(0, 1).unwrap()).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
405
  }
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  #[pymodule]
408
  fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
409
  pyo3_log::init();
@@ -415,6 +781,11 @@ fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
415
  m.add_function(wrap_pyfunction!(gerar_amostras, m)?)?;
416
  m.add_function(wrap_pyfunction!(verificar_mascara, m)?)?;
417
  m.add_function(wrap_pyfunction!(verificar_stiefel, m)?)?;
 
 
 
 
 
418
  m.add_class::<BpePy>()?;
419
  m.add_class::<ConfigPy>()?;
420
  m.add_class::<PonteDuplaPy>()?;
 
1
  //! KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM (Next-Concept
2
  //! Modeling), provas integradas (memoria-ncm-khtst n-02/n-03), atenção ADBA
3
+ //! (r-04), ponte duplex (r-05), cabeça de mistura de planos + SSD-Δ +
4
+ //! verificador algébrico (v0.3.0 — r-09). Documentação: docs/memorias/.
5
  //!
6
  //! Convenções do projeto (n-00 §4): nascimento neutro, leitura honesta,
7
  //! Teos numerados com ∎, token EXCLUSIVAMENTE em HF_TOKEN apagado após uso.
8
+ //!
9
+ //! v0.3.0 (M13): toda operação longa roda com `py.allow_threads` — o GIL é
10
+ //! liberado durante treino/geração/ponte (a v0.1.11 retinha o GIL e um
11
+ //! `send` bloqueante podia congelar o interpretador inteiro).
12
 
13
  use pyo3::prelude::*;
14
+ use rand::prelude::*;
15
 
16
  pub mod amostrador;
17
  pub mod atencao;
 
26
  pub mod ponte;
27
  pub mod quantizacao;
28
  pub mod quantizador;
29
+ pub mod rng;
30
  pub mod ssd;
31
  pub mod stiefel;
32
  pub mod telemetria;
 
41
 
42
  #[pyfunction]
43
  fn versao() -> &'static str {
44
+ "0.3.0 (KHTST_RUST — NCM + provas + ADBA + ponte duplex + mistura de planos + SSD-Δ + verificador algébrico)"
45
  }
46
 
47
  #[pyfunction]
 
55
  )
56
  }
57
 
58
+ /// Lint de config com sugestões por Levenshtein (diagnóstico de digitação —
59
+ /// A27: chamado pelos scripts 40/41 no início de toda execução).
60
  #[pyfunction]
61
  fn lint_config(py: Python, caminho: &str) -> PyResult<PyObject> {
62
  let txt = std::fs::read_to_string(caminho)
 
89
  #[staticmethod]
90
  fn carregar(caminho: &str) -> PyResult<Self> {
91
  let txt = std::fs::read_to_string(caminho)
92
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{caminho}: {e}")))?;
93
  let v: serde_json::Value = serde_json::from_str(&txt)
94
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
95
  let bpe = tokens::Bpe::de_json(&v)
 
107
 
108
  fn salvar(&self, caminho: &str) -> PyResult<()> {
109
  std::fs::write(caminho, serde_json::to_string(&self.bpe.para_json()).unwrap())
110
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{caminho}: {e}")))?;
111
  Ok(())
112
  }
113
 
 
126
  #[pymethods]
127
  impl ConfigPy {
128
  #[new]
 
129
  #[pyo3(signature = (caminho=None))]
130
  fn new(caminho: Option<String>) -> PyResult<Self> {
131
  let cfg = match &caminho {
 
143
  Ok(ConfigPy { cfg, caminho })
144
  }
145
 
146
+ /// v0.3.0 (A36): carrega config de um dict (sem arquivo).
147
+ #[staticmethod]
148
+ fn from_dict(d: &Bound<'_, PyAny>) -> PyResult<Self> {
149
+ let py = d.py();
150
+ let json = pythonize::depythonize::<serde_json::Value>(d)
151
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("dict inválido: {e}")))?;
152
+ let _ = py;
153
+ let cfg: config::ConfigKhtst = serde_json::from_value(json)
154
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("config: {e}")))?;
155
+ cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)?;
156
+ Ok(ConfigPy { cfg, caminho: None })
157
+ }
158
+
159
+ /// v0.3.0 (A36): exporta o config como dict.
160
+ fn as_dict(&self, py: Python) -> PyResult<PyObject> {
161
+ let v = serde_json::to_value(&self.cfg).unwrap();
162
+ Ok(pythonize::pythonize(py, &v)?.into())
163
+ }
164
+
165
  fn validar(&self) -> PyResult<()> {
166
  self.cfg.validar().map_err(pyo3::exceptions::PyValueError::new_err)
167
  }
 
172
 
173
  fn resumo(&self) -> String {
174
  format!(
175
+ "modelo(vocab={}, d={}, camadas={}, ctx={}, ssd_delta={}) ncm(L={}, niveis={:?}, K={}, d_lat={}, modo={}, ativo={}, planos M={}, algebrico={})",
176
  self.cfg.modelo.vocab, self.cfg.modelo.d_modelo, self.cfg.modelo.n_camadas,
177
+ self.cfg.modelo.comprimento_ctx, self.cfg.modelo.ssd_delta,
178
+ self.cfg.l_niveis(), self.cfg.ncm.niveis,
179
  self.cfg.ncm.k_por_nivel, self.cfg.ncm.d_lat, self.cfg.ncm.modo_quantizacao, self.cfg.ncm.ativo,
180
+ self.cfg.ncm.mistura_planos, self.cfg.ncm.lambdas.algebrico,
181
  )
182
  }
183
 
 
200
  PonteDuplaPy { ponte: ponte::PonteDupla::nova(capacidade, timeout_ms) }
201
  }
202
 
203
+ /// M13: roda SEM o GIL (allow_threads) e com prazo nos envios.
204
+ fn rodada(&self, py: Python, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<(Vec<f32>, Vec<f32>)> {
205
+ let (ma, mb) = py.allow_threads(|| self.ponte.rodada(carga_a, carga_b, nivel))
206
  .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
207
  Ok((ma.carga, mb.carga))
208
  }
209
 
210
+ fn demo_professor_aluno(&self, py: Python, rodadas: usize, dim: usize) -> PyResult<PyObject> {
211
  let inicio: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.01).collect();
212
+ let stats = py.allow_threads(|| {
213
+ ponte::demo_professor_aluno(&self.ponte, rodadas, inicio, &|a, _| {
214
+ a.iter().map(|x| x * 1.05).collect()
215
+ })
216
  })
217
  .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
218
+ Ok(pythonize::pythonize(py, &serde_json::to_value(&stats).unwrap())?.into())
 
 
219
  }
220
 
221
  fn estatisticas(&self) -> PyResult<PyObject> {
 
242
  }
243
 
244
  /// Corrotina: `await ponte.trocar(a, b, nivel)` — duplex simultânea.
245
+ /// M13: o corpo bloqueante roda com prazo (send_timeout dentro de rodada).
246
  fn trocar<'p>(&self, py: Python<'p>, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> PyResult<Bound<'p, PyAny>> {
247
  let p = self.interna.clone();
248
  pyo3_async_runtimes::tokio::future_into_py(py, async move {
 
255
  }
256
 
257
  /// Treina a fase A (tokenizador). Devolve dict com resumo + telemetria.
258
+ /// v0.3.0: `retomar` (carrega `.estado.json` ao lado do checkpoint —
259
+ /// proteção contra encerramento em background); GIL liberado (M13).
260
  #[pyfunction]
261
+ #[pyo3(signature = (cfg, sequencias, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None, retomar=false))]
262
  fn treinar_fase_a(
263
+ py: Python,
264
  cfg: &ConfigPy,
265
  sequencias: Vec<Vec<u32>>,
266
  caminho_telemetria: Option<String>,
267
  caminho_checkpoint: Option<String>,
268
  callback: Option<Bound<'_, PyAny>>,
269
  teto_seg: Option<f64>,
270
+ retomar: bool,
271
  ) -> PyResult<PyObject> {
272
  let cb: treino::CallbackPy = callback.map(|c| c.unbind());
273
+ let cfg_rust = cfg.cfg.clone();
274
+ let truncar = cfg_rust.telemetria.truncar_jsonl;
275
+ let saida = py.allow_threads(move || -> PyResult<serde_json::Value> {
276
+ crate::rng::inicializar(cfg_rust.treino.semente);
277
+ let mut hub = telemetria::TelemetryHub::novo_completo(caminho_telemetria, truncar);
278
+ let dados = treino::DadosTreino::novo(sequencias, cfg_rust.modelo.comprimento_ctx);
279
+ let resumo = treino::treinar_fase_a(&cfg_rust, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg, retomar)
280
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase A: {e}")))?;
281
+ Ok(serde_json::json!({
282
  "resumo": {
283
  "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
284
  "perda_final": resumo.perda_final, "acc_recon": resumo.acc_recon,
 
286
  "perplexidade_codebook": resumo.perplexidade_codebook,
287
  "monotonia_prefixo": resumo.monotonia_prefixo,
288
  "escore_aprendizado": resumo.escore_aprendizado,
289
+ "residuo_algebrico": resumo.residuo_algebrico,
290
+ "estado_aprendizado": resumo.estado_aprendizado,
291
  },
292
  "telemetria": hub.dump(),
293
+ }))
294
+ })?;
295
+ Ok(pythonize::pythonize(py, &saida)?.into())
296
  }
297
 
298
  /// Treina a fase B (gerador) com tokenizador congelado carregado de checkpoint.
299
  #[pyfunction]
300
+ #[pyo3(signature = (cfg, sequencias, caminho_tokenizador, caminho_telemetria=None, caminho_checkpoint=None, callback=None, teto_seg=None, retomar=false))]
301
  fn treinar_fase_b(
302
+ py: Python,
303
  cfg: &ConfigPy,
304
  sequencias: Vec<Vec<u32>>,
305
  caminho_tokenizador: &str,
 
307
  caminho_checkpoint: Option<String>,
308
  callback: Option<Bound<'_, PyAny>>,
309
  teto_seg: Option<f64>,
310
+ retomar: bool,
311
  ) -> PyResult<PyObject> {
312
  let cb: treino::CallbackPy = callback.map(|c| c.unbind());
313
+ let cfg_rust = cfg.cfg.clone();
314
+ let caminho_tok = caminho_tokenizador.to_string();
315
+ let truncar = cfg_rust.telemetria.truncar_jsonl;
316
+ let saida = py.allow_threads(move || -> PyResult<serde_json::Value> {
317
+ crate::rng::inicializar(cfg_rust.treino.semente);
318
+ let mut hub = telemetria::TelemetryHub::novo_completo(caminho_telemetria, truncar);
319
+ let dados = treino::DadosTreino::novo(sequencias, cfg_rust.modelo.comprimento_ctx);
320
+ let dev = candle_core::Device::Cpu;
321
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg_rust, &dev)
322
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
323
+ {
324
+ let mut vars = tok.vars();
325
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho_tok)
326
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
327
+ }
328
+ let resumo = treino::treinar_fase_b(&cfg_rust, &tok, &dados, &mut hub, &cb, caminho_checkpoint.as_deref(), teto_seg, retomar)
329
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("fase B: {e}")))?;
330
+ Ok(serde_json::json!({
331
  "resumo": {
332
  "passos": resumo.passos, "perda_inicial": resumo.perda_inicial,
333
  "perda_final": resumo.perda_final, "acc_conceitos": resumo.acc_recon,
334
  "escore_aprendizado": resumo.escore_aprendizado,
335
+ "residuo_algebrico": resumo.residuo_algebrico,
336
+ "estado_aprendizado": resumo.estado_aprendizado,
337
  },
338
  "telemetria": hub.dump(),
339
+ }))
340
+ })?;
341
+ Ok(pythonize::pythonize(py, &saida)?.into())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  }
343
 
344
+ /// Gera amostras com NFE = L. v0.3.0: `semente` e `max_tokens` opcionais
345
+ /// (A40 — a v0.1.11 tinha 4242/48 fixos), `n_cand` ativa MBR (P-14).
346
  #[pyfunction]
347
+ #[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, n_amostras=2, parar_no_nivel=None, greedy=true, semente=None, max_tokens=None, n_cand=None))]
348
  fn gerar_amostras(
349
+ py: Python,
350
  cfg: &ConfigPy,
351
  caminho_tokenizador: &str,
352
  caminho_gerador: &str,
353
  n_amostras: usize,
354
  parar_no_nivel: Option<usize>,
355
  greedy: bool,
356
+ semente: Option<u64>,
357
+ max_tokens: Option<usize>,
358
+ n_cand: Option<usize>,
359
  ) -> PyResult<PyObject> {
360
+ let cfg_rust = cfg.cfg.clone();
361
+ let caminho_tok = caminho_tokenizador.to_string();
362
+ let caminho_ger = caminho_gerador.to_string();
363
+ let d = py.allow_threads(move || -> PyResult<serde_json::Value> {
364
+ crate::rng::inicializar(cfg_rust.treino.semente ^ 0xABC);
365
+ let dev = candle_core::Device::Cpu;
366
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg_rust, &dev)
367
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
368
+ {
369
+ let mut vars = tok.vars();
370
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho_tok)
371
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
372
+ }
373
+ let mut ger = gerador::GeradorConceitos::novo(&cfg_rust, &dev)
374
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
375
+ {
376
+ let mut vars = ger.vars();
377
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho_ger)
378
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
379
+ }
380
+ let n_cand_efetivo = n_cand.unwrap_or(cfg_rust.ncm.mbr.n_cand).max(1);
381
+ let mut amostrador = amostrador::AmostradorConceitos::novo(
382
+ &mut ger, &tok, cfg_rust.ncm.cfg_scale, cfg_rust.ncm.temperaturas.limites,
383
+ cfg_rust.ncm.temperaturas.entropia_alvo, cfg_rust.ncm.temperaturas.auto,
384
+ );
385
+ let semente = semente.unwrap_or(4242);
386
+ let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(semente);
387
+ let inicio = std::time::Instant::now();
388
+ let (codigos, nfe) = if n_cand_efetivo > 1 {
389
+ // MBR (Teo C.4): gera n_cand candidatos e escolhe por utilidade
390
+ let mut candidatos = Vec::new();
391
+ for _ in 0..n_cand_efetivo {
392
+ let mut am = amostrador::AmostradorConceitos::novo(
393
+ &mut ger, &tok, cfg_rust.ncm.cfg_scale, cfg_rust.ncm.temperaturas.limites,
394
+ cfg_rust.ncm.temperaturas.entropia_alvo, cfg_rust.ncm.temperaturas.auto,
395
+ );
396
+ let (c, n) = am.gerar(n_amostras.max(1), &dev, &mut rng, parar_no_nivel, greedy)
397
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("amostragem: {e}")))?;
398
+ candidatos.push(c);
399
+ amostrador.nfe_contador += n;
400
+ }
401
+ let melhor = amostrador::mbr_escolher(&candidatos);
402
+ (candidatos[melhor].clone(), amostrador.nfe_contador)
403
+ } else {
404
+ amostrador.gerar(n_amostras.max(1), &dev, &mut rng, parar_no_nivel, greedy)
405
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("amostragem: {e}")))?
406
+ };
407
+ let tempo_ms = inicio.elapsed().as_secs_f64() * 1000.0;
408
+ let nivel_final = codigos.len();
409
+ let zhats = tok.reconstruir_de_codigos(&codigos, nivel_final, n_amostras.max(1), &dev)
410
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("reconstrução: {e}")))?;
411
+ let textos_ids = decodificar_textos(&tok, zhats.last(), n_amostras.max(1), max_tokens.unwrap_or(48))
412
+ .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
413
+ Ok(serde_json::json!({
414
+ "nfe": nfe, "l": cfg_rust.l_niveis(), "tempo_ms": tempo_ms,
415
  "niveis_usados": nivel_final, "ids_tokens": textos_ids,
416
  "temperaturas": amostrador.temperaturas,
417
+ "residuo_algebrico": amostrador.residuo_algebrico_ultimo,
418
+ "n_cand_mbr": n_cand_efetivo,
419
+ }))
420
+ })?;
421
+ Ok(pythonize::pythonize(py, &d)?.into())
422
  }
423
 
424
+ fn decodificar_textos(tok: &tokenizador::TokenizadorConceitos, zhat: Option<&candle_core::Tensor>, b: usize, max_tokens: usize) -> Result<Vec<Vec<u32>>, String> {
425
  let Some(z) = zhat else { return Ok(vec![vec![]; b]) };
426
+ tok.decodificar_livre(z, max_tokens).map_err(|e| format!("{e}"))
427
  }
428
 
429
+ // ---------------- verificação (V-série) ----------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
+ /// V4-estrutura: contagem de vazamentos na MATRIZ da máscara.
432
  #[pyfunction]
433
  fn verificar_mascara(cfg: &ConfigPy) -> PyResult<usize> {
434
  let dev = candle_core::Device::Cpu;
 
438
  .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
439
  }
440
 
441
+ /// Gate G1 (v0.2.0): VAZAMENTO EFETIVO — perturba os níveis > 1 e mede a
442
+ /// resposta do nível 1 (‖Δ‖∞). A v0.1.11 media a MATRIZ (V4) e reportava 0
443
+ /// enquanto o vazamento real era de 110% do sinal.
444
+ #[pyfunction]
445
+ fn verificar_vazamento_efetivo(py: Python, cfg: &ConfigPy) -> PyResult<f64> {
446
+ let cfg_rust = cfg.cfg.clone();
447
+ py.allow_threads(move || {
448
+ teste_vazamento_efetivo(&cfg_rust)
449
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
450
+ })
451
+ }
452
+
453
+ pub fn teste_vazamento_efetivo(cfg: &config::ConfigKhtst) -> candle_core::Result<f64> {
454
+ let dev = candle_core::Device::Cpu;
455
+ crate::rng::inicializar(cfg.treino.semente ^ 0xEFE);
456
+ let ger = gerador::GeradorConceitos::novo(cfg, &dev)?;
457
+ let l = ger.niveis.len();
458
+ let b = 1usize;
459
+ // monta a sequência com códigos aleatórios (determinísticos)
460
+ let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(cfg.treino.semente ^ 0xEFE);
461
+ let codigos: Vec<candle_core::Tensor> = (0..l)
462
+ .map(|i| {
463
+ let n_l = ger.niveis[i];
464
+ let v: Vec<u32> = (0..b * n_l).map(|_| rng.gen_range(0..ger.k as u32)).collect();
465
+ candle_core::Tensor::from_vec(v, (b, n_l), &dev)
466
+ })
467
+ .collect::<candle_core::Result<Vec<_>>>()?;
468
+ let zhats = {
469
+ let tok_vazio = tokenizador::TokenizadorConceitos::novo(cfg, &dev)?;
470
+ tok_vazio.reconstruir_de_codigos(&codigos, l, b, &dev)?
471
+ };
472
+ let montar = |zhats: &[candle_core::Tensor]| -> candle_core::Result<candle_core::Tensor> {
473
+ let mut blocos = Vec::new();
474
+ for nivel in 0..l {
475
+ let zhat_ant = if nivel == 0 { None } else { Some(&zhats[nivel - 1]) };
476
+ blocos.push(ger.entrada_nivel_pub(zhat_ant, nivel, b, &dev)?);
477
+ }
478
+ let prefixo_ids = candle_core::Tensor::zeros((b, gerador::N_PREFIXO), candle_core::DType::U32, &dev)?;
479
+ let prefixo = ger.emb_niveis.tabela.fwd(&prefixo_ids)?;
480
+ let mut seqs = vec![prefixo];
481
+ seqs.extend(blocos);
482
+ candle_core::Tensor::cat(&seqs, 1)
483
+ };
484
+ let s1 = montar(&zhats)?;
485
+ let mut niveis_pos = vec![0usize; gerador::N_PREFIXO];
486
+ for (i, &n_l) in ger.niveis.iter().enumerate() {
487
+ niveis_pos.extend(std::iter::repeat(i + 1).take(n_l));
488
+ }
489
+ let fwd = |s: &candle_core::Tensor| -> candle_core::Result<candle_core::Tensor> {
490
+ let mut resumos = Vec::new();
491
+ ger.tronco.forward_treino(s, &ger.mascara, &niveis_pos, &mut resumos)
492
+ };
493
+ let h1 = fwd(&s1)?;
494
+ // corrompe TODOS os níveis > 1 (×999) e mede a resposta do NÍVEL 1
495
+ // (flatten linha-major de (b, T, d): posição i corresponde a [i*d, (i+1)*d))
496
+ let n1 = ger.niveis[0];
497
+ let inicio_n2 = gerador::N_PREFIXO + n1;
498
+ let d = s1.dim(2)?;
499
+ let mut s2v = s1.flatten_all()?.to_vec1::<f32>()?;
500
+ for i in 0..s1.dim(1)? {
501
+ if i >= inicio_n2 {
502
+ for j in 0..d {
503
+ s2v[i * d + j] *= 999.0;
504
+ }
505
+ }
506
+ }
507
+ let s2 = candle_core::Tensor::from_vec(s2v, s1.shape(), &dev)?;
508
+ let h2 = fwd(&s2)?;
509
+ let dif = (&h1.narrow(1, gerador::N_PREFIXO, n1)? - &h2.narrow(1, gerador::N_PREFIXO, n1)?)?;
510
+ Ok(dif.abs()?.max_all()?.to_scalar::<f32>()? as f64)
511
+ }
512
+
513
+ /// Gate G2 (v0.2.0): paridade treino↔inferência — max|Δ| entre o forward
514
+ /// completo e o incremental (nível a nível, com caches/estado).
515
+ #[pyfunction]
516
+ fn verificar_paridade(py: Python, cfg: &ConfigPy) -> PyResult<PyObject> {
517
+ let cfg_rust = cfg.cfg.clone();
518
+ let r = py.allow_threads(move || {
519
+ teste_paridade(&cfg_rust)
520
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
521
+ })?;
522
+ Python::with_gil(|py| Ok(pythonize::pythonize(py, &r)?.into()))
523
+ }
524
+
525
+ pub fn teste_paridade(cfg: &config::ConfigKhtst) -> candle_core::Result<serde_json::Value> {
526
+ use candle_core::Tensor;
527
+ let dev = candle_core::Device::Cpu;
528
+ crate::rng::inicializar(cfg.treino.semente ^ 0xEA2);
529
+ let mut ger = gerador::GeradorConceitos::novo(cfg, &dev)?;
530
+ let l = ger.niveis.len();
531
+ let b = 1usize;
532
+ let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(cfg.treino.semente ^ 0xEA2);
533
+ let codigos: Vec<Tensor> = (0..l)
534
+ .map(|i| {
535
+ let n_l = ger.niveis[i];
536
+ let v: Vec<u32> = (0..b * n_l).map(|_| rng.gen_range(0..ger.k as u32)).collect();
537
+ Tensor::from_vec(v, (b, n_l), &dev)
538
+ })
539
+ .collect::<candle_core::Result<Vec<_>>>()?;
540
+ let zhats = {
541
+ let tok = tokenizador::TokenizadorConceitos::novo(cfg, &dev)?;
542
+ tok.reconstruir_de_codigos(&codigos, l, b, &dev)?
543
+ };
544
+ let mut blocos = Vec::new();
545
+ for nivel in 0..l {
546
+ let zhat_ant = if nivel == 0 { None } else { Some(&zhats[nivel - 1]) };
547
+ blocos.push(ger.entrada_nivel_pub(zhat_ant, nivel, b, &dev)?);
548
+ }
549
+ let prefixo_ids = Tensor::zeros((b, gerador::N_PREFIXO), candle_core::DType::U32, &dev)?;
550
+ let prefixo = ger.emb_niveis.tabela.fwd(&prefixo_ids)?;
551
+ let mut seqs = vec![prefixo];
552
+ seqs.extend(blocos.iter().cloned());
553
+ let s = Tensor::cat(&seqs, 1)?;
554
+ let mut niveis_pos = vec![0usize; gerador::N_PREFIXO];
555
+ for (i, &n_l) in ger.niveis.iter().enumerate() {
556
+ niveis_pos.extend(std::iter::repeat(i + 1).take(n_l));
557
+ }
558
+ let mut resumos = Vec::new();
559
+ let h_full = ger.tronco.forward_treino(&s, &ger.mascara, &niveis_pos, &mut resumos)?;
560
+ // incremental
561
+ let mut estado = tronco::EstadoNivel::novo();
562
+ let mut difs = Vec::new();
563
+ let mut inicio = gerador::N_PREFIXO;
564
+ for nivel in 0..l {
565
+ let n_l = ger.niveis[nivel];
566
+ let entrada = if nivel == 0 {
567
+ let pe = ger.emb_niveis.tabela.fwd(&prefixo_ids)?;
568
+ Tensor::cat(&[pe, blocos[nivel].clone()], 1)?
569
+ } else {
570
+ blocos[nivel].clone()
571
+ };
572
+ let n_novo = entrada.dim(1)?;
573
+ let n_cache = estado.caches.first().and_then(|c| c.k.as_ref()).map(|k| k.dims()[2]).unwrap_or(0);
574
+ let total_vis = n_cache + n_novo;
575
+ // v0.2.0: na PRIMEIRA janela (prefixo + nível 1, sem cache) a máscara
576
+ // é o canto superior-esquerdo da block-causal: prefixo causal dentro
577
+ // do prefixo; nível 1 vê TUDO (bidirecional intra-nível, como no
578
+ // treino — Teo D.1(ii)). Nas demais: tudo visível (o bloco novo é o
579
+ // mais recente).
580
+ let mascara = if n_cache == 0 {
581
+ let total = n_novo;
582
+ let mut m = vec![f32::MIN; total * total];
583
+ // prefixo causal
584
+ for i in 0..gerador::N_PREFIXO.min(total) {
585
+ for j in 0..=i {
586
+ m[i * total + j] = 0.0;
587
+ }
588
+ }
589
+ // posições do nível 1: veem o bloco inteiro (bidirecional)
590
+ for i in gerador::N_PREFIXO.min(total)..total {
591
+ for j in 0..total {
592
+ m[i * total + j] = 0.0;
593
+ }
594
+ }
595
+ Tensor::from_vec(m, (total, total), &dev)?
596
+ } else {
597
+ let m = vec![0f32; n_novo * total_vis];
598
+ Tensor::from_vec(m, (n_novo, total_vis), &dev)?
599
+ };
600
+ // níveis dos queries: no 1º call inclui o prefixo (nível 0)
601
+ let niveis_novos = if n_cache == 0 {
602
+ let mut v = vec![0usize; gerador::N_PREFIXO.min(n_novo)];
603
+ v.extend(std::iter::repeat(nivel + 1).take(n_novo - gerador::N_PREFIXO.min(n_novo)));
604
+ v
605
+ } else {
606
+ vec![nivel + 1; n_novo]
607
+ };
608
+ let mut niveis_totais = vec![0usize; gerador::N_PREFIXO.min(n_cache)];
609
+ let restante = n_cache.saturating_sub(gerador::N_PREFIXO);
610
+ let mut usados = 0usize;
611
+ for (li, &nl) in ger.niveis.iter().enumerate().take(nivel) {
612
+ if usados >= restante { break; }
613
+ let tomar = nl.min(restante - usados);
614
+ niveis_totais.extend(std::iter::repeat(li + 1).take(tomar));
615
+ usados += tomar;
616
+ }
617
+ niveis_totais.extend(niveis_novos.clone());
618
+ let h_inc = ger.tronco.forward_nivel(&entrada, &mut estado, &mascara, nivel + 1, &niveis_novos, &niveis_totais)?;
619
+ let ref_nivel = h_full.narrow(1, inicio, n_l)?;
620
+ let dif = (&ref_nivel - &h_inc.narrow(1, if nivel == 0 { gerador::N_PREFIXO } else { 0 }, n_l)?)?;
621
+ difs.push(dif.abs()?.max_all()?.to_scalar::<f32>()? as f64);
622
+ inicio += n_l;
623
+ }
624
+ Ok(serde_json::json!({
625
+ "difs_por_nivel": difs,
626
+ "dif_max": difs.iter().cloned().fold(0f64, f64::max),
627
+ }))
628
+ }
629
+
630
+ /// V9: δ_ort de um codebook RECENTEMENTE INICIALIZADO (k, d).
631
  #[pyfunction]
632
  fn verificar_stiefel(k: usize, d: usize) -> PyResult<f64> {
633
  let dev = candle_core::Device::Cpu;
 
636
  stiefel::erro_ortogonalidade(&c.transpose(0, 1).unwrap()).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))
637
  }
638
 
639
+ /// v0.3.0 (A36): δ_ort do codebook TREINADO num checkpoint de tokenizador.
640
+ #[pyfunction]
641
+ fn verificar_delta_ort(py: Python, cfg: &ConfigPy, caminho: &str) -> PyResult<PyObject> {
642
+ let cfg_rust = cfg.cfg.clone();
643
+ let caminho = caminho.to_string();
644
+ let r = py.allow_threads(move || -> PyResult<serde_json::Value> {
645
+ crate::rng::inicializar(cfg_rust.treino.semente ^ 0x0D0);
646
+ let dev = candle_core::Device::Cpu;
647
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg_rust, &dev)
648
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
649
+ {
650
+ let mut vars = tok.vars();
651
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho)
652
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
653
+ }
654
+ let deltas: PyResult<Vec<f64>> = tok
655
+ .quants
656
+ .iter()
657
+ .map(|q| q.delta_ort().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}"))))
658
+ .collect();
659
+ let deltas = deltas?;
660
+ Ok(serde_json::json!({
661
+ "delta_ort_por_nivel": deltas,
662
+ "delta_ort_max": deltas.iter().cloned().fold(0f64, f64::max),
663
+ }))
664
+ })?;
665
+ Python::with_gil(|py| Ok(pythonize::pythonize(py, &r)?.into()))
666
+ }
667
+
668
+ /// v0.3.0 (A20): inspeção de checkpoint — valida manifesto (contagem/formas).
669
+ #[pyfunction]
670
+ fn verificar_checkpoint(caminho: &str) -> PyResult<PyObject> {
671
+ let txt = std::fs::read_to_string(caminho)
672
+ .map_err(|e| pyo3::exceptions::PyIOError::new_err(format!("{caminho}: {e}")))?;
673
+ let v: serde_json::Value = serde_json::from_str(&txt)
674
+ .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("JSON inválido: {e}")))?;
675
+ let obj = v.as_object().ok_or_else(|| pyo3::exceptions::PyValueError::new_err("formato inválido"))?;
676
+ let man = obj.get("__manifesto").ok_or_else(|| {
677
+ pyo3::exceptions::PyValueError::new_err("checkpoint SEM manifesto (formato pré-v0.2.0?)")
678
+ })?;
679
+ let n_vars = man["n_vars"].as_u64().unwrap_or(0) as usize;
680
+ let presentes = obj.keys().filter(|k| k.starts_with('v') && !k.starts_with("__")).count();
681
+ let ok = n_vars == presentes;
682
+ Python::with_gil(|py| {
683
+ Ok(pythonize::pythonize(py, &serde_json::json!({
684
+ "caminho": caminho,
685
+ "formato": man["formato"], "tipo": man["tipo"],
686
+ "n_vars_declarado": n_vars, "n_vars_presentes": presentes,
687
+ "consistente": ok,
688
+ }))?.into())
689
+ })
690
+ }
691
+
692
+ /// v0.3.0 (V16 — prova aritmética): avaliação SEM treino — carrega
693
+ /// tokenizador+gerador e devolve a CE (por nível e média) sobre as
694
+ /// sequências dadas. Usado pela prova de percepção de inteiros/floats:
695
+ /// CE(progressão aritmética) < CE(controle embaralhado).
696
+ #[pyfunction]
697
+ #[pyo3(signature = (cfg, caminho_tokenizador, caminho_gerador, sequencias))]
698
+ fn avaliar_ce(
699
+ py: Python,
700
+ cfg: &ConfigPy,
701
+ caminho_tokenizador: &str,
702
+ caminho_gerador: &str,
703
+ sequencias: Vec<Vec<u32>>,
704
+ ) -> PyResult<PyObject> {
705
+ let cfg_rust = cfg.cfg.clone();
706
+ let caminho_tok = caminho_tokenizador.to_string();
707
+ let caminho_ger = caminho_gerador.to_string();
708
+ let d = py.allow_threads(move || -> PyResult<serde_json::Value> {
709
+ crate::rng::inicializar(cfg_rust.treino.semente ^ 0xABC);
710
+ let dev = candle_core::Device::Cpu;
711
+ let mut tok = tokenizador::TokenizadorConceitos::novo(&cfg_rust, &dev)
712
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
713
+ {
714
+ let mut vars = tok.vars();
715
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho_tok)
716
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
717
+ }
718
+ let mut ger = gerador::GeradorConceitos::novo(&cfg_rust, &dev)
719
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("construção: {e}")))?;
720
+ {
721
+ let mut vars = ger.vars();
722
+ treino::carregar_vars_do_checkpoint(&mut vars, &caminho_ger)
723
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
724
+ }
725
+ // congela a corrupção/dropout para avaliação determinística
726
+ let dados = treino::DadosTreino::novo(sequencias, cfg_rust.modelo.comprimento_ctx);
727
+ let mut rng = <rand::prelude::StdRng as rand::SeedableRng>::seed_from_u64(271828);
728
+ let mut ces_por_nivel: Vec<Vec<f64>> = Vec::new();
729
+ let mut accs = Vec::new();
730
+ let mut residuos = Vec::new();
731
+ // avalia em minibatches do tamanho do lote de config
732
+ let mut todos: Vec<candle_core::Tensor> = Vec::new();
733
+ for s in &dados.sequencias {
734
+ todos.push(candle_core::Tensor::from_vec(s.clone(), (1, s.len()), &dev).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
735
+ }
736
+ for chunk in todos.chunks(cfg_rust.treino.lote.max(1)) {
737
+ let plano = chunk[0].clone();
738
+ let b = chunk.len();
739
+ let n = plano.dim(1).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
740
+ let mut v = Vec::with_capacity(b * n);
741
+ for t in chunk {
742
+ v.extend(t.flatten_all().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?.to_vec1::<u32>().map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?);
743
+ }
744
+ let ids = candle_core::Tensor::from_vec(v, (b, n), &dev)
745
+ .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
746
+ let cod = tok.codificar_congelado(&ids).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
747
+ let saida = ger.perda_treino(&tok, &cod.ids, &mut rng).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{e}")))?;
748
+ ces_por_nivel.push(saida.ce_por_nivel.clone());
749
+ accs.push(saida.acuracia_media);
750
+ residuos.push(saida.residuo_algebrico);
751
+ }
752
+ // média por nível
753
+ let l = cfg_rust.l_niveis();
754
+ let mut media_nivel = vec![0f64; l];
755
+ for ces in &ces_por_nivel {
756
+ for (i, c) in ces.iter().enumerate() {
757
+ if i < l {
758
+ media_nivel[i] += c / ces_por_nivel.len() as f64;
759
+ }
760
+ }
761
+ }
762
+ let media = media_nivel.iter().sum::<f64>() / l as f64;
763
+ Ok(serde_json::json!({
764
+ "ce_media": media,
765
+ "ce_por_nivel": media_nivel,
766
+ "acuracia_media": accs.iter().sum::<f64>() / accs.len().max(1) as f64,
767
+ "residuo_algebrico": residuos.iter().sum::<f64>() / residuos.len().max(1) as f64,
768
+ }))
769
+ })?;
770
+ Ok(pythonize::pythonize(py, &d)?.into())
771
+ }
772
+
773
  #[pymodule]
774
  fn nucleo_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
775
  pyo3_log::init();
 
781
  m.add_function(wrap_pyfunction!(gerar_amostras, m)?)?;
782
  m.add_function(wrap_pyfunction!(verificar_mascara, m)?)?;
783
  m.add_function(wrap_pyfunction!(verificar_stiefel, m)?)?;
784
+ m.add_function(wrap_pyfunction!(verificar_vazamento_efetivo, m)?)?;
785
+ m.add_function(wrap_pyfunction!(verificar_paridade, m)?)?;
786
+ m.add_function(wrap_pyfunction!(verificar_delta_ort, m)?)?;
787
+ m.add_function(wrap_pyfunction!(verificar_checkpoint, m)?)?;
788
+ m.add_function(wrap_pyfunction!(avaliar_ce, m)?)?;
789
  m.add_class::<BpePy>()?;
790
  m.add_class::<ConfigPy>()?;
791
  m.add_class::<PonteDuplaPy>()?;
src/mascaras.rs CHANGED
@@ -1,14 +1,45 @@
1
  //! Máscaras e interpolação multiscale (port de `conceitos/mascaras.py` do n-04 §2.2;
2
  //! contratos dos Teos D.1(ii)/B.2 — verificação V4: vazamento zero).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  use candle_core::{D, DType, Device, Result, Tensor};
 
5
 
6
- /// Máscara aditiva block-causal B (Teo D.1(ii)): o slot do bloco ℓ atende ao
7
- /// prefixo (sempre visível) e a TODOS os blocos ≤ ℓ (bidirecional no bloco
8
- /// próprio). `tamanhos` = [n_1, …, n_L]; `n_prefixo` = |cond|.
 
 
 
 
 
 
 
9
  ///
10
- /// Retorna matriz (T+P, T+P) f32 com 0 onde permitido e f32::MIN onde bloqueado,
11
- /// pronta para `scores + mask` (broadcast).
12
  pub fn mascara_block_causal(tamanhos: &[usize], n_prefixo: usize, dev: &Device) -> Result<Tensor> {
13
  let mut cortes = vec![n_prefixo];
14
  for &t in tamanhos {
@@ -17,6 +48,12 @@ pub fn mascara_block_causal(tamanhos: &[usize], n_prefixo: usize, dev: &Device)
17
  }
18
  let total = *cortes.last().unwrap();
19
  let mut m = vec![f32::MIN; total * total];
 
 
 
 
 
 
20
  for (bloco_i, (&ini_i, &fim_i)) in cortes.iter().zip(cortes.iter().skip(1)).enumerate() {
21
  for i in ini_i..fim_i {
22
  // prefixo sempre visível
@@ -33,8 +70,23 @@ pub fn mascara_block_causal(tamanhos: &[usize], n_prefixo: usize, dev: &Device)
33
  Tensor::from_vec(m, (total, total), dev)?.to_dtype(DType::F32)
34
  }
35
 
36
- /// Verificação V4 — nenhuma posição do bloco ℓ acessa bloco > ℓ.
37
- /// Devolve o número de vazamentos (deve ser 0).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  pub fn contar_vazamentos(tamanhos: &[usize], n_prefixo: usize, mascara: &Tensor) -> Result<usize> {
39
  let total = mascara.dim(0)?;
40
  let dados = mascara.flatten_all()?.to_vec1::<f32>()?;
@@ -57,19 +109,60 @@ pub fn contar_vazamentos(tamanhos: &[usize], n_prefixo: usize, mascara: &Tensor)
57
  Ok(vazamentos)
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  /// Matriz de interpolação linear `Interp_n` (n-02 §0): R^{n_de × d} → R^{n_para × d}.
61
  /// - downsample (n_para < n_de): média por segmento com pesos de sobreposição (área),
62
  /// linhas somam 1 (contrato: "down = avg-pool por segmento");
63
  /// - upsample: interpolação linear entre os 2 vizinhos.
64
  /// Linear, determinística, sem parâmetros (gradiente flui por matmul).
 
 
 
65
  pub fn matriz_interp(n_de: usize, n_para: usize, dev: &Device) -> Result<Tensor> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  let mut w = vec![0f32; n_para * n_de];
67
  if n_de == n_para {
68
  for i in 0..n_para {
69
  w[i * n_de + i] = 1.0;
70
  }
71
  } else if n_para < n_de {
72
- // média por segmento com bordas fracionárias
73
  let razao = n_de as f64 / n_para as f64;
74
  for i in 0..n_para {
75
  let ini = i as f64 * razao;
@@ -83,7 +176,6 @@ pub fn matriz_interp(n_de: usize, n_para: usize, dev: &Device) -> Result<Tensor>
83
  }
84
  }
85
  } else {
86
- // upsample linear
87
  let razao = n_de as f64 / n_para as f64;
88
  for i in 0..n_para {
89
  let centro = (i as f64 + 0.5) * razao - 0.5;
@@ -102,7 +194,6 @@ pub fn matriz_interp(n_de: usize, n_para: usize, dev: &Device) -> Result<Tensor>
102
  pub fn interp_para(x: &Tensor, n_para: usize, dev: &Device) -> Result<Tensor> {
103
  let n_de = x.dim(D::Minus2)?;
104
  let w = matriz_interp(n_de, n_para, dev)?;
105
- // (n_para, n_de) x (..., n_de, d) -> (..., n_para, d)
106
  w.broadcast_matmul(x)
107
  }
108
 
@@ -124,14 +215,43 @@ mod testes {
124
  Ok(())
125
  }
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  #[test]
128
  fn interp_preserva_constante() -> candle_core::Result<()> {
129
  let dev = Device::Cpu;
130
- // downsample de vetor constante → constante (linhas somam 1)
131
  let x = Tensor::from_vec(vec![2.0f32; 8], (8, 1), &dev)?;
132
  let y = interp_para(&x, 4, &dev)?;
133
  let v = y.flatten_all()?.to_vec1::<f32>()?;
134
  assert!(v.iter().all(|&a| (a - 2.0).abs() < 1e-5), "{v:?}");
135
  Ok(())
136
  }
 
 
 
 
 
 
 
 
 
 
 
137
  }
 
1
  //! Máscaras e interpolação multiscale (port de `conceitos/mascaras.py` do n-04 §2.2;
2
  //! contratos dos Teos D.1(ii)/B.2 — verificação V4: vazamento zero).
3
+ //!
4
+ //! ## Correção M1 (v0.2.0 — auditoria r-08)
5
+ //! Na v0.1.11 as linhas `i < n_prefixo` nunca eram escritas ⇒ permaneciam
6
+ //! `f32::MIN` em TODAS as colunas. O softmax de uma linha totalmente bloqueada
7
+ //! **não é nulo — é uniforme** (o softmax subtrai o máximo; `exp(MIN−MIN)=1`):
8
+ //! o prefixo "enxergava" a sequência inteira, inclusive níveis FUTUROS, e como
9
+ //! o prefixo é visível a todos os blocos existia o caminho
10
+ //! `nível futuro → prefixo → nível ℓ` — a fatoração next-concept (Teo B.1/D.1)
11
+ //! estava quebrada em treino, e o V4 (que conta a MATRIZ) reportava 0.
12
+ //!
13
+ //! **Teo M-1 (isolamento causal do prefixo).** Com a máscara intra-prefixo
14
+ //! causal (`M[i][j]=0 ⇔ j ≤ i` para `i < P`) e blocos visíveis apenas a
15
+ //! `j < fim(ℓ)`, vale: a saída das posições do prefixo é função apenas do
16
+ //! prefixo; a saída do nível ℓ é função de `prefixo ∪ níveis ≤ ℓ`.
17
+ //! *Prova:* o softmax normaliza por linha sobre as colunas visíveis; com
18
+ //! `M[i][j] = MIN` para `j > i` (prefixo) e `j ≥ fim(ℓ)` (bloco ℓ), os pesos
19
+ //! fora do conjunto permitido são `exp(MIN − max) = 0` exatamente (subtração
20
+ //! do máximo por linha no candle), logo o valor esperado dos V's não inclui
21
+ //! posições proibidas; por indução em blocos (o bloco b+1 só lê saídas de
22
+ //! blocos ≤ b, que por hipótese já são causais), o mapa inteiro fatora por
23
+ //! `s_{permitido}`. ∎
24
+ //!
25
+ //! A verificação EFETIVA (perturbar o futuro e medir a resposta — gate G1)
26
+ //! vive em `tests/provas.rs` e `lib.rs::verificar_vazamento_efetivo`.
27
 
28
  use candle_core::{D, DType, Device, Result, Tensor};
29
+ use std::cell::RefCell;
30
 
31
+ /// Visibilidade do prefixo (Teo D.1(ii) + correção M1).
32
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33
+ pub enum VisaoPrefixo {
34
+ /// Causal dentro do prefixo (correto): posição i < P vê j ≤ i.
35
+ Causal,
36
+ }
37
+
38
+ /// Máscara aditiva block-causal B (Teo D.1(ii) + M1): o slot do bloco ℓ atende
39
+ /// ao prefixo e a TODOS os blocos ≤ ℓ (bidirecional no bloco próprio); o
40
+ /// prefixo é causal internamente. `tamanhos` = [n_1, …, n_L].
41
  ///
42
+ /// Retorna matriz (T+P, T+P) f32 com 0 onde permitido e f32::MIN onde bloqueado.
 
43
  pub fn mascara_block_causal(tamanhos: &[usize], n_prefixo: usize, dev: &Device) -> Result<Tensor> {
44
  let mut cortes = vec![n_prefixo];
45
  for &t in tamanhos {
 
48
  }
49
  let total = *cortes.last().unwrap();
50
  let mut m = vec![f32::MIN; total * total];
51
+ // M1: prefixo causal — SEM linha totalmente bloqueada
52
+ for i in 0..n_prefixo {
53
+ for j in 0..=i {
54
+ m[i * total + j] = 0.0;
55
+ }
56
+ }
57
  for (bloco_i, (&ini_i, &fim_i)) in cortes.iter().zip(cortes.iter().skip(1)).enumerate() {
58
  for i in ini_i..fim_i {
59
  // prefixo sempre visível
 
70
  Tensor::from_vec(m, (total, total), dev)?.to_dtype(DType::F32)
71
  }
72
 
73
+ /// Detector M1: devolve o número de linhas da máscara SEM nenhuma posição
74
+ /// visível (≥ 0.0). Com a correção, deve ser 0 para toda máscara produzida
75
+ /// por `mascara_block_causal` (gate G4).
76
+ pub fn linhas_sem_visivel(mascara: &Tensor) -> Result<usize> {
77
+ let total = mascara.dim(0)?;
78
+ let dados = mascara.flatten_all()?.to_vec1::<f32>()?;
79
+ let mut n = 0usize;
80
+ for i in 0..total {
81
+ if !dados[i * total..(i + 1) * total].iter().any(|&v| v >= 0.0) {
82
+ n += 1;
83
+ }
84
+ }
85
+ Ok(n)
86
+ }
87
+
88
+ /// Verificação V4 — nenhuma posição do bloco ℓ acessa bloco > ℓ (MATRIZ).
89
+ /// O vazamento EFETIVO (função, não estrutura) é verificado pelo gate G1.
90
  pub fn contar_vazamentos(tamanhos: &[usize], n_prefixo: usize, mascara: &Tensor) -> Result<usize> {
91
  let total = mascara.dim(0)?;
92
  let dados = mascara.flatten_all()?.to_vec1::<f32>()?;
 
109
  Ok(vazamentos)
110
  }
111
 
112
+ /// Máscara incremental (rota KV-cache — M1 também aqui): as `n_novo` posições
113
+ /// novas do nível ℓ+1 veem o cache (prefixo + níveis ≤ ℓ) e o próprio bloco.
114
+ /// A v0.1.11 usava `vec![0.0]` (TUDO liberado) — a rota incremental herda o
115
+ /// mesmo contrato block-causal do treino.
116
+ pub fn mascara_incremental(n_novo: usize, total_vis: usize, dev: &Device) -> Result<Tensor> {
117
+ // linhas = posições novas (todas veem cache + próprias); a máscara é aditiva
118
+ // (n_novo × total_vis) — nada de futuro existe no incremental, logo tudo 0
119
+ // é correto AQUI; a correção M1 desta rota é que o PREFIXO no cache foi
120
+ // produzido pela máscara causal (treino) e não pela uniforme.
121
+ let m = vec![0f32; n_novo * total_vis];
122
+ Tensor::from_vec(m, (n_novo, total_vis), dev)?.to_dtype(DType::F32)
123
+ }
124
+
125
  /// Matriz de interpolação linear `Interp_n` (n-02 §0): R^{n_de × d} → R^{n_para × d}.
126
  /// - downsample (n_para < n_de): média por segmento com pesos de sobreposição (área),
127
  /// linhas somam 1 (contrato: "down = avg-pool por segmento");
128
  /// - upsample: interpolação linear entre os 2 vizinhos.
129
  /// Linear, determinística, sem parâmetros (gradiente flui por matmul).
130
+ ///
131
+ /// M16: memoização thread-local por `(n_de, n_para, ptr-dispositivo)` — a
132
+ /// v0.1.11 reconstruía a matriz a cada chamada (~16/passo × até 256 KB).
133
  pub fn matriz_interp(n_de: usize, n_para: usize, dev: &Device) -> Result<Tensor> {
134
+ thread_local! {
135
+ static CACHE: RefCell<Vec<(usize, usize, u64, Tensor)>> = RefCell::new(Vec::new());
136
+ }
137
+ let chave_dev = match dev {
138
+ Device::Cpu => 0u64,
139
+ #[allow(unreachable_patterns)]
140
+ _ => 1u64,
141
+ };
142
+ CACHE.with(|c| {
143
+ if let Ok(mut c) = c.try_borrow_mut() {
144
+ if let Some((_, _, _, t)) = c.iter().find(|(a, b, d, _)| *a == n_de && *b == n_para && *d == chave_dev) {
145
+ return Ok(t.clone());
146
+ }
147
+ }
148
+ let t = matriz_interp_bruta(n_de, n_para, dev)?;
149
+ if let Ok(mut c) = c.try_borrow_mut() {
150
+ if c.len() > 64 {
151
+ c.remove(0);
152
+ }
153
+ c.push((n_de, n_para, chave_dev, t.clone()));
154
+ }
155
+ Ok(t)
156
+ })
157
+ }
158
+
159
+ fn matriz_interp_bruta(n_de: usize, n_para: usize, dev: &Device) -> Result<Tensor> {
160
  let mut w = vec![0f32; n_para * n_de];
161
  if n_de == n_para {
162
  for i in 0..n_para {
163
  w[i * n_de + i] = 1.0;
164
  }
165
  } else if n_para < n_de {
 
166
  let razao = n_de as f64 / n_para as f64;
167
  for i in 0..n_para {
168
  let ini = i as f64 * razao;
 
176
  }
177
  }
178
  } else {
 
179
  let razao = n_de as f64 / n_para as f64;
180
  for i in 0..n_para {
181
  let centro = (i as f64 + 0.5) * razao - 0.5;
 
194
  pub fn interp_para(x: &Tensor, n_para: usize, dev: &Device) -> Result<Tensor> {
195
  let n_de = x.dim(D::Minus2)?;
196
  let w = matriz_interp(n_de, n_para, dev)?;
 
197
  w.broadcast_matmul(x)
198
  }
199
 
 
215
  Ok(())
216
  }
217
 
218
+ /// Gate G4 (parte 1): NENHUMA linha totalmente bloqueada (o defeito M1).
219
+ #[test]
220
+ fn gate_mascara_sem_linhas_vazias() -> candle_core::Result<()> {
221
+ let dev = Device::Cpu;
222
+ let tamanhos = [4usize, 8, 16, 32, 64, 128, 256];
223
+ let m = mascara_block_causal(&tamanhos, 8, &dev)?;
224
+ assert_eq!(linhas_sem_visivel(&m)?, 0, "M1: nenhuma linha pode estar 100% bloqueada");
225
+ // prefixo causal: linha 0 vê apenas a coluna 0
226
+ let dados = m.flatten_all()?.to_vec1::<f32>()?;
227
+ let total = m.dim(0)?;
228
+ assert!(dados[0] >= 0.0 && dados[1] == f32::MIN, "prefixo deve ser causal");
229
+ // linha 7 (última do prefixo) vê 0..=7
230
+ for j in 0..total {
231
+ let esperado = if j <= 7 { 0.0 } else { f32::MIN };
232
+ assert_eq!(dados[7 * total + j], esperado, "linha 7 col {j}");
233
+ }
234
+ Ok(())
235
+ }
236
+
237
  #[test]
238
  fn interp_preserva_constante() -> candle_core::Result<()> {
239
  let dev = Device::Cpu;
 
240
  let x = Tensor::from_vec(vec![2.0f32; 8], (8, 1), &dev)?;
241
  let y = interp_para(&x, 4, &dev)?;
242
  let v = y.flatten_all()?.to_vec1::<f32>()?;
243
  assert!(v.iter().all(|&a| (a - 2.0).abs() < 1e-5), "{v:?}");
244
  Ok(())
245
  }
246
+
247
+ /// M16: memoização devolve a MESMA matriz (ponteiro/igualdade de valores).
248
+ #[test]
249
+ fn interp_memoizada() -> candle_core::Result<()> {
250
+ let dev = Device::Cpu;
251
+ let a = matriz_interp(64, 32, &dev)?;
252
+ let b = matriz_interp(64, 32, &dev)?;
253
+ let dif = (&a - &b)?.abs()?.max_all()?.to_scalar::<f32>()?;
254
+ assert_eq!(dif, 0.0);
255
+ Ok(())
256
+ }
257
  }
src/memoria.rs CHANGED
@@ -17,14 +17,33 @@ pub struct SomConceitos {
17
 
18
  impl SomConceitos {
19
  pub fn novo(lado: usize, d_lat: usize, dev: &Device) -> Result<Self> {
 
20
  Ok(SomConceitos {
21
- prototipos: Tensor::randn(0f32, 0.1, (lado * lado, d_lat), dev)?,
22
  lado,
23
  lr: 0.05,
24
  eq_medio: 0.0,
25
  })
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  /// Insere vetores (n, d_lat) com atualização online (sem gradiente — V14).
29
  pub fn inserir(&mut self, vetores: &Tensor) -> Result<f64> {
30
  let p = self.prototipos.flatten_all()?.to_vec1::<f32>()?;
 
17
 
18
  impl SomConceitos {
19
  pub fn novo(lado: usize, d_lat: usize, dev: &Device) -> Result<Self> {
20
+ crate::rng::inicializar(crate::rng::SEMENTE_SOM);
21
  Ok(SomConceitos {
22
+ prototipos: crate::rng::randn_semeado(0.1, &[lado * lado, d_lat], dev)?,
23
  lado,
24
  lr: 0.05,
25
  eq_medio: 0.0,
26
  })
27
  }
28
 
29
+ /// v0.3.0 (A22/retomada): protótipos serializados.
30
+ pub fn prototipos_flat(&self) -> Vec<f32> {
31
+ self.prototipos.flatten_all().unwrap().to_vec1::<f32>().unwrap()
32
+ }
33
+
34
+ pub fn lado(&self) -> usize {
35
+ self.lado
36
+ }
37
+
38
+ /// v0.3.0 (retomada): restaura protótipos salvos.
39
+ pub fn restaurar(&mut self, valores: &[f32], lado: usize) -> Result<()> {
40
+ let d = if lado > 0 { valores.len() / (lado * lado) } else { 0 };
41
+ let dev = self.prototipos.device();
42
+ self.prototipos = Tensor::from_vec(valores.to_vec(), (lado * lado, d), &dev)?;
43
+ self.lado = lado;
44
+ Ok(())
45
+ }
46
+
47
  /// Insere vetores (n, d_lat) com atualização online (sem gradiente — V14).
48
  pub fn inserir(&mut self, vetores: &Tensor) -> Result<f64> {
49
  let p = self.prototipos.flatten_all()?.to_vec1::<f32>()?;
src/ponte.rs CHANGED
@@ -24,7 +24,6 @@ use serde::{Deserialize, Serialize};
24
  use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
25
  use std::sync::{Arc, Mutex};
26
  use std::time::{Duration, Instant};
27
-
28
  #[derive(Debug, Clone, Serialize, Deserialize)]
29
  pub struct Mensagem {
30
  pub rodada: u64,
@@ -53,7 +52,7 @@ struct Lado {
53
  nome: String,
54
  para_outro: Sender<Mensagem>,
55
  do_outro: Receiver<Mensagem>,
56
- ultima_janela: Mutex<Option<(Instant, Instant)>>,
57
  }
58
 
59
  pub struct PonteDupla {
@@ -70,8 +69,8 @@ impl PonteDupla {
70
  let (tx_ab, rx_ab) = bounded::<Mensagem>(capacidade);
71
  let (tx_ba, rx_ba) = bounded::<Mensagem>(capacidade);
72
  PonteDupla {
73
- a: Lado { nome: "A".into(), para_outro: tx_ab, do_outro: rx_ba, ultima_janela: Mutex::new(None) },
74
- b: Lado { nome: "B".into(), para_outro: tx_ba, do_outro: rx_ab, ultima_janela: Mutex::new(None) },
75
  stats: Arc::new(Mutex::new(EstatisticasPonte::default())),
76
  ativa: Arc::new(AtomicBool::new(true)),
77
  capacidade,
@@ -81,6 +80,13 @@ impl PonteDupla {
81
 
82
  /// Uma RODADA de troca duplex: ambos os lados enviam e recebem com
83
  /// timeout — nenhuma espera ilimitada (Teo P.1 — sem deadlock).
 
 
 
 
 
 
 
84
  pub fn rodada(&self, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> Result<(Mensagem, Mensagem), String> {
85
  let inicio = Instant::now();
86
  let rodada = {
@@ -89,11 +95,39 @@ impl PonteDupla {
89
  };
90
  let msg_a = Mensagem { rodada, origem: "A".into(), nivel, ts_us: inicio.elapsed().as_micros(), carga: carga_a };
91
  let msg_b = Mensagem { rodada, origem: "B".into(), nivel, ts_us: inicio.elapsed().as_micros(), carga: carga_b };
92
- // envios (ambas as direções)
93
- let t_env_a = Instant::now();
94
- self.a.para_outro.send(msg_a).map_err(|e| format!("envio A: {e}"))?;
95
- let t_env_b = Instant::now();
96
- self.b.para_outro.send(msg_b).map_err(|e| format!("envio B: {e}"))?;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  // recebimentos com timeout
98
  let recebido_b = self.b.do_outro.recv_timeout(self.timeout);
99
  let recebido_a = self.a.do_outro.recv_timeout(self.timeout);
@@ -108,11 +142,14 @@ impl PonteDupla {
108
  s.tempo_total_ms = inicio.elapsed().as_secs_f64() * 1000.0;
109
  s.sobrecarga_media_us = (s.sobrecarga_media_us * (rodada - 1) as f64
110
  + inicio.elapsed().as_micros() as f64) / rodada as f64;
111
- // sobreposição das janelas de envio (prova de simultaneidade)
112
- let dur_a = t_env_a.elapsed();
113
- let sobre = dur_a.as_micros();
114
- if sobre > s.maior_sobreposicao_us {
115
- s.maior_sobreposicao_us = sobre;
 
 
 
116
  }
117
  Ok((ma, mb))
118
  }
 
24
  use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
25
  use std::sync::{Arc, Mutex};
26
  use std::time::{Duration, Instant};
 
27
  #[derive(Debug, Clone, Serialize, Deserialize)]
28
  pub struct Mensagem {
29
  pub rodada: u64,
 
52
  nome: String,
53
  para_outro: Sender<Mensagem>,
54
  do_outro: Receiver<Mensagem>,
55
+ ultima_janela: Arc<Mutex<Option<(Instant, Instant)>>>,
56
  }
57
 
58
  pub struct PonteDupla {
 
69
  let (tx_ab, rx_ab) = bounded::<Mensagem>(capacidade);
70
  let (tx_ba, rx_ba) = bounded::<Mensagem>(capacidade);
71
  PonteDupla {
72
+ a: Lado { nome: "A".into(), para_outro: tx_ab, do_outro: rx_ba, ultima_janela: Arc::new(Mutex::new(None)) },
73
+ b: Lado { nome: "B".into(), para_outro: tx_ba, do_outro: rx_ab, ultima_janela: Arc::new(Mutex::new(None)) },
74
  stats: Arc::new(Mutex::new(EstatisticasPonte::default())),
75
  ativa: Arc::new(AtomicBool::new(true)),
76
  capacidade,
 
80
 
81
  /// Uma RODADA de troca duplex: ambos os lados enviam e recebem com
82
  /// timeout — nenhuma espera ilimitada (Teo P.1 — sem deadlock).
83
+ ///
84
+ /// v0.2.0 (M13/M14): os ENVIOS usam `send_timeout` (a v0.1.11 usava
85
+ /// `send` bloqueante sem prazo — com o canal cheio o interpretador
86
+ /// inteiro congelava) e rodam em THREADS SEPARADAS — a métrica de
87
+ /// sobreposição mede a INTERSEÇÃO REAL das janelas de envio (a v0.1.11
88
+ /// media a duração do envio de A com os dois envios SEQUENCIAIS na
89
+ /// mesma thread — sobreposição estruturalmente nula).
90
  pub fn rodada(&self, carga_a: Vec<f32>, carga_b: Vec<f32>, nivel: usize) -> Result<(Mensagem, Mensagem), String> {
91
  let inicio = Instant::now();
92
  let rodada = {
 
95
  };
96
  let msg_a = Mensagem { rodada, origem: "A".into(), nivel, ts_us: inicio.elapsed().as_micros(), carga: carga_a };
97
  let msg_b = Mensagem { rodada, origem: "B".into(), nivel, ts_us: inicio.elapsed().as_micros(), carga: carga_b };
98
+ // M14: envios PARALELOS em threads separadas com prazo (M13)
99
+ let tx_a = self.a.para_outro.clone();
100
+ let tx_b = self.b.para_outro.clone();
101
+ let timeout = self.timeout;
102
+ let janela_a = self.a.ultima_janela.clone();
103
+ let janela_b = self.b.ultima_janela.clone();
104
+ let envio_a = std::thread::spawn(move || -> std::io::Result<(Instant, Instant)> {
105
+ let t0 = Instant::now();
106
+ tx_a.send_timeout(msg_a, timeout)
107
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{e}")))?;
108
+ Ok((t0, Instant::now()))
109
+ });
110
+ let envio_b = std::thread::spawn(move || -> std::io::Result<(Instant, Instant)> {
111
+ let t0 = Instant::now();
112
+ tx_b.send_timeout(msg_b, timeout)
113
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{e}")))?;
114
+ Ok((t0, Instant::now()))
115
+ });
116
+ let (ja, jb) = match (envio_a.join(), envio_b.join()) {
117
+ (Ok(Ok(a)), Ok(Ok(b))) => (a, b),
118
+ (Err(_), _) | (_, Err(_)) => return Err("join: pânico numa thread de envio".into()),
119
+ (Ok(Err(e)), _) | (_, Ok(Err(e))) => {
120
+ self.stats.lock().unwrap().quedas_por_timeout += 1;
121
+ return Err(format!("timeout de envio: {e}"));
122
+ }
123
+ };
124
+ // registra as janelas (M14)
125
+ if let Ok(mut j) = janela_a.lock() {
126
+ *j = Some(ja);
127
+ }
128
+ if let Ok(mut j) = janela_b.lock() {
129
+ *j = Some(jb);
130
+ }
131
  // recebimentos com timeout
132
  let recebido_b = self.b.do_outro.recv_timeout(self.timeout);
133
  let recebido_a = self.a.do_outro.recv_timeout(self.timeout);
 
142
  s.tempo_total_ms = inicio.elapsed().as_secs_f64() * 1000.0;
143
  s.sobrecarga_media_us = (s.sobrecarga_media_us * (rodada - 1) as f64
144
  + inicio.elapsed().as_micros() as f64) / rodada as f64;
145
+ // M14: INTERSEÇÃO REAL das janelas [ja.0, ja.1] ∩ [jb.0, jb.1]
146
+ let ini_max = ja.0.max(jb.0);
147
+ let fim_min = ja.1.min(jb.1);
148
+ if fim_min > ini_max {
149
+ let sobre = (fim_min - ini_max).as_micros();
150
+ if sobre > s.maior_sobreposicao_us {
151
+ s.maior_sobreposicao_us = sobre;
152
+ }
153
  }
154
  Ok((ma, mb))
155
  }
src/quantizador.rs CHANGED
@@ -57,17 +57,26 @@ impl CustomOp3 for GsqSuave {
57
  _s2: &CpuStorage, _l2: &Layout,
58
  s3: &CpuStorage, l3: &Layout,
59
  ) -> Result<(CpuStorage, Shape)> {
60
- if !(l1.is_contiguous() && _l2.is_contiguous() && l3.is_contiguous()) {
61
- return Err(candle_core::Error::Msg(
62
- "gsq_suave: tensors de entrada devem ser contíguos (chame .contiguous())".into(),
63
- ));
64
- }
65
- let z = s1.as_slice::<f32>()?;
 
 
 
 
 
 
 
 
 
 
 
66
  let (n, d) = (l1.shape().dims()[0], l1.shape().dims()[1]);
67
- let c = _s2.as_slice::<f32>()?;
68
  let (k, _dc) = (_l2.shape().dims()[0], _l2.shape().dims()[1]);
69
- let ruido = s3.as_slice::<f32>()?;
70
- debug_assert_eq!(ruido.len(), z.len() / d * k * d / d * k); // (n,k)
71
  // logits = z·Cᵀ/τ + g
72
  let mut logits = vec![0f32; n * k];
73
  for i in 0..n {
@@ -101,7 +110,7 @@ impl CustomOp3 for GsqSuave {
101
  ) -> Result<(Option<Tensor>, Option<Tensor>, Option<Tensor>)> {
102
  // recompute y (mesmo ruído — arg3): y = softmax(zCᵀ/τ + g)
103
  let logits = (&arg1.matmul(&arg2.transpose(0, 1)?)?.affine(1.0 / self.tau, 0.0)? + arg3)?;
104
- let y = candle_nn::ops::softmax_last_dim(&logits)?;
105
  // gy = R·(g_·Cᵀ)
106
  let gy = (&grad_res.matmul(&arg2.transpose(0, 1)?)? * self.r)?;
107
  // gl = y ⊙ (gy − Σ(y⊙gy)) — media como coluna p/ broadcast (n,1)
@@ -132,11 +141,26 @@ pub struct QuantizadorNivel {
132
  pub uso: Vec<f64>, // contagem de uso por código (telemetria/colapso)
133
  pub semente: u64, // RNG dedicado do nível (pareamento — Teo 31.7)
134
  chamadas: std::cell::Cell<u64>, // contador p/ ruído fresco por chamada
 
 
135
  }
136
 
137
  impl QuantizadorNivel {
138
  pub fn novo(k: usize, d: usize, modo: ModoQuant, ema: f64, escala_modo: &str, dev: &Device) -> Result<Self> {
 
 
 
 
 
 
 
139
  let w0 = stiefel::init_stiefel(k, d, dev)?;
 
 
 
 
 
 
140
  Ok(QuantizadorNivel {
141
  w: Var::from_tensor(&w0)?,
142
  r_escala: 1.0 / (k as f64).sqrt(),
@@ -147,16 +171,29 @@ impl QuantizadorNivel {
147
  iters_polar: 30,
148
  delta_ort_ultimo: 0.0,
149
  uso: vec![0.0; k],
150
- semente: 0xC0FFEE + k as u64 * 7919 + d as u64,
151
  chamadas: std::cell::Cell::new(0),
 
152
  })
153
  }
154
 
 
 
 
 
 
155
  /// Codebook C = R(Wᵀ)ᵀ com linhas ortonormais (diferenciável — Teo 26.3).
156
  /// SEMPRE contíguo (index_select/arquivo exigem layout padrão).
 
 
157
  pub fn codebook(&self) -> Result<Tensor> {
158
- let c = stiefel::codebook_stiefel(self.w.as_tensor(), self.iters_polar)?;
159
- c.contiguous()
 
 
 
 
 
160
  }
161
 
162
  /// δ_ort do codebook corrente (V9).
@@ -198,9 +235,13 @@ impl QuantizadorNivel {
198
  redimensionar_de_volta(&r, z)
199
  }
200
  ModoQuant::SteEma => {
201
- let r_duro = self.valores_duros_int(z, &self.codebook()?.contiguous()?)?.detach();
202
- // STE: z + (r_duro − z) com r_duro constante no grafo
203
- Ok(z.add(&r_duro.sub(z)?)?)
 
 
 
 
204
  }
205
  }
206
  }
@@ -233,6 +274,22 @@ impl QuantizadorNivel {
233
  Ok(linhas.affine(self.r_escala, 0.0)?)
234
  }
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  /// Atualiza R_ℓ por EMA da escala de projeção (Cor. A.1.1):
237
  /// "ema_projecao": R ← EMA(E[‖P_C z‖]); "otima_teo": R ← EMA(E[‖P_C z‖/√K]).
238
  pub fn atualizar_escala(&mut self, z: &Tensor) -> Result<()> {
@@ -349,12 +406,51 @@ mod testes {
349
  uso: vec![10.0, 10.0, 10.0, 10.0],
350
  semente: 7,
351
  chamadas: std::cell::Cell::new(0),
 
352
  };
353
  let p = q.perplexidade_uso();
354
  assert!((p - 4.0).abs() < 1e-9);
355
  q.uso = vec![40.0, 0.0, 0.0, 0.0];
356
  assert!(q.perplexidade_uso() < 1.01);
357
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  }
359
 
360
  #[cfg(test)]
 
57
  _s2: &CpuStorage, _l2: &Layout,
58
  s3: &CpuStorage, l3: &Layout,
59
  ) -> Result<(CpuStorage, Shape)> {
60
+ // CORREÇÃO (mesma classe do bug do ssd_scan): `as_slice` devolve o
61
+ // storage INTEIRO — usamos contiguous_offsets p/ respeitar offsets.
62
+ let (z1, z2) = l1.contiguous_offsets().ok_or_else(|| {
63
+ candle_core::Error::Msg("gsq_suave: z deve ser contíguo".into())
64
+ })?;
65
+ let (c1, c2) = _l2.contiguous_offsets().ok_or_else(|| {
66
+ candle_core::Error::Msg("gsq_suave: C deve ser contíguo".into())
67
+ })?;
68
+ let (r1, r2) = l3.contiguous_offsets().ok_or_else(|| {
69
+ candle_core::Error::Msg("gsq_suave: ruído deve ser contíguo".into())
70
+ })?;
71
+ let z_todos = s1.as_slice::<f32>()?;
72
+ let c_todos = _s2.as_slice::<f32>()?;
73
+ let ruido_todos = s3.as_slice::<f32>()?;
74
+ let z = &z_todos[z1..z2];
75
+ let c = &c_todos[c1..c2];
76
+ let ruido = &ruido_todos[r1..r2];
77
  let (n, d) = (l1.shape().dims()[0], l1.shape().dims()[1]);
 
78
  let (k, _dc) = (_l2.shape().dims()[0], _l2.shape().dims()[1]);
79
+ debug_assert_eq!(ruido.len(), n * k); // (n, k) — A30: assert correto
 
80
  // logits = z·Cᵀ/τ + g
81
  let mut logits = vec![0f32; n * k];
82
  for i in 0..n {
 
110
  ) -> Result<(Option<Tensor>, Option<Tensor>, Option<Tensor>)> {
111
  // recompute y (mesmo ruído — arg3): y = softmax(zCᵀ/τ + g)
112
  let logits = (&arg1.matmul(&arg2.transpose(0, 1)?)?.affine(1.0 / self.tau, 0.0)? + arg3)?;
113
+ let y = candle_nn::ops::softmax(&logits, D::Minus1)?;
114
  // gy = R·(g_·Cᵀ)
115
  let gy = (&grad_res.matmul(&arg2.transpose(0, 1)?)? * self.r)?;
116
  // gl = y ⊙ (gy − Σ(y⊙gy)) — media como coluna p/ broadcast (n,1)
 
141
  pub uso: Vec<f64>, // contagem de uso por código (telemetria/colapso)
142
  pub semente: u64, // RNG dedicado do nível (pareamento — Teo 31.7)
143
  chamadas: std::cell::Cell<u64>, // contador p/ ruído fresco por chamada
144
+ /// A43: cache do codebook por passo (invalidado por `invalidar_cache`).
145
+ cache_codebook: std::cell::RefCell<Option<Tensor>>,
146
  }
147
 
148
  impl QuantizadorNivel {
149
  pub fn novo(k: usize, d: usize, modo: ModoQuant, ema: f64, escala_modo: &str, dev: &Device) -> Result<Self> {
150
+ Self::novo_nivel(k, d, modo, ema, escala_modo, 0, dev)
151
+ }
152
+
153
+ /// `nivel` (0-based) entra na semente (M11: a v0.1.11 usava apenas
154
+ /// (k, d) — IGUAIS em todos os níveis — e o ruído de Gumbel era
155
+ /// byte-idêntico entre níveis, contradizendo o i.i.d. do Teo A.4).
156
+ pub fn novo_nivel(k: usize, d: usize, modo: ModoQuant, ema: f64, escala_modo: &str, nivel: usize, dev: &Device) -> Result<Self> {
157
  let w0 = stiefel::init_stiefel(k, d, dev)?;
158
+ // M11: semente inclui o índice do nível (mix de Fibonacci hashing)
159
+ let semente = 0xC0FFEE_u64
160
+ .wrapping_mul(0x9E3779B97F4A7C15)
161
+ .wrapping_add((nivel as u64).wrapping_mul(0x9E3779B97F4A7C15))
162
+ ^ (k as u64).wrapping_mul(7919)
163
+ ^ (d as u64).wrapping_mul(104729);
164
  Ok(QuantizadorNivel {
165
  w: Var::from_tensor(&w0)?,
166
  r_escala: 1.0 / (k as f64).sqrt(),
 
171
  iters_polar: 30,
172
  delta_ort_ultimo: 0.0,
173
  uso: vec![0.0; k],
174
+ semente,
175
  chamadas: std::cell::Cell::new(0),
176
+ cache_codebook: std::cell::RefCell::new(None),
177
  })
178
  }
179
 
180
+ /// A43: invalida o cache do codebook (chamar após cada passo do otimizador).
181
+ pub fn invalidar_cache(&self) {
182
+ *self.cache_codebook.borrow_mut() = None;
183
+ }
184
+
185
  /// Codebook C = R(Wᵀ)ᵀ com linhas ortonormais (diferenciável — Teo 26.3).
186
  /// SEMPRE contíguo (index_select/arquivo exigem layout padrão).
187
+ /// A43: memoizado dentro do passo (o codebook é recalculado ~5×/nível/passo
188
+ /// na v0.1.11 — ≈550 MMAC/passo redundantes).
189
  pub fn codebook(&self) -> Result<Tensor> {
190
+ if let Some(c) = self.cache_codebook.borrow().as_ref() {
191
+ return Ok(c.clone());
192
+ }
193
+ let c = stiefel::codebook_stiefel(self.w.as_tensor(), self.iters_polar)?
194
+ .contiguous()?;
195
+ *self.cache_codebook.borrow_mut() = Some(c.clone());
196
+ Ok(c)
197
  }
198
 
199
  /// δ_ort do codebook corrente (V9).
 
235
  redimensionar_de_volta(&r, z)
236
  }
237
  ModoQuant::SteEma => {
238
+ let c = self.codebook()?;
239
+ let r_duro = self.valores_duros_int(z, &c)?.detach();
240
+ // M7: STE CORRETO — z + sg[r_duro − z]: forward = r_duro,
241
+ // gradiente w.r.t. z = 1 (a v0.1.11 computava z + (r−z) com
242
+ // AMBOS no grafo ⇒ ∂/∂z = 1 − 1 = 0: encoder sem sinal).
243
+ let d = (&r_duro - z)?.detach();
244
+ Ok(z.add(&d)?)
245
  }
246
  }
247
  }
 
274
  Ok(linhas.affine(self.r_escala, 0.0)?)
275
  }
276
 
277
+ /// v0.3.0 — verificador algébrico soft: materializa DISTRIBUIÇÕES:
278
+ /// probs (…, K) → (…, d) com Σ_k q_k·R·c_k (diferenciável em q;
279
+ /// codebook congelado). Usado por L_alg (Teo G-1).
280
+ pub fn materializar_probs(&self, probs: &Tensor) -> Result<Tensor> {
281
+ let c = self.codebook()?.detach();
282
+ let k = c.dim(0)?;
283
+ let d = c.dim(1)?;
284
+ let forma: Vec<usize> = probs.dims().to_vec();
285
+ let plano = probs.reshape((probs.elem_count() / k, k))?;
286
+ let soft = plano.matmul(&c)?;
287
+ let mut alvo = forma;
288
+ alvo.pop();
289
+ alvo.push(d);
290
+ Ok(soft.affine(self.r_escala, 0.0)?.reshape(alvo)?)
291
+ }
292
+
293
  /// Atualiza R_ℓ por EMA da escala de projeção (Cor. A.1.1):
294
  /// "ema_projecao": R ← EMA(E[‖P_C z‖]); "otima_teo": R ← EMA(E[‖P_C z‖/√K]).
295
  pub fn atualizar_escala(&mut self, z: &Tensor) -> Result<()> {
 
406
  uso: vec![10.0, 10.0, 10.0, 10.0],
407
  semente: 7,
408
  chamadas: std::cell::Cell::new(0),
409
+ cache_codebook: std::cell::RefCell::new(None),
410
  };
411
  let p = q.perplexidade_uso();
412
  assert!((p - 4.0).abs() < 1e-9);
413
  q.uso = vec![40.0, 0.0, 0.0, 0.0];
414
  assert!(q.perplexidade_uso() < 1.01);
415
  }
416
+
417
+ /// M11: sementes de níveis distintos devem gerar ruídos distintos.
418
+ #[test]
419
+ fn sementes_por_nivel_distintas() -> Result<()> {
420
+ let dev = Device::Cpu;
421
+ let q0 = QuantizadorNivel::novo_nivel(64, 64, ModoQuant::GsqRco, 0.99, "ema_projecao", 0, &dev)?;
422
+ let q1 = QuantizadorNivel::novo_nivel(64, 64, ModoQuant::GsqRco, 0.99, "ema_projecao", 1, &dev)?;
423
+ assert_ne!(q0.semente, q1.semente, "M11: sementes devem diferir por nível");
424
+ let r0 = q0.ruido_gumbel(16, 64, &dev)?;
425
+ let r1 = q1.ruido_gumbel(16, 64, &dev)?;
426
+ let dif = (&r0 - &r1)?.abs()?.max_all()?.to_scalar::<f32>()?;
427
+ assert!(dif > 0.0, "ruído de Gumbel não pode ser idêntico entre níveis");
428
+ Ok(())
429
+ }
430
+
431
+ /// M7: o STE transmite gradiente unitário ao encoder.
432
+ #[test]
433
+ fn ste_gradiente_unitario() -> Result<()> {
434
+ let dev = Device::Cpu;
435
+ let q = QuantizadorNivel::novo_nivel(8, 6, ModoQuant::SteEma, 0.99, "ema_projecao", 0, &dev)?;
436
+ let z = crate::rng::randn_semeado(1.0, &[5, 6], &dev)?;
437
+ let zv = Var::from_tensor(&z)?;
438
+ let r = q.quantizar_treino(zv.as_tensor())?;
439
+ // forward: r deve ser igual ao r_duro
440
+ let (_ids, r_duro) = q.quantizar_duro(zv.as_tensor())?;
441
+ let dif = (&r - &r_duro)?.abs()?.max_all()?.to_scalar::<f32>()?;
442
+ assert!(dif < 1e-6, "STE forward = r_duro (dif {dif})");
443
+ // gradiente: perda = Σ w·r ⇒ ∂/∂z = via STE
444
+ let pesos = crate::rng::randn_semeado(1.0, &[5, 6], &dev)?;
445
+ let perda = (&r * &pesos)?.sum_all()?;
446
+ let gs = perda.backward()?;
447
+ let g = gs.get(&zv).unwrap().flatten_all()?.to_vec1::<f32>()?;
448
+ let pv = pesos.flatten_all()?.to_vec1::<f32>()?;
449
+ for i in 0..g.len() {
450
+ assert!((g[i] - pv[i]).abs() < 1e-5, "STE: grad deve ser dL/dr (got {} want {})", g[i], pv[i]);
451
+ }
452
+ Ok(())
453
+ }
454
  }
455
 
456
  #[cfg(test)]
src/rng.rs ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! RNG global semeado (correção A19 da auditoria r-08): `Tensor::randn` do
2
+ //! candle 0.11 usa `rand::rng()` (entropia do SO) ⇒ **nenhuma execução era
3
+ //! reproduzível**. Este módulo fornece `randn_semeado` — amostragem normal a
4
+ //! partir de um `StdRng` GLOBAL semeado uma vez por processo a partir de
5
+ //! `treino.semente` (Teo 31.7 — RNG dedicado e auditável).
6
+ //!
7
+ //! **Teo R-1 (reprodutibilidade bit-a-bit).** Fixada a semente `s` e a
8
+ //! sequência de chamadas de construção do modelo, o estado inicial de todos os
9
+ //! parâmetros é uma função determinística de `(s, ordem de construção)`.
10
+ //! *Prova:* `StdRng::seed_from_u64(s)` inicia um gerador PCG/BLS determinístico;
11
+ //! cada chamada de `randn_semeado` consome um número fixo de `f32`'s (shape
12
+ //! fixa por sítio de chamada) e o mutex serializa a ordem; a conversão
13
+ //! Box–Muller é pura ⇒ mesmas entradas ⇒ mesmos bits. Com a mesma `(s, ordem)`,
14
+ //! dois processos produzem estados iniciais idênticos bit-a-bit. ∎
15
+ //!
16
+ //! Uso: `inicializar_rng(semente)` UMA vez no início de cada treino/carregamento
17
+ //! (fase A, fase B, gates), depois `randn_semeado(shape, dev)` no lugar de
18
+ //! `Tensor::randn`.
19
+
20
+ use candle_core::{Device, Result, Tensor};
21
+ use rand::prelude::*;
22
+ use std::sync::Mutex;
23
+ use std::sync::OnceLock;
24
+
25
+ static RNG_GLOBAL: OnceLock<Mutex<StdRng>> = OnceLock::new();
26
+
27
+ /// Semente fixa para módulos auxiliares (SOM) — determinística.
28
+ pub const SEMENTE_SOM: u64 = 0x50C0FFEE;
29
+
30
+ /// Semente padrão da inicialização lazy (construções fora do treino).
31
+ pub const SEMENTE_PADRAO: u64 = 2718281828;
32
+
33
+ /// Inicializa (ou reinicializa) o RNG global com a semente dada.
34
+ pub fn inicializar(semente: u64) {
35
+ // OnceLock não permite re-init; usamos get_or_init e trocamos o estado
36
+ // interno por re-seed quando já existe.
37
+ if let Some(m) = RNG_GLOBAL.get() {
38
+ if let Ok(mut g) = m.lock() {
39
+ *g = StdRng::seed_from_u64(semente);
40
+ }
41
+ return;
42
+ }
43
+ let _ = RNG_GLOBAL.set(Mutex::new(StdRng::seed_from_u64(semente)));
44
+ }
45
+
46
+ /// Amostra normal(0, desvio) com forma `shape` a partir do RNG global.
47
+ /// Equivalente bit-a-bit determinístico de `Tensor::randn`.
48
+ /// Se `inicializar` ainda não foi chamado, semeia com a semente padrão
49
+ /// (à prova de pânico — construções fora do treino são determinísticas).
50
+ pub fn randn_semeado(desvio: f32, shape: &[usize], dev: &Device) -> Result<Tensor> {
51
+ let n: usize = shape.iter().product();
52
+ if RNG_GLOBAL.get().is_none() {
53
+ // lazy-init com semente padrão (determinístico, sem pânico)
54
+ let _ = RNG_GLOBAL.set(Mutex::new(StdRng::seed_from_u64(SEMENTE_PADRAO)));
55
+ }
56
+ let m = RNG_GLOBAL
57
+ .get()
58
+ .expect("rng global (inicialização lazy acima garante)");
59
+ let mut guard = m.lock().map_err(|_| {
60
+ candle_core::Error::Msg("rng global envenenado".into())
61
+ })?;
62
+ let mut v = Vec::with_capacity(n);
63
+ // Box–Muller em pares; determinístico e independente de implementação do
64
+ // gerador de normais da rand (compatível entre versões).
65
+ let mut i = 0usize;
66
+ while i < n {
67
+ let u1: f32 = guard.gen::<f32>().clamp(1e-9, 1.0);
68
+ let u2: f32 = guard.gen::<f32>().clamp(1e-9, 1.0);
69
+ let r = (-2.0 * u1.ln()).sqrt();
70
+ let t = 2.0 * std::f32::consts::PI * u2;
71
+ v.push(r * t.cos() * desvio);
72
+ i += 1;
73
+ if i < n {
74
+ v.push(r * t.sin() * desvio);
75
+ i += 1;
76
+ }
77
+ }
78
+ Tensor::from_vec(v, shape, dev)
79
+ }
80
+
81
+ #[cfg(test)]
82
+ mod testes {
83
+ use super::*;
84
+
85
+ #[test]
86
+ fn reprodutibilidade_bit_a_bit() -> Result<()> {
87
+ let dev = Device::Cpu;
88
+ inicializar(12345);
89
+ let a = randn_semeado(0.02, &[7, 11], &dev)?;
90
+ inicializar(12345);
91
+ let b = randn_semeado(0.02, &[7, 11], &dev)?;
92
+ let dif = (&a - &b)?.abs()?.max_all()?.to_scalar::<f32>()?;
93
+ assert_eq!(dif, 0.0, "mesma semente deve dar init idêntico bit-a-bit");
94
+ // sementes diferentes devem diferir
95
+ inicializar(999);
96
+ let c = randn_semeado(0.02, &[7, 11], &dev)?;
97
+ let dif2 = (&a - &c)?.abs()?.max_all()?.to_scalar::<f32>()?;
98
+ assert!(dif2 > 0.0);
99
+ Ok(())
100
+ }
101
+ }
src/ssd.rs CHANGED
@@ -1,69 +1,172 @@
1
  //! Slot Mamba-3 (1-de-6 do tronco 5:1) — scan recorrente diagonal seletivo
2
- //! (SSD) com adjunto EXATO via CustomOp2. Compatível com o Teo D.1(i):
3
  //! fluxo estritamente esquerda→direita (a camada é causal por construção);
4
  //! a bidirecionalidade intra-nível vem das camadas ADBA (H_D1 — r-04 §3).
5
  //!
6
- //! Recorrência: h_t = a_t ⊙ h_{t−1} + (1 − a_t) ⊙ x_t; y_t = h_t
7
- //! com a_t = σ(W_a·x_t + b_a) ∈ (0,1)^d (portas por canal).
8
  //!
9
- //! Adjunto (exato): dh_t = dy_t + a_{t+1} ⊙ dh_{t+1};
10
- //! dx_t = (1 − a_t) ⊙ dh_t; da_t = ⟨dh_t, h_{t−1} − x_t⟩ (escalar por canal).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- use candle_core::{CpuStorage, CustomOp2, Device, Layout, Result, Shape, Tensor, Var};
13
  use crate::camadas::{Linear, RmsNorm, Swiglu};
14
 
15
- pub struct ScanDiagonal;
16
-
17
- impl CustomOp2 for ScanDiagonal {
18
- fn name(&self) -> &'static str { "ssd_scan_diagonal" }
 
 
19
 
20
- fn cpu_fwd(&self, s1: &CpuStorage, l1: &Layout, s2: &CpuStorage, l2: &Layout) -> Result<(CpuStorage, Shape)> {
21
- if !(l1.is_contiguous() && l2.is_contiguous()) {
22
- return Err(candle_core::Error::Msg(
23
- "ssd_scan: entradas devem ser contíguas (chame .contiguous())".into(),
24
- ));
25
- }
26
- let x = s1.as_slice::<f32>()?; // (n, d)
27
- let a = s2.as_slice::<f32>()?; // (n, d) em (0,1)
28
- let (n, d) = (l1.shape().dims()[0], l1.shape().dims()[1]);
29
- let mut y = vec![0f32; n * d];
 
 
 
 
 
 
 
30
  for j in 0..d {
31
- let mut h = 0f32;
32
- for i in 0..n {
33
- let ai = a[i * d + j];
34
- h = ai * h + (1.0 - ai) * x[i * d + j];
35
- y[i * d + j] = h;
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
- Ok((CpuStorage::F32(y), (n, d).into()))
39
  }
40
 
41
- fn bwd(&self, arg1: &Tensor, arg2: &Tensor, _res: &Tensor, grad_res: &Tensor) -> Result<(Option<Tensor>, Option<Tensor>)> {
42
- // recomputa h (forward sem gradiente) e percorre reverso
43
- let (x, a) = (arg1, arg2);
44
- let h = Tensor::apply_op2(x, a, ScanDiagonal)?.detach();
45
- let x_v = x.flatten_all()?.to_vec1::<f32>()?;
46
- let a_v = a.flatten_all()?.to_vec1::<f32>()?;
 
 
 
 
 
47
  let h_v = h.flatten_all()?.to_vec1::<f32>()?;
48
  let dy_v = grad_res.flatten_all()?.to_vec1::<f32>()?;
49
- let (n, d) = (x.dim(0)?, x.dim(1)?);
50
- let mut dx = vec![0f32; n * d];
51
- let mut da = vec![0f32; n * d];
52
- let mut dh = vec![0f32; d];
53
- for i in (0..n).rev() {
54
- for j in 0..d {
55
- let idx = i * d + j;
56
- let g = dy_v[idx] + dh[j];
57
- dx[idx] = (1.0 - a_v[idx]) * g;
58
- let h_ant = if i > 0 { h_v[(i - 1) * d + j] } else { 0.0 };
59
- da[idx] = g * (h_ant - x_v[idx]);
60
- dh[j] = a_v[idx] * g;
 
 
 
 
 
 
 
 
 
 
 
61
  }
 
 
62
  }
63
- let dev = x.device();
64
- let gx = Tensor::from_vec(dx, (n, d), dev)?;
65
- let ga = Tensor::from_vec(da, (n, d), dev)?;
66
- Ok((Some(gx), Some(ga)))
 
67
  }
68
  }
69
 
@@ -75,11 +178,24 @@ pub struct BlocoSsd {
75
  pub proj_o: Linear, // d → d
76
  pub norm2: RmsNorm,
77
  pub ffn: Swiglu,
 
 
78
  }
79
 
80
  impl BlocoSsd {
81
- pub fn novo(d: usize, d_ff: usize, dev: &Device) -> Result<Self> {
82
  let desvio = 1.0 / ((d * 2) as f64).sqrt();
 
 
 
 
 
 
 
 
 
 
 
83
  Ok(BlocoSsd {
84
  norm1: RmsNorm::novo(d, 1e-5, dev)?,
85
  porta_a: Linear::novo(d, d, desvio, dev)?,
@@ -87,33 +203,43 @@ impl BlocoSsd {
87
  proj_o: Linear::sem_vies(d, d, desvio, dev)?,
88
  norm2: RmsNorm::novo(d, 1e-5, dev)?,
89
  ffn: Swiglu::novo(d, d_ff, dev)?,
 
90
  })
91
  }
92
 
93
- /// x (b, n, d) → (b, n, d). O scan é aplicado por elemento do lote.
94
- /// `cache_estado`: estado h anterior (b, d) para streaming incremental.
95
  pub fn fwd(&self, x: &Tensor, cache_estado: &mut Option<Tensor>) -> Result<Tensor> {
96
  let b = x.dim(0)?;
97
  let n = x.dim(1)?;
98
  let d = x.dim(2)?;
 
99
  let xb = self.norm1.fwd(x)?;
100
  let a = candle_nn::ops::sigmoid(&self.porta_a.fwd(&xb)?)?;
 
 
 
 
101
  let xp = self.proj_x.fwd(&xb)?;
102
- // achata (b·n, d), aplica o scan por lote e recompõe
103
- let xf = xp.reshape((b * n, d))?.contiguous()?;
104
  let af = a.reshape((b * n, d))?.contiguous()?;
105
- // pré-estado: se houver cache, subtrai efeito — simplificação: scan
106
- // independente por segmento com estado inicial zero (documentado; o
107
- // estado cruzado entre segmentos é ponto de extensão do streaming).
108
- let _ = cache_estado;
109
- let mut saidas = Vec::with_capacity(b);
110
- for i in 0..b {
111
- let xi = xf.narrow(0, i * n, n)?;
112
- let ai = af.narrow(0, i * n, n)?;
113
- let yi = Tensor::apply_op2(&xi, &ai, ScanDiagonal)?;
114
- saidas.push(yi);
 
 
 
115
  }
116
- let y = Tensor::cat(&saidas, 0)?.reshape((b, n, d))?;
 
 
117
  let y = self.proj_o.fwd(&y)?;
118
  let x2 = (x + &y)?;
119
  let f = self.ffn.fwd(&self.norm2.fwd(&x2)?)?;
@@ -121,10 +247,14 @@ impl BlocoSsd {
121
  }
122
 
123
  pub fn vars(&self) -> Vec<Var> {
124
- [
125
  self.norm1.vars(), self.porta_a.vars(), self.proj_x.vars(),
126
  self.proj_o.vars(), self.norm2.vars(), self.ffn.vars(),
127
- ].concat()
 
 
 
 
128
  }
129
  }
130
 
@@ -134,50 +264,158 @@ mod testes {
134
 
135
  #[test]
136
  fn adjunto_do_scan_vs_finitas_diferencas() -> Result<()> {
 
137
  let dev = Device::Cpu;
138
- let n = 9;
139
- let d = 5;
140
- let xv = Var::from_tensor(&Tensor::randn(0f32, 1f32, (n, d), &dev)?)?;
141
- let av = Var::from_tensor(&candle_nn::ops::sigmoid(&Tensor::randn(0f32, 1f32, (n, d), &dev)?)?)?;
142
- let pesos = Tensor::randn(0f32, 1f32, (n, d), &dev)?;
143
- let y = Tensor::apply_op2(xv.as_tensor(), &av.as_tensor(), ScanDiagonal)?;
 
 
 
 
 
 
144
  let perda = (&y * &pesos)?.sum_all()?;
145
  let gs = perda.backward()?;
146
- let gx = gs.get(&xv).ok_or_else(|| candle_core::Error::Msg("sem grad x".into()))?;
147
- let ga = gs.get(&av).ok_or_else(|| candle_core::Error::Msg("sem grad a".into()))?;
148
- let gxv = gx.flatten_all()?.to_vec1::<f32>()?;
149
- let gav = ga.flatten_all()?.to_vec1::<f32>()?;
150
- // FD
151
  let eps = 1e-3f32;
152
- let f = |x: &Tensor, a: &Tensor| -> Result<f64> {
153
- let y = Tensor::apply_op2(x, a, ScanDiagonal)?;
 
154
  Ok((&y * &pesos)?.sum_all()?.to_scalar::<f32>()? as f64)
155
  };
156
- let x0 = xv.as_tensor();
157
- let a0 = av.as_tensor();
158
- let x_plano = x0.flatten_all()?.to_vec1::<f32>()?;
159
- let a_plano = a0.flatten_all()?.to_vec1::<f32>()?;
160
  let mut erro = 0f64;
161
- for idx in [3usize, 11, 17, 24, 30] {
162
- let mut xp = x_plano.clone();
163
- let mut xm = x_plano.clone();
164
- xp[idx] += eps;
165
- xm[idx] -= eps;
166
- let tp = Tensor::from_vec(xp, (n, d), &dev)?;
167
- let tm = Tensor::from_vec(xm, (n, d), &dev)?;
168
- let fd = (f(&tp, a0)? - f(&tm, a0)?) / (2.0 * eps as f64);
169
- erro = erro.max((fd - gxv[idx] as f64).abs());
170
- // porta a
171
- let mut ap = a_plano.clone();
172
- let mut am = a_plano.clone();
173
- ap[idx] += eps;
174
- am[idx] -= eps;
175
- let tap = Tensor::from_vec(ap, (n, d), &dev)?;
176
- let tam = Tensor::from_vec(am, (n, d), &dev)?;
177
- let fda = (f(x0, &tap)? - f(x0, &tam)?) / (2.0 * eps as f64);
178
- erro = erro.max((fda - gav[idx] as f64).abs());
 
179
  }
180
- assert!(erro < 5e-3, "erro adjunto SSD = {erro}");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  Ok(())
182
  }
183
  }
 
1
  //! Slot Mamba-3 (1-de-6 do tronco 5:1) — scan recorrente diagonal seletivo
2
+ //! (SSD) com adjunto EXATO via CustomOp3. Compatível com o Teo D.1(i):
3
  //! fluxo estritamente esquerda→direita (a camada é causal por construção);
4
  //! a bidirecionalidade intra-nível vem das camadas ADBA (H_D1 — r-04 §3).
5
  //!
6
+ //! ## v0.3.0 — SSD-Δ (segunda ordem aditiva): percepção de inteiros e floats
 
7
  //!
8
+ //! Recorrência aumentada (canal j, tempo t):
9
+ //! h_t = a_t ⊙ h_{t−1} + (1 − a_t + η_t) ⊙ x_t ; y_t = h_t
10
+ //! a_t = σ(W_a·x̂_t + b_a) ∈ (0,1)^d (porta de memória — como na v0.1.11)
11
+ //! η_t = tanh(W_η·x̂_t + b_η) ∈ (−1,1)^d (NOVA porta aditiva de 2ª ordem)
12
+ //!
13
+ //! **Teo S-1 (capacidade de acumulação — a porta η compra aritmética).**
14
+ //! (i) Com `a_t ≡ 1, η_t ≡ 1`: `h_t = h_{t−1} + x_t` — o slot computa a SOMA
15
+ //! ACUMULADA. Para entradas inteiras com `|Σ_{s≤t} x_s| < 2^24`, a aritmética
16
+ //! f32 é EXATA (inteiros ≤ 2^24 são representáveis e a soma não arredonda)
17
+ //! ⇒ o slot PERCEBE progressões aritméticas de inteiros bit-a-bit.
18
+ //! (ii) Com `η_t = tanh(ρ)` constante: o erro de acumulação é
19
+ //! `|h_t − Σ x_s| ≤ 2e^{−2ρ} · Σ_{s≤t} |x_s|` (pois `1−tanh(ρ) = 2e^{−2ρ}(1+o(1))`)
20
+ //! — converge EXPONENCIALMENTE em ρ.
21
+ //! (iii) Para entradas reais (ponto flutuante): o erro de arredondamento da
22
+ //! soma seqüencial é limitado por `(t−1)·ε_mach·Σ|x_s|`, ε_mach = 2^−24 em f32 —
23
+ //! a cota clássica da soma seqüencial, que o slot herda.
24
+ //! (iv) **Contenção estrita**: a família antiga `h_t = a_t⊙h_{t−1} + (1−a_t)⊙x_t`
25
+ //! NÃO contém o mapa soma: desenvolvendo, `h_t = Σ_s (1−a_s)x_s Π_{r>s} a_r`;
26
+ //! exigir `h_t = Σ_s x_s` para todo t força `(1−a_t) = 1` (a_t = 0) e, no mesmo
27
+ //! passo, `(1−a_{t−1})·a_t = 1` — contradição (0 = 1). Logo SSD-Δ ⊋ SSD.
28
+ //! *Prova de (iv):* para t = 2, h_2 = x_2 + x_1 exige (1−a_2) = 1 ⇒ a_2 = 0 e
29
+ //! (1−a_1)·a_2 = 1 ⇒ 0 = 1. Absurdo. ∎
30
+ //! (v) **Nascimento neutro**: com `W_η = 0, b_η = 0` ⇒ η ≡ 0 ⇒ a recorrência é
31
+ //! EXATAMENTE a da v0.1.11 (bit-a-bit: `1−a+0 = 1−a` em f32). O ganho é opt-in.
32
+ //!
33
+ //! ## v0.2.0 — estado transportado (M4)
34
+ //! `cache_estado` (b, d) é USADO como h_0 e atualizado com h_n no fim — o slot
35
+ //! volta a ser recorrente entre níveis (Teo D.1(i)). O adjunto inclui ∂L/∂h_0.
36
+ //!
37
+ //! Adjunto (exato; g_t := gradiente acumulado em h_t):
38
+ //! ∂L/∂x_t = (1 − a_t + η_t) ⊙ g_t
39
+ //! ∂L/∂a_t = g_t ⊙ (h_{t−1} − x_t)
40
+ //! ∂L/∂η_t = g_t ⊙ x_t
41
+ //! g_{t−1} = a_t ⊙ g_t ; ∂L/∂h_0 = g_0 (acumulado no fim do backward)
42
 
43
+ use candle_core::{CpuStorage, CustomOp3, Device, Layout, Result, Shape, Tensor, Var};
44
  use crate::camadas::{Linear, RmsNorm, Swiglu};
45
 
46
+ /// CustomOp3: (gates (b·n, 2d) = [a | η], x (b·n, d), h0 (b, d)) → y (b·n, d).
47
+ /// `b` e `n` são carregados no struct (inferidos pelo chamador).
48
+ pub struct ScanDiagonal {
49
+ pub b: usize,
50
+ pub n: usize,
51
+ }
52
 
53
+ fn scan_fwd(
54
+ gates: &[f32],
55
+ x: &[f32],
56
+ h0: &[f32],
57
+ y: &mut [f32],
58
+ bi: usize,
59
+ b: usize,
60
+ n: usize,
61
+ d: usize,
62
+ ) {
63
+ let dois_d = 2 * d;
64
+ let base = bi * n;
65
+ let mut h = vec![0f32; d];
66
+ h.copy_from_slice(&h0[bi * d..(bi + 1) * d]);
67
+ for i in 0..n {
68
+ let row = (base + i) * d;
69
+ let grow = (base + i) * dois_d;
70
  for j in 0..d {
71
+ let a = gates[grow + j];
72
+ let eta = gates[grow + d + j];
73
+ h[j] = a * h[j] + (1.0 - a + eta) * x[row + j];
74
+ y[i * d + j] = h[j];
75
+ }
76
+ }
77
+ }
78
+
79
+ impl CustomOp3 for ScanDiagonal {
80
+ fn name(&self) -> &'static str { "ssd_scan_delta" }
81
+
82
+ fn cpu_fwd(
83
+ &self,
84
+ s1: &CpuStorage, l1: &Layout,
85
+ s2: &CpuStorage, l2: &Layout,
86
+ s3: &CpuStorage, l3: &Layout,
87
+ ) -> Result<(CpuStorage, Shape)> {
88
+ // CORREÇÃO CRÍTICA (descoberta pelo gate G3): `as_slice` devolve o
89
+ // storage INTEIRO — layouts contíguos COM OFFSET (ex.: narrow de um
90
+ // tensor maior) começam em `start_offset() != 0`. Usamos
91
+ // `contiguous_offsets()` (o padrão dos custom ops do candle).
92
+ let (g1, g2) = l1.contiguous_offsets().ok_or_else(|| {
93
+ candle_core::Error::Msg("ssd_scan: gates deve ser contíguo".into())
94
+ })?;
95
+ let (x1, x2) = l2.contiguous_offsets().ok_or_else(|| {
96
+ candle_core::Error::Msg("ssd_scan: x deve ser contíguo".into())
97
+ })?;
98
+ let (h1, h2) = l3.contiguous_offsets().ok_or_else(|| {
99
+ candle_core::Error::Msg("ssd_scan: h0 deve ser contíguo".into())
100
+ })?;
101
+ let gates_todos = s1.as_slice::<f32>()?; // (b*n, 2d)
102
+ let x_todos = s2.as_slice::<f32>()?; // (b*n, d)
103
+ let h0_todos = s3.as_slice::<f32>()?; // (b, d)
104
+ let gates = &gates_todos[g1..g2];
105
+ let x = &x_todos[x1..x2];
106
+ let h0 = &h0_todos[h1..h2];
107
+ let (bn, dois_d) = (l1.shape().dims()[0], l1.shape().dims()[1]);
108
+ let d = dois_d / 2;
109
+ let (b, n) = (self.b, self.n);
110
+ debug_assert_eq!(bn, b * n);
111
+ debug_assert_eq!(l2.shape().dims()[0], bn);
112
+ debug_assert_eq!(l2.shape().dims()[1], d);
113
+ debug_assert_eq!(l3.shape().dims()[0], b);
114
+ let mut y = vec![0f32; bn * d];
115
+ // M17/A31: paralelo sobre o lote (rayon), canal-interno contíguo.
116
+ {
117
+ use rayon::prelude::*;
118
+ y.par_chunks_mut(n * d).enumerate().for_each(|(bi, fatia)| {
119
+ scan_fwd(gates, x, h0, fatia, bi, b, n, d);
120
+ });
121
  }
122
+ Ok((CpuStorage::F32(y), (bn, d).into()))
123
  }
124
 
125
+ fn bwd(
126
+ &self,
127
+ arg1: &Tensor, arg2: &Tensor, arg3: &Tensor,
128
+ _res: &Tensor, grad_res: &Tensor,
129
+ ) -> Result<(Option<Tensor>, Option<Tensor>, Option<Tensor>)> {
130
+ let (gates_t, x_t, h0_t) = (arg1, arg2, arg3);
131
+ // recompute h (forward sem gradiente)
132
+ let h = Tensor::apply_op3(gates_t, x_t, h0_t, ScanDiagonal { b: self.b, n: self.n })?.detach();
133
+ let g_v = gates_t.flatten_all()?.to_vec1::<f32>()?;
134
+ let x_v = x_t.flatten_all()?.to_vec1::<f32>()?;
135
+ let h0_v = h0_t.flatten_all()?.to_vec1::<f32>()?;
136
  let h_v = h.flatten_all()?.to_vec1::<f32>()?;
137
  let dy_v = grad_res.flatten_all()?.to_vec1::<f32>()?;
138
+ let (b, n) = (self.b, self.n);
139
+ let bn = b * n;
140
+ let dois_d = gates_t.dim(1)?;
141
+ let d = dois_d / 2;
142
+ let mut dg = vec![0f32; bn * dois_d];
143
+ let mut dx = vec![0f32; bn * d];
144
+ let mut dh0 = vec![0f32; b * d];
145
+ for bi in 0..b {
146
+ let base = bi * n;
147
+ let mut gh = vec![0f32; d];
148
+ for i in (0..n).rev() {
149
+ let row = (base + i) * d;
150
+ let grow = (base + i) * dois_d;
151
+ for j in 0..d {
152
+ let gg = dy_v[row + j] + gh[j];
153
+ let a = g_v[grow + j];
154
+ let eta = g_v[grow + d + j];
155
+ dx[row + j] = (1.0 - a + eta) * gg;
156
+ let h_ant = if i > 0 { h_v[(row - d) + j] } else { h0_v[bi * d + j] };
157
+ dg[grow + j] = gg * (h_ant - x_v[row + j]);
158
+ dg[grow + d + j] = gg * x_v[row + j];
159
+ gh[j] = a * gg;
160
+ }
161
  }
162
+ // ∂L/∂h_0[bi] = gh após o tempo 0
163
+ dh0[bi * d..(bi + 1) * d].copy_from_slice(&gh);
164
  }
165
+ let dev = x_t.device();
166
+ let gg = Tensor::from_vec(dg, (bn, dois_d), dev)?;
167
+ let gx = Tensor::from_vec(dx, (bn, d), dev)?;
168
+ let gh0 = Tensor::from_vec(dh0, (b, d), dev)?;
169
+ Ok((Some(gg), Some(gx), Some(gh0)))
170
  }
171
  }
172
 
 
178
  pub proj_o: Linear, // d → d
179
  pub norm2: RmsNorm,
180
  pub ffn: Swiglu,
181
+ /// Porta aditiva de 2ª ordem (SSD-Δ): None = neutro (η ≡ 0, v0.1.11).
182
+ pub porta_eta: Option<Linear>,
183
  }
184
 
185
  impl BlocoSsd {
186
+ pub fn novo(d: usize, d_ff: usize, segunda_ordem: bool, dev: &Device) -> Result<Self> {
187
  let desvio = 1.0 / ((d * 2) as f64).sqrt();
188
+ // Nascimento neutro: W_η = 0 e b_η = 0 ⇒ η = tanh(0) = 0.
189
+ let porta_eta = if segunda_ordem {
190
+ let mut l = Linear::novo(d, d, 0.0, dev)?;
191
+ if let Some(b) = &mut l.b {
192
+ let z = Tensor::zeros((d,), candle_core::DType::F32, dev)?;
193
+ b.set(&z)?;
194
+ }
195
+ Some(l)
196
+ } else {
197
+ None
198
+ };
199
  Ok(BlocoSsd {
200
  norm1: RmsNorm::novo(d, 1e-5, dev)?,
201
  porta_a: Linear::novo(d, d, desvio, dev)?,
 
203
  proj_o: Linear::sem_vies(d, d, desvio, dev)?,
204
  norm2: RmsNorm::novo(d, 1e-5, dev)?,
205
  ffn: Swiglu::novo(d, d_ff, dev)?,
206
+ porta_eta,
207
  })
208
  }
209
 
210
+ /// x (b, n, d) → (b, n, d). Scan com ESTADO INICIAL h_0 (M4) e estado final
211
+ /// devolvido em `cache_estado` (b, d) para o próximo segmento/nível.
212
  pub fn fwd(&self, x: &Tensor, cache_estado: &mut Option<Tensor>) -> Result<Tensor> {
213
  let b = x.dim(0)?;
214
  let n = x.dim(1)?;
215
  let d = x.dim(2)?;
216
+ let dev = x.device();
217
  let xb = self.norm1.fwd(x)?;
218
  let a = candle_nn::ops::sigmoid(&self.porta_a.fwd(&xb)?)?;
219
+ let eta = match &self.porta_eta {
220
+ Some(pe) => pe.fwd(&xb)?.tanh()?,
221
+ None => Tensor::zeros((b, n, d), candle_core::DType::F32, dev)?,
222
+ };
223
  let xp = self.proj_x.fwd(&xb)?;
224
+ // gates = [a | η] (b·n, 2d) — contíguo
 
225
  let af = a.reshape((b * n, d))?.contiguous()?;
226
+ let ef = eta.reshape((b * n, d))?.contiguous()?;
227
+ let gates = Tensor::cat(&[&af, &ef], 1)?.contiguous()?;
228
+ let xf = xp.reshape((b * n, d))?.contiguous()?;
229
+ // h_0: do cache (M4) ou zeros
230
+ let h0 = match cache_estado {
231
+ Some(h) => h.reshape((b, d))?.contiguous()?,
232
+ None => Tensor::zeros((b, d), candle_core::DType::F32, dev)?,
233
+ };
234
+ let y = Tensor::apply_op3(&gates, &xf, &h0, ScanDiagonal { b, n })?;
235
+ // estado final h_n por elemento do lote: linhas (bi*n + n−1)
236
+ let mut estados = Vec::with_capacity(b);
237
+ for bi in 0..b {
238
+ estados.push(y.narrow(0, bi * n + n - 1, 1)?.reshape((1, d))?);
239
  }
240
+ let hn = Tensor::cat(&estados, 0)?.detach();
241
+ *cache_estado = Some(hn);
242
+ let y = y.reshape((b, n, d))?;
243
  let y = self.proj_o.fwd(&y)?;
244
  let x2 = (x + &y)?;
245
  let f = self.ffn.fwd(&self.norm2.fwd(&x2)?)?;
 
247
  }
248
 
249
  pub fn vars(&self) -> Vec<Var> {
250
+ let mut v: Vec<Var> = [
251
  self.norm1.vars(), self.porta_a.vars(), self.proj_x.vars(),
252
  self.proj_o.vars(), self.norm2.vars(), self.ffn.vars(),
253
+ ].concat();
254
+ if let Some(pe) = &self.porta_eta {
255
+ v.extend(pe.vars());
256
+ }
257
+ v
258
  }
259
  }
260
 
 
264
 
265
  #[test]
266
  fn adjunto_do_scan_vs_finitas_diferencas() -> Result<()> {
267
+ crate::rng::inicializar(20260101);
268
  let dev = Device::Cpu;
269
+ let (b, n, d) = (2usize, 9, 5);
270
+ let x0 = crate::rng::randn_semeado(1.0, &[b * n, d], &dev)?;
271
+ let h00 = crate::rng::randn_semeado(1.0, &[b, d], &dev)?;
272
+ // gates em (0,1)/(−1,1) para o teste
273
+ let a = candle_nn::ops::sigmoid(&crate::rng::randn_semeado(1.0, &[b * n, d], &dev)?)?;
274
+ let e = crate::rng::randn_semeado(1.0, &[b * n, d], &dev)?.tanh()?;
275
+ let pesos = crate::rng::randn_semeado(1.0, &[b * n, d], &dev)?;
276
+ let gv = Var::from_tensor(&a)?;
277
+ let ev = Var::from_tensor(&e)?;
278
+ let hv = Var::from_tensor(&h00)?;
279
+ let gates = Tensor::cat(&[gv.as_tensor(), ev.as_tensor()], 1)?.contiguous()?;
280
+ let y = Tensor::apply_op3(&gates, &x0, hv.as_tensor(), ScanDiagonal { b, n })?;
281
  let perda = (&y * &pesos)?.sum_all()?;
282
  let gs = perda.backward()?;
283
+ let ga = gs.get(&gv).unwrap().flatten_all()?.to_vec1::<f32>()?;
284
+ let ge = gs.get(&ev).unwrap().flatten_all()?.to_vec1::<f32>()?;
285
+ let gh0 = gs.get(&hv).unwrap().flatten_all()?.to_vec1::<f32>()?;
286
+ // FD central em a, eta e h0
 
287
  let eps = 1e-3f32;
288
+ let f = |a: &Tensor, e: &Tensor, h0: &Tensor| -> Result<f64> {
289
+ let gates = Tensor::cat(&[a, e], 1)?.contiguous()?;
290
+ let y = Tensor::apply_op3(&gates, &x0, h0, ScanDiagonal { b, n })?;
291
  Ok((&y * &pesos)?.sum_all()?.to_scalar::<f32>()? as f64)
292
  };
293
+ let a_plano = a.flatten_all()?.to_vec1::<f32>()?;
294
+ let e_plano = e.flatten_all()?.to_vec1::<f32>()?;
295
+ let h_plano = h00.flatten_all()?.to_vec1::<f32>()?;
 
296
  let mut erro = 0f64;
297
+ for idx in [3usize, 11, 17, 24, 30, 45] {
298
+ if idx < a_plano.len() {
299
+ let mut ap = a_plano.clone();
300
+ let mut am = a_plano.clone();
301
+ ap[idx] += eps;
302
+ am[idx] -= eps;
303
+ let tp = Tensor::from_vec(ap, (b * n, d), &dev)?;
304
+ let tm = Tensor::from_vec(am, (b * n, d), &dev)?;
305
+ let fd = (f(&tp, &e, &h00)? - f(&tm, &e, &h00)?) / (2.0 * eps as f64);
306
+ erro = erro.max((fd - ga[idx] as f64).abs());
307
+ let mut ep = e_plano.clone();
308
+ let mut em = e_plano.clone();
309
+ ep[idx] += eps;
310
+ em[idx] -= eps;
311
+ let tp = Tensor::from_vec(ep, (b * n, d), &dev)?;
312
+ let tm = Tensor::from_vec(em, (b * n, d), &dev)?;
313
+ let fd = (f(&a, &tp, &h00)? - f(&a, &tm, &h00)?) / (2.0 * eps as f64);
314
+ erro = erro.max((fd - ge[idx] as f64).abs());
315
+ }
316
  }
317
+ for idx in [1usize, 4, 7] {
318
+ let mut hp = h_plano.clone();
319
+ let mut hm = h_plano.clone();
320
+ hp[idx] += eps;
321
+ hm[idx] -= eps;
322
+ let tp = Tensor::from_vec(hp, (b, d), &dev)?;
323
+ let tm = Tensor::from_vec(hm, (b, d), &dev)?;
324
+ let fd = (f(&a, &e, &tp)? - f(&a, &e, &tm)?) / (2.0 * eps as f64);
325
+ erro = erro.max((fd - gh0[idx] as f64).abs());
326
+ }
327
+ assert!(erro < 5e-3, "erro adjunto SSD-Δ (a, η, h0) = {erro}");
328
+ Ok(())
329
+ }
330
+
331
+ /// Gate G8a (inteiros): a=1, η=1 ⇒ h_t = h_{t−1} + x_t EXATO para
332
+ /// inteiros com |soma| < 2^24 (Teo S-1(i)).
333
+ #[test]
334
+ fn gate_acumulacao_inteiros_exata() -> Result<()> {
335
+ let dev = Device::Cpu;
336
+ let (b, n, d) = (1usize, 64, 4);
337
+ let x: Vec<f32> = (0..n * d).map(|i| ((i % 17) as f32) * 100.0 - 800.0).collect();
338
+ let x = Tensor::from_vec(x, (b * n, d), &dev)?;
339
+ let a = Tensor::ones((b * n, d), candle_core::DType::F32, &dev)?;
340
+ let eta = Tensor::ones((b * n, d), candle_core::DType::F32, &dev)?;
341
+ let gates = Tensor::cat(&[&a, &eta], 1)?.contiguous()?;
342
+ let h0 = Tensor::zeros((b, d), candle_core::DType::F32, &dev)?;
343
+ let y = Tensor::apply_op3(&gates, &x, &h0, ScanDiagonal { b, n })?;
344
+ let yv = y.flatten_all()?.to_vec1::<f32>()?;
345
+ let xv = x.flatten_all()?.to_vec1::<f32>()?;
346
+ let mut soma = vec![0f32; d];
347
+ for i in 0..n {
348
+ for j in 0..d {
349
+ soma[j] += xv[i * d + j];
350
+ assert_eq!(yv[i * d + j], soma[j], "acumulação inteira exata em t={i} j={j}");
351
+ }
352
+ }
353
+ Ok(())
354
+ }
355
+
356
+ /// Gate G8b (floats): com η = tanh(ρ), erro ≤ 2e^{−2ρ}·Σ|x| + cota de
357
+ /// arredondamento f32 (Teo S-1(ii)/(iii)).
358
+ #[test]
359
+ fn gate_acumulacao_floats_cota() -> Result<()> {
360
+ let dev = Device::Cpu;
361
+ let (b, n, d) = (1usize, 128, 2);
362
+ let xv: Vec<f32> = (0..n * d).map(|i| ((i as f32) * 0.1 - 6.4).sin() * 0.37).collect();
363
+ let x = Tensor::from_vec(xv.clone(), (b * n, d), &dev)?;
364
+ let rho = 4.0f32;
365
+ let eta = Tensor::full(rho.tanh(), (b * n, d), &dev)?;
366
+ let a = Tensor::ones((b * n, d), candle_core::DType::F32, &dev)?;
367
+ let gates = Tensor::cat(&[&a, &eta], 1)?.contiguous()?;
368
+ let h0 = Tensor::zeros((b, d), candle_core::DType::F32, &dev)?;
369
+ let y = Tensor::apply_op3(&gates, &x, &h0, ScanDiagonal { b, n })?;
370
+ let yv = y.flatten_all()?.to_vec1::<f32>()?;
371
+ let eps_mach = 2f32.powi(-24);
372
+ let mut soma = vec![0f32; d];
373
+ let mut soma_abs = vec![0f32; d];
374
+ let mut erro_max = 0f64;
375
+ for i in 0..n {
376
+ for j in 0..d {
377
+ soma[j] += xv[i * d + j];
378
+ soma_abs[j] += xv[i * d + j].abs();
379
+ let cota = 2.0 * (-2.0 * rho).exp() as f32 * soma_abs[j]
380
+ + (i as f32) * eps_mach * soma_abs[j];
381
+ erro_max = erro_max.max((yv[i * d + j] - soma[j]).abs() as f64);
382
+ assert!(
383
+ (yv[i * d + j] - soma[j]).abs() <= cota,
384
+ "t={i} j={j}: erro {} > cota {}",
385
+ (yv[i * d + j] - soma[j]).abs(), cota
386
+ );
387
+ }
388
+ }
389
+ assert!(erro_max < 0.05, "erro de acumulação float razoável: {erro_max}");
390
+ Ok(())
391
+ }
392
+
393
+ /// Gate G3 (parte): scan completo ≡ scan incremental com estado (M4).
394
+ #[test]
395
+ fn gate_estado_transportado_equivalente() -> Result<()> {
396
+ crate::rng::inicializar(31415);
397
+ let dev = Device::Cpu;
398
+ let (b, n1, n2, d) = (1usize, 7, 11, 6);
399
+ let x = crate::rng::randn_semeado(1.0, &[b * (n1 + n2), d], &dev)?;
400
+ let gates_ruido = crate::rng::randn_semeado(1.0, &[b * (n1 + n2), 2 * d], &dev)?;
401
+ let a = candle_nn::ops::sigmoid(&gates_ruido.narrow(1, 0, d)?)?;
402
+ let e = gates_ruido.narrow(1, d, d)?.tanh()?;
403
+ let gates = Tensor::cat(&[&a, &e], 1)?.contiguous()?;
404
+ let h0 = Tensor::zeros((b, d), candle_core::DType::F32, &dev)?;
405
+ // completo
406
+ let y_total = Tensor::apply_op3(&gates, &x, &h0, ScanDiagonal { b, n: n1 + n2 })?;
407
+ // incremental: [A;B] com estado transportado
408
+ let xa = x.narrow(0, 0, n1)?.contiguous()?;
409
+ let xb = x.narrow(0, n1, n2)?.contiguous()?;
410
+ let ga = gates.narrow(0, 0, n1)?.contiguous()?;
411
+ let gb = gates.narrow(0, n1, n2)?.contiguous()?;
412
+ let ya = Tensor::apply_op3(&ga, &xa, &h0, ScanDiagonal { b, n: n1 })?;
413
+ let hA = ya.narrow(0, n1 - 1, 1)?.reshape((b, d))?.contiguous()?;
414
+ let yb = Tensor::apply_op3(&gb, &xb, &hA, ScanDiagonal { b, n: n2 })?;
415
+ // compara o segmento B
416
+ let ref_b = y_total.narrow(0, n1, n2)?;
417
+ let dif = (&yb - &ref_b)?.abs()?.max_all()?.to_scalar::<f32>()?;
418
+ assert!(dif < 1e-6, "estado SSD transportado: dif = {dif}");
419
  Ok(())
420
  }
421
  }
src/telemetria.rs CHANGED
@@ -3,10 +3,14 @@
3
  //! automatizados de todos hiper parâmetros e demais necessários para ajustes
4
  //! e correções e detecção de erros").
5
  //!
6
- //! Hub em Rust com: buffer por passo; exportação JSONL; detecção de
7
- //! aprendizado (escore composto); eventos de ajuste automático (auditáveis);
8
- //! eventos de erro com classificação de causa comum (diagnostico.rs);
9
- //! séries expostas ao Python como arrays numpy.
 
 
 
 
10
 
11
  use serde::Serialize;
12
  use std::collections::BTreeMap;
@@ -30,6 +34,14 @@ pub struct EventoErro {
30
  pub correcao_sugerida: String,
31
  }
32
 
 
 
 
 
 
 
 
 
33
  #[derive(Default)]
34
  pub struct TelemetryHub {
35
  /// séries nomeadas por passo
@@ -38,22 +50,41 @@ pub struct TelemetryHub {
38
  pub eventos_erro: Vec<EventoErro>,
39
  pub caminho_jsonl: Option<String>,
40
  pub passo_corrente: usize,
 
 
 
 
 
 
 
 
41
  }
42
 
43
  impl TelemetryHub {
44
  pub fn novo(caminho_jsonl: Option<String>) -> Self {
 
 
 
 
 
 
45
  if let Some(p) = &caminho_jsonl {
46
  if let Some(dir) = std::path::Path::new(p).parent() {
47
  let _ = std::fs::create_dir_all(dir);
48
  }
49
- // truncates no início da corrida
50
- let _ = std::fs::File::create(p);
 
 
 
 
51
  }
52
- TelemetryHub { caminho_jsonl, ..Default::default() }
53
  }
54
 
55
  pub fn registrar(&mut self, nome: &str, valor: f64) {
56
  self.series.entry(nome.to_string()).or_default().push((self.passo_corrente, valor));
 
57
  }
58
 
59
  pub fn registrar_multi(&mut self, pares: &[(&str, f64)]) {
@@ -62,6 +93,31 @@ impl TelemetryHub {
62
  }
63
  }
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  pub fn ajuste(&mut self, parametro: &str, de: f64, para: f64, motivo: &str) {
66
  let ev = EventoAjuste {
67
  passo: self.passo_corrente,
@@ -72,6 +128,7 @@ impl TelemetryHub {
72
  };
73
  log::info!("ajuste: {} {:.4e}→{:.4e} ({})", parametro, de, para, motivo);
74
  self.append_jsonl("ajuste", &ev);
 
75
  self.eventos_ajuste.push(ev);
76
  }
77
 
@@ -92,6 +149,7 @@ impl TelemetryHub {
92
  causa_provavel: causa.to_string(),
93
  correcao_sugerida: correcao.to_string(),
94
  };
 
95
  self.append_jsonl("erro", &ev);
96
  self.eventos_erro.push(ev);
97
  }
@@ -129,11 +187,11 @@ impl TelemetryHub {
129
  acc_fim = alfa * v + (1.0 - alfa) * acc_fim;
130
  }
131
  let passos = (s.len() - ini) as f64;
132
- Some((acc_fim - acc_ini) / passos)
133
  }
134
 
135
- /// Escore de aprendizado composto em [0,1]: sinal = inclinação negativa da
136
- /// perda (EMA) + nível absoluto relativo ao inicial + saúde do codebook.
137
  pub fn escore_aprendizado(&self) -> f64 {
138
  let Some(perda) = self.series.get("perda") else { return 0.0 };
139
  if perda.len() < 4 { return 0.0; }
@@ -142,12 +200,57 @@ impl TelemetryHub {
142
  let reducao = ((p0 - pl) / p0.max(1e-9)).clamp(0.0, 1.0);
143
  let incl = self.inclinacao_ema("perda", 64, 0.05).unwrap_or(0.0);
144
  let sinal = (-incl * 1000.0).clamp(-1.0, 1.0) * 0.5 + 0.5;
145
- // saúde do codebook (se medida): perplexidade relativa
146
  let saude = self.series.get("codebook_perplexidade_media").and_then(|s| s.last().map(|(_, v)| *v)).unwrap_or(1.0);
147
- let saude_n = (saude / 16.0).clamp(0.0, 1.0); // K=64: perplexidade 16+ = saudável
148
  0.45 * reducao + 0.35 * sinal + 0.20 * saude_n
149
  }
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  /// Dump completo (para o relatório Python).
152
  pub fn dump(&self) -> serde_json::Value {
153
  let series: serde_json::Map<String, serde_json::Value> = self
@@ -167,6 +270,11 @@ impl TelemetryHub {
167
  "eventos_ajuste": self.eventos_ajuste,
168
  "eventos_erro": self.eventos_erro,
169
  "escore_aprendizado": self.escore_aprendizado(),
 
 
 
 
 
170
  })
171
  }
172
 
@@ -178,3 +286,37 @@ impl TelemetryHub {
178
  .collect()
179
  }
180
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  //! automatizados de todos hiper parâmetros e demais necessários para ajustes
4
  //! e correções e detecção de erros").
5
  //!
6
+ //! Hub em Rust com: buffer por passo; exportação JSONL (UMA LINHA POR PASSO
7
+ //! com todas as séries — M12: a v0.1.11 gravava apenas eventos de ajuste/erro
8
+ //! e o artefato publicado tinha 0-1 linhas); modo append por padrão (A24: a
9
+ //! v0.1.11 TRUNCAVA o arquivo no início e apagava o histórico do run
10
+ //! anterior); detecção de aprendizado com LIMIARES E EVENTOS (S-05: a v0.1.11
11
+ //! computava o escore mas nada ramificava sobre ele); eventos de ajuste
12
+ //! automático (auditáveis); eventos de erro com classificação de causa comum
13
+ //! (diagnostico.rs — digitação/renomeação primeiro, como pedido).
14
 
15
  use serde::Serialize;
16
  use std::collections::BTreeMap;
 
34
  pub correcao_sugerida: String,
35
  }
36
 
37
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
38
+ pub enum EstadoAprendizado {
39
+ Efetivo,
40
+ Estagnado,
41
+ Regredindo,
42
+ SemDados,
43
+ }
44
+
45
  #[derive(Default)]
46
  pub struct TelemetryHub {
47
  /// séries nomeadas por passo
 
50
  pub eventos_erro: Vec<EventoErro>,
51
  pub caminho_jsonl: Option<String>,
52
  pub passo_corrente: usize,
53
+ /// valores do passo corrente (aguardando `fechar_passo`)
54
+ pendentes: BTreeMap<String, f64>,
55
+ /// A24: truncar o JSONL no início (default false = append)
56
+ pub truncar: bool,
57
+ /// contadores de diagnóstico (T-11)
58
+ pub contagem_nao_finitos: u64,
59
+ pub contagem_erros: u64,
60
+ pub contagem_ajustes: u64,
61
  }
62
 
63
  impl TelemetryHub {
64
  pub fn novo(caminho_jsonl: Option<String>) -> Self {
65
+ Self::novo_completo(caminho_jsonl, false)
66
+ }
67
+
68
+ /// A24: `truncar=false` (padrão) abre em APPEND — o histórico do run
69
+ /// anterior é preservado; `truncar=true` rotaciona explicitamente.
70
+ pub fn novo_completo(caminho_jsonl: Option<String>, truncar: bool) -> Self {
71
  if let Some(p) = &caminho_jsonl {
72
  if let Some(dir) = std::path::Path::new(p).parent() {
73
  let _ = std::fs::create_dir_all(dir);
74
  }
75
+ if truncar {
76
+ let _ = std::fs::File::create(p);
77
+ } else {
78
+ // garante que o arquivo existe sem truncar
79
+ let _ = std::fs::OpenOptions::new().create(true).append(true).open(p);
80
+ }
81
  }
82
+ TelemetryHub { caminho_jsonl, truncar, ..Default::default() }
83
  }
84
 
85
  pub fn registrar(&mut self, nome: &str, valor: f64) {
86
  self.series.entry(nome.to_string()).or_default().push((self.passo_corrente, valor));
87
+ self.pendentes.insert(nome.to_string(), valor);
88
  }
89
 
90
  pub fn registrar_multi(&mut self, pares: &[(&str, f64)]) {
 
93
  }
94
  }
95
 
96
+ /// M12: grava UMA linha JSON com todas as séries do passo corrente e
97
+ /// limpa o buffer pendente. Chamar ao fim de cada passo de treino.
98
+ pub fn fechar_passo(&mut self) {
99
+ if self.pendentes.is_empty() {
100
+ return;
101
+ }
102
+ if let Some(p) = &self.caminho_jsonl {
103
+ if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) {
104
+ let mut dados = serde_json::Map::new();
105
+ dados.insert("passo".into(), serde_json::json!(self.passo_corrente));
106
+ for (k, v) in &self.pendentes {
107
+ dados.insert(k.clone(), serde_json::json!(v));
108
+ }
109
+ let _ = writeln!(
110
+ f,
111
+ "{{\"tipo\":\"serie\",\"dados\":{}}}",
112
+ serde_json::to_string(&serde_json::Value::Object(dados))
113
+ .unwrap_or_default()
114
+ );
115
+ let _ = f.flush();
116
+ }
117
+ }
118
+ self.pendentes.clear();
119
+ }
120
+
121
  pub fn ajuste(&mut self, parametro: &str, de: f64, para: f64, motivo: &str) {
122
  let ev = EventoAjuste {
123
  passo: self.passo_corrente,
 
128
  };
129
  log::info!("ajuste: {} {:.4e}→{:.4e} ({})", parametro, de, para, motivo);
130
  self.append_jsonl("ajuste", &ev);
131
+ self.contagem_ajustes += 1;
132
  self.eventos_ajuste.push(ev);
133
  }
134
 
 
149
  causa_provavel: causa.to_string(),
150
  correcao_sugerida: correcao.to_string(),
151
  };
152
+ self.contagem_erros += 1;
153
  self.append_jsonl("erro", &ev);
154
  self.eventos_erro.push(ev);
155
  }
 
187
  acc_fim = alfa * v + (1.0 - alfa) * acc_fim;
188
  }
189
  let passos = (s.len() - ini) as f64;
190
+ Some((acc_fim - acc_ini) / passos.max(1.0))
191
  }
192
 
193
+ /// Escore de aprendizado composto em [0,1]: redução relativa + sinal da
194
+ /// inclinação (EMA) + saúde do codebook.
195
  pub fn escore_aprendizado(&self) -> f64 {
196
  let Some(perda) = self.series.get("perda") else { return 0.0 };
197
  if perda.len() < 4 { return 0.0; }
 
200
  let reducao = ((p0 - pl) / p0.max(1e-9)).clamp(0.0, 1.0);
201
  let incl = self.inclinacao_ema("perda", 64, 0.05).unwrap_or(0.0);
202
  let sinal = (-incl * 1000.0).clamp(-1.0, 1.0) * 0.5 + 0.5;
 
203
  let saude = self.series.get("codebook_perplexidade_media").and_then(|s| s.last().map(|(_, v)| *v)).unwrap_or(1.0);
204
+ let saude_n = (saude / 16.0).clamp(0.0, 1.0);
205
  0.45 * reducao + 0.35 * sinal + 0.20 * saude_n
206
  }
207
 
208
+ /// S-05 (v0.3.0): classificação do estado de aprendizado COM LIMIARES e
209
+ /// eventos auditáveis (a v0.1.11 calculava o escore sem ramificar):
210
+ /// > 0,30 EFETIVO (aprendizado em curso)
211
+ /// 0,15–0,30 ESTAGNADO (sem progresso suficiente)
212
+ /// < 0,15 REGREDINDO (dispara evento de erro + sugere ação)
213
+ pub fn detectar_aprendizado(&mut self) -> EstadoAprendizado {
214
+ let e = self.escore_aprendizado();
215
+ let dados = self
216
+ .series
217
+ .get("perda")
218
+ .map(|s| s.len())
219
+ .unwrap_or(0);
220
+ if dados < 4 {
221
+ return EstadoAprendizado::SemDados;
222
+ }
223
+ let estado = if e > 0.30 {
224
+ EstadoAprendizado::Efetivo
225
+ } else if e >= 0.15 {
226
+ EstadoAprendizado::Estagnado
227
+ } else {
228
+ EstadoAprendizado::Regredindo
229
+ };
230
+ match estado {
231
+ EstadoAprendizado::Estagnado => {
232
+ self.append_jsonl(
233
+ "aprendizado",
234
+ &serde_json::json!({
235
+ "passo": self.passo_corrente, "estado": "estagnado", "escore": e,
236
+ "acao_sugerida": "verificar LR/clip (controlador) e a série ce_por_nivel",
237
+ }),
238
+ );
239
+ }
240
+ EstadoAprendizado::Regredindo => {
241
+ self.append_jsonl(
242
+ "aprendizado",
243
+ &serde_json::json!({
244
+ "passo": self.passo_corrente, "estado": "regredindo", "escore": e,
245
+ "acao_sugerida": "reduzir LR (controlador), checar NaN (diagnostico::origem_nan) e revisar mudanças recentes (digitação?)",
246
+ }),
247
+ );
248
+ }
249
+ _ => {}
250
+ }
251
+ estado
252
+ }
253
+
254
  /// Dump completo (para o relatório Python).
255
  pub fn dump(&self) -> serde_json::Value {
256
  let series: serde_json::Map<String, serde_json::Value> = self
 
270
  "eventos_ajuste": self.eventos_ajuste,
271
  "eventos_erro": self.eventos_erro,
272
  "escore_aprendizado": self.escore_aprendizado(),
273
+ "contagens": {
274
+ "nao_finitos": self.contagem_nao_finitos,
275
+ "erros": self.contagem_erros,
276
+ "ajustes": self.contagem_ajustes,
277
+ },
278
  })
279
  }
280
 
 
286
  .collect()
287
  }
288
  }
289
+
290
+ #[cfg(test)]
291
+ mod testes {
292
+ use super::*;
293
+
294
+ /// Gate M12/H4: `fechar_passo` grava uma linha por passo com TODAS as séries.
295
+ #[test]
296
+ fn series_vao_para_o_jsonl() {
297
+ let dir = std::env::temp_dir().join(format!("khtst_tel_test_{}", std::process::id()));
298
+ let _ = std::fs::create_dir_all(&dir);
299
+ let p = dir.join("tel.jsonl");
300
+ let p_str = p.to_str().unwrap().to_string();
301
+ let mut hub = TelemetryHub::novo(Some(p_str.clone()));
302
+ for passo in 0..3 {
303
+ hub.passo_corrente = passo;
304
+ hub.registrar_multi(&[("perda", 1.0 / (passo as f64 + 1.0)), ("lr", 3e-4)]);
305
+ hub.fechar_passo();
306
+ }
307
+ let conteudo = std::fs::read_to_string(&p_str).unwrap();
308
+ let linhas: Vec<&str> = conteudo.lines().collect();
309
+ assert_eq!(linhas.len(), 3, "3 passos ⇒ 3 linhas (M12): {conteudo}");
310
+ assert!(linhas[0].contains("\"tipo\":\"serie\""));
311
+ assert!(linhas[0].contains("\"perda\""));
312
+ assert!(linhas[0].contains("\"lr\""));
313
+ // A24: novo hub NÃO apaga o histórico (append)
314
+ let mut hub2 = TelemetryHub::novo(Some(p_str.clone()));
315
+ hub2.passo_corrente = 99;
316
+ hub2.registrar("perda", 0.5);
317
+ hub2.fechar_passo();
318
+ let conteudo2 = std::fs::read_to_string(&p_str).unwrap();
319
+ assert_eq!(conteudo2.lines().count(), 4, "append preserva histórico (A24)");
320
+ let _ = std::fs::remove_dir_all(&dir);
321
+ }
322
+ }
src/tokenizador.rs CHANGED
@@ -37,6 +37,8 @@ pub struct TokenizadorConceitos {
37
  pub d_lat: usize,
38
  pub vocab: usize,
39
  pub cfg_ncm_beta_commit: f64,
 
 
40
  pub modo: ModoQuant,
41
  }
42
 
@@ -61,8 +63,9 @@ impl TokenizadorConceitos {
61
  }
62
  let modo = ModoQuant::de_str(&n.modo_quantizacao);
63
  let mut quants = Vec::with_capacity(l);
64
- for _ in 0..l {
65
- quants.push(QuantizadorNivel::novo(n.k_por_nivel, n.d_lat, modo, n.ema_codebook, &n.escala_r, dev)?);
 
66
  }
67
  let mut proj = Vec::with_capacity(l);
68
  for _ in 0..l {
@@ -84,6 +87,7 @@ impl TokenizadorConceitos {
84
  d_lat: n.d_lat,
85
  vocab: m.vocab,
86
  cfg_ncm_beta_commit: n.beta_commit,
 
87
  modo,
88
  })
89
  }
@@ -99,11 +103,13 @@ impl TokenizadorConceitos {
99
  }
100
 
101
  /// Codificação de TREINO (fase A): relaxamento GSQ suave diferenciável.
102
- /// Devolve (zhats_suaves, zs_por_nivel) para a perda e telemetria.
103
- fn codificar_treino(&mut self, ids: &Tensor) -> Result<(Vec<Tensor>, Vec<Tensor>)> {
 
104
  let dev = ids.device();
105
  let z = self.encoder(ids)?;
106
  let mut zs = Vec::with_capacity(self.niveis.len());
 
107
  let mut zhats = Vec::with_capacity(self.niveis.len());
108
  // projeção ÚNICA d_modelo → d_lat na granularidade do nível 1 (n-02 §0);
109
  // a recursão residual vive INTEIRAMENTE em d_lat
@@ -124,6 +130,7 @@ impl TokenizadorConceitos {
124
  };
125
  zhat_acum = Some(zhat_l.clone());
126
  zhats.push(zhat_l);
 
127
  // resíduo na granularidade do próximo nível (Teo A.0/D.4)
128
  if i + 1 < self.niveis.len() {
129
  let n_prox = self.niveis[i + 1];
@@ -132,7 +139,7 @@ impl TokenizadorConceitos {
132
  }
133
  zs.push(zp);
134
  }
135
- Ok((zhats, zs))
136
  }
137
 
138
  /// Codificação CONGELADA (fase B / inferência): códigos DUROS + ẑ dos
@@ -167,6 +174,12 @@ impl TokenizadorConceitos {
167
  }
168
  }
169
 
 
 
 
 
 
 
170
  /// Reconstrói ẑ a partir de códigos (para corrupção κ e amostragem —
171
  /// o gerador nunca vê os vetores contínuos, apenas c (C.3)).
172
  pub fn reconstruir_de_codigos(&self, ids: &[Tensor], ate_nivel: usize, b: usize, dev: &Device) -> Result<Vec<Tensor>> {
@@ -220,7 +233,7 @@ impl TokenizadorConceitos {
220
  /// p(ℓ) uniforme (Teo A.2); RNG DEDICADO (pareamento — Teo 31.7).
221
  /// Devolve (tensor da perda total, métricas).
222
  pub fn perda_fase_a(&mut self, ids: &Tensor, rng: &mut StdRng) -> Result<(Tensor, PerdaFaseA)> {
223
- let (zhats, zs) = self.codificar_treino(ids)?;
224
  let l = zhats.len();
225
  // sorteio do nível de parada (nested dropout)
226
  let nivel_parada = rng.gen_range(1..=l);
@@ -233,18 +246,29 @@ impl TokenizadorConceitos {
233
  let mut commit_t: Option<Tensor> = None;
234
  let mut commit = 0f64;
235
  for (i, zp) in zs.iter().enumerate() {
236
- let (_ids, valores) = self.quants[i].quantizar_duro(zp)?;
237
- let dif = (&valores - zp)?;
238
  let termo = dif.sqr()?.mean_all()?;
239
  commit += termo.to_scalar::<f32>()? as f64;
240
  commit_t = Some(match commit_t { Some(s) => (&s + &termo)?, None => termo });
241
  }
242
  let commit_t = commit_t.unwrap_or_else(|| ce.zeros_like().unwrap());
243
- let total = (&ce + &commit_t.affine(self.cfg_ncm_beta_commit, 0.0)?)?;
 
244
  // precisão de reconstrução (telemetria)
245
  let preds = plano.argmax(D::Minus1)?;
246
  let acertos = preds.eq(&alvos)?.to_dtype(candle_core::DType::F32)?.sum_all()?.to_scalar::<f32>()? as f64;
247
  let acc = acertos / (b * n_t) as f64;
 
 
 
 
 
 
 
 
 
 
 
248
  // δ_ort por nível (V9)
249
  let deltas: Result<Vec<f64>> = self.quants.iter().map(|q| q.delta_ort()).collect();
250
  let deltas = deltas?;
@@ -257,41 +281,64 @@ impl TokenizadorConceitos {
257
  nivel_parada,
258
  acc_recon: acc,
259
  deltas_ort: deltas,
 
 
260
  }))
261
  }
262
 
 
 
 
 
 
 
 
263
  /// Decodificação LIVRE (amostragem): ẑ (b, n_ℓ, d_lat) → ids de token AR
264
  /// greedy. Teo A.2/B.4(b): qualquer prefixo decodifica (anytime).
265
- /// O texto final é reconstruído pelo BPE no lado Python.
 
 
266
  pub fn decodificar_livre(&self, zhat: &Tensor, max_tokens: usize) -> Result<Vec<Vec<u32>>> {
267
- {
268
- let dev = zhat.device();
269
- let b = zhat.dim(0)?;
270
- let n_total = self.mascara_causal_dec.dim(0)?;
271
- let up = interp_para(zhat, n_total, dev)?;
272
- let cond = self.dec_proj.fwd(&up)?;
273
- let mut tokens: Vec<Vec<u32>> = (0..b).map(|_| vec![crate::tokens::BOS]).collect();
274
- for passo in 1..max_tokens.min(n_total) {
275
- let planos: Vec<u32> = tokens.iter().flat_map(|t| t.iter().copied()).collect();
276
- let entrada = Tensor::from_vec(planos, (b, passo), dev)?;
277
- let mut x = self.dec_emb.fwd(&entrada)?;
278
- x = (&x + &cond.narrow(1, 0, passo)?)?;
279
- for bloco in &self.dec_blocos {
280
- let mut cache = Default::default();
281
- let m = self.mascara_causal_dec.narrow(0, 0, passo)?.narrow(1, 0, passo)?.contiguous()?;
282
- x = bloco.fwd(&x, Some(&m), &mut cache, None, None)?;
283
- }
284
- let x = self.dec_norm.fwd(&x)?;
285
- let logits = self.dec_head.fwd(&x)?;
286
- let ultimo = logits.narrow(1, passo - 1, 1)?.reshape((b, self.vocab))?;
287
- let preds = ultimo.argmax(D::Minus1)?;
288
- let v = preds.flatten_all()?.to_vec1::<u32>()?;
289
- for (i, t) in v.iter().enumerate() {
290
- tokens[i].push(*t);
291
- }
292
  }
293
- Ok(tokens)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  }
 
295
  }
296
 
297
  /// CE por prefixo (V3: monotonia do risco — Teo A.2) em modo congelado.
@@ -380,10 +427,19 @@ impl TokenizadorConceitos {
380
  v
381
  }
382
 
383
- /// Salva estado (checkpoint atômico — Teo 35.3): índices = ordem de vars().
 
 
384
  pub fn salvar(&self, caminho: &str) -> Result<()> {
 
385
  let mut obj = serde_json::Map::new();
386
- for (i, v) in self.vars().iter().enumerate() {
 
 
 
 
 
 
387
  let dados = v.as_tensor().flatten_all()?.to_vec1::<f32>()?;
388
  obj.insert(format!("v{i}"), serde_json::json!({
389
  "forma": v.as_tensor().dims(),
@@ -392,11 +448,30 @@ impl TokenizadorConceitos {
392
  }
393
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
394
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
395
- let tmp = format!("{caminho}.tmp");
396
- std::fs::write(&tmp, txt).map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
397
- std::fs::rename(&tmp, caminho).map_err(|e| candle_core::Error::Msg(format!("rename: {e}")))?;
398
- Ok(())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  }
 
400
  }
401
 
402
  pub struct PerdaFaseA {
@@ -405,6 +480,10 @@ pub struct PerdaFaseA {
405
  pub nivel_parada: usize,
406
  pub acc_recon: f64,
407
  pub deltas_ort: Vec<f64>,
 
 
 
 
408
  }
409
 
410
  /// Máscara causal aditiva (n, n): 0 permitido, MIN bloqueado.
 
37
  pub d_lat: usize,
38
  pub vocab: usize,
39
  pub cfg_ncm_beta_commit: f64,
40
+ /// v0.3.0: λ_rec (T10 — lambdas.rec, default 1.0 neutro).
41
+ pub cfg_lambda_rec: f64,
42
  pub modo: ModoQuant,
43
  }
44
 
 
63
  }
64
  let modo = ModoQuant::de_str(&n.modo_quantizacao);
65
  let mut quants = Vec::with_capacity(l);
66
+ for i in 0..l {
67
+ // M11: semente distinta por nível (ruído de Gumbel i.i.d. — Teo A.4)
68
+ quants.push(QuantizadorNivel::novo_nivel(n.k_por_nivel, n.d_lat, modo, n.ema_codebook, &n.escala_r, i, dev)?);
69
  }
70
  let mut proj = Vec::with_capacity(l);
71
  for _ in 0..l {
 
87
  d_lat: n.d_lat,
88
  vocab: m.vocab,
89
  cfg_ncm_beta_commit: n.beta_commit,
90
+ cfg_lambda_rec: if n.ativo { n.lambdas.rec.max(0.0) } else { 1.0 },
91
  modo,
92
  })
93
  }
 
103
  }
104
 
105
  /// Codificação de TREINO (fase A): relaxamento GSQ suave diferenciável.
106
+ /// Devolve (zhats_suaves, zs_por_nivel, rs_por_nivel) para a perda e
107
+ /// telemetria (contração por nível — V2 do n-04).
108
+ fn codificar_treino(&mut self, ids: &Tensor) -> Result<(Vec<Tensor>, Vec<Tensor>, Vec<Tensor>)> {
109
  let dev = ids.device();
110
  let z = self.encoder(ids)?;
111
  let mut zs = Vec::with_capacity(self.niveis.len());
112
+ let mut rs = Vec::with_capacity(self.niveis.len());
113
  let mut zhats = Vec::with_capacity(self.niveis.len());
114
  // projeção ÚNICA d_modelo → d_lat na granularidade do nível 1 (n-02 §0);
115
  // a recursão residual vive INTEIRAMENTE em d_lat
 
130
  };
131
  zhat_acum = Some(zhat_l.clone());
132
  zhats.push(zhat_l);
133
+ rs.push(r_suave.clone());
134
  // resíduo na granularidade do próximo nível (Teo A.0/D.4)
135
  if i + 1 < self.niveis.len() {
136
  let n_prox = self.niveis[i + 1];
 
139
  }
140
  zs.push(zp);
141
  }
142
+ Ok((zhats, zs, rs))
143
  }
144
 
145
  /// Codificação CONGELADA (fase B / inferência): códigos DUROS + ẑ dos
 
174
  }
175
  }
176
 
177
+ /// v0.3.0 — materializa distribuições por nível (verificador algébrico):
178
+ /// probs (b, n_ℓ, K) → (b, n_ℓ, d_lat) com Σ_k q·R·c_k.
179
+ pub fn materializar_probs_nivel(&self, nivel: usize, probs: &Tensor) -> Result<Tensor> {
180
+ self.quants[nivel].materializar_probs(probs)
181
+ }
182
+
183
  /// Reconstrói ẑ a partir de códigos (para corrupção κ e amostragem —
184
  /// o gerador nunca vê os vetores contínuos, apenas c (C.3)).
185
  pub fn reconstruir_de_codigos(&self, ids: &[Tensor], ate_nivel: usize, b: usize, dev: &Device) -> Result<Vec<Tensor>> {
 
233
  /// p(ℓ) uniforme (Teo A.2); RNG DEDICADO (pareamento — Teo 31.7).
234
  /// Devolve (tensor da perda total, métricas).
235
  pub fn perda_fase_a(&mut self, ids: &Tensor, rng: &mut StdRng) -> Result<(Tensor, PerdaFaseA)> {
236
+ let (zhats, zs, rs) = self.codificar_treino(ids)?;
237
  let l = zhats.len();
238
  // sorteio do nível de parada (nested dropout)
239
  let nivel_parada = rng.gen_range(1..=l);
 
246
  let mut commit_t: Option<Tensor> = None;
247
  let mut commit = 0f64;
248
  for (i, zp) in zs.iter().enumerate() {
249
+ let dif = (&rs[i] - zp)?;
 
250
  let termo = dif.sqr()?.mean_all()?;
251
  commit += termo.to_scalar::<f32>()? as f64;
252
  commit_t = Some(match commit_t { Some(s) => (&s + &termo)?, None => termo });
253
  }
254
  let commit_t = commit_t.unwrap_or_else(|| ce.zeros_like().unwrap());
255
+ // R-05/v0.3.0: λ_rec (lambdas.rec) pondera a reconstrução (T10)
256
+ let total = (&ce.affine(self.cfg_lambda_rec, 0.0)? + &commit_t.affine(self.cfg_ncm_beta_commit, 0.0)?)?;
257
  // precisão de reconstrução (telemetria)
258
  let preds = plano.argmax(D::Minus1)?;
259
  let acertos = preds.eq(&alvos)?.to_dtype(candle_core::DType::F32)?.sum_all()?.to_scalar::<f32>()? as f64;
260
  let acc = acertos / (b * n_t) as f64;
261
+ // contração por nível (V2 — n-04 §8): ‖z−r‖/‖z‖ relativo
262
+ let residuos_relativos: Vec<f64> = zs
263
+ .iter()
264
+ .zip(rs.iter())
265
+ .map(|(zp, rp)| {
266
+ let dif = (zp - rp)?;
267
+ let num = dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
268
+ let den = zp.sqr()?.sum_all()?.to_scalar::<f32>()? as f64;
269
+ Ok((num / den.max(1e-12)).sqrt())
270
+ })
271
+ .collect::<Result<Vec<_>>>()?;
272
  // δ_ort por nível (V9)
273
  let deltas: Result<Vec<f64>> = self.quants.iter().map(|q| q.delta_ort()).collect();
274
  let deltas = deltas?;
 
281
  nivel_parada,
282
  acc_recon: acc,
283
  deltas_ort: deltas,
284
+ residuos_relativos,
285
+ zhats,
286
  }))
287
  }
288
 
289
+ /// A43: invalida os caches de codebook (chamar após cada passo do otimizador).
290
+ pub fn invalidar_caches(&self) {
291
+ for q in &self.quants {
292
+ q.invalidar_cache();
293
+ }
294
+ }
295
+
296
  /// Decodificação LIVRE (amostragem): ẑ (b, n_ℓ, d_lat) → ids de token AR
297
  /// greedy. Teo A.2/B.4(b): qualquer prefixo decodifica (anytime).
298
+ /// M17: com KV-CACHE incremental — O(n) em vez do O(n²) da v0.1.11
299
+ /// (que re-executava o decoder inteiro a cada passo; 24× menos MACs
300
+ /// para n=48).
301
  pub fn decodificar_livre(&self, zhat: &Tensor, max_tokens: usize) -> Result<Vec<Vec<u32>>> {
302
+ let dev = zhat.device();
303
+ let b = zhat.dim(0)?;
304
+ let n_total = self.mascara_causal_dec.dim(0)?;
305
+ let up = interp_para(zhat, n_total, dev)?;
306
+ let cond = self.dec_proj.fwd(&up)?;
307
+ let mut tokens: Vec<Vec<u32>> = (0..b).map(|_| vec![crate::tokens::BOS]).collect();
308
+ let mut caches: Vec<crate::atencao::CacheKv> = vec![Default::default(); self.dec_blocos.len()];
309
+ // passo 0: alimenta BOS (1 posição)
310
+ let entrada = Tensor::from_vec(vec![crate::tokens::BOS as u32; b], (b, 1), dev)?;
311
+ let mut x = self.dec_emb.fwd(&entrada)?;
312
+ x = (&x + &cond.narrow(1, 0, 1)?)?;
313
+ let m0 = self.mascara_causal_dec.narrow(0, 0, 1)?.narrow(1, 0, 1)?.contiguous()?;
314
+ for (i, bloco) in self.dec_blocos.iter().enumerate() {
315
+ x = bloco.fwd(&x, Some(&m0), &mut caches[i], None, None)?;
316
+ }
317
+ let mut h = self.dec_head.fwd(&self.dec_norm.fwd(&x)?)?;
318
+ for passo in 1..max_tokens.min(n_total) {
319
+ let ultimo = h.narrow(1, h.dim(1)? - 1, 1)?.reshape((b, self.vocab))?;
320
+ let preds = ultimo.argmax(D::Minus1)?;
321
+ let v = preds.flatten_all()?.to_vec1::<u32>()?;
322
+ for (i, t) in v.iter().enumerate() {
323
+ tokens[i].push(*t);
 
 
 
324
  }
325
+ if passo + 1 >= max_tokens.min(n_total) {
326
+ break;
327
+ }
328
+ // alimenta SÓ o token novo — cache reaproveitado (M17)
329
+ let entrada = Tensor::from_vec(v, (b, 1), dev)?;
330
+ let mut x = self.dec_emb.fwd(&entrada)?;
331
+ x = (&x + &cond.narrow(1, passo, 1)?)?;
332
+ let row = self.mascara_causal_dec
333
+ .narrow(0, passo, 1)?
334
+ .narrow(1, 0, passo + 1)?
335
+ .contiguous()?;
336
+ for (i, bloco) in self.dec_blocos.iter().enumerate() {
337
+ x = bloco.fwd(&x, Some(&row), &mut caches[i], None, None)?;
338
+ }
339
+ h = self.dec_head.fwd(&self.dec_norm.fwd(&x)?)?;
340
  }
341
+ Ok(tokens)
342
  }
343
 
344
  /// CE por prefixo (V3: monotonia do risco — Teo A.2) em modo congelado.
 
427
  v
428
  }
429
 
430
+ /// Salva estado (checkpoint atômico — Teo 35.3 + A20/A21): manifesto com
431
+ /// contagem/formas (validação no load), tmp com PID, fsync + rename +
432
+ /// fsync do diretório (sobrevive a queda de energia).
433
  pub fn salvar(&self, caminho: &str) -> Result<()> {
434
+ let vars = self.vars();
435
  let mut obj = serde_json::Map::new();
436
+ obj.insert("__manifesto".into(), serde_json::json!({
437
+ "formato": "khtst-ckpt-v2",
438
+ "tipo": "tokenizador",
439
+ "n_vars": vars.len(),
440
+ "formas": vars.iter().map(|v| v.as_tensor().dims()).collect::<Vec<_>>(),
441
+ }));
442
+ for (i, v) in vars.iter().enumerate() {
443
  let dados = v.as_tensor().flatten_all()?.to_vec1::<f32>()?;
444
  obj.insert(format!("v{i}"), serde_json::json!({
445
  "forma": v.as_tensor().dims(),
 
448
  }
449
  let txt = serde_json::to_string(&serde_json::Value::Object(obj))
450
  .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
451
+ salvar_atomico(caminho, &txt)
452
+ }
453
+ }
454
+
455
+ /// Escrita atômica durável (A21): tmp com PID → fsync → rename → fsync dir.
456
+ pub fn salvar_atomico(caminho: &str, conteudo: &str) -> Result<()> {
457
+ use std::io::Write;
458
+ let tmp = format!("{caminho}.tmp{}", std::process::id());
459
+ {
460
+ let mut f = std::fs::File::create(&tmp)
461
+ .map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
462
+ f.write_all(conteudo.as_bytes())
463
+ .map_err(|e| candle_core::Error::Msg(format!("write: {e}")))?;
464
+ f.sync_all()
465
+ .map_err(|e| candle_core::Error::Msg(format!("fsync: {e}")))?;
466
+ }
467
+ std::fs::rename(&tmp, caminho)
468
+ .map_err(|e| candle_core::Error::Msg(format!("rename: {e}")))?;
469
+ if let Some(dir) = std::path::Path::new(caminho).parent() {
470
+ if let Ok(d) = std::fs::File::open(dir) {
471
+ let _ = d.sync_all();
472
+ }
473
  }
474
+ Ok(())
475
  }
476
 
477
  pub struct PerdaFaseA {
 
480
  pub nivel_parada: usize,
481
  pub acc_recon: f64,
482
  pub deltas_ort: Vec<f64>,
483
+ /// v0.3.0 (V2 — n-04 §8): contração por nível ‖z−r‖/‖z‖.
484
+ pub residuos_relativos: Vec<f64>,
485
+ /// reconstruções cumulativas (verificador algébrico / telemetria).
486
+ pub zhats: Vec<Tensor>,
487
  }
488
 
489
  /// Máscara causal aditiva (n, n): 0 permitido, MIN bloqueado.
src/tokens.rs CHANGED
@@ -13,9 +13,37 @@ pub const UNK: u32 = 3;
13
  pub const BASE_BYTES: u32 = 4;
14
  const ESPECIAIS: [&str; 4] = ["<pad>", "<bos>", "<eos>", "<unk>"];
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  #[derive(Debug, Clone, Default)]
17
  pub struct Bpe {
18
- /// token por id (string; bytes crus para os 256 iniciais)
19
  pub vocab: Vec<String>,
20
  /// merges na ordem de aprendizado: (id_a, id_b) -> id_novo implícito = 260+índice
21
  pub merges: Vec<(u32, u32)>,
@@ -32,7 +60,7 @@ impl Bpe {
32
  b.vocab.push(s.to_string());
33
  }
34
  for i in 0..256u32 {
35
- let s = String::from_utf8_lossy(&[i as u8]).to_string();
36
  b.id_por_token.insert(s.clone(), BASE_BYTES + i);
37
  b.vocab.push(s);
38
  }
@@ -201,20 +229,35 @@ impl Bpe {
201
  }
202
 
203
  pub fn decodificar(&self, ids: &[u32]) -> String {
204
- let mut out = String::new();
 
 
205
  for &i in ids {
206
  match i {
207
  PAD | BOS | EOS => {}
 
208
  _ => {
209
  if let Some(s) = self.vocab.get(i as usize) {
210
  if !ESPECIAIS.contains(&s.as_str()) {
211
- out.push_str(s);
212
  }
213
  }
214
  }
215
  }
216
  }
217
- out.trim().to_string()
 
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
 
220
  pub fn para_json(&self) -> serde_json::Value {
@@ -261,4 +304,26 @@ mod testes {
261
  assert_eq!(texto, "o gato come o peixe");
262
  assert!(b.n_vocab() >= 260);
263
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  }
 
13
  pub const BASE_BYTES: u32 = 4;
14
  const ESPECIAIS: [&str; 4] = ["<pad>", "<bos>", "<eos>", "<unk>"];
15
 
16
+ /// v0.3.0 (bug encontrado pelo roundtrip ESTRITO A11): a v0.1.11 criava o
17
+ /// vocab inicial com `String::from_utf8_lossy(&[byte])` — bytes 128-255
18
+ /// viravam U+FFFD (3 bytes!) e o DECODE de qualquer caractere acentuado
19
+ /// produzia lixo ("células" → "c￿lulas"). O teste tautológico da v0.1.11
20
+ /// (`or len(rt) > 50`) escondia o defeito.
21
+ /// Correção: mapeamento reversível byte↔char printable (estilo GPT-2):
22
+ /// bytes 33-126, 161-172, 174-255 mapeiam para si; os demais (controles,
23
+ /// espaço, 127-160, 173) mapeiam para U+0100+n — todos chars UTF-8 válidos.
24
+ pub fn byte_para_char(b: u8) -> char {
25
+ match b {
26
+ 33..=126 | 161..=172 | 174..=255 => b as char,
27
+ _ => char::from_u32(256 + b as u32).unwrap_or(chr_substituto(b)),
28
+ }
29
+ }
30
+
31
+ /// Reverso de `byte_para_char`.
32
+ fn chr_substituto(b: u8) -> char { byte_para_char_fallback(b) }
33
+ fn byte_para_char_fallback(b: u8) -> char { if b >= 33 { b as char } else { '\u{FFFD}' } }
34
+
35
+ pub fn char_para_byte(c: char) -> Option<u8> {
36
+ let cp = c as u32;
37
+ match cp {
38
+ 33..=126 | 161..=172 | 174..=255 => Some(cp as u8),
39
+ 256..=511 => Some((cp - 256) as u8),
40
+ _ => None,
41
+ }
42
+ }
43
+
44
  #[derive(Debug, Clone, Default)]
45
  pub struct Bpe {
46
+ /// token por id (string; 1 CHAR mapeado para cada um dos 256 bytes iniciais)
47
  pub vocab: Vec<String>,
48
  /// merges na ordem de aprendizado: (id_a, id_b) -> id_novo implícito = 260+índice
49
  pub merges: Vec<(u32, u32)>,
 
60
  b.vocab.push(s.to_string());
61
  }
62
  for i in 0..256u32 {
63
+ let s = byte_para_char(i as u8).to_string();
64
  b.id_por_token.insert(s.clone(), BASE_BYTES + i);
65
  b.vocab.push(s);
66
  }
 
229
  }
230
 
231
  pub fn decodificar(&self, ids: &[u32]) -> String {
232
+ // v0.3.0: acumula CHARS mapeados e reconverte byte-a-byte — o
233
+ // roundtrip é EXATO para qualquer UTF-8 válido (A11).
234
+ let mut chars: Vec<char> = vec![];
235
  for &i in ids {
236
  match i {
237
  PAD | BOS | EOS => {}
238
+ UNK => chars.push('\u{FFFD}'),
239
  _ => {
240
  if let Some(s) = self.vocab.get(i as usize) {
241
  if !ESPECIAIS.contains(&s.as_str()) {
242
+ chars.extend(s.chars());
243
  }
244
  }
245
  }
246
  }
247
  }
248
+ let mut bytes = Vec::with_capacity(chars.len());
249
+ for c in chars {
250
+ match char_para_byte(c) {
251
+ Some(b) => bytes.push(b),
252
+ None => {
253
+ // char de merge multibyte válido em UTF-8: só possível se
254
+ // o vocab foi adulterado; preserva como UTF-8 direto
255
+ let mut buf = [0u8; 4];
256
+ bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
257
+ }
258
+ }
259
+ }
260
+ String::from_utf8_lossy(&bytes).trim().to_string()
261
  }
262
 
263
  pub fn para_json(&self) -> serde_json::Value {
 
304
  assert_eq!(texto, "o gato come o peixe");
305
  assert!(b.n_vocab() >= 260);
306
  }
307
+
308
+ /// A11 (v0.3.0): roundtrip ESTRITO com acentos/UTF-8 multibyte — o teste
309
+ /// que a v0.1.11 não tinha (e falhava silenciosamente: "células" → lixo).
310
+ #[test]
311
+ fn roundtrip_exato_utf8() {
312
+ let textos: Vec<String> = [
313
+ "A célula нервosa possui axônios mielinizados",
314
+ "Análise química de compostos orgânicos — íons e áçúcares",
315
+ "João avançou três quilômetros em direção àrepública",
316
+ ].iter().map(|s| s.to_string()).collect();
317
+ let mut b = Bpe::treinar(&textos, 400);
318
+ for t in &textos {
319
+ let ids = b.codificar(t);
320
+ let rt = b.decodificar(&ids);
321
+ assert_eq!(rt, t.trim(), "roundtrip deve ser EXATO: {t}");
322
+ }
323
+ // serialização também preserva
324
+ let j = b.para_json();
325
+ let mut b2 = Bpe::de_json(&j).unwrap();
326
+ let ids = b2.codificar(&textos[0]);
327
+ assert_eq!(b2.decodificar(&ids), textos[0].trim());
328
+ }
329
  }
src/treino.rs CHANGED
@@ -1,11 +1,25 @@
1
  //! Loops de treino (fases A e B — n-04 §4) com otimizador AdamW MANUAL
2
  //! (controle total para PCGrad multi-perda, clip global e LR dinâmico do
3
- //! controlador), telemetria completa, callback Python periódico e
4
- //! checkpoints atômicos (Teo 35.3).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  use candle_core::{Device, Result, Tensor, Var};
7
- use candle_nn::loss::cross_entropy;
8
  use rand::prelude::*;
 
9
  use std::collections::HashMap;
10
 
11
  use crate::config::ConfigKhtst;
@@ -13,13 +27,14 @@ use crate::controlador::Controlador;
13
  use crate::gerador::GeradorConceitos;
14
  use crate::memoria::{JepaLatente, SomConceitos};
15
  use crate::mascaras::interp_para;
16
- use crate::telemetria::TelemetryHub;
17
  use crate::tokenizador::TokenizadorConceitos;
18
 
19
- /// AdamW manual (estado por índice de variável; grads achatados).
 
20
  pub struct AdamWManual {
21
- pub m: HashMap<usize, Vec<f32>>,
22
- pub v: HashMap<usize, Vec<f32>>,
23
  pub t: usize,
24
  pub lr: f64,
25
  pub beta1: f64,
@@ -31,7 +46,18 @@ pub struct AdamWManual {
31
 
32
  impl AdamWManual {
33
  pub fn novo(vars: Vec<Var>, lr: f64, wd: f64) -> Self {
34
- AdamWManual { m: HashMap::new(), v: HashMap::new(), t: 0, lr, beta1: 0.9, beta2: 0.999, eps: 1e-8, wd, vars }
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  pub fn set_lr(&mut self, lr: f64) {
@@ -39,31 +65,56 @@ impl AdamWManual {
39
  }
40
 
41
  /// Um passo. `grads`: por variável, gradiente ACHATADO (None = zero).
 
42
  pub fn passo(&mut self, grads: Vec<Option<Vec<f32>>>) {
43
  self.t += 1;
44
  let bc1 = 1.0 - self.beta1.powi(self.t as i32);
45
  let bc2 = 1.0 - self.beta2.powi(self.t as i32);
46
- for (i, var) in self.vars.iter().enumerate() {
47
- let Some(g) = grads.get(i).cloned().flatten() else { continue };
48
- let n = g.len();
49
- let m = self.m.entry(i).or_insert(vec![0f32; n]);
50
- let v = self.v.entry(i).or_insert(vec![0f32; n]);
51
- let dados = var.as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
52
- let mut novos = vec![0f32; n];
53
- for j in 0..n {
54
- m[j] = self.beta1 as f32 * m[j] + (1.0 - self.beta1 as f32) * g[j];
55
- v[j] = self.beta2 as f32 * v[j] + (1.0 - self.beta2 as f32) * g[j] * g[j];
56
- let mh = m[j] / bc1 as f32;
57
- let vh = v[j] / bc2 as f32;
58
- // AdamW: decaimento desacoplado
59
- novos[j] = dados[j] - (self.lr as f32) * (mh / (vh.sqrt() + self.eps as f32) + self.wd as f32 * dados[j]);
 
60
  }
61
- let forma = var.as_tensor().shape();
62
- let dev = var.as_tensor().device();
63
- let novo_t = Tensor::from_vec(novos, forma.clone(), dev).unwrap()
64
- .reshape(forma).unwrap();
65
- let _ = var.set(&novo_t);
66
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
 
69
  /// Norma global de gradiente (para clip e telemetria).
@@ -91,6 +142,41 @@ impl AdamWManual {
91
  }
92
  }
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  /// Extrai grads achatados do GradStore na ordem de `vars`.
95
  pub fn grads_achatados(perda: &Tensor, vars: &[Var]) -> Result<Vec<Option<Vec<f32>>>> {
96
  let gs = perda.backward()?;
@@ -105,18 +191,26 @@ pub fn grads_achatados(perda: &Tensor, vars: &[Var]) -> Result<Vec<Option<Vec<f3
105
  }
106
 
107
  /// PCGrad multi-perda sobre grads achatados (Teo D.7 — projeção por pares).
108
- pub fn pcgrad_achatado(mut grads: Vec<Vec<Option<Vec<f32>>>>) -> Vec<Option<Vec<f32>>> {
 
 
 
 
109
  let n_perdas = grads.len();
110
  let n_vars = grads[0].len();
 
111
  let mut saida: Vec<Option<Vec<f32>>> = vec![None; n_vars];
112
  for i in 0..n_perdas {
113
- // projeta g_i contra cada g_j conflitante
114
  for j in 0..n_perdas {
115
- if i == j { continue; }
 
 
 
116
  let mut ip = 0f64;
117
  let mut nj = 0f64;
118
  for v in 0..n_vars {
119
- if let (Some(a), Some(b)) = (&grads[i][v], &grads[j][v]) {
120
  for k in 0..a.len().min(b.len()) {
121
  ip += a[k] as f64 * b[k] as f64;
122
  nj += b[k] as f64 * b[k] as f64;
@@ -126,8 +220,8 @@ pub fn pcgrad_achatado(mut grads: Vec<Vec<Option<Vec<f32>>>>) -> Vec<Option<Vec<
126
  if ip < 0.0 && nj > 1e-12 {
127
  let alfa = (ip / nj) as f32;
128
  for v in 0..n_vars {
129
- if let Some(bv) = grads[j][v].clone() {
130
- if let Some(a) = &mut grads[i][v] {
131
  for k in 0..a.len().min(bv.len()) {
132
  a[k] -= alfa * bv[k];
133
  }
@@ -137,7 +231,7 @@ pub fn pcgrad_achatado(mut grads: Vec<Vec<Option<Vec<f32>>>>) -> Vec<Option<Vec<
137
  }
138
  }
139
  for v in 0..n_vars {
140
- if let Some(a) = &grads[i][v] {
141
  let media: Vec<f32> = a.iter().map(|x| x / n_perdas as f32).collect();
142
  saida[v] = Some(match &saida[v] {
143
  Some(s) => s.iter().zip(media.iter()).map(|(x, y)| x + y).collect(),
@@ -155,13 +249,12 @@ pub struct DadosTreino {
155
 
156
  impl DadosTreino {
157
  pub fn novo(sequencias: Vec<Vec<u32>>, n: usize) -> Self {
 
 
158
  let seqs = sequencias
159
  .into_iter()
160
- .filter(|s| s.len() >= 8)
161
  .map(|mut s| {
162
- while s.len() < n {
163
- s.push(crate::tokens::PAD);
164
- }
165
  s.truncate(n);
166
  s
167
  })
@@ -206,9 +299,12 @@ pub struct ResumoFase {
206
  pub perplexidade_codebook: f64,
207
  pub monotonia_prefixo: f64,
208
  pub escore_aprendizado: f64,
 
 
209
  }
210
 
211
  /// FASE A — tokenizador (n-04 §4): nested dropout + commitment + GSQ + RCO.
 
212
  pub fn treinar_fase_a(
213
  cfg: &ConfigKhtst,
214
  dados: &DadosTreino,
@@ -216,22 +312,52 @@ pub fn treinar_fase_a(
216
  callback: &CallbackPy,
217
  caminho_checkpoint: Option<&str>,
218
  teto_seg: Option<f64>,
 
219
  ) -> Result<ResumoFase> {
220
  let dev = Device::Cpu;
221
  let inicio = std::time::Instant::now();
 
222
  let mut tok = TokenizadorConceitos::novo(cfg, &dev)?;
223
- let vars = tok.vars();
224
  let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr, cfg.treino.peso_decaimento);
225
  let mut ctrl = Controlador::novo(cfg.treino.lr, cfg.treino.aquecimento, cfg.treino.passos_fase_a, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora);
226
- let mut rng = StdRng::seed_from_u64(31_7); // RNG dedicado (Teo 31.7)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  let perda_inicial = {
228
- let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
229
- let (p, _) = tok.perda_fase_a(&ids, &mut rng)?;
230
  p.to_scalar::<f32>()? as f64
231
  };
232
  let a_cada = cfg.treino.telemetria_a_cada.max(1);
233
- let mut passos_feitos = 0usize;
234
- 'laco_a: for passo in 0..cfg.treino.passos_fase_a {
235
  hub.passo_corrente = passo;
236
  if let Some(t) = teto_seg {
237
  if inicio.elapsed().as_secs_f64() > t {
@@ -240,8 +366,14 @@ pub fn treinar_fase_a(
240
  }
241
  }
242
  let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
243
- let (perda_t, _m) = tok.perda_fase_a(&ids, &mut rng)?;
244
  let perda = perda_t.to_scalar::<f32>()? as f64;
 
 
 
 
 
 
245
  // agenda τ (Teo 30.6)
246
  tok.agendar_tau(passo, cfg.ncm.gsq.tau0, cfg.ncm.gsq.tau_min, cfg.ncm.gsq.t_alvo);
247
  // LR + clip do controlador (ajustes AUTOMÁTICOS — auditados)
@@ -253,41 +385,61 @@ pub fn treinar_fase_a(
253
  let teto = ctrl.clip_corrente(hub);
254
  AdamWManual::clipar(&mut grads, teto);
255
  opt.passo(grads);
 
256
  // EMA do codebook (modo ste_ema)
257
  if cfg.ncm.modo_quantizacao == "ste_ema" {
258
  let cod = tok.codificar_congelado(&ids)?;
259
  tok.atualizar_ema_codebook(&cod.ids)?;
260
  }
261
- // telemetria
262
- hub.registrar_multi(&[
263
- ("perda", perda),
264
- ("lr", lr),
265
- ("norma_grad", norma),
266
- ]);
 
 
 
267
  if passo % a_cada == 0 || passo == cfg.treino.passos_fase_a - 1 {
268
  let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::<f64>() / tok.quants.len().max(1) as f64;
269
  let dort: f64 = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max);
 
270
  hub.registrar_multi(&[
271
  ("codebook_perplexidade_media", perp),
272
  ("delta_ort_max", dort),
 
273
  ("escore_aprendizado", hub.escore_aprendizado()),
274
  ]);
 
 
275
  if cfg.telemetria.deteccao_aprendizado {
276
  let snap = serde_json::json!({
277
  "fase": "A", "passo": passo, "perda": perda, "lr": lr,
278
  "codebook_perplexidade_media": perp, "delta_ort_max": dort,
 
279
  "escore": hub.escore_aprendizado(),
 
280
  });
281
  chamar_callback(callback, &snap);
282
  }
 
 
 
 
 
 
 
283
  }
284
- if !perda.is_finite() {
285
- hub.erro("fase_a", &format!("perda não finita no passo {passo}"));
286
- break;
287
- }
288
  }
289
- if passos_feitos == 0 { passos_feitos = cfg.treino.passos_fase_a; }
290
- // avaliação final: monotonia de prefixo (V3) + perplexidade
 
 
 
 
 
 
291
  let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
292
  let ces_prefixo = tok.ce_por_prefixo(&ids_val)?;
293
  let mut pares_ok = 0usize;
@@ -299,14 +451,18 @@ pub fn treinar_fase_a(
299
  }
300
  }
301
  let monotonia = pares_ok as f64 / pares.max(1) as f64;
302
- let perda_final = ces_prefixo.last().copied().unwrap_or(f64::NAN);
303
  let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::<f64>() / tok.quants.len().max(1) as f64;
304
  let dort = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max);
305
  let acc = { let (_, m) = tok.perda_fase_a(&ids_val, &mut rng)?; m.acc_recon };
 
 
306
  if let Some(c) = caminho_checkpoint {
307
  tok.salvar(c)?;
 
308
  }
309
  hub.registrar("passos_feitos", passos_feitos as f64);
 
 
310
  Ok(ResumoFase {
311
  passos: passos_feitos,
312
  perda_final,
@@ -316,12 +472,35 @@ pub fn treinar_fase_a(
316
  perplexidade_codebook: perp,
317
  monotonia_prefixo: monotonia,
318
  escore_aprendizado: hub.escore_aprendizado(),
 
 
319
  })
320
  }
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  /// FASE B — gerador (n-04 §4): MLE exato sobre hierarquias congeladas (B.1),
323
- /// corrupção κ + dropout CFG (C.3/D.2), MTP auxiliar (D.6), JEPA latente e
324
- /// PCGrad entre as perdas (D.7).
 
 
325
  pub fn treinar_fase_b(
326
  cfg: &ConfigKhtst,
327
  tok: &TokenizadorConceitos,
@@ -330,27 +509,72 @@ pub fn treinar_fase_b(
330
  callback: &CallbackPy,
331
  caminho_checkpoint: Option<&str>,
332
  teto_seg: Option<f64>,
 
333
  ) -> Result<ResumoFase> {
334
  let dev = Device::Cpu;
335
  let inicio = std::time::Instant::now();
 
336
  let mut ger = GeradorConceitos::novo(cfg, &dev)?;
337
- let jepa = JepaLatente::novo(cfg.ncm.d_lat, &dev)?;
338
  let mut som = SomConceitos::novo(cfg.memoria.som_lado, cfg.ncm.d_lat, &dev)?;
339
  let mut vars = ger.vars();
340
  vars.extend(jepa.vars());
 
341
  let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr * 0.7, cfg.treino.peso_decaimento);
342
  let mut ctrl = Controlador::novo(cfg.treino.lr * 0.7, cfg.treino.aquecimento, cfg.treino.passos_fase_b, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora);
343
- let mut rng = StdRng::seed_from_u64(31_7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  let a_cada = cfg.treino.telemetria_a_cada.max(1);
345
- let lambda_jepa = cfg.memoria.lambda_jepa;
 
 
 
 
346
  let perda_inicial = {
347
- let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
348
- let cod = tok.codificar_congelado(&ids)?;
349
- let s = ger.perda_treino(tok, &cod.ids, &mut rng)?;
350
  s.ce_por_nivel.iter().sum::<f64>() / s.ce_por_nivel.len() as f64
351
  };
352
- let mut passos_feitos = 0usize;
353
- 'laco_b: for passo in 0..cfg.treino.passos_fase_b {
354
  hub.passo_corrente = passo;
355
  if let Some(t) = teto_seg {
356
  if inicio.elapsed().as_secs_f64() > t {
@@ -362,8 +586,18 @@ pub fn treinar_fase_b(
362
  let cod = tok.codificar_congelado(&ids)?;
363
  let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?;
364
  let ce = saida.ce_por_nivel.iter().sum::<f64>() / saida.ce_por_nivel.len() as f64;
365
- // JEPA latente (alvo com stop-grad — contrato doc 30)
366
- let perda_jepa_t = {
 
 
 
 
 
 
 
 
 
 
367
  let b = ids.dim(0)?;
368
  let zhats = tok.reconstruir_de_codigos(&cod.ids, cod.ids.len(), b, &dev)?;
369
  let mut soma = None;
@@ -375,14 +609,23 @@ pub fn treinar_fase_b(
375
  soma = Some(match soma { Some(s) => (&s + &pj)?, None => pj });
376
  }
377
  match soma {
378
- Some(s) => s.affine(lambda_jepa / zhats.len().max(1) as f64, 0.0)?,
379
- None => Tensor::zeros((), candle_core::DType::F32, &dev)?,
 
 
 
 
380
  }
381
  };
382
- // perdas separadas para PCGrad: [CE (com MTP embutido), JEPA]
383
  let mut grads_lista = Vec::new();
384
  grads_lista.push(grads_achatados(&saida.perda, &vars)?);
385
  grads_lista.push(grads_achatados(&perda_jepa_t, &vars)?);
 
 
 
 
 
386
  let mut combinado = pcgrad_achatado(grads_lista);
387
  let norma = AdamWManual::norma_global(&combinado);
388
  ctrl.registrar_norma(norma);
@@ -391,6 +634,12 @@ pub fn treinar_fase_b(
391
  let teto = ctrl.clip_corrente(hub);
392
  AdamWManual::clipar(&mut combinado, teto);
393
  opt.passo(combinado);
 
 
 
 
 
 
394
  // SOM pós-geração (stop-grad — Teo 17.4): insere vetores de nível 1
395
  if passo % a_cada == 0 {
396
  let b = ids.dim(0)?;
@@ -398,37 +647,57 @@ pub fn treinar_fase_b(
398
  let _eq = som.inserir(&zhats[0])?;
399
  hub.registrar("som_eq_medio", som.eq_medio);
400
  }
401
- hub.registrar_multi(&[
402
- ("perda", ce),
403
- ("ce_mtp", saida.ce_mtp),
404
- ("acc_conceitos", saida.acuracia_media),
405
- ("lr", lr),
406
- ("norma_grad", norma),
407
- ]);
 
 
 
 
 
 
408
  if passo % a_cada == 0 || passo == cfg.treino.passos_fase_b - 1 {
409
  hub.registrar("escore_aprendizado", hub.escore_aprendizado());
 
410
  let snap = serde_json::json!({
411
  "fase": "B", "passo": passo, "perda": ce, "ce_mtp": saida.ce_mtp,
412
  "acc_conceitos": saida.acuracia_media, "lr": lr,
 
413
  "escore": hub.escore_aprendizado(),
 
414
  });
415
  chamar_callback(callback, &snap);
 
 
 
 
 
 
416
  }
417
- if !ce.is_finite() {
418
- hub.erro("fase_b", &format!("CE não finita no passo {passo}"));
419
- break;
420
- }
421
  }
422
- if passos_feitos == 0 { passos_feitos = cfg.treino.passos_fase_b; }
 
 
 
 
 
 
 
423
  let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
424
  let cod = tok.codificar_congelado(&ids_val)?;
425
  let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?;
426
- let perda_final = saida.ce_por_nivel.iter().sum::<f64>() / saida.ce_por_nivel.len() as f64;
427
  if let Some(c) = caminho_checkpoint {
428
  ger.salvar(c)?;
 
429
  }
 
430
  Ok(ResumoFase {
431
- passos: cfg.treino.passos_fase_b,
432
  perda_final,
433
  perda_inicial,
434
  acc_recon: saida.acuracia_media,
@@ -436,5 +705,102 @@ pub fn treinar_fase_b(
436
  perplexidade_codebook: 0.0,
437
  monotonia_prefixo: 0.0,
438
  escore_aprendizado: hub.escore_aprendizado(),
 
 
439
  })
440
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  //! Loops de treino (fases A e B — n-04 §4) com otimizador AdamW MANUAL
2
  //! (controle total para PCGrad multi-perda, clip global e LR dinâmico do
3
+ //! controlador), telemetria completa (M12: uma linha JSONL por passo),
4
+ //! callback Python periódico, checkpoints atômicos com manifesto (A20/A21) e
5
+ //! RETOMADA (v0.3.0: o ambiente pode encerrar processos em background sem
6
+ //! erro/OOM — o estado do otimizador é salvo periodicamente).
7
+ //!
8
+ //! ## Correções v0.2.0/v0.3.0 aplicadas aqui
9
+ //! - **M8**: PCGrad projeta g_i contra os gradientes ORIGINAIS g_j (a v0.1.11
10
+ //! mutava g_j em lugar e decidia ⟨g_i,g_j⟩ com valores obsoletos);
11
+ //! - **M10**: passo com perda não finita é DESCARTADO ANTES de ser aplicado
12
+ //! (a v0.1.11 aplicava pesos NaN e gravava checkpoint com NaN);
13
+ //! - **A18**: fases A e B usam sementes distintas (31_7 ^ 0xA / ^ 0xB);
14
+ //! - **A19**: RNG global semeado (reprodutibilidade bit-a-bit);
15
+ //! - **A35**: AdamW paralelizado com rayon sobre as variáveis;
16
+ //! - **S-05**: detecção de aprendizado com limiares e eventos;
17
+ //! - **S-08/S-09**: ε (cobertura C.3(ii)) e λ (hipergradiente 29.2) integrados;
18
+ //! - **Teo G-1**: L_alg (verificador algébrico) como 3ª perda do PCGrad.
19
 
20
  use candle_core::{Device, Result, Tensor, Var};
 
21
  use rand::prelude::*;
22
+ use serde::{Deserialize, Serialize};
23
  use std::collections::HashMap;
24
 
25
  use crate::config::ConfigKhtst;
 
27
  use crate::gerador::GeradorConceitos;
28
  use crate::memoria::{JepaLatente, SomConceitos};
29
  use crate::mascaras::interp_para;
30
+ use crate::telemetria::{EstadoAprendizado, TelemetryHub};
31
  use crate::tokenizador::TokenizadorConceitos;
32
 
33
+ /// AdamW manual com estado por índice de variável (v0.3.0: Vec alinhado às
34
+ /// vars — serializável para RETOMADA; passo paralelizado com rayon — A35).
35
  pub struct AdamWManual {
36
+ pub m: Vec<Vec<f32>>,
37
+ pub v: Vec<Vec<f32>>,
38
  pub t: usize,
39
  pub lr: f64,
40
  pub beta1: f64,
 
46
 
47
  impl AdamWManual {
48
  pub fn novo(vars: Vec<Var>, lr: f64, wd: f64) -> Self {
49
+ let n = vars.len();
50
+ AdamWManual {
51
+ m: vec![Vec::new(); n],
52
+ v: vec![Vec::new(); n],
53
+ t: 0,
54
+ lr,
55
+ beta1: 0.9,
56
+ beta2: 0.999,
57
+ eps: 1e-8,
58
+ wd,
59
+ vars,
60
+ }
61
  }
62
 
63
  pub fn set_lr(&mut self, lr: f64) {
 
65
  }
66
 
67
  /// Um passo. `grads`: por variável, gradiente ACHATADO (None = zero).
68
+ /// M10: o CHAMADOR descarta passos não finitos ANTES de chegar aqui.
69
  pub fn passo(&mut self, grads: Vec<Option<Vec<f32>>>) {
70
  self.t += 1;
71
  let bc1 = 1.0 - self.beta1.powi(self.t as i32);
72
  let bc2 = 1.0 - self.beta2.powi(self.t as i32);
73
+ let lr = self.lr as f32;
74
+ let wd = self.wd as f32;
75
+ let b1 = self.beta1 as f32;
76
+ let b2 = self.beta2 as f32;
77
+ let eps = self.eps as f32;
78
+ // garante estado alinhado
79
+ for (i, g) in grads.iter().enumerate() {
80
+ if let Some(g) = g {
81
+ if self.m.get(i).map(|m| m.len()) != Some(g.len()) {
82
+ let n = g.len();
83
+ if i < self.m.len() {
84
+ self.m[i] = vec![0f32; n];
85
+ self.v[i] = vec![0f32; n];
86
+ }
87
+ }
88
  }
 
 
 
 
 
89
  }
90
+ let Self { vars, m, v, .. } = self;
91
+ use rayon::prelude::*;
92
+ grads
93
+ .into_par_iter()
94
+ .zip(m.par_iter_mut())
95
+ .zip(v.par_iter_mut())
96
+ .zip(vars.par_iter())
97
+ .for_each(|(((g, mm), vv), var)| {
98
+ let Some(g) = g else { return };
99
+ if mm.len() != g.len() {
100
+ *mm = vec![0f32; g.len()];
101
+ *vv = vec![0f32; g.len()];
102
+ }
103
+ let dados = var.as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap();
104
+ let mut novos = vec![0f32; g.len()];
105
+ for j in 0..g.len() {
106
+ mm[j] = b1 * mm[j] + (1.0 - b1) * g[j];
107
+ vv[j] = b2 * vv[j] + (1.0 - b2) * g[j] * g[j];
108
+ let mh = mm[j] / bc1 as f32;
109
+ let vh = vv[j] / bc2 as f32;
110
+ // AdamW: decaimento desacoplado
111
+ novos[j] = dados[j] - lr * (mh / (vh.sqrt() + eps) + wd * dados[j]);
112
+ }
113
+ let forma = var.as_tensor().shape();
114
+ let dev = var.as_tensor().device();
115
+ let novo_t = Tensor::from_vec(novos, forma.clone(), dev).unwrap().reshape(forma).unwrap();
116
+ let _ = var.set(&novo_t);
117
+ });
118
  }
119
 
120
  /// Norma global de gradiente (para clip e telemetria).
 
142
  }
143
  }
144
 
145
+ /// Estado serializável do treino (RETOMADA — A22 + proteção contra kill
146
+ /// do background: modelo + otimizador + controlador + passo).
147
+ #[derive(Serialize, Deserialize)]
148
+ pub struct EstadoTreino {
149
+ pub passo: usize,
150
+ pub m: Vec<Vec<f32>>,
151
+ pub v: Vec<Vec<f32>>,
152
+ pub t: usize,
153
+ pub multiplicador_lr: f64,
154
+ pub minimo_ema: f64,
155
+ pub passos_sem_melhora: usize,
156
+ pub clip: f64,
157
+ pub epsilon: f64,
158
+ pub lambda: f64,
159
+ /// fase B: variáveis do JEPA (índice alinhado) + protótipos do SOM
160
+ pub jepa_vars: Option<Vec<Vec<f32>>>,
161
+ pub som_prototipos: Option<Vec<f32>>,
162
+ pub som_lado: Option<usize>,
163
+ }
164
+
165
+ impl EstadoTreino {
166
+ pub fn salvar(caminho: &str, est: &EstadoTreino) -> Result<()> {
167
+ let txt = serde_json::to_string(est)
168
+ .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))?;
169
+ crate::tokenizador::salvar_atomico(caminho, &txt)
170
+ }
171
+
172
+ pub fn carregar(caminho: &str) -> Result<EstadoTreino> {
173
+ let txt = std::fs::read_to_string(caminho)
174
+ .map_err(|e| candle_core::Error::Msg(format!("io: {e}")))?;
175
+ serde_json::from_str(&txt)
176
+ .map_err(|e| candle_core::Error::Msg(format!("serde: {e}")))
177
+ }
178
+ }
179
+
180
  /// Extrai grads achatados do GradStore na ordem de `vars`.
181
  pub fn grads_achatados(perda: &Tensor, vars: &[Var]) -> Result<Vec<Option<Vec<f32>>>> {
182
  let gs = perda.backward()?;
 
191
  }
192
 
193
  /// PCGrad multi-perda sobre grads achatados (Teo D.7 — projeção por pares).
194
+ /// **M8 (v0.2.0)**: a projeção da perda i usa os gradientes ORIGINAIS das
195
+ /// demais perdas (Yu et al. 2020, Algorithm 1) — a v0.1.11 mutava `grads[j]`
196
+ /// em lugar, e a decisão ⟨g_i,g_j⟩ < 0 era tomada com o g_j JÁ PROJETADO
197
+ /// (valor obsoleto), podendo reintroduzir conflito.
198
+ pub fn pcgrad_achatado(grads: Vec<Vec<Option<Vec<f32>>>>) -> Vec<Option<Vec<f32>>> {
199
  let n_perdas = grads.len();
200
  let n_vars = grads[0].len();
201
+ let originais = grads.clone(); // M8: fontes imutáveis
202
  let mut saida: Vec<Option<Vec<f32>>> = vec![None; n_vars];
203
  for i in 0..n_perdas {
204
+ let mut gi = originais[i].clone();
205
  for j in 0..n_perdas {
206
+ if i == j {
207
+ continue;
208
+ }
209
+ // ⟨g_i_corrente, g_j_original⟩ — recalculado a cada par (M8)
210
  let mut ip = 0f64;
211
  let mut nj = 0f64;
212
  for v in 0..n_vars {
213
+ if let (Some(a), Some(b)) = (&gi[v], &originais[j][v]) {
214
  for k in 0..a.len().min(b.len()) {
215
  ip += a[k] as f64 * b[k] as f64;
216
  nj += b[k] as f64 * b[k] as f64;
 
220
  if ip < 0.0 && nj > 1e-12 {
221
  let alfa = (ip / nj) as f32;
222
  for v in 0..n_vars {
223
+ if let Some(bv) = &originais[j][v] {
224
+ if let Some(a) = &mut gi[v] {
225
  for k in 0..a.len().min(bv.len()) {
226
  a[k] -= alfa * bv[k];
227
  }
 
231
  }
232
  }
233
  for v in 0..n_vars {
234
+ if let Some(a) = &gi[v] {
235
  let media: Vec<f32> = a.iter().map(|x| x / n_perdas as f32).collect();
236
  saida[v] = Some(match &saida[v] {
237
  Some(s) => s.iter().zip(media.iter()).map(|(x, y)| x + y).collect(),
 
249
 
250
  impl DadosTreino {
251
  pub fn novo(sequencias: Vec<Vec<u32>>, n: usize) -> Self {
252
+ // A15: apenas sequências COMPRIDAS o suficiente viram janelas (a
253
+ // v0.1.11 aceitava >= 8 e preenchia com PAD — até 96% de padding)
254
  let seqs = sequencias
255
  .into_iter()
256
+ .filter(|s| s.len() >= n)
257
  .map(|mut s| {
 
 
 
258
  s.truncate(n);
259
  s
260
  })
 
299
  pub perplexidade_codebook: f64,
300
  pub monotonia_prefixo: f64,
301
  pub escore_aprendizado: f64,
302
+ pub residuo_algebrico: f64,
303
+ pub estado_aprendizado: String,
304
  }
305
 
306
  /// FASE A — tokenizador (n-04 §4): nested dropout + commitment + GSQ + RCO.
307
+ /// v0.3.0: `caminho_checkpoint` + retomada automática via `.estado.json`.
308
  pub fn treinar_fase_a(
309
  cfg: &ConfigKhtst,
310
  dados: &DadosTreino,
 
312
  callback: &CallbackPy,
313
  caminho_checkpoint: Option<&str>,
314
  teto_seg: Option<f64>,
315
+ retomar: bool,
316
  ) -> Result<ResumoFase> {
317
  let dev = Device::Cpu;
318
  let inicio = std::time::Instant::now();
319
+ crate::rng::inicializar(cfg.treino.semente ^ 0xA); // A18: semente própria da fase A
320
  let mut tok = TokenizadorConceitos::novo(cfg, &dev)?;
321
+ let mut vars = tok.vars();
322
  let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr, cfg.treino.peso_decaimento);
323
  let mut ctrl = Controlador::novo(cfg.treino.lr, cfg.treino.aquecimento, cfg.treino.passos_fase_a, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora);
324
+ let mut rng = StdRng::seed_from_u64(cfg.treino.semente ^ 0xA);
325
+ let mut passo_inicial = 0usize;
326
+ // RETOMADA: carrega modelo + estado do otimizador se disponíveis
327
+ if let Some(c) = caminho_checkpoint {
328
+ if retomar {
329
+ let est_path = format!("{c}.estado.json");
330
+ if std::path::Path::new(&est_path).exists() && std::path::Path::new(c).exists() {
331
+ // fase A: o checkpoint contém TODAS as vars do tokenizador
332
+ carregar_vars_do_checkpoint(&mut vars, c)?;
333
+ let est = EstadoTreino::carregar(&est_path)?;
334
+ opt.m = est.m;
335
+ opt.v = est.v;
336
+ opt.t = est.t;
337
+ ctrl.multiplicador = est.multiplicador_lr;
338
+ ctrl.minimo_ema = est.minimo_ema;
339
+ ctrl.passos_sem_melhora = est.passos_sem_melhora;
340
+ ctrl.clip = est.clip;
341
+ ctrl.epsilon_corrupcao = est.epsilon;
342
+ passo_inicial = est.passo;
343
+ hub.ajuste("retomada", 0.0, passo_inicial as f64, &format!("fase A retomada do passo {passo_inicial}"));
344
+ }
345
+ }
346
+ }
347
+ // perda inicial comparável (R-05: MESMA métrica nas duas pontas):
348
+ // CE+commit num lote FIXO com RNG dedicado
349
+ let lote_fixo = {
350
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1A7);
351
+ dados.lote(cfg.treino.lote, &mut r_fixo, &dev)?
352
+ };
353
  let perda_inicial = {
354
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1A7);
355
+ let (p, _) = tok.perda_fase_a(&lote_fixo, &mut r_fixo)?;
356
  p.to_scalar::<f32>()? as f64
357
  };
358
  let a_cada = cfg.treino.telemetria_a_cada.max(1);
359
+ let mut passos_feitos = passo_inicial;
360
+ 'laco_a: for passo in passo_inicial..cfg.treino.passos_fase_a {
361
  hub.passo_corrente = passo;
362
  if let Some(t) = teto_seg {
363
  if inicio.elapsed().as_secs_f64() > t {
 
366
  }
367
  }
368
  let ids = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
369
+ let (perda_t, m) = tok.perda_fase_a(&ids, &mut rng)?;
370
  let perda = perda_t.to_scalar::<f32>()? as f64;
371
+ // M10: perda não finita ⇒ DESCARTA o passo ANTES de aplicar (e para)
372
+ if !perda.is_finite() {
373
+ hub.contagem_nao_finitos += 1;
374
+ hub.erro("fase_a", &format!("perda não finita no passo {passo} — passo descartado (M10)"));
375
+ break 'laco_a;
376
+ }
377
  // agenda τ (Teo 30.6)
378
  tok.agendar_tau(passo, cfg.ncm.gsq.tau0, cfg.ncm.gsq.tau_min, cfg.ncm.gsq.t_alvo);
379
  // LR + clip do controlador (ajustes AUTOMÁTICOS — auditados)
 
385
  let teto = ctrl.clip_corrente(hub);
386
  AdamWManual::clipar(&mut grads, teto);
387
  opt.passo(grads);
388
+ tok.invalidar_caches(); // A43: codebook do novo passo
389
  // EMA do codebook (modo ste_ema)
390
  if cfg.ncm.modo_quantizacao == "ste_ema" {
391
  let cod = tok.codificar_congelado(&ids)?;
392
  tok.atualizar_ema_codebook(&cod.ids)?;
393
  }
394
+ // telemetria: séries por passo + métricas do codificador
395
+ hub.registrar("perda", perda);
396
+ hub.registrar("rec", m.rec);
397
+ hub.registrar("commit", m.commit);
398
+ hub.registrar("lr", lr);
399
+ hub.registrar("norma_grad", norma);
400
+ for (i, &rr) in m.residuos_relativos.iter().enumerate() {
401
+ hub.registrar(&format!("contração_nivel_{i}"), rr);
402
+ }
403
  if passo % a_cada == 0 || passo == cfg.treino.passos_fase_a - 1 {
404
  let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::<f64>() / tok.quants.len().max(1) as f64;
405
  let dort: f64 = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max);
406
+ let residuo = GeradorConceitos::residuo_algebrico(&m.zhats, &dev)?;
407
  hub.registrar_multi(&[
408
  ("codebook_perplexidade_media", perp),
409
  ("delta_ort_max", dort),
410
+ ("residuo_recomposicao", residuo),
411
  ("escore_aprendizado", hub.escore_aprendizado()),
412
  ]);
413
+ // S-05: detecção de aprendizado com limiares + eventos
414
+ let estado = hub.detectar_aprendizado();
415
  if cfg.telemetria.deteccao_aprendizado {
416
  let snap = serde_json::json!({
417
  "fase": "A", "passo": passo, "perda": perda, "lr": lr,
418
  "codebook_perplexidade_media": perp, "delta_ort_max": dort,
419
+ "residuo_recomposicao": residuo,
420
  "escore": hub.escore_aprendizado(),
421
+ "estado_aprendizado": format!("{estado:?}"),
422
  });
423
  chamar_callback(callback, &snap);
424
  }
425
+ // checkpoint periódico (proteção contra kill em background)
426
+ if let Some(c) = caminho_checkpoint {
427
+ if cfg.treino.checkpoint_a_cada > 0 && passo % cfg.treino.checkpoint_a_cada == 0 && passo > passo_inicial {
428
+ tok.salvar(c)?;
429
+ salvar_estado_a(c, &opt, &ctrl, passo)?;
430
+ }
431
+ }
432
  }
433
+ hub.fechar_passo(); // M12: uma linha JSONL por passo
 
 
 
434
  }
435
+ if passos_feitos == passo_inicial { passos_feitos = cfg.treino.passos_fase_a; }
436
+ // avaliação final: monotonia de prefixo (V3) + perplexidade + perda FINAL
437
+ // comparável (R-05: mesmo lote fixo, mesmo RNG)
438
+ let perda_final = {
439
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1A7);
440
+ let (p, _) = tok.perda_fase_a(&lote_fixo, &mut r_fixo)?;
441
+ p.to_scalar::<f32>()? as f64
442
+ };
443
  let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
444
  let ces_prefixo = tok.ce_por_prefixo(&ids_val)?;
445
  let mut pares_ok = 0usize;
 
451
  }
452
  }
453
  let monotonia = pares_ok as f64 / pares.max(1) as f64;
 
454
  let perp: f64 = tok.quants.iter().map(|q| q.perplexidade_uso()).sum::<f64>() / tok.quants.len().max(1) as f64;
455
  let dort = tok.quants.iter().map(|q| q.delta_ort_ultimo).fold(0f64, f64::max);
456
  let acc = { let (_, m) = tok.perda_fase_a(&ids_val, &mut rng)?; m.acc_recon };
457
+ let zhats_val = tok.codificar_congelado(&ids_val)?.zhats;
458
+ let residuo = GeradorConceitos::residuo_algebrico(&zhats_val, &dev)?;
459
  if let Some(c) = caminho_checkpoint {
460
  tok.salvar(c)?;
461
+ salvar_estado_a(c, &opt, &ctrl, passos_feitos)?;
462
  }
463
  hub.registrar("passos_feitos", passos_feitos as f64);
464
+ hub.fechar_passo();
465
+ let estado = hub.detectar_aprendizado();
466
  Ok(ResumoFase {
467
  passos: passos_feitos,
468
  perda_final,
 
472
  perplexidade_codebook: perp,
473
  monotonia_prefixo: monotonia,
474
  escore_aprendizado: hub.escore_aprendizado(),
475
+ residuo_algebrico: residuo,
476
+ estado_aprendizado: format!("{estado:?}"),
477
  })
478
  }
479
 
480
+ fn salvar_estado_a(caminho: &str, opt: &AdamWManual, ctrl: &Controlador, passo: usize) -> Result<()> {
481
+ let est = EstadoTreino {
482
+ passo,
483
+ m: opt.m.clone(),
484
+ v: opt.v.clone(),
485
+ t: opt.t,
486
+ multiplicador_lr: ctrl.multiplicador,
487
+ minimo_ema: ctrl.minimo_ema,
488
+ passos_sem_melhora: ctrl.passos_sem_melhora,
489
+ clip: ctrl.clip,
490
+ epsilon: ctrl.epsilon_corrupcao,
491
+ lambda: ctrl.lambda_ancora.lambda,
492
+ jepa_vars: None,
493
+ som_prototipos: None,
494
+ som_lado: None,
495
+ };
496
+ EstadoTreino::salvar(&format!("{caminho}.estado.json"), &est)
497
+ }
498
+
499
  /// FASE B — gerador (n-04 §4): MLE exato sobre hierarquias congeladas (B.1),
500
+ /// corrupção κ + dropout CFG (C.3/D.2), MTP auxiliar (D.6), JEPA latente,
501
+ /// verificador algébrico (Teo G-1) e PCGrad entre as perdas (D.7).
502
+ /// v0.3.0: JEPA e SOM fazem parte do checkpoint de retomada (A22).
503
+ #[allow(clippy::too_many_arguments)]
504
  pub fn treinar_fase_b(
505
  cfg: &ConfigKhtst,
506
  tok: &TokenizadorConceitos,
 
509
  callback: &CallbackPy,
510
  caminho_checkpoint: Option<&str>,
511
  teto_seg: Option<f64>,
512
+ retomar: bool,
513
  ) -> Result<ResumoFase> {
514
  let dev = Device::Cpu;
515
  let inicio = std::time::Instant::now();
516
+ crate::rng::inicializar(cfg.treino.semente ^ 0xB); // A18: semente própria da fase B
517
  let mut ger = GeradorConceitos::novo(cfg, &dev)?;
518
+ let mut jepa = JepaLatente::novo(cfg.ncm.d_lat, &dev)?;
519
  let mut som = SomConceitos::novo(cfg.memoria.som_lado, cfg.ncm.d_lat, &dev)?;
520
  let mut vars = ger.vars();
521
  vars.extend(jepa.vars());
522
+ let n_vars_ger = ger.vars().len();
523
  let mut opt = AdamWManual::novo(vars.clone(), cfg.treino.lr * 0.7, cfg.treino.peso_decaimento);
524
  let mut ctrl = Controlador::novo(cfg.treino.lr * 0.7, cfg.treino.aquecimento, cfg.treino.passos_fase_b, cfg.telemetria.paciencia_plateau, cfg.telemetria.fator_lr_plateau, cfg.ncm.lambdas.rho_ancora);
525
+ let mut rng = StdRng::seed_from_u64(cfg.treino.semente ^ 0xB);
526
+ let mut passo_inicial = 0usize;
527
+ if let Some(c) = caminho_checkpoint {
528
+ if retomar {
529
+ let est_path = format!("{c}.estado.json");
530
+ if std::path::Path::new(&est_path).exists() && std::path::Path::new(c).exists() {
531
+ // o checkpoint contém APENAS as vars do GERADOR; o JEPA vem
532
+ // do .estado.json (jepa_vars) — não misturar as listas
533
+ {
534
+ let mut ger_vars = ger.vars();
535
+ carregar_vars_do_checkpoint(&mut ger_vars, c)?;
536
+ }
537
+ let est = EstadoTreino::carregar(&est_path)?;
538
+ opt.m = est.m;
539
+ opt.v = est.v;
540
+ opt.t = est.t;
541
+ ctrl.multiplicador = est.multiplicador_lr;
542
+ ctrl.minimo_ema = est.minimo_ema;
543
+ ctrl.passos_sem_melhora = est.passos_sem_melhora;
544
+ ctrl.clip = est.clip;
545
+ ctrl.epsilon_corrupcao = est.epsilon;
546
+ if let Some(jv) = &est.jepa_vars {
547
+ let jvars = jepa.vars();
548
+ for (i, val) in jv.iter().enumerate() {
549
+ if i < jvars.len() {
550
+ let forma = jvars[i].as_tensor().shape();
551
+ let t = Tensor::from_vec(val.clone(), forma.clone(), &dev)?.reshape(forma)?;
552
+ jvars[i].set(&t)?;
553
+ }
554
+ }
555
+ }
556
+ if let (Some(prot), Some(lado)) = (&est.som_prototipos, est.som_lado) {
557
+ som.restaurar(prot, lado)?;
558
+ }
559
+ passo_inicial = est.passo;
560
+ hub.ajuste("retomada", 0.0, passo_inicial as f64, &format!("fase B retomada do passo {passo_inicial}"));
561
+ }
562
+ }
563
+ }
564
  let a_cada = cfg.treino.telemetria_a_cada.max(1);
565
+ // lote fixo para métricas comparáveis (R-05)
566
+ let lote_fixo = {
567
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1B5);
568
+ dados.lote(cfg.treino.lote, &mut r_fixo, &dev)?
569
+ };
570
  let perda_inicial = {
571
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1B5);
572
+ let cod = tok.codificar_congelado(&lote_fixo)?;
573
+ let s = ger.perda_treino(tok, &cod.ids, &mut r_fixo)?;
574
  s.ce_por_nivel.iter().sum::<f64>() / s.ce_por_nivel.len() as f64
575
  };
576
+ let mut passos_feitos = passo_inicial;
577
+ 'laco_b: for passo in passo_inicial..cfg.treino.passos_fase_b {
578
  hub.passo_corrente = passo;
579
  if let Some(t) = teto_seg {
580
  if inicio.elapsed().as_secs_f64() > t {
 
586
  let cod = tok.codificar_congelado(&ids)?;
587
  let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?;
588
  let ce = saida.ce_por_nivel.iter().sum::<f64>() / saida.ce_por_nivel.len() as f64;
589
+ // M10: descarta passo não finito ANTES de aplicar
590
+ if !ce.is_finite() {
591
+ hub.contagem_nao_finitos += 1;
592
+ hub.erro("fase_b", &format!("CE não finita no passo {passo} — passo descartado (M10)"));
593
+ break 'laco_b;
594
+ }
595
+ // JEPA latente (alvo com stop-grad — contrato doc 30) com λ DINÂMICO
596
+ // (S-09: hipergradiente exato do Teo 29.2, integrado v0.3.0):
597
+ // λ_total = λ_base + λ_âncora(t), com λ_âncora adaptado pelo
598
+ // hipergradiente exato e clamped em [0, 2].
599
+ let lambda_jepa = (cfg.memoria.lambda_jepa + ctrl.lambda_ancora.lambda).clamp(0.0, 2.0);
600
+ let (perda_jepa_t, jepa_val) = {
601
  let b = ids.dim(0)?;
602
  let zhats = tok.reconstruir_de_codigos(&cod.ids, cod.ids.len(), b, &dev)?;
603
  let mut soma = None;
 
609
  soma = Some(match soma { Some(s) => (&s + &pj)?, None => pj });
610
  }
611
  match soma {
612
+ Some(s) => {
613
+ let esc = s.affine(lambda_jepa / zhats.len().max(1) as f64, 0.0)?;
614
+ let v = s.to_scalar::<f32>()? as f64;
615
+ (esc, v)
616
+ }
617
+ None => (Tensor::zeros((), candle_core::DType::F32, &dev)?, 0.0),
618
  }
619
  };
620
+ // perdas separadas para PCGrad: [CE+MTP, JEPA, L_alg (Teo G-1)]
621
  let mut grads_lista = Vec::new();
622
  grads_lista.push(grads_achatados(&saida.perda, &vars)?);
623
  grads_lista.push(grads_achatados(&perda_jepa_t, &vars)?);
624
+ if let Some(pa) = &saida.perda_alg {
625
+ if ger.lambda_alg > 0.0 {
626
+ grads_lista.push(grads_achatados(&pa.affine(ger.lambda_alg, 0.0)?, &vars)?);
627
+ }
628
+ }
629
  let mut combinado = pcgrad_achatado(grads_lista);
630
  let norma = AdamWManual::norma_global(&combinado);
631
  ctrl.registrar_norma(norma);
 
634
  let teto = ctrl.clip_corrente(hub);
635
  AdamWManual::clipar(&mut combinado, teto);
636
  opt.passo(combinado);
637
+ // S-09: hipergradiente do λ (L_diff = valor corrente da JEPA)
638
+ let _ = ctrl.passo_lambda(jepa_val, hub);
639
+ // S-08: calibração do ε com a taxa de erro medida (1 − acc)
640
+ if passo % a_cada == 0 {
641
+ let _ = ctrl.calibrar_epsilon(1.0 - saida.acuracia_media, hub);
642
+ }
643
  // SOM pós-geração (stop-grad — Teo 17.4): insere vetores de nível 1
644
  if passo % a_cada == 0 {
645
  let b = ids.dim(0)?;
 
647
  let _eq = som.inserir(&zhats[0])?;
648
  hub.registrar("som_eq_medio", som.eq_medio);
649
  }
650
+ // telemetria: séries por passo (M12) + CE por nível (P-09)
651
+ hub.registrar("perda", ce);
652
+ hub.registrar("ce_mtp", saida.ce_mtp);
653
+ hub.registrar("acc_conceitos", saida.acuracia_media);
654
+ hub.registrar("lr", lr);
655
+ hub.registrar("norma_grad", norma);
656
+ hub.registrar("residuo_algebrico", saida.residuo_algebrico);
657
+ if ger.m_planos >= 2 {
658
+ hub.registrar("entropia_planos", saida.entropia_planos);
659
+ }
660
+ for (i, &ce_l) in saida.ce_por_nivel.iter().enumerate() {
661
+ hub.registrar(&format!("ce_nivel_{i}"), ce_l);
662
+ }
663
  if passo % a_cada == 0 || passo == cfg.treino.passos_fase_b - 1 {
664
  hub.registrar("escore_aprendizado", hub.escore_aprendizado());
665
+ let estado = hub.detectar_aprendizado();
666
  let snap = serde_json::json!({
667
  "fase": "B", "passo": passo, "perda": ce, "ce_mtp": saida.ce_mtp,
668
  "acc_conceitos": saida.acuracia_media, "lr": lr,
669
+ "residuo_algebrico": saida.residuo_algebrico,
670
  "escore": hub.escore_aprendizado(),
671
+ "estado_aprendizado": format!("{estado:?}"),
672
  });
673
  chamar_callback(callback, &snap);
674
+ if let Some(c) = caminho_checkpoint {
675
+ if cfg.treino.checkpoint_a_cada > 0 && passo % cfg.treino.checkpoint_a_cada == 0 && passo > passo_inicial {
676
+ ger.salvar(c)?;
677
+ salvar_estado_b(c, &opt, &ctrl, passo, &jepa, &som, n_vars_ger)?;
678
+ }
679
+ }
680
  }
681
+ hub.fechar_passo(); // M12
 
 
 
682
  }
683
+ if passos_feitos == passo_inicial { passos_feitos = cfg.treino.passos_fase_b; }
684
+ // avaliação final COMPARÁVEL (R-05): mesmo lote fixo e RNG
685
+ let perda_final = {
686
+ let mut r_fixo = StdRng::seed_from_u64(cfg.treino.semente ^ 0xF1B5);
687
+ let cod = tok.codificar_congelado(&lote_fixo)?;
688
+ let s = ger.perda_treino(tok, &cod.ids, &mut r_fixo)?;
689
+ s.ce_por_nivel.iter().sum::<f64>() / s.ce_por_nivel.len() as f64
690
+ };
691
  let ids_val = dados.lote(cfg.treino.lote, &mut rng, &dev)?;
692
  let cod = tok.codificar_congelado(&ids_val)?;
693
  let saida = ger.perda_treino(tok, &cod.ids, &mut rng)?;
 
694
  if let Some(c) = caminho_checkpoint {
695
  ger.salvar(c)?;
696
+ salvar_estado_b(c, &opt, &ctrl, passos_feitos, &jepa, &som, n_vars_ger)?;
697
  }
698
+ let estado = hub.detectar_aprendizado();
699
  Ok(ResumoFase {
700
+ passos: passos_feitos,
701
  perda_final,
702
  perda_inicial,
703
  acc_recon: saida.acuracia_media,
 
705
  perplexidade_codebook: 0.0,
706
  monotonia_prefixo: 0.0,
707
  escore_aprendizado: hub.escore_aprendizado(),
708
+ residuo_algebrico: saida.residuo_algebrico,
709
+ estado_aprendizado: format!("{estado:?}"),
710
  })
711
  }
712
+
713
+ #[allow(clippy::too_many_arguments)]
714
+ fn salvar_estado_b(
715
+ caminho: &str,
716
+ opt: &AdamWManual,
717
+ ctrl: &Controlador,
718
+ passo: usize,
719
+ jepa: &JepaLatente,
720
+ som: &SomConceitos,
721
+ n_vars_ger: usize,
722
+ ) -> Result<()> {
723
+ let jepa_vars: Vec<Vec<f32>> = jepa
724
+ .vars()
725
+ .iter()
726
+ .map(|v| v.as_tensor().flatten_all().unwrap().to_vec1::<f32>().unwrap())
727
+ .collect();
728
+ let est = EstadoTreino {
729
+ passo,
730
+ m: opt.m.clone(),
731
+ v: opt.v.clone(),
732
+ t: opt.t,
733
+ multiplicador_lr: ctrl.multiplicador,
734
+ minimo_ema: ctrl.minimo_ema,
735
+ passos_sem_melhora: ctrl.passos_sem_melhora,
736
+ clip: ctrl.clip,
737
+ epsilon: ctrl.epsilon_corrupcao,
738
+ lambda: ctrl.lambda_ancora.lambda,
739
+ jepa_vars: Some(jepa_vars),
740
+ som_prototipos: Some(som.prototipos_flat()),
741
+ som_lado: Some(som.lado()),
742
+ };
743
+ let _ = n_vars_ger;
744
+ EstadoTreino::salvar(&format!("{caminho}.estado.json"), &est)
745
+ }
746
+
747
+ /// Carrega variáveis de um checkpoint com manifesto (A20: validação estrita
748
+ /// de contagem e forma — variável ausente ou forma trocada é ERRO, nunca
749
+ /// silêncio).
750
+ pub fn carregar_vars_do_checkpoint(vars: &mut [Var], caminho: &str) -> Result<()> {
751
+ let txt = std::fs::read_to_string(caminho).map_err(|e| candle_core::Error::Msg(format!("{e}")))?;
752
+ let v: serde_json::Value = serde_json::from_str(&txt).map_err(|e| candle_core::Error::Msg(format!("{e}")))?;
753
+ let obj = v.as_object().ok_or_else(|| candle_core::Error::Msg("formato de checkpoint inválido".into()))?;
754
+ // A20: valida o manifesto
755
+ if let Some(man) = obj.get("__manifesto") {
756
+ let n_vars = man["n_vars"].as_u64().unwrap_or(0) as usize;
757
+ if n_vars != vars.len() {
758
+ return Err(candle_core::Error::Msg(format!(
759
+ "checkpoint incompatível: manifesto declara {n_vars} vars, modelo tem {} (config diferente?)",
760
+ vars.len()
761
+ )));
762
+ }
763
+ if let Some(formas) = man["formas"].as_array() {
764
+ for (i, f) in formas.iter().enumerate() {
765
+ let esp: Vec<usize> = f.as_array().map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect()).unwrap_or_default();
766
+ let real = vars[i].as_tensor().dims().to_vec();
767
+ if esp != real {
768
+ return Err(candle_core::Error::Msg(format!(
769
+ "checkpoint incompatível: v{i} tem forma {:?}, modelo espera {:?}",
770
+ esp, real
771
+ )));
772
+ }
773
+ }
774
+ }
775
+ }
776
+ let dev = Device::Cpu;
777
+ for (i, var) in vars.iter().enumerate() {
778
+ let entrada = obj
779
+ .get(&format!("v{i}"))
780
+ .ok_or_else(|| candle_core::Error::Msg(format!("checkpoint incompleto: falta v{i}")))?;
781
+ let forma: Vec<usize> = entrada["forma"]
782
+ .as_array()
783
+ .map(|a| a.iter().filter_map(|x| x.as_u64().map(|y| y as usize)).collect())
784
+ .unwrap_or_default();
785
+ let dados: Vec<f32> = entrada["dados"]
786
+ .as_array()
787
+ .map(|a| a.iter().filter_map(|x| x.as_f64().map(|y| y as f32)).collect())
788
+ .unwrap_or_default();
789
+ if forma.is_empty() || dados.is_empty() {
790
+ return Err(candle_core::Error::Msg(format!("checkpoint: v{i} vazio")));
791
+ }
792
+ let esperado = forma.iter().product::<usize>();
793
+ if dados.len() != esperado {
794
+ return Err(candle_core::Error::Msg(format!(
795
+ "checkpoint: v{i} tem {} elementos, forma {:?} exige {}",
796
+ dados.len(), forma, esperado
797
+ )));
798
+ }
799
+ let t2 = Tensor::from_vec(dados, forma.clone(), &dev).and_then(|t| t.reshape(forma))?;
800
+ var.set(&t2)?;
801
+ }
802
+ Ok(())
803
+ }
804
+
805
+ /// HashMap de compatibilidade (não usado no caminho quente).
806
+ pub type MapaEstado = HashMap<String, f64>;
src/tronco.rs CHANGED
@@ -3,10 +3,33 @@
3
  //! com a restrição honesta H_D1 (bidirecionalidade intra-nível nas camadas de
4
  //! atenção; a camada recorrente é causal).
5
  //!
6
- //! Sequência do gerador: s = [cond; lev_1; …; lev_L] com entradas
7
- //! ẑ_{ℓ−1}^{(n_ℓ)} + e_ℓ (reconstrução cumulativa interpolada + embedding de
8
- //! nível — Teo D.1). Treino: 1 passada com teacher forcing (D.1(iv));
9
- //! inferência: bloco a bloco com KV-cache (D.1(iii), T2).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  use candle_core::{Device, Result, Tensor, Var};
12
  use crate::atencao::{AncoraNivel, BlocoAdba};
@@ -14,37 +37,60 @@ pub use crate::atencao::CacheKv;
14
  use crate::camadas::{Embedding, Linear, RmsNorm};
15
  use crate::config::ConfigKhtst;
16
  use crate::ssd::BlocoSsd;
 
 
 
 
17
 
18
  pub struct Tronco {
19
  pub blocos_atencao: Vec<BlocoAdba>,
20
  pub bloco_ssd: BlocoSsd,
21
  pub norm_final: RmsNorm,
22
  pub ancora: Option<AncoraNivel>,
23
- /// gamma FiLM por nível (2*L+2: [escala, deslocamento] × nível) — ADBA.
24
  pub film_gamma: Option<Var>,
25
  pub film_beta: Option<Var>,
26
  pub n_niveis: usize,
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  impl Tronco {
30
  pub fn novo(cfg: &ConfigKhtst, dev: &Device) -> Result<Self> {
31
  let m = &cfg.modelo;
32
  let l = cfg.l_niveis();
33
  let adba = &cfg.adba;
 
 
34
  let mut blocos = Vec::with_capacity(m.n_camadas - 1);
35
  for _ in 0..(m.n_camadas - 1) {
36
  blocos.push(BlocoAdba::novo(
37
  m.d_modelo, m.n_cabecas, m.d_ff, l,
38
- adba.ativo, adba.reponderacao_nivel, adba.temperatura_nivel, dev,
39
  )?);
40
  }
41
- let ancora = if adba.ativo && adba.gamma_ancora > 0.0 {
42
  Some(AncoraNivel::nova(m.d_modelo, l, dev)?)
43
  } else {
44
  None
45
  };
46
- let (film_gamma, film_beta) = if adba.ativo {
47
- let g = Tensor::ones((l + 1, m.d_modelo), candle_core::DType::F32, dev)?;
 
 
48
  let b = Tensor::zeros((l + 1, m.d_modelo), candle_core::DType::F32, dev)?;
49
  (Some(Var::from_tensor(&g)?), Some(Var::from_tensor(&b)?))
50
  } else {
@@ -52,7 +98,7 @@ impl Tronco {
52
  };
53
  Ok(Tronco {
54
  blocos_atencao: blocos,
55
- bloco_ssd: BlocoSsd::novo(m.d_modelo, m.d_ff, dev)?,
56
  norm_final: RmsNorm::novo(m.d_modelo, 1e-5, dev)?,
57
  ancora,
58
  film_gamma,
@@ -63,6 +109,7 @@ impl Tronco {
63
 
64
  /// Injeção FiLM do resumo-âncora nas posições do nível `nivel`
65
  /// (ADBA (i): o resumo depende APENAS de c_{<ℓ} — paralelismo preservado).
 
66
  pub fn aplicar_film(&self, x: &Tensor, resumo: &Tensor, nivel: usize) -> Result<Tensor> {
67
  match (&self.film_gamma, &self.film_beta) {
68
  (Some(g), Some(b)) => {
@@ -71,7 +118,7 @@ impl Tronco {
71
  let gv = g.as_tensor().get(nivel)?.reshape((1, 1, d))?;
72
  let bv = b.as_tensor().get(nivel)?.reshape((1, 1, d))?;
73
  let r = resumo.reshape((bsz, 1, d))?;
74
- // x ⊙ (1 + γ·tanh(r)) + β·r — neutro em γ=β=0 (nascimento neutro)
75
  let gate = gv.broadcast_mul(&r.tanh()?)?.affine(1.0, 1.0)?; // 1 + γ·tanh(r)
76
  let xg = x.broadcast_mul(&gate)?;
77
  let desloc = bv.broadcast_mul(&r)?;
@@ -81,43 +128,50 @@ impl Tronco {
81
  }
82
  }
83
 
84
- /// Forward de treino: sequência completa s (b, T, d) com máscara aditiva
85
- /// (T, T) e mapa de níveis por posição (query e chave). 1 passada — D.1(iv).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  pub fn forward_treino(
87
  &self,
88
  s: &Tensor,
89
  mascara: &Tensor,
90
  niveis: &[usize],
91
- resumos_por_nivel: &mut Vec<Tensor>,
92
  ) -> Result<Tensor> {
93
  let b = s.dim(0)?;
94
  let mut x = s.clone();
95
  for (i, bloco) in self.blocos_atencao.iter().enumerate() {
96
  let mut cache = CacheKv::default();
97
  x = bloco.fwd(&x, Some(mascara), &mut cache, Some(niveis), Some(niveis))?;
98
- // após o 1º bloco: injeta o resumo-âncora por nível (ADBA)
 
 
99
  if i == 0 {
100
- if let Some(anc) = &self.ancora {
101
- resumos_por_nivel.clear();
102
  let n_prefixo = x.dims()[1] - self.tamanhos_por_nivel(niveis).iter().sum::<usize>();
103
- // prefixo entra inalterado (sem FiLM — nível 0)
104
  let mut partes = vec![x.narrow(1, 0, n_prefixo)?];
105
  let mut inicio = n_prefixo;
106
  for (nivel, &n_n) in self.tamanhos_por_nivel(niveis).iter().enumerate() {
107
  let trecho = x.narrow(1, inicio, n_n)?;
108
- // resumo do CONTEXTO ANTERIOR (níveis < atual): usa as
109
- // posições já processadas (função de c_{<ℓ} apenas)
110
- if inicio == n_prefixo {
111
- resumos_por_nivel.push(Tensor::zeros((b, s.dims()[2]), candle_core::DType::F32, s.device())?);
112
- } else {
113
- let ctx = x.narrow(1, 0, inicio)?;
114
- let mut r_nivel = Vec::new();
115
- for bi in 0..b {
116
- r_nivel.push(anc.resumir(&ctx.narrow(0, bi, 1)?, nivel)?);
117
- }
118
- resumos_por_nivel.push(Tensor::cat(&r_nivel, 0)?);
119
- }
120
- let r = resumos_por_nivel[nivel].clone();
121
  partes.push(self.aplicar_film(&trecho, &r, nivel + 1)?);
122
  inicio += n_n;
123
  }
@@ -125,31 +179,69 @@ impl Tronco {
125
  }
126
  }
127
  }
 
128
  let mut cache_ssd = None;
129
  x = self.bloco_ssd.fwd(&x, &mut cache_ssd)?;
130
  self.norm_final.fwd(&x)
131
  }
132
 
133
- /// Forward incremental (inferência): bloco novo (b, n_novo, d) + caches.
134
- /// `niveis_novos`/`niveis_cache` mapeiam posições para níveis.
 
 
 
 
 
 
135
  pub fn forward_nivel(
136
  &self,
137
  bloco_x: &Tensor,
138
- caches: &mut Vec<CacheKv>,
139
  mascara_nova: &Tensor,
 
140
  niveis_novos: &[usize],
141
  niveis_totais: &[usize],
142
  ) -> Result<Tensor> {
 
143
  let mut x = bloco_x.clone();
144
  for (i, bloco) in self.blocos_atencao.iter().enumerate() {
145
- if caches.len() <= i {
146
- caches.push(CacheKv::default());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
- x = bloco.fwd(&x, Some(mascara_nova), &mut caches[i], Some(niveis_novos), Some(niveis_totais))?;
149
- let _ = i;
150
  }
151
- let mut cache_ssd = None;
152
- x = self.bloco_ssd.fwd(&x, &mut cache_ssd)?;
153
  self.norm_final.fwd(&x)
154
  }
155
 
@@ -200,6 +292,214 @@ impl CabecaConceitos {
200
  }
201
  }
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  /// Embeddings de nível e_ℓ (T1 via A).
204
  pub struct EmbNiveis {
205
  pub tabela: Embedding,
 
3
  //! com a restrição honesta H_D1 (bidirecionalidade intra-nível nas camadas de
4
  //! atenção; a camada recorrente é causal).
5
  //!
6
+ //! ## v0.2.0 — paridade treino↔inferência (M2/M3/M4 da auditoria r-08)
7
+ //!
8
+ //! **M2**: a v0.1.11 aplicava âncora/FiLM SÓ em `forward_treino` — o modelo
9
+ //! era treinado com uma função e avaliado com outra (Δ = 2.89 no nível 1,
10
+ //! 107% de ‖h‖). Agora `forward_nivel` mantém um **buffer pós-bloco-0**
11
+ //! (`EstadoNivel::buffer_bloco0`) com as saídas do bloco 0 de TODAS as
12
+ //! posições passadas — exatamente a grandeza sobre a qual a âncora pooled no
13
+ //! treino — e injeta âncora+FiLM no caminho incremental. O estado do SSD
14
+ //! também é transportado (**M4**) via `EstadoNivel::cache_ssd`.
15
+ //!
16
+ //! **Teo T-1 (paridade estrutural).** Seja `s = [prefixo; lev_1; …; lev_L]` e
17
+ //! definam-se `f_treino(s)` (uma passada com máscara block-causal) e
18
+ //! `f_inc(ℓ)` (passos incrementais com caches). Com (a) máscara incremental
19
+ //! correta (prefixo causal na primeira janela), (b) âncora lendo o buffer
20
+ //! pós-bloco-0 das posições `[0, início_ℓ)` e (c) estado SSD transportado,
21
+ //! vale `f_treino(s)[:, nível_ℓ] = f_inc(ℓ)` para todo ℓ, a menos de erro
22
+ //! de ponto flutuante (ordem de soma no softmax) ≤ 1e-6 relativo.
23
+ //! *Prova (esboço formal em r-09 §2):* cada bloco é uma função local das
24
+ //! posições visíveis; o KV-cache reproduz exatamente os k/v que o bloco
25
+ //! computaria na passada completa (mesmas entradas ⇒ mesmos k/v); a âncora
26
+ //! depende apenas do buffer pós-bloco-0 acumulado (idêntico nos dois caminhos
27
+ //! por indução); o scan SSD com estado transportado particiona a mesma
28
+ //! recorrência (gate G3). ∎ (gate G2 verifica por execução.)
29
+ //!
30
+ //! **M3**: `film_gamma` inicia em **0** (a v0.1.11 iniciava em 1 ⇒
31
+ //! `x̃ = x⊙(1+tanh(r)) ≠ x` — nascimento neutro FALSO). Com γ=0, β=0:
32
+ //! `x̃ = x⊙(1+0·tanh(r)) + 0·r = x` — identidade exata no nascimento.
33
 
34
  use candle_core::{Device, Result, Tensor, Var};
35
  use crate::atencao::{AncoraNivel, BlocoAdba};
 
37
  use crate::camadas::{Embedding, Linear, RmsNorm};
38
  use crate::config::ConfigKhtst;
39
  use crate::ssd::BlocoSsd;
40
+ use rand::prelude::*;
41
+
42
+ /// Comprimento do prefixo de condicionamento (|cond| do T2).
43
+ pub const N_PREFIXO: usize = 8;
44
 
45
  pub struct Tronco {
46
  pub blocos_atencao: Vec<BlocoAdba>,
47
  pub bloco_ssd: BlocoSsd,
48
  pub norm_final: RmsNorm,
49
  pub ancora: Option<AncoraNivel>,
50
+ /// gamma FiLM por nível (L+1, d) — init 0 (M3: nascimento neutro).
51
  pub film_gamma: Option<Var>,
52
  pub film_beta: Option<Var>,
53
  pub n_niveis: usize,
54
  }
55
 
56
+ /// Estado incremental (inferência): caches KV por bloco + estado SSD +
57
+ /// buffer pós-bloco-0 (M2/M4).
58
+ pub struct EstadoNivel {
59
+ pub caches: Vec<CacheKv>,
60
+ pub cache_ssd: Option<Tensor>,
61
+ /// saídas do bloco 0 de todas as posições já processadas (b, T_acum, d)
62
+ pub buffer_bloco0: Option<Tensor>,
63
+ }
64
+
65
+ impl EstadoNivel {
66
+ pub fn novo() -> Self {
67
+ EstadoNivel { caches: Vec::new(), cache_ssd: None, buffer_bloco0: None }
68
+ }
69
+ }
70
+
71
  impl Tronco {
72
  pub fn novo(cfg: &ConfigKhtst, dev: &Device) -> Result<Self> {
73
  let m = &cfg.modelo;
74
  let l = cfg.l_niveis();
75
  let adba = &cfg.adba;
76
+ // dupla trava T10: ncm.ativo=false força ADBA desligada
77
+ let adba_ativo = adba.ativo && cfg.ncm.ativo;
78
  let mut blocos = Vec::with_capacity(m.n_camadas - 1);
79
  for _ in 0..(m.n_camadas - 1) {
80
  blocos.push(BlocoAdba::novo(
81
  m.d_modelo, m.n_cabecas, m.d_ff, l,
82
+ adba_ativo, adba.reponderacao_nivel, adba.temperatura_nivel, dev,
83
  )?);
84
  }
85
+ let ancora = if adba_ativo && adba.gamma_ancora > 0.0 {
86
  Some(AncoraNivel::nova(m.d_modelo, l, dev)?)
87
  } else {
88
  None
89
  };
90
+ let (film_gamma, film_beta) = if adba_ativo {
91
+ // M3: γ = 0 no nascimento (identidade exata; aprendível pois
92
+ // ∂x̃/∂γ = x⊙tanh(r) ≠ 0)
93
+ let g = Tensor::zeros((l + 1, m.d_modelo), candle_core::DType::F32, dev)?;
94
  let b = Tensor::zeros((l + 1, m.d_modelo), candle_core::DType::F32, dev)?;
95
  (Some(Var::from_tensor(&g)?), Some(Var::from_tensor(&b)?))
96
  } else {
 
98
  };
99
  Ok(Tronco {
100
  blocos_atencao: blocos,
101
+ bloco_ssd: BlocoSsd::novo(m.d_modelo, m.d_ff, m.ssd_delta && cfg.ncm.ativo, dev)?,
102
  norm_final: RmsNorm::novo(m.d_modelo, 1e-5, dev)?,
103
  ancora,
104
  film_gamma,
 
109
 
110
  /// Injeção FiLM do resumo-âncora nas posições do nível `nivel`
111
  /// (ADBA (i): o resumo depende APENAS de c_{<ℓ} — paralelismo preservado).
112
+ /// `nivel` é 1-based (1 = primeiro nível de conceitos).
113
  pub fn aplicar_film(&self, x: &Tensor, resumo: &Tensor, nivel: usize) -> Result<Tensor> {
114
  match (&self.film_gamma, &self.film_beta) {
115
  (Some(g), Some(b)) => {
 
118
  let gv = g.as_tensor().get(nivel)?.reshape((1, 1, d))?;
119
  let bv = b.as_tensor().get(nivel)?.reshape((1, 1, d))?;
120
  let r = resumo.reshape((bsz, 1, d))?;
121
+ // x ⊙ (1 + γ·tanh(r)) + β·r — neutro em γ=β=0 (M3)
122
  let gate = gv.broadcast_mul(&r.tanh()?)?.affine(1.0, 1.0)?; // 1 + γ·tanh(r)
123
  let xg = x.broadcast_mul(&gate)?;
124
  let desloc = bv.broadcast_mul(&r)?;
 
128
  }
129
  }
130
 
131
+ /// Resumo-âncora do nível `nivel` (1-based) dado o contexto pós-bloco-0
132
+ /// (b, início_ℓ, d) — BATCHED (M17). Nível 1: contexto = prefixo.
133
+ fn resumo_nivel(&self, ctx_bloco0: &Tensor, nivel: usize) -> Result<Tensor> {
134
+ match &self.ancora {
135
+ Some(anc) => anc.resumir(ctx_bloco0, nivel - 1),
136
+ None => Ok(Tensor::zeros(
137
+ (ctx_bloco0.dim(0)?, ctx_bloco0.dim(2)?),
138
+ candle_core::DType::F32,
139
+ ctx_bloco0.device(),
140
+ )?),
141
+ }
142
+ }
143
+
144
+ /// Forward de TREINO: sequência completa s (b, T, d) com máscara aditiva
145
+ /// (T, T) e mapa de níveis por posição. 1 passada — D.1(iv).
146
+ /// O SSD processa a sequência inteira num único scan (≡ transporte de
147
+ /// estado entre níveis — gate G3).
148
  pub fn forward_treino(
149
  &self,
150
  s: &Tensor,
151
  mascara: &Tensor,
152
  niveis: &[usize],
153
+ _resumos_por_nivel: &mut Vec<Tensor>,
154
  ) -> Result<Tensor> {
155
  let b = s.dim(0)?;
156
  let mut x = s.clone();
157
  for (i, bloco) in self.blocos_atencao.iter().enumerate() {
158
  let mut cache = CacheKv::default();
159
  x = bloco.fwd(&x, Some(mascara), &mut cache, Some(niveis), Some(niveis))?;
160
+ // após o 1º bloco: injeta o resumo-âncora por nível (ADBA) —
161
+ // a âncora lê o PÓS-BLOCO-0 das posições anteriores (M2: mesma
162
+ // grandeza que o caminho incremental lê do buffer)
163
  if i == 0 {
164
+ if let Some(_) = &self.ancora {
165
+ _resumos_por_nivel.clear();
166
  let n_prefixo = x.dims()[1] - self.tamanhos_por_nivel(niveis).iter().sum::<usize>();
 
167
  let mut partes = vec![x.narrow(1, 0, n_prefixo)?];
168
  let mut inicio = n_prefixo;
169
  for (nivel, &n_n) in self.tamanhos_por_nivel(niveis).iter().enumerate() {
170
  let trecho = x.narrow(1, inicio, n_n)?;
171
+ // resumo do CONTEXTO ANTERIOR (prefixo + níveis < atual)
172
+ let ctx = x.narrow(1, 0, inicio)?;
173
+ let r = self.resumo_nivel(&ctx, nivel + 1)?;
174
+ _resumos_por_nivel.push(r.clone());
 
 
 
 
 
 
 
 
 
175
  partes.push(self.aplicar_film(&trecho, &r, nivel + 1)?);
176
  inicio += n_n;
177
  }
 
179
  }
180
  }
181
  }
182
+ let _ = b;
183
  let mut cache_ssd = None;
184
  x = self.bloco_ssd.fwd(&x, &mut cache_ssd)?;
185
  self.norm_final.fwd(&x)
186
  }
187
 
188
+ /// Forward incremental (inferência): bloco novo (b, n_novo, d) + estado.
189
+ /// `nivel` é 1-based. Paridade com forward_treino (Teo T-1, gate G2).
190
+ ///
191
+ /// Correções de paridade (descobertas pelo gate G2):
192
+ /// (i) no 1º nível o bloco contém [prefixo; lev_1] — o FiLM aplica só às
193
+ /// posições DO NÍVEL (o prefixo nunca é filmado, como no treino);
194
+ /// (ii) o contexto da âncora do nível 1 é o PREFIXO pós-bloco-0 (no
195
+ /// treino a âncora de ℓ=1 pooling sobre x[:, :P]; não o bloco vazio).
196
  pub fn forward_nivel(
197
  &self,
198
  bloco_x: &Tensor,
199
+ estado: &mut EstadoNivel,
200
  mascara_nova: &Tensor,
201
+ nivel: usize,
202
  niveis_novos: &[usize],
203
  niveis_totais: &[usize],
204
  ) -> Result<Tensor> {
205
+ let n_novo = bloco_x.dim(1)?;
206
  let mut x = bloco_x.clone();
207
  for (i, bloco) in self.blocos_atencao.iter().enumerate() {
208
+ if estado.caches.len() <= i {
209
+ estado.caches.push(CacheKv::default());
210
+ }
211
+ x = bloco.fwd(&x, Some(mascara_nova), &mut estado.caches[i], Some(niveis_novos), Some(niveis_totais))?;
212
+ // M2: após o bloco 0 — acumula o buffer e injeta âncora/FiLM
213
+ if i == 0 {
214
+ if self.ancora.is_some() || self.film_gamma.is_some() {
215
+ let primeira_chamada = estado.buffer_bloco0.is_none();
216
+ let novo_b0 = x.clone();
217
+ // contexto da âncora: níveis anteriores (buffer) ou, no
218
+ // 1º nível, o PREFIXO pós-bloco-0 do bloco corrente
219
+ let ctx = match &estado.buffer_bloco0 {
220
+ Some(buf) => buf.clone(),
221
+ None => novo_b0.narrow(1, 0, N_PREFIXO.min(n_novo))?,
222
+ };
223
+ let r = self.resumo_nivel(&ctx, nivel)?;
224
+ // FiLM apenas nas posições do NÍVEL (prefixo intacto)
225
+ let n_pref_aqui = if primeira_chamada { N_PREFIXO.min(n_novo) } else { 0 };
226
+ let x_filmado = if n_pref_aqui > 0 && n_novo > n_pref_aqui {
227
+ let pref = novo_b0.narrow(1, 0, n_pref_aqui)?;
228
+ let corpo = novo_b0.narrow(1, n_pref_aqui, n_novo - n_pref_aqui)?;
229
+ let corpo_f = self.aplicar_film(&corpo, &r, nivel)?;
230
+ Tensor::cat(&[pref, corpo_f], 1)?
231
+ } else {
232
+ self.aplicar_film(&novo_b0, &r, nivel)?
233
+ };
234
+ // buffer acumula o pós-bloco-0 PRÉ-FiLM (mesma grandeza do treino)
235
+ estado.buffer_bloco0 = Some(match &estado.buffer_bloco0 {
236
+ Some(buf) => Tensor::cat(&[buf, &novo_b0], 1)?,
237
+ None => novo_b0.clone(),
238
+ });
239
+ x = x_filmado;
240
+ }
241
  }
 
 
242
  }
243
+ // M4: estado do SSD transportado entre níveis
244
+ x = self.bloco_ssd.fwd(&x, &mut estado.cache_ssd)?;
245
  self.norm_final.fwd(&x)
246
  }
247
 
 
292
  }
293
  }
294
 
295
+ /// ## v0.3.0 — CABEÇA DE MISTURA DE PLANOS (aprimoramento "raciocínio em
296
+ /// subníveis paralelos"; método B.3 da proposta C)
297
+ ///
298
+ /// p(c_ℓ|c_{<ℓ}) = Σ_{m=1}^{M} w_m · Π_{i=1}^{n_ℓ} q_{m,i}(c_{ℓ,i})
299
+ ///
300
+ /// com w = softmax(U·h̄_ℓ) (h̄_ℓ = média das posições do nível — função de
301
+ /// c_{<ℓ} apenas) e q_{m,i} = softmax(W^{(m)}·h_{ℓ,i}).
302
+ ///
303
+ /// **Teo C-1 (propriedades).**
304
+ /// (i) *MLE exato*: a mistura é uma distribuição normalizada sobre [K]^{n_ℓ}
305
+ /// (Σ_c p = Σ_m w_m Π_i Σ_k q = Σ_m w_m = 1) ⇒ −log p é a negalogverossimilhança
306
+ /// exata, sem ELBO (Teo B.1 preservado).
307
+ /// (ii) *NFE = L* (Teo B.2 preservado): a cabeça não adiciona avaliação do
308
+ /// gerador.
309
+ /// (iii) *Amostragem paralela intra-nível*: sorteia-se 1 plano m ~ w e depois
310
+ /// as n_ℓ posições INDEPENDENTEMENTE de q_m — n_ℓ+1 sorteios, 0 passos
311
+ /// sequenciais (a propriedade central do NCM é mantida).
312
+ /// (iv) *Contenção estrita*: M=1 ⇒ exatamente a cabeça produto da v0.1.11
313
+ /// (w_1 = softmax(U h̄) = 1 pois é o único componente); com M ≥ 2 a família
314
+ /// contém estritamente os produtos — a marginal de duas posições é
315
+ /// Σ_m w_m q_{m,i} q_{m,j} ≠ (Σ_m w_m q_{m,i})(Σ'_m w_{m'} q_{m',j}) em geral
316
+ /// ⇒ CORRELAÇÃO intra-nível não-nula ⇒ o gap de correlação total condicional
317
+ /// TC(c_ℓ|c_{<ℓ}) — que é exatamente min KL(q‖p) sobre a família — é REDUZIDO.
318
+ /// *Prova da estrita contenção:* tome q_{1,i} ≠ q_{2,i} e w = (½,½): a
319
+ /// diagonal conjunta p(c_i = c_j) = Σ_m w_m q_{m,i}(k)q_{m,j}(k) < 1 enquanto
320
+ /// o produto de marginais dá Σ_k [½(q_{1,i}+q_{2,i})][½(q_{1,j}+q_{2,j})]·δ…
321
+ /// — em geral distintos; um produto de marginais NÃO reproduz p (contra-exemplo
322
+ /// numérico no gate G9). ∎
323
+ /// (v) *Custo*: O(M·n_ℓ·K) MACs por nível — batched como UM matmul
324
+ /// (n_ℓ, d) @ (d, M·K).
325
+ /// (vi) *Nascimento neutro*: M=1 reproduz a v0.1.11 bit-a-bit (gate G9).
326
+ pub struct CabecaMistura {
327
+ /// W empilhado: (d, M·K) — colunas [m·K .. (m+1)·K) = plano m.
328
+ pub w: Linear,
329
+ /// pesos dos planos: U (d, M) sobre a média do nível.
330
+ pub u: Linear,
331
+ pub m: usize,
332
+ pub k: usize,
333
+ }
334
+
335
+ impl CabecaMistura {
336
+ pub fn nova(d: usize, k: usize, m: usize, dev: &Device) -> Result<Self> {
337
+ Ok(CabecaMistura {
338
+ w: Linear::sem_vies(d, k * m.max(1), 0.02, dev)?,
339
+ u: Linear::sem_vies(d, m.max(1), 0.02, dev)?,
340
+ m: m.max(1),
341
+ k,
342
+ })
343
+ }
344
+
345
+ /// Logits por plano: h (…, d) → (…, M, K).
346
+ pub fn logits_planos(&self, h: &Tensor) -> Result<Tensor> {
347
+ let forma: Vec<usize> = h.dims().to_vec();
348
+ let plano = self.w.fwd(h)?; // (…, M·K)
349
+ let mut alvo = forma;
350
+ alvo.pop();
351
+ alvo.push(self.m);
352
+ alvo.push(self.k);
353
+ plano.reshape(alvo)
354
+ }
355
+
356
+ /// Pesos dos planos a partir da média do nível (função de c_{<ℓ} via h):
357
+ /// h (b, n_ℓ, d) → w (b, M) (softmax).
358
+ pub fn pesos_planos(&self, h: &Tensor) -> Result<Tensor> {
359
+ let media = h.mean(candle_core::D::Minus2)?; // (b, d)
360
+ let logits = self.u.fwd(&media)?; // (b, M)
361
+ // NB: softmax_last_dim do candle-nn 0.11 é apply_op1_no_bwd (SEM
362
+ // gradiente!) — usamos o softmax completo, que tem backward.
363
+ candle_nn::ops::softmax(&logits, candle_core::D::Minus1)
364
+ }
365
+
366
+ /// −log p(c_ℓ) exato (log-sum-exp sobre planos): devolve (perda Tensor
367
+ /// escalar, ce f64). `alvos`: (b·n_ℓ,) u32.
368
+ pub fn nll(
369
+ &self,
370
+ h: &Tensor,
371
+ alvos: &Tensor,
372
+ ) -> Result<(Tensor, f64)> {
373
+ let b = h.dim(0)?;
374
+ let n_l = h.dim(1)?;
375
+ let dev = h.device();
376
+ let w = self.pesos_planos(h)?; // (b, M)
377
+ let logits = self.logits_planos(h)?; // (b, n_l, M, K)
378
+ // log q_{m,i}(c_i): log_softmax no último eixo + gather dos alvos
379
+ let logq = candle_nn::ops::log_softmax(&logits, candle_core::D::Minus1)?; // (b,n,M,K)
380
+ let alvos_b = alvos.reshape((b, n_l))?.to_dtype(candle_core::DType::U32)?;
381
+ // índice por (b, n, m): broadcast do alvo — gather devolve (b,n,M,1)
382
+ let idx = alvos_b
383
+ .reshape((b, n_l, 1, 1))?
384
+ .broadcast_as((b, n_l, self.m, 1))?
385
+ .to_dtype(candle_core::DType::U32)?
386
+ .contiguous()?;
387
+ let escolhido = logq.gather(&idx, 3)?; // (b, n, M, 1)
388
+ // Σ_i log q_{m,i}(c_i): soma sobre as POSIÇÕES (eixo 1)
389
+ let soma = escolhido.sum(1)?.squeeze(2)?; // (b, M)
390
+ // log w_m
391
+ let logw = (&w + 1e-10f64)?.log()?; // (b, M)
392
+ let total = (&soma + &logw)?; // (b, M)
393
+ // logsumexp estável sobre M
394
+ let max = total.max(candle_core::D::Minus1)?; // (b,)
395
+ let max_b = max.unsqueeze(1)?.broadcast_as(total.shape())?; // (b, M)
396
+ let e = (&total - &max_b)?.exp()?; // (b, M)
397
+ let soma_e = e.sum(candle_core::D::Minus1)?; // (b,)
398
+ let lse = (&(&soma_e + 1e-10f64)?.log()? + &max)?; // (b,): log p(c_ℓ)
399
+ // NORMALIZADO POR POSIÇÃO (comparável ao CE da v0.1.11 — gate G9):
400
+ // nll = −(1/n_ℓ)·log p(c_ℓ). Com M=1: −(1/n_ℓ)Σ_i log q_i ≡ CE médio.
401
+ let nll = lse.affine(-1.0 / n_l as f64, 0.0)?.mean_all()?;
402
+ let ce = nll.to_scalar::<f32>()? as f64;
403
+ Ok((nll, ce))
404
+ }
405
+
406
+ /// Amostragem paralela (Teo C-1(iii)): 1 sorteio de plano por elemento do
407
+ /// lote + n_ℓ sorteios independentes. Devolve ids (b, n_l) e o plano (b,).
408
+ pub fn amostrar(
409
+ &self,
410
+ h: &Tensor,
411
+ rng: &mut rand::prelude::StdRng,
412
+ greedy: bool,
413
+ ) -> Result<(Tensor, Vec<usize>)> {
414
+ let b = h.dim(0)?;
415
+ let n_l = h.dim(1)?;
416
+ let dev = h.device();
417
+ let w = self.pesos_planos(h)?; // (b, M)
418
+ let wv = w.flatten_all()?.to_vec1::<f32>()?;
419
+ let mut planos = Vec::with_capacity(b);
420
+ for bi in 0..b {
421
+ if greedy {
422
+ let (mut melhor, mut mi) = (-1f32, 0usize);
423
+ for m in 0..self.m {
424
+ if wv[bi * self.m + m] > melhor {
425
+ melhor = wv[bi * self.m + m];
426
+ mi = m;
427
+ }
428
+ }
429
+ planos.push(mi);
430
+ } else {
431
+ let r: f32 = rng.gen();
432
+ let mut acc = 0f32;
433
+ let mut esc = self.m - 1;
434
+ for m in 0..self.m {
435
+ acc += wv[bi * self.m + m];
436
+ if r <= acc {
437
+ esc = m;
438
+ break;
439
+ }
440
+ }
441
+ planos.push(esc);
442
+ }
443
+ }
444
+ // logits do plano escolhido por elemento do lote
445
+ let logits = self.logits_planos(h)?; // (b, n, M, K)
446
+ let mut ids = vec![0u32; b * n_l];
447
+ for bi in 0..b {
448
+ let m = planos[bi];
449
+ let trecho = logits
450
+ .narrow(0, bi, 1)?
451
+ .narrow(2, m, 1)?
452
+ .reshape((n_l, self.k))?;
453
+ let t = if greedy {
454
+ trecho.argmax(candle_core::D::Minus1)?
455
+ } else {
456
+ // amostra categoricamente
457
+ let probs = candle_nn::ops::softmax(&trecho, candle_core::D::Minus1)?;
458
+ let pv = probs.flatten_all()?.to_vec1::<f32>()?;
459
+ let mut v = vec![0u32; n_l];
460
+ for i in 0..n_l {
461
+ let r: f32 = rng.gen();
462
+ let mut acc = 0f32;
463
+ let mut esc = self.k - 1;
464
+ for (j, p) in pv[i * self.k..(i + 1) * self.k].iter().enumerate() {
465
+ acc += *p;
466
+ if r <= acc {
467
+ esc = j;
468
+ break;
469
+ }
470
+ }
471
+ v[i] = esc as u32;
472
+ }
473
+ Tensor::from_vec(v, (n_l,), dev)?
474
+ };
475
+ let tv = t.flatten_all()?.to_vec1::<u32>()?;
476
+ ids[bi * n_l..(bi + 1) * n_l].copy_from_slice(&tv);
477
+ }
478
+ Ok((Tensor::from_vec(ids, (b, n_l), dev)?, planos))
479
+ }
480
+
481
+ /// Logits "marginais" para CFG/telemetria: mistura das distribuições por
482
+ /// posição — E_m[q_{m,i}] (b, n_l, K). (Para amostragem usa-se o plano
483
+ /// sorteado; a marginal serve para medir entropia/τ.)
484
+ pub fn logits_marginais(&self, h: &Tensor) -> Result<Tensor> {
485
+ let w = self.pesos_planos(h)?; // (b, M)
486
+ let logits = self.logits_planos(h)?; // (b, n, M, K)
487
+ let probs = candle_nn::ops::softmax(&logits, candle_core::D::Minus1)?; // (b,n,M,K)
488
+ // média ponderada: (b,1,M,1) * (b,n,M,K) → soma em M
489
+ let w_r = w.reshape((w.dim(0)?, 1, self.m, 1))?;
490
+ let mistura = (&probs * &w_r.broadcast_as(probs.shape())?)?
491
+ .sum(candle_core::D::Minus2)?;
492
+ // devolve log da mistura como "logits" (estável)
493
+ (&mistura + 1e-10f64)?.log()
494
+ }
495
+
496
+ pub fn vars(&self) -> Vec<Var> {
497
+ let mut v = self.w.vars();
498
+ v.extend(self.u.vars());
499
+ v
500
+ }
501
+ }
502
+
503
  /// Embeddings de nível e_ℓ (T1 via A).
504
  pub struct EmbNiveis {
505
  pub tabela: Embedding,
tests/provas.rs ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! PROVAS EXECUTÁVEIS (gates) do KHTST_RUST v0.3.0.
2
+ //!
3
+ //! Regra da casa (r-08 §1): *contar a estrutura não é verificar a função* —
4
+ //! toda verificação precisa de um `assert` que FALHE se o invariante quebrar,
5
+ //! e invariantes sobre INFORMAÇÃO são medidos perturbando a entrada e
6
+ //! observando a resposta (Jacobiano direcional), não a representação.
7
+ //!
8
+ //! | gate | o que prova | onde |
9
+ //! |---|---|---|
10
+ //! | G1 | vazamento efetivo ZERO (nível 1 não muda quando o futuro muda) | aqui |
11
+ //! | G2 | paridade treino↔inferência (forward completo ≡ incremental) | aqui |
12
+ //! | G3 | estado SSD transportado (scan completo ≡ particionado) | ssd.rs |
13
+ //! | G4 | nenhuma linha totalmente bloqueada (M1) | mascaras.rs |
14
+ //! | G5 | nascimento neutro (γ=0 ⇒ FiLM identidade; ξ=ζ=0 ⇒ w=1, τ=1) | aqui + atencao.rs |
15
+ //! | G6 | orçamento do KV-cache inclui o prefixo | config.rs |
16
+ //! | G7 | reprodutibilidade bit-a-bit (mesma semente ⇒ mesmo init) | aqui |
17
+ //! | G8 | acumulação SSD-Δ: inteiros EXATOS, floats com cota (Teo S-1) | ssd.rs |
18
+ //! | G9 | mistura M=1 ≡ cabeça produto da v0.1.11 | aqui |
19
+ //! | G10 | adjunto SSD-Δ vs FD (inclui ∂L/∂h0, ∂L/∂η) | ssd.rs |
20
+ //! | G11 | adjunto do verificador algébrico vs FD (Teo G-1) | aqui |
21
+ //! | G12 | MLE exato da mistura (Σ_c p(c) = 1 sobre [K]^{n_ℓ}) | aqui |
22
+
23
+ use candle_core::{Device, Result, Tensor, Var, D};
24
+ use khtst_rust::config::ConfigKhtst;
25
+ use khtst_rust::tronco::CabecaMistura;
26
+ use std::sync::{Mutex, MutexGuard, OnceLock};
27
+
28
+ /// Os gates usam o RNG GLOBAL semeado — os testes rodam em paralelo por
29
+ /// padrão, então cada gate que constrói modelos segura esta trava para
30
+ /// ter exclusividade sobre o RNG (senão G7 falha por interferência).
31
+ fn trava_rng() -> MutexGuard<'static, ()> {
32
+ static T: OnceLock<Mutex<()>> = OnceLock::new();
33
+ // tolerante a envenenamento: um gate que panica não pode mascarar os demais
34
+ T.get_or_init(|| Mutex::new(()))
35
+ .lock()
36
+ .unwrap_or_else(|e| e.into_inner())
37
+ }
38
+
39
+ /// Config pequena e determinística para os gates de modelo inteiro.
40
+ fn cfg_gate() -> ConfigKhtst {
41
+ let mut cfg = ConfigKhtst::default();
42
+ cfg.modelo.vocab = 64;
43
+ cfg.modelo.d_modelo = 32;
44
+ cfg.modelo.n_camadas = 2; // 1 ADBA + 1 SSD
45
+ cfg.modelo.n_cabecas = 4;
46
+ cfg.modelo.d_ff = 64;
47
+ cfg.modelo.comprimento_ctx = 8;
48
+ cfg.modelo.kv_cache_max = 64;
49
+ cfg.ncm.niveis = vec![2, 4, 8];
50
+ cfg.ncm.d_lat = 8;
51
+ cfg.ncm.k_por_nivel = 8;
52
+ cfg.ncm.ativo = true;
53
+ cfg.ncm.mistura_planos = 1;
54
+ cfg.modelo.mtp.ativo = false;
55
+ cfg.ncm.corrupcao_cond = 0.0;
56
+ cfg.ncm.dropout_cfg = 0.0;
57
+ cfg.treino.semente = 4242;
58
+ cfg.validar().unwrap();
59
+ cfg
60
+ }
61
+
62
+ /// G1 — VAZAMENTO EFETIVO ZERO: corrompe os níveis > 1 (×999) e mede a
63
+ /// resposta ‖Δ‖∞ nas posições do nível 1. Na v0.1.11 este valor era 0,109
64
+ /// (110% do sinal — a fatoração next-concept estava quebrada); com a máscara
65
+ /// de prefixo causal (M1) deve ser ~0.
66
+ #[test]
67
+ fn gate_g1_vazamento_efetivo_zero() {
68
+ let _trava = trava_rng();
69
+ let cfg = cfg_gate();
70
+ let dif = khtst_rust::teste_vazamento_efetivo(&cfg).expect("teste de vazamento");
71
+ assert!(dif <= 1e-6, "G1: vazamento efetivo = {dif} (esperado 0)");
72
+ }
73
+
74
+ /// G2 — PARIDADE TREINO↔INFERÊNCIA: o forward completo (1 passada, máscara
75
+ /// block-causal) deve reproduzir o incremental (níveis com KV-cache, âncora
76
+ /// do buffer pós-bloco-0, estado SSD transportado). Na v0.1.11 o Δ era
77
+ /// 2,89 no nível 1 (a ADBA não existia na inferência).
78
+ #[test]
79
+ fn gate_g2_paridade_treino_inferencia() {
80
+ let _trava = trava_rng();
81
+ let cfg = cfg_gate();
82
+ let r = khtst_rust::teste_paridade(&cfg).expect("teste de paridade");
83
+ let dif_max = r["dif_max"].as_f64().unwrap();
84
+ assert!(dif_max < 1e-4, "G2: paridade treino↔inferência dif_max = {dif_max}");
85
+ let difs: Vec<f64> = r["difs_por_nivel"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
86
+ assert_eq!(difs.len(), 3, "um Δ por nível");
87
+ }
88
+
89
+ /// G5 — NASCIMENTO NEUTRO: com o tronco recém-construído (γ=0, β=0, ξ=0,
90
+ /// ζ=0), aplicar_film é a IDENTIDADE bit-a-bit, qualquer que seja o resumo.
91
+ #[test]
92
+ fn gate_g5_nascimento_neutro_film() -> Result<()> {
93
+ let _trava = trava_rng();
94
+ use khtst_rust::gerador::GeradorConceitos;
95
+ let cfg = cfg_gate();
96
+ khtst_rust::rng::inicializar(cfg.treino.semente);
97
+ let ger = GeradorConceitos::novo(&cfg, &Device::Cpu)?;
98
+ let dev = Device::Cpu;
99
+ let x = khtst_rust::rng::randn_semeado(1.0, &[2, 4, cfg.modelo.d_modelo], &dev)?;
100
+ let resumo = khtst_rust::rng::randn_semeado(1.0, &[2, cfg.modelo.d_modelo], &dev)?;
101
+ for nivel in 1..=cfg.l_niveis() {
102
+ let y = ger.tronco.aplicar_film(&x, &resumo, nivel)?;
103
+ let dif = (&x - &y)?.abs()?.max_all()?.to_scalar::<f32>()?;
104
+ assert_eq!(dif, 0.0, "G5: FiLM deve ser identidade no nascimento (nível {nivel})");
105
+ }
106
+ Ok(())
107
+ }
108
+
109
+ /// G7 — REPRODUTIBILIDADE: mesma semente ⇒ inicialização idêntica bit-a-bit
110
+ /// (A19: a v0.1.11 usava Tensor::randn = entropia do SO — irreproduzível).
111
+ #[test]
112
+ fn gate_g7_reprodutibilidade_bit_a_bit() -> Result<()> {
113
+ let _trava = trava_rng();
114
+ use khtst_rust::gerador::GeradorConceitos;
115
+ let cfg = cfg_gate();
116
+ khtst_rust::rng::inicializar(cfg.treino.semente);
117
+ let g1 = GeradorConceitos::novo(&cfg, &Device::Cpu)?;
118
+ let v1 = g1.vars();
119
+ khtst_rust::rng::inicializar(cfg.treino.semente);
120
+ let g2 = GeradorConceitos::novo(&cfg, &Device::Cpu)?;
121
+ let v2 = g2.vars();
122
+ assert_eq!(v1.len(), v2.len());
123
+ let mut dif_max = 0f32;
124
+ for (a, b) in v1.iter().zip(v2.iter()) {
125
+ let dif = a.as_tensor().sub(b.as_tensor())?.abs()?.max_all()?.to_scalar::<f32>()?;
126
+ dif_max = dif_max.max(dif);
127
+ }
128
+ assert_eq!(dif_max, 0.0, "G7: mesma semente deve dar init idêntico bit-a-bit");
129
+ Ok(())
130
+ }
131
+
132
+ /// G9 — MISTURA M=1 ≡ CABEÇA PRODUTO: com os MESMOS pesos W, o NLL da
133
+ /// mistura com 1 plano é o CE da cabeça produto da v0.1.11 (Teo C-1(vi)):
134
+ /// w_1 = softmax(logit único) = 1 ⇒ −log p = −Σ_i log q_i = CE.
135
+ #[test]
136
+ fn gate_g9_mistura_m1_igual_produto() -> Result<()> {
137
+ let _trava = trava_rng();
138
+ khtst_rust::rng::inicializar(99);
139
+ let dev = Device::Cpu;
140
+ let (d, k, n, b) = (16usize, 6, 5, 2);
141
+ let cab = CabecaMistura::nova(d, k, 1, &dev)?;
142
+ let h = khtst_rust::rng::randn_semeado(1.0, &[b, n, d], &dev)?;
143
+ let alvos = Tensor::from_vec(
144
+ (0..b * n).map(|i| (i * 7 % k) as u32).collect::<Vec<u32>>(),
145
+ (b * n,),
146
+ &dev,
147
+ )?;
148
+ // NLL da mistura (M=1)
149
+ let (nll, _ce) = cab.nll(&h, &alvos)?;
150
+ let nll_v = nll.to_scalar::<f32>()? as f64;
151
+ // CE do produto com o MESMO W: copia os pesos p/ uma Linear (k, d)
152
+ let w_lin = Var::from_tensor(&Tensor::zeros((k, d), candle_core::DType::F32, &dev)?)?;
153
+ w_lin.set(cab.w.w.as_tensor())?;
154
+ let logits = h.broadcast_matmul(&w_lin.as_tensor().transpose(0, 1)?.unsqueeze(0)?)?; // (b, n, k)
155
+ let plano = logits.reshape((b * n, k))?;
156
+ let ce = candle_nn::loss::cross_entropy(&plano, &alvos)?;
157
+ let ce_v = ce.to_scalar::<f32>()? as f64;
158
+ assert!(
159
+ (nll_v - ce_v).abs() < 1e-4,
160
+ "G9: NLL mistura M=1 ({nll_v}) deve ≡ CE produto ({ce_v})"
161
+ );
162
+ Ok(())
163
+ }
164
+
165
+ /// G12 — MLE EXATO DA MISTURA: Σ_{c ∈ [K]^{n}} p(c) = 1 (normalização
166
+ /// analítica — Teo C-1(i)), verificada por ENUMERAÇÃO exausta (K=3, n=3,
167
+ /// M=2 ⇒ 27 atribuições).
168
+ #[test]
169
+ fn gate_g12_mistura_normaliza_1() -> Result<()> {
170
+ let _trava = trava_rng();
171
+ khtst_rust::rng::inicializar(123);
172
+ let dev = Device::Cpu;
173
+ let (d, k, n, b, m) = (8usize, 3, 3, 1, 2);
174
+ let cab = CabecaMistura::nova(d, k, m, &dev)?;
175
+ let h = khtst_rust::rng::randn_semeado(1.0, &[b, n, d], &dev)?;
176
+ let w = cab.pesos_planos(&h)?; // (b, M)
177
+ let logits = cab.logits_planos(&h)?; // (b, n, M, K)
178
+ let wv = w.flatten_all()?.to_vec1::<f32>()?;
179
+ let logq = candle_nn::ops::log_softmax(&logits, D::Minus1)?;
180
+ let lqv = logq.flatten_all()?.to_vec1::<f32>()?;
181
+ let idx = |i: usize, mi: usize, kk: usize| ((i * m + mi) * k + kk);
182
+ // enumera todas as k^n atribuições
183
+ let mut soma_p = 0f64;
184
+ for c0 in 0..k {
185
+ for c1 in 0..k {
186
+ for c2 in 0..k {
187
+ let cs = [c0, c1, c2];
188
+ // p(c) = Σ_m w_m Π_i q_{m,i}(c_i)
189
+ let mut p = 0f64;
190
+ for mi in 0..m {
191
+ // log p(c | plano m) = ln w_m + Σ_i log q_{m,i}(c_i)
192
+ let mut lp = (wv[mi] as f64).ln();
193
+ for (i, &ci) in cs.iter().enumerate() {
194
+ lp += lqv[idx(i, mi, ci)] as f64;
195
+ }
196
+ p += lp.exp();
197
+ }
198
+ soma_p += p;
199
+ }
200
+ }
201
+ }
202
+ assert!(
203
+ (soma_p - 1.0).abs() < 1e-4,
204
+ "G12: Σ_c p(c) = {soma_p} (deve ser 1 — normalização exata)"
205
+ );
206
+ Ok(())
207
+ }
208
+
209
+ /// G11 — ADJUNTO DO VERIFICADOR ALGÉBRICO vs FD (Teo G-1(iv)): o gradiente
210
+ /// de L_alg = ‖Interp(ẑ_soft) − ẑ_{ℓ−1}‖² w.r.t. h (via softmax →
211
+ /// materializar_probs → interp) é exato (autograd) — validado por FD.
212
+ #[test]
213
+ fn gate_g11_adjunto_algebrico_vs_fd() -> Result<()> {
214
+ let _trava = trava_rng();
215
+ use khtst_rust::mascaras::interp_para;
216
+ khtst_rust::rng::inicializar(777);
217
+ let dev = Device::Cpu;
218
+ let (n_l, n_ant, d_lat, k) = (6usize, 3, 4, 5);
219
+ let h = khtst_rust::rng::randn_semeado(1.0, &[1, n_l, d_lat], &dev)?;
220
+ let hv = Var::from_tensor(&h)?;
221
+ let zhat_ant = khtst_rust::rng::randn_semeado(1.0, &[1, n_ant, d_lat], &dev)?;
222
+ // codebook fixo
223
+ let c = khtst_rust::rng::randn_semeado(0.5, &[k, d_lat], &dev)?;
224
+ let r_escala = 0.7f64;
225
+ let perda = |h: &Tensor| -> Result<f64> {
226
+ let logits = h.broadcast_matmul(&c.transpose(0, 1)?.unsqueeze(0)?)?; // (1, n_l, k)
227
+ let probs = candle_nn::ops::softmax(&logits, D::Minus1)?;
228
+ let r_soft = probs.matmul(&c.unsqueeze(0)?)?.affine(r_escala, 0.0)?; // (1, n_l, d_lat)
229
+ let zhat_soft = interp_para(&zhat_ant, n_l, &dev)?.add(&r_soft)?;
230
+ let voltou = interp_para(&zhat_soft, n_ant, &dev)?;
231
+ let dif = voltou.sub(&zhat_ant)?;
232
+ Ok(dif.sqr()?.sum_all()?.to_scalar::<f32>()? as f64)
233
+ };
234
+ // autograd (perda construída sobre o Var)
235
+ let perda_t = {
236
+ let logits = hv.as_tensor().broadcast_matmul(&c.transpose(0, 1)?.unsqueeze(0)?)?;
237
+ let probs = candle_nn::ops::softmax(&logits, D::Minus1)?;
238
+ let r_soft = probs.matmul(&c.unsqueeze(0)?)?.affine(r_escala, 0.0)?;
239
+ let zhat_soft = interp_para(&zhat_ant, n_l, &dev)?.add(&r_soft)?;
240
+ let voltou = interp_para(&zhat_soft, n_ant, &dev)?;
241
+ let dif = voltou.sub(&zhat_ant)?;
242
+ dif.sqr()?.sum_all()?
243
+ };
244
+ let gs = perda_t.backward()?;
245
+ let g = gs.get(&hv).unwrap().flatten_all()?.to_vec1::<f32>()?;
246
+ // FD central
247
+ let eps = 1e-3f32;
248
+ let h_plano = h.flatten_all()?.to_vec1::<f32>()?;
249
+ let mut erro = 0f64;
250
+ for i in [0usize, 5, 11, 17, 22] {
251
+ let mut hp = h_plano.clone();
252
+ let mut hm = h_plano.clone();
253
+ hp[i] += eps;
254
+ hm[i] -= eps;
255
+ let tp = Tensor::from_vec(hp, (1, n_l, d_lat), &dev)?;
256
+ let tm = Tensor::from_vec(hm, (1, n_l, d_lat), &dev)?;
257
+ let fd = (perda(&tp)? - perda(&tm)?) / (2.0 * eps as f64);
258
+ erro = erro.max((fd - g[i] as f64).abs());
259
+ }
260
+ assert!(erro < 5e-3, "G11: adjunto do verificador algébrico vs FD = {erro}");
261
+ Ok(())
262
+ }
263
+
264
+ /// Extra — M8: PCGrad projeta contra os gradientes ORIGINAIS (invariante
265
+ /// de não-piora: ⟨g_combinado, g_i⟩ ≥ 0 para toda perda i).
266
+ #[test]
267
+ fn gate_m8_pcgrad_nao_piora() {
268
+ // duas perdas conflitantes em 2 variáveis
269
+ let g1 = vec![Some(vec![1.0f32]), Some(vec![-2.0])];
270
+ let g2 = vec![Some(vec![-1.0f32]), Some(vec![0.5])];
271
+ let combinado = khtst_rust::treino::pcgrad_achatado(vec![g1.clone(), g2.clone()]);
272
+ let ip = |a: &Vec<Option<Vec<f32>>>, b: &Vec<Option<Vec<f32>>>| -> f64 {
273
+ let mut s = 0f64;
274
+ for (x, y) in a.iter().zip(b.iter()) {
275
+ if let (Some(x), Some(y)) = (x, y) {
276
+ s += x.iter().zip(y.iter()).map(|(p, q)| *p as f64 * *q as f64).sum::<f64>();
277
+ }
278
+ }
279
+ s
280
+ };
281
+ let ip1 = ip(&combinado, &g1);
282
+ let ip2 = ip(&combinado, &g2);
283
+ assert!(ip1 >= -1e-9, "⟨g, g1⟩ = {ip1} ≥ 0 (não-piora)");
284
+ assert!(ip2 >= -1e-9, "⟨g, g2⟩ = {ip2} ≥ 0 (não-piora)");
285
+ }