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 +1 -103
- Cargo.toml +7 -6
- README.md +45 -28
- configs/base_rust.json +22 -33
- dados/bpe.json +0 -0
- docs/memorias/r-08-auditoria-correcoes-v020.md +133 -0
- docs/memorias/r-09-aprimoramentos-v030.md +180 -0
- pyproject.toml +9 -2
- resultados/resumo_treino.json +13 -27
- resultados/suite_v.json +93 -13
- resultados/telemetria_fase_a.jsonl +0 -0
- resultados/telemetria_fase_b.jsonl +0 -0
- scripts/39_coletar_dados.py +50 -20
- scripts/40_treinar.py +77 -24
- scripts/41_teste.py +214 -35
- scripts/42_publicar.py +139 -56
- src/amostrador.rs +135 -30
- src/atencao.rs +144 -46
- src/camadas.rs +5 -3
- src/config.rs +106 -10
- src/controlador.rs +104 -26
- src/diagnostico.rs +23 -23
- src/gerador.rs +149 -24
- src/lib.rs +491 -120
- src/mascaras.rs +131 -11
- src/memoria.rs +20 -1
- src/ponte.rs +51 -14
- src/quantizador.rs +112 -16
- src/rng.rs +101 -0
- src/ssd.rs +341 -103
- src/telemetria.rs +154 -12
- src/tokenizador.rs +121 -42
- src/tokens.rs +70 -5
- src/treino.rs +453 -87
- src/tronco.rs +340 -40
- tests/provas.rs +285 -0
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.
|
| 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.
|
| 4 |
edition = "2021"
|
| 5 |
-
description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM (Next-Concept Modeling), provas integradas,
|
| 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`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## O que é
|
| 6 |
|
| 7 |
-
- **Tokenizador de conceitos** (Estágio A): VQ-VAE residual multiscale com codebooks **Stiefel/RCO** e quantização **GSQ** (
|
| 8 |
-
- **Gerador next-concept** (Estágio B): fatoração `p(c) = Π_ℓ p(c_ℓ|c_{<ℓ})` com predição **paralela** intra-nível —
|
| 9 |
-
- **Tronco híbrido 5:1**: 5 camadas
|
| 10 |
-
- **Ponte Duplex**: A↔B com canais limitados,
|
| 11 |
-
- **Telemetria completa**:
|
| 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
|
| 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 (
|
| 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 |
-
#
|
| 38 |
-
|
| 39 |
-
assert
|
| 40 |
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
```
|
| 45 |
|
| 46 |
## Estrutura
|
| 47 |
|
| 48 |
```
|
| 49 |
-
src/
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
```
|
| 56 |
|
| 57 |
-
## Verificações
|
| 58 |
|
| 59 |
-
- **
|
| 60 |
-
- **Suíte V**:
|
| 61 |
-
- **Treino real** (Wikipédia PT-BR,
|
|
|
|
| 62 |
|
| 63 |
## Provas e teoria
|
| 64 |
|
| 65 |
-
Toda a fundamentação
|
| 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":
|
| 48 |
"rec": 1.0,
|
| 49 |
-
"rho_ancora": 0.05
|
|
|
|
| 50 |
},
|
| 51 |
"temperaturas": {
|
| 52 |
"auto": true,
|
| 53 |
-
"entropia_alvo":
|
| 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 "clulas" 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.
|
| 8 |
-
description = "KHTST_RUST — refatoração Rust/PyO3 do KHTST com NCM, provas integradas, ADBA
|
| 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 |
-
"
|
| 18 |
-
"
|
| 19 |
-
"
|
| 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 |
-
"
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
|
| 30 |
-
|
| 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.
|
| 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":
|
| 10 |
-
"
|
| 11 |
},
|
| 12 |
"V4_mascara": {
|
| 13 |
"ok": true,
|
| 14 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
},
|
| 16 |
"V9_stiefel": {
|
| 17 |
"ok": true,
|
| 18 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
},
|
| 20 |
"V13_mtp_nivel": {
|
| 21 |
"ok": true,
|
| 22 |
-
"
|
|
|
|
|
|
|
| 23 |
},
|
| 24 |
"V10_nfe": {
|
| 25 |
"ok": true,
|
| 26 |
"nfe": 7,
|
| 27 |
"L": 7,
|
| 28 |
"nfe_anytime": 3,
|
| 29 |
-
"tempo_ms":
|
|
|
|
| 30 |
},
|
| 31 |
"V14_ponte": {
|
| 32 |
"ok": true,
|
| 33 |
"envios_a_para_b": 200,
|
| 34 |
"envios_b_para_a": 200,
|
| 35 |
-
"maior_sobreposicao_us":
|
| 36 |
"quedas_por_timeout": 0,
|
| 37 |
"recebidos_a": 200,
|
| 38 |
"recebidos_b": 200,
|
| 39 |
"rodadas_completas": 200,
|
| 40 |
-
"sobrecarga_media_us":
|
| 41 |
-
"tempo_total_ms":
|
| 42 |
},
|
| 43 |
"V5b_corrupcao": {
|
| 44 |
"ok": true,
|
| 45 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
},
|
| 47 |
"VEREDITO": {
|
| 48 |
"bloqueadoras_ok": true,
|
| 49 |
-
"n_verdes":
|
| 50 |
-
"n_total":
|
| 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 |
-
|
| 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 |
-
|
| 36 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Protocolo (n-04 §4): A = nested dropout + commitment + GSQ/RCO Stiefel;
|
| 6 |
-
B = MLE exato
|
| 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 |
-
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
seqs = []
|
| 14 |
with open(os.path.join(pasta, "dados", f"shard_{qual}.jsonl")) as f:
|
| 15 |
for linha in f:
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 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 |
-
|
|
|
|
| 31 |
os.makedirs(os.path.join(pasta, "resultados"), exist_ok=True)
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
cfg = K.Config(
|
|
|
|
| 35 |
bpe = K.Bpe.carregar(os.path.join(pasta, "dados", "bpe.json"))
|
| 36 |
|
| 37 |
-
|
| 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 |
|
| 56 |
-
f"
|
|
|
|
| 57 |
f"| perp_codebook {r['perplexidade_codebook']:.1f} "
|
| 58 |
f"| monotonia_prefixo {r['monotonia_prefixo']:.2f} "
|
| 59 |
-
f"|
|
|
|
|
| 60 |
|
| 61 |
if args.fase in ("b", "ab"):
|
| 62 |
-
print("=== FASE B — gerador next-concept (
|
| 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 |
|
| 76 |
-
f"
|
| 77 |
-
f"|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
def main():
|
| 8 |
pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 20 |
except Exception as e:
|
| 21 |
res["V1_config"] = {"ok": False, "erro": str(e)}
|
| 22 |
|
| 23 |
-
# V4 — máscara block-causal
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
# V13 — MTP ⊂ NCM:
|
|
|
|
| 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,
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
s = K.treinar_fase_b(cfg, seqs[:16],
|
| 40 |
-
caminho_tokenizador=
|
| 41 |
-
teto_seg=
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 +
|
| 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 κ
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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", "
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 94 |
return 0 if todas_ok else 1
|
| 95 |
|
|
|
|
| 96 |
if __name__ == "__main__":
|
| 97 |
-
sys
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
import json, os, shutil, sys
|
| 9 |
|
|
|
|
| 10 |
def main():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from huggingface_hub import HfApi
|
| 12 |
-
api = HfApi()
|
| 13 |
|
| 14 |
pasta = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 15 |
repo = "PowerMachine/khtst-rust"
|
| 16 |
|
| 17 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 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 |
-
|
| 58 |
-
commit_message="KHTST_RUST v0.
|
| 59 |
-
"
|
| 60 |
-
"
|
|
|
|
|
|
|
|
|
|
| 61 |
)
|
| 62 |
print("upload único concluído")
|
| 63 |
|
| 64 |
-
#
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
try:
|
| 71 |
-
api.delete_files(repo_id=
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
print(f"estados apagados de {repo_alvo}: {len(arquivos)} arquivos")
|
| 75 |
except Exception as e:
|
| 76 |
-
print(f"FALHA ao apagar de {
|
|
|
|
| 77 |
|
| 78 |
-
#
|
| 79 |
arqs = api.list_repo_files(repo_id=repo, repo_type="model")
|
| 80 |
print(f"repo final: {len(arqs)} arquivos")
|
| 81 |
-
assert
|
| 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 |
-
|
| 85 |
-
sujos = [a for a in arqs if a.endswith((".npz", ".pt", ".ckpt"
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
if __name__ == "__main__":
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 5 |
-
//!
|
| 6 |
-
//!
|
| 7 |
-
//!
|
| 8 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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::
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 81 |
-
let mut
|
| 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
|
| 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
|
| 105 |
-
|
| 106 |
-
|
| 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
|
| 115 |
self.nfe_contador += 1;
|
| 116 |
let hu = if let Some(eu) = entrada_uncond {
|
| 117 |
-
let hu = self.forward_bloco(g, &eu, &mut
|
| 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 |
-
|
| 136 |
nivel: usize,
|
| 137 |
) -> Result<Tensor> {
|
| 138 |
let n_novo = entrada.dim(1)?;
|
| 139 |
-
let n_cache =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
let total_vis = n_cache + n_novo;
|
| 141 |
-
//
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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::
|
| 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
|
| 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,
|
| 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 |
-
//! hipótese H2 do Teo C.2 (condicionais consistentes dado o plano).
|
| 14 |
//!
|
| 15 |
-
//!
|
| 16 |
-
//!
|
| 17 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
///
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
} else {
|
| 49 |
None
|
| 50 |
};
|
| 51 |
-
let
|
| 52 |
-
Some(Var::from_tensor(&Tensor::zeros(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
} else {
|
| 54 |
None
|
| 55 |
};
|
| 56 |
Ok(CabecaAdba {
|
| 57 |
q: mk()?, k: mk()?, v: mk()?, o: mk()?,
|
| 58 |
-
|
| 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 —
|
| 101 |
-
if let (Some(
|
| 102 |
let t_k = scores.dim(3)?;
|
| 103 |
-
let
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
let
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
}
|
| 113 |
-
// ADBA (iii): temperatura por nível do QUERY
|
| 114 |
-
if let (Some(
|
| 115 |
let t_q = scores.dim(2)?;
|
| 116 |
-
let
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
let
|
| 122 |
-
.
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 136 |
-
if let Some(t) = &self.
|
| 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, // (
|
| 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 =
|
| 189 |
Ok(AncoraNivel { q: Var::from_tensor(&q)?, proj: Linear::novo(d, d, 0.02, dev)? })
|
| 190 |
}
|
| 191 |
|
| 192 |
-
/// contexto (
|
|
|
|
|
|
|
| 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:
|
| 197 |
let escala = 1.0 / (d as f64).sqrt();
|
| 198 |
-
let scores = q
|
|
|
|
|
|
|
| 199 |
let pesos = candle_nn::ops::softmax(&scores, D::Minus1)?;
|
| 200 |
-
let resumo = pesos.matmul(contexto)?.reshape((
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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
|
| 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
|
|
|
|
|
|
|
| 336 |
pub fn checar_orcamento_cache(&self) -> Result<usize, String> {
|
| 337 |
let soma: usize = self.ncm.niveis.iter().copied().sum();
|
| 338 |
-
let
|
|
|
|
| 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 |
-
//!
|
| 6 |
-
//!
|
| 7 |
-
//!
|
| 8 |
-
//!
|
| 9 |
-
//!
|
| 10 |
-
//! -
|
| 11 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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.
|
| 55 |
-
self.
|
| 56 |
-
hub.ajuste(
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
self.passos_sem_melhora = 0;
|
| 59 |
-
|
|
|
|
| 60 |
}
|
| 61 |
-
// rastreio de mínimo
|
| 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.
|
| 71 |
-
self.
|
| 72 |
-
hub.ajuste(
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
self.passos_sem_melhora = 0;
|
| 75 |
}
|
| 76 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
let p = passo as f64;
|
| 78 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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
|
| 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
|
|
|
|
| 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
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
/
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
inicio += n_l;
|
| 183 |
}
|
|
|
|
| 184 |
let l = ces.len() as f64;
|
| 185 |
-
let mut perda =
|
| 186 |
-
for
|
| 187 |
-
perda = (&perda + &
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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().
|
| 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 |
-
|
| 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)
|
|
|
|
| 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.
|
| 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.
|
|
|
|
| 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 |
-
|
| 176 |
-
|
|
|
|
| 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 =
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
})
|
| 186 |
.map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
|
| 187 |
-
|
| 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
|
| 240 |
-
let
|
| 241 |
-
let
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
let
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 274 |
-
let
|
| 275 |
-
|
| 276 |
-
let
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
|
|
|
| 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
|
| 330 |
-
let
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
.
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
"niveis_usados": nivel_final, "ids_tokens": textos_ids,
|
| 356 |
"temperaturas": amostrador.temperaturas,
|
| 357 |
-
|
| 358 |
-
|
| 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,
|
| 365 |
}
|
| 366 |
|
| 367 |
-
|
| 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 |
-
///
|
| 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 |
-
///
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
///
|
| 37 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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
|
| 93 |
-
let
|
| 94 |
-
self.
|
| 95 |
-
let
|
| 96 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 112 |
-
let
|
| 113 |
-
let
|
| 114 |
-
if
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
))
|
| 64 |
-
}
|
| 65 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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::
|
| 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
|
| 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 =
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
//!
|
| 7 |
-
//! com a_t = σ(W_a·x_t + b_a) ∈ (0,1)^d (portas por canal).
|
| 8 |
//!
|
| 9 |
-
//!
|
| 10 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
use candle_core::{CpuStorage,
|
| 13 |
use crate::camadas::{Linear, RmsNorm, Swiglu};
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
for j in 0..d {
|
| 31 |
-
let
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
-
Ok((CpuStorage::F32(y), (
|
| 39 |
}
|
| 40 |
|
| 41 |
-
fn bwd(
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
let h_v = h.flatten_all()?.to_vec1::<f32>()?;
|
| 48 |
let dy_v = grad_res.flatten_all()?.to_vec1::<f32>()?;
|
| 49 |
-
let (
|
| 50 |
-
let
|
| 51 |
-
let
|
| 52 |
-
let
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
-
let dev =
|
| 64 |
-
let
|
| 65 |
-
let
|
| 66 |
-
|
|
|
|
| 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).
|
| 94 |
-
/// `cache_estado`
|
| 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 |
-
//
|
| 103 |
-
let xf = xp.reshape((b * n, d))?.contiguous()?;
|
| 104 |
let af = a.reshape((b * n, d))?.contiguous()?;
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
let
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
-
let
|
|
|
|
|
|
|
| 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
|
| 140 |
-
let
|
| 141 |
-
|
| 142 |
-
let
|
| 143 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
let perda = (&y * &pesos)?.sum_all()?;
|
| 145 |
let gs = perda.backward()?;
|
| 146 |
-
let
|
| 147 |
-
let
|
| 148 |
-
let
|
| 149 |
-
|
| 150 |
-
// FD
|
| 151 |
let eps = 1e-3f32;
|
| 152 |
-
let f = |
|
| 153 |
-
let
|
|
|
|
| 154 |
Ok((&y * &pesos)?.sum_all()?.to_scalar::<f32>()? as f64)
|
| 155 |
};
|
| 156 |
-
let
|
| 157 |
-
let
|
| 158 |
-
let
|
| 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 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
|
|
|
| 179 |
}
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 7 |
-
//!
|
| 8 |
-
//!
|
| 9 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]:
|
| 136 |
-
///
|
| 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);
|
| 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
|
| 65 |
-
|
|
|
|
| 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
|
| 103 |
-
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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 |
-
///
|
|
|
|
|
|
|
| 266 |
pub fn decodificar_livre(&self, zhat: &Tensor, max_tokens: usize) -> Result<Vec<Vec<u32>>> {
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
for (i, t) in v.iter().enumerate() {
|
| 290 |
-
tokens[i].push(*t);
|
| 291 |
-
}
|
| 292 |
}
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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):
|
|
|
|
|
|
|
| 384 |
pub fn salvar(&self, caminho: &str) -> Result<()> {
|
|
|
|
| 385 |
let mut obj = serde_json::Map::new();
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 212 |
}
|
| 213 |
}
|
| 214 |
}
|
| 215 |
}
|
| 216 |
}
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" → "clulas"). 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
|
| 4 |
-
//! checkpoints atômicos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 20 |
pub struct AdamWManual {
|
| 21 |
-
pub m:
|
| 22 |
-
pub v:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 114 |
for j in 0..n_perdas {
|
| 115 |
-
if i == j {
|
|
|
|
|
|
|
|
|
|
| 116 |
let mut ip = 0f64;
|
| 117 |
let mut nj = 0f64;
|
| 118 |
for v in 0..n_vars {
|
| 119 |
-
if let (Some(a), Some(b)) = (&
|
| 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) =
|
| 130 |
-
if let Some(a) = &mut
|
| 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) = &
|
| 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() >=
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
let perda_inicial = {
|
| 228 |
-
let
|
| 229 |
-
let (p, _) = tok.perda_fase_a(&
|
| 230 |
p.to_scalar::<f32>()? as f64
|
| 231 |
};
|
| 232 |
let a_cada = cfg.treino.telemetria_a_cada.max(1);
|
| 233 |
-
let mut passos_feitos =
|
| 234 |
-
'laco_a: for passo in
|
| 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,
|
| 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.
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 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 |
-
|
| 285 |
-
hub.erro("fase_a", &format!("perda não finita no passo {passo}"));
|
| 286 |
-
break;
|
| 287 |
-
}
|
| 288 |
}
|
| 289 |
-
if passos_feitos ==
|
| 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
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
let a_cada = cfg.treino.telemetria_a_cada.max(1);
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
let perda_inicial = {
|
| 347 |
-
let
|
| 348 |
-
let cod = tok.codificar_congelado(&
|
| 349 |
-
let s = ger.perda_treino(tok, &cod.ids, &mut
|
| 350 |
s.ce_por_nivel.iter().sum::<f64>() / s.ce_por_nivel.len() as f64
|
| 351 |
};
|
| 352 |
-
let mut passos_feitos =
|
| 353 |
-
'laco_b: for passo in
|
| 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 |
-
//
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) =>
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
};
|
| 382 |
-
// perdas separadas para PCGrad: [CE
|
| 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 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 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 |
-
|
| 418 |
-
hub.erro("fase_b", &format!("CE não finita no passo {passo}"));
|
| 419 |
-
break;
|
| 420 |
-
}
|
| 421 |
}
|
| 422 |
-
if passos_feitos ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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 |
-
//!
|
| 7 |
-
//!
|
| 8 |
-
//!
|
| 9 |
-
//!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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 |
-
|
| 39 |
)?);
|
| 40 |
}
|
| 41 |
-
let ancora = if
|
| 42 |
Some(AncoraNivel::nova(m.d_modelo, l, dev)?)
|
| 43 |
} else {
|
| 44 |
None
|
| 45 |
};
|
| 46 |
-
let (film_gamma, film_beta) = if
|
| 47 |
-
|
|
|
|
|
|
|
| 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 (
|
| 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 |
-
///
|
| 85 |
-
/// (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
pub fn forward_treino(
|
| 87 |
&self,
|
| 88 |
s: &Tensor,
|
| 89 |
mascara: &Tensor,
|
| 90 |
niveis: &[usize],
|
| 91 |
-
|
| 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(
|
| 101 |
-
|
| 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)
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 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) +
|
| 134 |
-
/// `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
pub fn forward_nivel(
|
| 136 |
&self,
|
| 137 |
bloco_x: &Tensor,
|
| 138 |
-
|
| 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 |
-
|
| 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 |
+
}
|