diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4265a7772..cbe1b1fb2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -324,7 +324,7 @@ jobs: -e CARGO_INCREMENTAL=0 \ -e CARGO_BUILD_JOBS=8 \ "$IMAGE" \ - bash -c 'cargo test -p aprender-core --test monorepo_invariants && cargo test -p aprender-core --test readme_contract && cargo test -p apr-cli --test cli_commands && cargo test -p aprender-core --test beat_sklearn_iris && cargo test -p aprender-core --test beat_sklearn_nmi && cargo test -p aprender-core --test beat_sklearn_metrics_parity && cargo test -p aprender-core --test beat_sklearn_gaussiannb_accuracy && cargo test -p aprender-core --test beat_sklearn_svc_accuracy && cargo test -p aprender-core --test beat_sklearn_pipeline_encoder && cargo test -p aprender-serve --test beat_fail_closed_garbage && cargo test -p aprender-compute --lib beat_nf4_bitsandbytes_equivalence && cargo test -p aprender-core --test beat_pytorch_autograd_grad && cargo test -p aprender-train-lora --lib beat_lora_merge_forward_equivalence && cargo test -p apr-cli --release --test beat_pytorch_deploy_footprint && cargo test -p aprender-serve --test beat_fail_closed_structural && cargo test -p aprender-serve --test ollama_http_compat && cargo test -p apr-cli --test ollama_ndjson_streaming && cargo test -p apr-cli --test falsification_chat_http_cli && cargo test -p aprender-contracts --test apr_serve_api_key_auth_contract && cargo test -p apr-cli --test falsify_auth_001 --test falsify_auth_002 --test falsify_auth_003 --no-fail-fast && cargo build --examples --workspace --keep-going' + bash -c 'cargo test -p aprender-core --test monorepo_invariants && cargo test -p aprender-core --test readme_contract && cargo test -p apr-cli --test cli_commands && cargo test -p aprender-train-inspect --test falsify_no_fabricated_metadata_2519 && cargo test -p aprender-train-bench --test falsify_no_fabricated_benchmarks_2519 && cargo test -p aprender-train-shell --test falsify_no_fabricated_fetch_2519 && cargo test -p aprender-core --test beat_sklearn_iris && cargo test -p aprender-core --test beat_sklearn_nmi && cargo test -p aprender-core --test beat_sklearn_metrics_parity && cargo test -p aprender-core --test beat_sklearn_gaussiannb_accuracy && cargo test -p aprender-core --test beat_sklearn_svc_accuracy && cargo test -p aprender-core --test beat_sklearn_pipeline_encoder && cargo test -p aprender-serve --test beat_fail_closed_garbage && cargo test -p aprender-compute --lib beat_nf4_bitsandbytes_equivalence && cargo test -p aprender-core --test beat_pytorch_autograd_grad && cargo test -p aprender-train-lora --lib beat_lora_merge_forward_equivalence && cargo test -p apr-cli --release --test beat_pytorch_deploy_footprint && cargo test -p aprender-serve --test beat_fail_closed_structural && cargo test -p aprender-serve --test ollama_http_compat && cargo test -p apr-cli --test ollama_ndjson_streaming && cargo test -p apr-cli --test falsification_chat_http_cli && cargo test -p aprender-contracts --test apr_serve_api_key_auth_contract && cargo test -p apr-cli --test falsify_auth_001 --test falsify_auth_002 --test falsify_auth_003 --no-fail-fast && cargo build --examples --workspace --keep-going' - name: Build.rs crate-root escape check (v0.31.1 yank guard) # Static Poka-Yoke: flags build.rs files that panic on files outside # CARGO_MANIFEST_DIR, which break `cargo install` from crates.io. diff --git a/Cargo.lock b/Cargo.lock index 49dc8701dd..8cc557e68a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,14 +8,7 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "cpp_demangle", - "fallible-iterator", "gimli 0.32.3", - "memmap2", - "object 0.37.3", - "rustc-demangle", - "smallvec", - "typed-arena", ] [[package]] @@ -261,6 +254,7 @@ dependencies = [ "aprender-common", "aprender-compute", "aprender-contracts", + "aprender-contracts-macros", "aprender-core", "aprender-data", "aprender-explain", @@ -271,6 +265,7 @@ dependencies = [ "aprender-profile", "aprender-registry", "aprender-serve", + "aprender-test-lib", "aprender-train", "aprender-train-common", "aprender-train-distill", @@ -295,12 +290,10 @@ dependencies = [ "glob", "half", "humansize", - "jugar-probar 0.4.2", "libc", - "parquet 57.3.1", + "parquet", "predicates", "proptest", - "provable-contracts-macros 0.3.1", "rayon", "regex", "rmp-serde", @@ -339,26 +332,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "aprender" -version = "0.25.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7053416de79df742f9da17a53dea7087830b83761a80f69bc2a91b708aab781c" -dependencies = [ - "bincode", - "getrandom 0.2.17", - "memmap2", - "minijinja", - "rand 0.8.6", - "rand_chacha 0.3.1", - "rayon", - "rmp-serde", - "serde", - "serde_json", - "trueno 0.14.6", - "trueno-quant", -] - [[package]] name = "aprender" version = "0.27.8" @@ -376,7 +349,6 @@ dependencies = [ "provable-contracts-macros 0.2.2", "rand 0.9.4", "rand_chacha 0.9.0", - "rayon", "rmp-serde", "rustfft", "safetensors 0.4.5", @@ -386,7 +358,7 @@ dependencies = [ "sha2 0.10.9", "tempfile", "thiserror 2.0.18", - "trueno 0.17.5", + "trueno", "trueno-quant", "ureq 2.12.1", ] @@ -427,11 +399,11 @@ dependencies = [ "aprender-gpu", "aprender-present-core", "aprender-present-terminal", + "aprender-test-lib", "chrono", "clap", "crossterm 0.28.1", "dirs 5.0.1", - "jugar-probar 1.0.4", "libc", "pollster", "proptest", @@ -475,6 +447,7 @@ name = "aprender-compute" version = "0.63.0" dependencies = [ "anyhow", + "aprender-contracts-macros", "aprender-core", "aprender-cuda-edge", "aprender-gemm-codegen", @@ -501,7 +474,6 @@ dependencies = [ "num_cpus", "pollster", "proptest", - "provable-contracts-macros 0.3.1", "rayon", "regex", "serde", @@ -578,9 +550,12 @@ dependencies = [ "apr-format", "aprender-common", "aprender-compute", + "aprender-contracts", + "aprender-contracts-macros", "aprender-data", "aprender-profile", "aprender-quant", + "aprender-test-lib", "aprender-train", "aprender-zram-core", "argon2", @@ -595,13 +570,10 @@ dependencies = [ "hf-xet", "hkdf", "js-sys", - "jugar-probar 0.5.1", "lz4_flex 0.11.6", "memmap2", "minijinja", "proptest", - "provable-contracts 0.3.1", - "provable-contracts-macros 0.3.1", "rand 0.9.4", "rand_chacha 0.9.0", "rayon", @@ -636,7 +608,7 @@ dependencies = [ name = "aprender-cupti" version = "0.63.0" dependencies = [ - "bindgen 0.71.1", + "bindgen", "bitflags 2.13.0", "libc", "thiserror 2.0.18", @@ -647,6 +619,7 @@ name = "aprender-data" version = "0.63.0" dependencies = [ "aes-gcm", + "aprender-test-lib", "argon2", "arrow 57.3.1", "arrow-csv", @@ -666,11 +639,10 @@ dependencies = [ "hex", "hkdf", "js-sys", - "jugar-probar 1.0.4", "lz4_flex 0.11.6", "memmap2", "nu-ansi-term", - "parquet 57.3.1", + "parquet", "predicates", "proptest", "rand 0.9.4", @@ -714,7 +686,7 @@ dependencies = [ "futures-intrusive", "js-sys", "lz4_flex 0.11.6", - "parquet 57.3.1", + "parquet", "proptest", "prost 0.13.5", "quickcheck", @@ -745,14 +717,14 @@ version = "0.63.0" dependencies = [ "aprender-compute", "aprender-db", + "aprender-test-lib", "arrow 57.3.1", "bincode", "criterion 0.5.1", "crossterm 0.28.1", "futures", - "jugar-probar 0.4.2", "num_cpus", - "parquet 57.3.1", + "parquet", "pepita", "pollster", "proptest", @@ -812,10 +784,10 @@ version = "0.63.0" dependencies = [ "aprender-common", "aprender-simulate", + "aprender-test-lib", "bytemuck", "criterion 0.7.0", "crossterm 0.28.1", - "jugar-probar 0.4.2", "libloading", "manzana", "pollster", @@ -832,12 +804,11 @@ dependencies = [ "anyhow", "aprender-compute", "aprender-core", - "aprender-db", "arrow 57.3.1", "bytemuck", "criterion 0.6.0", "futures-intrusive", - "parquet 57.3.1", + "parquet", "proptest", "serial_test", "tempfile", @@ -897,6 +868,8 @@ dependencies = [ "anyhow", "aprender-common", "aprender-compute", + "aprender-contracts", + "aprender-contracts-macros", "aprender-core", "aprender-cuda-edge", "aprender-data", @@ -930,15 +903,11 @@ dependencies = [ "futures-util", "glob", "indexmap 2.14.0", - "jugar-probar 1.0.4", "libc", "pepita", "pmcp", "predicates", - "presentar", "proptest", - "provable-contracts 0.2.2", - "provable-contracts-macros 0.2.2", "quick-xml 0.41.0", "reqwest 0.12.28", "resvg", @@ -956,7 +925,6 @@ dependencies = [ "tower 0.5.3", "tracing", "tracing-subscriber", - "trueno-ublk", "walkdir", "wasm-bindgen", "web-sys", @@ -981,9 +949,9 @@ dependencies = [ name = "aprender-present-core" version = "0.63.0" dependencies = [ + "aprender-contracts-macros", "criterion 0.7.0", "proptest", - "provable-contracts-macros 0.3.1", "serde", "serde_json", "serde_yaml_ng", @@ -1004,6 +972,7 @@ dependencies = [ name = "aprender-present-lib" version = "0.63.0" dependencies = [ + "aprender-contracts", "aprender-present-core", "aprender-present-layout", "aprender-present-test", @@ -1016,7 +985,6 @@ dependencies = [ "hex", "js-sys", "proptest", - "provable-contracts 0.3.1", "regex", "serde", "serde_json", @@ -1045,7 +1013,6 @@ dependencies = [ "serde_yaml_ng", "sysinfo 0.33.1", "thiserror 2.0.18", - "ttop", "unicode-segmentation", "unicode-width 0.2.0", ] @@ -1265,7 +1232,6 @@ version = "0.63.0" dependencies = [ "aprender-common", "aprender-compute", - "aprender-db", "aprender-serve", "async-trait", "bincode", @@ -1353,6 +1319,8 @@ dependencies = [ "anyhow", "approx", "aprender-compute", + "aprender-contracts", + "aprender-contracts-macros", "aprender-core", "aprender-cuda-edge", "aprender-data", @@ -1362,6 +1330,7 @@ dependencies = [ "aprender-profile-core", "aprender-quant", "aprender-registry", + "aprender-test-lib", "aprender-viz", "arc-swap", "arrow 57.3.1", @@ -1381,7 +1350,6 @@ dependencies = [ "http-body-util", "hyper 1.10.1", "indicatif 0.17.11", - "jugar-probar 0.4.2", "libc", "lz4_flex 0.11.6", "memmap2", @@ -1391,8 +1359,6 @@ dependencies = [ "once_cell", "predicates", "proptest", - "provable-contracts 0.2.2", - "provable-contracts-macros 0.2.2", "rand 0.9.4", "rayon", "reqwest 0.11.27", @@ -1434,6 +1400,8 @@ dependencies = [ name = "aprender-simulate" version = "0.63.0" dependencies = [ + "aprender-contracts", + "aprender-contracts-macros", "aprender-present-core", "aprender-present-terminal", "aprender-present-test", @@ -1450,8 +1418,6 @@ dependencies = [ "memmap2", "num-traits", "proptest", - "provable-contracts 0.2.2", - "provable-contracts-macros 0.2.2", "rand 0.9.4", "rand_pcg", "serde", @@ -1558,6 +1524,7 @@ dependencies = [ "aprender-compute", "aprender-present-core", "aprender-present-terminal", + "aprender-test-derive", "async-trait", "base64 0.22.1", "bincode", @@ -1570,7 +1537,6 @@ dependencies = [ "gif 0.13.3", "image", "js-sys", - "jugar-probar-derive", "mp4", "notify", "png 0.17.16", @@ -1600,9 +1566,9 @@ name = "aprender-test-showcase" version = "0.63.0" dependencies = [ "aprender-present-terminal", + "aprender-test-lib", "console_error_panic_hook", "crossterm 0.28.1", - "jugar-probar 1.0.4", "proptest", "serde", "serde_json", @@ -1619,6 +1585,8 @@ dependencies = [ "approx", "aprender-common", "aprender-compute", + "aprender-contracts", + "aprender-contracts-macros", "aprender-core", "aprender-data", "aprender-db", @@ -1628,6 +1596,7 @@ dependencies = [ "aprender-profile", "aprender-rag", "aprender-serve", + "aprender-test-lib", "aprender-viz", "arrow 57.3.1", "axum 0.8.9", @@ -1649,13 +1618,10 @@ dependencies = [ "insta", "js-sys", "jsonschema", - "jugar-probar 1.0.4", "ndarray 0.16.1", "nvml-wrapper", - "parquet 57.3.1", + "parquet", "proptest", - "provable-contracts 0.2.2", - "provable-contracts-macros 0.2.2", "rand 0.9.4", "rayon", "regex", @@ -1829,7 +1795,7 @@ dependencies = [ "clap", "criterion 0.7.0", "indicatif 0.18.4", - "parquet 57.3.1", + "parquet", "pest", "pest_derive", "proptest", @@ -1978,24 +1944,6 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" -[[package]] -name = "arrow" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" -dependencies = [ - "arrow-arith 54.3.1", - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-cast 54.3.1", - "arrow-data 54.3.1", - "arrow-ord 54.3.1", - "arrow-row 54.3.1", - "arrow-schema 54.3.1", - "arrow-select 54.3.1", - "arrow-string 54.3.1", -] - [[package]] name = "arrow" version = "57.3.1" @@ -2008,7 +1956,7 @@ dependencies = [ "arrow-cast 57.3.1", "arrow-csv", "arrow-data 57.3.1", - "arrow-ipc 57.3.1", + "arrow-ipc", "arrow-json", "arrow-ord 57.3.1", "arrow-row 57.3.1", @@ -2035,20 +1983,6 @@ dependencies = [ "arrow-string 58.3.0", ] -[[package]] -name = "arrow-arith" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "chrono", - "num", -] - [[package]] name = "arrow-arith" version = "57.3.1" @@ -2077,22 +2011,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-array" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" -dependencies = [ - "ahash 0.8.12", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "chrono", - "half", - "hashbrown 0.15.5", - "num", -] - [[package]] name = "arrow-array" version = "57.3.1" @@ -2129,17 +2047,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-buffer" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" -dependencies = [ - "bytes", - "half", - "num", -] - [[package]] name = "arrow-buffer" version = "57.3.1" @@ -2164,26 +2071,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-cast" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "arrow-select 54.3.1", - "atoi", - "base64 0.22.1", - "chrono", - "half", - "lexical-core", - "num", - "ryu", -] - [[package]] name = "arrow-cast" version = "57.3.1" @@ -2243,18 +2130,6 @@ dependencies = [ "regex", ] -[[package]] -name = "arrow-data" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" -dependencies = [ - "arrow-buffer 54.3.1", - "arrow-schema 54.3.1", - "half", - "num", -] - [[package]] name = "arrow-data" version = "57.3.1" @@ -2281,19 +2156,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-ipc" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ff528658b521e33905334723b795ee56b393dbe9cf76c8b1f64b648c65a60c" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "flatbuffers 24.12.23", -] - [[package]] name = "arrow-ipc" version = "57.3.1" @@ -2305,7 +2167,7 @@ dependencies = [ "arrow-data 57.3.1", "arrow-schema 57.3.1", "arrow-select 57.3.1", - "flatbuffers 25.12.19", + "flatbuffers", ] [[package]] @@ -2332,19 +2194,6 @@ dependencies = [ "simdutf8", ] -[[package]] -name = "arrow-ord" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "arrow-select 54.3.1", -] - [[package]] name = "arrow-ord" version = "57.3.1" @@ -2371,19 +2220,6 @@ dependencies = [ "arrow-select 58.3.0", ] -[[package]] -name = "arrow-row" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "half", -] - [[package]] name = "arrow-row" version = "57.3.1" @@ -2410,12 +2246,6 @@ dependencies = [ "half", ] -[[package]] -name = "arrow-schema" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" - [[package]] name = "arrow-schema" version = "57.3.1" @@ -2431,20 +2261,6 @@ dependencies = [ "bitflags 2.13.0", ] -[[package]] -name = "arrow-select" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" -dependencies = [ - "ahash 0.8.12", - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "num", -] - [[package]] name = "arrow-select" version = "57.3.1" @@ -2473,23 +2289,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "arrow-string" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" -dependencies = [ - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-data 54.3.1", - "arrow-schema 54.3.1", - "arrow-select 54.3.1", - "memchr", - "num", - "regex", - "regex-syntax", -] - [[package]] name = "arrow-string" version = "57.3.1" @@ -2573,18 +2372,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-compression" version = "0.4.42" @@ -2597,107 +2384,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if 1.0.4", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.4", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-net" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if 1.0.4", - "event-listener", - "futures-lite", - "rustix 1.1.4", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if 1.0.4", - "futures-core", - "futures-io", - "rustix 1.1.4", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - [[package]] name = "async-stream" version = "0.3.6" @@ -2720,12 +2406,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.89" @@ -2966,7 +2646,7 @@ dependencies = [ "http 0.2.12", "http 1.4.2", "http-body 1.0.1", - "lru 0.16.4", + "lru", "percent-encoding", "regex-lite", "sha2 0.11.0", @@ -3578,29 +3258,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.13.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex 1.3.0", - "syn 2.0.118", - "which 4.4.2", -] - [[package]] name = "bindgen" version = "0.71.1" @@ -3681,12 +3338,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "bitmaps" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" - [[package]] name = "bitstream-io" version = "4.10.0" @@ -3764,19 +3415,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bollard" version = "0.17.1" @@ -3853,39 +3491,18 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", - "brotli-decompressor 4.0.3", + "brotli-decompressor", ] [[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor 5.0.3", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" +name = "brotli-decompressor" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ @@ -4099,12 +3716,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cast" version = "0.3.0" @@ -4580,15 +4191,6 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.15.11" @@ -5380,28 +4982,8 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core", + "darling_macro", ] [[package]] @@ -5418,62 +5000,13 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", + "darling_core", "quote", "syn 2.0.118", ] @@ -5590,7 +5123,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn 2.0.118", @@ -5606,18 +5139,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "derive_setters" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e6f6fa1f03c14ae082120b84b3c7fbd7b8588d924cf2d7c3daf9afd49df8b9" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "dhat" version = "0.3.3" @@ -5698,16 +5219,6 @@ dependencies = [ "dirs-sys 0.5.0", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if 1.0.4", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.4.1" @@ -5819,79 +5330,6 @@ dependencies = [ "shared_thread", ] -[[package]] -name = "duende-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d727bf9ff95f2950ee82116f61fc76f997e8387ada8a69e0054fe9846387af78" -dependencies = [ - "async-trait", - "dirs-next", - "humantime", - "nix 0.29.0", - "pacha", - "repartir", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "toml 0.8.23", - "tracing", - "uuid", -] - -[[package]] -name = "duende-mlock" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a79abd55ed5f318a0ebddd9ab6027b393caee1e28f1840fe7bd29e1b5aa0af9" -dependencies = [ - "libc", -] - -[[package]] -name = "duende-platform" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e60d7f4f3fb9fe3b36818a547d1b2375f3aef1ec3ef00a7f90495e289ed54f0" -dependencies = [ - "async-trait", - "duende-core", - "libc", - "nix 0.29.0", - "repartir", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "duende-policy" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4edbbff1cb2ebb1c5a1300d352c40cb1c47482e72165c0070b18cff162dd306" -dependencies = [ - "async-trait", - "duende-core", - "repartir", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "duende-ublk" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb4bd3b34b81c94694c753578827e74d1fd432764646ef96c5421ceb412638b" -dependencies = [ - "io-uring", - "libc", - "thiserror 2.0.18", -] - [[package]] name = "dunce" version = "1.0.5" @@ -6157,27 +5595,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "exr" version = "1.74.0" @@ -6374,16 +5791,6 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flatbuffers" -version = "24.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" -dependencies = [ - "bitflags 1.3.2", - "rustc_version", -] - [[package]] name = "flatbuffers" version = "25.12.19" @@ -6632,19 +6039,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-locks" version = "0.7.1" @@ -6963,7 +6357,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", - "indexmap 2.14.0", "stable_deref_trait", ] @@ -7334,8 +6727,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -7973,7 +7364,7 @@ version = "15.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" dependencies = [ - "bitmaps 2.1.0", + "bitmaps", "rand_core 0.6.4", "rand_xoshiro", "sized-chunks", @@ -8076,15 +7467,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inotify" version = "0.10.2" @@ -8127,19 +7509,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling 0.23.0", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "instant" version = "0.1.13" @@ -8186,18 +7555,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-uring" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" -dependencies = [ - "bindgen 0.69.5", - "bitflags 2.13.0", - "cfg-if 1.0.4", - "libc", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -8388,136 +7745,27 @@ dependencies = [ ] [[package]] -name = "jugar-probar" -version = "0.4.2" +name = "khronos-egl" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08aff8480ddf05a63e8178afcfbc393ca8af1e74011c2b4fe587e72fcd44c45" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ - "base64 0.22.1", - "chrono", - "crossterm 0.28.1", - "gif 0.13.3", - "image", - "js-sys", - "mp4", - "notify", - "png 0.17.16", - "ratatui", - "regex", - "serde", - "serde_json", - "serde_yaml", - "sha2 0.10.9", - "thiserror 2.0.18", - "tracing", - "tracing-subscriber", - "trueno 0.11.0", - "uuid", - "wasm-bindgen", - "web-sys", + "libc", + "libloading", + "pkg-config", ] [[package]] -name = "jugar-probar" -version = "0.5.1" +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "konst" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5603ded7edb5ba47f3151dfbeeff0c244b9d07211b3df6b0a1786ccef83f7c" -dependencies = [ - "base64 0.22.1", - "bincode", - "chrono", - "crossterm 0.28.1", - "gif 0.13.3", - "image", - "js-sys", - "mp4", - "notify", - "png 0.17.16", - "proc-macro2", - "ratatui", - "regex", - "serde", - "serde_json", - "serde_yaml", - "sha2 0.10.9", - "syn 2.0.118", - "thiserror 2.0.18", - "tracing", - "tracing-subscriber", - "uuid", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "jugar-probar" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a299150747f498a5970f057f1da1f56fbc99a80dca81ef797a9cb014eecce9" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bincode", - "chromiumoxide", - "chrono", - "crossterm 0.28.1", - "futures", - "gif 0.14.2", - "image", - "js-sys", - "mp4", - "notify", - "png 0.18.1", - "proc-macro2", - "regex", - "serde", - "serde_json", - "serde_yaml_ng", - "sha2 0.10.9", - "syn 2.0.118", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "jugar-probar-derive" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05ebb156a58509410b63603cff6195b28f2c2f6050abd99595ded7dec3de5f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "konst" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" dependencies = [ "const_panic", "konst_proc_macros", @@ -8568,12 +7816,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lcov2cobertura" version = "1.0.9" @@ -8730,41 +7972,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libublk" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cc4f0d9771dc50a2807a495e80287911d1bf4871fad45663753692db7c432" -dependencies = [ - "async-lock", - "bitflags 2.13.0", - "bitmaps 3.2.1", - "derive_setters", - "futures-timer", - "io-uring", - "libc", - "libublk-rs-sys", - "log", - "serde", - "serde_json", - "slab", - "smol", - "thiserror 1.0.69", -] - -[[package]] -name = "libublk-rs-sys" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab204ac509937ddb9ca815e642e204f8944bb98c8f0dd613a7c2567c774e593" -dependencies = [ - "anyhow", - "bindgen 0.69.5", - "libc", - "regex", - "serde", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -8826,15 +8033,6 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru" version = "0.16.4" @@ -8856,7 +8054,7 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" dependencies = [ - "twox-hash 2.1.2", + "twox-hash", ] [[package]] @@ -8865,7 +8063,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90071f8077f8e40adfc4b7fe9cd495ce316263f19e75c2211eeff3fdf475a3d9" dependencies = [ - "twox-hash 2.1.2", + "twox-hash", ] [[package]] @@ -8874,7 +8072,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" dependencies = [ - "twox-hash 2.1.2", + "twox-hash", ] [[package]] @@ -9879,9 +9077,7 @@ version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ - "flate2", "memchr", - "ruzstd", ] [[package]] @@ -9891,11 +9087,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" dependencies = [ "crc32fast", - "flate2", "hashbrown 0.16.1", "indexmap 2.14.0", "memchr", - "ruzstd", ] [[package]] @@ -10184,28 +9378,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "pacha" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873be034730a0b6ae567897812926b649f13f12d59d0f1805a7eb5f3622702a8" -dependencies = [ - "anyhow", - "blake3", - "chrono", - "clap", - "ed25519-dalek", - "rand 0.8.6", - "rmp-serde", - "rusqlite", - "serde", - "serde_json", - "thiserror 2.0.18", - "toml 0.8.23", - "uuid", - "zstd", -] - [[package]] name = "page_size" version = "0.6.0" @@ -10227,12 +9399,6 @@ dependencies = [ "unicode-width 0.1.11", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -10256,39 +9422,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "parquet" -version = "54.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb15796ac6f56b429fd99e33ba133783ad75b27c36b4b5ce06f1f82cc97754e" -dependencies = [ - "ahash 0.8.12", - "arrow-array 54.3.1", - "arrow-buffer 54.3.1", - "arrow-cast 54.3.1", - "arrow-data 54.3.1", - "arrow-ipc 54.3.1", - "arrow-schema 54.3.1", - "arrow-select 54.3.1", - "base64 0.22.1", - "brotli 7.0.0", - "bytes", - "chrono", - "flate2", - "half", - "hashbrown 0.15.5", - "lz4_flex 0.11.6", - "num", - "num-bigint", - "paste", - "seq-macro", - "simdutf8", - "snap", - "thrift", - "twox-hash 1.6.3", - "zstd", -] - [[package]] name = "parquet" version = "57.3.1" @@ -10300,11 +9433,11 @@ dependencies = [ "arrow-buffer 57.3.1", "arrow-cast 57.3.1", "arrow-data 57.3.1", - "arrow-ipc 57.3.1", + "arrow-ipc", "arrow-schema 57.3.1", "arrow-select 57.3.1", "base64 0.22.1", - "brotli 8.0.4", + "brotli", "bytes", "chrono", "flate2", @@ -10319,7 +9452,7 @@ dependencies = [ "simdutf8", "snap", "thrift", - "twox-hash 2.1.2", + "twox-hash", "zstd", ] @@ -10520,17 +9653,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -10649,20 +9771,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if 1.0.4", - "concurrent-queue", - "hermit-abi 0.5.2", - "pin-project-lite", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - [[package]] name = "pollster" version = "0.4.0" @@ -10782,88 +9890,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "presentar" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb6890554d1df121309cf690a5d30ddd278310676918dacf6fc650d1f78feac" -dependencies = [ - "bincode", - "console_error_panic_hook", - "getrandom 0.2.17", - "js-sys", - "presentar-core", - "presentar-layout", - "presentar-widgets", - "presentar-yaml", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "presentar-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cec076597046cb63e9c064b708e010ab98a1f48db4c0004e8192724e383a6c8d" -dependencies = [ - "serde", - "serde_json", - "trueno 0.14.6", -] - -[[package]] -name = "presentar-layout" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "344e0a61e39945da7af93e7330ab74afa3797cd899cf7022486562b7e74cc01a" -dependencies = [ - "presentar-core", - "serde", -] - -[[package]] -name = "presentar-terminal" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc9e13136a2d3490fde1d76d0450cca37268a280e95d814964a41efa31bcc" -dependencies = [ - "bitvec", - "clap", - "compact_str 0.8.2", - "crossterm 0.28.1", - "presentar-core", - "serde_json", - "sysinfo 0.33.1", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-width 0.2.0", -] - -[[package]] -name = "presentar-widgets" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5401130deb743b51fe812a4d35d08706125bc1fef768c8244ed77c943533a42e" -dependencies = [ - "presentar-core", - "presentar-yaml", - "serde", -] - -[[package]] -name = "presentar-yaml" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562b2337f4821ad079e76778fe9cc692827ed1f2c0450986e0c686843a26a9c1" -dependencies = [ - "presentar-core", - "serde", - "serde_yaml_ng", -] - [[package]] name = "presser" version = "0.3.1" @@ -10988,31 +10014,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.13.0", - "chrono", - "flate2", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.13.0", - "chrono", - "hex", -] - [[package]] name = "profiling" version = "1.0.18" @@ -11076,61 +10077,22 @@ name = "prost-derive" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "prost-derive" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "provable-contracts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec46f6a8b0575811e6ab321e86f68e086e9acd7d79111106ce5bc676d9407716" -dependencies = [ - "provable-contracts-macros 0.2.2", - "regex", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", -] - -[[package]] -name = "provable-contracts" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49c4074b55824441df3872f57aecaeb69902a568dabffb59da9b15533a91cca4" -dependencies = [ - "provable-contracts-macros 0.3.1", - "regex", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.18", +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] -name = "provable-contracts-macros" -version = "0.1.1" +name = "prost-derive" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c383d865124fe9fda96af4a04d0c2641bcb93e16eb5e13f7c665f98c15333447" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ + "anyhow", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.118", @@ -11138,9 +10100,9 @@ dependencies = [ [[package]] name = "provable-contracts-macros" -version = "0.2.2" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0772baeb8ded27f9590b49756f98d88c40376659d74816ba752d39495e63137d" +checksum = "c383d865124fe9fda96af4a04d0c2641bcb93e16eb5e13f7c665f98c15333447" dependencies = [ "proc-macro2", "quote", @@ -11149,9 +10111,9 @@ dependencies = [ [[package]] name = "provable-contracts-macros" -version = "0.3.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6bb7beb246ab375bc516720bcab5c5c2b93adb63115e785454a5424ba89fc0" +checksum = "0772baeb8ded27f9590b49756f98d88c40376659d74816ba752d39495e63137d" dependencies = [ "proc-macro2", "quote", @@ -11529,27 +10491,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" -[[package]] -name = "ratatui" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" -dependencies = [ - "bitflags 2.13.0", - "cassowary", - "compact_str 0.8.2", - "crossterm 0.28.1", - "indoc", - "instability", - "itertools 0.13.0", - "lru 0.12.5", - "paste", - "strum 0.26.3", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - [[package]] name = "rav1e" version = "0.8.1" @@ -11675,7 +10616,7 @@ dependencies = [ "serde_yaml_ng", "smallvec", "thiserror 1.0.69", - "trueno 0.17.5", + "trueno", "trueno-quant", "uuid", ] @@ -11828,45 +10769,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "renacer" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9445ea7144e1feb5a108f5428349efff2221077175dc18c4afa825703774784e" -dependencies = [ - "addr2line 0.25.1", - "anyhow", - "aprender 0.25.9", - "backtrace", - "clap", - "crossbeam", - "crossterm 0.28.1", - "dashmap", - "fnv", - "gimli 0.32.3", - "hex", - "libc", - "memmap2", - "nix 0.30.1", - "object 0.38.1", - "rand 0.8.6", - "ratatui", - "regex", - "rmp-serde", - "serde", - "serde_json", - "sha2 0.10.9", - "static_assertions", - "thiserror 2.0.18", - "toml 0.8.23", - "tracing", - "tracing-subscriber", - "trueno 0.14.6", - "trueno-db", - "trueno-graph", - "trueno-viz", -] - [[package]] name = "renacer-core" version = "0.1.0" @@ -11894,22 +10796,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "repartir" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe68c3c52133131141c7b04a828af59f1352f85019a6a758c488be21e9f6089" -dependencies = [ - "futures", - "num_cpus", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tracing", - "uuid", -] - [[package]] name = "reqwest" version = "0.11.27" @@ -12512,9 +11398,6 @@ name = "ruzstd" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" -dependencies = [ - "twox-hash 2.1.2", -] [[package]] name = "ryu" @@ -13127,7 +12010,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" dependencies = [ - "bitmaps 2.1.0", + "bitmaps", "typenum", ] @@ -13155,23 +12038,6 @@ dependencies = [ "serde", ] -[[package]] -name = "smol" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" -dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", -] - [[package]] name = "snap" version = "1.1.1" @@ -14613,54 +13479,6 @@ dependencies = [ "tree-sitter-language", ] -[[package]] -name = "trueno" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0756605a19a0b79f5dca9b61fba7e428c497abe9ebeac2ef91b39d90b6da91" -dependencies = [ - "anyhow", - "bytemuck", - "futures-intrusive", - "num_cpus", - "pollster", - "thiserror 2.0.18", - "wgpu 27.0.1", -] - -[[package]] -name = "trueno" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b0f08c743a6d63e691f80624e67e306e83f9bc532ebc618b2352cd02126e7e" -dependencies = [ - "anyhow", - "chrono", - "hostname", - "num_cpus", - "serde", - "serde_json", - "thiserror 2.0.18", - "toml 0.8.23", -] - -[[package]] -name = "trueno" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e19fa22753d395f043b205999122520efd45a33e0867d137f20b777ad794ef" -dependencies = [ - "anyhow", - "chrono", - "hostname", - "num_cpus", - "serde", - "serde_json", - "thiserror 2.0.18", - "toml 0.8.23", - "trueno-quant", -] - [[package]] name = "trueno" version = "0.17.5" @@ -14686,39 +13504,6 @@ dependencies = [ "wgpu 27.0.1", ] -[[package]] -name = "trueno-db" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef9435a39b53dd71c59545ed2a2336d037481e3c2dd61eb8f3cdd6df4ac37cb" -dependencies = [ - "anyhow", - "arrow 54.3.1", - "axum 0.7.9", - "batuta-common", - "chrono", - "clap", - "console_error_panic_hook", - "dashmap", - "js-sys", - "parquet 54.3.1", - "rayon", - "rustc-hash 2.1.2", - "serde", - "serde-wasm-bindgen", - "serde_json", - "serde_yaml_ng", - "sqlparser", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-subscriber", - "trueno 0.17.5", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "trueno-gemm-codegen" version = "0.1.0" @@ -14730,22 +13515,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "trueno-graph" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb66018c97b3a2296df80bdaedcc8d47879e61cd43b466609e9d1a24ce4a0d3" -dependencies = [ - "anyhow", - "aprender 0.27.8", - "arrow 54.3.1", - "parquet 54.3.1", - "thiserror 2.0.18", - "tokio", - "trueno 0.17.5", - "trueno-db", -] - [[package]] name = "trueno-quant" version = "0.1.0" @@ -14755,68 +13524,6 @@ dependencies = [ "half", ] -[[package]] -name = "trueno-ublk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f73a7de38afcb76f90ed573edb5c8a1a8a0fc7052db1c8de8187513039e1c6c" -dependencies = [ - "anyhow", - "async-trait", - "clap", - "crossterm 0.28.1", - "ctrlc", - "duende-core", - "duende-mlock", - "duende-platform", - "duende-policy", - "duende-ublk", - "io-uring", - "libublk", - "nix 0.29.0", - "parking_lot", - "procfs", - "ratatui", - "rayon", - "renacer", - "rustc-hash 2.1.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tracing", - "tracing-subscriber", - "trueno-zram-core", -] - -[[package]] -name = "trueno-viz" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ffd53613bef43526c08f0a87d6058a10c276a599c7055caa985103475faf9" -dependencies = [ - "base64 0.22.1", - "batuta-common", - "crossterm 0.28.1", - "dirs 5.0.1", - "libc", - "png 0.17.16", - "ratatui", - "serde", - "serde_yaml_ng", - "thiserror 2.0.18", - "trueno 0.15.0", -] - -[[package]] -name = "trueno-zram-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75a0f63770d4b926254d02d2fc9a0abd286f333d9e2d19f18cf8b801daf235e" -dependencies = [ - "thiserror 2.0.18", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -14847,23 +13554,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "ttop" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7c5527eb2047094b6dd1d63ff325bfef64b82f64e6107a78f58c9307b1bb61" -dependencies = [ - "anyhow", - "batuta-common", - "clap", - "crossterm 0.28.1", - "presentar-core", - "presentar-terminal", - "serde", - "serde_yaml_ng", - "thiserror 2.0.18", -] - [[package]] name = "tungstenite" version = "0.24.0" @@ -14932,28 +13622,12 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if 1.0.4", - "static_assertions", -] - [[package]] name = "twox-hash" version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - [[package]] name = "typenum" version = "1.20.1" @@ -15044,17 +13718,6 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.11", -] - [[package]] name = "unicode-vo" version = "0.1.0" @@ -15309,7 +13972,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" dependencies = [ - "darling 0.20.11", + "darling", "once_cell", "proc-macro-error2", "proc-macro2", @@ -16423,18 +15086,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "which" version = "6.0.3" @@ -16484,7 +15135,7 @@ dependencies = [ "realizar", "symphonia", "thiserror 2.0.18", - "trueno 0.17.5", + "trueno", ] [[package]] @@ -17211,7 +15862,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", "syn 2.0.118", diff --git a/crates/aprender-train-bench/benches/sweep_benchmarks.rs b/crates/aprender-train-bench/benches/sweep_benchmarks.rs index 80d0f139ba..ed283e6ec6 100644 --- a/crates/aprender-train-bench/benches/sweep_benchmarks.rs +++ b/crates/aprender-train-bench/benches/sweep_benchmarks.rs @@ -1,43 +1,60 @@ //! Benchmarks for hyperparameter sweep execution. +//! +//! #2519: these used to time `Sweeper::run()` under +//! `.expect("sweep must succeed")`. That measured the cost of evaluating a +//! closed-form parabola, not of running a sweep -- and it would have hidden the +//! defect twice over, since a benchmark of fabricated work looks exactly like a +//! benchmark of fast work. `run()` now refuses (see `sweep.rs`), so what is +//! left to time is the honest arithmetic around it: enumerating the points a +//! sweep would visit, and formatting a result someone else measured. use criterion::{criterion_group, criterion_main, Criterion}; -use entrenar_bench::{SweepConfig, Sweeper}; +use entrenar_bench::sweep::{DataPoint, SweepConfig, SweepResult}; use std::hint::black_box; -fn bench_temperature_sweep(c: &mut Criterion) { - c.bench_function("temperature_sweep_5_points", |b| { - b.iter(|| { - let config = SweepConfig::temperature(1.0..5.0, 1.0).with_runs(1); - let sweeper = Sweeper::new(config); - black_box(sweeper.run().expect("sweep must succeed")) - }); +fn bench_sweep_point_enumeration(c: &mut Criterion) { + c.bench_function("temperature_values_15_points", |b| { + let config = SweepConfig::temperature(1.0..8.0, 0.5); + b.iter(|| black_box(config.parameter.values())); }); } -fn bench_alpha_sweep(c: &mut Criterion) { - c.bench_function("alpha_sweep_9_points", |b| { - b.iter(|| { - let config = SweepConfig::alpha(0.1..0.9, 0.1).with_runs(1); - let sweeper = Sweeper::new(config); - black_box(sweeper.run().expect("sweep must succeed")) - }); +fn bench_alpha_point_enumeration(c: &mut Criterion) { + c.bench_function("alpha_values_9_points", |b| { + let config = SweepConfig::alpha(0.1..0.9, 0.1); + b.iter(|| black_box(config.parameter.values())); }); } -fn bench_sweep_with_multiple_runs(c: &mut Criterion) { - c.bench_function("temperature_sweep_3_runs", |b| { - b.iter(|| { - let config = SweepConfig::temperature(1.0..5.0, 1.0).with_runs(3); - let sweeper = Sweeper::new(config); - black_box(sweeper.run().expect("sweep must succeed")) - }); +fn bench_result_table_formatting(c: &mut Criterion) { + // Values supplied here rather than invented by the crate under test. + let data_points: Vec = (0..15) + .map(|i| DataPoint { + parameter_value: 1.0 + f64::from(i) * 0.5, + mean_loss: 0.9 - f64::from(i) * 0.01, + std_loss: 0.003, + mean_accuracy: 0.75 + f64::from(i) * 0.004, + std_accuracy: 0.002, + runs: 3, + }) + .collect(); + let optimal = data_points.last().cloned(); + let result = SweepResult { + parameter_name: "temperature".to_string(), + data_points, + optimal, + config: SweepConfig::temperature(1.0..8.0, 0.5), + }; + + c.bench_function("sweep_result_to_table_15_rows", |b| { + b.iter(|| black_box(result.to_table())); }); } criterion_group!( benches, - bench_temperature_sweep, - bench_alpha_sweep, - bench_sweep_with_multiple_runs + bench_sweep_point_enumeration, + bench_alpha_point_enumeration, + bench_result_table_formatting ); criterion_main!(benches); diff --git a/crates/aprender-train-bench/src/cost.rs b/crates/aprender-train-bench/src/cost.rs index 317da072b4..daa69fc337 100644 --- a/crates/aprender-train-bench/src/cost.rs +++ b/crates/aprender-train-bench/src/cost.rs @@ -2,7 +2,9 @@ //! //! Provides Pareto frontier analysis for balancing training cost vs model performance. +use entrenar_common::{EntrenarError, Result}; use serde::{Deserialize, Serialize}; +use std::path::Path; /// A single configuration with cost and performance metrics #[derive(Debug, Clone, Serialize, Deserialize)] @@ -20,8 +22,13 @@ pub struct CostPerformancePoint { /// Memory usage in GB pub memory_gb: f64, /// Whether this point is on the Pareto frontier + /// + /// Defaulted on load: it is computed by [`CostPerformanceAnalysis::from_points`], + /// so a results file is not expected to supply it. + #[serde(default)] pub is_pareto_optimal: bool, /// Configuration parameters + #[serde(default)] pub config: ConfigParams, } @@ -403,8 +410,59 @@ fn truncate(s: &str, max_len: usize) -> String { } } +/// Load measured cost-performance points from a JSON file. +/// +/// The Pareto machinery above is genuine -- it only ever needed real input. +/// This is that input: an array of measured runs, e.g. +/// +/// ```json +/// [{"name":"LoRA r=32","gpu_hours":18.0,"cost_usd":39.78, +/// "accuracy":0.87,"loss":0.33,"memory_gb":24.0}] +/// ``` +/// +/// # Errors +/// +/// If the file cannot be read, does not parse as an array of +/// [`CostPerformancePoint`], or contains no points. +pub fn load_points(path: &Path) -> Result> { + let text = std::fs::read_to_string(path).map_err(|e| EntrenarError::Io { + context: format!("reading benchmark results: {}", path.display()), + source: e, + })?; + + let points: Vec = + serde_json::from_str(&text).map_err(|e| EntrenarError::Serialization { + message: format!( + "{}: expected a JSON array of cost-performance points: {e}", + path.display() + ), + })?; + + if points.is_empty() { + return Err(EntrenarError::ConfigValue { + field: "results".into(), + message: format!("{}: contains no data points", path.display()), + suggestion: "Provide at least one measured run".into(), + }); + } + + Ok(points) +} + /// Generate sample data points for testing/demo -pub fn generate_sample_points(cost_model: &CostModel) -> Vec { +// +// #2519: this eight-entry literal table was the production input to +// `cost-performance` and `recommend` -- `main.rs` called it under the comment +// "in a real scenario, load from results file" while IGNORING the `--results` +// flag it already accepted. Measured before this change, with no inputs: +// +// ✓ Top recommendation: LoRA r=32 (18.0 GPU-hours, $39.78, 87.0%) +// +// Every one of those numbers is written above. `load_points` is now the only +// way in, and this stays scoped to test builds so no production path can +// recommend a configuration nobody measured. +#[cfg(test)] +fn generate_sample_points(cost_model: &CostModel) -> Vec { // Sample configurations representing different trade-offs vec![ // Full fine-tuning (expensive, high accuracy) @@ -652,6 +710,52 @@ mod tests { assert!(!recommendations.is_empty()); } + #[test] + fn test_load_points_reads_measured_runs() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join("results.json"); + std::fs::write( + &path, + r#"[{"name":"A","gpu_hours":10.0,"cost_usd":22.1,"accuracy":0.8, + "loss":0.3,"memory_gb":16.0}, + {"name":"B","gpu_hours":20.0,"cost_usd":44.2,"accuracy":0.9, + "loss":0.2,"memory_gb":24.0}]"#, + ) + .expect("write results"); + + let points = load_points(&path).expect("results should load"); + assert_eq!(points.len(), 2); + // The values come from the file, not from a table in this crate. + assert_eq!(points[1].name, "B"); + assert!((points[1].accuracy - 0.9).abs() < 1e-9); + } + + #[test] + fn test_load_points_missing_file() { + let err = load_points(Path::new("/nonexistent/results.json")) + .expect_err("a missing results file must be an error"); + assert!(format!("{err}").contains("results.json")); + } + + #[test] + fn test_load_points_rejects_garbage() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join("results.json"); + std::fs::write(&path, "not json at all").expect("write results"); + + assert!(load_points(&path).is_err()); + } + + #[test] + fn test_load_points_rejects_empty_array() { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join("results.json"); + std::fs::write(&path, "[]").expect("write results"); + + let err = load_points(&path).expect_err("an empty results file must be an error"); + assert!(format!("{err}").contains("no data points")); + } + #[test] fn test_cost_models() { let a100 = CostModel::a100_80gb(); diff --git a/crates/aprender-train-bench/src/lib.rs b/crates/aprender-train-bench/src/lib.rs index 396b710e77..159e032800 100644 --- a/crates/aprender-train-bench/src/lib.rs +++ b/crates/aprender-train-bench/src/lib.rs @@ -28,6 +28,10 @@ pub use sweep::{SweepConfig, SweepResult, Sweeper}; use entrenar_common::Result; /// Run a temperature sweep. +/// +/// # Errors +/// +/// Always -- see [`Sweeper::run`] and #2519. Nothing in this crate trains. pub fn temperature_sweep( range: std::ops::Range, step: f32, @@ -38,6 +42,10 @@ pub fn temperature_sweep( } /// Compare multiple distillation strategies. +/// +/// # Errors +/// +/// Always -- see [`strategies::compare`] and #2519. pub fn compare_strategies(strategies: &[DistillStrategy]) -> Result { strategies::compare(strategies) } @@ -46,9 +54,18 @@ pub fn compare_strategies(strategies: &[DistillStrategy]) -> Result, }, } @@ -146,12 +150,14 @@ fn main() { min_accuracy, max_memory, gpu, + results, } => recommend_command( max_gpu_hours, max_cost, min_accuracy, max_memory, &gpu, + results.as_deref(), &config, ), }; @@ -392,9 +398,42 @@ fn ablation_command( Ok(()) } +/// Load the measured runs an analysis is about, or say why there are none. +/// +/// #2519: both callers used to do +/// +/// // Generate sample data points (in a real scenario, load from results file) +/// let points = generate_sample_points(&cost_model); +/// +/// while `cost-performance` accepted -- and ignored -- a `--results` flag, and +/// `recommend` had no way to supply results at all. The eight +/// "configurations" were a literal table in `cost.rs`, so `recommend` answered +/// `Top recommendation: LoRA r=32` from numbers nobody measured. The Pareto +/// analysis itself is genuine; it just never had real input. +fn require_results( + results_path: Option<&std::path::Path>, +) -> entrenar_common::Result> { + let Some(path) = results_path else { + return Err(entrenar_common::EntrenarError::ConfigValue { + field: "results".into(), + message: "no benchmark results to analyse: this crate does not run \ + training, so it has nothing of its own to report. It \ + previously substituted a hardcoded eight-configuration \ + table and recommended a winner from it" + .into(), + suggestion: "Pass --results : a JSON array of measured runs \ + with name, gpu_hours, cost_usd, accuracy, loss, memory_gb. \ + Tracked in #2519." + .into(), + }); + }; + + load_points(path) +} + fn cost_performance_command( gpu: &str, - _results_path: Option<&std::path::Path>, + results_path: Option<&std::path::Path>, cli: &entrenar_common::Cli, ) -> entrenar_common::Result<()> { // Parse GPU type @@ -408,8 +447,7 @@ fn cost_performance_command( ); } - // Generate sample data points (in a real scenario, load from results file) - let points = generate_sample_points(&cost_model); + let points = require_results(results_path)?; let analysis = CostPerformanceAnalysis::from_points(points); if cli.format == entrenar_common::OutputFormat::Json { @@ -570,6 +608,7 @@ fn recommend_command( min_accuracy: Option, max_memory: Option, gpu: &str, + results_path: Option<&std::path::Path>, cli: &entrenar_common::Cli, ) -> entrenar_common::Result<()> { let cost_model = parse_gpu_model(gpu)?; @@ -584,7 +623,7 @@ fn recommend_command( } let constraints = build_constraints(max_gpu_hours, max_cost, min_accuracy, max_memory); - let points = generate_sample_points(&cost_model); + let points = require_results(results_path)?; let analysis = CostPerformanceAnalysis::from_points(points); let recommendations = analysis.recommend(&constraints); diff --git a/crates/aprender-train-bench/src/strategies.rs b/crates/aprender-train-bench/src/strategies.rs index 2db39ff30d..c7d7a1e228 100644 --- a/crates/aprender-train-bench/src/strategies.rs +++ b/crates/aprender-train-bench/src/strategies.rs @@ -1,7 +1,6 @@ //! Distillation strategy comparison. -use crate::stats::StatisticalAnalyzer; -use entrenar_common::Result; +use entrenar_common::{EntrenarError, Result}; /// A distillation strategy to benchmark. #[derive(Debug, Clone)] @@ -77,6 +76,15 @@ impl DistillStrategy { } /// Simulate training with this strategy. + // + // #2519: retained ONLY for the unit tests that pin its per-variant literal + // table, so it stays on the record as a lookup rather than a run. Scoped to + // test builds so no production path can present it as a result again. Note + // what it ignores: every field of every variant. `KDOnly { alpha: 0.0 }` + // (no distillation at all) and `KDOnly { alpha: 0.7 }` get byte-identical + // metrics, which is why the `ablation` subcommand printed `Δ Loss +0.0000` + // for "+ KD (T=4)" over the CE-only baseline. + #[cfg(test)] fn simulate(&self, seed: u64) -> StrategyMetrics { let noise = (seed as f64 * 0.1).sin() * 0.02; @@ -157,93 +165,58 @@ pub struct PairwiseComparison { } /// Compare multiple strategies. +/// +/// # Errors +/// +/// If `strategies` is empty, and otherwise always: nothing here trains, so +/// there is no honest comparison to return -- see the #2519 note in the body. pub fn compare(strategies: &[DistillStrategy]) -> Result { - let runs_per_strategy = 5; - let mut results = Vec::new(); - let mut all_losses: Vec<(String, Vec)> = Vec::new(); - - for strategy in strategies { - let mut losses = Vec::new(); - let mut accuracies = Vec::new(); - let mut times = Vec::new(); - - for run in 0..runs_per_strategy { - let metrics = strategy.simulate(run as u64); - losses.push(metrics.final_loss); - accuracies.push(metrics.final_accuracy); - times.push(metrics.training_time_hours); - } - - let n = losses.len() as f64; - let mean_loss = losses.iter().sum::() / n; - let mean_accuracy = accuracies.iter().sum::() / n; - let mean_time = times.iter().sum::() / n; - - let std_loss = - (losses.iter().map(|x| (x - mean_loss).powi(2)).sum::() / (n - 1.0)).sqrt(); - let std_accuracy = (accuracies - .iter() - .map(|x| (x - mean_accuracy).powi(2)) - .sum::() - / (n - 1.0)) - .sqrt(); - - results.push(StrategyResult { - name: strategy.name().to_string(), - mean_loss, - std_loss, - mean_accuracy, - std_accuracy, - mean_time_hours: mean_time, - runs: runs_per_strategy, + // Kept: an empty strategy list is a genuine caller mistake with its own + // distinct diagnosis, and it is still worth naming separately from the + // refusal below. + if strategies.is_empty() { + return Err(EntrenarError::ConfigValue { + field: "strategies".into(), + message: "No strategies to compare".into(), + suggestion: "Pass at least one of: kd, progressive, attention, combined".into(), }); - - all_losses.push((strategy.name().to_string(), losses)); - } - - // Find best - let best_by_loss = results - .iter() - .min_by(|a, b| { - a.mean_loss - .partial_cmp(&b.mean_loss) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|r| r.name.clone()); - - let best_by_accuracy = results - .iter() - .max_by(|a, b| { - a.mean_accuracy - .partial_cmp(&b.mean_accuracy) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .map(|r| r.name.clone()); - - // Pairwise comparisons - let mut significance = Vec::new(); - for i in 0..all_losses.len() { - for j in (i + 1)..all_losses.len() { - let (name1, losses1) = &all_losses[i]; - let (name2, losses2) = &all_losses[j]; - - let test = StatisticalAnalyzer::welch_t_test(losses1, losses2); - - significance.push(PairwiseComparison { - strategy1: name1.clone(), - strategy2: name2.clone(), - p_value: test.p_value, - significant: test.significant, - effect_size: test.effect_size, - }); - } } - Ok(StrategyComparison { - results, - best_by_loss, - best_by_accuracy, - significance, + // #2519: this used to read + // + // for run in 0..runs_per_strategy { + // let metrics = strategy.simulate(run as u64); + // + // -- five "runs" of a per-variant LITERAL TABLE (`Combined -> 0.71/0.831`), + // plus a sinusoid of the run index standing in for run-to-run variance. It + // then fed those numbers to a real Welch t-test and printed p-values. + // + // Measured before this change, with no model and no data: + // + // Combined 0.714 ± 0.003 ★ 83.3% ± 0.2% ★ + // KD-only vs Combined: p=0.0000 ✓ (effect=35.69) + // ✓ Recommendation: Combined for best accuracy + // + // The p-value is the sharpest part of the defect: a correct statistical + // test applied to invented samples reports overwhelming significance, + // because the "variance" is a deterministic curve. The statistics were + // never wrong -- their input was fabricated, and the honest-looking + // machinery around it is what made the output persuasive. + // + // Refusing is strictly better than fabricating. Whether this binary should + // exist at all is tracked in #2519; this change does not prejudge it. + Err(EntrenarError::ConfigValue { + field: "strategies".into(), + message: format!( + "cannot compare {} distillation strategies: this crate never trains any \ + of them. It previously returned a per-variant literal table, ran a real \ + t-test over it and recommended a winner, which is why it is now an \ + error rather than a plausible-looking comparison", + strategies.len() + ), + suggestion: "Train each strategy for real (`apr distill`) and compare the \ + metrics those runs report. Tracked in #2519." + .into(), }) } @@ -300,6 +273,59 @@ impl StrategyComparison { mod tests { use super::*; + /// Build a `StrategyComparison` from values supplied by the caller, so the + /// formatter tests below exercise `to_table` without a comparison having to + /// invent the numbers it formats. + fn comparison_from(entries: &[(&str, f64, f64)]) -> StrategyComparison { + let results: Vec = entries + .iter() + .map(|&(name, mean_loss, mean_accuracy)| StrategyResult { + name: name.to_string(), + mean_loss, + std_loss: 0.0, + mean_accuracy, + std_accuracy: 0.0, + mean_time_hours: 2.0, + runs: 1, + }) + .collect(); + + let best_by_loss = results + .iter() + .min_by(|a, b| { + a.mean_loss + .partial_cmp(&b.mean_loss) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|r| r.name.clone()); + let best_by_accuracy = results + .iter() + .max_by(|a, b| { + a.mean_accuracy + .partial_cmp(&b.mean_accuracy) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .map(|r| r.name.clone()); + + let significance = results + .windows(2) + .map(|pair| PairwiseComparison { + strategy1: pair[0].name.clone(), + strategy2: pair[1].name.clone(), + p_value: 0.5, + significant: false, + effect_size: 0.0, + }) + .collect(); + + StrategyComparison { + results, + best_by_loss, + best_by_accuracy, + significance, + } + } + #[test] fn test_strategy_names() { assert_eq!(DistillStrategy::kd_only().name(), "KD-only"); @@ -308,36 +334,34 @@ mod tests { assert_eq!(DistillStrategy::combined().name(), "Combined"); } + // #2519: `test_compare_strategies` and `test_combined_is_best` used to + // assert `compare()` was Ok and that "Combined" won -- they asserted the + // literal table, so they would have gone RED on any honest fix. They now + // pin the refusal. #[test] - fn test_compare_strategies() { + fn test_compare_refuses_to_compare() { let strategies = vec![ DistillStrategy::kd_only(), DistillStrategy::progressive(), DistillStrategy::combined(), ]; - let comparison = compare(&strategies).expect("operation should succeed"); - - assert_eq!(comparison.results.len(), 3); - assert!(comparison.best_by_loss.is_some()); - assert!(comparison.best_by_accuracy.is_some()); + let err = compare(&strategies).expect_err("comparing untrained strategies must fail"); + assert!(format!("{err}").contains("never trains")); } #[test] - fn test_combined_is_best() { + fn test_compare_does_not_name_a_winner() { let strategies = vec![DistillStrategy::kd_only(), DistillStrategy::combined()]; - let comparison = compare(&strategies).expect("operation should succeed"); - - // Combined should generally be best - assert_eq!(comparison.best_by_accuracy.as_deref(), Some("Combined")); + // The old output ended in "Recommendation: Combined for best accuracy", + // derived from two hardcoded pairs of numbers. + assert!(compare(&strategies).is_err()); } #[test] fn test_comparison_table() { - let strategies = vec![DistillStrategy::kd_only(), DistillStrategy::progressive()]; - - let comparison = compare(&strategies).expect("operation should succeed"); + let comparison = comparison_from(&[("KD-only", 0.82, 0.782), ("Progressive", 0.75, 0.818)]); let table = comparison.to_table(); assert!(table.contains("KD-only")); @@ -468,16 +492,14 @@ mod tests { #[test] fn test_comparison_significance_markers() { - let strategies = vec![DistillStrategy::kd_only(), DistillStrategy::combined()]; - - let comparison = compare(&strategies).expect("operation should succeed"); + let comparison = comparison_from(&[("KD-only", 0.82, 0.782), ("Combined", 0.71, 0.831)]); // Should have one pairwise comparison assert_eq!(comparison.significance.len(), 1); } #[test] - fn test_compare_all_strategies() { + fn test_compare_all_strategies_still_refuses() { let strategies = vec![ DistillStrategy::kd_only(), DistillStrategy::progressive(), @@ -485,18 +507,42 @@ mod tests { DistillStrategy::combined(), ]; - let comparison = compare(&strategies).expect("operation should succeed"); + // Asking for more strategies does not make any of them run. + let err = compare(&strategies).expect_err("must refuse"); + assert!(format!("{err}").contains('4')); + } - // 4 choose 2 = 6 pairwise comparisons - assert_eq!(comparison.significance.len(), 6); - assert_eq!(comparison.results.len(), 4); + #[test] + fn test_compare_empty_fails_for_its_own_reason() { + // Non-vacuity: the refusal above is not a blanket "always Err" -- an + // empty list is still diagnosed as an empty list. + let err = compare(&[]).expect_err("an empty strategy list must fail"); + let text = format!("{err}"); + assert!(text.contains("No strategies to compare"), "got: {text}"); + assert!(!text.contains("never trains"), "got: {text}"); } #[test] - fn test_comparison_table_star_markers() { - let strategies = vec![DistillStrategy::kd_only(), DistillStrategy::combined()]; + fn test_simulate_ignores_every_strategy_field() { + // #2519's discriminating symptom: `simulate` matches only on the enum + // VARIANT, so a run with no distillation at all (alpha = 0.0, T = 1.0) + // is indistinguishable from one with alpha = 0.7, T = 4.0. That is why + // `ablation` printed "Δ Loss +0.0000" for adding KD. + let no_kd = DistillStrategy::KDOnly { + temperature: 1.0, + alpha: 0.0, + }; + let with_kd = DistillStrategy::KDOnly { + temperature: 4.0, + alpha: 0.7, + }; - let comparison = compare(&strategies).expect("operation should succeed"); + assert_eq!(no_kd.simulate(0).final_loss, with_kd.simulate(0).final_loss); + } + + #[test] + fn test_comparison_table_star_markers() { + let comparison = comparison_from(&[("KD-only", 0.82, 0.782), ("Combined", 0.71, 0.831)]); let table = comparison.to_table(); // Should have star marker for best diff --git a/crates/aprender-train-bench/src/sweep.rs b/crates/aprender-train-bench/src/sweep.rs index 4f558dda13..8800763a5f 100644 --- a/crates/aprender-train-bench/src/sweep.rs +++ b/crates/aprender-train-bench/src/sweep.rs @@ -1,6 +1,6 @@ //! Hyperparameter sweep executor (Kaizen principle). -use entrenar_common::Result; +use entrenar_common::{EntrenarError, Result}; /// Sweep configuration. #[derive(Debug, Clone)] @@ -117,55 +117,58 @@ impl Sweeper { } /// Run the sweep. + /// + /// # Errors + /// + /// Always. There is no training loop behind this type, so there is no + /// honest answer to return -- see the #2519 note in the body. pub fn run(&self) -> Result { - let values = self.config.parameter.values(); - let mut data_points = Vec::new(); - - for value in &values { - let mut metrics = Vec::new(); - - for run in 0..self.config.runs_per_point { - // Simulate training with this configuration - let result = self.simulate_training(*value, run); - metrics.push(result); - } - - // Aggregate metrics across runs - let mean_loss = metrics.iter().map(|m| m.loss).sum::() / metrics.len() as f64; - let mean_accuracy = - metrics.iter().map(|m| m.accuracy).sum::() / metrics.len() as f64; - let std_loss = self.calculate_std(&metrics.iter().map(|m| m.loss).collect::>()); - let std_accuracy = - self.calculate_std(&metrics.iter().map(|m| m.accuracy).collect::>()); - - data_points.push(DataPoint { - parameter_value: *value, - mean_loss, - std_loss, - mean_accuracy, - std_accuracy, - runs: metrics.len(), - }); - } - - // Find optimal - let optimal = data_points - .iter() - .min_by(|a, b| { - a.mean_loss - .partial_cmp(&b.mean_loss) - .unwrap_or(std::cmp::Ordering::Equal) - }) - .cloned(); - - Ok(SweepResult { - parameter_name: self.config.parameter.name().to_string(), - data_points, - optimal, - config: self.config.clone(), + // #2519: this used to read + // + // for run in 0..self.config.runs_per_point { + // // Simulate training with this configuration + // let result = self.simulate_training(*value, run); + // + // and then reported the aggregate as a sweep result, ★-marking the + // minimum as `Optimal`. `simulate_training` is a closed-form parabola + // whose vertex is the hardcoded constant its own comment names + // ("Temperature ~4.0 is optimal"); the sweep has no model, no data and + // no training loop, so the "measurement" never depended on anything. + // + // Measured before this change, with no arguments at all: + // + // Optimal: temperature = 4.00 (loss=0.6043, accuracy=80.7%) + // ... 3.50 -> 0.6543 4.50 -> 0.6543 (symmetric about the vertex) + // + // That is worse than a wrong number: it tells a user WHICH + // HYPERPARAMETER TO USE. Anyone tuning a real distillation on this + // output is being misled by arithmetic, not by an experiment. + // + // Refusing is strictly better than fabricating. Whether this binary + // should exist at all is a separate question, tracked in #2519; this + // change does not prejudge it, it only stops the tool from answering a + // question it never asked the hardware. + Err(EntrenarError::ConfigValue { + field: self.config.parameter.name().to_string(), + message: format!( + "cannot sweep `{}`: this crate has no training loop, no model and \ + no dataset. It previously returned a closed-form curve centred on \ + a baked-in constant and ★-marked its vertex as the best value, \ + which is why it is now an error rather than a plausible-looking \ + table", + self.config.parameter.name() + ), + suggestion: "Run real training (`apr finetune`, `apr distill`) once per \ + point and sweep over the metrics those runs report. \ + Tracked in #2519." + .into(), }) } + // #2519: retained ONLY for the unit tests that pin its arithmetic, so the + // closed form stays on the record as arithmetic. Scoped to test builds so + // no production path can present it as a measurement again. + #[cfg(test)] fn simulate_training(&self, param_value: f64, run: usize) -> TrainingMetrics { // Simulated training - in real implementation would run actual training // Using a simple model where: @@ -203,6 +206,12 @@ impl Sweeper { } } + // #2519: genuine arithmetic -- the standard deviation itself was never the + // problem, only the fabricated samples fed to it. `run()` was its sole + // production caller, so it is now referenced only by the unit tests that + // assert it; scoped to test builds to keep the crate warning-free without + // deleting a correct function. + #[cfg(test)] fn calculate_std(&self, values: &[f64]) -> f64 { if values.len() < 2 { return 0.0; @@ -304,6 +313,39 @@ impl SweepResult { mod tests { use super::*; + /// Build a `SweepResult` from values supplied by the caller, so the + /// formatter tests below exercise `to_table` without a sweep having to + /// invent the numbers it formats. + fn result_from(parameter_name: &str, points: &[(f64, f64, f64)]) -> SweepResult { + let data_points: Vec = points + .iter() + .map(|&(parameter_value, mean_loss, mean_accuracy)| DataPoint { + parameter_value, + mean_loss, + std_loss: 0.0, + mean_accuracy, + std_accuracy: 0.0, + runs: 1, + }) + .collect(); + + let optimal = data_points + .iter() + .min_by(|a, b| { + a.mean_loss + .partial_cmp(&b.mean_loss) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .cloned(); + + SweepResult { + parameter_name: parameter_name.to_string(), + data_points, + optimal, + config: SweepConfig::temperature(1.0..3.0, 1.0), + } + } + #[test] fn test_sweep_config_temperature() { let config = SweepConfig::temperature(1.0..5.0, 1.0); @@ -319,32 +361,40 @@ mod tests { assert_eq!(config.parameter.name(), "alpha"); } + // #2519: `test_sweeper_runs` and `test_sweeper_finds_optimal_temperature` + // used to assert `run()` was Ok and that its optimum sat near 4.0 -- i.e. + // they asserted the fabrication, and would have gone RED on any honest fix. + // They now pin the refusal, and the closed form is pinned separately below + // as arithmetic rather than as a result. #[test] - fn test_sweeper_runs() { + fn test_sweeper_refuses_to_sweep() { let config = SweepConfig::temperature(1.0..3.0, 1.0).with_runs(2); let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); - assert!(!result.data_points.is_empty()); - assert!(result.optimal.is_some()); + let err = sweeper + .run() + .expect_err("a sweep with no model and no data must not return results"); + assert!(format!("{err}").contains("no training loop")); } #[test] - fn test_sweeper_finds_optimal_temperature() { - let config = SweepConfig::temperature(2.0..6.0, 1.0).with_runs(1); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); + fn test_simulated_training_is_a_closed_form_not_a_measurement() { + // The vertex of the parabola is the hardcoded constant, and the curve is + // symmetric about it: equal deviations either side give the SAME loss. + // No experiment behaves like this, which is the whole #2519 finding. + let sweeper = Sweeper::new(SweepConfig::temperature(1.0..8.0, 0.5)); - // Optimal should be around 4.0 - let optimal = result.optimal.expect("operation should succeed"); - assert!((optimal.parameter_value - 4.0).abs() < 1.5); + let below = sweeper.simulate_training(3.5, 0); + let above = sweeper.simulate_training(4.5, 0); + assert_eq!(below.loss, above.loss); + + let vertex = sweeper.simulate_training(4.0, 0); + assert!(vertex.loss < below.loss); } #[test] fn test_sweep_result_table() { - let config = SweepConfig::temperature(1.0..3.0, 1.0); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); + let result = result_from("temperature", &[(1.0, 0.9, 0.74), (2.0, 0.8, 0.76)]); let table = result.to_table(); assert!(table.contains("temperature")); @@ -419,9 +469,7 @@ mod tests { #[test] fn test_sweep_result_fields() { - let config = SweepConfig::temperature(1.0..3.0, 1.0); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); + let result = result_from("temperature", &[(1.0, 0.9, 0.74)]); assert_eq!(result.parameter_name, "temperature"); assert!(!result.data_points.is_empty()); @@ -457,9 +505,7 @@ mod tests { #[test] fn test_sweep_result_table_optimal() { - let config = SweepConfig::temperature(3.0..5.0, 1.0); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); + let result = result_from("temperature", &[(3.0, 0.70, 0.787), (4.0, 0.60, 0.807)]); let table = result.to_table(); @@ -469,41 +515,31 @@ mod tests { } #[test] - fn test_sweep_deterministic() { + fn test_sweep_refusal_is_deterministic() { + // The refusal must not depend on the seed either: there is nothing to + // seed. Same error code, both times. let config = SweepConfig::temperature(1.0..3.0, 1.0).with_seed(42); - let sweeper = Sweeper::new(config.clone()); - let result1 = sweeper.run().expect("operation should succeed"); - - let sweeper2 = Sweeper::new(config); - let result2 = sweeper2.run().expect("operation should succeed"); + let err1 = Sweeper::new(config.clone()).run().expect_err("must refuse"); + let err2 = Sweeper::new(config).run().expect_err("must refuse"); - // Same seed should produce same results - assert_eq!( - result1.data_points[0].mean_loss, - result2.data_points[0].mean_loss - ); + assert_eq!(err1.code(), err2.code()); } #[test] - fn test_alpha_sweep_finds_optimal() { + fn test_alpha_sweep_also_refuses() { + // Both sweep parameters refuse -- the alpha curve was the same closed + // form with its vertex at the other hardcoded constant (0.7). let config = SweepConfig::alpha(0.3..0.9, 0.2).with_runs(1); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); + let err = Sweeper::new(config).run().expect_err("must refuse"); - // Optimal should be around 0.7 - let optimal = result.optimal.expect("operation should succeed"); - assert!((optimal.parameter_value - 0.7).abs() < 0.3); + assert!(format!("{err}").contains("alpha")); } #[test] - fn test_sweep_multiple_runs() { + fn test_sweep_refuses_regardless_of_runs_per_point() { + // Asking for more repeats of a computation that never ran does not make + // it a measurement. let config = SweepConfig::temperature(3.0..5.0, 1.0).with_runs(3); - let sweeper = Sweeper::new(config); - let result = sweeper.run().expect("operation should succeed"); - - // Each data point should have 3 runs - for point in &result.data_points { - assert_eq!(point.runs, 3); - } + assert!(Sweeper::new(config).run().is_err()); } } diff --git a/crates/aprender-train-bench/tests/falsify_no_fabricated_benchmarks_2519.rs b/crates/aprender-train-bench/tests/falsify_no_fabricated_benchmarks_2519.rs new file mode 100644 index 0000000000..aa3c46629a --- /dev/null +++ b/crates/aprender-train-bench/tests/falsify_no_fabricated_benchmarks_2519.rs @@ -0,0 +1,268 @@ +//! FALSIFY-BENCH-2519: nothing in this crate may report a measurement. +//! +//! `sweep.rs:169 simulate_training` was a closed-form parabola whose vertex is +//! the constant its own comment names: +//! +//! // Simulated training - in real implementation would run actual training +//! // - Temperature ~4.0 is optimal +//! let deviation = (param_value - 4.0).abs(); +//! let loss = 0.65 + deviation * 0.1 + noise; +//! +//! Measured before the fix, with no model, no data and no config: +//! +//! Optimal: temperature = 4.00 (loss=0.6043, accuracy=80.7%) +//! 3.50 -> 0.6543 4.50 -> 0.6543 (symmetric about the vertex) +//! +//! `strategies.rs:80 simulate` was the same defect as a per-variant literal +//! table (`Combined -> 0.71/0.831`), fed to a real Welch t-test that duly +//! reported `p=0.0000 ✓` and `Recommendation: Combined for best accuracy`. +//! `cost.rs generate_sample_points` was an eight-row literal table that +//! `recommend` turned into `Top recommendation: LoRA r=32`. +//! +//! This is worse than the `aprender-train-inspect` case it accompanies: those +//! outputs tell a user WHICH HYPERPARAMETER TO USE. +//! +//! Note on the API surface used below: these tests deliberately touch only +//! items that exist BOTH before and after the fix, so the file still compiles +//! against the pre-fix tree. A mutation check whose test target fails to +//! compile proves nothing, so the cost-analysis assertions go through the CLI +//! rather than through `cost::load_points`, which is new. + +use entrenar_bench::{ + compare_strategies, temperature_sweep, DistillStrategy, SweepConfig, Sweeper, +}; + +/// Write a two-run results file and return its path (with its tempdir, which +/// must stay alive for the duration of the test). +fn measured_results(body: &str) -> (tempfile::TempDir, std::path::PathBuf) { + let dir = tempfile::tempdir().expect("temp dir"); + let path = dir.path().join("measured.json"); + std::fs::write(&path, body).expect("write results"); + (dir, path) +} + +fn bench_cmd() -> assert_cmd::Command { + assert_cmd::Command::cargo_bin("aprender-train-bench").expect("binary should be built") +} + +#[test] +fn a_sweep_cannot_report_an_optimal_value_with_no_inputs() { + // The exact invocation from #2519: no model, no dataset, no config. + let result = temperature_sweep(1.0..8.0, 0.5, 3); + + let err = result.expect_err( + "a sweep with no model and no data returned Ok -- it is reporting a \ + closed-form curve as a measurement again, which is the #2519 defect", + ); + // The refusal must not smuggle the recommendation back in as advice: + // `Optimal: temperature = 4.00` is the exact line being retired. + let text = format!("{err}"); + assert!( + !text.contains("Optimal:") && !text.contains("4.0"), + "the refusal still names a recommended value. Got: {text}" + ); +} + +#[test] +fn a_strategy_comparison_cannot_name_a_winner_with_no_inputs() { + let strategies = [ + DistillStrategy::kd_only(), + DistillStrategy::progressive(), + DistillStrategy::attention(), + DistillStrategy::combined(), + ]; + + assert!( + compare_strategies(&strategies).is_err(), + "compare_strategies returned Ok without training anything. The old output \ + ended in `Recommendation: Combined for best accuracy`, derived from four \ + hardcoded pairs of numbers." + ); +} + +/// Discriminating test -- the analogue of "two different files of equal size +/// must not give identical answers". +/// +/// `1.0..2.5` and `5.5..7.0` are mirror images about the baked-in vertex 4.0, so +/// the closed form gives them the SAME losses in reverse order. Before the fix +/// both succeeded and did exactly that: proof the numbers came from arithmetic +/// on the parameter, not from anything that ran. Whatever this crate does, it +/// must not succeed here with mirrored results -- either it errors, or it +/// genuinely trained and the two ranges disagree. +#[test] +fn mirror_image_ranges_must_not_produce_the_same_curve_reversed() { + let low = temperature_sweep(1.0..2.5, 0.5, 1); + let high = temperature_sweep(5.5..7.0, 0.5, 1); + + if let (Ok(l), Ok(h)) = (&low, &high) { + let low_losses: Vec = l.data_points.iter().map(|p| p.mean_loss).collect(); + let high_losses: Vec = h.data_points.iter().rev().map(|p| p.mean_loss).collect(); + + assert_ne!( + low_losses, high_losses, + "temperatures 1.0-2.5 and 5.5-7.0 produced identical losses in mirror \ + order -- the answer is a function of |value - 4.0|, not of training" + ); + } +} + +/// Second discriminating angle: a range that contains no optimum at all still +/// got one. Before the fix, sweeping only the falling side of the parabola +/// still ★-marked its last point as `Optimal`, and the two sweeps below -- +/// which share no parameter value whatsoever -- both answered with confidence. +#[test] +fn disjoint_ranges_must_not_both_report_an_optimum() { + let a = temperature_sweep(1.0..2.0, 0.5, 1); + let b = temperature_sweep(6.0..7.0, 0.5, 1); + + let both_confident = matches!((&a, &b), (Ok(ra), Ok(rb)) + if ra.optimal.is_some() && rb.optimal.is_some()); + + assert!( + !both_confident, + "two disjoint temperature ranges, neither containing any measurement, \ + both reported an `Optimal` point" + ); +} + +#[test] +fn every_sweep_parameter_refuses_not_just_temperature() { + // The alpha curve was the same closed form with its vertex at the other + // hardcoded constant (0.7). Fixing only temperature would leave half the + // fabrication reachable. + let alpha = Sweeper::new(SweepConfig::alpha(0.1..0.9, 0.1).with_runs(3)).run(); + assert!(alpha.is_err(), "the alpha sweep still returns results"); +} + +/// Non-vacuity companion 1: the tests above are all satisfied by a crate that +/// refuses everything for one blanket reason. This pins that a genuine +/// computation on real input still SUCCEEDS -- the Pareto analysis was never +/// the problem, it just never had measured data to chew on. Both configuration +/// names below come from the file, and the `--max-cost` constraint really does +/// filter one of them out. +#[test] +fn analysis_of_real_measured_results_still_succeeds() { + let (_dir, path) = measured_results( + r#"[{"name":"cheap-run","gpu_hours":8.0,"cost_usd":17.68,"accuracy":0.81, + "loss":0.42,"memory_gb":18.0}, + {"name":"dear-run","gpu_hours":120.0,"cost_usd":265.2,"accuracy":0.92, + "loss":0.25,"memory_gb":56.0}]"#, + ); + + let output = bench_cmd() + .args(["recommend", "--max-cost", "50", "--results"]) + .arg(&path) + .output() + .expect("binary should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + output.status.success(), + "recommending from a real results file failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(stdout.contains("cheap-run"), "got:\n{stdout}"); + assert!( + !stdout.contains("dear-run"), + "the $50 constraint did not filter the $265 run:\n{stdout}" + ); +} + +/// Non-vacuity companion 2: the refusals are not one undifferentiated error. +/// An empty strategy list is still diagnosed as an empty strategy list, and a +/// results file that is missing still fails by naming the path. +#[test] +fn other_failures_keep_their_own_distinct_reasons() { + let empty = compare_strategies(&[]).expect_err("an empty strategy list must fail"); + let empty_text = format!("{empty}"); + assert!( + empty_text.contains("No strategies to compare"), + "got: {empty_text}" + ); + assert!(!empty_text.contains("never trains"), "got: {empty_text}"); + + let missing = bench_cmd() + .args([ + "cost-performance", + "--results", + "/nonexistent/measured.json", + ]) + .output() + .expect("binary should run"); + let stderr = String::from_utf8_lossy(&missing.stderr); + assert!(!missing.status.success()); + assert!(stderr.contains("measured.json"), "got:\n{stderr}"); +} + +/// The user-facing surface, since that is where the misleading table appeared. +/// `aprender-train-bench temperature` with no arguments exited 0 and printed +/// `Optimal: temperature = 4.00`. +#[test] +fn the_cli_no_longer_prints_a_recommended_hyperparameter() { + for subcommand in ["temperature", "alpha", "compare", "ablation"] { + let output = bench_cmd() + .arg(subcommand) + .output() + .expect("binary should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !output.status.success(), + "`{subcommand}` exited 0 with no model and no data:\n{stdout}" + ); + assert!( + !stdout.contains("Optimal:") && !stdout.contains("Recommendation:"), + "`{subcommand}` still recommends a configuration:\n{stdout}" + ); + } +} + +/// The cost commands took a `--results` flag and ignored it, substituting a +/// literal table. Refusing without measured input is the point; accepting it +/// when supplied is what keeps the refusal honest rather than a dead end. +#[test] +fn the_cli_requires_measured_results_before_recommending() { + for subcommand in ["recommend", "cost-performance"] { + let bare = bench_cmd() + .arg(subcommand) + .output() + .expect("binary should run"); + + let bare_stdout = String::from_utf8_lossy(&bare.stdout); + assert!( + !bare.status.success(), + "`{subcommand}` exited 0 with no measured results" + ); + assert!( + !bare_stdout.contains("Top recommendation") && !bare_stdout.contains("LoRA r="), + "`{subcommand}` still answers from the hardcoded table:\n{bare_stdout}" + ); + } + + let (_dir, path) = measured_results( + r#"[{"name":"only-run","gpu_hours":8.0,"cost_usd":17.68,"accuracy":0.81, + "loss":0.42,"memory_gb":18.0}]"#, + ); + + let supplied = bench_cmd() + .args(["recommend", "--results"]) + .arg(&path) + .output() + .expect("binary should run"); + + let supplied_stdout = String::from_utf8_lossy(&supplied.stdout); + assert!( + supplied.status.success(), + "`recommend --results` failed on a valid results file:\n{}", + String::from_utf8_lossy(&supplied.stderr) + ); + assert!( + supplied_stdout.contains("only-run"), + "`recommend --results` did not report the run from the file:\n{supplied_stdout}" + ); + // The old literal table must not resurface alongside the real one. + assert!( + !supplied_stdout.contains("LoRA r=32"), + "the hardcoded configuration table is still being mixed in:\n{supplied_stdout}" + ); +} diff --git a/crates/aprender-train-inspect/src/inspect.rs b/crates/aprender-train-inspect/src/inspect.rs index 490d5dc6e3..b9d1a99e15 100644 --- a/crates/aprender-train-inspect/src/inspect.rs +++ b/crates/aprender-train-inspect/src/inspect.rs @@ -1,6 +1,6 @@ //! Model inspection utilities. -use crate::architecture::{ArchitectureDetector, ArchitectureInfo}; +use crate::architecture::ArchitectureInfo; use entrenar_common::{EntrenarError, Result}; use std::collections::HashMap; use std::path::Path; @@ -105,35 +105,52 @@ pub fn inspect_model(path: impl AsRef) -> Result { }); } - let metadata = std::fs::metadata(path).map_err(|e| EntrenarError::Io { + // Kept although the size is no longer used for anything: it still surfaces + // a permission or I/O error against the real path, which is a genuine check. + // Reporting the SIZE was never the problem; inferring the model's + // architecture from it was. + let _metadata = std::fs::metadata(path).map_err(|e| EntrenarError::Io { context: format!("reading model metadata: {}", path.display()), source: e, })?; let format = detect_format(path); - // For real implementation, would parse the actual file - // Here we return simulated data based on file size - let estimated_params = estimate_params_from_size(metadata.len(), &format); - - let tensors = generate_mock_tensors(estimated_params); - let tensor_names: Vec = tensors.iter().map(|t| t.name.clone()).collect(); - - let shapes: HashMap> = tensors - .iter() - .map(|t| (t.name.clone(), t.shape.clone())) - .collect(); - - let detector = ArchitectureDetector::new().with_tensors(tensor_names); - let architecture = detector.detect_from_shapes(&shapes); - - Ok(ModelInfo { - path: path.to_path_buf(), - size_bytes: metadata.len(), - format, - architecture, - total_params: estimated_params, - tensors, + // #2519: this used to read + // + // // For real implementation, would parse the actual file + // // Here we return simulated data based on file size + // let estimated_params = estimate_params_from_size(metadata.len(), &format); + // let tensors = generate_mock_tensors(estimated_params); + // + // -- it INVENTED the tensor list from the file's SIZE and then ran + // architecture detection over the invented shapes. Measured: 5 KB of + // /dev/urandom named `.safetensors` exited 0 and reported + // + // Architecture llama | Hidden Dimension 768 | Layers 1 + // Vocab Size 256 | Tensors 9 + // + // A real one-tensor safetensors file got the SAME nine tensors, because the + // answer never depended on the file's contents. This crate is published to + // crates.io, so that output reached users as if it were an inspection. + // + // Worth noting what this defeated: `architecture.rs` carries an N-05 + // hardening that derives hidden-dim from tensors rather than hardcoding + // 4096. It does derive honestly -- from tensors that were fabricated one + // call earlier. The hardening was applied one layer above the lie. + // + // Refusing is strictly better than fabricating. Whether this binary should + // exist at all is a separate question, tracked in #2519; this change does + // not prejudge it, it only stops the tool from answering questions it + // cannot answer. + Err(EntrenarError::UnsupportedFormat { + format: format!( + "{format:?}: `inspect` cannot parse model files. It previously \ + synthesised a tensor list from the file SIZE and reported that as \ + the model's architecture, which is why it is now an error rather \ + than a plausible-looking answer. Use `apr inspect` or `apr tensors`, \ + which read the file. Tracked in #2519." + ), }) } @@ -149,6 +166,9 @@ fn detect_format(path: &Path) -> ModelFormat { } } +// #2519: retained ONLY for the unit tests that assert its arithmetic. Scoped +// to test builds so no production path can synthesise model facts again. +#[cfg(test)] fn estimate_params_from_size(size_bytes: u64, format: &ModelFormat) -> u64 { let bytes_per_param = match format { ModelFormat::SafeTensors | ModelFormat::PyTorch => 2, // Assume FP16 @@ -160,6 +180,9 @@ fn estimate_params_from_size(size_bytes: u64, format: &ModelFormat) -> u64 { size_bytes / bytes_per_param as u64 } +// #2519: retained ONLY for the unit tests that assert its arithmetic. Scoped +// to test builds so no production path can synthesise model facts again. +#[cfg(test)] fn generate_mock_tensors(total_params: u64) -> Vec { // Generate representative tensor structure let hidden_dim = if total_params > 10_000_000_000 { diff --git a/crates/aprender-train-inspect/tests/falsify_no_fabricated_metadata_2519.rs b/crates/aprender-train-inspect/tests/falsify_no_fabricated_metadata_2519.rs new file mode 100644 index 0000000000..36a11ac94a --- /dev/null +++ b/crates/aprender-train-inspect/tests/falsify_no_fabricated_metadata_2519.rs @@ -0,0 +1,97 @@ +//! FALSIFY-INSPECT-2519: `inspect_model` must never synthesise model facts. +//! +//! It used to build its tensor list from the file's SIZE and then run +//! architecture detection over the invented shapes: +//! +//! // For real implementation, would parse the actual file +//! // Here we return simulated data based on file size +//! let estimated_params = estimate_params_from_size(metadata.len(), &format); +//! let tensors = generate_mock_tensors(estimated_params); +//! +//! Measured before the fix: 5 KB of /dev/urandom named `.safetensors` exited 0 +//! and reported `Architecture llama | Hidden Dimension 768 | Layers 1 | +//! Vocab 256 | Tensors 9`. A real one-tensor safetensors file got the SAME nine +//! tensors, because the answer never depended on the contents. This crate is +//! published to crates.io, so that reached users as an "inspection". +//! +//! These tests are black box: they only need a path and an exit condition. + +use std::io::Write; + +fn write_bytes(name: &str, bytes: &[u8]) -> std::path::PathBuf { + // Per-process unique so concurrent test binaries cannot collide. + let dir = std::env::temp_dir().join(format!("apr-inspect-2519-{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let p = dir.join(name); + let mut f = std::fs::File::create(&p).expect("create fixture"); + f.write_all(bytes).expect("write fixture"); + p +} + +#[test] +fn garbage_bytes_are_not_reported_as_a_model() { + // Deliberately NOT a valid safetensors file. The extension is the only + // thing suggesting it is one -- which is exactly what the old code keyed on. + let junk: Vec = (0..5120u32).map(|i| (i % 251) as u8).collect(); + let path = write_bytes("garbage.safetensors", &junk); + + let result = entrenar_inspect::inspect::inspect_model(&path); + + assert!( + result.is_err(), + "inspect_model returned Ok for 5 KB of non-model bytes. It is \ + fabricating model facts again -- that is the #2519 defect." + ); +} + +#[test] +fn two_different_files_do_not_get_the_same_invented_answer() { + // The sharpest form of the old bug: the answer depended on SIZE, not + // contents, so two unrelated files of similar size got identical + // "architectures". Whatever inspect_model does, it must not succeed here + // with equal results -- either it errors, or it genuinely read the files. + let a = write_bytes("a.safetensors", &vec![0xAAu8; 5120]); + let b = write_bytes("b.safetensors", &vec![0x55u8; 5120]); + + let ra = entrenar_inspect::inspect::inspect_model(&a); + let rb = entrenar_inspect::inspect::inspect_model(&b); + + if let (Ok(ia), Ok(ib)) = (&ra, &rb) { + assert_ne!( + ( + ia.architecture.hidden_dim, + ia.architecture.num_layers, + ia.tensors.len() + ), + ( + ib.architecture.hidden_dim, + ib.architecture.num_layers, + ib.tensors.len() + ), + "two different files of equal size produced identical architecture \ + and tensor count -- the answer is derived from SIZE, not contents" + ); + } +} + +/// Non-vacuity companion. Both tests above are satisfied by a function that +/// errors unconditionally, including for reasons unrelated to fabrication. This +/// pins that a MISSING file still fails for its own distinct reason, so the +/// tests above are not merely observing a function that refuses everything for +/// one blanket cause. +#[test] +fn a_missing_file_fails_for_its_own_reason() { + let missing = std::env::temp_dir() + .join(format!("apr-inspect-2519-{}", std::process::id())) + .join("does-not-exist.safetensors"); + + let err = entrenar_inspect::inspect::inspect_model(&missing) + .expect_err("a missing path must be an error"); + let text = format!("{err}"); + + assert!( + text.contains("does-not-exist") || text.to_lowercase().contains("not found"), + "a missing file should fail by NAMING the path, not with the \ + cannot-parse message. Got: {text}" + ); +} diff --git a/crates/aprender-train-shell/src/commands.rs b/crates/aprender-train-shell/src/commands.rs index 763c84be17..1412c62704 100644 --- a/crates/aprender-train-shell/src/commands.rs +++ b/crates/aprender-train-shell/src/commands.rs @@ -1,6 +1,6 @@ //! Command parsing and execution for the REPL. -use crate::state::{HistoryEntry, LoadedModel, ModelRole, SessionState}; +use crate::state::{HistoryEntry, ModelRole, SessionState}; use entrenar_common::{EntrenarError, Result}; /// A parsed command. @@ -182,7 +182,10 @@ pub fn execute(cmd: &Command, state: &mut SessionState) -> Result { let start = std::time::Instant::now(); let result = match cmd { - Command::Fetch { model_id, role } => execute_fetch(model_id, *role, state), + // #2519: `role` is no longer read -- nothing can be loaded, so nothing + // can be assigned a teacher/student role. It stays in the parsed command + // because `parse` still validates the flags. + Command::Fetch { model_id, .. } => execute_fetch(model_id), Command::Inspect { target } => execute_inspect(target, state), Command::Memory { batch_size, @@ -228,35 +231,47 @@ pub fn execute(cmd: &Command, state: &mut SessionState) -> Result { result } -fn execute_fetch(model_id: &str, role: ModelRole, state: &mut SessionState) -> Result { - // Simulate model fetching - let model = LoadedModel { - id: model_id.to_string(), - path: std::path::PathBuf::from(format!("/tmp/models/{}", model_id.replace('/', "_"))), - architecture: detect_architecture(model_id), - parameters: estimate_params(model_id), - layers: estimate_layers(model_id), - hidden_dim: 4096, - role, - }; - - let name = if role == ModelRole::Teacher { - "teacher" - } else if role == ModelRole::Student { - "student" - } else { - model_id.split('/').next_back().unwrap_or(model_id) - }; - - state.add_model(name.to_string(), model.clone()); - - Ok(format!( - "✓ Fetched {}\n Architecture: {}\n Parameters: {:.1}B\n Layers: {}", - model_id, - model.architecture, - model.parameters as f64 / 1e9, - model.layers - )) +fn execute_fetch(model_id: &str) -> Result { + // #2519: this used to open with + // + // // Simulate model fetching + // let model = LoadedModel { + // architecture: detect_architecture(model_id), + // parameters: estimate_params(model_id), + // layers: estimate_layers(model_id), + // hidden_dim: 4096, + // + // and returned "✓ Fetched {model_id}". Two separate things were wrong. + // + // First, nothing was fetched: this crate has no HTTP client and no + // HuggingFace dependency, so no bytes ever moved. Measured before this + // change, on a model ID that cannot exist: + // + // ✓ Fetched does-not-exist/totally-fake-7b + // Architecture: unknown + // Parameters: 7.0B + // Layers: 32 + // + // Second, those figures are read out of the model ID STRING: "7b" in the + // name yields 7.0B and 32 layers, and hidden_dim was the literal 4096. The + // architecture line is the one part that behaved -- it warns and reports + // `unknown` -- which is why it is the only guess kept anywhere near honest. + // + // Refusing is strictly better than fabricating. Whether this binary should + // exist at all is tracked in #2519; this change does not prejudge it. + Err(EntrenarError::ConfigValue { + field: "fetch".into(), + message: format!( + "cannot fetch `{model_id}`: this shell has no HuggingFace client, so it \ + downloads nothing. It previously reported success for any string at all, \ + with a parameter count and layer count string-matched out of the model \ + ID itself" + ), + suggestion: "Download with `apr pull ` or `apr import hf://`, \ + then read the real file with `apr inspect` / `apr tensors`. \ + Tracked in #2519." + .into(), + }) } fn execute_inspect(target: &InspectTarget, state: &SessionState) -> Result { @@ -447,6 +462,12 @@ fn execute_help(topic: Option<&str>) -> Result { /// for inference uses tensor-name-based `ArchitectureDetector::detect()`. /// Order matters: more specific patterns must come before generic ones /// (e.g., "mistral" before "llama" since Mistral inherits LLaMA naming). +// +// #2519: `execute_fetch` was the only production caller of the three guessers +// below, so they are now referenced only by the tests that pin their behaviour. +// Scoped to test builds so no production path can present a substring match on +// a model ID as a fact about a model. +#[cfg(test)] const ARCH_PATTERNS: &[(&[&str], &str)] = &[ (&["qwen"], "qwen"), (&["phi"], "phi"), @@ -457,6 +478,7 @@ const ARCH_PATTERNS: &[(&[&str], &str)] = &[ (&["gpt"], "gpt"), ]; +#[cfg(test)] fn detect_architecture(model_id: &str) -> String { let lower = model_id.to_lowercase(); for (patterns, arch) in ARCH_PATTERNS { @@ -471,6 +493,7 @@ fn detect_architecture(model_id: &str) -> String { "unknown".to_string() } +#[cfg(test)] fn estimate_params(model_id: &str) -> u64 { let lower = model_id.to_lowercase(); if lower.contains("70b") { @@ -488,6 +511,7 @@ fn estimate_params(model_id: &str) -> u64 { } } +#[cfg(test)] fn estimate_layers(model_id: &str) -> u32 { let lower = model_id.to_lowercase(); if lower.contains("70b") { @@ -506,6 +530,8 @@ fn estimate_layers(model_id: &str) -> u32 { #[cfg(test)] mod tests { use super::*; + // #2519: only the tests construct models now that nothing is fetched. + use crate::state::LoadedModel; #[test] fn test_parse_fetch() { @@ -576,13 +602,24 @@ mod tests { )); } + // #2519: this used to assert `is_ok()` and that a "teacher" appeared in the + // session -- for a model nothing had downloaded. A test of that shape locks + // the fabrication in: it passes only because the output is invented. #[test] - fn test_execute_fetch() { + fn test_execute_fetch_refuses_and_loads_nothing() { let mut state = SessionState::new(); - let result = execute_fetch("meta-llama/Llama-2-7b", ModelRole::Teacher, &mut state); - - assert!(result.is_ok()); - assert!(state.get_model("teacher").is_some()); + let err = execute( + &Command::Fetch { + model_id: "meta-llama/Llama-2-7b".to_string(), + role: ModelRole::Teacher, + }, + &mut state, + ) + .expect_err("fetch must not claim to have downloaded a model"); + + assert!(format!("{err}").contains("no HuggingFace client")); + assert!(state.get_model("teacher").is_none()); + assert!(state.loaded_models().is_empty()); } #[test] diff --git a/crates/aprender-train-shell/tests/falsify_no_fabricated_fetch_2519.rs b/crates/aprender-train-shell/tests/falsify_no_fabricated_fetch_2519.rs new file mode 100644 index 0000000000..a1845dce44 --- /dev/null +++ b/crates/aprender-train-shell/tests/falsify_no_fabricated_fetch_2519.rs @@ -0,0 +1,255 @@ +//! FALSIFY-SHELL-2519: `fetch` must not claim to have downloaded a model. +//! +//! `commands.rs:232` opened with "Simulate model fetching" and built a +//! `LoadedModel` out of the model ID string: +//! +//! architecture: detect_architecture(model_id), +//! parameters: estimate_params(model_id), // "7b" in the NAME -> 7.0B +//! layers: estimate_layers(model_id), // "7b" in the NAME -> 32 +//! hidden_dim: 4096, // literal +//! +//! Measured before the fix, on an ID that cannot exist: +//! +//! Warning: could not detect architecture from model ID '...', defaulting +//! to 'unknown' +//! ✓ Fetched does-not-exist/totally-fake-7b +//! Architecture: unknown +//! Parameters: 7.0B +//! Layers: 32 +//! +//! This crate is the least egregious of the three in #2519: the architecture +//! line does warn, and does say `unknown`. Exactly two things are wrong, and +//! only those two are asserted here -- (a) `✓ Fetched` for something that was +//! never fetched, and (b) a parameter/layer count read out of the ID string. + +use entrenar_shell::commands::{execute, parse, Command}; +use entrenar_shell::state::ModelRole; +use entrenar_shell::SessionState; + +fn run(line: &str, state: &mut SessionState) -> entrenar_common::Result { + let cmd = parse(line).expect("these lines all parse"); + execute(&cmd, state) +} + +#[test] +fn a_model_that_cannot_exist_is_not_reported_as_fetched() { + let mut state = SessionState::new(); + + let result = run("fetch does-not-exist/totally-fake-7b", &mut state); + + let err = result.expect_err( + "fetch returned Ok for a model ID that cannot exist -- it is claiming a \ + download that never happened, which is the #2519 defect", + ); + let text = format!("{err}"); + assert!(!text.contains("Fetched"), "got: {text}"); + // (b): the figures were string-matched out of the ID, so the refusal must + // not restate them either. + assert!(!text.contains("7.0B"), "got: {text}"); + assert!(!text.contains("Layers"), "got: {text}"); + + assert!( + state.loaded_models().is_empty(), + "a model that was never downloaded was added to the session anyway" + ); +} + +/// Discriminating test: the answer came from the ID STRING, so an ID that lies +/// about its size was believed. `tiny/model-70b` and a genuine 70B checkpoint +/// got the same 70.0B / 80 layers, because nothing was ever read from a file. +/// Whatever `fetch` does, it must not report a size for a name. +#[test] +fn size_is_not_read_out_of_the_model_name() { + let mut state = SessionState::new(); + + for (id, claimed) in [ + ("tiny/model-70b", "70.0B"), + ("tiny/model-13b", "13.0B"), + ("tiny/model-7b", "7.0B"), + ] { + let output = + run(&format!("fetch {id}"), &mut state).unwrap_or_else(|e| format!("refused: {e}")); + + assert!( + !output.contains(claimed), + "`fetch {id}` still reports {claimed}, which is the substring of the \ + NAME and not a property of any file: {output}" + ); + } + + assert!(state.loaded_models().is_empty()); +} + +/// Two IDs differing only in the digits of their name must not be the sole +/// reason two different answers are given -- the equal-size-different-files +/// analogue. Either both are refused, or the shell actually read two files and +/// can say which bytes it read. +#[test] +fn two_names_differing_only_in_digits_get_no_confident_answer() { + let mut state = SessionState::new(); + + let seven = run("fetch fake/model-7b", &mut state); + let thirteen = run("fetch fake/model-13b", &mut state); + + if let (Ok(a), Ok(b)) = (&seven, &thirteen) { + assert!( + !a.contains("Parameters"), + "fetch reported a parameter count derived from the name: {a}" + ); + assert_ne!( + a, b, + "two nonexistent models produced identical descriptions: {a}" + ); + } +} + +/// Non-vacuity companion 1: `fetch` with no argument still fails at PARSE time +/// for its own distinct reason, so the refusal above is not a blanket "every +/// fetch errors for one cause". +#[test] +fn fetch_without_an_id_fails_for_its_own_reason() { + let err = parse("fetch").expect_err("`fetch` with no model ID must not parse"); + let text = format!("{err}"); + + assert!(text.contains("No model ID provided"), "got: {text}"); + assert!(!text.contains("HuggingFace client"), "got: {text}"); +} + +/// Non-vacuity companion 2: the shell still works. Commands that do their own +/// honest arithmetic or bookkeeping must still return Ok -- otherwise the tests +/// above would be observing a REPL that refuses everything. +#[test] +fn commands_that_do_real_work_still_succeed() { + let mut state = SessionState::new(); + + let set = run("set batch_size 64", &mut state).expect("set must still work"); + assert!(set.contains("64")); + + // Arithmetic on values the user supplied, not on invented model facts. + let memory = run("memory --batch 8 --seq 512", &mut state).expect("memory must still work"); + assert!(memory.contains("batch=8")); + assert!(memory.contains("seq=512")); + + let help = run("help fetch", &mut state).expect("help must still work"); + assert!(help.contains("fetch")); + + // And a genuinely unknown command is still diagnosed as one. + let unknown = execute( + &Command::Unknown { + input: "frobnicate".to_string(), + }, + &mut state, + ); + assert!(unknown.is_err()); +} + +/// `distill` depended on fetched models. With nothing loadable it must say so +/// rather than report progress -- it used to end at "Training started... +/// (simulated)", which is only reachable once two models are in the session. +#[test] +fn distill_cannot_start_training_on_models_that_were_never_fetched() { + let mut state = SessionState::new(); + + assert!(run("fetch a/teacher-7b --teacher", &mut state).is_err()); + assert!(run("fetch b/student-1b --student", &mut state).is_err()); + + let err = run("distill", &mut state).expect_err("distill must not start on nothing"); + assert!(format!("{err}").contains("teacher")); +} + +/// The single-command CLI surface (`-c`) is the non-interactive form of the +/// reproduction in #2519, and it must exit non-zero. +#[test] +fn the_single_command_surface_exits_non_zero() { + // Resolved at RUNTIME by asking cargo, not with + // env!("CARGO_BIN_EXE_aprender-train-shell"). That macro is evaluated at + // COMPILE time and failed the build in CI -- + // + // error: environment variable `CARGO_BIN_EXE_aprender-train-shell` + // not defined at compile time + // + // while compiling fine locally under the identical + // `cargo test -p aprender-train-shell --test ` command. Rather than + // keep guessing at the difference, this uses the pattern already proven for + // aprender-mcp: ask cargo which executable it produced. Same doctrine as + // scripts/apr_bin.sh -- never construct or assume a binary path. + let exe = cargo_built_binary(); + let output = std::process::Command::new(&exe) + .args(["-c", "fetch does-not-exist/totally-fake-7b"]) + .output() + .expect("binary should run"); + + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !output.status.success(), + "`-c 'fetch '` exited 0:\n{stdout}" + ); + assert!( + !stdout.contains("Fetched"), + "still claims a fetch:\n{stdout}" + ); + assert!(!stdout.contains("7.0B"), "still reports a size:\n{stdout}"); +} + +/// Roles are still parsed even though nothing can be loaded into them -- the +/// parse-level behaviour was never the defect, and quietly dropping it would be +/// a second regression hiding behind the first fix. +#[test] +fn role_flags_are_still_parsed() { + assert!(matches!( + parse("fetch some/model --teacher").expect("parses"), + Command::Fetch { + role: ModelRole::Teacher, + .. + } + )); + assert!(matches!( + parse("fetch some/model --student").expect("parses"), + Command::Fetch { + role: ModelRole::Student, + .. + } + )); +} + +/// Ask cargo for this package's binary, and fail loudly if it cannot say. +/// +/// A test that silently skipped when the binary was unavailable would be the +/// skip-class escape this repo bans -- and would have hidden the very defect +/// #2519 is about. +fn cargo_built_binary() -> std::path::PathBuf { + let out = std::process::Command::new(env!("CARGO")) + .args([ + "build", + "-p", + "aprender-train-shell", + "--bin", + "aprender-train-shell", + "--message-format=json-render-diagnostics", + ]) + .output() + .expect("cargo build must run"); + assert!( + out.status.success(), + "cargo build failed:\n{}", + String::from_utf8_lossy(&out.stderr) + ); + let mut found: Option = None; + for line in String::from_utf8_lossy(&out.stdout).lines() { + // Deliberately a substring match rather than a JSON dependency: this + // test crate must not grow one for a path lookup. + if !line.contains("\"compiler-artifact\"") { + continue; + } + if let Some(i) = line.find("\"executable\":\"") { + let rest = &line[i + 14..]; + if let Some(j) = rest.find('"') { + let p = std::path::PathBuf::from(&rest[..j]); + if p.file_name().is_some_and(|n| n == "aprender-train-shell") { + found = Some(p); + } + } + } + } + found.expect("cargo reported no executable for aprender-train-shell") +}